# Top-down 2D Layered TileMapLayer stacks, gameplay as terrain painting, a component library, the one-clock day/night pattern. # Top-down 2D & sim construction: components, terrains, and clocks How top-down games (farming sims, RPGs, life sims) are assembled the Godot way: a component library glued by signals, gameplay that *paints terrain*, and systems that run off one game clock. Distilled from a studied complete farming game and verified live. `platformer-2d.md` covers side-view actors; this file is the top-down counterpart. ## Layer the world as a TileMapLayer stack One `TileMapLayer` per meaning, in draw order, under a Y-sorted root: ``` Level (Node2D, y_sort_enabled) └── GameTilemap (Node2D, y_sort_enabled) ├── Water / Grass / TilledSoil (flat ground, no Y-sort) ├── Undergrowth (under actors) ├── Overgrowth (y_sort_enabled) (sorts against actors: trees, posts) └── Objects (y_sort_enabled) ``` Y-sort only works when **every** node in the chain from the common ancestor down has `y_sort_enabled` (root, actor, and the layers that must interleave). Flat ground layers stay unsorted. Gameplay layers are separate on purpose: `TilledSoil` is its own layer so tilling, saving, and crop placement each talk to exactly one node. ## Gameplay paints terrain with `tilemap.set_terrain` Actions that change the ground (tilling, paths, corruption) are *terrain painting*: the engine's autotiler picks tiles whose peering bits connect with neighbors, so one command yields tiling patches. **Build** (verified live): ``` tilemap get-info --node-path TilledSoil # discover terrain_sets: [{id, terrains:[{id,name}]}] tilemap set-terrain --node-path TilledSoil --cells '[[4,2],[5,2],[5,3]]' --terrain-set 0 --terrain 0 tilemap set-terrain --node-path TilledSoil --cells '[[5,2]]' --terrain-set 0 --terrain -1 # erase ``` **Gotcha (live-verified):** a terrain must include an *island* tile, meaning terrain assigned with **no** peering bits. Without one, painting isolated cells places *nothing, silently*. Author terrains in the TileSet editor (peering bits are visual work); paint them from code/CLI. The in-game version is a **cursor component**: mouse → cell via `layer.local_to_map(layer.get_local_mouse_position())`, gate by `player.global_position.distance_to(layer.map_to_local(cell))` (reach), then `set_cells_terrain_connect([cell], set, terrain, true)`. Same shape for planting: instantiate a crop scene at `map_to_local(cell)` under a `CropFields` container, so the position lands on the grid while the node lives off it. ## The component library (one Area2D, one job, one signal) Build these once as saved scenes; every entity composes them (`scene.instance` + `node.connect`): | component | base | contract | |---|---|---| | HitComponent | Area2D | carries `current_tool` + `hit_damage`; lives on the *player* | | HurtComponent | Area2D | `@export var tool`; emits `hurt(damage)` only when the overlapping HitComponent's tool matches | | DamageComponent | Node | accumulates; emits `max_damage_reached` | | CollectableComponent | Area2D | on player touch: `InventoryManager.add_collectable(name)`, free parent | | InteractableComponent | Area2D | emits `interactable_activated/deactivated` on body enter/exit | | GrowthCycleComponent | Node | day-tick driven; emits `crop_maturity`, `crop_harvesting` | The **tool-gating** trick makes one hit system serve axe/hoe/watering-can: HurtComponent compares its expected tool against the hitter's current tool, which keeps the `if` forest out of the player. An entity is then pure wiring: a tree connects `hurt → damage.apply_damage`, `max_damage_reached → drop log scene + queue_free`. A crop connects day-ticks to sprite frames (growth stages are spritesheet frames, so `sprite.frame = growth_state` covers it and no animation is needed). Swinging a tool = enabling the HitComponent's collision shape during the swing state. ## The state machine as child nodes When states own *behavior* (not just animation), make the machine a node with one child per state. The inspector then doubles as your state editor: ```gdscript class_name NodeStateMachine extends Node # machine: collects NodeState children, @export var initial_node_state: NodeState # connects their `transition` signal, # _physics_process: current._on_physics_process(delta); current._on_next_transitions() class_name NodeState extends Node # state: signal transition # virtuals: _on_enter/_on_exit/_on_process/_on_physics_process/_on_next_transitions ``` Each state `@export`s the nodes it drives (player, sprite, a collision shape) and stays self-contained: `Walk` reads input and moves the body; `Tilling` plays the swing and returns to `Idle` when `!sprite.is_playing()`. Transition rule: states decide *when to leave* (`transition.emit("Walk")` inside `_on_next_transitions`), the machine decides *how*. Use this when actions have logic; use the AnimationTree expression machine (`platformer-2d.md`) when states differ only in animation. NPCs reuse the same machine with different states: `Idle` (Timer wait) ⇄ `Walk` (pick a wander target via `NavigationServer2D.map_get_random_point(agent.get_navigation_map(), agent.navigation_layers, false)`, steer with `NavigationAgent2D`; with avoidance on, set `agent.velocity` and move in `velocity_computed`, which is the safe-velocity handshake). ## One clock, many consumers (day/night) A `DayAndNightCycleManager` autoload owns time as **radians** (`TAU / minutes_per_day` per game minute, so one day = one full circle) and emits three granularities: `game_time(float)` every frame, `time_tick(day,hour,min)` per game-minute, `time_tick_day(day)` per day. Consumers pick their granularity: - **Lighting**: a `CanvasModulate` component samples a `GradientTexture2D` with `color = gradient.sample(0.5 * (sin(time - PI/2) + 1.0))`. The gradient *is* the whole lighting design (night blue → dawn gold → noon white → dusk); tune colors, not code. - **Crops**: `GrowthCycleComponent` advances one growth state per `time_tick_day` (only if watered), emits `crop_harvesting` after `days_until_harvest`. - **HUD clock**: renders on `time_tick`. Build: `project add-autoload`, then `node add --type CanvasModulate` + attach the component. ## Save = components + polymorphic Resources Each saveable node carries a `SaveDataComponent` (in group `save_data_component`) holding a typed Resource; a per-level `SaveLevelDataComponent` sweeps the group, calls `_save_data(node)` on each, and `ResourceSaver.save()`s the collected array to `user://game_data/save_.tres`. Polymorphism does the heavy lifting: subclasses of a `NodeDataResource` base (`_save_data(node)` / `_load_data(root)`) each know their own shape: - `SceneDataResource` stores `scene_file_path` + position; load re-instantiates and re-parents. - `TilemapDataLayerResource` stores a layer's `get_used_cells()`; load **repaints** them with `set_cells_terrain_connect`, so the autotiler rebuilds seams instead of restoring raw cells. Adding a saveable thing = new Resource subclass + drop the component on it. No central save switch statement. (For the diff-based alternative, see `gdscript-architecture.md` two-tier save.) ## Small patterns worth stealing - **Doors**: open/close = `collision_layer` swap (1 ↔ 2) + animation. The wall stays; the player's mask simply stops matching. No node juggling. - **Ability gating via dialogue/managers**: tools start disabled; the guide NPC's dialogue emits `give_crop_seeds` → `ToolManager.enable_tool_button(...)`. Progression is a signal, not a flag check. - **Manager autoloads stay thin**: `ToolManager` = selected enum + two signals; `InventoryManager` = a Dictionary + `inventory_updated`. State + signals, no game logic (see `gdscript-architecture.md` autoload tiering). - **Reward juice**: chest feeding tweens each item from inventory to the chest (`position`, then `scale`, then `queue_free` callback) with staggered `create_timer` delays, so consumption reads physically. - **Shader feedback**: chopping sets `material.set_shader_parameter("shake_intensity", 0.5)` for a second. Hit feedback without touching the transform. ## Checklist - Ground layers flat. Only layers that interleave with actors get `y_sort_enabled`, and the whole ancestor chain needs it too. - Ground-changing gameplay is terrain painting; the terrain has an island tile. - Entities are component compositions wired by signals; tools gate hits by enum match. - Behavior states are child nodes emitting `transition`; animation-only states use the AnimationTree. - One time authority emits frame/minute/day signals; consumers subscribe at their granularity. - Saveables carry components + typed Resources; tilemap state saves as cells, loads as a repaint. - `scene save` after edits; verify live state with `runtime eval`.