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¶
simvx examples run ports_heartbeast_rpg # desktop
simvx examples run ports_heartbeast_rpg --test # headless smoke run
Controls¶
Action |
Keyboard |
Controller |
Touch |
|---|---|---|---|
Start |
Space or Enter |
A or Start |
Tap anywhere |
Move |
W/A/S/D or the arrow keys |
Left stick |
The drawn stick |
Attack |
J or Z |
A |
The drawn A |
Roll / dodge |
K or X |
B |
The drawn B |
Quit |
Escape |
Back |
– |
Movement is one typed move action rather than four opposed ones, so the keys,
a controller’s stick and the on-screen stick all feed the same binding and the
one furthest from rest wins. The on-screen pad is a TouchController: it
publishes a real controller, so the Controller bindings above read it with no
extra wiring, and it leaves the screen as soon as a physical controller is
used.
Architecture¶
controls.py The input contexts, each carried by the node that reads it
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 under the 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
└── 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 on-screen pad in screen space above a camera-transformed world.
Input contexts live with the node that reads them: the walking, swinging and rolling bindings arrive with the player, and the title card’s
startleaves when the card does, so nothing has to be registered frommain(), whereInputMaphas no tree yet.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:
rect,circle,polygonandtext, 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¶
simvx export web main.py -o 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. |
116 |
|
What the player can do, declared once and carried by the node that reads it. |
43 |
|
1 |
||
Short-lived visual effects: sword hit, bat death, cut grass. |
92 |
|
Enemy (Bat): Idle/Wander/Chase state machine with soft collision. |
227 |
|
Grass patches: destructible world decoration. |
55 |
|
HUD: heart health bar plus the controls strip along the top edge. |
89 |
|
Player character: 8-direction movement, sword attack, roll/dodge. |
311 |
|
Stats node: signal-based health tracking. |
53 |
|
Title card: the screen the port opens on, and the one way into play. |
62 |
|
World: the playfield, and the collision dispatcher that drives combat. |
339 |
|
Game-wide constants for the HeartBeast Action RPG port. |
91 |
Source¶
1#!/usr/bin/env python3
2# /// simvx
3# tags = ["port", "tier-2"]
4# upstream = "https://github.com/uheartbeast/youtube-tutorials"
5# web = { width = 1280, height = 720, responsive = true }
6# ///
7"""HeartBeast Action RPG: sword-and-roll combat port of the classic Godot tutorial.
8
9A SimVX re-implementation of HeartBeast's Action RPG tutorial (MIT, see
10ATTRIBUTION.md): top-down combat with acceleration/friction movement, a sword
11that swings in the facing direction, a roll that dashes and grants invincibility
12frames, bat enemies driven by an idle/wander/chase state machine, soft collision
13that keeps the bats from stacking, destructible grass, and a heart HUD.
14
15No upstream art is redistributed, so the player, the bats, the grass and the
16terrain are all drawn at runtime with the immediate-mode 2D API. It shows off
17signals carrying health and effect spawns, a Camera2D following the player at
183x zoom with limits pinned to the world edges, a screen-space CanvasLayer
19holding the HUD, and a TouchController that publishes a real pad so the same
20bindings serve keys, a controller and a finger.
21
22Move with WASD or the arrows, J or Z to swing, K or X to roll, ESC to quit. A
23controller steers with the left stick and swings on A, rolls on B. On a touch
24screen the same buttons are drawn on the glass: the stick walks, A swings and
25B rolls, and a tap anywhere starts the game.
26
27Run:
28 simvx examples run ports_heartbeast_rpg
29 simvx examples run ports_heartbeast_rpg --test # headless smoke run
30
31Web export:
32 simvx export web main.py -o heartbeast_rpg.html
33"""
34
35from __future__ import annotations
36
37import argparse
38
39from controls import SHELL # noqa: E402
40from nodes.ui import StartScreen # noqa: E402
41from nodes.world import World # noqa: E402
42from settings import HEIGHT, WIDTH # noqa: E402
43
44from simvx.core import Input, Node2D, Property, UpdateMode # noqa: E402
45from simvx.graphics import App # noqa: E402
46
47
48class GameRoot(Node2D):
49 """Root scene: the world under a title card, until the player starts.
50
51 Pass ``show_menu=False`` to drop straight into play, which is what the
52 headless smoke run wants.
53 """
54
55 # ESC has to keep working while the title card holds the tree paused.
56 # World opts itself back into PAUSABLE so gameplay does not inherit this.
57 update_mode = Property(
58 UpdateMode.ALWAYS,
59 hint="Processing behaviour while the tree is paused",
60 on_change="_invalidate_update_mode_cache",
61 )
62
63 # What is available on every screen. Walking and fighting come with the
64 # player and starting comes with the title card, each on the node that
65 # reads it, so the browser build and a test that loads one scene both have
66 # the bindings without anything calling ``main()``.
67 input_context = SHELL
68
69 def __init__(self, show_menu: bool = True, **kwargs):
70 super().__init__(name="GameRoot", **kwargs)
71 self._show_menu = show_menu
72 self._world: World | None = None
73 self._menu: StartScreen | None = None
74
75 def on_ready(self):
76 self._world = self.add_child(World())
77 if not self._show_menu:
78 return
79 self.tree.paused = True
80 self._world.set_touch_pad_visible(False)
81 self._menu = self.add_child(StartScreen())
82 self._menu.start_requested.connect(self.start_game)
83
84 def start_game(self):
85 """Drop the title card and let the world run."""
86 if self._menu is not None:
87 self._menu.destroy()
88 self._menu = None
89 self._world.set_touch_pad_visible(True)
90 self.tree.paused = False
91
92 def on_update(self, dt: float):
93 if Input.just_pressed("quit"):
94 self.app.quit()
95
96
97def main():
98 parser = argparse.ArgumentParser(description="HeartBeast Action RPG")
99 parser.add_argument("--test", action="store_true", help="Headless smoke run, exit after a few frames.")
100 args = parser.parse_args()
101
102 if args.test:
103 app = App(width=WIDTH, height=HEIGHT, title="HeartBeast RPG (test)", visible=False)
104 frame = app.run_headless(GameRoot(show_menu=False), frames=90, capture_frames=[89])[-1]
105 pixels = frame.reshape(-1, frame.shape[-1])
106 if bool((pixels == pixels[0]).all()):
107 raise SystemExit("blank frame: the world rendered nothing")
108 print(f"OK: rendered 90 frames headlessly, last frame {frame.shape[1]}x{frame.shape[0]}")
109 app.quit()
110 return
111
112 App(width=WIDTH, height=HEIGHT, title="HeartBeast Action RPG").run(GameRoot())
113
114
115if __name__ == "__main__":
116 main()