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:

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.

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.

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.

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:

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.