## 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 TileRendererScript = load("res://scripts/rendering/tile_renderer.gd") var SoundIndicatorScript = load("res://scripts/rendering/sound_indicator_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}}, ] var _test_entities_v2: Array = [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}, "visibility": "Forward"}, {"entity_id": 2, "x": 7.0, "y": 5.0, "z": 0, "kind": {"variant": "Npc", "data": null}, "visibility": "Peripheral"}, ] # -- Constants -- func test_tile_size_constant() -> void: assert_that(Constants.TILE_SIZE).is_equal(32) # -- GameState: tile and visibility data -- func test_game_state_tick_advances() -> void: GameState.apply_snapshot({"tick": 1, "tiles": _test_tiles}) assert_that(GameState.current_tick).is_equal(1) GameState.apply_snapshot({"tick": 2, "tiles": _test_tiles}) assert_that(GameState.current_tick).is_equal(2) 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_same_count_different_tiles_new_tick() -> void: var tiles_a := [{"x": 0, "y": 0, "z": 0, "type": "floor"}] var tiles_b := [{"x": 0, "y": 0, "z": 0, "type": "door"}] GameState.apply_snapshot({"tick": 1, "tiles": tiles_a}) assert_that(GameState.visible_tiles[0].type).is_equal("floor") GameState.apply_snapshot({"tick": 2, "tiles": tiles_b}) assert_that(GameState.current_tick).is_equal(2) assert_that(GameState.visible_tiles[0].type).is_equal("door") func test_game_state_same_count_different_visibility_new_tick() -> void: GameState.apply_snapshot({"tick": 1, "visible_positions": [{"x": 1, "y": 1}]}) assert_that(GameState.visible_positions.has(Vector2i(1, 1))).is_true() GameState.apply_snapshot({"tick": 2, "visible_positions": [{"x": 2, "y": 2}]}) assert_that(GameState.current_tick).is_equal(2) assert_that(GameState.visible_positions.has(Vector2i(1, 1))).is_false() assert_that(GameState.visible_positions.has(Vector2i(2, 2))).is_true() func test_game_state_stores_game_time() -> void: var gt := {"day": 1, "time_of_day": 720, "day_phase": "Evening", "paused": false} GameState.apply_snapshot({"tick": 1, "game_time": gt}) assert_that(GameState.game_time.day).is_equal(1) assert_that(GameState.game_time.time_of_day).is_equal(720) func test_game_state_stores_player_facing() -> void: GameState.apply_snapshot({"tick": 1, "player_facing": "Southeast"}) assert_that(GameState.player_facing).is_equal("Southeast") func test_game_state_derives_visible_positions_from_visible_tiles() -> void: var vtiles := [ {"x": 5, "y": 5, "z": 0, "visibility": "Forward"}, {"x": 6, "y": 5, "z": 0, "visibility": "Peripheral"}, ] GameState.apply_snapshot({"tick": 1, "visible_tiles": vtiles}) assert_that(GameState.visible_positions.has(Vector2i(5, 5))).is_true() assert_that(GameState.visible_positions.has(Vector2i(6, 5))).is_true() assert_that(GameState.visibility_sectors[Vector2i(5, 5)]).is_equal("Forward") assert_that(GameState.visibility_sectors[Vector2i(6, 5)]).is_equal("Peripheral") func test_game_state_skips_malformed_visible_tiles() -> void: var vtiles := [ {"x": 5, "y": 5, "z": 0, "visibility": "Forward"}, null, "not_a_dict", {"z": 0}, # missing x, y {"x": 6, "y": 6, "z": 0, "visibility": "Peripheral"}, ] GameState.apply_snapshot({"tick": 1, "visible_tiles": vtiles}) # Only the 2 valid entries should be stored 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, 6))).is_true() assert_that(GameState.visibility_sectors.size()).is_equal(2) 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: ## Protocol uses "visible_tiles" (not "tiles") for test snapshot — updated from stale assertion. SimBridge.reset_test_state() var snap = SimBridge._test_snapshot() assert_that(snap.has("visible_tiles")).is_true() assert_that(snap.visible_tiles.size()).is_greater(0) var tile = snap.visible_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: ## Protocol uses "visible_tiles" for position data — visible_positions is derived client-side. ## Updated from stale assertion: TestHarness snapshot never had a top-level "visible_positions". SimBridge.reset_test_state() var snap = SimBridge._test_snapshot() assert_that(snap.has("visible_tiles")).is_true() assert_that(snap.visible_tiles.size()).is_greater(0) var vtile = snap.visible_tiles[0] assert_that(vtile.has("x")).is_true() assert_that(vtile.has("y")).is_true() func test_sim_bridge_test_snapshot_has_player_entity() -> void: SimBridge.reset_test_state() 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.reset_test_state() 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: ## Protocol uses "visible_tiles" — updated from stale "tiles" assertion. SimBridge.reset_test_state() var snap = SimBridge._test_snapshot() var types: Dictionary = {} for tile in snap.visible_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() func test_sim_bridge_test_snapshot_has_v2_fields() -> void: SimBridge.reset_test_state() var snap = SimBridge._test_snapshot() assert_that(snap.has("game_time")).is_true() assert_that(snap.has("player_facing")).is_true() assert_that(snap.has("visible_tiles")).is_true() assert_that(snap.visible_tiles.size()).is_greater(0) var vtile = snap.visible_tiles[0] assert_that(vtile.has("visibility")).is_true() # Entities should have visibility assert_that(snap.entities[0].has("visibility")).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] # Sprite2D renderer: ENTITY_OFFSET_X=0.0, ENTITY_OFFSET_Y=TILE_SIZE-ENTITY_HEIGHT=0.0 var expected_x: float = 5.0 * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_X var expected_y: float = 5.0 * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_Y 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() # Regression test for #345: protocol uses entity_id (not id). # An entity dict keyed with "id" (old/wrong format) must be silently dropped. func test_entity_renderer_rejects_old_id_field_format() -> void: var renderer := _make_entity_renderer() # Old (wrong) format: "id" instead of "entity_id" renderer.update_entities([{"id": 99, "x": 3.0, "y": 3.0, "z": 0, "kind": {"variant": "Npc", "data": null}}]) assert_that(renderer.entity_nodes.size()).is_equal(0) # Correct format: "entity_id" — should be accepted renderer.update_entities([{"entity_id": 99, "x": 3.0, "y": 3.0, "z": 0, "kind": {"variant": "Npc", "data": null}}]) assert_that(renderer.entity_nodes.size()).is_equal(1) assert_that(renderer.entity_nodes.has(99)).is_true() 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 Sprite2D var npc_node = renderer.entity_nodes[2] as Sprite2D assert_that(player_node.self_modulate != npc_node.self_modulate).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() # -- EntityRenderer: D-033 colors and v2 features -- func test_entity_renderer_player_uses_d033_color() -> void: var renderer := _make_entity_renderer() renderer.update_entities(_test_entities_v2) var player_node = renderer.entity_nodes[1] as Sprite2D assert_that(player_node.self_modulate).is_equal(Constants.ENTITY_COLOR_PLAYER) renderer.queue_free() func test_entity_renderer_npc_uses_unknown_teal() -> void: var renderer := _make_entity_renderer() renderer.update_entities(_test_entities_v2) var npc_node = renderer.entity_nodes[2] as Sprite2D assert_that(npc_node.self_modulate).is_equal(Constants.ENTITY_COLOR_UNKNOWN) renderer.queue_free() func test_entity_renderer_object_uses_grey() -> void: var renderer := _make_entity_renderer() var obj := [{"entity_id": 3, "x": 1.0, "y": 1.0, "z": 0, "kind": {"variant": "Object", "data": null}, "visibility": "Forward"}] renderer.update_entities(obj) var node = renderer.entity_nodes[3] as Sprite2D assert_that(node.self_modulate).is_equal(Constants.ENTITY_COLOR_OBJECT) renderer.queue_free() func test_entity_renderer_peripheral_entity_dimmed() -> void: var renderer := _make_entity_renderer() renderer.update_entities(_test_entities_v2) var npc_node = renderer.entity_nodes[2] assert_that(npc_node.modulate.a).is_equal_approx(Constants.PERIPHERAL_ALPHA, 0.01) renderer.queue_free() func test_entity_renderer_forward_entity_full_alpha() -> void: var renderer := _make_entity_renderer() renderer.update_entities(_test_entities_v2) var player_node = renderer.entity_nodes[1] assert_that(player_node.modulate.a).is_equal_approx(1.0, 0.01) renderer.queue_free() func test_entity_renderer_player_has_facing_indicator() -> void: GameState.player_entity_id = 1 var renderer := _make_entity_renderer() renderer.update_entities(_test_entities_v2) var player_node = renderer.entity_nodes[1] var indicator = player_node.get_node_or_null("FacingIndicator") assert_that(indicator != null).is_true() assert_that(indicator is Polygon2D).is_true() renderer.queue_free() func test_entity_renderer_facing_indicator_rotation_accuracy() -> void: # D-054: Facing indicator now reads InputMapper.facing_angle (float), not # GameState.player_facing (string). Indicator rotation = facing_angle + PI/2. GameState.player_entity_id = 1 var renderer := _make_entity_renderer() # {facing_angle → expected indicator rotation} # Indicator 0 = North (up). facing_angle 0 = East. So rotation = angle + PI/2. var angles := { -PI / 2.0: 0.0, # North -PI / 4.0: PI / 4.0, # Northeast 0.0: PI / 2.0, # East PI / 4.0: 3.0 * PI / 4.0, # Southeast PI / 2.0: PI, # South 3.0 * PI / 4.0: 5.0 * PI / 4.0, # Southwest (raw: 3PI/4 + PI/2 = 5PI/4) PI: 3.0 * PI / 2.0, # West (raw: PI + PI/2 = 3PI/2) -3.0 * PI / 4.0: -PI / 4.0, # Northwest: wraps negative — Godot returns raw un-normalised rotation } renderer.update_entities(_test_entities_v2) var player_node = renderer.entity_nodes[1] var indicator = player_node.get_node_or_null("FacingIndicator") for angle in angles: InputMapper.facing_angle = angle renderer.update_entities(_test_entities_v2) assert_that(indicator.rotation).is_equal_approx(angles[angle], 0.001) InputMapper.facing_angle = -PI / 2.0 # Reset to default (North) renderer.queue_free() func test_entity_renderer_npc_has_no_facing_indicator() -> void: GameState.player_entity_id = 1 var renderer := _make_entity_renderer() renderer.update_entities(_test_entities_v2) var npc_node = renderer.entity_nodes[2] var indicator = npc_node.get_node_or_null("FacingIndicator") assert_that(indicator == null).is_true() renderer.queue_free() # -- EntityRenderer: regression #345 (entity_id field name bug) -- # Bug: entity_renderer.gd checked entity_data.has("id") instead of # entity_data.has("entity_id"), causing all entities to be silently dropped. # Fix: use entity_id (Protocol v2 canonical field name). func test_regression_345_entity_id_field_renders() -> void: # Regression: entity with correct "entity_id" field MUST be rendered. var renderer := _make_entity_renderer() renderer.update_entities([ {"entity_id": 99, "x": 4.0, "y": 4.0, "z": 0, "kind": {"variant": "Npc", "data": null}}, ]) assert_that(renderer.entity_nodes.size()).is_equal(1) assert_that(renderer.entity_nodes.has(99)).is_true() renderer.queue_free() func test_regression_345_old_id_field_not_rendered() -> void: # Negative regression: entity using OLD field name "id" (not "entity_id") # must be silently dropped. Pre-fix code accepted "id"; this verifies the fix. var renderer := _make_entity_renderer() renderer.update_entities([ {"id": 99, "x": 4.0, "y": 4.0, "z": 0, "kind": {"variant": "Npc", "data": null}}, ]) assert_that(renderer.entity_nodes.size()).is_equal(0) renderer.queue_free() func test_regression_345_mixed_batch_only_entity_id_renders() -> void: # Mixed batch: one entity with correct "entity_id", one with old "id" only. # Only the entity_id entity should appear — no cross-contamination. var renderer := _make_entity_renderer() renderer.update_entities([ {"entity_id": 1, "x": 1.0, "y": 1.0, "z": 0, "kind": {"variant": "Npc", "data": null}}, {"id": 2, "x": 2.0, "y": 2.0, "z": 0, "kind": {"variant": "Npc", "data": null}}, ]) assert_that(renderer.entity_nodes.size()).is_equal(1) assert_that(renderer.entity_nodes.has(1)).is_true() assert_that(renderer.entity_nodes.has(2)).is_false() renderer.queue_free() func test_regression_345_entity_id_node_keyed_by_id_value() -> void: # Regression: entity_nodes dict must be keyed by the entity_id VALUE, # not by a string "entity_id" or by the old "id" value. var renderer := _make_entity_renderer() renderer.update_entities([ {"entity_id": 42, "x": 3.0, "y": 3.0, "z": 0, "kind": {"variant": "Npc", "data": null}}, ]) assert_that(renderer.entity_nodes.has(42)).is_true() assert_that(renderer.entity_nodes.has("entity_id")).is_false() assert_that(renderer.entity_nodes.has(0)).is_false() renderer.queue_free() func test_regression_345_entity_position_set_from_entity_id_entity() -> void: # Regression: entity rendered via entity_id must have correct pixel position. var renderer := _make_entity_renderer() renderer.update_entities([ {"entity_id": 5, "x": 6.0, "y": 7.0, "z": 0, "kind": {"variant": "Npc", "data": null}}, ]) var node = renderer.entity_nodes[5] # Sprite2D renderer: ENTITY_OFFSET_X=0.0, ENTITY_OFFSET_Y=TILE_SIZE-ENTITY_HEIGHT=0.0 assert_that(node.position.x).is_equal_approx(6.0 * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_X, 0.01) assert_that(node.position.y).is_equal_approx(7.0 * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_Y, 0.01) renderer.queue_free() # -- SoundIndicatorRenderer: #126 D-018 medium-range fog-edge indicators -- func _make_sound_indicator_renderer() -> Node2D: var renderer = Node2D.new() renderer.set_script(SoundIndicatorScript) add_child(renderer) return renderer func test_sound_indicator_accepts_medium_events() -> void: var renderer := _make_sound_indicator_renderer() renderer.update_sound_events([ {"x": 10.0, "y": 10.0, "event_type": "Footstep"}, {"x": 15.0, "y": 15.0, "event_type": "Voice"}, ]) assert_that(renderer._indicators.size()).is_equal(2) renderer.queue_free() func test_sound_indicator_drops_events_without_position() -> void: var renderer := _make_sound_indicator_renderer() renderer.update_sound_events([ {"event_type": "Footstep"}, # missing x, y {"x": 5.0, "event_type": "Voice"}, # missing y {"x": 8.0, "y": 3.0, "event_type": "Footstep"}, # valid ]) assert_that(renderer._indicators.size()).is_equal(1) renderer.queue_free() func test_sound_indicator_appends_new_events() -> void: var renderer := _make_sound_indicator_renderer() renderer.update_sound_events([ {"x": 1.0, "y": 1.0, "event_type": "Voice"}, {"x": 2.0, "y": 2.0, "event_type": "Footstep"}, ]) assert_that(renderer._indicators.size()).is_equal(2) # New update appends — existing indicators persist until they expire renderer.update_sound_events([{"x": 5.0, "y": 5.0, "event_type": "Footstep"}]) assert_that(renderer._indicators.size()).is_equal(3) renderer.queue_free() func test_sound_indicator_empty_update_preserves_existing() -> void: var renderer := _make_sound_indicator_renderer() renderer.update_sound_events([{"x": 3.0, "y": 3.0, "event_type": "Voice"}]) assert_that(renderer._indicators.size()).is_equal(1) # Empty update does not clear existing indicators — they expire via _process renderer.update_sound_events([]) assert_that(renderer._indicators.size()).is_equal(1) renderer.queue_free() func test_sound_indicator_color_voice() -> void: var renderer := _make_sound_indicator_renderer() assert_that(renderer.color_for_type("voice")).is_equal(SoundIndicatorRenderer.COLOR_VOICE) assert_that(renderer.color_for_type("Voice")).is_equal(SoundIndicatorRenderer.COLOR_VOICE) assert_that(renderer.color_for_type("speech")).is_equal(SoundIndicatorRenderer.COLOR_VOICE) renderer.queue_free() func test_sound_indicator_color_danger() -> void: var renderer := _make_sound_indicator_renderer() assert_that(renderer.color_for_type("Gunshot")).is_equal(SoundIndicatorRenderer.COLOR_DANGER) assert_that(renderer.color_for_type("alert")).is_equal(SoundIndicatorRenderer.COLOR_DANGER) assert_that(renderer.color_for_type("danger")).is_equal(SoundIndicatorRenderer.COLOR_DANGER) renderer.queue_free() func test_sound_indicator_color_neutral_for_unknown() -> void: var renderer := _make_sound_indicator_renderer() assert_that(renderer.color_for_type("Footstep")).is_equal(SoundIndicatorRenderer.COLOR_NEUTRAL) assert_that(renderer.color_for_type("")).is_equal(SoundIndicatorRenderer.COLOR_NEUTRAL) assert_that(renderer.color_for_type("Unknown")).is_equal(SoundIndicatorRenderer.COLOR_NEUTRAL) renderer.queue_free() func test_sound_indicator_deduplicates_same_position() -> void: var renderer := _make_sound_indicator_renderer() renderer.update_sound_events([{"x": 5.0, "y": 5.0, "event_type": "Voice"}]) assert_that(renderer._indicators.size()).is_equal(1) # Same position again — should reset timer, not add a duplicate renderer.update_sound_events([{"x": 5.0, "y": 5.0, "event_type": "Voice"}]) assert_that(renderer._indicators.size()).is_equal(1) renderer.queue_free() func test_sound_indicator_elapsed_starts_at_zero() -> void: var renderer := _make_sound_indicator_renderer() renderer.update_sound_events([{"x": 10.0, "y": 5.0, "event_type": "Voice"}]) assert_that(renderer._indicators[0].elapsed).is_equal_approx(0.0, 0.001) renderer.queue_free() func test_sound_indicator_events_expire_after_lifetime() -> void: var renderer := _make_sound_indicator_renderer() renderer.update_sound_events([{"x": 10.0, "y": 5.0, "event_type": "Footstep"}]) # Manually age the indicator past its lifetime renderer._indicators[0].elapsed = SoundIndicatorRenderer.INDICATOR_LIFETIME + 0.01 renderer._process(0.0) # zero delta so no additional aging assert_that(renderer._indicators.size()).is_equal(0) renderer.queue_free() # -- GameState: medium_sound_events from snapshot -- func test_game_state_filters_medium_sound_events() -> void: GameState.apply_snapshot({ "tick": 1, "sound_events": [ {"x": 5.0, "y": 5.0, "event_type": "Footstep", "range_category": "Close"}, {"x": 8.0, "y": 8.0, "event_type": "Voice", "range_category": "Medium"}, {"x": 20.0, "y": 20.0, "event_type": "Footstep", "range_category": "Long"}, ] }) assert_that(GameState.medium_sound_events.size()).is_equal(1) assert_that(GameState.medium_sound_events[0].event_type).is_equal("Voice") func test_game_state_medium_sound_events_empty_when_no_field() -> void: GameState.apply_snapshot({"tick": 1}) assert_that(GameState.medium_sound_events.size()).is_equal(0) func test_game_state_medium_sound_events_cleared_between_ticks() -> void: GameState.apply_snapshot({ "tick": 1, "sound_events": [{"x": 5.0, "y": 5.0, "event_type": "Voice", "range_category": "Medium"}] }) assert_that(GameState.medium_sound_events.size()).is_equal(1) # Next tick without sound_events clears them GameState.apply_snapshot({"tick": 2}) assert_that(GameState.medium_sound_events.size()).is_equal(0) # -- 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()