/ GUNRPG.Application / Combat / BattleLogFormatter.cs
BattleLogFormatter.cs
  1  using GUNRPG.Application.Dtos;
  2  using GUNRPG.Core.Events;
  3  using GUNRPG.Core.Operators;
  4  
  5  namespace GUNRPG.Application.Combat;
  6  
  7  /// <summary>
  8  /// Formats combat events into human-readable battle log messages.
  9  /// </summary>
 10  public static class BattleLogFormatter
 11  {
 12      // Keep full deterministic event stream here for offline envelope capture.
 13      // API transport truncation is applied in ApiMapping.ToApiDto(CombatSessionDto).
 14      public static List<BattleLogEntryDto> FormatEvents(IReadOnlyList<ISimulationEvent> events, Operator player, Operator enemy)
 15      {
 16          return events
 17              .Select(evt => FormatEvent(evt, player, enemy))
 18              .Where(entry => entry != null)
 19              .Cast<BattleLogEntryDto>()
 20              .ToList();
 21      }
 22  
 23      private static BattleLogEntryDto? FormatEvent(ISimulationEvent evt, Operator player, Operator enemy)
 24      {
 25          return evt switch
 26          {
 27              ShotFiredEvent shotEvent => new BattleLogEntryDto
 28              {
 29                  EventType = "ShotFired",
 30                  TimeMs = shotEvent.EventTimeMs,
 31                  Message = "fired a shot!",
 32                  ActorName = GetActorName(shotEvent.OperatorId, player, enemy)
 33              },
 34              DamageAppliedEvent damageEvent => new BattleLogEntryDto
 35              {
 36                  EventType = "Damage",
 37                  TimeMs = damageEvent.EventTimeMs,
 38                  Message = $"{damageEvent.TargetName} took {damageEvent.Damage:F0} damage ({damageEvent.BodyPart})!",
 39                  ActorName = null // Shooter name will be prepended if needed
 40              },
 41              ShotMissedEvent missEvent => new BattleLogEntryDto
 42              {
 43                  EventType = "Miss",
 44                  TimeMs = missEvent.EventTimeMs,
 45                  Message = "missed!",
 46                  ActorName = GetActorName(missEvent.OperatorId, player, enemy)
 47              },
 48              ReloadCompleteEvent reloadEvent => new BattleLogEntryDto
 49              {
 50                  EventType = "Reload",
 51                  TimeMs = reloadEvent.EventTimeMs,
 52                  Message = "reloaded.",
 53                  ActorName = GetActorName(reloadEvent.OperatorId, player, enemy)
 54              },
 55              ADSCompleteEvent adsEvent => new BattleLogEntryDto
 56              {
 57                  EventType = "ADS",
 58                  TimeMs = adsEvent.EventTimeMs,
 59                  Message = "aimed down sights.",
 60                  ActorName = GetActorName(adsEvent.OperatorId, player, enemy)
 61              },
 62              MovementStartedEvent moveEvent => new BattleLogEntryDto
 63              {
 64                  EventType = "Movement",
 65                  TimeMs = moveEvent.EventTimeMs,
 66                  Message = $"started {FormatMovementType(moveEvent.MovementType)}.",
 67                  ActorName = moveEvent.Operator.Name
 68              },
 69              MovementEndedEvent moveEndEvent => new BattleLogEntryDto
 70              {
 71                  EventType = "Movement",
 72                  TimeMs = moveEndEvent.EventTimeMs,
 73                  Message = "stopped moving.",
 74                  ActorName = moveEndEvent.Operator.Name
 75              },
 76              CoverEnteredEvent coverEvent => new BattleLogEntryDto
 77              {
 78                  EventType = "Cover",
 79                  TimeMs = coverEvent.EventTimeMs,
 80                  Message = $"took {FormatCoverType(coverEvent.CoverType)} cover.",
 81                  ActorName = coverEvent.Operator.Name
 82              },
 83              CoverExitedEvent coverExitEvent => new BattleLogEntryDto
 84              {
 85                  EventType = "Cover",
 86                  TimeMs = coverExitEvent.EventTimeMs,
 87                  Message = "left cover.",
 88                  ActorName = coverExitEvent.Operator.Name
 89              },
 90              SuppressionStartedEvent suppressEvent => new BattleLogEntryDto
 91              {
 92                  EventType = "Suppression",
 93                  TimeMs = suppressEvent.EventTimeMs,
 94                  Message = "is suppressing!",
 95                  ActorName = GetActorName(suppressEvent.OperatorId, player, enemy)
 96              },
 97              _ => new BattleLogEntryDto
 98              {
 99                  // Deterministic offline sync stores every event; replay currently uses
100                  // Damage entries and keeps others for chain verification and future replay expansion.
101                  EventType = evt.GetType().Name,
102                  TimeMs = evt.EventTimeMs,
103                  Message = $"Unformatted event: {evt.GetType().Name}",
104                  ActorName = null
105              }
106          };
107      }
108  
109      private static string GetActorName(Guid operatorId, Operator player, Operator enemy)
110      {
111          if (operatorId == player.Id)
112              return player.Name;
113          else if (operatorId == enemy.Id)
114              return enemy.Name;
115          else
116              return "Unknown";
117      }
118  
119      private static string FormatMovementType(MovementState movementType)
120      {
121          return movementType switch
122          {
123              MovementState.Walking => "walking",
124              MovementState.Sprinting => "sprinting",
125              MovementState.Crouching => "crouching",
126              MovementState.Sliding => "sliding",
127              MovementState.Stationary => "stationary",
128              _ => movementType.ToString().ToLowerInvariant()
129          };
130      }
131  
132      private static string FormatCoverType(CoverState coverType)
133      {
134          return coverType switch
135          {
136              CoverState.Partial => "partial",
137              CoverState.Full => "full",
138              CoverState.None => "no",
139              _ => coverType.ToString().ToLowerInvariant()
140          };
141      }
142  }