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:
@@ -814,6 +814,30 @@ func (p *parser) parseBattleRosterRow(fields []string) {
|
||||
key := shipKey{race: p.pendingBattleRace, class: className}
|
||||
idx := p.assignShipIndex(key)
|
||||
|
||||
// Legacy battle rosters may list the same `(race, className)`
|
||||
// across multiple rows — different tech variants, ships pulled
|
||||
// from several stacks / planets, etc. We collapse those rows
|
||||
// into one BattleReportGroup keyed by `(race, className)` (the
|
||||
// viewer aggregates per class anyway) by SUMMING Number and
|
||||
// NumberLeft instead of overwriting; otherwise only the last
|
||||
// row's counts survive and the battle protocol's destroy count
|
||||
// would dwarf the recorded initial count (the original
|
||||
// motivation for the now-removed "phantom destroy" workaround).
|
||||
if existing, found := p.pendingBattle.ships[idx]; found {
|
||||
existing.Number += uint(number)
|
||||
existing.NumberLeft += uint(numLeft)
|
||||
// LoadQuantity is per-ship cargo — average is a fair fallback
|
||||
// when several stacks of the same class merge into one bucket.
|
||||
existing.LoadQuantity = report.F(
|
||||
(existing.LoadQuantity.F() + loadQuantity) / 2,
|
||||
)
|
||||
// Tech / LoadType / InBattle keep their first-seen values:
|
||||
// the viewer treats them as bucket-wide attributes and the
|
||||
// first row is normally the most representative tech variant.
|
||||
p.pendingBattle.ships[idx] = existing
|
||||
return
|
||||
}
|
||||
|
||||
p.pendingBattle.ships[idx] = report.BattleReportGroup{
|
||||
Race: p.pendingBattleRace,
|
||||
ClassName: className,
|
||||
|
||||
Reference in New Issue
Block a user