diff --git a/client/shaders/icon_tint.gdshader b/client/shaders/icon_tint.gdshader new file mode 100644 index 000000000..aa2fc88e8 --- /dev/null +++ b/client/shaders/icon_tint.gdshader @@ -0,0 +1,10 @@ +shader_type canvas_item; +// D-086: Runtime icon tint — replaces color channel with tint_color, preserves alpha mask. +// Applied to SVG-sourced TextureRect nodes so stance/status icons can be color-coded at runtime. +// Default: #c8d0e0 (insert chrome) — identical to the SVG's authored stroke color. +uniform vec4 tint_color : source_color = vec4(0.784, 0.816, 0.878, 1.0); + +void fragment() { + float a = texture(TEXTURE, UV).a; + COLOR = vec4(tint_color.rgb, a * tint_color.a); +} diff --git a/client/tests/test_ui_framework_sprint15.gd b/client/tests/test_ui_framework_sprint15.gd index ff0ced7bd..494e34b57 100644 --- a/client/tests/test_ui_framework_sprint15.gd +++ b/client/tests/test_ui_framework_sprint15.gd @@ -122,14 +122,14 @@ func test_minimap_placeholder_exists_in_ui_layer() -> void: assert_that(_instance.get_node_or_null("InsertOverlay/Minimap")).is_not_null() -func test_hud_exists_in_ui_layer() -> void: - # D-049: HUD must be in UILayer. +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("UILayer/HUD")).is_not_null() + assert_that(_instance.get_node_or_null("InsertOverlay/HUD")).is_not_null() func test_interaction_list_exists_in_insert_overlay() -> void: diff --git a/client/ui/stance_indicator.gd b/client/ui/stance_indicator.gd index a144037e0..1b4c53d60 100644 --- a/client/ui/stance_indicator.gd +++ b/client/ui/stance_indicator.gd @@ -1,23 +1,33 @@ extends Control ## D-053: Stance indicator — implant-styled badge (D-169). -## Sprint/Walk/Careful/Crouch. Color-coded for quick read. +## Sprint/Walk/Careful/Crouch. Icon (D-086) + color-coded label. const STANCE_COLORS := { "Sprint": Color("#d45d5d"), # Red — fast, loud, dangerous - "Walk": Color("#c8d0e0"), # Default — neutral white-blue - "Careful": Color("#6bc9a6"), # Green — quiet, observant + "Walk": Color("#c8d0e0"), # Default — neutral white-blue + "Careful": Color("#6bc9a6"), # Green — quiet, observant "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 _row: ImplantDataRow +var _icon: TextureRect +var _label: Label +var _icon_mat: ShaderMaterial 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 _panel = ImplantPanel.new() _panel.name = "StancePanel" @@ -25,17 +35,53 @@ func _ready() -> void: _panel.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_panel) - _row = ImplantDataRow.new("Walk", STANCE_COLORS["Walk"]) - _panel.add_component(_row) + var hbox := HBoxContainer.new() + hbox.add_theme_constant_override("separation", 4) + hbox.mouse_filter = Control.MOUSE_FILTER_IGNORE + + _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.mouse_filter = Control.MOUSE_FILTER_IGNORE + _icon_mat = ShaderMaterial.new() + _icon_mat.shader = icon_shader + _icon_mat.set_shader_parameter("tint_color", STANCE_COLORS["Walk"]) + _icon.material = _icon_mat + hbox.add_child(_icon) + + _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) + hbox.add_child(_label) + + _panel.add_component(hbox) + _apply_stance("Walk") + + +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 tex: + _icon.texture = tex + _icon_mat.set_shader_parameter("tint_color", color) + + if _label: + _label.text = stance + _label.add_theme_color_override("font_color", color) func update_from_state() -> void: var stance: String = GameState.player_stance if stance == _current_stance: return - _current_stance = stance - var color: Color = STANCE_COLORS.get(stance, Color("#c8d0e0")) - _row.set_content(stance, color) + _apply_stance(stance) func get_current_stance() -> String: