/ GUNRPG.Application / Distributed / DefaultGameEngine.cs
DefaultGameEngine.cs
 1  using GUNRPG.Core.Intents;
 2  
 3  namespace GUNRPG.Application.Distributed;
 4  
 5  /// <summary>
 6  /// Deterministic game engine for the distributed lockstep authority.
 7  /// Maintains a lightweight state model that tracks operator actions for hash verification.
 8  /// Used by both <see cref="LocalGameAuthority"/> and <see cref="DistributedAuthority"/>
 9  /// to ensure all nodes agree on the action sequence.
10  /// <para>
11  /// This engine mirrors the intent-level state changes from the existing combat system
12  /// (<see cref="Sessions.CombatSessionService"/>). The actual combat resolution (damage, AI,
13  /// phases) is handled by <see cref="Sessions.CombatSessionService"/> and
14  /// <see cref="Combat.DeterministicCombatEngine"/>; this engine provides the deterministic
15  /// action ledger used for P2P state consistency verification.
16  /// </para>
17  /// </summary>
18  public sealed class DefaultGameEngine : IDeterministicGameEngine
19  {
20      public GameStateDto Step(GameStateDto state, PlayerActionDto action)
21      {
22          var operators = state.Operators
23              .Select(op => op.OperatorId == action.OperatorId ? ApplyActionToSnapshot(op, action) : CloneSnapshot(op))
24              .ToList();
25  
26          // If the operator doesn't exist yet, create and apply
27          if (!operators.Any(op => op.OperatorId == action.OperatorId))
28          {
29              var newOp = new GameStateDto.OperatorSnapshot
30              {
31                  OperatorId = action.OperatorId,
32                  Name = $"Operator-{action.OperatorId.ToString()[..8]}",
33                  CurrentHealth = 100f,
34                  MaxHealth = 100f,
35                  EquippedWeaponName = "Default",
36                  UnlockedPerks = new List<string>()
37              };
38              operators.Add(ApplyActionToSnapshot(newOp, action));
39          }
40  
41          return new GameStateDto
42          {
43              ActionCount = state.ActionCount + 1,
44              Operators = operators.OrderBy(op => op.OperatorId).ToList()
45          };
46      }
47  
48      private static GameStateDto.OperatorSnapshot ApplyActionToSnapshot(
49          GameStateDto.OperatorSnapshot snapshot, PlayerActionDto action)
50      {
51          var health = snapshot.CurrentHealth;
52          var xp = snapshot.TotalXp;
53  
54          // Mirror intent-level state changes from the combat system
55          if (action.Primary == PrimaryAction.Fire)
56          {
57              xp += 10;
58          }
59  
60          if (action.Primary == PrimaryAction.Reload)
61          {
62              xp += 1;
63          }
64  
65          return new GameStateDto.OperatorSnapshot
66          {
67              OperatorId = snapshot.OperatorId,
68              Name = snapshot.Name,
69              TotalXp = xp,
70              CurrentHealth = health,
71              MaxHealth = snapshot.MaxHealth,
72              EquippedWeaponName = snapshot.EquippedWeaponName,
73              UnlockedPerks = snapshot.UnlockedPerks.ToList(),
74              ExfilStreak = snapshot.ExfilStreak,
75              IsDead = snapshot.IsDead
76          };
77      }
78  
79      private static GameStateDto.OperatorSnapshot CloneSnapshot(GameStateDto.OperatorSnapshot snapshot)
80      {
81          return new GameStateDto.OperatorSnapshot
82          {
83              OperatorId = snapshot.OperatorId,
84              Name = snapshot.Name,
85              TotalXp = snapshot.TotalXp,
86              CurrentHealth = snapshot.CurrentHealth,
87              MaxHealth = snapshot.MaxHealth,
88              EquippedWeaponName = snapshot.EquippedWeaponName,
89              UnlockedPerks = snapshot.UnlockedPerks.ToList(),
90              ExfilStreak = snapshot.ExfilStreak,
91              IsDead = snapshot.IsDead
92          };
93      }
94  }