explodorama / files / decisions.md
decisions.md
5551eb37 2026-10-20 Pavel Safronov | 1 | # Decisions |
| 2 | ||
| 3 | Each design or technical decision, why it was made, and when. Newest last. The full design is | |
| 4 | in [PLAN.md](PLAN.md); this file records the reasoning so it isn't lost. | |
| 5 | ||
| 6 | ## Design (agreed in planning, 2026-10-19) | |
| 7 | ||
| 8 | 1. **Enemies move before bombs explode.** Enemies announce their next move (intent arrows), so | |
| 9 | the player can plan traps. Luck matters less than planning. | |
| 10 | 2. **Run is an ability, not a charge** — gained like the NES roller skates. | |
3321c921 2026-10-27 Pavel Safronov | 11 | 3. **3 lives**, plus a rogue-like layer of coins and upgrades. |
5551eb37 2026-10-20 Pavel Safronov | 12 | 4. **No undo.** |
| 13 | 5. **All actions in PLAN.md §3** are in scope. | |
| 14 | 6. **Name: Explodorama.** "Bomberman" is a Konami trademark and must never appear in the game. | |
| 15 | 7. **Shop purchases last for the current run only.** | |
| 16 | 8. **Permanent upgrades come from a Workshop between runs** (Everspace-style tracks, tier n costs | |
d603166d 2026-10-25 Pavel Safronov | 17 | 5 × n coins). Later milestone. *Dropped 2026-10-24 (see that day's entry).* |
| 18 | 9. *(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 Safronov | 19 | time. See PLAN.md §9. |
| 20 | 10. **Curses only appear after the first shop.** | |
| 21 | 11. **Reapers and the turn limit come later.** | |
| 22 | 12. **50 levels and an ending**, with a victory certificate and QR code. | |
| 23 | 13. **Icons: Icongeek26 "Outline" from Flaticon**, with attribution, until there is final art. | |
644cfd0b 2026-10-20 Pavel Safronov | 24 | LittleJS ships no icons. *Replaced 2026-10-20 — see "Art: Kenney's New Platformer Pack" below.* |
5551eb37 2026-10-20 Pavel Safronov | 25 | |
| 26 | ## Technical | |
| 27 | ||
| 28 | ### 2026-10-19 — LittleJS pinned to 1.25.0 | |
| 29 | ||
| 30 | Exact version, the newest release that was at least two weeks old at install time (1.27.2 was | |
| 31 | 4 days old). Upgrade deliberately, not by range. | |
| 32 | ||
| 33 | ### 2026-10-19 — Canvas 2D renderer, not WebGL | |
| 34 | ||
6e94f995 2026-10-24 Pavel Safronov | 35 | *Superseded 2026-10-24: WebGL is back (see "WebGL again; Developer mode's gem is a rainbow gem").* |
| 36 | ||
5551eb37 2026-10-20 Pavel Safronov | 37 | `setGLEnable(false)`. Headless Chromium's full-viewport screenshots of LittleJS's WebGL canvas |
| 38 | showed a black strip below the HUD that is not in the drawing (a clipped screenshot of the same | |
| 39 | page didn't show it, and Canvas 2D doesn't show it at all). Screenshots are how changes are | |
| 40 | reviewed before committing, so they must be trustworthy. The board is a few hundred rectangles; | |
| 41 | Canvas 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 | ||
| 45 | Press shows the commit preview on the tile under the finger, sliding changes it, release | |
| 46 | commits. That needs press, move and release positions, which pointer events give directly. | |
| 47 | LittleJS 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 | ||
| 51 | A blast reaching another bomb sets it off and stops there; the second bomb's own blast covers | |
| 52 | the tiles beyond. Soft blocks hit during a chain are only removed once the whole chain is done, | |
| 53 | so 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 | |
| 58 | and service worker will be added then. | |
644cfd0b 2026-10-20 Pavel Safronov | 59 | |
| 60 | ### 2026-10-20 — Art: Kenney's New Platformer Pack, replacing the Flaticon plan | |
| 61 | ||
| 62 | The 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 | |
| 64 | power-ups, and its coins, doors, bomb and HUD icons. This replaces design decision 13 (Icongeek26 | |
| 65 | outline icons over a board of plain drawn shapes). The pack is CC0, so attribution is optional; | |
| 66 | the 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 Safronov | 79 | |
| 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 Safronov | 94 | state; replays stay exact. `SIM_VERSION` is now 2 (3 after the lingering-flame change below). |
c37a2b0b 2026-10-20 Pavel Safronov | 95 | - **Death banner says "Ouch!"**, not "Boom.", since enemies can kill too now. |
| 96 | ||
82adee18 2026-10-20 Pavel Safronov | 97 | ### 2026-10-20 — Touch shortcuts: tap self to wait, hold to bomb, tap far to auto-move |
| 98 | ||
| 99 | Asked for by the user after playing. All three live in the UI layer (`main.ts`); the sim only | |
| 100 | ever 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 | ||
| 127 | The user asked for the flag itself to act like Go, so the thumb can stay on the board. That | |
| 128 | took over "tap the flag again to clear it", so clearing moved to tapping a wall or crate (a tap | |
| 129 | that otherwise does nothing), besides Esc and the existing automatic cases. | |
| 130 | ||
| 131 | Testing the flag tap in a browser walked the player straight into a Blob's announced step. So | |
| 132 | auto-move now refuses any step it knows is deadly — onto an enemy, onto a tile an enemy's arrow | |
| 133 | points at, or into flame this turn — and shows that next step in red. Manual moves are not | |
| 134 | restricted: the player can still choose a risky step on purpose. | |
| 135 | ||
| 136 | ### 2026-10-20 — Lingering flame, and enemies planning 3 moves ahead | |
| 137 | ||
| 138 | The user found kills too hard: flame lasted one turn, and an enemy had to be on the exact tile at | |
| 139 | that moment. The cause: bombs take 3 turns, but enemies only showed 1 move, and Blobs keep | |
| 140 | their direction only 75% of the time, so a 3-turn guess was right about 42% of the time. Agreed | |
| 141 | fix, 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 Safronov | 161 | ### 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 Safronov | 164 | loses Crate Pass, Bomb Pass, Flame Pass, the Detonator and a Star — the NES rule plus "Keep". |
27f24fed 2026-10-20 Pavel Safronov | 165 | 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 Safronov | 181 | ### 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 Safronov | 198 | ### 2026-10-21 — Board sizes, Developer / God / crumbling-crates modes, player animation |
| 199 | ||
| 200 | Decisions 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 | ||
| 213 | Made 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 Safronov | 242 | ### 2026-10-22 — The outer wall gets borders |
| 243 | ||
| 244 | The 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 Safronov | 257 | |
| 258 | ### 2026-10-22 — "What is that?" moves behind a ? button | |
| 259 | ||
| 260 | The user asked for info to need a dedicated button first, so a plain tap on the board either does | |
| 261 | nothing 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 | ||
| 285 | User 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 | ||
| 303 | The user asked that nothing just appear, disappear or jump any more (enemies vanished, kicked | |
| 304 | bombs 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 Safronov | 327 | 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 Safronov | 332 | - **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 Safronov | 343 | ### 2026-10-22 — Fewer action buttons; Kick, Throw and Teleport become abilities |
| 344 | ||
| 345 | The user found there were too many action buttons and the "?" odd among them. Asked first: | |
| 346 | charges 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 Safronov | 373 | 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 Safronov | 375 | 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 Safronov | 417 | *Superseded 2026-10-23 by the gem's choice dialog (see "Gems: choose 1 of 5 power-ups").* |
| 418 | ||
99869630 2026-10-23 Pavel Safronov | 419 | User request. Taking a power-up opens the info dialog for it ("You picked it up!" and what it |
| 420 | does, from the same texts as "What is that?"). It opens 0.45 s after the step, so the walk and | |
| 421 | the pickup's rise-away animation are seen first; auto-move pauses while it is open, as for any | |
| 422 | dialog. Every power-up gets one, Heart and Coin bag included. Charge and coin drops don't: they | |
| 423 | are frequent, and their buttons and the top bar show them. Checked in a browser (a Kick | |
| 424 | power-up placed next to the player through a temporary hook, removed): closed at 0.2 s, open | |
0b61e800 2026-10-23 Pavel Safronov | 425 | with "Kick" by 0.9 s, the ability gained. *Partly superseded the same day for charges: see the |
| 426 | next entry.* | |
| 427 | ||
| 428 | ### 2026-10-22 — A dialog for the first charge of each kind in a run | |
| 429 | ||
| 430 | User request, after picking up a Shield on level 1 and getting no explanation: a new button | |
| 431 | appeared with nothing to say what it was. Now the **first** pickup of each kind of action charge | |
| 432 | in a run opens a dialog ("Shield charge", "You picked it up!", what it does, and that each charge | |
| 433 | is used once and takes the turn), at the same 0.45 s delay as a power-up's. Later pickups of | |
| 434 | that kind show nothing, which keeps the reason above (charges are frequent). Coins still get no | |
| 435 | dialog. | |
| 436 | ||
| 437 | "In this run" was chosen over "ever, in this browser": a new run shows them again. The kinds | |
| 438 | seen are kept in `main.ts`, not in the sim or the save: after a reload they are rebuilt from the | |
| 439 | saved 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 | |
| 441 | to the player through a temporary hook, removed): first Shield opens the dialog by 0.9 s, a | |
| 442 | second Shield doesn't, then a first Freeze does. | |
99869630 2026-10-23 Pavel Safronov | 443 | |
| 444 | ### 2026-10-22 — The almanac | |
| 445 | ||
| 446 | User request: to see every power-up and enemy (some sprites are to be replaced). **Menu → | |
| 447 | Almanac**: one dialog with three tabs — Power-ups (all 14, in reading order: stats, abilities, | |
| 448 | passes, the rest), Charges (all 7) and Enemies (the 3 built, each with the level it first | |
| 449 | appears on). Each entry is the sprite plus the same words as "What is that?", so there is one | |
| 450 | text per thing. Tabs swap the list without closing the dialog. Sprite file names aren't shown in | |
| 451 | the game; for picking replacements, labelled sheets of every sprite (name and file) were made | |
| 452 | outside 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 | ||
| 479 | User request: the astronaut on the Move button looked like the player. Move is now Board Game | |
| 480 | Icons' `character` (a game-piece figure) and Wait its `character_remove` (the figure with an ✕: | |
| 481 | stand 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 | |
| 483 | dark explanation strip. Skates was defined as "the Move icon plus fast-forward", so its badged | |
| 484 | icon 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 | ||
| 488 | User request, symbols named by the user: Teleport = the Move figure + an outbox (Game Icons | |
ebcd0a8f 2026-11-02 Pavel Safronov | 489 | `export`), Detonator = bomb + `exclamation`, Crate Pass = crate (`block_planks`) + `cross`, Bomb |
99869630 2026-10-23 Pavel Safronov | 490 | Pass = 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 | |
| 492 | no longer used and were removed; `lever` stays (the "Run settings" info icon). Star, Heart and | |
| 493 | Coin bag keep their plain sprites. | |
| 494 | ||
| 495 | ### 2026-10-22 — The fuse shows the timer, not a torch | |
| 496 | ||
| 497 | User request: the lit torch beside the fuse number in the top bar (and on its info dialog) was | |
| 498 | unclear. 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 | |
| 500 | the dark top bar. `torch_on_a` is no longer used and was removed. | |
| 501 | Also: the top bar's bombs, blast and fuse info still named the old gems ("Bomb +1 (blue gem) | |
| 502 | adds one"); they now say "The Bomb +1 power-up adds one" and so on. | |
| 503 | ||
| 504 | ### 2026-10-22 — Wait is an hourglass | |
| 505 | ||
| 506 | User 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 | ||
| 512 | User request. The power-up that lets Move go 2 tiles straight is now **Run** everywhere: on | |
| 513 | screen, 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 | |
| 515 | its old key `'skates'`, so every seed still has it on the same level. Earlier entries here keep | |
| 516 | the old name, as they were written. | |
| 517 | ||
52d68b59 2026-10-24 Pavel Safronov | 518 | ### 2026-10-23 — Auto-move never crosses the open exit |
| 519 | ||
| 520 | User report: the dotted way to a power-up went over the open exit, so walking it ended the | |
| 521 | level without the power-up. `playerPath` now treats the open exit as blocked unless it is the | |
| 522 | destination. If the only way somewhere is over it, there is no way (the flag can't be set): ending | |
| 523 | the level by surprise is worse. A locked exit is still crossed, since stepping on it does | |
| 524 | nothing; 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 | ||
| 530 | User report: with the player on the bottom row, the explanation strip covered that row, so it | |
| 531 | couldn't be tapped. The user's rule: **the strip must not cover any of the board.** Of the two | |
| 532 | ways to do that, the user chose a band that is always kept free (over a board that shrinks only | |
| 533 | while the strip is open). The strip is now part of the action bar, at its top; the board is | |
| 534 | fitted above the whole bar, as before. | |
| 535 | ||
| 536 | How tall: the tallest strip that could open now (every action that can be picked, plus the one | |
| 537 | open), measured off-screen (`reserve` in `ui/controls.ts`). Measured on a phone: 103 px with | |
| 538 | nothing collected, 181 px holding Pierce / Power / Line (Bomb's choices), 264 px in Developer | |
| 539 | mode; on a desktop about 105 px. Reserving the Developer-mode maximum always would have cost a | |
| 540 | phone about a third of the board, so the band follows what the player holds: the board resizes | |
| 541 | when an upgrade or a charge is gained or spent, never when an action is picked. It is measured | |
| 542 | again 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 | ||
| 546 | User request: instead of a fixed power-up, every level's pickup is a gem of random colour, and | |
| 547 | taking 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 | ||
| 557 | How it works: taking the gem sets `state.offer` (the event `powerUpTaken` carries it); while it | |
| 558 | is set, every action is unavailable except `{ id: 'choose', kind }`, which applies the power-up | |
| 559 | and clears the offer without a turn passing. The choice is an action in the run record, so it | |
| 560 | replays; a reload with a choice open shows it again. The offer comes from the seed and level, so | |
| 561 | a retry after a death offers the same, and dying can't reroll it (unless the death cost a pass, | |
| 562 | which may then be offered). A death on the pickup turn drops the offer, and the retry has the gem | |
| 563 | back; reaching the exit on the same step (a Run over the gem) still needs the choice before the | |
| 564 | next level. A Star chosen this way lasts its full 8 turns (taken mid-turn it had lost one). | |
| 565 | The colour is only how it looks: `gem_blue` / `gem_green` / `gem_red` / `gem_yellow` from the New | |
| 566 | Platformer Pack (the gems were power-up sprites until 2026-10-22). The old "You picked it up!" | |
| 567 | dialog is gone; the choice already says what each does. `SIM_VERSION` 8 (9 since the shelter rule below): saved runs from before | |
| 568 | are dropped (the record's meaning changed). Level layouts are unchanged: the colour uses the | |
| 569 | level stream's last draw, where the power-up's kind used to be. | |
| 570 | ||
| 571 | Checked in a browser (a gem placed next to the player through a temporary hook, removed): the | |
| 572 | dialog opens with 5 choices and no close button, stays open on Esc and on a backdrop tap, Wait | |
| 573 | is refused meanwhile, choosing Fire +1 raised the blast from 1 to 2 with the turn unchanged, and | |
| 574 | play 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 | ||
| 578 | User request: the exit (the pack's two-tile door squeezed into one tile) was too tall. It is now | |
| 579 | only the door's arched top piece (`door_closed_top` / `door_open_top`), at full tile size: a | |
| 580 | short doorway, orange while locked, dark once open. The bottom halves are no longer used and | |
| 581 | were removed from `assets/kenney/`. | |
| 582 | ||
| 583 | ### 2026-10-23 — No explanation strip during play | |
| 584 | ||
| 585 | User feedback on the band above: "the text box keeps resizing, that's definitely not going to | |
| 586 | work" (the band followed what the player held, so the board changed size as charges came and | |
| 587 | went). Since power-ups are now explained in the gem's choice and charges on their first pickup, | |
| 588 | the user chose to **show no explanation text during play** at all. "?" then a button still | |
| 589 | explains it (`actionInfo`). | |
| 590 | ||
| 591 | The 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 | ||
| 598 | The second row now holds up to six buttons, so its buttons are a sixth wide (they were a fifth): | |
| 599 | all six fit in one row even on a 320 px phone, so a third row never comes and goes. The row | |
| 600 | itself still appears with the first charge held and goes with the last one, as before. | |
| 601 | Checked in a browser with every charge held (390, 360 and 320 px wide): the bar didn't move | |
| 602 | while picking Move, Bomb, Pierce, Line and Decoy or after using charges; one tap on Shield only | |
| 603 | picked it and the second used it; Freeze with Enter; a Pierce bomb placed with its button and | |
| 604 | an arrow key. The charge texts no longer say "a choice under Bomb", and the first-pickup dialog | |
| 605 | and "?" say how each charge is used. | |
| 606 | ||
| 607 | ### 2026-10-23 — Auto-move's way is blue tiles, with no flag | |
| 608 | ||
| 609 | User request: no flag on the destination; highlight the way's tiles blue. Every tile of the way, | |
| 610 | the destination included, gets the same blue as a picked action's targets (the dots and the | |
| 611 | flag sprite are gone). The next step still turns red when it isn't safe. Tapping the destination | |
| 612 | still takes a step, as tapping the flag did. The Go button kept its flag icon at first; see the | |
| 613 | next entry. | |
| 614 | ||
| 615 | ### 2026-10-23 — Enemies let out by flame are sheltered from it | |
| 616 | ||
| 617 | User report: a bomb on the uncovered (locked) exit let out 3 enemies, and the remaining fire | |
| 618 | killed them at once. Why: they appear on the exit tile at the end of the turn, and the blast | |
| 619 | lingers 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 | ||
| 622 | User's choice (of: shelter them from that flame; let them out a turn later; immune to all flame | |
| 623 | for a turn; put them outside the blast): **they can't be hurt by the flame that let them out, | |
| 624 | but a new explosion kills them as usual.** `Enemy.sheltered` is set when they appear; on the next | |
| 625 | turn only new flame counts against them, then it is cleared. Tests check both halves, and that | |
| 626 | the first one fails without the rule. `SIM_VERSION` 9. | |
| 627 | ||
| 628 | ### 2026-10-23 — Developer mode starts each level next to the gem | |
| 629 | ||
| 630 | User request. In Developer mode the level's gem lies uncovered right of the start (a tile that is | |
| 631 | always open floor), every level and after every death, to try the choice without hunting for it. | |
| 632 | The crate that hid it stays a crate. Normal runs are unchanged. | |
| 633 | ||
| 634 | ### 2026-10-23 — Screenshots regenerated | |
| 635 | ||
| 636 | User request, after today's changes. `power-up` / `run` became `gem` (seed 55's gem uncovered) | |
| 637 | and `gem-offer` (its choice); `charges` now shows the six charge buttons with Pierce picked (Bomb's | |
| 638 | choices in the strip are gone); comments in `tools/screenshots.mjs` updated for the blue way and | |
| 639 | the missing strip. All 26 were taken again with `npm run screenshots`. | |
| 640 | ||
| 641 | ### 2026-10-23 — No glow behind the gem | |
| 642 | ||
| 643 | User request: the yellow circle drawn behind an uncovered gem is gone; the gem is drawn on its | |
| 644 | own, still bobbing gently. (The glow dates from when the pickup was a power-up icon that needed | |
| 645 | to stand out from drops; a gem is distinct enough.) | |
| 646 | ||
| 647 | ### 2026-10-23 — Developer mode is only the gem | |
| 648 | ||
| 649 | User request: Developer mode no longer gives every ability and pass, 5 bombs, blast 5 and | |
| 650 | unlimited charges; it **only** puts the level's gem uncovered right of the start (see "Developer | |
| 651 | mode starts each level next to the gem"). Gone with it: `DEV_STATS`, `DEV_CHARGES`, charges never | |
| 652 | being spent, and the ∞ badges and "(unlimited)" labels. Its description in the New run dialog, | |
| 653 | the top bar's mode tag and the docs say so. The `move-picked` screenshot, which used Developer | |
| 654 | mode to show every Move upgrade, now shows a plain Move. Open question 5 of PLAN.md §5b (Developer | |
| 655 | mode bypassing slots) no longer applies. `SIM_VERSION` 10, since a run saved with the option on | |
| 656 | would replay differently. | |
| 657 | ||
| 658 | ### 2026-10-23 — Go keeps the walk icon | |
| 659 | ||
| 660 | User request: while walking to a destination, the Move button (Go) no longer turns into a green | |
| 661 | flag; it keeps Move's walk icon. It still gets its green border, so it shows that a press walks | |
| 662 | a step, and its name for screen readers and the tooltip is still "Go". `flag_green_a` is now only | |
| 663 | the level's icon (top bar, level-cleared banner). | |
| 664 | ||
| 665 | ### 2026-10-23 — Charges work on pickup, as temporary changes | |
| 666 | ||
| 667 | User request: charges are no longer actions but temporary changes, applied the moment they are | |
| 668 | picked 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 Safronov | 672 | | Pierce | next 3 bombs' flame goes through crates | Crate Pass icon | |
52d68b59 2026-10-24 Pavel Safronov | 673 | | 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 | ||
| 680 | Line is gone (not in the list). Choices: 3 bombs per Pierce / Power / Nuke pickup (over 1 or 5); | |
| 681 | Nuke reach 2 (over 3: with a fuse of 3, 2 is escapable on foot, 3 hardly without Run). | |
| 682 | ||
| 683 | How it works: `state.charges` is replaced by `state.boosts` (bombs left of each bomb boost, and | |
| 684 | the 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, | |
| 686 | one more level of Diagonal). Every bomb placed or thrown uses one of each bomb boost held, so | |
| 687 | they combine (`Bomb.pierce` / `power` / `nuke`, replacing `Bomb.special`). Boosts and an unbroken | |
| 688 | shield go on to the next level and through a death, as charges did. The shield, freeze, decoy, | |
| 689 | pierce, power, line actions and their buttons, keys (H, F, Y, I, O, L) and the Shield / Freeze | |
| 690 | double tap are gone; the bar is Move, Bomb and Wait; Diagonal stays an action of Move (G, then Q | |
| 691 | E Z C) while it lasts. The top bar shows each active charge with its count (bombs, levels or | |
| 692 | turns left; the Shield while it's up), and tapping one explains it. Shield, Freeze and Decoy's | |
| 693 | one-colour icons turn white there (MONO_ICONS); the Diagonal icon, like Run's, stays dark on the | |
| 694 | bar. `?charges=N` now starts as if N of each lasting charge had been picked up (not Freeze or | |
| 695 | Decoy, which act on the spot). The new icons are built by `tools/compose-icons.sh` from source | |
| 696 | icons copied unchanged into `assets/kenney-board-icons/` (`skull`, `timer_0`, `pawns`, | |
| 697 | `arrow_diagonal_cross`) and `assets/kenney-icons/White/` (`locked`). `SIM_VERSION` 11. | |
| 698 | Screenshots: `pierce-decoy` became `nuke` (a bomb with all three badges and the Nuke's reach in | |
| 699 | the 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 | ||
| 703 | User 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 | |
| 729 | README'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, | |
| 731 | with the day's changes in them. | |
| 732 | ||
| 733 | ### 2026-10-23 — A death keeps every power-up | |
| 734 | ||
| 735 | User request: power-ups stay after a death. `carryOf` now carries everything to the retry — | |
| 736 | stats, every ability and pass (Detonator, Wall / Bomb / Flame Pass included), coins, charges' | |
| 737 | boosts and an unbroken Shield; a death costs only the life. (A Star can't be running at a death, | |
| 738 | since nothing hurts the player while it lasts.) The "Kept / Lost when you die" endings are gone | |
| 739 | from 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 | |
| 741 | choice on death (§9, not built) took the passes away in both options, so what Keep vs Fresh | |
| 742 | costs needs rethinking before it is built — marked open there. §5b's question 4 (what a death | |
| 743 | costs with slots) is settled: nothing. `SIM_VERSION` 13. | |
| 744 | ||
| 745 | ### 2026-10-23 — Enemies spread out | |
| 746 | ||
| 747 | User report: enemies stacked on one tile moved in the same direction, taking up one tile | |
| 748 | together — "not fun, they should go their separate ways". Why: enemies plan without looking at | |
| 749 | each other and may share a tile, so enemies on one tile make the same choices (Drifters go | |
| 750 | straight; Seekers take the same shortest path; Blobs keep their heading 3 times in 4), and the 3 | |
| 751 | let out of a blasted exit or gem start on one tile facing the same way (and since the shelter rule | |
| 752 | they live). | |
| 753 | ||
| 754 | Now, in `planIntents`, enemies are planned in turn and each keeps clear of where the ones before | |
| 755 | it will be at each of the next 3 moves (`spreadSteps`): a Blob wanders among the clear steps, a | |
| 756 | Drifter turns if straight on is another enemy's, a Seeker's step toward the player is dropped | |
| 757 | for a wander if another enemy takes that tile. A crowded step is used only when no open step is | |
| 758 | clear. Enemies still don't block each other, so each still makes exactly the moves its arrows | |
| 759 | show. Tests: three of a kind on one tile in an open room never share a tile over their 3 | |
| 760 | planned 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 | ||
| 765 | User report: on a phone (under `npm run dev`), the game froze with the player missing from the | |
| 766 | board. Why it froze rather than carrying on: LittleJS's development build stops its frame loop at | |
| 767 | the 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 | |
| 770 | texture; every sprite is 128 × 128 except the new Freeze icon (`charge_freeze`), inked from a | |
| 771 | 100 × 100 Game Icons file, so its tile was invalid and the development build's check threw while | |
| 772 | drawing the drops — after the coin drawn before it, so the enemies, fading things and the player | |
| 773 | were never drawn. Found by replaying the screenshot's situation (Small, Developer, Crumbling | |
| 774 | crates, the last enemy blasted) on 16 seeds in a browser: seed 13 dropped a Freeze and failed | |
| 775 | with "Sprite charge_freeze (texture 68): Assert failed". Fixed: `tools/compose-icons.sh` makes | |
| 776 | every inked icon 128 × 128; a test (`test/sprites.test.ts`) checks every PNG in `assets/kenney` | |
| 777 | and `assets/composed` is 128 × 128 (it fails with the old file); the same seed then played on to | |
| 778 | turn 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 Safronov | 832 | |
| 833 | ### 2026-10-23 — An enemy walks into the flame before it dies | |
| 834 | ||
| 835 | User report (spotted in the demo): an enemy walking into flame started its death on the new | |
| 836 | tile at once, without ever leaving the old one. Its step and its death come in the same turn, | |
| 837 | and the death replaced the step. Now an enemy that moved this turn and dies in it finishes the | |
| 838 | step first (the same 0.16 s hop as any enemy step, from the tile it left), then flips over and | |
| 839 | falls 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 | |
| 841 | browser: 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 | ||
| 845 | User request: the demo's scenes should show every feature of the game except the shop and the | |
| 846 | exit, and one more scene on the enemies' move arrows ("a good idea to watch those"). The scenes | |
| 847 | are 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 Safronov | 849 | a 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 Safronov | 850 | column; arrows: an enemy killed on the tile it stepped onto, the bomb waiting there. 27 scenes: |
| 851 | kill, arrows, escape, chain, lingering flame, the enemy types, drops, the gem (uncovered and | |
| 852 | taken; its choice dialog isn't shown), Bomb +1, Fire +1, Fuse +1, Run, Kick, Throw, Teleport, | |
ebcd0a8f 2026-11-02 Pavel Safronov | 853 | Diagonal, the Detonator, Crate Pass, Bomb Pass, Flame Pass, Star, Pierce, Power, Nuke, Shield, |
fa3e81a7 2026-10-24 Pavel Safronov | 854 | Freeze and Decoy. Heart and Coin bag are left out: they only change numbers in the top bar, |
| 855 | which the demo hides. The random play now also uses Run, Kick, Throw, Teleport and Diagonal | |
| 856 | when held. Pierce rarely found two crates in a row, so its room gets extra crates. Every | |
| 857 | caption now has an icon. Tests: each kind is found on 6 seeds, replays with the player alive, | |
| 858 | and `showsAtEnd` confirms its last turn is its moment; probes found every kind on 15–30 seeds, | |
| 859 | the slowest under 0.4 s. A minute of the demo in a browser played 8 scenes with no errors. | |
8da5a7c0 2026-10-24 Pavel Safronov | 860 | |
| 861 | ### 2026-10-23 — No second Decoy, Freeze or Shield while one would be wasted | |
| 862 | ||
| 863 | User report (from the demo): two Decoys were dropped, the second while the first still lay | |
| 864 | there. The last-enemy rule didn't cover it (the enemy wasn't the last). One decoy is out at a | |
| 865 | time, a second Freeze adds no turns, a second Shield nothing — so an enemy's charge drop now | |
| 866 | leaves out a Decoy, Freeze or Shield that already lies on the board or is working (`wasted` in | |
| 867 | enemies.ts), on top of the last-enemy rule. Pierce, Power and Nuke add up and are never left | |
| 868 | out, so there is always something to drop. A search over 40 seeds of every demo scene found 6 | |
| 869 | such drops before; after, only 2 Freezes dropped in the turn the old freeze ran out (rolled once | |
| 870 | it had, so not wasted). Tests: over 2,000 rolls each, none of the three drops while one lies | |
| 871 | there or works; Pierce still does. `SIM_VERSION` 16. | |
d5e3ad4d 2026-10-24 Pavel Safronov | 872 | |
| 873 | ### 2026-10-23 — The demo shows how you die, and the exit | |
| 874 | ||
| 875 | User request: "how does the player die?" — scenes for the ways to die (walking into flame, | |
| 876 | blocking yourself in, not getting far enough, a bomb setting off another, an enemy, "etc."), | |
| 877 | alternating living and dying scenes, "some funny ones maybe"; and the exit, with scenes ending | |
| 878 | naturally 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 Safronov | 897 | |
| 898 | ### 2026-10-23 — No drops on the uncovered exit | |
| 899 | ||
| 900 | User report: an enemy died and its charge landed on the closed (uncovered, still locked) exit. | |
| 901 | That hides the door, and once the last enemy dies the drop can only be taken by stepping on the | |
| 902 | exit — which ends the level — while auto-move won't cross the open exit at all. `dropSpot` now | |
| 903 | counts the uncovered exit as taken, like a drop, a bomb or the uncovered gem: a drop there rolls | |
| 904 | to the nearest free floor tile (up to 2 steps). An exit still under its crate isn't floor, so | |
| 905 | drops never reached it. Test: a Blob dying on the uncovered exit (in lingering flame — a new | |
| 906 | blast there would let enemies out) never leaves a drop on it over 500 rolls; it fails without | |
| 907 | the rule. `SIM_VERSION` 17. | |
| 908 | ||
| 909 | ### 2026-10-23 — No throwing over a pillar | |
| 910 | ||
| 911 | User question: "a bomb throw shouldn't be able to go through a column/pillar, right?" Throw had | |
| 912 | been "exactly 2 tiles away, over anything" since the first plan (as in the later Bomberman | |
| 913 | games, where a thrown bomb sails over blocks) — the plan's wording, not a user choice. Of the | |
| 914 | options (not over pillars or walls; only over open floor; keep over anything) the user chose | |
| 915 | the first: a pillar or the outer wall on the tile in between blocks a throw; a crate, a bomb or | |
| 916 | an enemy there can still be thrown over. Pillars look like tall solid columns, and crates stay | |
| 917 | a reason to have Throw. Teleport still goes "over anything" (the question was about throws). | |
| 918 | Tests: 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 | ||
| 922 | User report: with Diagonal, a plain tap on a free diagonal tile walked one tile sideways first, | |
| 923 | as auto-move does for a far tile — a tap on the board took a direct step only for the four | |
| 924 | straight neighbours, and auto-move's way uses straight steps. Now a plain tap on a diagonal | |
| 925 | neighbour, with Diagonal, is a diagonal step, as a straight neighbour is a step. And with Move | |
| 926 | picked, Diagonal now comes before Teleport in Move's order (`GROUPS` in picks.ts), so a tile | |
| 927 | next to the player is always a step, and Teleport is for tiles further away (it used to win a | |
| 928 | diagonal neighbour too). Auto-move's way still uses straight steps only. Checked in a browser: a | |
| 929 | tap 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 | ||
| 934 | User report: with Teleport, tapping a far tile still walked there one tile at a time. The | |
| 935 | user's rule: a tile 1 step away is always a step; 2 steps away is two steps, or one run with | |
| 936 | Run, or one teleport with Teleport (or both); further away, run 2 or teleport as far as possible, | |
| 937 | then reassess. Built as `autoMoveAction` (sim/path.ts), used for every move of auto-move, | |
| 938 | including the first one on the tap: a neighbour is a step; otherwise every option — a step along | |
| 939 | the way, a Run along it, each Teleport landing — is scored by the walk left from where it lands, | |
| 940 | and the shortest wins, ties going to Teleport, then Run, then the step. Only tiles auto-move | |
| 941 | calls safe are landed on, and never the open exit unless it is the destination. Auto-move's own | |
| 942 | teleport doesn't end auto-move (a move off the way normally means "changed my mind"). With | |
| 943 | Teleport, a tile out of walking reach but within Teleport's (behind a pillar) is one teleport. | |
| 944 | Tests cover each case; in a browser, a tile 7 steps away took 3 turns (teleport, teleport, step). | |
39c26813 2026-10-24 Pavel Safronov | 945 | |
| 946 | ### 2026-10-23 — Developer mode's gem is an extra one | |
| 947 | ||
| 948 | User request: the gem next to the player in Developer mode is not the level's gem but an extra | |
| 949 | one, added for the developer. It used to be the level's own gem, moved beside the start, which | |
| 950 | left the level without its hidden gem. A level can now hold several gems (`state.gems`, which | |
| 951 | replaces `powerUp`); Developer mode adds one marked `extra` right of the start, in the next | |
| 952 | colour after the level gem's so the two look different, and it draws its own offer (`'offer-extra'` | |
| 953 | stream) so it doesn't offer the same five as the level's gem. The level's own gem stays hidden | |
| 954 | under its crate exactly as in a normal run. Burning, revealing, taking and drops' "never on a gem" | |
| 955 | work per gem. Tests: the level's gem is unchanged and hidden; the extra one is another colour | |
| 956 | and its offer differs on most seeds; a normal run has one gem. `SIM_VERSION` 19. | |
00ad7d0a 2026-10-24 Pavel Safronov | 957 | |
| 958 | ### 2026-10-23 — One board; crates always crumble; God becomes Immortal | |
| 959 | ||
| 960 | User request: move away from the "Normal" and "Small" labels, make the small board the main one, | |
| 961 | make crate crumbling the default and drop it from the options, so the New run menu has only the | |
| 962 | two modes; and rename God to **Immortal**, noting that it earns no achievements but does allow | |
| 963 | playing 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 | ||
| 992 | User 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 | |
| 994 | resilient?" It had: the step into the doorway, an enemy walking into flame, and now the player | |
| 995 | into flame. Each time, something else in the same turn (the level cleared, a death) replaced | |
| 996 | the step's animation, because the player had one animation slot, set from events. | |
| 997 | ||
| 998 | Now movement and pose are separate. **The move comes from the state**: the player's tile before | |
| 999 | and after the turn (`playerTransition`, a pure function in effects.ts), drawn as a slide or, for | |
| 1000 | a teleport, vanishing and reappearing. **The pose** — hit, jump, crouch — is drawn only once the | |
| 1001 | move is done. Nothing in the turn can skip a move any more. Enemies' steps are likewise taken | |
| 1002 | from their change of position (and a dying enemy's last step too), not from events. A new level | |
| 1003 | or a retry starts still. Tests (`test/animation.test.ts`): a step into flame is a move then the | |
| 1004 | hit pose; into the open exit a move then the jump; a teleport; a bomb is a crouch with no move; | |
| 1005 | a new level has no move. CONTRIBUTING.md now asks new animations to follow the pattern. Checked | |
| 1006 | in a browser: 70 ms after stepping into a blast the player is part-way there, walking; at 450 ms | |
| 1007 | they are in the hit pose on the flame tile. | |
| 1008 | ||
| 1009 | ### 2026-10-23 — The Move button's key is M | |
| 1010 | ||
| 1011 | User request: the Move button's key hint said "WASD", which isn't the whole truth with | |
| 1012 | Diagonal; make it M, like B for Bomb. M now picks Move (then a direction or a tile), and the | |
| 1013 | button shows "M". W A S D and the arrows still step at once, as before. With Diagonal, Q / E / | |
| 1014 | Z / C now step diagonally at once too (they needed G first), or with Move picked. Checked in a | |
| 1015 | browser: 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 | ||
| 1019 | User request: auto-move "at least 50% faster". Once every enemy is gone, the walk to the | |
| 1020 | destination now takes a step every 0.5 s (`AUTO_STEP_SECONDS`, was 1 s) — twice as fast. A | |
| 1021 | step's slide takes 0.16 s, so each one still finishes before the next. Each step is still one | |
| 1022 | turn, and a Run or Teleport still counts as one move. | |
0580e85e 2026-10-24 Pavel Safronov | 1023 | |
| 1024 | ### 2026-10-24 — The death banner says how and where | |
| 1025 | ||
| 1026 | User request: when the player dies, a short text says how and on which level, then play goes on | |
| 1027 | as before. The sim now records the death in the state (`GameState.death`: the cause, the enemy | |
| 1028 | for an enemy, and the turn), since the banner is drawn from the state and a reloaded game | |
| 1029 | replays to it. Causes: walked into an enemy (first contact check), an enemy walked into the | |
| 1030 | player (after the enemies move), caught in a blast of a bomb that was due (or set off by the | |
| 1031 | Detonator), caught only by a bomb a chain set off (it wasn't due), or by last turn's lingering | |
| 1032 | flame. The banner: "Ouch! Level 3, turn 12: you walked into a Blob. 2 lives left."; game over | |
| 1033 | puts the same line before the seed. Nothing is recorded when a death is prevented (Shield, | |
| 1034 | Star, 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 | ||
| 1038 | User request: when dying, choose between the same seed (dungeon) again and a new one from a | |
| 1039 | random seed — and should a player be able to type a seed? There was no reason not to (the seed | |
| 1040 | is already shown, and sharing or replaying a dungeon, races and a daily seed all need it; the | |
| 1041 | victory 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 | ||
| 1050 | Checked in a browser: "1234" gives seed 1234, "pavel" a hashed one, empty a random one; at game | |
| 1051 | over 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 | ||
| 1055 | User request: a local high-score board, based on coins or level reached, with the power-ups in | |
| 1056 | use, enemies killed, how the run died; kept local but planned for a global board; seeds kept; and | |
| 1057 | start 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 | ||
| 1076 | Checked in a browser: two game overs (the second a Developer run with more coins) each said "New | |
| 1077 | high score: #1!"; the board showed both, the Developer one first, red-bordered. | |
0b25f1ef 2026-10-24 Pavel Safronov | 1078 | |
| 1079 | ### 2026-10-24 — The version number is the rules version | |
| 1080 | ||
| 1081 | User 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 Safronov | 1095 | |
| 1096 | ### 2026-10-24 — WebGL again; Developer mode's gem is a rainbow gem offering everything | |
| 1097 | ||
| 1098 | User request: try the WebGL renderer again, and make Developer mode's extra gem "some sort of | |
| 1099 | rainbow/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 Safronov | 1122 | |
| 1123 | ### 2026-10-24 — The rainbow gem offers every charge too | |
| 1124 | ||
| 1125 | User request: so Freeze and the other charges can be tried without waiting for an enemy to drop | |
| 1126 | them. 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 | |
| 1128 | one (`applyCharge`): bomb boosts add BOMB_BOOST, Shield goes up, Freeze freezes every enemy, | |
| 1129 | Decoy is left on the gem's tile. Choosing takes no turn and comes after the gem's turn, so | |
| 1130 | a 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 | |
| 1132 | offer), and 23 itself is from earlier today. | |
| 1133 | ||
36c5d6a2 2026-10-24 Pavel Safronov | 1134 | ### 2026-10-24 — Demos get to the point, and show the controls |
| 1135 | ||
| 1136 | User request: scenes sometimes ran many moves, the player and enemies dancing around, before | |
| 1137 | the thing they're about; it should come within 3–6 steps of the start. And the demo should | |
| 1138 | teach the controls: a tap effect, the action buttons, a virtual finger, with scenes for moving, | |
| 1139 | a 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 Safronov | 1188 | |
| 1189 | ### 2026-10-24 — No compatibility code for old versions; surviving a rules change shelved | |
| 1190 | ||
| 1191 | The game hasn't been released, so there are no saves or settings to carry forward. The one | |
| 1192 | piece of old-version code, reading the New run choices' former `god` key as Immortal, is gone, | |
| 1193 | with the God name in comments and test names. The `SIM_VERSION` check on resume stays: it isn't | |
| 1194 | compatibility code, it keeps a move log from being replayed under rules it wasn't played by. | |
| 1195 | Keeping a run across a rules change (a state snapshot, the record noting the change) or playing | |
| 1196 | older versions was discussed and shelved by the user: a lot of effort for a doubtful pay-off | |
| 1197 | while nobody else plays (PLAN.md §11). | |
58f1e700 2026-10-24 Pavel Safronov | 1198 | |
| 1199 | ### 2026-10-24 — The demo's finger starts at the player; a "You" tag | |
| 1200 | ||
| 1201 | User request: the eyes follow the hand, so it should start close to the player; and it isn't | |
| 1202 | immediately 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 Safronov | 1217 | |
| 1218 | ### 2026-10-24 — The top bar in the demo, its tour, "What is that?"; the player stands higher; bumps no longer clipped | |
| 1219 | ||
| 1220 | User request: add the top bar to the demos, describe its items, and show how to use **?** to | |
| 1221 | learn about enemies. Also: draw the player a few pixels higher, so its legs never touch the line | |
| 1222 | between tiles; and (spotted meanwhile) the Turn item, bumping when a turn passes, was cut off by | |
| 1223 | the 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 | ||
| 1254 | User request: "try out LittleJS post-processing: can we make the demo appear on a CRT TV with | |
| 1255 | lines … like someone is actually playing on a touch-capable CRT"; then, on a wide desktop, limit | |
| 1256 | its 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 Safronov | 1262 | 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 Safronov | 1264 | 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 Safronov | 1274 | 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 Safronov | 1277 | 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 | ||
| 1291 | User request: most demo rooms had no door, which made captions that mention it (the enemies | |
| 1292 | counter: "once they're all gone, the exit opens") strange. Every room now gets one, as every | |
| 1293 | level does (`placeDoor`): under a crate five rooms in six, otherwise already uncovered and | |
| 1294 | locked while enemies live (open once they're gone); never under the gem's crate or on a drop, | |
| 1295 | and at least 3 steps from the player. The exit scenes still place theirs themselves. Since the | |
| 1296 | exit is part of the rules (flame on an uncovered one lets enemies out; once open, walking in | |
| 1297 | clears the level), the random games play with it; a probe over 40 seeds of every kind found all | |
| 1298 | but one Nuke scene (a missing scene is skipped, and the tests' seeds all pass), the slowest | |
| 1299 | 0.44 s. At a scene's start, 79% of the doors are still covered. | |
33a0256b 2026-10-24 Pavel Safronov | 1300 | |
| 1301 | ### 2026-10-24 — Clips in the info dialogs: plan, and the drawing spike | |
| 1302 | ||
| 1303 | User request: now that the demo works, give the info dialogs animations made on the spot the same | |
| 1304 | way. The plan (every dialog's clip, which demo scene each reuses, the new ones, the order) is in | |
| 1305 | PLAN.md §13c. The user's answers: the finger where it helps, no CRT, a still where one frame says | |
| 1306 | it best under reduced motion, a 1 s hold and a new setup every loop, and every item on the list. | |
| 1307 | How 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 | ||
| 1359 | User request: "loop through all the possible demos in order, indicate which number demo this is, | |
| 1360 | and allow left/right controls to switch demo scenes. When a page is reloaded, the demos continue | |
| 1361 | from the same spot. Add a round arrow or something to restart from the beginning, but make it a | |
| 1362 | choice." | |
| 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 | ||
| 1382 | The user turned Service Desk on and gave its address; it lives in the source (`BUG_EMAIL` in | |
| 1383 | main.ts), not a CI variable: it is public by design — the game's email link carries it either way — | |
| 1384 | and if it draws spam, GitLab can give the project a new alias. The menu's **Report a bug** opens | |
| 1385 | an email to it (`mailto:`), subject "Bug report: version 23, level N", body "What happened:" with | |
| 1386 | room to write, then the details: version and build commit, the device and its user agent, seed, | |
| 1387 | level, turn, lives, status and modes, the last 5 errors the game caught and carried on from (kept | |
| 1388 | by `reportError`, their first lines), and the run record, which replays the run exactly. Some mail | |
| 1389 | apps cut off long links, so over ~1,900 characters the record stays out of the email; the full | |
| 1390 | details go on the clipboard (only then, so the clipboard isn't overwritten for nothing), and the | |
| 1391 | email asks to paste them. Checked in a browser: a short run's email carries the record (919 | |
| 1392 | characters); after 90 turns it doesn't, and the clipboard holds the full details. | |
| 1393 | ||
| 1394 | ### 2026-10-24 — Sensible play in the demo | |
| 1395 | ||
| 1396 | User 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 | |
| 1398 | turn 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 | ||
| 1405 | A random game gets 6 goes at each turn instead of 4. On 40 seeds of every kind, every scene was | |
| 1406 | found (the one Nuke miss is gone too), the slowest in 0.5 s, in the worker. Tests: in the living | |
| 1407 | scenes 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 | ||
| 1411 | User report: the almanac wouldn't scroll. By mouse wheel it did (fixed 2026-10-22); by finger it | |
| 1412 | didn't: LittleJS listens for touches on the whole document and cancels every one that isn't on a | |
| 1413 | control (no page scrolling in a game), which also cancelled a finger dragging the almanac's list, | |
| 1414 | and the gem's long offer. Touches inside the dialog now stop there (as the wheel already did), so | |
| 1415 | LittleJS never sees them; the board is behind the dialog anyway. The dialog's body also allows | |
| 1416 | vertical panning (`touch-action: pan-y`) against the body's `none`. Checked with simulated touch | |
| 1417 | drags in a mobile browser: the almanac's list scrolls (it didn't on the last three commits), and | |
| 1418 | taps on its tabs and buttons still work. | |
32dd53cd 2026-10-25 Pavel Safronov | 1419 | |
| 1420 | ### 2026-10-24 — Clips, step 3: the dialogs that reuse demo scenes | |
| 1421 | ||
| 1422 | Each 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, | |
| 1424 | Kick, Diagonal; Bomb: the long press, or Throw, the Detonator; Wait), the top bar's level (the | |
| 1425 | exit), 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, | |
| 1427 | the gem, the exit locked (blasting it) and open (walking out), a drop, and the first-pickup | |
| 1428 | dialog of each charge. Lives, enemies left, turn and par, crates and walls wait for step 4's new | |
| 1429 | scenes; 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, | |
| 1432 | Kick, Throw, Teleport, Diagonal, the Detonator), as the user chose ("when helpful"): it glides to | |
| 1433 | the tile, taps (a ripple) or holds (the ring, then the armed bomb's blast), and the turn plays as | |
| 1434 | it lets go; a button's tiles light up first where the scene picks one, as in play. The action | |
| 1435 | bar isn't in a clip, so Wait is shown as a tap on the player, which waits in play too. The | |
| 1436 | finger is an image over the dialog's canvas, placed by tile. Checked in a browser: the Bomb, | |
| 1437 | Coins 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 | |
| 1439 | LittleJS'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 | ||
| 1443 | Three 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 | |
| 1445 | blast 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 | |
| 1447 | a number shows — turn and par — gets no clip, and nor do the modes. On 40 seeds each, all three | |
| 1448 | new scenes were found, the slowest in 0.09 s. Checked in a browser: the Crate, Pillar and Enemies | |
| 1449 | left dialogs play their clips; no errors. | |
| 1450 | ||
| 1451 | ### 2026-10-24 — Clips, step 5: the offer, the almanac, the "Ouch!" replay | |
| 1452 | ||
| 1453 | The user chose, of the options offered: one clip above the offer's cards, almanac entries that | |
| 1454 | open 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 Safronov | 1490 | |
| 1491 | ### 2026-10-24 — Clips: the modes, and stills under reduced motion | |
| 1492 | ||
| 1493 | The last of the clips plan. **The modes' tag** now plays a clip: the rainbow gem beside the | |
| 1494 | player, taken (Developer mode; a new clip-only scene, `rainbowGem`), or the player unharmed in a | |
| 1495 | blast or by an enemy (`immortal`); a run with both shows Developer's. **Under reduced motion** a | |
| 1496 | clip is a still, as the user chose ("whatever is clearer"): the scene played to its end at once | |
| 1497 | and held — the moment just after it, flame still on the board, the player's pose — with no finger | |
| 1498 | and no next loop. Checked in a browser with reduced motion emulated: the Blast dialog shows its | |
| 1499 | still, the flame on the board; both mode clips play; no errors. | |
| 1500 | ||
| 1501 | ### 2026-10-24 — The gem's offer: select, then Choose | |
| 1502 | ||
| 1503 | User report: in the (rainbow) gem's offer a tap on a card's description chose it at once; it should | |
| 1504 | be two steps — tap to select and see it in action, then confirm. Now a tap anywhere on a card | |
| 1505 | selects it (ringed green, `aria-pressed`), the clip above the cards plays it, and a **Choose Kick** | |
| 1506 | button (it names the selection) takes it; the first card starts selected. Every gem's offer works | |
| 1507 | this way, not only the rainbow one, so it is the same everywhere. The separate picture button and | |
| 1508 | the mouse-hover preview of the step before are gone: the selection is the preview. Checked in a | |
| 1509 | browser with touch: a tap on Kick keeps the dialog open and changes the button to "Choose Kick"; | |
| 1510 | Choose takes it (Kick appears in the top bar). | |
| 1511 | ||
| 1512 | ### 2026-10-24 — Dialogs fit the visible screen | |
| 1513 | ||
| 1514 | User report (a screenshot from a phone): the gem's offer ran off the bottom of the screen, its | |
| 1515 | Choose button cut in half. The dialog's body had a height guessed from the screen's (`100vh` less | |
| 1516 | 150 or 260 px for the rest); on a phone `100vh` includes the part behind the browser's address | |
| 1517 | bar, and the offer's two-line title, clip and Choose button took more than the guess. Now the | |
| 1518 | panel is a column never taller than what the browser shows (`100dvh`, less room for the close | |
| 1519 | button above it; `100vh` where `dvh` isn't known) and only its body scrolls, so the title and | |
| 1520 | buttons always show, whatever is in it. Checked at 390 × 640, 390 × 844 and 1280 × 600: the | |
| 1521 | rainbow 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 | ||
| 1525 | User report: selecting Heart or Coin bag in the gem's offer left the clip's place empty. They have | |
| 1526 | no clip (nothing changes on the board: a life, coins), so their picture now shows there, big, on a | |
| 1527 | pale box the clip's size and shape; any other selection brings the clip back. On the way: a | |
| 1528 | hidden 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 | |
| 1530 | replay, relied on it too. Checked in a browser: Heart and Coin bag show their pictures, Kick its | |
| 1531 | clip again. | |
7f242d36 2026-10-25 Pavel Safronov | 1532 | |
| 1533 | ### 2026-10-24 — The gem screenshots show the gem again | |
| 1534 | ||
| 1535 | User report: `gem-offer.png` showed the board, not the offer. Its seed-55 key presses no longer | |
| 1536 | reached the gem (the levels have changed since they were written), and `gem.png`, the step | |
| 1537 | before, showed no gem either. A short search with the sim — random moves, bombs and waits on | |
| 1538 | seeds 1–120, keeping the shortest that ends with the gem taken and the player alive — found seed | |
| 1539 | 107: down, a bomb below, wait, up, wait three times, down twice (the gem uncovered beside the | |
| 1540 | player: `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 | ||
| 1544 | User requests: with Teleport, Run should extend it (Teleport already reaches every tile a run | |
| 1545 | does); a power-up to "send the bomb as far as it can fly" (RPG); a charge that flings the bomb | |
| 1546 | the same way but explodes instantly (ICBM); a power-up that explodes a bomb when an enemy steps | |
| 1547 | into its range (Proximity alert); and a power-up to slide in a straight line until a wall or a | |
| 1548 | crate, its highlight showing the next tile and then the far tile where the slide ends. The rules | |
| 1549 | below 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 | ||
| 1586 | User report: a slide looked jumpy, like half a dozen evenly spaced steps. Every move took a step's | |
| 1587 | 0.16 s whatever its length, so six tiles went by in about ten frames, each a big jump. A move of | |
| 1588 | more than 2 tiles that isn't a teleport (a slide) now takes 0.07 s per tile — 0.42 s for six — | |
| 1589 | eases out (fast at first, slowing as it stops), and is drawn crouched, with no hop and no walking | |
| 1590 | frames (`isSlide`, `moveProgress` in effects.ts; the "You" tag and the Shield's ring follow the | |
| 1591 | same curve). Steps and runs are as they were. Checked on a video of the demo's slide scene: the | |
| 1592 | player 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 | ||
| 1596 | User report: tapping a tile a slide reaches, with nothing picked, walked there by auto-move | |
| 1597 | instead of sliding. A tap's meaning (`tapMeaning`) now has a slide after a step and a diagonal | |
| 1598 | step and before auto-move's walk: the tile where a slide stops, with Slide held, is a slide. A | |
| 1599 | tile that is also a teleport target still slides only if picked with Move (there a teleport wins, | |
| 1600 | being safe); with nothing picked, a tile you can walk to was never a teleport anyway. The demo's | |
| 1601 | slide scene now shows the plain tap (its caption: "tap where you stop"). | |
d603166d 2026-10-25 Pavel Safronov | 1602 | |
| 1603 | ### 2026-10-24 — No Workshop | |
| 1604 | ||
| 1605 | User'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." | |
| 1607 | Every run now starts the same, and nothing a run gains outlives it. Nothing of it was built; the | |
| 1608 | plan'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 | |
| 1610 | Workshop progress; without it, Fresh's coin payout needs another look when §9 is built. Leftover | |
| 1611 | coins 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 | ||
| 1615 | User's call, on the flag raised with dropping the Workshop: no leftover coins carry over between | |
| 1616 | runs, 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 | |
| 1618 | power, or reset it for a coin payout) is dropped too, with its payout formula and its open | |
| 1619 | questions. What is built already matches: a death with lives left restarts the level from its | |
| 1620 | seed with everything the run had, and game over's Same dungeon starts the seed again from level | |
| 1621 | 1 with nothing carried over. The rest of the plan no longer mentions it (§0, §5, §5b, §9, the run | |
| 1622 | summary, the certificate, §14, the architecture and the milestones). | |
e78b7781 2026-10-25 Pavel Safronov | 1623 | |
| 1624 | ### 2026-10-24 — The shop | |
| 1625 | ||
| 1626 | User 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 | ||
| 1639 | How it works: `continue` from a cleared level that is due a shop opens it (`state.shop`); `buy` | |
| 1640 | and `reroll` are new actions in the run record, taking no turn; `continue` again starts the next | |
| 1641 | level. The draw is seeded by the seed, level and restock count. The dialog follows the gem's | |
| 1642 | offer (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 | |
| 1644 | in it. `SIM_VERSION` 25 (going on from level 3 now opens the shop). Tests: when it opens and | |
| 1645 | leaves, the draw (no held power-up, no Coin bag, seeded), buying (coins, at once, kept, once only), | |
| 1646 | too dear, the dearer restock, no board actions while open. Checked in a browser: reaching the shop | |
| 1647 | after 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 | ||
| 1651 | User report: the shop's three big buttons (Buy, Restock, Next level, stacked) and its clip left | |
| 1652 | little room for the items, so it needed scrolling all the time. Now the **selected item has its | |
| 1653 | own Buy button**, inside its card ("Buy for 6", or "Need 1 more" greyed when too dear), and the | |
| 1654 | dialog's buttons, **Restock and Next level, share one short row**; the shop's clip is smaller (at | |
| 1655 | most 200 px wide, from 300). The cards are no longer buttons (a button can't hold another), but | |
| 1656 | selectable boxes that the keyboard reaches too (Enter or Space). On a 390 × 844 phone the four | |
| 1657 | items now show with only the last one's bottom lines below the edge, where before only about two | |
| 1658 | did. | |
| 1659 | ||
| 1660 | ### 2026-10-24 — The ICBM flies | |
| 1661 | ||
| 1662 | User request: the ICBM was instant; make it a smoothly flying bomb. The rule stays (it goes off | |
| 1663 | the turn it is fired, so nothing can step out of the way); only the drawing changes. Since the | |
| 1664 | bomb has already exploded in the state the screen shows, the effects keep a **flight** (from the | |
| 1665 | player to where it lands: a bomb in a high arc, turning once, 0.3 s plus 0.06 s per tile) and a | |
| 1666 | time, `blastAt`, when the turn's blasts show. Until then the flame, the lingering flame, crates | |
| 1667 | bursting, enemies dying, drops and gems burned or uncovered, the exit opening and enemies let out | |
| 1668 | are held back — crates and enemies stay drawn as they were — and then play as usual. The danger | |
| 1669 | overlay already shows where the blast will land while the bomb flies. Checked on a video of a | |
| 1670 | launch 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 | ||
| 1674 | User's guidance: "some turns can take longer than others. If we have a charge or something rare or | |
| 1675 | involved, it's ok to take a bit more time and animate a more complex change of state." Recorded | |
| 1676 | in PLAN.md §13 (Animation timing) as a rule for what comes next: ordinary turns stay at a step's | |
| 1677 | pace; rare ones may play out in stages, the sim still settling the turn at once and the drawing | |
| 1678 | holding back what comes later, as the ICBM's flight does. Input stays unlocked, as decided on | |
| 1679 | 2026-10-22; the longer an animation, the more a quick tap can start the next turn over it — to | |
| 1680 | watch for, and lock only if it looks wrong. First use beyond the flight itself: an ICBM that | |
| 1681 | catches 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 | ||
| 1685 | User report: despite the name, the ICBM flew in a curve, as if thrown. It now flies straight along | |
| 1686 | its line, picking up speed (its distance grows with the square of the time) — 0.25 s plus 0.05 s | |
| 1687 | per tile — trailing seven puffs of grey smoke that grow and fade behind it; no arc, no spin. | |
| 1688 | Checked on a video of a launch: the bomb leaves the player slowly, speeds along the row with the | |
| 1689 | smoke behind it, and the blast starts as it arrives. | |
| 1690 | ||
| 1691 | ### 2026-10-24 — Diagonal widens Teleport into a square | |
| 1692 | ||
| 1693 | User question: with Teleport held, what should buying Diagonal do? Teleport (3 steps across plus | |
| 1694 | down, over anything) already reached every diagonal step, squeezes included, so Diagonal added | |
| 1695 | only a one-tap diagonal step and the Q E Z C keys. Of the two options offered — widen Teleport, or | |
| 1696 | stop offering Diagonal to a Teleport holder — the user chose to widen it, "so it doesn't waste a | |
| 1697 | power-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. | |
| 1699 | Run 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 Safronov | 1701 | |
| 1702 | ### 2026-10-24 — Coins and prices for a reasonable shop | |
| 1703 | ||
| 1704 | User request: make the shop reasonable, "perhaps leaning towards cheap", working out the coin | |
| 1705 | gains. Before, a level paid almost nothing but its par bonus: 2 Blobs at a 20% coin chance (1.7 | |
| 1706 | coins a drop on average) give under 1 coin a level, so three levels earned about 2 coins plus 5 for | |
| 1707 | each level cleared within par, and a playtest reached the first shop with 5 coins, short of the | |
| 1708 | cheapest 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 | ||
| 1718 | The first shop now sees about 11 coins (two cheap things) to about 26 within par (three or four). | |
| 1719 | The 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 | ||
| 1723 | User request: "add sounds and vibrations (where possible)". Built as decided on 2026-10-20: the New | |
| 1724 | Platformer Pack's 10 effects, plus two from the UI Pack (a current pack, so no new pack was | |
| 1725 | brought in): `tap-a` for every button, `switch-a` for the gem's choice and a restock. ZzFX, built | |
| 1726 | into 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 | ||
| 1745 | User request: with Developer mode on, its long description crowded the dialogs. In the game it now | |
| 1746 | reads only "A special gem lies next to you on every level." (the New run dialog, and the modes | |
| 1747 | tag's explanation). The details — every power-up not held and every charge, the level's own gem | |
| 1748 | still hidden, the red border on achievements — stay in the README. | |
| 1749 | ||
| 1750 | ### 2026-10-24 — Android Chrome at half a frame a second | |
| 1751 | ||
| 1752 | User report: on Android Chrome the game ran at about 0.5 fps, locally and on GitLab Pages, while | |
| 1753 | Firefox on the same phone ran smoothly. The likely cause: the CRT's `PostProcessPlugin` was on all | |
| 1754 | the time, a pass-through in play, and every frame it uploaded the whole WebGL canvas into a | |
| 1755 | texture (`texImage2D` from `glCanvas`). Chrome does that by reading the frame back from the GPU | |
| 1756 | and sending it up again, which stalls the GPU each frame — at a phone's 3× pixel ratio, ~10 MB a | |
| 1757 | frame. (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 Safronov | 1794 | |
| 1795 | ### 2026-10-24 — Sprinter, Ghost, Wisp and Hunter | |
| 1796 | ||
| 1797 | User request: "can you add more enemies next?" Built the four of PLAN.md §10 (the Reaper stays for | |
| 1798 | later), with the sprites §13 had chosen (mouse, block slime, fire slime, frog). The table's speeds | |
| 1799 | had to fit plans of one move per turn, which the arrows, the danger overlay and Proximity alert | |
| 1800 | all 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 Safronov | 1817 | |
| 1818 | ### 2026-10-24 — Five proposals written into the plan (PLAN.md §19) | |
| 1819 | ||
| 1820 | User request: think through five ideas, propose, and add them to the plan, to prioritise once the | |
| 1821 | plan is agreed. Written as §19, each with its open questions: a balance lab (bots and paired | |
| 1822 | experiments on the deterministic sim, since a full DFS/BFS of whole games is beyond reach: about | |
| 1823 | 10^50 leaves; search is used a few turns ahead instead), bigger scrolling boards, hazards and | |
| 1824 | objects (oil, barrels, bushes, metal crates, spikes, then acid, water, conveyors, ice, cracked | |
| 1825 | floor, springs), the Acid bomb that corrodes pillars and brings the roof down, and a Gauntlet mode. | |
| 1826 | Two conflicts with earlier choices are flagged there, not settled: bigger boards undo the one-size | |
| 1827 | board of 2026-10-23, and bushes that hide things would break "everything visible" (§4). The | |
| 1828 | Splat Pack, which the user named, would be a new art pack. | |
| 1829 | ||
| 1830 | ### 2026-10-24 — Answers on the §19 proposals | |
| 1831 | ||
| 1832 | The user's answers, recorded in PLAN.md §19: | |
| 1833 | ||
| 1834 | 1. **Balance lab:** the full version, with bots. | |
| 1835 | 2. **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. | |
| 1840 | 3. **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. | |
| 1843 | 4. **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. | |
| 1845 | 5. **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 Safronov | 1851 | |
| 1852 | ### 2026-10-24 — Proposal: one charge at a time (PLAN.md §19.7) | |
| 1853 | ||
| 1854 | User idea: carry one charge only; picking up another asks to sell the new one for coins or swap | |
| 1855 | it for the one held. Worked out as a proposal, with three questions for the user: whether the | |
| 1856 | swapped-out charge is sold or dropped, whether Freeze and Decoy become carried charges, and whether | |
| 1857 | a charge should be used deliberately (a second bomb button) — which would also fix the Nuke trap | |
| 1858 | the 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 | ||
| 1865 | Built as agreed (PLAN.md §19.1): `tools/balance/`, bundled with Vite into `.balance/lab.mjs` (the | |
| 1866 | sim is TypeScript; Node can't import it directly) and run on worker threads, one per core but one. | |
| 1867 | Choices 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 | ||
| 1880 | The first report's findings are in PLAN.md §19.1. The biggest: Teleport and Flame Pass turn a weak | |
| 1881 | player into a winner; Proximity alert costs a strong one 3 levels, by turning the defensive bomb | |
| 1882 | into 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 | ||
| 1886 | Built 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 | ||
| 1910 | User request, after the balance lab found Proximity alert cost the careful bot 3 levels: a bomb | |
| 1911 | goes off only when an enemy is on one of its 4 neighbouring tiles, not anywhere in its reach. A | |
| 1912 | bomb 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 Safronov | 1930 | |
| 1931 | ### 2026-10-25 — The demo's title on top; a compact menu | |
| 1932 | ||
| 1933 | Built from the queued UI items (user request). **The demo** reads top to bottom: the title | |
| 1934 | EXPLODORAMA! in its own bar, the TV (top bar included, moved down under the title), then the | |
| 1935 | caption; title and caption go when the game starts. **The menu**: Resume and New (was "New run") | |
| 1936 | side by side, then Almanac, Scores, Demo and Bug as one row of small word buttons (thinner edges so | |
| 1937 | ALMANAC fits a quarter row on a phone). Words rather than icons: they say what they do at a glance, | |
| 1938 | and 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 | ||
| 1942 | User question: "is there already a demo written for the charge swap?" There wasn't: the demo | |
| 1943 | skipped 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 | |
| 1945 | dialog 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, | |
| 1947 | which has no dialog, the choice just happens. Only this scene keeps turns that ask. No separate | |
| 1948 | charge-button scene: each charge's scene already shows the finger pressing it. | |
e6bb7c42 2026-10-25 Pavel Safronov | 1949 | |
| 1950 | ### 2026-10-25 — Bigger boards, a fixed view, enemies by concentration (built) | |
| 1951 | ||
| 1952 | PLAN.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 | ||
| 1973 | PLAN.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 | ||
| 1994 | From the lab's numbers (PLAN.md §19.1), the user decided: **Flame Pass becomes a charge counted in | |
| 1995 | flame hits** — it saves the player from 3 fire hits, then runs out (not a number of turns, as | |
ebcd0a8f 2026-11-02 Pavel Safronov | 1996 | proposed) — and **Teleport a charge of 3 jumps**. **Crate Pass and Bomb Pass stay power-ups** for |
e6bb7c42 2026-10-25 Pavel Safronov | 1997 | now, to be playtested more. Scheduled as the next task, after the bigger boards (being playtested |
| 1998 | by the user). Backlogged in PLAN.md §18. | |
| 1999 | ||
| 2000 | ### 2026-10-25 — Shield and Freeze: a confirming tap | |
| 2001 | ||
| 2002 | User request: the Shield went up the moment the charge button was pressed; add a confirmation. | |
| 2003 | Shield and Freeze are now aimed like the other actions, at one tile — the player's own: the charge | |
| 2004 | button picks them and lights that tile, and a tap on yourself uses them. Pressing the button again | |
| 2005 | also confirms (for these two only; for other actions a second press puts the action away), so the | |
| 2006 | keyboard needs only V, V. It is the same two steps as every other action, with nothing new to | |
| 2007 | learn. `SIM_VERSION` 33 (the actions now carry a tile). | |
| 2008 | ||
| 2009 | ### 2026-10-25 — Auto-move's way as a line of arrows | |
| 2010 | ||
| 2011 | User request: the blue tiles of auto-move's way weren't enough; arrows on them, forming a line. | |
| 2012 | Now a line runs from the player through the middle of each tile of the way, with an arrow on each | |
| 2013 | tile pointing the way that step goes (`drawPath` in render/board.ts). A darker blue than the tiles, | |
| 2014 | so it reads over them; the first step's line and arrow turn red when that step isn't safe, as its | |
| 2015 | tile 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 | ||
| 2025 | User report: scrolling the shop's items moved "You have N coins" out of view. The coins line now | |
| 2026 | sticks at the top of the shop's body, and the clip sticks just under it, so both stay while the | |
| 2027 | items scroll. | |
| 2028 | ||
| 2029 | ### 2026-10-25 — Dragging the board moves the view | |
| 2030 | ||
| 2031 | User 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 | |
| 2033 | that tile). Now, with nothing picked, a press that moves more than 12 CSS pixels is a drag: it | |
| 2034 | moves the view (kept inside the board) and letting go does nothing, nor does a long press arm a | |
| 2035 | bomb. The next turn brings the view back to the player, eased. With an action picked, sliding | |
| 2036 | still changes the tile chosen, as before (the commit preview). When the view is dragged off the | |
| 2037 | player, an edge arrow points back to them. Every player can drag the same way, so the fixed 9 × 13 | |
| 2038 | view stays fair. | |
| 2039 | ||
| 2040 | ### 2026-10-25 — Cards show what they change, in numbers | |
| 2041 | ||
| 2042 | User request: when a gem or the shop offers, say, Fire +1 to a player who already has one, show its | |
| 2043 | impact ("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 | |
| 2045 | when one is running, and a charge's uses when the same kind is carried. The current value, then | |
| 2046 | the new one: one step, since an offer or a shop never holds two of the same thing. Abilities and | |
| 2047 | passes have no number, so no line (`changeOf` in ui/info.ts). | |
| 2048 | ||
| 2049 | ### 2026-10-25 — TNT, and a limit on every stat | |
| 2050 | ||
| 2051 | User request: "make tnt a power-up that gives us 2 increases", and a maximum, "to limit | |
| 2052 | ridiculousnesses like board-spanning bombs". **TNT** raises the blast by 2 (its icon: the fireball | |
| 2053 | with Fire +1's badge; weight 6 against Fire +1's 20, price 7 against 4). **Bombs, blast and fuse | |
| 2054 | each stop at 5** (first guesses, to tune). A stat power-up that would be cut short isn't offered by | |
| 2055 | a gem or the shop, so nothing is half-wasted; cards and the top bar's explanations show the limit. | |
| 2056 | On cards the user asked whether to show the history ("1 → 2 → 3"): the current value and the new | |
| 2057 | one, 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 | ||
| 2061 | User report: on a phone the three tabs ran off both ends of the row, and scrolled entries showed | |
| 2062 | through them. The tabs now share the row equally at a smaller size (10 px Kenney Future), and the | |
| 2063 | sticky tab row sits above the entries scrolling under it. | |
| 2064 | ||
| 2065 | ### 2026-10-25 — Auto-move uses Diagonal | |
| 2066 | ||
| 2067 | User report: with Diagonal, auto-move walked round corners it could cut. The walk (`playerPath`) | |
| 2068 | now takes diagonal steps too when the player has Diagonal — squeezes included, as the action | |
| 2069 | allows — and a diagonal first step is taken as the Diagonal action. Among equally short ways the | |
| 2070 | search keeps its order (straight steps first), so the number of moves is what is saved. | |
fcf44f32 2026-10-25 Pavel Safronov | 2071 | |
| 2072 | ### 2026-10-25 — Flame Pass and Teleport become charges; a death drops the charge; boards fitted | |
| 2073 | ||
| 2074 | The user's order: stages 1–3 of the plan. | |
| 2075 | ||
| 2076 | 1. **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. | |
| 2084 | 2. **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. | |
| 2089 | 3. **A death drops the charge carried** where the player fell, with what was left; the retry has | |
ebcd0a8f 2026-11-02 Pavel Safronov | 2090 | it lying there (or on the nearest open floor, if the player fell inside a crate with Crate Pass). |
fcf44f32 2026-10-25 Pavel Safronov | 2091 | 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 Safronov | 2094 | |
| 2095 | ### 2026-10-25 — Measured: Diagonal without squeezing | |
| 2096 | ||
| 2097 | User 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 | |
| 2099 | of the two tiles beside it open, for the action and for auto-move's walk alike. On 300 seeds: the | |
| 2100 | average 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. | |
| 2102 | Left 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 Safronov | 2106 | |
| 2107 | ### 2026-10-25 — Board objects (first batch) and the Acid bomb; the worn look switched off | |
| 2108 | ||
| 2109 | The user's order: "implement 2, 1, 3" — the worn dungeon, the first hazards, the Acid bomb. | |
| 2110 | ||
| 2111 | 1. **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. | |
| 2114 | 2. **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. | |
| 2119 | 3. **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; | |
| 2124 | stalled 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 | ||
| 2128 | User request: "while they are in the charge slot, they are active. No activation needed. When the | |
| 2129 | appropriate damage hits the player, the charge is used up, and the charge slot is once again empty." | |
| 2130 | So the `shield` and `flamePass` actions, `shieldUp` and `flameHits` are gone: the charge held is | |
| 2131 | the protection. A Shield's use goes on any hit but a falling ceiling; a Flame Pass carries | |
| 2132 | FLAME_PASS_HITS (3) uses, one per fire hit (and per turn walked in flame), the slot emptying with | |
| 2133 | the last — the 3 hits the user chose earlier the same day, kept. A death can still drop one (only a | |
| 2134 | ceiling or being trapped kills through a Shield). The charge button is greyed out while one is | |
| 2135 | carried; 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 | ||
| 2139 | User request: under a falling ceiling, the Shield pushes the player to an empty spot; with none, | |
| 2140 | they die. Empty: open floor with no bomb or enemy, among the 8 tiles round them, the 4 straight ones | |
| 2141 | first (up, down, left, right), then the diagonals. The Shield is used up, and | |
| 2142 | absorbs 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 | ||
| 2147 | User report: with a long fuse, a bomb's tiles weren't marked until its last turns (the overlay showed | |
| 2148 | only this turn and the next). `predictDanger` now also returns `later`: playing on from the next turn, | |
| 2149 | the player waiting and enemies left where they are, every tile burned (chains, oil, bushes included) | |
| 2150 | or 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 | |
| 2152 | auto-move's safety check or the bots, which still look at `now` only. No rule change. | |
6c3fcc31 2026-10-26 Pavel Safronov | 2153 | |
| 2154 | ### 2026-10-25 — Board scenes, the almanac's Board tab, and the lab's stalls | |
| 2155 | ||
| 2156 | User request: work on the open items. | |
| 2157 | ||
| 2158 | 1. **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. | |
| 2163 | 2. **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. | |
| 2165 | 3. **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 | ||
| 2177 | User question: how would the original's placement — the bomb on the player's own tile — change | |
| 2178 | things? A lab-only rule switch (`RunOptions.lab.underfoot`, never set in play): Bomb (and a charged | |
| 2179 | bomb) goes on the player's tile; the player walks off it and can't walk back, enemies can't enter | |
| 2180 | it. 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 | ||
| 2190 | Read: slower (a bomb starts with you in its blast, and getting out takes turns), about twice the | |
| 2191 | turns 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 | |
| 2193 | out). Half the careful bot's extra stalls are its refusal to spend a life: walled in by rubble | |
| 2194 | next to a crate it waited for ever, where a person would bomb out. That case is a **soft-lock under | |
| 2195 | the current rule** too: no free tile to put a bomb on, and the crate means it doesn't count as | |
| 2196 | trapped. 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 Safronov | 2205 | |
| 2206 | ### 2026-10-26 — Bug reports to an address on fuzzyworld.net | |
| 2207 | ||
| 2208 | The project moved off GitLab (to code.fuzzyworld.net and apps.fuzzyworld.net), so GitLab Service | |
| 2209 | Desk is going away. `BUG_EMAIL` is now `bugs-k7q4+explodorama@fuzzyworld.net`: one forward-only | |
| 2210 | address shared by Pavel's apps, with `+explodorama` telling them apart. The random part makes the | |
| 2211 | address rotatable if it draws spam. (The old address was the *fuzzyworld* project's Service Desk, | |
| 2212 | not this game's.) The email itself is unchanged. | |
e3d8dc49 2026-10-27 Pavel Safronov | 2213 | |
| 2214 | ### 2026-10-26 — No tap-to-wait; deadly moves marked, and refused by the shortcuts | |
| 2215 | ||
| 2216 | User request: a tap on the player waited, which could be deadly; so could a tap in front of an | |
| 2217 | enemy or into flame. Asked: remove the tap-yourself shortcut, and "mark or disallow" moves that | |
| 2218 | guarantee 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 | ||
| 2236 | User report: an acid bomb placed diagonal to pillars left them untouched, since its flame was a | |
| 2237 | plain cross and only the first pillar an arm ran into cracked. Asked for "an area weapon, like the | |
| 2238 | nuke". Offered three readings (cracks in the Nuke's diamond with the usual cross of flame; the full | |
| 2239 | Nuke area; cracks in the 8 tiles round it); **the user chose the full Nuke area**: the acid bomb | |
| 2240 | burns every tile within `NUKE_RANGE` (2) steps, through anything, and every pillar in that diamond | |
| 2241 | is 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 | |
| 2243 | too strong. The Nuke itself still breaks a cracked pillar only along its cross. `SIM_VERSION` 40. | |
05e79f5f 2026-10-27 Pavel Safronov | 2244 | |
| 2245 | ### 2026-10-26 — Bushes drawn from Foliage Sprites | |
| 2246 | ||
| 2247 | User request (2026-10-26): new bush art. Kenney's **Foliage | |
| 2248 | Sprites** (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 | |
| 2250 | ink (`#353541`) and sizes to 128 px: four tall-grass tufts (`grass_a`–`grass_d`) and two round | |
| 2251 | canopies (`canopy_a`, `canopy_b`, composed but not used yet). A bush's picture is picked from its | |
| 2252 | cell index (`bushSprite`), so neighbours differ but a bush never changes picture between frames. | |
| 2253 | Credited in the menu and the README with the other packs. | |
5607bea2 2026-10-31 Pavel Safronov | 2254 | |
| 2255 | ### 2026-10-31 — Semver releases and an in-game list of changes | |
| 2256 | ||
| 2257 | User request (2026-10-31): the game gets semver versions, git-tagged releases, and a "what's new | |
| 2258 | since you last played" dialog with demo scenes and text. Decided with the user: **0.1.0 is the | |
| 2259 | base** (no history is rebuilt; the list starts there); the last version played is kept in | |
| 2260 | localStorage (`explodorama.lastVersion`); Claude keeps the version, `UNRELEASED` and the tags | |
| 2261 | consistent, **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 | |
| 2264 | visit only records the version and shows nothing; the dialog opens when the demo ends. Tapping the | |
| 2265 | version in the menu lists every release. A change reaching players goes into `UNRELEASED` as it | |
| 2266 | is finished; tagging moves them into a new release and bumps package.json (checked by | |
| 2267 | test/changelog.test.ts). | |
bb3823d2 2026-10-31 Pavel Safronov | 2268 | |
| 2269 | ### 2026-10-31 — Choices on a tap, crate approach, fireCharge, demo keys and shuffle (0.2.0 work) | |
| 2270 | ||
| 2271 | User requests, built together: (1) a tap on a crate walks to the tile beside it (`approachTile`; | |
| 2272 | with 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 | |
| 2274 | alone now walks too, it was refused), set it off. Walking onto an oil barrel stays a button-only | |
| 2275 | move. (3) The charge-swap dialog's third answer, `fireCharge`: use the charge held, then take the | |
| 2276 | new one (what remains of the held one is sold). Only a charge with nothing to aim can be used there, | |
| 2277 | which today is Freeze (aimed at the player's own tile); bomb charges, Decoy, Teleport and ICBM need a | |
| 2278 | target, so the third button is not shown for them (open: aim first, then swap?). No `SIM_VERSION` | |
| 2279 | bump: only a new action, which no old run holds. (4) Keys no longer end the demo, only a click or | |
| 2280 | tap; Tab/Enter/Space still work its buttons. A shuffle button (the dice, `explodorama.demoShuffle`) plays the | |
| 2281 | scenes in a random order (`scenePlan`), the choice kept across visits. | |
| 2282 | ||
| 2283 | ### 2026-10-31 — Released and next copies; early adopters | |
| 2284 | ||
| 2285 | User questions, 2026-10-31: how to try `main` while everyone else stays on the tagged release, and | |
| 2286 | an 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` | |
| 2288 | and the vite plugin reads `FW_APP`, uncommitted at the time of writing). The copies share an origin, | |
| 2289 | so each has its own localStorage keys (`src/channel.ts`); one key, `explodorama.earlyRuns`, is | |
| 2290 | shared. The incentive is a teaser and a link, not a gameplay reward: the released menu shows "Try it | |
| 2291 | early", the Changes dialog "Coming next" (from the next copy's `next.json`), scores finished on the | |
| 2292 | next copy are marked "Early adopter" and counted. Rejected: giving the Developer rainbow gem on the | |
| 2293 | next copy (scores would stop being comparable; Developer stays a separate option for either copy). | |
| 2294 | Scores are per copy, as storage is. | |
b8849d36 2026-11-01 Pavel Safronov | 2295 | |
| 2296 | ### 2026-10-31 — Scripted-or-found demo scenes: found, with a push; timeline bar | |
| 2297 | ||
| 2298 | User question: should demos be scripted rather than found by random play? Kept: scenes are found by | |
| 2299 | random play on a random room (varied, and always by the real rules), but a scene can now **start | |
| 2300 | from a set-up state** (`prepare`: rubble starts with a pillar already cracked), **ask for a longer | |
| 2301 | run** (`maxSteps`) and **play a tail** after its moment (`tail`: waiting turns, while safe). Scripting | |
| 2302 | a whole sequence (Acid, then a second bomb, then waiting) was not needed once the set-up did the | |
| 2303 | first 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 | |
| 2305 | scene's `moment`; chosen: normal speed, turn-by-turn fill, on both. | |
1f0038ac 2026-11-01 Pavel Safronov | 2306 | |
| 2307 | ### 2026-10-31 — Oil makes the player and the basic enemies slide | |
| 2308 | ||
| 2309 | User request (Pavel, 2026-10-31), details decided with him the same day: landing on oil, the player | |
| 2310 | and the basic enemies (everything but the flying Ghost and Wisp) slide on, over oil only, and stop on | |
| 2311 | the 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 | |
| 2314 | into them (a slide into an enemy would be an unforeseen death; walking in still is one); flame on | |
| 2315 | the way does not stop it, as with the Slide power-up, so it burns; (3) a diagonal step, Slide and | |
| 2316 | Teleport don't start a slide (a diagonal one would need squeeze rules); (4) the action's target stays | |
| 2317 | the neighbouring tile, so validation and replays are unchanged: the slide is a consequence worked | |
| 2318 | out in `step()` (player) and planning (enemies). Auto-move (`path.ts`) plans on tiles it lands on, | |
| 2319 | so it **keeps off any step that would slide** (`slips`), else it could bounce past its destination | |
| 2320 | forever or slide onto the open exit (reviewer, 2026-10-31): a tile in the middle of a spill is not | |
| 2321 | an auto-move destination, a single tap next to you still moves and slides. Walking onto an enemy on | |
8edb5cb9 2026-11-01 Pavel Safronov | 2322 | oil is a death although the slide carries on. Known, left: slides pass over raised spikes and the |
1f0038ac 2026-11-01 Pavel Safronov | 2323 | Decoy without effect; (5) an enemy's slide is |
| 2324 | announced like any move: its plan holds the tile where it stops, so the arrows and the danger | |
| 2325 | overlay 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 | ||
| 2329 | User request (Pavel, 2026-10-31). This reverses the 2026-10-21 decision that the credit lives in | |
| 2330 | the menu. Kenney's packs are CC0, so no credit is required; the README's Attribution section stays | |
| 2331 | as the credit (and still thanks Kenney and links his donation page). | |
8edb5cb9 2026-11-01 Pavel Safronov | 2332 | |
| 2333 | ### 2026-10-31 — Two gems on one turn offer one after the other | |
| 2334 | ||
| 2335 | User report: a slide (oil, or the Slide power-up) over two gems used the first gem's offer and | |
| 2336 | dropped the second unseen. Now the second waits in `waitingGems` and its offer is drawn when the | |
| 2337 | first is answered (and any charge choice from it), so it knows what the first gave (a held ability | |
| 2338 | is 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; | |
| 2339 | the retry has the gems back. `SIM_VERSION` 42. | |
| 2340 | ||
| 2341 | ### 2026-10-31 — Developer mode's test scenes | |
| 2342 | ||
| 2343 | User request: to check edge cases by eye (two gems on one turn, and the like), add test-only demos | |
| 2344 | and 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 | |
| 2347 | the almanac's **Tests** tab in a Developer run (each with "what to expect") and in the landing demo | |
| 2348 | with `?dev`. In the demo the gem and charge choices open as real dialogs (new gesture `pick`); in | |
| 2349 | an almanac clip they are answered unseen. `test/test-scenes.test.ts` asserts each scene's outcome, | |
| 2350 | so the same cases are checked without looking. To add a case: a name in `TestKind` and a `TEST_SCENES` entry; the | |
| 2351 | demo table and icons follow from the list. Not in the 0.2.0 change list (a developer aid). | |
b89399da 2026-11-01 Pavel Safronov | 2352 | |
| 2353 | ### 2026-11-01 — Which version is this: a box in the menu | |
| 2354 | ||
| 2355 | User request: make it obvious in the menu which version is playing and how to switch. The menu now | |
| 2356 | opens with a box: **Official version** / **Early adopter version**, a button to the other and both | |
| 2357 | addresses. 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 | |
| 2359 | old "Try it early" / "Back to the released version" links in the menu's bottom line are gone (the | |
| 2360 | Changes dialog's teaser keeps its link). The released copy shows the button only once next.json | |
| 2361 | answered. 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 | ||
| 2366 | User request: a charge picked up for the second time in a run is simply taken (no dialog, as the | |
| 2367 | player knows the idea), but the icons are small, so say what it was. New `ui/toast.ts`: a note with | |
| 2368 | the charge's picture and name above the action bar, fading after ~2 s, one at a time. Shown for any | |
| 2369 | charge pickup that opened no dialog (not the first of its kind, not a sell-or-swap); not in the | |
| 2370 | demo. The toast is the general mechanism for other "you got X" notes later. | |
| 2371 | ||
| 2372 | Follow-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 | |
| 2374 | it also shows over dialogs. **Rule: each new kind of UI piece like this gets a sample on the | |
| 2375 | almanac's Tests tab** ("Interface pieces", tap to show; `TOAST_SAMPLES` in `ui/almanac.ts`), so | |
| 2376 | Developer mode can show it without reproducing the situation. | |
| 2377 | ||
| 2378 | ### 2026-11-01 — Clips show the choices; almanac tabs wrap | |
| 2379 | ||
| 2380 | User report (screenshot): five almanac tabs ran over each other (the Tests tab made five); the tabs are | |
| 2381 | now a grid of as many equal columns as fit, none under 104 px. And a clip of "slide over two gems" | |
| 2382 | only 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 | |
| 2384 | waiting", or a charge's keep/take; held 2 s, then the pick plays. (The real dialog isn't opened in a | |
| 2385 | clip: it would cover the clip.) Only test scenes have such choices. | |
| 2386 | ||
| 2387 | Timing, same day: the choice comes after the turn that led to it has come to rest (a ~1.1 s pause in a | |
| 2388 | clip, 1.2 s in the `?dev` demo), shows ~2 s, goes, and the second follows after a pause (the clip's | |
| 2389 | own gap after the first goes, then 0.2 s), rather than at once over the slide. | |
d94649ba 2026-11-01 Pavel Safronov | 2390 | |
| 2391 | ### 2026-11-01 — A dragged view stays put through turns | |
| 2392 | ||
| 2393 | User report: the view snapped back to the player after every turn, a sudden jump, even when they | |
| 2394 | had put it there on purpose. Now a drag "holds" the view (`held` in camera.ts): a turn moves the | |
| 2395 | player but not the view (`keepView` shifts the offset by the move). It follows the player again | |
| 2396 | when a turn would take the player out of the view (checked after the turn, not while dragging, so a | |
| 2397 | far 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 Safronov | 2398 | |
| 2399 | ### 2026-11-01 — Controller support: pure core first (Q29, stage 1) | |
| 2400 | ||
| 2401 | Controller 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 | |
| 2402 | calls the same functions (tapTile, direction, action buttons, inspect), so every rule stays shared. | |
| 2403 | Stage 1 is two pure modules with tests, nothing wired into the game yet: `ui/padMap.ts` (a gamepad | |
| 2404 | snapshot to intents) and `ui/spatial.ts` (which element to focus next in a direction). | |
| 2405 | Map (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, | |
| 2406 | the 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 | |
| 2408 | view; right stick looks around. A held direction repeats after 300 ms, then every ~110 ms; the left | |
| 2409 | stick turns on at 0.5 and off at 0.35; the right stick has a 0.2 deadzone. The first frame of a | |
| 2410 | fresh pad only records what is held, so the press that wakes the pad in Chrome does nothing. | |
| 2411 | Dialogs: Cross/Triangle/Square pick buttons directly in small dialogs (Q42, to be settled in | |
| 2412 | stage 4). Desktop wrapper for Steam: Electron (Q41). Full plan lives in the queue (Q29). | |
| 2413 | ||
| 2414 | Stage 2 (board play, wired in; `ui/gamepad.ts`): the pad calls the same functions as keys and taps | |
| 2415 | and never fakes pointer events. First attempt, a free tile cursor (the d-pad moved it, A tapped | |
| 2416 | it), was replaced the same day by Pavel's design, which reuses the action bar: the face buttons are | |
| 2417 | Cross Move/Go, Circle Wait, Square Bomb, Triangle Charge (the intent also carries the raw face | |
| 2418 | index, since dialogs will map them differently); LB cancels, RB and View ask "What is that?", Start | |
| 2419 | is the menu, R3 is `resetPan` (the view follows the player again, not the full `resetView`). RB or View | |
| 2420 | turn "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); | |
| 2422 | the d-pad jumps between them, the label says "Find out what this is", RB or View again explains | |
| 2423 | the tile (the call a tap makes in inspect mode), LB cancels. | |
| 2424 | Pressing 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 | |
| 2427 | d-pad and left stick then jump between those tiles (`nextFocus`, board coordinates, nothing wraps); | |
| 2428 | the right stick looks around. The same face button again does it (`tapTile`, so choices, the | |
| 2429 | deadly check and Freeze work as for a tap); another one switches action; LB (`cancel()`, shared | |
| 2430 | with Esc) puts it away. Wait, and Go (Move with a flag set), have no tile: they aim at the | |
| 2431 | player's own tile and the same button does them. With nothing aimed, the d-pad and left stick walk | |
| 2432 | as the arrow keys do (a diagonal only with the Diagonal power). A one-line label over the action | |
| 2433 | bar, only while the pad is in use, says what confirming does (`ui/targetLabel.ts`, covering | |
| 2434 | Move/Run/Diagonal/Slide/Kick, Bomb/Throw/RPG/Detonate, each charge, Wait and Go, and "costs a | |
| 2435 | life!"). The view moves for the target only near its edge (`revealTile`) and then stays put; the | |
| 2436 | aim ends when a turn is played, a level or run starts, or a mouse, touch or key acts. LittleJS's | |
| 2437 | own gamepad reading is disabled (`setGamepadsEnable(false)`). The pad is ignored while the window | |
| 2438 | is unfocused, a standard pad is preferred, and a non-standard one only gets one "not recognised" | |
| 2439 | toast. In the demo the d-pad left/right switch scenes, at most every 400 ms, and Cross, Circle, | |
| 2440 | Square, Triangle, LB or Start end it. | |
| 2441 | ||
| 2442 | Action-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, | |
| 2444 | and 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, | |
| 2446 | Triangle and Circle; Xbox and any other standard pad get A, X, Y and B. They revert with the | |
| 2447 | existing removal of `body.pad` on a pointer or key. | |
| 2448 | ||
| 2449 | Stage 3 (dialogs and the banner; `ui/padDialogMap.ts` pure and tested, `ui/padDialog.ts` DOM glue, | |
| 2450 | Pavel approved the layout): a dialog's footer buttons map to face buttons: the primary one to | |
| 2451 | Cross, the others left to right to Triangle then Square, and the button that dismisses (by label: | |
| 2452 | Close, Cancel, Back, Got it, Keep playing, Resume; only if the dialog is not `required`) to Circle. | |
| 2453 | Circle 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, | |
| 2455 | almanac, 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 | |
| 2457 | clicks the focused one. Cards (`.offer-choice`: gem offer, shop) are not "controls": moving onto one | |
| 2458 | clicks it (selects it, plays its clip) and Cross presses the footer's primary (gem offer) or the | |
| 2459 | selected card's Buy (shop, where Cross is thus taken and Restock and Next level get Triangle and | |
| 2460 | Square; a plain Cross there would have left the shop). The body is watched with a | |
| 2461 | MutationObserver, so any rebuild (shop purchase, almanac tab, Back) re-applies glyphs and the | |
| 2462 | selector (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. | |
| 2465 | In selector mode Circle presses the footer's dismissing button when there is one (so Back runs its | |
| 2466 | action), else closes. The shop's Buy always uses the card marked `aria-pressed` at press time. Moving | |
| 2467 | the selector scrolls the body clear of its sticky tabs, clip or coin line. LB/RB click the previous/next `[data-tab]`; the | |
| 2468 | right stick scrolls `#dialog-body` (900 px/s at full push). A text field is never selectable (a | |
| 2469 | pad 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 | |
| 2471 | the 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 | ||
| 2474 | After 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 | |
| 2476 | d-pad with no aim does not walk any more: it aims the neighbour tile (`aim.button = 'step'`), and | |
| 2477 | the label and the later confirm both come from `directionPlan` / `diagonalPlan`, the very function | |
| 2478 | the arrow keys run, so every keyboard rule (kick, run, Bomb Pass choice, the deadly refusal) holds; | |
| 2479 | a step that can do nothing shows "Can't go there" and the crosshair without the blue tile. Cross | |
| 2480 | confirms any aim (`confirmAim`); Circle backs out of any aim (`padBack`: the pick it made goes, the | |
| 2481 | flag stays) and, with none, aims Wait (so Wait needs Cross: one slip no longer ends a turn). | |
| 2482 | LB / RB are `prev` / `next` (LB no longer cancels, RB no longer inspects; View alone does): on the | |
| 2483 | board they cycle through one list (`padCycle.ts`): for Move, Bomb, Charge, Wait in that order, each | |
| 2484 | button's `pickTargets` tiles (nearest first), or a single item for a button that goes at once (Wait, | |
| 2485 | Go); it is built when the first press finds no cycle aim, kept in the aim, and thrown away with the | |
| 2486 | aim at the end of a turn. The label gets "CURR/ALL · ". Cross picks the item's button as a face | |
| 2487 | button 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 | |
| 2488 | while cycling. In dialogs LB / RB switch tabs; View does nothing there. The demo is ended by LB no | |
| 2489 | more (only face buttons, Start). | |
| 2490 | ||
| 2491 | The cycle aim looks as if its action were picked without picking it: `drawBoard` takes `pick` (the | |
| 2492 | action 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 | |
| 2494 | badges and blast, a decoy, the moved player). The d-pad step aim uses `ghost` alone. No separate | |
| 2495 | sprite table was needed, since the preview plays the action on a copy of the state. Not previewed: | |
| 2496 | Wait (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 | |
| 2499 | testing them).** The pad now has a cursor, a tile among the actionable ones (`ui/padTiles.ts`, | |
| 2500 | pure and tested): the player's own tile (Wait) and every tile `pickTargets` gives for Move, Bomb or | |
| 2501 | Charge. All of them are lit blue (`View.lit`) and the cursor is the dashed square. The d-pad moves | |
| 2502 | it with `nextFocus` on board coordinates; the first press with none showing puts it on the nearest | |
| 2503 | actionable tile that way, or on the player's own tile. It is dropped when a turn ends, a level or | |
| 2504 | run starts, a dialog opens or a mouse/key takes over, and on any other change it stays if its tile | |
| 2505 | is still actionable, else goes to the nearest one that is, else goes (`refreshCursor`). The face | |
| 2506 | buttons 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. | |
| 2508 | There 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 | |
| 2510 | to there (`{id, target}`), and of waiting for Wait: the same test as the board's red tiles and the | |
| 2511 | Wait button's red. A deadly action is played, since the button was marked; only a tap or key with | |
| 2512 | nothing picked refuses. Greyed ones flash when pressed. With no cursor, a face button only shows the | |
| 2513 | cursor on the player's own tile. The label lists the possible buttons with their names; the ghost | |
| 2514 | is 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 | ||
| 2519 | Glyph layout and families: the action bar's picture now sits beside the icon on one row (36px icon, | |
| 2520 | 32px picture, both able to shrink on a narrow bar; the key hint is hidden under `body.pad`), and | |
| 2521 | the cursor's label draws the button picture before each line instead of its name. A Nintendo | |
| 2522 | family (`padFamily`: ids with nintendo, switch, pro controller or vendor 057e) uses the Switch | |
| 2523 | set (plain: the pack has no coloured variant); glyphs are chosen by physical position, so the | |
| 2524 | bottom button (index 0, Move) shows B, left (Bomb) Y, top (Charge) X and right (Wait) A. The cursor | |
| 2525 | tile is drawn red when every action possible there would cost a life (`cursorDeadly`); if only | |
| 2526 | some would, it stays blue and the red-marked buttons say which. | |
| 2527 | ||
aef53511 2026-11-02 Pavel Safronov | 2528 | Cursor always on (Pavel, 2026-11-01; supersedes "the first d-pad press shows the cursor" and "a face |
| 2529 | button with no cursor shows it" above): while a pad is in use and the board is being played (no | |
| 2530 | dialog, 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`) | |
| 2532 | whenever it was cleared: a turn ending, a new level or run, a dialog closing, "What is that?" | |
| 2533 | ending, an action done. The d-pad moves it from there with `moveCursor` (no "nearest tile that | |
| 2534 | way" special case; `firstCursor` is gone). A face button always acts on the cursor's tile or | |
| 2535 | flashes if it cannot. L1 clears "What is that?" first, then a leftover pick and the flag, and | |
| 2536 | returns the cursor to the player's tile. A key or mouse still removes the cursor and the pad | |
| 2537 | styling (`padOff`); the next pad input brings it back on the player's tile, and that waking | |
| 2538 | face press only shows it (so a stray Circle does not wait a turn). | |
| 2539 | ||
ebcd0a8f 2026-11-02 Pavel Safronov | 2540 | ### 2026-11-01 — A screen shows the whole board when it fits (supersedes the fixed 9 × 13 view) |
| 2541 | ||
| 2542 | Decided 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 | |
| 2545 | whole board, and a screen too small for the board scrolls as before (phones keep the 9 × 13 view or | |
| 2546 | whatever fits at a playable tile size). So early boards (9 × 11) always show fully, and a large | |
| 2547 | screen shows a late 13 × 17 board whole. Trade-off accepted: a big screen sees more of the board | |
| 2548 | than a phone; scores from different screens are therefore not strictly like for like. Rules and | |
| 2549 | SIM_VERSION are unchanged (the view is render and UI only). Still to do when the camera part is | |
| 2550 | built: update PLAN.md §19.2 and the README, and decide whether the score board notes the screen | |
| 2551 | class. Not built yet. | |
41566e2d 2026-11-02 Pavel Safronov | 2552 | |
| 2553 | ### 2026-11-01 — The level finishes itself once the last enemy is gone | |
| 2554 | ||
92cc4ee3 2026-11-02 Pavel Safronov | 2555 | **Superseded 2026-11-02 by "Autopilot replaces the hoover (Q73)"; the `hoover` action no longer exists.** |
| 2556 | ||
41566e2d 2026-11-02 Pavel Safronov | 2557 | User request: on a controller, tapping each coin, gem and charge and then the exit was a pain. |
| 2558 | Decided with the user: (1) the gathering is a new sim action `hoover` taking no turn (like `buy`), | |
| 2559 | so runs record and replay it and old records are untouched (no SIM_VERSION bump); (2) the walk to | |
| 2560 | the exit costs turns as ever, par and turn count included; (3) gems queue their offers one after | |
| 2561 | another 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 | |
| 2563 | is the existing boxed-in life loss, and when the only way is unsafe it waits for the player. | |
| 2564 | Triggered once per level by the UI (`autoFinish` in main.ts), ended by the player's own move or | |
| 2565 | cancelling the flag. | |
| 2566 | ||
| 2567 | ||
| 2568 | ### 2026-11-01 — Fullscreen from the first tap, click or key | |
| 2569 | ||
| 2570 | User request: start fullscreen. Browsers only allow `requestFullscreen()` after a user gesture, so | |
| 2571 | the game cannot do it on load: the first pointer or key event does it, once (leaving it with Esc or | |
| 2572 | F11 is respected). A controller press is not a gesture, so a pad-only player stays windowed until | |
| 2573 | touching the screen or a key. A setting, "Start in fullscreen" (on by default), turns it off. No | |
| 2574 | in-game toggle key (F is the RPG bomb; F11 is the browser's own). The Electron build (Q30) will | |
| 2575 | simply start fullscreen. iPhone Safari offers no Fullscreen API for pages, so nothing happens there. | |
92cc4ee3 2026-11-02 Pavel Safronov | 2576 | |
| 2577 | ### 2026-11-02 — Fullscreen only with a controller; a button; a menu switch (Q60) | |
| 2578 | ||
| 2579 | User request, amending 2026-11-01. Decided: (1) the first-gesture auto-fullscreen now happens only | |
| 2580 | while a controller is connected (`shouldAutoFullscreen`, `ui/fullscreenRule.ts`, pure): the pad | |
| 2581 | player has no pointer to press a button with, a phone or mouse player does, and being pushed into | |
| 2582 | fullscreen annoyed them; "connected" is a pad in `navigator.getGamepads()` or one the pad input has | |
| 2583 | seen (Chrome lists a pad only after its first press); the "Start in fullscreen" setting stays and | |
| 2584 | is worded for it; (2) a visible fullscreen toggle in the top bar, only where `document.fullscreenEnabled`, | |
| 2585 | following `fullscreenchange` (so F11 and Esc update it); on a phone's portrait bar three stacked | |
| 2586 | round buttons would grow the bar, so all three shrink to 31 px then (the bar's height is | |
| 2587 | unchanged); icons are Game Icons `larger` / `smaller`; (3) for a controller, a "Fullscreen" switch | |
| 2588 | in the menu's Settings, no pad button (every one is taken); a pad press is no browser gesture, so | |
| 2589 | in 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 | ||
| 2594 | User request after testing. Decided: (1) the game never goes fullscreen by itself. This supersedes | |
| 2595 | 2026-11-01 "Fullscreen from the first tap, click or key" and the Q60 rule that made it happen only | |
| 2596 | with a controller (`shouldAutoFullscreen` and `ui/fullscreenRule.ts` are gone, with the "Start in | |
| 2597 | fullscreen" setting and its stored value, which is simply no longer read). What stays from Q60: the | |
| 2598 | top bar's fullscreen button, the menu's "Fullscreen" switch (with the toast "Press F11, or use the | |
| 2599 | fullscreen 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 | |
| 2601 | cursor went back to the player's tile, where only Wait was offered, though the route was still drawn. | |
| 2602 | Now 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, | |
| 2605 | new 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 | ||
| 2609 | User 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 | |
| 2611 | the scene restarts for it (a switch between the two scene lists is the existing between-scenes | |
| 2612 | switch, 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 | |
| 2614 | mouse pick on a real pad press. Nothing else clears it: no mouse move, click, tap or button press, so | |
| 2615 | the button keeps cycling and the pick stays wherever the user stopped (a mouse player must be able to | |
| 2616 | watch the controller demos, and a pad player the touch ones); only holding the confirm button, which | |
| 2617 | ends the demo, leaves it; (3) mouse is the touch scenes with a pointer in the finger's place and "Click anywhere | |
| 2618 | to play"; the pointer is `cursor_none` from Kenney's Cursor Pack (assets/kenney-cursor; user supplied the | |
| 2619 | pack, 2026-11-02, replacing a stopgap sword); the touch | |
| 2620 | captions still say "finger"; (4) the Q59 hint "Hold {move} to play" and the pad family drawn in | |
| 2621 | captions and on the virtual controller follow the effective input, not the real pad; (5) clips | |
| 2622 | keep 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 | ||
| 2626 | User request: with a pad in use, the demo and the dialogs' clips show the controller, not the finger. | |
| 2627 | Decided: (1) the virtual controller (`ui/padOverlay.ts`) is CSS shapes with Kenney Input Prompts | |
| 2628 | glyphs 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 | |
| 2630 | a scene is never half finger and half pad; (3) switching to the pad starts its list at scene 1, and | |
| 2631 | switching 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 | |
| 2633 | gameplay scenes without the touch-only ones (long-press bomb, auto-move, the top bar, "What is | |
| 2634 | that?" 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, | |
| 2636 | a pick, or a tile where no single face button does the action; (6) the stick scene only lights the | |
| 2637 | stick, because the demo shows a room whole and there is nothing to scroll; (7) the Start scene | |
| 2638 | opens the real menu over the scene, showing the run's own state, not the scene's; (8) the turn | |
| 2639 | plan (`ui/padDemo.ts`) is pure and works from the concrete action, so every plain move, bomb, charge | |
| 2640 | and 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 | ||
| 2644 | User request: introduce holding a button to confirm it, with a ring filling clockwise from 12 | |
| 2645 | o'clock; the action happens on release and only if the ring filled; on the demo screen the player | |
| 2646 | holds the bottom face button to play. Decided: (1) pad only: touch, mouse and keyboard already have | |
| 2647 | deliberate gestures, and the pad's face buttons are easy to hit by accident; (2) the pad's four | |
| 2648 | face buttons only (board actions, the face-button choices in dialogs and on the banner, and the | |
| 2649 | demo's start); the d-pad, shoulders, View, R3 and Start stay instant, since they move, cancel or | |
| 2650 | look 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 | |
| 2652 | release, not on completion, so the ring filling is a warning that can still be backed out of by | |
| 2653 | letting go early, which does nothing; (5) one hold at a time, a second face button is ignored | |
| 2654 | until the first is released; a swapped or unplugged pad and a window that loses focus cancel; (6) | |
| 2655 | in the demo the bottom button (Cross/A, Move) is the way in and other face buttons do nothing, so a | |
| 2656 | stray press cannot throw the demo away; (7) the waking press (see 2026-11-01) still only shows the | |
| 2657 | cursor, with no ring; (8) the ring is a CSS conic gradient, no art, and one floating element is | |
| 2658 | moved over the glyph (`ui/holdRing.ts`, inside the open `<dialog>` because it is in the top | |
| 2659 | layer); it does not animate, so reduced motion changes nothing. The demo scenes' own scripted | |
| 2660 | presses 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 | ||
| 2664 | User request: on a Nintendo Switch pad, A is the "go ahead" button and B is "back", but the face | |
| 2665 | buttons were mapped by position only (bottom Move, left Bomb, top Charge, right Wait), so on a | |
| 2666 | Switch pad the bottom button, labelled B, confirmed, against a decade of habit. Decided: (1) only | |
| 2667 | the Nintendo family differs; Xbox, PlayStation and generic keep the position mapping, because their | |
| 2668 | bottom 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 | |
| 2670 | demo'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, | |
| 2672 | index 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 | |
| 2674 | family and says what each face button means (the intent still carries the raw index); `padGlyph.ts` | |
| 2675 | names a face button's picture by its position (`faceGlyph`) and an action's picture as the face | |
| 2676 | button 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 | |
| 2678 | banner already open is assigned again when the family becomes known; (5) the demo's virtual | |
| 2679 | controller 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 | |
| 2681 | button on the virtual controller stays lit for `PAD_LIT_SECONDS` = 0.7 s (was 0.3 to 0.35), and the | |
| 2682 | demo's and the clips' beats were respaced so presses never overlap (a d-pad press is lit, lets go, | |
| 2683 | then 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 | ||
| 2687 | Pavel disliked the CSS-drawn pad outline (rounded boxes and two tilted grips), and its hand-placed | |
| 2688 | percentages 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 | |
| 2690 | hand): a 1000-wide box, the body as an SVG path, and the centre and radius of the d-pad, right and left | |
| 2691 | stick, four face buttons and Start, and the shoulder boxes; (2) `ui/padPlaces.ts` (pure, tested) turns a | |
| 2692 | family into each lit part's place and size, with the face position taken from `faceOf` (so Nintendo's Move | |
| 2693 | on the right and Wait at the bottom need no special case; the `.nintendo` CSS is gone); (3) `padOverlay.ts` | |
| 2694 | draws the body, the left stick (never lit) and the shoulders in an inline SVG and lays Kenney's pictures | |
| 2695 | over it in percent of the box, so one size variable (`--pad-size`) still scales everything; `setFamily` | |
| 2696 | swaps 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 | |
| 2698 | Switch Pro ones are from Pavel's own photos; (5) the SNES / N64 Switch Online pads are not covered. | |
| 2699 | The outline is hand-drawn by Claude from measurements, so it stays on the hand-made list (art | |
| 2700 | provenance #16, Q39). | |
| 2701 | ||
| 2702 | ### 2026-11-02 — The virtual controller uses real line-art pictures (Q63, supersedes the outline and photo parts above) | |
| 2703 | ||
| 2704 | Pavel rejected the hand-drawn SVG outline. Decided: (1) the body is a picture, one per family, from | |
| 2705 | OpenGameArt's "Controller style 1" (Xbox, also generic), "5" (PlayStation) and "3" (Nintendo), CC BY 4.0 | |
| 2706 | by 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, | |
| 2709 | so `padOverlay.ts` shows only the `<img>` (lines made visible by a CSS drop-shadow) and Kenney's pictures | |
| 2710 | over the drawn buttons; the layout lost `outline` and `lstick`; (3) the positions are measured by Claude | |
| 2711 | from the drawings (the script's table), so mliu92's CC BY-SA 3.0 Xbox photo and Pavel's PS5 and Switch Pro | |
| 2712 | photos are no longer used and the CC BY-SA licence note is gone; (4) the hand-made outline is gone from | |
| 2713 | the 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 | ||
| 2717 | After 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 | ||
| 2721 | User decision: the deployed game always links to the repository (the menu's "Source code" link, `REPO_URL` | |
| 2722 | in `main.ts`, in the live and the next copy), and the repository's README Attribution section lists every | |
| 2723 | credit. 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 | |
| 2725 | names the author with a link to their OpenGameArt profile and each work's page, names the licence with | |
| 2726 | its link, and says what was changed (scaled and trimmed); `assets/controller-art/LICENSE.txt` repeats | |
| 2727 | it beside the files. Conditions: the repository stays publicly readable, and the link stays in every | |
| 2728 | build. 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 | ||
| 2732 | An audit found the keyboard could not play everything: tile targets only along the player's four straight | |
| 2733 | lines (nearest valid tile: Teleport, Decoy, Detonate only partly usable), `?` then an arrow moved the player, | |
| 2734 | Enter on the banner pressed its first button whatever had the focus, Space waited with a button focused, no | |
| 2735 | pan, 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 | |
| 2739 | the 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 | |
| 2741 | home, hide); (3) `padActive` as the cursor's switch became `inputMode` ('pad' | 'keys' | null, `ui/keyCursor.ts`): | |
| 2742 | the cursor key presses, modifiers, Alt-pan and Home keep the keyboard's mode; a plain arrow or any key or mouse | |
| 2743 | press 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 | |
| 2745 | dialog's "Walk there"; the aim's keyboard mode ends with "What is that?"; (5) Alt + arrows `panView` (0.5 tile a press, | |
| 2746 | repeating), 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 | |
| 2748 | Enter with its green (or only) button; (7) the label and hint draw key caps (`kbd.key-cap`) instead of pad | |
| 2749 | pictures. 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 | ||
| 2753 | The demo's input button gains **keyboard** as its third entry (touch, mouse, keyboard, ps5, xbox, switch, | |
| 2754 | generic; the pick rules of Q61 unchanged). The keyboard demo is the controller demo's shape, not the touch one: | |
| 2755 | the touch scenes minus the touch-only ones (long-press bomb, auto-move, top bar tour, "What is that?" by tap) and | |
| 2756 | the charge swap, with its own saved place (`demoAtKeys`), a **key overlay** (`ui/keyOverlay.ts`: Kenney's | |
| 2757 | Keyboard & 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 | |
| 2760 | for a step, an action key then an arrow ("B, then a direction"; also M and V), the same key twice to confirm Freeze | |
| 2761 | on yourself, else Shift + arrows over `cursorPath` then the action key. The first Shift + arrow only wakes the cursor on | |
| 2762 | the player (as in play), so it counts as a press and repeats the first hop's direction. Every candidate is | |
| 2763 | replayed through the game's own pure rules (`runKeys`: keyButton, moveCursor, tileActions, resolvePick, tapChoices, | |
| 2764 | isDeadly) and only one that ends on the action's tile with the same action id is used; the test does the same | |
| 2765 | on every demo scene and seed. `runKeys` mirrors main.ts `directionPlan` (without its questions) rather than | |
| 2766 | sharing it; a change there needs one here. (2) Each turn is assumed to start with the cursor asleep. In play, a Shift + arrow right | |
| 2767 | after another cursor turn hops at once; the demo always wakes it, which only costs it a press. (3) Only M B V Space, | |
| 2768 | the 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 | |
| 2769 | plus M B V reaches the same tiles; W A S D are named in the first caption, not drawn. (4) New scenes: the Shift | |
| 2770 | cursor, `?` then an arrow then Enter, Alt + arrows (the 2x zoomed look view of the stick scene) then Home, Esc opens | |
| 2771 | the menu. Captions are `SceneDef.keyCaption` with `{arrows}`, `{M}`, `{shift}` ... tokens (`keyCaptionHtml`, throws on | |
| 2772 | an unknown one). (5) The hint reads "Press Enter or Esc to play"; the demo's key handler already ended it on those | |
| 2773 | and 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. | |
| 2775 | Correction (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`). | |
| 2776 | Known 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 | ||
| 2780 | User request (Pavel tried it): waiting for the button to be let go felt late. Decided: (1) the action | |
| 2781 | happens 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 | |
| 2783 | before the ring is full is still the way to back out, and the full ring is now the point of no | |
| 2784 | return; (2) the button is then **used up** (`HoldState.done` in `ui/padHold.ts`): it cannot fire again | |
| 2785 | and, since a new hold cannot start while one is held or used up, it starts none in whatever the | |
| 2786 | action opened (a dialog, the demo ending into the game, a level change, the banner) until it has | |
| 2787 | been let go; a second face button is ignored meanwhile, as before; (3) the ring (`hold` callbacks) | |
| 2788 | reaches 1 at completion and stays full (green) until release, or goes at once if the place the | |
| 2789 | buttons act in changes (`holdEnd`; the button stays used up); unplug, blur, a swapped pad and a pad | |
| 2790 | error end the used-up state too; an instant button (Start, a bumper) does not; (4) holds that have not | |
| 2791 | completed keep every Q59 cancel rule (early release, context change, instant button, blur, unplug, | |
| 2792 | swap, error); (5) the demo's hold-to-play starts play at completion and the same held button then | |
| 2793 | does nothing in the game; the waking press rules (2026-11-01) are unchanged. No rules change (no | |
| 2794 | SIM_VERSION bump). Not verifiable without a real pad: how it feels, and that a held button across | |
| 2795 | the demo-to-game change shows no ring. | |
| 2796 | ||
| 2797 | ### 2026-11-02 — The hold ring is anchored to its originator (Q72) | |
| 2798 | ||
| 2799 | Bug (Pavel, with a pad): the ring stayed on screen a moment after the dialog that began it closed. | |
| 2800 | Cause: the ring was positioned only inside `hold()` calls from the pad poll, and the dialog stays | |
| 2801 | open for a 150 ms pop-out (`closing`) after a button closes it; the ring, a child of the dialog, | |
| 2802 | stayed at the old spot meanwhile, and `holdEnd` came only when the poll saw the context change. | |
| 2803 | Decided: the ring belongs to the picture it was shown on (`holdRing.ts` keeps it as `origin`), | |
| 2804 | follows 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, | |
| 2806 | tested). A completed hold's button still counts as used up until release (Q68); only the ring goes. | |
| 2807 | The demo's ring lives inside the demo controller, so it goes with it (and `releaseAll`). Not | |
| 2808 | verifiable 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 | ||
| 2812 | User request: on the demo's virtual controller a pressed A/B/X/Y was hard to tell from an idle one | |
| 2813 | when both were coloured (the earlier brightening of the idle colours made it worse). Decided: the | |
| 2814 | face glyphs are grey at rest (`grayscale(1)`, brightened so they read on the dark demo) and take | |
| 2815 | their colour only while pressed (`.pad-part.face.on`: full colour, a little saturation and | |
| 2816 | brightness, the scale pulse); the filter transition makes the change gradual. Amends the same | |
| 2817 | day's "brighter idle colours" point. The white pictures (d-pad, stick, shoulders, Start) are | |
| 2818 | unchanged. 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 | ||
| 2822 | Pavel's idea: next to the bright blue (single actions) show a second, faded blue on every tile | |
| 2823 | auto-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 | |
| 2825 | first move (`autoMoveAction`), minus the bright tiles and the player's own, with the walk's length | |
| 2826 | for 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 | |
| 2829 | possible, and it taps the tile with nothing picked, so every tap rule and safety check applies; | |
| 2830 | the ghost shows nothing there. (4) Faded tiles are drawn only with the pad's or keyboard's cursor | |
| 2831 | and 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 | |
| 2833 | and dialogs). Keyboard Shift+arrow repeat is the OS's. Cost: a scan of the floor, 40 ms on a | |
| 2834 | 13 x 17 level, once per state (`currentFar`). Demo scenes for it come later. | |
| 2835 | ||
| 2836 | Added the same day (Pavel approved): **the way is previewed before it is chosen.** With the cursor | |
| 2837 | on a faded tile, or the mouse (pointer type mouse, no button down; never touch) over a far walkable | |
| 2838 | tile, with nothing picked, no way set, no dialog and no demo, the board draws auto-move's way as the | |
| 2839 | real 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 | |
| 2842 | as before. The result is cached per tile, state and danger (`previewNow`). Click and tap behaviour | |
| 2843 | is 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 | ||
| 2847 | Supersedes "The level finishes itself once the last enemy is gone" (2026-11-01, Q56). User | |
| 2848 | request: the turnless hoover made gathering free, which is not the game's economy, and it was | |
| 2849 | not a walk the player could watch. Decided with the user: (1) the `hoover` action is removed | |
| 2850 | entirely (`types.ts`, `turn.ts`: the branch, `canHoover`, `hoover`); (2) in its place, once the last | |
| 2851 | enemy is gone, an autopilot walks the character with ordinary turns (move, run, diagonal, wait), so | |
| 2852 | replays 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 | |
| 2854 | TURNS, not nearest-first: pairwise walking distances from `playerPath`, then the cheapest order | |
| 2855 | through every reachable coin, charge and uncovered gem ending at the exit (exhaustive over subsets | |
| 2856 | for up to 10 targets, nearest-first beyond), a target lying on the chosen leg counted as collected | |
| 2857 | free, ties broken by fewest direction changes, then smaller y, then smaller x (deterministic, so | |
| 2858 | replays agree); (4) it is re-planned every turn from the current state, using auto-move's safety | |
| 2859 | rules (`safeStep`); unreachable targets are skipped; a gem under a crumbling crate, or no way to the | |
| 2860 | exit while crates crumble, means waiting; (5) the autopilot's turns count, par bonus included (no | |
| 2861 | par protection; accepted). (6) SIM_VERSION 42 to 43, because a record holding `hoover` can no | |
| 2862 | longer replay. Autopilot's wording, speed, note and stop controls are decided in the UI part. | |
| 2863 | ||
| 2864 | UI part (same day, decided with the user): (7) the note is `#autopilot`, top centre below the top | |
| 2865 | bar, a pill like the toast but not a popover (dialogs cover it), text `role="status"`: "Autopilot: | |
| 2866 | cleaning up", "Autopilot: waiting for the crates", "Autopilot: off to the exit". It shows at once | |
| 2867 | on 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 Safronov | 2868 | (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 Safronov | 2869 | stays at 0.5), no new animation; a Run only when owned. (9) Stop: Esc, pad L1 (its picture is on |
| 2870 | the note) and a Stop button on the note for touch and mouse; stopping ends it for the level; so | |
| 2871 | does turning the setting off. A stall (6 waits in a row outside the crates phase, or no safe turn) | |
| 2872 | stops it with the toast "Autopilot stopped / Your turn". (10) While it walks the board takes no | |
| 2873 | input (`boardLocked`): taps, long-press, the action bar (dimmed), movement and action keys, `?`, | |
| 2874 | Shift + arrows, the pad cursor and face buttons; it still takes dialogs (a gem or charge pauses | |
| 2875 | the walk until answered), the menu, fullscreen, the top bar's info taps and looking around. A hold | |
| 2876 | begun before the lock is cancelled through the pad's context string. (11) The setting keeps its | |
| 2877 | stored key `autoFinish` so players' choices survive the rename. | |
| 2878 | ||
| 2879 | ## A controller's selector shows what to hold (Q74, 2026-11-02) | |
| 2880 | ||
| 2881 | Bug report: with a controller the menu's switches (Sound, Autopilot) could not be changed. They | |
| 2882 | could: a hold of Cross toggles them. But a short press did nothing and nothing showed that it | |
| 2883 | should be held, because in selector mode only the footer's dismissing button had a picture, so | |
| 2884 | the hold ring (which goes round a `.pad-glyph[data-face]`) had nothing to go round. Decided: the | |
| 2885 | selected item carries the confirm button's picture (added on selection, removed when the selection | |
| 2886 | moves; skipped where the item has a picture already, the dismissing button's Circle and a shop | |
| 2887 | item's Buy), so the ring shows on it; a confirm button let go early shows the toast "Hold to | |
| 2888 | choose" once per dialog; the almanac's neighbouring tabs show LB / RB. The 0.5 s rule is | |
| 2889 | unchanged. Same day, the autopilot (decision 4 above) also waits for a ticking bomb or burning | |
| 2890 | flame 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 | ||
| 2894 | Bug report (controller): the player stood two tiles from an empty floor tile in a straight line, | |
| 2895 | holding an RPG. The cursor on it said "Fire an RPG bomb to this tile", Bomb was possible and Move | |
| 2896 | greyed, so the player could bomb a tile but not walk there. Cause (confirmed): the tile is bright | |
| 2897 | blue (an RPG target), `farTiles` leaves bright tiles out of the faded set, and `tileActions` offers | |
| 2898 | Move 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 | |
| 2900 | auto-move can walk, Move is offered". New pure `walkTo` / `cursorActions` in `ui/padTiles.ts`: on a | |
| 2901 | bright tile where Move is not possible but a safe, non-deadly walk exists, the Move entry becomes | |
| 2902 | the walk (`far: true`, "Walk here · N steps", "Keep walking" for the destination), and the other | |
| 2903 | buttons stay as `tileActions` has them. Pressing it takes `padDo`'s existing `far` path (nothing | |
| 2904 | picked, tap the tile), so it walks. Tiles Move can target keep their plain Move; faded tiles are | |
| 2905 | unchanged. The faded tint still leaves bright tiles out (they are blue already). No SIM_VERSION | |
| 2906 | change. Test: `test/q75.test.ts`. | |
| 2907 | ||
| 2908 | ## The cursor still reaches every faded tile while a walk is set (Q75, 2026-11-02) | |
| 2909 | ||
| 2910 | Bug report (controller): a coin an enemy dropped, in a pocket beside a bomb target, could not be | |
| 2911 | reached by the cursor, and no faded tile showed anywhere; the screenshot had L1 "Cancel" on, which | |
| 2912 | means 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 | |
| 2914 | not deadly, and the d-pad hops from the RPG tile down to it). The cause is Q69/Q70's rule that, | |
| 2915 | with a destination set, `farTiles` lists only the destination: any other walk was out of reach of | |
| 2916 | the 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 | |
| 2918 | a walk is set; the cursor starts on the destination as before (`cursorStart`), and pressing Move | |
| 2919 | on another faded tile sets a new destination (`tapTile`). `farTiles`' `destination` parameter stays | |
| 2920 | for its tests. With an action picked, faded tiles are still hidden, as the picked button's targets | |
| 2921 | are 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 | ||
| 2925 | User request: the cursor mode switched back to the mouse whenever the mouse moved (more than 3 px), | |
| 2926 | which 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 | |
| 2928 | Q65 keyboard-cursor rule 'a mouse move of 3 px or more hands the cursor back, like the pad'. The mouse's | |
| 2929 | hover route preview only shows while no pad or keyboard cursor is active, so it does not appear while | |
| 2930 | the mouse merely moves over a pad-driven board; a click on the board is the way back. | |
720cc9d3 2026-11-03 Pavel Safronov | 2931 | |
| 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 Safronov | 2955 | - **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). |