forked from xiangwang25/whale-town-front-v2
264 lines
9.3 KiB
GDScript
264 lines
9.3 KiB
GDScript
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
|