## Sprint 15 — Basic UI framework validation tests (#74) ## Validates HUD structure, z-layer hierarchy, insert_active control, ## and monologue display wiring per D-049, D-056, D-057, D-061, OQ-07. class_name TestUIFrameworkSprint15 extends GdUnitTestSuite const MAIN_SCENE = preload("res://scenes/main.tscn") var _instance: Node = null func before_test() -> void: SimBridge.reset_test_state() GameState.current_tick = 0 GameState.player_position = Vector2.ZERO GameState.visible_entities = [] GameState.visible_tiles = [] GameState.visible_positions = {} GameState.current_monologue = null GameState.current_dialogue = null GameState.insert_active = true func after_test() -> void: if _instance and is_instance_valid(_instance): _instance.queue_free() _instance = null # ------------------------------------------------------------------------- # D-076: Layout constants # ------------------------------------------------------------------------- func test_dialogue_max_width_set() -> void: # DIALOGUE_MAX_WIDTH = 1200px (supersedes D-076 640px default per Tyre review). assert_that(Constants.DIALOGUE_MAX_WIDTH).is_equal(1200) # ------------------------------------------------------------------------- # D-049: Z-layer scene hierarchy # ------------------------------------------------------------------------- func test_insert_overlay_is_canvas_layer_10() -> void: # D-049: InsertOverlay = conceptual layer 6 (insert scope) = CanvasLayer 10. # Constants.CANVAS_INSERT must match. var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) var insert_overlay: CanvasLayer = _instance.get_node("InsertOverlay") assert_that(insert_overlay).is_not_null() assert_that(insert_overlay.layer).is_equal(Constants.CANVAS_INSERT) func test_ui_layer_is_canvas_layer_20() -> void: # D-049: UILayer = conceptual layer 7 (UI/monologue scope) = CanvasLayer 20. var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) var ui_layer: CanvasLayer = _instance.get_node("UILayer") assert_that(ui_layer).is_not_null() assert_that(ui_layer.layer).is_equal(Constants.CANVAS_UI) func test_modal_layer_is_canvas_layer_30() -> void: # D-049: ModalLayer = pause/inventory modal scope = CanvasLayer 30. var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) var modal_layer: CanvasLayer = _instance.get_node("ModalLayer") assert_that(modal_layer).is_not_null() assert_that(modal_layer.layer).is_equal(Constants.CANVAS_MODAL) func test_ui_layer_above_insert_overlay() -> void: # D-049: UILayer (20) must render above InsertOverlay (10). assert_that(Constants.CANVAS_UI).is_greater(Constants.CANVAS_INSERT) func test_modal_layer_above_ui_layer() -> void: # D-049: ModalLayer (30) must render above UILayer (20). assert_that(Constants.CANVAS_MODAL).is_greater(Constants.CANVAS_UI) # ------------------------------------------------------------------------- # D-049: Required nodes exist in correct layers # ------------------------------------------------------------------------- func test_monologue_display_exists_in_ui_layer() -> void: # D-049 / #117 / #414: MonologueDisplay must be in UILayer (layer 7). var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) assert_that(_instance.get_node_or_null("UILayer/MonologueDisplay")).is_not_null() func test_stance_indicator_exists_in_ui_layer() -> void: # D-053: StanceIndicator must be in UILayer (layer 7), top-right, color-coded. var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) assert_that(_instance.get_node_or_null("UILayer/StanceIndicator")).is_not_null() func test_minimap_placeholder_exists_in_ui_layer() -> void: # D-013/D-049: Minimap is on InsertOverlay (z-layer 6), NOT UILayer. # Sprint 18 #151 (Stig): moved from UILayer to InsertOverlay per D-049 spec. var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) assert_that(_instance.get_node_or_null("InsertOverlay/Minimap")).is_not_null() func test_hud_exists_in_insert_overlay() -> void: # D-051: HUD moved to InsertOverlay (layer 10) for bloom treatment (#786, PR #115). var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) assert_that(_instance.get_node_or_null("InsertOverlay/HUD")).is_not_null() func test_interaction_list_exists_in_insert_overlay() -> void: # D-057: InteractionList must be in InsertOverlay (z-layer 6). var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) assert_that(_instance.get_node_or_null("InsertOverlay/InteractionList")).is_not_null() func test_dialogue_box_exists_in_insert_overlay() -> void: # D-061: DialogueBox must be in InsertOverlay (z-layer 6). var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) assert_that(_instance.get_node_or_null("InsertOverlay/DialogueBox")).is_not_null() func test_world_radial_exists_in_insert_overlay() -> void: # D-058: WorldRadial must be in InsertOverlay (z-layer 6). var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) assert_that(_instance.get_node_or_null("InsertOverlay/WorldRadial")).is_not_null() func test_cursor_renderer_exists_in_ui_layer() -> void: # D-056: CursorRenderer must be in UILayer (topmost, z-layer 7). var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) assert_that(_instance.get_node_or_null("UILayer/CursorRenderer")).is_not_null() # ------------------------------------------------------------------------- # OQ-07 / D-056: insert_active controls z-layer 6 visibility # ------------------------------------------------------------------------- func test_gamestate_insert_active_defaults_true() -> void: # OQ-07: v0.1 characters all have inserts — default true. assert_that(GameState.insert_active).is_true() func test_insert_active_propagates_on_process() -> void: # OQ-07 (#522): After apply_snapshot with insert_active=false, # the next _process() call must propagate the state to z-layer-6 nodes. var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) # Inject a snapshot with insert_active = false var snap := SimBridge._test_snapshot() snap["insert_active"] = false GameState.apply_snapshot(snap) assert_that(GameState.insert_active).is_false() func test_insert_active_true_from_snapshot() -> void: # OQ-07: Snapshot with insert_active=true keeps GameState in default-on state. var snap := SimBridge._test_snapshot() snap["insert_active"] = true GameState.apply_snapshot(snap) assert_that(GameState.insert_active).is_true() func test_insert_active_missing_field_defaults_true() -> void: # OQ-07: Old servers without insert_active field must not disable the insert. var snap := SimBridge._test_snapshot() snap.erase("insert_active") GameState.apply_snapshot(snap) assert_that(GameState.insert_active).is_true() # ------------------------------------------------------------------------- # #241 stub: follow_target_id for entity sprite system # ------------------------------------------------------------------------- func test_follow_target_id_stub_exists() -> void: # #72 / #241: follow_target_id stub must exist on GameState with default -1. # Populated by server ticket #241 (Follow verb) when it lands. assert_that(GameState.follow_target_id).is_equal(-1) func test_follow_target_id_is_negative_one_by_default() -> void: # #72: -1 means "not following" — client #72 checks this for entity highlight. SimBridge.reset_test_state() GameState.apply_snapshot(SimBridge._test_snapshot()) # Server doesn't send follow_target_id yet — must stay -1 after snapshot assert_that(GameState.follow_target_id).is_equal(-1) # ------------------------------------------------------------------------- # Monologue display wiring (#414) # ------------------------------------------------------------------------- func test_monologue_display_receives_first_tick_monologue() -> void: # #414 / #74: MonologueDisplay must show monologue from tick 1 test snapshot. # Verifies the wiring: GameState.current_monologue → main.gd → MonologueDisplay. var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) # Tick 1 snapshot has a monologue (SimBridge test mode) # _ready() consumes tick 0 (no monologue). _process() here gets tick 1. _instance._process(0.016) # If monologue_display received it, current_monologue is cleared (consume-once) assert_that(GameState.current_monologue).is_null() # ------------------------------------------------------------------------- # Regression: Sprint 14 integration proofs (D-030 regression markers) # ------------------------------------------------------------------------- func test_fog_group_exists_in_world() -> void: # Sprint 14 regression: fog rendering must still be present after sprint 15 changes. var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) assert_that(_instance.get_node_or_null("World/FogGroup")).is_not_null() func test_floor_tiles_in_fog_group() -> void: # Sprint 14 regression: FloorTiles must be in FogGroup (D-049 z:0). var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) assert_that(_instance.get_node_or_null("World/FogGroup/FloorTiles")).is_not_null() func test_entities_in_ysort_group() -> void: # Sprint 14 regression: Entities must be in YSortGroup for y-sort ordering (D-049 z:100). var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) assert_that(_instance.get_node_or_null("World/FogGroup/YSortGroup/Entities")).is_not_null() # ------------------------------------------------------------------------- # #71: Tilemap z-filter — FloorTiles only renders floor level 0 # ------------------------------------------------------------------------- func test_tile_renderer_skips_nonzero_z() -> void: # #71: Tiles with z != 0 must be filtered out by update_tiles(). var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) var tile_renderer: TileMapLayer = _instance.get_node("World/FogGroup/FloorTiles") assert_that(tile_renderer).is_not_null() # Feed tiles at z=0 and z=1 var tiles: Array = [ {"x": 0, "y": 0, "z": 0, "type": "floor"}, {"x": 1, "y": 0, "z": 1, "type": "floor"}, {"x": 2, "y": 0, "z": 0, "type": "wall"}, {"x": 3, "y": 0, "z": 2, "type": "door"}, ] tile_renderer.update_tiles(tiles) # z=0 tiles should be present assert_that(tile_renderer.get_cell_source_id(Vector2i(0, 0))).is_not_equal(-1) assert_that(tile_renderer.get_cell_source_id(Vector2i(2, 0))).is_not_equal(-1) # z=1 and z=2 tiles should NOT be present (-1 = no cell) assert_that(tile_renderer.get_cell_source_id(Vector2i(1, 0))).is_equal(-1) assert_that(tile_renderer.get_cell_source_id(Vector2i(3, 0))).is_equal(-1) # ------------------------------------------------------------------------- # Stance indicator icon tests (#787) # ------------------------------------------------------------------------- func test_stance_indicator_applies_known_stance() -> void: # #787: _apply_stance updates icon + label for each known stance. var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) var si = _instance.get_node_or_null("UILayer/StanceIndicator") assert_that(si).is_not_null() si._apply_stance("Careful") assert_that(si.get_current_stance()).is_equal("Careful") func test_stance_indicator_unknown_stance_no_crash() -> void: # #787: Unknown stance falls back gracefully — no crash, no texture change. var scene := MAIN_SCENE _instance = scene.instantiate() auto_free(_instance) add_child(_instance) var si = _instance.get_node_or_null("UILayer/StanceIndicator") assert_that(si).is_not_null() si._apply_stance("InvalidStance") assert_that(si.get_current_stance()).is_equal("InvalidStance")