按 code-conventions §1.4.1 第三轮定稿执行:core 22+schemas 18+presentation 根 14+views 28,全部头注 #→##、删日期/变更叙事/GDD 与架构反向链接/字段与域清单/机制描述,保留职责摘要+真实使用约束(防事故禁令/单一入口/信号契约每信号一行)。main.gd 31→5 行、save_manager 45→5 行、adventure_runner 37→5 行为最大单件。子代理分模块执行(波一 core+schemas、波二 presentation+views,并发≤2)。验收=tools/verify_comment_only_change.py 逐字节校验(82 文件头注区外逐行一致=零行为变更)+audit_file_headers 全绿;纯注释批免冒烟(先例 5843270)
242 lines
17 KiB
GDScript
242 lines
17 KiB
GDScript
## 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
|
||
|
||
# 计数键封闭枚举(原 AttunementGrowthTable.COUNTER_KEYS——迭代 21 T5 谓词表退役后就地内联;
|
||
# 迭代 22 T1 换代:clear_count(旧工坊按关累计渠道)与 C6 通关口径(通关=打通整局冒险)不符,
|
||
# 换名 adventure_cleared(生涯通关局数,save_manager 生涯计数域在役)并扩 vision_used(真视类工具
|
||
# 累计使用——信标网络行为条件计数,本批新增;use_treasure 受理成功处累计、冒险收束导出入档);
|
||
# 旧 clear_count 零行消费,换名安全)
|
||
const UNLOCK_COUNTER_KEYS := ["adventure_cleared", "boss_kill_count", "vision_used"]
|
||
|
||
const DIMENSIONS := ["info", "risk", "action", "plan", "counter"]
|
||
# 同调批扩 enum(迭代 11 第①批 T1)——新增九 kind 全部条件触发/机制修正类;mine_reveal/ap_grant 语义不变。
|
||
# cell_removal = 爆破工兵包中立移除格(T7 原位换装时启用——T6 影响评估前置);mine_hit_halve 系固定
|
||
# 「减半」语义不消费 magnitude(反弹 N 由 reflect_damage 行 magnitude 承载)。
|
||
# 内容数据驱动扩展批 T2 挂账:主动件 kind 词族换代(reveal_tiles/preview/grant_ap/remove_mines/
|
||
# refresh_pick——效果引擎 ACTIVE_KINDS 同名闭集),被动件 effect_kinds 词族随 T4 秘宝迁移退役
|
||
# (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",
|
||
# 迭代 22 秘宝实装批 T1 扩签八型:
|
||
# 常时参数面六型(无事件订阅——开局/装板参数注入,注入面按装配集 kind 分发读取):
|
||
# light_radius_bonus=微光半径加值〔阔视野带〕/rap_transfer_mult=停手转存率千分比〔储备罐 125=1.25〕/
|
||
# dig_range_bonus=挖掘距离加值〔远程镐〕/flag_range_free=插旗不限相邻〔信标网络〕/
|
||
# refresh_stock_grant=三选一刷新存量一次性加仓〔刷新练习〕/shop_refresh_bonus=商店免费刷新加值〔刷新执照〕;
|
||
# 主动分发两型:safe_dig=安全格直开(天眼——走普通挖格规则全额兑付,C5 裁)/mine_count_preview=危险格数量预告(巢穴预告)
|
||
"light_radius_bonus", "rap_transfer_mult", "dig_range_bonus", "flag_range_free",
|
||
"refresh_stock_grant", "shop_refresh_bonus", "safe_dig", "mine_count_preview"]
|
||
const TARGETS := ["none", "area_3x3", "board_wide", "cell"] # 迭代 22 T1 扩 cell=单格真视(线香——≤3×3 窄域契约内)
|
||
const USE_PHASES := ["active_dig"]
|
||
const FORMS := ["", "passive", "active_charged"]
|
||
# 档位词表(迭代 22 T1——槽位档位约束数据面:槽位可装 ≤ 槽位档位的件;词表沿 feats/adventure TIERS)
|
||
const TIERS := ["common", "excellent", "rare", "legendary"]
|
||
# 档位序(迭代 22 T2——装配档位校验取数源:件档 rank ≤ 装入位序(格 1=普通格/格 2=优秀格/格 3=稀有格
|
||
# 递增,向下兼容=高档格可装低档件);rank 越大档越高)
|
||
const TIER_RANK := {"common": 1, "excellent": 2, "rare": 3, "legendary": 4}
|
||
# 充能重置周期(迭代 22 T1 扩签——现役仅 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", "flag_range_free",
|
||
"refresh_stock_grant", "shop_refresh_bonus"]
|
||
# 真视类 kind(迭代 22 T1 护栏扩面——活行 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"] # 迭代 22 T1 扩三键(档位/充能周期/钥石购买价)
|
||
|
||
|
||
func validate() -> Array:
|
||
var errs: Array[Variant] = []
|
||
if rows.is_empty():
|
||
# 空表合法态(2026-09-17 用户拍板——切片期无道具内容;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
|
||
if String(row.get("display_name", "")) == "":
|
||
errs.append({"category": "domain", "message": "%s display_name 空" % iid})
|
||
var retired: bool = bool(row.get("retired", false))
|
||
var deferred: bool = bool(row.get("deferred", false))
|
||
# 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 kinds: Array[Variant] = row.get("effect_kinds", [])
|
||
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)]})
|
||
# triggers 事件订阅行(内容数据驱动扩展批 T2/T4——被动件换代形态;与 effect_kinds 互斥校验
|
||
# 面=passive 行(换代单形态——效果行为只走订阅);active_charged 行两形合法并存
|
||
# (effect_kinds=主动分发通道 + triggers=受理闸订阅,掘进蓄能先例))
|
||
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)
|
||
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]})
|
||
if retired or deferred:
|
||
# 退役/失义行:免 effect_kinds 非空与承重护栏校验(登记面保留,不入投放与 F2)
|
||
continue
|
||
# ---- 活行校验 ----
|
||
if kinds.is_empty() and not row.has("triggers"):
|
||
errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred,被动件应挂 triggers 订阅)" % iid})
|
||
var kind_seen := {}
|
||
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,禁全图看雷;
|
||
# 迭代 22 T1 护栏扩面——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})
|
||
# magnitude:MAGNITUDE_KINDS 行消费(≥1);其余活行恒 0(同调批 T1——消费面自 ap_grant 扩至 N 值 kind)
|
||
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]})
|
||
# ---- 同调行约束(form 非空——装配面;过渡行 form="" 不校验)----
|
||
if form != "":
|
||
if int(row.get("slot_cost", 1)) != 1:
|
||
errs.append({"category": "load_bearing", "message": "%s 同调占位统一 1 槽(slot_cost 恒 1——底稿裁决 5,schema 强制)" % iid})
|
||
# 档位必填(迭代 22 T1——槽位档位约束数据面:件档≤格档判定的取数源)
|
||
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))
|
||
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=积点制/滑耗制合法态(迭代 22 T1——试炼镐转化获取,无固定充能上限):
|
||
# 必须带使用闸——ap_charge_safe 滑耗(安全格积点)或 treasure_used+counter_spend 受理闸,二者缺一即孤儿行
|
||
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})
|
||
# 充能重置周期(迭代 22 T1 扩签: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]})
|
||
# 解锁混合制字段校验(迭代 22 T1 契约 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]})
|
||
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]})
|
||
# unlock_requires 谓词行:空 = 已解锁(首批全解锁合法态,底稿 §2.2);非空键 ∈ 计数键封闭枚举
|
||
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 非空)——装配覆盖层/效果接线/同调位上限的唯一取数口径(迭代 11 第①批 T1)
|
||
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 空 = 切片期过渡形态活行)——掉落投放/停点池/过渡覆盖面的取数口径;
|
||
# 同调行永不入过渡渠道。T7 退役面切换后本口径随渠道消亡。
|
||
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 {}
|