- 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 令)
215 lines
9.6 KiB
GDScript
215 lines
9.6 KiB
GDScript
# 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()
|