fuzzyworld

explodorama / files / decisions.md

decisions.md

Blame: the commit that last changed each line · file · history

5551eb37 2026-10-20 Pavel Safronov1# Decisions
2
3Each design or technical decision, why it was made, and when. Newest last. The full design is
4in [PLAN.md](PLAN.md); this file records the reasoning so it isn't lost.
5
6## Design (agreed in planning, 2026-10-19)
7
81. **Enemies move before bombs explode.** Enemies announce their next move (intent arrows), so
9 the player can plan traps. Luck matters less than planning.
102. **Run is an ability, not a charge** — gained like the NES roller skates.
3321c921 2026-10-27 Pavel Safronov113. **3 lives**, plus a rogue-like layer of coins and upgrades.
5551eb37 2026-10-20 Pavel Safronov124. **No undo.**
135. **All actions in PLAN.md §3** are in scope.
146. **Name: Explodorama.** "Bomberman" is a Konami trademark and must never appear in the game.
157. **Shop purchases last for the current run only.**
168. **Permanent upgrades come from a Workshop between runs** (Everspace-style tracks, tier n costs
d603166d 2026-10-25 Pavel Safronov17 5 × n coins). Later milestone. *Dropped 2026-10-24 (see that day's entry).*
189. *(Dropped 2026-10-24, see that day's entry.)* **On death the player chooses Keep or Fresh**, balanced so each is picked about half the
5551eb37 2026-10-20 Pavel Safronov19 time. See PLAN.md §9.
2010. **Curses only appear after the first shop.**
2111. **Reapers and the turn limit come later.**
2212. **50 levels and an ending**, with a victory certificate and QR code.
2313. **Icons: Icongeek26 "Outline" from Flaticon**, with attribution, until there is final art.
644cfd0b 2026-10-20 Pavel Safronov24 LittleJS ships no icons. *Replaced 2026-10-20 — see "Art: Kenney's New Platformer Pack" below.*
5551eb37 2026-10-20 Pavel Safronov25
26## Technical
27
28### 2026-10-19 — LittleJS pinned to 1.25.0
29
30Exact version, the newest release that was at least two weeks old at install time (1.27.2 was
314 days old). Upgrade deliberately, not by range.
32
33### 2026-10-19 — Canvas 2D renderer, not WebGL
34
6e94f995 2026-10-24 Pavel Safronov35*Superseded 2026-10-24: WebGL is back (see "WebGL again; Developer mode's gem is a rainbow gem").*
36
5551eb37 2026-10-20 Pavel Safronov37`setGLEnable(false)`. Headless Chromium's full-viewport screenshots of LittleJS's WebGL canvas
38showed a black strip below the HUD that is not in the drawing (a clipped screenshot of the same
39page didn't show it, and Canvas 2D doesn't show it at all). Screenshots are how changes are
40reviewed before committing, so they must be trustworthy. The board is a few hundred rectangles;
41Canvas 2D is plenty. Revisit only if drawing becomes slow.
42
43### 2026-10-19 — Taps handled with pointer events on the canvas, not LittleJS's mouse input
44
45Press shows the commit preview on the tile under the finger, sliding changes it, release
46commits. That needs press, move and release positions, which pointer events give directly.
47LittleJS still handles nothing on the HTML buttons: it skips `preventDefault` for buttons.
48
49### 2026-10-19 — Bombs block flame by exploding; flame stops on a bomb
50
51A blast reaching another bomb sets it off and stops there; the second bomb's own blast covers
52the tiles beyond. Soft blocks hit during a chain are only removed once the whole chain is done,
53so every flame that reaches a block that turn stops at it.
54
55### 2026-10-19 — PWA deferred to M7
56
57`vite-plugin-pwa` needs icons, which come with the icon set in M7. The CI checks for a manifest
58and service worker will be added then.
644cfd0b 2026-10-20 Pavel Safronov59
60### 2026-10-20 — Art: Kenney's New Platformer Pack, replacing the Flaticon plan
61
62The user supplied Kenney's New Platformer Pack 1.1 and asked for the game to be re-styled with it:
63`character_green` as the player, the pack's enemies for enemies, its blocks for the board, gems as
64power-ups, and its coins, doors, bomb and HUD icons. This replaces design decision 13 (Icongeek26
65outline icons over a board of plain drawn shapes). The pack is CC0, so attribution is optional;
66the README credits Kenney anyway.
67
68- **Separate textures, no atlas.** Every sprite is its own LittleJS texture, listed in
69 `src/render/sprites.ts`. The Canvas 2D renderer doesn't batch, so an atlas would add a build
70 step and save nothing. Revisit only if the WebGL renderer comes back.
71- **Copied, not referenced.** Only the sprites in use or assigned to a planned feature are copied
72 into `assets/kenney/`, unchanged and under their pack names, so the build doesn't depend on a
73 folder outside the repo and every file can be traced back to the pack.
74- **Not inlined.** `assetsInlineLimit: 0`: Vite would otherwise turn every sprite under 4 KB into
75 a data URL inside the script, including the art for enemies and pickups that aren't drawn yet.
76- **Not pixelated.** `setTilesPixelated(false)`: the sprites are smooth art drawn at about
77 100–110 device pixels from 128, which looks jagged without smoothing.
78- **Pale floor, not grass.** The player is mint green; a green floor hid them.
c37a2b0b 2026-10-20 Pavel Safronov79
80### 2026-10-20 — Enemies, exit and charges: rules settled while building M4/M5
81
82- **Bombs can't be placed on an enemy's tile** (Bomb and Throw). Otherwise a bomb could sit
83 under an enemy, which the turn rules don't cover.
84- **Enemies don't block each other** and may share a tile. Simpler, and the NES did the same.
85- **Shield absorbs every hit for the rest of the turn it breaks in**, not just the first one, so
86 an enemy step followed by a flame in the same turn doesn't kill through a shield.
87- **Freeze cancels the move already announced**, in the same turn, rather than the one after.
88 The arrows the player sees are what Freeze stops, which is easier to read.
89- **Kicked bombs stop at enemies** as well as at walls, crates and bombs.
90- **Exit-spawned enemies drop nothing** (3 of the level's toughest type), so flaming the exit is
91 only a punishment, never a coin farm.
92- **Par = 50 + 10 per enemy, bonus 5 coins.** A first guess, to tune by playtesting.
93- **`?charges=N` is part of the run record** (`startCharges`), since it changes the starting
82adee18 2026-10-20 Pavel Safronov94 state; replays stay exact. `SIM_VERSION` is now 2 (3 after the lingering-flame change below).
c37a2b0b 2026-10-20 Pavel Safronov95- **Death banner says "Ouch!"**, not "Boom.", since enemies can kill too now.
96
82adee18 2026-10-20 Pavel Safronov97### 2026-10-20 — Touch shortcuts: tap self to wait, hold to bomb, tap far to auto-move
98
99Asked for by the user after playing. All three live in the UI layer (`main.ts`); the sim only
100ever sees ordinary Wait, Bomb and Move actions, so the run record and replays are unchanged.
101
102- **Auto-move is one step per press, not a walk that plays out by itself.** The game is
103 turn-based and the world moves between steps, so the player keeps the chance to stop. The
104 path ignores enemies (they move), but Go never steps onto one.
105- **Long press is 450 ms**, a common long-press length that doesn't feel slow. It only arms a
106 bomb on a tile next to the player with nothing picked, so it can't clash with the two-tap
107 targeting or with the press-and-slide preview.
108
109### 2026-10-20 — The other seven action charges
110
111- **Fuse ± is one charge with two buttons** (Fuse +1, Fuse −1) rather than one button that asks
112 which way afterwards: the action log stays a plain `{ id, target }`, and the choice is
113 visible up front. Timers go from 1 to 9.
114- **Pierce flame still stops at bombs** (setting them off), like any flame; it only ignores
115 crates.
116- **Power bomb fire = the level's width**, so it always reaches the walls; crates stop it.
117- **Line places bombs one per tile from the tile picked**, until capacity or a blocked tile
118 ends the row. With the starting capacity of 1 it is just a bomb; it gets better with Bomb+.
119- **Decoy blocks nothing** and only Seekers chase (the other types don't target the player).
120 It lasts 3 enemy plannings. One at a time.
121- **Teleport is rare** by drop weight (½ of each other charge), as the plan asked.
122- Keys: `=` / `−` Fuse, G Diagonal (then Q E Z C), P Teleport, I Pierce, O Power, L Line,
123 Y Decoy.
124
125### 2026-10-20 — Tapping the auto-move flag steps toward it
126
127The user asked for the flag itself to act like Go, so the thumb can stay on the board. That
128took over "tap the flag again to clear it", so clearing moved to tapping a wall or crate (a tap
129that otherwise does nothing), besides Esc and the existing automatic cases.
130
131Testing the flag tap in a browser walked the player straight into a Blob's announced step. So
132auto-move now refuses any step it knows is deadly — onto an enemy, onto a tile an enemy's arrow
133points at, or into flame this turn — and shows that next step in red. Manual moves are not
134restricted: the player can still choose a risky step on purpose.
135
136### 2026-10-20 — Lingering flame, and enemies planning 3 moves ahead
137
138The user found kills too hard: flame lasted one turn, and an enemy had to be on the exact tile at
139that moment. The cause: bombs take 3 turns, but enemies only showed 1 move, and Blobs keep
140their direction only 75% of the time, so a 3-turn guess was right about 42% of the time. Agreed
141fix, two parts:
142
143- **Flame lingers one extra turn** (option 1 of the ones offered). It is the turn-based version
144 of the NES flames staying on screen briefly, which is where most early kills come from there.
145 Lingering flame kills creatures (enemies, player, decoy) but not crates, drops, the exit or
146 bombs, so the drops a kill leaves survive. The player can't enter it, since walking into fire
147 you can see would only ever be a mistake. Changes the PLAN.md §1 rule "flame is instantaneous".
148- **Enemies commit to 3 moves and show them** (instead of option 3, making Blobs steadier): the
149 user asked for the next 2–3 moves to be shown, stacked vertically, future ones fading. First
150 drawn above the enemy's head on a white backing; the user found that blocky and spilling into
151 the tile above, so they are now see-through arrows down the right side of the enemy's own
152 tile, with the enemy drawn a little smaller and to the left to make room (the user briefly
153 tried full-size enemies under the arrows, then chose the smaller enemy).
154 Each arrow is one polygon, so it has no darker band where shaft and head would overlap. The moves are a real plan, not a forecast: the enemy makes exactly those moves unless
155 the board blocks one, so the player can plan a 3-turn bomb against them. Moves a newly placed
156 bomb blocks are dropped and re-planned in phase 5; the blocked-this-turn rule for the next move
157 is unchanged. Layout chosen by the user over a chain of arrows along the path.
158- Auto-move's safety check uses each enemy's next planned move.
159- Starting fire stays at 1, so Fire+ pickups (M6) still have room to matter.
160
27f24fed 2026-10-20 Pavel Safronov161### 2026-10-20 — M6 part 1: power-ups, the ending, mid-run save
162
163- **Interim death rule until Keep / Fresh:** a death keeps stats, Skates, coins and charges and
ebcd0a8f 2026-11-02 Pavel Safronov164 loses Crate Pass, Bomb Pass, Flame Pass, the Detonator and a Star — the NES rule plus "Keep".
27f24fed 2026-10-20 Pavel Safronov165 Keep / Fresh (PLAN.md §9) will replace the stats/Skates part.
166- **The power-up is under any crate but the exit's**, including near the start, so an early
167 Bomb+ or Fire+ is possible. **Skates are on level 1 or 2**, chosen by the seed, and not in the
168 random draw anywhere else (so there is never a useless second pair).
169- **A flamed power-up spawns the same punishment as a flamed exit** (3 of the level's toughest
170 type, no drops). Lingering flame doesn't destroy it, like drops.
171- **Run collects what is on the tile it passes through** (coins, drops, the power-up), since
172 running past a coin without taking it would feel like a bug. Running into an enemy there kills.
173- **Star and Flame Pass:** Star blocks every hit for 8 turns (counting the one it is taken in);
174 Flame Pass blocks flame only. Neither kills enemies on contact.
175- **Save = the run record** in `localStorage`, replayed on load. It is a few KB even for a long
176 run, and replay is exact because the sim is deterministic. Test URLs neither load nor save.
177- **Ending:** clearing level 50 sets status `won`; the banner shows total turns, lives lost and
178 the seed. The victory certificate is M8.
179- `SIM_VERSION` is now 4: older saved runs are not resumed (they would replay differently).
180
a4fab0fb 2026-10-21 Pavel Safronov181### 2026-10-21 — Playtest feedback: flag, new run, info dialogs, Kenney UI packs
182
183- **The auto-move flag clears when the player moves off its path.** Any player move (move, run,
184 diagonal, teleport) that doesn't land on a tile of the current path means "changed my mind".
185 Bombs, waits and charges don't clear it.
186- **New run lives in a menu, behind a confirmation**, since a run can't be got back. The menu
187 button is in the top bar; Esc opens it when there is nothing to cancel.
188- **Tapping anything that isn't an action explains it.** One native `<dialog>` (focus trap, Esc
189 to close) for info, the menu and confirmations. Enemies, bombs and burning tiles always explain
190 rather than act, even next to the player: a tap on an enemy used to walk into it, which was
191 never what anyone meant. Walls and crates keep clearing the flag when there is one (that tap
192 did nothing else), otherwise explain. Things you can walk to offer "Walk there".
193- **UI art from Kenney's UI Pack 2.0 and UI Pack: RPG Expansion** (the user added both, CC0),
194 as CSS 9-slice `border-image`s so the panels and buttons stretch to any text. Credited in the
195 README and the menu like the platformer pack. The action bar keeps its CSS-drawn buttons for
196 now; they already follow the same look.
197
beb9f2b7 2026-10-21 Pavel Safronov198### 2026-10-21 — Board sizes, Developer / God / crumbling-crates modes, player animation
199
200Decisions made with the user:
201
202- **Small board = 9 × 11, 20 levels**, fewer enemies (2 → 5) and crates (25%). 9 × 11 keeps the
203 NES pillar pattern and fits a 390-wide phone at about 43 px a tile.
204- **Developer and God are separate checkboxes** (the user split them mid-way): Developer gives
205 everything (every ability and pass, unlimited charges, 5 bombs, blast 5) but stays killable,
206 "mainly to test out all the possible actions and power-ups"; God makes the player unkillable.
207 They combine.
208- **Crumbling crates: each crate 1–3 turns**, seeded, once the last enemy dies. Not flame, so
209 nothing is punished.
210- **Options are chosen in a New-run dialog** and remembered for next time; not asked on the
211 very first launch.
212
213Made by me:
214
215- **Options are part of the run record and state** (`SIM_VERSION` 5), so replays and saves stay
216 exact; older saves aren't resumed.
217- **Developer charges are 99 that are never spent**, shown as ∞, rather than a special case in
218 every check.
219- **The new-run dialog replaces the separate "Start a new run?" confirmation**: it carries the
220 warning itself when a run is in progress.
221- **Player animation is drawn, not simulated:** the sim still moves the player instantly; the
222 renderer slides the sprite over 0.16 s on walking frames, crouches for a bomb, jumps on a
223 cleared level, bobs and glances at the camera when idle, shakes in the hit pose. Teleports
224 just appear. Enemies got the same hop afterwards (user request): each `enemyMoved` event
225 slides that enemy over the same 0.16 s.
226
227### 2026-10-21 — Playtest feedback: credit, wall info, drops, enemy hops
228
229- **The Kenney credit is only in the menu** (the user's "settings / about" screen), no longer on
230 the death / game-over banner.
231- **Wall info shows the exact tile**: the stone outer wall, or the pillar in this level's
232 colour, titled "Outer wall" or "Pillar".
233- **Drops never share a tile.** A drop whose tile is taken (a drop, a bomb, the uncovered
234 power-up) rolls to the nearest free floor tile within 2 steps, searching over floor only (not
235 through walls or crates), else stays. It changes where drops land, so `SIM_VERSION` is 6 (a
236 run saved while testing version 5 would not replay the same).
237- **Enemies hop between tiles** like the player.
238- **Enemies sharing a tile is unchanged** for now: the NES original let enemies pass through
239 each other (from memory, not verified). Listed as open question 10 in PLAN.md §14.
240- **Exclusive power-ups** are written up as an idea with its conflicts (PLAN.md §5b), not built.
241
d05d464d 2026-10-22 Pavel Safronov242### 2026-10-22 — The outer wall gets borders
243
244The user found the walls too uniform and asked for the pack's bordered wall tiles.
245
246- **The outer wall is a stone frame**: the edge, corner and strip pieces of the pack's
247 `terrain_stone` set, so a dark outline runs along both sides of the ring (towards the board and
248 towards the screen edge) with rounded outer corners. Top and bottom strips show the pack's dark
249 ledge on their upper side, as in the pack. `terrain_stone_block_center` is no longer used and
250 was removed from `assets/kenney/`.
251- **Inner corners are drawn squares**: the pack has no inner-corner piece, so where two sides of
252 the frame meet the outline would have a gap; a square in the outline colour fills it.
253- **Pillars are unchanged**: the coloured blocks already have their own outline.
254- **Floors are drawn before walls** (render only): each tile is drawn a hair oversized to hide
255 seams, and a floor drawn after its neighbouring wall painted over that wall's edge, leaving
256 thin light lines through the stone.
ea380ee8 2026-10-22 Pavel Safronov257
258### 2026-10-22 — "What is that?" moves behind a ? button
259
260The user asked for info to need a dedicated button first, so a plain tap on the board either does
261nothing or starts walking.
262
263- **A plain tap:** the player = Wait; the flag = one step; a tile next to the player = move;
264 any other tile the player can walk to (now including the exit, the power-up and drops) = set
265 the auto-move flag; a wall or crate = clear the flag if there is one (kept from 2026-10-21,
266 since that was how the flag gets cleared by touch); an enemy, a bomb or burning flame =
267 nothing.
268- **The ? button sits in the action bar, beside Wait**, not in the top bar next to the menu: the
269 top bar's second row on a 390 px phone had no room for a second round button, and the menu
270 wrapped onto a third row, shrinking the board. In the action bar it is picked like an action
271 (green while on, key **?**), which is what it is: pick, then tap a tile. Move / Bomb / Wait
272 give up about 16 px each for it.
273- **One tap, then off**, whatever the tap hit (a plain floor tile has nothing to explain, so it
274 just turns the mode off). Picking an action, the ? again, or Esc also turns it off; turning it
275 on drops any picked action. While it is on, a long press doesn't arm a bomb.
276- **Its icon is the Game Icons pack's `question`** (the user added the pack, CC0), not a "?" typed
277 in the Kenney Future font, which read poorly. Credited in the README and the menu.
278- **No key hints on phones** (user request): at 600 px wide or less, the action buttons drop
279 their WASD / B / Space / ? labels (the charge buttons already had), and Move / Bomb / Wait put
280 icon and name side by side. Wider screens keep the hints.
281- **Top-bar items keep their plain-tap info**: they are not on the board and do nothing else.
282
283### 2026-10-22 — Auto-move walks by itself once the enemies are gone, and runs with Skates
284
285User request, before committing the ? button.
286
287- **No enemies left: one tap is enough.** A set flag is walked to automatically, one step
288 `AUTO_STEP_SECONDS` (1 s) after the last action — so the first step comes at once if you
289 haven't just acted. Each step is still an ordinary turn (bombs tick, crates crumble) and is
290 recorded as usual. It pauses while an action is picked, "What is that?" is on or a dialog is
291 open. With enemies on the board nothing changes: Go / the flag, one press per step.
292- **An unsafe next step just waits**, as for Go: auto-move never takes it, and nothing changes
293 until you act (flame from last turn only goes out after your next action), so you Wait or
294 step aside and the walk carries on.
295- **With Skates, auto-move runs** whenever the next two tiles of the way go straight on and Run
296 is allowed there, if both tiles pass the same safety check as a single step. This applies to
297 every auto-move step: Go, tapping the flag, and the automatic walk.
298- **Checked by driving the game in a browser** (seed 1, enemies removed through a temporary hook
299 that was taken out again), since this lives in `main.ts`, outside the tested sim.
300
301### 2026-10-22 — Everything on the board is animated
302
303The user asked that nothing just appear, disappear or jump any more (enemies vanished, kicked
304bombs teleported), with flame fading out over about half a second.
305
306- **`render/effects.ts`** turns each turn's events, plus the state before and after, into short
307 animations; `board.ts` draws them. The sim is unchanged (no new events, `SIM_VERSION` stays).
308- **What animates, and how:**
309 - appearing (≈0.22 s, a pop with a slight overshoot): placed bombs (Line's one after another),
310 drops, enemies spawned from a burned exit or power-up, the decoy, a revealed exit or
311 power-up, a new shield ring;
312 - moving: a kicked bomb slides (0.05 s a tile), a thrown bomb flies in an arc (0.25 s), a
313 teleport shrinks away and grows back (0.15 s each);
314 - changing: a bomb swells as its number goes down or a Fuse charge changes it; the exit
315 swells as it opens; Freeze flashes the enemies icy blue (0.6 s);
316 - going away: an enemy flips over with its eyes shut, hops and falls away fading (0.5 s);
317 a crate swells, tips and fades (0.4 s), over whatever it uncovered; flame from last turn
318 fades as it goes out (0.5 s, at the start of that turn); a drop or power-up taken rises and
319 fades (0.4 s, starting as the player arrives); one burned, or the decoy, shrinks and spins
320 away; a broken shield ring widens and fades.
321 - the banner comes in after 0.35 s, so the death or level-cleared jump is seen first; dialogs
322 pop up over a fading backdrop. Both are off with "reduce motion".
323- **Follow-up (user request, same day): the top bar's numbers and the dialog closing.** A
324 top-bar item that changed (a number, a heart, a pass arriving) is replaced and bumps (scales
325 to 1.3 and back in 0.3 s); unchanged items are kept as they are, so a bump isn't cut short by
326 the next update. Dialogs close with a 0.15 s shrink-and-fade — buttons, the close button, the
52d68b59 2026-10-24 Pavel Safronov327 backdrop and Esc alike — and a button's action runs once it has closed. *Fixed 2026-10-22
328 (user report: hearts, flag and enemies pulsed every turn):* the kept items are re-attached on
329 every update, and one still carrying the `bump` class played it again each time, so anything
330 that had changed once kept pulsing. The class now comes off when the bump ends
331 (`animationend`). The turn counter still bumps every turn, because it changes every turn.
ea380ee8 2026-10-22 Pavel Safronov332- **No input lock** (PLAN.md planned `animate.ts` to own one). The game is turn-based and the
333 sim has already moved on, so a quick next tap simply starts the next turn's animations over
334 the end of the last ones; locking input would only slow play down.
335- **One death animation for every enemy** rather than the Blob's spare `_flat` sprite, since the
336 fly and saw have no flat frame.
337- **Screenshots wait 0.8 s** after their action (was 0.25 s), so they show the settled board
338 and the banner.
339- **Checked by capturing frame bursts in a browser** (seed 1, Developer mode, a temporary hook
340 to act directly and to put an enemy in a blast, removed afterwards): bomb pop, kick slide,
341 throw arc, shield ring, crate burst, enemy death, flame fading, teleport.
342
99869630 2026-10-23 Pavel Safronov343### 2026-10-22 — Fewer action buttons; Kick, Throw and Teleport become abilities
344
345The user found there were too many action buttons and the "?" odd among them. Asked first:
346charges or permanent, kick on a plain tap, how far to fold, where to explain.
347
348- **"?" moves to the top bar**, a round button beside the menu. The two are stacked in a column
349 at the right of the top bar on a phone (beside its two rows), side by side on a wide screen —
350 in a row with the items, the menu would have wrapped onto a third row.
351- **Icons only.** The names are in the explanation strip, the tooltip and the screen-reader
352 label. Key hints stay on wide screens.
353- **Upgrades widen Move and Bomb instead of adding buttons** (`sim/picks.ts`). Move: step, kick
354 (walking into a bomb), run (Skates), teleport, diagonal step (a charge). Bomb: place, throw.
355 The tile tapped decides. Preference order: a kick before walking onto the bomb with Bomb Pass
356 (the user's "walking into a bomb kicks it"; a bomb with no room to slide is still walked over),
357 a plain step before a run, anything free before spending a charge.
358- **Kick, Throw and Teleport are permanent for the run** (user's choice, over "spend a charge"
359 or "for the level"). Made by me: they are **power-ups now, hidden under crates like Skates**
360 (draw weights Kick 8, Throw 8, Teleport 3), not enemy drops — permanent things are power-ups
361 in this game, and as drops every run would have collected all three within a few levels.
362 **Kept on death**, like Skates ("permanent for the run"). They no longer drop from enemies;
363 the remaining 8 charges are equally likely (Teleport's half weight went with it).
364 `SIM_VERSION` 7: saves from before don't resume.
365- **Kick on a plain tap, always** (user's choice): tapping a bomb next to you, or an arrow key
366 into it, kicks it once you have Kick.
367- **Folded too (user's choice): Diagonal, Pierce, Power, Line.** They stay one-use charges (the
368 user chose "permanent" only for Kick, Throw and Teleport). Diagonal steps are Move targets
369 while you hold one; Pierce, Power and Line are chips in Bomb's strip, each with its count.
370 **Fuse +1 and −1 became one Fuse button** with +1 / −1 chips. Shield, Freeze, Decoy and
371 Detonate keep icon buttons, in a second row of up to five.
372- **The explanation strip** (user's choice: above the action bar) sits over the board's bottom
52d68b59 2026-10-24 Pavel Safronov373 edge, so the board doesn't move when it appears. *Superseded 2026-10-23: it must not cover the
374 board; see "The strip gets its own band".* It lists only the upgrades held, so picking
99869630 2026-10-23 Pavel Safronov375 an action teaches what it can do now. Shield and Freeze, which happen at once, show theirs
376 for 2.5 s after use; Wait shows none (it is pressed every few turns) — "?" then Wait explains
377 it, as "?" then any button does.
378- **Not yet:** small upgrade badges on Move and Bomb (the user: "that can wait").
379- **Checked in a browser** (Developer mode, a temporary read-only hook, removed): Bomb's strip;
380 a tap 2 away throws; Move teleports 3 tiles; the Pierce chip places a Pierce bomb; the Fuse
381 −1 chip; "?" then Wait explains Wait; Shield's strip after use; an arrow into a bomb kicks it
382 (found walking over it with Bomb Pass, hence the kick-first order).
383
384### 2026-10-22 — Playtest: Detonator by tapping, Use to confirm, no Fuse charge, a real Freeze
385
386- **On a phone the menu sits above "?"** (`column-reverse`); side by side on a wide screen, as
387 the user liked it.
388- **The Detonator works by tapping a bomb** (user request): while you have it, every bomb pulses
389 with a red glow, and a tap on one sets it off this turn. Its button is gone; Bomb's group now
390 includes Detonate too. **A plain tap on a bomb next to you detonates rather than kicks** (user's
391 choice): you still kick by walking into it — an arrow key, or Move picked and then the bomb.
392- **Nothing but Wait goes off from its button** (user request: Shield went off at once). Shield and
393 Freeze open the strip with a **Use** chip, like Bomb's choices; Enter confirms too; pressing
394 the button again cancels. Wait stays one tap (user's choice: it spends nothing and is pressed
395 every few turns). Decoy already wanted a tile. The 2.5 s after-use explanation went with it.
396- **The Fuse ±1 charge is removed** (user's choice). Asked whether it was in the original: no —
397 the 1985 game had fixed fuses and no fuse power-up at all; both Fuse +1 (the power-up, kept)
398 and Fuse ±1 (the charge) were this game's additions. 7 charges remain, equally likely.
399- **Fire is walkable while it can't hurt you** (user report: with Flame Pass, tapping the burning
400 tiles round them did nothing, while the Move button worked). The tap rule refused burning tiles
401 outright; now it, `playerCanEnter` and auto-move's safety check all go by `flameSafe`: Flame
402 Pass, a Star or God mode. A Star now lets you step into fire too (it couldn't before).
403- **Freeze lasts this turn and the next 3** (user request: one skipped move just wasted the
404 turn). `frozen` is now a count. Auto-move ignores frozen enemies' arrows. Drawn with LittleJS's
405 colour tinting (the renderer is Canvas 2D, so no shaders: multiply towards ice blue plus an
406 added cold glow), a see-through ice block with a rim and a glint, and twinkling sparkles; it
407 thaws a step each turn over 0.5 s, with a little crack-shake, and the arrows go faint and icy
408 until it is over.
409- `SIM_VERSION` stays 7: the previous change hasn't been committed or played anywhere yet.
410- **Checked in a browser** (Developer mode, temporary hook, removed): Shield waits for Use; Freeze
411 holds the nearest enemy in place for 4 turns (frozen 3, 2, 1, 0) and then it moves, with
412 frames of the ice thawing; a bomb pulses and a tap detonates it; with Flame Pass a tap on a
413 burning tile next to you walks into it.
414
415### 2026-10-22 — A dialog when a power-up is picked up
416
52d68b59 2026-10-24 Pavel Safronov417*Superseded 2026-10-23 by the gem's choice dialog (see "Gems: choose 1 of 5 power-ups").*
418
99869630 2026-10-23 Pavel Safronov419User request. Taking a power-up opens the info dialog for it ("You picked it up!" and what it
420does, from the same texts as "What is that?"). It opens 0.45 s after the step, so the walk and
421the pickup's rise-away animation are seen first; auto-move pauses while it is open, as for any
422dialog. Every power-up gets one, Heart and Coin bag included. Charge and coin drops don't: they
423are frequent, and their buttons and the top bar show them. Checked in a browser (a Kick
424power-up placed next to the player through a temporary hook, removed): closed at 0.2 s, open
0b61e800 2026-10-23 Pavel Safronov425with "Kick" by 0.9 s, the ability gained. *Partly superseded the same day for charges: see the
426next entry.*
427
428### 2026-10-22 — A dialog for the first charge of each kind in a run
429
430User request, after picking up a Shield on level 1 and getting no explanation: a new button
431appeared with nothing to say what it was. Now the **first** pickup of each kind of action charge
432in a run opens a dialog ("Shield charge", "You picked it up!", what it does, and that each charge
433is used once and takes the turn), at the same 0.45 s delay as a power-up's. Later pickups of
434that kind show nothing, which keeps the reason above (charges are frequent). Coins still get no
435dialog.
436
437"In this run" was chosen over "ever, in this browser": a new run shows them again. The kinds
438seen are kept in `main.ts`, not in the sim or the save: after a reload they are rebuilt from the
439saved run's replay (`replay` now takes an `onEvent` callback), so the save format and
440`SIM_VERSION` are unchanged. A death doesn't reset them. Checked in a browser (drops placed next
441to the player through a temporary hook, removed): first Shield opens the dialog by 0.9 s, a
442second Shield doesn't, then a first Freeze does.
99869630 2026-10-23 Pavel Safronov443
444### 2026-10-22 — The almanac
445
446User request: to see every power-up and enemy (some sprites are to be replaced). **Menu →
447Almanac**: one dialog with three tabs — Power-ups (all 14, in reading order: stats, abilities,
448passes, the rest), Charges (all 7) and Enemies (the 3 built, each with the level it first
449appears on). Each entry is the sprite plus the same words as "What is that?", so there is one
450text per thing. Tabs swap the list without closing the dialog. Sprite file names aren't shown in
451the game; for picking replacements, labelled sheets of every sprite (name and file) were made
452outside the repo. Screenshots: `almanac`, `almanac-enemies`.
453
454### 2026-10-22 — Almanac scrolling; badged power-up icons
455
456- **The almanac didn't scroll with a wheel or trackpad**: LittleJS cancels every wheel event on
457 the page. The dialog now stops wheel events from reaching it (`dialog.ts`), so long dialogs
458 scroll. **Its scroll bar is hidden** (the user saw scroll bars on desktop); the cut-off last
459 entry shows there is more. Measured on a 1280 × 720 window: a wheel turn scrolled 300 px,
460 where it scrolled 0 before.
461- **Almanac screenshots show whole lists**: one per tab (`almanac`, `almanac-charges`,
462 `almanac-enemies`), taken in a 390 × 2100 window.
463- **Power-ups that improve something you have show that thing plus a modifier badge** (user
464 request, with the icons named by the user): Bomb +1 bomb + plus, Fire +1 bomb + expanding
465 arrows, Fuse +1 bomb + a three-quarter timer (from Board Game Icons, which the user added),
466 Skates the walking astronaut + fast-forward, Kick bomb + arrow right, Throw bomb + arrow
467 up-right. Made by me: the badge's look (green disc, dark rim, white symbol, 56 px of 128, top
468 right), and building the icons ahead of time with a script (`tools/compose-icons.sh`) rather
469 than drawing two sprites wherever an icon appears — the board, the top bar, dialogs and the
470 almanac all take one picture. The replaced sprites (four gems, `spring_out`, `sign_right`) are
471 no longer used and were removed from `assets/kenney/`.
472- **Watch for:** a bomb-shaped power-up lying on the floor could be mistaken for a real bomb, and
473 the Skates icon for the player. Both sit on the yellow power-up glow, carry the badge, bob, and
474 the bombs have no number; not changed until the user has seen them in play.
475- **Detonator's text** was stale ("pick one of your bombs"): now "your bombs pulse: tap one".
476
477### 2026-10-22 — Move and Wait get their own icons
478
479User request: the astronaut on the Move button looked like the player. Move is now Board Game
480Icons' `character` (a game-piece figure) and Wait its `character_remove` (the figure with an ✕:
481stand still). Those icons are white, so `tools/compose-icons.sh` makes ink-coloured copies
482(`icon_move`, `icon_wait`), and CSS turns them white again on a picked (green) button and in the
483dark explanation strip. Skates was defined as "the Move icon plus fast-forward", so its badged
484icon now uses the new figure too (made by me, for consistency).
485
486### 2026-10-22 — Badged icons for the rest of the abilities and passes
487
488User request, symbols named by the user: Teleport = the Move figure + an outbox (Game Icons
ebcd0a8f 2026-11-02 Pavel Safronov489`export`), Detonator = bomb + `exclamation`, Crate Pass = crate (`block_planks`) + `cross`, Bomb
99869630 2026-10-23 Pavel Safronov490Pass = bomb + shield (Board Game Icons `shield`), Flame Pass = fireball + shield. Built by
491`tools/compose-icons.sh` like the first six. `bush`, `ladder_top`, `torch_off` and `window` are
492no longer used and were removed; `lever` stays (the "Run settings" info icon). Star, Heart and
493Coin bag keep their plain sprites.
494
495### 2026-10-22 — The fuse shows the timer, not a torch
496
497User request: the lit torch beside the fuse number in the top bar (and on its info dialog) was
498unclear. It is now the three-quarter timer of the Fuse +1 badge (Board Game Icons
499`timer_CW_75`), made ink-coloured by `tools/compose-icons.sh` (`icon_fuse`) and turned white on
500the dark top bar. `torch_on_a` is no longer used and was removed.
501Also: the top bar's bombs, blast and fuse info still named the old gems ("Bomb +1 (blue gem)
502adds one"); they now say "The Bomb +1 power-up adds one" and so on.
503
504### 2026-10-22 — Wait is an hourglass
505
506User request: the Wait button shows Board Game Icons' `hourglass` instead of the figure with an
507✕. Made ink-coloured like the others (`icon_wait`), white on a dark or green background.
508`character_remove` is no longer used and was removed from `assets/kenney-board-icons/`.
509
510### 2026-10-22 — "Skates" is called "Run"
511
512User request. The power-up that lets Move go 2 tiles straight is now **Run** everywhere: on
513screen, in the almanac, the docs, and the code (the power-up's id `skates` → `run`, sprite
514`powerup_run`, `skatesLevel` → `runLevel`). The seed hash that picks level 1 or 2 for it keeps
515its old key `'skates'`, so every seed still has it on the same level. Earlier entries here keep
516the old name, as they were written.
517
52d68b59 2026-10-24 Pavel Safronov518### 2026-10-23 — Auto-move never crosses the open exit
519
520User report: the dotted way to a power-up went over the open exit, so walking it ended the
521level without the power-up. `playerPath` now treats the open exit as blocked unless it is the
522destination. If the only way somewhere is over it, there is no way (the flag can't be set): ending
523the level by surprise is worse. A locked exit is still crossed, since stepping on it does
524nothing; the way is worked out again every turn, so it goes round the exit as soon as it opens.
525
526### 2026-10-23 — The strip gets its own band
527
528*Superseded the same day: the strip is gone (see "No explanation strip during play").*
529
530User report: with the player on the bottom row, the explanation strip covered that row, so it
531couldn't be tapped. The user's rule: **the strip must not cover any of the board.** Of the two
532ways to do that, the user chose a band that is always kept free (over a board that shrinks only
533while the strip is open). The strip is now part of the action bar, at its top; the board is
534fitted above the whole bar, as before.
535
536How tall: the tallest strip that could open now (every action that can be picked, plus the one
537open), measured off-screen (`reserve` in `ui/controls.ts`). Measured on a phone: 103 px with
538nothing collected, 181 px holding Pierce / Power / Line (Bomb's choices), 264 px in Developer
539mode; on a desktop about 105 px. Reserving the Developer-mode maximum always would have cost a
540phone about a third of the board, so the band follows what the player holds: the board resizes
541when an upgrade or a charge is gained or spent, never when an action is picked. It is measured
542again on a window resize. The fade-in now plays only when the strip's content changes.
543
544### 2026-10-23 — Gems: choose 1 of 5 power-ups
545
546User request: instead of a fixed power-up, every level's pickup is a gem of random colour, and
547taking it shows a choice of power-ups. User's answers on the open points:
548
549- **5 offered** (`OFFER_SIZE`), all different, **drawn with today's rarities**
550 (`POWER_UP_WEIGHTS`, now in `sim/turn.ts`), listed in the almanac's order. Abilities and passes
551 already held are left out (they would change nothing); if fewer than 5 remain, fewer are offered.
552- **No Run guarantee** any more: Run is in the draw with weight 10 (it had none, being
553 guaranteed on level 1 or 2). `runLevel` is gone.
554- **Choosing takes no turn and can't be put off**: the dialog has no close button, and Esc and
555 the backdrop don't close it (`required` in `ui/dialog.ts`). The world doesn't move meanwhile.
556
557How it works: taking the gem sets `state.offer` (the event `powerUpTaken` carries it); while it
558is set, every action is unavailable except `{ id: 'choose', kind }`, which applies the power-up
559and clears the offer without a turn passing. The choice is an action in the run record, so it
560replays; a reload with a choice open shows it again. The offer comes from the seed and level, so
561a retry after a death offers the same, and dying can't reroll it (unless the death cost a pass,
562which may then be offered). A death on the pickup turn drops the offer, and the retry has the gem
563back; reaching the exit on the same step (a Run over the gem) still needs the choice before the
564next level. A Star chosen this way lasts its full 8 turns (taken mid-turn it had lost one).
565The colour is only how it looks: `gem_blue` / `gem_green` / `gem_red` / `gem_yellow` from the New
566Platformer Pack (the gems were power-up sprites until 2026-10-22). The old "You picked it up!"
567dialog is gone; the choice already says what each does. `SIM_VERSION` 8 (9 since the shelter rule below): saved runs from before
568are dropped (the record's meaning changed). Level layouts are unchanged: the colour uses the
569level stream's last draw, where the power-up's kind used to be.
570
571Checked in a browser (a gem placed next to the player through a temporary hook, removed): the
572dialog opens with 5 choices and no close button, stays open on Esc and on a backdrop tap, Wait
573is refused meanwhile, choosing Fire +1 raised the blast from 1 to 2 with the turn unchanged, and
574play went on. All 5 choices fit without scrolling on a 1280 × 720 desktop and a 390 × 844 phone.
575
576### 2026-10-23 — A short exit doorway
577
578User request: the exit (the pack's two-tile door squeezed into one tile) was too tall. It is now
579only the door's arched top piece (`door_closed_top` / `door_open_top`), at full tile size: a
580short doorway, orange while locked, dark once open. The bottom halves are no longer used and
581were removed from `assets/kenney/`.
582
583### 2026-10-23 — No explanation strip during play
584
585User feedback on the band above: "the text box keeps resizing, that's definitely not going to
586work" (the band followed what the player held, so the board changed size as charges came and
587went). Since power-ups are now explained in the gem's choice and charges on their first pickup,
588the user chose to **show no explanation text during play** at all. "?" then a button still
589explains it (`actionInfo`).
590
591The strip also held two controls, which needed a new home (user's choices):
592
593- **Pierce, Power and Line get their own buttons** in the second row, like Shield, Freeze and
594 Decoy, shown while held, with a count: tap one, then a tile. Bomb no longer stands for them.
595- **Shield and Freeze are used with a second tap on their button** (or Enter); the first tap
596 only picks them (green), and anything else cancels. Still never a single accidental tap.
597
598The second row now holds up to six buttons, so its buttons are a sixth wide (they were a fifth):
599all six fit in one row even on a 320 px phone, so a third row never comes and goes. The row
600itself still appears with the first charge held and goes with the last one, as before.
601Checked in a browser with every charge held (390, 360 and 320 px wide): the bar didn't move
602while picking Move, Bomb, Pierce, Line and Decoy or after using charges; one tap on Shield only
603picked it and the second used it; Freeze with Enter; a Pierce bomb placed with its button and
604an arrow key. The charge texts no longer say "a choice under Bomb", and the first-pickup dialog
605and "?" say how each charge is used.
606
607### 2026-10-23 — Auto-move's way is blue tiles, with no flag
608
609User request: no flag on the destination; highlight the way's tiles blue. Every tile of the way,
610the destination included, gets the same blue as a picked action's targets (the dots and the
611flag sprite are gone). The next step still turns red when it isn't safe. Tapping the destination
612still takes a step, as tapping the flag did. The Go button kept its flag icon at first; see the
613next entry.
614
615### 2026-10-23 — Enemies let out by flame are sheltered from it
616
617User report: a bomb on the uncovered (locked) exit let out 3 enemies, and the remaining fire
618killed them at once. Why: they appear on the exit tile at the end of the turn, and the blast
619lingers one more turn (since 2026-10-20), so the next turn they stand or step inside it and die
620— the NES punishment almost never bit. The same held for a flamed gem.
621
622User's choice (of: shelter them from that flame; let them out a turn later; immune to all flame
623for a turn; put them outside the blast): **they can't be hurt by the flame that let them out,
624but a new explosion kills them as usual.** `Enemy.sheltered` is set when they appear; on the next
625turn only new flame counts against them, then it is cleared. Tests check both halves, and that
626the first one fails without the rule. `SIM_VERSION` 9.
627
628### 2026-10-23 — Developer mode starts each level next to the gem
629
630User request. In Developer mode the level's gem lies uncovered right of the start (a tile that is
631always open floor), every level and after every death, to try the choice without hunting for it.
632The crate that hid it stays a crate. Normal runs are unchanged.
633
634### 2026-10-23 — Screenshots regenerated
635
636User request, after today's changes. `power-up` / `run` became `gem` (seed 55's gem uncovered)
637and `gem-offer` (its choice); `charges` now shows the six charge buttons with Pierce picked (Bomb's
638choices in the strip are gone); comments in `tools/screenshots.mjs` updated for the blue way and
639the missing strip. All 26 were taken again with `npm run screenshots`.
640
641### 2026-10-23 — No glow behind the gem
642
643User request: the yellow circle drawn behind an uncovered gem is gone; the gem is drawn on its
644own, still bobbing gently. (The glow dates from when the pickup was a power-up icon that needed
645to stand out from drops; a gem is distinct enough.)
646
647### 2026-10-23 — Developer mode is only the gem
648
649User request: Developer mode no longer gives every ability and pass, 5 bombs, blast 5 and
650unlimited charges; it **only** puts the level's gem uncovered right of the start (see "Developer
651mode starts each level next to the gem"). Gone with it: `DEV_STATS`, `DEV_CHARGES`, charges never
652being spent, and the ∞ badges and "(unlimited)" labels. Its description in the New run dialog,
653the top bar's mode tag and the docs say so. The `move-picked` screenshot, which used Developer
654mode to show every Move upgrade, now shows a plain Move. Open question 5 of PLAN.md §5b (Developer
655mode bypassing slots) no longer applies. `SIM_VERSION` 10, since a run saved with the option on
656would replay differently.
657
658### 2026-10-23 — Go keeps the walk icon
659
660User request: while walking to a destination, the Move button (Go) no longer turns into a green
661flag; it keeps Move's walk icon. It still gets its green border, so it shows that a press walks
662a step, and its name for screen readers and the tooltip is still "Go". `flag_green_a` is now only
663the level's icon (top bar, level-cleared banner).
664
665### 2026-10-23 — Charges work on pickup, as temporary changes
666
667User request: charges are no longer actions but temporary changes, applied the moment they are
668picked up. The new set and icons are the user's; the open points were the user's choices too:
669
670| Charge | Effect | Icon |
671|---|---|---|
ebcd0a8f 2026-11-02 Pavel Safronov672| Pierce | next 3 bombs' flame goes through crates | Crate Pass icon |
52d68b59 2026-10-24 Pavel Safronov673| Power | next 3 bombs reach to the walls | Fire+ icon |
674| Nuke (new) | next 3 bombs also burn every tile within 2 steps (Manhattan), through anything but walls | bomb + Board Game Icons skull badge |
675| Diagonal | diagonal steps for the rest of this level and all of the next ("round" = level) | Move figure + diagonal-arrows badge |
676| Shield | the next hit is absorbed, then it's gone | Move figure in the empty ring `timer_0` |
677| Freeze | every enemy frozen for 3 turns, the pickup turn included | Game Icons `locked` |
678| Decoy | a copy of the player on the pickup tile (user's choice over "next to you") | `pawns` |
679
680Line is gone (not in the list). Choices: 3 bombs per Pierce / Power / Nuke pickup (over 1 or 5);
681Nuke reach 2 (over 3: with a fuse of 3, 2 is escapable on foot, 3 hardly without Run).
682
683How it works: `state.charges` is replaced by `state.boosts` (bombs left of each bomb boost, and
684the last level Diagonal lasts to); Shield, Freeze and Decoy act through the existing `shield`,
685`frozen` and `decoy`. `applyCharge` runs on pickup; picking up more adds to it (3 more bombs,
686one more level of Diagonal). Every bomb placed or thrown uses one of each bomb boost held, so
687they combine (`Bomb.pierce` / `power` / `nuke`, replacing `Bomb.special`). Boosts and an unbroken
688shield go on to the next level and through a death, as charges did. The shield, freeze, decoy,
689pierce, power, line actions and their buttons, keys (H, F, Y, I, O, L) and the Shield / Freeze
690double tap are gone; the bar is Move, Bomb and Wait; Diagonal stays an action of Move (G, then Q
691E Z C) while it lasts. The top bar shows each active charge with its count (bombs, levels or
692turns left; the Shield while it's up), and tapping one explains it. Shield, Freeze and Decoy's
693one-colour icons turn white there (MONO_ICONS); the Diagonal icon, like Run's, stays dark on the
694bar. `?charges=N` now starts as if N of each lasting charge had been picked up (not Freeze or
695Decoy, which act on the spot). The new icons are built by `tools/compose-icons.sh` from source
696icons copied unchanged into `assets/kenney-board-icons/` (`skull`, `timer_0`, `pawns`,
697`arrow_diagonal_cross`) and `assets/kenney-icons/White/` (`locked`). `SIM_VERSION` 11.
698Screenshots: `pierce-decoy` became `nuke` (a bomb with all three badges and the Nuke's reach in
699the danger overlay); `charges` shows the top bar with every lasting charge.
700
701### 2026-10-23 — Diagonal is a power-up; shield, auto-move, top bar, installing
702
703User requests, from one round of playtesting:
704
705- **Diagonal becomes a power-up** ("a nice walk power-up"): an ability like Run, Teleport, Kick
706 and Throw, kept on death, weight 8 in the gem's draw (as Kick and Throw), icon
707 `powerup_diagonal` (the former charge icon). It is no longer a charge: six charges remain.
708- **The Shield's ring walks with the player** (it jumped from the old tile to the new one): the
709 Shield's and a Star's rings are drawn where the player is drawn (`playerPos`), sliding with a
710 step. **It is smaller**: 0.88 of a tile across (was 1.08, which spilled onto the next tiles).
711- **Setting an auto-move way takes the first step at once**, as a tap next to the player does;
712 an unsafe first step isn't taken (it shows red, as before). "Walk there" in a dialog does the
713 same.
714- **The board never moves when the top bar fills up.** The user is fine with the bar resizing,
715 but not the board moving. The bar now has a fixed number of rows, by screen width only: three
716 on a phone or a screen under 900 px (lives, level, enemies, coins / turn, bombs, blast, fuse /
717 passes, Star and charges, a row kept even while empty), one row on a wide screen. Rows never
718 wrap; a row that gets too long scrolls sideways. Checked in a browser at 320, 390, 700, 899 and
719 1280 px wide: the bar's height is the same with nothing, with every charge, and with every mode
720 tag. With all four mode tags on a phone, the first row scrolls, hiding part of the hearts.
721- **Installable**, as PLAN.md §17 planned (display fullscreen, icons, offline cache), but without
722 a PWA plugin: a manifest, icons built by `tools/compose-icons.sh` (the bomb on the board's sky
723 blue; a maskable one with room round it; an Apple touch icon), and a network-first service
724 worker (`static/sw.js`) registered only in the build, so development keeps Vite's live reload.
725 Checked in Chromium against the built game: no manifest or installability errors, the worker
726 active, and the game loads with the network cut.
727
728`SIM_VERSION` 12 (Diagonal moved from the charges to the powers). Also fixed: rewriting the
729README's charge section earlier the same day had dropped the sections between it and "Lives"
730(the preview notes, Bombs, Danger, Enemies, Drops and coins, The exit, Power-ups); they are back,
731with the day's changes in them.
732
733### 2026-10-23 — A death keeps every power-up
734
735User request: power-ups stay after a death. `carryOf` now carries everything to the retry —
736stats, every ability and pass (Detonator, Wall / Bomb / Flame Pass included), coins, charges'
737boosts and an unbroken Shield; a death costs only the life. (A Star can't be running at a death,
738since nothing hurts the player while it lasts.) The "Kept / Lost when you die" endings are gone
739from the power-up texts, the README's "Lost when you die?" column and the death banner's "Lost:
740…" list too. This drops the NES rule PLAN.md §5 and §9 were built on: the planned Keep / Fresh
741choice on death (§9, not built) took the passes away in both options, so what Keep vs Fresh
742costs needs rethinking before it is built — marked open there. §5b's question 4 (what a death
743costs with slots) is settled: nothing. `SIM_VERSION` 13.
744
745### 2026-10-23 — Enemies spread out
746
747User report: enemies stacked on one tile moved in the same direction, taking up one tile
748together — "not fun, they should go their separate ways". Why: enemies plan without looking at
749each other and may share a tile, so enemies on one tile make the same choices (Drifters go
750straight; Seekers take the same shortest path; Blobs keep their heading 3 times in 4), and the 3
751let out of a blasted exit or gem start on one tile facing the same way (and since the shelter rule
752they live).
753
754Now, in `planIntents`, enemies are planned in turn and each keeps clear of where the ones before
755it will be at each of the next 3 moves (`spreadSteps`): a Blob wanders among the clear steps, a
756Drifter turns if straight on is another enemy's, a Seeker's step toward the player is dropped
757for a wander if another enemy takes that tile. A crowded step is used only when no open step is
758clear. Enemies still don't block each other, so each still makes exactly the moves its arrows
759show. Tests: three of a kind on one tile in an open room never share a tile over their 3
760planned moves (they all fail without the rule); they still share when there is no other way.
761`SIM_VERSION` 14.
762
763### 2026-10-23 — Guards against a frozen game, and the game's logs in the dev terminal
764
765User report: on a phone (under `npm run dev`), the game froze with the player missing from the
766board. Why it froze rather than carrying on: LittleJS's development build stops its frame loop at
767the first error in a frame (its release build goes on).
768
769**The cause:** a Freeze drop on the board. LittleJS cuts a 128 × 128 tile out of each board
770texture; every sprite is 128 × 128 except the new Freeze icon (`charge_freeze`), inked from a
771100 × 100 Game Icons file, so its tile was invalid and the development build's check threw while
772drawing the drops — after the coin drawn before it, so the enemies, fading things and the player
773were never drawn. Found by replaying the screenshot's situation (Small, Developer, Crumbling
774crates, the last enemy blasted) on 16 seeds in a browser: seed 13 dropped a Freeze and failed
775with "Sprite charge_freeze (texture 68): Assert failed". Fixed: `tools/compose-icons.sh` makes
776every inked icon 128 × 128; a test (`test/sprites.test.ts`) checks every PNG in `assets/kenney`
777and `assets/composed` is 128 × 128 (it fails with the old file); the same seed then played on to
778turn 20, and the 16 seeds without an error.
779
780- **Guards** (user request): the game's per-frame update and draw are wrapped (`guarded` in
781 main.ts): an error is reported once while it repeats, and the next frame runs as usual. A
782 turn the rules refuse (`step` throws) is reported and nothing happens, instead of an uncaught
783 error. `sprite()` now names an unknown sprite instead of handing LittleJS texture −1.
784- **Logs on the dev desktop** (user request: "a callback to show the logs as npm run dev
785 output"): Vite 8's own `server.forwardConsole` sends the browser's console (log, info, warn,
786 error) and uncaught errors over its dev connection to the terminal running `npm run dev`,
787 marked `(client)`, from any device on the Wi-Fi. A first version had its own plugin for this;
788 Vite's built-in does the same, so it was dropped. Caught errors are logged with their stack and
789 the device ("Android 390×844") by `src/devlog.ts`. Nothing of this is in the build.
790- **The top bar's first row** was full of mode tags in that game (Small, Dev, Crumble), which
791 pushed the level, enemies and coins out of sight. The mode tags now start the third row, so the
792 first row always shows hearts, level, enemies and coins.
793
794### 2026-10-23 — Into the doorway, and a demo before the game
795
796- **The step into the exit is animated** (user report: it happened instantly). The turn reports
797 the step and then the level cleared; the cleared jump replaced the step's walk at once. The
798 jump now remembers where the step began: the player walks into the doorway (0.16 s), then jumps.
799- **A demo before the game** (user request: some people never played the original; scenes
800 generated at play time and shown view-only, like a screensaver, before the player starts).
801 Made up on the spot from the real rules rather than recorded: a small random room, random
802 games on it until one shows the scene (an enemy killed, a narrow escape, a chain reaction,
803 Freeze, the Detonator), replayed with a caption. Details in PLAN.md §13a. Choices made here,
804 since the request left them open: it plays **every time the game opens** (not only the first
805 time), until any tap or key, which then does nothing else (a click on a button that appears
806 under the finger is swallowed too); the scenes cycle through all five kinds in a shuffled
807 order; captions are one plain sentence each; test URLs skip it unless `?demo`, and with `?seed`
808 it is the same every time (for the screenshot). The rooms are 9 × 7, so on a tall phone the
809 room sits in the middle with sky above and below. Tests: every kind is found on 10 seeds,
810 replays by the rules with the player alive throughout, and ends on its moment; the same seed
811 gives the same scene. Checked in a browser: scenes play with captions and no errors; a tap
812 ends the demo and the game starts at turn 0 with nothing else done.
813- LittleJS's development build draws its version and frame rate in the top left; the top bar
814 covered it, the demo doesn't, so it is turned off (`setDebugWatermark(false)`).
815
816### 2026-10-23 — The last enemy drops a charge that lasts; icons in the demo's captions
817
818- **No wasted charges from the last enemy** (user report: a charge dropped by the last enemy,
819 like a Decoy, was often wasted). Freeze and Decoy only work on enemies. Of three options (the
820 last enemy drops only lasting charges; Freeze and Decoy picked up with no enemies are saved for
821 the next level; both), the user chose the first: when a turn's kills leave no enemy on the
822 board, every charge they drop is drawn from Pierce, Power, Nuke and Shield, which go on to the
823 next level. Decided once the turn's kills are all done, so two enemies dying together are both
824 "last". A Freeze or Decoy dropped earlier and picked up after the last kill still does nothing
825 (accepted). Tests: over 2,000 rolls a last drop is never Freeze or Decoy (and all four lasting
826 kinds come up), an ordinary drop still can be. `SIM_VERSION` 15.
827- **The demo's captions show the icon** of the charge or power-up a scene is about (user
828 request: "better visual associations with concepts"): the padlock for Freeze, the Detonator's
829 badged bomb for the Detonator — the same icon as on the board, in the top bar and the almanac.
830 The three scenes about bombs in general have none. The icon sits beside the words, not in the
831 line, so a two-line caption stays evenly spaced.
fa3e81a7 2026-10-24 Pavel Safronov832
833### 2026-10-23 — An enemy walks into the flame before it dies
834
835User report (spotted in the demo): an enemy walking into flame started its death on the new
836tile at once, without ever leaving the old one. Its step and its death come in the same turn,
837and the death replaced the step. Now an enemy that moved this turn and dies in it finishes the
838step first (the same 0.16 s hop as any enemy step, from the tile it left), then flips over and
839falls away there — as the player walks into the doorway before the level-cleared jump.
840`WALK_SECONDS` moved to effects.ts, which schedules the death after the step. Checked in a
841browser: 70 ms in, the Blob is half-way into the flame; at 350 ms it is dying on the flame tile.
842
843### 2026-10-23 — The demo shows every feature
844
845User request: the demo's scenes should show every feature of the game except the shop and the
846exit, and one more scene on the enemies' move arrows ("a good idea to watch those"). The scenes
847are now a table (`SCENES` in `sim/demo.ts`), each with its enemies, what the player starts with
848(a power-up held, or a charge lying two steps away to pick up first) and its moment — e.g. Kick:
ebcd0a8f 2026-11-02 Pavel Safronov849a kick and then a kill; Crate Pass: a step onto a crate; Nuke: a kill off the bomb's row and
fa3e81a7 2026-10-24 Pavel Safronov850column; arrows: an enemy killed on the tile it stepped onto, the bomb waiting there. 27 scenes:
851kill, arrows, escape, chain, lingering flame, the enemy types, drops, the gem (uncovered and
852taken; its choice dialog isn't shown), Bomb +1, Fire +1, Fuse +1, Run, Kick, Throw, Teleport,
ebcd0a8f 2026-11-02 Pavel Safronov853Diagonal, the Detonator, Crate Pass, Bomb Pass, Flame Pass, Star, Pierce, Power, Nuke, Shield,
fa3e81a7 2026-10-24 Pavel Safronov854Freeze and Decoy. Heart and Coin bag are left out: they only change numbers in the top bar,
855which the demo hides. The random play now also uses Run, Kick, Throw, Teleport and Diagonal
856when held. Pierce rarely found two crates in a row, so its room gets extra crates. Every
857caption now has an icon. Tests: each kind is found on 6 seeds, replays with the player alive,
858and `showsAtEnd` confirms its last turn is its moment; probes found every kind on 15–30 seeds,
859the slowest under 0.4 s. A minute of the demo in a browser played 8 scenes with no errors.
8da5a7c0 2026-10-24 Pavel Safronov860
861### 2026-10-23 — No second Decoy, Freeze or Shield while one would be wasted
862
863User report (from the demo): two Decoys were dropped, the second while the first still lay
864there. The last-enemy rule didn't cover it (the enemy wasn't the last). One decoy is out at a
865time, a second Freeze adds no turns, a second Shield nothing — so an enemy's charge drop now
866leaves out a Decoy, Freeze or Shield that already lies on the board or is working (`wasted` in
867enemies.ts), on top of the last-enemy rule. Pierce, Power and Nuke add up and are never left
868out, so there is always something to drop. A search over 40 seeds of every demo scene found 6
869such drops before; after, only 2 Freezes dropped in the turn the old freeze ran out (rolled once
870it had, so not wasted). Tests: over 2,000 rolls each, none of the three drops while one lies
871there or works; Pierce still does. `SIM_VERSION` 16.
d5e3ad4d 2026-10-24 Pavel Safronov872
873### 2026-10-23 — The demo shows how you die, and the exit
874
875User request: "how does the player die?" — scenes for the ways to die (walking into flame,
876blocking yourself in, not getting far enough, a bomb setting off another, an enemy, "etc."),
877alternating living and dying scenes, "some funny ones maybe"; and the exit, with scenes ending
878naturally when the player walks out through the open door.
879
880- **Seven death scenes**, each ending on the death: into a blast (stepped from a safe tile onto
881 one that burns that turn), not far enough (was in reach, moved, still in reach; the room gets
882 Fire 3 so it happens), blocked in (every step is about to burn), chain (caught only by a bomb
883 that wasn't due, so the chain set it off), an enemy, and the funny two: the Detonator set off
884 while standing next to the bomb, and a Run straight into an enemy. Walking into lingering
885 flame isn't possible (the game refuses that step), so "walking into flame" is the blast one.
886 The random play still refuses deadly moves except the one that is the scene's death.
887- **The exit**: one scene where it opens and the player walks out — the scene ends there, with
888 the jump into the doorway (once open, the random play heads for it four times in five, or it
889 is rarely found) — and one where flame on the uncovered exit lets three enemies out. I read
890 "demos end naturally when the player walks into an open door" as that exit scene; other living
891 scenes still end on their own moment.
892- **Living and dying scenes take turns**, each from its own shuffled queue, so the 29 living
893 ones aren't spread thin between deaths. The "Ouch!" and level-cleared banners are hidden in
894 the demo. Tests: every kind found on 6 seeds, the player alive throughout and dead at the end
895 of a death scene, and `showsAtEnd` confirms the moment. In a browser: 12 scenes in 70 s,
896 alternating, no errors.
28670c3e 2026-10-24 Pavel Safronov897
898### 2026-10-23 — No drops on the uncovered exit
899
900User report: an enemy died and its charge landed on the closed (uncovered, still locked) exit.
901That hides the door, and once the last enemy dies the drop can only be taken by stepping on the
902exit — which ends the level — while auto-move won't cross the open exit at all. `dropSpot` now
903counts the uncovered exit as taken, like a drop, a bomb or the uncovered gem: a drop there rolls
904to the nearest free floor tile (up to 2 steps). An exit still under its crate isn't floor, so
905drops never reached it. Test: a Blob dying on the uncovered exit (in lingering flame — a new
906blast there would let enemies out) never leaves a drop on it over 500 rolls; it fails without
907the rule. `SIM_VERSION` 17.
908
909### 2026-10-23 — No throwing over a pillar
910
911User question: "a bomb throw shouldn't be able to go through a column/pillar, right?" Throw had
912been "exactly 2 tiles away, over anything" since the first plan (as in the later Bomberman
913games, where a thrown bomb sails over blocks) — the plan's wording, not a user choice. Of the
914options (not over pillars or walls; only over open floor; keep over anything) the user chose
915the first: a pillar or the outer wall on the tile in between blocks a throw; a crate, a bomb or
916an enemy there can still be thrown over. Pillars look like tall solid columns, and crates stay
917a reason to have Throw. Teleport still goes "over anything" (the question was about throws).
918Tests: a pillar blocks, a crate, bomb or enemy doesn't. `SIM_VERSION` 18.
919
920### 2026-10-23 — A tap on a diagonal neighbour steps there
921
922User report: with Diagonal, a plain tap on a free diagonal tile walked one tile sideways first,
923as auto-move does for a far tile — a tap on the board took a direct step only for the four
924straight neighbours, and auto-move's way uses straight steps. Now a plain tap on a diagonal
925neighbour, with Diagonal, is a diagonal step, as a straight neighbour is a step. And with Move
926picked, Diagonal now comes before Teleport in Move's order (`GROUPS` in picks.ts), so a tile
927next to the player is always a step, and Teleport is for tiles further away (it used to win a
928diagonal neighbour too). Auto-move's way still uses straight steps only. Checked in a browser: a
929tap on a free diagonal neighbour moves there in one turn. No rule changed, so no new
930`SIM_VERSION`.
931
932### 2026-10-23 — Auto-move runs and teleports
933
934User report: with Teleport, tapping a far tile still walked there one tile at a time. The
935user's rule: a tile 1 step away is always a step; 2 steps away is two steps, or one run with
936Run, or one teleport with Teleport (or both); further away, run 2 or teleport as far as possible,
937then reassess. Built as `autoMoveAction` (sim/path.ts), used for every move of auto-move,
938including the first one on the tap: a neighbour is a step; otherwise every option — a step along
939the way, a Run along it, each Teleport landing — is scored by the walk left from where it lands,
940and the shortest wins, ties going to Teleport, then Run, then the step. Only tiles auto-move
941calls safe are landed on, and never the open exit unless it is the destination. Auto-move's own
942teleport doesn't end auto-move (a move off the way normally means "changed my mind"). With
943Teleport, a tile out of walking reach but within Teleport's (behind a pillar) is one teleport.
944Tests cover each case; in a browser, a tile 7 steps away took 3 turns (teleport, teleport, step).
39c26813 2026-10-24 Pavel Safronov945
946### 2026-10-23 — Developer mode's gem is an extra one
947
948User request: the gem next to the player in Developer mode is not the level's gem but an extra
949one, added for the developer. It used to be the level's own gem, moved beside the start, which
950left the level without its hidden gem. A level can now hold several gems (`state.gems`, which
951replaces `powerUp`); Developer mode adds one marked `extra` right of the start, in the next
952colour after the level gem's so the two look different, and it draws its own offer (`'offer-extra'`
953stream) so it doesn't offer the same five as the level's gem. The level's own gem stays hidden
954under its crate exactly as in a normal run. Burning, revealing, taking and drops' "never on a gem"
955work per gem. Tests: the level's gem is unchanged and hidden; the extra one is another colour
956and its offer differs on most seeds; a normal run has one gem. `SIM_VERSION` 19.
00ad7d0a 2026-10-24 Pavel Safronov957
958### 2026-10-23 — One board; crates always crumble; God becomes Immortal
959
960User request: move away from the "Normal" and "Small" labels, make the small board the main one,
961make crate crumbling the default and drop it from the options, so the New run menu has only the
962two modes; and rename God to **Immortal**, noting that it earns no achievements but does allow
963playing to the end.
964
965- **One board** (user's choice of "delete the big one entirely" over keeping it behind a test
966 URL): 9 × 11, 20 levels, density 25%, 2 enemies on level 1 + 1 per 4 levels up to 5, par
967 25 + 8 per enemy — the former Small board. `RunOptions.size`, `BoardSize`, `SIZES` and the
968 big board's numbers are gone; the camera always fits the whole board. This changes recorded
969 decision #12 ("50 levels, like the NES") and PLAN.md §11's NES-size board: both are marked
970 as changed, not deleted. The screenshots `small`, `small-desktop` and `scrolled` are gone.
971- **Crates always crumble** once the last enemy is gone (`RunOptions.crumble` removed).
972- **Immortal** (`RunOptions.immortal`, was `god`): nothing can kill the player, so a run can be
973 played to the end, but it earns no achievements — said in the New run dialog, the menu and its
974 tag; there are no achievements yet, so PLAN.md notes it for the victory certificate and the
975 Workshop. Saved menu choices with the old `god` key still read as Immortal.
976- The New run dialog now has just Developer and Immortal; URL parameters are `?dev` and
977 `?immortal`. `SIM_VERSION` 20.
978
979### 2026-10-23 — A readable demo title; Developer runs' achievements in red
980
981- The demo's "Explodorama" title was hard to read in Kenney Future (blocky letters, an X like an
982 H). It now uses the rounded system font the game's text uses, extra bold. Kenney Future stays
983 for buttons and dialog titles, where it is smaller.
984- User request: a Developer run earns achievements like any run, but each carries a **red
985 border**, "like the red quarters some bars used in jukeboxes". There are no achievements yet,
986 so for now the victory banner of a Developer run is framed in red and says "(Developer run)";
987 PLAN.md records the rule for the certificate and the rest. (Immortal runs earn none.) The New
988 run dialog and the run's info say so. Also fixed: the victory banner said "1 turns".
989
990### 2026-10-23 — Moves are animated from the change of state
991
992User report: in the demo, the player walking into flame jumped straight onto the tile — and
993"this issue has happened before (no animation upon a movement); can this pattern be made more
994resilient?" It had: the step into the doorway, an enemy walking into flame, and now the player
995into flame. Each time, something else in the same turn (the level cleared, a death) replaced
996the step's animation, because the player had one animation slot, set from events.
997
998Now movement and pose are separate. **The move comes from the state**: the player's tile before
999and after the turn (`playerTransition`, a pure function in effects.ts), drawn as a slide or, for
1000a teleport, vanishing and reappearing. **The pose** — hit, jump, crouch — is drawn only once the
1001move is done. Nothing in the turn can skip a move any more. Enemies' steps are likewise taken
1002from their change of position (and a dying enemy's last step too), not from events. A new level
1003or a retry starts still. Tests (`test/animation.test.ts`): a step into flame is a move then the
1004hit pose; into the open exit a move then the jump; a teleport; a bomb is a crouch with no move;
1005a new level has no move. CONTRIBUTING.md now asks new animations to follow the pattern. Checked
1006in a browser: 70 ms after stepping into a blast the player is part-way there, walking; at 450 ms
1007they are in the hit pose on the flame tile.
1008
1009### 2026-10-23 — The Move button's key is M
1010
1011User request: the Move button's key hint said "WASD", which isn't the whole truth with
1012Diagonal; make it M, like B for Bomb. M now picks Move (then a direction or a tile), and the
1013button shows "M". W A S D and the arrows still step at once, as before. With Diagonal, Q / E /
1014Z / C now step diagonally at once too (they needed G first), or with Move picked. Checked in a
1015browser: the button shows M, M picks Move, and Z steps down-left in one turn.
1016
1017### 2026-10-23 — The auto-walk is twice as fast
1018
1019User request: auto-move "at least 50% faster". Once every enemy is gone, the walk to the
1020destination now takes a step every 0.5 s (`AUTO_STEP_SECONDS`, was 1 s) — twice as fast. A
1021step's slide takes 0.16 s, so each one still finishes before the next. Each step is still one
1022turn, and a Run or Teleport still counts as one move.
0580e85e 2026-10-24 Pavel Safronov1023
1024### 2026-10-24 — The death banner says how and where
1025
1026User request: when the player dies, a short text says how and on which level, then play goes on
1027as before. The sim now records the death in the state (`GameState.death`: the cause, the enemy
1028for an enemy, and the turn), since the banner is drawn from the state and a reloaded game
1029replays to it. Causes: walked into an enemy (first contact check), an enemy walked into the
1030player (after the enemies move), caught in a blast of a bomb that was due (or set off by the
1031Detonator), caught only by a bomb a chain set off (it wasn't due), or by last turn's lingering
1032flame. The banner: "Ouch! Level 3, turn 12: you walked into a Blob. 2 lives left."; game over
1033puts the same line before the seed. Nothing is recorded when a death is prevented (Shield,
1034Star, Flame Pass, Immortal). Tests cover each cause. `SIM_VERSION` 21.
1035
1036### 2026-10-24 — Same dungeon or a new one at game over; an optional seed
1037
1038User request: when dying, choose between the same seed (dungeon) again and a new one from a
1039random seed — and should a player be able to type a seed? There was no reason not to (the seed
1040is already shown, and sharing or replaying a dungeon, races and a daily seed all need it; the
1041victory certificate is built on the seed), so, with the user's choices:
1042
1043- **At game over only** (over "every death"): the banner's buttons are **Same dungeon** (the same
1044 seed and modes, from level 1) and **New dungeon** (a random seed, same modes). A death with
1045 lives left still has the one Try again. After the victory it is still New run (the dialog).
1046- **An optional seed field** in the New run dialog: empty for a random seed; a whole number is
1047 used as it is; any other text (a word to share) is hashed into a seed. Unlike a `?seed` link
1048 it is a real, saved run. A seed gives the same dungeon only with the same rules version.
1049
1050Checked in a browser: "1234" gives seed 1234, "pavel" a hashed one, empty a random one; at game
1051over both buttons show, and Same dungeon restarts the same seed at level 1.
1052
1053### 2026-10-24 — A local high-score board, planned for a global one; the daily dungeon planned
1054
1055User request: a local high-score board, based on coins or level reached, with the power-ups in
1056use, enemies killed, how the run died; kept local but planned for a global board; seeds kept; and
1057start thinking about a daily dungeon with a seed from the date.
1058
1059- **Ranking** (user's choice of "level, then coins"): a win first, then the level, then coins,
1060 then fewer turns. The best 25 finished runs are kept in `localStorage`.
1061- **Entries** (`sim/scores.ts`, pure and tested) hold the board's facts and the whole run record
1062 with format and rules versions, so a global board can verify a score by replaying it (the sim
1063 is deterministic TypeScript that a server can run). Enemies killed is a new run-wide count
1064 (`runKills`), so `SIM_VERSION` 22.
1065- **Which runs:** finished ones only (game over, or the win); not Immortal (no achievements);
1066 Developer with a red border, as decided; never from a test URL.
1067- **Where:** Menu → High scores, and a "High scores" button on the game-over and victory banners —
1068 those banners cover the menu, which found that the menu couldn't be reached at game over. The
1069 banner says "New high score: #N!" when a run makes the board.
1070- **Planned, not built** (PLAN.md §12b): the global board — verification by replay, no accounts
1071 (a name and an install id), opt-in submission, a small backend — and the daily dungeon (seed
1072 `hashSeed('daily', UTC date)`, fixed modes, its own board, a shareable result). **This would be
1073 the app's first backend**; PLAN.md recorded "no backend or telemetry, on purpose", so building
1074 it needs the user's go-ahead. Open: whether only the first daily run counts, or the best.
1075
1076Checked in a browser: two game overs (the second a Developer run with more coins) each said "New
1077high score: #1!"; the board showed both, the Developer one first, red-bordered.
0b25f1ef 2026-10-24 Pavel Safronov1078
1079### 2026-10-24 — The version number is the rules version
1080
1081User request: a version number in the menu and the README, using the `SIM_VERSION` counter.
1082
1083- **One number:** the game's version is `SIM_VERSION` (now 22), not `package.json`'s 0.1.0,
1084 which nothing bumps. It goes up exactly when a rule change makes old runs replay differently,
1085 so it also says which saved runs and seeds carry over.
1086- **Where:** a small line at the bottom of the menu, under the art credit, and a "Version:" line
1087 near the top of the README. A test (`test/version.test.ts`) fails if the README's number and
1088 `SIM_VERSION` disagree, so a bump can't leave the README behind.
1089- A change that alters only the look (no rule change) keeps the same version.
1090- **The commit and the repository** (user request, same day): the menu also shows the short
1091 commit hash the build came from, linked to it on GitLab, and a "Source code" link. The hash
1092 is read from git when Vite starts (`vite.config.ts`, as `VITE_COMMIT`), so the deployed build
1093 shows the commit CI built. A build with uncommitted changes says "plus local changes", since
1094 the hash alone would claim code it doesn't contain; with no git, the commit is left out.
6e94f995 2026-10-24 Pavel Safronov1095
1096### 2026-10-24 — WebGL again; Developer mode's gem is a rainbow gem offering everything
1097
1098User request: try the WebGL renderer again, and make Developer mode's extra gem "some sort of
1099rainbow/glittering/strange gem", clearly a very special one, offering every possible choice
1100(minus what the player already has) instead of the usual 5.
1101
1102- **WebGL is back** (`setGLEnable(true)`; supersedes "Canvas 2D renderer, not WebGL"). The black
1103 strip in the screenshots came from Playwright's default *headless shell*: with the same build,
1104 the full Chromium in its new headless mode (`channel: 'chromium'`), headed Chromium, and the
1105 SwiftShader and Metal back ends all draw it correctly. `tools/screenshots.mjs` now launches
1106 `channel: 'chromium'`, so the screenshots stay trustworthy. Still separate textures, no atlas:
1107 a few hundred sprites a frame don't need batching across textures.
1108- **One WebGL difference fixed:** an added colour's alpha is added to the sprite's alpha, so the
1109 frozen enemies' sparkles (added white, alpha 1) would have drawn as white squares; they add
1110 `C.glint` (white, alpha 0) instead.
1111- **The rainbow gem** (`GemColor` `'rainbow'`, only the extra gem; the level's gems keep the four
1112 colours): `assets/composed/gem_rainbow.png`, built by `tools/compose-icons.sh` from the green
1113 gem — its body in a red-to-violet rainbow over its own shading, the outline unchanged (Kenney
1114 art from the packs already in use). On the board it turns like a coin, flashes as it faces you,
1115 and five sparkles circle it, each a flat colour cycling through the hues. Its info title is
1116 "Rainbow gem (Developer mode)".
1117- **Its offer is every power-up that would do anything**: abilities and passes already held
1118 are left out; stats, Star, Heart and Coin bag are always in. Listed in the usual order, with
1119 nothing drawn (so no random stream). The dialog scrolls (15 at most). `SIM_VERSION` 23, since a
1120 Developer run saved before would replay differently.
1121- Screenshots: `dev` shows the rainbow gem; new `dev-offer` shows its choice.
e3f29330 2026-10-24 Pavel Safronov1122
1123### 2026-10-24 — The rainbow gem offers every charge too
1124
1125User request: so Freeze and the other charges can be tried without waiting for an enemy to drop
1126them. The rainbow gem's offer lists every power-up that would do anything, then all six charges
1127(`OfferKind` = a power-up or a charge; `isCharge`). A chosen charge works exactly as a picked-up
1128one (`applyCharge`): bomb boosts add BOMB_BOOST, Shield goes up, Freeze freezes every enemy,
1129Decoy is left on the gem's tile. Choosing takes no turn and comes after the gem's turn, so
1130a Freeze chosen this way skips the enemies' next 3 moves (one picked up skips its own turn's). A level's own gem never offers a charge.
1131`SIM_VERSION` stays 23: a run recorded before still replays the same (its choices are still on
1132offer), and 23 itself is from earlier today.
1133
36c5d6a2 2026-10-24 Pavel Safronov1134### 2026-10-24 — Demos get to the point, and show the controls
1135
1136User request: scenes sometimes ran many moves, the player and enemies dancing around, before
1137the thing they're about; it should come within 3–6 steps of the start. And the demo should
1138teach the controls: a tap effect, the action buttons, a virtual finger, with scenes for moving,
1139a bomb by long press, kicking a bomb, and so on.
1140
1141- **The moment comes 3–6 turns in.** Rather than limiting the random games to 6 turns (most
1142 would fail), a game still runs until it shows the moment, and the scene is its last 3–6
1143 turns, from a **calm** board: no bomb (so each bomb is seen placed and counting down), no
1144 flame, no drop the room didn't start with (a coin or charge left by an enemy killed before
1145 the scene would be a mystery) and auto-move not under way. The shortest such ending wins, and
1146 it is replayed from its start to check the moment still comes (a charge must be picked up, a
1147 kick made, within the scene). I read "within 3–6 steps" as: at least 3 turns of lead-up, at
1148 most 6, so the moment is never the very first turn.
1149- **Scene changes for the window:** Fuse +1 now starts the player at fuse 4 (start 3, plus one;
1150 it was 5); Pierce, Power and Nuke lie next to the player rather than two tiles away; Pierce's
1151 room has Fire 2 and a straight row (Pierce, a free tile, two crates), and its moment is now
1152 a crate burnt *behind* another — before, two crates on different sides counted, which any bomb
1153 does.
1154- **Gestures.** Every turn of a scene comes with the gesture that plays it, chosen so the real
1155 controls would read it as exactly that action: a tap (step, diagonal step, kick, detonate),
1156 a long press (a bomb), a button and then a tile (Run, Teleport and Throw; a bomb in the Bomb
1157 button scene), Wait's button, a far tap (auto-move) and Go. What a tap means moved from
1158 `tapTile` into the sim as `tapMeaning` (picks.ts) so the demo and its test use the same rules;
1159 `tapTile` behaves exactly as before. Auto-move's safety check moved to path.ts (`safeStep`)
1160 for the same reason. A random turn no gesture can play (a step onto a bomb with Bomb Pass,
1161 whose tap would kick or do nothing) is never chosen.
1162- **The finger** is the UI Pack: RPG Expansion's pointing hand (`cursorHand_beige`), already one
1163 of the game's packs. Its PNG is 27 px, too small to scale up, so its three shapes were cut out
1164 of the pack's vector sheet into an SVG. It glides to each tile or button, sinks as it presses,
1165 and leaves a white ripple (with a thin dark rim, to show on the pale floor); a long press
1166 draws a yellow ring closing in over the real 450 ms, then the armed bomb's preview for 0.6 s.
1167 The board answers through the real view state: the picked button's tiles and the preview
1168 under the finger, the armed bomb, auto-move's way and Move turning into Go.
1169- **The action bar shows in the demo**, as a dark rounded tray right under the board (8 px
1170 below it, as wide as it), and the caption panel stays apart at the bottom, at least 24 px
1171 below: the board and its buttons are one group, the explanation another (user request, same
1172 day — at first the bar sat on the caption panel, which read as part of the text). The camera
1173 fits board and bar together above the panel; main.ts places the bar every frame from the
1174 board's screen rectangle. A press shows as the real one (`.pressed`, like `:active`).
1175- **Five control scenes** — step (3 taps), bomb by long press, bomb with the Bomb button, Wait
1176 (a wait, then a kill), auto-move (a far tap, then Go until there) — play first, in that order,
1177 and again each time the living scenes have all been shown. Kicking has no scene of its own:
1178 the Kick scene already shows it, and its caption now says "tap a bomb next to you"; the
1179 Run, Throw, Teleport, Diagonal and Detonator captions say how too.
1180- **Timing:** a tap turn takes about 1.2 s, a button-and-tile turn about 2 s, so a scene is
1181 4–12 s. Tests: every kind on 6 seeds is 3–6 turns long from a calm start, and each gesture,
1182 read by the real rules (`tapMeaning`, `resolvePick`, `autoMoveAction` with `safeStep`), gives
1183 that turn's action. A probe found every one of the 41 kinds on 40 seeds each, the slowest
1184 0.39 s (Nuke). Checked in a browser: two minutes of the demo played 15 scenes (the five
1185 control scenes first) with no errors; frame by frame, the finger, ripple, ring, button
1186 presses, highlighted tiles and auto-move's way look as described; a tap ended the demo and left
1187 no finger or marks behind. The `demo` screenshot is now taken mid long-press.
3cf3607b 2026-10-24 Pavel Safronov1188
1189### 2026-10-24 — No compatibility code for old versions; surviving a rules change shelved
1190
1191The game hasn't been released, so there are no saves or settings to carry forward. The one
1192piece of old-version code, reading the New run choices' former `god` key as Immortal, is gone,
1193with the God name in comments and test names. The `SIM_VERSION` check on resume stays: it isn't
1194compatibility code, it keeps a move log from being replayed under rules it wasn't played by.
1195Keeping a run across a rules change (a state snapshot, the record noting the change) or playing
1196older versions was discussed and shelved by the user: a lot of effort for a doubtful pay-off
1197while nobody else plays (PLAN.md §11).
58f1e700 2026-10-24 Pavel Safronov1198
1199### 2026-10-24 — The demo's finger starts at the player; a "You" tag
1200
1201User request: the eyes follow the hand, so it should start close to the player; and it isn't
1202immediately clear which of the moving figures is the player.
1203
1204- **The finger appears beside the player** at the start of every scene: its tip at the
1205 bottom-right corner of the player's tile, faded in there rather than gliding in from wherever
1206 the last scene ended (or, for the first scene, the middle of the screen). It is placed once the
1207 camera shows the new scene, so the first scene gets it right too.
1208- **A "You" tag** over the player for the whole demo: a green pill (the accent colour, as on a
1209 picked button) with a dark rim and a point down at them, following where the player is drawn,
1210 so it walks, runs and teleports with them (`playerPos`, now exported from board.ts). It hugs
1211 the top of the player's head, always over them (user's call, same day: for a while it flipped
1212 under the player on the top row, where it blended into the stone wall; now it stays put, and a
1213 white halo round its dark rim keeps it apart from the stone). It sits
1214 above the finger, so a tap on the tile over the player doesn't hide it. Only in the demo; in
1215 play, the action bar's Move button and the tapping make it clear soon enough, and the tag would
1216 cover the tile above. Checked in a browser frame by frame.
e3384c6b 2026-10-24 Pavel Safronov1217
1218### 2026-10-24 — The top bar in the demo, its tour, "What is that?"; the player stands higher; bumps no longer clipped
1219
1220User request: add the top bar to the demos, describe its items, and show how to use **?** to
1221learn about enemies. Also: draw the player a few pixels higher, so its legs never touch the line
1222between tiles; and (spotted meanwhile) the Turn item, bumping when a turn passes, was cut off by
1223the bar's edge.
1224
1225- **The top bar shows in the demo**, as in play; the board, the action bar under it and the
1226 caption panel fit below it. Each scene now starts at turn 0 with no coins (a scene is the end of
1227 a longer game: the counter would have opened at, say, 14). Safe because a scene starts with no
1228 bomb out, and the turn number only matters relative to a bomb's (`placedTurn`). Demo rooms have
1229 par 20 instead of a dummy 99, which the bar would show.
1230- **The top-bar tour**, after the control scenes: the finger points (no tap: each item would open
1231 a dialog, twice as long) at lives, level, enemies left, coins, turn and par, bombs / blast /
1232 fuse, and the row of power-ups and charges, each ringed in yellow for 2.6 s while the caption
1233 says what it is; the last says that in a game a tap on any item reads more. The room is a
1234 still one (no turns), given Kick and 3 Pierce so the last row has something to show.
1235- **"What is that?"**, next: the finger taps **?** and then an enemy, through the real `inspect`
1236 and `tapTile`, so the real info dialog opens; 3.2 s later it taps Got it. A tap that ends the
1237 demo closes the dialog too. In the demo the dialog isn't a modal (user request, same day: the
1238 modal dimmed the caption panel as well): `setDialogInDemo` opens it with `show()`, centred over
1239 the scene above the panel and dimming only that (a wide box-shadow), with the panel above it.
1240 That also lets the finger go over it; a modal sits in the page's top layer, above any z-index.
1241- **The tour's ring moves with the finger** (user request, same day: it changed before the finger
1242 got there): it changes over half-way along the glide, about where the finger crosses from one
1243 item to the next, and for the first item once the finger arrives; the caption with it.
1244- **The player stands 0.07 of a tile higher** (`PLAYER_LIFT` in board.ts): the sprites' feet are at
1245 their very bottom edge, and the idle bob dipped them below it. Walking, teleporting and the
1246 move preview use the same lift; the decoy is drawn smaller, so it needed none.
1247- **Top-bar bumps aren't clipped**: a row that can scroll sideways also clips, so an item bumping
1248 (scale 1.3) at a row's edge — Turn, first in its row — was cut off. Each row has 6 / 16 px of
1249 room inside, given back with negative margins; checked on a phone and a wide screen that the
1250 layout is otherwise unchanged.
1251
1252### 2026-10-24 — The demo on a CRT; EXPLODORAMA!
1253
1254User request: "try out LittleJS post-processing: can we make the demo appear on a CRT TV with
1255lines … like someone is actually playing on a touch-capable CRT"; then, on a wide desktop, limit
1256its width with black bars beside it. And the app is now called **EXPLODORAMA!**.
1257
1258- **Two layers**, because the board is a WebGL canvas but the top bar, the action bar, the "You"
1259 tag and the dialog are HTML, which no shader reaches. LittleJS's `PostProcessPlugin` shades the
1260 board with a CRT's signal: colour fringes toward the edges (`postProcessChromatic`) and faint
1261 static (`postProcessNoise`). It is a pass-through in play (`setShaderCode()` with nothing) and
0b63d158 2026-10-25 Pavel Safronov1262 the CRT only while the demo runs. (*Since 2026-10-24 it is switched off in play instead —
1263 `postProcess.enabled`, LittleJS 1.28.0 — see "Android Chrome at half a frame a second".*) A layer over the whole screen part, `#demo-crt`, adds what
e3384c6b 2026-10-24 Pavel Safronov1264 belongs to the tube: scan lines 3 px apart, a vignette, a faint glare, and the glass's rounded
1265 corners in a dark bezel, with a slight flicker (none with reduced motion). Then (user request,
1266 same day) what moves on an old TV: a soft dark bar rolling down every 7 s, as a TV filmed by a
1267 camera shows, and two thin bright lines sweeping down, out of step (3.4 s and 5.3 s); all
1268 hidden with reduced motion. And (user request, same day) **interference**, as when a vacuum
1269 cleaner is switched on nearby: every 12–25 s (the first after 8 s), for 0.5 s (1.6 s at
1270 first, too distracting), the picture
1271 tears — its lines jump sideways, some far, in a custom post-process piece before the colour
1272 fringes, driven by a shader value, `interference`, set each frame and fluttering like a motor —
1273 the static gets ten times louder (`staticNoise`), and an SVG-noise snow jumps over the whole
33a0256b 2026-10-24 Pavel Safronov1274 screen, top bar and buttons included; then it stops at once. None with reduced motion. Then
1275 the sideways jumps were halved (user request, same day: the left-to-right distortion was too
1276 much); the static, snow and darkening stayed as they were. The finger is in
e3384c6b 2026-10-24 Pavel Safronov1277 front of the glass (above it), the ripples on the screen (below it); the caption panel is not
1278 on the TV.
1279- **No glow, no bulge.** LittleJS's TV effect glows from black up, and even a glow only past 0.8
1280 brightness washed the pale board out (the sky measured 243 green instead of 227); without it
1281 the colours match play. A bulged screen would bend the board but not the HTML over it, and the
1282 finger would miss the tiles it points at.
1283- **At most 4:3**: on a wide screen the TV is as wide as 4:3 of its height and centred, with black
1284 bars either side (a huge box-shadow); the top bar narrows to it. On a phone it is the full width.
1285- **EXPLODORAMA!**, all caps with an exclamation mark: the page title, the home-screen name
1286 (manifest and Apple's), the demo's title and the README's heading. The package name stays
1287 `explodorama`, and the docs still write Explodorama in running text.
1288
1289### 2026-10-24 — Every demo room has its exit
1290
1291User request: most demo rooms had no door, which made captions that mention it (the enemies
1292counter: "once they're all gone, the exit opens") strange. Every room now gets one, as every
1293level does (`placeDoor`): under a crate five rooms in six, otherwise already uncovered and
1294locked while enemies live (open once they're gone); never under the gem's crate or on a drop,
1295and at least 3 steps from the player. The exit scenes still place theirs themselves. Since the
1296exit is part of the rules (flame on an uncovered one lets enemies out; once open, walking in
1297clears the level), the random games play with it; a probe over 40 seeds of every kind found all
1298but one Nuke scene (a missing scene is skipped, and the tests' seeds all pass), the slowest
12990.44 s. At a scene's start, 79% of the doors are still covered.
33a0256b 2026-10-24 Pavel Safronov1300
1301### 2026-10-24 — Clips in the info dialogs: plan, and the drawing spike
1302
1303User request: now that the demo works, give the info dialogs animations made on the spot the same
1304way. The plan (every dialog's clip, which demo scene each reuses, the new ones, the order) is in
1305PLAN.md §13c. The user's answers: the finger where it helps, no CRT, a still where one frame says
1306it best under reduced motion, a 1 s hold and a new setup every loop, and every item on the list.
1307How to draw them was left to me:
1308
1309- **One canvas, drawn twice a frame while a clip shows**: the clip's room first, copied into a
1310 `<canvas>` in the dialog, then the canvas cleared and the game drawn. The clips use exactly the
1311 board's drawing and animation code, with nothing duplicated. Rejected: drawing the clip behind
1312 a hole in the dialog (the modal's backdrop covers it too), a second renderer (a second copy of
1313 the board's drawing), frames rendered ahead (heavy, and no new setup each loop).
1314- **The spike** (`ui/clip.ts`): LittleJS builds the camera's transform only at the start of a
1315 frame (`glPreRender`, not exported), so the clip doesn't move the camera; the board gets an
1316 origin instead (`setBoardOrigin` in board.ts, used by `toWorld` and `toTile`), and the clip's room
1317 is drawn centred where the camera already looks, at the game's scale. After `glFlush`, that patch
1318 of the WebGL canvas is copied with `drawImage` (the drawing buffer is still there within the
1319 frame), then `glClearCanvas`, the origin back at 0, and the game drawn as before. Its own view
1320 (effects, flames, moves) for now repeats main.ts's `animateTurn`; step 2 makes that view an
1321 object both use. A clip stops when its dialog closes.
1322- **First clip, to prove it:** "What is that?" on an enemy played the demo's `enemies` scene in
1323 the dialog, a turn every 0.6 s, its moment held 1 s, then a new setup. Checked in a browser: the
1324 clip plays crisply above the words, loops with new rooms, the dimmed game behind is undisturbed,
1325 and the board is normal once the dialog closes; no errors.
1326- **Enemies get no clip after all** (user's call, on seeing it): the scene was a busy room with all
1327 three kinds, and the player learns soon enough how a Blob moves; the others aren't different
1328 enough to need one either. Their dialogs keep only their picture, on the board and in the
1329 almanac. The first clip is now "Still burning" (flame from last turn), with the `lingering`
1330 scene. Clips should keep their rooms simple, with as few enemies as the moment needs.
1331
1332### 2026-10-24 — The demo from the menu; a shorter menu; bug reports on the backlog
1333
1334- **Watch the demo** (user request: "just seems fun"): a menu button that starts the demo again,
1335 from its first scene (the controls'), as when the game opens; the run waits behind it, and the
1336 tap that ends it brings the run back as it was (an auto-move destination is dropped).
1337- **The menu names the run's modes and nothing more** ("Modes: Developer, Immortal."), instead of
1338 a paragraph on each (user request: the menu looked busy). The tags in the top bar still open
1339 the full explanation.
1340- **Bug reports** through GitLab Service Desk (user request) are on PLAN.md's new Backlog: the
1341 user turns it on in the project's settings, then a menu button opens an email to its address,
1342 filled in with the version, device, seed and run record.
1343
1344### 2026-10-24 — Clips, step 2: one animation object, scenes made in a worker
1345
1346- **The board's animation state is one object** (`Animation` in `render/animate.ts`: effects,
1347 flames, the player's move, pose and facing, enemy steps), with one `animateTurn` that starts a
1348 turn's animations. main.ts keeps one for the game and the demo; each clip has its own. Nothing
1349 about play changed; main.ts lost six module variables and its copy of the turn's animation.
1350- **Scenes are made in a Web Worker** (`sim/scene-worker.ts`; `makeScene` in `ui/scenes.ts`): one
1351 can take ~0.45 s, which on the page stalls every animation. The demo makes its next scene, and a
1352 clip its next loop, while the current one plays. Where a worker can't be made (or fails), they
1353 are made on the page as before. Vite builds the worker as its own file (32 kB).
1354- Checked in a browser: three minutes of the demo with no errors, the "Still burning" clip, and the
1355 menu's Watch the demo and back.
1356
1357### 2026-10-24 — The demo in order, numbered, with ◀ ▶ and ↺, carrying on where it was
1358
1359User request: "loop through all the possible demos in order, indicate which number demo this is,
1360and allow left/right controls to switch demo scenes. When a page is reloaded, the demos continue
1361from the same spot. Add a round arrow or something to restart from the beginning, but make it a
1362choice."
1363
1364- **One fixed order**, looped (`DEMO_ORDER`): the seven control scenes (the five on the controls,
1365 the top-bar tour, "What is that?"), then the living scenes in the table's order with a way to
1366 die after every four — the dying ones used to take turns with the living, shuffled; I kept them
1367 spread out rather than all at the end. 43 in all. Each is still made new every time it plays.
1368- **The panel shows "n / 43"**, between **◀** and **▶** (Game Icons' arrow, the left one mirrored),
1369 with **↺** (the UI Pack's repeat icon, as on New run) set apart after them: back to the first, only
1370 when pressed. **← and →** do the same as ◀ ▶. These buttons and keys don't end the demo; any
1371 other tap or key still does. Skipping stops whatever was under way (the finger's script, an open
1372 dialog, the tour's ring) and plays the chosen scene as soon as it is made.
1373- **It carries on where it was**: the scene playing is kept in `localStorage`
1374 (`explodorama.demoAt`), so the demo starts there next time — on a reload, and from the menu's
1375 Watch the demo. Not on a test URL, which starts at the first scene and saves nothing (the
1376 screenshots stay the same).
1377- Checked in a browser: ▶ and → step forward, ← and ◀ back (wrapping from 1 to 43), a reload
1378 resumes at the same scene, ↺ goes to 1, and a tap elsewhere ends the demo; no errors.
1379
1380### 2026-10-24 — Report a bug, through GitLab Service Desk
1381
1382The user turned Service Desk on and gave its address; it lives in the source (`BUG_EMAIL` in
1383main.ts), not a CI variable: it is public by design — the game's email link carries it either way —
1384and if it draws spam, GitLab can give the project a new alias. The menu's **Report a bug** opens
1385an email to it (`mailto:`), subject "Bug report: version 23, level N", body "What happened:" with
1386room to write, then the details: version and build commit, the device and its user agent, seed,
1387level, turn, lives, status and modes, the last 5 errors the game caught and carried on from (kept
1388by `reportError`, their first lines), and the run record, which replays the run exactly. Some mail
1389apps cut off long links, so over ~1,900 characters the record stays out of the email; the full
1390details go on the clipboard (only then, so the clipboard isn't overwritten for nothing), and the
1391email asks to paste them. Checked in a browser: a short run's email carries the record (919
1392characters); after 90 turns it doesn't, and the clipboard holds the full details.
1393
1394### 2026-10-24 — Sensible play in the demo
1395
1396User report: an early scene placed its bomb on the exit; demos should show reasonable actions,
1397"unless the entire point is to paint ourselves into a corner". The random play now only takes a
1398turn a sensible player might (`reasonable` in sim/demo.ts), except in the scenes about dying:
1399
1400- nothing that lets enemies out of an uncovered exit or gem — neither a turn whose flame does,
1401 nor a bomb whose blast would reach one (the scene about blasting the exit is exempt);
1402- a bomb, placed or thrown, only where its blast reaches a crate, an enemy, a tile an enemy is
1403 going to, or another bomb (to set it off) — never one that does nothing.
1404
1405A random game gets 6 goes at each turn instead of 4. On 40 seeds of every kind, every scene was
1406found (the one Nuke miss is gone too), the slowest in 0.5 s, in the worker. Tests: in the living
1407scenes no enemies are let out (but in the exit-blast scene) and no bomb goes on the exit.
1408
1409### 2026-10-24 — Long dialogs scroll by touch again
1410
1411User report: the almanac wouldn't scroll. By mouse wheel it did (fixed 2026-10-22); by finger it
1412didn't: LittleJS listens for touches on the whole document and cancels every one that isn't on a
1413control (no page scrolling in a game), which also cancelled a finger dragging the almanac's list,
1414and the gem's long offer. Touches inside the dialog now stop there (as the wheel already did), so
1415LittleJS never sees them; the board is behind the dialog anyway. The dialog's body also allows
1416vertical panning (`touch-action: pan-y`) against the body's `none`. Checked with simulated touch
1417drags in a mobile browser: the almanac's list scrolls (it didn't on the last three commits), and
1418taps on its tabs and buttons still work.
32dd53cd 2026-10-25 Pavel Safronov1419
1420### 2026-10-24 — Clips, step 3: the dialogs that reuse demo scenes
1421
1422Each info text (`Info` in ui/info.ts) now names the scene its dialog plays (`clip`), and
1423`showInfo` plays it: the action buttons (Move: plain steps, or the move picked — Run, Teleport,
1424Kick, Diagonal; Bomb: the long press, or Throw, the Detonator; Wait), the top bar's level (the
1425exit), coins, bombs, blast, fuse, Star, each pass and each charge held, and on the board a bomb
1426(its countdown and blast, or the Pierce / Power / Nuke shape), the decoy, flame still burning,
1427the gem, the exit locked (blasting it) and open (walking out), a drop, and the first-pickup
1428dialog of each charge. Lives, enemies left, turn and par, crates and walls wait for step 4's new
1429scenes; the modes have none yet.
1430
1431**The finger** shows how in the clips about the actions and the power-ups worked by hand (Run,
1432Kick, Throw, Teleport, Diagonal, the Detonator), as the user chose ("when helpful"): it glides to
1433the tile, taps (a ripple) or holds (the ring, then the armed bomb's blast), and the turn plays as
1434it lets go; a button's tiles light up first where the scene picks one, as in play. The action
1435bar isn't in a clip, so Wait is shown as a tap on the player, which waits in play too. The
1436finger is an image over the dialog's canvas, placed by tile. Checked in a browser: the Bomb,
1437Coins and Blast dialogs play their clips, the Bomb one with the finger's long press; no errors.
1438(The development build's LittleJS debug text showed behind the dialog after Esc: that is
1439LittleJS's own debug key in the dev build, not in the built game.)
1440
1441### 2026-10-24 — Clips, step 4: the new scenes
1442
1443Three scenes made for the dialogs only (`clipOnly`: not in the demo, whose 43 stay as they were):
1444**lastEnemy** for "Enemies left" (the last one dies and the uncovered exit opens), **crate** (a
1445blast breaks the crate over the gem) and **pillar** for walls (flame, Fire 2, stopped by a pillar).
1446"Lives" plays the touched-by-an-enemy death (`dieEnemy`). The top bar isn't in a clip, so what only
1447a number shows — turn and par — gets no clip, and nor do the modes. On 40 seeds each, all three
1448new scenes were found, the slowest in 0.09 s. Checked in a browser: the Crate, Pillar and Enemies
1449left dialogs play their clips; no errors.
1450
1451### 2026-10-24 — Clips, step 5: the offer, the almanac, the "Ouch!" replay
1452
1453The user chose, of the options offered: one clip above the offer's cards, almanac entries that
1454open their clip, and a replay of the player's own last turns on the banner.
1455
1456- **The gem's offer:** a clip above the cards, kept in view while they scroll (sticky). It plays
1457 the first power-up with a clip, then the one whose picture is tapped (or pointed at with a
1458 mouse). The picture is now its own square button beside the card, ringed green while it plays;
1459 choosing is still one tap on the card. Heart and Coin bag have no clip: the clip goes blank.
1460- **The almanac:** entries with a clip (every power-up but Heart and Coin bag, every charge) are
1461 buttons with a green ▶; one opens that thing's own dialog with its clip and **Back**, which
1462 returns to the almanac on the same tab. Enemies, without clips, stay plain.
1463- **The "Ouch!" replay:** main.ts keeps the last 4 turns of the current attempt (cleared when a
1464 level starts or restarts); on a death, and on game over, the banner plays them as a fixed scene,
1465 over and over: a 9 × 7 window of the level round where the player died (the clip draws the
1466 whole level and copies only that part). Not after a resumed save, which has no turns kept.
1467- **Under it:** `startClip` takes a kind (a new setup each loop) or a fixed scene, can switch a
1468 canvas already playing, and several clips can play at once (the banner's and a dialog's): each
1469 is drawn and copied in turn. A clip plays while its canvas is on the page and shown — not in a
1470 closed dialog or a hidden banner. Every `canvas.clip` is sized by its frame (the banner's showed
1471 only a corner at first, at its natural size). The screenshot tool now waits for the game's own
1472 canvas, not the first on the page.
1473- Checked in a browser: the rainbow gem's offer plays Bomb +1, then Kick when its picture is
1474 tapped, and choosing Kick works; the almanac's Throw entry opens with its clip, Back returns;
1475 bombing yourself shows the replay on the banner, and Try again hides it; no errors. All
1476 screenshots retaken: most changed by a few thousand pixels, the player now standing a little
1477 higher; the death, almanac, offer and info shots show the new clips.
1478
1479### 2026-10-24 — A clip's finger starts beside the player; the screenshot tool's taps
1480
1481- **In a clip, the finger now appears beside the player** at the start of each loop (its tip
1482 half a tile below and right of the player, faded in there), then glides to its first tile, as
1483 in the demo (user report: in the Move dialog it came in from the top left corner, where it sat
1484 before its first move). Checked in a browser: the Move clip's finger starts by the player.
1485- **The screenshot tool tapped the wrong tiles** (user report: `info-enemy.png` showed a crate). Its
1486 `tileCentre` still assumed the old 13-row board scrolled to the left edge; it now works out the
1487 9 × 11 board fitted and centred between the bars. The enemy shot taps a Blob (1, 7) now; the
1488 auto-move and long-press shots, which tap too, show what their comments say: the way to (3, 3)
1489 in blue with Go, and a bomb armed right of the player.
ac534e57 2026-10-25 Pavel Safronov1490
1491### 2026-10-24 — Clips: the modes, and stills under reduced motion
1492
1493The last of the clips plan. **The modes' tag** now plays a clip: the rainbow gem beside the
1494player, taken (Developer mode; a new clip-only scene, `rainbowGem`), or the player unharmed in a
1495blast or by an enemy (`immortal`); a run with both shows Developer's. **Under reduced motion** a
1496clip is a still, as the user chose ("whatever is clearer"): the scene played to its end at once
1497and held — the moment just after it, flame still on the board, the player's pose — with no finger
1498and no next loop. Checked in a browser with reduced motion emulated: the Blast dialog shows its
1499still, the flame on the board; both mode clips play; no errors.
1500
1501### 2026-10-24 — The gem's offer: select, then Choose
1502
1503User report: in the (rainbow) gem's offer a tap on a card's description chose it at once; it should
1504be two steps — tap to select and see it in action, then confirm. Now a tap anywhere on a card
1505selects it (ringed green, `aria-pressed`), the clip above the cards plays it, and a **Choose Kick**
1506button (it names the selection) takes it; the first card starts selected. Every gem's offer works
1507this way, not only the rainbow one, so it is the same everywhere. The separate picture button and
1508the mouse-hover preview of the step before are gone: the selection is the preview. Checked in a
1509browser with touch: a tap on Kick keeps the dialog open and changes the button to "Choose Kick";
1510Choose takes it (Kick appears in the top bar).
1511
1512### 2026-10-24 — Dialogs fit the visible screen
1513
1514User report (a screenshot from a phone): the gem's offer ran off the bottom of the screen, its
1515Choose button cut in half. The dialog's body had a height guessed from the screen's (`100vh` less
1516150 or 260 px for the rest); on a phone `100vh` includes the part behind the browser's address
1517bar, and the offer's two-line title, clip and Choose button took more than the guess. Now the
1518panel is a column never taller than what the browser shows (`100dvh`, less room for the close
1519button above it; `100vh` where `dvh` isn't known) and only its body scrolls, so the title and
1520buttons always show, whatever is in it. Checked at 390 × 640, 390 × 844 and 1280 × 600: the
1521rainbow gem's offer and the almanac fit, the Choose button 30 px above the panel's bottom edge.
1522
1523### 2026-10-24 — Heart and Coin bag show their picture in the offer
1524
1525User report: selecting Heart or Coin bag in the gem's offer left the clip's place empty. They have
1526no clip (nothing changes on the board: a life, coins), so their picture now shows there, big, on a
1527pale box the clip's size and shape; any other selection brings the clip back. On the way: a
1528hidden clip canvas didn't hide (`canvas.clip` sets `display: block`, which beats `hidden`), so
1529`canvas.clip[hidden]` is `display: none` now — the "Ouch!" banner's canvas, hidden when there is no
1530replay, relied on it too. Checked in a browser: Heart and Coin bag show their pictures, Kick its
1531clip again.
7f242d36 2026-10-25 Pavel Safronov1532
1533### 2026-10-24 — The gem screenshots show the gem again
1534
1535User report: `gem-offer.png` showed the board, not the offer. Its seed-55 key presses no longer
1536reached the gem (the levels have changed since they were written), and `gem.png`, the step
1537before, showed no gem either. A short search with the sim — random moves, bombs and waits on
1538seeds 1–120, keeping the shortest that ends with the gem taken and the player alive — found seed
1539107: down, a bomb below, wait, up, wait three times, down twice (the gem uncovered beside the
1540player: `gem.png`), right onto it (`gem-offer.png`, the offer open).
1541
1542### 2026-10-24 — Run extends Teleport; RPG, Proximity alert, Slide and the ICBM charge
1543
1544User requests: with Teleport, Run should extend it (Teleport already reaches every tile a run
1545does); a power-up to "send the bomb as far as it can fly" (RPG); a charge that flings the bomb
1546the same way but explodes instantly (ICBM); a power-up that explodes a bomb when an enemy steps
1547into its range (Proximity alert); and a power-up to slide in a straight line until a wall or a
1548crate, its highlight showing the next tile and then the far tile where the slide ends. The rules
1549below are the user's choices of the options offered (all the recommended ones).
1550
1551- **Run + Teleport:** Teleport reaches 4 steps instead of 3 while Run is held too
1552 (`teleportRange`). Run's own move stays, and is never better than a teleport.
1553- **RPG** (power-up, weight 6, part of Bomb, key F): fired in a straight line, the bomb flies until
1554 the next tile is a wall, crate, bomb or enemy and lands just before it (`flightEnd`), 2 tiles away
1555 or more (1 is a plain bomb); it counts down as usual, within the bomb limit. It flies straight,
1556 like a kicked bomb (`bombFired`), not in a throw's arc.
1557- **ICBM** (charge, key I): 3 launches per pickup, added up like Pierce; Bomb fires one as an RPG
1558 bomb at any distance and it goes off that same turn; the bomb limit doesn't count, and it works
1559 without the RPG power-up. It lasts past the last enemy, so the last one may drop it. Choice made
1560 here: while launches are left, a far tile picked with Bomb is an ICBM, not an RPG bomb (Bomb's
1561 group puts ICBM first; the preview shows it going off) — otherwise a player holding both could
1562 only use launches by key. The other way round would hide the ICBM behind every RPG target.
1563- **Proximity alert** (power-up, weight 5, passive): bombs placed while it is held carry the flag;
1564 after the enemies move, each one with an enemy in its reach is set to go off that turn
1565 (`proximityAlert`), so an enemy already in reach when the bomb is placed sets it off too —
1566 with the player perhaps beside it. The danger overlay runs the same check on where the enemies'
1567 arrows say they will be, this turn and the next.
1568- **Slide** (power-up, weight 6, part of Move, key L): over free floor until the next tile is a
1569 wall, crate or bomb (`slidePath`); it picks up what it passes (coins, charges, an uncovered gem),
1570 crashes into an enemy on the way (a life lost, like Run's) and burns in flame on the way unless
1571 flame can't hurt; the open exit stops it there. Highlighted is the end tile, 2 or more away
1572 (next to the player is a step). A teleport to the same tile wins (it can't crash or burn).
1573- **Keys:** with an action picked, a direction now takes the nearest tile that way it can use, at
1574 any distance (it tried 1 and 2), so F, I and L then a direction work.
1575- **Icons** (`tools/compose-icons.sh`, from Game Icons and Board Game Icons, packs already in use):
1576 RPG a bomb with a forward arrow, Proximity a bomb with a warning sign, Slide the Move figure with a
1577 two-way arrow, ICBM a bomb with a target.
1578- **The demo and clips:** four new scenes (rpg, proximity, slide, icbm; 47 in all), the finger in
1579 the RPG, ICBM and Slide clips; the almanac, the offer, the top bar and the texts know all four.
1580- Offers and drops draw from more kinds now, so old runs would replay differently: `SIM_VERSION`
1581 24. Tests: the new rules each (13), and every demo scene still found on every seed tried (40 per
1582 kind, the slowest 0.38 s). Checked in a browser: the four new demo scenes play.
1583
1584### 2026-10-24 — A slide glides
1585
1586User report: a slide looked jumpy, like half a dozen evenly spaced steps. Every move took a step's
15870.16 s whatever its length, so six tiles went by in about ten frames, each a big jump. A move of
1588more than 2 tiles that isn't a teleport (a slide) now takes 0.07 s per tile — 0.42 s for six —
1589eases out (fast at first, slowing as it stops), and is drawn crouched, with no hop and no walking
1590frames (`isSlide`, `moveProgress` in effects.ts; the "You" tag and the Shield's ring follow the
1591same curve). Steps and runs are as they were. Checked on a video of the demo's slide scene: the
1592player moves a little less each frame down to the stop, crouched.
1593
1594### 2026-10-24 — A tap on a slide's end slides
1595
1596User report: tapping a tile a slide reaches, with nothing picked, walked there by auto-move
1597instead of sliding. A tap's meaning (`tapMeaning`) now has a slide after a step and a diagonal
1598step and before auto-move's walk: the tile where a slide stops, with Slide held, is a slide. A
1599tile that is also a teleport target still slides only if picked with Move (there a teleport wins,
1600being safe); with nothing picked, a tile you can walk to was never a teleport anyway. The demo's
1601slide scene now shows the plain tap (its caption: "tap where you stop").
d603166d 2026-10-25 Pavel Safronov1602
1603### 2026-10-24 — No Workshop
1604
1605User's call: drop the Workshop, the permanent upgrades between runs planned since the start
1606(decision 8): "I can't imagine cross-game permanent upgrades that wouldn't ruin the whole thing."
1607Every run now starts the same, and nothing a run gains outlives it. Nothing of it was built; the
1608plan's mentions are gone (PLAN.md §7 says why). One knock-on, flagged to the user: Keep or Fresh
1609(§9) was balanced on coins being worth more than their shop price, as leftovers would become
1610Workshop progress; without it, Fresh's coin payout needs another look when §9 is built. Leftover
1611coins at the end of a run now count only on the high-score board.
1612
1613### 2026-10-24 — No Keep or Fresh; nothing carried between runs
1614
1615User's call, on the flag raised with dropping the Workshop: no leftover coins carry over between
1616runs, and a retry — "fresh or re-run" — is just another try at the same board, "no added benefit
1617(except their knowledge)". So the Keep or Fresh choice after a death (decision 9: keep the run's
1618power, or reset it for a coin payout) is dropped too, with its payout formula and its open
1619questions. What is built already matches: a death with lives left restarts the level from its
1620seed with everything the run had, and game over's Same dungeon starts the seed again from level
16211 with nothing carried over. The rest of the plan no longer mentions it (§0, §5, §5b, §9, the run
1622summary, the certificate, §14, the architecture and the milestones).
e78b7781 2026-10-25 Pavel Safronov1623
1624### 2026-10-24 — The shop
1625
1626User request: the next thing, the shop. The user chose from the options offered:
1627
1628- **Every few levels**, not after each one: after levels 3, 6, 9, 12, 15 and 18. The interval (3)
1629 is my reading of "every few"; it is one constant (`SHOP_EVERY`). It opens when the player goes on
1630 from the level-cleared banner, before the next level starts, and **Next level** leaves it.
1631- **Power-ups and charges** (recommended): 4 items, drawn by the gem's rarity weights, charges at
1632 weight 7 (about an uncommon power-up); never a power-up already held, nor a Coin bag.
1633- **Prices by rarity** (recommended): 6 for Bomb +1 and Fire +1 up to 16 for Teleport and Proximity
1634 alert and 18 for a Heart; charges 6–9. A first guess to tune: the test run found to reach the
1635 shop earned 5 coins in 3 levels (one par bonus, no drops picked up), so prices may well be
1636 too high.
1637- **Restock, getting dearer** (recommended): 2 coins, then 3, 4… within a shop.
1638
1639How it works: `continue` from a cleared level that is due a shop opens it (`state.shop`); `buy`
1640and `reroll` are new actions in the run record, taking no turn; `continue` again starts the next
1641level. The draw is seeded by the seed, level and restock count. The dialog follows the gem's
1642offer (select a card, its clip plays above, then Buy), with buttons that keep it open
1643(`keepOpen` in dialog.ts) and are greyed when unaffordable. A saved run closed in the shop reopens
1644in it. `SIM_VERSION` 25 (going on from level 3 now opens the shop). Tests: when it opens and
1645leaves, the draw (no held power-up, no Coin bag, seeded), buying (coins, at once, kept, once only),
1646too dear, the dearer restock, no board actions while open. Checked in a browser: reaching the shop
1647after level 3, selecting, restocking (5 → 3 coins), leaving to level 4; a new `shop` screenshot.
1648
1649### 2026-10-24 — The shop leaves room for its items
1650
1651User report: the shop's three big buttons (Buy, Restock, Next level, stacked) and its clip left
1652little room for the items, so it needed scrolling all the time. Now the **selected item has its
1653own Buy button**, inside its card ("Buy for 6", or "Need 1 more" greyed when too dear), and the
1654dialog's buttons, **Restock and Next level, share one short row**; the shop's clip is smaller (at
1655most 200 px wide, from 300). The cards are no longer buttons (a button can't hold another), but
1656selectable boxes that the keyboard reaches too (Enter or Space). On a 390 × 844 phone the four
1657items now show with only the last one's bottom lines below the edge, where before only about two
1658did.
1659
1660### 2026-10-24 — The ICBM flies
1661
1662User request: the ICBM was instant; make it a smoothly flying bomb. The rule stays (it goes off
1663the turn it is fired, so nothing can step out of the way); only the drawing changes. Since the
1664bomb has already exploded in the state the screen shows, the effects keep a **flight** (from the
1665player to where it lands: a bomb in a high arc, turning once, 0.3 s plus 0.06 s per tile) and a
1666time, `blastAt`, when the turn's blasts show. Until then the flame, the lingering flame, crates
1667bursting, enemies dying, drops and gems burned or uncovered, the exit opening and enemies let out
1668are held back — crates and enemies stay drawn as they were — and then play as usual. The danger
1669overlay already shows where the blast will land while the bomb flies. Checked on a video of a
1670launch on a test board: the bomb arcs across the row and the blast starts as it lands.
1671
1672### 2026-10-24 — Longer animations for rare or involved turns
1673
1674User's guidance: "some turns can take longer than others. If we have a charge or something rare or
1675involved, it's ok to take a bit more time and animate a more complex change of state." Recorded
1676in PLAN.md §13 (Animation timing) as a rule for what comes next: ordinary turns stay at a step's
1677pace; rare ones may play out in stages, the sim still settling the turn at once and the drawing
1678holding back what comes later, as the ICBM's flight does. Input stays unlocked, as decided on
16792026-10-22; the longer an animation, the more a quick tap can start the next turn over it — to
1680watch for, and lock only if it looks wrong. First use beyond the flight itself: an ICBM that
1681catches the player now shows the hit pose when it lands, not when it is fired.
1682
1683### 2026-10-24 — The ICBM speeds straight, trailing smoke
1684
1685User report: despite the name, the ICBM flew in a curve, as if thrown. It now flies straight along
1686its line, picking up speed (its distance grows with the square of the time) — 0.25 s plus 0.05 s
1687per tile — trailing seven puffs of grey smoke that grow and fade behind it; no arc, no spin.
1688Checked on a video of a launch: the bomb leaves the player slowly, speeds along the row with the
1689smoke behind it, and the blast starts as it arrives.
1690
1691### 2026-10-24 — Diagonal widens Teleport into a square
1692
1693User question: with Teleport held, what should buying Diagonal do? Teleport (3 steps across plus
1694down, over anything) already reached every diagonal step, squeezes included, so Diagonal added
1695only a one-tap diagonal step and the Q E Z C keys. Of the two options offered — widen Teleport, or
1696stop offering Diagonal to a Teleport holder — the user chose to widen it, "so it doesn't waste a
1697power-up if the order is weird": with Diagonal, Teleport counts a diagonal step as one
1698(`teleportSteps`), so its reach is a square, 7 × 7 (9 × 9 with Run too), instead of a diamond.
1699Run gives Teleport length, Diagonal gives it shape. Diagonal's own steps and keys are unchanged.
1700`SIM_VERSION` 26. Test: a tile 3 across and 3 up is out of reach without Diagonal, in with it.
0b63d158 2026-10-25 Pavel Safronov1701
1702### 2026-10-24 — Coins and prices for a reasonable shop
1703
1704User request: make the shop reasonable, "perhaps leaning towards cheap", working out the coin
1705gains. Before, a level paid almost nothing but its par bonus: 2 Blobs at a 20% coin chance (1.7
1706coins a drop on average) give under 1 coin a level, so three levels earned about 2 coins plus 5 for
1707each level cleared within par, and a playtest reached the first shop with 5 coins, short of the
1708cheapest item (6). Three changes, worked out in PLAN.md §8 (the coin budget):
1709
1710- **Every cleared level pays 2 coins** (`CLEAR_BONUS`), so even a slow player gets a baseline; par
1711 pays 5 more, as before.
1712- **Enemies drop coins more often:** Blob 60% (was 20%), Seeker 80% (50%), Drifter 50% (20%),
1713 about 1 coin a kill. Charge chances are unchanged.
1714- **Prices roughly two thirds of before:** Bomb +1 / Fire +1 4, Fuse +1 5, most abilities and
1715 passes 6, Slide / RPG / Detonator 8, Flame Pass 9, Teleport / Proximity alert 10, Heart 12, Star
1716 6, charges 3–6; a restock 1 coin, then one more each time.
1717
1718The first shop now sees about 11 coins (two cheap things) to about 26 within par (three or four).
1719The banner says what the level paid: "+2 coins." or "+7 coins, under par!". `SIM_VERSION` 27.
1720
1721### 2026-10-24 — Sounds and vibration
1722
1723User request: "add sounds and vibrations (where possible)". Built as decided on 2026-10-20: the New
1724Platformer Pack's 10 effects, plus two from the UI Pack (a current pack, so no new pack was
1725brought in): `tap-a` for every button, `switch-a` for the gem's choice and a restock. ZzFX, built
1726into LittleJS, for what the packs lack: the blast, a bomb set down, the ICBM's launch.
1727
1728- **MP3, not the packs' Ogg:** older Safari versions can't decode Ogg Vorbis, so iPhones a few years
1729 old would have been silent. Converted with ffmpeg (`-q:a 4`), same names.
1730- **Timed with the animation:** sounds go off when what they belong to shows — a blast and its
1731 kills at `fx.blastAt` (an ICBM's on landing), a pickup when the walk ends — not when the sim
1732 settles the turn. A chain is one louder boom, not several at once.
1733- **Vibration where the browser has it:** `navigator.vibrate`, which Android browsers have and
1734 iOS Safari doesn't (no web page can vibrate an iPhone); the menu's switch only shows where it
1735 works. Short buzzes for blasts (longer for chains), a hit, a broken shield, pickups, a cleared
1736 level.
1737- **Switches in the menu**, applied at once and remembered in this browser; both on by default.
1738- **Silent demo and clips:** they loop by themselves and would be noise; only turns the player
1739 plays make sound.
1740- **No music**, and no walking sound (every turn would tick). The ZzFX parameters were chosen
1741 without listening (the shell has no audio): to tune by ear.
1742
1743### 2026-10-24 — A one-line Developer description
1744
1745User request: with Developer mode on, its long description crowded the dialogs. In the game it now
1746reads only "A special gem lies next to you on every level." (the New run dialog, and the modes
1747tag's explanation). The details — every power-up not held and every charge, the level's own gem
1748still hidden, the red border on achievements — stay in the README.
1749
1750### 2026-10-24 — Android Chrome at half a frame a second
1751
1752User report: on Android Chrome the game ran at about 0.5 fps, locally and on GitLab Pages, while
1753Firefox on the same phone ran smoothly. The likely cause: the CRT's `PostProcessPlugin` was on all
1754the time, a pass-through in play, and every frame it uploaded the whole WebGL canvas into a
1755texture (`texImage2D` from `glCanvas`). Chrome does that by reading the frame back from the GPU
1756and sending it up again, which stalls the GPU each frame — at a phone's 3× pixel ratio, ~10 MB a
1757frame. (Headless Chrome warned "GPU stall due to ReadPixels" too.)
1758
1759- **Fix, part 1:** `postProcess.enabled` (LittleJS 1.26.1): the plugin is made once with the CRT
1760 shader and is **enabled only while the demo runs**, so play does no copy at all.
1761- **Fix, part 2, the demo:** LittleJS 1.27.1 copies the frame on the GPU (`copyTexImage2D`) instead
1762 of uploading the canvas. **Upgraded to the latest, 1.28.0** (2026-10-20, 4 days old), at the
1763 user's request ("let's pick up the latest version … it slows down the entire phone"): an
1764 explicit exception to the two-week pinning rule of 2026-10-19, which otherwise stands. The
1765 breaking changes of 1.26–1.28 (video capture and the Electron example removed; 3D cameras moved
1766 to a plugin) touch nothing here.
1767- **Checked** by counting calls in Chromium: in play, no canvas copies or readbacks per frame (was
1768 one upload a frame); in the demo, one GPU-side copy a frame and no upload.
1769- **Still to watch:** a dialog's clip copies its room from the WebGL canvas into its own 2D canvas
1770 with `drawImage` each frame (ui/clip.ts). Chrome may do that on the GPU or by reading back,
1771 depending on the canvas; only while a clip shows.
1772- Not yet checked on the phone (no Android device here): the user is to confirm.
1773- **Follow-up (same day):** still slow on the phone with 1.28.0. User request: an FPS count. Added
1774 `ui/fps.ts`, a meter in the bottom left (in `npm run dev`, or `?fps`): frames a second, the
1775 slowest frame, the game's own update-and-draw time per frame (so a slow frame with little game
1776 time points at the browser or GPU), the canvas size and pixel ratio. And three switches to
1777 narrow it down on the device: `?gl=0` (Canvas 2D), `?ratio=1` (no high-density canvas),
1778 `?noaa` (no WebGL antialiasing). In headless Chromium (software GPU) the default ran at about
1779 44 fps and each switch at 60, so antialiasing is one suspect; the phone will tell.
1780- **Found (same day):** the user's measurements on their Android phone in Chrome, with the meter:
1781 12 fps as it was (game code 0.7 ms a frame, so the time was the browser's), **60 with `?noaa`**,
1782 52 with `?ratio=1`, 60 with `?gl=0`, the same in the demo and in play; Firefox on the same phone,
1783 and Chrome, Firefox and Safari on the desktop, ran at 60 throughout. So it was **WebGL's
1784 multisample antialiasing**, which that phone's Chrome couldn't afford at full resolution.
1785 **Antialiasing is now off** (`glSetAntialias(false)`); `?aa` turns it back on, for comparing.
1786 The board is sprites on a square grid, which antialiasing hardly smooths: the screenshots look
1787 the same. The meter and the switches stay, for the next slow device.
1788- **After the fix (same day):** smooth on the phone. The meter no longer shows by itself in
1789 `npm run dev` (user request), only with `?fps`. No automatic tuning to the device (user
1790 question, answered): the game draws a few hundred sprites, which any phone of the last years
1791 manages at 60 fps once antialiasing is off; the cost was one setting, not the hardware's
1792 limits. Such tuning (e.g. dropping the pixel ratio after a few slow seconds) is to add only if
1793 a slow device turns up, measured first with `?fps` and the switches.
565a6873 2026-10-25 Pavel Safronov1794
1795### 2026-10-24 — Sprinter, Ghost, Wisp and Hunter
1796
1797User request: "can you add more enemies next?" Built the four of PLAN.md §10 (the Reaper stays for
1798later), with the sprites §13 had chosen (mouse, block slime, fire slime, frog). The table's speeds
1799had to fit plans of one move per turn, which the arrows, the danger overlay and Proximity alert
1800all read:
1801
1802- **Every other turn is a rest**, a planned move to the tile it is on (a dot among the arrows;
1803 eyes closed). Plans keep alternating as they are topped up; with nothing kept, the turn's
1804 evenness and the enemy's id decide, so a group doesn't all move on the same turns. A Freeze
1805 holds the plan as it is, rests included.
1806- **A Sprinter's move is a dash of 2 tiles** in a line (drawn as two arrows), 1 if the second
1807 tile is blocked. The middle tile counts: a bomb there stops the dash, and the player there is
1808 caught (the Sprinter stops on them). Auto-move's safety check doesn't step onto either tile.
1809- **Ghosts and Wisps go through crates**, as the NES's Kondoria and Ovapi went through soft
1810 blocks; drawn see-through inside one. Flame stops on a crate and burns it, so it kills one
1811 inside the first crate it reaches but not one behind it.
1812- **A new type every 3 levels**, not 6, so a 20-level run meets all seven by level 19. This moves
1813 the Seeker to level 4 and the Drifter to 7.
1814- **Drops** follow the table's words: Sprinter and Wisp mostly a charge, Ghost coins, Hunter both.
1815
1816`SIM_VERSION` 28. Tests in `test/new-enemies.test.ts`.
b80fa925 2026-10-25 Pavel Safronov1817
1818### 2026-10-24 — Five proposals written into the plan (PLAN.md §19)
1819
1820User request: think through five ideas, propose, and add them to the plan, to prioritise once the
1821plan is agreed. Written as §19, each with its open questions: a balance lab (bots and paired
1822experiments on the deterministic sim, since a full DFS/BFS of whole games is beyond reach: about
182310^50 leaves; search is used a few turns ahead instead), bigger scrolling boards, hazards and
1824objects (oil, barrels, bushes, metal crates, spikes, then acid, water, conveyors, ice, cracked
1825floor, springs), the Acid bomb that corrodes pillars and brings the roof down, and a Gauntlet mode.
1826Two conflicts with earlier choices are flagged there, not settled: bigger boards undo the one-size
1827board of 2026-10-23, and bushes that hide things would break "everything visible" (§4). The
1828Splat Pack, which the user named, would be a new art pack.
1829
1830### 2026-10-24 — Answers on the §19 proposals
1831
1832The user's answers, recorded in PLAN.md §19:
1833
18341. **Balance lab:** the full version, with bots.
18352. **Boards:** a fixed view with the same number of tiles on every screen (bars beside it on a wide
1836 one), boards bigger in both directions, a view that follows the player, and edge indicators.
1837 On the user's question, a "concentration" scheme was proposed: enemies weighted by danger,
1838 divided by open tiles, rising along a curve fitted by the lab's bots to a target difficulty;
1839 board size then set by how long a level takes. The largest size is left to that measurement.
18403. **Objects:** all wanted; the Splat Pack is approved. On hiding things in bushes, five approaches
1841 were set out with their pros and cons; recommended: bushes hide drops and the player (from
1842 chasers), and enemies inside show faint — to choose.
18434. **Rubble:** crushes what is under it and stays impassable for good. Two new deaths: crushed,
1844 and trapped (the level can no longer be won), which costs a life at once.
18455. **Gauntlet:** one life back per cleared level up to 3, endless, and no passes in a loadout. The
1846 passes in the whole game are to leave the power-ups or become temporary charges: the balance
1847 lab's first experiment decides which.
1848- **Final answers (same day):** bushes B + D + E as recommended; the 9 × 13 view; one acid bomb
1849 cracks a pillar and a second hit brings the roof down 4 turns later. The plan is agreed; the
1850 balance lab comes first.
d3e4ed26 2026-10-25 Pavel Safronov1851
1852### 2026-10-24 — Proposal: one charge at a time (PLAN.md §19.7)
1853
1854User idea: carry one charge only; picking up another asks to sell the new one for coins or swap
1855it for the one held. Worked out as a proposal, with three questions for the user: whether the
1856swapped-out charge is sold or dropped, whether Freeze and Decoy become carried charges, and whether
1857a charge should be used deliberately (a second bomb button) — which would also fix the Nuke trap
1858the balance lab found. Not built.
1859- **Answers (same day):** the swapped-out charge is sold; Freeze and Decoy become carried charges;
1860 charges are used deliberately with their own button (Shield stays passive). To build right after
1861 the balance lab's first report.
1862
1863### 2026-10-25 — The balance lab, and its first report
1864
1865Built as agreed (PLAN.md §19.1): `tools/balance/`, bundled with Vite into `.balance/lab.mjs` (the
1866sim is TypeScript; Node can't import it directly) and run on worker threads, one per core but one.
1867Choices made while building:
1868
1869- **Three skills of one bot**, not three bots: they differ in how many turns ahead they check they
1870 can survive (careful: as far as the enemies' plans and the bombs' fuses, up to 6; average 2;
1871 sloppy 1) and how often they slip (0, 3%, 8% of turns: some action that doesn't kill at once).
1872 The survival check plays the real rules (`step()`), moving and waiting only.
1873- **What it is after:** the open exit; else an uncovered gem or a drop; else a tile next to where a
1874 bomb would catch a crate or an enemy's route, with a walk out of the blast in time. A tile it keeps
1875 returning to costs a little more each time, so it moves on rather than waiting for ever.
1876- **Only what a player sees:** the hidden exit and gem aren't used.
1877- **Paired experiments:** the same seeds per arm; differences with 95% intervals.
1878- **Gem and shop:** the bots take the first thing a gem offers, and don't shop (coins aren't used).
1879
1880The first report's findings are in PLAN.md §19.1. The biggest: Teleport and Flame Pass turn a weak
1881player into a winner; Proximity alert costs a strong one 3 levels, by turning the defensive bomb
1882into a trap; and Nuke charges can make a level impossible (the Nuke trap).
1883
1884### 2026-10-25 — One charge at a time, used on purpose (built)
1885
1886Built as agreed (PLAN.md §19.7, §3.3). Choices made while building:
1887
1888- **The slot** is `state.charge` (kind and uses left); `boosts` and `shield` are gone. Shield is a
1889 charge in the slot that the hurt check spends.
1890- **Uses:** the charge button (key V, after B for Bomb, since C is a diagonal step) stands for
1891 `charged` (a Pierce, Power or Nuke bomb), `icbm`, `freeze` (acts at once, like Wait) and `decoy`
1892 (on the player's tile or a free one next to them). It is hidden with no charge, or a Shield, and
1893 greyed while it can't be used (no room for another bomb). ICBM left the Bomb button.
1894- **The choice** is `chargeOffer` with `sellCharge` / `swapCharge`, taking no turn, like the gem's.
1895 A second different charge picked up in the same turn, while one is waiting, is sold. A death that
1896 turn drops the choice (the retry has the charge lying there again). From a gem or the shop the
1897 charge goes straight in, the old one sold: those are chosen on purpose.
1898- **Price:** half the shop price, rounded up, scaled by the uses left (2–3 coins for a full one).
1899- **Drops:** with every charge now carried to the next level, the last enemy may drop any charge
1900 (it dropped only the lasting ones); a Decoy, Freeze or Shield isn't dropped while one lies on the
1901 board.
1902- **The demo** skips any turn that would ask the choice (no gesture plays a dialog), and its
1903 captions name the charge button.
1904- **A fix on the way:** a run resumed with a choice open (a gem's, a charge's, or the shop) lost
1905 its dialog when the opening demo ended; it now opens again.
1906- `?charges=n` is now `?charge=<kind>`. `SIM_VERSION` 29.
1907
1908### 2026-10-25 — Proximity alert: only an enemy next to the bomb
1909
1910User request, after the balance lab found Proximity alert cost the careful bot 3 levels: a bomb
1911goes off only when an enemy is on one of its 4 neighbouring tiles, not anywhere in its reach. A
1912bomb dropped between you and an enemy coming down a corridor now waits until the enemy is at it.
1913- **Follow-up (user report, 2026-10-25):** in Developer mode a Decoy chosen from the rainbow gem
1914 replaced a held Nuke without asking. Now every charge that comes along — picked up, chosen from a
1915 gem, or bought (the shop waits for the answer, then comes back) — asks when another kind is held.
1916 And the charge button is always shown (user request): the charge and its uses, or an empty dashed
1917 slot; greyed out while it can't act. "What is that?" on it explains the button, carrying one at a
1918 time and the sell-or-swap choice; to make that possible, "What is that?" now works on greyed-out
1919 buttons too.
1920- **Shield and Decoy (user request, 2026-10-25):** "why does shield work by itself?" — now it is
1921 raised with the charge button like the rest. Raised, it stays up **3 turns** (the turn included;
1922 `SHIELD_TURNS`, my choice, to tune) and absorbs the next hit; carried, it does nothing. Without a
1923 limit it would be the old passive Shield behind one tap. The **Decoy** goes up to 3 tiles away in
1924 a straight line, each tile on the way free (floor, no bomb, enemy or flame), as a slide or kick
1925 stops at whatever is in the way; no longer on the player's own tile. `SIM_VERSION` 30.
1926- **No time limits (user request, 2026-10-25):** 3 turns was very short for both. A raised **Shield**
1927 now stays up until a hit lands, however many levels later (`shieldUp`, carried with the run). A
1928 **Decoy** lasts until the level ends, or until it takes a single hit: an enemy reaching its tile,
1929 or flame. `SIM_VERSION` 31.
0bfdc8e6 2026-10-25 Pavel Safronov1930
1931### 2026-10-25 — The demo's title on top; a compact menu
1932
1933Built from the queued UI items (user request). **The demo** reads top to bottom: the title
1934EXPLODORAMA! in its own bar, the TV (top bar included, moved down under the title), then the
1935caption; title and caption go when the game starts. **The menu**: Resume and New (was "New run")
1936side by side, then Almanac, Scores, Demo and Bug as one row of small word buttons (thinner edges so
1937ALMANAC fits a quarter row on a phone). Words rather than icons: they say what they do at a glance,
1938and the Kenney icons for an almanac or a bug report would need explaining.
1939
1940### 2026-10-25 — A demo scene for the charge swap
1941
1942User question: "is there already a demo written for the charge swap?" There wasn't: the demo
1943skipped every turn that would ask, since its finger only played the board and the action bar. Now a
1944`chargeSwap` scene starts carrying a Pierce with a Nuke lying near; when the pickup asks, the real
1945dialog opens over the scene (not dimming the caption, as "What is that?" does) and the finger taps
1946**Take Nuke**. A new gesture, `choice`, plays it (`swapCharge` / `sellCharge`); in a dialog's clip,
1947which has no dialog, the choice just happens. Only this scene keeps turns that ask. No separate
1948charge-button scene: each charge's scene already shows the finger pressing it.
e6bb7c42 2026-10-25 Pavel Safronov1949
1950### 2026-10-25 — Bigger boards, a fixed view, enemies by concentration (built)
1951
1952PLAN.md §19.2, as agreed. Choices made while building:
1953
1954- **Sizes:** 9 × 11 for levels 1–3, then a step every 3 levels to 15 × 21 from level 16 (growing in
1955 both directions, more in height, as phones are tall). The largest is a first guess; the lab is
1956 to measure how long levels take.
1957- **The view:** 9 × 13 tiles everywhere, the bars round it the page's dark ink (`#view-mask`, a box
1958 whose shadow covers the rest). A tap outside the view does nothing, so nothing unseen is tapped.
1959 On a phone the 9 columns set the tile size, so the bars are above and below; on a desktop, beside.
1960- **Following** is eased (a fifth of the way each frame) and stops at the board's edges.
1961- **Indicators:** enemies and bombs with a red arrow, the exit and gem with a white one; a picture on
1962 a pale disc just inside the edge, toward the thing.
1963- **Enemies by concentration:** threat per open tile, from 1/26 (2 Blobs on level 1, as before) to
1964 1/11 on level 20; the weights by type are guesses for the lab to fit. Par grows with the board:
1965 10 + 0.3 a tile + 8 an enemy (41 on level 1, as before).
1966- **Screenshots:** the tool asks the game where a tile is (`window.tilePoint`), since the board
1967 scrolls; and `?shop` (with `?level=3`) opens the shop directly, since the key sequence that played
1968 through level 3 stopped fitting the new enemy counts.
1969- `SIM_VERSION` 32.
1970
1971### 2026-10-25 — Bigger boards, a fixed view, enemies by concentration (built)
1972
1973PLAN.md §19.2, as agreed. Choices made while building:
1974
1975- **Sizes:** 9 × 11 for levels 1–3, then a step every 3 levels to 15 × 21 from level 16 (growing in
1976 both directions, more in height, as phones are tall). The largest is a first guess; the lab is
1977 to measure how long levels take.
1978- **The view:** 9 × 13 tiles everywhere, the bars round it the page's dark ink (`#view-mask`, a box
1979 whose shadow covers the rest). A tap outside the view does nothing, so nothing unseen is tapped.
1980 On a phone the 9 columns set the tile size, so the bars are above and below; on a desktop, beside.
1981- **Following** is eased (a fifth of the way each frame) and stops at the board's edges.
1982- **Indicators:** enemies and bombs with a red arrow, the exit and gem with a white one; a picture on
1983 a pale disc just inside the edge, toward the thing.
1984- **Enemies by concentration:** threat per open tile, from 1/26 (2 Blobs on level 1, as before) to
1985 1/11 on level 20; the weights by type are guesses for the lab to fit. Par grows with the board:
1986 10 + 0.3 a tile + 8 an enemy (41 on level 1, as before).
1987- **Screenshots:** the tool asks the game where a tile is (`window.tilePoint`), since the board
1988 scrolls; and `?shop` (with `?level=3`) opens the shop directly, since the key sequence that played
1989 through level 3 stopped fitting the new enemy counts.
1990- `SIM_VERSION` 32.
1991
1992### 2026-10-25 — Flame Pass and Teleport to become charges (scheduled)
1993
1994From the lab's numbers (PLAN.md §19.1), the user decided: **Flame Pass becomes a charge counted in
1995flame hits** — it saves the player from 3 fire hits, then runs out (not a number of turns, as
ebcd0a8f 2026-11-02 Pavel Safronov1996proposed) — and **Teleport a charge of 3 jumps**. **Crate Pass and Bomb Pass stay power-ups** for
e6bb7c42 2026-10-25 Pavel Safronov1997now, to be playtested more. Scheduled as the next task, after the bigger boards (being playtested
1998by the user). Backlogged in PLAN.md §18.
1999
2000### 2026-10-25 — Shield and Freeze: a confirming tap
2001
2002User request: the Shield went up the moment the charge button was pressed; add a confirmation.
2003Shield and Freeze are now aimed like the other actions, at one tile — the player's own: the charge
2004button picks them and lights that tile, and a tap on yourself uses them. Pressing the button again
2005also confirms (for these two only; for other actions a second press puts the action away), so the
2006keyboard needs only V, V. It is the same two steps as every other action, with nothing new to
2007learn. `SIM_VERSION` 33 (the actions now carry a tile).
2008
2009### 2026-10-25 — Auto-move's way as a line of arrows
2010
2011User request: the blue tiles of auto-move's way weren't enough; arrows on them, forming a line.
2012Now a line runs from the player through the middle of each tile of the way, with an arrow on each
2013tile pointing the way that step goes (`drawPath` in render/board.ts). A darker blue than the tiles,
2014so it reads over them; the first step's line and arrow turn red when that step isn't safe, as its
2015tile already did. Drawn under the pieces, so the player and enemies stay on top.
2016- **Follow-up (user report, same day):** with Run, every tile of the way lit up though only every
2017 other one is landed on. The way drawn is now where auto-move will land, move by move
2018 (`autoRoute` in sim/path.ts: `autoMoveAction` played out on the player's position, the first move
2019 only where it is safe now): a Run's landings, a Teleport's, or each step. The walk itself
2020 (`playerPath`) still decides auto-move's moves and when it ends. When the first move can't be
2021 made now, the walk is drawn as before, its first tile red.
2022
2023### 2026-10-25 — The shop's coins stay in view
2024
2025User report: scrolling the shop's items moved "You have N coins" out of view. The coins line now
2026sticks at the top of the shop's body, and the clip sticks just under it, so both stay while the
2027items scroll.
2028
2029### 2026-10-25 — Dragging the board moves the view
2030
2031User report: a swipe on the board, meant to look around, teleported the player and a turn went by
2032(the press landed on one tile and the release on another, which with nothing picked is a tap on
2033that tile). Now, with nothing picked, a press that moves more than 12 CSS pixels is a drag: it
2034moves the view (kept inside the board) and letting go does nothing, nor does a long press arm a
2035bomb. The next turn brings the view back to the player, eased. With an action picked, sliding
2036still changes the tile chosen, as before (the commit preview). When the view is dragged off the
2037player, an edge arrow points back to them. Every player can drag the same way, so the fixed 9 × 13
2038view stays fair.
2039
2040### 2026-10-25 — Cards show what they change, in numbers
2041
2042User request: when a gem or the shop offers, say, Fire +1 to a player who already has one, show its
2043impact ("fire 2 → 3"). Each card of the gem's offer and the shop now has a line under the name:
2044**Blast 2 → 3**, **Bombs 1 → 2**, **Fuse 3 → 4**, **Lives 3 → 4**, **Coins 5 → 15**, a Star's turns
2045when one is running, and a charge's uses when the same kind is carried. The current value, then
2046the new one: one step, since an offer or a shop never holds two of the same thing. Abilities and
2047passes have no number, so no line (`changeOf` in ui/info.ts).
2048
2049### 2026-10-25 — TNT, and a limit on every stat
2050
2051User request: "make tnt a power-up that gives us 2 increases", and a maximum, "to limit
2052ridiculousnesses like board-spanning bombs". **TNT** raises the blast by 2 (its icon: the fireball
2053with Fire +1's badge; weight 6 against Fire +1's 20, price 7 against 4). **Bombs, blast and fuse
2054each stop at 5** (first guesses, to tune). A stat power-up that would be cut short isn't offered by
2055a gem or the shop, so nothing is half-wasted; cards and the top bar's explanations show the limit.
2056On cards the user asked whether to show the history ("1 → 2 → 3"): the current value and the new
2057one, with the limit, say what matters for choosing ("Blast 2 → 4 (max 5)"). `SIM_VERSION` 34.
2058
2059### 2026-10-25 — The almanac's tabs fit, and stay on top
2060
2061User report: on a phone the three tabs ran off both ends of the row, and scrolled entries showed
2062through them. The tabs now share the row equally at a smaller size (10 px Kenney Future), and the
2063sticky tab row sits above the entries scrolling under it.
2064
2065### 2026-10-25 — Auto-move uses Diagonal
2066
2067User report: with Diagonal, auto-move walked round corners it could cut. The walk (`playerPath`)
2068now takes diagonal steps too when the player has Diagonal — squeezes included, as the action
2069allows — and a diagonal first step is taken as the Diagonal action. Among equally short ways the
2070search keeps its order (straight steps first), so the number of moves is what is saved.
fcf44f32 2026-10-25 Pavel Safronov2071
2072### 2026-10-25 — Flame Pass and Teleport become charges; a death drops the charge; boards fitted
2073
2074The user's order: stages 1–3 of the plan.
2075
20761. **Flame Pass and Teleport are charges.** Flame Pass is raised with the charge button and a tap
2077 on yourself, like the Shield, and then takes the next **3 fire hits** — a blast, a chain or
2078 lingering flame each use one; every flame in that turn counts as that one hit — whatever the
2079 level, until used. While raised the player may also walk into flame (each turn there uses a
2080 hit). Teleport gives **3 jumps** from the charge button, with its reach as before (Run 4,
2081 Diagonal a square). It left the Move group, the meaning of a tap (a far tile only Teleport
2082 reached was a teleport) and auto-move, so a jump is only ever spent on purpose. Both power-ups
2083 are gone from the gem's offers and the shop's power-ups; the shop sells the charges for 7.
20842. **The boards, fitted with the lab** (40 seeds, careful bot). Up to 15 × 21, late levels took
2085 100–145 turns; capping at 13 × 17 (and stepping later) brings every level to about 75–95, in
2086 the 80–120 aimed at. A sparser board (concentration 1/11 at level 20) made the bot hunt its last
2087 enemy for long (stalls 8%); 1/9 keeps it at 3% and wins at 93%. The average bot still dies
2088 early (about 5 levels), as before: the careful bot is no measure of difficulty for people.
20893. **A death drops the charge carried** where the player fell, with what was left; the retry has
ebcd0a8f 2026-11-02 Pavel Safronov2090 it lying there (or on the nearest open floor, if the player fell inside a crate with Crate Pass).
fcf44f32 2026-10-25 Pavel Safronov2091 A raised Shield or Flame Pass is kept. A pending choice now carries how much is left too.
2092
2093`SIM_VERSION` 35. The lab now also measures each charge carried from level 1.
a313eb7d 2026-10-25 Pavel Safronov2094
2095### 2026-10-25 — Measured: Diagonal without squeezing
2096
2097User question: how would a no-squeeze Diagonal change things? A lab-only rule switch
2098(`RunOptions.lab.noSqueeze`, never set in play; the game is unchanged) makes a diagonal step need one
2099of the two tiles beside it open, for the action and for auto-move's walk alike. On 300 seeds: the
2100average bot gains +3.4 levels with it instead of +7.7 (wins +1% instead of +11%); the careful bot
2101+0.3 instead of +1.0, with more stalled runs (9%). So the squeeze is most of Diagonal's strength.
2102Left to the user to decide.
2103- **Decided (user, same day): no squeezing is the rule.** A diagonal step needs one of the two
2104 tiles beside it passable, for the action and auto-move alike. The lab keeps the old rule as a
2105 variant (`options.lab.squeeze`) to compare against. `SIM_VERSION` 36.
5f066960 2026-10-26 Pavel Safronov2106
2107### 2026-10-25 — Board objects (first batch) and the Acid bomb; the worn look switched off
2108
2109The user's order: "implement 2, 1, 3" — the worn dungeon, the first hazards, the Acid bomb.
2110
21111. **The worn dungeon** was built (`render/decor.ts`: Splat Pack stains in two of six looks per
2112 level, denser along walls) and then **switched off** at the user's word: the splats looked too
2113 alike and not the look wanted. The code stays, off, until another approach is chosen.
21142. **Bushes, metal crates, oil, spikes, oil barrels** (PLAN.md §19.3), from levels 2, 3, 5, 8 and
2115 11. Choices made in building (the plan left them open): bushes smoulder a turn before catching;
2116 enemies avoid spikes entirely rather than planning round raised ones; barrels burst into oil
2117 and a reach-2 blast the next turn, not counting to the player's bombs; metal is placed only
2118 where everything stays reachable.
21193. **Acid** (PLAN.md §19.4): one bomb per charge; any later blast breaks a cracked pillar; the pillar's
2120 tile becomes floor and the 8 round it fall 4 turns later; crushed and trapped ignore Shield and
2121 Star (rubble would leave the player inside a wall).
2122
2123`SIM_VERSION` 37. The balance lab (40 seeds, careful bot): wins 80%, 19.5 levels, 0.45 lives lost;
2124stalled runs 15% (9% before), no deaths on spikes; one trapped (the bot used a dropped Acid).
2125
2126### 2026-10-25 — Shield and Flame Pass work while carried
2127
2128User request: "while they are in the charge slot, they are active. No activation needed. When the
2129appropriate damage hits the player, the charge is used up, and the charge slot is once again empty."
2130So the `shield` and `flamePass` actions, `shieldUp` and `flameHits` are gone: the charge held is
2131the protection. A Shield's use goes on any hit but a falling ceiling; a Flame Pass carries
2132FLAME_PASS_HITS (3) uses, one per fire hit (and per turn walked in flame), the slot emptying with
2133the last — the 3 hits the user chose earlier the same day, kept. A death can still drop one (only a
2134ceiling or being trapped kills through a Shield). The charge button is greyed out while one is
2135carried; its "What is that?" says it works by itself. `SIM_VERSION` 38.
2136
2137### 2026-10-25 — A Shield pushes the player out from under a falling ceiling
2138
2139User request: under a falling ceiling, the Shield pushes the player to an empty spot; with none,
2140they die. Empty: open floor with no bomb or enemy, among the 8 tiles round them, the 4 straight ones
2141first (up, down, left, right), then the diagonals. The Shield is used up, and
2142absorbs the rest of that turn's hits as usual. A Star still doesn't help. Within `SIM_VERSION` 38
2143(not yet released).
2144
2145### 2026-10-25 — Danger "later": a bomb's tiles shown from the start
2146
2147User report: with a long fuse, a bomb's tiles weren't marked until its last turns (the overlay showed
2148only this turn and the next). `predictDanger` now also returns `later`: playing on from the next turn,
2149the player waiting and enemies left where they are, every tile burned (chains, oil, bushes included)
2150or crushed until nothing is left to go off (at most 12 more turns), minus what is already `now` or
2151`next`. Drawn as a red outline, so it reads as "not yet" beside the two fills. Not used by
2152auto-move's safety check or the bots, which still look at `now` only. No rule change.
6c3fcc31 2026-10-26 Pavel Safronov2153
2154### 2026-10-25 — Board scenes, the almanac's Board tab, and the lab's stalls
2155
2156User request: work on the open items.
2157
21581. **Demo scenes for the objects:** `bush` (stepping into one), `bushFire` (a smouldering bush
2159 catching), `metal` (a flame stopped short at one), `oil` (a spill burning at once), `spikes`
2160 (stepping on them while down), `barrel` (a burst barrel going off). A room gets a few of the
2161 object by the player (`placeObjects` in demo.ts); a scene's reset to turn 0 shifts the spikes'
2162 phase so they keep their rhythm. Found for every test seed like the rest.
21632. **The almanac's Board tab:** `BOARD_TEXT` in ui/info.ts, shared with "What is that?" so the two
2164 can't disagree. Four tabs don't fit a phone's row: two rows of two below 440 px.
21653. **The lab's stalls (15% of runs since the objects):** the traces (the board printout now shows
2166 the objects) found two causes. (a) The bot never bombed an oil barrel, so barrels shutting in the
2167 exit or the last enemy (with a metal crate on the other side) stopped it: a bot fault, fixed in
2168 the bot (barrels count as crates in its plans). (b) Its last Teleport jump landed in a one-tile
2169 pocket between two crates: nowhere to step, nowhere to put a bomb, stuck for good — a trap a
2170 person could fall into too. **Rule:** the last jump of a Teleport charge may only land on a tile
2171 with a passable tile next to it (or next to the player's own); earlier jumps may, since the next
2172 gets out (*replaced the next day by "boxed in costs a life", below*). `SIM_VERSION` 39. Now: stalls 3% (1 of 40, the bot dithering by an enemy, its old
2173 weakness), wins 90%, 19.1 levels, 0.53 lives lost.
2174
2175### 2026-10-26 — Measured: bombs under the player (the NES rule)
2176
2177User question: how would the original's placement — the bomb on the player's own tile — change
2178things? A lab-only rule switch (`RunOptions.lab.underfoot`, never set in play): Bomb (and a charged
2179bomb) goes on the player's tile; the player walks off it and can't walk back, enemies can't enter
2180it. The bots stand on the spot instead of next to it; nothing else of theirs changed.
2181`npm run balance -- underfoot` (reports in docs/balance/underfoot-*.md), 200 seeds:
2182
2183| Bot | Levels | Won | Lives lost | Turns per level | Turns in danger | Stalled |
2184|---|---|---|---|---|---|---|
2185| careful, next to (now) | 19.32 | 93% | 0.24 | 83.4 | 5.4% | 4% |
2186| careful, under (NES) | −0.56 ± 0.61 | −6 ± 5% ✱ | +0.12 ± 0.13 | +12.3 ✱ | +5.0 ✱ | 8% |
2187| average, next to (now) | 4.11 | 0% | 3.00 | 75.5 | 3.4% | 0% |
2188| average, under (NES) | +0.62 ± 0.47 ✱ | 0 | −0.02 | +9.2 ✱ | +9.3 ✱ | 1% |
2189
2190Read: slower (a bomb starts with you in its blast, and getting out takes turns), about twice the
2191turns spent in danger, own-blast deaths up for the careful bot; the weaker bot does a little better
2192(enemies can't step onto you while you stand on your bomb, and a bomb can't shut your only way
2193out). Half the careful bot's extra stalls are its refusal to spend a life: walled in by rubble
2194next to a crate it waited for ever, where a person would bomb out. That case is a **soft-lock under
2195the current rule** too: no free tile to put a bomb on, and the crate means it doesn't count as
2196trapped. Left to the user.
2197- **Decided (user, same day): bombs stay next to the player**, and the summary goes in the README
2198 (*Measured: bombs under the player*). The variant stays in the lab. With it: **boxed in costs a
2199 life** (`boxedIn` in actions.ts, death cause `boxedIn`): at the end of a turn, nothing available
2200 but Wait, and no bomb, flame, falling ceiling, smouldering bush or crumbling crate left that could
2201 change it. A Shield or Star doesn't help. It replaces the Teleport restriction of the day before
2202 (the last jump couldn't land in a dead end): the user would rather the player pay for the
2203 mistake, and it covers rubble boxing the player in next to a crate too. Lab: stalls 0% (40
2204 seeds), wins 93%.
ba8e4ea0 2026-10-27 Pavel Safronov2205
2206### 2026-10-26 — Bug reports to an address on fuzzyworld.net
2207
2208The project moved off GitLab (to code.fuzzyworld.net and apps.fuzzyworld.net), so GitLab Service
2209Desk is going away. `BUG_EMAIL` is now `bugs-k7q4+explodorama@fuzzyworld.net`: one forward-only
2210address shared by Pavel's apps, with `+explodorama` telling them apart. The random part makes the
2211address rotatable if it draws spam. (The old address was the *fuzzyworld* project's Service Desk,
2212not this game's.) The email itself is unchanged.
e3d8dc49 2026-10-27 Pavel Safronov2213
2214### 2026-10-26 — No tap-to-wait; deadly moves marked, and refused by the shortcuts
2215
2216User request: a tap on the player waited, which could be deadly; so could a tap in front of an
2217enemy or into flame. Asked: remove the tap-yourself shortcut, and "mark or disallow" moves that
2218guarantee a death, a button in red if pressing it would.
2219- **A tap on yourself does nothing.** Wait is its button (Space). The Freeze charge's
2220 confirming tap on yourself, with the charge button picked, stays.
2221- **Deadly** means the player loses a life **this turn**, found by playing the action on a copy
2222 with `step()` (`isDeadly`, sim/danger.ts), so it can't disagree with the rules: a Shield or Flame
2223 Pass taking the hit, or Immortal, isn't deadly. Only one turn ahead: "every follow-up dies too"
2224 (a dead end a blast fills next turn) would need a search, and later enemy moves aren't known yet.
2225- **Both mark and disallow, split by how deliberate the input is.** Shortcuts (a tap with
2226 nothing picked, direction keys with nothing picked, the long press, auto-move) refuse a deadly
2227 move, with a red flash and a buzz. Deliberate inputs only mark it: Wait turns red, a picked
2228 button's deadly tiles are red with a cross, and both still act. Disallowing those too could
2229 leave a player with no move at all (every choice deadly, e.g. boxed in), and dying on purpose
2230 is the player's call.
2231- The demo's gestures follow the same rule (a deadly step is shown as Move then the tile), so
2232 what it shows still works in play. No `SIM_VERSION` change: the rules are the same.
2233
2234### 2026-10-26 — Acid becomes an area weapon
2235
2236User report: an acid bomb placed diagonal to pillars left them untouched, since its flame was a
2237plain cross and only the first pillar an arm ran into cracked. Asked for "an area weapon, like the
2238nuke". Offered three readings (cracks in the Nuke's diamond with the usual cross of flame; the full
2239Nuke area; cracks in the 8 tiles round it); **the user chose the full Nuke area**: the acid bomb
2240burns every tile within `NUKE_RANGE` (2) steps, through anything, and every pillar in that diamond
2241is reached (cracked, or broken if cracked before). It is now a Nuke plus cracks in a 1-bomb charge
2242(the Nuke gives 3 bombs); its price (6) is unchanged, to revisit with the balance lab if it proves
2243too strong. The Nuke itself still breaks a cracked pillar only along its cross. `SIM_VERSION` 40.
05e79f5f 2026-10-27 Pavel Safronov2244
2245### 2026-10-26 — Bushes drawn from Foliage Sprites
2246
2247User request (2026-10-26): new bush art. Kenney's **Foliage
2248Sprites** (CC0; the user added the pack, 2026-10-26) supply white shaded shapes, which
2249`tools/compose-icons.sh` tints green (multiplied, so its shading stays), outlines in the art's
2250ink (`#353541`) and sizes to 128 px: four tall-grass tufts (`grass_a`–`grass_d`) and two round
2251canopies (`canopy_a`, `canopy_b`, composed but not used yet). A bush's picture is picked from its
2252cell index (`bushSprite`), so neighbours differ but a bush never changes picture between frames.
2253Credited in the menu and the README with the other packs.
5607bea2 2026-10-31 Pavel Safronov2254
2255### 2026-10-31 — Semver releases and an in-game list of changes
2256
2257User request (2026-10-31): the game gets semver versions, git-tagged releases, and a "what's new
2258since you last played" dialog with demo scenes and text. Decided with the user: **0.1.0 is the
2259base** (no history is rebuilt; the list starts there); the last version played is kept in
2260localStorage (`explodorama.lastVersion`); Claude keeps the version, `UNRELEASED` and the tags
2261consistent, **Pavel decides when a release happens**. `SIM_VERSION` stays as the rules version
2262(shown as "rules 40" beside the release) and is independent of it. The data lives in
2263`src/changelog.ts` (releases, each change with kind, date, text and optional clip); a first
2264visit only records the version and shows nothing; the dialog opens when the demo ends. Tapping the
2265version in the menu lists every release. A change reaching players goes into `UNRELEASED` as it
2266is finished; tagging moves them into a new release and bumps package.json (checked by
2267test/changelog.test.ts).
bb3823d2 2026-10-31 Pavel Safronov2268
2269### 2026-10-31 — Choices on a tap, crate approach, fireCharge, demo keys and shuffle (0.2.0 work)
2270
2271User requests, built together: (1) a tap on a crate walks to the tile beside it (`approachTile`;
2272with Run it did nothing); (2) a tap or arrow key on a bomb that two actions could answer asks which
2273(`tapChoices`, `tapMeaning` 'choose'): kick, walk onto it (Bomb Pass; a tap on a bomb with Bomb Pass
2274alone now walks too, it was refused), set it off. Walking onto an oil barrel stays a button-only
2275move. (3) The charge-swap dialog's third answer, `fireCharge`: use the charge held, then take the
2276new one (what remains of the held one is sold). Only a charge with nothing to aim can be used there,
2277which today is Freeze (aimed at the player's own tile); bomb charges, Decoy, Teleport and ICBM need a
2278target, so the third button is not shown for them (open: aim first, then swap?). No `SIM_VERSION`
2279bump: only a new action, which no old run holds. (4) Keys no longer end the demo, only a click or
2280tap; Tab/Enter/Space still work its buttons. A shuffle button (the dice, `explodorama.demoShuffle`) plays the
2281scenes in a random order (`scenePlan`), the choice kept across visits.
2282
2283### 2026-10-31 — Released and next copies; early adopters
2284
2285User questions, 2026-10-31: how to try `main` while everyone else stays on the tagged release, and
2286an incentive to do so. Decided: two deployed copies. Released: `fw deploy --ref <tag>` to
2287`/explodorama/`. Next: `fw deploy --as explodorama-next` from `main` (fuzzyworld-tools gained `--as`
2288and the vite plugin reads `FW_APP`, uncommitted at the time of writing). The copies share an origin,
2289so each has its own localStorage keys (`src/channel.ts`); one key, `explodorama.earlyRuns`, is
2290shared. The incentive is a teaser and a link, not a gameplay reward: the released menu shows "Try it
2291early", the Changes dialog "Coming next" (from the next copy's `next.json`), scores finished on the
2292next copy are marked "Early adopter" and counted. Rejected: giving the Developer rainbow gem on the
2293next copy (scores would stop being comparable; Developer stays a separate option for either copy).
2294Scores are per copy, as storage is.
b8849d36 2026-11-01 Pavel Safronov2295
2296### 2026-10-31 — Scripted-or-found demo scenes: found, with a push; timeline bar
2297
2298User question: should demos be scripted rather than found by random play? Kept: scenes are found by
2299random play on a random room (varied, and always by the real rules), but a scene can now **start
2300from a set-up state** (`prepare`: rubble starts with a pillar already cracked), **ask for a longer
2301run** (`maxSteps`) and **play a tail** after its moment (`tail`: waiting turns, while safe). Scripting
2302a whole sequence (Acid, then a second bomb, then waiting) was not needed once the set-up did the
2303first half. The Almanac's Rubble clip now plays the `rubble` scene, not `acid`. A timeline bar
2304(`ui/timeline.ts`) under the demo caption and every clip shows turns played, with a mark at the
2305scene's `moment`; chosen: normal speed, turn-by-turn fill, on both.
1f0038ac 2026-11-01 Pavel Safronov2306
2307### 2026-10-31 — Oil makes the player and the basic enemies slide
2308
2309User request (Pavel, 2026-10-31), details decided with him the same day: landing on oil, the player
2310and the basic enemies (everything but the flying Ghost and Wisp) slide on, over oil only, and stop on
2311the last oiled tile of the line; a slide that ends in flame kills. Chosen where it was left open:
2312(1) the slide goes the way the step went and is straight, it does not follow a bend in the spill;
2313(2) it stops short of a bomb (unless Bomb Pass) and of an enemy for the player, rather than running
2314into them (a slide into an enemy would be an unforeseen death; walking in still is one); flame on
2315the way does not stop it, as with the Slide power-up, so it burns; (3) a diagonal step, Slide and
2316Teleport don't start a slide (a diagonal one would need squeeze rules); (4) the action's target stays
2317the neighbouring tile, so validation and replays are unchanged: the slide is a consequence worked
2318out in `step()` (player) and planning (enemies). Auto-move (`path.ts`) plans on tiles it lands on,
2319so it **keeps off any step that would slide** (`slips`), else it could bounce past its destination
2320forever or slide onto the open exit (reviewer, 2026-10-31): a tile in the middle of a spill is not
2321an auto-move destination, a single tap next to you still moves and slides. Walking onto an enemy on
8edb5cb9 2026-11-01 Pavel Safronov2322oil is a death although the slide carries on. Known, left: slides pass over raised spikes and the
1f0038ac 2026-11-01 Pavel Safronov2323Decoy without effect; (5) an enemy's slide is
2324announced like any move: its plan holds the tile where it stops, so the arrows and the danger
2325overlay stay truthful (a doubled arrow, as for a Sprinter's dash). `SIM_VERSION` 41.
2326
2327### 2026-10-31 — The Kenney credit leaves the in-game menu
2328
2329User request (Pavel, 2026-10-31). This reverses the 2026-10-21 decision that the credit lives in
2330the menu. Kenney's packs are CC0, so no credit is required; the README's Attribution section stays
2331as the credit (and still thanks Kenney and links his donation page).
8edb5cb9 2026-11-01 Pavel Safronov2332
2333### 2026-10-31 — Two gems on one turn offer one after the other
2334
2335User report: a slide (oil, or the Slide power-up) over two gems used the first gem's offer and
2336dropped the second unseen. Now the second waits in `waitingGems` and its offer is drawn when the
2337first is answered (and any charge choice from it), so it knows what the first gave (a held ability
2338is not offered again). Event `gemQueued` makes the second gem rise away at pickup (sound too); `offerAgain` opens its dialog. It works when the first gem's slide ends on the open exit, too (the level is cleared, the offers still come). A death the same turn drops the queue;
2339the retry has the gems back. `SIM_VERSION` 42.
2340
2341### 2026-10-31 — Developer mode's test scenes
2342
2343User request: to check edge cases by eye (two gems on one turn, and the like), add test-only demos
2344and almanac entries to Developer mode. They are **fixed** scenes (`SceneDef.fixed`, built by
2345`sim/testScenes.ts`): a hand-drawn room from `sim/picture.ts` (the tests' `board`, moved into `src/`;
2346`%` is a gem) and a scripted game, not a random rollout, so they are exact and repeatable. Shown on
2347the almanac's **Tests** tab in a Developer run (each with "what to expect") and in the landing demo
2348with `?dev`. In the demo the gem and charge choices open as real dialogs (new gesture `pick`); in
2349an almanac clip they are answered unseen. `test/test-scenes.test.ts` asserts each scene's outcome,
2350so the same cases are checked without looking. To add a case: a name in `TestKind` and a `TEST_SCENES` entry; the
2351demo table and icons follow from the list. Not in the 0.2.0 change list (a developer aid).
b89399da 2026-11-01 Pavel Safronov2352
2353### 2026-11-01 — Which version is this: a box in the menu
2354
2355User request: make it obvious in the menu which version is playing and how to switch. The menu now
2356opens with a box: **Official version** / **Early adopter version**, a button to the other and both
2357addresses. Addresses come from package.json (`homepage`, new `nextHomepage`) through vite.config.ts
2358(`VITE_HOME_URL`, `VITE_NEXT_URL`) into `src/channel.ts`, one source for the app and the README. The
2359old "Try it early" / "Back to the released version" links in the menu's bottom line are gone (the
2360Changes dialog's teaser keeps its link). The released copy shows the button only once next.json
2361answered. Not shown in `npm run dev` (channel `local`). Kenney's credit stays out of the menu
2362(since 0.2.0 work, 2026-10-31); CC0 needs none.
2363
2364### 2026-11-01 — A toast for a charge picked up again
2365
2366User request: a charge picked up for the second time in a run is simply taken (no dialog, as the
2367player knows the idea), but the icons are small, so say what it was. New `ui/toast.ts`: a note with
2368the charge's picture and name above the action bar, fading after ~2 s, one at a time. Shown for any
2369charge pickup that opened no dialog (not the first of its kind, not a sell-or-swap); not in the
2370demo. The toast is the general mechanism for other "you got X" notes later.
2371
2372Follow-up, same day: the toast says how many were picked up and the total held ("Picked up 3 × Nuke",
2373"5 charges in all"; the second line only when more is held than was picked up). It is a popover, so
2374it also shows over dialogs. **Rule: each new kind of UI piece like this gets a sample on the
2375almanac's Tests tab** ("Interface pieces", tap to show; `TOAST_SAMPLES` in `ui/almanac.ts`), so
2376Developer mode can show it without reproducing the situation.
2377
2378### 2026-11-01 — Clips show the choices; almanac tabs wrap
2379
2380User report (screenshot): five almanac tabs ran over each other (the Tests tab made five); the tabs are
2381now a grid of as many equal columns as fit, none under 104 px. And a clip of "slide over two gems"
2382only showed the slide, not the two dialogs. A clip now draws the open choice over itself
2383(`showChoice` in `ui/clip.ts`): the gem's cards with the scripted pick lit and "another gem is
2384waiting", or a charge's keep/take; held 2 s, then the pick plays. (The real dialog isn't opened in a
2385clip: it would cover the clip.) Only test scenes have such choices.
2386
2387Timing, same day: the choice comes after the turn that led to it has come to rest (a ~1.1 s pause in a
2388clip, 1.2 s in the `?dev` demo), shows ~2 s, goes, and the second follows after a pause (the clip's
2389own gap after the first goes, then 0.2 s), rather than at once over the slide.
d94649ba 2026-11-01 Pavel Safronov2390
2391### 2026-11-01 — A dragged view stays put through turns
2392
2393User report: the view snapped back to the player after every turn, a sudden jump, even when they
2394had put it there on purpose. Now a drag "holds" the view (`held` in camera.ts): a turn moves the
2395player but not the view (`keepView` shifts the offset by the move). It follows the player again
2396when a turn would take the player out of the view (checked after the turn, not while dragging, so a
2397far look stays put) and on a new level, a new run or the demo (`resetView`). Supersedes "the next turn brings the view back" in the 2026-10-25 entry above.
ebcd0a8f 2026-11-02 Pavel Safronov2398
2399### 2026-11-01 — Controller support: pure core first (Q29, stage 1)
2400
2401Controller support is mandatory before Steam. It works like the keyboard plus a way to aim at tiles (first a tile cursor, then, in stage 2, a target among the blue tiles), and
2402calls the same functions (tapTile, direction, action buttons, inspect), so every rule stays shared.
2403Stage 1 is two pure modules with tests, nothing wired into the game yet: `ui/padMap.ts` (a gamepad
2404snapshot to intents) and `ui/spatial.ts` (which element to focus next in a direction).
2405Map (W3C standard mapping; face buttons and shoulders superseded by stage 2 below): D-pad and left stick move (stick in 8 sectors, d-pad can give diagonals,
2406the caller filters by the Diagonal power); A confirm; B cancel; X Bomb; Y Wait; LB/RB previous/next
2407(charge/move on the board, tabs and scenes elsewhere); View: What is that?; Start: menu; R3: reset
2408view; right stick looks around. A held direction repeats after 300 ms, then every ~110 ms; the left
2409stick turns on at 0.5 and off at 0.35; the right stick has a 0.2 deadzone. The first frame of a
2410fresh pad only records what is held, so the press that wakes the pad in Chrome does nothing.
2411Dialogs: Cross/Triangle/Square pick buttons directly in small dialogs (Q42, to be settled in
2412stage 4). Desktop wrapper for Steam: Electron (Q41). Full plan lives in the queue (Q29).
2413
2414Stage 2 (board play, wired in; `ui/gamepad.ts`): the pad calls the same functions as keys and taps
2415and never fakes pointer events. First attempt, a free tile cursor (the d-pad moved it, A tapped
2416it), was replaced the same day by Pavel's design, which reuses the action bar: the face buttons are
2417Cross Move/Go, Circle Wait, Square Bomb, Triangle Charge (the intent also carries the raw face
2418index, since dialogs will map them differently); LB cancels, RB and View ask "What is that?", Start
2419is the menu, R3 is `resetPan` (the view follows the player again, not the full `resetView`). RB or View
2420turn "What is that?" on and aim at the nearest tile with something to explain
2421(`padTarget.explainableTiles`: what `tileInfo` explains, not plain pillars or the outer wall);
2422the d-pad jumps between them, the label says "Find out what this is", RB or View again explains
2423the tile (the call a tap makes in inspect mode), LB cancels.
2424Pressing a face button picks that action as its action-bar button does, so the blue tiles show
2425(`pickTargets`, the same list a tap uses), and aims at the nearest of them (`padTarget.ts`
2426`firstTarget`), drawn with a dashed square from Kenney's Crosshair Pack, breathing slightly. The
2427d-pad and left stick then jump between those tiles (`nextFocus`, board coordinates, nothing wraps);
2428the right stick looks around. The same face button again does it (`tapTile`, so choices, the
2429deadly check and Freeze work as for a tap); another one switches action; LB (`cancel()`, shared
2430with Esc) puts it away. Wait, and Go (Move with a flag set), have no tile: they aim at the
2431player's own tile and the same button does them. With nothing aimed, the d-pad and left stick walk
2432as the arrow keys do (a diagonal only with the Diagonal power). A one-line label over the action
2433bar, only while the pad is in use, says what confirming does (`ui/targetLabel.ts`, covering
2434Move/Run/Diagonal/Slide/Kick, Bomb/Throw/RPG/Detonate, each charge, Wait and Go, and "costs a
2435life!"). The view moves for the target only near its edge (`revealTile`) and then stays put; the
2436aim ends when a turn is played, a level or run starts, or a mouse, touch or key acts. LittleJS's
2437own gamepad reading is disabled (`setGamepadsEnable(false)`). The pad is ignored while the window
2438is unfocused, a standard pad is preferred, and a non-standard one only gets one "not recognised"
2439toast. In the demo the d-pad left/right switch scenes, at most every 400 ms, and Cross, Circle,
2440Square, Triangle, LB or Start end it.
2441
2442Action-bar glyphs (Q45): while `body.pad` is set, each of the four action buttons shows its face button
2443(`ui/padGlyph.ts`, Kenney Input Prompts, the colour variants, Default size) where the key hint sits,
2444and the hint is hidden (not removed, so the button keeps its size). The family comes from
2445`padFamily` when the pad connects (`controls.setPadFamily`): PlayStation gets Cross, Square,
2446Triangle and Circle; Xbox and any other standard pad get A, X, Y and B. They revert with the
2447existing removal of `body.pad` on a pointer or key.
2448
2449Stage 3 (dialogs and the banner; `ui/padDialogMap.ts` pure and tested, `ui/padDialog.ts` DOM glue,
2450Pavel approved the layout): a dialog's footer buttons map to face buttons: the primary one to
2451Cross, the others left to right to Triangle then Square, and the button that dismisses (by label:
2452Close, Cancel, Back, Got it, Keep playing, Resume; only if the dialog is not `required`) to Circle.
2453Circle with no such button closes via `dismissDialog()` (what Esc does). If the buttons do not fit
2454(more than three besides the dismissing one), or the body has controls to move between (menu,
2455almanac, New run, the changes list), "selector mode" applies: a d-pad selector (nextFocus over
2456`getBoundingClientRect`) covers the body's buttons, links, switches and the footer, and Cross
2457clicks the focused one. Cards (`.offer-choice`: gem offer, shop) are not "controls": moving onto one
2458clicks it (selects it, plays its clip) and Cross presses the footer's primary (gem offer) or the
2459selected card's Buy (shop, where Cross is thus taken and Restock and Next level get Triangle and
2460Square; a plain Cross there would have left the shop). The body is watched with a
2461MutationObserver, so any rebuild (shop purchase, almanac tab, Back) re-applies glyphs and the
2462selector (same index; the shop and gem cards prefer the one with `aria-pressed`); a new dialog
2463(event `dialogopen`) starts the selector on the footer's first primary button in selector mode
2464(Resume, Start), else the `aria-pressed` item (the almanac's current tab), else the first element.
2465In selector mode Circle presses the footer's dismissing button when there is one (so Back runs its
2466action), else closes. The shop's Buy always uses the card marked `aria-pressed` at press time. Moving
2467the selector scrolls the body clear of its sticky tabs, clip or coin line. LB/RB click the previous/next `[data-tab]`; the
2468right stick scrolls `#dialog-body` (900 px/s at full push). A text field is never selectable (a
2469pad cannot type): the seed stays for keyboard. The banner's visible buttons use the same mapping
2470(first Cross, then Triangle, Square). Known gap: Back from an almanac entry or a change returns to
2471the almanac's current tab, not to the entry that was opened (the scroll position is kept). View
2472(like RB) also switches tabs, as the pad cannot tell them apart.
2473
2474After Pavel's first pad test (board only): the right-hand face buttons stay. The left stick is off
2475(`LEFT_STICK_ENABLED = false`; `mapPad` takes a `leftStick` argument so its tests still run). The
2476d-pad with no aim does not walk any more: it aims the neighbour tile (`aim.button = 'step'`), and
2477the label and the later confirm both come from `directionPlan` / `diagonalPlan`, the very function
2478the arrow keys run, so every keyboard rule (kick, run, Bomb Pass choice, the deadly refusal) holds;
2479a step that can do nothing shows "Can't go there" and the crosshair without the blue tile. Cross
2480confirms any aim (`confirmAim`); Circle backs out of any aim (`padBack`: the pick it made goes, the
2481flag stays) and, with none, aims Wait (so Wait needs Cross: one slip no longer ends a turn).
2482LB / RB are `prev` / `next` (LB no longer cancels, RB no longer inspects; View alone does): on the
2483board they cycle through one list (`padCycle.ts`): for Move, Bomb, Charge, Wait in that order, each
2484button's `pickTargets` tiles (nearest first), or a single item for a button that goes at once (Wait,
2485Go); it is built when the first press finds no cycle aim, kept in the aim, and thrown away with the
2486aim at the end of a turn. The label gets "CURR/ALL · ". Cross picks the item's button as a face
2487button would and taps its tile (`tapTile`, so a choice dialog asks; a deadly item is still played, the label having warned "(costs a life!)", as with a picked action by touch: only the d-pad step and a tap with nothing picked refuse). The d-pad does nothing
2488while cycling. In dialogs LB / RB switch tabs; View does nothing there. The demo is ended by LB no
2489more (only face buttons, Start).
2490
2491The cycle aim looks as if its action were picked without picking it: `drawBoard` takes `pick` (the
2492action whose tiles are lit blue, and which of them cost a life, computed when the aim is made) and
2493`ghost` (an action and tile whose result the existing commit preview draws faded: bombs with their
2494badges and blast, a decoy, the moved player). The d-pad step aim uses `ghost` alone. No separate
2495sprite table was needed, since the preview plays the action on a copy of the state. Not previewed:
2496Wait (nothing changes), and a Freeze or ICBM beyond what the commit preview already draws.
2497
2498**Tile first (supersedes the aim, d-pad step and LB / RB cycle models above, 2026-11-01, Pavel after
2499testing them).** The pad now has a cursor, a tile among the actionable ones (`ui/padTiles.ts`,
2500pure and tested): the player's own tile (Wait) and every tile `pickTargets` gives for Move, Bomb or
2501Charge. All of them are lit blue (`View.lit`) and the cursor is the dashed square. The d-pad moves
2502it with `nextFocus` on board coordinates; the first press with none showing puts it on the nearest
2503actionable tile that way, or on the player's own tile. It is dropped when a turn ends, a level or
2504run starts, a dialog opens or a mouse/key takes over, and on any other change it stays if its tile
2505is still actionable, else goes to the nearest one that is, else goes (`refreshCursor`). The face
2506buttons act on the cursor's tile AT ONCE, through the path a tap with that button picked takes
2507(`selected` set, then `tapTile`; Wait is `pick('wait')`): a choice dialog for a bomb still asks.
2508There is no second press to confirm. A button's availability on the tile comes from `resolvePick`
2509(Wait: only the player's tile). A button is marked deadly by `isDeadly` of the action it resolves
2510to there (`{id, target}`), and of waiting for Wait: the same test as the board's red tiles and the
2511Wait button's red. A deadly action is played, since the button was marked; only a tap or key with
2512nothing picked refuses. Greyed ones flash when pressed. With no cursor, a face button only shows the
2513cursor on the player's own tile. The label lists the possible buttons with their names; the ghost
2514is the first possible of Move, Bomb, Charge (none on the own tile). LB backs out; RB and View are
2515"What is that?" (aim kind `inspect`, unchanged); in dialogs LB / RB (and View) switch tabs. Removed:
2516`padCycle.ts`, the cycle and step aims, same-button-confirm and Circle-backs-out. The keyboard's
2517`directionPlan` stays (the arrow keys use it).
2518
2519Glyph layout and families: the action bar's picture now sits beside the icon on one row (36px icon,
252032px picture, both able to shrink on a narrow bar; the key hint is hidden under `body.pad`), and
2521the cursor's label draws the button picture before each line instead of its name. A Nintendo
2522family (`padFamily`: ids with nintendo, switch, pro controller or vendor 057e) uses the Switch
2523set (plain: the pack has no coloured variant); glyphs are chosen by physical position, so the
2524bottom button (index 0, Move) shows B, left (Bomb) Y, top (Charge) X and right (Wait) A. The cursor
2525tile is drawn red when every action possible there would cost a life (`cursorDeadly`); if only
2526some would, it stays blue and the red-marked buttons say which.
2527
aef53511 2026-11-02 Pavel Safronov2528Cursor always on (Pavel, 2026-11-01; supersedes "the first d-pad press shows the cursor" and "a face
2529button with no cursor shows it" above): while a pad is in use and the board is being played (no
2530dialog, not the demo, not "What is that?", status playing) the cursor is always shown
2531(`cursorWanted`, `ensureCursor` each frame). It rests on the player's own tile (`restingCursor`)
2532whenever it was cleared: a turn ending, a new level or run, a dialog closing, "What is that?"
2533ending, an action done. The d-pad moves it from there with `moveCursor` (no "nearest tile that
2534way" special case; `firstCursor` is gone). A face button always acts on the cursor's tile or
2535flashes if it cannot. L1 clears "What is that?" first, then a leftover pick and the flag, and
2536returns the cursor to the player's tile. A key or mouse still removes the cursor and the pad
2537styling (`padOff`); the next pad input brings it back on the player's tile, and that waking
2538face press only shows it (so a stray Circle does not wait a turn).
2539
ebcd0a8f 2026-11-02 Pavel Safronov2540### 2026-11-01 — A screen shows the whole board when it fits (supersedes the fixed 9 × 13 view)
2541
2542Decided by the user, knowing it changes the earlier rule that no screen sees more than another
2543(PLAN.md §19.2, decisions.md 2026-10-24/25: the view is 9 columns × 13 rows). From the UI rework
2544(Q52) on, the number of tiles shown depends on the screen: the view grows with the space, up to the
2545whole board, and a screen too small for the board scrolls as before (phones keep the 9 × 13 view or
2546whatever fits at a playable tile size). So early boards (9 × 11) always show fully, and a large
2547screen shows a late 13 × 17 board whole. Trade-off accepted: a big screen sees more of the board
2548than a phone; scores from different screens are therefore not strictly like for like. Rules and
2549SIM_VERSION are unchanged (the view is render and UI only). Still to do when the camera part is
2550built: update PLAN.md §19.2 and the README, and decide whether the score board notes the screen
2551class. Not built yet.
41566e2d 2026-11-02 Pavel Safronov2552
2553### 2026-11-01 — The level finishes itself once the last enemy is gone
2554
92cc4ee3 2026-11-02 Pavel Safronov2555**Superseded 2026-11-02 by "Autopilot replaces the hoover (Q73)"; the `hoover` action no longer exists.**
2556
41566e2d 2026-11-02 Pavel Safronov2557User request: on a controller, tapping each coin, gem and charge and then the exit was a pain.
2558Decided with the user: (1) the gathering is a new sim action `hoover` taking no turn (like `buy`),
2559so runs record and replay it and old records are untouched (no SIM_VERSION bump); (2) the walk to
2560the exit costs turns as ever, par and turn count included; (3) gems queue their offers one after
2561another and a charge choice pauses the gathering; (4) it applies to every input, with a setting
2562("Finish the level for me", on by default); (5) auto-move's safety rules apply, no way to the exit
2563is the existing boxed-in life loss, and when the only way is unsafe it waits for the player.
2564Triggered once per level by the UI (`autoFinish` in main.ts), ended by the player's own move or
2565cancelling the flag.
2566
2567
2568### 2026-11-01 — Fullscreen from the first tap, click or key
2569
2570User request: start fullscreen. Browsers only allow `requestFullscreen()` after a user gesture, so
2571the game cannot do it on load: the first pointer or key event does it, once (leaving it with Esc or
2572F11 is respected). A controller press is not a gesture, so a pad-only player stays windowed until
2573touching the screen or a key. A setting, "Start in fullscreen" (on by default), turns it off. No
2574in-game toggle key (F is the RPG bomb; F11 is the browser's own). The Electron build (Q30) will
2575simply start fullscreen. iPhone Safari offers no Fullscreen API for pages, so nothing happens there.
92cc4ee3 2026-11-02 Pavel Safronov2576
2577### 2026-11-02 — Fullscreen only with a controller; a button; a menu switch (Q60)
2578
2579User request, amending 2026-11-01. Decided: (1) the first-gesture auto-fullscreen now happens only
2580while a controller is connected (`shouldAutoFullscreen`, `ui/fullscreenRule.ts`, pure): the pad
2581player has no pointer to press a button with, a phone or mouse player does, and being pushed into
2582fullscreen annoyed them; "connected" is a pad in `navigator.getGamepads()` or one the pad input has
2583seen (Chrome lists a pad only after its first press); the "Start in fullscreen" setting stays and
2584is worded for it; (2) a visible fullscreen toggle in the top bar, only where `document.fullscreenEnabled`,
2585following `fullscreenchange` (so F11 and Esc update it); on a phone's portrait bar three stacked
2586round buttons would grow the bar, so all three shrink to 31 px then (the bar's height is
2587unchanged); icons are Game Icons `larger` / `smaller`; (3) for a controller, a "Fullscreen" switch
2588in the menu's Settings, no pad button (every one is taken); a pad press is no browser gesture, so
2589in a plain browser the request can be refused: the page calls it anyway and, if it rejects, shows
2590"Press F11, or use the fullscreen button". The Electron wrapper (Q41) will make it work.
2591
2592### 2026-11-02 — No automatic fullscreen; the cursor stays on the destination while walking (Q70)
2593
2594User request after testing. Decided: (1) the game never goes fullscreen by itself. This supersedes
25952026-11-01 "Fullscreen from the first tap, click or key" and the Q60 rule that made it happen only
2596with a controller (`shouldAutoFullscreen` and `ui/fullscreenRule.ts` are gone, with the "Start in
2597fullscreen" setting and its stored value, which is simply no longer read). What stays from Q60: the
2598top bar's fullscreen button, the menu's "Fullscreen" switch (with the toast "Press F11, or use the
2599fullscreen button" when a pad press is refused, since it is no browser gesture) and the
2600`fullscreenchange` syncing; (2) bug report: with a pad, after the first step of an auto-move walk the
2601cursor went back to the player's tile, where only Wait was offered, though the route was still drawn.
2602Now the cursor starts each turn on the destination while a walk is on (`cursorStart` in
2603`ui/padTiles.ts`, pure; `ensureCursor` in `main.ts`; the destination is a faded candidate, label
2604"Keep walking · N steps"), so Go continues at once; when the walk ends (arrived, cancelled, blocked,
2605new level) it starts on the player again. The keyboard cursor shares the code.
2606
2607### 2026-11-02 — The demo's input button (Q61)
2608
2609User request: a demo button cycling touch, mouse, controller (ps5, xbox, switch, generic). Decided:
2610(1) the pick overrides detection for the demo only, session only (`demoForced`, not stored), and
2611the scene restarts for it (a switch between the two scene lists is the existing between-scenes
2612switch, from the button it is immediate); (2) what clears it: the demo ending, or the controller setting changing (one plugged in or unplugged,
2613`dropDemoPick`); amended the same day at the user's request, an earlier rule also dropped a touch or
2614mouse pick on a real pad press. Nothing else clears it: no mouse move, click, tap or button press, so
2615the button keeps cycling and the pick stays wherever the user stopped (a mouse player must be able to
2616watch the controller demos, and a pad player the touch ones); only holding the confirm button, which
2617ends the demo, leaves it; (3) mouse is the touch scenes with a pointer in the finger's place and "Click anywhere
2618to play"; the pointer is `cursor_none` from Kenney's Cursor Pack (assets/kenney-cursor; user supplied the
2619pack, 2026-11-02, replacing a stopgap sword); the touch
2620captions still say "finger"; (4) the Q59 hint "Hold {move} to play" and the pad family drawn in
2621captions and on the virtual controller follow the effective input, not the real pad; (5) clips
2622keep following the real input. No rules change (no SIM_VERSION bump).
2623
2624### 2026-11-02 — Controller demos: the pad's own demo and clips (Q58)
2625
2626User request: with a pad in use, the demo and the dialogs' clips show the controller, not the finger.
2627Decided: (1) the virtual controller (`ui/padOverlay.ts`) is CSS shapes with Kenney Input Prompts
2628glyphs on them, no drawn art (superseded by the later Q63 entries: it is now a line drawing from OpenGameArt) (art-provenance #16 notes the outline as layout, like the old-TV look);
2629(2) the input is switched only between scenes (and a clip at its next loop), never mid-script, so
2630a scene is never half finger and half pad; (3) switching to the pad starts its list at scene 1, and
2631switching back resumes the touch list where it was left: the two lists keep separate saved places;
2632(4) the pad's list is its own scenes (cursor, L1, R1, the stick, Start) plus the controls and
2633gameplay scenes without the touch-only ones (long-press bomb, auto-move, the top bar, "What is
2634that?" by tap); (5) `chargeSwap` and any dialog choice are deferred through `PAD_LATER`
2635(`sim/demo.ts`): the pad cannot show a dialog's answer yet, and `padPlan` returns null for a choice,
2636a pick, or a tile where no single face button does the action; (6) the stick scene only lights the
2637stick, because the demo shows a room whole and there is nothing to scroll; (7) the Start scene
2638opens the real menu over the scene, showing the run's own state, not the scene's; (8) the turn
2639plan (`ui/padDemo.ts`) is pure and works from the concrete action, so every plain move, bomb, charge
2640and wait has a pad version without per-scene scripts. No rules change (no SIM_VERSION bump).
2641
2642### 2026-11-02 — Hold to confirm on the pad (Q59)
2643
2644User request: introduce holding a button to confirm it, with a ring filling clockwise from 12
2645o'clock; the action happens on release and only if the ring filled; on the demo screen the player
2646holds the bottom face button to play. Decided: (1) pad only: touch, mouse and keyboard already have
2647deliberate gestures, and the pad's face buttons are easy to hit by accident; (2) the pad's four
2648face buttons only (board actions, the face-button choices in dialogs and on the banner, and the
2649demo's start); the d-pad, shoulders, View, R3 and Start stay instant, since they move, cancel or
2650look and cost nothing to hit by mistake; (3) 0.5 s as one constant, `HOLD_SECONDS` in
2651`ui/padHold.ts`, long enough to be deliberate and short enough not to slow a turn; (4) fire on
2652release, not on completion, so the ring filling is a warning that can still be backed out of by
2653letting go early, which does nothing; (5) one hold at a time, a second face button is ignored
2654until the first is released; a swapped or unplugged pad and a window that loses focus cancel; (6)
2655in the demo the bottom button (Cross/A, Move) is the way in and other face buttons do nothing, so a
2656stray press cannot throw the demo away; (7) the waking press (see 2026-11-01) still only shows the
2657cursor, with no ring; (8) the ring is a CSS conic gradient, no art, and one floating element is
2658moved over the glyph (`ui/holdRing.ts`, inside the open `<dialog>` because it is in the top
2659layer); it does not animate, so reduced motion changes nothing. The demo scenes' own scripted
2660presses call the game directly and show no ring. No rules change (no SIM_VERSION bump).
2661
2662### 2026-11-02 — Nintendo pads: A confirms, B backs out (amends the 2026-11-01 position-only mapping)
2663
2664User request: on a Nintendo Switch pad, A is the "go ahead" button and B is "back", but the face
2665buttons were mapped by position only (bottom Move, left Bomb, top Charge, right Wait), so on a
2666Switch pad the bottom button, labelled B, confirmed, against a decade of habit. Decided: (1) only
2667the Nintendo family differs; Xbox, PlayStation and generic keep the position mapping, because their
2668bottom button is already the confirm one; (2) on a Nintendo pad **Move/Go and every dialog
2669"confirm" (primary footer button, shop Buy, the selector's press, the banner's main button, the
2670demo's hold-to-play) are the right button (A, W3C index 1)**, and **Wait and every dialog
2671"dismiss" are the bottom button (B, index 0)**; Bomb stays left (Y, index 2), Charge top (X,
2672index 3); (3) the rule lives in the pure modules, not in `main.ts`: `padMap.ts` has one table
2673(`FACE_ACTIONS`) with `faceOf`, `faceActionOf`, `confirmFace` and `cancelFace`; `mapPad` takes the
2674family and says what each face button means (the intent still carries the raw index); `padGlyph.ts`
2675names a face button's picture by its position (`faceGlyph`) and an action's picture as the face
2676button that does it (`glyphFor`); `padDialogMap.ts` (`assignFaces`, `faceAction`) takes the family;
2677(4) the action bar's `data-face` (where the hold ring looks) follows the family, and a dialog or
2678banner already open is assigned again when the family becomes known; (5) the demo's virtual
2679controller puts the Move picture on the right and Wait at the bottom for Nintendo (CSS class
2680`.nintendo` on the overlay), so the picture sits where the button is. Also (same day): a pressed
2681button on the virtual controller stays lit for `PAD_LIT_SECONDS` = 0.7 s (was 0.3 to 0.35), and the
2682demo's and the clips' beats were respaced so presses never overlap (a d-pad press is lit, lets go,
2683then the cursor hops and rests `PAD_AFTER_SECONDS` = 0.3 s). No rules change (no SIM_VERSION bump).
2684
2685### 2026-11-02 — The virtual controller is laid out from measured data (Q63)
2686
2687Pavel disliked the CSS-drawn pad outline (rounded boxes and two tilted grips), and its hand-placed
2688percentages put buttons only roughly where they are on a real pad. Decided: (1) each family has a
2689**measured layout** (`ui/padLayouts.ts`, generated by `tools/make-pad-layouts.py`, not edited by
2690hand): a 1000-wide box, the body as an SVG path, and the centre and radius of the d-pad, right and left
2691stick, four face buttons and Start, and the shoulder boxes; (2) `ui/padPlaces.ts` (pure, tested) turns a
2692family into each lit part's place and size, with the face position taken from `faceOf` (so Nintendo's Move
2693on the right and Wait at the bottom need no special case; the `.nintendo` CSS is gone); (3) `padOverlay.ts`
2694draws the body, the left stick (never lit) and the shoulders in an inline SVG and lays Kenney's pictures
2695over it in percent of the box, so one size variable (`--pad-size`) still scales everything; `setFamily`
2696swaps layout and pictures at once; (4) the Xbox layout was measured from a CC BY-SA 3.0 photo by mliu92
2697(attribution in the README), so the layouts carry that licence, and generic pads use it; the PS5 and
2698Switch Pro ones are from Pavel's own photos; (5) the SNES / N64 Switch Online pads are not covered.
2699The outline is hand-drawn by Claude from measurements, so it stays on the hand-made list (art
2700provenance #16, Q39).
2701
2702### 2026-11-02 — The virtual controller uses real line-art pictures (Q63, supersedes the outline and photo parts above)
2703
2704Pavel rejected the hand-drawn SVG outline. Decided: (1) the body is a picture, one per family, from
2705OpenGameArt's "Controller style 1" (Xbox, also generic), "5" (PlayStation) and "3" (Nintendo), CC BY 4.0
2706by xUMR (https://opengameart.org/users/xumr); `tools/make-pad-layouts.py` scales them to 1200 px wide and trims them into
2707`assets/controller-art/` (README Attribution, art provenance, `LICENSE.txt` there); "Controller style 4"
2708(DualShock-style) is not used; (2) the drawings already hold every button, both sticks and the shoulders,
2709so `padOverlay.ts` shows only the `<img>` (lines made visible by a CSS drop-shadow) and Kenney's pictures
2710over the drawn buttons; the layout lost `outline` and `lstick`; (3) the positions are measured by Claude
2711from the drawings (the script's table), so mliu92's CC BY-SA 3.0 Xbox photo and Pavel's PS5 and Switch Pro
2712photos are no longer used and the CC BY-SA licence note is gone; (4) the hand-made outline is gone from
2713the Q39 list. Size is still the one `--pad-size` variable.
2714
2715### 2026-11-02 — Virtual controller polish, stick demos, time bar on the TV
2716
2717After Pavel tried the demo: (1) the white pictures (d-pad, stick) are not dimmed at rest, since the dimming made them read grey and then "turn white" when pressed; the d-pad and stick show a press by the red arm already in their own picture, shoulders and Start by a scale and a glow; only the four coloured face buttons have an idle (opacity 0.75, brightness 1.6) and a lit look. (2) The pictures are drawn under the line drawing, which is on top, and each face picture is clipped to its drawn circle (`clip-path: circle(--clip)`, `--clip` = 50% / `GLYPH_SCALE`; lit, the clip shrinks by the 1.15 scale so the circle stays put); hold rings stay above the drawing. (3) Reduced motion keeps no scale and the plain clip. (4) The stick scene needs a view that can scroll, but the demo shows rooms whole, so `updateCamera` got `zoom` (with `whole`): the scene shows 1/2 of the room, the stick calls `panView` + `endPan` (eased), and pressing it `resetPan`s. `demoZoom` is reset in `resetView` (so every new scene and the end of the demo) and in `skipTo`; the view mask and the action bar's place follow the zoomed view. A second scene, padLook, pushes the stick toward an enemy of the enemies room (`padDemoOrder`: the stick, look, Start). Chosen over a taller board because it works for any room and costs only the zoom parameter. (5) The time bar `#demo-time` is a fixed element on the TV's bottom edge, inside the glass (above the CRT, under the caption panel), instead of in the panel; the panel is shorter by it, and `--demo-panel` / the TV's size math measure the panel as before, so nothing else changed.
2718
2719### 2026-11-02 — Attribution policy
2720
2721User decision: the deployed game always links to the repository (the menu's "Source code" link, `REPO_URL`
2722in `main.ts`, in the live and the next copy), and the repository's README Attribution section lists every
2723credit. That is the way the game satisfies CC BY 4.0's attribution for xUMR's controller drawings
2724("in any reasonable manner", with a link to where the credit information is; section 3(a)): the README
2725names the author with a link to their OpenGameArt profile and each work's page, names the licence with
2726its link, and says what was changed (scaled and trimmed); `assets/controller-art/LICENSE.txt` repeats
2727it beside the files. Conditions: the repository stays publicly readable, and the link stays in every
2728build. Not legal advice. Kenney's CC0 art needs no credit (the README credits it anyway); no credit in the menu itself.
2729
2730### 2026-11-02 — The keyboard gets the controller's cursor, beside the plain arrows (Q65 part A)
2731
2732An audit found the keyboard could not play everything: tile targets only along the player's four straight
2733lines (nearest valid tile: Teleport, Decoy, Detonate only partly usable), `?` then an arrow moved the player,
2734Enter on the banner pressed its first button whatever had the focus, Space waited with a button focused, no
2735pan, dialogs opened with no default. Decided, a hybrid: (1) the plain arrows / W A S D keep `directionPlan`
2736(2026-11-01 recorded that the keyboard deliberately keeps it; it is fast and a habit), now documented as
2737"B, then a direction"; (2) **Shift + arrows** wake a keyboard cursor that is the pad's tile-first cursor
2738(`padTiles.ts`, `padTarget.ts` reused untouched), so every blue tile is reachable. The wake press only shows
2739the cursor. While it shows, the action keys use `padDo` on its tile, Enter does the first possible action
2740(never Wait, nothing on the player's tile) or confirms a picked one, Esc backs out stepwise (cancel, cursor
2741home, hide); (3) `padActive` as the cursor's switch became `inputMode` ('pad' | 'keys' | null, `ui/keyCursor.ts`):
2742the cursor key presses, modifiers, Alt-pan and Home keep the keyboard's mode; a plain arrow or any key or mouse
2743press otherwise hands the board back, as the pad rule says; `cursorWanted` is unchanged (main.ts passes
2744`inputMode !== null`); (4) `?` aims over `explainableTiles` and Enter or `?` explains it, so far auto-move comes through the info
2745dialog's "Walk there"; the aim's keyboard mode ends with "What is that?"; (5) Alt + arrows `panView` (0.5 tile a press,
2746repeating), Home `resetPan`; (6) Enter and Space leave a focused button, link or input to the browser
2747(`targetTakesKey`), and a closed dialog blurs the button that got the focus back; a dialog panel answers
2748Enter with its green (or only) button; (7) the label and hint draw key caps (`kbd.key-cap`) instead of pad
2749pictures. The pad's behaviour is unchanged. Part B (the demo for keyboards) is separate.
2750
2751### 2026-11-02 — The demo plays for the keyboard too (Q65 part B)
2752
2753The demo's input button gains **keyboard** as its third entry (touch, mouse, keyboard, ps5, xbox, switch,
2754generic; the pick rules of Q61 unchanged). The keyboard demo is the controller demo's shape, not the touch one:
2755the touch scenes minus the touch-only ones (long-press bomb, auto-move, top bar tour, "What is that?" by tap) and
2756the charge swap, with its own saved place (`demoAtKeys`), a **key overlay** (`ui/keyOverlay.ts`: Kenney's
2757Keyboard & Mouse glyphs, Default style, on a CSS grid; white at rest, a pressed key lit yellow for
2758`PAD_LIT_SECONDS`, so presses never overlap) and a dashed board cursor when the keys use Shift. Decided:
2759(1) each turn is played with the **fewest key presses the game accepts** (`ui/keyDemo.ts` `keyPlan`): a plain arrow
2760for a step, an action key then an arrow ("B, then a direction"; also M and V), the same key twice to confirm Freeze
2761on yourself, else Shift + arrows over `cursorPath` then the action key. The first Shift + arrow only wakes the cursor on
2762the player (as in play), so it counts as a press and repeats the first hop's direction. Every candidate is
2763replayed through the game's own pure rules (`runKeys`: keyButton, moveCursor, tileActions, resolvePick, tapChoices,
2764isDeadly) and only one that ends on the action's tile with the same action id is used; the test does the same
2765on every demo scene and seed. `runKeys` mirrors main.ts `directionPlan` (without its questions) rather than
2766sharing it; a change there needs one here. (2) Each turn is assumed to start with the cursor asleep. In play, a Shift + arrow right
2767after another cursor turn hops at once; the demo always wakes it, which only costs it a press. (3) Only M B V Space,
2768the arrows, Shift, Alt, Enter, Esc, ? and Home are drawn; R K T P F I L X G and Q E Z C have no cap, since the cursor
2769plus M B V reaches the same tiles; W A S D are named in the first caption, not drawn. (4) New scenes: the Shift
2770cursor, `?` then an arrow then Enter, Alt + arrows (the 2x zoomed look view of the stick scene) then Home, Esc opens
2771the menu. Captions are `SceneDef.keyCaption` with `{arrows}`, `{M}`, `{shift}` ... tokens (`keyCaptionHtml`, throws on
2772an unknown one). (5) The hint reads "Press Enter or Esc to play"; the demo's key handler already ended it on those
2773and swallows every other key. (6) Clips keep following the real input. The key pictures were copied from the pack
2774(15 files), art-provenance and README Attribution counts updated.
2775Correction (2026-11-02): "swallows every other key" in (5) is too strong. In the demo ← and → change scenes, Esc and Enter end it, and Tab, Enter and Space work the demo's own buttons; the rest does nothing. The keyboard cursor's wake (Shift + arrow, `?`) is also off while the demo plays or a dialog is open (`modeAfterKey`'s `locked`).
2776Known gaps (left as they are): the demo's `runKeys` mirrors main.ts `directionPlan` instead of sharing it (a shared pure `directionPlan` would remove the double upkeep); the keyboard demo's `?` scene does not jump the aim to the enemy as the real game's aim does.
2777
2778### 2026-11-02 — Hold to confirm acts when the ring fills (amends Q59 decision 4, "fire on release")
2779
2780User request (Pavel tried it): waiting for the button to be let go felt late. Decided: (1) the action
2781happens the moment the ring is full, while the button is still down; decision (4) of the Q59 entry
2782("fire on release, so the ring is a warning that can be backed out of") is replaced: letting go
2783before the ring is full is still the way to back out, and the full ring is now the point of no
2784return; (2) the button is then **used up** (`HoldState.done` in `ui/padHold.ts`): it cannot fire again
2785and, since a new hold cannot start while one is held or used up, it starts none in whatever the
2786action opened (a dialog, the demo ending into the game, a level change, the banner) until it has
2787been let go; a second face button is ignored meanwhile, as before; (3) the ring (`hold` callbacks)
2788reaches 1 at completion and stays full (green) until release, or goes at once if the place the
2789buttons act in changes (`holdEnd`; the button stays used up); unplug, blur, a swapped pad and a pad
2790error end the used-up state too; an instant button (Start, a bumper) does not; (4) holds that have not
2791completed keep every Q59 cancel rule (early release, context change, instant button, blur, unplug,
2792swap, error); (5) the demo's hold-to-play starts play at completion and the same held button then
2793does nothing in the game; the waking press rules (2026-11-01) are unchanged. No rules change (no
2794SIM_VERSION bump). Not verifiable without a real pad: how it feels, and that a held button across
2795the demo-to-game change shows no ring.
2796
2797### 2026-11-02 — The hold ring is anchored to its originator (Q72)
2798
2799Bug (Pavel, with a pad): the ring stayed on screen a moment after the dialog that began it closed.
2800Cause: the ring was positioned only inside `hold()` calls from the pad poll, and the dialog stays
2801open for a 150 ms pop-out (`closing`) after a button closes it; the ring, a child of the dialog,
2802stayed at the old spot meanwhile, and `holdEnd` came only when the poll saw the context change.
2803Decided: the ring belongs to the picture it was shown on (`holdRing.ts` keeps it as `origin`),
2804follows it every animation frame, and hides in the frame the picture is not connected, not drawn
2805(`checkVisibility`, zero size) or inside a dialog that is closing or closed (`ringVisible`, pure,
2806tested). A completed hold's button still counts as used up until release (Q68); only the ring goes.
2807The demo's ring lives inside the demo controller, so it goes with it (and `releaseAll`). Not
2808verifiable without a real pad: that it feels immediate on the actual hardware.
2809
2810### 2026-11-02 — Face buttons grey at rest, coloured when pressed
2811
2812User request: on the demo's virtual controller a pressed A/B/X/Y was hard to tell from an idle one
2813when both were coloured (the earlier brightening of the idle colours made it worse). Decided: the
2814face glyphs are grey at rest (`grayscale(1)`, brightened so they read on the dark demo) and take
2815their colour only while pressed (`.pad-part.face.on`: full colour, a little saturation and
2816brightness, the scale pulse); the filter transition makes the change gradual. Amends the same
2817day's "brighter idle colours" point. The white pictures (d-pad, stick, shoulders, Start) are
2818unchanged. CSS only (`style.css`, `.pad-overlay .pad-part.face`).
2819
2820### 2026-11-02 — Faded blue: auto-move's reach, for the pad's and keyboard's cursor (Q69)
2821
2822Pavel's idea: next to the bright blue (single actions) show a second, faded blue on every tile
2823auto-move can take you to, so the cursor can hop there and confirm. Decided: (1) `farTiles`
2824(`ui/padTiles.ts`) is the set a tap would walk to (`tapMeaning` = 'walk') with a safe, not deadly
2825first move (`autoMoveAction`), minus the bright tiles and the player's own, with the walk's length
2826for the label; with a destination set, only it (so Go can be pressed from the cursor).
2827(2) The cursor's candidates are the bright plus the faded tiles, one set for `moveCursor` and
2828`keepCursor`; no special order, nearest in the direction wins. (3) On a faded tile only Move/Go is
2829possible, and it taps the tile with nothing picked, so every tap rule and safety check applies;
2830the ghost shows nothing there. (4) Faded tiles are drawn only with the pad's or keyboard's cursor
2831and when nothing is picked; mouse and touch stay as they were (Pavel was asked; answer pending).
2832(5) No modifier button: the d-pad repeat is faster instead (250 ms, then 75 ms; shared with menus
2833and dialogs). Keyboard Shift+arrow repeat is the OS's. Cost: a scan of the floor, 40 ms on a
283413 x 17 level, once per state (`currentFar`). Demo scenes for it come later.
2835
2836Added the same day (Pavel approved): **the way is previewed before it is chosen.** With the cursor
2837on a faded tile, or the mouse (pointer type mouse, no button down; never touch) over a far walkable
2838tile, with nothing picked, no way set, no dialog and no demo, the board draws auto-move's way as the
2839real one is drawn (`routePreview` in `ui/routePreview.ts`, pure: the same `autoMoveAction`,
2840`autoRoute`, `isDeadly` rules, first tile red when unsafe; `View.preview`; one drawing block in
2841`render/board.ts`). Nothing is set and no step taken; Move/Go, M, Enter or a click then commits it
2842as before. The result is cached per tile, state and danger (`previewNow`). Click and tap behaviour
2843is unchanged; mouse hover is the only mouse change (the faded tint itself is still pad and keyboard only).
2844
2845### 2026-11-02 — Autopilot replaces the hoover (Q73)
2846
2847Supersedes "The level finishes itself once the last enemy is gone" (2026-11-01, Q56). User
2848request: the turnless hoover made gathering free, which is not the game's economy, and it was
2849not a walk the player could watch. Decided with the user: (1) the `hoover` action is removed
2850entirely (`types.ts`, `turn.ts`: the branch, `canHoover`, `hoover`); (2) in its place, once the last
2851enemy is gone, an autopilot walks the character with ordinary turns (move, run, diagonal, wait), so
2852replays need no new action; the sim part is the pure `sim/autopilot.ts` (`autopilotTargets`,
2853`autopilotPlan`), the UI part follows separately; (3) the route is the one with the FEWEST TOTAL
2854TURNS, not nearest-first: pairwise walking distances from `playerPath`, then the cheapest order
2855through every reachable coin, charge and uncovered gem ending at the exit (exhaustive over subsets
2856for up to 10 targets, nearest-first beyond), a target lying on the chosen leg counted as collected
2857free, ties broken by fewest direction changes, then smaller y, then smaller x (deterministic, so
2858replays agree); (4) it is re-planned every turn from the current state, using auto-move's safety
2859rules (`safeStep`); unreachable targets are skipped; a gem under a crumbling crate, or no way to the
2860exit while crates crumble, means waiting; (5) the autopilot's turns count, par bonus included (no
2861par protection; accepted). (6) SIM_VERSION 42 to 43, because a record holding `hoover` can no
2862longer replay. Autopilot's wording, speed, note and stop controls are decided in the UI part.
2863
2864UI part (same day, decided with the user): (7) the note is `#autopilot`, top centre below the top
2865bar, a pill like the toast but not a popover (dialogs cover it), text `role="status"`: "Autopilot:
2866cleaning up", "Autopilot: waiting for the crates", "Autopilot: off to the exit". It shows at once
2867on the last kill, even if a dialog from that turn is open; the walk starts after a 1.0 s beat
720cc9d3 2026-11-03 Pavel Safronov2868(and after the answer). (8) Speed: 2.5 moves a second (`AUTOPILOT_STEP_SECONDS` 0.4, was 0.25 = 4 a second until Q89, when a bug report found it too fast; auto-move
92cc4ee3 2026-11-02 Pavel Safronov2869stays at 0.5), no new animation; a Run only when owned. (9) Stop: Esc, pad L1 (its picture is on
2870the note) and a Stop button on the note for touch and mouse; stopping ends it for the level; so
2871does turning the setting off. A stall (6 waits in a row outside the crates phase, or no safe turn)
2872stops it with the toast "Autopilot stopped / Your turn". (10) While it walks the board takes no
2873input (`boardLocked`): taps, long-press, the action bar (dimmed), movement and action keys, `?`,
2874Shift + arrows, the pad cursor and face buttons; it still takes dialogs (a gem or charge pauses
2875the walk until answered), the menu, fullscreen, the top bar's info taps and looking around. A hold
2876begun before the lock is cancelled through the pad's context string. (11) The setting keeps its
2877stored key `autoFinish` so players' choices survive the rename.
2878
2879## A controller's selector shows what to hold (Q74, 2026-11-02)
2880
2881Bug report: with a controller the menu's switches (Sound, Autopilot) could not be changed. They
2882could: a hold of Cross toggles them. But a short press did nothing and nothing showed that it
2883should be held, because in selector mode only the footer's dismissing button had a picture, so
2884the hold ring (which goes round a `.pad-glyph[data-face]`) had nothing to go round. Decided: the
2885selected item carries the confirm button's picture (added on selection, removed when the selection
2886moves; skipped where the item has a picture already, the dismissing button's Circle and a shop
2887item's Buy), so the ring shows on it; a confirm button let go early shows the toast "Hold to
2888choose" once per dialog; the almanac's neighbouring tabs show LB / RB. The 0.5 s rule is
2889unchanged. Same day, the autopilot (decision 4 above) also waits for a ticking bomb or burning
2890flame that cuts it off from a target (phase `bombs`), and such waits are not stalls.
2891
2892## Move on a tile that is also a bomb target walks (Q75b, 2026-11-02)
2893
2894Bug report (controller): the player stood two tiles from an empty floor tile in a straight line,
2895holding an RPG. The cursor on it said "Fire an RPG bomb to this tile", Bomb was possible and Move
2896greyed, so the player could bomb a tile but not walk there. Cause (confirmed): the tile is bright
2897blue (an RPG target), `farTiles` leaves bright tiles out of the faded set, and `tileActions` offers
2898Move only where Move itself can target (next to you, or 2 straight with Run). `tapMeaning` does say
2899'walk' for such a tile (nothing picked), so a tap would have walked. Decided: the rule is "wherever
2900auto-move can walk, Move is offered". New pure `walkTo` / `cursorActions` in `ui/padTiles.ts`: on a
2901bright tile where Move is not possible but a safe, non-deadly walk exists, the Move entry becomes
2902the walk (`far: true`, "Walk here · N steps", "Keep walking" for the destination), and the other
2903buttons stay as `tileActions` has them. Pressing it takes `padDo`'s existing `far` path (nothing
2904picked, tap the tile), so it walks. Tiles Move can target keep their plain Move; faded tiles are
2905unchanged. The faded tint still leaves bright tiles out (they are blue already). No SIM_VERSION
2906change. Test: `test/q75.test.ts`.
2907
2908## The cursor still reaches every faded tile while a walk is set (Q75, 2026-11-02)
2909
2910Bug report (controller): a coin an enemy dropped, in a pocket beside a bomb target, could not be
2911reached by the cursor, and no faded tile showed anywhere; the screenshot had L1 "Cancel" on, which
2912means an action was picked or an auto-move destination was set. Investigated: the rules are right
2913(`test/q75.test.ts`: `tapMeaning` says 'walk', the coin is a `farTiles` entry, the first step is
2914not deadly, and the d-pad hops from the RPG tile down to it). The cause is Q69/Q70's rule that,
2915with a destination set, `farTiles` lists only the destination: any other walk was out of reach of
2916the cursor and nothing was tinted, with only "Cancel" to get out. Decided: `currentFar` in
2917`main.ts` no longer passes the destination, so every faded tile stays a candidate and tinted while
2918a walk is set; the cursor starts on the destination as before (`cursorStart`), and pressing Move
2919on another faded tile sets a new destination (`tapTile`). `farTiles`' `destination` parameter stays
2920for its tests. With an action picked, faded tiles are still hidden, as the picked button's targets
2921are the point. No SIM_VERSION change.
2922
2923### 2026-11-02 — Moving the mouse no longer takes the board off the pad or keyboard cursor
2924
2925User request: the cursor mode switched back to the mouse whenever the mouse moved (more than 3 px),
2926which made screenshots difficult while testing with a controller. Decided: only a click or tap
2927(`pointerdown`) or a key hands the board back (`handBack`); a plain mouse move does not. Amends the
2928Q65 keyboard-cursor rule 'a mouse move of 3 px or more hands the cursor back, like the pad'. The mouse's
2929hover route preview only shows while no pad or keyboard cursor is active, so it does not appear while
2930the mouse merely moves over a pad-driven board; a click on the board is the way back.
720cc9d3 2026-11-03 Pavel Safronov2931
2932### 2026-11-02 — Q79-Q82: cursor and aim fixes (Pavel's pad tests)
2933
2934- **Q79**: Wait pressed with the cursor on the player starts the next turn's cursor on the player, even
2935 with an auto-move walk set (`cursorPinned`); other actions still restart on the walk's destination or the player.
2936- **Q80**: `cursorStart` only picks the walk's destination if it is still a candidate tile (a flame can make
2937 it unsafe); a d-pad press from a tile with no action jumps to the nearest that has one (never stuck).
2938- **Q81**: modifier keys alone and anything with Cmd held (`ignoredForMode`) never hand the board back
2939 from the pad (screenshots). A click still does. Amends the 'any key hands the board back' rule.
2940- **Q82**: the "What is that?" aim now includes plain pillars and the outer wall (`explainableTiles(s, true)`;
2941 the demo keeps the old list), but starts on the nearest tile that is not a plain wall (`aimStart`). Up from the
2942 top row of tiles goes on to the top-bar items (Left/Right along them, Down back to the board; `Aim.hud`,
2943 outlined with `.pad-aimed`); the confirm button explains the item (`hudInfo`). Supersedes 'not plain pillars or the outer wall' above.
2944- **Q83**: while "What is that?" is on (pad), the action bar's four buttons are greyed and the label reads "Hold to find out what this is" beside the confirm button's picture, which carries the hold ring.
2945- **Q82 detail**: Up from the top row lands on the top-bar item nearest the aimed tile's column on screen.
2946- **Q84**: the L1/R1 legend (`#pad-hint`) sits at the top of the board under the top bar; L1 shows an undo arrow, R1 a magnifying glass (Game Icons), with full wording (`IDLE_L1_TEXT`).
2947- **Q85**: inspection with the pad follows the hold scheme: R1 on, d-pad moves, hold Cross (confirm) views, hold Circle (cancel) leaves; L1 only brings the aim back to the player's side. R1 while on does nothing (dimmed in the legend). The keyboard's Esc still leaves it. Supersedes 'RB again explains, LB leaves it' (2026-11-01).
2948- **Q86**: "What is that?" can rest on the player's own tile (`tileInfo` says "You"); L1 in the mode puts the aim there. The demo's list (`explainableTiles(s)`) and the aim's start still leave the player out.
2949- **Q87**: after the aim explains something (pad, keyboard), "What is that?" and the aim stay on; leaving is a held Circle, Esc or the dialog's "Walk there". Touch/mouse taps still end it (open question for Pavel).
2950- **Q88**: in "What is that?" the pad's Cross (explain) and Circle (leave) act on a single press, no ring (`PadTargets.instant`); amends Q85's hold. `INSPECT_HOLD = false` is the switch for the later option (with Q71's expert options).
2951- **Q88 detail**: the info dialogs opened by the aim (`inspectDialog`) also take a single press; every other dialog keeps hold-to-confirm (the menu has New, which must not fire on a tap).
2952- **Q88 detail 2**: the single press covers everything under an inspection action: dialogs opened from the info dialogs inherit `inspectDialog` until no dialog is left open.
2953- **Q89**: autopilot step 0.25 s -> 0.4 s (bug report: too fast). Touch got a Cancel button (a picked action or auto-move destination had no way out but Esc/L1): round, over the bar's top right, absolute so the bar's height (the board's inset) is unchanged; hidden for the pad, the demos and while inspecting. The "demo became really tall" screenshot (touch, scene 5/57, the board drawn as one thin column, sprites squeezed, no bar) could not be reproduced yet (open question to Pavel).
2954- **Q90**: bug report (seed 395025326, level 5): 'Couldn't move'. Replay: `offer` (5 gems) and `chargeOffer` (Acid) both pending after a slide; `isAvailable` refuses all actions while either is set, and no dialog was showing. Cause of the missing dialog unknown (offer dialog is `required`); suspects: a dialog opened over it (menu / bug report) replacing it, or a timing race between the pickup timers. Fix is a safety net: `reopenPendingChoice` reopens after 1.5 s without a dialog.
805e7a9f 2026-11-03 Pavel Safronov2955- **Menu Changes button (2026-11-02, Pavel: the version link was easy to miss)**: a fifth small button, Changes, opens the same list as the version link (which stays). The small buttons now sit on a six-column grid: Almanac, Scores, Changes (two columns each) in one row, Demo and Bug (three each) under them, because five in one row do not fit a phone.
2956- **Demo controls on a controller (2026-11-02, Pavel: shuffle/restart had no button)**: in the demo the d-pad up = shuffle, down = back to the first scene (left/right were already scene before/after), R1 or View = cycle the Input button; the same 400 ms throttle as left/right so a held d-pad does not flip shuffle repeatedly. Each demo control shows its pad picture on its corner while the demo is drawn for a controller (`syncDemoGlyphs`, Q67 rule). Tried only with a mocked pad in a browser: no unit test (the logic is in main.ts).
2957- **The banner is a panel (2026-11-02, Pavel: the level-cleared screen looked unfinished, 'should it be a dialog?')**: restyled, not migrated. `#banner` keeps its own elements and the pad code in `padDialog.ts` (a `<dialog>` would mean moving the status-driven refresh, the replay clip and the hold ring); it now has a `.panel` child with the dialog's parchment 9-slice, the dialog's backdrop and `.ui-button` buttons (primary green, the others plain). Side effect: the old `button.secondary` / `button.link` styles are gone. Checked in a browser at phone and desktop width with a mocked pad (the glyphs sit on the three buttons).