155 lines
4.7 KiB
TypeScript
155 lines
4.7 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import type { Card, ClientMessage, GameView, PlayerView } from '../types'
|
|
import { CardView } from './CardView'
|
|
|
|
interface Props {
|
|
view: GameView
|
|
you: PlayerView
|
|
send: (msg: ClientMessage) => void
|
|
}
|
|
|
|
// ArrangePhase lets the player order their deck for battle. Rightmost card
|
|
// fights first; food cards buff the next pet to their left... i.e. foods
|
|
// apply to the next pet later in the play order. Internally `order` stays in
|
|
// play order (index 0 fights first) to match the backend; we only reverse for
|
|
// display. Drag cards or use the arrow buttons to reorder, then lock in.
|
|
export function ArrangePhase({ view, you, send }: Props) {
|
|
const [order, setOrder] = useState<Card[]>(you.deck ?? [])
|
|
const dragIndex = useRef<number | null>(null)
|
|
const [dragging, setDragging] = useState(false)
|
|
const locked = you.ready
|
|
|
|
// Resync only if the deck's actual contents changed — every broadcast
|
|
// creates a fresh array, and blindly resetting would wipe an in-progress
|
|
// ordering whenever the opponent acts.
|
|
useEffect(() => {
|
|
setOrder((prev) => {
|
|
const deck = you.deck ?? []
|
|
const ids = new Set(deck.map((c) => c.id))
|
|
if (prev.length === deck.length && prev.every((c) => ids.has(c.id))) {
|
|
return prev
|
|
}
|
|
return deck
|
|
})
|
|
}, [you.deck])
|
|
|
|
function move(from: number, to: number) {
|
|
if (to < 0 || to >= order.length) return
|
|
setOrder((o) => {
|
|
const next = [...o]
|
|
const [c] = next.splice(from, 1)
|
|
next.splice(to, 0, c)
|
|
return next
|
|
})
|
|
}
|
|
|
|
// Which pets do the foods land on? Compute buff per card for preview.
|
|
const bonuses = new Map<string, number>()
|
|
{
|
|
let pendingApples = 0
|
|
for (const c of order) {
|
|
if (c.kind === 'food') {
|
|
if (c.food === 'apple') pendingApples++
|
|
} else {
|
|
bonuses.set(c.id, pendingApples)
|
|
pendingApples = 0
|
|
}
|
|
}
|
|
}
|
|
const trailingFoods = (() => {
|
|
let n = 0
|
|
for (let i = order.length - 1; i >= 0 && order[i].kind === 'food'; i--) n++
|
|
return n
|
|
})()
|
|
|
|
const opponent = view.players.find((p) => p.seat !== view.youSeat)
|
|
|
|
if (locked) {
|
|
return (
|
|
<div className="centered">
|
|
<h2>Order locked in ⚔️</h2>
|
|
<p className="muted">
|
|
Waiting for {opponent?.name ?? 'your opponent'} to arrange their deck…
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="arrange">
|
|
<div className="shop-status">
|
|
<span className="status-hot">Arrange your battle line</span>
|
|
</div>
|
|
<p className="hint">
|
|
The <strong>rightmost</strong> card fights first. Food cards power up the
|
|
next pet to their <strong>left</strong>.
|
|
{trailingFoods > 0 && (
|
|
<span className="warn-text">
|
|
{' '}
|
|
⚠️ {trailingFoods} food card{trailingFoods > 1 ? 's' : ''} on the far
|
|
left will be wasted!
|
|
</span>
|
|
)}
|
|
</p>
|
|
|
|
<div className="arrange-row">
|
|
{order
|
|
.map((c, i) => ({ c, i }))
|
|
.reverse()
|
|
.map(({ c, i }) => (
|
|
<div
|
|
key={c.id}
|
|
className="arrange-card"
|
|
draggable
|
|
onDragStart={() => {
|
|
dragIndex.current = i
|
|
setDragging(true)
|
|
}}
|
|
onDragOver={(e) => {
|
|
e.preventDefault()
|
|
if (dragIndex.current !== null && dragIndex.current !== i) {
|
|
move(dragIndex.current, i)
|
|
dragIndex.current = i
|
|
}
|
|
}}
|
|
onDragEnd={() => {
|
|
dragIndex.current = null
|
|
setDragging(false)
|
|
}}
|
|
>
|
|
<CardView card={c} bonus={bonuses.get(c.id) ?? 0} noMagnify={dragging} />
|
|
<div className="arrange-arrows">
|
|
<button
|
|
className="btn btn-ghost btn-sm"
|
|
disabled={i === order.length - 1}
|
|
onClick={() => move(i, i + 1)}
|
|
aria-label="move left"
|
|
>
|
|
◀
|
|
</button>
|
|
<button
|
|
className="btn btn-ghost btn-sm"
|
|
disabled={i === 0}
|
|
onClick={() => move(i, i - 1)}
|
|
aria-label="move right"
|
|
>
|
|
▶
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<div className="arrange-marker">⚔️ first</div>
|
|
</div>
|
|
|
|
<div className="actions">
|
|
<button
|
|
className="btn btn-primary btn-big"
|
|
onClick={() => send({ type: 'arrange', order: order.map((c) => c.id) })}
|
|
>
|
|
Lock in & battle ⚔️
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|