Files
super-auto-pets-board-game/web/src/components/ArrangePhase.tsx
T

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>
)
}