Clear Code Zelda

Top-down ARPG, y-sort, particles, state machines, upgrade UI.

▶ Run in browser

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

WASD / arrows

Walk (8-direction)

SPACE

Sword attack (melee)

LEFT/RIGHT CTRL

Cast magic (heal / flame)

Q

Cycle weapon (sword/lance/axe/rapier/sai)

E

Cycle spell (heal/flame)

M

Open / close upgrade menu

LEFT / RIGHT

Upgrade selection (in menu)

SPACE / ENTER

Buy upgrade (in menu)

ESC

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 radius

  • Y-sort drawing via YSortContainer so player and enemies depth-sort by y

  • Particle 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.ogg shipped 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()