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
+78 -6
View File
@@ -31,6 +31,7 @@ class FakeClient extends Node:
var quickslots := []
var casts := []
var skill_uses := []
var guild_skill_uses := []
var quickslot_ops := []
var saids := []
var emotes := []
@@ -44,11 +45,39 @@ class FakeClient extends Node:
func get_points() -> Dictionary: return points
func get_quickslots() -> Array: return quickslots
func get_target() -> Dictionary: return {"vid": 2000}
func cast_skill(mi, rot, x, y) -> bool: casts.append([mi, rot, x, y]); return true
func cast_skill(mi, rot, x, y, arg = 0) -> bool: casts.append([mi, rot, x, y, arg]); return true
func use_skill(id, target_vid) -> bool: skill_uses.append([id, target_vid]); return true
func quickslot_add(pos, type, ref) -> bool: quickslot_ops.append(["add", pos, type, ref]); return true
func quickslot_del(pos) -> bool: quickslot_ops.append(["del", pos]); return true
func quickslot_swap(pos, change_pos) -> bool: quickslot_ops.append(["swap", pos, change_pos]); return true
func use_guild_skill(id, target_vid) -> bool: guild_skill_uses.append([id, target_vid]); return true
func quickslot_add(pos, type, ref) -> bool:
quickslot_ops.append(["add", pos, type, ref])
quickslots = quickslots.filter(func(q): return int(q.get("pos", -1)) != int(pos))
quickslots.append({"pos": pos, "type": type, "ref": ref})
quickslots_changed.emit()
return true
func quickslot_del(pos) -> bool:
quickslot_ops.append(["del", pos])
quickslots = quickslots.filter(func(q): return int(q.get("pos", -1)) != int(pos))
quickslots_changed.emit()
return true
func quickslot_swap(pos, change_pos) -> bool:
quickslot_ops.append(["swap", pos, change_pos])
var a := -1
var b := -1
for i in quickslots.size():
if int(quickslots[i].get("pos", -1)) == int(pos): a = i
if int(quickslots[i].get("pos", -1)) == int(change_pos): b = i
if a >= 0 and b >= 0:
var tmp: Dictionary = quickslots[a]
quickslots[a] = quickslots[b]
quickslots[b] = tmp
quickslots[a]["pos"] = pos
quickslots[b]["pos"] = change_pos
elif a >= 0:
quickslots[a]["pos"] = change_pos
elif b >= 0:
quickslots[b]["pos"] = pos
quickslots_changed.emit()
return true
func skill_up(id) -> bool: saids.append(id); return true
func use_item(w, c) -> bool: return true
func send_emoticon(id) -> bool: emotes.append(id); return true
@@ -87,6 +116,13 @@ func _run() -> void:
_ck(String(s1.get("name", "")) != "", "skill 1 has a name (%s)" % s1.get("name"))
# 列偏移修正后:skill 1 'Three-Way Cut' motion_name=samyeon motion_idx=1, ATTACK_SKILL
_ck(st.motion_idx_of(1) == 1, "skill 1 motion_idx == 1 (列偏移已修)")
# 40250 constinfo.py 开启 USE_SKILL_EFFECT_UPGRADE_ENABLE=1 后,
# CPythonSkill::GetSkillMotionIndex(iGrade) 使用 SKILL_GRADEGAP=25。
_ck(st.has_method("motion_idx_for_grade"), "技能表提供等级动作索引入口")
if st.has_method("motion_idx_for_grade"):
_ck(st.motion_idx_for_grade(1, 1) == 26, "skill 1 M 级动作索引 = base + 25")
_ck(st.motion_idx_for_grade(1, 2) == 51, "skill 1 G 级动作索引 = base + 50")
_ck(st.motion_grade_by_idx(26) == 1, "远端动作索引 26 可反解为 M 级")
_ck(String(s1.get("motion", "")) == "samyeon", "skill 1 motion_name == samyeon")
var skill_icon := UiAssets.load_tex(assets, "ETC/ymir work/ui/skill/warrior/samyeon_01.sub")
_ck(skill_icon != null, "skill 1 icon loads from .sub + shared SkillWarrior.dds")
@@ -128,6 +164,10 @@ func _run() -> void:
_ck(st.is_need_bow(46) and st.is_ranged(46), "IsNeedBow(46)")
_ck(st.can_use_weapon_type(46, st.WEAPON_BOW), "CanUseWeaponType(46, BOW)")
_ck(not st.can_use_weapon_type(46, st.WEAPON_SWORD), "CanUseWeaponType(46, SWORD) == false")
_ck(st.motion_loop_count_of(46, 5) == 2,
"skill 46 MotionLoopCountFormula(5) == 2")
_ck(st.motion_state_arg(46, 5) == 2,
"skill 46 OnUseSkill arg packs loop count in low nibble")
# skill 138 Horse Stump:HORSE_SKILL|SEARCH_TARGET|CHARGE_ATTACK
_ck(st.is_horse_skill(138), "IsHorseSkill(138)")
_ck(st.is_auto_search_target(138), "IsAutoSearchTarget(138)")
@@ -303,6 +343,11 @@ func _run() -> void:
var qb: Node = Quickbar.new()
get_root().add_child(qb)
qb.setup(fc, st, ui, func() -> Node: return pl)
var cooldown_default: float = qb._skill_cooldown(1)
fc.points["casting_speed"] = 20
_ck(absf(qb._skill_cooldown(1) - cooldown_default * 1.8) < 0.001,
"POINT_CASTING_SPEED=20 applies the 40250 180% cooldown scale")
fc.points["casting_speed"] = 0
var cast_targets: Array = []
qb.skill_cast_started.connect(func(sid: int, vid: int): cast_targets.append([sid, vid]))
qb.assign(0, "skill", 1)
@@ -316,6 +361,11 @@ func _run() -> void:
_ck(cast_targets == [[1, 2000]], "successful cast publishes resolved visual target")
if fc.casts.size() == 1:
_ck(int(fc.casts[0][0]) == st.motion_idx_of(1), "cast used skill 1's motion_idx")
_ck(int(fc.casts[0][4]) == 0, "skill 1 with no loop formula sends OnUseSkill arg 0")
fc.skills[0]["master"] = 1
_ck(qb._motion_idx_for_skill(1) == 26,
"quickbar uses the current M grade motion index (base + SKILL_GRADEGAP)")
fc.skills[0]["master"] = 0
# 冷却中不再施放
qb.activate(0)
_ck(fc.skill_uses.size() == 1, "on cooldown -> no second use_skill")
@@ -330,6 +380,28 @@ func _run() -> void:
qb.activate(0)
_ck(fc.casts.size() == 1, "server refresh cannot reset skill cooldown")
fc.quickslots = [] # restore this fixture's original server state for later UI cases
# ClientVS22::ClickSkillSlot routes GUILD skills to UseGuildSkill rather than
# silently dropping them. The dedicated path still enforces guild-war-only.
var guild_fc := FakeClient.new()
get_root().add_child(guild_fc)
var guild_qb: Node = Quickbar.new()
get_root().add_child(guild_qb)
guild_qb.setup(guild_fc, st, ui, func() -> Node: return pl)
guild_qb.assign(0, "skill", 152, false)
var guild_rejected: Array = []
guild_qb.skill_rejected.connect(func(sid: int, code: String): guild_rejected.append([sid, code]))
guild_qb._skill_gate.guild_war_active = false
guild_qb.activate(0)
_ck(guild_rejected == [[152, "ONLY_FOR_GUILD_WAR"]] and guild_fc.guild_skill_uses.is_empty(),
"guild skill outside guild war -> rejected without ordinary CG_USE_SKILL")
guild_qb._skill_gate.guild_war_active = true
guild_qb.activate(0)
_ck(guild_fc.guild_skill_uses == [[152, 0]],
"guild skill -> use_guild_skill(skill, 0)")
_ck(guild_fc.casts.size() == 1 and int(guild_fc.casts[0][4]) == 1,
"guild skill -> FUNC_SKILL action with loop count 1")
guild_qb.free()
guild_fc.free()
# 40250 quickslot skill refs are player skill-slot numbers. The same ref=2
# selects a different skill after switching warrior skill group.
fc.skill_group = 2
@@ -342,8 +414,8 @@ func _run() -> void:
_ck(fc.quickslot_ops == [["add", 0, 2, 5]],
"assign warrior group 2 skill 20 persists player skill-slot 5")
fc.skill_group = 1
fc.quickslots = []
qb.assign(0, "skill", 1, false)
fc.quickslots = [{"pos": 0, "type": 2, "ref": 1}, {"pos": 1, "type": 2, "ref": 1}]
qb.restore_from_server()
qb._clear(1)
# GC_SKILL_COOLTIME_END 提前解锁
fc.skill_cooldown_end.emit(1)