Combat

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.

Server
services.AddCrossplayHostility();
Unity package
com.crossplay.hostility
Depends on
world

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

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

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

6102 SetStanceRequest

Client → server: "set my stance to S" — consent PvP. WHEN this is allowed (safe zone only? cooldown? shrine?) is the game's policy.

6103 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> StanceResultReceived

    The server's verdict on your stance change.

  • event Action<HostilityChanged> EntityHostilityChanged

    An entity's hostility state changed (or was revealed): render it.