// constructAsset.js
(function () {
/* global adventurejs A */
var p = adventurejs.Game.prototype;
/**
* Takes a generic object and returns a classed object.
* @method adventurejs.Game#constructAsset
* @memberOf adventurejs.Game
* @param {Object} source A generic object containing properties to copy to a game object.
* @returns {Object} Returns an instance of whatever class was defined.
*/
p.constructAsset = function Game_constructAsset(source) {
var id, dest, tempdest;
/**
* Most classes convert name to ID,
* with the exception of Exits
* which use location + direction.
*/
if (source.id) {
// use provided ID
id = A.normalize(source.id);
} else if (source.class === "Exit") {
// exit ID = room_direction
id = A.normalize(
source.place[Object.keys(source.place)[0]] + " " + source.direction
);
} else if ("string" === typeof source.name) {
// normalize provided name
id = A.normalize(source.name);
} else {
// if nothing was provided, use class_uid
id = source.class.toLowerCase + A.UID.get();
}
if (!this.class_lookup[source.class]) this.class_lookup[source.class] = [];
this.class_lookup[source.class].push(id);
/**
* if author has set source.place
* ensure that the name of its parent object is serialized
*/
if (
"object" === typeof source.place &&
Object.keys(source.place).length > 0
) {
source.place[Object.keys(source.place)[0]] = A.normalize(
source.place[Object.keys(source.place)[0]]
);
}
// instantiate new class to temp so we can test it
tempdest = new adventurejs[source.class](
source.name,
this.game_name,
"",
id
);
// We've instantiated a temp instance so we can ask this question.
const is_platonic = source.is?.platonic || source.platonic;
if (!is_platonic) {
// If it's not platonic, we're just going to keep
// the temp instance we created
dest = tempdest;
} else {
// If it's platonic, we're going to instantiate a new Platonic
// instance, copy some of the temp asset's values
// to the Platonic instance, then delete the temp instance.
// We're doing this to get at the class's vocabulary for world_lookup.
const clas = source.class.toLowerCase();
dest = new adventurejs.Platonic(
tempdest.name,
tempdest.game_name,
tempdest.context_id,
tempdest.id
).set({
class: "Platonic",
platonic: true,
dispensary: tempdest.dispensary,
singular: tempdest.singular,
plural: tempdest.plural,
keywords: tempdest.keywords,
singularPluralPairs: tempdest.singularPluralPairs,
});
// save a reference to game.platonic_classes
this.game.platonic_classes[source.class].platonic = dest.id;
// We are also saving data about the platonic item to
// game.potential_lookup
this.game.potential_lookup[clas] = {
type: "platonic",
class: source.class,
name: source.name,
singular: tempdest.singular,
plural: tempdest.plural,
singularPluralPairs: tempdest.singularPluralPairs,
};
// Save a copy using the plural form
// unsure as of yet whether it's needed
if (tempdest.plural) {
this.game.potential_lookup[tempdest.plural] =
this.game.potential_lookup[clas];
}
}
/**
* Give it a reference back to game.
* We can't give game objects a direct reference because
* circular references cause problems when writing saves out to JSON.
* Instead we save the instance name that is scoped to window,
* and get it with object.game property.
*
*/
dest.game_name = this.game_name;
/**
* We allow author to define singular & plural as strings, but we
* actually store those as a nested array in singularPluralPairs,
* eg [ ["key","keys"] ];
*
* We don't expect authors to define singularPluralPairs themselves,
* but we do allow for it, for cases of advanced users.
*/
if ("undefined" === typeof source.singularPluralPairs) {
source.singularPluralPairs = [];
}
// strip spaces
if ("string" === typeof source.singular)
source.singular = source.singular.trim();
if ("string" === typeof source.plural) source.plural = source.plural.trim();
if (
"string" === typeof source.singular &&
"string" === typeof source.plural
) {
dest.singularPluralPairs.push([source.singular, source.plural]);
}
/**
* HANDLE ASPECTS
* Look for uninitialized Aspects in game file.
* Tangible Assets have Aspects,
* which are containers for in/on/under etc.
* Some predefined Tangible subclasses, like Table or Bathtub, may have
* preset Aspects for things like 'on' or 'in'. But, in case author has
* defined properties for an Aspect that hasn't been defined for this class
* of asset, we'll be nice and instantiate an Aspect for them.
* Also, if we encounter an aspect that is not in our dictionary's list of
* prepositions, add the preposition to the dictionary.
*/
if (source.aspects && dest.aspects) {
// did we receive an array?
if (Array.isArray(source.aspects)) {
// authors are invited to use arrays as a shorthand to create aspects
// aspects: [ 'on', 'under', 'behind' ],
for (var i = 0; i < source.aspects.length; i++) {
var aspect = source.aspects[i];
if (dest.aspects[aspect]) {
dest.aspects[aspect].enabled = true;
} else {
dest.aspects[aspect] = new adventurejs.Aspect(
aspect,
this.game_name,
id
).set({ enabled: true });
}
}
delete source.aspects;
} else {
for (var aspect in source.aspects) {
// Has dest got an Aspect instance here?
if (!dest.aspects[aspect]?.class) {
dest.aspects[aspect] = new adventurejs.Aspect(
aspect,
this.game_name,
id
).set({ enabled: true });
}
if (source.aspects[aspect].vessel) {
if ("boolean" === typeof source.aspects[aspect].vessel) {
// we accept aspects:{on:{vessel:true}} as a shortcut
if (dest.aspects[aspect].vessel?.class) {
// if existing vessel, set enabled
dest.aspects[aspect].enabled = source.aspects[aspect].vessel;
} else if (source.aspects[aspect].vessel) {
// only if true, create new vessel
dest.aspects[aspect].vessel = new adventurejs.Vessel(
aspect,
this.game_name,
dest.id
).set({ enabled: true });
}
// else if false and no vessel, do nothing
// since source vessel was boolean, delete so it doesn't get cloned
delete source.aspects[aspect].vessel;
} // vessel is not boolean
else {
if (!dest.aspects[aspect].vessel?.class) {
dest.aspects[aspect].vessel = new adventurejs.Vessel(
aspect,
this.game_name,
dest.id
).set({ enabled: true });
}
}
}
if (source.aspects[aspect].nest) {
if ("boolean" === typeof source.aspects[aspect].nest) {
// we accept aspects:{on:{nest:true}} as a shortcut
if (dest.aspects[aspect].nest?.class) {
// if existing nest, set enabled
dest.aspects[aspect].enabled = source.aspects[aspect].nest;
} else if (source.aspects[aspect].nest) {
// only if true, create new nest
dest.aspects[aspect].nest = new adventurejs.Nest(
aspect,
this.game_name,
dest.id
).set({ enabled: true });
}
// else if false and no nest, do nothing
// since source nest was boolean, delete so it doesn't get cloned
delete source.aspects[aspect].nest;
} // nest is not boolean
else {
if (!dest.aspects[aspect].nest?.class) {
dest.aspects[aspect].nest = new adventurejs.Nest(
aspect,
this.game_name,
dest.id
).set({ enabled: true });
}
}
}
// if aspect is an unknown preposition, add it to dictionary
if (-1 === this.game.dictionary.prepositions.indexOf(aspect)) {
this.game.dictionary.prepositions.push(aspect);
}
// we allow authors to set aspects to true/false as a shortcut
// which we write to aspect.enabled - if we got one of those
// we want to set it & delete it so it doesn't get cloned
if (
true === source.aspects[aspect] ||
false === source.aspects[aspect]
) {
dest.aspects[aspect].enabled = source.aspects[aspect];
delete source.aspects[aspect];
}
} // for
}
}
/**
* HANDLE VERB SUBSCRIPTIONS
* Each asset subscribes to verbs to allow them to act upon it.
* VerbSubscriptions are pre-defined for classes, but if author
* has added new VerbSubscriptions or modifiers to pre-defined
* ones, we want to merge them in.
*/
var object = ["dov", "iov"];
for (var of = 0; of < object.length; of++) {
// if (id === "global_hole") console.warn("global_hole", object);
if (source && source[object[of]]) {
// if (id === "global_hole")
// console.warn("global_hole", source[object[of]]);
// is source[ object[of] ] an array?
if (Array.isArray(source[object[of]])) {
dest.setObjectOfVerbs(object[of], source[object[of]]);
} else {
for (var verb in source[object[of]]) {
dest.setVerbSubscription(object[of], {
[verb]: source[object[of]][verb],
});
}
}
// we've copied everything we need from source and
// we don't want to recopy in the clone block, so...
delete source[object[of]];
}
}
/**
* HANDLE DESCRIPTIONS
* Descriptions can be strings or objects, and some default
* descriptions on some of our classes are strings, where
* the author may use objects. If so, ensure that we overwrite
* description.string with description.object
*/
if (source["descriptions"]) {
dest["descriptions"] = A.clone.call(this.game, source["descriptions"]);
delete source["descriptions"];
}
// ALL OTHER PROPERTIES
var keys = Object.keys(source);
for (var i = 0; i < keys.length; i++) {
var prop = keys[i];
// ignore native prototype properties
if (!Object.prototype.hasOwnProperty.call(source, prop)) continue;
// if("undefined" !== typeof source[ prop ].class
// && "undefined" !== typeof dest[ prop ]
// && "undefined" === typeof dest[ prop ].class)
// {
// dest[ prop ] = new adventurejs[ source[ prop ].class ]( source[ prop ].id, this.game_name );
// }
// convert strings to arrays as needed
if ("string" === typeof source[prop] && Array.isArray(dest[prop])) {
source[prop] = source[prop].toLowerCase().split(",");
}
if (Array.isArray(source[prop])) {
// trim spaces
for (var p = 0; p < source[prop].length; p++) {
if ("string" === typeof source[prop][p]) {
source[prop][p] = source[prop][p].trim();
}
}
/*
* Some classes may have predefined words for some properties,
* and we don't want to overwrite those.
* If author has added more words we want
* to concat rather than replace.
*
* Example: Say we have class SteelSword with adjectives=["steel"]
* and author creates an instance and adds adjectives=["sharp"]
* We need to ensure that both "steel sword" and "sharp sword" will work.
*/
source[prop] = source[prop].concat(dest[prop]);
}
}
/**
* Clone remaining properties from the generic source
* to the dest using the Set() method of Atom,
* which is the superclass for all game objects.
*/
if (source.class) delete source.class; // we already got class
if (source.id) delete source.id; // we already got id
dest.set(source);
// if( source.class === "Exit" ) {
// var directionSynonyms = this.game.dictionary.direction_lookup[ source.direction ];
// for( var i = 0; i < directionSynonyms.length; i++ )
// {
// dest.adjectives.push( directionSynonyms[i] );
// }
// }
return dest;
};
})();