Combat

Stats

Numbered clamped stats with thresholds and regen.

Server
services.AddCrossplayStats();
Unity package
com.crossplay.stats
Depends on
world

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> StatDepleted

    A 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.

2301 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.

2302 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.

2303 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> StatChanged

    A 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 SmoothingSeconds

    Seconds 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.