More random UI fixes.

This commit is contained in:
Greyson Parrelli
2026-07-24 11:23:38 -04:00
parent 6558806ba0
commit add2218978
6 changed files with 92 additions and 38 deletions
+3 -3
View File
@@ -723,7 +723,7 @@ func (g *Game) TradeStart(playerID string, cardIDs []string) error {
ids[i] = c.ID ids[i] = c.ID
} }
g.addLog(LogEntry{Seat: p.Seat, Icon: "🔄", Kind: LogTrade, Cards: ids, 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)}) p.Name, strings.Join(names, ", "), suit, nextTier)})
g.Pending = &PendingTrade{ g.Pending = &PendingTrade{
PlayerID: playerID, 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). // we have to reveal that pet (its effect log names it anyway).
if hasBuyEffect(chosen) { if hasBuyEffect(chosen) {
g.addLog(LogEntry{Seat: p.Seat, Icon: "🔄", Kind: LogTradePick, CardName: chosen.Name, 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)}) p.Name, article(chosen.Name), chosen.Name)})
} else { } else {
g.addLog(LogEntry{Seat: p.Seat, Icon: "🔄", Kind: LogTradePick, 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. // Pets obtained via the Triple action trigger their Buy effects.
g.applyShopTrigger(p, chosen, TriggerBuy) g.applyShopTrigger(p, chosen, TriggerBuy)
+9 -3
View File
@@ -537,13 +537,19 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
? { left: peek.rect.left } ? { left: peek.rect.left }
: { right: window.innerWidth - peek.rect.right }), : { 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( return createPortal(
<div className={`deck-peek deck-peek-${peek.dir}`} style={style}> <div className={`deck-peek deck-peek-${peek.dir}`} style={style}>
<div className="deck-peek-label"> <div className="deck-peek-label">
{peek.seat === youSeat ? 'Your' : 'Opponents'} deck · {lineup.length} card {mine ? 'Your' : 'Opponents'} deck · {lineup.length} card
{lineup.length !== 1 ? 's' : ''} (first on the right) {lineup.length !== 1 ? 's' : ''} (first on the {firstSide})
</div> </div>
<div className="deck-peek-cards"> <div className={`deck-peek-cards first-${firstSide}`}>
<div className="deck-peek-first"> first</div>
{lineup.map((c, i) => ( {lineup.map((c, i) => (
<CardView key={c.id || i} card={c} size="sm" /> <CardView key={c.id || i} card={c} size="sm" />
))} ))}
+1 -1
View File
@@ -62,7 +62,7 @@ const TRIGGER_GLOSS: Record<string, string> = {
Hurt: 'when it survives taking damage', Hurt: 'when it survives taking damage',
Sell: 'when you sell it in the shop', Sell: 'when you sell it in the shop',
Buy: 'when you buy it', 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', 'After Attacking': 'right after it attacks in a clash',
'Battle Prep': 'as the battle is arranged', 'Battle Prep': 'as the battle is arranged',
'Shop start': 'when the shop opens each round', 'Shop start': 'when the shop opens each round',
+6 -6
View File
@@ -227,7 +227,7 @@ export function ShopPhase({ view, you, send }: Props) {
<div className="shop-status"> <div className="shop-status">
{pending && !myPending ? ( {pending && !myPending ? (
<span className="muted"> <span className="muted">
{opponent?.name ?? 'Opponent'} is trading up a tier {opponent?.name ?? 'Opponent'} is tripling up a tier
</span> </span>
) : you.ready ? ( ) : you.ready ? (
<span className="muted"> <span className="muted">
@@ -239,7 +239,7 @@ export function ShopPhase({ view, you, send }: Props) {
Too many pets! Sell down to {view.maxPets} before you can pass 🍎 Too many pets! Sell down to {view.maxPets} before you can pass 🍎
</span> </span>
) : myTurn ? ( ) : myTurn ? (
<span className="status-hot">Your turn buy, sell, trade, or pass</span> <span className="shop-status-pill">Your turn buy, sell, triple, or pass</span>
) : ( ) : (
<span className="muted"> <span className="muted">
{view.players[view.turn]?.name ?? 'Opponent'}s turn {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' ? 'Tap a card to buy it for 1 🪙 — selling and trading are free'
: avocados > 0 : avocados > 0
? 'No coins — tap a card to buy it by discarding an Avocado 🥑' ? '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'}
</div> </div>
)} )}
</section> </section>
@@ -398,9 +398,9 @@ export function ShopPhase({ view, you, send }: Props) {
className="btn btn-secondary" className="btn btn-secondary"
disabled={!myTurn || !sameSuit || view.round >= view.maxRounds} disabled={!myTurn || !sameSuit || view.round >= view.maxRounds}
onClick={() => act({ type: 'trade', cards: selected })} 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 ? ( {sameSuit && selectedCards[0].suit ? (
<span className={`suit-dot suit-${selectedCards[0].suit}`} /> <span className={`suit-dot suit-${selectedCards[0].suit}`} />
) : ( ) : (
@@ -455,7 +455,7 @@ export function ShopPhase({ view, you, send }: Props) {
</div> </div>
)} )}
{/* Trade picker */} {/* Triple picker */}
{myPending && pending && ( {myPending && pending && (
<div className="modal-backdrop"> <div className="modal-backdrop">
<div className="modal"> <div className="modal">
+15 -15
View File
@@ -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 // 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. // (its arranged lineup is already public). Shown as a centered modal.
const [deckPeek, setDeckPeek] = useState<{ seat: number; rect: DOMRect } | null>(null) const [deckPeek, setDeckPeek] = useState<{ seat: number } | null>(null)
// The battle outcome is known before the replay plays out, so we hold its // 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 // "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
<button <button
className={`chip chip-btn ${deckPeek?.seat === p.seat ? 'is-active' : ''}`} className={`chip chip-btn ${deckPeek?.seat === p.seat ? 'is-active' : ''}`}
title="See their deck from last round's battle" title="See their deck from last round's battle"
onClick={(e) => { onClick={() =>
const rect = e.currentTarget.getBoundingClientRect() setDeckPeek((cur) => (cur?.seat === p.seat ? null : { seat: p.seat }))
setDeckPeek((cur) => (cur?.seat === p.seat ? null : { seat: p.seat, rect })) }
}}
> >
👁 deck 👁 deck
</button> </button>
@@ -214,25 +213,26 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
const lineup = view.battle?.lineups?.[deckPeek.seat] ?? [] const lineup = view.battle?.lineups?.[deckPeek.seat] ?? []
if (!lineup.length) return null if (!lineup.length) return null
const oppName = view.players.find((p) => p.seat === deckPeek.seat)?.name ?? 'Opponent' 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( return createPortal(
<> <div className="modal-backdrop" onClick={() => setDeckPeek(null)}>
<div className="deck-peek-backdrop" onClick={() => setDeckPeek(null)} />
<div <div
className="deck-peek deck-peek-toolbar" className="modal deck-peek-modal"
style={{ top: deckPeek.rect.bottom + 8, left }} onClick={(e) => e.stopPropagation()}
> >
<div className="deck-peek-label"> <div className="deck-peek-label">
{oppName}s deck last round · {lineup.length} card {oppName}s deck from last rounds battle · {lineup.length} card
{lineup.length !== 1 ? 's' : ''} (first on the right) {lineup.length !== 1 ? 's' : ''} (first on the left)
</div> </div>
<div className="deck-peek-cards"> {/* 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. */}
<div className="deck-peek-cards first-left">
<div className="deck-peek-first"> first</div>
{lineup.map((c, i) => ( {lineup.map((c, i) => (
<CardView key={c.id || i} card={c} size="sm" /> <CardView key={c.id || i} card={c} size="sm" />
))} ))}
</div> </div>
</div> </div>
</>, </div>,
document.body, document.body,
) )
})()} })()}
+58 -10
View File
@@ -1246,6 +1246,24 @@ h3 {
animation: pulse 1.6s ease-in-out infinite; 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 { @keyframes pulse {
0%, 0%,
100% { 100% {
@@ -2601,22 +2619,52 @@ h3 {
.deck-peek-cards { .deck-peek-cards {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center;
gap: 6px; gap: 6px;
max-width: min(60vw, 560px); 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 > * { .deck-peek-cards > * {
direction: ltr; direction: ltr;
} }
/* Backdrop behind the toolbar deck-peek: any click outside dismisses it. Sits /* Compact echo of the arrangement phase's "⚔️ first" indicator. */
below the popover (z 60) but above the board. */ .deck-peek-first {
.deck-peek-backdrop { display: flex;
position: fixed; align-items: center;
inset: 0; align-self: stretch;
z-index: 55; 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;
} }