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.
|
Apples and bees are **temporary**: they leave your deck after the battle.
|
||||||
Most trophies after round 6 wins.
|
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 |
|
| Tier | Pets | Food |
|
||||||
| ---- | ---- | ---- |
|
| ---- | ---- | ---- |
|
||||||
@@ -92,6 +98,29 @@ All six tiers use the real card data:
|
|||||||
| 5 | Monkey, Rhino, Crocodile, Scorpion, Seal, Shark, Turkey | Chili |
|
| 5 | Monkey, Rhino, Crocodile, Scorpion, Seal, Shark, Turkey | Chili |
|
||||||
| 6 | Gorilla, Fly, Leopard, Mammoth, Cat, Snake, Wolverine | Melon |
|
| 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
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+10
-6
@@ -901,13 +901,17 @@ func (g *Game) buildShopDecks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Catalog returns one representative card for every pet and food in the game,
|
// Catalog returns the default pack's representative cards.
|
||||||
// tier by tier, for the debug "buy any card" panel. IDs are name-based
|
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.
|
// 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
|
var cards []Card
|
||||||
for tierIdx := range petTiers {
|
for tierIdx := range pets {
|
||||||
for _, t := range petTiers[tierIdx] {
|
for _, t := range pets[tierIdx] {
|
||||||
suit := SuitRed
|
suit := SuitRed
|
||||||
if len(t.Suits) > 0 {
|
if len(t.Suits) > 0 {
|
||||||
suit = 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,
|
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{
|
cards = append(cards, Card{
|
||||||
ID: "food-" + f.Name, Kind: KindFood, Name: f.Name, Tier: tierIdx + 1,
|
ID: "food-" + f.Name, Kind: KindFood, Name: f.Name, Tier: tierIdx + 1,
|
||||||
Food: f.Food, Perk: f.Perk, Effects: f.Effects, EffectText: f.EffectText,
|
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.
|
// inside resolveBattle, so battlePrep is excluded here.
|
||||||
if trigger == TriggerSell || trigger == TriggerBuy {
|
if trigger == TriggerSell || trigger == TriggerBuy {
|
||||||
p.PendingApplesInPlay += e.count()
|
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:
|
case ActionStartTrumpets:
|
||||||
// Golden pack: Bird of Paradise banks Trumpets for the next battle.
|
// Golden pack: Bird of Paradise banks Trumpets for the next battle.
|
||||||
p.PendingTrumpets += e.count()
|
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:
|
case ActionReactivateBuys:
|
||||||
// Golden pack: Catfish re-fires every pet's Buy ability at battle prep.
|
// Golden pack: Catfish re-fires every pet's Buy ability at battle prep.
|
||||||
if trigger == TriggerBattlePrep {
|
if trigger == TriggerBattlePrep {
|
||||||
|
|||||||
@@ -51,9 +51,14 @@ func (s *Server) Handler() http.Handler {
|
|||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleCatalog returns every card in the game, for the debug panel.
|
// handleCatalog returns every card in a pack (?pack=…, default Turtle), for the
|
||||||
func (s *Server) handleCatalog(w http.ResponseWriter, _ *http.Request) {
|
// debug panel. Unknown packs fall back to the default.
|
||||||
writeJSON(w, game.Catalog())
|
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.
|
// 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 })
|
return post('/api/join', { code, name })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchCatalog(): Promise<Card[]> {
|
export async function fetchCatalog(pack?: string): Promise<Card[]> {
|
||||||
const res = await fetch('/api/catalog')
|
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')
|
if (!res.ok) throw new Error('failed to load catalog')
|
||||||
return (await res.json()) as Card[]
|
return (await res.json()) as Card[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,22 @@ import { CardView } from './CardView'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
canGrant: boolean // shop phase — grants only land then
|
canGrant: boolean // shop phase — grants only land then
|
||||||
|
pack: string // active pack, so the catalog matches the game
|
||||||
send: (msg: ClientMessage) => void
|
send: (msg: ClientMessage) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// DebugPanel is a testing aid (server DEBUG mode only): a collapsible drawer
|
// 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
|
// listing every card in the active pack, tier by tier. Clicking one drops it
|
||||||
// your deck for free, off-turn.
|
// into your deck for free, off-turn.
|
||||||
export function DebugPanel({ canGrant, send }: Props) {
|
export function DebugPanel({ canGrant, pack, send }: Props) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [catalog, setCatalog] = useState<Card[]>([])
|
const [catalog, setCatalog] = useState<Card[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCatalog()
|
fetchCatalog(pack)
|
||||||
.then(setCatalog)
|
.then(setCatalog)
|
||||||
.catch(() => setCatalog([]))
|
.catch(() => setCatalog([]))
|
||||||
}, [])
|
}, [pack])
|
||||||
|
|
||||||
const tiers = [...new Set(catalog.map((c) => c.tier ?? 0))].sort((a, b) => a - b)
|
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>}
|
{error && <div className="toast">{error}</div>}
|
||||||
{view.debug && (
|
{view.debug && (
|
||||||
<DebugPanel canGrant={view.phase === 'shop'} send={send} />
|
<DebugPanel canGrant={view.phase === 'shop'} pack={view.pack} send={send} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user