Initial commit.

This commit is contained in:
Greyson Parrelli
2026-07-22 23:07:29 -04:00
commit 612a4e6227
38 changed files with 6106 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
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. Leftmost card
// fights first; food cards buff the next pet to their right... i.e. foods
// apply "down" to the next pet later in the order. 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 locked = you.ready
// If the server-side deck changes (shouldn't during arrange, but be safe),
// resync.
useEffect(() => {
setOrder(you.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>leftmost</strong> card fights first. Food cards power up the
next pet to their <strong>right</strong>.
{trailingFoods > 0 && (
<span className="warn-text">
{' '}
{trailingFoods} food card{trailingFoods > 1 ? 's' : ''} at the end
will be wasted!
</span>
)}
</p>
<div className="arrange-row">
<div className="arrange-marker"> first</div>
{order.map((c, i) => (
<div
key={c.id}
className="arrange-card"
draggable
onDragStart={() => (dragIndex.current = i)}
onDragOver={(e) => {
e.preventDefault()
if (dragIndex.current !== null && dragIndex.current !== i) {
move(dragIndex.current, i)
dragIndex.current = i
}
}}
onDragEnd={() => (dragIndex.current = null)}
>
<CardView card={c} bonus={bonuses.get(c.id) ?? 0} />
<div className="arrange-arrows">
<button
className="btn btn-ghost btn-sm"
disabled={i === 0}
onClick={() => move(i, i - 1)}
aria-label="move earlier"
>
</button>
<button
className="btn btn-ghost btn-sm"
disabled={i === order.length - 1}
onClick={() => move(i, i + 1)}
aria-label="move later"
>
</button>
</div>
</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>
)
}