Add DEBUG-gated panel to buy any card in the shop
When the server's DEBUG env var is on, expose a card catalog endpoint and a collapsible client panel that drops any pet or food straight into your deck (free, off-turn). Gated server-side so it is inert in normal play.
This commit is contained in:
+7
-1
@@ -1,4 +1,4 @@
|
||||
import type { Session } from './types'
|
||||
import type { Card, Session } from './types'
|
||||
|
||||
const SESSION_KEY = 'sapbg-session'
|
||||
|
||||
@@ -21,6 +21,12 @@ export function joinGame(code: string, name: string): Promise<Session> {
|
||||
return post('/api/join', { code, name })
|
||||
}
|
||||
|
||||
export async function fetchCatalog(): Promise<Card[]> {
|
||||
const res = await fetch('/api/catalog')
|
||||
if (!res.ok) throw new Error('failed to load catalog')
|
||||
return (await res.json()) as Card[]
|
||||
}
|
||||
|
||||
export function loadSession(): Session | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(SESSION_KEY)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { Card, ClientMessage } from '../types'
|
||||
import { fetchCatalog } from '../api'
|
||||
import { CardView } from './CardView'
|
||||
|
||||
interface Props {
|
||||
canGrant: boolean // shop/cleanup phase — grants only land then
|
||||
send: (msg: ClientMessage) => void
|
||||
}
|
||||
|
||||
// DebugPanel is a testing aid (server DEBUG mode only): a collapsible drawer
|
||||
// listing every card in the game, tier by tier. Clicking one drops it into
|
||||
// your deck for free, off-turn.
|
||||
export function DebugPanel({ canGrant, send }: Props) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [catalog, setCatalog] = useState<Card[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
fetchCatalog()
|
||||
.then(setCatalog)
|
||||
.catch(() => setCatalog([]))
|
||||
}, [])
|
||||
|
||||
const tiers = [...new Set(catalog.map((c) => c.tier ?? 0))].sort((a, b) => a - b)
|
||||
|
||||
return (
|
||||
<div className={`debug-panel ${open ? 'is-open' : ''}`}>
|
||||
<button className="debug-toggle" onClick={() => setOpen((o) => !o)}>
|
||||
🐛 {open ? '›' : '‹'} Debug
|
||||
</button>
|
||||
{open && (
|
||||
<div className="debug-body">
|
||||
<div className="debug-head">
|
||||
Buy any card
|
||||
{!canGrant && <span className="muted"> · only in the shop</span>}
|
||||
</div>
|
||||
{tiers.map((tier) => (
|
||||
<div key={tier} className="debug-tier">
|
||||
<div className="debug-tier-label">Tier {tier}</div>
|
||||
<div className="debug-grid">
|
||||
{catalog
|
||||
.filter((c) => (c.tier ?? 0) === tier)
|
||||
.map((c) => (
|
||||
<CardView
|
||||
key={c.id}
|
||||
card={c}
|
||||
size="sm"
|
||||
disabled={!canGrant}
|
||||
onClick={canGrant ? () => send({ type: 'debugAdd', name: c.name }) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { ShopPhase } from './ShopPhase'
|
||||
import { ArrangePhase } from './ArrangePhase'
|
||||
import { BattlePhase } from './BattlePhase'
|
||||
import { GameOver } from './GameOver'
|
||||
import { DebugPanel } from './DebugPanel'
|
||||
|
||||
// Table connects to the game and routes to the right phase screen.
|
||||
export function Table({ session, onLeave }: { session: Session; onLeave: () => void }) {
|
||||
@@ -69,6 +70,12 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
||||
<div className="banner banner-warn">An opponent is disconnected…</div>
|
||||
)}
|
||||
{error && <div className="toast">{error}</div>}
|
||||
{view.debug && (
|
||||
<DebugPanel
|
||||
canGrant={view.phase === 'shop' || view.phase === 'cleanup'}
|
||||
send={send}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1276,3 +1276,66 @@ h3 {
|
||||
height: 110px;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- debug panel (server DEBUG mode) --- */
|
||||
|
||||
.debug-panel {
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
right: 0;
|
||||
z-index: 120;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.debug-toggle {
|
||||
background: #7a1f1f;
|
||||
color: var(--cream);
|
||||
border: 2px solid var(--cocoa);
|
||||
border-right: none;
|
||||
border-radius: 8px 0 0 8px;
|
||||
font-weight: 800;
|
||||
font-size: 0.8rem;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.debug-panel.is-open .debug-toggle {
|
||||
writing-mode: horizontal-tb;
|
||||
transform: none;
|
||||
border-radius: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.debug-body {
|
||||
width: min(340px, 80vw);
|
||||
max-height: calc(100vh - 80px);
|
||||
overflow-y: auto;
|
||||
background: rgba(18, 53, 31, 0.97);
|
||||
border: 2px solid var(--cocoa);
|
||||
border-radius: 8px 0 0 8px;
|
||||
padding: 12px;
|
||||
box-shadow: -6px 6px 18px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.debug-head {
|
||||
color: var(--gold);
|
||||
font-weight: 800;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.debug-tier-label {
|
||||
color: var(--cream);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
opacity: 0.8;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
|
||||
.debug-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ export interface GameView {
|
||||
pending?: PendingTrade
|
||||
battle?: BattleResult
|
||||
winnerSeat: number
|
||||
debug?: boolean // server DEBUG mode: unlocks the buy-any-card panel
|
||||
}
|
||||
|
||||
export type ClientMessage =
|
||||
@@ -99,6 +100,7 @@ export type ClientMessage =
|
||||
| { type: 'pass' }
|
||||
| { type: 'arrange'; order: string[] }
|
||||
| { type: 'ready' }
|
||||
| { type: 'debugAdd'; name: string }
|
||||
|
||||
export interface Session {
|
||||
gameId: string
|
||||
|
||||
Reference in New Issue
Block a user