- 装备校验:can_equip 按 CanEquipNow→FindEquipCell→EquipItem 精确重排, 新增 find_equip_cell() 1:1 复刻(RING/UNIQUE 占用翻转、COSTUME/BELT/DS), 帝国 antiflag 降级为 tooltip 提示,LIMIT_PCBANG/REAL_TIME* 语义与倒计时文案 - 快捷栏:死亡全槽禁用+激活守卫、变身(affect 220)技能置灰、 物品槽失配置灰(服务器回收/换武器后刷新) - 公会:GUILD_AUTH_* 权限位门控踢人/公告/技能升级,/gskillup、 被宣战应答弹窗(/war·/nowar)、资金存取行(40250 服务端已禁用仍保留 1:1) - 好友/情侣:messenger 三固定组(好友/公会/情侣),lover_login/logout/ near/far/divorce 命令事件→在线/在身边状态与离婚隐藏 - 交易/仓库:ExchangeState.last_notice(ALREADY/LESS_GOLD)结束浮层提示; 仓库改密 /safebox_change_password 三段输入;关窗补 /safebox_close·/mall_close; 仓库金钱经核实 40250 无 wire 协议,UI 定界只读 - app_flow:重连时 guild_marks_ready 重复连接加 is_connected 守卫 - 相机 look_at 零向量守卫(首帧/传送落点) - 测试:equip/love/guild 断言更新,79 项中 78 绿(game_camera_test 为 HEAD 既有失败);真服冒烟 LIVE_SMOKE PASS(192.168.21.203 全链路) - docs/CLIENT-GAP.md 同步增量 129 Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018kUFvQGYusuY3dkfGitmAe
188 lines
11 KiB
GDScript
188 lines
11 KiB
GDScript
# equip_rules_test —— 装备穿戴合法性校验(EquipRules)1:1 自检。
|
||
# 顺序对齐服务器真实链:CanEquipNow(JOB→limit→UNIQUE 去重) → FindEquipCell →
|
||
# EquipItem 后置(燕尾服/变身/性别/骑乘)。
|
||
# canonical #17。 godot --headless --path project --script equip_rules_test.gd
|
||
extends SceneTree
|
||
|
||
const EquipRules = preload("res://equip_rules.gd")
|
||
|
||
var failed := 0
|
||
|
||
func _ck(ok: bool, message: String) -> void:
|
||
if not ok:
|
||
failed += 1
|
||
printerr("FAIL: " + message)
|
||
|
||
func _weapon(extra := {}) -> Dictionary:
|
||
var d := {"type": 1, "sub_type": 0, "anti_flags": 0, "wear_flags": 1 << 4, "limits": []}
|
||
d.merge(extra, true)
|
||
return d
|
||
|
||
func _init() -> void:
|
||
_test_race_tables()
|
||
_test_default_wear()
|
||
_test_slot_accepts()
|
||
_test_antiflag()
|
||
_test_wearable()
|
||
_test_limits()
|
||
_test_order_and_newslots()
|
||
_test_find_equip_cell()
|
||
_test_post_checks()
|
||
if failed == 0:
|
||
print("PASS: equip_rules_test (CanEquipNow→FindEquipCell→EquipItem 后置 1:1)")
|
||
quit(0)
|
||
else:
|
||
quit(1)
|
||
|
||
func _test_race_tables() -> void:
|
||
# race 0..7 = WARRIOR_M / ASSASSIN_W / SURA_M / SHAMAN_W / WARRIOR_W / ASSASSIN_M / SURA_W / SHAMAN_M
|
||
_ck(EquipRules.sex_of_race(0) == EquipRules.SEX_MALE, "race 0 male")
|
||
_ck(EquipRules.sex_of_race(1) == EquipRules.SEX_FEMALE, "race 1 female")
|
||
_ck(EquipRules.sex_of_race(4) == EquipRules.SEX_FEMALE, "race 4 female (warrior W)")
|
||
_ck(EquipRules.sex_of_race(5) == EquipRules.SEX_MALE, "race 5 male (assassin M)")
|
||
_ck(EquipRules.job_of_race(0) == EquipRules.JOB_WARRIOR, "race 0 warrior")
|
||
_ck(EquipRules.job_of_race(1) == EquipRules.JOB_ASSASSIN, "race 1 assassin")
|
||
_ck(EquipRules.job_of_race(6) == EquipRules.JOB_SURA, "race 6 sura")
|
||
_ck(EquipRules.job_of_race(7) == EquipRules.JOB_SHAMAN, "race 7 shaman")
|
||
|
||
func _test_default_wear() -> void:
|
||
_ck(EquipRules.default_wear_for(_weapon()) == EquipRules.WEAR_WEAPON, "weapon -> WEAR_WEAPON")
|
||
_ck(EquipRules.default_wear_for({"type": 2, "sub_type": 0}) == EquipRules.WEAR_BODY, "armor body -> WEAR_BODY")
|
||
_ck(EquipRules.default_wear_for({"type": 2, "sub_type": 1}) == EquipRules.WEAR_HEAD, "armor head -> WEAR_HEAD")
|
||
_ck(EquipRules.default_wear_for({"type": 2, "sub_type": 2}) == EquipRules.WEAR_SHIELD, "armor shield -> WEAR_SHIELD")
|
||
_ck(EquipRules.default_wear_for({"type": 2, "sub_type": 5}) == EquipRules.WEAR_NECK, "armor neck -> WEAR_NECK")
|
||
_ck(EquipRules.default_wear_for({"type": 28, "sub_type": 0}) == EquipRules.WEAR_COSTUME_BODY, "costume body -> 19")
|
||
_ck(EquipRules.default_wear_for({"type": 28, "sub_type": 1}) == EquipRules.WEAR_COSTUME_HAIR, "costume hair -> 20")
|
||
_ck(EquipRules.default_wear_for({"type": 33}) == EquipRules.WEAR_RING1, "ring -> WEAR_RING1")
|
||
_ck(EquipRules.default_wear_for({"type": 34}) == EquipRules.WEAR_BELT, "belt -> WEAR_BELT")
|
||
_ck(EquipRules.default_wear_for({"type": 3}) == -1, "potion -> not equippable")
|
||
_ck(not EquipRules.is_equippable({"type": 3}), "is_equippable(potion) false")
|
||
_ck(EquipRules.is_equippable(_weapon()), "is_equippable(weapon) true")
|
||
|
||
func _test_slot_accepts() -> void:
|
||
_ck(EquipRules.slot_accepts(_weapon(), EquipRules.WEAR_WEAPON), "weapon accepted at WEAR_WEAPON")
|
||
_ck(not EquipRules.slot_accepts(_weapon(), EquipRules.WEAR_BODY), "weapon rejected at WEAR_BODY")
|
||
_ck(EquipRules.slot_accepts({"type": 2, "wear_flags": 1 << 0}, EquipRules.WEAR_BODY), "body armor at WEAR_BODY")
|
||
_ck(EquipRules.slot_accepts({"type": 2, "wear_flags": 1 << 7}, EquipRules.WEAR_UNIQUE2), "unique flag at WEAR_UNIQUE2")
|
||
_ck(EquipRules.slot_accepts({"type": 33}, EquipRules.WEAR_RING1), "ring type at WEAR_RING1 (no wear_flags)")
|
||
_ck(not EquipRules.slot_accepts(_weapon(), EquipRules.WEAR_RING1), "weapon rejected at WEAR_RING1")
|
||
_ck(EquipRules.slot_accepts({"type": 34}, EquipRules.WEAR_BELT), "belt type at WEAR_BELT")
|
||
|
||
func _test_antiflag() -> void:
|
||
var female_only := _weapon({"anti_flags": EquipRules.ANTIFLAG_FEMALE})
|
||
_ck(not EquipRules.can_equip(female_only, -1, {"race": 1})["ok"], "ANTI_FEMALE blocks female (race 1)")
|
||
_ck(EquipRules.can_equip(female_only, -1, {"race": 1})["code"] == "ANTI_SEX", "code ANTI_SEX")
|
||
_ck(EquipRules.can_equip(female_only, -1, {"race": 0})["ok"], "ANTI_FEMALE ok for male (race 0)")
|
||
|
||
var male_only := _weapon({"anti_flags": EquipRules.ANTIFLAG_MALE})
|
||
_ck(not EquipRules.can_equip(male_only, -1, {"race": 0})["ok"], "ANTI_MALE blocks male")
|
||
_ck(EquipRules.can_equip(male_only, -1, {"sex": EquipRules.SEX_FEMALE})["ok"], "ANTI_MALE ok when sex override female")
|
||
|
||
var no_warrior := _weapon({"anti_flags": EquipRules.ANTIFLAG_WARRIOR})
|
||
_ck(EquipRules.can_equip(no_warrior, -1, {"race": 0})["code"] == "ANTI_JOB", "ANTI_WARRIOR blocks warrior (race 0)")
|
||
_ck(EquipRules.can_equip(no_warrior, -1, {"race": 1})["ok"], "ANTI_WARRIOR ok for assassin (race 1)")
|
||
|
||
# 帝国 antiflag 不是装备门(服务器装备路径从不检查)——can_equip 放行,
|
||
# 冲突位只由 empire_warning() 给 tooltip。
|
||
var no_emp_a := _weapon({"anti_flags": EquipRules.ANTIFLAG_EMPIRE_A})
|
||
_ck(EquipRules.can_equip(no_emp_a, -1, {"empire": 1})["ok"], "ANTI_EMPIRE no longer blocks equip")
|
||
_ck(EquipRules.empire_warning(no_emp_a, 1) == EquipRules.ANTIFLAG_EMPIRE_A, "empire_warning reports conflict")
|
||
_ck(EquipRules.empire_warning(no_emp_a, 2) == 0, "empire_warning clean for empire 2")
|
||
_ck(EquipRules.empire_warning(no_emp_a, 0) == 0, "empire_warning clean without empire")
|
||
|
||
func _test_wearable() -> void:
|
||
var res := EquipRules.can_equip(_weapon(), EquipRules.WEAR_BODY, {"race": 0})
|
||
_ck(not res["ok"] and res["code"] == "NOT_WEARABLE_HERE", "weapon at WEAR_BODY -> NOT_WEARABLE_HERE")
|
||
_ck(EquipRules.can_equip(_weapon(), EquipRules.WEAR_WEAPON, {"race": 0})["ok"], "weapon at WEAR_WEAPON ok")
|
||
|
||
func _test_limits() -> void:
|
||
var lv30 := _weapon({"limits": [{"type": EquipRules.LIMIT_LEVEL, "value": 30}]})
|
||
var r := EquipRules.can_equip(lv30, -1, {"race": 0, "level": 20})
|
||
_ck(not r["ok"] and r["code"] == "LIMIT_LEVEL" and int(r["need"]) == 30, "level 20 < 30 -> LIMIT_LEVEL need 30")
|
||
_ck(EquipRules.can_equip(lv30, -1, {"race": 0, "level": 30})["ok"], "level 30 == 30 ok")
|
||
_ck(EquipRules.can_equip(lv30, -1, {"race": 0, "level": 99})["ok"], "level 99 ok")
|
||
|
||
var str50 := _weapon({"limits": [{"type": EquipRules.LIMIT_STR, "value": 50}]})
|
||
_ck(EquipRules.can_equip(str50, -1, {"race": 0})["ok"], "STR limit skipped when ctx has no st (-1)")
|
||
_ck(not EquipRules.can_equip(str50, -1, {"race": 0, "st": 40})["ok"], "st 40 < 50 -> fail")
|
||
_ck(EquipRules.can_equip(str50, -1, {"race": 0, "st": 50})["ok"], "st 50 ok")
|
||
_ck(EquipRules.can_equip(str50, -1, {"race": 0, "st": 40})["code"] == "LIMIT_STR", "code LIMIT_STR")
|
||
|
||
var con20 := _weapon({"limits": [{"type": EquipRules.LIMIT_CON, "value": 20}]})
|
||
_ck(not EquipRules.can_equip(con20, -1, {"race": 0, "ht": 10})["ok"], "ht 10 < 20 -> LIMIT_CON")
|
||
|
||
# value <= 0 的 limit 项被忽略
|
||
var zero := _weapon({"limits": [{"type": EquipRules.LIMIT_LEVEL, "value": 0}]})
|
||
_ck(EquipRules.can_equip(zero, -1, {"race": 0, "level": 1})["ok"], "limit value 0 ignored")
|
||
|
||
func _test_order_and_newslots() -> void:
|
||
# limit 先于性别(服务器顺序:CanEquipNow 的 limit 在 EquipItem 的
|
||
# FN_check_item_sex 之前):两个都失败 → 返回 LIMIT_LEVEL
|
||
var both := _weapon({"anti_flags": EquipRules.ANTIFLAG_FEMALE,
|
||
"limits": [{"type": EquipRules.LIMIT_LEVEL, "value": 99}]})
|
||
_ck(EquipRules.can_equip(both, -1, {"race": 1, "level": 1})["code"] == "LIMIT_LEVEL",
|
||
"limit checked before sex antiflag (server order)")
|
||
# JOB 仍是最先的 antiflag 检查
|
||
var job_first := _weapon({"anti_flags": EquipRules.ANTIFLAG_WARRIOR,
|
||
"limits": [{"type": EquipRules.LIMIT_LEVEL, "value": 99}]})
|
||
_ck(EquipRules.can_equip(job_first, -1, {"race": 0, "level": 1})["code"] == "ANTI_JOB",
|
||
"job antiflag checked before limit")
|
||
|
||
# 显式槽不匹配(客户端防御层)在 limit 之后
|
||
var wl := _weapon({"limits": [{"type": EquipRules.LIMIT_LEVEL, "value": 99}]})
|
||
_ck(EquipRules.can_equip(wl, EquipRules.WEAR_BODY, {"race": 0, "level": 99})["code"] == "NOT_WEARABLE_HERE",
|
||
"explicit slot mismatch -> NOT_WEARABLE_HERE")
|
||
|
||
# 新装备槽:ring / belt / costume 按 type 判定,无 wear_flags 也能穿
|
||
_ck(EquipRules.can_equip({"type": 33}, EquipRules.WEAR_RING1, {"race": 0})["ok"], "ring at RING1 ok")
|
||
_ck(EquipRules.can_equip({"type": 34}, EquipRules.WEAR_BELT, {"race": 0})["ok"], "belt at BELT ok")
|
||
_ck(EquipRules.can_equip({"type": 28, "sub_type": 0}, -1, {"race": 0})["ok"], "costume body default slot ok")
|
||
_ck(EquipRules.can_equip({"type": 28}, EquipRules.WEAR_BELT, {"race": 0})["code"] == "NOT_WEARABLE_HERE",
|
||
"costume at BELT rejected")
|
||
|
||
# 空物品
|
||
_ck(EquipRules.can_equip({}, -1, {})["code"] == "NO_ITEM", "empty item -> NO_ITEM")
|
||
|
||
func _test_find_equip_cell() -> void:
|
||
# item.cpp:472 的 else-if 优先级与 RING/UNIQUE 占用翻转。
|
||
_ck(EquipRules.find_equip_cell(_weapon()) == EquipRules.WEAR_WEAPON, "weapon cell")
|
||
_ck(EquipRules.find_equip_cell(_weapon({"wear_flags": (1 << 0) | (1 << 4)})) == EquipRules.WEAR_BODY,
|
||
"BODY wins over WEAPON in else-if chain")
|
||
_ck(EquipRules.find_equip_cell({"type": 33}) == EquipRules.WEAR_RING1, "ring -> RING1 when free")
|
||
_ck(EquipRules.find_equip_cell({"type": 33}, {EquipRules.WEAR_RING1: 1234}) == EquipRules.WEAR_RING2,
|
||
"ring -> RING2 when RING1 occupied")
|
||
var uniq := {"type": 2, "wear_flags": 1 << 7, "vnum": 70001}
|
||
_ck(EquipRules.find_equip_cell(uniq) == EquipRules.WEAR_UNIQUE1, "unique -> UNIQUE1 when free")
|
||
_ck(EquipRules.find_equip_cell(uniq, {EquipRules.WEAR_UNIQUE1: 70002}) == EquipRules.WEAR_UNIQUE2,
|
||
"unique -> UNIQUE2 when UNIQUE1 occupied")
|
||
# 无 wear_flag 且非 COSTUME/DS/RING/BELT → -1(TOTEM 同)
|
||
_ck(EquipRules.find_equip_cell({"type": 3}) == -1, "no wear flag -> -1")
|
||
_ck(EquipRules.find_equip_cell({"type": 26, "wear_flags": 1 << 0}) == -1, "totem -> -1")
|
||
# 龙魂石走客户端专用路径
|
||
_ck(EquipRules.find_equip_cell({"type": 29, "sub_type": 3}) == -1, "DS -> -1 (dedicated path)")
|
||
|
||
func _test_post_checks() -> void:
|
||
# EquipItem 后置门(char_item.cpp:6122+)。
|
||
# 变身中除 ARROW 外一律拒绝
|
||
var bow := _weapon({"wear_flags": 1 << 9})
|
||
_ck(EquipRules.can_equip(_weapon(), -1, {"race": 0, "polymorphed": true})["code"] == "POLYMORPH",
|
||
"polymorphed blocks weapon")
|
||
_ck(EquipRules.can_equip(bow, -1, {"race": 0, "polymorphed": true})["ok"],
|
||
"polymorphed allows ARROW")
|
||
# 燕尾服骑乘禁止(vnum 11901..11904)
|
||
var tux := {"type": 2, "sub_type": 0, "wear_flags": 1 << 0, "vnum": 11901}
|
||
_ck(EquipRules.can_equip(tux, -1, {"race": 0, "riding": true})["code"] == "RIDE_TUXEDO",
|
||
"tuxedo blocked while riding")
|
||
_ck(EquipRules.can_equip(tux, -1, {"race": 0, "riding": false})["ok"], "tuxedo ok on foot")
|
||
# 骑乘物品已骑乘禁止(UNIQUE + RIDE/MOUNT_RIDE 子类)
|
||
var horse := {"type": 16, "sub_type": 2, "wear_flags": 1 << 7, "vnum": 50051}
|
||
_ck(EquipRules.can_equip(horse, -1, {"race": 0, "riding": true})["code"] == "RIDE_ITEM",
|
||
"ride item blocked while riding")
|
||
_ck(EquipRules.can_equip(horse, -1, {"race": 0, "riding": false})["ok"], "ride item ok on foot")
|
||
# UNIQUE 同 vnum 去重
|
||
var u := {"type": 2, "wear_flags": 1 << 7, "vnum": 70001}
|
||
_ck(EquipRules.can_equip(u, -1, {"race": 0, "worn": {EquipRules.WEAR_UNIQUE1: 70001}})["code"] == "UNIQUE_DUP",
|
||
"same vnum in UNIQUE1 -> UNIQUE_DUP")
|
||
_ck(EquipRules.can_equip(u, -1, {"race": 0, "worn": {EquipRules.WEAR_UNIQUE1: 70002}})["ok"],
|
||
"different vnum in UNIQUE1 ok")
|