namespace NeonSprawl.Server.Game.Combat; /// /// Orchestrates deterministic combat resolution: ability catalog damage + prototype target HP (NEO-81). /// Cast HTTP wiring: (NEO-82). /// NEO-84 telemetry hook sites: (ability_used, enemy_defeat). /// public static class CombatOperations { // --- Reserved telemetry hook sites (NEO-84): no emit until future modules --- // encounter_start — future E5.M3 encounter spawn/engage path (not wired in Slice 1). // player_death — future player HP / death resolution (player HP out of scope in Slice 1). /// /// Resolves one ability against one prototype combat target using catalog damage and the health store. /// Defeated targets deny before damage application; zero-damage abilities succeed without mutating HP. /// public static CombatResult TryResolve( string abilityId, string targetId, IAbilityDefinitionRegistry abilityRegistry, ICombatEntityHealthStore healthStore) { if (!abilityRegistry.TryNormalizeKnown(abilityId, out var normalizedAbility) || !abilityRegistry.TryGetDefinition(normalizedAbility, out var definition)) { return Deny(CombatReasonCodes.UnknownAbility); } if (!healthStore.TryGet(targetId, out var snapshot)) { return Deny(CombatReasonCodes.UnknownTarget); } if (snapshot.Defeated) { return Deny(CombatReasonCodes.TargetDefeated, snapshot.CurrentHp); } if (definition.BaseDamage == 0) { // --- Telemetry hook site (NEO-84): future E9.M1 catalog event `ability_used` --- // TODO(E9.M1): catalog emit — on every successful resolve (including zero-damage abilities). // Planned payload fields: abilityId, targetId, damageDealt (0), targetRemainingHp, targetDefeated. // playerId is not in engine scope; correlate route id at AbilityCastApi accept (NEO-30 funnel). return new CombatResult( Success: true, ReasonCode: null, DamageDealt: 0, TargetRemainingHp: snapshot.CurrentHp, TargetDefeated: snapshot.Defeated); } if (!healthStore.TryApplyDamage(targetId, definition.BaseDamage, out var after)) { return Deny(CombatReasonCodes.UnknownTarget); } if (after.Defeated) { // --- Telemetry hook site (NEO-84): future E9.M1 catalog event `enemy_defeat` --- // TODO(E9.M1): catalog emit — once per target defeat transition (lethal hit only; re-hit denied earlier). // Planned payload fields: targetId, abilityId (killing blow), damageDealt, final HP 0; correlate playerId at cast layer. } // --- Telemetry hook site (NEO-84): future E9.M1 catalog event `ability_used` --- // TODO(E9.M1): catalog emit — on every successful resolve after damage application. // Planned payload fields: abilityId, targetId, damageDealt, targetRemainingHp, targetDefeated. // playerId is not in engine scope; correlate route id at AbilityCastApi accept (NEO-30 funnel). return new CombatResult( Success: true, ReasonCode: null, DamageDealt: definition.BaseDamage, TargetRemainingHp: after.CurrentHp, TargetDefeated: after.Defeated); } private static CombatResult Deny(string reasonCode, int? targetRemainingHp = null) => new( Success: false, ReasonCode: reasonCode, DamageDealt: 0, TargetRemainingHp: targetRemainingHp, TargetDefeated: false); }