Mr. Rescue¶
Arcade firefighting in a procedurally generated burning building.
▶ Run in browserUpstream: 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¶
# from the repo root
uv run python examples/ports/mr_rescue/main.py # interactive
uv run python examples/ports/mr_rescue/main.py --test # headless capture (8 stages)
uv run simvx export web examples/ports/mr_rescue/main.py \
-o /tmp/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
uint8RGBA; the SFX and the ambient music bed are synthesized withAudioSynth. 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
CanvasLayerand 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 |
|---|---|---|
Mr. Rescue: Arcade firefighting in a procedurally generated burning building. |
213 |
|
Scripted headless playthrough: the implementation behind |
233 |
|
0 |
||
Procedural audio for Mr. Rescue: synthesized SFX + a looping ambient bed. |
213 |
|
Procedural multi-floor building generator. |
328 |
|
Civilian: IDLE/WALK/PANIC/BURN/CARRIED/FLY state machine. |
204 |
|
Mr. Rescue palette: warm-on-charcoal arcade pixels. |
94 |
|
FireBug: single enemy type (port collapses upstream’s 6 to one). |
149 |
|
Fire grid: sparse cell map of flames with health and spread timers. |
221 |
|
Active gameplay scene: wires together grid, fires, player, civilians, enemies. |
467 |
|
HUD: top status strip + bottom water/heat/casualty bar. |
198 |
|
Title and end screens. |
233 |
|
CPU particle pool: water mist, smoke, sparkles, ash. |
141 |
|
Firefighter player: state machine + water gun raycast. |
693 |
|
Procedural NumPy textures. |
593 |
|
Tile grid + collision for the building. |
188 |
Source¶
1#!/usr/bin/env python3
2"""Mr. Rescue: Arcade firefighting in a procedurally generated burning building.
3
4A port of Simon Larsen's "Mr. Rescue": spray water at flames that spread cell to
5cell, carry civilians out through the roof, and get clear before your suit
6overheats. Everything is generated at runtime: NumPy pixel-art textures,
7synthesized SFX and music, and a randomised three-storey building. Shows
8tile-grid platforming with coyote time and jump buffering, Camera2D follow with
9screenshake and hitstop, a CanvasLayer HUD, signal-driven scene flow
10(menu, game, end), and bloom via WorldEnvironment.
11
12# /// simvx
13# tags = ["port", "tier-2"]
14# upstream = "https://github.com/SimonLarsen/mrrescue"
15# web = { width = 1024, height = 800, responsive = true }
16# ///
17
18Run interactively::
19
20 uv run python examples/ports/mr_rescue/main.py
21
22Headless smoke test (captures stage screenshots into ``screenshots/``)::
23
24 uv run python examples/ports/mr_rescue/main.py --test
25
26Web export::
27
28 uv run simvx export web examples/ports/mr_rescue/main.py -o /tmp/mr_rescue.html
29
30Controls
31--------
32- Arrow keys / WASD: move, aim the gun, climb ladders
33- Space: jump
34- Shift: spray water
35- E: grab / throw the carried civilian
36- Enter, Space or click / tap: confirm in menus
37- Esc: quit
38"""
39
40from __future__ import annotations
41
42import sys
43from pathlib import Path
44
45# Make the port folder importable in --test and direct runs alike.
46_PORT_DIR = Path(__file__).resolve().parent
47if str(_PORT_DIR) not in sys.path:
48 sys.path.insert(0, str(_PORT_DIR))
49
50from nodes import colours as C
51from nodes.audio import get_sfx
52from nodes.game import GameScene
53from nodes.menu import EndScreen, TitleScreen
54
55from simvx.core import (
56 AudioPlayer,
57 Input,
58 InputMap,
59 Key,
60 MouseButton,
61 Node,
62)
63from simvx.core.world_environment import WorldEnvironment
64from simvx.graphics import App
65
66WINDOW_W = 1024
67WINDOW_H = 800
68
69
70# ---------------------------------------------------------------------------- root
71
72
73class MrRescueRoot(Node):
74 """Phase machine: menu → game → end."""
75
76 def __init__(self, *, seed: int | None = None, **kwargs):
77 super().__init__(**kwargs)
78 self.phase = "menu"
79 self.section = 1
80 self._seed = seed
81 self.scene: Node | None = None
82 self._menu_sfx = None
83
84 def on_ready(self):
85 # InputMap MUST live in on_ready (web exporter skips main()).
86 InputMap.add_action("left", [Key.LEFT, Key.A])
87 InputMap.add_action("right", [Key.RIGHT, Key.D])
88 InputMap.add_action("up", [Key.UP, Key.W])
89 InputMap.add_action("down", [Key.DOWN, Key.S])
90 InputMap.add_action("jump", [Key.SPACE])
91 InputMap.add_action("shoot", [Key.LEFT_SHIFT, Key.RIGHT_SHIFT])
92 InputMap.add_action("grab", [Key.E])
93 # Menus confirm on a click/tap too (touch arrives as MouseButton.LEFT on
94 # web), so the responsive export is reachable without a keyboard.
95 InputMap.add_action("start", [Key.ENTER, Key.SPACE, MouseButton.LEFT])
96 InputMap.add_action("quit", [Key.ESCAPE])
97
98 env = self.add_child(WorldEnvironment())
99 env.sky_mode = "disabled"
100 env.tonemap_mode = "aces"
101 env.tonemap_exposure = 1.05
102 env.bloom_enabled = True
103 env.bloom_threshold = 0.78
104 env.bloom_intensity = 0.52
105 env.bloom_soft_knee = 0.5
106
107 # One-shot menu confirm cue (gameplay SFX live on the GameScene's bank).
108 self._menu_sfx = self.add_child(AudioPlayer(stream=get_sfx("menu_confirm"), bus="SFX"))
109
110 # The HUD is owned by the GameScene (in its own CanvasLayer) so it is
111 # created and destroyed with the game phase: clean lifecycle, with no HUD
112 # state to reset between phases.
113 self._enter_menu()
114
115 # ----------------------------------------------------------- phase swaps
116
117 def _drop_sub(self):
118 # Destroying the sub-scene takes its Camera2D with it, and Camera2D's
119 # teardown releases the tree's active-camera slot, so the menu phases
120 # come back up in screen space with no extra bookkeeping here.
121 if self.scene is not None:
122 self.scene.destroy()
123 self.scene = None
124
125 def _enter_menu(self):
126 self.phase = "menu"
127 self._drop_sub()
128 self.section = 1
129 ts = TitleScreen()
130 ts.start.connect(self._enter_game)
131 self.add_child(ts)
132 self.scene = ts
133
134 def _play_menu_sfx(self):
135 if self._menu_sfx is not None:
136 self._menu_sfx.play()
137
138 def _restart(self):
139 self._play_menu_sfx()
140 self._enter_menu()
141
142 def _enter_game(self):
143 self.phase = "game"
144 self._play_menu_sfx()
145 self._drop_sub()
146 game = GameScene(
147 viewport_w=WINDOW_W,
148 viewport_h=WINDOW_H,
149 section=self.section,
150 level=1,
151 seed=self._seed,
152 )
153 game.victory.connect(self._on_victory)
154 game.failure.connect(self._on_failure)
155 self.add_child(game)
156 self.scene = game
157
158 def _on_victory(self):
159 self._enter_end(victory=True, reason="ALL CIVILIANS RESCUED")
160
161 def _on_failure(self, reason: str):
162 msg = {
163 "casualty": "TOO MANY CIVILIANS LOST",
164 "overheat": "YOUR SUIT OVERHEATED",
165 }.get(reason, "")
166 self._enter_end(victory=False, reason=msg)
167
168 def _enter_end(self, *, victory: bool, reason: str):
169 self.phase = "end"
170 # Snapshot stats before we drop the game scene (only it ends the run).
171 game = self.scene
172 rescued = max(0, game.civilians_total - game.casualties - len(game.civilians))
173 es = EndScreen(
174 victory=victory,
175 score=game.score,
176 rescued=rescued,
177 civilians_total=game.civilians_total,
178 casualties=game.casualties,
179 reason=reason,
180 )
181 self._drop_sub()
182 es.restart.connect(self._restart)
183 self.add_child(es)
184 self.scene = es
185
186 # ----------------------------------------------------------- update
187
188 def on_update(self, dt: float):
189 if Input.is_action_just_pressed("quit"):
190 if self.app is not None:
191 self.app.quit()
192
193
194# --------------------------------------------------------------------- entry
195
196
197def main():
198 if "--test" in sys.argv:
199 from harness import capture_playthrough
200
201 capture_playthrough(
202 MrRescueRoot,
203 window_size=(WINDOW_W, WINDOW_H),
204 bg_colour=C.BG,
205 out_dir=_PORT_DIR / "screenshots",
206 )
207 return
208 app = App(title="Mr. Rescue", width=WINDOW_W, height=WINDOW_H, bg_colour=C.BG)
209 app.run(MrRescueRoot())
210
211
212if __name__ == "__main__":
213 main()