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.
313 lines (312 loc) • 11.9 kB
JavaScript
/**
* @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();
}
}
export default TinyNeedBar;