--- title: "Sprint 16 — Client Briefing" description: "Zone ID extraction optimization, sprite camera angle adjustment" type: sprint status: archived sprint: 16 team: "client" --- # 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 ` 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 ```