1121 lines
67 KiB
GDScript
1121 lines
67 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# Validation Question: 三关连探冒险(地精洞窟 → 幽暗地域 → 灰矮人矿城)能否以一条命走完——
|
||
# 主种子恰一次注入、生命/GP/加算区跨关同账、大奖励非末关恰兑付一次、三结局摘要五件事成立(P0)
|
||
# Date: 2026-09-21(冒险结构实现批——设计权威 design/gdd/systems/冒险循环/冒险结构.md)
|
||
# 冒险结构编排器(Core 层零 UI 依赖——headless 可跑完整冒险):一局的关卡序列、生命账本、GP 冒险
|
||
# 账本、统一冒险加算区、关卡大奖励、三结局结算与主种子接线的唯一所有者。
|
||
#
|
||
# 接口契约四件套落点(冒险结构.md「与关卡节点地图的边界」——本类是正式侧承载):
|
||
# ① 开始关卡 = start_level_external(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接);
|
||
# ② 战斗结果上报 = runner 经共享账本属性直写(LevelRunner._on_battle_finished 回写 hero_hp 到
|
||
# _ledger——零直调战斗域,战后生命终值随账本回流本类);
|
||
# ③ 治疗封顶回报 = apply_rest(恢复点节点兑付时 LevelRunner 经 _rest_handler 回调——按比例回血并
|
||
# 封顶的执行归本类);
|
||
# ④ 关卡结束 = _on_level_finished(LevelRunner.level_finished 到达——结果 + 生命终值 + 节点位置)。
|
||
#
|
||
# 接口漂移清偿登记(06 清单①②③⑥⑦——LevelRunner 侧同批登记):①种子域归本类
|
||
# (start_adventure 调 rng.new_adventure 全冒险恰一次;LevelRunner 冒险模式不重播种);
|
||
# ②生命账本归本类(共享 _ledger);③恢复兑付执行归本类(_rest_handler);⑥收束先清库后宣布——
|
||
# 现行无关卡域库存(格面资源批 2026-09-15 后为空集),战斗结束 retire_mapping + despawn_all 先于
|
||
# battle_finished 即事实清库,不另设空接口;⑦失败结算归本类(LevelRunner 只上报结果)。
|
||
#
|
||
# 收束次序登记(大奖励兑付时点——冒险结构.md「关卡大奖励」):关卡节点地图先清库、后宣布关卡
|
||
# 结束(次序不可换);大奖励在宣布之后、下一关开始之前兑付——本类 _on_level_finished 兑付、
|
||
# continue_to_next_level 开始下一关,两步间为呈现层大奖励屏(冒烟/自动路径立即确认)。
|
||
# 结算受理只算一次(契约):state != "running" 的关卡结束宣布与放弃请求一律拒绝并留诊断,
|
||
# 账本与摘要零变化。
|
||
# 停点前移批(2026-09-23,用户拍板「关卡开始即出节点图选路、选节点才进战斗」):每关开始 =
|
||
# 入口选路停点——_start_current_level 以 defer_entry 起关,level_started 之后才 present_entry_choice
|
||
# (信号顺序契约:呈现层先重取 runner 实例,选路口才不会被空防御丢弃);pick 入口候选走同一
|
||
# _enter_node(entry)、呈现零 RNG 消耗——与旧「start 直进首战」逐位等价,测量面零漂移。
|
||
# 关卡切换(continue_to_next_level)后新 LevelRunner 实例重新走 defer 流程;abandon 在入口停点
|
||
# 天然非战斗期(current_battle 为空)——受理语义与选路停点一致。
|
||
# 纪律:ADR-0001 S6——连 runner 信号一律 _forward_* 方法 Callable(禁 lambda 捕获 self 成环);
|
||
# Core 层禁 UI 引用;数值零字面量——level_clear_hp_bonus / keystone_per_level 只读 db.adventure,
|
||
# 基础上限只读 db.hero。
|
||
# 断点续玩(迭代 8 卡①):save_manager 经 set_save_manager 注入(缺省 null = 不写断点——模拟/
|
||
# 冒烟零触达零扰动);停点快照挂 choices_presented 转发点(选路停点=唯一可恢复停点形态,战斗中
|
||
# 不存);三结局 _settle 清断点;resume_adventure 经 rng.restore_adventure 回填「主种子+计数器」
|
||
# ——续玩从断点号位精确续接原随机时间线。
|
||
class_name AdventureRunner
|
||
extends RefCounted
|
||
|
||
signal level_started(level_index: int, display_name: String) # 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行
|
||
signal curio_choice_presented(payload: Dictionary) # 专长三选一停点(非末关通关恰一次;迭代 20 T5 换代正名——旧「提升三选一」退役,通道白名单后候选恒专长+回血在载荷)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared}
|
||
signal curio_resolved(result: Dictionary) # 三选一兑付完成(pick_curio/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index}
|
||
signal adventure_finished(summary: Dictionary) # 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子(+additive keystone_drops 迭代 10)
|
||
# ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明;ADR-0001 S6 经 _forward_* 方法)----
|
||
signal node_entered(node: Dictionary, info: Dictionary)
|
||
signal choices_presented(candidates: Array)
|
||
signal step_row(row: Dictionary)
|
||
signal round_started(round_index: int)
|
||
signal board_changed()
|
||
signal wallet_changed(event: String, gp: int, extra: Dictionary)
|
||
|
||
var db: ConfigDb
|
||
var rng: RngServiceCore
|
||
var state := "idle" # idle / running / settling(冒险结构.md 三态)
|
||
var ledger := {"hero_hp": 0, "gp": 0} # 冒险账本(生命 + GP 钱包——经 start_level_external 注入 runner 共享)
|
||
var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 统一冒险加算区(商店购买与三选一属性件同一本账;四轴 +ap——迭代 10 奖励重构)
|
||
var purchased := {} # product_id → 已购档数(跨关延续、冒险收束清零——递增定价输入)
|
||
var level_index := 0 # 当前关卡序号(1 起)
|
||
var levels_total := 0 # 序列总关数(读 level 表行数)
|
||
var keystone := 0 # 钥石入账累计(摘要第三件事的账面)
|
||
var master_seed := 0 # 本局主种子(摘要第五件事)
|
||
var summary := {} # 最近一次结算的冒险摘要(idle 后保留——重开才新生成)
|
||
var _runner: LevelRunner # 当前关卡行进器(每关新建——关卡域状态天然隔离)
|
||
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
|
||
var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面)
|
||
var _override := [0, 0] # 调试浮窗百分比通道 [atk_pct, hp_pct](每关新 runner 都要应用)
|
||
var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0}
|
||
var _first_script := false # 首次脚本模式(关卡节点图生成 v0.4——true = 全关零抽签取池内模板 0;测量夹具恒 false) # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板)
|
||
var _last_treasure_cleared := 0 # additive(迭代 7 T4——最近一次关卡末清空计数,summary 取数面)
|
||
var _treasure_unlocked := {} # aux 解锁门控集(迭代 7 T5——params.treasure_unlocked 接收面)
|
||
var _attunement: Array = [] # 同调装配集(迭代 11 第①批 T5——params.attunement 接收面,start_level 外下传 LevelRunner)
|
||
var _attune_refresh_x := 0 # 刷新件余量(迭代 11 第①批 T5b P1 甲——每碰三选一重置=X 表值;优先消耗、不结转)
|
||
var _attune_refresh_y := 0 # Y 冒险周期池(开局参数面注入,用完即无——营地升级行表值)
|
||
var _curio_paid_refreshes := 0 # 本次停点已付费刷新次数(2026-10-06 用户令对齐商店 MC-1——GP 刷新价曲线输入,随停点创建清零=与商店进店清零同构)
|
||
var attune_charges := {} # 同调主动件冒险充能账本(迭代 11 第①批 T6——跨关卡共享 dict;settlement use_treasure 扣减直改同引用;断点随 attunement_charges 键入档)
|
||
var vision_counter := {"used": 0} # 真视类工具使用计数(迭代 22 秘宝批 T3——跨关卡共享 dict;编排器受理成功 +1;断点随 vision_counter_used 键入档,收束聚合入档 save.add_vision_used)
|
||
var adventure_counters := {} # adventure 域计数器账本(迭代 20 T6——攒层/付费计数等跨战持久件账;效果引擎 rebuild 回灌/战末导出回写共享引用;断点随 adventure_counters 键入档)
|
||
# ---- 凭证件三本新账(迭代 20 T4——奇物.md 凭证件表新 5 件;F-1 刷新写错账修复承载面)----
|
||
var _refresh_stock := 0 # 刷新存量账(冒险级——刷新券加仓;**不经 _attune_refresh_x**:X 每停点重置会吞存量=旧写错账缺陷 F-1)
|
||
var _pending_rap := 0 # 储备金袋待注入(节点级——下一战 RAP +N,战首注入后清零)
|
||
var _battery_pending := 0 # 同调电池待分配充能(冒险级——choose_battery_target 玩家点选消费,禁自动替选)
|
||
var _time_sand_queue := 0 # 时沙待注入(回合级——下一战首轮 AP +N,战首入队后清零)
|
||
var _pending_insurance := 0 # 完美保险待注入(节点级——下一战标错豁免 N 次,战首注入后清零;战内未用即过期)
|
||
var _save_manager = null # 断点承载注入(迭代 8 卡①——null = 不写断点;模拟/冒烟缺省)
|
||
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
|
||
# ---- 三选一停点态(迭代 10 奖励重构批)----
|
||
var _curio_taken := {} # 件获取次数计数(kind 维度派生账本——断点附随件与摘要读数承载;池库存正主=_curio_deck)
|
||
var _curio_deck: Array = [] # 令牌池(TP0 v2.2——持久多重集,元素 {"kind","tier"};洗牌抽候选/选中踢池未选回池/池耗即枯竭;商店货架同库共账;迭代 12 T1 起规则改写型特性带=relic 令牌随表追加)
|
||
var _feats: Array[String] = [] # 特性持有集(迭代 12 T1——局内随机渠道获得、冒险级跨关携带、收束清零;🔴 层级纪律承载面:不入同调位不入局外树)
|
||
var _tree_rules: Array = [] # 树 rule 键集(迭代 12 T10——Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面)
|
||
var _curio_pending := {} # 停点态:{"candidates": Array, "payload": Dictionary, "chosen"/"incoming": 替换在途}——空 = 无停点
|
||
var _curio_result := {} # 已兑付待推进态(curio_resolved 载荷副本——中断续玩不重兑付)
|
||
var _keystone_drops: Array = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops——结算明细/复盘归因数据源)
|
||
var _node_curio_pending := {} # 奖励节点三选一停点态(迭代 10 追加批——共享池;断点不载:续玩重建到选路停点重走,恢复的计数器保证同候选重抽)
|
||
var _feat_ticket_pity := 0 # 门票保底计数(T5——展示次数:节点开面/货架抽签/刷新重展各计一次;触发清零)
|
||
var _feat_ticket_sold := 0 # 门票限购计数(T5——选中/买下即 +1;拿到即开不进持有集)
|
||
var _feat_ticket_appeared := 0 # 门票出现计数(T5——quota 上限面)
|
||
var _shop_mode := "legacy" # 商店模式(迭代 11 第②批 T3——"legacy"=固定三件加定价〔off 对照承载,缺省零扰动〕/"pool"=共享池抽 3 买 1 重抽;真实入口 main.gd 显式投 pool)
|
||
|
||
|
||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||
db = db_ref
|
||
rng = rng_ref
|
||
|
||
|
||
# ---- 冒险生命周期 ----
|
||
|
||
# 冒险开始(主种子注入恰一次——全冒险唯一 rng.new_adventure 调用点;重开 = 全新一轮:
|
||
# 账本/加算区/已购档/钥石/摘要全重置,生命置满基础上限,从第 1 关起步)。
|
||
# params.hero_override = 永久强化 flat 注入(元进度实现批——缺省 {} = 全零白板,与 B1 空参数路径逐位一致;
|
||
# -1/缺键 = 表值兜底口径,规范化为 0 flat)
|
||
func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||
if state != "idle":
|
||
push_error("[冒险结构] start_adventure 拒绝——state=%s(重开须待上一局结算完毕)" % state)
|
||
return false
|
||
master_seed = seed_val
|
||
rng.new_adventure(seed_val)
|
||
ledger = {"hero_hp": 0, "gp": 0}
|
||
bonus = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||
purchased = {}
|
||
keystone = 0
|
||
summary = {}
|
||
_reset_curio_state()
|
||
levels_total = db.level_count() # 表访问收口(2026-10-02——行集直扫退役)
|
||
level_index = 1
|
||
_pending_next_level = false
|
||
_base_override = _normalize_override(params.get("hero_override", {}))
|
||
_first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入)
|
||
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 迭代 11 第②批 T3——缺省 legacy(off 对照零扰动)
|
||
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
|
||
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive(迭代 11 第①批 T5)
|
||
_attune_refresh_y = maxi(0, int(params.get("attune_refresh_y", 0))) # additive(迭代 11 第①批 T5b——Y 冒险周期池)
|
||
_seed_attune_charges() # 迭代 11 第①批 T6——冒险开始充能账本按装备主动件重置(每冒险充能语义)
|
||
vision_counter = {"used": 0} # 迭代 22 秘宝批 T3——真视使用计数冒险开始归零(跨局累计归生涯档)
|
||
# 刷新练习开局注入(迭代 22 秘宝批 T3——refresh_stock_grant 装配即得一次性加仓:
|
||
# 装配集 kind 分发扫描;只入存量账 _refresh_stock、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕)
|
||
for iid_v2 in _attunement:
|
||
var rp_row: Dictionary = db.treasure_item_row(String(iid_v2))
|
||
if (rp_row.get("effect_kinds", []) as Array).has("refresh_stock_grant"):
|
||
_refresh_stock += int(rp_row.get("magnitude", 0))
|
||
# 特性开局注入(迭代 12 T7——MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派
|
||
# 各 2 件档一成型——测量面专用;真实局规则改写型特性只走局内随机渠道,红线②层级纪律不破〕)
|
||
var feat_seed: Array = params.get("relics_seed", []) if params.get("relics_seed", []) is Array else []
|
||
_tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # 迭代 12 T10——A/B 面 rule 键集
|
||
if not feat_seed.is_empty():
|
||
_feats = []
|
||
for r in feat_seed:
|
||
_feats.append(String(r))
|
||
ledger["hero_hp"] = effective_max_hp() # 生命账本置满(生效上限口径——T17 修复:含调试浮窗 pct,与战斗内同源)
|
||
state = "running"
|
||
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
|
||
|
||
|
||
# 断点续玩(迭代 8 卡①——「继续上局」入口;bp = save.breakpoint_snapshot() 快照,params 同 start_adventure
|
||
# 的注入参数面):账面(生命/GP/加算区/已购档/钥石累计)逐字段回填 + rng.restore_adventure 回填
|
||
# 「主种子+计数器」→ _start_current_level 续玩路径(模板随快照强制、重建到停点节点,零重抽)。
|
||
# 快照缺字段按防御默认(空快照拒绝——调用方先 has_breakpoint)。
|
||
func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
|
||
if state != "idle":
|
||
push_error("[冒险结构] resume_adventure 拒绝——state=%s(非 idle 不可续玩)" % state)
|
||
return false
|
||
if bp.is_empty():
|
||
push_error("[冒险结构] resume_adventure 拒绝——空断点快照")
|
||
return false
|
||
master_seed = int(bp.get("master_seed", 0))
|
||
rng.restore_adventure(master_seed, int(bp.get("rng_counter", 0)))
|
||
ledger = {"hero_hp": int(bp.get("hero_hp", 0)), "gp": int(bp.get("gp", 0))}
|
||
var bp_bonus: Dictionary = bp.get("bonus", {}) as Dictionary
|
||
bonus = {"atk": maxi(0, int(bp_bonus.get("atk", 0))),
|
||
"max_hp": maxi(0, int(bp_bonus.get("max_hp", 0))),
|
||
"armor_class": maxi(0, int(bp_bonus.get("armor_class", 0))),
|
||
"ap": maxi(0, int(bp_bonus.get("ap", 0)))}
|
||
purchased = (bp.get("shop_purchased", {}) as Dictionary).duplicate(true)
|
||
keystone = int(bp.get("keystone", 0))
|
||
summary = {}
|
||
levels_total = db.level_count() # 表访问收口(2026-10-02——行集直扫退役)
|
||
level_index = int(bp.get("level_index", 1))
|
||
_pending_next_level = false
|
||
_reset_curio_state()
|
||
var bp_taken: Dictionary = bp.get("bonus_taken", {}) as Dictionary
|
||
for id in _curio_taken.keys():
|
||
_curio_taken[id] = maxi(0, int(bp_taken.get(id, 0)))
|
||
for id in bp_taken.keys():
|
||
_curio_taken[String(id)] = maxi(0, int(bp_taken.get(id, 0))) # 迭代 20 T2——身份维键(relic:<feat_id>)随快照还原
|
||
# 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径;
|
||
# 迭代 12 T1——relic 令牌 feat_id 随还原)
|
||
var bp_deck = bp.get("bonus_deck", null)
|
||
if bp_deck is Array:
|
||
var restored: Array = []
|
||
for t in (bp_deck as Array):
|
||
var tok: Dictionary = t as Dictionary
|
||
var rtok := {"kind": String(tok.get("kind", "")), "tier": String(tok.get("tier", ""))}
|
||
if String(rtok["kind"]) == "relic":
|
||
rtok["feat_id"] = String(tok.get("feat_id", ""))
|
||
restored.append(rtok)
|
||
_curio_deck = restored
|
||
# 特性持有集还原(迭代 12 T1——缺键 = 旧版断点防御空集)
|
||
_feats = []
|
||
for r in (bp.get("relics", []) as Array):
|
||
_feats.append(String(r))
|
||
_keystone_drops = (bp.get("keystone_drops", []) as Array).duplicate(true)
|
||
_base_override = _normalize_override(params.get("hero_override", {}))
|
||
# 生命账本越界钳制(T17——spawn 守卫 EC16 免疫):断点可能由旧口径版本写出(如调试上限
|
||
# 调高期带出的账本值 > 新口径生效上限,实例:hero_hp=28 > 基础 26)——恢复后钳到生效上限,
|
||
# 否则首战 spawn「hero_hp 入参越界」拒收 → 「敌人系统 spawn 被拒」fatal。上限 = 表值+永久
|
||
# 强化 flat+加算区 flat 后乘调试浮窗 pct(effective_max_hp 同式——此刻加算区尚未恢复,
|
||
# 钳制用 flat 已含的保守口径即可,加算区恢复后其余战斗照常)。
|
||
ledger["hero_hp"] = mini(int(ledger.get("hero_hp", 0)), maxi(1,
|
||
int(round(float(int(db.hero.max_hp) + int(_base_override.get("max_hp", 0))) * (100.0 + float(_override[1])) / 100.0))))
|
||
_first_script = false # 续玩模板已随快照强制(已抽模板零重抽)——脚本模式不重放
|
||
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 迭代 11 第②批 T3——续玩参数面同 start_adventure(缺省 legacy)
|
||
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
|
||
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive(迭代 11 第①批 T5)
|
||
_attune_refresh_y = maxi(0, int(params.get("attune_refresh_y", 0))) # additive(迭代 11 第①批 T5b——Y 冒险周期池)
|
||
_seed_attune_charges() # 迭代 11 第①批 T6——冒险开始充能账本按装备主动件重置(每冒险充能语义)
|
||
_tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # 迭代 12 T10——A/B 面 rule 键集(尾裁微批b 2026-10-08 卡⓪受理闭环:续玩参数面同 start_adventure :135 同式,消费面 set_tree_rules 下传现成零改)
|
||
# 三选一停点续玩分支(迭代 10——选择态/已兑付态不重跑关卡;奖励选择态不重抽、已兑付不重付)
|
||
var bp_pending: Dictionary = bp.get("bonus_pending", {}) as Dictionary
|
||
_feat_ticket_pity = int(bp.get("feat_ticket_pity", 0)) # T5 扩列——旧档缺键缺省 0(存档兼容)
|
||
_feat_ticket_sold = int(bp.get("feat_ticket_sold", 0))
|
||
# 迭代 20 T6/T7——adventure 域计数器回填(缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先)
|
||
var bp_counters = bp.get("adventure_counters", null)
|
||
if bp_counters is Dictionary:
|
||
adventure_counters = (bp_counters as Dictionary).duplicate(true)
|
||
_refresh_stock = maxi(0, int(bp.get("refresh_stock", 0))) # 迭代 20 T4/T7——旧档缺键缺省 0
|
||
vision_counter = {"used": maxi(0, int(bp.get("vision_counter_used", 0)))} # 迭代 22 秘宝批 T3——真视计数随断点回填(缺键=旧断点缺省 0)
|
||
_battery_pending = maxi(0, int(bp.get("battery_pending", 0)))
|
||
var bp_result: Dictionary = bp.get("bonus_result", {}) as Dictionary
|
||
if not bp_pending.is_empty():
|
||
_pending_next_level = true
|
||
# 候选令牌化迁移(TP0——旧版断点候选为 kind 字符串,按普通档令牌防御归一;单版本周期口径)
|
||
var migrated: Array = []
|
||
for c in (bp_pending.get("candidates", []) as Array):
|
||
if c is Dictionary:
|
||
# T4——feat_id 随迁移(通关渠道候选=纯专长令牌,身份键 relic|tier|feat_id 三段全要)
|
||
var mig := {"kind": String((c as Dictionary).get("kind", "")), "tier": String((c as Dictionary).get("tier", ""))}
|
||
if String(mig["kind"]) == "relic":
|
||
mig["feat_id"] = String((c as Dictionary).get("feat_id", ""))
|
||
migrated.append(mig)
|
||
else:
|
||
migrated.append({"kind": String(c), "tier": db.adventure.TIER_COMMON})
|
||
_curio_pending = {"candidates": migrated}
|
||
var payload: Dictionary = (bp_pending.get("payload", {}) as Dictionary).duplicate(true)
|
||
state = "running"
|
||
curio_choice_presented.emit(payload)
|
||
return true
|
||
if not bp_result.is_empty():
|
||
_pending_next_level = true
|
||
_curio_result = bp_result.duplicate(true)
|
||
state = "running"
|
||
curio_resolved.emit(_curio_result.duplicate(true))
|
||
return true
|
||
_resume_bp = bp.duplicate(true)
|
||
# 迭代 11 第①批 T6——充能账本随断点回局(缺键 = 旧档/无断点,保持 _seed_attune_charges 种子态)
|
||
var bp_charges = bp.get("attunement_charges", null)
|
||
if bp_charges is Dictionary:
|
||
attune_charges = (bp_charges as Dictionary).duplicate(true)
|
||
state = "running"
|
||
return _start_current_level()
|
||
|
||
|
||
# 三选一停点态归零(冒险开始/结算收束共用;令牌池按表配额重建——池消耗即上限,账本随池重生。
|
||
# 迭代 20 T6——adventure 域计数器账本随冒险开始清零;resume 路径调用后由断点附随件回填)
|
||
func _reset_curio_state() -> void:
|
||
_curio_taken = {"attack": 0, "max_hp": 0, "armor": 0, "gp": 0, "ap": 0, "heal": 0}
|
||
_curio_deck = _build_curio_deck()
|
||
_feats = []
|
||
_curio_pending = {}
|
||
_curio_result = {}
|
||
_keystone_drops = []
|
||
_node_curio_pending = {}
|
||
adventure_counters = {}
|
||
_refresh_stock = 0
|
||
_pending_rap = 0
|
||
_battery_pending = 0
|
||
_time_sand_queue = 0
|
||
_pending_insurance = 0
|
||
|
||
|
||
# 令牌池构建(TP0 v2.2——按表 kind×tier 配额展开为持久多重集;构建序确定 = CURIO_KINDS×TIERS 逐层,
|
||
# 随机性只发生在洗牌消费 reward_pick 封闭流的时刻。迭代 12 T1:规则改写型特性带=relic 活行每件 1 张追加。
|
||
# 迭代 20 T2/T3 四级换代——relic 令牌档 = 行 rarity 直映射(四级词表内直用;旧三档值 transitional
|
||
# 回退映射 rarest→rare/其余→excellent,T3 行换代后回退分支自然休眠);relic 入普通档随新档位框架
|
||
# 解禁〔旧「永不入普通档」系 TP0 三档时代约束,四级框架未沿用——奇物.md 池结构节无此限制〕)
|
||
func _build_curio_deck() -> Array:
|
||
var deck: Array = []
|
||
for kind in db.adventure.CURIO_KINDS:
|
||
for tier in db.adventure.TIERS:
|
||
for i in int(db.adventure.curio_quota(String(kind), String(tier))):
|
||
deck.append({"kind": String(kind), "tier": String(tier)})
|
||
for row in db.feat_rows():
|
||
var rraw := String(row.get("rarity", ""))
|
||
var rtier := rraw if db.adventure.TIERS.has(rraw) else ("rare" if rraw == "rarest" else "excellent")
|
||
deck.append({"kind": "relic", "tier": rtier, "feat_id": String(row.get("feat_id", ""))})
|
||
return deck
|
||
|
||
|
||
# 令牌唯一身份(抽签去重/兑付匹配共用——属性加成型特性=kind,规则改写型特性=feat_id;同档多张不同 relic 可同屏)
|
||
func _token_identity(tok: Dictionary) -> String:
|
||
if String(tok.get("kind", "")) == "relic":
|
||
return "relic:" + String(tok.get("feat_id", ""))
|
||
return String(tok.get("kind", ""))
|
||
|
||
|
||
# 调试浮窗百分比通道(测试工具覆盖——每关新 runner 都要应用;正式侧归元进度)
|
||
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
||
_override = [atk_pct, hp_pct]
|
||
if _runner != null:
|
||
_runner.set_hero_override(atk_pct, hp_pct)
|
||
|
||
|
||
# 冒险账本侧生效上限(T17 修复——账本置满/回血封顶与战斗内上限同式同源):
|
||
# (表值 + 永久强化 flat + 加算区 flat)后乘调试浮窗百分比——叠加次序与 LevelRunner.effective_max_hp
|
||
# 逐字一致。旧口径两处只算 flat 不乘 pct:调生命上限 26→104 时开局血仍 26(账本 26 / 战斗内 104)、
|
||
# 通关 20% 回血封顶按 26 算。pct=0(模拟/冒烟默认)时乘法恒等——MC 锚零漂移。
|
||
func effective_max_hp() -> int:
|
||
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(_override[1])) / 100.0)))
|
||
|
||
|
||
# 断点承载注入(迭代 8 卡①——组合根冒险开始时调用;模拟/冒烟不注入 = 断点面整体静默)
|
||
func set_save_manager(sm) -> void:
|
||
_save_manager = sm
|
||
|
||
|
||
# 当前关卡行进器读面(呈现层/冒烟用——每关新建实例,调用方须在 level_started 后重取)
|
||
func runner() -> LevelRunner:
|
||
return _runner
|
||
|
||
|
||
# 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶)。算术宿主 = LevelRunner.rest_heal
|
||
#(2026-10-02 修复批——同式双落点字面量退役,与 LevelRunner.rest_heal_amount 同一单点)
|
||
func apply_rest(pct: int, hp: int, max_hp: int) -> int:
|
||
return LevelRunner.rest_heal(pct, hp, max_hp)
|
||
|
||
|
||
# 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;三选一未兑付拒绝推进)
|
||
func continue_to_next_level() -> bool:
|
||
if not _pending_next_level:
|
||
push_error("[冒险结构] continue_to_next_level 拒绝——无待确认的关卡推进")
|
||
return false
|
||
if not _curio_pending.is_empty():
|
||
push_error("[冒险结构] continue_to_next_level 拒绝——三选一停点未兑付(零自动推进契约)")
|
||
return false
|
||
_pending_next_level = false
|
||
_curio_result = {}
|
||
level_index += 1
|
||
return _start_current_level()
|
||
|
||
|
||
# 停点空候选消化(2026-10-06 用户令——池空=三选一环节整体跳过:候选 0 件时停点视同已处理,
|
||
# 呈现层跳开选择面直接走后续推进。仅空候选合法;非空停点拒绝——零自动推进契约不破,正常
|
||
# 消化仍只走 pick_curio/apply_node_curio 兑付。触发源=渠道白名单过滤后池空(关卡 Boss 通关
|
||
# 渠道只出专长,专长零配置/旧档 deck 无 relic 余库即空集)。)
|
||
func skip_empty_curio() -> bool:
|
||
if _curio_pending.is_empty():
|
||
return false
|
||
if not (_curio_pending.get("candidates", []) as Array).is_empty():
|
||
return false
|
||
_curio_pending = {}
|
||
return true
|
||
|
||
|
||
# ---- 提升三选一(迭代 10 奖励重构批——选择停点零自动推进、重复兑付被拒;兑付恰一件、未选项零生效)----
|
||
|
||
# 候选令牌匹配(身份键恒等——属性加成型特性 kind+tier、规则改写型特性 feat_id;迭代 12 T1 规则改写型特性带)
|
||
func _find_token(candidates: Array, kind: String, tier: String, feat_id: String = "") -> Dictionary:
|
||
for t in candidates:
|
||
var tok: Dictionary = t as Dictionary
|
||
if String(tok.get("kind", "")) != kind or String(tok.get("tier", "")) != tier:
|
||
continue
|
||
if kind == "relic" and String(tok.get("feat_id", "")) != feat_id:
|
||
continue
|
||
return tok
|
||
return {}
|
||
|
||
|
||
# 玩家点选兑付(呈现层/冒烟/自动模拟确认入口):令牌 ∈ 停点候选(身份键精确匹配——relic 件
|
||
# 另须 feat_id)。属性件写加算区、AP 件写第四轴、GP 件走钱包「奖励入账」入口、恢复件当即回血、
|
||
# relic 件入特性持有集(🔴 层级纪律:只走局内随机渠道)、道具件受同调位约束(随池种退役不可达)。
|
||
# 消费=选中令牌踢池(未选回池由「抽签不摘牌、兑付才摘牌」语义承载——TP0 v2.2)。
|
||
func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
|
||
if state != "running" or _curio_pending.is_empty():
|
||
push_error("[冒险结构] pick_curio 拒绝——无待选停点(state=%s)" % state)
|
||
return false
|
||
var candidates: Array = _curio_pending.get("candidates", []) as Array
|
||
if _find_token(candidates, kind, tier, feat_id).is_empty():
|
||
push_error("[冒险结构] pick_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id])
|
||
return false
|
||
if kind == "feat_ticket":
|
||
return _redeem_feat_ticket() # T5 拿到即开——停点候选替换为专长三选一(零摘牌/不进持有集)
|
||
var value: int = int(db.adventure.curio_value(kind, tier))
|
||
var hp_before := int(ledger.get("hero_hp", 0))
|
||
if not _apply_curio_pick(kind, tier, value, feat_id):
|
||
push_error("[冒险结构] pick_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
|
||
return false
|
||
_finish_curio_pick(kind, value, hp_before, tier, feat_id)
|
||
return true
|
||
|
||
|
||
# 兑付载荷 relic 伴随键(迭代 12 T1——反馈行取数面;非 relic 件零新增键)
|
||
func _feat_payload_keys(feat_id: String) -> Dictionary:
|
||
if feat_id == "":
|
||
return {}
|
||
var row: Dictionary = db.feat_row(feat_id)
|
||
return {"feat_id": feat_id, "feat_name": String(row.get("display_name", "")), "feat_archetype": String(row.get("archetype", ""))}
|
||
|
||
|
||
# 三选一件兑付共享体(2026-10-02 修复批双胞胎收口;TP0 改令牌消费——摘牌单点;迭代 12 T1 relic 分支):
|
||
# 属性件写加算区 + 件获取计数、AP 件写第四轴、GP 件走钱包、恢复件当即回血(封顶生效上限——
|
||
# 与账本置满/通关回血同式 effective_max_hp)、relic 件入持有集(散件单体保底——效果接线面
|
||
# 按挂点消费,档位质变由 FeatsEngine 求值)。兑付即从 _curio_deck 摘除该令牌(选中踢池;
|
||
# 未选候选从未摘牌=自动回池)。道具件不入此体——未知件返回 false 由调用方按渠道名报错。
|
||
func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = "") -> bool:
|
||
if kind == "relic" and db.feat_row(feat_id).is_empty():
|
||
push_error("[冒险结构] relic 兑付失败——表无此件: %s" % feat_id)
|
||
return false # 先校验后摘牌(令牌零丢失)
|
||
if not _consume_deck_token(kind, tier, feat_id):
|
||
push_error("[冒险结构] 令牌消费失败——牌库无此令牌: %s|%s|%s" % [kind, tier, feat_id])
|
||
return false
|
||
if kind == "refresh":
|
||
_refresh_stock += value # T4 F-1 修复——刷新券入刷新存量账(冒险级;**退役 _attune_refresh_x 写口**:X 每停点 _reset_refresh_x 重置会吞存量=写错账缺陷正主;摘牌先于落账=选中即消尽,旧早退路径零摘牌缺陷同批修正)
|
||
_curio_taken["refresh"] = int(_curio_taken.get("refresh", 0)) + 1
|
||
return true
|
||
if kind == "rap":
|
||
_pending_rap += value # T4——储备金袋:下一战 RAP +N(节点级,战首注入后清零)
|
||
_curio_taken["rap"] = int(_curio_taken.get("rap", 0)) + 1
|
||
return true
|
||
if kind == "attune_battery":
|
||
_battery_pending += value # T4——同调电池:待分配充能(choose_battery_target 玩家点选消费——禁自动替选)
|
||
_curio_taken["attune_battery"] = int(_curio_taken.get("attune_battery", 0)) + 1
|
||
return true
|
||
if kind == "time_sand":
|
||
_time_sand_queue += value # T4——时沙:下一战首轮 AP +N(回合级池增量,不改上限)
|
||
_curio_taken["time_sand"] = int(_curio_taken.get("time_sand", 0)) + 1
|
||
return true
|
||
if kind == "perfect_insurance":
|
||
_pending_insurance += value # T4——完美保险:下一战标错豁免 N 次(节点级,未用即过期)
|
||
_curio_taken["perfect_insurance"] = int(_curio_taken.get("perfect_insurance", 0)) + 1
|
||
return true
|
||
if kind == "relic":
|
||
if not _feats.has(feat_id):
|
||
_feats.append(feat_id)
|
||
_curio_taken["relic"] = int(_curio_taken.get("relic", 0)) + 1
|
||
_curio_taken["relic:" + feat_id] = int(_curio_taken.get("relic:" + feat_id, 0)) + 1 # 迭代 20 T2 案②——身份维已拿计数(cap 消费面)
|
||
return true
|
||
# 加算形态路由(内容数据驱动扩展批 T5——match 收敛进效果类型:属性值提升四类走
|
||
# adventure_table.CURIO_APPLY_ROUTES 声明路由(bonus 落点键),加新加算类令牌零代码)
|
||
var sink := db.adventure.curio_apply_route(kind)
|
||
if sink != "":
|
||
bonus[sink] = int(bonus.get(sink, 0)) + value
|
||
_curio_taken[kind] = int(_curio_taken.get(kind, 0)) + 1
|
||
return true
|
||
# 通道面(副作用形态——显式分支)
|
||
match kind:
|
||
"gp":
|
||
if _runner != null:
|
||
_runner.grant_curio_gp(value)
|
||
_curio_taken["gp"] = int(_curio_taken.get("gp", 0)) + 1
|
||
"heal":
|
||
ledger["hero_hp"] = mini(int(ledger.get("hero_hp", 0)) + value, effective_max_hp())
|
||
_curio_taken["heal"] = int(_curio_taken.get("heal", 0)) + 1
|
||
_:
|
||
return false
|
||
return true
|
||
|
||
|
||
# 从牌库摘除一枚令牌(TP0——选中踢池单点;迭代 12 T1 relic 按 feat_id 精确摘;找不到 = false)
|
||
func _consume_deck_token(kind: String, tier: String, feat_id: String = "") -> bool:
|
||
for i in _curio_deck.size():
|
||
var tok: Dictionary = _curio_deck[i] as Dictionary
|
||
if String(tok.get("kind", "")) != kind or String(tok.get("tier", "")) != tier:
|
||
continue
|
||
if kind == "relic" and String(tok.get("feat_id", "")) != feat_id:
|
||
continue
|
||
_curio_deck.remove_at(i)
|
||
return true
|
||
return false
|
||
|
||
|
||
# 特性持有集读面(效果接线/求值/摘要唯一取数口——副本防外部改写)
|
||
func feats_held() -> Array:
|
||
return _feats.duplicate()
|
||
|
||
|
||
# ---- 商店共享池消费面(迭代 11 第②批 T3——P1 共享池单点账本;TP0 令牌池换代——库存=牌库剩余令牌)----
|
||
|
||
# 库存余量(TP0:stock = 牌库内该 kind 剩余令牌数——「池消耗即上限」,种类次数上限退役;
|
||
# 奖励三选一兑付摘牌 与 商店购买消费 由同一 _curio_deck 承载——两渠道跨关共库,全冒险一次性)。
|
||
func pool_stock(kind: String) -> int:
|
||
var n := 0
|
||
for t in _curio_deck:
|
||
if String((t as Dictionary).get("kind", "")) == kind:
|
||
n += 1
|
||
return n
|
||
|
||
|
||
# 商店购买消费 handler(LevelRunner.buy_pool_item 经 Callable 调用——ADR-0001 S6 方法引用):
|
||
# 消费在架令牌本体(kind+tier 精确——货架卡即令牌,TP0 令牌级货架)→ _apply_curio_pick 同口
|
||
# 写加算区+账本+摘牌(与奖励三选一真单点——买一件=奖励面少一件的库存语义由同库成立)。
|
||
# 未知 kind 拒绝(货架词表封闭的前置防御)。
|
||
func _consume_pool_purchase(kind: String, tier: String) -> bool:
|
||
if kind == "feat_ticket":
|
||
return _redeem_feat_ticket() # T5——门票买下即开(零摘牌不进持有集;临时停点开专长三选一面)
|
||
if not db.adventure.CURIO_KINDS.has(kind):
|
||
push_error("[冒险结构] 商店购买消费拒绝——件不在池词表: %s" % kind)
|
||
return false
|
||
return _apply_curio_pick(kind, tier, int(db.adventure.curio_value(kind, tier)))
|
||
|
||
|
||
# 终局池快照(_settle 清账前调用——_reset_curio_state 会重置 _curio_taken,摘要 additive 键
|
||
# pool_stock 须在此前取数;货架四种 stock=cap−taken,读数面 = 商店买走 vs 奖励领走 vs 枯竭)
|
||
func _pool_stock_snapshot() -> Dictionary:
|
||
var out := {}
|
||
for row in db.shop_pool_rows():
|
||
var kind := String(row.get("kind_id", ""))
|
||
out[kind] = pool_stock(kind)
|
||
return out
|
||
|
||
|
||
# 兑付收口(恰一次):清停点态 → 记已兑付态 → emit → 停点快照(等继续确认——中断续玩不重兑付)。
|
||
# picked = kind;tier 随载荷(TP0——大小档位读数面);relic 伴随键随载荷(迭代 12 T1——反馈行取数)
|
||
func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String = "", feat_id: String = "") -> void:
|
||
_curio_pending = {}
|
||
_curio_result = {
|
||
"picked": picked,
|
||
"tier": tier,
|
||
"value": value,
|
||
"hp_before": hp_before,
|
||
"hp_after": int(ledger.get("hero_hp", 0)),
|
||
"level_index": level_index,
|
||
}
|
||
_curio_result.merge(_feat_payload_keys(feat_id))
|
||
curio_resolved.emit(_curio_result.duplicate(true))
|
||
_snapshot_breakpoint()
|
||
|
||
|
||
# ---- 奖励节点三选一(迭代 10 追加批——用户拍板:与通关三选一共享同池同权重同次数上限)----
|
||
|
||
# 候选呈现(节点到达时调用——恰一 reward_pick scope 洗牌 + 道具件掷定 scope;本停点断点不载,
|
||
# 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)。
|
||
func node_curio_candidates() -> Dictionary:
|
||
var cands: Array = _draw_curio_candidates("reward_node")
|
||
return {"candidates": cands}
|
||
|
||
|
||
func present_node_curio(candidates: Array) -> void:
|
||
_reset_refresh_x() # 迭代 11 第①批 T5b——碰三选一重置 X 余量(不结转)
|
||
_curio_paid_refreshes = 0 # 付费刷计数随停点清零(对齐商店进店清零——2026-10-06)
|
||
_node_curio_pending = {"candidates": candidates.duplicate(true)}
|
||
|
||
|
||
# 点选兑付(节点域——无部分回血无钥石;令牌消费与牌库摘牌与通关三选一同库;relic 同权入持有集)
|
||
func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
|
||
if kind == "feat_ticket":
|
||
return _redeem_feat_ticket() # T5 拿到即开——节点停点候选替换为专长三选一(呈现层本地重绘)
|
||
if _node_curio_pending.is_empty():
|
||
push_error("[冒险结构] apply_node_curio 拒绝——无待选停点")
|
||
return false
|
||
var candidates: Array = _node_curio_pending.get("candidates", []) as Array
|
||
if _find_token(candidates, kind, tier, feat_id).is_empty():
|
||
push_error("[冒险结构] apply_node_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id])
|
||
return false
|
||
var value: int = int(db.adventure.curio_value(kind, tier))
|
||
if not _apply_curio_pick(kind, tier, value, feat_id):
|
||
push_error("[冒险结构] apply_node_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id])
|
||
return false
|
||
_finish_node_bonus(kind, value, tier, feat_id)
|
||
return true
|
||
|
||
|
||
# 冒险开始充能账本重置(T6——装备 active_charged 件各回 charges_per_adventure;被动件不入账)
|
||
func _seed_attune_charges() -> void:
|
||
attune_charges = {}
|
||
for iid in _attunement:
|
||
var arow: Dictionary = db.treasure_item_row(String(iid))
|
||
if String(arow.get("form", "")) == "active_charged":
|
||
attune_charges[String(iid)] = maxi(0, int(arow.get("charges_per_adventure", 0)))
|
||
|
||
|
||
# ---- 三选一刷新(迭代 11 第①批 T5b——P1 甲 X/Y 双源:X=刷新件每碰三选一重给〔优先消耗、不结转〕、
|
||
# Y=营地升级行冒险周期池〔用完即无〕;重抽消费 reward_pick 既有洗牌流,池规则全保留——不放回/
|
||
# 达上限剔牌/掷定前置直显;停点可中断)----
|
||
|
||
func _attune_refresh_x_total() -> int:
|
||
# 内容数据驱动扩展批 T4——按 id 特判拆除:kind 分发查询(装配集内 effect_kinds 含 bonus_refresh
|
||
# 的行 magnitude 合计;刷新件类加件零 id 分支)
|
||
var total := 0
|
||
for iid_v in _attunement:
|
||
var arow: Dictionary = db.treasure_item_row(String(iid_v))
|
||
if (arow.get("effect_kinds", []) as Array).has("bonus_refresh"):
|
||
total += int(arow.get("magnitude", 0))
|
||
return total
|
||
|
||
|
||
func _reset_refresh_x() -> void:
|
||
_attune_refresh_x = _attune_refresh_x_total()
|
||
|
||
|
||
# 当前可刷新次数(X 优先 + Y 池 + 刷新存量账——呈现层余量读数口;T4 F-1 修复扩第三源)
|
||
func attune_refresh_left() -> int:
|
||
return _attune_refresh_x + _attune_refresh_y + _refresh_stock
|
||
|
||
|
||
# 刷新存量账读数(T4——刷新券加仓余量单独读面;呈现层与断点消费)
|
||
func refresh_stock() -> int:
|
||
return _refresh_stock
|
||
|
||
|
||
# 同调电池充能分配(T4——玩家点选哪件已装配主动秘宝 +N;**禁自动替选**:
|
||
# 无待分配/件不在装配集/非主动充能件=拒绝零副作用)
|
||
func choose_battery_target(item_id: String) -> bool:
|
||
if _battery_pending <= 0:
|
||
push_error("[冒险结构] 电池分配拒绝——无待分配充能")
|
||
return false
|
||
if not _attunement.has(item_id):
|
||
push_error("[冒险结构] 电池分配拒绝——件不在装配集: %s" % item_id)
|
||
return false
|
||
var arow: Dictionary = db.treasure_item_row(item_id)
|
||
if String(arow.get("form", "")) != "active_charged":
|
||
push_error("[冒险结构] 电池分配拒绝——非主动充能件: %s" % item_id)
|
||
return false
|
||
_battery_pending -= 1
|
||
attune_charges[item_id] = int(attune_charges.get(item_id, 0)) + 1
|
||
return true
|
||
|
||
|
||
# 待分配充能读数(呈现层电池选件面数据源)
|
||
func battery_pending() -> int:
|
||
return _battery_pending
|
||
|
||
|
||
# 节点停点在途态读回(组合根重 present 用——candidates 值拷贝)
|
||
func node_curio_pending_state() -> Dictionary:
|
||
return {
|
||
"candidates": (_node_curio_pending.get("candidates", []) as Array).duplicate(true),
|
||
}
|
||
|
||
|
||
# 三选一刷新(免费位 X/Y——组合根唯一调用口;无停点/无余量 false 零副作用)。通关停点重抽后重发
|
||
# curio_choice_presented(呈现层重建卡片+刷新余量);节点停点由组合根读 node_curio_pending_state 重 present。
|
||
func refresh_curio() -> bool:
|
||
return _refresh_curio_common(false)
|
||
|
||
|
||
# 三选一 GP 付费刷新(2026-10-06 用户令对齐商店 MC-1——免费位 X/Y 用尽后转付费,同旋钮同曲线同钱包;
|
||
# 组合根先经 curio_stop_pending+钱包支出再调,本口只做计数与重抽)。池规则全保留——不放回/达上限剔牌/掷定前置直显。
|
||
func refresh_curio_paid() -> bool:
|
||
return _refresh_curio_common(true)
|
||
|
||
|
||
# 本次 GP 付费刷新价(与商店货架刷新同源:ceil(基价 × 曲线^本次停点已付费刷)——读同两个调参旋钮,单点调参)
|
||
func curio_refresh_gp_cost() -> int:
|
||
return int(ceil(float(db.adventure.shop_refresh_cost_base)
|
||
* pow(db.adventure.shop_refresh_cost_curve, float(_curio_paid_refreshes))))
|
||
|
||
|
||
# 三选一停点在途谓词(付费路径扣费前置校验——无停点禁扣钱,防空转烧钱包)
|
||
func curio_stop_pending() -> bool:
|
||
return not _curio_pending.is_empty() or not _node_curio_pending.is_empty()
|
||
|
||
|
||
func _refresh_curio_common(paid: bool) -> bool:
|
||
if not curio_stop_pending():
|
||
return false
|
||
if paid:
|
||
_curio_paid_refreshes += 1
|
||
else:
|
||
# 免费位消耗序:X(刷新件每停点重给,优先消耗)→ Y(营地冒险周期池)→ 存量账(刷新券加仓,
|
||
# 迭代 20 T4 F-1 修复新账——跨停点持久)
|
||
if _attune_refresh_x <= 0 and _attune_refresh_y <= 0 and _refresh_stock <= 0:
|
||
return false
|
||
if _attune_refresh_x > 0:
|
||
_attune_refresh_x -= 1
|
||
elif _attune_refresh_y > 0:
|
||
_attune_refresh_y -= 1
|
||
else:
|
||
_refresh_stock -= 1
|
||
if not _curio_pending.is_empty():
|
||
var cands: Array = _draw_curio_candidates("level_clear")
|
||
_curio_pending["candidates"] = cands.duplicate(true)
|
||
var payload: Dictionary = (_curio_pending.get("payload", {}) as Dictionary).duplicate(true)
|
||
payload["candidates"] = cands
|
||
payload["refreshed"] = true # 重发载荷带标——呈现层直落选择面,不再重开胜利总结面(2026-10-06)
|
||
curio_choice_presented.emit(payload) # 同信号重 present(零自动推进契约不破——停点仍在途)
|
||
_snapshot_breakpoint()
|
||
return true
|
||
if not _node_curio_pending.is_empty():
|
||
var cands2: Array = _draw_curio_candidates("reward_node")
|
||
_node_curio_pending["candidates"] = cands2.duplicate(true)
|
||
return true
|
||
return false
|
||
|
||
|
||
func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id: String = "") -> void:
|
||
_node_curio_pending = {}
|
||
var payload := {
|
||
"context": "reward_node",
|
||
"picked": picked,
|
||
"tier": tier,
|
||
"value": value,
|
||
"level_index": level_index,
|
||
}
|
||
payload.merge(_feat_payload_keys(feat_id))
|
||
curio_resolved.emit(payload)
|
||
|
||
|
||
# 令牌池洗牌不放回抽 3(TP0 v2.2——Fisher-Yates 全库洗牌后按序收集;**同 kind 去重每 kind 至多
|
||
# 一枚**〔先到先得,档位取该 kind 首枚——「三选一=三种不同选项」体验延续,与货架抽签同规则〕;
|
||
# kind 出现率∝剩余令牌数〔档位配额即分布——无独立权重 knob〕;抽签不摘牌、兑付才摘牌=未选
|
||
# 自动回池;池耗即枯竭——余库不足 N kind 时出齐剩余,空库 = 零候选合法态)。候选 = 令牌字典
|
||
# {"kind","tier"}。消费 reward_pick 封闭流——一次停点恰一 scope,同种子同候选。
|
||
# 渠道白名单过滤(迭代 15 卡② T4——渠道轴定位:关底 Boss 击败=通关渠道只出专长(feat 令牌);
|
||
# 奖励节点/商店只出属性值提升+资源(非 relic 令牌),专长在非 Boss 渠道只以门票形态出现(T5)。
|
||
# 过滤式而非物理拆池:deck 物理结构/存档键 bonus_deck 混编余库/消耗语义(选中踢池未选回池)原样,
|
||
# 白名单在候选视图层生效——配额结构锚 TIER_QUOTAS 70/25/3=98 零改动(专长令牌本系活行追加面,在锚外)。
|
||
# 迭代 20 T2 案D 换代——曝光槽:非 Boss 渠道三选一 = **2 格全池 + 1 格优秀档以上子池**
|
||
# (rank≥excellent;子池空该格回退全池;池空直通同族兜底);凭证(feat_ticket)不进子池
|
||
# 〔独立计数体系〕;已拿上限 cap(案②提前退役):taken≥cap 的身份在候选视图剔除〔牌库本体不动,
|
||
# 拿了才计、刷出不计——_curio_cap_ok〕。
|
||
func _draw_curio_candidates(channel: String = "reward_node") -> Array:
|
||
var pool := _curio_deck.duplicate()
|
||
if channel == "level_clear":
|
||
pool = pool.filter(func(tok): return String((tok as Dictionary).get("kind", "")) == "relic")
|
||
else:
|
||
pool = pool.filter(func(tok):
|
||
var k := String((tok as Dictionary).get("kind", ""))
|
||
return k != "relic" and k != "feat_ticket")
|
||
pool = pool.filter(func(tok):
|
||
var t: Dictionary = tok as Dictionary
|
||
return _curio_cap_ok(String(t.get("kind", "")), String(t.get("feat_id", ""))))
|
||
var rg: RandomNumberGenerator = rng.open_stream("reward_pick")
|
||
if channel == "level_clear":
|
||
# 迭代 20 T5——Boss 渠道按档配比加权抽取(无放回加权抽:逐席按 boss_feat_weight 掷点,
|
||
# 身份去重照旧;rarity 语义约束兑付面——奇物.md v3.1,配比数值归调参批)
|
||
var out_w: Array = []
|
||
var seen_w: Array = []
|
||
var want_w := int(db.adventure.curio_candidate_count)
|
||
while out_w.size() < want_w and not pool.is_empty():
|
||
var total_w := 0
|
||
for tok_w in pool:
|
||
total_w += db.adventure.boss_feat_weight(String((tok_w as Dictionary).get("tier", "")))
|
||
if total_w <= 0:
|
||
break
|
||
var roll_w := rg.randi_range(1, total_w)
|
||
var pick_i := 0
|
||
for i_w in pool.size():
|
||
roll_w -= db.adventure.boss_feat_weight(String((pool[i_w] as Dictionary).get("tier", "")))
|
||
if roll_w <= 0:
|
||
pick_i = i_w
|
||
break
|
||
var tok_p: Dictionary = pool[pick_i]
|
||
var ident_p := _token_identity(tok_p)
|
||
if seen_w.has(ident_p):
|
||
pool.remove_at(pick_i) # 同身份已出——剔牌重掷(无放回)
|
||
continue
|
||
seen_w.append(ident_p)
|
||
var cand_p := {"kind": String(tok_p.get("kind", "")), "tier": String(tok_p.get("tier", "")), "feat_id": String(tok_p.get("feat_id", ""))}
|
||
out_w.append(cand_p)
|
||
pool.remove_at(pick_i)
|
||
return out_w
|
||
for i in pool.size():
|
||
var j := rg.randi_range(i, pool.size() - 1)
|
||
var tmp = pool[i]
|
||
pool[i] = pool[j]
|
||
pool[j] = tmp
|
||
# 子池(优秀档以上)独立洗牌——同 stream 续消费,确定性保持
|
||
var sub := pool.filter(func(tok):
|
||
return db.adventure.tier_rank(String((tok as Dictionary).get("tier", ""))) >= db.adventure.tier_rank(db.adventure.TIER_EXCELLENT))
|
||
for i in sub.size():
|
||
var j2 := rg.randi_range(i, sub.size() - 1)
|
||
var tmp2 = sub[i]
|
||
sub[i] = sub[j2]
|
||
sub[j2] = tmp2
|
||
var out: Array = []
|
||
var seen: Array = []
|
||
# 收集器:全池序取 n_full 格(普通档打底)→ 子池序取 1 格(曝光槽)→ 子池空回退全池续取
|
||
var want := int(db.adventure.curio_candidate_count)
|
||
var n_full := maxi(1, want - 1) if want >= 2 else want
|
||
while out.size() < want:
|
||
var src: Array = pool
|
||
if out.size() >= n_full and not sub.is_empty():
|
||
src = sub # 曝光槽位——优先子池
|
||
var placed := false
|
||
for tok in src:
|
||
var ident := _token_identity(tok)
|
||
if seen.has(ident):
|
||
continue
|
||
seen.append(ident)
|
||
var cand := {"kind": String((tok as Dictionary).get("kind", "")), "tier": String((tok as Dictionary).get("tier", ""))}
|
||
if ident.begins_with("relic:"):
|
||
cand["feat_id"] = String((tok as Dictionary).get("feat_id", ""))
|
||
out.append(cand)
|
||
placed = true
|
||
break
|
||
if not placed:
|
||
if out.size() >= n_full and not sub.is_empty() and not pool.is_empty():
|
||
sub = [] # 子池枯竭——曝光槽回退全池续取
|
||
continue
|
||
break # 全池枯竭——出齐剩余(池空直通同族兜底)
|
||
# T5 门票注入(非 Boss 渠道唯一专长入口——替换末位候选占一席,候选数不变;判定含 pity 清零记账)
|
||
if channel == "reward_node" and not out.is_empty() and _feat_ticket_consider(rg):
|
||
out[out.size() - 1] = {"kind": "feat_ticket", "tier": "excellent"}
|
||
return out
|
||
|
||
|
||
# 已拿上限判定(迭代 20 T2 案②——身份维度:kind 走 curio_cap、relic 走 feats 行 cap 字段;
|
||
# taken 只在兑付时 +1〔拿了才计〕、刷出不计)。缺省放行=白名单外 kind 兜底。
|
||
func _curio_cap_ok(kind: String, feat_id: String = "") -> bool:
|
||
var cap := 99
|
||
if kind == "relic":
|
||
var row: Dictionary = db.feat_row(feat_id)
|
||
cap = int(row.get("cap", 1)) if not row.is_empty() else 1
|
||
return _curio_taken.get("relic:" + feat_id, 0) < cap
|
||
if not db.adventure.CURIO_KINDS.has(kind):
|
||
return true
|
||
cap = int(db.adventure.curio_cap(kind))
|
||
return int(_curio_taken.get(kind, 0)) < cap
|
||
|
||
|
||
# —— 专长获取门票(T5 迭代 15 卡②——非 Boss 渠道的专长入口;四口径契约④:拿到即开不可囤积/
|
||
# 罕见只在概率不在价格/一局限购一次/计步保底连续 5 展示未出第 6 必出——计数=展示次数、刷新
|
||
# 重展计入、节点与商店共享、触发清零、限购满不再触发。调用即一次展示判定与记账)——
|
||
func _feat_ticket_consider(rg: RandomNumberGenerator) -> bool:
|
||
if _feat_ticket_sold >= int(db.adventure.feat_ticket_limit):
|
||
return false # 限购满——不再触发(保底冻结)
|
||
if _feat_ticket_appeared >= int(db.adventure.feat_ticket_quota):
|
||
return false # 配额满——本局不再出现
|
||
var offer: bool = _feat_ticket_pity >= int(db.adventure.feat_ticket_pity_step) or rg.randi_range(1, 100) <= int(db.adventure.feat_ticket_chance)
|
||
if offer:
|
||
_feat_ticket_pity = 0
|
||
_feat_ticket_appeared += 1
|
||
else:
|
||
_feat_ticket_pity += 1
|
||
return offer
|
||
|
||
|
||
# 门票兑付(拿到即开——停点候选替换为专长三选一;不进持有集/零摘牌;限购计数 +1):
|
||
# 节点停点=本地替换(呈现层读在途态重绘);店内无停点=临时停点 + payload.context 标记(呈现层
|
||
# 重 present 无胜利总结面,确认后只关面回店)。
|
||
func _redeem_feat_ticket() -> bool:
|
||
_feat_ticket_sold += 1
|
||
_feat_ticket_pity = 0
|
||
var cands: Array = _draw_curio_candidates("level_clear") # 专长池(与通关渠道同白名单)
|
||
if cands.is_empty():
|
||
push_error("[冒险结构] 门票兑付失败——专长池空集")
|
||
_feat_ticket_sold -= 1
|
||
return false
|
||
if not _node_curio_pending.is_empty():
|
||
_node_curio_pending["candidates"] = cands.duplicate(true)
|
||
return true
|
||
var payload: Dictionary = {"context": "feat_ticket", "candidates": cands.duplicate(true)}
|
||
_curio_pending = {"candidates": cands.duplicate(true), "payload": payload.duplicate(true)}
|
||
curio_choice_presented.emit(payload)
|
||
_snapshot_breakpoint()
|
||
return true
|
||
|
||
|
||
# Boss 钥石逐杀掉落(迭代 10——渠道改 Boss/精英击杀掉落;首杀/复杀两档读 meta.tres boss_keystone;
|
||
# 首杀判据 = 存档 boss_first_kill 标记位〔断点未注入 = 恒首杀——模拟/冒烟零扰动;标记仍在收束
|
||
# apply_adventure_summary 逐通关关次置位〕;末关 Boss 照常入账;明细随摘要 additive 键供结算面归因)
|
||
func _boss_keystone_drop(li: int) -> void:
|
||
var first := true
|
||
if _save_manager != null:
|
||
first = not _save_manager.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))
|
||
_keystone_drops.append({"level": li, "first": first, "amount": amount})
|
||
keystone += amount
|
||
|
||
|
||
# 主动放弃(仅非战斗期受理——战斗域单事务在飞;与失败同型结算,已获大奖励不追回。
|
||
# MVP 无界面入口,语义先行预留;结算受理只算一次——非 running 态拒绝)
|
||
func abandon() -> bool:
|
||
if state != "running":
|
||
push_error("[冒险结构] abandon 拒绝——state=%s(非进行中不可放弃)" % state)
|
||
return false
|
||
if _runner != null and _runner.current_battle() != null:
|
||
push_error("[冒险结构] abandon 拒绝——战斗进行中(战斗域单事务在飞)")
|
||
return false
|
||
_settle("abandoned")
|
||
return true
|
||
|
||
|
||
# ---- 关卡推进(私有——序列编排单一出口)----
|
||
|
||
# 树限次计数按关重置(迭代 21 T3——危险直觉锋/守两组「每关限次」账本键:跨战持久由 adventure
|
||
# 域导出/回灌承载,此处随关清零闭合生命周期四态;键集=效果引擎树合成行声明的 counter_id;
|
||
# 独立成口=V10 断言面直呼)
|
||
func _reset_tree_limited_counters() -> void:
|
||
for cid in ["tree_sense_sharp_used", "tree_sense_guard_used"]:
|
||
adventure_counters.erase(cid)
|
||
|
||
|
||
func _start_current_level() -> bool:
|
||
_runner = LevelRunner.new(db, rng)
|
||
_reset_tree_limited_counters() # 树限次计数按关重置(V10 生命周期四态闭合——断言面直呼本口)
|
||
# core↔core 接线只许方法 Callable(ADR-0001 S6——lambda 捕获 self 会延长 RefCounted 生命周期乃至成环)
|
||
_runner.node_entered.connect(_forward_node_entered)
|
||
_runner.choices_presented.connect(_forward_choices_presented)
|
||
_runner.step_row.connect(_forward_step_row)
|
||
_runner.round_started.connect(_forward_round_started)
|
||
_runner.board_changed.connect(_forward_board_changed)
|
||
_runner.wallet_changed.connect(_forward_wallet_changed)
|
||
_runner.level_finished.connect(_on_level_finished)
|
||
_runner.set_hero_override(int(_override[0]), int(_override[1]))
|
||
# 断点续玩分支(迭代 8 卡①):模板随快照强制(已抽模板零重抽零 RNG)、resume_state 带
|
||
# 停点节点与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。
|
||
var resume_state: Dictionary = {}
|
||
var template_pick: int = 0 if _first_script else -1 # 首次脚本 = 每关固定模板 0 零抽签(v0.4 拍板点①);否则逐关抽签
|
||
if not _resume_bp.is_empty():
|
||
resume_state = {"node_id": int(_resume_bp.get("node_id", -1)),
|
||
"visited_ids": _resume_bp.get("visited_ids", []) as Array}
|
||
template_pick = int(_resume_bp.get("template_pick", -1))
|
||
# 接口①开始关卡:生命经共享账本交接(账本当前值即开局生命——冒险开始置满 / 大奖励回满已写好);
|
||
# _base_override 共享引用随载荷传入(永久强化 flat——LevelRunner.effective_* 叠加序第二项)。
|
||
# 停点前移批 2026-09-23:defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在
|
||
# 该回调重取 runner 实例),后入口候选呈现;顺序反了选路口会被呈现层的 runner 空防御静默丢弃。
|
||
# 呈现零 RNG 消耗、pick 走同一 _enter_node——测量面逐位零漂移。
|
||
var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased,
|
||
Callable(self, "apply_rest"), _base_override, true, template_pick, resume_state)
|
||
if ok:
|
||
_runner.set_attunement(_attunement) # 迭代 11 第①批 T5——同调装配下传(enc 携带进战斗结算)
|
||
_runner.set_attune_charges(attune_charges) # 迭代 11 第①批 T6——充能账本下传(能力件骑库存面注入)
|
||
_runner.set_vision_counter(vision_counter) # 迭代 22 秘宝批 T3——真视使用计数共享 dict 下传(编排器受理成功 +1)
|
||
_runner.set_feats(_feats) # 迭代 12 T1——特性持有集下传(begin_battle enc.feats 求值面)
|
||
_runner.set_tree_rules(_tree_rules) # 迭代 12 T10——树 rule 键集下传(A/B 面消费)
|
||
if _shop_mode == "pool":
|
||
# 商店共享池注入(迭代 11 第②批 T3——库存账本引用+消费 handler 双下传;legacy 模式零注入零扰动。
|
||
# 迭代 20 T2——cap 检查回调四参注入)
|
||
_runner.set_shop_pool(_curio_deck, Callable(self, "_consume_pool_purchase"), Callable(self, "_feat_ticket_consider"), Callable(self, "_curio_cap_ok"))
|
||
_runner.set_adventure_counters(adventure_counters) # 迭代 20 T6——adventure 域计数器共享引用下传
|
||
# 迭代 20 T4——凭证件三账注入(节点级/回合级一次性:注入即清零,未用即过期=寿命档语义)
|
||
_runner.set_voucher_grants(_pending_rap, _time_sand_queue, _pending_insurance)
|
||
_pending_rap = 0
|
||
_time_sand_queue = 0
|
||
_pending_insurance = 0
|
||
# 三选一道具件跨关携带注入(迭代 10)——迭代 11 第①批 T7 退役:aux 卡出池后 carry 恒空集,
|
||
# 注入面随停点渠道退役拆除(容器字段保留=替换子交互机械惰性承载,零投放零行为)
|
||
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
|
||
# 入口停点断点(node_id=-1)与 defer_entry 同态(node_index=-1+单候选)——走入口呈现;
|
||
# 中途停点(node_id>=0)走停点呈现(restore_treasure_items 先于呈现=C 区库存就位)。
|
||
if resume_state.is_empty() or int(resume_state.get("node_id", -1)) < 0:
|
||
_runner.present_entry_choice()
|
||
else:
|
||
_runner.restore_treasure_items(_resume_bp.get("aux_items", []) as Array)
|
||
_runner.present_resumed_choice()
|
||
_resume_bp = {}
|
||
return ok
|
||
|
||
|
||
# 接口④关卡结束(结果 + 生命终值 + 节点位置——胜负皆报;结算受理只算一次)
|
||
func _on_level_finished(payload: Dictionary) -> void:
|
||
if state != "running":
|
||
push_error("[冒险结构] 重复关卡结束宣布拒绝——state=%s(账本与摘要零变化)" % state)
|
||
return
|
||
_last_level_payload = payload.duplicate(true)
|
||
var result := String(payload.get("result", ""))
|
||
if result == "defeat":
|
||
ledger["hero_hp"] = 0 # 倒下记 0(生命账本战后回写口径)
|
||
_settle("failed")
|
||
return
|
||
if result != "victory":
|
||
push_error("[冒险结构] 关卡结束结果出域: %s" % result)
|
||
return
|
||
_last_treasure_cleared = int(payload.get("aux_cleared", 0)) # additive(迭代 7 T4——末关 summary 取数面)
|
||
# Boss 钥石逐杀掉落(迭代 10——渠道改击杀掉落,末关照常入账;首杀/复杀读 meta.tres boss_keystone)
|
||
_boss_keystone_drop(level_index)
|
||
if level_index < levels_total:
|
||
# 通关兑付(非末关恰一次;迭代 10 三层奖励框架):部分回血先行 → 加权洗牌掷 3 → 三选一停点
|
||
#(顺序即契约——回血按当刻账本与上限结算,选择停点零自动推进)
|
||
var hp_before := int(ledger.get("hero_hp", 0))
|
||
var max_eff := effective_max_hp() # T17 修复:回血封顶口径含调试浮窗 pct(同战斗内上限)
|
||
var heal := mini(int(floor(float(max_eff) * float(int(db.adventure.clear_heal_pct)) / 100.0)), max_eff - hp_before)
|
||
ledger["hero_hp"] = hp_before + heal
|
||
# 置位先于 emit(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位)
|
||
_pending_next_level = true
|
||
_reset_refresh_x() # 迭代 11 第①批 T5b——碰三选一重置 X 余量(不结转)
|
||
_curio_paid_refreshes = 0 # 付费刷计数随停点清零(对齐商店进店清零——2026-10-06)
|
||
var pick_payload: Dictionary = {
|
||
"level_index": level_index,
|
||
"candidates": _draw_curio_candidates("level_clear"), # T4 渠道白名单——关底 Boss 击败=通关渠道只出专长
|
||
"heal_gained": heal,
|
||
"hp_before": hp_before,
|
||
"hp_after": int(ledger.get("hero_hp", 0)),
|
||
"keystone_gained": int(_keystone_drops.back().get("amount", 0)) if not _keystone_drops.is_empty() else 0,
|
||
"keystone_total": keystone,
|
||
"gp": int(ledger.get("gp", 0)),
|
||
"aux_cleared": int(payload.get("aux_cleared", 0)), # additive 扩签(迭代 7 T4——关卡末清空反馈取数)
|
||
}
|
||
_curio_pending = {"candidates": (pick_payload["candidates"] as Array).duplicate(true),
|
||
"payload": pick_payload.duplicate(true)}
|
||
curio_choice_presented.emit(pick_payload)
|
||
_snapshot_breakpoint() # 选择停点可中断(契约 5——续玩不重抽不静默丢失)
|
||
return
|
||
# 末关通关:不发三选一与部分回血(没有下一战可受益)、Boss 钥石照常入账——冒险完成结算
|
||
_settle("completed")
|
||
|
||
|
||
func _settle(result: String) -> void:
|
||
# 冒险摘要五件事:打穿没有 / 通关几关 / 止步第几关 / 钥石入账合计 / 本局主种子
|
||
summary = {
|
||
"result": result,
|
||
"cleared": result == "completed",
|
||
"levels_cleared": levels_total if result == "completed" else level_index - 1,
|
||
"stopped_at": 0 if result == "completed" else level_index,
|
||
"keystone_total": keystone,
|
||
"master_seed": master_seed,
|
||
"aux_cleared_last": _last_treasure_cleared, # additive 扩签(迭代 7 T4——末关清空反馈;非末关走 bonus 载荷)
|
||
"keystone_drops": _keystone_drops.duplicate(true), # additive 扩签(迭代 10——逐杀掉落明细;settle 入账明细/复盘归因数据源)
|
||
"vision_used_delta": int(vision_counter.get("used", 0)), # additive 扩签(迭代 22 秘宝批 T3——真视使用本局计数;settle 聚合入档)
|
||
"pool_stock": _pool_stock_snapshot(), # additive 扩签(迭代 11 第②批 T5——终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面)
|
||
"relics": _feats.duplicate(), # additive 扩签(迭代 12 T1——特性终局持有集:清账前快照,成型判读取数面)
|
||
}
|
||
# 败退明细(数值验证工具取数面 2026-09-22——tools/balance_verifier.py 止步节点分布数据源;
|
||
# 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知)
|
||
if result == "failed" and not _last_level_payload.is_empty():
|
||
summary["defeat"] = {
|
||
"level_index": level_index,
|
||
"node_id": int(_last_level_payload.get("node_id", -1)),
|
||
"node_type": String(_last_level_payload.get("node_type", "")),
|
||
}
|
||
if _runner != null:
|
||
_runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现)
|
||
_reset_curio_state() # 三选一停点态收束清零(结算受理只算一次语义——重复宣布零变化)
|
||
if _save_manager != null:
|
||
_save_manager.clear_breakpoint() # 三结局清断点(迭代 8 卡①/T3——退出冒险=放弃流程+清除断点)
|
||
_save_manager.write_to_disk()
|
||
state = "settling"
|
||
adventure_finished.emit(summary)
|
||
state = "idle"
|
||
|
||
|
||
# ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明)----
|
||
|
||
func _forward_node_entered(node: Dictionary, info: Dictionary) -> void:
|
||
node_entered.emit(node, info)
|
||
|
||
|
||
func _forward_choices_presented(candidates: Array) -> void:
|
||
choices_presented.emit(candidates)
|
||
_snapshot_breakpoint() # 停点快照(迭代 8 卡①——选路停点=唯一可恢复停点形态;战斗中不存)
|
||
|
||
|
||
# ---- 断点快照(迭代 8 卡①——七字段拍板面+恢复必需附随件)----
|
||
# 七字段(拍板面):关卡序号/节点位/生命/属性加成区/GP 钱包/商店已购档/已抽模板;
|
||
# 附随件(续玩正确性必需,随批注记):主种子+取数计数器(rng 快照契约回填——续接原时间线)、
|
||
# 钥石累计(大奖励中途值——终局摘要五件事不失真)、已到节点序(呈现层图条重建)、道具库存
|
||
# (奖励节点已选件——防静默丢失)。注入缺省 null = 整面静默(模拟/冒烟零扰动)。
|
||
func _snapshot_breakpoint() -> void:
|
||
if _save_manager == null or state != "running":
|
||
return
|
||
var treasure_snapshot: Array = []
|
||
if _runner != null and _runner.treasure_inventory != null:
|
||
treasure_snapshot = _runner.treasure_inventory.snapshot_inventory()
|
||
# 三选一停点续玩分支 _runner 为 null(关卡已收束、下一关未开)——节点域字段按缺省落档,
|
||
# 该两态的续玩只消费 bonus_* 附随件(选择停点/已兑付停点快照,迭代 10)
|
||
var bp: Dictionary = {
|
||
"level_index": level_index,
|
||
"node_id": _runner.current_node_id() if _runner != null else -1,
|
||
"hero_hp": int(ledger.get("hero_hp", 0)),
|
||
"bonus": {
|
||
"atk": int(bonus.get("atk", 0)),
|
||
"max_hp": int(bonus.get("max_hp", 0)),
|
||
"armor_class": int(bonus.get("armor_class", 0)),
|
||
"ap": int(bonus.get("ap", 0)),
|
||
},
|
||
"gp": int(ledger.get("gp", 0)),
|
||
"shop_purchased": purchased.duplicate(true),
|
||
"shop_shelf": _runner.shop_bp_shelf() if _runner != null else [], # 迭代 11 第②批 T2——P2 甲货架入断点(离开店/非店停点 = 空架合法态)
|
||
"shop_visit_buys": _runner.shop_bp_visit_buys() if _runner != null else 0, # 迭代 11 第②批 T2——本次进店已购次数(A 链定价输入,P2 甲扩列)
|
||
"attunement_charges": attune_charges.duplicate(true), # 迭代 11 第①批 T6——同调主动件充能余量随断点(save v4 域)
|
||
"template_pick": _runner.active_template_index if _runner != null else -1,
|
||
"master_seed": master_seed,
|
||
"rng_counter": rng.counter(),
|
||
"keystone": keystone,
|
||
"visited_ids": _runner.visited_ids() if _runner != null else [],
|
||
"aux_items": treasure_snapshot,
|
||
"bonus_taken": _curio_taken.duplicate(true), # 三选一件获取次数(kind 维度派生账本——断点附随件与读数承载)
|
||
"bonus_deck": _curio_deck.duplicate(true), # 令牌池余库(TP0 v2.2——池消耗即上限,续玩余库逐枚还原)
|
||
"relics": _feats.duplicate(), # 特性持有集(迭代 12 T1——局内随机渠道获得态续玩防丢失)
|
||
"bonus_pending": _curio_pending.duplicate(true), # 三选一停点态(空 = 无停点——选择停点可中断契约)
|
||
"feat_ticket_pity": _feat_ticket_pity, # T5 门票保底计数(扩列向后兼容——旧档缺键缺省 0)
|
||
"feat_ticket_sold": _feat_ticket_sold, # T5 门票限购计数(同上)
|
||
"adventure_counters": adventure_counters.duplicate(true), # 迭代 20 T6/T7——adventure 域计数器随断点(续玩跨战账不丢)
|
||
"vision_counter_used": int(vision_counter.get("used", 0)), # 迭代 22 秘宝批 T3——真视使用计数随断点(续玩跨战账不丢)
|
||
"refresh_stock": _refresh_stock, # 迭代 20 T4/T7——刷新存量账随断点(F-1 新账)
|
||
"battery_pending": _battery_pending, # 迭代 20 T4/T7——电池待分配充能随断点
|
||
"bonus_result": _curio_result.duplicate(true), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
|
||
"keystone_drops": _keystone_drops.duplicate(true), # 逐杀掉落明细(摘要归因续接——迭代 10)
|
||
}
|
||
_save_manager.set_breakpoint(bp)
|
||
_save_manager.write_to_disk()
|
||
|
||
|
||
func _forward_step_row(row: Dictionary) -> void:
|
||
step_row.emit(row)
|
||
|
||
|
||
func _forward_round_started(ri: int) -> void:
|
||
round_started.emit(ri)
|
||
|
||
|
||
func _forward_board_changed() -> void:
|
||
board_changed.emit()
|
||
|
||
|
||
func _forward_wallet_changed(event: String, gp_val: int, extra: Dictionary) -> void:
|
||
wallet_changed.emit(event, gp_val, extra)
|