完善客户端功能并同步差距文档

This commit is contained in:
shenlei
2026-09-03 18:40:01 +09:00
parent f93a172296
commit 13ccb8d02d
135 changed files with 21881 additions and 1259 deletions
+150 -8
View File
@@ -12,15 +12,22 @@ class FakeClient extends Node:
signal skill_cooldown_end(skill: int)
signal quickslots_changed()
var skills := [{"id": 1, "level": 5, "master": 0}, {"id": 16, "level": 3, "master": 2}]
var skill_group := 1
var points := {"level": 30, "skill_active": 5, "skill_support": 3, "skill_horse": 2}
var quickslots := []
var casts := []
var skill_uses := []
var quickslot_ops := []
var saids := []
var emotes := []
var main := 1000
func get_main_vid() -> int: return main
func get_entity(_v) -> Dictionary: return {"race": 0}
# vid 2000 是选中目标:给个 kind=2(怪)让 §3.8 目标解析判其可攻击。
func get_entity(_v) -> Dictionary: return {"race": 0, "kind": 2}
func get_inventory() -> Array: return []
func get_skills() -> Array: return skills
func get_skill_group() -> int: return skill_group
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
@@ -30,6 +37,7 @@ class FakeClient extends Node:
func quickslot_swap(pos, change_pos) -> bool: quickslot_ops.append(["swap", pos, change_pos]); 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
var _fail := 0
func _ck(c: bool, m: String) -> void:
@@ -79,6 +87,95 @@ func _run() -> void:
_ck(st.can_level_up(125) == false, "skill 125 'Item Creation' CANNOT_LEVEL_UP")
_ck(st.for_category("HORSE").size() >= 3, "HORSE 分类有技能")
# --- §3.8 TSkillData 判定位解析(增量 87):对齐 PythonSkill.cpp 谓词 ---
# skill 1 Three-Way CutATTACK_SKILL|NEED_TARGET|WEAPON_LIMITATIONW=SWORD|TWO_HANDED
_ck(st.attr_bits(1) & st.ATTR["ATTACK_SKILL"] != 0, "attr_bits(1) 含 ATTACK_SKILL 位")
_ck(st.is_need_target(1), "IsNeedTarget(1)")
_ck(st.is_can_use_skill(1), "IsCanUseSkill(1)(非被动)")
_ck(st.has_weapon_limitation(1), "skill 1 WEAPON_LIMITATION")
_ck(st.can_use_weapon_type(1, st.WEAPON_SWORD), "CanUseWeaponType(1, SWORD)")
_ck(st.can_use_weapon_type(1, st.WEAPON_TWO_HANDED), "CanUseWeaponType(1, TWO_HANDED)")
_ck(not st.can_use_weapon_type(1, st.WEAPON_BOW), "CanUseWeaponType(1, BOW) == false")
# skill 121 LeadershipPASSIVE —— IsCanUseSkill 必须挡下
_ck(st.is_passive(121) and not st.is_can_use_skill(121), "IsCanUseSkill(121 被动) == false")
# skill 18 StumpATTACK_SKILL|STANDING_SKILL
_ck(st.is_standing(18) and st.is_attack(18), "IsStandingSkill(18) && IsAttackSkill(18)")
_ck(not st.is_standing(1), "skill 1 非 STANDING_SKILL")
# skill 46 Repetitive ShotBOW
_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")
# skill 138 Horse StumpHORSE_SKILL|SEARCH_TARGET|CHARGE_ATTACK
_ck(st.is_horse_skill(138), "IsHorseSkill(138)")
_ck(st.is_auto_search_target(138), "IsAutoSearchTarget(138)")
_ck(st.is_charge_skill(138), "IsChargeSkill(138)")
_ck(st.skill_type_of(138) == st.SKILL_TYPE_HORSE, "skill_type_of(138) == SKILL_TYPE_HORSE")
# skill 137 Horseback SlashMOVING_SKILL
_ck(st.is_moving_skill(137), "IsMovingSkill(137)")
# skill 16 Spirit Strike (W)CAN_CHANGE_DIRECTION
_ck(st.can_change_direction(16), "CanChangeDirection(16)")
_ck(not st.can_change_direction(1), "skill 1 无 CAN_CHANGE_DIRECTION")
# skill 151 Dragon EyesJOB=GUILD
_ck(st.skill_type_of(151) == st.SKILL_TYPE_GUILD and st.is_guild_skill(151),
"skill_type_of(151) == SKILL_TYPE_GUILD")
# skill 152 Blood of Dragon GodONLY_FOR_GUILD_WAR
_ck(st.is_only_for_guild_war(152), "IsOnlyForGuildWar(152)")
# skill 109 CureCAN_USE_FOR_ME|ONLY_FOR_ALLIANCE
_ck(st.can_use_for_me(109) and st.is_only_for_alliance(109),
"CanUseForMe(109) && IsOnlyForAlliance(109)")
# skill 5 DashCHARGE_ATTACKskill 31 AmbushMELEE_ATTACK
_ck(st.is_charge_skill(5), "IsChargeSkill(5)")
_ck(st.is_melee(31), "IsMeleeSkill(31)")
# skill 63 Enchanted BladeTOGGLE|STANDING_SKILL|WEAPON_LIMITATION W=SWORD
_ck(st.is_toggle(63) and st.is_standing(63), "IsToggleSkill(63) && IsStandingSkill(63)")
_ck(st.can_use_weapon_type(63, st.WEAPON_SWORD)
and not st.can_use_weapon_type(63, st.WEAPON_DAGGER),
"skill 63 只吃 SWORD")
# 无 WEAPON_LIMITATION 的技能:CanUseWeaponType 恒真(PythonSkill.cpp:1143
_ck(st.can_use_weapon_type(106, st.WEAPON_FAN) and st.can_use_weapon_type(106, st.WEAPON_BOW),
"skill 106 无武器限制 -> CanUseWeaponType 恒真")
# _fold_bits:瓶子 / USE_HP 记号(当前语言数据里没有对应技能,直接验折叠逻辑)
_ck(SkillTable._fold_bits("NEED_EMPTY_BOTTLE|NEED_POISON_BOTTLE", st.ATTR)
== (st.ATTR["NEED_EMPTY_BOTTLE"] | st.ATTR["NEED_POISON_BOTTLE"]),
"_fold_bits 折叠 NEED_*_BOTTLE")
_ck(SkillTable._fold_bits("use_hp , circle_range foo", st.ATTR)
== (st.ATTR["USE_HP"] | st.ATTR["CIRCLE_RANGE"]),
"_fold_bits 大小写/逗号/空格宽容,未知记号忽略")
_ck(SkillTable._fold_bits("SWORD|DOUBLE_SWORD", st.NEED_WEAPON)
== (1 << st.WEAPON_SWORD | 1 << st.WEAPON_DAGGER),
"_fold_bits DOUBLE_SWORD 复用 DAGGER 位")
# --- §3.10 技能升级门(增量 90):max_level / level_limit / CanLevelUpSkill ---
# skill 1 samyeon.msk 有 MaxLevel "30"
_ck(st.max_level_of(1) == 30, "max_level_of(1) == 30samyeon.msk MaxLevel")
# skill 121 Leadership 是 SUPPORT,无 job_dir -> _load_msk 提前返回;skilldesc 无 MAX_LEVEL 列
_ck(st.max_level_of(121) == 20, "max_level_of(121) == 20SSkillData 构造默认)")
# 本 40250 shipped skilldesc.txt 只有 22 列,无 LEVEL_LIMIT 列 -> 恒 0
_ck(st.skill_level_limit(1) == 0, "skill_level_limit(1) == 0(本 build 无 LEVEL_LIMIT 数据)")
# skillCanLevelUpSkill:未满级且非 CANNOT_LEVEL_UP
_ck(st.can_level_up_skill(1, 5), "can_level_up_skill(1, 5) == true")
_ck(st.can_level_up_skill(1, 29), "can_level_up_skill(1, 29) == true29 < 30")
_ck(not st.can_level_up_skill(1, 30), "can_level_up_skill(1, 30) == false>= MaxLevel")
_ck(not st.can_level_up_skill(1, 31), "can_level_up_skill(1, 31) == false")
_ck(not st.can_level_up_skill(125, 1), "can_level_up_skill(125, 1) == falseCANNOT_LEVEL_UP")
_ck(not st.can_level_up_skill(9999, 1), "can_level_up_skill(未知 id) == false")
# --- §3.4 扇形/圆形补目标元数据(增量 91):GetTargetCount / __GetSkillTargetRange / FlyShape ---
# skill 47 'Arrow Shower' = gwangyeok.mskFAN_RANGE + TargetCountFormula + Range 2500
_ck(st.is_fan_range(47), "skill 47 IsFanRangegwangyeok FAN_RANGE")
_ck(st.fly_shape(47) == 1, "fly_shape(47) == FAN(1)")
_ck(st.target_range(47) == 2500, "target_range(47) == 2500gwangyeok.msk Range")
# "2 + floor(6 * SkillPoint)"lv1 SkillPoint=0.05 -> 2lv20(master) SkillPoint=0.5 -> 5
_ck(st.target_count(47, 1) == 2, "target_count(47, lv1) == 2")
_ck(st.target_count(47, 20) == 5, "target_count(47, lv20) == 5")
# skill 91 'Flying Talisman' = bipabu.mskFAN_RANGE + Range 1800,无 TargetCountFormula
_ck(st.is_fan_range(91), "skill 91 IsFanRangebipabu FAN_RANGE")
_ck(st.target_range(91) == 1800, "target_range(91) == 1800bipabu.msk Range")
_ck(st.target_count(91, 20) == 0, "target_count(91) == 0bipabu 无 TargetCountFormula -> 仅主目标)")
# 非扇形/圆形技能:fly_shape == SINGLE(0),无 target_range
_ck(st.fly_shape(1) == 0, "fly_shape(1) == SINGLE(0)samyeon 非范围技)")
_ck(st.target_count(1, 10) == 0, "target_count(1) == 0(无 TargetCountFormula")
var fc := FakeClient.new()
get_root().add_child(fc)
var ui: CanvasLayer = UiManager.new()
@@ -102,6 +199,25 @@ func _run() -> void:
if sk._rows.has(16):
_ck(String(sk._rows[16]["lv"].text) == "Lv 3 G", "skill 16 master 2 -> 'Lv 3 G'")
# --- §3.10 加号按钮门控(增量 90):主动页 ---
if sk._rows.has(1) and sk._rows[1]["up"] != null:
_ck(sk._rows[1]["up"].visible, "主动页 skill 1lv5 grade0, skill_active=5, group=1)加号可见")
if sk._rows.has(16) and sk._rows[16]["up"] != null:
_ck(not sk._rows[16]["up"].visible, "skill 16 skillGrade!=0 -> 加号隐藏")
# 技能组未选(GetMainActorSkillGroup()==0-> 整列隐藏加号
fc.skill_group = 0
sk.refresh()
if sk._rows.has(1) and sk._rows[1]["up"] != null:
_ck(not sk._rows[1]["up"].visible, "skill_group==0 -> __CanUseSkillNow 挡下,加号隐藏")
fc.skill_group = 1
# 该页技能点为 0 -> 加号隐藏
fc.points = {"level": 30, "skill_active": 0, "skill_support": 3, "skill_horse": 2}
sk.refresh()
if sk._rows.has(1) and sk._rows[1]["up"] != null:
_ck(not sk._rows[1]["up"].visible, "SKILL_ACTIVE 点数为 0 -> 加号隐藏")
fc.points = {"level": 30, "skill_active": 5, "skill_support": 3, "skill_horse": 2}
sk.refresh()
# 切到「辅助」页 -> 列 SUPPORT,被动技能无 []
sk._switch_tab(1)
_ck(sk._cat_key() == "SUPPORT", "tab 1 -> SUPPORT")
@@ -113,8 +229,29 @@ func _run() -> void:
any_up = true
_ck(any_up or sup.all(func(i): return st.is_passive(i) or not st.can_level_up(i)),
"辅助页有可加点项 或 全是被动")
# §3.10HIDE_SUPPORT_SKILL_POINT -> 辅助页所有加号按钮隐藏(即便技能点足够)
var sup_any_visible := false
for rid in sk._rows:
if sk._rows[rid]["up"] != null and sk._rows[rid]["up"].visible:
sup_any_visible = true
_ck(not sup_any_visible, "辅助页 HIDE_SUPPORT_SKILL_POINT -> 加号全隐藏")
sk._switch_tab(2)
_ck(sk._cat_key() == "HORSE", "tab 2 -> HORSE")
# §3.10:马术页 —— skill_horse 点数 > 0 且 skillLevel < 20 -> 加号可见
var horse_up_id := 0
for rid in sk._rows:
if sk._rows[rid]["up"] != null:
horse_up_id = rid
break
if horse_up_id != 0:
_ck(sk._rows[horse_up_id]["up"].visible,
"马术页 skill %dlv0 < 20, skill_horse=2)加号可见" % horse_up_id)
fc.points = {"level": 30, "skill_active": 5, "skill_support": 3, "skill_horse": 0}
sk.refresh()
_ck(not sk._rows[horse_up_id]["up"].visible, "SKILL_HORSE 点数为 0 -> 马术加号隐藏")
fc.points = {"level": 30, "skill_active": 5, "skill_support": 3, "skill_horse": 2}
sk.refresh()
sk._switch_tab(0) # 切回主动,后面的 quickbar 断言不受影响
# quickbar
@@ -140,25 +277,30 @@ func _run() -> void:
qb.activate(0)
_ck(fc.skill_uses.size() == 2, "cooldown_end -> can use_skill again")
_ck(fc.casts.size() == 2, "cooldown_end -> can cast again")
# 36 槽 = 4 页 × 9 格;第四页的第 9 格正好是全局槽 35。
# ClientVS22 的本地快捷栏显示 4 页 × 8 格;服务器仍保留 36 槽,
# 因此最后 4 个槽只可由协议恢复,不能从键盘本地显示/编辑。
qb.set_page(3)
qb.assign(8, "item", 9)
_ck(qb._state.size() == 36 and qb._state[35].kind == "item" and qb._state[35].id == 9,
"quickbar has 36 slots; page 4 slot 9 = global slot 35")
_ck(fc.quickslot_ops.back() == ["add", 35, 1, 9], "page 4 assign -> CG_QUICKSLOT_ADD{35,item,9}")
qb.assign(7, "item", 9)
_ck(qb._state.size() == 36 and qb._state[31].kind == "item" and qb._state[31].id == 9,
"quickbar has 36 server slots; page 4 slot 8 = global slot 31")
_ck(fc.quickslot_ops.back() == ["add", 31, 1, 9], "page 4 assign -> CG_QUICKSLOT_ADD{31,item,9}")
qb.set_page(0)
qb._swap(0, 1) # UI 的两次点击最终调用同一个交换操作
_ck(fc.quickslot_ops.back() == ["swap", 0, 1], "quickslot move -> CG_QUICKSLOT_SWAP")
qb._clear(1)
_ck(fc.quickslot_ops.back() == ["del", 1], "quickslot clear -> CG_QUICKSLOT_DEL")
# 服务器 GC_QUICKSLOT_* 恢复:type 2 技能 / type 1 道具 -> 填格
# 服务器 GC_QUICKSLOT_* 恢复:type 2 技能 / type 1 道具 / type 3 表情 -> 填格
var qb2: Node = Quickbar.new()
get_root().add_child(qb2)
fc.quickslots = [{"pos": 2, "type": 2, "ref": 16}, {"pos": 3, "type": 1, "ref": 9}]
fc.quickslots = [{"pos": 2, "type": 2, "ref": 16}, {"pos": 3, "type": 1, "ref": 9},
{"pos": 4, "type": 3, "ref": 11}]
qb2.setup(fc, st, ui, func() -> Node: return pl) # setup 里自动 restore_from_server
_ck(qb2._state[2].kind == "skill" and int(qb2._state[2].id) == 16, "quickslot 恢复:格2=技能16")
_ck(qb2._state[3].kind == "item" and int(qb2._state[3].id) == 9, "quickslot 恢复:格3=道具9")
_ck(qb2._state[4].kind == "emote" and int(qb2._state[4].id) == 11, "quickslot 恢复:格4=表情11")
qb2.activate(4)
_ck(fc.emotes == [11], "表情快捷栏 -> send_emoticon(11)")
fc.quickslots = [{"pos": 0, "type": 2, "ref": 1}]
fc.quickslots_changed.emit()
_ck(qb2._state[0].kind == "skill" and int(qb2._state[0].id) == 1, "quickslots_changed -> 重填")