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 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
+186
-52
@@ -10,12 +10,18 @@ extends Node
|
||||
|
||||
signal anim_state(state: String)
|
||||
signal target_selected(node: Node3D)
|
||||
signal ground_item_clicked(vid: int)
|
||||
signal moved(pos: Vector3)
|
||||
# game.py OnRender:wndMgr.IsPickedWindow 门内 chr.Pick() 拾到的角色 -> ShowCharacterTextTail。
|
||||
# 悬停命中的实体 vid 变化时 emit(无命中 / 鼠标压 UI / 悬停地面物品 -> emit 0)。见 pick_show.gd。
|
||||
signal hover_entity_changed(vid: int)
|
||||
|
||||
var player: Node3D
|
||||
var player: Node3D:
|
||||
set(v):
|
||||
player = v
|
||||
if player:
|
||||
_src_pos = player.position
|
||||
_dst_pos = player.position
|
||||
var camera: Camera3D
|
||||
var world: Node
|
||||
var cursor_manager: Node
|
||||
@@ -26,10 +32,10 @@ var cancel_fishing_ground := Callable() # __OnPressGround 专用入口
|
||||
var pickables: Array[Node3D] = [] # 可点选实体(NPC/怪等)
|
||||
var _hover_vid := 0 # 上一帧悬停命中的实体 vid(hover_entity_changed 去抖)
|
||||
|
||||
const SPEED_WALK := 8.0
|
||||
const SPEED_RUN := 18.0
|
||||
const SPEED_WALK := 1.8
|
||||
const SPEED_RUN := 4.8
|
||||
const RUN_HOLD_KEY := KEY_SHIFT
|
||||
const ARRIVE_EPS := 0.4
|
||||
const ARRIVE_EPS := 0.05
|
||||
const PICK_RADIUS := 1.4 # 点选命中半径(米)
|
||||
# __IsMovableGroundDistance:点地目标离脚下太近就不动(避免原地抖)。参考默认由
|
||||
# player.SetMovableGroundDistance 从 game.py 设置;此处用参考缺省值(~1 m)。§3.1
|
||||
@@ -47,7 +53,9 @@ const ACTOR_COLLIDE_MAX_DIST_M := 8.0
|
||||
const ROT_SPEED_DEFAULT_DEG := 1200.0
|
||||
const ROT_SPEED_HORSE_DEG := 300.0
|
||||
|
||||
var default_run := true # 40250 对齐:默认奔跑模式(ActorInstance.cpp:544 m_isWalking = FALSE)
|
||||
var force_run := false # 脚本化 / 自动跑
|
||||
var force_walk := false # 步行模式
|
||||
var frozen := false # 受击硬直等:本帧不响应移动输入(net_play 设)
|
||||
var locked := false # CInstanceBase::isLock():完全锁死移动(过场 / 特定 affect)
|
||||
var moving_skill := false # IsUsingMovingSkill():移动技能中,只允许转向不平移
|
||||
@@ -70,13 +78,31 @@ var _dst_rot := 0.0
|
||||
var _reserved_ground: Variant = null # __ReserveClickGround 的待处理点
|
||||
var _reserved_delay_time := 0.0 # NEW_IsEmptyReservedDelayTime 递减到 0 才生效
|
||||
|
||||
var active := true # 进游戏装配/加载中置假,防止过早响应点地
|
||||
# 40250 CInstanceBase::NEW_Stop() / CPythonPlayer::NEW_Stop():停止点地移动与方向输入,重置预约
|
||||
func stop() -> void:
|
||||
_is_going = false
|
||||
_reserved_ground = null
|
||||
_reserved_delay_time = 0.0
|
||||
_last_wasd = Vector2.ZERO
|
||||
_mobile_active = false
|
||||
_mobile_axis = Vector2.ZERO
|
||||
_mobile_ui_touches.clear()
|
||||
_multi_gesture = false
|
||||
_touch_count = 0
|
||||
_tap_index = -1
|
||||
if player:
|
||||
_src_pos = player.position
|
||||
_dst_pos = player.position
|
||||
anim_state.emit("wait")
|
||||
|
||||
# CInstanceBase::__IsSyncing():Dead / Stun / Pushing 中不接受移动输入(net_play 置 frozen)。
|
||||
func is_going() -> bool:
|
||||
return _is_going
|
||||
|
||||
# NEW_Goto / NEW_MoveToDirection 的三道前置门:syncing / moving-skill(只转向) / lock。
|
||||
func _can_translate() -> bool:
|
||||
return not frozen and not locked and not moving_skill
|
||||
return active and not frozen and not locked and not moving_skill
|
||||
|
||||
const _TAP_TRAVEL_MAX := 12.0 # 触点位移超过这么多像素 -> 视作拖拽(归相机),不是轻点
|
||||
const _TAP_TIME_MAX := 0.35 # 按下到抬起超过这么久 -> 不是轻点
|
||||
@@ -138,7 +164,7 @@ func walk_to(world_pos: Vector3) -> void:
|
||||
# CInstanceBase::NEW_Goto(InstanceBaseMovement.cpp:251):三门通过后设 Src/Dst + m_isGoing。
|
||||
# 被门挡下(syncing / lock)时按 __ReserveClickGround 预约,delay 后重试。
|
||||
func _goto(dst_flat: Vector3) -> bool:
|
||||
if player == null:
|
||||
if not active or player == null:
|
||||
return false
|
||||
var flat := Vector3(dst_flat.x - player.position.x, 0.0, dst_flat.z - player.position.z)
|
||||
# __IsMovableGroundDistance:离脚下太近不动
|
||||
@@ -155,6 +181,8 @@ func _goto(dst_flat: Vector3) -> bool:
|
||||
return true
|
||||
|
||||
func _unhandled_input(e: InputEvent) -> void:
|
||||
if not active:
|
||||
return
|
||||
if e is InputEventMouseButton and e.button_index == MOUSE_BUTTON_LEFT and e.pressed:
|
||||
_on_click(e.position)
|
||||
elif e is InputEventScreenTouch:
|
||||
@@ -186,8 +214,16 @@ func _on_touch(e: InputEventScreenTouch) -> void:
|
||||
_multi_gesture = false
|
||||
|
||||
func _on_click(screen_pos: Vector2) -> void:
|
||||
if camera == null:
|
||||
if not active or camera == null or player == null:
|
||||
return
|
||||
|
||||
# 0) 掉落物点选(对齐 40250 __GetPickedItemID 优先于 Actor/Ground)
|
||||
if ground_items and ground_items.has_method("pick_at"):
|
||||
var picked_item_vid: int = ground_items.call("pick_at", camera, screen_pos)
|
||||
if picked_item_vid != 0:
|
||||
ground_item_clicked.emit(picked_item_vid)
|
||||
return
|
||||
|
||||
var from := camera.project_ray_origin(screen_pos)
|
||||
var dir := camera.project_ray_normal(screen_pos)
|
||||
|
||||
@@ -198,6 +234,8 @@ func _on_click(screen_pos: Vector2) -> void:
|
||||
for n in pickables:
|
||||
if not is_instance_valid(n):
|
||||
continue
|
||||
if bool(n.get_meta("dead", false)):
|
||||
continue
|
||||
var t := _ray_pick_t(from, dir, n)
|
||||
if t >= 0.0 and t < best_t:
|
||||
best_t = t
|
||||
@@ -251,17 +289,18 @@ func _ray_pick_t(from: Vector3, dir: Vector3, node: Node3D) -> float:
|
||||
var t2 := (hi - o) / d
|
||||
if t1 > t2:
|
||||
var tmp := t1; t1 = t2; t2 = tmp
|
||||
tmin = maxf(tmin, t1)
|
||||
tmax = minf(tmax, t2)
|
||||
if tmin > tmax:
|
||||
if t1 > tmin: tmin = t1
|
||||
if t2 < tmax: tmax = t2
|
||||
if tmin > tmax or tmax < 0.0:
|
||||
return -1.0
|
||||
return tmin if tmin >= 0.0 else (tmax if tmax >= 0.0 else -1.0)
|
||||
return tmin if tmin >= 0.0 else tmax
|
||||
|
||||
# 射线 vs 地表:沿射线在 [0, 80m] 区间二分找 ray.y == world.sample_height(ray.x, ray.z)。
|
||||
func _ray_ground(from: Vector3, dir: Vector3) -> Variant:
|
||||
if world == null or not world.has_method("sample_height"):
|
||||
return null
|
||||
var t := 0.0
|
||||
var last_above: bool = from.y - float(world.call("sample_height", from.x, from.z)) > 0.0
|
||||
var t := 0.0
|
||||
for _i in 240:
|
||||
t += 1.0
|
||||
var p := from + dir * t
|
||||
@@ -285,11 +324,27 @@ func _ray_ground(from: Vector3, dir: Vector3) -> Variant:
|
||||
return null
|
||||
|
||||
func _wasd() -> Vector2:
|
||||
if ui_manager and ui_manager.has_method("blocks_game_input"):
|
||||
var dummy := InputEventKey.new()
|
||||
dummy.keycode = KEY_W
|
||||
if ui_manager.blocks_game_input(dummy):
|
||||
return Vector2.ZERO
|
||||
var is_up := Input.is_key_pressed(KEY_W) or Input.is_physical_key_pressed(KEY_W) or Input.is_key_pressed(KEY_UP)
|
||||
var is_down := Input.is_key_pressed(KEY_S) or Input.is_physical_key_pressed(KEY_S) or Input.is_key_pressed(KEY_DOWN)
|
||||
var is_left := Input.is_key_pressed(KEY_A) or Input.is_physical_key_pressed(KEY_A) or Input.is_key_pressed(KEY_LEFT)
|
||||
var is_right := Input.is_key_pressed(KEY_D) or Input.is_physical_key_pressed(KEY_D) or Input.is_key_pressed(KEY_RIGHT)
|
||||
if not (is_up or is_down or is_left or is_right):
|
||||
return Vector2.ZERO
|
||||
var d := Vector2.ZERO
|
||||
if Input.is_key_pressed(KEY_W) or Input.is_key_pressed(KEY_UP): d.y -= 1
|
||||
if Input.is_key_pressed(KEY_S) or Input.is_key_pressed(KEY_DOWN): d.y += 1
|
||||
if Input.is_key_pressed(KEY_A) or Input.is_key_pressed(KEY_LEFT): d.x -= 1
|
||||
if Input.is_key_pressed(KEY_D) or Input.is_key_pressed(KEY_RIGHT): d.x += 1
|
||||
# 对齐 40250 CPythonPlayer::NEW_GetMultiKeyDirRotation: Up优先于Down,Left优先于Right
|
||||
if is_up:
|
||||
d.y = -1.0
|
||||
elif is_down:
|
||||
d.y = 1.0
|
||||
if is_left:
|
||||
d.x = -1.0
|
||||
elif is_right:
|
||||
d.x = 1.0
|
||||
return d.normalized()
|
||||
|
||||
func _blocked(x: float, z: float) -> bool:
|
||||
@@ -298,40 +353,62 @@ func _blocked(x: float, z: float) -> bool:
|
||||
# CActorInstance::TestActorCollision(ActorInstanceCollisionDetection.cpp)近似:距离门
|
||||
# (800 cm)→ body 球重叠 → 「趋近」判定(新位比旧位更近才算撞,远离时放行 → 能从
|
||||
# 重叠里走出来)。每个 pickable(已生成的远端 NPC / 怪 / 他人节点)当作一个 Actor 碰撞体;
|
||||
# `skip_actor_collision`(CanSkipCollision)为真整段跳过(≈ CheckAdvancing 开头)。
|
||||
func _actor_blocked(next_pos: Vector3) -> bool:
|
||||
if skip_actor_collision or player == null:
|
||||
static func _is_static_actor(n: Node3D) -> bool:
|
||||
if not is_instance_valid(n):
|
||||
return false
|
||||
var cur := player.position
|
||||
var shape: String = String(n.get_meta("cursor_shape", ""))
|
||||
if shape == "DOOR":
|
||||
return true
|
||||
var kind: int = int(n.get_meta("kind", -1))
|
||||
if kind in [0, 1, 3]: # KIND_BUILDING, KIND_WOODEN_DOOR, KIND_STONE
|
||||
return true
|
||||
return false
|
||||
|
||||
# 40250 Actor-Actor 阻挡判定与阻挡实体获取
|
||||
# ActorInstanceCollisionDetection.cpp:640: 仅在两实体距离缩小(d_next < d_now)且侵入包围半径时阻挡
|
||||
func _get_blocking_actor(next_pos: Vector3) -> Node3D:
|
||||
if skip_actor_collision or player == null or locked:
|
||||
return null
|
||||
var cur: Vector3 = player.global_position if player.is_inside_tree() else player.position
|
||||
var combined := ACTOR_BODY_RADIUS_M * 2.0
|
||||
for n in pickables:
|
||||
if not is_instance_valid(n):
|
||||
continue
|
||||
if bool(n.get_meta("dead", false)): # rVictim.IsDead()
|
||||
if bool(n.get_meta("dead", false)):
|
||||
continue
|
||||
var op: Vector3 = n.global_position
|
||||
var op: Vector3 = n.global_position if n.is_inside_tree() else n.position
|
||||
var d_now := Vector2(op.x - cur.x, op.z - cur.z).length()
|
||||
if d_now > ACTOR_COLLIDE_MAX_DIST_M: # LengthSq > 800² → 不检
|
||||
if d_now > ACTOR_COLLIDE_MAX_DIST_M:
|
||||
continue
|
||||
var d_next := Vector2(op.x - next_pos.x, op.z - next_pos.z).length()
|
||||
if d_next < combined and d_next <= d_now + 1e-4:
|
||||
return true
|
||||
return false
|
||||
if d_next < combined and d_next < d_now:
|
||||
return n
|
||||
return null
|
||||
|
||||
# SetAdvancingRotation(InstanceBaseMovement.cpp):按转向速度逐帧收敛朝向;移动方向本身
|
||||
# 不受此限制(骑乘时 rotation_speed_deg = 300 → 转向明显变慢)。
|
||||
func _actor_blocked(next_pos: Vector3) -> bool:
|
||||
var b := _get_blocking_actor(next_pos)
|
||||
return b != null and _is_static_actor(b)
|
||||
|
||||
# SetAdvancingRotation(InstanceBaseMovement.cpp:142-154):
|
||||
# 差值 > 45° 时全速转向(步战 1200°/s,骑乘 300°/s);差值 <= 45° 时按 5/12 阻尼减速(500°/s 或 125°/s)。
|
||||
func _turn_toward(target_yaw: float, dt: float) -> void:
|
||||
if player == null:
|
||||
return
|
||||
var max_step := deg_to_rad(rotation_speed_deg) * dt
|
||||
var diff: float = wrapf(target_yaw - player.rotation.y, -PI, PI)
|
||||
player.rotation.x = 0.0
|
||||
player.rotation.z = 0.0
|
||||
var cur_yaw: float = wrapf(player.rotation.y, -PI, PI)
|
||||
var diff: float = wrapf(target_yaw - cur_yaw, -PI, PI)
|
||||
var spd := rotation_speed_deg
|
||||
if absf(diff) <= deg_to_rad(45.0):
|
||||
spd *= 5.0 / 12.0
|
||||
var max_step := deg_to_rad(spd) * dt
|
||||
if max_step <= 0.0 or absf(diff) <= max_step:
|
||||
player.rotation.y = target_yaw
|
||||
else:
|
||||
player.rotation.y += signf(diff) * max_step
|
||||
player.rotation.y = wrapf(cur_yaw + signf(diff) * max_step, -PI, PI)
|
||||
|
||||
func _process(dt: float) -> void:
|
||||
if player == null:
|
||||
if player == null or not active:
|
||||
return
|
||||
_update_hover_cursor()
|
||||
|
||||
@@ -345,9 +422,20 @@ func _process(dt: float) -> void:
|
||||
|
||||
if frozen or locked:
|
||||
# 停止平移,但保留 m_isGoing(解锁后继续走向 Dst,等价预约不丢)。
|
||||
anim_state.emit("wait")
|
||||
# 处于攻击或施法锁定中(locked),不发送 wait 动画中断技能/普攻动作
|
||||
if frozen and not locked:
|
||||
anim_state.emit("wait")
|
||||
if world and world.has_method("sample_height") and is_instance_valid(player):
|
||||
player.position.y = float(world.call("sample_height", player.position.x, player.position.z))
|
||||
return
|
||||
_run = force_run or Input.is_key_pressed(RUN_HOLD_KEY)
|
||||
if force_walk:
|
||||
_run = false
|
||||
elif force_run:
|
||||
_run = true
|
||||
elif default_run:
|
||||
_run = not Input.is_key_pressed(RUN_HOLD_KEY) and not Input.is_physical_key_pressed(RUN_HOLD_KEY)
|
||||
else:
|
||||
_run = Input.is_key_pressed(RUN_HOLD_KEY) or Input.is_physical_key_pressed(RUN_HOLD_KEY)
|
||||
var wish := Vector3.ZERO
|
||||
var wasd := _wasd()
|
||||
var mobile_axis := _mobile_axis
|
||||
@@ -362,6 +450,7 @@ func _process(dt: float) -> void:
|
||||
return
|
||||
_mobile_active = true
|
||||
_is_going = false
|
||||
_reserved_ground = null
|
||||
_last_wasd = Vector2.ZERO
|
||||
# GameCamera 位于 head + (sin yaw, cos yaw)*dist 并看向 head:视线的水平前方是
|
||||
# -(sin yaw, cos yaw),屏幕右 = 前方 × UP。
|
||||
@@ -380,6 +469,7 @@ func _process(dt: float) -> void:
|
||||
return
|
||||
_last_wasd = wasd
|
||||
_is_going = false # 键盘覆盖点地(NEW_MoveToDirection:m_isGoing = FALSE)
|
||||
_reserved_ground = null
|
||||
var yaw: float = (camera.heading() if camera and camera.has_method("heading") else 0.0)
|
||||
var fwd := -Vector3(sin(yaw), 0, cos(yaw))
|
||||
var right := Vector3(-fwd.z, 0, fwd.x)
|
||||
@@ -387,10 +477,13 @@ func _process(dt: float) -> void:
|
||||
elif _is_going:
|
||||
_mobile_active = false
|
||||
var flat := Vector3(_dst_pos.x - player.position.x, 0, _dst_pos.z - player.position.z)
|
||||
if flat.length() <= ARRIVE_EPS:
|
||||
var dist := flat.length()
|
||||
if dist <= ARRIVE_EPS:
|
||||
player.position.x = _dst_pos.x
|
||||
player.position.z = _dst_pos.z
|
||||
_is_going = false
|
||||
else:
|
||||
wish = flat.normalized()
|
||||
wish = flat / dist
|
||||
else:
|
||||
_mobile_active = false
|
||||
_last_wasd = Vector2.ZERO
|
||||
@@ -401,22 +494,52 @@ func _process(dt: float) -> void:
|
||||
if moving_skill:
|
||||
_turn_toward(atan2(wish.x, wish.z), dt)
|
||||
else:
|
||||
var want_speed := (SPEED_RUN if _run else SPEED_WALK) * server_speed_scale
|
||||
var base_speed := SPEED_RUN if _run else SPEED_WALK
|
||||
if player and player.has_method("get_move_motion_speeds"):
|
||||
var ms: Vector2 = player.call("get_move_motion_speeds")
|
||||
var m_spd: float = (ms.y if _run else ms.x) * 0.01
|
||||
if m_spd > 0.0:
|
||||
base_speed = m_spd
|
||||
var want_speed := base_speed * server_speed_scale
|
||||
var before := player.position
|
||||
var np := player.position + wish * want_speed * dt
|
||||
# 逐轴推进:地形 ATTRIBUTE_BLOCK + Actor 碰撞任一挡下该轴 → 不动那一轴
|
||||
# (单个 Actor 时等价 AdjustDynamicCollisionMovement 的滑移;两轴都挡 = BlockMovement)。
|
||||
var block_x := _actor_blocked(Vector3(np.x, before.y, player.position.z)) \
|
||||
or _blocked(np.x, player.position.z)
|
||||
var block_z := _actor_blocked(Vector3(player.position.x, before.y, np.z)) \
|
||||
or _blocked(player.position.x, np.z)
|
||||
if not block_x:
|
||||
player.position.x = np.x
|
||||
if not block_z:
|
||||
player.position.z = np.z
|
||||
# 动画看「实际位移」而非意图:贴墙 / 顶人磨蹭时该切回 wait
|
||||
var move_step := want_speed * dt
|
||||
if _is_going:
|
||||
var to_dst := Vector2(_dst_pos.x - player.position.x, _dst_pos.z - player.position.z).length()
|
||||
if move_step >= to_dst:
|
||||
move_step = to_dst
|
||||
var np := player.position + wish * move_step
|
||||
var direct_blocked := false
|
||||
var blocker := _get_blocking_actor(np)
|
||||
if blocker:
|
||||
direct_blocked = true
|
||||
var b_pos: Vector3 = blocker.global_position if blocker.is_inside_tree() else blocker.position
|
||||
var to_player := Vector3(player.position.x - b_pos.x, 0, player.position.z - b_pos.z)
|
||||
var norm := to_player.normalized()
|
||||
var tangent := Vector3(-norm.z, 0, norm.x)
|
||||
if tangent.dot(wish) < 0.0:
|
||||
tangent = -tangent
|
||||
var slide_np := player.position + tangent * (wish.dot(tangent) * move_step)
|
||||
if not _actor_blocked(slide_np) and not _blocked(slide_np.x, slide_np.z):
|
||||
np = slide_np
|
||||
direct_blocked = false
|
||||
if direct_blocked or _blocked(np.x, np.z) or _actor_blocked(np):
|
||||
# 逐轴推进降级保底:地形 ATTRIBUTE_BLOCK 或静态物体阻挡
|
||||
var block_x := _actor_blocked(Vector3(np.x, before.y, player.position.z)) \
|
||||
or _blocked(np.x, player.position.z)
|
||||
var block_z := _actor_blocked(Vector3(player.position.x, before.y, np.z)) \
|
||||
or _blocked(player.position.x, np.z)
|
||||
if not block_x:
|
||||
player.position.x = np.x
|
||||
if not block_z:
|
||||
player.position.z = np.z
|
||||
else:
|
||||
player.position = np
|
||||
|
||||
var disp := Vector2(player.position.x - before.x, player.position.z - before.z).length()
|
||||
if disp > want_speed * dt * 0.25:
|
||||
if _is_going and Vector2(_dst_pos.x - player.position.x, _dst_pos.z - player.position.z).length() <= ARRIVE_EPS:
|
||||
_is_going = false
|
||||
var is_manual := (wasd != Vector2.ZERO or mobile_axis != Vector2.ZERO)
|
||||
if disp > want_speed * dt * 0.25 or is_manual:
|
||||
speed = want_speed
|
||||
_turn_toward(atan2(wish.x, wish.z), dt)
|
||||
else:
|
||||
@@ -424,7 +547,8 @@ func _process(dt: float) -> void:
|
||||
|
||||
if world and world.has_method("sample_height"):
|
||||
player.position.y = float(world.call("sample_height", player.position.x, player.position.z))
|
||||
moved.emit(player.position)
|
||||
if speed > 0.01:
|
||||
moved.emit(player.position)
|
||||
|
||||
if speed <= 0.01:
|
||||
anim_state.emit("wait")
|
||||
@@ -436,6 +560,10 @@ func _process(dt: float) -> void:
|
||||
func _update_hover_cursor() -> void:
|
||||
if cursor_manager == null or not cursor_manager.has_method("set_cursor") or camera == null:
|
||||
return
|
||||
if camera.has_method("is_dragging") and bool(camera.call("is_dragging")):
|
||||
cursor_manager.set_cursor("CAMERA_ROTATE")
|
||||
_set_hover_vid(0)
|
||||
return
|
||||
var mouse_pos := get_viewport().get_mouse_position()
|
||||
if ui_manager and ui_manager.has_method("blocks_game_input"):
|
||||
var motion := InputEventMouseMotion.new()
|
||||
@@ -455,6 +583,8 @@ func _update_hover_cursor() -> void:
|
||||
for n in pickables:
|
||||
if not is_instance_valid(n):
|
||||
continue
|
||||
if bool(n.get_meta("dead", false)):
|
||||
continue
|
||||
var t := _ray_pick_t(from, dir, n)
|
||||
if t >= 0.0 and t < best_t:
|
||||
best_t = t
|
||||
@@ -463,8 +593,12 @@ func _update_hover_cursor() -> void:
|
||||
cursor_manager.set_cursor(String(best.get_meta("cursor_shape", "ATTACK")))
|
||||
_set_hover_vid(int(best.get_meta("vid", 0)))
|
||||
else:
|
||||
cursor_manager.set_cursor("NORMAL")
|
||||
_set_hover_vid(0)
|
||||
var ghit: Variant = _ray_ground(from, dir)
|
||||
if ghit != null and world != null and world.has_method("is_blocked") and bool(world.call("is_blocked", ghit.x, ghit.z)):
|
||||
cursor_manager.set_cursor("CANT_GO")
|
||||
else:
|
||||
cursor_manager.set_cursor("NORMAL")
|
||||
|
||||
# game.py OnRender chr.Pick() -> ShowCharacterTextTail:悬停命中的角色 vid 变化时才 emit。
|
||||
func _set_hover_vid(vid: int) -> void:
|
||||
|
||||
Reference in New Issue
Block a user