Pre-release
AdventureJS Docs Downloads
Score: 0 Moves: 0
// SubstanceEmitter.js
(function () {
  /* global adventurejs A */

  /**
   * @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.FaucetHandle|FaucetHandle}
   * 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.is = new adventurejs.SubstanceEmitter_Is(
        "is",
        this.game_name,
        this.id
      );

      this.aspects.in = new adventurejs.Aspect("in", this.game_name, this.id);

      this.aspects.in.vessel = new adventurejs.Vessel(
        "in",
        game_name,
        this.id
      ).set({ 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.describe_temperatures = true;

      // this.setDOVs(["turn"]);
      this.setDOVs([
        // { turn: { with_nothing: true } },
        { turnOn: { with_nothing: true } },
        { turnOff: { with_nothing: true } },
      ]);

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

        if (direct_object.linked_components?.GraduatedControllers?.length) {
          // 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.linked_components.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"
          return this.game.dictionary.doVerb("turnOn");
        }
        // no controllers so just toggle it
        else {
          this.game.debug(
            `D1561 | SubstanceEmitter.js | ${direct_object.id} has no GraduatedControllers `
          );

          this.rate_of_flow = 1;
          //this.setEmitter(true);

          // msg += `{We} can't turn on ${direct_object.articlename}. `;
          // this.game.dictionary.verbs[params.verb].handleFailure(msg);
          // return null;
        }
      };

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

        if (direct_object.linked_components?.GraduatedControllers?.length) {
          // turn off all controllers
          var controllers =
            direct_object.linked_components.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("turnOff");
            }
          }
          if (0 === controllers_to_turn_off_count) {
            msg = `${direct_object.Articlename_isnt} on. `;
            this.game.dictionary.verbs[params.verb].handleFailure(msg);
          }
          return null;
        }

        // no controllers so just toggle it
        else {
          this.game.debug(
            `D1554 | SubstanceEmitter.js | ${direct_object.id} has no GraduatedControllers `
          );

          this.rate_of_flow = 0;
          //this.setEmitter(false);

          // msg += `{We} can't turn off ${direct_object.articlename}. `;
          // 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.aspects.in.vessel.rate_of_flow;
    }
    set rate_of_flow(rate) {
      this.aspects.in.vessel.rate_of_flow = rate;
      if (rate > 0) {
        this.setEmitter(true);
      }
      if (rate <= 0) {
        this.setEmitter(false);
      }
    }

    /**
     * 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.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.aspects.in.vessel.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) {
      this.game.log(
        "L1537",
        "log",
        "high",
        `[SubstanceEmitter.js] ${this.name} setEmitter ${String(enable)}`,
        "SubstanceEmitter"
      );

      if (!this.aspects.in?.vessel) return false;

      if (enable && !this.aspects.in.vessel.is.emitting) {
        this.aspects.in.vessel.is.emitting = true;
        this.game.registerInterval({ id: this.id, callback: "emit" });
      }
      if (!enable && this.aspects.in.vessel.is.emitting) {
        this.aspects.in.vessel.is.emitting = false;
        this.game.unregisterInterval({ id: this.id, callback: "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(
        "L1440",
        "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._room) {
        this.game.print(msg);
      }

      return ""; // for getStringOrArrayOrFunction
    }

    /**
     * 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.is.known = true; // ??

      // check if any GraduatedControllers were registered
      if (!this.linked_components?.GraduatedControllers?.length) {
        // no controllers means toggle
        new_rate = old_rate === 0 ? 1 : 0;
      } else {
        // how many GraduatedControllers?
        controller_count = this.linked_components.GraduatedControllers.length;
        percent_per_controller = 1 / controller_count;

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

          this.game.log(
            "L1441",
            "log",
            "high",
            `[SubstanceEmitter.js] GraduatedController ${id}`,
            "SubstanceEmitter"
          );
          this.game.log(
            "L1442",
            "log",
            "high",
            `[SubstanceEmitter.js] .current_position ${controller.current_position}`,
            "SubstanceEmitter"
          );
          this.game.log(
            "L1443",
            "log",
            "high",
            `[SubstanceEmitter.js] .control_positions ${controller.control_positions}`,
            "SubstanceEmitter"
          );
          this.game.log(
            "L1444",
            "log",
            "high",
            `[SubstanceEmitter.js] .set_substance_id ${controller.set_substance_id}`,
            "SubstanceEmitter"
          );
          this.game.log(
            "L1445",
            "log",
            "high",
            `[SubstanceEmitter.js] .set_substance_temperature ${controller.set_substance_temperature}`,
            "SubstanceEmitter"
          );

          if (controller?.current_position > 0) {
            controller_percent =
              controller.current_position / (controller.control_positions - 1);
            new_rate = new_rate + controller_percent * percent_per_controller;
            if (controller.set_substance_id) {
              let asset = this.game.getAsset(controller.set_substance_id);
              if (!asset) {
                let msg = `Undefined substance "${controller.set_substance_id}" assigned to ${this.id}. `;
                this.game.log("L1539", "error", "high", msg, "Asset");
              }
              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;

        if (1 === substances.length) {
          this.game.log(
            "L1446",
            "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(
            "L1447",
            "log",
            "high",
            `[SubstanceEmitter.js] ${msg}`,
            "SubstanceEmitter"
          );

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

          this.game.log(
            "L1448",
            "log",
            "high",
            `[SubstanceEmitter.js] sub1 ${sub1}`,
            "SubstanceEmitter"
          );
          this.game.log(
            "L1449",
            "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
        }
      }

      this.is.on = new_rate > 0 ? true : false;
      if (this.linked_parent) {
        let parent = this.game.getAsset(this.linked_parent);
        parent.is.on = this.is.on;
      }

      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;
})();