fix(assets): purge stale 5-layer/peripheral references from fog spec

Round 3 review fixes — thorough spec cleanup:
- Remove visibility_sectors from data flow (peripheral removed in #569)
- Remove player_pos uniform (cone center implicit in visibility_tex)
- Update FogState pseudocode: remove sector step, add zone tint step
- Update lifecycle diagram to match single update_from_state() call
- Fix "5-layer fog" → "3-state fog" in Files to Create and impl notes
- Mark zone tint open question as resolved (Sprint 22, D-077)
- Document filter_nearest rationale on zone_tint_tex (D-073 hard zones)
- Note low-saturation tint is intentional per D-046 Hopper test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 23:26:30 +01:00
co-authored by Claude Opus 4.6
parent a1bdab949b
commit 2f22fbe4c1
3 changed files with 22 additions and 21 deletions
+2
View File
@@ -17,6 +17,8 @@ const EXP_VISIBLE: int = 255 # Currently in LOS — clear (written each frame
# Zone temperature tints (D-059 + D-046, Sprint 22) — keyed by zone_id string from server.
# Matches audio_manager.gd ZONE_ASSETS zone_id strings for consistent zone semantics.
# Low saturation is intentional (D-046): tints are subtle — distinguishable as warm/cool/neutral
# in side-by-side comparison, not garish. The "Hopper test" validates this.
# Colors are dark tints used as the fog overlay in the deep fog zone:
# hub/workplace: #1a1f2e (cool blue-dark — terminal, institutional)
# bar: #2a1f15 (warm amber-dark — social, inhabited)
+1 -1
View File
@@ -12,7 +12,7 @@ shader_type canvas_item;
uniform sampler2D visibility_tex : filter_linear, repeat_disable;
uniform sampler2D exploration_tex : filter_linear, repeat_disable;
uniform sampler2D zone_tint_tex : filter_nearest, repeat_disable;
uniform sampler2D zone_tint_tex : filter_nearest, repeat_disable; // nearest: zones have hard boundaries (D-073)
uniform sampler2D noise_tex : filter_linear, repeat_enable;
uniform vec2 rect_pos; // World-space position of the ColorRect (pixels)
uniform vec2 rect_sz; // World-space size of the ColorRect (pixels)
+19 -20
View File
@@ -58,9 +58,8 @@ The initial instinct is to put a fragment shader on the CanvasGroup itself (as `
```
Server (each tick)
└─ ObserverSnapshot
├─ visible_positions: Dictionary<Vector2i, true> (LOS result)
─ visibility_sectors: Dictionary<Vector2i, "Forward"|"Peripheral">
└─ visible_tiles: Array<{x, y, z, type}> (known map extent)
├─ visible_positions: Dictionary<Vector2i, true> (LOS result, forward cone only)
─ visible_tiles: Array<{x, y, z, type, zone_id}> (known map extent + zone metadata)
GameState (autoload)
└─ Stores all above
@@ -78,7 +77,6 @@ FogOverlay (Node2D) [fog_shader.gd]
│ uniform sampler2D exploration_tex; # Historical explored
│ uniform sampler2D zone_tint_tex; # Zone temperature colors
│ uniform float time; # For noise animation
│ uniform vec2 player_pos; # Vision cone center
│ uniform vec2 map_offset; # World-to-texture mapping
│ uniform vec2 map_size; # Texture dimensions in tiles
└─ FogEntities (Node2D)
@@ -107,13 +105,13 @@ var zone_tint_texture: ImageTexture
func update_from_state() -> void:
# Called every tick by fog_shader.gd
# 1. Resize textures if map_bounds changed
# 1. Resize textures if map_bounds changed (grow-only)
# 2. Clear visibility_image to 0 (black)
# 3. Write visible_positions from GameState → red channel = 255
# 4. Write visibility_sectors: Forward = 255, Peripheral = 180
# 5. Update exploration_image: any currently-visible pixel → 255,
# previously-visible pixels decay toward 128 over time
# 6. Upload images to textures
# 3. Write visible_positions from GameState → red channel = 255 (forward cone only)
# 4. Update exploration_image: visible pixels → 255,
# tiles leaving LOS decay to 128 (EXP_EXPLORED)
# 5. Update zone_tint_image: write zone_id → temperature color per tile
# 6. Upload changed images to textures
```
**Performance note:** `Image.set_pixel()` in a loop is ~0.05ms for 400 tiles. Acceptable. For larger maps, switch to `Image.set_data()` with a pre-built `PackedByteArray`.
@@ -202,13 +200,14 @@ Fog entities are NOT shader effects — they're GDScript-spawned sprites under `
```
Per tick (in _process or on snapshot signal):
1. FogState.update_visibility(GameState.visible_positions, GameState.visibility_sectors)
Write visibility_image, upload to visibility_texture
2. FogState.update_exploration(GameState.visible_positions)
Mark visible tiles as explored, apply decay to non-visible explored tiles
Upload to exploration_texture
3. FogOverlay._process():
→ Update shader uniforms (visibility_tex, exploration_tex, time, player_pos)
1. FogState.update_from_state()
Grow bounds if new tiles visible
→ Write visibility from GameState.visible_positions (forward cone)
Decay exploration: tiles leaving LOS → EXP_EXPLORED (128)
Write zone tint from visible_tiles[].zone_id
→ Upload changed textures
2. FogOverlay._process():
→ Update shader uniforms (visibility_tex, exploration_tex, zone_tint_tex, time)
→ Update fog entity positions/states from ObserverSnapshot fog entity data
```
@@ -228,7 +227,7 @@ Per tick (in _process or on snapshot signal):
|------|------|---------|
| `client/scripts/autoloads/fog_state.gd` | Autoload | Fog texture management, exploration persistence |
| `client/scripts/rendering/fog_shader.gd` | Script | FogOverlay node controller, shader uniform updates |
| `client/shaders/fog.gdshader` | Shader | Fragment shader for 5-layer fog |
| `client/shaders/fog.gdshader` | Shader | Fragment shader for 3-state fog (clear / explored / unexplored) |
| `client/scenes/fog_sound_ping.tscn` | Scene | Sound ping rings (deferred to Sprint 7+, #431) |
| `client/scenes/fog_entity_ghost.tscn` | Scene | Recognized entity ghost (deferred to Sprint 7+, #431) |
| `client/scenes/fog_entity_blob.tscn` | Scene | Unrecognized entity blob (deferred to Sprint 7+, #431) |
@@ -253,10 +252,10 @@ Per tick (in _process or on snapshot signal):
2. **Coordinate mapping is the hardest part.** Getting screen pixels → world tiles → texture UVs correct requires careful math. Test with a known map layout.
3. **Use Godot's NoiseTexture2D** resource for the Perlin noise rather than computing it in the shader. Pass it as a uniform. Scroll the UV offset with TIME for animation.
4. **The gradient edge** (Layer 1, 3-4 tile radius via 7x7 Gaussian) is the most visible quality differentiator. Use `smoothstep()` with the distance from the nearest non-visible tile. This may require encoding distance-to-edge in the visibility texture rather than binary 0/255.
5. **Fog entities are Sprint 7+ (#431).** For this sprint, just get the 5-layer fog shader working. The FogEntities node can be empty.
5. **Fog entities are Sprint 7+ (#431).** For this sprint, just get the 3-state fog shader working. The FogEntities node can be empty.
6. **Test with the existing sim_bridge test mode** — it provides a visible_positions Dictionary with a 4-tile radius and Bresenham LOS. Good enough to validate the shader.
## Open Questions
- **Q: How does the "maps app" data reach the client?** Layer 4 (unexplored + maps) needs to know which unexplored tiles the character's insert has map data for. This likely requires a new field in ObserverSnapshot (e.g., `mapped_tiles`). For Sprint 6, treat all explored tiles as "has maps" and all unexplored as "no maps" (layers 3 and 5 only, skip layer 4). Layer 4 is a v0.1.2+ feature.
- **Q: Zone temperature tints — where do they come from?** Currently no per-tile zone data in the snapshot. For Sprint 6, use a hardcoded default (neutral dark). Zone tints require server-side zone metadata.
- **Resolved (Sprint 22, #563):** Zone temperature tints come from `zone_id` field on `visible_tiles[]` in `ObserverSnapshot` (D-077). `fog_state.gd` maps zone_id strings to `ZONE_TINTS` color dictionary. Without zone metadata, defaults to neutral dark `#1a1a1a`.