Pre-release
Adventure.js Docs Downloads
Score: 0 Moves: 0
// SubstanceEmitter.js
(function () {
  /*global adventurejs A*/
  "use strict";

  /**
   * @ajspath adventurejs.Atom.Asset.Matter.Tangible.Thing.SubstanceEmitter
   * @augments adventurejs.Thing
   * @class adventurejs.SubstanceEmitter
   * @ajsconstruct MyGame.createAsset({ "class":"SubstanceEmitter", "name":"foo", [...] })
   * @ajsconstructedby adventurejs.Game#createAsset
   * @ajsnavheading BaseClasses
   * @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 Generates a specified substance, in specified quantity per turn.
   * @tutorial Substances_Emitters
   * @ajssubstancecontainer in
   * @ajstangiblecontainer in
   * @classdesc
   * <p>
   * <strong>SubstanceEmitter</strong> is a subclass of
   * {@link adventurejs.Thing|Thing},
   * and is a special class that emits a specified
   * {@link adventurejs.Substance|Substance},
   * like water from a
   * {@link adventurejs.Faucet|Faucet},
   * or dirt from a mining chute.
   * SubstanceEmitter has a
   * {@link adventurejs.Aspect|Aspect},
   * which in turn has a
   * {@link adventurejs.Vessel|Vessel}.
   * which has its
   * <code class="property"><a href="#is_emitter">is_emitter</a></code>
   * property set to true.
   * In other words,
   * <code class="property">Tangible.Aspect.Vessel.is_emitter = true</code>,
   * or as a practical example:
   * <code class="property">faucet.aspects.in.vessel.is_emitter = true</code>.
   * </p>
   * <p>
   * A SubstanceEmitter can be linked with a
   * {@link adventurejs.GraduatedController|GraduatedController}
   * such as a {@link adventurejs.Handle|Handle}
   * to control its rate of flow.
   * See the {@link adventurejs.Sink|Sink} page for an example
   * that includes a Sink with linked Faucet and Handle.
   * </p>
   * <h3 class="examples">Example:</h3>
   * <pre class="display"><code class="language-javascript">MyGame.createAsset({
   *   class: "SubstanceEmitter",
   *   name: "waterfall",
   *   descriptions: { look: "A thin waterfall sputters out of the cave wall. ", },
   *   substance_id: "water",
   *   max_volume_of_flow_per_turn: 10000, // in ml
   *   place: { attached: "cave wall" },
   * });
   * </code></pre>
   * <p>
   * To learn more, see
   * <a href="/doc/Substances_AboutSubstances.html">Substances</a>.
   * </p>
   * @todo add linked GraduatedController example
   **/
  class SubstanceEmitter extends adventurejs.Thing {
    constructor(name, game_name) {
      super(name, game_name);
      this.class = "SubstanceEmitter";

      this.aspects.in = new adventurejs.Aspect("in", this.game_name).set({
        parent_id: this.id,
      });

      this.aspects.in.vessel = new adventurejs.Vessel("in", game_name).set({
        parent_id: this.id,
        is_emitter: true,
        max_volume_of_flow_per_turn: 0,
      });

      // duplicate prop to substance, but justified
      this.rate_of_flow = 0; // 0 to 1 // setting this sets substance rate_of_flow

      // duplicate prop to substance, questionable
      //this.is_emitting = false;
      //this.substance_id = ""; //??
      //this.target_id = "";
      //this.max_volume_of_flow_per_turn = 0;

      this.can.be_filled_from = true;

      this.describe_temperatures = true;

      this.setDOVs(["turn"]);

      this.dov.turn.doAfterTry = function (params) {
        var input = this.game.getInput();
        var direct_object = input.getAsset(1);
        var direct_preposition = input.getPreposition(1);

        // sentence structure: verb preposition noun
        if (input.hasStructure("verb preposition noun")) {
          if (direct_preposition === "on") return this.turnOn_doAfterTry();
          if (direct_preposition === "off") return this.turnOff_doAfterTry();
        } // verb preposition noun
      };

      this.turnOn_doAfterTry = function (params) {
        var input = this.game.getInput();
        var direct_object = input.getAsset(1);
        var msg = "";

        // nothing with which to turn the faucet on
        // @TODO create option to turn on faucet without a controller
        // if( !direct_object.hasProperty('registered_parts.GraduatedControllers')
        // || 0 === direct_object.registered_parts.GraduatedControllers.length )
        if (!direct_object.registered_parts?.GraduatedControllers?.length) {
          this.game.debug(
            `F1561 | pour.js | ${direct_object.id} has no GraduatedControllers `
          );
          msg += `$(We) can't turn on ${direct_object.articlename}. `;
          this.game.dictionary.verbs[params.verb].handleFailure(msg);
          return null;
        } else {
          // get a list of controllers for this item
          // the reason there may be multiple controllers is to
          // handle things like sinks with multiple faucets
          // or a soda machine with multiple soda choices on one nozzle
          var controllers = direct_object.registered_parts.GraduatedControllers;
          var controller_count = controllers.length;
          // if there are multiple controllers, pick one at random
          var rand = Math.floor(Math.random() * controller_count);
          var controller = this.game.getAsset(controllers[rand]);
          input.setAsset(1, controller);
          input.allow_circular_verb = true;
          // Pass turn verb to the controller
          // because it has specialized logic of its own
          // that will call back to this object.
          // The reason for this circuity is to handle the player saying
          // "turn on faucet" instead of "turn handle"
          // It's the handle that turns on the faucet, but it's stupid
          // to tell the player "you can't turn on the faucet"
          this.game.dictionary.doVerb("turn");
          return null;
        }
      };

      this.turnOff_doAfterTry = function (params) {
        var input = this.game.getInput();
        var direct_object = input.getAsset(1);
        var msg = "";

        // nothing with which to turn the faucet off
        if (!direct_object.registered_parts.GraduatedControllers) {
          this.game.debug(
            `F1554 | SubstanceEmitter.js | ${direct_object.id} has no GraduatedControllers `
          );
          msg += `$(We) can't turn off ${direct_object.articlename}. `;
          this.game.dictionary.verbs[params.verb].handleFailure(msg);
          return null;
        }
        // turn off all controllers
        var controllers = direct_object.registered_parts.GraduatedControllers;
        var controller_count = controllers.length;
        var controllers_to_turn_off_count = 0;
        for (var c = 0; c < controller_count; c++) {
          var controller = this.game.getAsset(controllers[c]);
          if (0 < controller.current_position) {
            console.warn("turning off controller " + controller.id);
            controllers_to_turn_off_count++;
            input.setAsset(1, controller);
            input.allow_circular_verb = true;
            this.game.dictionary.doVerb("turn");
          }
        }
        if (0 === controllers_to_turn_off_count) {
          msg = `${direct_object.Articlename} isn't on. `;
          this.game.dictionary.verbs[params.verb].handleFailure(msg);
        }
        return null;
      };
    }

    /**
     * Shortcut to
     * Tangible.Aspect.Vessel.rate_of_flow.
     * @var {Getter/Setter} adventurejs.SubstanceEmitter#rate_of_flow
     */
    get rate_of_flow() {
      //return this.__rate_of_flow;
      return this.aspects.in.vessel.rate_of_flow;
    }
    set rate_of_flow(rate) {
      if (rate > 0) {
        this.setEmitter(true);
      }
      if (rate <= 0) {
        this.setEmitter(false);
      }
      //this.__rate_of_flow = rate;
      this.aspects.in.vessel.rate_of_flow = rate;
    }

    /**
     * Shortcut to
     * Tangible.Aspect.Vessel.volume_of_flow_per_turn
     * x Tangible.Aspect.Vessel.rate_of_flow
     * @var {Getter} adventurejs.SubstanceEmitter#volume
     */
    get volume() {
      return (
        this.aspects.in.vessel.volume_of_flow_per_turn *
        this.aspects.in.vessel.rate_of_flow
      );
    }

    /**
     * Shortcut to
     * Tangible.Aspect.Vessel.is_emitting.
     * @var {Getter/Setter} adventurejs.SubstanceEmitter#is_emitting
     */
    get is_emitting() {
      return this.aspects.in.vessel.is_emitting;
    }
    set is_emitting(bool) {
      this.aspects.in.vessel.is_emitting = bool;
    }

    /**
     * Shortcut to
     * Tangible.Aspect.Vessel.substance_id.
     * @var {Getter/Setter} adventurejs.SubstanceEmitter#substance_id
     */
    get substance_id() {
      return this.aspects.in.vessel.substance_id;
    }
    set substance_id(id) {
      this.aspects.in.vessel.substance_id = id;
    }

    /**
     * Shortcut to
     * Tangible.Aspect.Vessel.target_id.
     * @var {Getter/Setter} adventurejs.SubstanceEmitter#target_id
     */
    get target_id() {
      return this.aspects.in.vessel.target_id;
    }
    set target_id(id) {
      this.aspects.in.vessel.target_id = id;
    }

    /**
     * Shortcut to
     * Tangible.Aspect.Vessel.volume_of_flow_per_turn.
     * @var {Getter} adventurejs.SubstanceEmitter#volume_of_flow_per_turn
     */
    get volume_of_flow_per_turn() {
      return this.aspects.in.vessel.volume_of_flow_per_turn;
    }

    /**
     * Shortcut to
     * Tangible.Aspect.Vessel.max_volume_of_flow_per_turn.
     * @var {Getter/Setter} adventurejs.SubstanceEmitter#max_volume_of_flow_per_turn
     */
    get max_volume_of_flow_per_turn() {
      return this.aspects.in.vessel.max_volume_of_flow_per_turn;
    }
    set max_volume_of_flow_per_turn(maxvol) {
      this.aspects.in.vessel.max_volume_of_flow_per_turn = maxvol;
    }

    /**
     * Turns emitter on/off by setting this.is_emitting bool.
     * Also registers / unregisters with
     * {@link Adventurejs.Game#registerInterval|Game#registerInterval}
     * /
     * {@link Adventurejs.Game#unregisterInterval|Game#unregisterInterval},
     * which adds / subtracts this.emit() as a callback to
     * game.world._intervals. Registered callbacks are called at the
     * end of every turn. They're also saved with saved games, so that
     * restored games can resume with registered callbacks.
     * @memberof adventurejs.SubstanceEmitter
     * @method adventurejs.SubstanceEmitter#setEmitter
     */
    setEmitter(enable) {
      if (enable && false === this.is_emitting) {
        this.is_emitting = true;
        this.game.registerInterval(this.id, "emit");
      }
      if (false === enable && true === this.is_emitting) {
        this.is_emitting = false;
        this.game.unregisterInterval(this.id, "emit");
      }
    }

    /**
     * Causes the emitter to pour substance
     * into its target_id. If target already contains substance,
     * they are mixed. If target is infinite, it remains unaffected.
     * If target is full, it overflows into its container.
     * This gets registered as a callback through
     * {@link Adventurejs.Game#registerInterval|Game#registerInterval}
     * so that it can be called on every turn,
     * and its state can be saved with saved games.
     * @memberof adventurejs.SubstanceEmitter
     * @method adventurejs.SubstanceEmitter#emit
     */
    emit() {
      this.game.log(
        "log",
        "high",
        "SubstanceEmitter.js > " + this.name + " emit ",
        "SubstanceEmitter"
      );
      var msg = "";

      // has the emitter got flow?
      // if not, we're outta here
      if (0 >= this.aspects.in.vessel.volume_of_flow_per_turn) {
        return;
      }

      // create a SubstanceMixer to handle the details
      var mixer = new adventurejs.SubstanceMixer(this.game.game_name).set({
        source_input: this.id,
        source_aspect: "in",
        source_substance_id: this.aspects.in.vessel.substance_id,
        target_input: this.target_id,
      });

      var results = mixer.mix();
      if (A.isFalseOrNull(results)) return results;

      // Get temperature string. Temperature could be a spoiler
      // authors might not want, which is why we have an option.
      if (true === this.describe_temperatures) {
        var temp = this.game.dictionary.getStringLookupByRange(
          "substance_temperatures",
          this.aspects.in.vessel.temperature
        );
        msg = A.propercase(temp) + " " + mixer.source_substance_asset.name;
      } else {
        // no temp string
        msg = mixer.source_substance_asset.Name;
      }

      msg += " pours from the " + mixer.source_asset.name;

      var parent = mixer.source_asset.getPlaceAsset();
      if (mixer.target_asset) {
        var iparent = mixer.target_asset.getPlaceAsset();
        msg += " into the ";
        msg += mixer.target_asset.name;

        if (mixer.did_overflow_target) {
          msg +=
            " and spills out onto " +
            ("Room" === iparent.class ? "the floor" : iparent.articlename);
        } else if (mixer.can_drain_target) {
          msg += ", where it quickly drains away";
        }
      } else {
        msg += " and spills out onto ";
        msg += "Room" === parent.class ? "the floor" : parent.articlename;
      }
      msg += ". ";

      // print a thing if player is present
      if (mixer.source_asset.getRoomId() === this.game.world._currentRoom) {
        this.game.print(msg);
      }
    }

    /**
     * Responds to changes made to an associated GraduatedController.
     * @memberof adventurejs.SubstanceEmitter
     * @method adventurejs.SubstanceEmitter#onChangeGraduatedController
     */
    onChangeGraduatedController(direct_object) {
      // passing direct_object because it may not
      // be the same object found in game.getInput()
      //console.warn( "change " + direct_object.id + " > " + game );
      var old_rate = this.rate_of_flow;
      var new_rate = 0;
      var controller,
        controller_count,
        controller_percent,
        percent_per_controller;
      var substances = [];
      var temperatures = [];
      this.aspects.in.vessel.vessel_is_known = true;

      // check if any GraduatedControllers were registered
      if (
        "undefined" !== typeof this.registered_parts.GraduatedControllers &&
        0 < this.registered_parts.GraduatedControllers.length
      ) {
        // how many GraduatedControllers?
        controller_count = this.registered_parts.GraduatedControllers.length;
        percent_per_controller = 1 / controller_count;

        // check state of each controller
        // and calculate what volume it's contributing
        this.registered_parts.GraduatedControllers.forEach(function (id) {
          controller = this.game.getAsset(id);

          this.game.log(
            "log",
            "high",
            "SubstanceEmitter.js > GraduatedController " + id,
            "SubstanceEmitter"
          );
          this.game.log(
            "log",
            "high",
            "SubstanceEmitter.js >  .current_position " +
              controller.current_position,
            "SubstanceEmitter"
          );
          this.game.log(
            "log",
            "high",
            "SubstanceEmitter.js >  .control_positions " +
              controller.control_positions,
            "SubstanceEmitter"
          );
          this.game.log(
            "log",
            "high",
            "SubstanceEmitter.js >  .set_substance_id " +
              controller.set_substance_id,
            "SubstanceEmitter"
          );
          this.game.log(
            "log",
            "high",
            "SubstanceEmitter.js >  .set_substance_temperature " +
              controller.set_substance_temperature,
            "SubstanceEmitter"
          );

          if (controller && controller.current_position > 0) {
            controller_percent =
              controller.control_positions / (controller.current_position + 1);
            //console.warn( " - controller_percent " + controller_percent );
            new_rate = new_rate + controller_percent * percent_per_controller;
            if (controller.set_substance_id) {
              substances.push(controller.set_substance_id);
              if (!isNaN(controller.set_substance_temperature)) {
                temperatures.push(controller.set_substance_temperature);
              }
            }
          }
        }, this);
        this.rate_of_flow = new_rate;
        //console.warn( " - new_rate " + new_rate );
        //console.warn( " - substances " + substances.toString() );

        if (1 === substances.length) {
          this.game.log(
            "log",
            "high",
            "SubstanceEmitter.js > one substance found, set temperature of " +
              this.name +
              " to " +
              temperatures[0],
            "SubstanceEmitter"
          );
          this.aspects.in.vessel.substance_id = substances[0];
          this.aspects.in.vessel.temperature = temperatures[0];
        } else if (2 === substances.length) {
          var output_substance_id;
          var output_substance_asset;
          this.aspects.in.vessel.substance_id = substances[0];

          // TODO This is simplified/incomplete. Ideally temp should be
          // effected by volume of each input so if one handle is open
          // more than the other it should add more weight
          // see SubstanceMixer.mixTemps
          this.aspects.in.vessel.temperature = (
            (temperatures[0] + temperatures[1]) /
            2
          ).toFixed(0);
          var msg =
            "two substances found, set temperature of " +
            this.name +
            " to average of " +
            temperatures[0] +
            " and " +
            temperatures[1];
          this.game.log(
            "log",
            "high",
            "SubstanceEmitter.js > " + msg,
            "SubstanceEmitter"
          );

          var sub1 = this.game.getAsset(substances[0]);
          var sub2 = this.game.getAsset(substances[1]);

          this.game.log(
            "log",
            "high",
            "SubstanceEmitter.js > sub1 " + sub1,
            "SubstanceEmitter"
          );
          this.game.log(
            "log",
            "high",
            "SubstanceEmitter.js > sub2 " + sub2,
            "SubstanceEmitter"
          );

          if (sub1 && sub2) {
            if (sub1.mixwith[sub2.id]) {
              output_substance_id = sub1.mixwith[sub2.id];
            } else if (sub2.mixwith[sub1.id]) {
              output_substance_id = sub2.mixwith[sub1.id];
            }
          }
          if (output_substance_id) {
            output_substance_asset = this.game.getAsset(output_substance_id);
            if (output_substance_asset) {
              this.aspects.in.substance_id = output_substance_id;
            }
          }
        } else if (2 < substances.length) {
          // TODO more than two graduatedControllers
          // example being a soda fountain with multiple switches
        }
      }

      var msg = "";
      if (new_rate === old_rate) {
        msg = "Nothing happens. ";
      } else if (new_rate > old_rate && 0 === old_rate) {
        msg = this.Articlename + " turns on. ";
      } else if (new_rate > old_rate) {
        msg = this.Articlename + "'s pressure increases. ";
      } else if (new_rate === 0) {
        msg = this.Articlename + " shuts off. ";
      } else if (new_rate < old_rate) {
        msg = this.Articlename + "'s pressure decreases. ";
      } else {
        msg = "Nothing happens. ";
      }

      if (msg) return msg;
      else return true;
    }
  }
  adventurejs.SubstanceEmitter = SubstanceEmitter;
})();