@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
342 lines (285 loc) • 7.48 kB
JavaScript
import { assert } from "../../assert.js";
import List from "../../collection/list/List.js";
import { chain } from "../../function/chain.js";
import Vector1 from "../../geom/Vector1.js";
import { EPSILON } from "../../math/EPSILON.js";
import { epsilonEquals } from "../../math/epsilonEquals.js";
import { max2 } from "../../math/max2.js";
import { min2 } from "../../math/min2.js";
import LinearModifier from "./LinearModifier.js";
/**
* Modifiable statistic.
* Allows non-destructive linear arithmetic.
* Useful when we wish to be able to reverse part or the whole of the computation
*
* Main purpose of the class is to facilitate implementation of RPG-like stats, such as maximum health, or armor.
* These stats can then be modified by various skills, equipment and effects. Each such modification will be added as a separate {@link LinearModifier} and can be reversed by removing the modifier.
*/
class Stat extends Number {
/**
* Unique identifier of a stat, such a health, armor etc.
* @type {number}
*/
id = 0;
/**
* List of modifiers.
* Order has no influence on final computed value
* @private
* @readonly
* @type {List<LinearModifier>}
*/
__modifiers = new List();
/**
* Computed value will be pass through this function before being exposed.
* Useful for rounding and clamping.
* see {@link Stat.Process} for some options.
* @type {function(number): number}
*/
postprocess = Stat.Process.NONE;
/**
* @param {number} [value]
* @constructor
*/
constructor(value = 0) {
super();
assert.isNumber(value, "value");
/**
* Base value that modifiers will affect
* @type {Vector1}
*/
this.base = new Vector1(value);
/**
* Final computed value. Do not modify this directly
* @type {Vector1}
*/
this.value = new Vector1(value);
this.base.onChanged.add(this.updateValue, this);
}
/**
*
* @returns {Signal<LinearModifier>}
*/
get onModifierAdded() {
return this.__modifiers.on.added;
}
/**
*
* @returns {Signal<LinearModifier>}
*/
get onModifierRemoved() {
return this.__modifiers.on.removed;
}
/**
* Remove all modifiers from the stat
*/
resetModifiers() {
this.__modifiers.reset();
}
/**
*
* @returns {number}
*/
valueOf() {
return this.getValue();
}
/**
*
* @returns {string}
*/
toString() {
return String(this.getValue());
}
/**
*
* @returns {number}
*/
getValue() {
return this.value.x;
}
updateValue() {
const x = this.base.x;
const modifiers = this.__modifiers;
const newValue = Stat.applyModifiers(x, modifiers);
const finalValue = this.postprocess(newValue);
this.value.set(finalValue);
}
/**
*
* @returns {number}
*/
getBaseValue() {
return this.base.getValue();
}
/**
*
* @param {number} v
*/
setBaseValue(v) {
this.base.set(v);
}
/**
* NOTE: do not modify result
* @return {LinearModifier[]}
*/
getModifiers() {
return this.__modifiers.data;
}
/**
*
* @param {LinearModifier} mod
*/
addModifier(mod) {
this.__modifiers.add(mod);
this.updateValue();
}
/**
*
* @param {LinearModifier} mod
* @return {boolean}
*/
hasModifier(mod) {
return this.__modifiers.contains(mod);
}
/**
*
* @param {LinearModifier} mod
* @returns {boolean}
*/
removeModifier(mod) {
const removed = this.__modifiers.removeOneOf(mod);
this.updateValue();
return removed;
}
/**
*
* @param {Stat} other
* @returns {boolean}
*/
equals(other) {
const v0 = this.getValue();
const b1 = other.getValue();
// use small tolerance to allow for numeric error resulting from the order in which stat modifiers are combined
return epsilonEquals(v0, b1, EPSILON);
}
/**
*
* @param {Stat} other
*/
copy(other) {
this.base.copy(other.base);
this.__modifiers.copy(other.__modifiers);
this.updateValue();
}
/**
* Copy base value from another stat
* @param {Stat} other
*/
copyBase(other) {
this.base.copy(other.base);
}
/**
*
* @param {function(number):number} f
* @param {number} v
*/
setBaseFromParametricFunction(f, v) {
const base_value = f(v);
this.base.set(base_value);
}
/**
* @param {Stat} other
*/
addNonTransientModifiersFromStat(other) {
const modifiers = other.__modifiers.data;
const n = modifiers.length;
for (let i = 0; i < n; i++) {
const linearModifier = modifiers[i];
if (linearModifier.transient) {
continue;
}
//make a copy
const clone = linearModifier.clone();
this.addModifier(clone);
}
}
toJSON() {
return {
base: this.base.toJSON(),
modifiers: this.__modifiers.toJSON()
};
}
fromJSON(json) {
this.base.fromJSON(json.base);
this.__modifiers.fromJSON(json.modifiers, LinearModifier);
this.updateValue();
}
/**
*
* @returns {Signal}
*/
get onChanged() {
return this.value.onChanged;
}
/**
*
* @param {number} input
* @param {List<LinearModifier>}modifiers
* @returns {number}
*/
static applyModifiers(input, modifiers) {
let a = 1;
let b = 0;
let i;
const l = modifiers.length;
const modifiersData = modifiers.data;
// Combine all modifiers
for (i = 0; i < l; i++) {
const m = modifiersData[i];
a += (m.a - 1);
b += m.b;
}
return input * a + b;
}
}
Stat.Process = {
/**
* @param {number} v
* @return {number}
*/
ROUND_DOWN: function (v) {
return v | 0;
},
NONE: function (v) {
return v;
},
/**
* Clamp the lowest value forcing the result to always be >= value
* @param {number} value
* @returns {function(number): number}
*/
clampMin: function (value) {
return function max(v) {
return max2(value, v);
}
},
/**
* Clamp the highest value, forcing the result to always be <= value
* @param {number} value
* @return {function(number): number}
*/
clampMax: function (value) {
return function min(v) {
return min2(v, value);
}
},
/**
* Allows us to chain multiple effects
*/
chain: chain
};
/**
* @readonly
* @type {boolean}
*/
Stat.prototype.isStat = true;
export default Stat;