/ GUNRPG.Application / Combat / CombatOutcome.cs
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  }