# Overworld System ## Scene structure ``` OverworldScene (Node3D) ← always resident, managed by SCENE singleton └─ Map (Node3D) ← emptied and repopulated on each map change TestMap (Node3D) ← typical map root, extends Node3D ├─ Player (Entity) ← movementType = PLAYER, entityId = "player" ├─ NPC/object (Entity) ... ← interactType drives what happens on interact ├─ TestMapBase (StaticBody3D) ← reusable terrain plane (200×200, collision layer 1) └─ Camera3D (OverworldCamera) ← targetNode → Player ``` ## Map transitions Use `OVERWORLD.mapChange(path, destinationNodeName)` to switch maps. ```gdscript OVERWORLD.mapChange("res://overworld/map/SomeMap.tscn", "SpawnPoint") ``` Flow: fade-out begins → map loads on a background thread → when both complete, `OVERWORLD.mapChanged` fires → `OverworldScene` clears `Map` children and instances the new map → fade-in begins. The `destinationNodeName` is passed with the signal for the new map to use as a spawn point (not yet wired to player placement — see [stubs](stubs.md)). ## Entity All overworld objects (player, NPCs, items, triggers) are instances of [entity/Entity.tscn](../../overworld/entity/Entity.tscn) with different export values. | Export | Purpose | |---|---| | `entityId:String` | UUID; use the Inspector button to regenerate | | `movementType:MovementType` | `NONE` (static), `DISABLED`, or `PLAYER` (input-driven) | | `interactType:InteractType` | What happens when the player presses Interact nearby | | `dialogueResource:DialogueResource` | `.dialogue` file — required for `CONVERSATION` | | `dialogueTitle:String` | Dialogue section to start from (default `"start"`) | | `oneTimeItem:ItemResource` | Item granted on interact — required for `ONE_TIME_ITEM` | | `cutscene:CutsceneResource` | Cutscene to run — required for `CUTSCENE` | ### Interaction types | `InteractType` | Behaviour | |---|---| | `NONE` | Not interactable | | `CONVERSATION` | Runs `dialogueResource` from `dialogueTitle` via `DialogueAction` | | `ONE_TIME_ITEM` | Grants `oneTimeItem`, then frees the entity | | `CUTSCENE` | Queues and starts `cutscene` | | `BATTLE_TEST` | Starts a test battle (hardcoded enemy, for dev use) | To add a new interaction type: add a value to `Entity.InteractType`, then add the matching `match` branch in `EntityInteractableArea.onInteract()` ([entity/EntityInteractableArea.gd](../../overworld/entity/EntityInteractableArea.gd)). ### Collision layers | Area | Layer | Mask | Purpose | |---|---|---|---| | `EntityInteractingArea` | 0 | 2 | Player's reach — detects nearby interactables | | `EntityInteractableArea` | 2 | 0 | Entity's surface — detected by other reaches | The asymmetric setup means entities never trigger themselves. ## Movement `EntityMovement` (a child `Node` under `Components`) handles all physics each frame: 1. Apply gravity if airborne 2. Apply friction (`velocity.x/z *= delta * FRICTION`) 3. If `_canMove()` and `movementType == PLAYER`: read input, compute camera-relative direction, set velocity 4. `move_and_slide()` Movement is blocked (`_canMove() → false`) when `UI.dialogueActive`, `UI.TEXTBOX` is open, or `UI.GAME_MENU.isOpen()`. Camera-relative direction is derived from the active `Camera3D`'s basis — the camera's Y-zeroed and renormalized X/Z axes map input axes to world axes. The entity faces (`look_at`) the movement direction each frame. ## Camera `OverworldCamera` orbits around `targetNode` using yaw/pitch angles. It operates in two modes: `CameraMode.FREE` (stays where placed) and `CameraMode.CENTERED` (eases to orbit behind the player using a velocity-based approach). | Export | Default | Purpose | |---|---|---| | `targetNode:Node3D` | — | Node to orbit (assign Player in scene) | | `pivotOffset:Vector3` | `(0, 1.2, 0)` | Orbit point above entity origin | | `distance:float` | `7.0` | Orbit radius | | `minDistance:float` | `2.5` | Minimum camera distance (collision won't push closer) | | `pitchMin/Max:float` | `-80° / 70°` | Vertical clamp | | `orbitSensitivity:float` | `144.0` | Degrees/sec at full controller input | | `orbitAcceleration:float` | `600.0` | Controller ramp speed (deg/s²) | | `orbitFriction:float` | `10.0` | Controller/centering velocity decay rate | | `mouseSensitivity:float` | `0.3` | Right-click drag sensitivity | | `centeredDelay:float` | `1.0` | Seconds of walking + no camera input before switching to CENTERED | | `centeredFollowRate:float` | `0.75` | Base centering rate; scales up with angular distance from target | | `centeredMaxFollowRate:float` | `3.0` | Cap on the dynamic follow rate | | `centeredAcceleration:float` | `180.0` | How fast `_centerVelocity` ramps up toward target (deg/s²) | | `centeredMaxYawDiff:float` | `120.0` | If yaw offset exceeds this, centering is suppressed (e.g. player walking toward camera) | | `centeredPitch:float` | `30.0` | Target pitch in CENTERED mode | | `collisionMask:int` | `1` | Physics layers the camera avoids; entities are on layer 2 and excluded by default | **Mode transitions:** - FREE → CENTERED: after `centeredDelay` seconds of player movement with no camera input, or immediately via `center_camera` (G / LB) - CENTERED → FREE: any manual camera input (controller stick or right-click drag) **CENTERED mode behaviour:** - "Behind" direction is derived from the player's velocity vector (not facing direction), so centering tracks actual movement and eases out naturally as the player decelerates - Yaw uses a velocity model: `_centerVelocity` (deg/s) accelerates toward the target rate via `centeredAcceleration` and friction-decays via `orbitFriction` when the player stops — no sudden snap - Pitch recenters at 3× the yaw rate via direct lerp - If `abs(yawDiff) > centeredMaxYawDiff`, centering is suppressed and `_centerVelocity` decays instead Collision resolution uses a sphere-plane circle intersection so the camera satisfies both `minDistance` and terrain clearance simultaneously. ## Adding a new map 1. Create a new scene (`Node3D` root) in `overworld/map/` 2. Add `Entity` instances, set `interactType` and relevant exports in the Inspector 3. Instance `TestMapBase` (or your own terrain) as a child 4. Add a `Camera3D` with `OverworldCamera` script; set `targetNode` to the Player entity 5. Add a `Player` entity with `movementType = PLAYER` and `entityId = "player"` 6. Switch to it with `OVERWORLD.mapChange("res://overworld/map/YourMap.tscn", "")`