- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
154 lines
4.4 KiB
GDScript
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
|
|
|