NEON-28: camera contracts, E6.M2 adjacency, occluder purge

pull/38/head
VinPropane 2026-04-10 22:11:40 -04:00
parent 7e674c3710
commit 15d304fa2c
9 changed files with 126 additions and 25 deletions

View File

@ -1,12 +1,23 @@
extends RefCounted
## Client-local camera snapshot for consumers (risk UX, UI, debug). Refreshed each frame by
## `isometric_follow_camera.gd`. No `class_name` — see repo Godot headless / CI notes.
## Client-local **per-frame** snapshot from the follow rig (`isometric_follow_camera.gd`).
## The rig creates **one** instance and assigns fields every `_process` after pose is final.
## No `class_name` — see repo Godot headless / CI notes.
##
## **Consumer contract (NEON-28):** Risk UX / HUD may read this object from the rigs
## `camera_state` getter. **Policy-style data** (occlusion enabled,
## zoom resource identity, pitch, presentation compass) is **not** duplicated here — read the
## rigs exports (`allow_yaw`, `presentation_yaw_deg`, resources) when needed. Future E6.M2 work
## may promote selected flags into this type; until then, keep a single source on the rig.
##
## **Authority:** Camera pose is client-only; the server must not use it for gameplay checks
## (`docs/decomposition/modules/client_server_authority.md` — E1.M2).
## Orbit yaw (rad) around the follow target **vertical axis**, **relative to** the rigs fixed
## presentation compass (`presentation_yaw_deg` on `isometric_follow_camera.gd`).
## This is **not** total camera compass—only the optional orbit delta.
## Prototype UX keeps this at **0** (`allow_yaw` false).
## **Not** total camera compass — only the optional orbit delta. Prototype UX keeps this at **0**
## (`allow_yaw` false). If orbit is enabled mid-project, HUD and world-anchored gameplay should
## treat framing as presentation + this delta (see E1.M2 rotation policy).
var yaw: float = 0.0
## Scene path to the follow anchor (typically `CharacterBody3D` / `Node3D`).

View File

@ -2,12 +2,14 @@ extends Node3D
## NEON-25: client-local isometric follow; updates [CameraState] each `_process`.
## NEON-26: discrete zoom bands via [member zoom_band_config]; wheel / `camera_zoom_*` actions.
## NEON-28: purge freed occluder keys from [member _occluder_overrides] before ray pass (see
## [method occluder_override_key_is_valid]).
## Pitch / roll are fixed by presentation exports; **orbit yaw** in state stays **0** while
## [member allow_yaw] is false (no rotate input bound). Future orbit: read relative input here,
## add to `_orbit_yaw_rad`, clamp with [member max_yaw_deg].
## TODO(E9.M1): map throttled product telemetry (for example `camera_zoom_changed`)
## to this signal when schema exists.
## TODO(E9.M1): map throttled product telemetry (for example `camera_zoom_changed`) to
## [signal zoom_band_changed] when schema exists; optional occlusion/perf counters per module doc.
signal zoom_band_changed(new_index: int, distance: float)
const CameraStateScript := preload("res://scripts/camera_state.gd")
@ -223,6 +225,31 @@ static func occlusion_policy_is_valid(policy: Resource) -> bool:
return policy != null and policy.get_script() == OcclusionPolicyScript and policy.is_valid()
## True if [param body] is a live [Node3D] suitable as an [member _occluder_overrides] key.
## Used before occlusion ray work and from tests (NEON-28).
static func occluder_override_key_is_valid(body: Variant) -> bool:
return body is Node3D and is_instance_valid(body)
func _restore_occluder_overrides_list(overrides: Array) -> void:
for entry in overrides:
var mi: MeshInstance3D = entry["mesh"]
if is_instance_valid(mi):
mi.set_surface_override_material(entry["surface"], entry["original"])
## Drop invalid keys from [member _occluder_overrides] and restore any surviving meshes.
func _purge_invalid_occluder_override_keys() -> void:
var keys: Array = _occluder_overrides.keys()
for body in keys:
if occluder_override_key_is_valid(body):
continue
var overrides: Variant = _occluder_overrides.get(body)
if overrides != null:
_restore_occluder_overrides_list(overrides)
_occluder_overrides.erase(body)
## Cast iterative rays from the smoothed eye to the player focus.
## Bodies in the [member occluder_group] that intersect the ray are faded;
## bodies that leave the path have their materials restored.
@ -231,6 +258,8 @@ func _update_occlusion(focus: Vector3) -> void:
_restore_all_occluders()
return
_purge_invalid_occluder_override_keys()
var policy = occlusion_policy
var space_state := get_world_3d().direct_space_state
var newly_blocking: Array[Node3D] = []
@ -309,14 +338,12 @@ func _apply_occluder_fade(body: Node3D) -> void:
func _restore_occluder(body: Node3D) -> void:
if not _occluder_overrides.has(body):
return
for entry in _occluder_overrides[body]:
var mi: MeshInstance3D = entry["mesh"]
if is_instance_valid(mi):
mi.set_surface_override_material(entry["surface"], entry["original"])
_restore_occluder_overrides_list(_occluder_overrides[body])
_occluder_overrides.erase(body)
## Restore all currently faded occluders (called on policy disable or node exit).
func _restore_all_occluders() -> void:
_purge_invalid_occluder_override_keys()
for body in _occluder_overrides.keys():
_restore_occluder(body)

View File

@ -1,4 +1,4 @@
# Static helpers for res://scripts/isometric_follow_camera.gd (NEON-25 framing, NEON-27 guard).
# Static helpers for res://scripts/isometric_follow_camera.gd (NEON-25 framing, NEON-27/28 guards).
extends GdUnitTestSuite
const IsoScript := preload("res://scripts/isometric_follow_camera.gd")
@ -62,3 +62,23 @@ func test_occlusion_policy_is_valid_false_when_disabled() -> void:
var p = OcclusionPolicyScript.new()
p.enabled = false
assert_that(IsoScript.occlusion_policy_is_valid(p)).is_false()
func test_occluder_override_key_is_valid_null() -> void:
assert_that(IsoScript.occluder_override_key_is_valid(null)).is_false()
func test_occluder_override_key_is_valid_non_node() -> void:
assert_that(IsoScript.occluder_override_key_is_valid(42)).is_false()
func test_occluder_override_key_is_valid_live_node3d() -> void:
var n := Node3D.new()
assert_that(IsoScript.occluder_override_key_is_valid(n)).is_true()
n.free()
func test_occluder_override_key_is_valid_false_after_free() -> void:
var n := Node3D.new()
n.free()
assert_that(IsoScript.occluder_override_key_is_valid(n)).is_false()

View File

@ -58,10 +58,10 @@ Deliver the foundational client runtime: movement, locked isometric camera, worl
### Slice 2 - Locked isometric camera
- Scope: E1.M2 end-to-end: follow-center, zoom bands, occlusion policy, no rotation.
- Scope: E1.M2 end-to-end: follow-center, zoom bands, occlusion policy, **yaw fixed in prototype UX** with **`CameraState.yaw`** and rig seam (`allow_yaw` / `presentation_yaw_deg`) documented for optional mid-project orbit ([E1_M2 — rotation policy](../modules/E1_M2_IsometricCameraController.md#mid-project-rotation-policy-practical-compromise)).
- Dependencies: E1.M1
- Acceptance criteria:
- Camera never rotates; zoom stays within configured bands; player remains readable during motion.
- **Orbit not exposed in UX** for players in prototype (`allow_yaw` default off); zoom stays within configured bands; player remains readable during motion; occlusion policy active; `CameraState` contract documented for dependents.
- Telemetry hooks: `camera_zoom_changed` (optional throttled), occluder stress markers if needed for perf.
### Slice 3 - Interaction, targeting, and ability input wiring

View File

@ -7,7 +7,7 @@
| **Module ID** | E1.M2 |
| **Epic** | [Epic 1 — Core Player Runtime](../epics/epic_01_core_player_runtime.md) |
| **Stage target** | Prototype |
| **Status** | In progress — follow + `CameraState` + **discrete zoom bands** + **occlusion policy** shipped ([NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25)[NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27)); hardening open ([NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)); see [dependency register](module_dependency_register.md) |
| **Status** | **Ready** (prototype slice) — follow + `CameraState` + **discrete zoom bands** + **occlusion** + pick-through ([NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25)[NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27), [NEON-30](https://neon-sprawl.atlassian.net/browse/NEON-30)); contracts + hardening ([NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)); see [dependency register](module_dependency_register.md) |
| **Jira** | Feature [NEON-10](https://neon-sprawl.atlassian.net/browse/NEON-10); prototype backlog stories under parent [NEON-1](https://neon-sprawl.atlassian.net/browse/NEON-1) — [Jira backlog](#jira-backlog) |
## Purpose
@ -39,10 +39,37 @@ Delivers an isometric follow camera that keeps the player readable during motion
| Contract | Role |
|----------|------|
| `CameraState` | Current follow target, zoom level, **yaw** (default `0`; optional future orbit), and policy flags for consumers (e.g. risk UX). |
| `CameraState` | Per-frame snapshot: follow path, effective distance, zoom band index, focus, **yaw** (orbit delta; prototype `0`). **Policy / presentation exports are not duplicated** in this type for the prototype — read the rig when needed ([Consumer contract](#consumer-contract-neon-28)). |
| `ZoomBandConfig` | Data-driven min/max or discrete zoom steps. |
| `OcclusionPolicy` | Rules for fading, dithering, or offset when geometry blocks the view. |
## Consumer contract (NEON-28)
Single place for dependents (e.g. **E6.M2**): what is on `CameraState` vs what stays on the rig. **Authority:** [E1.M2 — client vs server](client_server_authority.md#e1m2-isometriccameracontroller).
### `CameraState` fields (`client/scripts/camera_state.gd`)
Refreshed **every `_process`** on `IsometricFollowCamera` after pose is final. Same object each frame (rig-owned).
| Field | Meaning |
|--------|---------|
| `yaw` | Orbit delta (rad) around the follow target **relative to** `presentation_yaw_deg`**not** full compass. **0** while `allow_yaw` is false (prototype UX). |
| `follow_target_path` | `NodePath` to the follow anchor (e.g. player). |
| `distance` | Effective follow distance this tick (active band or fallback). |
| `zoom_band_index` | Active discrete band index, or **0** when zoom config is invalid / unused. |
| `focus_world` | World look-at focus last tick. |
### Rig-only reads (`client/scripts/isometric_follow_camera.gd`, `World/IsometricFollowCamera` in `main.tscn`)
| Export / node | Meaning |
|----------------|---------|
| `allow_yaw`, `max_yaw_deg` | Orbit seam (no rotate input bound in prototype). |
| `presentation_yaw_deg`, `pitch_elevation_deg`, `follow_distance`, `focus_vertical_offset` | Fixed isometric presentation (pitch/roll not exposed as free axes). |
| `zoom_band_config`, `occlusion_policy` | Resources driving bands and occlusion. |
| Child `Camera3D` | Active render camera (`ground_pick` uses viewport / `fallback_camera` from `main.gd`). |
**Telemetry:** Throttled `camera_zoom_changed` and occlusion/perf hooks are **TODO(E9.M1)** until an event schema exists (`zoom_band_changed` signal on the rig today).
## Module dependencies
- **E1.M1** — InputAndMovementRuntime: camera follows the player anchor derived from movement/position.
@ -55,7 +82,7 @@ Delivers an isometric follow camera that keeps the player readable during motion
See Epic 1 **Slice 2 — Locked isometric camera**: follow-center, zoom bands, occlusion policy, **yaw fixed for players in prototype** (implementation still models yaw per [mid-project rotation policy](#mid-project-rotation-policy-practical-compromise)); optional telemetry such as throttled `camera_zoom_changed` and perf stress markers for occluders.
## Implementation snapshot (NEON-25 + NEON-26 + NEON-27 + NEON-30, 2026-04-08 / 2026-04-09)
## Implementation snapshot (NEON-25 + NEON-26 + NEON-27 + NEON-30 + NEON-28, 2026-04-08 / 2026-04-10)
- **Godot:** `client/scripts/isometric_follow_camera.gd` on **`World/IsometricFollowCamera`**; child **`Camera3D`** (current). **`scripts/camera_state.gd`** — `RefCounted` tick snapshot (`yaw` = orbit component, **0** while **`allow_yaw`** is false; **`distance`** = effective follow distance; **`zoom_band_index`**; **`focus_world`**, **`follow_target_path`**).
- **Seam:** **`allow_yaw`** / **`max_yaw_deg`** on the rig; orbit would adjust **`_orbit_yaw_rad`** (no input bound yet). **Presentation** compass uses **`presentation_yaw_deg`** separately from orbit **`yaw`** in state.
@ -66,6 +93,8 @@ See Epic 1 **Slice 2 — Locked isometric camera**: follow-center, zoom bands, o
- **Click-through input (NEON-30):** `scripts/ground_pick.gd` unconditionally skips bodies in the `"occluder"` group during click-to-move ground-pick raycasts. When a cast hits an occluder the ray origin advances `OCCLUDER_PICK_THROUGH` meters past the hit point and continues, independent of whether `OcclusionPolicy` is currently fading that body. This keeps the `"occluder"` group as the sole tagging convention shared between the camera occlusion system and ground-pick input — no dedicated collision layer is added for this case.
- **Integration hardening (NEON-28):** [Consumer contract](#consumer-contract-neon-28) + `camera_state.gd` header; **E6.M2** adjacency notes ([E6_M2_ConsentAndRiskUxSignals.md](E6_M2_ConsentAndRiskUxSignals.md)); invalid / freed occluder bodies are **purged** from `_occluder_overrides` before each occlusion ray pass and before full restore (`occluder_override_key_is_valid`, tests in `isometric_follow_camera_test.gd`).
## Jira backlog
Parent epic: [NEON-1 — Epic 1 — Core Player Runtime](https://neon-sprawl.atlassian.net/browse/NEON-1).

View File

@ -13,6 +13,15 @@
Entry warnings, in-zone risk UI, and clear PvP state communication using eligibility from [E6.M1](E6_M1_PvPEligibilityAndFlagState.md) and camera/readability context from [E1.M2](E1_M2_IsometricCameraController.md).
## E1.M2 camera adjacency (NEON-28)
When implementing zone-risk HUD and related **client** presentation, treat [E1.M2](E1_M2_IsometricCameraController.md) as the framing source:
- **May read** the follow rigs **`camera_state`** snapshot (`client/scripts/camera_state.gd`) — field meanings and refresh cadence: [Consumer contract](E1_M2_IsometricCameraController.md#consumer-contract-neon-28).
- **Not duplicated in `CameraState` today:** occlusion on/off, bound zoom/occlusion resources, `presentation_yaw_deg`, `pitch_elevation_deg` — read **`IsometricFollowCamera`** exports (`client/scripts/isometric_follow_camera.gd`) when needed.
- **If `CameraState.yaw` becomes non-zero** (orbit enabled mid-project per E1.M2 rotation policy), screen-space layouts that assumed a fixed diagonal may need a compass / telegraph pass; keep **gameplay-adjacent** semantics **world-anchored** where possible.
- **Authority:** [Client vs server — E1.M2](client_server_authority.md#e1m2-isometriccameracontroller) — the server must **not** use client-reported camera pose for gameplay checks (targeting, LOS, zone rules).
## Responsibilities
- Show `RiskPrompt` on low-sec entry; drive `ZoneRiskState` / `PvPIndicatorState` in HUD.

View File

@ -47,7 +47,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not
| Module | Register status | Snapshot | Plans / pointers |
|--------|-----------------|----------|-------------------|
| E1.M1 | Ready | Prototype milestone **Done** ([NEON-9](https://neon-sprawl.atlassian.net/browse/NEON-9)). Authoritative `PositionState` + **MoveCommand** over HTTP (JSON v1 snap); **in-memory** store by default, **Postgres** when configured ([NEON-5](../../plans/NEON-5-implementation-plan.md)); shared **NpgsqlDataSource** disposed on host shutdown ([NEON-15](../../plans/NEON-15-implementation-plan.md)); Godot sync + path-follow ([NEON-4](../../plans/NEON-4-implementation-plan.md), [NEON-8](../../plans/NEON-8-implementation-plan.md)); **InteractionRequest** + horizontal range ([NEON-6](../../plans/NEON-6-implementation-plan.md)). Follow-on: prediction/reconciliation, Slice 1 telemetry, Protobuf wire per [contracts.md](contracts.md). | [NEON-3](../../plans/NEON-3-implementation-plan.md), [NEON-4](../../plans/NEON-4-implementation-plan.md), [NEON-5](../../plans/NEON-5-implementation-plan.md), [NEON-6](../../plans/NEON-6-implementation-plan.md), [NEON-7](../../plans/NEON-7-implementation-plan.md), [NEON-8](../../plans/NEON-8-implementation-plan.md), [NEON-15](../../plans/NEON-15-implementation-plan.md); `server/NeonSprawl.Server/Game/PositionState/`, `Game/Interaction/`; [server README](../../../server/README.md) |
| E1.M2 | In progress | **Slice 2 prototype stories shipped:** fixed-yaw isometric follow + `CameraState` seam ([NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25)); discrete zoom bands via `ZoomBandConfig` resource ([NEON-26](https://neon-sprawl.atlassian.net/browse/NEON-26)); `OcclusionPolicy` RayCast-based material fade with `"occluder"` group tagging ([NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27)). Open: integration hardening + dependent contract notes ([NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)). Client-local; no server use of camera pose. | [NEON-25](../../plans/NEON-25-implementation-plan.md), [NEON-26](../../plans/NEON-26-implementation-plan.md), [NEON-27](../../plans/NEON-27-implementation-plan.md); `client/scripts/isometric_follow_camera.gd`, `camera_state.gd`, `zoom_band_config.gd`, `occlusion_policy.gd`; [E1_M2_IsometricCameraController.md](E1_M2_IsometricCameraController.md) |
| E1.M2 | Ready | **Slice 2 prototype slice closed:** follow + `CameraState` ([NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25)); zoom bands ([NEON-26](https://neon-sprawl.atlassian.net/browse/NEON-26)); occlusion ([NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27)); occluder pick-through ([NEON-30](https://neon-sprawl.atlassian.net/browse/NEON-30)); contracts + hardening ([NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)). Client-local; no server use of camera pose. | [NEON-25](../../plans/NEON-25-implementation-plan.md), [NEON-26](../../plans/NEON-26-implementation-plan.md), [NEON-27](../../plans/NEON-27-implementation-plan.md), [NEON-28](../../plans/NEON-28-implementation-plan.md), [NEON-30](../../plans/NEON-30-implementation-plan.md); `client/scripts/isometric_follow_camera.gd`, `camera_state.gd`, `zoom_band_config.gd`, `occlusion_policy.gd`, `ground_pick.gd`; [E1_M2_IsometricCameraController.md](E1_M2_IsometricCameraController.md) |
---

View File

@ -13,11 +13,11 @@ Fleshed-out scope, contracts, and integration notes live in **per-module documen
| Module ID | Module Name | Depends On | Contract Needed | Phase Required | Status |
|---|---|---|---|---|---|
| E1.M1 | InputAndMovementRuntime | None | MoveCommand, PositionState, InteractionRequest | Prototype | Ready |
| E1.M2 | IsometricCameraController | E1.M1 | CameraState, ZoomBandConfig, OcclusionPolicy | Prototype | In progress |
| E1.M2 | IsometricCameraController | E1.M1 | CameraState, ZoomBandConfig, OcclusionPolicy | Prototype | Ready |
| E1.M3 | InteractionAndTargetingLayer | E1.M1 | TargetState, InteractableDescriptor, SelectionEvent | Prototype | Planned |
| E1.M4 | AbilityInputScaffold | E1.M3, E5.M1 | AbilityCastRequest, HotbarLoadout, CooldownSnapshot | Prototype | Planned |
**E1.M2 note:** Client follow rig + per-tick `CameraState` + **discrete zoom bands** (`ZoomBandConfig`, [NEON-26](https://neon-sprawl.atlassian.net/browse/NEON-26)) + **`OcclusionPolicy`** RayCast fade ([NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27)) are in repo ([NEON-25](https://neon-sprawl.atlassian.net/browse/NEON-25)[NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27)). Integration hardening remains ([NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)).
**E1.M2 note:** Prototype slice **Ready**: `client/scripts/isometric_follow_camera.gd` + child `Camera3D` on **`World/IsometricFollowCamera`** in `client/scenes/main.tscn`; per-tick **`CameraState`** (`client/scripts/camera_state.gd`); **`ZoomBandConfig`** ([NEON-26](https://neon-sprawl.atlassian.net/browse/NEON-26)); **`OcclusionPolicy`** ([NEON-27](https://neon-sprawl.atlassian.net/browse/NEON-27)); pick-through **`"occluder"`** convention ([NEON-30](https://neon-sprawl.atlassian.net/browse/NEON-30)); consumer contract + occluder lifecycle hardening ([NEON-28](https://neon-sprawl.atlassian.net/browse/NEON-28)). See [E1_M2_IsometricCameraController.md](E1_M2_IsometricCameraController.md).
### Epic 2 — Skills and Progression Framework

View File

@ -39,15 +39,15 @@
Jira acceptance criteria (verbatim intent):
- [ ] **Module dependency register / module doc:** E1.M2 **Ready** (or agreed status) with pointers to scripts/scenes.
- [ ] **`CameraState` contract** summarized in one place and matches module doc (**includes yaw**).
- [ ] **E6.M2 note:** what consumers may read vs undefined; how **yaw** affects camera-adjacent UX if non-zero later.
- [ ] **Epic 1 Slice 2** acceptance satisfied: prototype keeps **yaw fixed for players**, zoom within bands, readable motion, occlusion policy active; aligns with updated module wording (not “camera has no yaw,” but **yaw not exposed / remains default in UX**).
- [x] **Module dependency register / module doc:** E1.M2 **Ready** (or agreed status) with pointers to scripts/scenes.
- [x] **`CameraState` contract** summarized in one place and matches module doc (**includes yaw**).
- [x] **E6.M2 note:** what consumers may read vs undefined; how **yaw** affects camera-adjacent UX if non-zero later.
- [x] **Epic 1 Slice 2** acceptance satisfied: prototype keeps **yaw fixed for players**, zoom within bands, readable motion, occlusion policy active; aligns with updated module wording (not “camera has no yaw,” but **yaw not exposed / remains default in UX**).
**Supporting engineering checks (not a substitute for Jira AC):**
- [ ] Occluder invalid-key purge covered by tests or documented manual steps (see [Tests](#tests)).
- [ ] [epic_01_core_player_runtime.md](../decomposition/epics/epic_01_core_player_runtime.md) **Slice 2** bullets use the same **yaw / presentation** language as the module (replace over-simplified “no rotation” if it contradicts the rotation policy).
- [x] Occluder invalid-key purge covered by tests or documented manual steps (see [Tests](#tests)).
- [x] [epic_01_core_player_runtime.md](../decomposition/epics/epic_01_core_player_runtime.md) **Slice 2** bullets use the same **yaw / presentation** language as the module (replace over-simplified “no rotation” if it contradicts the rotation policy).
## Technical approach
@ -109,3 +109,8 @@ Jira acceptance criteria (verbatim intent):
- **`CameraState` policy flags:** Product may later require explicit booleans for risk UX; if E6.M2 text stays non-committal, keep flags out and document **rig as source** until a follow-on story.
- None otherwise.
## Shipped (2026-04-10)
- `camera_state.gd` consumer contract header; `isometric_follow_camera.gd` invalid occluder purge + `occluder_override_key_is_valid`; tests in `isometric_follow_camera_test.gd`.
- Docs: `E1_M2_IsometricCameraController.md` (Ready, consumer contract, NEON-28 snapshot), `E6_M2_ConsentAndRiskUxSignals.md` (E1.M2 adjacency), `module_dependency_register.md`, `documentation_and_implementation_alignment.md`, `epic_01_core_player_runtime.md` Slice 2.