Clear Code Zelda¶
Top-down ARPG, y-sort, particles, state machines, upgrade UI.
▶ Run in browserUpstream: https://github.com/clear-code-projects/Zelda
Tags: port tier-1
Clear Code Zelda: SimVX port¶
SimVX port of Clear Code’s “Zelda” Pygame ARPG (Tier 1 Port #8).
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¶
From /home/fezzik/dev/simvx:
# Windowed
uv run python ../ported_games/clear_code_zelda/simvx_port/main.py
# Headless smoke test (renders 12 frames then exits)
uv run python ../ported_games/clear_code_zelda/simvx_port/main.py --test
# Headless screenshots (frame 30/60/120)
uv run python ../ported_games/clear_code_zelda/simvx_port/capture.py
# Full scripted harness: 13 scripted stages, one screenshot each
uv run python ../ported_games/clear_code_zelda/simvx_port/harness.py
# Web export
uv run simvx export web ../ported_games/clear_code_zelda/simvx_port/main.py \
-o ../ported_games/clear_code_zelda/simvx_port/web/index.html
Controls¶
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 |
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
AStarGrid2D→ 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
HUD: HP bar, energy bar, kills counter, XP counter, weapon/magic indicators, help strip
Upgrade screen: 5 panels (health/energy/attack/magic/speed) with selection, buy via SPACE, cost scales 1.4x per buy
AABB collision against invisible boundary tiles + objects (grass is walkable but slashable)
Web export (~2.6 MB single-file HTML)
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.
No background music. Upstream’s
main.oggshipped but is not auto-played here (web autoplay policy +feedback_app_quit_pattern).Camera is unsmoothed. Player stays exactly centred; upstream’s port has no smoothing either.
“Death” wraps to full-heal, not a Game Over screen; the upstream has the same loop.
Source¶
1#!/usr/bin/env python3
2"""Clear Code Zelda: Top-down ARPG, y-sort, particles, state machines, upgrade UI.
3
4# /// simvx
5# tags = ["port", "tier-1"]
6# upstream = "https://github.com/clear-code-projects/Zelda"
7# web = { width = 1280, height = 720, responsive = true }
8# ///
9
10Top-down ARPG: walk a map, chop grass, slay enemies, cast spells,
11spend EXP on stat upgrades.
12
13Run:
14 uv run python ported_games/clear_code_zelda/simvx_port/main.py
15 uv run python ported_games/clear_code_zelda/simvx_port/main.py --test
16"""
17from __future__ import annotations
18
19import argparse
20import sys
21from pathlib import Path
22
23# Allow flat sibling imports (settings, support, nodes.*)
24_PORT_DIR = Path(__file__).resolve().parent
25if str(_PORT_DIR) not in sys.path:
26 sys.path.insert(0, str(_PORT_DIR))
27
28from simvx.core import InputMap, Key, MouseButton, Node2D
29from simvx.graphics import App
30
31from nodes.level import Level
32from settings import HEIGHT, WIDTH
33
34
35class ZeldaRoot(Node2D):
36 """Root scene node: registers actions then loads the level."""
37
38 def on_ready(self):
39 InputMap.add_action("move_up", [Key.W, Key.UP])
40 InputMap.add_action("move_down", [Key.S, Key.DOWN])
41 InputMap.add_action("move_left", [Key.A, Key.LEFT])
42 InputMap.add_action("move_right", [Key.D, Key.RIGHT])
43 InputMap.add_action("attack", [Key.SPACE, MouseButton.LEFT])
44 InputMap.add_action("magic", [Key.LEFT_CONTROL, Key.RIGHT_CONTROL])
45 InputMap.add_action("weapon_swap", [Key.Q])
46 InputMap.add_action("magic_swap", [Key.E])
47 InputMap.add_action("upgrade_menu",[Key.M])
48 InputMap.add_action("ui_left", [Key.LEFT, Key.A])
49 InputMap.add_action("ui_right", [Key.RIGHT, Key.D])
50 InputMap.add_action("ui_select", [Key.SPACE, Key.ENTER])
51 InputMap.add_action("quit", [Key.ESCAPE])
52
53 self.add_child(Level())
54
55 def on_process(self, dt: float):
56 from simvx.core import Input
57 if Input.is_action_just_pressed("quit"):
58 self.app.quit()
59
60
61def main():
62 parser = argparse.ArgumentParser()
63 parser.add_argument("--test", action="store_true", help="Headless smoke run, exit after a few frames.")
64 args = parser.parse_args()
65
66 if args.test:
67 # Headless: render a few frames then exit. Useful for CI smoke.
68 app = App(width=WIDTH, height=HEIGHT, title="Clear Code Zelda (test)", visible=False)
69 frames = app.run_headless(ZeldaRoot(), frames=12)
70 print(f"OK: rendered {len(frames)} frames")
71 return
72
73 App(width=WIDTH, height=HEIGHT, title="Clear Code Zelda").run(ZeldaRoot())
74
75
76if __name__ == "__main__":
77 main()