bd11cd80da
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>
104 lines
3.0 KiB
Go
104 lines
3.0 KiB
Go
package controller
|
|
|
|
import (
|
|
"galaxy/model/report"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func TransformBattle(c *Cache, b *Battle) *report.BattleReport {
|
|
r := &report.BattleReport{
|
|
ID: b.ID,
|
|
Planet: b.Planet,
|
|
PlanetName: c.MustPlanet(b.Planet).Name,
|
|
Races: make(map[int]uuid.UUID),
|
|
Ships: make(map[int]report.BattleReportGroup),
|
|
Protocol: make([]report.BattleActionReport, len(b.Protocol)),
|
|
}
|
|
|
|
cacheShipClass := make(map[uuid.UUID]int)
|
|
cacheRaceName := make(map[uuid.UUID]int)
|
|
processedGroup := make(map[int]bool)
|
|
|
|
addShipGroup := func(groupId int, inBattle bool) int {
|
|
shipClass := c.ShipGroupShipClass(groupId)
|
|
sg := c.ShipGroup(groupId)
|
|
// Several ship-groups of the same race/class can take part
|
|
// in the same battle (different tech upgrades, arrivals from
|
|
// different planets, …). They share a single
|
|
// BattleReportGroup entry keyed by ShipClass.ID — when a
|
|
// later group lands on a cached class we add its Number and
|
|
// NumberLeft into the existing entry instead of dropping
|
|
// them, so the protocol's per-class destroy counts reconcile
|
|
// with the recorded totals. `processedGroup` guards against
|
|
// double-counting a single groupId across multiple shots in
|
|
// the protocol — `ship()` runs on every attacker and defender
|
|
// reference, the merge must happen once per groupId.
|
|
if existing, ok := cacheShipClass[shipClass.ID]; ok {
|
|
if !processedGroup[groupId] {
|
|
bg := r.Ships[existing]
|
|
bg.Number += b.InitialNumbers[groupId]
|
|
bg.NumberLeft += sg.Number
|
|
if inBattle {
|
|
bg.InBattle = true
|
|
}
|
|
r.Ships[existing] = bg
|
|
processedGroup[groupId] = true
|
|
}
|
|
return existing
|
|
}
|
|
itemNumber := len(r.Ships)
|
|
bg := &report.BattleReportGroup{
|
|
Race: c.g.Race[c.RaceIndex(sg.OwnerID)].Name,
|
|
InBattle: inBattle,
|
|
Number: b.InitialNumbers[groupId],
|
|
NumberLeft: sg.Number,
|
|
ClassName: shipClass.Name,
|
|
LoadType: sg.CargoString(),
|
|
LoadQuantity: report.F(sg.Load.F()),
|
|
Tech: make(map[string]report.Float, len(sg.Tech)),
|
|
}
|
|
for t, v := range sg.Tech {
|
|
bg.Tech[t.String()] = report.F(v.F())
|
|
}
|
|
r.Ships[itemNumber] = *bg
|
|
cacheShipClass[shipClass.ID] = itemNumber
|
|
processedGroup[groupId] = true
|
|
return itemNumber
|
|
}
|
|
|
|
ship := func(groupId int) int {
|
|
return addShipGroup(groupId, true)
|
|
}
|
|
|
|
race := func(groupId int) int {
|
|
race := c.ShipGroupOwnerRace(groupId)
|
|
if v, ok := cacheRaceName[race.ID]; ok {
|
|
return v
|
|
} else {
|
|
itemNumber := len(r.Races)
|
|
r.Races[itemNumber] = race.ID
|
|
cacheRaceName[race.ID] = itemNumber
|
|
return itemNumber
|
|
}
|
|
}
|
|
|
|
for i := range b.Protocol {
|
|
r.Protocol[i] = report.BattleActionReport{
|
|
Attacker: race(b.Protocol[i].Attacker),
|
|
AttackerShipClass: ship(b.Protocol[i].Attacker),
|
|
Defender: race(b.Protocol[i].Defender),
|
|
DefenderShipClass: ship(b.Protocol[i].Defender),
|
|
Destroyed: b.Protocol[i].Destroyed,
|
|
}
|
|
}
|
|
|
|
for sgi, inBattle := range b.ObserverGroups {
|
|
if !inBattle {
|
|
addShipGroup(sgi, false)
|
|
}
|
|
}
|
|
|
|
return r
|
|
}
|