Files
gplan-2/game/core/meta_progression.gd
T
DKChan a6a2bf885e 迭代11第①批:魔法道具同调实施批 T0-T10 全落(重闸 645/0 + MC off 逐位 PASS)
- T1 表与schema:attunement_growth.tres 谓词表(m1/m2/m3+Σgrant≤slot_cap 校验+slots() 求值口径)+道具表扩列(form/charges/slot_cost/unlock_requires/effective_rounds/charge_n+effect_kinds 九kind+KEY_WHITELIST 零裸flat护栏)+七同调行
- T2 save v4(boss_kill_count 收束入账+attunement_equipped+attune_refresh_level+断点 attunement_charges+迁移链 v3→v4)
- T3 Attunement 求值服务(纯函数零 await 零 RNG)
- T4 营地装配覆盖层(工坊扩容程序占位+Y 升级行 attune_refresh_upgrade 行族)
- T5 被动六件接线+装配注入链+刷新 X/Y 双源(BonusPickOverlay 刷新钮,autoplay 默认不刷)
- T6 掘进蓄能全链+摆钟能力面(骑库存面差额注入+断点充能键);炸格 P5 甲影响评估破面大→用户裁拆后批,blast_kit deferred 留位
- T7 退役面五件(BONUS_IDS 除名 aux_item+transitional_sync_slots 过渡键退役=迭代10欠账③清偿+授予路/四容量闸拆除+nest_pendulum 换装 active_charged)
- T8 冒烟六新断言(645/0)+MC attune 开关+贪心画像分层 off/on 各 N=60 双策略(on 画像功率显著带平移归调参批;off 新锚=对拍正主留盘主区 sim-data,81000 降批入口基准)
- T9 连带修订九件(魔法道具定位重写/元进度v1.6/地图战斗资源v0.16/行动经济v0.7/05不做v1.3/关卡节点地图v1.4/node-reward v2.2/registry 三处/06 收口+炸格拆后批新条)
- T10 重闸全绿:冒烟 645/0+熵增 PASS+doc 三闸零新增+MC off 复跑同种子逐位一致 PASS;出口人闸后移迭代 12(用户 2026-10-03 令)
2026-10-03 15:41:30 +08:00

215 lines
9.6 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: 钥石能否在营地换成永久强化并在下一局开局生效——冒险收束入账(大奖励 + 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_count(): # 表访问收口(2026-10-02——行集直扫退役)
total += save.clear_count(i + 1)
return total
_:
return 0 # 未知渠道恒 0 = 永不解锁(fail-safe 非致命——渠道词表校验兜底,不静默误解锁)
# 道具解锁读数访问器(组合根/呈现层解锁门控查询面——禁再穿透 meta.save 直读 save_manager)
func is_item_unlocked(item_id: String) -> bool:
return save.item_unlocked(item_id)
# 工坊六行只读投影(呈现层数据源):{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, total, new_unlocks};入账与标记经 save.apply_adventure_summary
# (keystone_drops 副本随摘要入 recent_summaries)。
# 迭代 10 奖励重构批:钥石改 Boss 逐杀掉落——掉落金额已在冒险内逐杀入账(summary.keystone_total
# 含全部掉落;首杀/复杀裁决在 AdventureRunner._boss_keystone_drop,首杀标记仍在下方
# apply_adventure_summary 逐通关关次置位)。本函数不再派生掉落金额,只做标记+入账+归档。
func settle_adventure(summary: Dictionary) -> Dictionary:
# 首次冒险布尔位置位(关卡节点图生成 v0.4——完成/失败/放弃三结局同经本收束挂点,写入只在收束)
save.mark_first_adventure_done()
var drops: Array = (summary.get("keystone_drops", []) as Array).duplicate(true)
save.apply_adventure_summary(summary, drops)
var new_unlocks := refresh_workshop_unlocks()
save.write_to_disk()
var total := int(summary.get("keystone_total", 0))
return {
"big_reward": total,
"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"),
}
# ---- 同调位账本(迭代 11 第①批 T4——装配关系/同调位求值/Y 刷新升级;设计权威 proposal §2 + 任务书 P1 甲)----
# 装配切换(营地装配覆盖层唯一写口):已装 → 卸下恒可;未装 → 非同调行拒绝 + 满位禁选;
# 装配关系写存档持久(冒险中途不可换装——底稿 §8.5,本口只服务营地)。
func toggle_attunement(item_id: String) -> bool:
var equipped: Array = save.attunement_equipped()
if not equipped.has(item_id):
var arow: Dictionary = db.aux_item_row(item_id)
if arow.is_empty() or String(arow.get("form", "")) == "":
return false # 非同调行(过渡/退役行)禁装配
if equipped.size() >= Attunement.slots(db, save):
return false # 满位禁选
equipped.append(item_id)
else:
equipped.erase(item_id)
save.set_attunement_equipped(equipped)
return save.write_to_disk()
# 营地装配面投影(账面归本类、拼句归 CampOverlay——_camp_data 同款分工)
func attune_camp_status() -> Dictionary:
var equipped: Array = save.attunement_equipped()
var items: Array[Dictionary] = []
for row in db.aux_attunement_rows():
var iid := String(row.get("item_id", ""))
items.append({
"item_id": iid,
"display_name": String(row.get("display_name", "")),
"form": String(row.get("form", "")),
"equipped": equipped.has(iid),
})
var lvl := save.attune_refresh_level()
var next_row: Dictionary = db.meta_attune_refresh_row(lvl + 1)
return {
"slots": Attunement.slots(db, save),
"equipped": equipped,
"items": items,
"refresh": {
"level": lvl,
"charges": attune_refresh_charges(),
"price": int(next_row.get("keystone_cost", 0)),
"maxed": next_row.is_empty(),
},
}
# 每冒险开局刷新次数 Y(当前等级行表值;0 级 = 0 次即无刷新能力——P1 甲初始 0 同构)
func attune_refresh_charges() -> int:
return int(db.meta_attune_refresh_row(save.attune_refresh_level()).get("refresh_charges", 0))
# Y 升级购买(直购价 = 下一级行表值 keystone_cost;已满级/余额不足 false 零副作用——buy_forge 同款)
func buy_attune_refresh() -> bool:
var next_row: Dictionary = db.meta_attune_refresh_row(save.attune_refresh_level() + 1)
if next_row.is_empty():
return false
if not save.spend_keystone(int(next_row.get("keystone_cost", 0))):
return false
save.set_attune_refresh_level(save.attune_refresh_level() + 1)
return save.write_to_disk()