CombatOutcome.cs
1 using GUNRPG.Core.Equipment; 2 using GUNRPG.Core.Operators; 3 4 namespace GUNRPG.Application.Combat; 5 6 /// <summary> 7 /// Represents the final, authoritative result of a completed combat session. 8 /// This is the only allowed handoff from combat (infil) to operator progression (exfil). 9 /// 10 /// CombatOutcome is: 11 /// - Explicit: Contains everything Exfil needs 12 /// - Immutable: Properties are get-only, set via constructor 13 /// - Pure data: No domain behavior 14 /// - Free of service logic: Just a data contract 15 /// 16 /// Combat logic must never mutate operator state. CombatOutcome must be producible 17 /// deterministically from a completed session. 18 /// </summary> 19 public sealed class CombatOutcome 20 { 21 /// <summary> 22 /// The combat session that produced this outcome. 23 /// </summary> 24 public Guid SessionId { get; } 25 26 /// <summary> 27 /// The operator ID involved in combat. 28 /// </summary> 29 public OperatorId OperatorId { get; } 30 31 /// <summary> 32 /// Whether the operator died during combat. 33 /// </summary> 34 public bool OperatorDied { get; } 35 36 /// <summary> 37 /// Experience points gained during combat. 38 /// </summary> 39 public int XpGained { get; } 40 41 /// <summary> 42 /// Gear lost during combat (empty if none lost). 43 /// </summary> 44 public IReadOnlyCollection<GearId> GearLost { get; } 45 46 /// <summary> 47 /// Whether the combat was a victory (operator survived and enemy defeated). 48 /// Optional metadata for context. 49 /// </summary> 50 public bool IsVictory { get; } 51 52 /// <summary> 53 /// Number of turns the operator survived. 54 /// Optional metadata for context. 55 /// </summary> 56 public int TurnsSurvived { get; } 57 58 /// <summary> 59 /// Amount of damage taken during combat. 60 /// Optional metadata for context. 61 /// </summary> 62 public float DamageTaken { get; } 63 64 /// <summary> 65 /// When the combat ended. 66 /// Optional metadata for context. 67 /// </summary> 68 public DateTimeOffset CompletedAt { get; } 69 70 public CombatOutcome( 71 Guid sessionId, 72 OperatorId operatorId, 73 bool operatorDied, 74 int xpGained, 75 IReadOnlyCollection<GearId> gearLost, 76 bool isVictory = false, 77 int turnsSurvived = 0, 78 float damageTaken = 0f, 79 DateTimeOffset? completedAt = null) 80 { 81 if (xpGained < 0) 82 throw new ArgumentException("XP gained cannot be negative", nameof(xpGained)); 83 if (turnsSurvived < 0) 84 throw new ArgumentException("Turns survived cannot be negative", nameof(turnsSurvived)); 85 if (damageTaken < 0) 86 throw new ArgumentException("Damage taken cannot be negative", nameof(damageTaken)); 87 88 // Enforce invariant: victory requires operator survival 89 if (isVictory && operatorDied) 90 throw new ArgumentException("Cannot have victory when operator died (victory requires operator survival and enemy defeat)", nameof(isVictory)); 91 92 SessionId = sessionId; 93 OperatorId = operatorId; 94 OperatorDied = operatorDied; 95 XpGained = xpGained; 96 97 // Defensive copy to ensure true immutability 98 GearLost = (gearLost ?? throw new ArgumentNullException(nameof(gearLost))).ToArray(); 99 100 IsVictory = isVictory; 101 TurnsSurvived = turnsSurvived; 102 DamageTaken = damageTaken; 103 CompletedAt = completedAt ?? DateTimeOffset.UtcNow; 104 } 105 }