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

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: 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 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 start leaves when the card does, so nothing has to be registered from main(), where InputMap has no tree yet.

  • 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: rect, circle, polygon and 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

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

main.py

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

116

controls.py

What the player can do, declared once and carried by the node that reads it.

43

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.

227

nodes/grass.py

Grass patches: destructible world decoration.

55

nodes/hud.py

HUD: heart health bar plus the controls strip along the top edge.

89

nodes/player.py

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

311

nodes/stats.py

Stats node: signal-based health tracking.

53

nodes/ui.py

Title card: the screen the port opens on, and the one way into play.

62

nodes/world.py

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

339

settings.py

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