Clear Code Zelda¶
top-down ARPG with sword combat, magic, enemy AI and stat upgrades.
▶ Run in browserUpstream: https://github.com/clear-code-projects/Zelda
Licence: this port's own code is offered under CC0-1.0, 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-1
Clear Code Zelda: SimVX port¶
SimVX port of Clear Code’s “Zelda” Pygame ARPG.
Top-down ARPG with sword combat, magic, enemy AI, particles, y-sort, and a stat-upgrade screen. Source assets are CC0 (Ninja Adventure pack) and reused unchanged.
Run¶
# the export runs in a copy made by `simvx examples copy ports_clear_code_zelda`
simvx examples run ports_clear_code_zelda # interactive
simvx examples run ports_clear_code_zelda --test # headless smoke run (12 frames)
simvx export web main.py \
-o clear_code_zelda.html
Controls¶
The game opens on a title screen; SPACE, ENTER or a click starts it. Everything is playable from the keyboard or entirely with a pointer (mouse or touch).
Key |
Action |
|---|---|
|
Walk (8-direction) |
|
Sword attack (melee) |
|
Cast magic (heal / flame) |
|
Cycle weapon (sword/lance/axe/rapier/sai) |
|
Cycle spell (heal/flame) |
|
Open / close upgrade menu |
|
Upgrade selection (in menu) |
|
Buy upgrade (in menu) |
|
Quit |
With a pointer, drag the stick in the lower-left corner to walk and tap the buttons
in the lower-right: ATK swings, MAG casts, Q and E swap weapon and spell, M
opens the upgrade screen. In the upgrade screen, a click selects a panel and a second
click on the selected panel buys it.
What works¶
57x50 tile map loaded from upstream CSV layouts (boundary, grass, objects, entities)
Player movement with 12-direction animation set (walk/idle/attack × 4 facings)
Sword attack with directional weapon sprite + cooldown
5 weapon types with different damage/cooldown values, swappable on-the-fly
2 spells (heal restores HP, flame is a projectile)
4 enemy types (squid, raccoon, spirit, bamboo) with idle/move/attack states
Enemy AI: notice radius → chase via
NavGrid2D→ attack radiusY-sort drawing via
YSortContainerso player and enemies depth-sort by yParticle effects: leaves on grass slash, magic sparkles on heal/flame, monster-specific death puffs
Hit feedback: the sprite flashes on damage and enemies are knocked back
Soundtrack plus sword, hit, heal, flame, monster-attack and death SFX through
AudioPlayerHUD: HP bar, energy bar, kills counter, XP counter, weapon/magic indicators, help strip
Upgrade screen: 5 panels (health/energy/attack/magic/speed) with selection, cost scales 1.4x per buy
AABB collision against invisible boundary tiles + objects (grass is walkable but slashable)
Web export
How it is put together¶
PlayerandEnemypublishSignals (attack_started,magic_cast,damaged,attacked,died) andLevelconnects to them, so neither side has to know about the other’s internals.Current values live on
Propertyfields (hp,max_hp,speed, …); the parallelstatsdict is the table the upgrade screen iterates over, andPlayer.upgrade_statis the only place that writes both.The upgrade screen pauses the tree (
tree.paused), and the HUDCanvasLayerruns withUpdateMode.ALWAYSso the overlay can still close itself.
What’s intentionally simplified¶
No procedural floor texture. Upstream uses a 3648×3200 ground.png (~180 KB). The port draws a flat green rect under the world to keep the web bundle small and avoid the texture-cache for one giant repeating image. Visually matches at a glance.
Flame projectile is a single trail of looping puffs, not the upstream’s chained particle anim with sparkle.
Sound is non-positional. The camera is faked by translating the y-sort container rather than with a
Camera2D, so there is no 2D listener to pan against; every effect plays through a plainAudioPlayeron the SFX bus, with the soundtrack on the Music bus.Camera is unsmoothed. Player stays exactly centred; the upstream has no smoothing either.
“Death” wraps to full-heal, not a Game Over screen; the upstream has the same loop.
Source files¶
File |
Summary |
Lines |
|---|---|---|
Clear Code Zelda: top-down ARPG with sword combat, magic, enemy AI and stat upgrades. |
101 |
|
Capture headless screenshots at frames 30, 60, 120 for review. |
36 |
|
What the player can do, declared once and carried by the nodes that read it. |
52 |
|
Scripted input harness for the Zelda port: 15 stages, one screenshot each. |
94 |
|
0 |
||
Multi-PNG animated sprite: swaps Sprite2D.texture between a list of files. |
71 |
|
Enemy with idle/move/attack state machine + simple distance-based pathing. |
231 |
|
Level: the actual gameplay scene. |
477 |
|
Heal and flame spells: wrappers that spawn particle effects + damage hits. |
79 |
|
Title screen: shown before the level so the game opens on a menu. |
74 |
|
Particle effect node: plays a folder of frames once and self-destructs. |
86 |
|
Player character: 8-direction movement, sword attack, magic, weapon/spell switch. |
324 |
|
Player HUD: health bar, energy bar, EXP counter, weapon/magic boxes. |
91 |
|
Upgrade screen: five vertical attribute panels with a selection cursor. |
172 |
|
Sword/weapon strike sprite: spawned in front of the player on attack. |
49 |
|
Game-wide constants for the Clear Code Zelda port. |
129 |
|
Asset loading helpers: counterparts to the upstream |
24 |
Source¶
1#!/usr/bin/env python3
2# /// simvx
3# tags = ["port", "tier-1"]
4# upstream = "https://github.com/clear-code-projects/Zelda"
5# web = { width = 1280, height = 720, responsive = true }
6# ///
7"""Clear Code Zelda: top-down ARPG with sword combat, magic, enemy AI and stat upgrades.
8
9A SimVX re-implementation of Clear Code's Pygame "Zelda" ARPG tutorial. CSV tile
10layouts build a 57x50 map, a YSortContainer depth-sorts the player, enemies and
11props, NavGrid2D drives enemy chase pathfinding around walls, and the HUD,
12title screen and upgrade overlay draw in screen space on a CanvasLayer.
13
14Walk with WASD or the arrows, SPACE to swing, CTRL to cast, Q/E to swap weapon
15and spell, M for the upgrade screen, ESC to quit. On a controller the left
16stick walks, A swings, B casts, the shoulders swap weapon and spell, and START
17opens the upgrade screen. On a touch screen the same pad is drawn on the glass,
18and every screen can also be left with a tap.
19
20Run:
21 simvx examples run ports_clear_code_zelda
22 simvx examples run ports_clear_code_zelda --test # headless smoke run
23"""
24
25from __future__ import annotations
26
27import argparse
28
29from controls import SHELL # noqa: E402
30from nodes.level import Level # noqa: E402
31from nodes.menu import StartScreen # noqa: E402
32from settings import HEIGHT, WIDTH # noqa: E402
33
34from simvx.core import CanvasLayer, Input, Node2D, Property, UpdateMode # noqa: E402
35from simvx.graphics import App # noqa: E402
36
37
38class ZeldaRoot(Node2D):
39 """Root scene node: declares the shell actions, then shows the title screen.
40
41 Pass ``show_menu=False`` to drop straight into the level, which is what the
42 headless smoke run and the capture scripts want.
43 """
44
45 #: Registered by the tree whenever this scene becomes the root, so a web
46 #: export and a test that loads the scene both have them.
47 input_context = SHELL
48
49 # ESC must still quit while the upgrade screen has the tree paused. The
50 # level sets itself back to PAUSABLE so gameplay does not inherit this.
51 update_mode = Property(
52 UpdateMode.ALWAYS,
53 hint="Processing behaviour while the tree is paused",
54 on_change="_invalidate_update_mode_cache",
55 )
56
57 def __init__(self, show_menu: bool = True, **kwargs):
58 super().__init__(**kwargs)
59 self._show_menu = show_menu
60 self._menu_layer: CanvasLayer | None = None
61 self._menu: StartScreen | None = None
62
63 def on_ready(self):
64 if self._show_menu:
65 self._menu_layer = self.add_child(CanvasLayer(name="MenuLayer"))
66 self._menu_layer.layer = 200
67 self._menu = self._menu_layer.add_child(StartScreen())
68 self._menu.start_requested.connect(self.start_game)
69 else:
70 self.start_game()
71
72 def start_game(self):
73 """Drop the title screen (if any) and load the level."""
74 if self._menu_layer is not None:
75 self._menu_layer.destroy()
76 self._menu_layer = None
77 self._menu = None
78 self.add_child(Level())
79
80 def on_update(self, dt: float):
81 if Input.just_pressed("quit"):
82 self.app.quit()
83
84
85def main():
86 parser = argparse.ArgumentParser()
87 parser.add_argument("--test", action="store_true", help="Headless smoke run, exit after a few frames.")
88 args = parser.parse_args()
89
90 if args.test:
91 # Headless: render a few frames of actual gameplay then exit.
92 app = App(width=WIDTH, height=HEIGHT, title="Clear Code Zelda (test)", visible=False)
93 frames = app.run_headless(ZeldaRoot(show_menu=False), frames=12, capture_frames=[11])
94 print(f"OK: rendered 12 frames, last one {frames[0].shape}")
95 return
96
97 App(width=WIDTH, height=HEIGHT, title="Clear Code Zelda").run(ZeldaRoot())
98
99
100if __name__ == "__main__":
101 main()