From add2218978ebde26814e2e161bb7b4e0386dbcee Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 24 Jul 2026 11:23:38 -0400 Subject: [PATCH] More random UI fixes. --- internal/game/game.go | 6 +-- web/src/components/BattlePhase.tsx | 12 ++++-- web/src/components/CardView.tsx | 2 +- web/src/components/ShopPhase.tsx | 12 +++--- web/src/components/Table.tsx | 30 ++++++------- web/src/styles.css | 68 +++++++++++++++++++++++++----- 6 files changed, 92 insertions(+), 38 deletions(-) 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 @@ -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; }