diff --git a/client/tests/test_ui_framework_sprint15.gd b/client/tests/test_ui_framework_sprint15.gd index 494e34b57..938aa98f3 100644 --- a/client/tests/test_ui_framework_sprint15.gd +++ b/client/tests/test_ui_framework_sprint15.gd @@ -313,3 +313,33 @@ func test_tile_renderer_skips_nonzero_z() -> void: # 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") diff --git a/client/ui/gauntlet_hud.tscn b/client/ui/gauntlet_hud.tscn index da3e169da..3c307e8a8 100644 --- a/client/ui/gauntlet_hud.tscn +++ b/client/ui/gauntlet_hud.tscn @@ -2,16 +2,16 @@ [ext_resource type="Script" path="res://ui/gauntlet_hud.gd" id="1_gauntlet"] -; #496: Gauntlet HUD — room timer + personal bests, top-right below StanceIndicator (bottom at y:244) +; #496: Gauntlet HUD — room timer + personal bests, top-right below StanceIndicator (bottom at y:246), 8px gap [node name="GauntletHUD" type="Control"] layout_mode = 3 anchors_preset = 1 anchor_left = 1.0 anchor_right = 1.0 offset_left = -160.0 -offset_top = 252.0 +offset_top = 254.0 offset_right = -16.0 -offset_bottom = 278.0 +offset_bottom = 280.0 grow_horizontal = 0 mouse_filter = 2 script = ExtResource("1_gauntlet") diff --git a/client/ui/stance_indicator.gd b/client/ui/stance_indicator.gd index 1b4c53d60..75a83c062 100644 --- a/client/ui/stance_indicator.gd +++ b/client/ui/stance_indicator.gd @@ -10,24 +10,32 @@ const STANCE_COLORS := { "Crouch": Color("#e8c547"), # Amber — very quiet, slow } -const STANCE_ICONS := { - "Sprint": "res://assets/icons/icon_stance_sprint.svg", - "Walk": "res://assets/icons/icon_stance_walk.svg", - "Careful": "res://assets/icons/icon_stance_careful.svg", - "Crouch": "res://assets/icons/icon_stance_crouch.svg", -} - var _current_stance: String = "Walk" var _panel: ImplantPanel var _icon: TextureRect var _label: Label var _icon_mat: ShaderMaterial +var _icon_cache: Dictionary = {} # stance name → Texture2D func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE var theme_res := load("res://ui/implant/default_implant.tres") as ImplantTheme var icon_shader := load("res://shaders/icon_tint.gdshader") as Shader + if not theme_res: + push_error("StanceIndicator: failed to load implant theme") + return + if not icon_shader: + push_error("StanceIndicator: failed to load icon_tint shader") + return + + # Preload all stance icons at startup + for stance_name in STANCE_ICONS: + var tex := load(STANCE_ICONS[stance_name]) as Texture2D + if tex: + _icon_cache[stance_name] = tex + else: + push_error("StanceIndicator: failed to load icon for %s" % stance_name) _panel = ImplantPanel.new() _panel.name = "StancePanel" @@ -42,7 +50,7 @@ func _ready() -> void: _icon = TextureRect.new() _icon.expand_mode = TextureRect.EXPAND_KEEP_SIZE _icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - _icon.custom_minimum_size = Vector2(18.0, 18.0) + _icon.custom_minimum_size = Vector2(20.0, 20.0) # D-086: 20×20 viewBox _icon.mouse_filter = Control.MOUSE_FILTER_IGNORE _icon_mat = ShaderMaterial.new() _icon_mat.shader = icon_shader @@ -53,8 +61,7 @@ func _ready() -> void: _label = Label.new() _label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _label.mouse_filter = Control.MOUSE_FILTER_IGNORE - if theme_res: - _label.add_theme_font_size_override("font_size", theme_res.font_body) + _label.add_theme_font_size_override("font_size", theme_res.font_body) hbox.add_child(_label) _panel.add_component(hbox) @@ -64,13 +71,13 @@ func _ready() -> void: func _apply_stance(stance: String) -> void: _current_stance = stance var color: Color = STANCE_COLORS.get(stance, Color("#c8d0e0")) - var icon_path: String = STANCE_ICONS.get(stance, "") - if _icon and not icon_path.is_empty(): - var tex := load(icon_path) as Texture2D + if _icon: + var tex: Texture2D = _icon_cache.get(stance) if tex: _icon.texture = tex - _icon_mat.set_shader_parameter("tint_color", color) + if _icon_mat: + _icon_mat.set_shader_parameter("tint_color", color) if _label: _label.text = stance diff --git a/client/ui/stance_indicator.tscn b/client/ui/stance_indicator.tscn index bc14f6abb..f20fe4c2f 100644 --- a/client/ui/stance_indicator.tscn +++ b/client/ui/stance_indicator.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://ui/stance_indicator.gd" id="1_stance"] ; D-053: Stance indicator — below minimap (bottom at y:176), 24px gap. -; Control height 44px to contain ImplantPanel (padding 12+12 + row 18 = 42px minimum). +; Control height 46px: border(1) + padding(12) + icon(20) + padding(12) + border(1). [node name="StanceIndicator" type="Control"] layout_mode = 3 anchors_preset = 1 @@ -12,7 +12,7 @@ anchor_right = 1.0 offset_left = -100.0 offset_top = 200.0 offset_right = -16.0 -offset_bottom = 244.0 +offset_bottom = 246.0 grow_horizontal = 0 mouse_filter = 2 script = ExtResource("1_stance")