Developer FAQ
Quick answers to the questions that come up most when you start working in the Underroot codebase. Each one links to the page with the full story.
What's the tech stack?
Godot 4 (targeting 4.6, CI pins 4.6.2) and GDScript, strictly typed, with no C#/.NET. Default viewport is 1280×720, Forward+ renderer. The whole game runs off ~33 autoload singletons that talk to each other through a single EventBus. See Introduction and Tech Stack and The Autoload Model.
Where do I start reading?
Start with Repository Layout and Boot Flow for the directory map and the boot chain (IntroStory → TitleScreen → Main), then The Scene Tree for how the running game is wired. After that, jump to whichever system you're touching.
Where does game balance live?
In the JSON files under data/, not in the scripts. balance.json holds the tuning sections (maw, survival, village, and so on), and the other files define materials, tools, recipes, machines, layers, and more. Scripts read these through DataRegistry and only fall back to hardcoded constants if a key is missing. Full map: Data Schemas Reference.
How do I add a new material, tool, or recipe?
Edit the matching data/*.json file — no code needed for the common cases. There's a step-by-step (including the crafting-category rule that decides whether a recipe even shows up) in Adding Materials Tools and Recipes. Machines, challenges, cosmetics, and redemption codes each have their own guide in the Developer section too.
Why isn't my new recipe showing up in the craft menu?
Almost always the category. CraftMenu only renders tools, structures, machines, and components — a recipe with any other category is silently invisible. The other gotcha is unlock gating: recipes unlock once every input material has been seen. Details in Adding Materials Tools and Recipes.
How do systems talk to each other?
Through the EventBus autoload. Managers emit signals; they don't call each other directly, and scene nodes never connect signals to each other. If you're adding cross-system behavior, add or listen to an EventBus signal. See EventBus and the Signal Convention.
How do I look up game data at runtime?
DataRegistry.get_material(id), get_tool(id), get_recipe(id), get_machine_def(type). They all return {} on a miss, so always read fields with .get("key", default). More in DataRegistry and the Data Files.
Which way is down? What's the coordinate system?
Underground is positive Y — the surface is world Y 0 and tiles go down as Y increases. Tiles are 32 px (GameConstants.TILE_SIZE). The base sits near X 0, the forest is to the left (negative X), and the Maw comes from the right. Full rundown in World Coordinate System and Camera.
Why are there two HUDs? What's hud_style?
Underroot ships two HUD skins: the classic sidebar-and-bars layout and the talisman skin (corner instruments + TAB ledger). HudShell.style() (scripts/ui/HudShell.gd, static — not an autoload) says which is live; the choice persists as hud_style in settings.json. Fresh machines default to talismans; machines that predate the skin stay classic until they flip the Settings > "Legacy UI" toggle. A flip emits EventBus.hud_style_changed and the HUD rebuilds its chrome in place — no reload. Skin-dependent code asks HudShell.style(), never a sibling panel. See The Talismans HUD and HUD and Bars Reference.
What are player profiles? Who owns what?
Several people can share one machine: each profile owns the personal state (cosmetics ownership, lifetime stats, tips/nudges, redeemed codes, Top Runs, tutorial state), each run owns its look (run.cosmetics_loadout) and its owner tag (run.profile_id), and the machine owns settings.json (music, perf, hud_style). config.json is the profile store (config_version = 2, profiles.pN blobs); every SaveManager accessor is scoped to the active profile. The TitleScreen's "Playing as" chip switches; switching is a full title reload. See Player Profiles and Save System and Migration.
Someone deleted a run they wanted — can it come back?
Usually, yes. Every delete path routes through delete_slot(), which renames the file to slot_N.deleted.json (a one-deep, per-slot trash that is never reaped on a timer). Close the game and rename it back to slot_N.json in user://saves/ — any free index works, the index is just the filename. If the run belonged to a deleted profile, find it via the picker's "Show all" and reassign with ⇄. Full procedure in Save System and Migration. It's gone for real only if the same slot was deleted again or a factory reset ran.
How are saves structured, and where do they live?
Twelve JSON slots at user://saves/slot_N.json, plus the profile-holding config.json and the machine-level settings.json. The slot format is versioned (SAVE_VERSION = 4) with an in-place migration path and corrupt-slot quarantine, so a bad file never blocks boot; config.json has its own versioned shape (config_version = 2) migrated on read. On Windows that's %APPDATA%\Godot\app_userdata\Underroot\. See Save System and Migration.
How do I start a fresh run in code?
GameManager.reset_for_new_run(). It rebuilds current_run and calls each subsystem's reset() in dependency order — don't reach into managers' private fields yourself. A lineage carry-on after a death is not a new run and deliberately skips this. See GameManager and the Run Lifecycle.
How do I run the checks before I push?
Three headless gates, same as CI: the parse-check (tools/parse_check.ps1), the data validator (validate_data.gd, expects DATA OK), and the smoke test (smoke_test.gd, expects SMOKE PASS). The exact commands, including the Godot console-exe requirement, are in Verification and CI. Run the data validator after any data/*.json edit.
The UI is all in code — where are the .tscn files?
There mostly aren't any. Almost every UI node is built procedurally in _ready() via add_child(); the only scene files are the entry screens (IntroStory, TitleScreen) and the World/Main roots. There are a few Godot-specific gotchas around CanvasLayer sizing and modal centering — UI Construction Conventions covers them.
What's the branch and commit workflow?
One branch and PR per item. Don't run git checkout -- project.godot — uncommitted local tweaks live there; back it up and restore by copy instead. Strip any dev/cheat codes before merging, and keep redemption codes out of commit messages and PR descriptions. See Contributor Workflow.