Critical: - deactivate_insert() now called when selecting non-Insert spoke, cancelling with no selection, or pressing Escape while insert is active. Fixes simulation staying paused permanently after Insert. Warnings: - Checklist conditions with empty id excluded from get_results() and get_total_count() — prevents impossible-to-complete checklists. Warns at load time when empty-id conditions are found. - _content_base now checks res://content/ first (exported builds), falls back to ../content for editor/dev mode. - 26 new tests for D-054 functions: _angle_to_octant (8 octants), _snap_to_octant_dir (9 cases incl. zero/tiny), _wasd_to_world_dir (8 facing/movement combos). New test file: test_input_mapper_facing.gd. Suggestions: - Cached get_theme_default_font() in checklist overlay _ready(). - Documented InputMapper → GameState coupling as intentional. - Documented YAML parser # truncation limitation. - _insert_active reset on Escape dismiss (Tyre #3). - SimBridge test mode SetFacing reads action_data.facing instead of InputMapper global (Tyre #4). - Removed dead _facing_to_rotation() from entity_renderer.gd (Tyre #5). - Fixed 2 failing facing indicator tests to use InputMapper.facing_angle instead of GameState.player_facing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
344 lines
14 KiB
GDScript
344 lines
14 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 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}},
|
|
]
|
|
|
|
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:
|
|
SimBridge.reset_test_state()
|
|
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.reset_test_state()
|
|
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.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:
|
|
SimBridge.reset_test_state()
|
|
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()
|
|
|
|
func test_sim_bridge_test_snapshot_has_v2_fields() -> void:
|
|
SimBridge.reset_test_state()
|
|
var snap = SimBridge._test_snapshot()
|
|
assert_that(snap.has("version")).is_true()
|
|
assert_that(snap.version).is_equal(Protocol.PROTOCOL_VERSION)
|
|
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]
|
|
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()
|
|
|
|
|
|
# -- 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 ColorRect
|
|
assert_that(player_node.color).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 ColorRect
|
|
assert_that(npc_node.color).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 ColorRect
|
|
assert_that(node.color).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
|
|
PI: 3.0 * PI / 2.0, # West
|
|
-3.0 * PI / 4.0: 7.0 * PI / 4.0, # Northwest (note: -PI/2 wraps)
|
|
}
|
|
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()
|
|
|
|
|
|
# -- 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()
|