extends Sprite3D const ICON_SIZE := 10 const ICON_MARGIN := 1 const BORDER_W := 1 @onready var viewport: SubViewport = $SubViewport @onready var health_bar: ProgressBar = $SubViewport/HealthBar @onready var border: ColorRect = $SubViewport/Border @onready var parent_node: Node = get_parent() var shield_bar: ProgressBar = null var style_normal: StyleBoxFlat var style_aggro: StyleBoxFlat var effect_container: HBoxContainer = null var base_viewport_height: int = 0 func _ready() -> void: shield_bar = $SubViewport.get_node_or_null("ShieldBar") border.visible = false style_normal = health_bar.get_theme_stylebox("fill").duplicate() style_aggro = style_normal.duplicate() style_aggro.bg_color = Color(0.2, 0.4, 0.9, 1) base_viewport_height = viewport.size.y _create_effect_container() texture = viewport.get_texture() EventBus.target_changed.connect(_on_target_changed) EventBus.health_changed.connect(_on_health_changed) EventBus.shield_changed.connect(_on_shield_changed) EventBus.effect_applied.connect(_on_effect_applied) EventBus.effect_expired.connect(_on_effect_expired) _init_bars() func _create_effect_container() -> void: effect_container = HBoxContainer.new() effect_container.name = "EffectContainer" var y_pos: float = 0.0 if shield_bar and shield_bar.visible: y_pos = shield_bar.offset_bottom + 2 else: y_pos = health_bar.offset_bottom + 2 effect_container.position = Vector2(2, y_pos) effect_container.add_theme_constant_override("separation", ICON_MARGIN) viewport.add_child(effect_container) func _init_bars() -> void: var max_health: Variant = Stats.get_stat(parent_node, "max_health") if max_health != null: health_bar.max_value = max_health health_bar.value = Stats.get_stat(parent_node, "health") var max_shield: Variant = Stats.get_stat(parent_node, "max_shield") if shield_bar: if max_shield != null and max_shield > 0: shield_bar.max_value = max_shield shield_bar.value = Stats.get_stat(parent_node, "shield") else: shield_bar.visible = false effect_container.position.y = health_bar.offset_bottom + 2 func _process(_delta: float) -> void: var player: Node = get_tree().get_first_node_in_group("player") if player and "target" in parent_node and parent_node.target == player: health_bar.add_theme_stylebox_override("fill", style_aggro) else: health_bar.add_theme_stylebox_override("fill", style_normal) func _on_health_changed(entity: Node, current: float, max_val: float) -> void: if entity != parent_node: return health_bar.max_value = max_val health_bar.value = current func _on_shield_changed(entity: Node, current: float, max_val: float) -> void: if entity != parent_node or shield_bar == null: return shield_bar.max_value = max_val shield_bar.value = current func _on_target_changed(_player: Node, target: Node) -> void: border.visible = (target == get_parent()) func _on_effect_applied(target: Node, effect: Effect) -> void: if target != parent_node: return _add_effect_icon(effect) func _on_effect_expired(target: Node, effect: Effect) -> void: if target != parent_node: return _remove_effect_icon(effect) func _add_effect_icon(effect: Effect) -> void: var panel := _create_icon_panel(effect) var insert_idx: int = _get_sorted_index(effect.type) effect_container.add_child(panel) effect_container.move_child(panel, insert_idx) _resize_viewport() func _remove_effect_icon(effect: Effect) -> void: for child in effect_container.get_children(): if child.has_meta("effect_type") and child.has_meta("effect_name"): if child.get_meta("effect_type") == effect.type and child.get_meta("effect_name") == effect.effect_name: child.queue_free() _resize_viewport.call_deferred() return func _get_sorted_index(type: int) -> int: var idx := 0 for child in effect_container.get_children(): if not child.has_meta("effect_type"): continue var child_type: int = child.get_meta("effect_type") if child_type <= type: idx += 1 else: break return idx func _create_icon_panel(effect: Effect) -> PanelContainer: var panel := PanelContainer.new() var style := StyleBoxFlat.new() match effect.type: Effect.Type.AURA: style.bg_color = Color(0.15, 0.15, 0.3, 1) style.border_color = Color(0.3, 0.5, 1.0, 1) Effect.Type.BUFF: style.bg_color = Color(0.15, 0.3, 0.15, 1) style.border_color = Color(0.3, 1.0, 0.3, 1) Effect.Type.DEBUFF: style.bg_color = Color(0.3, 0.15, 0.15, 1) style.border_color = Color(1.0, 0.3, 0.3, 1) style.set_border_width_all(BORDER_W) style.set_content_margin_all(0) panel.add_theme_stylebox_override("panel", style) var label := Label.new() label.text = effect.effect_name.left(1) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.add_theme_font_size_override("font_size", 7) label.add_theme_color_override("font_color", Color.WHITE) label.custom_minimum_size = Vector2(ICON_SIZE, ICON_SIZE) panel.add_child(label) panel.custom_minimum_size = Vector2(ICON_SIZE + 2, ICON_SIZE + 2) panel.set_meta("effect_type", effect.type) panel.set_meta("effect_name", effect.effect_name) return panel func _resize_viewport() -> void: var icon_count := 0 for child in effect_container.get_children(): if not child.is_queued_for_deletion(): icon_count += 1 if icon_count > 0: var needed: int = int(effect_container.position.y) + ICON_SIZE + 4 viewport.size.y = max(base_viewport_height, needed) border.offset_bottom = viewport.size.y else: viewport.size.y = base_viewport_height border.offset_bottom = base_viewport_height