Narrate the battle in the event log, synced to the replay step.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import type { BattleEvent, Card, ClientMessage, GameView } from '../types'
|
||||
import { CardView } from './CardView'
|
||||
@@ -8,6 +9,9 @@ import { artFor } from '../petArt'
|
||||
interface Props {
|
||||
view: GameView
|
||||
send: (msg: ClientMessage) => void
|
||||
// Step is owned by Table so the event log can stay in sync with the replay.
|
||||
step: number
|
||||
setStep: Dispatch<SetStateAction<number>>
|
||||
}
|
||||
|
||||
interface UnitVis {
|
||||
@@ -171,10 +175,9 @@ function unitPop(ev: BattleEvent | null, seat: number, events: BattleEvent[], st
|
||||
|
||||
// BattlePhase plays back the battle log: cards flip off each deck, rocks
|
||||
// fly, pets clash, the fallen fade out, then the round result lands.
|
||||
export function BattlePhase({ view, send }: Props) {
|
||||
export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
const battle = view.battle!
|
||||
const events = battle.events ?? []
|
||||
const [step, setStep] = useState(0)
|
||||
const [acked, setAcked] = useState(false)
|
||||
// The deck-peek popover lives inside .battlefield, which sets overflow-x
|
||||
// (and thus overflow-y) to auto — so an absolutely-positioned popover gets
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { LogEntry } from '../types'
|
||||
import type { BattleEvent, LogEntry } from '../types'
|
||||
|
||||
// A single battle line, derived from the battle event stream and fed in by
|
||||
// BattlePhase so the log stays in step with the replay controls.
|
||||
// Table so the log stays in step with the replay controls.
|
||||
export interface BattleLogLine {
|
||||
key: string
|
||||
icon?: string
|
||||
@@ -10,6 +10,40 @@ export interface BattleLogLine {
|
||||
seat: number // -1 = neutral/system
|
||||
}
|
||||
|
||||
const BATTLE_ICONS: Record<BattleEvent['type'], string> = {
|
||||
prep: '🍎',
|
||||
reveal: '➡️',
|
||||
summon: '✨',
|
||||
mill: '🔥',
|
||||
rock: '🪨',
|
||||
clash: '⚔️',
|
||||
shield: '🛡️',
|
||||
strip: '💨',
|
||||
steal: '🍎',
|
||||
eat: '🍎',
|
||||
heal: '💚',
|
||||
}
|
||||
|
||||
// battleLogLines turns the battle events revealed up to `step` into readable
|
||||
// log lines. Only events the backend gave text to produce a line; the rest
|
||||
// still animate but stay out of the narration.
|
||||
export function battleLogLines(events: BattleEvent[], step: number): BattleLogLine[] {
|
||||
const lines: BattleLogLine[] = []
|
||||
for (let i = 0; i < step && i < events.length; i++) {
|
||||
const ev = events[i]
|
||||
if (!ev.text) continue
|
||||
lines.push({
|
||||
key: `b${i}`,
|
||||
icon: BATTLE_ICONS[ev.type],
|
||||
text: ev.text,
|
||||
// A clash names both pets, so it stays neutral; everything else takes
|
||||
// its acting seat's colour.
|
||||
seat: ev.type === 'clash' ? -1 : ev.seat ?? -1,
|
||||
})
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
interface Props {
|
||||
entries: LogEntry[]
|
||||
youSeat: number
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import { useGame } from '../useGame'
|
||||
import type { Session } from '../types'
|
||||
import { Lobby } from './Lobby'
|
||||
@@ -6,7 +8,7 @@ import { ArrangePhase } from './ArrangePhase'
|
||||
import { BattlePhase } from './BattlePhase'
|
||||
import { GameOver } from './GameOver'
|
||||
import { DebugPanel } from './DebugPanel'
|
||||
import { EventLog } from './EventLog'
|
||||
import { EventLog, battleLogLines } from './EventLog'
|
||||
|
||||
// Table connects to the game and routes to the right phase screen.
|
||||
export function Table({ session, onLeave }: { session: Session; onLeave: () => void }) {
|
||||
@@ -23,6 +25,31 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
||||
const you = view.players[view.youSeat]
|
||||
const opponents = view.players.filter((p) => p.seat !== view.youSeat)
|
||||
|
||||
// Battle replay step lives here (not inside BattlePhase) so the event log,
|
||||
// a sibling, can render the battle narration up to the same step. Deriving
|
||||
// the effective step from the current battle round resets it to 0 whenever a
|
||||
// new battle arrives, without a separate effect.
|
||||
const battleRound = view.battle?.round ?? -1
|
||||
const [stepState, setStepState] = useState<{ round: number; step: number }>({
|
||||
round: -1,
|
||||
step: 0,
|
||||
})
|
||||
const step = stepState.round === battleRound ? stepState.step : 0
|
||||
const setStep: Dispatch<SetStateAction<number>> = (upd) =>
|
||||
setStepState((prev) => {
|
||||
const cur = prev.round === battleRound ? prev.step : 0
|
||||
const next = typeof upd === 'function' ? (upd as (n: number) => number)(cur) : upd
|
||||
return { round: battleRound, step: next }
|
||||
})
|
||||
|
||||
const battleLines = useMemo(
|
||||
() =>
|
||||
view.phase === 'battle' && view.battle?.events
|
||||
? battleLogLines(view.battle.events, step)
|
||||
: undefined,
|
||||
[view.phase, view.battle, step],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="table">
|
||||
<header className="topbar">
|
||||
@@ -64,11 +91,17 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
||||
<ShopPhase view={view} you={you} send={send} />
|
||||
)}
|
||||
{view.phase === 'arrange' && <ArrangePhase view={view} you={you} send={send} />}
|
||||
{view.phase === 'battle' && <BattlePhase view={view} send={send} />}
|
||||
{view.phase === 'battle' && (
|
||||
<BattlePhase view={view} send={send} step={step} setStep={setStep} />
|
||||
)}
|
||||
{view.phase === 'gameover' && <GameOver view={view} onLeave={onLeave} />}
|
||||
</main>
|
||||
{view.phase !== 'lobby' && (
|
||||
<EventLog entries={view.log ?? []} youSeat={view.youSeat} />
|
||||
<EventLog
|
||||
entries={view.log ?? []}
|
||||
youSeat={view.youSeat}
|
||||
battleLines={battleLines}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user