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