fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
This commit is contained in:
+122
-20
@@ -7,8 +7,10 @@
|
||||
# qb.assign(1, "item", 5) # 当前页 1 号格放背包 5 格的药水
|
||||
# # 输入:数字 1..4 -> 本页 0..3,F1..F4 -> 本页 4..7。
|
||||
#
|
||||
# 技能施放:先发送 M2Client.use_skill(skill_id, target_vid),再同步
|
||||
# M2Client.cast_skill(motion_idx, heading, x_cm, y_cm)(默认朝向 = 玩家 yaw)。
|
||||
# 技能施放顺序对齐 ClientVS22/PythonPlayerSkill.cpp:先由本地动作生成
|
||||
# FUNC_SKILL|motion + arg,再发送 CG_USE_SKILL(skill, target)。
|
||||
# M2Client.cast_skill(motion_idx, heading, x_cm, y_cm, arg)(默认朝向 = 玩家 yaw);
|
||||
# arg 对齐 CActorInstance::__OnUseSkill:低 4 位是动作循环次数,第 4 位是移动技能。
|
||||
# 移动端按住技能按钮时,activate_aimed() 会把拖动方向转换为相同的
|
||||
# server heading;校验、冷却和目标选择仍走 activate() 的同一条链路。
|
||||
# 冷却:本地预测时长由 SkillTable 提供;GC_SKILL_COOLTIME_END 可提前解锁。
|
||||
@@ -185,6 +187,10 @@ func assign(slot: int, kind: String, id: int, persist := true) -> bool:
|
||||
server_ref = id
|
||||
if type == 0 or not client.quickslot_add(global, type, server_ref):
|
||||
return false
|
||||
# The reference client does not mutate its local quickslot here. The
|
||||
# server's GC_QUICKSLOT_ADD packet is consumed by EntityStore and emits
|
||||
# quickslots_changed, which calls restore_from_server().
|
||||
return true
|
||||
_state[global] = {"kind": kind, "id": id,
|
||||
"ref": _skill_slot_for_id(id) if kind == "skill" else id,
|
||||
"cd_end": float(_skill_cooldowns.get(id, 0.0)) if kind == "skill" else 0.0}
|
||||
@@ -272,7 +278,7 @@ func _activate_slot(slot: int, screen_direction: Vector2) -> void:
|
||||
if _player_dead():
|
||||
return
|
||||
if s.kind == "skill":
|
||||
var mi: int = table.motion_idx_of(s.id) if table else 0
|
||||
var mi: int = _motion_idx_for_skill(int(s.id))
|
||||
var p: Node3D = _player_getter.call() if _player_getter.is_valid() else null
|
||||
var yaw := 0.0
|
||||
var xy := Vector2.ZERO
|
||||
@@ -296,6 +302,9 @@ func _activate_slot(slot: int, screen_direction: Vector2) -> void:
|
||||
var gate: Dictionary = _skill_gate.click_skill_slot(int(s.id))
|
||||
if not bool(gate.get("ok", false)):
|
||||
var code := String(gate.get("code", ""))
|
||||
if code == "GUILD_ROUTED":
|
||||
_activate_guild_skill(int(s.id))
|
||||
return
|
||||
# PythonPlayerSkill::__CheckSpecialSkill(123) 不是失败:参考端
|
||||
# 直接进入 NEW_Fishing,并把 EQUIP_FISHING_ROD 作为唯一错误反馈。
|
||||
if code == "SPECIAL_SKILL" and int(s.id) == PlayerSkill.SKILL_INDEX_FISHING \
|
||||
@@ -327,16 +336,21 @@ func _activate_slot(slot: int, screen_direction: Vector2) -> void:
|
||||
var resolved := int(gate.get("target_vid", 0))
|
||||
if resolved != 0:
|
||||
target_vid = resolved
|
||||
# use_skill is the authoritative server intent. Keep the CG_MOVE skill
|
||||
# state packet as a separate visual/action-state sync, just like the
|
||||
# original client did.
|
||||
# PythonPlayerEventHandler::OnUseSkill 先上行 FUNC_SKILL|motion + arg,
|
||||
# 然后 __SendUseSkill 才发送 CG_USE_SKILL。动作包失败时不产生后续技能意图。
|
||||
var motion_sent := true
|
||||
if client.has_method("cast_skill"):
|
||||
var motion_arg: int = 0
|
||||
if table and table.has_method("motion_state_arg"):
|
||||
motion_arg = int(table.call("motion_state_arg", int(s.id), _skill_level(int(s.id))))
|
||||
motion_sent = bool(client.cast_skill(mi, yaw, int(xy.x), int(xy.y), motion_arg))
|
||||
if not motion_sent:
|
||||
return
|
||||
skill_cast_started.emit(int(s.id), target_vid)
|
||||
var intent_sent := true
|
||||
if client.has_method("use_skill"):
|
||||
intent_sent = client.use_skill(int(s.id), target_vid)
|
||||
if intent_sent:
|
||||
if client.has_method("cast_skill"):
|
||||
client.cast_skill(mi, yaw, int(xy.x), int(xy.y))
|
||||
skill_cast_started.emit(int(s.id), target_vid)
|
||||
s.cd_end = _now() + _skill_cooldown(int(s.id))
|
||||
_state[global] = s
|
||||
_skill_cooldowns[int(s.id)] = s.cd_end
|
||||
@@ -357,6 +371,33 @@ func _activate_slot(slot: int, screen_direction: Vector2) -> void:
|
||||
elif s.kind == "emote" and client.has_method("send_emoticon"):
|
||||
client.send_emoticon(s.id)
|
||||
|
||||
func _activate_guild_skill(skill_id: int) -> void:
|
||||
if _skill_gate == null or not _skill_gate.has_method("use_guild_skill"):
|
||||
return
|
||||
_skill_gate.slot_cd_end = float(_skill_cooldowns.get(skill_id, 0.0))
|
||||
var gate: Dictionary = _skill_gate.use_guild_skill(skill_id)
|
||||
if not bool(gate.get("ok", false)):
|
||||
var code := String(gate.get("code", ""))
|
||||
if not PlayerSkill.is_silent_code(code):
|
||||
skill_rejected.emit(skill_id, code)
|
||||
return
|
||||
var motion_idx: int = int(table.motion_idx_of(skill_id)) if table else 0
|
||||
var motion_sent := true
|
||||
if client.has_method("cast_skill"):
|
||||
# UseGuildSkill calls NEW_UseSkill(..., loop=1, moving=false).
|
||||
motion_sent = bool(client.cast_skill(motion_idx, 0.0, 0, 0, 1))
|
||||
if not motion_sent:
|
||||
return
|
||||
skill_cast_started.emit(skill_id, 0)
|
||||
if not client.has_method("use_guild_skill") or not client.use_guild_skill(skill_id, 0):
|
||||
return
|
||||
var cd_end := _now() + _skill_cooldown(skill_id)
|
||||
_skill_cooldowns[skill_id] = cd_end
|
||||
for peer in _state:
|
||||
if peer.kind == "skill" and peer.id == skill_id:
|
||||
peer.cd_end = cd_end
|
||||
skill_activated.emit(skill_id)
|
||||
|
||||
## 从技能窗口右键直接激活技能
|
||||
func activate_skill_direct(skill_id: int) -> void:
|
||||
if skill_id <= 0 or _player_dead():
|
||||
@@ -364,7 +405,7 @@ func activate_skill_direct(skill_id: int) -> void:
|
||||
var s := {"kind": "skill", "id": skill_id, "cd_end": float(_skill_cooldowns.get(skill_id, 0.0))}
|
||||
if _now() < s.cd_end:
|
||||
return
|
||||
var mi: int = table.motion_idx_of(skill_id) if table else 0
|
||||
var mi: int = _motion_idx_for_skill(skill_id)
|
||||
var p: Node3D = _player_getter.call() if _player_getter.is_valid() else null
|
||||
var yaw := 0.0
|
||||
var xy := Vector2.ZERO
|
||||
@@ -383,6 +424,9 @@ func activate_skill_direct(skill_id: int) -> void:
|
||||
var gate: Dictionary = _skill_gate.click_skill_slot(skill_id)
|
||||
if not bool(gate.get("ok", false)):
|
||||
var code := String(gate.get("code", ""))
|
||||
if code == "GUILD_ROUTED":
|
||||
_activate_guild_skill(skill_id)
|
||||
return
|
||||
if code == "TOGGLE_OFF":
|
||||
if client.has_method("use_skill"):
|
||||
client.use_skill(skill_id, 0)
|
||||
@@ -393,13 +437,20 @@ func activate_skill_direct(skill_id: int) -> void:
|
||||
var resolved := int(gate.get("target_vid", 0))
|
||||
if resolved != 0:
|
||||
target_vid = resolved
|
||||
# Direct skill-window activation follows the same OnUseSkill -> SendUseSkill order.
|
||||
var motion_sent := true
|
||||
if client.has_method("cast_skill"):
|
||||
var motion_arg: int = 0
|
||||
if table and table.has_method("motion_state_arg"):
|
||||
motion_arg = int(table.call("motion_state_arg", skill_id, _skill_level(skill_id)))
|
||||
motion_sent = bool(client.cast_skill(mi, yaw, int(xy.x), int(xy.y), motion_arg))
|
||||
if not motion_sent:
|
||||
return
|
||||
skill_cast_started.emit(skill_id, target_vid)
|
||||
var intent_sent := true
|
||||
if client.has_method("use_skill"):
|
||||
intent_sent = client.use_skill(skill_id, target_vid)
|
||||
if intent_sent:
|
||||
if client.has_method("cast_skill"):
|
||||
client.cast_skill(mi, yaw, int(xy.x), int(xy.y))
|
||||
skill_cast_started.emit(skill_id, target_vid)
|
||||
var cd_end := _now() + _skill_cooldown(skill_id)
|
||||
_skill_cooldowns[skill_id] = cd_end
|
||||
for peer in _state:
|
||||
@@ -410,7 +461,7 @@ func activate_skill_direct(skill_id: int) -> void:
|
||||
and (table.is_fan_range(skill_id) or table.is_circle_range(skill_id)):
|
||||
var lvl := _skill_level(skill_id)
|
||||
net_play.send_fly_targeting(target_vid, float(table.target_range(skill_id)),
|
||||
table.target_count(skill_id, lvl), table.fly_shape(skill_id))
|
||||
table.target_count(skill_id, lvl), table.fly_shape(skill_id))
|
||||
skill_activated.emit(skill_id)
|
||||
|
||||
func _aim_heading(player_node: Node3D, screen_direction: Vector2, fallback: float) -> float:
|
||||
@@ -479,13 +530,27 @@ func _skill_level(sid: int) -> int:
|
||||
return int(sk.get("level", 0))
|
||||
return 0
|
||||
|
||||
# CPythonSkill::GetSkillMotionIndex(iGrade);保留旧 SkillTable 的单参数回退,
|
||||
# 让最小测试桩和第三方表仍可工作。
|
||||
func _motion_idx_for_skill(skill_id: int) -> int:
|
||||
if table == null:
|
||||
return 0
|
||||
var grade := _skill_grade(skill_id)
|
||||
if table.has_method("motion_idx_for_grade"):
|
||||
return int(table.call("motion_idx_for_grade", skill_id, grade))
|
||||
return int(table.motion_idx_of(skill_id))
|
||||
|
||||
func get_page() -> int:
|
||||
return _page
|
||||
|
||||
func set_page(page: int) -> void:
|
||||
if page < 0 or page >= PAGE_COUNT or page == _page:
|
||||
# CPythonPlayer::SetQuickPage wraps the requested page by
|
||||
# QUICKSLOT_MAX_LINE. Keyboard/game code and recovery paths can call this
|
||||
# entry point directly with -1 or PAGE_COUNT.
|
||||
var normalized := posmod(page, PAGE_COUNT)
|
||||
if normalized == _page:
|
||||
return
|
||||
_page = page
|
||||
_page = normalized
|
||||
_move_from = -1
|
||||
_refresh_page()
|
||||
|
||||
@@ -568,15 +633,28 @@ func _clear(slot: int) -> void:
|
||||
var global := _global_slot(slot)
|
||||
if _state[global].kind == "":
|
||||
return
|
||||
if client and client.has_method("quickslot_del") and not client.quickslot_del(global):
|
||||
if client and client.has_method("quickslot_del"):
|
||||
if not client.quickslot_del(global):
|
||||
return
|
||||
# 40250 changes the authoritative local slot only from
|
||||
# GC_QUICKSLOT_DEL. Do not clear the Godot mirror optimistically: a
|
||||
# rejected, delayed, or out-of-order request must remain recoverable from
|
||||
# the last server snapshot.
|
||||
return
|
||||
_state[global] = {"kind": "", "id": 0, "cd_end": 0.0}
|
||||
_refresh_slot(slot)
|
||||
|
||||
func _swap(local_a: int, local_b: int) -> void:
|
||||
if local_a < 0 or local_a >= SLOTS_PER_PAGE or local_b < 0 or local_b >= SLOTS_PER_PAGE:
|
||||
return
|
||||
if local_a == local_b:
|
||||
return
|
||||
var a := _global_slot(local_a)
|
||||
var b := _global_slot(local_b)
|
||||
if client and client.has_method("quickslot_swap") and not client.quickslot_swap(a, b):
|
||||
if client and client.has_method("quickslot_swap"):
|
||||
if not client.quickslot_swap(a, b):
|
||||
return
|
||||
# GC_QUICKSLOT_SWAP is the only local state transition.
|
||||
return
|
||||
var tmp: Dictionary = _state[a]
|
||||
_state[a] = _state[b]
|
||||
@@ -1533,6 +1611,7 @@ func _process(dt: float) -> void:
|
||||
_st_gauge_img.texture = _st_frames[_gauge_idx % _st_frames.size()]
|
||||
|
||||
func _skill_cooldown(skill_id: int) -> float:
|
||||
var base := DEFAULT_CD
|
||||
if table and table.has_method("cooldown_of"):
|
||||
var level := 0
|
||||
if client and client.has_method("get_skills"):
|
||||
@@ -1542,8 +1621,31 @@ func _skill_cooldown(skill_id: int) -> float:
|
||||
break
|
||||
var configured := float(table.cooldown_of(skill_id, level))
|
||||
if configured > 0.0:
|
||||
return configured
|
||||
return DEFAULT_CD
|
||||
base = configured
|
||||
|
||||
# CPythonPlayer::__RunCoolTime (PythonPlayerSkill.cpp:796) applies the
|
||||
# exact POINT_CASTING_SPEED piecewise scale after GetSkillCoolTime().
|
||||
# M2Client exposes the named field; the points array fallback keeps this
|
||||
# compatible with headless/test clients that only provide raw points.
|
||||
# POINT_CASTING_SPEED is a percentage-like point whose neutral/default
|
||||
# value is 100 in the reference client. Keep that neutral value while a
|
||||
# headless fixture has not supplied a points snapshot yet.
|
||||
var casting_speed := 100
|
||||
if client and client.has_method("get_points"):
|
||||
var points: Dictionary = client.get_points()
|
||||
if points.has("casting_speed"):
|
||||
casting_speed = int(points.get("casting_speed", 0))
|
||||
else:
|
||||
var values: Array = points.get("points", [])
|
||||
if values.size() > 21:
|
||||
casting_speed = int(values[21])
|
||||
var speed_scale := 100
|
||||
var speed_delta := 100 - casting_speed
|
||||
if speed_delta > 0:
|
||||
speed_scale = 100 + speed_delta
|
||||
elif speed_delta < 0:
|
||||
speed_scale = 10000 / (100 - speed_delta)
|
||||
return base * float(speed_scale) / 100.0
|
||||
|
||||
func _now() -> float:
|
||||
return Time.get_ticks_msec() / 1000.0
|
||||
|
||||
Reference in New Issue
Block a user