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:
+15
-1
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,6 +18,15 @@ import (
|
|||||||
"github.com/greyson/super-auto-pets-board-game/internal/store"
|
"github.com/greyson/super-auto-pets-board-game/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// isTruthy reports whether an env value means "on".
|
||||||
|
func isTruthy(v string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||||
|
case "1", "true", "yes", "on":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := env.Load(".env"); err != nil {
|
if err := env.Load(".env"); err != nil {
|
||||||
slog.Error("failed to read .env", "err", err)
|
slog.Error("failed to read .env", "err", err)
|
||||||
@@ -25,6 +35,10 @@ func main() {
|
|||||||
dataDir := env.Get("DATA_DIR", "data")
|
dataDir := env.Get("DATA_DIR", "data")
|
||||||
port := env.Get("PORT", "8080")
|
port := env.Get("PORT", "8080")
|
||||||
staticDir := env.Get("STATIC_DIR", "web/dist")
|
staticDir := env.Get("STATIC_DIR", "web/dist")
|
||||||
|
debug := isTruthy(env.Get("DEBUG", ""))
|
||||||
|
if debug {
|
||||||
|
slog.Warn("DEBUG mode on: the buy-any-card panel is enabled")
|
||||||
|
}
|
||||||
|
|
||||||
st, err := store.Open(dataDir)
|
st, err := store.Open(dataDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -40,7 +54,7 @@ func main() {
|
|||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: ":" + port,
|
Addr: ":" + port,
|
||||||
Handler: server.New(st, staticDir).Handler(),
|
Handler: server.New(st, staticDir, debug).Handler(),
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -519,6 +519,60 @@ func (g *Game) buildShopDecks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Catalog returns one representative card for every pet and food in the game,
|
||||||
|
// tier by tier, for the debug "buy any card" panel. IDs are name-based
|
||||||
|
// placeholders (not real instances); pets use their first printed suit.
|
||||||
|
func Catalog() []Card {
|
||||||
|
var cards []Card
|
||||||
|
for tierIdx := range petTiers {
|
||||||
|
for _, t := range petTiers[tierIdx] {
|
||||||
|
suit := SuitRed
|
||||||
|
if len(t.Suits) > 0 {
|
||||||
|
suit = t.Suits[0]
|
||||||
|
}
|
||||||
|
cards = append(cards, Card{
|
||||||
|
ID: "pet-" + t.Name, Kind: KindPet, Name: t.Name, Tier: tierIdx + 1,
|
||||||
|
Power: t.Power, Suit: suit, Effects: t.Effects, EffectText: t.EffectText,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, f := range foodTiers[tierIdx] {
|
||||||
|
cards = append(cards, Card{
|
||||||
|
ID: "food-" + f.Name, Kind: KindFood, Name: f.Name, Tier: tierIdx + 1,
|
||||||
|
Food: f.Food, Perk: f.Perk, Effects: f.Effects, EffectText: f.EffectText,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cards
|
||||||
|
}
|
||||||
|
|
||||||
|
// cardByName mints a fresh instance of the named pet or food from its
|
||||||
|
// template (pets take their first printed suit). Returns false if unknown.
|
||||||
|
func (g *Game) cardByName(name string) (Card, bool) {
|
||||||
|
for tierIdx := range petTiers {
|
||||||
|
for _, t := range petTiers[tierIdx] {
|
||||||
|
if t.Name == name {
|
||||||
|
suit := SuitRed
|
||||||
|
if len(t.Suits) > 0 {
|
||||||
|
suit = t.Suits[0]
|
||||||
|
}
|
||||||
|
return Card{
|
||||||
|
ID: g.newCardID(), Kind: KindPet, Name: t.Name, Tier: tierIdx + 1,
|
||||||
|
Power: t.Power, Suit: suit, Effects: t.Effects, EffectText: t.EffectText,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, f := range foodTiers[tierIdx] {
|
||||||
|
if f.Name == name {
|
||||||
|
return Card{
|
||||||
|
ID: g.newCardID(), Kind: KindFood, Name: f.Name, Tier: tierIdx + 1,
|
||||||
|
Food: f.Food, Perk: f.Perk, Effects: f.Effects, EffectText: f.EffectText,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Card{}, false
|
||||||
|
}
|
||||||
|
|
||||||
// newApple mints an apple food card. Apples are temporary: they vanish from
|
// newApple mints an apple food card. Apples are temporary: they vanish from
|
||||||
// the deck after the next battle.
|
// the deck after the next battle.
|
||||||
func (g *Game) newApple() Card {
|
func (g *Game) newApple() Card {
|
||||||
|
|||||||
@@ -430,6 +430,25 @@ func (g *Game) TradeChoose(playerID string, pick int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DebugGrant drops any card straight into a player's deck during the shop,
|
||||||
|
// free and off-turn — a testing aid gated behind the server's DEBUG flag, not
|
||||||
|
// a normal action. No buy effects fire.
|
||||||
|
func (g *Game) DebugGrant(playerID, name string) error {
|
||||||
|
if g.Phase != PhaseShop && g.Phase != PhaseCleanup {
|
||||||
|
return ErrWrongPhase
|
||||||
|
}
|
||||||
|
p := g.PlayerByID(playerID)
|
||||||
|
if p == nil {
|
||||||
|
return errors.New("unknown player")
|
||||||
|
}
|
||||||
|
c, ok := g.cardByName(name)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%w: no card named %q", ErrInvalidAction, name)
|
||||||
|
}
|
||||||
|
p.Deck = append(p.Deck, c)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Pass forfeits the player's remaining coins and ends their shopping.
|
// Pass forfeits the player's remaining coins and ends their shopping.
|
||||||
func (g *Game) Pass(playerID string) error {
|
func (g *Game) Pass(playerID string) error {
|
||||||
p, err := g.requireShopTurn(playerID)
|
p, err := g.requireShopTurn(playerID)
|
||||||
|
|||||||
@@ -482,3 +482,33 @@ func TestViewHidesSecrets(t *testing.T) {
|
|||||||
t.Fatal("trade options must be visible to the trader")
|
t.Fatal("trade options must be visible to the trader")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDebugGrant(t *testing.T) {
|
||||||
|
g, p1, _ := testGame(t)
|
||||||
|
before := len(p1.Deck)
|
||||||
|
if err := g.DebugGrant(p1.ID, "Ant"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(p1.Deck) != before+1 || p1.Deck[before].Name != "Ant" || !p1.Deck[before].IsPet() {
|
||||||
|
t.Fatalf("granted card should be an Ant appended to the deck: %+v", p1.Deck)
|
||||||
|
}
|
||||||
|
if p1.Deck[before].ID == "" {
|
||||||
|
t.Fatal("granted card should get a real instance ID")
|
||||||
|
}
|
||||||
|
// Unknown card name is rejected.
|
||||||
|
if err := g.DebugGrant(p1.ID, "Nonexistent"); err == nil {
|
||||||
|
t.Fatal("unknown card name should error")
|
||||||
|
}
|
||||||
|
// Foods can be granted too.
|
||||||
|
if err := g.DebugGrant(p1.ID, "Honey"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !Catalog()[0].IsPet() { // sanity on the shared catalog helper
|
||||||
|
t.Fatal("catalog should start with a pet")
|
||||||
|
}
|
||||||
|
// Not allowed outside shop/cleanup.
|
||||||
|
g.Phase = PhaseBattle
|
||||||
|
if err := g.DebugGrant(p1.ID, "Ant"); err == nil {
|
||||||
|
t.Fatal("grant should be rejected outside the shop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ type View struct {
|
|||||||
Pending *PendingTrade `json:"pending,omitempty"`
|
Pending *PendingTrade `json:"pending,omitempty"`
|
||||||
Battle *BattleResult `json:"battle,omitempty"`
|
Battle *BattleResult `json:"battle,omitempty"`
|
||||||
WinnerSeat int `json:"winnerSeat"`
|
WinnerSeat int `json:"winnerSeat"`
|
||||||
|
// Debug is set by the server when its DEBUG flag is on, unlocking the
|
||||||
|
// client's "buy any card" panel. Not part of the pure game state.
|
||||||
|
Debug bool `json:"debug,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ViewFor builds the state visible to the given player.
|
// ViewFor builds the state visible to the given player.
|
||||||
|
|||||||
@@ -22,17 +22,20 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
store *store.Store
|
store *store.Store
|
||||||
staticDir string
|
staticDir string
|
||||||
|
debug bool // DEBUG mode: unlocks the "buy any card" panel
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
rooms map[string]*room // by game ID
|
rooms map[string]*room // by game ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a server. staticDir is the built frontend to serve (may be
|
// New creates a server. staticDir is the built frontend to serve (may be
|
||||||
// empty or missing during backend-only development).
|
// empty or missing during backend-only development). debug unlocks testing
|
||||||
func New(st *store.Store, staticDir string) *Server {
|
// aids like the debug card panel.
|
||||||
|
func New(st *store.Store, staticDir string, debug bool) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
store: st,
|
store: st,
|
||||||
staticDir: staticDir,
|
staticDir: staticDir,
|
||||||
|
debug: debug,
|
||||||
rooms: make(map[string]*room),
|
rooms: make(map[string]*room),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,15 +46,22 @@ func (s *Server) Handler() http.Handler {
|
|||||||
mux.HandleFunc("POST /api/games", s.handleCreate)
|
mux.HandleFunc("POST /api/games", s.handleCreate)
|
||||||
mux.HandleFunc("POST /api/join", s.handleJoin)
|
mux.HandleFunc("POST /api/join", s.handleJoin)
|
||||||
mux.HandleFunc("GET /api/ws", s.handleWS)
|
mux.HandleFunc("GET /api/ws", s.handleWS)
|
||||||
|
mux.HandleFunc("GET /api/catalog", s.handleCatalog)
|
||||||
mux.HandleFunc("/", s.handleStatic)
|
mux.HandleFunc("/", s.handleStatic)
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleCatalog returns every card in the game, for the debug panel.
|
||||||
|
func (s *Server) handleCatalog(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
writeJSON(w, game.Catalog())
|
||||||
|
}
|
||||||
|
|
||||||
// room is one live game plus its connections.
|
// room is one live game plus its connections.
|
||||||
type room struct {
|
type room struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
game *game.Game
|
game *game.Game
|
||||||
conns map[*client]struct{}
|
conns map[*client]struct{}
|
||||||
|
debug bool // mirrors Server.debug, for broadcastLocked
|
||||||
}
|
}
|
||||||
|
|
||||||
// getRoom returns the room for a game ID, loading it from the store if it
|
// getRoom returns the room for a game ID, loading it from the store if it
|
||||||
@@ -66,7 +76,7 @@ func (s *Server) getRoom(gameID string) (*room, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r := &room{game: g, conns: make(map[*client]struct{})}
|
r := &room{game: g, conns: make(map[*client]struct{}), debug: s.debug}
|
||||||
s.rooms[gameID] = r
|
s.rooms[gameID] = r
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
@@ -117,7 +127,7 @@ func (s *Server) handleCreate(w http.ResponseWriter, req *http.Request) {
|
|||||||
httpError(w, http.StatusBadRequest, err.Error())
|
httpError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
r := &room{game: g, conns: make(map[*client]struct{})}
|
r := &room{game: g, conns: make(map[*client]struct{}), debug: s.debug}
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.rooms[g.ID] = r
|
s.rooms[g.ID] = r
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type clientMessage struct {
|
|||||||
Cards []string `json:"cards"` // discard, trade
|
Cards []string `json:"cards"` // discard, trade
|
||||||
Pick int `json:"pick"` // tradeChoose
|
Pick int `json:"pick"` // tradeChoose
|
||||||
Order []string `json:"order"` // arrange
|
Order []string `json:"order"` // arrange
|
||||||
|
Name string `json:"name"` // debugAdd
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverMessage struct {
|
type serverMessage struct {
|
||||||
@@ -133,6 +134,12 @@ func (s *Server) apply(r *room, c *client, msg clientMessage) {
|
|||||||
err = g.SubmitOrder(c.playerID, msg.Order)
|
err = g.SubmitOrder(c.playerID, msg.Order)
|
||||||
case "ready":
|
case "ready":
|
||||||
err = g.AcknowledgeBattle(c.playerID)
|
err = g.AcknowledgeBattle(c.playerID)
|
||||||
|
case "debugAdd":
|
||||||
|
if !s.debug {
|
||||||
|
err = game.ErrInvalidAction
|
||||||
|
} else {
|
||||||
|
err = g.DebugGrant(c.playerID, msg.Name)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err = game.ErrInvalidAction
|
err = game.ErrInvalidAction
|
||||||
}
|
}
|
||||||
@@ -149,6 +156,7 @@ func (s *Server) apply(r *room, c *client, msg clientMessage) {
|
|||||||
func (r *room) broadcastLocked() {
|
func (r *room) broadcastLocked() {
|
||||||
for c := range r.conns {
|
for c := range r.conns {
|
||||||
view := r.game.ViewFor(c.playerID)
|
view := r.game.ViewFor(c.playerID)
|
||||||
|
view.Debug = r.debug
|
||||||
data, err := json.Marshal(serverMessage{Type: "state", State: &view})
|
data, err := json.Marshal(serverMessage{Type: "state", State: &view})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to marshal view", "err", err)
|
slog.Error("failed to marshal view", "err", err)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func TestE2EBattleAckReturnsToShop(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer st.Close()
|
defer st.Close()
|
||||||
srv := New(st, "")
|
srv := New(st, "", false)
|
||||||
ts := httptest.NewServer(srv.Handler())
|
ts := httptest.NewServer(srv.Handler())
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
base := ts.URL
|
base := ts.URL
|
||||||
|
|||||||
+7
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { Session } from './types'
|
import type { Card, Session } from './types'
|
||||||
|
|
||||||
const SESSION_KEY = 'sapbg-session'
|
const SESSION_KEY = 'sapbg-session'
|
||||||
|
|
||||||
@@ -21,6 +21,12 @@ export function joinGame(code: string, name: string): Promise<Session> {
|
|||||||
return post('/api/join', { code, name })
|
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 {
|
export function loadSession(): Session | null {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(SESSION_KEY)
|
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 { ArrangePhase } from './ArrangePhase'
|
||||||
import { BattlePhase } from './BattlePhase'
|
import { BattlePhase } from './BattlePhase'
|
||||||
import { GameOver } from './GameOver'
|
import { GameOver } from './GameOver'
|
||||||
|
import { DebugPanel } from './DebugPanel'
|
||||||
|
|
||||||
// Table connects to the game and routes to the right phase screen.
|
// Table connects to the game and routes to the right phase screen.
|
||||||
export function Table({ session, onLeave }: { session: Session; onLeave: () => void }) {
|
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>
|
<div className="banner banner-warn">An opponent is disconnected…</div>
|
||||||
)}
|
)}
|
||||||
{error && <div className="toast">{error}</div>}
|
{error && <div className="toast">{error}</div>}
|
||||||
|
{view.debug && (
|
||||||
|
<DebugPanel
|
||||||
|
canGrant={view.phase === 'shop' || view.phase === 'cleanup'}
|
||||||
|
send={send}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1276,3 +1276,66 @@ h3 {
|
|||||||
height: 110px;
|
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
|
pending?: PendingTrade
|
||||||
battle?: BattleResult
|
battle?: BattleResult
|
||||||
winnerSeat: number
|
winnerSeat: number
|
||||||
|
debug?: boolean // server DEBUG mode: unlocks the buy-any-card panel
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClientMessage =
|
export type ClientMessage =
|
||||||
@@ -99,6 +100,7 @@ export type ClientMessage =
|
|||||||
| { type: 'pass' }
|
| { type: 'pass' }
|
||||||
| { type: 'arrange'; order: string[] }
|
| { type: 'arrange'; order: string[] }
|
||||||
| { type: 'ready' }
|
| { type: 'ready' }
|
||||||
|
| { type: 'debugAdd'; name: string }
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
gameId: string
|
gameId: string
|
||||||
|
|||||||
Reference in New Issue
Block a user