Skip to content

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 }} />.

clamp((base + Σflat) × (1 + Σpercent) × Π(1 + more), min, max)

A StatModifier sets one or more of three kinds:

  • flat adds to the base.
  • percent sums with the stat’s other percents. 0.2 is +20%.
  • more multiplies with the stat’s other mores. Two mores of 0.5 make ×2.25, where two percents of 0.5 make ×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.

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 that source added, on every stat: an item taken off, a buff cancelled.
  • readStat(entity, name) returns the resolved value, or undefined where 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 from useTrait(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.

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.

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>
);
}