From 3465b932ce11bb2a89ec7f5fa74ad3ce0377b0fd Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 30 Oct 2026 06:11:49 +0800 Subject: [PATCH] =?UTF-8?q?iter44-=E5=8D=A12=EF=BC=9A=E5=8D=A1=E2=91=A1(?= =?UTF-8?q?=E6=88=98=E6=96=97=E7=BC=96=E6=8E=92=E5=BD=92=E7=BA=A6=E6=89=B9?= =?UTF-8?q?):=20RoundCycle+BattleFacts=E4=B8=A4=E6=96=B0=E4=BB=B6=C2=B7?= =?UTF-8?q?=E5=AE=BF=E4=B8=BB38=E2=86=9215=C2=B7treasure=5Finventory?= =?UTF-8?q?=E7=BB=8Freward=5Fgrant()=E5=87=BA=E5=8F=A3=E7=9B=B4=E6=B3=A8?= =?UTF-8?q?=C2=B7=5Fhero=5Fatk=E6=B4=BE=E7=94=9F=E8=AF=BB=C2=B7=E5=AE=8C?= =?UTF-8?q?=E7=BE=8E=E8=B4=A6=E4=B8=89mark=E5=86=99=E5=8F=A3=C2=B7behavior?= =?UTF-8?q?=5Froll=E6=B5=81=E9=9A=8F=E4=BB=B6=E8=BF=81=E7=A7=BB=E5=8F=96?= =?UTF-8?q?=E5=8F=B7=E5=BA=8F=E9=80=90=E5=AD=97=C2=B7=E5=AF=B9=E7=85=A7?= =?UTF-8?q?=E8=A1=A8V1-V5=E6=96=B0=E5=A2=9E=E6=96=AD=E8=A8=80=E5=86=85?= =?UTF-8?q?=E5=B5=8C=C2=B7=E9=94=AE=E8=B4=A615/15/8=C2=B7811=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/battle_facts.gd | 48 +++ game/core/battle_facts.gd.uid | 1 + game/core/battle_orchestrator.gd | 281 ++++++------------ game/core/level_runner.gd | 2 +- game/core/settlement/combat_segment.gd | 4 +- game/core/settlement/grant_segment.gd | 42 +-- game/core/settlement/round_cycle.gd | 106 +++++++ game/core/settlement/round_cycle.gd.uid | 1 + game/tests/static_scan.sh | 4 +- .../battle_settlement/roll_multiplier_test.gd | 22 ++ .../round_flow_skill_pool_test.gd | 9 +- .../battle_settlement/seal_polarity_test.gd | 12 +- .../battle_settlement/trigger_cond_test.gd | 6 +- game/tests/unit/core/adventure_feats_test.gd | 2 +- game/tests/unit/core/feat_vouchers_test.gd | 22 +- .../unit/core/meta_progression_forge_test.gd | 8 +- game/tests/unit/core/settle_segments_test.gd | 15 +- game/tests/unit/core/treasure_effects_test.gd | 21 +- 18 files changed, 346 insertions(+), 260 deletions(-) create mode 100644 game/core/battle_facts.gd create mode 100644 game/core/battle_facts.gd.uid create mode 100644 game/core/settlement/round_cycle.gd create mode 100644 game/core/settlement/round_cycle.gd.uid diff --git a/game/core/battle_facts.gd b/game/core/battle_facts.gd new file mode 100644 index 00000000..5a4d8b7e --- /dev/null +++ b/game/core/battle_facts.gd @@ -0,0 +1,48 @@ +## 战斗事实件:战斗事实的唯一记录面——本战受理事实(encounter:打什么、带什么进来)、 +## 同调装配事实(attunement)、轮 facts 缓冲(reveals/seals/path)、封印判定账(去重两集+完美三账)、 +## 装板溯源(seq/provenance)、条件事实计数(refresh_used、feat_first_dig_used、attune_safe_seen)。 +## 零注入引用、零信号、零协作对象调用——纯数据+谓词+写口;缓冲写入点在宿主 A2 处理器, +## 身份快照捕获留宿主(fact 件不持协作引用,防它变成第二编排面)。 +## 完美账写口单点=三 mark 方法(契约 2:perfect_hit_false 保持纯净,豁免只累加 perfect_forgiven); +## 其余成员公有直读写(段类/快照/宿主经 oc._facts.X——fx_host._fx_bonus_delta 跨件直读写同款形态)。 +## reset_round 只清 reveals/seals/path 三项、不碰 refresh_used(_battle_over 后快照读到的 +## refresh_used 必须保持末轮值)。 +## 成员冻结:15/15 贴顶——实现期禁增员;扩容阀=先把封印判定账五成员二次拆出(SealLedger)再落新账。 +class_name BattleFacts +extends RefCounted + +var encounter: Dictionary[String, Variant] = {} # 本战受理事实(begin_battle assign 后全生命期只读) +var reveals: Array[Dictionary] = [] # 轮 facts 缓冲(cell_revealed 批) +var seals: Array[Dictionary] = [] # 封印判定批缓冲(seal_judged 批) +var path := "" # 轮末路径事实(round_end 载荷 path_tag) +var refresh_used := 0 # 本轮刷新计数(复位点=出口分发,语句随迁) +var board_seq := 0 # 板序号(本战斗第几张板——快照归因用) +var board_from_midround := false # 装板 provenance:轮内刷新换来的图=刷新图(清图奖励不发) +var seal_consumed := {} # 封印兑付资格账(危险格实例为键;install 重置) +var seal_miss_seen := {} # 封印未中行去重(每安全格实例一次) +var perfect_hit_true := 0 # 整图完美封印计数(install 重置) +var perfect_hit_false := 0 # 完美误标计数(纯净禁回退改写——契约 2) +var perfect_forgiven := 0 # 完美保险豁免累计(单向累加自有账) +var feat_first_dig_used := false # 条件事实计数(魔矢每战首次) +var attunement: Array[Variant] = [] # 本战装配集快照(enc.attunement 受理携带;受理后只读) +var attune_safe_seen := 0 # 积点制件安全开格计数(装配集自动兑付进度) + + +# 轮复位(原宿主 _reset_round_facts 逐字——三调用点:begin_battle 首板 install 后/出口分发/终局序列) +func reset_round() -> void: + reveals.clear() + seals.clear() + path = "" + + +# 完美封印三账写口(授予段直写升级为账本单点——契约 2 执行点) +func mark_perfect_hit() -> void: + perfect_hit_true += 1 + + +func mark_perfect_miss() -> void: + perfect_hit_false += 1 + + +func mark_perfect_forgiven() -> void: + perfect_forgiven += 1 diff --git a/game/core/battle_facts.gd.uid b/game/core/battle_facts.gd.uid new file mode 100644 index 00000000..2ed94de1 --- /dev/null +++ b/game/core/battle_facts.gd.uid @@ -0,0 +1 @@ +uid://bo0eolh0vmvgb diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 138fe7e3..94b29869 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -3,7 +3,7 @@ ## 效果引擎施加面归 BattleFxHost、奖励授予管线归 RewardGrant(两拆件,经组件出口 fx_host()/ ## reward_grant() 取件,禁链式穿透其内部);本类保留宿主职责面=状态机与生命周期、轮 facts 缓冲、 ## A2 三事件订阅、五段驱动、attune 族、行发射基建(_emit_row/_assert_row_source/_fatal)、呈现投影、刷新面。 -## 零 await;随机只经 rng 封闭流(encounter/behavior_roll/armor_roll/gp_drop)——同种子同输入逐位复现。 +## 零 await;随机只经 rng 封闭流(_facts.encounter/behavior_roll/armor_roll/gp_drop)——同种子同输入逐位复现。 ## 呈现层读取唯一出口 = presentation_snapshot()/diagnostic_snapshot(),禁链式穿透内部协作对象。 ## 伤害只向 DamageController 开单(何时开单归编排、怎么算归控制器);刷新唯一通道 player_request_refresh,禁 RAP 满自动刷。 class_name BattleOrchestrator @@ -34,6 +34,8 @@ const SOURCE_EXEMPT_TYPES := ["decay", "verdict"] # fx_grant_gp=专长·GP 入账(钱包面而非棋盘面,行无格子可指) const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp"] +var _facts: BattleFacts # 战斗事实件(battle_facts.gd——事实族 15 成员单点) +var _rounds: RoundCycle # 轮循环驱动件(settlement/round_cycle.gd——五段持有/段序/重入闸/摇号) var db: ConfigDb var rng: RngServiceCore # E1 封装收口:协作对象 enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/diagnostic_snapshot() 成文出口。 @@ -45,60 +47,43 @@ var action_economy: ActionEconomy var _enemy_system: UnitContainer var _status_effects: StatusContainer var _reward_economy: RewardEconomy -var treasure_inventory: TreasureItems = null: - set(v): - treasure_inventory = v - if _reward_grant != null: - _reward_grant.treasure_inventory = v # 库存注入转发(注入时序在宿主构造之后——外部赋值路径零改址) var _damage: DamageController # 伤害控制器(单据唯一兑付管线) var _fx_host: BattleFxHost # 效果引擎宿主件(fx 族持有与兑付——拆件定稿轴件一) var _reward_grant: RewardGrant # 奖励授予件(授予管线与秘宝使用——拆件定稿轴件二) -var encounter: Dictionary[String, Variant] = {} var round_index := 0 var battle_result := "" # "" / "victory" / "defeat" / "fatal_error" -var _facts_reveals: Array[Dictionary] = [] -var _facts_seals: Array[Dictionary] = [] -var _facts_path := "" -var _settling := false # 结算五段 Segment(段边界=审计五段,段序骨架冻结于 # SettlementTable.segment_order 语义:授予→增益/减益→战斗→衰减→判定。本类做段序编排+ # 行发射/开单原语单点——段体逻辑归 game/core/settlement/ 五类) -var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点) -var _aura_seg = null # AuraSegment(②增益/减益段) -var _combat_seg = null # CombatSegment(③战斗段) -var _decay_seg = null # DecaySegment(④衰减段) -var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归本类) -var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round) -var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用) -var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励) -var _seal_consumed := {} # 兑付资格以危险格实例为键(整图;换图重置) -# 同调装配运行态(begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动) -var attunement: Array[Variant] = [] # 装备中同调件 item_id 集 -var _mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(触发条件事实面——本场累计/上轮转存/当轮累计三读数合一账) -# 转存时点=_roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle -var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(装配集 ap_charge_safe 行扫描;满 charge_n 自动兑付当轮 AP+1,无玩家发起入口) +# 同调装配运行态(begin_battle 经 enc 携带 attunement 键注入 facts;off=空集全部效果零扰动) +# 转存时点=roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle var nest_preview_count := -1 # 巢穴预告读数口(board_installed 命中落值;-1=未装配/未发射;呈现桥消费) # 特性运行态(relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经 # 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动) -var _feat_first_dig_used := false -# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter.assign 后全生命期可读; +# 主角攻击基值派生读(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 后全生命期可读; # null=调试/单场直构面,账面消费跳过。领域拆分批成员删除后的引用承接,读面即引用、无第二账) func _journal() -> GainJournal: - return encounter.get("journal", null) if encounter.get("journal", null) is GainJournal else null + return _facts.encounter.get("journal", null) if _facts.encounter.get("journal", null) is GainJournal else null # 同调配装件读面(begin_battle 经 enc["loadout"] 派生——充能单源化的 reset_cycle/consume_charge # 消费面;null=直构零装配,充能面跳过) func _attune_loadout() -> AttuneLoadout: - return encounter.get("loadout", null) + return _facts.encounter.get("loadout", null) # 授予件战斗域回调束(bind_battle 注入——键集封闭三键:充能消耗/真视计数/负向踩雷计数; -# 宿主侧各转 _attune_loadout/_journal/_mines_hits 单点,授予件对账本零强引用) +# 宿主侧各转 _attune_loadout/_journal/_rounds.mines_hits 单点,授予件对账本零强引用) func _battle_hooks() -> Dictionary[Variant, Variant]: return { "attune_consume": _hook_attune_consume, @@ -108,8 +93,8 @@ func _battle_hooks() -> Dictionary[Variant, Variant]: func _hook_attune_consume(item_id: String) -> void: - if _attune_loadout() != null and treasure_inventory != null: - _attune_loadout().consume_charge(item_id, treasure_inventory) + if _attune_loadout() != null and _reward_grant.treasure_inventory != null: + _attune_loadout().consume_charge(item_id, _reward_grant.treasure_inventory) func _hook_vision_add() -> void: @@ -120,8 +105,8 @@ func _hook_vision_add() -> void: # 负向 blast(雷)踩雷计数入账(触发条件事实面——正向 spark_aoe/清图奖励不计入; # 极性判定在授予件 _flat_blast 传点,本口只记账) func _book_negative_blast() -> void: - _mines_hits["round"] += 1 - _mines_hits["total"] += 1 + _rounds.mines_hits["round"] += 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) @@ -136,10 +121,10 @@ func _fx_host_fact(fact: String) -> Variant: "round_index": return round_index "battle_index": - return int(encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面) + return int(_facts.encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面) "seals_count": var hits_n := 0 - for fevt in _facts_seals: + for fevt in _facts.seals: if bool(fevt.get("hit", false)): hits_n += 1 return hits_n @@ -151,7 +136,7 @@ func _fx_host_fact(fact: String) -> Variant: # attune 积点读投影(RewardGrant attune_safe_seen_cb 注入面——use_treasure 拒绝文案读数;经读投影出户防组件回调宿主私有族) func _attune_safe_seen_read() -> int: - return _attune_safe_seen + return _facts.attune_safe_seen # 组件出口(拆件后取件面——呈现与测试经此取件,禁链式穿透协作对象内部) @@ -164,7 +149,7 @@ func reward_grant() -> RewardGrant: func _attune_has(item_id: String) -> bool: - return attunement.has(item_id) + return _facts.attunement.has(item_id) # 装配集只读出口(_attune_has 契约化出口,save v6 只读访问器先例。只读零副作用—— @@ -179,7 +164,7 @@ func attune_has(item_id: String) -> bool: # kind 分发查询先例=_attune_refresh_x_total bonus_refresh。零装配=0) func attune_param_sum(kind: String) -> int: var total := 0 - for iid_v in attunement: + for iid_v in _facts.attunement: var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v)) if (arow.get("effect_kinds", []) as Array).has(kind): total += int(arow.get("magnitude", 0)) @@ -189,7 +174,7 @@ func attune_param_sum(kind: String) -> int: # 装配集内 ap_charge_safe 行读取(取数源=装配集行扫描; # 首行语义=装配集内至多一件积点制〔试炼镐〕) func _attune_charge_safe_row() -> Dictionary: - for iid_v in attunement: + for iid_v in _facts.attunement: var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v)) if (arow.get("effect_kinds", []) as Array).has("ap_charge_safe"): return arow @@ -200,9 +185,9 @@ func _attune_charge_safe_row() -> Dictionary: # (used handle 不回收=本轮已耗充能作废,补新 handle=「回满」;差额注入防双份=level_runner # set_attune_charges 同构)。reset ∈ {battle, level}——adventure 周期归冒险开始种子面) func _reseed_attune_charges(reset: String) -> void: - if _attune_loadout() == null or treasure_inventory == null: + if _attune_loadout() == null or _reward_grant.treasure_inventory == null: return # 直构零装配/库存未就位——充能回满跳过(空装配同语义) - _attune_loadout().reset_cycle(reset, treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体) + _attune_loadout().reset_cycle(reset, _reward_grant.treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体) # 积点制件进度读数(自动兑付无玩家发起入口,读数口=C 区秘宝槽悬停浮窗进度行: @@ -213,18 +198,13 @@ func attune_convert_state() -> Dictionary: if row.is_empty() or String(row.get("form", "")) != "active_charged": return {"assembled": false, "points": 0, "need": 0, "display_name": "", "item_id": ""} var cn := int(row.get("charge_n", 0)) - return {"assembled": true, "points": _attune_safe_seen % cn if cn > 0 else 0, "need": cn, + return {"assembled": true, "points": _facts.attune_safe_seen % cn if cn > 0 else 0, "need": cn, "display_name": String(row.get("display_name", "")), "item_id": String(row.get("item_id", ""))} # 特性求值(效果行为=数据行 triggers 订阅,效果引擎 dispatch 分发; # 流派档位质变=FeatsEngine 纯函数求值 # (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击) -var _seal_miss_seen := {} # 封印未中行去重(每安全格实例一次) -var _perfect_hit_true := 0 # 整图完美封印计数(install 重置) -var _perfect_hit_false := 0 -var _perfect_forgiven := 0 # 完美保险豁免累计(单向累加自有账;契约 2 禁改写 _perfect_hit_false) -var _hero_atk := 0 # 主角攻击基值快照(encounter.hero_atk;E4 收口——表值运行期不可变) func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: @@ -234,6 +214,7 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: minesweeper = MinesweeperRules.new(db, action_economy) _enemy_system = UnitContainer.new(db) _status_effects = StatusContainer.new(db) + _facts = BattleFacts.new() # 战斗事实件(受理事实/轮缓冲/封印账/溯源/条件计数——成员迁移见设计 §4) _reward_economy = RewardEconomy.new(db, rng) _reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线) _damage = DamageController.new(db, rng, _enemy_system, _status_effects) @@ -252,11 +233,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: minesweeper.round_end_board.connect(_on_round_end) action_economy.round_end_stopped.connect(_on_round_end) # 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕) - _grant_seg = GrantSegment.new(self, _fx_host, _reward_grant) - _aura_seg = AuraSegment.new(self) - _combat_seg = CombatSegment.new(self, _fx_host, _reward_grant) - _decay_seg = DecaySegment.new(self) - _verdict_seg = VerdictSegment.new(self) + # 结算五段接线随轮循环驱动件迁驻(round_cycle.gd——段件持有/段序/重入闸/摇号单点;构造序禁环不变) + _rounds = RoundCycle.new(self, _fx_host, _reward_grant) # ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)---- @@ -269,7 +247,7 @@ func begin_battle(enc: Dictionary) -> bool: return false if not _install_and_reset(): return false - _reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险) + _facts.reset_round() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险) _begin_battle_fx_state(enc) _begin_battle_attune_params(enc) _begin_battle_preview_economy(enc) @@ -281,14 +259,14 @@ func begin_battle(enc: Dictionary) -> bool: if not action_economy.notify_round_start(): _fatal("行动经济 首轮 notify_round_start 被拒") return false - _roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读) + _rounds.roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读) round_started.emit(round_index) return true -# 受理段:encounter 装载+缓冲清零+护甲合流+重载闸+开域门+遭遇骰与敌人系统 spawn 受理门。 +# 受理段:_facts.encounter 装载+缓冲清零+护甲合流+重载闸+开域门+遭遇骰与敌人系统 spawn 受理门。 func _begin_battle_accept(enc: Dictionary) -> bool: - encounter.assign(enc) + _facts.encounter.assign(enc) _reward_grant.reset_battle() # 战斗域零 GP 持久(每场战斗缓冲从零起算;案C 闸复位) _damage.set_hero_ac_override(int(enc.get("hero_armor_class", -1))) # 主角护甲合流入参(伤害控制器 defender_ac 消费) db.set_reload_gate(true) @@ -299,9 +277,9 @@ func _begin_battle_accept(enc: Dictionary) -> bool: if dice.is_empty(): _fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle"))]) return false - # E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径) - _hero_atk = int(enc.get("hero_atk", int(db.combat.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) + # E4 收口:加成经 _facts.encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径; + # _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) _fatal("敌人系统 spawn 被拒") return false return true @@ -309,7 +287,7 @@ func _begin_battle_accept(enc: Dictionary) -> bool: # 遭遇骰(难度曲线与Boss GDD Core Rule 8):行有 unit_count_max 时按 [unit_count, max] 均匀骰; # 行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 -# encounter 封闭流恰一 scope(数量骰先、atk 骰后——区间宽度为零的行不掷=「定值不骰」既有口径); +# _facts.encounter 封闭流恰一 scope(数量骰先、atk 骰后——区间宽度为零的行不掷=「定值不骰」既有口径); # 敌人系统零随机纪律保持(收 override 即确定值)。 # 返回 [count_overrides, atk_overrides];行集为空 = 返回 [](fail 判据交受理段 _fatal)。 func _begin_battle_dice(enc: Dictionary) -> Array: @@ -352,12 +330,12 @@ func _begin_battle_dice_roll(rows: Array, estream: RandomNumberGenerator, roll_a # 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零) # 扩签运行态就位(归 fx_host)+账本挂接+桶挂引用+揭示/AC 两回调+rider 档位恒零+订阅重建。 func _begin_battle_fx_state(enc: Dictionary) -> void: - # 同调装配运行态注入(enc.attunement 携带;off=空集零扰动。淬毒 N 注入 + # 同调装配运行态注入(enc._facts.attunement 携带;off=空集零扰动。淬毒 N 注入 # 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install) - attunement = (enc.get("attunement", []) as Array).duplicate() - _attune_safe_seen = 0 + _facts.attunement = (enc.get("attunement", []) as Array).duplicate() + _facts.attune_safe_seen = 0 _fx_host.reset_battle_state(int(enc.get("hero_armor_class", -1))) - _mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款) + _rounds.mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款) var adv_bucket: Variant = enc.get("adv_bucket", null) _fx_host.mount_buckets(adv_bucket if adv_bucket is Dictionary else {}, enc.get("level_counters")) # 冒险桶宿主字典(直构兜底自有空字典);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂 _reward_grant.bind_battle(_battle_hooks()) # 授予件战斗域回调束(充能消耗/真视计数/踩雷计数) @@ -368,13 +346,13 @@ func _begin_battle_fx_state(enc: Dictionary) -> void: # 不设档位机制」):rider_tier 两注入恒零,散件单体效果=唯一形态 _damage.inject["rider_tier_bonus"] = 0 _damage.inject["rider_tier_crit"] = false - _feat_first_dig_used = false + _facts.feat_first_dig_used = false # 树 rule 键消费(Fork 激活侧效果=效果引擎合成 # 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键 # =stat 面投影不入战斗(T8 表内容 provisional 口径披露)) var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array # 效果引擎订阅重建(装载序=feats 表序→treasure 表序→树规则,写死保种子复放——细则见 battle_fx_host.rebuild_subscriptions) - _fx_host.rebuild_subscriptions(feat_ids, attunement, tree_rules) + _fx_host.rebuild_subscriptions(feat_ids, _facts.attunement, tree_rules) # 秘宝常时参数注入(装配集 kind 分发:远程镐挖距放宽/信标网络封印可达豁免; @@ -415,19 +393,19 @@ func _begin_battle_preview_economy(enc: Dictionary) -> void: # 板序推进+board_changed 发射+秘宝叠加层退役——时序逐字;复位职责留宿主(成员单点),不随件。 func _install_and_reset(during_round := false) -> bool: var installer := BoardInstaller.new({"db": db, "rng": rng, "minesweeper": minesweeper, - "reward_economy": _reward_economy, "encounter": encounter, "fatal_cb": _fatal}) + "reward_economy": _reward_economy, "encounter": _facts.encounter, "fatal_cb": _fatal}) if not installer.install(during_round): return false - _seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8) - _seal_miss_seen.clear() - _perfect_hit_true = 0 # 整图完美计数随 install 重置 - _perfect_hit_false = 0 - _perfect_forgiven = 0 # 豁免累计随 install 重置(domain=battle 每战清零) - _board_from_midround_refresh = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false - _board_seq += 1 + _facts.seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8) + _facts.seal_miss_seen.clear() + _facts.perfect_hit_true = 0 # 整图完美计数随 install 重置 + _facts.perfect_hit_false = 0 + _facts.perfect_forgiven = 0 # 豁免累计随 install 重置(domain=battle 每战清零) + _facts.board_from_midround = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false + _facts.board_seq += 1 board_changed.emit() - if treasure_inventory != null: - treasure_inventory.retire_overlays() # 叠加层随图消亡(换图/刷新图装板点;已花道具不返还) + if _reward_grant.treasure_inventory != null: + _reward_grant.treasure_inventory.retire_overlays() # 叠加层随图消亡(换图/刷新图装板点;已花道具不返还) return true @@ -448,9 +426,9 @@ func _on_cell_revealed(evt: Dictionary) -> void: if String(evt.get("kind", "")) == "safe": var cs_row: Dictionary[Variant, Variant] = _attune_charge_safe_row() if not cs_row.is_empty(): - _attune_safe_seen += 1 + _facts.attune_safe_seen += 1 var cn_b: int = int(cs_row.get("charge_n", 999)) - if cn_b > 0 and _attune_safe_seen % cn_b == 0: + if cn_b > 0 and _facts.attune_safe_seen % cn_b == 0: action_economy.grant_ap(int(cs_row.get("magnitude", 1))) if bool(evt.get("resource", false)): # 身份快照随缓冲捕获(批次隔离——轮内刷新后 mapping 已换批,授予必须用缓冲期身份, @@ -459,47 +437,20 @@ func _on_cell_revealed(evt: Dictionary) -> void: if snap != null: evt = evt.duplicate() evt["reward_snapshot"] = snap - _facts_reveals.append(evt) + _facts.reveals.append(evt) func _on_seal_judged(evt: Dictionary) -> void: - _facts_seals.append(evt) + _facts.seals.append(evt) func _on_round_end(_payload: Dictionary) -> void: - _facts_path = String(_payload.get("path_tag", "")) - _maybe_settle() + _facts.path = String(_payload.get("path_tag", "")) + _rounds.settle_if_due() -func _maybe_settle() -> void: - if _settling or battle_result != "": - return - if action_economy.phase != action_economy.PHASE_WAITING: - return - _settle() -# ---- 每轮结算序列(五段——段序骨架冻结于 SettlementTable.segment_order)---- - -func _settle() -> void: - _settling = true - var ri := round_index - # 段序编排(五段——段体归 game/core/settlement/,本类只做段序+行发射/开单原语单点) - _grant_seg.run(ri) # ① 授予段(案C 闸随段装载) - _aura_seg.run(ri) # ② 增益/减益段(续存实例组合行) - _combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕) - _decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减) - var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判) - if verdict != "continue": - return # 终局:_settling 不回置——_battle_over 已接管 - _settling = false - - -# ---- 授予段 helpers(极性路由)---- - -# 开格产出乘区(贪婪矿灯/矿脉直觉/赌徒的血签事件收集器注入): -# positive=power ×(1+静态 mult%);negative=power ×(1+cost%)。 -# 全零收集器=原快照原样返回(零扰动)。 func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int) -> Dictionary: if positive and pos_m <= 0: return snap @@ -556,57 +507,7 @@ func set_treasure_unlocked(ids: Dictionary) -> void: # 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。 # 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。 # 摇定后、兑现前死亡 → 本拍技能作废。 -func _roll_round_actions() -> void: - var pity_m: int = int(db.combat.skill.pity_threshold) - # facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts, - # 容器内直读主角行) - var facts := { - "round_index": round_index, - "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_total": int(_mines_hits["total"]), - } - for u in _enemy_system.query_alive("enemy"): - # Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行 - # 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性 - # 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机 - # 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏) - for sid in _enemy_system.check_trigger_unlocks(u, facts): - var srow_u: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid) - _emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"}, - round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))], - 0, {"skill_id": sid}) - # enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射, - # 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值) - var pct_v := 0 - for pe in (u.get("pool", []) as Array): - var pe_d: Dictionary[Variant, Variant] = pe - if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"): - pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0)) - break - # target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记, - # 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value) - var eht_hits := _fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v, - "is_boss": String(encounter.get("node_type", "")) == "boss"}) - for h in eht_hits: - if String(h.get("kind", "")) == "target_attack_bonus": - u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0)) - var entries: Array[Variant] = _enemy_system.rollable_skills(u, facts) - if _enemy_system.basic_streak(u) >= pity_m: - var specials: Array[Variant] = _enemy_system.special_skills(u, facts) - if not specials.is_empty(): - entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合 - var stream: RandomNumberGenerator = rng.open_stream("behavior_roll") - _enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream)) - _enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记) - _mines_hits["last"] = _mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新) - _mines_hits["round"] = 0 - -# ---- 战斗段/算术读数(段体与掷点/折扣/暴击/克制/池乘区/下限/交付管线归 damage_controller.gd -# 与 settlement/combat_segment.gd;本类只保留薄委托——_defender_ac 供结算/呈现快照消费)---- - -# 防守方护甲等级读数——实现归 伤害控制器 defender_ac(本委托供结算快照/呈现快照) func _defender_ac(unit: Dictionary) -> int: return _damage.defender_ac(unit) @@ -643,12 +544,12 @@ func _emit_settlement_snapshot(verdict: String) -> void: buffs.append({"id": eff.effect_id, "side": "enemy", "layers": eff.layers, "left": eff.remaining_settlements}) var mines_hit := 0 var safe_n := 0 - for evt in _facts_reveals: + for evt in _facts.reveals: if evt.kind == "mine": mines_hit += 1 elif evt.kind == "safe": safe_n += 1 - var board_stats := {"seq": _board_seq} + var board_stats := {"seq": _facts.board_seq} if minesweeper.current_board() != null: board_stats["w"] = minesweeper.current_board().width board_stats["h"] = minesweeper.current_board().height @@ -656,18 +557,18 @@ func _emit_settlement_snapshot(verdict: String) -> void: board_stats["seals"] = minesweeper.seal_count() settlement_snapshot.emit({ "kind": "settlement", - "level": int(encounter.get("level_index", 0)), - "depth": int(encounter.get("node_depth", 0)), + "level": int(_facts.encounter.get("level_index", 0)), + "depth": int(_facts.encounter.get("node_depth", 0)), "round": round_index, "verdict": verdict, "unit_count": enemies.size(), "spawn_count": _enemy_system.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」 "hero": {"hp": int(_enemy_system.hero_unit().get("hp", 0)), "max_hp": int(_enemy_system.hero_unit().get("max_hp", 0)), "atk": int(_enemy_system.hero_unit().get("atk", 0)), "armor_class": _defender_ac(_enemy_system.hero_unit())}, - "economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _refresh_used_this_round}, + "economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _facts.refresh_used}, "board": board_stats, "enemies": enemies, "buffs": buffs, - "round_facts": {"path": _facts_path, "safe": safe_n, "mines": mines_hit, "seal_events": _facts_seals.size(), }, + "round_facts": {"path": _facts.path, "safe": safe_n, "mines": mines_hit, "seal_events": _facts.seals.size(), }, }) @@ -675,7 +576,7 @@ func _emit_settlement_snapshot(verdict: String) -> void: func _exit_distribution() -> void: var mode := "same_board" - if _facts_path == "cleared": + if _facts.path == "cleared": mode = "new_board" # cleared 轮恒走 new_board(契约义务);非清图轮恒同图(等待期刷新通道已移除) if mode == "new_board": _reward_economy.retire_mapping() @@ -689,16 +590,16 @@ func _exit_distribution() -> void: round_index += 1 _fx_host.emit_round_start() action_economy.notify_round_start() - _reset_round_facts() - _refresh_used_this_round = 0 - _roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道) + _facts.reset_round() + _facts.refresh_used = 0 + _rounds.roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道) round_started.emit(round_index) # 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道 func can_refresh() -> bool: return battle_result == "" \ - and _refresh_used_this_round < int(db.run.action_economy.refresh_cap_per_round) \ + and _facts.refresh_used < int(db.run.action_economy.refresh_cap_per_round) \ and action_economy.phase == action_economy.PHASE_ACTIVE \ and action_economy.rap >= int(db.run.action_economy.rap_refresh_cost) @@ -711,7 +612,7 @@ func player_request_refresh() -> bool: return false if not action_economy.request_refresh(): return false - _refresh_used_this_round += 1 + _facts.refresh_used += 1 _reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提) minesweeper.retire_board(true) # 旧板退役(轮内通道) return _install_and_reset(true) # 当场换图——失败即编排违例 fail-fast @@ -737,11 +638,11 @@ func _battle_over(result: String) -> void: _reward_economy.retire_mapping() _enemy_system.despawn_all() _status_effects.notify_battle_end() - if treasure_inventory != null: - treasure_inventory.retire_overlays() # 叠加层随战斗结束清空(库存不动——关卡域跨战斗) + if _reward_grant.treasure_inventory != null: + _reward_grant.treasure_inventory.retire_overlays() # 叠加层随战斗结束清空(库存不动——关卡域跨战斗) action_economy.notify_combat_end() # → Idle,RAP 清零 db.set_reload_gate(false) - _reset_round_facts() + _facts.reset_round() battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "gp_dropped": gp_dropped}) @@ -749,7 +650,7 @@ func _battle_over(result: String) -> void: # gp_dropped = gp_drop 封闭流掷定(tier 行的 roll_min..roll_max 均匀整数——每战斗末恰一 scope) # + level_gp_bonus(level_index) + escrow。掷定行缺位 = 数据面缺行(fail 报错按 0 兑付——不哑掉)。 func _roll_gp_drop() -> int: - var tier := "boss" if String(encounter.get("node_type", "")) == "boss" else "normal" + var tier := "boss" if String(_facts.encounter.get("node_type", "")) == "boss" else "normal" var drops: Dictionary[Variant, Variant] = db.run.shop.gp_drop_row(tier) var roll := 0 if drops.is_empty(): @@ -757,11 +658,11 @@ func _roll_gp_drop() -> int: else: var stream: RandomNumberGenerator = rng.open_stream("gp_drop") roll = stream.randi_range(int(drops.get("roll_min", 0)), int(drops.get("roll_max", 0))) - var drop := roll + db.run.shop.level_gp_bonus_at(int(encounter.get("level_index", 0))) + var drop := roll + db.run.shop.level_gp_bonus_at(int(_facts.encounter.get("level_index", 0))) # 财富之道·聚(战斗掉落 GP 乘算·入账前:只乘战斗掉落两成分〔掷定+关卡加成〕, # 店内 escrow 不乘〔范围=仅战斗掉落渠道〕;本函数返回值=结算框「GP 增量」行 # 显示源,落在入账前=显示即实得;乘数 provisional 150%) - if (encounter.get("tree_rules", []) as Array).has("economy_hoard"): + if (_facts.encounter.get("tree_rules", []) as Array).has("economy_hoard"): drop = int(round(float(drop) * 1.5)) return drop + _reward_grant.escrow @@ -812,7 +713,7 @@ func presentation_snapshot() -> Dictionary: "phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "can_refresh": can_refresh(), - "refresh_used": _refresh_used_this_round, # 受理计数入呈现成文面(HUD 余量单源读此处) + "refresh_used": _facts.refresh_used, # 受理计数入呈现成文面(HUD 余量单源读此处) "hero": hero, "enemies": enemies, "hero_effects": _status_effects.query_active("hero").duplicate(true), # 值拷贝出户(快照消费方直改不渗入状态容器) @@ -839,12 +740,12 @@ func diagnostic_snapshot() -> Dictionary: return { "battle_result": battle_result, "round": round_index, - "settling": _settling, - "refresh_used_round": _refresh_used_this_round, - "board_seq": _board_seq, - "facts_path": _facts_path, - "facts_reveals": _facts_reveals.size(), - "facts_seals": _facts_seals.size(), + "settling": _rounds.is_settling(), + "refresh_used_round": _facts.refresh_used, + "board_seq": _facts.board_seq, + "facts_path": _facts.path, + "facts_reveals": _facts.reveals.size(), + "facts_seals": _facts.seals.size(), "action_economy": {"phase": String(action_economy.phase), "ap": action_economy.ap, "rap": action_economy.rap}, "minesweeper_board_installed": minesweeper.current_board() != null, "minesweeper_revealed": minesweeper.revealed_cells().size() if minesweeper.current_board() != null else 0, @@ -855,12 +756,6 @@ func diagnostic_snapshot() -> Dictionary: } -func _reset_round_facts() -> void: - _facts_reveals.clear() - _facts_seals.clear() - _facts_path = "" - - # ---- 行协议(Core Rule:公共字段 + 行类型特有字段)---- func _emit_row(head: Dictionary, ri: int, display: String, value: int, extra: Dictionary) -> void: @@ -911,6 +806,6 @@ func _fatal(msg: String) -> void: ## 完美封印谓词(两账相减:有效误标=误标总数−豁免数) -## 契约 2:_perfect_hit_false 保持纯净,豁免只累加 _perfect_forgiven,禁回退式改写与拦截式吞事件。 +## 契约 2:_facts.perfect_hit_false 保持纯净,豁免只累加 _facts.perfect_forgiven,禁回退式改写与拦截式吞事件。 func perfect_sweep_earned() -> bool: - return _perfect_hit_false - _perfect_forgiven <= 0 and _perfect_hit_true >= 1 + return _facts.perfect_hit_false - _facts.perfect_forgiven <= 0 and _facts.perfect_hit_true >= 1 diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 05d110d5..df01c3f4 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -517,7 +517,7 @@ func _start_battle(node: Dictionary) -> bool: _settlement = BattleOrchestrator.new(db, rng) if treasure_inventory == null: treasure_inventory = TreasureItems.new(db) # 惰性建——关卡域单实例(跨战斗延续) - _settlement.treasure_inventory = treasure_inventory # 库存注入(授予路由+use_treasure 编排消费面) + _settlement.reward_grant().treasure_inventory = self.treasure_inventory # 库存注入(经授予件出口直注——注入时序在宿主构造之后不变) if _settlement != null: _settlement.set_treasure_unlocked(_treasure_unlocked) # 解锁门控下传(装板摇号前) # core↔core 接线只许方法 Callable(静态规则 S6——lambda 捕获 self 会延长 diff --git a/game/core/settlement/combat_segment.gd b/game/core/settlement/combat_segment.gd index db8bae28..1e9a6ac9 100644 --- a/game/core/settlement/combat_segment.gd +++ b/game/core/settlement/combat_segment.gd @@ -26,7 +26,7 @@ func run(ri: int) -> void: # 同一次 _settle 内,facts 已定。 var round_safe := 0 var round_mine := false - for evt in oc._facts_reveals: + for evt in oc._facts.reveals: if evt.kind == "safe": round_safe += 1 elif evt.kind == "mine": @@ -53,7 +53,7 @@ func _hero_strike(ri: int) -> void: var hero_target = oc._enemy_system.default_target() if hero_alive and hero_target != null: var ht := oc._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": oc._hero_atk(), "target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri, "origin_cell": Vector2i(-1, -1), "attack_type": String(oc._enemy_system.hero_unit().get("attack_type", "normal"))}) oc._damage.submit(ht) diff --git a/game/core/settlement/grant_segment.gd b/game/core/settlement/grant_segment.gd index 2de0123c..1c396e51 100644 --- a/game/core/settlement/grant_segment.gd +++ b/game/core/settlement/grant_segment.gd @@ -34,7 +34,7 @@ func run(ri: int) -> void: # 开格/踩雷兑付批(轮 facts_reveals 资源事件序:safe 正向乘区兑付 / mine_hit 减半负向兑付) func _reveal_batch(ri: int) -> void: - for evt in oc._facts_reveals: + for evt in oc._facts.reveals: if not bool(evt.get("resource", false)): continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②) var snap = evt.get("reward_snapshot") @@ -61,7 +61,7 @@ func _reveal_safe(evt: Dictionary, snap: Dictionary, ri: int) -> void: 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, - "is_first_dig_of_battle": not oc._feat_first_dig_used, + "is_first_dig_of_battle": not oc._facts.feat_first_dig_used, "terrain_kind": oc._harmful_variants(evt.get("variant_ids", []) as Array), "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, @@ -91,8 +91,8 @@ func _reveal_safe_apply(dr_hits: Array, evt: Dictionary, ri: int) -> void: if String(h.get("kind", "")) == "grant_material": _apply_material_hit(h, evt.cell, ri) var spell_hits: Array[Variant] = oc._hits_of(dr_hits, "spawn_spell") - if not spell_hits.is_empty() and not oc._feat_first_dig_used: - oc._feat_first_dig_used = true + if not spell_hits.is_empty() and not oc._facts.feat_first_dig_used: + oc._facts.feat_first_dig_used = true var spell_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", "奥术矢", {"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"}) @@ -160,7 +160,7 @@ func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: in # 危险格恒无变异——布置段排除,双层叠罚不存在;封印格直挖照吃变异伤害——地形后果同 mine_hit 口径, # 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场) func _terrain_batch(ri: int) -> void: - for evt in oc._facts_reveals: + for evt in oc._facts.reveals: var vids: Array[Variant] = evt.get("variant_ids", []) for vid in vids: var vrow: Dictionary[Variant, Variant] = oc.db.combat.variation.row_by_id(String(vid)) @@ -203,7 +203,7 @@ func _terrain_damage_reduce(dt_hits: Array, dt_amount: int) -> int: # 台账披露待追认〕;完美布防=本轮封印批全中(hit>0 且 miss==0)时封印伤 ×mult——全对推理奖励)。 # 伤害单据案C 暂存战斗段末兑付。 func _seal_batch(ri: int) -> void: - for evt in oc._facts_seals: + for evt in oc._facts.seals: _judge_seal(evt, ri) _seal_strike_batch(ri) @@ -213,7 +213,7 @@ func _seal_batch(ri: int) -> void: func _seal_strike_batch(ri: int) -> void: var fs_hits := 0 var fs_misses := 0 - for fevt in oc._facts_seals: + for fevt in oc._facts.seals: if bool(fevt.get("hit", false)): fs_hits += 1 else: @@ -223,7 +223,7 @@ func _seal_strike_batch(ri: int) -> void: var strike := _seal_strike_value(fs_fx, fs_hits, fs_misses) if strike.is_empty(): return - for fevt in oc._facts_seals: + for fevt in oc._facts.seals: if not bool(fevt.get("hit", false)): continue var st_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_seal_strike", "战封印军规", @@ -272,8 +272,8 @@ func _round_end_apply(ri: int) -> void: var rap_pct: int = oc.attune_param_sum("rap_transfer_mult") oc.action_economy.finish_round_stop(fx_host.engine.has_effect_on("round_end", "grant_ap", "transfer"), rap_pct if rap_pct > 0 else 100) - if oc._facts_path == "cleared": - reward_grant.grant_clear_reward(ri, oc._board_from_midround_refresh, oc.perfect_sweep_earned()) + if oc._facts.path == "cleared": + reward_grant.grant_clear_reward(ri, oc._facts.board_from_midround, oc.perfect_sweep_earned()) fx_host.flush_pending_reveals(ri) # 揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入) oc._damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界) @@ -305,10 +305,10 @@ func _judge_seal(evt: Dictionary, ri: int) -> void: # 殉爆票据 → 施加面 → 雷区图谱额外兑付 → 基线正向兑付) func _judge_seal_hit(evt: Dictionary, ri: int) -> void: var cell: Vector2i = evt.cell - if oc._seal_consumed.has(cell): + if oc._facts.seal_consumed.has(cell): return # 去重跳过;零明细行 - oc._seal_consumed[cell] = true - oc._perfect_hit_true += 1 + oc._facts.seal_consumed[cell] = true + oc._facts.mark_perfect_hit() var fh_hits := fx_host.fx_emit("seal_hit", {"cell": cell, "round": ri, "terrain_kind": oc._harmful_adjacent(cell)}) _seal_burst_tickets(fh_hits, cell, ri) @@ -367,8 +367,8 @@ func _seal_preview_area(cell: Vector2i, ri: int) -> void: var n_snap = oc._reward_economy.query_full_snapshot(n) 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", ""))) - if oc.treasure_inventory != null and not types.is_empty(): - oc.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell)) + if reward_grant.treasure_inventory != null and not types.is_empty(): + 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}) @@ -376,10 +376,10 @@ func _seal_preview_area(cell: Vector2i, ri: int) -> void: # grant_ap mode:queue 下一轮兑现 / forgive_seal_miss 完美保险豁免 → 归因行 → 负向兑付) func _judge_seal_miss(evt: Dictionary, ri: int) -> void: var cell: Vector2i = evt.cell - oc._perfect_hit_false += 1 - if oc._seal_miss_seen.has(cell): + oc._facts.mark_perfect_miss() + if oc._facts.seal_miss_seen.has(cell): return # 每格实例一次去重 - oc._seal_miss_seen[cell] = true + oc._facts.seal_miss_seen[cell] = true # 危险直觉·守挂点(seal_missed 发射:契约 12 触发主语=「当回合发生的标错 # 动作」——去重闸=新旧标错判别器〔后续轮重判旧封印被闸挡回零触发〕;双条件②「结算时封印仍在 # 场」由发射面既有过滤承载〔被挖开的封印已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮 @@ -391,11 +391,11 @@ func _judge_seal_miss(evt: Dictionary, ri: int) -> void: elif String(h.get("kind", "")) == "forgive_seal_miss": # 完美保险(订阅式豁免:本战首次标错计一次豁免,只累加自有账 # _perfect_forgiven,绝不回写 _perfect_hit_false=契约 2 计数纯净;发射时机与消费点零改动=契约 3) - oc._perfect_forgiven += 1 + oc._facts.mark_perfect_forgiven() _apply_counter_hit(h) # 豁免同时记账(insurance_used battle 域——下次标错 when lt 1 不再命中) oc._emit_row({"type": "extended", "source_class": "seal_miss", "origin_cell": cell}, ri, "完美保险·标错豁免", 0, - {"effect_id": "insurance_used", "forgiven": oc._perfect_forgiven}) - var hl := "perfect_broken" if oc._perfect_hit_false - oc._perfect_forgiven == 1 else "" + {"effect_id": "insurance_used", "forgiven": oc._facts.perfect_forgiven}) + var hl := "perfect_broken" if oc._facts.perfect_hit_false - oc._facts.perfect_forgiven == 1 else "" oc._emit_row({"type": "seal_miss", "source_class": "seal_miss", "origin_cell": cell, "highlight": hl}, ri, "封印未中", 0, {}) var snap = oc._reward_economy.grant(cell) if not (snap is Dictionary): diff --git a/game/core/settlement/round_cycle.gd b/game/core/settlement/round_cycle.gd new file mode 100644 index 00000000..370e6c77 --- /dev/null +++ b/game/core/settlement/round_cycle.gd @@ -0,0 +1,106 @@ +## 轮循环驱动件:每轮结算周期的唯一驱动——结算重入门闸(settling)、五段段序执行 +## (段序骨架冻结于 SettlementTable.segment_order)、开轮摇号(共识点 B:技能选择前置到轮次开始, +## 摇定即本拍结算的选技)、踩雷计数账(转存时点=摇号轮末)。 +## 单一变化原因:轮结算周期的推进结构(段序、重入闸、摇号时序)。 +## 耦合边界:oc=宿主引用(五段段类既定同款直调形态)——经 oc 取 facts/协作件/行发射/出口分发; +## 五段构造参数与持有形态不变(grant/combat 三引用、aura/decay/verdict 单引用);判定段零改动 +## (_exit_distribution 留宿主);随机只经 oc.rng 的 behavior_roll 封闭流(流名、开流时点、取号序逐字); +## 零信号连接、零 await;随编排器每战新建同灭。 +class_name RoundCycle +extends RefCounted + +var oc: BattleOrchestrator # 宿主引用(段类同款直调——facts/协作件/行发射/出口分发取数单点) +var mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(本场累计/上轮转存/当轮累计三读数合一账) +var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点) +var _aura_seg = null # AuraSegment(②增益/减益段) +var _combat_seg = null # CombatSegment(③战斗段) +var _decay_seg = null # DecaySegment(④衰减段) +var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归宿主) +var _settling := false + + +func _init(host: BattleOrchestrator, fx_host_ref: BattleFxHost, reward_grant_ref: RewardGrant) -> void: + oc = host + # 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕——五行自宿主 _init 逐字迁入) + _grant_seg = GrantSegment.new(oc, fx_host_ref, reward_grant_ref) + _aura_seg = AuraSegment.new(oc) + _combat_seg = CombatSegment.new(oc, fx_host_ref, reward_grant_ref) + _decay_seg = DecaySegment.new(oc) + _verdict_seg = VerdictSegment.new(oc) + + +# 结算重入门闸+段序体(原宿主 _maybe_settle 三闸与 _settle 合体——闸序 settling→battle_result→phase +# 逐字;终局早退不回置 _settling,_battle_over 接管) +func settle_if_due() -> void: + if _settling or oc.battle_result != "": + return + if oc.action_economy.phase != oc.action_economy.PHASE_WAITING: + return + _settling = true + var ri := oc.round_index + # 段序编排(五段——段体归 game/core/settlement/,宿主只做行发射/开单原语单点) + _grant_seg.run(ri) # ① 授予段(案C 闸随段装载) + _aura_seg.run(ri) # ② 增益/减益段(续存实例组合行) + _combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕) + _decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减) + var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判) + if verdict != "continue": + return # 终局:_settling 不回置——_battle_over 已接管 + _settling = false + + +func is_settling() -> bool: + return _settling + + +# ---- 开轮摇号(共识点 B:技能选择前置到轮次开始,数值计算留结算段——原宿主函数逐字迁入)---- +# 每实例:保底判定(连续执行普攻 ≥ M → 必出特殊技、特殊技内按权重、不受冷却限制)→ 排除冷却中技能 +# (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从统一流顺序取号摇定本拍动作。 +# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。 +# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。 +# 摇定后、兑现前死亡 → 本拍技能作废。 +func roll_round_actions() -> void: + var pity_m: int = int(oc.db.combat.skill.pity_threshold) + # facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts, + # 容器内直读主角行) + var facts := { + "round_index": oc.round_index, + "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")), + "mines_hit_last": int(mines_hits["last"]), + "mines_hit_total": int(mines_hits["total"]), + } + for u in oc._enemy_system.query_alive("enemy"): + # Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行 + # 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性 + # 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机 + # 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏) + for sid in oc._enemy_system.check_trigger_unlocks(u, facts): + var srow_u: Dictionary[Variant, Variant] = oc.db.combat.skill.row_by_id(sid) + oc._emit_row({"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))], + 0, {"skill_id": sid}) + # enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射, + # 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值) + var pct_v := 0 + for pe in (u.get("pool", []) as Array): + var pe_d: Dictionary[Variant, Variant] = pe + if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"): + pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0)) + break + # target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记, + # 主角攻击面 _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, + "is_boss": String(oc._facts.encounter.get("node_type", "")) == "boss"}) + for h in eht_hits: + if String(h.get("kind", "")) == "target_attack_bonus": + 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) + if oc._enemy_system.basic_streak(u) >= pity_m: + var specials: Array[Variant] = oc._enemy_system.special_skills(u, facts) + if not specials.is_empty(): + entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合 + var stream: RandomNumberGenerator = oc.rng.open_stream("behavior_roll") + oc._enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream)) + oc._enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记) + mines_hits["last"] = mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新) + mines_hits["round"] = 0 diff --git a/game/core/settlement/round_cycle.gd.uid b/game/core/settlement/round_cycle.gd.uid new file mode 100644 index 00000000..f7d726e5 --- /dev/null +++ b/game/core/settlement/round_cycle.gd.uid @@ -0,0 +1 @@ +uid://ce07smbypdcyj diff --git a/game/tests/static_scan.sh b/game/tests/static_scan.sh index df9951d6..c76494db 100644 --- a/game/tests/static_scan.sh +++ b/game/tests/static_scan.sh @@ -314,7 +314,7 @@ fi # get_tree());排除注释行(行首 #/##)与字符串字面量(双引号包裹段内的 await 非代码), # 口径仿 S1/S2 的 grep 排除手法。 # 自检:清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿。 -S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd" +S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd" S8_MISS=0 for f in $S8_FILES; do if [ ! -f "$f" ]; then @@ -356,7 +356,7 @@ fi # 自检:扫描面为空即判红(require_scan_face);B1 文件集与三族自家文件缺位即判红 # (文件被改名=豁免名单失效,宁可红不可静默放行)。 if require_scan_face "S7" "$DIR_CORE"; then -S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd" +S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd" S7_MISS=0 for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do if [ ! -f "$f" ]; then diff --git a/game/tests/unit/battle_settlement/roll_multiplier_test.gd b/game/tests/unit/battle_settlement/roll_multiplier_test.gd index ac3c8980..ac4365af 100644 --- a/game/tests/unit/battle_settlement/roll_multiplier_test.gd +++ b/game/tests/unit/battle_settlement/roll_multiplier_test.gd @@ -34,9 +34,31 @@ func _init() -> void: _smoke_pool_wiring() _smoke_damage_types() _smoke_attack_chain_fresh_pool() + _smoke_hero_atk_derived() quit(Kit.finish("roll_multiplier_test")) +# ---- 主角攻击基值派生读(注册面批同代编排归约 V4——_hero_atk 快照成员退役改派生读: +# enc 携 hero_atk 键受理后经派生读供 spawn 与攻击单 base_value 两消费点;缺省回退表值—— +# 受理后不变量(encounter 受理后只读)。攻击单 base_value 贯通由 _smoke_damage_types 的 +# hero_atk_val=100 全链路行值承载(同夹具既有覆盖——派生读在攻击链上工作即行值随 override 走)---- +func _smoke_hero_atk_derived() -> void: + var rng_a = load("res://core/rng_service.gd").new() + rng_a.set_master_seed(4242) + var s_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a) + Kit.check_quiet(s_a.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 100}), + "起战受理(override 100)〔编排归约 V4〕") + Kit.check_quiet(int(s_a._enemy_system.hero_unit().get("atk", -1)) == 100, + "主角攻击单基值==enc 携带 override(_hero_atk 派生读贯通 spawn 消费点)〔编排归约 V4〕") + var rng_b = load("res://core/rng_service.gd").new() + rng_b.set_master_seed(4242) + var s_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b) + Kit.check_quiet(s_b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}), + "起战受理(缺省)〔编排归约 V4〕") + Kit.check_quiet(int(s_b._enemy_system.hero_unit().get("atk", -1)) == int(_db.combat.hero.atk), + "缺省主角攻击单基值==hero 表值(派生读回退面)〔编排归约 V4〕") + + # ---- 敌方攻击类型轴(表列/单位快照/单据载荷/矩阵命中证词)---- # 克制矩阵命中证词 = 同种子三甲型配对实测(不是查表存在性证词):同种子下 armor_roll 流消费逐位一致、 # 阈值(hero armor_class)不变,三甲型唯一差异 = 克制点数 cdelta(normal_vs_light 0 / heavy −1 / ward +1) diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index 2b972ee1..677a9f25 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -83,9 +83,10 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary: var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(42) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) - var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false} + var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false, "econ_refresh_used": -1} settlement.step_row.connect(func(row): out.stream.append(row)) settlement.battle_finished.connect(func(payload): out.result = payload.get("result", "")) + settlement.settlement_snapshot.connect(func(snap): out.econ_refresh_used = int(snap.get("economy", {}).get("refresh_used", -1))) # 末次发射=终局轮快照 settlement.minesweeper.cell_revealed.connect(func(evt): out.events.append(evt)) settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt)) settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag})) @@ -108,6 +109,10 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary: # P0-3 回归断言:notify_combat_end 曾被并进行尾注释从不执行—— # 战斗收束后相位必须回 Idle(RAP 清零),任何「相位卡 Waiting」回归在此被抓 Kit.check("战斗收束后行动经济相位回 Idle(notify_combat_end 生效,实测=%s)" % String(action_economy.phase), action_economy.phase == "Idle") + # 复位边界(注册面批同代编排归约 V2——BattleFacts.reset_round 不碰 refresh_used 的可观测提交面): + # 终局序列含 facts.reset_round(),其后呈现快照的 refresh_used 必须等于末轮结算快照的 economy 值 + Kit.check("L%d·D%d 终局后呈现快照 refresh_used==末轮结算快照 economy 值(复位不碰 refresh_used)〔编排归约 V2〕" % [L, D], + int(settlement.presentation_snapshot().get("refresh_used", -1)) == out.econ_refresh_used) return out @@ -1019,7 +1024,7 @@ func _smoke_counter_buckets() -> void: if not oA.minesweeper.current_board().is_mine(ca): safe_a = ca break - oA._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1) + oA._rounds._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1) Kit.check("V6 第1战标错落 battle 桶 insurance_used=1(_apply_counter_hit 静态写口经 ctx)〔无状态化 V6〕", EffectEngine.counter(oA.fx_host().counters_ctx, "insurance_used") == 1) # 卡④ wr2①——完美保险标错行=单一扩展行型(原 insurance_used 实名行型收敛) var v6_ins := {} diff --git a/game/tests/unit/battle_settlement/seal_polarity_test.gd b/game/tests/unit/battle_settlement/seal_polarity_test.gd index e10b1241..862711b1 100644 --- a/game/tests/unit/battle_settlement/seal_polarity_test.gd +++ b/game/tests/unit/battle_settlement/seal_polarity_test.gd @@ -374,10 +374,10 @@ func _smoke_clear_reward_zero_paths() -> void: Kit.check("高光守卫构造战斗启动", s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"})) for u in s_b._enemy_system.all_enemies(): s_b._enemy_system.apply_damage(u, int(u.hp)) - s_b._perfect_hit_true = 1 # 测试通道:构造完美谓词成立(真值在正常流由封印判定累加) - s_b._perfect_hit_false = 0 + s_b._facts.perfect_hit_true = 1 # 测试通道:构造完美谓词成立(真值在正常流由封印判定累加) + s_b._facts.perfect_hit_false = 0 rows_b.clear() - s_b.reward_grant().grant_clear_reward(1, s_b._board_from_midround_refresh, s_b.perfect_sweep_earned()) + s_b.reward_grant().grant_clear_reward(1, s_b._facts.board_from_midround, s_b.perfect_sweep_earned()) Kit.check("② grant_clear_reward 守卫路径重走首份 → 未生效行仍在案", _has_source_row(rows_b, "reward_ineffective", "clear_reward", "clear_blast")) Kit.check("② 首份零兑付 → 高光行不发(不记名义值 2)", _rows_of_type(rows_b, "highlight").is_empty()) # ③ 第二份被拒:唯一存活敌 HP ≤ 首份 aoe 量 → 首份击杀 → 第二份无存活敌(zero + 未生效行) @@ -389,10 +389,10 @@ func _smoke_clear_reward_zero_paths() -> void: Kit.check("第二份被拒构造战斗启动", s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"})) var boss = s2._enemy_system.all_enemies()[0] boss.hp = 1 # 测试通道:压低至首份 aoe 必杀(弱形式——正常流 Boss 血量不可控) - s2._perfect_hit_true = 1 - s2._perfect_hit_false = 0 + s2._facts.perfect_hit_true = 1 + s2._facts.perfect_hit_false = 0 rows2.clear() - s2.reward_grant().grant_clear_reward(1, s2._board_from_midround_refresh, s2.perfect_sweep_earned()) + s2.reward_grant().grant_clear_reward(1, s2._facts.board_from_midround, s2.perfect_sweep_earned()) Kit.check("③ 首份 aoe 真实兑付(击杀唯一存活敌)", _has_source_row(rows2, "damage", "clear_reward")) Kit.check("③ 第二份被拒产出 reward_ineffective 行(零兑付)", _has_source_row(rows2, "reward_ineffective", "clear_reward", "clear_blast")) var hl2 := _rows_of_type(rows2, "highlight") diff --git a/game/tests/unit/battle_settlement/trigger_cond_test.gd b/game/tests/unit/battle_settlement/trigger_cond_test.gd index cd7c0709..026b88d6 100644 --- a/game/tests/unit/battle_settlement/trigger_cond_test.gd +++ b/game/tests/unit/battle_settlement/trigger_cond_test.gd @@ -116,19 +116,19 @@ func _init() -> void: "feats": [], "tree_rules": [], "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle", "adv_bucket": {}}), "V6 前置:战斗受理") # facts 组包经真实摇号路径消费(mines 全零时条件技 sneak_ambush 不入集合——facts 四键在位的可观察面) - Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["last"]) == 0 and int(orch._mines_hits["round"]) == 0, + Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["last"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0, "V6 开战清零:三计数全零起〔触发器批 V6〕") # 正向 blast(spark_aoe)不递增——负向计数守卫(flat 行应用面自编排器迁至奖励授予件 # ——迭代40 卡① 拆件;计数经授予件 battle_hooks.negative_blast 回宿主 _mines_hits) var att_v6 := {"source_class": "reward", "cell": Vector2i(-1, -1), "ri": 1} orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "positive", "power": 2, "display_name": "爆裂", "reward_id": "spark_aoe"}, att_v6) - Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["round"]) == 0, + Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0, "V6 正向 blast(spark_aoe)不递增(负向守卫——清图/爆裂格零误计)〔触发器批 V6〕") # 负向 blast(雷)递增 orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "negative", "power": 2, "display_name": "警觉", "reward_id": "enemy_alert"}, att_v6) - Kit.check_quiet(int(orch._mines_hits["total"]) == 1 and int(orch._mines_hits["round"]) == 1, + Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 1 and int(orch._rounds.mines_hits["round"]) == 1, "V6 负向 blast(雷)递增恰一次〔触发器批 V6〕") # ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)---- diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index 320ecbad..28e5093f 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -850,7 +850,7 @@ func _drive_to_pref(autoplay, prefer: String) -> Dictionary: r.pick_next_node(int(hit.get("id", -1))) out["node"] = hit if prefer == "battle": - out["enc"] = r.current_battle().encounter + out["enc"] = r.current_battle()._facts.encounter else: var cands: Array = (adr.stop().node_curio_candidates() as Dictionary).get("candidates", []) as Array adr.stop().present_node_curio(cands) diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index ee376c43..33631f57 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -174,10 +174,10 @@ func _init() -> void: Kit.check_quiet(safe_cells.size() == 2, "保险 rig:两枚安全格定位(去重闸按格实例——限次样本须异格)〔卡② R2〕") var rows_orch: Array[Dictionary] = [] orch.step_row.connect(func(row): rows_orch.append(row)) # step_row 公开信号面(insurance_used 载荷/seal_miss highlight=豁免账与误标账观测面) - Kit.check_quiet(int(orch._perfect_hit_false) == 0 and int(orch._perfect_forgiven) == 0, + Kit.check_quiet(int(orch._facts.perfect_hit_false) == 0 and int(orch._facts.perfect_forgiven) == 0, "保险前置:误标账与豁免账双零起〔卡② R2〕") - orch._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1) - Kit.check_quiet(int(orch._perfect_hit_false) == 1, + orch._rounds._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1) + Kit.check_quiet(int(orch._facts.perfect_hit_false) == 1, "计数纯净:首次标错 _perfect_hit_false 落 1 未被回退改写(契约 2)〔卡② R2〕") var ins_row := {} for r in rows_orch: @@ -187,7 +187,7 @@ func _init() -> void: "豁免一次:首次标错 _perfect_forgiven=1(forgive_seal_miss 派发)〔卡② R2〕") Kit.check_quiet(int(orch.action_economy.pending_ap_delta()) == 1, "事件不吞:同一标错的 seal_missed 仍被危险直觉·守收到(queue AP+1)〔卡② R2〕") - orch._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2) + orch._rounds._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2) var ins_count := 0 var miss_hl_last := "" for r in rows_orch: @@ -199,11 +199,11 @@ func _init() -> void: "限次:第二次标错不获豁免(forgiven 恒 1——when counter lt 1)〔卡② R2〕") Kit.check_quiet(not orch.perfect_sweep_earned(), "判定相减:误标 2 − 豁免 1 = 有效 1 → 完美不成立〔卡② R2 真值表〕") - orch._perfect_hit_false = 1 - orch._perfect_hit_true = 1 + orch._facts.perfect_hit_false = 1 + orch._facts.perfect_hit_true = 1 Kit.check_quiet(orch.perfect_sweep_earned(), "判定相减:误标1−豁免1=有效0且标中≥1→完美成立(保险每战限次一次迁移保绿)〔无状态化 V7〕") - orch._perfect_hit_true = 0 + orch._facts.perfect_hit_true = 0 Kit.check_quiet(not orch.perfect_sweep_earned(), "判定守卫:标中数 0 时零误标也不成立(整图 ≥1 标中条件零松动)〔卡② R2 真值表〕") var rig_rng2 = RngScript.new() @@ -218,7 +218,7 @@ func _init() -> void: var rows_orch2: Array[Dictionary] = [] orch2.step_row.connect(func(row): rows_orch2.append(row)) # step_row 公开信号面(insurance_used 载荷=豁免账观测面) var sc2 = safe_cells[0] - orch2._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1) + orch2._rounds._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1) var ins_row2 := {} for r in rows_orch2: if String(r.get("effect_id", "")) == "insurance_used": @@ -245,7 +245,7 @@ func _init() -> void: if not o10.minesweeper.current_board().is_mine(c10): sc10 = c10 break - o10._grant_seg._judge_seal({"cell": sc10, "hit": false}, 1) + o10._rounds._grant_seg._judge_seal({"cell": sc10, "hit": false}, 1) var sm10: Dictionary = {} for r10 in rows10: if String(r10.get("type", "")) == "seal_miss": @@ -272,14 +272,14 @@ func _init() -> void: if not o11.minesweeper.current_board().is_mine(c11): sc11 = c11 break - o11._grant_seg._judge_seal({"cell": sc11, "hit": false}, 1) + o11._rounds._grant_seg._judge_seal({"cell": sc11, "hit": false}, 1) var sm11: Dictionary = {} for r11 in rows11: if String(r11.get("type", "")) == "seal_miss": sm11 = r11 Kit.check_quiet(String(sm11.get("highlight", "")) == "", "A11 豁免:首次标错获豁免 → seal_miss 行 highlight==空串〔卡① N7〕") - o11._perfect_hit_true = 1 + o11._facts.perfect_hit_true = 1 Kit.check_quiet(o11.perfect_sweep_earned(), "A11 真值:误标 1 − 豁免 1 = 有效 0 且标中≥1 → 完美成立(与 highlight==空 一致)〔卡① N7〕") diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index 0b3360c8..508396dd 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -705,16 +705,16 @@ func _smoke_tree_rules() -> void: Kit.check("守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)", int(orch.action_economy.pending_ap_delta()) == 0) # 场景①:当回合新封印、结算判标错且封印仍在场 → queue +1(本轮 AP 零变化=queue 承载、下轮 notify_round_start 兑现) - orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 1) + orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 1) Kit.check("守①:当回合标错且封印在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)", int(orch.action_economy.pending_ap_delta()) == 1) # 场景③④:之前回合标错重判(封印在场状态)与同格重复——去重闸挡回,最多返还一次 - orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 2) - orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 3) + orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 2) + orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 3) Kit.check("守③④:旧封印重判零触发+同格最多一次(_seal_miss_seen 去重闸=「当回合标错动作」判别器)", int(orch.action_economy.pending_ap_delta()) == 1) # 场景②负样本:封印命中(hit=true 正向兑付路径)零触发 - orch._grant_seg._judge_seal({"cell": safe, "hit": true}, 4) + orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": true}, 4) Kit.check("守②负样本:标中/被挖封印零触发(标错事件只在 miss 分支发射——发射面过滤承载)", int(orch.action_economy.pending_ap_delta()) == 1) # 场景⑤:末轮判出→无下一轮=queue 不兑现(编排器每战新建——队列实例随战亡,合法作废态) diff --git a/game/tests/unit/core/settle_segments_test.gd b/game/tests/unit/core/settle_segments_test.gd index e7f23209..1714b14e 100644 --- a/game/tests/unit/core/settle_segments_test.gd +++ b/game/tests/unit/core/settle_segments_test.gd @@ -23,12 +23,15 @@ func _init() -> void: Kit.check_quiet(load("res://core/settlement/decay_segment.gd").new(oc) != null, "V4 DecaySegment 直接 new()〔迭代27 T2〕") Kit.check_quiet(load("res://core/settlement/verdict_segment.gd").new(oc) != null, "V4 VerdictSegment 直接 new()〔迭代27 T2〕") - # V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位) - Kit.check_quiet(oc._grant_seg != null and oc._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕") - Kit.check_quiet(oc._aura_seg != null and oc._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕") - Kit.check_quiet(oc._combat_seg != null and oc._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕") - Kit.check_quiet(oc._decay_seg != null and oc._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕") - Kit.check_quiet(oc._verdict_seg != null and oc._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕") + # V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位)——注册面批同代归约 V1: + # 段件宿主直持改经 _rounds(轮循环驱动件)承载,接线断言改指 _rounds 段族 + Kit.check_quiet(oc._rounds != null and oc._rounds.oc == oc, "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._aura_seg != null and oc._rounds._aura_seg.oc == 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._decay_seg != null and oc._rounds._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕") + Kit.check_quiet(oc._rounds._verdict_seg != null and oc._rounds._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕") # counter_add 写口真实路径(geologist 订阅行承载:公共命令链 begin_battle→move→dig→stop # 经授予段 counter_add 单点落 counters_ctx battle 桶——受理载荷+公开读数口 fx_host().counters_ctx) var rng_c = load("res://core/rng_service.gd").new() diff --git a/game/tests/unit/core/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd index 4f232f6a..fdceacb8 100644 --- a/game/tests/unit/core/treasure_effects_test.gd +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -162,13 +162,13 @@ func _init() -> void: "线香:装配起战(battle 级充能 1)") var inv8 = load("res://core/treasure_items.gd").new(db) var rec8: Dictionary = inv8.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0) - ob8.treasure_inventory = inv8 + ob8.reward_grant().treasure_inventory = inv8 var h8: int = int(rec8.get("handle", -1)) # handle 取 add_item 返回(snapshot 投影无 handle 键) var tgt8: Vector2i = _first_closed_safe(ob8) Kit.check_quiet(tgt8 != Vector2i(-9, -9), "线香:夹具存在未开安全格") var v8: Dictionary = ob8.reward_grant().use_treasure(h8, tgt8) Kit.check_quiet(bool(v8.get("ok", false)), "线香正常:单格真视受理(cell 目标解析)") - Kit.check_quiet(bool(ob8.treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8)) + Kit.check_quiet(bool(ob8.reward_grant().treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8)) and ob8.minesweeper.is_revealed(tgt8) == false, "线香断言:目标格入真视叠加层且未被开格(真视≠开格——信息红线)") Kit.check_quiet(inv8.snapshot_inventory().is_empty() or bool(inv8.snapshot_inventory()[0].get("used", false)), @@ -240,7 +240,7 @@ func _init() -> void: "天眼:装配起战(level 级充能 1)") var inv11 = load("res://core/treasure_items.gd").new(db) var rec11: Dictionary = inv11.add_item("sky_eye", "attunement", Vector2i(-1, -1), 0) - ob11.treasure_inventory = inv11 + ob11.reward_grant().treasure_inventory = inv11 var h11: int = int(rec11.get("handle", -1)) var tgt11: Vector2i = _first_closed_safe(ob11) var v11: Dictionary = ob11.reward_grant().use_treasure(h11, tgt11) @@ -254,7 +254,7 @@ func _init() -> void: if ob11.minesweeper.is_revealed(c): opened_safe = false Kit.check_quiet(mines_shown == 0 or opened_safe, "天眼反例①:危险格仅显示不被开格(棋盘事实不可变)") - var overlay11: Array = ob11.treasure_inventory.query_overlays().get("mine_reveal", []) + var overlay11: Array = ob11.reward_grant().treasure_inventory.query_overlays().get("mine_reveal", []) var overlay_ok := true for c_v in (v11.get("resolved", []) as Array): if not overlay11.has(c_v): @@ -289,7 +289,7 @@ func _init() -> void: "计数链:装配起战(GainJournal 携带)") var inv13 = load("res://core/treasure_items.gd").new(db) var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0) - ob13.treasure_inventory = inv13 + ob13.reward_grant().treasure_inventory = inv13 var h13: int = int(rec13.get("handle", -1)) var tgt13: Vector2i = _first_closed_safe(ob13) if tgt13 != Vector2i(-9, -9): @@ -306,8 +306,13 @@ func _init() -> void: "装配不可变:双件装配冒险启动") Kit.check_quiet(adr14.runner().pick_next_node(int(adr14.runner().next_candidate_ids()[0])), "装配不可变:进首战") var ob14 = adr14.runner().current_battle() - Kit.check_quiet(ob14 != null and (ob14.attunement as Array) == ["morning_order", "wide_scout_band"], + Kit.check_quiet(ob14 != null and (ob14._facts.attunement as Array) == ["morning_order", "wide_scout_band"], "装配不可变:战斗运行态装配集与冒险装配集逐位同源(运行零写口——快照对比)") + # 注册面批同代编排归约 V5:库存注入改址(level_runner 注入改经 reward_grant() 出口直注)后 + # 引用同一性——战斗域经出口持有的库存与关卡域同一实例;use_treasure 受理链经新地址走通 + # 由本套件 ob8/ob11/ob13 三段既有受理链承载(注入面同批改址后全绿即证据)。 + Kit.check_quiet(ob14 != null and ob14.reward_grant().treasure_inventory == adr14.runner().treasure_inventory, + "库存注入改址:战斗域经 reward_grant 出口持有的库存与关卡域同一引用〔编排归约 V5〕") # ⑮ 扩展行收敛(迭代 38 卡④ wr2④⑤——套件首次 feats 注入,V6 形态自 round_flow_skill_pool_test 移植: # enc 带 feats 键+ledger,专长触发经 _judge_seal 真实封印判定管线) @@ -332,7 +337,7 @@ func _init() -> void: Kit.check_quiet(mines15.size() == 3, "封印匠:板上存在三枚危险格(断言前提)") if mines15.size() == 3: for mv in mines15: - ob15._grant_seg._judge_seal({"cell": mv, "hit": true}, 1) + ob15._rounds._grant_seg._judge_seal({"cell": mv, "hit": true}, 1) var smith_row := {} for r in rows15: if String(r.get("effect_id", "")) == "mine_neutralized": @@ -354,7 +359,7 @@ func _init() -> void: cell15b = cv break if cell15b != null: - ob15b._grant_seg._judge_seal({"cell": cell15b, "hit": true}, 1) + ob15b._rounds._grant_seg._judge_seal({"cell": cell15b, "hit": true}, 1) var beacon_row := {} for r in rows15b: if String(r.get("effect_id", "")) == "fx_preview_area":