Forest and Surface
Aboveground and near-surface content: the forest ecosystem (trees, berry bushes, the pond, boulders) under ForestManager, the surface stone boulders RockManager scatters, and the discovery pockets DiscoveryManager resolves underground. These are the renewable and one-shot resources the player harvests outside of tile digging.
The forest sits left of world x 0 (negative X); underground is positive Y. All harvest yields flow through Inventory, SurvivalManager, and EventBus.mined_yield for floating-text feedback.
ForestManager
scripts/forest/ForestManager.gd owns the forest ecosystem. It holds two dictionaries keyed by pixel position and a set of scalar pond/boulder amounts:
var nodes: Dictionary = {} # Vector2i(pixel_x, 0) -> ForestNode (trees)
var bushes: Dictionary = {} # Vector2i(pixel_x, 0) -> BerryBush
var pond_water: float = POND_MAX_WATER
_ready calls _generate_forest() and _generate_bushes(). _process runs the regeneration tick for bushes, trees, and the pond every frame.
Trees
Trees are generated left of TREE_RIGHT (-820) out to FOREST_LEFT (-3200), with varied spacing (tight clusters, normal spread, open clearings) and a drifting height cluster so patches share a "feel". A tree's wood capacity scales with visual height via wood_capacity(height) — far-forest giants hold ~20–25 wood, village-edge trees ~8–9, rewarding the deeper trek. Generation skips positions near a boulder (_near_boulder) or inside the graveyard clearing (_in_graveyard).
Harvest and regen:
get_tree_near(world_x, radius)returns the nearest non-depleted tree.harvest_one(node)removes one wood, marks the treedepletedat zero, addsTerrainDigging.roll_yield("wood")to inventory, and emitstree_harvestedandmined_yield.- Regen adds one wood per
TREE_REGEN_INTERVAL(180 s); a fully depleted tree waits an extraTREE_DORMANT_PERIOD(300 s) before the first regrowth.
Berry bushes
Bushes are generated from FOREST_LEFT to FOREST_RIGHT (-585) with varied spacing. _generate_bushes always writes a guaranteed bush at TUTORIAL_BUSH_X (-840), overwriting any random bush on that tile, so the forest tutorial beat is always reachable.
get_bush_near(world_x, radius)returns the nearest non-depleted bush.harvest_bush(bush)takesChallengeManager.berry_harvest_amount()berries (capped by stock), adds them as food viaSurvivalManager.add_supplies, and sets aDORMANT_PERIODrest timer when stripped.- Regen (in
_process) accumulatesBerryBush.REGEN_RATEper second up toMAX_BERRIES, but only after anydormant_timerhas elapsed.
Pond
A single pond at POND_X (-1400) holds up to POND_MAX_WATER (100). harvest_pond(amount) returns the collected water (clamped to what remains) and emits pond_changed; the caller adds it to supplies. Regen adds POND_REGEN_RATE * ChallengeManager.pond_regen_mult() per second.
Boulders
Three forest boulders sit at BOULDER_POSITIONS (-1500, -900, -2930) with max stone amounts [50, 30, 42]. Each boulder's stone is a separate variable (boulder_stone, boulder_stone_2, boulder_stone_3); index 0 maps to the legacy save key boulder_stone. Accessors boulder_stone_at(idx) / _set_boulder_stone(idx, val) bridge the index-based API to those fields.
get_boulder_near(world_x)returns the X of the nearest harvestable boulder, orNAN.harvest_boulder(bx)removes 1–3 stone, adds it to inventory, and emitsboulder_harvested/mined_yield.devour_boulder(idx)empties a boulder and flagsboulders_devoured[idx]when a Maw front (the Two Fronts west crawl) grinds over it, soForestDisplaystops drawing it.
Determinism, reset, and load
Both generators seed their RNG with a fixed constant plus WorldManager.world_seed, so each fresh run's layout is unique but reproducible. The autoload-boot pass runs with seed 0 and is discarded by either load_from_save or reset(). reset() regenerates the forest and bushes and restores full boulder stock. load_from_save / load_bushes_from_save restore serialized state, skipping any legacy tree or bush that now falls inside the graveyard clearing.
ForestNode and BerryBush
scripts/forest/ForestNode.gd (class_name ForestNode, extends RefCounted) is a single tree: position (pixel coords, y=0), wood_amount, gather_time (3.0 s default), visual height, depleted, and regen_timer. create, to_dict, and from_dict handle construction and persistence.
scripts/forest/BerryBush.gd (class_name BerryBush) is a single bush with berry_amount, a fractional regen_accum (not saved), and a dormant_timer. Constants define its economy:
| Constant | Value | Meaning |
|---|---|---|
MAX_BERRIES |
20 | Full stock. |
HARVEST_MIN / HARVEST_MAX |
1 / 5 | Handful range per pick. |
GATHER_TIME |
1.5 | Seconds per harvest. |
REGEN_RATE |
0.05 | Berries/sec — full in ~6.7 min. |
DORMANT_PERIOD |
360.0 | Rest after full depletion (one day). |
from_dict clamps berry_amount to MAX_BERRIES so saves from before the 100 -> 20 capacity retune do not carry jumbo stocks.
RockManager: surface stone
scripts/world/RockManager.gd scatters ROCK_COUNT_MIN..ROCK_COUNT_MAX (6–9) surface boulders between ROCK_ZONE_LEFT (-720) and ROCK_ZONE_RIGHT (210), enough stone to bootstrap a pickaxe before underground mining. Positions are randomised each run with a minimum separation, and each rock carries ROCK_AMOUNT_MIN..ROCK_AMOUNT_MAX (3–6) stone.
generate(rng_seed)runs on_readyand again onreset_for_new_run.restore_from_save(rock_data)rebuilds rocks from saved{x, amount, depleted}dicts.get_rock_near(world_x, radius)returns the nearest non-depleted rock.get_gather_time(rock, tool_id)divides the rock'sgather_timeby the tool'sdig_speed_multiplier.harvest_one(rock)adds one stone to inventory, marks the rockdepletedat zero (emittingrock_depleted), and emitsrock_harvestedandmined_yield.
SurfaceRock (scripts/world/SurfaceRock.gd) is the per-rock record consumed by these methods; RockManager distinguishes these surface rocks from ForestManager's three larger forest boulders.
DiscoveryManager: underground pockets
scripts/world/DiscoveryManager.gd resolves discovery pockets — water, mushrooms, ore caches, treasure, and hazards — when a tile is dug. Definitions live in data/discoveries.json; each entry has layers, per-layer layer_chances (falling back to a flat chance), a reward_type, and a reward.
Deterministic per-tile resolution
Every roll hashes the tile position with _hash_salt(), which mixes WorldManager.world_seed with AstrolabeManager.get_uses_count() * _RESEED_EPOCH_PRIME. So discoveries are stable per tile within a run, the world seed scatters them afresh each new run, and each Astrolabe ritual reshuffles them entirely. Legacy saves that never fired the ritual load with seed 0 and reproduce their original layout.
get_tile_discovery(pos) returns the discovery id at a tile (or ""), backed by a _disc_cache that both dynamic inputs invalidate — the ritual hook and world-seed changes clear it. DiscoveryLayer queries this for every visible cell, so the cache matters; that render layer also caps its animated-marker count to an FPS-adaptive budget (the markers nearest the camera get the full pulsing/glowing art, the rest render as cheap static dots), so a dense post-ritual world can't sink the frame rate. Consumed tiles (consumed_positions) always return "".
Resolving on dig
_on_tile_dug (connected to EventBus.tile_dug) walks the discovery defs in order, each consuming a probability slice, and calls _apply_discovery on a hit. A fixed tutorial exception: GameConstants.TUTORIAL_POCKET_TILE always resolves as a root_cache holding exactly TUTORIAL_POCKET_ROOTS strange roots, before any hash roll.
_apply_discovery records the type for the run completion metric, resolves the reward (depth-scaled via layer_rewards when present, with array values decoded by _resolve_reward_value — [min, max] uniform or [min, max, exponent] power curve), then branches on reward_type:
reward_type |
Effect |
|---|---|
survival |
Adds food/water via SurvivalManager.add_supplies. Toxic sub-types drain instead (see below). |
inventory |
Adds materials to Inventory. Gold hoards scale by the Axel's chest_gold_mult. |
hazard |
Drains current/stored food and water via _apply_hazard_reward. |
maw_resonance |
Triggers MawController.resonance_surge (3–5 walls torn through). |
plague_spore |
Queues 2–4 staggered villager deaths over real time. |
Each discovery's effective spawn chance is computed by _discovery_chance: the layer base chance times the Astrolabe density multiplier (AstrolabeManager.discovery_mult_for), with water pockets additionally scaled by WeatherManager.water_pocket_mult().
Sub-hazards
Several survival discoveries carry a second, independently-hashed sub-roll:
- Mushrooms resolve a variety in two steps — a layer-gated toxic/safe pick, then a hash within the pool (uniform for toxic, depth-weighted quality bands for good varieties). Toxic varieties skip food and apply an illness via
_apply_mushroom_hazard; Death Cap and Destroying Angel are additionally depth-gated (they degrade to milder effects in shallow layers, only turning lethal deep). Mushroom spawn chance is boosted near a Water Pocket (_mushroom_effective_chance, 1- and 2-tile Chebyshev radius); that boost is a flat designed rate (0.15within one tile,0.08within two), so_discovery_chanceapplies it as a floor —max(base × mult, boost)— and never re-multiplies it by the Astrolabe discovery-density mult (which would otherwise triple it to0.45and, since water density also scales, blanket the mid layers with mushrooms). - Water Pockets and Mineral Springs can resolve to
poison(drains current water) orbrine(drains stored water first, then current); springs are more likely saline.get_tile_water_hazardexposes the same deterministic verdict for display. - The Plague Mask cosmetic (
ward_bad_ingest_chance) can shrug off any tainted ingest.
Plague deaths are staggered in _process at PLAGUE_DEATH_INTERVAL (120 s), each intercepted by MachineManager.apothecary_try_save() before emitting villager_died. reset() clears session state, consumed positions, the cache, and the plague queue.
Key files
| File | Role |
|---|---|
scripts/forest/ForestManager.gd |
Trees, bushes, pond, and forest boulders — generation, harvest, regen, persistence. |
scripts/forest/ForestNode.gd |
Single-tree record (ForestNode). |
scripts/forest/BerryBush.gd |
Single-bush record and berry economy constants (BerryBush). |
scripts/world/RockManager.gd |
Surface stone boulders — scatter, harvest, save/restore. |
scripts/world/DiscoveryManager.gd |
Underground discovery pockets — deterministic resolution, rewards, sub-hazards. |
data/discoveries.json |
Discovery definitions: layers, chances, reward types, rewards. |