diff --git a/internal/game/battle.go b/internal/game/battle.go
index 4d8c27c..b418c47 100644
--- a/internal/game/battle.go
+++ b/internal/game/battle.go
@@ -2,7 +2,6 @@ package game
import (
"fmt"
- "slices"
)
// BattleUnit is a pet in play with its attached foods applied. Power
@@ -1165,19 +1164,20 @@ func (g *Game) runBattle() *BattleResult {
Text: fmt.Sprintf("%s eats an apple (now +%d).", q.unit.Card.Name, q.unit.Bonus)})
}
case ActionShuffleApples:
- // Komodo: only if it's the side's first pet, shuffle apples into
- // random deck positions (deterministic via the battle draw tape).
+ // Komodo: only if it's the side's first pet, add apples to the
+ // remaining deck and shuffle the whole thing so the apples and the
+ // existing pets intermix (not just apples dropped in).
if q.effect.Condition == ConditionFirstPet && sides[q.seat].petsPlayed != 1 {
continue
}
s := sides[q.seat]
for range q.effect.count() {
- pos := g.battleDraw(len(s.stack) + 1)
apple := g.newApple()
- s.stack = slices.Insert(s.stack, pos, apple)
+ s.stack = append(s.stack, apple)
emit(BattleEvent{Type: "summon", Seat: q.seat, Card: &apple,
Text: fmt.Sprintf("%s shuffles an apple into %s's deck.", q.unit.Card.Name, pname(q.seat))})
}
+ g.battleShuffle(s.stack)
case ActionSpendRocks:
// Nurse Shark: spend as many Trumpets as available (up to Count) to
// throw two rocks each.
diff --git a/internal/game/cards.go b/internal/game/cards.go
index e06a221..9e6d60d 100644
--- a/internal/game/cards.go
+++ b/internal/game/cards.go
@@ -196,8 +196,8 @@ const (
ActionRevealForApples EffectAction = "revealForApples"
// --- Golden pack, tier 6 ---
- // ActionShuffleApples (play) shuffles Count apples into random positions of
- // the owner's remaining deck (Komodo). Gated by ConditionFirstPet.
+ // ActionShuffleApples (play) adds Count apples to the owner's remaining
+ // deck and shuffles the whole deck (Komodo). Gated by ConditionFirstPet.
ActionShuffleApples EffectAction = "shuffleApples"
// ActionReactivateBuys (battle prep) re-fires the Buy ability of every pet
// in the owner's hand (Catfish).
diff --git a/internal/game/game.go b/internal/game/game.go
index a23ad82..05f4d9b 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -159,6 +159,15 @@ func (g *Game) battleDraw(n int) int {
// rollRockDie rolls one rock die: 0, 1, or 2 with equal probability.
func (g *Game) rollRockDie() int { return g.battleDraw(3) }
+// battleShuffle does an in-place Fisher-Yates over a battle deck, routing its
+// randomness through battleDraw like every other in-battle draw (Komodo).
+func (g *Game) battleShuffle(s []Card) {
+ for i := len(s) - 1; i > 0; i-- {
+ j := g.battleDraw(i + 1)
+ s[i], s[j] = s[j], s[i]
+ }
+}
+
var (
ErrNotYourTurn = errors.New("not your turn")
ErrWrongPhase = errors.New("action not allowed in this phase")
diff --git a/internal/game/view.go b/internal/game/view.go
index 06e3a83..6b1c111 100644
--- a/internal/game/view.go
+++ b/internal/game/view.go
@@ -18,9 +18,12 @@ type PlayerView struct {
Avocados int `json:"avocados,omitempty"`
// FirstBuyFree / BuysThisRound (Golden pack) are self-only: they'd reveal
// hidden deck facts (a Manta Ray) to opponents.
- FirstBuyFree bool `json:"firstBuyFree,omitempty"`
- BuysThisRound int `json:"buysThisRound,omitempty"`
- Deck []Card `json:"deck,omitempty"` // self only
+ FirstBuyFree bool `json:"firstBuyFree,omitempty"`
+ BuysThisRound int `json:"buysThisRound,omitempty"`
+ // Trumpets is the count banked for the next battle (Bird of Paradise).
+ // Self-only — shown under the player's own deck.
+ Trumpets int `json:"trumpets,omitempty"`
+ Deck []Card `json:"deck,omitempty"` // self only
}
// View is the full game state as seen by one player.
@@ -105,6 +108,7 @@ func (g *Game) ViewFor(playerID string) View {
pv.Deck = p.Deck
pv.FirstBuyFree = p.FirstBuyFree
pv.BuysThisRound = p.BuysThisRound
+ pv.Trumpets = p.PendingTrumpets
}
v.Players = append(v.Players, pv)
}
diff --git a/web/src/components/BattlePhase.tsx b/web/src/components/BattlePhase.tsx
index 4a2f0f7..f2a6ce0 100644
--- a/web/src/components/BattlePhase.tsx
+++ b/web/src/components/BattlePhase.tsx
@@ -541,7 +541,7 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
{peek.seat === youSeat ? 'Your' : 'Opponent’s'} deck · {lineup.length} card
- {lineup.length !== 1 ? 's' : ''} (top first)
+ {lineup.length !== 1 ? 's' : ''} (first on the right)
{lineup.map((c, i) => (
diff --git a/web/src/components/CardView.tsx b/web/src/components/CardView.tsx
index bc40b63..395fa26 100644
--- a/web/src/components/CardView.tsx
+++ b/web/src/components/CardView.tsx
@@ -206,19 +206,19 @@ export function CardView({
onMouseLeave={preview ? undefined : () => setHover(null)}
>
{previewEl}
- {/* Power sits in a spiky burst badge in the top-left corner (pets only),
- with any battle damage marker beside it. It's absolutely placed so it
- doesn't eat into the room the name and ability text need. */}
+
+ {card.tier ? `T${card.tier}` : '·'}
+ {card.suit && }
+
+ {/* Power sits in a spiky burst badge on its own centered row (pets only),
+ with any battle damage marker beside it, so it never overlaps the art,
+ name, or ability text. */}
{card.kind === 'pet' && (
0 ? 'is-buffed' : ''}`}>{power}
{damage > 0 && !dead && −{damage}}
)}
-
- {card.tier ? `T${card.tier}` : '·'}
- {card.suit && }
-
{artFor(card.name)}
diff --git a/web/src/components/ShopPhase.tsx b/web/src/components/ShopPhase.tsx
index a7bbe92..d0ca718 100644
--- a/web/src/components/ShopPhase.tsx
+++ b/web/src/components/ShopPhase.tsx
@@ -73,6 +73,7 @@ export function ShopPhase({ view, you, send }: Props) {
const [useAvocado, setUseAvocado] = useState(false)
const myTurn = view.turn === view.youSeat && !you.ready
const avocados = you.avocados ?? 0
+ const trumpets = you.trumpets ?? 0
const freeBuy = !!you.firstBuyFree // Manta Ray: next buy costs no gold
const canBuy = myTurn && (you.coins > 0 || avocados > 0 || freeBuy)
// Nudge the player to pass once there's nothing left to buy.
@@ -376,6 +377,11 @@ export function ShopPhase({ view, you, send }: Props) {
))}
)}
+ {trumpets > 0 && (
+
+ 🎺 ×{trumpets}
+
+ )}
{/* Actions */}
diff --git a/web/src/components/Table.tsx b/web/src/components/Table.tsx
index 8ccb690..e4b72fc 100644
--- a/web/src/components/Table.tsx
+++ b/web/src/components/Table.tsx
@@ -224,7 +224,7 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
>
{oppName}’s deck last round · {lineup.length} card
- {lineup.length !== 1 ? 's' : ''} (top first)
+ {lineup.length !== 1 ? 's' : ''} (first on the right)
{lineup.map((c, i) => (
diff --git a/web/src/styles.css b/web/src/styles.css
index cda9598..d815e83 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -983,9 +983,8 @@ h3 {
.card-top {
width: 100%;
display: flex;
- /* The power badge now floats over the top-left corner, so tier + suit sit
- together at the top-right. */
- justify-content: flex-end;
+ /* Tier sits at the top-left, suit dot at the top-right. */
+ justify-content: space-between;
align-items: center;
gap: 5px;
font-size: 0.72rem;
@@ -993,16 +992,15 @@ h3 {
min-height: 14px;
}
-/* Combat stats (power badge + battle damage marker) pinned to the top-left
- corner. Absolute so they don't push the name and ability text down. */
+/* Combat stats (power badge + battle damage marker) get their own centered
+ row just under the tier/suit line, so they never overlap the art or text. */
.card-combat {
- position: absolute;
- top: 5px;
- left: 5px;
z-index: 2;
display: flex;
align-items: center;
+ justify-content: center;
gap: 4px;
+ margin: 2px 0 1px;
}
.card-tier {
@@ -1290,6 +1288,19 @@ h3 {
inset 0 0 0 1px rgba(246, 201, 78, 0.06);
}
+/* Trumpets banked for the next battle, shown as a pill under the deck. */
+.trumpet-indicator {
+ margin: 12px auto 0;
+ width: fit-content;
+ font-family: var(--font-display);
+ font-size: 1.05rem;
+ padding: 5px 14px;
+ border-radius: 999px;
+ border: 1px solid rgba(246, 201, 78, 0.3);
+ background: rgba(0, 0, 0, 0.28);
+ color: var(--gold);
+}
+
/* Coins shown as big golden discs above the buy row. */
.shop-coins {
display: flex;
@@ -2592,6 +2603,14 @@ h3 {
flex-wrap: wrap;
gap: 6px;
max-width: min(60vw, 560px);
+ /* Read right-to-left like the arrangement screen: the first (top) card sits
+ on the right and later pets flow left. */
+ direction: rtl;
+}
+
+/* Keep each card's own contents left-to-right despite the rtl row. */
+.deck-peek-cards > * {
+ direction: ltr;
}
/* Backdrop behind the toolbar deck-peek: any click outside dismisses it. Sits
diff --git a/web/src/types.ts b/web/src/types.ts
index ec61843..4b87591 100644
--- a/web/src/types.ts
+++ b/web/src/types.ts
@@ -32,6 +32,7 @@ export interface PlayerView {
avocados?: number // set-aside Avocado tokens (Golden pack)
firstBuyFree?: boolean // Manta Ray: next buy is free (self only)
buysThisRound?: number // Blue-Ringed Octopus counter (self only)
+ trumpets?: number // banked Trumpets for next battle (Bird of Paradise, self only)
deck?: Card[]
}