PirateMaker¶
An in-game level editor and the level it builds, in one process.
▶ Run in browserUpstream: https://github.com/clear-code-projects/PirateMaker
Licence: this port's own code is offered under the terms it inherits from the original, not the SimVX Examples Licence the rest of the gallery carries; its attribution does not reduce them to a single identifier. 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
PirateMaker: SimVX port¶
A side-scrolling pirate platformer with its level editor built into the game. Port of clear-code-projects/PirateMaker (CC0 code, Pixelfrog CC0 art). Paint a level onto an infinite grid, press Play, and run it in the same process: no reload, no export, no separate editor build.
What this shows¶
Node2D composition for both modes, swapped in and out under one root.
One pannable canvas container: every tile and object is a child of a single
Node2D, so panning the view is one transform, not per-sprite bookkeeping.Sprite2Dat native texture size, withdraw_sizefor hit-testing, so nothing in the port has to measure a PNG.CanvasLayerfor screen-pinned UI (title menu, tile palette, controls strip) that ignores the world transform.Camera2Dfollowing the player in play mode.InputMapactions registered once on the root, so they survive mode swaps.AudioPlayerfor looping music per mode plus one-shot coin, hit and jump effects.Signalto keep the editor and the play mode from knowing about the root.JSON save/load of the whole canvas.
Run¶
Run it with the example library installed; the export runs in a copy made by simvx examples copy ports_piratemaker:
# Interactive
simvx examples run ports_piratemaker
# Headless capture (8 frames -> screenshots/)
# Web export
simvx export web main.py -o piratemaker.html
Controls¶
Everything below except the [ / ] shortcut also has an on-screen button, and
every tile is reachable by tapping a palette quadrant, so the port plays with a
mouse or by touch alone.
Editor |
Input |
|---|---|
Paint the selected tile or object |
Left button (drag to keep painting) |
Erase a tile or object |
Right button, or Erase then the left button |
Pick a palette entry |
Left button on a palette quadrant; tap it again to step to the next entry |
Cycle within a group |
Right button on that quadrant |
Swap foreground / background palms |
Middle button on the palm quadrant, or Palm fg/bg |
Step through every tile |
|
Move the pirate or the horizon |
Drag them |
Pan the view |
Middle-button drag, the wheel, or Pan then the left button |
Play the level |
|
Save / load |
|
Quit |
|
Erase and Pan latch what the left button does and stay lit while active; press the same button again to go back to painting. They exist so the editor works on a touch screen, which has no right button, no middle button and no wheel.
Play |
Input |
|---|---|
Move |
Left / Right, |
Jump |
|
Back to the editor |
|
A level needs at least one terrain or water tile before it will play.
Levels¶
levels/level.json is the example level that ships with the port and is what
Load reads until you save your own. Save writes levels/user_level.json,
which is local scratch and is not part of the checkout, so building a level
never dirties version control.
Layout¶
main.py:PirateMakerRootruns the title menu and swaps editor and play modes.settings.py: tile size, window size, colours, theEDITOR_DATAtile table.support.py: folder-of-PNGs animation loaders.nodes/editor.py:EditorMode,EditorObject,Cloud,CanvasTile, palette.nodes/level.py:PlayModebuilds a runnable level from the editor’s grid dict.nodes/player.py: player movement, gravity, AABB collision, animation.nodes/enemies.py: spikes, tooth, shell, pearl.nodes/hud.py: title menu, bottom controls strip, on-screen movement pad.nodes/folder_sprite.py:Sprite2Dthat cycles a folder of frames.nodes/save_load.py: JSON save/load.
Asset credits and per-asset licences are in ATTRIBUTION.md.
Source files¶
File |
Summary |
Lines |
|---|---|---|
PirateMaker: An in-game level editor and the level it builds, in one process. |
139 |
|
Headless |
102 |
|
PirateMaker port: node modules. |
1 |
|
PirateMaker editor mode. |
956 |
|
PirateMaker enemies: Spikes (static), Tooth (patrol), Shell+Pearl. |
150 |
|
Multi-PNG animated sprite: cycles Sprite2D.texture across a folder of frames. |
71 |
|
Screen-space UI: title menu, bottom controls strip, on-screen movement pad. |
328 |
|
PirateMaker play mode. |
305 |
|
PirateMaker Player node: controls, gravity, AABB collision, animation. |
178 |
|
Level JSON save/load: persists editor canvas state. |
145 |
|
PirateMaker port: global constants and editor data table. |
99 |
|
Synthesized sound effects for the PirateMaker port. |
68 |
|
Asset / animation helpers for the PirateMaker port. |
49 |
Source¶
1#!/usr/bin/env python3
2# /// simvx
3# tags = ["port", "tier-2"]
4# upstream = "https://github.com/clear-code-projects/PirateMaker"
5# web = { width = 1280, height = 720, responsive = true }
6# ///
7"""PirateMaker: An in-game level editor and the level it builds, in one process.
8
9A port of Clear Code's PirateMaker. Paint terrain, water, gold, enemies and palm
10trees onto an infinite grid, drag the pirate and the horizon into place, then hit
11Play and run the level you just made: same process, no reload, no export step.
12
13Shows off Node2D scene composition, one pannable canvas container standing in for
14a camera, Sprite2D and CanvasLayer, Camera2D follow, InputMap actions, AudioPlayer
15music and sound effects, and JSON level save/load.
16
17Editor: left button paints, right button erases (and cycles the palette quadrant
18under the cursor), middle button or the wheel pans, `[` and `]` step through
19tiles, S saves, L loads, Enter plays, Q quits.
20Play: Left/Right or A/D to move, Space to jump, Esc back to the editor.
21All of those except the bracket keys have an on-screen button too (tapping a
22palette quadrant steps through its tiles), and the strip's Erase and Pan buttons
23latch what the left button does, so the whole thing works by touch.
24
25Run it with ``simvx examples run ports_piratemaker``, or ``python main.py`` in a
26copy made by ``simvx examples copy ports_piratemaker``.
27"""
28
29from __future__ import annotations
30
31from nodes.editor import EditorMode
32from nodes.hud import MenuScreen
33from nodes.level import PlayMode
34from settings import WINDOW_HEIGHT, WINDOW_WIDTH
35
36from simvx.core import Input, InputMap, Key, Mouse, Node2D
37from simvx.graphics import App
38
39
40class PirateMakerRoot(Node2D):
41 """Root scene: title menu, then the editor and (when playing) the level.
42
43 Mode swap is local: the editor stays as a child, hidden while a level plays.
44 InputMap actions are registered once here so they survive every swap.
45 """
46
47 def on_ready(self) -> None:
48 # Movement (play mode)
49 InputMap.add_action("move_left", [Key.LEFT, Key.A])
50 InputMap.add_action("move_right", [Key.RIGHT, Key.D])
51 InputMap.add_action("jump", [Key.SPACE, Key.UP, Key.W])
52
53 # Editor selection
54 InputMap.add_action("select_prev", [Key.LEFT_BRACKET])
55 InputMap.add_action("select_next", [Key.RIGHT_BRACKET])
56
57 # Mode swap
58 InputMap.add_action("play", [Key.ENTER])
59 InputMap.add_action("editor", [Key.ESCAPE])
60
61 # Painting / panning
62 InputMap.add_action("paint", [Mouse.LEFT])
63 InputMap.add_action("erase", [Mouse.RIGHT])
64 InputMap.add_action("pan", [Mouse.MIDDLE])
65
66 # Save / load slot
67 InputMap.add_action("save_level", [Key.S])
68 InputMap.add_action("load_level", [Key.L])
69
70 # Quit
71 InputMap.add_action("quit_app", [Key.Q])
72
73 self.editor: EditorMode | None = None
74 self.level: PlayMode | None = None
75
76 self.menu: MenuScreen | None = self.add_child(MenuScreen())
77 self.menu.button_pressed.connect(self._on_menu_button)
78
79 def _on_menu_button(self, action: str) -> None:
80 if action == "start":
81 self.open_editor()
82 elif action == "quit":
83 self.quit()
84
85 def quit(self) -> None:
86 self.app.quit()
87
88 def open_editor(self) -> None:
89 """Dismiss the title menu and build the editor."""
90 if self.menu is not None:
91 self.menu.destroy()
92 self.menu = None
93 if self.editor is None:
94 self.editor = self.add_child(EditorMode())
95 self.editor.play_requested.connect(self.start_play)
96 self.editor.quit_requested.connect(self.quit)
97
98 def on_update(self, dt: float) -> None:
99 editing = self.editor is not None and self.editor.visible
100
101 # Editor -> play (Enter)
102 if editing and Input.just_pressed("play"):
103 self.start_play()
104
105 # Play -> editor (Esc)
106 if self.level is not None and Input.just_pressed("editor"):
107 self.return_to_editor()
108
109 # Hard quit (Q from the editor only)
110 if editing and Input.just_pressed("quit_app"):
111 self.quit()
112
113 def start_play(self) -> None:
114 """Build a level dict from the editor canvas and spawn a PlayMode child."""
115 if self.editor is None or self.level is not None:
116 return
117 grid = self.editor.create_grid()
118 if not grid["terrain"] and not grid["water"]:
119 # Nothing to stand on: refuse to play
120 return
121 self.editor.set_active(False)
122 self.level = self.add_child(PlayMode(grid))
123 self.level.editor_requested.connect(self.return_to_editor)
124
125 def return_to_editor(self) -> None:
126 if self.level is not None:
127 self.level.stop_music()
128 self.level.destroy()
129 self.level = None
130 if self.editor is not None:
131 self.editor.set_active(True)
132
133
134def main() -> None:
135 App(width=WINDOW_WIDTH, height=WINDOW_HEIGHT, title="PirateMaker").run(PirateMakerRoot())
136
137
138if __name__ == "__main__":
139 main()