Hostility (PvP Flags)
WHO MAY HARM WHOM, answered in one place: consent stances (opt-in verb), combat flags on aggression, escalation tiers on innocent kills, safe zones — one IHarmResolver seam every combat consumer asks (Crossplay:Hostility:DriveTargeting bridges Targeting). PvE pairs pass through untouched; defaults = the classic no-PvP baseline.
Providers you can swap 1
Infrastructure seams. Crossplay ships a working implementation of each — replacing one changes where data lives or how it moves, never a game rule.
IHarmResolver provider
THE seam combat consumers ask: may attacker A harm target B right now? The Hostility piece provides the standard resolver (stances + flags + tiers + safe zones); a game with its own vocabulary registers its own resolver BEFORE AddCrossplayHostility and every consumer (Targeting bridge, ability gates, the game's rules) follows it instead.
-
bool CanHarm(WorldEntity attacker, WorldEntity target)True when the attacker may harm the target.
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
IHostilityService
Server-side Hostility API — the state machine around IHarmResolver: stances, combat flags, escalation tiers. Feed it from your combat flow (NotifyHarm / NotifyKill — the projectile bridge feeds harm automatically) and read it anywhere.
-
void NotifyHarm(long attackerId, long targetId)Records "attacker harmed target". When the target was an unflagged, clean player, the ATTACKER gets combat-flagged for the configured window. Call it from your damage flow (the projectile bridge does it for catalog projectiles).
-
void NotifyKill(long attackerId, long victimId)Records "attacker killed victim". When the victim was innocent (unflagged, tier 0), the attacker's escalation tier rises by one. Call it from your death flow.
-
void SetStance(long entityId, byte stance)Sets an entity's stance server-side (no policy — the server is authoritative).
-
event Action<long, byte, float, ushort> StateChangedRaised when an entity's visible state changes: (entity id, stance, flag seconds, tier).
-
bool TryGetState(long entityId, out byte stance, out float flagSeconds, out ushort tier)The entity's current state (false = no record: a clean, passive entity).
-
bool TrySetStance(ISession session, byte stance, out ushort reasonCode)The stance verb behind SetStanceRequest: options gate → flag gate → change.
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
HostilityOptions
Configurable hostility parameters (defaults here; never inlined in logic).
-
bool AllowStanceChanges { get; set; }Whether clients may change their own stance (consent PvP). Off (the default) the stance verb refuses and stances only move server-side (a game verb, a duel flow).
-
bool DriveTargeting { get; set; }When true (via the Hosting composition), the Targeting piece's hostility seam is bridged to this resolver. The piece itself never reads this — it documents the switch.
-
float FlagOnHarmSeconds { get; set; }Seconds an attacker stays combat-flagged after harming another player (0 = the flag mechanic is off). While flagged, anyone may harm you back and you cannot lower your stance — the classic "you started it" window.
-
List<string> SafeZones { get; set; }Zone keys where player-vs-player harm is always refused (safe towns, lobbies, staging areas). Finer-grained areas are the game's own IHarmResolver.
-
float TierDecaySeconds { get; set; }Seconds one escalation tier takes to decay (0 = tiers never decay). Tiers rise through NotifyKill when the victim was innocent.
Wire messages 3
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
HostilityChanged Server → observers + the entity: the visible hostility state. One game tints a nameplate red, another shows a karma icon, another plays a siren — the piece cannot tell.
SetStanceRequest Client → server: "set my stance to S" — consent PvP. WHEN this is allowed (safe zone only? cooldown? shrine?) is the game's policy.
SetStanceResponse Server → actor: the stance verdict.
Unity services you inject 1
Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.
IHostilityClient
The Hostility piece, client side: set your PvP stance (consent opt-in, when the server allows it) and observe EVERY entity's hostility state — stance, combat flag, escalation tier — broadcast on change and replayed on interest reveal. Bind EntityHostilityChanged to nameplates (red names, flag icons, karma marks); the harm checks themselves run server-side.
-
void SetStance(byte stance)Set my stance (see HostilityStances; the server's policy validates).
-
event Action<SetStanceResponse> StanceResultReceivedThe server's verdict on your stance change.
-
event Action<HostilityChanged> EntityHostilityChangedAn entity's hostility state changed (or was revealed): render it.