# The Scene Tree

The runtime scene tree Underroot builds during gameplay: the `World.tscn` node tree, the overlay `CanvasLayer`s `Main.gd` stacks on top of it, and the per-frame `_process` heartbeat in `World.gd` that drives the simulation.

## Two roots: Main and World

Gameplay is a two-level structure. `scenes/main/Main.tscn` is a bare `Node` running `scenes/main/Main.gd`; it is the runtime root. In `_ready()`, Main instantiates `World.tscn` as a child and then layers a series of overlay `CanvasLayer`s beside it:

```gdscript
func _load_world() -> void:
	var world_scene := load("res://scenes/world/World.tscn") as PackedScene
	add_child(world_scene.instantiate())
```

So the world (terrain, controllers, camera, the core HUD) lives under the `World` node, while transient full-screen surfaces (cutscenes, popups, the game-over screen) are siblings added by Main.

## The World.tscn node tree

`World.tscn` is rooted at a `Node2D` running `scenes/world/World.gd`. Its children, in tree order:

```text
World (Node2D) — World.gd
  VillageBackdrop        (Node2D, z=-50) — surface scenery
  UndergroundBackground  (Node2D, z=-1)
  TerrainDisplay         (Node2D)        — viewport-culled tile drawing
  GraveyardDisplay       (Node2D)        — deep-forest graveyard hill
  ForestDisplay          (Node2D)        — trees / bushes / pond
  RockDisplay            (Node2D)        — surface rocks
  WallDisplay            (Node2D)        — placed walls
  MawDisplay             (Node2D, z=-1, pos (700,0)) — east Maw creature
    MawDisplayFront      (Node2D, z=7, front_layer)
  MawDisplayWest         (Node2D, z=-1, pos (-700,0), scale (-1,1), hidden) — Two Fronts west Maw (front_index=1)
    MawDisplayWestFront  (Node2D, z=7, front_layer)
  PlayerMarker           (Node2D)        — animated player dot
  DigController          (Node2D)        — left-click dig input
  GatherController       (Node2D)        — surface harvest input
  BuildController        (Node2D)        — right-click wall placement
  MachineDisplay         (Node2D)        — draws placed machines
  MachineController      (Node2D)        — machine placement/interaction
  DiscoveryLayer         (Node2D, z=1)   — discovery pocket hints
  ParticleLayer          (Node2D, z=10)  — dig/harvest particles
  DynamiteController     (Node2D, z=11)  — dynamite / mining charges
  Camera2D               (pos (0,120), zoom (0.85,0.85))
  HUD                    (CanvasLayer)   — HUD.gd
  BuildMenu              (CanvasLayer)   — BuildMenu.gd (hosts CraftMenu)
  VillagerVisitPopup     (CanvasLayer, layer=5)
  TutorialOverlay        (CanvasLayer, layer=10)
  VillageBar             (CanvasLayer)
```

A few structural details from `World.tscn`:

- **The Maw is multi-front in the scene, not just the controller.** There is a canonical east `MawDisplay` at `(700, 0)` and a mirrored, initially hidden `MawDisplayWest` at `(-700, 0)` with `scale = (-1, 1)` and `front_index = 1` for the Two Fronts challenge. Each has a `front_layer` child (`z=7`) that draws the creature's foreground over the terrain.
- **Z-ordering is explicit.** `VillageBackdrop` sits far back at `z=-50`; particles (`z=10`) and dynamite (`z=11`) draw in front of the world; the Maw's front layers use `z=7`.
- **The camera** starts at `Vector2(0, 120)` with zoom `0.85`, matching the constants used across the coordinate system.
- **The HUD's children are skin-dependent.** `HUD.gd` spawns the active HUD skin's surfaces at runtime — classic threat panel + bottom bar, or the talisman instruments (medallion, crest, charm, rail, ledger, speech bubbles). The scene-declared `VillageBar` is hidden and processing-disabled under the talisman skin, never freed. See [The Talismans HUD](/docs/underroot/the-talismans-hud).

`World.gd` also spawns a few helper nodes at runtime that are not in the `.tscn`: a particle burst pool, a `PerfMonitor`, a `PerfSuggestionModal`, and — on web builds only — a `WebGraphicsNotice`.

## Main.tscn runtime overlays

Every overlay is a `CanvasLayer` (or a `Node2D` wrapped in a `CanvasLayer` when it needs to draw in screen space) that `Main._ready()` adds after loading the world. From `scenes/main/Main.gd`:

| Overlay | Script | Notes |
|---|---|---|
| Offline summary | `scripts/ui/OfflineSummaryController.gd` | Catch-up report on resume |
| Game over screen | `scripts/ui/GameOverScreen.gd` | Death / lineage screen |
| Screen flash | `scenes/ui/ScreenFlash.gd` | Danger vignette pulses |
| Floating text pool | `scenes/ui/FloatingTextPool.gd` | Pooled floating labels |
| Village building popup | `scenes/ui/VillageBuildingPopup.gd` | |
| Healer intro popup | `scenes/ui/HealerIntroPopup.gd` | |
| Storage warning popup | `scenes/ui/StorageWarningPopup.gd` | |
| Weather display | `scenes/ui/WeatherDisplay.gd` | `Node2D` in a `CanvasLayer` (layer 3) for screen-space rain |
| Storm cutscene | `scenes/ui/StormCutscene.gd` | |
| Astrolabe cutscene | `scenes/ui/AstrolabeCutscene.gd` | |
| Astrolabe unlock modal | `scenes/ui/AstrolabeUnlockModal.gd` | |
| New materials modal | `scenes/ui/NewMaterialsModal.gd` | |
| Surge music modal | `scenes/ui/SurgeMusicModal.gd` | |
| Milestone cutscene | `scenes/ui/MilestoneCutscene.gd` | |
| Challenge mastery cutscene | `scenes/ui/ChallengeMasteryCutscene.gd` | |
| Challenge aid modal | `scenes/ui/ChallengeAidModal.gd` | |
| Storm particle canvas | `scenes/ui/StormParticleCanvas.gd` | `Node2D` in a `CanvasLayer` (layer 17) |
| Storm toast | `scenes/ui/StormToast.gd` | |

Most are created by small `_load_*` helpers that new a `CanvasLayer`, `set_script(...)`, and `add_child(...)`. After wiring the overlays, `_ready()` runs `_check_offline_return()`, calls `GameManager.show_breach_toasts()`, and starts music unless a fresh run is about to hand control to Wren's tutorial intro.

## The World.gd heartbeat

`World.gd` is the simulation's heartbeat: its `_process(delta)` advances the core managers every frame. The relevant part:

```gdscript
func _process(delta: float) -> void:
	if not _world_initialized:
		_world_initialized = true
		# restore announced layers, seed current layer index from camera...
	if not _intro_active:
		SurvivalManager.process_survival(delta)
		if not bool(GameManager.current_run.get("tutorial_clock_frozen", false)):
			MawController.process_chew(delta)
	VillageManager.process_village(delta)
	MachineManager.process_machines(delta)
	_pan_camera(delta)
	_apply_camera_shake(delta)
	_check_layer_transition()
	_request_redraw(delta)
```

The four simulation ticks it drives each frame:

- `SurvivalManager.process_survival(delta)` — food/water/fuel drain.
- `MawController.process_chew(delta)` — the Maw eating toward the base.
- `VillageManager.process_village(delta)` — population and daily village logic.
- `MachineManager.process_machines(delta)` — automated machine work.

Two clocks can be frozen. While Wren's blocking intro modal is up, `_intro_active` is true and **both** survival drain and the Maw are paused (the player can't act behind the modal). During the guided tutorial's Phase A, `current_run.tutorial_clock_frozen` keeps survival live (so the berry beat visibly moves the meter) but holds the Maw's grace clock until the pivot beat. `VillageManager` and `MachineManager` always tick, so a placed machine never stalls.

The rest of `_process` handles the camera: WASD/arrow panning and drag in `_pan_camera` / `_unhandled_input`, periodic rumble and shake in `_apply_camera_shake` (rumble only when a Maw front is genuinely close to its defense line or under 10 minutes to breach), layer-band announcements in `_check_layer_transition`, and dirty-flag redraw gating in `_request_redraw` (day/night tint changes and throttled pond shimmer).

> `World.gd` also owns the procedural `_draw()` for the underground layer bands, depth fog, surface strip, ambient tint, and pond. To avoid repainting static geometry every frame, `_request_redraw` only calls `queue_redraw()` when the day tint moves or the pond shimmer timer elapses (throttled to 30 fps).

## How this differs from Main's autosave

The heartbeat lives in `World.gd`, but autosave does not — it is a separate `_process` timer in `Main.gd` that calls `SaveManager.save_game()` every 20 seconds (`AUTO_SAVE_INTERVAL := 20.0`). See [Repository Layout and Boot Flow](/docs/underroot/repository-layout-and-boot-flow) for that path.

## Key files

- `scenes/world/World.tscn` — the authored world node tree.
- `scenes/world/World.gd` — the world root; per-frame heartbeat, camera, and `_draw()`.
- `scenes/main/Main.tscn` / `scenes/main/Main.gd` — runtime root; instantiates World and all overlays.

## Related

- [Repository Layout and Boot Flow](/docs/underroot/repository-layout-and-boot-flow)
- [World Coordinate System and Camera](/docs/underroot/world-coordinate-system-and-camera)
- [The Autoload Model](/docs/underroot/the-autoload-model)
- [HUD and Bars Reference](/docs/underroot/hud-and-bars-reference)
- [The Talismans HUD](/docs/underroot/the-talismans-hud)
- [The Maw](/docs/underroot/the-maw)