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