Mr. Rescue

Arcade firefighting in a procedurally generated burning building.

▶ Run in browser

Upstream: https://github.com/SimonLarsen/mrrescue

Licence: this port's own code is offered under MIT, not the SimVX Examples Licence the rest of the gallery carries. See ATTRIBUTION.md for the upstream it re-implements, the terms of anything it bundles, and the credit each one requires.

Ports live in the repository only, not in the simvx-examples distribution, because each is a derivative work licensed individually against the game it re-implements. Read it with git clone https://git.simvx.com/simvx/simvx.

Tags: port tier-2

Mr. Rescue: SimVX port

A SimVX port of Mr. Rescue by Simon Larsen: an arcade firefighter platformer where you spray water at spreading fires and carry civilians out of a procedurally-stitched 3-storey building before your suit overheats.

Run

# the export runs in a copy made by `simvx examples copy ports_mr_rescue`
simvx examples run ports_mr_rescue            # interactive
simvx export web main.py \
    -o mr_rescue.html

Controls

Key

Action

Arrow keys / WASD

Move horizontally, climb ladders, aim gun up/down

Space

Jump

Shift

Spray water (drains the tank; it overloads when empty)

E

Grab a civilian / throw the carried one

Enter, Space or click

Confirm in menus

Esc

Quit

What it shows

  • Procedural everything. Tiles, characters, the night skyline and the building layout are generated at runtime as NumPy uint8 RGBA; the SFX and the ambient music bed are synthesized with AudioSynth. No asset files ship with the port.

  • Game feel. Coyote time and jump buffering on the tile-grid platforming, Camera2D follow with clamping and screenshake, and hitstop that freezes the gameplay subtree (via UpdateMode.DISABLED) on rescues and kills.

  • Scene structure. A root phase machine swaps title / game / end scenes and listens to Signals for victory and failure; the gameplay scene listens to the player’s, civilians’ and enemies’ own signals for score, audio and particles.

  • Screen-space UI. The HUD lives on the gameplay scene’s own CanvasLayer and lays out against the live framebuffer size, so it tracks a resized desktop window or a responsive web canvas.

Notes

The port keeps one enemy type (the fire bug) rather than upstream’s six, and leaves out the bosses, the lightmap, the on-screen joystick UI and the licensed music tracks. Fire spread, the heat/water economy, ladders and the carry-and- throw rescue loop all follow upstream’s design.

License: port code under MIT (see LICENSE). It mirrors upstream’s zlib gameplay design, with CC-BY-SA 3.0 attribution to Simon Larsen for the original concept and visual reference; no upstream art is reused, as every texture is procedural. See ATTRIBUTION.md.

Source files

File

Summary

Lines

main.py

Mr. Rescue: Arcade firefighting in a procedurally generated burning building.

180

harness.py

Scripted headless playthrough that captures one screenshot per stage.

245

nodes/__init__.py

0

nodes/audio.py

Procedural audio for Mr. Rescue: synthesized SFX + a looping ambient bed.

213

nodes/building.py

Procedural multi-floor building generator.

328

nodes/civilian.py

Civilian: IDLE/WALK/PANIC/BURN/CARRIED/FLY state machine.

204

nodes/colours.py

Mr. Rescue palette: warm-on-charcoal arcade pixels.

94

nodes/enemy.py

FireBug: single enemy type (port collapses upstream’s 6 to one).

149

nodes/fire.py

Fire grid: sparse cell map of flames with health and spread timers.

206

nodes/game.py

Active gameplay scene: wires together grid, fires, player, civilians, enemies.

467

nodes/hud.py

HUD: top status strip + bottom water/heat/casualty bar.

198

nodes/menu.py

Title and end screens.

230

nodes/particles.py

CPU particle pool: water mist, smoke, sparkles, ash.

135

nodes/player.py

Firefighter player: state machine + water gun raycast.

693

nodes/textures.py

Procedural NumPy textures.

593

nodes/tile_grid.py

Tile grid + collision for the building.

188

Source

  1#!/usr/bin/env python3
  2# /// simvx
  3# tags = ["port", "tier-2"]
  4# upstream = "https://github.com/SimonLarsen/mrrescue"
  5# web = { width = 1024, height = 800, responsive = true }
  6# ///
  7"""Mr. Rescue: Arcade firefighting in a procedurally generated burning building.
  8
  9A port of Simon Larsen's "Mr. Rescue": spray water at flames that spread cell to
 10cell, carry civilians out through the roof, and get clear before your suit
 11overheats. Everything is generated at runtime: NumPy pixel-art textures,
 12synthesized SFX and music, and a randomised three-storey building. Shows
 13tile-grid platforming with coyote time and jump buffering, Camera2D follow with
 14screenshake and hitstop, a CanvasLayer HUD, signal-driven scene flow
 15(menu, game, end), and bloom via WorldEnvironment.
 16
 17Run it with ``simvx examples run ports_mr_rescue``, or copy it with
 18``simvx examples copy ports_mr_rescue`` and run ``python main.py`` there;
 19``simvx export web main.py -o mr_rescue.html`` in the copy builds a web page.
 20
 21Controls
 22--------
 23- Arrow keys / WASD: move, aim the gun, climb ladders
 24- Space: jump
 25- Shift: spray water
 26- E: grab / throw the carried civilian
 27- Enter, Space or click / tap: confirm in menus
 28- Esc: quit
 29"""
 30
 31from __future__ import annotations
 32
 33from nodes import colours as C
 34from nodes.audio import get_sfx
 35from nodes.game import GameScene
 36from nodes.menu import EndScreen, TitleScreen
 37
 38from simvx.core import AudioPlayer, Input, InputMap, Key, Mouse, Node
 39from simvx.core.world_environment import WorldEnvironment
 40from simvx.graphics import App
 41
 42WINDOW_W = 1024
 43WINDOW_H = 800
 44
 45
 46# ---------------------------------------------------------------------------- root
 47
 48
 49class MrRescueRoot(Node):
 50    """Phase machine: menu → game → end."""
 51
 52    def __init__(self, *, seed: int | None = None, **kwargs):
 53        super().__init__(**kwargs)
 54        self.phase = "menu"
 55        self.section = 1
 56        self._seed = seed
 57        self.scene: Node | None = None
 58        self._menu_sfx = None
 59
 60    def on_ready(self):
 61        # InputMap MUST live in on_ready: run_headless resets input state, so a
 62        # headless run loses anything registered before it.
 63        InputMap.add_action("left", [Key.LEFT, Key.A])
 64        InputMap.add_action("right", [Key.RIGHT, Key.D])
 65        InputMap.add_action("up", [Key.UP, Key.W])
 66        InputMap.add_action("down", [Key.DOWN, Key.S])
 67        InputMap.add_action("jump", [Key.SPACE])
 68        InputMap.add_action("shoot", [Key.LEFT_SHIFT, Key.RIGHT_SHIFT])
 69        InputMap.add_action("grab", [Key.E])
 70        # Menus confirm on a click/tap too (touch arrives as Mouse.LEFT on
 71        # web), so the responsive export is reachable without a keyboard.
 72        InputMap.add_action("start", [Key.ENTER, Key.SPACE, Mouse.LEFT])
 73        InputMap.add_action("quit", [Key.ESCAPE])
 74
 75        env = self.add_child(WorldEnvironment())
 76        env.sky_mode = "disabled"
 77        env.tonemap_mode = "aces"
 78        env.tonemap_exposure = 1.05
 79        env.bloom_enabled = True
 80        env.bloom_threshold = 0.78
 81        env.bloom_intensity = 0.52
 82        env.bloom_soft_knee = 0.5
 83
 84        # One-shot menu confirm cue (gameplay SFX live on the GameScene's bank).
 85        self._menu_sfx = self.add_child(AudioPlayer(stream=get_sfx("menu_confirm"), bus="SFX"))
 86
 87        # The HUD is owned by the GameScene (in its own CanvasLayer) so it is
 88        # created and destroyed with the game phase: clean lifecycle, with no HUD
 89        # state to reset between phases.
 90        self._enter_menu()
 91
 92    # ----------------------------------------------------------- phase swaps
 93
 94    def _drop_sub(self):
 95        # Destroying the sub-scene takes its Camera2D with it, and Camera2D's
 96        # teardown releases the tree's active-camera slot, so the menu phases
 97        # come back up in screen space with no extra bookkeeping here.
 98        if self.scene is not None:
 99            self.scene.destroy()
100            self.scene = None
101
102    def _enter_menu(self):
103        self.phase = "menu"
104        self._drop_sub()
105        self.section = 1
106        ts = TitleScreen()
107        ts.start.connect(self._enter_game)
108        self.add_child(ts)
109        self.scene = ts
110
111    def _play_menu_sfx(self):
112        if self._menu_sfx is not None:
113            self._menu_sfx.play()
114
115    def _restart(self):
116        self._play_menu_sfx()
117        self._enter_menu()
118
119    def _enter_game(self):
120        self.phase = "game"
121        self._play_menu_sfx()
122        self._drop_sub()
123        game = GameScene(
124            viewport_w=WINDOW_W,
125            viewport_h=WINDOW_H,
126            section=self.section,
127            level=1,
128            seed=self._seed,
129        )
130        game.victory.connect(self._on_victory)
131        game.failure.connect(self._on_failure)
132        self.add_child(game)
133        self.scene = game
134
135    def _on_victory(self):
136        self._enter_end(victory=True, reason="ALL CIVILIANS RESCUED")
137
138    def _on_failure(self, reason: str):
139        msg = {
140            "casualty": "TOO MANY CIVILIANS LOST",
141            "overheat": "YOUR SUIT OVERHEATED",
142        }.get(reason, "")
143        self._enter_end(victory=False, reason=msg)
144
145    def _enter_end(self, *, victory: bool, reason: str):
146        self.phase = "end"
147        # Snapshot stats before we drop the game scene (only it ends the run).
148        game = self.scene
149        rescued = max(0, game.civilians_total - game.casualties - len(game.civilians))
150        es = EndScreen(
151            victory=victory,
152            score=game.score,
153            rescued=rescued,
154            civilians_total=game.civilians_total,
155            casualties=game.casualties,
156            reason=reason,
157        )
158        self._drop_sub()
159        es.restart.connect(self._restart)
160        self.add_child(es)
161        self.scene = es
162
163    # ----------------------------------------------------------- update
164
165    def on_update(self, dt: float):
166        if Input.just_pressed("quit"):
167            if self.app is not None:
168                self.app.quit()
169
170
171# --------------------------------------------------------------------- entry
172
173
174def main():
175    app = App(title="Mr. Rescue", width=WINDOW_W, height=WINDOW_H, bg_colour=C.BG)
176    app.run(MrRescueRoot())
177
178
179if __name__ == "__main__":
180    main()