fuzzyworld

explodorama / files / src / sim / charges.ts

charges.ts

3.7 KB · history · blame

1import { PRICES } from './shop'
2import 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). */
12export const BOMB_BOOST = 3
13
14/** How many fire hits a Flame Pass saves the player from (user's choice, 2026-10-25). */
15export 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 */
22export const PASSIVE_CHARGES: readonly ChargeId[] = ['shield', 'flamePass']
23
24/** What one charge gives when picked up: bombs or launches, or a single use. */
25export 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. */
35export 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. */
38export function salePrice(kind: ChargeId, left = CHARGE_AMOUNT[kind]): number {
39 return Math.ceil(((PRICES[kind] / 2) * left) / CHARGE_AMOUNT[kind])
40}
41
42function 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 */
54export 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. */
69export 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. */
76export 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. */
83export 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. */
89export function holds(s: GameState, kind: ChargeId): boolean {
90 return s.charge?.kind === kind
91}