Status Effects
Timed opaque markers (buffs/debuffs) applied to entities.
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.
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[]> EffectsChangedAn 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.