235 lines
7.7 KiB
GDScript
235 lines
7.7 KiB
GDScript
# equip_rules.gd —— 装备穿戴合法性校验(纯函数校验器)。
|
||
#
|
||
# 语义顺序对齐服务器 `char_item.cpp::CanEquipNow`:
|
||
# antiflag(性别 / 职业 / 帝国) → wearable_flag 与目标 wear 槽匹配 → limit(等级 / 属性)。
|
||
#
|
||
# 参考枚举 1:1 自 `REF/GameLib/ItemData.h`(`EItemAntiFlag` / `EItemWearableFlag` /
|
||
# `EWearPositions` / `ELimitTypes` / `EItemType` / `EArmorSubTypes` / `ECostumeSubTypes`)。
|
||
# 40250 客户端本身**不做** `CanEquipNow` 前置门(`PythonItemModule` 只暴露 `IsWearableFlag` /
|
||
# `GetLimit`,真正的仲裁在服务器)。这里按 §3.9 `__CanShot` 的方式在客户端防御式重建一道前置
|
||
# 门——精确的 tie-break 顺序、PCBANG / REAL_TIME 类 limit、已穿同槽替换等留待接服务器比对(seam)。
|
||
extends RefCounted
|
||
|
||
# --- EItemAntiFlag(ItemData.h,classic 排列:帝国位在 1<<9..1<<11)---
|
||
const ANTIFLAG_FEMALE := 1 << 0
|
||
const ANTIFLAG_MALE := 1 << 1
|
||
const ANTIFLAG_WARRIOR := 1 << 2
|
||
const ANTIFLAG_ASSASSIN := 1 << 3
|
||
const ANTIFLAG_SURA := 1 << 4
|
||
const ANTIFLAG_SHAMAN := 1 << 5
|
||
const ANTIFLAG_EMPIRE_A := 1 << 9
|
||
const ANTIFLAG_EMPIRE_B := 1 << 10
|
||
const ANTIFLAG_EMPIRE_R := 1 << 11
|
||
|
||
# --- EItemWearableFlag ---
|
||
const WEARABLE_BODY := 1 << 0
|
||
const WEARABLE_HEAD := 1 << 1
|
||
const WEARABLE_FOOTS := 1 << 2
|
||
const WEARABLE_WRIST := 1 << 3
|
||
const WEARABLE_WEAPON := 1 << 4
|
||
const WEARABLE_NECK := 1 << 5
|
||
const WEARABLE_EAR := 1 << 6
|
||
const WEARABLE_UNIQUE := 1 << 7
|
||
const WEARABLE_SHIELD := 1 << 8
|
||
const WEARABLE_ARROW := 1 << 9
|
||
|
||
# --- EWearPositions(0..10)+ 新装备系统槽(GameType.h,19..23)---
|
||
const WEAR_BODY := 0
|
||
const WEAR_HEAD := 1
|
||
const WEAR_FOOTS := 2
|
||
const WEAR_WRIST := 3
|
||
const WEAR_WEAPON := 4
|
||
const WEAR_NECK := 5
|
||
const WEAR_EAR := 6
|
||
const WEAR_UNIQUE1 := 7
|
||
const WEAR_UNIQUE2 := 8
|
||
const WEAR_ARROW := 9
|
||
const WEAR_SHIELD := 10
|
||
const WEAR_MAX_NUM := 11
|
||
const WEAR_COSTUME_BODY := 19
|
||
const WEAR_COSTUME_HAIR := 20
|
||
const WEAR_RING1 := 21
|
||
const WEAR_RING2 := 22
|
||
const WEAR_BELT := 23
|
||
|
||
# --- ELimitTypes ---
|
||
const LIMIT_NONE := 0
|
||
const LIMIT_LEVEL := 1
|
||
const LIMIT_STR := 2
|
||
const LIMIT_DEX := 3
|
||
const LIMIT_INT := 4
|
||
const LIMIT_CON := 5
|
||
const LIMIT_PCBANG := 6
|
||
const LIMIT_REAL_TIME := 7
|
||
|
||
# --- EItemType(相关项)+ 子类 ---
|
||
const ITEM_TYPE_WEAPON := 1
|
||
const ITEM_TYPE_ARMOR := 2
|
||
const ITEM_TYPE_COSTUME := 28
|
||
const ITEM_TYPE_RING := 33
|
||
const ITEM_TYPE_BELT := 34
|
||
|
||
const ARMOR_BODY := 0
|
||
const ARMOR_HEAD := 1
|
||
const ARMOR_SHIELD := 2
|
||
const ARMOR_WRIST := 3
|
||
const ARMOR_FOOTS := 4
|
||
const ARMOR_NECK := 5
|
||
const ARMOR_EAR := 6
|
||
|
||
const COSTUME_BODY := 0
|
||
const COSTUME_HAIR := 1
|
||
|
||
# --- 职业 / 性别 <- race(common length.h `MAIN_RACE_*`:job = race % 4;性别见下表)---
|
||
const JOB_WARRIOR := 0
|
||
const JOB_ASSASSIN := 1
|
||
const JOB_SURA := 2
|
||
const JOB_SHAMAN := 3
|
||
const SEX_MALE := 0
|
||
const SEX_FEMALE := 1
|
||
# race 0..7 = WARRIOR_M / ASSASSIN_W / SURA_M / SHAMAN_W / WARRIOR_W / ASSASSIN_M / SURA_W / SHAMAN_M
|
||
const SEX_BY_RACE := [SEX_MALE, SEX_FEMALE, SEX_MALE, SEX_FEMALE, SEX_FEMALE, SEX_MALE, SEX_FEMALE, SEX_MALE]
|
||
const JOB_ANTIFLAG := [ANTIFLAG_WARRIOR, ANTIFLAG_ASSASSIN, ANTIFLAG_SURA, ANTIFLAG_SHAMAN]
|
||
# 帝国 1/2/3(GC_EMPIRE)→ 对应 anti 位
|
||
const EMPIRE_ANTIFLAG := {1: ANTIFLAG_EMPIRE_A, 2: ANTIFLAG_EMPIRE_B, 3: ANTIFLAG_EMPIRE_R}
|
||
|
||
|
||
static func sex_of_race(race: int) -> int:
|
||
return int(SEX_BY_RACE[race]) if race >= 0 and race < SEX_BY_RACE.size() else SEX_MALE
|
||
|
||
|
||
static func job_of_race(race: int) -> int:
|
||
return (race % 4) if race >= 0 else JOB_WARRIOR
|
||
|
||
|
||
# 物品默认落在哪个 wear 槽(type / sub_type 决定)。不可穿戴 → -1。
|
||
static func default_wear_for(item: Dictionary) -> int:
|
||
var itype := int(item.get("type", -1))
|
||
var sub := int(item.get("sub_type", -1))
|
||
match itype:
|
||
ITEM_TYPE_WEAPON:
|
||
return WEAR_WEAPON
|
||
ITEM_TYPE_COSTUME:
|
||
return WEAR_COSTUME_HAIR if sub == COSTUME_HAIR else WEAR_COSTUME_BODY
|
||
ITEM_TYPE_RING:
|
||
return WEAR_RING1
|
||
ITEM_TYPE_BELT:
|
||
return WEAR_BELT
|
||
ITEM_TYPE_ARMOR:
|
||
match sub:
|
||
ARMOR_HEAD: return WEAR_HEAD
|
||
ARMOR_SHIELD: return WEAR_SHIELD
|
||
ARMOR_WRIST: return WEAR_WRIST
|
||
ARMOR_FOOTS: return WEAR_FOOTS
|
||
ARMOR_NECK: return WEAR_NECK
|
||
ARMOR_EAR: return WEAR_EAR
|
||
_: return WEAR_BODY
|
||
return -1
|
||
|
||
|
||
static func is_equippable(item: Dictionary) -> bool:
|
||
return default_wear_for(item) >= 0
|
||
|
||
|
||
# wear 槽 → 需要的 wearable_flag 位(新装备槽 19..23 无 wearable_flag → 0)。
|
||
static func wear_to_wearable(wear: int) -> int:
|
||
match wear:
|
||
WEAR_BODY: return WEARABLE_BODY
|
||
WEAR_HEAD: return WEARABLE_HEAD
|
||
WEAR_FOOTS: return WEARABLE_FOOTS
|
||
WEAR_WRIST: return WEARABLE_WRIST
|
||
WEAR_WEAPON: return WEARABLE_WEAPON
|
||
WEAR_NECK: return WEARABLE_NECK
|
||
WEAR_EAR: return WEARABLE_EAR
|
||
WEAR_UNIQUE1, WEAR_UNIQUE2: return WEARABLE_UNIQUE
|
||
WEAR_ARROW: return WEARABLE_ARROW
|
||
WEAR_SHIELD: return WEARABLE_SHIELD
|
||
return 0
|
||
|
||
|
||
# 目标槽能否接受这件物品:新装备槽按 item type,旧 11 槽按 wearable_flag 位。
|
||
static func slot_accepts(item: Dictionary, wear: int) -> bool:
|
||
var itype := int(item.get("type", -1))
|
||
match wear:
|
||
WEAR_COSTUME_BODY, WEAR_COSTUME_HAIR:
|
||
return itype == ITEM_TYPE_COSTUME
|
||
WEAR_RING1, WEAR_RING2:
|
||
return itype == ITEM_TYPE_RING
|
||
WEAR_BELT:
|
||
return itype == ITEM_TYPE_BELT
|
||
var need := wear_to_wearable(wear)
|
||
if need == 0:
|
||
return false
|
||
return (int(item.get("wear_flags", 0)) & need) != 0
|
||
|
||
|
||
static func _fail(code: String, need := 0) -> Dictionary:
|
||
return {"ok": false, "code": code, "need": need}
|
||
|
||
|
||
# 主校验。
|
||
# item : proto 字典 —— {type, sub_type, anti_flags, wear_flags, limits:[{type,value}]}
|
||
# wear : 目标 EWearPositions(或新装备槽 19..23);传 -1 用 default_wear_for(item)
|
||
# ctx : {race, sex, job, empire, level, st, dx, ht, iq}
|
||
# —— sex/job 缺省时由 race 推;属性 st/dx/ht/iq 缺省或 < 0 = 不判(seam)
|
||
# 返回 {ok: bool, code: String, need: int}。code 见下方常量注释;need = 失败 limit 的要求值。
|
||
static func can_equip(item: Dictionary, wear: int, ctx: Dictionary) -> Dictionary:
|
||
if item.is_empty():
|
||
return _fail("NO_ITEM")
|
||
var target := wear if wear >= 0 else default_wear_for(item)
|
||
if target < 0:
|
||
return _fail("NOT_EQUIPPABLE")
|
||
|
||
var anti := int(item.get("anti_flags", 0))
|
||
var race := int(ctx.get("race", 0))
|
||
var sex := int(ctx.get("sex", sex_of_race(race)))
|
||
var job := int(ctx.get("job", job_of_race(race)))
|
||
var empire := int(ctx.get("empire", 0))
|
||
|
||
# 1) anti-flag:性别
|
||
if sex == SEX_FEMALE and (anti & ANTIFLAG_FEMALE):
|
||
return _fail("ANTI_SEX")
|
||
if sex == SEX_MALE and (anti & ANTIFLAG_MALE):
|
||
return _fail("ANTI_SEX")
|
||
# 2) anti-flag:职业
|
||
if job >= 0 and job < JOB_ANTIFLAG.size() and (anti & int(JOB_ANTIFLAG[job])):
|
||
return _fail("ANTI_JOB")
|
||
# 3) anti-flag:帝国
|
||
var emp_bit := int(EMPIRE_ANTIFLAG.get(empire, 0))
|
||
if emp_bit != 0 and (anti & emp_bit):
|
||
return _fail("ANTI_EMPIRE")
|
||
|
||
# 4) wearable_flag 与目标槽匹配
|
||
if not slot_accepts(item, target):
|
||
return _fail("NOT_WEARABLE_HERE")
|
||
|
||
# 5) limit(等级 / 属性)—— 逐条 GetLimit
|
||
for lim in item.get("limits", []):
|
||
var t := int(lim.get("type", 0))
|
||
var v := int(lim.get("value", 0))
|
||
if v <= 0:
|
||
continue
|
||
match t:
|
||
LIMIT_LEVEL:
|
||
if int(ctx.get("level", 0)) < v:
|
||
return _fail("LIMIT_LEVEL", v)
|
||
LIMIT_STR:
|
||
if _stat(ctx, "st") >= 0 and _stat(ctx, "st") < v:
|
||
return _fail("LIMIT_STR", v)
|
||
LIMIT_DEX:
|
||
if _stat(ctx, "dx") >= 0 and _stat(ctx, "dx") < v:
|
||
return _fail("LIMIT_DEX", v)
|
||
LIMIT_INT:
|
||
if _stat(ctx, "iq") >= 0 and _stat(ctx, "iq") < v:
|
||
return _fail("LIMIT_INT", v)
|
||
LIMIT_CON:
|
||
if _stat(ctx, "ht") >= 0 and _stat(ctx, "ht") < v:
|
||
return _fail("LIMIT_CON", v)
|
||
_:
|
||
pass # PCBANG / REAL_TIME* → 服务器判(seam)
|
||
return {"ok": true, "code": "", "need": 0}
|
||
|
||
|
||
static func _stat(ctx: Dictionary, key: String) -> int:
|
||
return int(ctx.get(key, -1))
|