Move the UI to be vertical.
This commit is contained in:
@@ -253,7 +253,7 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
// (and thus overflow-y) to auto — so an absolutely-positioned popover gets
|
||||
// clipped to the battlefield. We anchor it to the hovered stack's viewport
|
||||
// rect and portal it to <body> so it floats over the whole window instead.
|
||||
const [peek, setPeek] = useState<{ seat: number; dir: 'left' | 'right'; rect: DOMRect } | null>(
|
||||
const [peek, setPeek] = useState<{ seat: number; pos: 'top' | 'bottom'; rect: DOMRect } | null>(
|
||||
null,
|
||||
)
|
||||
const lineups = battle.lineups
|
||||
@@ -319,7 +319,11 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
? lastEvent.dice
|
||||
: null
|
||||
|
||||
function renderSide(seat: number, dir: 'left' | 'right') {
|
||||
// pos is the half this seat occupies: 'top' = opponent, 'bottom' = you. The
|
||||
// two pets meet at a horizontal clash line. Each half is a row —
|
||||
// [set-aside | pet | apples] — with the deck under the pet (bottom) or over
|
||||
// it (top). Set-aside stays on the left and apples on the right for both.
|
||||
function renderSide(seat: number, pos: 'top' | 'bottom') {
|
||||
const s = sides[seat]
|
||||
const clashing = !done && lastEvent?.type === 'clash' && s.unit && !s.unit.dying
|
||||
const clashDying = lastEvent?.type === 'clash' && s.unit?.dying
|
||||
@@ -336,7 +340,7 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
(lastEvent.type === 'reveal' && lastEvent.card?.kind === 'food'))
|
||||
? lastEvent.card?.id
|
||||
: null
|
||||
// A pet just set aside rises up from the arena into the set-aside row.
|
||||
// A pet just set aside slides into the set-aside row beside the arena.
|
||||
const newSetAsideId =
|
||||
!done && lastEvent?.type === 'setaside' && lastEvent.seat === seat
|
||||
? lastEvent.card?.id
|
||||
@@ -346,47 +350,49 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
|
||||
const lineup = lineups?.[seat] ?? []
|
||||
const stackEl = (
|
||||
<div
|
||||
className={`stackpile ${lineup.length ? 'peekable' : ''}`}
|
||||
onMouseEnter={
|
||||
lineup.length
|
||||
? (e) => setPeek({ seat, dir, rect: e.currentTarget.getBoundingClientRect() })
|
||||
: undefined
|
||||
}
|
||||
onMouseLeave={() => setPeek((p) => (p?.seat === seat ? null : p))}
|
||||
title={lineup.length ? 'Hover to see the whole deck' : undefined}
|
||||
>
|
||||
{s.stack > 0 ? (
|
||||
<div className="card-back">
|
||||
<span className="card-back-count">{s.stack}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card-slot-empty stack-empty" />
|
||||
)}
|
||||
{summoning && lastEvent?.card && (
|
||||
// Fly the spawned apple/bee out from where the pet stood (the unit
|
||||
// zone sits on the inner edge) onto the top of the deck, so it reads
|
||||
// as coming from the pet that just fainted.
|
||||
<div className={`summon-pop ${dir === 'left' ? 'summon-from-right' : 'summon-from-left'}`}>
|
||||
<CardView card={lastEvent.card} size="sm" />
|
||||
</div>
|
||||
)}
|
||||
{milling && lastEvent?.card && (
|
||||
<div className="summon-pop mill-pop">
|
||||
<CardView card={lastEvent.card} size="sm" dead />
|
||||
</div>
|
||||
)}
|
||||
{rockDice && lastEvent?.seat === seat && (
|
||||
// The dice roll sits directly under the throwing side's deck.
|
||||
<div className="stack-dice">
|
||||
<DiceRoll key={step} dice={rockDice} side={dir} />
|
||||
</div>
|
||||
)}
|
||||
<div className={`battle-deck battle-deck-${pos}`}>
|
||||
<div
|
||||
className={`stackpile ${lineup.length ? 'peekable' : ''}`}
|
||||
onMouseEnter={
|
||||
lineup.length
|
||||
? (e) => setPeek({ seat, pos, rect: e.currentTarget.getBoundingClientRect() })
|
||||
: undefined
|
||||
}
|
||||
onMouseLeave={() => setPeek((p) => (p?.seat === seat ? null : p))}
|
||||
title={lineup.length ? 'Hover to see the whole deck' : undefined}
|
||||
>
|
||||
{s.stack > 0 ? (
|
||||
<div className="card-back">
|
||||
<span className="card-back-count">{s.stack}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card-slot-empty stack-empty" />
|
||||
)}
|
||||
{summoning && lastEvent?.card && (
|
||||
// Fly the spawned apple/bee out from where the pet stood (toward the
|
||||
// clash line) onto the deck, so it reads as coming from the pet that
|
||||
// just fainted.
|
||||
<div className={`summon-pop summon-to-${pos}`}>
|
||||
<CardView card={lastEvent.card} size="sm" />
|
||||
</div>
|
||||
)}
|
||||
{milling && lastEvent?.card && (
|
||||
<div className="summon-pop mill-pop">
|
||||
<CardView card={lastEvent.card} size="sm" dead />
|
||||
</div>
|
||||
)}
|
||||
{rockDice && lastEvent?.seat === seat && (
|
||||
// The dice roll sits beside the throwing side's deck.
|
||||
<div className="stack-dice">
|
||||
<DiceRoll key={step} dice={rockDice} side={pos} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Set-aside pets (Blowfish, Badger, …) sit in a row above the active pet
|
||||
// until their pending effect resolves.
|
||||
// Set-aside pets (Blowfish, Badger, …) sit in a row to the left of the
|
||||
// active pet until their pending effect resolves.
|
||||
const setAsideEl = s.setAside.length > 0 && (
|
||||
<div className="setaside-row">
|
||||
{s.setAside.map((c) => (
|
||||
@@ -403,8 +409,8 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
)
|
||||
|
||||
// Foods attached to the pet (or, before one is in play, waiting for the
|
||||
// next) fan out below it: a vertical stack overlapping ~¾, so only each
|
||||
// card's top edge shows — like cards laid out on a table.
|
||||
// next) fan out to the right of it: a horizontal row overlapping ~¾, so
|
||||
// only each card's left edge shows — like cards laid out on a table.
|
||||
const foods = s.unit ? s.unit.foods : s.pending
|
||||
const foodFanEl = foods.length > 0 && (
|
||||
<div className="food-fan">
|
||||
@@ -412,7 +418,7 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
<div
|
||||
key={f.id}
|
||||
className={`food-fan-card ${s.leaving.includes(f.id) ? 'is-leaving' : ''} ${
|
||||
f.id === newFoodId ? `food-in food-in-${dir}` : ''
|
||||
f.id === newFoodId ? `food-in food-in-${pos}` : ''
|
||||
}`}
|
||||
style={{ zIndex: i + 1 }}
|
||||
>
|
||||
@@ -431,9 +437,9 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
key={`${s.unit.card.id}-${clashing || rockVictim ? step : 'idle'}`}
|
||||
className={[
|
||||
'battle-unit',
|
||||
clashing || clashDying ? `clash-${dir}` : '',
|
||||
clashing || clashDying ? `clash-${pos}` : '',
|
||||
s.unit.dying ? 'unit-dying' : '',
|
||||
revealing ? `unit-reveal unit-reveal-${dir}` : '',
|
||||
revealing ? `unit-reveal unit-reveal-${pos}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
@@ -454,15 +460,19 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
</div>
|
||||
)
|
||||
|
||||
return dir === 'left' ? (
|
||||
<div className="battle-side">
|
||||
{stackEl}
|
||||
{unitEl}
|
||||
</div>
|
||||
) : (
|
||||
<div className="battle-side">
|
||||
{unitEl}
|
||||
{stackEl}
|
||||
return (
|
||||
<div className={`battle-side battle-side-${pos}`}>
|
||||
{pos === 'top' ? (
|
||||
<>
|
||||
{stackEl}
|
||||
{unitEl}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{unitEl}
|
||||
{stackEl}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -520,35 +530,37 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="battlefield">
|
||||
{renderSide(youSeat, 'left')}
|
||||
{renderSide(oppSeat, 'top')}
|
||||
<div className="battle-center" aria-hidden>
|
||||
<span className="battle-center-bolt">⚡</span>
|
||||
</div>
|
||||
{renderSide(oppSeat, 'right')}
|
||||
{renderSide(youSeat, 'bottom')}
|
||||
</div>
|
||||
|
||||
{peek &&
|
||||
(() => {
|
||||
const lineup = lineups?.[peek.seat] ?? []
|
||||
if (!lineup.length) return null
|
||||
// Your deck sits at the bottom of the board, so float the peek above
|
||||
// it; the rival's sits at the top, so float it below.
|
||||
const centerX = peek.rect.left + peek.rect.width / 2
|
||||
const style: React.CSSProperties = {
|
||||
bottom: window.innerHeight - peek.rect.top + 8,
|
||||
...(peek.dir === 'left'
|
||||
? { left: peek.rect.left }
|
||||
: { right: window.innerWidth - peek.rect.right }),
|
||||
left: centerX,
|
||||
transform: 'translateX(-50%)',
|
||||
...(peek.pos === 'bottom'
|
||||
? { bottom: window.innerHeight - peek.rect.top + 8 }
|
||||
: { top: peek.rect.bottom + 8 }),
|
||||
}
|
||||
// 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'
|
||||
// Each player's first pet is the one nearest the clash line; show the
|
||||
// lineup first-to-last, left to right, for both.
|
||||
return createPortal(
|
||||
<div className={`deck-peek deck-peek-${peek.dir}`} style={style}>
|
||||
<div className={`deck-peek deck-peek-${peek.pos}`} style={style}>
|
||||
<div className="deck-peek-label">
|
||||
{mine ? 'Your' : 'Opponent’s'} deck · {lineup.length} card
|
||||
{lineup.length !== 1 ? 's' : ''} (first on the {firstSide})
|
||||
{lineup.length !== 1 ? 's' : ''} (first on the left)
|
||||
</div>
|
||||
<div className={`deck-peek-cards first-${firstSide}`}>
|
||||
<div className="deck-peek-cards first-left">
|
||||
<div className="deck-peek-first">⚔️ first</div>
|
||||
{lineup.map((c, i) => (
|
||||
<CardView key={c.id || i} card={c} size="sm" />
|
||||
|
||||
Reference in New Issue
Block a user