More UX improvements.

This commit is contained in:
Greyson Parrelli
2026-07-24 09:07:54 -04:00
parent 962e3bb5ff
commit 10b6346874
7 changed files with 60 additions and 20 deletions
+30 -1
View File
@@ -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
```
+10 -6
View File
@@ -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,
+2 -2
View File
@@ -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 {
+8 -3
View File
@@ -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.
+3 -2
View File
@@ -23,8 +23,9 @@ 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')
export async function fetchCatalog(pack?: string): Promise<Card[]> {
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[]
}
+6 -5
View File
@@ -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<Card[]>([])
useEffect(() => {
fetchCatalog()
fetchCatalog(pack)
.then(setCatalog)
.catch(() => setCatalog([]))
}, [])
}, [pack])
const tiers = [...new Set(catalog.map((c) => c.tier ?? 0))].sort((a, b) => a - b)
+1 -1
View File
@@ -157,7 +157,7 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
)}
{error && <div className="toast">{error}</div>}
{view.debug && (
<DebugPanel canGrant={view.phase === 'shop'} send={send} />
<DebugPanel canGrant={view.phase === 'shop'} pack={view.pack} send={send} />
)}
</div>
)