Files
gplan-2/game/core/meta_progression.gd
T
DKChan 03c4b4c933 docs: 文档正确性审查批+去毒+命名统一+天赋树成文等十一批落籍(2026-10-09 用户令)
一、里程碑语义立规:M3=Alpha 完整实现/M4=Beta 完整实现,未明令关闭 M3 前只排 M3 需求(roadmap M-4 节头立规注+M-3 节头口径指针+迭代 21+ 行注+06 倍率差条注);迭代 20/21/22 规划冻结(roadmap 迭代 20 行注——先解决眼前问题,重启=用户直接发令)
二、文档正确性审查批三波:全 GDD 29 份过审——状态记录按代码证据订正/里程碑删前缀(M-x+迭代N→迭代N,裸 M-4→Beta 阶段(未排期))/退役词退役件当现役清零/在役旧实现节加隔离横幅;重件=秘宝.md v1.7 老正文深层清算(横幅重写消「过渡形态已退役」误读/沿革节换代/六件旧件登记摘除/验收与 charges 矛盾订正)+奇物 v3.8+冒险结构 v0.18+关卡节点地图 v1.7+图生成 v0.7+难度曲线与Boss v0.26+战斗结算 v0.27+敌人系统 v0.16+地图战斗资源 v0.21+增益状态 v0.13+战斗表现 v0.6+元进度 v1.9+生涯 v0.3+00 v1.3+02 v1.16+03 v1.1+ux 八份(battle 23 处/camp/shop/node-reward/level-clear/interaction-patterns/settlement-summary/level-map)
三、去毒批:奇物 v3.7(魔法道具→秘宝/档名三档→四级/门票与刷新「尚未入表」假话订正/验收第 3 条改令牌口径/资源成员指路凭证件表)+秘宝 v1.6(速览旧目录退役标注/退役件示例换现役)
四、道具退役素材库新建(design/research/道具退役素材库.md,案乙已追认):秘宝退役 9 件含去向+专长退役 27 件名单+待核 4 行;魔法提灯(fog_lantern)裁退役(06 条目已裁+秘宝.md 注记换代+素材库迁表一;拆线归实装批)
五、B2/B3/B4 裁决落文:元进度.md 道具工坊行删旧写新(v1.10——旧六行登记作废、指路秘宝新目录+解锁门槛谓词分级);奇物.md P1「两项合成高阶项」注销(v3.9——用户定性脏信息);§3 刷新句按最新口径换代(刷新券替代、不进持有集)
六、天赋树独立成文(B1 裁决):新建 design/gdd/systems/跨局循环/天赋树.md v0.1(33 节点机制全按 meta_tree.tres 实态+迭代 12 落籍裁决+被删底稿 git 取证——脊柱行门槛/钥石单币/Fork A/B 面/存档 v5 域/注入双通道)+元进度.md 定名节换代(v1.11)+02-系统清单跨局循环区加行(v1.17)
七、A 类卫生包:版本历史表理排四份(秘宝/奇物含 v2.7 断裂行修复/敌人系统/图生成,只调行序)+缺号注记三处(增益状态 v0.6 未考/冒险结构 v0.15/关卡节点地图 v1.6=MC-1 批)+node-reward v2.2 双号改 a/b+07 补建版本历史表+06 删「设置与本地化」空节+「文档/结构闸存量欠账」块挪文末「文档与结构」节+「L1 商店编排」两悬空指针改指 GP 换算失衡条+难度曲线两处过期指向核销注记(v0.27)+B5「现役 UI/文案两处自相冲突」登记 06 待办(battle_narrator.gd:160 旧话/hero_info_panel 两段同标「特性」——随后续迭代修)
八、命名统一(用户令「任务书」全库改「需求卡片」):167 文件约 800 处(design/ 全部文档+roadmap+CLAUDE.md+active.md+entities.yaml+game/ 注释与数据批注);两文件改名(需求卡片-M4数值批/需求卡片-冒险前资源配比预设批);残留=自造短语全量清单(黑话证据文档故意跳过)
九、CLAUDE.md:批次工作流标准清单收口段新增第 12 条「文档同步闸」(需求卡片必列关联文档同步面、合并前同步完毕方算收口,原 12/13 顺延);呈报六问铁规适用范围补立(前序批随批落籍)
十、active.md 快照节+工作日志 414-421(并行会话 416 黑话清扫条目按共享工作日志惯例一并入库);06 中并行会话「active.md 定性与裁剪」条目未提交,按零吞并纪律留在工作区未入库

零测试(文档批两入口铁规);主区前序脏件(.gitignore/.zcode/config.json/playnote/project.godot/.zcodeignore/美术审计报告 html 等)未带
2026-10-09 19:12:46 +08:00

407 lines
17 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.treasure_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 禁写表(表运行期只读)。
# 迭代 12 T8:树四线 flat 并入(脊柱 stat+锻造脉+彩蛋——同一覆盖值通道;hit 线消费面=
# 命中折扣档归数值批,本批不入 override 三字段通道——披露)。
func hero_override() -> Dictionary:
return {
"atk": forge_flat("atk") + tree_stat_flat("atk"),
"max_hp": forge_flat("max_hp") + tree_stat_flat("max_hp"),
"armor_class": forge_flat("armor_class") + tree_stat_flat("armor_class"),
}
# ---- 天赋树段(迭代 12 T8——R1 甲融合案;设计权威 整合定稿 §1.4 一树双币三脉一脊)----
# 全树已购总等级(row-gating 输入——「投入点数累计解锁新行」)
func tree_spent_total() -> int:
var total := 0
for lvl in (save.tree_nodes() as Dictionary).values():
total += int(lvl)
return total
# 节点可购校验(row-gating + 显式前置 + 币 + cap;free 节点=迁移/彩蛋不经购买)
func tree_can_buy(node_id: String) -> bool:
var row: Dictionary = db.tree_node(node_id)
if row.is_empty():
return false
var lvl := save.tree_level(node_id)
if lvl >= int(row.get("cap", 1)):
return false
var row_num := int(row.get("row", 0))
if row_num > 0 and tree_spent_total() < db.meta_tree.row_gate(row_num):
return false
for req in (row.get("requires", []) as Array):
if save.tree_level(String(req)) < 1:
return false
var fg := String(row.get("fork_group", ""))
if fg != "" and save.fork_side(fg) != "":
return false # 同组已激活一侧——互斥(切另侧走 tree_toggle_fork 免费切换)
return true
# 节点购买(营地树 UI 唯一写口):树全钥石单币(2026-10-07 用户裁甲——原 T8 双币骨架披露销账;
# 表内 currency 字段保留为呈现元数据)。free=迁移点亮/彩蛋觉醒零币(price 0,零额消费无副作用)。
func tree_buy(node_id: String) -> bool:
var row: Dictionary = db.tree_node(node_id)
if row.is_empty() or not tree_can_buy(node_id):
return false
var price := int(row.get("price", 0))
if not save.spend_keystone(price):
return false
save.set_tree_level(node_id, save.tree_level(node_id) + 1)
_check_easter_egg()
return save.write_to_disk()
# Fork 激活/切换(营地树 UI——同组换侧零币=免费切换裁定;未激活组首次激活=消费购买面 tree_buy
# 已购该侧后此处激活;同侧重复=零操作)
func tree_toggle_fork(group: String, side: String) -> bool:
if not db.meta_tree.FORK_GROUPS.has(group) or not (side == "a" or side == "b"):
return false
var cur := save.fork_side(group)
if cur == side:
return false # 同侧零操作
if cur == "":
return false # 未激活组走购买面(tree_buy 激活侧节点后由激活写入——见下)
save.set_fork_side(group, side)
return save.write_to_disk()
# Fork 侧节点购买入口(购节点即激活该侧——组内互斥由 can_buy 拦截另侧)
func tree_buy_fork(node_id: String) -> bool:
var row: Dictionary = db.tree_node(node_id)
if row.is_empty() or String(row.get("kind", "")) != "fork":
return tree_buy(node_id)
if not tree_can_buy(node_id):
return false
var price := int(row.get("price", 0))
if not save.spend_keystone(price):
return false
save.set_tree_level(node_id, 1)
save.set_fork_side(String(row.get("fork_group", "")), String(row.get("fork_side", "")))
return save.write_to_disk()
# 树效果投影:四线 flat 汇总 + rule 键集(激活 Fork 侧 effects 并入)
func tree_stat_flat(key: String) -> int:
var total := 0
for row in db.tree_rows():
var lvl := save.tree_level(String(row.get("node_id", "")))
if lvl <= 0:
continue
total += int((row.get("effects", {}) as Dictionary).get(key, 0)) * lvl
return total
func tree_rule_keys() -> Array:
var out: Array = []
var sides: Dictionary = save.fork_sides()
for row in db.tree_rows():
if String(row.get("kind", "")) != "fork":
continue
var fg := String(row.get("fork_group", ""))
if String(sides.get(fg, "")) != String(row.get("fork_side", "")):
continue
for k in (row.get("effects", {}) as Dictionary).keys():
if not out.has(String(k)):
out.append(String(k))
return out
# 营地树面投影(CampOverlay _rebuild_tree 数据源——行板只读;账面归本类、拼句归视图同款分工)
func tree_camp_rows() -> Array:
var out: Array = []
for row in db.tree_rows():
if String(row.get("kind", "")) == "fork":
continue # Fork 行由 fork_groups 段承载
out.append({
"node_id": String(row.get("node_id", "")),
"display_name": String(row.get("display_name", "")),
"vein": String(row.get("vein", "")),
"kind": String(row.get("kind", "")),
"cap": int(row.get("cap", 1)),
"level": save.tree_level(String(row.get("node_id", ""))),
"price": int(row.get("price", 0)),
"currency": String(row.get("currency", "gp")),
"can_buy": tree_can_buy(String(row.get("node_id", ""))),
})
return out
func tree_fork_groups() -> Array:
var out: Array = []
var sides: Dictionary = save.fork_sides()
for fg in db.meta_tree.FORK_GROUPS:
var group_rows: Array = []
for row in db.tree_rows():
if String(row.get("fork_group", "")) == String(fg):
group_rows.append(row)
if group_rows.is_empty():
continue
var sides_out: Array = []
for row in group_rows:
sides_out.append({
"side": String(row.get("fork_side", "")),
"node_id": String(row.get("node_id", "")),
"display_name": String(row.get("display_name", "")),
"owned": save.tree_level(String(row.get("node_id", ""))) > 0,
})
out.append({
"group": String(fg),
"display_name": String(group_rows[0].get("display_name", String(fg))).split("·")[0],
"active_side": String(sides.get(String(fg), "")),
"sides": sides_out,
})
return out
# 彩蛋觉醒检查(锻造脉三节点全满→easter_ley 免费点亮——发现率监控 T10 呈报)
func _check_easter_egg() -> void:
var row: Dictionary = db.tree_node("easter_ley")
if row.is_empty() or save.tree_level("easter_ley") > 0:
return
for req in (row.get("requires", []) as Array):
var req_row: Dictionary = db.tree_node(String(req))
if save.tree_level(String(req)) < int(req_row.get("cap", 1)):
return
save.set_tree_level("easter_ley", 1)
# 三体系迁移(迭代 12 T9——v4→v5 旧域只读→树域点亮;幂等=重复迁移零变化):
# forge_levels 等级→锻造脉节点树级原样映射(钥石付价不变零亏损——「宁多勿少」以零亏损满足);
# item_unlocks 任一真→ks_workshop 点亮;同调槽位解锁态→socket 点亮(谓词表 Slots 求值≥i)。
func migrate_tree() -> Dictionary:
var migrated := {"forge": 0, "unlock": 0, "socket": 0}
for pair in [["forge_atk", "atk"], ["forge_hp", "max_hp"], ["forge_ac", "armor_class"]]:
var lvl := save.forge_level(String(pair[1]))
if lvl > 0 and save.tree_level(String(pair[0])) < lvl:
save.set_tree_level(String(pair[0]), lvl)
migrated["forge"] += 1
var any_unlock := false
for iid in db.treasure_live_ids().keys():
if save.item_unlocked(String(iid)):
any_unlock = true
break
if not any_unlock:
for row in db.treasure_attunement_rows():
if save.item_unlocked(String(row.get("item_id", ""))):
any_unlock = true
break
if any_unlock and save.tree_level("ks_workshop") < 1:
save.set_tree_level("ks_workshop", 1)
migrated["unlock"] += 1
var slots := Attunement.slots(db, save)
for i in range(1, slots + 1):
if save.tree_level("att_socket%d" % i) < 1:
save.set_tree_level("att_socket%d" % i, 1)
migrated["socket"] += 1
save.write_to_disk()
return migrated
# ---- 同调位账本(迭代 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.treasure_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.treasure_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()