合并(迭代51): 四卡合回——①EffectPort蓄力层删键(biz_fact闭面四键/finish_round_stop纯RAP转存/零残留)②5.1封装修复(settlement七段束注入对编排器零引用/搬家五方法归位/断点存档双向公开面/golden同种子逐位零漂移)③P2 NODE_TYPES词表单点化④L1深度3混编(地精+熊地精两行/行值整行复用/静态骰和3); 独立验收审计终判通过(高档全量,S13一键经正规通道--refresh记基线收口); 受影响套件全绿+冒烟绿; post-merge钩子推送触发远端CI
ci / gates (push) Failing after 10m42s

This commit is contained in:
DKChan committed 2026-11-04 05:08:53 +08:00
commit fb27dc2fca
50 files changed
+1784 -473

No files matched your search

+9 -22
View File
@@ -11,7 +11,6 @@ signal round_end_stopped(payload: Dictionary) ## 双发射源之停手路径
const PHASE_IDLE := "Idle" const PHASE_IDLE := "Idle"
const PHASE_ACTIVE := "Active" const PHASE_ACTIVE := "Active"
const PHASE_WAITING := "Waiting" const PHASE_WAITING := "Waiting"
const CHARGE_LAYER_CAP := 3 # 蓄力层上限(机制结构常量,非玩法表值——整备军备面)
# 相位轴:内部 enum + match,外部 phase getter 保持 # 相位轴:内部 enum + match,外部 phase getter 保持
# String 面(presentation_snapshot 与冒烟链的 "Idle"/"Active"/"Waiting" 对照契约)。 # String 面(presentation_snapshot 与冒烟链的 "Idle"/"Active"/"Waiting" 对照契约)。
@@ -32,15 +31,10 @@ var rap: int:
var _last_round_end: Dictionary[String, String] = {} # {path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源 var _last_round_end: Dictionary[String, String] = {} # {path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源
var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start
var _pending_ap_delta := 0 # 下轮行动点修正排队(action 型 ±1 排队,轮开始兑现后清零) var _pending_ap_delta := 0 # 下轮行动点修正排队(action 型 ±1 排队,轮开始兑现后清零)
# 蓄力流运行态:转蓄力=round_end grant_ap(mode:transfer) 订阅(finish_round_stop 定夺)、 # 效果端口件(dig_paid/dig_stop 发射面经端口路由宿主;组装期单点注入——
# 深挖对价=dig_paid counter_spend 订阅(request_dig 施加面);charge_layers=蓄力层账本
var _charge_layers := 0
var charge_layers: int:
get: return _charge_layers
# 效果端口件(dig_paid/dig_stop/charge_full 发射面经端口路由宿主;组装期单点注入——
# host _init bind_port;缺省 null=未绑零扰动,与原 _fx==null 守卫同语义) # host _init bind_port;缺省 null=未绑零扰动,与原 _fx==null 守卫同语义)
var _port: EffectPort = null var _port: EffectPort = null
var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策在 round_end 发射点——grant_ap(transfer) 订阅定夺) var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策在 round_end 发射点)
# 效果端口件注入(host _init 组装期单点——先于 begin_battle 任何发射) # 效果端口件注入(host _init 组装期单点——先于 begin_battle 任何发射)
@@ -165,9 +159,9 @@ func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool:
# 停手:剩余 AP 1:1 转存 RAP;行动经济 直发 stopped 路径 round_end(双发射源)。 # 停手:剩余 AP 1:1 转存 RAP;行动经济 直发 stopped 路径 round_end(双发射源)。
# 转蓄力层〔上限 3〕替代 RAP 转存——「不动也是行动」;cleared 分支 AP 恒清零维持作废语义。 # cleared 分支 AP 恒清零维持作废语义。
# 本方法只记账——剩余 AP 存 _last_stop_ap 后清零,转存决策在 round_end 发射点 # 本方法只记账——剩余 AP 存 _last_stop_ap 后清零,转存决策在 round_end 发射点
# finish_round_stop(grant_ap(mode:transfer) 订阅定夺)。 # finish_round_stop(转存率定夺)。
func request_stop() -> bool: func request_stop() -> bool:
if _phase != Phase.ACTIVE: if _phase != Phase.ACTIVE:
_reject("phase") _reject("phase")
@@ -193,18 +187,11 @@ func grant_reserve_ap(n: int) -> void:
_rap += maxi(0, n) _rap += maxi(0, n)
# 轮末停手转存决策(round_end 发射点调用——编排器授予段尾):transfer=true=剩余 AP 转蓄力层 # 轮末停手 RAP 转存(round_end 发射点调用——编排器授予段尾):按转存率转存 RAP
# (上限 CHARGE_LAYER_CAP,整备军备面);false=按转存率转存 RAP(transfer_pct 百分比口径缺省 100=1:1; # (transfer_pct 百分比口径缺省 100=1:1;储备罐 rap_transfer_mult magnitude 125=1.25:1,
# 储备罐 rap_transfer_mult magnitude 125=1.25:1,整数除法向下取整=每 4 AP 多转 1)。 # 整数除法向下取整=每 4 AP 多转 1)。非停手轮(cleared/耗尽)_last_stop_ap=0 零操作。
# 非停手轮(cleared/耗尽)_last_stop_ap=0 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。 func finish_round_stop(transfer_pct: int = 100) -> void:
func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void: _rap += _last_stop_ap * maxi(1, transfer_pct) / 100
if transfer:
var prev_layers := _charge_layers
_charge_layers = mini(CHARGE_LAYER_CAP, _charge_layers + _last_stop_ap)
if _port != null and _charge_layers >= CHARGE_LAYER_CAP and prev_layers < CHARGE_LAYER_CAP:
_port.emit("charge_full", {"layers": _charge_layers})
else:
_rap += _last_stop_ap * maxi(1, transfer_pct) / 100
_last_stop_ap = 0 _last_stop_ap = 0
+34 -32
View File
@@ -104,7 +104,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
_reset_curio_state() _reset_curio_state()
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数) levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
level_index = 1 level_index = 1
_bp_store._pending_next_level = false _bp_store.set_pending_next_level(false)
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 缺省 legacy(off 对照零扰动) _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 {} _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 _attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
@@ -112,7 +112,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
_refresh_bank.set_refresh_y(maxi(0, int(params.get("attune_refresh_y", 0)))) # additive(Y 冒险周期池) _refresh_bank.set_refresh_y(maxi(0, int(params.get("attune_refresh_y", 0)))) # additive(Y 冒险周期池)
_loadout.set_attunement(_attunement) # 装配集注入(活引用——种行面) _loadout.set_attunement(_attunement) # 装配集注入(活引用——种行面)
adventure_inventory.reset() # 冒险级库存重置(新局空库存) adventure_inventory.reset() # 冒险级库存重置(新局空库存)
_bp_store._loadout_seeded = false # 同调行投行延至首关 bind 时点(库存就位后) _bp_store.set_loadout_seeded(false) # 同调行投行延至首关 bind 时点(库存就位后)
adventure_counters = {} # 冒险桶清零(效果引擎 adv_ 计数器宿主——冒险开始归零) adventure_counters = {} # 冒险桶清零(效果引擎 adv_ 计数器宿主——冒险开始归零)
# 刷新练习开局注入(refresh_stock_grant 装配即得一次性加仓: # 刷新练习开局注入(refresh_stock_grant 装配即得一次性加仓:
# 装配集 kind 分发扫描;只入存量账、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕) # 装配集 kind 分发扫描;只入存量账、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕)
@@ -154,7 +154,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
summary = {} summary = {}
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数) levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
level_index = int(bp.get("level_index", 1)) level_index = int(bp.get("level_index", 1))
_bp_store._pending_next_level = false _bp_store.set_pending_next_level(false)
_reset_curio_state() # 续玩三选一态回填前置清理(池与件计数归件、停点态归本类;桶归宿主字典) _reset_curio_state() # 续玩三选一态回填前置清理(池与件计数归件、停点态归本类;桶归宿主字典)
_bp_store.restore_curio_state(bp) # 续玩三选一态回填(池余库/获取计数/特性持有集——件侧;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS) _bp_store.restore_curio_state(bp) # 续玩三选一态回填(池余库/获取计数/特性持有集——件侧;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS)
_bp_store.apply_resume_params(params) # 续玩参数面——数值账规范化与越界钳制(件侧) _bp_store.apply_resume_params(params) # 续玩参数面——数值账规范化与越界钳制(件侧)
@@ -165,7 +165,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
_bp_store.restore_stop_bank_fragments(bp, params) # 停点件/刷新账片段回填(件侧——凭证计数;刷新存量/Y 池缺键回退参数面) _bp_store.restore_stop_bank_fragments(bp, params) # 停点件/刷新账片段回填(件侧——凭证计数;刷新存量/Y 池缺键回退参数面)
_tree_rules = params.get("tree_rules", {}) if params.get("tree_rules", {}) is Dictionary else {} # A/B 面规则键参数字典(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传) _tree_rules = params.get("tree_rules", {}) if params.get("tree_rules", {}) is Dictionary else {} # A/B 面规则键参数字典(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传)
# 续玩计数器回填(adventure 域计数——缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先——桶归宿主字典) # 续玩计数器回填(adventure 域计数——缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先——桶归宿主字典)
var bp_counters = _bp_store._bp_read("adventure_counters", bp) var bp_counters = _bp_store.bp_read("adventure_counters", bp)
if bp_counters is Dictionary: if bp_counters is Dictionary:
adventure_counters = (bp_counters as Dictionary).duplicate(true) adventure_counters = (bp_counters as Dictionary).duplicate(true)
return _resume_pending_or_start(bp) return _resume_pending_or_start(bp)
@@ -177,18 +177,18 @@ func _resume_pending_or_start(bp: Dictionary) -> bool:
var bp_pending: Dictionary[Variant, Variant] = bp.get("bonus_pending", {}) as Dictionary var bp_pending: Dictionary[Variant, Variant] = bp.get("bonus_pending", {}) as Dictionary
var bp_result: Dictionary[Variant, Variant] = bp.get("bonus_result", {}) as Dictionary var bp_result: Dictionary[Variant, Variant] = bp.get("bonus_result", {}) as Dictionary
if not bp_pending.is_empty(): if not bp_pending.is_empty():
_bp_store._pending_next_level = true _bp_store.set_pending_next_level(true)
_state = State.RUNNING _state = State.RUNNING
_stop.restore_pending(bp_pending) # 选择停点直回(续玩不重跑关卡——信号经停点件中继) _stop.restore_pending(bp_pending) # 选择停点直回(续玩不重跑关卡——信号经停点件中继)
return true return true
if not bp_result.is_empty(): if not bp_result.is_empty():
_bp_store._pending_next_level = true _bp_store.set_pending_next_level(true)
_stop.restore_resolved(bp_result) # 已兑付不重兑付只重发(置态+重发收编一步——信号面归停点件) _stop.restore_resolved(bp_result) # 已兑付不重兑付只重发(置态+重发收编一步——信号面归停点件)
_state = State.RUNNING _state = State.RUNNING
return true return true
_bp_store._resume_bp = bp.duplicate(true) _bp_store.set_resume_bp(bp.duplicate(true))
# 充能读侧以行集为源(键值仅校验读数——回填时点在首关 bind 行集就位后,经 _loadout_on_level_inventory) # 充能读侧以行集为源(键值仅校验读数——回填时点在首关 bind 行集就位后,经 _loadout_on_level_inventory)
_bp_store._loadout_seeded = false _bp_store.set_loadout_seeded(false)
_loadout.set_attunement(_attunement) _loadout.set_attunement(_attunement)
adventure_inventory.reset() adventure_inventory.reset()
_state = State.RUNNING _state = State.RUNNING
@@ -198,7 +198,7 @@ func _resume_pending_or_start(bp: Dictionary) -> bool:
func _reset_curio_state() -> void: func _reset_curio_state() -> void:
_curio.reset() # 池与件获取计数(池半归件;停点态归停点件下行清理) _curio.reset() # 池与件获取计数(池半归件;停点态归停点件下行清理)
_stop.reset_adventure_state() _stop.reset_adventure_state()
_bp_store._loadout_seeded = false _bp_store.set_loadout_seeded(false)
_stats.feats = [] _stats.feats = []
adventure_counters = {} adventure_counters = {}
@@ -217,7 +217,7 @@ func effective_max_hp() -> int:
# 断点承载注入(组合根冒险开始时调用;模拟/冒烟不注入 = 断点面整体静默) # 断点承载注入(组合根冒险开始时调用;模拟/冒烟不注入 = 断点面整体静默)
func set_save_manager(sm: SaveManagerCore) -> void: func set_save_manager(sm: SaveManagerCore) -> void:
_bp_store._save_manager = sm _bp_store.bind_save_manager(sm)
# 当前关卡行进器读面(呈现层/冒烟用——每关新建实例,调用方须在 level_started 后重取) # 当前关卡行进器读面(呈现层/冒烟用——每关新建实例,调用方须在 level_started 后重取)
@@ -232,13 +232,13 @@ func apply_rest(pct: int, hp: int, max_hp: int) -> int:
# 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;三选一未兑付拒绝推进) # 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;三选一未兑付拒绝推进)
func continue_to_next_level() -> bool: func continue_to_next_level() -> bool:
if not _bp_store._pending_next_level: if not _bp_store.pending_next_level():
push_error("[冒险结构] continue_to_next_level 拒绝——无待确认的关卡推进") push_error("[冒险结构] continue_to_next_level 拒绝——无待确认的关卡推进")
return false return false
if _stop.curio_stop_pending(): if _stop.curio_stop_pending():
push_error("[冒险结构] continue_to_next_level 拒绝——三选一停点未兑付(零自动推进契约)") push_error("[冒险结构] continue_to_next_level 拒绝——三选一停点未兑付(零自动推进契约)")
return false return false
_bp_store._pending_next_level = false _bp_store.set_pending_next_level(false)
_stop.clear_resolved() _stop.clear_resolved()
level_index += 1 level_index += 1
return _start_current_level() return _start_current_level()
@@ -297,9 +297,9 @@ func _loadout_on_level_inventory() -> void:
var inv = _runner.treasure_inventory var inv = _runner.treasure_inventory
if inv == null: if inv == null:
return return
if not _bp_store._loadout_seeded: if not _bp_store.loadout_seeded():
_bp_store._loadout_seeded = true _bp_store.set_loadout_seeded(true)
var bp_charges: Variant = _bp_store._bp_read("attunement_charges", _bp_store._resume_bp, null) if not _bp_store._resume_bp.is_empty() else null var bp_charges: Variant = _bp_store.bp_read("attunement_charges", _bp_store.resume_bp(), null) if not _bp_store.resume_bp().is_empty() else null
if bp_charges is Dictionary: if bp_charges is Dictionary:
_loadout.restore_fragment({"attunement_charges": bp_charges}, inv) # 旧档读侧:行集为源+键值校验读数 _loadout.restore_fragment({"attunement_charges": bp_charges}, inv) # 旧档读侧:行集为源+键值校验读数
else: else:
@@ -322,8 +322,9 @@ func _loadout_on_level_inventory() -> void:
# apply_adventure_summary 逐关卡完成关次置位〕;末关 Boss 照常入账;明细随摘要 additive 键供结算面归因) # apply_adventure_summary 逐关卡完成关次置位〕;末关 Boss 照常入账;明细随摘要 additive 键供结算面归因)
func _boss_keystone_drop(li: int) -> void: func _boss_keystone_drop(li: int) -> void:
var first := true var first := true
if _bp_store._save_manager != null: var sm_keystone = _bp_store.save_manager()
first = not _bp_store._save_manager.meta_state.career_state().boss_first_kill.has(str(li)) if sm_keystone != null:
first = not sm_keystone.meta_state.career_state().boss_first_kill.has(str(li))
var row: Dictionary[Variant, Variant] = db.crossrun.meta.boss_keystone_row(li) var row: Dictionary[Variant, Variant] = db.crossrun.meta.boss_keystone_row(li)
var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0)) var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0))
_journal.record_keystone_drop({"level": li, "first": first, "amount": amount}) # 逐杀明细+累计单写原语 _journal.record_keystone_drop({"level": li, "first": first, "amount": amount}) # 逐杀明细+累计单写原语
@@ -373,9 +374,9 @@ func _start_current_level() -> bool:
# 断点续玩分支:图由主种子+参数档重生成同图(产图与计数器号位无关,停点零重抽语义随之消解), # 断点续玩分支:图由主种子+参数档重生成同图(产图与计数器号位无关,停点零重抽语义随之消解),
# resume_state 带停点句柄与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。 # resume_state 带停点句柄与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。
var resume_state: Dictionary[Variant, Variant] = {} var resume_state: Dictionary[Variant, Variant] = {}
if not _bp_store._resume_bp.is_empty(): if not _bp_store.resume_bp().is_empty():
resume_state = {"node_id": int(_bp_store._bp_read("node_id", _bp_store._resume_bp, -1)), resume_state = {"node_id": int(_bp_store.bp_read("node_id", _bp_store.resume_bp(), -1)),
"visited_ids": _bp_store._bp_read("visited_ids", _bp_store._resume_bp, []) as Array} "visited_ids": _bp_store.bp_read("visited_ids", _bp_store.resume_bp(), []) as Array}
# 接口①开始关卡:生命经主角账共享交接(账面当前值即开局生命——冒险开始置满 / 大奖励回满已写好); # 接口①开始关卡:生命经主角账共享交接(账面当前值即开局生命——冒险开始置满 / 大奖励回满已写好);
# 主角账三件与冒险桶注入(构造注入 ctx——拆件批 T3 收口形态)。 # 主角账三件与冒险桶注入(构造注入 ctx——拆件批 T3 收口形态)。
# defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在 # defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在
@@ -385,7 +386,7 @@ func _start_current_level() -> bool:
if ok: if ok:
_emit_level_started() _emit_level_started()
_present_level_entry(resume_state) _present_level_entry(resume_state)
_bp_store._resume_bp = {} _bp_store.clear_resume_bp()
return ok return ok
@@ -409,7 +410,7 @@ func _present_level_entry(resume_state: Dictionary) -> void:
if resume_state.is_empty() or int(resume_state.get("node_id", -1)) < 0: if resume_state.is_empty() or int(resume_state.get("node_id", -1)) < 0:
_runner.present_entry_choice() _runner.present_entry_choice()
else: else:
_runner.restore_treasure_items(_bp_store._bp_read("aux_items", _bp_store._resume_bp, []) as Array) _runner.restore_treasure_items(_bp_store.bp_read("aux_items", _bp_store.resume_bp(), []) as Array)
_runner.present_resumed_choice() _runner.present_resumed_choice()
@@ -418,7 +419,7 @@ func _on_level_finished(payload: Dictionary) -> void:
if state != "running": if state != "running":
push_error("[冒险结构] 重复关卡结束宣布拒绝——state=%s(账本与摘要零变化)" % state) push_error("[冒险结构] 重复关卡结束宣布拒绝——state=%s(账本与摘要零变化)" % state)
return return
_bp_store._last_level_payload = payload.duplicate(true) _bp_store.set_last_level_payload(payload.duplicate(true))
var result := String(payload.get("result", "")) var result := String(payload.get("result", ""))
if result == "defeat": if result == "defeat":
_stats.hero_hp = 0 # 倒下记 0(生命账本战后回写口径) _stats.hero_hp = 0 # 倒下记 0(生命账本战后回写口径)
@@ -427,7 +428,7 @@ func _on_level_finished(payload: Dictionary) -> void:
if result != "victory": if result != "victory":
push_error("[冒险结构] 关卡结束结果出域: %s" % result) push_error("[冒险结构] 关卡结束结果出域: %s" % result)
return return
_bp_store._last_treasure_cleared = int(payload.get("aux_cleared", 0)) # additive(末关 summary 取数面) _bp_store.set_last_treasure_cleared(int(payload.get("aux_cleared", 0))) # additive(末关 summary 取数面)
# Boss 钥石逐杀掉落(击杀掉落渠道,末关照常入账;首杀/复杀读 meta.tres boss_keystone) # Boss 钥石逐杀掉落(击杀掉落渠道,末关照常入账;首杀/复杀读 meta.tres boss_keystone)
_boss_keystone_drop(level_index) _boss_keystone_drop(level_index)
if level_index < levels_total: if level_index < levels_total:
@@ -435,7 +436,7 @@ func _on_level_finished(payload: Dictionary) -> void:
#(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位); #(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位);
# 停点件内部分回血先行 → 加权洗牌掷 3 → 三选一停点(顺序即契约——回血按当刻账本与上限结算, # 停点件内部分回血先行 → 加权洗牌掷 3 → 三选一停点(顺序即契约——回血按当刻账本与上限结算,
# 选择停点零自动推进)。载荷_base 随附钱包读数(停点件零钱包依赖——GP 件入账回调仍走 grant_curio_gp) # 选择停点零自动推进)。载荷_base 随附钱包读数(停点件零钱包依赖——GP 件入账回调仍走 grant_curio_gp)
_bp_store._pending_next_level = true _bp_store.set_pending_next_level(true)
var stop_base: Dictionary[Variant, Variant] = payload.duplicate(true) var stop_base: Dictionary[Variant, Variant] = payload.duplicate(true)
stop_base["gp"] = _wallet.gp stop_base["gp"] = _wallet.gp
_stop.open_level_clear(stop_base) _stop.open_level_clear(stop_base)
@@ -453,7 +454,7 @@ func _settle(result: String) -> void:
"stopped_at": 0 if result == "completed" else level_index, "stopped_at": 0 if result == "completed" else level_index,
"keystone_total": _journal.keystone_gained(), "keystone_total": _journal.keystone_gained(),
"master_seed": master_seed, "master_seed": master_seed,
"aux_cleared_last": _bp_store._last_treasure_cleared, # additive 扩签(末关清空反馈;非末关走 bonus 载荷) "aux_cleared_last": _bp_store.last_treasure_cleared(), # additive 扩签(末关清空反馈;非末关走 bonus 载荷)
"keystone_drops": _journal.keystone_drop_log(), # additive 扩签(逐杀掉落明细;settle 入账明细/复盘归因数据源) "keystone_drops": _journal.keystone_drop_log(), # additive 扩签(逐杀掉落明细;settle 入账明细/复盘归因数据源)
"vision_used_delta": _journal.vision_used(), # additive 扩签(真视使用本局计数;settle 聚合入档) "vision_used_delta": _journal.vision_used(), # additive 扩签(真视使用本局计数;settle 聚合入档)
"pool_stock": _pool_stock_snapshot(), # additive 扩签(终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面) "pool_stock": _pool_stock_snapshot(), # additive 扩签(终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面)
@@ -461,19 +462,20 @@ func _settle(result: String) -> void:
} }
# 败退明细(数值验证工具取数面——tools/balance_verifier.py 止步节点分布数据源; # 败退明细(数值验证工具取数面——tools/balance_verifier.py 止步节点分布数据源;
# 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知) # 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知)
if result == "failed" and not _bp_store._last_level_payload.is_empty(): if result == "failed" and not _bp_store.last_level_payload().is_empty():
summary["defeat"] = { summary["defeat"] = {
"level_index": level_index, "level_index": level_index,
"node_id": int(_bp_store._last_level_payload.get("node_id", -1)), "node_id": int(_bp_store.last_level_payload().get("node_id", -1)),
"node_type": String(_bp_store._last_level_payload.get("node_type", "")), "node_type": String(_bp_store.last_level_payload().get("node_type", "")),
} }
if _runner != null: if _runner != null:
summary["gp_final"] = int(_runner.gp) # gp_final 清账前快照(终局面板取数源=runner 钱包单点) summary["gp_final"] = int(_runner.gp) # gp_final 清账前快照(终局面板取数源=runner 钱包单点)
_runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现) _runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现)
_reset_curio_state() # 三选一停点态收束清零(结算受理只算一次语义——重复宣布零变化) _reset_curio_state() # 三选一停点态收束清零(结算受理只算一次语义——重复宣布零变化)
if _bp_store._save_manager != null: var sm_end = _bp_store.save_manager()
_bp_store._save_manager.clear_breakpoint() # 三结局清断点(退出冒险=放弃流程+清除断点) if sm_end != null:
_bp_store._save_manager.write_to_disk() sm_end.clear_breakpoint() # 三结局清断点(退出冒险=放弃流程+清除断点)
sm_end.write_to_disk()
_state = State.SETTLING _state = State.SETTLING
adventure_finished.emit(summary) adventure_finished.emit(summary)
_state = State.IDLE _state = State.IDLE
+21 -9
View File
@@ -1,5 +1,5 @@
## 效果引擎宿主件(自 battle_orchestrator 收编 fx 族):事件发射面(fx_emit——18 发射点统一入口)、 ## 效果引擎宿主件(自 battle_orchestrator 收编 fx 族):事件发射面(fx_emit——18 发射点统一入口)、
## 补充事实判定(charge_layers 自答,余五事实转宿主)、主角 AC 三值累积与同步(本轮/本战累积器+ ## 补充事实判定(biz_fact 闭面四键)、主角 AC 三值累积与同步(本轮/本战累积器+
## 基线快照)、bonus_delta 四轴累积(战末宿主 drain 汇入账本)、揭示待兑队列与兑付(含再入闸—— ## 基线快照)、bonus_delta 四轴累积(战末宿主 drain 汇入账本)、揭示待兑队列与兑付(含再入闸——
## 同步信号回环防二次缓冲二次兑付)、round/battle 桶计数器持有(桶随编排器生灭语义不变)、 ## 同步信号回环防二次缓冲二次兑付)、round/battle 桶计数器持有(桶随编排器生灭语义不变)、
## round_start 发射与施加、订阅重建、桶挂引用。生命周期=随编排器(编排器 _init 同建同灭,每战新建)。 ## round_start 发射与施加、订阅重建、桶挂引用。生命周期=随编排器(编排器 _init 同建同灭,每战新建)。
@@ -34,7 +34,7 @@ var ac := {"base": -1, "round": 0, "battle": 0} # ac 束(base=主角 AC 基
var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 本战 bonus_delta 累积(战末宿主 bonus_delta_drain 读后清零) var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 本战 bonus_delta 累积(战末宿主 bonus_delta_drain 读后清零)
var _fx_new_resource_types := {} # 本战已见资源类型集(地质学家 fact new_resource_type 判定——battle 域) var _fx_new_resource_types := {} # 本战已见资源类型集(地质学家 fact new_resource_type 判定——battle 域)
var reveal := {"queue": [], "busy": false} # reveal 束(queue=揭示效果待兑队列〔见血开窍/斩首学者/地质学家/跳格采掘共用——授予段末统一兑付,防轮 facts 迭代中追加重入〕;busy=兑付中标记——宿主 _on_cell_revealed 缓冲旁路判定,防二次缓冲二次兑付) var reveal := {"queue": [], "busy": false} # reveal 束(queue=揭示效果待兑队列〔见血开窍/斩首学者/地质学家/跳格采掘共用——授予段末统一兑付,防轮 facts 迭代中追加重入〕;busy=兑付中标记——宿主 _on_cell_revealed 缓冲旁路判定,防二次缓冲二次兑付)
var facts_inject := {"dig_streak": 0, "rider_tier_bonus": 0, "rider_tier_crit": false} # 业务发射事实束(键集封闭——原 damage.inject 运行态束迁宿主:dig_streak=combat_segment 每轮写;rider 两键=begin_battle 每战复位;charge_layers 不在束内=活读 action_economy) var facts_inject := {"dig_streak": 0, "rider_tier_bonus": 0, "rider_tier_crit": false} # 业务发射事实束(键集封闭——原 damage.inject 运行态束迁宿主:dig_streak=combat_segment 每轮写;rider 两键=begin_battle 每战复位)
func _init(bundle: Dictionary) -> void: func _init(bundle: Dictionary) -> void:
@@ -89,13 +89,27 @@ func counter_add(counter_id: String, value: int, set_form: bool) -> void:
EffectEngine.counter_add(counters_ctx, counter_id, value, set_form) EffectEngine.counter_add(counters_ctx, counter_id, value, set_form)
# 业务发射事实面(闭面五键——三 when 事实 {charge_layers 活读/dig_streak 轮内槽/hp_ratio 主角 # 新资源类型首见登记(授予段 dig_reveal 地质学家判定面——返回 true=首见并置位;空串短路零登记)
func mark_new_resource_type(ctype: String) -> bool:
if ctype == "":
return false
if _fx_new_resource_types.has(ctype):
return false
_fx_new_resource_types[ctype] = true
return true
# bonus_delta 轴累积(授予段血税双向施加面——axis 在册四轴才累积,轴外静默)
func add_bonus_delta(axis: String, delta: int) -> void:
if _fx_bonus_delta.has(axis):
_fx_bonus_delta[axis] += delta
# 业务发射事实面(闭面四键——三 when 事实 {dig_streak 轮内槽/hp_ratio 主角
# 血量比例算式逐字承 damage_controller 原式}∪ 两 rider 运行态读数 {facts_inject 现值}; # 血量比例算式逐字承 damage_controller 原式}∪ 两 rider 运行态读数 {facts_inject 现值};
# 其余事实一律 null——battle_index/round_index/seals_count/nest_count 禁进,扩面=行为变化) # 其余事实一律 null——battle_index/round_index/seals_count/nest_count 禁进,扩面=行为变化)
func biz_fact(name: String) -> Variant: func biz_fact(name: String) -> Variant:
match name: match name:
"charge_layers":
return action_economy.charge_layers
"dig_streak": "dig_streak":
return int(facts_inject["dig_streak"]) return int(facts_inject["dig_streak"])
"hp_ratio": "hp_ratio":
@@ -216,11 +230,9 @@ func reveal_busy() -> bool:
return bool(reveal.busy) return bool(reveal.busy)
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数;charge_layers 本件自答 # 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数;
# 〔action_economy 直读〕,余五事实转宿主 facts_cb——宿主侧单点,未知事实回落 null) # 全部转宿主 facts_cb——宿主侧单点,未知事实回落 null)
func _fx_fact(fact: String) -> Variant: func _fx_fact(fact: String) -> Variant:
if fact == "charge_layers":
return action_economy.charge_layers
return facts_cb.call(fact) return facts_cb.call(fact)
+22 -65
View File
@@ -65,12 +65,6 @@ var nest_preview_count := -1 # 巢穴预告读数口(board_insta
# 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动) # 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动)
# 主角攻击基值派生读(enc 携带 hero_atk 键受理后只读——E4 收口语义不变:表值运行期不可变,
# 派生值恒等;「读面即引用、无第二账」同款先例)
func _hero_atk() -> int:
return int(_facts.encounter.get("hero_atk", int(db.combat.hero.atk)))
# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter 受理 assign 后全生命期可读; # 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter 受理 assign 后全生命期可读;
# null=调试/单场直构面,账面消费跳过。领域拆分批成员删除后的引用承接,读面即引用、无第二账) # null=调试/单场直构面,账面消费跳过。领域拆分批成员删除后的引用承接,读面即引用、无第二账)
func _journal() -> GainJournal: func _journal() -> GainJournal:
@@ -110,7 +104,7 @@ func _book_negative_blast() -> void:
_rounds.mines_hits["total"] += 1 _rounds.mines_hits["total"] += 1
# 效果引擎宿主侧事实单点(fx_host facts_cb 注入——round_index/battle_index/seals_count/nest_count/hp_ratio 五事实;charge_layers 归 fx_host 自答;未知事实回落 null) # 效果引擎宿主侧事实单点(fx_host facts_cb 注入——round_index/battle_index/seals_count/nest_count/hp_ratio 五事实;未知事实回落 null)
func _fx_host_fact(fact: String) -> Variant: func _fx_host_fact(fact: String) -> Variant:
match fact: match fact:
"nest_count": "nest_count":
@@ -218,7 +212,7 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
_facts = BattleFacts.new() # 战斗事实件(受理事实/轮缓冲/封印账/溯源/条件计数——成员迁移见设计 §4) _facts = BattleFacts.new() # 战斗事实件(受理事实/轮缓冲/封印账/溯源/条件计数——成员迁移见设计 §4)
_reward_economy = RewardEconomy.new(db, rng) _reward_economy = RewardEconomy.new(db, rng)
_reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线) _reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线)
_damage = DamageController.new(db, rng, _enemy_system, _status_effects) _damage = DamageController.new(db, rng, _enemy_system, _status_effects, _facts)
_damage.emit_row_cb = _emit_row # 行协议归属本类(9 类行协议 + 来源断言不外泄) _damage.emit_row_cb = _emit_row # 行协议归属本类(9 类行协议 + 来源断言不外泄)
_fx_host = BattleFxHost.new({"db": db, "rng": rng, "minesweeper": minesweeper, "action_economy": action_economy, _fx_host = BattleFxHost.new({"db": db, "rng": rng, "minesweeper": minesweeper, "action_economy": action_economy,
"damage": _damage, "emit_cb": _emit_row, "facts_cb": _fx_host_fact}) "damage": _damage, "emit_cb": _emit_row, "facts_cb": _fx_host_fact})
@@ -232,9 +226,19 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
minesweeper.seal_judged.connect(_on_seal_judged) minesweeper.seal_judged.connect(_on_seal_judged)
minesweeper.round_end_board.connect(_on_round_end) minesweeper.round_end_board.connect(_on_round_end)
action_economy.round_end_stopped.connect(_on_round_end) action_economy.round_end_stopped.connect(_on_round_end)
# 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕) # 结算五段接线(引用束注入——BoardInstaller 先例 architecture.md:124:六子对象+既有公开协作件
# 结算五段接线随轮循环驱动件迁驻(round_cycle.gd——段件持有/段序/重入闸/摇号单点;构造序禁环不变) # 引用+两公开读数引用+七编排器方法回调+两公开读投影,20 键封闭束;段类各取所需,
_rounds = RoundCycle.new(self, _fx_host, _reward_grant) # 段类对编排器零反向引用——跨件单向接线,注入 Callable 亦属接线面)
var seg_bundle: Dictionary[Variant, Variant] = {
"facts": _facts, "units": _enemy_system, "damage": _damage, "status_effects": _status_effects,
"reward_economy": _reward_economy, "reward_grant": _reward_grant, "fx_host": _fx_host,
"minesweeper": minesweeper, "db": db, "action_economy": action_economy, "rng": rng,
"emit_row_cb": _emit_row, "fatal_cb": _fatal, "install_cb": _install_and_reset,
"battle_over_cb": _battle_over, "exit_distribution_cb": _exit_distribution,
"emit_snapshot_cb": _emit_settlement_snapshot, "emit_composition_cb": _emit_composition_effects,
"attune_param_sum_cb": attune_param_sum, "perfect_sweep_cb": perfect_sweep_earned,
}
_rounds = RoundCycle.new(seg_bundle)
# ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)---- # ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)----
@@ -259,7 +263,7 @@ func begin_battle(enc: Dictionary) -> bool:
if not action_economy.notify_round_start(): if not action_economy.notify_round_start():
_fatal("行动经济 首轮 notify_round_start 被拒") _fatal("行动经济 首轮 notify_round_start 被拒")
return false return false
_rounds.roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读) _rounds.roll_round_actions(round_index) # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读)
round_started.emit(round_index) round_started.emit(round_index)
return true return true
@@ -278,8 +282,8 @@ func _begin_battle_accept(enc: Dictionary) -> bool:
_fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle"))]) _fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle"))])
return false return false
# E4 收口:加成经 _facts.encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径; # E4 收口:加成经 _facts.encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径;
# _hero_atk 派生读=受理后不变量,无快照成员) # _damage.hero_atk() 派生读=受理后不变量,无快照成员)
if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _hero_atk(), "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": String(enc.get("node_type", "battle"))}): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss) if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _damage.hero_atk(), "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": String(enc.get("node_type", "battle"))}): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss)
_fatal("敌人系统 spawn 被拒") _fatal("敌人系统 spawn 被拒")
return false return false
return true return true
@@ -444,54 +448,7 @@ func _on_seal_judged(evt: Dictionary) -> void:
func _on_round_end(_payload: Dictionary) -> void: func _on_round_end(_payload: Dictionary) -> void:
_facts.path = String(_payload.get("path_tag", "")) _facts.path = String(_payload.get("path_tag", ""))
_rounds.settle_if_due() _rounds.settle_if_due(battle_result, round_index)
func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int) -> Dictionary:
if positive and pos_m <= 0:
return snap
if not positive and neg_m <= 0:
return snap
var out: Dictionary[Variant, Variant] = snap.duplicate()
var power := int(out.get("power", int(_reward_economy.polarity_row(String(out.get("cell_type", "")), "positive" if positive else "negative").power)))
var pct := pos_m if positive else neg_m
out["power"] = maxi(1, int(round(float(power) * (1.0 + float(pct) / 100.0))))
return out
# 伤害型变异过滤(terrain_kind fact 取数面——variation 表 effect=flat_damage 判据;
# 🔴 R4 甲永不对冲边界=仅限伤害型,信息型(hide_number 族)永不出现在本集)
func _harmful_variants(vids: Array) -> Array:
var out: Array[Variant] = []
for vid_v in vids:
var vid := String(vid_v)
var vrow: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(vid)
if String(vrow.get("effect", "")) == "flat_damage":
out.append(vid)
return out
# 邻接伤害型变异收集(terrain_kind fact 取数面——seal_hit 发射点用:封印命中相邻八格的伤害型变异集;
# 🔴 R4 甲永不对冲边界=仅限伤害型,信息型〔hide_number 族〕不入集)
func _harmful_adjacent(cell: Vector2i) -> Array:
var out: Array[Variant] = []
var board: BoardData = minesweeper.current_board()
if board == null:
return out
for n in board.neighbors8(cell):
out.append_array(_harmful_variants(board.variant_ids(n)))
return out
# 效果命中过滤(按效果类型取命中子集——发射点分桶施加面)
func _hits_of(hits: Array, kind: String) -> Array:
var out: Array[Variant] = []
for h in hits:
if String((h as Dictionary).get("kind", "")) == kind:
out.append(h)
return out
# aux 解锁门控转发(LevelRunner 注入;E1/E3 封装:外部经公有面,禁直读 _reward_economy) # aux 解锁门控转发(LevelRunner 注入;E1/E3 封装:外部经公有面,禁直读 _reward_economy)
@@ -586,13 +543,13 @@ func _exit_distribution() -> void:
action_economy.notify_round_start() action_economy.notify_round_start()
_facts.reset_round() _facts.reset_round()
_facts.refresh_used = 0 _facts.refresh_used = 0
_rounds.roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道) _rounds.roll_round_actions(round_index) # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道)
round_started.emit(round_index) round_started.emit(round_index)
# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道(折价成本同源:判据归 BattleRefreshFlow) # 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道(折价成本同源:判据归 BattleRefreshFlow)
func can_refresh() -> bool: func can_refresh() -> bool:
return BattleRefreshFlow.can_refresh(self) return _rounds.refresh_flow().can_refresh(battle_result)
# 玩家经 HUD框架发起、战斗结算 代理调用(禁止 RAP 满自动刷)。 # 玩家经 HUD框架发起、战斗结算 代理调用(禁止 RAP 满自动刷)。
@@ -600,7 +557,7 @@ func can_refresh() -> bool:
# 非挖掘期请求一律拒绝——非清图轮恒同图。刷新门/折价扣费/换图/成功事实归 BattleRefreshFlow # 非挖掘期请求一律拒绝——非清图轮恒同图。刷新门/折价扣费/换图/成功事实归 BattleRefreshFlow
# (迭代 46 卡A T3 拆出——编排器入口与唯一通道不变,C10 结构预算只降不升)。 # (迭代 46 卡A T3 拆出——编排器入口与唯一通道不变,C10 结构预算只降不升)。
func player_request_refresh() -> bool: func player_request_refresh() -> bool:
return BattleRefreshFlow.execute(self) return _rounds.refresh_flow().execute(battle_result, round_index)
func _battle_over(result: String) -> void: func _battle_over(result: String) -> void:
+61 -3
View File
@@ -157,7 +157,7 @@ func restore_curio_state(bp: Dictionary) -> void:
_curio.taken[String(id)] = maxi(0, int(bp_taken.get(id, 0))) # 身份维键(relic:<feat_id>)随快照还原 _curio.taken[String(id)] = maxi(0, int(bp_taken.get(id, 0))) # 身份维键(relic:<feat_id>)随快照还原
# 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径; # 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径;
# relic 令牌 feat_id 随还原) # relic 令牌 feat_id 随还原)
var bp_deck = _bp_read("bonus_deck", bp) var bp_deck = bp_read("bonus_deck", bp)
if bp_deck is Array: if bp_deck is Array:
var restored: Array[Variant] = [] var restored: Array[Variant] = []
for t in (bp_deck as Array): for t in (bp_deck as Array):
@@ -192,8 +192,9 @@ func restore_stop_bank_fragments(bp: Dictionary, params: Dictionary) -> void:
# 读侧单点(BREAKPOINT_FIELDS 策略标签分发;未登记键 fail-fast 不静默按 plain 兜)。 # 读侧单点(BREAKPOINT_FIELDS 策略标签分发;未登记键 fail-fast 不静默按 plain 兜)。
# 二次消费面(_start_current_level 经 _resume_bp 取 node_id/visited_ids/aux_items)同走此口。 # 二次消费面(_start_current_level 经 _resume_bp 取 node_id/visited_ids/aux_items)同走此口;
func _bp_read(key: String, bp: Dictionary, fallback: Variant = null) -> Variant: # 公开化(5.1 封装修复——宿主读侧同口,_bp_read 更名非并存)
func bp_read(key: String, bp: Dictionary, fallback: Variant = null) -> Variant:
if not BREAKPOINT_FIELDS.has(key): if not BREAKPOINT_FIELDS.has(key):
push_error("[冒险结构] 断点字段未登记(读侧): %s" % key) push_error("[冒险结构] 断点字段未登记(读侧): %s" % key)
var v: Variant = bp.get(key, fallback) var v: Variant = bp.get(key, fallback)
@@ -216,3 +217,60 @@ func _bp_read_int(key: String, bp: Dictionary, fallback: int, params: Dictionary
return maxi(0, int(bp.get(key, int(params.get(key, fallback))))) return maxi(0, int(bp.get(key, int(params.get(key, fallback)))))
_: _:
return int(bp.get(key, fallback)) return int(bp.get(key, fallback))
# ---- 断点域六成员公开读写面(5.1 封装修复——宿主私取清零:方法体一律一行转发自有成员,零逻辑增量)----
# 断点承载注入(宿主构造后接线——save_manager 后绑)
func bind_save_manager(sm) -> void:
_save_manager = sm
# 断点承载读投影(可 null——模拟/冒烟缺省态)
func save_manager() -> Variant:
return _save_manager
func set_pending_next_level(v: bool) -> void:
_pending_next_level = v
func pending_next_level() -> bool:
return _pending_next_level
func set_loadout_seeded(v: bool) -> void:
_loadout_seeded = v
func loadout_seeded() -> bool:
return _loadout_seeded
func set_resume_bp(bp: Dictionary) -> void:
_resume_bp = bp
func clear_resume_bp() -> void:
_resume_bp = {}
# 续玩快照读(返引用零拷贝——宿主读侧只读)
func resume_bp() -> Dictionary:
return _resume_bp
func set_last_level_payload(payload: Dictionary) -> void:
_last_level_payload = payload
func last_level_payload() -> Dictionary:
return _last_level_payload
func set_last_treasure_cleared(v: int) -> void:
_last_treasure_cleared = v
func last_treasure_cleared() -> int:
return _last_treasure_cleared
+4 -4
View File
@@ -153,10 +153,10 @@ func _assign_loaded(loaded: Dictionary) -> void:
presentation.camp_scene_layout = loaded.get("camp_scene_layout") presentation.camp_scene_layout = loaded.get("camp_scene_layout")
for problem: String in presentation.camp_scene_layout.validate(): for problem: String in presentation.camp_scene_layout.validate():
_fatal("camp_scene_layout", problem) # 布局几何红线(命中区越画面/相交/侵顶栏)=数据致命 _fatal("camp_scene_layout", problem) # 布局几何红线(命中区越画面/相交/侵顶栏)=数据致命
# resolver 协作件注入三表(resolve 委托承载——resolver 文件与键零搬移) # resolver 协作件注入三表(resolve 委托承载——resolver 文件与键零搬移;5.1 封装修复改经 bind_resolver 公开挂载口)
combat.board_gen._resolver = _resolver combat.board_gen.bind_resolver(_resolver)
combat.enemy._resolver = _resolver combat.enemy.bind_resolver(_resolver)
run.difficulty._resolver = _resolver run.difficulty.bind_resolver(_resolver)
# 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体: # 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体:
+26 -10
View File
@@ -44,16 +44,33 @@ var _hero_ac_override := -1 # 主角护甲等级合流入参(begin_battle
# 效果端口件(组装期单点注入——host _init bind_port):发射/计数器/事实/揭示/AC 全经端口路由, # 效果端口件(组装期单点注入——host _init bind_port):发射/计数器/事实/揭示/AC 全经端口路由,
# 缺省 null=未绑(与原 _fx==null 零扰动守卫同语义——单测直构不接端口的夹具零扰动照跑) # 缺省 null=未绑(与原 _fx==null 零扰动守卫同语义——单测直构不接端口的夹具零扰动照跑)
var _port: EffectPort = null var _port: EffectPort = null
var facts: BattleFacts # 战斗事实件引用(构造注入——hero_atk 派生读消费面)
func _init(db_ref, rng_ref, units_ref, effects_ref) -> void: func _init(db_ref, rng_ref, units_ref, effects_ref, facts_ref) -> void:
db = db_ref db = db_ref
rng = rng_ref rng = rng_ref
units = units_ref units = units_ref
effects = effects_ref effects = effects_ref
facts = facts_ref
units.unit_died.connect(_on_unit_died) units.unit_died.connect(_on_unit_died)
# 主角攻击基值派生读(enc 携带 hero_atk 键受理后只读——E4 收口语义不变:表值运行期不可变,
# 派生值恒等;自编排器归位(5.1 封装修复——伤害域原语),「读面即引用、无第二账」同款先例)
func hero_atk() -> int:
return int(facts.encounter.get("hero_atk", int(db.combat.hero.atk)))
# 效果命中过滤(按效果类型取命中子集——发射点分桶施加面;自编排器归位)
static func hits_of(hits: Array, kind: String) -> Array:
var out: Array[Variant] = []
for h in hits:
if String((h as Dictionary).get("kind", "")) == kind:
out.append(h)
return out
# 主角护甲等级合流入参(战斗编排器 begin_battle 调用——encounter.hero_armor_class;缺参/-1 = 表值兜底) # 主角护甲等级合流入参(战斗编排器 begin_battle 调用——encounter.hero_armor_class;缺参/-1 = 表值兜底)
func set_hero_ac_override(v: int) -> void: func set_hero_ac_override(v: int) -> void:
_hero_ac_override = v _hero_ac_override = v
@@ -268,8 +285,7 @@ func _hero_attack_ctx(t: Dictionary) -> Dictionary:
# 按序改写族四拍+掷点快照(armor_roll 发射点在此):第一拍矿工重锤命中交换(正负双修原生形态: # 按序改写族四拍+掷点快照(armor_roll 发射点在此):第一拍矿工重锤命中交换(正负双修原生形态:
# 命中判定减成 trade_hit 点〔更易命中〕换攻击增伤 trade_atk%——高 AC 负期望/低 AC 正期望; # 命中判定减成 trade_hit 点〔更易命中〕换攻击增伤 trade_atk%——高 AC 负期望/低 AC 正期望;
# 改写位先于掷点折扣读数)→ 第二拍专注免掷骰恒定折扣(方差买断成期望——稳定器人格;roll 记 0 归因) # 改写位先于掷点折扣读数)→ 第二拍专注免掷骰恒定折扣(方差买断成期望——稳定器人格;roll 记 0 归因)
# → 第三拍厚积薄发满层必中(spike ready 时 discount 恒全额、暴击面走倍乘不加掷;when charge_layers≥3 # → 第三拍满层必中(spike ready 时 discount 恒全额、暴击面走倍乘不加掷)→ 第四拍猎弱本能对轻甲敌折扣档改善 value 档
# 在 dispatch 判定——满层持续状态口径)→ 第四拍猎弱本能对轻甲敌折扣档改善 value 档
# (对冲 pierce 列 L1 −1 开局罚;改善 = 折扣表向全额方向移档,nat 最大面/已全额不重复)。 # (对冲 pierce 列 L1 −1 开局罚;改善 = 折扣表向全额方向移档,nat 最大面/已全额不重复)。
# clamp_check 同在此发射(唯一发射点——命中随返回值穿给 _ctx_clamp_delta)。 # clamp_check 同在此发射(唯一发射点——命中随返回值穿给 _ctx_clamp_delta)。
func _ctx_rewrites(t: Dictionary, target: Dictionary, armor: String, matchup: String) -> Dictionary: func _ctx_rewrites(t: Dictionary, target: Dictionary, armor: String, matchup: String) -> Dictionary:
@@ -335,7 +351,7 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float:
for h in ac_hits: for h in ac_hits:
if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_atk", 0)) > 0: if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_atk", 0)) > 0:
base_value *= 1.0 + float(int(h.trade_atk)) / 100.0 base_value *= 1.0 + float(int(h.trade_atk)) / 100.0
# 加法族:整备军备「每层下轮首段 +1」(层数×value 加算)——attack_bonus per charge_layers。 # 加法族:attack_bonus per 计数器层值(层数×value 加算)。
# value_counter 扩展〔疼痛记忆/弃图兑换/陷阱本能——rider 值=计数器账面, # value_counter 扩展〔疼痛记忆/弃图兑换/陷阱本能——rider 值=计数器账面,
# 值读于施加时点=命中序先行 rider 后清零行〕 # 值读于施加时点=命中序先行 rider 后清零行〕
for h in ac_hits: for h in ac_hits:
@@ -349,7 +365,7 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float:
if String(h.get("kind", "")) == "counter_add": if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null) var set_v = h.get("set", null)
_port.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) _port.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
# 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载) # 任意真族:modify_ticket mode=spike 满层倍乘(强制必中已在折扣拍承载)
if ctx["spike_ready"]: if ctx["spike_ready"]:
for h in ac_hits: for h in ac_hits:
if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike": if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike":
@@ -424,16 +440,16 @@ func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary
# 敌方伤害技:掷点 → 折扣/暴击 → 克制点数(attack_type vs 主角护甲类型—— # 敌方伤害技:掷点 → 折扣/暴击 → 克制点数(attack_type vs 主角护甲类型——
# 无类型恒中性 delta 0,写法同 hero_attack 的 _counter_delta 消费位)→ hero_defense×enemy_output # 无类型恒中性 delta 0,写法同 hero_attack 的 _counter_delta 消费位)→ hero_defense×enemy_output
# 池乘区 → 下限 → +rider(后置必中附加)→ 交付 → commit_action(作废单不 commit——死亡即不动手)。 # 池乘区 → 下限 → +rider(后置必中附加)→ 交付 → commit_action(作废单不 commit——死亡即不动手)。
# 掷点算术归 _enemy_roll;damage_taken 统一发射+消费归 _emit_hero_damage_taken(发射在本函数—— # 掷点算术归 _enemy_roll;damage_taken 统一发射+消费归 emit_hero_damage_taken(发射在本函数——
# 与减伤落快照的先后序固定;「掷点后、交付前」消费时点不变)。 # 与减伤落快照的先后序固定;「掷点后、交付前」消费时点不变)。
func _resolve_enemy_skill(t: Dictionary) -> void: func _resolve_enemy_skill(t: Dictionary) -> void:
var hero: Dictionary[Variant, Variant] = units.hero_unit() var hero: Dictionary[Variant, Variant] = units.hero_unit()
var roll := _enemy_roll(t) var roll := _enemy_roll(t)
var total: int = int(roll["total"]) var total: int = int(roll["total"])
# damage_taken 发射(kind=enemy_skill;铁壁整备减伤订阅面:shield 加法族合计 value×per(charge_layers), # damage_taken 发射(kind=enemy_skill;shield 减伤订阅面:加法族合计 value×per 计数器层值,
# 稳健拓荒树规则同面(rebuild 合成订阅);减后过下限保底。damage_reduce〔背水 hp_ratio when〕/ # 稳健拓荒树规则同面(rebuild 合成订阅);减后过下限保底。damage_reduce〔背水 hp_ratio when〕/
# counter_add〔保险丝/石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 damage_taken 载荷携带) # counter_add〔保险丝/石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 damage_taken 载荷携带)
var dt_res := _emit_hero_damage_taken(total, "enemy_skill", String(t.context.get("skill_id", "")), t.origin_cell, int(t.round_index)) var dt_res := emit_hero_damage_taken(total, "enemy_skill", String(t.context.get("skill_id", "")), t.origin_cell, int(t.round_index))
total = int(dt_res["total"]) total = int(dt_res["total"])
units.apply_damage(hero, total) units.apply_damage(hero, total)
var payload: Dictionary[Variant, Variant] = {"target": "hero", "skill_id": String(t.context.get("skill_id", "")), "baseline": int(t.context.get("baseline", 0)), var payload: Dictionary[Variant, Variant] = {"target": "hero", "skill_id": String(t.context.get("skill_id", "")), "baseline": int(t.context.get("baseline", 0)),
@@ -469,7 +485,7 @@ func _enemy_roll(t: Dictionary) -> Dictionary:
# 资源直伤(后三者经 board_flat_hero 单据进入)都经此 dispatch 一次 damage_taken 并消费一次; # 资源直伤(后三者经 board_flat_hero 单据进入)都经此 dispatch 一次 damage_taken 并消费一次;
# 同一伤害单据重复发射由 context.hero_damage_consumed 标记闸拦(地形路径在开单点标记)。 # 同一伤害单据重复发射由 context.hero_damage_consumed 标记闸拦(地形路径在开单点标记)。
# payload 必含 kind/amount/source/cell/round/hp_ratio;返回 {total, hits} 交调用侧按减后值开单/交付)。 # payload 必含 kind/amount/source/cell/round/hp_ratio;返回 {total, hits} 交调用侧按减后值开单/交付)。
func _emit_hero_damage_taken(amount: int, damage_kind: String, source: String, cell: Vector2i, ri: int) -> Dictionary: func emit_hero_damage_taken(amount: int, damage_kind: String, source: String, cell: Vector2i, ri: int) -> Dictionary:
var hu: Dictionary[Variant, Variant] = units.hero_unit() var hu: Dictionary[Variant, Variant] = units.hero_unit()
var payload: Dictionary[Variant, Variant] = {"kind": damage_kind, "amount": amount, "source": source, var payload: Dictionary[Variant, Variant] = {"kind": damage_kind, "amount": amount, "source": source,
"cell": cell, "round": ri, "hp_ratio": int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1))))))} "cell": cell, "round": ri, "hp_ratio": int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1))))))}
@@ -545,7 +561,7 @@ func _resolve_board_blast(t: Dictionary) -> void:
func _resolve_board_flat_hero(t: Dictionary) -> void: func _resolve_board_flat_hero(t: Dictionary) -> void:
var dealt: int = int(t.base_value) var dealt: int = int(t.base_value)
if not bool((t.get("context", {}) as Dictionary).get("hero_damage_consumed", false)): if not bool((t.get("context", {}) as Dictionary).get("hero_damage_consumed", false)):
var res_flat := _emit_hero_damage_taken(dealt, "board_flat_hero", String(t.get("source_class", "")), t.origin_cell, int(t.round_index)) var res_flat := emit_hero_damage_taken(dealt, "board_flat_hero", String(t.get("source_class", "")), t.origin_cell, int(t.round_index))
dealt = int(res_flat["total"]) dealt = int(res_flat["total"])
dealt = maxi(int(db.combat.settlement.damage_roll.damage_floor), dealt) # 伤害下限全来源(减后过下限) dealt = maxi(int(db.combat.settlement.damage_roll.damage_floor), dealt) # 伤害下限全来源(减后过下限)
units.apply_damage(units.hero_unit(), dealt) units.apply_damage(units.hero_unit(), dealt)
+5 -5
View File
@@ -11,7 +11,7 @@ extends RefCounted
## 装板完成〔巢穴预告——每板恰一次,战斗板生成/重建发射点〕) ## 装板完成〔巢穴预告——每板恰一次,战斗板生成/重建发射点〕)
const EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "attack_commit", "clamp_check", const EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "attack_commit", "clamp_check",
"armor_roll", "kill", "damage_taken", "enemy_hp_threshold", "round_start", "seal_settle", "armor_roll", "kill", "damage_taken", "enemy_hp_threshold", "round_start", "seal_settle",
"round_end", "reward_roll", "treasure_used", "charge_full", "dig_stop", "battle_won", "seal_missed", "round_end", "reward_roll", "treasure_used", "dig_stop", "battle_won", "seal_missed",
"board_installed", "board_installed",
# 换图系双事件(refresh_request=玩家请求刷新〔省力周转 apply_discount 订阅面——折扣只改门槛 # 换图系双事件(refresh_request=玩家请求刷新〔省力周转 apply_discount 订阅面——折扣只改门槛
# 与扣费不改拒绝判据〕;board_refreshed=成功刷新事实〔换图系七件订阅主挂点——装新板后才发射〕) # 与扣费不改拒绝判据〕;board_refreshed=成功刷新事实〔换图系七件订阅主挂点——装新板后才发射〕)
@@ -56,7 +56,7 @@ const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open
## 当时有封印〔破封一击〕;ap_exhausted_last=上轮 AP 耗尽〔一滴不剩〕;hp_ratio=生命比例〔背水 ## 当时有封印〔破封一击〕;ap_exhausted_last=上轮 AP 耗尽〔一滴不剩〕;hp_ratio=生命比例〔背水
## 06 前置条兑付〕;amount=单次伤害量〔保险丝〕;ap_left=停手剩余 AP〔弃图兑换〕) ## 06 前置条兑付〕;amount=单次伤害量〔保险丝〕;ap_left=停手剩余 AP〔弃图兑换〕)
const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_armor_class", const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_armor_class",
"seals_count", "charge_layers", "nest_count", "round_index", "counter", "seals_count", "nest_count", "round_index", "counter",
"new_resource_type", "is_edge_cell", "is_blank_zero", "dug_under_seal", "new_resource_type", "is_edge_cell", "is_blank_zero", "dug_under_seal",
"ap_exhausted_last", "hp_ratio", "amount", "ap_left", "pct", "ap_exhausted_last", "hp_ratio", "amount", "ap_left", "pct",
"battle_index"] # 关卡域战斗序(1 基,enc 携带;开局姿态组「每关首轮窗」条件面) "battle_index"] # 关卡域战斗序(1 基,enc 携带;开局姿态组「每关首轮窗」条件面)
@@ -200,7 +200,7 @@ func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary)
_triggers.append(t) _triggers.append(t)
## 订阅查询面(发射点缺省行为仲裁用——如 round_end 无 transfer 订阅时走 RAP 转存) ## 订阅查询面(发射点缺省行为仲裁用——round_end 走 RAP 转存)
func has_effect_on(event: String, kind: String, mode: String = "") -> bool: func has_effect_on(event: String, kind: String, mode: String = "") -> bool:
for tr in _triggers: for tr in _triggers:
if String(tr.on) != event: if String(tr.on) != event:
@@ -458,11 +458,11 @@ static func _vtr_from_refs(rid: String, tr: Dictionary, row: Dictionary, errs: A
## 组合规则(需求卡片 §3.4 六族——效果类型的必填属性,发射点施加面引用) ## 组合规则(需求卡片 §3.4 六族——效果类型的必填属性,发射点施加面引用)
## 加法族(数值相加,可交换):add_ticket_rider、attack_bonus、grant_material、grant_ap(非 transfer)、 ## 加法族(数值相加,可交换):add_ticket_rider、attack_bonus、grant_material、grant_ap、
## counter_add、shield——发射点逐条累加收集器。 ## counter_add、shield——发射点逐条累加收集器。
## 乘法族(收集后一次施加,正负分桶):yield_mult、damage_mult。 ## 乘法族(收集后一次施加,正负分桶):yield_mult、damage_mult。
## 任意真族(任一命中即生效):spawn_spell、spike。 ## 任意真族(任一命中即生效):spawn_spell、spike。
## 按序改写族(收集→单点按声明序施加):modify_ticket 及一切改写窗口效果。 ## 按序改写族(收集→单点按声明序施加):modify_ticket 及一切改写窗口效果。
## 按序消耗族(按订阅序消耗共享资源,耗尽即止;可选 fallback):counter_spend。 ## 按序消耗族(按订阅序消耗共享资源,耗尽即止;可选 fallback):counter_spend。
## 即时执行族(每条命中各自执行一次,执行序=声明序):open_ticket、grant_ap(mode:transfer)、 ## 即时执行族(每条命中各自执行一次,执行序=声明序):open_ticket、
## rewrite_board、apply_status——发射点逐条执行。 ## rewrite_board、apply_status——发射点逐条执行。
+2 -2
View File
@@ -1,7 +1,7 @@
## 效果端口件:战斗业务类(DamageController/ActionEconomy)唯一的效果发射出口——收事件与载荷、 ## 效果端口件:战斗业务类(DamageController/ActionEconomy)唯一的效果发射出口——收事件与载荷、
## 交效果引擎结算、取回命中批。六 Callable 路由宿主(BattleFxHost.fx_emit_biz/counter_read/ ## 交效果引擎结算、取回命中批。六 Callable 路由宿主(BattleFxHost.fx_emit_biz/counter_read/
## counter_add/biz_fact/enqueue_reveal/apply_ac_bonus),经组装期 bind_port 单点注入。 ## counter_add/biz_fact/enqueue_reveal/apply_ac_bonus),经组装期 bind_port 单点注入。
## 闭面红线:事实回调=biz_fact 闭面五键(charge_layers/dig_streak/hp_ratio/rider_tier_bonus/ ## 闭面红线:事实回调=biz_fact 闭面四键(dig_streak/hp_ratio/rider_tier_bonus/
## rider_tier_crit),battle_index/round_index/seals_count/nest_count 禁进业务发射事实面。 ## rider_tier_crit),battle_index/round_index/seals_count/nest_count 禁进业务发射事实面。
## 单向接线:本件只持宿主注入的 Callable,禁持 BattleFxHost 对象引用(宿主 bundle 已持两业务类 ## 单向接线:本件只持宿主注入的 Callable,禁持 BattleFxHost 对象引用(宿主 bundle 已持两业务类
## ——对象引用即成 host→damage→port→host 强引用环,两件均随每战新建=泄漏)。 ## ——对象引用即成 host→damage→port→host 强引用环,两件均随每战新建=泄漏)。
@@ -50,7 +50,7 @@ func counter_add(counter_id: String, value: int, set_form: bool) -> void:
counter_add_cb.call(counter_id, value, set_form) counter_add_cb.call(counter_id, value, set_form)
# 事实取值(闭面五键宿主侧单点——未绑→null) # 事实取值(闭面四键宿主侧单点——未绑→null)
func fact(name: String) -> Variant: func fact(name: String) -> Variant:
if not fact_cb.is_valid(): if not fact_cb.is_valid():
return null return null
+24
View File
@@ -53,6 +53,30 @@ func current_board() -> BoardData:
return _board return _board
# 伤害型变异过滤(terrain_kind fact 取数面——variation 表 effect=flat_damage 判据;
# 🔴 R4 甲永不对冲边界=仅限伤害型,信息型(hide_number 族)永不出现在本集。自编排器归位)
func harmful_variants(vids: Array) -> Array:
var out: Array[Variant] = []
for vid_v in vids:
var vid := String(vid_v)
var vrow: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(vid)
if String(vrow.get("effect", "")) == "flat_damage":
out.append(vid)
return out
# 邻接伤害型变异收集(terrain_kind fact 取数面——seal_hit 发射点用:封印命中相邻八格的伤害型变异集;
# 🔴 R4 甲永不对冲边界=仅限伤害型,信息型〔hide_number 族〕不入集。自编排器归位)
func harmful_adjacent(cell: Vector2i) -> Array:
var out: Array[Variant] = []
var board: BoardData = current_board()
if board == null:
return out
for n in board.neighbors8(cell):
out.append_array(harmful_variants(board.variant_ids(n)))
return out
# 已开格记录查询面(cell → {kind, digit};未开 = {})——呈现层渲染消费面(board_view 徽章/四格态) # 已开格记录查询面(cell → {kind, digit};未开 = {})——呈现层渲染消费面(board_view 徽章/四格态)
func reveal_record(c: Vector2i) -> Dictionary: func reveal_record(c: Vector2i) -> Dictionary:
return _revealed.get(c, {}) return _revealed.get(c, {})
+1 -2
View File
@@ -7,7 +7,6 @@
class_name NodeMapGenerator class_name NodeMapGenerator
extends RefCounted extends RefCounted
const NODE_TYPES := ["battle", "boss", "reward", "rest", "shop"]
const FILLER_TYPES := ["battle", "reward", "rest"] const FILLER_TYPES := ["battle", "reward", "rest"]
# 多终点图的支路互斥段必须各有独有类型(契约 5③),而商店与 Boss 是每条支路共有件, # 多终点图的支路互斥段必须各有独有类型(契约 5③),而商店与 Boss 是每条支路共有件,
# 独有类型只能来自 battle/reward/rest 三件 ⇒ 归属大岔口各区域的填充节点取互斥签名类型。 # 独有类型只能来自 battle/reward/rest 三件 ⇒ 归属大岔口各区域的填充节点取互斥签名类型。
@@ -133,7 +132,7 @@ func _vo_node_fields(nodes: Array, errs: Array, by_id: Dictionary, indeg: Dictio
if by_id.has(id): if by_id.has(id):
errs.append({"category": "dup_key", "message": "节点句柄重复: %d" % id}) errs.append({"category": "dup_key", "message": "节点句柄重复: %d" % id})
by_id[id] = n by_id[id] = n
if not NODE_TYPES.has(String(n["type"])): if not LevelTable.NODE_TYPES.has(String(n["type"])):
errs.append({"category": "enum", "message": "节点类型出域: %s" % n["type"]}) errs.append({"category": "enum", "message": "节点类型出域: %s" % n["type"]})
if int(n["depth"]) < 0: if int(n["depth"]) < 0:
errs.append({"category": "domain", "message": "节点深度为负: %d" % n["depth"]}) errs.append({"category": "domain", "message": "节点深度为负: %d" % n["depth"]})
+14
View File
@@ -67,6 +67,20 @@ func reset_battle() -> void:
defer.queue = [] defer.queue = []
# 奖励乘区调整(yield_mult 命中聚合——自编排器归位〔5.1 封装修复·奖励域原语〕:power 按
# pos/neg 乘区百分比放大、下限 1;正负两态由参分;零命中/零乘区原快照直返)
func feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int) -> Dictionary:
if positive and pos_m <= 0:
return snap
if not positive and neg_m <= 0:
return snap
var out: Dictionary[Variant, Variant] = snap.duplicate()
var power := int(out.get("power", int(reward_economy.polarity_row(String(out.get("cell_type", "")), "positive" if positive else "negative").power)))
var pct := pos_m if positive else neg_m
out["power"] = maxi(1, int(round(float(power) * (1.0 + float(pct) / 100.0))))
return out
# 揭示奖励兑付(BattleFxHost.flush_pending_reveals 的兑付回调面——授予面收编: # 揭示奖励兑付(BattleFxHost.flush_pending_reveals 的兑付回调面——授予面收编:
# snapshot 查询与极性路由同在本件,fx_host 对授予面零强引用) # snapshot 查询与极性路由同在本件,fx_host 对授予面零强引用)
func reveal_redeem(cell: Vector2i, ri: int) -> void: func reveal_redeem(cell: Vector2i, ri: int) -> void:
+33 -13
View File
@@ -48,7 +48,7 @@ func _init(path: String = "") -> void:
# 无激活态(注入路径空,一切写盘路径拒绝并返回 false);玩家侧经选档界面显式进档/新档后才可写。 # 无激活态(注入路径空,一切写盘路径拒绝并返回 false);玩家侧经选档界面显式进档/新档后才可写。
# 旧隐式单档一次性迁入槽 1(Q1 甲,sv12)在本口执行。 # 旧隐式单档一次性迁入槽 1(Q1 甲,sv12)在本口执行。
func _ready() -> void: func _ready() -> void:
slots._migrate_legacy_save(LEGACY_SAVE_PATH, slots.slot_path(1), LEGACY_SAVE_PATH + LEGACY_MIGRATED_SUFFIX) slots.migrate_legacy_save(LEGACY_SAVE_PATH, slots.slot_path(1), LEGACY_SAVE_PATH + LEGACY_MIGRATED_SUFFIX)
# 迁移注册口:from_version → from_version+1 的升级函数(读档时链式调用)。fn 契约: # 迁移注册口:from_version → from_version+1 的升级函数(读档时链式调用)。fn 契约:
@@ -63,10 +63,10 @@ func register_migration(from_version: int, fn: Callable) -> void:
# 损坏兜底:原文件改名 <path>.corrupt.bak(已存在则覆盖)+ push_warning + 默认数据继续——不静默丢档。 # 损坏兜底:原文件改名 <path>.corrupt.bak(已存在则覆盖)+ push_warning + 默认数据继续——不静默丢档。
func load_from_disk() -> bool: func load_from_disk() -> bool:
_read_only = false _read_only = false
if not FileAccess.file_exists(slots._path): if not FileAccess.file_exists(slots.active_path()):
_data = _default_data() # 新档合法态 _data = _default_data() # 新档合法态
return true return true
var f := FileAccess.open(slots._path, FileAccess.READ) var f := FileAccess.open(slots.active_path(), FileAccess.READ)
if f == null: if f == null:
return _corrupt_fallback("读打开失败(%s)" % error_string(FileAccess.get_open_error())) return _corrupt_fallback("读打开失败(%s)" % error_string(FileAccess.get_open_error()))
return _load_parsed(f) return _load_parsed(f)
@@ -102,21 +102,21 @@ func _load_parsed(f: FileAccess) -> bool:
# 无激活态一律拒绝并返回 false(sv3:启动态不隐式激活,盘面零触碰); # 无激活态一律拒绝并返回 false(sv3:启动态不隐式激活,盘面零触碰);
# 高版本拒读态一律拒绝(契约 4 禁写);写失败返回 false。 # 高版本拒读态一律拒绝(契约 4 禁写);写失败返回 false。
func write_to_disk() -> bool: func write_to_disk() -> bool:
if slots._path == "": if slots.active_path() == "":
push_error("[存档序列化] 写盘拒绝——无激活存档位(启动态不隐式激活;经选档进档/新档后落盘)") push_error("[存档序列化] 写盘拒绝——无激活存档位(启动态不隐式激活;经选档进档/新档后落盘)")
return false return false
if _read_only: if _read_only:
push_error("[存档序列化] 写盘拒绝——档案版本高于当前(拒读禁写态)") push_error("[存档序列化] 写盘拒绝——档案版本高于当前(拒读禁写态)")
return false return false
var tmp := slots._path + ".tmp" var tmp := slots.active_path() + ".tmp"
var f := FileAccess.open(tmp, FileAccess.WRITE) var f := FileAccess.open(tmp, FileAccess.WRITE)
if f == null: if f == null:
push_error("[存档序列化] 临时文件打开失败:%s(%s)" % [tmp, error_string(FileAccess.get_open_error())]) push_error("[存档序列化] 临时文件打开失败:%s(%s)" % [tmp, error_string(FileAccess.get_open_error())])
return false return false
f.store_string(JSON.stringify(_data, "\t", true)) f.store_string(JSON.stringify(_data, "\t", true))
f.close() f.close()
if not _rename_overwrite(tmp, slots._path): if not rename_overwrite(tmp, slots.active_path()):
push_error("[存档序列化] 原子改名失败(tmp 保留待查):%s → %s" % [tmp, slots._path]) push_error("[存档序列化] 原子改名失败(tmp 保留待查):%s → %s" % [tmp, slots.active_path()])
return false return false
return true return true
@@ -214,7 +214,7 @@ func _peek_migrated(parsed: Variant, fail: Array) -> Variant:
## 摘要窥视内核(只读推演单点——SlotStore 两 peek 方法经槽路径派生委托至此; ## 摘要窥视内核(只读推演单点——SlotStore 两 peek 方法经槽路径派生委托至此;
## 迁移链在此只读推演——生产迁移 Callable 为纯函数,零盘面副作用) ## 迁移链在此只读推演——生产迁移 Callable 为纯函数,零盘面副作用)
func _peek_summary_at(path: String) -> Dictionary: func peek_summary_at(path: String) -> Dictionary:
var out := {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0} var out := {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0}
if not FileAccess.file_exists(path): if not FileAccess.file_exists(path):
out["readable"] = true # 空槽=可读零值合法态 out["readable"] = true # 空槽=可读零值合法态
@@ -275,15 +275,35 @@ func _default_data() -> Dictionary[String, Variant]:
} }
# 原地重置默认态(SlotStore 槽位原语的协作口——激活/建删/换注回默认数据) # 原地重置默认态(SlotStore 槽位原语的协作口——激活/建删/换注回默认数据;
func _reset_to_default() -> void: # 公开化〔5.1 封装修复——SlotStore 反向私取承接,_reset_to_default 更名非并存〕)
func reset_to_default() -> void:
_data = _default_data() _data = _default_data()
# 禁写位公开面(SlotStore 激活/换注三态恢复消费——一行转发自有成员,零逻辑增量)
func set_read_only(v: bool) -> void:
_read_only = v
func read_only() -> bool:
return _read_only
# 数据快照还原公开面(SlotStore 激活/新建失败回退三态恢复消费——零拷贝语义:snapshot 返 _data
# 本引用、restore 直还引用,行为与原 _data 直读写逐位等价)
func data_snapshot() -> Dictionary[String, Variant]:
return _data
func restore_data(data: Dictionary[String, Variant]) -> void:
_data = data
# 损坏兜底:坏档改名 .corrupt.bak(已存在则覆盖)→ 默认数据继续。不静默丢档(警告留痕)、不 crash。 # 损坏兜底:坏档改名 .corrupt.bak(已存在则覆盖)→ 默认数据继续。不静默丢档(警告留痕)、不 crash。
func _corrupt_fallback(reason: String) -> bool: func _corrupt_fallback(reason: String) -> bool:
var bak := slots._path + ".corrupt.bak" var bak := slots.active_path() + ".corrupt.bak"
if not _rename_overwrite(slots._path, bak): if not rename_overwrite(slots.active_path(), bak):
push_warning("[存档序列化] 坏档备份改名失败(原文件保留原地):%s——%s" % [bak, reason]) push_warning("[存档序列化] 坏档备份改名失败(原文件保留原地):%s——%s" % [bak, reason])
else: else:
push_warning("[存档序列化] 检测到损坏档案,已备份为 %s——%s(用默认新档继续)" % [bak, reason]) push_warning("[存档序列化] 检测到损坏档案,已备份为 %s——%s(用默认新档继续)" % [bak, reason])
@@ -314,7 +334,7 @@ func _normalize(data: Dictionary) -> Dictionary[String, Variant]:
# 覆盖式改名(Windows 原生 rename 目标已存在时会失败——先删后改,显式覆盖语义;IO 原语单点—— # 覆盖式改名(Windows 原生 rename 目标已存在时会失败——先删后改,显式覆盖语义;IO 原语单点——
# SlotStore 迁移流经本件调用) # SlotStore 迁移流经本件调用)
func _rename_overwrite(from: String, to: String) -> bool: func rename_overwrite(from: String, to: String) -> bool:
var dir := DirAccess.open(from.get_base_dir()) var dir := DirAccess.open(from.get_base_dir())
if dir == null: if dir == null:
return false return false
+9 -6
View File
@@ -2,15 +2,18 @@ class_name AuraSegment
extends RefCounted extends RefCounted
# ② 增益/减益段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 # ② 增益/减益段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。
# 职责:续存实例组合行(本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦)。 # 职责:续存实例组合行(本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦)。
# 行发射归宿主编排器(oc)——零算法改动(🔴 零行为变更契约)。 # 耦合边界(5.1 封装修复换代):引用束注入——构造收编排器组装的 20 键封闭束,本段断言 1 键
# (emit_composition_cb)解包;组合行发射经回调回宿主保留面——对编排器零引用。
# 零算法改动(🔴 零行为变更契约)。
var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主 var emit_composition_cb: Callable # 宿主 _emit_composition_effects——续存实例组合行发射(保留面)
func _init(host) -> void: func _init(bundle: Dictionary) -> void:
oc = host assert(bundle.has("emit_composition_cb"), "[增益减益段] 引用束缺键: emit_composition_cb")
emit_composition_cb = bundle.emit_composition_cb
func run(ri: int) -> void: func run(ri: int) -> void:
oc._emit_composition_effects("hero", ri) emit_composition_cb.call("hero", ri)
oc._emit_composition_effects("enemy", ri) emit_composition_cb.call("enemy", ri)
+47 -17
View File
@@ -2,42 +2,72 @@ class_name BattleRefreshFlow
extends RefCounted extends RefCounted
## 换图刷新流程件(迭代 46 卡A T3/T4 自编排器拆出):刷新可用性门、折价扣费、换图执行与成功事实登记。 ## 换图刷新流程件(迭代 46 卡A T3/T4 自编排器拆出):刷新可用性门、折价扣费、换图执行与成功事实登记。
## 拆分边界(需求卡片 C13):本件只负责可用性、扣费、换图与成功事实登记——七件效果的算法留在 ## 拆分边界(需求卡片 C13):本件只负责可用性、扣费、换图与成功事实登记——七件效果的算法留在
## 效果引擎、授予段与伤害控制器。调用路径=呈现层→编排器单行转发→本件→行动经济/装板壳/授予段。 ## 效果引擎、授予段与伤害控制器。调用路径=呈现层→编排器单行转发→RoundCycle.refresh_flow()→本件
## →行动经济/装板壳/授予段(编排器入口与单行转发形态不变)。
## 耦合边界(5.1 封装修复换代):去 static 改实例——构造收 RoundCycle 复制的引用束,断言 8 键
## (facts/db/action_economy/fx_host/reward_economy/minesweeper/install_cb/grant_seg)解包同名成员;
## 变值标量(battle_result/round_index)走调用参数现值交付;授予段引用直接交换(段间引用,
## 不再穿编排器链式取段)——对编排器零引用。
## 反自动刷新(C11):只经玩家入口发起,禁 RAP 满自动刷——编排器公开面单行转发,无自动调用点。 ## 反自动刷新(C11):只经玩家入口发起,禁 RAP 满自动刷——编排器公开面单行转发,无自动调用点。
## 成功序钉死(T4):折价扣 RAP→refresh_used+1→旧资源退役→旧板退役→装新板→授予段成功事实发射; ## 成功序钉死(T4):折价扣 RAP→refresh_used+1→旧资源退役→旧板退役→装新板→授予段成功事实发射;
## 装板失败返回 false 且零效果——成功事实只在新板就位后发射。 ## 装板失败返回 false 且零效果——成功事实只在新板就位后发射。
## 随编排器每战新建同灭(RoundCycle 持有)。
var facts: BattleFacts # 战斗事实件(refresh_used 受理计数)
var db: ConfigDb
var action_economy: ActionEconomy
var fx_host: BattleFxHost # 效果宿主件(refresh_request 发射面)
var reward_economy: RewardEconomy # 地图战斗资源(旧批退役面)
var minesweeper: MinesweeperRules # 排雷玩法(旧板退役面)
var install_cb: Callable # 宿主 _install_and_reset——装板受理薄壳(保留面)
var grant_seg = null # GrantSegment 段间引用(成功事实发射——直接交接零链式)
func _init(bundle: Dictionary) -> void:
for k in ["facts", "db", "action_economy", "fx_host", "reward_economy", "minesweeper", "install_cb", "grant_seg"]:
assert(bundle.has(k), "[换图刷新流程件] 引用束缺键: %s" % k)
facts = bundle.facts
db = bundle.db
action_economy = bundle.action_economy
fx_host = bundle.fx_host
reward_economy = bundle.reward_economy
minesweeper = bundle.minesweeper
install_cb = bundle.install_cb
grant_seg = bundle.grant_seg
## 折价成本读数(refresh_request 命中省力周转 apply_discount——只认 kind==apply_discount 且 ## 折价成本读数(refresh_request 命中省力周转 apply_discount——只认 kind==apply_discount 且
## mode==refresh,取声明序第一命中,floor(base×cost_mult/100);无命中=表基线逐位不变。 ## mode==refresh,取声明序第一命中,floor(base×cost_mult/100);无命中=表基线逐位不变。
## can_refresh 与 execute 同源取数〔V3——can 与执行同源判断〕;折扣只改变门槛与扣费, ## can_refresh 与 execute 同源取数〔V3——can 与执行同源判断〕;折扣只改变门槛与扣费,
## 不改变拒绝判据〔C2——每轮上限与相位判据照旧〕) ## 不改变拒绝判据〔C2——每轮上限与相位判据照旧〕)
static func _effective_refresh_cost(oc: BattleOrchestrator) -> int: func _effective_refresh_cost() -> int:
var base := int(oc.db.run.action_economy.rap_refresh_cost) var base := int(db.run.action_economy.rap_refresh_cost)
for h in oc.fx_host().fx_emit("refresh_request", {"mode": "refresh", "base": base}): for h in fx_host.fx_emit("refresh_request", {"mode": "refresh", "base": base}):
if String(h.get("kind", "")) == "apply_discount" and String(h.get("mode", "")) == "refresh": if String(h.get("kind", "")) == "apply_discount" and String(h.get("mode", "")) == "refresh":
return int(floor(float(base) * float(int(h.get("cost_mult", 100))) / 100.0)) return int(floor(float(base) * float(int(h.get("cost_mult", 100))) / 100.0))
return base return base
## 可用性门(HUD 刷新按钮同源判据):战斗未终局、每轮上限未满、Active 相位、RAP 抵折后价。 ## 可用性门(HUD 刷新按钮同源判据):战斗未终局、每轮上限未满、Active 相位、RAP 抵折后价。
static func can_refresh(oc: BattleOrchestrator) -> bool: ## battle_result=调用参数现值交付。
return oc.battle_result == "" \ func can_refresh(battle_result: String) -> bool:
and oc._facts.refresh_used < int(oc.db.run.action_economy.refresh_cap_per_round) \ return battle_result == "" \
and oc.action_economy.phase == oc.action_economy.PHASE_ACTIVE \ and facts.refresh_used < int(db.run.action_economy.refresh_cap_per_round) \
and oc.action_economy.rap >= _effective_refresh_cost(oc) and action_economy.phase == action_economy.PHASE_ACTIVE \
and action_economy.rap >= _effective_refresh_cost()
## 执行(玩家请求刷新唯一通道体):扣费受理后继续;扣费拒绝/门拒零效果零事实。 ## 执行(玩家请求刷新唯一通道体):扣费受理后继续;扣费拒绝/门拒零效果零事实。
static func execute(oc: BattleOrchestrator) -> bool: ## battle_result/round_index=调用参数现值交付。
if not can_refresh(oc): func execute(battle_result: String, round_index: int) -> bool:
if not can_refresh(battle_result):
return false return false
if not oc.action_economy.request_refresh(_effective_refresh_cost(oc)): if not action_economy.request_refresh(_effective_refresh_cost()):
return false return false
oc._facts.refresh_used += 1 facts.refresh_used += 1
oc._reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提) reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提)
oc.minesweeper.retire_board(true) # 旧板退役(轮内通道) minesweeper.retire_board(true) # 旧板退役(轮内通道)
if not oc._install_and_reset(true): # 当场换图——失败即编排违例 fail-fast(零成功事实) if not bool(install_cb.call(true)): # 当场换图——失败即编排违例 fail-fast(零成功事实)
return false return false
oc._rounds._grant_seg.apply_board_refreshed(oc.round_index) # 成功事实:新板就位后一次发射 grant_seg.apply_board_refreshed(round_index) # 成功事实:新板就位后一次发射
return true return true
+51 -34
View File
@@ -1,22 +1,41 @@
class_name CombatSegment class_name CombatSegment
extends RefCounted extends RefCounted
# ③ 战斗段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 # ③ 战斗段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。
# 职责:案C 闸复位 + 本轮扫雷事实推导(执拗 streak 取数面)+ 蓄力层注入 + 主角先手 # 职责:案C 闸复位 + 本轮扫雷事实推导(执拗 streak 取数面)+ 主角先手
# + 敌方行动兑现 + 地图伤害账殿后(补提交统一结账)。 # + 敌方行动兑现 + 地图伤害账殿后(补提交统一结账)。
# 调用序:主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕;hero hp=0 短路。 # 调用序:主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕;hero hp=0 短路。
# 行发射/开单单点归宿主(oc)——案C 暂存闸归 RewardGrant(段持三引用直调,零转发壳)—— # 耦合边界(5.1 封装修复换代):引用束注入——构造收编排器组装的 20 键封闭束,本段断言 10 键
# (facts/units/damage/status_effects/fx_host/minesweeper/db/emit_row_cb/fatal_cb/reward_grant)
# 解包同名成员;行发射经 emit_row_cb 回宿主、fatal 经 fatal_cb 回宿主——对编排器零引用。
# 本函数体为原编排器 _combat_segment 逐字搬运(🔴 零行为变更契约)。 # 本函数体为原编排器 _combat_segment 逐字搬运(🔴 零行为变更契约)。
# run 编排骨架+三私有族(_hero_strike/_enemy_actions/_board_deferred_settle);段序逐字。 # run 编排骨架+三私有族(_hero_strike/_enemy_actions/_board_deferred_settle);段序逐字。
var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) var facts: BattleFacts # 战斗事实件(轮缓冲 streak 取数面)
var fx_host: BattleFxHost # 效果引擎宿主件(三引用构造——本轮无消费面,构造形与授予段同形) var units: UnitContainer # 单位容器(目标解析/存活查询)
var reward_grant: RewardGrant # 奖励授予件(案C 暂存闸 defer 持有方) var damage: DamageController # 伤害控制器(开单/提交面)
var status_effects: StatusContainer # 状态容器(增益/妨害施加面)
var fx_host: BattleFxHost # 效果引擎宿主件(facts_inject 写面)
var minesweeper: MinesweeperRules # 排雷玩法(束形统一交付面——卡面 10 键分配)
var db: ConfigDb
var emit_row_cb: Callable # 宿主 _emit_row——行协议面单点(5 参 head 束形)
var fatal_cb: Callable # 宿主 _fatal——battle_result 唯一写点留宿主
var reward_grant: RewardGrant # 奖励授予件(案C 暂存闸 defer 持有方)
func _init(host, fx_host_ref: BattleFxHost, reward_grant_ref: RewardGrant) -> void: func _init(bundle: Dictionary) -> void:
oc = host for k in ["facts", "units", "damage", "status_effects", "fx_host", "minesweeper", "db",
fx_host = fx_host_ref "emit_row_cb", "fatal_cb", "reward_grant"]:
reward_grant = reward_grant_ref assert(bundle.has(k), "[战斗段] 引用束缺键: %s" % k)
facts = bundle.facts
units = bundle.units
damage = bundle.damage
status_effects = bundle.status_effects
fx_host = bundle.fx_host
minesweeper = bundle.minesweeper
db = bundle.db
emit_row_cb = bundle.emit_row_cb
fatal_cb = bundle.fatal_cb
reward_grant = bundle.reward_grant
func run(ri: int) -> void: func run(ri: int) -> void:
@@ -26,14 +45,12 @@ func run(ri: int) -> void:
# 同一次 _settle 内,facts 已定。 # 同一次 _settle 内,facts 已定。
var round_safe := 0 var round_safe := 0
var round_mine := false var round_mine := false
for evt in oc._facts.reveals: for evt in facts.reveals:
if evt.kind == "safe": if evt.kind == "safe":
round_safe += 1 round_safe += 1
elif evt.kind == "mine": elif evt.kind == "mine":
round_mine = true round_mine = true
oc.fx_host().facts_inject["dig_streak"] = 0 if round_mine else round_safe fx_host.facts_inject["dig_streak"] = 0 if round_mine else round_safe
# 蓄力层不再逐轮注入(效果端口件闭面——fact charge_layers 由宿主 biz_fact 活读 action_economy
# 现值:唯一写点=finish_round_stop 于上轮授予段尾,本轮授予段发射前无第二写点,活读等价)
_hero_strike(ri) _hero_strike(ri)
_enemy_actions(ri) _enemy_actions(ri)
_board_deferred_settle(ri) _board_deferred_settle(ri)
@@ -48,26 +65,26 @@ func run(ri: int) -> void:
# 护甲等级掷点:基础分量吃掷点判定(含暴击);棋盘来源伤害不掷不折; # 护甲等级掷点:基础分量吃掷点判定(含暴击);棋盘来源伤害不掷不折;
# 任何伤害落地 ≥ damage_floor(全局 max(1,·) 适用所有来源,下限在控制器管线单点收口)。 # 任何伤害落地 ≥ damage_floor(全局 max(1,·) 适用所有来源,下限在控制器管线单点收口)。
func _hero_strike(ri: int) -> void: func _hero_strike(ri: int) -> void:
var hero_alive: bool = int(oc._enemy_system.hero_unit().get("hp", 0)) > 0 var hero_alive: bool = int(units.hero_unit().get("hp", 0)) > 0
var hero_target = oc._enemy_system.default_target() var hero_target = units.default_target()
if hero_alive and hero_target != null: if hero_alive and hero_target != null:
var ht := oc._damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击", var ht := damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击",
{"channel": "combat", "base_value": oc._hero_atk(), "target_rule": DamageController.TARGET_FRONT_ALIVE, {"channel": "combat", "base_value": damage.hero_atk(), "target_rule": DamageController.TARGET_FRONT_ALIVE,
"ri": ri, "origin_cell": Vector2i(-1, -1), "ri": ri, "origin_cell": Vector2i(-1, -1),
"attack_type": String(oc._enemy_system.hero_unit().get("attack_type", "normal"))}) "attack_type": String(units.hero_unit().get("attack_type", "normal"))})
oc._damage.submit(ht) damage.submit(ht)
# 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。 # 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。
# 死亡即不动手:摇定后、兑现前死亡的敌不在存活序中——本拍技能作废,不 commit、不进冷却。 # 死亡即不动手:摇定后、兑现前死亡的敌不在存活序中——本拍技能作废,不 commit、不进冷却。
func _enemy_actions(ri: int) -> void: func _enemy_actions(ri: int) -> void:
for u in oc._enemy_system.query_alive("enemy"): for u in units.query_alive("enemy"):
if int(oc._enemy_system.hero_unit().hp) <= 0: if int(units.hero_unit().hp) <= 0:
break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板) break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板)
var skill_id := oc._enemy_system.pending_action(u) var skill_id := units.pending_action(u)
var srow: Dictionary[Variant, Variant] = oc.db.combat.skill.row_by_id(skill_id) var srow: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(skill_id)
if srow.is_empty(): if srow.is_empty():
oc._fatal("敌实例待发动作悬空: %s" % skill_id) fatal_cb.call("敌实例待发动作悬空: %s" % skill_id)
continue continue
if String(srow.type) == "buff" or String(srow.type) == "debuff": if String(srow.type) == "buff" or String(srow.type) == "debuff":
# 增益/妨害技 = 0 伤害施放行:不掷不折;状态授予即数值兑现,技能冷却与增益持续两计时器分账。 # 增益/妨害技 = 0 伤害施放行:不掷不折;状态授予即数值兑现,技能冷却与增益持续两计时器分账。
@@ -75,9 +92,9 @@ func _enemy_actions(ri: int) -> void:
# 敌方增益技只强化施放者自己,不再全体受益);debuff → 主角(off_guard/sapped 同义主角侧)。 # 敌方增益技只强化施放者自己,不再全体受益);debuff → 主角(off_guard/sapped 同义主角侧)。
var dur: int = int(srow.get("buff_duration", 1)) var dur: int = int(srow.get("buff_duration", 1))
var owner_key: String = StatusContainer.owner_key_for(u) if String(srow.type) == "buff" else StatusContainer.OWNER_HERO var owner_key: String = StatusContainer.owner_key_for(u) if String(srow.type) == "buff" else StatusContainer.OWNER_HERO
oc._status_effects.apply_effect(String(srow.buff_ref), owner_key, {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur) status_effects.apply_effect(String(srow.buff_ref), owner_key, {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur)
oc._emit_row({"type": "cast", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1)}, ri, "%s 施放 %s" % [u.display_name, String(srow.display_name)], 0, {"skill_id": skill_id, "effect_id": String(srow.buff_ref), "duration": dur}) emit_row_cb.call({"type": "cast", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1)}, ri, "%s 施放 %s" % [u.display_name, String(srow.display_name)], 0, {"skill_id": skill_id, "effect_id": String(srow.buff_ref), "duration": dur})
oc._enemy_system.commit_action(u) units.commit_action(u)
continue continue
# 伤害技(含普攻——倍率恒 1)= 敌方伤害技单据:基线 = round(atk×倍率)、rider 预计算归开单侧 # 伤害技(含普攻——倍率恒 1)= 敌方伤害技单据:基线 = round(atk×倍率)、rider 预计算归开单侧
# (表值算术零随机),掷点/克制查表(spec 束 attack_type = 敌方攻击类型——迭代 24 N3 轴开列前恒空串不查表)/ # (表值算术零随机),掷点/克制查表(spec 束 attack_type = 敌方攻击类型——迭代 24 N3 轴开列前恒空串不查表)/
@@ -86,27 +103,27 @@ func _enemy_actions(ri: int) -> void:
var baseline: int = int(round(float(u.atk) * float(srow.get("damage_mult", 1.0)))) var baseline: int = int(round(float(u.atk) * float(srow.get("damage_mult", 1.0))))
var rider := 0 var rider := 0
if srow.has("rider_mult"): if srow.has("rider_mult"):
rider = maxi(int(oc.db.combat.settlement.damage_roll.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult)))) rider = maxi(int(db.combat.settlement.damage_roll.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult))))
var et := oc._damage.open_ticket(DamageController.KIND_ENEMY_SKILL, "enemy_action", var et := damage.open_ticket(DamageController.KIND_ENEMY_SKILL, "enemy_action",
"%s %s" % [u.display_name, String(srow.display_name)], "%s %s" % [u.display_name, String(srow.display_name)],
{"channel": "combat", "base_value": baseline, "target_rule": DamageController.TARGET_HERO, {"channel": "combat", "base_value": baseline, "target_rule": DamageController.TARGET_HERO,
"ri": ri, "origin_cell": Vector2i(-1, -1), "ri": ri, "origin_cell": Vector2i(-1, -1),
"attack_type": String(u.get("attack_type", "normal")), "rider": rider}) "attack_type": String(u.get("attack_type", "normal")), "rider": rider})
et["source_unit"] = u et["source_unit"] = u
et["context"] = {"skill_id": skill_id, "baseline": baseline} et["context"] = {"skill_id": skill_id, "baseline": baseline}
oc._damage.submit(et) damage.submit(et)
# 地图伤害兑付(案C:战斗段末——怪物还手后按授予段事件序补提交统一结账; # 地图伤害兑付(案C:战斗段末——怪物还手后按授予段事件序补提交统一结账;
# 主角死于怪物 → 地图账不兑,逐单留「未生效」行不作废静默——没生效也要留痕口径; # 主角死于怪物 → 地图账不兑,逐单留「未生效」行不作废静默——没生效也要留痕口径;
# 补提交后若地图直伤反杀主角,控制器 _halted 作废余单——战斗已败,胜负归判定段) # 补提交后若地图直伤反杀主角,控制器 _halted 作废余单——战斗已败,胜负归判定段)
func _board_deferred_settle(ri: int) -> void: func _board_deferred_settle(ri: int) -> void:
if int(oc._enemy_system.hero_unit().get("hp", 0)) <= 0: if int(units.hero_unit().get("hp", 0)) <= 0:
for t in (reward_grant.defer["queue"] as Array): for t in (reward_grant.defer["queue"] as Array):
oc._emit_row({"type": "reward_ineffective", "source_class": String(t.get("source_class", "")), "origin_cell": t.get("origin_cell", Vector2i(-1, -1))}, ri, emit_row_cb.call({"type": "reward_ineffective", "source_class": String(t.get("source_class", "")), "origin_cell": t.get("origin_cell", Vector2i(-1, -1))}, ri,
"%s 未生效(主角已倒下)" % String(t.get("display", "")), 0, {"forfeit": "hero_down"}) "%s 未生效(主角已倒下)" % String(t.get("display", "")), 0, {"forfeit": "hero_down"})
else: else:
for t in (reward_grant.defer["queue"] as Array): for t in (reward_grant.defer["queue"] as Array):
oc._damage.submit(t) damage.submit(t)
(reward_grant.defer["queue"] as Array).clear() (reward_grant.defer["queue"] as Array).clear()
oc._damage.flush() # 战斗段界结账(B 模式——主角单+敌方单+地图单按开单序统一兑现;A 模式地图单补提交时即结) damage.flush() # 战斗段界结账(B 模式——主角单+敌方单+地图单按开单序统一兑现;A 模式地图单补提交时即结)
+12 -6
View File
@@ -1,16 +1,22 @@
class_name DecaySegment class_name DecaySegment
extends RefCounted extends RefCounted
# ④ 衰减段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 # ④ 衰减段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。
# 职责:状态衰减恰一次恒执行含末轮——先兑付后衰减。行发射归宿主编排器(oc)—— # 职责:状态衰减恰一次恒执行含末轮——先兑付后衰减。
# 耦合边界(5.1 封装修复换代):引用束注入——构造收编排器组装的 20 键封闭束,本段断言 2 键
# (status_effects/emit_row_cb)解包同名成员;行发射经 emit_row_cb 回宿主——对编排器零引用。
# 零算法改动(🔴 零行为变更契约)。 # 零算法改动(🔴 零行为变更契约)。
var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主 var status_effects: StatusContainer # 状态容器(衰减推进面)
var emit_row_cb: Callable # 宿主 _emit_row——行协议面单点(5 参 head 束形)
func _init(host) -> void: func _init(bundle: Dictionary) -> void:
oc = host for k in ["status_effects", "emit_row_cb"]:
assert(bundle.has(k), "[衰减段] 引用束缺键: %s" % k)
status_effects = bundle.status_effects
emit_row_cb = bundle.emit_row_cb
func run(ri: int) -> void: func run(ri: int) -> void:
var k: int = oc._status_effects.decay_round() var k: int = status_effects.decay_round()
oc._emit_row({"type": "decay", "source_class": "", "origin_cell": Vector2i(-1, -1)}, ri, "状态衰减", k, {}) emit_row_cb.call({"type": "decay", "source_class": "", "origin_cell": Vector2i(-1, -1)}, ri, "状态衰减", k, {})
+103 -84
View File
@@ -2,24 +2,49 @@ class_name GrantSegment
extends RefCounted extends RefCounted
# ① 授予段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 # ① 授予段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。
# 职责:结算队列(开格/封印判定/清图)→ 极性路由逐批兑付 + 封印判/军规开单 + round_end 施加 + # 职责:结算队列(开格/封印判定/清图)→ 极性路由逐批兑付 + 封印判/军规开单 + round_end 施加 +
# 转存收口 + 揭示兑付 + 段界结账。行发射/开单单点归宿主(oc)——兑付管线归 RewardGrant、 # 转存收口 + 揭示兑付 + 段界结账。行发射经 emit_row_cb 回宿主——兑付管线归 RewardGrant、
# 效果施加归 BattleFxHost(段持三引用直调,零转发壳)—— # 效果施加归 BattleFxHost(段持注入引用直调,零转发壳)——
# 本段只搬运时序,零算法改动(🔴 零行为变更契约:同种子完整局输出逐位一致)。 # 本段只搬运时序,零算法改动(🔴 零行为变更契约:同种子完整局输出逐位一致)。
# 耦合边界(5.1 封装修复换代):引用束注入——构造收编排器组装的 20 键封闭束,本段断言 12 键
# (facts/units/damage/reward_economy/fx_host/minesweeper/db/action_economy/reward_grant/
# emit_row_cb/attune_param_sum_cb/perfect_sweep_cb)解包同名成员;对编排器零引用(跨件单向接线,
# 注入 Callable 亦属接线面);变值标量(round_index)走调用参数现值交付。
# run 编排骨架+批私有族(_reveal_batch/_terrain_batch/_seal_batch/_round_end_apply,段内施加面 # run 编排骨架+批私有族(_reveal_batch/_terrain_batch/_seal_batch/_round_end_apply,段内施加面
# 再拆 _reveal_safe_apply/_reveal_mine_apply/_seal_hit_effects 等单面函数);地形 damage_taken # 再拆 _reveal_safe_apply/_reveal_mine_apply/_seal_hit_effects 等单面函数);地形 damage_taken
# 消费随迭代 46 卡A T2 并入伤害控制器全局消费族(本段零本地减伤函数); # 消费随迭代 46 卡A T2 并入伤害控制器全局消费族(本段零本地减伤函数);
# kind 分派单点化(P1-1 同批):counter_add 五位三行组 / grant_material 三位兑付块 # kind 分派单点化(P1-1 同批):counter_add 五位三行组 / grant_material 三位兑付块
# 收 _apply_counter_hit / _apply_material_hit 两助手(分支本体逐字保留在助手内)。 # 收 _apply_counter_hit / _apply_material_hit 两助手(分支本体逐字保留在助手内)。
var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) var facts: BattleFacts # 战斗事实件(轮缓冲/封印账/溯源)
var fx_host: BattleFxHost # 效果引擎宿主件(fx 发射/施加/揭示队列/计数器桶) var units: UnitContainer # 单位容器(hero_unit 读面)
var reward_grant: RewardGrant # 奖励授予件(极性路由/行应用/提交面/清图奖励/escrow) var damage: DamageController # 伤害控制器(开单/承伤发射面)
var reward_economy: RewardEconomy # 地图战斗资源(grant/query_full_snapshot 消费面)
var fx_host: BattleFxHost # 效果引擎宿主件(fx 发射/施加/揭示队列/计数器桶)
var minesweeper: MinesweeperRules # 排雷玩法(板读取/变异过滤/危险格移除面)
var db: ConfigDb
var action_economy: ActionEconomy
var reward_grant: RewardGrant # 奖励授予件(极性路由/行应用/提交面/清图奖励/escrow)
var emit_row_cb: Callable # 宿主 _emit_row——行协议面单点(5 参 head 束形)
var attune_param_sum_cb: Callable # 宿主 attune_param_sum 读投影——转存率合计读数
var perfect_sweep_cb: Callable # 宿主 perfect_sweep_earned 读投影——清图奖励第二份判定
func _init(host, fx_host_ref: BattleFxHost, reward_grant_ref: RewardGrant) -> void: func _init(bundle: Dictionary) -> void:
oc = host for k in ["facts", "units", "damage", "reward_economy", "fx_host", "minesweeper", "db",
fx_host = fx_host_ref "action_economy", "reward_grant", "emit_row_cb", "attune_param_sum_cb", "perfect_sweep_cb"]:
reward_grant = reward_grant_ref assert(bundle.has(k), "[授予段] 引用束缺键: %s" % k)
facts = bundle.facts
units = bundle.units
damage = bundle.damage
reward_economy = bundle.reward_economy
fx_host = bundle.fx_host
minesweeper = bundle.minesweeper
db = bundle.db
action_economy = bundle.action_economy
reward_grant = bundle.reward_grant
emit_row_cb = bundle.emit_row_cb
attune_param_sum_cb = bundle.attune_param_sum_cb
perfect_sweep_cb = bundle.perfect_sweep_cb
func run(ri: int) -> void: func run(ri: int) -> void:
@@ -45,10 +70,10 @@ func apply_board_refreshed(ri: int) -> void:
for h in br_hits: for h in br_hits:
if String(h.get("kind", "")) == "grant_gp": if String(h.get("kind", "")) == "grant_gp":
reward_grant.escrow += int(h.get("value", 0)) reward_grant.escrow += int(h.get("value", 0))
oc._emit_row({"type": "effect", "source_class": "fx_grant_gp", "origin_cell": Vector2i(-1, -1)}, ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": reward_grant.escrow}) emit_row_cb.call({"type": "effect", "source_class": "fx_grant_gp", "origin_cell": Vector2i(-1, -1)}, ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": reward_grant.escrow})
for h in br_hits: for h in br_hits:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast": if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast":
var bb_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_board_refresh", "崩壁冲击", var bb_t := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_board_refresh", "崩壁冲击",
{"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE, {"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": Vector2i(-1, -1)}) "ri": ri, "origin_cell": Vector2i(-1, -1)})
reward_grant.defer_ticket(bb_t) reward_grant.defer_ticket(bb_t)
@@ -56,7 +81,7 @@ func apply_board_refreshed(ri: int) -> void:
# 开格/踩雷兑付批(轮 facts_reveals 资源事件序:safe 正向乘区兑付 / mine_hit 减半负向兑付) # 开格/踩雷兑付批(轮 facts_reveals 资源事件序:safe 正向乘区兑付 / mine_hit 减半负向兑付)
func _reveal_batch(ri: int) -> void: func _reveal_batch(ri: int) -> void:
for evt in oc._facts.reveals: for evt in facts.reveals:
if not bool(evt.get("resource", false)): if not bool(evt.get("resource", false)):
continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②) continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②)
var snap = evt.get("reward_snapshot") var snap = evt.get("reward_snapshot")
@@ -75,16 +100,14 @@ func _reveal_batch(ri: int) -> void:
# new_resource_type〔地质学家——快照 cell_type 首见判定,见后集登记〕) # new_resource_type〔地质学家——快照 cell_type 首见判定,见后集登记〕)
func _reveal_safe(evt: Dictionary, snap: Dictionary, ri: int) -> void: func _reveal_safe(evt: Dictionary, snap: Dictionary, ri: int) -> void:
if bool(evt.get("dug_under_seal", false)): if bool(evt.get("dug_under_seal", false)):
oc._emit_row({"type": "reward_ineffective", "source_class": "safe_cell", "origin_cell": evt.cell}, ri, "奖励作废(封印格直接挖开——零惩罚零奖励)", 0, {}) emit_row_cb.call({"type": "reward_ineffective", "source_class": "safe_cell", "origin_cell": evt.cell}, ri, "奖励作废(封印格直接挖开——零惩罚零奖励)", 0, {})
return return
var board_b: BoardData = oc.minesweeper.current_board() var board_b: BoardData = minesweeper.current_board()
var ctype := String(snap.get("cell_type", "")) var ctype := String(snap.get("cell_type", ""))
var is_new_type := ctype != "" and not fx_host._fx_new_resource_types.has(ctype) var is_new_type := fx_host.mark_new_resource_type(ctype)
if is_new_type:
fx_host._fx_new_resource_types[ctype] = true
var dr_hits := fx_host.fx_emit("dig_reveal", {"cell": evt.cell, "round": ri, var dr_hits := fx_host.fx_emit("dig_reveal", {"cell": evt.cell, "round": ri,
"is_first_dig_of_battle": not oc._facts.feat_first_dig_used, "is_first_dig_of_battle": not facts.feat_first_dig_used,
"terrain_kind": oc._harmful_variants(evt.get("variant_ids", []) as Array), "terrain_kind": minesweeper.harmful_variants(evt.get("variant_ids", []) as Array),
"is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0 "is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0
or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1, or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1,
"is_blank_zero": board_b.digit_of(evt.cell) == 0, "is_blank_zero": board_b.digit_of(evt.cell) == 0,
@@ -96,7 +119,7 @@ func _reveal_safe(evt: Dictionary, snap: Dictionary, ri: int) -> void:
continue continue
pos_m += int(h.get("mult", 0)) pos_m += int(h.get("mult", 0))
neg_m += int(h.get("cost_mult", 0)) neg_m += int(h.get("cost_mult", 0))
reward_grant.grant_routed(oc._feat_yield_adjust(snap, true, pos_m, neg_m), "positive", {"source_class": "safe_cell", "cell": evt.cell, "ri": ri}) reward_grant.grant_routed(reward_grant.feat_yield_adjust(snap, true, pos_m, neg_m), "positive", {"source_class": "safe_cell", "cell": evt.cell, "ri": ri})
_reveal_safe_apply(dr_hits, evt, ri) _reveal_safe_apply(dr_hits, evt, ri)
@@ -107,15 +130,15 @@ func _reveal_safe_apply(dr_hits: Array, evt: Dictionary, ri: int) -> void:
for h in dr_hits: for h in dr_hits:
if String(h.get("kind", "")) == "counter_add": if String(h.get("kind", "")) == "counter_add":
_apply_counter_hit(h) _apply_counter_hit(h)
if not oc._hits_of(dr_hits, "reveal_tiles").is_empty(): if not DamageController.hits_of(dr_hits, "reveal_tiles").is_empty():
fx_host.enqueue_reveal() fx_host.enqueue_reveal()
for h in dr_hits: for h in dr_hits:
if String(h.get("kind", "")) == "grant_material": if String(h.get("kind", "")) == "grant_material":
_apply_material_hit(h, evt.cell, ri) _apply_material_hit(h, evt.cell, ri)
var spell_hits: Array[Variant] = oc._hits_of(dr_hits, "spawn_spell") var spell_hits: Array[Variant] = DamageController.hits_of(dr_hits, "spawn_spell")
if not spell_hits.is_empty() and not oc._facts.feat_first_dig_used: if not spell_hits.is_empty() and not facts.feat_first_dig_used:
oc._facts.feat_first_dig_used = true facts.feat_first_dig_used = true
var spell_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", "奥术矢", var spell_t := damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", "奥术矢",
{"channel": "board", "base_value": int((spell_hits[0] as Dictionary).get("value", 0)), {"channel": "board", "base_value": int((spell_hits[0] as Dictionary).get("value", 0)),
"target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri, "origin_cell": evt.cell, "attack_type": "magic"}) "target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri, "origin_cell": evt.cell, "attack_type": "magic"})
reward_grant.submit(spell_t, false) reward_grant.submit(spell_t, false)
@@ -128,19 +151,19 @@ func _reveal_mine(evt: Dictionary, snap: Dictionary, ri: int) -> void:
var mine_power := int(snap.get("power", 0)) var mine_power := int(snap.get("power", 0))
var mh_hits := fx_host.fx_emit("mine_hit", {"cell": evt.cell, "round": ri, var mh_hits := fx_host.fx_emit("mine_hit", {"cell": evt.cell, "round": ri,
"damage": mine_power, "dug_under_seal": bool(evt.get("dug_under_seal", false)), "damage": mine_power, "dug_under_seal": bool(evt.get("dug_under_seal", false)),
"hp_ratio": int(round(100.0 * float(int(oc._enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(oc._enemy_system.hero_unit().get("max_hp", 1))))))}) "hp_ratio": int(round(100.0 * float(int(units.hero_unit().get("hp", 0))) / float(maxi(1, int(units.hero_unit().get("max_hp", 1))))))})
var applied := _reveal_mine_apply(mh_hits, snap, evt.cell, ri) var applied := _reveal_mine_apply(mh_hits, snap, evt.cell, ri)
if int(applied["mine_reduce"]) > 0: if int(applied["mine_reduce"]) > 0:
snap["power"] = maxi(0, mine_power - int(applied["mine_reduce"])) # 减伤落快照(负向兑付按减后值——安全帽每层减 1) snap["power"] = maxi(0, mine_power - int(applied["mine_reduce"])) # 减伤落快照(负向兑付按减后值——安全帽每层减 1)
for h in mh_hits: for h in mh_hits:
if String(h.get("kind", "")) == "counter_add": if String(h.get("kind", "")) == "counter_add":
_apply_counter_hit(h) _apply_counter_hit(h)
if not oc._hits_of(mh_hits, "reveal_tiles").is_empty(): if not DamageController.hits_of(mh_hits, "reveal_tiles").is_empty():
fx_host.enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付) fx_host.enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付)
reward_grant.grant_routed(oc._feat_yield_adjust(snap, false, 0, 0), "negative", {"source_class": "mine_hit", "cell": evt.cell, "ri": ri, "halve_pct": int(applied["halve_pct"])}) reward_grant.grant_routed(reward_grant.feat_yield_adjust(snap, false, 0, 0), "negative", {"source_class": "mine_hit", "cell": evt.cell, "ri": ri, "halve_pct": int(applied["halve_pct"])})
for h in mh_hits: for h in mh_hits:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect": if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect":
var nct := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", var nct := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹",
{"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE, {"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": evt.cell}) "ri": ri, "origin_cell": evt.cell})
reward_grant.submit(nct, false) reward_grant.submit(nct, false)
@@ -162,15 +185,14 @@ func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: in
else: else:
mine_reduce += int(h.get("value", 0)) # 背水——定值减免 mine_reduce += int(h.get("value", 0)) # 背水——定值减免
"heal": "heal":
var hu_heal: Dictionary[Variant, Variant] = oc._enemy_system.hero_unit() var hu_heal: Dictionary[Variant, Variant] = units.hero_unit()
hu_heal["hp"] = mini(int(hu_heal.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_heal.get("max_hp", 1)))) hu_heal["hp"] = mini(int(hu_heal.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_heal.get("max_hp", 1))))
"bonus_delta": "bonus_delta":
var axis := String(h.get("axis", "")) var axis := String(h.get("axis", ""))
if fx_host._fx_bonus_delta.has(axis): fx_host.add_bonus_delta(axis, int(h.get("delta", 0)))
fx_host._fx_bonus_delta[axis] += int(h.get("delta", 0))
"blast_scaled": "blast_scaled":
var blast_v := maxi(int(oc.db.combat.settlement.damage_roll.damage_floor), int(float(int(snap.get("power", 0))) * float(h.get("mult", 1)))) var blast_v := maxi(int(db.combat.settlement.damage_roll.damage_floor), int(float(int(snap.get("power", 0))) * float(h.get("mult", 1))))
var bs_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发", var bs_t := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发",
{"channel": "board", "base_value": blast_v, "target_rule": DamageController.TARGET_ALL_ALIVE, {"channel": "board", "base_value": blast_v, "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": cell}) "ri": ri, "origin_cell": cell})
reward_grant.submit(bs_t, false) reward_grant.submit(bs_t, false)
@@ -182,21 +204,21 @@ func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: in
# 危险格恒无变异——布置段排除,双层叠罚不存在;封印格直挖照吃变异伤害——地形后果同 mine_hit 口径, # 危险格恒无变异——布置段排除,双层叠罚不存在;封印格直挖照吃变异伤害——地形后果同 mine_hit 口径,
# 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场) # 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场)
func _terrain_batch(ri: int) -> void: func _terrain_batch(ri: int) -> void:
for evt in oc._facts.reveals: for evt in facts.reveals:
var vids: Array[Variant] = evt.get("variant_ids", []) var vids: Array[Variant] = evt.get("variant_ids", [])
for vid in vids: for vid in vids:
var vrow: Dictionary[Variant, Variant] = oc.db.combat.variation.row_by_id(String(vid)) var vrow: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(String(vid))
if String(vrow.get("effect", "")) != "flat_damage": if String(vrow.get("effect", "")) != "flat_damage":
continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面) continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面)
# 地形变异伤害单据——无类型必中,下限保底在控制器管线内 # 地形变异伤害单据——无类型必中,下限保底在控制器管线内
# damage_taken 统一发射(迭代 46 卡A T2——地形路径并入全局消费族 _emit_hero_damage_taken: # damage_taken 统一发射(迭代 46 卡A T2——地形路径并入全局消费族 emit_hero_damage_taken:
# 发射在单据开立前、减后值开单,组合消费含 shield〔全局受伤语义〕; # 发射在单据开立前、减后值开单,组合消费含 shield〔全局受伤语义〕;
# hero_damage_consumed 标记入单据 context——战斗段末 _resolve_board_flat_hero 跳过重复发射; # hero_damage_consumed 标记入单据 context——战斗段末 _resolve_board_flat_hero 跳过重复发射;
# grant_material〔荆棘/地脉〕移到单据提交后(兑付值序一致——收益不因减伤变化)) # grant_material〔荆棘/地脉〕移到单据提交后(兑付值序一致——收益不因减伤变化))
var dt_amount := int(vrow.get("effect_value", 0)) var dt_amount := int(vrow.get("effect_value", 0))
var dt_res := oc._damage._emit_hero_damage_taken(dt_amount, "terrain", String(vid), evt.cell, ri) var dt_res := damage.emit_hero_damage_taken(dt_amount, "terrain", String(vid), evt.cell, ri)
dt_amount = int(dt_res["total"]) dt_amount = int(dt_res["total"])
var vt := oc._damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), var vt := damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)),
{"channel": "board", "base_value": dt_amount, "target_rule": DamageController.TARGET_HERO, {"channel": "board", "base_value": dt_amount, "target_rule": DamageController.TARGET_HERO,
"ri": ri, "origin_cell": evt.cell, "context": {"variant_id": String(vid), "hero_damage_consumed": true}}) "ri": ri, "origin_cell": evt.cell, "context": {"variant_id": String(vid), "hero_damage_consumed": true}})
reward_grant.submit(vt, false) reward_grant.submit(vt, false)
@@ -210,7 +232,7 @@ func _terrain_batch(ri: int) -> void:
# 台账披露待追认〕;完美布防=本轮封印批全中(hit>0 且 miss==0)时封印伤 ×mult——全对推理奖励)。 # 台账披露待追认〕;完美布防=本轮封印批全中(hit>0 且 miss==0)时封印伤 ×mult——全对推理奖励)。
# 伤害单据案C 暂存战斗段末兑付。 # 伤害单据案C 暂存战斗段末兑付。
func _seal_batch(ri: int) -> void: func _seal_batch(ri: int) -> void:
for evt in oc._facts.seals: for evt in facts.seals:
_judge_seal(evt, ri) _judge_seal(evt, ri)
_seal_strike_batch(ri) _seal_strike_batch(ri)
@@ -220,7 +242,7 @@ func _seal_batch(ri: int) -> void:
func _seal_strike_batch(ri: int) -> void: func _seal_strike_batch(ri: int) -> void:
var fs_hits := 0 var fs_hits := 0
var fs_misses := 0 var fs_misses := 0
for fevt in oc._facts.seals: for fevt in facts.seals:
if bool(fevt.get("hit", false)): if bool(fevt.get("hit", false)):
fs_hits += 1 fs_hits += 1
else: else:
@@ -230,10 +252,10 @@ func _seal_strike_batch(ri: int) -> void:
var strike := _seal_strike_value(fs_fx, fs_hits, fs_misses) var strike := _seal_strike_value(fs_fx, fs_hits, fs_misses)
if strike.is_empty(): if strike.is_empty():
return return
for fevt in oc._facts.seals: for fevt in facts.seals:
if not bool(fevt.get("hit", false)): if not bool(fevt.get("hit", false)):
continue continue
var st_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_seal_strike", "战封印军规", var st_t := damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_seal_strike", "战封印军规",
{"channel": "board", "base_value": int(strike["value"]), "target_rule": DamageController.TARGET_FRONT_ALIVE, {"channel": "board", "base_value": int(strike["value"]), "target_rule": DamageController.TARGET_FRONT_ALIVE,
"ri": ri, "origin_cell": fevt.cell, "attack_type": "normal"}) "ri": ri, "origin_cell": fevt.cell, "attack_type": "normal"})
reward_grant.submit(st_t, false) reward_grant.submit(st_t, false)
@@ -259,8 +281,7 @@ func _seal_strike_value(fs_fx: Array, fs_hits: int, fs_misses: int) -> Dictionar
# round_end 发射+施加(每轮结算收口:熔火之躯反打单开立〔open_ticket 即时执行〕+积层清零〔counter_add # round_end 发射+施加(每轮结算收口:熔火之躯反打单开立〔open_ticket 即时执行〕+积层清零〔counter_add
# set〕;整备转蓄力由 finish_round_stop 承载——停手侧 AP 已在 request_stop 记账,转存决策=grant_ap # set〕;停手侧 AP 已在 request_stop 记账,finish_round_stop 按转存率转存 RAP)+施加面:grant_gp〔爆破会计/收盘——escrow 战斗域
# (mode:transfer) 订阅定夺,无订阅走 RAP 转存缺省)+施加面:grant_gp〔爆破会计/收盘——escrow 战斗域
# 缓冲,随 gp_dropped 上抛〕/grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始 # 缓冲,随 gp_dropped 上抛〕/grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始
# 加成同面〕+段界收口(转存率入参、清图奖励、揭示兑付、段界结账) # 加成同面〕+段界收口(转存率入参、清图奖励、揭示兑付、段界结账)
func _round_end_apply(ri: int) -> void: func _round_end_apply(ri: int) -> void:
@@ -273,17 +294,16 @@ func _round_end_apply(ri: int) -> void:
for h in re_fx: for h in re_fx:
if String(h.get("kind", "")) == "grant_gp": if String(h.get("kind", "")) == "grant_gp":
reward_grant.escrow += int(h.get("value", 0)) reward_grant.escrow += int(h.get("value", 0))
oc._emit_row({"type": "effect", "source_class": "fx_grant_gp", "origin_cell": Vector2i(-1, -1)}, ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": reward_grant.escrow}) emit_row_cb.call({"type": "effect", "source_class": "fx_grant_gp", "origin_cell": Vector2i(-1, -1)}, ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": reward_grant.escrow})
elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
oc.action_economy.queue_ap_delta(int(h.get("value", 0))) action_economy.queue_ap_delta(int(h.get("value", 0)))
# 转存率入参(储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1;未装配=100 与 1:1 逐位一致) # 转存率入参(储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1;未装配=100 与 1:1 逐位一致)
var rap_pct: int = oc.attune_param_sum("rap_transfer_mult") var rap_pct: int = int(attune_param_sum_cb.call("rap_transfer_mult"))
oc.action_economy.finish_round_stop(fx_host.engine.has_effect_on("round_end", "grant_ap", "transfer"), action_economy.finish_round_stop(rap_pct if rap_pct > 0 else 100)
rap_pct if rap_pct > 0 else 100) if facts.path == "cleared":
if oc._facts.path == "cleared": reward_grant.grant_clear_reward(ri, facts.board_from_midround, bool(perfect_sweep_cb.call()))
reward_grant.grant_clear_reward(ri, oc._facts.board_from_midround, oc.perfect_sweep_earned())
fx_host.flush_pending_reveals(ri) # 揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入) fx_host.flush_pending_reveals(ri) # 揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入)
oc._damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界) damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界)
# round_end heal value_counter 兑付(迭代 46 卡A T5——坑道小憩:层值=本轮成功刷新累计恢复点, # round_end heal value_counter 兑付(迭代 46 卡A T5——坑道小憩:层值=本轮成功刷新累计恢复点,
@@ -296,12 +316,12 @@ func _round_end_heal_value_counter(re_fx: Array, ri: int) -> void:
var layers := EffectEngine.counter(fx_host.counters_ctx, String(h.get("value_counter", ""))) var layers := EffectEngine.counter(fx_host.counters_ctx, String(h.get("value_counter", "")))
if layers <= 0: if layers <= 0:
continue continue
var hu: Dictionary[Variant, Variant] = oc._enemy_system.hero_unit() var hu: Dictionary[Variant, Variant] = units.hero_unit()
if int(hu.get("hp", 0)) >= int(hu.get("max_hp", 1)): if int(hu.get("hp", 0)) >= int(hu.get("max_hp", 1)):
oc._emit_row({"type": "reward_ineffective", "source_class": "fx_board_refresh", "origin_cell": Vector2i(-1, -1)}, ri, "坑道小憩 未生效(满血)", 0, {"batch_key": String(h.get("value_counter", ""))}) emit_row_cb.call({"type": "reward_ineffective", "source_class": "fx_board_refresh", "origin_cell": Vector2i(-1, -1)}, ri, "坑道小憩 未生效(满血)", 0, {"batch_key": String(h.get("value_counter", ""))})
continue continue
var healed: int = oc._enemy_system.heal(hu, layers) var healed: int = units.heal(hu, layers)
oc._emit_row({"type": "heal", "source_class": "fx_board_refresh", "origin_cell": Vector2i(-1, -1)}, ri, "坑道小憩 回复", healed, {"target": "hero"}) emit_row_cb.call({"type": "heal", "source_class": "fx_board_refresh", "origin_cell": Vector2i(-1, -1)}, ri, "坑道小憩 回复", healed, {"target": "hero"})
# round_end blast 通道反打单(per-counter 积层读数×value=反打总量;source_class 用中性名, # round_end blast 通道反打单(per-counter 积层读数×value=反打总量;source_class 用中性名,
@@ -312,10 +332,10 @@ func _round_end_blast_tickets(re_fx: Array, ri: int) -> void:
var mg_layers := EffectEngine.counter(fx_host.counters_ctx, String(h.get("per", ""))) var mg_layers := EffectEngine.counter(fx_host.counters_ctx, String(h.get("per", "")))
var mg_total := int(h.get("value", 0)) * mg_layers var mg_total := int(h.get("value", 0)) * mg_layers
if mg_total > 0: if mg_total > 0:
var mg_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_blast_counter", var mg_t := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_blast_counter",
"积层反噬 ×%d" % mg_layers, "积层反噬 ×%d" % mg_layers,
{"channel": "board", "base_value": mg_total, "target_rule": DamageController.TARGET_ALL_ALIVE, {"channel": "board", "base_value": mg_total, "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": oc.minesweeper.hero_pos()}) "ri": ri, "origin_cell": minesweeper.hero_pos()})
reward_grant.submit(mg_t, false) reward_grant.submit(mg_t, false)
@@ -331,12 +351,12 @@ func _judge_seal(evt: Dictionary, ri: int) -> void:
# 殉爆票据 → 施加面 → 雷区图谱额外兑付 → 基线正向兑付) # 殉爆票据 → 施加面 → 雷区图谱额外兑付 → 基线正向兑付)
func _judge_seal_hit(evt: Dictionary, ri: int) -> void: func _judge_seal_hit(evt: Dictionary, ri: int) -> void:
var cell: Vector2i = evt.cell var cell: Vector2i = evt.cell
if oc._facts.seal_consumed.has(cell): if facts.seal_consumed.has(cell):
return # 去重跳过;零明细行 return # 去重跳过;零明细行
oc._facts.seal_consumed[cell] = true facts.seal_consumed[cell] = true
oc._facts.mark_perfect_hit() facts.mark_perfect_hit()
var fh_hits := fx_host.fx_emit("seal_hit", {"cell": cell, "round": ri, var fh_hits := fx_host.fx_emit("seal_hit", {"cell": cell, "round": ri,
"terrain_kind": oc._harmful_adjacent(cell)}) "terrain_kind": minesweeper.harmful_adjacent(cell)})
_seal_burst_tickets(fh_hits, cell, ri) _seal_burst_tickets(fh_hits, cell, ri)
_seal_hit_effects(fh_hits, cell, ri) _seal_hit_effects(fh_hits, cell, ri)
# 雷区图谱(封印流——标中雷额外获得一份格面资源:封印开始产出对标开格; # 雷区图谱(封印流——标中雷额外获得一份格面资源:封印开始产出对标开格;
@@ -344,7 +364,7 @@ func _judge_seal_hit(evt: Dictionary, ri: int) -> void:
for h in fh_hits: for h in fh_hits:
if String(h.get("kind", "")) == "grant_material": if String(h.get("kind", "")) == "grant_material":
_apply_material_hit(h, cell, ri, "seal_hit") _apply_material_hit(h, cell, ri, "seal_hit")
var snap = oc._reward_economy.grant(cell) var snap = reward_economy.grant(cell)
if not (snap is Dictionary): if not (snap is Dictionary):
return return
reward_grant.grant_routed(snap, "positive", {"source_class": "seal_hit", "cell": cell, "ri": ri, "highlight": "seal_hit"}) reward_grant.grant_routed(snap, "positive", {"source_class": "seal_hit", "cell": cell, "ri": ri, "highlight": "seal_hit"})
@@ -353,10 +373,10 @@ func _judge_seal_hit(evt: Dictionary, ri: int) -> void:
# 标中殉爆票据(行 triggers open_ticket channel blast value_from magnitude——每标中一面封印全体存活敌 # 标中殉爆票据(行 triggers open_ticket channel blast value_from magnitude——每标中一面封印全体存活敌
# 吃 N 群伤,无类型走 blast 通道;封印爆裂〔专长〕同面消费) # 吃 N 群伤,无类型走 blast 通道;封印爆裂〔专长〕同面消费)
func _seal_burst_tickets(fh_hits: Array, cell: Vector2i, ri: int) -> void: func _seal_burst_tickets(fh_hits: Array, cell: Vector2i, ri: int) -> void:
for h in oc._hits_of(fh_hits, "open_ticket"): for h in DamageController.hits_of(fh_hits, "open_ticket"):
if String(h.get("channel", "")) != "blast": if String(h.get("channel", "")) != "blast":
continue continue
var fbt := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_seal_burst", "封印爆裂", var fbt := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_seal_burst", "封印爆裂",
{"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE, {"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": cell, "highlight": "highlight"}) "ri": ri, "origin_cell": cell, "highlight": "highlight"})
reward_grant.submit(fbt, false) reward_grant.submit(fbt, false)
@@ -375,37 +395,36 @@ func _seal_hit_effects(fh_hits: Array, cell: Vector2i, ri: int) -> void:
fx_host.apply_ac_bonus(h) fx_host.apply_ac_bonus(h)
"bonus_delta": "bonus_delta":
var axis_f := String(h.get("axis", "")) var axis_f := String(h.get("axis", ""))
if fx_host._fx_bonus_delta.has(axis_f): fx_host.add_bonus_delta(axis_f, int(h.get("delta", 0)))
fx_host._fx_bonus_delta[axis_f] += int(h.get("delta", 0))
"remove_mines": "remove_mines":
var removed: Array[Variant] = oc.minesweeper.remove_mines_in([cell]) var removed: Array[Variant] = minesweeper.remove_mines_in([cell])
if not removed.is_empty(): if not removed.is_empty():
oc._emit_row({"type": "extended", "source_class": "fx_seal_smith", "origin_cell": cell}, ri, "封印匠·危险格中立移除", 0, {"effect_id": "mine_neutralized"}) emit_row_cb.call({"type": "extended", "source_class": "fx_seal_smith", "origin_cell": cell}, ri, "封印匠·危险格中立移除", 0, {"effect_id": "mine_neutralized"})
"preview_area": "preview_area":
_seal_preview_area(cell, ri) _seal_preview_area(cell, ri)
# 示警烟火施加面(3×3 邻格资源类型预告——类型去重集入 overlay;信息呈现零回写) # 示警烟火施加面(3×3 邻格资源类型预告——类型去重集入 overlay;信息呈现零回写)
func _seal_preview_area(cell: Vector2i, ri: int) -> void: func _seal_preview_area(cell: Vector2i, ri: int) -> void:
var board_p: BoardData = oc.minesweeper.current_board() var board_p: BoardData = minesweeper.current_board()
var types: Array[Variant] = [] var types: Array[Variant] = []
for n in board_p.neighbors8(cell): for n in board_p.neighbors8(cell):
var n_snap = oc._reward_economy.query_full_snapshot(n) var n_snap = reward_economy.query_full_snapshot(n)
if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))): if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))):
types.append(String((n_snap as Dictionary).get("cell_type", ""))) types.append(String((n_snap as Dictionary).get("cell_type", "")))
if reward_grant.treasure_inventory != null and not types.is_empty(): if reward_grant.treasure_inventory != null and not types.is_empty():
reward_grant.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell)) reward_grant.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
oc._emit_row({"type": "extended", "source_class": "fx_warning_beacon", "origin_cell": cell}, ri, "示警烟火·周围资源类型", 0, {"effect_id": "fx_preview_area", "types": types}) emit_row_cb.call({"type": "extended", "source_class": "fx_warning_beacon", "origin_cell": cell}, ri, "示警烟火·周围资源类型", 0, {"effect_id": "fx_preview_area", "types": types})
# 封印未中(每格实例一次去重——归因行与负向兑付同键,防逐轮惩罚轰炸;seal_missed 发射序: # 封印未中(每格实例一次去重——归因行与负向兑付同键,防逐轮惩罚轰炸;seal_missed 发射序:
# grant_ap mode:queue 下一轮兑现 / forgive_seal_miss 完美保险豁免 → 归因行 → 负向兑付) # grant_ap mode:queue 下一轮兑现 / forgive_seal_miss 完美保险豁免 → 归因行 → 负向兑付)
func _judge_seal_miss(evt: Dictionary, ri: int) -> void: func _judge_seal_miss(evt: Dictionary, ri: int) -> void:
var cell: Vector2i = evt.cell var cell: Vector2i = evt.cell
oc._facts.mark_perfect_miss() facts.mark_perfect_miss()
if oc._facts.seal_miss_seen.has(cell): if facts.seal_miss_seen.has(cell):
return # 每格实例一次去重 return # 每格实例一次去重
oc._facts.seal_miss_seen[cell] = true facts.seal_miss_seen[cell] = true
# 危险直觉·守挂点(seal_missed 发射:契约 12 触发主语=「当回合发生的标错 # 危险直觉·守挂点(seal_missed 发射:契约 12 触发主语=「当回合发生的标错
# 动作」——去重闸=新旧标错判别器〔后续轮重判旧封印被闸挡回零触发〕;双条件②「结算时封印仍在 # 动作」——去重闸=新旧标错判别器〔后续轮重判旧封印被闸挡回零触发〕;双条件②「结算时封印仍在
# 场」由发射面既有过滤承载〔被挖开的封印已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮 # 场」由发射面既有过滤承载〔被挖开的封印已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮
@@ -413,17 +432,17 @@ func _judge_seal_miss(evt: Dictionary, ri: int) -> void:
# adventure 域计数器闸) # adventure 域计数器闸)
for h in fx_host.fx_emit("seal_missed", {"cell": cell, "round": ri}): for h in fx_host.fx_emit("seal_missed", {"cell": cell, "round": ri}):
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
oc.action_economy.queue_ap_delta(int(h.get("value", 0))) action_economy.queue_ap_delta(int(h.get("value", 0)))
elif String(h.get("kind", "")) == "forgive_seal_miss": elif String(h.get("kind", "")) == "forgive_seal_miss":
# 完美保险(订阅式豁免:本战首次标错计一次豁免,只累加自有账 # 完美保险(订阅式豁免:本战首次标错计一次豁免,只累加自有账
# _perfect_forgiven,绝不回写 _perfect_hit_false=契约 2 计数纯净;发射时机与消费点零改动=契约 3) # _perfect_forgiven,绝不回写 _perfect_hit_false=契约 2 计数纯净;发射时机与消费点零改动=契约 3)
oc._facts.mark_perfect_forgiven() facts.mark_perfect_forgiven()
_apply_counter_hit(h) # 豁免同时记账(insurance_used battle 域——下次标错 when lt 1 不再命中) _apply_counter_hit(h) # 豁免同时记账(insurance_used battle 域——下次标错 when lt 1 不再命中)
oc._emit_row({"type": "extended", "source_class": "seal_miss", "origin_cell": cell}, ri, "完美保险·标错豁免", 0, emit_row_cb.call({"type": "extended", "source_class": "seal_miss", "origin_cell": cell}, ri, "完美保险·标错豁免", 0,
{"effect_id": "insurance_used", "forgiven": oc._facts.perfect_forgiven}) {"effect_id": "insurance_used", "forgiven": facts.perfect_forgiven})
var hl := "perfect_broken" if oc._facts.perfect_hit_false - oc._facts.perfect_forgiven == 1 else "" var hl := "perfect_broken" if facts.perfect_hit_false - facts.perfect_forgiven == 1 else ""
oc._emit_row({"type": "seal_miss", "source_class": "seal_miss", "origin_cell": cell, "highlight": hl}, ri, "封印未中", 0, {}) emit_row_cb.call({"type": "seal_miss", "source_class": "seal_miss", "origin_cell": cell, "highlight": hl}, ri, "封印未中", 0, {})
var snap = oc._reward_economy.grant(cell) var snap = reward_economy.grant(cell)
if not (snap is Dictionary): if not (snap is Dictionary):
return return
reward_grant.grant_routed(snap, "negative", {"source_class": "seal_miss", "cell": cell, "ri": ri}) reward_grant.grant_routed(snap, "negative", {"source_class": "seal_miss", "cell": cell, "ri": ri})
@@ -437,6 +456,6 @@ func _apply_counter_hit(h: Dictionary) -> void:
# grant_material 兑付块单点(三位分支本体逐字收拢——source_class 两态 safe_cell/seal_hit 由参承载) # grant_material 兑付块单点(三位分支本体逐字收拢——source_class 两态 safe_cell/seal_hit 由参承载)
func _apply_material_hit(h: Dictionary, cell: Vector2i, ri: int, source_class: String = "safe_cell") -> void: func _apply_material_hit(h: Dictionary, cell: Vector2i, ri: int, source_class: String = "safe_cell") -> void:
var g_snap = oc._reward_economy.grant(cell) var g_snap = reward_economy.grant(cell)
if g_snap is Dictionary: if g_snap is Dictionary:
reward_grant.grant_routed(g_snap, "positive", {"source_class": source_class, "cell": cell, "ri": ri, "label_suffix": String(h.get("label", ""))}) reward_grant.grant_routed(g_snap, "positive", {"source_class": source_class, "cell": cell, "ri": ri, "label_suffix": String(h.get("label", ""))})
+67 -38
View File
@@ -1,15 +1,26 @@
## 轮循环驱动件:每轮结算周期的唯一驱动——结算重入门闸(settling)、五段段序执行 ## 轮循环驱动件:每轮结算周期的唯一驱动——结算重入门闸(settling)、五段段序执行
## (段序骨架冻结于 SettlementTable.segment_order)、开轮摇号(共识点 B:技能选择前置到轮次开始, ## (段序骨架冻结于 SettlementTable.segment_order)、开轮摇号(共识点 B:技能选择前置到轮次开始,
## 摇定即本拍结算的选技)、踩雷计数账(转存时点=摇号轮末)。 ## 摇定即本拍结算的选技)、踩雷计数账(转存时点=摇号轮末)、换图刷新流程件持有与交接。
## 单一变化原因:轮结算周期的推进结构(段序、重入闸、摇号时序)。 ## 单一变化原因:轮结算周期的推进结构(段序、重入闸、摇号时序)。
## 耦合边界:oc=宿主引用(五段段类既定同款直调形态)——经 oc 取 facts/协作件/行发射/出口分发; ## 耦合边界(5.1 封装修复换代):引用束注入——构造收编排器组装的 20 键封闭束(六子对象+协作件
## 五段构造参数与持有形态不变(grant/combat 三引用、aura/decay/verdict 单引用);判定段零改动 ## 引用+读数引用+七回调+两读投影),解包自用七键(facts/units/fx_host/db/action_economy/rng/
## (_exit_distribution 留宿主);随机只经 oc.rng 的 behavior_roll 封闭流(流名、开流时点、取号序逐字); ## emit_row_cb)后原束分发五段(各段同束自取所需),段类对编排器零引用;变值标量
## 零信号连接、零 await;随编排器每战新建同灭。 ## (battle_result/round_index)走调用参数现值交付(settle_if_due/roll_round_actions 签名扩展);
## 换图刷新流程件=本件持有:五段构造后复制束写入 grant_seg 段间引用再建 BattleRefreshFlow
## (授予段引用直接交换——不再穿编排器链式取段)。随机只经注入 rng 的 behavior_roll 封闭流
## (流名、开流时点、取号序逐字);零信号连接、零 await;随编排器每战新建同灭。
## 类变量封闭面(15≤15 贴顶禁再增):束解包 7(facts/units/fx_host/db/action_economy/rng/emit_row_cb)
## +mines_hits+五段+_settling+_refresh_flow。
class_name RoundCycle class_name RoundCycle
extends RefCounted extends RefCounted
var oc: BattleOrchestrator # 宿主引用(段类同款直调——facts/协作件/行发射/出口分发取数单点) var facts: BattleFacts # 战斗事实件(受理事实/轮缓冲/封印账/溯源/条件计数)
var units: UnitContainer # 单位容器(摇号/解锁/冷却消费面)
var fx_host: BattleFxHost # 效果宿主件(enemy_hp_threshold 发射面)
var db: ConfigDb
var action_economy: ActionEconomy
var rng: RngServiceCore
var emit_row_cb: Callable # 宿主 _emit_row——行协议面单点(5 参 head 束形)
var mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(本场累计/上轮转存/当轮累计三读数合一账) var mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(本场累计/上轮转存/当轮累计三读数合一账)
var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点) var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点)
var _aura_seg = null # AuraSegment(②增益/减益段) var _aura_seg = null # AuraSegment(②增益/减益段)
@@ -17,27 +28,40 @@ var _combat_seg = null # CombatSegment(③战斗段)
var _decay_seg = null # DecaySegment(④衰减段) var _decay_seg = null # DecaySegment(④衰减段)
var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归宿主) var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归宿主)
var _settling := false var _settling := false
var _refresh_flow: BattleRefreshFlow # 换图刷新流程件(本件持有——编排器两公开方法单行转发经此)
func _init(host: BattleOrchestrator, fx_host_ref: BattleFxHost, reward_grant_ref: RewardGrant) -> void: func _init(bundle: Dictionary) -> void:
oc = host for k in ["facts", "units", "fx_host", "db", "action_economy", "rng", "emit_row_cb"]:
# 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕——五行自宿主 _init 逐字迁入) assert(bundle.has(k), "[轮循环] 引用束缺键: %s" % k)
_grant_seg = GrantSegment.new(oc, fx_host_ref, reward_grant_ref) facts = bundle.facts
_aura_seg = AuraSegment.new(oc) units = bundle.units
_combat_seg = CombatSegment.new(oc, fx_host_ref, reward_grant_ref) fx_host = bundle.fx_host
_decay_seg = DecaySegment.new(oc) db = bundle.db
_verdict_seg = VerdictSegment.new(oc) action_economy = bundle.action_economy
rng = bundle.rng
emit_row_cb = bundle.emit_row_cb
# 结算五段接线(五段同束自取所需;信号连接纪律不涉段〔段零信号连接〕)
_grant_seg = GrantSegment.new(bundle)
_aura_seg = AuraSegment.new(bundle)
_combat_seg = CombatSegment.new(bundle)
_decay_seg = DecaySegment.new(bundle)
_verdict_seg = VerdictSegment.new(bundle)
# 换图刷新流程件(段间引用交付:复制束写入授予段引用——授予段直接交换图流程件,
# 编排器 _rounds._grant_seg 跨两层链式取段随之消灭)
var flow_bundle: Dictionary[Variant, Variant] = bundle.duplicate()
flow_bundle["grant_seg"] = _grant_seg
_refresh_flow = BattleRefreshFlow.new(flow_bundle)
# 结算重入门闸+段序体(原宿主 _maybe_settle 三闸与 _settle 合体——闸序 settling→battle_result→phase # 结算重入门闸+段序体(原宿主 _maybe_settle 三闸与 _settle 合体——闸序 settling→battle_result→phase
# 逐字;终局早退不回置 _settling,_battle_over 接管) # 逐字;终局早退不回置 _settling,_battle_over 接管。battle_result/round_index=调用参数现值交付)
func settle_if_due() -> void: func settle_if_due(battle_result: String, ri: int) -> void:
if _settling or oc.battle_result != "": if _settling or battle_result != "":
return return
if oc.action_economy.phase != oc.action_economy.PHASE_WAITING: if action_economy.phase != action_economy.PHASE_WAITING:
return return
_settling = true _settling = true
var ri := oc.round_index
# 段序编排(五段——段体归 game/core/settlement/,宿主只做行发射/开单原语单点) # 段序编排(五段——段体归 game/core/settlement/,宿主只做行发射/开单原语单点)
_grant_seg.run(ri) # ① 授予段(案C 闸随段装载) _grant_seg.run(ri) # ① 授予段(案C 闸随段装载)
_aura_seg.run(ri) # ② 增益/减益段(续存实例组合行) _aura_seg.run(ri) # ② 增益/减益段(续存实例组合行)
@@ -53,31 +77,36 @@ func is_settling() -> bool:
return _settling return _settling
# 换图刷新流程件读出口(编排器 can_refresh/player_request_refresh 两公开方法单行转发经此)
func refresh_flow() -> BattleRefreshFlow:
return _refresh_flow
# ---- 开轮摇号(共识点 B:技能选择前置到轮次开始,数值计算留结算段——原宿主函数逐字迁入)---- # ---- 开轮摇号(共识点 B:技能选择前置到轮次开始,数值计算留结算段——原宿主函数逐字迁入)----
# 每实例:保底判定(连续执行普攻 ≥ M → 必出特殊技、特殊技内按权重、不受冷却限制)→ 排除冷却中技能 # 每实例:保底判定(连续执行普攻 ≥ M → 必出特殊技、特殊技内按权重、不受冷却限制)→ 排除冷却中技能
# (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从统一流顺序取号摇定本拍动作。 # (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从统一流顺序取号摇定本拍动作。
# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。 # 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。
# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。 # 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。
# 摇定后、兑现前死亡 → 本拍技能作废。 # 摇定后、兑现前死亡 → 本拍技能作废。round_index=调用参数现值交付。
func roll_round_actions() -> void: func roll_round_actions(ri: int) -> void:
var pity_m: int = int(oc.db.combat.skill.pity_threshold) var pity_m: int = int(db.combat.skill.pity_threshold)
# facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts, # facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts,
# 容器内直读主角行) # 容器内直读主角行)
var facts := { var facts_pack := {
"round_index": oc.round_index, "round_index": ri,
"last_round_end": "none" if oc.action_economy.last_round_end().is_empty() else String(oc.action_economy.last_round_end().get("path_tag", "none")), "last_round_end": "none" if action_economy.last_round_end().is_empty() else String(action_economy.last_round_end().get("path_tag", "none")),
"mines_hit_last": int(mines_hits["last"]), "mines_hit_last": int(mines_hits["last"]),
"mines_hit_total": int(mines_hits["total"]), "mines_hit_total": int(mines_hits["total"]),
} }
for u in oc._enemy_system.query_alive("enemy"): for u in units.query_alive("enemy"):
# Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行 # Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行
# 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性 # 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性
# 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机 # 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机
# 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏) # 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏)
for sid in oc._enemy_system.check_trigger_unlocks(u, facts): for sid in units.check_trigger_unlocks(u, facts_pack):
var srow_u: Dictionary[Variant, Variant] = oc.db.combat.skill.row_by_id(sid) var srow_u: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid)
oc._emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"}, emit_row_cb.call({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"},
oc.round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))], ri, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
0, {"skill_id": sid}) 0, {"skill_id": sid})
# enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射, # enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射,
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值) # 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值)
@@ -89,18 +118,18 @@ func roll_round_actions() -> void:
break break
# target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记, # target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记,
# 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value) # 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value)
var eht_hits := oc._fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v, var eht_hits := fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v,
"is_boss": String(oc._facts.encounter.get("node_type", "")) == "boss"}) "is_boss": String(facts.encounter.get("node_type", "")) == "boss"})
for h in eht_hits: for h in eht_hits:
if String(h.get("kind", "")) == "target_attack_bonus": if String(h.get("kind", "")) == "target_attack_bonus":
u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0)) u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0))
var entries: Array[Variant] = oc._enemy_system.rollable_skills(u, facts) var entries: Array[Variant] = units.rollable_skills(u, facts_pack)
if oc._enemy_system.basic_streak(u) >= pity_m: if units.basic_streak(u) >= pity_m:
var specials: Array[Variant] = oc._enemy_system.special_skills(u, facts) var specials: Array[Variant] = units.special_skills(u, facts_pack)
if not specials.is_empty(): if not specials.is_empty():
entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合 entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合
var stream: RandomNumberGenerator = oc.rng.open_stream("behavior_roll") var stream: RandomNumberGenerator = rng.open_stream("behavior_roll")
oc._enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream)) units.set_pending_action(u, SkillLottery.draw(entries, stream))
oc._enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记) units.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记)
mines_hits["last"] = mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新) mines_hits["last"] = mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新)
mines_hits["round"] = 0 mines_hits["round"] = 0
+26 -12
View File
@@ -3,31 +3,45 @@ extends RefCounted
# ⑤ 判定段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 # ⑤ 判定段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。
# 职责:防御性优先级 hero 先判(双死结构不可达的纵深防御)→ 全量状态快照(despawn 前 # 职责:防御性优先级 hero 先判(双死结构不可达的纵深防御)→ 全量状态快照(despawn 前
# 组装——终局含 result)→ 终局行+收束分派 / 下一轮出口分发。 # 组装——终局含 result)→ 终局行+收束分派 / 下一轮出口分发。
# 耦合边界(5.1 封装修复换代):引用束注入——构造收编排器组装的 20 键封闭束,本段断言 5 键
# (units/emit_row_cb/emit_snapshot_cb/battle_over_cb/exit_distribution_cb)解包;行发射/快照/
# 终局收束经回调回宿主保留面(exit_distribution 轮推进保留现设计——七段结构冻结)——
# 对编排器零引用。
# 返回值 = 裁定("victory"/"defeat"/"continue")——宿主 _settle 据此保持旧早退语义 # 返回值 = 裁定("victory"/"defeat"/"continue")——宿主 _settle 据此保持旧早退语义
#(终局不回置 _settling,_battle_over 接管);本段零算法改动(🔴 零行为变更契约)。 #(终局不回置 _settling,_battle_over 接管);本段零算法改动(🔴 零行为变更契约)。
var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主 var units: UnitContainer # 单位容器(hero 存活/敌存活判定)
var emit_row_cb: Callable # 宿主 _emit_row——行协议面单点(5 参 head 束形)
var emit_snapshot_cb: Callable # 宿主 _emit_settlement_snapshot——结算快照发射(保留面)
var battle_over_cb: Callable # 宿主 _battle_over——终局收束(保留面)
var exit_distribution_cb: Callable # 宿主 _exit_distribution——下一轮出口分发(保留面)
func _init(host) -> void: func _init(bundle: Dictionary) -> void:
oc = host for k in ["units", "emit_row_cb", "emit_snapshot_cb", "battle_over_cb", "exit_distribution_cb"]:
assert(bundle.has(k), "[判定段] 引用束缺键: %s" % k)
units = bundle.units
emit_row_cb = bundle.emit_row_cb
emit_snapshot_cb = bundle.emit_snapshot_cb
battle_over_cb = bundle.battle_over_cb
exit_distribution_cb = bundle.exit_distribution_cb
func run(ri: int) -> String: func run(ri: int) -> String:
var verdict := "continue" var verdict := "continue"
if int(oc._enemy_system.hero_unit().get("hp", 0)) <= 0: if int(units.hero_unit().get("hp", 0)) <= 0:
verdict = "defeat" verdict = "defeat"
elif oc._enemy_system.query_alive("enemy").is_empty(): elif units.query_alive("enemy").is_empty():
verdict = "victory" verdict = "victory"
oc._emit_settlement_snapshot(verdict) # 全量状态快照(despawn 前组装——终局含 result) emit_snapshot_cb.call(verdict) # 全量状态快照(despawn 前组装——终局含 result)
if verdict == "defeat": if verdict == "defeat":
oc._emit_row({"type": "verdict", "source_class": "", "origin_cell": Vector2i(-1, -1)}, ri, "战斗失败", 0, {}) emit_row_cb.call({"type": "verdict", "source_class": "", "origin_cell": Vector2i(-1, -1)}, ri, "战斗失败", 0, {})
oc._battle_over("defeat") battle_over_cb.call("defeat")
return verdict return verdict
if verdict == "victory": if verdict == "victory":
oc._emit_row({"type": "verdict", "source_class": "", "origin_cell": Vector2i(-1, -1)}, ri, "战斗胜利", 1, {}) emit_row_cb.call({"type": "verdict", "source_class": "", "origin_cell": Vector2i(-1, -1)}, ri, "战斗胜利", 1, {})
oc._battle_over("victory") battle_over_cb.call("victory")
return verdict return verdict
oc._emit_row({"type": "verdict", "source_class": "", "origin_cell": Vector2i(-1, -1)}, ri, "下一轮次", 0, {}) emit_row_cb.call({"type": "verdict", "source_class": "", "origin_cell": Vector2i(-1, -1)}, ri, "下一轮次", 0, {})
oc._exit_distribution() exit_distribution_cb.call()
return verdict return verdict
+29 -24
View File
@@ -1,6 +1,6 @@
## 存档槽位文件域件(D3 拆件——自 save_manager 收编槽位族):槽位挂载与模板、激活与建删、 ## 存档槽位文件域件(D3 拆件——自 save_manager 收编槽位族):槽位挂载与模板、激活与建删、
## 摘要窥视(peek 族)的唯一所有者。槽位态(路径/激活号/模板)随件承载;数据重置与读写盘 ## 摘要窥视(peek 族)的唯一所有者。槽位态(路径/激活号/模板)随件承载;数据重置与读写盘
## 经注入的 save 内部协作家族(_reset_to_default/_load_parsed/_rename_overwrite)完成—— ## 经注入的 save 公开协作家族(reset_to_default/data_snapshot/restore_data/rename_overwrite 等)完成——
## 本件零 _data 副本零缓存(load 整体换新不陈旧)。 ## 本件零 _data 副本零缓存(load 整体换新不陈旧)。
class_name SlotStore class_name SlotStore
extends RefCounted extends RefCounted
@@ -23,9 +23,9 @@ func _init(save_ref, path: String = "") -> void:
## 注入槽派生名时激活号同步记账,注入任意隔离路径时激活号 0(写盘仍合法——隔离面行为零变化)。 ## 注入槽派生名时激活号同步记账,注入任意隔离路径时激活号 0(写盘仍合法——隔离面行为零变化)。
func attach_path(path: String) -> void: func attach_path(path: String) -> void:
_path = path _path = path
_save._read_only = false _save.set_read_only(false)
_active_slot = _slot_index_of(path) _active_slot = _slot_index_of(path)
_save._reset_to_default() _save.reset_to_default()
_save.load_from_disk() _save.load_from_disk()
@@ -35,8 +35,8 @@ func attach_slot_template(template: String) -> void:
_slot_template = template _slot_template = template
_path = "" _path = ""
_active_slot = 0 _active_slot = 0
_save._read_only = false _save.set_read_only(false)
_save._reset_to_default() _save.reset_to_default()
# 槽文件路径唯一派生口(模板单点——禁第二处拼名) # 槽文件路径唯一派生口(模板单点——禁第二处拼名)
@@ -54,6 +54,11 @@ func has_write_target() -> bool:
return _path != "" return _path != ""
# 写盘目标路径读投影(has_write_target 同族——save_manager 迁移口/写盘路径单点消费)
func active_path() -> String:
return _path
# 进档原语(sv5,契约 6/8):激活槽 N+读盘,复用 load_from_disk 迁移/兜底链按槽成立。 # 进档原语(sv5,契约 6/8):激活槽 N+读盘,复用 load_from_disk 迁移/兜底链按槽成立。
# 高版本拒读槽:激活不成立、恢复切换前三态(路径/激活号/禁写位/数据)——防误写他槽; # 高版本拒读槽:激活不成立、恢复切换前三态(路径/激活号/禁写位/数据)——防误写他槽;
# 损坏槽:兜底链接管(.corrupt.bak 改名+空态继续,激活成立)。 # 损坏槽:兜底链接管(.corrupt.bak 改名+空态继续,激活成立)。
@@ -62,17 +67,17 @@ func activate_slot(slot: int) -> bool:
return false return false
var prev_path := _path var prev_path := _path
var prev_slot := _active_slot var prev_slot := _active_slot
var prev_ro: bool = _save._read_only var prev_ro: bool = _save.read_only()
var prev_data = _save._data var prev_data = _save.data_snapshot()
_path = slot_path(slot) _path = slot_path(slot)
_active_slot = slot _active_slot = slot
_save._read_only = false _save.set_read_only(false)
_save._reset_to_default() _save.reset_to_default()
if not _save.load_from_disk(): if not _save.load_from_disk():
_path = prev_path _path = prev_path
_active_slot = prev_slot _active_slot = prev_slot
_save._read_only = prev_ro _save.set_read_only(prev_ro)
_save._data = prev_data _save.restore_data(prev_data)
return false return false
return true return true
@@ -84,17 +89,17 @@ func create_slot(slot: int) -> bool:
return false return false
var prev_path := _path var prev_path := _path
var prev_slot := _active_slot var prev_slot := _active_slot
var prev_ro: bool = _save._read_only var prev_ro: bool = _save.read_only()
var prev_data = _save._data var prev_data = _save.data_snapshot()
_path = slot_path(slot) _path = slot_path(slot)
_active_slot = slot _active_slot = slot
_save._read_only = false _save.set_read_only(false)
_save._reset_to_default() _save.reset_to_default()
if not _save.write_to_disk(): if not _save.write_to_disk():
_path = prev_path _path = prev_path
_active_slot = prev_slot _active_slot = prev_slot
_save._read_only = prev_ro _save.set_read_only(prev_ro)
_save._data = prev_data _save.restore_data(prev_data)
return false return false
return true return true
@@ -116,7 +121,7 @@ func delete_slot(slot: int) -> bool:
if _active_slot == slot: if _active_slot == slot:
_path = "" _path = ""
_active_slot = 0 _active_slot = 0
_save._reset_to_default() _save.reset_to_default()
return true return true
@@ -126,12 +131,12 @@ func delete_slot(slot: int) -> bool:
func peek_slot_summary(slot: int) -> Dictionary: func peek_slot_summary(slot: int) -> Dictionary:
if slot < 1 or slot > _save.SLOT_COUNT: if slot < 1 or slot > _save.SLOT_COUNT:
return {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0} # 越界=非槽位,非空槽 return {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0} # 越界=非槽位,非空槽
return _save._peek_summary_at(slot_path(slot)) return _save.peek_summary_at(slot_path(slot))
# 窥视注入口(契约 7 隔离面同款——测试/探针经路径注入隔离,禁各自拼槽文件名) # 窥视注入口(契约 7 隔离面同款——测试/探针经路径注入隔离,禁各自拼槽文件名)
func peek_summary_at(path: String) -> Dictionary: func peek_summary_at(path: String) -> Dictionary:
return _save._peek_summary_at(path) return _save.peek_summary_at(path)
# 注入路径的槽号反查(命中槽派生名返回槽号,否则 0——激活号记账用,写盘判定以 _path 非空为准) # 注入路径的槽号反查(命中槽派生名返回槽号,否则 0——激活号记账用,写盘判定以 _path 非空为准)
@@ -146,8 +151,8 @@ func _slot_index_of(path: String) -> int:
# 旧档字节原样落槽 1,读取时由正式进档路径的兜底/拒读链接管,不静默丢数据); # 旧档字节原样落槽 1,读取时由正式进档路径的兜底/拒读链接管,不静默丢数据);
# dst 已存在不重迁(幂等判据——判别锚④:去判据则重复迁移覆盖槽 1);迁移成功后 src 改名 # dst 已存在不重迁(幂等判据——判别锚④:去判据则重复迁移覆盖槽 1);迁移成功后 src 改名
# bak 留痕(不删——处置字节留痕,防再次隐式读写)。任一步失败:源保留原地,下次启动重试。 # bak 留痕(不删——处置字节留痕,防再次隐式读写)。任一步失败:源保留原地,下次启动重试。
# tmp+rename 原子形态与 write_to_disk 同纪律(改名原语 _rename_overwrite 归 save IO 域单点)。生产调用=src/dst/bak 为 旧单档/槽 1/留痕名。 # tmp+rename 原子形态与 write_to_disk 同纪律(改名原语 rename_overwrite 归 save IO 域单点)。生产调用=src/dst/bak 为 旧单档/槽 1/留痕名。
func _migrate_legacy_save(src: String, dst: String, bak: String) -> void: func migrate_legacy_save(src: String, dst: String, bak: String) -> void:
if not FileAccess.file_exists(src): if not FileAccess.file_exists(src):
return # 执行前置:旧档不存在=无操作 return # 执行前置:旧档不存在=无操作
if FileAccess.file_exists(dst): if FileAccess.file_exists(dst):
@@ -156,10 +161,10 @@ func _migrate_legacy_save(src: String, dst: String, bak: String) -> void:
if not _copy_file(src, tmp): if not _copy_file(src, tmp):
push_warning("[存档序列化] 旧单档迁移失败(源保留,下次启动重试):%s → %s" % [src, tmp]) push_warning("[存档序列化] 旧单档迁移失败(源保留,下次启动重试):%s → %s" % [src, tmp])
return return
if not _save._rename_overwrite(tmp, dst): if not _save.rename_overwrite(tmp, dst):
push_warning("[存档序列化] 旧单档迁移改名失败(tmp 保留待查,源保留重试):%s → %s" % [tmp, dst]) push_warning("[存档序列化] 旧单档迁移改名失败(tmp 保留待查,源保留重试):%s → %s" % [tmp, dst])
return return
_save._rename_overwrite(src, bak) _save.rename_overwrite(src, bak)
push_warning("[存档序列化] 旧单档已迁入 %s,原件留痕于 %s" % [dst, bak]) push_warning("[存档序列化] 旧单档已迁入 %s,原件留痕于 %s" % [dst, bak])
+6 -1
View File
@@ -113,7 +113,12 @@ func _validate_shave_params(row: Dictionary) -> Array[Dictionary]:
return errs return errs
var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移) var _resolver # R1 解析协作件(ConfigDB load_tables 装配段经 bind_resolver 注入——resolver 文件与键零搬移)
# 解析协作件挂载口(ConfigDB 装配段公开面〔5.1 封装修复——直写 _resolver 改经此口〕)
func bind_resolver(resolver: ConfigDbResolver) -> void:
_resolver = resolver
## 进度行解析委托(消费面 db.combat.board_gen.resolve_row——R1 ceil/clamp 语义归 ConfigDbResolver) ## 进度行解析委托(消费面 db.combat.board_gen.resolve_row——R1 ceil/clamp 语义归 ConfigDbResolver)
+6 -1
View File
@@ -30,7 +30,12 @@ func validate() -> Array:
return errs return errs
var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移) var _resolver # R1 解析协作件(ConfigDB load_tables 装配段经 bind_resolver 注入——resolver 文件与键零搬移)
# 解析协作件挂载口(ConfigDB 装配段公开面〔5.1 封装修复——直写 _resolver 改经此口〕)
func bind_resolver(resolver: ConfigDbResolver) -> void:
_resolver = resolver
## 难度修饰解析委托(消费面 db.run.difficulty.resolve_modifiers——clamp 语义归 ConfigDbResolver) ## 难度修饰解析委托(消费面 db.run.difficulty.resolve_modifiers——clamp 语义归 ConfigDbResolver)
+6 -1
View File
@@ -209,7 +209,12 @@ static func band_of(band_keys: Array, D: int) -> int:
# 禁双算法并存〔状态记录 312〕;L 维 ceil/clamp 与越顶 clamp 语义归 resolver)。 # 禁双算法并存〔状态记录 312〕;L 维 ceil/clamp 与越顶 clamp 语义归 resolver)。
var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移) var _resolver # R1 解析协作件(ConfigDB load_tables 装配段经 bind_resolver 注入——resolver 文件与键零搬移)
# 解析协作件挂载口(ConfigDB 装配段公开面〔5.1 封装修复——直写 _resolver 改经此口〕)
func bind_resolver(resolver: ConfigDbResolver) -> void:
_resolver = resolver
## 带寻址行集解析委托(消费面 db.combat.enemy.resolve_rowset——D 维 band_of 落带语义归 ConfigDbResolver) ## 带寻址行集解析委托(消费面 db.combat.enemy.resolve_rowset——D 维 band_of 落带语义归 ConfigDbResolver)
+31 -12
View File
@@ -82,11 +82,10 @@ rows = [{
"level_index": 1, "level_index": 1,
"row_kind": "battle", "row_kind": "battle",
"depth_band": 3, "depth_band": 3,
"enemy_id": "hobgoblin", "enemy_id": "bugbear",
"display_name": "大地精", "display_name": "熊地精",
"unit_count": 2, "unit_count": 1,
"unit_count_max": 3, "hp": 22,
"hp": 14,
"atk": 1, "atk": 1,
"atk_min": 0, "atk_min": 0,
"atk_max": 2, "atk_max": 2,
@@ -106,6 +105,31 @@ rows = [{
}, { }, {
"level_index": 1, "level_index": 1,
"row_kind": "battle", "row_kind": "battle",
"depth_band": 3,
"enemy_id": "goblin",
"display_name": "地精",
"unit_count": 1,
"unit_count_max": 2,
"hp": 8,
"atk": 1,
"atk_min": 0,
"atk_max": 2,
"pool": [{
"skill_id": "attack",
"weight": 9
}, {
"skill_id": "lunge",
"weight": 1
}, {
"skill_id": "desperate_guard",
"weight": 1
}],
"attack_type": "normal",
"armor_type": "light",
"armor_class": 0
}, {
"level_index": 1,
"row_kind": "battle",
"depth_band": 6, "depth_band": 6,
"enemy_id": "bugbear", "enemy_id": "bugbear",
"display_name": "熊地精", "display_name": "熊地精",
@@ -883,12 +907,6 @@ rows = [{
; (设计文档 §6,快照后恢复基线)。验证出带微调旋钮序:L2 d1/d6/d7 血 22→20 → d4 24→22 → d9 112→108。 ; (设计文档 §6,快照后恢复基线)。验证出带微调旋钮序:L2 d1/d6/d7 血 22→20 → d4 24→22 → d9 112→108。
; provisional 未提交。 ; provisional 未提交。
; ;
; 实体名修正批(2026-09-24——用户裁决,DND 阶梯对齐):(1,3) 熊地精双雄 → 大地精
; (enemy_id bugbear→hobgoblin,仅本行;L2 bugbear 三行熊地精系不动)。依据:①DND 阶梯
; 地精(goblin)→大地精(hobgoblin)→熊地精(bugbear) 与实测每只血量 4-8→14→50 严格同构——
; 单只熊地精 50 血在前、三只 14 血在后,同族每只血量倒挂的观感由阶名归位化解;
; ②「双雄」名硬编码数量、数量却是 [2,3] 变量骰——摇出 3 只即成「三个双雄」自相矛盾
; (用户截图实证),集体名(猎群/巢群/小队/大地精)为正解。hp/atk/池/数量区间零改动。
; M-3 迭代 5 判读锁值批(2026-09-28):(2,9) 卓尔 hp 130→100(杠杆扫描 C3 组合——Boss 杠杆对普通档 +4pp; ; M-3 迭代 5 判读锁值批(2026-09-28):(2,9) 卓尔 hp 130→100(杠杆扫描 C3 组合——Boss 杠杆对普通档 +4pp;
; r_boss 锚漂移加重 100/389≈0.26 低于域 [0.5,1.1] 下限,收锚裁决挂迭代 5 出口人闸;读数见 ; r_boss 锚漂移加重 100/389≈0.26 低于域 [0.5,1.1] 下限,收锚裁决挂迭代 5 出口人闸;读数见
; design/qa/m3-iter5-lever-scan-2026-09-28.md)。 ; design/qa/m3-iter5-lever-scan-2026-09-28.md)。
@@ -916,6 +934,7 @@ rows = [{
; 读数预期:(2,5) 场敌人 3-4 只(3=重击手 1+蜘蛛 2,4=蜘蛛掷 3);L2 难度带位移读数随 Alpha 收官 M4 数值批 ; 读数预期:(2,5) 场敌人 3-4 只(3=重击手 1+蜘蛛 2,4=蜘蛛掷 3);L2 难度带位移读数随 Alpha 收官 M4 数值批
; 统一执行(07 在册义务——MC 取证跑批届时另报批,本批零跑)。禁区排查:巨蜘蛛首遇位 (2,4)「固定 2 不加骰」 ; 统一执行(07 在册义务——MC 取证跑批届时另报批,本批零跑)。禁区排查:巨蜘蛛首遇位 (2,4)「固定 2 不加骰」
; 零触碰、Boss d9 零触碰、跨关混编禁令过(两敌种首遇关均 ≤L2)、既有 19 行现值零变更。 ; 零触碰、Boss d9 零触碰、跨关混编禁令过(两敌种首遇关均 ≤L2)、既有 19 行现值零变更。
; 2026-11-04 追加第二行:(1,3) 地精+熊地精(行序熊地精在前)
; 敌人表内容修批 N1(2026-10-13 用户令「按推荐方式」,迭代 24 卡①):(1,2) 熊地精降档 hp 50>>22、armor_class 3>>2—— ; 敌人表内容修批 N1(2026-10-13 用户令「按推荐方式」,迭代 24 卡①):(1,2) 熊地精降档 hp 50>>22、armor_class 3>>2——
; 2026-09-24 阶名化解方案((1,3) 改名大地精以阶名归位化解同族血量倒挂观感)实机验证未达预期,按体验反馈重裁: ; 2026-09-24 阶名化解方案((1,3) 改名大地精以阶名归位化解同族血量倒挂观感)实机验证未达预期,按体验反馈重裁:
; 第一关总量曲线 12/10/50/35/72 的 5 倍墙消除(22 后曲线 12/10/22/35/72 相邻比最大 2.2);敌种 bugbear/池/数量/atk 区间零改动。 ; 第一关总量曲线 12/10/50/35/72 的 5 倍墙消除(22 后曲线 12/10/22/35/72 相邻比最大 2.2);敌种 bugbear/池/数量/atk 区间零改动。
@@ -926,4 +945,4 @@ rows = [{
; ;
; 行键 = (level_index, row_kind, depth_band, enemy_id);row_kind ∈ {battle, boss}——battle=战斗节点配池,boss=终点行集; ; 行键 = (level_index, row_kind, depth_band, enemy_id);row_kind ∈ {battle, boss}——battle=战斗节点配池,boss=终点行集;
; depth_band=节点深度所在的带键,请求落带取首个 ≥ 深度的键(偏难一侧);越顶带由 resolver clamp、跨表校验判为调参事故。 ; depth_band=节点深度所在的带键,请求落带取首个 ≥ 深度的键(偏难一侧);越顶带由 resolver clamp、跨表校验判为调参事故。
; hobgoblin 行为缺口行:无立绘、不在敌种表——留任在役,补齐三件套(立绘+语义模板+敌种表行)归敌种内容批。 ; (1,3) 大地精行 2026-11-04 用户裁退役,改地精+熊地精混编(行序熊地精在前、行值整行复用 (1,2)/(1,0)、静态骰和 3);沿革见 git 提交史
+5 -2
View File
@@ -208,9 +208,10 @@ func _suites() -> Array:
{"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0}, {"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0},
{"path": "res://tests/unit/battle_settlement/effect_port_test.gd", "expect": 0}, # 效果端口件(效果端口与掷格分件批 V1–V3——端口单实例/业务类零引擎直连源码锚/事实面闭集两对照) {"path": "res://tests/unit/battle_settlement/effect_port_test.gd", "expect": 0}, # 效果端口件(效果端口与掷格分件批 V1–V3——端口单实例/业务类零引擎直连源码锚/事实面闭集两对照)
{"path": "res://tests/unit/battle_settlement/roll_multiplier_test.gd", "expect": 0}, # 掷点与乘区宿主(敌攻击类型轴/护甲掷点行为面/乘区接线/伤害类型克制与标错/fresh 池攻击链——批 B 自 smoke_all 迁入) {"path": "res://tests/unit/battle_settlement/roll_multiplier_test.gd", "expect": 0}, # 掷点与乘区宿主(敌攻击类型轴/护甲掷点行为面/乘区接线/伤害类型克制与标错/fresh 池攻击链——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/battle_settlement/settlement_wiring_snapshot_test.gd", "expect": 0}, # 结算接线同种子逐项 golden 对拍主证〔封装5.1〕(架构审查 5.1 封装修复——段类束注入改造零行为变更基线)
{"path": "res://tests/unit/battle_settlement/seal_polarity_test.gd", "expect": 0}, # 封印与极性宿主(末点 AP 封印/封印不可逆四情况/清图奖励双份与弱形式/刷新 provenance 守卫/极性路由与封印溢价/负向字典/格面资源资格——批 B 自 smoke_all 迁入) {"path": "res://tests/unit/battle_settlement/seal_polarity_test.gd", "expect": 0}, # 封印与极性宿主(末点 AP 封印/封印不可逆四情况/清图奖励双份与弱形式/刷新 provenance 守卫/极性路由与封印溢价/负向字典/格面资源资格——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/battle_settlement/round_flow_skill_pool_test.gd", "expect": 0}, # 轮内流与技能池宿主(全战斗回放/轮内刷新与 V5 快照单源/遭遇区间骰/结算五段全序/技能池数据面·回放独立·EV 锚·死亡打断·校验拒绝/妨害附加/四桶计数——批 B 自 smoke_all 迁入) {"path": "res://tests/unit/battle_settlement/round_flow_skill_pool_test.gd", "expect": 0}, # 轮内流与技能池宿主(全战斗回放/轮内刷新与 V5 快照单源/遭遇区间骰/结算五段全序/技能池数据面·回放独立·EV 锚·死亡打断·校验拒绝/妨害附加/四桶计数——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/battle_settlement/refresh_feats_test.gd", "expect": 0}, # 换图系专长七件宿主(迭代 46 卡A——基座产 RAP 与折价同源/成功事实恰一次/全局受伤四路径恰一次/石层缓冲/崩壁延迟/蓄力层/坑道小憩/矿道红利 GP/七件数据与牌库/无专长基线;原经卡片申报走 02 关联档选跑,卡片退役后补登记由 02 全套件承接) {"path": "res://tests/unit/battle_settlement/refresh_feats_test.gd", "expect": 0}, # 换图系专长七件宿主(迭代 46 卡A——基座产 RAP 与折价同源/成功事实恰一次/全局受伤四路径恰一次/石层缓冲/崩壁延迟/蓄力周转/坑道小憩/矿道红利 GP/七件数据与牌库/无专长基线;原经卡片申报走 02 关联档选跑,卡片退役后补登记由 02 全套件承接)
{"path": "res://tests/unit/battle_settlement/variant_placement_test.gd", "expect": 0}, # 变异布置宿主(战内布置段门控与数量公式/全局变异兜底回落与资源面偏斜——批 B 自 smoke_all 迁入) {"path": "res://tests/unit/battle_settlement/variant_placement_test.gd", "expect": 0}, # 变异布置宿主(战内布置段门控与数量公式/全局变异兜底回落与资源面偏斜——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/battle_settlement/skill_unlock_test.gd", "expect": 0}, # Boss 半血解锁技·容器机制(门谓词/闩不可逆/双面过滤/保底不逼锁定/零扰动) {"path": "res://tests/unit/battle_settlement/skill_unlock_test.gd", "expect": 0}, # Boss 半血解锁技·容器机制(门谓词/闩不可逆/双面过滤/保底不逼锁定/零扰动)
{"path": "res://tests/unit/battle_settlement/extended_row_violation_test.gd", "expect": 1, "negative": true}, # 卡④ wr2-np 扩展行必载 effect_id 负路径(行协议运行期违约面首个负面套件——独立进程豁免错误扫描) {"path": "res://tests/unit/battle_settlement/extended_row_violation_test.gd", "expect": 1, "negative": true}, # 卡④ wr2-np 扩展行必载 effect_id 负路径(行协议运行期违约面首个负面套件——独立进程豁免错误扫描)
@@ -225,7 +226,9 @@ func _suites() -> Array:
{"path": "res://tests/unit/core/tree_requires_level_test.gd", "expect": 0}, # R1 requires_level 等级阈值前置(schema 四拒一过+判定三态+投影两态) {"path": "res://tests/unit/core/tree_requires_level_test.gd", "expect": 0}, # R1 requires_level 等级阈值前置(schema 四拒一过+判定三态+投影两态)
{"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役) {"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役)
{"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴) {"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴)
{"path": "res://tests/unit/core/settle_segments_test.gd", "expect": 0}, # 结算五段 Segment(V4:直接 new/编排器接线/真实一轮走段序) {"path": "res://tests/unit/core/settle_segments_test.gd", "expect": 0}, # 结算五段 Segment(V4:束构造/编排器接线/真实一轮走段序)
{"path": "res://tests/unit/core/encapsulation_sweep_test.gd", "expect": 0}, # 五处结构性零命中扫描〔封装5.1 V2–V7〕(架构审查 5.1 封装修复——七段对编排器零引用/五方法零残留/断点存档公开面/效果累积面/配置挂载面,非空前置+正锚)
{"path": "res://tests/unit/core/encapsulation_p2_sweep_test.gd", "expect": 0}, # NODE_TYPES 词表单点化结构性扫描〔架构P2 V1/V2〕(架构审查 P2 代码面——生成器本地副本归零+schema 唯一定点,非空前置+正锚)
{"path": "res://tests/unit/core/save_slot_isolation_test.gd", "expect": 0}, # 卡① 三槽位间隔离/进档失败恢复/宿主链引用同一(sv1/sv5/sv9/sv10) {"path": "res://tests/unit/core/save_slot_isolation_test.gd", "expect": 0}, # 卡① 三槽位间隔离/进档失败恢复/宿主链引用同一(sv1/sv5/sv9/sv10)
{"path": "res://tests/unit/core/save_boot_no_activate_test.gd", "expect": 0}, # 卡① 启动态类级契约:无激活态/拒写盘面零触碰/越界拒绝(sv2/sv3) {"path": "res://tests/unit/core/save_boot_no_activate_test.gd", "expect": 0}, # 卡① 启动态类级契约:无激活态/拒写盘面零触碰/越界拒绝(sv2/sv3)
{"path": "res://tests/unit/core/save_slot_summary_test.gd", "expect": 0}, # 卡① 摘要三项按槽窥视:异值/空槽/坏槽三态/零副作用(sv7/sv8) {"path": "res://tests/unit/core/save_slot_summary_test.gd", "expect": 0}, # 卡① 摘要三项按槽窥视:异值/空槽/坏槽三态/零副作用(sv7/sv8)
+1 -2
View File
@@ -141,7 +141,7 @@ fi
# 豁免单新增三处(2026-10-26 令牌配额口径清扫批 B4b 用户裁定保留):adventure_table.gd:26 档名映射对照注 # 豁免单新增三处(2026-10-26 令牌配额口径清扫批 B4b 用户裁定保留):adventure_table.gd:26 档名映射对照注
# +adventure_table.gd:93 与 curio_deck.gd:130 四口径契约④原名引述——合法载体非残留,改写即断链 # +adventure_table.gd:93 与 curio_deck.gd:130 四口径契约④原名引述——合法载体非残留,改写即断链
# 豁免单再增 13 处(2026-10-26 口径换代批用户裁「带日期历史读数注释保留原文」): # 豁免单再增 13 处(2026-10-26 口径换代批用户裁「带日期历史读数注释保留原文」):
# hero.tres:21/:36、enemy.tres:751/:891、action_economy.tres:15/:16/:26/:27、status_effects.tres:91、 # hero.tres:21/:36、enemy.tres:751、action_economy.tres:15/:16/:26/:27、status_effects.tres:91、
# meta.tres:48/:56/:62(.tres 十二处历史读数/带日期批记录)+level_autoplay.gd:560(完美档 41% 历史实证)—— # meta.tres:48/:56/:62(.tres 十二处历史读数/带日期批记录)+level_autoplay.gd:560(完美档 41% 历史实证)——
# 历史读数原文锚定当时实测口径,改写即失真;现行口径描述不豁免(已随批换代) # 历史读数原文锚定当时实测口径,改写即失真;现行口径描述不豁免(已随批换代)
@@ -219,7 +219,6 @@ else
if(FILENAME~"hero.tres"&&FNR==21)next if(FILENAME~"hero.tres"&&FNR==21)next
if(FILENAME~"hero.tres"&&FNR==36)next if(FILENAME~"hero.tres"&&FNR==36)next
if(FILENAME~"enemy.tres"&&FNR==751)next if(FILENAME~"enemy.tres"&&FNR==751)next
if(FILENAME~"enemy.tres"&&FNR==891)next
if(FILENAME~"action_economy.tres"&&FNR==15)next if(FILENAME~"action_economy.tres"&&FNR==15)next
if(FILENAME~"action_economy.tres"&&FNR==16)next if(FILENAME~"action_economy.tres"&&FNR==16)next
if(FILENAME~"action_economy.tres"&&FNR==26)next if(FILENAME~"action_economy.tres"&&FNR==26)next
@@ -13,6 +13,7 @@ const RngServiceScript := preload("res://core/rng_service.gd")
const UnitContainerScript := preload("res://core/unit_container.gd") const UnitContainerScript := preload("res://core/unit_container.gd")
const StatusContainerScript := preload("res://core/status_container.gd") const StatusContainerScript := preload("res://core/status_container.gd")
const DamageControllerScript := preload("res://core/damage_controller.gd") const DamageControllerScript := preload("res://core/damage_controller.gd")
const BattleFactsScript := preload("res://core/battle_facts.gd")
const TEST_SEED := 20260928 # 夹具种子(基建值非玩法数值——回放确定性契约的输入) const TEST_SEED := 20260928 # 夹具种子(基建值非玩法数值——回放确定性契约的输入)
@@ -52,7 +53,7 @@ func _harness(db, depth: int = 0):
rng.set_master_seed(TEST_SEED) rng.set_master_seed(TEST_SEED)
var units = UnitContainerScript.new(db) var units = UnitContainerScript.new(db)
var effects = StatusContainerScript.new(db) var effects = StatusContainerScript.new(db)
var ctrl = DamageControllerScript.new(db, rng, units, effects) var ctrl = DamageControllerScript.new(db, rng, units, effects, BattleFactsScript.new()) # facts 构造注入(5.1 封装修复——hero_atk 派生读消费面;夹具空 encounter=表值缺省)
ctrl.emit_row_cb = Callable(self, "_on_row") ctrl.emit_row_cb = Callable(self, "_on_row")
effects.notify_battle_start() effects.notify_battle_start()
var spawn_ok: bool = units.spawn({"level_index": 1, "node_depth": depth, "hero_hp": int(db.combat.hero.max_hp)}) var spawn_ok: bool = units.spawn({"level_index": 1, "node_depth": depth, "hero_hp": int(db.combat.hero.max_hp)})
@@ -410,7 +411,7 @@ func _test_pool_clamp(db) -> void:
# ---- 个体池读数 / 个体 AC 合流(增益状态 v1.5——敌方状态个体化批)---- # ---- 个体池读数 / 个体 AC 合流(增益状态 v1.5——敌方状态个体化批)----
func _test_pool_consumption_per_unit(db) -> void: func _test_pool_consumption_per_unit(db) -> void:
var h = _harness(db, 3) # depth 3 = hobgoblin 行集(unit_count 2-3——个体对照面需 ≥2 敌) var h = _harness(db, 3) # depth 3 = 混编行集(熊地精 1 + 地精 1–2,最少 2 只——个体对照面需 ≥2 敌)
if not h.spawn_ok: if not h.spawn_ok:
return return
var enemies: Array = h.units.all_enemies() var enemies: Array = h.units.all_enemies()
@@ -13,7 +13,7 @@ const ConfigDBScript := preload("res://core/config_db.gd")
const EffectEngineScript := preload("res://core/effect_engine.gd") const EffectEngineScript := preload("res://core/effect_engine.gd")
const TEST_SEED := 42 # 夹具种子(基建值非玩法数值——同种子两跑对比的隔离前提) const TEST_SEED := 42 # 夹具种子(基建值非玩法数值——同种子两跑对比的隔离前提)
const LAYER_COUNTER := "swap_charge_layers" # 整备军备蓄力层计数器(feats.tres charge_rotation 行) const LAYER_COUNTER := "swap_charge_layers" # 蓄力周转计数器(feats.tres swap_charge_layers 行)
var _db var _db
@@ -54,7 +54,7 @@ func _hero_attack_value(rows: Array) -> int:
func _smoke_effect_port_wiring() -> void: func _smoke_effect_port_wiring() -> void:
print("--- 效果端口:装配链单实例+真实发射路由 ---") print("--- 效果端口:装配链单实例+真实发射路由 ---")
# 充能跑(整备军备在册——attack_commit 订阅行 attack_bonus per 蓄力层计数器) # 充能跑(蓄力周转 charge_rotation 在册——attack_commit 订阅行 attack_bonus per 蓄力周转计数器)
var orch_c = _orch(TEST_SEED) var orch_c = _orch(TEST_SEED)
var rows_c: Array = [] var rows_c: Array = []
orch_c.step_row.connect(func(row): rows_c.append(row)) orch_c.step_row.connect(func(row): rows_c.append(row))
@@ -64,7 +64,7 @@ func _smoke_effect_port_wiring() -> void:
var port_a = orch_c.action_economy._port var port_a = orch_c.action_economy._port
Kit.check("V1 端口在座(damage/action_economy 双绑)", port_d != null and port_a != null) Kit.check("V1 端口在座(damage/action_economy 双绑)", port_d != null and port_a != null)
Kit.check("V1 端口单实例(两业务类持同一 EffectPort)", port_d == port_a) Kit.check("V1 端口单实例(两业务类持同一 EffectPort)", port_d == port_a)
# 蓄力层预置 3(调试注入两层·数据面粒度——计数器账面直写,与 refresh_feats_test V8 同法) # 蓄力周转预置 3(调试注入两层·数据面粒度——计数器账面直写,与 refresh_feats_test V8 同法)
EffectEngineScript.counter_add(orch_c.fx_host().counters_ctx, LAYER_COUNTER, 3) EffectEngineScript.counter_add(orch_c.fx_host().counters_ctx, LAYER_COUNTER, 3)
# 最小回合:停手即轮末结算(主角先手攻击在战斗段同步兑付——零 await 同步链) # 最小回合:停手即轮末结算(主角先手攻击在战斗段同步兑付——零 await 同步链)
orch_c.minesweeper.settle_batch([{"action": "stop"}]) orch_c.minesweeper.settle_batch([{"action": "stop"}])
@@ -80,7 +80,7 @@ func _smoke_effect_port_wiring() -> void:
orch_0.minesweeper.settle_batch([{"action": "stop"}]) orch_0.minesweeper.settle_batch([{"action": "stop"}])
var v_0 := _hero_attack_value(rows_0) var v_0 := _hero_attack_value(rows_0)
Kit.check("V1 对照跑攻击行在流", v_0 >= 0) Kit.check("V1 对照跑攻击行在流", v_0 >= 0)
Kit.check("V1 delta 读数非零(蓄力层加算进入主角攻击基值)", v_c - v_0 >= 1) Kit.check("V1 delta 读数非零(蓄力周转加算进入主角攻击基值)", v_c - v_0 >= 1)
# ---- V2 业务类零引擎直连(源码契约锚——theme_projection_test 源码锚惯例)---- # ---- V2 业务类零引擎直连(源码契约锚——theme_projection_test 源码锚惯例)----
@@ -100,7 +100,7 @@ func _test_biz_fact_face_closed() -> void:
print("--- 效果端口:业务发射事实面闭集 ---") print("--- 效果端口:业务发射事实面闭集 ---")
var orch = _orch(TEST_SEED) var orch = _orch(TEST_SEED)
Kit.check("V3 begin_battle 受理", orch.begin_battle(_enc([]))) Kit.check("V3 begin_battle 受理", orch.begin_battle(_enc([])))
# 合成行直改数据面(调试注入两层·数据面粒度):effect 段逐字抄现役铁壁 shield 表行(kind/value 零推表); # 合成行直改数据面(调试注入两层·数据面粒度):effect 段逐字抄现役 shield 表行(kind/value 零推表);
# rebuild 走持有 id 过滤真实表行——合成行经直改表行进入装载域(订阅面换代为合成行独占,确定性) # rebuild 走持有 id 过滤真实表行——合成行经直改表行进入装载域(订阅面换代为合成行独占,确定性)
var pos_id := "probe_shield_pos" var pos_id := "probe_shield_pos"
var neg_id := "probe_shield_neg" var neg_id := "probe_shield_neg"
@@ -127,3 +127,5 @@ func _test_biz_fact_face_closed() -> void:
ctrl.flush_submit(t_b) ctrl.flush_submit(t_b)
var hp2 := int(ctrl.units.hero_unit().get("hp", 0)) var hp2 := int(ctrl.units.hero_unit().get("hp", 0))
Kit.check("V3 阴性对照:battle_index 不答——零减量(10→10,事实面未扩)", hp1 - hp2 == 10) Kit.check("V3 阴性对照:battle_index 不答——零减量(10→10,事实面未扩)", hp1 - hp2 == 10)
# 闭面收窄 V4(键退役验收——经 begin_battle 真实装配链取宿主应答;键在→返 0,键退役→null)
Kit.check("V4 闭面收窄:charge_layers 键退役——宿主事实应答 null", orch.fx_host().biz_fact("charge_layers") == null)
@@ -309,12 +309,15 @@ func _smoke_encounter_roll() -> void:
scan_bad += 1 scan_bad += 1
continue continue
var n: int = settlement_scan.presentation_snapshot().enemies.size() var n: int = settlement_scan.presentation_snapshot().enemies.size()
var erow2: Dictionary = _db.combat.enemy.resolve_rowset(1, int(depth_v), "battle")[0] # 行集首行(扫描面 = 战斗节点) var erowset2: Array[Dictionary] = _db.combat.enemy.resolve_rowset(1, int(depth_v), "battle") # 扫描面 = 战斗节点
var lo2: int = int(erow2.get("unit_count", 1)) var lo2 := 0
var hi2: int = int(erow2.get("unit_count_max", lo2)) var hi2 := 0
for er in erowset2: # 逐行求和(同带多行各掷各的——与 unit_container spawn 守卫同口径,非首行单行口径)
lo2 += int(er.get("unit_count", 1))
hi2 += int(er.get("unit_count_max", int(er.get("unit_count", 1))))
if n < 1 or n < lo2 or n > hi2: if n < 1 or n < lo2 or n > hi2:
scan_bad += 1 scan_bad += 1
Kit.check("全深度 × 12 种子 spawn 全域内(%d 场,∈[unit_count, unit_count_max] 且 ≥1)——空遭遇不可达(Bug#1 回归)" % scan_total, scan_bad == 0) Kit.check("全深度 × 12 种子 spawn 全域内(%d 场,∈[Σunit_count, Σunit_count_max] 逐行求和且 ≥1)——空遭遇不可达(Bug#1 回归;混编行集口径)" % scan_total, scan_bad == 0)
# 节点类型 → 敌人表 row_kind(非 Boss 战斗位恒 battle——深度带批寻址第三维) # 节点类型 → 敌人表 row_kind(非 Boss 战斗位恒 battle——深度带批寻址第三维)
@@ -569,7 +572,8 @@ func _smoke_skill_pool_data() -> void:
"1_battle_0_goblin": {"attack": 9, "lunge": 1, "desperate_guard": 1}, "1_battle_0_goblin": {"attack": 9, "lunge": 1, "desperate_guard": 1},
"1_battle_1_goblin": {"attack": 9, "lunge": 1, "desperate_guard": 1}, "1_battle_1_goblin": {"attack": 9, "lunge": 1, "desperate_guard": 1},
"1_battle_2_bugbear": {"attack": 6, "charge": 2, "heavy": 2}, "1_battle_2_bugbear": {"attack": 6, "charge": 2, "heavy": 2},
"1_battle_3_hobgoblin": {"attack": 6, "charge": 2, "heavy": 2}, "1_battle_3_bugbear": {"attack": 6, "charge": 2, "heavy": 2},
"1_battle_3_goblin": {"attack": 9, "lunge": 1, "desperate_guard": 1},
"1_boss_4_ogre_chief": {"attack": 6, "heavy": 2, "harden": 2, "quake_smash": 2}, "1_boss_4_ogre_chief": {"attack": 6, "heavy": 2, "harden": 2, "quake_smash": 2},
"2_battle_0_bugbear": {"attack": 6, "lunge": 1, "heavy": 2}, "2_battle_0_bugbear": {"attack": 6, "lunge": 1, "heavy": 2},
"2_battle_1_bugbear": {"attack": 6, "lunge": 1, "heavy": 2}, "2_battle_1_bugbear": {"attack": 6, "lunge": 1, "heavy": 2},
@@ -608,7 +612,7 @@ func _smoke_skill_pool_data() -> void:
got[String(entry.skill_id)] = int(entry.weight) got[String(entry.skill_id)] = int(entry.weight)
if got != expect_pools[rk]: if got != expect_pools[rk]:
pools_ok = false pools_ok = false
Kit.check("二十八行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok) Kit.check("二十九行十种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok)
var basic_count_ok := true var basic_count_ok := true
for row in _db.combat.enemy.rows: for row in _db.combat.enemy.rows:
var n := 0 var n := 0
@@ -688,7 +692,7 @@ func _smoke_skill_pool_data() -> void:
and String(_db.combat.settlement.core.effect_pool_map.get("fortify", "")) == "enemy_defense") and String(_db.combat.settlement.core.effect_pool_map.get("fortify", "")) == "enemy_defense")
# ② 同种子回放逐位复现(D3 大地精,多实例同种) # ② 同种子回放逐位复现(depth 3 = 混编行集〔熊地精 1 + 地精 1–2〕)
# 单敌轨迹不独立复放——轨迹取数与其它流交错,无法脱离全序重建; # 单敌轨迹不独立复放——轨迹取数与其它流交错,无法脱离全序重建;
# 同种子全战斗逐位复现由 ② 全对拍承载,单点定位归事件日志。 # 同种子全战斗逐位复现由 ② 全对拍承载,单点定位归事件日志。
@@ -708,7 +712,7 @@ func _fresh_rng(seed_val: int):
# ---- ① GP 冒险钱包·到账 ---- # ---- ① GP 冒险钱包·到账 ----
# ② 同种子回放逐位复现(D3 大地精,多实例同种) # ② 同种子回放逐位复现(depth 3 = 混编行集〔熊地精 1 + 地精 1–2〕)
# 单敌轨迹不独立复放——轨迹取数与其它流交错,无法脱离全序重建; # 单敌轨迹不独立复放——轨迹取数与其它流交错,无法脱离全序重建;
# 同种子全战斗逐位复现由 ② 全对拍承载,单点定位归事件日志。 # 同种子全战斗逐位复现由 ② 全对拍承载,单点定位归事件日志。
func _smoke_skill_pool_replay_independence() -> void: func _smoke_skill_pool_replay_independence() -> void:
@@ -718,8 +722,8 @@ func _smoke_skill_pool_replay_independence() -> void:
if not seqs.is_empty() and int(seqs.count) >= 2: if not seqs.is_empty() and int(seqs.count) >= 2:
seed_val = s seed_val = s
break break
Kit.check("多实例种子可达(seed=%d——D3 遭遇 ≥2 实例)" % seed_val, seed_val > 0) Kit.check("多实例种子可达(seed=%d——depth 3 混编遭遇 ≥2 实例)" % seed_val, seed_val > 0)
# 敌 hp 低(表值 6)时 hero_atk=1 十轮内必然击杀(实例错位、轨迹断裂)—— # 敌 hp 低(混编带地精 hp 8)时 hero_atk=1 十轮内必然击杀(实例错位、轨迹断裂)——
# 观测窗取 5 轮(5 拍 ×1 伤 < 6 HP 全员存活;仍覆盖保底 M=4 的触发窗)。 # 观测窗取 5 轮(5 拍 ×1 伤 < 6 HP 全员存活;仍覆盖保底 M=4 的触发窗)。
var rounds := 5 var rounds := 5
var run1 := _record_pool_trajectories(seed_val, rounds) var run1 := _record_pool_trajectories(seed_val, rounds)
@@ -1105,7 +1109,7 @@ func _smoke_enemy_buff_targets_self() -> void:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg)
var rows: Array = [] var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row)) settlement.step_row.connect(func(row): rows.append(row))
# L1 depth 3(hobgoblin 行集 2-3 只——depth 0 单 goblin 常被主角先手一击秒杀,死亡即不动手、cast 不发生) # L1 depth 3(混编行集 熊地精 1 + 地精 1–2——depth 0 单 goblin 常被主角先手一击秒杀,死亡即不动手、cast 不发生)
if not settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(_db.combat.hero.max_hp)}): if not settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(_db.combat.hero.max_hp)}):
continue continue
for turn in range(4): for turn in range(4):
@@ -0,0 +1,501 @@
# tests/unit/battle_settlement/settlement_wiring_snapshot_test.gd — 结算接线同种子逐项 golden 对拍宿主(架构审查 5.1 封装修复批 R1 主证)
# 断言面 = 同种子定死驱动序下 step_row 全量行流 + settlement_snapshot 全量载荷 + battle_finished
# 载荷 + 收束 presentation_snapshot 关键键(round/battle_result/hero.hp/enemies.size()),
# 与 GOLDEN 常量逐位相等〔封装5.1 V1〕——段类束注入改造的零行为变更主证:
# 改前基线在未改树上固化(GOLDEN 为空时走转储通道取数),改后全程保持绿。
# 驱动序(卡面逐字定死):begin_battle(voucher_rap=20) → 轮1 choose_dig 移动/挖/封邻格/停 →
# player_request_refresh(换图刷新链入流)→ 轮2 一挖一停 → 轮3 径直停手。
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB 显式实例化构造注入+自建 RngService(掷点隔离形态)。
# 取数通道:GOLDEN 为空时打印捕获序列 JSON(两格缩进)后按专用码 3 退出——非判定码族(0 绿/1 红/2 空跑)。
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/settlement_wiring_snapshot_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const ConfigDBScript := preload("res://core/config_db.gd")
const AutoPlayer := preload("res://sim/auto_player.gd")
const TEST_SEED := 4242 # golden 固化种子(卡面定死——改前改后同种子对拍前提)
const DUMP_EXIT := 3 # 空跑转储专用退出码(GOLDEN 未固化态取数用)
# 改前基线(JSON 两格缩进单文件固化——与捕获序列 JSON.stringify 逐字相等即绿;空串=未固化→转储模式)
const GOLDEN := r"""
{
"final": {
"battle_result": "",
"enemies_n": 2,
"hero_hp": 21,
"round": 4
},
"finished": [],
"rows": [
{
"display_name": "迟滞(下轮行动点 -1)",
"effect_id": "ap_delta",
"origin_cell": [
1,
0
],
"pool_value": -1,
"round_index": 1,
"source_class": "mine_hit",
"type": "effect",
"value": -1
},
{
"display_name": "行动点(下轮行动点 +1)",
"effect_id": "ap_delta",
"highlight": "seal_hit",
"origin_cell": [
1,
1
],
"pool_value": 0,
"round_index": 1,
"source_class": "seal_hit",
"type": "effect",
"value": 1
},
{
"ac": 1,
"display_name": "主角攻击",
"killed": false,
"normal": 2,
"origin_cell": [
-1,
-1
],
"roll": 12,
"round_index": 1,
"source_class": "hero_attack",
"target": "地精一伙",
"target_id": "goblin",
"tier": 100,
"type": "damage",
"value": 2
},
{
"ac": 10,
"attack_type": "normal",
"baseline": 1,
"crit": true,
"display_name": "地精一伙 普通攻击(暴击)",
"origin_cell": [
-1,
-1
],
"roll": 20,
"round_index": 1,
"skill_id": "attack",
"source_class": "enemy_action",
"target": "hero",
"tier": 100,
"type": "damage",
"value": 2
},
{
"ac": 10,
"attack_type": "normal",
"baseline": 1,
"display_name": "地精一伙 普通攻击(被压制)",
"origin_cell": [
-1,
-1
],
"roll": 2,
"round_index": 1,
"skill_id": "attack",
"source_class": "enemy_action",
"target": "hero",
"tier": 25,
"type": "damage",
"value": 1
},
{
"display_name": "状态衰减",
"origin_cell": [
-1,
-1
],
"round_index": 1,
"source_class": "",
"type": "decay",
"value": 0
},
{
"display_name": "下一轮次",
"origin_cell": [
-1,
-1
],
"round_index": 1,
"source_class": "",
"type": "verdict",
"value": 0
},
{
"display_name": "暴走",
"effect_id": "enrage",
"origin_cell": [
0,
0
],
"pool_value": 25,
"round_index": 2,
"source_class": "mine_hit",
"type": "effect",
"value": 25
},
{
"ac": 1,
"display_name": "主角攻击",
"killed": true,
"normal": 2,
"origin_cell": [
-1,
-1
],
"roll": 2,
"round_index": 2,
"source_class": "hero_attack",
"target": "地精一伙",
"target_id": "goblin",
"tier": 100,
"type": "damage",
"value": 2
},
{
"ac": 10,
"attack_type": "normal",
"baseline": 1,
"display_name": "地精一伙 普通攻击(被压制)",
"origin_cell": [
-1,
-1
],
"roll": 1,
"round_index": 2,
"skill_id": "attack",
"source_class": "enemy_action",
"target": "hero",
"tier": 25,
"type": "damage",
"value": 1
},
{
"display_name": "状态衰减",
"origin_cell": [
-1,
-1
],
"round_index": 2,
"source_class": "",
"type": "decay",
"value": 1
},
{
"display_name": "下一轮次",
"origin_cell": [
-1,
-1
],
"round_index": 2,
"source_class": "",
"type": "verdict",
"value": 0
},
{
"ac": 1,
"display_name": "主角攻击",
"killed": false,
"normal": 2,
"origin_cell": [
-1,
-1
],
"roll": 8,
"round_index": 3,
"source_class": "hero_attack",
"target": "地精一伙",
"target_id": "goblin",
"tier": 100,
"type": "damage",
"value": 2
},
{
"ac": 10,
"attack_type": "normal",
"baseline": 1,
"display_name": "地精一伙 普通攻击(被压制)",
"origin_cell": [
-1,
-1
],
"roll": 6,
"round_index": 3,
"skill_id": "attack",
"source_class": "enemy_action",
"target": "hero",
"tier": 50,
"type": "damage",
"value": 1
},
{
"display_name": "状态衰减",
"origin_cell": [
-1,
-1
],
"round_index": 3,
"source_class": "",
"type": "decay",
"value": 0
},
{
"display_name": "下一轮次",
"origin_cell": [
-1,
-1
],
"round_index": 3,
"source_class": "",
"type": "verdict",
"value": 0
}
],
"snaps": [
{
"board": {
"h": 10,
"revealed": 42,
"seals": 1,
"seq": 1,
"w": 10
},
"buffs": [],
"depth": 1,
"economy": {
"ap": 0,
"phase": "Waiting",
"rap": 26,
"refresh_used": 0
},
"enemies": [
{
"armor": "light",
"armor_class": 1,
"atk": 1,
"hp": 2,
"id": "goblin",
"max_hp": 4,
"pending_skill": ""
},
{
"armor": "light",
"armor_class": 1,
"atk": 1,
"hp": 4,
"id": "goblin",
"max_hp": 4,
"pending_skill": ""
}
],
"hero": {
"armor_class": 10,
"atk": 2,
"hp": 23,
"max_hp": 26
},
"kind": "settlement",
"level": 1,
"round": 1,
"round_facts": {
"mines": 1,
"path": "stopped",
"safe": 0,
"seal_events": 1
},
"spawn_count": 2,
"unit_count": 2,
"verdict": "continue"
},
{
"board": {
"h": 10,
"revealed": 43,
"seals": 0,
"seq": 2,
"w": 10
},
"buffs": [],
"depth": 1,
"economy": {
"ap": 0,
"phase": "Waiting",
"rap": 25,
"refresh_used": 1
},
"enemies": [
{
"armor": "light",
"armor_class": 1,
"atk": 1,
"hp": 4,
"id": "goblin",
"max_hp": 4,
"pending_skill": ""
}
],
"hero": {
"armor_class": 10,
"atk": 2,
"hp": 22,
"max_hp": 26
},
"kind": "settlement",
"level": 1,
"round": 2,
"round_facts": {
"mines": 1,
"path": "stopped",
"safe": 42,
"seal_events": 0
},
"spawn_count": 2,
"unit_count": 1,
"verdict": "continue"
},
{
"board": {
"h": 10,
"revealed": 43,
"seals": 0,
"seq": 2,
"w": 10
},
"buffs": [],
"depth": 1,
"economy": {
"ap": 0,
"phase": "Waiting",
"rap": 33,
"refresh_used": 0
},
"enemies": [
{
"armor": "light",
"armor_class": 1,
"atk": 1,
"hp": 2,
"id": "goblin",
"max_hp": 4,
"pending_skill": ""
}
],
"hero": {
"armor_class": 10,
"atk": 2,
"hp": 21,
"max_hp": 26
},
"kind": "settlement",
"level": 1,
"round": 3,
"round_facts": {
"mines": 0,
"path": "stopped",
"safe": 0,
"seal_events": 0
},
"spawn_count": 2,
"unit_count": 1,
"verdict": "continue"
}
]
}
"""
var _db
func _init() -> void:
_db = ConfigDBScript.new()
var load_ok: bool = _db.load_tables("res://data/tables", true)
Kit.check("前置:表加载零 error", load_ok and _db.diagnostics_count("error") == 0)
var orch = load("res://core/battle_orchestrator.gd").new(_db, _make_rng())
var rows: Array = []
var snaps: Array = []
var finished: Array = []
orch.step_row.connect(func(row: Dictionary) -> void: rows.append(row))
orch.settlement_snapshot.connect(func(s: Dictionary) -> void: snaps.append(s))
orch.battle_finished.connect(func(p: Dictionary) -> void: finished.append(p))
var accepted: bool = orch.begin_battle({"level_index": 1, "node_depth": 1,
"hero_hp": int(_db.combat.hero.max_hp), "voucher_rap": 20})
Kit.check("前置:begin_battle 受理", accepted)
if accepted:
_drive(orch)
var final_snap: Dictionary = orch.presentation_snapshot()
var captured := {"rows": rows, "snaps": snaps, "finished": finished,
"final": {"round": int(final_snap.get("round", -1)),
"battle_result": String(final_snap.get("battle_result", "")),
"hero_hp": int((final_snap.get("hero", {}) as Dictionary).get("hp", -1)),
"enemies_n": (final_snap.get("enemies", []) as Array).size()}}
if GOLDEN == "":
print("[golden-dump] begin")
print(JSON.stringify(captured, " "))
print("[golden-dump] end")
quit(DUMP_EXIT)
return
# GOLDEN 三引号字面量首尾各带一个换行(多行字符串语法载体)——对拍两侧 strip 首尾空白后逐位比
var captured_json := JSON.stringify(captured, " ").strip_edges()
var golden_json := GOLDEN.strip_edges()
if captured_json != golden_json:
print("[captured-dump] begin")
print(JSON.stringify(captured, " "))
print("[captured-dump] end")
Kit.check("封装5.1 V1 同种子逐项对拍:行流/快照/终局载荷/presentation 逐位同 golden",
captured_json == golden_json)
quit(Kit.finish("settlement_wiring_snapshot_test"))
func _make_rng():
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(TEST_SEED)
return rng
# 驱动序(卡面定死):轮1 挖+封+停 → 刷新 → 轮2 一挖一停 → 轮3 停手
func _drive(orch) -> void:
var ms = orch.minesweeper
# 轮 1:choose_dig 非空则 move→dig→封首个未揭示主角邻格→stop
var pick: Dictionary = AutoPlayer.choose_dig(ms)
if not pick.is_empty() and pick.get("dig_cell", null) != null:
ms.settle_batch([{"action": "move", "cell": pick["hero_cell"]}])
ms.settle_batch([{"action": "dig", "cell": pick["dig_cell"]}])
var c := _first_unrevealed_adjacent(orch)
if c.x >= 0:
ms.settle_batch([{"action": "seal_place", "cell": c}])
ms.settle_batch([{"action": "stop"}])
# 轮 1 结算后刷新(voucher_rap=20 保证 RAP 抵折后价——覆盖换图刷新链)
orch.player_request_refresh()
# 轮 2:一挖一停
var pick2: Dictionary = AutoPlayer.choose_dig(ms)
if not pick2.is_empty() and pick2.get("dig_cell", null) != null:
ms.settle_batch([{"action": "move", "cell": pick2["hero_cell"]}])
ms.settle_batch([{"action": "dig", "cell": pick2["dig_cell"]}])
ms.settle_batch([{"action": "stop"}])
# 轮 3:径直停手
ms.settle_batch([{"action": "stop"}])
# 首个未揭示且与主角 8 邻接的格(卡面定死封印目标选取;无候选=返回 (-1,-1) 跳过封印)
func _first_unrevealed_adjacent(orch) -> Vector2i:
var ms = orch.minesweeper
var hero: Vector2i = ms.hero_pos()
var board = ms.current_board()
for y in board.height:
for x in board.width:
var c := Vector2i(x, y)
if not ms.is_revealed(c) and board.neighbors8(hero).has(c):
return c
return Vector2i(-1, -1)
@@ -0,0 +1 @@
uid://uys0a2kxc8a6
+4 -4
View File
@@ -668,7 +668,7 @@ func _smoke_feats() -> void:
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {})) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}))
var pm_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}) var pm_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
var pm_bonus := 0 var pm_bonus := 0
for h in orch._hits_of(pm_hits, "attack_bonus"): for h in DamageController.hits_of(pm_hits, "attack_bonus"):
pm_bonus = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", ""))) pm_bonus = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", "")))
Kit.check("疼痛记忆:攻击附加层值伤害=层值 2·用后清零(set 行随施加面)〔专长批 V3〕", pm_bonus == 2) Kit.check("疼痛记忆:攻击附加层值伤害=层值 2·用后清零(set 行随施加面)〔专长批 V3〕", pm_bonus == 2)
fx.rebuild(["fuse_wire"], [], {}) fx.rebuild(["fuse_wire"], [], {})
@@ -688,7 +688,7 @@ func _smoke_feats() -> void:
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {})) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}))
var ti_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}) var ti_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
var ti_val := 0 var ti_val := 0
for h in orch._hits_of(ti_hits, "attack_bonus"): for h in DamageController.hits_of(ti_hits, "attack_bonus"):
ti_val = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", ""))) ti_val = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", "")))
Kit.check("陷阱本能:踩雷本轮攻击 +2(attack_bonus 通道——面板加算)〔专长批 V3〕", ti_val == 2) Kit.check("陷阱本能:踩雷本轮攻击 +2(attack_bonus 通道——面板加算)〔专长批 V3〕", ti_val == 2)
fx.rebuild(["chain_traps", "pain_memory"], [], {}) fx.rebuild(["chain_traps", "pain_memory"], [], {})
@@ -724,7 +724,7 @@ func _smoke_feats() -> void:
EffectEngine.counter_add(orch.fx_host().counters_ctx, "stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账 EffectEngine.counter_add(orch.fx_host().counters_ctx, "stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账
Kit.check("弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕", _hits_kind(me_hits, "counter_add").size() == 1) Kit.check("弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕", _hits_kind(me_hits, "counter_add").size() == 1)
var me_atk := 0 var me_atk := 0
for h in orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus"): for h in DamageController.hits_of(fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus"):
me_atk = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", ""))) me_atk = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", "")))
Kit.check("弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕", me_atk == 4) Kit.check("弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕", me_atk == 4)
Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty()) Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty())
@@ -788,7 +788,7 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
autoplay._drive_level(a.runner(), st, null, "normal", "min") autoplay._drive_level(a.runner(), st, null, "normal", "min")
if String(st["result"]) != "": if String(st["result"]) != "":
return false return false
if a._bp_store._pending_next_level: if a._bp_store.pending_next_level():
# 三选一停点确认(确定性选件后推进,与 run_adventure 自动策略同口径;TP0 令牌化) # 三选一停点确认(确定性选件后推进,与 run_adventure 自动策略同口径;TP0 令牌化)
if not a.stop()._pending.is_empty(): if not a.stop()._pending.is_empty():
var bcands: Array = a.stop()._pending["candidates"] var bcands: Array = a.stop()._pending["candidates"]
@@ -41,7 +41,7 @@ func _enemy_rows() -> Array:
return [ return [
{"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "goblin"}, {"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "goblin"},
{"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "bugbear"}, {"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "bugbear"},
{"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "hobgoblin"}, {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "giant_spider"},
{"level_index": 1, "row_kind": "boss", "depth_band": 9, "enemy_id": "ogre_chief"}, {"level_index": 1, "row_kind": "boss", "depth_band": 9, "enemy_id": "ogre_chief"},
] ]
@@ -56,10 +56,10 @@ func _difficulty_table():
func _test_band_hit() -> void: func _test_band_hit() -> void:
var hit: Array = _resolver.resolve_enemy_rowset(1, 4, "battle") var hit: Array = _resolver.resolve_enemy_rowset(1, 4, "battle")
Kit.check("带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕", Kit.check("带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕",
hit.size() == 1 and String((hit[0] as Dictionary).get("enemy_id", "")) == "hobgoblin") hit.size() == 1 and String((hit[0] as Dictionary).get("enemy_id", "")) == "giant_spider")
var exact: Array = _resolver.resolve_enemy_rowset(1, 5, "battle") var exact: Array = _resolver.resolve_enemy_rowset(1, 5, "battle")
Kit.check("精确落带:请求深度 5 命中带 5〔fv1〕", Kit.check("精确落带:请求深度 5 命中带 5〔fv1〕",
exact.size() == 1 and String((exact[0] as Dictionary).get("enemy_id", "")) == "hobgoblin") exact.size() == 1 and String((exact[0] as Dictionary).get("enemy_id", "")) == "giant_spider")
func _test_rowset_order_preserved() -> void: func _test_rowset_order_preserved() -> void:
@@ -75,7 +75,7 @@ func _test_top_band_clamp() -> void:
_diags.clear() _diags.clear()
var rows: Array = _resolver.resolve_enemy_rowset(1, 99, "battle") var rows: Array = _resolver.resolve_enemy_rowset(1, 99, "battle")
Kit.check("越顶带 clamp 取顶带行集(battle 顶带 5)〔fv3〕", Kit.check("越顶带 clamp 取顶带行集(battle 顶带 5)〔fv3〕",
rows.size() == 1 and String((rows[0] as Dictionary).get("enemy_id", "")) == "hobgoblin") rows.size() == 1 and String((rows[0] as Dictionary).get("enemy_id", "")) == "giant_spider")
var clamp_hit := false var clamp_hit := false
for d in _diags: for d in _diags:
if d.contains("warning/resolve_depth_clamp"): if d.contains("warning/resolve_depth_clamp"):
@@ -0,0 +1,76 @@
# tests/unit/core/encapsulation_p2_sweep_test.gd — 架构审查 P2·NODE_TYPES 词表单点化结构性扫描宿主
# 断言面 = 两组(均带「扫描面非空前置+正锚」):
# V1 node_map_generator:非空+本地 const NODE_TYPES 定义零命中+LevelTable.NODE_TYPES.has( 正锚
# (词表唯一落点归 schema owner——表行校验 LevelTable;生成器 class_name 直引=S3 豁免面在案形态)
# V2 生产三目录(core/data/presentation)`NODE_TYPES :=` 定义恰好 1 处且落 data/schemas/level_table.gd
# 手法沿 encapsulation_sweep_test 源 token 扫描先例(两卡独立 worktree 独立批——不并件)。
# 运行:godot --headless --path game --script res://tests/unit/core/encapsulation_p2_sweep_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
_sweep_generator()
_sweep_production_face()
quit(Kit.finish("encapsulation_p2_sweep_test"))
func _src(path: String) -> String:
var src := FileAccess.get_file_as_string(path)
Kit.check("扫描面非空前置:%s" % path, src.length() > 0)
return src
func _zero(pattern: String, src: String, label: String) -> void:
var re := RegEx.create_from_string(pattern)
Kit.check("%s:%s 零命中〔架构P2〕" % [label, pattern], re.search(src) == null)
func _anchor(pattern: String, src: String, label: String) -> void:
var re := RegEx.create_from_string(pattern)
Kit.check("%s:%s 正锚在案〔架构P2〕" % [label, pattern], re.search(src) != null)
# ① V1 生成器词表定义删除与改指
func _sweep_generator() -> void:
var src := _src("res://core/node_map_generator.gd")
if src.length() == 0:
return
_zero("(?m)^const NODE_TYPES", src, "生成器本地词表副本归零")
_anchor("LevelTable\\.NODE_TYPES\\.has\\(", src, "schema 词表直引在用")
# ② V2 生产三目录定义唯一性(递归扫 .gd,恰好 1 处且落 level_table.gd)
func _sweep_production_face() -> void:
var files: Array[String] = []
for dir_path in ["res://core", "res://data", "res://presentation"]:
_collect_gd(dir_path, files)
Kit.check("扫描面非空前置:生产三目录 .gd ≥ 1 件", files.size() > 0)
var re := RegEx.create_from_string("NODE_TYPES\\s*:?=")
var hits: Array[String] = []
for f in files:
var src := FileAccess.get_file_as_string(f)
if src.length() > 0 and re.search(src) != null:
hits.append(f)
Kit.check("生产面 NODE_TYPES 定义恰好 1 处(实得 %d)〔架构P2〕" % hits.size(), hits.size() == 1)
if hits.size() == 1:
Kit.check("唯一定义落 data/schemas/level_table.gd〔架构P2〕", hits[0] == "res://data/schemas/level_table.gd")
# 递归收集 .gd(跳过 .godot 缓存与 .uid 边车)
func _collect_gd(dir_path: String, out: Array[String]) -> void:
var dir := DirAccess.open(dir_path)
if dir == null:
return
dir.list_dir_begin()
var name := dir.get_next()
while name != "":
var full := dir_path + "/" + name
if dir.current_is_dir():
if not name.begins_with("."):
_collect_gd(full, out)
elif name.ends_with(".gd"):
out.append(full)
name = dir.get_next()
dir.list_dir_end()
@@ -0,0 +1 @@
uid://50nvccb1clcg
@@ -0,0 +1,110 @@
# tests/unit/core/encapsulation_sweep_test.gd — 架构审查 5.1 封装修复·五处结构性零命中扫描宿主
# 断言面 = 七段断言组(全部带「扫描面非空前置+正锚」——非空防扫描面为空静默绿,正锚证新面在用):
# V2 settlement 七文件:非空+class_name 正锚+\boc\b 全文零命中(段类对编排器引用归零,含头注)
# V3 battle_orchestrator:非空+搬家五方法定义零命中(宿主原方法删除禁留壳)
# V4 adventure_runner:非空+_bp_store._ 零命中+bp_read 正锚(公开面在用)
# V5 save_manager:非空+slots._ 零命中+active_path 正锚;slot_store:非空+_save._ 零命中+公开面正锚
# V6 grant_segment:非空+两 fx 私键零命中+mark_new_resource_type 正锚
# V7 config_db:非空+._resolver 直写零命中+bind_resolver 正锚
# 手法沿 player_copy_sweep_test 源 token 扫描先例(不并件——扫描面归属与尾缀语义本批特有)。
# 运行:godot --headless --path game --script res://tests/unit/core/encapsulation_sweep_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
_sweep_settlement_segments()
_sweep_orchestrator_methods()
_sweep_breakpoint_pair()
_sweep_save_pair()
_sweep_fx_host_accumulators()
_sweep_config_resolver()
quit(Kit.finish("encapsulation_sweep_test"))
# 源文件读取+非空前置(返 "" 即已判红——调用侧跳过后续正则断言)
func _src(path: String) -> String:
var src := FileAccess.get_file_as_string(path)
Kit.check("扫描面非空前置:%s" % path, src.length() > 0)
return src
# 正则零命中断言(-pattern 意图写入断言文案——可 grep 定位)
func _zero(pattern: String, src: String, label: String, path: String) -> void:
var re := RegEx.create_from_string(pattern)
Kit.check("%s:%s 零命中〔封装5.1〕" % [label, pattern], re.search(src) == null)
# 正锚在案断言(正则命中 ≥1——新公开面/新形态在用)
func _anchor(pattern: String, src: String, label: String, path: String) -> void:
var re := RegEx.create_from_string(pattern)
Kit.check("%s:%s 正锚在案〔封装5.1〕" % [label, pattern], re.search(src) != null)
# ① V2 settlement 七文件(束注入换代——对编排器引用归零)
func _sweep_settlement_segments() -> void:
var classes := {
"res://core/settlement/grant_segment.gd": "GrantSegment",
"res://core/settlement/combat_segment.gd": "CombatSegment",
"res://core/settlement/round_cycle.gd": "RoundCycle",
"res://core/settlement/verdict_segment.gd": "VerdictSegment",
"res://core/settlement/battle_refresh_flow.gd": "BattleRefreshFlow",
"res://core/settlement/aura_segment.gd": "AuraSegment",
"res://core/settlement/decay_segment.gd": "DecaySegment",
}
for path in classes:
var src := _src(path)
if src.length() == 0:
continue
_anchor("class_name %s" % classes[path], src, "class_name 正锚(%s)" % classes[path], path)
_zero("\\boc\\b", src, "对编排器标识符全归零(%s)" % classes[path], path)
# ② V3 编排器搬家五方法零残留
func _sweep_orchestrator_methods() -> void:
var src := _src("res://core/battle_orchestrator.gd")
if src.length() == 0:
return
_zero("func _hero_atk\\(|func _feat_yield_adjust\\(|func _harmful_variants\\(|func _harmful_adjacent\\(|func _hits_of\\(",
src, "编排器搬家五方法定义零残留(宿主删除禁留壳)", "res://core/battle_orchestrator.gd")
# ③ V4 断点域(adventure_runner → BreakpointStore 公开面)
func _sweep_breakpoint_pair() -> void:
var src := _src("res://core/adventure_runner.gd")
if src.length() == 0:
return
_zero("_bp_store\\._", src, "宿主对断点件私取归零", "res://core/adventure_runner.gd")
_anchor("_bp_store\\.bp_read\\(", src, "断点公开读侧在用", "res://core/adventure_runner.gd")
# ④ V5 存档双向公开面(save_manager↔slot_store)
func _sweep_save_pair() -> void:
var src_m := _src("res://core/save_manager.gd")
if src_m.length() > 0:
_zero("slots\\._", src_m, "save_manager 对槽位件私取归零", "res://core/save_manager.gd")
_anchor("slots\\.active_path\\(\\)", src_m, "槽路径读投影在用", "res://core/save_manager.gd")
var src_s := _src("res://core/slot_store.gd")
if src_s.length() > 0:
_zero("_save\\._", src_s, "slot_store 反向私取归零", "res://core/slot_store.gd")
_anchor("_save\\.(set_read_only|read_only|reset_to_default|data_snapshot|restore_data|peek_summary_at|rename_overwrite)",
src_s, "存档公开承接面在用", "res://core/slot_store.gd")
# ⑤ V6 效果宿主累积面(grant → BattleFxHost 公开累积方法)
func _sweep_fx_host_accumulators() -> void:
var src := _src("res://core/settlement/grant_segment.gd")
if src.length() == 0:
return
_zero("_fx_new_resource_types|_fx_bonus_delta", src, "授予段对效果宿主私键直写归零", "res://core/settlement/grant_segment.gd")
_anchor("mark_new_resource_type\\(", src, "首见登记公开方法在用", "res://core/settlement/grant_segment.gd")
# ⑥ V7 配置挂载面(config_db → 三 schema bind_resolver)
func _sweep_config_resolver() -> void:
var src := _src("res://core/config_db.gd")
if src.length() == 0:
return
_zero("\\._resolver\\s*=", src, "解析协作件直写归零", "res://core/config_db.gd")
_anchor("bind_resolver\\(_resolver\\)", src, "挂载口在用", "res://core/config_db.gd")
@@ -0,0 +1 @@
uid://dq1huyomw37m2
+1 -1
View File
@@ -78,7 +78,7 @@ func _init() -> void:
# 账本保留(契约 1):主动件充能与每冒险种行入口零改动(单源化后=差额种行幂等——不吞装配) # 账本保留(契约 1):主动件充能与每冒险种行入口零改动(单源化后=差额种行幂等——不吞装配)
a._attunement = ["incense_stick"] a._attunement = ["incense_stick"]
a._loadout.set_attunement(["incense_stick"]) a._loadout.set_attunement(["incense_stick"])
a._bp_store._loadout_seeded = false # 模拟首关 bind 时点(分派走 seed_for_adventure 分支——成员随断点件收编改址) a._bp_store.set_loadout_seeded(false) # 模拟首关 bind 时点(分派走 seed_for_adventure 分支——成员随断点件收编改址)
a._loadout_on_level_inventory() # 种行(首关 bind 时点语义) a._loadout_on_level_inventory() # 种行(首关 bind 时点语义)
var incense_left := 0 var incense_left := 0
for r in a.runner().treasure_inventory.snapshot_inventory(): for r in a.runner().treasure_inventory.snapshot_inventory():
@@ -724,7 +724,7 @@ func _smoke_level_graph() -> void:
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1) _db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1) _db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1) _db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
Kit.check("混编断言段注入行全量还原(表回 28 行——迭代 35 生成图深度扩面覆盖批后的基线行数;首批混编投放基线 20 行+覆盖 8 行)", int(_db.combat.enemy.rows.size()) == 28) Kit.check("混编断言段注入行全量还原(表回 29 行——L1 混编批后的基线行数;首批混编投放基线 21 行+覆盖 8 行)", int(_db.combat.enemy.rows.size()) == 29)
# ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 / # ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 /
# 终点恒可见 / 入口 BFS 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。 # 终点恒可见 / 入口 BFS 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。
@@ -1314,12 +1314,15 @@ func _smoke_shop_pool() -> void:
expect_p = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0)) expect_p = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0))
Kit.check("首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" % [ckind, int(c.get("price", -1)), expect_p], int(c.get("price", -1)) == expect_p) Kit.check("首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" % [ckind, int(c.get("price", -1)), expect_p], int(c.get("price", -1)) == expect_p)
# ③ 买 1 全重抽:出账+加算区+账本三落点 + 现价 A 链递增(买后 visit 计数=1;TP0——消费在架令牌本体) # ③ 买 1 全重抽:出账+加算区+账本三落点 + 现价 A 链递增(买后 visit 计数=1;TP0——消费在架令牌本体)
# 白盒抬 GP 保证首购场景确定构造(直改测试基建——非生产行为,A 链段同款先例;(1,3) 混编后战斗流
# 消费序变化使 gp 分布漂移,种子扫描不再隐式保证首购空间——买链判据本身零变化)
r8.gp = int(r8.gp) + 128
var buy_c: Dictionary = {} var buy_c: Dictionary = {}
for c in r8.shop.shop_shelf(): for c in r8.shop.shop_shelf():
if int(c.get("price", 1 << 30)) <= int(r8.gp): if int(c.get("price", 1 << 30)) <= int(r8.gp):
buy_c = c buy_c = c
break break
Kit.check("可购卡在架(GP %d——种子扫描保证首购空间)" % int(r8.gp), not buy_c.is_empty()) Kit.check("可购卡在架(GP %d——白盒抬 GP 保证首购空间)" % int(r8.gp), not buy_c.is_empty())
if not buy_c.is_empty(): if not buy_c.is_empty():
var kind := String(buy_c.get("kind_id", "")) var kind := String(buy_c.get("kind_id", ""))
var tier := String(buy_c.get("tier", "")) var tier := String(buy_c.get("tier", ""))
@@ -1478,7 +1481,7 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
autoplay._drive_level(a.runner(), st, null, "normal", "min") autoplay._drive_level(a.runner(), st, null, "normal", "min")
if String(st["result"]) != "": if String(st["result"]) != "":
return false return false
if a._bp_store._pending_next_level: if a._bp_store.pending_next_level():
# 三选一停点确认(确定性选件后推进,与 run_adventure 自动策略同口径;TP0 令牌化) # 三选一停点确认(确定性选件后推进,与 run_adventure 自动策略同口径;TP0 令牌化)
if not a.stop()._pending.is_empty(): if not a.stop()._pending.is_empty():
var bcands: Array = a.stop()._pending["candidates"] var bcands: Array = a.stop()._pending["candidates"]
+23 -13
View File
@@ -15,23 +15,33 @@ func _init() -> void:
quit(1) quit(1)
return return
# V4-1 段类直接 new()(构造收宿主引用——五类同构) # V4-1 段类直接 new()(引用束注入——编排器 _init 同款 20 键束组装,逐段 new(bundle) 成功断言)
var oc = load("res://core/battle_orchestrator.gd").new(db, load("res://core/rng_service.gd").new()) var oc = load("res://core/battle_orchestrator.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(load("res://core/settlement/grant_segment.gd").new(oc, oc.fx_host(), oc.reward_grant()) != null, "V4 GrantSegment 直接 new()〔迭代27 T2〕") var seg_bundle: Dictionary[Variant, Variant] = {
Kit.check_quiet(load("res://core/settlement/aura_segment.gd").new(oc) != null, "V4 AuraSegment 直接 new()〔迭代27 T2〕") "facts": oc._facts, "units": oc._enemy_system, "damage": oc._damage,
Kit.check_quiet(load("res://core/settlement/combat_segment.gd").new(oc, oc.fx_host(), oc.reward_grant()) != null, "V4 CombatSegment 直接 new()〔迭代27 T2〕") "status_effects": oc._status_effects, "reward_economy": oc._reward_economy,
Kit.check_quiet(load("res://core/settlement/decay_segment.gd").new(oc) != null, "V4 DecaySegment 直接 new()〔迭代27 T2〕") "reward_grant": oc.reward_grant(), "fx_host": oc.fx_host(), "minesweeper": oc.minesweeper,
Kit.check_quiet(load("res://core/settlement/verdict_segment.gd").new(oc) != null, "V4 VerdictSegment 直接 new()〔迭代27 T2〕") "db": oc.db, "action_economy": oc.action_economy, "rng": oc.rng,
"emit_row_cb": oc._emit_row, "fatal_cb": oc._fatal, "install_cb": oc._install_and_reset,
"battle_over_cb": oc._battle_over, "exit_distribution_cb": oc._exit_distribution,
"emit_snapshot_cb": oc._emit_settlement_snapshot, "emit_composition_cb": oc._emit_composition_effects,
"attune_param_sum_cb": oc.attune_param_sum, "perfect_sweep_cb": oc.perfect_sweep_earned,
}
Kit.check_quiet(load("res://core/settlement/grant_segment.gd").new(seg_bundle) != null, "V4 GrantSegment 束构造〔迭代27 T2〕")
Kit.check_quiet(load("res://core/settlement/aura_segment.gd").new(seg_bundle) != null, "V4 AuraSegment 束构造〔迭代27 T2〕")
Kit.check_quiet(load("res://core/settlement/combat_segment.gd").new(seg_bundle) != null, "V4 CombatSegment 束构造〔迭代27 T2〕")
Kit.check_quiet(load("res://core/settlement/decay_segment.gd").new(seg_bundle) != null, "V4 DecaySegment 束构造〔迭代27 T2〕")
Kit.check_quiet(load("res://core/settlement/verdict_segment.gd").new(seg_bundle) != null, "V4 VerdictSegment 束构造〔迭代27 T2〕")
# V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位)——注册面批同代归约 V1: # V4-2 编排器 _init 接线(段持注入引用同一性——宿主回指对象随束注入退役;段序编排面在位):
# 段件宿主直持改经 _rounds(轮循环驱动件)承载,接线断言改指 _rounds 段族 # 段件宿主直持改经 _rounds(轮循环驱动件)承载,接线断言改指 _rounds 段族
Kit.check_quiet(oc._rounds != null and oc._rounds.oc == oc, "V1 编排器接线·轮循环驱动件在座且宿主回指〔编排归约 V1〕") Kit.check_quiet(oc._rounds != null and oc._rounds.facts == oc._facts, "V1 编排器接线·轮循环驱动件在座且束注入同一性〔编排归约 V1〕")
Kit.check_quiet(oc._facts != null, "V1 编排器接线·战斗事实件在座〔编排归约 V1〕") Kit.check_quiet(oc._facts != null, "V1 编排器接线·战斗事实件在座〔编排归约 V1〕")
Kit.check_quiet(oc._rounds._grant_seg != null and oc._rounds._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕") Kit.check_quiet(oc._rounds._grant_seg != null and oc._rounds._grant_seg.facts == oc._facts, "V4 编排器接线·授予段(束注入同一性·facts)〔迭代27 T2〕")
Kit.check_quiet(oc._rounds._aura_seg != null and oc._rounds._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕") Kit.check_quiet(oc._rounds._aura_seg != null and oc._rounds._aura_seg.emit_composition_cb.get_object() == oc, "V4 编排器接线·增益/减益段(回调对象同一性)〔迭代27 T2〕")
Kit.check_quiet(oc._rounds._combat_seg != null and oc._rounds._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕") Kit.check_quiet(oc._rounds._combat_seg != null and oc._rounds._combat_seg.fx_host == oc.fx_host(), "V4 编排器接线·战斗段(束注入同一性·fx_host)〔迭代27 T2〕")
Kit.check_quiet(oc._rounds._decay_seg != null and oc._rounds._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕") Kit.check_quiet(oc._rounds._decay_seg != null and oc._rounds._decay_seg.status_effects == oc._status_effects, "V4 编排器接线·衰减段(束注入同一性·status_effects)〔迭代27 T2〕")
Kit.check_quiet(oc._rounds._verdict_seg != null and oc._rounds._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕") Kit.check_quiet(oc._rounds._verdict_seg != null and oc._rounds._verdict_seg.units == oc._enemy_system, "V4 编排器接线·判定段(束注入同一性·units)〔迭代27 T2〕")
# counter_add 写口真实路径(geologist 订阅行承载:公共命令链 begin_battle→move→dig→stop # counter_add 写口真实路径(geologist 订阅行承载:公共命令链 begin_battle→move→dig→stop
# 经授予段 counter_add 单点落 counters_ctx battle 桶——受理载荷+公开读数口 fx_host().counters_ctx) # 经授予段 counter_add 单点落 counters_ctx battle 桶——受理载荷+公开读数口 fx_host().counters_ctx)
var rng_c = load("res://core/rng_service.gd").new() var rng_c = load("res://core/rng_service.gd").new()
@@ -129,19 +129,19 @@ func _init() -> void:
ae6.queue_ap_delta(4 - int(db.run.action_economy.ap_per_round)) # 排队修正到测试口径 4 AP(notify_round_start 兑现) ae6.queue_ap_delta(4 - int(db.run.action_economy.ap_per_round)) # 排队修正到测试口径 4 AP(notify_round_start 兑现)
ae6.notify_round_start() ae6.notify_round_start()
ae6.request_stop() ae6.request_stop()
ae6.finish_round_stop(false, 100) ae6.finish_round_stop(100)
Kit.check_quiet(ae6.rap == 4, "储备罐基线:未装配 1:1(4 AP 停手转存 RAP 4——逐位同旧)") Kit.check_quiet(ae6.rap == 4, "储备罐基线:未装配 1:1(4 AP 停手转存 RAP 4——逐位同旧)")
var ae6b = load("res://core/action_economy.gd").new(db) var ae6b = load("res://core/action_economy.gd").new(db)
ae6b.queue_ap_delta(4 - int(db.run.action_economy.ap_per_round)) ae6b.queue_ap_delta(4 - int(db.run.action_economy.ap_per_round))
ae6b.notify_round_start() ae6b.notify_round_start()
ae6b.request_stop() ae6b.request_stop()
ae6b.finish_round_stop(false, 125) ae6b.finish_round_stop(125)
Kit.check_quiet(ae6b.rap == 5, "储备罐正常:125% 转存(4 AP→RAP 5=1.25:1 整除口径;不碰 AP 上限)") Kit.check_quiet(ae6b.rap == 5, "储备罐正常:125% 转存(4 AP→RAP 5=1.25:1 整除口径;不碰 AP 上限)")
var ae6c = load("res://core/action_economy.gd").new(db) var ae6c = load("res://core/action_economy.gd").new(db)
ae6c.queue_ap_delta(3 - int(db.run.action_economy.ap_per_round)) ae6c.queue_ap_delta(3 - int(db.run.action_economy.ap_per_round))
ae6c.notify_round_start() ae6c.notify_round_start()
ae6c.request_stop() ae6c.request_stop()
ae6c.finish_round_stop(false, 125) ae6c.finish_round_stop(125)
Kit.check_quiet(ae6c.rap == 3, "储备罐边界:3 AP→3(floor 3.75——不越参数面直改停手结算)") Kit.check_quiet(ae6c.rap == 3, "储备罐边界:3 AP→3(floor 3.75——不越参数面直改停手结算)")
# ⑦ 巢穴预告(装板后一次报数量——明细行经 step_row 信号收集) # ⑦ 巢穴预告(装板后一次报数量——明细行经 step_row 信号收集)
@@ -219,7 +219,7 @@ func _test_rowset_discipline() -> void:
Kit.check("Boss 带混编行集加载通过(P4 乙——Boss 多行合法,无硬闸)", errs.is_empty()) Kit.check("Boss 带混编行集加载通过(P4 乙——Boss 多行合法,无硬闸)", errs.is_empty())
# 负向:同行集行不相邻 = rowset_rule 且报错定位到带键(R6 行序纪律 + R3 错误类别可定位) # 负向:同行集行不相邻 = rowset_rule 且报错定位到带键(R6 行序纪律 + R3 错误类别可定位)
var t_gap = EnemyTableScript.new() var t_gap = EnemyTableScript.new()
t_gap.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "battle", 9, "hobgoblin"), t_gap.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "battle", 9, "giant_spider"),
mk_row.call(1, "battle", 5, "bugbear"), mk_row.call(1, "battle", 0, "goblin"), mk_row.call(1, "battle", 5, "bugbear"), mk_row.call(1, "battle", 0, "goblin"),
mk_row.call(1, "boss", 9, "ogre_chief")]) mk_row.call(1, "boss", 9, "ogre_chief")])
errs = t_gap.validate() errs = t_gap.validate()
@@ -1,7 +1,7 @@
# tests/unit/data_config/table_structure_guard_test.gd — 表结构守卫单元测试宿主 # tests/unit/data_config/table_structure_guard_test.gd — 表结构守卫单元测试宿主
# 断言面 = data/tables 各表的结构锚与守卫,七套: # 断言面 = data/tables 各表的结构锚与守卫,七套:
# adventure 冒险表(档位配额/件值对/同调目录 12 件/装配→战斗贯通) # adventure 冒险表(档位配额/件值对/同调目录 12 件/装配→战斗贯通)
# enemy 敌人表(20 行带键行序/hobgoblin 留任/三 Boss 行) # enemy 敌人表(29 行带键行序/混编行集/三 Boss 行)
# AC 单轨与掷点参数(status_effect/reward/settlement 三处无双轨/免伤墙护栏/敌 AC 带宽) # AC 单轨与掷点参数(status_effect/reward/settlement 三处无双轨/免伤墙护栏/敌 AC 带宽)
# variation 地形变异(行数/候选池门控/跨字段互锁负样本) # variation 地形变异(行数/候选池门控/跨字段互锁负样本)
# difficulty 难度表(空表零行为/修饰套乘/越域 clamp) # difficulty 难度表(空表零行为/修饰套乘/越域 clamp)
@@ -206,15 +206,15 @@ func _smoke_adventure_table() -> void:
v18_ok and locked_n == 12) v18_ok and locked_n == 12)
# ---- 敌人表深度带键结构(迭代 34 建·迭代 35 覆盖批换代:28 行键序=覆盖表序·hobgoblin 留任·Boss 行集带键清单)---- # ---- 敌人表深度带键结构(迭代 34 建·L1 混编批换代:29 行键序=覆盖表序·(1,3) 混编行集·Boss 行集带键清单)----
func _smoke_enemy_bands() -> void: func _smoke_enemy_bands() -> void:
print("--- 敌人表深度带键结构(28 行键序 · hobgoblin 留任 · Boss 带键覆盖表)---") print("--- 敌人表深度带键结构(29 行键序 · (1,3) 混编 · Boss 带键覆盖表)---")
# ① 28 行计数 + 键序 = 覆盖表序(文件行序不变——行序即契约) # ① 29 行计数 + 键序 = 覆盖表序(文件行序不变——行序即契约;(1,3) 两行相邻、熊地精在前)
# 迭代 35 生成图深度扩面覆盖批随动:战斗带顶扩到各关最大生成深度(L1→6/L2→9/L3→13)、 # 迭代 35 生成图深度扩面覆盖批随动:战斗带顶扩到各关最大生成深度(L1→6/L2→9/L3→13)、
# Boss 行按终点深度分带(L1 6/L2 8·9/L3 11·12·13),模板时代带键 4/9/9 全部在册不动 # Boss 行按终点深度分带(L1 6/L2 8·9/L3 11·12·13),模板时代带键 4/9/9 全部在册不动
var expect_keys: Array[String] = [ var expect_keys: Array[String] = [
"1|battle|0|goblin", "1|battle|1|goblin", "1|battle|2|bugbear", "1|battle|3|hobgoblin", "1|battle|0|goblin", "1|battle|1|goblin", "1|battle|2|bugbear", "1|battle|3|bugbear", "1|battle|3|goblin",
"1|battle|6|bugbear", "1|battle|6|bugbear",
"1|boss|4|ogre_chief", "1|boss|6|ogre_chief", "1|boss|4|ogre_chief", "1|boss|6|ogre_chief",
"2|battle|0|bugbear", "2|battle|1|bugbear", "2|battle|4|giant_spider", "2|battle|0|bugbear", "2|battle|1|bugbear", "2|battle|4|giant_spider",
@@ -229,16 +229,45 @@ func _smoke_enemy_bands() -> void:
for row in _db.combat.enemy.rows: for row in _db.combat.enemy.rows:
got_keys.append("%d|%s|%d|%s" % [int(row.get("level_index", -1)), String(row.get("row_kind", "")), got_keys.append("%d|%s|%d|%s" % [int(row.get("level_index", -1)), String(row.get("row_kind", "")),
int(row.get("depth_band", -1)), String(row.get("enemy_id", ""))]) int(row.get("depth_band", -1)), String(row.get("enemy_id", ""))])
Kit.check("敌人表 28 行且键序 = 覆盖表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕", Kit.check("敌人表 29 行且键序 = 覆盖表序((1,3) 两行相邻、熊地精在前)〔L1混编 v1〕",
_db.combat.enemy.rows.size() == 28 and got_keys == expect_keys) _db.combat.enemy.rows.size() == 29 and got_keys == expect_keys)
# ② hobgoblin 缺口行留任随迁(不删行不补立绘——补齐归敌种内容批) # ② (1,3) 混编构成与行序+静态最坏骰和+行值整行复用(R1-R4)
var hob := {} var rowset13: Array[Dictionary] = _db.combat.enemy.resolve_rowset(1, 3, "battle")
var ids13: Array[String] = []
var dice_sum := 0
for r in rowset13:
ids13.append(String(r.get("enemy_id", "")))
dice_sum += int(r.get("unit_count_max", int(r.get("unit_count", 1))))
var expect_ids13: Array[String] = ["bugbear", "goblin"]
Kit.check("(1,3) 行集恰两行、enemy_id 序列 == [bugbear, goblin](行序=文件序=生成序——默认目标取首行)〔L1混编 v2〕",
rowset13.size() == 2 and ids13 == expect_ids13)
Kit.check("(1,3) 静态最坏骰和 Σ(unit_count_max,固定行缺省) == 3〔L1混编 v3〕", dice_sum == 3)
var row12 := {}
var row10 := {}
for row in _db.combat.enemy.rows: for row in _db.combat.enemy.rows:
if String(row.get("enemy_id", "")) == "hobgoblin": if int(row.get("level_index", -1)) == 1 and String(row.get("row_kind", "")) == "battle" \
hob = row and int(row.get("depth_band", -1)) == 2:
Kit.check("hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕", row12 = row
int(hob.get("level_index", -1)) == 1 and String(hob.get("row_kind", "")) == "battle" \ if int(row.get("level_index", -1)) == 1 and String(row.get("row_kind", "")) == "battle" \
and int(hob.get("depth_band", -1)) == 3) and int(row.get("depth_band", -1)) == 0:
row10 = row
var mix_ok := rowset13.size() == 2
if mix_ok:
for fld in ["hp", "atk", "atk_min", "atk_max", "pool", "attack_type", "armor_type", "armor_class", "unit_count", "unit_count_max"]:
mix_ok = mix_ok and rowset13[0].get(fld) == row12.get(fld) and rowset13[1].get(fld) == row10.get(fld)
Kit.check("(1,3) 两行逐字段 == (1,2)/(1,0) 行(hp/atk 区间/pool/ac/类型轴——零数值新增,不写字面量)〔L1混编 v4〕", mix_ok)
# ②b 表内 enemy_id 集合不含 hobgoblin(退役键零残留)
var id_set := {}
for row in _db.combat.enemy.rows:
id_set[String(row.get("enemy_id", ""))] = true
Kit.check("表内 enemy_id 集合不含 hobgoblin〔L1混编 v5〕", not id_set.has("hobgoblin"))
# ②c 深度 4/5 落带不变(band 6 单行 bugbear——本批零触碰护栏)
var rowset14: Array[Dictionary] = _db.combat.enemy.resolve_rowset(1, 4, "battle")
var rowset15: Array[Dictionary] = _db.combat.enemy.resolve_rowset(1, 5, "battle")
var deep_ok: bool = rowset14.size() == 1 and rowset15.size() == 1 \
and String(rowset14[0].get("enemy_id", "")) == "bugbear" and String(rowset15[0].get("enemy_id", "")) == "bugbear" \
and int(rowset14[0].get("depth_band", -1)) == 6 and int(rowset15[0].get("depth_band", -1)) == 6
Kit.check("(1,4)/(1,5) battle 行集 == 单行 bugbear(band 6)〔L1混编 v6〕", deep_ok)
# ③ Boss 行集带键 = 覆盖表(旧判据「各恰一行=图 Boss 节点深度」属单终点模板时代; # ③ Boss 行集带键 = 覆盖表(旧判据「各恰一行=图 Boss 节点深度」属单终点模板时代;
# 生成图多终点要求每终点深度解析到互异带键 ⇒ 每关 Boss 行必然多于一行—— # 生成图多终点要求每终点深度解析到互异带键 ⇒ 每关 Boss 行必然多于一行——
# 迭代 35 覆盖批随动改写为带键清单,「带键互异」的运行时把关在 level_shop_economy_test〔v11〕) # 迭代 35 覆盖批随动改写为带键清单,「带键互异」的运行时把关在 level_shop_economy_test〔v11〕)
@@ -335,7 +364,8 @@ func _smoke_ac_swap_guard() -> void:
# ③c 熊地精首遇带降档锚定 + 第一关总量曲线护栏 # ③c 熊地精首遇带降档锚定 + 第一关总量曲线护栏
# 首遇带行 = 数据驱动定位(契约 8:该 (1,battle,bugbear) 的最低 depth_band)锚定值 hp 22 / ac 2; # 首遇带行 = 数据驱动定位(契约 8:该 (1,battle,bugbear) 的最低 depth_band)锚定值 hp 22 / ac 2;
# 敌种 bugbear 与池 attack6/charge2/heavy2 钉死不动(防顺手改多); # 敌种 bugbear 与池 attack6/charge2/heavy2 钉死不动(防顺手改多);
# 曲线护栏 = 深度带升序相邻带「hp × 中位数量」总量比 ≤ 2.5(12/10/22/35/72 现行全绿;不用单调不回撤——会红在首带→次带) # 曲线护栏 = 深度带升序相邻带「hp × 中位数量」逐带累加总量比 ≤ 2.5(12/10/22/34/72 现行全绿;不用单调不回撤——会红在首带→次带;
# 同带多行按行累加——(1,3) 混编两行同带,单行覆盖口径在双行行集下失真,与 R6 spawn 守卫逐行求和同口径非放宽)
var l12_band := _first_encounter_band(1, "bugbear") var l12_band := _first_encounter_band(1, "bugbear")
var l12_row: Dictionary = {} var l12_row: Dictionary = {}
for row in _db.combat.enemy.rows: for row in _db.combat.enemy.rows:
@@ -356,7 +386,8 @@ func _smoke_ac_swap_guard() -> void:
continue continue
var cnt_lo: int = int(row.get("unit_count", 1)) var cnt_lo: int = int(row.get("unit_count", 1))
var cnt_hi: int = int(row.get("unit_count_max", cnt_lo)) var cnt_hi: int = int(row.get("unit_count_max", cnt_lo))
l1_totals[int(row.get("depth_band", -1))] = int(row.get("hp", 0)) * (cnt_lo + cnt_hi) / 2.0 var bkey: int = int(row.get("depth_band", -1))
l1_totals[bkey] = float(l1_totals.get(bkey, 0.0)) + int(row.get("hp", 0)) * (cnt_lo + cnt_hi) / 2.0
var curve_ok := true var curve_ok := true
var curve_detail := "" var curve_detail := ""
var depths_sorted := l1_totals.keys() var depths_sorted := l1_totals.keys()
@@ -368,7 +399,7 @@ func _smoke_ac_swap_guard() -> void:
if ratio > 2.5: if ratio > 2.5:
curve_ok = false curve_ok = false
curve_detail += " (%d→%d):%.2f" % [depths_sorted[i - 1], depths_sorted[i], ratio] curve_detail += " (%d→%d):%.2f" % [depths_sorted[i - 1], depths_sorted[i], ratio]
Kit.check("第一关相邻深度带总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,带键升序;越带%s)" % [ Kit.check("第一关相邻深度带总量比 ≤ 2.5(12/10/22/34/72——迭代 24 N1 曲线护栏,带键升序逐带累加;越带%s)" % [
curve_detail if curve_detail != "" else "无"], curve_ok) curve_detail if curve_detail != "" else "无"], curve_ok)
# ④ 掷点参数域(加载校验已过——此处钉形状语义) # ④ 掷点参数域(加载校验已过——此处钉形状语义)
var bounds: Array = _db.combat.settlement.damage_roll.roll_tier_bounds var bounds: Array = _db.combat.settlement.damage_roll.roll_tier_bounds
@@ -111,7 +111,7 @@ func _test_snapshot_effects_per_unit() -> void:
var rng = load("res://core/rng_service.gd").new() var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20261103) rng.set_master_seed(20261103)
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng) var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
# depth 3 = hobgoblin 行集 2-3 只——两卡对照面 # depth 3 = 混编行集(熊地精 1 + 地精 1–2,最少 2 只)——两卡对照面
var open_ok: bool = settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(db.combat.hero.max_hp)}) var open_ok: bool = settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(db.combat.hero.max_hp)})
Kit.check("V8 前置:开局(L1 depth3 双敌行集)", open_ok) Kit.check("V8 前置:开局(L1 depth3 双敌行集)", open_ok)
if not open_ok: if not open_ok:
+29
View File
@@ -0,0 +1,29 @@
# 迭代 51 反哺点清单(2026-11-04)
> 实现批禁写 `design/`——以下各条由用户裁决后归文档批落文。裁决一条落一条,落完把该条清掉。
## 一、design/engineering/architecture/architecture.md(十处)
1. **:124(BattleFxHost 行)**:「A3 action_economy(queue_ap_delta/charge_layers 读)」依赖句删 charge_layers 读——蓄力层账本已退役(卡①),A3 对 action_economy 依赖余 queue_ap_delta 通道。
2. **:146(轴戊 EffectPort 句)**:「闭面五键」改「闭面四键」;「facts 取值束 dig_streak/charge_layers/rider_tier_*」删 charge_layers——biz_fact 闭面收窄为 dig_streak/hp_ratio/rider_tier_bonus/rider_tier_crit(卡①,键退役后宿主应答 null)。
3. **:125(RoundCycle 行)**:「三参直构(host/fx_host_ref/reward_grant_ref——五段构造透传不落成员)」→「引用束注入(编排器 20 键封闭束,解包自用七键后原束分发五段;持有 BattleRefreshFlow);公开方法 3→4(refresh_flow() 读出口);类变量 8→15(贴顶禁再增)」(卡②)。
4. **:123(BattleRefreshFlow 行)**:「static 两入口收 oc」→「实例束注入 8 键(facts/db/action_economy/fx_host/reward_economy/minesweeper/install_cb/grant_seg);宿主 can_refresh/player_request_refresh 单行转发形态不变,经 RoundCycle.refresh_flow() 取件;grant_seg 段间引用直接交接(编排器 _rounds._grant_seg 跨两层链式取段消灭);类变量 0→8」(卡②)。
5. **:127(settlement/ 五段段类行)**:构造形态句 →「引用束注入(各段断言各自必需键);段类对编排器零引用(\boc\b 零命中——跨件单向接线,注入 Callable 亦属接线面)」(卡②)。
6. **:122(damage_controller 行)**:类变量 12→13(+facts 构造注入);公开面净增 hero_atk()/static hits_of()/emit_hero_damage_taken 公开化(_emit 去下划线非并存)(卡②)。
7. **:121(RewardGrant 行)**:+feat_yield_adjust(_feat_yield_adjust 自编排器归位)(卡②)。
8. **:120(BattleFxHost 行)**:+mark_new_resource_type/+add_bonus_delta 两公开累积方法(类变量 15 不变,方法-only)(卡②)。
9. **核心层表 A2 minesweeper_rules 行**:公开面 19→21(贴顶超 1——**随卡② S13 停卡呈报待用户裁**:特批准记基线则登记 21 贴顶超阈值态并立还债条目;harmful_variants/harmful_adjacent 自编排器归位)(卡②)。
10. **核心层表 breakpoint_store 行+save_manager/slot_store 行**:breakpoint_store 公开方法面 5→18(13 新面+bp_read 公开化);save_manager 公开面 7→14(+set_read_only/read_only/reset_to_default/data_snapshot/restore_data+peek_summary_at/rename_overwrite 两更名公开化);slot_store「双向公开面」(+active_path;migrate_legacy_save 公开化;反向私取 21 处清零)(卡②)。
11. **:119(B1 battle_orchestrator 行)**:「NODE_TYPES 词表双份」表述删句、改「词表单点归 LevelTable(生成器 class_name 直引——S3 豁免面在案形态)」(卡③)。
## 二、GDD 确认行
- design/gdd/systems/战斗循环/战斗结算.md:**零反哺**(明细流水词表、五段段序、段边界零触碰——零行为变更批,golden 逐位对拍在案)。
- design/gdd/systems/表现与支撑/数据配置.md:**零反哺**(热重载规格保留原状——P2 卡不收件面)。
- 术语表:**零新增术语**(引用束注入/读投影/公开方法面均仓库现行词)。
- 卡④:难度曲线与Boss.md/敌人系统.md/08-Alpha版本标准.md **零动作**(文档批已随写卡段落文,本批零新增反哺)。
## 三、待办清单(实现批不碰,归收尾段/用户)
- 卡② S13 一键(minesweeper_rules 公开方法 21>20):待用户按实现报告 §六裁决;若特批准记基线,建议同批立「minesweeper_rules 公开面还债」条目归后续批。
- EffectEngine.has_effect_on 零消费方(卡① 备案):是否立死码处置条目,归用户裁。
+102
View File
@@ -0,0 +1,102 @@
# 迭代 51 实现报告(2026-11-04)
> 四卡:①EffectPort 蓄力层删键批 ②架构审查 5.1 封装修复批 ③架构审查 P2 代码面批 ④L1 深度 3 混编。
> 工作区=g-plan2-iter51(分支 iter51,基点 63fe2da0);提交序列见 git log(卡① 一笔/卡② 四笔 T1+T2、T3+T4、T5、——/卡③ 一笔/卡④ 一笔)。
> **卡② 停卡呈报在案(S13 一键待裁)**——见「六、停卡呈报」,合并等用户裁决。
## 一、各卡状态
| 卡 | 状态 | 提交 | 卡内验证读数 |
|---|---|---|---|
| ① EffectPort 蓄力层删键 | 完成 | ace15701 | effect_port_test 16 checks(含新 V4)/treasure_effects PASS/refresh_feats 85 checks;零残留 grep 达成(例外见决定记录 2);S13 零新违规 |
| ② 5.1 封装修复 | **施工完成·S13 停卡呈报待裁** | 568c474d/d43de922/0e1763bd | golden 主证改前固化改后逐位同(seed 4242,3 checks)/settle_segments 18 checks/adventure_feats 138 checks/battle_settlement 七套件绿/存档五套件绿(17+97+22+13+29)/sweep 38 checks/冒烟绿;**S13 新违规 1 条**(minesweeper_rules 公开方法 21>20 贴顶——§六) |
| ③ P2 NODE_TYPES 单点化 | 完成 | 52bb63d7 | encapsulation_p2_sweep 6 checks/node_map_determinism 21 checks(同种子逐位——零行为变更主证)/冒烟绿;S13 键零变化 |
| ④ L1 深度 3 混编 | 完成 | afaab6a7 | table_structure_guard 83 checks(v1–v6)/round_flow 136 checks(含 _smoke_enemy_buff_targets_self 种子搜索命中)/level_shop 195 checks/damage_controller 50/battle_narrator 22/config_db_test 29/config_db_resolver 13;全仓 hobgoblin 零残留(例外见决定记录 7);Alpha 零读数(静态论证) |
## 二、实际改动—验收对照表
### 卡①(对照表 3 行全绿)
| # | 需求点 | 改动 | 验收测试(落点+尾缀) | 真实入口 | 判据绑定面 | 结果 |
|---|---|---|---|---|---|---|
| 1 | 事实应答面收窄 | battle_fx_host.gd biz_fact/_fx_fact 两分支删 | effect_port_test.gd:131「V4 闭面收窄」 | begin_battle 真实装配链取宿主 biz_fact | 键在→返 0、键退役→null | 绿(16 checks 内) |
| 2 | 账本与转蓄分支退役、签名简化 | action_economy.gd finish_round_stop 纯 RAP 转存+grant_segment 调用简化 | treasure_effects_test.gd ⑥ 三断言(132/138/144 调用单参化,期望值零变) | request_stop→round_end→finish_round_stop 真实停手路径 | RAP 转存读数 4/5/3 逐位零漂移 | 绿(PASS) |
| 3 | 事件/事实两词表位退役 | effect_engine.gd EVENTS/FACTS 删位 | 静态判据+零残留 grep+S13+--code 域(合回 CI) | —(词表常量面) | 静态判据+评审注记(申报观测面局限在卡) | 绿(grep 零命中) |
### 卡②(对照表 R1–R6 全绿,全部「非行为改动」行)
| # | 需求点 | 改动 | 验收测试 | 真实入口 | 判据绑定面 | 结果 |
|---|---|---|---|---|---|---|
| R1 | 七段解耦+束交付 | 七文件重写+编排器 20 键束+三调用点 | settlement_wiring_snapshot_test〔封装5.1 V1〕+settle_segments_test 束构造/同一性断言+round_flow/seal_polarity/refresh_feats 既有全套 | begin_battle→settle_batch→player_request_refresh 全公开入口 | 同种子行流逐位同 golden+束引用同一性+\boc\b 七文件零命中 | 绿 |
| R2 | 搬家五方法归位 | damage_controller +hero_atk/hits_of/emit 公开化+facts;reward_grant +feat_yield_adjust;minesweeper +harmful_variants/harmful_adjacent;编排器五方法删 | golden V1(五方法消费路径全在行流内)+roll_multiplier_test 既有+sweep V3 | begin_battle/授予段真实结算链 | 行流逐位同+五方法宿主定义唯一 | 绿 |
| R3 | grant→BattleFxHost 累积方法 | battle_fx_host +mark_new_resource_type/+add_bonus_delta;grant 四处改址 | seal_polarity/round_flow 既有回归+sweep V6 | 授予段真实结算链 | bonus_delta 轴累积与首见判定逐位同+grant 源零私键 | 绿 |
| R4 | adventure_runner→BreakpointStore | breakpoint_store +13 公开方法(bp_read 公开化);adventure_runner 37 处改址 | save_rng_roundtrip_test 两断言既有+sweep V4 | resume_adventure/start_adventure→write_to_disk/load_from_disk | 断点续玩逐位同+源零私取 | 绿 |
| R5 | save_manager↔SlotStore 双向 | slot_store +active_path/migrate 公开化+21 处改址;save_manager +5 公开方法/2 更名+九行改址 | save_manager_domain_test+save_slot_isolation_test+sweep V5 | write_to_disk/load_from_disk/_ready 迁移口/activate_slot/peek_slot_summary | 写盘读回迁移激活窥视逐位同+双向源零私取 | 绿 |
| R6 | config_db→三表挂载 | 三 schema +bind_resolver;config_db 三行改址 | config_db_resolver_test 既有全套+sweep V7 | ConfigDB.load_tables→resolve_* | 行集解析与难度 clamp 逐位同+源零直写 | 绿 |
**零行为变更证明**:T1 golden 改前基线固化(seed 4242 未改树转储 7444 字符 JSON 填入常量)、改后全程绿=行流/快照/终局载荷/presentation 逐位同;既有套件零转红;sweep 恒零。
### 卡③(对照表 R1 全绿)
| # | 需求点 | 改动 | 验收测试 | 真实入口 | 判据绑定面 | 结果 |
|---|---|---|---|---|---|---|
| R1 | NODE_TYPES 单点化 | node_map_generator :10 删+:135 改 LevelTable.NODE_TYPES.has | encapsulation_p2_sweep_test〔架构P2 V1/V2〕+node_map_determinism_test 既有全套 | LevelRunner 关卡推进→节点图生成;LevelTable 表行加载校验 | 词表定义唯一落点(正锚计数=1)+生成链同种子逐位同 | 绿 |
### 卡④(对照表 R1–R10 全绿)
| # | 需求点 | 改动 | 验收测试(尾缀〔L1混编 vN〕) | 真实入口 | 判据绑定面 | 结果 |
|---|---|---|---|---|---|---|
| R1 | 行数与键序 | enemy.tres (1,3) 一行换两行 | table_structure_guard「敌人表 29 行且键序=覆盖表序((1,3) 两行相邻、熊地精在前)」 | ConfigDb.load_tables→EnemyTable.rows | 遍历 rows 拼四段串逐行比对 | 绿(83 checks 内) |
| R2 | 混编构成与行序 | 同上 | 同文件「(1,3) 行集恰两行、enemy_id 序列==[bugbear,goblin]」 | resolve_rowset(1,3,"battle") | 返回长度与 enemy_id 序列 | 绿 |
| R3 | 静态最坏骰和 | 同上 | 同文件「Σ(unit_count_max,固定行缺省)==3」 | 同上 | 求和值==3 | 绿 |
| R4 | 行值整行复用 | 同上 | 同文件「(1,3) 两行逐字段==(1,2)/(1,0) 行」 | EnemyTable.rows | 字段逐项比对零字面量 | 绿 |
| R5 | 零残留 | 表+七文件 | 同文件「表内 enemy_id 集合不含 hobgoblin」+T2 回读命令 | rows 遍历+grep | 集合不含+grep 空 | 绿 |
| R6 | 混编下 spawn 数全域内 | round_flow :312-317 口径逐行求和 | _smoke_encounter_roll 全深度×12 种子扫描 | begin_battle→presentation_snapshot().enemies.size() | 逐场数量∈[Σunit_count,Σunit_count_max] 且 ≥1 | 绿(136 checks 内) |
| R7 | 双池各自独立 | 表两行 | _smoke_skill_pool_data「(1,3) 两行池模板」 | 表读取 | 键与权重逐项 | 绿 |
| R8 | 注入段还原行数 | level_shop :727 | _smoke_level_graph「表回 29 行」 | 同套件注入/还原流程 | rows.size()==29 | 绿 |
| R9 | 深度 4/5 落带不变 | 表(零触碰) | table_guard「(1,4)/(1,5) battle 行集==单行 bugbear(band 6)」 | resolve_rowset(1,4/5) | 长度与 enemy_id、band 归属 | 绿 |
| R10 | 深度 3 最少 2 敌(回归护栏) | 两处夹具注释 | damage_controller_test _test_pool_consumption_per_unit+battle_narrator_test _test_snapshot_effects_per_unit 在混编行集下仍绿 | UnitContainer.spawn(1,3) | all_enemies().size()≥2 | 绿 |
## 三、S13 对账
- 改前基线:gdstyle_gate 5 键基线内、基线外 0(T1 现场读数;adventure_runner 类变量 23 照登)。
- 改后:**基线外新违规 1 条=game/core/minesweeper_rules.gd|quality/max-public-methods(21>20 贴顶)**——卡② 停卡呈报项(§六)。其余全部触件 S13 键只降不升:action_economy 删 2 成员、七段束解包全在预算内(grant 12/combat 10/verdict 5/aura 1/decay 2/round_cycle 15 贴顶/battle_refresh_flow 8)、breakpoint_store 类变量 15 贴顶方法-only、编排器 15 不变。
## 四、实机目检总清单(真人检测只登记不阻塞)
- 卡①②③:无实机目检项(死码删除/封装收口/词表单点化——零玩家可观测提交面;卡② 零行为变更由 golden 逐位对拍承载)。
- 卡④(抄体验验证清单 ⑱ L1 节点 3 三项):① 混编遭遇实机目检(L1 节点 3 遇敌=熊地精 1+地精 1–2,默认目标=熊地精);② 开战叙述首行=熊地精;③ 战斗结算面两种数量语义并存目检。读数义务:Alpha 零读数(2026-11-04 用户裁),带位移读数归数值统一修正批。
## 五、能力清单核对
game/tests/README.md:本批零编辑(四卡对照表能力行全部「既有」;测试函数名零改名——settle_segments/round_flow/level_shop/damage_controller/battle_narrator 代表用例名在册不变;卡② 新增两测试件按「经宿主链覆盖」申报=snapshot 测试挂 R1 行 begin_battle 全结算链、sweep 挂各 R 行源扫描)。名实核对一致,无回表项。
## 六、停卡呈报(卡② S13 一键——实现段零裁断,缺陷移交)
**分歧点**:minesweeper_rules.gd 公开方法数。写卡 §14 申报「18→20 贴顶(+2)」;master 基点实数 **19**(`git show master:game/core/minesweeper_rules.gd` 逐 func 清点),本批 +2 搬家方法(harmful_variants/harmful_adjacent)后 **21>20**(S13 默认阈值)——gdstyle_gate 基线外新违规 1 条。
**两种读法**:
- 读法 A(写卡考证误差 1,预算按实况校准为 19→21):+2 是卡② 五方法归位裁决的直接后果(grant_segment :87/:339 两消费方直调,两方法都必须公开,少任何一件破坏搬家合同);21 超阈值需用户特批准记基线(迭代 38 先例 c4c69701——S13 恶化经用户特批准记入基线存量红)。但 static_scan S13 头注「登记值只准随还债下降,禁抬高到现状」——特批是唯一合法通道,闸件纪律不因批内需要自动放行。
- 读法 B(卡面预算准、实现走形):不存在第三形态——拆函数降计数=实现段自行拆件(L3 ③禁);改闸件豁免=禁(卡② §13;先例迭代 38 卡② 停卡);把方法挪去 BoardData 或他件=职责面重设计(上游裁决)。
**建议案**:读法 A——用户特批准记基线 1 键(minesweeper_rules 公开方法 21),理由:+2 归位是 2026-11-03 会话裁决的机械承载、消费面决定两方法必须公开、基点考证误差 1 属写卡段缺陷(非实现超预算)、特批有迭代 38 先例。批后可开「minesweeper_rules 公开面还债」条目(归后续批:如 settle_batch/fx_reveal 族收敛)随还债降键。
**该红不阻塞的实证**:卡② 全部验收断言绿、golden 零漂移、sweep 恒零——S13 是结构预算闸不是行为闸;红项单键、修复路径已指认(特批记基线或后续还债批)。用户裁决后本迭代即可走独立验收审计→合并;若用户裁读法 B(拆面),开修复 worktree `fix-iter51-s13` 处置后重审计。
## 七、实现决定记录(代拍板白名单外备案)
1. **卡① has_effect_on 零消费方保留**:删 grant_segment 调用后 `EffectEngine.has_effect_on` 全仓零调用。卡面 :203 处置=注释改写、函数保留(T0 步骤明写「:203 句改……」未列函数删除),未代裁扩大删除面。后续可另立待办处置(纯框架 API 死码)。
2. **卡① 零残留 grep 唯一例外**:effect_port_test.gd:131 V4 断言文案与 `biz_fact("charge_layers")` 参数引证退役键名——卡面 T0 定死的断言文案(证明「键退役→null」必须引用键名),断言优先于 grep 字面。
3. **卡① 三处 swap 域描述词换代**:run_all.gd:213 manifest 注释、effect_port_test :67/:83 的「蓄力层」简称改「蓄力周转」(swap_charge_layers 在役域,词面清旧称;断言判据零变——:83 属断言文案措辞,判据 `v_c - v_0 >= 1` 不动)。
4. **卡② oc._ 计数 102 vs 卡面 105**:迭代 50 甲案删 attack_segments 分支所致(卡面预告的漂移,grep 符号锚定位,禁改判据)。
5. **卡④ 曲线护栏逐带累加**:table_structure_guard_test 第一关总量比断言原单行覆盖口径在 (1,3) 双行同带下失真(后行覆盖前行→band3 总量 12→72 比值 6.00 假红);改同带逐行累加(band3=22×1+8×1.5=34,比值 2.12)——与卡④ R6 已裁「逐行求和与 spawn 守卫同口径」同构,阈值 2.5 零变化,非放宽。期望序列文案 12/10/22/35/72→12/10/22/34/72(34=混编实况)。
6. **卡④ level_shop 白盒抬 GP**:(1,3) 混编改战斗流随机消费序→_smoke_shop_pool 种子扫描选中的场景 gp 分布漂移(进店刷新扣费后 gp=1<全架最低价)→「可购卡在架」假红。修法=③ 买段前白盒 `r8.gp += 128`(A 银段 :1345 同款先例「直改测试基建——非生产行为」),买链判据零变化;MC-1 刷新段保持原位(进店未买态语义),未动段落序。
7. **卡④ R5 零残留 grep 唯一例外**:table_structure_guard_test 新增 v5 断言文案与 `id_set.has("hobgoblin")` 引证退役键名(卡面 §八 R5 定死验收文案)。
8. **卡② 测试连带三处迁移**(写卡 §3.2 漏盘点):adventure_feats_test :791 与 level_shop_economy_test :1484 `a._bp_store._pending_next_level` → `pending_next_level()`;feat_vouchers_test :81 `_loadout_seeded = false` → `set_loadout_seeded(false)`(断点公开面在用,引用形态迁移非削弱);damage_controller_test :55 直构夹具补 BattleFacts 第五参(facts 构造注入——hero_atk 消费面,夹具空 encounter=表值缺省)。**审计订正**:level_shop 一处曾随回滚实验丢失(卡④ 提交后由独立验收审计查出,:1484 补迁移复跑 195 checks 绿——动态访问失效成员使停点分支恒假、依赖种子被扫描跳过掩盖红,属申报落实的漏项非削弱)。
9. **卡② snapshot 测试对拍形态**:GOLDEN 三引号 raw string 首尾各带换行(多行字符串语法载体),比较两侧 strip_edges 后逐位等——判据零变(内容逐位),语法载体适配。
## 八、遗留读数义务与反哺点
- 遗留读数:卡④ Alpha 零读数(归数值统一修正批);卡② S13 一键(§六待裁)。
- 反哺点:见 `reports/iter51-反哺点-2026-11-04.md`(architecture.md 十处+GDD/术语确认行)。
- 待办清单条目状态推进与 roadmap「当前位」换代:归文档批(本批禁写)。
+190
View File
@@ -0,0 +1,190 @@
# 迭代 51 独立验收审计(2026-11-04)
> 审计身份:独立会话审计员,未参与本批实现(实现由另一会话完成)。
> 工作区:D:\code\g-plan2-iter51(分支 iter51,基点 master=63fe2da0,diff 范围 63fe2da0..HEAD,HEAD=ef3cd8db,工作树 clean)。
> 审计范围:四张需求卡片(①EffectPort 蓄力层删键批 ②架构审查 5.1 封装修复批 ③架构审查 P2 代码面批 ④L1 深度 3 混编)+实现报告 §一–§八+全量 diff 逐文件核对(48 件,+1594/−474)。
> **验收档:四卡均未申报验收档——按高档全量执行**(对照表逐行核对+判据绑定面抽查+最窄测试复跑抽查+逐处削弱检查+直查例外核对+S13 对账复核)。
> 纪律:只读生产码与测试;禁变异类测试、禁撤销类取证;本报告为本审计唯一写入文件。
## 一、最窄测试复跑读数
环境:Godot 4.7.2 console 版(D:\code\godot\Godot_v4.7.2-stable_win64_console.exe)headless,工作目录 game/,`--script` 单件直跑;跑前确认 `game/.godot/global_script_class_cache.cfg` 在位(直接跑)。退出码 0=绿。
| 套件 | 归属 | 退出码 | 读数 |
|---|---|---|---|
| effect_port_test | 卡① V4(五件套) | 0 | all green(16 checks,V4 断言在列全 PASS) |
| settlement_wiring_snapshot_test | 卡② R1 golden 主证(五件套) | 0 | all green(3 checks,golden 逐位同) |
| encapsulation_sweep_test | 卡② V2–V7(五件套) | 0 | all green(38 checks) |
| encapsulation_p2_sweep_test | 卡③ V1/V2(五件套) | 0 | all green(6 checks) |
| table_structure_guard_test | 卡④ v1–v6(五件套) | 0 | all green(83 checks,v1–v6 六条全 PASS) |
| round_flow_skill_pool_test | 审计自选(卡④ R6/R7/R10) | 0 | all green(136 checks,含 buff_targets_self 种子搜索命中 seed=1) |
| save_rng_roundtrip_test | 审计自选(卡② R4) | 0 | all green(17 checks) |
| treasure_effects_test | 卡① 行 2 | 0 | PASS |
| settle_segments_test | 卡② R1 | 0 | all green(18 checks) |
| adventure_feats_test | 卡② R2 | 0 | all green(138 checks) |
| seal_polarity_test | 卡② R3 | 0 | all green(70 checks) |
| refresh_feats_test | 卡① 契约 2/卡② R1 | 0 | all green(85 checks) |
| roll_multiplier_test | 卡② R2 | 0 | all green(42 checks) |
| config_db_resolver_test | 卡② R6/卡④ | 0 | all green(13 checks) |
| config_db_test | 卡④ | 0 | all green(29 checks) |
| save_manager_domain_test | 卡② R5 | 0 | all green(97 checks) |
| save_slot_isolation_test | 卡② R5 | 0 | all green(22 checks) |
| feat_vouchers_test | 卡② 连带迁移 | 0 | all green(49 checks) |
| damage_controller_test | 卡④ R10 | 0 | all green(50 checks) |
| battle_narrator_test | 卡④ R10 | 0 | all green(22 checks) |
| level_shop_economy_test | 卡④ R8 | 0 | all green(195 checks) |
| node_map_determinism_test | 卡③ 零行为主证 | 0 | all green(21 checks) |
## 二、卡① EffectPort 蓄力层删键批
### 对照表逐行核对(3 行)
| 行 | 验收测试存在性 | 尾缀 grep | 真实入口判定 | 结果 |
|---|---|---|---|---|
| 1 | effect_port_test.gd:131 断言在(V3 组 `_test_biz_fact_face_closed` 末尾) | 「V4 闭面收窄:charge_layers 键退役——宿主事实应答 null」grep 命中 :131 | begin_battle 真实装配链(:102 `orch.begin_battle(_enc([]))` 受理后 :131 取 `orch.fx_host().biz_fact("charge_layers")`——fx_host() 公开访问器+biz_fact 公开方法,未绕过装配链直调下层) | 通过 |
| 2 | treasure_effects_test 三断言在,期望值 rap==4/5/3 逐字未动(diff 仅调用实参行) | 无新增尾缀(既有断言回归锚,卡面即如此定) | request_stop→finish_round_stop 真实停手路径;直构 ActionEconomy 夹具=卡面 §8 测试方式两分法明示的调试接口验证形态,且卡面 §7 T0 逐字定死该三行改单参——非绕过 | 通过 |
| 3 | 静态判据:effect_engine.gd EVENTS `charge_full` 位、FACTS `charge_layers` 位删除(diff 证实 :14/:59 两处删位) | —(词表常量面,卡面明示无独立运行期提交面) | —(静态判据+评审注记,卡面申报观测面局限在案) | 通过 |
零残留复核:`grep -rn "charge_layers|charge_full|CHARGE_LAYER|蓄力层|转蓄力|厚积薄发|铁壁|整备军备" game/ --include=*.gd --include=*.tres` 全部命中仅在 swap_charge_layers 在役域(feats.tres :1018-1056 六行、refresh_feats_test `test_charge_layers_consumed_same_round` 函数名与其断言、effect_port_test :16 注释)+V4 断言键名引证(effect_port_test :131)——前者=卡面 §1 完成定义明写豁免域,后者=报告决定记录 2 申报的唯一例外(证明「键退役→null」必须引用键名)。达卡面 T1 零残留判据。
### 削弱检查(逐处)
| diff 处 | 申报出处 | 结论 |
|---|---|---|
| treasure_effects_test 三处 finish_round_stop(false,N)→(N)(:132/:138/:144) | 卡① §8 测试削弱申报 | 通过——断言期望值与检查语义逐字零变化 |
| effect_port_test :57 注释+:83 断言文案「蓄力层→蓄力周转」 | 卡① §8 申报(V1 两处文案) | 通过——判据 `v_c - v_0 >= 1` 不动 |
| effect_port_test :16/:103 误标注释改写 | 卡① §7 T1 申报(三处注释) | 通过——注释级 |
| run_all.gd:213 manifest 注释「蓄力层→蓄力周转」 | 报告决定记录 3 | 通过——注释级,判据零变 |
| has_effect_on 函数保留(grant_segment 调用删后全仓零调用方) | 报告决定记录 1(卡面 T0 :203 只列注释改写、未列函数删除) | 通过——未代裁扩大删除面,与卡面一致 |
无未申报删改。
### 直查例外核对
对照表三行均未声明「内部字段直查=受控例外」:行 1 走公开面(fx_host()/biz_fact);行 2 直构夹具=卡面 §8 两分法明示形态;行 3 静态面。无需例外注记,判定=通过。
## 三、卡② 架构审查 5.1 封装修复批
### 对照表逐行核对(R1–R6)
| 行 | 验收测试存在性 | 尾缀 grep | 真实入口判定 | 结果 |
|---|---|---|---|---|
| R1 | settlement_wiring_snapshot_test.gd:456 主对拍在(GOLDEN 常量 :22-416 固化,seed 4242;dump 通道 :443-448);settle_segments_test :30-34 束构造 5 条+:38-44 同一性 6 条在 | 「封装5.1 V1」:4/:456 命中 | begin_battle→minesweeper.settle_batch→player_request_refresh 全公开入口;段类消费全经真实编排器接线(settle_segments 直构束=卡面 §8 申报 1 的适配形态) | 通过 |
| R2 | golden V1 覆盖五方法消费路径(主角攻击 base_value/奖励乘区/terrain_kind 事实/hero_atk override 行均在 golden 行流内);roll_multiplier_test 既有回归绿;sweep V3 编排器五方法定义零命中 | sweep :69-70 五方法名零命中断言(〔封装5.1〕尾缀 :36/:42 模板命中 38 处) | begin_battle spawn 经 `_damage.hero_atk()`(battle_orchestrator.gd diff :285 证实改指),未绕过 | 通过 |
| R3 | seal_polarity_test 70 checks/round_flow 136 checks 绿+sweep V6(grant 两私键零命中+mark_new_resource_type 正锚 :100-101) | 〔封装5.1〕尾缀在案 | 授予段真实结算链(两套件全走 begin_battle) | 通过 |
| R4 | save_rng_roundtrip_test diff 零改动(未触碰)、17 checks 绿;sweep V4(adventure_runner `_bp_store\._` 零命中+`_bp_store\.bp_read\(` 正锚 :78-79) | 〔封装5.1〕尾缀在案 | AdventureRunner.resume_adventure/start_adventure→write_to_disk/load_from_disk(integration 件真实入口) | 通过 |
| R5 | save_manager_domain_test 97/save_slot_isolation_test 22 绿;sweep V5(save_manager `slots\._` 零命中+active_path 正锚;slot_store `_save\._` 零命中+七公开面正锚 :84-92) | 〔封装5.1〕尾缀在案 | write_to_disk/load_from_disk/_ready 迁移口/activate_slot/peek_slot_summary(两套件承载) | 通过 |
| R6 | config_db_resolver_test 13 checks 绿(rowset 保序/行类路由/难度 clamp);sweep V7(config_db `\._resolver\s*=` 零命中+`bind_resolver\(_resolver\)` 正锚 :109-110) | 〔封装5.1〕尾缀在案 | ConfigDB.load_tables→resolve_*(三 schema bind_resolver 类型环当轮真实跑通——T4 义务承载) | 通过 |
零行为变更主证复核:golden 对拍断言绿(本审计复跑);段类对编排器引用归零由 sweep V2 七文件 `\boc\b` 零命中承载;束 20 键组装与五方法删除、RoundCycle/BattleRefreshFlow 形态换代在 diff 逐文件证实。
### 削弱检查(逐处)
| diff 处 | 申报出处 | 结论 |
|---|---|---|
| settle_segments_test :20-24 五段直构断言改束构造成功(:30-34) | 卡② §8 申报 1 | 通过——段类可独立实例化断言意图保持 |
| settle_segments_test :28-34 宿主回指 6 条改注入同一性(:38-44——round_cycle.facts/grant.facts/aura 回调对象 get_object()==oc/combat.fx_host/decay.status_effects/verdict.units) | 卡② §8 申报 2 | 通过——6 条全数对上,强度对应回指 |
| adventure_feats_test :671/:691/:727 三处 orch._hits_of→DamageController.hits_of | 卡② §8 申报 3 | 通过——断言内容(attack_bonus 命中过滤)零改动 |
| adventure_feats_test :791 `a._bp_store._pending_next_level`→`pending_next_level()` | 报告 §七 8 | 落地证实(grep 现值为公开方法调用) |
| feat_vouchers_test :81 `_loadout_seeded = false`→`set_loadout_seeded(false)` | 报告 §七 8 | 落地证实 |
| damage_controller_test :56 直构夹具补 BattleFacts 第五参 | 报告 §七 8 | 通过——夹具空 encounter=表值缺省,断言零改动 |
| snapshot 测试 strip_edges 对拍(:449-451) | 报告 §七 9 | 通过——内容逐位判据零变,三引号语法载体适配 |
| **level_shop_economy_test :1484 `a._bp_store._pending_next_level`——报告 §七 8 与卡④ commit信息均申报为已迁移,实际未改** | —— | **申报失实,见第六节;非削弱(该行零改动=零删改)** |
无未申报删改、无阈值放宽、无扫描面缩小。
### 直查例外核对
settle_segments_test 直查 `oc._facts`/`oc._rounds._grant_seg.*` 等编排器内部(同一性断言)=卡② §5 明示受控例外(「sim/probe/tests 的测量面直构惯例与测试受控例外——零清扫:orch._facts/orch._rounds/orch._emit_row 等测试直连维持现状」)。报告 §七 8 三处测试直连迁移=附加申报(其中一处未落地,见第六节,且该处属同一受控例外面、运行态零差异)。例外均有注记,判定=通过。
## 四、卡③ 架构审查 P2 代码面批
### 对照表逐行核对(R1)
| 行 | 验收测试存在性 | 尾缀 grep | 真实入口判定 | 结果 |
|---|---|---|---|---|
| R1 | encapsulation_p2_sweep_test V1(:40-41 生成器 `^const NODE_TYPES` 零命中+`LevelTable\.NODE_TYPES\.has\(` 正锚)/V2(:56-58 生产三目录 `NODE_TYPES` 定义恰 1 处且落 data/schemas/level_table.gd);node_map_determinism_test 既有全套 21 checks 绿 | 「〔架构P2〕」尾缀 :27/:32/:56/:58 命中 | 源扫描(静态)+LevelRunner 关卡推进生成链(determinism 同种子逐位——零行为变更主证) | 通过 |
生产面独立复核:`grep -rnE "NODE_TYPES\s*:?\s*=" core data presentation` 唯一命中 data/schemas/level_table.gd:11;node_map_generator.gd 本地 const 删、:135 改 `LevelTable.NODE_TYPES.has(...)`(diff 证实);S3 schema class_name 直引豁免面形态(卡面 §3 在案)。
### 削弱检查
卡面 §8 申报零条;diff 中本卡测试改动仅新增 sweep 件+run_all manifest +1 行——零删改既有断言。通过。
### 直查例外核对
纯源扫描与既有套件回归,无内部字段直查。通过。
## 五、卡④ L1 深度 3 混编
### 对照表逐行核对(R1–R10)
| 行 | 验收测试存在性 | 尾缀 grep | 真实入口判定 | 结果 |
|---|---|---|---|---|
| R1 | table_structure_guard_test :232 v1(29 行键序=(1,3) 两行相邻、熊地精在前) | 〔L1混编 v1〕:232 命中 | ConfigDb.load_tables→EnemyTable.rows 遍历拼四段串逐行比对 | 通过 |
| R2 | :242 v2(行集恰两行、enemy_id 序列==[bugbear,goblin]) | 〔L1混编 v2〕:242 命中 | `resolve_rowset(1,3,"battle")` 返回序列——判据绑返回长度与 enemy_id 序列(被测数据改动特有提交面) | 通过 |
| R3 | :244 v3(Σ(unit_count_max,固定行缺省)==3) | 〔L1混编 v3〕:244 命中 | 同上求和值==3 | 通过 |
| R4 | :258 v4(两行逐字段==(1,2)/(1,0) 行,零字面量) | 〔L1混编 v4〕:258 命中 | EnemyTable.rows 字段逐项比对 | 通过 |
| R5 | :263 v5(表内 enemy_id 集合不含 hobgoblin)+T2 回读命令 | 〔L1混编 v5〕:263 命中 | rows 遍历+grep | 通过 |
| R6 | round_flow :320 全域扫描逐行求和(72 场全绿) | 断言文案「逐行求和」:320 命中 | begin_battle→presentation_snapshot().enemies.size() | 通过 |
| R7 | round_flow :575-576 池模板两键(bugbear 6/2/2+goblin 9/1/1,顺序按表行序) | :575-576 两键在案 | 表读取逐键逐权重比对 | 通过 |
| R8 | level_shop :727「表回 29 行」 | :727 断言在案 | 同套件既有注入/还原流程(rows.size()==29) | 通过 |
| R9 | :270 v6((1,4)/(1,5) battle 行集==单行 bugbear(band 6)) | 〔L1混编 v6〕:270 命中 | resolve_rowset(1,4/5,"battle") | 通过 |
| R10 | damage_controller_test _test_pool_consumption_per_unit/battle_narrator_test :114-115 _test_snapshot_effects_per_unit 混编行集下绿(50/22 checks) | —(回归护栏行,卡面即不设新尾缀) | UnitContainer.spawn(1,3)→all_enemies().size()≥2 | 通过 |
行值整行复用独立复核:enemy.tres (1,3) bugbear 行(:82-104)与 (1,2) 行(:58-80)逐字段一致(hp22/atk1/atk 0-2/池 attack6-charge2-heavy2/normal/light/ac2/unit_count 1 无 unit_count_max);(1,3) goblin 行(:106-129)与 (1,0) 行(:8-31)逐字段一致(hp8/池 attack9-lunge1-desperate_guard1/normal/light/ac0/unit_count 1-2)。行序 bugbear 在前、两行相邻。表注四处处置(实体名修正段删/首批投放段尾追加/N1 括注保留/缺口句换新注)diff 逐处证实。零残留复核:全仓 `hobgoblin` grep(data/core/presentation/probe/sim/tests/tools)唯一命中=v5 断言自身键名引证(table_structure_guard_test :259 注释+:263 断言)=报告决定记录 7 申报的唯一例外。
### 削弱检查(逐处)
| diff 处 | 申报出处 | 结论 |
|---|---|---|
| table_guard :234-241 hobgoblin 留任断言删(原〔fv12〕) | 卡④ §十二 申报① | 通过——钉已退役事实;原位换 v2–v6 五条新断言,覆盖强度净增 |
| round_flow :312-317 首行区间改逐行求和 | 卡④ §十二 申报② | 通过——与 unit_container spawn 守卫同口径,72 场全绿,双行行集下非放宽(旧口径必红) |
| round_flow :615 文案二十八行九种→二十九行十种 | 卡④ §十二 申报③ | 通过——只改文案不改判据 |
| static_scan.sh 删 FNR==891 豁免(:145 头注+:222 执行点) | 卡④ §十二 申报④ | 通过——对象行已随表注段删除,留置会误放别行 |
| 曲线护栏单行覆盖口径改逐带累加(期望序列 12/10/22/35/72→12/10/22/34/72,阈值 2.5 不动) | 报告 §七 5 | 通过——(1,3) 双行同带下旧「后行覆盖前行」口径失真(band3 总量 12→72 比值假红),新口径=同带逐行累加、与 R6 逐行求和同构;阈值零变化 |
| level_shop :1314-1324 白盒抬 GP 128+文案改 | 报告 §七 6 | 通过——测试基建直改(A 银段 :1345 同款先例在案),买链判据零变化 |
| config_db_resolver_test 合成行 hobgoblin→giant_spider(:44,三条期望串同步) | 卡④ §七 T2 明写(并明写不换 bugbear 的理由) | 通过——夹具级,断言结构零变 |
| config_db_test :222 「不相邻」样本换 giant_spider | 卡④ §七 T2 明写 | 通过——同上 |
| damage_controller_test :414/battle_narrator_test :114 两处夹具注释改述 | 卡④ §七 T2 明写(断言零改动) | 通过 |
| doc_term_audit.py :144 注释敌种枚举去 hobgoblin | 卡④ §七 T2 明写 | 通过——注释级 |
无未申报删改、无阈值放宽、无未申报扫描面缩小。
### 直查例外核对
卡④ 判据全部绑公开面(rows/resolve_rowset 返回序列/presentation_snapshot/UnitContainer.spawn)——无需例外注记。通过。
## 六、申报失实项(非验收缺漏,需订正)
**实现报告 §七 第 8 条与卡④ commit afaab6a7 提交信息均申报「卡②断点公开面三处测试直连迁移(pending_next_level×2/loadout_seeded)」,实际落地两处:**
- adventure_feats_test :791 `a._bp_store.pending_next_level()`——已落地;
- feat_vouchers_test :81 `a._bp_store.set_loadout_seeded(false)`——已落地;
- level_shop_economy_test :1484 `if a._bp_store._pending_next_level:`——**未改**(该文件 diff 全量核对仅 :724-727 与 :1314-1324 两处触碰)。
定性与影响:非验收缺漏——该行零改动=无断言删改、无削弱;测试侧 `_bp_store._` 直查属卡② §5 明示受控例外面;BreakpointStore `_pending_next_level` 成员保留(breakpoint_store.gd:60),运行态与改前逐位同,套件全绿(level_shop 195 checks)。但实现报告与提交信息的该条申报与实况不符,构成产出物事实错误——建议订正实现报告 §七 第 8 条(改为「两处落地、level_shop 一处未迁」);commit 信息不可改,以本节记录在案。
## 七、S13 对账复核
实测 `python tools/gdstyle_gate.py`(工作目录 game/,退出码 1):
```
[gdstyle_gate] 扫描面 158 个 .gd;命中 6 条,基线内 5 条,基线外新违规 1 条,较登记值恶化 0 条
FAIL 新增 game/core/minesweeper_rules.gd|quality/max-public-methods
[gdstyle_gate] FAIL — 基线外新违规 1 条/恶化 0 条——新码须修到基线口径内,恶化须还债或经用户批准随批重生成基线
```
与实现报告 §六 逐字一致。独立考证:基点 63fe2da0 的 minesweeper_rules.gd 公开方法实数 19(`git show 63fe2da0:game/core/minesweeper_rules.gd` 逐 func 清点);HEAD 21(含新增 harmful_variants/harmful_adjacent 两搬家方法,diff :58/:70 证实)→ 21>20(S13 默认阈值)。其余触件只降不升(action_economy 类变量 14→12 实测;gdstyle 无第二键恶化)。
该项为实现报告 §六已停卡呈报待用户裁的结构闸项(写卡考证 18 失实、基点实数 19、+2 搬家方法),**不属于验收闭合缺陷**——本审计如实记录其状态与呈报位置,不计入审计终判。**合并前置条件=用户对该键的裁决**(特批准记基线随还债降键,或裁实现报告 §六读法 B 开 fix-iter51-s13 修复批后重审计)。
## 八、终判
**通过**——四卡对照表全部行(卡① 3 行/卡② R1–R6/卡③ R1/卡④ R1–R10)验收断言真实存在、尾缀可 grep 定位、真实入口未绕过被测改动点、判据绑被测改动特有的可观测提交面;最窄复跑五件套+审计自选两件+对照表引用全部其余套件绿(22 件全 0 退出码);测试削弱全部落在四卡申报清单与实现报告决定记录内,零未申报删改;直查例外均有注记;卡① 蓄力层族与卡④ hobgoblin 零残留 grep 达成且例外均为申报例外。
记录在案三项(均不改变终判):
1. **S13 一键**(minesweeper_rules 公开方法 21>20)——停卡呈报在案(实现报告 §六)。**合并前置条件=用户对该键的裁决**;裁决后按既定通道走。
2. **申报失实一项**——实现报告 §七 第 8 条「三处测试直连迁移」实际落地两处(level_shop_economy_test :1484 未迁),建议订正报告;非验收缺漏、非测试削弱。
3. 卡② golden 改前固化读数以 GOLDEN 常量在案(settlement_wiring_snapshot_test.gd:22-416,seed 4242),改后对拍绿——对拍读数在案义务已履行。
+1 -1
View File
@@ -141,7 +141,7 @@ TERM_CONTEXT_EXEMPT = {
"雾格": re.compile(r"不做清单|战争迷雾类机制"), "雾格": re.compile(r"不做清单|战争迷雾类机制"),
# heavy_multiplier 字段与 ambusher 敌人行同于 2026-09-20 八型批整体退役(enemy_table.gd # heavy_multiplier 字段与 ambusher 敌人行同于 2026-09-20 八型批整体退役(enemy_table.gd
# 与 enemy.tres 均零命中);battle.md「敌人意图线索映射目录」是 2026-09-12 成文占位表, # 与 enemy.tres 均零命中);battle.md「敌人意图线索映射目录」是 2026-09-12 成文占位表,
# 现行敌人为 goblin/hobgoblin/bugbear/giant_spider/drider/duergar 系,占位行里的退役 # 现行敌人为 goblin/bugbear/giant_spider/drider/duergar 系,占位行里的退役
# 敌人 id 只能读作历史留档(真正的现行规格在 敌人系统.md「目标顺位与意图线索」节)。 # 敌人 id 只能读作历史留档(真正的现行规格在 敌人系统.md「目标顺位与意图线索」节)。
"伏击": re.compile(r"ambusher|成文占位"), "伏击": re.compile(r"ambusher|成文占位"),
"heavy_multiplier|重击倍率": re.compile(r"ambusher|heavy_multiplier|成文占位"), "heavy_multiplier|重击倍率": re.compile(r"ambusher|heavy_multiplier|成文占位"),
+1
View File
@@ -8,5 +8,6 @@
game/core/adventure_runner.gd|quality/max-class-variables = 23 game/core/adventure_runner.gd|quality/max-class-variables = 23
game/core/level_runner.gd|quality/max-class-variables = 30 game/core/level_runner.gd|quality/max-class-variables = 30
game/core/level_runner.gd|quality/max-public-methods = 33 game/core/level_runner.gd|quality/max-public-methods = 33
game/core/minesweeper_rules.gd|quality/max-public-methods = 21
game/presentation/views/board_view.gd|quality/max-class-variables = 31 game/presentation/views/board_view.gd|quality/max-class-variables = 31
game/presentation/views/board_view.gd|quality/max-public-methods = 27 game/presentation/views/board_view.gd|quality/max-public-methods = 27