Improve dice rolls.
This commit is contained in:
@@ -3,9 +3,13 @@ import type { Dispatch, SetStateAction } from 'react'
|
|||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import type { BattleEvent, Card, ClientMessage, GameView } from '../types'
|
import type { BattleEvent, Card, ClientMessage, GameView } from '../types'
|
||||||
import { CardView } from './CardView'
|
import { CardView } from './CardView'
|
||||||
import { DiceTray } from './Dice'
|
import { DiceRoll, ROLL_MS } from './DiceRoll'
|
||||||
import { artFor } from '../petArt'
|
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 {
|
interface Props {
|
||||||
view: GameView
|
view: GameView
|
||||||
send: (msg: ClientMessage) => void
|
send: (msg: ClientMessage) => void
|
||||||
@@ -34,7 +38,9 @@ const EVENT_MS: Record<BattleEvent['type'], number> = {
|
|||||||
reveal: 800,
|
reveal: 800,
|
||||||
summon: 1000,
|
summon: 1000,
|
||||||
mill: 700,
|
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,
|
clash: 1400,
|
||||||
shield: 900,
|
shield: 900,
|
||||||
strip: 1100,
|
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.
|
// paused freezes auto-advance so the player can walk the log manually.
|
||||||
const [paused, setPaused] = useState(false)
|
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(() => {
|
useEffect(() => {
|
||||||
if (done || paused) return
|
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)
|
const t = window.setTimeout(() => setStep((s) => s + 1), delay)
|
||||||
return () => window.clearTimeout(t)
|
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.
|
// Manual controls. Stepping by hand pauses playback so it doesn't fight you.
|
||||||
const stepTo = (n: number) => {
|
const stepTo = (n: number) => {
|
||||||
@@ -210,9 +232,12 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
|||||||
setStep(0)
|
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(
|
const sides = useMemo(
|
||||||
() => replay(events, battle.stackSizes, step),
|
() => replay(events, battle.stackSizes, upto),
|
||||||
[events, battle.stackSizes, step],
|
[events, battle.stackSizes, upto],
|
||||||
)
|
)
|
||||||
|
|
||||||
const youSeat = view.youSeat
|
const youSeat = view.youSeat
|
||||||
@@ -222,6 +247,14 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
|||||||
const won = battle.winnerSeat === youSeat
|
const won = battle.winnerSeat === youSeat
|
||||||
const draw = battle.winnerSeat < 0
|
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') {
|
function renderSide(seat: number, dir: 'left' | 'right') {
|
||||||
const s = sides[seat]
|
const s = sides[seat]
|
||||||
const clashing = !done && lastEvent?.type === 'clash' && s.unit && !s.unit.dying
|
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 summoning = !done && lastEvent?.type === 'summon' && lastEvent.seat === seat
|
||||||
const milling = !done && lastEvent?.type === 'mill' && lastEvent.seat === seat
|
const milling = !done && lastEvent?.type === 'mill' && lastEvent.seat === seat
|
||||||
const revealing = !done && lastEvent?.type === 'reveal' && 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 lineup = lineups?.[seat] ?? []
|
||||||
const stackEl = (
|
const stackEl = (
|
||||||
@@ -386,18 +420,10 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
|||||||
<div className="battlefield">
|
<div className="battlefield">
|
||||||
{renderSide(youSeat, 'left')}
|
{renderSide(youSeat, 'left')}
|
||||||
<div className="battle-center" aria-hidden>
|
<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>
|
<span className="battle-center-bolt">⚡</span>
|
||||||
</div>
|
</div>
|
||||||
{renderSide(oppSeat, 'right')}
|
{renderSide(oppSeat, 'right')}
|
||||||
|
{rockDice && <DiceRoll key={step} dice={rockDice} side={rockSide} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{peek &&
|
{peek &&
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import type { CSSProperties } from 'react'
|
|
||||||
|
|
||||||
// The rock die has six faces: 0, 0, 1, 1, 2, 2. We lay them on a cube so a
|
|
||||||
// real 3D tumble can settle with the rolled value facing the camera.
|
|
||||||
// front/back = 0, right/left = 1, top/bottom = 2
|
|
||||||
// To bring a value to the front we rotate the cube to whichever face carries
|
|
||||||
// it: 0 -> identity, 1 -> show the right face, 2 -> show the top face.
|
|
||||||
const LAND: Record<number, { rx: number; ry: number }> = {
|
|
||||||
0: { rx: 0, ry: 0 },
|
|
||||||
1: { rx: 0, ry: -90 },
|
|
||||||
2: { rx: 90, ry: 0 },
|
|
||||||
}
|
|
||||||
|
|
||||||
function pips(n: number) {
|
|
||||||
return (
|
|
||||||
<span className={`die-pips die-pips-${n}`} aria-hidden>
|
|
||||||
{Array.from({ length: n }, (_, i) => (
|
|
||||||
<span key={i} className="die-pip" />
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Die({ value, index }: { value: number; index: number }) {
|
|
||||||
const land = LAND[value] ?? LAND[0]
|
|
||||||
// Stagger each die so they don't tumble in lockstep, and give each its own
|
|
||||||
// landing orientation. The keyframe adds the spins on top of these targets.
|
|
||||||
const style = {
|
|
||||||
'--rx': `${land.rx}deg`,
|
|
||||||
'--ry': `${land.ry}deg`,
|
|
||||||
'--delay': `${index * 90}ms`,
|
|
||||||
} as CSSProperties
|
|
||||||
|
|
||||||
// The outer .die carries the drop-shadow; the shadow must live off the
|
|
||||||
// preserve-3d element, since a `filter` collapses 3D children into a plane.
|
|
||||||
return (
|
|
||||||
<div className="die" style={style}>
|
|
||||||
<div className="die-cube">
|
|
||||||
<div className="die-face die-front">{pips(0)}</div>
|
|
||||||
<div className="die-face die-back">{pips(0)}</div>
|
|
||||||
<div className="die-face die-right">{pips(1)}</div>
|
|
||||||
<div className="die-face die-left">{pips(1)}</div>
|
|
||||||
<div className="die-face die-top">{pips(2)}</div>
|
|
||||||
<div className="die-face die-bottom">{pips(2)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiceTray rolls one cube per die face rolled. `key` should change per rock
|
|
||||||
// event so React remounts and replays the tumble.
|
|
||||||
export function DiceTray({ dice }: { dice: number[] }) {
|
|
||||||
return (
|
|
||||||
<div className="dice-tray" aria-hidden>
|
|
||||||
{dice.map((v, i) => (
|
|
||||||
<Die key={i} value={v} index={i} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
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.
|
||||||
|
export const ROLL_MS = 650
|
||||||
|
const TICK_MS = 70
|
||||||
|
|
||||||
|
// DiceRoll shows one rock die per rolled face in fixed slots under the
|
||||||
|
// thrower's side: they shake and flicker through faces in place, then settle
|
||||||
|
// 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: 'left' | 'right' }) {
|
||||||
|
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)
|
||||||
|
const stop = window.setTimeout(() => {
|
||||||
|
window.clearInterval(flicker)
|
||||||
|
setDisplay(dice)
|
||||||
|
setSettled(true)
|
||||||
|
}, ROLL_MS)
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(flicker)
|
||||||
|
window.clearTimeout(stop)
|
||||||
|
}
|
||||||
|
// Runs once per mount; the caller remounts (via key) for each new roll.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`dice-roll dice-roll-${side}`} aria-hidden>
|
||||||
|
{display.map((v, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`droll droll-n-${v} ${settled ? 'is-settled' : 'is-rolling'}`}
|
||||||
|
style={{ '--i': i } as CSSProperties}
|
||||||
|
>
|
||||||
|
{Array.from({ length: v }, (_, r) => (
|
||||||
|
<span key={r} className="droll-rock" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ export function Home({ onSession }: { onSession: (s: Session) => void }) {
|
|||||||
<input
|
<input
|
||||||
value={name}
|
value={name}
|
||||||
maxLength={20}
|
maxLength={20}
|
||||||
placeholder="e.g. Greyson"
|
placeholder="Enter your name"
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
+78
-106
@@ -1187,6 +1187,7 @@ h3 {
|
|||||||
/* The battlefield is the lit heart of the table — a spotlit arena inset into
|
/* The battlefield is the lit heart of the table — a spotlit arena inset into
|
||||||
the felt, with a faint centre line where the two sides meet. */
|
the felt, with a faint centre line where the two sides meet. */
|
||||||
.battlefield {
|
.battlefield {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -1439,136 +1440,107 @@ h3 {
|
|||||||
animation: unit-reveal 500ms ease;
|
animation: unit-reveal 500ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rock-fly-right {
|
/* --- rock dice --- */
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-70px) translateY(-10px) rotate(0);
|
|
||||||
}
|
|
||||||
20% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(70px) translateY(4px) rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rock-fly-left {
|
/* A little tray of dice under the thrower's side. Fixed slots: the dice
|
||||||
0% {
|
scramble in place, then settle showing the rolled rock faces. */
|
||||||
opacity: 0;
|
.dice-roll {
|
||||||
transform: translateX(70px) translateY(-10px) rotate(0);
|
|
||||||
}
|
|
||||||
20% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(-70px) translateY(4px) rotate(-360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rock-fly {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 1.4rem;
|
bottom: 8px;
|
||||||
z-index: 7;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rock-fly-right {
|
|
||||||
animation: rock-fly-right 700ms ease-in forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rock-fly-left {
|
|
||||||
animation: rock-fly-left 700ms ease-in forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- 3D rock dice --- */
|
|
||||||
|
|
||||||
.dice-tray {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 8;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 7px;
|
||||||
perspective: 420px;
|
z-index: 8;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.dice-roll-left {
|
||||||
|
left: 27%;
|
||||||
|
}
|
||||||
|
.dice-roll-right {
|
||||||
|
left: 73%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.die {
|
.droll {
|
||||||
--size: 34px;
|
|
||||||
--half: 17px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: var(--size);
|
width: 34px;
|
||||||
height: var(--size);
|
height: 34px;
|
||||||
/* A filter here would flatten the cube, so it only carries the shadow. */
|
border-radius: 7px;
|
||||||
filter: drop-shadow(0 4px 5px rgba(0, 0, 0, 0.45));
|
box-sizing: border-box;
|
||||||
|
border: 2px solid var(--cocoa);
|
||||||
|
background: linear-gradient(145deg, var(--cream), var(--cream-dark));
|
||||||
|
box-shadow:
|
||||||
|
0 3px 5px rgba(0, 0, 0, 0.45),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.die-cube {
|
.droll.is-rolling {
|
||||||
position: absolute;
|
animation: droll-shake 170ms linear infinite;
|
||||||
inset: 0;
|
animation-delay: calc(var(--i) * 45ms);
|
||||||
transform-style: preserve-3d;
|
|
||||||
/* From a heavily-spun start, settle onto this die's rolled face. */
|
|
||||||
animation: die-roll 720ms cubic-bezier(0.2, 0.8, 0.3, 1) both;
|
|
||||||
animation-delay: var(--delay, 0ms);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes die-roll {
|
.droll.is-settled {
|
||||||
|
animation: droll-settle 280ms cubic-bezier(0.2, 0.85, 0.3, 1) both;
|
||||||
|
animation-delay: calc(var(--i) * 55ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes droll-shake {
|
||||||
0% {
|
0% {
|
||||||
transform: rotateX(calc(var(--rx) - 900deg)) rotateY(calc(var(--ry) - 1080deg));
|
transform: rotate(-9deg) translateY(0) scale(1.06);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate(7deg) translateY(-2px) scale(1.09);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(-6deg) translateY(1px) scale(1.05);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: rotate(8deg) translateY(-1px) scale(1.08);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: rotateX(var(--rx)) rotateY(var(--ry));
|
transform: rotate(-9deg) translateY(0) scale(1.06);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.die-face {
|
@keyframes droll-settle {
|
||||||
|
0% {
|
||||||
|
transform: scale(1.22) rotate(-5deg);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: scale(0.95) rotate(1deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1) rotate(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A rock icon: a lumpy gray boulder. */
|
||||||
|
.droll-rock {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
width: 14px;
|
||||||
display: grid;
|
height: 14px;
|
||||||
place-items: center;
|
background: radial-gradient(circle at 35% 28%, #838a90, #34373c);
|
||||||
border-radius: 6px;
|
clip-path: polygon(22% 6%, 60% 0%, 90% 24%, 100% 58%, 82% 96%, 42% 100%, 8% 82%, 0% 40%);
|
||||||
background: linear-gradient(145deg, var(--cream), var(--cream-dark));
|
|
||||||
border: 2px solid var(--cocoa);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.die-front {
|
.droll-n-1 .droll-rock {
|
||||||
transform: translateZ(var(--half));
|
top: 50%;
|
||||||
}
|
left: 50%;
|
||||||
.die-back {
|
transform: translate(-50%, -50%);
|
||||||
transform: rotateY(180deg) translateZ(var(--half));
|
|
||||||
}
|
|
||||||
.die-right {
|
|
||||||
transform: rotateY(90deg) translateZ(var(--half));
|
|
||||||
}
|
|
||||||
.die-left {
|
|
||||||
transform: rotateY(-90deg) translateZ(var(--half));
|
|
||||||
}
|
|
||||||
.die-top {
|
|
||||||
transform: rotateX(90deg) translateZ(var(--half));
|
|
||||||
}
|
|
||||||
.die-bottom {
|
|
||||||
transform: rotateX(-90deg) translateZ(var(--half));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.die-pips {
|
.droll-n-2 .droll-rock {
|
||||||
display: grid;
|
width: 12px;
|
||||||
gap: 4px;
|
height: 12px;
|
||||||
padding: 5px;
|
|
||||||
}
|
}
|
||||||
.die-pips-1 {
|
.droll-n-2 .droll-rock:nth-child(1) {
|
||||||
grid-template-columns: 1fr;
|
top: 30%;
|
||||||
|
left: 30%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
.die-pips-2 {
|
.droll-n-2 .droll-rock:nth-child(2) {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
top: 70%;
|
||||||
}
|
left: 70%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
.die-pip {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: radial-gradient(circle at 35% 30%, #6b7075, #2c2f33);
|
|
||||||
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.35);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fx-pop {
|
@keyframes fx-pop {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/App.tsx","./src/api.ts","./src/main.tsx","./src/petArt.ts","./src/types.ts","./src/useGame.ts","./src/components/ArrangePhase.tsx","./src/components/BattlePhase.tsx","./src/components/CardView.tsx","./src/components/DebugPanel.tsx","./src/components/Dice.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/api.ts","./src/main.tsx","./src/petArt.ts","./src/types.ts","./src/useGame.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