UNPKG

tiny-essentials

Version:

Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.

346 lines (310 loc) 11.1 kB
'use strict'; /** * @typedef {Object} TickResult * @property {number} prevValue - Infinite value before applying decay. * @property {number} removedTotal - Total amount removed this tick. * @property {number} removedPercent - Percentage of max removed this tick. * @property {number} currentPercent - Current percentage relative to max. * @property {number} remainingValue - Current clamped value (≥ 0). * @property {number} infiniteRemaining - Current infinite value (can be negative). */ /** * Represents a decay factor applied to the need bar. * * - `amount`: base value reduced per tick. * - `multiplier`: multiplier applied to the amount. * * @typedef {Object} BarFactor * @property {number} amount - Base reduction value per tick. * @property {number} multiplier - Multiplier applied to the amount. */ /** * Represents the serialized state of a TinyNeedBar instance. * * This object is typically produced by {@link TinyNeedBar#toJSON} and * can be used to recreate an instance via {@link TinyNeedBar.fromJSON}. * * @typedef {Object} SerializedData * @property {number} maxValue - Maximum value of the bar at the moment of serialization. * @property {number} currentValue - Current clamped value (never below 0). * @property {number} infiniteValue - Infinite value (can go negative). * @property {Record<string, BarFactor>} factors - Active decay factors indexed by their keys. */ /** * A utility class to simulate a "need bar" system. * * The bar decreases over time according to defined factors (each with an amount and multiplier). * - The **main factor** controls the base decay per tick. * - Additional factors can be added dynamically. * * The system tracks two values: * - `currentValue` → cannot go below zero. * - `infiniteValue` → can decrease infinitely into negative numbers. */ class TinyNeedBar { /** * Stores all factors that influence decay. * Each entry contains an amount and a multiplier. * @type {Map<string, BarFactor>} */ #factors = new Map(); /** Maximum value of the bar. @type {number} */ #maxValue; /** Current clamped value of the bar (never below 0). @type {number} */ #currentValue; /** Current "infinite" value of the bar (can go negative). @type {number} */ #infiniteValue; /** * Returns a snapshot of all currently active factors. * Each factor is returned as a plain object to prevent direct mutation of the internal map. * * @returns {Record<string, BarFactor>} A record of all factors indexed by their key. */ get factors() { /** @type {Record<string, BarFactor>} */ const factors = {}; for (let [name, factor] of this.#factors.entries()) { factors[name] = { ...factor }; } return factors; } /** * Returns the current percentage of the bar relative to the maximum value. * * @returns {number} Percentage from `0` to `100`. */ get currentPercent() { return (this.#currentValue / this.#maxValue) * 100; } /** * Updates the maximum possible value of the bar. * Ensures `currentValue` never exceeds the new maximum. * * @param {number} value - New maximum value. */ set maxValue(value) { if (typeof value !== 'number' || Number.isNaN(value) || value <= 0) throw new TypeError('maxValue must be a positive number.'); this.#maxValue = value; this.#currentValue = Math.min(this.#currentValue, value); } /** * Returns the maximum possible value of the bar. * * @returns {number} The maximum value. */ get maxValue() { return this.#maxValue; } /** * Returns the current clamped value of the bar. * This value will never be below `0`. * * @returns {number} Current value (≥ 0). */ get currentValue() { return this.#currentValue; } /** * Updates the infinite value of the bar. * Automatically recalculates the `currentValue` (never below 0). * * @param {number} value - New infinite value. */ set infiniteValue(value) { if (typeof value !== 'number' || Number.isNaN(value)) throw new TypeError('infiniteValue must be a number.'); this.#infiniteValue = value; this.#currentValue = Math.max(0, value); this.#currentValue = Math.min(this.#currentValue, this.#maxValue); } /** * Returns the current infinite value of the bar. * Unlike `currentValue`, this one can go below zero. * * @returns {number} Current infinite value. */ get infiniteValue() { return this.#infiniteValue; } /** * Creates a new need bar instance. * * @param {number} [maxValue=100] - Maximum value of the bar. * @param {number} [baseDecay=1] - Base amount reduced each tick. * @param {number} [baseDecayMulti=1] - Multiplier applied to the base decay. */ constructor(maxValue = 100, baseDecay = 1, baseDecayMulti = 1) { if (typeof maxValue !== 'number' || Number.isNaN(maxValue) || maxValue <= 0) throw new TypeError('maxValue must be a positive number.'); this.#maxValue = maxValue; this.setFactor('main', baseDecay, baseDecayMulti); this.#currentValue = maxValue; this.#infiniteValue = maxValue; } /** * Retrieves a specific factor by its key. * * @param {string} key - The unique key of the factor. * @returns {BarFactor} The requested factor object. * @throws {Error} If the factor does not exist. */ getFactor(key) { if (typeof key !== 'string' || !key) throw new TypeError('Key must be a non-empty string.'); const result = this.#factors.get(key); if (!result) throw new Error(`Factor with key "${key}" not found.`); return { ...result }; } /** * Checks if a specific factor exists by key. * * @param {string} key - The factor key to check. * @returns {boolean} `true` if the factor exists, otherwise `false`. */ hasFactor(key) { if (typeof key !== 'string' || !key) throw new TypeError('Key must be a non-empty string.'); return this.#factors.has(key); } /** * Defines or updates a decay factor. * * @param {string} key - Unique identifier for the factor. * @param {number} amount - Amount reduced per tick. * @param {number} [multiplier=1] - Multiplier applied to the amount. */ setFactor(key, amount, multiplier = 1) { if (typeof key !== 'string' || !key) throw new TypeError('Key must be a non-empty string.'); if (typeof amount !== 'number' || Number.isNaN(amount)) throw new TypeError('Amount must be a valid number.'); if (typeof multiplier !== 'number' || Number.isNaN(multiplier)) throw new TypeError('Multiplier must be a valid number.'); this.#factors.set(key, { amount, multiplier }); } /** * Removes a decay factor by its key. * * @param {string} key - The factor key to remove. * @returns {boolean} Returns `true` if the factor existed and was successfully removed, * or `false` if the factor did not exist. */ removeFactor(key) { if (typeof key !== 'string' || !key) throw new TypeError('Key must be a non-empty string.'); return this.#factors.delete(key); } /** * Applies a total decay value to the bar and updates both current and infinite values. * This is a private helper used by the public tick methods. * * @param {number} removedTotal - The total amount to remove from the bar during this tick. * @returns {TickResult} An object containing detailed information about the tick. */ #tick(removedTotal) { const prevValue = this.#infiniteValue; this.#infiniteValue -= removedTotal; this.#currentValue = Math.max(0, this.#currentValue - removedTotal); const removedPercent = (removedTotal / this.#maxValue) * 100; return { prevValue, removedTotal, removedPercent, currentPercent: this.currentPercent, remainingValue: this.#currentValue, infiniteRemaining: this.#infiniteValue, }; } /** * Executes one tick of decay, applying all active factors. * * @returns {TickResult} */ tick() { let removedTotal = 0; for (let [_, factor] of this.#factors.entries()) { removedTotal += factor.amount * factor.multiplier; } return this.#tick(removedTotal); } /** * Executes one tick of decay using a temporary factor. * * @param {BarFactor} tempFactor - Temporary factor to apply only in this tick. * @returns {TickResult} */ tickWithTempFactor(tempFactor) { if (typeof tempFactor !== 'object' || tempFactor === null) throw new TypeError('You must provide a valid factor object.'); if (typeof tempFactor.amount !== 'number' || Number.isNaN(tempFactor.amount)) throw new TypeError('Temp factor "amount" must be a valid number.'); if ( 'multiplier' in tempFactor && (typeof tempFactor.multiplier !== 'number' || Number.isNaN(tempFactor.multiplier)) ) throw new TypeError('Temp factor "multiplier" must be a valid number if provided.'); let removedTotal = 0; for (let [_, factor] of this.#factors.entries()) { removedTotal += factor.amount * factor.multiplier; } if (tempFactor) removedTotal += tempFactor.amount * (tempFactor.multiplier ?? 1); return this.#tick(removedTotal); } /** * Executes one tick using only the specified factor. * * @param {BarFactor} factor - The single factor to apply. * @returns {TickResult} */ tickSingleFactor(factor) { if (typeof factor !== 'object' || factor === null) throw new TypeError('You must provide a valid factor object.'); if (typeof factor.amount !== 'number' || Number.isNaN(factor.amount)) throw new TypeError('Temp factor "amount" must be a valid number.'); if ( 'multiplier' in factor && (typeof factor.multiplier !== 'number' || Number.isNaN(factor.multiplier)) ) throw new TypeError('Temp factor "multiplier" must be a valid number if provided.'); if (!factor) throw new Error('You must provide a factor to apply.'); const removedTotal = factor.amount * (factor.multiplier ?? 1); return this.#tick(removedTotal); } /** * Serializes the current state of the need bar. * @returns {SerializedData} */ toJSON() { return { maxValue: this.#maxValue, currentValue: this.#currentValue, infiniteValue: this.#infiniteValue, factors: this.factors, }; } /** * Restores a need bar from a serialized object. * @param {SerializedData} data * @returns {TinyNeedBar} */ static fromJSON(data) { const bar = new TinyNeedBar(data.maxValue, 0, 0); bar.infiniteValue = data.infiniteValue; bar.#factors.clear(); for (const [key, factor] of Object.entries(data.factors)) { bar.setFactor(key, factor.amount, factor.multiplier); } return bar; } /** * Creates a deep clone of this need bar. * @returns {TinyNeedBar} */ clone() { return TinyNeedBar.fromJSON(this.toJSON()); } /** * Clear the factors map, clearing all factor data. */ clearFactors() { this.#factors.clear(); } } module.exports = TinyNeedBar;