Fix a bunch of bugs.
This commit is contained in:
@@ -206,6 +206,15 @@ 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.kind === 'pet' && (
|
||||
<div className="card-combat">
|
||||
<span className={`card-power ${bonus > 0 ? 'is-buffed' : ''}`}>{power}</span>
|
||||
{damage > 0 && !dead && <span className="card-damage">−{damage}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className="card-top">
|
||||
<span className="card-tier">{card.tier ? `T${card.tier}` : '·'}</span>
|
||||
{card.suit && <span className={`suit-dot suit-${card.suit}`} title={`${card.suit} suit`} />}
|
||||
@@ -214,27 +223,13 @@ export function CardView({
|
||||
{artFor(card.name)}
|
||||
</div>
|
||||
<div className="card-name">{card.name}</div>
|
||||
{card.kind === 'pet' ? (
|
||||
<>
|
||||
{card.effectText && (
|
||||
<div className="card-effect" ref={effectRef}>
|
||||
{renderEffect(card.effectText)}
|
||||
</div>
|
||||
)}
|
||||
<div className="card-bottom">
|
||||
<span className={`card-power ${bonus > 0 ? 'is-buffed' : ''}`}>{power}</span>
|
||||
{damage > 0 && !dead && <span className="card-damage">−{damage}</span>}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="card-effect" ref={effectRef}>
|
||||
{card.effectText
|
||||
? renderEffect(card.effectText)
|
||||
: card.food === 'apple'
|
||||
? '+1 power (this battle)'
|
||||
: ''}
|
||||
</div>
|
||||
)}
|
||||
<div className="card-effect" ref={effectRef}>
|
||||
{card.effectText
|
||||
? renderEffect(card.effectText)
|
||||
: card.kind === 'food' && card.food === 'apple'
|
||||
? '+1 power (this battle)'
|
||||
: ''}
|
||||
</div>
|
||||
{selected && <div className="card-check">✓</div>}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -93,6 +93,17 @@ export function ShopPhase({ view, you, send }: Props) {
|
||||
wasMyTurn.current = myTurn
|
||||
}, [myTurn, view.pending, view.pendingReveal])
|
||||
const overPets = you.petCount > view.maxPets
|
||||
|
||||
// Show every coin you started the round with, fading the spent ones rather
|
||||
// than dropping them. Coins only fall within a round, so the highest count
|
||||
// seen this round is the starting purse. Reset when the round changes.
|
||||
const purse = useRef({ round: view.round, max: you.coins })
|
||||
if (purse.current.round !== view.round) {
|
||||
purse.current = { round: view.round, max: you.coins }
|
||||
}
|
||||
purse.current.max = Math.max(purse.current.max, you.coins)
|
||||
const totalCoins = purse.current.max
|
||||
|
||||
const deck = you.deck ?? []
|
||||
const opponent = view.players.find((p) => p.seat !== view.youSeat)
|
||||
const pending = view.pending
|
||||
@@ -235,16 +246,24 @@ export function ShopPhase({ view, you, send }: Props) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{turnBanner && <div className="turn-banner">Your turn!</div>}
|
||||
{turnBanner && (
|
||||
<div className="turn-banner">
|
||||
<span className="turn-banner-pill">Your turn!</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Coins as big golden discs above the buy row. */}
|
||||
<div className="shop-coins" aria-label={`${you.coins} gold`}>
|
||||
{Array.from({ length: Math.max(you.coins, 0) }, (_, i) => (
|
||||
<span key={i} className="coin-disc">
|
||||
🪙
|
||||
</span>
|
||||
))}
|
||||
{you.coins === 0 && <span className="coin-empty muted">out of gold</span>}
|
||||
{/* Coins as big golden discs above the buy row. Spent coins stay put but
|
||||
grey out, so you can see what you started the round with. */}
|
||||
<div className="shop-coins" aria-label={`${you.coins} of ${totalCoins} gold`}>
|
||||
{totalCoins > 0 ? (
|
||||
Array.from({ length: totalCoins }, (_, i) => (
|
||||
<span key={i} className={`coin-disc ${i >= you.coins ? 'is-spent' : ''}`}>
|
||||
🪙
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="coin-empty muted">out of gold</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Shop row */}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useGame } from '../useGame'
|
||||
import { useCatalog } from '../useCatalog'
|
||||
import { CardView } from './CardView'
|
||||
import type { Card, Session } from '../types'
|
||||
import { Lobby } from './Lobby'
|
||||
import { ShopPhase } from './ShopPhase'
|
||||
@@ -33,6 +35,10 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
||||
return { round: battleRound, step: next }
|
||||
})
|
||||
|
||||
// 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)
|
||||
|
||||
// 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
|
||||
// battle and appended as the final battle line only once the replay reaches
|
||||
@@ -145,6 +151,21 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
||||
{view.phase === 'shop' && p.seat !== view.youSeat && (
|
||||
<span className="chip">🪙 {p.coins}</span>
|
||||
)}
|
||||
{/* Peek at the opponent's deck from last round's battle. */}
|
||||
{view.phase === 'shop' &&
|
||||
p.seat !== view.youSeat &&
|
||||
(view.battle?.lineups?.[p.seat]?.length ?? 0) > 0 && (
|
||||
<button
|
||||
className={`chip chip-btn ${deckPeek?.seat === p.seat ? 'is-active' : ''}`}
|
||||
title="See their deck from last round's battle"
|
||||
onClick={(e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
setDeckPeek((cur) => (cur?.seat === p.seat ? null : { seat: p.seat, rect }))
|
||||
}}
|
||||
>
|
||||
👁 deck
|
||||
</button>
|
||||
)}
|
||||
{(p.avocados ?? 0) > 0 && (
|
||||
<span className="chip" title="Set-aside Avocados">🥑 {p.avocados}</span>
|
||||
)}
|
||||
@@ -186,6 +207,35 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
||||
{view.debug && (
|
||||
<DebugPanel canGrant={view.phase === 'shop'} pack={view.pack} send={send} />
|
||||
)}
|
||||
|
||||
{deckPeek &&
|
||||
view.phase === 'shop' &&
|
||||
(() => {
|
||||
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(
|
||||
<>
|
||||
<div className="deck-peek-backdrop" onClick={() => setDeckPeek(null)} />
|
||||
<div
|
||||
className="deck-peek deck-peek-toolbar"
|
||||
style={{ top: deckPeek.rect.bottom + 8, left }}
|
||||
>
|
||||
<div className="deck-peek-label">
|
||||
{oppName}’s deck last round · {lineup.length} card
|
||||
{lineup.length !== 1 ? 's' : ''} (top first)
|
||||
</div>
|
||||
<div className="deck-peek-cards">
|
||||
{lineup.map((c, i) => (
|
||||
<CardView key={c.id || i} card={c} size="sm" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>,
|
||||
document.body,
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user