Dig Build and Gather Controllers
Four input controllers turn mouse and keyboard events into world mutations: digging tiles, placing and binding walls, harvesting surface resources, setting explosive charges. Each is a Node2D under World that reads input in _unhandled_input, advances an in-progress action in _process, and draws its own preview or progress overlay in _draw.
All four bail early on GameManager.is_world_input_blocked(), so world input is suppressed while a modal or menu owns the screen. Coordinates convert with GameConstants.TILE_SIZE (32 px); underground is positive Y and the surface is Y 0.
DigController
scenes/world/DigController.gd handles left-click digging with a timed progress bar.
Input and start
A left click is ignored when a build material is selected (_build_menu.selected_material non-empty), so the same button can serve the build menu. Otherwise the mouse position converts to a tile and _try_start_dig runs:
- Tiles above ground (
y < 0) are rejected. TerrainDigging.can_dig(tile_pos, tool_id)must pass; a failure routes to_warn_cannot_dig.- Underground tiles must satisfy
WorldManager.is_surface_connected— otherwise the player gets"No tunnel to the surface here.". - Duration is captured once at start:
TerrainDigging.get_dig_time(...) / CosmeticManager.get_effect_mult("dig_speed_mult"). Capturing it means equipping a faster tool mid-dig cannot retroactively rush the in-progress tile.
The controller sets GameManager.is_working = true and emits EventBus.dig_started. A right click cancels via _cancel_dig.
Progress and completion
_process accumulates delta into _progress and calls queue_redraw() each frame; _draw renders a tile highlight and a bottom-aligned progress bar. When _progress >= _duration, _complete_dig runs:
_check_overshoot_warning(_target)
TerrainDigging.apply_dig(_target)
_check_world_bottom(_target)
GameManager.current_run["last_dig_x"] = _target.x
GameManager.current_run["last_dig_y"] = _target.y
last_dig_x / last_dig_y record where the player personally dug, so the "Dig Front" jump returns to their own hole rather than a machine's face. They are set here rather than off EventBus.tile_dug, which machines and the Astrolabe also fire.
Warnings
_warn_cannot_digsurfaces a toast only when the tile is exposed but too hard for the tool — it stays silent for open space and buried tiles, which the player cannot act on. It names the material and, viaTerrainDigging.required_tool_for, the pickaxe needed, and throttles repeats of the same message to once per 2.5 s._check_overshoot_warningfires once per run when the player digs a material exactly one tier above the tool'smax_material_tier(the 4x-durability overreach case), warning that the pickaxe is wearing fast._check_world_bottomsetscurrent_run.bottom_reachedand emitsEventBus.world_bottom_reachedthe first time a dig reaches the deepest layer'sdepth_end.
BuildController and BuildManager
scenes/world/BuildController.gd handles wall placement, upgrade, removal, and root binding on the surface line (y = 0), delegating all state changes to BuildManager (scripts/building/BuildManager.gd). Walls are built from raw materials — one unit per placed layer.
Build zones and Maw bodies
Two guards decide where a wall may go:
_in_build_zone(world_x)— the east zone is always open (x >= PLACE_X_MIN, i.e.0). A west zone ([WEST_BUILD_OUTER_X, WEST_WALL_LINE_X]) opens only whileMawController.has_west_front()is true, so a normal run stays east-only and the village core between the lines is never buildable._in_maw_body(tile_x)— a tile at or behind any front's mouth on that front's side is unbuildable.
Input modes
BuildController runs in one of three transient modes, set from BuildMenu signals (bind_requested, remove_requested) or by holding modifiers:
| Interaction | Result |
|---|---|
| Right click (or left click with a material selected) | _try_place_wall |
| Shift + place | _try_upgrade_wall |
| Remove mode, left click | _try_remove_wall (Shift = whole column) |
| Bind mode, left click | _try_bind_wall |
| Escape / right click in a mode | Cancel the mode |
_process maintains a placement preview (_show_preview, _preview_tile_x) whenever a material is selected and the cursor is over a buildable surface tile. _draw renders the stacking preview, an upgrade preview when Shift is held over an existing wall (including a +N% mix bonus hint), and pulsing remove/bind overlays with labels.
BuildManager: wall data operations
WallData (scripts/building/WallData.gd, class_name WallData) defines the wall dictionary and its math; BuildManager applies them and touches Inventory:
| BuildManager method | Behaviour |
|---|---|
can_build(position, material_id) |
Material must have is_wall_material and the player must hold at least 1 unit. |
build_wall(position, material_id) |
New column costs 1 unit via WallData.create; an existing column routes to _add_layer_unit. Emits wall_built. |
_add_layer_unit |
Adds one unit (up to max_height_units, default 9), merging into a matching layer or appending a new material layer. First mixed material fires wall_mix_discovered. |
can_upgrade / upgrade_wall |
Converts all foreign-material units in a column to one material, refunding the replaced units and charging for the new ones. |
remove_wall |
Destroys the whole column, no refund. Emits wall_removed (not wall_destroyed, so it skips Maw-attack shake). |
remove_wall_unit |
Peels one unit off the top; a column down to its last unit is removed outright (binding lost with it). |
Each unit stacks HP: a layer contributes wall_hp * units, and recalc_max_hp applies a mix bonus of 1.0 + mix_bonus_per_type * (layer_count - 1). All wall HP is scaled by ChallengeManager.wall_hp_mult().
Root binding
_try_bind_wall binds a wall with strange root so it resists the Maw. It requires ROOTS_REQUIRED strange roots (from balance.json -> walls.roots_required, default 5), a wall at that column, no existing binding, and the column not on cooldown (MawController.is_column_on_cooldown). On success it removes the roots, calls WorldManager.bind_wall, and — if the owning front is already chewing that exact column — primes an immediate trigger via MawController.prime_root_trigger.
GatherController
scenes/world/GatherController.gd handles surface harvesting: berry bushes, trees, surface rocks, forest boulders, the pond, and gravestone collection. It sits at z_index = 7 so its progress bars draw above the player marker.
A left click routes to _try_start_gather, which checks targets in order. Surface rocks are checked first because they straddle y = 0; everything else requires world_pos.y < 0. The gravestone is an instant collect when clicked within 24 px. Each remaining target (boulder, pond, bush, tree) starts a timed gather whose duration is captured at start:
| Target | Manager call on completion | Notes |
|---|---|---|
| Berry bush | ForestManager.harvest_bush |
Time divided by CosmeticManager.get_berry_speed_mult() (gloves). |
| Tree | ForestManager.harvest_one + ToolState.use_gather_tool() |
Time from ForestManager.get_gather_time. |
| Surface rock | RockManager.harvest_one + ToolState.use_gather_tool() |
|
| Boulder | ForestManager.harvest_boulder |
Time = BOULDER_BASE_GATHER_TIME / dig_speed_multiplier. |
| Pond | ForestManager.harvest_pond then SurvivalManager.add_supplies |
Fixed POND_GATHER_TIME / POND_GATHER_AMOUNT. |
_process advances whichever single gather is active and calls the manager on completion; _draw renders a small progress bar at the target's position. A right click cancels all active gathers via _cancel_all.
DynamiteController
scenes/world/DynamiteController.gd places two kinds of explosive charge: dynamite (a compact radial blast) and the mining charge (a directional 2x7 corridor blast). Tuning is read from balance.json -> dynamite and -> mining_charge in _ready.
Entering placement and aiming
Placement mode starts from EventBus.machine_placement_requested (for dynamite or mining_charge) or the T key for dynamite, and requires at least one of the item in inventory. While placing the mining charge, R rotates through four directions (right / down / left / up); Escape cancels. _draw shows an orange tile preview for dynamite and a blue footprint plus direction arrow for the mining charge, tinted by validity.
Placement rules
_try_place_dynamite / _try_place_mc share the same guards: the tile must be underground (y > 0), solid, exposed (TerrainDigging.is_exposed), and free of an existing fuse. On success the item is consumed and a random fuse time (FUSE_MIN..FUSE_MAX) is stored in _fuses, with the charge type in _fuses_type and the mining charge's direction in _fuses_dir.
Detonation
_process counts each fuse down and calls _explode at zero. Dynamite carves a diamond of radius BLAST_REACH plus the four diagonals; the mining charge carves the _mc_blast_tiles footprint (MC_BLAST_LENGTH tiles long, two wide, in the aimed direction). Both call _dig_blast_tiles, which for each solid tile removes it, adds TerrainDigging.roll_yield units to inventory, and emits tile_dug and mined_yield — the same yield path as a manual dig.
If the player's tile falls within the blast (radial DAMAGE_RADIUS for dynamite, the footprint for the mining charge), they take BLAST_FOOD_DMG / BLAST_WATER_DMG (or the MC_ variants) off food and water and get a "Caught in the blast!" toast; otherwise a "BOOM". Each blast emits EventBus.dynamite_exploded and draws a fading flash ring.
Key files
| File | Role |
|---|---|
scenes/world/DigController.gd |
Left-click dig, progress bar, overreach/too-hard warnings, dig-front tracking. |
scenes/world/BuildController.gd |
Wall placement, upgrade, removal, root binding, previews; build-zone and Maw-body guards. |
scripts/building/BuildManager.gd |
Wall state changes and inventory cost; stacking, mixing, upgrade, removal. |
scripts/building/WallData.gd |
Wall dictionary shape and HP/stacking math. |
scenes/world/GatherController.gd |
Surface harvest input for bushes, trees, rocks, boulders, pond, gravestone. |
scenes/world/DynamiteController.gd |
Dynamite and mining-charge placement, fuses, and detonation. |