主卡T2(拆件批wr2/wr3): CurioStop建件+CurioDeck纯池化——curio_stop.gd新件(停点三态/刷新三源账/兑付族/凭证出箱voucher_outbox/信号面自带/rebind_level_runner每关注入GP回调防调死对象/open_level_clear收编关卡完成停点段);adventure_runner停点面迁出(停点方法族留组合根薄委托——T5 C类收口;stop()件出口;信号中继法转发——T3直连前承转;_resume_pending迁stop.restore_pending含token_migration;resume已兑付分支stop.restore_resolved;_bp_parts停点键改stop片段取数);CurioDeck纯池化(bind_handlers/consume_pool_purchase反向回调退役——商店消费链ShopSession→注入Callable→stop.consume_pool_purchase兑付单点);测试挂死级改址随批(直读写已迁字段30+处→stop()件测试构造面,语义=原直写——G2b清单停点域全部);cs1/cs2/cs3现役断言承继尾缀落点(拆件批 V2/V3)。验收:bp1对拍292dumps×27键逐位零漂移(改前/after_T1/after_T2三时点同种子逐位一致)+九套件零红+csf集成绿+冒烟17项0失败〔拆件批 V2〕〔拆件批 V3〕
This commit is contained in:
1 parent
8d7b12e063
commit
4a9e4300ad
31 files changed
+189353
-709
No files matched your search
+145
-444
@@ -25,64 +25,61 @@ var _state: State = State.IDLE
|
||||
var state: String: # 外部契约面(只读——外部零写点实证;词表 "idle"/"running"/"settling")
|
||||
get:
|
||||
return State.keys()[_state].to_lower()
|
||||
var ledger := {"hero_hp": 0, "gp": 0} # 冒险账本(生命 + GP 钱包——经 start_level_external 注入 runner 共享)
|
||||
var run_ledger := RunLedger.new() # 共享四本账(attune_charges/vision_counter/adventure_counters/bonus 单类持账)
|
||||
var purchased := {} # product_id → 已购档数(跨关延续、冒险收束清零——递增定价输入)
|
||||
var _hero: HeroLedger # 主角实体账(生命/钱包/加算区/类型轴/特性集/钥石/真视——拆件批 T1 建件)
|
||||
var adventure_counters: Dictionary[Variant, Variant] = {} # 冒险桶(效果引擎 adv_ 计数器宿主字典——跨战持久)
|
||||
var run_ledger := RunLedger.new() # 充能账瘦壳(attune_charges——整类退役归姊妹副卡)
|
||||
var level_index := 0 # 当前关卡序号(1 起)
|
||||
var levels_total := 0 # 序列总关数(读 level 表行数)
|
||||
var keystone := 0 # 钥石入账累计(摘要第三件事的账面)
|
||||
var master_seed := 0 # 本局主种子(摘要第五件事)
|
||||
# 两根类型轴冒险开始快照(契约 8:营地选中态经参数面注入、冒险开始后锁死;
|
||||
# 公有=参数面快照的可断言面〔契约 9 写口唯一,本字段只读不改〕。空串=未注入→消费面回退锻造配置默认值)
|
||||
var hero_attack_type := ""
|
||||
var hero_armor_type := ""
|
||||
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}
|
||||
# 永久强化 flat 注入(冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板)
|
||||
var _last_treasure_cleared := 0 # additive(最近一次关卡末清空计数,summary 取数面)
|
||||
var _treasure_unlocked := {} # aux 解锁门控集(params.treasure_unlocked 接收面)
|
||||
var _attunement: Array[Variant] = [] # 同调装配集(params.attunement 接收面,start_level 外下传 LevelRunner)
|
||||
var _attune_refresh_x := 0 # 刷新件余量(每碰三选一重置=X 表值;优先消耗、不结转)
|
||||
var _attune_refresh_y := 0 # Y 冒险周期池(开局参数面注入,用完即无——营地升级行表值)
|
||||
var _curio_paid_refreshes := 0 # 本次停点已付费刷新次数(对齐商店 MC-1——GP 刷新价曲线输入,随停点创建清零=与商店进店清零同构)
|
||||
# ---- 凭证件三本账(奇物.md 凭证件表 5 件;F-1 刷新写错账修复承载面)----
|
||||
var _refresh_stock := 0 # 刷新存量账(冒险级——刷新券加仓;**不经 _attune_refresh_x**:X 每停点重置会吞存量=写错账缺陷 F-1)
|
||||
var _pending_rap := 0 # 储备金袋待注入(节点级——下一战 RAP +N,战首注入后清零)
|
||||
var _time_sand_queue := 0 # 时沙待注入(回合级——下一战首轮 AP +N,战首入队后清零)
|
||||
var _save_manager = null # 断点承载注入(null = 不写断点;模拟/冒烟缺省)
|
||||
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
|
||||
# ---- 三选一停点态 ----
|
||||
var _curio: CurioDeck = null # 令牌池件(_init 建;多重集/抽签/cap/凭证判定唯一宿主)
|
||||
# ---- 三选一停点(编排迁 CurioStop 件——停点三态/刷新三源/凭证出箱/信号面)----
|
||||
var _curio: CurioDeck = null # 令牌池件(_init 建;纯池——多重集/抽签/cap/凭证判定唯一宿主)
|
||||
var _stop: CurioStop = null # 停点件(_init 建;每冒险一次——关卡换实例不换件)
|
||||
var _curio_taken: Dictionary[Variant, Variant]: # 件获取次数计数(kind 维度派生账本——断点附随件与摘要读数承载)
|
||||
get: return _curio.taken
|
||||
set(v): _curio.taken = v
|
||||
var _curio_deck: Array[Variant]: # 令牌池(TP0 v2.2——持久多重集,元素 {"kind","tier"};商店货架同库共账)
|
||||
get: return _curio.deck
|
||||
set(v): _curio.deck = v
|
||||
var _feats: Array[String] = [] # 特性持有集(局内随机渠道获得、冒险级跨关携带、收束清零;🔴 层级纪律承载面:不入同调位不入局外树)
|
||||
var _tree_rules: Array[Variant] = [] # 树 rule 键集(Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面)
|
||||
var _curio_pending := {} # 停点态:{"candidates": Array, "payload": Dictionary, "chosen"/"incoming": 替换在途}——空 = 无停点
|
||||
var _curio_result := {} # 已兑付待推进态(curio_resolved 载荷副本——中断续玩不重兑付)
|
||||
var _keystone_drops: Array[Variant] = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops——结算明细/复盘归因数据源)
|
||||
var _node_curio_pending := {} # 奖励节点三选一停点态(共享池;断点不载:续玩重建到选路停点重走,恢复的计数器保证同候选重抽)
|
||||
var _feat_ticket_pity: int: # 专长凭证保底计数(T5——展示次数:节点开面/货架抽签/刷新重展各计一次;触发清零)
|
||||
get: return _curio.ticket_pity
|
||||
set(v): _curio.ticket_pity = v
|
||||
var _feat_ticket_sold: int: # 专长凭证限购计数(T5——选中/买下即 +1;拿到即开不进持有集)
|
||||
get: return _curio.ticket_sold
|
||||
set(v): _curio.ticket_sold = v
|
||||
var _shop_mode := "legacy" # 商店模式("legacy"=固定三件加定价〔off 对照承载,缺省零扰动〕/"pool"=共享池抽 3 买 1 重抽;真实入口 main.gd 显式投 pool)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
db = db_ref
|
||||
rng = rng_ref
|
||||
_hero = HeroLedger.new(db, {})
|
||||
_curio = CurioDeck.new(db, rng)
|
||||
_curio.bind_handlers(_apply_curio_pick, _redeem_feat_ticket)
|
||||
_stop = CurioStop.new(db, _curio, _hero, _snapshot_breakpoint)
|
||||
_stop.curio_choice_presented.connect(_relay_stop_choice_presented)
|
||||
_stop.curio_resolved.connect(_relay_stop_resolved)
|
||||
|
||||
|
||||
# 件出口(组件交出非委托壳——呈现/测量面取主角账单点;拆件批接口块)
|
||||
func hero() -> HeroLedger:
|
||||
return _hero
|
||||
|
||||
|
||||
# 件出口(停点件——呈现/测量面取停点编排单点;拆件批接口块)
|
||||
func stop() -> CurioStop:
|
||||
return _stop
|
||||
|
||||
|
||||
# 停点件信号中继(呈现层直订面在 T3 信号直连前由本类承转——方法转发非 lambda)
|
||||
func _relay_stop_choice_presented(payload: Dictionary) -> void:
|
||||
curio_choice_presented.emit(payload)
|
||||
|
||||
|
||||
func _relay_stop_resolved(result: Dictionary) -> void:
|
||||
curio_resolved.emit(result)
|
||||
|
||||
|
||||
# ---- 冒险生命周期 ----
|
||||
@@ -97,51 +94,39 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
return false
|
||||
master_seed = seed_val
|
||||
rng.new_adventure(seed_val)
|
||||
ledger = {"hero_hp": 0, "gp": 0}
|
||||
run_ledger.bonus = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
purchased = {}
|
||||
keystone = 0
|
||||
_hero.rebase_override(params.get("hero_override", {})) # 永久强化 flat 注入(规范化归主角账)
|
||||
_hero.reset_adventure_accounts() # 账面重置(钱包/加算区/已购档/钥石/日记/特性清零+生命置满基础上限)
|
||||
_hero.attack_type = String(params.get("attack_type", "")) # 两根类型轴快照(契约 8 锁死:开局后不随存档选中变化)
|
||||
_hero.armor_type = String(params.get("armor_type", ""))
|
||||
summary = {}
|
||||
_reset_curio_state()
|
||||
levels_total = db.level_count() # 表访问收口(关数读 level 表行数)
|
||||
level_index = 1
|
||||
_pending_next_level = false
|
||||
_base_override = _normalize_override(params.get("hero_override", {}))
|
||||
hero_attack_type = String(params.get("attack_type", "")) # 两根类型轴快照(契约 8 锁死:开局后不随存档选中变化)
|
||||
hero_armor_type = String(params.get("armor_type", ""))
|
||||
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 缺省 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
|
||||
_attune_refresh_y = maxi(0, int(params.get("attune_refresh_y", 0))) # additive(Y 冒险周期池)
|
||||
_stop.set_attunement(_attunement) # 停点件 X 基数查询消费面(活引用)
|
||||
_stop.set_refresh_y(maxi(0, int(params.get("attune_refresh_y", 0)))) # additive(Y 冒险周期池)
|
||||
_seed_attune_charges() # 冒险开始充能账本按装备主动件重置(每冒险充能语义)
|
||||
run_ledger.vision_counter = {"used": 0} # 真视使用计数冒险开始归零(跨局累计归生涯档)
|
||||
adventure_counters = {} # 冒险桶清零(效果引擎 adv_ 计数器宿主——冒险开始归零)
|
||||
# 刷新练习开局注入(refresh_stock_grant 装配即得一次性加仓:
|
||||
# 装配集 kind 分发扫描;只入存量账 _refresh_stock、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕)
|
||||
# 装配集 kind 分发扫描;只入存量账、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕)
|
||||
for iid_v2 in _attunement:
|
||||
var rp_row: Dictionary[Variant, Variant] = 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))
|
||||
_stop.add_refresh_stock(int(rp_row.get("magnitude", 0)))
|
||||
# 特性开局注入(MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派
|
||||
# 各 2 件档一成型——测量面专用;真实局规则改写型特性只走局内随机渠道,红线②层级纪律不破〕)
|
||||
var feat_seed: Array[Variant] = 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 [] # 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() # 生命账本置满(生效上限口径:含调试浮窗 pct,与战斗内同源)
|
||||
_hero.feats.append(String(r))
|
||||
_state = 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
|
||||
|
||||
|
||||
# 断点续玩(「继续上局」入口;bp = save.breakpoint_snapshot() 快照,params 同 start_adventure
|
||||
# 的注入参数面):账面(生命/GP/加算区/已购档/钥石累计)逐字段回填 + rng.restore_adventure 回填
|
||||
# 「主种子+计数器」→ _start_current_level 续玩路径(模板随快照强制、重建到停点节点,零重抽)。
|
||||
@@ -160,20 +145,22 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
|
||||
return false
|
||||
master_seed = int(bp.get("master_seed", 0))
|
||||
rng.restore_adventure(master_seed, int(bp.get("rng_counter", 0)))
|
||||
_resume_ledger(bp)
|
||||
_resume_ledger(bp, params)
|
||||
_resume_curio_state(bp)
|
||||
_resume_params(bp, params)
|
||||
_resume_counters(bp)
|
||||
var bp_pending: Dictionary[Variant, Variant] = bp.get("bonus_pending", {}) as Dictionary
|
||||
var bp_result: Dictionary[Variant, Variant] = bp.get("bonus_result", {}) as Dictionary
|
||||
if not bp_pending.is_empty():
|
||||
_resume_pending(bp_pending)
|
||||
_pending_next_level = true
|
||||
_state = State.RUNNING
|
||||
_stop.restore_pending(bp_pending) # 选择停点直回(续玩不重跑关卡——信号经停点件中继)
|
||||
return true
|
||||
if not bp_result.is_empty():
|
||||
_pending_next_level = true
|
||||
_curio_result = bp_result.duplicate(true)
|
||||
_stop.restore_resolved(bp_result)
|
||||
_state = State.RUNNING
|
||||
curio_resolved.emit(_curio_result.duplicate(true))
|
||||
curio_resolved.emit(_stop.resolved_payload())
|
||||
return true
|
||||
_resume_bp = bp.duplicate(true)
|
||||
# 充能账本随断点回局(缺键 = 旧档/无断点,保持 _seed_attune_charges 种子态)
|
||||
@@ -184,16 +171,10 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
|
||||
return _start_current_level()
|
||||
|
||||
|
||||
# 续玩账面回填(账本/加算区/已购档/钥石/关序——段序与原读侧一致;bonus 子键 clamp 类夹 ≥0)
|
||||
func _resume_ledger(bp: Dictionary) -> void:
|
||||
ledger = {"hero_hp": int(bp.get("hero_hp", 0)), "gp": int(bp.get("gp", 0))}
|
||||
var bp_bonus: Dictionary[Variant, Variant] = bp.get("bonus", {}) as Dictionary
|
||||
run_ledger.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))
|
||||
# 续玩账面回填(生命/钱包/加算区/已购档/钥石/逐杀明细/特性/真视——主角账片段单点;
|
||||
# bonus 子键 clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载)
|
||||
func _resume_ledger(bp: Dictionary, params: Dictionary) -> void:
|
||||
_hero.restore_fragment(bp, params)
|
||||
summary = {}
|
||||
levels_total = db.level_count() # 表访问收口(关数读 level 表行数)
|
||||
level_index = int(bp.get("level_index", 1))
|
||||
@@ -220,91 +201,55 @@ func _resume_curio_state(bp: Dictionary) -> void:
|
||||
rtok["feat_id"] = String(tok.get("feat_id", "")) # relic 令牌 feat_id 随还原
|
||||
restored.append(rtok)
|
||||
_curio_deck = restored
|
||||
# 特性持有集还原(缺键 = 旧版断点防御空集)
|
||||
_feats = []
|
||||
# 特性持有集还原(缺键 = 旧版断点防御空集——主角账字段单点)
|
||||
_hero.feats = []
|
||||
for r in (bp.get("relics", []) as Array):
|
||||
_feats.append(String(r))
|
||||
_keystone_drops = (bp.get("keystone_drops", []) as Array).duplicate(true)
|
||||
_hero.feats.append(String(r))
|
||||
|
||||
# 续玩参数面回填(同 start_adventure 参数面;生命账本越界钳制在此落——clamp 类)
|
||||
func _resume_params(bp: Dictionary, params: Dictionary) -> void:
|
||||
_base_override = _normalize_override(params.get("hero_override", {}))
|
||||
hero_attack_type = String(params.get("attack_type", "")) # 续玩参数面同 start_adventure(账号层选中态随 resume 重注入)
|
||||
hero_armor_type = String(params.get("armor_type", ""))
|
||||
_hero.rebase_override(params.get("hero_override", {})) # 续玩参数面同 start_adventure(规范化归主角账)
|
||||
_hero.attack_type = String(params.get("attack_type", "")) # 续玩参数面同 start_adventure(账号层选中态随 resume 重注入)
|
||||
_hero.armor_type = String(params.get("armor_type", ""))
|
||||
# 生命账本越界钳制(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))))
|
||||
# 强化 flat 后乘调试浮窗 pct(保守口径不含加算区——与原钳制式逐字同,加算区恢复后其余战斗照常)。
|
||||
_hero.hero_hp = mini(_hero.hero_hp, maxi(1,
|
||||
int(round(float(int(db.hero.max_hp) + int(_hero.base_override_flat().get("max_hp", 0))) * (100.0 + float(_hero.debug_hp_pct())) / 100.0))))
|
||||
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 续玩参数面同 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
|
||||
# Y 余量优先读断点(花掉不回血=堵「用完退菜单再续玩满血复活」可利用漏洞);缺键=旧版断点,
|
||||
# 回退参数面整额注入(旧档行为零变化)——params_fallback 类
|
||||
_attune_refresh_y = _bp_read_int("attune_refresh_y", bp, 0, params)
|
||||
_seed_attune_charges() # 冒险开始充能账本按装备主动件重置(每冒险充能语义)
|
||||
_stop.restore_fragment(bp, params) # 停点件片段回填(凭证计数/刷新存量/Y 池——Y 缺键回退参数面 params_fallback 类)
|
||||
_tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # A/B 面 rule 键集(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传)
|
||||
|
||||
|
||||
# 续玩计数器回填(凭证计数/冒险域计数/刷新存量/真视计数)
|
||||
# 续玩计数器回填(冒险域计数——凭证计数/刷新存量/Y 池随停点件片段回填)
|
||||
func _resume_counters(bp: Dictionary) -> void:
|
||||
_feat_ticket_pity = int(bp.get("feat_ticket_pity", 0)) # T5 扩列——旧档缺键缺省 0(存档兼容)
|
||||
_feat_ticket_sold = int(bp.get("feat_ticket_sold", 0))
|
||||
# adventure 域计数器回填(缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先)
|
||||
# adventure 域计数器回填(缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先——桶归宿主字典)
|
||||
var bp_counters = _bp_read("adventure_counters", bp)
|
||||
if bp_counters is Dictionary:
|
||||
run_ledger.adventure_counters = (bp_counters as Dictionary).duplicate(true)
|
||||
_refresh_stock = _bp_read_int("refresh_stock", bp, 0) # 旧档缺键缺省 0(clamp 类)
|
||||
run_ledger.vision_counter = {"used": _bp_read_int("vision_counter_used", bp, 0)} # 真视计数随断点回填(clamp 类)
|
||||
|
||||
|
||||
# 续玩选择停点回填(token_migration 类:旧版断点候选为 kind 字符串,按普通档令牌防御归一)
|
||||
func _resume_pending(bp_pending: Dictionary) -> void:
|
||||
_pending_next_level = true
|
||||
var migrated: Array[Variant] = []
|
||||
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[Variant, Variant] = (bp_pending.get("payload", {}) as Dictionary).duplicate(true)
|
||||
_state = State.RUNNING
|
||||
curio_choice_presented.emit(payload)
|
||||
adventure_counters = (bp_counters as Dictionary).duplicate(true)
|
||||
|
||||
|
||||
func _reset_curio_state() -> void:
|
||||
_curio.reset() # 池与件获取计数(池半归件;停点态归本类下行清理)
|
||||
_feats = []
|
||||
_curio_pending = {}
|
||||
_curio_result = {}
|
||||
_keystone_drops = []
|
||||
_node_curio_pending = {}
|
||||
run_ledger.adventure_counters = {}
|
||||
_refresh_stock = 0
|
||||
_pending_rap = 0
|
||||
_time_sand_queue = 0
|
||||
_curio.reset() # 池与件获取计数(池半归件;停点态归停点件下行清理)
|
||||
_stop.reset_adventure_state()
|
||||
_hero.feats = []
|
||||
adventure_counters = {}
|
||||
|
||||
|
||||
# 调试浮窗百分比通道(测试工具覆盖——每关新 runner 都要应用;正式侧归元进度)
|
||||
# 调试浮窗百分比通道(测试工具覆盖——每关新 runner 都要应用;正式侧归元进度。单点归主角账)
|
||||
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
||||
_override = [atk_pct, hp_pct]
|
||||
_hero.set_debug_override(atk_pct, hp_pct)
|
||||
if _runner != null:
|
||||
_runner.set_hero_override(atk_pct, hp_pct)
|
||||
|
||||
|
||||
# 冒险账本侧生效上限(账本置满/回血封顶与战斗内上限同式同源):
|
||||
# (表值 + 永久强化 flat + 加算区 flat)后乘调试浮窗百分比——叠加次序与 LevelRunner.effective_max_hp
|
||||
# 逐字一致。pct=0(模拟/冒烟默认)时乘法恒等——MC 锚零漂移。
|
||||
# 冒险账本侧生效上限(公式单点归主角账——置满/回血封顶与战斗内上限同式同源)
|
||||
func effective_max_hp() -> int:
|
||||
var base: int = int(db.hero.max_hp) + int(_base_override.get("max_hp", 0)) + int(run_ledger.bonus.get("max_hp", 0))
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(_override[1])) / 100.0)))
|
||||
return _hero.effective_max_hp()
|
||||
|
||||
|
||||
# 断点承载注入(组合根冒险开始时调用;模拟/冒烟不注入 = 断点面整体静默)
|
||||
@@ -317,10 +262,9 @@ func runner() -> LevelRunner:
|
||||
return _runner
|
||||
|
||||
|
||||
# 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶)。算术宿主 = LevelRunner.rest_heal
|
||||
#(与 LevelRunner.rest_heal_amount 同一单点)
|
||||
# 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶)。算术宿主 = HeroLedger.rest_heal(公式单点)
|
||||
func apply_rest(pct: int, hp: int, max_hp: int) -> int:
|
||||
return LevelRunner.rest_heal(pct, hp, max_hp)
|
||||
return HeroLedger.rest_heal(pct, hp, max_hp)
|
||||
|
||||
|
||||
# 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;三选一未兑付拒绝推进)
|
||||
@@ -328,26 +272,18 @@ 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():
|
||||
if _stop.curio_stop_pending():
|
||||
push_error("[冒险结构] continue_to_next_level 拒绝——三选一停点未兑付(零自动推进契约)")
|
||||
return false
|
||||
_pending_next_level = false
|
||||
_curio_result = {}
|
||||
_stop.clear_resolved()
|
||||
level_index += 1
|
||||
return _start_current_level()
|
||||
|
||||
|
||||
# 停点空候选消化(池空=三选一环节整体跳过:候选 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
|
||||
return _stop.skip_empty_curio()
|
||||
|
||||
|
||||
# ---- 提升三选一(选择停点零自动推进、重复兑付被拒;兑付恰一件、未选项零生效)----
|
||||
@@ -357,164 +293,46 @@ func _find_token(candidates: Array, kind: String, tier: String, feat_id: String
|
||||
return _curio.find_token(candidates, kind, tier, feat_id)
|
||||
|
||||
|
||||
# 玩家点选兑付(呈现层/冒烟/自动模拟确认入口):令牌 ∈ 停点候选(身份键精确匹配——relic 件
|
||||
# 另须 feat_id)。属性件写加算区、AP 件写第四轴、GP 件走钱包「奖励入账」入口、恢复件当即回血、
|
||||
# relic 件入特性持有集(🔴 层级纪律:只走局内随机渠道)、道具件受同调位约束。
|
||||
# 消费=选中令牌踢池(未选回池由「抽签不摘牌、兑付才摘牌」语义承载——TP0 v2.2)。
|
||||
# 玩家点选兑付(state 守卫留应用层——编排归停点件;呈现层直调面 T5 改 stop())
|
||||
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)
|
||||
if state != "running":
|
||||
push_error("[冒险结构] pick_curio 拒绝——state=%s(非 running 不可兑付)" % state)
|
||||
return false
|
||||
var candidates: Array[Variant] = _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 伴随键(反馈行取数面;非 relic 件零新增键)
|
||||
func _feat_payload_keys(feat_id: String) -> Dictionary:
|
||||
if feat_id == "":
|
||||
return {}
|
||||
var row: Dictionary[Variant, Variant] = db.feat_row(feat_id)
|
||||
return {"feat_id": feat_id, "feat_name": String(row.get("display_name", "")), "feat_archetype": String(row.get("archetype", ""))}
|
||||
|
||||
|
||||
# 三选一件兑付共享体(TP0——令牌消费、摘牌单点,含 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 == "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 == "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 # 身份维已拿计数(cap 消费面)
|
||||
return true
|
||||
# 加算形态路由(match 收敛进效果类型:属性值提升四类走
|
||||
# adventure_table.CURIO_APPLY_ROUTES 声明路由(bonus 落点键),加新加算类令牌零代码)
|
||||
var sink := db.adventure.curio_apply_route(kind)
|
||||
if sink != "":
|
||||
run_ledger.bonus[sink] = int(run_ledger.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——选中踢池单点;relic 按 feat_id 精确摘;找不到 = false)
|
||||
func _consume_deck_token(kind: String, tier: String, feat_id: String = "") -> bool:
|
||||
return _curio.consume_deck_token(kind, tier, feat_id)
|
||||
return _stop.pick_curio(kind, tier, feat_id)
|
||||
|
||||
|
||||
# 特性持有集读面(效果接线/求值/摘要唯一取数口——副本防外部改写)
|
||||
func feats_held() -> Array:
|
||||
return _feats.duplicate()
|
||||
return _hero.feats_held()
|
||||
|
||||
|
||||
# ---- 商店共享池消费面(共享池单点账本;TP0——库存=牌库剩余令牌)----
|
||||
# ---- 商店共享池消费面(纯池——库存=牌库剩余令牌)----
|
||||
|
||||
# 库存余量(池消耗即上限;奖励摘牌与商店购买同库承载,全冒险一次性)
|
||||
func pool_stock(kind: String) -> int:
|
||||
return _curio.pool_stock(kind)
|
||||
|
||||
|
||||
# 商店购买消费 handler(LevelRunner.buy_pool_item 经 Callable 调用——回 _apply_curio_pick 同口)
|
||||
func _consume_pool_purchase(kind: String, tier: String) -> bool:
|
||||
return _curio.consume_pool_purchase(kind, tier)
|
||||
return _stop.pool_stock(kind)
|
||||
|
||||
|
||||
# 终局池快照(_settle 清账前调用——复位会重置计数,摘要 additive 键须在此前取数)
|
||||
func _pool_stock_snapshot() -> Dictionary:
|
||||
return _curio.stock_snapshot()
|
||||
return _stop.pool_stock_snapshot()
|
||||
|
||||
|
||||
# 兑付收口(恰一次):清停点态 → 记已兑付态 → emit → 停点快照(等继续确认——中断续玩不重兑付)。
|
||||
# picked = kind;tier 随载荷(TP0——大小档位读数面);relic 伴随键随载荷(反馈行取数)
|
||||
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()
|
||||
|
||||
|
||||
# ---- 奖励节点三选一(与关卡完成三选一共享同池同权重同次数上限)----
|
||||
# ---- 奖励节点三选一(与关卡完成三选一共享同池同权重同次数上限——编排归停点件)----
|
||||
|
||||
# 候选呈现(节点到达时调用——恰一 reward_pick scope 洗牌 + 道具件掷定 scope;本停点断点不载,
|
||||
# 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)。
|
||||
func node_curio_candidates() -> Dictionary:
|
||||
var cands: Array[Variant] = _draw_curio_candidates("reward_node")
|
||||
return {"candidates": cands}
|
||||
return _stop.node_curio_candidates()
|
||||
|
||||
|
||||
func present_node_curio(candidates: Array) -> void:
|
||||
_reset_refresh_x() # 碰三选一重置 X 余量(不结转)
|
||||
_curio_paid_refreshes = 0 # 付费刷计数随停点清零(对齐商店进店清零)
|
||||
_node_curio_pending = {"candidates": candidates.duplicate(true)}
|
||||
_stop.present_node_curio(candidates)
|
||||
|
||||
|
||||
# 点选兑付(节点域——无部分回血无钥石;令牌消费与牌库摘牌与关卡完成三选一同库;relic 同权入持有集)
|
||||
# 点选兑付(节点域——编排归停点件;呈现层直调面 T5 改 stop())
|
||||
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[Variant] = _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
|
||||
return _stop.apply_node_curio(kind, tier, feat_id)
|
||||
|
||||
|
||||
# 冒险开始充能账本重置(T6——装备 active_charged 件各回 charges_per_adventure;被动件不入账)
|
||||
@@ -526,118 +344,41 @@ func _seed_attune_charges() -> void:
|
||||
run_ledger.attune_charges[String(iid)] = maxi(0, int(arow.get("charges_per_adventure", 0)))
|
||||
|
||||
|
||||
# ---- 三选一刷新(X/Y 双源:X=刷新件每碰三选一重给〔优先消耗、不结转〕、
|
||||
# Y=营地升级行冒险周期池〔用完即无〕;重抽消费 reward_pick 既有洗牌流,池规则全保留——不放回/
|
||||
# 达上限剔牌/掷定前置直显;停点可中断)----
|
||||
|
||||
func _attune_refresh_x_total() -> int:
|
||||
# kind 分发查询(装配集内 effect_kinds 含 bonus_refresh
|
||||
# 的行 magnitude 合计;刷新件类加件零 id 分支)
|
||||
var total := 0
|
||||
for iid_v in _attunement:
|
||||
var arow: Dictionary[Variant, Variant] = 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 池 + 刷新存量账——呈现层余量读数口)
|
||||
func attune_refresh_left() -> int:
|
||||
return _attune_refresh_x + _attune_refresh_y + _refresh_stock
|
||||
return _stop.attune_refresh_left()
|
||||
|
||||
|
||||
# 刷新存量账读数(T4——刷新券加仓余量单独读面;呈现层与断点消费)
|
||||
func refresh_stock() -> int:
|
||||
return _refresh_stock
|
||||
return _stop.refresh_stock()
|
||||
|
||||
|
||||
# 节点停点在途态读回(组合根重 present 用——candidates 值拷贝)
|
||||
func node_curio_pending_state() -> Dictionary:
|
||||
return {
|
||||
"candidates": (_node_curio_pending.get("candidates", []) as Array).duplicate(true),
|
||||
}
|
||||
return _stop.node_curio_pending_state()
|
||||
|
||||
|
||||
# 三选一刷新(免费位 X/Y——组合根唯一调用口;无停点/无余量 false 零副作用)。关卡完成停点重抽后重发
|
||||
# curio_choice_presented(呈现层重建卡片+刷新余量);节点停点由组合根读 node_curio_pending_state 重 present。
|
||||
# 三选一刷新(免费位 X/Y——组合根唯一调用口;无停点/无余量 false 零副作用)
|
||||
func refresh_curio() -> bool:
|
||||
return _refresh_curio_common(false)
|
||||
return _stop.refresh_curio()
|
||||
|
||||
|
||||
# 三选一 GP 付费刷新(对齐商店 MC-1——免费位 X/Y 用尽后转付费,同旋钮同曲线同钱包;
|
||||
# 组合根先经 curio_stop_pending+钱包支出再调,本口只做计数与重抽)。池规则全保留——不放回/达上限剔牌/掷定前置直显。
|
||||
# 三选一 GP 付费刷新(对齐商店 MC-1——组合根先经 curio_stop_pending+钱包支出再调,本口只做计数与重抽)
|
||||
func refresh_curio_paid() -> bool:
|
||||
return _refresh_curio_common(true)
|
||||
return _stop.refresh_curio_paid()
|
||||
|
||||
|
||||
# 本次 GP 付费刷新价(与商店货架刷新同源:ceil(基价 × 曲线^本次停点已付费刷)——读同两个调参旋钮,单点调参)
|
||||
# 本次 GP 付费刷新价(与商店货架刷新同源——单点调参)
|
||||
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))))
|
||||
return _stop.curio_refresh_gp_cost()
|
||||
|
||||
|
||||
# 三选一停点在途谓词(付费路径扣费前置校验——无停点禁扣钱,防空转烧钱包)
|
||||
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(营地冒险周期池)→ 存量账(刷新券加仓,
|
||||
# 跨停点持久)
|
||||
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[Variant] = _draw_curio_candidates("level_clear")
|
||||
_curio_pending["candidates"] = cands.duplicate(true)
|
||||
var payload: Dictionary[Variant, Variant] = (_curio_pending.get("payload", {}) as Dictionary).duplicate(true)
|
||||
payload["candidates"] = cands
|
||||
payload["refreshed"] = true # 重发载荷带标——呈现层直落选择面,不再重开胜利总结面
|
||||
curio_choice_presented.emit(payload) # 同信号重 present(零自动推进契约不破——停点仍在途)
|
||||
_snapshot_breakpoint()
|
||||
return true
|
||||
if not _node_curio_pending.is_empty():
|
||||
var cands2: Array[Variant] = _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)
|
||||
|
||||
|
||||
# 三选一候选抽签(实装归 CurioDeck 件;消费 reward_pick 封闭流——一次停点恰一 scope)
|
||||
func _draw_curio_candidates(channel: String = "reward_node") -> Array:
|
||||
return _curio.draw_candidates(channel)
|
||||
|
||||
|
||||
# 已拿上限判定(身份维度;缺省放行=白名单外 kind 兜底)
|
||||
func _curio_cap_ok(kind: String, feat_id: String = "") -> bool:
|
||||
return _curio.cap_ok(kind, feat_id)
|
||||
return _stop.curio_stop_pending()
|
||||
|
||||
|
||||
# —— 专长获取专长凭证四口径判定(实装归 CurioDeck 件:调用即一次展示判定与记账)——
|
||||
@@ -645,25 +386,9 @@ func _feat_ticket_consider(rg: RandomNumberGenerator) -> bool:
|
||||
return _curio.feat_ticket_consider(rg)
|
||||
|
||||
|
||||
# 专长凭证兑付(拿到即开——停点候选替换为专长三选一;不进持有集/零摘牌;限购计数 +1):
|
||||
# 节点停点=本地替换(呈现层读在途态重绘);店内无停点=临时停点 + payload.context 标记(呈现层
|
||||
# 重 present 无胜利总结面,确认后只关面回店)。
|
||||
func _redeem_feat_ticket() -> bool:
|
||||
_feat_ticket_sold += 1
|
||||
_feat_ticket_pity = 0
|
||||
var cands: Array[Variant] = _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[Variant, Variant] = {"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
|
||||
# 已拿上限判定(身份维度;缺省放行=白名单外 kind 兜底)
|
||||
func _curio_cap_ok(kind: String, feat_id: String = "") -> bool:
|
||||
return _curio.cap_ok(kind, feat_id)
|
||||
|
||||
|
||||
# Boss 钥石逐杀掉落(掉落渠道 = Boss/精英击杀;首杀/复杀两档读 meta.tres boss_keystone;
|
||||
@@ -675,8 +400,7 @@ func _boss_keystone_drop(li: int) -> void:
|
||||
first = not _save_manager.boss_first_kill_marked(li)
|
||||
var row: Dictionary[Variant, Variant] = 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
|
||||
_hero.record_keystone_drop({"level": li, "first": first, "amount": amount}) # 逐杀明细+累计单写原语
|
||||
|
||||
|
||||
# 主动放弃(仅非战斗期受理——战斗域单事务在飞;与失败同型结算,已获大奖励不追回。
|
||||
@@ -697,19 +421,19 @@ func abandon() -> bool:
|
||||
func _start_current_level() -> bool:
|
||||
_runner = LevelRunner.new(db, rng)
|
||||
_bind_level_runner()
|
||||
_stop.rebind_level_runner(_runner, level_index) # 停点件每关注入时点(GP 件入账回调换代——防跨关调死对象)
|
||||
# 断点续玩分支:图由主种子+参数档重生成同图(产图与计数器号位无关,停点零重抽语义随之消解),
|
||||
# resume_state 带停点句柄与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。
|
||||
var resume_state: Dictionary[Variant, Variant] = {}
|
||||
if not _resume_bp.is_empty():
|
||||
resume_state = {"node_id": int(_bp_read("node_id", _resume_bp, -1)),
|
||||
"visited_ids": _bp_read("visited_ids", _resume_bp, []) as Array}
|
||||
# 接口①开始关卡:生命经共享账本交接(账本当前值即开局生命——冒险开始置满 / 大奖励回满已写好);
|
||||
# _base_override 共享引用随载荷传入(永久强化 flat——LevelRunner.effective_* 叠加序第二项)。
|
||||
# 接口①开始关卡:生命经主角账共享交接(账面当前值即开局生命——冒险开始置满 / 大奖励回满已写好);
|
||||
# 主角账与冒险桶注入(set_hero——T1 过渡面,T3 收构造注入)。
|
||||
# defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在
|
||||
# 该回调重取 runner 实例),后入口候选呈现;顺序反了选路口会被呈现层的 runner 空防御静默丢弃。
|
||||
# 呈现零 RNG 消耗、pick 走同一 _enter_node——测量面逐位零漂移。
|
||||
var ok: bool = _runner.start_level_external(level_index, ledger, purchased,
|
||||
apply_rest, _base_override, true, resume_state)
|
||||
var ok: bool = _runner.start_level_external(level_index, apply_rest, true, resume_state)
|
||||
if ok:
|
||||
_inject_level_params()
|
||||
_present_level_entry(resume_state)
|
||||
@@ -728,25 +452,24 @@ func _bind_level_runner() -> void:
|
||||
_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]))
|
||||
_runner.set_run_ledger(run_ledger) # 共享四账单类下传(run_ledger=四账类,ledger=关卡账)
|
||||
_runner.set_hero(_hero, adventure_counters) # 主角账与冒险桶注入(账面单点共享——T1 过渡面)
|
||||
_runner.set_run_ledger(run_ledger) # 充能账瘦壳下传
|
||||
_runner.sync_attune_inventory() # 能力件骑库存面注入(随账下传同拍)
|
||||
|
||||
|
||||
# 关卡参数注入(装配集/特性/rule 键/类型轴/商店池/凭证件 + level_started 广播)
|
||||
func _inject_level_params() -> void:
|
||||
_runner.set_attunement(_attunement) # 同调装配下传(enc 携带进战斗结算)
|
||||
_runner.set_feats(_feats) # 特性持有集下传(begin_battle enc.feats 求值面)
|
||||
_runner.set_feats(_hero.feats) # 特性持有集下传(活引用——关内兑付 relic 当关生效;begin_battle enc.feats 求值面)
|
||||
_runner.set_tree_rules(_tree_rules) # 树 rule 键集下传(A/B 面消费)
|
||||
_runner.set_hero_axes(hero_attack_type, hero_armor_type) # 两根类型轴快照下传(effective_ac 甲型基础 AC 层+enc 携带进 spawn)
|
||||
_runner.set_hero_axes(_hero.attack_type, _hero.armor_type) # 两根类型轴快照下传(enc 携带进 spawn)
|
||||
if _shop_mode == "pool":
|
||||
# 商店共享池注入(库存账本引用+消费 handler 双下传;legacy 模式零注入零扰动。
|
||||
# cap 检查回调四参注入)
|
||||
_runner.set_shop_pool(_curio_deck, _consume_pool_purchase, _feat_ticket_consider, _curio_cap_ok)
|
||||
# 凭证件三账注入(节点级/回合级一次性:注入即清零,未用即过期=寿命档语义)
|
||||
_runner.set_voucher_grants(_pending_rap, _time_sand_queue)
|
||||
_pending_rap = 0
|
||||
_time_sand_queue = 0
|
||||
# 商店共享池注入(库存账本引用+消费 handler 双下传——消费经注入 Callable 直达停点件兑付单点;
|
||||
# legacy 模式零注入零扰动。cap 检查回调四参注入)
|
||||
_runner.set_shop_pool(_curio_deck, _stop.consume_pool_purchase, _feat_ticket_consider, _curio_cap_ok)
|
||||
# 凭证件出箱注入(节点级/回合级一次性:注入即清零,未用即过期=寿命档语义)
|
||||
var outbox: Dictionary[Variant, Variant] = _stop.voucher_outbox()
|
||||
_runner.set_voucher_grants(int(outbox.get("rap", 0)), int(outbox.get("time_sand", 0)))
|
||||
# 三选一道具件跨关携带注入面不存在(aux 卡不在池种内→ carry 恒空集;
|
||||
# 容器字段保留=替换子交互惰性承载,零投放零行为)
|
||||
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
|
||||
@@ -769,7 +492,7 @@ func _on_level_finished(payload: Dictionary) -> void:
|
||||
_last_level_payload = payload.duplicate(true)
|
||||
var result := String(payload.get("result", ""))
|
||||
if result == "defeat":
|
||||
ledger["hero_hp"] = 0 # 倒下记 0(生命账本战后回写口径)
|
||||
_hero.hero_hp = 0 # 倒下记 0(生命账本战后回写口径)
|
||||
_settle("failed")
|
||||
return
|
||||
if result != "victory":
|
||||
@@ -779,31 +502,12 @@ func _on_level_finished(payload: Dictionary) -> void:
|
||||
# Boss 钥石逐杀掉落(击杀掉落渠道,末关照常入账;首杀/复杀读 meta.tres boss_keystone)
|
||||
_boss_keystone_drop(level_index)
|
||||
if level_index < levels_total:
|
||||
# 关卡完成兑付(非末关恰一次;三层奖励框架):部分回血先行 → 加权洗牌掷 3 → 三选一停点
|
||||
#(顺序即契约——回血按当刻账本与上限结算,选择停点零自动推进)
|
||||
var hp_before := int(ledger.get("hero_hp", 0))
|
||||
var max_eff := effective_max_hp() # 回血封顶口径含调试浮窗 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 必须已就位)
|
||||
# 关卡完成兑付(非末关恰一次;三层奖励框架):编排归停点件——推进位先于 emit 置位
|
||||
#(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位);
|
||||
# 停点件内部分回血先行 → 加权洗牌掷 3 → 三选一停点(顺序即契约——回血按当刻账本与上限结算,
|
||||
# 选择停点零自动推进)
|
||||
_pending_next_level = true
|
||||
_reset_refresh_x() # 碰三选一重置 X 余量(不结转)
|
||||
_curio_paid_refreshes = 0 # 付费刷计数随停点清零(对齐商店进店清零)
|
||||
var pick_payload: Dictionary[Variant, Variant] = {
|
||||
"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 扩签(关卡末清空反馈取数)
|
||||
}
|
||||
_curio_pending = {"candidates": (pick_payload["candidates"] as Array).duplicate(true),
|
||||
"payload": pick_payload.duplicate(true)}
|
||||
curio_choice_presented.emit(pick_payload)
|
||||
_snapshot_breakpoint() # 选择停点可中断(契约 5——续玩不重抽不静默丢失)
|
||||
_stop.open_level_clear(payload)
|
||||
return
|
||||
# 末关的关卡完成:不发三选一与部分回血(没有下一战可受益)、Boss 钥石照常入账——冒险完成结算
|
||||
_settle("completed")
|
||||
@@ -816,13 +520,13 @@ func _settle(result: String) -> void:
|
||||
"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,
|
||||
"keystone_total": _hero.keystone_gained(),
|
||||
"master_seed": master_seed,
|
||||
"aux_cleared_last": _last_treasure_cleared, # additive 扩签(末关清空反馈;非末关走 bonus 载荷)
|
||||
"keystone_drops": _keystone_drops.duplicate(true), # additive 扩签(逐杀掉落明细;settle 入账明细/复盘归因数据源)
|
||||
"vision_used_delta": int(run_ledger.vision_counter.get("used", 0)), # additive 扩签(真视使用本局计数;settle 聚合入档)
|
||||
"keystone_drops": _hero.keystone_drop_log(), # additive 扩签(逐杀掉落明细;settle 入账明细/复盘归因数据源)
|
||||
"vision_used_delta": _hero.vision_used(), # additive 扩签(真视使用本局计数;settle 聚合入档)
|
||||
"pool_stock": _pool_stock_snapshot(), # additive 扩签(终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面)
|
||||
"relics": _feats.duplicate(), # additive 扩签(特性终局持有集:清账前快照,成型判读取数面)
|
||||
"relics": _hero.feats_held(), # additive 扩签(特性终局持有集:清账前快照,成型判读取数面)
|
||||
}
|
||||
# 败退明细(数值验证工具取数面——tools/balance_verifier.py 止步节点分布数据源;
|
||||
# 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知)
|
||||
@@ -910,39 +614,36 @@ func _bp_parts() -> Dictionary[Variant, Variant]:
|
||||
treasure_snapshot = _runner.treasure_inventory.snapshot_inventory()
|
||||
# 三选一停点续玩分支 _runner 为 null(关卡已收束、下一关未开)——节点域字段按缺省落档,
|
||||
# 该两态的续玩只消费 bonus_* 附随件(选择停点/已兑付停点快照)
|
||||
var hero_frag: Dictionary[Variant, Variant] = _hero.breakpoint_fragment() # 主角账片段(账本键+获得日记——T4 拼盘收口前先同源取数)
|
||||
var stop_frag: Dictionary[Variant, Variant] = _stop.breakpoint_fragment() # 停点件片段(T4 拼盘收口前先同源取数)
|
||||
return {
|
||||
"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(run_ledger.bonus.get("atk", 0)),
|
||||
"max_hp": int(run_ledger.bonus.get("max_hp", 0)),
|
||||
"armor_class": int(run_ledger.bonus.get("armor_class", 0)),
|
||||
"ap": int(run_ledger.bonus.get("ap", 0)),
|
||||
},
|
||||
"gp": int(ledger.get("gp", 0)),
|
||||
"shop_purchased": purchased.duplicate(true),
|
||||
"hero_hp": int(hero_frag.get("hero_hp", 0)),
|
||||
"bonus": hero_frag.get("bonus", {}),
|
||||
"gp": int(hero_frag.get("gp", 0)),
|
||||
"shop_purchased": hero_frag.get("shop_purchased", {}),
|
||||
"shop_shelf": _runner.shop_bp_shelf() if _runner != null else [], # 货架入断点(离开店/非店停点 = 空架合法态)
|
||||
"shop_visit_buys": _runner.shop_bp_visit_buys() if _runner != null else 0, # 本次进店已购次数(A 链定价输入)
|
||||
"attunement_charges": run_ledger.attune_charges.duplicate(true), # 同调主动件充能余量随断点(save v4 域)
|
||||
"graph_gen_v2": 1, # 生成图换代代差标记(缺键 = 换代前旧断点 → resume 显式拒绝)
|
||||
"master_seed": master_seed,
|
||||
"rng_counter": rng.counter(),
|
||||
"keystone": keystone,
|
||||
"keystone": int(hero_frag.get("keystone", 0)),
|
||||
"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(), # 特性持有集(局内随机渠道获得态续玩防丢失)
|
||||
"bonus_pending": _curio_pending.duplicate(true), # 三选一停点态(空 = 无停点——选择停点可中断契约)
|
||||
"feat_ticket_pity": _feat_ticket_pity, # T5 专长凭证保底计数(扩列向后兼容——旧档缺键缺省 0)
|
||||
"feat_ticket_sold": _feat_ticket_sold, # T5 专长凭证限购计数(同上)
|
||||
"adventure_counters": run_ledger.adventure_counters.duplicate(true), # adventure 域计数器随断点(续玩跨战账不丢)
|
||||
"vision_counter_used": int(run_ledger.vision_counter.get("used", 0)), # 真视使用计数随断点(续玩跨战账不丢)
|
||||
"refresh_stock": _refresh_stock, # 刷新存量账随断点
|
||||
"attune_refresh_y": _attune_refresh_y, # Y 冒险周期池余量随断点(续玩不再自参数面整额重注入=堵「用完退菜单再续玩满血复活」可利用漏洞;扩列向后兼容——旧档缺键回退参数面)
|
||||
"bonus_result": _curio_result.duplicate(true), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
|
||||
"keystone_drops": _keystone_drops.duplicate(true), # 逐杀掉落明细(摘要归因续接)
|
||||
"relics": hero_frag.get("relics", []), # 特性持有集(局内随机渠道获得态续玩防丢失)
|
||||
"bonus_pending": stop_frag.get("bonus_pending", {}), # 三选一停点态(空 = 无停点——选择停点可中断契约)
|
||||
"feat_ticket_pity": int(stop_frag.get("feat_ticket_pity", 0)), # T5 专长凭证保底计数(扩列向后兼容——旧档缺键缺省 0)
|
||||
"feat_ticket_sold": int(stop_frag.get("feat_ticket_sold", 0)), # T5 专长凭证限购计数(同上)
|
||||
"adventure_counters": adventure_counters.duplicate(true), # adventure 域计数器随断点(续玩跨战账不丢——桶归宿主字典)
|
||||
"vision_counter_used": int(hero_frag.get("vision_counter_used", 0)), # 真视使用计数随断点(续玩跨战账不丢)
|
||||
"refresh_stock": int(stop_frag.get("refresh_stock", 0)), # 刷新存量账随断点
|
||||
"attune_refresh_y": int(stop_frag.get("attune_refresh_y", 0)), # Y 冒险周期池余量随断点(续玩不再自参数面整额重注入=堵「用完退菜单再续玩满血复活」可利用漏洞;扩列向后兼容——旧档缺键回退参数面)
|
||||
"bonus_result": stop_frag.get("bonus_result", {}), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
|
||||
"keystone_drops": hero_frag.get("keystone_drops", []), # 逐杀掉落明细(摘要归因续接)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -72,7 +72,8 @@ var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换
|
||||
var _seal_consumed := {} # 兑付资格以危险格实例为键(整图;换图重置)
|
||||
# 同调装配运行态(begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动)
|
||||
var attunement: Array[Variant] = [] # 装备中同调件 item_id 集
|
||||
var _ledger: RunLedger = null # 共享四本账(begin_battle 经 enc["ledger"] 持引用;缺省新账=调试面)
|
||||
var _ledger: RunLedger = null # 同调充能账瘦壳(begin_battle 经 enc["ledger"] 持引用;缺省新账=调试面)
|
||||
var _hero: HeroLedger = null # 主角实体账(begin_battle 经 enc["hero"] 持引用;null=调试/单场直构面——账面消费跳过)
|
||||
var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(装配集 ap_charge_safe 行扫描;满 charge_n 自动兑付当轮 AP+1,无玩家发起入口)
|
||||
# ---- 效果引擎扩签运行态(新效果类型施加面;空订阅零扰动)----
|
||||
var _fx_hero_ac_base := -1 # 主角 AC 基线快照(enc.hero_armor_class;-1=表值兜底未解析)
|
||||
@@ -386,7 +387,10 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
_fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
var ledger_ref = enc.get("ledger", null)
|
||||
_ledger = ledger_ref if ledger_ref is RunLedger else RunLedger.new() # 缺省新账=调试/单场零扰动面
|
||||
counters_ctx.adventure_bucket = _ledger.adventure_counters # 挂冒险桶引用(跨战持久=同一字典引用,非副本搬运)
|
||||
var hero_ref = enc.get("hero", null)
|
||||
_hero = hero_ref if hero_ref is HeroLedger else null # null=调试/单场直构——账面消费面跳过(空账守卫同语义)
|
||||
var bucket_ref: Variant = enc.get("adv_bucket", null)
|
||||
counters_ctx.adventure_bucket = bucket_ref if bucket_ref is Dictionary else {} # 挂冒险桶引用(归宿主字典——跨战持久=同一字典引用;直构兜底自有空字典)
|
||||
var level_ref: Variant = enc.get("level_counters")
|
||||
if level_ref is Dictionary:
|
||||
counters_ctx.level_bucket = level_ref # 挂关卡桶引用(LevelRunner 每关 new 下传)
|
||||
@@ -763,10 +767,10 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
|
||||
if not rec.is_empty() and String(rec.get("source_class", "")) == "attunement":
|
||||
_ledger.attune_charge_consume(String(rec.get("item_id", "")))
|
||||
# 真视类工具使用计数(信标网络解锁行为条件:受理成功恰计一次;
|
||||
# 共享 dict 引用下传〔attune_charges 同款〕,冒险收束聚合入档 save.add_vision_used)
|
||||
# 主角账单点记录,冒险收束聚合入档 save.add_vision_used)
|
||||
if kinds.has("reveal_tiles") or kinds.has("safe_dig") or kinds.has("mine_reveal"):
|
||||
if not _ledger.vision_counter.is_empty():
|
||||
_ledger.vision_count_add(1) # 受理成功恰计一次(缺省空账=调试不计)
|
||||
if _hero != null:
|
||||
_hero.record_vision_use() # 受理成功恰计一次(未注入=调试直构不计——空账守卫同语义)
|
||||
return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec}
|
||||
|
||||
|
||||
@@ -1127,11 +1131,10 @@ func _battle_over(result: String) -> void:
|
||||
if String(h.get("kind", "")) == "heal":
|
||||
var hu_w: Dictionary[Variant, Variant] = _enemy_system.hero_unit()
|
||||
hu_w["hp"] = mini(int(hu_w.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_w.get("max_hp", 1))))
|
||||
# bonus_delta 战末汇入(血税双向账:本战累积并入冒险加算区共享引用后清零)+
|
||||
# adventure 域计数器导出回写(T6——共享引用覆写,attune_charges 先例)
|
||||
if not _ledger.bonus.is_empty():
|
||||
# bonus_delta 战末汇入(血税双向账:本战累积经主角账单写原语汇入获得日记三层后清零)
|
||||
if _hero != null:
|
||||
for axis in _fx_bonus_delta.keys():
|
||||
_ledger.bonus_add_axis(axis, int(_fx_bonus_delta[axis]))
|
||||
_hero.gain_bonus(String(axis), int(_fx_bonus_delta[axis]), "battle_export", String(axis))
|
||||
_fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
# 搬运链删除:adventure 桶经 ctx 引用直写 RunLedger,无战末导出时点(旧导出/汇入搬运链删除)
|
||||
# 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提)
|
||||
|
||||
+1
-22
@@ -1,7 +1,6 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 令牌池件:三选一/商店共享池的持久多重集、抽签、库存与专长凭证判定唯一所有者(自 adventure_runner 拆出,零行为变更)。
|
||||
## 停点编排与兑付推进不在此件(留冒险编排器)——件只管池与判定;随机只消费注入的 RngServiceCore(reward_pick 封闭流)。
|
||||
## 宿主兑付单点经 bind_handlers 注入方法 Callable(选中摘牌后的加算区/账本落点仍在宿主)。
|
||||
## 停点编排与兑付推进归 CurioStop 件——件只管池与判定(纯池化:反向回调退役);随机只消费注入的 RngServiceCore(reward_pick 封闭流)。
|
||||
class_name CurioDeck
|
||||
extends RefCounted
|
||||
|
||||
@@ -12,8 +11,6 @@ var taken: Dictionary[Variant, Variant] = {} # 件获取次数计
|
||||
var ticket_pity := 0 # 专长凭证保底计数(展示次数;触发清零)
|
||||
var ticket_sold := 0 # 专长凭证限购计数(选中/买下即 +1)
|
||||
var ticket_appeared := 0 # 专长凭证出现计数(quota 上限面)
|
||||
var _apply_pick := Callable() # 宿主兑付回调(AdventureRunner._apply_curio_pick)
|
||||
var _redeem_ticket := Callable() # 宿主凭证兑付回调(AdventureRunner._redeem_feat_ticket)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
@@ -21,12 +18,6 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
rng = rng_ref
|
||||
|
||||
|
||||
# 宿主回调注入(商店购买消费面经此回宿主兑付单点——core↔core 方法 Callable,禁 lambda 捕获 self)
|
||||
func bind_handlers(apply_pick: Callable, redeem_ticket: Callable) -> void:
|
||||
_apply_pick = apply_pick
|
||||
_redeem_ticket = redeem_ticket
|
||||
|
||||
|
||||
# 池与计数归零(宿主停点复位调用的池半;令牌池按表配额重建——池消耗即上限,账本随池重生)
|
||||
func reset() -> void:
|
||||
taken = {"attack": 0, "max_hp": 0, "armor": 0, "gp": 0, "ap": 0, "heal": 0}
|
||||
@@ -101,18 +92,6 @@ func consume_deck_token(kind: String, tier: String, feat_id: String = "") -> boo
|
||||
return false
|
||||
|
||||
|
||||
# 商店购买消费入口(LevelRunner.buy_pool_item 经 Callable 调用):专长凭证买下即开(零摘牌不进持有集);
|
||||
# 其余件同口回宿主兑付单点(消费在架令牌本体 kind+tier——写加算区+账本+摘牌与奖励三选一真单点)。
|
||||
# 未知 kind 拒绝(货架词表封闭的前置防御)。
|
||||
func consume_pool_purchase(kind: String, tier: String) -> bool:
|
||||
if kind == "feat_ticket":
|
||||
return bool(_redeem_ticket.call())
|
||||
if not db.adventure.CURIO_KINDS.has(kind):
|
||||
push_error("[冒险结构] 商店购买消费拒绝——件不在池词表: %s" % kind)
|
||||
return false
|
||||
return bool(_apply_pick.call(kind, tier, int(db.adventure.curio_value(kind, tier))))
|
||||
|
||||
|
||||
# 已拿上限判定(身份维度:kind 走 curio_cap、relic 走 feats 行 cap 字段;
|
||||
# taken 只在兑付时 +1〔拿了才计〕、刷出不计)。缺省放行=白名单外 kind 兜底。
|
||||
func cap_ok(kind: String, feat_id: String = "") -> bool:
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
## 三选一停点件:停点编排唯一所有者(冒险域拆件批自 AdventureRunner 迁入——架构原则 9 本域承载)。
|
||||
## 停点三态(关卡完成三选一在途/已兑付待推进/奖励节点三选一在途);刷新三源账(X 刷新件每碰三选一
|
||||
## 重给/优先消耗不结转、Y 营地升级行冒险周期池/用完即无、刷新券存量账/冒险级跨停点持久);兑付方法族
|
||||
## (pick/refresh/redeem/skip);凭证出箱(RAP/时沙待注入——战首取走清零);三选一信号面自带。
|
||||
## 零自动推进停点语义承载:兑付只经 pick/apply 显式入口,停点在途时推进被拒(宿主关序受理读放行谓词)。
|
||||
## 生命周期=每冒险一次(AdventureRunner 冒险开始建;关卡换实例不换件——每关注入时点经 rebind_level_runner)。
|
||||
## 随机只经注入 CurioDeck 的 reward_pick 封闭流;兑付落账经注入 HeroLedger 单写原语;落盘经注入方法
|
||||
## Callable(core↔core 接线禁 lambda 捕获 self)。Core 层零 UI。
|
||||
class_name CurioStop
|
||||
extends RefCounted
|
||||
|
||||
signal curio_choice_presented(payload: Dictionary) ## 专长三选一停点(非末关的关卡完成恰一次;通道白名单后候选恒专长+回血在载荷)——{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}
|
||||
|
||||
var db: ConfigDb
|
||||
var _deck: CurioDeck # 令牌池件(抽签/摘牌/cap/凭证判定——纯池)
|
||||
var _hero: HeroLedger # 主角账(兑付落账单写原语——加算区/生命/特性)
|
||||
var _snapshot_call := Callable() # 落盘回调(宿主断点快照受理——发射点后触发)
|
||||
var _grant_gp := Callable() # 当前关卡对象 grant_curio_gp 方法 Callable(GP 件入账维持现路径与 wallet_changed 发射序——每关注入时点更新,防调死对象)
|
||||
# ---- 停点三态 ----
|
||||
var _pending := {} # 关卡完成三选一在途:{"candidates": Array, "payload": Dictionary}——空 = 无停点
|
||||
var _resolved := {} # 已兑付待推进态(curio_resolved 载荷副本——中断续玩不重兑付)
|
||||
var _node_pending := {} # 奖励节点三选一停点态(共享池;断点不载:续玩重建到选路停点重走,恢复的计数器保证同候选重抽)
|
||||
# ---- 刷新三源账 ----
|
||||
var _refresh_x := 0 # 刷新件余量(每碰三选一重置=X 表值;优先消耗、不结转)
|
||||
var _refresh_y := 0 # Y 冒险周期池(开局参数面注入,用完即无——营地升级行表值)
|
||||
var _paid_refreshes := 0 # 本次停点已付费刷新次数(对齐商店 MC-1——GP 刷新价曲线输入,随停点创建清零=与商店进店清零同构)
|
||||
var _refresh_stock := 0 # 刷新存量账(冒险级——刷新券加仓;**不经 _refresh_x**:X 每停点重置会吞存量=写错账缺陷 F-1)
|
||||
# ---- 凭证出箱(奇物.md 凭证件表——节点级/回合级一次性,战首取走清零)----
|
||||
var _pending_rap := 0 # 储备金袋待注入(下一战 RAP +N,战首注入后清零)
|
||||
var _time_sand_queue := 0 # 时沙待注入(下一战首轮 AP +N,战首入队后清零)
|
||||
var _level_index := 0 # 兑付载荷关卡序号(宿主每关注入——与 AdventureRunner.level_index 同源单写)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, deck_ref: CurioDeck, hero_ref: HeroLedger, snapshot_call: Callable) -> void:
|
||||
db = db_ref
|
||||
_deck = deck_ref
|
||||
_hero = hero_ref
|
||||
_snapshot_call = snapshot_call
|
||||
|
||||
|
||||
# 每关注入时点(AdventureRunner._start_current_level 建件接线处调用):关卡序号快照+
|
||||
# 当前关卡对象 GP 入账方法 Callable(第二关起绑新 runner——防跨关调死对象)
|
||||
func rebind_level_runner(runner: LevelRunner, level_index: int) -> void:
|
||||
_level_index = level_index
|
||||
if runner != null:
|
||||
_grant_gp = runner.grant_curio_gp
|
||||
|
||||
|
||||
# ---- 五组入口之一:关卡完成开停点(宿主结算受理调用——回血先行、抽签、发信号、落盘)----
|
||||
|
||||
# 关卡完成兑付(非末关恰一次;三层奖励框架):部分回血先行 → 加权洗牌掷 3 → 三选一停点
|
||||
#(顺序即契约——回血按当刻账本与上限结算,选择停点零自动推进)。调用方(宿主关序)须先置
|
||||
# 「已兑付待推进」关序位(信号同步派发——处理器内立即确认时推进位必须已就位)。
|
||||
func open_level_clear(payload_base: Dictionary) -> void:
|
||||
var hp_before := _hero.hero_hp
|
||||
var max_eff := _hero.effective_max_hp() # 回血封顶口径含调试浮窗 pct(同战斗内上限)
|
||||
var heal := mini(int(floor(float(max_eff) * float(int(db.adventure.clear_heal_pct)) / 100.0)), max_eff - hp_before)
|
||||
_hero.hero_hp = hp_before + heal
|
||||
_reset_refresh_x() # 碰三选一重置 X 余量(不结转)
|
||||
_paid_refreshes = 0 # 付费刷计数随停点清零(对齐商店进店清零)
|
||||
var pick_payload: Dictionary[Variant, Variant] = {
|
||||
"level_index": _level_index,
|
||||
"candidates": draw_candidates("level_clear"), # T4 渠道白名单——关底 Boss 击败=关卡完成渠道只出专长
|
||||
"heal_gained": heal,
|
||||
"hp_before": hp_before,
|
||||
"hp_after": _hero.hero_hp,
|
||||
"keystone_gained": int(_hero.keystone_drop_log().back().get("amount", 0)) if not _hero.keystone_drop_log().is_empty() else 0,
|
||||
"keystone_total": _hero.keystone_gained(),
|
||||
"gp": _hero.gp,
|
||||
"aux_cleared": int(payload_base.get("aux_cleared", 0)), # additive 扩签(关卡末清空反馈取数)
|
||||
}
|
||||
_pending = {"candidates": (pick_payload["candidates"] as Array).duplicate(true),
|
||||
"payload": pick_payload.duplicate(true)}
|
||||
curio_choice_presented.emit(pick_payload)
|
||||
if _snapshot_call.is_valid():
|
||||
_snapshot_call.call() # 选择停点可中断(契约 5——续玩不重抽不静默丢失)
|
||||
|
||||
|
||||
# ---- 五组入口之二:奖励节点三选一 ----
|
||||
|
||||
# 候选呈现(节点到达时调用——恰一 reward_pick scope 洗牌 + 道具件掷定 scope;本停点断点不载,
|
||||
# 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)。
|
||||
func node_curio_candidates() -> Dictionary:
|
||||
var cands := draw_candidates("reward_node")
|
||||
return {"candidates": cands}
|
||||
|
||||
|
||||
func present_node_curio(candidates: Array) -> void:
|
||||
_reset_refresh_x() # 碰三选一重置 X 余量(不结转)
|
||||
_paid_refreshes = 0 # 付费刷计数随停点清零(对齐商店进店清零)
|
||||
_node_pending = {"candidates": candidates.duplicate(true)}
|
||||
|
||||
|
||||
# ---- 五组入口之三:兑付族(选择停点零自动推进、重复兑付被拒;兑付恰一件、未选项零生效)----
|
||||
|
||||
# 玩家点选兑付(呈现层/冒烟/自动模拟确认入口):令牌 ∈ 停点候选(身份键精确匹配——relic 件
|
||||
# 另须 feat_id)。属性件写加算区、AP 件写第四轴、GP 件走钱包「奖励入账」入口、恢复件当即回血、
|
||||
# relic 件入特性持有集(🔴 层级纪律:只走局内随机渠道)、道具件受同调位约束。
|
||||
# 消费=选中令牌踢池(未选回池由「抽签不摘牌、兑付才摘牌」语义承载——TP0 v2.2)。
|
||||
func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
|
||||
if _pending.is_empty():
|
||||
push_error("[停点件] pick_curio 拒绝——无待选停点")
|
||||
return false
|
||||
var candidates: Array[Variant] = _pending.get("candidates", []) as Array
|
||||
if _deck.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 := _hero.hero_hp
|
||||
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 伴随键(反馈行取数面;非 relic 件零新增键)
|
||||
func _feat_payload_keys(feat_id: String) -> Dictionary:
|
||||
if feat_id == "":
|
||||
return {}
|
||||
var row: Dictionary[Variant, Variant] = db.feat_row(feat_id)
|
||||
return {"feat_id": feat_id, "feat_name": String(row.get("display_name", "")), "feat_archetype": String(row.get("archetype", ""))}
|
||||
|
||||
|
||||
# 三选一件兑付共享体(TP0——令牌消费、摘牌单点,含 relic 分支):
|
||||
# 属性件写加算区 + 件获取计数、AP 件写第四轴、GP 件走钱包、恢复件当即回血(封顶生效上限——
|
||||
# 与账本置满/关卡完成回血同式 effective_max_hp)、relic 件入持有集(散件单体保底——效果接线面
|
||||
# 按挂点消费,档位质变由 FeatsEngine 求值)。兑付即从牌库摘除该令牌(选中踢池;
|
||||
# 未选候选从未摘牌=自动回池)。道具件不入此体——未知件返回 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 _deck.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——刷新券入刷新存量账(冒险级;**不写 _refresh_x**:X 每停点 _reset_refresh_x 重置会吞存量;摘牌先于落账=选中即消尽)
|
||||
_bump_taken("refresh")
|
||||
return true
|
||||
if kind == "rap":
|
||||
_pending_rap += value # T4——储备金袋:下一战 RAP +N(节点级,战首注入后清零)
|
||||
_bump_taken("rap")
|
||||
return true
|
||||
if kind == "time_sand":
|
||||
_time_sand_queue += value # T4——时沙:下一战首轮 AP +N(回合级池增量,不改上限)
|
||||
_bump_taken("time_sand")
|
||||
return true
|
||||
if kind == "relic":
|
||||
if not _hero.feats.has(feat_id):
|
||||
_hero.feats.append(feat_id)
|
||||
_bump_taken("relic")
|
||||
_bump_taken("relic:" + feat_id) # 身份维已拿计数(cap 消费面)
|
||||
return true
|
||||
# 加算形态路由(match 收敛进效果类型:属性值提升四类走
|
||||
# adventure_table.CURIO_APPLY_ROUTES 声明路由(bonus 落点键),加新加算类令牌零代码)
|
||||
var sink := db.adventure.curio_apply_route(kind)
|
||||
if sink != "":
|
||||
_hero.gain_bonus(sink, value, "curio_pick", kind) # 加算区单写原语(获得日记明细+汇总同拍)
|
||||
_bump_taken(kind)
|
||||
return true
|
||||
# 通道面(副作用形态——显式分支)
|
||||
match kind:
|
||||
"gp":
|
||||
if _grant_gp.is_valid():
|
||||
_grant_gp.call(value)
|
||||
_bump_taken("gp")
|
||||
"heal":
|
||||
_hero.hero_hp = mini(_hero.hero_hp + value, _hero.effective_max_hp())
|
||||
_bump_taken("heal")
|
||||
_:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# 件获取次数计数递增(kind 维度派生账本——池件侧承载)
|
||||
func _bump_taken(kind: String) -> void:
|
||||
_deck.taken[kind] = int(_deck.taken.get(kind, 0)) + 1
|
||||
|
||||
|
||||
# 兑付收口(恰一次):清停点态 → 记已兑付态 → emit → 停点快照(等继续确认——中断续玩不重兑付)。
|
||||
# picked = kind;tier 随载荷(TP0——大小档位读数面);relic 伴随键随载荷(反馈行取数)
|
||||
func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String = "", feat_id: String = "") -> void:
|
||||
_pending = {}
|
||||
_resolved = {
|
||||
"picked": picked,
|
||||
"tier": tier,
|
||||
"value": value,
|
||||
"hp_before": hp_before,
|
||||
"hp_after": _hero.hero_hp,
|
||||
"level_index": _level_index,
|
||||
}
|
||||
_resolved.merge(_feat_payload_keys(feat_id))
|
||||
curio_resolved.emit(_resolved.duplicate(true))
|
||||
if _snapshot_call.is_valid():
|
||||
_snapshot_call.call()
|
||||
|
||||
|
||||
# 点选兑付(节点域——无部分回血无钥石;令牌消费与牌库摘牌与关卡完成三选一同库;relic 同权入持有集)
|
||||
func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") -> bool:
|
||||
if kind == "feat_ticket":
|
||||
return _redeem_feat_ticket() # T5 拿到即开——节点停点候选替换为专长三选一(呈现层本地重绘)
|
||||
if _node_pending.is_empty():
|
||||
push_error("[停点件] apply_node_curio 拒绝——无待选停点")
|
||||
return false
|
||||
var candidates: Array[Variant] = _node_pending.get("candidates", []) as Array
|
||||
if _deck.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
|
||||
|
||||
|
||||
# 停点空候选消化(池空=三选一环节整体跳过:候选 0 件时停点视同已处理,
|
||||
# 呈现层跳开选择面直接走后续推进。仅空候选合法;非空停点拒绝——零自动推进契约不破,正常
|
||||
# 消化仍只走 pick_curio/apply_node_curio 兑付。触发源=渠道白名单过滤后池空(关卡 Boss 击破=关卡完成
|
||||
# 渠道只出专长,专长零配置/旧档 deck 无 relic 余库即空集)。)
|
||||
func skip_empty_curio() -> bool:
|
||||
if _pending.is_empty():
|
||||
return false
|
||||
if not (_pending.get("candidates", []) as Array).is_empty():
|
||||
return false
|
||||
_pending = {}
|
||||
return true
|
||||
|
||||
|
||||
# 专长凭证兑付(拿到即开——停点候选替换为专长三选一;不进持有集/零摘牌;限购计数 +1):
|
||||
# 节点停点=本地替换(呈现层读在途态重绘);店内无停点=临时停点 + payload.context 标记(呈现层
|
||||
# 重 present 无胜利总结面,确认后只关面回店)。
|
||||
func _redeem_feat_ticket() -> bool:
|
||||
_deck.ticket_sold += 1 # 限购计数(本体在池件——原宿主转发属性迁点)
|
||||
_deck.ticket_pity = 0
|
||||
var cands := draw_candidates("level_clear") # 专长池(与关卡完成渠道同白名单)
|
||||
if cands.is_empty():
|
||||
push_error("[停点件] 专长凭证兑付失败——专长池空集")
|
||||
_deck.ticket_sold -= 1
|
||||
return false
|
||||
if not _node_pending.is_empty():
|
||||
_node_pending["candidates"] = cands.duplicate(true)
|
||||
return true
|
||||
var payload: Dictionary[Variant, Variant] = {"context": "feat_ticket", "candidates": cands.duplicate(true)}
|
||||
_pending = {"candidates": cands.duplicate(true), "payload": payload.duplicate(true)}
|
||||
curio_choice_presented.emit(payload)
|
||||
if _snapshot_call.is_valid():
|
||||
_snapshot_call.call()
|
||||
return true
|
||||
|
||||
|
||||
# ---- 五组入口之四:刷新族(X/Y 双源:X=刷新件每碰三选一重给〔优先消耗、不结转〕、
|
||||
# Y=营地升级行冒险周期池〔用完即无〕;重抽消费 reward_pick 既有洗牌流,池规则全保留——不放回/
|
||||
# 达上限剔牌/掷定前置直显;停点可中断)----
|
||||
|
||||
# 刷新件余量基数(kind 分发查询——装配集内 effect_kinds 含 bonus_refresh 的行 magnitude 合计;
|
||||
# 刷新件类加件零 id 分支)
|
||||
func _attune_refresh_x_total() -> int:
|
||||
var total := 0
|
||||
for iid_v in _attunement:
|
||||
var arow: Dictionary[Variant, Variant] = 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
|
||||
|
||||
|
||||
var _attunement: Array[Variant] = [] # 同调装配集(活引用——宿主冒险开始注入,刷新件基数查询消费面)
|
||||
|
||||
|
||||
func set_attunement(ids: Array[Variant]) -> void:
|
||||
_attunement = ids
|
||||
|
||||
|
||||
func _reset_refresh_x() -> void:
|
||||
_refresh_x = _attune_refresh_x_total()
|
||||
|
||||
|
||||
# 当前可刷新次数(X 优先 + Y 池 + 刷新存量账——呈现层余量读数口)
|
||||
func attune_refresh_left() -> int:
|
||||
return _refresh_x + _refresh_y + _refresh_stock
|
||||
|
||||
|
||||
# 刷新存量账读数(T4——刷新券加仓余量单独读面;呈现层与断点消费)
|
||||
func refresh_stock() -> int:
|
||||
return _refresh_stock
|
||||
|
||||
|
||||
# 节点停点在途态读回(组合根重 present 用——candidates 值拷贝)
|
||||
func node_curio_pending_state() -> Dictionary:
|
||||
return {
|
||||
"candidates": (_node_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 付费刷新(对齐商店 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(_paid_refreshes))))
|
||||
|
||||
|
||||
# 三选一停点在途谓词(付费路径扣费前置校验——无停点禁扣钱,防空转烧钱包)
|
||||
func curio_stop_pending() -> bool:
|
||||
return not _pending.is_empty() or not _node_pending.is_empty()
|
||||
|
||||
|
||||
# 放行谓词(宿主关序受理——已兑付待推进且无在途停点)
|
||||
func stop_resolved() -> bool:
|
||||
return not _resolved.is_empty() and _pending.is_empty()
|
||||
|
||||
|
||||
func _refresh_curio_common(paid: bool) -> bool:
|
||||
if not curio_stop_pending():
|
||||
return false
|
||||
if paid:
|
||||
_paid_refreshes += 1
|
||||
else:
|
||||
# 免费位消耗序:X(刷新件每停点重给,优先消耗)→ Y(营地冒险周期池)→ 存量账(刷新券加仓,
|
||||
# 跨停点持久)
|
||||
if _refresh_x <= 0 and _refresh_y <= 0 and _refresh_stock <= 0:
|
||||
return false
|
||||
if _refresh_x > 0:
|
||||
_refresh_x -= 1
|
||||
elif _refresh_y > 0:
|
||||
_refresh_y -= 1
|
||||
else:
|
||||
_refresh_stock -= 1
|
||||
if not _pending.is_empty():
|
||||
var cands := draw_candidates("level_clear")
|
||||
_pending["candidates"] = cands.duplicate(true)
|
||||
var payload: Dictionary[Variant, Variant] = (_pending.get("payload", {}) as Dictionary).duplicate(true)
|
||||
payload["candidates"] = cands
|
||||
payload["refreshed"] = true # 重发载荷带标——呈现层直落选择面,不再重开胜利总结面
|
||||
curio_choice_presented.emit(payload) # 同信号重 present(零自动推进契约不破——停点仍在途)
|
||||
if _snapshot_call.is_valid():
|
||||
_snapshot_call.call()
|
||||
return true
|
||||
if not _node_pending.is_empty():
|
||||
var cands2 := draw_candidates("reward_node")
|
||||
_node_pending["candidates"] = cands2.duplicate(true)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id: String = "") -> void:
|
||||
_node_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)
|
||||
|
||||
|
||||
# 三选一候选抽签(实装归 CurioDeck 件;消费 reward_pick 封闭流——一次停点恰一 scope)
|
||||
func draw_candidates(channel: String = "reward_node") -> Array:
|
||||
return _deck.draw_candidates(channel)
|
||||
|
||||
|
||||
# —— 专长获取专长凭证四口径判定(实装归 CurioDeck 件:调用即一次展示判定与记账)——
|
||||
func feat_ticket_consider(rg: RandomNumberGenerator) -> bool:
|
||||
return _deck.feat_ticket_consider(rg)
|
||||
|
||||
|
||||
# 已拿上限判定(身份维度;缺省放行=白名单外 kind 兜底)
|
||||
func curio_cap_ok(kind: String, feat_id: String = "") -> bool:
|
||||
return _deck.cap_ok(kind, feat_id)
|
||||
|
||||
|
||||
# 商店共享池消费单点(兑付单点——ShopSession.buy_pool_item 经注入 Callable 调用,回 _apply_curio_pick
|
||||
# 同口;专长凭证买下即开零摘牌)。未知 kind 拒绝(货架词表封闭的前置防御)。
|
||||
func consume_pool_purchase(kind: String, tier: String) -> bool:
|
||||
if kind == "feat_ticket":
|
||||
return _redeem_feat_ticket()
|
||||
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)))
|
||||
|
||||
|
||||
# 库存余量(池消耗即上限;奖励摘牌与商店购买同库承载,全冒险一次性)
|
||||
func pool_stock(kind: String) -> int:
|
||||
return _deck.pool_stock(kind)
|
||||
|
||||
|
||||
# 终局池快照(_settle 清账前调用——复位会重置计数,摘要 additive 键须在此前取数)
|
||||
func pool_stock_snapshot() -> Dictionary:
|
||||
return _deck.stock_snapshot()
|
||||
|
||||
|
||||
# ---- 冒险级复位(宿主 _reset_curio_state 调用——停点半:池归 deck.reset、停点三态与刷新三源清零)----
|
||||
func reset_adventure_state() -> void:
|
||||
_pending = {}
|
||||
_resolved = {}
|
||||
_node_pending = {}
|
||||
_refresh_x = 0
|
||||
_refresh_y = 0
|
||||
_paid_refreshes = 0
|
||||
_refresh_stock = 0
|
||||
_pending_rap = 0
|
||||
_time_sand_queue = 0
|
||||
|
||||
|
||||
# Y 池注入(冒险开始/续玩参数面——宿主单写)
|
||||
func set_refresh_y(v: int) -> void:
|
||||
_refresh_y = maxi(0, v)
|
||||
|
||||
|
||||
func refresh_y() -> int:
|
||||
return _refresh_y
|
||||
|
||||
|
||||
# 刷新存量账加仓(refresh_stock_grant 装配即得一次性加仓——只入存量账、临时账不动=V8 契约)
|
||||
func add_refresh_stock(n: int) -> void:
|
||||
_refresh_stock += n
|
||||
|
||||
|
||||
# 续玩选择停点回填(token_migration 类:旧版断点候选为 kind 字符串,按普通档令牌防御归一;
|
||||
# 发射回宿主信号面由调用方承接——本件只载态)
|
||||
func restore_pending(bp_pending: Dictionary) -> void:
|
||||
var migrated: Array[Variant] = []
|
||||
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})
|
||||
_pending = {"candidates": migrated}
|
||||
var payload: Dictionary[Variant, Variant] = (bp_pending.get("payload", {}) as Dictionary).duplicate(true)
|
||||
curio_choice_presented.emit(payload)
|
||||
|
||||
|
||||
# ---- 凭证出箱(战首取走清零——LevelRunner.set_voucher_grants 注入面同语义)----
|
||||
func voucher_outbox() -> Dictionary:
|
||||
var out := {"rap": _pending_rap, "time_sand": _time_sand_queue}
|
||||
_pending_rap = 0
|
||||
_time_sand_queue = 0
|
||||
return out
|
||||
|
||||
|
||||
# 已兑付态清除(宿主 continue_to_next_level 推进受理时调用——重兑付防御复位面)
|
||||
func clear_resolved() -> void:
|
||||
_resolved = {}
|
||||
|
||||
|
||||
# 兑付态直读(宿主 resume 分支载入已兑付载荷用——副本)
|
||||
func resolved_payload() -> Dictionary:
|
||||
return _resolved.duplicate(true)
|
||||
|
||||
|
||||
# 已兑付态注入(resume 分支——宿主置关序位后由本件承载载荷)
|
||||
func restore_resolved(payload: Dictionary) -> void:
|
||||
_resolved = payload.duplicate(true)
|
||||
|
||||
|
||||
# ---- 断点自交片段(应用层拼盘口收件)----
|
||||
# 片段键:bonus_pending/bonus_result/feat_ticket_pity/feat_ticket_sold/refresh_stock/attune_refresh_y
|
||||
#(feat_ticket_* 现役为宿主字段——随停点件迁移同片段承载;读侧 params 回退经参传入)
|
||||
|
||||
func breakpoint_fragment() -> Dictionary[Variant, Variant]:
|
||||
return {
|
||||
"bonus_pending": _pending.duplicate(true),
|
||||
"bonus_result": _resolved.duplicate(true),
|
||||
"feat_ticket_pity": _deck.ticket_pity,
|
||||
"feat_ticket_sold": _deck.ticket_sold,
|
||||
"refresh_stock": _refresh_stock,
|
||||
"attune_refresh_y": _refresh_y,
|
||||
}
|
||||
|
||||
|
||||
# 片段回填(凭证计数/刷新存量/Y 池;停点在途态与已兑付态归宿主主分支
|
||||
# restore_pending/restore_resolved 承接——token_migration 与信号发射时序在彼处)
|
||||
func restore_fragment(frag: Dictionary, params: Dictionary) -> void:
|
||||
_deck.ticket_pity = int(frag.get("feat_ticket_pity", 0)) # T5 扩列——旧档缺键缺省 0(存档兼容)
|
||||
_deck.ticket_sold = int(frag.get("feat_ticket_sold", 0))
|
||||
_refresh_stock = maxi(0, int(frag.get("refresh_stock", 0))) # 旧档缺键缺省 0(clamp 类)
|
||||
# Y 余量优先读断点(花掉不回血=堵「用完退菜单再续玩满血复活」可利用漏洞);缺键=旧版断点,
|
||||
# 回退参数面整额注入(旧档行为零变化)——params_fallback 类
|
||||
_refresh_y = maxi(0, int(frag.get("attune_refresh_y", int(params.get("attune_refresh_y", 0)))))
|
||||
@@ -0,0 +1 @@
|
||||
uid://bi31ynewc51ve
|
||||
@@ -1,6 +1,6 @@
|
||||
## EffectContext——效果引擎四桶计数器引用束(无状态引擎的外部状态承载)。
|
||||
## 四桶按生命周期分:round(轮始整桶清空)/battle(随编排器生灭)/level(随 LevelRunner 生灭)/
|
||||
## adventure(随 RunLedger 生灭)。本类零方法零行为,只持引用;桶容器由宿主对象 new,本类只挂引用。
|
||||
## adventure(随冒险应用层生灭——拆件批桶归宿主字典)。本类零方法零行为,只持引用;桶容器由宿主对象 new,本类只挂引用。
|
||||
class_name EffectContext
|
||||
extends RefCounted
|
||||
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
## 主角实体账:冒险生命周期主角数值与获得记录唯一所有者(冒险域拆件批自 AdventureRunner 迁入)。
|
||||
## 生命与 GP 钱包(gain/spend/clear 单写原语);锻造强化快照(永久强化 flat)与调试浮窗百分比通道;
|
||||
## 攻击/甲类型轴快照;特性持有集(活引用注入——关内兑付当关生效);本局钥石增量与逐杀明细;
|
||||
## 真视使用记录;统一冒险加算区(商店购买/三选一属性件/战末导出三通道唯一落点)。
|
||||
## 获得记录三层结构=获得记录列表(record_gain 追加、返回记录 id)→ 属性增益明细(带来源指回)→
|
||||
## 四轴汇总(由明细同步维护;summary 与断点读汇总,日记为正主)。
|
||||
## 生效上限/攻击/护甲公式与回血封顶公式单点归本件(与战斗内上限同式同源,pct=0 时乘法恒等——MC 锚零漂移)。
|
||||
## Core 层零 UI;随机零消费;断点自交片段(账本键+获得日记键)——拼盘落盘归冒险应用层。
|
||||
class_name HeroLedger
|
||||
extends RefCounted
|
||||
|
||||
# 加算区四轴(键序即断点 bonus 键序——与现役 run_ledger.bonus 置账形态逐位同形)
|
||||
const BONUS_AXES: Array[String] = ["atk", "max_hp", "armor_class", "ap"]
|
||||
|
||||
var db: ConfigDb
|
||||
var hero_hp: int = 0 # 生命账(原冒险账本 ledger["hero_hp"] 单值化)
|
||||
var gp: int = 0 # GP 钱包(原 ledger["gp"]——LevelRunner 与 ShopSession 读写全改指此单点)
|
||||
var feats: Array[String] = [] # 特性持有集(活引用注入——关内兑付 relic 须当关生效;收束清零)
|
||||
var attack_type := "" # 攻击类型轴快照(空串=未注入→锻造配置默认值回退)
|
||||
var armor_type := "" # 甲类型轴快照(同上)
|
||||
var _base_override: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0}
|
||||
var _override: Array[int] = [0, 0] # 调试浮窗百分比通道 [atk_pct, hp_pct](每关新 runner 应用面)
|
||||
var _gain_log: Array[Variant] = [] # 获得记录列表(三选一件/商店购买/战末奖励各一条)
|
||||
var _gain_seq := 0 # 获得记录 id 发号(1 起)
|
||||
var _bonus_detail: Array[Variant] = [] # 属性增益明细(带来源指回 gain_id)
|
||||
var _bonus_total: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
var _keystone := 0 # 钥石入账累计(摘要第三件事的账面)
|
||||
var _keystone_drops: Array[Variant] = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops)
|
||||
var _vision_used := 0 # 真视使用记录(本局计数——冒险收束聚合入档)
|
||||
var _purchased: Dictionary[Variant, Variant] = {} # product_id → 已购档数(递增定价输入;冒险开始清零)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, base_override_ref: Dictionary) -> void:
|
||||
db = db_ref
|
||||
_base_override = _normalize_override(base_override_ref)
|
||||
|
||||
|
||||
# 永久强化 flat 重注(单关测试模式 start_level 收 base_override_in 时调用——实例不重建,值重注)
|
||||
func rebase_override(raw: Dictionary) -> void:
|
||||
_base_override = _normalize_override(raw)
|
||||
|
||||
|
||||
# 调试浮窗百分比通道(测试工具覆盖——每关新 runner 都要应用;正式侧归元进度)
|
||||
func set_debug_override(atk_pct: int, hp_pct: int) -> void:
|
||||
_override = [atk_pct, hp_pct]
|
||||
|
||||
|
||||
func debug_atk_pct() -> int:
|
||||
return int(_override[0])
|
||||
|
||||
|
||||
func debug_hp_pct() -> int:
|
||||
return int(_override[1])
|
||||
|
||||
|
||||
# 永久强化 flat 快照读面(续玩生命钳制保守上限用——不含加算区)
|
||||
func base_override_flat() -> Dictionary[Variant, Variant]:
|
||||
return _base_override.duplicate(true)
|
||||
|
||||
|
||||
# 注入参数规范化:三字段缺键/-1 = 表值兜底(→ 0 flat);负值一律 clamp 0(白板 = 全零合法态)
|
||||
func _normalize_override(raw: Dictionary) -> Dictionary[Variant, Variant]:
|
||||
var out: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0}
|
||||
for k in out.keys():
|
||||
out[k] = maxi(0, int(raw.get(k, 0)))
|
||||
return out
|
||||
|
||||
|
||||
# ---- 生效公式(单点——叠加次序钉死:表值 → 永久强化 flat → 加算区 flat 先加,
|
||||
# 调试浮窗百分比后乘;pct=0 时乘法恒等——MC 锚零漂移)----
|
||||
|
||||
func effective_max_hp() -> int:
|
||||
var base: int = int(db.hero.max_hp) + int(_base_override.get("max_hp", 0)) + bonus_total("max_hp")
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(_override[1])) / 100.0)))
|
||||
|
||||
|
||||
func effective_atk() -> int:
|
||||
var base: int = int(db.hero.atk) + int(_base_override.get("atk", 0)) + bonus_total("atk")
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(_override[0])) / 100.0)))
|
||||
|
||||
|
||||
# 护甲等级生效值(入战 AC 三层可加:甲型基础 AC + 甲路线等级加值 + 加算区 armor_class 轴;
|
||||
# 无调试浮窗百分比通道,纯 flat)
|
||||
func effective_ac() -> int:
|
||||
return maxi(0, _armor_base_ac() + int(_base_override.get("armor_class", 0)) + bonus_total("armor_class"))
|
||||
|
||||
|
||||
# 甲型基础 AC(三层之第一层——选中甲路线行 base_ac;未注入/缺行=锻造配置默认值路线,
|
||||
# 与 hero 表两轴删除前 armor_class=10 行为连续)
|
||||
func _armor_base_ac() -> int:
|
||||
var at: String = armor_type if armor_type != "" else db.forge_default_armor_type()
|
||||
var row: Dictionary[Variant, Variant] = db.forge_armor_route_by_type(at)
|
||||
if row.is_empty():
|
||||
return int(db.hero.armor_class) # 表缺失防御兜底(现役基准)
|
||||
return int(row.get("base_ac", 0))
|
||||
|
||||
|
||||
# 回血公式单点(恢复点节点兑付/账本置满封顶同式;算术宿主——原 LevelRunner.rest_heal 迁入)
|
||||
static func rest_heal(pct: int, hp: int, max_hp: int) -> int:
|
||||
return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp)
|
||||
|
||||
|
||||
# ---- GP 钱包单写原语 ----
|
||||
|
||||
func gain_gp(amount: int) -> void:
|
||||
gp = maxi(0, gp + amount)
|
||||
|
||||
|
||||
# 余额不足 false 零副作用(出账前置校验由调用方守卫时本方法不重复拒绝)
|
||||
func spend_gp(amount: int) -> bool:
|
||||
if amount <= 0 or gp < amount:
|
||||
return false
|
||||
gp = gp - amount
|
||||
return true
|
||||
|
||||
|
||||
func clear_wallet() -> void:
|
||||
gp = 0
|
||||
|
||||
|
||||
# 单关模式重开重置(值重置非实例重建——shop 绑 hero 对象引用不换代;语义=原单关 start_level 重置面)
|
||||
func reset_for_single_level() -> void:
|
||||
_base_override = {"atk": 0, "max_hp": 0, "armor_class": 0}
|
||||
reset_adventure_accounts()
|
||||
|
||||
|
||||
# 冒险账面重置(冒险开始/单关重开共用值面——生命置满基础上限、钱包与账目清零)
|
||||
func reset_adventure_accounts() -> void:
|
||||
gp = 0
|
||||
_bonus_total = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
_gain_log = []
|
||||
_gain_seq = 0
|
||||
_bonus_detail = []
|
||||
_purchased = {}
|
||||
_keystone = 0
|
||||
_keystone_drops = []
|
||||
_vision_used = 0
|
||||
feats = []
|
||||
hero_hp = effective_max_hp()
|
||||
|
||||
|
||||
# ---- 获得记录三层 ----
|
||||
|
||||
# 获得记录列表追加(三通道各一条——source_class 词表:curio_pick/shop_product/battle_export;
|
||||
# 返回记录 id 供明细来源指回)
|
||||
func record_gain(source_class: String, kind: String, detail: Dictionary) -> int:
|
||||
_gain_seq += 1
|
||||
var rec: Dictionary[Variant, Variant] = {"id": _gain_seq, "source_class": source_class, "kind": kind}
|
||||
rec.merge(detail.duplicate(true))
|
||||
_gain_log.append(rec)
|
||||
return _gain_seq
|
||||
|
||||
|
||||
# 统一冒险加算区单写原语(三通道统一入口:三选一属性件 source_class="curio_pick";
|
||||
# 商店属性商品 source_class="shop_product"、kind=product_id;战末导出 source_class="battle_export"、
|
||||
# kind=轴名。明细+汇总同步维护——写时同拍,无事后对账)
|
||||
func gain_bonus(axis: String, value: int, source_class: String, kind: String) -> void:
|
||||
var gain_id := record_gain(source_class, kind, {"axis": axis, "value": value})
|
||||
_bonus_detail.append({"gain_id": gain_id, "axis": axis, "value": value,
|
||||
"source_class": source_class, "kind": kind})
|
||||
_bonus_total[axis] = int(_bonus_total.get(axis, 0)) + value
|
||||
|
||||
|
||||
# 汇总读面(生效公式/断点/摘要唯一取数口)
|
||||
func bonus_total(axis: String) -> int:
|
||||
return int(_bonus_total.get(axis, 0))
|
||||
|
||||
|
||||
func bonus_snapshot() -> Dictionary[Variant, Variant]:
|
||||
return _bonus_total.duplicate(true)
|
||||
|
||||
|
||||
# 商店已购档数记账(购买条目——递增定价输入;定价读数走 purchase_tier)
|
||||
func record_purchase(product_id: String) -> void:
|
||||
_purchased[product_id] = purchase_tier(product_id) + 1
|
||||
|
||||
|
||||
func purchase_tier(product_id: String) -> int:
|
||||
return int(_purchased.get(product_id, 0))
|
||||
|
||||
|
||||
func purchased_snapshot() -> Dictionary[Variant, Variant]:
|
||||
return _purchased.duplicate(true)
|
||||
|
||||
|
||||
# 本局钥石增量(Boss 逐杀掉落渠道——首杀/复杀金额由调用方按存档标记判读后入行)
|
||||
func record_keystone_drop(drop: Dictionary) -> void:
|
||||
_keystone_drops.append(drop.duplicate(true))
|
||||
_keystone += int(drop.get("amount", 0))
|
||||
|
||||
|
||||
func keystone_gained() -> int:
|
||||
return _keystone
|
||||
|
||||
|
||||
func keystone_drop_log() -> Array:
|
||||
return _keystone_drops.duplicate(true)
|
||||
|
||||
|
||||
# 真视使用记录(真视类工具受理成功恰计一次;冒险收束聚合入档)
|
||||
func record_vision_use() -> void:
|
||||
_vision_used += 1
|
||||
|
||||
|
||||
func vision_used() -> int:
|
||||
return _vision_used
|
||||
|
||||
|
||||
# 特性持有集读面(效果接线/求值/摘要唯一取数口——副本防外部改写)
|
||||
func feats_held() -> Array:
|
||||
return feats.duplicate()
|
||||
|
||||
|
||||
# ---- 断点自交片段(应用层拼盘口收件——本件只交自己的账)----
|
||||
# 片段键:hero_hp/bonus(汇总快照)/gp/shop_purchased/keystone/keystone_drops/relics/vision_counter_used
|
||||
# +获得日记键(唯一声明的断点扩列;旧档缺日记→汇总直载跳过比对;日记在档→由日记推导汇总比对)
|
||||
|
||||
func breakpoint_fragment() -> Dictionary[Variant, Variant]:
|
||||
return {
|
||||
"hero_hp": hero_hp,
|
||||
"bonus": bonus_snapshot(),
|
||||
"gp": gp,
|
||||
"shop_purchased": purchased_snapshot(),
|
||||
"keystone": _keystone,
|
||||
"keystone_drops": keystone_drop_log(),
|
||||
"relics": feats.duplicate(),
|
||||
"vision_counter_used": _vision_used,
|
||||
"gain_journal": {"records": _gain_log.duplicate(true), "details": _bonus_detail.duplicate(true)},
|
||||
}
|
||||
|
||||
|
||||
# 片段回填(日记缺键 = 旧档——汇总直载跳过比对;日记在档 = 由明细推导汇总、与留存汇总不符
|
||||
# 即报错中止——日记为正主,汇总仅作一致性校验读数)
|
||||
func restore_fragment(frag: Dictionary, _params: Dictionary) -> void:
|
||||
hero_hp = int(frag.get("hero_hp", 0))
|
||||
gp = int(frag.get("gp", 0))
|
||||
_purchased = (frag.get("shop_purchased", {}) as Dictionary).duplicate(true)
|
||||
_keystone = int(frag.get("keystone", 0))
|
||||
_keystone_drops = (frag.get("keystone_drops", []) as Array).duplicate(true)
|
||||
feats = []
|
||||
for r in (frag.get("relics", []) as Array):
|
||||
feats.append(String(r))
|
||||
_vision_used = maxi(0, int(frag.get("vision_counter_used", 0))) # clamp 类(现役读侧策略同款)
|
||||
var journal: Variant = frag.get("gain_journal", null)
|
||||
if journal is Dictionary and not (journal as Dictionary).is_empty():
|
||||
_restore_from_journal(journal as Dictionary, frag)
|
||||
return
|
||||
# 旧档回退:汇总键直载(clamp 类——四轴子键夹 ≥0)
|
||||
_bonus_total = {}
|
||||
var bp_bonus: Dictionary[Variant, Variant] = frag.get("bonus", {}) as Dictionary
|
||||
for axis in BONUS_AXES:
|
||||
_bonus_total[axis] = maxi(0, int(bp_bonus.get(axis, 0)))
|
||||
|
||||
|
||||
# 日记回填(记录列表与明细全量还原;汇总由明细推导——与留存汇总逐轴比对,不符即致命中止)
|
||||
func _restore_from_journal(journal: Dictionary, frag: Dictionary) -> void:
|
||||
_gain_log = (journal.get("records", []) as Array).duplicate(true)
|
||||
_gain_seq = _gain_log.size()
|
||||
for rec in _gain_log:
|
||||
_gain_seq = maxi(_gain_seq, int((rec as Dictionary).get("id", 0)))
|
||||
_bonus_detail = (journal.get("details", []) as Array).duplicate(true)
|
||||
var derived: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
for d in _bonus_detail:
|
||||
var axis := String((d as Dictionary).get("axis", ""))
|
||||
if not derived.has(axis):
|
||||
push_error("[主角账] 断点日记明细含未知加算轴: %s(拒绝载入)" % axis)
|
||||
derived.clear()
|
||||
break
|
||||
derived[axis] = int(derived[axis]) + int((d as Dictionary).get("value", 0))
|
||||
if derived.is_empty():
|
||||
return # 已报错中止(fail-fast——明细非法整面拒绝)
|
||||
var stored: Dictionary[Variant, Variant] = frag.get("bonus", {}) as Dictionary
|
||||
for axis in derived.keys():
|
||||
if int(stored.get(axis, -1)) != int(derived[axis]):
|
||||
push_error("[主角账] 断点汇总与日记推导不一致(axis=%s 汇总=%d 推导=%d)——拒绝载入"
|
||||
% [axis, int(stored.get(axis, -1)), int(derived[axis])])
|
||||
return
|
||||
_bonus_total = derived
|
||||
@@ -0,0 +1 @@
|
||||
uid://cweny8ulalase
|
||||
+45
-66
@@ -2,7 +2,7 @@
|
||||
## 关卡域生命周期编排者:节点图行进、HP 跨战斗携带、恢复点兑付、关卡末收束(垂直切片验证:一关内数值提升可感知);商店族归 ShopSession 件。
|
||||
## Core 层零 UI 依赖(headless 可跑完整关卡);零自动推进——兑付确认后经 choices_presented,推进只走 pick_next_node。
|
||||
## 种子域:单关模式 start_level 自播种;冒险模式 start_level_external 不重播种(主种子恰一次归 AdventureRunner)。
|
||||
## hero_atk_pct/hero_hp_pct 为测试专用覆盖——经 encounter 显式传入战斗,零表写,db.hero 运行期只读。
|
||||
## 调试浮窗百分比通道单点归主角账(set_hero_override 转调——公式消费面同源)。
|
||||
## 信号顺序契约:呈现层须在 level_started 回调重取 runner 实例,choices_presented 才不会被空防御丢弃。
|
||||
class_name LevelRunner
|
||||
extends RefCounted
|
||||
@@ -22,18 +22,20 @@ var node_index := -1
|
||||
# ---- 账本载体(冒险结构实现批——漂移②清偿)----
|
||||
# 冒险模式 = AdventureRunner 共享字典(Dictionary 引用共享——hero_hp/gp 属性读写直接落冒险账本);
|
||||
# 单关模式 = 本实例私有账本(start_level 重建,既有单关语义逐位保留)。
|
||||
var _ledger: Dictionary[Variant, Variant] = {"hero_hp": 0, "gp": 0}
|
||||
var level_counters: Dictionary[Variant, Variant] = {} # 关卡桶(lvl_ 前缀计数器;每关随 LevelRunner new 天然空=按关清零,替代旧硬编码清除名单)
|
||||
var _rest_handler: Callable # 治疗封顶回报执行体(默认内置 rest_heal_amount;冒险模式 = AdventureRunner.apply_rest——接口③)
|
||||
var _external := false # 冒险模式标记(钱包收束时机与播种语义分叉点)
|
||||
# hero_hp / gp 属性化(既有读写点零改动——值类型经属性落 _ledger)
|
||||
# 主角账引用(拆件批 T1 过渡面:冒险模式经 set_hero 注入共享实体账、单关模式 _init 自建私有账;
|
||||
# 构造注入收口归 T3 ctx 面——后置 setter 随构造注入一并退役)
|
||||
var _hero: HeroLedger = HeroLedger.new(null, {}) # 直构缺省私有账(db 随 _init 补)
|
||||
var _adv_bucket: Dictionary[Variant, Variant] = {} # 冒险桶(效果引擎 adv_ 计数器宿主字典——冒险模式随 hero 注入;单关=本实例自有)
|
||||
# hero_hp / gp 属性化(既有读写点零改动——值类型经属性落主角账)
|
||||
var hero_hp: int:
|
||||
get: return int(_ledger["hero_hp"])
|
||||
set(v): _ledger["hero_hp"] = v
|
||||
get: return _hero.hero_hp
|
||||
set(v): _hero.hero_hp = v
|
||||
var gp: int: # GP 冒险钱包(局内货币——冒险开始 = 0、收束清零;不入存档不入摘要)
|
||||
get: return int(_ledger["gp"])
|
||||
set(v): _ledger["gp"] = v
|
||||
var base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 注入(元进度实现批——冒险模式 = AdventureRunner 共享字典经 start_level_external 注入;单关测试模式 = 私有全零)
|
||||
get: return _hero.gp
|
||||
set(v): _hero.gp = v
|
||||
var _shop: ShopSession = null # 商店件(_init 建;双模式货架/定价/购买/刷新唯一宿主)
|
||||
# 协作对象私有(E1/E3 封装收口):settlement 读取经 current_battle() 成文出口
|
||||
var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null)
|
||||
@@ -61,12 +63,7 @@ var _shop_visit_buys: int:
|
||||
get: return _shop.visit_buys
|
||||
var _shop_paid_refreshes: int:
|
||||
get: return _shop.paid_refreshes
|
||||
var _shop_purchased: Dictionary[Variant, Variant]: # product_id → 已购档数(现价重算输入;冒险开始清零)
|
||||
get: return _shop.purchased
|
||||
set(v): _shop.purchased = v
|
||||
|
||||
var hero_atk_pct := 0 # 测试面板覆盖值(切片专用)
|
||||
var hero_hp_pct := 0
|
||||
# ---- 关卡相位轴(显式 Phase enum:内部 enum + 守卫换算,对齐 行动经济 Phase 先例;
|
||||
# level_result:String 与 awaiting_choice() 是 AdventureRunner/呈现层消费的外部契约面)----
|
||||
enum Phase { IDLE, RUNNING, AWAITING_CHOICE, ENDED }
|
||||
@@ -80,6 +77,7 @@ var _generator # 参数化长图生成器(惰性建——NodeMapGenerator
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
db = db_ref
|
||||
rng = rng_ref
|
||||
_hero.db = db # 直构私有账补表引用(构造参形态 T3 收口)
|
||||
_shop = ShopSession.new(db, rng)
|
||||
_bind_shop()
|
||||
|
||||
@@ -92,49 +90,31 @@ func _is_running() -> bool:
|
||||
# ---- 测试面板覆盖(模拟天赋树属性提升——仅测试用,正式侧归 元进度;经 encounter 传入战斗)----
|
||||
|
||||
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
||||
hero_atk_pct = atk_pct
|
||||
hero_hp_pct = hp_pct
|
||||
_hero.set_debug_override(atk_pct, hp_pct)
|
||||
|
||||
|
||||
# 叠加次序(钉死):表值 → 永久强化 flat(base_override)→ 加算区 flat(bonus)先加
|
||||
# → 调试浮窗百分比后乘(调试数值工具恒乘最后)
|
||||
# 生效公式单点归主角账(拆件批 T1——本类删公式本体,读面转调;叠加次序注释随公式迁主角账)
|
||||
func effective_atk() -> int:
|
||||
var base: int = int(db.hero.atk) + int(base_override.get("atk", 0)) + int(run_ledger.bonus.get("atk", 0))
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(hero_atk_pct)) / 100.0)))
|
||||
return _hero.effective_atk()
|
||||
|
||||
|
||||
func effective_max_hp() -> int:
|
||||
var base: int = int(db.hero.max_hp) + int(base_override.get("max_hp", 0)) + int(run_ledger.bonus.get("max_hp", 0))
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(hero_hp_pct)) / 100.0)))
|
||||
return _hero.effective_max_hp()
|
||||
|
||||
|
||||
# 护甲等级生效值(入战 AC 三层可加:甲型基础 AC〔forge.tres 选中甲路线 base_ac〕+
|
||||
# 甲路线等级加值〔base_override.armor_class——hero_override 锻造分量〕+ 加算区 armor_class 轴;
|
||||
# 无调试浮窗百分比通道,纯 flat)
|
||||
func effective_ac() -> int:
|
||||
return maxi(0, _armor_base_ac() + int(base_override.get("armor_class", 0)) + int(run_ledger.bonus.get("armor_class", 0)))
|
||||
|
||||
|
||||
# 甲型基础 AC(三层之第一层——选中甲路线行 base_ac;未注入/缺行=锻造配置默认值路线=轻甲 10,
|
||||
# 与 hero.tres 两轴删除前 armor_class=10 行为连续)
|
||||
func _armor_base_ac() -> int:
|
||||
var at: String = _hero_armor_type if _hero_armor_type != "" else db.forge_default_armor_type()
|
||||
var row: Dictionary[Variant, Variant] = db.forge_armor_route_by_type(at)
|
||||
if row.is_empty():
|
||||
return int(db.hero.armor_class) # 表缺失防御兜底(现役基准)
|
||||
return int(row.get("base_ac", 0))
|
||||
return _hero.effective_ac()
|
||||
|
||||
|
||||
# 行动点加成生效值(统一冒险加算区第四轴 ap;经战斗开始入参 hero_ap_bonus
|
||||
# 传入战斗域,兑现 = 每场战斗开始首轮行动点基线 +加成值〔行动经济 GDD 跨冒险行动点加成轴节〕)
|
||||
func effective_ap_bonus() -> int:
|
||||
return maxi(0, int(run_ledger.bonus.get("ap", 0)))
|
||||
return maxi(0, _hero.bonus_total("ap"))
|
||||
|
||||
|
||||
# 覆盖复位(关卡结束归零——表值不可变,仅清百分比;调用方兼容保留)
|
||||
func restore_hero_base() -> void:
|
||||
hero_atk_pct = 0
|
||||
hero_hp_pct = 0
|
||||
_hero.set_debug_override(0, 0)
|
||||
|
||||
|
||||
# ---- 关卡生命周期 ----
|
||||
@@ -148,7 +128,6 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
|
||||
# 注入面只服务数值验证工具,非玩家可达路径)
|
||||
_external = false
|
||||
_rest_handler = rest_heal_amount
|
||||
_ledger = {"hero_hp": 0, "gp": 0}
|
||||
var row := _find_level_row(level_index)
|
||||
if row.is_empty():
|
||||
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
|
||||
@@ -158,14 +137,8 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
|
||||
_phase = Phase.IDLE
|
||||
_candidates = []
|
||||
visited = []
|
||||
gp = 0 # 冒险钱包清零开局(局内货币生命周期起点)
|
||||
run_ledger.bonus = {"atk": 0, "max_hp": 0, "armor_class": 0} # 加算区冒险开始全 0(单关入口零元进度——注入面见上)
|
||||
base_override = {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat:缺省全零白板(单关入口零元进度——注入面见上)
|
||||
_shop_purchased = {} # 已购档数清零(跨冒险不延续——递增定价按冒险内累计)
|
||||
_bind_shop() # 钱包引用换代重绑(_ledger 已重建)
|
||||
for k in base_override.keys():
|
||||
base_override[k] = maxi(0, int(base_override_in.get(k, 0))) # 注入规范化(契约同冒险开始参数面)
|
||||
hero_hp = effective_max_hp()
|
||||
_hero.rebase_override(base_override_in) # 永久强化 flat 重注(规范化归主角账——契约同冒险开始参数面)
|
||||
_hero.reset_for_single_level() # 账面值重置(钱包/加算区/已购档/钥石/日记清零+生命置满——实例不重建,shop 绑定不换代)
|
||||
rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构)
|
||||
if not _assign_generated_graph(row, seed_val):
|
||||
_phase = Phase.ENDED
|
||||
@@ -179,30 +152,24 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
|
||||
return _enter_node(entries[0])
|
||||
|
||||
|
||||
# 冒险结构接口①:开始关卡(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接;
|
||||
# 种子域归冒险结构:本路径不重播种;账本归冒险结构:本路径不重置 gp/bonus/_shop_purchased,
|
||||
# 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在 ledger 写好)。
|
||||
# base_override = 永久强化 flat 共享引用(缺省 {} 全零白板,与 B1 空参数路径同值)。
|
||||
# 冒险结构接口①:开始关卡(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经主角账共享交接;
|
||||
# 种子域归冒险结构:本路径不重播种;账本归冒险结构:本路径不重置 gp/加算区/已购档,
|
||||
# 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在主角账写好)。
|
||||
# 流程与 start_level 同款(_find_level_row → 生成图装配 → 状态初始化 → _enter_node(入口)),
|
||||
# 差异仅在账本/加算区/已购档/恢复执行体的注入与零重置。
|
||||
# 差异仅在恢复执行体的注入与零重置(主角账/冒险桶经 set_hero 注入——T1 过渡面,T3 收构造)。
|
||||
# defer_entry 尾参(缺省 false = 直接进节点)——true 时不自动
|
||||
# _enter_node(entry),改置选路态(awaiting_choice + 单候选 = 入口节点),由 AdventureRunner 在
|
||||
# level_started 之后调 present_entry_choice() 呈现、玩家 pick_next_node 显式进首战。拆两步的
|
||||
# 信号顺序契约:呈现层须先经 level_started 重取 runner 实例,choices_presented 才不会被
|
||||
# _on_choices_presented 的 runner 空防御静默丢弃。入口呈现零 RNG 消耗、pick 走同一个
|
||||
# _enter_node(entry)——测量面零漂移红线。
|
||||
func start_level_external(level_index: int, ledger: Dictionary,
|
||||
purchased_ref: Dictionary, rest_handler: Callable, base_override_ref: Dictionary = {},
|
||||
func start_level_external(level_index: int, rest_handler: Callable,
|
||||
defer_entry: bool = false, resume_state: Dictionary = {}) -> bool:
|
||||
var row := _find_level_row(level_index)
|
||||
if row.is_empty():
|
||||
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
|
||||
return false
|
||||
_external = true
|
||||
_ledger = ledger
|
||||
base_override = base_override_ref
|
||||
_shop_purchased = purchased_ref
|
||||
_bind_shop() # 钱包共享引用换代重绑(_ledger = 冒险账本)
|
||||
_rest_handler = rest_handler
|
||||
if not _assign_generated_graph(row, rng.master_seed):
|
||||
return false
|
||||
@@ -516,7 +483,9 @@ func _start_battle(node: Dictionary) -> bool:
|
||||
"hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(),
|
||||
"attack_type": _hero_attack_type, "armor_type": _hero_armor_type, # 两根类型轴(spawn 英雄快照;空串=锻造配置默认值回退)
|
||||
"attunement": _attunement,
|
||||
"ledger": run_ledger, # 共享四账单类下传(单引用键)
|
||||
"ledger": run_ledger, # 充能账瘦壳下传(单引用键——bonus/真视/桶已迁主角账与冒险桶)
|
||||
"hero": _hero, # 主角实体账下传(战末 bonus 汇入/真视计数单写原语消费面)
|
||||
"adv_bucket": _adv_bucket, # 冒险桶下传(效果引擎 adv_ 计数器宿主字典——跨战持久同一引用)
|
||||
"level_counters": level_counters, # 关卡桶下传(同关跨战共享引用、随本类 new 天然空)
|
||||
"feats": _feats,
|
||||
"tree_rules": _tree_rules,
|
||||
@@ -677,15 +646,26 @@ func set_hero_axes(attack_type: String, armor_type: String) -> void:
|
||||
_hero_armor_type = armor_type
|
||||
|
||||
|
||||
# 共享四本账注入(RunLedger 单类下传,含 bonus/attune_charges/
|
||||
# vision_counter/adventure_counters 四账;战斗侧只走账类原语,战末 export/开局 seed
|
||||
# 时点语义不变)
|
||||
# 共享充能账瘦壳注入(RunLedger 瘦身后只承载 attune_charges;战斗侧只走账类原语)
|
||||
var run_ledger: RunLedger = RunLedger.new()
|
||||
|
||||
|
||||
# 主角账只读出口(加算区生产读面承接—— AdventureRunner.hero() 同款组件交出)
|
||||
func hero() -> HeroLedger:
|
||||
return _hero
|
||||
|
||||
|
||||
# 主角实体账注入(冒险模式共享 AdventureRunner 建件——生命/钱包/加算区/类型轴/特性集单点;
|
||||
# 随行注入冒险桶引用。T1 过渡 setter——T3 构造注入收口后本组 setter 退役)
|
||||
func set_hero(h: HeroLedger, adv_bucket: Dictionary) -> void:
|
||||
_hero = h
|
||||
_adv_bucket = adv_bucket
|
||||
_bind_shop() # 账面换代重绑(钱包/购买加算落点面)
|
||||
|
||||
|
||||
func set_run_ledger(l: RunLedger) -> void:
|
||||
run_ledger = l
|
||||
_bind_shop() # 账类换代重绑(加算区 flat 落点面)
|
||||
_bind_shop() # 账类换代重绑(充能账读数面)
|
||||
|
||||
|
||||
# 凭证件三账注入(储备金袋 RAP/时沙 首轮 AP/完美保险 标错豁免;节点级一次性,
|
||||
@@ -753,11 +733,10 @@ func close_wallet() -> void:
|
||||
# 实装归 ShopSession 件(双模式货架/定价/购买/刷新唯一宿主);本类保留同签名委托与件注入/
|
||||
# 白盒入口——外部调用面与断点取数面零变化。
|
||||
|
||||
# 件注入(建件与宿主引用换代时调用——钱包/账类/图行/装配集为共享引用,出账直接落同一引用)
|
||||
# 件注入(建件与宿主引用换代时调用——钱包/主角账/图行/装配集为共享引用,出账直接落同一引用)
|
||||
func _bind_shop() -> void:
|
||||
_shop.bind({
|
||||
"wallet": _ledger,
|
||||
"run_ledger": run_ledger,
|
||||
"hero": _hero,
|
||||
"level_row": level_row,
|
||||
"tree_rules": _tree_rules,
|
||||
"attunement": _attunement,
|
||||
|
||||
+3
-19
@@ -1,19 +1,13 @@
|
||||
class_name RunLedger
|
||||
extends RefCounted
|
||||
# 共享四本账显式承载(审计 P1-4「共享可变字典跨三层:所有权名义单点、实际三处
|
||||
# 可写」收口 = 单类持四账)。
|
||||
# 同调充能账瘦壳(冒险域拆件批瘦身半程——bonus 账/真视账/冒险桶已迁 HeroLedger 与冒险应用层)。
|
||||
# 所有权:AdventureRunner 持有(开局构造、冒险全程唯一宿主);LevelRunner 经 set_run_ledger
|
||||
# 下传并装配 encounter;战斗侧(BattleOrchestrator)经 encounter["ledger"] 拿本类引用,
|
||||
# 只走下方 read/append 原语;adventure 桶经 EffectContext 引用被效果引擎就地直写(无战末 export/
|
||||
# 开局 seed 搬运时点——跨战持久=同一字典引用,随本类生灭)。
|
||||
# 只走下方原语。整类文件退役归姊妹副卡(充能域封装批——单源化后零引用即删)。
|
||||
var attune_charges: Dictionary[Variant, Variant] = {} # 同调主动件充能账(use_treasure 扣减;断点随局回档)
|
||||
var vision_counter: Dictionary[Variant, Variant] = {} # 真视类使用计数(冒险开始置 {"used":0};
|
||||
# 缺省空 = 调试/单场战斗不计——enc 缺键守卫语义)
|
||||
var adventure_counters: Dictionary[Variant, Variant] = {} # adventure 桶(adv_ 前缀计数器;经 EffectContext 引用被引擎就地直写、跨战持久随本类生灭)
|
||||
var bonus: Dictionary[Variant, Variant] = {} # 统一冒险加算区(冒险开始置四轴零账——商店购买/三选一属性件/战末 bonus_delta 汇入)
|
||||
|
||||
|
||||
# ---- 战斗侧原语(裁点②甲:战斗侧只拿 read/append 两族;宿主侧冒险/关卡域直读字段域) ----
|
||||
# ---- 战斗侧原语(宿主侧冒险/关卡域直读字段域) ----
|
||||
|
||||
func attune_charge_has(item_id: String) -> bool:
|
||||
return attune_charges.has(item_id)
|
||||
@@ -27,13 +21,3 @@ func attune_charge_consume(item_id: String) -> void:
|
||||
# 原编排器 use_treasure 扣减语义逐字(has 闸 + maxi(0,·) 下限)
|
||||
if attune_charges.has(item_id):
|
||||
attune_charges[item_id] = maxi(0, int(attune_charges[item_id]) - 1)
|
||||
|
||||
|
||||
func vision_count_add(n: int) -> void:
|
||||
vision_counter["used"] = int(vision_counter.get("used", 0)) + n
|
||||
|
||||
|
||||
func bonus_add_axis(axis: String, delta: int) -> void:
|
||||
# 原编排器战末汇入逐字语义(has 闸——账外轴丢弃)
|
||||
if bonus.has(axis):
|
||||
bonus[axis] = int(bonus.get(axis, 0)) + delta
|
||||
+10
-12
@@ -1,6 +1,6 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 商店会话件:关卡商店双模式(legacy 固定三件加定价/pool 共享令牌池)的货架、定价、购买与刷新唯一宿主。
|
||||
## 账本/钱包/装配集归宿主 LevelRunner(共享引用经 bind 注入,出账直接落同一引用改写宿主账本);
|
||||
## 钱包/主角账/装配集归宿主(HeroLedger 共享引用经 bind 注入——GP 读写与购买加算走主角账单点);
|
||||
## Core 层零 UI 依赖;公开面由 LevelRunner 同签名委托承载(外部调用面零变化);
|
||||
## legacy 模式零 RNG 零池消费(off 对照承载)。
|
||||
class_name ShopSession
|
||||
@@ -8,9 +8,8 @@ extends RefCounted
|
||||
|
||||
var db: ConfigDb
|
||||
var rng: RngServiceCore
|
||||
var _refs: Dictionary[Variant, Variant] = {} # wallet/run_ledger/level_row/tree_rules/attunement 宿主共享引用绑定
|
||||
var _refs: Dictionary[Variant, Variant] = {} # hero/level_row/tree_rules/attunement 宿主共享引用绑定
|
||||
var _hooks: Dictionary[Variant, Variant] = {} # wallet_emit/max_hp_gain/battle 宿主回调绑定
|
||||
var purchased: Dictionary[Variant, Variant] = {} # product_id → 已购档数(现价重算输入;冒险开始清零)
|
||||
var pool_mode := false # pool 模式封印(set_shop_pool 注入时置位)
|
||||
var pool_deck: Array[Variant] = [] # 共享池令牌库共享引用(库存=剩余令牌数单点)
|
||||
var pool_consume := Callable() # 池消费 handler(跨渠道共库兑付)
|
||||
@@ -64,7 +63,7 @@ func _spend_discounted(price: int) -> int:
|
||||
|
||||
|
||||
func purchased_tier(product_id: String) -> int:
|
||||
return int(purchased.get(product_id, 0))
|
||||
return _hero().purchase_tier(product_id)
|
||||
|
||||
|
||||
# 购买(未知商品/属性键出域/余额不足拒绝——返回 false 零副作用):
|
||||
@@ -75,7 +74,7 @@ func buy_product(product_id: String) -> bool:
|
||||
push_error("[关卡节点地图] 商店房未知商品: %s" % product_id)
|
||||
return false
|
||||
var attr := String(prow.get("attr", ""))
|
||||
if not _bonus().has(attr):
|
||||
if not HeroLedger.BONUS_AXES.has(attr):
|
||||
push_error("[关卡节点地图] 商品属性键出域: %s(attr=%s)" % [product_id, attr])
|
||||
return false
|
||||
var price := current_price(prow)
|
||||
@@ -84,11 +83,10 @@ func buy_product(product_id: String) -> bool:
|
||||
return false # 余额不足(覆盖层按钮禁用为常态防线——此处兜底拒绝+静默留痕)
|
||||
_set_gp(_gp() - price)
|
||||
var flat := int(prow.get("flat_value", 0))
|
||||
var bonus := _bonus()
|
||||
bonus[attr] = int(bonus[attr]) + flat
|
||||
_hero().gain_bonus(attr, flat, "shop_product", product_id) # 加算区单写原语(明细+汇总同拍——wr1-s 同值承载)
|
||||
if attr == "max_hp":
|
||||
(_hooks["max_hp_gain"] as Callable).call(flat) # 购血:上限与当前生命同步 +Δ(封顶新上限)
|
||||
purchased[product_id] = purchased_tier(product_id) + 1
|
||||
_hero().record_purchase(product_id)
|
||||
_emit("buy", {"product": product_id, "price": price})
|
||||
return true
|
||||
|
||||
@@ -312,15 +310,15 @@ func shop_bp_visit_buys() -> int:
|
||||
# ---- 宿主引用读取(共享引用绑定面——钱包/账类/图行/装配集)----
|
||||
|
||||
func _gp() -> int:
|
||||
return int((_refs["wallet"] as Dictionary).get("gp", 0))
|
||||
return _hero().gp
|
||||
|
||||
|
||||
func _set_gp(v: int) -> void:
|
||||
(_refs["wallet"] as Dictionary)["gp"] = v
|
||||
_hero().gp = v
|
||||
|
||||
|
||||
func _bonus() -> Dictionary:
|
||||
return (_refs["run_ledger"] as RunLedger).bonus
|
||||
func _hero() -> HeroLedger:
|
||||
return _refs["hero"] as HeroLedger
|
||||
|
||||
|
||||
func _level_index() -> int:
|
||||
|
||||
@@ -17,7 +17,7 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB
|
||||
# 共账本〔level_runner bonus{atk,max_hp,armor_class}〕;空加算=整节隐藏〔视图内空态纪律〕。
|
||||
# 呈现只读投影零核心触碰;行文格式=白名单项)
|
||||
if runner != null and level_active:
|
||||
var b: Dictionary[Variant, Variant] = runner.run_ledger.bonus # 加算区住 RunLedger
|
||||
var b: Dictionary[Variant, Variant] = runner.hero().bonus_snapshot() # 加算区住主角账
|
||||
var lines: Array[String] = []
|
||||
if int(b.get("atk", 0)) != 0:
|
||||
lines.append("攻击 %+d" % int(b.get("atk", 0)))
|
||||
@@ -310,7 +310,7 @@ static func gp_summary_text(payload: Dictionary, runner: Variant, save_manager:
|
||||
static func bonus_summary_text(runner: Variant) -> String:
|
||||
if runner == null:
|
||||
return ""
|
||||
var b: Dictionary[Variant, Variant] = runner.run_ledger.bonus # 加算区住 RunLedger
|
||||
var b: Dictionary[Variant, Variant] = runner.hero().bonus_snapshot() # 加算区住主角账
|
||||
var parts: Array[String] = []
|
||||
if int(b.get("atk", 0)) != 0:
|
||||
parts.append("攻 %+d" % int(b.get("atk", 0)))
|
||||
|
||||
@@ -29,13 +29,13 @@ static func _on_adventure_level_started(root: Variant, level_index: int, display
|
||||
# 开场行传当前生效值(runner.effective_atk/effective_max_hp 与战斗面板同源:
|
||||
# 表值+永久强化 flat+加算区 flat 后乘调试浮窗 pct,本回调时点 runner 已带全部加成);「含面板加成」
|
||||
# 后缀判断认任一加成源——永久强化 atk/max_hp flat 或调试浮窗 pct 或局内加算区 atk/max_hp
|
||||
#(run_ledger.bonus——属性值提升入账后显示值已含加算区 flat,注记随动;armor_class/ap 不入条件:
|
||||
#(hero 加算区——属性值提升入账后显示值已含加算区 flat,注记随动;armor_class/ap 不入条件:
|
||||
# 开场行只显示 atk/max_hp 两值,注记指向不存在的显示轴反成失真),白板局不带
|
||||
var ov: Dictionary[Variant, Variant] = root.meta.hero_override()
|
||||
var boosted: bool = int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 \
|
||||
or root.debug_overlay.atk_pct() > 0 or root.debug_overlay.hp_pct() > 0 \
|
||||
or int(root.run_scope.runner.run_ledger.bonus.get("atk", 0)) > 0 \
|
||||
or int(root.run_scope.runner.run_ledger.bonus.get("max_hp", 0)) > 0
|
||||
or root.run_scope.runner.hero().bonus_total("atk") > 0 \
|
||||
or root.run_scope.runner.hero().bonus_total("max_hp") > 0
|
||||
root.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed),
|
||||
root.run_scope.runner.effective_atk(), root.run_scope.runner.effective_max_hp(), boosted))
|
||||
|
||||
@@ -204,9 +204,9 @@ static func _exit_level_screen(root: Variant) -> void:
|
||||
# 选路屏状态栏装配(视图零核心态拼装——architecture §1.3:读核心态→拼展示文案集中于装配器)。
|
||||
# 打开本屏/兑付确认回执后由组合根取成品喂入 apply_status(刷新时点与原视图内自刷一致)。
|
||||
static func level_map_status_payload(runner: LevelRunner) -> Dictionary:
|
||||
var atk_bonus := int(runner.run_ledger.bonus.get("atk", 0)) # 加算区住 RunLedger
|
||||
var ac_bonus := int(runner.run_ledger.bonus.get("armor_class", 0))
|
||||
var hp_bonus := int(runner.run_ledger.bonus.get("max_hp", 0))
|
||||
var atk_bonus := runner.hero().bonus_total("atk") # 加算区住主角账(读面经 hero() 出口承接)
|
||||
var ac_bonus := runner.hero().bonus_total("armor_class")
|
||||
var hp_bonus := runner.hero().bonus_total("max_hp")
|
||||
var atk_txt := "%d" % runner.effective_atk()
|
||||
if atk_bonus > 0:
|
||||
atk_txt += " +%d" % atk_bonus # 加成角标(ASCII 加号——防像素字体缺 ⁺ 字形出豆腐块)
|
||||
|
||||
@@ -416,7 +416,7 @@ func _ready() -> void:
|
||||
# 未适配则走到即卡死)。两态分派:result 态(兑付反馈——点继续推进)/
|
||||
# 待选态(点首件+确认)。真实信号路径全线。〔无替换子态——装配不可变契约。〕
|
||||
# 三选一停点输入锁定断言(停点态断言面:待选/兑付两态共用此层)。
|
||||
if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure._node_curio_pending.is_empty() and String(main.curio_overlay._header).contains("关卡奖励点"):
|
||||
if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty() and String(main.curio_overlay._header).contains("关卡奖励点"):
|
||||
# 停点态判据 = _node_curio_pending(奖励节点三选一停点住此——原条件查
|
||||
# _curio_pending〔关卡完成三选一态〕与奖励头部互斥、恒不可命中=拦截能力未证病根,
|
||||
# 卡⑤ d4 转正随修;头部校验串本体零触碰)
|
||||
@@ -434,10 +434,10 @@ func _ready() -> void:
|
||||
_problems.append("三选一停点打开时输入未锁定")
|
||||
if main.curio_overlay._continue_btn.visible:
|
||||
main.curio_overlay.press_continue()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure._node_curio_pending.is_empty():
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
|
||||
main.curio_overlay.press_card(0) # 奖励节点待选态:点首件+确认(真实按钮信号路径——与关卡完成停点同形)
|
||||
main.curio_overlay.press_confirm()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure._curio_pending.is_empty():
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
|
||||
main.curio_overlay.press_card(0)
|
||||
main.curio_overlay.press_confirm()
|
||||
else:
|
||||
@@ -604,7 +604,7 @@ func _ready() -> void:
|
||||
elif main.curio_overlay.visible:
|
||||
if main.curio_overlay._continue_btn.visible:
|
||||
main.curio_overlay.press_continue()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure._curio_pending.is_empty():
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
|
||||
main.curio_overlay.press_card(0)
|
||||
main.curio_overlay.press_confirm()
|
||||
else:
|
||||
@@ -713,7 +713,7 @@ func _ready() -> void:
|
||||
while ol_r_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1:
|
||||
ol_r_wait += 1
|
||||
await get_tree().process_frame
|
||||
if main.run_scope.runner == null or int(main.run_scope.runner.run_ledger.bonus.get("atk", 0)) != 5:
|
||||
if main.run_scope.runner == null or main.run_scope.runner.hero().bonus_total("atk") != 5:
|
||||
_problems.append("开场行路径②③前提失守:续玩重建后加算区 atk!=5(断点回填链断)")
|
||||
if ol_r_wait >= 24:
|
||||
_problems.append("开场行路径②③失守:续玩重建未发开场行(选路停点形应重发——形态/驱动复核)")
|
||||
|
||||
@@ -209,7 +209,7 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
|
||||
logger.attach_battle(r.current_battle())
|
||||
if t == "boss":
|
||||
st.boss_depth = int(node.get("depth", -1))
|
||||
st.hp_at_boss = int(adr.ledger["hero_hp"])
|
||||
st.hp_at_boss = adr.hero().hero_hp
|
||||
elif t == "reward":
|
||||
# 奖励节点三选一(共享池确定性策略;TP0 起 316-f 判读:1 级最稀有、
|
||||
# 同档并列取最左;替换放弃新件;兑付后 continue_after_node 回图选路)
|
||||
@@ -259,7 +259,7 @@ static func _on_adventure_wallet(event: String, gp_val: int, extra: Dictionary,
|
||||
# 奖励事件随 reward_events 留档)
|
||||
static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> void:
|
||||
var pl := payload.duplicate(true)
|
||||
pl["bonus_max_hp"] = int(adr.run_ledger.bonus.get("max_hp", 0)) # 观测面:停点时刻加算区快照(回血精确断言用)
|
||||
pl["bonus_max_hp"] = adr.hero().bonus_total("max_hp") # 观测面:停点时刻加算区快照(回血精确断言用)
|
||||
st.reward_events.append({"curio_pick": pl})
|
||||
var candidates: Array = payload.get("candidates", []) as Array
|
||||
if candidates.is_empty():
|
||||
|
||||
@@ -62,16 +62,16 @@ func _drive_dump_run(db, seed_val: int) -> Dictionary:
|
||||
var guard := 2000
|
||||
while String(ctx["result"]) == "" and guard > 0:
|
||||
guard -= 1
|
||||
if not (adr._node_curio_pending as Dictionary).is_empty():
|
||||
if not (adr.stop()._node_pending as Dictionary).is_empty():
|
||||
_dump(ctx, "node_curio_pending")
|
||||
var ncands: Array = (adr._node_curio_pending.get("candidates", []) as Array)
|
||||
var ncands: Array = (adr.stop()._node_pending.get("candidates", []) as Array)
|
||||
var npick: Dictionary = Autoplay._pick_token(ncands, "off")
|
||||
if not adr.apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", ""))):
|
||||
ctx["result"] = "node_advance_failed"
|
||||
continue
|
||||
if not (adr._curio_pending as Dictionary).is_empty():
|
||||
if not (adr.stop()._pending as Dictionary).is_empty():
|
||||
_dump(ctx, "curio_pending")
|
||||
var cands: Array = (adr._curio_pending.get("candidates", []) as Array)
|
||||
var cands: Array = (adr.stop()._pending.get("candidates", []) as Array)
|
||||
var pick: Dictionary = Autoplay._pick_token(cands, "off")
|
||||
if not adr.pick_curio(String(pick.get("kind", "")), String(pick.get("tier", "")), String(pick.get("feat_id", ""))):
|
||||
ctx["result"] = "advance_failed"
|
||||
|
||||
@@ -77,7 +77,7 @@ func _feats_breakpoint_roundtrip() -> void:
|
||||
_db.feats = rt
|
||||
var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72))
|
||||
a_r.start_adventure(72) # 源 :723(受理断言留单元宿主)
|
||||
a_r._curio_pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||||
a_r.stop()._pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||||
a_r.pick_curio("relic", "rare", "x3") # 源 :781(兑付断言留单元宿主)
|
||||
# 断点往返(持有集+余库 feat_id 随行)
|
||||
var bp_path := "user://_smoke_feats_bp.json" # 段首清场(存档域隔离纪律——上轮残留断点先删)
|
||||
@@ -86,7 +86,7 @@ func _feats_breakpoint_roundtrip() -> void:
|
||||
var sm_r = load("res://core/save_manager.gd").new(bp_path)
|
||||
sm_r.load_from_disk()
|
||||
a_r.set_save_manager(sm_r)
|
||||
a_r._on_level_finished({"result": "victory", "hp": int(a_r.ledger.get("hero_hp", 26)), "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
a_r._on_level_finished({"result": "victory", "hp": int(a_r.hero().hero_hp), "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
var bp_r: Dictionary = sm_r.breakpoint_snapshot()
|
||||
Kit.check("断点附随件:relics 持有集入快照(迭代 12 T1 扩列)", (bp_r.get("relics", []) as Array).has("x3"))
|
||||
Kit.check("断点附随件:adventure 计数器账本键在案(T6/T7 扩列)", bp_r.has("adventure_counters"))
|
||||
|
||||
@@ -56,8 +56,9 @@ func _smoke_generated_map_determinism() -> void:
|
||||
var rng_c = _fresh_rng(910005)
|
||||
var counter_before: int = rng_c.counter()
|
||||
var runner2 = load("res://core/level_runner.gd").new(_db, rng_c)
|
||||
runner2.hero_hp = 20 # 冒险模式生命经主角账预置(旧 ledger 注入参数的等效构造面)
|
||||
Kit.check("冒险模式起关受理(入口停点)", runner2.start_level_external(3,
|
||||
{"hero_hp": 20, "gp": 0}, {}, Callable(self, "_smoke_rest_stub"), {}, true))
|
||||
Callable(self, "_smoke_rest_stub"), true))
|
||||
Kit.check("起关产图零计数器消费(同图不依赖取数号位)", rng_c.counter() == counter_before)
|
||||
var params3: Dictionary = _db.level_gen_params_row(3)
|
||||
var entry_count: int = int(params3.get("entry_count", -1))
|
||||
|
||||
@@ -56,6 +56,7 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/core/node_map_growth_test.gd", "expect": 0}, # 生成图生长递增+参数无解 fail-fast 子进程通道(卡① V8/V4)
|
||||
{"path": "res://tests/unit/core/level_table_validate_test.gd", "expect": 0}, # 消费侧加载校验四检+良构基底正负向(卡② T1/V1〔v1〕)
|
||||
{"path": "res://tests/unit/core/breakpoint_resume_test.gd", "expect": 0}, # 断点往返/续玩确定性/幂等/退出清断点
|
||||
{"path": "res://tests/unit/core/hero_ledger_test.gd", "expect": 0}, # 主角账公式逐值/三通道日记/断点片段往返与旧档回退(拆件批 V1:hr1/hr1-s/hr2)
|
||||
{"path": "res://tests/unit/core/curio_refresh_paid_test.gd", "expect": 0}, # 三选一刷新免费双源+GP 付费档(对齐商店 MC-1:价曲线/优先序/停点清零/钱包单口)
|
||||
{"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值)
|
||||
{"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ehnthaqk7h12
|
||||
@@ -348,20 +348,20 @@ func _smoke_adventure() -> void:
|
||||
l2_price_bonus = int(pb.get("bonus", 0))
|
||||
Kit.check("进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" % [hp_price + l2_price_bonus, hp_price, l2_price_bonus], gp0 >= hp_price + l2_price_bonus)
|
||||
Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.buy_product("hp_up"))
|
||||
var acc_shop := int(a8.run_ledger.bonus.get("max_hp", -1)) # L1 三选一后 + 商店 3(首件随种子)
|
||||
var acc_shop: int = a8.hero().bonus_total("max_hp") # L1 三选一后 + 商店 3(首件随种子)
|
||||
Kit.check("统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop, acc_shop >= 3)
|
||||
a8._on_level_finished({"result": "victory", "hp": int(r8.hero_hp), "node_index": r8.node_index,
|
||||
"node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟 L2 关卡完成(victory 载荷直调——非末关)
|
||||
Kit.check("L2 关卡完成三选一停点在案(直调兑付前——零自动推进)", not a8._curio_pending.is_empty())
|
||||
Kit.check("L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕", not a8.stop()._pending.is_empty())
|
||||
Kit.check("未兑付推进拒绝(选择停点零自动推进契约)", not a8.continue_to_next_level())
|
||||
var l2_cands: Array = a8._curio_pending["candidates"]
|
||||
var l2_cands: Array = a8.stop()._pending["candidates"]
|
||||
var l2_pick: Dictionary = l2_cands[0]
|
||||
Kit.check("三选一点选兑付受理(候选取首件)", a8.pick_curio(String(l2_pick.get("kind", "")), String(l2_pick.get("tier", "")), String(l2_pick.get("feat_id", ""))))
|
||||
var picked2: String = String(a8._curio_result.get("picked", ""))
|
||||
var expect_acc: int = acc_shop + (int(a8._curio_result.get("value", 0)) if picked2 == "max_hp" else 0)
|
||||
Kit.check("三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" % [int(a8.run_ledger.bonus.get("max_hp", -1)), acc_shop, picked2], int(a8.run_ledger.bonus.get("max_hp", -1)) == expect_acc)
|
||||
var picked2: String = String(a8.stop()._resolved.get("picked", ""))
|
||||
var expect_acc: int = acc_shop + (int(a8.stop()._resolved.get("value", 0)) if picked2 == "max_hp" else 0)
|
||||
Kit.check("三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" % [a8.hero().bonus_total("max_hp"), acc_shop, picked2], a8.hero().bonus_total("max_hp") == expect_acc)
|
||||
var max_eff8: int = a8.effective_max_hp() # 账本生效上限含调试浮窗 pct(a8 已 set 400)
|
||||
Kit.check("关卡完成部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" % [int(a8.ledger["hero_hp"]), max_eff8], int(a8.ledger["hero_hp"]) <= max_eff8)
|
||||
Kit.check("关卡完成部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" % [a8.hero().hero_hp, max_eff8], a8.hero().hero_hp <= max_eff8)
|
||||
Kit.check("兑付后推进受理(下一关开始)", a8.continue_to_next_level())
|
||||
# ⑨ 恢复点数学(接口③封顶公式三读数 + 同式两落点一致)
|
||||
var a9 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(99))
|
||||
@@ -391,7 +391,7 @@ func _smoke_adventure() -> void:
|
||||
var dk := String((a11._curio_deck[di] as Dictionary).get("kind", ""))
|
||||
if dk == "attack" or dk == "ap":
|
||||
a11._curio_deck.remove_at(di)
|
||||
var draw: Array = a11._draw_curio_candidates()
|
||||
var draw: Array = a11.stop().draw_candidates()
|
||||
var draw_kinds := {}
|
||||
for t in draw:
|
||||
draw_kinds[String((t as Dictionary).get("kind", ""))] = true
|
||||
@@ -401,7 +401,7 @@ func _smoke_adventure() -> void:
|
||||
# white-box 测试属停点渠道,零投放零行为)
|
||||
var treasure_seen := false
|
||||
for i in 40:
|
||||
for t in a11._draw_curio_candidates():
|
||||
for t in a11.stop().draw_candidates():
|
||||
if String((t as Dictionary).get("kind", "")) == "treasure_item":
|
||||
treasure_seen = true
|
||||
break
|
||||
@@ -411,8 +411,8 @@ func _smoke_adventure() -> void:
|
||||
a12.start_adventure(12)
|
||||
var cap := {"payload": {}}
|
||||
a12.curio_choice_presented.connect(func(p): cap["payload"] = p)
|
||||
var max_full := int(_db.hero.max_hp) + int(a12._base_override.get("max_hp", 0))
|
||||
a12.ledger["hero_hp"] = max_full
|
||||
var max_full: int = int(_db.hero.max_hp) + int(a12.hero().base_override_flat().get("max_hp", 0))
|
||||
a12.hero().hero_hp = max_full
|
||||
a12._on_level_finished({"result": "victory", "hp": max_full, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
Kit.check("满血关卡完成回血 0 合法(封顶口径——heal_gained=0 非负溢出)", int(cap["payload"].get("heal_gained", -1)) == 0)
|
||||
# ⑬ 奖励节点共享池(同库同池同摘牌;TP0:attack 令牌耗尽后节点域同样零出现)
|
||||
@@ -431,9 +431,9 @@ func _smoke_adventure() -> void:
|
||||
Kit.check("奖励节点共享池:令牌耗尽同样不出候选(同库同池——%s)" % str(ncands13), not has_attack13)
|
||||
a13.present_node_curio(ncands13)
|
||||
a13.present_node_curio([{"kind": "gp", "tier": "common"}])
|
||||
var gp13 := int(a13.ledger.get("gp", 0))
|
||||
Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.adventure.curio_value("gp", "common")), a13.apply_node_curio("gp", "common") and int(a13.ledger.get("gp", 0)) == gp13 + int(_db.adventure.curio_value("gp", "common")))
|
||||
Kit.check("节点停点态兑付后清零(恰一次语义同构)", a13._node_curio_pending.is_empty())
|
||||
var gp13: int = a13.hero().gp
|
||||
Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.adventure.curio_value("gp", "common")), a13.apply_node_curio("gp", "common") and a13.hero().gp == gp13 + int(_db.adventure.curio_value("gp", "common")))
|
||||
Kit.check("节点停点态兑付后清零(恰一次语义同构)", a13.stop()._node_pending.is_empty())
|
||||
var s10: Dictionary = a10.summary
|
||||
Kit.check("放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)", String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)))
|
||||
|
||||
@@ -513,10 +513,10 @@ func _smoke_feats() -> void:
|
||||
var feat_seen: Dictionary = {}
|
||||
var node_relic_seen := false
|
||||
for i in 60:
|
||||
for t in a_r._draw_curio_candidates("level_clear"):
|
||||
for t in a_r.stop().draw_candidates("level_clear"):
|
||||
if String((t as Dictionary).get("kind", "")) == "relic":
|
||||
feat_seen[String(t.get("feat_id", ""))] = true
|
||||
for t in a_r._draw_curio_candidates("reward_node"):
|
||||
for t in a_r.stop().draw_candidates("reward_node"):
|
||||
if String((t as Dictionary).get("kind", "")) == "relic":
|
||||
node_relic_seen = true
|
||||
Kit.check("关卡完成渠道只出专长(60 次重抽 ≥2 种不同 relic——身份键 feat_id 同档可多件)", feat_seen.size() >= 2)
|
||||
@@ -527,7 +527,7 @@ func _smoke_feats() -> void:
|
||||
var stock_before := {}
|
||||
for kk in ["attack", "gp", "heal"]:
|
||||
stock_before[kk] = a_t.pool_stock(kk)
|
||||
a_t._node_curio_pending = {"candidates": [{"kind": "feat_ticket", "tier": "excellent"}, {"kind": "attack", "tier": "common"}]}
|
||||
a_t.stop()._node_pending = {"candidates": [{"kind": "feat_ticket", "tier": "excellent"}, {"kind": "attack", "tier": "common"}]}
|
||||
Kit.check("A5 专长凭证兑付受理(节点渠道——拿到即开)", a_t.apply_node_curio("feat_ticket", "excellent"))
|
||||
var tkt_cands: Array = (a_t.node_curio_pending_state().get("candidates", []) as Array)
|
||||
var tkt_all_relic := tkt_cands.size() > 0
|
||||
@@ -541,23 +541,23 @@ func _smoke_feats() -> void:
|
||||
a_t.apply_node_curio("relic", String((tkt_cands[0] as Dictionary).get("tier", "rare")), String((tkt_cands[0] as Dictionary).get("feat_id", "")))
|
||||
Kit.check("A5 专长面兑付受理(专长凭证开出候选可正常入持有集)", (a_t.feats_held() as Array).size() == feats_n + 1)
|
||||
# A6 保底:pity 直填 5 → 下次展示必出(第 6 次必出口径);限购满 → 恒不触发
|
||||
a_t._feat_ticket_sold = 0 # A5 已兑付一枚专长凭证(sold=1=limit)——A6 段重置计数隔离验证
|
||||
a_t._feat_ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
|
||||
a_t.stop()._deck.ticket_sold = 0 # A5 已兑付一枚专长凭证(sold=1=limit)——A6 段重置计数隔离验证
|
||||
a_t.stop()._deck.ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
|
||||
var rg_t: RandomNumberGenerator = a_t.rng.open_stream("reward_pick")
|
||||
Kit.check("A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)", a_t._feat_ticket_consider(rg_t))
|
||||
Kit.check("A6 触发后清零", a_t._feat_ticket_pity == 0)
|
||||
a_t._feat_ticket_sold = int(a_t.db.adventure.feat_ticket_limit)
|
||||
a_t._feat_ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
|
||||
Kit.check("A6 限购满不再触发(保底冻结)", not a_t._feat_ticket_consider(rg_t))
|
||||
a_t._feat_ticket_sold = 0
|
||||
a_t._feat_ticket_pity = 0
|
||||
Kit.check("A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)", a_t.stop().feat_ticket_consider(rg_t))
|
||||
Kit.check("A6 触发后清零", a_t.stop()._deck.ticket_pity == 0)
|
||||
a_t.stop()._deck.ticket_sold = int(a_t.db.adventure.feat_ticket_limit)
|
||||
a_t.stop()._deck.ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
|
||||
Kit.check("A6 限购满不再触发(保底冻结)", not a_t.stop().feat_ticket_consider(rg_t))
|
||||
a_t.stop()._deck.ticket_sold = 0
|
||||
a_t.stop()._deck.ticket_pity = 0
|
||||
var offered := false
|
||||
for i in 200:
|
||||
if a_t._feat_ticket_consider(rg_t):
|
||||
if a_t.stop().feat_ticket_consider(rg_t):
|
||||
offered = true
|
||||
break
|
||||
Kit.check("A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)", offered)
|
||||
a_r._curio_pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||||
a_r.stop()._pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||||
Kit.check("relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" % [deck_relic, deck_relic - 1], a_r.pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.pool_stock("relic") == deck_relic - 1)
|
||||
# 断点往返三条(基点 1420–1426:relics 快照/计数器键/余库逐枚还原)按主会话裁决②改判集成宿主——
|
||||
# 驱动代码(save_manager 装配+_on_level_finished+快照取读)随三条同去,集成批落位时重建。
|
||||
@@ -774,8 +774,8 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
|
||||
return false
|
||||
if a._pending_next_level:
|
||||
# 三选一停点确认(确定性选件后推进,与 run_adventure 自动策略同口径;TP0 令牌化)
|
||||
if not a._curio_pending.is_empty():
|
||||
var bcands: Array = a._curio_pending["candidates"]
|
||||
if not a.stop()._pending.is_empty():
|
||||
var bcands: Array = a.stop()._pending["candidates"]
|
||||
var bpick: Dictionary = autoplay._pick_token(bcands)
|
||||
a.pick_curio(String(bpick.get("kind", "")), String(bpick.get("tier", "")), String(bpick.get("feat_id", "")))
|
||||
a.continue_to_next_level()
|
||||
@@ -839,7 +839,7 @@ func _drive_to_pref(autoplay, prefer: String) -> Dictionary:
|
||||
var cands: Array = (adr.node_curio_candidates() as Dictionary).get("candidates", []) as Array
|
||||
adr.present_node_curio(cands)
|
||||
out["cands"] = cands
|
||||
out["pending"] = not (adr._node_curio_pending as Dictionary).is_empty()
|
||||
out["pending"] = not (adr.stop()._node_pending as Dictionary).is_empty()
|
||||
return out
|
||||
var ids: Array = r.next_candidate_ids()
|
||||
if ids.is_empty():
|
||||
|
||||
@@ -95,7 +95,7 @@ func _init() -> void:
|
||||
Kit.check_quiet(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)")
|
||||
adr_e._curio_taken["attack"] = 1 # 白盒预置计数(断点附随件往返的判据面——计数不可由加算区反推)
|
||||
adr_e._on_level_finished({"result": "victory", "hp": 30, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
var cands_e: Array = (adr_e._curio_pending.get("candidates", []) as Array).duplicate()
|
||||
var cands_e: Array = (adr_e.stop()._pending.get("candidates", []) as Array).duplicate()
|
||||
Kit.check_quiet(not cands_e.is_empty(), "P5 非末关的关卡完成三选一停点在案(直调 victory——L1 非末关)")
|
||||
var sm_f = SaveManagerScript.new(_bp_path)
|
||||
Kit.check_quiet(sm_f.load_from_disk() and sm_f.has_breakpoint(), "P5 选择停点断点在档(选择停点可中断契约)")
|
||||
@@ -114,7 +114,7 @@ func _init() -> void:
|
||||
Kit.check_quiet(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)")
|
||||
var pick_e: Dictionary = cands_e[0]
|
||||
Kit.check_quiet(adr_f.pick_curio(String(pick_e.get("kind", "")), String(pick_e.get("tier", "")), String(pick_e.get("feat_id", ""))), "P5 点选兑付受理")
|
||||
var picked_e: String = String(adr_f._curio_result.get("picked", ""))
|
||||
var picked_e: String = String(adr_f.stop()._resolved.get("picked", ""))
|
||||
if adr_f._curio_taken.has(picked_e):
|
||||
Kit.check_quiet(int(adr_f._curio_taken[picked_e]) == (2 if picked_e == "attack" else 1),
|
||||
"P5 兑付计数一致(%s——预置 1 + 本次兑付)" % picked_e)
|
||||
|
||||
@@ -24,24 +24,24 @@ func _init() -> void:
|
||||
Kit.check_quiet(adr.refresh_curio() == false and adr.refresh_curio_paid() == false, "无停点:免费/付费刷新皆拒绝零副作用")
|
||||
|
||||
# ---- ② 免费位优先序 X→Y(白盒造关卡完成停点——smoke_all ⑫ 同构)----
|
||||
adr._curio_pending = {"candidates": _cands(), "payload": {}}
|
||||
adr.stop()._pending = {"candidates": _cands(), "payload": {}}
|
||||
Kit.check_quiet(adr.curio_stop_pending() == true, "关卡完成停点在途=谓词 true")
|
||||
adr._attune_refresh_x = 1
|
||||
adr._attune_refresh_y = 2
|
||||
Kit.check_quiet(adr.refresh_curio() and adr._attune_refresh_x == 0 and adr._attune_refresh_y == 2, "X 优先消耗(x1y2→x0y2)")
|
||||
Kit.check_quiet(adr.refresh_curio() and adr._attune_refresh_y == 1, "X 穷转 Y 池(y2→y1)")
|
||||
adr.stop()._refresh_x = 1
|
||||
adr.stop()._refresh_y = 2
|
||||
Kit.check_quiet(adr.refresh_curio() and adr.stop()._refresh_x == 0 and adr.stop()._refresh_y == 2, "X 优先消耗(x1y2→x0y2)")
|
||||
Kit.check_quiet(adr.refresh_curio() and adr.stop()._refresh_y == 1, "X 穷转 Y 池(y2→y1)")
|
||||
|
||||
# ---- ③ GP 付费档:计数递增爬价 + 零触碰免费位 ----
|
||||
Kit.check_quiet(adr.curio_refresh_gp_cost() == base, "首刷报价=基价(免费位尚余 y1,报价独立于免费位)")
|
||||
Kit.check_quiet(adr.refresh_curio_paid() and adr._attune_refresh_y == 1, "付费刷新不触碰免费位(y1 保持)")
|
||||
Kit.check_quiet(adr.refresh_curio_paid() and adr.stop()._refresh_y == 1, "付费刷新不触碰免费位(y1 保持)")
|
||||
Kit.check_quiet(adr.curio_refresh_gp_cost() == int(ceil(float(base) * curve)), "二刷价爬升=ceil(基价×曲线)(已付费刷=1)")
|
||||
Kit.check_quiet(adr.refresh_curio_paid() and adr.curio_refresh_gp_cost() == int(ceil(float(base) * curve * curve)), "三刷价再爬升=ceil(基价×曲线²)(已付费刷=2)")
|
||||
|
||||
# ---- ④ 停点重建清零付费计数(对齐商店进店清零——不结转)----
|
||||
adr.present_node_curio(_cands())
|
||||
Kit.check_quiet(adr._curio_paid_refreshes == 0 and adr.curio_refresh_gp_cost() == base, "节点停点重建=付费计数清零回基价")
|
||||
Kit.check_quiet(adr.stop()._paid_refreshes == 0 and adr.curio_refresh_gp_cost() == base, "节点停点重建=付费计数清零回基价")
|
||||
adr.refresh_curio_paid()
|
||||
Kit.check_quiet(adr._curio_paid_refreshes == 1, "付费刷新计数在途递增(=1)")
|
||||
Kit.check_quiet(adr.stop()._paid_refreshes == 1, "付费刷新计数在途递增(=1)")
|
||||
adr.present_node_curio(_cands())
|
||||
Kit.check_quiet(adr.curio_refresh_gp_cost() == base, "再碰三选一付费计数仍清零(同 X 余量不结转口径)")
|
||||
|
||||
@@ -56,10 +56,10 @@ func _init() -> void:
|
||||
var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2, "graph_gen_v2": 1}
|
||||
var adr_r = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(adr_r.resume_adventure(bp_y, {"hero_override": {}, "attune_refresh_y": 3}), "续玩受理(最小断点+参数面)")
|
||||
Kit.check_quiet(adr_r._attune_refresh_y == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)")
|
||||
Kit.check_quiet(adr_r.stop()._refresh_y == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)")
|
||||
var adr_o = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(adr_o.resume_adventure({"level_index": 1, "hero_hp": 10, "graph_gen_v2": 1}, {"hero_override": {}, "attune_refresh_y": 3}), "旧断点续玩受理(快照缺键)")
|
||||
Kit.check_quiet(adr_o._attune_refresh_y == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)")
|
||||
Kit.check_quiet(adr_o.stop()._refresh_y == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)")
|
||||
|
||||
quit(Kit.finish("curio_refresh_paid"))
|
||||
|
||||
|
||||
@@ -27,42 +27,42 @@ func _init() -> void:
|
||||
quit(Kit.finish("feat_vouchers"))
|
||||
return
|
||||
# V2——adventure 桶新冒险置空:start_adventure 后宿主字典为空(寿命随 RunLedger,无残留)
|
||||
Kit.check_quiet(a.run_ledger.adventure_counters.is_empty(),
|
||||
"V2:新冒险 adventure 桶置空(start_adventure 后 run_ledger.adventure_counters 为空)〔无状态化 V2〕")
|
||||
Kit.check_quiet(a.adventure_counters.is_empty(),
|
||||
"V2:新冒险 adventure 桶置空(start_adventure 后宿主桶 adventure_counters 为空)〔无状态化 V2〕")
|
||||
|
||||
# V9-1 刷新券:入存量账(冒险级)——反例=写进 _attune_refresh_x(X 每停点重置会吞存量)
|
||||
a._reset_refresh_x() # X 归零基线(模拟新停点)
|
||||
Kit.check_quiet(a._apply_curio_pick("refresh", "excellent", 1), "刷新券兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a.refresh_stock() == 1 and a._attune_refresh_x == 0,
|
||||
a.stop()._reset_refresh_x() # X 归零基线(模拟新停点)
|
||||
Kit.check_quiet(a.stop()._apply_curio_pick("refresh", "excellent", 1), "刷新券兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a.refresh_stock() == 1 and a.stop()._refresh_x == 0,
|
||||
"F-1 修复:刷新券入存量账、X 账零写入(写错账反例不复发)〔专长批 V9〕")
|
||||
a._reset_refresh_x() # 再碰停点——X 重置但存量不吞
|
||||
a.stop()._reset_refresh_x() # 再碰停点——X 重置但存量不吞
|
||||
Kit.check_quiet(a.refresh_stock() == 1 and a.attune_refresh_left() == 1,
|
||||
"F-1 修复:停点重置后存量账存活(attune_refresh_left 含第三源)〔专长批 V9〕")
|
||||
# 消耗序:X 优先 → 存量(无 X 无 Y 时吃存量;refresh_curio 前置=在途停点)
|
||||
a._refresh_stock = 1
|
||||
a._attune_refresh_y = 0
|
||||
a._node_curio_pending = {"candidates": [{"kind": "attack", "tier": "common"}]}
|
||||
a.stop()._refresh_stock = 1
|
||||
a.stop()._refresh_y = 0
|
||||
a.stop()._node_pending = {"candidates": [{"kind": "attack", "tier": "common"}]}
|
||||
Kit.check_quiet(a.refresh_curio() and a.refresh_stock() == 0,
|
||||
"刷新消耗序:X/Y 空时吃存量账〔专长批 V9〕")
|
||||
a._node_curio_pending = {}
|
||||
a.stop()._node_pending = {}
|
||||
# 摘牌:选中即消尽(禁早退路径零摘牌)
|
||||
var deck_refresh_before: int = a.pool_stock("refresh")
|
||||
Kit.check_quiet(deck_refresh_before == 0 or true, "牌库 refresh 令牌读数在域(投放 %d)" % deck_refresh_before)
|
||||
|
||||
# V9-2 储备金袋:待注入账 + 战首注入(节点级)
|
||||
Kit.check_quiet(a._apply_curio_pick("rap", "rare", 3), "储备金袋兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a._pending_rap == 3, "储备金袋:待注入 RAP +3(节点级账)〔专长批 V9〕")
|
||||
Kit.check_quiet(a.stop()._apply_curio_pick("rap", "rare", 3), "储备金袋兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a.stop()._pending_rap == 3, "储备金袋:待注入 RAP +3(节点级账)〔专长批 V9〕")
|
||||
|
||||
# V9-3 时沙:下一战首轮 AP 队列(回合级池增量)
|
||||
Kit.check_quiet(a._apply_curio_pick("time_sand", "rare", 2), "时沙兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a._time_sand_queue == 2, "时沙:首轮 AP 队列 +2(池增量非上限)〔专长批 V9〕")
|
||||
Kit.check_quiet(a.stop()._apply_curio_pick("time_sand", "rare", 2), "时沙兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a.stop()._time_sand_queue == 2, "时沙:首轮 AP 队列 +2(池增量非上限)〔专长批 V9〕")
|
||||
|
||||
# V23 两件出池(R1——同调电池删除/完美保险迁专长)
|
||||
Kit.check_quiet(not db.adventure.CURIO_KINDS.has("attune_battery")
|
||||
and not db.adventure.CURIO_KINDS.has("perfect_insurance"),
|
||||
"出池词表:CURIO_KINDS 零两件(三选一池物理不可再出)〔卡② R1〕")
|
||||
Kit.check_quiet(not a._apply_curio_pick("attune_battery", "rare", 1)
|
||||
and not a._apply_curio_pick("perfect_insurance", "rare", 1),
|
||||
Kit.check_quiet(not a.stop()._apply_curio_pick("attune_battery", "rare", 1)
|
||||
and not a.stop()._apply_curio_pick("perfect_insurance", "rare", 1),
|
||||
"出池反例:两件兑付不再受理(牌库无令牌=拒绝零副作用)〔卡② R1〕")
|
||||
Kit.check_quiet(not a.has_method("choose_battery_target") and not a.has_method("battery_pending"),
|
||||
"电池符号清零:runner 无点选函数与 getter(编译级孤儿面)〔卡② R1〕")
|
||||
@@ -88,34 +88,34 @@ func _init() -> void:
|
||||
"配额锚:稀有档合计 7、投放品种={ap,rap,time_sand}、令牌池总量 102(validate 硬断言同批)〔卡② R3〕")
|
||||
Kit.check_quiet(db.adventure.validate().is_empty(), "AdventureTable.validate() 零违例(两件出池后)〔卡② R3〕")
|
||||
|
||||
# V9-6 全链:凭证令牌兑付=牌库摘牌(选中即消尽)
|
||||
# V9-6 全链:凭证令牌兑付=牌库摘牌(选中即消尽)〔拆件批 V2——cs3 凭证拿到即开现役断言承继〕
|
||||
a._curio_deck = [{"kind": "rap", "tier": "rare"}, {"kind": "attack", "tier": "common"}]
|
||||
a._curio_pending = {"candidates": a._curio_deck.duplicate()}
|
||||
a.stop()._pending = {"candidates": a._curio_deck.duplicate()}
|
||||
Kit.check_quiet(a.pick_curio("rap", "rare") and a.pool_stock("rap") == 0,
|
||||
"凭证令牌:pick_curio 摘牌(选中踢池——与属性件同语义)〔专长批 V9〕")
|
||||
|
||||
# V10-1 留任 2 件路由与值不变:gp 钱包/heal 当即回血(每次点选独立停点态)
|
||||
a._curio_deck = [{"kind": "gp", "tier": "common"}, {"kind": "heal", "tier": "excellent"}]
|
||||
a._curio_pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
|
||||
a.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
|
||||
var gp_taken_before := int(a._curio_taken.get("gp", 0))
|
||||
Kit.check_quiet(a.pick_curio("gp", "common") and int(a._curio_taken.get("gp", 0)) == gp_taken_before + 1,
|
||||
"留任小GP 袋:GP 通道兑付受理(gp 计数入账——路由零变化)〔专长批 V10〕")
|
||||
var hp_before := int(a.ledger.get("hero_hp", 0))
|
||||
a._curio_pending = {"candidates": [{"kind": "heal", "tier": "excellent"}]}
|
||||
Kit.check_quiet(a.pick_curio("heal", "excellent") and int(a.ledger.get("hero_hp", 0)) == mini(hp_before + int(db.adventure.curio_value("heal", "excellent")), a.effective_max_hp()),
|
||||
var hp_before: int = a.hero().hero_hp
|
||||
a.stop()._pending = {"candidates": [{"kind": "heal", "tier": "excellent"}]}
|
||||
Kit.check_quiet(a.pick_curio("heal", "excellent") and a.hero().hero_hp == mini(hp_before + int(db.adventure.curio_value("heal", "excellent")), a.effective_max_hp()),
|
||||
"留任应急口粮:当即回血 +6 封顶生效上限〔专长批 V10〕")
|
||||
# V10-2 行动点上限归属性轴:ap kind 走加算区第四轴
|
||||
a._curio_deck = [{"kind": "ap", "tier": "rare"}]
|
||||
a._curio_pending = {"candidates": a._curio_deck.duplicate()}
|
||||
var ap_bonus_before := int(a.run_ledger.bonus.get("ap", 0))
|
||||
Kit.check_quiet(a.pick_curio("ap", "rare") and int(a.run_ledger.bonus.get("ap", 0)) == ap_bonus_before + int(db.adventure.curio_value("ap", "rare")),
|
||||
a.stop()._pending = {"candidates": a._curio_deck.duplicate()}
|
||||
var ap_bonus_before: int = a.hero().bonus_total("ap")
|
||||
Kit.check_quiet(a.pick_curio("ap", "rare") and a.hero().bonus_total("ap") == ap_bonus_before + int(db.adventure.curio_value("ap", "rare")),
|
||||
"行动点上限归属性轴:ap 兑付写加算区第四轴(bonus.ap 承载)〔专长批 V10〕")
|
||||
|
||||
# V16——断点扩容往返:新账本随断点快照/续玩回填(breakpoint_resume_test P5 同构)
|
||||
var sm = load("res://core/save_manager.gd").new("user://_feat_vouchers_bp.json")
|
||||
sm.load_from_disk()
|
||||
a.run_ledger.adventure_counters = {"pain_layers": 3, "adv_twin_digs": 5} # 四账住 RunLedger
|
||||
a._refresh_stock = 2
|
||||
a.adventure_counters = {"pain_layers": 3, "adv_twin_digs": 5} # 冒险桶宿主字典直写(测试构造面)
|
||||
a.stop()._refresh_stock = 2
|
||||
a.set_save_manager(sm)
|
||||
a._on_level_finished({"result": "victory", "hp": 10, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
var bp16: Dictionary = sm.breakpoint_snapshot()
|
||||
@@ -138,7 +138,7 @@ func _init() -> void:
|
||||
var b4 = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(b4.resume_adventure(bp_legacy2, {}),
|
||||
"V13:旧档 adventure_counters 含改名前旧键续玩受理零崩溃(无迁移——旧键留桶成死键)〔卡② V13/H1〕")
|
||||
Kit.check_quiet(int(b2.run_ledger.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_stock() == 2,
|
||||
Kit.check_quiet(int(b2.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_stock() == 2,
|
||||
"V16:续玩回填逐项一致(adventure 桶随断点往返·计数恢复)〔无状态化 V3〕")
|
||||
# V25 完美保险专长验收组(R2 全项:装载/限次一次/计数纯净/两账相减/跨战清零/事件不吞)
|
||||
var fi: Dictionary = db.feats.row_by_id("perfect_insurance")
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 主角实体账单测(拆件批 T1——hr1 公式逐值/hr1-s 商店加算同值/hr2 三通道日记与断点片段)。
|
||||
# 公式判据值全部由表读值推导(禁数值字面量断言);真链段走 start_adventure 与 runner 接口③。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/core/hero_ledger_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const HeroLedgerScript := preload("res://core/hero_ledger.gd")
|
||||
const AdventureRunnerScript := preload("res://core/adventure_runner.gd")
|
||||
const LevelRunnerScript := preload("res://core/level_runner.gd")
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("[hero_ledger_test] FAIL — 数值表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# ---- hr1:生效公式逐值(表值+永久强化 flat 后乘调试 pct——与改前同式)----
|
||||
var hero = HeroLedgerScript.new(db, {"atk": 3, "max_hp": 5, "armor_class": 2})
|
||||
Kit.check_quiet(hero.effective_max_hp() == int(db.hero.max_hp) + 5,
|
||||
"hr1 生效上限 = 表值+永久强化 flat(pct=0 乘法恒等)")
|
||||
Kit.check_quiet(hero.effective_atk() == int(db.hero.atk) + 3,
|
||||
"hr1 生效攻击 = 表值+永久强化 flat")
|
||||
Kit.check_quiet(hero.effective_ac() == int(db.forge_armor_route_by_type(db.forge_default_armor_type()).get("base_ac", 0)) + 2,
|
||||
"hr1 生效护甲 = 甲型基础 AC+甲路线等级加值(纯 flat 无 pct)")
|
||||
hero.set_debug_override(50, 50)
|
||||
Kit.check_quiet(hero.effective_max_hp() == int(round(float(int(db.hero.max_hp) + 5) * 1.5)),
|
||||
"hr1 调试 pct=50 后乘——上限 ×1.5(叠加次序:flat 先加 pct 后乘)")
|
||||
hero.set_debug_override(0, 0)
|
||||
hero.gain_bonus("max_hp", 4, "battle_export", "max_hp")
|
||||
Kit.check_quiet(hero.effective_max_hp() == int(db.hero.max_hp) + 5 + 4,
|
||||
"hr1 加算区 flat 与永久强化 flat 同列基数(三段叠加)")
|
||||
|
||||
# hr1 回血封顶公式逐值(算术宿主=HeroLedger.rest_heal 单点——改前 LevelRunner.rest_heal 同式)
|
||||
Kit.check_quiet(HeroLedgerScript.rest_heal(30, 25, 26) == mini(int(floor(26.0 * 30.0 / 100.0)), 26 - 25),
|
||||
"hr1 回血 = floor(上限×比例÷100) 后封顶缺口(比例部分 7 > 缺口 1 → 1)")
|
||||
Kit.check_quiet(HeroLedgerScript.rest_heal(30, 10, 26) == int(floor(26.0 * 30.0 / 100.0)),
|
||||
"hr1 回血比例部分未触封顶时按比例整值")
|
||||
|
||||
# ---- hr1 真链段:start_adventure 置满+接口③治疗封顶回报(runner._rest_handler → 主角账公式)----
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(1234)
|
||||
var adr = AdventureRunnerScript.new(db, rng)
|
||||
Kit.check_quiet(adr.start_adventure(1234, {}), "hr1 真链冒险启动(seed 1234)")
|
||||
var runner = adr.runner()
|
||||
Kit.check_quiet(runner.hero_hp == adr.hero().effective_max_hp(),
|
||||
"hr1 冒险开始生命账本置满生效上限(含 pct 口径——与战斗内同源)")
|
||||
var healed: int = adr.apply_rest(30, runner.hero_hp, runner.effective_max_hp()) # 接口③真链(关卡恢复点兑付的同一 handler)
|
||||
Kit.check_quiet(healed == HeroLedgerScript.rest_heal(30, runner.hero_hp - healed, runner.effective_max_hp()),
|
||||
"hr1 接口③真链回报 = 主角账回血公式(恢复点兑付同链)")
|
||||
|
||||
# ---- hr1-s:商店属性商品加算走主角账单写原语(购买后 bonus_total 与旧径同值)----
|
||||
runner._gain_wallet(1000) # 钱包注入(测试直调写口——测试构造惯例)
|
||||
var products: Array = runner.shop_products()
|
||||
var bought := false
|
||||
for p in products:
|
||||
if int(p.get("price", 1 << 30)) <= 1000 and String(p.get("attr", "")) != "":
|
||||
var attr := String(p.get("attr", ""))
|
||||
var before: int = runner.hero().bonus_total(attr)
|
||||
bought = runner.buy_product(String(p.get("product_id", "")))
|
||||
if bought:
|
||||
Kit.check_quiet(runner.hero().bonus_total(attr) == before + int(p.get("flat_value", 0)),
|
||||
"hr1-s 商店购买后 bonus_total 增量 = 商品 flat(加算走 gain_bonus 单写原语——同值承载)")
|
||||
Kit.check_quiet(runner.hero().purchase_tier(String(p.get("product_id", ""))) == 1,
|
||||
"hr1-s 购买档数记主角账(递增定价输入单点)")
|
||||
break
|
||||
Kit.check_quiet(bought, "hr1-s 真链购买受理(GP 充足路径)")
|
||||
|
||||
# ---- hr2:三通道日记(明细来源指回+汇总同步维护)----
|
||||
var h2 = HeroLedgerScript.new(db, {})
|
||||
h2.gain_bonus("atk", 5, "curio_pick", "attack")
|
||||
h2.gain_bonus("atk", 2, "shop_product", "p_atk_1")
|
||||
h2.gain_bonus("armor_class", 1, "battle_export", "armor_class")
|
||||
Kit.check_quiet(h2.bonus_total("atk") == 7 and h2.bonus_total("armor_class") == 1,
|
||||
"hr2 三通道汇总累计(三选一件+商店+战末同落加算区)")
|
||||
var log_arr: Array = h2._gain_log
|
||||
Kit.check_quiet(log_arr.size() == 3, "hr2 获得记录列表三通道各一条")
|
||||
Kit.check_quiet(String((log_arr[0] as Dictionary).get("source_class", "")) == "curio_pick"
|
||||
and String((log_arr[1] as Dictionary).get("source_class", "")) == "shop_product"
|
||||
and String((log_arr[2] as Dictionary).get("source_class", "")) == "battle_export",
|
||||
"hr2 记录来源指回正确(curio_pick/shop_product/battle_export 词表)")
|
||||
var details: Array = h2._bonus_detail
|
||||
Kit.check_quiet(details.size() == 3 and int((details[0] as Dictionary).get("gain_id", 0)) == int((log_arr[0] as Dictionary).get("id", -1)),
|
||||
"hr2 明细带来源指回(gain_id ↔ 记录 id)")
|
||||
Kit.check_quiet(h2.keystone_gained() == 0 and h2.vision_used() == 0,
|
||||
"hr2 钥石/真视零通道时零账")
|
||||
|
||||
# hr2 钥石/真视/购买记账
|
||||
h2.record_keystone_drop({"level": 1, "first": true, "amount": 3})
|
||||
Kit.check_quiet(h2.keystone_gained() == 3 and (h2.keystone_drop_log()[0] as Dictionary).get("amount", 0) == 3,
|
||||
"hr2 钥石逐杀明细+累计单写")
|
||||
h2.record_vision_use()
|
||||
Kit.check_quiet(h2.vision_used() == 1, "hr2 真视使用恰计一次")
|
||||
|
||||
# hr2 断点片段往返(日记在档=正主)
|
||||
var frag: Dictionary = h2.breakpoint_fragment()
|
||||
var h3 = HeroLedgerScript.new(db, {})
|
||||
h3.restore_fragment(frag, {})
|
||||
Kit.check_quiet(h3.bonus_total("atk") == 7 and h3.bonus_total("armor_class") == 1,
|
||||
"hr2 片段往返:日记在档由明细推导汇总(逐轴相等)")
|
||||
Kit.check_quiet(h3.hero_hp == h2.hero_hp and h3.gp == h2.gp and h3.keystone_gained() == 3 and h3.vision_used() == 1,
|
||||
"hr2 片段往返:账面键逐值(生命/钱包/钥石/真视)")
|
||||
Kit.check_quiet((h3._gain_log as Array).size() == 3 and (h3._bonus_detail as Array).size() == 3,
|
||||
"hr2 片段往返:日记全量还原")
|
||||
|
||||
# hr2 旧档回退(无日记键→汇总直载跳过比对)
|
||||
var legacy: Dictionary = frag.duplicate(true)
|
||||
legacy.erase("gain_journal")
|
||||
var h4 = HeroLedgerScript.new(db, {})
|
||||
h4.restore_fragment(legacy, {})
|
||||
Kit.check_quiet(h4.bonus_total("atk") == 7 and h4.bonus_total("armor_class") == 1 and (h4._gain_log as Array).is_empty(),
|
||||
"hr2 旧档缺日记→汇总键直载(跳过比对——旧档行为零变化)")
|
||||
|
||||
# hr2 日记在档推导比对不符→拒绝载入(fail-fast——汇总不回填)
|
||||
var tampered: Dictionary = frag.duplicate(true)
|
||||
(tampered["bonus"] as Dictionary)["atk"] = 99
|
||||
var h5 = HeroLedgerScript.new(db, {})
|
||||
h5.restore_fragment(tampered, {})
|
||||
Kit.check_quiet(h5.bonus_total("atk") == 0,
|
||||
"hr2 汇总与日记推导不符→拒载(汇总不回填——日记为正主)")
|
||||
|
||||
quit(Kit.finish("hero_ledger_test", "PASS — 主角账公式/三通道日记/断点片段往返与旧档回退 全绿"))
|
||||
@@ -203,9 +203,9 @@ func _test_resume_rebuild(db) -> void:
|
||||
t.info_visible_layers = 2
|
||||
t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30}]
|
||||
db.level = t
|
||||
var ledger := {"hero_hp": 10, "gp": 0}
|
||||
var resumed = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
var ok: bool = resumed.start_level_external(1, ledger, {}, Callable(), {}, false,
|
||||
resumed.hero_hp = 10 # 冒险模式生命经主角账预置(旧 ledger 注入参数的等效构造面)
|
||||
var ok: bool = resumed.start_level_external(1, Callable(), false,
|
||||
{"node_id": 2, "visited_ids": [0, 1, 2]})
|
||||
Kit.check_quiet(ok, "续玩用例:resume_state 重建停点成功")
|
||||
if not ok:
|
||||
|
||||
@@ -721,8 +721,7 @@ func _smoke_level_graph() -> void:
|
||||
# ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 /
|
||||
# 终点恒可见 / 入口 BFS 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。
|
||||
var vis_runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(ACTIVE_GRAPH_SEED))
|
||||
var vis_ledger := {"hero_hp": 10, "gp": 0}
|
||||
var vis_ok: bool = vis_runner.start_level_external(2, vis_ledger, {}, Callable(), {}, true)
|
||||
var vis_ok: bool = vis_runner.start_level_external(2, Callable(), true)
|
||||
Kit.check("可见性冒烟:L2 入口停点就位(defer_entry——无当前节点,锚层 0)", vis_ok and vis_runner.awaiting_choice() and vis_runner.current_node_id() == -1)
|
||||
var vis_ids: Array = vis_runner.type_visible_ids()
|
||||
var vis_cand_in := true
|
||||
@@ -1034,7 +1033,7 @@ func _smoke_shop_room() -> void:
|
||||
Kit.check("购买 atk_up 受理(现价 6)", runner.buy_product("atk_up"))
|
||||
Kit.check("钱包扣现价 6(%d → %d)" % [gp0, runner.gp], runner.gp == gp0 - 6)
|
||||
# 加算区住 RunLedger
|
||||
Kit.check("统一冒险加算区 atk +1(bonus={atk,max_hp,armor_class} 账本)", int(runner.run_ledger.bonus.get("atk", -1)) == 1 and int(runner.run_ledger.bonus.get("max_hp", -1)) == 0 and int(runner.run_ledger.bonus.get("armor_class", -1)) == 0)
|
||||
Kit.check("统一冒险加算区 atk +1(加算区住主角账)", runner.hero().bonus_total("atk") == 1 and runner.hero().bonus_total("max_hp") == 0 and runner.hero().bonus_total("armor_class") == 0)
|
||||
by_id = _shop_products_by_id(runner.shop_products())
|
||||
Kit.check("atk_up 二购现价 = 4+2+2=8(步长 2 × 已购 1 档,现价现算)", int(by_id["atk_up"]["price"]) == 8 and int(by_id["atk_up"]["purchased"]) == 1)
|
||||
# 行进段百分比归零(购买/生效断言按加算区裸值口径——调试浮窗百分比后乘;hp 快照钳到新口径上限防 spawn 越界,测试通道)
|
||||
@@ -1319,10 +1318,10 @@ func _smoke_shop_pool() -> void:
|
||||
var val_map := {"attack": "atk", "max_hp": "max_hp", "armor": "armor_class", "ap": "ap"}
|
||||
var gp0: int = int(r8.gp)
|
||||
var taken0: int = int(a8._curio_taken.get(kind, 0))
|
||||
var val0: int = int(a8.run_ledger.bonus.get(String(val_map[kind]), -999))
|
||||
var val0: int = a8.hero().bonus_total(String(val_map[kind]))
|
||||
Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.buy_pool_item(kind))
|
||||
Kit.check("钱包扣现价(%d → %d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - int(buy_c.get("price", 0)))
|
||||
Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.adventure.curio_value(kind, tier))], int(a8.run_ledger.bonus.get(String(val_map[kind]), -999)) == val0 + int(_db.adventure.curio_value(kind, tier)))
|
||||
Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.adventure.curio_value(kind, tier))], a8.hero().bonus_total(String(val_map[kind])) == val0 + int(_db.adventure.curio_value(kind, tier)))
|
||||
Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8._curio_taken.get(kind, 0))], int(a8._curio_taken.get(kind, 0)) == taken0 + 1)
|
||||
# A 链:同visit 第二购现价 = ceil(base×b^(l2−1)×A^1)(货架已全重抽——新卡新价同公式)
|
||||
# A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为)
|
||||
@@ -1359,12 +1358,12 @@ func _smoke_shop_pool() -> void:
|
||||
break
|
||||
Kit.check("库存尽剔牌库(attack 令牌耗尽——池消耗即上限跨渠道共库 reward→shop 向)", not attack_seen)
|
||||
var stock_before: int = a8.pool_stock("attack")
|
||||
Kit.check("池空消费拒绝(handler 零副作用——买光即枯竭硬闸)", not a8._consume_pool_purchase("attack", "common") and a8.pool_stock("attack") == stock_before)
|
||||
Kit.check("池空消费拒绝(handler 零副作用——买光即枯竭硬闸)", not a8.stop().consume_pool_purchase("attack", "common") and a8.pool_stock("attack") == stock_before)
|
||||
# ⑥ 跨渠道共库 shop→reward 向:回填一枚 attack 令牌 → 消费口受理 → 奖励牌库同步摘牌
|
||||
a8._curio_deck.append({"kind": "attack", "tier": "common"})
|
||||
Kit.check("商店消费口受理(_apply_curio_pick 同口——真单点库)", a8._consume_pool_purchase("attack", "common"))
|
||||
Kit.check("商店消费口受理(停点件兑付单点同口——真单点库)〔拆件批 V3〕", a8.stop().consume_pool_purchase("attack", "common"))
|
||||
var reward_attack_seen := false
|
||||
for t6 in a8._draw_curio_candidates():
|
||||
for t6 in a8.stop().draw_candidates():
|
||||
if String((t6 as Dictionary).get("kind", "")) == "attack":
|
||||
reward_attack_seen = true
|
||||
break
|
||||
@@ -1405,8 +1404,8 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
|
||||
return false
|
||||
if a._pending_next_level:
|
||||
# 三选一停点确认(确定性选件后推进,与 run_adventure 自动策略同口径;TP0 令牌化)
|
||||
if not a._curio_pending.is_empty():
|
||||
var bcands: Array = a._curio_pending["candidates"]
|
||||
if not a.stop()._pending.is_empty():
|
||||
var bcands: Array = a.stop()._pending["candidates"]
|
||||
var bpick: Dictionary = autoplay._pick_token(bcands)
|
||||
a.pick_curio(String(bpick.get("kind", "")), String(bpick.get("tier", "")), String(bpick.get("feat_id", "")))
|
||||
a.continue_to_next_level()
|
||||
|
||||
@@ -290,17 +290,17 @@ func _meta_settle_and_workshop() -> void:
|
||||
Kit.check("充能账本按装备 active_charged 件重置(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面)", # 充能账住 RunLedger
|
||||
int(a12.run_ledger.attune_charges.get("incense_stick", -1)) == 1 and int(a12.run_ledger.attune_charges.get("sky_eye", -1)) == 1)
|
||||
a12._attunement = []
|
||||
a12._attune_refresh_x = 1
|
||||
a12._attune_refresh_y = 2
|
||||
a12._curio_pending = {"candidates": [{"kind": "gp", "tier": "common"}, {"kind": "attack", "tier": "common"}, {"kind": "max_hp", "tier": "common"}], "payload": {}}
|
||||
a12.stop()._refresh_x = 1
|
||||
a12.stop()._refresh_y = 2
|
||||
a12.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}, {"kind": "attack", "tier": "common"}, {"kind": "max_hp", "tier": "common"}], "payload": {}}
|
||||
Kit.check("刷新双计数器——X 优先消耗(x1y2 → x0y2)",
|
||||
a12.refresh_curio() and a12._attune_refresh_x == 0 and a12._attune_refresh_y == 2)
|
||||
a12.refresh_curio() and a12.stop()._refresh_x == 0 and a12.stop()._refresh_y == 2)
|
||||
Kit.check("刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)",
|
||||
a12.refresh_curio() and a12.refresh_curio() and a12._attune_refresh_y == 0
|
||||
a12.refresh_curio() and a12.refresh_curio() and a12.stop()._refresh_y == 0
|
||||
and a12.refresh_curio() == false)
|
||||
a12._reset_refresh_x()
|
||||
a12.stop()._reset_refresh_x()
|
||||
Kit.check("刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕",
|
||||
a12._attune_refresh_x == 0 and a12.attune_refresh_left() == 0)
|
||||
a12.stop()._refresh_x == 0 and a12.attune_refresh_left() == 0)
|
||||
# ⑬ 主菜单开局传参面(开新/续玩两入口同调三键随开局下传防回归:真实入口曾
|
||||
# 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params
|
||||
# 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源)
|
||||
@@ -365,7 +365,7 @@ func _meta_injection() -> void:
|
||||
int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0)))
|
||||
Kit.check("首战 hero_max_hp 与开局生命账本 = 表值 + 注入档",
|
||||
int(r.effective_max_hp()) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0))
|
||||
and int(adr.ledger["hero_hp"]) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)))
|
||||
and int(adr.hero().hero_hp) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)))
|
||||
# 白板:同种子下 空参数 = 零强化参数 = 纯表值(逐位一致——注入通道默认值漏改在此转红)
|
||||
var adr_w = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
|
||||
Kit.check("白板冒险启动(无 params 实参——B1 空参数路径)", adr_w.start_adventure(920251))
|
||||
@@ -511,10 +511,10 @@ func _smoke_forge_axes() -> void:
|
||||
Kit.check("入战 AC 三层:甲型基础 12(重甲)+路线加值 1+局内 0〔锻造工坊批 V7〕",
|
||||
int(r5.effective_ac()) == 12 + 1)
|
||||
Kit.check("参数面快照入 runner(attack_type=normal/armor_type=heavy)〔锻造工坊批 V6〕",
|
||||
adr5.hero_attack_type == "normal" and adr5.hero_armor_type == "heavy")
|
||||
adr5.hero().attack_type == "normal" and adr5.hero().armor_type == "heavy")
|
||||
# V6 锁死:开冒险后改存档选中→参数面/注入值不变(快照冻结;light=初始持有已解锁可切)
|
||||
Kit.check("冒险开始后锁死:改存档选中不影响参数面快照与生效值(契约 8)〔锻造工坊批 V6〕",
|
||||
mp5.forge_select_armor("light") and adr5.hero_armor_type == "heavy"
|
||||
mp5.forge_select_armor("light") and adr5.hero().armor_type == "heavy"
|
||||
and int(r5.effective_ac()) == 12 + 1)
|
||||
# V5/V7 末端:enc 两轴直达 spawn 英雄快照(hero_attack 行携带选中类型键)
|
||||
var run_ax = _run_battle(1, 0, {"attack_type": "pierce", "armor_type": "ward"})
|
||||
|
||||
@@ -35,10 +35,10 @@ func _init() -> void:
|
||||
# T10——空配置验收闸(三选一池空兜底/商店空货架)
|
||||
var e = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
e.start_adventure(501)
|
||||
e._curio_pending = {"candidates": []}
|
||||
Kit.check_quiet(e.skip_empty_curio() and e._curio_pending.is_empty(),
|
||||
e.stop()._pending = {"candidates": []}
|
||||
Kit.check_quiet(e.skip_empty_curio() and e.stop()._pending.is_empty(),
|
||||
"空配置闸:三选一池空=停点整体跳过(skip_empty_curio 消化)〔专长批 T10〕")
|
||||
e._curio_pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
|
||||
e.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
|
||||
Kit.check_quiet(not e.skip_empty_curio(),
|
||||
"空配置闸反例:非空停点拒绝跳过(零自动推进契约不破)〔专长批 T10〕")
|
||||
var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
@@ -73,7 +73,7 @@ func check_v7(a, db) -> void:
|
||||
_tok("armor", "common"), _tok("heal", "excellent"), _tok("refresh", "excellent"), _tok("ap", "rare")]
|
||||
var hi_seen := false
|
||||
for i in 40:
|
||||
var cands: Array = a._draw_curio_candidates("reward_node")
|
||||
var cands: Array = a.stop().draw_candidates("reward_node")
|
||||
if cands.size() != 3:
|
||||
continue
|
||||
var hi := 0
|
||||
@@ -87,7 +87,7 @@ func check_v7(a, db) -> void:
|
||||
a._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
|
||||
var ok_fb := true
|
||||
for i in 20:
|
||||
var cands2: Array = a._draw_curio_candidates("reward_node")
|
||||
var cands2: Array = a.stop().draw_candidates("reward_node")
|
||||
if cands2.size() != 3:
|
||||
ok_fb = false
|
||||
break
|
||||
@@ -100,13 +100,13 @@ func check_v7(a, db) -> void:
|
||||
_tok("feat_ticket", "excellent")]
|
||||
var ticket_seen := false
|
||||
for i in 30:
|
||||
for t in a._draw_curio_candidates("reward_node"):
|
||||
for t in a.stop().draw_candidates("reward_node"):
|
||||
if String(t.get("kind", "")) == "feat_ticket":
|
||||
ticket_seen = true
|
||||
Kit.check_quiet(not ticket_seen, "凭证不进抽签池(feat_ticket 只经保底注入通道)〔专长批 V7〕")
|
||||
# 构成④:池空=零候选合法态(同族兜底入口)
|
||||
a._curio_deck = []
|
||||
Kit.check_quiet(a._draw_curio_candidates("reward_node").is_empty(), "池空=零候选合法态(skip_empty_curio 兜底入口)〔专长批 V7〕")
|
||||
Kit.check_quiet(a.stop().draw_candidates("reward_node").is_empty(), "池空=零候选合法态(skip_empty_curio 兜底入口)〔专长批 V7〕")
|
||||
|
||||
|
||||
func check_v6(b, db) -> void:
|
||||
@@ -116,21 +116,21 @@ func check_v6(b, db) -> void:
|
||||
# 拿了消耗:直置停点候选后 pick_curio——牌库摘牌单点
|
||||
b._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")]
|
||||
var gp_before: int = b.pool_stock("gp")
|
||||
b._curio_pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")]}
|
||||
b.stop()._pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")]}
|
||||
Kit.check_quiet(b.pick_curio("gp", "common") and b.pool_stock("gp") == gp_before - 1,
|
||||
"拿了消耗:pick_curio 后牌库摘牌(选中踢池)〔专长批 V6〕")
|
||||
# 没拿回池:抽候选不摘牌——重复抽签同令牌可再现
|
||||
var deck_size: int = b._curio_deck.size()
|
||||
var seen_again := false
|
||||
for i in 30:
|
||||
var cands: Array = b._draw_curio_candidates("reward_node")
|
||||
var cands: Array = b.stop().draw_candidates("reward_node")
|
||||
if cands.size() == 2 and b._curio_deck.size() == deck_size:
|
||||
seen_again = true
|
||||
Kit.check_quiet(seen_again, "没拿回池:抽签不摘牌(未选候选再次可现,牌库余量不变)〔专长批 V6〕")
|
||||
# 只刷出不计:抽签后 _curio_taken 零变化
|
||||
var taken_before: Dictionary = b._curio_taken.duplicate()
|
||||
for i in 10:
|
||||
b._draw_curio_candidates("reward_node")
|
||||
b.stop().draw_candidates("reward_node")
|
||||
var unchanged := true
|
||||
for k in taken_before.keys():
|
||||
if int(b._curio_taken.get(k, 0)) != int(taken_before[k]):
|
||||
@@ -145,7 +145,7 @@ func check_v12(m, db) -> void:
|
||||
var tier_n := {"common": 0, "excellent": 0, "rare": 0, "legendary": 0}
|
||||
var total := 0
|
||||
for i in 300:
|
||||
for t in m._draw_curio_candidates("level_clear"):
|
||||
for t in m.stop().draw_candidates("level_clear"):
|
||||
tier_n[String(t.get("tier", ""))] = int(tier_n.get(String(t.get("tier", "")), 0)) + 1
|
||||
total += 1
|
||||
Kit.check_quiet(total == 900, "V12 前置:300 停点 ×3 候选=900 席(实 %d)" % total)
|
||||
@@ -169,11 +169,11 @@ func check_v8(c, db) -> void:
|
||||
# cap 只在拿了才计:刷出不计
|
||||
c._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
|
||||
for i in 20:
|
||||
c._draw_curio_candidates("reward_node")
|
||||
c.stop().draw_candidates("reward_node")
|
||||
Kit.check_quiet(bool(c._curio_cap_ok("gp", "")) and int(c._curio_taken.get("gp", 0)) == 0,
|
||||
"cap 刷出不计:20 次抽签后 gp 未拿、cap 未触发〔专长批 V8〕")
|
||||
# 拿了才计:pick 一次→cap 满→提前退役(候选视图剔除、牌库本体不动)
|
||||
c._curio_pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]}
|
||||
c.stop()._pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]}
|
||||
Kit.check_quiet(c.pick_curio("gp", "common"), "V8 前置:兑付 gp 一枚")
|
||||
Kit.check_quiet(not bool(c._curio_cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕")
|
||||
var gp_in_deck := 0
|
||||
@@ -182,7 +182,7 @@ func check_v8(c, db) -> void:
|
||||
gp_in_deck += 1
|
||||
var gp_cand := false
|
||||
for i in 30:
|
||||
for t in c._draw_curio_candidates("reward_node"):
|
||||
for t in c.stop().draw_candidates("reward_node"):
|
||||
if String(t.get("kind", "")) == "gp":
|
||||
gp_cand = true
|
||||
Kit.check_quiet(gp_in_deck >= 0 and not gp_cand,
|
||||
|
||||
@@ -119,7 +119,7 @@ func _init() -> void:
|
||||
"刷新练习:装配冒险启动")
|
||||
Kit.check_quiet(int(adr5.refresh_stock()) == 1 and int(adr5.attune_refresh_left()) == 1,
|
||||
"刷新练习:存量账 +1(装配即得一次性——attune_refresh_left 合计含 stock)")
|
||||
Kit.check_quiet(int(adr5._attune_refresh_x) == 0,
|
||||
Kit.check_quiet(int(adr5.stop()._refresh_x) == 0,
|
||||
"刷新练习反例:临时账 X 不动(写临时账/重复加仓=违规形态)")
|
||||
|
||||
# ⑥ 储备罐(转存率 125=1.25:1 整除口径 / 100=1:1 逐位同旧)
|
||||
@@ -269,13 +269,12 @@ func _init() -> void:
|
||||
Kit.check_quiet(ob12b.minesweeper.seal_range_free == false,
|
||||
"信标网络反例:未装配封印可达豁免=缺省 false(零装配零扰动)")
|
||||
|
||||
# ⑬ 真视使用计数链(受理成功恰计一次——RunLedger 单类持账)
|
||||
# ⑬ 真视使用计数链(受理成功恰计一次——主角账单点记录)
|
||||
var rng13 = load("res://core/rng_service.gd").new(); rng13.set_master_seed(321)
|
||||
var ob13 = orch_s.new(db, rng13)
|
||||
var lg13 = load("res://core/run_ledger.gd").new()
|
||||
lg13.vision_counter = {"used": 0}
|
||||
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "ledger": lg13}),
|
||||
"计数链:装配起战(RunLedger 携带)")
|
||||
var lg13 = load("res://core/hero_ledger.gd").new(db, {})
|
||||
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "hero": lg13}),
|
||||
"计数链:装配起战(HeroLedger 携带)")
|
||||
var inv13 = load("res://core/treasure_items.gd").new(db)
|
||||
var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
|
||||
ob13.treasure_inventory = inv13
|
||||
@@ -283,7 +282,7 @@ func _init() -> void:
|
||||
var tgt13: Vector2i = _first_closed_safe(ob13)
|
||||
if tgt13 != Vector2i(-9, -9):
|
||||
ob13.use_treasure(h13, tgt13)
|
||||
Kit.check_quiet(int(lg13.vision_counter.get("used", 0)) == 1,
|
||||
Kit.check_quiet(lg13.vision_used() == 1,
|
||||
"计数链正常:受理成功恰 +1(信标网络解锁行为条件数据源)")
|
||||
|
||||
# ⑭ 装配不可变(V6:冒险全程装配快照三面同源且零写口——
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user