# 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 |