HeartBeast Action RPG¶
sword-and-roll combat port of the classic Godot tutorial.
▶ Run in browserUpstream: https://github.com/uheartbeast/youtube-tutorials
Licence: this port's own code is offered under MIT, 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-2
HeartBeast Action RPG: SimVX Port¶
A SimVX re-implementation of HeartBeast’s Action RPG tutorial, whose canonical
source is the Action RPG folder of
uheartbeast/youtube-tutorials.
What it is¶
The complete combat loop of the tutorial: movement with acceleration and friction, a sword that swings in the facing direction, a roll that dashes and grants invincibility frames, bats driven by an Idle to Wander to Chase state machine, soft collision so bats push each other apart instead of stacking, destructible grass, and a heart HUD.
No upstream art is redistributed (see ATTRIBUTION.md), so every visual is
drawn at runtime with the immediate-mode 2D API.
Running¶
uv run python examples/ports/heartbeast_rpg/main.py # desktop
uv run python examples/ports/heartbeast_rpg/main.py --test # headless smoke run
Controls¶
Action |
Keyboard |
Pointer / touch |
|---|---|---|
Start |
Space or Enter |
Tap anywhere |
Move |
W/A/S/D or the arrow keys |
Drag from the lower-left half |
Attack |
J or Z |
Tap ATK |
Roll / dodge |
K or X |
Tap ROLL |
Quit |
Escape |
– |
Architecture¶
nodes/
├── effects.py HitEffect, EnemyDeathEffect, GrassEffect (self-destructing)
├── enemy.py Bat: Idle/Wander/Chase state machine plus soft collision
├── grass.py Destructible grass tufts
├── hud.py Heart health bar and the bottom controls strip
├── player.py Movement physics, sword, roll, invincibility frames
├── stats.py Signal-based health tracker (mirrors the upstream Stats scene)
├── ui.py Title card and the on-screen stick / action buttons
└── world.py Playfield, camera, decorations, collision dispatcher
What it shows off in SimVX¶
Signals carry health changes and effect spawns:
Statsemitshealth_changed/no_health, the bats emit hit and death signals that the world turns into particle bursts, and the HUD only ever listens.Camera2D does the following and the clamping: the port sets
target,zoomand the fourlimit_*properties from the window size, rather than reimplementing them.CanvasLayer keeps the HUD, the title card and the pointer controls in screen space above a camera-transformed world.
The retained 2D contract: nodes that animate every frame declare
dynamic, and nodes that change on an event (the hearts) callqueue_redraw()instead.Procedural 2D drawing throughout:
draw_rect,draw_circle,draw_polygonanddraw_text, with no textures at all.
Feature parity with upstream¶
Feature |
Status |
Notes |
|---|---|---|
Player movement (accel/friction) |
Yes |
Matches the upstream speed values |
8-directional movement |
Yes |
Normalised diagonals |
Sword attack with rotation |
Yes |
Hitbox swings along the facing |
Roll/dodge with i-frames |
Yes |
0.3s, 250 px/s dash |
Enemy AI (Idle/Wander/Chase) |
Yes |
Detection radius of 120 px |
Soft collision |
Yes |
Push-apart between overlapping bats |
Destructible grass |
Yes |
80 tufts, leaf burst on destruction |
Invincibility frames plus blink |
Yes |
Player and bats both blink |
Health system (signals) |
Yes |
Stats node with health_changed / no_health |
Heart HUD |
Yes |
6 hearts, updated from signals |
Bounded camera |
Yes |
Camera2D target plus limits, clamped to the map |
Death/respawn |
Yes |
2s delay, then full health |
Hit/death/grass effects |
Yes |
Fading particle bursts that self-destruct |
Tree/bush decorations |
Yes |
Drawn procedurally |
Terrain |
Partial |
A drawn ground rect and a polygon path, not a TileMap |
Sprite animation |
Partial |
Wing flap is procedural, no sprite sheets |
Audio |
No |
Upstream SFX are not redistributable |
Web export¶
uv run simvx export web examples/ports/heartbeast_rpg/main.py -o /tmp/heartbeast_rpg.html
The exporter auto-detects GameRoot; no extra flags are needed.
Licence¶
The upstream tutorial code is MIT (Copyright (c) 2020 Heart GameDev); the
notice is preserved in UPSTREAM_LICENSE.md. The upstream pixel art and audio
are not covered by that licence and are not bundled here. See
ATTRIBUTION.md for the full provenance.
Source files¶
File |
Summary |
Lines |
|---|---|---|
HeartBeast Action RPG: sword-and-roll combat port of the classic Godot tutorial. |
129 |
|
1 |
||
Short-lived visual effects: sword hit, bat death, cut grass. |
92 |
|
Enemy (Bat): Idle/Wander/Chase state machine with soft collision. |
232 |
|
Grass patches: destructible world decoration. |
56 |
|
HUD: heart health bar plus the on-screen controls strip. |
85 |
|
Player character: 8-direction movement, sword attack, roll/dodge. |
342 |
|
Stats node: signal-based health tracking. |
53 |
|
Title card and pointer controls, so the port opens on a menu and plays by touch. |
170 |
|
World: the playfield, and the collision dispatcher that drives combat. |
341 |
|
Game-wide constants for the HeartBeast Action RPG port. |
91 |
Source¶
1#!/usr/bin/env python3
2"""HeartBeast Action RPG: sword-and-roll combat port of the classic Godot tutorial.
3
4# /// simvx
5# tags = ["port", "tier-2"]
6# upstream = "https://github.com/uheartbeast/youtube-tutorials"
7# web = { width = 1280, height = 720, responsive = true }
8# ///
9
10A SimVX re-implementation of HeartBeast's Action RPG tutorial (MIT, see
11ATTRIBUTION.md): top-down combat with acceleration/friction movement, a sword
12that swings in the facing direction, a roll that dashes and grants invincibility
13frames, bat enemies driven by an idle/wander/chase state machine, soft collision
14that keeps the bats from stacking, destructible grass, and a heart HUD.
15
16No upstream art is redistributed, so the player, the bats, the grass and the
17terrain are all drawn at runtime with the immediate-mode 2D API. It shows off
18signals carrying health and effect spawns, a Camera2D following the player at
193x zoom with limits pinned to the world edges, and a screen-space CanvasLayer
20holding the HUD and the pointer controls.
21
22Move with WASD or the arrows, J or Z to swing, K or X to roll, ESC to quit. On
23a touch screen or with the mouse, drag anywhere in the lower left to walk and
24tap ATK / ROLL on the right.
25
26Run:
27 uv run python examples/ports/heartbeast_rpg/main.py
28 uv run python examples/ports/heartbeast_rpg/main.py --test # headless smoke run
29
30Web export:
31 uv run simvx export web examples/ports/heartbeast_rpg/main.py -o /tmp/heartbeast_rpg.html
32"""
33
34from __future__ import annotations
35
36import argparse
37import sys
38from pathlib import Path
39
40# Allow flat sibling imports (settings, nodes.*)
41_PORT_DIR = Path(__file__).resolve().parent
42if str(_PORT_DIR) not in sys.path:
43 sys.path.insert(0, str(_PORT_DIR))
44
45from nodes.ui import StartScreen # noqa: E402
46from nodes.world import World # noqa: E402
47from settings import HEIGHT, WIDTH # noqa: E402
48
49from simvx.core import Input, Key, Node2D, Property, UpdateMode # noqa: E402
50from simvx.graphics import App # noqa: E402
51
52
53class GameRoot(Node2D):
54 """Root scene: the world under a title card, until the player starts.
55
56 Pass ``show_menu=False`` to drop straight into play, which is what the
57 headless smoke run wants.
58 """
59
60 # ESC has to keep working while the title card holds the tree paused.
61 # World opts itself back into PAUSABLE so gameplay does not inherit this.
62 update_mode = Property(
63 UpdateMode.ALWAYS,
64 hint="Processing behaviour while the tree is paused",
65 on_change="_invalidate_update_mode_cache",
66 )
67
68 # The declarative table the scene tree registers at mount, and re-applies on
69 # every scene swap. The web export never runs ``main()``, so registering
70 # here rather than there is what keeps the browser build playable.
71 input_actions = {
72 "move_up": [Key.W, Key.UP],
73 "move_down": [Key.S, Key.DOWN],
74 "move_left": [Key.A, Key.LEFT],
75 "move_right": [Key.D, Key.RIGHT],
76 "attack": [Key.J, Key.Z],
77 "roll": [Key.K, Key.X],
78 "start": [Key.SPACE, Key.ENTER],
79 "quit": [Key.ESCAPE],
80 }
81
82 def __init__(self, show_menu: bool = True, **kwargs):
83 super().__init__(name="GameRoot", **kwargs)
84 self._show_menu = show_menu
85 self._world: World | None = None
86 self._menu: StartScreen | None = None
87
88 def on_ready(self):
89 self._world = self.add_child(World())
90 if not self._show_menu:
91 return
92 self.tree.paused = True
93 self._world.set_pointer_controls_visible(False)
94 self._menu = self.add_child(StartScreen())
95 self._menu.start_requested.connect(self.start_game)
96
97 def start_game(self):
98 """Drop the title card and let the world run."""
99 if self._menu is not None:
100 self._menu.destroy()
101 self._menu = None
102 self._world.set_pointer_controls_visible(True)
103 self.tree.paused = False
104
105 def on_update(self, dt: float):
106 if Input.is_action_just_pressed("quit"):
107 self.app.quit()
108
109
110def main():
111 parser = argparse.ArgumentParser(description="HeartBeast Action RPG")
112 parser.add_argument("--test", action="store_true", help="Headless smoke run, exit after a few frames.")
113 args = parser.parse_args()
114
115 if args.test:
116 app = App(width=WIDTH, height=HEIGHT, title="HeartBeast RPG (test)", visible=False)
117 frame = app.run_headless(GameRoot(show_menu=False), frames=90, capture_frames=[89])[-1]
118 pixels = frame.reshape(-1, frame.shape[-1])
119 if bool((pixels == pixels[0]).all()):
120 raise SystemExit("blank frame: the world rendered nothing")
121 print(f"OK: rendered 90 frames headlessly, last frame {frame.shape[1]}x{frame.shape[0]}")
122 app.quit()
123 return
124
125 App(width=WIDTH, height=HEIGHT, title="HeartBeast Action RPG").run(GameRoot())
126
127
128if __name__ == "__main__":
129 main()