# VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 玩家能否在营地用钥石换永久强化、且强化在下一局开局生效——存档变厚成为玩家可感的现实 #(元进度 GDD 验收标准,本批 = 其实现批) # Date: 2026-09-21(元进度实现批——设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2) # 数据配置 MetaTable schema(元进度/永久强化 行族四轴)。 # 行族四轴(数据契约对齐 ConfigDB.meta_* 访问器——消费面一律走访问器,禁直读行集): # forge_products = 属性锻造商品行(键 attr:{max_hp, atk, armor_class} 恰三唯一——注入契约三字段必齐; # 投值语言锁定 flat 整数;几何定价 tier 级价格 = 迭代 p = floor(p × price_ratio_pct/100),tier 0 = base_price)。 # boss_keystone = Boss 钥石两档行(level_index 连续从 1;first_kill ≥ repeat_kill——首杀全量、 # 重复杀固定小额恒定不递减,两档数值归元进度设计批配读数)。 # workshop_items = 道具工坊行(item_id = registry aux_items 六件词表封闭恰全;首版解锁渠道 = 总通关次数 # 达标 clear_threshold——2026-09-20 排期裁决;局内效果整体归 Alpha 秘宝重设计批,本表只有解锁状态面)。 # conversion = 区间平移换算率行(attr 与 forge 词表一致恰三;per_level_shift_pct ∈ [0,100]—— # registry 键 meta_progression_conversion_table;护甲行强条目:验收读数必须按解锁后参数重测)。 # attune_refresh_upgrade = Y 刷新升级行族(迭代 11 第①批 T4——P1 甲:每冒险开局给 Y 次、冒险周期池、 # 用完即无;level 连续从 1,等级 L → refresh_charges=Y 表值,钥石直购 keystone_cost;数值全 provisional)。 # 行值全部 provisional 归 元进度设计批配读数(用户拍板骨架先行,先例 = GP+商店房批)。 class_name MetaTable extends Resource @export var forge_products: Array[Dictionary] = [] # Array[Dictionary] 键 attr @export var boss_keystone: Array[Dictionary] = [] # Array[Dictionary] 键 level_index @export var workshop_items: Array[Dictionary] = [] # Array[Dictionary] 键 item_id @export var conversion: Array[Dictionary] = [] # Array[Dictionary] 键 attr @export var attune_refresh_upgrade: Array[Dictionary] = [] # Array[Dictionary] 键 level——Y 刷新升级行族(迭代 11 第①批 T4) const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] const WORKSHOP_ITEM_IDS := ["probe_dust", "sapper_order", "loot_compass", "nest_pendulum", "catacomb_scroll", "blast_kit"] # 工坊解锁渠道词表(可替换轴——2026-09-21 用户拍板:解锁条件须抽象、可替换、可扩展。 # 现行唯一合法值 total_clears = 总通关次数;扩新渠道 = 词表加值 + MetaProgression._workshop_progress # 增一分支,阈值比较与解锁写档零改动。缺省(行无 channel 字段)= total_clears) const WORKSHOP_CHANNELS := ["total_clears"] func validate() -> Array: var errs: Array[Dictionary] = [] # 行族空护栏(五族皆为 ConfigDB 消费面——缺族即致命) for family in ["forge_products", "boss_keystone", "workshop_items", "conversion", "attune_refresh_upgrade"]: if get(family).is_empty(): errs.append({"category": "empty_table", "message": "MetaTable 行族为空: %s" % family}) # forge_products:attr 词表封闭 + 唯一 + 覆盖恰三(注入契约三字段必齐)+ flat/价格域 + 公比域 [100,400] var seen_attr := {} for row in forge_products: var attr: String = row.get("attr", "") if not FORGE_ATTRS.has(attr): errs.append({"category": "enum", "message": "forge_products attr 出域 {max_hp, atk, armor_class}: %s" % attr}) if seen_attr.has(attr): errs.append({"category": "dup_key", "message": "forge_products attr 重复: %s" % attr}) seen_attr[attr] = true if String(row.get("display_name", "")) == "": errs.append({"category": "domain", "message": "forge_products %s display_name 空" % attr}) var flat: int = int(row.get("flat_value", 0)) if flat < 1 or flat > 999: errs.append({"category": "domain", "message": "forge_products %s flat_value 越域 [1,999]: %d" % [attr, flat]}) var base: int = int(row.get("base_price", 0)) if base < 1 or base > 999: errs.append({"category": "domain", "message": "forge_products %s base_price 越域 [1,999]: %d" % [attr, base]}) var ratio: int = int(row.get("price_ratio_pct", -1)) if ratio < 100 or ratio > 400: errs.append({"category": "domain", "message": "forge_products %s price_ratio_pct 越域 [100,400]: %d" % [attr, ratio]}) if not forge_products.is_empty(): for attr_key in FORGE_ATTRS: if not seen_attr.has(attr_key): errs.append({"category": "coverage", "message": "forge_products 缺属性键: %s(注入契约三字段必齐——恰三)" % attr_key}) # boss_keystone:level_index 唯一 + 连续从 1 + 两档域 + first ≥ repeat(首杀全量、重复小额恒定) var seen_li := {} for row in boss_keystone: var li: int = int(row.get("level_index", 0)) if seen_li.has(li): errs.append({"category": "dup_key", "message": "boss_keystone level_index 重复: %d" % li}) seen_li[li] = true var first: int = int(row.get("first_kill", -1)) var rep: int = int(row.get("repeat_kill", -1)) if first < 1 or first > 999 or rep < 0 or rep > 999: errs.append({"category": "domain", "message": "boss_keystone L%d 两档金额越域 first [1,999] / repeat [0,999]: %d/%d" % [li, first, rep]}) if first < rep: errs.append({"category": "cross_field", "message": "boss_keystone L%d 违反 first_kill(%d) ≥ repeat_kill(%d)——首杀全量、重复杀小额" % [li, first, rep]}) for i in boss_keystone.size(): var li_at: int = int(boss_keystone[i].get("level_index", 0)) if li_at != i + 1: errs.append({"category": "structural", "message": "boss_keystone level_index 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, li_at]}) break # workshop_items:item_id 词表封闭 + 唯一 + 覆盖恰六(registry aux_items 词表)+ 阈值域 [1,99] var seen_item := {} for row in workshop_items: var iid: String = row.get("item_id", "") if not WORKSHOP_ITEM_IDS.has(iid): errs.append({"category": "enum", "message": "workshop_items item_id 出域 aux_items 六件词表: %s" % iid}) if seen_item.has(iid): errs.append({"category": "dup_key", "message": "workshop_items item_id 重复: %s" % iid}) seen_item[iid] = true if String(row.get("display_name", "")) == "": errs.append({"category": "domain", "message": "workshop_items %s display_name 空" % iid}) var th: int = int(row.get("clear_threshold", -1)) if th < 1 or th > 99: errs.append({"category": "domain", "message": "workshop_items %s clear_threshold 越域 [1,99]: %d" % [iid, th]}) # channel:可替换解锁渠道(缺省 total_clears——词表封闭,扩渠道 = 词表加值 + 判定量分支) var channel: String = row.get("channel", "total_clears") if not WORKSHOP_CHANNELS.has(channel): errs.append({"category": "enum", "message": "workshop_items %s channel 出域 %s: %s" % [iid, str(WORKSHOP_CHANNELS), channel]}) if not workshop_items.is_empty(): for iid_key in WORKSHOP_ITEM_IDS: if not seen_item.has(iid_key): errs.append({"category": "coverage", "message": "workshop_items 缺道具键: %s(六件词表恰全)" % iid_key}) # conversion:attr 词表与 forge 一致 + 唯一 + 覆盖恰三 + 平移量域 [0,100] var seen_conv := {} for row in conversion: var cattr: String = row.get("attr", "") if not FORGE_ATTRS.has(cattr): errs.append({"category": "enum", "message": "conversion attr 出域 {max_hp, atk, armor_class}: %s" % cattr}) if seen_conv.has(cattr): errs.append({"category": "dup_key", "message": "conversion attr 重复: %s" % cattr}) seen_conv[cattr] = true var shift: int = int(row.get("per_level_shift_pct", -1)) if shift < 0 or shift > 100: errs.append({"category": "domain", "message": "conversion %s per_level_shift_pct 越域 [0,100]: %d" % [cattr, shift]}) if not conversion.is_empty(): for conv_key in FORGE_ATTRS: if not seen_conv.has(conv_key): errs.append({"category": "coverage", "message": "conversion 缺属性行: %s(三行与 forge 词表一致)" % conv_key}) # attune_refresh_upgrade:level 唯一 + 连续从 1 + charges/cost 域(迭代 11 第①批 T4) var seen_al := {} for row in attune_refresh_upgrade: var al: int = int(row.get("level", -1)) if seen_al.has(al): errs.append({"category": "dup_key", "message": "attune_refresh_upgrade level 重复: %d" % al}) seen_al[al] = true var ach: int = int(row.get("refresh_charges", -1)) if ach < 1 or ach > 9: errs.append({"category": "domain", "message": "attune_refresh_upgrade L%d refresh_charges 越域 [1,9]: %d" % [al, ach]}) var acost: int = int(row.get("keystone_cost", -1)) if acost < 1 or acost > 999: errs.append({"category": "domain", "message": "attune_refresh_upgrade L%d keystone_cost 越域 [1,999]: %d" % [al, acost]}) for i in attune_refresh_upgrade.size(): var al_at: int = int(attune_refresh_upgrade[i].get("level", 0)) if al_at != i + 1: errs.append({"category": "structural", "message": "attune_refresh_upgrade level 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, al_at]}) break return errs