Files
mtgodot-poc/project/ui/mouse_controller.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

334 lines
10 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.
# 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()