ui/phase-27: battle viewer (radial scene, playback, map markers)
Engine wire change: Report.battle switched from []uuid.UUID to
[]BattleSummary{id, planet, shots} so the map can place battle
markers without N extra fetches. FBS schema + generated Go/TS
regenerated; transcoder + report controller updated; openapi
adds the BattleSummary schema with a freeze test.
Backend gateway forwards engine GET /api/v1/battle/:turn/:uuid as
/api/v1/user/games/{game_id}/battles/{turn}/{battle_id} (handler
plus engineclient.FetchBattle, contract test stub, openapi spec).
UI:
- BattleViewer (lib/battle-player/) is a logically isolated SVG
radial scene that consumes a BattleReport prop. Planet at the
centre, races on the outer ring at equal angular spacing, race
clusters by (race, className) with <class>:<numLeft> labels;
observer groups (inBattle: false) are not drawn; eliminated
races drop out and survivors re-distribute on the next frame.
- Shot line per frame: red on destroyed, green otherwise; erased
on the next frame. Playback controls: play/pause + step ± +
rewind + 1x/2x/4x speed (400/200/100 ms per frame).
- Page wrapper (lib/active-view/battle.svelte) loads BattleReport
via api/battle-fetch.ts; synthetic-gameId prefix routes to a
fixture loader, otherwise REST through the gateway. Always-
visible <ol> text protocol satisfies the accessibility ask.
- section-battles.svelte links every battle UUID into the viewer.
- map/battle-markers.ts: yellow X cross of 2 LinePrim through the
corners of the planet's circumscribed square (stroke width
clamps from 1 px at 1 shot to 5 px at 100+ shots); bombing
marker is a stroke-only ring (yellow when damaged, red when
wiped). Wired into state-binding.ts; click handler dispatches
battle clicks to the viewer and bombing clicks to the matching
Reports row.
- i18n keys for the viewer in en + ru.
Docs: ui/docs/battle-viewer-ux.md, FUNCTIONAL.md §6.5 + ru
mirror, ui/PLAN.md Phase 27 decisions + deferred TODOs (push
event, richer class visuals, animated re-distribution).
Tests: Vitest unit (radial layout + timeline frame builder +
marker stroke formula + marker primitives), Playwright e2e for
the viewer (Reports link → viewer, playback step, not-found),
backend engineclient FetchBattle (200 / 404 / bad input), engine
openapi freezes (BattleReport, BattleReportGroup,
BattleActionReport, BattleSummary, Report.battle items).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+36
-12
@@ -10,7 +10,6 @@ import (
|
||||
fbs "galaxy/schema/fbs/report"
|
||||
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ReportToPayload converts model.Report from the internal representation to
|
||||
@@ -120,7 +119,7 @@ func ReportToPayload(report *model.Report) ([]byte, error) {
|
||||
otherScienceVector := encodeReportOffsetVector(builder, len(otherScienceOffsets), fbs.ReportStartOtherScienceVector, otherScienceOffsets)
|
||||
localShipClassVector := encodeReportOffsetVector(builder, len(localShipClassOffsets), fbs.ReportStartLocalShipClassVector, localShipClassOffsets)
|
||||
otherShipClassVector := encodeReportOffsetVector(builder, len(otherShipClassOffsets), fbs.ReportStartOtherShipClassVector, otherShipClassOffsets)
|
||||
battleVector := encodeReportUUIDVector(builder, report.Battle)
|
||||
battleVector := encodeReportBattleSummaries(builder, report.Battle)
|
||||
bombingVector := encodeReportOffsetVector(builder, len(bombingOffsets), fbs.ReportStartBombingVector, bombingOffsets)
|
||||
incomingGroupVector := encodeReportOffsetVector(builder, len(incomingGroupOffsets), fbs.ReportStartIncomingGroupVector, incomingGroupOffsets)
|
||||
localPlanetVector := encodeReportOffsetVector(builder, len(localPlanetOffsets), fbs.ReportStartLocalPlanetVector, localPlanetOffsets)
|
||||
@@ -734,13 +733,29 @@ func decodeReportBattleVector(flatReport *fbs.Report, result *model.Report) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
result.Battle = make([]uuid.UUID, length)
|
||||
item := new(commonfbs.UUID)
|
||||
result.Battle = make([]model.BattleSummary, length)
|
||||
item := new(fbs.BattleSummary)
|
||||
idHolder := new(commonfbs.UUID)
|
||||
for i := 0; i < length; i++ {
|
||||
if !flatReport.Battle(item, i) {
|
||||
return fmt.Errorf("decode report battle %d: battle is missing", i)
|
||||
}
|
||||
if item.Id(idHolder) == nil {
|
||||
return fmt.Errorf("decode report battle %d: battle id is missing", i)
|
||||
}
|
||||
result.Battle[i] = uuidFromHiLo(item.Hi(), item.Lo())
|
||||
planet, err := uint64ToUint(item.Planet(), "planet")
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode report battle %d: %w", i, err)
|
||||
}
|
||||
shots, err := uint64ToUint(item.Shots(), "shots")
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode report battle %d: %w", i, err)
|
||||
}
|
||||
result.Battle[i] = model.BattleSummary{
|
||||
ID: uuidFromHiLo(idHolder.Hi(), idHolder.Lo()),
|
||||
Planet: planet,
|
||||
Shots: shots,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1299,17 +1314,26 @@ func encodeReportOffsetVector(
|
||||
return builder.EndVector(length)
|
||||
}
|
||||
|
||||
func encodeReportUUIDVector(builder *flatbuffers.Builder, ids []uuid.UUID) flatbuffers.UOffsetT {
|
||||
if len(ids) == 0 {
|
||||
func encodeReportBattleSummaries(builder *flatbuffers.Builder, summaries []model.BattleSummary) flatbuffers.UOffsetT {
|
||||
if len(summaries) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
fbs.ReportStartBattleVector(builder, len(ids))
|
||||
for i := len(ids) - 1; i >= 0; i-- {
|
||||
hi, lo := uuidToHiLo(ids[i])
|
||||
commonfbs.CreateUUID(builder, hi, lo)
|
||||
offsets := make([]flatbuffers.UOffsetT, len(summaries))
|
||||
for i := range summaries {
|
||||
hi, lo := uuidToHiLo(summaries[i].ID)
|
||||
fbs.BattleSummaryStart(builder)
|
||||
fbs.BattleSummaryAddId(builder, commonfbs.CreateUUID(builder, hi, lo))
|
||||
fbs.BattleSummaryAddPlanet(builder, uint64(summaries[i].Planet))
|
||||
fbs.BattleSummaryAddShots(builder, uint64(summaries[i].Shots))
|
||||
offsets[i] = fbs.BattleSummaryEnd(builder)
|
||||
}
|
||||
return builder.EndVector(len(ids))
|
||||
|
||||
fbs.ReportStartBattleVector(builder, len(offsets))
|
||||
for i := len(offsets) - 1; i >= 0; i-- {
|
||||
builder.PrependUOffsetT(offsets[i])
|
||||
}
|
||||
return builder.EndVector(len(offsets))
|
||||
}
|
||||
|
||||
func encodeReportRouteEntryVector(builder *flatbuffers.Builder, route map[uint]string) flatbuffers.UOffsetT {
|
||||
|
||||
@@ -255,9 +255,17 @@ func sampleReport() *model.Report {
|
||||
OtherShipClass: []model.OthersShipClass{
|
||||
{Race: "Martians", ShipClass: model.ShipClass{Name: "destroyer", Drive: model.Float(1.75), Armament: 6, Weapons: model.Float(2.25), Shields: model.Float(2.75), Cargo: model.Float(3.25), Mass: model.Float(10.5)}},
|
||||
},
|
||||
Battle: []uuid.UUID{
|
||||
uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
||||
uuid.MustParse("22222222-2222-2222-2222-222222222222"),
|
||||
Battle: []model.BattleSummary{
|
||||
{
|
||||
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
||||
Planet: 4,
|
||||
Shots: 17,
|
||||
},
|
||||
{
|
||||
ID: uuid.MustParse("22222222-2222-2222-2222-222222222222"),
|
||||
Planet: 11,
|
||||
Shots: 3,
|
||||
},
|
||||
},
|
||||
Bombing: []*model.Bombing{
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user