PirateMaker

An in-game level editor and the level it builds, in one process.

▶ Run in browser

Upstream: 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.

  • Sprite2D at native texture size, with draw_size for hit-testing, so nothing in the port has to measure a PNG.

  • CanvasLayer for screen-pinned UI (title menu, tile palette, controls strip) that ignores the world transform.

  • Camera2D following the player in play mode.

  • InputMap actions registered once on the root, so they survive mode swaps.

  • AudioPlayer for looping music per mode plus one-shot coin, hit and jump effects.

  • Signal to 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

[ and ]

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

Enter, or Play level

Save / load

S / L, or Save / Load

Quit

Q, or 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, A / D, or the on-screen pad

Jump

Space, W, Up, or Jump

Back to the editor

Esc, or Back to 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: PirateMakerRoot runs the title menu and swaps editor and play modes.

  • settings.py: tile size, window size, colours, the EDITOR_DATA tile table.

  • support.py: folder-of-PNGs animation loaders.

  • nodes/editor.py: EditorMode, EditorObject, Cloud, CanvasTile, palette.

  • nodes/level.py: PlayMode builds 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: Sprite2D that 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

main.py

PirateMaker: An in-game level editor and the level it builds, in one process.

139

harness.py

Headless --test mode: capture screenshots covering the title menu, an empty

102

nodes/__init__.py

PirateMaker port: node modules.

1

nodes/editor.py

PirateMaker editor mode.

956

nodes/enemies.py

PirateMaker enemies: Spikes (static), Tooth (patrol), Shell+Pearl.

150

nodes/folder_sprite.py

Multi-PNG animated sprite: cycles Sprite2D.texture across a folder of frames.

71

nodes/hud.py

Screen-space UI: title menu, bottom controls strip, on-screen movement pad.

328

nodes/level.py

PirateMaker play mode.

305

nodes/player.py

PirateMaker Player node: controls, gravity, AABB collision, animation.

178

nodes/save_load.py

Level JSON save/load: persists editor canvas state.

145

settings.py

PirateMaker port: global constants and editor data table.

99

sfx.py

Synthesized sound effects for the PirateMaker port.

68

support.py

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()