/ GUNRPG.Application / Sessions / CombatSession.cs
CombatSession.cs
  1  using GUNRPG.Application.Combat;
  2  using GUNRPG.Core;
  3  using GUNRPG.Core.AI;
  4  using GUNRPG.Core.Combat;
  5  using GUNRPG.Core.Equipment;
  6  using GUNRPG.Core.Operators;
  7  using GUNRPG.Core.VirtualPet;
  8  
  9  namespace GUNRPG.Application.Sessions;
 10  
 11  /// <summary>
 12  /// Aggregates combat state, AI, and operator lifecycle for a single session.
 13  /// </summary>
 14  public sealed class CombatSession
 15  {
 16      public Guid Id { get; }
 17      public OperatorId OperatorId { get; }  // Reference to the operator (does not store progression)
 18      public CombatSystemV2 Combat { get; }
 19      public SimpleAIV2 Ai { get; }
 20      public OperatorManager OperatorManager { get; }
 21      public PetState PetState { get; set; }
 22      public int EnemyLevel { get; }
 23      public int Seed { get; }
 24      public SessionPhase Phase { get; private set; }
 25      public int TurnNumber { get; private set; }
 26      public DateTimeOffset CreatedAt { get; }
 27      public DateTimeOffset? CompletedAt { get; private set; }
 28      public DateTimeOffset LastActionTimestamp { get; private set; }
 29      public bool PostCombatResolved { get; set; }
 30  
 31      public Operator Player => Combat.Player;
 32      public Operator Enemy => Combat.Enemy;
 33  
 34      public CombatSession(
 35          Guid id,
 36          OperatorId operatorId,
 37          CombatSystemV2 combat,
 38          SimpleAIV2 ai,
 39          OperatorManager operatorManager,
 40          PetState petState,
 41          int enemyLevel,
 42          int seed,
 43          SessionPhase phase,
 44          int turnNumber,
 45          DateTimeOffset createdAt,
 46          DateTimeOffset? completedAt = null,
 47          DateTimeOffset? lastActionTimestamp = null)
 48      {
 49          Id = id;
 50          OperatorId = operatorId;
 51          Combat = combat;
 52          Ai = ai;
 53          OperatorManager = operatorManager;
 54          PetState = petState;
 55          EnemyLevel = enemyLevel;
 56          Seed = seed;
 57          Phase = phase;
 58          TurnNumber = turnNumber;
 59          CreatedAt = createdAt;
 60          CompletedAt = completedAt;
 61          LastActionTimestamp = lastActionTimestamp ?? createdAt;
 62      }
 63  
 64      public static CombatSession CreateDefault(string? playerName = null, int? seed = null, float? startingDistance = null, string? enemyName = null, Guid? id = null, Guid? operatorId = null)
 65      {
 66          var resolvedSeed = seed ?? Random.Shared.Next();
 67          var name = string.IsNullOrWhiteSpace(playerName) ? "Player" : playerName.Trim();
 68          var foeName = string.IsNullOrWhiteSpace(enemyName) ? "Enemy" : enemyName.Trim();
 69  
 70          var player = new Operator(name)
 71          {
 72              EquippedWeapon = WeaponFactory.CreateSokol545(),
 73              DistanceToOpponent = startingDistance ?? 15f
 74          };
 75          player.CurrentAmmo = player.EquippedWeapon!.MagazineSize;
 76  
 77          var enemy = new Operator(foeName)
 78          {
 79              EquippedWeapon = WeaponFactory.CreateSturmwolf45(),
 80              DistanceToOpponent = startingDistance ?? 15f
 81          };
 82          enemy.CurrentAmmo = enemy.EquippedWeapon!.MagazineSize;
 83  
 84          var debugOptions = new CombatDebugOptions { VerboseShotLogs = false };
 85          var combat = new CombatSystemV2(player, enemy, seed: resolvedSeed, debugOptions: debugOptions);
 86          var ai = new SimpleAIV2(seed: resolvedSeed);
 87          var operatorManager = new OperatorManager();
 88  
 89          var petState = new PetState(player.Id, 100f, 0f, 0f, 0f, 100f, 0f, 100f, DateTimeOffset.UtcNow);
 90          var enemyLevel = Math.Max(0, new Random(resolvedSeed).Next(-2, 3));
 91          var resolvedOperatorId = operatorId.HasValue
 92              ? OperatorId.FromGuid(operatorId.Value)
 93              : OperatorId.FromGuid(player.Id);
 94  
 95          return new CombatSession(
 96              id ?? Guid.NewGuid(),
 97              resolvedOperatorId,
 98              combat,
 99              ai,
100              operatorManager,
101              petState,
102              enemyLevel,
103              resolvedSeed,
104              SessionPhase.Planning,
105              1,
106              DateTimeOffset.UtcNow);
107      }
108  
109      public void TransitionTo(SessionPhase nextPhase)
110      {
111          if (!IsValidTransition(Phase, nextPhase))
112          {
113              throw new InvalidOperationException($"Invalid session phase transition: {Phase} -> {nextPhase}");
114          }
115  
116          Phase = nextPhase;
117          LastActionTimestamp = DateTimeOffset.UtcNow;
118          
119          // Record completion time when transitioning to Completed phase
120          if (nextPhase == SessionPhase.Completed && CompletedAt == null)
121          {
122              CompletedAt = DateTimeOffset.UtcNow;
123          }
124      }
125  
126      public void AdvanceTurnCounter()
127      {
128          TurnNumber++;
129          LastActionTimestamp = DateTimeOffset.UtcNow;
130      }
131  
132      public void RecordAction()
133      {
134          LastActionTimestamp = DateTimeOffset.UtcNow;
135      }
136  
137      /// <summary>
138      /// Gets the combat outcome for this session.
139      /// Can only be called after the session has completed.
140      /// </summary>
141      /// <returns>The combat outcome.</returns>
142      /// <exception cref="InvalidOperationException">Thrown if the session is not yet completed.</exception>
143      public CombatOutcome GetOutcome()
144      {
145          if (Phase != SessionPhase.Completed)
146          {
147              throw new InvalidOperationException(
148                  $"Cannot produce outcome: session is not completed (current phase: {Phase})");
149          }
150  
151          var player = Player;
152          var enemy = Enemy;
153  
154          var operatorDied = !player.IsAlive;
155          var damageTaken = player.MaxHealth - player.Health;
156  
157          // Calculate XP based on outcome
158          var isVictory = player.IsAlive && !enemy.IsAlive;
159  
160          int xpGained;
161          if (isVictory)
162          {
163              xpGained = 100; // Base XP for victory
164          }
165          else if (!operatorDied)
166          {
167              xpGained = 50; // Partial XP for surviving
168          }
169          else
170          {
171              xpGained = 0; // No XP for death
172          }
173  
174          var operatorId = OperatorId;
175  
176          // For now, no gear is lost (will be expanded later with actual gear system)
177          var gearLost = Array.Empty<GearId>();
178  
179          return new CombatOutcome(
180              sessionId: Id,
181              operatorId: operatorId,
182              operatorDied: operatorDied,
183              xpGained: xpGained,
184              gearLost: gearLost,
185              isVictory: isVictory,
186              turnsSurvived: TurnNumber,
187              damageTaken: damageTaken,
188              completedAt: CompletedAt ?? DateTimeOffset.UtcNow);
189      }
190  
191      private static bool IsValidTransition(SessionPhase current, SessionPhase next)
192      {
193          return (current, next) switch
194          {
195              (SessionPhase.Created, SessionPhase.Planning) => true,
196              (SessionPhase.Planning, SessionPhase.Resolving) => true,
197              (SessionPhase.Resolving, SessionPhase.Planning) => true,
198              (SessionPhase.Planning, SessionPhase.Completed) => true,
199              (SessionPhase.Resolving, SessionPhase.Completed) => true,
200              _ => current == next
201          };
202      }
203  }