// Character.js
(function () {
/* global adventurejs A */
/**
* @ajspath adventurejs.Atom.Asset.Matter.Tangible.Character
* @augments adventurejs.Tangible
* @class adventurejs.Character
* @ajsconstruct MyGame.createAsset({ "class":"Character", "name":"foo", [...] })
* @ajsconstructedby adventurejs.Game#createAsset
* @ajsnavheading CharacterClasses
* @param {String} game_name The name of the top level game object.
* @param {String} name A name for the object, to be serialized and used as ID.
* @summary Base class for all character types.
* @tutorial Characters_NPCs
* @tutorial GetStarted_CreateAPlayer
* @ajstangiblecontainer in
* @todo more methods, better example
* @classdesc
* <p>
* <strong>Character</strong> is anything that moves or speaks
* or takes or gives. {@link adventurejs.Player|Player} and
* {@link adventurejs.NPC|NPCs} are both Characters.
* Character can sense things, hold things, be asked things,
* be told things...
* </p>
* <h3 class="examples">Example:</h3>
* <pre class="display"><code class="language-javascript">MyGame.createAsset({
* "class": "Character",
* "name": "Yurtle",
* "synonyms": ["turtle"],
* "place": {in: "Pond"},
* })
* </code></pre>
*/
class Character extends adventurejs.Tangible {
constructor(name, game_name) {
super(name, game_name);
this.class = "Character";
this.is = new adventurejs.Character_Is("is", this.game_name, this.id);
this.can = new adventurejs.Character_Can("can", this.game_name, this.id);
this.setDOVs(["feed"]);
this.setIOVs(["throw", "take", "give", "hold", "tie"]);
this.posture = "stand";
this.aspects.in = new adventurejs.Aspect("in", this.game_name, this.id);
this.default_aspect = "in";
this.dont_use_articles = true;
this.name_is_proper = true;
/**
* We can track the user's y position. This option sets the maximum distance that the
* player can reach vertically, ie when climbing an object like a tree. 1 is considered
* to be about the height of a person.
* @var {Boolean} adventurejs.Character#reach_height
* @default 1
*/
this.reach_height = 1;
/**
* We can track the user's x/z position. This option sets the maximum distance that the
* player can reach horizontally within a room. 0.33 is considered to be about arm's length.
* @var {Boolean} adventurejs.Character#reach_length
* @default .33
*/
this.reach_length = 0.33;
/**
* We can track the user's x/z position. This option sets the default distance that the
* player travels horizontally when moving within a room. 1 is considered to be about
* the height of a person.
* @var {Boolean} adventurejs.Character#stride_length
* @default .33
*/
this.stride_length = 0.33;
/**
* We can track the user's y position. This option sets the maximum distance that the
* player can travel vertically. 1 is considered to be about
* the height of a person.
* @var {Boolean} adventurejs.Character#jump_height
* @default 1
*/
this.jump_height = 1;
/**
* We can track the user's x/z position. This option sets the maximum distance that the
* player travels horizontally when moving within a room. 1 is considered to be about
* the height of a person.
* @var {Boolean} adventurejs.Character#jump_length
* @default 1
*/
this.jump_length = 1;
/**
* Generic message to return when an NPC is given
* a command that they can't act on. Can be left blank
* to use a default message.
* @var {String} adventurejs.Character#ignore_msg
* @default ""
*/
this.ignore_msg = "";
/**
* Object to keep track of assets known by the character.
* @var {String} adventurejs.Character#knows_about
* @default {}
*/
this.knows_about = {}; // ok
/**
* Object to keep track of assets seen by the character.
* @var {String} adventurejs.Character#has_seen
* @default {}
*/
this.has_seen = {}; // ok
/**
* Object to keep track of room assets visited by the character.
* @var {String} adventurejs.Character#has_been
* @default {}
*/
this.has_been = {}; // ok
/**
* Object to keep track of room assets visited by the character.
* @var {String} adventurejs.Character#nest
* @default {}
*/
this.nest = {};
}
/**
* Get / set nest. Nest is related to but differs from place.
* Non-character classes can be placed in any other asset.
* Character classes can only be placed in rooms, and nest
* in other assets. This is so they can be, for example,
* nested on a bicycle while in a room.
* The private var _nest is an
* object with two properties: aspect and asset.
* For example: { aspect:"in", asset:"room" }
* However, the public var place appears in the form
* { in: "room" }. This is to make it easier and more
* intuitive for authors to set asset nests.
* @var {Object} adventurejs.Character#nest
*/
get nest() {
return this._nest;
}
set nest(nest) {
if (Object(nest) !== nest) {
let msg = `${this.id}.nest received a malformed value `;
this.game.log("L1428", "error", "critical", msg, "Tangible");
nest = {};
} else {
var keys = Object.keys(nest);
if (keys.length > 1) {
let msg = `${this.id}.nest received more than one location. Using the first. `;
for (let i = 0; i < keys.length; i++) {
msg += keys[i] + ", ";
}
nest = { [keys[0]]: nest[keys[0]] };
this.game.log("L1429", "error", "critical", msg, "Character");
}
if (keys.length === 1) {
// serialize asset name
nest[keys[0]] = A.serialize(nest[keys[0]]);
// verify asset
if ("string" !== typeof nest[keys[0]]) {
const msg = `${this.id}.nest set to an invalid asset `;
this.game.log("L1430", "error", "critical", msg, "Tangible");
}
}
if (keys.length === 0) {
}
}
this._nest = nest;
}
/**
* Character knowledge is stored such that it is written
* with saved games. This utility allows an author to back up
* a character's knowledge in case they should want
* to temporarily delete it, such as giving a character
* temporary amnesia, and then later restore it.
* @memberof adventurejs.Character
* @method adventurejs.Character#saveKnowledge
*/
saveKnowledge() {
this.game.world._vars[`${this.id}_knows_about`] = this.knows_about;
this.game.world._vars[`${this.id}_has_seen`] = this.has_seen;
}
/**
* Character knowledge is stored such that it is written
* with saved games. Should an author want
* to temporarily delete it, such as by giving a character
* temporary amnesia, this allows them to later restore it.
* @memberof adventurejs.Character
* @method adventurejs.Character#restoreKnowledge
*/
restoreKnowledge() {
if (this.game.world._vars[`${this.id}_knows_about`]) {
this.knows_about = this.game.world._vars[`${this.id}_knows_about`];
delete this.game.world._vars[`${this.id}_knows_about`];
}
if (this.game.world._vars[`${this.id}_has_seen`]) {
this.has_seen = this.game.world._vars[`${this.id}_has_seen`];
delete this.game.world._vars[`${this.id}_has_seen`];
}
}
/**
* Clear all of a character's knowledge.
* @memberof adventurejs.Character
* @method adventurejs.Character#clearKnowledge
*/
clearKnowledge() {
this.knows_about = {};
this.has_seen = {};
}
/**
* Set whether character knows about an asset.
* @memberof adventurejs.Character
* @method adventurejs.Character#knowAsset
* @param {*} asset
* @param {Boolean} recurse If true, will make content of aspects known.
*/
knowAsset(asset, recurse = true) {
if ("string" === typeof asset) {
asset = this.game.getAsset(asset);
}
if (!asset || !asset.id) return;
this.knows_about[asset.id] = true;
if (asset.linked_asset) {
let linked_asset = this.game.getAsset(asset.linked_asset);
if (linked_asset && linked_asset.id) {
this.knows_about[linked_asset.id] = true;
}
}
if (recurse) {
for (let aspect in asset.aspects) {
if (asset.aspects[aspect].know_contents_with_parent) {
this.knowAspect(asset, asset.aspects[aspect], recurse);
}
}
}
}
/**
* Set whether character knows about an aspect of an asset
* and all of its contents.
* @memberof adventurejs.Character
* @method adventurejs.Character#knowAspect
* @param {*} asset
* @param {*} aspect
* @param {Boolean} recurse If true, will make content of aspects known.
*/
knowAspect(asset, aspect, recurse = true) {
if ("string" === typeof asset) {
asset = this.game.getAsset(asset);
}
if (!asset || !asset.id) return;
if ("string" === typeof aspect) {
aspect = asset.aspects[aspect];
}
if (!aspect) return;
if (aspect.context_id !== asset.id) return;
if (
aspect.name === "in" &&
asset.isDOV("open") &&
asset.is.closed &&
asset.appearance.opacity >= 1
) {
return;
}
if (aspect.vessel) {
this.knows_about[aspect.vessel.id] = true;
}
var contents = aspect.contents;
for (let i = 0; i < contents.length; i++) {
let nested_asset = this.game.getAsset(contents[i]);
this.knowAsset(nested_asset, recurse);
}
}
/**
* Ask whether character knows about an asset.
* @memberof adventurejs.Character
* @method adventurejs.Character#knowsAbout
* @param {*} object
* @returns {Boolean}
*/
knowsAbout(object) {
let target, asset, parts, vessel;
if (object.id) asset = object;
// just to complicate things, asset can also be a vessel
if ("string" === typeof object) {
if (object.includes("|")) {
// it's a nested object
parts = object.split("|");
// error if it has more than 3 parts
if (parts.length !== 3) return false;
// error if middle piece is something other than "in"
if (parts[1] !== "in") return false;
// error if last piece is something other than vessel
if (parts[2] !== "vessel") return false;
asset = this.game.getAsset(parts[0]);
vessel = asset.getVessel();
} else asset = this.game.getAsset(object);
}
target = vessel ? vessel : asset;
return true === this.knows_about[target.id] || target.is?.known
? true
: false;
}
/**
* Set whether character has seen an asset.
* @memberof adventurejs.Character
* @method adventurejs.Character#see
* @param {*} asset
*/
seeAsset(asset, recurse = true) {
if ("string" === typeof asset) {
asset = this.game.getAsset(asset);
}
if (!asset || !asset.id) return;
this.has_seen[asset.id] = true;
if (asset.linked_asset) {
let linked_asset = this.game.getAsset(asset.linked_asset);
if (linked_asset && linked_asset.id) {
this.has_seen[linked_asset.id] = true;
}
}
if (recurse) {
for (let aspect in asset.aspects) {
if (aspect.see_contents_with_parent) {
this.seeAspect(asset, aspect, recurse);
}
}
}
}
/**
* Set whether character has seen an aspect of an asset
* and all of its contents.
* @memberof adventurejs.Character
* @method adventurejs.Character#seeAspect
* @param {*} asset
* @param {*} aspect
* @param {Boolean} recurse If true, will make content of aspects seen.
*/
seeAspect(asset, aspect, recurse = true) {
if ("string" === typeof asset) {
asset = this.game.getAsset(asset);
}
if (!asset || !asset.id) return;
if ("string" === typeof aspect) {
aspect = asset.aspects[aspect];
}
if (!aspect) return;
if (aspect.context_id !== asset.id) return;
if (
aspect.name === "in" &&
asset.is.closed &&
asset.appearance.opacity >= 1
) {
return;
}
if (aspect.vessel?.see_contents_with_parent) {
this.has_seen[aspect.vessel.id] = true;
}
if (aspect.see_contents_with_parent) {
var contents = aspect.contents;
for (let i = 0; i < contents.length; i++) {
let nested_asset = this.game.getAsset(contents[i]);
this.seeAsset(nested_asset, recurse);
}
}
}
/**
* Ask whether character has seen an asset.
* @memberof adventurejs.Character
* @method adventurejs.Character#hasSeen
* @param {*} object
* @returns {Boolean}
*/
hasSeen(object) {
let target, asset, parts, vessel;
if (object.id) asset = object;
// just to complicate things, asset can also be a vessel
if ("string" === typeof object) {
if (object.includes("|")) {
// it's a nested object
parts = object.split("|");
// error if it has more than 3 parts
if (parts.length !== 3) return false;
// error if middle piece is something other than vessel
if (parts[1] !== "vessel") return false;
// error if third piece is something other than "in"
if (parts[2] !== "in") return false;
asset = this.game.getAsset(parts[0]);
vessel = asset.getVessel();
} else asset = this.game.getAsset(object);
}
target = vessel ? vessel : asset;
return true === this.has_seen[target.id] || target.is?.seen
? true
: false;
}
/**
* Set whether character has been in a room asset.
* @memberof adventurejs.Character
* @method adventurejs.Character#been
* @param {*} room
*/
visitRoom(room) {
if ("string" === typeof room) {
room = this.game.getAsset(room);
}
if (!room || !room.id) return;
this.knowAsset(room, true);
this.seeAsset(room, true);
this.has_been[room.id] = true;
// player recognizes exits
// @TODO is this handled by knowRecursively?
var keys = Object.keys(room.exits);
for (var i = 0; i < keys.length; i++) {
var exitID = room.exits[keys[i]];
var exit = room.game.world[exitID];
if (exit.is.hidden || this.is.blind) {
/* || TODO dark / no visibility */
} else {
this.knowAsset(exit);
this.seeAsset(exit);
}
}
}
/**
* Ask whether character has been to a room.
* @memberof adventurejs.Character
* @method adventurejs.Character#hasVisitedRoom
* @param {*} room
* @returns {Boolean}
*/
hasVisitedRoom(room) {
if ("string" === typeof room) {
room = this.game.getAsset(room);
}
if (!room) return false;
return true === this.has_been[room.id] ? true : false;
}
/**
* Handle asset initialization for Character.
* @memberof adventurejs.Character
* @method adventurejs.Character#initialize
* @param {Object} game
* @returns {Boolean}
*/
initialize(game) {
super.initialize(game);
let room = this.getRoomAsset();
if (room) {
this.knowAsset(room, true);
this.seeAsset(room, true);
this.has_been[room.id] = true;
}
let place = this.getPlaceAsset();
if (place && !place.hasClass("Room")) {
// set nest to place and move player to room
this.nest = { [this._place.aspect]: this._place.asset };
this.place = { in: room.id };
}
let nest = this.getNestAsset();
if (nest && nest.hasClass("Room")) {
this.place = { in: nest.id };
this.nest = {};
}
return true;
} // p.initialize
/**
* Ask whether character is current player.
* @memberof adventurejs.Character
* @method adventurejs.Character#isPlayer
* @returns {Boolean}
*/
isPlayer() {
return this.id === this.game.getPlayer().id;
}
}
adventurejs.Character = Character;
})();