docs(sprints): add sprint 16 "Converse" briefings

Plan 8 tickets across server (4), client (2), copy (1), visual (1).
Focus: two-way dialogue exchange, trust-gated gossip, sprite pipeline,
and housekeeping fixes. No carry-overs from sprint 15.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 15:56:10 +01:00
co-authored by Claude Opus 4.6
parent 07199c8ddc
commit e1cbcc243b
5 changed files with 430 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
# Sprint 16: Converse — Client Tasks
**Goal:** Dialogue becomes a two-way exchange — players pick options and the server responds; NPC information-giving deepens through trust and trait-shaped delivery; the sprite pipeline gets its correct art-direction angle; and housekeeping fixes land across all three active teams.
**Branch:** `client`
**Agents:** Stig (dev), Tyre (arch), Hoshe (QA)
## Carry-over from Sprint 15
None. Sprint 15 was 100% complete (16/16 done).
## New Tickets
| # | Title | Blocked by |
|---|-------|------------|
| #543 | Optimize zone_id extraction O(N) → O(1) dictionary lookup | — |
| #540 | Adjust sprite camera to D-019 angle (~15-20° from vertical) | #541 (visual, must land first) |
Use `db/connectors/ticket show <id>` for full details.
## Key Decisions
- `decisions/perception.md` — D-019 (top-down camera, angle amendment: ~15-20° from vertical, "the angle"), D-043 (visual style, functional warmth), D-049 (z-level rendering stack)
- `decisions/architecture.md` — D-066 (dual-scale grid: 0.5m sim, 1m visual, 2x retina), D-077 (zone temperature memory, server-tracked zone_id)
## Open Questions to Resolve Early
None blocking either ticket.
## Notes
### #543 — Optimize zone_id extraction O(N) → O(1) dictionary lookup
**What exists:** `client/scripts/autoloads/game_state.gd` `apply_snapshot()` function. The current zone_id extraction (lines 243-252) does a linear scan of `visible_tiles` on every snapshot to find the tile whose coordinates match the player position:
```gdscript
current_zone_id = ""
var _px := int(player_position.x)
var _py := int(player_position.y)
for _ztile in visible_tiles:
if _ztile is Dictionary and _ztile.get("x") == _px and _ztile.get("y") == _py:
current_zone_id = _ztile.get("zone_id", "")
break
```
Within the same `apply_snapshot()` call (lines 256-268), `visible_tiles` is already iterated to build `visible_positions` (a `Dictionary` of `Vector2i → true`). The optimization is to build a second dictionary `_tile_by_coord: Dictionary` (mapping `Vector2i → tile Dictionary`) in that same existing loop, then replace the O(N) zone_id scan with an O(1) lookup.
**What to deliver:**
1. Declare `_tile_by_coord` as a local variable in `apply_snapshot()` (or as a member var if AudioManager or others need tile data by coord)
2. In the existing `visible_tiles` iteration loop (lines 256-268), add: `_tile_by_coord[pos] = vtile`
3. Replace the O(N) zone_id scan (lines 243-252) with:
```gdscript
var player_pos_key := Vector2i(int(player_position.x), int(player_position.y))
var player_tile = _tile_by_coord.get(player_pos_key, null)
current_zone_id = player_tile.get("zone_id", "") if player_tile else ""
```
4. Remove the old loop block (lines 249-252)
**Gotcha:** The zone_id scan (lines 243-252) currently runs BEFORE the `visible_tiles` iteration that builds `visible_positions` (lines 256-268). You need to reorder: build `_tile_by_coord` in the main tile loop first, then do the zone_id lookup. The comment on line 244 ("O(1) via visible_positions dict would be ideal") confirms this was known and flagged by Tyre in a PR review — this ticket resolves it.
**Net-zero complexity:** The tile loop is already there. This adds one dict-set per tile in the existing loop, then removes the separate zone_id scan loop entirely. No behavioral change; the same `current_zone_id` value is produced.
**Test:** Verify `current_zone_id` is populated correctly after the refactor by checking the existing client P-tests or adding a unit test in gdUnit4.
### #540 — Adjust sprite camera to D-019 angle (~15-20° from vertical)
**Blocked by #541 (visual).** Do not start until Araminta's 3D sprite render pipeline is set up and has produced at least a test sprite at the new angle.
**What exists:** The Godot client renders entity, object, and wall sprites. Current sprites are rendered at 90° (pure top-down). D-019 amendment specifies "the angle": Camera3D at -72.5° from horizontal (= ~17.5° from vertical, the midpoint of the 15-20° range). This matches Rimworld's orthographic-with-art-tilt approach. The Godot gameplay camera stays orthographic — the tilt is entirely an art convention baked into sprites.
**What to deliver:**
Once #541 delivers the offline render pipeline and a set of test sprites at -72.5°:
1. Update `client/scripts/rendering/entity_renderer.gd` to reference the new sprite assets (path may change per Araminta's pipeline output structure)
2. Verify `client/scripts/rendering/tile_renderer.gd` and `world_renderer.gd` — wall/object sprites from #541 should slot in at the same visual tile size (64×64px runtime per D-043 resolution chain)
3. Confirm the z-sorting logic in `entity_renderer.gd` still produces correct results: entities south-facing should overlap objects on the same row correctly with the tilted perspective
4. Check that the fog shader in `fog_shader.gd` is unaffected — fog is screen-space and driven by the LOS mask, not by sprite perspective (D-019: "tile grid remains square/orthogonal, vision cone math remains pure 2D")
**Key constraint from D-066:** Entity sprites render across a 2×2 sim tile footprint. With the tilt, the south-facing face of entities/objects is now visible — confirm the sprite footprint is still contained within the 2×2 sim tile bounding box so interaction range (2 sim tiles) remains accurate.
**What NOT to change:** The `client/scripts/rendering/fog_shader.gd`, fog vision cone shape, or any LOS computation. Those are 2D sim-space and are unaffected by the art-direction angle.
## Dependency Chain
```
#543 (zone_id O(1)) → standalone, no dependencies
#541 (visual: 3D render pipeline) → #540 (sprite camera angle)
```
## PR Workflow
When ready to submit, create a PR with `tea` CLI. **All flags are required** to avoid TTY prompts (see CLAUDE.md "Gitea access" section):
```bash
tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(client): sprint 16 zone lookup + sprite angle" --description "body" --base main --head client
```
+90
View File
@@ -0,0 +1,90 @@
# Sprint 16: Converse — Copy Tasks
**Goal:** Dialogue becomes a two-way exchange — players pick options and the server responds; NPC information-giving deepens through trust and trait-shaped delivery; the sprite pipeline gets its correct art-direction angle; and housekeeping fixes land across all three active teams.
**Branch:** `copy`
**Agents:** Mellanie (author), Paula (narrative), Gestalt (systems)
## Carry-over from Sprint 15
None. Sprint 15 was 100% complete (16/16 done).
## New Tickets
| # | Title | Blocked by |
|---|-------|------------|
| #542 | Migrate line IDs from location-scoped to NPC-scoped namespace | — |
Use `db/connectors/ticket show 542` for full details.
## Key Decisions
- `decisions/content.md` — D-035 (tag taxonomy; Sprint 15 amendment: line ID namespace change from location-scoped to NPC-scoped), D-028 (dialogue architecture, line pool format)
- `decisions/process.md` — D-022 (process workflow)
## Notes
### #542 — Migrate line IDs from location-scoped to NPC-scoped namespace
**What exists:** All dialogue and monologue YAML files under `content/campaigns/main/systems/krenn/stations/sova/districts/transit/`. There are approximately 20 dialogue pool files and a set of monologue pool files. The current line ID scheme uses the location as prefix:
- Old: `the-terminal_d_039` (all NPCs at The Terminal share one ID sequence)
- New: `kael-davan_d_001`, `dock-worker_d_001` (each NPC has an independent sequence)
The D-035 Sprint 15 amendment confirmed this change. The collision that prompted it (`the-terminal_d_039` appeared in multiple NPC files) demonstrates the old scheme does not scale to procedurally generated populations.
**Dialogue pool files to update (location + NPC name):**
- `dialogue/maintenance-corridors/kael-davan.yaml`
- `dialogue/maintenance-corridors/ring-operative.yaml`
- `dialogue/maintenance-corridors/transit-worker.yaml`
- `dialogue/the-last-shift/bar-owner.yaml`
- `dialogue/the-last-shift/bar-regular.yaml`
- `dialogue/the-last-shift/bartender.yaml`
- `dialogue/the-last-shift/day-worker.yaml`
- `dialogue/the-last-shift/kael-davan.yaml`
- `dialogue/the-last-shift/pc-detective.yaml`
- `dialogue/the-last-shift/pc-smuggler.yaml`
- `dialogue/the-last-shift/sera-venn.yaml`
- `dialogue/the-terminal/courier.yaml`
- `dialogue/the-terminal/dock-worker.yaml`
- `dialogue/the-terminal/kael-davan.yaml`
- `dialogue/the-terminal/maintenance-tech.yaml`
- `dialogue/the-terminal/new-hire.yaml`
- `dialogue/the-terminal/pc-detective.yaml`
- `dialogue/the-terminal/pc-smuggler.yaml`
- `dialogue/the-terminal/scheduler.yaml`
- `dialogue/the-terminal/shift-supervisor.yaml`
**Monologue files:** Check all `monologue/detective/*.yaml` and `monologue/smuggler/*.yaml` (if present) for any `m_` prefixed line IDs that use the old location-slug convention.
**ID scheme rules:**
For each NPC file, assign a fresh sequence starting at `_001`:
- Dialogue lines: `{npc-slug}_d_{###}` — e.g., `kael-davan_d_001`
- Monologue lines: `{npc-slug}_m_s_{###}` (smuggler) or `{npc-slug}_m_d_{###}` (detective) per D-032
The npc-slug is the NPC's name in kebab-case: `kael-davan`, `dock-worker`, `shift-supervisor`, `pc-smuggler`, `pc-detective`, etc.
Each file's IDs restart at `_001` — do not carry a running counter across files. The 999-line ceiling is per-NPC, not per-location.
**Schema:** The `dialogue-pool.schema.json` regex pattern already accepts the new format (`^[a-z][a-z0-9-]*`). Only the `description` field in the schema needs updating to reference the new convention. No code changes required — line IDs are opaque strings to the server.
**Authoring docs:** Update `docs/workshops/content-gap-analysis_v0_1/` or any voice guide that references the old `{location_slug}` convention. Check `docs/briefings/mellanie.md` and related copy briefings for any hardcoded examples.
**Golden tests:** Run `make pre-pr` after all renames to confirm no golden test snapshots reference old IDs. The content cross-reference validation (CI ticket #464, done) will catch any mismatches between schema and file content.
**This is a mechanical rename with zero content changes.** Do not rewrite lines, adjust trust tiers, or add new content in this ticket. Pure ID substitution only.
## Dependency Chain
```
#542 (line ID namespace migration) → standalone, no dependencies
```
## PR Workflow
When ready to submit, create a PR with `tea` CLI. **All flags are required** to avoid TTY prompts (see CLAUDE.md "Gitea access" section):
```bash
tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(content): migrate line IDs to NPC-scoped namespace (D-035)" --description "body" --base main --head copy
```
+73
View File
@@ -0,0 +1,73 @@
# Sprint 16: Converse — Joint Coordination
**Goal:** Dialogue becomes a two-way exchange — players pick options and the server responds; NPC information-giving deepens through trust and trait-shaped delivery; the sprite pipeline gets its correct art-direction angle; and housekeeping fixes land across all three active teams.
## Pre-Sprint
No blocking decisions or schema work required before implementation starts. All relevant decisions are confirmed (D-019, D-028, D-035, D-075, D-077). Sprint 16 can begin immediately.
| Item | Owner | Status |
|------|-------|--------|
| D-035 line ID scheme (NPC-scoped) | confirmed | done |
| D-075 layered confidence gate on trust | confirmed | done |
| D-019 camera angle amendment | confirmed | done |
| D-077 zone_id on VisibleTile | confirmed, implemented | done |
## Ticket Overview
| # | Title | Team | Blocked by |
|---|-------|------|------------|
| #538 | Arch: Move dialogue systems BridgePlugin → NpcPlugin | server | — |
| #539 | Server: implement DialogueResponse verb handler | server | #538 |
| #171 | Layer 3: Trust-gated gossip | server | — |
| #338 | Line variety tracker — prevent repeat dialogue | server | — |
| #543 | Optimize zone_id extraction O(N) → O(1) | client | — |
| #540 | Adjust sprite camera to D-019 angle | client | #541 |
| #542 | Migrate line IDs to NPC-scoped namespace | copy | — |
| #541 | 3D sprite render pipeline setup | visual | — |
## Cross-team Dependencies
**#541 (visual) → #540 (client):** Araminta must deliver at minimum a test entity sprite and a wall sprite at the -72.5° angle before Stig can validate the import path and z-sorting behavior. Early delivery of test sprites (even before the full pipeline doc is written) unblocks client work. Coordinate via the `visual``client` handoff — Araminta commits test sprites to `client/assets/sprites/` on the visual branch; Stig pulls and verifies fit in `entity_renderer.gd`.
**#538 (server) → #539 (server):** Sequential within the server team. #538 refactors registration; #539 adds a new system in the correct location. Dudley should complete and merge #538 before starting #539.
**#542 (copy) → server golden tests:** After #542 renames all line IDs, the server golden test fixtures in `server/tests/` may reference old line IDs (e.g., `the-terminal_d_039`). Run `make pre-pr` (which includes the content cross-reference validation and fixture staleness check) after #542 merges. If golden tests fail, the server team updates fixtures with `make golden-update`.
## Sprint Completion Proof
The sprint is complete when all of the following are observable:
1. **DialogueResponse closes the loop:** Player opens dialogue with any NPC, selects response option 1 or 2 (non-confrontation), and the NPC delivers a contextually appropriate follow-up line. No silent no-op on option selection. Observable in-game or via the test client replay.
2. **Trust-gated gossip unlocks progressively:** A player with `KnowsOf` confidence on a target NPC receives different (more forthcoming) dialogue lines than a player with `Suspects` confidence. Observable via the line previewer CLI or a gauntlet test room. The same NPC gives Surface-tier lines on first contact and Real-tier lines after investigation.
3. **No line repeats within 1 game-hour:** The player can initiate dialogue with the same NPC 5+ times in quick succession and observe that no dialogue line_id repeats. Observable via the WRONG button capture or server logs.
4. **Zone_id lookup is O(1):** `game_state.gd` `apply_snapshot()` no longer contains the linear tile scan for zone_id (lines 249-252 removed). `current_zone_id` populates correctly. Confirmed by code review + existing client tests passing.
5. **Sprite camera angle:** At least one entity sprite and one wall sprite are rendered at -72.5° from horizontal and display correctly in the Godot client without z-sorting artifacts. Observable in the rendering scene.
6. **Line ID migration complete:** `make pre-pr` passes with no content validation errors. No YAML file under `content/` contains line IDs with the old `{location_slug}_` prefix. Observable via CI.
## Test Plan Alignment (D-030)
Sprint 16 is in the Sprint 3-4 integration phase per D-030 test priorities.
- **#539 (DialogueResponse handler):** New system requires an integration test: player sends `DialogueResponse { response_id }`, assert `DialogueResponseBuffer` is populated with a follow-up line. Use existing test harness pattern from `dialogue.rs` game_loop tests.
- **#171 (Trust-gated gossip):** Requires a unit test: initialize player KG with `KnowsDetails` on target, run `process_talk_interaction`, assert a `Secret`-tier line is selected. Mirror with `Suspects` confidence, assert only `Surface`-tier lines are selected.
- **#338 (Line variety tracker):** Requires a regression test: send `Talk` 10 times to same NPC within 600 ticks, assert no line_id appears twice.
- **#543 (O(1) zone lookup):** Code-change-only; existing client tests cover `apply_snapshot()` behavior. Add assertion that `current_zone_id` is non-empty when the player is on a zone-tagged tile.
- **#542 (line ID rename):** Covered by `make pre-pr` content cross-reference validation (CI #464).
## PR Merge Order Recommendation
To minimize merge conflicts and test failures:
1. `copy` #542 — no code dependencies, merge first to unblock golden test validation
2. `server` #538 — plugin refactor, isolated change
3. `server` #171 and #338 — parallel, both isolated from #539
4. `server` #539 — depends on #538 being merged
5. `visual` #541 — can merge any time, gates #540
6. `client` #543 — standalone, merge any time
7. `client` #540 — merge after #541 lands
+100
View File
@@ -0,0 +1,100 @@
# Sprint 16: Converse — Server Tasks
**Goal:** Dialogue becomes a two-way exchange — players pick options and the server responds; NPC information-giving deepens through trust and trait-shaped delivery; the sprite pipeline gets its correct art-direction angle; and housekeeping fixes land across all three active teams.
**Branch:** `server`
**Agents:** Dudley (simulation), Tyre (arch), Hoshe (QA)
## Carry-over from Sprint 15
None. Sprint 15 was 100% complete (16/16 done).
## New Tickets
| # | Title | Blocked by |
|---|-------|------------|
| #538 | Arch: Move dialogue systems from BridgePlugin to NpcPlugin | — |
| #539 | Server: implement DialogueResponse verb handler | #538 |
| #171 | Layer 3: Trust-gated gossip | — |
| #338 | Line variety tracker — prevent repeat dialogue | — (unblocked: #305 done) |
Use `db/connectors/ticket show <id>` for full details.
## Key Decisions
- `decisions/content.md` — D-028 (dialogue architecture, four relational layers), D-035 (tag taxonomy, trust/access tiers), D-075 (layered confidence gate on trust tier)
- `decisions/architecture.md` — D-041 (knowledge graph data model, KnowledgeConfidence hierarchy), D-010 (no baking player identity into game loop)
## Notes
### #538 — Arch: Move dialogue systems from BridgePlugin to NpcPlugin
**What exists:** `server/src/bridge/mod.rs` (lines 181-187) registers `process_talk_interaction`, `process_walk_away`, and `process_confrontation_response` inside `BridgePlugin::build()`. These systems depend on NPC-layer resources (`TrustEventQueue`, `KnowledgeGraph`, `KnowledgeEventQueue`) but live in the bridge module, which is supposed to own only wire protocol concerns.
**What to deliver:** Move the three `.add_systems()` calls from `BridgePlugin::build()` to `NpcPlugin::build()` in `server/src/npc/mod.rs`. The function implementations in `server/src/simulation/dialogue.rs` do not move — only their registration site changes. The scheduling constraints (`.after(process_player_input)`, `.before(compute_observer_snapshot)`) must be preserved exactly. `NpcPlugin` already has access to `TrustEventQueue` and other NPC resources (it `init_resource`s them), so no new resource registration is needed.
**Gotcha:** The `game_loop` integration tests in `dialogue.rs` that manually set up `TrustEventQueue` were the signal that this was misplaced. After the move, those tests should no longer need the manual init. Verify no test breaks.
**Integration point for #539:** This must merge before #539 starts, since #539 adds a new dialogue system that should register in `NpcPlugin` from the start.
### #539 — Server: implement DialogueResponse verb handler
**What exists:** `process_talk_interaction` in `server/src/simulation/dialogue.rs` handles the initial `Talk` verb. It selects a line via the 4-layer pipeline and writes to `DialogueResponseBuffer`. The client sends `DialogueResponse { response_id }` when the player picks an option, but no server system consumes it. `PlayerAction::DialogueResponse` is already defined in the bridge types.
**What to deliver:** A new system `process_dialogue_response` in `server/src/simulation/dialogue.rs` that:
1. Reads `DialogueResponse` actions from the player input queue
2. Looks up the NPC's `DialogueProfile` (location, role) and active `KnowledgeGraph` state
3. Runs the same 4-layer filtering pipeline (`query_dialogue` on `LinePoolIndexResource`) to select a follow-up line based on the response_id context
4. Writes the result to `DialogueResponseBuffer` for snapshot inclusion
5. For conversations that have ended (no follow-up lines), clears `ActiveDialogue`
**Key design constraints from D-028 + D-062:** Dialogue options the player hasn't unlocked are invisible — so `response_id` will only arrive for options that were legitimately sent. No validation needed beyond confirming the response_id maps to a known dialogue context. The server does not need to validate "was this a valid choice?" — if the client sent it, it was shown.
**Integration with #171:** If trust-gated gossip (#171) is complete in the same sprint, this system should also route `Real`/`Secret` tier responses through the trust-gated pipeline. If #171 lands after, stub with `Surface`-only for now.
**Register in NpcPlugin** (after #538 lands).
### #171 — Layer 3: Trust-gated gossip
**What exists:** The trust tier gate is already fully implemented. `relationship_to_trust()` in `server/src/simulation/dialogue.rs` (lines 199-216) takes `RelationshipState` and `KnowledgeConfidence` and returns `TrustTier` per D-075. The `LinePoolIndexResource` query pipeline in `content/mod.rs` accepts a `TrustTier` and filters lines accordingly. The content YAML files already carry `trust: real` and `trust: secret` tags on authored lines.
**What to deliver:** Three disclosure tiers per NPC role (surface / real / secret). The engine is ready — the gap is that `process_talk_interaction` may not be passing the correct `KnowledgeConfidence` from the observer's `KnowledgeGraph` to `relationship_to_trust()`. Verify in `process_talk_interaction` that:
1. The observer's `KnowledgeGraph` is queried for the NPC's `StableId`
2. `confidence_of(&target_sid)` is passed as the second arg to `relationship_to_trust()`
3. The resulting `TrustTier` filters lines in `query_dialogue`
If this path is already wired (check near line 345 in `dialogue.rs`), the ticket may be a content validation: confirm that authored lines with `trust: real` and `trust: secret` actually surface for a player with `KnowsOf+` / `KnowsDetails+` knowledge. Write a test that initializes a player KG with `KnowsDetails` on a target NPC, runs `process_talk_interaction`, and asserts a `Secret`-tier line was selected.
**Progressive revelation:** The same NPC gives different answers on first meeting (Surface only) vs. after investigation (Real/Secret unlocked). This is not a UI change — it is already handled by D-062's invisible locks. The engine must simply populate `DialogueResponseBuffer` with the tier-appropriate line.
### #338 — Line variety tracker — prevent repeat dialogue
**What exists:** `DialogueCooldownTracker` is already implemented as a `Component` in `server/src/simulation/dialogue.rs` (lines 78-105). It tracks `line_id → tick_used` in a `BTreeMap<String, u64>` with a 600-tick (1 game-hour) cooldown. `record()`, `is_on_cooldown()`, and `prune()` methods exist.
**What to deliver:** Wire the tracker into `process_talk_interaction`:
1. Query `DialogueCooldownTracker` on the player entity (it may already be queried — check the system signature near line 345)
2. After the 4-layer pipeline returns candidates, filter out lines where `tracker.is_on_cooldown(line_id, current_tick)` returns true
3. Call `tracker.record(selected_line_id, current_tick)` after selection
4. Call `tracker.prune(current_tick)` each time to prevent unbounded growth
If the tracker is already being queried but not used to filter, this is a 10-line change. If it is not queried at all, add it to the system signature and wire it in. Write a test that sends `Talk` repeatedly to the same NPC and asserts no line_id appears twice within `LINE_COOLDOWN_TICKS`.
## Dependency Chain
```
#538 (BridgePlugin → NpcPlugin refactor)
→ #539 (DialogueResponse verb handler)
#171 (Layer 3: Trust-gated gossip) → standalone, parallel track
[may inform #539 if both land same sprint]
#338 (Line variety tracker wiring) → standalone, parallel track
```
## PR Workflow
When ready to submit, create a PR with `tea` CLI. **All flags are required** to avoid TTY prompts (see CLAUDE.md "Gitea access" section):
```bash
tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(simulation): sprint 16 dialogue server" --description "body" --base main --head server
```
+69
View File
@@ -0,0 +1,69 @@
# Sprint 16: Converse — Visual Tasks
**Goal:** Dialogue becomes a two-way exchange — players pick options and the server responds; NPC information-giving deepens through trust and trait-shaped delivery; the sprite pipeline gets its correct art-direction angle; and housekeeping fixes land across all three active teams.
**Branch:** `visual`
**Agents:** Araminta (art direction)
## Carry-over from Sprint 15
None. Sprint 15 was 100% complete (16/16 done).
## New Tickets
| # | Title | Blocked by |
|---|-------|------------|
| #541 | 3D sprite render pipeline setup | — |
Use `db/connectors/ticket show 541` for full details.
## Key Decisions
- `decisions/perception.md` — D-019 (camera angle amendment: -72.5° from horizontal = ~17.5° from vertical, "the angle"; Camera3D in the offline 3D pipeline produces sprites at this angle), D-043 (visual style: functional warmth, clean 2D with bold silhouettes), D-044 (visual hierarchy: entity > object > structure, resolution chain 1024→256→64), D-049 (z-level rendering stack, 8 layers)
- `decisions/architecture.md` — D-066 (dual-scale grid: 64×64px visual tiles on 1m grid; entity sprites: 24×32px footprint within 64×64px visual tile)
## Notes
### #541 — 3D sprite render pipeline setup
**What to deliver:** Establish the offline 3D sprite render pipeline used to produce all entity, object, and wall sprites for the game. This pipeline is a Blender (or equivalent) render setup, not a runtime system.
**Camera spec (D-019 amendment):**
- Camera3D positioned at **-72.5° from horizontal** (= 17.5° from vertical, midpoint of 15-20° range)
- Orthographic projection (not perspective) to match the Godot gameplay camera model
- This is "the angle" — all sprites for v0.1 are rendered at this angle
**Output spec (D-043, D-044):**
- Source resolution: 1024×1024px (highest fidelity, for outline processing)
- Working resolution: 256×256px (outlines applied at 4-8px, dark blue-grey `#333340`)
- Runtime resolution: 64×64px (bilinear interpolation from 256×256)
- Entity sprites: 24×32px footprint within the 64×64px visual tile (D-044)
- Sprites are shape templates — no baked shadows, no baked lighting, no baked mood (lighting is applied at runtime by Godot's PointLight2D pipeline)
**Lighting rig:**
- Three-point studio rig (key, fill, rim) to produce clean silhouettes without baked directional shadows
- Do NOT bake lighting direction into sprites — the runtime Godot lighting system (PointLight2D per fixture) provides all scene lighting
- Keep surface colors flat and readable; the 3D render should produce crisp outlines and readable silhouettes, not mood-lit final frames
**Export process:**
- Document the render settings (resolution, camera angle, lighting rig) so any team member can re-render consistent sprites
- Export at least one entity test sprite (generic NPC silhouette) and one structural test sprite (wall segment) to validate the pipeline before full production
- Store render source files in the `visual` branch; export the runtime 64×64 sprites to `client/assets/sprites/` (or the established asset path) for the client team
**Blocks #540 (client):** The client team cannot implement the camera angle adjustment (#540) until this pipeline produces correctly-angled sprites. Deliver at minimum a test entity sprite and one wall sprite so the client team can begin #540 integration work.
**Key constraint from D-066:** Entity sprites render across a 2×2 sim tile footprint. The 24×32px entity footprint within a 64×64px visual tile must be respected — test sprites should fit this bounding box so the client's z-sorting logic (y-based sort) remains accurate.
## Dependency Chain
```
#541 (3D render pipeline setup) → #540 (client: sprite camera angle)
```
## PR Workflow
When ready to submit, create a PR with `tea` CLI. **All flags are required** to avoid TTY prompts (see CLAUDE.md "Gitea access" section):
```bash
tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(assets): 3D sprite render pipeline setup (D-019 angle)" --description "body" --base main --head visual
```