From 2ffcd9cd3f1a3f05d93fe9d6fb713fc1dd70c297 Mon Sep 17 00:00:00 2001 From: architect-bot Date: Thu, 20 Aug 2026 21:09:30 +0000 Subject: [PATCH] WIP checkpoint: PC2: Design the test level & player spawn approach --- docs/architecture/pc2_test_level.md | 194 ++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 docs/architecture/pc2_test_level.md diff --git a/docs/architecture/pc2_test_level.md b/docs/architecture/pc2_test_level.md new file mode 100644 index 0000000..0f4bd55 --- /dev/null +++ b/docs/architecture/pc2_test_level.md @@ -0,0 +1,194 @@ +# PC2 Test Level & Player Spawn — Design Note + +> **Status:** Decided (PC2) +> **Owner:** Architecture +> **Consumers:** the two PC2 implementation tasks — (1) build the test level, +> (2) build the player & spawn it. Both must be able to follow this note +> without further design decisions. + +## 1. Decision: static scene, not chunk streaming + +**The PC2 playable test level is a single, hand-authored, static Godot 3D +scene.** It is **not** built through the chunk streaming system +(`scripts/chunk_stream_manager.gd`, `chunk_scene.gd`, `chunk.gd`, +`chunk_data_decoder.gd`). + +This is a deliberate decision, not a default: + +- **The streaming system has never been exercised with real content.** It is a + faithful port of the `docs/architecture/world.md` format spec, but nothing in + the project produces or serves chunk payloads for it to consume. `scenes/main.tscn` + wires `chunk_stream_manager.gd` onto a bare `Node3D` with no `player` reference + and no `_server_rpc`, so at runtime it does nothing. There is no server-side + chunk authoring/streaming endpoint to integrate against. +- **PC2 scope is single-player, local-only, no networking.** The value of chunk + streaming is server-authoritative streaming around a moving player across a + network boundary. None of those preconditions exist in PC2, so exercising the + pipeline here would mean building a chunk *authoring + serving* path first — + clearly out of checkpoint scope. +- **A static scene is the lowest-risk way to get "spawn a player in a real 3D + level and walk around" working**, which is the actual PC2 outcome being tested. + +Chunk streaming is **not removed and not broken by this decision** — it stays as +an optional, later milestone. The static level is the PC2 proof of the *player* +and *camera/physics* half of the problem; chunk streaming remains the later proof +of the *world data* half. + +## 2. Where things live + +| Artifact | Path | Owned by | +|---|---|---| +| Test level scene | `scenes/worlds/test_level.tscn` | level task | +| Player scene | `scenes/player.tscn` | player task | +| Player controller script | `scripts/player.gd` | player task | +| Game composition scene | `scenes/game.tscn` | player task | + +- **`scenes/worlds/`** is the convention for all level scenes (it does not exist + yet; the level task creates it). Future levels follow `scenes/worlds/.tscn`. +- `scenes/player.tscn` is **reusable** across levels — it must not hard-code any + reference to `test_level`; it discovers the spawn point at runtime (see §3). + +## 3. Player spawn convention + +The level scene **must** contain a `Marker3D` named exactly **`PlayerSpawn`**. +That node's world transform is the spawn point. + +- `Marker3D` is a plain `Node3D` that shows a cross gizmo in the editor and + carries no gameplay behavior — it is the established Godot idiom for "position + hint." (Confirmed present in Godot 4.6.2.) +- The marker's **position** is authoritative; its rotation is ignored by the + player (spawn yaw is the player's concern, default forward is fine). +- Coordinate system: **Y-up, units = meters, 1 unit = 1 m.** The marker must sit + at least **1.0 m above the ground's top surface** (recommended `y = 1.0` where + the ground top is `y = 0`) so the player capsule does not spawn intersecting + the floor and get popped out by the physics solver. +- There is exactly **one** `PlayerSpawn` per level. (If a level ever needs + multiple spawns, that is a later convention — not PC2.) + +**Lookup rule (implemented by `player.gd`, not by the level):** + +``` +# in player.gd _ready() +var spawn := get_tree().current_scene.find_child("PlayerSpawn", true, false) +if spawn is Node3D: + global_position = spawn.global_position +else: + push_warning("player.gd: no PlayerSpawn marker found; spawning at origin") + global_position = Vector3(0, 1.0, 0) # fallback, do not crash +``` + +The lookup runs from the current scene root so it finds the marker whether it is +a direct child of the level instance or nested under a `Level` wrapper. The +fallback keeps the player runnable in a scene that forgot its marker, but a +**missing `PlayerSpawn` is a level bug** and the level task's smoke test must fail +on it (see §6). + +## 4. Composition (who puts level + player together) + +`scenes/game.tscn` is the PC2 playable entry point: + +``` +Game (Node3D) +├── Level (instance of res://scenes/worlds/test_level.tscn) +└── Player (instance of res://scenes/player.tscn) +``` + +- `Game` is a plain `Node3D` with **no script** — composition is done by scene + instantiation, not code, so there is nothing to keep in sync. +- The **level scene is self-contained and has zero dependency on the player** — + it loads and is smoke-testable on its own. The level task does not instance or + reference `player.tscn`. +- The **player scene is self-contained and has zero dependency on any specific + level** — it only requires that the *current scene* contains a `PlayerSpawn`. +- Ownership of `game.tscn` goes to the **player task**, which depends on the + level task having landed `test_level.tscn` first (chain with `depends_on`). + +## 5. Level content requirements (level task) + +`scenes/worlds/test_level.tscn`, root node `TestLevel` (`Node3D`). Required +children: + +| Node | Type | Notes | +|---|---|---| +| `Ground` | `StaticBody3D` | `BoxShape3D` (e.g. `40 × 1 × 40`) centered at `(0, -0.5, 0)` so the **top face is `y = 0`**; matching `MeshInstance3D` (`BoxMesh`) so it is visible and collidable. Use a `StaticBody3D` rather than a bare `PlaneMesh` so collision is explicit and reliable. | +| `Props` | `Node3D` | 3–5 simple props as `StaticBody3D` + `MeshInstance3D` using built-in primitives only (`BoxMesh`, `CylinderMesh`, `SphereMesh`). `assets/` is empty and PC2 must not require imported content. Positioned on the ground surface; scale freely. | +| `Sun` | `DirectionalLight3D` | Pitch it downward (~ -45°), enable shadows if the compatibility renderer allows; shadows are optional. | +| `WorldEnvironment` | `WorldEnvironment` | Ambient light and/or a simple background color. Keep it plain — the project uses `gl_compatibility`. | +| `PlayerSpawn` | `Marker3D` | Per §3. Recommended `(0, 1.0, 0)`. | + +Notes: + +- **Do not** add any `script` to the level. It is pure content. Any per-level + behavior belongs to the player (or a future game-mode script), not the level. +- Keep the whole level on the XZ plane around the origin; nothing off at extreme + coordinates, no thin walls the capsule can tunnel through. + +## 6. Player contract (player task) + +`scenes/player.tscn`, root `Player` (`CharacterBody3D`) + `scripts/player.gd`. + +Minimum for PC2: + +- **Body:** `CharacterBody3D` with a `CollisionShape3D` using `CapsuleShape3D` + (~`radius 0.3`, ~`height 1.8`) and a matching `CapsuleMesh` for visibility. +- **Camera:** a `Camera3D` child. First-person (camera at capsule head height) is + the lowest-risk choice; third-person is fine too but is more work. +- **Spawn:** per §3 — read `PlayerSpawn` from the current scene, fall back + gracefully. +- **Movement:** gravity + `move_and_slide()` so the player stands on `Ground`. + WASD/arrow locomotion is the hard requirement. Mouse-look is recommended but + may be split into its own task if the controller grows. +- **No networking, no avatar UI, no chunk code** — PC2 is local-only. Do not add + `MultiplayerAPI`, `ChunkStreamManager`, or any avatar customization here. + +The player must work when instanced under `Game` alongside a level; it must not +assume it is the only node in the scene. + +## 7. Entry point / how a human plays it + +- `scenes/game.tscn` is the PC2 playable scene. For playtesting, load it directly + (e.g. `godot --path . scenes/game.tscn`), or temporarily set + `application/run/main_scene` to it. +- **Wiring "Play → Join a world" in the existing `world_list` UI is out of scope + for this note** and out of scope for the level/player tasks. The current menu + flow ends at browse-worlds (PC1). If a checkpoint wants a clickable path into + the test level, that navigation wiring is its own small task — do not fold it + into level or player. + +## 8. Validation (both tasks must run this before "done") + +```sh +# one-time, first checkout only — builds the class/import cache +godot --headless --path . --import + +# then, to catch parse/load errors: +godot --headless --path . --quit +``` + +- **Level task:** additionally load `test_level.tscn` headlessly and assert it + (a) loads without error and (b) contains a `Marker3D` named `PlayerSpawn` + positioned ≥ 1.0 m above the ground top. A smoke script following the existing + `scripts/smoke_test_*.gd` pattern is the right vehicle. +- **Player task:** a smoke test that instantiates `game.tscn`, waits a frame, + and asserts the player's global position equals the `PlayerSpawn` position + (within a small epsilon). This is the check that proves the spawn convention + actually wired through — not just "the scene loads." + +## 9. Explicitly out of scope for PC2 + +- Networking / multiplayer / prediction (single-player, local-only). +- Chunk streaming integration and server-side chunk authoring/serving. +- Avatar customization UI and glTF avatar loading. +- Join-a-world navigation from the world list. +- Any content moderation / age-gating logic in the level. + +## 10. Summary + +| Concern | Decision | +|---|---| +| Level construction | Static hand-authored Godot 3D scene (chunk streaming deferred, not removed) | +| Level location | `scenes/worlds/test_level.tscn` | +| Spawn convention | `Marker3D` named `PlayerSpawn`, ≥ 1.0 m above ground, looked up by the player | +| Player | `scenes/player.tscn` (`CharacterBody3D`) + `scripts/player.gd`, reusable across levels | +| Composition | `scenes/game.tscn` instances `Level` + `Player`; no script | +| Entry point | Load `scenes/game.tscn` directly; UI join-wiring deferred | -- 2.52.0