explodorama / files / src / sim / types.ts
types.ts
5551eb37 2026-10-20 Pavel Safronov | 1 | /** |
| 2 | * The shapes of the game's state, actions and events. Everything in `sim/` is plain data and | |
| 3 | * pure functions: no DOM, no LittleJS, no `Math.random()`. | |
| 4 | */ | |
| 5 | ||
| 6 | /** A tile position. x grows to the right, y grows downward; (0, 0) is the top-left corner. */ | |
| 7 | export interface Vec { | |
| 8 | x: number | |
| 9 | y: number | |
| 10 | } | |
| 11 | ||
| 12 | export type Dir = 'up' | 'down' | 'left' | 'right' | |
| 13 | ||
| 14 | export const DIRS: Record<Dir, Vec> = { | |
| 15 | up: { x: 0, y: -1 }, | |
| 16 | down: { x: 0, y: 1 }, | |
| 17 | left: { x: -1, y: 0 }, | |
| 18 | right: { x: 1, y: 0 }, | |
| 19 | } | |
| 20 | ||
| 21 | export const DIR_LIST: readonly Dir[] = ['up', 'down', 'left', 'right'] | |
| 22 | ||
5f066960 2026-10-26 Pavel Safronov | 23 | /** |
| 24 | * `wall` is a hard wall or pillar (indestructible); `block` is a soft block (flame destroys it). | |
| 25 | * Since 2026-10-25 (PLAN.md §19.3, §19.4): `metal`, a metal crate (indestructible, anywhere); | |
| 26 | * `barrel`, an oil barrel (blocks like a crate; flame lights it and it goes off the next turn); | |
| 27 | * `rubble`, a fallen ceiling (indestructible, for good). Bushes, oil and spikes lie on floor. | |
| 28 | */ | |
| 29 | export type Cell = 'floor' | 'wall' | 'block' | 'metal' | 'barrel' | 'rubble' | |
| 30 | ||
| 31 | /** Cells nothing goes through or into, and flame stops before: walls, metal crates, rubble. */ | |
| 32 | export function isSolid(cell: Cell): boolean { | |
| 33 | return cell === 'wall' || cell === 'metal' || cell === 'rubble' | |
| 34 | } | |
| 35 | ||
| 36 | /** Spikes on a floor tile: up on the turns where (turn + phase) is even, and then they hurt. */ | |
| 37 | export interface Spike { | |
| 38 | cell: number | |
| 39 | phase: number | |
| 40 | } | |
5551eb37 2026-10-20 Pavel Safronov | 41 | |
| 42 | export interface Bomb { | |
| 43 | pos: Vec | |
| 44 | /** How many more player actions until it explodes (PLAN.md §1). */ | |
| 45 | timer: number | |
| 46 | /** Blast radius in tiles, in each of the 4 directions. */ | |
| 47 | fire: number | |
| 48 | /** The turn it was placed on. A bomb is not ticked on the turn it is placed. */ | |
| 49 | placedTurn: number | |
52d68b59 2026-10-24 Pavel Safronov | 50 | /** Pierce: its flame goes through soft blocks, burning them all. Set from the player's boosts when placed. */ |
| 51 | pierce?: boolean | |
| 52 | /** Power: its flame reaches to the walls. */ | |
| 53 | power?: boolean | |
| 54 | /** Nuke: it also burns every tile within NUKE_RANGE steps, through anything. */ | |
| 55 | nuke?: boolean | |
7f242d36 2026-10-25 Pavel Safronov | 56 | /** Proximity alert: it goes off the turn an enemy is in its reach (the power-up, when placed). */ |
| 57 | proximity?: boolean | |
5f066960 2026-10-26 Pavel Safronov | 58 | /** A lit oil barrel, not the player's: it doesn't count to their limit, nor can they set it off. */ |
| 59 | barrel?: boolean | |
| 60 | /** Acid (the charge, 2026-10-25): its flame also cracks each pillar it reaches. */ | |
| 61 | acid?: boolean | |
5551eb37 2026-10-20 Pavel Safronov | 62 | } |
| 63 | ||
| 64 | export interface Stats { | |
| 65 | /** Most of the player's own bombs on the board at once. */ | |
| 66 | capacity: number | |
| 67 | fire: number | |
| 68 | /** The timer a newly placed bomb starts at. */ | |
| 69 | fuse: number | |
| 70 | } | |
| 71 | ||
c37a2b0b 2026-10-20 Pavel Safronov | 72 | /** |
| 73 | * `dead`: lost a life and waiting for `continue`. `gameOver`: no lives left. | |
| 74 | * `cleared`: stepped onto the open exit; `continue` goes to the next level. | |
27f24fed 2026-10-20 Pavel Safronov | 75 | * `won`: cleared the last level (PLAN.md §11); the run is over. |
c37a2b0b 2026-10-20 Pavel Safronov | 76 | */ |
27f24fed 2026-10-20 Pavel Safronov | 77 | export type Status = 'playing' | 'dead' | 'gameOver' | 'cleared' | 'won' |
| 78 | ||
00ad7d0a 2026-10-24 Pavel Safronov | 79 | /** |
| 80 | * Chosen when a run starts, fixed for the run, part of its record. (A board size and Crumbling | |
| 81 | * crates were options too until 2026-10-23: now there is one board, and crates always crumble.) | |
| 82 | */ | |
beb9f2b7 2026-10-21 Pavel Safronov | 83 | export interface RunOptions { |
6e94f995 2026-10-24 Pavel Safronov | 84 | /** |
| 85 | * Developer mode: an extra, rainbow gem lies right of the start, offering every power-up not | |
| 86 | * held yet; the level keeps its own. Nothing else. | |
| 87 | */ | |
beb9f2b7 2026-10-21 Pavel Safronov | 88 | dev: boolean |
00ad7d0a 2026-10-24 Pavel Safronov | 89 | /** |
3cf3607b 2026-10-24 Pavel Safronov | 90 | * Immortal: nothing can kill the player, so the run |
00ad7d0a 2026-10-24 Pavel Safronov | 91 | * can be played to the end — but it earns no achievements (when they exist: the victory |
d603166d 2026-10-25 Pavel Safronov | 92 | * certificate). |
00ad7d0a 2026-10-24 Pavel Safronov | 93 | */ |
| 94 | immortal: boolean | |
a313eb7d 2026-10-25 Pavel Safronov | 95 | /** |
| 96 | * Rule variants for the balance lab to compare (PLAN.md §19.1), never set in play. `squeeze`: the | |
| 97 | * old Diagonal, which could step between two blocked tiles (the rule until 2026-10-25). | |
6c3fcc31 2026-10-26 Pavel Safronov | 98 | * `underfoot`: the NES rule, a bomb goes on the player's own tile, not one next to them (to |
| 99 | * measure, 2026-10-25; the player walks off it, and nothing walks onto it). | |
a313eb7d 2026-10-25 Pavel Safronov | 100 | */ |
6c3fcc31 2026-10-26 Pavel Safronov | 101 | lab?: { squeeze?: boolean; underfoot?: boolean } |
beb9f2b7 2026-10-21 Pavel Safronov | 102 | } |
| 103 | ||
00ad7d0a 2026-10-24 Pavel Safronov | 104 | export const DEFAULT_OPTIONS: RunOptions = { dev: false, immortal: false } |
beb9f2b7 2026-10-21 Pavel Safronov | 105 | |
52d68b59 2026-10-24 Pavel Safronov | 106 | /** The power-ups (PLAN.md §5, after the NES eight). Taking a level's gem offers a choice of them. */ |
27f24fed 2026-10-20 Pavel Safronov | 107 | export type PowerUpKind = |
fcf44f32 2026-10-25 Pavel Safronov | 108 | | 'bombPlus' | 'firePlus' | 'tnt' | 'fusePlus' | 'run' | 'wallPass' | 'bombPass' | 'detonator' |
| 109 | | 'star' | 'coinBag' | 'heart' | 'kick' | 'throw' | 'diagonal' | |
7f242d36 2026-10-25 Pavel Safronov | 110 | | 'rpg' | 'proximity' | 'slide' |
27f24fed 2026-10-20 Pavel Safronov | 111 | |
6e94f995 2026-10-24 Pavel Safronov | 112 | /** |
| 113 | * A gem's colour, from the seed. Only how it looks: every level's gem offers the same kind of | |
| 114 | * choice. Rainbow is only Developer mode's extra gem (user request, 2026-10-24). | |
| 115 | */ | |
| 116 | export type GemColor = 'blue' | 'green' | 'red' | 'yellow' | 'rainbow' | |
52d68b59 2026-10-24 Pavel Safronov | 117 | |
| 118 | /** | |
| 119 | * The level's gem, hidden one per level under a soft block. Taking it offers OFFER_SIZE | |
| 120 | * power-ups to choose one from (user request, 2026-10-23: it used to be one fixed power-up). | |
| 121 | */ | |
27f24fed 2026-10-20 Pavel Safronov | 122 | export interface PowerUp { |
| 123 | pos: Vec | |
52d68b59 2026-10-24 Pavel Safronov | 124 | color: GemColor |
6e94f995 2026-10-24 Pavel Safronov | 125 | /** |
| 126 | * Developer mode's extra gem, beside the start; the level keeps its own (user request, | |
| 127 | * 2026-10-23). It is the rainbow one and offers every power-up not held yet (2026-10-24). | |
| 128 | */ | |
39c26813 2026-10-24 Pavel Safronov | 129 | extra?: boolean |
27f24fed 2026-10-20 Pavel Safronov | 130 | } |
| 131 | ||
99869630 2026-10-23 Pavel Safronov | 132 | /** |
52d68b59 2026-10-24 Pavel Safronov | 133 | * Abilities from power-ups. Run, Detonate, Kick, Throw, Teleport and Diagonal are actions; the |
| 134 | * passes change where the player can go. All are kept through a death (since 2026-10-23). | |
99869630 2026-10-23 Pavel Safronov | 135 | */ |
27f24fed 2026-10-20 Pavel Safronov | 136 | export interface Powers { |
52d68b59 2026-10-24 Pavel Safronov | 137 | /** The Run power-up (called Skates until 2026-10-22): 2 tiles in a straight line. */ |
27f24fed 2026-10-20 Pavel Safronov | 138 | run: boolean |
52d68b59 2026-10-24 Pavel Safronov | 139 | /** Kick: walk into a bomb and it slides away. */ |
99869630 2026-10-23 Pavel Safronov | 140 | kick: boolean |
28670c3e 2026-10-24 Pavel Safronov | 141 | /** Throw: a bomb 2 tiles away in a straight line, over a crate, bomb or enemy (not a pillar or wall). */ |
99869630 2026-10-23 Pavel Safronov | 142 | throw: boolean |
52d68b59 2026-10-24 Pavel Safronov | 143 | /** Diagonal: Move can also step diagonally (a charge until 2026-10-23, user request). */ |
| 144 | diagonal: boolean | |
| 145 | /** Detonator: the Detonate action, set off one of your bombs now. */ | |
27f24fed 2026-10-20 Pavel Safronov | 146 | detonate: boolean |
52d68b59 2026-10-24 Pavel Safronov | 147 | /** Walk through soft blocks. */ |
27f24fed 2026-10-20 Pavel Safronov | 148 | wallPass: boolean |
52d68b59 2026-10-24 Pavel Safronov | 149 | /** Walk over bombs. */ |
27f24fed 2026-10-20 Pavel Safronov | 150 | bombPass: boolean |
7f242d36 2026-10-25 Pavel Safronov | 151 | /** RPG: Bomb can also fire a bomb in a straight line, as far as it flies (user request, 2026-10-24). */ |
| 152 | rpg: boolean | |
| 153 | /** Proximity alert: your bombs go off as soon as an enemy is in their reach. */ | |
| 154 | proximity: boolean | |
| 155 | /** Slide: Move can also slide in a straight line until a wall, crate or bomb stops you. */ | |
| 156 | slide: boolean | |
27f24fed 2026-10-20 Pavel Safronov | 157 | } |
c37a2b0b 2026-10-20 Pavel Safronov | 158 | |
565a6873 2026-10-25 Pavel Safronov | 159 | /** The enemy types so far (PLAN.md §10); the Reaper is for later. */ |
| 160 | export type EnemyKind = 'blob' | 'seeker' | 'drifter' | 'sprinter' | 'ghost' | 'wisp' | 'hunter' | |
c37a2b0b 2026-10-20 Pavel Safronov | 161 | |
| 162 | export interface Enemy { | |
| 163 | /** Stable for the enemy's life, so the renderer can follow it between turns. */ | |
| 164 | id: number | |
| 165 | kind: EnemyKind | |
| 166 | pos: Vec | |
82adee18 2026-10-20 Pavel Safronov | 167 | /** The way its last planned move goes. Blobs and Drifters keep going this way. */ |
c37a2b0b 2026-10-20 Pavel Safronov | 168 | dir: Dir |
82adee18 2026-10-20 Pavel Safronov | 169 | /** |
565a6873 2026-10-25 Pavel Safronov | 170 | * Its next moves, up to PLAN_AHEAD of them, first one first (announced in phase 5, taken one |
| 171 | * per turn in phase 2): where it will be after each. Usually the next tile; the tile it is on | |
| 172 | * for a rest (Ghost, Sprinter); 2 tiles on for a Sprinter's dash. Empty: it has nowhere to go | |
| 173 | * and stays put. | |
82adee18 2026-10-20 Pavel Safronov | 174 | */ |
| 175 | plan: Vec[] | |
c37a2b0b 2026-10-20 Pavel Safronov | 176 | /** Spawned as a punishment (the exit was flamed): drops nothing. */ |
| 177 | spawned: boolean | |
52d68b59 2026-10-24 Pavel Safronov | 178 | /** |
| 179 | * Just let out by a flamed exit or gem: the flame that let it out (still lingering next turn) | |
| 180 | * can't hurt it; any new explosion can (user's choice, 2026-10-23: they used to die in it at once). | |
| 181 | */ | |
| 182 | sheltered: boolean | |
c37a2b0b 2026-10-20 Pavel Safronov | 183 | } |
| 184 | ||
99869630 2026-10-23 Pavel Safronov | 185 | /** |
52d68b59 2026-10-24 Pavel Safronov | 186 | * The charges dropped by enemies (PLAN.md §3.3). Since 2026-10-23 (user request) they aren't |
| 187 | * actions: each works the moment it is picked up, as a temporary change (`applyCharge`). | |
| 188 | * Pierce, Power and Nuke change the next BOMB_BOOST bombs; Shield blocks the next hit; Freeze | |
| 189 | * freezes every enemy; Decoy leaves a copy of the player on the pickup tile. (Line was a charge | |
| 190 | * until then, and Diagonal, which became a power-up the same day; Kick, Throw and Teleport until | |
| 191 | * 2026-10-22; Fuse ±1 was removed on 2026-10-22.) | |
99869630 2026-10-23 Pavel Safronov | 192 | */ |
5f066960 2026-10-26 Pavel Safronov | 193 | export type ChargeId = 'pierce' | 'power' | 'nuke' | 'shield' | 'freeze' | 'decoy' | 'icbm' | 'flamePass' | 'teleport' | 'acid' |
c37a2b0b 2026-10-20 Pavel Safronov | 194 | |
fcf44f32 2026-10-25 Pavel Safronov | 195 | /** Flame Pass and Teleport were power-ups until 2026-10-25, when the balance lab found each won the game alone. */ |
5f066960 2026-10-26 Pavel Safronov | 196 | export const CHARGE_IDS: readonly ChargeId[] = ['pierce', 'power', 'nuke', 'shield', 'freeze', 'decoy', 'icbm', 'flamePass', 'teleport', 'acid'] |
82adee18 2026-10-20 Pavel Safronov | 197 | |
e3f29330 2026-10-24 Pavel Safronov | 198 | /** |
| 199 | * What a gem can offer: a power-up, or a charge (only Developer mode's rainbow gem, to test them | |
| 200 | * at once: user request, 2026-10-24). A chosen charge works as if picked up. | |
| 201 | */ | |
| 202 | export type OfferKind = PowerUpKind | ChargeId | |
| 203 | ||
| 204 | export function isCharge(kind: OfferKind): kind is ChargeId { | |
| 205 | return (CHARGE_IDS as readonly OfferKind[]).includes(kind) | |
| 206 | } | |
| 207 | ||
52d68b59 2026-10-24 Pavel Safronov | 208 | /** What picked-up charges still have to give. Kept from level to level and through deaths. */ |
d3e4ed26 2026-10-25 Pavel Safronov | 209 | /** |
| 210 | * The one charge the player carries (PLAN.md §19.7; user request, 2026-10-24) and what is left | |
| 211 | * of it: bombs for Pierce, Power and Nuke, launches for ICBM, 1 for the rest. Used deliberately, | |
| 212 | * with the charge button (a Shield works by itself, when the player is hit). | |
| 213 | */ | |
| 214 | export interface HeldCharge { | |
| 215 | kind: ChargeId | |
| 216 | left: number | |
52d68b59 2026-10-24 Pavel Safronov | 217 | } |
82adee18 2026-10-20 Pavel Safronov | 218 | |
| 219 | /** A stand-in for the player that chasing enemies go after instead, for a few turns. */ | |
d3e4ed26 2026-10-25 Pavel Safronov | 220 | /** |
| 221 | * A Decoy put down with the charge button: chasers head for it instead of the player. It lasts | |
| 222 | * until the level ends, an enemy reaches it, or flame burns it (user request, 2026-10-25; it lasted | |
| 223 | * 3 turns). | |
| 224 | */ | |
82adee18 2026-10-20 Pavel Safronov | 225 | export interface Decoy { |
| 226 | pos: Vec | |
| 227 | } | |
c37a2b0b 2026-10-20 Pavel Safronov | 228 | |
| 229 | /** Something lying on a floor tile to walk over and collect: coins, or one action charge. */ | |
fcf44f32 2026-10-25 Pavel Safronov | 230 | export type Drop = |
| 231 | | { pos: Vec; kind: 'coins'; amount: number } | |
| 232 | /** `left`: what is left of it, for a charge dropped where the player died (full when absent). */ | |
| 233 | | { pos: Vec; kind: ChargeId; left?: number } | |
5551eb37 2026-10-20 Pavel Safronov | 234 | |
| 235 | export interface GameState { | |
| 236 | seed: number | |
beb9f2b7 2026-10-21 Pavel Safronov | 237 | options: RunOptions |
5551eb37 2026-10-20 Pavel Safronov | 238 | level: number |
| 239 | /** Turns taken on this attempt at this level, starting at 0. */ | |
| 240 | turn: number | |
| 241 | width: number | |
| 242 | height: number | |
| 243 | /** Row-major: the cell at (x, y) is `cells[y * width + x]`. */ | |
| 244 | cells: Cell[] | |
| 245 | player: Vec | |
| 246 | lives: number | |
| 247 | stats: Stats | |
| 248 | bombs: Bomb[] | |
| 249 | status: Status | |
c37a2b0b 2026-10-20 Pavel Safronov | 250 | enemies: Enemy[] |
| 251 | nextEnemyId: number | |
| 252 | drops: Drop[] | |
| 253 | /** Under a soft block until that block burns. Open (walkable to win) once no enemies are left. */ | |
| 254 | exit: Vec | |
| 255 | /** Finishing the level in at most this many turns pays PAR_BONUS coins. */ | |
| 256 | par: number | |
| 257 | coins: number | |
d3e4ed26 2026-10-25 Pavel Safronov | 258 | /** The one charge carried, if any; kept to the next level and through a death. */ |
| 259 | charge: HeldCharge | null | |
| 260 | /** | |
| 261 | * A charge just picked up while another is carried: the player sells it, or swaps it for the one | |
| 262 | * held (which is sold). Nothing else happens until they choose; it takes no turn. | |
| 263 | */ | |
fcf44f32 2026-10-25 Pavel Safronov | 264 | chargeOffer: HeldCharge | null |
d3e4ed26 2026-10-25 Pavel Safronov | 265 | /** |
5f066960 2026-10-26 Pavel Safronov | 266 | * The board's objects on floor (PLAN.md §19.3; 2026-10-25): bushes (walk through; they burn, and |
| 267 | * fire spreads to the bushes touching them a turn later), oil (flame on any of a spill burns all | |
| 268 | * of it at once), spikes (up every other turn). Cell indices. | |
fcf44f32 2026-10-25 Pavel Safronov | 269 | */ |
5f066960 2026-10-26 Pavel Safronov | 270 | bushes: number[] |
| 271 | /** Bushes that catch fire next turn, from a bush that burned this turn. */ | |
| 272 | smoldering: number[] | |
| 273 | oil: number[] | |
| 274 | spikes: Spike[] | |
| 275 | /** Where the enemies last saw the player: a bush hides them, and chasers head here instead. */ | |
| 276 | lastSeen: Vec | |
| 277 | /** Acid (§19.4): pillars cracked by an acid bomb; a second blast breaks them. Cell indices. */ | |
| 278 | cracked: number[] | |
| 279 | /** The ceiling round a broken pillar, coming down: these tiles, in this many turns. */ | |
| 280 | collapsing: { cells: number[]; turns: number }[] | |
99869630 2026-10-23 Pavel Safronov | 281 | /** Freeze: how many more enemy moves are skipped (FREEZE_TURNS when used; the turn it is used counts). */ |
| 282 | frozen: number | |
27f24fed 2026-10-20 Pavel Safronov | 283 | powers: Powers |
| 284 | /** Turns played and lives lost over the whole run, for the ending. */ | |
| 285 | runTurns: number | |
0580e85e 2026-10-24 Pavel Safronov | 286 | /** Enemies killed over the whole run, for the high-score board (2026-10-24). */ |
| 287 | runKills: number | |
27f24fed 2026-10-20 Pavel Safronov | 288 | deaths: number |
| 289 | /** Turns of Star invincibility left. */ | |
| 290 | star: number | |
39c26813 2026-10-24 Pavel Safronov | 291 | /** |
| 292 | * This level's gems: hidden while its block stands, then lying in the open until taken; gone | |
| 293 | * once taken or burned. One per level, and in Developer mode an extra one (since 2026-10-23). | |
| 294 | */ | |
| 295 | gems: PowerUp[] | |
52d68b59 2026-10-24 Pavel Safronov | 296 | /** |
e3f29330 2026-10-24 Pavel Safronov | 297 | * The power-ups (and, from the rainbow gem, charges) the gem just taken offers. While it is |
| 298 | * set, the only action is `choose`; a death (or game over) in the same turn drops it, and the | |
| 299 | * retry has the gem back. | |
52d68b59 2026-10-24 Pavel Safronov | 300 | */ |
e3f29330 2026-10-24 Pavel Safronov | 301 | offer: OfferKind[] | null |
8edb5cb9 2026-11-01 Pavel Safronov | 302 | /** Gems taken on the same turn as the one on offer (a slide over two): each offers in turn, after the one before is answered. */ |
| 303 | waitingGems: PowerUp[] | |
e78b7781 2026-10-25 Pavel Safronov | 304 | /** The shop, open after a cleared level every few levels until the player goes on (sim/shop.ts). */ |
| 305 | shop: Shop | null | |
82adee18 2026-10-20 Pavel Safronov | 306 | /** |
| 307 | * Tile indices still on fire from last turn's explosions. Flame lingers one extra turn: it | |
| 308 | * kills creatures that are in it or walk into it this turn, and the player can't step or put | |
| 309 | * a bomb there. It doesn't burn crates, drops or bombs a second time. | |
| 310 | */ | |
| 311 | burning: number[] | |
0580e85e 2026-10-24 Pavel Safronov | 312 | /** How the player last died on this attempt, for the banner; null while alive. */ |
| 313 | death: Death | null | |
00ad7d0a 2026-10-24 Pavel Safronov | 314 | /** Crumbling crates (once the last enemy is gone): tile index and turns until it is gone. */ |
beb9f2b7 2026-10-21 Pavel Safronov | 315 | crumbling: { cell: number; turns: number }[] |
82adee18 2026-10-20 Pavel Safronov | 316 | decoy: Decoy | null |
c37a2b0b 2026-10-20 Pavel Safronov | 317 | /** The AI and drop random streams' positions (`Rng.state`), so a turn is a pure function of the state. */ |
| 318 | aiRng: number | |
| 319 | dropRng: number | |
5551eb37 2026-10-20 Pavel Safronov | 320 | } |
| 321 | ||
| 322 | /** The actions the player can pick on the board. */ | |
7f242d36 2026-10-25 Pavel Safronov | 323 | export type ActionId = |
| 324 | | 'move' | 'bomb' | 'wait' | 'run' | 'detonate' | 'kick' | 'throw' | 'teleport' | 'diagonal' | 'rpg' | 'icbm' | 'slide' | |
5f066960 2026-10-26 Pavel Safronov | 325 | /** The carried charge, used (2026-10-24): a Pierce, Power or Nuke bomb; a Freeze; a Decoy. */ |
| 326 | | 'charged' | 'freeze' | 'decoy' | |
5551eb37 2026-10-20 Pavel Safronov | 327 | |
52d68b59 2026-10-24 Pavel Safronov | 328 | /** `choose`: one of the gem's offer. It takes no turn. */ |
e78b7781 2026-10-25 Pavel Safronov | 329 | export type Action = |
| 330 | | { id: ActionId; target?: Vec } | |
| 331 | | { id: 'continue' } | |
| 332 | | { id: 'choose'; kind: OfferKind } | |
d3e4ed26 2026-10-25 Pavel Safronov | 333 | /** A charge picked up while carrying another: sell the new one, or swap it in (the old one sold). No turn. */ |
| 334 | | { id: 'sellCharge' } | |
| 335 | | { id: 'swapCharge' } | |
bb3823d2 2026-10-31 Pavel Safronov | 336 | /** The same choice, a third way: use the charge held now (`fireableHeld`), then take the new one. No turn. */ |
| 337 | | { id: 'fireCharge' } | |
e78b7781 2026-10-25 Pavel Safronov | 338 | /** In the shop (sim/shop.ts): buy one of its items, or restock it. */ |
| 339 | | { id: 'buy'; kind: OfferKind } | |
| 340 | | { id: 'reroll' } | |
| 341 | ||
| 342 | /** The shop between levels (sim/shop.ts, PLAN.md §8): what it has, what is sold, how often restocked. */ | |
| 343 | export interface Shop { | |
| 344 | items: OfferKind[] | |
| 345 | sold: OfferKind[] | |
| 346 | rerolls: number | |
| 347 | } | |
5551eb37 2026-10-20 Pavel Safronov | 348 | |
0580e85e 2026-10-24 Pavel Safronov | 349 | /** |
| 350 | * How the player died, for the death and game-over banners (user request, 2026-10-24): walked | |
| 351 | * into an enemy, or one walked into them; caught in a blast of a bomb that was due, or only by | |
| 352 | * one a chain set off; or by flame still burning from last turn. | |
| 353 | */ | |
| 354 | export interface Death { | |
5f066960 2026-10-26 Pavel Safronov | 355 | /** `spread`: flame over oil or through bushes, with no blast there (PLAN.md §19.3). */ |
6c3fcc31 2026-10-26 Pavel Safronov | 356 | /** `boxedIn`: nowhere to step or put a bomb, for good (2026-10-26). */ |
| 357 | cause: 'walkedIntoEnemy' | 'caughtByEnemy' | 'blast' | 'chain' | 'lingering' | 'spread' | 'spikes' | 'crushed' | 'trapped' | 'boxedIn' | |
0580e85e 2026-10-24 Pavel Safronov | 358 | /** The enemy, for the two enemy causes. */ |
| 359 | enemy?: EnemyKind | |
| 360 | /** The turn it happened on (counted from 1, as the top bar shows after it). */ | |
| 361 | turn: number | |
| 362 | } | |
| 363 | ||
5551eb37 2026-10-20 Pavel Safronov | 364 | /** What `step()` reports happened, in order, for the renderer to play back. */ |
| 365 | export type GameEvent = | |
| 366 | | { type: 'moved'; from: Vec; to: Vec } | |
1f0038ac 2026-11-01 Pavel Safronov | 367 | /** The player slid on over oil after a move: from the tile they landed on to where they stopped. */ |
| 368 | | { type: 'oilSlid'; from: Vec; to: Vec } | |
5551eb37 2026-10-20 Pavel Safronov | 369 | | { type: 'waited' } |
| 370 | | { type: 'bombPlaced'; pos: Vec; timer: number } | |
| 371 | | { type: 'bombTicked'; pos: Vec; timer: number } | |
| 372 | | { type: 'exploded'; pos: Vec; tiles: Vec[] } | |
| 373 | | { type: 'blockDestroyed'; pos: Vec } | |
| 374 | | { type: 'playerDied'; pos: Vec; livesLeft: number } | |
| 375 | | { type: 'levelRestarted' } | |
c37a2b0b 2026-10-20 Pavel Safronov | 376 | | { type: 'enemyMoved'; id: number; from: Vec; to: Vec } |
| 377 | | { type: 'enemyDied'; id: number; pos: Vec } | |
| 378 | | { type: 'enemiesSpawned'; pos: Vec; count: number } | |
| 379 | | { type: 'dropSpawned'; drop: Drop } | |
| 380 | | { type: 'dropBurned'; drop: Drop } | |
| 381 | | { type: 'pickedUp'; drop: Drop } | |
| 382 | | { type: 'exitRevealed'; pos: Vec } | |
| 383 | | { type: 'exitOpened'; pos: Vec } | |
0b63d158 2026-10-25 Pavel Safronov | 384 | | { type: 'levelCleared'; level: number; turns: number; clearBonus: number; parBonus: number } |
c37a2b0b 2026-10-20 Pavel Safronov | 385 | | { type: 'levelStarted'; level: number } |
| 386 | | { type: 'bombKicked'; from: Vec; to: Vec } | |
7f242d36 2026-10-25 Pavel Safronov | 387 | /** An RPG or ICBM bomb flew from the player to where it landed. */ |
| 388 | | { type: 'bombFired'; from: Vec; to: Vec } | |
| 389 | /** Proximity alert: an enemy in a bomb's reach set it off. */ | |
| 390 | | { type: 'proximityTriggered'; pos: Vec } | |
c37a2b0b 2026-10-20 Pavel Safronov | 391 | | { type: 'shieldBroke'; pos: Vec } |
d3e4ed26 2026-10-25 Pavel Safronov | 392 | /** A charge picked up while another was carried: the choice to make. */ |
| 393 | | { type: 'chargeOffered'; kind: ChargeId; held: ChargeId } | |
| 394 | /** A charge sold for coins: the new one, or the old one swapped out. */ | |
| 395 | | { type: 'chargeSold'; kind: ChargeId; coins: number } | |
fcf44f32 2026-10-25 Pavel Safronov | 396 | | { type: 'flameBlocked'; pos: Vec; hitsLeft: number } |
d3e4ed26 2026-10-25 Pavel Safronov | 397 | /** Freeze used: every enemy stops for FREEZE_TURNS turns. */ |
| 398 | | { type: 'frozen'; turns: number } | |
82adee18 2026-10-20 Pavel Safronov | 399 | | { type: 'decoyPlaced'; pos: Vec } |
d3e4ed26 2026-10-25 Pavel Safronov | 400 | /** A Decoy gone: burned by flame, or reached by an enemy. */ |
82adee18 2026-10-20 Pavel Safronov | 401 | | { type: 'decoyGone'; pos: Vec; burned: boolean } |
27f24fed 2026-10-20 Pavel Safronov | 402 | | { type: 'powerUpRevealed'; powerUp: PowerUp } |
| 403 | | { type: 'powerUpBurned'; powerUp: PowerUp } | |
e3f29330 2026-10-24 Pavel Safronov | 404 | | { type: 'powerUpTaken'; powerUp: PowerUp; offer: OfferKind[] } |
8edb5cb9 2026-11-01 Pavel Safronov | 405 | /** A second gem taken on the turn: it leaves the board now, its offer comes later (`offerAgain`). */ |
| 406 | | { type: 'gemQueued'; powerUp: PowerUp } | |
| 407 | /** A gem taken earlier this turn now makes its offer (the first one is answered). */ | |
| 408 | | { type: 'offerAgain'; powerUp: PowerUp; offer: OfferKind[] } | |
e3f29330 2026-10-24 Pavel Safronov | 409 | | { type: 'powerUpChosen'; kind: OfferKind } |
e78b7781 2026-10-25 Pavel Safronov | 410 | | { type: 'shopOpened' } |
| 411 | | { type: 'bought'; kind: OfferKind; price: number } | |
| 412 | | { type: 'rerolled'; price: number } | |
27f24fed 2026-10-20 Pavel Safronov | 413 | | { type: 'detonated'; pos: Vec } |
| 414 | | { type: 'runWon'; turns: number; deaths: number } | |
beb9f2b7 2026-10-21 Pavel Safronov | 415 | | { type: 'cratesCrumbling'; count: number } |
| 416 | | { type: 'crateCrumbled'; pos: Vec } | |
5f066960 2026-10-26 Pavel Safronov | 417 | /** Flame that spread without a blast: over an oil spill, or through bushes (PLAN.md §19.3). */ |
| 418 | | { type: 'fireSpread'; tiles: Vec[] } | |
| 419 | /** Flame reached an oil barrel: it bursts, spilling oil, and goes off next turn. */ | |
| 420 | | { type: 'barrelLit'; pos: Vec } | |
| 421 | /** Acid (PLAN.md §19.4): a pillar cracked; a cracked one broken, and the ceiling round it to fall. */ | |
| 422 | | { type: 'pillarCracked'; pos: Vec } | |
| 423 | | { type: 'pillarBroke'; pos: Vec; tiles: Vec[]; turns: number } | |
| 424 | | { type: 'ceilingFell'; tiles: Vec[] } |