Initial pass at unicorn pack.
This commit is contained in:
@@ -23,6 +23,8 @@ interface UnitVis {
|
||||
bonus: number
|
||||
damage: number
|
||||
dying: boolean
|
||||
spooked: number // Unicorn pack ailment: lowers the pet's clash attack
|
||||
exposed: number // Unicorn pack ailment: raises damage it takes per hit
|
||||
}
|
||||
|
||||
interface SideVis {
|
||||
@@ -54,6 +56,9 @@ const EVENT_MS: Record<BattleEvent['type'], number> = {
|
||||
release: 500,
|
||||
trumpet: 800,
|
||||
prevent: 900,
|
||||
mana: 800,
|
||||
ailment: 900,
|
||||
bounce: 1000,
|
||||
}
|
||||
|
||||
const appleCount = (foods: Card[]) => foods.filter((f) => f.food === 'apple').length
|
||||
@@ -104,12 +109,16 @@ function replay(events: BattleEvent[], stackSizes: number[], upto: number): Side
|
||||
bonus: 0,
|
||||
damage: 0,
|
||||
dying: false,
|
||||
spooked: 0,
|
||||
exposed: 0,
|
||||
}
|
||||
break
|
||||
}
|
||||
s.stack--
|
||||
const card = ev.card!
|
||||
if (card.kind === 'food') {
|
||||
if (card.kind === 'food' || card.kind === 'ailment') {
|
||||
// Ailments waiting on top of the deck sit in the pending fan until a
|
||||
// pet arrives; the backend then emits an 'ailment' event to attach it.
|
||||
s.pending.push(card)
|
||||
} else {
|
||||
s.unit = {
|
||||
@@ -118,6 +127,8 @@ function replay(events: BattleEvent[], stackSizes: number[], upto: number): Side
|
||||
bonus: ev.bonus ?? appleCount(s.pending),
|
||||
damage: 0,
|
||||
dying: false,
|
||||
spooked: 0,
|
||||
exposed: 0,
|
||||
}
|
||||
s.pending = []
|
||||
}
|
||||
@@ -187,6 +198,24 @@ function replay(events: BattleEvent[], stackSizes: number[], upto: number): Side
|
||||
break // pure animation; no state change
|
||||
case 'trumpet':
|
||||
break // pure animation; the pool isn't drawn on the board
|
||||
case 'mana':
|
||||
break // pure animation; the Mana pool isn't drawn on the board
|
||||
case 'ailment': {
|
||||
// A pet gained (count > 0) an ailment; count 0 means Baku shrugged it.
|
||||
const u = sides[ev.seat!].unit
|
||||
const kind = ev.card?.ailment
|
||||
if (u && ev.count && kind === 'spooked') u.spooked += ev.count
|
||||
else if (u && ev.count && kind === 'exposed') u.exposed += ev.count
|
||||
break
|
||||
}
|
||||
case 'bounce': {
|
||||
// The target pet is sent to the bottom of its deck: it leaves play and
|
||||
// the deck grows by one.
|
||||
const t = sides[ev.target!]
|
||||
t.unit = null
|
||||
t.stack++
|
||||
break
|
||||
}
|
||||
case 'prevent': {
|
||||
// Cone Snail shaved damage off the hit; the reduced total rides along.
|
||||
const u = sides[ev.seat!].unit
|
||||
@@ -228,6 +257,14 @@ function unitPop(ev: BattleEvent | null, seat: number, events: BattleEvent[], st
|
||||
case 'trumpet':
|
||||
if (ev.seat !== seat) return null
|
||||
return (ev.count ?? 0) >= 0 ? `🎺 +${ev.count}` : `🎺 ${ev.count}`
|
||||
case 'mana':
|
||||
if (ev.seat !== seat) return null
|
||||
return (ev.count ?? 0) >= 0 ? `🔮 +${ev.count}` : `🔮 ${ev.count}`
|
||||
case 'ailment':
|
||||
if (ev.seat !== seat) return null
|
||||
return ev.card?.ailment === 'spooked' ? '👻' : '🎯'
|
||||
case 'bounce':
|
||||
return ev.target === seat ? '🌀' : null
|
||||
case 'eat':
|
||||
return ev.seat === seat ? '🍎' : null
|
||||
case 'heal':
|
||||
@@ -337,7 +374,8 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
!done &&
|
||||
lastEvent?.seat === seat &&
|
||||
(lastEvent.type === 'prep' ||
|
||||
(lastEvent.type === 'reveal' && lastEvent.card?.kind === 'food'))
|
||||
(lastEvent.type === 'reveal' &&
|
||||
(lastEvent.card?.kind === 'food' || lastEvent.card?.kind === 'ailment')))
|
||||
? lastEvent.card?.id
|
||||
: null
|
||||
// A pet just set aside slides into the set-aside row beside the arena.
|
||||
@@ -450,6 +488,26 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
||||
damage={s.unit.damage}
|
||||
dead={s.unit.dying}
|
||||
/>
|
||||
{(s.unit.spooked > 0 || s.unit.exposed > 0) && (
|
||||
<div className="ailment-badges">
|
||||
{s.unit.spooked > 0 && (
|
||||
<span
|
||||
className="ailment-badge ailment-spooked"
|
||||
title={`Spooked ×${s.unit.spooked}: deals ${s.unit.spooked} less damage`}
|
||||
>
|
||||
👻{s.unit.spooked > 1 ? s.unit.spooked : ''}
|
||||
</span>
|
||||
)}
|
||||
{s.unit.exposed > 0 && (
|
||||
<span
|
||||
className="ailment-badge ailment-exposed"
|
||||
title={`Exposed ×${s.unit.exposed}: takes ${s.unit.exposed} extra damage per hit`}
|
||||
>
|
||||
🎯{s.unit.exposed > 1 ? s.unit.exposed : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{pop && (
|
||||
<div key={`pop-${step}`} className={pop.startsWith('−') ? 'damage-pop' : 'fx-pop'}>
|
||||
{pop}
|
||||
|
||||
@@ -180,7 +180,7 @@ export function CardView({
|
||||
const classes = [
|
||||
'card',
|
||||
`card-${size}`,
|
||||
card.kind === 'food' ? 'card-food' : 'card-pet',
|
||||
card.kind === 'ailment' ? 'card-ailment' : card.kind === 'food' ? 'card-food' : 'card-pet',
|
||||
selected ? 'is-selected' : '',
|
||||
disabled ? 'is-disabled' : '',
|
||||
dead ? 'is-dead' : '',
|
||||
@@ -228,7 +228,11 @@ export function CardView({
|
||||
? renderEffect(card.effectText)
|
||||
: card.kind === 'food' && card.food === 'apple'
|
||||
? '+1 power (this battle)'
|
||||
: ''}
|
||||
: card.ailment === 'spooked'
|
||||
? 'Deals 1 less damage'
|
||||
: card.ailment === 'exposed'
|
||||
? 'Takes 1 extra damage'
|
||||
: ''}
|
||||
</div>
|
||||
{selected && <div className="card-check">✓</div>}
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,9 @@ const BATTLE_ICONS: Record<BattleEvent['type'], string> = {
|
||||
release: '↩️',
|
||||
trumpet: '🎺',
|
||||
prevent: '🛡️',
|
||||
mana: '🔮',
|
||||
ailment: '👻',
|
||||
bounce: '🌀',
|
||||
}
|
||||
|
||||
// battleLogLines turns the battle events revealed up to `step` into readable
|
||||
|
||||
@@ -74,6 +74,11 @@ export function ShopPhase({ view, you, send }: Props) {
|
||||
const myTurn = view.turn === view.youSeat && !you.ready
|
||||
const avocados = you.avocados ?? 0
|
||||
const trumpets = you.trumpets ?? 0
|
||||
const mana = you.mana ?? 0
|
||||
const shopPeek = you.shopPeek
|
||||
const hasBigfoot = (you.deck ?? []).some((c) =>
|
||||
(c.effects as { action?: string }[] | undefined)?.some((e) => e.action === 'peekShop'),
|
||||
)
|
||||
const freeBuy = !!you.firstBuyFree // Manta Ray: next buy costs no gold
|
||||
const canBuy = myTurn && (you.coins > 0 || avocados > 0 || freeBuy)
|
||||
// Nudge the player to pass once there's nothing left to buy.
|
||||
@@ -382,6 +387,28 @@ export function ShopPhase({ view, you, send }: Props) {
|
||||
🎺 ×{trumpets}
|
||||
</div>
|
||||
)}
|
||||
{mana > 0 && (
|
||||
<div className="trumpet-indicator" title="Mana — a persistent resource that powers Unicorn abilities">
|
||||
🔮 ×{mana}
|
||||
</div>
|
||||
)}
|
||||
{hasBigfoot && (
|
||||
<div className="peek-indicator">
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
disabled={!myTurn || !!shopPeek}
|
||||
onClick={() => send({ type: 'peek' })}
|
||||
title="Bigfoot: look at the top of the shop deck (once per round)"
|
||||
>
|
||||
👁 Peek shop deck
|
||||
</button>
|
||||
{shopPeek && (
|
||||
<div className="peek-card" title="Next card off the shop deck">
|
||||
<CardView card={shopPeek} size="sm" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Actions */}
|
||||
@@ -496,6 +523,28 @@ export function ShopPhase({ view, you, send }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Water of Youth sacrifice picker (Unicorn pack) */}
|
||||
{view.pendingSacrifice?.playerId === you.id && (
|
||||
<div className="modal-backdrop">
|
||||
<div className="modal">
|
||||
<h3>Sacrifice a pet to draw a free tier {view.pendingSacrifice.tier} card</h3>
|
||||
<div className="modal-cards">
|
||||
{(view.pendingSacrifice.options ?? []).map((id) => {
|
||||
const c = deck.find((d) => d.id === id)
|
||||
return c ? (
|
||||
<CardView
|
||||
key={id}
|
||||
card={c}
|
||||
size="lg"
|
||||
onClick={() => send({ type: 'sacrificeChoose', card: id })}
|
||||
/>
|
||||
) : null
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cardFlyer &&
|
||||
createPortal(
|
||||
<div
|
||||
|
||||
@@ -173,6 +173,9 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
||||
{(p.avocados ?? 0) > 0 && (
|
||||
<span className="chip" title="Set-aside Avocados">🥑 {p.avocados}</span>
|
||||
)}
|
||||
{(p.mana ?? 0) > 0 && (
|
||||
<span className="chip" title="Mana pool">🔮 {p.mana}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user