ui/phase-27: root-cause aggregation of duplicate (race, className) rows
Legacy reports list the same `(race, className)` pair across several roster rows; the engine likewise creates one ShipGroup per arrival. Both the legacy parser and `TransformBattle` were keyed on shipClass without summing — only the last row / group's counts survived, so a protocol's destroy count appeared to exceed the recorded initial roster. The UI worked around this with phantom-frame logic. Both parser and engine now SUM `Number`/`NumberLeft` across rows / groups sharing the same class; the phantom-frame workaround is gone. KNNTS041 turn 41 planet #7 reconciles: `Nails:pup` 1168 initial − 86 survivors = 1082 destroys. The engine's previously latent nil-map write on `bg.Tech` (would have paniced on any group with non-empty Tech) is fixed in the same patch — it blocked the aggregation regression test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+27
-15
@@ -132,23 +132,35 @@ on the same defender therefore look like two distinct pulses
|
||||
rather than one continuous line. On pause the line and flash
|
||||
stay drawn so the user can study the current shot.
|
||||
|
||||
## Phantom destroys
|
||||
## Aggregated ship-class buckets
|
||||
|
||||
Legacy emitters (the `dg` engine format that feeds the synthetic-
|
||||
report path) occasionally log more `Destroyed` (and `Shields`)
|
||||
lines against a ship-group bucket than the bucket's initial
|
||||
population — the emitter keeps recording hits past the moment a
|
||||
group emptied. `buildFrames` marks every such frame as
|
||||
`phantom: true` and skips the race-total decrement so the race
|
||||
stays on the scene until its actual ships are gone.
|
||||
Real legacy reports list the same `(race, className)` pair across
|
||||
several roster rows — different tech variants, ships pulled from
|
||||
multiple stacks or planets. The legacy-report parser
|
||||
([parser.go](../../tools/local-dev/legacy-report/parser.go))
|
||||
collapses those rows into a single `BattleReportGroup` keyed by
|
||||
`(race, className)` by SUMMING `Number` and `NumberLeft`; the
|
||||
engine's `TransformBattle`
|
||||
([battle_transform.go](../../game/internal/controller/battle_transform.go))
|
||||
applies the same merge keyed by `ShipClass.ID`, guarded by a
|
||||
processed-group set so the same source `groupId` is not summed
|
||||
twice across multiple protocol references.
|
||||
|
||||
During play the BattleViewer fast-forwards through streaks of
|
||||
phantom frames via a 0 ms timer so the user never sees a silent
|
||||
gap (KNNTS041 had ~30 phantom frames between shots 224 and 255
|
||||
right after the last `Nails:pup` died). Step controls and the
|
||||
scrubber can still land on a phantom frame deliberately — useful
|
||||
when inspecting the protocol entry that the engine emitted into
|
||||
the void.
|
||||
Without this aggregation only the last roster row's counts
|
||||
survived, and the protocol's destroy count against the class
|
||||
would dwarf the recorded initial count — KNNTS041 turn 41 planet
|
||||
\#7 had 7 separate `Nails:pup` rows totalling 1168 ships; the
|
||||
buggy parser stored only the last row's 88, so the 1082 destroys
|
||||
in the protocol looked like phantom hits past the empty bucket.
|
||||
After the fix both sides reconcile: 1168 initial − 86 survivors =
|
||||
1082 destroys.
|
||||
|
||||
`buildFrames`
|
||||
([timeline.ts](../src/lib/battle-player/timeline.ts)) keeps a
|
||||
defence-in-depth clamp `if (left > 0)` on the destroy decrement so
|
||||
a malformed protocol never pushes a race below zero; in normal
|
||||
operation the clamp is a no-op because parser + engine already
|
||||
folded duplicate rows together.
|
||||
|
||||
## Final-frame freeze
|
||||
|
||||
|
||||
Reference in New Issue
Block a user