Files
settled-reach/client/tests/test_rendering.gd
T
jpmschweitzerandClaude Opus 4.6 aa3a3fea7d fix(client): address PR review — invalidation, constants, tests
Review feedback from Hoshe + Tyre on PR #8:

- Extract shared TILE_SIZE to Constants class_name (Tyre #5, Hoshe #7)
- Fix tile invalidation: detect tile/visibility count changes instead
  of one-shot dirty flag, supports chunk loading (Tyre #3, #4)
- Fog dirty tracking: only re-render when visible_positions changes
- Add bounds warning for unknown tile types (Hoshe #1)
- Add player-not-found warning in GameState (Hoshe #5)
- Use Image.fill_rect() instead of pixel loops (Tyre #10)
- Document _player_pos as reserved for fog decay #113 (Tyre #6)
- Add TODO(#130) for D-033 relationship colors (Hoshe #3, Tyre #8)
- Add class_name to EntityRenderer, FogRenderer, TileRenderer
- 20 new rendering tests (D-030 Layer 1): entity lifecycle, fog
  registration, tile type mapping, snapshot completeness, constants
- 65 total tests passing, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:15:42 +01:00

217 lines
7.6 KiB
GDScript

## D-030 Layer 1: Tests for rendering components (tile, fog, entity renderers)
## Validates renderers handle snapshot data correctly.
class_name TestRendering
extends GdUnitTestSuite
var EntityRendererScript = load("res://scripts/rendering/entity_renderer.gd")
var FogRendererScript = load("res://scripts/rendering/fog_renderer.gd")
var TileRendererScript = load("res://scripts/rendering/tile_renderer.gd")
# -- Test data matching Protocol decoded format --
var _test_tiles: Array = [
{"x": 0, "y": 0, "z": 0, "type": "floor"},
{"x": 1, "y": 0, "z": 0, "type": "wall"},
{"x": 2, "y": 0, "z": 0, "type": "door"},
{"x": 3, "y": 0, "z": 0, "type": "object"},
{"x": 0, "y": 1, "z": 0, "type": "floor"},
]
var _test_entities: Array = [
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}},
{"entity_id": 2, "x": 7.0, "y": 5.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
]
# -- Constants --
func test_tile_size_constant() -> void:
assert_that(Constants.TILE_SIZE).is_equal(32)
# -- GameState: tile and visibility data --
func test_game_state_stores_tiles() -> void:
GameState.apply_snapshot({"tick": 1, "tiles": _test_tiles})
assert_that(GameState.visible_tiles.size()).is_equal(5)
func test_game_state_stores_visible_positions() -> void:
var positions := [{"x": 5, "y": 5}, {"x": 6, "y": 5}]
GameState.apply_snapshot({"tick": 1, "visible_positions": positions})
assert_that(GameState.visible_positions.size()).is_equal(2)
assert_that(GameState.visible_positions.has(Vector2i(5, 5))).is_true()
assert_that(GameState.visible_positions.has(Vector2i(6, 5))).is_true()
func test_game_state_clears_old_visible_positions() -> void:
GameState.apply_snapshot({"tick": 1, "visible_positions": [{"x": 1, "y": 1}]})
assert_that(GameState.visible_positions.size()).is_equal(1)
GameState.apply_snapshot({"tick": 2, "visible_positions": [{"x": 2, "y": 2}, {"x": 3, "y": 3}]})
assert_that(GameState.visible_positions.size()).is_equal(2)
assert_that(GameState.visible_positions.has(Vector2i(1, 1))).is_false()
func test_game_state_warns_on_missing_player() -> void:
GameState.player_entity_id = 999
GameState.player_position = Vector2(5, 5)
GameState.apply_snapshot({"tick": 1, "entities": _test_entities})
assert_that(GameState.player_position).is_equal(Vector2(5, 5))
# -- SimBridge: test data completeness --
func test_sim_bridge_test_snapshot_has_tiles() -> void:
SimBridge._test_tick = 0
var snap = SimBridge._test_snapshot()
assert_that(snap.has("tiles")).is_true()
assert_that(snap.tiles.size()).is_greater(0)
var tile = snap.tiles[0]
assert_that(tile.has("x")).is_true()
assert_that(tile.has("y")).is_true()
assert_that(tile.has("type")).is_true()
func test_sim_bridge_test_snapshot_has_visible_positions() -> void:
SimBridge._test_tick = 0
var snap = SimBridge._test_snapshot()
assert_that(snap.has("visible_positions")).is_true()
assert_that(snap.visible_positions.size()).is_greater(0)
var pos = snap.visible_positions[0]
assert_that(pos.has("x")).is_true()
assert_that(pos.has("y")).is_true()
func test_sim_bridge_test_snapshot_has_player_entity() -> void:
SimBridge._test_tick = 0
var snap = SimBridge._test_snapshot()
var has_player := false
for entity in snap.entities:
if entity.kind.variant == "Player":
has_player = true
break
assert_that(has_player).is_true()
func test_sim_bridge_test_snapshot_has_npc() -> void:
SimBridge._test_tick = 0
var snap = SimBridge._test_snapshot()
var has_npc := false
for entity in snap.entities:
if entity.kind.variant == "Npc":
has_npc = true
break
assert_that(has_npc).is_true()
func test_sim_bridge_test_tiles_contain_all_types() -> void:
SimBridge._test_tick = 0
var snap = SimBridge._test_snapshot()
var types: Dictionary = {}
for tile in snap.tiles:
types[tile.type] = true
assert_that(types.has("floor")).is_true()
assert_that(types.has("wall")).is_true()
assert_that(types.has("door")).is_true()
# -- EntityRenderer: lifecycle --
func _make_entity_renderer() -> Node2D:
var renderer = Node2D.new()
renderer.set_script(EntityRendererScript)
add_child(renderer)
return renderer
func test_entity_renderer_creates_nodes() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities)
assert_that(renderer.entity_nodes.size()).is_equal(2)
assert_that(renderer.entity_nodes.has(1)).is_true()
assert_that(renderer.entity_nodes.has(2)).is_true()
renderer.queue_free()
func test_entity_renderer_removes_stale_entities() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities)
assert_that(renderer.entity_nodes.size()).is_equal(2)
renderer.update_entities([_test_entities[0]])
assert_that(renderer.entity_nodes.size()).is_equal(1)
assert_that(renderer.entity_nodes.has(1)).is_true()
renderer.queue_free()
func test_entity_renderer_positions_centered() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities([_test_entities[0]])
var node = renderer.entity_nodes[1]
var offset: float = (Constants.TILE_SIZE - 24) / 2.0
var expected_x: float = 5.0 * Constants.TILE_SIZE + offset
var expected_y: float = 5.0 * Constants.TILE_SIZE + offset
assert_that(node.position.x).is_equal_approx(expected_x, 0.01)
assert_that(node.position.y).is_equal_approx(expected_y, 0.01)
renderer.queue_free()
func test_entity_renderer_skips_missing_entity_id() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities([{"x": 1.0, "y": 1.0, "z": 0, "kind": {"variant": "Npc", "data": null}}])
assert_that(renderer.entity_nodes.size()).is_equal(0)
renderer.queue_free()
func test_entity_renderer_player_color_differs_from_npc() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities)
var player_node = renderer.entity_nodes[1] as ColorRect
var npc_node = renderer.entity_nodes[2] as ColorRect
assert_that(player_node.color != npc_node.color).is_true()
renderer.queue_free()
func test_entity_renderer_empty_entities_clears_all() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities)
assert_that(renderer.entity_nodes.size()).is_equal(2)
renderer.update_entities([])
assert_that(renderer.entity_nodes.size()).is_equal(0)
renderer.queue_free()
# -- FogRenderer: position registration --
func _make_fog_renderer() -> TileMapLayer:
var fog = TileMapLayer.new()
fog.set_script(FogRendererScript)
return fog
func test_fog_renderer_registers_positions() -> void:
var fog := _make_fog_renderer()
fog.register_tile_positions(_test_tiles)
assert_that(fog._all_tile_positions.size()).is_equal(5)
assert_that(fog._all_tile_positions.has(Vector2i(0, 0))).is_true()
assert_that(fog._all_tile_positions.has(Vector2i(3, 0))).is_true()
assert_that(fog._all_tile_positions.has(Vector2i(0, 1))).is_true()
fog.free()
func test_fog_renderer_clears_on_re_register() -> void:
var fog := _make_fog_renderer()
fog.register_tile_positions(_test_tiles)
assert_that(fog._all_tile_positions.size()).is_equal(5)
fog.register_tile_positions([{"x": 10, "y": 10, "z": 0, "type": "floor"}])
assert_that(fog._all_tile_positions.size()).is_equal(1)
assert_that(fog._all_tile_positions.has(Vector2i(0, 0))).is_false()
fog.free()
func test_fog_renderer_handles_empty_data() -> void:
var fog := _make_fog_renderer()
fog._initialized = true
fog.update_fog({}, Vector2.ZERO)
fog.register_tile_positions([])
assert_that(fog._all_tile_positions.size()).is_equal(0)
fog.free()
# -- TileRenderer: tile type constants --
func test_tile_type_map_covers_required_types() -> void:
var tile_script = TileRendererScript
var required := ["floor", "wall", "door", "object"]
for tile_type in required:
assert_that(tile_script.TILE_TYPE_MAP.has(tile_type)).is_true()