Various UX improvements.

This commit is contained in:
Greyson Parrelli
2026-07-23 11:55:12 -04:00
parent 816ad13f7e
commit 547cd81a99
3 changed files with 103 additions and 15 deletions
+32 -8
View File
@@ -30,6 +30,9 @@ interface SideVis {
pending: Card[] // foods revealed (or prepped) waiting for a pet
unit: UnitVis | null
setAside: Card[] // fainted pets kept beside the arena with a pending effect
// Ids of cards (set-aside pets or spent food perks) released this step: kept
// in their arrays for one beat so they can animate out, cleared next event.
leaving: string[]
}
// Milliseconds each event type stays on screen during playback.
@@ -57,10 +60,24 @@ const appleCount = (foods: Card[]) => foods.filter((f) => f.food === 'apple').le
// seat's visual state. Units that died in the last applied event are still
// present with dying=true so they can animate out.
function replay(events: BattleEvent[], stackSizes: number[], upto: number): SideVis[] {
const sides: SideVis[] = stackSizes.map((n) => ({ stack: n, pending: [], unit: null, setAside: [] }))
const sides: SideVis[] = stackSizes.map((n) => ({
stack: n,
pending: [],
unit: null,
setAside: [],
leaving: [],
}))
for (let k = 0; k < upto && k < events.length; k++) {
for (const s of sides) {
if (s.unit?.dying) s.unit = null // clear last step's casualties
if (s.leaving.length) {
// Drop cards that finished animating out last step.
const gone = new Set(s.leaving)
s.setAside = s.setAside.filter((c) => !gone.has(c.id))
if (s.unit) s.unit.foods = s.unit.foods.filter((c) => !gone.has(c.id))
s.pending = s.pending.filter((c) => !gone.has(c.id))
s.leaving = []
}
}
const ev = events[k]
switch (ev.type) {
@@ -152,12 +169,10 @@ function replay(events: BattleEvent[], stackSizes: number[], upto: number): Side
break
case 'release': {
// A spent card leaves the play area: a set-aside pet (Turtle) or a
// used-up food perk (Melon), which lives in the pet's food fan.
const s = sides[ev.seat!]
// used-up food perk (Melon), which lives in the pet's food fan. Flag it
// rather than removing it, so it animates out before unmounting.
const id = ev.card?.id
s.setAside = s.setAside.filter((c) => c.id !== id)
if (s.unit) s.unit.foods = s.unit.foods.filter((c) => c.id !== id)
s.pending = s.pending.filter((c) => c.id !== id)
if (id) sides[ev.seat!].leaving.push(id)
break
}
}
@@ -327,7 +342,12 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
const setAsideEl = s.setAside.length > 0 && (
<div className="setaside-row">
{s.setAside.map((c) => (
<CardView key={c.id} card={c} size="sm" />
<div
key={c.id}
className={`setaside-card ${s.leaving.includes(c.id) ? 'is-leaving' : ''}`}
>
<CardView card={c} size="sm" />
</div>
))}
</div>
)
@@ -339,7 +359,11 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
const foodFanEl = foods.length > 0 && (
<div className="food-fan">
{foods.map((f, i) => (
<div key={f.id} className="food-fan-card" style={{ zIndex: i + 1 }}>
<div
key={f.id}
className={`food-fan-card ${s.leaving.includes(f.id) ? 'is-leaving' : ''}`}
style={{ zIndex: i + 1 }}
>
<CardView card={f} size="sm" />
</div>
))}