Files
mtgodot-poc/project/equip_rules_test.gd
T
shenleiandClaude Code 8092ff44e2 装备校验链/快捷栏/公会/好友情侣/交易仓库五项差距修复(增量 129)
- 装备校验: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
2026-09-07 21:43:30 +09:00

188 lines
11 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 → -1TOTEM 同)
_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")