feat(content): 元进度实现批 T2–T4——MetaProgression 账本服务+SaveManager 两档入账第二参+注入通道(adventure/level runner base_override)+营地覆盖层/入口/顶栏/结算明细(冒烟 522 绿)
This commit is contained in:
1 parent
5c0efa934e
commit
2e419a17a3
11 files changed
+501
-34
No files matched your search
@@ -55,6 +55,7 @@ var summary := {} # 最近一次结算的冒险摘要(
|
|||||||
var _runner: LevelRunner # 当前关卡行进器(每关新建——关卡域状态天然隔离)
|
var _runner: LevelRunner # 当前关卡行进器(每关新建——关卡域状态天然隔离)
|
||||||
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
|
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
|
||||||
var _override := [0, 0] # TunePanel 百分比通道 [atk_pct, hp_pct](每关新 runner 都要应用)
|
var _override := [0, 0] # TunePanel 百分比通道 [atk_pct, hp_pct](每关新 runner 都要应用)
|
||||||
|
var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板)
|
||||||
|
|
||||||
|
|
||||||
func _init(db_ref, rng_ref) -> void:
|
func _init(db_ref, rng_ref) -> void:
|
||||||
@@ -65,8 +66,10 @@ func _init(db_ref, rng_ref) -> void:
|
|||||||
# ---- 冒险生命周期 ----
|
# ---- 冒险生命周期 ----
|
||||||
|
|
||||||
# 冒险开始(主种子注入恰一次——全冒险唯一 rng.new_adventure 调用点;重开 = 全新一轮:
|
# 冒险开始(主种子注入恰一次——全冒险唯一 rng.new_adventure 调用点;重开 = 全新一轮:
|
||||||
# 账本/加算区/已购档/钥石/摘要全重置,生命置满基础上限,从第 1 关起步)
|
# 账本/加算区/已购档/钥石/摘要全重置,生命置满基础上限,从第 1 关起步)。
|
||||||
func start_adventure(seed_val: int) -> bool:
|
# params.hero_override = 永久强化 flat 注入(元进度实现批——缺省 {} = 全零白板,与 B1 空参数路径逐位一致;
|
||||||
|
# -1/缺键 = 表值兜底口径,规范化为 0 flat)
|
||||||
|
func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||||
if state != "idle":
|
if state != "idle":
|
||||||
push_error("[冒险结构] start_adventure 拒绝——state=%s(重开须待上一局结算完毕)" % state)
|
push_error("[冒险结构] start_adventure 拒绝——state=%s(重开须待上一局结算完毕)" % state)
|
||||||
return false
|
return false
|
||||||
@@ -80,11 +83,20 @@ func start_adventure(seed_val: int) -> bool:
|
|||||||
levels_total = db.level.rows.size()
|
levels_total = db.level.rows.size()
|
||||||
level_index = 1
|
level_index = 1
|
||||||
_pending_next_level = false
|
_pending_next_level = false
|
||||||
ledger["hero_hp"] = int(db.hero.max_hp) # 生命账本置满(基础上限读英雄表——加算区刚归零)
|
_base_override = _normalize_override(params.get("hero_override", {}))
|
||||||
|
ledger["hero_hp"] = int(db.hero.max_hp) + int(_base_override["max_hp"]) # 生命账本置满(表值+永久强化 flat——加算区刚归零)
|
||||||
state = "running"
|
state = "running"
|
||||||
return _start_current_level()
|
return _start_current_level()
|
||||||
|
|
||||||
|
|
||||||
|
# 注入参数规范化:三字段缺键/-1 = 表值兜底(→ 0 flat);负值一律 clamp 0(白板 = 全零合法态)
|
||||||
|
func _normalize_override(raw: Dictionary) -> Dictionary:
|
||||||
|
var out := {"atk": 0, "max_hp": 0, "armor_class": 0}
|
||||||
|
for k in out.keys():
|
||||||
|
out[k] = maxi(0, int(raw.get(k, 0)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# TunePanel 百分比通道(测试面板覆盖——每关新 runner 都要应用;正式侧归元进度)
|
# TunePanel 百分比通道(测试面板覆盖——每关新 runner 都要应用;正式侧归元进度)
|
||||||
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
||||||
_override = [atk_pct, hp_pct]
|
_override = [atk_pct, hp_pct]
|
||||||
@@ -139,9 +151,10 @@ func _start_current_level() -> bool:
|
|||||||
_runner.wallet_changed.connect(_forward_wallet_changed)
|
_runner.wallet_changed.connect(_forward_wallet_changed)
|
||||||
_runner.level_finished.connect(_on_level_finished)
|
_runner.level_finished.connect(_on_level_finished)
|
||||||
_runner.set_hero_override(int(_override[0]), int(_override[1]))
|
_runner.set_hero_override(int(_override[0]), int(_override[1]))
|
||||||
# 接口①开始关卡:生命经共享账本交接(账本当前值即开局生命——冒险开始置满 / 大奖励回满已写好)
|
# 接口①开始关卡:生命经共享账本交接(账本当前值即开局生命——冒险开始置满 / 大奖励回满已写好);
|
||||||
|
# _base_override 共享引用随载荷传入(永久强化 flat——LevelRunner.effective_* 叠加序第二项)
|
||||||
var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased,
|
var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased,
|
||||||
Callable(self, "apply_rest"))
|
Callable(self, "apply_rest"), _base_override)
|
||||||
if ok:
|
if ok:
|
||||||
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
|
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
|
||||||
return ok
|
return ok
|
||||||
@@ -164,14 +177,14 @@ func _on_level_finished(payload: Dictionary) -> void:
|
|||||||
# 大奖励(非末关通关恰一次):上限提升写加算区 → 血回满 = 新上限 → 钥石入账(顺序即契约)
|
# 大奖励(非末关通关恰一次):上限提升写加算区 → 血回满 = 新上限 → 钥石入账(顺序即契约)
|
||||||
var hp_bonus: int = int(db.adventure.level_clear_hp_bonus)
|
var hp_bonus: int = int(db.adventure.level_clear_hp_bonus)
|
||||||
bonus["max_hp"] = int(bonus.get("max_hp", 0)) + hp_bonus
|
bonus["max_hp"] = int(bonus.get("max_hp", 0)) + hp_bonus
|
||||||
ledger["hero_hp"] = int(db.hero.max_hp) + int(bonus["max_hp"])
|
ledger["hero_hp"] = int(db.hero.max_hp) + int(_base_override["max_hp"]) + int(bonus["max_hp"])
|
||||||
keystone += int(db.adventure.keystone_per_level)
|
keystone += int(db.adventure.keystone_per_level)
|
||||||
# 置位先于 emit(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位)
|
# 置位先于 emit(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位)
|
||||||
_pending_next_level = true
|
_pending_next_level = true
|
||||||
big_reward_paid.emit({
|
big_reward_paid.emit({
|
||||||
"from_level": level_index,
|
"from_level": level_index,
|
||||||
"hp_bonus": hp_bonus,
|
"hp_bonus": hp_bonus,
|
||||||
"new_max_hp": int(db.hero.max_hp) + int(bonus["max_hp"]),
|
"new_max_hp": int(db.hero.max_hp) + int(_base_override["max_hp"]) + int(bonus["max_hp"]),
|
||||||
"keystone_gained": int(db.adventure.keystone_per_level),
|
"keystone_gained": int(db.adventure.keystone_per_level),
|
||||||
"keystone_total": keystone,
|
"keystone_total": keystone,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ var gp: int: # GP 冒险钱包(局内货币—
|
|||||||
get: return int(_ledger["gp"])
|
get: return int(_ledger["gp"])
|
||||||
set(v): _ledger["gp"] = v
|
set(v): _ledger["gp"] = v
|
||||||
var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0} # 统一冒险加算区(冒险开始全 0)——商店购买与关卡大奖励写同一本账(冒险模式 = AdventureRunner 共享字典;大奖励接线归 AdventureRunner._on_level_finished)
|
var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0} # 统一冒险加算区(冒险开始全 0)——商店购买与关卡大奖励写同一本账(冒险模式 = AdventureRunner 共享字典;大奖励接线归 AdventureRunner._on_level_finished)
|
||||||
|
var base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 注入(元进度实现批——冒险模式 = AdventureRunner 共享字典经 start_level_external 注入;单关测试模式 = 私有全零)
|
||||||
var _shop_purchased := {} # product_id → 已购档数(现价重算输入;冒险开始清零)
|
var _shop_purchased := {} # product_id → 已购档数(现价重算输入;冒险开始清零)
|
||||||
# 协作对象私有(E1/E3 封装收口 2026-09-13):settlement 读取经 current_battle() 成文出口
|
# 协作对象私有(E1/E3 封装收口 2026-09-13):settlement 读取经 current_battle() 成文出口
|
||||||
var _settlement = null # 当前战斗编排器(关卡结束置 null)
|
var _settlement = null # 当前战斗编排器(关卡结束置 null)
|
||||||
@@ -84,21 +85,21 @@ func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
|||||||
hero_hp_pct = hp_pct
|
hero_hp_pct = hp_pct
|
||||||
|
|
||||||
|
|
||||||
# 叠加次序(GP+商店房实施批):基础表值 + 加算区(商店/大奖励 flat 先加)→ TunePanel 百分比后乘
|
# 叠加次序(元进度实现批钉死):表值 → 永久强化 flat(base_override)→ 加算区 flat(bonus)先加
|
||||||
# (调整板 = 测试工具按现状保持——百分比恒乘在加算生效值上)
|
# → TunePanel 百分比后乘(调整板 = 测试工具恒乘最后——代拍板 6,与 GP 批既有次序兼容)
|
||||||
func effective_atk() -> int:
|
func effective_atk() -> int:
|
||||||
var base: int = int(db.hero.atk) + int(bonus.get("atk", 0))
|
var base: int = int(db.hero.atk) + int(base_override.get("atk", 0)) + int(bonus.get("atk", 0))
|
||||||
return maxi(1, int(round(float(base) * (100.0 + float(hero_atk_pct)) / 100.0)))
|
return maxi(1, int(round(float(base) * (100.0 + float(hero_atk_pct)) / 100.0)))
|
||||||
|
|
||||||
|
|
||||||
func effective_max_hp() -> int:
|
func effective_max_hp() -> int:
|
||||||
var base: int = int(db.hero.max_hp) + int(bonus.get("max_hp", 0))
|
var base: int = int(db.hero.max_hp) + int(base_override.get("max_hp", 0)) + int(bonus.get("max_hp", 0))
|
||||||
return maxi(1, int(round(float(base) * (100.0 + float(hero_hp_pct)) / 100.0)))
|
return maxi(1, int(round(float(base) * (100.0 + float(hero_hp_pct)) / 100.0)))
|
||||||
|
|
||||||
|
|
||||||
# 护甲等级生效值(加算区 armor_class 轴——无 TunePanel 百分比通道,纯 flat)
|
# 护甲等级生效值(永久强化 flat + 加算区 armor_class 轴——无 TunePanel 百分比通道,纯 flat)
|
||||||
func effective_ac() -> int:
|
func effective_ac() -> int:
|
||||||
return maxi(0, int(db.hero.armor_class) + int(bonus.get("armor_class", 0)))
|
return maxi(0, int(db.hero.armor_class) + int(base_override.get("armor_class", 0)) + int(bonus.get("armor_class", 0)))
|
||||||
|
|
||||||
|
|
||||||
# 覆盖复位(关卡结束归零——表值不可变,仅清百分比;调用方兼容保留)
|
# 覆盖复位(关卡结束归零——表值不可变,仅清百分比;调用方兼容保留)
|
||||||
@@ -128,6 +129,7 @@ func start_level(level_index: int, seed_val: int) -> bool:
|
|||||||
_graph_mode = false
|
_graph_mode = false
|
||||||
gp = 0 # 冒险钱包清零开局(GP+商店房实施批——局内货币生命周期起点)
|
gp = 0 # 冒险钱包清零开局(GP+商店房实施批——局内货币生命周期起点)
|
||||||
bonus = {"atk": 0, "max_hp": 0, "armor_class": 0} # 加算区冒险开始全 0
|
bonus = {"atk": 0, "max_hp": 0, "armor_class": 0} # 加算区冒险开始全 0
|
||||||
|
base_override = {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 单关测试模式恒全零(白板——单关入口零元进度)
|
||||||
_shop_purchased = {} # 已购档数清零(跨冒险不延续——递增定价按冒险内累计)
|
_shop_purchased = {} # 已购档数清零(跨冒险不延续——递增定价按冒险内累计)
|
||||||
for node in nodes():
|
for node in nodes():
|
||||||
if node.has("next"):
|
if node.has("next"):
|
||||||
@@ -146,11 +148,12 @@ func start_level(level_index: int, seed_val: int) -> bool:
|
|||||||
|
|
||||||
# 冒险结构接口①:开始关卡(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接;
|
# 冒险结构接口①:开始关卡(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接;
|
||||||
# 种子域归冒险结构:本路径不重播种;账本归冒险结构:本路径不重置 gp/bonus/_shop_purchased,
|
# 种子域归冒险结构:本路径不重播种;账本归冒险结构:本路径不重置 gp/bonus/_shop_purchased,
|
||||||
# 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在 ledger 写好)
|
# 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在 ledger 写好)。
|
||||||
|
# base_override = 永久强化 flat 共享引用(元进度实现批——缺省 {} 全零白板,与 B1 空参数路径逐位一致)。
|
||||||
# 流程与 start_level 同款(_find_level_row → 状态初始化 → _graph_mode 探测 → _enter_node(entry)),
|
# 流程与 start_level 同款(_find_level_row → 状态初始化 → _graph_mode 探测 → _enter_node(entry)),
|
||||||
# 差异仅在账本/加算区/已购档/恢复执行体的注入与零重置。
|
# 差异仅在账本/加算区/已购档/恢复执行体的注入与零重置。
|
||||||
func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dictionary,
|
func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dictionary,
|
||||||
purchased_ref: Dictionary, rest_handler: Callable) -> bool:
|
purchased_ref: Dictionary, rest_handler: Callable, base_override_ref: Dictionary = {}) -> bool:
|
||||||
var row := _find_level_row(level_index)
|
var row := _find_level_row(level_index)
|
||||||
if row.is_empty():
|
if row.is_empty():
|
||||||
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
|
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
|
||||||
@@ -158,6 +161,7 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti
|
|||||||
_external = true
|
_external = true
|
||||||
_ledger = ledger
|
_ledger = ledger
|
||||||
bonus = bonus_ref
|
bonus = bonus_ref
|
||||||
|
base_override = base_override_ref
|
||||||
_shop_purchased = purchased_ref
|
_shop_purchased = purchased_ref
|
||||||
_rest_handler = rest_handler
|
_rest_handler = rest_handler
|
||||||
level_row = row
|
level_row = row
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# 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 纪律);② 道具工坊解锁(首版只挂总通关次数渠道——
|
||||||
|
# Σ level_clear_counts 达标,代拍板 2;六件局内效果归 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
|
||||||
|
var save
|
||||||
|
|
||||||
|
|
||||||
|
func _init(db_ref, save_ref) -> 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()
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 道具工坊(首版全走通关次数渠道——钥石直购价随定价批后补,最终双渠道并存)----
|
||||||
|
|
||||||
|
# 总通关次数 = Σ level_clear_counts(工坊解锁判定量——代拍板 2)
|
||||||
|
func _total_clears() -> int:
|
||||||
|
var total := 0
|
||||||
|
for i in db.level.rows.size():
|
||||||
|
total += save.clear_count(i + 1)
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
# 工坊六行只读投影(呈现层数据源):{item_id, display_name, threshold, unlocked, progress}
|
||||||
|
func workshop_status() -> Array:
|
||||||
|
var total := _total_clears()
|
||||||
|
var out: Array = []
|
||||||
|
for row in db.meta_workshop_items():
|
||||||
|
var iid := String(row.get("item_id", ""))
|
||||||
|
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": total,
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# 达标未解锁 → unlock_item + 写盘;返回新解锁 item_id 列(冒险收束后与营地打开时各调一次)
|
||||||
|
func refresh_workshop_unlocks() -> Array:
|
||||||
|
var new_unlocks: Array = []
|
||||||
|
var total := _total_clears()
|
||||||
|
for row in db.meta_workshop_items():
|
||||||
|
var iid := String(row.get("item_id", ""))
|
||||||
|
if save.item_unlocked(iid):
|
||||||
|
continue
|
||||||
|
if total >= 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:
|
||||||
|
var drops: Array = []
|
||||||
|
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"),
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b22wboiq6n3ra
|
||||||
@@ -171,17 +171,23 @@ func recent_summaries() -> Array:
|
|||||||
|
|
||||||
|
|
||||||
# 域更新原语(正式冒险收束唯一入账口——组合根经本函数入账,禁直改内部结构):
|
# 域更新原语(正式冒险收束唯一入账口——组合根经本函数入账,禁直改内部结构):
|
||||||
# 钥石入账 + 逐通关关次 clear_count +1 与 Boss 首杀标记 + 摘要头部插入(cap 10,最新在前)。
|
# 钥石入账(summary.keystone_total + boss_drops 逐笔 amount)+ 逐通关关次 clear_count +1 与
|
||||||
# 本批不含 Boss 金额(首杀全量/重复杀小额归元进度批——届时增第二参 boss_drops,默认 [] 零行为兼容)。
|
# Boss 首杀标记 + 摘要头部插入(cap 10,最新在前,元素附 boss_drops 副本——复盘可归因)。
|
||||||
func apply_adventure_summary(summary: Dictionary) -> void:
|
# boss_drops 归元进度批消费(两档金额读 meta 表——MetaProgression.settle_adventure 组装;
|
||||||
|
# 缺省 [] 零行为兼容——调用方不带两档时入账面与批前逐位一致)。
|
||||||
|
func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> void:
|
||||||
add_keystone(int(summary.get("keystone_total", 0)))
|
add_keystone(int(summary.get("keystone_total", 0)))
|
||||||
|
for drop in boss_drops:
|
||||||
|
add_keystone(int(drop.get("amount", 0)))
|
||||||
var cleared := int(summary.get("levels_cleared", 0))
|
var cleared := int(summary.get("levels_cleared", 0))
|
||||||
for li in range(1, cleared + 1):
|
for li in range(1, cleared + 1):
|
||||||
var key := str(li)
|
var key := str(li)
|
||||||
_data["meta"]["level_clear_counts"][key] = int(_data["meta"]["level_clear_counts"].get(key, 0)) + 1
|
_data["meta"]["level_clear_counts"][key] = int(_data["meta"]["level_clear_counts"].get(key, 0)) + 1
|
||||||
_data["meta"]["boss_first_kill"][key] = true
|
_data["meta"]["boss_first_kill"][key] = true
|
||||||
|
var entry: Dictionary = summary.duplicate(true)
|
||||||
|
entry["boss_drops"] = boss_drops.duplicate(true)
|
||||||
var arr: Array = _data["meta"]["recent_summaries"]
|
var arr: Array = _data["meta"]["recent_summaries"]
|
||||||
arr.push_front(summary.duplicate(true))
|
arr.push_front(entry)
|
||||||
while arr.size() > SUMMARY_CAP:
|
while arr.size() > SUMMARY_CAP:
|
||||||
arr.pop_back()
|
arr.pop_back()
|
||||||
|
|
||||||
|
|||||||
@@ -102,3 +102,20 @@ static func adventure_summary_text(summary: Dictionary) -> String:
|
|||||||
return "%s\n通关 %d 关 · 止步第 %d 层 · 钥石合计 %d · 主种子 %d" % [
|
return "%s\n通关 %d 关 · 止步第 %d 层 · 钥石合计 %d · 主种子 %d" % [
|
||||||
verdict, int(summary.get("levels_cleared", 0)), int(summary.get("stopped_at", 0)),
|
verdict, int(summary.get("levels_cleared", 0)), int(summary.get("stopped_at", 0)),
|
||||||
int(summary.get("keystone_total", 0)), int(summary.get("master_seed", 0))]
|
int(summary.get("keystone_total", 0)), int(summary.get("master_seed", 0))]
|
||||||
|
|
||||||
|
|
||||||
|
# 钥石入账明细文案(元进度实现批——settle_adventure 明细的结算屏追加段:大奖励 / 逐关 Boss
|
||||||
|
# 首杀或重复两档 / 新解锁道具;name_lookup = item_id → 显示名投影,由组合根自 meta 表喂入)
|
||||||
|
static func keystone_settle_text(detail: Dictionary, name_lookup: Dictionary = {}) -> String:
|
||||||
|
var parts: Array = ["大奖励 +%d" % int(detail.get("big_reward", 0))]
|
||||||
|
for d in detail.get("boss_drops", []):
|
||||||
|
parts.append("第 %d 关 Boss %s +%d" % [int(d.get("level", 0)),
|
||||||
|
"首杀" if bool(d.get("first", false)) else "重复杀", int(d.get("amount", 0))])
|
||||||
|
var line := "钥石入账:" + " · ".join(parts)
|
||||||
|
var unlocks: Array = detail.get("new_unlocks", [])
|
||||||
|
if not unlocks.is_empty():
|
||||||
|
var names: Array = []
|
||||||
|
for iid in unlocks:
|
||||||
|
names.append(String(name_lookup.get(String(iid), String(iid))))
|
||||||
|
line += "\n工坊新解锁:" + "、".join(names)
|
||||||
|
return line + "\n本局入账合计 +%d 钥石" % int(detail.get("total", 0))
|
||||||
@@ -16,10 +16,14 @@ extends RefCounted
|
|||||||
|
|
||||||
# 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配)
|
# 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配)
|
||||||
static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView,
|
static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView,
|
||||||
settlement, runner, level_active: bool, db, refresh_remaining: int, gp_label: Label = null) -> void:
|
settlement, runner, level_active: bool, db, refresh_remaining: int, gp_label: Label = null,
|
||||||
|
keystone_label: Label = null, keystone_balance: int = 0) -> void:
|
||||||
# GP 余额常驻(GP+商店房实施批 2026-09-21——A 区顶栏邻接位;runner 缺位 = 非冒险态按 0 读数)
|
# GP 余额常驻(GP+商店房实施批 2026-09-21——A 区顶栏邻接位;runner 缺位 = 非冒险态按 0 读数)
|
||||||
if gp_label != null:
|
if gp_label != null:
|
||||||
gp_label.text = "金币 %d" % (int(runner.gp) if runner != null else 0)
|
gp_label.text = "金币 %d" % (int(runner.gp) if runner != null else 0)
|
||||||
|
# 钥石余额常驻(元进度实现批 2026-09-21——A 区顶栏邻接位;数据源 MetaProgression 账本,购买/收束后随全量重查刷新)
|
||||||
|
if keystone_label != null:
|
||||||
|
keystone_label.text = "钥石 %d" % keystone_balance
|
||||||
if settlement == null:
|
if settlement == null:
|
||||||
_refresh_out_of_battle(stage, info, runner, level_active, db)
|
_refresh_out_of_battle(stage, info, runner, level_active, db)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ const ModalStack := preload("res://presentation/modal_stack.gd")
|
|||||||
const TuneSimController := preload("res://presentation/tune_sim_controller.gd")
|
const TuneSimController := preload("res://presentation/tune_sim_controller.gd")
|
||||||
const DebugSessionScript := preload("res://presentation/debug_session.gd")
|
const DebugSessionScript := preload("res://presentation/debug_session.gd")
|
||||||
const SaveManagerScript := preload("res://core/save_manager.gd") # 存档承载(preload 避开全局类缓存时序——与 DebugSessionScript 同款)
|
const SaveManagerScript := preload("res://core/save_manager.gd") # 存档承载(preload 避开全局类缓存时序——与 DebugSessionScript 同款)
|
||||||
|
const MetaProgressionScript := preload("res://core/meta_progression.gd") # 元进度账本服务(元进度实现批——同 preload 纪律)
|
||||||
|
|
||||||
var db
|
var db
|
||||||
var settlement = null # 当前战斗编排器(关卡与调试模式统一读它)
|
var settlement = null # 当前战斗编排器(关卡与调试模式统一读它)
|
||||||
var runner = null # 关卡节点地图 图版推进器(完整关卡模式;冒险模式 = 每关重取 adventure.runner())
|
var runner = null # 关卡节点地图 图版推进器(完整关卡模式;冒险模式 = 每关重取 adventure.runner())
|
||||||
var adventure = null # 冒险结构编排器(冒险结构实现批——三关连探;转发信号照 runner 接线原样对接)
|
var adventure = null # 冒险结构编排器(冒险结构实现批——三关连探;转发信号照 runner 接线原样对接)
|
||||||
var save_manager = null # 存档序列化承载(存档序列化批——启动读盘/冒险收束写盘;只经访问器与 apply 原语,禁读内部结构)
|
var save_manager = null # 存档序列化承载(存档序列化批——启动读盘/冒险收束写盘;只经访问器与 apply 原语,禁读内部结构)
|
||||||
|
var meta = null # 元进度账本服务(元进度实现批——余额/定价/购买/工坊解锁/Boss两档入账/注入计算;零信号纯被动)
|
||||||
var _adventure_mode := false # 冒险模式标记(inter 层确认分派与终局分派)
|
var _adventure_mode := false # 冒险模式标记(inter 层确认分派与终局分派)
|
||||||
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
|
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
|
||||||
var _level_active := false
|
var _level_active := false
|
||||||
@@ -56,11 +58,13 @@ var board_view: BoardView # 探针兼容别名(= board_zone.bo
|
|||||||
var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar)
|
var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar)
|
||||||
var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下)
|
var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下)
|
||||||
var gp_label: Label # A 区顶栏 GP 余额常驻位(GP+商店房实施批——文案装配归 HudPresenter)
|
var gp_label: Label # A 区顶栏 GP 余额常驻位(GP+商店房实施批——文案装配归 HudPresenter)
|
||||||
|
var keystone_label: Label # A 区顶栏钥石余额常驻位(元进度实现批——文案装配归 HudPresenter)
|
||||||
# 模态 overlay(z 序 = 登记序;pattern 6 层级纪律)
|
# 模态 overlay(z 序 = 登记序;pattern 6 层级纪律)
|
||||||
var legend_overlay: LegendOverlay
|
var legend_overlay: LegendOverlay
|
||||||
var review_overlay: SettlementReviewOverlay
|
var review_overlay: SettlementReviewOverlay
|
||||||
var node_overlay: NodeOverlay
|
var node_overlay: NodeOverlay
|
||||||
var shop_overlay: ShopOverlay # 商店覆盖层(GP+商店房实施批——商店房节点兑付呈现)
|
var shop_overlay: ShopOverlay # 商店覆盖层(GP+商店房实施批——商店房节点兑付呈现)
|
||||||
|
var camp_overlay: CampOverlay # 营地覆盖层(元进度实现批——主菜单营地入口兑付呈现)
|
||||||
var tune_panel: TunePanel
|
var tune_panel: TunePanel
|
||||||
|
|
||||||
var _modals = null # 模态层级状态机(modal_stack.gd——缺陷⑤纪律载体)
|
var _modals = null # 模态层级状态机(modal_stack.gd——缺陷⑤纪律载体)
|
||||||
@@ -82,6 +86,8 @@ func _ready() -> void:
|
|||||||
# (fatal 只留给数值表——fail-fast 边界在表加载,不在档);高版本拒读态后续写盘被类内拒绝。
|
# (fatal 只留给数值表——fail-fast 边界在表加载,不在档);高版本拒读态后续写盘被类内拒绝。
|
||||||
save_manager = SaveManagerScript.new()
|
save_manager = SaveManagerScript.new()
|
||||||
save_manager.load_from_disk()
|
save_manager.load_from_disk()
|
||||||
|
# 元进度账本(元进度实现批):存档就绪后建——余额/定价/购买/入账/注入全经此单点
|
||||||
|
meta = MetaProgressionScript.new(db, save_manager)
|
||||||
_debug = DebugSessionScript.new(db, get_node("/root/RngService"),
|
_debug = DebugSessionScript.new(db, get_node("/root/RngService"),
|
||||||
Callable(self, "_debug_hero_override"), Callable(self, "_seed_value"))
|
Callable(self, "_debug_hero_override"), Callable(self, "_seed_value"))
|
||||||
_debug.battle_created.connect(_on_debug_battle_created)
|
_debug.battle_created.connect(_on_debug_battle_created)
|
||||||
@@ -98,6 +104,7 @@ func _build_ui() -> void:
|
|||||||
var battle_ui: Dictionary = UiAssembler.build_battle_ui(root, db)
|
var battle_ui: Dictionary = UiAssembler.build_battle_ui(root, db)
|
||||||
strip_view = battle_ui.strip_view
|
strip_view = battle_ui.strip_view
|
||||||
gp_label = battle_ui.gp_label
|
gp_label = battle_ui.gp_label
|
||||||
|
keystone_label = battle_ui.keystone_label
|
||||||
stage = battle_ui.stage
|
stage = battle_ui.stage
|
||||||
log_view = battle_ui.log_view
|
log_view = battle_ui.log_view
|
||||||
info_panel = battle_ui.info_panel
|
info_panel = battle_ui.info_panel
|
||||||
@@ -128,6 +135,7 @@ func _build_ui() -> void:
|
|||||||
node_overlay = overlays.node_overlay
|
node_overlay = overlays.node_overlay
|
||||||
_end_overlay = overlays.end_overlay
|
_end_overlay = overlays.end_overlay
|
||||||
shop_overlay = overlays.shop_overlay
|
shop_overlay = overlays.shop_overlay
|
||||||
|
camp_overlay = overlays.camp_overlay
|
||||||
tune_panel = overlays.tune_panel
|
tune_panel = overlays.tune_panel
|
||||||
route_overlay = RouteOverlay.new()
|
route_overlay = RouteOverlay.new()
|
||||||
add_child(route_overlay)
|
add_child(route_overlay)
|
||||||
@@ -140,13 +148,15 @@ func _build_ui() -> void:
|
|||||||
route_overlay.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119)
|
route_overlay.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119)
|
||||||
shop_overlay.buy_requested.connect(_on_shop_buy)
|
shop_overlay.buy_requested.connect(_on_shop_buy)
|
||||||
shop_overlay.leave_pressed.connect(_on_shop_leave)
|
shop_overlay.leave_pressed.connect(_on_shop_leave)
|
||||||
|
camp_overlay.buy_requested.connect(_on_camp_buy)
|
||||||
|
camp_overlay.closed.connect(_on_camp_closed)
|
||||||
_end_overlay.review_pressed.connect(_on_end_review)
|
_end_overlay.review_pressed.connect(_on_end_review)
|
||||||
_end_overlay.continue_pressed.connect(_on_battle_end_continue)
|
_end_overlay.continue_pressed.connect(_on_battle_end_continue)
|
||||||
tune_panel.closed.connect(_close_tune_panel)
|
tune_panel.closed.connect(_close_tune_panel)
|
||||||
tune_panel.apply_requested.connect(_on_tune_apply)
|
tune_panel.apply_requested.connect(_on_tune_apply)
|
||||||
tune_panel.simulate_requested.connect(_on_tune_simulate)
|
tune_panel.simulate_requested.connect(_on_tune_simulate)
|
||||||
_modals = ModalStack.new()
|
_modals = ModalStack.new()
|
||||||
for m: Control in [legend_overlay, review_overlay, node_overlay, route_overlay, _end_overlay, shop_overlay, tune_panel]:
|
for m: Control in [legend_overlay, review_overlay, node_overlay, route_overlay, _end_overlay, shop_overlay, camp_overlay, tune_panel]:
|
||||||
_register_modal(m)
|
_register_modal(m)
|
||||||
_tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp))
|
_tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp))
|
||||||
|
|
||||||
@@ -158,6 +168,7 @@ func _build_ui() -> void:
|
|||||||
start.lv3btn.pressed.connect(_start_full_level.bind(3))
|
start.lv3btn.pressed.connect(_start_full_level.bind(3))
|
||||||
start.tunebtn.pressed.connect(_show_tune_panel)
|
start.tunebtn.pressed.connect(_show_tune_panel)
|
||||||
start.dbgbtn.pressed.connect(_start_debug_levels)
|
start.dbgbtn.pressed.connect(_start_debug_levels)
|
||||||
|
start.campbtn.pressed.connect(_open_camp)
|
||||||
|
|
||||||
var inter: Dictionary = UiAssembler.build_inter_overlay(self)
|
var inter: Dictionary = UiAssembler.build_inter_overlay(self)
|
||||||
_inter_overlay = inter.overlay
|
_inter_overlay = inter.overlay
|
||||||
@@ -258,7 +269,8 @@ func _start_adventure() -> void:
|
|||||||
var seed_val := _seed_value()
|
var seed_val := _seed_value()
|
||||||
logger.start_session(seed_val)
|
logger.start_session(seed_val)
|
||||||
adventure.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct())
|
adventure.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct())
|
||||||
if not adventure.start_adventure(seed_val):
|
# 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板)
|
||||||
|
if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}}):
|
||||||
_fatal_screen("冒险启动失败——见输出日志")
|
_fatal_screen("冒险启动失败——见输出日志")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -342,7 +354,7 @@ func _on_big_reward(reward: Dictionary) -> void:
|
|||||||
_refresh_presentations()
|
_refresh_presentations()
|
||||||
|
|
||||||
|
|
||||||
# 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事进文案
|
# 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案
|
||||||
func _on_adventure_finished(summary: Dictionary) -> void:
|
func _on_adventure_finished(summary: Dictionary) -> void:
|
||||||
_level_active = false
|
_level_active = false
|
||||||
var result: String = String(summary.get("result", ""))
|
var result: String = String(summary.get("result", ""))
|
||||||
@@ -351,18 +363,19 @@ func _on_adventure_finished(summary: Dictionary) -> void:
|
|||||||
_sync_settlement_visuals() # → 全关
|
_sync_settlement_visuals() # → 全关
|
||||||
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④)
|
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④)
|
||||||
stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面)
|
stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面)
|
||||||
|
# 存档入账(元进度实现批):settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变——
|
||||||
|
# 单关测试入口与调试两层不经过本处理器,天然不写档不污染生涯数据)。
|
||||||
|
var detail: Dictionary = {}
|
||||||
|
if meta != null:
|
||||||
|
detail = meta.settle_adventure(summary)
|
||||||
|
var text := Narrator.adventure_summary_text(summary)
|
||||||
|
if not detail.is_empty():
|
||||||
|
text += "\n" + Narrator.keystone_settle_text(detail, _item_name_lookup())
|
||||||
if result == "completed":
|
if result == "completed":
|
||||||
_end_overlay.open_victory(Narrator.adventure_summary_text(summary),
|
_end_overlay.open_victory(text, "返回冒险", board_view.get_global_rect())
|
||||||
"返回冒险", board_view.get_global_rect())
|
|
||||||
else:
|
else:
|
||||||
_end_overlay.open_defeat(Narrator.adventure_summary_text(summary),
|
_end_overlay.open_defeat(text, "返回", board_view.get_global_rect())
|
||||||
"返回", board_view.get_global_rect())
|
|
||||||
logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷)
|
logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷)
|
||||||
# 存档入账(存档序列化批):正式冒险收束唯一写档点——域更新原语 + 原子写盘。
|
|
||||||
# 单关测试入口与调试两层不经过本处理器,天然不写档(契约:测试模式不污染生涯数据)。
|
|
||||||
if save_manager != null:
|
|
||||||
save_manager.apply_adventure_summary(summary)
|
|
||||||
save_manager.write_to_disk()
|
|
||||||
if runner != null:
|
if runner != null:
|
||||||
runner.restore_hero_base()
|
runner.restore_hero_base()
|
||||||
strip_view.refresh(runner)
|
strip_view.refresh(runner)
|
||||||
@@ -370,6 +383,15 @@ func _on_adventure_finished(summary: Dictionary) -> void:
|
|||||||
_refresh_presentations()
|
_refresh_presentations()
|
||||||
|
|
||||||
|
|
||||||
|
# 工坊 item_id → 显示名投影(keystone_settle_text 文案数据源——装配归本根,拼句归 Narrator)
|
||||||
|
func _item_name_lookup() -> Dictionary:
|
||||||
|
var lookup := {}
|
||||||
|
if meta != null:
|
||||||
|
for row in db.meta_workshop_items():
|
||||||
|
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
|
||||||
|
return lookup
|
||||||
|
|
||||||
|
|
||||||
func _on_battle_end_continue() -> void:
|
func _on_battle_end_continue() -> void:
|
||||||
# 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石已在收束时入存档——存档序列化批)
|
# 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石已在收束时入存档——存档序列化批)
|
||||||
_close_modal(_end_overlay)
|
_close_modal(_end_overlay)
|
||||||
@@ -622,6 +644,57 @@ func _on_abandon_requested() -> void:
|
|||||||
board_zone.toast.show_toast("战斗进行中——无法放弃", "warn")
|
board_zone.toast.show_toast("战斗进行中——无法放弃", "warn")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 营地覆盖层与钥石账本(元进度实现批——写调用唯一归组合根)----
|
||||||
|
|
||||||
|
# 打开营地(主菜单入口——开合与开始层互斥仿 TunePanel 先例:开始覆盖层非模态且树序在后,
|
||||||
|
# 规范 2.3 遮挡纪律;打开时先刷工坊解锁——达标未解锁就地入账)
|
||||||
|
func _open_camp() -> void:
|
||||||
|
if meta == null:
|
||||||
|
return
|
||||||
|
meta.refresh_workshop_unlocks()
|
||||||
|
_start_overlay.visible = false
|
||||||
|
camp_overlay.open_camp(_camp_data())
|
||||||
|
_refresh_presentations()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_camp_closed() -> void:
|
||||||
|
camp_overlay.visible = false
|
||||||
|
_start_overlay.visible = true
|
||||||
|
_refresh_presentations() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面)
|
||||||
|
|
||||||
|
|
||||||
|
# 营地快照投影(纯读——锻造三行/工坊六行/余额/最近冒险;拼句归 CampOverlay,账面归 MetaProgression)
|
||||||
|
func _camp_data() -> Dictionary:
|
||||||
|
var forge_rows: Array = []
|
||||||
|
if meta != null:
|
||||||
|
for attr in MetaTable.FORGE_ATTRS:
|
||||||
|
var row: Dictionary = db.meta_forge_row(attr)
|
||||||
|
forge_rows.append({
|
||||||
|
"attr": attr,
|
||||||
|
"display_name": String(row.get("display_name", "")),
|
||||||
|
"level": meta.forge_level(attr),
|
||||||
|
"flat": int(row.get("flat_value", 0)),
|
||||||
|
"price": meta.forge_price(attr),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"balance": meta.keystone_balance() if meta != null else 0,
|
||||||
|
"forge": forge_rows,
|
||||||
|
"workshop": meta.workshop_status() if meta != null else [],
|
||||||
|
"recent": save_manager.recent_summaries() if save_manager != null else [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _on_camp_buy(attr: String) -> void:
|
||||||
|
if meta == null:
|
||||||
|
return
|
||||||
|
if not meta.buy_forge(attr):
|
||||||
|
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
|
||||||
|
board_zone.toast.show_toast("钥石不足——买不起这件强化", "warn")
|
||||||
|
return
|
||||||
|
camp_overlay.refresh_camp(_camp_data())
|
||||||
|
_refresh_presentations()
|
||||||
|
|
||||||
|
|
||||||
# ---- 商店覆盖层与 GP 钱包(GP+商店房实施批——写调用唯一归组合根)----
|
# ---- 商店覆盖层与 GP 钱包(GP+商店房实施批——写调用唯一归组合根)----
|
||||||
|
|
||||||
func _on_shop_buy(product_id: String) -> void:
|
func _on_shop_buy(product_id: String) -> void:
|
||||||
@@ -728,7 +801,8 @@ func _handle_outcome_feedback(outcome: Dictionary) -> void:
|
|||||||
func _refresh_presentations() -> void:
|
func _refresh_presentations() -> void:
|
||||||
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
|
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
|
||||||
var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_round)
|
var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_round)
|
||||||
HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining, gp_label)
|
HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining,
|
||||||
|
gp_label, keystone_label, meta.keystone_balance() if meta != null else 0)
|
||||||
_sync_settlement_visuals()
|
_sync_settlement_visuals()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ static func build_battle_ui(root: Control, db) -> Dictionary:
|
|||||||
var gp_label := UiKit.label("金币 0", 15, true)
|
var gp_label := UiKit.label("金币 0", 15, true)
|
||||||
gp_label.add_theme_color_override("font_color", UiKit.GOLD_DEEP)
|
gp_label.add_theme_color_override("font_color", UiKit.GOLD_DEEP)
|
||||||
strip_row.add_child(gp_label)
|
strip_row.add_child(gp_label)
|
||||||
|
var keystone_label := UiKit.label("钥石 0", 15, true) # 元进度实现批——A 区顶栏钥石余额常驻(文案装配归 HudPresenter,此处只建节点)
|
||||||
|
keystone_label.add_theme_color_override("font_color", UiKit.GOLD)
|
||||||
|
strip_row.add_child(keystone_label)
|
||||||
|
|
||||||
# B 区:战斗画面(全宽 × 252px——常驻双舞台,布局决策 D1)
|
# B 区:战斗画面(全宽 × 252px——常驻双舞台,布局决策 D1)
|
||||||
var stage := BattleStageView.new(db)
|
var stage := BattleStageView.new(db)
|
||||||
@@ -56,6 +59,7 @@ static func build_battle_ui(root: Control, db) -> Dictionary:
|
|||||||
return {
|
return {
|
||||||
"strip_view": strip_view,
|
"strip_view": strip_view,
|
||||||
"gp_label": gp_label,
|
"gp_label": gp_label,
|
||||||
|
"keystone_label": keystone_label,
|
||||||
"stage": stage,
|
"stage": stage,
|
||||||
"log_view": stage.log_view,
|
"log_view": stage.log_view,
|
||||||
"info_panel": info_panel,
|
"info_panel": info_panel,
|
||||||
@@ -85,6 +89,10 @@ static func build_overlays(host: Control, db, base_atk: int, base_hp: int) -> Di
|
|||||||
var shop_overlay := ShopOverlay.new()
|
var shop_overlay := ShopOverlay.new()
|
||||||
host.add_child(shop_overlay)
|
host.add_child(shop_overlay)
|
||||||
|
|
||||||
|
# 营地覆盖层(元进度实现批 2026-09-21——主菜单营地入口兑付呈现;开合仿 TunePanel 与开始层互斥)
|
||||||
|
var camp_overlay := CampOverlay.new()
|
||||||
|
host.add_child(camp_overlay)
|
||||||
|
|
||||||
var tune_panel := TunePanel.new(db, base_atk, base_hp)
|
var tune_panel := TunePanel.new(db, base_atk, base_hp)
|
||||||
tune_panel.visible = false # 显式初始化——原版依赖"被不透明开始层遮蔽"的树序巧合(缺陷⑤家族);模态锁输入以 visible 为准
|
tune_panel.visible = false # 显式初始化——原版依赖"被不透明开始层遮蔽"的树序巧合(缺陷⑤家族);模态锁输入以 visible 为准
|
||||||
host.add_child(tune_panel)
|
host.add_child(tune_panel)
|
||||||
@@ -95,6 +103,7 @@ static func build_overlays(host: Control, db, base_atk: int, base_hp: int) -> Di
|
|||||||
"node_overlay": node_overlay,
|
"node_overlay": node_overlay,
|
||||||
"end_overlay": end_overlay,
|
"end_overlay": end_overlay,
|
||||||
"shop_overlay": shop_overlay,
|
"shop_overlay": shop_overlay,
|
||||||
|
"camp_overlay": camp_overlay,
|
||||||
"tune_panel": tune_panel,
|
"tune_panel": tune_panel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +140,8 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary:
|
|||||||
box.add_child(tunebtn)
|
box.add_child(tunebtn)
|
||||||
var dbgbtn := UiKit.button("调试 · 连续关卡两层(HP 关内延续)")
|
var dbgbtn := UiKit.button("调试 · 连续关卡两层(HP 关内延续)")
|
||||||
box.add_child(dbgbtn)
|
box.add_child(dbgbtn)
|
||||||
|
var campbtn := UiKit.button("营地 · 永久强化(钥石换永久属性——跨局成长账本)") # 元进度实现批——主菜单营地入口(双入口框架的主菜单一侧)
|
||||||
|
box.add_child(campbtn)
|
||||||
|
|
||||||
# 玩法帮助文案(表值驱动——2026-09-16 AP 压速批遗留硬编码 8 随手修正;护甲等级掷点批同步"减伤"→"护甲强化")
|
# 玩法帮助文案(表值驱动——2026-09-16 AP 压速批遗留硬编码 8 随手修正;护甲等级掷点批同步"减伤"→"护甲强化")
|
||||||
var help_text := "玩法:每轮 %d AP——挖开(1AP)/插旗(1AP)/移动(免费)/连锁(免费)。\n每个非封锁格都藏一种格面资源(增伤/护甲强化/回血/爆裂/削弱)。开安全格、标中雷 = 正向兑付;\n开雷格、标错安全格 = 负向兑付——全部在轮末结算逐行兑现;吃掷点的基础攻击按护甲等级判定,无 miss、最低保底 1 点。\n停手:剩余 AP 转存 RAP;弃图刷新:RAP−%d 当场换图继续挖(每轮剩 %d 次)。\n敌人全灭胜、主角倒下负。悬停可看三态光标与可挖域微光。" % [
|
var help_text := "玩法:每轮 %d AP——挖开(1AP)/插旗(1AP)/移动(免费)/连锁(免费)。\n每个非封锁格都藏一种格面资源(增伤/护甲强化/回血/爆裂/削弱)。开安全格、标中雷 = 正向兑付;\n开雷格、标错安全格 = 负向兑付——全部在轮末结算逐行兑现;吃掷点的基础攻击按护甲等级判定,无 miss、最低保底 1 点。\n停手:剩余 AP 转存 RAP;弃图刷新:RAP−%d 当场换图继续挖(每轮剩 %d 次)。\n敌人全灭胜、主角倒下负。悬停可看三态光标与可挖域微光。" % [
|
||||||
@@ -140,7 +151,8 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary:
|
|||||||
box.add_child(help)
|
box.add_child(help)
|
||||||
|
|
||||||
return {"overlay": overlay, "box": box, "seed_edit": seed_edit,
|
return {"overlay": overlay, "box": box, "seed_edit": seed_edit,
|
||||||
"lvbtn": lvbtn, "lv2btn": lv2btn, "lv3btn": lv3btn, "tunebtn": tunebtn, "dbgbtn": dbgbtn}
|
"lvbtn": lvbtn, "lv2btn": lv2btn, "lv3btn": lv3btn, "tunebtn": tunebtn, "dbgbtn": dbgbtn,
|
||||||
|
"campbtn": campbtn}
|
||||||
|
|
||||||
|
|
||||||
# 调试两层模式的层间过渡覆盖层(历史入口专用)。返回 {overlay, label, next_btn}——接线归组合根
|
# 调试两层模式的层间过渡覆盖层(历史入口专用)。返回 {overlay, label, next_btn}——接线归组合根
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||||
|
# Date: 2026-09-21(元进度实现批——营地覆盖层呈现)
|
||||||
|
# 营地覆盖层(模态):标题「营地 · 永久强化」+ 钥石余额 / 柜台一「属性锻造」(三行可购——显示名 ·
|
||||||
|
# 当前 Lv · 每级 +flat · 下级价格 · 购买按钮余额不足禁用)/ 柜台二「道具工坊」(六行——显示名 ·
|
||||||
|
# 解锁条件 · 进度 · 状态;局内效果排 Alpha 魔法道具重设计批——代拍板 3,注记一行)/ 柜台三
|
||||||
|
# 「奖励格图鉴」(占位锁定行——内容排 Alpha 地图战斗资源差异化批——代拍板 4)/「最近冒险」列表
|
||||||
|
# (recent_summaries 投影:结果/通关数/钥石合计/种子)/ 关闭按钮。
|
||||||
|
# 挂接方式:仿 ShopOverlay(dim + CenterContainer + box);开合与开始层互斥仿 TunePanel 先例
|
||||||
|
# (开始覆盖层非模态且树序在后——打开本层前由组合根隐藏开始层,规范 2.3 遮挡纪律)。
|
||||||
|
# 高内聚边界:纯视图 setter 面——账本快照经组合根喂入(MetaProgression 只读投影),购买/关闭
|
||||||
|
# 意图经信号外发——零核心写调用、零 MetaProgression 持有。视觉 = ui_kit 程序占位
|
||||||
|
#(代拍板 8——视觉终稿以表现层样张过审为前置,归 HUD 协作批)。
|
||||||
|
class_name CampOverlay
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
signal buy_requested(attr: String)
|
||||||
|
signal closed
|
||||||
|
|
||||||
|
const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表,与 ShopOverlay 同款)
|
||||||
|
|
||||||
|
var _balance_label: Label
|
||||||
|
var _forge_holder: VBoxContainer
|
||||||
|
var _workshop_holder: VBoxContainer
|
||||||
|
var _forge_rows: Array = [] # [{attr, level_l, price_l, btn}]——refresh_camp 就地重刷(零重建回流)
|
||||||
|
var _workshop_rows: Array = [] # [{item_id, progress_l, status_l}]
|
||||||
|
var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建)
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
var dim := ColorRect.new()
|
||||||
|
dim.color = Color(0, 0, 0, 0.72)
|
||||||
|
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
add_child(dim)
|
||||||
|
var center := CenterContainer.new()
|
||||||
|
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
dim.add_child(center)
|
||||||
|
var box := VBoxContainer.new()
|
||||||
|
box.add_theme_constant_override("separation", 6)
|
||||||
|
center.add_child(box)
|
||||||
|
var title := UiKit.label("营地 · 永久强化", 24, true)
|
||||||
|
title.add_theme_color_override("font_color", UiKit.WARM)
|
||||||
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
box.add_child(title)
|
||||||
|
_balance_label = UiKit.label("钥石 0", 18, true)
|
||||||
|
_balance_label.add_theme_color_override("font_color", UiKit.GOLD)
|
||||||
|
_balance_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
box.add_child(_balance_label)
|
||||||
|
box.add_child(HSeparator.new())
|
||||||
|
|
||||||
|
box.add_child(_counter_header("属性锻造(钥石直购 · 每级 flat 整数 · 价格几何递增)"))
|
||||||
|
_forge_holder = VBoxContainer.new()
|
||||||
|
_forge_holder.add_theme_constant_override("separation", 4)
|
||||||
|
box.add_child(_forge_holder)
|
||||||
|
|
||||||
|
box.add_child(_counter_header("道具工坊(累计通关次数解锁 · 局内效果排 Alpha 魔法道具重设计批)"))
|
||||||
|
_workshop_holder = VBoxContainer.new()
|
||||||
|
_workshop_holder.add_theme_constant_override("separation", 4)
|
||||||
|
box.add_child(_workshop_holder)
|
||||||
|
|
||||||
|
box.add_child(_counter_header("奖励格图鉴"))
|
||||||
|
var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)", 13, false)
|
||||||
|
lock_l.add_theme_color_override("font_color", UiKit.WARM_FAINT)
|
||||||
|
box.add_child(lock_l)
|
||||||
|
box.add_child(HSeparator.new())
|
||||||
|
|
||||||
|
box.add_child(_counter_header("最近冒险"))
|
||||||
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.custom_minimum_size = Vector2(560, 96)
|
||||||
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
box.add_child(scroll)
|
||||||
|
_recent_list = UiKit.label("", 13, false)
|
||||||
|
_recent_list.add_theme_color_override("font_color", UiKit.WARM_SOFT)
|
||||||
|
_recent_list.custom_minimum_size = Vector2(544, 0)
|
||||||
|
_recent_list.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||||
|
scroll.add_child(_recent_list)
|
||||||
|
|
||||||
|
var close_btn := UiKit.button("关闭营地(回开始界面)")
|
||||||
|
close_btn.pressed.connect(func(): closed.emit())
|
||||||
|
box.add_child(close_btn)
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func _counter_header(text: String) -> Label:
|
||||||
|
var l := UiKit.label(text, 15, true)
|
||||||
|
l.add_theme_color_override("font_color", UiKit.WARM_SOFT)
|
||||||
|
return l
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 柜台行构建(open_camp 重建;行结构仿 ShopOverlay 商品卡)----
|
||||||
|
|
||||||
|
# 打开营地(全量重建三柜台行 + 最近冒险——数据 = 组合根 _camp_data() 投影)
|
||||||
|
func open_camp(data: Dictionary) -> void:
|
||||||
|
_rebuild_forge(data)
|
||||||
|
_rebuild_workshop(data)
|
||||||
|
_rebuild_recent(data)
|
||||||
|
refresh_camp(data)
|
||||||
|
visible = true
|
||||||
|
|
||||||
|
|
||||||
|
# 状态重刷(购买后调用——余额/价格/禁用态/工坊状态就地改文案,零重建)
|
||||||
|
func refresh_camp(data: Dictionary) -> void:
|
||||||
|
var balance := int(data.get("balance", 0))
|
||||||
|
_balance_label.text = "钥石 %d" % balance
|
||||||
|
var forge_by_attr := {}
|
||||||
|
for row in data.get("forge", []):
|
||||||
|
forge_by_attr[String(row.get("attr", ""))] = row
|
||||||
|
for card in _forge_rows:
|
||||||
|
var row: Dictionary = forge_by_attr.get(String(card.attr), {})
|
||||||
|
if row.is_empty():
|
||||||
|
continue
|
||||||
|
card.level_l.text = "Lv %d" % int(row.get("level", 0))
|
||||||
|
card.price_l.text = "下级价格 %d 钥石" % int(row.get("price", 0))
|
||||||
|
card.btn.disabled = balance < int(row.get("price", 0)) # 余额不足禁用置灰
|
||||||
|
var ws_by_id := {}
|
||||||
|
for row in data.get("workshop", []):
|
||||||
|
ws_by_id[String(row.get("item_id", ""))] = row
|
||||||
|
for card in _workshop_rows:
|
||||||
|
var row: Dictionary = ws_by_id.get(String(card.item_id), {})
|
||||||
|
if row.is_empty():
|
||||||
|
continue
|
||||||
|
var th := int(row.get("threshold", 0))
|
||||||
|
var prog := int(row.get("progress", 0))
|
||||||
|
var unlocked := bool(row.get("unlocked", false))
|
||||||
|
card.progress_l.text = "累计通关 %d / %d 次" % [prog, th]
|
||||||
|
card.status_l.text = "已解锁" if unlocked else "未解锁"
|
||||||
|
card.status_l.add_theme_color_override("font_color", UiKit.GOLD if unlocked else UiKit.WARM_FAINT)
|
||||||
|
|
||||||
|
|
||||||
|
func _rebuild_forge(data: Dictionary) -> void:
|
||||||
|
_forge_rows.clear()
|
||||||
|
for child in _forge_holder.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
for row in data.get("forge", []):
|
||||||
|
_forge_rows.append(_build_forge_row(_forge_holder, row))
|
||||||
|
|
||||||
|
|
||||||
|
func _build_forge_row(holder: VBoxContainer, row: Dictionary) -> Dictionary:
|
||||||
|
var card := PanelContainer.new()
|
||||||
|
card.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.BG_OVERLAY, UiKit.GOLD_DIM, 4, 1, Vector4(12, 4, 12, 4)))
|
||||||
|
var line := HBoxContainer.new()
|
||||||
|
line.add_theme_constant_override("separation", 12)
|
||||||
|
card.add_child(line)
|
||||||
|
var attr := String(row.get("attr", ""))
|
||||||
|
var name_l := UiKit.label("%s(每级 %+d flat)" % [String(row.get("display_name", "")), int(row.get("flat", 0))], 14, true)
|
||||||
|
name_l.custom_minimum_size = Vector2(210, 0)
|
||||||
|
line.add_child(name_l)
|
||||||
|
var level_l := UiKit.label("", 14, false)
|
||||||
|
level_l.custom_minimum_size = Vector2(64, 0)
|
||||||
|
line.add_child(level_l)
|
||||||
|
var price_l := UiKit.label("", 14, false)
|
||||||
|
price_l.add_theme_color_override("font_color", UiKit.WARM_SOFT)
|
||||||
|
price_l.custom_minimum_size = Vector2(150, 0)
|
||||||
|
line.add_child(price_l)
|
||||||
|
var btn := UiKit.button("锻造 +1 级")
|
||||||
|
btn.pressed.connect(func(): buy_requested.emit(attr))
|
||||||
|
line.add_child(btn)
|
||||||
|
holder.add_child(card)
|
||||||
|
return {"attr": attr, "level_l": level_l, "price_l": price_l, "btn": btn}
|
||||||
|
|
||||||
|
|
||||||
|
func _rebuild_workshop(data: Dictionary) -> void:
|
||||||
|
_workshop_rows.clear()
|
||||||
|
for child in _workshop_holder.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
for row in data.get("workshop", []):
|
||||||
|
var card := PanelContainer.new()
|
||||||
|
card.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.BG_OVERLAY, UiKit.WARM_FAINT, 4, 1, Vector4(12, 4, 12, 4)))
|
||||||
|
var line := HBoxContainer.new()
|
||||||
|
line.add_theme_constant_override("separation", 12)
|
||||||
|
card.add_child(line)
|
||||||
|
var name_l := UiKit.label(String(row.get("display_name", "")), 14, true)
|
||||||
|
name_l.custom_minimum_size = Vector2(210, 0)
|
||||||
|
line.add_child(name_l)
|
||||||
|
var progress_l := UiKit.label("", 13, false)
|
||||||
|
progress_l.add_theme_color_override("font_color", UiKit.WARM_SOFT)
|
||||||
|
progress_l.custom_minimum_size = Vector2(150, 0)
|
||||||
|
line.add_child(progress_l)
|
||||||
|
var status_l := UiKit.label("", 13, true)
|
||||||
|
line.add_child(status_l)
|
||||||
|
_workshop_holder.add_child(card)
|
||||||
|
_workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l})
|
||||||
|
|
||||||
|
|
||||||
|
func _rebuild_recent(data: Dictionary) -> void:
|
||||||
|
var lines: Array = []
|
||||||
|
var recent: Array = data.get("recent", [])
|
||||||
|
if recent.is_empty():
|
||||||
|
lines.append(" (暂无冒险记录——完成或结束一次冒险后入账)")
|
||||||
|
for s in recent:
|
||||||
|
var verdict := "失败"
|
||||||
|
if String(s.get("result", "")) == "completed":
|
||||||
|
verdict = "完成"
|
||||||
|
elif String(s.get("result", "")) == "abandoned":
|
||||||
|
verdict = "放弃"
|
||||||
|
lines.append("%s · 通关 %d 关 · 钥石 +%d · 种子 %d" % [verdict,
|
||||||
|
int(s.get("levels_cleared", 0)), int(s.get("keystone_total", 0)), int(s.get("master_seed", 0))])
|
||||||
|
_recent_list.text = "\n".join(lines)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dvg2wmjh87p8a
|
||||||
Reference in new issue
Block a user