Curitor
Gameplay Systems · Topic Guide

Stats & Effects

Stats are the hub the other systems plug into: combat subtracts from them, equipment boosts them, effects tick them, HUD bars read them, and the network mirrors them. You define a stat as an asset — everything else follows, including the networking.

Definitions and instances

A StatDefinition asset describes a stat: its id, its display info, its starting and minimum values, and how its maximum is computed. It is authored once, in the Project window, and shared by every character that uses it — Create → Party Core Kit → Stats → Stat Definition.

At runtime each character holds one StatInstance per definition: the live value, its current maximum, and a OnChanged event. The asset is never mutated at runtime — it is the recipe, not the pot. HUD widgets, combat and the network layer all read the instance and subscribe to its event; nothing polls.

There is deliberately no "vital stat" flag. Whether a stat shows on the HUD, whether it replicates, and what happens when it reaches zero are all configuration on the consuming side, not properties baked into the definition. The one thing the definition does decide is whether the stat crosses the wire, because that question has exactly one right answer per stat.

The kit ships four stat definitions you can use, copy or ignore — Health, Stamina, Level and Attack — plus two effect assets, Poison and Regeneration.

StatDefinition

FieldTypeDefaultWhat it does
idstringUnique machine-readable key. This is what CharacterStats.Get("health") looks up, and what save files and network payloads carry.
displayNamestringShown in UI and the debug HUD.
iconSpriteIcon for stat bars. Optional.
baseValuefloat100Starting current value when the character spawns.
minValuefloat0Minimum allowed current value — and, for health, the threshold that defines death: a character is alive while Current is above minValue.
maxFormulaMaxFormulaTypeFixedHow the maximum is computed. Fixed, PlusScalingStat or TimesScalingStat — see below.
baseMaxValuefloat100Base component of the max formula. For Fixed this is the entire maximum.
scalingStatStatDefinitionWhich other stat drives the scaling. Only used when maxFormula is not Fixed.
scaleFactorfloat10Multiplier applied to the scaling stat's current value before it is folded into the maximum.
displayMaxboolfalseHUD readouts show this stat's MAX instead of its current value. For power-style stats (attack) the maximum is the number that matters — base plus gear plus formula — while Current is unused. Vitals leave this off.
replicatedboolfalseMirror this stat's current value to every peer in online play. Set it on vitals other players can see or affect; leave it off for purely local stats such as level.

The three max formulas, written out, because "max health grows with level" being one dropdown instead of a script is most of the point:

Fixed             max = baseMaxValue
PlusScalingStat   max = baseMaxValue + scalingStat.Current * scaleFactor
TimesScalingStat  max = baseMaxValue * (1 + scalingStat.Current * scaleFactor)

On top of the formula, equipment adds a flat bonus — StatInstance.GearMaxBonus — so a chestplate that grants +20 max health does not have to know anything about level scaling, and level scaling does not have to know about chestplates.

Putting stats on a character

CharacterStats is the component that turns definitions into instances. The player prefab already carries one; anything else that should have stats — a bot, a destructible prop, a boss — gets one the same way.

FieldTypeDefaultWhat it does
groupCharacterGroupPlayerWhich side this character belongs to. Drives group-filtered features such as overhead health bars.
definitionsStatDefinition[]The stats this character has. Order matters online: replicated stats are matched between peers by their position in this list.
startingEffectsStatEffectDefinition[]Effects already active when the character spawns. Usually empty — gameplay applies effects at runtime.
knownEffectsStatEffectDefinition[]Every effect this character can RECEIVE over the network. Combat effect-on-hit payloads travel as ids and resolve against this list, so an effect that is not here cannot arrive from another machine.

The floating bar over a character's head is OverheadStatBar, and Setup builds one pointed at Health by default — its Stat field is empty, which means Health, so an already-installed project keeps its health bar exactly as it was. Assign a different StatDefinition to Stat and the bar follows that instead, resolved through CharacterStats.Get the same way StatBarUI already does — including hiding itself, rather than showing a frozen number, for a stat the character does not have or has disabled for the round.

For a stamina bar over every player, drop a second OverheadStatBarnext to the first on the player prefab's overhead canvas. Point its Stat field at Staminaand leave everything else — position, sprite, fill colours — at the stock bar's defaults.

Effects

A StatEffectDefinition applies a signed change-per-second to one stat while it is active — poison zones, regeneration buffs, a stamina drink. Effects show on the HUD with a countdown, refresh their duration when re-applied rather than stacking a second copy, and are composed onto the world with stock instructions, no code.

FieldTypeDefaultWhat it does
idstringUnique machine-readable key, e.g. 'poison'. This is what travels over the network.
displayNamestringShown in UI and the debug HUD.
iconSpriteIcon for effect lists. Optional.
targetStatStatDefinitionWhich stat this effect drains or restores while active.
changePerSecondfloat-5Signed change applied to the target stat each second. Negative drains it (a debuff), positive restores it (a buff).
durationfloat0Seconds until the effect expires on its own. 0 means it lasts until something explicitly removes it — for example leaving the volume that applied it.

The sign of changePerSecond is the only thing that makes an effect a buff or a debuff (IsHarmful reads it), and a duration of zero is how you build a zone effect: apply on enter, remove on exit, no timer involved.

Turning a stat off for a round or a zone

Some rounds want sprinting to be free. Some want nobody to be able to die. Rather than adding a flag to sprint and another to damage, a stat can simply be disabled, and one sentence describes what that means:

The same holds for health: with health disabled, CharacterStats.IsAlive reports true and damage has nowhere to land. a StatBarUI whose stat is disabled hides itself, because it subscribes to OnEnabledChanged rather than watching the value.

Suppression is held by source, not by a counter or a flag. A stat stays disabled while anyone is still asking for it to be — so a round-wide rule and a zone rule can overlap without one accidentally cancelling the other, and a trigger volume that fires twice for one entry (compound colliders do exactly that) cannot mis-count its way into a stuck state.

Two ways to reach it, both without code:

FieldTypeWhat it does
RoundController.disabledStatsStatDefinition[]Stats switched off for everyone while this round runs. Restored — and refilled — when the round ends. Add Stamina for a round where sprinting is free; add Health for one nobody can lose.
Set Stat EnabledinstructionThe stock scripting instruction. Put it on a trigger volume's enter and exit lists to scope the rule to an area instead of a whole round.

Composing with stats, without code

Four stock instructions cover the common cases, and they take a StatDefinition asset rather than a string, so a typo is not a possible bug:

FieldTypeWhat it does
Modify StatinstructionAdds a signed amount to a stat. The amount is a resolving number property, so it can be a constant or read from elsewhere.
Apply Stat EffectinstructionStarts an effect on the character. Re-applying refreshes its duration rather than stacking.
Remove Stat EffectinstructionEnds an effect early — the exit half of a zone effect.
Set Stat EnabledinstructionDisables or re-enables a stat, held by this instruction's source.

A poison pool, start to finish, is four clicks:

  1. Add a trigger volume

    Put a TriggerBehaviour on a collider marked as a trigger, covering the pool.
  2. On enter, apply the effect

    Add an Apply Stat Effect instruction to the enter list and assign the Poison asset.
  3. On exit, remove it

    Add a Remove Stat Effect instruction to the exit list with the same asset. Because Poison ships with a duration of 0, leaving the pool is what ends it.
  4. Press play

    The effect appears in the HUD effect list while the player stands in it, and health drains at the asset's changePerSecond.

Reading and writing stats from code

Where you do want code, the surface is small. Get the instance once and subscribe — do not re-resolve it, because the instance stays valid and identical for the life of the character, including across being disabled and re-enabled.

using Curitor.PartyCoreKit.Stats;
using UnityEngine;

public class LowHealthWarning : MonoBehaviour
{
    [SerializeField] private CharacterStats stats;

    private StatInstance _health;

    private void OnEnable()
    {
        _health = stats.Health;              // or stats.Get("health")
        if (_health == null) return;

        _health.OnChanged += HandleChanged;
        _health.OnEnabledChanged += HandleEnabledChanged;
    }

    private void OnDisable()
    {
        if (_health == null) return;

        _health.OnChanged -= HandleChanged;
        _health.OnEnabledChanged -= HandleEnabledChanged;
    }

    // Ratio is Current / Max, already clamped to 0..1.
    private void HandleChanged(float previous, float current)
    {
        bool critical = _health.Ratio < 0.25f;
        // ... drive a vignette, a heartbeat sound, whatever you like
    }

    // A disabled stat is not "at zero" — it is absent. Hide, do not warn.
    private void HandleEnabledChanged(bool enabled)
    {
        gameObject.SetActive(enabled);
    }
}

To change a value, call Modify on the instance rather than writing to it. That single call is what routes the change to the machine allowed to make it when the game is online — see below.

stats.Health.Modify(-25f);        // damage, routed correctly in every mode
stats.ConsumeStamina(10f);        // returns silently when stamina is disabled
stats.AddEffect(poisonAsset);     // refreshes duration if already active
stats.DisableStat(staminaAsset, this);   // held by 'this' until released

Multiplayer