fix: 装备属性面板避让逻辑 + 多项功能更新

- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
This commit is contained in:
shen
2026-09-21 16:38:59 -07:00
parent 1522a8a1a1
commit c93894313a
5498 changed files with 4558004 additions and 1442 deletions
+153 -12
View File
@@ -35,6 +35,7 @@ var _hover_vid := 0 # 上一帧悬停命中的实体 vid(ho
const SPEED_WALK := 1.8
const SPEED_RUN := 4.8
const KEYBOARD_MOVE_TARGET_M := 3.0 # 40250 NEW_MoveToDirection: Dst = Cur + 300 px
const RUN_HOLD_KEY := KEY_SHIFT
const ARRIVE_EPS := 0.05
const PICK_RADIUS := 1.4 # 点选命中半径(米)
@@ -59,7 +60,10 @@ var force_run := false # 脚本化 / 自动跑
var force_walk := false # 步行模式
var frozen := false # 受击硬直等:本帧不响应移动输入(net_play 设)
var locked := false # CInstanceBase::isLock():完全锁死移动(过场 / 特定 affect)
var dead := false # 40250 CActorInstance::IsDead:死亡期间停止一切本地移动输入
var moving_skill := false # IsUsingMovingSkill():移动技能中,只允许转向不平移
var private_shop_open := false # CPythonPlayer::IsOpenPrivateShop:摆摊期间输入只保留按键状态
var processing_emotion := false # CPythonPlayer::__IsProcessingEmotion:情绪动作期间禁止移动
var server_speed_scale := 1.0 # GC_CHANGE_SPEED moving_speed / 100
var rotation_speed_deg := ROT_SPEED_DEFAULT_DEG # 上/下马时由 net_play 切换(§3.1 / §6.4
# CActorInstance::CanSkipCollision():网络状态包驱动的移动整段跳过 Actor 碰撞
@@ -78,11 +82,16 @@ var _mobile_active := false
var _mobile_ui_touches := {}
# --- §3.1 Src/Dst 移动模型(对齐 InstanceBaseMovement.cpp NEW_Goto / NEW_MoveToDirection---
# _is_going = m_isGoing(点地走向 Dst);方向键移动 _is_going 保持 false。
# _is_going = m_isGoing(点地走向 Dst);方向键移动保持独立的 300cm Dst
# 因为 40250 NEW_MoveToDirection 会把 m_isGoing 置 false。
var _src_pos := Vector3.ZERO
var _dst_pos := Vector3.ZERO
var _is_going := false
var _dst_rot := 0.0
var _direction_dst := Vector3.ZERO
var _direction_wish := Vector3.ZERO
var _direction_active := false
var _direction_resume_pending := false
var _reserved_ground: Variant = null # __ReserveClickGround 的待处理点
var _reserved_delay_time := 0.0 # NEW_IsEmptyReservedDelayTime 递减到 0 才生效
@@ -112,11 +121,66 @@ func stop() -> void:
_dst_pos = player.position
anim_state.emit("wait")
func set_dead(value: bool) -> void:
if dead == value:
return
dead = value
if dead:
# GC_DEAD/HP<=0 has the same movement boundary as NEW_Stop, but must
# retain the dead flag until a positive HP/re-entry transition arrives.
stop()
else:
# Re-entry starts from the server/current actor position. Do not emit a
# second wait here; the next authoritative animation state owns that.
if player:
_src_pos = player.position
_dst_pos = player.position
# 40250 CPythonPlayer::OpenPrivateShop / ClosePrivateShop。
# 个人摊位建立后,方向键仍由 NEW_SetSingleDirKeyState 记录,但
# NEW_SetMultiDirKeyState -> __CanMove 不允许产生新的平移;收摊后由
# Update() 再次提交仍按住的方向键。
func set_private_shop_open(value: bool) -> void:
if private_shop_open == value:
return
private_shop_open = value
if private_shop_open:
_is_going = false
_reserved_ground = null
_reserved_delay_time = 0.0
_direction_active = false
_direction_resume_pending = _wasd() != Vector2.ZERO
_direction_wish = Vector3.ZERO
if player:
_direction_dst = player.position
# 40250 CPythonPlayer::StartEmotionProcess / EndEmotionProcess。
# 情绪开始时会清预约动作并锁住主角,方向位不能丢,动作结束后由
# PlayerController 的下一次 Update 恢复与原客户端 m_isDirKey 相同的重试。
func set_processing_emotion(value: bool) -> void:
if processing_emotion == value:
return
processing_emotion = value
if processing_emotion:
_is_going = false
_reserved_ground = null
_reserved_delay_time = 0.0
_direction_active = false
_direction_resume_pending = _wasd() != Vector2.ZERO
_direction_wish = Vector3.ZERO
if player:
_direction_dst = player.position
func _clear_direction_keys() -> void:
_key_up = false
_key_down = false
_key_left = false
_key_right = false
_direction_active = false
_direction_resume_pending = false
_direction_wish = Vector3.ZERO
if player:
_direction_dst = player.position
func _notification(what: int) -> void:
if what == NOTIFICATION_APPLICATION_FOCUS_OUT:
@@ -144,6 +208,46 @@ func _set_direction_key_state(e: InputEventKey, pressed: bool) -> void:
_key_left = pressed
elif logical in [KEY_D, KEY_RIGHT] or physical in [KEY_D, KEY_RIGHT]:
_key_right = pressed
_refresh_direction_target()
func _keyboard_wish(dir: Vector2) -> Vector3:
if dir == Vector2.ZERO:
return Vector3.ZERO
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)
return (fwd * -dir.y + right * dir.x).normalized()
# CPythonPlayer::NEW_SetMultiDirKeyState -> CInstanceBase::NEW_MoveToDirection.
# The reference writes a new 300-pixel destination only when the direction state
# changes; it does not continuously extend the destination every frame.
func _refresh_direction_target() -> void:
if not active or player == null:
return
var dir := _wasd()
if dir == Vector2.ZERO:
_direction_active = false
_direction_resume_pending = false
_direction_wish = Vector3.ZERO
_direction_dst = player.position
return
if dead or frozen or locked or private_shop_open or processing_emotion:
_direction_active = false
_direction_resume_pending = true
_direction_wish = Vector3.ZERO
_direction_dst = player.position
return
_direction_wish = _keyboard_wish(dir)
# NEW_MoveToDirection keeps the facing update for moving skills, but does
# not create a translating destination while the skill owns movement.
if moving_skill:
_direction_active = false
_direction_resume_pending = true
_direction_dst = player.position
return
_direction_dst = player.position + _direction_wish * KEYBOARD_MOVE_TARGET_M
_direction_active = true
_direction_resume_pending = false
# CInstanceBase::__IsSyncing()Dead / Stun / Pushing 中不接受移动输入(net_play 置 frozen)。
func is_going() -> bool:
@@ -151,7 +255,8 @@ func is_going() -> bool:
# NEW_Goto / NEW_MoveToDirection 的三道前置门:syncing / moving-skill(只转向) / lock。
func _can_translate() -> bool:
return active and not frozen and not locked and not moving_skill
return active and not dead and not frozen and not locked and not moving_skill \
and not private_shop_open and not processing_emotion
const _TAP_TRAVEL_MAX := 12.0 # 触点位移超过这么多像素 -> 视作拖拽(归相机),不是轻点
const _TAP_TIME_MAX := 0.35 # 按下到抬起超过这么久 -> 不是轻点
@@ -230,7 +335,7 @@ func _goto(dst_flat: Vector3) -> bool:
return true
func _unhandled_input(e: InputEvent) -> void:
if not active:
if not active or dead:
return
if e is InputEventKey:
var key := e as InputEventKey
@@ -287,7 +392,7 @@ 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)):
if bool(n.get_meta("dead", false)) or not bool(n.get_meta("can_pick", true)):
continue
var t := _ray_pick_t(from, dir, n)
if t >= 0.0 and t < best_t:
@@ -306,6 +411,12 @@ func _on_click(screen_pos: Vector2) -> void:
return
ground_clicked.emit(Vector3(hit.x, hit.y, hit.z))
_goto(hit)
else:
# 40250 __OnClickSmart falls through to NEW_Stop when the click hits
# neither an item, actor, nor ground. Without this boundary a previous
# click-to-move destination keeps running after clicking empty UI-free
# space, which makes the actor appear to move on its own.
stop()
# 射线 vs 实体(节点下所有 MeshInstance3D 的世界 AABB 合并)的进入距离 t;不命中返回 -1。
func _ray_pick_t(from: Vector3, dir: Vector3, node: Node3D) -> float:
@@ -419,7 +530,7 @@ func _get_blocking_actor(next_pos: Vector3) -> Node3D:
for n in pickables:
if not is_instance_valid(n):
continue
if bool(n.get_meta("dead", false)):
if bool(n.get_meta("dead", false)) or not bool(n.get_meta("can_pick", true)):
continue
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()
@@ -454,6 +565,10 @@ func _turn_toward(target_yaw: float, dt: float) -> void:
func _process(dt: float) -> void:
if player == null or not active:
return
if dead:
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
_update_hover_cursor()
# __ReserveClickGround 的 delay 递减(NEW_IsEmptyReservedDelayTime);到点且门已开则重试。
@@ -464,7 +579,15 @@ func _process(dt: float) -> void:
_reserved_ground = null
_goto(rg)
if frozen or locked:
if frozen or locked or private_shop_open or processing_emotion:
# 40250 keeps m_isDirKey and retries NEW_SetMultiDirKeyState from
# CPythonPlayer::Update while the actor is locked/syncing. Do not lose a
# held WASD direction or let its old fixed target resume after unlock.
if _wasd() != Vector2.ZERO:
_direction_active = false
_direction_resume_pending = true
_direction_wish = Vector3.ZERO
_direction_dst = player.position
# 停止平移,但保留 m_isGoing(解锁后继续走向 Dst,等价预约不丢)。
# 处于攻击或施法锁定中(locked),不发送 wait 动画中断技能/普攻动作
if frozen and not locked:
@@ -482,6 +605,19 @@ func _process(dt: float) -> void:
_run = Input.is_key_pressed(RUN_HOLD_KEY) or Input.is_physical_key_pressed(RUN_HOLD_KEY)
var wish := Vector3.ZERO
var wasd := _wasd()
if moving_skill and wasd != Vector2.ZERO and _direction_active:
# IsUsingMovingSkill() takes ownership of the actor motion immediately;
# discard the old translating destination but keep the held direction for
# the skill's facing update and for the post-skill retry.
_direction_active = false
_direction_resume_pending = true
_direction_wish = _keyboard_wish(wasd)
_direction_dst = player.position
if wasd != Vector2.ZERO and _direction_resume_pending and _can_translate():
# The next Update after a lock/moving-skill edge is the same retry point
# as the reference's per-frame m_isDirKey branch.
_refresh_direction_target()
wasd = _wasd()
var mobile_axis := _mobile_axis
if mobile_axis != Vector2.ZERO:
@@ -514,10 +650,10 @@ func _process(dt: float) -> void:
_last_wasd = wasd
_is_going = false # 键盘覆盖点地(NEW_MoveToDirectionm_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)
wish = (fwd * -wasd.y + right * wasd.x).normalized()
# The reference creates a fixed 300-pixel destination on the key event;
# do not continuously extend it from the current position every frame.
if _direction_active or moving_skill:
wish = _direction_wish
elif _is_going:
_mobile_active = false
var flat := Vector3(_dst_pos.x - player.position.x, 0, _dst_pos.z - player.position.z)
@@ -533,6 +669,7 @@ func _process(dt: float) -> void:
_last_wasd = Vector2.ZERO
var speed := 0.0
var keyboard_motion := wasd != Vector2.ZERO and _direction_active
if wish != Vector3.ZERO:
# IsUsingMovingSkill():移动技能中只转向,不平移。
if moving_skill:
@@ -547,8 +684,9 @@ func _process(dt: float) -> void:
var want_speed := base_speed * server_speed_scale
var before := player.position
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 _is_going or keyboard_motion:
var target := _direction_dst if keyboard_motion else _dst_pos
var to_dst := Vector2(target.x - player.position.x, target.z - player.position.z).length()
if move_step >= to_dst:
move_step = to_dst
var np := player.position + wish * move_step
@@ -582,6 +720,9 @@ func _process(dt: float) -> void:
var disp := Vector2(player.position.x - before.x, player.position.z - before.z).length()
if _is_going and Vector2(_dst_pos.x - player.position.x, _dst_pos.z - player.position.z).length() <= ARRIVE_EPS:
_is_going = false
if keyboard_motion and Vector2(_direction_dst.x - player.position.x, _direction_dst.z - player.position.z).length() <= ARRIVE_EPS:
_direction_active = false
_direction_resume_pending = 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