diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index faebd1b..7f91c56 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -102,6 +102,9 @@ const SHOP_MERCHANT_BADGE_SIZE := Vector2(50, 50) const SHOP_ITEM_ICON_SIZE := Vector2(48, 48) const HUD_UNIT_PORTRAIT_SIZE := Vector2(92, 104) const HUD_UNIT_PORTRAIT_STACK_SIZE := Vector2(88, 100) +const HUD_UNIT_INFO_SIZE := Vector2(320, 126) +const HUD_UNIT_SUMMARY_SIZE := Vector2(320, 58) +const HUD_UNIT_BAR_SIZE := Vector2(252, 12) const HUD_COMMAND_GRID_SIZE := Vector2(420, 46) const HUD_COMMAND_BUTTON_SIZE := Vector2(72, 42) const HUD_COMMAND_GRID_COLUMNS := 4 @@ -319,6 +322,9 @@ var screen_backdrop: ColorRect var hud_unit_portrait_panel: PanelContainer var hud_unit_portrait_texture: TextureRect var hud_unit_portrait_label: Label +var hud_unit_info_column: VBoxContainer +var hud_unit_hp_bar: ProgressBar +var hud_unit_mp_bar: ProgressBar var selected_label: Label var cell_info_panel: PanelContainer var cell_info_label: Label @@ -851,6 +857,36 @@ func _configure_local_command_button(button: Button, icon_kind: String, label: S button.mouse_exited.connect(_on_local_command_button_mouse_exited) +func _make_hud_unit_resource_row(label_text: String, fill_color: Color) -> Dictionary: + var row := HBoxContainer.new() + row.custom_minimum_size = Vector2(HUD_UNIT_INFO_SIZE.x, 18.0) + row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row.add_theme_constant_override("separation", 6) + + var row_label := Label.new() + row_label.text = label_text + row_label.custom_minimum_size = Vector2(42, 16) + row_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _apply_label_style(row_label, UI_OLD_BRONZE, 11) + row.add_child(row_label) + + var bar := ProgressBar.new() + bar.custom_minimum_size = HUD_UNIT_BAR_SIZE + bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL + bar.min_value = 0.0 + bar.max_value = 1.0 + bar.value = 0.0 + bar.show_percentage = false + bar.add_theme_stylebox_override("background", _make_panel_style(Color(0.045, 0.024, 0.014, 0.95), UI_LACQUER_EDGE, 1, 0, 0, 0)) + bar.add_theme_stylebox_override("fill", _make_panel_style(fill_color, fill_color.lightened(0.28), 1, 0, 0, 0)) + row.add_child(bar) + + return { + "row": row, + "bar": bar + } + + func _format_local_command_tooltip(label: String, hint: String) -> String: var normalized_label := label.strip_edges() var normalized_hint := hint.strip_edges() @@ -1582,12 +1618,26 @@ func _create_hud() -> void: _apply_label_style(hud_unit_portrait_label, UI_PARCHMENT_TEXT) hud_unit_portrait_stack.add_child(hud_unit_portrait_label) + hud_unit_info_column = VBoxContainer.new() + hud_unit_info_column.custom_minimum_size = HUD_UNIT_INFO_SIZE + hud_unit_info_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL + hud_unit_info_column.add_theme_constant_override("separation", 5) + selected_row.add_child(hud_unit_info_column) + selected_label = Label.new() selected_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - selected_label.custom_minimum_size = Vector2(320, 126) + selected_label.custom_minimum_size = HUD_UNIT_SUMMARY_SIZE selected_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _apply_label_style(selected_label, UI_PARCHMENT_TEXT) - selected_row.add_child(selected_label) + _apply_label_style(selected_label, UI_PARCHMENT_TEXT, 13) + hud_unit_info_column.add_child(selected_label) + + var hp_row := _make_hud_unit_resource_row("병력", Color(0.66, 0.12, 0.075, 1.0)) + hud_unit_hp_bar = hp_row["bar"] as ProgressBar + hud_unit_info_column.add_child(hp_row["row"] as Control) + + var mp_row := _make_hud_unit_resource_row("기력", Color(0.20, 0.42, 0.70, 1.0)) + hud_unit_mp_bar = mp_row["bar"] as ProgressBar + hud_unit_info_column.add_child(mp_row["row"] as Control) var mission_title_row := HBoxContainer.new() mission_title_row.add_theme_constant_override("separation", 8) @@ -6081,9 +6131,12 @@ func _update_hud() -> void: if hovered_unit.is_empty(): selected_label.text = "전장 관망" selected_label.tooltip_text = "마우스를 부대 위에 올리면 상세 군세가 표시됩니다." + _update_hud_unit_resource_bars({}, selected_label.tooltip_text) else: + var hover_detail := _format_unit_focus_text(hovered_unit, "Hover") selected_label.text = _format_unit_focus_summary_text(hovered_unit, "Hover") - selected_label.tooltip_text = _format_unit_focus_text(hovered_unit, "Hover") + selected_label.tooltip_text = hover_detail + _update_hud_unit_resource_bars(hovered_unit, hover_detail) _update_wait_button({}) _update_tactic_button({}) _hide_tactic_menu() @@ -6093,8 +6146,10 @@ func _update_hud() -> void: _hide_equip_menu() else: _update_hud_unit_portrait(selected) + var selected_detail := _format_unit_focus_text(selected, "Selected") selected_label.text = _format_unit_focus_summary_text(selected, "Selected") - selected_label.tooltip_text = _format_unit_focus_text(selected, "Selected") + selected_label.tooltip_text = selected_detail + _update_hud_unit_resource_bars(selected, selected_detail) _update_wait_button(selected) _update_tactic_button(selected) _update_item_button(selected) @@ -6216,23 +6271,47 @@ func _format_unit_focus_summary_text(unit: Dictionary, prefix: String) -> String for index in range(3): visible_parts.append(str(status_parts[index]).strip_edges()) status_text = "%s 외 %d" % [_join_strings(visible_parts, ", "), status_parts.size() - 3] - var summary := "%s: %s%s\n%s 품계 %d · %s\n병력 %d/%d · 기력 %d/%d\n군령: %s" % [ + return "%s: %s%s\n%s 품계 %d · %s\n군령: %s" % [ _unit_focus_prefix_text(prefix), name, control_label, _unit_team_text(str(unit.get("team", ""))), int(unit.get("level", 1)), _unit_class_display_name(unit), - int(unit.get("hp", 0)), - int(unit.get("max_hp", 1)), - int(unit.get("mp", 0)), - int(unit.get("max_mp", 0)), status_text ] - var status_effects := state.get_status_effect_summary(str(unit.get("id", ""))) - if not status_effects.is_empty(): - summary += "\n상태: %s" % status_effects - return summary + + +func _update_hud_unit_resource_bars(unit: Dictionary, detail_text: String) -> void: + if unit.is_empty(): + _set_hud_unit_resource_bar(hud_unit_hp_bar, 0, 1, "병력", "", false) + _set_hud_unit_resource_bar(hud_unit_mp_bar, 0, 1, "기력", "", false) + return + var hp_max := maxi(1, int(unit.get("max_hp", 1))) + var hp_value := clampi(int(unit.get("hp", 0)), 0, hp_max) + _set_hud_unit_resource_bar(hud_unit_hp_bar, hp_value, hp_max, "병력", detail_text, true) + + var mp_max := int(unit.get("max_mp", 0)) + var mp_value := clampi(int(unit.get("mp", 0)), 0, maxi(0, mp_max)) + _set_hud_unit_resource_bar(hud_unit_mp_bar, mp_value, maxi(1, mp_max), "기력", detail_text, mp_max > 0) + + +func _set_hud_unit_resource_bar(bar: ProgressBar, value: int, max_value: int, label_text: String, detail_text: String, visible: bool) -> void: + if bar == null: + return + var row := bar.get_parent() as Control + if row != null: + row.visible = visible + bar.max_value = float(maxi(1, max_value)) + bar.value = float(clampi(value, 0, maxi(1, max_value))) + var tooltip := "" + if visible: + tooltip = "%s %d/%d" % [label_text, value, max_value] + if not detail_text.strip_edges().is_empty(): + tooltip = "%s\n%s" % [tooltip, detail_text] + bar.tooltip_text = tooltip + if row != null: + row.tooltip_text = tooltip func _unit_focus_prefix_text(prefix: String) -> String: diff --git a/tools/smoke_status_effect_summary.gd b/tools/smoke_status_effect_summary.gd index 24b4230..69a7507 100644 --- a/tools/smoke_status_effect_summary.gd +++ b/tools/smoke_status_effect_summary.gd @@ -67,7 +67,10 @@ func _check_hud_reuses_status_summary(failures: Array[String]) -> void: scene.state.selected_unit_id = "cao_cao" scene.hover_cell = Vector2i(1, 6) scene._update_hud() - _check_contains(failures, "selected HUD status", scene.selected_label.text, "상태: 방비 -2 (2회)") + if scene.selected_label.text.contains("상태:"): + failures.append("selected HUD should keep status effects in tooltip/markers, not visible text: %s" % scene.selected_label.text) + _check_contains(failures, "selected HUD status tooltip", scene.selected_label.tooltip_text, "상태: 방비 -2 (2회)") + _check_contains(failures, "selected HP bar status tooltip", scene.hud_unit_hp_bar.tooltip_text, "상태: 방비 -2 (2회)") _check_contains(failures, "hover tile tooltip status", scene.cell_info_label.tooltip_text, "상태: 방비 -2 (2회)") scene.state.clear_selection() scene._refresh_ranges() diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 69de7e5..21ce3e5 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -3043,8 +3043,21 @@ func _check_hud_focus_text(failures: Array[String]) -> void: failures.append("Cao Cao HUD focus should include terrain move cost: %s" % cao_text) if not cao_text.contains("방비 +0"): failures.append("Cao Cao HUD focus should include terrain defense: %s" % cao_text) - if not cao_summary.contains("군기: 조조") or not cao_summary.contains("병력") or cao_summary.contains("무위") or cao_summary.contains("지형 2,7"): - failures.append("Cao Cao visible HUD summary should stay compact while tooltip keeps detail: %s" % cao_summary) + if not cao_summary.contains("군기: 조조") or cao_summary.contains("병력") or cao_summary.contains("기력") or cao_summary.contains("무위") or cao_summary.contains("지형 2,7"): + failures.append("Cao Cao visible HUD summary should leave resource/detail text to bars and tooltip: %s" % cao_summary) + scene._create_hud() + scene.battle_started = true + scene.state.selected_unit_id = "cao_cao" + scene.hover_cell = Vector2i(1, 6) + scene._update_hud() + if scene.hud_unit_hp_bar == null or int(scene.hud_unit_hp_bar.value) != int(cao_cao.get("hp", 0)) or int(scene.hud_unit_hp_bar.max_value) != int(cao_cao.get("max_hp", 1)): + failures.append("selected HUD should expose Cao Cao HP through a resource bar") + if scene.hud_unit_mp_bar == null or int(scene.hud_unit_mp_bar.value) != int(cao_cao.get("mp", 0)) or int(scene.hud_unit_mp_bar.max_value) != int(cao_cao.get("max_mp", 0)): + failures.append("selected HUD should expose Cao Cao MP through a resource bar") + if not scene.hud_unit_hp_bar.tooltip_text.contains("병력") or not scene.hud_unit_hp_bar.tooltip_text.contains("군기: 조조"): + failures.append("selected HP bar tooltip should preserve unit detail: %s" % scene.hud_unit_hp_bar.tooltip_text) + if scene.selected_label.text.contains("병력") or scene.selected_label.text.contains("기력"): + failures.append("selected label should not duplicate HP/MP bars: %s" % scene.selected_label.text) var xiahou_dun: Dictionary = scene.state.get_unit("xiahou_dun") xiahou_dun["pos"] = Vector2i(4, 1) var forest_text := scene._unit_current_terrain_text(xiahou_dun)