Files

119 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |
**Input lock:** `_canOrbit()` returns false when `UI.activeConversation` is true. All manual orbit input (controller stick, right-click drag, `center_camera`) is suppressed. If right-click was held when a conversation starts, the mouse is released automatically. The camera stays at its current position and the positioning math still runs, so it remains correctly placed relative to the (non-moving) player.
**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", "")`