Pre-release
Adventure.js Docs Downloads
Score: 0 Moves: 0
// constructAsset.js

(function () {
  /*global adventurejs A*/
  "use strict";

  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;
    var dest;

    /**
     * Most classes convert name to ID,
     * with the exception of Exits
     * which use location + direction.
     */
    if (source.class === "Exit") {
      id = A.serialize(
        source.place[Object.keys(source.place)[0]] + " " + source.direction
      );
    } else if (source.name) {
      id = A.serialize(source.name);
    }

    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.serialize(
        source.place[Object.keys(source.place)[0]]
      );
    }

    // create new class object with serialized name
    dest = new adventurejs[source.class](id, this.game_name);

    /**
     * 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 noun & plural as strings, but we
     * actually store those as a nested array in singlePluralPairs,
     * eg [ ["key","keys"] ];
     *
     * We don't expect authors to define singlePluralPairs themselves,
     * but we do allow for it, for cases of advanced users.
     */
    if ("undefined" === typeof source.singlePluralPairs) {
      source.singlePluralPairs = [];
    }

    // strip spaces
    if ("string" === typeof source.noun) source.noun = source.noun.trim();
    if ("string" === typeof source.plural) source.plural = source.plural.trim();

    // TODO singlePluralPairs setup was being duplicated here and in Asset.js
    // are we losing anything if we shut down this one?
    // make new pair
    // if( "string" === typeof source.noun
    //   && "string" === typeof source.plural
    //   && "" !== source.noun
    //   && "" !== source.plural
    // ) {
    //   source.singlePluralPairs.push( [ source.noun, source.plural ] );
    // }
    // the base class may already have noun/plural pairs so append those
    //source.singlePluralPairs = source.singlePluralPairs.concat( dest.singlePluralPairs );

    /**
     * HANDLE ASPECTS
     * Look for uninitialized Aspects in game file.
     * Tangible Assets have Aspects, aka 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
            ).set({ parent_id: id, 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
            ).set({ parent_id: id, 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
                ).set({ parent_id: id, 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
                ).set({ parent_id: id, 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 (source && 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]];
      }
    }

    // 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.
     */
    dest.set(source);

    // if( source.class === "Exit" ) {
    //   var directionSynonyms = this.game.dictionary.directionLookup[ source.direction ];
    //   for( var i = 0; i < directionSynonyms.length; i++ )
    // {
    //     dest.adjectives.push( directionSynonyms[i] );
    //   }
    // }

    return dest;
  };
})();