## Summary
Sprint 2 "See" client-side rendering pipeline:
- **#116 Camera lock to character** — Camera2D smoothing at 2x zoom, locked to player (D-015)
- **#129 Tile rendering engine** — programmatic TileSet with floor/wall/door/object placeholders
- **#131 Fog overlay rendering** — three visibility states (visible/fog-edge/hidden) via TileMapLayer
- **Entity renderer fix** — protocol field mismatches corrected, entities centered in tiles
Also includes test environment (8x8 room + corridor) and near-black background for development without server.
Remaining client tickets (#130, #113) are blocked on joint design decisions (#358, #360).
## Tickets
Closes #116, #129, #131
## Test plan
- [ ] Run Godot in test mode — room with walls, door, corridor renders correctly
- [ ] Fog overlay dims corners and corridor, clear area around player
- [ ] Fog-edge transition visible between clear and fogged tiles
- [ ] Entities (white player, blue NPC) centered within tiles
- [ ] Camera smoothly follows player position
- [ ] Background is dark outside the tiled area
- [ ] No regressions in existing gdUnit4 tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Joint tasks with ticket IDs (#356-#360), dependency chains,
and sprint completion proof criteria for client team reference.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
entity_renderer.gd used wrong field names ("id" instead of
"entity_id", "type" instead of "kind.variant", "position" instead
of x/y). Now matches Protocol.decode_entity() output exactly.
Also centers entities (24x24) within 32px tiles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sprint 2 rendering pipeline: tiles (#129), fog (#131), camera (#116).
- tile_renderer.gd: programmatic TileSet with floor/wall/door/object
placeholder tiles, renders from snapshot tile data
- fog_renderer.gd: TileMapLayer overlay with three visibility states
(visible/fog-edge/hidden), computed from visible_positions data
- Camera2D: smoothing enabled (speed 6.0), 2x zoom, locked to player
- game_state.gd: stores visible_tiles and visible_positions from snapshots
- sim_bridge.gd: test data with 8x8 room, corridor, and Manhattan
distance visibility for development without server
- Scene render order: Tiles -> FogOverlay -> Entities
- Background clear color set to near-black for unexplored areas
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Functionally sound rendering implementation with good alignment to D-020 and D-015. Three critical gaps prevent approval: (1) no rendering component tests per D-030, (2) missing error handling for malformed snapshot data, and (3) fog edge calculation boundary inconsistency with the shadowcasting model.
#
File
Severity
Issue
1
tile_renderer.gd:~59
warning
Missing bounds validation and warnings for unknown tile types in update_tiles()
2
fog_renderer.gd:~80
critical
Fog edge includes 8-directional neighbors but D-011 shadowcasting may only propagate cardinally
3
entity_renderer.gd:~47
suggestion
Hardcoded colors don't match D-033 palette; add TODO(#130)
4
world_renderer.gd:~19
warning
_tiles_dirty never resets if first snapshot has empty tiles
5
game_state.gd:~28
suggestion
Player entity ID hardcoded to 1; add warning when player not found
6
sim_bridge.gd:~239
critical
Test snapshot needs updated fixture tests for new fields
7
main.gd:~16
suggestion
Magic number 32.0 should reference a constant
8
client/tests/
critical
Zero test coverage for 4 new rendering scripts (D-030 Layer 1 blocker)
9
world_renderer.gd:~24
suggestion
Fog update_fog() called every frame; add dirty tracking
10
fog_renderer.gd
warning
D-011 "fog returns over time" not implemented
Tyre (Architecture): REQUEST_CHANGES
Solid architectural foundation with excellent renderer separation (D-020). Four critical issues: (1-2) EntityKind wire format uses {variant, data} struct but Rust serializes as plain string; (3-4) tile invalidation bug prevents chunk-based exploration (D-012).
#
File
Severity
Issue
1
entity_renderer.gd:45-52
critical
entity_data.kind.variant won't work — Rust EntityKind serializes as plain string
2
sim_bridge.gd:249,255
critical
Test data uses {variant: "Player", data: null} but wire format is just "Player"
3
world_renderer.gd:17-24
critical
_tiles_dirty = false after first render — no invalidation for new chunks (D-012)
4
fog_renderer.gd:55-70
critical
register_tile_positions() only called once — new tiles won't get fog (D-011)
5
tile_renderer.gd:9
warning
TILE_SIZE duplicated in 3 files — extract to shared constants
6
fog_renderer.gd:55
warning
_player_pos unused — remove or document as reserved for fog decay
7
main.gd:37
warning
Camera smoothing uses Godot defaults with no explicit configuration
8
entity_renderer.gd:45-52
warning
No relationship-based color (D-033) — add TODO(#130)
9
fog_renderer.gd:66-72
suggestion
Fog edge neighbor iteration acceptable now; note for future optimization
10
tile_renderer.gd:42-75
suggestion
Use Image.fill_rect() instead of pixel-by-pixel loops
Verdict: CHANGES REQUESTED
Key blockers:
EntityKind wire format mismatch — will crash against real server
Tile invalidation — tiles only render once, chunk exploration broken
Zero rendering test coverage — D-030 violation
## Dual-Agent Review: client -> main
### Hoshe (Code Quality): REQUEST_CHANGES
Functionally sound rendering implementation with good alignment to D-020 and D-015. Three critical gaps prevent approval: (1) no rendering component tests per D-030, (2) missing error handling for malformed snapshot data, and (3) fog edge calculation boundary inconsistency with the shadowcasting model.
| # | File | Severity | Issue |
|---|------|----------|-------|
| 1 | tile_renderer.gd:~59 | warning | Missing bounds validation and warnings for unknown tile types in `update_tiles()` |
| 2 | fog_renderer.gd:~80 | critical | Fog edge includes 8-directional neighbors but D-011 shadowcasting may only propagate cardinally |
| 3 | entity_renderer.gd:~47 | suggestion | Hardcoded colors don't match D-033 palette; add TODO(#130) |
| 4 | world_renderer.gd:~19 | warning | `_tiles_dirty` never resets if first snapshot has empty tiles |
| 5 | game_state.gd:~28 | suggestion | Player entity ID hardcoded to 1; add warning when player not found |
| 6 | sim_bridge.gd:~239 | critical | Test snapshot needs updated fixture tests for new fields |
| 7 | main.gd:~16 | suggestion | Magic number `32.0` should reference a constant |
| 8 | client/tests/ | critical | Zero test coverage for 4 new rendering scripts (D-030 Layer 1 blocker) |
| 9 | world_renderer.gd:~24 | suggestion | Fog `update_fog()` called every frame; add dirty tracking |
| 10 | fog_renderer.gd | warning | D-011 "fog returns over time" not implemented |
### Tyre (Architecture): REQUEST_CHANGES
Solid architectural foundation with excellent renderer separation (D-020). Four critical issues: (1-2) EntityKind wire format uses `{variant, data}` struct but Rust serializes as plain string; (3-4) tile invalidation bug prevents chunk-based exploration (D-012).
| # | File | Severity | Issue |
|---|------|----------|-------|
| 1 | entity_renderer.gd:45-52 | critical | `entity_data.kind.variant` won't work — Rust `EntityKind` serializes as plain string |
| 2 | sim_bridge.gd:249,255 | critical | Test data uses `{variant: "Player", data: null}` but wire format is just `"Player"` |
| 3 | world_renderer.gd:17-24 | critical | `_tiles_dirty = false` after first render — no invalidation for new chunks (D-012) |
| 4 | fog_renderer.gd:55-70 | critical | `register_tile_positions()` only called once — new tiles won't get fog (D-011) |
| 5 | tile_renderer.gd:9 | warning | `TILE_SIZE` duplicated in 3 files — extract to shared constants |
| 6 | fog_renderer.gd:55 | warning | `_player_pos` unused — remove or document as reserved for fog decay |
| 7 | main.gd:37 | warning | Camera smoothing uses Godot defaults with no explicit configuration |
| 8 | entity_renderer.gd:45-52 | warning | No relationship-based color (D-033) — add TODO(#130) |
| 9 | fog_renderer.gd:66-72 | suggestion | Fog edge neighbor iteration acceptable now; note for future optimization |
| 10 | tile_renderer.gd:42-75 | suggestion | Use `Image.fill_rect()` instead of pixel-by-pixel loops |
### Verdict: CHANGES REQUESTED
**Key blockers:**
1. EntityKind wire format mismatch — will crash against real server
2. Tile invalidation — tiles only render once, chunk exploration broken
3. Zero rendering test coverage — D-030 violation
8/10 fixed. EntityKind format was correct all along (Protocol decoder normalizes). But count-based dirty tracking introduces 2 new issues.
Correction: Round 1 critical #1-2 (EntityKind wire format) were false positives — Protocol._decode_enum_variant() normalizes bare strings to {variant, data} structs. Current code is correct.
Count-based dirty tracking missed content changes when tile/visibility
count stayed the same (e.g., door opening, player moving through
corridor). Now re-renders all layers when GameState.current_tick
advances, which correctly handles every snapshot change.
Adds 3 tests for tick advancement and same-count-different-content
scenarios. 68 total tests, 0 failures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Round 2 fix applied (6ff6be9): replaced count-based dirty tracking with tick-based invalidation. Now re-renders all layers when GameState.current_tick advances — correctly handles door opens, object moves, and visibility shifts where count is unchanged. 3 new tests added (68 total, 0 failures).
Round 2 fix applied (6ff6be9): replaced count-based dirty tracking with tick-based invalidation. Now re-renders all layers when GameState.current_tick advances — correctly handles door opens, object moves, and visibility shifts where count is unchanged. 3 new tests added (68 total, 0 failures).
Reviewing fix commit: 6ff6be9 fix(client): use tick-based invalidation for world renderer
Hoshe (Code Quality): APPROVE
Tick-based invalidation fully resolves both round 2 critical issues. 3 new tests cover tick advancement and same-count-different-content scenarios. 68 total tests, 0 failures. No new issues.
Tyre (Architecture): APPROVE
Tick-based invalidation is the architecturally correct approach — aligns client invalidation with the server's natural tick boundary. D-010/D-012/D-020 compliant. All scenarios validated (door open, chunk loading, player movement, z-level changes). Performance overhead negligible at v0.1 scale.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Sprint 2 "See" client-side rendering pipeline:
Also includes test environment (8x8 room + corridor) and near-black background for development without server.
Remaining client tickets (#130, #113) are blocked on joint design decisions (#358, #360).
Tickets
Closes #116, #129, #131
Test plan
🤖 Generated with Claude Code
entity_renderer.gd used wrong field names ("id" instead of "entity_id", "type" instead of "kind.variant", "position" instead of x/y). Now matches Protocol.decode_entity() output exactly. Also centers entities (24x24) within 32px tiles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>Dual-Agent Review: client -> main
Hoshe (Code Quality): REQUEST_CHANGES
Functionally sound rendering implementation with good alignment to D-020 and D-015. Three critical gaps prevent approval: (1) no rendering component tests per D-030, (2) missing error handling for malformed snapshot data, and (3) fog edge calculation boundary inconsistency with the shadowcasting model.
update_tiles()_tiles_dirtynever resets if first snapshot has empty tiles32.0should reference a constantupdate_fog()called every frame; add dirty trackingTyre (Architecture): REQUEST_CHANGES
Solid architectural foundation with excellent renderer separation (D-020). Four critical issues: (1-2) EntityKind wire format uses
{variant, data}struct but Rust serializes as plain string; (3-4) tile invalidation bug prevents chunk-based exploration (D-012).entity_data.kind.variantwon't work — RustEntityKindserializes as plain string{variant: "Player", data: null}but wire format is just"Player"_tiles_dirty = falseafter first render — no invalidation for new chunks (D-012)register_tile_positions()only called once — new tiles won't get fog (D-011)TILE_SIZEduplicated in 3 files — extract to shared constants_player_posunused — remove or document as reserved for fog decayImage.fill_rect()instead of pixel-by-pixel loopsVerdict: CHANGES REQUESTED
Key blockers:
Review fixes applied in
aa3a3fe— all critical/warning items addressed. See commit for details.Dual-Agent Review Round 2: client -> main
Reviewing fix commit:
aa3a3fe fix(client): address PR review — invalidation, constants, testsHoshe (Code Quality): APPROVE
All 10 round 1 issues addressed. 20 new tests added (65 total passing). Constants centralized, warnings added, documentation improved. No new issues.
Tyre (Architecture): REQUEST_CHANGES
8/10 fixed. EntityKind format was correct all along (Protocol decoder normalizes). But count-based dirty tracking introduces 2 new issues.
Correction: Round 1 critical #1-2 (EntityKind wire format) were false positives — Protocol._decode_enum_variant() normalizes bare strings to
{variant, data}structs. Current code is correct.Recommended fix: Replace count-based tracking with tick-based invalidation. ~10 lines.
Verdict: CHANGES REQUESTED
One remaining blocker: count-based dirty tracking will silently fail when tile data changes without count changing. Small fix needed.
Round 2 fix applied (
6ff6be9): replaced count-based dirty tracking with tick-based invalidation. Now re-renders all layers when GameState.current_tick advances — correctly handles door opens, object moves, and visibility shifts where count is unchanged. 3 new tests added (68 total, 0 failures).Dual-Agent Review Round 3: client -> main
Reviewing fix commit:
6ff6be9 fix(client): use tick-based invalidation for world rendererHoshe (Code Quality): APPROVE
Tick-based invalidation fully resolves both round 2 critical issues. 3 new tests cover tick advancement and same-count-different-content scenarios. 68 total tests, 0 failures. No new issues.
Tyre (Architecture): APPROVE
Tick-based invalidation is the architecturally correct approach — aligns client invalidation with the server's natural tick boundary. D-010/D-012/D-020 compliant. All scenarios validated (door open, chunk loading, player movement, z-level changes). Performance overhead negligible at v0.1 scale.
Verdict: APPROVED
All issues from rounds 1-3 resolved. PR ready to merge.
Pull request closed