## 三选一停点件:停点编排唯一所有者(冒险域拆件批自 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 # 已兑付态重发(宿主 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 分支——宿主置关序位后由本件承载载荷) 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)))))