Combat

Status Effects

Timed opaque markers (buffs/debuffs) applied to entities.

Server
services.AddCrossplayStatusEffects();
Unity package
com.crossplay.statuseffects
Depends on
world

Seams you implement 1

Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayStatusEffects(); and it wins.

IStatusEffectRules game SPI

SPI the game implements to give effects their meaning — a burning DoT modifies a stat on tick, a stun gates ability rules, a shield changes damage math. Default: pure timed markers. Register your own BEFORE AddCrossplayStatusEffects (a TryAdd seam). All callbacks fire on the poll thread; the framework already handles the timers and replication — the rules only apply CONSEQUENCE.

sealed class EffectRules : IStatusEffectRules
{
    public void OnApplied(long id, ushort effectId, byte[] data) { }
    public void OnTick(long id, ushort effectId, byte[] data, float seconds)
    {
        if (effectId == Burning) _stats.Modify(id, HealthStatId, -5 * seconds); // DoT
    }
    public void OnExpired(long id, ushort effectId) { }
}
// services.AddSingleton<IStatusEffectRules, EffectRules>(); // BEFORE AddCrossplayStatusEffects()
  • void OnApplied(long entityId, ushort effectId, byte[] data)

    An effect landed (fresh or refreshed) — data is the opaque payload it was applied with.

  • void OnExpired(long entityId, ushort effectId)

    The effect ended (expired on its timer or removed early) — undo any lingering consequence.

  • void OnTick(long entityId, ushort effectId, byte[] data, float intervalSeconds)

    The periodic tick, once per intervalSeconds per live effect (apply DoT/aura math here).

Services you call 1

Crossplay implements these. Resolve them from DI and call them from your own systems.

IStatusEffectService

Server-side API for applying timed effects to world entities — the game calls this from its own logic (an ability that stuns, a trap that poisons); the framework runs the timers, replication, and expiry.

  • bool Apply(long entityId, ushort effectId, float durationSeconds, byte[] data = null)

    Applies (or refreshes — same id replaces) an effect for a duration. False when the duration is ≤ 0, the entity is unknown, or the per-entity effect cap is reached.

  • bool Has(long entityId, ushort effectId)

    True while the entity carries the effect (rules use this to gate: stunned?).

  • bool Remove(long entityId, ushort effectId)

    Removes an effect early. False when not present.

Configuration 1

Every tunable lives in an options object — there are no magic numbers to hunt for.

StatusEffectOptions

Configurable status-effect parameters (defaults here; never inlined in logic).

  • int MaxEffectsPerEntity { get; set; }

    Most simultaneous effects per entity.

  • float TickIntervalSeconds { get; set; }

    Seconds between periodic-tick callbacks into the rules (DoTs, auras…).

Wire messages 1

The protocol this piece speaks. Ids are allocated per piece so they can never collide.

2501 EntityEffects

Server → observers (and the owner): the entity's full current effect list — sent on any change and replayed on interest reveal, so late arrivals render the right state.

Unity services you inject 1

Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.

IStatusEffectsClient

The StatusEffects piece, client side: every visible entity's current timed effects (opaque, game-defined), pushed on change and replayed on reveal — drive your own icons/particles/tints. Smoothing: RemainingOf counts each effect's timer DOWN locally between server pushes, so an on-screen duration ticks smoothly instead of jumping once a second. The next server push re-syncs it. Presentation only; the server owns expiry.

  • event Action<long, EffectInstance[]> EffectsChanged

    An entity's effect list changed (the full current list).

  • EffectInstance[] EffectsOf(long entityId)

    The latest known effect list for an entity (empty when none).

  • float RemainingOf(long entityId, ushort effectId)

    An effect's remaining seconds, decremented locally since the last push (0 when absent).

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.

ICrossplayStatusEffectsView

Narrow contract the presenter drives. UI-only: render calls in. The StatusEffects wire is pure server push (there is no client request path), so this view raises no intents.