# 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")