HeartBeast Action RPG¶
The canonical Godot tutorial port, sword + roll + enemies.
▶ Run in browserUpstream: https://github.com/uheartbeast/godot-action-rpg-tutorial
Tags: port tier-2
HeartBeast Action RPG: SimVX Port¶
Tier 2 port #18 of uheartbeast/godot-action-rpg-tutorial.
What it is¶
A faithful port of the canonical HeartBeast Godot Action RPG tutorial to SimVX. Implements the complete combat loop: player movement with acceleration/friction, directional sword attacks, roll/dodge with invincibility frames, enemy AI with Idle→Wander→Chase state machine, soft collision between enemies, destructible grass, and a heart-based HUD.
Running¶
# From /home/fezzik/dev/simvx:
uv run python ../ported_games/heartbeast_rpg/simvx_port/main.py # desktop
uv run python ../ported_games/heartbeast_rpg/simvx_port/main.py --test # headless smoke
uv run python ../ported_games/heartbeast_rpg/simvx_port/capture.py # screenshots
Controls¶
Action |
Keys |
|---|---|
Move |
W/A/S/D or Arrow Keys |
Attack |
J, Z, or Left Mouse |
Roll/Dodge |
K or X |
Quit |
Escape |
Architecture¶
nodes/
├── camera.py Bounded Camera2D with world clamping
├── effects.py HitEffect, EnemyDeathEffect, GrassEffect (auto-destroy)
├── enemy.py Bat enemy with Idle/Wander/Chase FSM + soft collision
├── grass.py Destructible grass patches
├── hud.py Heart-based health bar overlay
├── player.py Player: movement physics, sword, roll, invulnerability
├── stats.py Signal-based health tracker (mirrors upstream Stats.tscn)
└── world.py Main scene: tilemap, entities, collision dispatcher
Feature parity with upstream¶
Feature |
Status |
Notes |
|---|---|---|
Player movement (accel/friction) |
✅ |
Matches upstream speed values |
8-directional movement |
✅ |
Normalised diagonal |
Sword attack with rotation |
✅ |
Hitbox follows movement direction |
Roll/dodge with i-frames |
✅ |
0.3s duration, 250 px/s |
Enemy AI (Idle/Wander/Chase) |
✅ |
3-state FSM with detection radius |
Soft collision |
✅ |
Push-apart between overlapping enemies |
Destructible grass |
✅ |
20 patches, effect on destruction |
Invincibility frames + flash |
✅ |
Blink effect for both player and enemies |
Health system (signals) |
✅ |
Stats node with health_changed/no_health |
Heart-based HUD |
✅ |
6 hearts, updates via signals |
Bounded camera |
✅ |
Clamped to world edges |
Death/respawn |
✅ |
2s delay, full health restore |
Hit/death/grass effects |
✅ |
Particle burst, auto-destroy |
Tree/Bush decorations |
✅ |
Procedural draw |
Audio |
⬜ |
SFX not included (licensing) |
Animation (sprite flip) |
✅ |
Wing flap simulation |
Tilemap terrain |
✅ |
Procedural grass + dirt path |
Web Export¶
cd /home/fezzik/dev/simvx
uv run simvx export web ../ported_games/heartbeast_rpg/simvx_port/main.py \
-o ../ported_games/heartbeast_rpg/simvx_port/web/index.html
Output: web/index.html (~2613 KB), auto-detects GameRoot, no extra flags needed.
License¶
Upstream tutorial code by uheartbeast (Godot co-creator) is CC0/MIT. Assets (HeartBeast pixel art, OpenGameArt music) are NOT bundled; this port uses procedural rendering for all visuals. Audio is stubbed for licensing compliance.
See also¶
NOTES.md: porting friction, engine gaps, BUGS.md cross-refs../PORTS.md: master port list../../simvx/CLAUDE.md: engine rules
Source¶
1#!/usr/bin/env python3
2"""HeartBeast Action RPG: The canonical Godot tutorial port, sword + roll + enemies.
3
4# /// simvx
5# tags = ["port", "tier-2"]
6# upstream = "https://github.com/uheartbeast/godot-action-rpg-tutorial"
7# web = { width = 1280, height = 720, responsive = true }
8# ///
9
10Canonical Godot tutorial port: top-down ARPG with player movement,
11sword combat, roll/dodge, enemy AI with state machines, health system,
12invincibility frames, soft collisions, and grass destruction.
13
14Run:
15 uv run python ported_games/heartbeast_rpg/simvx_port/main.py
16 uv run python ported_games/heartbeast_rpg/simvx_port/main.py --test
17"""
18from __future__ import annotations
19
20import argparse
21import sys
22from pathlib import Path
23
24# Allow flat sibling imports (settings, nodes.*)
25_PORT_DIR = Path(__file__).resolve().parent
26if str(_PORT_DIR) not in sys.path:
27 sys.path.insert(0, str(_PORT_DIR))
28
29from simvx.core import InputMap, Key, MouseButton, Node2D
30from simvx.graphics import App
31
32from nodes.world import World
33from settings import HEIGHT, WIDTH, INPUT_ACTIONS
34
35
36# Key name → Key enum mapping (avoids reflection)
37_KEY_MAP = {
38 "w": "W", "a": "A", "s": "S", "d": "D",
39 "j": "J", "k": "K", "z": "Z", "x": "X",
40 "up": "UP", "down": "DOWN", "left": "LEFT", "right": "RIGHT",
41 "escape": "ESCAPE",
42}
43
44
45class GameRoot(Node2D):
46 """Root scene node: registers input actions then loads the world."""
47
48 def on_ready(self):
49 from simvx.core import Key, MouseButton
50
51 for action, keys in INPUT_ACTIONS.items():
52 bindings = []
53 for k in keys:
54 if k == "mouse_left":
55 bindings.append(MouseButton.LEFT)
56 else:
57 enum_name = _KEY_MAP.get(k.lower(), k.upper())
58 bindings.append(getattr(Key, enum_name))
59 InputMap.add_action(action, bindings)
60
61 self.add_child(World())
62
63 def on_process(self, dt: float):
64 from simvx.core import Input
65 if Input.is_action_just_pressed("escape"):
66 self.app.quit()
67
68
69def main():
70 parser = argparse.ArgumentParser(description="HeartBeast Action RPG")
71 parser.add_argument(
72 "--test", action="store_true",
73 help="Headless smoke run, exit after a few frames."
74 )
75 args = parser.parse_args()
76
77 if args.test:
78 app = App(width=WIDTH, height=HEIGHT, title="HeartBeast RPG (test)", visible=False)
79 frames = app.run_headless(GameRoot(), frames=30)
80 print(f"OK: rendered {len(frames)} frames headlessly")
81 return
82
83 App(width=WIDTH, height=HEIGHT, title="HeartBeast Action RPG").run(GameRoot())
84
85
86if __name__ == "__main__":
87 main()