HeartBeast Action RPG

sword-and-roll combat port of the classic Godot tutorial.

▶ Run in browser

Upstream: 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: Stats emits health_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, zoom and the four limit_* 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) call queue_redraw() instead.

  • Procedural 2D drawing throughout: draw_rect, draw_circle, draw_polygon and draw_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

main.py

HeartBeast Action RPG: sword-and-roll combat port of the classic Godot tutorial.

129

nodes/__init__.py

1

nodes/effects.py

Short-lived visual effects: sword hit, bat death, cut grass.

92

nodes/enemy.py

Enemy (Bat): Idle/Wander/Chase state machine with soft collision.

232

nodes/grass.py

Grass patches: destructible world decoration.

56

nodes/hud.py

HUD: heart health bar plus the on-screen controls strip.

85

nodes/player.py

Player character: 8-direction movement, sword attack, roll/dodge.

342

nodes/stats.py

Stats node: signal-based health tracking.

53

nodes/ui.py

Title card and pointer controls, so the port opens on a menu and plays by touch.

170

nodes/world.py

World: the playfield, and the collision dispatcher that drives combat.

341

settings.py

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()