More UX improvements.
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user