extends PanelContainer ## Bottom-left panel showing selected unit info: name, stats, HP bar, movement, type. ## Shown when a unit is selected, hidden when deselected. ## ## p0-33/p0-35: emits signals for every action button (Move/Fortify/Skip/ ## FoundCity/BuildImprovement). The world_map controller wires the signals ## to the actual handlers — the panel itself never mutates the simulation ## directly. Disabled buttons keep a tooltip explaining WHY they cannot fire. const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd") ## Action signals consumed by world_map.gd. Disconnected from the slim HUD ## panel that p0-33 retired. signal move_pressed signal fortify_pressed signal skip_pressed signal found_city_pressed signal build_improvement_pressed ## Disabled-button outline color (matches the action-required tutorial badge). const DISABLED_OUTLINE_COLOR: Color = Color(0.95, 0.35, 0.35, 0.85) ## The currently selected unit (UnitScript or Dictionary). Typed as RefCounted ## because both unit entity types are RefCounted subclasses at the autoload boundary. var _selected_unit: RefCounted = null @onready var _unit_name: Label = %UnitName @onready var _combat_type: Label = %CombatType @onready var _attack_label: Label = %AttackLabel @onready var _defense_label: Label = %DefenseLabel @onready var _hp_label: Label = %HPLabel @onready var _hp_bar: ProgressBar = %HPBar @onready var _movement_label: Label = %MovementLabel @onready var _move_button: Button = %MoveButton @onready var _fortify_button: Button = %FortifyButton @onready var _skip_button: Button = %SkipButton @onready var _found_city_button: Button = %FoundCityButton @onready var _build_improvement_button: Button = %BuildImprovementButton func _ready() -> void: visible = false mouse_filter = Control.MOUSE_FILTER_STOP _move_button.text = ThemeVocabulary.lookup("move") _fortify_button.text = ThemeVocabulary.lookup("fortify") _skip_button.text = ThemeVocabulary.lookup("skip_turn") _found_city_button.text = ThemeVocabulary.lookup("found_city") _build_improvement_button.text = ThemeVocabulary.lookup("build_improvement") _apply_static_tooltips() EventBus.unit_selected.connect(_on_unit_selected) EventBus.unit_deselected.connect(_on_unit_deselected) EventBus.unit_moved.connect(_on_unit_moved) _move_button.pressed.connect(func() -> void: move_pressed.emit()) _fortify_button.pressed.connect(func() -> void: fortify_pressed.emit()) _skip_button.pressed.connect(func() -> void: skip_pressed.emit()) _found_city_button.pressed.connect(func() -> void: found_city_pressed.emit()) _build_improvement_button.pressed.connect(func() -> void: build_improvement_pressed.emit()) ## Resolve tooltip_text for stats rows through ThemeVocabulary so theme packs ## can localize hover copy. Action-button tooltips depend on per-unit state ## and are recomputed in `_refresh_action_buttons`. func _apply_static_tooltips() -> void: _attack_label.tooltip_text = ThemeVocabulary.lookup("tooltip_attack") _attack_label.mouse_filter = Control.MOUSE_FILTER_STOP _defense_label.tooltip_text = ThemeVocabulary.lookup("tooltip_defense") _defense_label.mouse_filter = Control.MOUSE_FILTER_STOP _hp_label.tooltip_text = ThemeVocabulary.lookup("tooltip_hit_points") _hp_label.mouse_filter = Control.MOUSE_FILTER_STOP _hp_bar.tooltip_text = ThemeVocabulary.lookup("tooltip_hit_points") _movement_label.tooltip_text = ThemeVocabulary.lookup("tooltip_movement") _movement_label.mouse_filter = Control.MOUSE_FILTER_STOP func _on_unit_selected(unit: Variant) -> void: _selected_unit = unit as RefCounted _refresh_display() visible = true func _on_unit_deselected() -> void: _selected_unit = null visible = false func _on_unit_moved(_unit: Variant, _from: Vector2i, _to: Vector2i) -> void: if _selected_unit != null: _refresh_display() func _refresh_display() -> void: if _selected_unit == null: return var type_id: String = _get_type_id(_selected_unit) _unit_name.text = ThemeVocabulary.lookup(type_id) var combat_type: String = _get_combat_type(_selected_unit) _combat_type.text = ThemeVocabulary.lookup(combat_type) var attack: int = _get_attack(_selected_unit) var defense: int = _get_defense(_selected_unit) var hp: int = _get_hp(_selected_unit) var max_hp: int = _get_max_hp(_selected_unit) var move_remaining: int = _get_movement_remaining(_selected_unit) var move_total: int = _get_movement_total(_selected_unit) _attack_label.text = ThemeVocabulary.lookup("fmt_key_int") % [ThemeVocabulary.lookup("attack"), attack] _defense_label.text = ThemeVocabulary.lookup("fmt_key_int") % [ThemeVocabulary.lookup("defense"), defense] _hp_label.text = ThemeVocabulary.lookup("fmt_current_of_max") % [ThemeVocabulary.lookup("hit_points"), hp, max_hp] _movement_label.text = ThemeVocabulary.lookup("fmt_current_of_max") % [ ThemeVocabulary.lookup("movement"), move_remaining, move_total, ] _update_hp_bar(hp, max_hp) _refresh_action_buttons(move_remaining) _refresh_items() func _refresh_action_buttons(move_remaining: int) -> void: ## p0-33/p0-35: each action button shows tooltip + red-outline when disabled. var is_civilian: bool = _is_civilian(_selected_unit) var is_founder: bool = _is_founder(_selected_unit) var can_build_improvements: bool = _can_build_improvements(_selected_unit) _apply_button_state(_move_button, move_remaining > 0, "tooltip_move", "tooltip_move_no_movement") _apply_button_state(_fortify_button, move_remaining > 0 and not is_civilian, "tooltip_fortify", "tooltip_fortify_civilian" if is_civilian else "tooltip_fortify_no_movement") _apply_button_state(_skip_button, move_remaining > 0, "tooltip_skip_turn", "tooltip_skip_turn_no_movement") _found_city_button.visible = is_founder if is_founder: _apply_button_state(_found_city_button, move_remaining > 0, "tooltip_found_city", "tooltip_found_city_no_movement") _build_improvement_button.visible = can_build_improvements if can_build_improvements: _apply_button_state(_build_improvement_button, move_remaining > 0, "tooltip_build_improvement", "tooltip_build_improvement_no_movement") ## Apply enabled/disabled visual + tooltip swap for a single action button. func _apply_button_state( btn: Button, enabled: bool, tooltip_key_active: String, tooltip_key_disabled: String ) -> void: btn.disabled = not enabled btn.tooltip_text = ThemeVocabulary.lookup( tooltip_key_active if enabled else tooltip_key_disabled ) if enabled: btn.remove_theme_color_override("font_outline_color") btn.remove_theme_constant_override("outline_size") else: btn.add_theme_color_override("font_outline_color", DISABLED_OUTLINE_COLOR) btn.add_theme_constant_override("outline_size", 2) func _update_hp_bar(hp: int, max_hp: int) -> void: if max_hp <= 0: _hp_bar.value = 0 return var ratio: float = float(hp) / float(max_hp) _hp_bar.max_value = max_hp _hp_bar.value = hp if ratio > 0.66: _hp_bar.modulate = Color(0.3, 0.9, 0.3) elif ratio > 0.33: _hp_bar.modulate = Color(0.95, 0.8, 0.1) else: _hp_bar.modulate = Color(0.9, 0.25, 0.25) func _refresh_items() -> void: ## Dynamically add/remove item rows below the movement label. var vbox: VBoxContainer = _unit_name.get_parent() as VBoxContainer if vbox == null: return for child: Node in vbox.get_children(): if child.has_meta("item_row"): child.queue_free() if _selected_unit == null or not _selected_unit is UnitScript: return var items: Array = (_selected_unit as UnitScript).equipped_items if items.is_empty(): return var button_row_idx: int = -1 for i: int in range(vbox.get_child_count()): if vbox.get_child(i).name == "ButtonRow": button_row_idx = i break if button_row_idx < 0: return var sep: HSeparator = HSeparator.new() sep.set_meta("item_row", true) vbox.add_child(sep) vbox.move_child(sep, button_row_idx) var insert_at: int = button_row_idx + 1 for entry: Dictionary in items: var item_id: String = entry.get("item_id", "") var item_data: Dictionary = DataLoader.get_item(item_id) var item_name: String = item_id if not item_data.is_empty(): item_name = str(item_data.get("name", item_id)) var category: String = item_data.get("category", "?") var tier: int = int(item_data.get("tier", 0)) var charges: int = entry.get("charges_remaining", -1) var row: Button = Button.new() row.set_meta("item_row", true) row.flat = true row.focus_mode = Control.FOCUS_NONE row.tooltip_text = _format_item_tooltip(item_data) row.pressed.connect(_on_item_slot_pressed.bind(item_id)) var row_hbox: HBoxContainer = HBoxContainer.new() row_hbox.mouse_filter = Control.MOUSE_FILTER_IGNORE row.add_child(row_hbox) var tier_tag: String = "T%d " % tier if tier > 0 else "" var slot_label: Label = Label.new() slot_label.text = "%s[%s]" % [tier_tag, category.left(3).to_upper()] slot_label.add_theme_font_size_override("font_size", 10) slot_label.add_theme_color_override("font_color", Color(0.85, 0.75, 0.4)) row_hbox.add_child(slot_label) var name_label: Label = Label.new() name_label.text = item_name name_label.add_theme_font_size_override("font_size", 11) name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL row_hbox.add_child(name_label) if charges >= 0: var charges_label: Label = Label.new() charges_label.text = "(%d)" % charges charges_label.add_theme_font_size_override("font_size", 10) charges_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6)) row_hbox.add_child(charges_label) vbox.add_child(row) vbox.move_child(row, insert_at) insert_at += 1 func _format_item_tooltip(item_data: Dictionary) -> String: if item_data.is_empty(): return "" var lines: Array[String] = [] var display: String = str(item_data.get("name", "")) var tier: int = int(item_data.get("tier", 0)) if tier > 0: lines.append("%s T%d" % [display, tier]) else: lines.append(display) var stats: Array = item_data.get("stats", []) var shown: int = 0 for stat: Dictionary in stats: if shown >= 3: break var stat_label: String = str(stat.get("label", "")) if stat_label != "": lines.append("• " + stat_label) shown += 1 return "\n".join(lines) func _on_item_slot_pressed(item_id: String) -> void: ## Clicking an equipped item slot unequips it (item returns to Treasury). ## Equipping is driven by the crafting_complete_modal and the Treasury tab. if _selected_unit == null or not _selected_unit is UnitScript: return var unit: UnitScript = _selected_unit as UnitScript for i: int in range(unit.equipped_items.size()): var entry_dict: Dictionary = unit.equipped_items[i] if str(entry_dict.get("item_id", "")) == item_id: unit.equipped_items.remove_at(i) EventBus.item_equipped.emit(unit, item_id) _refresh_items() return # -- Unit property access -- func _get_type_id(unit: RefCounted) -> String: if unit is UnitScript: return (unit as UnitScript).type_id return str(unit.get("type_id")) func _get_combat_type(unit: RefCounted) -> String: if unit is UnitScript: return (unit as UnitScript).get_combat_type() return str(unit.get("combat_type")) func _get_attack(unit: RefCounted) -> int: if unit is UnitScript: return (unit as UnitScript).get_attack() var data: Dictionary = DataLoader.get_unit(str(unit.get("type_id"))) return data.get("attack", 0) as int + int(unit.get("bonus_attack")) func _get_defense(unit: RefCounted) -> int: if unit is UnitScript: return (unit as UnitScript).get_defense() var data: Dictionary = DataLoader.get_unit(str(unit.get("type_id"))) return data.get("defense", 0) as int + int(unit.get("bonus_defense")) func _get_hp(unit: RefCounted) -> int: if unit is UnitScript: return (unit as UnitScript).hp return int(unit.get("hp")) func _get_max_hp(unit: RefCounted) -> int: if unit is UnitScript: return (unit as UnitScript).max_hp return int(unit.get("max_hp")) func _get_movement_remaining(unit: RefCounted) -> int: if unit is UnitScript: return (unit as UnitScript).movement_remaining return int(unit.get("movement_remaining")) func _get_movement_total(unit: RefCounted) -> int: if unit is UnitScript: return (unit as UnitScript).get_movement() var data: Dictionary = DataLoader.get_unit(str(unit.get("type_id"))) return data.get("movement", 2) as int func _is_civilian(unit: RefCounted) -> bool: if unit is UnitScript: return (unit as UnitScript).is_civilian() return str(unit.get("combat_type")) == "civilian" func _is_founder(unit: RefCounted) -> bool: if unit is UnitScript: return (unit as UnitScript).can_found_city var data: Dictionary = DataLoader.get_unit(str(unit.get("type_id"))) var keywords: Array = data.get("keywords", []) return "found_city" in keywords func _can_build_improvements(unit: RefCounted) -> bool: if unit is UnitScript: return (unit as UnitScript).can_build_improvements var data: Dictionary = DataLoader.get_unit(str(unit.get("type_id"))) var keywords: Array = data.get("keywords", []) return "build_improvement" in keywords or "worker" in keywords func _get_position(unit: RefCounted) -> Vector2i: if unit is UnitScript: return (unit as UnitScript).position if unit.get("position") is Vector2i: return unit.get("position") as Vector2i return Vector2i.ZERO