Improve dice rolls.
This commit is contained in:
@@ -3,9 +3,13 @@ import type { Dispatch, SetStateAction } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import type { BattleEvent, Card, ClientMessage, GameView } from '../types'
|
||||
import { CardView } from './CardView'
|
||||
import { DiceTray } from './Dice'
|
||||
import { DiceRoll, ROLL_MS } from './DiceRoll'
|
||||
import { artFor } from '../petArt'
|
||||
|
||||
// How long a settled rock roll (and its damage) stays on screen before the
|
||||
// battle advances to the next step.
|
||||
const ROCK_PAUSE_MS = 1000
|
||||
|
||||
interface Props {
|
||||
view: GameView
|
||||
send: (msg: ClientMessage) => void
|
||||
@@ -34,7 +38,9 @@ const EVENT_MS: Record<BattleEvent['type'], number> = {
|
||||
reveal: 800,
|
||||
summon: 1000,
|
||||
mill: 700,
|
||||
rock: 1500,
|
||||
// Lead-in before the dice appear; the roll itself is timed by ROLL_MS +
|
||||
// ROCK_PAUSE_MS once it's on screen (see the auto-advance effect).
|
||||
rock: 500,
|
||||
clash: 1400,
|
||||
shield: 900,
|
||||
strip: 1100,
|
||||
@@ -193,12 +199,28 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
// paused freezes auto-advance so the player can walk the log manually.
|
||||
const [paused, setPaused] = useState(false)
|
||||
|
||||
// A rock event on screen scrambles its dice first; only once they settle do
|
||||
// we apply the damage and reveal the result.
|
||||
const showingRock = !done && lastEvent?.type === 'rock'
|
||||
const [rockSettled, setRockSettled] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!showingRock) {
|
||||
setRockSettled(true)
|
||||
return
|
||||
}
|
||||
setRockSettled(false)
|
||||
const t = window.setTimeout(() => setRockSettled(true), ROLL_MS)
|
||||
return () => window.clearTimeout(t)
|
||||
}, [step, showingRock])
|
||||
|
||||
useEffect(() => {
|
||||
if (done || paused) return
|
||||
const delay = EVENT_MS[events[step].type] ?? 1000
|
||||
// The rock roll owns its timing: hold until the dice settle, then a full
|
||||
// beat with the damage showing before moving on.
|
||||
const delay = showingRock ? ROLL_MS + ROCK_PAUSE_MS : (EVENT_MS[events[step].type] ?? 1000)
|
||||
const t = window.setTimeout(() => setStep((s) => s + 1), delay)
|
||||
return () => window.clearTimeout(t)
|
||||
}, [step, done, paused, events])
|
||||
}, [step, done, paused, events, showingRock])
|
||||
|
||||
// Manual controls. Stepping by hand pauses playback so it doesn't fight you.
|
||||
const stepTo = (n: number) => {
|
||||
@@ -210,9 +232,12 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
setStep(0)
|
||||
}
|
||||
|
||||
// While the dice are still scrambling, replay only up to just before the
|
||||
// rock so its damage/faint hasn't landed yet.
|
||||
const upto = showingRock && !rockSettled ? step - 1 : step
|
||||
const sides = useMemo(
|
||||
() => replay(events, battle.stackSizes, step),
|
||||
[events, battle.stackSizes, step],
|
||||
() => replay(events, battle.stackSizes, upto),
|
||||
[events, battle.stackSizes, upto],
|
||||
)
|
||||
|
||||
const youSeat = view.youSeat
|
||||
@@ -222,6 +247,14 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
const won = battle.winnerSeat === youSeat
|
||||
const draw = battle.winnerSeat < 0
|
||||
|
||||
// Dice for the rock event currently on screen, shown under the thrower's
|
||||
// side. `step` keys the tray so the scramble replays for every rock event.
|
||||
const rockDice =
|
||||
!done && lastEvent?.type === 'rock' && lastEvent.dice && lastEvent.dice.length > 0
|
||||
? lastEvent.dice
|
||||
: null
|
||||
const rockSide: 'left' | 'right' = lastEvent?.seat === youSeat ? 'left' : 'right'
|
||||
|
||||
function renderSide(seat: number, dir: 'left' | 'right') {
|
||||
const s = sides[seat]
|
||||
const clashing = !done && lastEvent?.type === 'clash' && s.unit && !s.unit.dying
|
||||
@@ -230,7 +263,8 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
const summoning = !done && lastEvent?.type === 'summon' && lastEvent.seat === seat
|
||||
const milling = !done && lastEvent?.type === 'mill' && lastEvent.seat === seat
|
||||
const revealing = !done && lastEvent?.type === 'reveal' && lastEvent.seat === seat
|
||||
const pop = done ? null : unitPop(lastEvent, seat, events, step)
|
||||
// Hold the −N / miss! pop until the dice settle.
|
||||
const pop = done || (showingRock && !rockSettled) ? null : unitPop(lastEvent, seat, events, step)
|
||||
|
||||
const lineup = lineups?.[seat] ?? []
|
||||
const stackEl = (
|
||||
@@ -386,18 +420,10 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
<div className="battlefield">
|
||||
{renderSide(youSeat, 'left')}
|
||||
<div className="battle-center" aria-hidden>
|
||||
{!done && lastEvent?.type === 'rock' &&
|
||||
(lastEvent.dice && lastEvent.dice.length > 0 ? (
|
||||
// Remount per rock event (keyed by step) so the tumble replays.
|
||||
<DiceTray key={step} dice={lastEvent.dice} />
|
||||
) : (
|
||||
<div className={`rock-fly ${lastEvent.target === youSeat ? 'rock-fly-left' : 'rock-fly-right'}`}>
|
||||
🪨
|
||||
</div>
|
||||
))}
|
||||
<span className="battle-center-bolt">⚡</span>
|
||||
</div>
|
||||
{renderSide(oppSeat, 'right')}
|
||||
{rockDice && <DiceRoll key={step} dice={rockDice} side={rockSide} />}
|
||||
</div>
|
||||
|
||||
{peek &&
|
||||
|
||||
Reference in New Issue
Block a user