From 10b634687471d8141b027619eb65ff479d47863f Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 24 Jul 2026 09:07:54 -0400 Subject: [PATCH] More UX improvements. --- README.md | 31 ++++++++++++++++++++++++++++++- internal/game/cards.go | 16 ++++++++++------ internal/game/game.go | 4 ++-- internal/server/server.go | 11 ++++++++--- web/src/api.ts | 5 +++-- web/src/components/DebugPanel.tsx | 11 ++++++----- web/src/components/Table.tsx | 2 +- 7 files changed, 60 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4e0cf22..d7a9f14 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,13 @@ Six rounds, each with its own shop tier deck. Per round: Apples and bees are **temporary**: they leave your deck after the battle. Most trophies after round 6 wins. -All six tiers use the real card data: +## Packs + +Two card packs are playable, chosen by the host in the lobby (a third, +Unicorn, is stubbed for later). Each pack is six tiers, one per round, and +every pet ships as two copies. + +### Turtle pack | Tier | Pets | Food | | ---- | ---- | ---- | @@ -92,6 +98,29 @@ All six tiers use the real card data: | 5 | Monkey, Rhino, Crocodile, Scorpion, Seal, Shark, Turkey | Chili | | 6 | Gorilla, Fly, Leopard, Mammoth, Cat, Snake, Wolverine | Melon | +### Golden pack + +| Tier | Pets | Food | +| ---- | ---- | ---- | +| 1 | Groundhog, Pied Tamarin, Chipmunk, Cone Snail, Bulldog, Opossum | — | +| 2 | Black-Necked Stilt, Lizard, Hercules Beetle, Stoat, Desert Rain Frog, Honduran White Bat | — | +| 3 | Guinea Fowl, Surgeon Fish, Osprey, Anteater, Bear, Royal Flycatcher, Flea | Avocado | +| 4 | Saiga Antelope, Vaquita, Poison Dart Frog, Manta Ray, Slug, Cockatoo, Manatee | Potato | +| 5 | Nyala, Nurse Shark, Giant Isopod, Blue-Ringed Octopus, Raccoon, Fire Ant, Macaque | Durian | +| 6 | Highland Cow, Wildebeest, Grizzly Bear, Catfish, Komodo, Bird of Paradise, German Shepherd | Tomato | + +The Golden pack adds mechanics the Turtle pack doesn't have: + +- **Trumpets** — an ephemeral battle resource pets earn and spend mid-fight. +- **Golden Retriever** — once per battle, a side that runs out of cards but + still holds Trumpets fields one, its power equal to those Trumpets. +- **Avocado** — a persistent set-aside token you can discard in place of gold. +- **Nurse Shark** — the one interactive battle moment: the fight pauses and + asks how many Trumpets to spend on rocks (the bot answers with a fixed + policy, so simulated rollouts stay valid). +- **Cockatoo** — a shop-time reveal, plus Manta Ray's free first buy, + Blue-Ringed Octopus' per-buy apples, and more. + ## Layout ``` diff --git a/internal/game/cards.go b/internal/game/cards.go index 016e8ce..98dda1b 100644 --- a/internal/game/cards.go +++ b/internal/game/cards.go @@ -901,13 +901,17 @@ 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 +// Catalog returns the default pack's representative cards. +func Catalog() []Card { return CatalogForPack(DefaultPack) } + +// CatalogForPack returns one representative card for every pet and food in a +// pack, 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 { +func CatalogForPack(pack string) []Card { + pets, foods := packTiers(pack) var cards []Card - for tierIdx := range petTiers { - for _, t := range petTiers[tierIdx] { + for tierIdx := range pets { + for _, t := range pets[tierIdx] { suit := SuitRed if len(t.Suits) > 0 { suit = t.Suits[0] @@ -917,7 +921,7 @@ func Catalog() []Card { Power: t.Power, Suit: suit, Effects: t.Effects, EffectText: t.EffectText, }) } - for _, f := range foodTiers[tierIdx] { + for _, f := range foods[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, diff --git a/internal/game/game.go b/internal/game/game.go index 2d6ab3b..30baf1c 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -617,12 +617,12 @@ func (g *Game) applyShopTrigger(p *Player, c Card, trigger EffectTrigger) { // inside resolveBattle, so battlePrep is excluded here. if trigger == TriggerSell || trigger == TriggerBuy { p.PendingApplesInPlay += e.count() - g.logf(p.Seat, "🍎", "%s sets up %d apple%s in play for the battle.", c.Name, e.count(), plural(e.count())) + g.logf(p.Seat, "🍎", "%s — %s will start the next battle with %d apple%s in play.", c.Name, p.Name, e.count(), plural(e.count())) } case ActionStartTrumpets: // Golden pack: Bird of Paradise banks Trumpets for the next battle. p.PendingTrumpets += e.count() - g.logf(p.Seat, "🎺", "%s sets up %d Trumpet%s in play for the battle.", c.Name, e.count(), plural(e.count())) + g.logf(p.Seat, "🎺", "%s — %s will start the next battle with %d Trumpet%s.", c.Name, p.Name, e.count(), plural(e.count())) case ActionReactivateBuys: // Golden pack: Catfish re-fires every pet's Buy ability at battle prep. if trigger == TriggerBattlePrep { diff --git a/internal/server/server.go b/internal/server/server.go index e3ebd14..6120798 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -51,9 +51,14 @@ func (s *Server) Handler() http.Handler { 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()) +// handleCatalog returns every card in a pack (?pack=…, default Turtle), for the +// debug panel. Unknown packs fall back to the default. +func (s *Server) handleCatalog(w http.ResponseWriter, req *http.Request) { + pack := req.URL.Query().Get("pack") + if pack == "" { + pack = game.DefaultPack + } + writeJSON(w, game.CatalogForPack(pack)) } // room is one live game plus its connections. diff --git a/web/src/api.ts b/web/src/api.ts index 09fe012..199ac42 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -23,8 +23,9 @@ export function joinGame(code: string, name: string): Promise { return post('/api/join', { code, name }) } -export async function fetchCatalog(): Promise { - const res = await fetch('/api/catalog') +export async function fetchCatalog(pack?: string): Promise { + const url = pack ? `/api/catalog?pack=${encodeURIComponent(pack)}` : '/api/catalog' + const res = await fetch(url) if (!res.ok) throw new Error('failed to load catalog') return (await res.json()) as Card[] } diff --git a/web/src/components/DebugPanel.tsx b/web/src/components/DebugPanel.tsx index 545bc5c..69e16c4 100644 --- a/web/src/components/DebugPanel.tsx +++ b/web/src/components/DebugPanel.tsx @@ -5,21 +5,22 @@ import { CardView } from './CardView' interface Props { canGrant: boolean // shop phase — grants only land then + pack: string // active pack, so the catalog matches the game 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) { +// listing every card in the active pack, tier by tier. Clicking one drops it +// into your deck for free, off-turn. +export function DebugPanel({ canGrant, pack, send }: Props) { const [open, setOpen] = useState(false) const [catalog, setCatalog] = useState([]) useEffect(() => { - fetchCatalog() + fetchCatalog(pack) .then(setCatalog) .catch(() => setCatalog([])) - }, []) + }, [pack]) const tiers = [...new Set(catalog.map((c) => c.tier ?? 0))].sort((a, b) => a - b) diff --git a/web/src/components/Table.tsx b/web/src/components/Table.tsx index bbd01fb..b144dd3 100644 --- a/web/src/components/Table.tsx +++ b/web/src/components/Table.tsx @@ -157,7 +157,7 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v )} {error &&
{error}
} {view.debug && ( - + )} )