Fix shop action costs and pass semantics.
Only buying costs gold; selling and trading (Triple) are free. Pass is now a final action, legal only at or under the pet limit: it forfeits remaining gold and ends that player's shopping for the round, with the shop closing once everyone has passed. That makes the separate cleanup phase unreachable (you sell down in-shop before passing), so it is removed. The client asks for confirmation before passing, and the bot knows buys are the only coin sink, when passing is legal, and that it must sell down before it can pass.
This commit is contained in:
@@ -4,7 +4,7 @@ import { fetchCatalog } from '../api'
|
||||
import { CardView } from './CardView'
|
||||
|
||||
interface Props {
|
||||
canGrant: boolean // shop/cleanup phase — grants only land then
|
||||
canGrant: boolean // shop phase — grants only land then
|
||||
send: (msg: ClientMessage) => void
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,10 @@ interface Props {
|
||||
|
||||
export function ShopPhase({ view, you, send }: Props) {
|
||||
const [selected, setSelected] = useState<string[]>([])
|
||||
const cleanup = view.phase === 'cleanup'
|
||||
const myTurn = !cleanup && view.turn === view.youSeat && you.coins > 0
|
||||
const [confirmPass, setConfirmPass] = useState(false)
|
||||
const myTurn = view.turn === view.youSeat && !you.ready
|
||||
const canBuy = myTurn && you.coins > 0
|
||||
const overPets = you.petCount > view.maxPets
|
||||
const deck = you.deck ?? []
|
||||
const opponent = view.players.find((p) => p.seat !== view.youSeat)
|
||||
const pending = view.pending
|
||||
@@ -81,12 +83,6 @@ export function ShopPhase({ view, you, send }: Props) {
|
||||
const sameSuit =
|
||||
selectedCards.length === 3 &&
|
||||
selectedCards.every((c) => c.suit && c.suit === selectedCards[0].suit)
|
||||
const excessPets = you.petCount - view.maxPets
|
||||
const cleanupReady =
|
||||
cleanup &&
|
||||
excessPets > 0 &&
|
||||
selectedCards.length === excessPets &&
|
||||
selectedCards.every((c) => c.kind === 'pet')
|
||||
|
||||
function act(msg: ClientMessage) {
|
||||
// Sold cards vanish before the apple entry arrives, so remember where they
|
||||
@@ -105,24 +101,21 @@ export function ShopPhase({ view, you, send }: Props) {
|
||||
<div className="shop">
|
||||
{/* Status line */}
|
||||
<div className="shop-status">
|
||||
{cleanup ? (
|
||||
excessPets > 0 ? (
|
||||
<span className="status-hot">
|
||||
Too many pets! Sell <strong>{excessPets}</strong> — they become
|
||||
apples 🍎
|
||||
</span>
|
||||
) : (
|
||||
<span className="muted">
|
||||
Waiting for {opponent?.name ?? 'opponent'} to sell down to{' '}
|
||||
{view.maxPets} pets…
|
||||
</span>
|
||||
)
|
||||
) : pending && !myPending ? (
|
||||
{pending && !myPending ? (
|
||||
<span className="muted">
|
||||
{opponent?.name ?? 'Opponent'} is trading up a tier…
|
||||
</span>
|
||||
) : you.ready ? (
|
||||
<span className="muted">
|
||||
You passed — waiting for {opponent?.name ?? 'opponent'} to finish
|
||||
shopping…
|
||||
</span>
|
||||
) : myTurn && overPets ? (
|
||||
<span className="status-hot">
|
||||
Too many pets! Sell down to {view.maxPets} before you can pass 🍎
|
||||
</span>
|
||||
) : myTurn ? (
|
||||
<span className="status-hot">Your turn — spend a coin 🪙</span>
|
||||
<span className="status-hot">Your turn — buy, sell, trade, or pass</span>
|
||||
) : (
|
||||
<span className="muted">
|
||||
{view.players[view.turn]?.name ?? 'Opponent'}’s turn…
|
||||
@@ -131,30 +124,34 @@ export function ShopPhase({ view, you, send }: Props) {
|
||||
</div>
|
||||
|
||||
{/* Shop row */}
|
||||
{!cleanup && (
|
||||
<section className="shop-row-wrap">
|
||||
<div className="section-label">
|
||||
Shop · Tier {view.round}
|
||||
<span className="muted"> · {view.deckCounts[view.round - 1]} left in deck</span>
|
||||
<section className="shop-row-wrap">
|
||||
<div className="section-label">
|
||||
Shop · Tier {view.round}
|
||||
<span className="muted"> · {view.deckCounts[view.round - 1]} left in deck</span>
|
||||
</div>
|
||||
<div className="shop-row">
|
||||
{view.shopRow.map((c, i) =>
|
||||
c.id ? (
|
||||
<CardView
|
||||
key={c.id}
|
||||
card={c}
|
||||
size="lg"
|
||||
disabled={!canBuy}
|
||||
onClick={canBuy ? () => act({ type: 'buy', row: i }) : undefined}
|
||||
/>
|
||||
) : (
|
||||
<div key={`empty-${i}`} className="card-slot-empty" />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
{myTurn && (
|
||||
<div className="hint">
|
||||
{canBuy
|
||||
? 'Tap a card to buy it for 1 🪙 — selling and trading are free'
|
||||
: 'No coins left — you can still sell, trade, or pass'}
|
||||
</div>
|
||||
<div className="shop-row">
|
||||
{view.shopRow.map((c, i) =>
|
||||
c.id ? (
|
||||
<CardView
|
||||
key={c.id}
|
||||
card={c}
|
||||
size="lg"
|
||||
disabled={!myTurn}
|
||||
onClick={myTurn ? () => act({ type: 'buy', row: i }) : undefined}
|
||||
/>
|
||||
) : (
|
||||
<div key={`empty-${i}`} className="card-slot-empty" />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
{myTurn && <div className="hint">Tap a card to buy it for 1 🪙</div>}
|
||||
</section>
|
||||
)}
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Your deck */}
|
||||
<section className="deck-wrap">
|
||||
@@ -183,52 +180,75 @@ export function ShopPhase({ view, you, send }: Props) {
|
||||
|
||||
{/* Actions */}
|
||||
<div className="actions">
|
||||
{cleanup ? (
|
||||
excessPets > 0 && (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!cleanupReady}
|
||||
onClick={() => act({ type: 'sell', cards: selected })}
|
||||
>
|
||||
Sell {excessPets} pet{excessPets > 1 ? 's' : ''} → 🍎
|
||||
</button>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
disabled={!myTurn || selected.length === 0}
|
||||
onClick={() => act({ type: 'sell', cards: selected })}
|
||||
title="Convert selected cards into apples (+1 power each, this battle only)"
|
||||
>
|
||||
Sell {selected.length > 0 ? selected.length : ''} → 🍎 (1 🪙)
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Trade 3{' '}
|
||||
{sameSuit && selectedCards[0].suit ? (
|
||||
<span className={`suit-dot suit-${selectedCards[0].suit}`} />
|
||||
) : (
|
||||
'matching'
|
||||
)}{' '}
|
||||
↑ Tier {Math.min(view.round + 1, view.maxRounds)} (1 🪙)
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
disabled={!myTurn}
|
||||
onClick={() => act({ type: 'pass' })}
|
||||
title="Give up your remaining coins"
|
||||
>
|
||||
Pass
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
disabled={!myTurn || selected.length === 0}
|
||||
onClick={() => act({ type: 'sell', cards: selected })}
|
||||
title="Convert selected cards into apples (+1 power each, this battle only) — free"
|
||||
>
|
||||
Sell {selected.length > 0 ? selected.length : ''} → 🍎
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Trade 3{' '}
|
||||
{sameSuit && selectedCards[0].suit ? (
|
||||
<span className={`suit-dot suit-${selectedCards[0].suit}`} />
|
||||
) : (
|
||||
'matching'
|
||||
)}{' '}
|
||||
↑ Tier {Math.min(view.round + 1, view.maxRounds)}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
disabled={!myTurn || overPets}
|
||||
onClick={() => setConfirmPass(true)}
|
||||
title={
|
||||
overPets
|
||||
? `Sell down to ${view.maxPets} pets before passing`
|
||||
: 'End your shopping for this round'
|
||||
}
|
||||
>
|
||||
Pass
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Pass confirmation */}
|
||||
{confirmPass && (
|
||||
<div className="modal-backdrop" onClick={() => setConfirmPass(false)}>
|
||||
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
||||
<h3>Done shopping?</h3>
|
||||
<p className="muted">
|
||||
Passing ends your shopping for the rest of this round
|
||||
{you.coins > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
and gives up your remaining {you.coins} 🪙
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</p>
|
||||
<div className="actions">
|
||||
<button className="btn btn-ghost" onClick={() => setConfirmPass(false)}>
|
||||
Keep shopping
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
setConfirmPass(false)
|
||||
act({ type: 'pass' })
|
||||
}}
|
||||
>
|
||||
Pass ✋
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Trade picker */}
|
||||
{myPending && pending && (
|
||||
<div className="modal-backdrop">
|
||||
|
||||
@@ -102,7 +102,7 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
||||
)}
|
||||
<span className="topbar-name">{p.name}</span>
|
||||
<span className="chip">🏆 {p.trophies}</span>
|
||||
{(view.phase === 'shop' || view.phase === 'cleanup') && (
|
||||
{view.phase === 'shop' && (
|
||||
<span className="chip">🪙 {p.coins}</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -119,9 +119,7 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
||||
<div className="table-body">
|
||||
<main className="table-main">
|
||||
{view.phase === 'lobby' && <Lobby view={view} />}
|
||||
{(view.phase === 'shop' || view.phase === 'cleanup') && (
|
||||
<ShopPhase view={view} you={you} send={send} />
|
||||
)}
|
||||
{view.phase === 'shop' && <ShopPhase view={view} you={you} send={send} />}
|
||||
{view.phase === 'arrange' && <ArrangePhase view={view} you={you} send={send} />}
|
||||
{view.phase === 'battle' && (
|
||||
<BattlePhase view={view} send={send} step={step} setStep={setStep} />
|
||||
@@ -138,10 +136,7 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
||||
)}
|
||||
{error && <div className="toast">{error}</div>}
|
||||
{view.debug && (
|
||||
<DebugPanel
|
||||
canGrant={view.phase === 'shop' || view.phase === 'cleanup'}
|
||||
send={send}
|
||||
/>
|
||||
<DebugPanel canGrant={view.phase === 'shop'} send={send} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user