feat(凭证件裁削): 卡② 同调电池删除+完美保险迁专长+配额净删落地(迭代 31 卡②)

用户 2026-10-18 逐裁四项落码:
- T1/T2 同调电池整件删除:CURIO_KINDS 摘 kind、三字段与三处分发表分支删净、
  runner 侧 _battery_pending/断点键/getter/choose_battery_target 全清(呈现层零接线=有账本无入口)。
- T3 完美保险出池:kind/字段/分发摘除,_pending_insurance 与 level_runner 注入通道
  (voucher_insurance 键、set_voucher_grants 第三参)整链拆除,battle_orchestrator._fx_insurance 删。
- T4 引擎扩签 forgive_seal_miss(EFFECT_KINDS 31→32)+两账相减:新增 _perfect_forgiven 单向累加、
  谓词抽成 perfect_sweep_earned()(有效误标=误标总数−豁免数),_perfect_hit_false 保持纯净=契约 2;
  seal_missed 发射时机与消费点零改动=契约 3(危险直觉·守同事件订阅面在验收组里对拍)。
- T5 feats.tres 第 30 行完美保险专长:rarity rare/cap 省略/domain=battle 显式写出(战斗域首例)/
  desc「本场首次封印落空,不计入完美封印判定」;限次由行 when counter(insurance_used) lt 1 承载,
  do 自带 counter_id 记账由授予段消费点落账(代拍板 3 选型:单行落表,理由见实现报告)。
- T6 配额净删:TIER_QUOTAS rare 9→7、令牌池总量 104→102,validate() 硬断言过;smoke 配额锚/牌库
  构成/专长行数随批换代。
- T7 验收:feat_vouchers_test 39 项全绿(出池反例/断点缺键兼容/账本保留/配额锚/装载/限次一次/
  计数纯净/两账相减真值表/跨战清零/事件不吞)+forge_table 词表闭集断言 31→32。
  变异验证 6 条:5 条证红;「domain battle→adventure」在本 rig 不可观测(未红),已如实登记。
- 存量缺陷顺带修:旧 _emit_row("insurance_used","",…) 空来源会触发行来源断言(旧路径无消费点故从未走红),
  新订阅面给出 source_class=seal_miss。

基线(卡① 合回后 master 4f953ce8):冒烟 1151/0、单元 34 套全绿、探针 --click 全绿(user:// 存档移开态)。
This commit is contained in:
DKChan committed 2026-10-19 04:45:49 +08:00
1 parent 4f953ce829
commit 072c19b4a5
10 files changed
+183 -110

No files matched your search

+1 -39
View File
@@ -52,9 +52,7 @@ var _curio_paid_refreshes := 0 # 本次停点已付费刷新次数(
# ---- 凭证件三本新账(迭代 20 T4——奇物.md 凭证件表新 5 件;F-1 刷新写错账修复承载面)----
var _refresh_stock := 0 # 刷新存量账(冒险级——刷新券加仓;**不经 _attune_refresh_x**:X 每停点重置会吞存量=旧写错账缺陷 F-1)
var _pending_rap := 0 # 储备金袋待注入(节点级——下一战 RAP +N,战首注入后清零)
var _battery_pending := 0 # 同调电池待分配充能(冒险级——choose_battery_target 玩家点选消费,禁自动替选)
var _time_sand_queue := 0 # 时沙待注入(回合级——下一战首轮 AP +N,战首入队后清零)
var _pending_insurance := 0 # 完美保险待注入(节点级——下一战标错豁免 N 次,战首注入后清零;战内未用即过期)
var _save_manager = null # 断点承载注入(迭代 8 卡①——null = 不写断点;模拟/冒烟缺省)
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
# ---- 三选一停点态(迭代 10 奖励重构批)----
@@ -210,7 +208,6 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
run_ledger.adventure_counters = (bp_counters as Dictionary).duplicate(true)
_refresh_stock = maxi(0, int(bp.get("refresh_stock", 0))) # 迭代 20 T4/T7——旧档缺键缺省 0
run_ledger.vision_counter = {"used": maxi(0, int(bp.get("vision_counter_used", 0)))} # 迭代 22 秘宝批 T3——真视计数随断点回填(缺键=旧断点缺省 0)
_battery_pending = maxi(0, int(bp.get("battery_pending", 0)))
var bp_result: Dictionary[Variant, Variant] = bp.get("bonus_result", {}) as Dictionary
if not bp_pending.is_empty():
_pending_next_level = true
@@ -258,9 +255,7 @@ func _reset_curio_state() -> void:
run_ledger.adventure_counters = {}
_refresh_stock = 0
_pending_rap = 0
_battery_pending = 0
_time_sand_queue = 0
_pending_insurance = 0
# 令牌池构建(TP0 v2.2——按表 kind×tier 配额展开为持久多重集;构建序确定 = CURIO_KINDS×TIERS 逐层,
@@ -412,18 +407,10 @@ func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String =
_pending_rap += value # T4——储备金袋:下一战 RAP +N(节点级,战首注入后清零)
_curio_taken["rap"] = int(_curio_taken.get("rap", 0)) + 1
return true
if kind == "attune_battery":
_battery_pending += value # T4——同调电池:待分配充能(choose_battery_target 玩家点选消费——禁自动替选)
_curio_taken["attune_battery"] = int(_curio_taken.get("attune_battery", 0)) + 1
return true
if kind == "time_sand":
_time_sand_queue += value # T4——时沙:下一战首轮 AP +N(回合级池增量,不改上限)
_curio_taken["time_sand"] = int(_curio_taken.get("time_sand", 0)) + 1
return true
if kind == "perfect_insurance":
_pending_insurance += value # T4——完美保险:下一战标错豁免 N 次(节点级,未用即过期)
_curio_taken["perfect_insurance"] = int(_curio_taken.get("perfect_insurance", 0)) + 1
return true
if kind == "relic":
if not _feats.has(feat_id):
_feats.append(feat_id)
@@ -593,29 +580,6 @@ func refresh_stock() -> int:
return _refresh_stock
# 同调电池充能分配(T4——玩家点选哪件已装配主动秘宝 +N;**禁自动替选**:
# 无待分配/件不在装配集/非主动充能件=拒绝零副作用)
func choose_battery_target(item_id: String) -> bool:
if _battery_pending <= 0:
push_error("[冒险结构] 电池分配拒绝——无待分配充能")
return false
if not _attunement.has(item_id):
push_error("[冒险结构] 电池分配拒绝——件不在装配集: %s" % item_id)
return false
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(item_id)
if String(arow.get("form", "")) != "active_charged":
push_error("[冒险结构] 电池分配拒绝——非主动充能件: %s" % item_id)
return false
_battery_pending -= 1
run_ledger.attune_charges[item_id] = int(run_ledger.attune_charges.get(item_id, 0)) + 1
return true
# 待分配充能读数(呈现层电池选件面数据源)
func battery_pending() -> int:
return _battery_pending
# 节点停点在途态读回(组合根重 present 用——candidates 值拷贝)
func node_curio_pending_state() -> Dictionary:
return {
@@ -917,10 +881,9 @@ func _start_current_level() -> bool:
# 迭代 20 T2——cap 检查回调四参注入)
_runner.set_shop_pool(_curio_deck, _consume_pool_purchase, _feat_ticket_consider, _curio_cap_ok)
# 迭代 20 T4——凭证件三账注入(节点级/回合级一次性:注入即清零,未用即过期=寿命档语义)
_runner.set_voucher_grants(_pending_rap, _time_sand_queue, _pending_insurance)
_runner.set_voucher_grants(_pending_rap, _time_sand_queue)
_pending_rap = 0
_time_sand_queue = 0
_pending_insurance = 0
# 三选一道具件跨关携带注入(迭代 10)——迭代 11 第①批 T7 退役:aux 卡出池后 carry 恒空集,
# 注入面随停点渠道退役拆除(容器字段保留=替换子交互机械惰性承载,零投放零行为)
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
@@ -1073,7 +1036,6 @@ func _snapshot_breakpoint() -> void:
"vision_counter_used": int(run_ledger.vision_counter.get("used", 0)), # 迭代 22 秘宝批 T3——真视使用计数随断点(续玩跨战账不丢)
"refresh_stock": _refresh_stock, # 迭代 20 T4/T7——刷新存量账随断点(F-1 新账)
"attune_refresh_y": _attune_refresh_y, # 2026-10-13 缺陷修批——Y 冒险周期池余量随断点(续玩不再自参数面整额重注入=堵「用完退菜单再续玩满血复活」可利用漏洞;扩列向后兼容——旧档缺键回退参数面)
"battery_pending": _battery_pending, # 迭代 20 T4/T7——电池待分配充能随断点
"bonus_result": _curio_result.duplicate(true), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
"keystone_drops": _keystone_drops.duplicate(true), # 逐杀掉落明细(摘要归因续接——迭代 10)
}
+10 -4
View File
@@ -78,7 +78,6 @@ var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} #
var _fx_new_resource_types := {} # 本战已见资源类型集(地质学家 fact new_resource_type 判定——battle 域)
var _fx_pending_reveals: Array[Dictionary] = [] # 揭示效果待兑队列(见血开窍/斩首学者/地质学家/跳格采掘——授予段末统一兑付,防轮 facts 迭代中追加重入)
var _fx_revealing := false # fx 揭示兑付中标记(_on_cell_revealed 缓冲旁路——fx 揭示的奖励由兑付点直接入账,防二次缓冲二次兑付)
var _fx_insurance := 0 # 完美保险余量(迭代 20 T4——本战标错豁免次数;消费于 seal_miss 首见判定)
# 特性运行态(内容数据驱动扩展批 T3——relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经
# 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动)
var _feat_first_dig_used := false
@@ -265,6 +264,7 @@ func attune_convert_state() -> Dictionary:
var _seal_miss_seen := {} # 封印未中行去重(每安全格实例一次)
var _perfect_hit_true := 0 # 板级完美排雷计数(install 重置)
var _perfect_hit_false := 0
var _perfect_forgiven := 0 # 完美保险豁免累计(迭代 31 卡②——单向累加自有账;契约 2 禁改写 _perfect_hit_false)
var _hero_atk := 0 # 主角攻击基值快照(encounter.hero_atk;E4 收口——表值运行期不可变)
@@ -415,11 +415,10 @@ func begin_battle(enc: Dictionary) -> bool:
var ap_bonus: int = int(enc.get("hero_ap_bonus", 0)) + int(enc.get("voucher_time_sand", 0)) # 迭代 20 T4——时沙同通道(首轮池增量,不改上限)
if ap_bonus > 0:
action_economy.queue_ap_delta(ap_bonus)
# 迭代 20 T4——储备金袋(节点级 RAP 注入)与完美保险(标错豁免次数)接收
# 迭代 20 T4——储备金袋(节点级 RAP 注入)接收(完美保险注入通道随迭代 31 卡② 拆除——改由 feats.tres 订阅承载)
var voucher_rap: int = int(enc.get("voucher_rap", 0))
if voucher_rap > 0:
action_economy.grant_reserve_ap(voucher_rap)
_fx_insurance = int(enc.get("voucher_insurance", 0))
# 工兵晨令直调拆除(内容数据驱动扩展批 T4——round_start grant_ap(mode:queue) 订阅行承载,
# when round_index ≤ effective_rounds 行字段 value_from 定影;发射位先于轮切换=当轮兑现)
# round_start 发射+施加(内容数据驱动扩展批 T3——发射位先于行动经济轮切换:树规则 grant_ap
@@ -470,6 +469,7 @@ func _install_new_board(during_round := false) -> bool:
_seal_miss_seen.clear()
_perfect_hit_true = 0 # 板级完美计数随 install 重置
_perfect_hit_false = 0
_perfect_forgiven = 0 # 豁免累计随 install 重置(domain=battle 每战清零)
_board_from_midround_refresh = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false
_board_seq += 1
board_changed.emit()
@@ -916,7 +916,7 @@ func _grant_clear_reward(ri: int) -> void:
var snap = _reward_economy.grant_clear()
if not (snap is Dictionary):
return
var perfect: bool = _perfect_hit_false == 0 and _perfect_hit_true >= 1
var perfect: bool = perfect_sweep_earned()
var first_ok := _grant_clear_identity(snap, ri, "(清图奖励)")
var second_ok := false
if perfect and first_ok:
@@ -1291,3 +1291,9 @@ func _fatal(msg: String) -> void:
push_error("[战斗结算/fatal] " + msg)
battle_result = "fatal_error" # release 构建剥离 assert 后的唯一阻断面——受理/结算/刷新入口全查 battle_result == ""
assert(false, msg)
## 完美排雷谓词(迭代 31 卡② T4——两账相减:有效误标=误标总数−豁免数)
## 契约 2:_perfect_hit_false 保持纯净,豁免只累加 _perfect_forgiven,禁回退式改写与拦截式吞事件。
func perfect_sweep_earned() -> bool:
return _perfect_hit_false - _perfect_forgiven <= 0 and _perfect_hit_true >= 1
+4 -1
View File
@@ -38,7 +38,10 @@ const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open
# 迭代 22 秘宝实装批 T3 扩签一型:mine_count_preview=危险格数量预告(巢穴预告——board_installed
# 订阅 do;只报数量不标位置、不碰危险格分布=棋盘事实不可变契约;参数面常时 kind 不入本词表——
# 它们无订阅行,schema 行内 effect_kinds 校验走 treasure_item_table 自有词表)
"mine_count_preview"]
"mine_count_preview",
# 迭代 31 卡② 凭证件裁削批 T4 扩签一型:forgive_seal_miss=完美保险(本场首次封印落空不计入完美
# 封印判定——订阅 seal_missed 的豁免效果,消费点在授予段;feats.tres 首例 domain=battle 行)
"forgive_seal_miss"]
# 注:rewrite_board 已除名(迭代 20 T9——净化工兵随老 27 件退役,V14 连带清理;
# 编排器消费分支 T1 先行摘除)
+1 -5
View File
@@ -475,13 +475,11 @@ func _start_battle(node: Dictionary) -> bool:
"battle_index": _battle_index(),
"voucher_rap": _voucher_rap,
"voucher_time_sand": _voucher_time_sand,
"voucher_insurance": _voucher_insurance,
"node_type": String(node.get("type", ""))}):
push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
return false
_voucher_rap = 0 # 迭代 20 T4——节点级一次性:注入即消费(战斗未启动则保留至下次注入前不清)
_voucher_time_sand = 0
_voucher_insurance = 0
return true
@@ -643,13 +641,11 @@ func set_run_ledger(l: RunLedger) -> void:
# battle begin 消费后归零;缺省 0=零扰动)
var _voucher_rap := 0
var _voucher_time_sand := 0
var _voucher_insurance := 0
func set_voucher_grants(rap: int, time_sand: int, insurance: int) -> void:
func set_voucher_grants(rap: int, time_sand: int) -> void:
_voucher_rap = maxi(0, rap)
_voucher_time_sand = maxi(0, time_sand)
_voucher_insurance = maxi(0, insurance)
# 同调主动件充能账骑库存面注入(迭代 11 第①批 T6 原语义 + 迭代 27 T3 换代——账本住
+7 -6
View File
@@ -296,12 +296,6 @@ func _judge_seal(evt: Dictionary, ri: int) -> void:
if oc._seal_miss_seen.has(cell):
return # 每格实例一次去重(归因行与负向兑付同键——防逐轮惩罚轰炸)
oc._seal_miss_seen[cell] = true
# 完美保险(迭代 20 T4——本战下一次标错不破坏完美资格:_perfect_hit_false 回退=本次标错
# 对完美谓词无痕;只保一次/件,节点级用完即止)
if oc._fx_insurance > 0:
oc._fx_insurance -= 1
oc._perfect_hit_false -= 1
oc._emit_row("insurance_used", "", cell, ri, "完美保险·标错豁免", 0, {"left": oc._fx_insurance}, "")
var hl := ""
if oc._perfect_hit_false == 1:
hl = "perfect_broken" # 本板完美谓词已破的归因锚
@@ -314,6 +308,13 @@ func _judge_seal(evt: Dictionary, ri: int) -> void:
for h in oc._fx_emit("seal_missed", {"cell": cell, "round": ri}):
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
oc.action_economy.queue_ap_delta(int(h.get("value", 0)))
elif String(h.get("kind", "")) == "forgive_seal_miss":
# 完美保险(迭代 31 卡② T4——订阅式豁免:本战首次标错计一次豁免,只累加自有账
# _perfect_forgiven,绝不回写 _perfect_hit_false=契约 2 计数纯净;发射时机与消费点零改动=契约 3)
oc._perfect_forgiven += 1
_apply_counter_hit(h) # 豁免同时记账(insurance_used battle 域——下次标错 when lt 1 不再命中)
oc._emit_row("insurance_used", "seal_miss", cell, ri, "完美保险·标错豁免", 0,
{"forgiven": oc._perfect_forgiven}, "")
var snap = oc._reward_economy.grant(cell)
if not (snap is Dictionary):
return
+9 -18
View File
@@ -16,10 +16,12 @@ extends Resource
## 类内异质性:GP与回复是即时兑付,不进持有集、不持续生效。)
## 三选一提升池 kind 词表(封闭——断点附随件与牌库剔除态依赖它;扩件走本表+registry 修订)。
## treasure_item 池种退役(迭代 11 第①批 T7——同调批出池);heal 恢复件新入池(v2.2 令牌池追认 b)。
## 迭代 20 T4——资源凭证五件入池:rap 储备金袋/attune_battery 同调电池/time_sand 时沙/
## perfect_insurance 完美保险(配额暂 0——T4 落路由同批给投放数,防无路由 kind 出候选)。
## 迭代 20 T4 资源凭证入池五件;迭代 31 卡② 凭证件裁削批(用户 2026-10-18 逐裁)两件出池:
## 同调电池整件删除(呈现层零接线、玩家无分配途径——有 API 有账本唯独没有入口);
## 完美保险整件迁出资源类归专长(它给的是规则豁免而非可调动的余量——判据见奇物.md §1.1,
## 落表 feats.tres 第 30 行,效果类型 forgive_seal_miss)。留任 rap 储备金袋/time_sand 时沙。
const CURIO_KINDS: Array[String] = ["attack", "max_hp", "armor", "gp", "ap", "heal", "refresh",
"rap", "attune_battery", "time_sand", "perfect_insurance"] # T6 扩第七类(refresh=资源类成员——作为奇物授予的三选一重抽次数,作用对象=三选一界面非角色;见 registry refresh_boundary)
"rap", "time_sand"] # T6 扩第七类(refresh=资源类成员——作为奇物授予的三选一重抽次数,作用对象=三选一界面非角色;见 registry refresh_boundary)
## 令牌档位词表(迭代 20 T2 换代——跨层四级稀有度框架〔01-核心循环 v1.5 档名映射:普通→普通、
## 稀有→优秀、罕见→稀有、传说=新增头奖位〕;1 级最稀有=legendary 优先)
const TIERS: Array[String] = ["common", "excellent", "rare", "legendary"]
@@ -28,10 +30,11 @@ const TIER_EXCELLENT := "excellent"
const TIER_RARE := "rare"
const TIER_LEGENDARY := "legendary"
## 档位配额结构锚(迭代 20 换代——T2 四级直映射 70/25/3/0;T4 凭证四件入稀有档投放 +6
## (rap 2/battery 1/时沙 2/保险 1)→ 稀有 9、总量 104。已裁换代授权内的 provisional 投放
## (rap 2/battery 1/时沙 2/保险 1)→ 稀有 9、总量 104;迭代 31 卡② 净删两件投放(battery 1/保险 1)
## → 稀有 7、总量 102(用户裁「净删」,禁把令牌转给别件保住总量)。已裁换代授权内的 provisional 投放
## (卡文 T2「四级配额表重构」+Q3 单迭代全量),偏离 GDD 占位记录随代拍板追认单呈报;
## validate() 硬断言;此后改总量=重裁非调参)。
const TIER_QUOTAS := {"common": 70, "excellent": 25, "rare": 9, "legendary": 0}
const TIER_QUOTAS := {"common": 70, "excellent": 25, "rare": 7, "legendary": 0}
# —— 通关部分回血(固定型,非末关通关兑付)——
@export var clear_heal_pct: int = 20 # [0,100] knob——回复生效上限的百分比,封顶上限不回满
@@ -56,9 +59,7 @@ const TIER_QUOTAS := {"common": 70, "excellent": 25, "rare": 9, "legendary": 0}
@export var curio_quota_refresh_excellent: int = 2 # [0,99]——刷新次数件(T6 新入池资源类成员;provisional 归曲线调参批)
@export var curio_quota_ap_rare: int = 3 # [0,99]——AP 件(稀有档;旧罕见档物理承袭)
@export var curio_quota_rap_rare: int = 2 # [0,99]——储备金袋(T4 投放 provisional 归调参批)
@export var curio_quota_attune_battery_rare: int = 1 # [0,99]——同调电池(T4 投放 provisional 归调参批)
@export var curio_quota_time_sand_rare: int = 2 # [0,99]——时沙(T4 投放 provisional 归调参批)
@export var curio_quota_perfect_insurance_rare: int = 1 # [0,99]——完美保险(T4 投放 provisional 归调参批)
# —— 令牌件值(小/大数值对——v2.2 追认 b 定稿:20/50、+1/+2、+1/+2、+5/+10、恢复+6、AP+1)——
@export var curio_value_attack_common: int = 1 # [0,999] knob——小增伤
@@ -73,9 +74,7 @@ const TIER_QUOTAS := {"common": 70, "excellent": 25, "rare": 9, "legendary": 0}
@export var curio_value_refresh_excellent: int = 1 # [0,9] knob(provisional)——刷新次数件兑付值(三选一重抽次数 +N——_attune_refresh_x 增量,不进加算区)
@export var curio_value_ap_rare: int = 1 # [0,10] knob——AP 件每场战斗首轮行动点基线增量
@export var curio_value_rap_rare: int = 3 # [0,99] knob(provisional)——储备金袋 RAP +N
@export var curio_value_attune_battery_rare: int = 1 # [0,9] knob(provisional)——同调电池充能 +N
@export var curio_value_time_sand_rare: int = 2 # [0,99] knob(provisional)——时沙 下轮 AP +N
@export var curio_value_perfect_insurance_rare: int = 1 # [0,9] knob(provisional)——完美保险 资格保护次数
# —— 已拿上限 cap(迭代 20 T2 概率与上限解耦〔奇物.md 池结构案D〕——案②令牌池+提前退役:
# 概率位=档+令牌数〔牌库物理张数〕、上限=本字段〔拿了才计、刷出不计〕;cap 满后该 kind 从候选
@@ -88,9 +87,7 @@ const TIER_QUOTAS := {"common": 70, "excellent": 25, "rare": 9, "legendary": 0}
@export var curio_cap_heal: int = 99 # [1,99]
@export var curio_cap_refresh: int = 99 # [1,99]
@export var curio_cap_rap: int = 99 # [1,99]
@export var curio_cap_attune_battery: int = 99 # [1,99]
@export var curio_cap_time_sand: int = 99 # [1,99]
@export var curio_cap_perfect_insurance: int = 99 # [1,99]
# —— 专长获取门票(迭代 15 卡② T5——非 Boss 渠道的专长入口形态,奇物.md v2.7 渠道轴定位;
# 四口径契约④:拿到即开不可囤积/罕见只在概率不在价格/一局限购一次/计步保底连续 5 展示未出
@@ -135,9 +132,7 @@ func curio_quota(kind: String, tier: String) -> int:
["refresh", TIER_EXCELLENT]: return curio_quota_refresh_excellent
["ap", TIER_RARE]: return curio_quota_ap_rare
["rap", TIER_RARE]: return curio_quota_rap_rare
["attune_battery", TIER_RARE]: return curio_quota_attune_battery_rare
["time_sand", TIER_RARE]: return curio_quota_time_sand_rare
["perfect_insurance", TIER_RARE]: return curio_quota_perfect_insurance_rare
return 0
@@ -156,9 +151,7 @@ func curio_value(kind: String, tier: String = TIER_COMMON) -> int:
["refresh", TIER_EXCELLENT]: return curio_value_refresh_excellent
["ap", TIER_RARE]: return curio_value_ap_rare
["rap", TIER_RARE]: return curio_value_rap_rare
["attune_battery", TIER_RARE]: return curio_value_attune_battery_rare
["time_sand", TIER_RARE]: return curio_value_time_sand_rare
["perfect_insurance", TIER_RARE]: return curio_value_perfect_insurance_rare
return 0
@@ -173,9 +166,7 @@ func curio_cap(kind: String) -> int:
"heal": return curio_cap_heal
"refresh": return curio_cap_refresh
"rap": return curio_cap_rap
"attune_battery": return curio_cap_attune_battery
"time_sand": return curio_cap_time_sand
"perfect_insurance": return curio_cap_perfect_insurance
return 99
@@ -236,7 +227,7 @@ func validate() -> Array:
for tier in TIERS:
var total := tier_total(tier)
if total != int(TIER_QUOTAS.get(tier, -1)):
errs.append({"category": "domain", "message": "档位配额结构失锚(四级锚 70/25/3/0——%s 应=%d 实=%d;改总量须重裁)" % [tier, int(TIER_QUOTAS.get(tier, -1)), total]})
errs.append({"category": "domain", "message": "档位配额结构失锚(四级锚 70/25/7/0——%s 应=%d 实=%d;改总量须重裁)" % [tier, int(TIER_QUOTAS.get(tier, -1)), total]})
if curio_candidate_count < 1 or curio_candidate_count > 6:
errs.append({"category": "domain", "message": "curio_candidate_count 越域 [1,6]: %d" % curio_candidate_count})
return errs
+29
View File
@@ -11,6 +11,10 @@ script = ExtResource("1")
; (契约 6 命名纪律:旗〔旧称〕=封印、危险格=陷阱/巢穴风味层)。dispatch 预评估口径注记:「每第 N 次/积
; 满触发」系 when 于命中前读计数器现值——行 when 按 N-1 落值(双管镐 mod_eq ref 1/地质学家 eq 3/
; 跳格 eq 2/封印匠 mod_eq ref 2/保险丝 eq 1——effect_words_t1_test mod_eq 断言钉住该口径)。
; 迭代 31 卡② 凭证件裁削批 T5——完美保险自资源类迁入专长表(第 30 行):rarity rare/cap 省略
; (承 twin_pick/seal_burst 惯例)/domain=battle 显式写出(现役 29 行全为 adventure 或不写,本行
; 系战斗域首例——豁免计数每战清零)。限次落法=单订阅行:when counter(insurance_used) lt 1 承载
; 「本场首次」,do 自带 counter_id 记账由授予段消费点落账(dispatch 不代执行计数器)。
rows = [
{
"feat_id": "twin_pick",
@@ -849,5 +853,30 @@ rows = [
}
}
]
},
{
"feat_id": "perfect_insurance",
"display_name": "完美保险",
"rarity": "rare",
"desc": "本场首次封印落空,不计入完美封印判定",
"triggers": [
{
"on": "seal_missed",
"when": [
{
"fact": "counter",
"id": "insurance_used",
"op": "lt",
"value": 1
}
],
"do": {
"kind": "forgive_seal_miss",
"counter_id": "insurance_used",
"value": 1,
"domain": "battle"
}
}
]
}
]
+14 -14
View File
@@ -800,8 +800,8 @@ func _smoke_adventure_table() -> void:
"clear_heal_pct ∈ [0,100](%d——通关部分回血比例)" % int(_db.adventure.clear_heal_pct))
for tier in _db.adventure.TIERS:
check(int(_db.adventure.tier_total(tier)) == int(_db.adventure.TIER_QUOTAS[tier]),
"档位配额结构锚(四级 70/25/3/0 直映射承袭——迭代 20 T2):%s = %d 张" % [tier, int(_db.adventure.TIER_QUOTAS[tier])])
check(int(_db.adventure.deck_total()) == 104, "令牌池总量 = 104 张(四级 普通 70/优秀 25/稀有 9/传说 0——T2 直映射 98+T4 凭证投放 6)〔专长批 V5〕")
"档位配额结构锚(四级 70/25/7/0——迭代 31 卡② 净删两件投放):%s = %d 张" % [tier, int(_db.adventure.TIER_QUOTAS[tier])])
check(int(_db.adventure.deck_total()) == 102, "令牌池总量 = 102 张(四级 普通 70/优秀 25/稀有 7/传说 0——迭代 31 卡② 净删两件投放 104→102)〔凭证裁削批 V5〕")
check(int(_db.adventure.curio_value("gp", "common")) == 20 and int(_db.adventure.curio_value("gp", "excellent")) == 50
and int(_db.adventure.curio_value("attack", "common")) == 1 and int(_db.adventure.curio_value("attack", "excellent")) == 2
and int(_db.adventure.curio_value("max_hp", "common")) == 5 and int(_db.adventure.curio_value("max_hp", "excellent")) == 10
@@ -1280,8 +1280,8 @@ func _smoke_adventure() -> void:
func _smoke_feats() -> void:
print("--- 专长重设计实装(feat_table 四级 · 专长带池接线 · 订阅面逐件)---")
# ① 现行表牌库构成:重设计 29 件全量入池——98 数值令牌+29 relic 令牌(四级 rarity 直映射)
check(_db.feats != null and (_db.feats.rows as Array).size() == 29, "feat_table 已加载(重设计 29 件=基础 27+迁入 2——卡文 §10.4 推荐案口径)")
# ① 现行表牌库构成:重设计 30 件全量入池——102 数值令牌+30 relic 令牌(四级 rarity 直映射;迭代 31 卡② 完美保险迁入)
check(_db.feats != null and (_db.feats.rows as Array).size() == 30, "feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)")
check(_db.feats.validate().is_empty(), "feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)")
var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71))
check(a_r0.start_adventure(71, {"first_script": true}), "专长带场景:冒险启动(seed 71)")
@@ -1292,8 +1292,8 @@ func _smoke_feats() -> void:
feat_tokens += 1
var tk := String((t as Dictionary).get("tier", ""))
tier_counts[tk] = int(tier_counts.get(tk, 0)) + 1
check(feat_tokens == 29 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 8 and a_r0._curio_deck.size() == 133,
"专长带入池:29 件四级直映射(普通 12/优秀 9/稀有 8/传说 0)——牌库 104+29=133〔专长批 V2〕")
check(feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0._curio_deck.size() == 132,
"专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕")
# ② 旧 27 件退役钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归批末静态核查)
check(_db.feats.row_by_id("arcane_ember").is_empty() and _db.feats.row_by_id("magma_body").is_empty(),
"旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕")
@@ -1311,14 +1311,14 @@ func _smoke_feats() -> void:
for tv in ((row_v as Dictionary).get("triggers", []) as Array):
if String(((tv as Dictionary).get("do", {}) as Dictionary).get("kind", "")) == "rewrite_board":
rewrite_rows += 1
check(rewrite_rows == 0, "棋盘事实不可变:新 29 行零棋盘改写类订阅(rewrite_board 已除名消费面)〔专长批 V19〕")
check(rewrite_rows == 0, "棋盘事实不可变:新 30 行零棋盘改写类订阅(rewrite_board 已除名消费面)〔专长批 V19〕")
var bad_words := 0
for row_v in (_db.feats.rows as Array):
var r_d: Dictionary = row_v as Dictionary
for txt_v in [String(r_d.get("display_name", "")), String(r_d.get("desc", ""))]:
if String(txt_v).contains("雷") or String(txt_v).contains("旗"):
bad_words += 1
check(bad_words == 0, "对外命名纪律:29 行 display_name/desc 零「雷/旗」字面(封印/陷阱风味系)〔专长批 V21 初筛〕")
check(bad_words == 0, "对外命名纪律:30 行 display_name/desc 零「雷/旗」字面(封印/陷阱风味系)〔专长批 V21 初筛〕")
check(float((_db.feats.row_by_id("trap_burst").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("mult", 0)) > 1.0
and int((_db.feats.row_by_id("blood_honey").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("value", 99)) < 5
and int((_db.feats.row_by_id("stone_skin").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("cap", 0)) == 3,
@@ -1346,8 +1346,8 @@ func _smoke_feats() -> void:
for t in a_r._curio_deck:
if String((t as Dictionary).get("kind", "")) == "relic":
deck_relic += 1
check(deck_relic == 5 and a_r._curio_deck.size() == 109,
"专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 104+5=109")
check(deck_relic == 5 and a_r._curio_deck.size() == 107,
"专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107")
# T4 渠道白名单断言(迭代 15 卡②——渠道轴定位:通关渠道只出专长/节点渠道零专长)
var feat_seen: Dictionary = {}
var node_relic_seen := false
@@ -6094,7 +6094,7 @@ func _smoke_tree_rules() -> void:
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": ["sense_guard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "voucher_insurance": 0, "node_type": "battle"}),
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
"守侧 rig:战斗编排启动(tree_rules=sense_guard 携带)")
var board = orch.minesweeper.current_board()
var safe := Vector2i(-1, -1)
@@ -6122,7 +6122,7 @@ func _smoke_tree_rules() -> void:
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": ["sense_guard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "voucher_insurance": 0, "node_type": "battle"}),
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
"守⑤ rig:次战编排启动(新战新队列实例)")
check(int(orch2.action_economy.pending_ap_delta()) == 0,
"守⑤:末轮判出不返(queue 只在 notify_round_start 兑现、战亡即弃——无跨战泄漏)")
@@ -6146,7 +6146,7 @@ func _smoke_tree_rules() -> void:
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": [], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "voucher_insurance": 0, "node_type": "battle"}),
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
"聚 rig:基线编排启动(无增益侧)")
orch3.encounter["tree_rules"] = []
var gp_base: int = orch3._roll_gp_drop()
@@ -6155,7 +6155,7 @@ func _smoke_tree_rules() -> void:
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": ["economy_hoard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "voucher_insurance": 0, "node_type": "battle"}),
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
"聚 rig:增益编排启动(tree_rules=economy_hoard)")
orch4.encounter["tree_rules"] = ["economy_hoard"]
var gp_hoard: int = orch4._roll_gp_drop()
+106 -21
View File
@@ -1,12 +1,18 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 凭证件五件+留任二件单测(刷新券/储备金袋/时沙/完美保险/同调电池 五件 + 留任二件——刷新写错账修复钉面)。
# 覆盖:刷新券→刷新存量账(非 _attune_refresh_x——写错账复发反例)/储备金袋→RAP/时沙→首轮 AP 池/
# 完美保险→标错豁免/同调电池→充能账(choose_battery_target 玩家点选——禁自动替选反例);
# 凭证件留任二件+两件出池+完美保险专长单测(刷新券/储备金袋/时沙 三件路由+留任二件——刷新写错账修复钉面)。
# 覆盖:刷新券→刷新存量账(非 _attune_refresh_x——写错账复发反例)/储备金袋→RAP/时沙→首轮 AP 池;
# 迭代 31 卡② 凭证件裁削批——同调电池整件删除、完美保险迁专长:两件出池反例(词表/兑付/符号清零/断点缺键)、
# 配额净删锚(稀有 7/总量 102)、完美保险专长验收组(装载/限次一次/计数纯净/两账相减判定/跨战清零/事件不吞);
# 留任 2 件路由与值不变(GP 钱包/当即回血)+行动点上限归属性轴(bonus.ap 落点)。
# 运行:godot --headless --path game --script res://tests/unit/core/feat_vouchers_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const EffectEngineScript := preload("res://core/effect_engine.gd")
func EffectEngineKindProbe(_db) -> bool:
return EffectEngineScript.EFFECT_KINDS.has("forgive_seal_miss")
func _init() -> void:
@@ -48,20 +54,36 @@ func _init() -> void:
Kit.check_quiet(a._apply_curio_pick("time_sand", "rare", 2), "时沙兑付受理〔专长批 V9〕")
Kit.check_quiet(a._time_sand_queue == 2, "时沙:首轮 AP 队列 +2(池增量非上限)〔专长批 V9〕")
# V9-4 完美保险:标错豁免待注入(节点级)
Kit.check_quiet(a._apply_curio_pick("perfect_insurance", "rare", 1), "完美保险兑付受理〔专长批 V9〕")
Kit.check_quiet(a._pending_insurance == 1, "完美保险:豁免待注入 1(节点级)〔专长批 V9〕")
# V9-5 同调电池:待分配充能 + 玩家点选(禁自动替选反例)+ 跨层写充能账
Kit.check_quiet(a._apply_curio_pick("attune_battery", "rare", 1), "同调电池兑付受理〔专长批 V9〕")
Kit.check_quiet(a.battery_pending() == 1, "同调电池:待分配充能 1〔专长批 V9〕")
Kit.check_quiet(not a.choose_battery_target("incense_stick"),
"电池反例:件不在装配集拒绝(禁自动替选——零副作用)〔专长批 V9·秘宝批换主语〕")
a._attunement = ["incense_stick"] # 注入装配集(incense_stick=新目录 active_charged)
a.run_ledger.attune_charges = {"incense_stick": 1} # 迭代 27 T3——四账住 RunLedger
Kit.check_quiet(a.choose_battery_target("incense_stick") and int(a.run_ledger.attune_charges["incense_stick"]) == 2,
"电池正例:玩家点选 incense_stick→充能账 +1(跨层写 attune_charges)〔专长批 V9·秘宝批换主语〕")
Kit.check_quiet(a.battery_pending() == 0, "电池:分配后待账清零〔专长批 V9〕")
# V23 两件出池(迭代 31 卡② 凭证件裁削批 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),
"出池反例:两件兑付不再受理(牌库无令牌=拒绝零副作用)〔卡② R1〕")
Kit.check_quiet(not a.has_method("choose_battery_target") and not a.has_method("battery_pending"),
"电池符号清零:runner 无点选函数与 getter(编译级孤儿面)〔卡② R1〕")
Kit.check_quiet(int(db.adventure.curio_cap("attune_battery")) == 99,
"出池后未知 kind 走分发表 default(cap 读数兜底 99——零崩溃面)〔卡② R1〕")
# 账本保留(契约 1):主动件充能账与每冒险重置入口零改动
a._attunement = ["incense_stick"]
a.run_ledger.attune_charges = {"incense_stick": 99}
a._seed_attune_charges()
Kit.check_quiet(int(a.run_ledger.attune_charges.get("incense_stick", -1)) != 99,
"账本保留:删件后 _seed_attune_charges 仍按装备重置充能账(attune_charges 语义零变化)〔卡② R1〕")
# V24 配额净删锚(R3——用户裁「净删」,rare 9→7/总量 104→102)
var rare_sum := 0
var rare_kinds: Array[String] = []
for k in db.adventure.CURIO_KINDS:
var q := int(db.adventure.curio_quota(k, "rare"))
rare_sum += q
if q > 0:
rare_kinds.append(k)
Kit.check_quiet(int(db.adventure.TIER_QUOTAS["rare"]) == 7 and rare_sum == 7
and int(db.adventure.deck_total()) == 102 and rare_kinds.has("ap") and rare_kinds.has("rap") and rare_kinds.has("time_sand")
and rare_kinds.size() == 3,
"配额锚:稀有档合计 7、投放品种={ap,rap,time_sand}、令牌池总量 102(validate 硬断言同批)〔卡② R3〕")
Kit.check_quiet(db.adventure.validate().is_empty(), "AdventureTable.validate() 零违例(两件出池后)〔卡② R3〕")
# V9-6 全链:凭证令牌兑付=牌库摘牌(选中即消尽)
a._curio_deck = [{"kind": "rap", "tier": "rare"}, {"kind": "attack", "tier": "common"}]
@@ -91,17 +113,80 @@ func _init() -> void:
sm.load_from_disk()
a.run_ledger.adventure_counters = {"pain_layers": 3, "twin_digs": 5} # 迭代 27 T3——四账住 RunLedger
a._refresh_stock = 2
a._battery_pending = 1
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()
Kit.check_quiet(int((bp16.get("adventure_counters", {}) as Dictionary).get("pain_layers", -1)) == 3
and int(bp16.get("refresh_stock", -1)) == 2 and int(bp16.get("battery_pending", -1)) == 1,
"V16:新账本三件入断点快照(adventure_counters/refresh_stock/battery_pending)〔专长批 V16〕")
and int(bp16.get("refresh_stock", -1)) == 2 and not bp16.has("battery_pending"),
"V16:账本入断点快照;电池键已随卡② 摘除(断点结构零该键)〔专长批 V16+卡② R1〕")
var b2 = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(b2.resume_adventure(bp16.duplicate(), {}), "V16 前置:续玩受理")
Kit.check_quiet(int(b2.run_ledger.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_stock() == 2 and b2.battery_pending() == 1,
var bp_legacy: Dictionary = bp16.duplicate(true)
bp_legacy["battery_pending"] = 7 # 旧断点档遗留键(Alpha 无迁移——缺键/多键兼容面)
var b3 = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(b3.resume_adventure(bp_legacy, {}), "V16:旧档含 battery_pending 键续玩受理(缺键/多键零报错)〔卡② R1〕")
Kit.check_quiet(int(b2.run_ledger.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_stock() == 2,
"V16:续玩回填逐项一致(计数恢复——跨战账不丢)〔专长批 V16〕")
# V25 完美保险专长验收组(迭代 31 卡② T4/T5——R2 全项:装载/限次一次/计数纯净/两账相减/跨战清零/事件不吞)
var fi: Dictionary = db.feats.row_by_id("perfect_insurance")
Kit.check_quiet(not fi.is_empty() and String(fi.get("rarity", "")) == "rare"
and String(fi.get("desc", "")).find("雷") == -1 and String(fi.get("desc", "")).find("封印") >= 0,
"专长行合法:第 30 行 perfect_insurance 在表、rarity rare、desc 零「雷」字面〔卡② R2〕")
Kit.check_quiet(db.feats.validate().is_empty(), "feats.validate() 零违例(新行订阅形态与词表合法)〔卡② R2〕")
var RigScript := load("res://core/battle_orchestrator.gd")
var RngScript := load("res://core/rng_service.gd")
var rig_rng = RngScript.new()
rig_rng.set_master_seed(778001) # EC7 未播种取数致命——rig 须先播种子
var orch = RigScript.new(db, rig_rng)
Kit.check_quiet(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": ["perfect_insurance"], "tree_rules": ["sense_guard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
"保险 rig:战斗启动(feats=perfect_insurance 与 tree_rules=sense_guard 双订阅 seal_missed)〔卡② R2〕")
var safe_cells: Array = []
for c in orch.minesweeper.current_board().all_cells():
if not orch.minesweeper.current_board().is_mine(c):
safe_cells.append(c)
if safe_cells.size() >= 2:
break
Kit.check_quiet(safe_cells.size() == 2, "保险 rig:两枚安全格定位(去重闸按格实例——限次样本须异格)〔卡② R2〕")
Kit.check_quiet(int(orch._perfect_hit_false) == 0 and int(orch._perfect_forgiven) == 0,
"保险前置:误标账与豁免账双零起〔卡② R2〕")
orch._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1)
Kit.check_quiet(int(orch._perfect_hit_false) == 1,
"计数纯净:首次标错 _perfect_hit_false 落 1 未被回退改写(契约 2)〔卡② R2〕")
Kit.check_quiet(int(orch._perfect_forgiven) == 1,
"豁免一次:首次标错 _perfect_forgiven=1(forgive_seal_miss 派发)〔卡② R2〕")
Kit.check_quiet(int(orch.action_economy.pending_ap_delta()) == 1,
"事件不吞:同一标错的 seal_missed 仍被危险直觉·守收到(queue AP+1)〔卡② R2〕")
orch._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2)
Kit.check_quiet(int(orch._perfect_hit_false) == 2 and int(orch._perfect_forgiven) == 1,
"限次:第二次标错不获豁免(forgiven 恒 1——when counter lt 1)〔卡② R2〕")
Kit.check_quiet(not orch.perfect_sweep_earned(),
"判定相减:误标 2 − 豁免 1 = 有效 1 → 完美不成立〔卡② R2 真值表〕")
orch._perfect_hit_false = 1
orch._perfect_hit_true = 1
Kit.check_quiet(orch.perfect_sweep_earned(),
"判定相减:误标 1 − 豁免 1 = 有效 0 且标中≥1 → 完美成立〔卡② R2 真值表〕")
orch._perfect_hit_true = 0
Kit.check_quiet(not orch.perfect_sweep_earned(),
"判定守卫:标中数 0 时零误标也不成立(板级 ≥1 标中条件零松动)〔卡② R2 真值表〕")
var rig_rng2 = RngScript.new()
rig_rng2.set_master_seed(778002)
var orch2 = RigScript.new(db, rig_rng2)
Kit.check_quiet(orch2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": ["perfect_insurance"], "tree_rules": [], "battle_index": 2,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
"次战 rig:第二战编排启动(feats 同件)〔卡② R2〕")
var sc2 = safe_cells[0]
orch2._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1)
Kit.check_quiet(int(orch2._perfect_forgiven) == 1 and int(orch2._perfect_hit_false) == 1,
"跨战清零:domain=battle 豁免账每战重建——次战首次标错再豁免一次〔卡② R2〕")
Kit.check_quiet(EffectEngineKindProbe(db), "词表闭集:forgive_seal_miss 在 EFFECT_KINDS(出域即加载致命——扩签合规)〔卡② R4〕")
sm.clear_breakpoint()
quit(Kit.finish("feat_vouchers"))
@@ -194,7 +194,7 @@ func _test_negative_cases() -> void:
# 后现行 31;本批零扩签,钉现行数(将来任何一批加词即转红提示复核)。
func _test_effect_kinds_closed() -> void:
Kit.check("效果引擎效果类型闭集 31 种零新增(锻造工坊批不开效果类型词)〔锻造工坊批 V11〕",
EffectEngineScript.EFFECT_KINDS.size() == 31)
Kit.check("效果引擎效果类型闭集 32 种(锻造工坊批零新增口径随迭代 31 卡② 扩签 forgive_seal_miss 换代)〔锻造工坊批 V11〕",
EffectEngineScript.EFFECT_KINDS.size() == 32)
Kit.check("效果引擎事件词表在册 20 种(本批零扩签参照面)〔锻造工坊批 V11〕",
EffectEngineScript.EVENTS.size() == 20)