84 lines
3.7 KiB
C#
84 lines
3.7 KiB
C#
namespace NeonSprawl.Server.Game.Combat;
|
|
|
|
/// <summary>
|
|
/// Orchestrates deterministic combat resolution: ability catalog damage + prototype target HP (NEO-81).
|
|
/// Cast HTTP wiring: <see cref="AbilityInput.AbilityCastApi"/> (NEO-82).
|
|
/// NEO-84 telemetry hook sites: <see cref="TryResolve"/> (<c>ability_used</c>, <c>enemy_defeat</c>).
|
|
/// </summary>
|
|
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).
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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.
|
|
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);
|
|
}
|