fix(slice): 战斗阶段七项修复 + 顺手修七项 + 文档口径收编(含全仓语义命名清理)
代码修复(垂直切片,冒烟 193/193 · 三探针全绿): - 遭遇流盐值修正(旧值超 int64 被截断,与 registry 黄金向量不符)+ 黄金向量断言入冒烟 - 完美排雷第二份按身份类型真实兑付;零兑付不发高光行;失败记"奖励未生效"行 - 轮内刷新图 provenance 守卫:刷新图清图不发清图奖励 - 可推理率求解器禁读雷位真值(no-peek)——L1/L2 实测 0.953/0.951、一次生成即达标 - 清图分类改传真实旗集(空旗集捷径会误判批次优先级) - 达标判定回归单一判据(可推理率 ≥ 及格线) - 等待期(Waiting)刷新通道整体拆除——非清图轮恒同图 顺手修:清图轮剩余 AP 当场清零 + 耗尽轮断言;degraded_product 降级标记;产物雷数 ≤N 断言; 明细流水"无来源即炸"及两层豁免成文;主角零值伤害行;窗口段三锚行(13→15 类行协议); 图标键 fail-fast + 补齐暴走/失衡两键。 文档:可选类注入递减基准改"按实际获取件数"、首件保证改"必注入"口径(+未挖到不补发待办); 教学位掷骰口径自洽;标错反噬三池收编(状态表四行→六行);行协议 13→15 与豁免清单回写 《战斗结算》与 registry,下游引用同步收敛。 另含:全仓旧代号→语义命名清理(注释/标识符,81 文件);CLAUDE.md 登记 Godot 路径与验证命令。
This commit is contained in:
1 parent
9cca04c9af
commit
cb548776e0
89 files changed
+2139
-2230
No files matched your search
@@ -54,7 +54,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
|
||||
|
||||
- **v1 在范围内**:D2(10 表 + ConfigDB + RngService)· A1(single_subset 求解器 + 双判据验收)· A2 · A3(AP8/RAP)· A4(迷雾,L2)· B1(七段结算 + 13 类行协议 + 高光双落)· B2(grub/ambusher/伏击池 cap4)· B3(四行状态容器)· B4(install 预分配/F5 保底/道具窗口)· D1-lite 程序化呈现
|
||||
- **v2 追加**:LevelTable(线性节点链数据表)· C1-lite 关卡推进器 `level_runner.gd`(原 c1_level_runner,2026-09-11 领域命名批更名)(关卡域生命周期:共享 B4 关内延续 + HP 跨战斗携带 + 奖励/恢复节点兑付 + 关卡末清库)· Boss 遭遇行(Dmax=4)· 节点奖励 API(B4 `grant_node_active`)· **测试用角色数值调整板**(atk%/hp% 覆盖 + 批量模拟,模拟天赋树——正式侧归 C4)
|
||||
- **裁剪不变**:A5 / C1 路径分支选择(用户拍板线性)· C2–C4 / B5 战斗动画 / D3–D6 / A1 黄金向量与 F4 批量工具 / gdUnit / 标错惩罚(A2 OQ1)
|
||||
- **裁剪不变**:A5 / C1 路径分支选择(用户拍板线性)· C2–C4 / B5 战斗动画 / D3–D6 / A1 黄金向量与 F4 批量工具 / gdUnit · 注:标错惩罚(原 A2 OQ1)已于 2026-09-12 批上线(标空旗三选一确定性反噬:暴走 / 伏击 +2 层 / 失衡,坐标×板序号派生、不走随机流),本批(2026-09-15)已收编进现行文档,不再属裁剪项
|
||||
|
||||
## 2026-09-11 可选类供给控制器(用户拍板批——修改记录)
|
||||
|
||||
@@ -95,6 +95,10 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
|
||||
|
||||
**正式侧义务(GDD/registry 修订清单)**:A3 GDD Rule 11(受理相位 Waiting → Active∪Waiting + 相位机表 + 时间税语义注记)· A2 GDD install 四不变式轮内场景 · B1 GDD 编排专章(轮内换图 + facts 快照协议)· B4 GDD E5/E6(query_full_snapshot/grant 快照直通)· game-concept 轮次流程 + "战斗:扫雷图 = 1:M"措辞(一轮跨多图新形态)· registry `action_economy`(refresh_max_per_round)· 数值连锁重跑(清图支配性条款⑤/获取期望——模拟不覆盖真人刷图行为,需 playtest 实测)。
|
||||
|
||||
**2026-09-15 补记(用户拍板)**:**Waiting(等待结算)相位刷新通道整体移除**——呈现层入口此前已按 OQ12 退役(2026-09-12 战斗屏批),本批拆除核心层残留:账本侧 Waiting 受理入口 `request_refresh()` 删除、编排侧 `can_request_refresh` 不再放行 Waiting、`player_request_refresh` 等待期登记意图分支删除、出口分发删除 `_refresh_used_this_waiting → new_board` 换图分支(非清图轮恒同图)、字段与诊断快照键同步移除。文档侧无需修订——[行动经济](../../design/gdd/systems/排雷循环/行动经济.md)只写挖掘期刷新、[战斗结算](../../design/gdd/systems/战斗循环/战斗结算.md)明文"非清图轮恒同图",拆完代码即双双收敛。冒烟断言改为"等待期刷新请求被拒且状态不变"。
|
||||
|
||||
**同批其它改动(口径读法)**:明细行协议 **13 类 → 15 类**(新增窗口打开锚行 `window_open`、窗口跳过锚行 `window_skip`,服务"窗口段恒在案"验收;单点定义见[战斗结算](../../design/gdd/systems/战斗循环/战斗结算.md));状态表 **四行 → 六行**(暴走 / 失衡两行随标错反噬批收编);达标判定回归**单一判据**(可推理率 ≥ 及格线);求解器禁读雷位真值。本文正文的 v1/v2 范围登记与各历史批条目保留当时陈述,计数以现行文档与代码为准。
|
||||
|
||||
## 2026-09-11 关卡扩展批 · 切片级偏差登记(实现期决策,正式化时须回 GDD/ADR)
|
||||
|
||||
1. **C1 线性推进器 provisional**:节点链来自 LevelTable(结构位 = 未来 C1 的数据承载面);路径分支/奖励点分布定稿权归 C1/C3。节点类型 closed enum {battle, reward, rest, boss};末节点必须 Boss(LevelTable schema 校验)。
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# A3 行动经济:被动账本(AP 硬重置 / 统一价目 / 三路径结束仲裁 cleared > stopped > ap_exhausted /
|
||||
# 行动经济:被动账本(AP 硬重置 / 统一价目 / 三路径结束仲裁 cleared > stopped > ap_exhausted /
|
||||
# RAP 池战斗域 / 双发射源之 stopped 直发)。相位域 = {Idle, Active, Waiting}。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/a3-action-economy.md(系统 ID A3——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/a3-action-economy.md(系统 ID A3·行动经济——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name ActionEconomy
|
||||
extends RefCounted
|
||||
|
||||
signal round_end_stopped(payload: Dictionary) # 双发射源之停手路径(A3 直发;payload 与 A2 棋盘路径同构)
|
||||
signal round_end_stopped(payload: Dictionary) # 双发射源之停手路径(行动经济 直发;payload 与 排雷玩法 棋盘路径同构)
|
||||
|
||||
const PHASE_IDLE := "Idle"
|
||||
const PHASE_ACTIVE := "Active"
|
||||
@@ -25,7 +25,7 @@ var ap: int:
|
||||
var _rap := 0
|
||||
var rap: int:
|
||||
get: return _rap
|
||||
var _last_round_end: Dictionary = {} # {path_tag, continuation_hint}——A2 组包 round_end 的合法数据源
|
||||
var _last_round_end: Dictionary = {} # {path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源
|
||||
var last_round_end: Dictionary:
|
||||
get: return _last_round_end
|
||||
var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start
|
||||
@@ -53,7 +53,7 @@ func _cost(kind: String) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# 通用扣减(A2 用于旗动作;挖握走 request_dig——cleared 仲裁需要 clears_map 参数)
|
||||
# 通用扣减(排雷玩法 用于旗动作;挖握走 request_dig——cleared 仲裁需要 clears_map 参数)
|
||||
func try_spend(action_kind: String) -> bool:
|
||||
if phase != PHASE_ACTIVE:
|
||||
_reject("phase")
|
||||
@@ -61,7 +61,7 @@ func try_spend(action_kind: String) -> bool:
|
||||
return _spend_and_arbitrate(action_kind, false)
|
||||
|
||||
|
||||
# A2 挖掘专用:chain_size = 连锁附加格数(不含首格);clears_map = 本次动作后全图安全格开完
|
||||
# 排雷玩法 挖掘专用:chain_size = 连锁附加格数(不含首格);clears_map = 本次动作后全图安全格开完
|
||||
func request_dig(chain_size: int, clears_map: bool) -> bool:
|
||||
if phase != PHASE_ACTIVE:
|
||||
_reject("phase")
|
||||
@@ -76,7 +76,8 @@ func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool:
|
||||
return false
|
||||
_ap -= cost
|
||||
if clears_map:
|
||||
# cleared 胜出(同帧仲裁 cleared > ap_exhausted;剩余 AP 作废不入 RAP——EC7/EC10)
|
||||
# cleared 胜出(同帧仲裁 cleared > ap_exhausted);剩余 AP 当场清零作废、不进 RAP(EC7/EC10)
|
||||
_ap = 0
|
||||
_finish_round("cleared", "new_board")
|
||||
elif _ap == 0:
|
||||
# AP 归 0 瞬间轮次结束——不存在 0-AP 操作窗口
|
||||
@@ -84,7 +85,7 @@ func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
# 停手:剩余 AP 1:1 转存 RAP;A3 直发 stopped 路径 round_end(双发射源)
|
||||
# 停手:剩余 AP 1:1 转存 RAP;行动经济 直发 stopped 路径 round_end(双发射源)
|
||||
func request_stop() -> bool:
|
||||
if phase != PHASE_ACTIVE:
|
||||
_reject("phase")
|
||||
@@ -96,20 +97,9 @@ func request_stop() -> bool:
|
||||
return true
|
||||
|
||||
|
||||
# 玩家发起的刷新(B1 在 Waiting 编排期代理调用;禁止 RAP 满自动刷)
|
||||
func request_refresh() -> bool:
|
||||
if phase != PHASE_WAITING:
|
||||
_reject("phase")
|
||||
return false
|
||||
if _rap < db.action_economy.rap_refresh_cost:
|
||||
return false # EC13 资源不足拒绝——合法相位内资源拒绝,非编排错误
|
||||
_rap -= db.action_economy.rap_refresh_cost
|
||||
return true
|
||||
|
||||
|
||||
# 轮内刷新(2026-09-12 方案 A 用户拍板):Active 相位受理——仅扣 RAP,相位与 AP 均不动。
|
||||
# 刷新不打断轮次、不触发结算(敌方时间税语义变更已登记 GDD 修订义务);换图编排归 B1
|
||||
# (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 D1 发起)纪律不变。
|
||||
# 轮内刷新(2026-09-12 方案 A 用户拍板;等待期通道 2026-09-15 用户拍板整体移除):唯一入口 =
|
||||
# Active 相位——仅扣 RAP,相位与 AP 均不动。刷新不打断轮次、不触发结算;换图编排归 战斗结算
|
||||
# (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 HUD框架发起)纪律不变。
|
||||
func request_refresh_active() -> bool:
|
||||
if phase != PHASE_ACTIVE:
|
||||
_reject("phase")
|
||||
@@ -120,7 +110,7 @@ func request_refresh_active() -> bool:
|
||||
return true
|
||||
|
||||
|
||||
# B1 回发:结算完成(仅记录编排意图;phase 停留 Waiting——AC-14 门不被绕过)
|
||||
# 战斗结算 回发:结算完成(仅记录编排意图;phase 停留 Waiting——AC-14 门不被绕过)
|
||||
func notify_settlement_done(next_round_mode: String) -> bool:
|
||||
if phase != PHASE_WAITING:
|
||||
_reject("phase")
|
||||
@@ -149,15 +139,19 @@ func notify_combat_end() -> bool:
|
||||
_reject("phase")
|
||||
return false
|
||||
_phase = PHASE_IDLE
|
||||
_rap = 0 # RAP 战斗域清零(用户拍板;D1 可见反馈义务)
|
||||
_rap = 0 # RAP 战斗域清零(用户拍板;HUD框架可见反馈义务)
|
||||
_ap = 0
|
||||
return true
|
||||
|
||||
|
||||
func _finish_round(path_tag: String, continuation_hint: String) -> void:
|
||||
# 实现层断言兜底(行动经济 轮次结束仲裁表):耗尽轮剩余恒为 0;清图轮剩余已当场清零(不进储备池)。
|
||||
# 两断言守在唯一仲裁出口,未来若在扣费与仲裁之间挪动 AP 写入在此红灯。
|
||||
assert(path_tag != "ap_exhausted" or _ap == 0, "耗尽轮剩余行动点恒为 0(实现层断言兜底)")
|
||||
assert(path_tag != "cleared" or _ap == 0, "清图轮剩余行动点当场清零(不进储备池)")
|
||||
_last_round_end = {"path_tag": path_tag, "continuation_hint": continuation_hint}
|
||||
_phase = PHASE_WAITING
|
||||
|
||||
|
||||
func _reject(source: String) -> void:
|
||||
push_warning("[A3] 动作被拒(%s 相位守卫/资源)" % source if source == "phase" else "[A3] 资源不足拒绝")
|
||||
push_warning("[行动经济] 动作被拒(%s 相位守卫/资源)" % source if source == "phase" else "[行动经济] 资源不足拒绝")
|
||||
@@ -1,12 +1,12 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# B1 战斗结算编排中枢:轮次生命周期唯一 owner · 段序骨架(授予→窗口→增益/减益→道具→战斗→衰减→判定)·
|
||||
# 13 类行协议可回放步骤流(零随机零 await)· 高光双落(完美排雷 ×2 / 标中旗 highlight)· 伏击预截流 ·
|
||||
# 战斗结算编排中枢:轮次生命周期唯一 owner · 段序骨架(授予→窗口→增益/减益→道具→战斗→衰减→判定)·
|
||||
# 15 类行协议可回放步骤流(零随机零 await;2026-09-15 窗口锚行批 13→15)· 高光双落(完美排雷 ×2 / 标中旗 highlight)· 伏击预截流 ·
|
||||
# Waiting 相位结算触发(不依赖事件到达序)。
|
||||
# 切片登记:① 事件总线载体 = 直连信号(实现期 ADR 项,provisional)② 多次刷新 EC15 简化为每 Waiting 至多
|
||||
# 一次(UI 禁二次发起;多刷契约义务不触)③ 效果行协议 = 授予段逐 grant 行 + 增益/减益段续存实例组合行。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b1-settlement.md(系统 ID B1——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 切片登记:① 事件总线载体 = 直连信号(实现期 ADR 项,provisional)② Waiting 相位刷新通道 2026-09-15 用户拍板整体
|
||||
# 拆除(仅 Active 挖掘期受理;旧"每 Waiting 至多一次"简化登记随之作废)③ 效果行协议 = 授予段逐 grant 行 + 增益/减益段续存实例组合行。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b1-settlement.md(系统 ID B1·战斗结算——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name BattleOrchestrator
|
||||
extends RefCounted
|
||||
|
||||
@@ -17,18 +17,29 @@ signal round_started(round_index: int)
|
||||
signal board_changed()
|
||||
signal settlement_snapshot(snap: Dictionary) # 每结算末尾的全量状态快照(playtest_logger 消费——JSONL 数值分析)
|
||||
|
||||
# 明细流水行型协议(战斗结算 Core Rule 14;15 类——2026-09-15 窗口锚行批由 13 类扩为 15 类,
|
||||
# 清单改动须同步 battle_log_view 的 _format_row 与本文档头注):
|
||||
# 效果行 effect · 伤害行 damage · 治疗行 heal · 道具行 item · 溢出未生效行 overflow ·
|
||||
# 奖励未生效行 reward_ineffective · 已消耗未生效行 consumed_ineffective · 使用被拒行 use_rejected ·
|
||||
# 标旗未中行 flag_miss · 蓄力行 charge · 衰减行 decay · 判定行 verdict · 高光行 highlight ·
|
||||
# 窗口打开锚行 window_open · 窗口跳过锚行 window_skip(后两类 = 2026-09-15 补的窗口段锚行)
|
||||
# 行来源断言豁免集合(行型层)——判定段 / 衰减段 / 窗口锚行本就无棋盘来源,勿一刀切误炸
|
||||
const SOURCE_EXEMPT_TYPES := ["decay", "verdict", "window_open", "window_skip"]
|
||||
# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图 / 库存件而非棋盘格
|
||||
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "item"]
|
||||
|
||||
var db
|
||||
var rng
|
||||
# E1 封装收口(2026-09-13):协作对象 a1/b2/b3/b4 私有化——跨层读取一律经 presentation_snapshot()/
|
||||
# diagnostic_snapshot() 成文出口(呈现层链式穿透 b1._b2.hero_unit() 的根因收口)。
|
||||
# a2/a3 保持公有 = 架构文档 API Boundaries 的 D1 消费面(意图提交 settle_batch + compute_move_path
|
||||
# E1 封装收口(2026-09-13):协作对象 board_generator/enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/
|
||||
# diagnostic_snapshot() 成文出口(呈现层链式穿透 settlement._enemy_system.hero_unit() 的根因收口)。
|
||||
# minesweeper/action_economy 保持公有 = 架构文档 API Boundaries 的 HUD框架消费面(意图提交 settle_batch + compute_move_path
|
||||
# 只读;ap/rap/相位读取)——非本文档登记的契约面,勿增新用途。
|
||||
var _a1: BoardGenerator
|
||||
var a2: MinesweeperRules
|
||||
var a3: ActionEconomy
|
||||
var _b2: UnitContainer
|
||||
var _b3: StatusContainer
|
||||
var _b4: RewardEconomy
|
||||
var _board_generator: BoardGenerator
|
||||
var minesweeper: MinesweeperRules
|
||||
var action_economy: ActionEconomy
|
||||
var _enemy_system: UnitContainer
|
||||
var _status_effects: StatusContainer
|
||||
var _reward_economy: RewardEconomy
|
||||
|
||||
var encounter: Dictionary = {}
|
||||
var round_index := 0
|
||||
@@ -40,9 +51,9 @@ var _facts_path := ""
|
||||
var _settling := false
|
||||
var _awaiting_window := false
|
||||
var _window_items: Array = []
|
||||
var _refresh_used_this_waiting := false
|
||||
var _refresh_used_this_round := 0 # 每轮刷新计数(Active+Waiting 合计,兜底上限 = ActionEconomyTable.refresh_max_per_round)
|
||||
var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_max_per_round)
|
||||
var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
|
||||
var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励)
|
||||
var _flag_consumed := {} # 兑付资格以雷格实例为键(板级;换图重置)
|
||||
var _flag_miss_seen := {} # 标旗未中行去重(每雷格实例一次)
|
||||
var _perfect_hit_true := 0 # 板级完美排雷计数(install 重置)
|
||||
@@ -54,38 +65,38 @@ var _charge_dmg_a := 0 # 伤害A 充能:本轮开出的资
|
||||
var _charge_dmg_b := 0 # 伤害B 充能:本轮首次标中雷计数 ×damage_b_per_flag(每雷实例一次)
|
||||
|
||||
|
||||
func _init(db_ref, rng_ref, shared_b4 = null) -> void:
|
||||
func _init(db_ref, rng_ref, shared_reward_economy = null) -> void:
|
||||
db = db_ref
|
||||
rng = rng_ref
|
||||
_a1 = BoardGenerator.new()
|
||||
a3 = ActionEconomy.new(db)
|
||||
a2 = MinesweeperRules.new(db, a3)
|
||||
_b2 = UnitContainer.new(db)
|
||||
_b3 = StatusContainer.new(db)
|
||||
# 库存关卡域(2026-09-11 关内延续):连续关卡模式注入共享 B4 容器,库存跨战斗延续
|
||||
_b4 = shared_b4 if shared_b4 != null else RewardEconomy.new(db, rng)
|
||||
a2.cell_revealed.connect(_on_cell_revealed)
|
||||
a2.flag_judged.connect(_on_flag_judged)
|
||||
a2.round_end_board.connect(_on_round_end)
|
||||
a3.round_end_stopped.connect(_on_round_end)
|
||||
_board_generator = BoardGenerator.new()
|
||||
action_economy = ActionEconomy.new(db)
|
||||
minesweeper = MinesweeperRules.new(db, action_economy)
|
||||
_enemy_system = UnitContainer.new(db)
|
||||
_status_effects = StatusContainer.new(db)
|
||||
# 库存关卡域(2026-09-11 关内延续):连续关卡模式注入共享 奖励道具经济 容器,库存跨战斗延续
|
||||
_reward_economy = shared_reward_economy if shared_reward_economy != null else RewardEconomy.new(db, rng)
|
||||
minesweeper.cell_revealed.connect(_on_cell_revealed)
|
||||
minesweeper.flag_judged.connect(_on_flag_judged)
|
||||
minesweeper.round_end_board.connect(_on_round_end)
|
||||
action_economy.round_end_stopped.connect(_on_round_end)
|
||||
|
||||
|
||||
# ---- 战斗开始序列(编排专章①:门 → B3 → B2 → install+roll → A3)----
|
||||
# ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)----
|
||||
|
||||
func begin_battle(enc: Dictionary) -> bool:
|
||||
if round_index != 0:
|
||||
push_warning("[B1] begin_battle 重复受理")
|
||||
push_warning("[战斗结算] begin_battle 重复受理")
|
||||
return false
|
||||
encounter = enc
|
||||
db.set_reload_gate(true)
|
||||
if not _b3.notify_battle_start():
|
||||
_fatal("B3 开域被拒(编排违例)")
|
||||
if not _status_effects.notify_battle_start():
|
||||
_fatal("增益状态 开域被拒(编排违例)")
|
||||
return false
|
||||
_b4.notify_battle_start() # 可选类供给控制器战斗域清零(2026-09-11 供给控制器批——首件保证按战重置)
|
||||
# 遭遇骰(C3 GDD Core Rule 8——2026-09-12 用户拍板):行有 unit_count_max 时按 [unit_count, max] 均匀骰;
|
||||
_reward_economy.notify_battle_start() # 可选类供给控制器战斗域清零(2026-09-11 供给控制器批——首件保证按战重置)
|
||||
# 遭遇骰(难度曲线与Boss GDD Core Rule 8——2026-09-12 用户拍板):行有 unit_count_max 时按 [unit_count, max] 均匀骰;
|
||||
# 2026-09-14 敌方攻击区间批扩展:行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值
|
||||
# (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后);
|
||||
# 挖掘决策域之外——P1 合规;B2 零随机纪律保持(收 override 即确定值)。
|
||||
# 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。
|
||||
var erow_pre: Dictionary = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth))
|
||||
var roll_lo: int = int(erow_pre.get("unit_count", 1))
|
||||
var roll_hi: int = int(erow_pre.get("unit_count_max", roll_lo))
|
||||
@@ -101,9 +112,9 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
rolled_atk = int(erow_pre.atk_min) + estream.randi_range(0, int(erow_pre.atk_max) - int(erow_pre.atk_min))
|
||||
# E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径)
|
||||
_hero_atk = int(enc.get("hero_atk", int(db.hero.atk)))
|
||||
if not _b2.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), rolled,
|
||||
if not _enemy_system.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), rolled,
|
||||
_hero_atk, int(enc.get("hero_max_hp", -1)), rolled_atk):
|
||||
_fatal("B2 spawn 被拒")
|
||||
_fatal("敌人系统 spawn 被拒")
|
||||
return false
|
||||
var erow: Dictionary = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth))
|
||||
_enemy_cap = int(erow.get("advantage_per_round_cap", 0))
|
||||
@@ -111,8 +122,8 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
if not _install_new_board():
|
||||
return false
|
||||
_reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(reward_eligible 恒 false 的双保险)
|
||||
if not a3.notify_round_start():
|
||||
_fatal("A3 首轮 notify_round_start 被拒")
|
||||
if not action_economy.notify_round_start():
|
||||
_fatal("行动经济 首轮 notify_round_start 被拒")
|
||||
return false
|
||||
round_index = 1
|
||||
round_started.emit(round_index)
|
||||
@@ -121,14 +132,14 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
|
||||
func _install_new_board(during_round := false) -> bool:
|
||||
var stream: RandomNumberGenerator = rng.open_stream("board_gen")
|
||||
var board: BoardData = _a1.generate(int(encounter.level_index), int(encounter.node_depth), stream, db)
|
||||
var board: BoardData = _board_generator.generate(int(encounter.level_index), int(encounter.node_depth), stream, db)
|
||||
if board == null:
|
||||
return false
|
||||
var res: Dictionary = a2.install_board(board, during_round)
|
||||
var res: Dictionary = minesweeper.install_board(board, during_round)
|
||||
if not res.accepted:
|
||||
_fatal("A2 install 拒绝(不变式违例): %s" % str(res))
|
||||
_fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res))
|
||||
return false
|
||||
# B4 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = S ∖ R₀
|
||||
# 奖励道具经济 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = S ∖ R₀
|
||||
var r0 := {}
|
||||
for c in board.compute_r0():
|
||||
r0[c] = true
|
||||
@@ -137,13 +148,14 @@ func _install_new_board(during_round := false) -> bool:
|
||||
if r0.has(c):
|
||||
continue
|
||||
eligible.append({"cell": c, "fog": board.variant_ids(c).size() > 0})
|
||||
if not _b4.roll_board(eligible):
|
||||
_fatal("B4 roll_board 被拒")
|
||||
if not _reward_economy.roll_board(eligible):
|
||||
_fatal("奖励道具经济 roll_board 被拒")
|
||||
return false
|
||||
_flag_consumed.clear() # 换图新雷 = 新实例,资格自然重置(A3 Rule 8)
|
||||
_flag_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8)
|
||||
_flag_miss_seen.clear()
|
||||
_perfect_hit_true = 0 # 板级完美计数随 install 重置
|
||||
_perfect_hit_false = 0
|
||||
_board_from_midround_refresh = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false
|
||||
_board_seq += 1
|
||||
board_changed.emit()
|
||||
return true
|
||||
@@ -159,7 +171,7 @@ func _on_cell_revealed(evt: Dictionary) -> void:
|
||||
if evt.kind == "safe" and bool(evt.get("reward_eligible", false)):
|
||||
# 身份快照随缓冲捕获(批次隔离——轮内刷新后 mapping 已换批,授予必须用缓冲期身份,
|
||||
# 防旧图已挖格在轮末错拿新图同坐标身份)
|
||||
var snap = _b4.query_full_snapshot(evt.cell)
|
||||
var snap = _reward_economy.query_full_snapshot(evt.cell)
|
||||
if snap != null:
|
||||
evt = evt.duplicate()
|
||||
evt["reward_snapshot"] = snap
|
||||
@@ -178,7 +190,7 @@ func _on_round_end(_payload: Dictionary) -> void:
|
||||
func _maybe_settle() -> void:
|
||||
if _settling or battle_result != "":
|
||||
return
|
||||
if a3.phase != a3.PHASE_WAITING:
|
||||
if action_economy.phase != action_economy.PHASE_WAITING:
|
||||
return
|
||||
_settle()
|
||||
|
||||
@@ -196,7 +208,7 @@ func _settle() -> void:
|
||||
elif evt.kind == "safe" and evt.reward_eligible:
|
||||
_charge_dmg_a += 1 # 伤害A 充能:探索本身计费,与该格奖励是否兑付成功无关(初始展开格 reward_eligible=false 不计)
|
||||
_grant_cell(evt.cell, ri, "safe_cell", evt.get("reward_snapshot"))
|
||||
# reward_eligible=false(initial 通道)不入库——B1 义务②(A3 EC5 委派闭环)
|
||||
# reward_eligible=false(initial 通道)不入库——战斗结算 义务②(行动经济 EC5 委派闭环)
|
||||
for evt in _facts_flags:
|
||||
_judge_flag(evt, ri)
|
||||
if not mine_cells.is_empty():
|
||||
@@ -206,22 +218,26 @@ func _settle() -> void:
|
||||
# ② 道具使用窗口(授予段后、增益/减益段前——当轮拾取当轮可用)
|
||||
_window_items = _assemble_window()
|
||||
if not _window_items.is_empty():
|
||||
# 窗口打开锚行(战斗结算 验收:"窗口段恒在案")——零棋盘来源,行型豁免
|
||||
_emit_row("window_open", "", Vector2i(-1, -1), ri, "道具窗口(%d 件可选)" % _window_items.size(), _window_items.size(), {})
|
||||
_awaiting_window = true
|
||||
window_opened.emit(_window_items)
|
||||
_settling = false
|
||||
return # 窗口选择序列 = 结算回放输入事件
|
||||
# 空库存不弹窗,但窗口段仍要在案("空库存记跳过")——锚行不可省
|
||||
_emit_row("window_skip", "", Vector2i(-1, -1), ri, "道具窗口跳过(空库存)", 0, {})
|
||||
_finish_settlement([])
|
||||
|
||||
|
||||
func _assemble_window() -> Array:
|
||||
# 窗口数据组装 = B4 query_inventory 投影 + B1 侧守卫预判 usable_now(B4 零跨系统读不破)
|
||||
# 窗口数据组装 = 奖励道具经济 query_inventory 投影 + 战斗结算 侧守卫预判 usable_now(奖励道具经济 零跨系统读不破)
|
||||
var out: Array = []
|
||||
for inst in _b4.query_inventory():
|
||||
for inst in _reward_economy.query_inventory():
|
||||
if inst.status != "InStock":
|
||||
continue
|
||||
var usable := true
|
||||
if inst.category == "heal_pack":
|
||||
usable = int(_b2.hero_unit().hp) < int(_b2.hero_unit().max_hp) # 满血治疗包禁选
|
||||
usable = int(_enemy_system.hero_unit().hp) < int(_enemy_system.hero_unit().max_hp) # 满血治疗包禁选
|
||||
out.append({
|
||||
"index": inst.index,
|
||||
"item_id": inst.item_id,
|
||||
@@ -237,18 +253,22 @@ func _assemble_window() -> Array:
|
||||
|
||||
func submit_window_selection(selection: Array) -> void:
|
||||
if not _awaiting_window:
|
||||
push_warning("[B1] 无窗口期的选择提交(编排违例)")
|
||||
push_warning("[战斗结算] 无窗口期的选择提交(编排违例)")
|
||||
return
|
||||
_awaiting_window = false
|
||||
_settling = true
|
||||
if selection.size() > 1:
|
||||
push_warning("[B1] 窗口至多选用一件(2026-09-11 用户拍板——避免结算复杂度)——超出部分忽略")
|
||||
_finish_settlement(selection.slice(0, 1))
|
||||
push_warning("[战斗结算] 窗口至多选用一件(2026-09-11 用户拍板——避免结算复杂度)——超出部分忽略")
|
||||
var picked: Array = selection.slice(0, 1)
|
||||
if picked.is_empty():
|
||||
# 零选择跳过锚行(战斗结算 验收:"零选择记跳过")——窗口段在案的第二半
|
||||
_emit_row("window_skip", "", Vector2i(-1, -1), round_index, "道具窗口跳过(未选用)", 0, {})
|
||||
_finish_settlement(picked)
|
||||
|
||||
|
||||
func _finish_settlement(selection: Array) -> void:
|
||||
var ri := round_index
|
||||
# ③ 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——B3 义务⑦)
|
||||
# ③ 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦)
|
||||
_emit_composition_effects("hero", ri)
|
||||
_emit_composition_effects("enemy", ri)
|
||||
# ④ 道具段(窗口选定序逐件生效;逐件动态守卫)
|
||||
@@ -257,13 +277,13 @@ func _finish_settlement(selection: Array) -> void:
|
||||
# ⑤ 战斗段(调用序钉死 query_attack → apply_damage → advance;hero hp=0 短路)
|
||||
_combat_segment(ri)
|
||||
# ⑥ 衰减段(恰一次恒执行含末轮——先兑付后衰减)
|
||||
var k: int = _b3.decay_round()
|
||||
var k: int = _status_effects.decay_round()
|
||||
_emit_row("decay", "", Vector2i(-1, -1), ri, "状态衰减", k, {}, "")
|
||||
# ⑦ 判定段(防御性优先级 hero 先判——双死结构不可达的纵深防御)
|
||||
var verdict := "continue"
|
||||
if int(_b2.hero_unit().get("hp", 0)) <= 0:
|
||||
if int(_enemy_system.hero_unit().get("hp", 0)) <= 0:
|
||||
verdict = "defeat"
|
||||
elif _b2.query_alive("enemy").is_empty():
|
||||
elif _enemy_system.query_alive("enemy").is_empty():
|
||||
verdict = "victory"
|
||||
_emit_settlement_snapshot(verdict) # 全量状态快照(despawn 前组装——终局含 result)
|
||||
if verdict == "defeat":
|
||||
@@ -283,55 +303,57 @@ func _finish_settlement(selection: Array) -> void:
|
||||
|
||||
# snap_override 非空 = 冻结快照授予(轮内刷新批——缓冲期身份,绕过现行 mapping/_granted)
|
||||
func _grant_cell(cell: Vector2i, ri: int, source_class: String, snap_override = null) -> void:
|
||||
var snap = snap_override if snap_override != null else _b4.grant(cell)
|
||||
var snap = snap_override if snap_override != null else _reward_economy.grant(cell)
|
||||
if not (snap is Dictionary):
|
||||
return
|
||||
if snap.category == "buff":
|
||||
var ok: bool = _b3.apply_effect(snap.effect_id, {"source_class": source_class, "origin_cell": cell, "round_index": ri})
|
||||
var ok: bool = _status_effects.apply_effect(snap.effect_id, {"source_class": source_class, "origin_cell": cell, "round_index": ri})
|
||||
if ok:
|
||||
var row: Dictionary = db.status_effect_row(snap.effect_id)
|
||||
var pool := _pool_effective(snap.effect_id)
|
||||
_emit_row("effect", source_class, cell, ri, String(row.display_name), int(row.magnitude), {"effect_id": snap.effect_id, "pool_value": pool})
|
||||
else:
|
||||
_emit_row("reward_ineffective", source_class, cell, ri, "奖励未生效(满层)", 0, {"batch_key": String(snap.effect_id)})
|
||||
elif _resolve_instant(snap, source_class, cell, ri):
|
||||
pass # 即时类:授予段当场兑付(不进库存、零决策——2026-09-11 两类奖励裁决)
|
||||
elif _resolve_instant(snap, source_class, cell, ri) != "not_instant":
|
||||
pass # 即时类:授予段当场兑付(兑付行/零兑付未生效行均由 _resolve_instant 自记——2026-09-11 两类奖励裁决)
|
||||
else:
|
||||
_b4.stock(snap, cell, ri)
|
||||
_reward_economy.stock(snap, cell, ri)
|
||||
_emit_row("item", source_class, cell, ri, "%s 入库" % snap.display_name, int(snap.power), {})
|
||||
|
||||
|
||||
# 即时类当场兑付(2026-09-11 两类奖励裁决):instant_aoe = 全体存活敌各 power 点;
|
||||
# instant_heal = 主角 +power(clamp,满血 → "奖励未生效"行)。返回 false = 非即时类(道具类走库存)
|
||||
func _resolve_instant(snap: Dictionary, source_class: String, cell: Vector2i, ri: int, suffix: String = "", highlight: String = "") -> bool:
|
||||
# instant_heal = 主角 +power(clamp,满血 → "奖励未生效"行)。
|
||||
# 返回 "redeemed" = 实际有效兑付 / "zero" = 零兑付(奖励未生效行已记)/ "not_instant" = 非即时类(道具类走库存)。
|
||||
# 零兑付必须可区分:完美排雷高光行只记实际有效兑付,首份即零兑付则高光行不发(战斗结算 授予段规则)。
|
||||
func _resolve_instant(snap: Dictionary, source_class: String, cell: Vector2i, ri: int, suffix: String = "", highlight: String = "") -> String:
|
||||
if snap.category == "instant_aoe":
|
||||
var alive: Array = _b2.query_alive("enemy")
|
||||
var alive: Array = _enemy_system.query_alive("enemy")
|
||||
if alive.is_empty():
|
||||
_emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(无存活敌人)%s" % [snap.display_name, suffix], 0, {"batch_key": "instant_aoe"}, highlight)
|
||||
return true
|
||||
return "zero"
|
||||
for u in alive:
|
||||
_b2.apply_damage(u, int(snap.power)) # 恒定表值,逐个;受击即死——死亡即不动手
|
||||
_enemy_system.apply_damage(u, int(snap.power)) # 恒定表值,逐个;受击即死——死亡即不动手
|
||||
_emit_row("damage", source_class, cell, ri, "%s → 全体敌人 ×%d%s" % [snap.display_name, alive.size(), suffix], int(snap.power), {"target": "all_enemies", "count": alive.size()}, highlight)
|
||||
return true
|
||||
return "redeemed"
|
||||
if snap.category == "instant_heal":
|
||||
if int(_b2.hero_unit().hp) >= int(_b2.hero_unit().max_hp):
|
||||
if int(_enemy_system.hero_unit().hp) >= int(_enemy_system.hero_unit().max_hp):
|
||||
_emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(满血)%s" % [snap.display_name, suffix], 0, {"batch_key": "instant_heal"}, highlight)
|
||||
return true
|
||||
var healed: int = _b2.heal(_b2.hero_unit(), int(snap.power)) # clamp 归 B1
|
||||
return "zero"
|
||||
var healed: int = _enemy_system.heal(_enemy_system.hero_unit(), int(snap.power)) # clamp 归 战斗结算
|
||||
_emit_row("heal", source_class, cell, ri, "%s 回复%s" % [snap.display_name, suffix], healed, {"target": "hero"}, highlight)
|
||||
return true
|
||||
return false
|
||||
return "redeemed"
|
||||
return "not_instant"
|
||||
|
||||
|
||||
func _judge_flag(evt: Dictionary, ri: int) -> void:
|
||||
var cell: Vector2i = evt.cell
|
||||
if evt.hit:
|
||||
if _flag_consumed.has(cell):
|
||||
return # 去重跳过(资格以雷格实例为键——A2 诚实重复上报的对侧);零明细行
|
||||
return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行
|
||||
_flag_consumed[cell] = true
|
||||
_perfect_hit_true += 1
|
||||
_charge_dmg_b += int(db.settlement.damage_b_per_flag) # 伤害B 充能:与 weaken 消费同键(每雷实例一次——忘拔旗不重复充能)
|
||||
var ok: bool = _b3.apply_effect("weaken", {"source_class": "flag_hit", "origin_cell": cell, "round_index": ri})
|
||||
var ok: bool = _status_effects.apply_effect("weaken", {"source_class": "flag_hit", "origin_cell": cell, "round_index": ri})
|
||||
if ok:
|
||||
var row: Dictionary = db.status_effect_row("weaken")
|
||||
_emit_row("effect", "flag_hit", cell, ri, String(row.display_name), int(row.magnitude), {"effect_id": "weaken", "pool_value": _pool_effective("weaken")}, "flag_hit")
|
||||
@@ -357,7 +379,7 @@ func _apply_miss_backlash(cell: Vector2i, ri: int) -> void:
|
||||
var src: Dictionary = {"source_class": "flag_miss", "origin_cell": cell, "round_index": ri}
|
||||
match kind:
|
||||
0:
|
||||
if _b3.apply_effect("enrage", src):
|
||||
if _status_effects.apply_effect("enrage", src):
|
||||
var row: Dictionary = db.status_effect_row("enrage")
|
||||
_emit_row("effect", "flag_miss", cell, ri, "反噬·%s(敌方)" % String(row.display_name), int(row.magnitude), {"effect_id": "enrage", "pool_value": _pool_effective("enrage")})
|
||||
else:
|
||||
@@ -365,14 +387,14 @@ func _apply_miss_backlash(cell: Vector2i, ri: int) -> void:
|
||||
1:
|
||||
var granted := 0
|
||||
for i in range(2):
|
||||
if _b3.apply_effect("ambush", src):
|
||||
if _status_effects.apply_effect("ambush", src):
|
||||
granted += 1
|
||||
if granted > 0:
|
||||
_emit_row("effect", "flag_miss", cell, ri, "反噬·敌方伏击 +%d 层" % granted, granted, {"effect_id": "ambush", "pool_value": _b3.pool_layers("ambush") * _m_amb})
|
||||
_emit_row("effect", "flag_miss", cell, ri, "反噬·敌方伏击 +%d 层" % granted, granted, {"effect_id": "ambush", "pool_value": _status_effects.pool_layers("ambush") * _m_amb})
|
||||
else:
|
||||
_emit_row("reward_ineffective", "flag_miss", cell, ri, "反噬未生效(满层)", 0, {"batch_key": "ambush"})
|
||||
2:
|
||||
if _b3.apply_effect("off_guard", src):
|
||||
if _status_effects.apply_effect("off_guard", src):
|
||||
var row2: Dictionary = db.status_effect_row("off_guard")
|
||||
_emit_row("effect", "flag_miss", cell, ri, "反噬·%s(我方)" % String(row2.display_name), int(row2.magnitude), {"effect_id": "off_guard", "pool_value": _pool_effective("off_guard")})
|
||||
else:
|
||||
@@ -380,13 +402,13 @@ func _apply_miss_backlash(cell: Vector2i, ri: int) -> void:
|
||||
|
||||
|
||||
func _convert_ambush(mine_cells: Array, ri: int) -> void:
|
||||
# B2 义务②⑥:预截流计划面 → 前 granted 颗逐颗转化(事件规范序);每颗雷恰一次
|
||||
var plan: Dictionary = _b2.ambush_grant_plan(mine_cells.size(), _enemy_cap)
|
||||
# 敌人系统 义务②⑥:预截流计划面 → 前 granted 颗逐颗转化(事件规范序);每颗雷恰一次
|
||||
var plan: Dictionary = _enemy_system.ambush_grant_plan(mine_cells.size(), _enemy_cap)
|
||||
for i in range(plan.granted):
|
||||
var cell: Vector2i = mine_cells[i]
|
||||
var ok: bool = _b3.apply_effect("ambush", {"source_class": "mine_hit", "origin_cell": cell, "round_index": ri})
|
||||
var ok: bool = _status_effects.apply_effect("ambush", {"source_class": "mine_hit", "origin_cell": cell, "round_index": ri})
|
||||
if ok:
|
||||
var layers := _b3.pool_layers("ambush")
|
||||
var layers := _status_effects.pool_layers("ambush")
|
||||
_emit_row("effect", "mine_hit", cell, ri, "伏击", _m_amb, {"effect_id": "ambush", "pool_value": layers * _m_amb})
|
||||
for i in range(plan.granted, plan.granted + plan.overflow):
|
||||
var cell: Vector2i = mine_cells[i]
|
||||
@@ -394,25 +416,37 @@ func _convert_ambush(mine_cells: Array, ri: int) -> void:
|
||||
|
||||
|
||||
func _grant_clear_reward(ri: int) -> void:
|
||||
var snap = _b4.grant_clear()
|
||||
if _board_from_midround_refresh:
|
||||
return # 轮内刷新换来的新图不发清图奖励(行动经济/战斗结算 明文——刷新图不是清图挣来的)
|
||||
var snap = _reward_economy.grant_clear()
|
||||
if not (snap is Dictionary):
|
||||
return
|
||||
var perfect: bool = _perfect_hit_false == 0 and _perfect_hit_true >= 1
|
||||
# 首份兑付
|
||||
# 首份兑付(first_ok = 实际有效兑付——零兑付/被拒均不算)
|
||||
var first_ok := _grant_clear_identity(snap, ri)
|
||||
# 完美排雷:真实双份交付(第二份走既有授予通道;grant_clear 本身仍恰一次)
|
||||
# 完美排雷:真实双份交付(第二份按身份类型走真实兑付——即时类重走 _resolve_instant;
|
||||
# 增益类重授容器;道具类入库。grant_clear 本身仍恰一次)
|
||||
var second_ok := false
|
||||
if perfect and first_ok:
|
||||
if snap.category == "buff":
|
||||
second_ok = _b3.apply_effect(snap.effect_id, {"source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "round_index": ri})
|
||||
second_ok = _status_effects.apply_effect(snap.effect_id, {"source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "round_index": ri})
|
||||
if second_ok:
|
||||
var row: Dictionary = db.status_effect_row(snap.effect_id)
|
||||
_emit_row("effect", "clear_reward", Vector2i(-1, -1), ri, "%s(完美第二份)" % String(row.display_name), int(row.magnitude), {"effect_id": snap.effect_id, "pool_value": _pool_effective(snap.effect_id)})
|
||||
else:
|
||||
_emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "完美第二份未生效(满层)", 0, {"batch_key": String(snap.effect_id)})
|
||||
else:
|
||||
var s2 = _b4.stock(snap, Vector2i(-1, -1), ri)
|
||||
second_ok = s2 is Dictionary
|
||||
if second_ok:
|
||||
_emit_row("item", "clear_reward", Vector2i(-1, -1), ri, "%s 入库(完美第二份)" % snap.display_name, int(snap.power), {})
|
||||
var res2 := _resolve_instant(snap, "clear_reward", Vector2i(-1, -1), ri, "(完美第二份)")
|
||||
if res2 == "redeemed":
|
||||
second_ok = true
|
||||
elif res2 == "not_instant":
|
||||
var s2 = _reward_economy.stock(snap, Vector2i(-1, -1), ri)
|
||||
second_ok = s2 is Dictionary
|
||||
if second_ok:
|
||||
_emit_row("item", "clear_reward", Vector2i(-1, -1), ri, "%s 入库(完美第二份)" % snap.display_name, int(snap.power), {})
|
||||
else:
|
||||
_emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "完美第二份未生效(入库被拒)", 0, {"batch_key": String(snap.reward_id)})
|
||||
# res2 == "zero":零兑付的奖励未生效行已由 _resolve_instant 自记
|
||||
if perfect and first_ok:
|
||||
# 高光行 value = 实际有效兑付份数(禁记名义值;首份满层零兑付 → 降级不发)
|
||||
_emit_row("highlight", "clear_reward", Vector2i(-1, -1), ri, "完美排雷!清图奖励 ×2", (2 if second_ok else 1), {}, "perfect_sweep")
|
||||
@@ -420,17 +454,19 @@ func _grant_clear_reward(ri: int) -> void:
|
||||
|
||||
func _grant_clear_identity(snap: Dictionary, ri: int) -> bool:
|
||||
if snap.category == "buff":
|
||||
var ok: bool = _b3.apply_effect(snap.effect_id, {"source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "round_index": ri})
|
||||
var ok: bool = _status_effects.apply_effect(snap.effect_id, {"source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "round_index": ri})
|
||||
if ok:
|
||||
var row: Dictionary = db.status_effect_row(snap.effect_id)
|
||||
_emit_row("effect", "clear_reward", Vector2i(-1, -1), ri, String(row.display_name), int(row.magnitude), {"effect_id": snap.effect_id, "pool_value": _pool_effective(snap.effect_id)}, "clear_reward_h2")
|
||||
return true
|
||||
_emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "清图奖励未生效(满层)", 0, {"batch_key": snap.effect_id})
|
||||
return false
|
||||
if _resolve_instant(snap, "clear_reward", Vector2i(-1, -1), ri, "(清图奖励)", "clear_reward_h2"):
|
||||
return true
|
||||
var s = _b4.stock(snap, Vector2i(-1, -1), ri)
|
||||
var res := _resolve_instant(snap, "clear_reward", Vector2i(-1, -1), ri, "(清图奖励)", "clear_reward_h2")
|
||||
if res != "not_instant":
|
||||
return res == "redeemed" # 即时类零兑付(无存活敌/满血)= 未生效——首份即零兑付则高光行不发
|
||||
var s = _reward_economy.stock(snap, Vector2i(-1, -1), ri)
|
||||
if not (s is Dictionary):
|
||||
_emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "清图奖励未生效(入库被拒)", 0, {"batch_key": String(snap.reward_id)})
|
||||
return false
|
||||
_emit_row("item", "clear_reward", Vector2i(-1, -1), ri, "%s 入库(清图奖励)" % snap.display_name, int(snap.power), {}, "clear_reward_h2")
|
||||
return true
|
||||
@@ -443,29 +479,29 @@ func _apply_selected_item(window_index: int, ri: int) -> void:
|
||||
return
|
||||
var item: Dictionary = _window_items[window_index]
|
||||
if item.category == "heal_pack":
|
||||
# 逐件动态守卫:前件推满后本件拦截(不烧包——B1 不调 use)
|
||||
if int(_b2.hero_unit().hp) >= int(_b2.hero_unit().max_hp):
|
||||
# 逐件动态守卫:前件推满后本件拦截(不烧包——战斗结算 不调 use)
|
||||
if int(_enemy_system.hero_unit().hp) >= int(_enemy_system.hero_unit().max_hp):
|
||||
_emit_row("use_rejected", "item", item.source_cell, ri, "%s 被拒(满血)" % item.display_name, 0, {})
|
||||
return
|
||||
var receipt = _b4.use(item.index)
|
||||
var receipt = _reward_economy.use(item.index)
|
||||
if not (receipt is Dictionary):
|
||||
return
|
||||
var healed: int = _b2.heal(_b2.hero_unit(), receipt.power) # clamp 归 B1
|
||||
var healed: int = _enemy_system.heal(_enemy_system.hero_unit(), receipt.power) # clamp 归 战斗结算
|
||||
_emit_row("heal", "item", receipt.source_cell, ri, "%s 回复" % item.display_name, healed, {"target": "hero"})
|
||||
else:
|
||||
# 伤害道具:恒定表值不吃修正;零效果判定读 B2 存活集(照常消耗——E10)
|
||||
if _b2.query_alive("enemy").is_empty():
|
||||
var receipt0 = _b4.use(item.index)
|
||||
# 伤害道具:恒定表值不吃修正;零效果判定读 敌人系统 存活集(照常消耗——E10)
|
||||
if _enemy_system.query_alive("enemy").is_empty():
|
||||
var receipt0 = _reward_economy.use(item.index)
|
||||
if not (receipt0 is Dictionary):
|
||||
return
|
||||
_emit_row("consumed_ineffective", "item", receipt0.source_cell, ri, "已消耗未生效(无存活敌人)", 0, {})
|
||||
return
|
||||
var receipt = _b4.use(item.index)
|
||||
var receipt = _reward_economy.use(item.index)
|
||||
if not (receipt is Dictionary):
|
||||
return
|
||||
var target = _b2.default_target()
|
||||
var target = _enemy_system.default_target()
|
||||
var killed_before: bool = not target.alive
|
||||
_b2.apply_damage(target, receipt.power)
|
||||
_enemy_system.apply_damage(target, receipt.power)
|
||||
var killed: bool = not killed_before and not target.alive
|
||||
_emit_row("damage", "item", receipt.source_cell, ri, "%s 命中" % item.display_name, receipt.power, {"target": target.display_name, "killed": killed})
|
||||
|
||||
@@ -473,11 +509,11 @@ func _apply_selected_item(window_index: int, ri: int) -> void:
|
||||
# ---- 战斗段 ----
|
||||
|
||||
func _combat_segment(ri: int) -> void:
|
||||
var L := _b3.pool_layers("ambush") # 授予段后的当轮值;池共享不消耗
|
||||
var L := _status_effects.pool_layers("ambush") # 授予段后的当轮值;池共享不消耗
|
||||
# ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动)。
|
||||
# 三段伤害构成(2026-09-12 用户拍板):基础(atk×(1+attack_up),不吃克制)+ 伤害A(开格充能×克制)+ 伤害B(标雷充能×克制),
|
||||
# 按目标护甲类型分别乘克制系数后 floor 相加;attack_up 仍只乘基础攻击(刀/AOE 恒定表值不吃修正不变)。
|
||||
var hero_target = _b2.default_target()
|
||||
var hero_target = _enemy_system.default_target()
|
||||
if hero_target != null:
|
||||
var armor: String = String(hero_target.get("armor_type", "none"))
|
||||
var up := clampi(_pool_effective("attack_up"), int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_hi))
|
||||
@@ -486,13 +522,17 @@ func _combat_segment(ri: int) -> void:
|
||||
var comp_b := _charge_damage_component("dmg_b", _charge_dmg_b, armor)
|
||||
var d_hero: int = base + comp_a + comp_b
|
||||
if d_hero > 0:
|
||||
_b2.apply_damage(hero_target, d_hero)
|
||||
_enemy_system.apply_damage(hero_target, d_hero)
|
||||
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, "主角攻击", d_hero, {"target": hero_target.display_name, "killed": not hero_target.alive, "base": base, "dmg_a": comp_a, "dmg_b": comp_b})
|
||||
for u in _b2.query_alive("enemy"):
|
||||
if int(_b2.hero_unit().hp) <= 0:
|
||||
else:
|
||||
# 零值伤害行(战斗结算 边界表:"合成后为 0 时不调扣血接口,改写零值伤害行——免伤与零输出必须可见";
|
||||
# 敌方侧同款口径已就绪——照抄。三分量字段齐备保 P2 归因不断链)
|
||||
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, "主角攻击(被减伤归零)", 0, {"target": hero_target.display_name, "base": base, "dmg_a": comp_a, "dmg_b": comp_b})
|
||||
for u in _enemy_system.query_alive("enemy"):
|
||||
if int(_enemy_system.hero_unit().hp) <= 0:
|
||||
break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板)
|
||||
var beat: String = _b2.query_beat(u)
|
||||
var atk: Dictionary = _b2.query_attack(u, L, _m_amb) # 调用序①兑付读值
|
||||
var beat: String = _enemy_system.query_beat(u)
|
||||
var atk: Dictionary = _enemy_system.query_attack(u, L, _m_amb) # 调用序①兑付读值
|
||||
if beat == "charge":
|
||||
_emit_row("charge", "enemy_action", Vector2i(-1, -1), ri, "%s 蓄力(本轮踩雷白喂)" % u.display_name, 0, {"target": "hero"})
|
||||
else:
|
||||
@@ -500,9 +540,9 @@ func _combat_segment(ri: int) -> void:
|
||||
if d_tick <= 0:
|
||||
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, "%s 攻击(被减伤归零)" % u.display_name, 0, {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach)})
|
||||
else:
|
||||
_b2.apply_damage(_b2.hero_unit(), d_tick) # 调用序②受击上报
|
||||
_enemy_system.apply_damage(_enemy_system.hero_unit(), d_tick) # 调用序②受击上报
|
||||
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, "%s 攻击" % u.display_name, d_tick, {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach)})
|
||||
_b2.advance(u) # 调用序③推进——每结算每存活敌恰一次(hero 不在推进域)
|
||||
_enemy_system.advance(u) # 调用序③推进——每结算每存活敌恰一次(hero 不在推进域)
|
||||
|
||||
|
||||
# F2 乘区:池内加算 → clamp [-100,+100] → 池间乘算 → floor(单一取整规则)——算术归 DamageMath(O3)
|
||||
@@ -530,7 +570,7 @@ func _armor_effectiveness(component: String, armor: String) -> int:
|
||||
|
||||
func _clamped_pool(pool: String) -> int:
|
||||
var total := 0
|
||||
for eff in _b3.query_active("hero") + _b3.query_active("enemy"):
|
||||
for eff in _status_effects.query_active("hero") + _status_effects.query_active("enemy"):
|
||||
var row: Dictionary = db.status_effect_row(eff.effect_id)
|
||||
if row.is_empty():
|
||||
continue
|
||||
@@ -544,16 +584,16 @@ func _pool_effective(effect_id: String) -> int:
|
||||
var row: Dictionary = db.status_effect_row(effect_id)
|
||||
if not row.is_empty():
|
||||
side = String(row.target_side)
|
||||
for eff in _b3.query_active(side):
|
||||
for eff in _status_effects.query_active(side):
|
||||
if eff.effect_id == effect_id:
|
||||
return eff.effective_value
|
||||
return 0
|
||||
|
||||
|
||||
func _emit_composition_effects(side: String, ri: int) -> void:
|
||||
for eff in _b3.query_active(side):
|
||||
for eff in _status_effects.query_active(side):
|
||||
if eff.round_index < ri:
|
||||
# 续存实例(前轮授予)——每结算兑付一次的可见面(合并实例只持首授归因——B3 义务⑦)
|
||||
# 续存实例(前轮授予)——每结算兑付一次的可见面(合并实例只持首授归因——增益状态 义务⑦)
|
||||
_emit_row("effect", eff.source_class, eff.origin_cell, ri, eff.display_name, eff.snap_magnitude, {"effect_id": eff.effect_id, "pool_value": eff.effective_value})
|
||||
|
||||
|
||||
@@ -561,12 +601,12 @@ func _emit_composition_effects(side: String, ri: int) -> void:
|
||||
|
||||
func _emit_settlement_snapshot(verdict: String) -> void:
|
||||
var enemies: Array = []
|
||||
for u in _b2.query_alive("enemy"):
|
||||
for u in _enemy_system.query_alive("enemy"):
|
||||
enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "beat": query_beat_of(u), "armor": String(u.get("armor_type", "none"))})
|
||||
var buffs: Array = []
|
||||
for eff in _b3.query_active("hero"):
|
||||
for eff in _status_effects.query_active("hero"):
|
||||
buffs.append({"id": eff.effect_id, "side": "hero", "layers": eff.layers, "left": eff.remaining_settlements})
|
||||
for eff in _b3.query_active("enemy"):
|
||||
for eff in _status_effects.query_active("enemy"):
|
||||
buffs.append({"id": eff.effect_id, "side": "enemy", "layers": eff.layers, "left": eff.remaining_settlements})
|
||||
var mines_hit := 0
|
||||
var safe_n := 0
|
||||
@@ -576,11 +616,11 @@ func _emit_settlement_snapshot(verdict: String) -> void:
|
||||
elif evt.kind == "safe":
|
||||
safe_n += 1
|
||||
var board_stats := {"seq": _board_seq}
|
||||
if a2.current_board() != null:
|
||||
board_stats["w"] = a2.current_board().width
|
||||
board_stats["h"] = a2.current_board().height
|
||||
board_stats["revealed"] = a2.revealed_cells().size()
|
||||
board_stats["flags"] = a2.flag_count()
|
||||
if minesweeper.current_board() != null:
|
||||
board_stats["w"] = minesweeper.current_board().width
|
||||
board_stats["h"] = minesweeper.current_board().height
|
||||
board_stats["revealed"] = minesweeper.revealed_cells().size()
|
||||
board_stats["flags"] = minesweeper.flag_count()
|
||||
settlement_snapshot.emit({
|
||||
"kind": "settlement",
|
||||
"level": int(encounter.get("level_index", 0)),
|
||||
@@ -588,20 +628,20 @@ func _emit_settlement_snapshot(verdict: String) -> void:
|
||||
"round": round_index,
|
||||
"verdict": verdict,
|
||||
"unit_count": enemies.size(),
|
||||
"spawn_count": _b2.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」(playtest-2026-09-12 报告 Bug#1 误读教训)
|
||||
"hero": {"hp": int(_b2.hero_unit().get("hp", 0)), "max_hp": int(_b2.hero_unit().get("max_hp", 0)), "atk": int(_b2.hero_unit().get("atk", 0))},
|
||||
"economy": {"phase": String(a3.phase), "ap": int(a3.ap), "rap": int(a3.rap), "refresh_used": _refresh_used_this_round},
|
||||
"spawn_count": _enemy_system.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」(playtest-2026-09-12 报告 Bug#1 误读教训)
|
||||
"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))},
|
||||
"economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _refresh_used_this_round},
|
||||
"board": board_stats,
|
||||
"enemies": enemies,
|
||||
"buffs": buffs,
|
||||
"ambush_layers": _b3.pool_layers("ambush"),
|
||||
"inventory_in_stock": _b4.in_stock_count(),
|
||||
"ambush_layers": _status_effects.pool_layers("ambush"),
|
||||
"inventory_in_stock": _reward_economy.in_stock_count(),
|
||||
"round_facts": {"path": _facts_path, "safe": safe_n, "mines": mines_hit, "flag_events": _facts_flags.size(), "charge_dmg_a": _charge_dmg_a, "charge_dmg_b": _charge_dmg_b},
|
||||
})
|
||||
|
||||
|
||||
func query_beat_of(u: Dictionary) -> String:
|
||||
return _b2.query_beat(u)
|
||||
return _enemy_system.query_beat(u)
|
||||
|
||||
|
||||
# ---- 出口分发与战斗结束序列 ----
|
||||
@@ -609,77 +649,67 @@ func query_beat_of(u: Dictionary) -> String:
|
||||
func _exit_distribution() -> void:
|
||||
var mode := "same_board"
|
||||
if _facts_path == "cleared":
|
||||
mode = "new_board" # cleared 轮恒走 new_board(契约义务)
|
||||
elif _refresh_used_this_waiting:
|
||||
mode = "new_board" # stopped/ap_exhausted 轮:仅 Waiting 期玩家刷新意图达成时换图(违约 fail-fast 的对侧)
|
||||
mode = "new_board" # cleared 轮恒走 new_board(契约义务);非清图轮恒同图(等待期刷新通道已移除)
|
||||
if mode == "new_board":
|
||||
_b4.retire_mapping()
|
||||
a2.retire_board()
|
||||
_reward_economy.retire_mapping()
|
||||
minesweeper.retire_board()
|
||||
if not _install_new_board():
|
||||
_fatal("换图编排失败")
|
||||
return
|
||||
a3.notify_settlement_done(mode)
|
||||
a3.notify_round_start()
|
||||
action_economy.notify_settlement_done(mode)
|
||||
action_economy.notify_round_start()
|
||||
_reset_round_facts()
|
||||
_refresh_used_this_waiting = false
|
||||
_refresh_used_this_round = 0
|
||||
round_index += 1
|
||||
round_started.emit(round_index)
|
||||
|
||||
|
||||
# 呈现层查询面(HUD 刷新按钮可用性)
|
||||
# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道
|
||||
func can_request_refresh() -> bool:
|
||||
return battle_result == "" \
|
||||
and _refresh_used_this_round < int(db.action_economy.refresh_max_per_round) \
|
||||
and (a3.phase == a3.PHASE_ACTIVE or a3.phase == a3.PHASE_WAITING) \
|
||||
and (a3.phase != a3.PHASE_WAITING or not _refresh_used_this_waiting) \
|
||||
and a3.rap >= int(db.action_economy.rap_refresh_cost)
|
||||
and action_economy.phase == action_economy.PHASE_ACTIVE \
|
||||
and action_economy.rap >= int(db.action_economy.rap_refresh_cost)
|
||||
|
||||
|
||||
# 玩家经 D1 发起、B1 代理调用(禁止 RAP 满自动刷)。
|
||||
# 轮内刷新(2026-09-12 方案 A 用户拍板):Active 相位立即换图继续挖(RAP -8;AP 保留、相位不动、
|
||||
# 不触发结算);Waiting 相位保持现状(登记意图,exit_distribution 换图)。每轮上限 = 表值(两相位合计)。
|
||||
# 玩家经 HUD框架发起、战斗结算 代理调用(禁止 RAP 满自动刷)。
|
||||
# 挖掘期唯一通道(2026-09-12 方案 A 拍板 + 2026-09-15 等待期通道整体移除):Active 相位立即换图
|
||||
# 继续挖(RAP -8;AP 保留、相位不动、不触发结算);非挖掘期请求一律拒绝——非清图轮恒同图。
|
||||
func player_request_refresh() -> bool:
|
||||
if not can_request_refresh():
|
||||
return false
|
||||
if a3.phase == a3.PHASE_ACTIVE:
|
||||
if not a3.request_refresh_active():
|
||||
return false
|
||||
_refresh_used_this_round += 1
|
||||
_b4.retire_mapping() # 旧图批次退役(B4 Ready→retired;E6 新批重置前提)
|
||||
a2.retire_board(true) # 旧板退役(轮内通道)
|
||||
return _install_new_board(true) # 当场换图——失败即编排违例 fail-fast
|
||||
if not a3.request_refresh():
|
||||
if not action_economy.request_refresh_active():
|
||||
return false
|
||||
_refresh_used_this_round += 1
|
||||
_refresh_used_this_waiting = true
|
||||
return true
|
||||
_reward_economy.retire_mapping() # 旧图批次退役(奖励道具经济 Ready→retired;E6 新批重置前提)
|
||||
minesweeper.retire_board(true) # 旧板退役(轮内通道)
|
||||
return _install_new_board(true) # 当场换图——失败即编排违例 fail-fast
|
||||
|
||||
|
||||
func _battle_over(result: String) -> void:
|
||||
battle_result = result
|
||||
# 库存关卡域(2026-09-11 关内延续):battle_over 不再清库存——跨战斗延续,关卡末经 level_clear() 清空;
|
||||
# 映射随战斗结束退役(下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提)
|
||||
var hero_hp_left := int(_b2.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层")
|
||||
_b4.retire_mapping()
|
||||
_b2.despawn_all()
|
||||
_b3.notify_battle_end()
|
||||
a3.notify_combat_end() # → Idle,RAP 清零
|
||||
var hero_hp_left := int(_enemy_system.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层")
|
||||
_reward_economy.retire_mapping()
|
||||
_enemy_system.despawn_all()
|
||||
_status_effects.notify_battle_end()
|
||||
action_economy.notify_combat_end() # → Idle,RAP 清零
|
||||
db.set_reload_gate(false)
|
||||
_reset_round_facts()
|
||||
battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "remaining_items": _b4.in_stock_count()})
|
||||
battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "remaining_items": _reward_economy.in_stock_count()})
|
||||
|
||||
|
||||
# ---- 呈现状态面(E1 封装收口 2026-09-13):呈现层 / headless 自动玩家的唯一读取出口——
|
||||
# 协作对象(_b2/_b3/_b4)私有化后,跨层读取一律经本快照或下列命名面(diagnostic_snapshot 同款模式,
|
||||
# 禁再链式穿透 b1.b2.hero 一类路径)。载荷 schema 成文(code-conventions 2.1):
|
||||
# 协作对象(_enemy_system/_status_effects/_reward_economy)私有化后,跨层读取一律经本快照或下列命名面(diagnostic_snapshot 同款模式,
|
||||
# 禁再链式穿透 settlement.enemy_system.hero 一类路径)。载荷 schema 成文(code-conventions 2.1):
|
||||
# round / battle_result / phase / ap / rap / can_request_refresh / hero{hp,max_hp,atk,alive} /
|
||||
# enemies[{order,display_name,hp,max_hp,enemy_id,armor_type,beat,ring_size,ring_index,ambush_layers,alive}] /
|
||||
# ambush_layers / hero_effects[](B3 query_active 投影) / inventory[](B4 query_inventory 投影) / in_stock
|
||||
# ambush_layers / hero_effects[](增益状态 query_active 投影) / inventory[](奖励道具经济 query_inventory 投影) / in_stock
|
||||
func presentation_snapshot() -> Dictionary:
|
||||
var enemies: Array = []
|
||||
var all: Array = _b2.all_enemies()
|
||||
var ambush: int = _b3.pool_layers("ambush")
|
||||
var all: Array = _enemy_system.all_enemies()
|
||||
var ambush: int = _status_effects.pool_layers("ambush")
|
||||
for i in all.size():
|
||||
var u: Dictionary = all[i]
|
||||
var alive: bool = bool(u.get("alive", false))
|
||||
@@ -690,44 +720,44 @@ func presentation_snapshot() -> Dictionary:
|
||||
"hp": int(u.hp), "max_hp": int(u.max_hp),
|
||||
"enemy_id": String(u.enemy_id),
|
||||
"armor_type": String(u.get("armor_type", "none")),
|
||||
"beat": String(_b2.query_beat(u)) if alive else "",
|
||||
"beat": String(_enemy_system.query_beat(u)) if alive else "",
|
||||
"ring_size": ring.size(),
|
||||
"ring_index": int(u.get("steps", 0)) % maxi(ring.size(), 1),
|
||||
"ambush_layers": ambush,
|
||||
"alive": alive,
|
||||
})
|
||||
var hero := {}
|
||||
var h: Dictionary = _b2.hero_unit()
|
||||
var h: Dictionary = _enemy_system.hero_unit()
|
||||
if not h.is_empty():
|
||||
hero = {"hp": int(h.hp), "max_hp": int(h.max_hp), "atk": int(h.atk), "alive": bool(h.get("alive", false))}
|
||||
return {
|
||||
"round": round_index,
|
||||
"battle_result": battle_result,
|
||||
"phase": String(a3.phase),
|
||||
"ap": int(a3.ap), "rap": int(a3.rap),
|
||||
"phase": String(action_economy.phase),
|
||||
"ap": int(action_economy.ap), "rap": int(action_economy.rap),
|
||||
"can_request_refresh": can_request_refresh(),
|
||||
"hero": hero,
|
||||
"enemies": enemies,
|
||||
"ambush_layers": ambush,
|
||||
"hero_effects": _b3.query_active("hero"),
|
||||
"inventory": _b4.query_inventory(),
|
||||
"in_stock": _b4.in_stock_count(),
|
||||
"hero_effects": _status_effects.query_active("hero"),
|
||||
"inventory": _reward_economy.query_inventory(),
|
||||
"in_stock": _reward_economy.in_stock_count(),
|
||||
}
|
||||
|
||||
|
||||
# 战斗进行中谓词(hero 在场且存活)——组合根高频相位判断的低成本面
|
||||
func is_battle_live() -> bool:
|
||||
return battle_result == "" and not _b2.hero_unit().is_empty()
|
||||
return battle_result == "" and not _enemy_system.hero_unit().is_empty()
|
||||
|
||||
|
||||
# 库存余量(层间过渡文案 / 终局清库反馈)
|
||||
func stock_count() -> int:
|
||||
return _b4.in_stock_count()
|
||||
return _reward_economy.in_stock_count()
|
||||
|
||||
|
||||
# 奖励身份解析(组合根注入 BoardView 徽章回调的只读代理——Ready 相位守卫归 B4)
|
||||
# 奖励身份解析(组合根注入 BoardView 徽章回调的只读代理——Ready 相位守卫归 奖励道具经济)
|
||||
func query_reward_identity(cell: Vector2i):
|
||||
return _b4.query_identity(cell)
|
||||
return _reward_economy.query_identity(cell)
|
||||
|
||||
|
||||
# 诊断观测面(组合根诊断转储 / 行为探针消费):私有编排状态的成文只读出口——
|
||||
@@ -738,26 +768,25 @@ func diagnostic_snapshot() -> Dictionary: return {
|
||||
"settling": _settling,
|
||||
"awaiting_window": _awaiting_window,
|
||||
"refresh_used_round": _refresh_used_this_round,
|
||||
"refresh_used_waiting": _refresh_used_this_waiting,
|
||||
"board_seq": _board_seq,
|
||||
"facts_path": _facts_path,
|
||||
"facts_reveals": _facts_reveals.size(),
|
||||
"facts_flags": _facts_flags.size(),
|
||||
"a3": {"phase": String(a3.phase), "ap": a3.ap, "rap": a3.rap},
|
||||
"a2_board_installed": a2.current_board() != null,
|
||||
"a2_revealed": a2.revealed_cells().size() if a2.current_board() != null else 0,
|
||||
"a2_flags": a2.flag_count() if a2.current_board() != null else 0,
|
||||
"b4_phase_stock": [_b4.in_stock_count()],
|
||||
"enemies_alive": _b2.query_alive("enemy").size(),
|
||||
"hero_hp": int(_b2.hero_unit().get("hp", -1)) if not _b2.hero_unit().is_empty() else -1,
|
||||
"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,
|
||||
"minesweeper_flags": minesweeper.flag_count() if minesweeper.current_board() != null else 0,
|
||||
"reward_economy_phase_stock": [_reward_economy.in_stock_count()],
|
||||
"enemies_alive": _enemy_system.query_alive("enemy").size(),
|
||||
"hero_hp": int(_enemy_system.hero_unit().get("hp", -1)) if not _enemy_system.hero_unit().is_empty() else -1,
|
||||
}
|
||||
|
||||
|
||||
# 关卡结束编排(C1/C2 关卡结构落位前由上层显式调用——2026-09-11 关内延续):
|
||||
# 关卡结束编排(关卡节点地图/冒险结构 关卡结构落位前由上层显式调用——2026-09-11 关内延续):
|
||||
# 清空库存并返回快照计数("剩余道具 ×N 随关卡结束清空"反馈行 / 折算结构位的数据面)
|
||||
func level_clear() -> Dictionary:
|
||||
var remaining := _b4.in_stock_count()
|
||||
_b4.clear_inventory()
|
||||
var remaining := _reward_economy.in_stock_count()
|
||||
_reward_economy.clear_inventory()
|
||||
return {"remaining": remaining}
|
||||
|
||||
|
||||
@@ -772,6 +801,7 @@ func _reset_round_facts() -> void:
|
||||
# ---- 行协议(Core Rule 14:公共字段 + 行类型特有字段)----
|
||||
|
||||
func _emit_row(type: String, source_class: String, origin_cell: Vector2i, ri: int, display: String, value: int, extra: Dictionary, highlight: String = "") -> void:
|
||||
_assert_row_source(type, source_class, origin_cell)
|
||||
var row := {
|
||||
"type": type,
|
||||
"source_class": source_class,
|
||||
@@ -787,6 +817,19 @@ func _emit_row(type: String, source_class: String, origin_cell: Vector2i, ri: in
|
||||
step_row.emit(row)
|
||||
|
||||
|
||||
# 行来源断言(战斗结算 明细流水:"任何一行找不到来源即断言失败——宁可报错"):
|
||||
# 来源字段 = source_class(公共字段)+ origin_cell(棋盘来源行的真实格)。
|
||||
# 豁免按两层成文(见 SOURCE_EXEMPT_TYPES / SOURCE_EXEMPT_CLASSES 常量注释),其余行必须带真实棋盘格。
|
||||
func _assert_row_source(type: String, source_class: String, origin_cell: Vector2i) -> void:
|
||||
if SOURCE_EXEMPT_TYPES.has(type):
|
||||
return
|
||||
assert(source_class != "", "[战斗结算] 明细行缺来源类别(type=%s)——任何一行找不到来源即断言失败" % type)
|
||||
if SOURCE_EXEMPT_CLASSES.has(source_class):
|
||||
return
|
||||
assert(origin_cell.x >= 0 and origin_cell.y >= 0,
|
||||
"[战斗结算] 明细行来源格越界(type=%s source=%s cell=%s)——棋盘来源行必须指回真实格" % [type, source_class, str(origin_cell)])
|
||||
|
||||
|
||||
func _fatal(msg: String) -> void:
|
||||
push_error("[B1/fatal] " + msg)
|
||||
push_error("[战斗结算/fatal] " + msg)
|
||||
assert(false, msg)
|
||||
@@ -1,8 +1,8 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# A1 棋盘产物数据契约(A2 消费;生成后不可变——A2 持快照即持活引用,生成器不再触碰)。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/a1-board-generation.md(系统 ID A1——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 棋盘产物数据契约(排雷玩法消费;生成后不可变——排雷玩法持快照即持活引用,生成器不再触碰)。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/a1-board-generation.md(系统 ID A1·棋盘生成——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name BoardData
|
||||
extends RefCounted
|
||||
|
||||
@@ -15,8 +15,9 @@ var variants := {} # Vector2i → Array[String](生成期快照,抗表
|
||||
var biome := ""
|
||||
var level_index: int = 1
|
||||
var node_depth: int = 0
|
||||
var metrics := {} # 八项生成期指标
|
||||
var illegal_product := false
|
||||
var metrics := {} # 生成期指标集(键清单单点定义在 board_generator._finalize)
|
||||
var illegal_product := false # 非法产物标记(接受最不坏出口——零合法板诚实降级)
|
||||
var degraded_product := false # 降级产物标记(接受最优出口——有合法板但无一达标;与 illegal_product 对称)
|
||||
|
||||
|
||||
func in_bounds(c: Vector2i) -> bool:
|
||||
@@ -93,7 +94,7 @@ func fog_cells() -> Array:
|
||||
return out
|
||||
|
||||
|
||||
# R₀ = 出生 3×3 ∪ (出生 3×3 经 0 格的 flood 闭包)——与 A2 F3 逐字同构(A1 F1 求解器初始态)。
|
||||
# R₀ = 出生 3×3 ∪ (出生 3×3 经 0 格的 flood 闭包)——与 排雷玩法 F3 逐字同构(棋盘生成 F1 求解器初始态)。
|
||||
func compute_r0() -> Array:
|
||||
var b3 := spawn_cells()
|
||||
var r0 := {}
|
||||
@@ -110,7 +111,7 @@ func compute_r0() -> Array:
|
||||
return r0.keys()
|
||||
|
||||
|
||||
# A1 Rule 4 排除域(以最终产物板计):R₀ ∪ 外扩 1 环——生成期确定性计算。
|
||||
# 棋盘生成 Rule 4 排除域(以最终产物板计):R₀ ∪ 外扩 1 环——生成期确定性计算。
|
||||
func exclusion_domain() -> Dictionary:
|
||||
var r0 := compute_r0()
|
||||
var dom := {}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# A1 确定性棋盘生成器:五阶段管线(参数→布雷→变异→验证循环→产出)+ F1 可推理率度量
|
||||
# 确定性棋盘生成器:五阶段管线(参数→布雷→变异→验证循环→产出)+ F1 可推理率度量
|
||||
# (single_subset 求解器)+ F3 雾预算 + F5 验收算子(达标即出/接受最优/接受最不坏)。
|
||||
# 切片裁剪登记:黄金向量回归套件与 F4 批量工具不做(C3 调参期工具域)。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/a1-board-generation.md(系统 ID A1——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 切片裁剪登记:黄金向量回归套件与 F4 批量工具不做(难度曲线与Boss 调参期工具域)。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/a1-board-generation.md(系统 ID A1·棋盘生成——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name BoardGenerator
|
||||
extends RefCounted
|
||||
|
||||
@@ -52,7 +52,7 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat
|
||||
if best_legal == null or inf > best_legal_score:
|
||||
best_legal = b
|
||||
best_legal_score = inf
|
||||
if inf >= params.target and m["inferability_core"] >= params.target:
|
||||
if inf >= params.target: # 单一判据(棋盘生成:现行规则已回归"可推理率 ≥ 及格线")
|
||||
accepted = b
|
||||
accept_mode = "first_pass"
|
||||
break
|
||||
@@ -60,12 +60,13 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat
|
||||
if best_legal != null:
|
||||
accepted = best_legal
|
||||
accept_mode = "accept_best"
|
||||
push_warning("[A1] 重试耗尽无达标——接受最优出图(强告警埋点,target=%.2f best=%.3f)" % [params.target, best_legal_score])
|
||||
accepted.degraded_product = true # 降级标记(独立于 illegal_product——三分出口表要求标记随产物上抛)
|
||||
push_warning("[棋盘生成] 重试耗尽无达标——接受最优出图(强告警埋点,target=%.2f best=%.3f)" % [params.target, best_legal_score])
|
||||
else:
|
||||
accepted = best_bad
|
||||
accept_mode = "accept_worst"
|
||||
accepted.illegal_product = true
|
||||
push_error("[A1] attempts 耗尽零合法——接受最不坏出图 + 非法产物标记(玩家永不卡死)")
|
||||
push_error("[棋盘生成] attempts 耗尽零合法——接受最不坏出图 + 非法产物标记(玩家永不卡死)")
|
||||
_finalize(accepted, params, attempts_used, accept_mode, db)
|
||||
return accepted
|
||||
|
||||
@@ -275,7 +276,7 @@ func measure_inferability(b: BoardData) -> Dictionary:
|
||||
for con in derived:
|
||||
if con.mines == 0:
|
||||
for c in con.unknown:
|
||||
if safe_set.has(c) and not r.has(c):
|
||||
if not r.has(c): # 同 no-peek 口径:推导结论不再用 safe_set 复核(约束本身可靠)
|
||||
r[c] = true
|
||||
changed = true
|
||||
elif con.mines == con.unknown.size():
|
||||
@@ -313,8 +314,8 @@ func _build_constraints(b: BoardData, r: Dictionary, known_mines: Dictionary, sa
|
||||
for n in b.neighbors8(c):
|
||||
if known_mines.has(n):
|
||||
known_here += 1
|
||||
elif not r.has(n) and safe_set.has(n):
|
||||
unknown[n] = true
|
||||
elif not r.has(n):
|
||||
unknown[n] = true # 未揭示格一律计入未知(含雷格)——求解器禁读 safe_set 真值(棋盘生成:雷位不许偷看)
|
||||
var d_eff := d - known_here
|
||||
if d_eff < 0 or (d_eff > 0 and unknown.is_empty()):
|
||||
continue # 矛盾态(非法板)——不产约束,验收由 legality 闸承担
|
||||
@@ -413,7 +414,7 @@ func legality(b: BoardData, params: Dictionary, db) -> bool:
|
||||
|
||||
func _legality_faults(b: BoardData, params: Dictionary) -> Array:
|
||||
var faults: Array = []
|
||||
# ① 出生区无雷 ② 出生区∪R₀ 无变异 ③ 数字恒真 ④ R₀ ≠ S(A2 修订义务)
|
||||
# ① 出生区无雷 ② 出生区∪R₀ 无变异 ③ 数字恒真 ④ R₀ ≠ S(排雷玩法 修订义务)
|
||||
for c in b.spawn_cells():
|
||||
if b.is_mine(c):
|
||||
faults.append("inv1_mine_in_spawn")
|
||||
@@ -477,12 +478,16 @@ func _has_all_fog_window(b: BoardData, fog: Array) -> bool:
|
||||
return false
|
||||
|
||||
|
||||
# ==================== 产出上报(八项指标) ====================
|
||||
# ==================== 产出上报(生成期指标集) ====================
|
||||
|
||||
func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, db) -> void:
|
||||
var m := measure_inferability(b)
|
||||
var fog_budget := measure_fog_budget(b, db.action_economy.dig_ap_cost)
|
||||
var fog := b.fog_cells()
|
||||
# 产物断言(棋盘生成 出生区安全与反退化布雷):实际雷数 ≤ 目标雷数 N——先布后删只减不增,失败即致命
|
||||
var n_target: int = int(round(float(params.density) * float(params.w * params.h - 9)))
|
||||
if b.mines.size() > n_target:
|
||||
_fatal("产物断言违例:实际雷数 %d > 目标雷数 N=%d(先布后删只减不增)" % [b.mines.size(), n_target])
|
||||
var attempt_facts := b.metrics # 保留尝试期事实(ring_removed / shortfall)
|
||||
b.metrics = {
|
||||
"metric_version": METRIC_VERSION,
|
||||
@@ -490,16 +495,20 @@ func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode
|
||||
"inferability_core": m.inferability_core,
|
||||
"actual_fog_ratio": float(fog.size()) / float(b.width * b.height),
|
||||
"actual_mines": b.mines.size(),
|
||||
"target_mines": n_target,
|
||||
"actual_safe_count": b.safe_cells().size(),
|
||||
"fog_burden_ratio": fog_budget.fog_burden_ratio,
|
||||
"delta_budget_fog": fog_budget.delta_budget_fog,
|
||||
"attempts_used": attempts_used,
|
||||
"accept_mode": accept_mode,
|
||||
"degraded_product": accept_mode == "accept_best",
|
||||
"illegal_product": accept_mode == "accept_worst",
|
||||
"shortfall": attempt_facts.get("shortfall", 0),
|
||||
"ring_removed": attempt_facts.get("ring_removed", 0),
|
||||
"r0_count": m["r0"],
|
||||
}
|
||||
b.illegal_product = accept_mode == "accept_worst"
|
||||
b.degraded_product = accept_mode == "accept_best"
|
||||
|
||||
|
||||
func _badness(b: BoardData, params: Dictionary) -> Array:
|
||||
@@ -537,5 +546,5 @@ func _lex_less(a: Array, b: Array) -> bool:
|
||||
|
||||
|
||||
func _fatal(msg: String) -> void:
|
||||
push_error("[A1/fatal] " + msg)
|
||||
push_error("[棋盘生成/fatal] " + msg)
|
||||
assert(false, msg)
|
||||
@@ -1,9 +1,9 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 ConfigDB autoload——数值表单点访问入口(Rule 2)+ 校验双通道(Rule 6)+ R1 行解析。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 切片注:headless --script 模式下由冒烟脚本显式实例化(D2 AC 前言);玩法代码禁止绕过本单点自建 load()。
|
||||
# ConfigDB autoload(数据配置)——数值表单点访问入口(Rule 2)+ 校验双通道(Rule 6)+ R1 行解析。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 切片注:headless --script 模式下由冒烟脚本显式实例化(数据配置 AC 前言);玩法代码禁止绕过本单点自建 load()。
|
||||
extends Node
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
@@ -37,7 +37,7 @@ var difficulty: DifficultyTable
|
||||
var is_ready := false
|
||||
var _diagnostics: Array = [] # {severity, category, message}
|
||||
var _warn_dedup: Dictionary = {} # R1 四元组去重注册表
|
||||
var _reload_gate := false # D2 EC8——B1 唯一调用方
|
||||
var _reload_gate := false # 数据配置 EC8——战斗结算 唯一调用方
|
||||
|
||||
|
||||
func push_diag(severity: String, category: String, message: String) -> void:
|
||||
@@ -148,7 +148,7 @@ func _cross_validate(loaded: Dictionary) -> void:
|
||||
has_buff = true
|
||||
if safe_rows.size() > 0 and not has_buff:
|
||||
push_diag("error", "no_live_buff_row", "safe_cell 池零活 buff 行——tier_min_buff 无定义")
|
||||
# ③ clear_default 恒正当量(A3 条款④——2026-09-11 供给控制器批:清图槽改即时类一次性伤害,正当量含即时类)
|
||||
# ③ clear_default 恒正当量(行动经济 条款④——2026-09-11 供给控制器批:清图槽改即时类一次性伤害,正当量含即时类)
|
||||
var clear_value_ok := false
|
||||
for row in clear_rows:
|
||||
if row.get("category", "") == "buff" and String(row.get("effect_id", "")) != "":
|
||||
@@ -188,7 +188,7 @@ func _cross_validate(loaded: Dictionary) -> void:
|
||||
for eff in st_tab.effect_pool_map.keys():
|
||||
if not effect_ids.has(eff):
|
||||
push_diag("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff)
|
||||
# ⑦ source_label_map 键集封闭性(键 = B3 source_class ∪ {item, enemy_action, flag_miss})
|
||||
# ⑦ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {item, enemy_action, flag_miss})
|
||||
var required_sources := ["safe_cell", "flag_hit", "mine_hit", "clear_reward", "item", "hero_attack", "enemy_action", "flag_miss"]
|
||||
for s in required_sources:
|
||||
if not st_tab.source_label_map.has(s):
|
||||
@@ -228,7 +228,7 @@ func resolve_enemy_rowset(L: int, D: int) -> Dictionary:
|
||||
return _resolve_progress(enemy.rows, L, D, "enemy")
|
||||
|
||||
|
||||
# C3 DifficultyTable 消费面(B2.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
|
||||
# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
|
||||
func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary:
|
||||
if difficulty == null:
|
||||
return {}
|
||||
@@ -303,7 +303,7 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Diction
|
||||
return {}
|
||||
|
||||
|
||||
# ---- A4 variant_pool_gate:候选池门控(∅ biome_tag = 通配)----
|
||||
# ---- 地形变异 variant_pool_gate:候选池门控(∅ biome_tag = 通配)----
|
||||
|
||||
func variant_pool(level_index: int, board_biomes: Array) -> Array:
|
||||
var pool: Array = []
|
||||
@@ -325,7 +325,7 @@ func status_effect_row(effect_id: String) -> Dictionary:
|
||||
return status_effects.row_by_id(effect_id)
|
||||
|
||||
|
||||
# ---- D2 EC8:debug 热重载门(B1 唯一调用方;切片实现 = 门标志 + 拒绝语义)----
|
||||
# ---- 数据配置 EC8:debug 热重载门(战斗结算 唯一调用方;切片实现 = 门标志 + 拒绝语义)----
|
||||
|
||||
func set_reload_gate(gated: bool) -> void:
|
||||
_reload_gate = gated
|
||||
@@ -333,7 +333,7 @@ func set_reload_gate(gated: bool) -> void:
|
||||
|
||||
func reload() -> bool:
|
||||
if _reload_gate:
|
||||
push_diag("warning", "reload_gated", "战斗中热重载被拒绝(D2 EC8)")
|
||||
push_diag("warning", "reload_gated", "战斗中热重载被拒绝(数据配置 EC8)")
|
||||
return false
|
||||
var ok := load_tables("res://data/tables", false)
|
||||
if ok:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b1-settlement.md(系统 ID B1——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# B1 战斗段伤害公式族(零状态纯算术宿主——规范 1.2「工具性纯函数挂明确宿主类」;
|
||||
# 自 battle_orchestrator 拆出 2026-09-13,封装审查 O3)。池读取与表值读取仍归 B1,本类只做算术。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b1-settlement.md(系统 ID B1·战斗结算——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 战斗结算·战斗段伤害公式族(零状态纯算术宿主——规范 1.2「工具性纯函数挂明确宿主类」;
|
||||
# 自 battle_orchestrator 拆出 2026-09-13,封装审查 O3)。池读取与表值读取仍归 战斗结算,本类只做算术。
|
||||
class_name DamageMath
|
||||
extends RefCounted
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证
|
||||
# Date: 2026-09-11
|
||||
# C1 关卡节点地图(provisional 线性形态——节点链来自 LevelTable,路径分支/形态定稿权归 C1):
|
||||
# 关卡域生命周期唯一编排者——共享 B4 容器跨战斗延续(2026-09-11 关内延续裁决)、HP 跨战斗携带、
|
||||
# 奖励点/恢复点节点兑付、关卡末 level_clear 清库。Core 层零 UI 依赖(headless 可跑完整关卡——D2 Rule 7)。
|
||||
# 测试专用:hero_atk_pct/hero_hp_pct 数值覆盖(调整板模拟天赋树——正式侧归 C4 元进度,切片不落盘)。
|
||||
# 关卡节点地图(provisional 线性形态——节点链来自 LevelTable,路径分支/形态定稿权归 关卡节点地图):
|
||||
# 关卡域生命周期唯一编排者——共享 奖励道具经济 容器跨战斗延续(2026-09-11 关内延续裁决)、HP 跨战斗携带、
|
||||
# 奖励点/恢复点节点兑付、关卡末 level_clear 清库。Core 层零 UI 依赖(headless 可跑完整关卡——数据配置 Rule 7)。
|
||||
# 测试专用:hero_atk_pct/hero_hp_pct 数值覆盖(调整板模拟天赋树——正式侧归元进度,切片不落盘)。
|
||||
# E4 收口(2026-09-13):覆盖值经 encounter 显式传入战斗(不再写 db.hero 表——表值运行期不可变,
|
||||
# 双基线同步/双写叠加 6→18→54 整类隐患随表写一并移除)。
|
||||
# 对应 GDD:C1 关卡节点地图(未设计——provisional 切片先行,定稿后回写;命名规范见 CLAUDE.md)
|
||||
# 对应 GDD:关卡节点地图(未设计——provisional 切片先行,定稿后回写;命名规范见 CLAUDE.md)
|
||||
class_name LevelRunner
|
||||
extends RefCounted
|
||||
|
||||
signal node_entered(node: Dictionary, info: Dictionary) # info 按 type:battle/boss={} · reward={snapshot} · rest={healed,hp,max_hp}
|
||||
signal step_row(row: Dictionary) # 转发当前 B1 结算行
|
||||
signal window_opened(items: Array) # 转发当前 B1 道具窗口
|
||||
signal step_row(row: Dictionary) # 转发当前 战斗结算 结算行
|
||||
signal window_opened(items: Array) # 转发当前 战斗结算 道具窗口
|
||||
signal round_started(round_index: int)
|
||||
signal board_changed()
|
||||
signal level_finished(payload: Dictionary) # {result, hp, node_index, node_type, remaining_items}
|
||||
@@ -23,9 +23,9 @@ var rng
|
||||
var level_row: Dictionary = {}
|
||||
var node_index := -1
|
||||
var hero_hp := 0
|
||||
# 协作对象私有(E1/E3 封装收口 2026-09-13):b1/b4 读取经 current_battle()/inventory() 成文出口
|
||||
var _b1 = null # 当前战斗编排器(关卡结束置 null)
|
||||
var _b4 = null # 关卡域共享 B4 容器(库存跨战斗延续)
|
||||
# 协作对象私有(E1/E3 封装收口 2026-09-13):settlement/reward_economy 读取经 current_battle()/inventory() 成文出口
|
||||
var _settlement = null # 当前战斗编排器(关卡结束置 null)
|
||||
var _reward_economy = null # 关卡域共享 奖励道具经济 容器(库存跨战斗延续)
|
||||
|
||||
var hero_atk_pct := 0 # 测试面板覆盖值(切片专用)
|
||||
var hero_hp_pct := 0
|
||||
@@ -38,7 +38,7 @@ func _init(db_ref, rng_ref) -> void:
|
||||
rng = rng_ref
|
||||
|
||||
|
||||
# ---- 测试面板覆盖(模拟天赋树属性提升——仅测试用,正式侧归 C4;经 encounter 传入战斗)----
|
||||
# ---- 测试面板覆盖(模拟天赋树属性提升——仅测试用,正式侧归 元进度;经 encounter 传入战斗)----
|
||||
|
||||
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
||||
hero_atk_pct = atk_pct
|
||||
@@ -64,14 +64,14 @@ func restore_hero_base() -> void:
|
||||
func start_level(level_index: int, seed_val: int) -> bool:
|
||||
var row := _find_level_row(level_index)
|
||||
if row.is_empty():
|
||||
push_error("[C1] level_index %d 在 LevelTable 无行" % level_index)
|
||||
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
|
||||
return false
|
||||
level_row = row
|
||||
node_index = -1
|
||||
level_result = ""
|
||||
hero_hp = effective_max_hp()
|
||||
_b4 = RewardEconomy.new(db, rng)
|
||||
rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 C2)
|
||||
_reward_economy = RewardEconomy.new(db, rng)
|
||||
rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构)
|
||||
_running = true
|
||||
return _enter_next_node()
|
||||
|
||||
@@ -99,15 +99,15 @@ func _enter_next_node() -> bool:
|
||||
node_index += 1
|
||||
var node: Dictionary = current_node()
|
||||
if node.is_empty():
|
||||
push_error("[C1] 节点链越界(末节点 Boss 应已结束关卡)")
|
||||
push_error("[关卡节点地图] 节点链越界(末节点 Boss 应已结束关卡)")
|
||||
return false
|
||||
match String(node.get("type", "")):
|
||||
"battle", "boss":
|
||||
return _start_battle(node)
|
||||
"reward":
|
||||
var inst = _b4.grant_node_active(Vector2i(-1, -1), 0)
|
||||
var inst = _reward_economy.grant_node_active(Vector2i(-1, -1), 0)
|
||||
if not (inst is Dictionary):
|
||||
push_error("[C1] 奖励点入库被拒")
|
||||
push_error("[关卡节点地图] 奖励点入库被拒")
|
||||
return false
|
||||
node_entered.emit(node, {"snapshot": inst.snapshot})
|
||||
return true
|
||||
@@ -115,25 +115,25 @@ func _enter_next_node() -> bool:
|
||||
var healed := _apply_rest()
|
||||
node_entered.emit(node, {"healed": healed, "hp": hero_hp, "max_hp": effective_max_hp()})
|
||||
return true
|
||||
push_error("[C1] 未知节点类型: %s" % str(node))
|
||||
push_error("[关卡节点地图] 未知节点类型: %s" % str(node))
|
||||
return false
|
||||
|
||||
|
||||
func _start_battle(node: Dictionary) -> bool:
|
||||
_b1 = BattleOrchestrator.new(db, rng, _b4) # 共享 B4——库存跨战斗延续(关内延续裁决)
|
||||
_settlement = BattleOrchestrator.new(db, rng, _reward_economy) # 共享 奖励道具经济——库存跨战斗延续(关内延续裁决)
|
||||
# core↔core 接线只许方法 Callable(ADR-0001 静态规则 S6——lambda 捕获 self 会延长
|
||||
# RefCounted 生命周期乃至成环:b1 的信号持捕获 runner 的 lambda,runner 又持 b1)
|
||||
_b1.step_row.connect(_forward_step_row)
|
||||
_b1.window_opened.connect(_forward_window_opened)
|
||||
_b1.round_started.connect(_forward_round_started)
|
||||
_b1.board_changed.connect(_forward_board_changed)
|
||||
_b1.battle_finished.connect(_on_battle_finished)
|
||||
# node_entered 先于 begin_battle:呈现层须先拿到 b1 引用,才能响应 begin 期间发出的
|
||||
# RefCounted 生命周期乃至成环:settlement 的信号持捕获 runner 的 lambda,runner 又持 settlement)
|
||||
_settlement.step_row.connect(_forward_step_row)
|
||||
_settlement.window_opened.connect(_forward_window_opened)
|
||||
_settlement.round_started.connect(_forward_round_started)
|
||||
_settlement.board_changed.connect(_forward_board_changed)
|
||||
_settlement.battle_finished.connect(_on_battle_finished)
|
||||
# node_entered 先于 begin_battle:呈现层须先拿到 settlement 引用,才能响应 begin 期间发出的
|
||||
# board_changed(首板安装)并重建棋盘——后发会导致首战棋盘不渲染(2026-09-11 playtest 缺陷①)
|
||||
node_entered.emit(node, {})
|
||||
if not _b1.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("node_depth", 0)),
|
||||
if not _settlement.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("node_depth", 0)),
|
||||
"hero_hp": hero_hp, "hero_atk": effective_atk(), "hero_max_hp": effective_max_hp()}):
|
||||
push_error("[C1] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
|
||||
push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
|
||||
return false
|
||||
return true
|
||||
|
||||
@@ -160,15 +160,15 @@ func _on_battle_finished(payload: Dictionary) -> void:
|
||||
if result == "victory":
|
||||
hero_hp = maxi(1, int(payload.get("hero_hp", 0))) # despawn 前捕获值(载荷自包含)
|
||||
if String(node.get("type", "")) == "boss" or result == "defeat":
|
||||
# Boss 胜利 = 关卡通关;任意战斗败北 = 关卡失败(Roguelike——冒险即终,重开归 C2/C4)
|
||||
var left: int = _b1.level_clear()["remaining"] # 库存随关卡结束清空(关内延续裁决③)
|
||||
if _b4.in_stock_count() != 0:
|
||||
push_error("[C1] level_clear 后库存非空(清库违例——fail-fast)")
|
||||
# Boss 胜利 = 关卡通关;任意战斗败北 = 关卡失败(Roguelike——冒险即终,重开归 冒险结构/元进度)
|
||||
var left: int = _settlement.level_clear()["remaining"] # 库存随关卡结束清空(关内延续裁决③)
|
||||
if _reward_economy.in_stock_count() != 0:
|
||||
push_error("[关卡节点地图] level_clear 后库存非空(清库违例——fail-fast)")
|
||||
return
|
||||
level_result = "victory" if result == "victory" else "defeat"
|
||||
var hp_left := hero_hp if result == "victory" else 0
|
||||
_running = false
|
||||
_b1 = null
|
||||
_settlement = null
|
||||
level_finished.emit({"result": level_result, "hp": hp_left, "node_index": node_index,
|
||||
"node_type": String(node.get("type", "")), "remaining_items": left})
|
||||
return
|
||||
@@ -187,11 +187,11 @@ func _apply_rest() -> int:
|
||||
# ---- 只读出口(E1/E3 封装收口:呈现层/工具经此读当前战斗与关卡库存,禁直读协作对象)----
|
||||
|
||||
func current_battle():
|
||||
return _b1
|
||||
return _settlement
|
||||
|
||||
|
||||
func inventory() -> Array:
|
||||
return _b4.query_inventory() if _b4 != null else []
|
||||
return _reward_economy.query_inventory() if _reward_economy != null else []
|
||||
|
||||
|
||||
# ---- 玩家输入转发(呈现层只与 runner 对话)----
|
||||
@@ -205,11 +205,11 @@ func continue_after_node() -> bool:
|
||||
|
||||
|
||||
func submit_window_selection(selection: Array) -> void:
|
||||
if _b1 != null:
|
||||
_b1.submit_window_selection(selection)
|
||||
if _settlement != null:
|
||||
_settlement.submit_window_selection(selection)
|
||||
|
||||
|
||||
func player_request_refresh() -> bool:
|
||||
if _b1 == null:
|
||||
if _settlement == null:
|
||||
return false
|
||||
return _b1.player_request_refresh()
|
||||
return _settlement.player_request_refresh()
|
||||
@@ -1,20 +1,20 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# A2 排雷玩法规则引擎:零随机/零表读取/零 UI——输入 = A1 产物 + 意图序列,输出 = 三事件流。
|
||||
# 排雷玩法规则引擎:零随机/零表读取/零 UI——输入 = 棋盘生成 产物 + 意图序列,输出 = 三事件流。
|
||||
# 核心契约:install 四不变式 · 初始展开 = R₀ 恰等 · 连锁不越旗 · 批次语义序仲裁(cleared > stop > 其余)。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/a2-minesweeper-rules.md(系统 ID A2——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/a2-minesweeper-rules.md(系统 ID A2·排雷玩法——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name MinesweeperRules
|
||||
extends RefCounted
|
||||
|
||||
signal cell_revealed(evt: Dictionary)
|
||||
signal flag_judged(evt: Dictionary)
|
||||
signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(A2 外发;stopped 由 A3 直发)
|
||||
signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(排雷玩法 外发;stopped 由 行动经济 直发)
|
||||
|
||||
const SUB_REASONS := ["not_adjacent", "target_revealed", "target_concealed", "flag_present", "flag_absent", "no_path", "out_of_catalog"]
|
||||
|
||||
var db
|
||||
var a3
|
||||
var action_economy
|
||||
# 棋盘状态私有(E2 封装收口 2026-09-13):对外只暴露 current_board()/reveal_record() 等查询面,
|
||||
# 禁止跨模块直读/直写原始字典——先例教训 = board_view 绕过查询面直读 revealed。
|
||||
var _board: BoardData = null
|
||||
@@ -24,9 +24,9 @@ var _hero := Vector2i.ZERO
|
||||
var _spawn: Dictionary = {}
|
||||
|
||||
|
||||
func _init(db_ref, a3_ref) -> void:
|
||||
func _init(db_ref, action_economy_ref) -> void:
|
||||
db = db_ref
|
||||
a3 = a3_ref
|
||||
action_economy = action_economy_ref
|
||||
|
||||
|
||||
# ---- 板态 ----
|
||||
@@ -35,7 +35,7 @@ func has_board() -> bool:
|
||||
return _board != null
|
||||
|
||||
|
||||
# 板产物只读出口(E2 收口——B1 快照 / 组合根 rebuild / 探针经此取板,禁再直读字段)
|
||||
# 板产物只读出口(E2 收口——战斗结算 快照 / 组合根 rebuild / 探针经此取板,禁再直读字段)
|
||||
func current_board() -> BoardData:
|
||||
return _board
|
||||
|
||||
@@ -73,20 +73,20 @@ func clear_map_cleared() -> bool:
|
||||
return safe_remaining() == 0
|
||||
|
||||
|
||||
# ---- install(编排边界相位 = A3 Idle/Waiting;Active 拒绝——防轮内换板)----
|
||||
# ---- install(编排边界相位 = 行动经济 Idle/Waiting;Active 拒绝——防轮内换板)----
|
||||
|
||||
# during_round = 轮内刷新编排路径(2026-09-12 方案 A,B1 唯一调用方):Active 相位合法——
|
||||
# during_round = 轮内刷新编排路径(2026-09-12 方案 A,战斗结算 唯一调用方):Active 相位合法——
|
||||
# 玩家挖掘中立即换图,棋盘状态(revealed/flags)随 install 重置,初始展开事件照发(initial 通道)
|
||||
func install_board(b: BoardData, during_round := false) -> Dictionary:
|
||||
if a3.phase == a3.PHASE_ACTIVE and not during_round:
|
||||
if action_economy.phase == action_economy.PHASE_ACTIVE and not during_round:
|
||||
return _outcome(false, "rejected", "phase", "")
|
||||
if a3.phase == a3.PHASE_IDLE and during_round:
|
||||
if action_economy.phase == action_economy.PHASE_IDLE and during_round:
|
||||
return _outcome(false, "rejected", "phase", "")
|
||||
if b == null:
|
||||
return _outcome(false, "rejected", "board_invalid", "out_of_catalog")
|
||||
var inv := _install_invariant_faults(b)
|
||||
if not inv.is_empty():
|
||||
push_error("[A2] install 不变式违例: %s" % str(inv))
|
||||
push_error("[排雷玩法] install 不变式违例: %s" % str(inv))
|
||||
return _outcome(false, "rejected", "board_invalid", inv[0])
|
||||
_board = b
|
||||
_revealed.clear()
|
||||
@@ -113,7 +113,7 @@ func flag_count() -> int:
|
||||
func retire_board(during_round := false) -> Dictionary:
|
||||
if _board == null:
|
||||
return _outcome(true, "executed", "", "") # 幂等受理
|
||||
if a3.phase == a3.PHASE_ACTIVE and not during_round:
|
||||
if action_economy.phase == action_economy.PHASE_ACTIVE and not during_round:
|
||||
return _outcome(false, "rejected", "phase", "")
|
||||
_board = null
|
||||
_revealed.clear()
|
||||
@@ -148,7 +148,7 @@ func _install_invariant_faults(b: BoardData) -> Array:
|
||||
return faults
|
||||
|
||||
|
||||
# 初始展开规范序:触发格 BFS、8 邻行主序(≡ A1 F1 R₀ 算子;禁外扩环、禁只开 3×3)
|
||||
# 初始展开规范序:触发格 BFS、8 邻行主序(≡ 棋盘生成 F1 R₀ 算子;禁外扩环、禁只开 3×3)
|
||||
func _initial_expansion_order(b: BoardData) -> Array:
|
||||
var order: Array = b.spawn_cells().duplicate()
|
||||
var seen := {}
|
||||
@@ -172,7 +172,7 @@ func settle_batch(intents: Array) -> Array:
|
||||
var outcomes: Array = []
|
||||
if intents.is_empty():
|
||||
return outcomes
|
||||
if a3.phase != a3.PHASE_ACTIVE or _board == null:
|
||||
if action_economy.phase != action_economy.PHASE_ACTIVE or _board == null:
|
||||
for intent in intents:
|
||||
outcomes.append(_outcome(false, "rejected", "phase", ""))
|
||||
return outcomes
|
||||
@@ -191,7 +191,7 @@ func settle_batch(intents: Array) -> Array:
|
||||
continue
|
||||
var res := _execute(item.intent)
|
||||
outcomes.append(res)
|
||||
if a3.phase != a3.PHASE_ACTIVE:
|
||||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||||
round_over = true
|
||||
return outcomes
|
||||
|
||||
@@ -202,7 +202,7 @@ func _classify(intent: Dictionary) -> int:
|
||||
var c: Vector2i = intent.get("cell", Vector2i(-9, -9))
|
||||
if _board_face_dig(c) == "" and not _board.is_mine(c):
|
||||
# 模拟 Chain(O, c) 后清图判定
|
||||
var chain := _chain_closure(c, {})
|
||||
var chain := _chain_closure(c, _flags) # 与真实执行同口径——空旗集捷径会在旗挡连锁时误判 C_dig(排雷玩法:真模拟)
|
||||
var remaining := 0
|
||||
for sc in _board.safe_cells():
|
||||
if not _revealed.has(sc) and not chain.has(sc):
|
||||
@@ -227,7 +227,7 @@ func _execute(intent: Dictionary) -> Dictionary:
|
||||
return _do_move(intent.get("cell", Vector2i(-9, -9)))
|
||||
"stop":
|
||||
return _do_stop()
|
||||
push_warning("[A2] 目录外意图: %s" % str(intent))
|
||||
push_warning("[排雷玩法] 目录外意图: %s" % str(intent))
|
||||
return _outcome(false, "rejected", "board_face", "out_of_catalog")
|
||||
|
||||
|
||||
@@ -245,8 +245,8 @@ func _do_dig(c: Vector2i) -> Dictionary:
|
||||
for cc in chain.keys():
|
||||
if cc != c and not _revealed.has(cc):
|
||||
chain_size += 1
|
||||
# 资源面/相位 + cleared 仲裁在 A3 事务内原子完成
|
||||
if not a3.request_dig(chain_size, clears):
|
||||
# 资源面/相位 + cleared 仲裁在 行动经济 事务内原子完成
|
||||
if not action_economy.request_dig(chain_size, clears):
|
||||
return _outcome(false, "rejected", "resource_face", "")
|
||||
# 事务内执行:首格 + 连锁新格揭示(BFS 规范序,含链开雾格——零特判);事件 = O′ ∖ O
|
||||
var reveal_order: Array = _chain_reveal_order(c, _flags)
|
||||
@@ -267,7 +267,7 @@ func _do_dig(c: Vector2i) -> Dictionary:
|
||||
"cell": rc, "kind": "safe", "digit": _board.digit_of(rc),
|
||||
"variant_ids": _board.variant_ids(rc), "channel": channel, "reward_eligible": true,
|
||||
})
|
||||
if a3.phase != a3.PHASE_ACTIVE:
|
||||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||||
_emit_round_end_flags_and_payload()
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
@@ -335,12 +335,12 @@ func _do_flag_place(c: Vector2i) -> Dictionary:
|
||||
sub = "flag_present"
|
||||
if sub != "":
|
||||
return _outcome(false, "rejected", "board_face", sub)
|
||||
if not a3.try_spend("flag_place"):
|
||||
if not action_economy.try_spend("flag_place"):
|
||||
return _outcome(false, "rejected", "resource_face", "")
|
||||
_flags[c] = true # 旗动作零格子事件外发(发射面封闭;反馈经调用返回值命令式驱动)
|
||||
if a3.phase != a3.PHASE_ACTIVE:
|
||||
# 旗动作花完末点 AP 同样终结轮次(A3 三出口——AP 用完与动作种类无关;缺漏使 round_end
|
||||
# 永不发出、B1 的 Waiting 结算点永不到达 = 整局软锁。playtest 缺陷②,行为探针复现)
|
||||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||||
# 旗动作花完末点 AP 同样终结轮次(行动经济 三出口——AP 用完与动作种类无关;缺漏使 round_end
|
||||
# 永不发出、战斗结算 的 Waiting 结算点永不到达 = 整局软锁。playtest 缺陷②,行为探针复现)
|
||||
_emit_round_end_flags_and_payload()
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
@@ -355,10 +355,10 @@ func _do_flag_pull(c: Vector2i) -> Dictionary:
|
||||
sub = "flag_absent"
|
||||
if sub != "":
|
||||
return _outcome(false, "rejected", "board_face", sub)
|
||||
if not a3.try_spend("flag_pull"):
|
||||
if not action_economy.try_spend("flag_pull"):
|
||||
return _outcome(false, "rejected", "resource_face", "")
|
||||
_flags.erase(c)
|
||||
if a3.phase != a3.PHASE_ACTIVE:
|
||||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||||
_emit_round_end_flags_and_payload() # 同 flag_place——末点 AP 轮末发射(playtest 缺陷②)
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
@@ -384,7 +384,7 @@ func compute_move_path(target: Vector2i) -> Array:
|
||||
return []
|
||||
if target == _hero:
|
||||
return []
|
||||
# BFS(8 邻行主序)——路径 = 合法性载体 + D1 预览数据源
|
||||
# BFS(8 邻行主序)——路径 = 合法性载体 + HUD框架预览数据源
|
||||
var prev := {_hero: _hero}
|
||||
var queue: Array = [_hero]
|
||||
while not queue.is_empty():
|
||||
@@ -405,20 +405,20 @@ func compute_move_path(target: Vector2i) -> Array:
|
||||
return path
|
||||
|
||||
|
||||
# ---- 停手(经批次入口委托 A3;先发旗判定批再委托——事件序契约 flag_judged < round_end 全路径统一)----
|
||||
# ---- 停手(经批次入口委托 行动经济;先发旗判定批再委托——事件序契约 flag_judged < round_end 全路径统一)----
|
||||
|
||||
func _do_stop() -> Dictionary:
|
||||
_emit_flags_batch() # 基线 = 事务开始旗位(停手不改旗位)
|
||||
if not a3.request_stop():
|
||||
if not action_economy.request_stop():
|
||||
return _outcome(false, "rejected", "phase", "")
|
||||
# A3 直发 stopped 路径 round_end;A2 禁双发(双发射源纪律)
|
||||
# 行动经济 直发 stopped 路径 round_end;排雷玩法 禁双发(双发射源纪律)
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
|
||||
# ---- 轮末旗判定 + 棋盘路径 round_end 外发 ----
|
||||
|
||||
func _emit_flags_batch() -> void:
|
||||
# 判定基线 = 轮次结束事务开始时旗位;行主序枚举(确定性);被拔的不判;诚实重复上报(去重归 B1)
|
||||
# 判定基线 = 轮次结束事务开始时旗位;行主序枚举(确定性);被拔的不判;诚实重复上报(去重归 战斗结算)
|
||||
var cells: Array = _flags.keys()
|
||||
cells.sort_custom(func(a, b): return a.y * 1000 + a.x < b.y * 1000 + b.x)
|
||||
for c in cells:
|
||||
@@ -427,7 +427,7 @@ func _emit_flags_batch() -> void:
|
||||
|
||||
func _emit_round_end_flags_and_payload() -> void:
|
||||
_emit_flags_batch()
|
||||
var payload: Dictionary = a3.last_round_end # 逐字段取自 A3(读取权 = 接口契约)
|
||||
var payload: Dictionary = action_economy.last_round_end # 逐字段取自 行动经济(读取权 = 接口契约)
|
||||
round_end_board.emit({"path_tag": payload.get("path_tag", ""), "continuation_hint": payload.get("continuation_hint", "")})
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 可选类供给能否按"格极稀 / 板≈0.78 / 战斗期望≈1(首件保证 + 指数递减)"三尺度被精确控制
|
||||
# Date: 2026-09-10
|
||||
# B4 奖励与道具经济:install 期身份预分配(roll_board 恰一次、消费 reward_roll 封闭流恰一 scope 批)·
|
||||
# 奖励道具经济:install 期身份预分配(roll_board 恰一次、消费 reward_roll 封闭流恰一 scope 批)·
|
||||
# 授予期零随机 · 库存关卡域(battle_over 全清)· 道具伤害恒定表值 · Ready 后只读。
|
||||
# 2026-09-11 可选类供给控制器批(用户拍板):格子池只剩即时类;可选类经 RewardSupplyController 注入
|
||||
# (战斗首件保证 + p0×decay^(j-1) 指数递减);旧 F5 格池刀保底(k_dmg)休眠;清图槽改一次性伤害。
|
||||
# 2026-09-14 伤害量值区间批(用户拍板②):区间行(power_min/max 可选配对)在身份预分配批内追加取号
|
||||
# (④ 步,既有取号序零扰动);快照 E14 口径不变——快照 power = roll 值,B1/呈现零改动;授予期零随机不变。
|
||||
# (④ 步,既有取号序零扰动);快照 E14 口径不变——快照 power = roll 值,战斗结算/呈现零改动;授予期零随机不变。
|
||||
# 切片登记:取号约定 = randi_range(0, W-1)(实现期 ADR 事实,切片内恒定)。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b4-reward-item-economy.md(系统 ID B4——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b4-reward-item-economy.md(系统 ID B4·奖励道具经济——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name RewardEconomy
|
||||
extends RefCounted
|
||||
|
||||
@@ -34,7 +34,7 @@ func _init(db_ref, rng_ref) -> void:
|
||||
_supply = RewardSupplyController.new(db.reward_config)
|
||||
|
||||
|
||||
# ---- 战斗开始(B1.begin_battle 编排调用——B4 唯一调用方纪律不变)----
|
||||
# ---- 战斗开始(战斗结算.begin_battle 编排调用——奖励道具经济 唯一调用方纪律不变)----
|
||||
# 供给控制器战斗域计数清零;库存(关卡域)不受影响——关内延续照旧
|
||||
func notify_battle_start() -> void:
|
||||
_supply.notify_battle_start()
|
||||
@@ -45,18 +45,18 @@ func notify_battle_start() -> void:
|
||||
func roll_board(eligible: Array) -> bool:
|
||||
# 相位守卫:Ready 再调 = double-roll 拒绝(E5);Retired(换图后)= 合法新批重置(E6 roll 半)
|
||||
if _phase == "ready":
|
||||
push_warning("[B4] roll_board 相位守卫拒绝(E5——double-roll 编排违约显式暴露)")
|
||||
push_warning("[奖励道具经济] roll_board 相位守卫拒绝(E5——double-roll 编排违约显式暴露)")
|
||||
return false
|
||||
if eligible.is_empty():
|
||||
_fatal("roll_board 空资格清单(E1 兜底 fail-fast——A2 不变式④前置)")
|
||||
_fatal("roll_board 空资格清单(E1 兜底 fail-fast——排雷玩法 不变式④前置)")
|
||||
return false
|
||||
# 行主序规范化 + 去重(排序责任不外推给 B1)
|
||||
# 行主序规范化 + 去重(排序责任不外推给 战斗结算)
|
||||
var seen := {}
|
||||
var list: Array = []
|
||||
for e in eligible:
|
||||
var c: Vector2i = e.cell
|
||||
if seen.has(c):
|
||||
push_warning("[B4] 资格清单重复格已去重: %s" % str(c))
|
||||
push_warning("[奖励道具经济] 资格清单重复格已去重: %s" % str(c))
|
||||
continue
|
||||
seen[c] = true
|
||||
list.append({"cell": c, "fog": bool(e.get("fog", false))})
|
||||
@@ -86,7 +86,7 @@ func roll_board(eligible: Array) -> bool:
|
||||
var target = list[stream.randi_range(0, list.size() - 1)] # 均匀随机(拍板③——雾格不加权)
|
||||
draft[target.cell] = weighted_pick(inject_rows, stream.randi_range(0, _pool_weight(inject_rows) - 1))
|
||||
# ④ 伤害量值区间批(2026-09-14):身份定稿后逐格 roll power(区间行恰一取号,定值行零取号);
|
||||
# 授予期零随机纪律不变——快照 E14 口径,B1/呈现零改动
|
||||
# 授予期零随机纪律不变——快照 E14 口径,战斗结算/呈现零改动
|
||||
for cell in draft.keys():
|
||||
var row: Dictionary = _row_by_id(draft[cell])
|
||||
if row.has("power_min"):
|
||||
@@ -100,7 +100,7 @@ func roll_board(eligible: Array) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
# ---- 关卡节点奖励(2026-09-11 关卡扩展批——线性奖励点,正式归属 B4/C1 修订)----
|
||||
# ---- 关卡节点奖励(2026-09-11 关卡扩展批——线性奖励点,正式归属 奖励道具经济/关卡节点地图 修订)----
|
||||
# 奖励点 = 用户拍板的关卡节点类型:在本表全部可选类活行(damage_item / heal_pack,即"主动道具")
|
||||
# 中 roll 一件并直接入库。消耗 reward_roll 恰一 scope(与 board install 批同流异 scope——流集合封闭不变)。
|
||||
# cell=(-1,-1)/round_index=0 = 非格子来源标记(关卡节点授予,区别于安全格拾取的呈现面)。
|
||||
@@ -139,7 +139,7 @@ func query_identity(cell: Vector2i):
|
||||
|
||||
|
||||
# 身份完整快照(轮内刷新批次隔离用——facts 缓冲期捕获,授予期 mapping 可能已换批):
|
||||
# 返回与 grant 相同结构的快照(display_name/power/effect_id 齐备),供 B1 冻结授予直通
|
||||
# 返回与 grant 相同结构的快照(display_name/power/effect_id 齐备),供 战斗结算 冻结授予直通
|
||||
func query_full_snapshot(cell: Vector2i):
|
||||
if _phase != "ready":
|
||||
_diag("query_full_snapshot 非 Ready 相位")
|
||||
@@ -150,7 +150,7 @@ func query_full_snapshot(cell: Vector2i):
|
||||
return _snapshot(_row_by_id(rid), int(_power_roll.get(cell, -1)))
|
||||
|
||||
|
||||
# ---- 受理面(B1 编排期调用;E6 收口 2026-09-13:失败统一返回 null——此前 false/null 混用)----
|
||||
# ---- 受理面(战斗结算 编排期调用;E6 收口 2026-09-13:失败统一返回 null——此前 false/null 混用)----
|
||||
|
||||
func grant(cell: Vector2i):
|
||||
if _phase != "ready":
|
||||
@@ -160,7 +160,7 @@ func grant(cell: Vector2i):
|
||||
_diag("grant 清单外坐标(E19)")
|
||||
return null
|
||||
if _granted.has(cell):
|
||||
_diag("grant 同格重复受理(E18 幂等兜底——受理恰一次归 B1)")
|
||||
_diag("grant 同格重复受理(E18 幂等兜底——受理恰一次归 战斗结算)")
|
||||
return _granted[cell]
|
||||
var row: Dictionary = _row_by_id(_mapping[cell])
|
||||
var snap := _snapshot(row, int(_power_roll.get(cell, -1)))
|
||||
@@ -202,7 +202,7 @@ func stock(snapshot: Dictionary, source_cell: Vector2i, round_index: int):
|
||||
return inst
|
||||
|
||||
|
||||
# 窗口选用消耗:零游戏态守卫(照常消耗——E10;满血/零效果守卫归 B1)
|
||||
# 窗口选用消耗:零游戏态守卫(照常消耗——E10;满血/零效果守卫归 战斗结算)
|
||||
func use(index: int):
|
||||
if _inventory_cleared:
|
||||
_diag("use 战斗已清库")
|
||||
@@ -275,7 +275,7 @@ func retire_mapping() -> bool:
|
||||
return true
|
||||
|
||||
|
||||
# ---- F6 当量供给(B1 条款①对照实物输入)----
|
||||
# ---- F6 当量供给(战斗结算 条款①对照实物输入)----
|
||||
|
||||
func equivalence_supply() -> Dictionary:
|
||||
var live: Array = []
|
||||
@@ -339,9 +339,9 @@ func _snapshot(row: Dictionary, rolled_power: int = -1) -> Dictionary:
|
||||
|
||||
|
||||
func _diag(msg: String) -> void:
|
||||
push_warning("[B4] " + msg)
|
||||
push_warning("[奖励道具经济] " + msg)
|
||||
|
||||
|
||||
func _fatal(msg: String) -> void:
|
||||
push_error("[B4/fatal] " + msg)
|
||||
push_error("[奖励道具经济/fatal] " + msg)
|
||||
assert(false, msg)
|
||||
@@ -1,12 +1,12 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 可选类供给能否按"格极稀 <0.01 / 板≈0.78 / 战斗期望≈1(首件保证 + 指数递减)"三尺度被精确控制
|
||||
# Date: 2026-09-11
|
||||
# 可选类供给控制器(2026-09-11 用户拍板批——B4 私有组合件):
|
||||
# 可选类供给控制器(2026-09-11 用户拍板批——奖励道具经济 私有组合件):
|
||||
# 格子权重池不再产出可选类;本控制器在 install 期决定"本板是否注入 1 件可选类"——
|
||||
# 战斗内尚未获取首件 → 本板必注入(首件保证);已获取 j 件 → 概率 p0×decay^(j-1)(指数下降、永不绝育)。
|
||||
# 行值快照(p0/decay 构造期快照——抗热重载,B2 同款);零自有随机数(判定取号经 B4 传入的 reward_roll
|
||||
# 封闭流实例——Rule 5);"获取"以实际发放入库为准(预分配未挖到不计数——授予侧经 B4.grant 挂钩上报)。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b4-reward-item-economy.md(系统 ID B4——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 行值快照(p0/decay 构造期快照——抗热重载,敌人系统 同款);零自有随机数(判定取号经 奖励道具经济 传入的 reward_roll
|
||||
# 封闭流实例——Rule 5);"获取"以实际发放入库为准(预分配未挖到不计数——授予侧经 奖励道具经济.grant 挂钩上报)。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b4-reward-item-economy.md(系统 ID B4·奖励道具经济——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name RewardSupplyController
|
||||
extends RefCounted
|
||||
|
||||
@@ -20,12 +20,12 @@ func _init(reward_config) -> void:
|
||||
_decay = clampf(float(reward_config.active_decay), 0.001, 1.0)
|
||||
|
||||
|
||||
# B1.begin_battle 编排调用(B4 转发)——战斗域计数重置;跨战斗库存延续不受影响
|
||||
# 战斗结算.begin_battle 编排调用(奖励道具经济 转发)——战斗域计数重置;跨战斗库存延续不受影响
|
||||
func notify_battle_start() -> void:
|
||||
_granted = 0
|
||||
|
||||
|
||||
# install 期注入判定(B4.roll_board 在身份批 + 清图槽之后调用;取号 = 传入封闭流恰一次)
|
||||
# install 期注入判定(奖励道具经济.roll_board 在身份批 + 清图槽之后调用;取号 = 传入封闭流恰一次)
|
||||
func should_inject(stream: RandomNumberGenerator) -> bool:
|
||||
if _granted == 0:
|
||||
return true # 首件保证
|
||||
@@ -33,7 +33,7 @@ func should_inject(stream: RandomNumberGenerator) -> bool:
|
||||
return stream.randf() < p
|
||||
|
||||
|
||||
# B4.grant 实际发放可选类时上报(发放即获取;满血拒"使用"不影响——那是消耗侧语义)
|
||||
# 奖励道具经济.grant 实际发放可选类时上报(发放即获取;满血拒"使用"不影响——那是消耗侧语义)
|
||||
func notify_granted() -> void:
|
||||
_granted += 1
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 RngService autoload——封闭命名流单点(Rule 5)。玩法代码禁止 randomize()/自建 RandomNumberGenerator。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# RngService autoload(数据配置)——封闭命名流单点(Rule 5)。玩法代码禁止 randomize()/自建 RandomNumberGenerator。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# R2 派生公式(黄金向量已预验证):derive = (SALT XOR wrap64(master + scope×GOLDEN)) AND 0x7FFF…
|
||||
# 取号语义:全新实例首个 open_stream = scope 0(B4 预推进术语的登记口径)。
|
||||
# 取号语义:全新实例首个 open_stream = scope 0(奖励道具经济 预推进术语的登记口径)。
|
||||
extends Node
|
||||
|
||||
const STREAM_NAMES := ["board_gen", "reward_roll", "encounter"] # 2026-09-12 增 encounter(遭遇随机化批,用户拍板——registry rng_service 条目同步)
|
||||
|
||||
# 分段表达式书写(D2 R2 实现级规约——禁 ≥2^63 完整字面量)
|
||||
# 分段表达式书写(数据配置 R2 实现级规约——禁 ≥2^63 完整字面量)
|
||||
const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15
|
||||
const SALT_BOARD_GEN := (0xA0761D64 << 32) | 0x78BD642F # 0xA0761D6478BD642F
|
||||
const SALT_REWARD_ROLL := (0xE7037ED1 << 32) | 0xA0B428DB # 0xE7037ED1A0B428DB
|
||||
const SALT_ENCOUNTER := (0x5DEECE66D << 32) | 0x2545F491 # 0x5DEECE66D2545F491(遭遇骰——2026-09-12)
|
||||
const SALT_ENCOUNTER := (0x8EBC6AF0 << 32) | 0x9C88C6E3 # 0x8EBC6AF09C88C6E3(遭遇骰——registry stream_seed_derivation 权威值;旧值 0x5DEECE66D<<32 超 int64 被截断,黄金向量漂移)
|
||||
const OUT_MASK := 0x7FFFFFFFFFFFFFFF
|
||||
const INT64_MIN := -9223372036854775808
|
||||
|
||||
# master_seed 只读出口(E5 封装收口 2026-09-13):写入只经 set_master_seed(计数器全零断言
|
||||
# 不可绕过);外部读 master_seed 供 D3 快照契约观测。
|
||||
# 不可绕过);外部读 master_seed 供 存档序列化 快照契约观测。
|
||||
var _master_seed: int = -1 # -1 = 未播种(取数即致命,EC7)
|
||||
var master_seed: int:
|
||||
get: return _master_seed
|
||||
@@ -60,7 +60,7 @@ func set_master_seed(seed_val: int) -> void:
|
||||
_counters["reward_roll"] = 0
|
||||
|
||||
|
||||
# 切片登记:冒险开始重播种入口(正式侧调用权归 C2;切片主菜单 = 冒险起点)
|
||||
# 切片登记:冒险开始重播种入口(正式侧调用权归 冒险结构;切片主菜单 = 冒险起点)
|
||||
func new_adventure(seed_val: int) -> void:
|
||||
_counters["board_gen"] = 0
|
||||
_counters["reward_roll"] = 0
|
||||
@@ -74,7 +74,7 @@ func open_stream(stream: String) -> RandomNumberGenerator:
|
||||
push_error("[RngService] 未知名流: %s" % stream)
|
||||
assert(false, "未知名流(EC12)")
|
||||
if _master_seed < 0:
|
||||
push_error("[RngService] 未设 master_seed 即取数(EC7——C2 未接线的显式暴露)")
|
||||
push_error("[RngService] 未设 master_seed 即取数(EC7——冒险结构 未接线的显式暴露)")
|
||||
assert(false, "未播种流取数致命(EC7)")
|
||||
var scope: int = _counters[stream]
|
||||
_counters[stream] = scope + 1
|
||||
@@ -83,6 +83,6 @@ func open_stream(stream: String) -> RandomNumberGenerator:
|
||||
return rg
|
||||
|
||||
|
||||
# 只读计数器(D3 快照契约的观测面)
|
||||
# 只读计数器(存档序列化 快照契约的观测面)
|
||||
func counter(stream: String) -> int:
|
||||
return _counters[stream]
|
||||
@@ -1,10 +1,10 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# B3 统一状态容器:三向效果同构实例 · 单一时限量纲 remaining_settlements(先兑付后衰减)·
|
||||
# 增益状态·统一状态容器:三向效果同构实例 · 单一时限量纲 remaining_settlements(先兑付后衰减)·
|
||||
# 按行叠层(stack 授予不触碰 R)· 授予时数值快照抗热重载 · 零随机零信号。
|
||||
# 注:MVP 单敌假设经 B2 回验不成立(定多敌)——enemy 侧 side-wide 语义按 registry 改写执行。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b3-status-effect.md(系统 ID B3——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 注:MVP 单敌假设经 敌人系统 回验不成立(定多敌)——enemy 侧 side-wide 语义按 registry 改写执行。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b3-status-effect.md(系统 ID B3·增益状态——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name StatusContainer
|
||||
extends RefCounted
|
||||
|
||||
@@ -19,21 +19,21 @@ func _init(db_ref) -> void:
|
||||
|
||||
func notify_battle_start() -> bool:
|
||||
if _open:
|
||||
push_warning("[B3] Active 下重复 notify_battle_start(EC11)")
|
||||
push_warning("[增益状态] Active 下重复 notify_battle_start(EC11)")
|
||||
return false
|
||||
_open = true
|
||||
_instances.clear()
|
||||
return true
|
||||
|
||||
|
||||
# 唯一授予入口(挖掘期零直写——B1 结算编排期统一调用)
|
||||
# 唯一授予入口(挖掘期零直写——战斗结算 结算编排期统一调用)
|
||||
func apply_effect(effect_id: String, source: Dictionary) -> bool:
|
||||
if not _open:
|
||||
push_warning("[B3] 域闭授予(EC1)")
|
||||
push_warning("[增益状态] 域闭授予(EC1)")
|
||||
return false
|
||||
var row: Dictionary = db.status_effect_row(effect_id)
|
||||
if row.is_empty():
|
||||
push_warning("[B3] 未知 effect_id(EC2): %s" % effect_id)
|
||||
push_warning("[增益状态] 未知 effect_id(EC2): %s" % effect_id)
|
||||
return false
|
||||
var policy: String = row.stack_policy
|
||||
var dur: int = int(row.duration_settlements)
|
||||
@@ -57,12 +57,12 @@ func apply_effect(effect_id: String, source: Dictionary) -> bool:
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
if inst.layers >= max_stack:
|
||||
return false # 满层拒绝原子——容器零写入(溢出行归 B1)
|
||||
return false # 满层拒绝原子——容器零写入(溢出行归 战斗结算)
|
||||
inst.layers += 1 # stack 授予不触碰 R(spec 决议)
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display))
|
||||
return true
|
||||
push_warning("[B3] 未知 stack_policy")
|
||||
push_warning("[增益状态] 未知 stack_policy")
|
||||
return false
|
||||
|
||||
|
||||
@@ -99,12 +99,12 @@ func query_active(side: String) -> Array:
|
||||
"display_name": inst.snap_display,
|
||||
"snap_magnitude": inst.snap_m,
|
||||
"snap_unit": inst.snap_unit,
|
||||
"effective_value": inst.layers * inst.snap_m, # L×m 加算(percent 越域不 clamp——收口归 B1)
|
||||
"effective_value": inst.layers * inst.snap_m, # L×m 加算(percent 越域不 clamp——收口归 战斗结算)
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
# 池层数读值(B1 战斗段伏击池 L 的消费面)
|
||||
# 池层数读值(战斗结算·战斗段伏击池 L 的消费面)
|
||||
func pool_layers(effect_id: String) -> int:
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
@@ -112,7 +112,7 @@ func pool_layers(effect_id: String) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# 轮末衰减:先兑付后衰减;每实例恰 −1;归 0 移除;返回移除数 k(恰一次执行由 B1 编排信任承载)
|
||||
# 轮末衰减:先兑付后衰减;每实例恰 −1;归 0 移除;返回移除数 k(恰一次执行由 战斗结算 编排信任承载)
|
||||
func decay_round() -> int:
|
||||
if not _open:
|
||||
return 0 # EC13 域闭兜底读法——容器恒空,无独立分支不诊断
|
||||
@@ -130,7 +130,7 @@ func decay_round() -> int:
|
||||
|
||||
func notify_battle_end() -> bool:
|
||||
if not _open:
|
||||
push_warning("[B3] BattleClosed 下重复 notify_battle_end(EC11)")
|
||||
push_warning("[增益状态] BattleClosed 下重复 notify_battle_end(EC11)")
|
||||
return false
|
||||
_open = false
|
||||
_instances.clear() # 全清不跨战斗
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# B2 战斗单位系统:hero/enemy 统一受伤死亡接口 · 行为环确定性轮转(死亡即不动手/先作用后推进)·
|
||||
# 行值快照抗热重载 · side-wide 伏击池读值由调用方注入(B2 无池写面)· 编排被动零事件。
|
||||
# 切片登记接口漂移:heal() 为切片新增(B1 治疗 clamp 需要写入口;B2 API 封闭枚举原本无治疗面——
|
||||
# 正式侧应走 B2 GDD 修订,切片以头注登记)。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b2-enemy-system.md(系统 ID B2——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 敌人系统·战斗单位容器:hero/enemy 统一受伤死亡接口 · 行为环确定性轮转(死亡即不动手/先作用后推进)·
|
||||
# 行值快照抗热重载 · side-wide 伏击池读值由调用方注入(敌人系统 无池写面)· 编排被动零事件。
|
||||
# 切片登记接口漂移:heal() 为切片新增(战斗结算 治疗 clamp 需要写入口;敌人系统 API 封闭枚举原本无治疗面——
|
||||
# 正式侧应走 敌人系统 GDD 修订,切片以头注登记)。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b2-enemy-system.md(系统 ID B2·敌人系统——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name UnitContainer
|
||||
extends RefCounted
|
||||
|
||||
@@ -25,35 +25,35 @@ func _init(db_ref) -> void:
|
||||
|
||||
# E4 收口(2026-09-13):加成值经参数显式传入(atk_override/max_hp_override,-1 = 表值兜底)——
|
||||
# 此前调用方写 db.hero 表值再由本类读表 = 共享可变单例传参(双写叠加 6→18→54 教训根源)。
|
||||
# 接口漂移登记:spawn 签名 +2 参数(B2 GDD API 封闭枚举——正式侧 GDD/registry 修订批随 ADR-0009)。
|
||||
# enemy_atk_override(2026-09-14 敌方攻击区间批):B1 遭遇骰同流的 spawn 期整战持久 roll 结果
|
||||
# (-1 = 表值定值路径;≥0 = 行值替换后再乘关卡 atk_mult——修饰语义不变)。B2 零随机纪律保持。
|
||||
# 接口漂移登记:spawn 签名 +2 参数(敌人系统 GDD API 封闭枚举——正式侧 GDD/registry 修订批随 ADR-0009)。
|
||||
# enemy_atk_override(2026-09-14 敌方攻击区间批):战斗结算 遭遇骰同流的 spawn 期整战持久 roll 结果
|
||||
# (-1 = 表值定值路径;≥0 = 行值替换后再乘关卡 atk_mult——修饰语义不变)。敌人系统 零随机纪律保持。
|
||||
func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override: int = -1,
|
||||
atk_override: int = -1, max_hp_override: int = -1, enemy_atk_override: int = -1) -> bool:
|
||||
if _spawned:
|
||||
push_warning("[B2] Alive 相位重复 spawn(相位守卫)")
|
||||
push_warning("[敌人系统] Alive 相位重复 spawn(相位守卫)")
|
||||
return false
|
||||
var row: Dictionary = db.resolve_enemy_rowset(level_index, node_depth)
|
||||
if row.is_empty():
|
||||
push_error("[B2] 遭遇行解析失败/越域 (%d,%d)" % [level_index, node_depth])
|
||||
push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d)" % [level_index, node_depth])
|
||||
assert(false, "遭遇行悬空(R1 前置守卫)")
|
||||
return false
|
||||
var eff_atk: int = atk_override if atk_override >= 1 else int(db.hero.atk)
|
||||
var eff_max_hp: int = max_hp_override if max_hp_override >= 1 else int(db.hero.max_hp)
|
||||
if hero_hp > eff_max_hp or hero_hp <= 0:
|
||||
push_warning("[B2] hero_hp 入参越界(EC16): %d" % hero_hp)
|
||||
push_warning("[敌人系统] hero_hp 入参越界(EC16): %d" % hero_hp)
|
||||
return false
|
||||
# max_hp = 生效上限(非入参 hero_hp)——2026-09-11 关卡扩展批修复:跨战斗携带 HP 时上限不得压扁
|
||||
# (恢复点/治疗包的满血守卫与治疗上限随之失效——切片登记接口语义澄清)
|
||||
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": "探险者", "steps": 0, "pattern": [], "enemy_id": "hero"}
|
||||
_enemies.clear() # C3 DifficultyTable 修饰(2026-09-12 结构位兑现):施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10)
|
||||
_enemies.clear() # 难度曲线与Boss DifficultyTable 修饰(2026-09-12 结构位兑现):施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10)
|
||||
var mod: Dictionary = db.resolve_difficulty_modifiers(level_index, node_depth)
|
||||
var hp_mult: float = float(mod.get("hp_mult", 1.0))
|
||||
var atk_mult: float = float(mod.get("atk_mult", 1.0))
|
||||
# unit_count_override ≥1 = B1 遭遇骰结果(B2 零随机纪律——收到即确定值;-1 = 行值定值路径)
|
||||
# unit_count_override ≥1 = 战斗结算 遭遇骰结果(敌人系统 零随机纪律——收到即确定值;-1 = 行值定值路径)
|
||||
var count: int = unit_count_override if unit_count_override >= 1 else int(row.unit_count)
|
||||
if count < 1 or count > 3:
|
||||
push_warning("[B2] 实例化数量越域: %d" % count)
|
||||
push_warning("[敌人系统] 实例化数量越域: %d" % count)
|
||||
return false
|
||||
for i in range(count):
|
||||
var eff_row_atk: int = enemy_atk_override if enemy_atk_override >= 0 else int(row.atk)
|
||||
@@ -67,7 +67,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override:
|
||||
"heavy_multiplier": int(row.heavy_multiplier),
|
||||
"pattern": Array(row.pattern), # 行值快照(拷贝)
|
||||
"advantage_per_round_cap": int(row.advantage_per_round_cap),
|
||||
"armor_type": String(row.get("armor_type", "none")), # 护甲类型快照(克制矩阵消费面——B1 战斗段)
|
||||
"armor_type": String(row.get("armor_type", "none")), # 护甲类型快照(克制矩阵消费面——战斗结算·战斗段)
|
||||
"steps": 0,
|
||||
"alive": true,
|
||||
"is_hero": false,
|
||||
@@ -126,11 +126,11 @@ func apply_damage(u: Dictionary, n: int) -> bool:
|
||||
return false
|
||||
u.hp = maxi(0, u.hp - n)
|
||||
if u.hp == 0:
|
||||
u.alive = false # Dead 不可逆(死亡判定时机归 B1)
|
||||
u.alive = false # Dead 不可逆(死亡判定时机归 战斗结算)
|
||||
return true
|
||||
|
||||
|
||||
# 治疗(切片登记接口漂移——B1 治疗 clamp 的写入口):返回实际回复量
|
||||
# 治疗(切片登记接口漂移——战斗结算 治疗 clamp 的写入口):返回实际回复量
|
||||
func heal(u: Dictionary, n: int) -> int:
|
||||
if n < 0 or u.is_empty() or _despawned or not u.alive:
|
||||
_diag("heal 非法/失效句柄")
|
||||
@@ -174,7 +174,7 @@ func all_enemies() -> Array:
|
||||
return _enemies.duplicate()
|
||||
|
||||
|
||||
# F3 机制算术纯函数(B1 预截流消费面):cap 判定在授予前;守恒 granted + overflow = mines_r
|
||||
# F3 机制算术纯函数(战斗结算 预截流消费面):cap 判定在授予前;守恒 granted + overflow = mines_r
|
||||
func ambush_grant_plan(mines_r: int, cap: int) -> Dictionary:
|
||||
var granted: int = mini(mines_r, cap)
|
||||
return {"granted": granted, "overflow": mines_r - granted}
|
||||
@@ -191,4 +191,4 @@ func despawn_all() -> bool:
|
||||
|
||||
|
||||
func _diag(msg: String) -> void:
|
||||
push_warning("[B2] " + msg)
|
||||
push_warning("[敌人系统] " + msg)
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 ActionEconomyTable schema(A3 回填值,单行全局;Rule 4 豁免)。
|
||||
# 数据配置 ActionEconomyTable schema(行动经济 回填值,单行全局;Rule 4 豁免)。
|
||||
# 数值域与跨字段校验依据 design/archive/gdd-2026-09-14/a3-action-economy.md "ActionEconomyTable Schema 定稿"。
|
||||
class_name ActionEconomyTable
|
||||
extends Resource
|
||||
@@ -34,7 +34,7 @@ func validate() -> Array:
|
||||
errs.append({"category": "frozen", "message": "move_ap_cost 结构冻结为 0: %d" % move_ap_cost})
|
||||
if chain_ap_cost != 0:
|
||||
errs.append({"category": "frozen", "message": "chain_ap_cost 结构冻结为 0: %d" % chain_ap_cost})
|
||||
# 跨字段(A3 三轮评审增补):任一动作成本 ≤ ap_per_round,违规加载期致命
|
||||
# 跨字段(行动经济 三轮评审增补):任一动作成本 ≤ ap_per_round,违规加载期致命
|
||||
for pair in [["dig_ap_cost", dig_ap_cost], ["flag_place_ap_cost", flag_place_ap_cost], ["flag_pull_ap_cost", flag_pull_ap_cost]]:
|
||||
if pair[1] > ap_per_round:
|
||||
errs.append({"category": "cross_field", "message": "%s(%d) > ap_per_round(%d)——挖掘/旗动作永久不可用软锁" % [pair[0], pair[1], ap_per_round]})
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 BoardGenTable schema(A1 回填值;键 (level_index, node_depth)——首个进度键表)。
|
||||
# 数据配置 BoardGenTable schema(棋盘生成 回填值;键 (level_index, node_depth)——首个进度键表)。
|
||||
# 依据 design/archive/gdd-2026-09-14/a1-board-generation.md "BoardGenTable Schema 定稿" + F2/F2b/F2c 谓词。
|
||||
class_name BoardGenTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary]
|
||||
const SOLVER_RULES := ["single", "single_subset"]
|
||||
const FOG_RATIO_ABS_CAP := 0.25 # B1 修订——雾占比绝对上限
|
||||
const FOG_RATIO_ABS_CAP := 0.25 # 战斗结算 修订——雾占比绝对上限
|
||||
const COST_BUDGET := 16384 # F2c provisional 量级护栏(禁墙钟逃生)
|
||||
const TARGET_ABS_FLOOR := 0.85 # B1 修订——target 域下限(P1 绝对锚)
|
||||
const TARGET_ABS_FLOOR := 0.85 # 战斗结算 修订——target 域下限(P1 绝对锚)
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
@@ -53,7 +53,7 @@ func validate() -> Array:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) max_gen_attempts 越域 [1,32]" % [L, D]})
|
||||
var fc: int = int(row.get("fog_region_count", 0))
|
||||
var fr: float = float(row.get("fog_target_ratio", 0.0))
|
||||
# F2 规范合取形 + 雾占比绝对上限(B1 修订)
|
||||
# F2 规范合取形 + 雾占比绝对上限(战斗结算 修订)
|
||||
var fog_ok := (fc == 0 and fr == 0.0) or (fc >= 1 and fr > 0.0 and fr <= minf(8.0 * d, FOG_RATIO_ABS_CAP) and fr < 1.0)
|
||||
if not fog_ok:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) fog_domain_ok 违例(count=%d ratio=%.3f)" % [L, D, fc, fr]})
|
||||
@@ -69,7 +69,7 @@ func validate() -> Array:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) cost_feasible 违例 attempts×W×H ≤ %d" % [L, D, COST_BUDGET]})
|
||||
var biomes: Array = row.get("board_biomes", [])
|
||||
if biomes.is_empty():
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) board_biomes 非空域校验违例(A4⑮)" % [L, D]})
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) board_biomes 非空域校验违例(地形变异⑮)" % [L, D]})
|
||||
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
|
||||
for L in anchor_ok.keys():
|
||||
if not anchor_ok[L]:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-12
|
||||
# D2 DifficultyTable schema(C3 GDD Core Rule 1 兑现——B2 Rule 10 "接口未定 provisional" 的建表兑现,走 D2 接口漂移规则)。
|
||||
# 全局修饰结构位:按 (L,D) 精确匹配施加乘数于 EnemyTable 行值(B2.spawn 实例化时、行值快照前)。
|
||||
# MVP 空表(零行为——AC-10);首个使用场景 = 精英节点(C1 定稿联动,C3 OQ5);Boss 走 Dmax 直填行不走本表。
|
||||
# 数据配置 DifficultyTable schema(难度曲线与Boss GDD Core Rule 1 兑现——敌人系统 Rule 10 "接口未定 provisional" 的建表兑现,走 数据配置 接口漂移规则)。
|
||||
# 全局修饰结构位:按 (L,D) 精确匹配施加乘数于 EnemyTable 行值(敌人系统.spawn 实例化时、行值快照前)。
|
||||
# MVP 空表(零行为——AC-10);首个使用场景 = 精英节点(关卡节点地图 定稿联动,难度曲线与Boss OQ5);Boss 走 Dmax 直填行不走本表。
|
||||
class_name DifficultyTable
|
||||
extends Resource
|
||||
|
||||
@@ -24,7 +24,7 @@ func validate() -> Array:
|
||||
seen[key] = true
|
||||
if L < 1 or D < 0:
|
||||
errs.append({"category": "domain", "message": "难度键越域 (%d,%d)" % [L, D]})
|
||||
# 乘数越域 = warning 级(使用时 clamp——组合事故非配置事故,C3 GDD Edge Case 预登记)
|
||||
# 乘数越域 = warning 级(使用时 clamp——组合事故非配置事故,难度曲线与Boss GDD Edge Case 预登记)
|
||||
for k in ["hp_mult", "atk_mult"]:
|
||||
var v: float = float(row.get(k, 1.0))
|
||||
if v < MULT_MIN or v > MULT_MAX:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 EnemyTable / HeroTable schema(B2 回填值)。EnemyTable 键 (L,D,enemy_id),每行集恰一敌种;
|
||||
# 数据配置 EnemyTable / HeroTable schema(敌人系统 回填值)。EnemyTable 键 (L,D,enemy_id),每行集恰一敌种;
|
||||
# HeroTable 单行全局(Rule 4 豁免)。依据 design/archive/gdd-2026-09-14/b2-enemy-system.md Schema 节。
|
||||
class_name EnemyTable
|
||||
extends Resource
|
||||
@@ -39,7 +39,7 @@ func validate() -> Array:
|
||||
var uc: int = int(row.get("unit_count", 0))
|
||||
if uc < 1 or uc > 3:
|
||||
errs.append({"category": "domain", "message": "%s unit_count 越域 [1,3]" % eid})
|
||||
# unit_count_max(可省略——遭遇随机化批,C3 GDD Core Rule 8):区间 [unit_count, max],缺省 = 定值
|
||||
# unit_count_max(可省略——遭遇随机化批,难度曲线与Boss GDD Core Rule 8):区间 [unit_count, max],缺省 = 定值
|
||||
if row.has("unit_count_max"):
|
||||
var ucm: int = int(row.get("unit_count_max", uc))
|
||||
if ucm < uc or ucm > 3:
|
||||
@@ -51,7 +51,7 @@ func validate() -> Array:
|
||||
if atk < 0 or atk > 999:
|
||||
errs.append({"category": "domain", "message": "%s atk 越域 [0,999]" % eid})
|
||||
# atk_min/atk_max(可省略配对——2026-09-14 敌方攻击区间批):遭遇骰同流 spawn 期整战持久 roll;
|
||||
# 中点锚 = A3 条款①同款期望当量纪律(调区间必同步调 atk)
|
||||
# 中点锚 = 行动经济 条款①同款期望当量纪律(调区间必同步调 atk)
|
||||
var has_amin: bool = row.has("atk_min")
|
||||
var has_amax: bool = row.has("atk_max")
|
||||
if has_amin != has_amax:
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 HeroTable schema(B2 接口漂移新增;单行全局,Rule 4 豁免)。
|
||||
# 数据配置 HeroTable schema(敌人系统 接口漂移新增;单行全局,Rule 4 豁免)。
|
||||
# atk = 基础攻击恒存在(2026-09-11 用户裁决):域 [1,999]、0 = 加载拒绝——零基线死锁 schema 层杜绝。
|
||||
class_name HeroTable
|
||||
extends Resource
|
||||
|
||||
@export var max_hp: int = 50 # [1,999] knob(provisional,溯源 = 原型锚)
|
||||
@export var atk: int = 6 # [1,999] knob——基础攻击(溯源 = 概念原型 HERO_BASE_ATK,provisional 归 C3);消费面 = B1 战斗段先手攻击
|
||||
@export var atk: int = 6 # [1,999] knob——基础攻击(溯源 = 概念原型 HERO_BASE_ATK,provisional 归 难度曲线与Boss);消费面 = 战斗结算·战斗段先手攻击
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证
|
||||
# Date: 2026-09-11
|
||||
# D2 LevelTable schema(provisional——C1 关卡节点地图未设计,本表为线性切片形态的结构位:
|
||||
# nodes 数组即未来节点链的数据承载面;路径分支/奖励点分布的定稿权归 C1/C3)。
|
||||
# 数据配置 LevelTable schema(provisional——关卡节点地图未设计,本表为线性切片形态的结构位:
|
||||
# nodes 数组即未来节点链的数据承载面;路径分支/奖励点分布的定稿权归 关卡节点地图/难度曲线与Boss)。
|
||||
# 节点类型 closed enum:battle(战斗,须带 node_depth)/ reward(随机主动道具点)/ rest(血量恢复点)/ boss(关底 Boss,须带 node_depth)。
|
||||
class_name LevelTable
|
||||
extends Resource
|
||||
@@ -51,7 +51,7 @@ func validate() -> Array:
|
||||
errs.append({"category": "structural", "message": "level %d Boss 节点数 = %d(须恰一)" % [L, boss_count]})
|
||||
if battle_count < 2:
|
||||
errs.append({"category": "structural", "message": "level %d 战斗节点(含 Boss)= %d(≥2 才构成关卡)" % [L, battle_count]})
|
||||
# 线性切片纪律:末节点必须是 Boss(关底收尾——C2 关卡定义)
|
||||
# 线性切片纪律:末节点必须是 Boss(关底收尾——冒险结构 关卡定义)
|
||||
if String(nodes[nodes.size() - 1].get("type", "")) != "boss":
|
||||
errs.append({"category": "structural", "message": "level %d 末节点非 Boss(线性关卡以 Boss 收尾)" % L})
|
||||
return errs
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 RewardConfigTable schema(B4 full 评审接口漂移新增;单行全局——B4 标量 knob 的 Rule 1 载体)。
|
||||
# 数据配置 RewardConfigTable schema(奖励道具经济 full 评审接口漂移新增;单行全局——奖励道具经济 标量 knob 的 Rule 1 载体)。
|
||||
class_name RewardConfigTable
|
||||
extends Resource
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 RewardTable / RewardConfigTable schema(B4 回填值)。
|
||||
# 数据配置 RewardTable / RewardConfigTable schema(奖励道具经济 回填值)。
|
||||
# RewardTable 行 = Dictionary,键 reward_id(Rule 4 豁免:定义表);跨字段校验族 + 池护栏。
|
||||
class_name RewardTable
|
||||
extends Resource
|
||||
@@ -68,6 +68,6 @@ func validate() -> Array:
|
||||
if cat == "buff":
|
||||
errs.append({"category": "cross_field", "message": "%s buff 行禁区间字段(power 恒 0)" % rid})
|
||||
if power != int((pmin + pmax) / 2.0):
|
||||
# 中点锚:power = 区间中点(A3 条款①期望当量锚——调区间必同步调 power)
|
||||
# 中点锚:power = 区间中点(行动经济 条款①期望当量锚——调区间必同步调 power)
|
||||
errs.append({"category": "cross_field", "message": "%s power(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [rid, power, (pmin + pmax) / 2]})
|
||||
return errs
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 SettlementTable schema(B1 回填值;单行全局,Rule 4 豁免)——八字段口径。
|
||||
# 数据配置 SettlementTable schema(战斗结算 回填值;单行全局,Rule 4 豁免)——八字段口径。
|
||||
# 三行致命 manifest:segorder_violation / poolmap_dangling / pperfect_oob。
|
||||
class_name SettlementTable
|
||||
extends Resource
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 StatusEffectTable schema(B3 回填值;行 = Dictionary,键 effect_id;Rule 4 豁免)。
|
||||
# 数据配置 StatusEffectTable schema(增益状态 回填值;行 = Dictionary,键 effect_id;Rule 4 豁免)。
|
||||
# 依据 design/archive/gdd-2026-09-14/b3-status-effect.md "StatusEffectTable Schema 定稿"。
|
||||
class_name StatusEffectTable
|
||||
extends Resource
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# D2 VariationTable schema(A4 回填值;行 = Dictionary,键 variant_id)。
|
||||
# 数据配置 VariationTable schema(地形变异 回填值;行 = Dictionary,键 variant_id)。
|
||||
# 依据 design/archive/gdd-2026-09-14/a4-terrain-variation.md "VariationTable Schema 定稿"。
|
||||
class_name VariationTable
|
||||
extends Resource
|
||||
@@ -38,7 +38,7 @@ func validate() -> Array:
|
||||
errs.append({"category": "enum", "message": "%s info_mask 出域: %s" % [vid, row.get("info_mask")]})
|
||||
if not CELL_CONSTRAINTS.has(row.get("cell_constraint", "")):
|
||||
errs.append({"category": "enum", "message": "%s cell_constraint 出域: %s" % [vid, row.get("cell_constraint")]})
|
||||
# 跨字段(A4 re-review 增补,加载期致命):hide_numbers ⇒ safe_nonzero
|
||||
# 跨字段(地形变异 re-review 增补,加载期致命):hide_numbers ⇒ safe_nonzero
|
||||
if row.get("info_mask", "") == "hide_numbers" and row.get("cell_constraint", "") != "safe_nonzero":
|
||||
errs.append({"category": "cross_field", "message": "%s hide_numbers ⇒ cell_constraint 必为 safe_nonzero" % vid})
|
||||
return errs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-13(封装审查 O1/O2 修复批——自 main.gd 拆出,规范 1.1 审视线 / 单一实现纪律)
|
||||
# 调试模式编排控制器:单场遭遇(第 1/2 层)与连续两层(库存与 HP 关内延续)的模式状态机与战斗生命周期。
|
||||
# 职责边界:层序/携带 HP/共享 B4 状态 + B1 生命周期编排(创建 / begin_battle / 关卡末 level_clear)。
|
||||
# 职责边界:层序/携带 HP/共享 奖励道具经济 状态 + 战斗结算 生命周期编排(创建 / begin_battle / 关卡末 level_clear)。
|
||||
# 授权先例:与 tune_sim_controller 同款(code-review 2026-09-12 批)——控制器承接模式编排,
|
||||
# 组合根仍持有全部信号接线(battle_created 先于 begin_battle 发出,ADR-0001 接线唯一归组合根)
|
||||
# 与终局 UI 决策;玩家意图写调用(settle_batch / 窗口提交 / 刷新)不经本类。
|
||||
@@ -10,8 +10,8 @@
|
||||
# O2 收口:调试两层的"库存与 HP 延续"语义唯一实现点(此前与 main.gd 调试分支双实现漂移)。
|
||||
extends RefCounted
|
||||
|
||||
signal battle_created(b1, level: int, carry_hp: int) # begin_battle 前——组合根完成信号接线(ADR-0001)
|
||||
signal battle_ready(b1, level: int, carry_hp: int) # begin_battle 完成后——组合根 rebuild 棋盘 + 刷新呈现
|
||||
signal battle_created(settlement, level: int, carry_hp: int) # begin_battle 前——组合根完成信号接线(ADR-0001)
|
||||
signal battle_ready(settlement, level: int, carry_hp: int) # begin_battle 完成后——组合根 rebuild 棋盘 + 刷新呈现
|
||||
|
||||
var db
|
||||
var rng # /root/RngService(组合根注入)
|
||||
@@ -22,8 +22,8 @@ var _level_mode := false
|
||||
var _level_battles: Array = []
|
||||
var _level_idx := 0
|
||||
var _carry_hp := 0
|
||||
var _shared_b4 = null
|
||||
var b1 = null # 当前战斗编排器(组合根在 battle_created 接线、battle_ready 接手)
|
||||
var _shared_reward_economy = null
|
||||
var settlement = null # 当前战斗编排器(组合根在 battle_created 接线、battle_ready 接手)
|
||||
|
||||
|
||||
func _init(db_ref, rng_ref, hero_override: Callable, seed_value: Callable) -> void:
|
||||
@@ -41,18 +41,18 @@ func is_level_mode() -> bool:
|
||||
# 无库存延续、standalone 终局语义保留供探针战斗收尾)
|
||||
func start_single(level: int) -> bool:
|
||||
_level_mode = false
|
||||
_shared_b4 = null
|
||||
_shared_reward_economy = null
|
||||
return _start_battle(level, -1)
|
||||
|
||||
|
||||
# 连续两层(调试入口——共享 B4 + HP 携带;首层满血开局)
|
||||
# 连续两层(调试入口——共享 奖励道具经济 + HP 携带;首层满血开局)
|
||||
func start_debug_levels() -> bool:
|
||||
_level_mode = true
|
||||
_level_battles = [1, 2]
|
||||
_level_idx = 0
|
||||
_carry_hp = 0
|
||||
rng.new_adventure(_seed_value.call())
|
||||
_shared_b4 = RewardEconomy.new(db, rng)
|
||||
_shared_reward_economy = RewardEconomy.new(db, rng)
|
||||
return _start_battle(_level_battles[0], -1)
|
||||
|
||||
|
||||
@@ -69,13 +69,13 @@ func on_battle_finished(payload: Dictionary) -> Dictionary:
|
||||
_carry_hp = maxi(1, int(payload.get("hero_hp", 0))) # despawn 前捕获值
|
||||
_level_idx += 1
|
||||
return {"outcome": "next_layer", "layer": _level_idx, "carry_hp": _carry_hp,
|
||||
"items_left": b1.stock_count()}
|
||||
"items_left": settlement.stock_count()}
|
||||
var remaining := 0
|
||||
var is_level_end := result == "victory" and _level_mode
|
||||
if is_level_end:
|
||||
remaining = int(b1.level_clear()["remaining"]) # 库存随关卡结束清空(关内延续裁决③)
|
||||
remaining = int(settlement.level_clear()["remaining"]) # 库存随关卡结束清空(关内延续裁决③)
|
||||
elif _level_mode:
|
||||
b1.level_clear() # 失败同样清库(关卡域生命周期闭合——调试模式与 runner 模式同规)
|
||||
settlement.level_clear() # 失败同样清库(关卡域生命周期闭合——调试模式与 runner 模式同规)
|
||||
return {"outcome": "level_end" if is_level_end else "standalone_end",
|
||||
"layer": _level_idx, "carry_hp": _carry_hp, "items_left": 0, "remaining": remaining}
|
||||
|
||||
@@ -83,20 +83,20 @@ func on_battle_finished(payload: Dictionary) -> Dictionary:
|
||||
# 会话复位(组合根终局出口"返回开始界面"调用)
|
||||
func reset() -> void:
|
||||
_level_mode = false
|
||||
_shared_b4 = null
|
||||
b1 = null
|
||||
_shared_reward_economy = null
|
||||
settlement = null
|
||||
|
||||
|
||||
func _start_battle(level: int, carry_hp: int) -> bool:
|
||||
if not _level_mode or _level_idx == 0:
|
||||
rng.new_adventure(_seed_value.call())
|
||||
var ov: Dictionary = _hero_override.call() # E4:生效值经 encounter 传入——零表写
|
||||
b1 = BattleOrchestrator.new(db, rng, _shared_b4 if _level_mode else null)
|
||||
battle_created.emit(b1, level, carry_hp)
|
||||
settlement = BattleOrchestrator.new(db, rng, _shared_reward_economy if _level_mode else null)
|
||||
battle_created.emit(settlement, level, carry_hp)
|
||||
var hp: int = carry_hp if carry_hp > 0 else int(ov.max_hp)
|
||||
if not b1.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": hp,
|
||||
if not settlement.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": hp,
|
||||
"hero_atk": int(ov.atk), "hero_max_hp": int(ov.max_hp)}):
|
||||
push_error("[DebugSession] 战斗启动失败(level %d)" % level)
|
||||
return false
|
||||
battle_ready.emit(b1, level, carry_hp)
|
||||
battle_ready.emit(settlement, level, carry_hp)
|
||||
return true
|
||||
@@ -1,14 +1,14 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/battle.md(五区呈现装配器)——读核心只读面(B1 呈现快照 / runner)→
|
||||
# 实现 GDD:design/ux/battle.md(五区呈现装配器)——读核心只读面(战斗结算 呈现快照 / runner)→
|
||||
# 组装文案/投影 → 驱动各视图 setter。
|
||||
# 高内聚边界:"核心状态 → 呈现文案"的全部翻译集中于此;视图保持零核心依赖;
|
||||
# 静态无状态——呈现不持久化任何核心句柄。
|
||||
# E1 封装收口(2026-09-13):核心状态读取统一经 B1.presentation_snapshot() 成文快照面——
|
||||
# 禁再链式穿透 b1.b2.hero / b1.b2.enemies / b1.b3.query_active 一类路径(B1 协作对象已私有化,
|
||||
# E1 封装收口(2026-09-13):核心状态读取统一经 战斗结算.presentation_snapshot() 成文快照面——
|
||||
# 禁再链式穿透 settlement.enemy_system.hero / settlement.enemy_system.enemies / settlement.status_effects.query_active 一类路径(战斗结算 协作对象已私有化,
|
||||
# 敌人含死亡残位的投影由快照 enemies[] 承载,含 alive/beat/ring 字段)。
|
||||
# 更新触发注记(battle.md Data Requirements):旗/拔/移动成功反馈零事件外发(A2 发射面封闭)——
|
||||
# 更新触发注记(battle.md Data Requirements):旗/拔/移动成功反馈零事件外发(排雷玩法 发射面封闭)——
|
||||
# 经 ActionOutcome 返回值命令式驱动;AP/RAP 无推送信号——组合根每次调用返回后重查;
|
||||
# B3 纯被动容器零信号——状态栏刷新绑 B1 结算编排完成时点;库存 B4 查询面——写路径回执后重查。
|
||||
# 增益状态 纯被动容器零信号——状态栏刷新绑 战斗结算 结算编排完成时点;库存 奖励道具经济 查询面——写路径回执后重查。
|
||||
# Date: 2026-09-12(五区改造批;基础 = 2026-09-11 HUD 装配器;2026-09-13 快照面改造)
|
||||
class_name HudPresenter
|
||||
extends RefCounted
|
||||
@@ -16,11 +16,11 @@ extends RefCounted
|
||||
|
||||
# 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配)
|
||||
static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView,
|
||||
inv: InventoryPanelView, b1, runner, level_active: bool, db, refresh_remaining: int) -> void:
|
||||
if b1 == null:
|
||||
inv: InventoryPanelView, settlement, runner, level_active: bool, db, refresh_remaining: int) -> void:
|
||||
if settlement == null:
|
||||
_refresh_out_of_battle(stage, info, inv, runner, level_active, db)
|
||||
return
|
||||
var s: Dictionary = b1.presentation_snapshot()
|
||||
var s: Dictionary = settlement.presentation_snapshot()
|
||||
var hero: Dictionary = s.hero
|
||||
if hero.is_empty() or not bool(hero.get("alive", false)):
|
||||
_refresh_out_of_battle(stage, info, inv, runner, level_active, db)
|
||||
@@ -65,7 +65,7 @@ static func _hero_status_summary(effects: Array) -> String:
|
||||
|
||||
static func _refresh_economy(info: HeroInfoPanelView, ap_bar: ApBarView, s: Dictionary, db, refresh_remaining: int) -> void:
|
||||
ap_bar.set_ap(int(s.ap), int(db.action_economy.ap_per_round))
|
||||
# RAP 活值投影(C 区点阵)——清零反馈分支(A3 D1 义务)依赖本处持续投喂才会可达
|
||||
# RAP 活值投影(C 区点阵)——清零反馈分支(行动经济 D1 义务)依赖本处持续投喂才会可达
|
||||
info.set_rap(int(s.rap), int(db.action_economy.rap_refresh_cost))
|
||||
var digging := String(s.phase) == ActionEconomy.PHASE_ACTIVE
|
||||
ap_bar.set_refresh_state(refresh_remaining,
|
||||
@@ -110,7 +110,7 @@ static func _inventory_row(name_: String, category: String, power: int, status:
|
||||
return {"display_name": name_, "category": category, "desc": desc, "tooltip": tooltip, "status": status}
|
||||
|
||||
|
||||
# 窗口条目投影(B1 window_opened 载荷 → E 区窗口态行;pos = 窗口条目位置——缺陷④契约)
|
||||
# 窗口条目投影(战斗结算 window_opened 载荷 → E 区窗口态行;pos = 窗口条目位置——缺陷④契约)
|
||||
static func window_rows(items: Array, hero_hp: int, hero_max_hp: int) -> Array:
|
||||
var rows: Array = []
|
||||
for pos in items.size():
|
||||
@@ -133,7 +133,7 @@ static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelVi
|
||||
info.set_attack(runner.effective_atk(), "(关卡推进中)")
|
||||
stage.hero_card.set_data(runner.hero_hp, runner.effective_max_hp(), runner.effective_atk(), "(关卡推进中)", "")
|
||||
stage.clear_enemies() # 节点间隙清残位敌卡(死亡瞬间的灰化呈现已在结算行驱动完成)
|
||||
info.set_rap(0, int(db.action_economy.rap_refresh_cost)) # battle_over RAP 清零反馈(A3 义务)
|
||||
info.set_rap(0, int(db.action_economy.rap_refresh_cost)) # battle_over RAP 清零反馈(行动经济 义务)
|
||||
var rows: Array = []
|
||||
for inst in runner.inventory():
|
||||
if inst.status == "InStock":
|
||||
@@ -144,7 +144,7 @@ static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelVi
|
||||
inv.show_inventory(rows)
|
||||
|
||||
|
||||
# 三源拒绝可感知(A2 reject_reason/sub_reason → 玩家文案;pattern 1 双源拒绝反馈)
|
||||
# 三源拒绝可感知(排雷玩法 reject_reason/sub_reason → 玩家文案;pattern 1 双源拒绝反馈)
|
||||
static func outcome_text(outcome: Dictionary) -> String:
|
||||
if outcome.accepted:
|
||||
return "执行完成。"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证
|
||||
# 实现 GDD:design/ux/battle.md(战斗屏五区 A/B/C/D/E + 全屏 States + 遥测)。
|
||||
# Date: 2026-09-12(battle.md 五区改造批;基础 = 2026-09-11 D1-lite 组合根;code-review 修复批重构)
|
||||
# 2026-09-13(封装审查修复批):核心状态读取收口至 B1/runner 成文只读面(presentation_snapshot /
|
||||
# current_board / current_battle 等——禁链式穿透 b1.b2.hero 一类路径);调试模式编排抽离
|
||||
# Date: 2026-09-12(battle.md 五区改造批;基础 = 2026-09-11 HUD框架-lite 组合根;code-review 修复批重构)
|
||||
# 2026-09-13(封装审查修复批):核心状态读取收口至 战斗结算/runner 成文只读面(presentation_snapshot /
|
||||
# current_board / current_battle 等——禁链式穿透 settlement.enemy_system.hero 一类路径);调试模式编排抽离
|
||||
# debug_session.gd(规范 1.1 审视线 + 调试语义单一实现)。
|
||||
# 组合根(composition root):唯一的模式流转与 core↔view 接线处。
|
||||
# 职责边界:① 构建五区视图并接线信号(节点构建归 ui_assembler.gd——规范 1.1,本类只接线)
|
||||
# ② 模式管理(完整关卡 C1 runner / 调试两层——后者编排归 debug_session.gd;单场原语仅探针使用)
|
||||
# ③ 核心写调用只在此处发生(a2.settle_batch / b1.submit_window_selection / 刷新编排入口 / runner 推进;
|
||||
# ② 模式管理(完整关卡 关卡节点地图 runner / 调试两层——后者编排归 debug_session.gd;单场原语仅探针使用)
|
||||
# ③ 核心写调用只在此处发生(minesweeper.settle_batch / settlement.submit_window_selection / 刷新编排入口 / runner 推进;
|
||||
# E4 收口:加成值零表写——面板百分比经 runner.set_hero_override / debug_session 生效值 Callable
|
||||
# 以 encounter 参数入战斗,db.hero 运行期只读)
|
||||
# ④ 全屏 States 状态机(挖掘/结算/道具窗口/终局)与一次性提示去重。
|
||||
# 渲染与文案归 views/,核心状态→文案装配归 hud_presenter.gd 与 battle_narrator.gd,节点构建归
|
||||
# ui_assembler.gd,模态层级归 modal_stack.gd,批量模拟编排归 tune_sim_controller.gd,调试模式编排归
|
||||
# debug_session.gd——本类不拼呈现字符串。
|
||||
# ADR-0001 纪律:呈现层永不直接订阅 A2/A3——只订 B1/runner 转发信号;相位读取仅用于驱动视图状态。
|
||||
# ADR-0001 纪律:呈现层永不直接订阅 排雷玩法/行动经济——只订 战斗结算/runner 转发信号;相位读取仅用于驱动视图状态。
|
||||
extends Control
|
||||
|
||||
const UiAssembler := preload("res://presentation/ui_assembler.gd")
|
||||
@@ -25,8 +25,8 @@ const TuneSimController := preload("res://presentation/tune_sim_controller.gd")
|
||||
const DebugSessionScript := preload("res://presentation/debug_session.gd")
|
||||
|
||||
var db
|
||||
var b1 = null # 当前战斗编排器(关卡与调试模式统一读它)
|
||||
var runner = null # C1 线性关卡推进器(完整关卡模式)
|
||||
var settlement = null # 当前战斗编排器(关卡与调试模式统一读它)
|
||||
var runner = null # 关卡节点地图 线性关卡推进器(完整关卡模式)
|
||||
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
|
||||
var _level_active := false
|
||||
var _seed_edit: LineEdit
|
||||
@@ -102,7 +102,7 @@ func _build_ui() -> void:
|
||||
inventory_panel.skipped.connect(_on_window_skipped)
|
||||
inventory_panel.reject_reason.connect(_on_inventory_reject)
|
||||
|
||||
# 奖励徽章数据源:B4 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效)
|
||||
# 奖励徽章数据源:奖励道具经济 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效)
|
||||
board_view.set_identity_resolver(_identity_lookup)
|
||||
|
||||
logger = PlaytestLogger.new()
|
||||
@@ -174,10 +174,10 @@ func _input_locked() -> bool:
|
||||
# 模态/非战斗期棋盘输入锁(各 overlay 自身可见即锁定)
|
||||
return legend_overlay.is_open() or review_overlay.is_open() or _end_overlay.is_open() \
|
||||
or node_overlay.visible or tune_panel.visible or _start_overlay.visible \
|
||||
or _inter_overlay.visible or b1 == null
|
||||
or _inter_overlay.visible or settlement == null
|
||||
|
||||
|
||||
# ==================== 完整关卡模式(C1 线性推进器)====================
|
||||
# ==================== 完整关卡模式(关卡节点地图 线性推进器)====================
|
||||
|
||||
func _start_full_level() -> void:
|
||||
_start_overlay.visible = false
|
||||
@@ -194,7 +194,7 @@ func _start_full_level() -> void:
|
||||
_reset_run_scope()
|
||||
log_view.clear()
|
||||
var seed_val := _seed_value()
|
||||
logger.start_session(seed_val) # 各战斗的 attach 在 _on_runner_node 内(b1 逐战创建)
|
||||
logger.start_session(seed_val) # 各战斗的 attach 在 _on_runner_node 内(settlement 逐战创建)
|
||||
if not runner.start_level(1, seed_val):
|
||||
_fatal_screen("关卡启动失败——见输出日志")
|
||||
return
|
||||
@@ -214,8 +214,8 @@ func _reset_run_scope() -> void:
|
||||
func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
|
||||
var t := String(node.get("type", ""))
|
||||
if t == "battle" or t == "boss":
|
||||
b1 = runner.current_battle() # 呈现层统一读 b1(与调试模式共用接线)
|
||||
logger.attach_battle(b1)
|
||||
settlement = runner.current_battle() # 呈现层统一读 settlement(与调试模式共用接线)
|
||||
logger.attach_battle(settlement)
|
||||
_boss_active = t == "boss"
|
||||
_show_board_fx_for_node(t)
|
||||
log_view.append_bb(Narrator.node_banner(runner, node, t))
|
||||
@@ -237,12 +237,12 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
|
||||
func _on_level_finished(payload: Dictionary) -> void:
|
||||
_level_active = false
|
||||
var result: String = String(payload.get("result", ""))
|
||||
b1 = null
|
||||
settlement = null
|
||||
_sync_settlement_visuals() # → 全关
|
||||
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(B3×4④)
|
||||
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④)
|
||||
stage.gray_all_enemies() # 死亡兜底:卡体灰化残位(禁瞬消语义的战斗收尾面)
|
||||
if result == "victory":
|
||||
# 关卡结算:库存清空可见反馈(B4 D1×⑤)+ 胜利终局 overlay(金蜡封落于棋盘中央)
|
||||
# 关卡结算:库存清空可见反馈(奖励道具经济 D1×⑤)+ 胜利终局 overlay(金蜡封落于棋盘中央)
|
||||
var remaining: int = int(payload.get("remaining_items", 0))
|
||||
inventory_panel.play_cleared_feedback(remaining)
|
||||
_end_overlay.open_victory(Narrator.level_end_text("victory", payload, runner.effective_max_hp()),
|
||||
@@ -265,7 +265,7 @@ func _on_battle_end_continue() -> void:
|
||||
if runner != null:
|
||||
runner.restore_hero_base()
|
||||
runner = null
|
||||
b1 = null
|
||||
settlement = null
|
||||
tune_panel.reset_sliders()
|
||||
strip_view.refresh(null)
|
||||
_start_overlay.visible = true
|
||||
@@ -289,16 +289,16 @@ func _start_debug_levels() -> void:
|
||||
_fatal_screen("战斗启动失败——见输出日志")
|
||||
|
||||
|
||||
func _on_debug_battle_created(b1_ref, _level: int, _carry: int) -> void:
|
||||
# begin_battle 前接手 + 接线(ADR-0001:signal.connect 唯一归组合根)。b1 必须先于 begin_battle
|
||||
func _on_debug_battle_created(settlement_ref, _level: int, _carry: int) -> void:
|
||||
# begin_battle 前接手 + 接线(ADR-0001:signal.connect 唯一归组合根)。settlement 必须先于 begin_battle
|
||||
# 就位——begin 期间发出的 board_changed 要用新板重建棋盘并触发雾首遇提示(缺陷①同款时序义务)
|
||||
b1 = b1_ref
|
||||
_wire_battle(b1_ref)
|
||||
settlement = settlement_ref
|
||||
_wire_battle(settlement_ref)
|
||||
|
||||
|
||||
func _on_debug_battle_ready(b1_ref, _level: int, _carry: int) -> void:
|
||||
b1 = b1_ref
|
||||
board_view.rebuild(b1.a2.current_board(), b1.a2)
|
||||
func _on_debug_battle_ready(settlement_ref, _level: int, _carry: int) -> void:
|
||||
settlement = settlement_ref
|
||||
board_view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
|
||||
_check_board_hints()
|
||||
_refresh_presentations()
|
||||
|
||||
@@ -320,11 +320,11 @@ func _scaled(base: int, pct: int) -> int:
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0)))
|
||||
|
||||
|
||||
# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——B4 Ready 域外零读、零诊断噪音)
|
||||
# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——奖励道具经济 Ready 域外零读、零诊断噪音)
|
||||
func _identity_lookup(c: Vector2i):
|
||||
if b1 == null or b1.a2.current_board() == null or b1.a3.phase != b1.a3.PHASE_ACTIVE:
|
||||
if settlement == null or settlement.minesweeper.current_board() == null or settlement.action_economy.phase != settlement.action_economy.PHASE_ACTIVE:
|
||||
return null
|
||||
return b1.query_reward_identity(c)
|
||||
return settlement.query_reward_identity(c)
|
||||
|
||||
|
||||
func _show_board_fx_for_node(node_type: String = "") -> void:
|
||||
@@ -334,12 +334,12 @@ func _show_board_fx_for_node(node_type: String = "") -> void:
|
||||
|
||||
|
||||
func _on_battle_finished(payload: Dictionary) -> void:
|
||||
# 仅调试模式接线(完整关卡的结束归 C1 runner._on_level_finished)
|
||||
# 仅调试模式接线(完整关卡的结束归 关卡节点地图 runner._on_level_finished)
|
||||
var result: String = String(payload.get("result", ""))
|
||||
_sync_settlement_visuals() # → 全关(battle_over 反馈面)
|
||||
stage.gray_all_enemies() # 死亡兜底:卡体灰化残位(禁瞬消语义的战斗收尾面)
|
||||
info_panel.set_rap(0, int(db.action_economy.rap_refresh_cost)) # RAP 清零滚动 + 微文案
|
||||
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(B3×4④)
|
||||
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④)
|
||||
var decision: Dictionary = _debug.on_battle_finished(payload)
|
||||
match String(decision.outcome):
|
||||
"next_layer":
|
||||
@@ -365,18 +365,18 @@ func _on_battle_finished(payload: Dictionary) -> void:
|
||||
|
||||
# ==================== core↔view 接线(战斗信号统一走这里)====================
|
||||
|
||||
func _wire_battle(b1_ref) -> void:
|
||||
b1_ref.step_row.connect(_on_step_row)
|
||||
b1_ref.window_opened.connect(_on_window_opened)
|
||||
b1_ref.battle_finished.connect(_on_battle_finished)
|
||||
b1_ref.round_started.connect(_on_round_started)
|
||||
b1_ref.board_changed.connect(_on_board_changed)
|
||||
func _wire_battle(settlement_ref) -> void:
|
||||
settlement_ref.step_row.connect(_on_step_row)
|
||||
settlement_ref.window_opened.connect(_on_window_opened)
|
||||
settlement_ref.battle_finished.connect(_on_battle_finished)
|
||||
settlement_ref.round_started.connect(_on_round_started)
|
||||
settlement_ref.board_changed.connect(_on_board_changed)
|
||||
|
||||
|
||||
func _on_step_row(row: Dictionary) -> void:
|
||||
log_view.append_row(row)
|
||||
stage.on_step_row_fx(row)
|
||||
# 高光演出(B1×3①② 占位):完美排雷横幅 / 标中旗加重弹道
|
||||
# 高光演出(战斗结算×3①② 占位):完美排雷横幅 / 标中旗加重弹道
|
||||
if String(row.get("type", "")) == "highlight":
|
||||
board_zone.fx.play_perfect_sweep()
|
||||
elif String(row.get("highlight", "")) == "flag_hit":
|
||||
@@ -392,12 +392,12 @@ func _on_round_started(ri: int) -> void:
|
||||
|
||||
|
||||
func _on_board_changed() -> void:
|
||||
# 关卡模式下 board_changed 可能先于主场景接手 b1 到达(begin_battle 期间发)——先从 runner 同步
|
||||
if b1 == null and _level_active and runner != null:
|
||||
b1 = runner.current_battle()
|
||||
if b1 == null:
|
||||
# 关卡模式下 board_changed 可能先于主场景接手 settlement 到达(begin_battle 期间发)——先从 runner 同步
|
||||
if settlement == null and _level_active and runner != null:
|
||||
settlement = runner.current_battle()
|
||||
if settlement == null:
|
||||
return
|
||||
board_view.rebuild(b1.a2.current_board(), b1.a2)
|
||||
board_view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
|
||||
_check_board_hints()
|
||||
_refresh_presentations()
|
||||
|
||||
@@ -405,13 +405,16 @@ func _on_board_changed() -> void:
|
||||
# ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ----
|
||||
|
||||
func _check_board_hints() -> void:
|
||||
var board = b1.a2.current_board()
|
||||
var board = settlement.minesweeper.current_board()
|
||||
if board == null:
|
||||
return
|
||||
# 零合法板(A1 EC5 降级)诚实提示——非静默(toast,非模态)
|
||||
# 零合法板(棋盘生成 EC5 降级)诚实提示——非静默(toast,非模态)
|
||||
if board.illegal_product:
|
||||
board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn")
|
||||
# 雾首遇(每 run 一次;必含三要点:无雷盲区不赌命 / 雾格不传播连锁 / 雾内插旗纯损——A4 D1×②)
|
||||
# 接受最优降级(有合法板但无一达标)——降级标记独立于非法产物标记,同走诚实告知
|
||||
elif board.degraded_product:
|
||||
board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn")
|
||||
# 雾首遇(每 run 一次;必含三要点:无雷盲区不赌命 / 雾格不传播连锁 / 雾内插旗纯损——地形变异 D1×②)
|
||||
if not board.fog_cells().is_empty() and not _hints_shown.has("fog_first"):
|
||||
_hints_shown["fog_first"] = true
|
||||
_show_hint("迷雾=无雷盲区,不赌命开雾;雾格不传播连锁;雾内插旗纯损。", "fog_first")
|
||||
@@ -442,7 +445,7 @@ func _show_hint(text: String, hint_id: String) -> void:
|
||||
|
||||
func _on_window_opened(items: Array) -> void:
|
||||
_window_payload = items
|
||||
var hero: Dictionary = b1.presentation_snapshot().hero # E1 收口:经呈现快照面读 HP
|
||||
var hero: Dictionary = settlement.presentation_snapshot().hero # E1 收口:经呈现快照面读 HP
|
||||
var hp: int = int(hero.get("hp", 0)) if not hero.is_empty() else 0
|
||||
var max_hp: int = int(hero.get("max_hp", 0)) if not hero.is_empty() else 1
|
||||
inventory_panel.open_window(HudPresenter.window_rows(items, hp, max_hp))
|
||||
@@ -456,16 +459,16 @@ func _on_window_item_selected(pos: int) -> void:
|
||||
logger.log_ux("ux_window_choice", {"selected_item_id": String(it.get("item_id", "")),
|
||||
"usable_now": true, "skip": false})
|
||||
inventory_panel.close_window()
|
||||
if b1 != null:
|
||||
b1.submit_window_selection([pos])
|
||||
if settlement != null:
|
||||
settlement.submit_window_selection([pos])
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_window_skipped() -> void:
|
||||
logger.log_ux("ux_window_choice", {"selected_item_id": "", "usable_now": false, "skip": true})
|
||||
inventory_panel.close_window()
|
||||
if b1 != null:
|
||||
b1.submit_window_selection([])
|
||||
if settlement != null:
|
||||
settlement.submit_window_selection([])
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
@@ -485,13 +488,13 @@ func _on_board_action(cell: Vector2i, kind: String) -> void:
|
||||
# 玩家动作 → 核心写调用的唯一通道(模态/窗口期/无战斗拒绝在此收口)
|
||||
if _input_locked() or inventory_panel.is_window_open():
|
||||
return
|
||||
var outcomes: Array = b1.a2.settle_batch([{"action": kind, "cell": cell}])
|
||||
var outcomes: Array = settlement.minesweeper.settle_batch([{"action": kind, "cell": cell}])
|
||||
var accepted: bool = not outcomes.is_empty() and bool(outcomes[0].accepted)
|
||||
logger.log_action(kind, cell, accepted)
|
||||
_handle_outcome_feedback(outcomes[0] if not outcomes.is_empty() else {})
|
||||
# settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 b1 已被
|
||||
# runner 置空,清图演出判定须先判 b1(基线既有重入隐患,2026-09-13 加固)
|
||||
if accepted and kind == "dig" and b1 != null and b1.a2.current_board() != null and b1.a2.clear_map_cleared():
|
||||
# settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被
|
||||
# runner 置空,清图演出判定须先判 settlement(基线既有重入隐患,2026-09-13 加固)
|
||||
if accepted and kind == "dig" and settlement != null and settlement.minesweeper.current_board() != null and settlement.minesweeper.clear_map_cleared():
|
||||
board_zone.fx.play_clear_burst()
|
||||
board_view.refresh()
|
||||
_refresh_presentations()
|
||||
@@ -500,8 +503,8 @@ func _on_board_action(cell: Vector2i, kind: String) -> void:
|
||||
func _on_stop_pressed() -> void:
|
||||
if _input_locked() or inventory_panel.is_window_open():
|
||||
return
|
||||
# stop 经批次提交(A2 D1×6①——不直调 A3)
|
||||
var outcomes: Array = b1.a2.settle_batch([{"action": "stop"}])
|
||||
# stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济)
|
||||
var outcomes: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
logger.log_action("stop", Vector2i(-1, -1), not outcomes.is_empty() and bool(outcomes[0].accepted))
|
||||
if not outcomes.is_empty() and String(outcomes[0].get("outcome", "")) == "voided":
|
||||
# voided ≠ 拒绝——独立文案(pattern 1:防静默改判读感欺骗)
|
||||
@@ -518,16 +521,16 @@ func _on_refresh_confirmed() -> void:
|
||||
var ok: bool = false
|
||||
if _level_active and runner != null:
|
||||
ok = runner.player_request_refresh()
|
||||
elif b1 != null:
|
||||
ok = b1.player_request_refresh()
|
||||
elif settlement != null:
|
||||
ok = settlement.player_request_refresh()
|
||||
logger.log_action("refresh", Vector2i(-1, -1), ok)
|
||||
if ok:
|
||||
_refresh_used_round += 1
|
||||
logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_round})
|
||||
board_zone.toast.show_toast("RAP−8 · 旧板旗作废——新图已就位,AP 保留继续挖", "gold")
|
||||
else:
|
||||
# 资源面/上限拒绝(EC13 合法拒绝可见;A3 无载荷——客户端按上下文推断)
|
||||
var rap: int = int(b1.a3.rap) if b1 != null else 0
|
||||
# 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断)
|
||||
var rap: int = int(settlement.action_economy.rap) if settlement != null else 0
|
||||
var cost: int = int(db.action_economy.rap_refresh_cost)
|
||||
var result := "rejected_rap" if rap < cost else "rejected_cap"
|
||||
logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_round + 1})
|
||||
@@ -556,7 +559,7 @@ func _handle_outcome_feedback(outcome: Dictionary) -> void:
|
||||
func _refresh_presentations() -> void:
|
||||
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
|
||||
var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_max_per_round) - _refresh_used_round)
|
||||
HudPresenter.refresh(stage, info_panel, ap_bar, inventory_panel, b1, runner, _level_active, db, refresh_remaining)
|
||||
HudPresenter.refresh(stage, info_panel, ap_bar, inventory_panel, settlement, runner, _level_active, db, refresh_remaining)
|
||||
_sync_settlement_visuals()
|
||||
|
||||
|
||||
@@ -571,17 +574,17 @@ func _sync_settlement_visuals() -> void:
|
||||
|
||||
|
||||
func _is_settling() -> bool:
|
||||
if b1 == null or not b1.is_battle_live():
|
||||
if settlement == null or not settlement.is_battle_live():
|
||||
return false
|
||||
return b1.a3.phase == b1.a3.PHASE_WAITING
|
||||
return settlement.action_economy.phase == settlement.action_economy.PHASE_WAITING
|
||||
|
||||
|
||||
func _phase_text() -> String:
|
||||
# 文案装配归 narrator(b1 缺位 = 非战斗态——组合根模式状态守卫)
|
||||
if b1 == null:
|
||||
# 文案装配归 narrator(settlement 缺位 = 非战斗态——组合根模式状态守卫)
|
||||
if settlement == null:
|
||||
return "第 — 轮 · —"
|
||||
return Narrator.phase_text(int(b1.round_index), String(b1.battle_result),
|
||||
inventory_panel.is_window_open(), b1.a3.phase == b1.a3.PHASE_WAITING,
|
||||
return Narrator.phase_text(int(settlement.round_index), String(settlement.battle_result),
|
||||
inventory_panel.is_window_open(), settlement.action_economy.phase == settlement.action_economy.PHASE_WAITING,
|
||||
_boss_active, Narrator.boss_name(runner, db))
|
||||
|
||||
|
||||
@@ -604,9 +607,9 @@ func _on_diagnose_pressed() -> void:
|
||||
var ctx := {"mode": "level" if _level_active else "debug", "seed": _seed_value()}
|
||||
if _level_active and runner != null:
|
||||
ctx["runner"] = {"node_index": runner.node_index, "level_result": String(runner.level_result),
|
||||
"hero_hp": runner.hero_hp, "b1_is_null": runner.current_battle() == null}
|
||||
if b1 != null:
|
||||
ctx["b1"] = b1.diagnostic_snapshot() # 诊断观测面(私有字段零直读——code-review 2026-09-12)
|
||||
"hero_hp": runner.hero_hp, "settlement_is_null": runner.current_battle() == null}
|
||||
if settlement != null:
|
||||
ctx["settlement"] = settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读——code-review 2026-09-12)
|
||||
var path: String = logger.dump_diagnosis(ctx)
|
||||
board_zone.toast.show_toast("已转储诊断: " + path, "info")
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
# 实现 GDD:design/ux/battle.md(§Events Fired 遥测义务——ux 行族)。
|
||||
# Date: 2026-09-12(五区改造批增 ux 遥测;基础 = 2026-09-12 日志器首版)
|
||||
# 试玩日志器(呈现层组件——订阅核心信号持久化 JSONL,供数值分析;卡死诊断转储)。
|
||||
# 职责边界:核心层只发 settlement_snapshot 信号(B1)/既有信号(battle_finished/node_entered/
|
||||
# 职责边界:核心层只发 settlement_snapshot 信号(战斗结算)/既有信号(battle_finished/node_entered/
|
||||
# level_finished/step_row);本组件负责订阅、缓冲与写文件——核心零日志感知(零 UI 依赖不变)。
|
||||
# 文件 = user://logs/playtest_<seed>_<时间戳>.jsonl(每行一次结算/终局/节点的全量状态 JSON)。
|
||||
# ux 行族(呈现层观测,不改 core):ux_round_input_ratio(原始输入动作数∶AP 消耗)、
|
||||
# ux_round_exit{path_tag}(快照 round_facts.path——呈现层只订 B1 的 ADR 纪律下唯一合法来源)、
|
||||
# ux_round_exit{path_tag}(快照 round_facts.path——呈现层只订 战斗结算 的 ADR 纪律下唯一合法来源)、
|
||||
# ux_refresh_attempt / ux_window_choice / ux_reject / ux_hint_shown(组合根显式上报)。
|
||||
# 诊断转储 = 全量核心状态 + 最近结算/操作环形缓冲 → user://logs/diag_<时间戳>.json(卡死时点按钮)。
|
||||
class_name PlaytestLogger
|
||||
@@ -20,7 +20,7 @@ var _seed := 0
|
||||
var _recent_steps: Array = [] # 环形缓冲:最近结算行
|
||||
var _recent_actions: Array = [] # 环形缓冲:最近玩家动作
|
||||
var _snap_count := 0
|
||||
var ap_per_round := 8 # 组合根注入(AP 消耗口径分母——A3 无推送信号)
|
||||
var ap_per_round := 8 # 组合根注入(AP 消耗口径分母——行动经济 无推送信号)
|
||||
var _ux_inputs := 0 # 本结算轮原始输入动作数(log_action 计数,快照后归零)
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ func log_path() -> String:
|
||||
return _path
|
||||
|
||||
|
||||
func attach_battle(b1) -> void:
|
||||
b1.step_row.connect(_on_step_row)
|
||||
b1.settlement_snapshot.connect(_on_settlement_snapshot)
|
||||
b1.battle_finished.connect(func(p): _append({
|
||||
func attach_battle(settlement) -> void:
|
||||
settlement.step_row.connect(_on_step_row)
|
||||
settlement.settlement_snapshot.connect(_on_settlement_snapshot)
|
||||
settlement.battle_finished.connect(func(p): _append({
|
||||
"kind": "battle_end", "result": String(p.get("result", "")),
|
||||
"hero_hp": int(p.get("hero_hp", 0)), "remaining_items": int(p.get("remaining_items", 0))}))
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# 〔pattern 7 破坏性确认异位:RAP−8 · 旧板旗作废显式代价〕)。
|
||||
# Date: 2026-09-12
|
||||
# 高内聚边界:纯视图——意图经信号外发(stop_pressed/refresh_confirmed/legend_toggled),
|
||||
# 状态由组合根经 setter 注入;刷新可用性预判 = RAP≥8 读数(A3 无推送信号——每次调用返回后重查)。
|
||||
# 状态由组合根经 setter 注入;刷新可用性预判 = RAP≥8 读数(行动经济 无推送信号——每次调用返回后重查)。
|
||||
class_name ApBarView
|
||||
extends PanelContainer
|
||||
|
||||
@@ -92,7 +92,7 @@ func _init() -> void:
|
||||
refresh_box.add_child(_refresh_sub)
|
||||
|
||||
|
||||
# ---- setter 面(组合根每次核心调用返回后重查注入——A3 无推送信号) ----
|
||||
# ---- setter 面(组合根每次核心调用返回后重查注入——行动经济 无推送信号) ----
|
||||
|
||||
func set_ap(ap: int, ap_max: int) -> void:
|
||||
while _dots.get_child_count() < ap_max:
|
||||
|
||||
@@ -58,7 +58,7 @@ func _init() -> void:
|
||||
visible = false
|
||||
|
||||
|
||||
# 终局画面数据锚 = B1 判定行(verdict)——判定行文案由组合根装配传入
|
||||
# 终局画面数据锚 = 战斗结算 判定行(verdict)——判定行文案由组合根装配传入
|
||||
func open_victory(verdict_line: String, continue_label: String, board_rect: Rect2) -> void:
|
||||
_dim.color = Color(COL_WARM, 0.28)
|
||||
_verdict.text = verdict_line
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
# 实时流 / 点击展开回看 overlay〔pattern 6 模态纪律〕;行模板「{行为} {值} → {目标}(来源)」、
|
||||
# 聚合计数行、零值行可见、效果行 value≠pool_value 禁相加、未生效四类行 icon/划线形状冗余、
|
||||
# highlight 金框〔flag_hit/clear_reward_h2/perfect_broken〕、击杀标记)。
|
||||
# Date: 2026-09-12(基础 = 2026-09-11 13 类行协议格式化版)
|
||||
# 高内聚边界:B1 步骤流 → 玩家文案的全部格式化规则内聚于此;来源文案映射来自 SettlementTable。
|
||||
# Date: 2026-09-12(基础 = 2026-09-11 13 类行协议格式化版;2026-09-15 窗口锚行批 13→15 类:
|
||||
# 新增 window_open / window_skip 两类锚行格式化——战斗结算 行协议清单同步)
|
||||
# 高内聚边界:战斗结算 步骤流 → 玩家文案的全部格式化规则内聚于此;来源文案映射来自 SettlementTable。
|
||||
class_name BattleLogView
|
||||
extends PanelContainer
|
||||
|
||||
@@ -162,7 +163,7 @@ func _format_row(row: Dictionary, agg_count := -1) -> String:
|
||||
var suffix := "(%s)" % src if src != "" else ""
|
||||
match String(row.get("type", "")):
|
||||
"effect":
|
||||
# 效果行 value = 单次贡献值,pool_value = 池合成值——禁相加呈现(B1 呈现细则)
|
||||
# 效果行 value = 单次贡献值,pool_value = 池合成值——禁相加呈现(战斗结算 呈现细则)
|
||||
var pool: int = int(row.get("pool_value", 0))
|
||||
var col := "#7cc47c" if v > 0 else "#c47c7c"
|
||||
var body := "%s 单次%+d(池 %+d)%s" % [name_, v, pool, suffix]
|
||||
@@ -199,6 +200,10 @@ func _format_row(row: Dictionary, agg_count := -1) -> String:
|
||||
return "[color=#e0b860][b]◆ %s[/b][/color]" % name_
|
||||
"highlight":
|
||||
return prefix + _gold_frame("★ %s" % name_)
|
||||
"window_open":
|
||||
return prefix + "[color=#c8b070]▤ %s[/color]" % name_
|
||||
"window_skip":
|
||||
return prefix + "[color=#9a8a72]▤ %s[/color]" % name_
|
||||
return prefix + name_ + suffix
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/battle.md(D 区演出层——清图金粒喷泉+彩纸 3–5s 占位、Boss 紫赤影子爬棋盘
|
||||
# 一次性占位、完美排雷金光带+横幅占位、标中旗加重弹道占位;全部程序化色板原语,真实美术归 B5)。
|
||||
# 一次性占位、完美排雷金光带+横幅占位、标中旗加重弹道占位;全部程序化色板原语,真实美术归 战斗表现)。
|
||||
# Date: 2026-09-12
|
||||
# 高内聚边界:纯演出——组合根按核心信号调用 play_*;本类零核心依赖、不自持游戏状态。
|
||||
class_name BoardFxView
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/battle.md(战斗屏 D 区 BoardView——四格态/悬停预承诺光标/可挖域呼吸微光/
|
||||
# BFS 路径预览/奖励徽章/雾区全开一次性完成信号/结算期去饱和锁输入)。
|
||||
# Date: 2026-09-12(battle.md 五区改造批;基础 = 2026-09-11 D1-lite 版)
|
||||
# Date: 2026-09-12(battle.md 五区改造批;基础 = 2026-09-11 HUD框架-lite 版)
|
||||
# 棋盘视图:格子渲染(art-bible 明亮风、数字高对比、雾形状+色相双通道、
|
||||
# 绘制优先级 数字>小人>旗>奖励徽章>变异纹带>底色)+ 玩家输入意图翻译 + 悬停预承诺三态。
|
||||
# 高内聚边界:只读 A2 查询面(+B4 身份解析回调)做渲染与意图识别,动作经信号外发——不调用核心写接口
|
||||
# (settle_batch 归组合根编排),不持久持有战斗态(a2 引用随 refresh 更新,仅作只读查询)。
|
||||
# 高内聚边界:只读 排雷玩法 查询面(+奖励道具经济 身份解析回调)做渲染与意图识别,动作经信号外发——不调用核心写接口
|
||||
# (settle_batch 归组合根编排),不持久持有战斗态(minesweeper 引用随 refresh 更新,仅作只读查询)。
|
||||
# 缺陷①回归约束:格子控件常驻、逐格重绘在 refresh 内完成,无布局回流(格控件/子标记定长,不增删)。
|
||||
# ADR-0003 形态约束:GridContainer + 逐格常驻控件(探针耦合 _cells/click_cell——禁盲目迁移 _draw)。
|
||||
class_name BoardView
|
||||
@@ -30,6 +30,7 @@ const COL_HL_TARGET := Color8(240, 168, 40) # 悬停目标格边框(金)
|
||||
const REWARD_BADGE := { # 已开格奖励身份 icon(tier 档位另经字号/形状区分)
|
||||
"instant_aoe": "爆", "instant_heal": "急", "damage_item": "刃", "heal_pack": "医", "buff": "纹",
|
||||
}
|
||||
const BADGE_PLACEHOLDER := "?" # 未知图标键占位纹样(HUD框架:禁静默空图标)
|
||||
const CURSOR_DIG := Input.CURSOR_CROSS # 挖——镐形自绘纹理载体
|
||||
const CURSOR_FLAG := Input.CURSOR_POINTING_HAND # 旗——旗形自绘纹理载体
|
||||
const CURSOR_MOVE_SEM := Input.CURSOR_ARROW # 移动——箭头(命名避开 Control.CURSOR_MOVE 原生成员遮蔽)
|
||||
@@ -40,8 +41,8 @@ var _cells := {} # Vector2i → Button
|
||||
var _cell_sb := {} # Vector2i → StyleBoxFlat(呼吸微光就地改边框色)
|
||||
var _marker := {} # Vector2i → {"dot": ColorRect, "badge": Label, "hl": Panel}
|
||||
var _board: BoardData = null
|
||||
var _a2 = null # A2 只读查询引用(refresh 更新;null = 无战斗)
|
||||
var _identity_cb := Callable() # B4 身份解析回调(组合根注入只读面)——Vector2i → Dictionary/null
|
||||
var _minesweeper = null # 排雷玩法 只读查询引用(refresh 更新;null = 无战斗)
|
||||
var _identity_cb := Callable() # 奖励道具经济 身份解析回调(组合根注入只读面)——Vector2i → Dictionary/null
|
||||
var _diggable := {} # 可挖域(8 向相邻未开格)——呼吸微光作用集
|
||||
var _breath_t := 0.0
|
||||
var _dig_glow := false # 挖掘期才允许唯一在动元素(art-bible §2.1)
|
||||
@@ -49,7 +50,7 @@ var _hover := Vector2i(-9, -9)
|
||||
var _last_click := {"cell": Vector2i(-9, -9), "ms": -100000}
|
||||
var _fog_done := {} # 本板已完成雾区连通分量(键 = 分量最小格)
|
||||
var _fog_parts: Array = [] # 本板雾区连通分量(rebuild 期计算——合并呈现域)
|
||||
var _r0 := {} # 出生展开区 R₀(B4 mapping 资格域 = S∖R₀——徽章查询跳过,防 E19 诊断噪音)
|
||||
var _r0 := {} # 出生展开区 R₀(奖励道具经济 mapping 资格域 = S∖R₀——徽章查询跳过,防 E19 诊断噪音)
|
||||
var _desat_mat: ShaderMaterial
|
||||
var _desat_tween: Tween
|
||||
var _cell := 46.0 # 当前格边长(set_cell_area 按容器尺寸重算——battle.md 格尺寸公式)
|
||||
@@ -91,7 +92,7 @@ func set_desaturated(on: bool) -> void:
|
||||
(_cells[c] as Button).material = _desat_mat if on else null
|
||||
|
||||
|
||||
# 组合根注入 B4 身份解析只读回调(query_identity)——奖励徽章数据源
|
||||
# 组合根注入 奖励道具经济 身份解析只读回调(query_identity)——奖励徽章数据源
|
||||
func set_identity_resolver(cb: Callable) -> void:
|
||||
_identity_cb = cb
|
||||
|
||||
@@ -107,10 +108,10 @@ func set_cell_area(avail: Vector2) -> void:
|
||||
(_cells[c] as Button).custom_minimum_size = Vector2(_cell, _cell)
|
||||
|
||||
|
||||
# 战斗启动/换板:按板尺寸重建格子控件(a2 引用随之更新)
|
||||
func rebuild(board: BoardData, a2) -> void:
|
||||
# 战斗启动/换板:按板尺寸重建格子控件(minesweeper 引用随之更新)
|
||||
func rebuild(board: BoardData, minesweeper) -> void:
|
||||
_board = board
|
||||
_a2 = a2
|
||||
_minesweeper = minesweeper
|
||||
for child in _grid.get_children():
|
||||
child.queue_free()
|
||||
_cells.clear()
|
||||
@@ -144,12 +145,12 @@ func rebuild(board: BoardData, a2) -> void:
|
||||
|
||||
# 逐格重绘(挖掘/结算后调用)
|
||||
func refresh() -> void:
|
||||
if _board == null or _a2 == null:
|
||||
if _board == null or _minesweeper == null:
|
||||
return
|
||||
var hero: Vector2i = _a2.hero_pos()
|
||||
var hero: Vector2i = _minesweeper.hero_pos()
|
||||
_diggable.clear()
|
||||
for n in _board.neighbors8(hero):
|
||||
if not _a2.is_revealed(n):
|
||||
if not _minesweeper.is_revealed(n):
|
||||
_diggable[n] = true
|
||||
for y in _board.height:
|
||||
for x in _board.width:
|
||||
@@ -167,16 +168,16 @@ func click_cell(cell: Vector2i, button: int) -> void:
|
||||
|
||||
|
||||
func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
|
||||
if _a2 == null or not (ev is InputEventMouseButton and ev.pressed):
|
||||
if _minesweeper == null or not (ev is InputEventMouseButton and ev.pressed):
|
||||
return
|
||||
if ev.button_index == MOUSE_BUTTON_LEFT:
|
||||
if _a2.is_revealed(c):
|
||||
if _minesweeper.is_revealed(c):
|
||||
action_requested.emit(c, "move")
|
||||
else:
|
||||
action_requested.emit(c, "dig")
|
||||
_detect_chord(c)
|
||||
elif ev.button_index == MOUSE_BUTTON_RIGHT:
|
||||
if _a2.has_flag(c):
|
||||
if _minesweeper.has_flag(c):
|
||||
action_requested.emit(c, "flag_pull")
|
||||
else:
|
||||
action_requested.emit(c, "flag_place")
|
||||
@@ -212,11 +213,11 @@ func _refresh_hover_after_paint() -> void:
|
||||
|
||||
|
||||
func _apply_hover_visuals(c: Vector2i) -> void:
|
||||
if _a2 == null or not _cells.has(c):
|
||||
if _minesweeper == null or not _cells.has(c):
|
||||
return
|
||||
_clear_hover_visuals(c)
|
||||
var revealed: bool = _a2.is_revealed(c)
|
||||
var flag: bool = _a2.has_flag(c)
|
||||
var revealed: bool = _minesweeper.is_revealed(c)
|
||||
var flag: bool = _minesweeper.has_flag(c)
|
||||
var adjacent: bool = _diggable.has(c)
|
||||
var hl: Panel = _marker[c]["hl"]
|
||||
if revealed:
|
||||
@@ -253,9 +254,9 @@ func _set_cursor_shape(c: Vector2i, shape: int) -> void:
|
||||
|
||||
func _show_path_preview(c: Vector2i) -> void:
|
||||
# D3 合法性指示:悬停已开格 → BFS 路径蓝细线点迹(compute_move_path 只读面)
|
||||
if _a2 == null:
|
||||
if _minesweeper == null:
|
||||
return
|
||||
var path: Array = _a2.compute_move_path(c)
|
||||
var path: Array = _minesweeper.compute_move_path(c)
|
||||
for pc in path:
|
||||
if _marker.has(pc):
|
||||
_marker[pc]["dot"].visible = true
|
||||
@@ -271,36 +272,45 @@ func _hide_path_preview() -> void:
|
||||
func _update_badge(c: Vector2i, btn: Button) -> void:
|
||||
var badge: Label = _marker[c]["badge"]
|
||||
badge.visible = false
|
||||
if _a2 == null or not _a2.is_revealed(c) or not _identity_cb.is_valid():
|
||||
if _minesweeper == null or not _minesweeper.is_revealed(c) or not _identity_cb.is_valid():
|
||||
return
|
||||
if _r0.has(c):
|
||||
return # R₀ 初始展开格无奖励身份(B4 资格域 = S∖R₀)
|
||||
var rec: Dictionary = _a2.reveal_record(c) # E2 收口:已开记录经查询面(禁直读 A2 状态字典)
|
||||
return # R₀ 初始展开格无奖励身份(奖励道具经济 资格域 = S∖R₀)
|
||||
var rec: Dictionary = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面(禁直读 排雷玩法 状态字典)
|
||||
if rec.get("kind", "") != "safe":
|
||||
return
|
||||
var ident = _identity_cb.call(c)
|
||||
if not (ident is Dictionary):
|
||||
return
|
||||
badge.text = String(REWARD_BADGE.get(String(ident.get("category", "")), "◇"))
|
||||
badge.text = _badge_glyph(String(ident.get("category", "")))
|
||||
badge.add_theme_font_size_override("font_size", maxi(10, int(_cell * 0.28)))
|
||||
badge.visible = true
|
||||
|
||||
|
||||
# 奖励图标键解析(HUD框架:查不到的键立即断言 + ? 占位纹样 + 诊断;键集 = 奖励道具经济 category 五值枚举)
|
||||
func _badge_glyph(category: String) -> String:
|
||||
if REWARD_BADGE.has(category):
|
||||
return REWARD_BADGE[category]
|
||||
push_error("[棋盘] 未知奖励图标键(映射表外 category——立即断言): %s" % category)
|
||||
assert(false, "未知奖励图标键: " + category)
|
||||
return BADGE_PLACEHOLDER
|
||||
|
||||
|
||||
# ---- 四格态渲染(灰度截图可辨:完整覆尘/碎裂裸土/雾纹带/已开雾 tint) ----
|
||||
|
||||
func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
var btn: Button = _cells.get(c)
|
||||
if btn == null:
|
||||
return
|
||||
var revealed: bool = _a2.is_revealed(c)
|
||||
var revealed: bool = _minesweeper.is_revealed(c)
|
||||
var fog: bool = _board.variant_ids(c).size() > 0
|
||||
var flag: bool = _a2.has_flag(c)
|
||||
var flag: bool = _minesweeper.has_flag(c)
|
||||
var is_hero: bool = c == hero
|
||||
var text := ""
|
||||
var bg: Color
|
||||
var digit_col := COL_DIGIT
|
||||
if revealed:
|
||||
var rec: Dictionary = _a2.reveal_record(c) # E2 收口:已开记录经查询面
|
||||
var rec: Dictionary = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面
|
||||
if rec.get("kind", "") == "mine":
|
||||
bg = COL_FLOOR
|
||||
text = "雷"
|
||||
@@ -415,7 +425,7 @@ func _check_fog_completion() -> void:
|
||||
continue
|
||||
var all_open := true
|
||||
for c in comp:
|
||||
if not _a2.is_revealed(c):
|
||||
if not _minesweeper.is_revealed(c):
|
||||
all_open = false
|
||||
break
|
||||
if all_open:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/battle.md(B 区敌人卡 ×1–3——敌像占位、血条、顺位徽标、意图徽标〔常驻:
|
||||
# 挖掘期全程可见,映射 = enemy_id 恒定、环点 = 拍数直觉编码,B2 数据源 = query_beat 当前拍〕、
|
||||
# 状态槽〔敌方 side 池口径:伏击层数全体共享单值各卡同值——B1 流水经 B3 投影〕)。
|
||||
# 挖掘期全程可见,映射 = enemy_id 恒定、环点 = 拍数直觉编码,敌人系统 数据源 = query_beat 当前拍〕、
|
||||
# 状态槽〔敌方 side 池口径:伏击层数全体共享单值各卡同值——战斗结算 流水经 增益状态 投影〕)。
|
||||
# Date: 2026-09-12
|
||||
# 敌人死亡呈现(B2 资产下限同等 blocking):血条归零 → 卡体灰化 + 碎裂退场,禁瞬消;
|
||||
# 敌人死亡呈现(敌人系统 资产下限同等 blocking):血条归零 → 卡体灰化 + 碎裂退场,禁瞬消;
|
||||
# 卡体保留灰化残位——定长卡槽,死亡动画不回流布局。
|
||||
# 意图线索映射目录(B2×4① 成文占位——资产定稿归 B5,本处按 enemy_id 程序化占位)。
|
||||
# 意图线索映射目录(敌人系统×4① 成文占位——资产定稿归 战斗表现,本处按 enemy_id 程序化占位)。
|
||||
class_name EnemyCardView
|
||||
extends PanelContainer
|
||||
|
||||
@@ -65,7 +65,7 @@ func _init() -> void:
|
||||
col.add_child(_status_label)
|
||||
|
||||
|
||||
# data = B3/B2 投影:{order, display_name, hp, max_hp, enemy_id, beat, ring_size, ring_index, ambush_layers}
|
||||
# data = 增益状态/敌人系统 投影:{order, display_name, hp, max_hp, enemy_id, beat, ring_size, ring_index, ambush_layers}
|
||||
func set_data(d: Dictionary) -> void:
|
||||
_name_label.text = "%s" % String(d.get("display_name", "敌人"))
|
||||
_hp_bar.max_value = maxi(1, int(d.get("max_hp", 1)))
|
||||
@@ -81,12 +81,12 @@ func set_data(d: Dictionary) -> void:
|
||||
|
||||
|
||||
func _set_order(i: int) -> void:
|
||||
# 顺位徽标 = 行主序生成序(固定顺位——B2 顺位标记义务)
|
||||
# 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务)
|
||||
var base: String = _name_label.text.trim_prefix("① ").trim_prefix("② ").trim_prefix("③ ")
|
||||
_name_label.text = "%s %s" % [String("①②③"[clampi(i - 1, 0, 2)]), base]
|
||||
|
||||
|
||||
# beat = B2 行为环词表原键(attack/charge/heavy,B1 query_beat 直出——presenter 不翻译);
|
||||
# beat = 敌人系统 行为环词表原键(attack/charge/heavy,战斗结算 query_beat 直出——presenter 不翻译);
|
||||
# 名(BEAT_NAMES)/色(COL_BEAT)/重击标记(▼)渲染映射唯一定义处 = 本类(code-review 2026-09-12:
|
||||
# 双翻译曾使色相与重击通道永久失效)。信号/入参契约:set_data 每结算行刷新、set_intent 每拍刷新。
|
||||
func set_intent(beat: String, ring_size: int, ring_index: int) -> void:
|
||||
@@ -151,7 +151,7 @@ func _revive() -> void:
|
||||
|
||||
|
||||
func _portrait_glyph(enemy_id: String) -> String:
|
||||
# enemy_id 恒定映射(B2 Rule 8③)——程序化占位字形:小体量墩形/垂直延伸/最大体量
|
||||
# enemy_id 恒定映射(敌人系统 Rule 8③)——程序化占位字形:小体量墩形/垂直延伸/最大体量
|
||||
match enemy_id:
|
||||
"goblin":
|
||||
return "▂"
|
||||
|
||||
@@ -68,7 +68,7 @@ func set_idle() -> void:
|
||||
_status_label.text = ""
|
||||
|
||||
|
||||
# battle_over 反馈(B3×4④):状态逐槽化光收回 + 微文案——英雄卡侧紧凑引用同步
|
||||
# battle_over 反馈(增益状态×4④):状态逐槽化光收回 + 微文案——英雄卡侧紧凑引用同步
|
||||
func set_status_cleared_feedback() -> void:
|
||||
_status_label.text = "状态随战斗结束消散"
|
||||
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/battle.md(C 区角色信息 240×520——HP/攻击(+修正)/RAP 直角细线数据框
|
||||
# 〔修正来源角标〕、状态效果槽 ×4、小人立绘占位;RAP 清零可见反馈〔A3 义务〕;
|
||||
# 〔修正来源角标〕、状态效果槽 ×4、小人立绘占位;RAP 清零可见反馈〔行动经济 义务〕;
|
||||
# 状态槽空态 = 槽隐藏上移补位〔EC12〕)。
|
||||
# Date: 2026-09-12
|
||||
# 高内聚边界:纯视图 setter 面——装配(含修正文案)归 hud_presenter.gd;定长槽 + clip_contents
|
||||
@@ -70,7 +70,7 @@ func _init() -> void:
|
||||
_status_feedback.add_theme_color_override("font_color", Color8(140, 124, 100))
|
||||
col.add_child(_status_feedback)
|
||||
|
||||
# 小人立绘占位(程序化色块——真实立绘归 B5)
|
||||
# 小人立绘占位(程序化色块——真实立绘归 战斗表现)
|
||||
var portrait := ColorRect.new()
|
||||
portrait.color = Color8(46, 110, 142, 56)
|
||||
portrait.custom_minimum_size = Vector2(0, 120)
|
||||
@@ -92,7 +92,7 @@ func set_hero(hp: int, max_hp: int) -> void:
|
||||
|
||||
|
||||
func set_attack(atk: int, mod_text: String) -> void:
|
||||
# 修正来源角标:mod_text 形如「(+50% 增伤)」——来源 = 效果 display_name(B3 投影)
|
||||
# 修正来源角标:mod_text 形如「(+50% 增伤)」——来源 = 效果 display_name(增益状态 投影)
|
||||
_atk_label.text = "攻击 %d %s" % [atk, mod_text]
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ func set_rap(rap: int, cap_hint: int) -> void:
|
||||
_rap_label.text = "RAP %d(攒 %d 刷图)" % [rap, cap_hint]
|
||||
_render_rap_dots(rap)
|
||||
if prev > 0 and rap == 0:
|
||||
# battle_over RAP 清零可见反馈(A3 D1 义务):数字滚动归零 + 微文案占位
|
||||
# battle_over RAP 清零可见反馈(行动经济 D1 义务):数字滚动归零 + 微文案占位
|
||||
_rap_feedback.text = "随战斗结束消散"
|
||||
var tw := create_tween()
|
||||
tw.tween_property(_rap_label, "modulate", Color(1.4, 1.2, 0.7), 0.12)
|
||||
@@ -116,7 +116,7 @@ func set_status_effects(effects: Array) -> void:
|
||||
_status_feedback.text = ""
|
||||
|
||||
|
||||
# battle_over:状态槽全清可见反馈(逐槽化光 + 微文案——B3×4④)
|
||||
# battle_over:状态槽全清可见反馈(逐槽化光 + 微文案——增益状态×4④)
|
||||
func set_status_cleared_feedback() -> void:
|
||||
slot_row.set_cleared_feedback()
|
||||
_status_feedback.text = "状态随战斗结束消散"
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@
|
||||
# 〔布局决策 D2 / pattern 2 原位窗口激活〕:描边金光点亮、标题切换+时机指示、usable_now 禁选态
|
||||
# +原因〔pattern 8 禁用态即理由〕、跳过按钮、政策注记常驻)。
|
||||
# Date: 2026-09-12(改造自 item_window_overlay.gd——中央模态形态废弃,原位激活替代)
|
||||
# 契约红线:选择语义 = 窗口条目位置(B1 义务④"选定序"),库存句柄不出本类
|
||||
# 契约红线:选择语义 = 窗口条目位置(战斗结算 义务④"选定序"),库存句柄不出本类
|
||||
# (缺陷④契约约束的呈现侧收口点)。来源 {cell, round} 不呈现于列表与 tooltip(2026-09-12 拍板)。
|
||||
class_name InventoryPanelView
|
||||
extends PanelContainer
|
||||
@@ -63,7 +63,7 @@ func _init() -> void:
|
||||
col.add_child(_policy)
|
||||
|
||||
|
||||
# ---- 常读态(挖掘期规划面——B4 D1 义务①) ----
|
||||
# ---- 常读态(挖掘期规划面——奖励道具经济 D1 义务①) ----
|
||||
|
||||
func show_inventory(rows: Array) -> void:
|
||||
if Time.get_ticks_msec() < _cleared_hold_until:
|
||||
@@ -76,7 +76,7 @@ func show_inventory(rows: Array) -> void:
|
||||
_rebuild_rows(rows, false)
|
||||
|
||||
|
||||
# 空态(指引性,非报错——B4 EC 空态)
|
||||
# 空态(指引性,非报错——奖励道具经济 EC 空态)
|
||||
func show_empty() -> void:
|
||||
if Time.get_ticks_msec() < _cleared_hold_until:
|
||||
return
|
||||
@@ -86,7 +86,7 @@ func show_empty() -> void:
|
||||
|
||||
# ---- 窗口态(pattern 2 原位激活:面板描边金光点亮、标题切换 + 时机指示常驻) ----
|
||||
|
||||
# items = B1 组装窗口投影(usable_now 由 B1 组装——呈现层只消费不推断)
|
||||
# items = 战斗结算 组装窗口投影(usable_now 由 战斗结算 组装——呈现层只消费不推断)
|
||||
func open_window(items: Array) -> void:
|
||||
_window_mode = true
|
||||
_set_window_visual(true)
|
||||
@@ -110,7 +110,7 @@ func is_open() -> bool:
|
||||
return _window_mode
|
||||
|
||||
|
||||
# 库存清空反馈(B4 D1×⑤):逐件化光点飞出 + 「剩余道具 ×N 随关卡结束清空」占位
|
||||
# 库存清空反馈(奖励道具经济 D1×⑤):逐件化光点飞出 + 「剩余道具 ×N 随关卡结束清空」占位
|
||||
func play_cleared_feedback(remaining: int) -> void:
|
||||
_cleared_hold_until = Time.get_ticks_msec() + 3000
|
||||
var rows := _list.get_children()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/battle.md(D 区图例按钮 + 模态 overlay——格态/变异/雾语义图例,
|
||||
# 「迷雾 = 无雷盲区」条目恒在〔A4 D1×②〕;状态槽空态说明成文载入〔B3 D1×①〕)。
|
||||
# 「迷雾 = 无雷盲区」条目恒在〔地形变异 D1×②〕;状态槽空态说明成文载入〔增益状态 D1×①〕)。
|
||||
# Date: 2026-09-12
|
||||
# pattern 6 模态 overlay 层级纪律:打开时由组合根隐藏上方遮挡层 + 锁棋盘输入;
|
||||
# 本类自持开关(toggle),探针断言"打开后自身 visible"——遮挡物断言归场景探针。
|
||||
@@ -41,7 +41,7 @@ func _init() -> void:
|
||||
box.add_theme_constant_override("separation", 6)
|
||||
_panel.add_child(box)
|
||||
box.add_child(UiKit.label("图例 · 格态与变异", 20, true))
|
||||
# 「迷雾 = 无雷盲区」条目恒在(A4 图例条目恒在义务)
|
||||
# 「迷雾 = 无雷盲区」条目恒在(地形变异 图例条目恒在义务)
|
||||
box.add_child(UiKit.label("迷雾 = 无雷盲区:雾格不藏雷,不需要赌命开雾。", 14, true))
|
||||
box.add_child(HSeparator.new())
|
||||
for e in ENTRIES:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/battle.md(A 区关卡进度条——全宽 × 28px 顶栏,当前节点高亮、已过节点暗金)。
|
||||
# Date: 2026-09-12(battle.md 五区改造批——对齐 28px 顶栏与高亮样式;基础 = 2026-09-11 C1-lite 呈现)
|
||||
# 关卡节点进度条视图(C1-lite 呈现):线性节点链 战→赏→疗→王 的推进状态。
|
||||
# Date: 2026-09-12(battle.md 五区改造批——对齐 28px 顶栏与高亮样式;基础 = 2026-09-11 关卡节点地图-lite 呈现)
|
||||
# 关卡节点进度条视图(关卡节点地图-lite 呈现):线性节点链 战→赏→疗→王 的推进状态。
|
||||
# 高内聚边界:只读 runner 的节点链/进度做渲染,无任何写面。
|
||||
class_name LevelStripView
|
||||
extends HBoxContainer
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 实现 GDD:design/ux/battle.md(C 区状态效果槽 ×4〔B3 D1×①〕——effect_id→icon 形状+色相双通道、
|
||||
# 实现 GDD:design/ux/battle.md(C 区状态效果槽 ×4〔增益状态 D1×①〕——effect_id→icon 形状+色相双通道、
|
||||
# 层数角标、剩余结算次数点、悬停 tooltip(来源+语义+剩余);空态 = 槽隐藏(数据框上移补位,
|
||||
# EC12 空态裁决);极性映射 = B1 SettlementTable 效果语义而非 magnitude 符号〔B3 D1×③〕)。
|
||||
# EC12 空态裁决);极性映射 = 战斗结算 SettlementTable 效果语义而非 magnitude 符号〔增益状态 D1×③〕)。
|
||||
# Date: 2026-09-12
|
||||
# 高内聚边界:纯视图——效果投影装配归 hud_presenter.gd;B3 纯被动容器零信号——刷新绑 B1
|
||||
# 高内聚边界:纯视图——效果投影装配归 hud_presenter.gd;增益状态 纯被动容器零信号——刷新绑 战斗结算
|
||||
# 结算编排完成时点(battle.md 更新触发注记)。
|
||||
class_name StatusSlotRow
|
||||
extends HBoxContainer
|
||||
|
||||
const MAX_SLOTS := 4
|
||||
# 极性/语义映射(数据源 = SettlementTable.effect_pool_map 效果语义 + StatusEffectTable display_name):
|
||||
# 益 = ▲/◆/▼(金/蓝族);害 = ⚡(旗朱族)——形状+色相双通道,文字名常驻第三通道
|
||||
# 益 = ▲/◆/▼(金/蓝族);害 = ⚡/⚔/✕(旗朱族)——形状+色相双通道,文字名常驻第三通道。
|
||||
# 状态表六行全映射(attack_up/defense_up/weaken/ambush/enrage/off_guard)——HUD框架 图标键契约
|
||||
# "一键一枚专属纹样,查不到的键立即断言 + ? 占位 + 诊断"(2026-09-15 补暴走/失衡两键)。
|
||||
const EFFECT_ICON := {
|
||||
"attack_up": ["▲", Color8(176, 122, 20), "益"],
|
||||
"defense_up": ["◆", Color8(46, 110, 142), "益"],
|
||||
"weaken": ["▼", Color8(46, 110, 142), "益"],
|
||||
"ambush": ["⚡", Color8(198, 61, 34), "害"],
|
||||
"enrage": ["⚔", Color8(198, 61, 34), "害"],
|
||||
"off_guard": ["✕", Color8(198, 61, 34), "害"],
|
||||
}
|
||||
const EFFECT_SEMANTIC := {
|
||||
"attack_up": "主角攻击加成(hero_offense 池加算)",
|
||||
"defense_up": "受到伤害减免(hero_defense 池加算)",
|
||||
"weaken": "敌方输出削弱(enemy_output 池乘算)",
|
||||
"ambush": "敌方全体攻击附加(flat_attach——踩雷转化,战斗段共享不消耗)",
|
||||
"enrage": "敌方输出加成(enemy_output 池加算——标错反噬)",
|
||||
"off_guard": "主角承伤加深(hero_defense 池加算——标错反噬)",
|
||||
}
|
||||
const SOURCE_NAMES := {"safe_cell": "安全格", "flag_hit": "标中旗", "mine_hit": "踩雷", "clear_reward": "清图奖励"}
|
||||
# 未知图标键的占位纹样(HUD框架:占位也要可见,禁静默空图标)
|
||||
const ICON_PLACEHOLDER := ["?", Color8(120, 106, 88), "?"]
|
||||
|
||||
var _tip: PanelContainer
|
||||
var _tip_label: Label
|
||||
@@ -54,7 +62,7 @@ func _init() -> void:
|
||||
_tip.add_child(_tip_label)
|
||||
|
||||
|
||||
# effects = B3 query_active 投影数组(含空态);空槽隐藏,非空按插入序填充(最多 4 槽可见——超出截断)
|
||||
# effects = 增益状态 query_active 投影数组(含空态);空槽隐藏,非空按插入序填充(最多 4 槽可见——超出截断)
|
||||
func set_effects(effects: Array) -> void:
|
||||
_cleared_feedback = false
|
||||
_sync_slots(effects.size())
|
||||
@@ -63,7 +71,7 @@ func set_effects(effects: Array) -> void:
|
||||
_slots[i].visible = true
|
||||
|
||||
|
||||
# battle_over 反馈(B3×4④):逐槽化光收回 + 微文案(下次 set_effects 恢复常态)
|
||||
# battle_over 反馈(增益状态×4④):逐槽化光收回 + 微文案(下次 set_effects 恢复常态)
|
||||
func set_cleared_feedback() -> void:
|
||||
_cleared_feedback = true
|
||||
for slot in _slots:
|
||||
@@ -116,7 +124,7 @@ func _fill_slot(slot: PanelContainer, eff: Dictionary) -> void:
|
||||
var col := slot.get_child(0) as VBoxContainer
|
||||
var icon := col.get_child(0) as Label
|
||||
var meta := col.get_child(1) as Label
|
||||
var icon_cfg: Array = EFFECT_ICON.get(String(eff.effect_id), ["◇", Color8(120, 106, 88), "?"])
|
||||
var icon_cfg: Array = _icon_cfg(String(eff.effect_id))
|
||||
icon.text = String(icon_cfg[0])
|
||||
icon.add_theme_color_override("font_color", icon_cfg[1])
|
||||
# 层数角标 + 剩余结算次数点(● 未耗余量)
|
||||
@@ -132,7 +140,7 @@ func _on_slot_hover(slot: Control) -> void:
|
||||
if not slot.has_meta("eff"):
|
||||
return
|
||||
var eff: Dictionary = slot.get_meta("eff")
|
||||
var icon_cfg: Array = EFFECT_ICON.get(String(eff.effect_id), ["◇", Color8(120, 106, 88), "?"])
|
||||
var icon_cfg: Array = _icon_cfg(String(eff.effect_id))
|
||||
var src: String = String(SOURCE_NAMES.get(String(eff.get("source_class", "")), String(eff.get("source_class", ""))))
|
||||
var semantic: String = String(EFFECT_SEMANTIC.get(String(eff.effect_id), String(eff.get("display_name", ""))))
|
||||
_tip_label.text = "%s〔%s〕\n%s\n剩余结算 %d 次\n来源:%s(第 %d 轮)" % [
|
||||
@@ -142,3 +150,12 @@ func _on_slot_hover(slot: Control) -> void:
|
||||
add_child(_tip)
|
||||
_tip.position = Vector2(0, -70)
|
||||
_tip.visible = true
|
||||
|
||||
|
||||
# 图标键解析(HUD框架:查不到的键按错误处理——立即断言 + ? 占位纹样 + 诊断,禁止静默显示空图标)
|
||||
func _icon_cfg(effect_id: String) -> Array:
|
||||
if EFFECT_ICON.has(effect_id):
|
||||
return EFFECT_ICON[effect_id]
|
||||
push_error("[状态槽] 未知图标键(映射表外键——立即断言): %s" % effect_id)
|
||||
assert(false, "未知图标键: " + effect_id)
|
||||
return ICON_PLACEHOLDER
|
||||
@@ -1,6 +1,6 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-11(2026-09-12 code-review 修复批重构)
|
||||
# 测试用角色数值调整板(模拟天赋树属性提升——仅测试,正式侧归 C4 元进度)。
|
||||
# 测试用角色数值调整板(模拟天赋树属性提升——仅测试,正式侧归元进度)。
|
||||
# 纯视图边界(规范 1.3):本类只承载滑杆 UI 与生效值预览;加成应用 / 重置落表 / 批量模拟编排
|
||||
# 全部经信号外发由组合根承接(apply_requested / reset_requested / simulate_requested / closed),
|
||||
# 模拟进度与报告经 setter 注入——视图零核心写、零 smoke 脚本依赖、不拼报告文案(装配归 presenter)。
|
||||
|
||||
@@ -3,32 +3,32 @@
|
||||
# 自动玩家策略集(headless 冒烟/扫描/转储共用的理性玩家近似):
|
||||
# choose_dig = 挖最近可挖格(先移到邻接已开格);window_selection = 道具窗口确定性选择
|
||||
# (治疗包优先于刀;半血阈值/轮次门/刀力口径经参数表达——四调用点原语义逐一保留)。
|
||||
# 读取面:B1.presentation_snapshot() / A2 查询面(E1/E2 收口后的成文 API——禁直读核心状态)。
|
||||
# 读取面:战斗结算.presentation_snapshot() / 排雷玩法 查询面(E1/E2 收口后的成文 API——禁直读核心状态)。
|
||||
# 等价性证明:smoke_all 同种子回放断言(步骤流逐行相同)在重构后保持全绿。
|
||||
extends RefCounted
|
||||
|
||||
|
||||
# 行主序枚举未开格,找与已开集 8 邻接者;小人先移到该邻接已开格(BFS 可达)。无可挖 → {}
|
||||
static func choose_dig(a2) -> Dictionary:
|
||||
var revealed: Array = a2.revealed_cells()
|
||||
static func choose_dig(minesweeper) -> Dictionary:
|
||||
var revealed: Array = minesweeper.revealed_cells()
|
||||
var revealed_set := {}
|
||||
for c in revealed:
|
||||
revealed_set[c] = true
|
||||
var board = a2.current_board()
|
||||
var board = minesweeper.current_board()
|
||||
for y in board.height:
|
||||
for x in board.width:
|
||||
var c := Vector2i(x, y)
|
||||
if a2.is_revealed(c) or a2.has_flag(c):
|
||||
if minesweeper.is_revealed(c) or minesweeper.has_flag(c):
|
||||
continue
|
||||
for n in board.neighbors8(c):
|
||||
if revealed_set.has(n):
|
||||
var path: Array = a2.compute_move_path(n)
|
||||
if not path.is_empty() or n == a2.hero_pos():
|
||||
var path: Array = minesweeper.compute_move_path(n)
|
||||
if not path.is_empty() or n == minesweeper.hero_pos():
|
||||
return {"dig_cell": c, "hero_cell": n}
|
||||
return {}
|
||||
|
||||
|
||||
# 道具窗口确定性选择(s = b1.presentation_snapshot();半血以下治疗包优先,否则刀):
|
||||
# 道具窗口确定性选择(s = settlement.presentation_snapshot();半血以下治疗包优先,否则刀):
|
||||
# opts.heal_hp_div = 治疗包启用分母(hero_hp × div ≤ max_hp——原四实现 2/3 两档)
|
||||
# opts.round_gate = 刀启用轮次门(round ≥ gate 才用刀;0 = 不设门——monte_carlo 口径)
|
||||
# opts.knife_compare = "best"(最强单刀 ≥ 敌余 HP)| "sum"(刀力合计——dump/monte 口径)
|
||||
@@ -72,50 +72,50 @@ static func window_selection(s: Dictionary, items: Array, opts: Dictionary = {})
|
||||
# ==================== 完美策略档(2026-09-14 用户拍板:OQ7 技能轴上界测量仪——只作测量,非玩法数值) ====================
|
||||
# 策略语义:零盲挖(只挖已证安全格 ⇒ 构造保证零踩雷)· 已证雷格全标旗(旗先于挖——旗 = 纯收益
|
||||
# 〔轮末 weaken + 伤害B 首次充能〕且雷上旗不阻连锁)· 卡死且刷新资源可用 → 轮内刷新
|
||||
# (经 b1.player_request_refresh 核心编排)· 否则停手(残 AP 1:1 转存 RAP——下轮刷新资源)。
|
||||
# 推理规则集 = A1 F1 measure_inferability 同族(single + subset——solver_rules=single_subset 冻结域);
|
||||
# 口径差异 = unknown 集不含 A1 生成期 safe_set oracle(真实玩家约束的未知邻格含雷格)——比生成期闸
|
||||
# 保守,卡死率略高属口径诚实。输入只经 A2 查询面(reveal_record/is_revealed/has_flag/current_board
|
||||
# (经 settlement.player_request_refresh 核心编排)· 否则停手(残 AP 1:1 转存 RAP——下轮刷新资源)。
|
||||
# 推理规则集 = 棋盘生成 F1 measure_inferability 同族(single + subset——solver_rules=single_subset 冻结域);
|
||||
# 口径差异 = unknown 集不含 棋盘生成 生成期 safe_set oracle(真实玩家约束的未知邻格含雷格)——比生成期闸
|
||||
# 保守,卡死率略高属口径诚实。输入只经 排雷玩法 查询面(reveal_record/is_revealed/has_flag/current_board
|
||||
# 的邻居与可达面),禁读棋盘雷区真值(测推理上界,非全知作弊)。
|
||||
# 道具窗口:轮次门解除(round_gate=0)+ 治疗无浪费(heal_no_waste)。
|
||||
# 待拍板(未实现):非卡死主动刷新、囤刀倾泻 vs 即用即耗、完美板目标加权。
|
||||
|
||||
# 单步决策入口(每动作一查——挖掘揭示新数字后重推理):move/dig/flag/refresh/stop 五值。
|
||||
static func perfect_action(a2, b1) -> Dictionary:
|
||||
var ded := _deduce(a2)
|
||||
static func perfect_action(minesweeper, settlement) -> Dictionary:
|
||||
var ded := _deduce(minesweeper)
|
||||
for c in ded["mines"]:
|
||||
if not a2.is_revealed(c) and not a2.has_flag(c):
|
||||
return _plan_on(a2, c, "flag")
|
||||
if not minesweeper.is_revealed(c) and not minesweeper.has_flag(c):
|
||||
return _plan_on(minesweeper, c, "flag")
|
||||
for c in ded["safe"]:
|
||||
if not a2.is_revealed(c) and not a2.has_flag(c):
|
||||
return _plan_on(a2, c, "dig")
|
||||
if b1 != null and bool(b1.can_request_refresh()):
|
||||
if not minesweeper.is_revealed(c) and not minesweeper.has_flag(c):
|
||||
return _plan_on(minesweeper, c, "dig")
|
||||
if settlement != null and bool(settlement.can_request_refresh()):
|
||||
return {"kind": "refresh"}
|
||||
return {"kind": "stop"}
|
||||
|
||||
|
||||
# 目标格需英雄 8 邻——不可达先移到目标的已开 8 邻格(行主序首个可达者;同 choose_dig 寻路口径)
|
||||
static func _plan_on(a2, c: Vector2i, kind: String) -> Dictionary:
|
||||
var d := Vector2i(absi(c.x - a2.hero_pos().x), absi(c.y - a2.hero_pos().y))
|
||||
static func _plan_on(minesweeper, c: Vector2i, kind: String) -> Dictionary:
|
||||
var d := Vector2i(absi(c.x - minesweeper.hero_pos().x), absi(c.y - minesweeper.hero_pos().y))
|
||||
if maxi(d.x, d.y) == 1:
|
||||
return {"kind": kind, "cell": c}
|
||||
var board = a2.current_board()
|
||||
var board = minesweeper.current_board()
|
||||
for n in board.neighbors8(c):
|
||||
if a2.is_revealed(n) and (n == a2.hero_pos() or not a2.compute_move_path(n).is_empty()):
|
||||
if minesweeper.is_revealed(n) and (n == minesweeper.hero_pos() or not minesweeper.compute_move_path(n).is_empty()):
|
||||
return {"kind": "move", "cell": n}
|
||||
return {"kind": "stop"} # 不可达(理论不可达——已证格必邻接已开区);防御性停手防死循环
|
||||
|
||||
|
||||
# 推理不动点:single(约束余雷=0⇒未知全安全 / 余雷=未知数⇒全雷)+ subset(U_a ⊂ U_b ⇒ 差集约束)。
|
||||
# 约束只依赖 revealed/flags/mines 推断集——mines 集增长才需重建约束(safe 标记不改正约约束)。
|
||||
static func _deduce(a2) -> Dictionary:
|
||||
var board = a2.current_board()
|
||||
static func _deduce(minesweeper) -> Dictionary:
|
||||
var board = minesweeper.current_board()
|
||||
var safe := {}
|
||||
var mines := {}
|
||||
var changed := true
|
||||
while changed:
|
||||
changed = false
|
||||
var cons := _constraints(a2, board, mines)
|
||||
var cons := _constraints(minesweeper, board, mines)
|
||||
for con in cons:
|
||||
if int(con["mines"]) == 0:
|
||||
for c in con["unknown"].keys():
|
||||
@@ -127,7 +127,7 @@ static func _deduce(a2) -> Dictionary:
|
||||
if not mines.has(c):
|
||||
mines[c] = true
|
||||
changed = true
|
||||
var cons2 := _constraints(a2, board, mines)
|
||||
var cons2 := _constraints(minesweeper, board, mines)
|
||||
for i in cons2.size():
|
||||
for j in range(i + 1, cons2.size()):
|
||||
var der := _subset_derived(cons2[i], cons2[j])
|
||||
@@ -148,10 +148,10 @@ static func _deduce(a2) -> Dictionary:
|
||||
|
||||
# 约束集 = 已开数字格:mines_left = digit − 已知雷旗(旗位 + 已证雷格),unknown = 未开无旗 8 邻。
|
||||
# 已爆雷格(kind=mine)不带数字约束——其贡献已含在邻格 digit 内(数字恒真不变式 inv3)。
|
||||
static func _constraints(a2, board, mines: Dictionary) -> Array:
|
||||
static func _constraints(minesweeper, board, mines: Dictionary) -> Array:
|
||||
var out: Array = []
|
||||
for c in a2.revealed_cells():
|
||||
var rec: Dictionary = a2.reveal_record(c)
|
||||
for c in minesweeper.revealed_cells():
|
||||
var rec: Dictionary = minesweeper.reveal_record(c)
|
||||
if String(rec.get("kind", "")) != "safe":
|
||||
continue
|
||||
var d := int(rec.get("digit", -1))
|
||||
@@ -160,9 +160,9 @@ static func _constraints(a2, board, mines: Dictionary) -> Array:
|
||||
var unknown := {}
|
||||
var known := 0
|
||||
for n in board.neighbors8(c):
|
||||
if a2.has_flag(n):
|
||||
if minesweeper.has_flag(n):
|
||||
known += 1
|
||||
elif not a2.is_revealed(n):
|
||||
elif not minesweeper.is_revealed(n):
|
||||
if mines.has(n):
|
||||
known += 1
|
||||
else:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# H2 Boss/敌攻击链缺陷(bug——fresh 池下攻击拍仍零兑现)
|
||||
# 快照盲区说明:settlement_snapshot 取在衰减段之后,时效 1 的 defense_up 当轮授予当轮衰减——
|
||||
# 真人局 JSONL buffs[] 恒空 ≠ 池为空,本探针改从结算行(授予/续存 effect 行的 pool_value)重建战斗段时点池值。
|
||||
# 观测面:全部读 B1 结算行 / 表值 / 呈现快照(零核心穿透);池归属/墙深/池界全读表(D2 Rule 1——零数值字面量)。
|
||||
# 观测面:全部读战斗结算步骤行 / 表值 / 呈现快照(零核心穿透);池归属/墙深/池界全读表(数据配置 Rule 1——零数值字面量)。
|
||||
# 模式(-- 后参数):boss [seed](默认 42:Run A 零挖掘 fresh 池 + Run B 自动挖掘墙行为)
|
||||
# level [seed](全关遭遇回放 + 多种子 spawn 扫描——报告 Bug#1 证据面)
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/damage_probe.gd -- boss 42
|
||||
@@ -83,37 +83,37 @@ func _probe_boss(db, seed_val: int) -> void:
|
||||
func _run_boss(db, seed_val: int, dig: bool) -> Dictionary:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_val)
|
||||
var b1 = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
var cap := {"rows": [], "snaps": [], "result": ""}
|
||||
b1.step_row.connect(func(row): cap["rows"].append(row))
|
||||
b1.settlement_snapshot.connect(func(s): cap["snaps"].append(s))
|
||||
b1.battle_finished.connect(func(p): cap["result"] = String(p.get("result", "")))
|
||||
b1.window_opened.connect(func(items):
|
||||
var s: Dictionary = b1.presentation_snapshot()
|
||||
b1.submit_window_selection(AutoPlayer.window_selection(s, items, {"heal_hp_div": 2, "round_gate": 3, "knife_compare": "best"})))
|
||||
if not b1.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.hero.max_hp)}):
|
||||
settlement.step_row.connect(func(row): cap["rows"].append(row))
|
||||
settlement.settlement_snapshot.connect(func(s): cap["snaps"].append(s))
|
||||
settlement.battle_finished.connect(func(p): cap["result"] = String(p.get("result", "")))
|
||||
settlement.window_opened.connect(func(items):
|
||||
var s: Dictionary = settlement.presentation_snapshot()
|
||||
settlement.submit_window_selection(AutoPlayer.window_selection(s, items, {"heal_hp_div": 2, "round_gate": 3, "knife_compare": "best"})))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.hero.max_hp)}):
|
||||
cap["result"] = "begin_failed"
|
||||
return cap
|
||||
var rounds := 0
|
||||
while b1.battle_result == "" and rounds < 40:
|
||||
while settlement.battle_result == "" and rounds < 40:
|
||||
rounds += 1
|
||||
var start_round: int = b1.round_index
|
||||
var start_round: int = settlement.round_index
|
||||
if dig:
|
||||
var a2 = b1.a2
|
||||
var minesweeper = settlement.minesweeper
|
||||
var guard := 400
|
||||
while b1.a3.phase == "Active" and b1.round_index == start_round and guard > 0:
|
||||
while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0:
|
||||
guard -= 1
|
||||
var plan := AutoPlayer.choose_dig(a2)
|
||||
var plan := AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != a2.hero_pos():
|
||||
a2.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if b1.a3.phase == "Active":
|
||||
a2.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if b1.a3.phase == "Active" and b1.round_index == start_round:
|
||||
a2.settle_batch([{"action": "stop"}])
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if settlement.action_economy.phase == "Active":
|
||||
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
else:
|
||||
b1.a2.settle_batch([{"action": "stop"}])
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
return cap
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary:
|
||||
var pv := int(row.get("pool_value", 0))
|
||||
last_pool[eid] = pv
|
||||
grants.append("%s→池%d" % [String(row.get("display_name", "")), pv])
|
||||
# 池归属表驱动(settlement.effect_pool_map),percent 池加算后 clamp(B1 F2 同口径)
|
||||
# 池归属表驱动(settlement.effect_pool_map),percent 池加算后 clamp(战斗结算 F2 同口径)
|
||||
var pools := {"hero_defense": 0, "enemy_output": 0, "hero_offense": 0, "flat_attach": 0}
|
||||
for eid in last_pool:
|
||||
var pool_name: String = String(db.settlement.effect_pool_map.get(eid, ""))
|
||||
@@ -254,7 +254,7 @@ func _probe_level(db, seed_val: int) -> void:
|
||||
all_ge_1 = false
|
||||
if spawn2 != int(prediction.get(int(b.get("depth", -1)), -2)):
|
||||
all_match = false
|
||||
check(all_ge_1, "全部战斗 spawn ≥ 1(C3 Rule 8 区间下限——空遭遇在当前实现不可达)")
|
||||
check(all_ge_1, "全部战斗 spawn ≥ 1(难度曲线与Boss Rule 8 区间下限——空遭遇在当前实现不可达)")
|
||||
check(all_match, "spawn 与独立遭遇骰预测逐战斗一致(encounter 封闭流同种子可回放)")
|
||||
var expect_battles := _battle_nodes_upto(db, 1, int(cap.get("end_node", -1)))
|
||||
check(battles.size() == expect_battles, "战斗推进数与节点链一致(实测 %d/预期 %d——败北截断或全链跑通)" % [battles.size(), expect_battles])
|
||||
@@ -309,10 +309,10 @@ func _run_level(db, seed_val: int) -> Dictionary:
|
||||
var guard_battles := 20
|
||||
while cap["result"] == "" and guard_battles > 0:
|
||||
guard_battles -= 1
|
||||
var b1 = runner.current_battle()
|
||||
if b1 == null:
|
||||
var settlement = runner.current_battle()
|
||||
if settlement == null:
|
||||
break
|
||||
_drive_battle(runner, b1, cap)
|
||||
_drive_battle(runner, settlement, cap)
|
||||
return cap
|
||||
|
||||
|
||||
@@ -332,11 +332,11 @@ func _battle_nodes_upto(db, level_index: int, node_idx: int) -> int:
|
||||
|
||||
|
||||
func _on_level_window(items: Array, runner) -> void:
|
||||
var b1 = runner.current_battle()
|
||||
if b1 == null:
|
||||
var settlement = runner.current_battle()
|
||||
if settlement == null:
|
||||
runner.submit_window_selection([])
|
||||
return
|
||||
var s: Dictionary = b1.presentation_snapshot()
|
||||
var s: Dictionary = settlement.presentation_snapshot()
|
||||
runner.submit_window_selection(AutoPlayer.window_selection(s, items, {"heal_hp_div": 2, "round_gate": 3, "knife_compare": "best"}))
|
||||
|
||||
|
||||
@@ -351,33 +351,33 @@ func _on_level_node_entered(node: Dictionary, _info: Dictionary, runner, cap: Di
|
||||
func _on_level_round_started(ri: int, runner, cap: Dictionary) -> void:
|
||||
if ri != 1 or cap["battles"].is_empty():
|
||||
return
|
||||
var b1 = runner.current_battle()
|
||||
if b1 == null:
|
||||
var settlement = runner.current_battle()
|
||||
if settlement == null:
|
||||
return
|
||||
var s: Dictionary = b1.presentation_snapshot()
|
||||
var s: Dictionary = settlement.presentation_snapshot()
|
||||
cap["battles"][cap["battles"].size() - 1]["spawn"] = s.enemies.size()
|
||||
|
||||
|
||||
func _drive_battle(runner, b1, cap: Dictionary) -> void:
|
||||
func _drive_battle(runner, settlement, cap: Dictionary) -> void:
|
||||
var round_guard := 60
|
||||
while cap["result"] == "" and runner.current_battle() == b1 and b1.battle_result == "" and round_guard > 0:
|
||||
while cap["result"] == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
|
||||
round_guard -= 1
|
||||
var start_round: int = b1.round_index
|
||||
var a2 = b1.a2
|
||||
var start_round: int = settlement.round_index
|
||||
var minesweeper = settlement.minesweeper
|
||||
var action_guard := 400
|
||||
while runner.current_battle() == b1 and b1.battle_result == "" and b1.a3.phase == "Active" \
|
||||
and b1.round_index == start_round and action_guard > 0:
|
||||
while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \
|
||||
and settlement.round_index == start_round and action_guard > 0:
|
||||
action_guard -= 1
|
||||
var plan := AutoPlayer.choose_dig(a2)
|
||||
var plan := AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != a2.hero_pos():
|
||||
a2.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if b1.a3.phase != "Active" or b1.round_index != start_round:
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
|
||||
break
|
||||
a2.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if runner.current_battle() == b1 and b1.battle_result == "" and b1.a3.phase == "Active" and b1.round_index == start_round:
|
||||
a2.settle_batch([{"action": "stop"}])
|
||||
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
|
||||
|
||||
# 战斗判定摘要:round-1 victory 的战斗必须能从结算行分辨「首轮全灭」vs「空场」——
|
||||
@@ -445,11 +445,11 @@ func _scan_spawns(db, seed0: int, n_seeds: int) -> Dictionary:
|
||||
rng.set_master_seed(seed0 + i)
|
||||
for depth in depths:
|
||||
total += 1
|
||||
var b1 = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
if not b1.begin_battle({"level_index": 1, "node_depth": int(depth), "hero_hp": int(db.hero.max_hp)}):
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": int(depth), "hero_hp": int(db.hero.max_hp)}):
|
||||
bad += 1
|
||||
continue
|
||||
var n: int = b1.presentation_snapshot().enemies.size()
|
||||
var n: int = settlement.presentation_snapshot().enemies.size()
|
||||
var row: Dictionary = db.resolve_enemy_rowset(1, int(depth))
|
||||
var lo := int(row.get("unit_count", 1))
|
||||
var hi := int(row.get("unit_count_max", lo))
|
||||
|
||||
@@ -12,43 +12,43 @@ func _initialize() -> void:
|
||||
db.load_tables("res://data/tables", false)
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(42) # 种子 = 42;第二个命令行参数可覆盖(关卡 种子)
|
||||
var b1 = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
var stream: Array = []
|
||||
b1.step_row.connect(func(row): stream.append(row))
|
||||
b1.window_opened.connect(func(items):
|
||||
var s: Dictionary = b1.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况
|
||||
settlement.step_row.connect(func(row): stream.append(row))
|
||||
settlement.window_opened.connect(func(items):
|
||||
var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况
|
||||
var selection: Array = AutoPlayer.window_selection(s, items,
|
||||
{"heal_hp_div": 3, "round_gate": 3, "knife_compare": "sum"}) # O4:原 dump_battle 口径
|
||||
b1.submit_window_selection(selection))
|
||||
settlement.submit_window_selection(selection))
|
||||
var level := 2
|
||||
if OS.get_cmdline_user_args().size() > 0:
|
||||
level = int(OS.get_cmdline_user_args()[0])
|
||||
if OS.get_cmdline_user_args().size() > 1:
|
||||
rng.set_master_seed(int(OS.get_cmdline_user_args()[1]))
|
||||
b1.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": db.hero.max_hp})
|
||||
settlement.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": db.hero.max_hp})
|
||||
print("=== 棋盘摘要 L%d ===" % level)
|
||||
var bd = b1.a2.current_board()
|
||||
var bd = settlement.minesweeper.current_board()
|
||||
print("尺寸 %dx%d · 雷 %d · 安全格 %d · 雾格 %d · R₀ %d" % [bd.width, bd.height, bd.mines.size(), bd.safe_cells().size(), bd.fog_cells().size(), bd.metrics.r0_count])
|
||||
print("指标: inferability=%.3f core=%.3f attempts=%s accept=%s" % [bd.metrics.inferability, bd.metrics.inferability_core, bd.metrics.attempts_used, bd.metrics.accept_mode])
|
||||
var rounds := 0
|
||||
while b1.battle_result == "" and rounds < 40:
|
||||
while settlement.battle_result == "" and rounds < 40:
|
||||
rounds += 1
|
||||
var start_round: int = b1.round_index
|
||||
var a2 = b1.a2
|
||||
var a3 = b1.a3
|
||||
var start_round: int = settlement.round_index
|
||||
var minesweeper = settlement.minesweeper
|
||||
var action_economy = settlement.action_economy
|
||||
var guard := 400
|
||||
while a3.phase == "Active" and b1.round_index == start_round and guard > 0:
|
||||
while action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0:
|
||||
guard -= 1
|
||||
var plan := AutoPlayer.choose_dig(a2)
|
||||
var plan := AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != a2.hero_pos():
|
||||
a2.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if a3.phase == "Active":
|
||||
a2.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if a3.phase == "Active" and b1.round_index == start_round:
|
||||
a2.settle_batch([{"action": "stop"}])
|
||||
print("战斗结果: %s(%d 轮)" % [b1.battle_result, rounds])
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if action_economy.phase == "Active":
|
||||
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if action_economy.phase == "Active" and settlement.round_index == start_round:
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
print("战斗结果: %s(%d 轮)" % [settlement.battle_result, rounds])
|
||||
print("=== 结算步骤流(%d 行)===" % stream.size())
|
||||
for row in stream:
|
||||
print(JSON.stringify(row))
|
||||
|
||||
@@ -6,7 +6,7 @@ extends SceneTree
|
||||
func _initialize() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
db.load_tables("res://data/tables", true)
|
||||
var a1 = load("res://core/board_generator.gd").new()
|
||||
var board_generator = load("res://core/board_generator.gd").new()
|
||||
print("=== 密度实验(L1 10×10 target0.95 / L2 12×12 雾 target0.92,各 5 种子)===")
|
||||
print("d\t|M|/N\tR₀\tR₀/|S|\t残余挖掘\tinf\tcore\tatt")
|
||||
for d in [0.22, 0.25, 0.28, 0.30, 0.33]:
|
||||
@@ -29,7 +29,7 @@ func _initialize() -> void:
|
||||
var params := _params_from(row)
|
||||
var rg := RandomNumberGenerator.new()
|
||||
rg.seed = 1000 + s
|
||||
var b = a1.generate(1, 0, rg, _db(db, row))
|
||||
var b = board_generator.generate(1, 0, rg, _db(db, row))
|
||||
if b == null:
|
||||
continue
|
||||
agg.m += b.mines.size()
|
||||
|
||||
@@ -45,10 +45,10 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct:
|
||||
var guard_battles := 20
|
||||
while st.result == "" and guard_battles > 0:
|
||||
guard_battles -= 1
|
||||
var this_b1 = runner.current_battle()
|
||||
if this_b1 == null:
|
||||
break # level_finished 已发(b1 置 null)而信号尚未置 result 的防御
|
||||
_autoplay_battle(runner, this_b1, st, logger)
|
||||
var this_settlement = runner.current_battle()
|
||||
if this_settlement == null:
|
||||
break # level_finished 已发(settlement 置 null)而信号尚未置 result 的防御
|
||||
_autoplay_battle(runner, this_settlement, st, logger)
|
||||
if st.result == "":
|
||||
st.result = "timeout"
|
||||
st.timeout = true
|
||||
@@ -71,8 +71,8 @@ static func _on_step_row(row: Dictionary, _runner, st: Dictionary, collect_rows:
|
||||
|
||||
|
||||
static func _on_window_opened(items: Array, runner, st: Dictionary, logger) -> void:
|
||||
var b1 = runner.current_battle()
|
||||
var s: Dictionary = b1.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况
|
||||
var settlement = runner.current_battle()
|
||||
var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况
|
||||
var opts := {"heal_hp_div": 2, "round_gate": 3, "knife_compare": "best"} # O4:原 autoplay 口径
|
||||
if String(st.get("policy", "normal")) == "perfect":
|
||||
opts = {"heal_hp_div": 2, "round_gate": 0, "knife_compare": "best", "heal_no_waste": true}
|
||||
@@ -116,64 +116,64 @@ static func _on_level_finished(p: Dictionary, st: Dictionary, logger) -> void:
|
||||
logger.log_level_end(p)
|
||||
|
||||
|
||||
static func _autoplay_battle(runner, b1, st: Dictionary, logger) -> void:
|
||||
static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void:
|
||||
if String(st.get("policy", "normal")) == "perfect":
|
||||
_autoplay_battle_perfect(runner, b1, st, logger)
|
||||
_autoplay_battle_perfect(runner, settlement, st, logger)
|
||||
return
|
||||
var round_guard := 60
|
||||
while st.result == "" and runner.current_battle() == b1 and b1.battle_result == "" and round_guard > 0:
|
||||
while st.result == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
|
||||
round_guard -= 1
|
||||
st.rounds += 1
|
||||
var start_round: int = b1.round_index
|
||||
var start_round: int = settlement.round_index
|
||||
var action_guard := 400
|
||||
while runner.current_battle() == b1 and b1.battle_result == "" and b1.a3.phase == "Active" \
|
||||
and b1.round_index == start_round and action_guard > 0:
|
||||
while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \
|
||||
and settlement.round_index == start_round and action_guard > 0:
|
||||
action_guard -= 1
|
||||
var plan := AutoPlayer.choose_dig(b1.a2)
|
||||
var plan := AutoPlayer.choose_dig(settlement.minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != b1.a2.hero_pos():
|
||||
var out_m: Array = b1.a2.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if plan.hero_cell != settlement.minesweeper.hero_pos():
|
||||
var out_m: Array = settlement.minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if logger != null:
|
||||
logger.log_action("move", plan.hero_cell, _accepted(out_m), {"round": start_round})
|
||||
if b1.a3.phase != "Active" or b1.round_index != start_round:
|
||||
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
|
||||
break
|
||||
var out_d: Array = b1.a2.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
var out_d: Array = settlement.minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if logger != null:
|
||||
logger.log_action("dig", plan.dig_cell, _accepted(out_d), {"round": start_round})
|
||||
if runner.current_battle() == b1 and b1.battle_result == "" and b1.a3.phase == "Active" and b1.round_index == start_round:
|
||||
var out_s: Array = b1.a2.settle_batch([{"action": "stop"}])
|
||||
if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
|
||||
var out_s: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
if logger != null:
|
||||
logger.log_action("stop", Vector2i(-1, -1), _accepted(out_s), {"round": start_round})
|
||||
|
||||
|
||||
# 完美策略档战斗驱动(2026-09-14):单步决策循环——move/dig/flag 经 A2 settle_batch 提交,
|
||||
# refresh 经 b1.player_request_refresh(B1 核心编排:A3 扣 RAP + 退役 + 当场换新板),
|
||||
# 完美策略档战斗驱动(2026-09-14):单步决策循环——move/dig/flag 经 排雷玩法 settle_batch 提交,
|
||||
# refresh 经 settlement.player_request_refresh(战斗结算 核心编排:行动经济 扣 RAP + 退役 + 当场换新板),
|
||||
# stop 出内环走统一停手提交。连续拒绝 ≥3 = 编排违约防御(fail-fast 语义,停手防死循环)。
|
||||
static func _autoplay_battle_perfect(runner, b1, st: Dictionary, logger) -> void:
|
||||
static func _autoplay_battle_perfect(runner, settlement, st: Dictionary, logger) -> void:
|
||||
var round_guard := 60
|
||||
while st.result == "" and runner.current_battle() == b1 and b1.battle_result == "" and round_guard > 0:
|
||||
while st.result == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
|
||||
round_guard -= 1
|
||||
st.rounds += 1
|
||||
var start_round: int = b1.round_index
|
||||
var start_round: int = settlement.round_index
|
||||
var action_guard := 400
|
||||
var rejects := 0
|
||||
while runner.current_battle() == b1 and b1.battle_result == "" and b1.a3.phase == "Active" \
|
||||
and b1.round_index == start_round and action_guard > 0 and rejects < 3:
|
||||
while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \
|
||||
and settlement.round_index == start_round and action_guard > 0 and rejects < 3:
|
||||
action_guard -= 1
|
||||
var act := AutoPlayer.perfect_action(b1.a2, b1)
|
||||
var act := AutoPlayer.perfect_action(settlement.minesweeper, settlement)
|
||||
var kind := String(act.get("kind", "stop"))
|
||||
if kind == "stop":
|
||||
break
|
||||
var ok := false
|
||||
if kind == "move":
|
||||
ok = _submit_intent(b1, "move", act.get("cell"), logger, start_round)
|
||||
ok = _submit_intent(settlement, "move", act.get("cell"), logger, start_round)
|
||||
elif kind == "dig":
|
||||
ok = _submit_intent(b1, "dig", act.get("cell"), logger, start_round)
|
||||
ok = _submit_intent(settlement, "dig", act.get("cell"), logger, start_round)
|
||||
elif kind == "flag":
|
||||
ok = _submit_intent(b1, "flag_place", act.get("cell"), logger, start_round)
|
||||
ok = _submit_intent(settlement, "flag_place", act.get("cell"), logger, start_round)
|
||||
elif kind == "refresh":
|
||||
ok = bool(b1.player_request_refresh())
|
||||
ok = bool(settlement.player_request_refresh())
|
||||
if logger != null:
|
||||
logger.log_action("refresh", Vector2i(-1, -1), ok, {"round": start_round})
|
||||
if ok:
|
||||
@@ -182,14 +182,14 @@ static func _autoplay_battle_perfect(runner, b1, st: Dictionary, logger) -> void
|
||||
rejects = 0
|
||||
else:
|
||||
rejects += 1
|
||||
if runner.current_battle() == b1 and b1.battle_result == "" and b1.a3.phase == "Active" and b1.round_index == start_round:
|
||||
var out_s: Array = b1.a2.settle_batch([{"action": "stop"}])
|
||||
if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
|
||||
var out_s: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
if logger != null:
|
||||
logger.log_action("stop", Vector2i(-1, -1), _accepted(out_s), {"round": start_round})
|
||||
|
||||
|
||||
static func _submit_intent(b1, action: String, cell, logger, round_index: int) -> bool:
|
||||
var out: Array = b1.a2.settle_batch([{"action": action, "cell": cell}])
|
||||
static func _submit_intent(settlement, action: String, cell, logger, round_index: int) -> bool:
|
||||
var out: Array = settlement.minesweeper.settle_batch([{"action": action, "cell": cell}])
|
||||
if logger != null:
|
||||
logger.log_action(action, cell, _accepted(out), {"round": round_index})
|
||||
return _accepted(out)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-11
|
||||
# 关卡期望扫描(C3 调参期关卡侧批量工具的原型——对应 A1 F4 / 战斗侧 monte_carlo.gd):
|
||||
# 关卡期望扫描(难度曲线与Boss 调参期关卡侧批量工具的原型——对应 棋盘生成 F4 / 战斗侧 monte_carlo.gd):
|
||||
# 扫描 atk% × hp% 网格(模拟天赋树数值提升档位),输出每档"通关率 / 整关承伤期望 / 败北节点分布 /
|
||||
# 通关剩余 HP 期望 / 进 Boss HP 期望 / 平均轮数"——把"多少数值能通关、通关要掉多少血"变成可测量数据。
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/level_monte_carlo.gd -- [N] [seed0]
|
||||
@@ -53,8 +53,8 @@ func _initialize() -> void:
|
||||
var hp_boss: Array = []
|
||||
var rounds_all: Array = []
|
||||
var actives_per_battle: Array = []
|
||||
var normal_dmg_pct: Array = [] # 每普通战斗承伤占生效 max_hp 比(C3 F4 口径——≤20% 带)
|
||||
var boss_dmg_pct: Array = [] # Boss 战承伤占生效 max_hp 比(C3 F4 口径——≤50% 带)
|
||||
var normal_dmg_pct: Array = [] # 每普通战斗承伤占生效 max_hp 比(难度曲线与Boss F4 口径——≤20% 带)
|
||||
var boss_dmg_pct: Array = [] # Boss 战承伤占生效 max_hp 比(难度曲线与Boss F4 口径——≤50% 带)
|
||||
var wipes := 0 # 普通战斗 round-1 即胜数(首轮全灭跳战——拍板④观测面)
|
||||
var normal_battles := 0
|
||||
var death_nodes := {}
|
||||
|
||||
@@ -52,44 +52,44 @@ func _run_encounter(db, L: int) -> void:
|
||||
func _run_battle(db, seed_val: int, L: int) -> Dictionary:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_val)
|
||||
var b1 = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
var out := {"rounds": 0, "result": "", "hero_dmg": 0, "safe_opened": 0, "board_swaps": 0, "buffs": 0}
|
||||
b1.battle_finished.connect(func(payload): out.result = payload.get("result", ""))
|
||||
b1.board_changed.connect(func(): out.board_swaps += 1)
|
||||
b1.step_row.connect(func(row):
|
||||
settlement.battle_finished.connect(func(payload): out.result = payload.get("result", ""))
|
||||
settlement.board_changed.connect(func(): out.board_swaps += 1)
|
||||
settlement.step_row.connect(func(row):
|
||||
if row.type == "damage" and row.source_class == "enemy_action":
|
||||
out.hero_dmg += int(row.value)
|
||||
elif row.type == "effect" and row.source_class == "safe_cell" and (row.effect_id == "attack_up" or row.effect_id == "defense_up"):
|
||||
out.buffs += 1)
|
||||
b1.a2.cell_revealed.connect(func(evt):
|
||||
settlement.minesweeper.cell_revealed.connect(func(evt):
|
||||
if evt.kind == "safe" and evt.channel != "initial":
|
||||
out.safe_opened += 1)
|
||||
b1.window_opened.connect(func(items):
|
||||
var s: Dictionary = b1.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况
|
||||
settlement.window_opened.connect(func(items):
|
||||
var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况
|
||||
var selection: Array = AutoPlayer.window_selection(s, items,
|
||||
{"heal_hp_div": 2, "round_gate": 0, "knife_compare": "sum"}) # O4:原 monte_carlo 口径
|
||||
b1.submit_window_selection(selection))
|
||||
if not b1.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}):
|
||||
settlement.submit_window_selection(selection))
|
||||
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}):
|
||||
return out
|
||||
var guard_rounds := 40
|
||||
while out.result == "" and guard_rounds > 0:
|
||||
guard_rounds -= 1
|
||||
out.rounds += 1
|
||||
var start_round: int = b1.round_index
|
||||
var a2 = b1.a2
|
||||
var start_round: int = settlement.round_index
|
||||
var minesweeper = settlement.minesweeper
|
||||
var guard := 400
|
||||
while b1.a3.phase == "Active" and b1.round_index == start_round and guard > 0 and out.result == "":
|
||||
while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0 and out.result == "":
|
||||
guard -= 1
|
||||
var plan := AutoPlayer.choose_dig(a2)
|
||||
var plan := AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != a2.hero_pos():
|
||||
a2.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if b1.a3.phase != "Active" or b1.round_index != start_round:
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
|
||||
break
|
||||
a2.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if b1.a3.phase == "Active" and b1.round_index == start_round and out.result == "":
|
||||
a2.settle_batch([{"action": "stop"}])
|
||||
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if settlement.action_economy.phase == "Active" and settlement.round_index == start_round and out.result == "":
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
if out.result == "":
|
||||
out.result = "timeout"
|
||||
return out
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-11
|
||||
# 奖励期望测量(供给控制器批口径):逐深度实测 |资格格|(safe ∖ R0)分布、每板注入件数(j=0 → 恒 1)、
|
||||
# 注入种类分布(60/30/10 拍板值)、格子池零可选类验证——走真实 B4.roll_board 管线(含注入判定取号序)。
|
||||
# 注入种类分布(60/30/10 拍板值)、格子池零可选类验证——走真实 奖励道具经济.roll_board 管线(含注入判定取号序)。
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/reward_expectation.gd
|
||||
extends SceneTree
|
||||
|
||||
@@ -14,8 +14,8 @@ func _initialize() -> void:
|
||||
print("表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
var a1 = load("res://core/board_generator.gd").new()
|
||||
var b4_factory = load("res://core/reward_economy.gd")
|
||||
var board_generator = load("res://core/board_generator.gd").new()
|
||||
var reward_economy_factory = load("res://core/reward_economy.gd")
|
||||
var rng_factory = load("res://core/rng_service.gd")
|
||||
var active_ids := {"knife_t1": true, "knife_adv": true, "heal_pack": true}
|
||||
print("=== 奖励期望测量(供给控制器批 · N=%d 板/深度 · 每板全新战斗态 j=0)===" % N)
|
||||
@@ -28,7 +28,7 @@ func _initialize() -> void:
|
||||
for i in range(N):
|
||||
var rng = rng_factory.new()
|
||||
rng.set_master_seed(90000 + depth * 1000 + i)
|
||||
var board = a1.generate(1, depth, rng.open_stream("board_gen"), db)
|
||||
var board = board_generator.generate(1, depth, rng.open_stream("board_gen"), db)
|
||||
if board == null:
|
||||
continue
|
||||
var r0 := {}
|
||||
@@ -39,15 +39,15 @@ func _initialize() -> void:
|
||||
if not r0.has(c):
|
||||
eligible.append(c)
|
||||
elig.append(eligible.size())
|
||||
var b4 = b4_factory.new(db, rng)
|
||||
var reward_economy = reward_economy_factory.new(db, rng)
|
||||
var list: Array = []
|
||||
for c in eligible:
|
||||
list.append({"cell": c, "fog": board.variant_ids(c).size() > 0})
|
||||
if not b4.roll_board(list):
|
||||
if not reward_economy.roll_board(list):
|
||||
continue
|
||||
var n_active := 0
|
||||
for c in eligible:
|
||||
var snap = b4.query_identity(c)
|
||||
var snap = reward_economy.query_identity(c)
|
||||
if snap != null and active_ids.has(String(snap.reward_id)):
|
||||
n_active += 1
|
||||
species[String(snap.reward_id)] += 1.0
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -76,12 +76,12 @@ func _ready() -> void:
|
||||
_done = true
|
||||
print("[click_probe] 关卡结束:%s" % main._end_overlay.verdict_text().replace("\n", " "))
|
||||
break
|
||||
if main.b1 == null or main.runner == null:
|
||||
if main.settlement == null or main.runner == null:
|
||||
continue
|
||||
var diag: Dictionary = main.b1.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
|
||||
var diag: Dictionary = main.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
|
||||
var st := {
|
||||
"node": main.runner.node_index, "round": main.b1.round_index,
|
||||
"phase": main.b1.a3.phase, "ap": main.b1.a3.ap,
|
||||
"node": main.runner.node_index, "round": main.settlement.round_index,
|
||||
"phase": main.settlement.action_economy.phase, "ap": main.settlement.action_economy.ap,
|
||||
"win": main.inventory_panel.is_window_open(), "settling": diag.settling,
|
||||
"await": diag.awaiting_window,
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func _ready() -> void:
|
||||
_stall_frames = 0
|
||||
_ap_spent_this_round = 0
|
||||
_last_state = st
|
||||
if main.b1.battle_result == "" and main.b1.a3.phase == "Active":
|
||||
if main.settlement.battle_result == "" and main.settlement.action_economy.phase == "Active":
|
||||
_run_modal_probes_once()
|
||||
_play_one_action()
|
||||
print("[click_probe] 循环退出(done=%s budget=%s)选用生效=%s 选用失效=%s 问题=%d" % [
|
||||
@@ -156,17 +156,17 @@ func _run_modal_probes_once() -> void:
|
||||
# ④ 弃图刷新确认链(pattern 7 异位:确认 chips 与常规确认不同位 + 显式代价文案)
|
||||
if not _tested_refresh_chain and main.ap_bar.is_refresh_ready():
|
||||
_tested_refresh_chain = true
|
||||
var rap_before: int = int(main.b1.a3.rap)
|
||||
var board_before = main.b1.a2.current_board()
|
||||
var rap_before: int = int(main.settlement.action_economy.rap)
|
||||
var board_before = main.settlement.minesweeper.current_board()
|
||||
main.ap_bar.press_refresh_for_probe()
|
||||
if not main.ap_bar.confirm_visible():
|
||||
_problems.append("弃图刷新确认 chips 未出现(异位确认缺失)")
|
||||
main.ap_bar.press_confirm_for_probe()
|
||||
if int(main.b1.a3.rap) != rap_before - 8:
|
||||
_problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.b1.a3.rap)])
|
||||
if main.b1.a3.phase != "Active":
|
||||
_problems.append("轮内刷新后相位丢失(%s)" % main.b1.a3.phase)
|
||||
if main.b1.a2.current_board() == board_before:
|
||||
if int(main.settlement.action_economy.rap) != rap_before - 8:
|
||||
_problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.settlement.action_economy.rap)])
|
||||
if main.settlement.action_economy.phase != "Active":
|
||||
_problems.append("轮内刷新后相位丢失(%s)" % main.settlement.action_economy.phase)
|
||||
if main.settlement.minesweeper.current_board() == board_before:
|
||||
_problems.append("轮内刷新未当场换图")
|
||||
if main._refresh_used_round != 1:
|
||||
_problems.append("刷新轮内计数错误(%d)" % main._refresh_used_round)
|
||||
@@ -187,13 +187,13 @@ func _assert_window_state_once() -> void:
|
||||
|
||||
|
||||
func _play_one_action() -> void:
|
||||
var b1 = main.b1
|
||||
if b1 == null:
|
||||
return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 b1 可能已被置空(基线既有噪声——加固)
|
||||
var a2 = b1.a2
|
||||
var settlement = main.settlement
|
||||
if settlement == null:
|
||||
return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 settlement 可能已被置空(基线既有噪声——加固)
|
||||
var minesweeper = settlement.minesweeper
|
||||
# 行为①:随机插旗(右键——未开相邻格),约 20% 轮次
|
||||
if _ap_spent_this_round == 0 and _rand.randf() < 0.2:
|
||||
var target := _random_unknown_adjacent(a2)
|
||||
var target := _random_unknown_adjacent(minesweeper)
|
||||
if target.x >= 0:
|
||||
main.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
|
||||
_flags_placed.append(target)
|
||||
@@ -202,33 +202,33 @@ func _play_one_action() -> void:
|
||||
# 行为②:随机拔旗(右键已插旗格),约 10% 轮次
|
||||
if _rand.randf() < 0.1 and not _flags_placed.is_empty():
|
||||
var fc: Vector2i = _flags_placed.pop_back()
|
||||
if a2.has_flag(fc) and not a2.is_revealed(fc):
|
||||
if minesweeper.has_flag(fc) and not minesweeper.is_revealed(fc):
|
||||
main.board_view.click_cell(fc, MOUSE_BUTTON_RIGHT)
|
||||
_ap_spent_this_round += 1
|
||||
return
|
||||
# 行为③:随机无效点击(非相邻格/已开格),约 8% 轮次
|
||||
if _rand.randf() < 0.08:
|
||||
main.board_view.click_cell(Vector2i(_rand.randi_range(0, a2.current_board().width - 1), _rand.randi_range(0, a2.current_board().height - 1)), MOUSE_BUTTON_LEFT)
|
||||
main.board_view.click_cell(Vector2i(_rand.randi_range(0, minesweeper.current_board().width - 1), _rand.randi_range(0, minesweeper.current_board().height - 1)), MOUSE_BUTTON_LEFT)
|
||||
return
|
||||
# 挖掘主策略(最近可挖格)
|
||||
var plan = AutoPlayer.choose_dig(a2) # O4 收口:策略单一实现
|
||||
var plan = AutoPlayer.choose_dig(minesweeper) # O4 收口:策略单一实现
|
||||
if not plan.is_empty():
|
||||
if plan.hero_cell != a2.hero_pos():
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
main.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
|
||||
main.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT)
|
||||
_ap_spent_this_round += 1
|
||||
# 行为④:提前停手(已花 ≥3 AP 时 15%)——停手经 AP 条按钮真实信号路径
|
||||
if _ap_spent_this_round >= 3 and b1.a3.phase == "Active" and _rand.randf() < 0.15:
|
||||
if _ap_spent_this_round >= 3 and settlement.action_economy.phase == "Active" and _rand.randf() < 0.15:
|
||||
_click_stop()
|
||||
elif b1.a3.phase == "Active":
|
||||
elif settlement.action_economy.phase == "Active":
|
||||
_click_stop()
|
||||
|
||||
|
||||
func _answer_window() -> void:
|
||||
# 行为⑥:选用首件可用道具 / 跳过(选择语义 = 窗口条目位置——与 UI 契约一致,缺陷④回归载体)
|
||||
if _rand.randf() < 0.5 and main.b1 != null:
|
||||
var b1 = main.b1
|
||||
var s: Dictionary = b1.presentation_snapshot() # E1 收口:经呈现快照面读库存/英雄
|
||||
if _rand.randf() < 0.5 and main.settlement != null:
|
||||
var settlement = main.settlement
|
||||
var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读库存/英雄
|
||||
var pos := -1
|
||||
for inst in s.inventory:
|
||||
if inst.status != "InStock":
|
||||
@@ -237,11 +237,11 @@ func _answer_window() -> void:
|
||||
var usable: bool = int(s.hero.hp) < int(s.hero.max_hp) or inst.category == "damage_item"
|
||||
if usable:
|
||||
var inv_index: int = int(inst.index)
|
||||
b1.submit_window_selection([pos])
|
||||
settlement.submit_window_selection([pos])
|
||||
main.inventory_panel.close_window()
|
||||
# 缺陷④回归断言:被选件必须真实消耗(Used),不得误耗邻件/留存
|
||||
var after: Array = b1.presentation_snapshot().inventory
|
||||
if not b1.is_battle_live():
|
||||
var after: Array = settlement.presentation_snapshot().inventory
|
||||
if not settlement.is_battle_live():
|
||||
# 终局结算窗口:选用后战斗立即结束、level_clear 合法清库——Used 断言不适用
|
||||
_used_ok += 1
|
||||
elif inv_index < after.size() and String(after[inv_index].status) == "Used":
|
||||
@@ -258,20 +258,20 @@ func _answer_window() -> void:
|
||||
|
||||
|
||||
func _skip_window() -> void:
|
||||
if main.b1 != null:
|
||||
main.b1.submit_window_selection([])
|
||||
if main.settlement != null:
|
||||
main.settlement.submit_window_selection([])
|
||||
main.inventory_panel.close_window()
|
||||
|
||||
|
||||
func _random_unknown_adjacent(a2) -> Vector2i:
|
||||
var hero: Vector2i = a2.hero_pos()
|
||||
func _random_unknown_adjacent(minesweeper) -> Vector2i:
|
||||
var hero: Vector2i = minesweeper.hero_pos()
|
||||
var cands: Array = []
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
if dx == 0 and dy == 0:
|
||||
continue
|
||||
var c: Vector2i = hero + Vector2i(dx, dy)
|
||||
if a2.current_board().in_bounds(c) and not a2.is_revealed(c) and not a2.has_flag(c):
|
||||
if minesweeper.current_board().in_bounds(c) and not minesweeper.is_revealed(c) and not minesweeper.has_flag(c):
|
||||
cands.append(c)
|
||||
if cands.is_empty():
|
||||
return Vector2i(-1, -1)
|
||||
@@ -279,7 +279,7 @@ func _random_unknown_adjacent(a2) -> Vector2i:
|
||||
|
||||
|
||||
func _click_stop() -> void:
|
||||
# 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——A2 D1×6① 回归载体)
|
||||
# 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
|
||||
main.ap_bar._btn_stop.pressed.emit()
|
||||
|
||||
|
||||
@@ -291,22 +291,22 @@ func _report_stall(st: Dictionary) -> void:
|
||||
why = "窗口悬置未被消费(面板窗口态=%s)" % str(main.inventory_panel.is_window_open())
|
||||
elif st.phase == "Active" and st.ap == 0:
|
||||
why = "Active 相位 AP=0(应在归零瞬间翻 Waiting)"
|
||||
elif st.phase == "Idle" and main.b1.battle_result == "":
|
||||
elif st.phase == "Idle" and main.settlement.battle_result == "":
|
||||
why = "Idle 相位但战斗未结束(begin 失败?)"
|
||||
else:
|
||||
why = "状态无推进(battle_result='%s')" % str(main.b1.battle_result)
|
||||
why = "状态无推进(battle_result='%s')" % str(main.settlement.battle_result)
|
||||
print("[click_probe] 🔴 卡死复现(PROBE_SEED=%s):%s" % [OS.get_environment("PROBE_SEED"), why])
|
||||
_dump_state()
|
||||
get_tree().quit(2)
|
||||
|
||||
|
||||
func _dump_state() -> void:
|
||||
var diag: Dictionary = main.b1.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
|
||||
var diag: Dictionary = main.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
|
||||
print(" node_index=%s round=%s phase=%s ap=%s rap=%s" % [
|
||||
str(main.runner.node_index), str(main.b1.round_index),
|
||||
str(main.b1.a3.phase), str(main.b1.a3.ap), str(main.b1.a3.rap)])
|
||||
str(main.runner.node_index), str(main.settlement.round_index),
|
||||
str(main.settlement.action_economy.phase), str(main.settlement.action_economy.ap), str(main.settlement.action_economy.rap)])
|
||||
print(" battle_result='%s' settling=%s awaiting_window=%s window_visible=%s window_open=%s" % [
|
||||
str(diag.battle_result), str(diag.settling), str(diag.awaiting_window),
|
||||
str(main.inventory_panel._window_mode), str(main.inventory_panel.is_window_open())])
|
||||
print(" board_installed=%s facts_path='%s' refresh_used=%s flags=%s" % [
|
||||
str(diag.a2_board_installed), str(diag.facts_path), str(diag.refresh_used_waiting), str(_flags_placed)])
|
||||
str(diag.minesweeper_board_installed), str(diag.facts_path), str(diag.refresh_used_round), str(_flags_placed)])
|
||||
@@ -25,7 +25,7 @@ func _ready() -> void:
|
||||
problems.append("节点进度条未构建(%d 子节点)" % main.strip_view.get_child_count())
|
||||
if main.info_panel.hp_text().begins_with("HP —") or main.info_panel.hp_text().contains("—"):
|
||||
problems.append("C 区血量未显示(%s)" % main.info_panel.hp_text())
|
||||
if main.b1 == null or main.b1.battle_result != "":
|
||||
if main.settlement == null or main.settlement.battle_result != "":
|
||||
problems.append("首战未处于进行中")
|
||||
if not main.log_view.parsed_text().contains("节点 1/"):
|
||||
problems.append("节点头日志缺失")
|
||||
@@ -46,7 +46,7 @@ func _ready() -> void:
|
||||
problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.log_view.mode(), str(main.log_view.custom_minimum_size.y)])
|
||||
if main.inventory_panel._title.text != "道包 · 关卡域":
|
||||
problems.append("E 区库存常读标题错误(%s)" % main.inventory_panel._title.text)
|
||||
# ---- 敌人卡投影(顺位/意图徽标常驻——B2×4)----
|
||||
# ---- 敌人卡投影(顺位/意图徽标常驻——敌人系统×4)----
|
||||
var card = main.stage.enemy_box.get_child(0)
|
||||
if card != null and card._name_label.text == "":
|
||||
problems.append("敌人卡名称为空")
|
||||
@@ -75,7 +75,29 @@ func _ready() -> void:
|
||||
slots_row.set_effects([])
|
||||
if not slots_row._slots.is_empty():
|
||||
problems.append("BUG-0001 回归:空态未清槽(%d)" % slots_row._slots.size())
|
||||
# ---- L2 雾图变体段(A4:雾首遇一次性提示 / 雾格呈现 / 去饱和接口可用性)----
|
||||
# ---- 图标键回归(HUD框架 图标键契约:状态表六行全映射、奖励 category 五值全映射;
|
||||
# 查不到的键 fail-fast + ? 占位——此处钉死映射不残缺,未知键断言由 _icon_cfg/_badge_glyph 承担)----
|
||||
if not StatusSlotRow.EFFECT_ICON.has("enrage") or not StatusSlotRow.EFFECT_ICON.has("off_guard"):
|
||||
problems.append("状态图标表缺暴走/失衡两键(states 表六行须全映射)")
|
||||
for cat in ["instant_aoe", "instant_heal", "damage_item", "heal_pack", "buff"]:
|
||||
if not BoardView.REWARD_BADGE.has(cat):
|
||||
problems.append("奖励图标表缺键(%s)" % cat)
|
||||
var probe_kb: Array = [
|
||||
{"effect_id": "enrage", "display_name": "暴走", "layers": 1, "remaining_settlements": 1, "source_class": "flag_miss", "round_index": 1, "effective_value": 25},
|
||||
{"effect_id": "off_guard", "display_name": "失衡", "layers": 1, "remaining_settlements": 1, "source_class": "flag_miss", "round_index": 1, "effective_value": 25},
|
||||
]
|
||||
slots_row.set_effects(probe_kb)
|
||||
if slots_row._slots.size() != 2:
|
||||
problems.append("图标键回归:enrage/off_guard 未建槽(%d)" % slots_row._slots.size())
|
||||
else:
|
||||
var ic_enrage: Label = slots_row._slots[0].get_child(0).get_child(0)
|
||||
var ic_off: Label = slots_row._slots[1].get_child(0).get_child(0)
|
||||
if ic_enrage.text == "?" or ic_enrage.text == "":
|
||||
problems.append("暴走图标键未映射(icon=%s)" % ic_enrage.text)
|
||||
if ic_off.text == "?" or ic_off.text == "":
|
||||
problems.append("失衡图标键未映射(icon=%s)" % ic_off.text)
|
||||
slots_row.set_effects([])
|
||||
# ---- L2 雾图变体段(地形变异:雾首遇一次性提示 / 雾格呈现 / 去饱和接口可用性)----
|
||||
# 2026-09-13 单场用户入口已删——探针直调 debug_session 原语(battle_created/ready 仍经组合根接线;
|
||||
# _reset_run_scope 复现原 _start_single 的提示去重复位,保证 fog_first 断言前置态)
|
||||
main._reset_run_scope()
|
||||
@@ -85,7 +107,7 @@ func _ready() -> void:
|
||||
if main.board_view.board() == null:
|
||||
problems.append("L2 单场未启动(board=null)")
|
||||
elif main.board_view.board().fog_cells().is_empty():
|
||||
problems.append("L2 板无雾格(A4 变体缺失)")
|
||||
problems.append("L2 板无雾格(地形变异 变体缺失)")
|
||||
elif not bool(main._hints_shown.get("fog_first", false)):
|
||||
problems.append("雾首遇一次性提示未触发(fog_first)")
|
||||
else:
|
||||
|
||||
@@ -59,7 +59,7 @@ func _ready() -> void:
|
||||
main._start_full_level() # 完整关卡路径(_start_single 按语义清零滑条 = 恒基线,不承载加成断言)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var hero: Dictionary = main.b1.presentation_snapshot().hero
|
||||
var hero: Dictionary = main.settlement.presentation_snapshot().hero
|
||||
if int(hero.get("atk", -1)) != 12 or int(hero.get("max_hp", -1)) != 75:
|
||||
problems.append("战斗加成单次生效违例(atk=%s max_hp=%s,期望 12/75)" % [
|
||||
str(hero.get("atk", "?")), str(hero.get("max_hp", "?"))])
|
||||
|
||||
Reference in new issue
Block a user