# VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 钥石能否在营地换成永久强化并在下一局开局生效——冒险收束入账(大奖励 + Boss # 首杀/重复杀两档)、营地消费、注入计算的跨局账本能否在零 UI 下独立成立(元进度 GDD 验收标准) # Date: 2026-09-21(元进度实现批——设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2) # 元进度账本服务(Core 层零 UI、纯被动服务——同 StatusContainer 先例:零信号零外发,读存档算账)。 # 职责三件:① 属性锻造定价与购买(几何递增——tier 级价格 = 迭代 floor(p × ratio/100),tier 0 = # base_price,floor 单一取整对齐 round_direction 纪律);② 道具工坊解锁(渠道由 meta.tres channel # 字段声明、判定量换算单点 _workshop_progress——2026-09-21 用户拍板可替换轴;首版 = 总通关次数, # 钥石直购价随定价批后补最终双渠道;六件局内效果归 Alpha 魔法道具重设计批,本类只有解锁状态); # ③ 冒险收束入账(Boss 两档 = 表值 first_kill/repeat_kill,首杀标记读存档判first;调 SaveManager # apply_adventure_summary 入账并附 boss_drops 副本——复盘可归因,验收 5)。 # 注入面:hero_override() = 三档 forge flat(全零 = 白板合法态;override 不写表——表运行期只读, # 消费方 AdventureRunner/LevelRunner 经冒险开始参数面显式传参)。 # 纪律:数值零字面量——价格/档位/阈值/两档金额只读 db.meta_* 访问器;存档只经 SaveManager 访问器, # 禁读内部结构;行集解析禁自扫表(一律走 ConfigDB 访问器)。 class_name MetaProgression extends RefCounted var db: ConfigDb var save: SaveManager func _init(db_ref: ConfigDb, save_ref: SaveManager) -> void: db = db_ref save = save_ref func keystone_balance() -> int: return save.keystone_balance() # ---- 属性锻造(钥石直购、可重复升级——几何递增定价)---- func forge_level(attr: String) -> int: return save.forge_level(attr) # 每级强化 flat 值 × 当前级(注入面的算料——投值语言锁定 flat 整数) func forge_flat(attr: String) -> int: return forge_level(attr) * int(db.meta_forge_row(attr).get("flat_value", 0)) # 下一级价格:tier = 当前级,迭代 floor(p × ratio/100) 恰 tier 次(tier 0 = base_price) func forge_price(attr: String) -> int: var row: Dictionary = db.meta_forge_row(attr) if row.is_empty(): return 0 var p := int(row.get("base_price", 0)) var ratio := int(row.get("price_ratio_pct", 100)) for _i in forge_level(attr): p = int(floor(float(p) * float(ratio) / 100.0)) return p # 购买:余额不足 / attr 出域 false 零副作用;成功 = 消费 + 升级 + 写盘 func buy_forge(attr: String) -> bool: if db.meta_forge_row(attr).is_empty(): return false if not save.spend_keystone(forge_price(attr)): return false if not save.set_forge_level(attr, forge_level(attr) + 1): return false return save.write_to_disk() # ---- 道具工坊(首版全走通关次数渠道——钥石直购价随定价批后补,最终双渠道并存)---- # 解锁判定量单点(可替换轴——2026-09-21 用户拍板:解锁条件须抽象、可替换、可扩展,禁与渠道实现耦合)。 # 契约:渠道由 meta.tres workshop_items.channel 声明(缺省 total_clears,词表 MetaTable.WORKSHOP_CHANNELS # 封闭);本方法是渠道 → 判定数值的唯一换算点——换/加渠道 = 词表加值 + 此处增一 match 分支, # 阈值比较(refresh/workshop_status)与解锁写档零改动。候选渠道(语义已预留未实现): # adventure_depth 最高通关深度 / keystone 钥石直购双渠道。 func _workshop_progress(channel: String) -> int: match channel: "total_clears": var total := 0 for i in db.level.rows.size(): total += save.clear_count(i + 1) return total _: return 0 # 未知渠道恒 0 = 永不解锁(fail-safe 非致命——渠道词表校验兜底,不静默误解锁) # 工坊六行只读投影(呈现层数据源):{item_id, display_name, threshold, unlocked, progress} func workshop_status() -> Array: var out: Array[Dictionary] = [] for row in db.meta_workshop_items(): var iid := String(row.get("item_id", "")) var arow: Dictionary = db.aux_item_row(iid) # 迭代 7 T5——失义/退役状态面(呈现「能力封存/已退役」) out.append({ "item_id": iid, "display_name": String(row.get("display_name", "")), "threshold": int(row.get("clear_threshold", 0)), "unlocked": save.item_unlocked(iid), "progress": _workshop_progress(String(row.get("channel", "total_clears"))), "deferred": bool(arow.get("deferred", false)), "retired": bool(arow.get("retired", false)), }) return out # 达标未解锁 → unlock_item + 写盘;返回新解锁 item_id 列(冒险收束后与营地打开时各调一次) func refresh_workshop_unlocks() -> Array: var new_unlocks: Array[String] = [] for row in db.meta_workshop_items(): var iid := String(row.get("item_id", "")) if save.item_unlocked(iid): continue var progress := _workshop_progress(String(row.get("channel", "total_clears"))) if progress >= int(row.get("clear_threshold", 99)): save.unlock_item(iid) new_unlocks.append(iid) if not new_unlocks.is_empty(): save.write_to_disk() return new_unlocks # ---- Boss 两档与收束入账(结算屏文案数据源)---- # 明细 {big_reward, boss_drops: [{level, first, amount}], total, new_unlocks}; # 入账与标记经 save.apply_adventure_summary(boss_drops 副本随摘要入 recent_summaries)。 func settle_adventure(summary: Dictionary) -> Dictionary: # 首次冒险布尔位置位(关卡节点图生成 v0.4——完成/失败/放弃三结局同经本收束挂点,写入只在收束) save.mark_first_adventure_done() var drops: Array[Dictionary] = [] for li in range(1, int(summary.get("levels_cleared", 0)) + 1): var first: bool = not save.boss_first_kill_marked(li) var row: Dictionary = db.meta_boss_keystone_row(li) var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0)) drops.append({"level": li, "first": first, "amount": amount}) save.apply_adventure_summary(summary, drops) var new_unlocks := refresh_workshop_unlocks() save.write_to_disk() var total := int(summary.get("keystone_total", 0)) for d in drops: total += int(d["amount"]) return { "big_reward": int(summary.get("keystone_total", 0)), "boss_drops": drops, "total": total, "new_unlocks": new_unlocks, } # ---- 跨局注入(冒险开始参数面——字段清单随柜台扩展,现 = 属性锻造三档)---- # 全零 = 白板合法态(无解锁不得改变任何局内规则);override 禁写表(表运行期只读)。 func hero_override() -> Dictionary: return { "atk": forge_flat("atk"), "max_hp": forge_flat("max_hp"), "armor_class": forge_flat("armor_class"), }