# GDQuest Open RPG Overworld + turn-based combat, party of three vs enemies. ```{raw} html ▶ Run in browser

Upstream: https://github.com/gdquest-demos/godot-open-rpg

``` **Tags:** `port` `tier-2` # GDQuest Open RPG: SimVX port Port of [gdquest-demos/godot-open-rpg](https://github.com/gdquest-demos/godot-open-rpg) (a small Godot 4 turn-based RPG demo) to SimVX. Tier 2 Port #17. ## Scope **This port ships a single 30x17 overworld map. That is deliberate; do not "fix" it by adding more maps unless explicitly asked.** Upstream ships three maps (forest / house / town) wired together by `area_transition` and `door` interaction templates, with a Dialogic-driven opening cutscene, multi-area save state, and inventory/pedestal puzzles. The Tier 2 port intentionally compresses all of that into one hand-authored map that covers every gameplay system end-to-end: - WASD + click-to-path traversal on a Python-authored tile grid. - 3 NPCs (Monk / Smith / Old Mage) with flat dialogue trees. - 3 encounter triggers (wolves / bears / bugcats) feeding the same battle scene with different enemy specs. - 1 save shrine writing `saves/open_rpg.json` (cell + party HP). - Fade-to-black overworld <-> battle transition. The slice was chosen so a player can exercise the full Field -> Encounter -> Battle -> Resolution loop within ~2 minutes without re-authoring the upstream's Dialogic timelines or Tiled maps. Multi-map work was not estimated to fit Tier 2 scope; see `../NOTES.md` "Deliberate deviations" and "Engine gaps" for the full deviation list. If you want to extend the port to multiple maps, expect to: 1. Replace module-level `TILES`/`TRIGGERS`/`NPCS` in `nodes/gameboard.py` with a `Map` dataclass keyed by name, and refactor `Overworld` + `Player` to take the active map by reference. 2. Add a `door_trigger` cell kind that calls `_change_map(name, cell)` on `RPGRoot`, paralleling the existing `encounter` and `save` trigger kinds. 3. Extend `SaveStore` to record the active map name alongside the cell. 4. Author 1-2 additional hand-painted maps (`source/overworld/maps/forest` and `town` are the upstream references; town is the most populated). 5. Add a multi-map screenshot pass to `nodes/harness.py`. This is a multi-day effort, not a quick fix. File a TODO and get explicit approval before starting. ## Run All commands run from `/home/fezzik/dev/simvx`: ```bash # Interactive uv run python ported_games/gdquest_open_rpg/simvx_port/main.py # Headless test capture (writes 7 screenshots to screenshots/) uv run python ported_games/gdquest_open_rpg/simvx_port/main.py --test # Web export uv run simvx export web /home/fezzik/dev/ported_games/gdquest_open_rpg/simvx_port/main.py \ -o /home/fezzik/dev/ported_games/gdquest_open_rpg/simvx_port/web/index.html ``` ## Controls ### Overworld - **WASD / Arrow keys** step the player one tile in the direction held. - **Left mouse button** clicks any walkable tile to path-find to it (BFS). - **E** interacts with the NPC in the cell directly in front of the player. - **Q** quits. ### Dialogue - **SPACE / ENTER / E** advances one line. Press during typewriter to fast-forward the current line; press again to advance. - **Left mouse button** also advances dialogue. ### Combat - **W / S / Up / Down** navigate the action menu / target cursor. - **A / D / Left / Right** switch target horizontally. - **ENTER / SPACE / E** confirm action / target selection. - **ESC / X** cancel back to the action menu. ## Mobile / touch Touchstart/move/end surface as `MouseButton.LEFT` in the web runtime, so click-to-path on the overworld and confirm-on-tap in dialogue work on touch devices. The combat UI still requires keyboard nav (no on-screen buttons in v1, flagged in NOTES.md as a follow-up). ## File map ``` simvx_port/ ├── main.py # RPGRoot scene + InputMap + headless --test entry ├── pyproject.toml # [tool.simvx] root = "RPGRoot" ├── nodes/ │ ├── __init__.py │ ├── settings.py # Constants (WIDTH/HEIGHT/TILE/colours) │ ├── sprites.py # Procedural NumPy-painted sprites for tiles + characters │ ├── gameboard.py # 30x17 hand-authored map + BFS pathfinder + DIALOGUES + TRIGGERS + NPCS │ ├── overworld.py # Tile sprite layer + trigger highlight overlay │ ├── player.py # PlayerController (step move, click-to-path, interact) │ ├── npc.py # Stationary NPC with dialogue id │ ├── dialogue.py # Bottom-anchored dialogue panel with typewriter │ ├── encounter* # Encounter trigger logic lives in main.py + gameboard.TRIGGERS │ ├── screen_transition.py # Fade-to-black overlay │ ├── stats.py # BattlerStats dataclass + spec prototypes (party + enemies) │ ├── actions.py # AttackAction / HealAction / AreaAttackAction (coroutine-style) │ ├── battler.py # Battler sprite (flash, shake, selection-bob, dies) │ ├── floating_label.py # Damage/heal/miss labels (+25 / -10 / MISS) │ ├── battle.py # BattleScene state machine + UI (action menu, target cursor, party panel) │ ├── audio_fx.py # Procedural NumPy SFX + music loops │ ├── save_io.py # JSON save/load (save shrines) │ └── harness.py # Headless --test capture (7 screenshots through full loop) ├── screenshots/ # Auto-captured by --test └── web/index.html # Pyodide bundle (~2.7 MB) ``` ## Architecture choices - **Procedural sprites everywhere**: no PNG dependency, all tiles + party + enemies + UI cursors painted in NumPy. Mirrors the Solitaire / Balatro port pattern. - **Pure-logic gameboard.** `nodes/gameboard.py` exports `TILES` (30x17 list-of-lists), `bfs_path()` for click-to-path, `TRIGGERS` (cell -> encounter/save), and `NPCS` (cell -> kind+dialogue). All map data hand-authored in Python; no Tiled / TileMap parsing. - **Step-based movement.** Player snaps to tile centres; a fixed-duration ease-out lerp animates between cells. After each arrival, on_process re-checks held-keys so a held direction continues stepping naturally. - **Coroutine-style actions.** Each `BattlerAction` is a per-frame state machine (`start(source, targets, scene)` + `tick(dt) -> done`). Avoids yielding into Python coroutines; the BattleScene drives one action at a time and pops to the next when `tick()` returns True. - **AI = random valid action + random target.** Enemy battlers cache their action choices at the START of the round; player choices come in via the action menu. Speed-sorted execution. - **Procedural audio.** Same pattern as Claustrowordia / SNKRX: NumPy sine/saw/noise → AudioStream.from_pcm. Tiny SFX + a 4-bar overworld loop + a 4-bar battle loop. - **JSON save.** Single `saves/open_rpg.json` (atomic write + one .bak) recording the player's last shrine cell. Matches Solitaire's pattern; game state itself doesn't need full persistence. - **`new_layer()` for HUD layering.** SimVX's Draw2D draws fill primitives BEFORE textured quads within a single batch, so a HUD panel drawn from a node-tree on_draw appears BEHIND any later sprite. Calling `renderer.new_layer()` before fills forces a fresh batch so HUD draws on top. See `NOTES.md` "Engine friction" #1. ## Visual reference `simvx_port/screenshots/`: | File | Stage | |---|---| | `01_overworld.png` | Initial overworld idle (player + 3 NPCs + tilemap + encounter highlights) | | `02_dialogue.png` | NPC interaction: Monk dialogue typing | | `03_encounter.png` | Encounter dialogue: Bear *roars* before battle | | `04_battle_player.png` | Battle scene loaded, party + enemies, HP bars | | `05_battle_target.png` | Wizard's action menu open (Attack / Heal) | | `06_battle_enemy.png` | Mid-battle (Squirrel injured, both enemies present) | | `07_victory.png` | VICTORY! banner over arena | ## Acceptance bar - [x] Source cloned to `source/` - [x] Game launches from `main.py` - [x] Player walks the overworld via WASD or click-to-path - [x] Dialogue with NPCs (typewriter + advance) - [x] Encounter trigger → battle scene swap (with fade transition) - [x] Full turn-based combat (3 player vs 2-3 enemies, action menu, target cursor) - [x] Damage rolls (variance + crit + miss) with floating labels - [x] Heal action (Wizard), area attack (Squirrel) - [x] Camera shake / hit flash / energy regen on damage taken - [x] Victory + defeat resolution with dialogue - [x] Save shrine (JSON persistence) - [x] Procedural audio (SFX + music loops) - [x] Headless `--test` captures 7 stage screenshots - [x] Web export builds cleanly (~2.7 MB) ## Source ```{literalinclude} ../../examples/ports/gdquest_open_rpg/main.py :language: python :linenos: ```