Stats
Stats gives an entity numbers that items, upgrades and buffs change: a speed, a jump, a launch. You declare each stat’s base, add modifiers, and read the result. The engine does the maths the same way every time.
Its props are in StatsProps. Each prop is one stat, named by the prop: <Stats speed={{ base: 6, max: 12 }} />.
The formula
Section titled “The formula”clamp((base + Σflat) × (1 + Σpercent) × Π(1 + more), min, max)A StatModifier sets one or more of three kinds:
flatadds to the base.percentsums with the stat’s other percents.0.2is +20%.moremultiplies with the stat’s other mores. Two mores of0.5make ×2.25, where two percents of0.5make ×2.
On a base of 1000, two percent: 1 and one more: 1 give 1000 × 3 × 2 = 6000. With all three as percent, they give 4000.
The base never changes under a modifier. min and max clamp the value when it is read, never the base, so a modifier that ends restores the value it hid.
The verbs
Section titled “The verbs”All take the entity and come from @daniel-zarinski/engine/core, so a system and a component call them alike:
addStatModifier(entity, name, modifier)adds a modifier to a stat.removeStatModifiers(entity, source)removes every modifier thatsourceadded, on every stat: an item taken off, a buff cancelled.readStat(entity, name)returns the resolved value, orundefinedwhere the entity has no stat by that name.setStatBase(entity, name, base)changes the base and keeps the bounds and the modifiers.resolveStat(stat)applies the formula to a stat you already hold, such as one fromuseTrait(entity, StatsTrait).
A modifier with seconds lasts that many simulated seconds. The step counts it down and drops it the step it runs out. A modifier without seconds stays until its source is removed.
Bases and modifiers are plain data, and a source is a string, so a save or a room can carry them as they are.
Where it runs
Section titled “Where it runs”Server. A stat has a consequence: it decides how fast, how high, how hard.
The step drops spent modifiers before the behaviours run, so every behaviour in a step reads the same value.
What other engines do
Section titled “What other engines do”| Engine or game | Kinds | Formula |
|---|---|---|
| Unreal 5 GAS | AddBase, MultiplyAdditive, MultiplyCompound, AddFinal, Override | ((Base + AddBase) × MulAdd × MulCompound) + AddFinal |
| Unity, the common Kryzarel pattern | Flat, PercentAdd, PercentMult | (base + Σflat) × (1 + Σpct) × Π(1 + mult) |
| Path of Exile, League of Legends | flat, increased, more | the same three buckets |
| Godot, the godot-attributes addon | add, multiply | ordered calculators |
| Roblox, PlayCanvas | none built in | none |
Stats keeps the three buckets that Unreal 5, Unity’s common pattern, Path of Exile and League share, and leaves out Unreal’s Override and AddFinal. It keeps Unreal’s split between a permanent base and temporary modifiers. Unlike Unreal, which clamps in two places, it clamps once, when the value is read. The name more is Path of Exile’s.
An item adds its modifiers when it is equipped and removes them by its source when it comes off:
import { useEffect } from "react";import { Entity, Stats, addStatModifier, removeStatModifiers, useEntity,} from "@daniel-zarinski/engine";
function Slingshot() { const sled = useEntity(); useEffect(() => { addStatModifier(sled, "launch", { source: "slingshot", more: 0.04 }); return () => removeStatModifiers(sled, "slingshot"); }, [sled]); return null;}
export function Sled() { return ( <Entity> <Stats launch={{ base: 20, min: 0 }} /> <Slingshot /> </Entity> );}