fuzzyworld

explodorama / commit

RPG, Proximity alert, Slide and the ICBM charge; Run extends Teleport

7f242d360a900de7ee15b7f9e924439edffee2e7 · Pavel Safronov · 2026-10-24 18:46:43 -0700 · parents ac534e578611

Claude Code usage: this commit ~144.3K out / ~97.9M cached · cost unknown (no per-model trailer) · session 4 tally 1.7K in / 826.7K out / 357.0M cached · usage by commit and session

RPG, Proximity alert, Slide and the ICBM charge; Run extends Teleport

- RPG (power-up): Bomb fires a bomb in a straight line, landing just
  before the first wall, crate, bomb or enemy.
- ICBM (charge, 3 launches): fired the same way, it goes off where it
  lands, outside the bomb limit; a far Bomb tile is an ICBM while
  launches are left.
- Proximity alert (power-up): bombs go off the turn an enemy is in
  reach; the danger overlay predicts it from the enemies' arrows.
- Slide (power-up): slide until a wall, crate or bomb, picking up what
  you pass; enemies crash, flame burns, the open exit stops you. A tap
  on the end tile slides; the slide glides, crouched, easing out.
- With Run, Teleport reaches 4 steps. Keys F, I, L; a picked action's
  direction key takes the nearest usable tile at any distance.
- Icons, texts, almanac, offer, top bar, four demo scenes and clips.
  SIM_VERSION 24.
- The gem and gem-offer screenshots reach the gem again (seed 107).

Usage: 1.7K in / 826.7K out / 357.0M cached (session tally)
rtk:   1.9M saved, 38.8% over 4234 cmds (global/lifetime)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
 CHANGELOG.md                                   |   1 +
 PLAN.md                                        |   8 +-
 README.md                                      |  26 ++--
 assets/composed/charge_icbm.png                | Bin 0 -> 6725 bytes
 assets/composed/powerup_proximity.png          | Bin 0 -> 6151 bytes
 assets/composed/powerup_rpg.png                | Bin 0 -> 3146 bytes
 assets/composed/powerup_slide.png              | Bin 0 -> 2254 bytes
 assets/kenney-board-icons/arrow_horizontal.png | Bin 0 -> 616 bytes
 assets/kenney-icons/White/forward.png          | Bin 0 -> 15117 bytes
 assets/kenney-icons/White/target.png           | Bin 0 -> 15427 bytes
 assets/kenney-icons/White/warning.png          | Bin 0 -> 15315 bytes
 decisions.md                                   |  70 ++++++++++
 docs/screenshots/almanac-charges.png           | Bin 282941 -> 314118 bytes
 docs/screenshots/almanac.png                   | Bin 410063 -> 523978 bytes
 docs/screenshots/auto-move.png                 | Bin 217157 -> 217119 bytes
 docs/screenshots/bomb-picked.png               | Bin 216471 -> 216434 bytes
 docs/screenshots/charges.png                   | Bin 228162 -> 228670 bytes
 docs/screenshots/countdown.png                 | Bin 221116 -> 221112 bytes
 docs/screenshots/death.png                     | Bin 235128 -> 235106 bytes
 docs/screenshots/demo.png                      | Bin 973013 -> 963970 bytes
 docs/screenshots/dev-offer.png                 | Bin 200791 -> 203343 bytes
 docs/screenshots/enemies.png                   | Bin 220425 -> 220100 bytes
 docs/screenshots/gem-offer.png                 | Bin 214178 -> 202477 bytes
 docs/screenshots/gem.png                       | Bin 214188 -> 200500 bytes
 docs/screenshots/info-hud.png                  | Bin 175377 -> 179052 bytes
 docs/screenshots/lingering.png                 | Bin 219627 -> 219582 bytes
 docs/screenshots/long-press.png                | Bin 219891 -> 219831 bytes
 docs/screenshots/menu.png                      | Bin 142122 -> 141390 bytes
 docs/screenshots/move-picked.png               | Bin 216615 -> 216563 bytes
 docs/screenshots/nuke.png                      | Bin 240183 -> 241416 bytes
 src/main.ts                                    |  11 +-
 src/render/board.ts                            |  11 +-
 src/render/effects.ts                          |  32 ++++-
 src/render/sprites.ts                          |  16 ++-
 src/sim/actions.ts                             | 117 ++++++++++++++--
 src/sim/danger.ts                              |   7 +-
 src/sim/demo.ts                                |  39 +++++-
 src/sim/enemies.ts                             |   2 +-
 src/sim/explosions.ts                          |  14 ++
 src/sim/picks.ts                               |  14 +-
 src/sim/record.ts                              |   2 +-
 src/sim/state.ts                               |   9 +-
 src/sim/turn.ts                                |  21 ++-
 src/sim/types.ts                               |  22 +++-
 src/ui/almanac.ts                              |   2 +-
 src/ui/clip.ts                                 |   2 +-
 src/ui/controls.ts                             |   7 +-
 src/ui/info.ts                                 |  28 +++-
 test/animation.test.ts                         |  21 ++-
 test/demo.test.ts                              |   2 +-
 test/enemies.test.ts                           |   5 +-
 test/new-powers.test.ts                        | 176 +++++++++++++++++++++++++
 test/powerups.test.ts                          |   6 +-
 tools/compose-icons.sh                         |   8 ++
 tools/screenshots.mjs                          |  11 +-
 55 files changed, 611 insertions(+), 79 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd23d57..281e921 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ saved before the change is dropped and the next visit starts a new run.
 
 ### Added
 
+- **New power-ups: RPG** (fire a bomb as far as it flies), **Proximity alert** (your bombs go off when an enemy is in reach) and **Slide** (slide until a wall, crate or bomb), and a new charge, **ICBM** (3 launches of a bomb that goes off where it lands). With Run, Teleport reaches 4 steps. Version 24: runs saved before don't resume.
 - **The "Ouch!" banner replays your own last turns** before a death. The gem's offer plays a clip of the power-up you look at, and almanac entries open their clip.
 - **Clips in the info dialogs:** most of them play a short scene of what they describe, new every loop, with a finger showing how for the actions and the power-ups you work yourself.
 - **Report a bug** in the menu: an email to the project's GitLab Service Desk (no account needed), filled in with the game's version, the device and the run, so it can be replayed.
diff --git a/PLAN.md b/PLAN.md
index 3950f96..3fcaf81 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -162,8 +162,11 @@ they first appear on).
 | **Run** | Run pickup (called Skates until 2026-10-22; NES "Speed"), or shop | 2 tiles in a straight line. **Both** tiles must be free — no jumping over anything. Highlight: path tile + landing tile. An enemy on the path tile = death (you ran into it). *Part of Move since 2026-10-22.* |
 | **Kick** | Kick power-up (*was a charge until 2026-10-22*) | Push an adjacent bomb by walking into it; it slides until the next tile is not free, keeping its timer. Part of Move. |
 | **Throw** | Throw power-up (*was a charge*) | A bomb **exactly 2 tiles away** in a straight line, over a crate, bomb or enemy; *since 2026-10-23 (user's choice)* not over a pillar or the outer wall (it went over anything). Part of Bomb. |
-| **Teleport** | Teleport power-up, rare (*was a charge*) | Move to any free tile within Manhattan distance 3, over anything. Part of Move. *Since 2026-10-23 (user request)* auto-move uses it: each move is a step, a Run or a Teleport, whichever leaves the walk left shortest (ties: Teleport, then Run), a neighbour always a step (`autoMoveAction` in `sim/path.ts`). |
+| **Teleport** | Teleport power-up, rare (*was a charge*) | Move to any free tile within Manhattan distance 3 (*4 with Run too, since 2026-10-24, user request: Teleport reaches every tile a run does, so Run extends it*), over anything. Part of Move. *Since 2026-10-23 (user request)* auto-move uses it: each move is a step, a Run or a Teleport, whichever leaves the walk left shortest (ties: Teleport, then Run), a neighbour always a step (`autoMoveAction` in `sim/path.ts`). |
 | **Diagonal** | Diagonal power-up (*a charge until 2026-10-23*, user request; weight 8) | Step one tile diagonally; both orthogonal neighbours may be blocked — a squeeze. Part of Move (G, then Q E Z C). |
+| **Slide** | Slide power-up (*2026-10-24, user request*; weight 6) | Slide in a straight line until the next tile is a wall, crate or bomb (user's choice); can't stop early. Picks up what it passes; an enemy on the way is crashed into (death, as Run's); flame on the way burns unless flame-safe; the open exit stops it. Highlight: the end tile, 2+ away. Part of Move, after Teleport (a teleport to the same tile wins). L. A tap on the end tile with nothing picked slides too (`tapMeaning`, before auto-move's walk; user report, same day). |
+| **RPG** | RPG power-up (*2026-10-24, user request*; weight 6) | Fire a bomb in a straight line: it flies until the next tile is a wall, crate, bomb or enemy (or flame) and lands just before it (user's choice), 2+ tiles away (1 is a plain bomb); counts down as usual, within the bomb limit. Part of Bomb. F. |
+| **Proximity alert** | Proximity power-up (*2026-10-24, user request*; weight 5) | Passive: the player's bombs placed while held go off the turn an enemy is in their reach, after the enemies move — one that walked in or one already there (user's choice). The danger overlay predicts it from the enemies' next moves. |
 | **Detonate** | Detonator pickup (NES "Detonator"), or shop | Explode one chosen own bomb now. *Since 2026-10-22:* tap the bomb (all of them pulse while you have it); no button. |
 
 *Since 2026-10-23 (user request) every ability and pass is kept through a death.* (Until then Detonate and the passes were lost, the NES rule.)
@@ -186,6 +189,7 @@ dialog saying what it does (`chargeInfo`). Icons are the user's choices (§13).
 | **Shield** | Armed on pickup; absorbs the next hit (and anything else that turn), then is gone. Kept to the next level. Icon: the Move figure in an empty ring. |
 | **Freeze** | Every enemy freezes for `FREEZE_TURNS` = 3 turns, the pickup turn included (was 4, as an action). Icon: a padlock. |
 | **Decoy** | A decoy on the pickup tile (user's choice over "next to you"); chasing enemies target it for 3 turns. Flame destroys it. Icon: three pawns. |
+| **ICBM** | *2026-10-24, user request.* 3 launches (`boosts.icbm`, user's choice): Bomb fires a bomb as an RPG's, at any distance, and it goes off that same turn (timer 0); the bomb limit doesn't apply; it works without the RPG power-up. While launches are left, a far tile picked with Bomb is an ICBM (before RPG in the group). Lasts to the next level. I. Icon: bomb + target badge. |
 | ~~**Line**~~ | *Removed 2026-10-23* (not in the user's new list). |
 | ~~**Fuse +1 / Fuse −1**~~ | *Removed 2026-10-22 (user request).* |
 
@@ -718,7 +722,7 @@ crate, an enemy, a tile an enemy is going to, or another bomb. *Since 2026-10-24
 or in one room in six already uncovered (locked while enemies live). *Since 2026-10-24 (user
 request)* the menu's **Watch the demo** plays it again; a tap returns to the run as it was. *Since 2026-10-24 (user request)* the scenes play **in one fixed
 order**, looped (`DEMO_ORDER` in main.ts: the controls' seven, then the table's living scenes with
-a way to die after every four); the panel numbers them (n / 43), ◀ ▶ and ← → step through them,
+a way to die after every four); the panel numbers them (n / 47 since the new power-ups; 43 at first), ◀ ▶ and ← → step through them,
 ↺ goes back to the first, and the place is kept in `localStorage` so the demo carries on there
 next time, after a reload too (not on a test URL, which starts at the first). Scenes are made in
 a Web Worker (`ui/scenes.ts`), the next while one plays. Ideas: a gem's choice, and showing the
diff --git a/README.md b/README.md
index 97942a4..0fdc256 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ seven charges that work when picked up, a hidden gem on every level that offers
 mid-run work. The shop, the Keep / Fresh choice on death, curses and more enemies are planned —
 see [PLAN.md](PLAN.md).
 
-Version: **23**, shown at the bottom of the menu with the commit the game was built from (a link
+Version: **24**, shown at the bottom of the menu with the commit the game was built from (a link
 to it on GitLab, plus "plus local changes" in a build with uncommitted changes) and a link to the
 [source code](https://gitlab.com/PavelSafronov/explodorama). The version is the rules version
 (`SIM_VERSION` in [src/sim/record.ts](src/sim/record.ts)): it goes up by one whenever a rule
@@ -45,15 +45,16 @@ screen — and the finger in front of the glass. The rest show every feature on
 board: bombs and their countdown, watching the enemies' arrows to bomb where they're going, a
 narrow escape, chain reactions, lingering flame, the three enemy types, drops, the gem, every
 power-up you can see in action (Bomb +1, Fire +1, Fuse +1, Run, Kick — a tap on a bomb next to
-you —, Throw, Teleport, Diagonal, the Detonator — a tap on your bomb —, Wall / Bomb / Flame Pass,
+you —, Throw, Teleport, Diagonal, Slide, RPG, the Detonator — a tap on your bomb —, Proximity
+alert, Wall / Bomb / Flame Pass,
 Star) and every charge (Pierce, Power, Nuke, Shield,
-Freeze, Decoy), and the exit (walking out once it opens; blasting it lets enemies out). Heart and
+Freeze, Decoy, ICBM), and the exit (walking out once it opens; blasting it lets enemies out). Heart and
 Coin bag only change numbers, so they have no scene; nor does the shop. Every other scene shows
 **a way to die**: walking into a blast, not getting far enough, blocking yourself in with your
 own bomb, one bomb setting off another, touching an enemy, and a couple of funny ones (the
 Detonator next to you, a Run into an enemy). The scenes play **in a fixed order** and loop —
 the controls first, then the rest, with a way to die after every four — each made new every
-time. The panel shows which one it is (**12 / 43**); **◀ ▶** (or the **← →** keys) go to the
+time. The panel shows which one it is (**12 / 47**); **◀ ▶** (or the **← →** keys) go to the
 one before or after, and **↺** back to the first. The demo carries on where it was the next time
 it plays, after a reload too. It is
 view-only; **tap anywhere else (or press any other key) to play**, and that tap does nothing
@@ -81,13 +82,14 @@ they make Move and Bomb do more.
 
 | Action | Phone | Keyboard | What it does |
 |---|---|---|---|
-| **Move** | Tap a tile next to you, or tap **Move** and then any highlighted tile | **M**, then a direction — or W A S D / arrow keys straight away (Q E Z C diagonally, with Diagonal) | Step one tile up, down, left or right onto an empty tile. **With the Run power-up:** or 2 tiles in a straight line. **With Teleport:** or any empty tile up to 3 steps away, over anything. **With Kick:** walking into a bomb kicks it. **With Diagonal:** or a diagonal step. |
-| **Bomb** | Tap **Bomb**, then a highlighted tile — or **press and hold** a tile next to you | **B**, then a direction | Place a bomb on an empty tile next to you. You can have 1 bomb on the board at a time. **With Throw:** or 2 tiles away in a straight line, over a crate, bomb or enemy (not a pillar). Pierce, Power and Nuke charges change your next bombs by themselves (see Charges). |
+| **Move** | Tap a tile next to you, or tap **Move** and then any highlighted tile | **M**, then a direction — or W A S D / arrow keys straight away (Q E Z C diagonally, with Diagonal) | Step one tile up, down, left or right onto an empty tile. **With the Run power-up:** or 2 tiles in a straight line. **With Teleport:** or any empty tile up to 3 steps away (4 with Run too), over anything. **With Kick:** walking into a bomb kicks it. **With Diagonal:** or a diagonal step. **With Slide:** or slide in a straight line until a wall, crate or bomb stops you. |
+| **Bomb** | Tap **Bomb**, then a highlighted tile — or **press and hold** a tile next to you | **B**, then a direction | Place a bomb on an empty tile next to you. You can have 1 bomb on the board at a time. **With Throw:** or 2 tiles away in a straight line, over a crate, bomb or enemy (not a pillar). **With RPG:** or fired in a straight line, as far as it flies. **With ICBM launches:** a far tile fires an ICBM, which goes off at once. Pierce, Power and Nuke charges change your next bombs by themselves (see Charges). |
 | **Wait** | Tap **Wait**, or tap **yourself** | Space | Do nothing; the world takes its turn. |
 
 When Move or Bomb can do several things, the tile you tap decides: next to you is a step (or a
-kick, into a bomb), 2 straight is a run, further is a teleport; next to you is a bomb, 2 away a
-throw. With Diagonal, a diagonal neighbour is a step too — tap it, picked or not — and Teleport is
+kick, into a bomb), 2 straight is a run, further is a teleport (or, out of its reach, where a slide
+stops). Without picking anything, a tap on the tile where a slide stops slides there; next to you is a bomb, 2 away a throw, further an ICBM while you have launches, otherwise an
+RPG bomb. With Diagonal, a diagonal neighbour is a step too — tap it, picked or not — and Teleport is
 for tiles further away.
 
 **Auto-move:** tap any tile further away that you can walk to — empty floor, the exit, the
@@ -159,6 +161,7 @@ through a death. The first time in a run you pick up each kind, a dialog says wh
 | **Shield** | the Move figure in a ring | A ring appears round you: the next hit (flame or enemy) is absorbed, and so is anything else that hits you that turn. Then it's gone. |
 | **Freeze** | a padlock | Every enemy freezes in place for **3 turns**, the pickup turn included: they turn blue inside a block of ice and thaw a step each turn, then carry on with the moves their (faded, icy) arrows show. |
 | **Decoy** | three pawns | A pale copy of you appears where you picked it up. Enemies that chase you (Seekers) chase it instead for 3 turns. Flame destroys it; it doesn't block anyone. |
+| **ICBM** | bomb with a target badge | 3 launches: **Bomb** fires a bomb as an RPG's (or **I**, then a direction) and it goes off the moment it lands, at any distance. Your bomb limit doesn't count. While you have launches, a far tile picked with Bomb is an ICBM. |
 
 A bomb that Pierce, Power or Nuke changed wears their icons as small badges, and one bomb can
 have all three: each bomb you place (or throw) uses one of each that you hold.
@@ -269,9 +272,12 @@ before moving on.
 | **Run** | the Move figure with a fast-forward badge | **Move** can run: 2 tiles in a straight line in one turn (or **R**, then a direction). Both tiles must be empty; an enemy on the tile in between is run into. Anything on that tile is picked up. |
 | **Kick** | bomb with a right-arrow badge | Walk into a bomb next to you and it slides away until the next tile is blocked (a wall, crate, bomb or enemy), keeping its number. |
 | **Throw** | bomb with an up-right-arrow badge | **Bomb** can also go exactly 2 tiles away in a straight line, over a crate, bomb or enemy in between — but not over a pillar or the outer wall (or **T**, then a direction). |
-| **Teleport** (rare) | the Move figure with an outbox badge | **Move** can take you to any empty tile within 3 steps (counting across and down), over anything (or **P**, then tap a tile). |
+| **Teleport** (rare) | the Move figure with an outbox badge | **Move** can take you to any empty tile within 3 steps (counting across and down), over anything (or **P**, then tap a tile). **With Run as well, 4 steps:** Teleport reaches everywhere a run does, so Run adds to it instead. |
 | **Diagonal** | the Move figure with a diagonal-arrows badge | **Move** can also step diagonally, even squeezing between two blocked tiles (or **Q E Z C**: up-left, up-right, down-left, down-right). A charge until 2026-10-23. |
+| **Slide** | the Move figure with a two-way-arrow badge | **Move** can also slide in a straight line until the next tile is a wall, crate or bomb — **tap the tile where it stops** (or **L**, then a direction) — picking up whatever it passes. It can't stop early: an enemy on the way is crashed into (a life lost), and flame on the way burns (unless Flame Pass or a Star). The open exit stops it. |
+| **RPG** | bomb with a forward-arrow badge | **Bomb** can also fire a bomb in a straight line (or **F**, then a direction): it flies until the next tile is a wall, crate, bomb or enemy and lands just before it, then counts down as usual. |
 | **Detonator** | bomb with a ! badge | Your bombs pulse with a red glow: **tap one** and it goes off this turn (or **X**, then tap it). No button. |
+| **Proximity alert** | bomb with a warning-sign badge | Your bombs go off the turn an enemy is in their reach — one that walks in, or one already there when you place it — wherever you are standing. The danger overlay shows it ahead, from the enemies' arrows. |
 | **Wall Pass** | crate with an ✕ badge | Walk through crates. |
 | **Bomb Pass** | bomb with a shield badge | Walk over bombs. |
 | **Flame Pass** | flame with a shield badge | Flame doesn't hurt you (enemies still do), and you can walk through it. |
@@ -279,7 +285,7 @@ before moving on.
 | **Heart** | heart | One more life. |
 | **Coin bag** | coin | 10 coins. |
 
-Run, Kick, Throw, Teleport and Diagonal (they were one-use charges before) add no buttons — they
+Run, Kick, Throw, Teleport, Diagonal, Slide and RPG add no buttons — they
 make Move and Bomb do more (**?** then the button says how). The Detonator adds none either:
 your bombs pulse, and a tap on one sets it off. The top bar shows a small icon for each ability
 and pass you hold.
diff --git a/assets/composed/charge_icbm.png b/assets/composed/charge_icbm.png
new file mode 100644
index 0000000..bf2a996
Binary files /dev/null and b/assets/composed/charge_icbm.png differ
diff --git a/assets/composed/powerup_proximity.png b/assets/composed/powerup_proximity.png
new file mode 100644
index 0000000..fd7ea53
Binary files /dev/null and b/assets/composed/powerup_proximity.png differ
diff --git a/assets/composed/powerup_rpg.png b/assets/composed/powerup_rpg.png
new file mode 100644
index 0000000..a33165a
Binary files /dev/null and b/assets/composed/powerup_rpg.png differ
diff --git a/assets/composed/powerup_slide.png b/assets/composed/powerup_slide.png
new file mode 100644
index 0000000..4b55b6d
Binary files /dev/null and b/assets/composed/powerup_slide.png differ
diff --git a/assets/kenney-board-icons/arrow_horizontal.png b/assets/kenney-board-icons/arrow_horizontal.png
new file mode 100644
index 0000000..b2279e5
Binary files /dev/null and b/assets/kenney-board-icons/arrow_horizontal.png differ
diff --git a/assets/kenney-icons/White/forward.png b/assets/kenney-icons/White/forward.png
new file mode 100644
index 0000000..edf9f6f
Binary files /dev/null and b/assets/kenney-icons/White/forward.png differ
diff --git a/assets/kenney-icons/White/target.png b/assets/kenney-icons/White/target.png
new file mode 100644
index 0000000..a7961da
Binary files /dev/null and b/assets/kenney-icons/White/target.png differ
diff --git a/assets/kenney-icons/White/warning.png b/assets/kenney-icons/White/warning.png
new file mode 100644
index 0000000..12ebcb1
Binary files /dev/null and b/assets/kenney-icons/White/warning.png differ
diff --git a/decisions.md b/decisions.md
index 20843d5..d880bdb 100644
--- a/decisions.md
+++ b/decisions.md
@@ -1528,3 +1528,73 @@ hidden clip canvas didn't hide (`canvas.clip` sets `display: block`, which beats
 `canvas.clip[hidden]` is `display: none` now — the "Ouch!" banner's canvas, hidden when there is no
 replay, relied on it too. Checked in a browser: Heart and Coin bag show their pictures, Kick its
 clip again.
+
+### 2026-10-24 — The gem screenshots show the gem again
+
+User report: `gem-offer.png` showed the board, not the offer. Its seed-55 key presses no longer
+reached the gem (the levels have changed since they were written), and `gem.png`, the step
+before, showed no gem either. A short search with the sim — random moves, bombs and waits on
+seeds 1–120, keeping the shortest that ends with the gem taken and the player alive — found seed
+107: down, a bomb below, wait, up, wait three times, down twice (the gem uncovered beside the
+player: `gem.png`), right onto it (`gem-offer.png`, the offer open).
+
+### 2026-10-24 — Run extends Teleport; RPG, Proximity alert, Slide and the ICBM charge
+
+User requests: with Teleport, Run should extend it (Teleport already reaches every tile a run
+does); a power-up to "send the bomb as far as it can fly" (RPG); a charge that flings the bomb
+the same way but explodes instantly (ICBM); a power-up that explodes a bomb when an enemy steps
+into its range (Proximity alert); and a power-up to slide in a straight line until a wall or a
+crate, its highlight showing the next tile and then the far tile where the slide ends. The rules
+below are the user's choices of the options offered (all the recommended ones).
+
+- **Run + Teleport:** Teleport reaches 4 steps instead of 3 while Run is held too
+  (`teleportRange`). Run's own move stays, and is never better than a teleport.
+- **RPG** (power-up, weight 6, part of Bomb, key F): fired in a straight line, the bomb flies until
+  the next tile is a wall, crate, bomb or enemy and lands just before it (`flightEnd`), 2 tiles away
+  or more (1 is a plain bomb); it counts down as usual, within the bomb limit. It flies straight,
+  like a kicked bomb (`bombFired`), not in a throw's arc.
+- **ICBM** (charge, key I): 3 launches per pickup, added up like Pierce; Bomb fires one as an RPG
+  bomb at any distance and it goes off that same turn; the bomb limit doesn't count, and it works
+  without the RPG power-up. It lasts past the last enemy, so the last one may drop it. Choice made
+  here: while launches are left, a far tile picked with Bomb is an ICBM, not an RPG bomb (Bomb's
+  group puts ICBM first; the preview shows it going off) — otherwise a player holding both could
+  only use launches by key. The other way round would hide the ICBM behind every RPG target.
+- **Proximity alert** (power-up, weight 5, passive): bombs placed while it is held carry the flag;
+  after the enemies move, each one with an enemy in its reach is set to go off that turn
+  (`proximityAlert`), so an enemy already in reach when the bomb is placed sets it off too —
+  with the player perhaps beside it. The danger overlay runs the same check on where the enemies'
+  arrows say they will be, this turn and the next.
+- **Slide** (power-up, weight 6, part of Move, key L): over free floor until the next tile is a
+  wall, crate or bomb (`slidePath`); it picks up what it passes (coins, charges, an uncovered gem),
+  crashes into an enemy on the way (a life lost, like Run's) and burns in flame on the way unless
+  flame can't hurt; the open exit stops it there. Highlighted is the end tile, 2 or more away
+  (next to the player is a step). A teleport to the same tile wins (it can't crash or burn).
+- **Keys:** with an action picked, a direction now takes the nearest tile that way it can use, at
+  any distance (it tried 1 and 2), so F, I and L then a direction work.
+- **Icons** (`tools/compose-icons.sh`, from Game Icons and Board Game Icons, packs already in use):
+  RPG a bomb with a forward arrow, Proximity a bomb with a warning sign, Slide the Move figure with a
+  two-way arrow, ICBM a bomb with a target.
+- **The demo and clips:** four new scenes (rpg, proximity, slide, icbm; 47 in all), the finger in
+  the RPG, ICBM and Slide clips; the almanac, the offer, the top bar and the texts know all four.
+- Offers and drops draw from more kinds now, so old runs would replay differently: `SIM_VERSION`
+  24. Tests: the new rules each (13), and every demo scene still found on every seed tried (40 per
+  kind, the slowest 0.38 s). Checked in a browser: the four new demo scenes play.
+
+### 2026-10-24 — A slide glides
+
+User report: a slide looked jumpy, like half a dozen evenly spaced steps. Every move took a step's
+0.16 s whatever its length, so six tiles went by in about ten frames, each a big jump. A move of
+more than 2 tiles that isn't a teleport (a slide) now takes 0.07 s per tile — 0.42 s for six —
+eases out (fast at first, slowing as it stops), and is drawn crouched, with no hop and no walking
+frames (`isSlide`, `moveProgress` in effects.ts; the "You" tag and the Shield's ring follow the
+same curve). Steps and runs are as they were. Checked on a video of the demo's slide scene: the
+player moves a little less each frame down to the stop, crouched.
+
+### 2026-10-24 — A tap on a slide's end slides
+
+User report: tapping a tile a slide reaches, with nothing picked, walked there by auto-move
+instead of sliding. A tap's meaning (`tapMeaning`) now has a slide after a step and a diagonal
+step and before auto-move's walk: the tile where a slide stops, with Slide held, is a slide. A
+tile that is also a teleport target still slides only if picked with Move (there a teleport wins,
+being safe); with nothing picked, a tile you can walk to was never a teleport anyway. The demo's
+slide scene now shows the plain tap (its caption: "tap where you stop").
diff --git a/docs/screenshots/almanac-charges.png b/docs/screenshots/almanac-charges.png
index e3f0657..5ff7e35 100644
Binary files a/docs/screenshots/almanac-charges.png and b/docs/screenshots/almanac-charges.png differ
diff --git a/docs/screenshots/almanac.png b/docs/screenshots/almanac.png
index 810ccae..15aef7f 100644
Binary files a/docs/screenshots/almanac.png and b/docs/screenshots/almanac.png differ
diff --git a/docs/screenshots/auto-move.png b/docs/screenshots/auto-move.png
index a033ca7..7f29757 100644
Binary files a/docs/screenshots/auto-move.png and b/docs/screenshots/auto-move.png differ
diff --git a/docs/screenshots/bomb-picked.png b/docs/screenshots/bomb-picked.png
index 0a79c17..a9fefb5 100644
Binary files a/docs/screenshots/bomb-picked.png and b/docs/screenshots/bomb-picked.png differ
diff --git a/docs/screenshots/charges.png b/docs/screenshots/charges.png
index 1d89a7f..b9d20a6 100644
Binary files a/docs/screenshots/charges.png and b/docs/screenshots/charges.png differ
diff --git a/docs/screenshots/countdown.png b/docs/screenshots/countdown.png
index cc3c063..0ac284f 100644
Binary files a/docs/screenshots/countdown.png and b/docs/screenshots/countdown.png differ
diff --git a/docs/screenshots/death.png b/docs/screenshots/death.png
index 3e07817..799938e 100644
Binary files a/docs/screenshots/death.png and b/docs/screenshots/death.png differ
diff --git a/docs/screenshots/demo.png b/docs/screenshots/demo.png
index edfdb53..39dc8eb 100644
Binary files a/docs/screenshots/demo.png and b/docs/screenshots/demo.png differ
diff --git a/docs/screenshots/dev-offer.png b/docs/screenshots/dev-offer.png
index 39ca629..b35771f 100644
Binary files a/docs/screenshots/dev-offer.png and b/docs/screenshots/dev-offer.png differ
diff --git a/docs/screenshots/enemies.png b/docs/screenshots/enemies.png
index ab0b126..6e4d19b 100644
Binary files a/docs/screenshots/enemies.png and b/docs/screenshots/enemies.png differ
diff --git a/docs/screenshots/gem-offer.png b/docs/screenshots/gem-offer.png
index 9f05d0b..55212c0 100644
Binary files a/docs/screenshots/gem-offer.png and b/docs/screenshots/gem-offer.png differ
diff --git a/docs/screenshots/gem.png b/docs/screenshots/gem.png
index 37727c7..508a005 100644
Binary files a/docs/screenshots/gem.png and b/docs/screenshots/gem.png differ
diff --git a/docs/screenshots/info-hud.png b/docs/screenshots/info-hud.png
index f46d8f6..4706a01 100644
Binary files a/docs/screenshots/info-hud.png and b/docs/screenshots/info-hud.png differ
diff --git a/docs/screenshots/lingering.png b/docs/screenshots/lingering.png
index 38164a7..77624f6 100644
Binary files a/docs/screenshots/lingering.png and b/docs/screenshots/lingering.png differ
diff --git a/docs/screenshots/long-press.png b/docs/screenshots/long-press.png
index c927e1f..67fc5c6 100644
Binary files a/docs/screenshots/long-press.png and b/docs/screenshots/long-press.png differ
diff --git a/docs/screenshots/menu.png b/docs/screenshots/menu.png
index c2b7886..731636a 100644
Binary files a/docs/screenshots/menu.png and b/docs/screenshots/menu.png differ
diff --git a/docs/screenshots/move-picked.png b/docs/screenshots/move-picked.png
index f8790a3..9591e51 100644
Binary files a/docs/screenshots/move-picked.png and b/docs/screenshots/move-picked.png differ
diff --git a/docs/screenshots/nuke.png b/docs/screenshots/nuke.png
index c4880b2..2a1bac9 100644
Binary files a/docs/screenshots/nuke.png and b/docs/screenshots/nuke.png differ
diff --git a/src/main.ts b/src/main.ts
index 57e1417..db872c6 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -528,10 +528,12 @@ function pick(id: ActionId, pressed = selected === id): void {
  */
 function direction(d: Dir): void {
   if (state.status !== 'playing') return
-  // Nothing picked: a step, or a kick into a bomb. A picked button reaches 2 tiles too (a run,
-  // a teleport, a throw) where 1 tile can't be used.
+  // Nothing picked: a step, or a kick into a bomb. A picked button reaches further too (a run,
+  // a teleport, a throw; an RPG, an ICBM, a slide as far as they go): the nearest tile that way
+  // it can use.
   const picked: ActionId = selected && ACTIONS[selected].needsTarget ? selected : 'move'
-  for (const reach of selected ? [1, 2] : [1]) {
+  const reaches = selected ? Array.from({ length: Math.max(state.width, state.height) }, (_, i) => i + 1) : [1]
+  for (const reach of reaches) {
     const p = { x: state.player.x + reach * DIRS[d].x, y: state.player.y + reach * DIRS[d].y }
     const id = resolvePick(state, picked, p)
     if (id) return act({ id, target: p })
@@ -562,6 +564,8 @@ const ACTION_KEYS: Record<string, ActionId> = {
   // M picks Move, as B picks Bomb (user request, 2026-10-23; the button said "WASD").
   KeyM: 'move',
   KeyB: 'bomb', KeyR: 'run', KeyX: 'detonate', KeyK: 'kick', KeyT: 'throw', KeyG: 'diagonal', KeyP: 'teleport',
+  // Added 2026-10-24: F fires an RPG bomb, I an ICBM, L slides.
+  KeyF: 'rpg', KeyI: 'icbm', KeyL: 'slide',
 }
 
 /** Q / E / Z / C: a diagonal step up-left, up-right, down-left, down-right (with Diagonal). */
@@ -789,6 +793,7 @@ const SCENE_ICONS: Record<SceneKind, string> = {
   rainbowGem: 'gem_green', immortal: 'star',
   dieIntoBlast: 'fireball', dieNotFar: 'fireball', dieBlockedIn: 'bomb', dieChain: 'bomb', dieEnemy: 'slime_normal_rest',
   dieDetonator: POWER_UP_ICONS.detonator, dieRun: POWER_UP_ICONS.run,
+  rpg: POWER_UP_ICONS.rpg, proximity: POWER_UP_ICONS.proximity, slide: POWER_UP_ICONS.slide, icbm: CHARGE_ICONS.icbm,
 }
 
 
diff --git a/src/render/board.ts b/src/render/board.ts
index 22a74e9..a423fd3 100644
--- a/src/render/board.ts
+++ b/src/render/board.ts
@@ -7,7 +7,7 @@ import { dirBetween } from '../sim/enemies'
 import { cellAt, cloneState, eq, exitOpen, idx } from '../sim/state'
 import type { ActionId, Bomb, Enemy, GameState, Vec } from '../sim/types'
 import {
-  appearScale, dropSprite, frostCrack, frostLevel, GONE_SECONDS, key, moveSeconds, progress, pulseScale, TELEPORT_SECONDS, WALK_SECONDS,
+  appearScale, dropSprite, frostCrack, frostLevel, GONE_SECONDS, isSlide, key, moveProgress, moveSeconds, progress, pulseScale, TELEPORT_SECONDS, WALK_SECONDS,
   type Effects, type Gone, type PlayerMove, type PlayerPose,
 } from './effects'
 import { CHARGE_ICONS, digitSprite, ENEMY_SPRITES, GEM_SPRITES, sprite, wallSprite, type BoardSprite } from './sprites'
@@ -247,7 +247,7 @@ export function playerPos(s: GameState, move: PlayerMove | null): Vector2 {
   const t = move ? time - move.at : Infinity
   if (!move || t >= moveSeconds(move)) return to
   if (move.teleport) return t < TELEPORT_SECONDS ? toWorld(s, move.from) : to
-  return toWorld(s, move.from).lerp(to, t / WALK_SECONDS)
+  return toWorld(s, move.from).lerp(to, moveProgress(move, t))
 }
 
 /**
@@ -273,7 +273,12 @@ function drawPlayer(s: GameState, move: PlayerMove | null, pose: PlayerPose, fac
       drawTile(out ? toWorld(s, move.from).add(PLAYER_LIFT) : to, vec2(k), sprite('character_green_front'), C.white, (1 - k) * 2, facingLeft)
       return
     }
-    const k = t / WALK_SECONDS
+    const k = moveProgress(move, t)
+    // A slide glides crouched, with no hop; a step hops on walking frames.
+    if (isSlide(move)) {
+      drawTile(toWorld(s, move.from).add(PLAYER_LIFT).lerp(to, k), vec2(1), sprite('character_green_duck'), C.white, 0, facingLeft)
+      return
+    }
     const pos = toWorld(s, move.from).add(PLAYER_LIFT).lerp(to, k).add(vec2(0, 0.08 * Math.sin(k * Math.PI)))
     drawTile(pos, vec2(1), sprite(k < 0.5 ? 'character_green_walk_a' : 'character_green_walk_b'), C.white, 0, facingLeft)
     return
diff --git a/src/render/effects.ts b/src/render/effects.ts
index e76ee92..65ff104 100644
--- a/src/render/effects.ts
+++ b/src/render/effects.ts
@@ -112,6 +112,12 @@ export function startEffects(fx: Effects, prev: GameState, next: GameState, even
         placed.add(key.bomb(e.pos))
         break
       }
+      // Fired (RPG, ICBM): it flies straight from the player, as fast as a kicked bomb slides.
+      case 'bombFired': {
+        const tiles = Math.abs(e.to.x - e.from.x) + Math.abs(e.to.y - e.from.y)
+        fx.slides.set(key.bomb(e.to), { from: e.from, at: now, seconds: tiles * KICK_SECONDS_PER_TILE + 0.08, arc: false })
+        break
+      }
       case 'bombKicked': {
         const tiles = Math.abs(e.to.x - e.from.x) + Math.abs(e.to.y - e.from.y)
         fx.slides.set(key.bomb(e.to), { from: e.from, at: now, seconds: tiles * KICK_SECONDS_PER_TILE + 0.08, arc: false })
@@ -249,6 +255,8 @@ export interface PlayerMove {
   from: Vec
   at: number
   teleport: boolean
+  /** Tiles from `from` to where the player is now, in a straight line or not. */
+  tiles?: number
 }
 
 /**
@@ -262,7 +270,26 @@ export interface PlayerPose {
 
 /** How long the player's move takes on screen: a step's slide, or a teleport's vanish and reappear. */
 export function moveSeconds(move: PlayerMove): number {
-  return move.teleport ? 2 * TELEPORT_SECONDS : WALK_SECONDS
+  if (move.teleport) return 2 * TELEPORT_SECONDS
+  return isSlide(move) ? SLIDE_SECONDS_PER_TILE * move.tiles! : WALK_SECONDS
+}
+
+/**
+ * A slide: further than a run's 2 tiles, not a teleport. It glides there crouched, slowing as it
+ * stops, at a speed per tile rather than in a step's time (user report, 2026-10-24: six tiles in a
+ * step's 0.16 s looked like a few jumps).
+ */
+export function isSlide(move: PlayerMove): boolean {
+  return !move.teleport && (move.tiles ?? 1) > 2
+}
+
+/** How long a slide takes per tile. */
+export const SLIDE_SECONDS_PER_TILE = 0.07
+
+/** How far along its move the player is, 0 to 1, `t` seconds in: a slide eases out, a step is even. */
+export function moveProgress(move: PlayerMove, t: number): number {
+  const k = Math.min(1, Math.max(0, t / moveSeconds(move)))
+  return isSlide(move) ? 1 - (1 - k) * (1 - k) : k
 }
 
 /** Seconds a teleport's vanishing (and then its reappearing) takes. */
@@ -281,7 +308,8 @@ export function playerTransition(
 ): { move: PlayerMove | null; pose: PlayerPose } {
   if (events.some((e) => e.type === 'levelStarted' || e.type === 'levelRestarted')) return { move: null, pose: { kind: 'none', at: now } }
   const moved = prev.player.x !== next.player.x || prev.player.y !== next.player.y
-  const move = moved ? { from: { ...prev.player }, at: now, teleport: teleported } : null
+  const tiles = Math.abs(next.player.x - prev.player.x) + Math.abs(next.player.y - prev.player.y)
+  const move = moved ? { from: { ...prev.player }, at: now, teleport: teleported, tiles } : null
   const kind = events.some((e) => e.type === 'playerDied')
     ? 'hit'
     : events.some((e) => e.type === 'levelCleared')
diff --git a/src/render/sprites.ts b/src/render/sprites.ts
index 75bcc9b..d26cd5b 100644
--- a/src/render/sprites.ts
+++ b/src/render/sprites.ts
@@ -98,6 +98,10 @@ const BOARD = [
   'charge_shield',
   'charge_freeze',
   'charge_decoy',
+  'powerup_rpg',
+  'powerup_proximity',
+  'powerup_slide',
+  'charge_icbm',
 ] as const
 
 export type BoardSprite = (typeof BOARD)[number]
@@ -152,7 +156,8 @@ export function wallSprite(level: number, width: number, height: number, p: { x:
  * The icon for each charge, on the board (as a drop), in the top bar while it lasts, and as a
  * bomb's badge (user's choices, 2026-10-23): Pierce is the Wall Pass icon, Power the Fire+ icon;
  * Nuke a bomb with a skull badge; Shield the
- * figure in an empty ring; Freeze a locked padlock; Decoy three pawns (tools/compose-icons.sh).
+ * figure in an empty ring; Freeze a locked padlock; Decoy three pawns; ICBM a bomb with a target
+ * (tools/compose-icons.sh).
  */
 export const CHARGE_ICONS: Record<ChargeId, BoardSprite> = {
   pierce: 'powerup_wall_pass',
@@ -161,6 +166,7 @@ export const CHARGE_ICONS: Record<ChargeId, BoardSprite> = {
   shield: 'charge_shield',
   freeze: 'charge_freeze',
   decoy: 'charge_decoy',
+  icbm: 'charge_icbm',
 }
 
 /**
@@ -195,8 +201,9 @@ export const GEM_SPRITES: Record<GemColor, BoardSprite> = {
  * Each power-up's sprite (PLAN.md §13). Those that change something you already have are that
  * thing with a modifier badge (user request, 2026-10-22): a bomb with +, with expanding arrows
  * (blast), with a timer (fuse), with an arrow right (kick) or up-right (throw), with ! (Detonator)
- * or a shield (Bomb Pass); the Move figure with fast-forward (Run) or an outbox (Teleport); a
- * crate with ✕ (Wall Pass); flame with a shield (Flame Pass). The rest by meaning.
+ * or a shield (Bomb Pass), with a forward arrow (RPG) or a warning sign (Proximity alert); the Move
+ * figure with fast-forward (Run), an outbox (Teleport) or a two-way arrow (Slide); a crate with ✕
+ * (Wall Pass); flame with a shield (Flame Pass). The rest by meaning.
  */
 export const POWER_UP_ICONS: Record<PowerUpKind, BoardSprite> = {
   bombPlus: 'powerup_bomb_plus',
@@ -214,4 +221,7 @@ export const POWER_UP_ICONS: Record<PowerUpKind, BoardSprite> = {
   throw: 'powerup_throw',
   teleport: 'powerup_teleport',
   diagonal: 'powerup_diagonal',
+  rpg: 'powerup_rpg',
+  proximity: 'powerup_proximity',
+  slide: 'powerup_slide',
 }
diff --git a/src/sim/actions.ts b/src/sim/actions.ts
index fe2b2dc..0353d90 100644
--- a/src/sim/actions.ts
+++ b/src/sim/actions.ts
@@ -1,5 +1,5 @@
 import { enemyAt } from './enemies'
-import { add, bombAt, cellAt, eq, isBurning, isFree, playerCanEnter } from './state'
+import { add, bombAt, cellAt, eq, exitOpen, isBurning, isFree, playerCanEnter } from './state'
 import { DIRS, DIR_LIST, type ActionId, type Bomb, type ChargeId, type GameEvent, type GameState, type Powers, type Vec } from './types'
 
 /**
@@ -13,6 +13,8 @@ export interface ActionDef {
   needsTarget: boolean
   /** The power-up ability it needs (PLAN.md §3.2); only offered once the player has it. */
   ability?: keyof Powers
+  /** Anything else it needs (a charge's launches left). */
+  available?: (s: GameState) => boolean
   validTargets(s: GameState): Vec[]
   apply(s: GameState, target: Vec | undefined, events: GameEvent[]): void
 }
@@ -27,7 +29,7 @@ function canHoldBomb(s: GameState, p: Vec): boolean {
 }
 
 /** A bomb from the player, with every boost they have left (Pierce, Power, Nuke), each using one. */
-function placeBomb(s: GameState, pos: Vec, events: GameEvent[]): void {
+function placeBomb(s: GameState, pos: Vec, events: GameEvent[]): Bomb {
   const bomb: Bomb = { pos: { ...pos }, timer: s.stats.fuse, fire: s.stats.fire, placedTurn: s.turn }
   for (const kind of BOMB_BOOSTS) {
     if (s.boosts[kind] <= 0) continue
@@ -36,8 +38,59 @@ function placeBomb(s: GameState, pos: Vec, events: GameEvent[]): void {
   }
   // A Power bomb's flame reaches as far as the level is wide: in practice, to the walls.
   if (bomb.power) bomb.fire = Math.max(s.width, s.height)
+  if (s.powers.proximity) bomb.proximity = true
   s.bombs.push(bomb)
   events.push({ type: 'bombPlaced', pos: { ...pos }, timer: s.stats.fuse })
+  return bomb
+}
+
+/**
+ * Where a bomb fired from the player in direction `d` lands (RPG, ICBM): it flies until the next
+ * tile is a wall, crate, bomb or enemy (or flame), and lands just before it (user's choice,
+ * 2026-10-24). Null if the tile next to the player can't hold it.
+ */
+export function flightEnd(s: GameState, d: Vec): Vec | null {
+  let p = add(s.player, d)
+  if (!canHoldBomb(s, p)) return null
+  while (canHoldBomb(s, add(p, d))) p = add(p, d)
+  return p
+}
+
+/** The landing tiles of a bomb fired each way, at least `min` tiles from the player. */
+function flightTargets(s: GameState, min: number): Vec[] {
+  return DIR_LIST.flatMap((d) => {
+    const p = flightEnd(s, DIRS[d])
+    return p && Math.abs(p.x - s.player.x) + Math.abs(p.y - s.player.y) >= min ? [p] : []
+  })
+}
+
+/** The way from the player to a tile in a straight line, one step. */
+function wayTo(s: GameState, target: Vec): Vec {
+  return { x: Math.sign(target.x - s.player.x), y: Math.sign(target.y - s.player.y) }
+}
+
+/**
+ * The tiles a slide from the player in direction `d` passes, in order, the last where it stops
+ * (user's choice, 2026-10-24): it goes on over free floor until the next tile is a wall, crate or
+ * bomb; it stops on an enemy (crashing into it) and on the open exit. Flame on the way doesn't
+ * stop it (step() burns the player, unless flame can't hurt them).
+ */
+export function slidePath(s: GameState, d: Vec): Vec[] {
+  const path: Vec[] = []
+  let p = s.player
+  for (;;) {
+    const q = add(p, d)
+    if (!isFree(s, q)) break
+    path.push(q)
+    if (enemyAt(s, q) || (exitOpen(s) && eq(q, s.exit))) break
+    p = q
+  }
+  return path
+}
+
+/** How far Teleport reaches: TELEPORT_RANGE, one more with Run (user request, 2026-10-24). */
+export function teleportRange(s: GameState): number {
+  return TELEPORT_RANGE + (s.powers.run ? 1 : 0)
 }
 
 function hasCapacity(s: GameState): boolean {
@@ -80,6 +133,7 @@ export function applyCharge(s: GameState, kind: ChargeId, events: GameEvent[]):
     case 'pierce':
     case 'power':
     case 'nuke':
+    case 'icbm':
       s.boosts[kind] += BOMB_BOOST
       break
     case 'shield':
@@ -117,7 +171,7 @@ export const ACTIONS: Record<ActionId, ActionDef> = {
     label: 'Bomb',
     needsTarget: true,
     validTargets: bombTargets,
-    apply: (s, target, events) => placeBomb(s, target!, events),
+    apply: (s, target, events) => void placeBomb(s, target!, events),
   },
   wait: {
     id: 'wait',
@@ -188,7 +242,7 @@ export const ACTIONS: Record<ActionId, ActionDef> = {
             const land = add(mid, DIRS[d])
             return cellAt(s, mid) !== 'wall' && canHoldBomb(s, land) ? [land] : []
           }),
-    apply: (s, target, events) => placeBomb(s, target!, events),
+    apply: (s, target, events) => void placeBomb(s, target!, events),
   },
   diagonal: {
     id: 'diagonal',
@@ -213,20 +267,66 @@ export const ACTIONS: Record<ActionId, ActionDef> = {
     needsTarget: true,
     ability: 'teleport',
     validTargets(s) {
+      const range = teleportRange(s)
       const tiles: Vec[] = []
-      for (let dy = -TELEPORT_RANGE; dy <= TELEPORT_RANGE; dy++) {
-        for (let dx = -TELEPORT_RANGE; dx <= TELEPORT_RANGE; dx++) {
+      for (let dy = -range; dy <= range; dy++) {
+        for (let dx = -range; dx <= range; dx++) {
           const p = add(s.player, { x: dx, y: dy })
-          if ((dx || dy) && Math.abs(dx) + Math.abs(dy) <= TELEPORT_RANGE && playerCanEnter(s, p)) tiles.push(p)
+          if ((dx || dy) && Math.abs(dx) + Math.abs(dy) <= range && playerCanEnter(s, p)) tiles.push(p)
         }
       }
       return tiles
     },
     apply: (s, target, events) => walkTo(s, target!, events),
   },
+  rpg: {
+    id: 'rpg',
+    label: 'RPG',
+    needsTarget: true,
+    ability: 'rpg',
+    // Fired each way, landing 2 tiles away or more (1 is a plain bomb); within the bomb limit.
+    validTargets: (s) => (hasCapacity(s) ? flightTargets(s, 2) : []),
+    apply(s, target, events) {
+      placeBomb(s, target!, events)
+      events.push({ type: 'bombFired', from: { ...s.player }, to: { ...target! } })
+    },
+  },
+  icbm: {
+    id: 'icbm',
+    label: 'ICBM',
+    needsTarget: true,
+    available: (s) => s.boosts.icbm > 0,
+    // Fired as an RPG's, at any distance; it goes off this turn, so the bomb limit doesn't apply.
+    validTargets: (s) => flightTargets(s, 1),
+    apply(s, target, events) {
+      s.boosts.icbm--
+      placeBomb(s, target!, events).timer = 0
+      events.push({ type: 'bombFired', from: { ...s.player }, to: { ...target! } })
+    },
+  },
+  slide: {
+    id: 'slide',
+    label: 'Slide',
+    needsTarget: true,
+    ability: 'slide',
+    // Where each way's slide stops, 2 tiles away or more (1 is a step).
+    validTargets: (s) =>
+      DIR_LIST.flatMap((d) => {
+        const path = slidePath(s, DIRS[d])
+        return path.length >= 2 ? [path[path.length - 1]!] : []
+      }),
+    apply: (s, target, events) => walkTo(s, target!, events),
+  },
+}
+
+/** The tiles a slide to `target` passed before stopping there (step() picks up and burns on them). */
+export function slidePassed(s: GameState, target: Vec): Vec[] {
+  return slidePath(s, wayTo(s, target)).slice(0, -1)
 }
 
-export const ACTION_ORDER: readonly ActionId[] = ['move', 'bomb', 'wait', 'run', 'detonate', 'kick', 'throw', 'diagonal', 'teleport']
+export const ACTION_ORDER: readonly ActionId[] = [
+  'move', 'bomb', 'wait', 'run', 'detonate', 'kick', 'throw', 'diagonal', 'teleport', 'rpg', 'icbm', 'slide',
+]
 
 export function isValidTarget(s: GameState, id: ActionId, target: Vec): boolean {
   return ACTIONS[id].validTargets(s).some((p) => eq(p, target))
@@ -238,5 +338,6 @@ export function isAvailable(s: GameState, id: ActionId): boolean {
   const ability = ACTIONS[id].ability
   if (ability && !s.powers[ability]) return false
   const def = ACTIONS[id]
+  if (def.available && !def.available(s)) return false
   return !def.needsTarget || def.validTargets(s).length > 0
 }
diff --git a/src/sim/danger.ts b/src/sim/danger.ts
index 4ccf13d..af55132 100644
--- a/src/sim/danger.ts
+++ b/src/sim/danger.ts
@@ -1,4 +1,4 @@
-import { detonate, tickBombs } from './explosions'
+import { detonate, proximityAlert, tickBombs } from './explosions'
 import { cloneState } from './state'
 import type { GameEvent, GameState } from './types'
 
@@ -17,9 +17,14 @@ export interface Danger {
 export function predictDanger(state: GameState): Danger {
   const s = cloneState(state)
   const ignored: GameEvent[] = []
+  // Proximity bombs: set off by where the enemies will be after their announced moves (frozen
+  // ones stay put), this turn and the next.
+  const enemiesAfter = (k: number) => state.enemies.map((e) => (state.frozen > k ? e.pos : (e.plan[k] ?? e.plan[e.plan.length - 1] ?? e.pos)))
+  proximityAlert(s, ignored, enemiesAfter(0))
   tickBombs(s, ignored)
   const exploding = detonate(s, ignored)
   s.turn++
+  proximityAlert(s, ignored, enemiesAfter(1))
   tickBombs(s, ignored)
   const later = detonate(s, ignored)
   return { now: new Set([...exploding, ...state.burning]), next: new Set([...later, ...exploding]) }
diff --git a/src/sim/demo.ts b/src/sim/demo.ts
index 63c356c..525ad78 100644
--- a/src/sim/demo.ts
+++ b/src/sim/demo.ts
@@ -37,6 +37,7 @@ export type SceneKind =
   | 'tapMove' | 'holdBomb' | 'bombButton' | 'wait' | 'autoMove'
   | 'kill' | 'arrows' | 'escape' | 'chain' | 'lingering' | 'enemies' | 'coins' | 'gem'
   | 'bombPlus' | 'firePlus' | 'fusePlus' | 'run' | 'kick' | 'throw' | 'teleport' | 'diagonal' | 'detonate'
+  | 'rpg' | 'proximity' | 'slide' | 'icbm'
   | 'wallPass' | 'bombPass' | 'flamePass' | 'star'
   | 'pierce' | 'power' | 'nuke' | 'shield' | 'freeze' | 'decoy'
   | 'exit' | 'doorBlast'
@@ -295,6 +296,31 @@ export const SCENES: Record<SceneKind, SceneDef> = {
     prepare: (s) => ((s.powers.detonate = true), (s.stats.fuse = 6)),
     shows: (t) => t.events.some((e) => e.type === 'detonated') && kills(t).length > 0,
   },
+  // Added 2026-10-24 (user request), with their power-ups and charge.
+  rpg: {
+    caption: 'RPG: press Bomb, then tap where it lands — it flies as far as it can.',
+    enemies: ['blob', 'drifter'],
+    prepare: (s) => (s.powers.rpg = true),
+    shows: (t, seen) => seen.used.has('rpg') && kills(t).length > 0,
+  },
+  proximity: {
+    caption: 'Proximity alert: your bombs go off as soon as an enemy is in their reach.',
+    enemies: ['blob', 'seeker'],
+    prepare: (s) => (s.powers.proximity = true),
+    shows: (t) => t.events.some((e) => e.type === 'proximityTriggered') && kills(t).length > 0,
+  },
+  slide: {
+    caption: 'Slide: tap where you stop — you slide until a wall, crate or bomb.',
+    enemies: ['blob'],
+    prepare: (s) => (s.powers.slide = true),
+    shows: (t) => t.action.id === 'slide' && steps(t.prev.player, t.next.player) >= 3,
+  },
+  icbm: {
+    caption: 'ICBM: press Bomb, then tap where it lands — it goes off at once.',
+    enemies: ['blob', 'drifter'],
+    prepare: dropping('icbm', 1),
+    shows: (t) => t.action.id === 'icbm' && kills(t).length > 0,
+  },
   wallPass: {
     caption: 'Wall Pass: walk through crates.',
     enemies: ['blob'],
@@ -706,7 +732,8 @@ function rollout(kind: SceneKind, start: GameState, rng: Rng): GameTurn[] | null
 function reasonable(kind: SceneKind, t: Turn): boolean {
   const punishing = kind !== 'doorBlast'
   if (punishing && t.events.some((e) => e.type === 'enemiesSpawned')) return false
-  if ((t.action.id !== 'bomb' && t.action.id !== 'throw') || !('target' in t.action) || !t.action.target) return true
+  const placing = t.action.id === 'bomb' || t.action.id === 'throw' || t.action.id === 'rpg'
+  if (!placing || !('target' in t.action) || !t.action.target) return true
   const target = t.action.target
   const bomb = t.next.bombs.find((b) => at(b.pos, target))
   if (!bomb) return true
@@ -721,8 +748,8 @@ function reasonable(kind: SceneKind, t: Turn): boolean {
 
 /**
  * The gesture that plays `action` in a scene, or null if none does (a step onto a bomb with Bomb
- * Pass: a tap there kicks it, or means nothing). A step, a diagonal step, a kick or a detonation is
- * a tap on the tile; a bomb is a long press, or the Bomb button and a tap in the scene about it;
+ * Pass: a tap there kicks it, or means nothing). A step, a diagonal step, a slide, a kick or a detonation is
+ * a tap on the tile (a slide too); a bomb is a long press, or the Bomb button and a tap in the scene about it;
  * Wait is its button; Run, Teleport and Throw are their button (Move or Bomb), then the tile.
  */
 function gestureFor(def: SceneDef, s: GameState, action: Action): Gesture | null {
@@ -735,7 +762,7 @@ function gestureFor(def: SceneDef, s: GameState, action: Action): Gesture | null
   return resolvePick(s, button, tile) === action.id ? { kind: 'button', button, tile } : null
 }
 
-const TAPPED: ReadonlySet<ActionId> = new Set(['move', 'diagonal', 'kick', 'detonate'])
+const TAPPED: ReadonlySet<ActionId> = new Set(['move', 'diagonal', 'kick', 'detonate', 'slide'])
 
 /** An action and the gesture that plays it in this scene, or null if no gesture does. */
 function playOf(def: SceneDef, s: GameState, action: Action | null): Play | null {
@@ -744,7 +771,9 @@ function playOf(def: SceneDef, s: GameState, action: Action | null): Play | null
 }
 
 /** Weights for a random action: mostly moving, bombing now and then, sometimes waiting; abilities when held. */
-const WEIGHTS: Partial<Record<ActionId, number>> = { move: 5, bomb: 2, wait: 1, detonate: 3, run: 2, kick: 3, throw: 2, teleport: 2, diagonal: 2 }
+const WEIGHTS: Partial<Record<ActionId, number>> = {
+  move: 5, bomb: 2, wait: 1, detonate: 3, run: 2, kick: 3, throw: 2, teleport: 2, diagonal: 2, rpg: 2, icbm: 3, slide: 3,
+}
 
 function randomAction(s: GameState, rng: Rng): Action {
   const choices = (Object.keys(WEIGHTS) as ActionId[]).filter((id) => isAvailable(s, id))
diff --git a/src/sim/enemies.ts b/src/sim/enemies.ts
index dddba3e..f9a4f35 100644
--- a/src/sim/enemies.ts
+++ b/src/sim/enemies.ts
@@ -175,7 +175,7 @@ const DROP_CHANCE: Record<EnemyKind, { coins: number; charge: number }> = {
  * and Decoy only work on enemies, so dropped by the last one they were wasted (user report,
  * 2026-10-23; the user's choice of fix).
  */
-export const LASTING_CHARGES: readonly ChargeId[] = ['pierce', 'power', 'nuke', 'shield']
+export const LASTING_CHARGES: readonly ChargeId[] = ['pierce', 'power', 'nuke', 'shield', 'icbm']
 
 /**
  * A Decoy, Freeze or Shield already lying on the board, or already working, makes another one
diff --git a/src/sim/explosions.ts b/src/sim/explosions.ts
index 1f60b85..f3601ef 100644
--- a/src/sim/explosions.ts
+++ b/src/sim/explosions.ts
@@ -60,6 +60,20 @@ export function detonate(s: GameState, events: GameEvent[]): Set<number> {
   return burned
 }
 
+/**
+ * Proximity alert (user request, 2026-10-24): each of the player's proximity bombs with an enemy
+ * in its reach goes off this turn. `enemyTiles`: where the enemies are (the danger overlay passes
+ * where they are going next).
+ */
+export function proximityAlert(s: GameState, events: GameEvent[], enemyTiles: Vec[] = s.enemies.map((e) => e.pos)): void {
+  for (const b of s.bombs) {
+    if (!b.proximity || b.timer <= 0) continue
+    if (!blastTiles(s, b).some((t) => enemyTiles.some((e) => e.x === t.x && e.y === t.y))) continue
+    b.timer = 0
+    events.push({ type: 'proximityTriggered', pos: { ...b.pos } })
+  }
+}
+
 /** Phase 3's countdown: every bomb not placed this turn loses 1 from its timer. */
 export function tickBombs(s: GameState, events: GameEvent[]): void {
   for (const b of s.bombs) {
diff --git a/src/sim/picks.ts b/src/sim/picks.ts
index 0303437..833c682 100644
--- a/src/sim/picks.ts
+++ b/src/sim/picks.ts
@@ -13,11 +13,13 @@ import type { ActionId, GameState, Vec } from './types'
  * Members are in order of preference: a tile two of them can reach goes to the earlier one, so
  * a kick beats walking onto the bomb with Bomb Pass (walking into a bomb kicks it, if it can
  * slide), a plain step beats a run, and a diagonal step beats a teleport to the same tile (a
- * tile next to you is a step; user report, 2026-10-23).
+ * tile next to you is a step; user report, 2026-10-23). A teleport beats a slide to the same
+ * tile (it can't crash or burn). While ICBM launches are left, a far tile is an ICBM rather than
+ * an RPG bomb (2026-10-24): the commit preview shows the blast going off at once.
  */
 const GROUPS: Partial<Record<ActionId, readonly ActionId[]>> = {
-  move: ['kick', 'move', 'run', 'diagonal', 'teleport'],
-  bomb: ['bomb', 'throw', 'detonate'],
+  move: ['kick', 'move', 'run', 'diagonal', 'teleport', 'slide'],
+  bomb: ['bomb', 'throw', 'icbm', 'rpg', 'detonate'],
 }
 
 /** The actions a picked one stands for: its group, or just itself. */
@@ -46,13 +48,14 @@ export function canPick(s: GameState, picked: ActionId): boolean {
 }
 
 /** What a tap on a tile means with no button picked (`tapMeaning`). */
-export type TapMeaning = 'wait' | 'detonate' | 'kick' | 'blocked' | 'move' | 'diagonal' | 'walk' | 'teleport'
+export type TapMeaning = 'wait' | 'detonate' | 'kick' | 'blocked' | 'move' | 'diagonal' | 'slide' | 'walk' | 'teleport'
 
 /**
  * What a tap on `p` means with nothing picked (main.ts `tapTile`; also the demo's taps), first
  * match wins: the player waits; one of your bombs, with the Detonator, goes off; a bomb next to
  * you, with Kick, is kicked; an enemy, any other bomb, or flame that can hurt you is 'blocked'
- * (never a move into it); then a step, a diagonal step, a far tile you can walk to ('walk': it
+ * (never a move into it); then a step, a diagonal step, the tile where a slide stops (with Slide:
+ * user report, 2026-10-24, it walked there instead), a far tile you can walk to ('walk': it
  * becomes auto-move's destination), or one you can only teleport to. Null: it means nothing.
  * Auto-move's own destination is the UI's to check: a tap on it is its next step.
  */
@@ -63,6 +66,7 @@ export function tapMeaning(s: GameState, p: Vec): TapMeaning | null {
   if (enemyAt(s, p) || bombAt(s, p) || (isBurning(s, p) && !flameSafe(s))) return 'blocked'
   if (isValidTarget(s, 'move', p)) return 'move'
   if (isAvailable(s, 'diagonal') && isValidTarget(s, 'diagonal', p)) return 'diagonal'
+  if (isAvailable(s, 'slide') && isValidTarget(s, 'slide', p)) return 'slide'
   if (cellAt(s, p) === 'floor' && playerPath(s, p)) return 'walk'
   // Out of walking reach (behind a pillar, say) but within Teleport's: one teleport there.
   if (isAvailable(s, 'teleport') && isValidTarget(s, 'teleport', p)) return 'teleport'
diff --git a/src/sim/record.ts b/src/sim/record.ts
index 7b6714b..57d621b 100644
--- a/src/sim/record.ts
+++ b/src/sim/record.ts
@@ -6,7 +6,7 @@ import { DEFAULT_OPTIONS, type Action, type GameEvent, type GameState, type RunO
  * Bumped whenever a rule change would make an old record replay differently. Records from
  * another version can't be trusted to replay (PLAN.md §11).
  */
-export const SIM_VERSION = 23
+export const SIM_VERSION = 24
 
 /** Everything needed to reproduce a run exactly: mid-run saves, replays, certificates. */
 export interface RunRecord {
diff --git a/src/sim/state.ts b/src/sim/state.ts
index b3a4313..b70cb5e 100644
--- a/src/sim/state.ts
+++ b/src/sim/state.ts
@@ -26,11 +26,14 @@ export interface Carry {
 }
 
 export function noPowers(): Powers {
-  return { run: false, kick: false, throw: false, teleport: false, diagonal: false, detonate: false, wallPass: false, bombPass: false, flamePass: false }
+  return {
+    run: false, kick: false, throw: false, teleport: false, diagonal: false, detonate: false, wallPass: false, bombPass: false, flamePass: false,
+    rpg: false, proximity: false, slide: false,
+  }
 }
 
 export function noBoosts(): Boosts {
-  return { pierce: 0, power: 0, nuke: 0 }
+  return { pierce: 0, power: 0, nuke: 0, icbm: 0 }
 }
 
 /**
@@ -39,7 +42,7 @@ export function noBoosts(): Boosts {
  * spot (a testing aid, `?charges=` in the URL).
  */
 export function newRun(seed: number, level = 1, n = 0, options: RunOptions = DEFAULT_OPTIONS): GameState {
-  const boosts = n > 0 ? { pierce: n * BOMB_BOOST, power: n * BOMB_BOOST, nuke: n * BOMB_BOOST } : noBoosts()
+  const boosts = n > 0 ? { pierce: n * BOMB_BOOST, power: n * BOMB_BOOST, nuke: n * BOMB_BOOST, icbm: n * BOMB_BOOST } : noBoosts()
   return createLevel(seed, level, options, {
     lives: START_LIVES, stats: { ...START_STATS }, coins: 0, boosts, shield: n > 0, powers: noPowers(), star: 0,
     runTurns: 0, runKills: 0, deaths: 0,
diff --git a/src/sim/turn.ts b/src/sim/turn.ts
index 60a8a5f..36df56a 100644
--- a/src/sim/turn.ts
+++ b/src/sim/turn.ts
@@ -1,7 +1,7 @@
-import { ACTIONS, applyCharge, isAvailable, isValidTarget } from './actions'
+import { ACTIONS, applyCharge, isAvailable, isValidTarget, slidePassed } from './actions'
 import { enemyAt, moveEnemies, planIntents, rollDrops } from './enemies'
 import { enemyKinds, LAST_LEVEL } from './levelgen'
-import { detonate, tickBombs } from './explosions'
+import { detonate, proximityAlert, tickBombs } from './explosions'
 import { add, bombAt, carryOf, cellAt, cloneState, createLevel, eq, exitOpen, idx, PAR_BONUS } from './state'
 import { stream } from './rng'
 import { CHARGE_IDS, DIRS, DIR_LIST, isCharge, type Action, type Death, type GameEvent, type GameState, type OfferKind, type Powers, type PowerUpKind, type Vec } from './types'
@@ -57,10 +57,14 @@ export function step(prev: GameState, action: Action): StepResult {
     throw new Error(`invalid target for ${action.id}: ${JSON.stringify(action.target)}`)
   }
   const from = s.player
+  // A slide passes over every tile before where it stops (worked out before the player moves).
+  const slid = action.id === 'slide' ? slidePassed(s, action.target!) : []
   def.apply(s, action.target, events)
   // A run passes through the tile in between: anything there is picked up, or run into.
   const passed: Vec | null = action.id === 'run' ? add(from, { x: (s.player.x - from.x) / 2, y: (s.player.y - from.y) / 2 }) : null
   if (passed) pickUp(s, passed, events)
+  // A slide picks up whatever it passes over.
+  for (const p of slid) pickUp(s, p, events)
   pickUp(s, s.player, events)
   if (exitOpen(s) && eq(s.player, s.exit)) {
     const parBonus = s.turn + 1 <= s.par ? PAR_BONUS : 0
@@ -97,6 +101,8 @@ export function step(prev: GameState, action: Action): StepResult {
   // Contact, first check: the player walked (or ran) into an enemy, or swapped places with one.
   const walkedInto = enemyAt(s, s.player) ?? (passed ? enemyAt(s, passed) : undefined)
   if (walkedInto) hurt({ cause: 'walkedIntoEnemy', enemy: walkedInto.kind, turn: s.turn + 1 })
+  // A slide over last turn's flame burns (it doesn't stop for it: user's choice, 2026-10-24).
+  if (s.status === 'playing' && slid.some((p) => s.burning.includes(idx(s, p)))) hurt({ cause: 'lingering', turn: s.turn + 1 })
 
   // Phase 2: enemies carry out their announced intents. Second contact check.
   if (s.status === 'playing') {
@@ -104,6 +110,9 @@ export function step(prev: GameState, action: Action): StepResult {
     const caughtBy = enemyAt(s, s.player)
     if (caughtBy) hurt({ cause: 'caughtByEnemy', enemy: caughtBy.kind, turn: s.turn + 1 })
   }
+  // Proximity alert: a bomb with an enemy in its reach goes off this turn (user's choice,
+  // 2026-10-24: wherever the enemy is now, one that just walked in or one already there).
+  proximityAlert(s, events)
 
   // Phase 3: bombs tick; those at 0 explode, in chains. Last turn's flame is still burning too.
   const lingering = new Set(s.burning)
@@ -282,13 +291,14 @@ export const OFFER_SIZE = 5
  */
 export const POWER_UP_WEIGHTS: [PowerUpKind, number][] = [
   ['bombPlus', 20], ['firePlus', 20], ['fusePlus', 6], ['run', 10], ['kick', 8], ['throw', 8], ['teleport', 3], ['diagonal', 8],
-  ['detonator', 9], ['wallPass', 8], ['bombPass', 8], ['flamePass', 5], ['star', 8], ['heart', 4], ['coinBag', 12],
+  ['slide', 6], ['rpg', 6], ['detonator', 9], ['proximity', 5], ['wallPass', 8], ['bombPass', 8], ['flamePass', 5], ['star', 8],
+  ['heart', 4], ['coinBag', 12],
 ]
 
 /** The power-ups that are an ability or pass: offering one already held would change nothing. */
 const HELD_AS: Partial<Record<PowerUpKind, keyof Powers>> = {
   run: 'run', kick: 'kick', throw: 'throw', teleport: 'teleport', diagonal: 'diagonal', detonator: 'detonate',
-  wallPass: 'wallPass', bombPass: 'bombPass', flamePass: 'flamePass',
+  wallPass: 'wallPass', bombPass: 'bombPass', flamePass: 'flamePass', rpg: 'rpg', proximity: 'proximity', slide: 'slide',
 }
 
 /**
@@ -330,5 +340,8 @@ function applyPowerUp(s: GameState, kind: PowerUpKind): void {
     case 'throw': s.powers.throw = true; break
     case 'teleport': s.powers.teleport = true; break
     case 'diagonal': s.powers.diagonal = true; break
+    case 'rpg': s.powers.rpg = true; break
+    case 'proximity': s.powers.proximity = true; break
+    case 'slide': s.powers.slide = true; break
   }
 }
diff --git a/src/sim/types.ts b/src/sim/types.ts
index 2622af9..069c30c 100644
--- a/src/sim/types.ts
+++ b/src/sim/types.ts
@@ -37,6 +37,8 @@ export interface Bomb {
   power?: boolean
   /** Nuke: it also burns every tile within NUKE_RANGE steps, through anything. */
   nuke?: boolean
+  /** Proximity alert: it goes off the turn an enemy is in its reach (the power-up, when placed). */
+  proximity?: boolean
 }
 
 export interface Stats {
@@ -78,6 +80,7 @@ export const DEFAULT_OPTIONS: RunOptions = { dev: false, immortal: false }
 export type PowerUpKind =
   | 'bombPlus' | 'firePlus' | 'fusePlus' | 'run' | 'wallPass' | 'bombPass' | 'flamePass' | 'detonator'
   | 'star' | 'coinBag' | 'heart' | 'kick' | 'throw' | 'teleport' | 'diagonal'
+  | 'rpg' | 'proximity' | 'slide'
 
 /**
  * A gem's colour, from the seed. Only how it looks: every level's gem offers the same kind of
@@ -122,6 +125,12 @@ export interface Powers {
   bombPass: boolean
   /** Flame doesn't hurt. */
   flamePass: boolean
+  /** RPG: Bomb can also fire a bomb in a straight line, as far as it flies (user request, 2026-10-24). */
+  rpg: boolean
+  /** Proximity alert: your bombs go off as soon as an enemy is in their reach. */
+  proximity: boolean
+  /** Slide: Move can also slide in a straight line until a wall, crate or bomb stops you. */
+  slide: boolean
 }
 
 /** The enemy types so far (PLAN.md §10). */
@@ -156,9 +165,9 @@ export interface Enemy {
  * until then, and Diagonal, which became a power-up the same day; Kick, Throw and Teleport until
  * 2026-10-22; Fuse ±1 was removed on 2026-10-22.)
  */
-export type ChargeId = 'pierce' | 'power' | 'nuke' | 'shield' | 'freeze' | 'decoy'
+export type ChargeId = 'pierce' | 'power' | 'nuke' | 'shield' | 'freeze' | 'decoy' | 'icbm'
 
-export const CHARGE_IDS: readonly ChargeId[] = ['pierce', 'power', 'nuke', 'shield', 'freeze', 'decoy']
+export const CHARGE_IDS: readonly ChargeId[] = ['pierce', 'power', 'nuke', 'shield', 'freeze', 'decoy', 'icbm']
 
 /**
  * What a gem can offer: a power-up, or a charge (only Developer mode's rainbow gem, to test them
@@ -176,6 +185,8 @@ export interface Boosts {
   pierce: number
   power: number
   nuke: number
+  /** ICBM launches left: a bomb fired as far as it flies, going off that same turn (user request, 2026-10-24). */
+  icbm: number
 }
 
 /** A stand-in for the player that chasing enemies go after instead, for a few turns. */
@@ -252,7 +263,8 @@ export interface GameState {
 }
 
 /** The actions the player can pick on the board. */
-export type ActionId = 'move' | 'bomb' | 'wait' | 'run' | 'detonate' | 'kick' | 'throw' | 'teleport' | 'diagonal'
+export type ActionId =
+  | 'move' | 'bomb' | 'wait' | 'run' | 'detonate' | 'kick' | 'throw' | 'teleport' | 'diagonal' | 'rpg' | 'icbm' | 'slide'
 
 /** `choose`: one of the gem's offer. It takes no turn. */
 export type Action = { id: ActionId; target?: Vec } | { id: 'continue' } | { id: 'choose'; kind: OfferKind }
@@ -291,6 +303,10 @@ export type GameEvent =
   | { type: 'levelCleared'; level: number; turns: number; parBonus: number }
   | { type: 'levelStarted'; level: number }
   | { type: 'bombKicked'; from: Vec; to: Vec }
+  /** An RPG or ICBM bomb flew from the player to where it landed. */
+  | { type: 'bombFired'; from: Vec; to: Vec }
+  /** Proximity alert: an enemy in a bomb's reach set it off. */
+  | { type: 'proximityTriggered'; pos: Vec }
   | { type: 'shieldBroke'; pos: Vec }
   | { type: 'decoyPlaced'; pos: Vec }
   | { type: 'decoyGone'; pos: Vec; burned: boolean }
diff --git a/src/ui/almanac.ts b/src/ui/almanac.ts
index 3f50f6b..51c5276 100644
--- a/src/ui/almanac.ts
+++ b/src/ui/almanac.ts
@@ -21,7 +21,7 @@ const TABS: [AlmanacTab, string][] = [
 
 /** The power-ups in the order they make sense to read: stats, abilities, passes, the rest. */
 const POWER_UPS: PowerUpKind[] = [
-  'bombPlus', 'firePlus', 'fusePlus', 'run', 'kick', 'throw', 'teleport', 'diagonal', 'detonator',
+  'bombPlus', 'firePlus', 'fusePlus', 'run', 'kick', 'throw', 'teleport', 'diagonal', 'slide', 'rpg', 'detonator', 'proximity',
   'wallPass', 'bombPass', 'flamePass', 'star', 'heart', 'coinBag',
 ]
 
diff --git a/src/ui/clip.ts b/src/ui/clip.ts
index 86d0433..e95d771 100644
--- a/src/ui/clip.ts
+++ b/src/ui/clip.ts
@@ -48,7 +48,7 @@ interface Clip {
  * choice, 2026-10-24: "when helpful"): the actions, and the power-ups worked by hand.
  */
 const FINGER_KINDS: ReadonlySet<SceneKind> = new Set([
-  'tapMove', 'holdBomb', 'bombButton', 'wait', 'run', 'kick', 'throw', 'teleport', 'diagonal', 'detonate',
+  'tapMove', 'holdBomb', 'bombButton', 'wait', 'run', 'kick', 'throw', 'teleport', 'diagonal', 'detonate', 'rpg', 'icbm', 'slide',
 ])
 /** The finger's glide, a tap, and a long press (as in play). */
 const GLIDE_SECONDS = 0.35
diff --git a/src/ui/controls.ts b/src/ui/controls.ts
index 035d8ed..caa46c4 100644
--- a/src/ui/controls.ts
+++ b/src/ui/controls.ts
@@ -41,8 +41,8 @@ interface ButtonDef {
 }
 
 const BUTTONS: ButtonDef[] = [
-  { id: 'move', label: 'Move', icon: 'icon_move', key: 'M', group: ['move', 'run', 'teleport', 'kick', 'diagonal'] },
-  { id: 'bomb', label: 'Bomb', icon: 'bomb', key: 'B', group: ['bomb', 'throw', 'detonate'] },
+  { id: 'move', label: 'Move', icon: 'icon_move', key: 'M', group: ['move', 'run', 'teleport', 'kick', 'diagonal', 'slide'] },
+  { id: 'bomb', label: 'Bomb', icon: 'bomb', key: 'B', group: ['bomb', 'throw', 'icbm', 'rpg', 'detonate'] },
   { id: 'wait', label: 'Wait', icon: 'icon_wait', key: 'Space', group: ['wait'] },
 ]
 
@@ -57,6 +57,9 @@ const PASSES: [keyof Powers, PowerUpKind, string][] = [
   ['wallPass', 'wallPass', 'Wall Pass'],
   ['bombPass', 'bombPass', 'Bomb Pass'],
   ['flamePass', 'flamePass', 'Flame Pass'],
+  ['rpg', 'rpg', 'RPG'],
+  ['proximity', 'proximity', 'Proximity alert'],
+  ['slide', 'slide', 'Slide'],
 ]
 
 /** A top-bar item: a button that opens its info (`data-info`, see ui/info.ts). */
diff --git a/src/ui/info.ts b/src/ui/info.ts
index f64baa9..5d9c0bc 100644
--- a/src/ui/info.ts
+++ b/src/ui/info.ts
@@ -1,4 +1,4 @@
-import { BOMB_BOOST, DECOY_TURNS, FREEZE_TURNS, NUKE_RANGE, TELEPORT_RANGE } from '../sim/actions'
+import { BOMB_BOOST, DECOY_TURNS, FREEZE_TURNS, NUKE_RANGE, TELEPORT_RANGE, teleportRange } from '../sim/actions'
 import { LAST_LEVEL } from '../sim/levelgen'
 import { bombAt, cellAt, eq, exitOpen, isBurning, PAR_BONUS } from '../sim/state'
 import { COIN_BAG, EXIT_SPAWN, OFFER_SIZE, STAR_TURNS } from '../sim/turn'
@@ -31,7 +31,7 @@ export const POWER_UP_TEXT: Record<PowerUpKind, { name: string; text: string }>
   bombPlus: { name: 'Bomb +1', text: 'You can have one more bomb on the board at a time.' },
   firePlus: { name: 'Fire +1', text: 'Your blasts reach one tile further in each direction.' },
   fusePlus: { name: 'Fuse +1', text: 'Your bombs start one higher: one more turn to get away.' },
-  run: { name: 'Run', text: 'Move can also run: 2 tiles in a straight line in one turn. Both tiles must be empty.' },
+  run: { name: 'Run', text: `Move can also run: 2 tiles in a straight line in one turn. Both tiles must be empty. With Teleport, it teleports ${TELEPORT_RANGE + 1} steps instead of ${TELEPORT_RANGE}.` },
   detonator: { name: 'Detonator', text: 'Your bombs pulse: tap one and it goes off this turn.' },
   wallPass: { name: 'Wall Pass', text: 'You can walk through crates.' },
   bombPass: { name: 'Bomb Pass', text: 'You can walk over bombs.' },
@@ -41,14 +41,18 @@ export const POWER_UP_TEXT: Record<PowerUpKind, { name: string; text: string }>
   heart: { name: 'Heart', text: 'One more life.' },
   kick: { name: 'Kick', text: 'Walk into a bomb next to you and it slides away until something stops it, keeping its number.' },
   throw: { name: 'Throw', text: 'Bomb can also put a bomb 2 tiles away in a straight line, over a crate, bomb or enemy in between (not a pillar).' },
-  teleport: { name: 'Teleport', text: `Move can also take you to any empty tile up to ${TELEPORT_RANGE} steps away, over anything.` },
+  teleport: { name: 'Teleport', text: `Move can also take you to any empty tile up to ${TELEPORT_RANGE} steps away, over anything (${TELEPORT_RANGE + 1} with Run).` },
   diagonal: { name: 'Diagonal', text: 'Move can also step diagonally, even squeezing between two blocked tiles.' },
+  rpg: { name: 'RPG', text: 'Bomb can also fire a bomb in a straight line: it flies until a wall, crate, bomb or enemy is next, and lands just before it.' },
+  proximity: { name: 'Proximity alert', text: 'Your bombs go off as soon as an enemy is in their reach — wherever you are standing.' },
+  slide: { name: 'Slide', text: 'Move can also slide in a straight line until a wall, crate or bomb stops you, picking up what you pass. An enemy on the way stops you the hard way; flame on the way burns.' },
 }
 
 /** The scene that shows each power-up, for its clip (PLAN.md §13c); Heart and Coin bag have none. */
 export const POWER_UP_CLIP: Partial<Record<PowerUpKind, SceneKind>> = {
   bombPlus: 'bombPlus', firePlus: 'firePlus', fusePlus: 'fusePlus', run: 'run', detonator: 'detonate', wallPass: 'wallPass',
   bombPass: 'bombPass', flamePass: 'flamePass', star: 'star', kick: 'kick', throw: 'throw', teleport: 'teleport', diagonal: 'diagonal',
+  rpg: 'rpg', proximity: 'proximity', slide: 'slide',
 }
 
 /** What each charge does once picked up (user request, 2026-10-23: they work at once, as a temporary change). */
@@ -59,6 +63,7 @@ export const CHARGE_TEXT: Record<ChargeId, { name: string; text: string }> = {
   shield: { name: 'Shield', text: 'The next hit (flame or enemy) is absorbed, and anything else that hits you that turn. Then the shield is gone.' },
   freeze: { name: 'Freeze', text: `Every enemy freezes in place for ${FREEZE_TURNS} turns, this one included, thawing as it goes.` },
   decoy: { name: 'Decoy', text: `Leaves a copy of you where you picked it up. Seekers chase it instead of you for ${DECOY_TURNS} turns. Flame destroys it.` },
+  icbm: { name: 'ICBM', text: `${BOMB_BOOST} launches: Bomb fires a bomb as far as it flies, as an RPG's, and it goes off the moment it lands. Your bomb limit doesn't count.` },
 }
 
 /**
@@ -70,6 +75,7 @@ export function chargeLeft(s: GameState, id: ChargeId): number | true | 0 {
     case 'pierce':
     case 'power':
     case 'nuke':
+    case 'icbm':
       return s.boosts[id]
     case 'shield':
       return s.shield || 0
@@ -119,7 +125,7 @@ export function modeLines(s: GameState): string[] {
 /** Each ability or pass, as the power-up that gives it (for its icon and name). */
 export const PASS_KIND: Record<keyof Powers, PowerUpKind> = {
   run: 'run', kick: 'kick', throw: 'throw', teleport: 'teleport', diagonal: 'diagonal', detonate: 'detonator', wallPass: 'wallPass',
-  bombPass: 'bombPass', flamePass: 'flamePass',
+  bombPass: 'bombPass', flamePass: 'flamePass', rpg: 'rpg', proximity: 'proximity', slide: 'slide',
 }
 
 export function hudInfo(item: HudItem, s: GameState): Info {
@@ -192,6 +198,7 @@ export function hudInfo(item: HudItem, s: GameState): Info {
     case 'shield':
     case 'freeze':
     case 'decoy':
+    case 'icbm':
       return { icon: CHARGE_ICONS[item], title: chargeTitle(s, item), body: [CHARGE_TEXT[item].text], clip: item }
     default: {
       const kind = PASS_KIND[item]
@@ -310,6 +317,7 @@ function chargeTitle(s: GameState, id: ChargeId): string {
   const left = chargeLeft(s, id)
   if (left === true) return name
   if (id === 'pierce' || id === 'power' || id === 'nuke') return `${name}: next ${left} ${left === 1 ? 'bomb' : 'bombs'}`
+  if (id === 'icbm') return `${name}: ${left} ${left === 1 ? 'launch' : 'launches'} left`
   return `${name}: ${turns(left)} left`
 }
 
@@ -330,22 +338,28 @@ export function actionInfo(s: GameState, picked: ActionId): Info {
     case 'run':
     case 'teleport':
     case 'kick':
-    case 'diagonal': {
+    case 'diagonal':
+    case 'slide': {
       const body = ['Tap a highlighted tile: a step to a tile next to you.']
       if (s.powers.run) body.push('Run: 2 tiles in a straight line, if both are empty.')
-      if (s.powers.teleport) body.push(`Teleport: any empty tile up to ${TELEPORT_RANGE} steps away, over anything.`)
+      if (s.powers.teleport) body.push(`Teleport: any empty tile up to ${teleportRange(s)} steps away, over anything.`)
       if (s.powers.kick) body.push('Kick: walk into a bomb next to you and it slides away until something stops it.')
       if (s.powers.diagonal) body.push('Diagonal: a diagonal step, even between two blocked tiles.')
+      if (s.powers.slide) body.push('Slide: the tile where a slide that way stops, by a wall, crate or bomb.')
       // Its clip: the move that was picked (a Run, a Teleport...), or plain steps.
       return { icon: 'icon_move', title: 'Move', body, clip: picked === 'move' ? 'tapMove' : picked }
     }
     case 'bomb':
     case 'throw':
-    case 'detonate': {
+    case 'detonate':
+    case 'rpg':
+    case 'icbm': {
       const body = [
         `Tap a highlighted tile next to you. The bomb goes off after ${turns(s.stats.fuse)} of yours; its blast reaches ${s.stats.fire} ${s.stats.fire === 1 ? 'tile' : 'tiles'} each way. Up to ${s.stats.capacity} on the board at once.`,
       ]
       if (s.powers.throw) body.push('Throw: or 2 tiles away in a straight line, over a crate, bomb or enemy (not a pillar).')
+      if (s.powers.rpg) body.push('RPG: or as far as it flies, landing before the first wall, crate, bomb or enemy.')
+      if (s.boosts.icbm > 0) body.push(`ICBM: ${s.boosts.icbm} ${s.boosts.icbm === 1 ? 'launch' : 'launches'} left — a bomb fired as far as it flies that goes off at once. While any are left, a far tile is an ICBM.`)
       if (s.powers.detonate) body.push('Detonator: tap one of your bombs (they pulse) and it goes off this turn. No need to pick Bomb first.')
       for (const k of ['pierce', 'power', 'nuke'] as const) {
         if (s.boosts[k] > 0) body.push(`${CHARGE_TEXT[k].name}: your next ${s.boosts[k]} ${s.boosts[k] === 1 ? 'bomb' : 'bombs'}.`)
diff --git a/test/animation.test.ts b/test/animation.test.ts
index 5f2b01f..acf54aa 100644
--- a/test/animation.test.ts
+++ b/test/animation.test.ts
@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import { playerTransition } from '../src/render/effects'
+import { isSlide, moveProgress, moveSeconds, playerTransition, SLIDE_SECONDS_PER_TILE, WALK_SECONDS } from '../src/render/effects'
 import { step } from '../src/sim/turn'
 import { board, v } from './helpers'
 
@@ -16,7 +16,7 @@ describe('the player always moves on screen', () => {
     const r = step(s, { id: 'move', target: v(2, 1) })
     expect(r.state.status).toBe('dead')
     const t = playerTransition(s, r.state, r.events, false, 10)
-    expect(t.move).toEqual({ from: v(1, 1), at: 10, teleport: false })
+    expect(t.move).toEqual({ from: v(1, 1), at: 10, teleport: false, tiles: 1 })
     expect(t.pose.kind).toBe('hit')
   })
 
@@ -38,7 +38,7 @@ describe('the player always moves on screen', () => {
       ######`)
     s.powers.teleport = true
     const tele = step(s, { id: 'teleport', target: v(4, 1) })
-    expect(playerTransition(s, tele.state, tele.events, true, 10).move).toEqual({ from: v(1, 1), at: 10, teleport: true })
+    expect(playerTransition(s, tele.state, tele.events, true, 10).move).toEqual({ from: v(1, 1), at: 10, teleport: true, tiles: 3 })
     const bomb = step(s, { id: 'bomb', target: v(2, 1) })
     const t = playerTransition(s, bomb.state, bomb.events, false, 10)
     expect(t.move).toBeNull()
@@ -55,3 +55,18 @@ describe('the player always moves on screen', () => {
     expect(playerTransition(cleared, next.state, next.events, false, 10).move).toBeNull()
   })
 })
+
+describe('a slide glides', () => {
+  it('takes longer the further it goes, eases out, and a step or run is unchanged', () => {
+    const step1 = { from: v(1, 1), at: 0, teleport: false, tiles: 1 }
+    const slide6 = { from: v(1, 1), at: 0, teleport: false, tiles: 6 }
+    expect(isSlide(step1)).toBe(false)
+    expect(isSlide({ ...step1, tiles: 2 })).toBe(false)
+    expect(isSlide(slide6)).toBe(true)
+    expect(moveSeconds(slide6)).toBeCloseTo(6 * SLIDE_SECONDS_PER_TILE)
+    expect(moveSeconds(step1)).toBe(WALK_SECONDS)
+    // Halfway in time, more than halfway there; then at rest at the end.
+    expect(moveProgress(slide6, moveSeconds(slide6) / 2)).toBeGreaterThan(0.5)
+    expect(moveProgress(slide6, moveSeconds(slide6))).toBe(1)
+  })
+})
diff --git a/test/demo.test.ts b/test/demo.test.ts
index c11301e..1ba5a28 100644
--- a/test/demo.test.ts
+++ b/test/demo.test.ts
@@ -15,7 +15,7 @@ function controlsTake(s: GameState, g: Gesture, destination: Vec | null): Action
   const safe = (p: Vec) => safeStep(s, predictDanger(s), p)
   if (g.kind === 'tap') {
     const means = tapMeaning(s, g.tile)
-    return means === 'move' || means === 'diagonal' || means === 'kick' || means === 'detonate' || means === 'teleport'
+    return means === 'move' || means === 'diagonal' || means === 'kick' || means === 'detonate' || means === 'teleport' || means === 'slide'
       ? { id: means, target: g.tile }
       : null
   }
diff --git a/test/enemies.test.ts b/test/enemies.test.ts
index 11403a6..622067f 100644
--- a/test/enemies.test.ts
+++ b/test/enemies.test.ts
@@ -672,8 +672,9 @@ describe('charge drops are never wasted', () => {
     expect(dropped((s) => (s.shield = true))).not.toContain('shield')
   })
 
-  it('Pierce, Power and Nuke still drop when one lies there: they add up', () => {
+  it('Pierce, Power, Nuke and ICBM still drop when one lies there: they add up', () => {
     expect(dropped((s) => s.drops.push({ pos: v(2, 1), kind: 'pierce' }))).toContain('pierce')
-    expect(dropped(() => {})).toEqual(new Set(['pierce', 'power', 'nuke', 'shield', 'freeze', 'decoy']))
+    expect(dropped((s) => s.drops.push({ pos: v(2, 1), kind: 'icbm' }))).toContain('icbm')
+    expect(dropped(() => {})).toEqual(new Set(['pierce', 'power', 'nuke', 'shield', 'freeze', 'decoy', 'icbm']))
   })
 })
diff --git a/test/new-powers.test.ts b/test/new-powers.test.ts
new file mode 100644
index 0000000..2f09736
--- /dev/null
+++ b/test/new-powers.test.ts
@@ -0,0 +1,176 @@
+import { describe, expect, it } from 'vitest'
+import { ACTIONS, BOMB_BOOST, isAvailable, TELEPORT_RANGE } from '../src/sim/actions'
+import { predictDanger } from '../src/sim/danger'
+import { idx } from '../src/sim/state'
+import { step } from '../src/sim/turn'
+import { board, v } from './helpers'
+
+// The power-ups and charge added 2026-10-24 (user request; the rules are the user's choices).
+
+describe('Run extends Teleport', () => {
+  it(`with Run, Teleport reaches ${TELEPORT_RANGE + 1} steps instead of ${TELEPORT_RANGE}`, () => {
+    const s = board(`
+      ########
+      #P.....#
+      ########`)
+    s.powers.teleport = true
+    expect(ACTIONS.teleport.validTargets(s)).not.toContainEqual(v(5, 1))
+    s.powers.run = true
+    expect(ACTIONS.teleport.validTargets(s)).toContainEqual(v(5, 1))
+    expect(ACTIONS.teleport.validTargets(s)).not.toContainEqual(v(6, 1))
+  })
+})
+
+describe('RPG', () => {
+  it('fires a bomb that lands just before the first wall, crate, bomb or enemy, 2 tiles away or more', () => {
+    const s = board(`
+      #########
+      #P....+.#
+      #.#######
+      #.......#
+      #B#######`)
+    s.powers.rpg = true
+    // Right: lands before the crate at x = 6. Down: lands before the Blob at y = 4.
+    expect(ACTIONS.rpg.validTargets(s)).toEqual(expect.arrayContaining([v(5, 1), v(1, 3)]))
+    const { state, events } = step(s, { id: 'rpg', target: v(5, 1) })
+    expect(state.bombs.map((b) => b.pos)).toEqual([v(5, 1)])
+    expect(state.bombs[0]!.timer).toBe(s.stats.fuse)
+    expect(events).toContainEqual({ type: 'bombFired', from: v(1, 1), to: v(5, 1) })
+  })
+
+  it('needs room in the bomb limit, and the tile next to the player free', () => {
+    const s = board(`
+      #######
+      #P1...#
+      #######`)
+    s.powers.rpg = true
+    expect(ACTIONS.rpg.validTargets(s)).toEqual([])
+  })
+})
+
+describe('ICBM', () => {
+  it(`gives ${BOMB_BOOST} launches; each lands as an RPG bomb and goes off that turn, outside the bomb limit`, () => {
+    const s = board(`
+      #########
+      #P....B+#
+      #########`)
+    s.drops.push({ pos: v(2, 1), kind: 'icbm' })
+    let state = step(s, { id: 'move', target: v(2, 1) }).state
+    expect(state.boosts.icbm).toBe(BOMB_BOOST)
+    expect(isAvailable(state, 'icbm')).toBe(true)
+    state.stats.capacity = 0
+    // The Blob is at x = 6 (it stays put): the bomb lands before it, at x = 5, and its blast gets it.
+    expect(ACTIONS.icbm.validTargets(state)).toContainEqual(v(5, 1))
+    const r = step(state, { id: 'icbm', target: v(5, 1) })
+    expect(r.events.some((e) => e.type === 'exploded')).toBe(true)
+    expect(r.state.enemies).toEqual([])
+    expect(r.state.bombs).toEqual([])
+    expect(r.state.boosts.icbm).toBe(BOMB_BOOST - 1)
+  })
+
+  it('is not there once its launches are spent', () => {
+    const s = board(`
+      #####
+      #P..#
+      #####`)
+    expect(isAvailable(s, 'icbm')).toBe(false)
+  })
+})
+
+describe('Proximity alert', () => {
+  it('sets off a bomb the turn an enemy walks into its reach', () => {
+    const s = board(`
+      ########
+      #P.5..B#
+      ########`)
+    s.powers.proximity = true
+    // The player's bomb (timer 5) gets the flag when placed; mark this one as theirs.
+    s.bombs[0]!.proximity = true
+    // Blast 1: the bomb at x = 3 reaches x = 4, where the Blob steps.
+    s.enemies[0]!.plan = [v(5, 1), v(4, 1)]
+    let state = step(s, { id: 'wait' }).state
+    expect(state.bombs).toHaveLength(1)
+    const r = step(state, { id: 'wait' })
+    const events = r.events
+    state = r.state
+    expect(events).toContainEqual({ type: 'proximityTriggered', pos: v(3, 1) })
+    expect(state.enemies).toEqual([])
+  })
+
+  it('also when the enemy is already in reach as the bomb is placed', () => {
+    const s = board(`
+      #######
+      #P.B..#
+      #######`)
+    s.powers.proximity = true
+    const { state } = step(s, { id: 'bomb', target: v(2, 1) })
+    expect(state.enemies).toEqual([])
+  })
+
+  it('the danger overlay shows it ahead, from the enemies’ next moves', () => {
+    const s = board(`
+      ########
+      #P.5..B#
+      ########`)
+    s.bombs[0]!.proximity = true
+    s.enemies[0]!.plan = [v(5, 1), v(4, 1)]
+    // Not this turn (the Blob is still out of reach), but next turn.
+    expect(predictDanger(s).now.has(idx(s, v(3, 1)))).toBe(false)
+    expect(predictDanger(s).next.has(idx(s, v(3, 1)))).toBe(true)
+  })
+
+  it('leaves a bomb placed without it alone', () => {
+    const s = board(`
+      ########
+      #P.5..B#
+      ########`)
+    s.enemies[0]!.plan = [v(4, 1)]
+    expect(step(s, { id: 'wait' }).state.enemies).toHaveLength(1)
+  })
+})
+
+describe('Slide', () => {
+  it('slides until a wall, crate or bomb is next, picking up what it passes', () => {
+    const s = board(`
+      #########
+      #P.$..+.#
+      #########`)
+    s.powers.slide = true
+    expect(ACTIONS.slide.validTargets(s)).toEqual([v(5, 1)])
+    const { state } = step(s, { id: 'slide', target: v(5, 1) })
+    expect(state.player).toEqual(v(5, 1))
+    expect(state.coins).toBe(1)
+  })
+
+  it('crashes into an enemy on the way', () => {
+    const s = board(`
+      ########
+      #P..B..#
+      ########`)
+    s.powers.slide = true
+    expect(ACTIONS.slide.validTargets(s)).toEqual([v(4, 1)])
+    expect(step(s, { id: 'slide', target: v(4, 1) }).state.status).toBe('dead')
+  })
+
+  it('is burned by flame on the way, unless flame can’t hurt', () => {
+    const s = board(`
+      ########
+      #P.....#
+      ########`)
+    s.powers.slide = true
+    s.burning = [idx(s, v(3, 1))]
+    expect(step(s, { id: 'slide', target: v(6, 1) }).state.status).toBe('dead')
+    s.powers.flamePass = true
+    expect(step(s, { id: 'slide', target: v(6, 1) }).state.status).toBe('playing')
+  })
+
+  it('stops on the open exit', () => {
+    const s = board(`
+      ########
+      #P..E..#
+      ########`)
+    s.powers.slide = true
+    expect(ACTIONS.slide.validTargets(s)).toEqual([v(4, 1)])
+    expect(step(s, { id: 'slide', target: v(4, 1) }).state.status).toBe('cleared')
+  })
+})
diff --git a/test/powerups.test.ts b/test/powerups.test.ts
index 890e4f0..a7f1a3d 100644
--- a/test/powerups.test.ts
+++ b/test/powerups.test.ts
@@ -53,7 +53,7 @@ describe('the offer', () => {
 
   it('leaves out abilities and passes already held', () => {
     const s = newRun(3)
-    s.powers = { run: true, kick: true, throw: true, teleport: true, diagonal: true, detonate: true, wallPass: true, bombPass: false, flamePass: true }
+    s.powers = { run: true, kick: true, throw: true, teleport: true, diagonal: true, detonate: true, wallPass: true, bombPass: false, flamePass: true, rpg: true, proximity: true, slide: true }
     for (let level = 1; level <= 30; level++) {
       s.level = level
       const offer = drawOffer(s)
@@ -64,7 +64,7 @@ describe('the offer', () => {
 
   it('is smaller when fewer would do anything', () => {
     const s = newRun(3)
-    s.powers = { run: true, kick: true, throw: true, teleport: true, diagonal: true, detonate: true, wallPass: true, bombPass: true, flamePass: true }
+    s.powers = { run: true, kick: true, throw: true, teleport: true, diagonal: true, detonate: true, wallPass: true, bombPass: true, flamePass: true, rpg: true, proximity: true, slide: true }
     // Left: Bomb+, Fire+, Fuse+, Star, Heart, Coin bag.
     expect(drawOffer(s)).toHaveLength(OFFER_SIZE)
   })
@@ -305,7 +305,7 @@ describe('passes', () => {
       #####
       #P1.#
       #####`, { capacity: 2 }), star: 0 }
-    s.powers = { run: true, kick: true, throw: true, teleport: true, diagonal: true, detonate: true, wallPass: true, bombPass: true, flamePass: false }
+    s.powers = { run: true, kick: true, throw: true, teleport: true, diagonal: true, detonate: true, wallPass: true, bombPass: true, flamePass: false, rpg: true, proximity: true, slide: true }
     const dead = step(s, { id: 'wait' }).state
     expect(dead.deaths).toBe(1)
     const again = step(dead, { id: 'continue' }).state
diff --git a/tools/compose-icons.sh b/tools/compose-icons.sh
index 6045045..888fdac 100644
--- a/tools/compose-icons.sh
+++ b/tools/compose-icons.sh
@@ -57,6 +57,14 @@ badge "$K/fireball.png" "$B/shield.png" powerup_flame_pass
 badge "$K/bomb.png" "$B/skull.png" charge_nuke
 ink "$I/locked.png" charge_freeze
 ink "$B/pawns.png" charge_decoy
+# The power-ups and charge added 2026-10-24 (user request): RPG, a bomb with a forward arrow;
+# Proximity alert, a bomb with a warning sign; Slide, the Move figure with a two-way arrow; the
+# ICBM charge, a bomb with a target.
+badge "$K/bomb.png" "$I/forward.png" powerup_rpg
+badge "$K/bomb.png" "$I/warning.png" powerup_proximity
+badge "$OUT/icon_move.png" "$B/arrow_horizontal.png" powerup_slide
+badge "$K/bomb.png" "$I/target.png" charge_icbm
+
 # Shield: the Move figure inside an empty ring (the board-game timer at 0).
 magick "$B/timer_0.png" -fill '#353541' -colorize 100 \
   \( "$OUT/icon_move.png" -resize 84x84 \) -gravity center -composite \
diff --git a/tools/screenshots.mjs b/tools/screenshots.mjs
index 2787087..4394f59 100644
--- a/tools/screenshots.mjs
+++ b/tools/screenshots.mjs
@@ -69,11 +69,12 @@ const SHOTS = [
   // With every lasting charge (?charges=1): a bomb placed to the right wears the Pierce, Power and
   // Nuke badges, and after a step down the danger shows the Nuke's reach; the top bar lists them.
   { name: 'nuke', query: 'charges=1', act: press('KeyB', 'KeyD', 'KeyS') },
-  // Seed 55 hides the gem under the crate two tiles below the start: bomb it open
-  // and step back once the flame has died down...
-  { name: 'gem', query: 'seed=55', act: press('KeyB', 'KeyS', 'KeyD', 'KeyD', 'Space', 'KeyA') },
-  // ...then walk back and take it: the choice of power-ups.
-  { name: 'gem-offer', query: 'seed=55', act: press('KeyB', 'KeyS', 'KeyD', 'KeyD', 'Space', 'KeyA', 'KeyA', 'KeyS', 'KeyS') },
+  // Seed 107: a bomb by the gem's crate, out of the way while it goes off, then back: the gem
+  // uncovered beside the player... (found by searching random play with the sim, 2026-10-24; the
+  // old seed-55 moves no longer reached a gem)
+  { name: 'gem', query: 'seed=107', act: press('KeyS', 'KeyB', 'KeyS', 'Space', 'KeyW', 'Space', 'Space', 'Space', 'KeyS', 'KeyS') },
+  // ...then onto it: the choice of power-ups.
+  { name: 'gem-offer', query: 'seed=107', act: press('KeyS', 'KeyB', 'KeyS', 'Space', 'KeyW', 'Space', 'Space', 'Space', 'KeyS', 'KeyS', 'KeyD') },
   // Developer mode: the rainbow gem uncovered right of the player, and the mode's tag in the top bar...
   { name: 'dev', query: 'dev', act: async () => {} },
   // ...and taking it: every power-up on offer.