Improve snappiness and add speed setting.
This commit is contained in:
Binary file not shown.
@@ -4,10 +4,11 @@ import { createPortal } from 'react-dom'
|
||||
import type { BattleEvent, Card, ClientMessage, GameView } from '../types'
|
||||
import { CardView } from './CardView'
|
||||
import { DiceRoll, ROLL_MS } from './DiceRoll'
|
||||
import { SPEED_OPTIONS, useBattleSpeed } from '../useBattleSpeed'
|
||||
|
||||
// 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
|
||||
const ROCK_PAUSE_MS = 650
|
||||
|
||||
interface Props {
|
||||
view: GameView
|
||||
@@ -37,28 +38,31 @@ interface SideVis {
|
||||
leaving: string[]
|
||||
}
|
||||
|
||||
// Milliseconds each event type stays on screen during playback.
|
||||
// Milliseconds each event type stays on screen during playback, at 1× speed.
|
||||
// These are the base beats — kept snappy so the replay reads quickly; the
|
||||
// battle-speed multiplier scales every one of them (and the CSS animations
|
||||
// keyed off --battle-speed) in lockstep.
|
||||
const EVENT_MS: Record<BattleEvent['type'], number> = {
|
||||
prep: 700,
|
||||
reveal: 800,
|
||||
summon: 1000,
|
||||
mill: 700,
|
||||
prep: 450,
|
||||
reveal: 520,
|
||||
summon: 650,
|
||||
mill: 450,
|
||||
// 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,
|
||||
steal: 1100,
|
||||
eat: 1000,
|
||||
heal: 900,
|
||||
setaside: 700,
|
||||
release: 500,
|
||||
trumpet: 800,
|
||||
prevent: 900,
|
||||
mana: 800,
|
||||
ailment: 900,
|
||||
bounce: 1000,
|
||||
rock: 320,
|
||||
clash: 900,
|
||||
shield: 600,
|
||||
strip: 720,
|
||||
steal: 720,
|
||||
eat: 640,
|
||||
heal: 600,
|
||||
setaside: 480,
|
||||
release: 380,
|
||||
trumpet: 540,
|
||||
prevent: 600,
|
||||
mana: 540,
|
||||
ailment: 600,
|
||||
bounce: 680,
|
||||
}
|
||||
|
||||
const appleCount = (foods: Card[]) => foods.filter((f) => f.food === 'apple').length
|
||||
@@ -297,6 +301,11 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
const done = step >= events.length
|
||||
const lastEvent = step > 0 ? events[step - 1] : null
|
||||
|
||||
// Playback speed multiplier, remembered across sessions. It divides every JS
|
||||
// step timer and feeds --battle-speed to the CSS so the animations quicken
|
||||
// right along with the pacing.
|
||||
const [speed, setSpeed] = useBattleSpeed()
|
||||
|
||||
// paused freezes auto-advance so the player can walk the log manually.
|
||||
const [paused, setPaused] = useState(false)
|
||||
|
||||
@@ -310,18 +319,19 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
return
|
||||
}
|
||||
setRockSettled(false)
|
||||
const t = window.setTimeout(() => setRockSettled(true), ROLL_MS)
|
||||
const t = window.setTimeout(() => setRockSettled(true), ROLL_MS / speed)
|
||||
return () => window.clearTimeout(t)
|
||||
}, [step, showingRock])
|
||||
}, [step, showingRock, speed])
|
||||
|
||||
useEffect(() => {
|
||||
if (done || paused) return
|
||||
// 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)
|
||||
// beat with the damage showing before moving on. Every beat is scaled by
|
||||
// the speed multiplier so faster playback shortens the whole replay.
|
||||
const base = showingRock ? ROLL_MS + ROCK_PAUSE_MS : (EVENT_MS[events[step].type] ?? 1000)
|
||||
const t = window.setTimeout(() => setStep((s) => s + 1), base / speed)
|
||||
return () => window.clearTimeout(t)
|
||||
}, [step, done, paused, events, showingRock])
|
||||
}, [step, done, paused, events, showingRock, speed])
|
||||
|
||||
// Manual controls. Stepping by hand pauses playback so it doesn't fight you.
|
||||
const stepTo = (n: number) => {
|
||||
@@ -422,7 +432,7 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
{rockDice && lastEvent?.seat === seat && (
|
||||
// The dice roll sits beside the throwing side's deck.
|
||||
<div className="stack-dice">
|
||||
<DiceRoll key={step} dice={rockDice} side={pos} />
|
||||
<DiceRoll key={step} dice={rockDice} side={pos} speed={speed} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -535,8 +545,12 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
// A clash on screen flashes the centre seam; key the burst by step so every
|
||||
// clash re-triggers it.
|
||||
const centerClash = !done && lastEvent?.type === 'clash'
|
||||
|
||||
return (
|
||||
<div className="battle">
|
||||
<div className="battle" style={{ '--battle-speed': speed } as React.CSSProperties}>
|
||||
<div className="battle-header">
|
||||
<h2>Battle! Round {battle.round}</h2>
|
||||
<div className="battle-controls">
|
||||
@@ -578,6 +592,24 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
>
|
||||
Skip ⏭
|
||||
</button>
|
||||
<div className="battle-speed" role="group" aria-label="Playback speed">
|
||||
<span className="battle-speed-label" aria-hidden>
|
||||
⏱
|
||||
</span>
|
||||
{SPEED_OPTIONS.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
className={`btn btn-ghost btn-sm battle-speed-btn ${
|
||||
speed === s ? 'is-active' : ''
|
||||
}`}
|
||||
onClick={() => setSpeed(s)}
|
||||
aria-pressed={speed === s}
|
||||
title={`Play at ${s}× speed`}
|
||||
>
|
||||
{s}×
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -589,7 +621,11 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
|
||||
<div className="battlefield">
|
||||
{renderSide(oppSeat, 'top')}
|
||||
<div className="battle-center" aria-hidden>
|
||||
<div
|
||||
key={centerClash ? `clash-${step}` : 'center'}
|
||||
className={`battle-center ${centerClash ? 'battle-center-clash' : ''}`}
|
||||
aria-hidden
|
||||
>
|
||||
<span className="battle-center-bolt">⚡</span>
|
||||
</div>
|
||||
{renderSide(youSeat, 'bottom')}
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { CSSProperties } from 'react'
|
||||
|
||||
// How long the dice scramble before settling, and how fast the faces flicker
|
||||
// while they do. ROLL_MS is shared with BattlePhase so damage lands exactly
|
||||
// when the dice settle.
|
||||
// when the dice settle. Both are the base (1×) durations; the battle speed
|
||||
// multiplier divides them so the roll keeps pace with the rest of the replay.
|
||||
export const ROLL_MS = 650
|
||||
const TICK_MS = 70
|
||||
|
||||
@@ -12,19 +13,27 @@ const TICK_MS = 70
|
||||
// on the values the server actually rolled. Faces carry rock icons (0, 1, or
|
||||
// 2 rocks) — the same die the game uses, not a pip D6. Mount it with a `key`
|
||||
// that changes per rock event so the scramble replays every time.
|
||||
export function DiceRoll({ dice, side }: { dice: number[]; side: 'top' | 'bottom' }) {
|
||||
export function DiceRoll({
|
||||
dice,
|
||||
side,
|
||||
speed = 1,
|
||||
}: {
|
||||
dice: number[]
|
||||
side: 'top' | 'bottom'
|
||||
speed?: number
|
||||
}) {
|
||||
const [display, setDisplay] = useState<number[]>(dice)
|
||||
const [settled, setSettled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const flicker = window.setInterval(() => {
|
||||
setDisplay(dice.map(() => Math.floor(Math.random() * 3)))
|
||||
}, TICK_MS)
|
||||
}, TICK_MS / speed)
|
||||
const stop = window.setTimeout(() => {
|
||||
window.clearInterval(flicker)
|
||||
setDisplay(dice)
|
||||
setSettled(true)
|
||||
}, ROLL_MS)
|
||||
}, ROLL_MS / speed)
|
||||
return () => {
|
||||
window.clearInterval(flicker)
|
||||
window.clearTimeout(stop)
|
||||
|
||||
+134
-32
@@ -1710,7 +1710,7 @@ h3 {
|
||||
its former neighbors glide into the gap. */
|
||||
.card-ghost {
|
||||
z-index: 3;
|
||||
animation: card-ghost-out 280ms cubic-bezier(0.4, 0, 0.7, 0.4) forwards;
|
||||
animation: card-ghost-out 240ms cubic-bezier(0.4, 0, 0.7, 0.4) forwards;
|
||||
}
|
||||
|
||||
@keyframes card-ghost-out {
|
||||
@@ -1886,11 +1886,43 @@ h3 {
|
||||
/* ---------- battle ---------- */
|
||||
|
||||
.battle {
|
||||
/* Playback-speed multiplier, set inline from the speed picker. Every battle
|
||||
animation divides its base duration by this, so the visuals quicken in
|
||||
lockstep with the JS step timers. Fallback keeps calc() valid if unset. */
|
||||
--battle-speed: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
/* Speed picker in the playback controls. */
|
||||
.battle-speed {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-left: 4px;
|
||||
padding-left: 6px;
|
||||
border-left: 1px solid rgba(253, 243, 220, 0.18);
|
||||
}
|
||||
|
||||
.battle-speed-label {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.7;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.battle-speed-btn {
|
||||
min-width: 2.6em;
|
||||
padding: 3px 5px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.battle-speed-btn.is-active {
|
||||
background: var(--gold);
|
||||
color: #2a1d0c;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.battle-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -2073,7 +2105,7 @@ h3 {
|
||||
left: 50%;
|
||||
margin-left: -42px;
|
||||
z-index: 6;
|
||||
animation: summon-pop 1000ms ease forwards;
|
||||
animation: summon-pop calc(720ms / var(--battle-speed)) ease forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -2151,7 +2183,7 @@ h3 {
|
||||
z-index: 60;
|
||||
font-size: 2rem;
|
||||
pointer-events: none;
|
||||
animation: spawn-fly 1000ms ease-out forwards;
|
||||
animation: spawn-fly 760ms ease-out forwards;
|
||||
filter: drop-shadow(0 3px 4px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
@@ -2179,7 +2211,7 @@ h3 {
|
||||
position: fixed;
|
||||
z-index: 60;
|
||||
transform-origin: top left;
|
||||
animation: buy-fly 480ms cubic-bezier(0.5, 0.02, 0.3, 1) forwards;
|
||||
animation: buy-fly 380ms cubic-bezier(0.5, 0.02, 0.3, 1) forwards;
|
||||
pointer-events: none;
|
||||
filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
@@ -2244,7 +2276,7 @@ h3 {
|
||||
|
||||
.setaside-card.is-leaving,
|
||||
.food-fan-card.is-leaving {
|
||||
animation: card-leave 400ms cubic-bezier(0.4, 0, 0.7, 0.4) forwards;
|
||||
animation: card-leave calc(300ms / var(--battle-speed)) cubic-bezier(0.4, 0, 0.7, 0.4) forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -2274,11 +2306,11 @@ h3 {
|
||||
}
|
||||
|
||||
.food-fan-card.food-in-bottom {
|
||||
animation: food-in-bottom 440ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
|
||||
animation: food-in-bottom calc(320ms / var(--battle-speed)) cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
|
||||
}
|
||||
|
||||
.food-fan-card.food-in-top {
|
||||
animation: food-in-top 440ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
|
||||
animation: food-in-top calc(320ms / var(--battle-speed)) cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
|
||||
}
|
||||
|
||||
/* A pet that faints with a pending effect slides out of the arena into the
|
||||
@@ -2298,7 +2330,7 @@ h3 {
|
||||
}
|
||||
|
||||
.setaside-card.setaside-in {
|
||||
animation: setaside-in 600ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
|
||||
animation: setaside-in calc(440ms / var(--battle-speed)) cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
|
||||
}
|
||||
|
||||
.battle-unit {
|
||||
@@ -2354,12 +2386,12 @@ h3 {
|
||||
|
||||
.battle-unit.unit-reveal-bottom {
|
||||
transform-origin: center bottom;
|
||||
animation: unit-reveal-bottom 520ms cubic-bezier(0.2, 0.8, 0.3, 1);
|
||||
animation: unit-reveal-bottom calc(380ms / var(--battle-speed)) cubic-bezier(0.2, 0.8, 0.3, 1);
|
||||
}
|
||||
|
||||
.battle-unit.unit-reveal-top {
|
||||
transform-origin: center top;
|
||||
animation: unit-reveal-top 520ms cubic-bezier(0.2, 0.8, 0.3, 1);
|
||||
animation: unit-reveal-top calc(380ms / var(--battle-speed)) cubic-bezier(0.2, 0.8, 0.3, 1);
|
||||
}
|
||||
|
||||
/* --- rock dice --- */
|
||||
@@ -2402,13 +2434,13 @@ h3 {
|
||||
}
|
||||
|
||||
.droll.is-rolling {
|
||||
animation: droll-shake 170ms linear infinite;
|
||||
animation-delay: calc(var(--i) * 45ms);
|
||||
animation: droll-shake calc(150ms / var(--battle-speed)) linear infinite;
|
||||
animation-delay: calc(var(--i) * 45ms / var(--battle-speed));
|
||||
}
|
||||
|
||||
.droll.is-settled {
|
||||
animation: droll-settle 280ms cubic-bezier(0.2, 0.85, 0.3, 1) both;
|
||||
animation-delay: calc(var(--i) * 55ms);
|
||||
animation: droll-settle calc(240ms / var(--battle-speed)) cubic-bezier(0.2, 0.85, 0.3, 1) both;
|
||||
animation-delay: calc(var(--i) * 55ms / var(--battle-speed));
|
||||
}
|
||||
|
||||
@keyframes droll-shake {
|
||||
@@ -2494,7 +2526,7 @@ h3 {
|
||||
font-size: 1rem;
|
||||
color: #7be495;
|
||||
text-shadow: 0 2px 0 rgba(0, 0, 0, 0.5);
|
||||
animation: fx-pop 1000ms ease forwards;
|
||||
animation: fx-pop calc(760ms / var(--battle-speed)) ease forwards;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
white-space: nowrap;
|
||||
@@ -2504,43 +2536,113 @@ h3 {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
/* You (bottom) lunge up into the seam; the opponent (top) lunges down. */
|
||||
/* You (bottom) lunge up into the seam; the opponent (top) lunges down. A quick
|
||||
crouch of anticipation, an explosive lunge, then an impact recoil with a
|
||||
scale punch and a little shake before settling — reads as a real collision
|
||||
rather than a gentle bump. */
|
||||
@keyframes clash-bottom {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
transform: translateY(0) scale(1) rotate(0);
|
||||
}
|
||||
35% {
|
||||
transform: translateY(-26px) rotate(2deg);
|
||||
12% {
|
||||
transform: translateY(10px) scale(0.95);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(6px);
|
||||
40% {
|
||||
transform: translateY(-34px) scale(1.09) rotate(2.5deg);
|
||||
}
|
||||
52% {
|
||||
transform: translateY(-21px) scale(0.98) rotate(-2.5deg);
|
||||
}
|
||||
67% {
|
||||
transform: translateY(8px) rotate(1.5deg);
|
||||
}
|
||||
83% {
|
||||
transform: translateY(-3px) rotate(-0.5deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
transform: translateY(0) scale(1) rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes clash-top {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
transform: translateY(0) scale(1) rotate(0);
|
||||
}
|
||||
35% {
|
||||
transform: translateY(26px) rotate(-2deg);
|
||||
12% {
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-6px);
|
||||
40% {
|
||||
transform: translateY(34px) scale(1.09) rotate(-2.5deg);
|
||||
}
|
||||
52% {
|
||||
transform: translateY(21px) scale(0.98) rotate(2.5deg);
|
||||
}
|
||||
67% {
|
||||
transform: translateY(-8px) rotate(-1.5deg);
|
||||
}
|
||||
83% {
|
||||
transform: translateY(3px) rotate(0.5deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
transform: translateY(0) scale(1) rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.battle-unit.clash-bottom {
|
||||
animation: clash-bottom 500ms ease;
|
||||
animation: clash-bottom calc(460ms / var(--battle-speed)) cubic-bezier(0.36, 0.7, 0.3, 1);
|
||||
}
|
||||
|
||||
.battle-unit.clash-top {
|
||||
animation: clash-top 500ms ease;
|
||||
animation: clash-top calc(460ms / var(--battle-speed)) cubic-bezier(0.36, 0.7, 0.3, 1);
|
||||
}
|
||||
|
||||
/* When the two pets meet, the centre seam cracks: the bolt flares and a bright
|
||||
shockwave ring bursts outward from the collision point. */
|
||||
@keyframes clash-bolt {
|
||||
0% {
|
||||
transform: scale(0.5) rotate(-12deg);
|
||||
opacity: 0.5;
|
||||
filter: drop-shadow(0 0 6px rgba(246, 201, 78, 0.5));
|
||||
}
|
||||
30% {
|
||||
transform: scale(1.7) rotate(6deg);
|
||||
opacity: 1;
|
||||
filter: drop-shadow(0 0 18px rgba(255, 236, 150, 0.95));
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotate(0);
|
||||
opacity: 0.5;
|
||||
filter: drop-shadow(0 0 8px rgba(246, 201, 78, 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes clash-shock {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(0.2);
|
||||
opacity: 0.85;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(2.6);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.battle-center-clash .battle-center-bolt {
|
||||
animation: clash-bolt calc(420ms / var(--battle-speed)) cubic-bezier(0.3, 0.8, 0.3, 1);
|
||||
}
|
||||
|
||||
.battle-center-clash::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid rgba(255, 232, 150, 0.9);
|
||||
box-shadow: 0 0 14px rgba(255, 224, 130, 0.7);
|
||||
animation: clash-shock calc(420ms / var(--battle-speed)) ease-out forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes dying {
|
||||
@@ -2551,7 +2653,7 @@ h3 {
|
||||
}
|
||||
|
||||
.battle-unit.unit-dying {
|
||||
animation: dying 700ms ease 500ms forwards;
|
||||
animation: dying calc(480ms / var(--battle-speed)) ease calc(360ms / var(--battle-speed)) forwards;
|
||||
}
|
||||
|
||||
@keyframes damage-pop {
|
||||
@@ -2577,7 +2679,7 @@ h3 {
|
||||
font-size: 1.4rem;
|
||||
color: #ff6b52;
|
||||
text-shadow: 0 2px 0 rgba(0, 0, 0, 0.5);
|
||||
animation: damage-pop 1100ms ease 250ms forwards;
|
||||
animation: damage-pop calc(700ms / var(--battle-speed)) ease calc(160ms / var(--battle-speed)) forwards;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
// Playback speed for the battle replay. A multiplier: 2 means everything (JS
|
||||
// step timers and CSS animation durations alike) runs twice as fast. The choice
|
||||
// is remembered across sessions in localStorage so a player who likes it quick
|
||||
// doesn't have to re-set it every battle.
|
||||
|
||||
const STORAGE_KEY = 'sap:battleSpeed'
|
||||
|
||||
// The values offered in the speed picker. 1 is the honest, savour-it pace;
|
||||
// higher values are for players who've seen enough clashes for one evening.
|
||||
export const SPEED_OPTIONS = [0.5, 1, 1.5, 2, 3] as const
|
||||
|
||||
const DEFAULT_SPEED = 1
|
||||
|
||||
function load(): number {
|
||||
if (typeof localStorage === 'undefined') return DEFAULT_SPEED
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
const n = raw ? Number(raw) : NaN
|
||||
// Guard against stale/garbage values, and snap to a known option.
|
||||
return SPEED_OPTIONS.includes(n as (typeof SPEED_OPTIONS)[number]) ? n : DEFAULT_SPEED
|
||||
}
|
||||
|
||||
export function useBattleSpeed(): [number, (n: number) => void] {
|
||||
const [speed, setSpeed] = useState<number>(load)
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, String(speed))
|
||||
} catch {
|
||||
// Private-mode / storage-disabled: keep the in-memory value, just don't persist.
|
||||
}
|
||||
}, [speed])
|
||||
|
||||
const set = useCallback((n: number) => setSpeed(n), [])
|
||||
return [speed, set]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/App.tsx","./src/anim.ts","./src/api.ts","./src/main.tsx","./src/petArt.ts","./src/types.ts","./src/useCatalog.ts","./src/useGame.ts","./src/vite-env.d.ts","./src/components/ArrangePhase.tsx","./src/components/BattlePhase.tsx","./src/components/CardView.tsx","./src/components/DebugPanel.tsx","./src/components/DiceRoll.tsx","./src/components/EventLog.tsx","./src/components/GameOver.tsx","./src/components/Home.tsx","./src/components/Lobby.tsx","./src/components/ShopPhase.tsx","./src/components/Table.tsx"],"version":"5.9.3"}
|
||||
{"root":["./src/App.tsx","./src/anim.ts","./src/api.ts","./src/main.tsx","./src/petArt.ts","./src/types.ts","./src/useBattleSpeed.ts","./src/useCatalog.ts","./src/useGame.ts","./src/vite-env.d.ts","./src/components/ArrangePhase.tsx","./src/components/BattlePhase.tsx","./src/components/CardView.tsx","./src/components/DebugPanel.tsx","./src/components/DiceRoll.tsx","./src/components/EventLog.tsx","./src/components/GameOver.tsx","./src/components/Home.tsx","./src/components/Lobby.tsx","./src/components/ShopPhase.tsx","./src/components/Table.tsx"],"version":"5.9.3"}
|
||||
Reference in New Issue
Block a user