Introduction and Tech Stack

Underroot is a 2D idle/incremental survival-crafting game built in Godot 4. Below: what it is, why Godot and GDScript, and a map of the systems the rest of these docs cover.

What Underroot is

You play a lone digger. Tunnel down for materials, grow a village on the surface, hold back the Maw — a creature that advances from the right and eats the world one wall at a time. Progress is depth reached, days survived, and village population. It persists across sessions: resources keep draining and threats keep advancing while you're away.

The project name and tagline are set in project.godot:

config/name="Underroot"
config/description="Every layer buys time."
config/version="1.0"

The publisher/company is Swavvy AB (export_presets.cfg, application/company_name).

Engine and language

Rendering approach

Underroot draws almost all of its world and UI procedurally rather than with sprites. World nodes and UI panels implement _draw() and paint shapes directly (see scenes/world/World.gd, whose _draw() renders the underground layer bands, depth fog, surface strip, ambient day/night tint, and the pond). Static image textures are reserved for story art and cutscenes — for example the intro slideshow images loaded in scenes/ui/IntroStory.gd.

Because the UI is procedural, nearly every control is built in code inside _ready() via add_child(); there are no .tscn files for UI nodes apart from the entry scenes (IntroStory.tscn, TitleScreen.tscn).

Display and viewport

The default viewport is 1280×720 (project.godot):

window/size/viewport_width=1280
window/size/viewport_height=720
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"

The stretch aspect is expand, so overlays that must fill the screen should read the live viewport size (get_viewport_rect().size) rather than assume a fixed 1280×720. The default clear color is a dark violet, Color(0.07, 0.05, 0.15, 1).

World coordinate convention

Underroot's world is Y-down: the ground surface is world Y 0, and underground is positive Y (tiles descend as Y increases). The tile size is TILE_SIZE = 32 pixels, defined once as GameConstants.TILE_SIZE in scripts/core/GameConstants.gd; controller and display scripts source it from there. World bounds and zone landmarks live in scenes/world/World.gd (WORLD_LEFT := -3200.0, WORLD_RIGHT := 1000.0, the forest to the left of 0, defenses near DEFENSE_LEFT := 250.0, and the Maw front at MAW_FRONT_X := 700.0).

High-level system map

Underroot is organized around a large set of autoload singletons (33 of them, listed in project.godot). They divide roughly into these areas:

Area Representative autoloads Role
Core runtime EventBus, GameManager, SaveManager, TimeManager Signals, run state, persistence, the day clock
World and digging WorldManager, RockManager, DiscoveryManager, BuildManager Tile grid, walls, surface rock, discovery pockets, wall stacking
The threat MawController, MawAdaptation, WeatherManager, AstrolabeManager Multi-front Maw, per-material familiarity, storms, ritual escalation
Village and survival SurvivalManager, VillageManager, ProjectManager, SpecialistManager Food/water/fuel drain, population, villager tasks, specialists
Economy and content DataRegistry, Inventory, ToolState, MachineManager, CraftingUnlockManager JSON data, resources, tools, machines, recipe unlocks
Idle/offline TaskQueue, OfflineSimulator Queued tasks and catch-up simulation on resume
Meta and cosmetics CosmeticManager, CodeManager, ChallengeManager, HarrowManager, SessionTelemetry Account-wide unlocks, redemption codes, challenge modifiers, local telemetry

All cross-system communication flows through the EventBus autoload (scripts/core/EventBus.gd); scene nodes do not connect signals directly to one another.

Two scripts run before anything else in the autoload order and exist purely to prepare the environment: FontSetup (attaches embedded fallback fonts, since Web builds have no OS fonts) and PerfSetup (applies Low Performance Mode at startup before any renderer reads PerformanceMode.enabled).

Persistence at a glance

Saves are plain JSON flat files. There are 12 save slots (user://saves/slot_N.json; SaveManager.MAX_SLOTS = 12), plus account-level config.json and settings.json. The save format is versioned — SaveManager.SAVE_VERSION = 3 — with in-place _migrate() upgrades and corrupt-slot quarantine so a bad file never blocks boot. On Windows the user data directory is %APPDATA%\Godot\app_userdata\Underroot\.

Key files