# MouseController —— ClientVS22 mousemodule.py 的全局物品拖放层。 # # 目标控件通过 register_target() 注册;物品源调用 attach_item() 后,控制器 # 接管鼠标释放,命中目标时调用回调,释放到窗口外 / 右键 / ESC / 失焦则取消。 # 光标状态由 CursorManager 管理,跟随图标不参与世界点击。 extends Node const CursorManager = preload("res://ui/cursor_manager.gd") var cursor: Node var audio: Node var _overlay: Control var _icon: TextureRect var _label: Label var _attached: Dictionary = {} var _targets: Array[Dictionary] = [] var _touch_index := -1 var _touch_position := Vector2.ZERO var world_drop_handler: Callable func setup(parent: Node, cursor_manager: Node = null, audio_node: Node = null) -> void: cursor = cursor_manager audio = audio_node _overlay = Control.new() _overlay.name = "MouseItemOverlay" _overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE _overlay.z_index = 1000 _overlay.visible = false _overlay.size = Vector2(72, 72) parent.add_child(_overlay) _icon = TextureRect.new() _icon.position = Vector2(0, 0) _icon.size = Vector2(48, 48) _icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE _icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED _icon.mouse_filter = Control.MOUSE_FILTER_IGNORE _overlay.add_child(_icon) _label = Label.new() _label.position = Vector2(2, 48) _label.size = Vector2(68, 20) _label.add_theme_font_size_override("font_size", 10) _label.mouse_filter = Control.MOUSE_FILTER_IGNORE _overlay.add_child(_label) set_process_input(true) var _drag_started := false var _attach_pos := Vector2(-1, -1) var _just_attached := false func _on_attach() -> void: _drag_started = false _just_attached = true if is_inside_tree(): _attach_pos = get_viewport().get_mouse_position() else: _attach_pos = Vector2(-1, -1) func attach_item(window: int, cell: int, vnum: int, count: int, texture: Texture2D = null, source := "inventory", metadata: Dictionary = {}, touch_index := -1, touch_position := Vector2(-1, -1)) -> bool: if vnum <= 0 or window < 0 or cell < 0: return false _attached = {"window": window, "cell": cell, "vnum": vnum, "count": maxi(1, count), "source": source} _touch_index = touch_index if touch_position.x >= 0.0 and touch_position.y >= 0.0: _touch_position = touch_position # Keep the instance fields that the reference mouse module exposes to the # receiving window (anti flags, sockets, attributes, ...), while retaining # the canonical source coordinates supplied by the caller. for key in metadata: if key not in ["window", "cell", "vnum", "count", "source"]: _attached[key] = metadata[key] if _icon: _icon.texture = texture _icon.visible = texture != null if _label: _label.text = "#%d%s" % [vnum, (" ×%d" % count) if count > 1 else ""] _label.visible = true if _overlay: _overlay.visible = true if cursor and cursor.has_method("set_cursor"): cursor.set_cursor(CursorManager.ITEM) _play_ui("pick.wav") _on_attach() return true func attach_money(amount: int, source := "inventory") -> bool: if amount <= 0: return false _attached = {"window": -1, "cell": -1, "vnum": -1, "count": amount, "source": source, "money": true} _touch_index = -1 _touch_position = Vector2.ZERO if _icon: _icon.texture = null _icon.visible = false if _label: _label.text = "金币 ×%d" % amount _label.visible = true if _overlay: _overlay.visible = true if cursor and cursor.has_method("set_cursor"): cursor.set_cursor(CursorManager.ITEM) _play_ui("pick.wav") _on_attach() return true func attach_skill(skill_id: int, slot_idx: int = 0, texture: Texture2D = null, skill_name := "", touch_index := -1, touch_position := Vector2(-1, -1)) -> bool: if skill_id <= 0: return false _attached = { "source": "skill", "skill_id": skill_id, "slot_idx": slot_idx, } _touch_index = touch_index if touch_position.x >= 0.0 and touch_position.y >= 0.0: _touch_position = touch_position if _icon: _icon.texture = texture _icon.visible = texture != null if _label: _label.text = skill_name if skill_name != "" else "技能 %d" % skill_id _label.visible = true if _overlay: _overlay.visible = true if cursor and cursor.has_method("set_cursor"): cursor.set_cursor(CursorManager.ITEM) _play_ui("pick.wav") _on_attach() return true func attach_emotion(emote_id: int, texture: Texture2D = null, emote_name := "", touch_index := -1, touch_position := Vector2(-1, -1)) -> bool: if emote_id <= 0: return false _attached = { "source": "emotion", "emote_id": emote_id, } _touch_index = touch_index if touch_position.x >= 0.0 and touch_position.y >= 0.0: _touch_position = touch_position if _icon: _icon.texture = texture _icon.visible = texture != null if _label: _label.text = emote_name if emote_name != "" else "动作 %d" % emote_id _label.visible = true if _overlay: _overlay.visible = true if cursor and cursor.has_method("set_cursor"): cursor.set_cursor(CursorManager.ITEM) _play_ui("pick.wav") _on_attach() return true func cancel() -> void: _attached.clear() _touch_index = -1 _touch_position = Vector2.ZERO _drag_started = false _attach_pos = Vector2(-1, -1) _just_attached = false if _overlay: _overlay.visible = false if cursor and cursor.has_method("reset"): cursor.reset() func is_attached() -> bool: return not _attached.is_empty() func attached() -> Dictionary: return _attached.duplicate(true) func register_target(control: Control, callback: Callable, owner: Node = null) -> void: if control == null or not callback.is_valid(): return _targets.append({"control": control, "callback": callback, "owner": owner}) func unregister_owner(owner: Node) -> void: for i in range(_targets.size() - 1, -1, -1): if _targets[i].get("owner", null) == owner: _targets.remove_at(i) func _process(_dt: float) -> void: if _overlay and _overlay.visible: var position := get_viewport().get_mouse_position() if _touch_index >= 0: position = _touch_position _overlay.global_position = position + Vector2(12, 12) # 清理已被窗口销毁的目标,防止拖放回调落到旧 UI。 for i in range(_targets.size() - 1, -1, -1): var c: Control = _targets[i].get("control", null) if c == null or not is_instance_valid(c): _targets.remove_at(i) func _set_handled() -> void: var vp := get_viewport() if vp: vp.set_input_as_handled() func _input(event: InputEvent) -> void: if not is_attached(): return if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE: cancel() _set_handled() return if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT: cancel() _set_handled() return if event is InputEventMouseMotion: if _attach_pos.x >= 0.0 and (_attach_pos - event.position).length() > 6.0: _drag_started = true return if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: # If user already picked up an item and left-clicks on a target: if not _just_attached: if _drop_at(event.position): _set_handled() return # If clicked outside any target, cancel attachment if _attach_pos.x >= 0.0 and (_attach_pos - event.position).length() > 6.0: _play_ui("loginfail.wav") cancel() _set_handled() return else: # Left mouse release (not pressed) _just_attached = false # If dragged, attempt drop at release position if _drag_started or (_attach_pos.x >= 0.0 and (_attach_pos - event.position).length() > 8.0): _drop_at(event.position) _set_handled() return # If not dragged (clicked to pick up), keep item attached to cursor! return if event is InputEventScreenDrag and event.index == _touch_index: _touch_position = event.position _set_handled() return if event is InputEventScreenTouch and not event.pressed and event.index == _touch_index: _touch_position = event.position _drop_at(event.position) _set_handled() func _drop_at(position: Vector2) -> bool: var payload := attached() if payload.is_empty(): return false var icon_center := position + Vector2(24, 24) var best_idx := -1 var best_dist := 999999.0 # Pass 1: exact hit test (cursor position or icon center) for i in range(_targets.size() - 1, -1, -1): var c: Control = _targets[i].get("control", null) if c == null or not is_instance_valid(c): _targets.remove_at(i) continue if not c.visible: continue var rect := c.get_global_rect() if rect.has_point(position): var d := position.distance_to(rect.get_center()) if d < best_dist: best_dist = d best_idx = i elif rect.has_point(icon_center): var d := icon_center.distance_to(rect.get_center()) if d < best_dist: best_dist = d best_idx = i # Pass 2: generous hit test with 16px tolerance margin if best_idx < 0: for i in range(_targets.size() - 1, -1, -1): var c: Control = _targets[i].get("control", null) if c == null or not is_instance_valid(c) or not c.visible: continue var rect := c.get_global_rect().grow(16.0) if rect.has_point(position): var d := position.distance_to(rect.get_center()) if d < best_dist: best_dist = d best_idx = i elif rect.has_point(icon_center): var d := icon_center.distance_to(rect.get_center()) if d < best_dist: best_dist = d best_idx = i if best_idx >= 0: var cb: Callable = _targets[best_idx].get("callback", Callable()) if cb.is_valid(): var ok := false if cb.get_argument_count() >= 2: ok = bool(cb.call(payload, position)) else: ok = bool(cb.call(payload)) if ok: _play_ui("drop.wav") cancel() return true if world_drop_handler.is_valid(): var ok := false if world_drop_handler.get_argument_count() >= 2: ok = bool(world_drop_handler.call(payload, position)) else: ok = bool(world_drop_handler.call(payload)) if ok: _play_ui("drop.wav") cancel() return true _play_ui("loginfail.wav") cancel() return false func _play_ui(name: String) -> void: if audio and audio.has_method("play_ui"): audio.play_ui(name) func _notification(what: int) -> void: if what == NOTIFICATION_WM_WINDOW_FOCUS_OUT: cancel()