UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

342 lines (285 loc) • 7.48 kB
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;