Extending the CRPG
How to add locations, items, traps, enemies and spells to the hand-built scenes of games/crpg-realm, and what the code assumes about a scene. You need to read GDScript and work in the Godot 4.3 editor. For data-only authoring, use the campaign editor.
On this page
Pick a route
| You want to | Use |
|---|---|
| Add maps, characters or items as data and play them | The editor and cartridges. Read its last section first: NPCs and quests in cartridges are still coded per story. |
| Add a rule, spell, monster or AI behaviour and prove it with a test | data/v1/*.json, ScenarioEngine.gd and a feature in tests/e2e/features/engine/. See testing. |
| Add a location, trap, enemy or spell to the real-time scenes | This guide and its three pages. |
The three pages:
- Scenes, doors and maps: the scene tree,
DoorPortal, collision and camera. - Items, traps and mods:
items.json, ground items and chests,Trap, and the mod loader. - Combat, party and spells: attack math, the Malakor fight, formations, enemy threat and the spell code.
Where things live
Path in games/crpg-realm/ | What it holds |
|---|---|
project.godot | Engine settings, main scene (CharacterSelect.tscn) and the ten autoloads |
scripts/ | One script per location (VillageSquare.gd, AncientCatacombs.gd, …), components (DoorPortal.gd, Trap.gd, TacticalEnemy.gd, GroundItem.gd), GameState.gd, CombatManager.gd, Pathfinder.gd, FogOfWar.gd, GameControlServer.gd |
scripts/engine/, scripts/demo/ | The scenario engine, dice, cartridge manager and demo mode |
scenes/ | One .tscn per location, plus HeroPlayer.tscn, PartyCompanion.tscn, ShadowHound.tscn, CharacterSelect.tscn, VictoryScreen.tscn, CartridgeWorld.tscn |
scenes/components/ | Building blocks to instance: DoorPortal, Trap, GroundItem, TacticalEnemy, NPCCharacter, FogOfWar, ActionLog, windows and toolbars |
data/v1/ | JSON content arrays. Which code reads which file is on the reference page. |
src/generated/v1/ | DataStoreV1.gd and the typed wrappers ItemData, SpellData, MonsterData, NPCData |
mods/ | Drop-in mods. The loader reads only a traps_file. |
tests/e2e/ | behave features, step definitions in features/steps/, the report generator |
DataStoreV1.gd starts with “Auto-generated”, but it has been edited by hand (races, traps, mod loading). Do not regenerate it with packages/crpg-builder. The generator template would overwrite those edits.
Rules the code depends on
Breaking these does not raise an error. The affected feature just stops seeing your nodes.
- Keep gameplay nodes flat. Enemies, traps, NPCs, ground items, doors and companions must be direct children of the scene root.
GameState.get_scene_traps(), thebattle.enemieslist in/api/v1/state, fireball targeting, enemy pack alerts and party lookup all scanget_tree().current_scene.get_children(). A trap under aTrapsgroup node is never detected, and an enemy under anEnemiesnode never hears an alert. - Add a
CombatManagerchild node to any scene with combat. It is aclass_name, not an autoload, and scene scripts reach it as$CombatManager. - Name the player node
HeroPlayer.GameState.get_scene_party_nodes(),TacticalEnemyand the HTTP API look it up by that name. ItsCamera2Dis the scene camera. - The root node name is the scene name.
/api/v1/statereports it, and the test stepthe current scene is "X"compares against it. - Set
mouse_filter = 2(Ignore) on the map’sGroundnode. Otherwise it swallows clicks beforeHeroPlayer._unhandled_inputsees them and click-to-move stops working. - Put content in
data/v1/*.json, but expect some behaviour to be keyed by id in code. Potion effects are inGameState.use_item(), spells inCombatManager.gd. Adding JSON alone is not always enough; each page says when.
Checklist: a new location
This is the shortest path to a new playable scene. The details of each step are on the three pages above. Duplicating an existing scene in the Godot editor is safer than writing a .tscn by hand.
- Duplicate a scene. In the FileSystem dock, right-click
scenes/AncientCatacombs.tscnand choose Duplicate. Rename the file and its root node (the root name must match the file name for tests). Delete the catacomb children you do not want (the traps, the sarcophagus, the skeleton and guardian, the two ground items,KeepSecretPassage). - Keep what scripts look up by path:
CombatManager,Ground, the boundaryStaticBody2D,HeroPlayer, and underCanvasLayertheNoticeLabel,PartyHUD(withQuestLabel,BtnOptions,BtnInventory,BtnPause),ActionLog,InventoryWindow,SettingsModalandCharacterStatusWindow. KeepFogOfWarif you want fog. -
Write the scene script. Attach a new script that follows the start of
AncientCatacombs.gd:extends Node2D @onready var hud = $CanvasLayer/PartyHUD @onready var hero = $HeroPlayer func _ready() -> void: if hero and hero.has_method("set_camera_limits"): hero.set_camera_limits(0, 0, 2560, 1440) hud.update_display("Active Quest: Explore the new room") if GameState.party_members.size() > 1 and not has_node("PartyCompanion"): var comp_scene = load("res://scenes/PartyCompanion.tscn") if comp_scene: var comp = comp_scene.instantiate() comp.global_position = hero.global_position + Vector2(-48, 24) add_child(comp) if has_node("FogOfWar"): $FogOfWar.register_actor(comp)This is a sketch derived from
AncientCatacombs.gd. Run the scene in the editor to check it. Use the size of your own map plate for the camera limits, and set the same size onFogOfWar.map_widthandmap_height. - Connect it with doors. Instance
scenes/components/DoorPortal.tscndirectly under the root of both scenes. Setdoor_id,door_name,target_scene(res://scenes/YourScene.tscn) andtarget_spawn(the position where the hero should appear). Details and pitfalls are on Scenes, doors and maps. - Add content. Ground items, chests, traps, enemies and NPCs are on the other two pages.
- Run it:
./debug.sh --scene res://scenes/YourScene.tscn. The short names indebug.shcover only five scenes. -
Test it. Add a feature under
tests/e2e/features/normal/. These steps exist incrpg_steps.pyandinfinity_ai_steps.py:Feature: Your location portals Background: Given the cRPG game is running and healthy Scenario: Travel from the catacombs to the new room and back Given an isolated test starting in scene "AncientCatacombs" with party "Lieutenant Vance" the "fighter" Then the current scene is "AncientCatacombs" When the infinity ai agent enters door "door-id-your-room" Then the current scene is "YourScene" When the infinity ai agent enters door "door-id-your-room-exit" Then the current scene is "AncientCatacombs"Scenario names must be unique across all features, because the video file name comes from the name. Run it with
python3 run_cucumber_tests.py tests/e2e/features/normal/<your-file>.feature. Theenters doorstep finds the node bydoor_id, walks the hero to it and callstry_enter().
Drive the game over HTTP
The “verify it” steps on the other pages use the control server. It listens on port 8080 by default (18090 under the test harness).
curl -s http://127.0.0.1:8080/api/v1/health
curl -s -X POST http://127.0.0.1:8080/api/v1/setup_state \
-H 'Content-Type: application/json' \
-d '{"scene": "AncientCatacombs", "name": "Lieutenant Vance", "class": "fighter", "companions": ["elora"]}'
curl -s http://127.0.0.1:8080/api/v1/state | python3 -m json.tool | head -40
setup_state re-initialises the hero and loads the scene. A bare scene name becomes res://scenes/<name>.tscn. The full endpoint and action list is on the reference page.