Add pets for tiers 4-6.

This commit is contained in:
Greyson Parrelli
2026-07-23 00:10:20 -04:00
parent 8d43bbf61e
commit 10ac457e2e
11 changed files with 1334 additions and 163 deletions
+92 -12
View File
@@ -18,19 +18,27 @@ interface UnitVis {
interface SideVis {
stack: number
pending: Card[] // revealed foods waiting for a pet
pending: Card[] // foods revealed (or prepped) waiting for a pet
unit: UnitVis | null
}
// Milliseconds each event type stays on screen during playback.
const EVENT_MS: Record<BattleEvent['type'], number> = {
prep: 700,
reveal: 800,
summon: 1000,
mill: 700,
rock: 1200,
clash: 1400,
shield: 900,
strip: 1100,
steal: 1100,
eat: 1000,
heal: 900,
}
const appleCount = (foods: Card[]) => foods.filter((f) => f.food === 'apple').length
// replay applies the first `upto` events to fresh stacks and returns each
// seat's visual state. Units that died in the last applied event are still
// present with dying=true so they can animate out.
@@ -42,6 +50,9 @@ function replay(events: BattleEvent[], stackSizes: number[], upto: number): Side
}
const ev = events[k]
switch (ev.type) {
case 'prep':
sides[ev.seat!].pending.push(ev.card!)
break
case 'reveal': {
const s = sides[ev.seat!]
s.stack--
@@ -52,7 +63,7 @@ function replay(events: BattleEvent[], stackSizes: number[], upto: number): Side
s.unit = {
card,
foods: s.pending,
bonus: s.pending.filter((f) => f.food === 'apple').length,
bonus: ev.bonus ?? appleCount(s.pending),
damage: 0,
dying: false,
}
@@ -63,6 +74,9 @@ function replay(events: BattleEvent[], stackSizes: number[], upto: number): Side
case 'summon':
sides[ev.seat!].stack++
break
case 'mill':
sides[ev.seat!].stack--
break
case 'rock': {
const u = sides[ev.target!].unit
if (u) {
@@ -78,16 +92,81 @@ function replay(events: BattleEvent[], stackSizes: number[], upto: number): Side
if (ev.died?.[seat]) s.unit.dying = true
})
break
case 'strip': {
const u = sides[ev.target!].unit
if (u) {
u.bonus -= appleCount(u.foods)
u.foods = []
if (ev.targetDied) u.dying = true
}
break
}
case 'steal': {
const from = sides[ev.target!].unit
const to = sides[ev.seat!].unit
const n = ev.count ?? 0
if (from && to) {
let moved = 0
from.foods = from.foods.filter((f) => {
if (f.food === 'apple' && moved < n) {
moved++
to.foods.push(f)
return false
}
return true
})
from.bonus -= moved
to.bonus += moved
if (ev.targetDied) from.dying = true
}
break
}
case 'eat': {
const u = sides[ev.seat!].unit
if (u) u.bonus = ev.bonus ?? u.bonus
break
}
case 'heal': {
const u = sides[ev.seat!].unit
if (u) u.damage = ev.damageAfter ?? u.damage
break
}
case 'shield':
break // pure animation; no state change
}
}
return sides
}
// unitPop decides the floating effect text over a seat's pet for the event
// currently playing. Null = nothing.
function unitPop(ev: BattleEvent | null, seat: number, events: BattleEvent[], step: number): string | null {
if (!ev) return null
switch (ev.type) {
case 'rock':
if (ev.target !== seat) return null
return ev.roll === 0 ? 'miss!' : `${ev.roll}`
case 'clash': {
const taken = clashDamageTaken(events, step - 1, seat)
return taken > 0 ? `${taken}` : null
}
case 'shield':
return ev.seat === seat ? '🛡️' : null
case 'eat':
return ev.seat === seat ? '🍎' : null
case 'heal':
return ev.seat === seat ? '💚 +1' : null
case 'strip':
return ev.target === seat ? '💨' : null
case 'steal':
if (ev.seat === seat) return `+🍎×${ev.count}`
if (ev.target === seat) return `−🍎×${ev.count}`
return null
default:
return null
}
}
// BattlePhase plays back the battle log: cards flip off each deck, rocks
// fly, pets clash, the fallen fade out, then the round result lands.
export function BattlePhase({ view, send }: Props) {
@@ -122,9 +201,10 @@ export function BattlePhase({ view, send }: Props) {
const clashing = !done && lastEvent?.type === 'clash' && s.unit && !s.unit.dying
const clashDying = lastEvent?.type === 'clash' && s.unit?.dying
const rockVictim = lastEvent?.type === 'rock' && lastEvent.target === seat
const eating = lastEvent?.type === 'eat' && lastEvent.seat === seat
const summoning = !done && lastEvent?.type === 'summon' && lastEvent.seat === seat
const milling = !done && lastEvent?.type === 'mill' && lastEvent.seat === seat
const revealing = !done && lastEvent?.type === 'reveal' && lastEvent.seat === seat
const pop = done ? null : unitPop(lastEvent, seat, events, step)
const stackEl = (
<div className="stackpile">
@@ -140,6 +220,11 @@ export function BattlePhase({ view, send }: Props) {
<CardView card={lastEvent.card} size="sm" />
</div>
)}
{milling && lastEvent?.card && (
<div className="summon-pop mill-pop">
<CardView card={lastEvent.card} size="sm" dead />
</div>
)}
</div>
)
@@ -182,16 +267,11 @@ export function BattlePhase({ view, send }: Props) {
))}
</div>
)}
{(lastEvent?.type === 'clash' || rockVictim) && !done && (
<div className="damage-pop">
{lastEvent?.type === 'rock'
? lastEvent.roll === 0
? 'miss!'
: `${lastEvent.roll}`
: `${clashDamageTaken(events, step - 1, seat)}`}
{pop && (
<div key={`pop-${step}`} className={pop.startsWith('') ? 'damage-pop' : 'fx-pop'}>
{pop}
</div>
)}
{eating && <div className="eat-pop">🍎 +1</div>}
</div>
)}
</div>
@@ -284,7 +364,7 @@ function clashDamageTaken(events: BattleEvent[], idx: number, seat: number): num
for (let k = idx - 1; k >= 0; k--) {
const e = events[k]
if (e.type === 'reveal' && e.seat === seat && e.card?.kind === 'pet') break
if (e.type === 'rock' && e.target === seat) {
if ((e.type === 'rock' || e.type === 'heal') && (e.target ?? e.seat) === seat) {
before = e.damageAfter ?? 0
break
}