Files
whale-town-front-v2/_Core/managers/InteractionManager.gd
2026-07-21 23:07:31 +08:00

264 lines
9.3 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Node
# 全地图统一交互收集角色附近的动作通过方向键选择、E 执行。
const SCAN_INTERVAL: float = 0.10
const MAX_ACTIONS_VISIBLE: int = 5
const ACCENT: Color = Color("58c7db")
const PANEL_COLOR: Color = Color(0.035, 0.071, 0.106, 0.94)
const INTERACTION_POINT_MARKERS_SCRIPT: Script = preload("res://_Core/ui/InteractionPointMarkers.gd")
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
var _actions: Array[Dictionary] = []
var _selected_index: int = 0
var _last_selected_id: String = ""
var _scan_elapsed: float = SCAN_INTERVAL
var _canvas: CanvasLayer
var _panel: PanelContainer
var _list: VBoxContainer
var _hint: Label
var _executing: bool = false
var _pointMarkers: Node2D
func _ready() -> void:
_build_hud()
func _process(delta: float) -> void:
_scan_elapsed += delta
if _scan_elapsed < SCAN_INTERVAL:
return
_scan_elapsed = 0.0
_refresh_actions()
func _unhandled_input(event: InputEvent) -> void:
if _actions.is_empty() or _is_text_input_focused():
return
if not (event is InputEventKey):
return
var key_event := event as InputEventKey
if not key_event.pressed or key_event.echo:
return
if key_event.keycode == KEY_UP:
_select_offset(-1)
get_viewport().set_input_as_handled()
elif key_event.keycode == KEY_DOWN:
_select_offset(1)
get_viewport().set_input_as_handled()
elif key_event.keycode == KEY_E:
_execute_selected()
get_viewport().set_input_as_handled()
func is_selection_active() -> bool:
return not _actions.is_empty() and not _is_text_input_focused()
func _refresh_actions() -> void:
if _is_text_input_focused() or SceneManager.is_changing_scene:
_set_actions([])
return
var player := _local_player()
if player == null:
_set_actions([])
return
var collected: Array[Dictionary] = []
for node in get_tree().get_nodes_in_group("whaletown_interactable"):
if not is_instance_valid(node) or not node.has_method("get_interaction_actions"):
continue
var actions_variant: Variant = node.call("get_interaction_actions", player)
if not (actions_variant is Array):
continue
for item in actions_variant as Array:
if item is Dictionary:
var action: Dictionary = item
if bool(action.get("available", true)):
collected.append(action)
collected.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
var priority_a := int(a.get("priority", 100))
var priority_b := int(b.get("priority", 100))
if priority_a != priority_b:
return priority_a < priority_b
var distance_a := float(a.get("distance", INF))
var distance_b := float(b.get("distance", INF))
if not is_equal_approx(distance_a, distance_b):
return distance_a < distance_b
return str(a.get("title", "")) < str(b.get("title", ""))
)
_set_actions(collected)
func _set_actions(next_actions: Array[Dictionary]) -> void:
_actions = next_actions
if _actions.is_empty():
_selected_index = 0
_last_selected_id = ""
_render()
return
var matched_index := -1
for index in _actions.size():
if str(_actions[index].get("id", "")) == _last_selected_id:
matched_index = index
break
_selected_index = matched_index if matched_index >= 0 else clampi(_selected_index, 0, _actions.size() - 1)
_last_selected_id = str(_actions[_selected_index].get("id", ""))
_render()
func _select_offset(offset: int) -> void:
if _actions.is_empty():
return
_selected_index = posmod(_selected_index + offset, _actions.size())
_last_selected_id = str(_actions[_selected_index].get("id", ""))
_render()
func _execute_selected() -> void:
if _executing or _actions.is_empty():
return
var action := _actions[_selected_index]
var callback_variant: Variant = action.get("callback", Callable())
if not (callback_variant is Callable) or not (callback_variant as Callable).is_valid():
return
_executing = true
_render()
(callback_variant as Callable).call()
await get_tree().create_timer(0.18).timeout
_executing = false
_refresh_actions()
func _local_player() -> Node2D:
var players := get_tree().get_nodes_in_group("whaletown_local_player")
return players.front() as Node2D if not players.is_empty() else null
func _build_hud() -> void:
_canvas = CanvasLayer.new()
_canvas.layer = 90
add_child(_canvas)
_panel = PanelContainer.new()
_panel.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
_panel.position = Vector2(-250, -282)
_panel.size = Vector2(500, 214)
_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
_panel.add_theme_stylebox_override("panel", _panel_style())
_canvas.add_child(_panel)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 14)
margin.add_theme_constant_override("margin_top", 10)
margin.add_theme_constant_override("margin_right", 14)
margin.add_theme_constant_override("margin_bottom", 10)
_panel.add_child(margin)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 4)
margin.add_child(content)
_list = VBoxContainer.new()
_list.add_theme_constant_override("separation", 2)
content.add_child(_list)
_hint = Label.new()
_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_hint.add_theme_color_override("font_color", Color(0.67, 0.79, 0.85, 1.0))
_hint.add_theme_font_size_override("font_size", 14)
_hint.text = "[↑/↓] 选择 [E] 交互"
content.add_child(_hint)
_render()
func _render() -> void:
if not is_instance_valid(_panel):
return
var show_hints: bool = _should_show_interaction_hints()
_panel.visible = show_hints and not _actions.is_empty()
_render_interaction_points()
for child in _list.get_children():
child.queue_free()
if _actions.is_empty():
return
var start: int = clampi(_selected_index - 2, 0, maxi(0, _actions.size() - MAX_ACTIONS_VISIBLE))
var finish: int = mini(_actions.size(), start + MAX_ACTIONS_VISIBLE)
for index in range(start, finish):
var action := _actions[index]
var row := Label.new()
row.custom_minimum_size = Vector2(0, 29)
row.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
row.add_theme_font_size_override("font_size", 16)
var selected := index == _selected_index
row.text = (" " if selected else " ") + str(action.get("title", "交互")) + ("" if selected and _executing else "")
row.add_theme_color_override("font_color", ACCENT if selected else Color(0.92, 0.96, 0.98, 1.0))
if selected:
row.add_theme_stylebox_override("normal", _selected_style())
_list.add_child(row)
_hint.text = "[↑/↓] 选择 [E] 交互 [Esc] 关闭" + (" %d" % _actions.size() if _actions.size() > 1 else "")
func _render_interaction_points() -> void:
if not _should_show_interaction_points():
if is_instance_valid(_pointMarkers):
var empty_points: Array[Vector2] = []
_pointMarkers.call("set_points", empty_points)
return
var markers: Node2D = _ensure_point_markers()
if markers == null:
return
var positions: Array[Vector2] = []
for node: Node in get_tree().get_nodes_in_group("whaletown_interactable"):
if not is_instance_valid(node) or not node.has_method("get_interaction_actions"):
continue
if node.has_method("get_interaction_marker_positions"):
var marker_positions_variant: Variant = node.call("get_interaction_marker_positions")
if marker_positions_variant is Array:
for position_variant: Variant in marker_positions_variant as Array:
if position_variant is Vector2:
positions.append(position_variant as Vector2)
continue
var source_node: Node2D = node as Node2D
if source_node != null:
positions.append(InteractionAnchorUtil.get_position(source_node))
markers.call("set_points", positions)
func _ensure_point_markers() -> Node2D:
var current_scene: Node = get_tree().current_scene
var world_root: Node2D = current_scene as Node2D
if world_root == null:
return null
if is_instance_valid(_pointMarkers) and _pointMarkers.get_parent() == world_root:
return _pointMarkers
var markers: Node2D = INTERACTION_POINT_MARKERS_SCRIPT.new() as Node2D
if markers == null:
return null
markers.name = "InteractionPointMarkers"
markers.z_index = 4096
markers.z_as_relative = false
world_root.add_child(markers)
_pointMarkers = markers
return _pointMarkers
func _should_show_interaction_hints() -> bool:
return _get_setting_enabled("show_interaction_hints", true)
func _should_show_interaction_points() -> bool:
return _get_setting_enabled("show_interaction_points", false)
func _get_setting_enabled(setting_key: String, fallback: bool) -> bool:
var settings_manager: Node = get_node_or_null("/root/SettingsManager")
if settings_manager != null and settings_manager.has_method("get_bool"):
return bool(settings_manager.call("get_bool", setting_key))
return fallback
func _panel_style() -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = PANEL_COLOR
style.border_color = Color(0.20, 0.45, 0.54, 0.86)
style.set_border_width_all(1)
style.corner_radius_top_left = 10
style.corner_radius_top_right = 10
style.corner_radius_bottom_left = 10
style.corner_radius_bottom_right = 10
style.shadow_color = Color(0, 0, 0, 0.35)
style.shadow_size = 10
return style
func _selected_style() -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color(0.12, 0.32, 0.38, 0.74)
style.corner_radius_top_left = 6
style.corner_radius_top_right = 6
style.corner_radius_bottom_left = 6
style.corner_radius_bottom_right = 6
style.content_margin_left = 6
return style
func _is_text_input_focused() -> bool:
var focus_owner := get_viewport().gui_get_focus_owner()
return focus_owner is LineEdit or focus_owner is TextEdit