// SubstanceMixer.js
(function () {
/*global adventurejs A*/
"use strict";
/**
* @ajspath adventurejs.Atom.SubstanceMixer
* @ajsconstruct var mixer = new adventurejs.SubstanceMixer( this.game.game_name )
* @augments adventurejs.Atom
* @class adventurejs.SubstanceMixer
* @ajsinternal
* @ajsnavheading BaseClasses
* @param {String} game_name Name of top level game instance that is scoped to window.
* @summary Shaken, not stirred.
* @todo Solid+liquid->slurry.
* @classdesc
* <p>
* <strong>SubstanceMixer</strong> is a special internal class
* that is instantiated whenever
* {@link adventurejs.Substance|Substances} are mixed
* together from two (or in theory, more) sources.
* For instance, if player pours a glass full of liquid
* into a bowl that also contains liquid, we make a new
* SubstanceMixer to handle the interaction. This is
* true even if both Vessels contain the same
* Substance. SubstanceMixer handles:
* <ul>
* <li>temperature conversion, if SubstancesContainers are
* at different temperatures</li>
* <li>volume operations - comparing available volume in
* source and destination, removing part or all from
* source as needed</li>
* <li>mixwith handling, if two Substances are set to mix
* with each other to form a third Substance</li>
* <li>overflow of the target container if needed</li>
* </ul>
* </p>
* <h3 class="examples">Example:</h3>
* <pre class="display"><code class="language-javascript">
* </code></pre>
**/
class SubstanceMixer extends adventurejs.Atom {
constructor(game_name) {
super(undefined, game_name);
this.game_name = game_name;
this.class = "SubstanceMixer";
//this.game_name = game_name;
/**
* <b>source_input</b> might be an asset id or a tri-part
* asset:aspect:substance id string. We're prepared to
* handle either to get an asset, or receive an asset object directly.
* @var {String} adventurejs.SubstanceMixer#source_input
* @default ""
*/
this.source_input = "";
/**
* <b>source_asset</b> is an asset with a vessel that contains the source substance.
* @var {Object|null} adventurejs.SubstanceMixer#source_asset
* @default null
*/
this.source_asset = null;
/**
* <b>source_aspect</b> is the asset aspect that contains
* the source substance.
* @var {String} adventurejs.SubstanceMixer#source_aspect
* @default ""
*/
this.source_aspect = "";
/**
* <b>source_vessel</b> is a reference to the Vessel object in the source asset.
* @var {Object|null} adventurejs.SubstanceMixer#source_vessel
* @default null
*/
this.source_vessel = null;
/**
* <b>source_substance_asset</b> is the asset of the substance that is contained.
* @var {Object|null} adventurejs.SubstanceMixer#source_substance_asset
* @default null
*/
this.source_substance_asset = null;
/**
* <b>source_substance_id</b> is the id of the substance asset that is contained, ie 'sand' or 'water'.
* @var {String} adventurejs.SubstanceMixer#source_substance_id
* @default ""
*/
this.source_substance_id = "";
/**
* <b>source_split</b> is used to store the provided source_input if it is a tri-part string.
* @var {Array|String|null} adventurejs.SubstanceMixer#source_split
* @default null
*/
this.source_split = null;
/**
* <b>source_volume</b> is the volume of the substance in the source vessel.
* @var {int} adventurejs.SubstanceMixer#source_volume
* @default 0
*/
this.source_volume = 0;
/**
* <b>source_volume_used</b> stores the volume of the source substance that has been used.
* @var {int} adventurejs.SubstanceMixer#source_volume_used
* @default 0
*/
this.source_volume_used = 0;
/**
* <b>target_input</b> might be an asset id or a tri-part
* asset:aspect:substance id string. We're prepared to
* handle either to get an asset, or receive an asset object directly.
* @var {String} adventurejs.SubstanceMixer#target_input
* @default ""
*/
this.target_input = "";
/**
* <b>target_asset</b> is an asset with a vessel that receives the source substance.
* @var {Object|null} adventurejs.SubstanceMixer#target_asset
* @default null
*/
this.target_asset = null;
/**
* <b>target_aspect</b> is the asset aspect which will receive
* the source substance.
* @var {String} adventurejs.SubstanceMixer#target_aspect
* @default ""
*/
this.target_aspect = "";
/**
* <b>target_vessel</b> is a reference to the Vessel object in the target asset.
* @var {Object|null} adventurejs.SubstanceMixer#target_vessel
* @default null
*/
this.target_vessel = null;
/**
* <b>target_substance_asset</b> is the asset of the substance that is
* contained by the target asset.
* @var {Object|null} adventurejs.SubstanceMixer#target_substance_asset
* @default null
*/
this.target_substance_asset = null;
/**
* <b>target_substance_id</b> is the id of the substance asset that
* is contained in the target asset, ie 'sand' or 'water'.
* @var {String} adventurejs.SubstanceMixer#target_substance_id
* @default ""
*/
this.target_substance_id = "";
/**
* <b>target_split</b> is used to store the provided target_input if it is a tri-part string.
* @var {Array|String|null} adventurejs.SubstanceMixer#target_split
* @default null
*/
this.target_split = null;
/**
* <b>target_volume</b> is the volume of the substance in the target vessel.
* @var {float} adventurejs.SubstanceMixer#target_volume
* @default 0.0
*/
this.target_volume = 0.0;
/**
* <b>target_freevolume</b> is the free volume of the substance in the target vessel.
* @var {float} adventurejs.SubstanceMixer#target_freevolume
* @default 0.0
*/
this.target_freevolume = 0.0;
/**
* <b>output_substance_asset</b> is the asset of the substance that
* results from mixing (which may just be the source substance).
* @var {Object|null} adventurejs.SubstanceMixer#output_substance_asset
* @default null
*/
this.output_substance_asset = null;
/**
* <b>output_substance_id</b> is the id of the substance asset that
* is that results from mixing, ie 'sand' or 'water'.
* @var {String} adventurejs.SubstanceMixer#output_substance_id
* @default ""
*/
this.output_substance_id = "";
/**
* <b>target_already_full</b> is used to indicate that the target vessel
* is already full.
* @var {Boolean} adventurejs.SubstanceMixer#target_already_full
* @default false
*/
this.target_already_full = false;
/**
* <b>did_fill_target</b> is used to indicate that the target vessel
* was filled by the source substance.
* @var {Boolean} adventurejs.SubstanceMixer#did_fill_target
* @default false
*/
this.did_fill_target = false;
/**
* <b>did_mix_substances</b>
* @var {Boolean} adventurejs.SubstanceMixer#did_mix_substances
* @default false
*/
this.did_mix_substances = false;
/**
* <b>did_displace_substance</b> is used if no mixwiths
* are provided. Source substance will simply displace
* target substance.
* @var {Boolean} adventurejs.SubstanceMixer#did_displace_substance
* @default false
*/
this.did_displace_substance = false;
/**
* <b>can_drain_target</b> is used to indicate that the target vessel
* can be drained.
* @var {Boolean} adventurejs.SubstanceMixer#can_drain_target
* @default false
*/
this.can_drain_target = false;
/**
* <b>did_overflow_target</b> may be set to true
* if the source is an emitter that emits a greater
* volume than the target can hold.
* @var {Boolean} adventurejs.SubstanceMixer#did_overflow_target
* @default false
*/
this.did_overflow_target = false;
/**
* <b>can_overflow_target</b> overrides did_overflow_target
* if it's true. It's used by fill verb, the idea being that,
* while an automated emitter might overflow a target,
* a person consciously filling a vessel would not.
* @var {Boolean} adventurejs.SubstanceMixer#can_overflow_target
* @default false
*/
this.can_overflow_target = false;
}
/**
* Mix two substances. This function acts upon the source /
* target objects, and saves its results to the
* SubstanceMixer object for reference by the caller.
* @memberof adventurejs.SubstanceMixer
* @method adventurejs.SubstanceMixer#mix
* @returns {boolean}
*/
mix() {
// SOURCE ASSET
// asset:aspect:substance
this.source_split = this.source_input.split(":");
if (!this.source_asset) {
// regardless of whether player input a substance containing object
// or they entered a substance and we parsed asset:aspect:substance,
// source_split[0] will be the container
this.source_asset = this.game.getAsset(this.source_split[0]);
}
if (Object(this.source_asset) !== this.source_asset) {
this.game.log(
"warn",
0,
"SubstanceMixer.source_asset undefined",
"SubstanceMixer"
);
return false;
}
this.game.log(
"log",
"high",
"SubstanceMixer.js > source_asset " + this.source_asset.id,
"SubstanceMixer"
);
// SOURCE ASPECT
// check for input that was parsed into "bowl:in:water" format
if (!this.source_aspect) {
if (1 < this.source_split.length) {
// if they entered a substance and we parsed asset:aspect:substance,
// source_split[1] will be the aspect
this.source_aspect = this.source_split[1];
} else {
// we did not receive a triplet, presumably player referred
// directly to container, not substance, so get
// the object's substance location...
this.source_aspect = this.source_asset.getAspectWithVessel();
}
}
if (!this.source_aspect) {
this.game.log(
"warn",
0,
"SubstanceMixer.source_aspect undefined",
"SubstanceMixer"
);
return false;
}
this.game.log(
"log",
"high",
"SubstanceMixer.js > source_aspect " + this.source_aspect,
"SubstanceMixer"
);
// SOURCE SUBSTANCE
if (!this.source_substance_id) {
if (3 === this.source_split.length) {
// asset:aspect:substance
this.source_substance_id = this.source_split[2];
} else {
this.source_substance_id =
this.source_asset.aspects[this.source_aspect].vessel.substance_id;
}
}
if (!this.source_substance_id) {
this.game.log(
"warn",
0,
"SubstanceMixer.source_substance_id undefined",
"SubstanceMixer"
);
return false;
}
this.game.log(
"log",
"high",
"SubstanceMixer.js > source_substance_id " + this.source_substance_id,
"SubstanceMixer"
);
// SOURCE SUBSTANCE CONTAINER
if (!this.source_vessel) {
// ... and then get the substance
this.source_vessel =
this.source_asset.aspects[this.source_aspect].vessel; //.substance_id;
}
if (!this.source_vessel) {
this.game.log(
"warn",
0,
"SubstanceMixer.source_vessel undefined",
"SubstanceMixer"
);
return false;
}
this.game.log(
"log",
"high",
"SubstanceMixer.js > source_vessel " + this.source_vessel.id,
"SubstanceMixer"
);
// SOURCE SUBSTANCE ASSET
if (this.source_substance_id) {
// get substance object by id from lookup table
this.source_substance_asset = this.game.getAsset(
this.source_substance_id
);
}
if (Object(this.source_substance_asset) !== this.source_substance_asset) {
this.game.log(
"warn",
0,
"SubstanceMixer.source_substance_asset undefined",
"SubstanceMixer"
);
return false;
}
this.game.log(
"log",
"high",
"SubstanceMixer.js > source_substance_asset " +
this.source_substance_asset.id,
"SubstanceMixer"
);
// TARGET ASSET
this.target_split = this.target_input.split(":");
if (!this.target_asset) {
this.target_asset = this.game.getAsset(this.target_split[0]);
}
this.game.log(
"log",
"high",
"SubstanceMixer.js > target_asset " + this.target_asset.id,
"SubstanceMixer"
);
// TARGET ASPECT
if (!this.target_aspect && 1 < this.target_split.length) {
this.target_aspect = this.target_split[1];
}
console.warn("target_asset", this.target_asset);
console.warn("target_aspect", this.target_aspect);
if (!this.target_aspect) {
// we did not receive a triplet, presumably player referred
// directly to container, not substance, so get
// the object's substance location...
this.target_aspect = this.target_asset.getAspectWithVessel();
}
console.warn("target_aspect", this.target_aspect);
this.game.log(
"log",
"high",
"SubstanceMixer.js > target_aspect " + this.target_aspect,
"SubstanceMixer"
);
// TARGET SUBSTANCE
if (!this.target_substance_id && 2 < this.target_split.length) {
this.target_substance_id = this.target_split[2];
}
this.game.log(
"log",
"high",
"SubstanceMixer.js > target_substance_id " + this.target_substance_id,
"SubstanceMixer"
);
// TARGET SUBSTANCE CONTAINER
if (!this.target_vessel) {
// ... and the substance it contains
this.target_vessel =
this.target_asset.aspects[this.target_aspect].vessel;
}
this.game.log(
"log",
"high",
"SubstanceMixer.js > target_vessel " + this.target_vessel.id,
"SubstanceMixer"
);
// TARGET SUBSTANCE ASSET
if (!this.target_substance_id) {
// ... and the substance it contains
this.target_substance_id = this.target_vessel.substance_id;
}
if (this.target_substance_id) {
// get the substance object from its id
this.target_substance_asset = this.game.getAsset(
this.target_substance_id
);
}
this.game.log(
"log",
"high",
"SubstanceMixer.js > target_substance_id " + this.target_substance_id,
"SubstanceMixer"
);
// Is target a drain or has target got a drain?
this.can_drain_target = this.target_vessel.can_drain;
// Is target draining or a body of water?
// Either case effectively means it can accept
// infinite volume without mixing substance or temp.
if (
Infinity === this.target_vessel.volume ||
this.target_vessel.can_drain === true
) {
// Is source an emitter?
if (this.source_asset instanceof adventurejs.SubstanceEmitter) {
/**
* @todo source emitter mixed into infinite/draining target?
*/
// ? check this.source_asset.volume_of_flow_per_turn ?
} // Or is source a container?
else {
// if the source is finite and the target is infinite,
// target will take all of source
// and we will not mix
if (
false ===
this.source_asset instanceof adventurejs.SubstanceEmitter &&
isFinite(this.source_vessel.volume)
) {
this.game.log(
"log",
"high",
"SubstanceMixer.js > target is infinite, source is finite, so target will take all of source",
"SubstanceMixer"
);
this.source_vessel.volume = 0;
}
}
} else {
// Target is not draining nor is it a body of water
// if target contains a different substance from source
// see if there's a mixwith for the two substances
// also mix temps
// TODO multiple substance mixing
if (
Object(this.source_substance_asset) === this.source_substance_asset &&
this.target_vessel.volume > 0 &&
this.target_substance_id != this.source_vessel.substance_id
) {
// if either substance has a mixwith for the other
if (
this.source_vessel.substance.mixwith[
this.target_vessel.substance_id
]
) {
this.output_substance_id =
this.source_vessel.substance.mixwith[
this.target_vessel.substance_id
];
this.did_mix_substances = true;
} else if (
this.target_vessel.substance.mixwith[
this.source_vessel.substance_id
]
) {
this.output_substance_id =
this.target_vessel.substance.mixwith[
this.source_vessel.substance_id
];
this.did_mix_substances = true;
}
// otherwise the source substance simply displaces the target substance
else {
this.output_substance_id = this.source_vessel.substance_id;
this.did_displace_substance = true;
}
} else {
this.output_substance_id = this.source_vessel.substance_id;
}
this.output_substance_asset = this.game.getAsset(
this.output_substance_id
);
// current source level
this.source_volume = this.source_vessel.getVolume();
// current target level
if (this.did_displace_substance) this.target_vessel.volume = 0;
this.target_volume = this.target_vessel.volume;
this.target_freevolume =
this.target_vessel.maxvolume - this.target_volume;
this.target_already_full =
this.target_vessel.volume === this.target_vessel.maxvolume;
if (this.source_volume > this.target_freevolume) {
// target is full so set it to its maxvolume
this.target_vessel.volume = this.target_vessel.maxvolume;
if (
this.source_asset instanceof adventurejs.SubstanceEmitter ||
!isFinite(this.source_vessel.volume)
) {
// it's assumed that emitters are automated and won't
// stop emitting just because the target is full
// so it overflows regardless of overflow setting
if (this.can_overflow_target) {
this.did_overflow_target = true;
}
if (this.target_already_full) {
// let's arbitrarily say that we're pouring an amount
// equal to half the target's free volume - an even mix
this.source_volume_used = this.source_volume;
} else {
this.source_volume_used = this.target_freevolume;
}
}
// else if ( !isFinite( this.source_vessel.volume ) )
// {
// // source is infinite but we need to determine quantity used
// // for mixtemps calculation further down the line
// }
// else if( false === this.target_already_full
// && Infinity === this.target_vessel.volume )
// {
// // don't overflow
// }
else if (
!this.target_already_full &&
(!this.target_vessel.can_overflow || !this.can_overflow_target)
) {
// target is not full and not can_overflow
// subtract poured volume from source
// and don't overflow target
//this.source_asset.aspects[ this.source_aspect ].vessel.volume = this.source_asset.aspects[ this.source_aspect ].vessel.volume - this.target_freevolume;
this.source_vessel.volume =
this.source_vessel.volume - this.target_freevolume;
this.source_volume_used = this.target_freevolume;
} else if (
this.target_already_full &&
!this.target_vessel.can_overflow &&
!this.can_overflow_target
) {
// target is full and not can_overflow
// do nothing...?
} else if (
this.target_vessel.can_overflow &&
this.can_overflow_target
) {
// overflow target and set source volume to zero
this.source_volume_used = this.source_vessel.volume;
//this.source_asset.aspects[ this.source_aspect ].vessel.volume = 0;
this.source_vessel.volume = 0;
this.did_overflow_target = true;
} else {
// subtract poured volume from source
this.source_asset.aspects[this.source_aspect].vessel.volume =
this.source_asset.aspects[this.source_aspect].vessel.volume -
this.target_freevolume;
this.source_volume_used = this.target_freevolume;
}
} // this.source_volume is not > this.target_freevolume
else {
// add source volume to target volume
this.target_vessel.volume =
this.target_vessel.volume + this.source_volume;
if (this.source_asset instanceof adventurejs.SubstanceEmitter) {
// @TODO figure out rate of flow
// temp: set to free volume
this.source_volume_used = this.target_freevolume;
} else if (!isFinite(this.source_vessel.volume)) {
this.source_volume_used = this.target_freevolume;
} else {
// empty source
this.source_volume_used = this.source_vessel.volume;
//this.source_asset.aspects[ this.source_aspect ].vessel.volume = 0;
this.source_vessel.volume = 0;
}
}
this.target_vessel.substance_id = this.output_substance_id;
//this.mixTemps();
if (!isFinite(this.target_volume)) {
// recipient is infinite, no temperature change
} else if (this.target_volume <= 0) {
this.target_vessel.temperature = Number(
this.source_vessel.temperature
);
} else if (this.target_volume > 0) {
// liters to grams
// grams = liters × 1000 × ingredient density
// grams to liters
// liters = grams / ( 1000 x ingredient density )
// Mixing Liquids and/or Solids - Final Temperatures
// Calculate the final temperature when liquids or solids are mixed
// https://www.engineeringtoolbox.com/temperature-mixing-liquid-solids-d_1754.html
var m1 =
this.source_volume_used * this.source_substance_asset.density;
var cp1 = this.source_substance_asset.specific_heat;
var t1 = this.source_vessel.temperature;
var m2 = this.target_volume * this.target_substance_asset.density;
var cp2 = this.target_substance_asset.specific_heat;
var t2 = this.target_vessel.temperature;
console.warn(
"m1",
m1,
"cp1",
cp1,
"t1",
t1,
"m2",
m2,
"cp2",
cp2,
"t2",
t2
);
var temperature; // = this.target_vessel.room_temperature;
if (
!isNaN(m1) &&
!isNaN(cp1) &&
!isNaN(t1) &&
!isNaN(m2) &&
!isNaN(cp2) &&
!isNaN(t2)
) {
temperature =
(m1 * cp1 * t1 + m2 * cp2 * t2) / (m1 * cp1 + m2 * cp2);
this.target_vessel.temperature = Number(temperature);
}
this.game.log(
"log",
"high",
"SubstanceMixer.js > source temp " + this.source_vessel.temperature,
"SubstanceMixer"
);
this.game.log(
"log",
"high",
"SubstanceMixer.js > target temp " + this.target_vessel.temperature,
"SubstanceMixer"
);
this.game.log(
"log",
"high",
"SubstanceMixer.js > mixed temp " + temperature,
"SubstanceMixer"
);
} // mixTemps
}
this.did_fill_target =
this.target_vessel.volume === this.target_vessel.maxvolume;
if (this.source_substance_asset) {
this.game.log(
"log",
"high",
"SubstanceMixer.js > source_substance_asset " +
this.source_substance_asset.id,
"SubstanceMixer"
);
this.source_substance_asset.setKnown();
}
if (this.target_substance_asset) {
this.game.log(
"log",
"high",
"SubstanceMixer.js > target_substance_asset " +
this.target_substance_asset.id,
"SubstanceMixer"
);
this.target_substance_asset.setKnown();
}
//if( "undefined" !== typeof this.output_substance_asset )
if (Object(this.output_substance_asset) === this.output_substance_asset) {
this.game.log(
"log",
"high",
"SubstanceMixer.js > output_substance_asset " +
this.output_substance_asset.id,
"SubstanceMixer"
);
this.output_substance_asset.setKnown();
}
// check for custom logic
var results = this.source_asset.onSubtractSubstanceFromThis(
this.source_substance_asset
);
if ("undefined" !== typeof results) return results;
// check for custom logic
results = this.target_asset.onAddSubstanceToThis(
this.source_substance_asset
);
if ("undefined" !== typeof results) return results;
return;
}
// liters to grams
// grams = liters × 1000 × ingredient density
// grams to liters
// liters = grams / ( 1000 x ingredient density )
// Mixing Liquids and/or Solids - Final Temperatures
// Calculate the final temperature when liquids or solids are mixed
// https://www.engineeringtoolbox.com/temperature-mixing-liquid-solids-d_1754.html
// p.mixTemps = function SubstanceMixer_mixTemps(){
// //var m1 = this.source_volume * this.source_substance_asset.density;
// var m1 = this.source_volume_used * this.source_substance_asset.density;
// var cp1 = this.source_substance_asset.specific_heat;
// var t1 = this.source_vessel.temperature;
// var m2 = this.target_volume * this.target_substance_asset.density;
// var cp2 = this.target_substance_asset.specific_heat;
// var t2 = this.target_vessel.temperature;
// var temperature;
// if(m1 && cp1 && t1 && m2 && cp2 && t2)
// {
// temperature = ( m1 * cp1 * t1 + m2 * cp2 * t2 ) / ( m1 * cp1 + m2 * cp2 );
// }
// //return Conv.rounding(temperature);
// //console.warn( "SubstanceMixer.mixTemps > " + temperature );
// return temperature;
// }
// Mix Liquids of Different Temperatures
// https://rechneronline.de/chemie-rechner/mix-temperatures.php
// p.mixTempOfLiquids = function SubstanceMixer_mixTempOfLiquids( source, target ){
// // our measure of volume is stored in milliliters
// // water is roughly 1 gram per milliliter but other substances are not
// var m1 = source.volume; // 'amount' in source script
// var c1 = source.heat_capacity; // 'heat capacity' in source script // ex: water = 4.2
// var t1 = source.temperature;
// var m2 = target.volume; // 'amount' in source script;
// var c2 = target.heat_capacity; // 'heat capacity' in source script // ex: water = 4.2
// var t2 = target.temperature;
// var result;
// if (m1 && m2) {
// target.volume = m1 + m2;
// }
// if (!isNaN(t1) && !isNaN(t2))
// result = ( ( m1 * c1 * t1 ) + ( m2 * c2 * t2 ) ) / ( ( m1 * c1 ) + ( m2 * c2 ) );
// return result;
// }
}
adventurejs.SubstanceMixer = SubstanceMixer;
})();