Files
gplan-2/game/data/schemas/treasure_item_table.gd
T

301 lines
18 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.
## VERTICAL SLICE - NOT FOR PRODUCTION
## 秘宝表 schema:逐 item_id 行(掉落件+同调件)的类型化读取(空表合法态;垂直切片验证:断头时捡到秘宝买回情报/行动点)。
## 🔴 零裸 flat 护栏(返工红线):行键白名单封闭 + effect_kinds 封闭枚举只收条件触发/机制修正类——无条件属性加项禁入表。
## 承重护栏(schema 强制不靠运行时拦截):真视窄域——真视类活行 target ≤3×3,禁全图看雷。
## retired/deferred 标记行免活行校验、不入投放。
class_name TreasureItemTable
extends Resource
@export var rows: Array[Variant] = [] # Array[Dictionary] 键 item_id
# 计数键封闭枚举(就地内联于本表,无独立谓词表;
# 解锁口径=C6(冒险成功=打通整局冒险),键名 adventure_cleared
# (生涯冒险成功局数,save_manager 生涯计数域在役);扩 vision_used(真视类工具
# 累计使用——信标网络行为条件计数;use_treasure 受理成功处累计、冒险收束导出入档);
# 键域以本枚举为准)
const UNLOCK_COUNTER_KEYS := ["adventure_cleared", "boss_kill_count", "vision_used"]
const DIMENSIONS := ["info", "risk", "action", "plan", "counter"]
# enum 九 kind 全部条件触发/机制修正类;mine_reveal/ap_grant 语义不变。
# cell_removal = 中立移除危险格通道;mine_hit_halve 系固定
# 「减半」语义不消费 magnitude(反弹 N 由 reflect_damage 行 magnitude 承载)。
# 主动件 kind 词族(reveal_tiles/preview/grant_ap/remove_mines/
# refresh_pick——效果引擎 ACTIVE_KINDS 同名闭集);被动件不走 effect_kinds 词族
# (passive 行走 triggers 事件订阅形态);旧词保留共存至迁移完成。
const EFFECT_KINDS := ["mine_reveal", "reward_preview", "ap_grant",
"mine_hit_halve", "reflect_damage", "ap_rounds_front", "ap_charge_safe", "cell_removal",
"reward_weight_bias", "bonus_refresh",
"reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick",
# 以下八型:
# 常时参数面六型(无事件订阅——开局/装板参数注入,注入面按装配集 kind 分发读取):
# light_radius_bonus=微光半径加值〔阔视野带〕/rap_transfer_mult=停手转存率千分比〔储备罐 125=1.25〕/
# dig_range_bonus=挖掘距离加值〔远程镐〕/seal_range_free=封印不限相邻〔信标网络〕/
# refresh_stock_grant=三选一刷新存量一次性加仓〔刷新练习〕/shop_refresh_bonus=商店免费刷新加值〔刷新执照〕;
# 主动分发两型:safe_dig=安全格直开(天眼——走普通挖格规则全额兑付,C5 裁)/mine_count_preview=危险格数量预告(巢穴预告)
"light_radius_bonus", "rap_transfer_mult", "dig_range_bonus", "seal_range_free",
"refresh_stock_grant", "shop_refresh_bonus", "safe_dig", "mine_count_preview"]
const TARGETS := ["none", "area_3x3", "board_wide", "cell"] # cell=单格真视(线香——≤3×3 窄域契约内)
const USE_PHASES := ["active_dig"]
const FORMS := ["", "passive", "active_charged"]
# 档位词表(槽位档位约束数据面:槽位可装 ≤ 槽位档位的件;词表沿 feats/adventure TIERS)
const TIERS := ["common", "excellent", "rare", "legendary"]
# 档位序(装配档位校验取数源:件档 rank ≤ 装入位序(格 1=普通格/格 2=优秀格/格 3=稀有格
# 递增,向下兼容=高档格可装低档件);rank 越大档越高)
const TIER_RANK := {"common": 1, "excellent": 2, "rare": 3, "legendary": 4}
# 充能重置周期(现役仅 adventure=每冒险;battle=每战重置〔线香〕/level=每关重置〔天眼〕)
const CHARGE_RESETS := ["adventure", "battle", "level"]
# magnitude 消费面(≥1 校验):grant_ap 与同调 N 值 kind;其余 kind 活行 magnitude 恒 0
const MAGNITUDE_KINDS := ["ap_grant", "reflect_damage", "ap_rounds_front",
"ap_charge_safe", "reward_weight_bias", "bonus_refresh", "grant_ap",
"light_radius_bonus", "rap_transfer_mult", "dig_range_bonus", "seal_range_free",
"refresh_stock_grant", "shop_refresh_bonus"]
# 真视类 kind(护栏:活行 target 必须 ∈ {area_3x3, cell}:真视窄域 ≤3×3 契约)
const REVEAL_KINDS := ["mine_reveal", "reveal_tiles"]
# 🔴 零裸 flat 结构护栏:行键白名单封闭(键出域=加载致命)——机制轴禁无条件属性加项的表结构承载
const KEY_WHITELIST := ["item_id", "display_name", "dimension_tags", "effect_kinds", "target",
"magnitude", "use_phase", "retired", "deferred", "form", "charges_per_adventure", "slot_cost",
"unlock_requires", "effective_rounds", "charge_n", "triggers", "desc",
"tier", "charge_reset", "unlock_keystone_cost"] # 三键=档位/充能周期/钥石购买价
func validate() -> Array:
var errs: Array[Variant] = []
if rows.is_empty():
# 空表合法态(切片期无道具内容;F2 覆盖检查仅对非空清单执行)
return errs
var seen := {}
for row in rows:
var iid: String = row.get("item_id", "")
if iid == "":
errs.append({"category": "domain", "message": "item_id 空"})
continue
if seen.has(iid):
errs.append({"category": "dup_key", "message": "item_id 重复: %s" % iid})
continue
seen[iid] = true
errs.append_array(_validate_row_head(row, iid))
if bool(row.get("retired", false)) or bool(row.get("deferred", false)):
# 退役/失义行:免 effect_kinds 非空与承重护栏校验(登记面保留,不入投放与 F2)
continue
var kind_seen := {}
errs.append_array(_validate_row_active(row, iid, kind_seen))
var form: String = String(row.get("form", ""))
if form != "":
errs.append_array(_validate_row_attunement(row, iid, kind_seen))
errs.append_array(_validate_row_unlock_costs(row, iid))
errs.append_array(_validate_row_effect_numbers(row, iid, kind_seen))
errs.append_array(_validate_row_unlock_requires(row, iid))
return errs
## 行头域:display_name/维度标签/target/use_phase/行键白名单/triggers 订阅形态/form 词表
func _validate_row_head(row: Dictionary, iid: String) -> Array[Variant]:
var errs: Array[Variant] = []
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "%s display_name 空" % iid})
# dimension_tags:五维词表 + 非空(退役/失义行仍须登记维度——语义面保留)
var tags: Array[Variant] = row.get("dimension_tags", [])
if tags.is_empty():
errs.append({"category": "domain", "message": "%s dimension_tags 空" % iid})
for tag in tags:
if not DIMENSIONS.has(String(tag)):
errs.append({"category": "enum", "message": "%s dimension_tag 出域 %s: %s" % [iid, str(DIMENSIONS), str(tag)]})
var target: String = String(row.get("target", "none"))
if not TARGETS.has(target):
errs.append({"category": "enum", "message": "%s target 出域 %s: %s" % [iid, str(TARGETS), target]})
var phase: String = String(row.get("use_phase", "active_dig"))
if not USE_PHASES.has(phase):
errs.append({"category": "enum", "message": "%s use_phase 出域 %s: %s" % [iid, str(USE_PHASES), phase]})
# 🔴 零裸 flat 结构护栏:键白名单封闭(同调批 T1——机制轴禁无条件属性加项)
for k in row.keys():
if not KEY_WHITELIST.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s(KEY_WHITELIST 外键禁入)" % [iid, String(k)]})
errs.append_array(_validate_row_triggers(row, iid))
var form: String = String(row.get("form", ""))
if not FORMS.has(form):
errs.append({"category": "enum", "message": "%s form 出域 %s: %s" % [iid, str(FORMS), form]})
return errs
## 订阅形态域:triggers 事件订阅行判据与行级触发词表校验(被动件形态;与 effect_kinds 互斥校验
## 面=passive 行(单形态——效果行为只走订阅);active_charged 行两形合法并存
## (effect_kinds=主动分发通道 + triggers=受理闸订阅,积点解锁先例))
func _validate_row_triggers(row: Dictionary, iid: String) -> Array[Variant]:
var errs: Array[Variant] = []
if row.has("triggers"):
if (row.get("effect_kinds", []) as Array).size() > 0 and String(row.get("form", "")) != "active_charged" and not bool(row.get("retired", false)):
errs.append({"category": "domain", "message": "%s triggers 与 effect_kinds 并存(换代单形态——被动件迁移期禁双挂)" % iid})
EffectEngine.validate_trigger_row(iid, row, row.get("triggers", []) as Array, errs)
return errs
## 活行域:effect_kinds 非空与词表/真视窄域与 board_wide 承重护栏/magnitude 语义(kind_seen 引用填充——同调域消费)
func _validate_row_active(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
var kinds: Array[Variant] = row.get("effect_kinds", [])
var target: String = String(row.get("target", "none"))
if kinds.is_empty() and not row.has("triggers"):
errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred,被动件应挂 triggers 订阅)" % iid})
for k in kinds:
var kind: String = String(k)
if not EFFECT_KINDS.has(kind):
errs.append({"category": "enum", "message": "%s effect_kind 出域 %s: %s" % [iid, str(EFFECT_KINDS), kind]})
if kind_seen.has(kind):
errs.append({"category": "dup_key", "message": "%s effect_kind 重复: %s" % [iid, kind]})
kind_seen[kind] = true
# 承重护栏①真视窄域:真视类活行 target 必须 area_3x3 或 cell(一次最多 3×3,禁全图看雷;
# reveal_tiles 与 cell 档纳入同护栏,线香单格=cell ⊂ 3×3 契约内)
for rv in REVEAL_KINDS:
if kind_seen.has(String(rv)) and not (target == "area_3x3" or target == "cell"):
errs.append({"category": "load_bearing", "message": "%s 真视窄域护栏:%s 行 target 必须 area_3x3 或 cell,实得 %s(秘宝 GDD 结构冻结)" % [iid, String(rv), target]})
# 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果
if target == "board_wide" and kind_seen.has("mine_reveal"):
errs.append({"category": "load_bearing", "message": "%s board_wide 行 effect_kinds 不得含 mine_reveal(全图域仅恢复推理原料类——Alpha 重定后落位)" % iid})
errs.append_array(_validate_row_magnitude(row, iid, kind_seen))
return errs
## N 值语义域:magnitude 越域与 MAGNITUDE_KINDS 行消费(≥1);其余活行恒 0(同调批 T1——消费面自 ap_grant 扩至 N 值 kind)
func _validate_row_magnitude(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
var mag: int = int(row.get("magnitude", 0))
if mag < 0 or mag > 999:
errs.append({"category": "domain", "message": "%s magnitude 越域 [0,999]: %d" % [iid, mag]})
var mag_consuming := false
for k in kind_seen.keys():
if MAGNITUDE_KINDS.has(String(k)):
mag_consuming = true
if mag_consuming and mag < 1:
errs.append({"category": "cross_field", "message": "%s N 值 kind 行 magnitude 应 ≥1,实得 %d" % [iid, mag]})
if not mag_consuming and mag != 0 and not row.has("triggers"):
errs.append({"category": "cross_field", "message": "%s 非 N 值 kind 活行 magnitude 应 0(trigger 行 magnitude=value_from 取数源豁免),实得 %d" % [iid, mag]})
return errs
## 同调充能与档位域(form 非空——装配面;过渡行 form="" 不校验):槽位统一/档位必填/充能值域与积点制受理闸/充能重置周期
func _validate_row_attunement(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
if int(row.get("slot_cost", 1)) != 1:
errs.append({"category": "load_bearing", "message": "%s 同调占位统一 1 槽(slot_cost 恒 1——底稿裁决 5,schema 强制)" % iid})
# 档位必填(槽位档位约束数据面:件档≤格档判定的取数源)
var tier: String = String(row.get("tier", ""))
if not TIERS.has(tier):
errs.append({"category": "domain", "message": "%s 同调行 tier 必填且 ∈ %s,实得 %s" % [iid, str(TIERS), tier]})
var charges: int = int(row.get("charges_per_adventure", 0))
var form: String = String(row.get("form", ""))
if form == "passive" and charges != 0:
errs.append({"category": "cross_field", "message": "%s passive 行 charges_per_adventure 应 0,实得 %d" % [iid, charges]})
if form == "active_charged" and charges < 0:
errs.append({"category": "domain", "message": "%s active_charged 行 charges_per_adventure 越域 ≥0,实得 %d" % [iid, charges]})
if form == "active_charged" and charges == 0:
# 充能 0=积点制/滑耗制合法态(试炼镐转化获取,无固定充能上限):
# 必须带使用闸——ap_charge_safe 滑耗(安全格积点)或 treasure_used+counter_spend 受理闸,二者缺一即孤儿行
errs.append_array(_validate_row_charge_gate(row, iid, kind_seen))
# 充能重置周期(adventure 缺省/battle 每战/level 每关;仅 active_charged 行可设)
var creset: String = String(row.get("charge_reset", "adventure"))
if not CHARGE_RESETS.has(creset):
errs.append({"category": "enum", "message": "%s charge_reset 出域 %s: %s" % [iid, str(CHARGE_RESETS), creset]})
if form != "active_charged" and row.has("charge_reset") and creset != "adventure":
errs.append({"category": "cross_field", "message": "%s 非 active_charged 行 charge_reset 仅可缺省(adventure),实得 %s" % [iid, creset]})
return errs
## 积点制受理闸域:charges=0 的 active_charged 行必须带使用闸(ap_charge_safe 滑耗或 counter_spend 受理闸)
func _validate_row_charge_gate(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
var has_gate := kind_seen.has("ap_charge_safe")
if not has_gate and row.has("triggers"):
for tr_v in (row.get("triggers", []) as Array):
if String((tr_v as Dictionary).get("do", {}).get("kind", "")) == "counter_spend":
has_gate = true
break
if not has_gate:
errs.append({"category": "cross_field", "message": "%s active_charged 行 charges=0(积点制)须带 ap_charge_safe 或 counter_spend 受理闸,实得二者皆无" % iid})
return errs
## 解锁互斥域(form 非空行,契约 6):钥石购买价与条件谓词互斥——每件单解锁方式,混合不做
func _validate_row_unlock_costs(row: Dictionary, iid: String) -> Array[Variant]:
var errs: Array[Variant] = []
# 解锁混合制字段校验(契约 6:钥石购买价与条件谓词互斥——每件单解锁方式,混合不做)
var price: int = int(row.get("unlock_keystone_cost", 0))
var ureq_count: int = (row.get("unlock_requires", []) as Array).size()
if price != 0 and ureq_count > 0:
errs.append({"category": "cross_field", "message": "%s 解锁方式互斥:unlock_keystone_cost 与 unlock_requires 不得并存(每件单解锁方式,契约 6)" % iid})
if price < 0 or price > 999:
errs.append({"category": "domain", "message": "%s unlock_keystone_cost 越域 [0,999](0=非购买件缺省),实得 %d" % [iid, price]})
return errs
## 效果数值域(form 非空行):effective_rounds 与 charge_n 的值域及 kind 交叉
func _validate_row_effect_numbers(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
var eff_rounds: int = int(row.get("effective_rounds", 0))
if eff_rounds < 0 or eff_rounds > 999:
errs.append({"category": "domain", "message": "%s effective_rounds 越域 [0,999]: %d" % [iid, eff_rounds]})
if kind_seen.has("ap_rounds_front") and eff_rounds < 1:
errs.append({"category": "cross_field", "message": "%s ap_rounds_front 行 effective_rounds 应 ≥1(P4 甲表值化),实得 %d" % [iid, eff_rounds]})
if not kind_seen.has("ap_rounds_front") and eff_rounds != 0 and not row.has("triggers"):
errs.append({"category": "cross_field", "message": "%s 非 ap_rounds_front 行 effective_rounds 应 0(trigger 行=when value_from 取数源豁免),实得 %d" % [iid, eff_rounds]})
var charge_n: int = int(row.get("charge_n", 0))
if charge_n < 0 or charge_n > 999:
errs.append({"category": "domain", "message": "%s charge_n 越域 [0,999]: %d" % [iid, charge_n]})
if kind_seen.has("ap_charge_safe") and charge_n < 1:
errs.append({"category": "cross_field", "message": "%s ap_charge_safe 行 charge_n 应 ≥1(每次充能需 N 安全格),实得 %d" % [iid, charge_n]})
if not kind_seen.has("ap_charge_safe") and charge_n != 0:
errs.append({"category": "cross_field", "message": "%s 非 ap_charge_safe 行 charge_n 应 0,实得 %d" % [iid, charge_n]})
return errs
## 谓词行域:unlock_requires 计数键封闭枚举与阈值(空 = 已解锁——首批全解锁合法态,底稿 §2.2)
func _validate_row_unlock_requires(row: Dictionary, iid: String) -> Array[Variant]:
var errs: Array[Variant] = []
var ureqs: Array[Variant] = row.get("unlock_requires", [])
var ukeys := {}
for req in ureqs:
var ukey: String = String(req.get("key", ""))
if not UNLOCK_COUNTER_KEYS.has(ukey):
errs.append({"category": "enum", "message": "%s unlock_requires 计数键出域 %s: %s" % [iid, str(UNLOCK_COUNTER_KEYS), ukey]})
if ukeys.has(ukey):
errs.append({"category": "dup_key", "message": "%s unlock_requires 计数键重复: %s" % [iid, ukey]})
ukeys[ukey] = true
var uth: int = int(req.get("threshold", 0))
if uth < 1 or uth > 999:
errs.append({"category": "domain", "message": "%s unlock_requires 阈值越域 [1,999]: %d" % [iid, uth]})
return errs
# 同调行词表(form 非空)——装配覆盖层/效果接线/同调位上限的唯一取数口径
func attunement_rows() -> Array:
var out: Array[Variant] = []
for row in rows:
if String(row.get("form", "")) != "" and not bool(row.get("retired", false)) and not bool(row.get("deferred", false)):
out.append(row)
return out
# 过渡行词表(form 空 = 切片期过渡形态活行)——掉落投放/停点池/过渡覆盖面的取数口径;
# 同调行永不入过渡渠道。
func transitional_rows() -> Array:
var out: Array[Variant] = []
for row in rows:
if String(row.get("form", "")) == "" and not bool(row.get("retired", false)) and not bool(row.get("deferred", false)):
out.append(row)
return out
# 活行词表(非 retired 非 deferred)——投放校验/奖励节点池/F2 覆盖面的唯一取数口径
func active_rows() -> Array:
var out: Array[Variant] = []
for row in rows:
if not bool(row.get("retired", false)) and not bool(row.get("deferred", false)):
out.append(row)
return out
func row_by_id(item_id: String) -> Dictionary:
for row in rows:
if String(row.get("item_id", "")) == item_id:
return row
return {}