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¶
# 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
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. |
180 |
|
Scripted headless playthrough that captures one screenshot per stage. |
245 |
|
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. |
206 |
|
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. |
230 |
|
CPU particle pool: water mist, smoke, sparkles, ash. |
135 |
|
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# /// 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()