feat(content): 元进度实现批 T0+T1——worktree 基线(冒烟437三闸绿+MC L1 normal 35%存档 qa)+ MetaTable 四行族建表(schema/tres/ConfigDB接线/冒烟4断言,441绿)
前置门偏差登记:规划书假设 B1/B2 已合并落 master,实测 B1 在并行 worktree 进行中(未提交)、 B2 worktree 刚建未开工——基线为 pre-B1/B2 master,T7 须在 B1/B2 合并后复验白板回归。
This commit is contained in:
1 parent
1d9d4c4799
commit
629b905056
6 files changed
+268
No files matched your search
@@ -0,0 +1,11 @@
|
||||
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||
|
||||
模拟日志: user://logs/playtest_4200_20260921T042329.jsonl
|
||||
=== 关卡期望扫描:level 1 · N=20/格 · 种子 4200+20 · 策略=normal · 种子模式=shared ===
|
||||
策略 atk%/hp% 通关率 整关承伤均/p90 通关剩余HP均(胜) 进BossHP均 平均轮数 踩雷/战 普通承伤/场% 跳战率 Boss承伤% 踩雷/局 刷新/局
|
||||
基线 atk +0% hp +0% 35% 16.4 / 22.0 4.3 14.0 15.2 0.03 14.6 7% 44.9 0.15 0.00 6.boss×12 4.battle×1
|
||||
(F4 带:承伤 ≤20%/普通场 · ≤50%/Boss 场,占生效 max_hp · F5 带:基线通关率 L1 [35,55]% / L2 [25,45]%——2026-09-20 用户拍板分带 · 跳战率 = 普通战斗 round-1 即胜(首轮全灭)占比 · 承伤含败者至死 · 败北节点 0-3=战斗节点序 4=Boss · 踩雷/战=每战斗负向兑付触发期望(格面资源批口径) · 完美档 = 技能轴上界测量(零盲挖/全标旗/卡死刷新——口径见 auto_player.gd)· 耗时 21.2 秒)
|
||||
WARNING: 49 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||
at: cleanup (core/object/object.cpp:2536)
|
||||
ERROR: 26 resources still in use at exit (run with --verbose for details).
|
||||
at: clear (core/io/resource.cpp:822)
|
||||
@@ -21,6 +21,7 @@ const TABLE_MANIFEST := {
|
||||
"level": "level.tres",
|
||||
"difficulty": "difficulty.tres",
|
||||
"shop": "shop.tres", # GP+商店房实施批 2026-09-21(表与 schema 落籍归并行数据面——本批只接线加载与消费)
|
||||
"meta": "meta.tres", # 元进度实现批 2026-09-21(MetaTable 四行族——schema 与表同批落籍)
|
||||
}
|
||||
|
||||
var action_economy: ActionEconomyTable
|
||||
@@ -37,6 +38,8 @@ var difficulty: DifficultyTable
|
||||
# GP+商店房实施批(2026-09-21):ShopTable 暂持无类型注记——schema class_name 落籍归并行数据面,
|
||||
# 落籍后可收紧为 `var shop: ShopTable`(消费面一律走下方 shop_* 访问器,禁直读行集)
|
||||
var shop = null
|
||||
# 元进度实现批(2026-09-21):MetaTable 类型化持用(schema 与表同批落籍,无 shop 先例的落籍间隙问题)
|
||||
var meta: MetaTable
|
||||
|
||||
var is_ready := false
|
||||
var _diagnostics: Array = [] # {severity, category, message}
|
||||
@@ -119,6 +122,7 @@ func load_tables(root: String, collect_only: bool) -> bool:
|
||||
level = loaded.get("level")
|
||||
difficulty = loaded.get("difficulty")
|
||||
shop = loaded.get("shop")
|
||||
meta = loaded.get("meta")
|
||||
is_ready = not has_errors
|
||||
return not has_errors
|
||||
|
||||
@@ -375,6 +379,46 @@ func _shop_rows(family: String) -> Array:
|
||||
return arr if arr is Array else []
|
||||
|
||||
|
||||
# ---- 元进度实现批(2026-09-21):MetaTable 消费面(行集解析单点——MetaProgression/呈现层禁自扫表)----
|
||||
# 行族四轴(数据契约,行集归 meta.tres,数值全 provisional 归元进度设计批配读数):
|
||||
# forge_products {attr, display_name, flat_value, base_price, price_ratio_pct} ·
|
||||
# boss_keystone {level_index, first_kill, repeat_kill} ·
|
||||
# workshop_items {item_id, display_name, clear_threshold} ·
|
||||
# conversion {attr, per_level_shift_pct}。
|
||||
|
||||
func meta_forge_row(attr: String) -> Dictionary:
|
||||
for row in _meta_rows("forge_products"):
|
||||
if String(row.get("attr", "")) == attr:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
func meta_boss_keystone_row(level_index: int) -> Dictionary:
|
||||
for row in _meta_rows("boss_keystone"):
|
||||
if int(row.get("level_index", -1)) == level_index:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
func meta_workshop_items() -> Array:
|
||||
return _meta_rows("workshop_items")
|
||||
|
||||
|
||||
func meta_conversion_row(attr: String) -> Dictionary:
|
||||
for row in _meta_rows("conversion"):
|
||||
if String(row.get("attr", "")) == attr:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# 行族数组读取(MetaTable 属性名 = 行族名;表缺失/属性缺失 = 空集非错误态)
|
||||
func _meta_rows(family: String) -> Array:
|
||||
if meta == null:
|
||||
return []
|
||||
var arr = meta.get(family)
|
||||
return arr if arr is Array else []
|
||||
|
||||
|
||||
# 技能行单点读取(敌人技能池批——单位容器执行上报 / 战斗结算兑现的消费面)
|
||||
func skill_row(skill_id: String) -> Dictionary:
|
||||
return skill.row_by_id(skill_id)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# 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"]
|
||||
|
||||
|
||||
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]})
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://b2nl5v8gb6cw
|
||||
@@ -0,0 +1,80 @@
|
||||
[gd_resource type="Resource" script_class="MetaTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/meta_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
forge_products = [{
|
||||
"attr": "max_hp",
|
||||
"display_name": "生命锻造",
|
||||
"flat_value": 3,
|
||||
"base_price": 10,
|
||||
"price_ratio_pct": 150
|
||||
}, {
|
||||
"attr": "atk",
|
||||
"display_name": "攻击锻造",
|
||||
"flat_value": 1,
|
||||
"base_price": 10,
|
||||
"price_ratio_pct": 150
|
||||
}, {
|
||||
"attr": "armor_class",
|
||||
"display_name": "护甲锻造",
|
||||
"flat_value": 1,
|
||||
"base_price": 10,
|
||||
"price_ratio_pct": 150
|
||||
}]
|
||||
boss_keystone = [{
|
||||
"level_index": 1,
|
||||
"first_kill": 5,
|
||||
"repeat_kill": 1
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"first_kill": 8,
|
||||
"repeat_kill": 2
|
||||
}, {
|
||||
"level_index": 3,
|
||||
"first_kill": 12,
|
||||
"repeat_kill": 3
|
||||
}]
|
||||
workshop_items = [{
|
||||
"item_id": "probe_dust",
|
||||
"display_name": "显影尘",
|
||||
"clear_threshold": 1
|
||||
}, {
|
||||
"item_id": "sapper_order",
|
||||
"display_name": "工兵令",
|
||||
"clear_threshold": 1
|
||||
}, {
|
||||
"item_id": "loot_compass",
|
||||
"display_name": "探宝罗盘",
|
||||
"clear_threshold": 2
|
||||
}, {
|
||||
"item_id": "nest_pendulum",
|
||||
"display_name": "探巢摆钟",
|
||||
"clear_threshold": 2
|
||||
}, {
|
||||
"item_id": "catacomb_scroll",
|
||||
"display_name": "地穴全图卷",
|
||||
"clear_threshold": 3
|
||||
}, {
|
||||
"item_id": "blast_kit",
|
||||
"display_name": "爆破工兵包",
|
||||
"clear_threshold": 3
|
||||
}]
|
||||
conversion = [{
|
||||
"attr": "max_hp",
|
||||
"per_level_shift_pct": 0
|
||||
}, {
|
||||
"attr": "atk",
|
||||
"per_level_shift_pct": 0
|
||||
}, {
|
||||
"attr": "armor_class",
|
||||
"per_level_shift_pct": 0
|
||||
}]
|
||||
|
||||
; 元进度实现批(2026-09-21,设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2):
|
||||
; 数值全 provisional 归元进度设计批配读数(骨架先行——元进度实现批 2026-09-21,先例 = GP+商店房批)。
|
||||
; forge flat 档与 GP 商店同族(血+3/攻+1/甲+1),base 10 / ratio 150 三行同;boss 两档 L1 5/1 · L2 8/2 · L3 12/3
|
||||
; (首杀全量、重复杀固定小额恒定不递减);工坊六件阈值 1/1/2/2/3/3 按总通关次数(首版只挂通关次数渠道,
|
||||
; 钥石直购价随定价批后补);conversion 全 0 = 未标定占位(标定归设计批,护甲行强条目:验收读数必须按
|
||||
; 解锁后参数重测)。
|
||||
@@ -98,6 +98,7 @@ func _run_core_smoke(db, rng) -> void:
|
||||
_smoke_shop_room()
|
||||
_smoke_shop_graph()
|
||||
_smoke_nine_types_gp_stream()
|
||||
_smoke_meta()
|
||||
|
||||
|
||||
# ---- 战斗屏棋盘布局·格边长公式回归(2026-09-19 真人试玩 bug⑥:L2 12×12 板下缘被裁约 1/6 格高——
|
||||
@@ -2820,6 +2821,27 @@ func _smoke_nine_types_gp_stream() -> void:
|
||||
check(rng.counter("gp_drop") == 0, "derive_at 纯函数不推计数器(gp_drop 流计数器恒 0)")
|
||||
|
||||
|
||||
# ---- 元进度·MetaTable 表结构(元进度实现批 T1——行值全 provisional 归元进度设计批配读数;
|
||||
# 注入契约三字段必齐 = forge/conversion 覆盖恰三,Boss 两档 first ≥ repeat)----
|
||||
func _smoke_meta() -> void:
|
||||
print("--- 元进度·MetaTable 表结构 ---")
|
||||
var meta_tbl = _db.meta
|
||||
check(meta_tbl != null and meta_tbl.validate().is_empty(),
|
||||
"MetaTable 非空 + validate 零错(四行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)")
|
||||
check(meta_tbl.forge_products.size() == 3 and meta_tbl.boss_keystone.size() == 3
|
||||
and meta_tbl.workshop_items.size() == 6 and meta_tbl.conversion.size() == 3,
|
||||
"MetaTable 四行族规模 3/3/6/3(forge/Boss/换算恰三 · 工坊恰六)")
|
||||
var attrs_ok := true
|
||||
for a in ["max_hp", "atk", "armor_class"]:
|
||||
attrs_ok = attrs_ok and not _db.meta_forge_row(a).is_empty() and not _db.meta_conversion_row(a).is_empty()
|
||||
check(attrs_ok, "forge/conversion attr 词表封闭恰全(注入契约三字段齐——meta_* 访问器可达)")
|
||||
var boss_ok := true
|
||||
for li in [1, 2, 3]:
|
||||
var brow: Dictionary = _db.meta_boss_keystone_row(li)
|
||||
boss_ok = boss_ok and not brow.is_empty() and int(brow.get("first_kill", 0)) >= int(brow.get("repeat_kill", -1))
|
||||
check(boss_ok, "boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)")
|
||||
|
||||
|
||||
# ---- 战斗日志视图·行格式化呈现层同步闸(2026-09-20 真人试玩卡断 bug 回归:八型批伏击退役拆除
|
||||
# 伤害行 attach 字段后本视图拆行分支漏网未同步——敌方首笔攻击行带 baseline 无 attach,
|
||||
# _format_row Invalid access 使结算段中途夭折、界面卡死。行协议变更必须同步呈现层〔战斗结算 §行协议〕,
|
||||
|
||||
Reference in new issue
Block a user