diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index cdd1a2dc..80a1afec 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -38,6 +38,7 @@ var _loadout_seeded := false # 同调行已种标记(每冒险一 # ---- 三选一停点(编排迁 CurioStop 件——停点三态/刷新三源/凭证出箱/信号面)---- var _curio: CurioDeck = null # 令牌池件(_init 建;纯池——多重集/抽签/cap/凭证判定唯一宿主) var _stop: CurioStop = null # 停点件(_init 建;每冒险一次——关卡换实例不换件) +var _refresh_bank: RefreshBank = null # 刷新账件(_init 建——三源账与付费报价;宿主预算注入与断点片段直收) var _loadout: AttuneLoadout = null # 同调配装件(_init 建;充能单源化——物化行唯一事实源) var adventure_inventory: TreasureItems # 冒险级秘宝库存(单源化——同调未用行跨关存活的承载面;关卡域语义归 level 使用面) var _tree_rules: Array[Variant] = [] # 树 rule 键集(Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面) @@ -51,7 +52,9 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: _wallet = GpWallet.new() _stats = HeroStats.new(db, {}, _journal) # 数值账消费日记汇总(同源单件注入) _curio = CurioDeck.new(db, rng) - _stop = CurioStop.new(db, _curio, _stats, _journal, _snapshot_breakpoint) + _refresh_bank = RefreshBank.new(db) + _stop = CurioStop.new(db, {"deck": _curio, "stats": _stats, "journal": _journal, "bank": _refresh_bank}, + _snapshot_breakpoint) _loadout = AttuneLoadout.new(db, _attunement) adventure_inventory = TreasureItems.new(db) @@ -73,6 +76,10 @@ func deck() -> CurioDeck: return _curio +func refresh_bank() -> RefreshBank: + return _refresh_bank + + # 件出口(停点件——呈现/测量面取停点编排单点;拆件批接口块) func stop() -> CurioStop: return _stop @@ -104,8 +111,8 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: _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 - _stop.set_refresh_x_cap(_attune_refresh_x_total()) # 停点件 X 基数预算注入(装配集冒险期间锁死) - _stop.set_refresh_y(maxi(0, int(params.get("attune_refresh_y", 0)))) # additive(Y 冒险周期池) + _refresh_bank.set_refresh_x_cap(_attune_refresh_x_total()) # 刷新账 X 基数预算注入(装配集冒险期间锁死) + _refresh_bank.set_refresh_y(maxi(0, int(params.get("attune_refresh_y", 0)))) # additive(Y 冒险周期池) _loadout.set_attunement(_attunement) # 装配集注入(活引用——种行面) adventure_inventory.reset() # 冒险级库存重置(新局空库存) _loadout_seeded = false # 同调行投行延至首关 bind 时点(库存就位后) @@ -115,7 +122,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: 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"): - _stop.add_refresh_stock(int(rp_row.get("magnitude", 0))) + _refresh_bank.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 [] @@ -220,8 +227,9 @@ func _resume_params(bp: Dictionary, params: Dictionary) -> void: _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 - _stop.set_refresh_x_cap(_attune_refresh_x_total()) # 停点件 X 基数预算注入(续玩参数面同 start) - _stop.restore_fragment(bp, params) # 停点件片段回填(凭证计数/刷新存量/Y 池——Y 缺键回退参数面 params_fallback 类) + _refresh_bank.set_refresh_x_cap(_attune_refresh_x_total()) # 刷新账 X 基数预算注入(续玩参数面同 start) + _stop.restore_fragment(bp, params) # 停点件片段回填(凭证计数) + _refresh_bank.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 下传) @@ -299,9 +307,9 @@ func feats_held() -> Array: # ---- 商店共享池消费面(纯池——库存=牌库剩余令牌)---- # 库存余量(池消耗即上限;奖励摘牌与商店购买同库承载,全冒险一次性) -# 终局池快照(_settle 清账前调用——复位会重置计数,摘要 additive 键须在此前取数) +# 终局池快照(_settle 清账前调用——复位会重置计数,摘要 additive 键须在此前取数;读面直连令牌池件) func _pool_stock_snapshot() -> Dictionary: - return _stop.pool_stock_snapshot() + return _curio.stock_snapshot() # ---- 奖励节点三选一(与关卡完成三选一共享同池同权重同次数上限——编排归停点件)---- @@ -576,6 +584,7 @@ func _bp_parts() -> Dictionary[Variant, Variant]: var stats_frag: Dictionary[Variant, Variant] = _stats.breakpoint_fragment() var wallet_frag: Dictionary[Variant, Variant] = _wallet.breakpoint_fragment() var journal_frag: Dictionary[Variant, Variant] = _journal.breakpoint_fragment() + var bank_frag: Dictionary[Variant, Variant] = _refresh_bank.breakpoint_fragment() var stop_frag: Dictionary[Variant, Variant] = _stop.breakpoint_fragment() var level_frag: Dictionary[Variant, Variant] = _runner.breakpoint_fragment() if _runner != null else { "node_id": -1, "visited_ids": [] as Array, "aux_items": [] as Array, @@ -605,8 +614,8 @@ func _bp_parts() -> Dictionary[Variant, Variant]: "feat_ticket_sold": int(stop_frag.get("feat_ticket_sold", 0)), # T5 专长凭证限购计数(同上) "adventure_counters": adventure_counters.duplicate(true), # adventure 域计数器随断点(续玩跨战账不丢——桶归宿主字典) "vision_counter_used": int(journal_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 冒险周期池余量随断点(扩列向后兼容——旧档缺键回退参数面) + "refresh_stock": int(bank_frag.get("refresh_stock", 0)), # 刷新存量账随断点 + "attune_refresh_y": int(bank_frag.get("attune_refresh_y", 0)), # Y 冒险周期池余量随断点(扩列向后兼容——旧档缺键回退参数面) "bonus_result": stop_frag.get("bonus_result", {}), # 已兑付待推进态(空 = 无——中断续玩不重兑付) "keystone_drops": journal_frag.get("keystone_drops", []), # 逐杀掉落明细(摘要归因续接) "gain_journal": journal_frag.get("gain_journal", {}), # 获得日记(唯一声明的断点扩列——日记为现行事实源) diff --git a/game/core/curio_stop.gd b/game/core/curio_stop.gd index b1d7d072..76dec168 100644 --- a/game/core/curio_stop.gd +++ b/game/core/curio_stop.gd @@ -23,22 +23,20 @@ var _grant_gp := Callable() # 当前关卡对象 grant_curio_gp 方 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) +# ---- 刷新账(三源账与付费计数归 RefreshBank 件——编排留本件:查停点态→bank 支出→重抽重发)---- +var _bank: RefreshBank # 刷新账件(构造注入——零停点态依赖) # ---- 凭证出箱(奇物.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, stats_ref: HeroStats, journal_ref: GainJournal, snapshot_call: Callable) -> void: +func _init(db_ref: ConfigDb, deps: Dictionary, snapshot_call: Callable) -> void: db = db_ref - _deck = deck_ref - _stats = stats_ref - _journal = journal_ref + _deck = deps.get("deck", null) as CurioDeck + _stats = deps.get("stats", null) as HeroStats + _journal = deps.get("journal", null) as GainJournal + _bank = deps.get("bank", null) as RefreshBank _snapshot_call = snapshot_call @@ -60,11 +58,11 @@ func open_level_clear(payload_base: Dictionary) -> void: var max_eff := _stats.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) _stats.hero_hp = hp_before + heal - _reset_refresh_x() # 碰三选一重置 X 余量(不结转) - _paid_refreshes = 0 # 付费刷计数随停点清零(对齐商店进店清零) + _bank.reset_x() # 碰三选一重置 X 余量(不结转) + _bank.reset_paid() # 付费刷计数随停点清零(对齐商店进店清零) var pick_payload: Dictionary[Variant, Variant] = { "level_index": _level_index, - "candidates": draw_candidates("level_clear"), # T4 渠道白名单——关底 Boss 击败=关卡完成渠道只出专长 + "candidates": _deck.draw_candidates("level_clear"), # T4 渠道白名单——关底 Boss 击败=关卡完成渠道只出专长 "heal_gained": heal, "hp_before": hp_before, "hp_after": _stats.hero_hp, @@ -85,13 +83,13 @@ func open_level_clear(payload_base: Dictionary) -> void: # 候选呈现(节点到达时调用——恰一 reward_pick scope 洗牌 + 道具件掷定 scope;本停点断点不载, # 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)。 func node_curio_candidates() -> Dictionary: - var cands := draw_candidates("reward_node") + var cands := _deck.draw_candidates("reward_node") return {"candidates": cands} func present_node_curio(candidates: Array) -> void: - _reset_refresh_x() # 碰三选一重置 X 余量(不结转) - _paid_refreshes = 0 # 付费刷计数随停点清零(对齐商店进店清零) + _bank.reset_x() # 碰三选一重置 X 余量(不结转) + _bank.reset_paid() # 付费刷计数随停点清零(对齐商店进店清零) _node_pending = {"candidates": candidates.duplicate(true)} @@ -128,7 +126,8 @@ func _feat_payload_keys(feat_id: String) -> Dictionary: return {"feat_id": feat_id, "feat_name": String(row.get("display_name", "")), "feat_archetype": String(row.get("archetype", ""))} -# 三选一件兑付共享体(TP0——令牌消费、摘牌单点,含 relic 分支): +# 三选一件兑付共享体(TP0——令牌消费、摘牌单点,含 relic 分支;领域拆分批拆三私有段: +# 资源件段/路由加算段/副作用通道段,兑付语义与顺序逐字不变): # 属性件写加算区 + 件获取计数、AP 件写第四轴、GP 件走钱包、恢复件当即回血(封顶生效上限—— # 与账本置满/关卡完成回血同式 effective_max_hp)、relic 件入持有集(散件单体保底——效果接线面 # 按挂点消费,档位质变由 FeatsEngine 求值)。兑付即从牌库摘除该令牌(选中踢池; @@ -140,8 +139,17 @@ func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = if not _deck.consume_deck_token(kind, tier, feat_id): push_error("[停点件] 令牌消费失败——牌库无此令牌: %s|%s|%s" % [kind, tier, feat_id]) return false + if _apply_resource_pick(kind, value, feat_id): # 资源件段(refresh/rap/time_sand/relic) + return true + if _apply_route_pick(kind, value): # 路由加算段(CURIO_APPLY_ROUTES 声明路由) + return true + return _apply_channel_pick(kind, value) # 副作用通道段(gp/heal 显式分支——未知件 false) + + +# 资源件段(refresh/rap/time_sand/relic——冒险级资源与凭证账面;非资源件 false 交回主路由) +func _apply_resource_pick(kind: String, value: int, feat_id: String) -> bool: if kind == "refresh": - _refresh_stock += value # T4 F-1——刷新券入刷新存量账(冒险级;**不写 _refresh_x**:X 每停点 _reset_refresh_x 重置会吞存量;摘牌先于落账=选中即消尽) + _bank.add_refresh_stock(value) # T4 F-1——刷新券入刷新存量账(冒险级;**不写 X 临时账**:X 每停点 reset_x 重置会吞存量;摘牌先于落账=选中即消尽) _bump_taken("refresh") return true if kind == "rap": @@ -158,14 +166,22 @@ func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = _bump_taken("relic") _bump_taken("relic:" + feat_id) # 身份维已拿计数(cap 消费面) return true - # 加算形态路由(match 收敛进效果类型:属性值提升四类走 - # adventure_table.CURIO_APPLY_ROUTES 声明路由(bonus 落点键),加新加算类令牌零代码) + return false + + +# 路由加算段(match 收敛进效果类型:属性值提升四类走 adventure_table.CURIO_APPLY_ROUTES 声明 +# 路由(bonus 落点键),加新加算类令牌零代码) +func _apply_route_pick(kind: String, value: int) -> bool: var sink := db.adventure.curio_apply_route(kind) - if sink != "": - _journal.gain_bonus(sink, value, "curio_pick", kind) # 加算区单写原语(获得日记明细+汇总同拍) - _bump_taken(kind) - return true - # 通道面(副作用形态——显式分支) + if sink == "": + return false + _journal.gain_bonus(sink, value, "curio_pick", kind) # 加算区单写原语(获得日记明细+汇总同拍) + _bump_taken(kind) + return true + + +# 副作用通道段(副作用形态——显式分支:GP 件走关卡对象入账回调、恢复件当即回血) +func _apply_channel_pick(kind: String, value: int) -> bool: match kind: "gp": if _grant_gp.is_valid(): @@ -240,7 +256,7 @@ func skip_empty_curio() -> bool: func _redeem_feat_ticket() -> bool: _deck.ticket_sold += 1 # 限购计数(本体在池件——原宿主转发属性迁点) _deck.ticket_pity = 0 - var cands := draw_candidates("level_clear") # 专长池(与关卡完成渠道同白名单) + var cands := _deck.draw_candidates("level_clear") # 专长池(与关卡完成渠道同白名单) if cands.is_empty(): push_error("[停点件] 专长凭证兑付失败——专长池空集") _deck.ticket_sold -= 1 @@ -256,33 +272,10 @@ func _redeem_feat_ticket() -> bool: return true -# ---- 五组入口之四:刷新族(X/Y 双源:X=刷新件每碰三选一重给〔优先消耗、不结转〕、 -# Y=营地升级行冒险周期池〔用完即无〕;重抽消费 reward_pick 既有洗牌流,池规则全保留——不放回/ +# ---- 五组入口之四:刷新编排(三源账与报价归 RefreshBank 件——本件只做停点态校验、 +# 支出分派与重抽重发;重抽消费 reward_pick 既有洗牌流,池规则全保留——不放回/ # 达上限剔牌/掷定前置直显;停点可中断)---- -var _refresh_x_cap := 0 # 刷新件余量基数(装配集 effect_kinds 含 bonus_refresh 的行 magnitude 合计—— - # 宿主冒险开始时预算注入;装配集冒险期间锁死〔契约 8〕故预算与逐停点查询等价) - - -# 刷新件基数注入(宿主 start_adventure/resume 时点计算——X 每碰三选一重置回此基数) -func set_refresh_x_cap(total: int) -> void: - _refresh_x_cap = maxi(0, total) - - -func _reset_refresh_x() -> void: - _refresh_x = _refresh_x_cap - - -# 当前可刷新次数(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 { @@ -302,12 +295,6 @@ 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() @@ -322,20 +309,12 @@ func _refresh_curio_common(paid: bool) -> bool: if not curio_stop_pending(): return false if paid: - _paid_refreshes += 1 + _bank.charge_paid() # 付费刷计数递增(报价曲线输入——账面归刷新账件) else: - # 免费位消耗序:X(刷新件每停点重给,优先消耗)→ Y(营地冒险周期池)→ 存量账(刷新券加仓, - # 跨停点持久) - if _refresh_x <= 0 and _refresh_y <= 0 and _refresh_stock <= 0: + if not _bank.spend_free(): # 免费位消耗序 X→Y→存量(三源全空 false 零副作用——序与语义逐字归账件) 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") + var cands := _deck.draw_candidates("level_clear") _pending["candidates"] = cands.duplicate(true) var payload: Dictionary[Variant, Variant] = (_pending.get("payload", {}) as Dictionary).duplicate(true) payload["candidates"] = cands @@ -345,7 +324,7 @@ func _refresh_curio_common(paid: bool) -> bool: _snapshot_call.call() return true if not _node_pending.is_empty(): - var cands2 := draw_candidates("reward_node") + var cands2 := _deck.draw_candidates("reward_node") _node_pending["candidates"] = cands2.duplicate(true) return true return false @@ -364,21 +343,6 @@ func _finish_node_bonus(picked: String, value: int, tier: String = "", 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: @@ -390,43 +354,16 @@ func consume_pool_purchase(kind: String, tier: String) -> bool: 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、停点三态与刷新三源清零)---- +# ---- 冒险级复位(宿主 _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 + _bank.reset_all() # 刷新三源与付费计数清零(账面归刷新账件) _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: @@ -453,29 +390,22 @@ func voucher_outbox() -> Dictionary: return out -# 已兑付态重发(宿主 resume 已兑付分支调用——信号面归停点件,续玩不重兑付只重发载荷) -func reemit_resolved() -> void: - curio_resolved.emit(_resolved.duplicate(true)) - - # 已兑付态清除(宿主 continue_to_next_level 推进受理时调用——重兑付防御复位面) func clear_resolved() -> void: _resolved = {} -# 兑付态直读(宿主 resume 分支载入已兑付载荷用——副本) -func resolved_payload() -> Dictionary: - return _resolved.duplicate(true) - - -# 已兑付态注入(resume 分支——宿主置关序位后由本件承载载荷) +# 已兑付态注入+重发(宿主 resume 已兑付分支调用——置态后重发载荷,信号面归停点件: +# 续玩不重兑付只重发;原 restore_resolved/reemit_resolved 两步收编一步,时序逐位同) func restore_resolved(payload: Dictionary) -> void: _resolved = payload.duplicate(true) + curio_resolved.emit(_resolved.duplicate(true)) # ---- 断点自交片段(应用层拼盘口收件)---- -# 片段键:bonus_pending/bonus_result/feat_ticket_pity/feat_ticket_sold/refresh_stock/attune_refresh_y -#(feat_ticket_* 现役为宿主字段——随停点件迁移同片段承载;读侧 params 回退经参传入) +# 片段键:bonus_pending/bonus_result/feat_ticket_pity/feat_ticket_sold +#(feat_ticket_* 现役为宿主字段——随停点件迁移同片段承载;refresh_stock/attune_refresh_y 两键 +# 归 RefreshBank 片段,宿主拼盘同一并集) func breakpoint_fragment() -> Dictionary[Variant, Variant]: return { @@ -483,17 +413,11 @@ func breakpoint_fragment() -> Dictionary[Variant, Variant]: "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: +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))))) diff --git a/game/core/refresh_bank.gd b/game/core/refresh_bank.gd new file mode 100644 index 00000000..4502be5b --- /dev/null +++ b/game/core/refresh_bank.gd @@ -0,0 +1,111 @@ +## 刷新账件:三选一刷新三源账唯一所有者(X 刷新件每碰三选一重给/优先消耗不结转、Y 营地升级行 +## 冒险周期池/用完即无、刷新券存量账/冒险级跨停点持久)+GP 付费刷计数与报价公式 +## (ceil(商店基价×曲线^本次停点已付费刷)——与商店货架刷新同两个调参旋钮,单点调参)。 +## 零停点态依赖:免费/付费支出的编排归 CurioStop(查停点态→本件支出→重抽重发),本件只记账。 +## 断点自交片段(refresh_stock/attune_refresh_y 两键,Y 缺键回退参数面)——拼盘落盘归冒险应用层。 +## Core 层零 UI;随机零消费。 +class_name RefreshBank +extends RefCounted + +var db: ConfigDb +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) +var _refresh_x_cap := 0 # 刷新件余量基数(装配集 effect_kinds 含 bonus_refresh 的行 magnitude 合计—— + # 宿主冒险开始时预算注入;装配集冒险期间锁死〔契约 8〕故预算与逐停点查询等价) + + +func _init(db_ref: ConfigDb) -> void: + db = db_ref + + +# 刷新件基数注入(宿主 start_adventure/resume 时点计算——X 每碰三选一重置回此基数) +func set_refresh_x_cap(total: int) -> void: + _refresh_x_cap = maxi(0, total) + + +# X 每碰三选一重置回基数(不结转——宿主停点开启时点调用) +func reset_x() -> void: + _refresh_x = _refresh_x_cap + + +# 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 + + +# 刷新存量账读数(T4——刷新券加仓余量单独读面;呈现层与断点消费) +func refresh_stock() -> int: + return _refresh_stock + + +# 当前可刷新次数(X 优先 + Y 池 + 刷新存量账——呈现层余量读数口) +func free_left() -> int: + return _refresh_x + _refresh_y + _refresh_stock + + +# 免费位消耗(X→Y→存量消耗序逐字;三源全空 false 零副作用) +func spend_free() -> bool: + # 免费位消耗序: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 + return true + + +# 付费刷计数递增(对齐商店 MC-1——免费位 X/Y 用尽后转付费,同旋钮同曲线同钱包) +func charge_paid() -> void: + _paid_refreshes += 1 + + +# 付费刷计数随停点清零(对齐商店进店清零) +func reset_paid() -> void: + _paid_refreshes = 0 + + +# 本次 GP 付费刷新价(与商店货架刷新同源:ceil(基价 × 曲线^本次停点已付费刷)) +func gp_cost() -> int: + return int(ceil(float(db.adventure.shop_refresh_cost_base) + * pow(db.adventure.shop_refresh_cost_curve, float(_paid_refreshes)))) + + +# 冒险级复位(宿主 reset_adventure_state 调用——刷新三源与付费计数全零) +func reset_all() -> void: + _refresh_x = 0 + _refresh_y = 0 + _paid_refreshes = 0 + _refresh_stock = 0 + + +# ---- 断点自交片段(应用层拼盘口收件——本件只交自己的账)---- +# 片段键:refresh_stock/attune_refresh_y + +func breakpoint_fragment() -> Dictionary[Variant, Variant]: + return { + "refresh_stock": _refresh_stock, + "attune_refresh_y": _refresh_y, + } + + +# 片段回填(刷新存量/Y 池。存量缺键缺省 0(clamp 类);Y 余量优先读断点(花掉不回血=堵 +# 「用完退菜单再续玩满血复活」可利用漏洞);缺键=旧版断点,回退参数面整额注入(旧档行为零变化) +# ——params_fallback 类) +func restore_fragment(frag: Dictionary, params: Dictionary) -> void: + _refresh_stock = maxi(0, int(frag.get("refresh_stock", 0))) # 旧档缺键缺省 0(clamp 类) + _refresh_y = maxi(0, int(frag.get("attune_refresh_y", int(params.get("attune_refresh_y", 0))))) diff --git a/game/core/refresh_bank.gd.uid b/game/core/refresh_bank.gd.uid new file mode 100644 index 00000000..3c9edf4a --- /dev/null +++ b/game/core/refresh_bank.gd.uid @@ -0,0 +1 @@ +uid://cvh8xf2u44xcg diff --git a/game/presentation/curio_interact_controller.gd b/game/presentation/curio_interact_controller.gd index e4cf062d..afa4bece 100644 --- a/game/presentation/curio_interact_controller.gd +++ b/game/presentation/curio_interact_controller.gd @@ -147,8 +147,8 @@ func show_pick() -> void: func _feed_refresh_display(adventure: AdventureRunner) -> void: var runner = _runner_get.call() var gp := int(runner.gp) if runner != null else 0 - var cost: int = adventure.stop().curio_refresh_gp_cost() - curio_overlay.set_refresh_display(adventure.stop().attune_refresh_left(), cost, gp >= cost) + var cost: int = adventure.refresh_bank().gp_cost() + curio_overlay.set_refresh_display(adventure.refresh_bank().free_left(), cost, gp >= cost) func _curio_card_texts(candidates: Array) -> Array: @@ -187,7 +187,7 @@ func on_refresh() -> void: var refreshed: bool = adventure.stop().refresh_curio() if not refreshed: # 免费位(X/Y)已穷——GP 付费档(先扣钱包后重抽,余额不足拒绝零副作用) - var cost: int = adventure.stop().curio_refresh_gp_cost() + var cost: int = adventure.refresh_bank().gp_cost() if runner == null or not runner.spend_gp(cost): toast.show_toast("GP不足——刷新失败", "warn") return diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index c88a601c..601ea855 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -396,7 +396,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.stop().draw_candidates() + var draw: Array = a11.deck().draw_candidates() var draw_kinds := {} for t in draw: draw_kinds[String((t as Dictionary).get("kind", ""))] = true @@ -406,7 +406,7 @@ func _smoke_adventure() -> void: # white-box 测试属停点渠道,零投放零行为) var treasure_seen := false for i in 40: - for t in a11.stop().draw_candidates(): + for t in a11.deck().draw_candidates(): if String((t as Dictionary).get("kind", "")) == "treasure_item": treasure_seen = true break @@ -518,10 +518,10 @@ func _smoke_feats() -> void: var feat_seen: Dictionary = {} var node_relic_seen := false for i in 60: - for t in a_r.stop().draw_candidates("level_clear"): + for t in a_r.deck().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.stop().draw_candidates("reward_node"): + for t in a_r.deck().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) @@ -531,7 +531,7 @@ func _smoke_feats() -> void: a_t.start_adventure(91) var stock_before := {} for kk in ["attack", "gp", "heal"]: - stock_before[kk] = a_t.stop().pool_stock(kk) + stock_before[kk] = a_t.deck().pool_stock(kk) a_t.stop()._node_pending = {"candidates": [{"kind": "feat_ticket", "tier": "excellent"}, {"kind": "attack", "tier": "common"}]} Kit.check("A5 专长凭证兑付受理(节点渠道——拿到即开)", a_t.stop().apply_node_curio("feat_ticket", "excellent")) var tkt_cands: Array = (a_t.stop().node_curio_pending_state().get("candidates", []) as Array) @@ -540,7 +540,7 @@ func _smoke_feats() -> void: if String((t as Dictionary).get("kind", "")) != "relic": tkt_all_relic = false Kit.check("A5 拿到即开——停点候选已替换为专长三选一(全 relic)", tkt_all_relic) - Kit.check("A5 零摘牌——专长凭证兑付不动令牌库(不进持有集)", a_t.stop().pool_stock("attack") == int(stock_before["attack"]) and a_t.stop().pool_stock("gp") == int(stock_before["gp"])) + Kit.check("A5 零摘牌——专长凭证兑付不动令牌库(不进持有集)", a_t.deck().pool_stock("attack") == int(stock_before["attack"]) and a_t.deck().pool_stock("gp") == int(stock_before["gp"])) Kit.check("A5 无专长凭证残留持有集", not (a_t.feats_held() as Array).has("feat_ticket")) var feats_n := (a_t.feats_held() as Array).size() a_t.stop().apply_node_curio("relic", String((tkt_cands[0] as Dictionary).get("tier", "rare")), String((tkt_cands[0] as Dictionary).get("feat_id", ""))) @@ -549,21 +549,21 @@ func _smoke_feats() -> void: 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.stop().feat_ticket_consider(rg_t)) + Kit.check("A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)", a_t.deck().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)) + Kit.check("A6 限购满不再触发(保底冻结)", not a_t.deck().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.stop().feat_ticket_consider(rg_t): + if a_t.deck().feat_ticket_consider(rg_t): offered = true break Kit.check("A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)", offered) 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.stop().pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.stop().pool_stock("relic") == deck_relic - 1) + Kit.check("relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" % [deck_relic, deck_relic - 1], a_r.stop().pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.deck().pool_stock("relic") == deck_relic - 1) # 断点往返三条(基点 1420–1426:relics 快照/计数器键/余库逐枚还原)按主会话裁决②改判集成宿主—— # 驱动代码(save_manager 装配+_on_level_finished+快照取读)随三条同去,集成批落位时重建。 _db.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知) diff --git a/game/tests/unit/core/curio_refresh_paid_test.gd b/game/tests/unit/core/curio_refresh_paid_test.gd index c698890e..e317fc72 100644 --- a/game/tests/unit/core/curio_refresh_paid_test.gd +++ b/game/tests/unit/core/curio_refresh_paid_test.gd @@ -26,24 +26,24 @@ func _init() -> void: # ---- ② 免费位优先序 X→Y(白盒造关卡完成停点——smoke_all ⑫ 同构)---- adr.stop()._pending = {"candidates": _cands(), "payload": {}} Kit.check_quiet(adr.stop().curio_stop_pending() == true, "关卡完成停点在途=谓词 true") - adr.stop()._refresh_x = 1 - adr.stop()._refresh_y = 2 - Kit.check_quiet(adr.stop().refresh_curio() and adr.stop()._refresh_x == 0 and adr.stop()._refresh_y == 2, "X 优先消耗(x1y2→x0y2)") - Kit.check_quiet(adr.stop().refresh_curio() and adr.stop()._refresh_y == 1, "X 穷转 Y 池(y2→y1)") + adr.refresh_bank()._refresh_x = 1 + adr.refresh_bank()._refresh_y = 2 + Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank()._refresh_x == 0 and adr.refresh_bank()._refresh_y == 2, "X 优先消耗(x1y2→x0y2)") + Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank()._refresh_y == 1, "X 穷转 Y 池(y2→y1)") # ---- ③ GP 付费档:计数递增爬价 + 零触碰免费位 ---- - Kit.check_quiet(adr.stop().curio_refresh_gp_cost() == base, "首刷报价=基价(免费位尚余 y1,报价独立于免费位)") - Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.stop()._refresh_y == 1, "付费刷新不触碰免费位(y1 保持)") - Kit.check_quiet(adr.stop().curio_refresh_gp_cost() == int(ceil(float(base) * curve)), "二刷价爬升=ceil(基价×曲线)(已付费刷=1)") - Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.stop().curio_refresh_gp_cost() == int(ceil(float(base) * curve * curve)), "三刷价再爬升=ceil(基价×曲线²)(已付费刷=2)") + Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "首刷报价=基价(免费位尚余 y1,报价独立于免费位)") + Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank()._refresh_y == 1, "付费刷新不触碰免费位(y1 保持)") + Kit.check_quiet(adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve)), "二刷价爬升=ceil(基价×曲线)(已付费刷=1)") + Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve * curve)), "三刷价再爬升=ceil(基价×曲线²)(已付费刷=2)") # ---- ④ 停点重建清零付费计数(对齐商店进店清零——不结转)---- adr.stop().present_node_curio(_cands()) - Kit.check_quiet(adr.stop()._paid_refreshes == 0 and adr.stop().curio_refresh_gp_cost() == base, "节点停点重建=付费计数清零回基价") + Kit.check_quiet(adr.refresh_bank()._paid_refreshes == 0 and adr.refresh_bank().gp_cost() == base, "节点停点重建=付费计数清零回基价") adr.stop().refresh_curio_paid() - Kit.check_quiet(adr.stop()._paid_refreshes == 1, "付费刷新计数在途递增(=1)") + Kit.check_quiet(adr.refresh_bank()._paid_refreshes == 1, "付费刷新计数在途递增(=1)") adr.stop().present_node_curio(_cands()) - Kit.check_quiet(adr.stop().curio_refresh_gp_cost() == base, "再碰三选一付费计数仍清零(同 X 余量不结转口径)") + Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "再碰三选一付费计数仍清零(同 X 余量不结转口径)") # ---- ⑤ 钱包单口支出 spend_gp(LevelRunner——三选一付费档与商店刷新共用同一冒险钱包)---- var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) @@ -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.stop()._refresh_y == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)") + Kit.check_quiet(adr_r.refresh_bank()._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.stop()._refresh_y == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)") + Kit.check_quiet(adr_o.refresh_bank()._refresh_y == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)") quit(Kit.finish("curio_refresh_paid")) diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index 3b0d4927..8c691ae7 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -31,22 +31,22 @@ func _init() -> void: "V2:新冒险 adventure 桶置空(start_adventure 后宿主桶 adventure_counters 为空)〔无状态化 V2〕") # V9-1 刷新券:入存量账(冒险级)——反例=写进 _attune_refresh_x(X 每停点重置会吞存量) - a.stop()._reset_refresh_x() # X 归零基线(模拟新停点) + a.refresh_bank().reset_x() # X 归零基线(模拟新停点) Kit.check_quiet(a.stop()._apply_curio_pick("refresh", "excellent", 1), "刷新券兑付受理〔专长批 V9〕") - Kit.check_quiet(a.stop().refresh_stock() == 1 and a.stop()._refresh_x == 0, + Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank()._refresh_x == 0, "F-1 修复:刷新券入存量账、X 账零写入(写错账反例不复发)〔专长批 V9〕") - a.stop()._reset_refresh_x() # 再碰停点——X 重置但存量不吞 - Kit.check_quiet(a.stop().refresh_stock() == 1 and a.stop().attune_refresh_left() == 1, + a.refresh_bank().reset_x() # 再碰停点——X 重置但存量不吞 + Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank().free_left() == 1, "F-1 修复:停点重置后存量账存活(attune_refresh_left 含第三源)〔专长批 V9〕") # 消耗序:X 优先 → 存量(无 X 无 Y 时吃存量;refresh_curio 前置=在途停点) - a.stop()._refresh_stock = 1 - a.stop()._refresh_y = 0 + a.refresh_bank()._refresh_stock = 1 + a.refresh_bank()._refresh_y = 0 a.stop()._node_pending = {"candidates": [{"kind": "attack", "tier": "common"}]} - Kit.check_quiet(a.stop().refresh_curio() and a.stop().refresh_stock() == 0, + Kit.check_quiet(a.stop().refresh_curio() and a.refresh_bank().refresh_stock() == 0, "刷新消耗序:X/Y 空时吃存量账〔专长批 V9〕") a.stop()._node_pending = {} # 摘牌:选中即消尽(禁早退路径零摘牌) - var deck_refresh_before: int = a.stop().pool_stock("refresh") + var deck_refresh_before: int = a.deck().pool_stock("refresh") Kit.check_quiet(deck_refresh_before == 0 or true, "牌库 refresh 令牌读数在域(投放 %d)" % deck_refresh_before) # V9-2 储备金袋:待注入账 + 战首注入(节点级) @@ -92,7 +92,7 @@ func _init() -> void: # V9-6 全链:凭证令牌兑付=牌库摘牌(选中即消尽)〔拆件批 V2——cs3 凭证拿到即开现役断言承继〕 a._curio.deck = [{"kind": "rap", "tier": "rare"}, {"kind": "attack", "tier": "common"}] a.stop()._pending = {"candidates": a._curio.deck.duplicate()} - Kit.check_quiet(a.stop().pick_curio("rap", "rare") and a.stop().pool_stock("rap") == 0, + Kit.check_quiet(a.stop().pick_curio("rap", "rare") and a.deck().pool_stock("rap") == 0, "凭证令牌:pick_curio 摘牌(选中踢池——与属性件同语义)〔专长批 V9〕") # V10-1 留任 2 件路由与值不变:gp 钱包/heal 当即回血(每次点选独立停点态) @@ -116,7 +116,7 @@ func _init() -> void: var sm = load("res://core/save_manager.gd").new("user://_feat_vouchers_bp.json") sm.load_from_disk() a.adventure_counters = {"pain_layers": 3, "adv_twin_digs": 5} # 冒险桶宿主字典直写(测试构造面) - a.stop()._refresh_stock = 2 + a.refresh_bank()._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() @@ -139,7 +139,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.adventure_counters.get("pain_layers", -1)) == 3 and b2.stop().refresh_stock() == 2, + Kit.check_quiet(int(b2.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_bank().refresh_stock() == 2, "V16:续玩回填逐项一致(adventure 桶随断点往返·计数恢复)〔无状态化 V3〕") # V25 完美保险专长验收组(R2 全项:装载/限次一次/计数纯净/两账相减/跨战清零/事件不吞) var fi: Dictionary = db.feats.row_by_id("perfect_insurance") diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index d69add19..1e8d830b 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -1357,13 +1357,13 @@ func _smoke_shop_pool() -> void: attack_seen = true break Kit.check("库存尽剔牌库(attack 令牌耗尽——池消耗即上限跨渠道共库 reward→shop 向)", not attack_seen) - var stock_before: int = a8.stop().pool_stock("attack") - Kit.check("池空消费拒绝(handler 零副作用——买光即枯竭硬闸)", not a8.stop().consume_pool_purchase("attack", "common") and a8.stop().pool_stock("attack") == stock_before) + var stock_before: int = a8.deck().pool_stock("attack") + Kit.check("池空消费拒绝(handler 零副作用——买光即枯竭硬闸)", not a8.stop().consume_pool_purchase("attack", "common") and a8.deck().pool_stock("attack") == stock_before) # ⑥ 跨渠道共库 shop→reward 向:回填一枚 attack 令牌 → 消费口受理 → 奖励牌库同步摘牌 a8._curio.deck.append({"kind": "attack", "tier": "common"}) Kit.check("商店消费口受理(停点件兑付单点同口——真单点库)〔拆件批 V3〕", a8.stop().consume_pool_purchase("attack", "common")) var reward_attack_seen := false - for t6 in a8.stop().draw_candidates(): + for t6 in a8.deck().draw_candidates(): if String((t6 as Dictionary).get("kind", "")) == "attack": reward_attack_seen = true break diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index 39960be5..4755425a 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -290,17 +290,17 @@ func _meta_settle_and_workshop() -> void: Kit.check("充能按装备 active_charged 件种行(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面;读数=行派生)", a12._loadout.charges_left("incense_stick", a12.runner().treasure_inventory) == 1 and a12._loadout.charges_left("sky_eye", a12.runner().treasure_inventory) == 1) a12._attunement = [] - a12.stop()._refresh_x = 1 - a12.stop()._refresh_y = 2 + a12.refresh_bank()._refresh_x = 1 + a12.refresh_bank()._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.stop().refresh_curio() and a12.stop()._refresh_x == 0 and a12.stop()._refresh_y == 2) + a12.stop().refresh_curio() and a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank()._refresh_y == 2) Kit.check("刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)", - a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.stop()._refresh_y == 0 + a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank()._refresh_y == 0 and a12.stop().refresh_curio() == false) - a12.stop()._reset_refresh_x() + a12.refresh_bank().reset_x() Kit.check("刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕", - a12.stop()._refresh_x == 0 and a12.stop().attune_refresh_left() == 0) + a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank().free_left() == 0) # ⑬ 主菜单开局传参面(开新/续玩两入口同调三键随开局下传防回归:真实入口曾 # 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params # 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源) diff --git a/game/tests/unit/core/pool_case_d_test.gd b/game/tests/unit/core/pool_case_d_test.gd index df25397f..c7768486 100644 --- a/game/tests/unit/core/pool_case_d_test.gd +++ b/game/tests/unit/core/pool_case_d_test.gd @@ -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.stop().draw_candidates("reward_node") + var cands: Array = a.deck().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.stop().draw_candidates("reward_node") + var cands2: Array = a.deck().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.stop().draw_candidates("reward_node"): + for t in a.deck().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.stop().draw_candidates("reward_node").is_empty(), "池空=零候选合法态(skip_empty_curio 兜底入口)〔专长批 V7〕") + Kit.check_quiet(a.deck().draw_candidates("reward_node").is_empty(), "池空=零候选合法态(skip_empty_curio 兜底入口)〔专长批 V7〕") func check_v6(b, db) -> void: @@ -115,22 +115,22 @@ func check_v6(b, db) -> void: return # 拿了消耗:直置停点候选后 pick_curio——牌库摘牌单点 b._curio.deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")] - var gp_before: int = b.stop().pool_stock("gp") + var gp_before: int = b.deck().pool_stock("gp") b.stop()._pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")]} - Kit.check_quiet(b.stop().pick_curio("gp", "common") and b.stop().pool_stock("gp") == gp_before - 1, + Kit.check_quiet(b.stop().pick_curio("gp", "common") and b.deck().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.stop().draw_candidates("reward_node") + var cands: Array = b.deck().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.stop().draw_candidates("reward_node") + b.deck().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.stop().draw_candidates("level_clear"): + for t in m.deck().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) @@ -163,13 +163,13 @@ func check_v8(c, db) -> void: # 概率位与 cap 两属性独立:改 cap 不动牌库(概率位)、改牌库不动 cap 字段 var cap_field_before := int(db.adventure.curio_cap_gp) db.adventure.curio_cap_gp = 1 # 独立设 cap(概率位=gp 令牌数不动) - var gp_tokens: int = c.stop().pool_stock("gp") + var gp_tokens: int = c.deck().pool_stock("gp") Kit.check_quiet(gp_tokens > 0 and int(db.adventure.curio_value("gp", "common")) == 20, "改 cap 不动概率位:牌库 gp 令牌 %d 枚在库、件值字段不受影响〔专长批 V8〕" % gp_tokens) # cap 只在拿了才计:刷出不计 c._curio.deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")] for i in 20: - c.stop().draw_candidates("reward_node") + c.deck().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 满→提前退役(候选视图剔除、牌库本体不动) @@ -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.stop().draw_candidates("reward_node"): + for t in c.deck().draw_candidates("reward_node"): if String(t.get("kind", "")) == "gp": gp_cand = true Kit.check_quiet(gp_in_deck >= 0 and not gp_cand, diff --git a/game/tests/unit/core/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd index c7614fc2..060aa522 100644 --- a/game/tests/unit/core/treasure_effects_test.gd +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -117,9 +117,9 @@ func _init() -> void: adr5.set_save_manager(save_s.new("user://treasure_effects_p.json")) Kit.check_quiet(adr5.start_adventure(261, {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["refresh_practice"]}), "刷新练习:装配冒险启动") - Kit.check_quiet(int(adr5.stop().refresh_stock()) == 1 and int(adr5.stop().attune_refresh_left()) == 1, + Kit.check_quiet(int(adr5.refresh_bank().refresh_stock()) == 1 and int(adr5.refresh_bank().free_left()) == 1, "刷新练习:存量账 +1(装配即得一次性——attune_refresh_left 合计含 stock)") - Kit.check_quiet(int(adr5.stop()._refresh_x) == 0, + Kit.check_quiet(int(adr5.refresh_bank()._refresh_x) == 0, "刷新练习反例:临时账 X 不动(写临时账/重复加仓=违规形态)") # ⑥ 储备罐(转存率 125=1.25:1 整除口径 / 100=1:1 逐位同旧)