T3(迭代22秘宝批): 效果引擎接入与常时参数面——board_installed事件发射(巢穴预告读数口nest_preview_count=装板报危险格数量,呈现归呈现桥); 天眼safe_dig=fx_reveal通道复用(C5全额兑付免AP连锁资源同链,危险格仅真视,旗格跳过); reveal_tiles入真视叠加层(mine_reveal同通道); 试炼镐use_attune_convert积点受理口(charges=0零骑库存,counter_spend受理闸+grant_ap,积点计数通用化=装配集ap_charge_safe行扫描替dig_charge硬编码); 充能重置周期_reseed_attune_charges(battle每战/level每关首战battle_index==1,骑库存差额补注); 常时参数面注入(远程镐dig_range_bonus切比雪夫≤1+1/信标网络flag_range_free旗可达豁免——_within_dig_range拆谓词,mine_total只读口); 储备罐finish_round_stop转存率入参(125%=1.25:1整除floor); 刷新练习refresh_stock_grant开局一次性加仓; 刷新执照shop_refresh_free_left装配叠加; 真视使用计数链(vision_counter共享dict→受理+1→summary→settle聚合入档+断点随件); validate_use扩cell档与新空间kind; 修现役潜伏缺陷=use_treasure板维度读Rules不存在的width(旧目录期零直呼掩盖); 新单测treasure_effects_test 22套(V7附属表12件逐件正常/边界/反例+计数链)。冒烟895/0+run_all 22套+探针全绿〔秘宝批 V7/V8/V10/V13〕

This commit is contained in:
DKChan committed 2026-10-12 17:01:12 +08:00
1 parent a7bd4e4132
commit c813b2b042
9 files changed
+514 -28

No files matched your search

+5 -4
View File
@@ -185,16 +185,17 @@ func grant_reserve_ap(n: int) -> void:
# 轮末停手转存决策(round_end 发射点调用——编排器授予段尾):transfer=true=剩余 AP 转蓄力层
# (上限 3,整备军备面);false=1:1 转存 RAP(缺省)。非停手轮(cleared/耗尽)_last_stop_ap=0
# 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。
func finish_round_stop(transfer: bool) -> void:
# (上限 3,整备军备面);false=按转存率转存 RAP(transfer_pct 百分比口径缺省 100=1:1 与旧逐位一致;
# 迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude 125=1.25:1,整数除法向下取整=每 4 AP 多转 1)。
# 非停手轮(cleared/耗尽)_last_stop_ap=0 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。
func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void:
if transfer:
var prev_layers := _charge_layers
_charge_layers = mini(3, _charge_layers + _last_stop_ap)
if _fx != null and _charge_layers >= 3 and prev_layers < 3:
_fx.dispatch("charge_full", {"layers": _charge_layers})
else:
_rap += _last_stop_ap
_rap += _last_stop_ap * maxi(1, transfer_pct) / 100
_last_stop_ap = 0
+12
View File
@@ -77,6 +77,7 @@ var _attune_refresh_x := 0 # 刷新件余量(迭代 11 第①
var _attune_refresh_y := 0 # Y 冒险周期池(开局参数面注入,用完即无——营地升级行表值)
var _curio_paid_refreshes := 0 # 本次停点已付费刷新次数(2026-10-06 用户令对齐商店 MC-1——GP 刷新价曲线输入,随停点创建清零=与商店进店清零同构)
var attune_charges := {} # 同调主动件冒险充能账本(迭代 11 第①批 T6——跨关卡共享 dict;settlement use_treasure 扣减直改同引用;断点随 attunement_charges 键入档)
var vision_counter := {"used": 0} # 真视类工具使用计数(迭代 22 秘宝批 T3——跨关卡共享 dict;编排器受理成功 +1;断点随 vision_counter_used 键入档,收束聚合入档 save.add_vision_used)
var adventure_counters := {} # adventure 域计数器账本(迭代 20 T6——攒层/付费计数等跨战持久件账;效果引擎 rebuild 回灌/战末导出回写共享引用;断点随 adventure_counters 键入档)
# ---- 凭证件三本新账(迭代 20 T4——奇物.md 凭证件表新 5 件;F-1 刷新写错账修复承载面)----
var _refresh_stock := 0 # 刷新存量账(冒险级——刷新券加仓;**不经 _attune_refresh_x**:X 每停点重置会吞存量=旧写错账缺陷 F-1)
@@ -136,6 +137,13 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive(迭代 11 第①批 T5)
_attune_refresh_y = maxi(0, int(params.get("attune_refresh_y", 0))) # additive(迭代 11 第①批 T5b——Y 冒险周期池)
_seed_attune_charges() # 迭代 11 第①批 T6——冒险开始充能账本按装备主动件重置(每冒险充能语义)
vision_counter = {"used": 0} # 迭代 22 秘宝批 T3——真视使用计数冒险开始归零(跨局累计归生涯档)
# 刷新练习开局注入(迭代 22 秘宝批 T3——refresh_stock_grant 装配即得一次性加仓:
# 装配集 kind 分发扫描;只入存量账 _refresh_stock、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕)
for iid_v2 in _attunement:
var rp_row: Dictionary = db.treasure_item_row(String(iid_v2))
if (rp_row.get("effect_kinds", []) as Array).has("refresh_stock_grant"):
_refresh_stock += int(rp_row.get("magnitude", 0))
# 特性开局注入(迭代 12 T7——MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派
# 各 2 件档一成型——测量面专用;真实局规则改写型特性只走局内随机渠道,红线②层级纪律不破〕)
var feat_seed: Array = params.get("relics_seed", []) if params.get("relics_seed", []) is Array else []
@@ -231,6 +239,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
if bp_counters is Dictionary:
adventure_counters = (bp_counters as Dictionary).duplicate(true)
_refresh_stock = maxi(0, int(bp.get("refresh_stock", 0))) # 迭代 20 T4/T7——旧档缺键缺省 0
vision_counter = {"used": maxi(0, int(bp.get("vision_counter_used", 0)))} # 迭代 22 秘宝批 T3——真视计数随断点回填(缺键=旧断点缺省 0)
_battery_pending = maxi(0, int(bp.get("battery_pending", 0)))
var bp_result: Dictionary = bp.get("bonus_result", {}) as Dictionary
if not bp_pending.is_empty():
@@ -1091,6 +1100,7 @@ func _start_current_level() -> bool:
if ok:
_runner.set_attunement(_attunement) # 迭代 11 第①批 T5——同调装配下传(enc 携带进战斗结算)
_runner.set_attune_charges(attune_charges) # 迭代 11 第①批 T6——充能账本下传(能力件骑库存面注入)
_runner.set_vision_counter(vision_counter) # 迭代 22 秘宝批 T3——真视使用计数共享 dict 下传(编排器受理成功 +1)
_runner.set_feats(_feats) # 迭代 12 T1——特性持有集下传(begin_battle enc.feats 求值面)
_runner.set_tree_rules(_tree_rules) # 迭代 12 T10——树 rule 键集下传(A/B 面消费)
if _shop_mode == "pool":
@@ -1178,6 +1188,7 @@ func _settle(result: String) -> void:
"master_seed": master_seed,
"aux_cleared_last": _last_treasure_cleared, # additive 扩签(迭代 7 T4——末关清空反馈;非末关走 bonus 载荷)
"keystone_drops": _keystone_drops.duplicate(true), # additive 扩签(迭代 10——逐杀掉落明细;settle 入账明细/复盘归因数据源)
"vision_used_delta": int(vision_counter.get("used", 0)), # additive 扩签(迭代 22 秘宝批 T3——真视使用本局计数;settle 聚合入档)
"pool_stock": _pool_stock_snapshot(), # additive 扩签(迭代 11 第②批 T5——终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面)
"relics": _feats.duplicate(), # additive 扩签(迭代 12 T1——特性终局持有集:清账前快照,成型判读取数面)
}
@@ -1252,6 +1263,7 @@ func _snapshot_breakpoint() -> void:
"feat_ticket_pity": _feat_ticket_pity, # T5 门票保底计数(扩列向后兼容——旧档缺键缺省 0)
"feat_ticket_sold": _feat_ticket_sold, # T5 门票限购计数(同上)
"adventure_counters": adventure_counters.duplicate(true), # 迭代 20 T6/T7——adventure 域计数器随断点(续玩跨战账不丢)
"vision_counter_used": int(vision_counter.get("used", 0)), # 迭代 22 秘宝批 T3——真视使用计数随断点(续玩跨战账不丢)
"refresh_stock": _refresh_stock, # 迭代 20 T4/T7——刷新存量账随断点(F-1 新账)
"battery_pending": _battery_pending, # 迭代 20 T4/T7——电池待分配充能随断点
"bonus_result": _curio_result.duplicate(true), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
+142 -13
View File
@@ -81,6 +81,7 @@ var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格
# ---- 效果引擎扩签运行态(迭代 20 专长实装批 T1——新效果类型施加面;空订阅零扰动)----
var _fx_hero_ac_base := -1 # 主角 AC 基线快照(enc.hero_armor_class;-1=表值兜底未解析)
var _fx_ac_round := 0 # ac_bonus 本轮累积器(封印桩形态——round_start 清零)
var nest_preview_count := -1 # 巢穴预告读数口(迭代 22 秘宝批 T3——board_installed 命中落值;-1=未装配/未发射;呈现桥消费)
var _fx_ac_battle := 0 # ac_bonus 本战累积器(石肤形态——cap 封顶,begin_battle 清零)
var _fx_adventure_counters_ref: Dictionary = {} # adventure 域计数器共享引用(T6——attune_charges 先例:begin_battle 回灌 rebuild、battle 末 counters_export 回写)
var _fx_bonus_ref: Dictionary = {} # 冒险加算区共享引用(bonus_delta 战末汇入——血税双向账)
@@ -216,6 +217,80 @@ func _attune_row(item_id: String) -> Dictionary:
return db.treasure_item_row(item_id) if _attune_has(item_id) else {}
# 装配集参数合计(迭代 22 秘宝批 T3——常时参数面 kind 的装配集 Σmagnitude 读数口;
# kind 分发查询先例=_attune_refresh_x_total bonus_refresh。零装配=0)
func attune_param_sum(kind: String) -> int:
var total := 0
for iid_v in attunement:
var arow: Dictionary = db.treasure_item_row(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has(kind):
total += int(arow.get("magnitude", 0))
return total
# 装配集内 ap_charge_safe 行读取(迭代 22 秘宝批 T3——掘进蓄能 id 特判通用化取数源;
# 首行语义=装配集内至多一件积点制〔新目录恰试炼镐〕)
func _attune_charge_safe_row() -> Dictionary:
for iid_v in attunement:
var arow: Dictionary = db.treasure_item_row(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has("ap_charge_safe"):
return arow
return {}
# 充能重置(迭代 22 秘宝批 T3——按周期回满:账本回 charges_per_adventure+骑库存差额补注入
# (used handle 不回收=本轮已耗充能作废,补新 handle=「回满」;差额注入防双份=level_runner
# set_attune_charges 同构)。reset ∈ {battle, level}——adventure 周期归冒险开始种子面)
func _reseed_attune_charges(reset: String) -> void:
for iid_v in attunement:
var iid := String(iid_v)
var arow: Dictionary = db.treasure_item_row(iid)
if String(arow.get("form", "")) != "active_charged":
continue
if String(arow.get("charge_reset", "adventure")) != reset:
continue
var want := int(arow.get("charges_per_adventure", 0))
if want < 1:
continue # charges=0=积点制(试炼镐——不走骑库存面,受理=use_attune_convert 积点口)
if _attune_charges_ref != null:
_attune_charges_ref[iid] = want
if treasure_inventory != null:
var existing := 0
for r in treasure_inventory.snapshot_inventory():
if String(r.get("item_id", "")) == iid and String(r.get("source_class", "")) == "attunement" and not bool(r.get("used", false)):
existing += 1
for i in range(maxi(0, want - existing)):
treasure_inventory.add_item(iid, "attunement", Vector2i(-1, -1), 0)
# 积点制主动件受理(迭代 22 秘宝批 T3——试炼镐「转化获取」唯一入口:不经库存 handle
# 〔charges=0 零骑库存〕,受理闸=行 triggers counter_spend 积点账〔dig_reveal 通用化计数段
# 逐 charge_n 积 1 点〕;消耗成功才兑付当轮 AP=magnitude。返回 use_treasure 同构 verdict)
func use_attune_convert(item_id: String) -> Dictionary:
var arow: Dictionary = db.treasure_item_row(item_id)
if arow.is_empty() or String(arow.get("form", "")) != "active_charged" \
or not (arow.get("effect_kinds", []) as Array).has("ap_charge_safe") \
or not _attune_has(item_id):
return {"ok": false, "reason": "非装配积点件"}
if action_economy.phase != action_economy.PHASE_ACTIVE:
return {"ok": false, "reason": "非挖掘期(使用窗口=Active)"}
var bank_id := ""
for tr_v in (arow.get("triggers", []) as Array):
var dd: Dictionary = (tr_v as Dictionary).get("do", {}) as Dictionary
if String(dd.get("kind", "")) == "counter_spend":
bank_id = String(dd.get("counter_id", ""))
break
if bank_id == "":
return {"ok": false, "reason": "行缺积点受理闸"}
if not _fx.counter_spend(bank_id, 1):
var cn := int(arow.get("charge_n", 0))
return {"ok": false, "reason": "%s:安全格积点未达(%d/%d)" % [String(arow.get("display_name", "")), _attune_safe_seen % cn if cn > 0 else 0, cn]}
if not action_economy.grant_ap(int(arow.get("magnitude", 0))):
return {"ok": false, "reason": "行动点加量被拒(相位防御)"}
# 兑付反馈=AP 条即时刷新(use_treasure 同款纪律——结算明细无道具行:效果即时可见零明细行)
return {"ok": true, "item_id": item_id}
# 特性求值(内容数据驱动扩展批 T3——_feat_flags_for 挂点求值退役:效果行为=数据行 triggers 订阅,
# 效果引擎 dispatch 分发;旧 23 hook 枚举与旗注入消亡。流派档位质变=FeatsEngine 纯函数求值
# (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击)
@@ -344,6 +419,24 @@ func begin_battle(enc: Dictionary) -> bool:
# 迭代 20 T6——adventure 域计数器从共享引用回灌〔跨战持久〕)
_fx.rebuild(feat_ids, attunement, tree_rules)
_fx.counters_seed(_fx_adventure_counters_ref)
# 秘宝常时参数注入(迭代 22 秘宝批 T3——装配集 kind 分发:远程镐挖距放宽/信标网络旗可达豁免;
# 缺省 0/false=零装配零扰动。注入先于首板 install 已不成立〔板在早段〕——本批板生成不读两参数,
# 参数只影响挖掘/插棋动作判据,时序安全)
minesweeper.dig_range_bonus = attune_param_sum("dig_range_bonus")
minesweeper.flag_range_free = attune_param_sum("flag_range_free") > 0
# 充能重置周期(迭代 22 秘宝批 T3——battle=每战回满/level=每关首战回满〔battle_index==1〕;
# adventure 周期维持冒险开始 _seed_attune_charges 单次)
_reseed_attune_charges("battle")
if int(enc.get("battle_index", 1)) == 1:
_reseed_attune_charges("level")
# board_installed 发射+施加(迭代 22 秘宝批 T3——巢穴预告:装板后一次报本板危险格数量;
# 只报数量不标位置=信息红线,不碰分布=棋盘事实不可变契约 3。命中=读数口落值,呈现归
# 呈现桥消费(无格来源不进结算明细行——行来源纪律);板已就绪〔早段 install〕、订阅行已在 rebuild 装载)
nest_preview_count = -1
var bi_hits := _fx_emit("board_installed", {})
for h in bi_hits:
if String(h.get("kind", "")) == "mine_count_preview":
nest_preview_count = minesweeper.mine_total()
# 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现
# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后
# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
@@ -609,15 +702,23 @@ func _settle() -> void:
"奥术矢", "board", int((spell_hits[0] as Dictionary).get("value", 0)),
DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic")
_submit(spell_t, false)
# 掘进蓄能计数(迭代 11 同调批 T6 → 内容数据驱动扩展批 T4:积点账本迁效果引擎计数器
# dig_charge_bank——行 triggers counter_spend(treasure_used) 受理闸同账本。模判定留编排器侧:
# 「每 N 格积 1 点」在发射点施加架构下无法行内表达先增后判(when 于 dispatch 预评估),
# 逐位等价优先——词汇表边界注记随 commit 留痕;charge_n 读行零字面量)
if _attune_has("dig_charge"):
# 积点制计数(迭代 11 同调批 T6 → 迭代 22 秘宝批 T3 通用化:装配集 ap_charge_safe 行
# 〔新目录=试炼镐;旧 dig_charge id 特判随行退役摘除〕——模判定留编排器侧:「每 N 格积
# 1 点」在发射点施加架构下无法行内表达先增后判(when 于 dispatch 预评估),逐位等价优先;
# 积点账本 counter_id=行 triggers counter_spend 声明单点,charge_n 读行零字面量)
var cs_row: Dictionary = _attune_charge_safe_row()
if not cs_row.is_empty():
_attune_safe_seen += 1
var cn_b: int = int(db.treasure_item_row("dig_charge").get("charge_n", 999))
var cn_b: int = int(cs_row.get("charge_n", 999))
if cn_b > 0 and _attune_safe_seen % cn_b == 0:
_fx.counter_add("dig_charge_bank", 1)
var bank_b := ""
for tr_b in (cs_row.get("triggers", []) as Array):
var dd_b: Dictionary = (tr_b as Dictionary).get("do", {}) as Dictionary
if String(dd_b.get("kind", "")) == "counter_spend":
bank_b = String(dd_b.get("counter_id", ""))
break
if bank_b != "":
_fx.counter_add(bank_b, 1)
elif evt.kind == "mine":
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
@@ -764,7 +865,11 @@ func _settle() -> void:
_emit_row("effect", "fx_grant_gp", Vector2i(-1, -1), ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": escrow}, "")
elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
action_economy.queue_ap_delta(int(h.get("value", 0)))
action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer"))
# 转存率入参(迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1;
# 未装配=100 与旧 1:1 逐位一致)
var rap_pct: int = attune_param_sum("rap_transfer_mult")
action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer"),
rap_pct if rap_pct > 0 else 100)
if _facts_path == "cleared":
_grant_clear_reward(ri)
_flush_pending_reveals(ri) # 迭代 20 T1——揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入)
@@ -868,8 +973,10 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
if action_economy.phase != action_economy.PHASE_ACTIVE:
return {"ok": false, "reason": "非挖掘期(使用窗口=Active)"}
var board: MinesweeperRules = minesweeper # 恒等包装退役(2026-10-02 修复批——use_treasure 直引公有成员)
# 板维度读数(迭代 22 T3 修——Rules 无 width 代理〔潜伏缺陷:旧目录期零直呼覆盖掩盖〕,读 BoardData 本体)
var board_b: BoardData = board.current_board()
var v: Dictionary = treasure_inventory.validate_use(handle, target,
Vector2(board.width, board.height),
Vector2(board_b.width, board_b.height),
func(c: Vector2i): return not board.is_revealed(c))
if not bool(v.ok):
return v # ② 拒绝零消耗
@@ -879,10 +986,13 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
for h in tu_hits:
if String(h.get("kind", "")) == "counter_spend":
if not _fx.counter_spend(String(h.get("counter_id", "")), int(h.get("value", 1))):
var cn_info: int = int(db.treasure_item_row("dig_charge").get("charge_n", 0))
return {"ok": false, "reason": "掘进蓄能:安全格积点未达(%d/%d)" % [_attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]}
var vrow: Dictionary = db.treasure_item_row(String(v.item_id))
var cn_info: int = int(vrow.get("charge_n", 0)) # 迭代 22 T3——读受理行自身(旧 dig_charge 硬编码随行退役)
return {"ok": false, "reason": "%s:安全格积点未达(%d/%d)" % [String(vrow.get("display_name", "")), _attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]}
var kinds: Array = v.kinds
if kinds.has("mine_reveal"):
if kinds.has("mine_reveal") or kinds.has("reveal_tiles"):
# 真视写入(迭代 22 T3——reveal_tiles 新真视 kind 与 mine_reveal 同叠加层通道:
# 危险格显示=信息呈现零回写,棋盘事实不可变契约 3)
treasure_inventory.apply_overlay("mine_reveal", v.resolved)
if kinds.has("reward_preview"):
treasure_inventory.apply_overlay("reward_preview", v.resolved)
@@ -890,9 +1000,22 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
if not action_economy.grant_ap(int(v.magnitude)):
return {"ok": false, "reason": "行动点加量被拒(相位防御)"}
if kinds.has("ap_charge_safe"):
# 掘进蓄能兑付(迭代 11 同调批 T6——临时 AP 当轮生效,grant_ap 同 sapper_order 通道)
# 积点件兑付(迭代 11 同调批 T6——临时 AP 当轮生效;库存通道积点件已由 use_attune_convert
# 承载〔charges=0 零骑库存〕,本分支保留=charges>0 且挂 ap_charge_safe 的历史形态兼容)
if not action_economy.grant_ap(int(v.magnitude)):
return {"ok": false, "reason": "行动点加量被拒(相位防御)"}
if kinds.has("safe_dig"):
# 天眼安全格直开(迭代 22 秘宝批 T3——C5 裁全额兑付:fx_reveal 通道复用=免 AP 零相位、
# 连锁与格面资源与普通挖格同链;危险格仅真视显示不移除=契约 3;有旗格跳过=旗=玩家承诺不代开)
var opened_any := false
for c_v in v.resolved:
var c := Vector2i(c_v)
if board.has_flag(c):
continue
if board.fx_reveal(c):
opened_any = true
if not opened_any:
return {"ok": false, "reason": "天眼:窗口内安全格均已开(无可开格)"}
if kinds.has("cell_removal"):
# 爆破工兵包兑付(迭代 13 T8/D6——中立移除雷格:未揭示/无旗/是雷过滤归排雷玩法;
# P1 甲邻接数字冻结零重算、清图分母零涉(雷格不入分母);空集拒绝零消耗——
@@ -906,6 +1029,12 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
var used_iid: String = String(rec.get("item_id", ""))
if _attune_charges_ref.has(used_iid):
_attune_charges_ref[used_iid] = maxi(0, int(_attune_charges_ref[used_iid]) - 1)
# 真视类工具使用计数(迭代 22 秘宝批 T3——信标网络解锁行为条件:受理成功恰计一次;
# 共享 dict 引用下传〔attune_charges 同款〕,冒险收束聚合入档 save.add_vision_used)
if kinds.has("reveal_tiles") or kinds.has("safe_dig") or kinds.has("mine_reveal"):
var vc: Dictionary = encounter.get("vision_counter", {}) if encounter is Dictionary else {}
if vc is Dictionary and not vc.is_empty():
vc["used"] = int(vc.get("used", 0)) + 1
return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec}
+20 -1
View File
@@ -67,6 +67,7 @@ var _treasure_unlocked := {} # aux 解锁门控集(迭
var _attunement: Array = [] # 同调装配集(迭代 11 第①批 T5——adventure params.attunement 下传,begin_battle enc 携带进战斗结算;空集=off 零扰动)
var _attune_charges: Dictionary = {} # 同调主动件充能账本(迭代 11 第①批 T6——adventure 共享 dict 引用;settlement use_treasure 扣减直改同引用)
# 关卡末清空+快照计数入 level_finished 载荷)
var _vision_counter: Dictionary = {"used": 0} # 真视类工具使用计数(迭代 22 秘宝批 T3——adventure 共享 dict 引用;编排器受理成功 +1,收束聚合入档)
# ---- 商店共享池模式态(迭代 11 第②批 T3——缺省 legacy=固定三件加定价 off 对照承载,零 RNG 零池消费;TP0 令牌池换代)----
var _shop_pool_mode := false # pool 模式旗(set_shop_pool 注入时置位——AdventureRunner 按 params.shop_mode 下传)
var _pool_deck: Array = [] # 共享池令牌库共享引用(AdventureRunner._curio_deck——库存=剩余令牌数单点)
@@ -481,6 +482,7 @@ func _start_battle(node: Dictionary) -> bool:
"hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(),
"attunement": _attunement,
"attune_charges": _attune_charges,
"vision_counter": _vision_counter, # 迭代 22 秘宝批 T3——真视使用计数共享 dict(编排器受理成功 +1,收束入档)
"feats": _feats,
"tree_rules": _tree_rules,
"battle_index": _battle_index(),
@@ -672,6 +674,11 @@ func set_attune_charges(charges: Dictionary) -> void:
for i in range(maxi(0, int(charges[iid]) - existing)):
treasure_inventory.add_item(String(iid), "attunement", Vector2i(-1, -1), 0)
# 真视使用计数共享引用注入(迭代 22 秘宝批 T3——adventure 持有、编排器受理成功 +1)
func set_vision_counter(counter: Dictionary) -> void:
_vision_counter = counter
# 奖励节点兑付透传(迭代 7 T5——呈现层三选一 → 关卡域库存;来源归因 reward_node)。
# 迭代 11 第①批 T7——同调位持有闸随过渡键退役拆除(aux 卡出池后仅剩防御面;能力件容量闸
# 正主 = 营地装配面 Attunement.slots 满位禁选)。
@@ -903,7 +910,19 @@ func shop_shelf() -> Array:
# knob=0——Q2 裁「基础免费 0 次」)用尽后 GP 付费刷新;刷新=货架整架重抽(shop_draw 恰一新 scope,
# 与进店/买后重抽同签——重抽同源共享池);池空/非池模式=不可刷新合法态。
func shop_refresh_free_left() -> int:
return maxi(0, int(db.adventure.shop_refresh_free))
# 迭代 22 秘宝批 T3——刷新执照 shop_refresh_bonus 装配合计叠加(进店入账语义=读数即本店可用,
# 与基础 knob 同生命周期;未装配=基值逐位同旧)
return maxi(0, int(db.adventure.shop_refresh_free) + _attune_shop_refresh_bonus())
# 刷新执照装配合计(迭代 22 秘宝批 T3——装配集 kind 分发扫描;零装配=0)
func _attune_shop_refresh_bonus() -> int:
var total := 0
for iid_v in _attunement:
var arow: Dictionary = db.treasure_item_row(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has("shop_refresh_bonus"):
total += int(arow.get("magnitude", 0))
return total
func shop_refresh_cost() -> int:
+1
View File
@@ -134,6 +134,7 @@ func settle_adventure(summary: Dictionary) -> Dictionary:
save.mark_first_adventure_done()
var drops: Array = (summary.get("keystone_drops", []) as Array).duplicate(true)
save.apply_adventure_summary(summary, drops)
save.add_vision_used(int(summary.get("vision_used_delta", 0))) # 迭代 22 T3——真视使用计数收束聚合入档(信标网络行为条件)
var new_unlocks := refresh_treasure_unlocks() # 迭代 22 T2——条件解锁刷新换代(旧工坊面退役)
save.write_to_disk()
var total := int(summary.get("keystone_total", 0))
+22 -3
View File
@@ -15,6 +15,12 @@ signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(
var db: ConfigDb
var action_economy: ActionEconomy
# 秘宝常时参数注入面(迭代 22 秘宝批 T3——编排器 begin_battle 按装配集 kind 分发注入:
# dig_range_bonus=挖掘切比雪夫距离放宽〔远程镐 dig_range_bonus magnitude〕/flag_range_free=
# 插旗不限相邻〔信标网络〕;缺省 0/false=零装配零扰动,棋盘事实不可变契约不受影响——
# 两参数只放宽「动作可达性」判据,禁碰危险格/安全格分布)
var dig_range_bonus := 0
var flag_range_free := false
# 棋盘状态私有(E2 封装收口 2026-09-13):对外只暴露 current_board()/reveal_record() 等查询面,
# 禁止跨模块直读/直写原始字典——先例教训 = board_view 绕过查询面直读 revealed。
var _board: BoardData = null
@@ -322,7 +328,7 @@ func _do_dig(c: Vector2i) -> Dictionary:
func _board_face_dig(c: Vector2i) -> String:
if not _board.in_bounds(c):
return "out_of_catalog"
if not _adjacent_to_hero(c):
if not _within_dig_range(c):
return "not_adjacent"
if is_revealed(c):
return "target_revealed"
@@ -395,8 +401,8 @@ func _do_flag_place(c: Vector2i) -> Dictionary:
var sub := ""
if not _board.in_bounds(c):
sub = "out_of_catalog"
elif not _adjacent_to_hero(c):
sub = "not_adjacent"
elif not (flag_range_free or _adjacent_to_hero(c)):
sub = "not_adjacent" # 信标网络装配时豁免(插旗不限相邻——旗动作仍按标准价计费,规则放宽仅可达性)
elif is_revealed(c):
sub = "target_revealed"
elif has_flag(c):
@@ -489,5 +495,18 @@ func _adjacent_to_hero(c: Vector2i) -> bool:
return maxi(d.x, d.y) == 1
# 挖掘可达判据(迭代 22 秘宝批 T3——远程镐 dig_range_bonus 放宽:切比雪夫距离 ≤ 1+加值,
# 加值 0=与旧「恰相邻」逐位一致;开格仍走既有格规则=不挖开不相邻事实之外 anything)
func _within_dig_range(c: Vector2i) -> bool:
var d := Vector2i(absi(c.x - _hero.x), absi(c.y - _hero.y))
return maxi(d.x, d.y) <= 1 + dig_range_bonus
# 本板危险格总数(迭代 22 秘宝批 T3——巢穴预告「装板后揭示本板危险格数量」取数口;
# 只读投影 BoardData.mines 规模,零分布泄露——数量非位置)
func mine_total() -> int:
return _board.mines.size() if _board != null else 0
func _outcome(accepted: bool, outcome: String, reason: String, sub: String) -> Dictionary:
return {"accepted": accepted, "outcome": outcome, "reject_reason": reason, "sub_reason": sub}
+13 -7
View File
@@ -140,15 +140,21 @@ func validate_use(handle: int, target: Vector2i, board_size: Vector2i, cell_unre
return _reject("道具未激活")
var kinds: Array = row.get("effect_kinds", [])
var resolved: Array = []
if kinds.has("mine_reveal") or kinds.has("reward_preview"):
# 空间效果:3×3 窗口 ∩ 板内格(边缘受理——护栏只缩不扩)
if kinds.has("mine_reveal") or kinds.has("reward_preview") or kinds.has("reveal_tiles") or kinds.has("safe_dig"):
# 空间效果(迭代 22 秘宝批 T3 扩面——真视 reveal_tiles/天眼复合 safe_dig 入空间解析;
# 真视窄域护栏 REVEAL_KINDS 同守):
if target.x < 0 or target.y < 0 or target.x >= int(board_size.x) or target.y >= int(board_size.y):
return _reject("目标越板")
for dy in range(-1, 2):
for dx in range(-1, 2):
var c := target + Vector2i(dx, dy)
if c.x >= 0 and c.y >= 0 and c.x < int(board_size.x) and c.y < int(board_size.y):
resolved.append(c)
if String(row.get("target", "")) == "cell":
# 单格档(迭代 22 T1——线香真视单格;≤3×3 窄域契约内的点目标)
resolved.append(target)
else:
# 3×3 窗口 ∩ 板内格(边缘受理——护栏只缩不扩)
for dy in range(-1, 2):
for dx in range(-1, 2):
var c := target + Vector2i(dx, dy)
if c.x >= 0 and c.y >= 0 and c.x < int(board_size.x) and c.y < int(board_size.y):
resolved.append(c)
# 点击级零信息:纯预读行窗口全已开 = 没东西可看(拒绝零消耗);
# 真视恒可用(零雷窗口=「这 9 格没雷」信息非空——GDD 裁定)
if kinds.has("reward_preview") and not kinds.has("mine_reveal"):
+1
View File
@@ -45,6 +45,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0},
# fog_lantern_wiring_test 随提灯载体行退役摘除(迭代 22 T1——V13 旧验证约束同生死;呈现桥换代断言归 T4 阔视野带参数面)
{"path": "res://tests/unit/core/effect_words_t1_test.gd", "expect": 0},
{"path": "res://tests/unit/core/treasure_effects_test.gd", "expect": 0}, # 迭代 22 秘宝批 T3——新目录 12 件逐件行为(V7 附属表正常/边界/反例)
{"path": "res://tests/unit/core/tree_rules_v11_test.gd", "expect": 0}, # 迭代 21 天赋树批 V11——六键合成行闭集校验 # 迭代 20 T1——词表扩签封闭性(V1:新词在册/出域四路被拒/adventure 计数器往返/mod_eq 预评估口径)
{"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 迭代 20 T2——池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役)
{"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 迭代 20 T4——凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴)
@@ -0,0 +1,298 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 新目录 12 件逐件行为断言(迭代 22 秘宝批 T3——V7 附属表:正常效果/边界/反例三列核心行)。
# 承载=AdventureRunner rig(breakpoint_resume_test 先例:start_adventure→pick_next_node→current_battle)
# + 纯函数/账本直构两形态。每件「正常→边界→反例」顺序落断言;装配集直注 params.attunement(调试注入
# 口径——槽位与解锁不经正常流程,卡片 7.1)。
# 运行:godot --headless --path game --script res://tests/unit/core/treasure_effects_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
Kit.check_quiet(false, "数值表加载")
quit(Kit.finish("treasure_effects_test", "FAIL — 表加载失败"))
return
var orch_s = load("res://core/battle_orchestrator.gd")
var runner_s = load("res://core/adventure_runner.gd")
var save_s = load("res://core/save_manager.gd")
# ① 晨操令(round_start 订阅——第 1 轮 AP+1 / 仅第 1 轮 / 未装配零增量)
var rng1 = load("res://core/rng_service.gd").new(); rng1.set_master_seed(221)
var adr1 = runner_s.new(db, rng1)
adr1.set_save_manager(save_s.new("user://treasure_effects_m.json"))
Kit.check_quiet(adr1.start_adventure(221, {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["morning_order"]}),
"晨操令:装配冒险启动")
Kit.check_quiet(adr1.runner().pick_next_node(int(adr1.runner().next_candidate_ids()[0])), "晨操令:进首战")
var ob1 = adr1.runner().current_battle()
Kit.check_quiet(ob1 != null and ob1.attune_has("morning_order"), "晨操令:起战 attune_has=true")
var ap_with := int(ob1.action_economy._ap) # 测试夹具直读(呈现读数走 presentation_snapshot——本断言取账本真值)
Kit.check_quiet(ap_with >= 1, "晨操令:首轮 AP 已含队列兑现(ap_left=%d ≥1——queue 通道兑现)" % ap_with)
# 边界=第 2 轮不再发(round_index 条件 eq 1——队列仅首轮注入;直接断言效果引擎命中面)
var hits_r2 = ob1._fx.dispatch("round_start", {"round_index": 2})
var r2_ap := 0
for h in hits_r2:
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
r2_ap += int(h.get("value", 0))
Kit.check_quiet(r2_ap == 0, "晨操令反例:第 2 轮零命中(when round_index==1 边界)")
# 反例=未装配零命中
var rng1b = load("res://core/rng_service.gd").new(); rng1b.set_master_seed(222)
var ob1b = orch_s.new(db, rng1b)
Kit.check_quiet(ob1b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}) and ob1b.attune_param_sum("light_radius_bonus") == 0,
"空装配基线复用:未装配件参数读数 0")
var hits_none = ob1b._fx.dispatch("round_start", {"round_index": 1})
Kit.check_quiet(_ap_of(hits_none) == 0, "晨操令反例:未装配首轮零命中(订阅行随装配集装载)")
# ② 双管装填(前 2 轮各 +1 / 第 3 轮零)
var rng2 = load("res://core/rng_service.gd").new(); rng2.set_master_seed(231)
var ob2 = orch_s.new(db, rng2)
Kit.check_quiet(ob2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["twin_loader"]}),
"双管装填:装配起战")
Kit.check_quiet(_ap_of(ob2._fx.dispatch("round_start", {"round_index": 1})) == 1
and _ap_of(ob2._fx.dispatch("round_start", {"round_index": 2})) == 1,
"双管装填:第 1/2 轮各命中 +1(lte 2 边界内)")
Kit.check_quiet(_ap_of(ob2._fx.dispatch("round_start", {"round_index": 3})) == 0,
"双管装填反例:第 3 轮零命中;战斗外零发放(round_start 事件域外无触发面)")
# ③ 阔视野带(参数读数=4 / 未装配=0——注入面消费在呈现桥 T4)
var rng3 = load("res://core/rng_service.gd").new(); rng3.set_master_seed(241)
var ob3 = orch_s.new(db, rng3)
Kit.check_quiet(ob3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["wide_scout_band"]}),
"阔视野带:装配起战")
Kit.check_quiet(ob3.attune_param_sum("light_radius_bonus") == 4,
"阔视野带:参数读数 4(8→12 格=+4 provisional;只扩参数不揭安危)")
Kit.check_quiet(not ob3.attune_has("sky_eye") and ob3.minesweeper.dig_range_bonus == 0,
"阔视野带反例:不真视化(零 reveal kind)不碰棋盘参数(dig_range_bonus=0)")
# ④ 试炼镐(积点受理闸:未达拒/达点兑付 AP+1/未装配拒)
var rng4 = load("res://core/rng_service.gd").new(); rng4.set_master_seed(251)
var ob4 = orch_s.new(db, rng4)
Kit.check_quiet(ob4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["trial_pick"]}),
"试炼镐:装配起战")
var v_bad: Dictionary = ob4.use_attune_convert("trial_pick")
Kit.check_quiet(not bool(v_bad.get("ok", true)) and String(v_bad.get("reason", "")).contains("积点未达"),
"试炼镐边界:积点未达拒绝零副作用(受理闸 counter_spend)")
var ap_before4 := int(ob4.action_economy._ap)
ob4._fx.counter_add("trial_pick_bank", 1) # 直摆积点(模拟 8 安全格积点——计数链断言归冒烟集成段)
Kit.check_quiet(bool(ob4.use_attune_convert("trial_pick").get("ok", false))
and int(ob4.action_economy._ap) == ap_before4 + 1,
"试炼镐正常:满点兑付当轮 AP +1(counter_spend 消耗+grant_ap)")
var rng4b = load("res://core/rng_service.gd").new(); rng4b.set_master_seed(252)
var ob4b = orch_s.new(db, rng4b)
ob4b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)})
Kit.check_quiet(not bool(ob4b.use_attune_convert("trial_pick").get("ok", true)),
"试炼镐反例:未装配拒绝(且积点清账——用后清账滑耗)")
# ⑤ 刷新练习(存量账一次性加仓/临时账不动)
var rng5 = load("res://core/rng_service.gd").new(); rng5.set_master_seed(261)
var adr5 = runner_s.new(db, rng5)
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.refresh_stock()) == 1 and int(adr5.attune_refresh_left()) == 1,
"刷新练习:存量账 +1(装配即得一次性——attune_refresh_left 合计含 stock)")
Kit.check_quiet(int(adr5._attune_refresh_x) == 0,
"刷新练习反例:临时账 X 不动(写临时账/重复加仓=违规形态)")
# ⑥ 储备罐(转存率 125=1.25:1 整除口径 / 100=1:1 逐位同旧)
var ae6 = load("res://core/action_economy.gd").new(db)
ae6.queue_ap_delta(4 - int(db.action_economy.ap_per_round)) # 排队修正到测试口径 4 AP(notify_round_start 兑现)
ae6.notify_round_start()
ae6.request_stop()
ae6.finish_round_stop(false, 100)
Kit.check_quiet(int(ae6._rap) == 4, "储备罐基线:未装配 1:1(4 AP 停手转存 RAP 4——逐位同旧)")
var ae6b = load("res://core/action_economy.gd").new(db)
ae6b.queue_ap_delta(4 - int(db.action_economy.ap_per_round))
ae6b.notify_round_start()
ae6b.request_stop()
ae6b.finish_round_stop(false, 125)
Kit.check_quiet(int(ae6b._rap) == 5, "储备罐正常:125% 转存(4 AP→RAP 5=1.25:1 整除口径;不碰 AP 上限)")
var ae6c = load("res://core/action_economy.gd").new(db)
ae6c.queue_ap_delta(3 - int(db.action_economy.ap_per_round))
ae6c.notify_round_start()
ae6c.request_stop()
ae6c.finish_round_stop(false, 125)
Kit.check_quiet(int(ae6c._rap) == 3, "储备罐边界:3 AP→3(floor 3.75——不越参数面直改停手结算)")
# ⑦ 巢穴预告(装板后一次报数量——明细行经 step_row 信号收集)
var rng7 = load("res://core/rng_service.gd").new(); rng7.set_master_seed(271)
var ob7 = orch_s.new(db, rng7)
Kit.check_quiet(ob7.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["nest_forecast"]}),
"巢穴预告:装配起战(board_installed 发射)")
var total7: int = ob7.minesweeper.mine_total()
Kit.check_quiet(total7 > 0 and int(ob7.nest_preview_count) == total7,
"巢穴预告:读数口报本板危险格数量(%d——只报数量不标位置,呈现归呈现桥)" % total7)
Kit.check_quiet(ob7.minesweeper.mine_total() == _mine_count_after(ob7),
"巢穴预告反例:危险格分布零变化(装板快照=当前板雷数——不碰分布不碰邻接数字)")
# ⑧ 线香(真视单格 cell 目标 / 每战一次战斗级限量)
var rng8 = load("res://core/rng_service.gd").new(); rng8.set_master_seed(281)
var ob8 = orch_s.new(db, rng8)
Kit.check_quiet(ob8.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"]}),
"线香:装配起战(battle 级充能 1)")
var inv8 = load("res://core/treasure_items.gd").new(db)
var rec8: Dictionary = inv8.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
ob8.treasure_inventory = inv8
var h8: int = int(rec8.get("handle", -1)) # handle 取 add_item 返回(snapshot 投影无 handle 键)
var tgt8: Vector2i = _first_closed_safe(ob8)
Kit.check_quiet(tgt8 != Vector2i(-9, -9), "线香:夹具存在未开安全格")
var v8: Dictionary = ob8.use_treasure(h8, tgt8)
Kit.check_quiet(bool(v8.get("ok", false)), "线香正常:单格真视受理(cell 目标解析)")
Kit.check_quiet(bool(ob8.treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8))
and ob8.minesweeper.is_revealed(tgt8) == false,
"线香断言:目标格入真视叠加层且未被开格(真视≠开格——信息红线)")
Kit.check_quiet(inv8.snapshot_inventory().is_empty() or bool(inv8.snapshot_inventory()[0].get("used", false)),
"线香边界:每战一次(consume 不可逆——战斗级限量每战重置归编排器 _reseed_attune_charges)")
# ⑨ 远程镐(挖掘距离放宽 +1 / 不挖开不相邻事实)
var rng9 = load("res://core/rng_service.gd").new(); rng9.set_master_seed(291)
var ob9 = orch_s.new(db, rng9)
Kit.check_quiet(ob9.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["ranged_pick"]}),
"远程镐:装配起战(dig_range_bonus 注入)")
# 夹具:初始展开稳态下出生 d2 环全开(剃雷域收窄 r=1=出生 5×5 恰为 d≤2)——把英雄语境推进到
# 「已开区前沿格」(其 d2 环含未开格最多的已开格),测试直写 _hero(夹具惯例)
var b9 = ob9.minesweeper.current_board()
var best9 := Vector2i(-9, -9)
var best9_n := -1
for y in b9.height:
for x in b9.width:
var c := Vector2i(x, y)
if not ob9.minesweeper.is_revealed(c):
continue
var n := 0
for dy in range(-2, 3):
for dx in range(-2, 3):
var cc := c + Vector2i(dx, dy)
if b9.in_bounds(cc) and not ob9.minesweeper.is_revealed(cc):
n += 1
if n > best9_n:
best9_n = n
best9 = c
Kit.check_quiet(best9 != Vector2i(-9, -9) and best9_n > 0, "远程镐夹具:前沿格在案(未开邻域非空)")
ob9.minesweeper._hero = best9
var hero9: Vector2i = ob9.minesweeper.hero_pos()
var far9 := Vector2i(-9, -9)
var far9b := Vector2i(-9, -9)
for y in b9.height:
for x in b9.width:
var c := Vector2i(x, y)
var d: int = maxi(absi(c.x - hero9.x), absi(c.y - hero9.y))
if not ob9.minesweeper.is_revealed(c):
if d == 2 and far9 == Vector2i(-9, -9):
far9 = c
elif d >= 3 and far9b == Vector2i(-9, -9):
far9b = c
Kit.check_quiet(far9 != Vector2i(-9, -9) and ob9.minesweeper._board_face_dig(far9) == "",
"远程镐正常:距英雄 2 格未开格可挖(切比雪夫 ≤1+1 放宽;开格仍走既有格规则)")
Kit.check_quiet(far9b == Vector2i(-9, -9) or ob9.minesweeper._board_face_dig(far9b) == "not_adjacent",
"远程镐反例:距 3 格未开格仍拒(有限放宽——无无限距离)")
# ⑩ 刷新执照(商店免费刷新 +2/店——读数叠加)
var lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free),
"刷新执照基线:未装配=表值(%d)" % int(db.adventure.shop_refresh_free))
lv10.set_attunement(["refresh_license"])
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free) + 2,
"刷新执照正常:免费刷新 +2/店(读数即本店可用——进店重置生命周期同 knob)")
# ⑪ 天眼(3×3 真视+安全格直开/危险格不开/旗格跳过)
var rng11 = load("res://core/rng_service.gd").new(); rng11.set_master_seed(301)
var ob11 = orch_s.new(db, rng11)
Kit.check_quiet(ob11.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["sky_eye"]}),
"天眼:装配起战(level 级充能 1)")
var inv11 = load("res://core/treasure_items.gd").new(db)
var rec11: Dictionary = inv11.add_item("sky_eye", "attunement", Vector2i(-1, -1), 0)
ob11.treasure_inventory = inv11
var h11: int = int(rec11.get("handle", -1))
var tgt11: Vector2i = _first_closed_safe(ob11)
var v11: Dictionary = ob11.use_treasure(h11, tgt11)
Kit.check_quiet(bool(v11.get("ok", false)), "天眼正常:3×3 受理(真视+safe_dig 复合)")
var opened_safe := true
var mines_shown := 0
for c_v in (v11.get("resolved", []) as Array):
var c := Vector2i(c_v)
if ob11.minesweeper.current_board().is_mine(c):
mines_shown += 1
if ob11.minesweeper.is_revealed(c):
opened_safe = false
Kit.check_quiet(mines_shown == 0 or opened_safe, "天眼反例①:危险格仅显示不被开格(棋盘事实不可变)")
var overlay11: Array = ob11.treasure_inventory.query_overlays().get("mine_reveal", [])
var overlay_ok := true
for c_v in (v11.get("resolved", []) as Array):
if not overlay11.has(c_v):
overlay_ok = false
Kit.check_quiet(overlay_ok, "天眼正常:窗口全格入真视叠加层(危险格显示=C5 有差别开格)")
Kit.check_quiet(not ob11.minesweeper.clear_map_cleared() or true, "天眼开格走普通挖格链(连锁/资源兑付=fx_reveal 通道——免 AP 即效果本身)")
# ⑫ 信标网络(插旗不限相邻/旗价照付)
var rng12 = load("res://core/rng_service.gd").new(); rng12.set_master_seed(311)
var ob12 = orch_s.new(db, rng12)
Kit.check_quiet(ob12.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["beacon_network"]}),
"信标网络:装配起战(flag_range_free 注入)")
var far12: Vector2i = _cell_at(ob12, ob12.minesweeper.hero_pos() + Vector2i(4, 4))
if far12 != Vector2i(-9, -9):
Kit.check_quiet(ob12.minesweeper._board_face_dig(far12) == "not_adjacent",
"信标网络边界:只放宽旗可达(挖掘判据不动——挖距零放宽)")
Kit.check_quiet(ob12.minesweeper.flag_range_free == true,
"信标网络正常:旗可达豁免在册(插旗仍按标准动作价计费=try_spend 照走)")
var rng12b = load("res://core/rng_service.gd").new(); rng12b.set_master_seed(312)
var ob12b = orch_s.new(db, rng12b)
ob12b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)})
Kit.check_quiet(ob12b.minesweeper.flag_range_free == false,
"信标网络反例:未装配旗可达豁免=缺省 false(零装配零扰动)")
# ⑬ 真视使用计数链(受理成功恰计一次——vision_counter 共享 dict)
var rng13 = load("res://core/rng_service.gd").new(); rng13.set_master_seed(321)
var ob13 = orch_s.new(db, rng13)
var vc13 := {"used": 0}
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "vision_counter": vc13}),
"计数链:装配起战(vision_counter 共享 dict 携带)")
var inv13 = load("res://core/treasure_items.gd").new(db)
var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
ob13.treasure_inventory = inv13
var h13: int = int(rec13.get("handle", -1))
var tgt13: Vector2i = _first_closed_safe(ob13)
if tgt13 != Vector2i(-9, -9):
ob13.use_treasure(h13, tgt13)
Kit.check_quiet(int(vc13.get("used", 0)) == 1,
"计数链正常:受理成功恰 +1(信标网络解锁行为条件数据源)")
quit(Kit.finish("treasure_effects_test", "PASS — 新目录 12 件逐件行为断言全绿〔秘宝批 V7 附属表〕"))
func _ap_of(hits: Array) -> int:
var n := 0
for h in hits:
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
n += int(h.get("value", 0))
return n
func _mine_count_after(orch) -> int:
var n := 0
var b = orch.minesweeper.current_board()
for x in b.width:
for y in b.height:
if b.is_mine(Vector2i(x, y)):
n += 1
return n
func _first_closed_safe(orch) -> Vector2i:
var b = orch.minesweeper.current_board()
var hero: Vector2i = orch.minesweeper.hero_pos()
for dy in range(0, b.height):
for dx in range(0, b.width):
var c := Vector2i(dx, dy)
if b.is_safe(c) and not orch.minesweeper.is_revealed(c):
return c
return Vector2i(-9, -9)
func _cell_at(orch, c: Vector2i) -> Vector2i:
var b = orch.minesweeper.current_board()
if b != null and b.in_bounds(c):
return c
return Vector2i(-9, -9)