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 }