Files
mtgodot-poc/project/ui/cursor_manager.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

154 lines
4.4 KiB
GDScript

# CursorManager —— 对齐 ClientVS22 mousemodule.py 的光标状态机。
#
# 光标和“鼠标跟随物品”是两层:本节点只管理当前交互语义,
# MouseController 负责跟随鼠标的物品图标。使用 40250 的 .sub 纹理注册真实光标,
# 没有对应资源时回退到 Godot 系统光标。
extends Node
signal changed(shape: String)
const UiAssets = preload("res://ui/ui_assets.gd")
const NORMAL := "NORMAL"
const ATTACK := "ATTACK"
const TARGET := "TARGET"
const TALK := "TALK"
const CANT_GO := "CANT_GO"
const PICK := "PICK"
const DOOR := "DOOR"
const CHAIR := "CHAIR"
const MAGIC := "MAGIC"
const BUY := "BUY"
const SELL := "SELL"
const CAMERA_ROTATE := "CAMERA_ROTATE"
const HSIZE := "HSIZE"
const VSIZE := "VSIZE"
const HVSIZE := "HVSIZE"
const ITEM := "ITEM"
const ALL_CURSORS := [
NORMAL, ATTACK, TARGET, TALK, CANT_GO, PICK, DOOR, CHAIR, MAGIC,
BUY, SELL, CAMERA_ROTATE, HSIZE, VSIZE, HVSIZE, ITEM
]
var _shape := NORMAL
var _assets_root := ""
var _custom_cursors_registered := false
var _cursor_images: Dictionary = {}
func setup(assets_root: String) -> void:
_assets_root = assets_root
_register_custom_cursors()
func _register_custom_cursors() -> void:
if _assets_root.is_empty():
return
var cursors := {
NORMAL: "ETC/ymir work/ui/cursor/cursor.sub",
ATTACK: "ETC/ymir work/ui/cursor/cursor_attack.sub",
TARGET: "ETC/ymir work/ui/cursor/cursor_attack.sub",
TALK: "ETC/ymir work/ui/cursor/cursor_talk.sub",
CANT_GO: "ETC/ymir work/ui/cursor/cursor_no.sub",
PICK: "ETC/ymir work/ui/cursor/cursor_pick.sub",
DOOR: "ETC/ymir work/ui/cursor/cursor_door.sub",
CHAIR: "ETC/ymir work/ui/cursor/cursor_chair.sub",
MAGIC: "ETC/ymir work/ui/cursor/cursor_chair.sub",
BUY: "ETC/ymir work/ui/cursor/cursor_buy.sub",
SELL: "ETC/ymir work/ui/cursor/cursor_sell.sub",
CAMERA_ROTATE: "ETC/ymir work/ui/cursor/cursor_camera_rotate.sub",
HSIZE: "ETC/ymir work/ui/cursor/cursor_hsize.sub",
VSIZE: "ETC/ymir work/ui/cursor/cursor_vsize.sub",
HVSIZE: "ETC/ymir work/ui/cursor/cursor_hvsize.sub",
}
var shape_to_godot := {
NORMAL: Input.CURSOR_ARROW,
ATTACK: Input.CURSOR_CROSS,
TALK: Input.CURSOR_POINTING_HAND,
PICK: Input.CURSOR_CAN_DROP,
CANT_GO: Input.CURSOR_FORBIDDEN,
BUY: Input.CURSOR_DRAG,
SELL: Input.CURSOR_MOVE,
DOOR: Input.CURSOR_HELP,
CHAIR: Input.CURSOR_WAIT,
CAMERA_ROTATE: Input.CURSOR_BDIAGSIZE,
HSIZE: Input.CURSOR_HSIZE,
VSIZE: Input.CURSOR_VSIZE,
HVSIZE: Input.CURSOR_FDIAGSIZE,
}
var hotspots := {
HSIZE: Vector2(16, 16),
VSIZE: Vector2(16, 16),
HVSIZE: Vector2(16, 16),
}
for name in cursors:
var rel: String = cursors[name]
var tex: Texture2D = UiAssets.load_tex(_assets_root, rel)
if tex != null:
var img: Image = _extract_image(tex)
if img != null:
_cursor_images[name] = img
var hs: Vector2 = hotspots.get(name, Vector2.ZERO)
if shape_to_godot.has(name):
Input.set_custom_mouse_cursor(img, shape_to_godot[name], hs)
_custom_cursors_registered = true
func _extract_image(tex: Texture2D) -> Image:
if tex is AtlasTexture:
var at := tex as AtlasTexture
if at.atlas:
var base_img: Image = at.atlas.get_image()
if base_img:
var r := Rect2i(int(at.region.position.x), int(at.region.position.y), int(at.region.size.x), int(at.region.size.y))
return base_img.get_region(r)
elif tex:
return tex.get_image()
return null
func set_cursor(shape: String) -> void:
if shape not in ALL_CURSORS:
shape = NORMAL
_shape = shape
Input.set_default_cursor_shape(_godot_shape(shape))
if _cursor_images.has(shape):
var hs := Vector2(16, 16) if shape in [HSIZE, VSIZE, HVSIZE] else Vector2.ZERO
Input.set_custom_mouse_cursor(_cursor_images[shape], Input.CURSOR_ARROW, hs)
changed.emit(shape)
func get_cursor() -> String:
return _shape
func reset() -> void:
set_cursor(NORMAL)
func _godot_shape(shape: String) -> Input.CursorShape:
match shape:
ATTACK, TARGET:
return Input.CURSOR_CROSS
PICK:
return Input.CURSOR_CAN_DROP
TALK:
return Input.CURSOR_POINTING_HAND
DOOR:
return Input.CURSOR_HELP
CHAIR, MAGIC:
return Input.CURSOR_WAIT
BUY:
return Input.CURSOR_DRAG
SELL:
return Input.CURSOR_MOVE
CAMERA_ROTATE:
return Input.CURSOR_BDIAGSIZE
HSIZE:
return Input.CURSOR_HSIZE
VSIZE:
return Input.CURSOR_VSIZE
HVSIZE:
return Input.CURSOR_FDIAGSIZE
CANT_GO:
return Input.CURSOR_FORBIDDEN
ITEM:
return Input.CURSOR_DRAG
_:
return Input.CURSOR_ARROW