diff --git a/internal/game/battle.go b/internal/game/battle.go
index 59c22f1..7693590 100644
--- a/internal/game/battle.go
+++ b/internal/game/battle.go
@@ -724,10 +724,14 @@ func (g *Game) resolveBattle() {
g.Battle = res
g.Phase = PhaseBattle
+ // Tagged "result" so the client can hold it back until the replay finishes
+ // (the outcome is known now, but showing it early would spoil the battle).
if winner < 0 {
- g.logf(-1, "⚔️", "Round %d battle ends in a draw.", g.Round)
+ g.addLog(LogEntry{Seat: -1, Icon: "⚔️", Kind: "result",
+ Text: fmt.Sprintf("Round %d battle ends in a draw.", g.Round)})
} else {
- g.logf(winner, "⚔️", "%s wins the round %d battle (+%d🏆).", pname(winner), g.Round, res.Trophies)
+ g.addLog(LogEntry{Seat: winner, Icon: "⚔️", Kind: "result",
+ Text: fmt.Sprintf("%s wins the round %d battle (+%d🏆).", pname(winner), g.Round, res.Trophies)})
}
for _, p := range g.Players {
p.Ready = false
diff --git a/internal/game/log.go b/internal/game/log.go
index 012a9dc..9d40ac8 100644
--- a/internal/game/log.go
+++ b/internal/game/log.go
@@ -14,6 +14,7 @@ type LogEntry struct {
Phase Phase `json:"phase"`
Seat int `json:"seat"` // acting seat, or -1 when none
Icon string `json:"icon,omitempty"` // leading emoji
+ Kind string `json:"kind,omitempty"` // e.g. "result" (battle outcome)
Text string `json:"text"` // the sentence itself
Source string `json:"source,omitempty"` // card id that caused a spawn
Spawn string `json:"spawn,omitempty"` // "apple" | "bee" for spawn entries
diff --git a/web/src/components/Table.tsx b/web/src/components/Table.tsx
index 0988a11..a0b1cc7 100644
--- a/web/src/components/Table.tsx
+++ b/web/src/components/Table.tsx
@@ -32,13 +32,38 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
return { round: battleRound, step: next }
})
- const battleLines = useMemo(
- () =>
- view?.phase === 'battle' && view.battle?.events
- ? battleLogLines(view.battle.events, step)
- : undefined,
- [view, step],
- )
+ // The battle outcome is known before the replay plays out, so we hold its
+ // "result" log entry back: it's dropped from the persistent list during the
+ // battle and appended as the final battle line only once the replay reaches
+ // the end (and reappears in the persistent log in later phases).
+ const inBattle = view?.phase === 'battle'
+ const events = view?.battle?.events ?? null
+ const battleDone = !!(inBattle && events && step >= events.length)
+
+ const entries = useMemo(() => {
+ const log = view?.log ?? []
+ if (!inBattle) return log
+ return log.filter((e) => !(e.kind === 'result' && e.round === battleRound))
+ }, [view, inBattle, battleRound])
+
+ const battleLines = useMemo(() => {
+ if (!inBattle || !events) return undefined
+ const lines = battleLogLines(events, step)
+ if (battleDone) {
+ const result = (view?.log ?? []).find(
+ (e) => e.kind === 'result' && e.round === battleRound,
+ )
+ if (result) {
+ lines.push({
+ key: `result-${result.seq}`,
+ icon: result.icon,
+ text: result.text,
+ seat: result.seat,
+ })
+ }
+ }
+ return lines
+ }, [view, inBattle, events, step, battleDone, battleRound])
if (!view) {
return (
@@ -98,11 +123,7 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
{view.phase === 'gameover' && }
{view.phase !== 'lobby' && (
-
+
)}
diff --git a/web/src/types.ts b/web/src/types.ts
index 42617e4..257090b 100644
--- a/web/src/types.ts
+++ b/web/src/types.ts
@@ -83,6 +83,7 @@ export interface LogEntry {
phase: Phase
seat: number // acting seat, or -1
icon?: string
+ kind?: string // e.g. 'result' (battle outcome)
text: string
source?: string // card id that caused a spawn
spawn?: string // 'apple' | 'bee'