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

119 lines
7.7 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
# 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;护甲行强条目:验收读数必须按解锁后参数重测)。
# 行值全部 provisional 归 元进度设计批配读数(用户拍板骨架先行,先例 = GP+商店房批)。
class_name MetaTable
extends Resource
@export var forge_products: Array = [] # Array[Dictionary] 键 attr
@export var boss_keystone: Array = [] # Array[Dictionary] 键 level_index
@export var workshop_items: Array = [] # Array[Dictionary] 键 item_id
@export var conversion: Array = [] # Array[Dictionary] 键 attr
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 = []
# 行族空护栏(四族皆为 ConfigDB 消费面——缺族即致命)
for family in ["forge_products", "boss_keystone", "workshop_items", "conversion"]:
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})
return errs