diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index d0349cf..64833cd 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -186,6 +186,10 @@ const HUD_COMMAND_GRID_SIZE := Vector2(420, 46) const HUD_COMMAND_BUTTON_SIZE := Vector2(72, 42) const HUD_COMMAND_GRID_COLUMNS := 4 const TOP_TOOL_BUTTON_SIZE := Vector2(46, 46) +const COMMAND_HINT_PANEL_SIZE := Vector2(246, 78) +const COMMAND_HINT_ICON_SIZE := Vector2(30, 30) +const COMMAND_HINT_TITLE_SIZE := Vector2(184, 22) +const COMMAND_HINT_DETAIL_SIZE := Vector2(208, 32) const PREP_MENU_BUTTON_SIZE := Vector2(48, 38) const BATTLE_UNIT_LIST_PANEL_SIZE := Vector2(420, 360) const BATTLE_UNIT_LIST_ROW_SIZE := Vector2(372, 58) @@ -541,6 +545,11 @@ var targeting_hint_title_label: Label var targeting_hint_detail_label: Label var targeting_hint_back_button: Button var targeting_hint_cancel_move_button: Button +var command_hint_panel: PanelContainer +var command_hint_icon: TextureRect +var command_hint_title_label: Label +var command_hint_detail_label: Label +var command_hint_source_button: Button var threat_button: Button var battle_unit_list_button: Button var battle_unit_list_panel: PanelContainer @@ -1150,6 +1159,7 @@ func _configure_hud_command_button(button: Button, icon_kind: String = "") -> vo button.expand_icon = true button.set_meta("icon_only", true) button.set_meta("command_icon", icon_kind) + _connect_command_hint_button(button) func _configure_top_tool_button(button: Button, node_name: String, icon_kind: String, tooltip: String) -> void: @@ -1164,7 +1174,9 @@ func _configure_top_tool_button(button: Button, node_name: String, icon_kind: St button.icon = _make_toolbar_icon(icon_kind) button.expand_icon = true button.set_meta("icon_only", true) + button.set_meta("command_icon", icon_kind) button.add_theme_font_size_override("font_size", 1) + _connect_command_hint_button(button) func _configure_title_menu_button(button: Button, node_name: String, icon_kind: String, label: String, tooltip: String, minimum_size: Vector2, important: bool = false) -> void: @@ -1508,6 +1520,153 @@ func _format_local_command_tooltip(label: String, hint: String) -> String: return "%s\n%s" % [normalized_label, normalized_hint] +func _connect_command_hint_button(button: Button) -> void: + if button == null or bool(button.get_meta("command_hint_connected", false)): + return + button.mouse_entered.connect(_on_command_hint_button_mouse_entered.bind(button)) + button.mouse_exited.connect(_on_command_hint_button_mouse_exited) + button.set_meta("command_hint_connected", true) + + +func _create_command_hint_panel(root: Control) -> void: + if root == null: + return + command_hint_panel = PanelContainer.new() + command_hint_panel.name = "CommandHintPanel" + command_hint_panel.visible = false + command_hint_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE + command_hint_panel.custom_minimum_size = COMMAND_HINT_PANEL_SIZE + command_hint_panel.size = COMMAND_HINT_PANEL_SIZE + _apply_panel_style(command_hint_panel, "target_hint") + root.add_child(command_hint_panel) + + var row := HBoxContainer.new() + row.name = "CommandHintRow" + row.custom_minimum_size = Vector2(COMMAND_HINT_PANEL_SIZE.x - 22.0, COMMAND_HINT_PANEL_SIZE.y - 18.0) + row.add_theme_constant_override("separation", 8) + command_hint_panel.add_child(row) + + command_hint_icon = TextureRect.new() + command_hint_icon.name = "CommandHintIcon" + command_hint_icon.custom_minimum_size = COMMAND_HINT_ICON_SIZE + command_hint_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + command_hint_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + row.add_child(command_hint_icon) + + var text_column := VBoxContainer.new() + text_column.name = "CommandHintText" + text_column.add_theme_constant_override("separation", 2) + row.add_child(text_column) + + command_hint_title_label = Label.new() + command_hint_title_label.name = "CommandHintTitle" + command_hint_title_label.custom_minimum_size = COMMAND_HINT_TITLE_SIZE + command_hint_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT + _apply_label_style(command_hint_title_label, UI_OLD_BRONZE, LOCAL_COMMAND_TITLE_FONT_SIZE) + text_column.add_child(command_hint_title_label) + + command_hint_detail_label = Label.new() + command_hint_detail_label.name = "CommandHintDetail" + command_hint_detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + command_hint_detail_label.custom_minimum_size = COMMAND_HINT_DETAIL_SIZE + _apply_label_style(command_hint_detail_label, UI_PARCHMENT_TEXT, LOCAL_COMMAND_DETAIL_FONT_SIZE) + text_column.add_child(command_hint_detail_label) + + +func _on_command_hint_button_mouse_entered(button: Button) -> void: + _show_command_hint_for_button(button) + + +func _on_command_hint_button_mouse_exited() -> void: + _hide_command_hint() + + +func _show_command_hint_for_button(button: Button) -> void: + if command_hint_panel == null or button == null or not is_instance_valid(button): + return + var label := _command_hint_button_label(button) + var detail := _command_hint_button_detail(button) + if label.is_empty() and detail.is_empty(): + _hide_command_hint() + return + var tooltip := _format_local_command_tooltip(label, detail) + command_hint_source_button = button + command_hint_panel.visible = true + command_hint_panel.tooltip_text = tooltip + command_hint_panel.position = _command_hint_panel_position(button) + command_hint_panel.move_to_front() + if command_hint_title_label != null: + _set_fitted_label_text(command_hint_title_label, label, LOCAL_COMMAND_TITLE_FONT_SIZE, LOCAL_COMMAND_TITLE_MIN_FONT_SIZE, COMMAND_HINT_TITLE_SIZE) + command_hint_title_label.tooltip_text = tooltip + if command_hint_detail_label != null: + _set_fitted_label_text(command_hint_detail_label, detail, LOCAL_COMMAND_DETAIL_FONT_SIZE, LOCAL_COMMAND_DETAIL_MIN_FONT_SIZE, COMMAND_HINT_DETAIL_SIZE) + command_hint_detail_label.tooltip_text = tooltip + if command_hint_icon != null: + command_hint_icon.texture = _make_toolbar_icon(str(button.get_meta("command_icon", "status"))) + command_hint_icon.tooltip_text = tooltip + _set_control_tree_tooltip(command_hint_panel, tooltip) + + +func _hide_command_hint() -> void: + command_hint_source_button = null + if command_hint_panel != null: + command_hint_panel.visible = false + + +func _refresh_command_hint_if_source(button: Button) -> void: + if command_hint_panel == null or not command_hint_panel.visible: + return + if command_hint_source_button == button: + _show_command_hint_for_button(button) + + +func _command_hint_button_label(button: Button) -> String: + var label := str(button.get_meta("command_label", "")).strip_edges() + if not label.is_empty(): + return label + var tooltip_lines := str(button.tooltip_text).split("\n", false) + if not tooltip_lines.is_empty(): + return str(tooltip_lines[0]).strip_edges() + return str(button.text).strip_edges() + + +func _command_hint_button_detail(button: Button) -> String: + if bool(button.disabled): + var disabled_hint := str(button.get_meta("disabled_hint", "")).strip_edges() + var command_hint := str(button.get_meta("command_hint", "")).strip_edges() + if not command_hint.is_empty(): + return command_hint + if not disabled_hint.is_empty(): + return disabled_hint + var hint := str(button.get_meta("command_hint", "")).strip_edges() + if not hint.is_empty(): + return hint + var tooltip_lines := str(button.tooltip_text).split("\n", false) + if tooltip_lines.size() > 1: + var detail_lines: Array[String] = [] + for index in range(1, tooltip_lines.size()): + detail_lines.append(str(tooltip_lines[index]).strip_edges()) + return _join_strings(detail_lines, "\n") + return "" + + +func _command_hint_panel_position(button: Button) -> Vector2: + var button_rect := button.get_global_rect() + var viewport_size := _viewport_size_for_layout() + var panel_size := COMMAND_HINT_PANEL_SIZE + var gap := 8.0 + var x := button_rect.position.x + if button_rect.get_center().x > viewport_size.x * 0.5: + x = button_rect.end.x - panel_size.x + var y := button_rect.end.y + gap + if y + panel_size.y > viewport_size.y - 10.0: + y = button_rect.position.y - panel_size.y - gap + return Vector2( + clampf(x, 10.0, maxf(10.0, viewport_size.x - panel_size.x - 10.0)), + clampf(y, 10.0, maxf(10.0, viewport_size.y - panel_size.y - 10.0)) + ) + + func _make_top_hud_chip(node_name: String, icon_kind: String, chip_size: Vector2, variant: String) -> Dictionary: var chip := PanelContainer.new() chip.name = node_name @@ -3361,6 +3520,8 @@ func _create_hud() -> void: log_box.add_theme_stylebox_override("normal", _make_panel_style(Color(0.11, 0.07, 0.045, 0.78), UI_RICE_PAPER_DARK, 1, 6, 6, 0)) side_column.add_child(log_box) + _create_command_hint_panel(root) + screen_backdrop = _make_screen_backdrop() root.add_child(screen_backdrop) @@ -16041,6 +16202,7 @@ func _set_action_button_state(button: Button, label: String, disabled: bool, too button.text = normalized_label button.tooltip_text = normalized_tooltip button.disabled = disabled + _refresh_command_hint_if_source(button) func _set_action_button_blocked(button: Button, base_label: String, reason_label: String, tooltip: String) -> void: @@ -16051,6 +16213,7 @@ func _set_action_button_blocked(button: Button, base_label: String, reason_label _set_action_button_state(button, base_label, true, blocked_tooltip) if button != null: button.set_meta("disabled_hint", reason) + _refresh_command_hint_if_source(button) func _action_phase_block_reason() -> Dictionary: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 4c3208a..dabd072 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -4205,6 +4205,19 @@ func _check_hud_command_grid_layout(failures: Array[String]) -> void: continue if button.tooltip_text.strip_edges().is_empty(): failures.append("unit command icon should expose its command through tooltip: %s" % str(button.name)) + scene._on_command_hint_button_mouse_entered(scene.wait_button) + if scene.command_hint_panel == null or not scene.command_hint_panel.visible: + failures.append("hovering a unit command icon should show the command hint panel") + else: + if scene.command_hint_title_label == null or scene.command_hint_title_label.text != "대기": + failures.append("unit command hint should show command title: %s" % ("" if scene.command_hint_title_label == null else scene.command_hint_title_label.text)) + if scene.command_hint_detail_label == null or not scene.command_hint_detail_label.text.contains("부대 미지정"): + failures.append("disabled unit command hint should show the blocked reason: %s" % ("" if scene.command_hint_detail_label == null else scene.command_hint_detail_label.text)) + if scene.command_hint_icon == null or scene.command_hint_icon.texture == null: + failures.append("unit command hint should show generated command icon artwork") + scene._on_command_hint_button_mouse_exited() + if scene.command_hint_panel != null and scene.command_hint_panel.visible: + failures.append("unit command hint panel should hide when leaving the icon") var top_toolbar := scene.end_turn_button.get_parent() as HBoxContainer if top_toolbar == null or top_toolbar.name != "TopToolBar": failures.append("global battle commands should move to the top icon toolbar") @@ -4228,6 +4241,16 @@ func _check_hud_command_grid_layout(failures: Array[String]) -> void: _check_button_uses_generated_surface(failures, button, str(button.name)) if button.custom_minimum_size.x < BattleSceneScript.TOP_TOOL_BUTTON_SIZE.x or button.custom_minimum_size.y < BattleSceneScript.TOP_TOOL_BUTTON_SIZE.y: failures.append("top tool button should reserve stable icon space: %s" % str(button.custom_minimum_size)) + scene._on_command_hint_button_mouse_entered(scene.end_turn_button) + if scene.command_hint_panel == null or not scene.command_hint_panel.visible: + failures.append("hovering a top command icon should show the command hint panel") + else: + if scene.command_hint_title_label == null or scene.command_hint_title_label.text != "차례 종료": + failures.append("top command hint should show command title: %s" % ("" if scene.command_hint_title_label == null else scene.command_hint_title_label.text)) + if scene.command_hint_detail_label == null or not scene.command_hint_detail_label.text.contains("적군"): + failures.append("top command hint should keep command detail: %s" % ("" if scene.command_hint_detail_label == null else scene.command_hint_detail_label.text)) + if scene.command_hint_icon == null or scene.command_hint_icon.texture == null or str(scene.end_turn_button.get_meta("command_icon", "")) != "end_turn": + failures.append("top command hint should use generated top command icon artwork") scene.free()