Auto-Targeting
Server-only auto-attack driver: enrolled attackers acquire a target (ITargetSelector) and attack on a cadence (IAttackAction) over World — survivors/auto-shooter auto-fire, turrets, sentries, pet auto-attack. Genre-neutral defaults (nearest-in-cone selection, two-faction PvE hostility, inert attack); zero wire.
Seams you implement 4
Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayTargeting(); and it wins.
IAttackAction game SPI
Effect seam: what does an attack DO? The piece decides WHEN to attack and WHO; the meaning — a projectile volley, a melee hit, a hitscan ray, a chain, a heal — is the game's. The default NullAttackAction is inert (targets are still acquired; nothing fires). A generic projectile-firing action ships as a Hosting bridge (over the Projectiles piece); games register their own for melee / hitscan / bespoke effects. Runs on the poll thread — keep it allocation-free.
-
void Attack(WorldEntity attacker, WorldEntity target)Perform one attack from attacker against target.
IAttackCadenceProvider game SPI
Per-attacker fire-cadence seam: how many seconds between THIS attacker's shots right now? Targeting asks it when scheduling each attacker's next shot; a game supplies per-weapon or upgraded fire rates — typically from a Stats-backed Hosting bridge — without Targeting ever knowing what a "weapon" is. Absent (no game registers one), every attacker uses the global CooldownSeconds — today's behavior exactly. The analogue of Movement's per-entity speed seam, for the fire-rate axis.
-
bool TryGetCooldown(long attackerEntityId, out double cooldownSeconds)The seconds between shots for this attacker, or false to use the options cooldown.
ITargetHostility game SPI
Faction seam: is a candidate a valid target for an attacker? The piece never decides who is an enemy — a game's teams / PvE / neutral rules do. The default TwoFactionHostility treats players and server entities as opposing sides (the common PvE shape); register your own BEFORE AddCrossplayTargeting for teams, friendly-fire, or PvP.
-
bool IsHostile(WorldEntity attacker, WorldEntity candidate)True if attacker may target candidate.
ITargetSelector game SPI
Acquisition seam: which entity (if any) should this attacker attack right now? The default NearestInConeSelector picks the nearest hostile within range (and cone, when the options narrow it) using the world's spatial queries. Register your own BEFORE AddCrossplayTargeting for lowest-health / highest-threat / priority-tag targeting.
-
bool TrySelectTarget(WorldEntity attacker, TargetingContext context, out WorldEntity target)Selects a target for attacker, or returns false if none.
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
ITargetingService
Enrolls entities as auto-attackers and, on a cadence, drives each to acquire a target and attack. Attach any entity explicitly (turret / pet / specific NPC), or set AutoAttachPlayers to auto-enroll players. Detach is automatic on entity removal. The mechanism is genre-neutral; WHO is hostile and WHAT an attack does are seams (ITargetHostility / IAttackAction).
-
bool Attach(long entityId)Enrolls an entity as an attacker (idempotent).
-
int Count { get; }Number of live attackers (diagnostics).
-
bool Detach(long entityId)Stops driving an entity.
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
TargetingOptions
Targeting tuning — defaults live here, never inline in the service (rule 4).
-
bool AutoAttachPlayers { get; set; }When true, every player entity is auto-enrolled as an attacker on world entry (the survivors-like / auto-shooter default). When false, the game enrolls attackers explicitly via Attach (turrets, sentries, specific NPCs, pets).
-
float ConeDegrees { get; set; }Firing arc, degrees. 360 = all directions (the attacker can hit any target around it — the auto-shooter default); < 360 = only targets within this cone of the attacker's facing (turrets, directional weapons).
-
double CooldownSeconds { get; set; }Seconds between attacks per attacker — the global fire rate.
-
ushort CooldownStatId { get; set; }Per-attacker fire-rate stat (0 = use the global CooldownSeconds). When set, the attacker's value of this stat is its cooldown in seconds — so per-weapon fire rates and fire-rate upgrades ride the ordinary Stats wire (a Hosting bridge reads it via IAttackCadenceProvider). Mirrors AbilityDefinition.CooldownStatId.
-
bool ExcludeNonCombatants { get; set; }Exclude the framework's own transient NON-COMBATANT entity classes — in-flight projectiles and loot pickups — from being valid targets, so stock auto-fire never shoots a bolt or a dropped coin. Enforced by the Hosting composition when Loot/Projectiles are present; a game's ITargetHostility then only expresses real faction/team rules. Default true (the sensible default); set false to restore the classic "any un-owned entity is a target" behavior.
-
int MaxUpdatesPerTick { get; set; }Cap on attacker evaluations per server tick — a crowd of attackers degrades re-acquisition rate instead of the tick (rule 11: unbounded work gets a budget).
-
float Range { get; set; }Acquisition range, world units.