Stats
Numbered clamped stats with thresholds and regen.
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
IStatsService
Server-authoritative numbered stats on world entities — the resource layer every combat style shares (health, stamina, shields, meter, ammo…). The framework clamps to [0, max], replicates changes to observers (full replay on interest reveal, like EntityState), raises a typed event when a stat hits zero, and applies per-stat regen on its tick. WHAT any stat id means — and every formula that changes one — is the game's, through this API. Stats are SERVER-WRITTEN: there is no client "set my health" message (the wire is push-only). The game calls Set / Modify from its own logic (an ability's damage, a heal, a pickup) and the framework replicates the result to everyone who can see the entity.
-
float Modify(long entityId, ushort statId, float delta)Adds delta (clamped). Returns the new value; NaN if undefined.
-
void Set(long entityId, ushort statId, float value, float max)Defines/overwrites a stat on an entity (value clamped to [0, max]).
-
void SetRegen(long entityId, ushort statId, float perSecond)Per-second regeneration (negative = decay). Applied on the regen tick while defined.
-
event Action<long, ushort> StatDepletedA stat just reached exactly 0 (entityId, statId) — death/exhaustion is the game's call.
-
bool TryGet(long entityId, ushort statId, out float value, out float max)Reads a stat.
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
StatsOptions
Configurable stats parameters (defaults here; never inlined in logic).
-
Dictionary<ushort, StatAllocationRule> Allocations { get; set; }Allocatable point pools (donor-gap E1), keyed by the TARGET stat id: spending N points of the rule's pool stat adds N × the rule's per-point value/max to the target — strength points, perk points, ship upgrades as data. Points live AS a stat (grant them via Modify on level-up), so pools replicate for free. Empty (the default) = the allocation verb is inert.
-
Dictionary<byte, Dictionary<ushort, StatSeed>> EntityTemplates { get; set; }Per-kind initial stat templates, keyed by an entity's appearance-kind byte (appearance[0]) → statId → initial seed. Pure DATA the composition layer applies on spawn — the piece itself never watches the world. Empty (the default) = inert.
-
Dictionary<ushort, float> FlagDeltaAbove { get; set; }Cheat heuristic: per-stat ceilings on a SINGLE mutation's |delta| — anything beyond is flagged to the anomaly sink (never blocked; consequence is the game's). Empty = off. "Impossible instant gains" in the game's own numbers.
-
int MaxStatsPerEntity { get; set; }Most distinct stats per entity.
-
float RegenIntervalSeconds { get; set; }Seconds between regen applications (regen itself is per-stat, set by the game).
Wire messages 3
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
EntityStatsChanged Server → observers (and the owner): stat values that changed — sent on change and replayed in full whenever the entity enters an observer's view, so late arrivals see the truth.
AllocateStatRequest Client → server: "spend Points from the rule's pool into TargetStatId". Strength points, skill points, perk points, ship upgrades — the piece moves numbers between stats by a configured exchange; the meaning is the game's.
AllocateStatResponse Server → actor: the allocation verdict.
Unity services you inject 1
Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.
IStatsClient
The Stats piece, client side: every visible entity's numbered stats, pushed on change and replayed on reveal. Map ids onto your own bars/meters — the framework never says "health". Smoothing: TryGetDisplay returns a value that eases toward each server push instead of snapping, so a health bar slides. It's presentation only — TryGet always gives the exact authoritative number; the HUD reads the display value each frame.
-
event Action<long, ushort, float, float> StatChangedA stat changed on a visible entity: (entityId, statId, value, max).
-
bool TryGet(long entityId, ushort statId, out float value, out float max)The exact latest authoritative value for an entity's stat.
-
bool TryGetDisplay(long entityId, ushort statId, out float value, out float max)The SMOOTHED display value (eased toward the latest push over SmoothingSeconds).
-
float SmoothingSecondsSeconds a display value takes to converge on a new push (0 = snap).
UI views you can replace 1
Each ships a working uGUI panel you can drag into a scene. Substitute your own view to keep the logic and change the look — the presenter never knows.
ICrossplayStatsView
Narrow contract the presenter drives. UI-only: render calls in. The Stats wire is pure server push (there is no client request path), so this view raises no intents.