fix(ui): address PR #118 review — 5 items

- Preload stance icons at _ready() into cache Dictionary
- Null-guard shader + theme loads with push_error() + early return
- Guard _icon_mat.set_shader_parameter inside if _icon_mat:
- Icon size 18→20px per D-086, container height 44→46px to fit
- Add _apply_stance test coverage (known + unknown stance)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-06 16:41:27 +02:00
co-authored by Claude Opus 4.6
parent bb61d860d6
commit 746ed05e2d
4 changed files with 56 additions and 19 deletions
@@ -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")
+3 -3
View File
@@ -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")
+21 -14
View File
@@ -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
+2 -2
View File
@@ -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")