diff --git a/internal/game/game.go b/internal/game/game.go
index 05f4d9b..4ad2434 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -723,7 +723,7 @@ func (g *Game) TradeStart(playerID string, cardIDs []string) error {
ids[i] = c.ID
}
g.addLog(LogEntry{Seat: p.Seat, Icon: "🔄", Kind: LogTrade, Cards: ids,
- Text: fmt.Sprintf("%s traded in %s (%s) for a tier %d pick.",
+ Text: fmt.Sprintf("%s tripled %s (%s) for a tier %d pick.",
p.Name, strings.Join(names, ", "), suit, nextTier)})
g.Pending = &PendingTrade{
PlayerID: playerID,
@@ -757,11 +757,11 @@ func (g *Game) TradeChoose(playerID string, pick int) error {
// we have to reveal that pet (its effect log names it anyway).
if hasBuyEffect(chosen) {
g.addLog(LogEntry{Seat: p.Seat, Icon: "🔄", Kind: LogTradePick, CardName: chosen.Name,
- Text: fmt.Sprintf("%s's trade pick is %s %s — its buy ability triggers.",
+ Text: fmt.Sprintf("%s's Triple pick is %s %s — its buy ability triggers.",
p.Name, article(chosen.Name), chosen.Name)})
} else {
g.addLog(LogEntry{Seat: p.Seat, Icon: "🔄", Kind: LogTradePick,
- Text: fmt.Sprintf("%s keeps their trade pick hidden.", p.Name)})
+ Text: fmt.Sprintf("%s keeps their Triple pick hidden.", p.Name)})
}
// Pets obtained via the Triple action trigger their Buy effects.
g.applyShopTrigger(p, chosen, TriggerBuy)
diff --git a/web/src/components/BattlePhase.tsx b/web/src/components/BattlePhase.tsx
index f2a6ce0..9ad572f 100644
--- a/web/src/components/BattlePhase.tsx
+++ b/web/src/components/BattlePhase.tsx
@@ -537,13 +537,19 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
? { left: peek.rect.left }
: { right: window.innerWidth - peek.rect.right }),
}
+ // Your deck sits on the left of the board and fights from its right
+ // edge; the rival's sits on the right and fights from its left edge.
+ // Order each peek to match, so "first" always points toward the clash.
+ const mine = peek.seat === youSeat
+ const firstSide = mine ? 'right' : 'left'
return createPortal(
- {peek.seat === youSeat ? 'Your' : 'Opponent’s'} deck · {lineup.length} card
- {lineup.length !== 1 ? 's' : ''} (first on the right)
+ {mine ? 'Your' : 'Opponent’s'} deck · {lineup.length} card
+ {lineup.length !== 1 ? 's' : ''} (first on the {firstSide})
-
+
+
⚔️ first
{lineup.map((c, i) => (
))}
diff --git a/web/src/components/CardView.tsx b/web/src/components/CardView.tsx
index 395fa26..7b4ec83 100644
--- a/web/src/components/CardView.tsx
+++ b/web/src/components/CardView.tsx
@@ -62,7 +62,7 @@ const TRIGGER_GLOSS: Record
= {
Hurt: 'when it survives taking damage',
Sell: 'when you sell it in the shop',
Buy: 'when you buy it',
- Triple: 'when traded in as one of three',
+ Triple: 'when tripled as one of three same-suit pets',
'After Attacking': 'right after it attacks in a clash',
'Battle Prep': 'as the battle is arranged',
'Shop start': 'when the shop opens each round',
diff --git a/web/src/components/ShopPhase.tsx b/web/src/components/ShopPhase.tsx
index d0ca718..0f8695e 100644
--- a/web/src/components/ShopPhase.tsx
+++ b/web/src/components/ShopPhase.tsx
@@ -227,7 +227,7 @@ export function ShopPhase({ view, you, send }: Props) {
{pending && !myPending ? (
- {opponent?.name ?? 'Opponent'} is trading up a tier…
+ {opponent?.name ?? 'Opponent'} is tripling up a tier…
) : you.ready ? (
@@ -239,7 +239,7 @@ export function ShopPhase({ view, you, send }: Props) {
Too many pets! Sell down to {view.maxPets} before you can pass 🍎
) : myTurn ? (
- Your turn — buy, sell, trade, or pass
+ Your turn — buy, sell, triple, or pass
) : (
{view.players[view.turn]?.name ?? 'Opponent'}’s turn…
@@ -305,7 +305,7 @@ export function ShopPhase({ view, you, send }: Props) {
? 'Tap a card to buy it for 1 🪙 — selling and trading are free'
: avocados > 0
? 'No coins — tap a card to buy it by discarding an Avocado 🥑'
- : 'No coins left — you can still sell, trade, or pass'}
+ : 'No coins left — you can still sell, triple, or pass'}
)}
@@ -398,9 +398,9 @@ export function ShopPhase({ view, you, send }: Props) {
className="btn btn-secondary"
disabled={!myTurn || !sameSuit || view.round >= view.maxRounds}
onClick={() => act({ type: 'trade', cards: selected })}
- title="Trade 3 same-suit pets for a pick from the next tier — free"
+ title="Triple 3 same-suit pets for a pick from the next tier — free"
>
- Trade 3{' '}
+ Triple 3{' '}
{sameSuit && selectedCards[0].suit ? (
) : (
@@ -455,7 +455,7 @@ export function ShopPhase({ view, you, send }: Props) {
)}
- {/* Trade picker */}
+ {/* Triple picker */}
{myPending && pending && (
diff --git a/web/src/components/Table.tsx b/web/src/components/Table.tsx
index e4b72fc..3e92a00 100644
--- a/web/src/components/Table.tsx
+++ b/web/src/components/Table.tsx
@@ -36,8 +36,8 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
})
// A shop-phase peek at an opponent's deck from the previous round's battle
- // (its arranged lineup is already public). Anchored under the clicked button.
- const [deckPeek, setDeckPeek] = useState<{ seat: number; rect: DOMRect } | null>(null)
+ // (its arranged lineup is already public). Shown as a centered modal.
+ const [deckPeek, setDeckPeek] = useState<{ seat: number } | null>(null)
// The battle outcome is known before the replay plays out, so we hold its
// "result" log entry back: it's dropped from the persistent list during the
@@ -158,10 +158,9 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
{
- const rect = e.currentTarget.getBoundingClientRect()
- setDeckPeek((cur) => (cur?.seat === p.seat ? null : { seat: p.seat, rect }))
- }}
+ onClick={() =>
+ setDeckPeek((cur) => (cur?.seat === p.seat ? null : { seat: p.seat }))
+ }
>
👁 deck
@@ -214,25 +213,26 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
const lineup = view.battle?.lineups?.[deckPeek.seat] ?? []
if (!lineup.length) return null
const oppName = view.players.find((p) => p.seat === deckPeek.seat)?.name ?? 'Opponent'
- const left = Math.max(8, Math.min(deckPeek.rect.left, window.innerWidth - 380))
return createPortal(
- <>
-
setDeckPeek(null)} />
+
setDeckPeek(null)}>
e.stopPropagation()}
>
- {oppName}’s deck last round · {lineup.length} card
- {lineup.length !== 1 ? 's' : ''} (first on the right)
+ {oppName}’s deck from last round’s battle · {lineup.length} card
+ {lineup.length !== 1 ? 's' : ''} (first on the left)
-
+ {/* The rival battles on your right, so their first pet faces you
+ on the left — order the peek to match, first card on the left. */}
+
+
⚔️ first
{lineup.map((c, i) => (
))}
- >,
+
,
document.body,
)
})()}
diff --git a/web/src/styles.css b/web/src/styles.css
index d815e83..321d427 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -1246,6 +1246,24 @@ h3 {
animation: pulse 1.6s ease-in-out infinite;
}
+/* The "it's your turn" prompt as a solid golden badge with light text, so it
+ reads as a call to action rather than bare pulsing text. */
+.shop-status-pill {
+ display: inline-block;
+ font-family: var(--font-display);
+ font-size: 0.92em;
+ color: #fff8e6;
+ background: linear-gradient(180deg, var(--gold), var(--gold-deep));
+ padding: 0.22em 0.85em;
+ border-radius: 999px;
+ border: 2px solid rgba(255, 255, 255, 0.5);
+ text-shadow: 0 1px 3px rgba(120, 78, 8, 0.5);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.5),
+ inset 0 -4px 10px rgba(150, 100, 20, 0.3),
+ 0 4px 14px rgba(0, 0, 0, 0.35);
+}
+
@keyframes pulse {
0%,
100% {
@@ -2601,22 +2619,52 @@ h3 {
.deck-peek-cards {
display: flex;
flex-wrap: wrap;
+ align-items: center;
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. */
+/* first-right: the first pet sits on the right (matches your own arrangement).
+ first-left: the first pet sits on the left (a rival, who faces you from the
+ right of the board). The "⚔️ first" marker is the first DOM child, so it
+ lands next to the first card at whichever end. */
+.deck-peek-cards.first-right {
+ direction: rtl;
+}
+.deck-peek-cards.first-left {
+ direction: ltr;
+}
+
+/* Keep each card's own contents left-to-right regardless of row direction. */
.deck-peek-cards > * {
direction: ltr;
}
-/* Backdrop behind the toolbar deck-peek: any click outside dismisses it. Sits
- below the popover (z 60) but above the board. */
-.deck-peek-backdrop {
- position: fixed;
- inset: 0;
- z-index: 55;
+/* Compact echo of the arrangement phase's "⚔️ first" indicator. */
+.deck-peek-first {
+ display: flex;
+ align-items: center;
+ align-self: stretch;
+ font-family: var(--font-display);
+ font-size: 0.68rem;
+ color: var(--gold);
+ white-space: nowrap;
+ padding: 0 2px;
+}
+
+/* Deck peek shown as a centered modal (shop-phase rival deck): cap the width so
+ a wide lineup wraps instead of running off-screen. */
+.deck-peek-modal {
+ padding: 20px;
+ gap: 12px;
+ max-width: min(92vw, 720px);
+}
+
+.deck-peek-modal .deck-peek-cards {
+ max-width: 100%;
+ justify-content: flex-start;
+}
+
+.deck-peek-modal .deck-peek-label {
+ font-size: 0.85rem;
}