explodorama / files / src / sim / charges.ts
charges.ts
| 1 | import { PRICES } from './shop' |
| 2 | import type { ChargeId, GameEvent, GameState } from './types' |
| 3 | |
| 4 | /** |
| 5 | * One charge at a time (PLAN.md §19.7; user request, 2026-10-24). The player carries a single |
| 6 | * charge and uses it when they choose, with the charge button; a Shield works by itself. Picking |
| 7 | * up a different one asks: sell the new one for coins, or swap it in (the old one is sold). The |
| 8 | * same kind adds to what is held. |
| 9 | */ |
| 10 | |
| 11 | /** How many bombs one Pierce, Power or Nuke charge changes, and an ICBM's launches (user's choice, 2026-10-23). */ |
| 12 | export const BOMB_BOOST = 3 |
| 13 | |
| 14 | /** How many fire hits a Flame Pass saves the player from (user's choice, 2026-10-25). */ |
| 15 | export const FLAME_PASS_HITS = 3 |
| 16 | |
| 17 | /** |
| 18 | * Shield and Flame Pass work by themselves while in the charge slot (user request, 2026-10-25: |
| 19 | * they had to be raised with the charge button): a hit of their kind uses one of their uses, and |
| 20 | * the slot empties when none is left. |
| 21 | */ |
| 22 | export const PASSIVE_CHARGES: readonly ChargeId[] = ['shield', 'flamePass'] |
| 23 | |
| 24 | /** What one charge gives when picked up: bombs or launches, or a single use. */ |
| 25 | export const CHARGE_AMOUNT: Record<ChargeId, number> = { |
| 26 | pierce: BOMB_BOOST, power: BOMB_BOOST, nuke: BOMB_BOOST, icbm: BOMB_BOOST, shield: 1, freeze: 1, decoy: 1, |
| 27 | // Teleport: 3 jumps (user's choice, 2026-10-25); Flame Pass: its fire hits. |
| 28 | teleport: 3, flamePass: FLAME_PASS_HITS, |
| 29 | // Acid: one bomb (PLAN.md §19.4). |
| 30 | acid: 1, |
| 31 | } |
| 32 | |
| 33 | |
| 34 | /** The charges used as a bomb from the charge button. */ |
| 35 | export const BOMB_CHARGES: readonly ChargeId[] = ['pierce', 'power', 'nuke', 'acid'] |
| 36 | |
| 37 | /** What a charge sells for: half its shop price, rounded up, for as much of it as is left. */ |
| 38 | export function salePrice(kind: ChargeId, left = CHARGE_AMOUNT[kind]): number { |
| 39 | return Math.ceil(((PRICES[kind] / 2) * left) / CHARGE_AMOUNT[kind]) |
| 40 | } |
| 41 | |
| 42 | function sell(s: GameState, kind: ChargeId, left: number, events: GameEvent[]): void { |
| 43 | const coins = salePrice(kind, left) |
| 44 | s.coins += coins |
| 45 | events.push({ type: 'chargeSold', kind, coins }) |
| 46 | } |
| 47 | |
| 48 | /** |
| 49 | * A charge comes to the player — picked up, chosen from a gem or bought: into the empty slot, onto |
| 50 | * the same kind, or else as a choice to make (`chargeOffer`; user request, 2026-10-25: a gem's |
| 51 | * charge replaced the one held without asking). `swap`: the choice made, take it and sell the one |
| 52 | * held. A second one coming in the same turn, while a choice is already waiting, is sold. |
| 53 | */ |
| 54 | export function takeCharge(s: GameState, kind: ChargeId, events: GameEvent[], swap = false, left = CHARGE_AMOUNT[kind]): void { |
| 55 | const held = s.charge |
| 56 | if (!held) s.charge = { kind, left } |
| 57 | else if (held.kind === kind) held.left += left |
| 58 | else if (swap) { |
| 59 | sell(s, held.kind, held.left, events) |
| 60 | s.charge = { kind, left } |
| 61 | } else if (s.chargeOffer) sell(s, kind, left, events) |
| 62 | else { |
| 63 | s.chargeOffer = { kind, left } |
| 64 | events.push({ type: 'chargeOffered', kind, held: held.kind }) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | /** The choice: keep the charge held and sell the new one. */ |
| 69 | export function sellOffered(s: GameState, events: GameEvent[]): void { |
| 70 | const offered = s.chargeOffer! |
| 71 | s.chargeOffer = null |
| 72 | sell(s, offered.kind, offered.left, events) |
| 73 | } |
| 74 | |
| 75 | /** The choice: take the new charge; the one held is sold, for what is left of it. */ |
| 76 | export function swapOffered(s: GameState, events: GameEvent[]): void { |
| 77 | const offered = s.chargeOffer! |
| 78 | s.chargeOffer = null |
| 79 | takeCharge(s, offered.kind, events, true, offered.left) |
| 80 | } |
| 81 | |
| 82 | /** One use of the charge held: the slot empties when none is left. */ |
| 83 | export function spendCharge(s: GameState): void { |
| 84 | if (!s.charge) return |
| 85 | if (--s.charge.left <= 0) s.charge = null |
| 86 | } |
| 87 | |
| 88 | /** Whether the player holds this charge now. */ |
| 89 | export function holds(s: GameState, kind: ChargeId): boolean { |
| 90 | return s.charge?.kind === kind |
| 91 | } |