Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b5a96b6c7 | ||
|
|
96ec80aa97 | ||
|
|
fe0aeb8f28 | ||
|
|
75780e31fa | ||
|
|
ea03765adc | ||
|
|
b9546e7b69 | ||
|
|
c91bbdddee | ||
|
|
7a2d19fd12 | ||
|
|
bee73db51b | ||
|
|
932edff505 | ||
|
|
23bc6cc243 | ||
|
|
df6cafcb31 | ||
|
|
2822307436 | ||
|
|
6a3203a8db | ||
|
|
dd2de10ed1 | ||
|
|
d32ff34256 | ||
|
|
4e04d2f1ba | ||
|
|
d1c1d3822a | ||
|
|
d2b36e59cb |
No files matched your search
@@ -16,7 +16,7 @@
|
||||
|
||||
## 运行与验证(工作目录 = 含 project.godot 的切片目录)
|
||||
|
||||
- **交付前验收 = 跑 `tools/run_all_gates.sh`,按本批改动域选档(`--code` 只跑代码域/`--docs` 只跑文档域/不带参数=两域全量),所选域全绿才合回 master**——它是全部验证闸的唯一入口与唯一清单,**域划分与选闸算法也只写在它这一个地方**,闸件增减与分域调整都只改它,文档禁另列清单。域按**文件性质**判:代码域=`game/` 下非 `.md` 的行为载体(代码/测试脚本/探针/场景/数据表/素材),`game/` 内 `.md` 与 `game/` 外一切路径=文档域;同批两类都有时跑全量档。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件(关联档=只跑与本批改动关联的那部分测试),00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降,横切面改动(公共件/数据表/autoload/数值·存档·RNG·结算触面)回落全部套件;算法与套件族映射表只在闸脚本内定义。所选域全绿即可合回;全量回归由用户自发发起,合入后无自动兜底。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 与 `scope` 字段),合回时由 worktree-guard 按 diff 文件性质推出该验哪个域再校验回执(六况拦截细则见 process-conventions §3.9);非代码面的直改批验收=commit 点 git 钩子自动跑 `--docs`,无 worktree 与回执流程
|
||||
- **合回验证 = 本地 CI(2026-10-30 用户裁)**:git merge 合回 master 落地后,post-merge 钩子(`tools/hooks/post-merge`)后台触发 `tools/ci_runner.sh` 跑一次全量域闸(调 `tools/run_all_gates.sh` 不带参数档,02 全套件),结果落 `game/tests/.gates/ci-status.json`(commit/绿红/红闸名单+关键日志行/耗时)+`ci-history.jsonl` 流水账;CI 红态下每个修复提交经 post-commit 钩子(`tools/hooks/post-commit`)自动追绿复验,绿态与无状态零触发——主区小批直提不重复跑全量,同一份字节禁止第二次人手跑。CI 红不回滚合并,红项在案、修复提交追绿;开工先读 CI 状态,红未修不开新批。跑批起手自动 `--editor --quit` 刷类名缓存(.godot 缓存不入 git,防合并后失配假红)。`tools/run_all_gates.sh` 仍是全部验证闸的**唯一入口与唯一清单,域划分与选闸算法只写在它这一个地方**,闸件增减与分域调整都只改它,文档禁另列清单;`--code`/`--docs`/`--quick` 手动档只服务开发期按域跑批(开发期窄测纪律见 process-conventions §6),不构成验收;全量回归由用户自发发起
|
||||
- 单件直跑(调试期最窄确认用,**不构成验收**):冒烟测试 `python tools/run_smoke.py`(`game/tests/smoke_boot.tscn` 极小启动场景)· 系统测试 `python tools/run_system_test.py`(宿主 `game/tests/system_test.gd`)· 场景探针 `…console.exe --headless --path . res://probe/ui_probe.tscn` · 单元与集成 `… --script res://tests/run_all.gd`
|
||||
- 调参批量:`--script res://sim/adventure_monte_carlo.gd`(全冒险期望)· `res://sim/reward_expectation.gd`(奖励期望);关卡完成率/难度类测量一律走全冒险
|
||||
- 数值验证:`python tools/balance_verifier.py`(采集端 `res://sim/verifier_batch.gd`,报告与原始数据落 `sim-data/`)
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
| 时机 | 权威文档 |
|
||||
|---|---|
|
||||
| 批次开工 / 流程纪律 | `design/engineering/process-conventions.md`——车道铁律(代码面 worktree 批/其余主区直改)、五段流水线、批次清单、测试两入口与 MC/闸机细则、像素纪律 |
|
||||
| 批次开工 / 流程纪律 | `design/engineering/process-conventions.md`——车道铁律(主区直改+直提;大批约定走 worktree)、合回后本地 CI、五段流水线、批次清单、测试两入口与 MC/闸机细则、像素纪律 |
|
||||
| 流水线段入口 | 技能对照——doc-design=文档设计(兼任反哺点落文)/asset-ingest=美术录入/req-card=写卡/iter-plan=迭代编排/iter=实现(五者均装于项目 `.zcode/skills/`);登记不立技能;批后收尾与中间文档退役=batch-closure(同一目录);格式与状态机权威=`design/engineering/doc-conventions.md` §12 |
|
||||
| 定代码方案 / 写代码 / 改代码前 | `design/engineering/code-conventions.md`——结构/契约/语言/数值/流程五类硬纪律(反例库 = 同目录 code-conventions-cases.md) |
|
||||
| 定测试方案 / 写测试 / 改测试前 | `design/engineering/test-conventions.md`——探针标配 / 回归断言 / 提交前验证 / 验收清单落码 |
|
||||
@@ -85,11 +85,11 @@
|
||||
|
||||
## 硬性红线(勿违反;操作细则在 process-conventions.md 与各规约)
|
||||
|
||||
- 车道铁律(判据与改动域划分同一,按文件性质切)——代码面(`game/` 下非 `.md` 的行为载体)唯一强制独立 worktree(迭代批全套仪式不变);其余一切面(含 `game/` 内 `.md` 与 `.zcode/`、`.qoder/` 等不入 git 面)主工作区直改+直提,commit 点由 git pre-commit 钩子(`tools/hooks/pre-commit`,`core.hooksPath` 挂载)现场跑文档域闸,绿落地红拦;主区直提代码面被该钩子拦;`--no-verify` 仅属用户,agent 禁用(细则见 process-conventions §1)
|
||||
- 车道铁律(判据与改动域划分同一,按文件性质切;2026-10-30 用户裁本地 CI 化)——一切面(含代码面 `game/` 下非 `.md` 的行为载体与 `.zcode/`、`.qoder/` 等不入 git 面)主工作区直改+直提;commit 点由 git pre-commit 钩子(`tools/hooks/pre-commit`,`core.hooksPath` 挂载)现场跑文档域闸,绿落地红拦,合并收尾提交跳过(归合回后 CI);代码面全量验证归合回后本地 CI(`tools/hooks/post-merge` 触发,见「运行与验证」首条);大批次(迭代实现批等)按约定仍走独立 worktree(冻结→件核→并 master→重塑→主区合回→CI 验证),约定非强制;`--no-verify` 仅属用户,agent 禁用(细则见 process-conventions §1)
|
||||
- **仓库根禁止设会话状态文件**(`active.md` 已废除)——现状看 roadmap「当前位」,待办看待办清单,过程看 git 提交信息,产出物看 `reports/`;任何文档禁止另设「当前状态快照」节,两处记同一件事必然漂移(细则见 doc-conventions 第 10 节)
|
||||
- 测试执行两入口铁规——只有迭代 loop 开发与用户点名两个触发入口;其余会话零测试,需测试支撑先请示并注明未跑测
|
||||
- 交付前验收 = 跑 `tools/run_all_gates.sh` **按改动域**全绿(唯一入口、唯一闸清单、唯一域划分与选闸算法,禁在文档另列);代码域按**关联档选测**——02 单元与集成跑「卡片申报 ∪ diff 推导」并集套件、三件保险(冒烟/系统测试/静态扫描)恒跑、横切面回落全量,所选域绿即可合回,全量回归由用户自发(2026-10-29 用户裁,合入后无自动兜底)(算法见闸脚本);合回 master 由 worktree-guard 按 diff 文件性质推域校验回执强制(process-conventions §3);非代码面直改批的验收=commit 点 git 钩子自动跑 `--docs`(process-conventions §1)
|
||||
- **改动四类**——**需求改动**(游戏代码,含随逻辑写/改的测试代码):唯一走「写需求卡片+编入迭代批次」通道;迭代实现批禁写 `design/` 整个目录、两份根流程清单(`待办清单.md`/`体验验证清单.md`)与 `game/tests/README.md`(能力清单,归写卡段维护)(含新增/修改/删除),设计文档对它只读、是写测试与实现代码的输入依据;两份流程清单的条目状态只由流水线各段推进,实现批禁止顺手改;产出物只准写 `reports/`;批末只跑代码域闸。**工程改动**(tools/、闸件、模拟测量工具、纯测试维护——不改变游戏行为的配套代码)、**文档设计改动**、**美术素材改动**:不写需求卡片、不编迭代批次,登记在案后由用户发令在主工作区直改+直提(commit 点 git 钩子自动跑文档域闸),完成即删条目;触及代码面的工程改动(如纯测试维护)仍走 worktree 批;文档设计类写入走 doc-design 技能(登记条目格式由流水线闸强制)
|
||||
- 合回后验证 = 本地 CI 全量域闸全绿(唯一强制验证点;唯一入口、唯一闸清单、唯一域划分与选闸算法仍只在 `tools/run_all_gates.sh`,禁在文档另列;触发与追绿语义见「运行与验证」首条与 process-conventions §1)。开发期手动跑批按需选档(`--code` 关联档选测——02 跑「卡片申报 ∪ diff 推导」并集套件、三件保险恒跑、横切面回落全量——算法只在闸脚本内定义),手动跑不构成验收;全量回归由用户自发发起。文档面直改批验收=commit 点 git 钩子自动跑 `--docs`(process-conventions §1)
|
||||
- **改动四类**——**需求改动**(游戏代码,含随逻辑写/改的测试代码):唯一走「写需求卡片+编入迭代批次」通道;迭代实现批禁写 `design/` 整个目录、两份根流程清单(`待办清单.md`/`体验验证清单.md`)与 `game/tests/README.md`(能力清单,归写卡段维护)(含新增/修改/删除),设计文档对它只读、是写测试与实现代码的输入依据;两份流程清单的条目状态只由流水线各段推进,实现批禁止顺手改;产出物只准写 `reports/`;合回后由本地 CI 全量验证(红项修复提交追绿,process-conventions §1)。**工程改动**(tools/、闸件、模拟测量工具、纯测试维护——不改变游戏行为的配套代码)、**文档设计改动**、**美术素材改动**:不写需求卡片、不编迭代批次,登记在案后由用户发令在主工作区直改+直提(commit 点 git 钩子自动跑文档域闸),完成即删条目;大体量改动(含触及代码面的工程改动如纯测试维护)按约定走 worktree 批(非强制);文档设计类写入走 doc-design 技能(登记条目格式由流水线闸强制)
|
||||
- 五段流水线铁律——**需求改动**(游戏代码)按「登记 → 文档设计/美术设计 → 写需求卡片 → 迭代编排 → 实现」五段推进,段间不自动流转、每段转进由用户发令;编入迭代的卡片必须设计闭合+零待裁点——需求卡片任何工作量皆可写、皆可编入迭代,工作量限制只属迭代层(迭代估时总量 <5 个工作日=「冻结待凑批」、不可开工);流水线闸 `tools/pipeline_gate.py` 强制(已注册进 run_all_gates.sh 文档域);实现段零裁断——卡片没写清=上游缺陷,停该卡退回上游,禁内联求裁(process-conventions §2,格式与状态机=doc-conventions §12)。工程/文档设计/美术素材三类改动不走写卡与编排段:登记 →(如需设计讨论)→ 用户发令直改 → 主区直改+commit 点闸 → 完成删条目(车道细则=process-conventions §1)
|
||||
- MC/模拟:无数值改动零跑;档位硬闸见 `sim-data/README.md`「MC 采集档位表」,一档只能由用户发起
|
||||
- 闸机轮次纪律:新码先孤立验证 → 出红只跑红件最小单元 → 长批先最小参数试车 → 开工扫坑册(process-conventions §6)
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
|
||||
## 1. 分支纪律(车道铁律)
|
||||
|
||||
改动按文件性质分两车道(2026-10-26 用户裁,取代 2026-09-19 立的「一切改动走 worktree」铁律;判据与 §3 第 8 条改动域划分同一):
|
||||
一切面主工作区直改+直提(2026-10-30 用户裁本地 CI 化:验证挪到合回后,主区直提不再拦任何面;判据与 §3 第 8 条改动域划分同一):
|
||||
|
||||
1. **worktree 批车道(唯一强制面=代码面)**:一切触及代码面(`game/` 下非 `.md` 的行为载体:代码、测试脚本、探针、场景、数据表、素材)的改动——迭代实现批与触及代码面的工程改动(如纯测试维护)——先建独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`;所有改动与验证都在 worktree 内进行(未跟踪文件不会出现在新 worktree,需手工搬运);建区后立刻跑 `tools/setup_worktree_userdir.sh`——给该区独立 Godot user 目录(测试夹具、试玩日志、截图产物不与其他区共用;主区不生成,真实档保持原位)。迭代批开工细则见 §3 与 iter 技能
|
||||
2. **worktree 批收尾**:批次收口顺序固定为**冻结 → 件核 → 并 master → 重塑 → 跑一次域闸 → 合回**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先 `git merge master` 解冲突(必须在跑闸之前,闸跑完再并=那次闸白跑);**重塑**=迭代分支(iter<N> 形)在跑闸前执行 `python tools/reshape_cards.py`,把分支线性史按卡末态快照压成一卡一笔(可选追加一笔纯文档收尾),master 台账由此每卡一行——工具自带「重塑前后树逐字节一致」断言与 backup 分支,不改任何文件内容;非迭代工程小批免重塑;然后按本批改动域跑**一次** `tools/run_all_gates.sh`(见 §3.9),全绿后在主工作区 `git merge --no-ff <任务分支>`(合回闸对迭代分支数卡校验,未归卡提交拦下),再 `git worktree remove`+删分支。闸后代码面不得再改一个字节(回执作废须重跑),只允许追加 `reports/`、`design/` 等文档件提交。游戏类测试(冒烟/单测/MC/探针)批内默认零执行(两入口铁规,见 §4),用户点名或当场问才跑
|
||||
3. **直改车道(代码面之外的一切面)**:`design/`、`reports/`、`tools/`、根规则与清单文件、`game/` 内 `.md`、`.zcode/`、`.qoder/`——主工作区直接改、直接提交,无 worktree、无冻结/件核/合并仪式。验收=commit 点 git pre-commit 钩子(`tools/hooks/pre-commit`,经 `core.hooksPath` 挂载,在位性由 run_all_gates.sh 跑批自检)现场跑 `tools/run_all_gates.sh --docs`(秒级文本扫描):绿即落地,红即拦下,修红后重新 commit 钩子自动重跑;跑完把闸输出呈报对话即完成义务。预期大体量改动(跨文档结构重构等)时由用户开工点名自愿开 worktree,非规则强制
|
||||
4. **执法分层(全机械,零自觉依赖)**:①git pre-commit 钩子——直改车道的闸机检,并对主工作区直提代码面的提交一律拦(代码面唯一合法路径=worktree 批合回);任务 worktree 内的代码面提交跳过(归批末闸+合回闸管,不打扰迭代批中间提交)。②`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:主工作区代码面的 Edit/Write 与 staged 含代码面的 commit 拦截、`git commit --no-verify` 拦截(该旗标仅属用户)、合回闸回执六况校验(§3.9)。git 层钩子对一切提交者生效(含用户手工提交,紧急出口 `--no-verify`);ZCode 层只约束 agent
|
||||
1. **主区直改车道(一切面)**:`game/`(含代码面=`game/` 下非 `.md` 的行为载体)、`design/`、`reports/`、`tools/`、根规则与清单文件、`.zcode/`、`.qoder/`——直接改、直接提交。验收分两层:commit 点 git pre-commit 钩子(`tools/hooks/pre-commit`,经 `core.hooksPath` 挂载,在位性由 run_all_gates.sh 跑批自检)现场跑 `tools/run_all_gates.sh --docs`(秒级文本扫描)绿即落地红即拦,合并收尾提交跳过(MERGE_HEAD 在,内容归合回后 CI);代码面的全量验证归合回后本地 CI(下条)。开发期纪律不变:卡内只测自己改的东西、出红只跑红件最小单元(§6)
|
||||
2. **合回后验证(本地 CI,2026-10-30 用户裁)**:git merge 合回 master 落地后 post-merge 钩子(`tools/hooks/post-merge`)后台触发 `tools/ci_runner.sh merge` 对 master 尖跑一次全量域闸——唯一强制全量验证点;CI 红态下每个修复提交经 post-commit 钩子(`tools/hooks/post-commit`)自动追绿复验,绿态与无状态零触发。重跑判据:同一份字节禁止第二次人手跑;有意义的重跑只剩「验证修复」一类。CI 红不回滚合并——红项记 `game/tests/.gates/ci-status.json`,修复提交追绿;开工先读 CI 状态,红未修不开新批。跑批起手 `--editor --quit` 刷类名缓存(在册坑:.godot 缓存不入 git,合并后失配假红假绿)——原「合并后零复跑」的秒级动作由跑批件接管。并跑语义=跑完补跑(coalesce):跑批期间新触发不中断当前轮,轮尾对最新尖端补跑,绝不逐笔各跑一轮
|
||||
3. **worktree 批车道(大批约定,非强制)**:迭代实现批与大体量改动按约定仍走独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`;建区后立刻跑 `tools/setup_worktree_userdir.sh`——给该区独立 Godot user 目录(测试夹具、试玩日志、截图产物不与其他区共用;主区不生成,真实档保持原位)。收口顺序固定为**冻结 → 件核 → 并 master → 重塑 → 主区合回 → CI 验证**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先 `git merge master` 解冲突;**重塑**=迭代分支(iter<N> 形)合回前执行 `python tools/reshape_cards.py`,把分支线性史按卡末态快照压成一卡一笔(可选追加一笔纯文档收尾),master 台账由此每卡一行——工具自带「重塑前后树逐字节一致」断言与 backup 分支,不改任何文件内容;非迭代工程小批免重塑;然后主工作区 `git merge --no-ff <任务分支>`(合回守卫对迭代分支数卡校验,未归卡提交拦下),合并落地 post-merge 钩子触发 CI,红则修复提交(主区直提)追绿,全绿后 `git worktree remove`+删分支。游戏类测试(冒烟/单测/MC/探针)批内默认零执行(两入口铁规,见 §4),用户点名或当场问才跑
|
||||
4. **执法分层(全机械,零自觉依赖)**:①git pre-commit 钩子——一切提交的文档域闸机检(合并收尾提交豁免)。②post-merge/post-commit 钩子——本地 CI 触发(仅主工作区;任务 worktree 内的合并不触发)。③`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:`git commit --no-verify` 拦截(该旗标仅属用户)、iter<N> 分支合回的一卡一提交数卡校验、规则闸(写 design//game/ 前核对会话规约读取记录)。git 层钩子对一切提交者生效(含用户手工提交,紧急出口 `--no-verify`);ZCode 层只约束 agent
|
||||
|
||||
## 2. 五段流水线
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补
|
||||
|
||||
**完成**
|
||||
8. **批末按域验收才合并(worktree 批;直改车道无批末动作,验收在 commit 点由 git 钩子执行,见 §1 第 3 条)**——在任务 worktree 内跑 `tools/run_all_gates.sh`,**按本批改动域选档**:按文件性质判——代码面=`game/` 下非 `.md` 的行为载体(代码、测试脚本、探针、场景、数据表、素材)→ `--code`;文档件=`game/` 内 `.md`(README、生成清单)与 `game/` 外一切路径(`design/`、根规则文件、`tools/` 工具脚本、`.zcode/` 配置等)→ `--docs`;两类同批都有 → 全量档不带参数。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件——申报=在册需求卡片对照表「验收测试」列提到的测试文件名,推导=闸脚本内套件族映射表按改动文件归族;改动命中公共件/数据表/autoload/数值·存档·RNG·结算触面或映射不到 → 横切回落全部套件;00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降;03 场景探针与 05 渲染闸维持影响面口径(本批 game/ 改动全在测量面 game/tests、game/sim 时记 SKIP)。所选域全绿即可合回 master,全量回归由用户自发发起(合入后无自动兜底)。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。该脚本是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain`、`scope`(full|related,02 跑子集=related)与 `dirty_game` 字段),合回时由 worktree-guard 按 diff 文件性质推域强制校验(六况拦截:缺回执/quick 模式/回执未覆盖分支尖=回执 commit 之后代码面又有改动、或跑闸时代码面带着未提交改动(`dirty_game`)/域不覆盖本次改动/含新增红/master HEAD 非回执 commit 祖先=合并将产出未测内容;guard 只验域覆盖与全绿,不复核选套件清单)。回执与分支尖的对齐判据是**代码面**不是 commit 号:回执之后只追加文档件(`game/` 内 `.md`、`reports/`、`design/` 等)提交,回执照样有效,不构成复跑理由。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占合回全量——其质量义务=落批时人工确认闸可跑通且判据口径未变,闸真崩溃时合回闸 fail-closed(跑不出回执即拦),不以合回全量为兜底
|
||||
9. **合并后主区零复跑**(2026-10-24 用户裁:合回重跑验的是与回执同一份字节,零信息)——合回落地后 code 批只做两件秒级事:`--editor --quit` 重刷类名缓存(在册坑:.godot 缓存不入 git,与合并后代码失配会假红假绿)+冒烟启动确认可开机(合计约 1 分钟);docs 批零动作。全量验证**每批跑一次**=worktree 批末按域闸,顺序是「先并入 master、再跑闸」(§1 第 2 条);只有两种情形允许把这次跑作废重来——① 闸红且修法动了 `game/`,② 闸已开跑后 master 又被推进(合并将产出未测内容,由合回闸祖先判定拦下,§3.9 第六况)。「跑都跑了再跑一遍保险」「闸后顺手改点 `game/` 再补一跑」都不构成复跑理由;master 上永不跑全量
|
||||
8. **合回后 CI 验证(worktree 批;主区直改小批的验收在 commit 点由 git 钩子执行,见 §1 第 1 条)**——主工作区 `git merge --no-ff <任务分支>` 落地后 post-merge 钩子自动跑全量域闸(不带参数档,02 全套件),判定与红项落 `game/tests/.gates/ci-status.json`。全量是唯一强制档:跨卡触面、探针与未申报套件的消费漏网只有全量闸能抓(迭代 44 实证),关联档选测的盲区由它兜底。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。该脚本是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。开发期手动跑批按需选档(按文件性质判域:代码面→`--code`,文档件→`--docs`,两类都有→全量;`--code` 关联档选测——02 只跑「需求卡片申报 ∪ diff 推导」并集套件、00 冒烟/01 系统测试/09 静态扫描三件保险恒跑、横切面回落全量——算法见闸脚本),手动跑不构成验收。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占 CI 全量——其质量义务=落批时人工确认闸可跑通且判据口径未变;闸真崩溃时 CI 判红在案(跑不出回执=verdict 红,fail-closed 语义保留)
|
||||
9. **重跑语义(2026-10-30 用户裁,取代原「每批一次/合并后零复跑」口径)**——同一份字节禁止第二次人手跑;允许的重跑只剩两类:① CI 红后的修复提交(post-commit 钩子自动追绿复验,每笔修复至多触发一轮);② 用户自发发起的全量回归。「跑都跑了再跑一遍保险」「顺手改点再补一跑」都不构成重跑理由。类名缓存刷新与开机确认由 CI 跑批起手接管(§1 第 2 条),合并后无手工动作
|
||||
10. **批次完成后的登记义务**——合回 master 后按 doc-conventions §12.1 推进待办清单条目状态(做完整条删除),涉及迭代编排的在 roadmap 迭代总表与顶部「当前位」节换代——执行入口=项目技能 batch-closure(批后收尾与中间文档退役),跨会话教训写记忆库。批次过程与结论由 git 提交信息承载,禁止再写会话状态文件(见 doc-conventions 第 10 节)。`design/` 与根规则文件(`AGENTS.md`/`roadmap.md`/`待办清单.md`/`体验验证清单.md`)实现批禁写,见 §12
|
||||
11. **文档同步闸(实现批禁写文档)**——实现批**禁写 `design/` 整个目录与根规则文件**,**另禁写 `game/tests/README.md`(能力清单,归写卡段维护,test-conventions §8.9)**,要往外交代的东西一律落 `reports/`:实现报告 `reports/iter<N>-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter<N>-反哺点-<日期>.md`(每条写清哪个文件哪一节、改成什么、依据是什么)。设计权威文档(GDD/art-bible)**任何批都不直改**:用户在会话中逐点裁决后,由工程/文档设计改动批(直改类)写入 `design/`(执行入口=doc-design 技能),并把反哺条目从 `reports/` 清掉(吸收即死;退役执行=batch-closure 技能)。机械面文档(roadmap 行/术语表/README 工具注)同样归工程/文档设计改动批在合并前更新。反哺点未落地期间相关 GDD 节视为待反哺,读前先查 `reports/` 与待办清单;需求卡片必列「关联文档同步面」按两层标注
|
||||
12. **实机目检归用户,只登记不阻塞**——真人检测项登记进实现报告目检总清单与体验验证清单即完成批内义务;禁把等目检当停工理由或完成前置
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
**MC/verifier 触发细则**:仅在数值验证场景触发(改数值前后取证);normal 档为主、perfect 按需;**无数值改动的会话禁跑 MC**;结构位移类「存档锚」须先向用户请示定性再跑;需求卡片预设步骤的前提消失时(如「带值裁定后重锚」实际未投值)停问勿照字面执行。
|
||||
|
||||
**分级周期口径**:全部验证闸收编在 `tools/run_all_gates.sh` 一个入口下(**脚本即清单,且即域划分的唯一出处**),分三档——`--code`/`--docs` 按本批改动域只跑该域闸(闸件清单与域划分只写在脚本头部注释,本文不重列);`--quick` 是全量档的中段自检形态(跳过渲染闸与静态扫描);**不带参数=两域全量**,用于同一批既有代码面又有文档件改动。完成出库按所选域全绿,回执由合回闸按 diff 文件性质推域强制校验。真人实机目检归用户不进自动闸。
|
||||
**分级周期口径**:全部验证闸收编在 `tools/run_all_gates.sh` 一个入口下(**脚本即清单,且即域划分的唯一出处**),分三档——`--code`/`--docs` 按本批改动域只跑该域闸(闸件清单与域划分只写在脚本头部注释,本文不重列);`--quick` 是全量档的中段自检形态(跳过渲染闸与静态扫描);**不带参数=两域全量**。强制验证点=合回后本地 CI 的全量档(§1 第 2 条);分档只服务开发期手动跑批。真人实机目检归用户不进自动闸。
|
||||
|
||||
## 5. MC 采集档位细则
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
每个缺陷修复必须在探针或冒烟中留下能抓住该缺陷的断言(例:封印动作耗尽 AP → 必须进入第 2 轮;
|
||||
选用道具 → 库存态必须变 Used)。没有回归断言的修复视为未完成。
|
||||
|
||||
## 4. 提交前验证
|
||||
## 4. 验证时机(合回后本地 CI 制,2026-10-30 换代)
|
||||
|
||||
交付前跑 `tools/run_all_gates.sh`(不带 `--quick`),按改动域选档、所选域全绿——该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。闸件增减只改脚本。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件,00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降,横切面改动(公共件/数据表/autoload/数值·存档·RNG·结算触面)回落全部套件;所选域全绿即可合回,全量回归由用户自发发起(合入后无自动兜底)。
|
||||
开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,process-conventions §6);主区小改孤跑验证后直提。全量验证归合回后的本地 CI:git merge 合回 master 触发 post-merge 钩子跑 `tools/run_all_gates.sh` 不带参数档(两域全量,02 全套件——跨卡触面与关联档选测的盲区由全量兜底),结果落 `game/tests/.gates/ci-status.json`;CI 红态下修复提交经 post-commit 钩子自动追绿。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。
|
||||
行为探针使用随机化玩家行为(封印/停手/刷新/无效点击)——自动玩家不做的动作就是测试盲区。
|
||||
|
||||
## 5. 测试函数怎么写——验收清单条目的落码规范
|
||||
|
||||
+55
-231
@@ -27,19 +27,14 @@ var levels_total := 0 # 序列总关数(读 level 表行
|
||||
var master_seed := 0 # 本局主种子(摘要第五件事)
|
||||
var summary := {} # 最近一次结算的冒险摘要(idle 后保留——重开才新生成)
|
||||
var _runner: LevelRunner # 当前关卡行进器(每关新建——关卡域状态天然隔离)
|
||||
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
|
||||
var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面)
|
||||
var _last_treasure_cleared := 0 # additive(最近一次关卡末清空计数,summary 取数面)
|
||||
var _treasure_unlocked := {} # aux 解锁门控集(params.treasure_unlocked 接收面)
|
||||
var _attunement: Array[Variant] = [] # 同调装配集(params.attunement 接收面,start_level 外下传 LevelRunner)
|
||||
var _save_manager = null # 断点承载注入(null = 不写断点;模拟/冒烟缺省)
|
||||
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
|
||||
var _loadout_seeded := false # 同调行已种标记(每冒险一次——首关 bind 时点投行;行跨关存活后续关零动作)
|
||||
# ---- 三选一停点(编排迁 CurioStop 件——停点三态/刷新三源/凭证出箱/信号面)----
|
||||
var _curio: CurioDeck = null # 令牌池件(_init 建;纯池——多重集/抽签/cap/凭证判定唯一宿主)
|
||||
var _stop: CurioStop = null # 停点件(_init 建;每冒险一次——关卡换实例不换件)
|
||||
var _refresh_bank: RefreshBank = null # 刷新账件(_init 建——三源账与付费报价;宿主预算注入与断点片段直收)
|
||||
var _loadout: AttuneLoadout = null # 同调配装件(_init 建;充能单源化——物化行唯一事实源)
|
||||
var _bp_store: BreakpointStore # 断点组装与回放件(_init 建;六成员+27 键登记单源+读写侧机组随件——宿主读写经件)
|
||||
var adventure_inventory: TreasureItems # 冒险级秘宝库存(单源化——同调未用行跨关存活的承载面;关卡域语义归 level 使用面)
|
||||
var _tree_rules: Array[Variant] = [] # 树 rule 键集(Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面)
|
||||
var _shop_mode := "legacy" # 商店模式("legacy"=固定三件加定价〔off 对照承载,缺省零扰动〕/"pool"=共享池抽 3 买 1 重抽;真实入口 main.gd 显式投 pool)
|
||||
@@ -57,6 +52,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
_snapshot_breakpoint)
|
||||
_loadout = AttuneLoadout.new(db, _attunement)
|
||||
adventure_inventory = TreasureItems.new(db)
|
||||
_bp_store = BreakpointStore.new({"db": db, "rng": rng, "stats": _stats, "wallet": _wallet,
|
||||
"journal": _journal, "curio": _curio, "stop": _stop, "bank": _refresh_bank, "loadout": _loadout})
|
||||
|
||||
|
||||
# 件出口(组件交出非委托壳——呈现/测量面取主角账三件与令牌池单点;领域拆分批接口块)
|
||||
@@ -107,7 +104,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
_reset_curio_state()
|
||||
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
|
||||
level_index = 1
|
||||
_pending_next_level = false
|
||||
_bp_store._pending_next_level = false
|
||||
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 缺省 legacy(off 对照零扰动)
|
||||
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
|
||||
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
|
||||
@@ -115,7 +112,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
_refresh_bank.set_refresh_y(maxi(0, int(params.get("attune_refresh_y", 0)))) # additive(Y 冒险周期池)
|
||||
_loadout.set_attunement(_attunement) # 装配集注入(活引用——种行面)
|
||||
adventure_inventory.reset() # 冒险级库存重置(新局空库存)
|
||||
_loadout_seeded = false # 同调行投行延至首关 bind 时点(库存就位后)
|
||||
_bp_store._loadout_seeded = false # 同调行投行延至首关 bind 时点(库存就位后)
|
||||
adventure_counters = {} # 冒险桶清零(效果引擎 adv_ 计数器宿主——冒险开始归零)
|
||||
# 刷新练习开局注入(refresh_stock_grant 装配即得一次性加仓:
|
||||
# 装配集 kind 分发扫描;只入存量账、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕)
|
||||
@@ -152,99 +149,50 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
|
||||
return false
|
||||
master_seed = int(bp.get("master_seed", 0))
|
||||
rng.restore_adventure(master_seed, int(bp.get("rng_counter", 0)))
|
||||
_resume_ledger(bp, params)
|
||||
_resume_curio_state(bp)
|
||||
_resume_params(bp, params)
|
||||
_resume_counters(bp)
|
||||
# ---- 断点回放(R2 整族归件——件侧语句入 _bp_store、宿主侧语句按原语句序留本壳,序逐步保形)----
|
||||
_bp_store.restore_ledger(bp, params) # 续玩账面回填——主角账三件片段各还原各键(件侧;clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载)
|
||||
summary = {}
|
||||
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
|
||||
level_index = int(bp.get("level_index", 1))
|
||||
_bp_store._pending_next_level = false
|
||||
_reset_curio_state() # 续玩三选一态回填前置清理(池与件计数归件、停点态归本类;桶归宿主字典)
|
||||
_bp_store.restore_curio_state(bp) # 续玩三选一态回填(池余库/获取计数/特性持有集——件侧;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS)
|
||||
_bp_store.apply_resume_params(params) # 续玩参数面——数值账规范化与越界钳制(件侧)
|
||||
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 续玩参数面同 start_adventure(缺省 legacy)
|
||||
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
|
||||
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
|
||||
_refresh_bank.set_refresh_x_cap(_attune_refresh_x_total()) # 刷新账 X 基数预算注入(续玩参数面同 start)
|
||||
_bp_store.restore_stop_bank_fragments(bp, params) # 停点件/刷新账片段回填(件侧——凭证计数;刷新存量/Y 池缺键回退参数面)
|
||||
_tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # A/B 面 rule 键集(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传)
|
||||
# 续玩计数器回填(adventure 域计数——缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先——桶归宿主字典)
|
||||
var bp_counters = _bp_store._bp_read("adventure_counters", bp)
|
||||
if bp_counters is Dictionary:
|
||||
adventure_counters = (bp_counters as Dictionary).duplicate(true)
|
||||
var bp_pending: Dictionary[Variant, Variant] = bp.get("bonus_pending", {}) as Dictionary
|
||||
var bp_result: Dictionary[Variant, Variant] = bp.get("bonus_result", {}) as Dictionary
|
||||
if not bp_pending.is_empty():
|
||||
_pending_next_level = true
|
||||
_bp_store._pending_next_level = true
|
||||
_state = State.RUNNING
|
||||
_stop.restore_pending(bp_pending) # 选择停点直回(续玩不重跑关卡——信号经停点件中继)
|
||||
return true
|
||||
if not bp_result.is_empty():
|
||||
_pending_next_level = true
|
||||
_bp_store._pending_next_level = true
|
||||
_stop.restore_resolved(bp_result) # 已兑付不重兑付只重发(置态+重发收编一步——信号面归停点件)
|
||||
_state = State.RUNNING
|
||||
return true
|
||||
_resume_bp = bp.duplicate(true)
|
||||
_bp_store._resume_bp = bp.duplicate(true)
|
||||
# 充能读侧以行集为源(键值仅校验读数——回填时点在首关 bind 行集就位后,经 _loadout_on_level_inventory)
|
||||
_loadout_seeded = false
|
||||
_bp_store._loadout_seeded = false
|
||||
_loadout.set_attunement(_attunement)
|
||||
adventure_inventory.reset()
|
||||
_state = State.RUNNING
|
||||
return _start_current_level()
|
||||
|
||||
|
||||
# 续玩账面回填(生命/特性/钱包/加算区/已购档/钥石/逐杀明细/真视——主角账三件片段各还原各键;
|
||||
# bonus 子键 clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载)
|
||||
func _resume_ledger(bp: Dictionary, params: Dictionary) -> void:
|
||||
_stats.restore_fragment(bp, params)
|
||||
_wallet.restore_fragment(bp, params)
|
||||
_journal.restore_fragment(bp, params)
|
||||
summary = {}
|
||||
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
|
||||
level_index = int(bp.get("level_index", 1))
|
||||
_pending_next_level = false
|
||||
|
||||
|
||||
# 续玩三选一态回填(池与件计数归件、停点态归本类;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS)
|
||||
func _resume_curio_state(bp: Dictionary) -> void:
|
||||
_reset_curio_state()
|
||||
var bp_taken: Dictionary[Variant, Variant] = bp.get("bonus_taken", {}) as Dictionary
|
||||
for id in _curio.taken.keys():
|
||||
_curio.taken[id] = maxi(0, int(bp_taken.get(id, 0)))
|
||||
for id in bp_taken.keys():
|
||||
_curio.taken[String(id)] = maxi(0, int(bp_taken.get(id, 0))) # 身份维键(relic:<feat_id>)随快照还原
|
||||
# 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径;
|
||||
# relic 令牌 feat_id 随还原)
|
||||
var bp_deck = _bp_read("bonus_deck", bp)
|
||||
if bp_deck is Array:
|
||||
var restored: Array[Variant] = []
|
||||
for t in (bp_deck as Array):
|
||||
var tok: Dictionary[Variant, Variant] = t as Dictionary
|
||||
var rtok := {"kind": String(tok.get("kind", "")), "tier": String(tok.get("tier", ""))}
|
||||
if String(rtok["kind"]) == "relic":
|
||||
rtok["feat_id"] = String(tok.get("feat_id", "")) # relic 令牌 feat_id 随还原
|
||||
restored.append(rtok)
|
||||
_curio.deck = restored
|
||||
# 特性持有集还原(缺键 = 旧版断点防御空集——主角数值账字段单点)
|
||||
_stats.feats = []
|
||||
for r in (bp.get("relics", []) as Array):
|
||||
_stats.feats.append(String(r))
|
||||
|
||||
# 续玩参数面回填(同 start_adventure 参数面;生命账本越界钳制在此落——clamp 类)
|
||||
func _resume_params(bp: Dictionary, params: Dictionary) -> void:
|
||||
_stats.rebase_override(params.get("hero_override", {})) # 续玩参数面同 start_adventure(规范化归数值账)
|
||||
_stats.attack_type = String(params.get("attack_type", "")) # 续玩参数面同 start_adventure(账号层选中态随 resume 重注入)
|
||||
_stats.armor_type = String(params.get("armor_type", ""))
|
||||
# 生命账本越界钳制(T17——spawn 守卫 EC16 免疫):断点可能带出高于生效上限的账本值
|
||||
#(如调试浮窗调高期写出的值,实例:hero_hp=28 > 基础 26)——恢复后钳到生效上限,
|
||||
# 否则首战 spawn「hero_hp 入参越界」拒收 → 「敌人系统 spawn 被拒」fatal。钳制式单点归
|
||||
# 数值账 clamp_hp_to_resume_cap(保守口径不含加算区——与原钳制式逐字同)。
|
||||
_stats.clamp_hp_to_resume_cap(int(db.combat.hero.max_hp))
|
||||
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 续玩参数面同 start_adventure(缺省 legacy)
|
||||
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
|
||||
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
|
||||
_refresh_bank.set_refresh_x_cap(_attune_refresh_x_total()) # 刷新账 X 基数预算注入(续玩参数面同 start)
|
||||
_stop.restore_fragment(bp, params) # 停点件片段回填(凭证计数)
|
||||
_refresh_bank.restore_fragment(bp, params) # 刷新账片段回填(刷新存量/Y 池——Y 缺键回退参数面 params_fallback 类)
|
||||
_tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # A/B 面 rule 键集(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传)
|
||||
|
||||
|
||||
# 续玩计数器回填(冒险域计数——凭证计数/刷新存量/Y 池随停点件片段回填)
|
||||
func _resume_counters(bp: Dictionary) -> void:
|
||||
# adventure 域计数器回填(缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先——桶归宿主字典)
|
||||
var bp_counters = _bp_read("adventure_counters", bp)
|
||||
if bp_counters is Dictionary:
|
||||
adventure_counters = (bp_counters as Dictionary).duplicate(true)
|
||||
|
||||
|
||||
func _reset_curio_state() -> void:
|
||||
_curio.reset() # 池与件获取计数(池半归件;停点态归停点件下行清理)
|
||||
_stop.reset_adventure_state()
|
||||
_loadout_seeded = false
|
||||
_bp_store._loadout_seeded = false
|
||||
_stats.feats = []
|
||||
adventure_counters = {}
|
||||
|
||||
@@ -263,7 +211,7 @@ func effective_max_hp() -> int:
|
||||
|
||||
# 断点承载注入(组合根冒险开始时调用;模拟/冒烟不注入 = 断点面整体静默)
|
||||
func set_save_manager(sm: SaveManagerCore) -> void:
|
||||
_save_manager = sm
|
||||
_bp_store._save_manager = sm
|
||||
|
||||
|
||||
# 当前关卡行进器读面(呈现层/冒烟用——每关新建实例,调用方须在 level_started 后重取)
|
||||
@@ -278,13 +226,13 @@ func apply_rest(pct: int, hp: int, max_hp: int) -> int:
|
||||
|
||||
# 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;三选一未兑付拒绝推进)
|
||||
func continue_to_next_level() -> bool:
|
||||
if not _pending_next_level:
|
||||
if not _bp_store._pending_next_level:
|
||||
push_error("[冒险结构] continue_to_next_level 拒绝——无待确认的关卡推进")
|
||||
return false
|
||||
if _stop.curio_stop_pending():
|
||||
push_error("[冒险结构] continue_to_next_level 拒绝——三选一停点未兑付(零自动推进契约)")
|
||||
return false
|
||||
_pending_next_level = false
|
||||
_bp_store._pending_next_level = false
|
||||
_stop.clear_resolved()
|
||||
level_index += 1
|
||||
return _start_current_level()
|
||||
@@ -343,9 +291,9 @@ func _loadout_on_level_inventory() -> void:
|
||||
var inv = _runner.treasure_inventory
|
||||
if inv == null:
|
||||
return
|
||||
if not _loadout_seeded:
|
||||
_loadout_seeded = true
|
||||
var bp_charges: Variant = _bp_read("attunement_charges", _resume_bp, null) if not _resume_bp.is_empty() else null
|
||||
if not _bp_store._loadout_seeded:
|
||||
_bp_store._loadout_seeded = true
|
||||
var bp_charges: Variant = _bp_store._bp_read("attunement_charges", _bp_store._resume_bp, null) if not _bp_store._resume_bp.is_empty() else null
|
||||
if bp_charges is Dictionary:
|
||||
_loadout.restore_fragment({"attunement_charges": bp_charges}, inv) # 旧档读侧:行集为源+键值校验读数
|
||||
else:
|
||||
@@ -368,8 +316,8 @@ func _loadout_on_level_inventory() -> void:
|
||||
# apply_adventure_summary 逐关卡完成关次置位〕;末关 Boss 照常入账;明细随摘要 additive 键供结算面归因)
|
||||
func _boss_keystone_drop(li: int) -> void:
|
||||
var first := true
|
||||
if _save_manager != null:
|
||||
first = not _save_manager.meta_state.career_state().boss_first_kill.has(str(li))
|
||||
if _bp_store._save_manager != null:
|
||||
first = not _bp_store._save_manager.meta_state.career_state().boss_first_kill.has(str(li))
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.meta.boss_keystone_row(li)
|
||||
var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0))
|
||||
_journal.record_keystone_drop({"level": li, "first": first, "amount": amount}) # 逐杀明细+累计单写原语
|
||||
@@ -419,9 +367,9 @@ func _start_current_level() -> bool:
|
||||
# 断点续玩分支:图由主种子+参数档重生成同图(产图与计数器号位无关,停点零重抽语义随之消解),
|
||||
# resume_state 带停点句柄与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。
|
||||
var resume_state: Dictionary[Variant, Variant] = {}
|
||||
if not _resume_bp.is_empty():
|
||||
resume_state = {"node_id": int(_bp_read("node_id", _resume_bp, -1)),
|
||||
"visited_ids": _bp_read("visited_ids", _resume_bp, []) as Array}
|
||||
if not _bp_store._resume_bp.is_empty():
|
||||
resume_state = {"node_id": int(_bp_store._bp_read("node_id", _bp_store._resume_bp, -1)),
|
||||
"visited_ids": _bp_store._bp_read("visited_ids", _bp_store._resume_bp, []) as Array}
|
||||
# 接口①开始关卡:生命经主角账共享交接(账面当前值即开局生命——冒险开始置满 / 大奖励回满已写好);
|
||||
# 主角账三件与冒险桶注入(构造注入 ctx——拆件批 T3 收口形态)。
|
||||
# defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在
|
||||
@@ -431,7 +379,7 @@ func _start_current_level() -> bool:
|
||||
if ok:
|
||||
_emit_level_started()
|
||||
_present_level_entry(resume_state)
|
||||
_resume_bp = {}
|
||||
_bp_store._resume_bp = {}
|
||||
return ok
|
||||
|
||||
|
||||
@@ -455,7 +403,7 @@ func _present_level_entry(resume_state: Dictionary) -> void:
|
||||
if resume_state.is_empty() or int(resume_state.get("node_id", -1)) < 0:
|
||||
_runner.present_entry_choice()
|
||||
else:
|
||||
_runner.restore_treasure_items(_bp_read("aux_items", _resume_bp, []) as Array)
|
||||
_runner.restore_treasure_items(_bp_store._bp_read("aux_items", _bp_store._resume_bp, []) as Array)
|
||||
_runner.present_resumed_choice()
|
||||
|
||||
|
||||
@@ -464,7 +412,7 @@ func _on_level_finished(payload: Dictionary) -> void:
|
||||
if state != "running":
|
||||
push_error("[冒险结构] 重复关卡结束宣布拒绝——state=%s(账本与摘要零变化)" % state)
|
||||
return
|
||||
_last_level_payload = payload.duplicate(true)
|
||||
_bp_store._last_level_payload = payload.duplicate(true)
|
||||
var result := String(payload.get("result", ""))
|
||||
if result == "defeat":
|
||||
_stats.hero_hp = 0 # 倒下记 0(生命账本战后回写口径)
|
||||
@@ -473,7 +421,7 @@ func _on_level_finished(payload: Dictionary) -> void:
|
||||
if result != "victory":
|
||||
push_error("[冒险结构] 关卡结束结果出域: %s" % result)
|
||||
return
|
||||
_last_treasure_cleared = int(payload.get("aux_cleared", 0)) # additive(末关 summary 取数面)
|
||||
_bp_store._last_treasure_cleared = int(payload.get("aux_cleared", 0)) # additive(末关 summary 取数面)
|
||||
# Boss 钥石逐杀掉落(击杀掉落渠道,末关照常入账;首杀/复杀读 meta.tres boss_keystone)
|
||||
_boss_keystone_drop(level_index)
|
||||
if level_index < levels_total:
|
||||
@@ -481,7 +429,7 @@ func _on_level_finished(payload: Dictionary) -> void:
|
||||
#(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位);
|
||||
# 停点件内部分回血先行 → 加权洗牌掷 3 → 三选一停点(顺序即契约——回血按当刻账本与上限结算,
|
||||
# 选择停点零自动推进)。载荷_base 随附钱包读数(停点件零钱包依赖——GP 件入账回调仍走 grant_curio_gp)
|
||||
_pending_next_level = true
|
||||
_bp_store._pending_next_level = true
|
||||
var stop_base: Dictionary[Variant, Variant] = payload.duplicate(true)
|
||||
stop_base["gp"] = _wallet.gp
|
||||
_stop.open_level_clear(stop_base)
|
||||
@@ -499,7 +447,7 @@ func _settle(result: String) -> void:
|
||||
"stopped_at": 0 if result == "completed" else level_index,
|
||||
"keystone_total": _journal.keystone_gained(),
|
||||
"master_seed": master_seed,
|
||||
"aux_cleared_last": _last_treasure_cleared, # additive 扩签(末关清空反馈;非末关走 bonus 载荷)
|
||||
"aux_cleared_last": _bp_store._last_treasure_cleared, # additive 扩签(末关清空反馈;非末关走 bonus 载荷)
|
||||
"keystone_drops": _journal.keystone_drop_log(), # additive 扩签(逐杀掉落明细;settle 入账明细/复盘归因数据源)
|
||||
"vision_used_delta": _journal.vision_used(), # additive 扩签(真视使用本局计数;settle 聚合入档)
|
||||
"pool_stock": _pool_stock_snapshot(), # additive 扩签(终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面)
|
||||
@@ -507,19 +455,19 @@ func _settle(result: String) -> void:
|
||||
}
|
||||
# 败退明细(数值验证工具取数面——tools/balance_verifier.py 止步节点分布数据源;
|
||||
# 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知)
|
||||
if result == "failed" and not _last_level_payload.is_empty():
|
||||
if result == "failed" and not _bp_store._last_level_payload.is_empty():
|
||||
summary["defeat"] = {
|
||||
"level_index": level_index,
|
||||
"node_id": int(_last_level_payload.get("node_id", -1)),
|
||||
"node_type": String(_last_level_payload.get("node_type", "")),
|
||||
"node_id": int(_bp_store._last_level_payload.get("node_id", -1)),
|
||||
"node_type": String(_bp_store._last_level_payload.get("node_type", "")),
|
||||
}
|
||||
if _runner != null:
|
||||
summary["gp_final"] = int(_runner.gp) # gp_final 清账前快照(终局面板取数源=runner 钱包单点)
|
||||
_runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现)
|
||||
_reset_curio_state() # 三选一停点态收束清零(结算受理只算一次语义——重复宣布零变化)
|
||||
if _save_manager != null:
|
||||
_save_manager.clear_breakpoint() # 三结局清断点(退出冒险=放弃流程+清除断点)
|
||||
_save_manager.write_to_disk()
|
||||
if _bp_store._save_manager != null:
|
||||
_bp_store._save_manager.clear_breakpoint() # 三结局清断点(退出冒险=放弃流程+清除断点)
|
||||
_bp_store._save_manager.write_to_disk()
|
||||
_state = State.SETTLING
|
||||
adventure_finished.emit(summary)
|
||||
_state = State.IDLE
|
||||
@@ -530,131 +478,7 @@ func _on_level_choices_presented(_candidates: Array) -> void:
|
||||
_snapshot_breakpoint()
|
||||
|
||||
|
||||
# ---- 断点快照(字段登记单源:写侧拼装与读侧回退策略同登记一表)----
|
||||
# 键序即快照键序(27 顶层键——写侧/读侧/键集断言三面同源)。读侧回退策略词表:
|
||||
# plain 无特殊回退面(缺键取代码内缺省)/clamp 数值夹 ≥0/keep_on_null 缺键或类型不符保持既有态
|
||||
# dual_merge 双向合并(bonus_taken)/params_fallback 缺键回退参数面(attune_refresh_y)
|
||||
# token_migration 旧版候选形态迁移(bonus_pending.candidates)
|
||||
# 注入缺省 null = 整面静默(模拟/冒烟零扰动)。
|
||||
const BREAKPOINT_FIELDS := {
|
||||
"level_index": {"strategy": "plain"},
|
||||
"node_id": {"strategy": "plain"},
|
||||
"hero_hp": {"strategy": "clamp"},
|
||||
"bonus": {"strategy": "clamp"},
|
||||
"gp": {"strategy": "plain"},
|
||||
"shop_purchased": {"strategy": "plain"},
|
||||
"shop_shelf": {"strategy": "plain"},
|
||||
"shop_visit_buys": {"strategy": "plain"},
|
||||
"attunement_charges": {"strategy": "keep_on_null"},
|
||||
"graph_gen_v2": {"strategy": "plain"},
|
||||
"master_seed": {"strategy": "plain"},
|
||||
"rng_counter": {"strategy": "plain"},
|
||||
"keystone": {"strategy": "plain"},
|
||||
"visited_ids": {"strategy": "plain"},
|
||||
"aux_items": {"strategy": "plain"},
|
||||
"bonus_taken": {"strategy": "dual_merge"},
|
||||
"bonus_deck": {"strategy": "keep_on_null"},
|
||||
"relics": {"strategy": "plain"},
|
||||
"bonus_pending": {"strategy": "token_migration"},
|
||||
"feat_ticket_pity": {"strategy": "plain"},
|
||||
"feat_ticket_sold": {"strategy": "plain"},
|
||||
"adventure_counters": {"strategy": "keep_on_null"},
|
||||
"vision_counter_used": {"strategy": "clamp"},
|
||||
"refresh_stock": {"strategy": "clamp"},
|
||||
"attune_refresh_y": {"strategy": "params_fallback"},
|
||||
"bonus_result": {"strategy": "plain"},
|
||||
"keystone_drops": {"strategy": "plain"},
|
||||
"gain_journal": {"strategy": "plain"}, # 获得日记(拆件批 T4 扩列——唯一声明的断点扩展;旧档缺键=汇总直载跳过比对)
|
||||
}
|
||||
|
||||
|
||||
# ---- 断点快照委托(组装与读侧策略机组归 breakpoint_store 件——六成员+27 键登记单源随件;
|
||||
# 本壳一行转发:CurioStop 构造注入的 Callable 目标与本类 _on_level_choices_presented 调用点零改)----
|
||||
func _snapshot_breakpoint() -> void:
|
||||
if _save_manager == null or state != "running":
|
||||
return
|
||||
var parts: Dictionary[Variant, Variant] = _bp_parts()
|
||||
var bp: Dictionary[Variant, Variant] = {}
|
||||
for key in BREAKPOINT_FIELDS.keys():
|
||||
bp[key] = parts[key]
|
||||
_save_manager.set_breakpoint(bp)
|
||||
_save_manager.write_to_disk()
|
||||
|
||||
|
||||
# 断点写侧取值(键集以 BREAKPOINT_FIELDS 为准——未登记键即取值越界,fail-fast)
|
||||
func _bp_parts() -> Dictionary[Variant, Variant]:
|
||||
var stats_frag: Dictionary[Variant, Variant] = _stats.breakpoint_fragment()
|
||||
var wallet_frag: Dictionary[Variant, Variant] = _wallet.breakpoint_fragment()
|
||||
var journal_frag: Dictionary[Variant, Variant] = _journal.breakpoint_fragment()
|
||||
var bank_frag: Dictionary[Variant, Variant] = _refresh_bank.breakpoint_fragment()
|
||||
var stop_frag: Dictionary[Variant, Variant] = _stop.breakpoint_fragment()
|
||||
var level_frag: Dictionary[Variant, Variant] = _runner.breakpoint_fragment() if _runner != null else {
|
||||
"node_id": -1, "visited_ids": [] as Array, "aux_items": [] as Array,
|
||||
"shop_shelf": [] as Array, "shop_visit_buys": 0} # 三选一停点续玩分支 _runner 为 null——节点域按缺省落档
|
||||
# 该两态的续玩只消费 bonus_* 附随件(选择停点/已兑付停点快照)
|
||||
var parts: Dictionary[Variant, Variant] = {
|
||||
"level_index": level_index,
|
||||
"node_id": int(level_frag.get("node_id", -1)),
|
||||
"hero_hp": int(stats_frag.get("hero_hp", 0)),
|
||||
"bonus": journal_frag.get("bonus", {}),
|
||||
"gp": int(wallet_frag.get("gp", 0)),
|
||||
"shop_purchased": journal_frag.get("shop_purchased", {}),
|
||||
"shop_shelf": level_frag.get("shop_shelf", []), # 货架入断点(离开店/非店停点 = 空架合法态)
|
||||
"shop_visit_buys": int(level_frag.get("shop_visit_buys", 0)), # 本次进店已购次数(A 链定价输入)
|
||||
"attunement_charges": (_loadout.breakpoint_fragment(_runner.treasure_inventory) if _runner != null and _runner.treasure_inventory != null else {"attunement_charges": {}}).get("attunement_charges", {}), # 同调主动件充能余量随断点(行派生——save v4 域键名策略不变)
|
||||
"graph_gen_v2": 1, # 生成图换代代差标记(缺键 = 换代前旧断点 → resume 显式拒绝)
|
||||
"master_seed": master_seed,
|
||||
"rng_counter": rng.counter(),
|
||||
"keystone": int(journal_frag.get("keystone", 0)),
|
||||
"visited_ids": level_frag.get("visited_ids", []),
|
||||
"aux_items": level_frag.get("aux_items", []),
|
||||
"bonus_taken": _curio.taken.duplicate(true), # 三选一件获取次数(kind 维度派生账本——断点附随件与读数承载)
|
||||
"bonus_deck": _curio.deck.duplicate(true), # 令牌池余库(TP0 v2.2——池消耗即上限,续玩余库逐枚还原)
|
||||
"relics": stats_frag.get("relics", []), # 特性持有集(局内随机渠道获得态续玩防丢失)
|
||||
"bonus_pending": stop_frag.get("bonus_pending", {}), # 三选一停点态(空 = 无停点——选择停点可中断契约)
|
||||
"feat_ticket_pity": int(stop_frag.get("feat_ticket_pity", 0)), # T5 专长凭证保底计数(扩列向后兼容——旧档缺键缺省 0)
|
||||
"feat_ticket_sold": int(stop_frag.get("feat_ticket_sold", 0)), # T5 专长凭证限购计数(同上)
|
||||
"adventure_counters": adventure_counters.duplicate(true), # adventure 域计数器随断点(续玩跨战账不丢——桶归宿主字典)
|
||||
"vision_counter_used": int(journal_frag.get("vision_counter_used", 0)), # 真视使用计数随断点(续玩跨战账不丢)
|
||||
"refresh_stock": int(bank_frag.get("refresh_stock", 0)), # 刷新存量账随断点
|
||||
"attune_refresh_y": int(bank_frag.get("attune_refresh_y", 0)), # Y 冒险周期池余量随断点(扩列向后兼容——旧档缺键回退参数面)
|
||||
"bonus_result": stop_frag.get("bonus_result", {}), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
|
||||
"keystone_drops": journal_frag.get("keystone_drops", []), # 逐杀掉落明细(摘要归因续接)
|
||||
"gain_journal": journal_frag.get("gain_journal", {}), # 获得日记(唯一声明的断点扩列——日记为现行事实源)
|
||||
}
|
||||
# 拼盘完整性自断言(fail-fast——片段并集与登记键集不符即中止落盘)
|
||||
var stray: Array[Variant] = []
|
||||
for key in parts.keys():
|
||||
if not BREAKPOINT_FIELDS.has(key):
|
||||
stray.append(key)
|
||||
if not stray.is_empty():
|
||||
push_error("[冒险结构] 断点拼盘含未登记键: %s(fail-fast——片段与登记单源失同步)" % str(stray))
|
||||
return parts
|
||||
|
||||
|
||||
|
||||
# 读侧单点(BREAKPOINT_FIELDS 策略标签分发;未登记键 fail-fast 不静默按 plain 兜)。
|
||||
# 二次消费面(_start_current_level 经 _resume_bp 取 node_id/visited_ids/aux_items)同走此口。
|
||||
func _bp_read(key: String, bp: Dictionary, fallback: Variant = null) -> Variant:
|
||||
if not BREAKPOINT_FIELDS.has(key):
|
||||
push_error("[冒险结构] 断点字段未登记(读侧): %s" % key)
|
||||
var v: Variant = bp.get(key, fallback)
|
||||
if _bp_strategy(key) == "keep_on_null" and v != null and not (v is Dictionary or v is Array):
|
||||
return null # 类型不符——保持既有态(调用方跳过覆盖)
|
||||
return v
|
||||
|
||||
|
||||
# 读侧策略标签(BREAKPOINT_FIELDS 读侧消费面单点)
|
||||
func _bp_strategy(key: String) -> String:
|
||||
return String((BREAKPOINT_FIELDS.get(key, {}) as Dictionary).get("strategy", "plain"))
|
||||
|
||||
|
||||
# 数值类读侧(clamp 夹 ≥0 / params_fallback 断点缺键回退参数面整额注入 / plain 直取)
|
||||
func _bp_read_int(key: String, bp: Dictionary, fallback: int, params: Dictionary = {}) -> int:
|
||||
match _bp_strategy(key):
|
||||
"clamp":
|
||||
return maxi(0, int(bp.get(key, fallback)))
|
||||
"params_fallback":
|
||||
return maxi(0, int(bp.get(key, int(params.get(key, fallback)))))
|
||||
_:
|
||||
return int(bp.get(key, fallback))
|
||||
|
||||
|
||||
_bp_store.snapshot(state, level_index, master_seed, _runner, adventure_counters)
|
||||
@@ -0,0 +1,48 @@
|
||||
## 战斗事实件:战斗事实的唯一记录面——本战受理事实(encounter:打什么、带什么进来)、
|
||||
## 同调装配事实(attunement)、轮 facts 缓冲(reveals/seals/path)、封印判定账(去重两集+完美三账)、
|
||||
## 装板溯源(seq/provenance)、条件事实计数(refresh_used、feat_first_dig_used、attune_safe_seen)。
|
||||
## 零注入引用、零信号、零协作对象调用——纯数据+谓词+写口;缓冲写入点在宿主 A2 处理器,
|
||||
## 身份快照捕获留宿主(fact 件不持协作引用,防它变成第二编排面)。
|
||||
## 完美账写口单点=三 mark 方法(契约 2:perfect_hit_false 保持纯净,豁免只累加 perfect_forgiven);
|
||||
## 其余成员公有直读写(段类/快照/宿主经 oc._facts.X——fx_host._fx_bonus_delta 跨件直读写同款形态)。
|
||||
## reset_round 只清 reveals/seals/path 三项、不碰 refresh_used(_battle_over 后快照读到的
|
||||
## refresh_used 必须保持末轮值)。
|
||||
## 成员冻结:15/15 贴顶——实现期禁增员;扩容阀=先把封印判定账五成员二次拆出(SealLedger)再落新账。
|
||||
class_name BattleFacts
|
||||
extends RefCounted
|
||||
|
||||
var encounter: Dictionary[String, Variant] = {} # 本战受理事实(begin_battle assign 后全生命期只读)
|
||||
var reveals: Array[Dictionary] = [] # 轮 facts 缓冲(cell_revealed 批)
|
||||
var seals: Array[Dictionary] = [] # 封印判定批缓冲(seal_judged 批)
|
||||
var path := "" # 轮末路径事实(round_end 载荷 path_tag)
|
||||
var refresh_used := 0 # 本轮刷新计数(复位点=出口分发,语句随迁)
|
||||
var board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
|
||||
var board_from_midround := false # 装板 provenance:轮内刷新换来的图=刷新图(清图奖励不发)
|
||||
var seal_consumed := {} # 封印兑付资格账(危险格实例为键;install 重置)
|
||||
var seal_miss_seen := {} # 封印未中行去重(每安全格实例一次)
|
||||
var perfect_hit_true := 0 # 整图完美封印计数(install 重置)
|
||||
var perfect_hit_false := 0 # 完美误标计数(纯净禁回退改写——契约 2)
|
||||
var perfect_forgiven := 0 # 完美保险豁免累计(单向累加自有账)
|
||||
var feat_first_dig_used := false # 条件事实计数(魔矢每战首次)
|
||||
var attunement: Array[Variant] = [] # 本战装配集快照(enc.attunement 受理携带;受理后只读)
|
||||
var attune_safe_seen := 0 # 积点制件安全开格计数(装配集自动兑付进度)
|
||||
|
||||
|
||||
# 轮复位(原宿主 _reset_round_facts 逐字——三调用点:begin_battle 首板 install 后/出口分发/终局序列)
|
||||
func reset_round() -> void:
|
||||
reveals.clear()
|
||||
seals.clear()
|
||||
path = ""
|
||||
|
||||
|
||||
# 完美封印三账写口(授予段直写升级为账本单点——契约 2 执行点)
|
||||
func mark_perfect_hit() -> void:
|
||||
perfect_hit_true += 1
|
||||
|
||||
|
||||
func mark_perfect_miss() -> void:
|
||||
perfect_hit_false += 1
|
||||
|
||||
|
||||
func mark_perfect_forgiven() -> void:
|
||||
perfect_forgiven += 1
|
||||
@@ -0,0 +1 @@
|
||||
uid://bo0eolh0vmvgb
|
||||
@@ -3,7 +3,7 @@
|
||||
## 效果引擎施加面归 BattleFxHost、奖励授予管线归 RewardGrant(两拆件,经组件出口 fx_host()/
|
||||
## reward_grant() 取件,禁链式穿透其内部);本类保留宿主职责面=状态机与生命周期、轮 facts 缓冲、
|
||||
## A2 三事件订阅、五段驱动、attune 族、行发射基建(_emit_row/_assert_row_source/_fatal)、呈现投影、刷新面。
|
||||
## 零 await;随机只经 rng 封闭流(encounter/behavior_roll/armor_roll/gp_drop)——同种子同输入逐位复现。
|
||||
## 零 await;随机只经 rng 封闭流(_facts.encounter/behavior_roll/armor_roll/gp_drop)——同种子同输入逐位复现。
|
||||
## 呈现层读取唯一出口 = presentation_snapshot()/diagnostic_snapshot(),禁链式穿透内部协作对象。
|
||||
## 伤害只向 DamageController 开单(何时开单归编排、怎么算归控制器);刷新唯一通道 player_request_refresh,禁 RAP 满自动刷。
|
||||
class_name BattleOrchestrator
|
||||
@@ -34,6 +34,8 @@ const SOURCE_EXEMPT_TYPES := ["decay", "verdict"]
|
||||
# fx_grant_gp=专长·GP 入账(钱包面而非棋盘面,行无格子可指)
|
||||
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp"]
|
||||
|
||||
var _facts: BattleFacts # 战斗事实件(battle_facts.gd——事实族 15 成员单点)
|
||||
var _rounds: RoundCycle # 轮循环驱动件(settlement/round_cycle.gd——五段持有/段序/重入闸/摇号)
|
||||
var db: ConfigDb
|
||||
var rng: RngServiceCore
|
||||
# E1 封装收口:协作对象 enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/diagnostic_snapshot() 成文出口。
|
||||
@@ -45,60 +47,43 @@ var action_economy: ActionEconomy
|
||||
var _enemy_system: UnitContainer
|
||||
var _status_effects: StatusContainer
|
||||
var _reward_economy: RewardEconomy
|
||||
var treasure_inventory: TreasureItems = null:
|
||||
set(v):
|
||||
treasure_inventory = v
|
||||
if _reward_grant != null:
|
||||
_reward_grant.treasure_inventory = v # 库存注入转发(注入时序在宿主构造之后——外部赋值路径零改址)
|
||||
var _damage: DamageController # 伤害控制器(单据唯一兑付管线)
|
||||
var _fx_host: BattleFxHost # 效果引擎宿主件(fx 族持有与兑付——拆件定稿轴件一)
|
||||
var _reward_grant: RewardGrant # 奖励授予件(授予管线与秘宝使用——拆件定稿轴件二)
|
||||
|
||||
var encounter: Dictionary[String, Variant] = {}
|
||||
var round_index := 0
|
||||
var battle_result := "" # "" / "victory" / "defeat" / "fatal_error"
|
||||
|
||||
var _facts_reveals: Array[Dictionary] = []
|
||||
var _facts_seals: Array[Dictionary] = []
|
||||
var _facts_path := ""
|
||||
var _settling := false
|
||||
# 结算五段 Segment(段边界=审计五段,段序骨架冻结于
|
||||
# SettlementTable.segment_order 语义:授予→增益/减益→战斗→衰减→判定。本类做段序编排+
|
||||
# 行发射/开单原语单点——段体逻辑归 game/core/settlement/ 五类)
|
||||
var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点)
|
||||
var _aura_seg = null # AuraSegment(②增益/减益段)
|
||||
var _combat_seg = null # CombatSegment(③战斗段)
|
||||
var _decay_seg = null # DecaySegment(④衰减段)
|
||||
var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归本类)
|
||||
var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round)
|
||||
var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
|
||||
var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励)
|
||||
var _seal_consumed := {} # 兑付资格以危险格实例为键(整图;换图重置)
|
||||
# 同调装配运行态(begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动)
|
||||
var attunement: Array[Variant] = [] # 装备中同调件 item_id 集
|
||||
var _mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(触发条件事实面——本场累计/上轮转存/当轮累计三读数合一账)
|
||||
# 转存时点=_roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle
|
||||
var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(装配集 ap_charge_safe 行扫描;满 charge_n 自动兑付当轮 AP+1,无玩家发起入口)
|
||||
# 同调装配运行态(begin_battle 经 enc 携带 attunement 键注入 facts;off=空集全部效果零扰动)
|
||||
# 转存时点=roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle
|
||||
var nest_preview_count := -1 # 巢穴预告读数口(board_installed 命中落值;-1=未装配/未发射;呈现桥消费)
|
||||
# 特性运行态(relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经
|
||||
# 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动)
|
||||
var _feat_first_dig_used := false
|
||||
|
||||
|
||||
# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter.assign 后全生命期可读;
|
||||
# 主角攻击基值派生读(enc 携带 hero_atk 键受理后只读——E4 收口语义不变:表值运行期不可变,
|
||||
# 派生值恒等;「读面即引用、无第二账」同款先例)
|
||||
func _hero_atk() -> int:
|
||||
return int(_facts.encounter.get("hero_atk", int(db.combat.hero.atk)))
|
||||
|
||||
|
||||
# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter 受理 assign 后全生命期可读;
|
||||
# null=调试/单场直构面,账面消费跳过。领域拆分批成员删除后的引用承接,读面即引用、无第二账)
|
||||
func _journal() -> GainJournal:
|
||||
return encounter.get("journal", null) if encounter.get("journal", null) is GainJournal else null
|
||||
return _facts.encounter.get("journal", null) if _facts.encounter.get("journal", null) is GainJournal else null
|
||||
|
||||
|
||||
# 同调配装件读面(begin_battle 经 enc["loadout"] 派生——充能单源化的 reset_cycle/consume_charge
|
||||
# 消费面;null=直构零装配,充能面跳过)
|
||||
func _attune_loadout() -> AttuneLoadout:
|
||||
return encounter.get("loadout", null)
|
||||
return _facts.encounter.get("loadout", null)
|
||||
|
||||
|
||||
# 授予件战斗域回调束(bind_battle 注入——键集封闭三键:充能消耗/真视计数/负向踩雷计数;
|
||||
# 宿主侧各转 _attune_loadout/_journal/_mines_hits 单点,授予件对账本零强引用)
|
||||
# 宿主侧各转 _attune_loadout/_journal/_rounds.mines_hits 单点,授予件对账本零强引用)
|
||||
func _battle_hooks() -> Dictionary[Variant, Variant]:
|
||||
return {
|
||||
"attune_consume": _hook_attune_consume,
|
||||
@@ -108,8 +93,8 @@ func _battle_hooks() -> Dictionary[Variant, Variant]:
|
||||
|
||||
|
||||
func _hook_attune_consume(item_id: String) -> void:
|
||||
if _attune_loadout() != null and treasure_inventory != null:
|
||||
_attune_loadout().consume_charge(item_id, treasure_inventory)
|
||||
if _attune_loadout() != null and _reward_grant.treasure_inventory != null:
|
||||
_attune_loadout().consume_charge(item_id, _reward_grant.treasure_inventory)
|
||||
|
||||
|
||||
func _hook_vision_add() -> void:
|
||||
@@ -120,8 +105,8 @@ func _hook_vision_add() -> void:
|
||||
# 负向 blast(雷)踩雷计数入账(触发条件事实面——正向 spark_aoe/清图奖励不计入;
|
||||
# 极性判定在授予件 _flat_blast 传点,本口只记账)
|
||||
func _book_negative_blast() -> void:
|
||||
_mines_hits["round"] += 1
|
||||
_mines_hits["total"] += 1
|
||||
_rounds.mines_hits["round"] += 1
|
||||
_rounds.mines_hits["total"] += 1
|
||||
|
||||
|
||||
# 效果引擎宿主侧事实单点(fx_host facts_cb 注入——round_index/battle_index/seals_count/nest_count/hp_ratio 五事实;charge_layers 归 fx_host 自答;未知事实回落 null)
|
||||
@@ -136,10 +121,10 @@ func _fx_host_fact(fact: String) -> Variant:
|
||||
"round_index":
|
||||
return round_index
|
||||
"battle_index":
|
||||
return int(encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面)
|
||||
return int(_facts.encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面)
|
||||
"seals_count":
|
||||
var hits_n := 0
|
||||
for fevt in _facts_seals:
|
||||
for fevt in _facts.seals:
|
||||
if bool(fevt.get("hit", false)):
|
||||
hits_n += 1
|
||||
return hits_n
|
||||
@@ -151,7 +136,7 @@ func _fx_host_fact(fact: String) -> Variant:
|
||||
|
||||
# attune 积点读投影(RewardGrant attune_safe_seen_cb 注入面——use_treasure 拒绝文案读数;经读投影出户防组件回调宿主私有族)
|
||||
func _attune_safe_seen_read() -> int:
|
||||
return _attune_safe_seen
|
||||
return _facts.attune_safe_seen
|
||||
|
||||
|
||||
# 组件出口(拆件后取件面——呈现与测试经此取件,禁链式穿透协作对象内部)
|
||||
@@ -164,7 +149,7 @@ func reward_grant() -> RewardGrant:
|
||||
|
||||
|
||||
func _attune_has(item_id: String) -> bool:
|
||||
return attunement.has(item_id)
|
||||
return _facts.attunement.has(item_id)
|
||||
|
||||
|
||||
# 装配集只读出口(_attune_has 契约化出口,save v6 只读访问器先例。只读零副作用——
|
||||
@@ -179,7 +164,7 @@ func attune_has(item_id: String) -> bool:
|
||||
# kind 分发查询先例=_attune_refresh_x_total bonus_refresh。零装配=0)
|
||||
func attune_param_sum(kind: String) -> int:
|
||||
var total := 0
|
||||
for iid_v in attunement:
|
||||
for iid_v in _facts.attunement:
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
|
||||
if (arow.get("effect_kinds", []) as Array).has(kind):
|
||||
total += int(arow.get("magnitude", 0))
|
||||
@@ -189,7 +174,7 @@ func attune_param_sum(kind: String) -> int:
|
||||
# 装配集内 ap_charge_safe 行读取(取数源=装配集行扫描;
|
||||
# 首行语义=装配集内至多一件积点制〔试炼镐〕)
|
||||
func _attune_charge_safe_row() -> Dictionary:
|
||||
for iid_v in attunement:
|
||||
for iid_v in _facts.attunement:
|
||||
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
|
||||
if (arow.get("effect_kinds", []) as Array).has("ap_charge_safe"):
|
||||
return arow
|
||||
@@ -200,9 +185,9 @@ func _attune_charge_safe_row() -> Dictionary:
|
||||
# (used handle 不回收=本轮已耗充能作废,补新 handle=「回满」;差额注入防双份=level_runner
|
||||
# set_attune_charges 同构)。reset ∈ {battle, level}——adventure 周期归冒险开始种子面)
|
||||
func _reseed_attune_charges(reset: String) -> void:
|
||||
if _attune_loadout() == null or treasure_inventory == null:
|
||||
if _attune_loadout() == null or _reward_grant.treasure_inventory == null:
|
||||
return # 直构零装配/库存未就位——充能回满跳过(空装配同语义)
|
||||
_attune_loadout().reset_cycle(reset, treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体)
|
||||
_attune_loadout().reset_cycle(reset, _reward_grant.treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体)
|
||||
|
||||
|
||||
# 积点制件进度读数(自动兑付无玩家发起入口,读数口=C 区秘宝槽悬停浮窗进度行:
|
||||
@@ -213,18 +198,13 @@ func attune_convert_state() -> Dictionary:
|
||||
if row.is_empty() or String(row.get("form", "")) != "active_charged":
|
||||
return {"assembled": false, "points": 0, "need": 0, "display_name": "", "item_id": ""}
|
||||
var cn := int(row.get("charge_n", 0))
|
||||
return {"assembled": true, "points": _attune_safe_seen % cn if cn > 0 else 0, "need": cn,
|
||||
return {"assembled": true, "points": _facts.attune_safe_seen % cn if cn > 0 else 0, "need": cn,
|
||||
"display_name": String(row.get("display_name", "")), "item_id": String(row.get("item_id", ""))}
|
||||
|
||||
|
||||
# 特性求值(效果行为=数据行 triggers 订阅,效果引擎 dispatch 分发;
|
||||
# 流派档位质变=FeatsEngine 纯函数求值
|
||||
# (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击)
|
||||
var _seal_miss_seen := {} # 封印未中行去重(每安全格实例一次)
|
||||
var _perfect_hit_true := 0 # 整图完美封印计数(install 重置)
|
||||
var _perfect_hit_false := 0
|
||||
var _perfect_forgiven := 0 # 完美保险豁免累计(单向累加自有账;契约 2 禁改写 _perfect_hit_false)
|
||||
var _hero_atk := 0 # 主角攻击基值快照(encounter.hero_atk;E4 收口——表值运行期不可变)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
@@ -234,6 +214,7 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
minesweeper = MinesweeperRules.new(db, action_economy)
|
||||
_enemy_system = UnitContainer.new(db)
|
||||
_status_effects = StatusContainer.new(db)
|
||||
_facts = BattleFacts.new() # 战斗事实件(受理事实/轮缓冲/封印账/溯源/条件计数——成员迁移见设计 §4)
|
||||
_reward_economy = RewardEconomy.new(db, rng)
|
||||
_reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线)
|
||||
_damage = DamageController.new(db, rng, _enemy_system, _status_effects)
|
||||
@@ -252,11 +233,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
minesweeper.round_end_board.connect(_on_round_end)
|
||||
action_economy.round_end_stopped.connect(_on_round_end)
|
||||
# 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕)
|
||||
_grant_seg = GrantSegment.new(self, _fx_host, _reward_grant)
|
||||
_aura_seg = AuraSegment.new(self)
|
||||
_combat_seg = CombatSegment.new(self, _fx_host, _reward_grant)
|
||||
_decay_seg = DecaySegment.new(self)
|
||||
_verdict_seg = VerdictSegment.new(self)
|
||||
# 结算五段接线随轮循环驱动件迁驻(round_cycle.gd——段件持有/段序/重入闸/摇号单点;构造序禁环不变)
|
||||
_rounds = RoundCycle.new(self, _fx_host, _reward_grant)
|
||||
|
||||
|
||||
# ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)----
|
||||
@@ -269,7 +247,7 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
return false
|
||||
if not _install_and_reset():
|
||||
return false
|
||||
_reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险)
|
||||
_facts.reset_round() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险)
|
||||
_begin_battle_fx_state(enc)
|
||||
_begin_battle_attune_params(enc)
|
||||
_begin_battle_preview_economy(enc)
|
||||
@@ -281,14 +259,14 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
if not action_economy.notify_round_start():
|
||||
_fatal("行动经济 首轮 notify_round_start 被拒")
|
||||
return false
|
||||
_roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读)
|
||||
_rounds.roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读)
|
||||
round_started.emit(round_index)
|
||||
return true
|
||||
|
||||
|
||||
# 受理段:encounter 装载+缓冲清零+护甲合流+重载闸+开域门+遭遇骰与敌人系统 spawn 受理门。
|
||||
# 受理段:_facts.encounter 装载+缓冲清零+护甲合流+重载闸+开域门+遭遇骰与敌人系统 spawn 受理门。
|
||||
func _begin_battle_accept(enc: Dictionary) -> bool:
|
||||
encounter.assign(enc)
|
||||
_facts.encounter.assign(enc)
|
||||
_reward_grant.reset_battle() # 战斗域零 GP 持久(每场战斗缓冲从零起算;案C 闸复位)
|
||||
_damage.set_hero_ac_override(int(enc.get("hero_armor_class", -1))) # 主角护甲合流入参(伤害控制器 defender_ac 消费)
|
||||
db.set_reload_gate(true)
|
||||
@@ -299,9 +277,9 @@ func _begin_battle_accept(enc: Dictionary) -> bool:
|
||||
if dice.is_empty():
|
||||
_fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle"))])
|
||||
return false
|
||||
# E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径)
|
||||
_hero_atk = int(enc.get("hero_atk", int(db.combat.hero.atk)))
|
||||
if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _hero_atk, "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": String(enc.get("node_type", "battle"))}): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss)
|
||||
# E4 收口:加成经 _facts.encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径;
|
||||
# _hero_atk 派生读=受理后不变量,无快照成员)
|
||||
if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _hero_atk(), "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": String(enc.get("node_type", "battle"))}): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss)
|
||||
_fatal("敌人系统 spawn 被拒")
|
||||
return false
|
||||
return true
|
||||
@@ -309,7 +287,7 @@ func _begin_battle_accept(enc: Dictionary) -> bool:
|
||||
|
||||
# 遭遇骰(难度曲线与Boss GDD Core Rule 8):行有 unit_count_max 时按 [unit_count, max] 均匀骰;
|
||||
# 行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值
|
||||
# encounter 封闭流恰一 scope(数量骰先、atk 骰后——区间宽度为零的行不掷=「定值不骰」既有口径);
|
||||
# _facts.encounter 封闭流恰一 scope(数量骰先、atk 骰后——区间宽度为零的行不掷=「定值不骰」既有口径);
|
||||
# 敌人系统零随机纪律保持(收 override 即确定值)。
|
||||
# 返回 [count_overrides, atk_overrides];行集为空 = 返回 [](fail 判据交受理段 _fatal)。
|
||||
func _begin_battle_dice(enc: Dictionary) -> Array:
|
||||
@@ -352,12 +330,12 @@ func _begin_battle_dice_roll(rows: Array, estream: RandomNumberGenerator, roll_a
|
||||
# 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零)
|
||||
# 扩签运行态就位(归 fx_host)+账本挂接+桶挂引用+揭示/AC 两回调+rider 档位恒零+订阅重建。
|
||||
func _begin_battle_fx_state(enc: Dictionary) -> void:
|
||||
# 同调装配运行态注入(enc.attunement 携带;off=空集零扰动。淬毒 N 注入
|
||||
# 同调装配运行态注入(enc._facts.attunement 携带;off=空集零扰动。淬毒 N 注入
|
||||
# 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install)
|
||||
attunement = (enc.get("attunement", []) as Array).duplicate()
|
||||
_attune_safe_seen = 0
|
||||
_facts.attunement = (enc.get("attunement", []) as Array).duplicate()
|
||||
_facts.attune_safe_seen = 0
|
||||
_fx_host.reset_battle_state(int(enc.get("hero_armor_class", -1)))
|
||||
_mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款)
|
||||
_rounds.mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款)
|
||||
var adv_bucket: Variant = enc.get("adv_bucket", null)
|
||||
_fx_host.mount_buckets(adv_bucket if adv_bucket is Dictionary else {}, enc.get("level_counters")) # 冒险桶宿主字典(直构兜底自有空字典);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂
|
||||
_reward_grant.bind_battle(_battle_hooks()) # 授予件战斗域回调束(充能消耗/真视计数/踩雷计数)
|
||||
@@ -368,13 +346,13 @@ func _begin_battle_fx_state(enc: Dictionary) -> void:
|
||||
# 不设档位机制」):rider_tier 两注入恒零,散件单体效果=唯一形态
|
||||
_damage.inject["rider_tier_bonus"] = 0
|
||||
_damage.inject["rider_tier_crit"] = false
|
||||
_feat_first_dig_used = false
|
||||
_facts.feat_first_dig_used = false
|
||||
# 树 rule 键消费(Fork 激活侧效果=效果引擎合成
|
||||
# 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键
|
||||
# =stat 面投影不入战斗(T8 表内容 provisional 口径披露))
|
||||
var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array
|
||||
# 效果引擎订阅重建(装载序=feats 表序→treasure 表序→树规则,写死保种子复放——细则见 battle_fx_host.rebuild_subscriptions)
|
||||
_fx_host.rebuild_subscriptions(feat_ids, attunement, tree_rules)
|
||||
_fx_host.rebuild_subscriptions(feat_ids, _facts.attunement, tree_rules)
|
||||
|
||||
|
||||
# 秘宝常时参数注入(装配集 kind 分发:远程镐挖距放宽/信标网络封印可达豁免;
|
||||
@@ -415,19 +393,19 @@ func _begin_battle_preview_economy(enc: Dictionary) -> void:
|
||||
# 板序推进+board_changed 发射+秘宝叠加层退役——时序逐字;复位职责留宿主(成员单点),不随件。
|
||||
func _install_and_reset(during_round := false) -> bool:
|
||||
var installer := BoardInstaller.new({"db": db, "rng": rng, "minesweeper": minesweeper,
|
||||
"reward_economy": _reward_economy, "encounter": encounter, "fatal_cb": _fatal})
|
||||
"reward_economy": _reward_economy, "encounter": _facts.encounter, "fatal_cb": _fatal})
|
||||
if not installer.install(during_round):
|
||||
return false
|
||||
_seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8)
|
||||
_seal_miss_seen.clear()
|
||||
_perfect_hit_true = 0 # 整图完美计数随 install 重置
|
||||
_perfect_hit_false = 0
|
||||
_perfect_forgiven = 0 # 豁免累计随 install 重置(domain=battle 每战清零)
|
||||
_board_from_midround_refresh = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false
|
||||
_board_seq += 1
|
||||
_facts.seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8)
|
||||
_facts.seal_miss_seen.clear()
|
||||
_facts.perfect_hit_true = 0 # 整图完美计数随 install 重置
|
||||
_facts.perfect_hit_false = 0
|
||||
_facts.perfect_forgiven = 0 # 豁免累计随 install 重置(domain=battle 每战清零)
|
||||
_facts.board_from_midround = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false
|
||||
_facts.board_seq += 1
|
||||
board_changed.emit()
|
||||
if treasure_inventory != null:
|
||||
treasure_inventory.retire_overlays() # 叠加层随图消亡(换图/刷新图装板点;已花道具不返还)
|
||||
if _reward_grant.treasure_inventory != null:
|
||||
_reward_grant.treasure_inventory.retire_overlays() # 叠加层随图消亡(换图/刷新图装板点;已花道具不返还)
|
||||
return true
|
||||
|
||||
|
||||
@@ -448,9 +426,9 @@ func _on_cell_revealed(evt: Dictionary) -> void:
|
||||
if String(evt.get("kind", "")) == "safe":
|
||||
var cs_row: Dictionary[Variant, Variant] = _attune_charge_safe_row()
|
||||
if not cs_row.is_empty():
|
||||
_attune_safe_seen += 1
|
||||
_facts.attune_safe_seen += 1
|
||||
var cn_b: int = int(cs_row.get("charge_n", 999))
|
||||
if cn_b > 0 and _attune_safe_seen % cn_b == 0:
|
||||
if cn_b > 0 and _facts.attune_safe_seen % cn_b == 0:
|
||||
action_economy.grant_ap(int(cs_row.get("magnitude", 1)))
|
||||
if bool(evt.get("resource", false)):
|
||||
# 身份快照随缓冲捕获(批次隔离——轮内刷新后 mapping 已换批,授予必须用缓冲期身份,
|
||||
@@ -459,47 +437,20 @@ func _on_cell_revealed(evt: Dictionary) -> void:
|
||||
if snap != null:
|
||||
evt = evt.duplicate()
|
||||
evt["reward_snapshot"] = snap
|
||||
_facts_reveals.append(evt)
|
||||
_facts.reveals.append(evt)
|
||||
|
||||
|
||||
func _on_seal_judged(evt: Dictionary) -> void:
|
||||
_facts_seals.append(evt)
|
||||
_facts.seals.append(evt)
|
||||
|
||||
|
||||
func _on_round_end(_payload: Dictionary) -> void:
|
||||
_facts_path = String(_payload.get("path_tag", ""))
|
||||
_maybe_settle()
|
||||
_facts.path = String(_payload.get("path_tag", ""))
|
||||
_rounds.settle_if_due()
|
||||
|
||||
|
||||
func _maybe_settle() -> void:
|
||||
if _settling or battle_result != "":
|
||||
return
|
||||
if action_economy.phase != action_economy.PHASE_WAITING:
|
||||
return
|
||||
_settle()
|
||||
|
||||
|
||||
# ---- 每轮结算序列(五段——段序骨架冻结于 SettlementTable.segment_order)----
|
||||
|
||||
func _settle() -> void:
|
||||
_settling = true
|
||||
var ri := round_index
|
||||
# 段序编排(五段——段体归 game/core/settlement/,本类只做段序+行发射/开单原语单点)
|
||||
_grant_seg.run(ri) # ① 授予段(案C 闸随段装载)
|
||||
_aura_seg.run(ri) # ② 增益/减益段(续存实例组合行)
|
||||
_combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕)
|
||||
_decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减)
|
||||
var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判)
|
||||
if verdict != "continue":
|
||||
return # 终局:_settling 不回置——_battle_over 已接管
|
||||
_settling = false
|
||||
|
||||
|
||||
# ---- 授予段 helpers(极性路由)----
|
||||
|
||||
# 开格产出乘区(贪婪矿灯/矿脉直觉/赌徒的血签事件收集器注入):
|
||||
# positive=power ×(1+静态 mult%);negative=power ×(1+cost%)。
|
||||
# 全零收集器=原快照原样返回(零扰动)。
|
||||
func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int) -> Dictionary:
|
||||
if positive and pos_m <= 0:
|
||||
return snap
|
||||
@@ -556,57 +507,7 @@ func set_treasure_unlocked(ids: Dictionary) -> void:
|
||||
# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。
|
||||
# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。
|
||||
# 摇定后、兑现前死亡 → 本拍技能作废。
|
||||
func _roll_round_actions() -> void:
|
||||
var pity_m: int = int(db.combat.skill.pity_threshold)
|
||||
# facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts,
|
||||
# 容器内直读主角行)
|
||||
var facts := {
|
||||
"round_index": round_index,
|
||||
"last_round_end": "none" if action_economy.last_round_end().is_empty() else String(action_economy.last_round_end().get("path_tag", "none")),
|
||||
"mines_hit_last": int(_mines_hits["last"]),
|
||||
"mines_hit_total": int(_mines_hits["total"]),
|
||||
}
|
||||
for u in _enemy_system.query_alive("enemy"):
|
||||
# Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行
|
||||
# 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性
|
||||
# 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机
|
||||
# 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏)
|
||||
for sid in _enemy_system.check_trigger_unlocks(u, facts):
|
||||
var srow_u: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid)
|
||||
_emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"},
|
||||
round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
|
||||
0, {"skill_id": sid})
|
||||
# enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射,
|
||||
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值)
|
||||
var pct_v := 0
|
||||
for pe in (u.get("pool", []) as Array):
|
||||
var pe_d: Dictionary[Variant, Variant] = pe
|
||||
if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"):
|
||||
pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0))
|
||||
break
|
||||
# target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记,
|
||||
# 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value)
|
||||
var eht_hits := _fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v,
|
||||
"is_boss": String(encounter.get("node_type", "")) == "boss"})
|
||||
for h in eht_hits:
|
||||
if String(h.get("kind", "")) == "target_attack_bonus":
|
||||
u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0))
|
||||
var entries: Array[Variant] = _enemy_system.rollable_skills(u, facts)
|
||||
if _enemy_system.basic_streak(u) >= pity_m:
|
||||
var specials: Array[Variant] = _enemy_system.special_skills(u, facts)
|
||||
if not specials.is_empty():
|
||||
entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合
|
||||
var stream: RandomNumberGenerator = rng.open_stream("behavior_roll")
|
||||
_enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream))
|
||||
_enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记)
|
||||
_mines_hits["last"] = _mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新)
|
||||
_mines_hits["round"] = 0
|
||||
|
||||
|
||||
# ---- 战斗段/算术读数(段体与掷点/折扣/暴击/克制/池乘区/下限/交付管线归 damage_controller.gd
|
||||
# 与 settlement/combat_segment.gd;本类只保留薄委托——_defender_ac 供结算/呈现快照消费)----
|
||||
|
||||
# 防守方护甲等级读数——实现归 伤害控制器 defender_ac(本委托供结算快照/呈现快照)
|
||||
func _defender_ac(unit: Dictionary) -> int:
|
||||
return _damage.defender_ac(unit)
|
||||
|
||||
@@ -643,12 +544,12 @@ func _emit_settlement_snapshot(verdict: String) -> void:
|
||||
buffs.append({"id": eff.effect_id, "side": "enemy", "layers": eff.layers, "left": eff.remaining_settlements})
|
||||
var mines_hit := 0
|
||||
var safe_n := 0
|
||||
for evt in _facts_reveals:
|
||||
for evt in _facts.reveals:
|
||||
if evt.kind == "mine":
|
||||
mines_hit += 1
|
||||
elif evt.kind == "safe":
|
||||
safe_n += 1
|
||||
var board_stats := {"seq": _board_seq}
|
||||
var board_stats := {"seq": _facts.board_seq}
|
||||
if minesweeper.current_board() != null:
|
||||
board_stats["w"] = minesweeper.current_board().width
|
||||
board_stats["h"] = minesweeper.current_board().height
|
||||
@@ -656,18 +557,18 @@ func _emit_settlement_snapshot(verdict: String) -> void:
|
||||
board_stats["seals"] = minesweeper.seal_count()
|
||||
settlement_snapshot.emit({
|
||||
"kind": "settlement",
|
||||
"level": int(encounter.get("level_index", 0)),
|
||||
"depth": int(encounter.get("node_depth", 0)),
|
||||
"level": int(_facts.encounter.get("level_index", 0)),
|
||||
"depth": int(_facts.encounter.get("node_depth", 0)),
|
||||
"round": round_index,
|
||||
"verdict": verdict,
|
||||
"unit_count": enemies.size(),
|
||||
"spawn_count": _enemy_system.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」
|
||||
"hero": {"hp": int(_enemy_system.hero_unit().get("hp", 0)), "max_hp": int(_enemy_system.hero_unit().get("max_hp", 0)), "atk": int(_enemy_system.hero_unit().get("atk", 0)), "armor_class": _defender_ac(_enemy_system.hero_unit())},
|
||||
"economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _refresh_used_this_round},
|
||||
"economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _facts.refresh_used},
|
||||
"board": board_stats,
|
||||
"enemies": enemies,
|
||||
"buffs": buffs,
|
||||
"round_facts": {"path": _facts_path, "safe": safe_n, "mines": mines_hit, "seal_events": _facts_seals.size(), },
|
||||
"round_facts": {"path": _facts.path, "safe": safe_n, "mines": mines_hit, "seal_events": _facts.seals.size(), },
|
||||
})
|
||||
|
||||
|
||||
@@ -675,7 +576,7 @@ func _emit_settlement_snapshot(verdict: String) -> void:
|
||||
|
||||
func _exit_distribution() -> void:
|
||||
var mode := "same_board"
|
||||
if _facts_path == "cleared":
|
||||
if _facts.path == "cleared":
|
||||
mode = "new_board" # cleared 轮恒走 new_board(契约义务);非清图轮恒同图(等待期刷新通道已移除)
|
||||
if mode == "new_board":
|
||||
_reward_economy.retire_mapping()
|
||||
@@ -689,16 +590,16 @@ func _exit_distribution() -> void:
|
||||
round_index += 1
|
||||
_fx_host.emit_round_start()
|
||||
action_economy.notify_round_start()
|
||||
_reset_round_facts()
|
||||
_refresh_used_this_round = 0
|
||||
_roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道)
|
||||
_facts.reset_round()
|
||||
_facts.refresh_used = 0
|
||||
_rounds.roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道)
|
||||
round_started.emit(round_index)
|
||||
|
||||
|
||||
# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道
|
||||
func can_refresh() -> bool:
|
||||
return battle_result == "" \
|
||||
and _refresh_used_this_round < int(db.run.action_economy.refresh_cap_per_round) \
|
||||
and _facts.refresh_used < int(db.run.action_economy.refresh_cap_per_round) \
|
||||
and action_economy.phase == action_economy.PHASE_ACTIVE \
|
||||
and action_economy.rap >= int(db.run.action_economy.rap_refresh_cost)
|
||||
|
||||
@@ -711,7 +612,7 @@ func player_request_refresh() -> bool:
|
||||
return false
|
||||
if not action_economy.request_refresh():
|
||||
return false
|
||||
_refresh_used_this_round += 1
|
||||
_facts.refresh_used += 1
|
||||
_reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提)
|
||||
minesweeper.retire_board(true) # 旧板退役(轮内通道)
|
||||
return _install_and_reset(true) # 当场换图——失败即编排违例 fail-fast
|
||||
@@ -737,11 +638,11 @@ func _battle_over(result: String) -> void:
|
||||
_reward_economy.retire_mapping()
|
||||
_enemy_system.despawn_all()
|
||||
_status_effects.notify_battle_end()
|
||||
if treasure_inventory != null:
|
||||
treasure_inventory.retire_overlays() # 叠加层随战斗结束清空(库存不动——关卡域跨战斗)
|
||||
if _reward_grant.treasure_inventory != null:
|
||||
_reward_grant.treasure_inventory.retire_overlays() # 叠加层随战斗结束清空(库存不动——关卡域跨战斗)
|
||||
action_economy.notify_combat_end() # → Idle,RAP 清零
|
||||
db.set_reload_gate(false)
|
||||
_reset_round_facts()
|
||||
_facts.reset_round()
|
||||
battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "gp_dropped": gp_dropped})
|
||||
|
||||
|
||||
@@ -749,7 +650,7 @@ func _battle_over(result: String) -> void:
|
||||
# gp_dropped = gp_drop 封闭流掷定(tier 行的 roll_min..roll_max 均匀整数——每战斗末恰一 scope)
|
||||
# + level_gp_bonus(level_index) + escrow。掷定行缺位 = 数据面缺行(fail 报错按 0 兑付——不哑掉)。
|
||||
func _roll_gp_drop() -> int:
|
||||
var tier := "boss" if String(encounter.get("node_type", "")) == "boss" else "normal"
|
||||
var tier := "boss" if String(_facts.encounter.get("node_type", "")) == "boss" else "normal"
|
||||
var drops: Dictionary[Variant, Variant] = db.run.shop.gp_drop_row(tier)
|
||||
var roll := 0
|
||||
if drops.is_empty():
|
||||
@@ -757,11 +658,11 @@ func _roll_gp_drop() -> int:
|
||||
else:
|
||||
var stream: RandomNumberGenerator = rng.open_stream("gp_drop")
|
||||
roll = stream.randi_range(int(drops.get("roll_min", 0)), int(drops.get("roll_max", 0)))
|
||||
var drop := roll + db.run.shop.level_gp_bonus_at(int(encounter.get("level_index", 0)))
|
||||
var drop := roll + db.run.shop.level_gp_bonus_at(int(_facts.encounter.get("level_index", 0)))
|
||||
# 财富之道·聚(战斗掉落 GP 乘算·入账前:只乘战斗掉落两成分〔掷定+关卡加成〕,
|
||||
# 店内 escrow 不乘〔范围=仅战斗掉落渠道〕;本函数返回值=结算框「GP 增量」行
|
||||
# 显示源,落在入账前=显示即实得;乘数 provisional 150%)
|
||||
if (encounter.get("tree_rules", []) as Array).has("economy_hoard"):
|
||||
if (_facts.encounter.get("tree_rules", []) as Array).has("economy_hoard"):
|
||||
drop = int(round(float(drop) * 1.5))
|
||||
return drop + _reward_grant.escrow
|
||||
|
||||
@@ -812,7 +713,7 @@ func presentation_snapshot() -> Dictionary:
|
||||
"phase": String(action_economy.phase),
|
||||
"ap": int(action_economy.ap), "rap": int(action_economy.rap),
|
||||
"can_refresh": can_refresh(),
|
||||
"refresh_used": _refresh_used_this_round, # 受理计数入呈现成文面(HUD 余量单源读此处)
|
||||
"refresh_used": _facts.refresh_used, # 受理计数入呈现成文面(HUD 余量单源读此处)
|
||||
"hero": hero,
|
||||
"enemies": enemies,
|
||||
"hero_effects": _status_effects.query_active("hero").duplicate(true), # 值拷贝出户(快照消费方直改不渗入状态容器)
|
||||
@@ -839,12 +740,12 @@ func diagnostic_snapshot() -> Dictionary:
|
||||
return {
|
||||
"battle_result": battle_result,
|
||||
"round": round_index,
|
||||
"settling": _settling,
|
||||
"refresh_used_round": _refresh_used_this_round,
|
||||
"board_seq": _board_seq,
|
||||
"facts_path": _facts_path,
|
||||
"facts_reveals": _facts_reveals.size(),
|
||||
"facts_seals": _facts_seals.size(),
|
||||
"settling": _rounds.is_settling(),
|
||||
"refresh_used_round": _facts.refresh_used,
|
||||
"board_seq": _facts.board_seq,
|
||||
"facts_path": _facts.path,
|
||||
"facts_reveals": _facts.reveals.size(),
|
||||
"facts_seals": _facts.seals.size(),
|
||||
"action_economy": {"phase": String(action_economy.phase), "ap": action_economy.ap, "rap": action_economy.rap},
|
||||
"minesweeper_board_installed": minesweeper.current_board() != null,
|
||||
"minesweeper_revealed": minesweeper.revealed_cells().size() if minesweeper.current_board() != null else 0,
|
||||
@@ -855,12 +756,6 @@ func diagnostic_snapshot() -> Dictionary:
|
||||
}
|
||||
|
||||
|
||||
func _reset_round_facts() -> void:
|
||||
_facts_reveals.clear()
|
||||
_facts_seals.clear()
|
||||
_facts_path = ""
|
||||
|
||||
|
||||
# ---- 行协议(Core Rule:公共字段 + 行类型特有字段)----
|
||||
|
||||
func _emit_row(head: Dictionary, ri: int, display: String, value: int, extra: Dictionary) -> void:
|
||||
@@ -911,6 +806,6 @@ func _fatal(msg: String) -> void:
|
||||
|
||||
|
||||
## 完美封印谓词(两账相减:有效误标=误标总数−豁免数)
|
||||
## 契约 2:_perfect_hit_false 保持纯净,豁免只累加 _perfect_forgiven,禁回退式改写与拦截式吞事件。
|
||||
## 契约 2:_facts.perfect_hit_false 保持纯净,豁免只累加 _facts.perfect_forgiven,禁回退式改写与拦截式吞事件。
|
||||
func perfect_sweep_earned() -> bool:
|
||||
return _perfect_hit_false - _perfect_forgiven <= 0 and _perfect_hit_true >= 1
|
||||
return _facts.perfect_hit_false - _facts.perfect_forgiven <= 0 and _facts.perfect_hit_true >= 1
|
||||
@@ -0,0 +1,218 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 存档断点组装与回放唯一承载(AdventureRunner 拆件——单一变化原因=断点快照键面与回放策略):
|
||||
## 断点快照拼盘、回放回填、读侧策略分发;BREAKPOINT_FIELDS 27 键登记单源随件(键名/策略标签/键序冻结)。
|
||||
## 构造=引用束字典注入(db/rng/主角账三件/curio/stop/bank/loadout——件禁反向持宿主);
|
||||
## 快照宿主输入(state/level_index/master_seed/runner/adventure_counters)逐调传参;
|
||||
## resume_adventure 留宿主(壳+宿主侧回填语句——所有权缝分家,语句序逐步保形)。
|
||||
class_name BreakpointStore
|
||||
extends RefCounted
|
||||
|
||||
# ---- 断点快照(字段登记单源:写侧拼装与读侧回退策略同登记一表)----
|
||||
# 键序即快照键序(27 顶层键——写侧/读侧/键集断言三面同源)。读侧回退策略词表:
|
||||
# plain 无特殊回退面(缺键取代码内缺省)/clamp 数值夹 ≥0/keep_on_null 缺键或类型不符保持既有态
|
||||
# dual_merge 双向合并(bonus_taken)/params_fallback 缺键回退参数面(attune_refresh_y)
|
||||
# token_migration 旧版候选形态迁移(bonus_pending.candidates)
|
||||
# 注入缺省 null = 整面静默(模拟/冒烟零扰动)。
|
||||
const BREAKPOINT_FIELDS := {
|
||||
"level_index": {"strategy": "plain"},
|
||||
"node_id": {"strategy": "plain"},
|
||||
"hero_hp": {"strategy": "clamp"},
|
||||
"bonus": {"strategy": "clamp"},
|
||||
"gp": {"strategy": "plain"},
|
||||
"shop_purchased": {"strategy": "plain"},
|
||||
"shop_shelf": {"strategy": "plain"},
|
||||
"shop_visit_buys": {"strategy": "plain"},
|
||||
"attunement_charges": {"strategy": "keep_on_null"},
|
||||
"graph_gen_v2": {"strategy": "plain"},
|
||||
"master_seed": {"strategy": "plain"},
|
||||
"rng_counter": {"strategy": "plain"},
|
||||
"keystone": {"strategy": "plain"},
|
||||
"visited_ids": {"strategy": "plain"},
|
||||
"aux_items": {"strategy": "plain"},
|
||||
"bonus_taken": {"strategy": "dual_merge"},
|
||||
"bonus_deck": {"strategy": "keep_on_null"},
|
||||
"relics": {"strategy": "plain"},
|
||||
"bonus_pending": {"strategy": "token_migration"},
|
||||
"feat_ticket_pity": {"strategy": "plain"},
|
||||
"feat_ticket_sold": {"strategy": "plain"},
|
||||
"adventure_counters": {"strategy": "keep_on_null"},
|
||||
"vision_counter_used": {"strategy": "clamp"},
|
||||
"refresh_stock": {"strategy": "clamp"},
|
||||
"attune_refresh_y": {"strategy": "params_fallback"},
|
||||
"bonus_result": {"strategy": "plain"},
|
||||
"keystone_drops": {"strategy": "plain"},
|
||||
"gain_journal": {"strategy": "plain"}, # 获得日记(拆件批 T4 扩列——唯一声明的断点扩展;旧档缺键=汇总直载跳过比对)
|
||||
}
|
||||
|
||||
# 引用束(宿主构造注入——成员名与宿主侧原名一致,迁移函数体逐字)
|
||||
var db: ConfigDb
|
||||
var rng: RngServiceCore
|
||||
var _stats: HeroStats
|
||||
var _wallet: GpWallet
|
||||
var _journal: GainJournal
|
||||
var _curio: CurioDeck = null # 令牌池件(池余库/获取计数随断点)
|
||||
var _stop: CurioStop = null # 停点件(停点态/凭证计数片段)
|
||||
var _refresh_bank: RefreshBank = null # 刷新账件(刷新存量/Y 池片段)
|
||||
var _loadout: AttuneLoadout = null # 同调配装件(充能行集读侧)
|
||||
# 断点域六成员(自宿主收编——宿主其余读写点经本件)
|
||||
var _save_manager = null # 断点承载注入(null = 不写断点;模拟/冒烟缺省)
|
||||
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
|
||||
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
|
||||
var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面)
|
||||
var _last_treasure_cleared := 0 # additive(最近一次关卡末清空计数,summary 取数面)
|
||||
var _loadout_seeded := false # 同调行已种标记(每冒险一次——首关 bind 时点投行;行跨关存活后续关零动作)
|
||||
|
||||
|
||||
func _init(refs: Dictionary) -> void:
|
||||
db = refs["db"]
|
||||
rng = refs["rng"]
|
||||
_stats = refs["stats"]
|
||||
_wallet = refs["wallet"]
|
||||
_journal = refs["journal"]
|
||||
_curio = refs["curio"]
|
||||
_stop = refs["stop"]
|
||||
_refresh_bank = refs["bank"]
|
||||
_loadout = refs["loadout"]
|
||||
|
||||
|
||||
# 断点快照拼盘落盘(宿主 _snapshot_breakpoint 委托目标——宿主输入逐调传参)
|
||||
func snapshot(state: String, level_index: int, master_seed: int, runner: LevelRunner, counters: Dictionary[Variant, Variant]) -> void:
|
||||
if _save_manager == null or state != "running":
|
||||
return
|
||||
var parts: Dictionary[Variant, Variant] = _bp_parts(level_index, master_seed, runner, counters)
|
||||
var bp: Dictionary[Variant, Variant] = {}
|
||||
for key in BREAKPOINT_FIELDS.keys():
|
||||
bp[key] = parts[key]
|
||||
_save_manager.set_breakpoint(bp)
|
||||
_save_manager.write_to_disk()
|
||||
|
||||
|
||||
# 断点写侧取值(键集以 BREAKPOINT_FIELDS 为准——未登记键即取值越界,fail-fast)
|
||||
func _bp_parts(level_index: int, master_seed: int, runner: LevelRunner, counters: Dictionary[Variant, Variant]) -> Dictionary[Variant, Variant]:
|
||||
var stats_frag: Dictionary[Variant, Variant] = _stats.breakpoint_fragment()
|
||||
var wallet_frag: Dictionary[Variant, Variant] = _wallet.breakpoint_fragment()
|
||||
var journal_frag: Dictionary[Variant, Variant] = _journal.breakpoint_fragment()
|
||||
var bank_frag: Dictionary[Variant, Variant] = _refresh_bank.breakpoint_fragment()
|
||||
var stop_frag: Dictionary[Variant, Variant] = _stop.breakpoint_fragment()
|
||||
var level_frag: Dictionary[Variant, Variant] = runner.breakpoint_fragment() if runner != null else {
|
||||
"node_id": -1, "visited_ids": [] as Array, "aux_items": [] as Array,
|
||||
"shop_shelf": [] as Array, "shop_visit_buys": 0} # 三选一停点续玩分支 runner 为 null——节点域按缺省落档
|
||||
# 该两态的续玩只消费 bonus_* 附随件(选择停点/已兑付停点快照)
|
||||
var parts: Dictionary[Variant, Variant] = {
|
||||
"level_index": level_index,
|
||||
"node_id": int(level_frag.get("node_id", -1)),
|
||||
"hero_hp": int(stats_frag.get("hero_hp", 0)),
|
||||
"bonus": journal_frag.get("bonus", {}),
|
||||
"gp": int(wallet_frag.get("gp", 0)),
|
||||
"shop_purchased": journal_frag.get("shop_purchased", {}),
|
||||
"shop_shelf": level_frag.get("shop_shelf", []), # 货架入断点(离开店/非店停点 = 空架合法态)
|
||||
"shop_visit_buys": int(level_frag.get("shop_visit_buys", 0)), # 本次进店已购次数(A 链定价输入)
|
||||
"attunement_charges": (_loadout.breakpoint_fragment(runner.treasure_inventory) if runner != null and runner.treasure_inventory != null else {"attunement_charges": {}}).get("attunement_charges", {}), # 同调主动件充能余量随断点(行派生——save v4 域键名策略不变)
|
||||
"graph_gen_v2": 1, # 生成图换代代差标记(缺键 = 换代前旧断点 → resume 显式拒绝)
|
||||
"master_seed": master_seed,
|
||||
"rng_counter": rng.counter(),
|
||||
"keystone": int(journal_frag.get("keystone", 0)),
|
||||
"visited_ids": level_frag.get("visited_ids", []),
|
||||
"aux_items": level_frag.get("aux_items", []),
|
||||
"bonus_taken": _curio.taken.duplicate(true), # 三选一件获取次数(kind 维度派生账本——断点附随件与读数承载)
|
||||
"bonus_deck": _curio.deck.duplicate(true), # 令牌池余库(TP0 v2.2——池消耗即上限,续玩余库逐枚还原)
|
||||
"relics": stats_frag.get("relics", []), # 特性持有集(局内随机渠道获得态续玩防丢失)
|
||||
"bonus_pending": stop_frag.get("bonus_pending", {}), # 三选一停点态(空 = 无停点——选择停点可中断契约)
|
||||
"feat_ticket_pity": int(stop_frag.get("feat_ticket_pity", 0)), # T5 专长凭证保底计数(扩列向后兼容——旧档缺键缺省 0)
|
||||
"feat_ticket_sold": int(stop_frag.get("feat_ticket_sold", 0)), # T5 专长凭证限购计数(同上)
|
||||
"adventure_counters": counters.duplicate(true), # adventure 域计数器随断点(续玩跨战账不丢——桶归宿主字典)
|
||||
"vision_counter_used": int(journal_frag.get("vision_counter_used", 0)), # 真视使用计数随断点(续玩跨战账不丢)
|
||||
"refresh_stock": int(bank_frag.get("refresh_stock", 0)), # 刷新存量账随断点
|
||||
"attune_refresh_y": int(bank_frag.get("attune_refresh_y", 0)), # Y 冒险周期池余量随断点(扩列向后兼容——旧档缺键回退参数面)
|
||||
"bonus_result": stop_frag.get("bonus_result", {}), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
|
||||
"keystone_drops": journal_frag.get("keystone_drops", []), # 逐杀掉落明细(摘要归因续接)
|
||||
"gain_journal": journal_frag.get("gain_journal", {}), # 获得日记(唯一声明的断点扩列——日记为现行事实源)
|
||||
}
|
||||
# 拼盘完整性自断言(fail-fast——片段并集与登记键集不符即中止落盘)
|
||||
var stray: Array[Variant] = []
|
||||
for key in parts.keys():
|
||||
if not BREAKPOINT_FIELDS.has(key):
|
||||
stray.append(key)
|
||||
if not stray.is_empty():
|
||||
push_error("[冒险结构] 断点拼盘含未登记键: %s(fail-fast——片段与登记单源失同步)" % str(stray))
|
||||
return parts
|
||||
|
||||
|
||||
# 续玩账面回填(生命/特性/钱包/加算区/已购档/钥石/逐杀明细/真视——主角账三件片段各还原各键;
|
||||
# bonus 子键 clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载)
|
||||
#(宿主侧账面语句——summary/levels_total/level_index/_pending_next_level——按语句序留宿主壳)
|
||||
func restore_ledger(bp: Dictionary, params: Dictionary) -> void:
|
||||
_stats.restore_fragment(bp, params)
|
||||
_wallet.restore_fragment(bp, params)
|
||||
_journal.restore_fragment(bp, params)
|
||||
|
||||
|
||||
# 续玩三选一态回填(池与件计数归件、停点态归宿主;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS)
|
||||
#(前置清理 _reset_curio_state 留宿主——桶归宿主字典;本函数承接其后的件侧还原语句,序逐步保形)
|
||||
func restore_curio_state(bp: Dictionary) -> void:
|
||||
var bp_taken: Dictionary[Variant, Variant] = bp.get("bonus_taken", {}) as Dictionary
|
||||
for id in _curio.taken.keys():
|
||||
_curio.taken[id] = maxi(0, int(bp_taken.get(id, 0)))
|
||||
for id in bp_taken.keys():
|
||||
_curio.taken[String(id)] = maxi(0, int(bp_taken.get(id, 0))) # 身份维键(relic:<feat_id>)随快照还原
|
||||
# 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径;
|
||||
# relic 令牌 feat_id 随还原)
|
||||
var bp_deck = _bp_read("bonus_deck", bp)
|
||||
if bp_deck is Array:
|
||||
var restored: Array[Variant] = []
|
||||
for t in (bp_deck as Array):
|
||||
var tok: Dictionary[Variant, Variant] = t as Dictionary
|
||||
var rtok := {"kind": String(tok.get("kind", "")), "tier": String(tok.get("tier", ""))}
|
||||
if String(rtok["kind"]) == "relic":
|
||||
rtok["feat_id"] = String(tok.get("feat_id", "")) # relic 令牌 feat_id 随还原
|
||||
restored.append(rtok)
|
||||
_curio.deck = restored
|
||||
# 特性持有集还原(缺键 = 旧版断点防御空集——主角数值账字段单点)
|
||||
_stats.feats = []
|
||||
for r in (bp.get("relics", []) as Array):
|
||||
_stats.feats.append(String(r))
|
||||
|
||||
|
||||
# 续玩参数面——数值账规范化与越界钳制(件侧;宿主侧四重绑定与预算注入按语句序夹在两件侧调用之间)
|
||||
func apply_resume_params(params: Dictionary) -> void:
|
||||
_stats.rebase_override(params.get("hero_override", {})) # 续玩参数面同 start_adventure(规范化归数值账)
|
||||
_stats.attack_type = String(params.get("attack_type", "")) # 续玩参数面同 start_adventure(账号层选中态随 resume 重注入)
|
||||
_stats.armor_type = String(params.get("armor_type", ""))
|
||||
# 生命账本越界钳制(T17——spawn 守卫 EC16 免疫):断点可能带出高于生效上限的账本值
|
||||
#(如调试浮窗调高期写出的值,实例:hero_hp=28 > 基础 26)——恢复后钳到生效上限,
|
||||
# 否则首战 spawn「hero_hp 入参越界」拒收 → 「敌人系统 spawn 被拒」fatal。钳制式单点归
|
||||
# 数值账 clamp_hp_to_resume_cap(保守口径不含加算区——与原钳制式逐字同)。
|
||||
_stats.clamp_hp_to_resume_cap(int(db.combat.hero.max_hp))
|
||||
|
||||
|
||||
# 停点件/刷新账片段回填(凭证计数;刷新存量/Y 池——Y 缺键回退参数面 params_fallback 类)
|
||||
func restore_stop_bank_fragments(bp: Dictionary, params: Dictionary) -> void:
|
||||
_stop.restore_fragment(bp, params)
|
||||
_refresh_bank.restore_fragment(bp, params)
|
||||
|
||||
|
||||
# 读侧单点(BREAKPOINT_FIELDS 策略标签分发;未登记键 fail-fast 不静默按 plain 兜)。
|
||||
# 二次消费面(_start_current_level 经 _resume_bp 取 node_id/visited_ids/aux_items)同走此口。
|
||||
func _bp_read(key: String, bp: Dictionary, fallback: Variant = null) -> Variant:
|
||||
if not BREAKPOINT_FIELDS.has(key):
|
||||
push_error("[冒险结构] 断点字段未登记(读侧): %s" % key)
|
||||
var v: Variant = bp.get(key, fallback)
|
||||
if _bp_strategy(key) == "keep_on_null" and v != null and not (v is Dictionary or v is Array):
|
||||
return null # 类型不符——保持既有态(调用方跳过覆盖)
|
||||
return v
|
||||
|
||||
|
||||
# 读侧策略标签(BREAKPOINT_FIELDS 读侧消费面单点)
|
||||
func _bp_strategy(key: String) -> String:
|
||||
return String((BREAKPOINT_FIELDS.get(key, {}) as Dictionary).get("strategy", "plain"))
|
||||
|
||||
|
||||
# 数值类读侧(clamp 夹 ≥0 / params_fallback 断点缺键回退参数面整额注入 / plain 直取)
|
||||
func _bp_read_int(key: String, bp: Dictionary, fallback: int, params: Dictionary = {}) -> int:
|
||||
match _bp_strategy(key):
|
||||
"clamp":
|
||||
return maxi(0, int(bp.get(key, fallback)))
|
||||
"params_fallback":
|
||||
return maxi(0, int(bp.get(key, int(params.get(key, fallback)))))
|
||||
_:
|
||||
return int(bp.get(key, fallback))
|
||||
@@ -0,0 +1 @@
|
||||
uid://cw5qdd012h728
|
||||
@@ -9,7 +9,7 @@
|
||||
class_name LevelRunner
|
||||
extends RefCounted
|
||||
|
||||
signal node_entered(node: Dictionary, info: Dictionary) ## info 按 type:battle/boss={} · reward={}(三选一停点空载荷——内容经 present_node_curio 拉取) · rest={healed,hp,max_hp} · shop={gp}
|
||||
signal node_entered(node: Dictionary, info: Dictionary) ## info 按 type:battle/boss={} · reward={}(三选一停点空载荷——内容经 present_node_curio 拉取) · rest={healed,hp,max_hp} · shop={gp}(pool 态加 shelf:true)
|
||||
signal choices_presented(candidates: Array) ## 选路口(兑付确认后——候选节点引用数组,零自动推进)
|
||||
signal step_row(row: Dictionary) ## 转发当前 战斗结算 结算行
|
||||
signal round_started(round_index: int)
|
||||
@@ -517,7 +517,7 @@ func _start_battle(node: Dictionary) -> bool:
|
||||
_settlement = BattleOrchestrator.new(db, rng)
|
||||
if treasure_inventory == null:
|
||||
treasure_inventory = TreasureItems.new(db) # 惰性建——关卡域单实例(跨战斗延续)
|
||||
_settlement.treasure_inventory = treasure_inventory # 库存注入(授予路由+use_treasure 编排消费面)
|
||||
_settlement.reward_grant().treasure_inventory = self.treasure_inventory # 库存注入(经授予件出口直注——注入时序在宿主构造之后不变)
|
||||
if _settlement != null:
|
||||
_settlement.set_treasure_unlocked(_treasure_unlocked) # 解锁门控下传(装板摇号前)
|
||||
# core↔core 接线只许方法 Callable(静态规则 S6——lambda 捕获 self 会延长
|
||||
|
||||
@@ -26,7 +26,7 @@ func run(ri: int) -> void:
|
||||
# 同一次 _settle 内,facts 已定。
|
||||
var round_safe := 0
|
||||
var round_mine := false
|
||||
for evt in oc._facts_reveals:
|
||||
for evt in oc._facts.reveals:
|
||||
if evt.kind == "safe":
|
||||
round_safe += 1
|
||||
elif evt.kind == "mine":
|
||||
@@ -53,7 +53,7 @@ func _hero_strike(ri: int) -> void:
|
||||
var hero_target = oc._enemy_system.default_target()
|
||||
if hero_alive and hero_target != null:
|
||||
var ht := oc._damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击",
|
||||
{"channel": "combat", "base_value": oc._hero_atk, "target_rule": DamageController.TARGET_FRONT_ALIVE,
|
||||
{"channel": "combat", "base_value": oc._hero_atk(), "target_rule": DamageController.TARGET_FRONT_ALIVE,
|
||||
"ri": ri, "origin_cell": Vector2i(-1, -1),
|
||||
"attack_type": String(oc._enemy_system.hero_unit().get("attack_type", "normal"))})
|
||||
oc._damage.submit(ht)
|
||||
|
||||
@@ -34,7 +34,7 @@ func run(ri: int) -> void:
|
||||
|
||||
# 开格/踩雷兑付批(轮 facts_reveals 资源事件序:safe 正向乘区兑付 / mine_hit 减半负向兑付)
|
||||
func _reveal_batch(ri: int) -> void:
|
||||
for evt in oc._facts_reveals:
|
||||
for evt in oc._facts.reveals:
|
||||
if not bool(evt.get("resource", false)):
|
||||
continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②)
|
||||
var snap = evt.get("reward_snapshot")
|
||||
@@ -61,7 +61,7 @@ func _reveal_safe(evt: Dictionary, snap: Dictionary, ri: int) -> void:
|
||||
if is_new_type:
|
||||
fx_host._fx_new_resource_types[ctype] = true
|
||||
var dr_hits := fx_host.fx_emit("dig_reveal", {"cell": evt.cell, "round": ri,
|
||||
"is_first_dig_of_battle": not oc._feat_first_dig_used,
|
||||
"is_first_dig_of_battle": not oc._facts.feat_first_dig_used,
|
||||
"terrain_kind": oc._harmful_variants(evt.get("variant_ids", []) as Array),
|
||||
"is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0
|
||||
or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1,
|
||||
@@ -91,8 +91,8 @@ func _reveal_safe_apply(dr_hits: Array, evt: Dictionary, ri: int) -> void:
|
||||
if String(h.get("kind", "")) == "grant_material":
|
||||
_apply_material_hit(h, evt.cell, ri)
|
||||
var spell_hits: Array[Variant] = oc._hits_of(dr_hits, "spawn_spell")
|
||||
if not spell_hits.is_empty() and not oc._feat_first_dig_used:
|
||||
oc._feat_first_dig_used = true
|
||||
if not spell_hits.is_empty() and not oc._facts.feat_first_dig_used:
|
||||
oc._facts.feat_first_dig_used = true
|
||||
var spell_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", "奥术矢",
|
||||
{"channel": "board", "base_value": int((spell_hits[0] as Dictionary).get("value", 0)),
|
||||
"target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri, "origin_cell": evt.cell, "attack_type": "magic"})
|
||||
@@ -160,7 +160,7 @@ func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: in
|
||||
# 危险格恒无变异——布置段排除,双层叠罚不存在;封印格直挖照吃变异伤害——地形后果同 mine_hit 口径,
|
||||
# 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场)
|
||||
func _terrain_batch(ri: int) -> void:
|
||||
for evt in oc._facts_reveals:
|
||||
for evt in oc._facts.reveals:
|
||||
var vids: Array[Variant] = evt.get("variant_ids", [])
|
||||
for vid in vids:
|
||||
var vrow: Dictionary[Variant, Variant] = oc.db.combat.variation.row_by_id(String(vid))
|
||||
@@ -203,7 +203,7 @@ func _terrain_damage_reduce(dt_hits: Array, dt_amount: int) -> int:
|
||||
# 台账披露待追认〕;完美布防=本轮封印批全中(hit>0 且 miss==0)时封印伤 ×mult——全对推理奖励)。
|
||||
# 伤害单据案C 暂存战斗段末兑付。
|
||||
func _seal_batch(ri: int) -> void:
|
||||
for evt in oc._facts_seals:
|
||||
for evt in oc._facts.seals:
|
||||
_judge_seal(evt, ri)
|
||||
_seal_strike_batch(ri)
|
||||
|
||||
@@ -213,7 +213,7 @@ func _seal_batch(ri: int) -> void:
|
||||
func _seal_strike_batch(ri: int) -> void:
|
||||
var fs_hits := 0
|
||||
var fs_misses := 0
|
||||
for fevt in oc._facts_seals:
|
||||
for fevt in oc._facts.seals:
|
||||
if bool(fevt.get("hit", false)):
|
||||
fs_hits += 1
|
||||
else:
|
||||
@@ -223,7 +223,7 @@ func _seal_strike_batch(ri: int) -> void:
|
||||
var strike := _seal_strike_value(fs_fx, fs_hits, fs_misses)
|
||||
if strike.is_empty():
|
||||
return
|
||||
for fevt in oc._facts_seals:
|
||||
for fevt in oc._facts.seals:
|
||||
if not bool(fevt.get("hit", false)):
|
||||
continue
|
||||
var st_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_seal_strike", "战封印军规",
|
||||
@@ -272,8 +272,8 @@ func _round_end_apply(ri: int) -> void:
|
||||
var rap_pct: int = oc.attune_param_sum("rap_transfer_mult")
|
||||
oc.action_economy.finish_round_stop(fx_host.engine.has_effect_on("round_end", "grant_ap", "transfer"),
|
||||
rap_pct if rap_pct > 0 else 100)
|
||||
if oc._facts_path == "cleared":
|
||||
reward_grant.grant_clear_reward(ri, oc._board_from_midround_refresh, oc.perfect_sweep_earned())
|
||||
if oc._facts.path == "cleared":
|
||||
reward_grant.grant_clear_reward(ri, oc._facts.board_from_midround, oc.perfect_sweep_earned())
|
||||
fx_host.flush_pending_reveals(ri) # 揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入)
|
||||
oc._damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界)
|
||||
|
||||
@@ -305,10 +305,10 @@ func _judge_seal(evt: Dictionary, ri: int) -> void:
|
||||
# 殉爆票据 → 施加面 → 雷区图谱额外兑付 → 基线正向兑付)
|
||||
func _judge_seal_hit(evt: Dictionary, ri: int) -> void:
|
||||
var cell: Vector2i = evt.cell
|
||||
if oc._seal_consumed.has(cell):
|
||||
if oc._facts.seal_consumed.has(cell):
|
||||
return # 去重跳过;零明细行
|
||||
oc._seal_consumed[cell] = true
|
||||
oc._perfect_hit_true += 1
|
||||
oc._facts.seal_consumed[cell] = true
|
||||
oc._facts.mark_perfect_hit()
|
||||
var fh_hits := fx_host.fx_emit("seal_hit", {"cell": cell, "round": ri,
|
||||
"terrain_kind": oc._harmful_adjacent(cell)})
|
||||
_seal_burst_tickets(fh_hits, cell, ri)
|
||||
@@ -367,8 +367,8 @@ func _seal_preview_area(cell: Vector2i, ri: int) -> void:
|
||||
var n_snap = oc._reward_economy.query_full_snapshot(n)
|
||||
if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))):
|
||||
types.append(String((n_snap as Dictionary).get("cell_type", "")))
|
||||
if oc.treasure_inventory != null and not types.is_empty():
|
||||
oc.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
|
||||
if reward_grant.treasure_inventory != null and not types.is_empty():
|
||||
reward_grant.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
|
||||
oc._emit_row({"type": "extended", "source_class": "fx_warning_beacon", "origin_cell": cell}, ri, "示警烟火·周围资源类型", 0, {"effect_id": "fx_preview_area", "types": types})
|
||||
|
||||
|
||||
@@ -376,10 +376,10 @@ func _seal_preview_area(cell: Vector2i, ri: int) -> void:
|
||||
# grant_ap mode:queue 下一轮兑现 / forgive_seal_miss 完美保险豁免 → 归因行 → 负向兑付)
|
||||
func _judge_seal_miss(evt: Dictionary, ri: int) -> void:
|
||||
var cell: Vector2i = evt.cell
|
||||
oc._perfect_hit_false += 1
|
||||
if oc._seal_miss_seen.has(cell):
|
||||
oc._facts.mark_perfect_miss()
|
||||
if oc._facts.seal_miss_seen.has(cell):
|
||||
return # 每格实例一次去重
|
||||
oc._seal_miss_seen[cell] = true
|
||||
oc._facts.seal_miss_seen[cell] = true
|
||||
# 危险直觉·守挂点(seal_missed 发射:契约 12 触发主语=「当回合发生的标错
|
||||
# 动作」——去重闸=新旧标错判别器〔后续轮重判旧封印被闸挡回零触发〕;双条件②「结算时封印仍在
|
||||
# 场」由发射面既有过滤承载〔被挖开的封印已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮
|
||||
@@ -391,11 +391,11 @@ func _judge_seal_miss(evt: Dictionary, ri: int) -> void:
|
||||
elif String(h.get("kind", "")) == "forgive_seal_miss":
|
||||
# 完美保险(订阅式豁免:本战首次标错计一次豁免,只累加自有账
|
||||
# _perfect_forgiven,绝不回写 _perfect_hit_false=契约 2 计数纯净;发射时机与消费点零改动=契约 3)
|
||||
oc._perfect_forgiven += 1
|
||||
oc._facts.mark_perfect_forgiven()
|
||||
_apply_counter_hit(h) # 豁免同时记账(insurance_used battle 域——下次标错 when lt 1 不再命中)
|
||||
oc._emit_row({"type": "extended", "source_class": "seal_miss", "origin_cell": cell}, ri, "完美保险·标错豁免", 0,
|
||||
{"effect_id": "insurance_used", "forgiven": oc._perfect_forgiven})
|
||||
var hl := "perfect_broken" if oc._perfect_hit_false - oc._perfect_forgiven == 1 else ""
|
||||
{"effect_id": "insurance_used", "forgiven": oc._facts.perfect_forgiven})
|
||||
var hl := "perfect_broken" if oc._facts.perfect_hit_false - oc._facts.perfect_forgiven == 1 else ""
|
||||
oc._emit_row({"type": "seal_miss", "source_class": "seal_miss", "origin_cell": cell, "highlight": hl}, ri, "封印未中", 0, {})
|
||||
var snap = oc._reward_economy.grant(cell)
|
||||
if not (snap is Dictionary):
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
## 轮循环驱动件:每轮结算周期的唯一驱动——结算重入门闸(settling)、五段段序执行
|
||||
## (段序骨架冻结于 SettlementTable.segment_order)、开轮摇号(共识点 B:技能选择前置到轮次开始,
|
||||
## 摇定即本拍结算的选技)、踩雷计数账(转存时点=摇号轮末)。
|
||||
## 单一变化原因:轮结算周期的推进结构(段序、重入闸、摇号时序)。
|
||||
## 耦合边界:oc=宿主引用(五段段类既定同款直调形态)——经 oc 取 facts/协作件/行发射/出口分发;
|
||||
## 五段构造参数与持有形态不变(grant/combat 三引用、aura/decay/verdict 单引用);判定段零改动
|
||||
## (_exit_distribution 留宿主);随机只经 oc.rng 的 behavior_roll 封闭流(流名、开流时点、取号序逐字);
|
||||
## 零信号连接、零 await;随编排器每战新建同灭。
|
||||
class_name RoundCycle
|
||||
extends RefCounted
|
||||
|
||||
var oc: BattleOrchestrator # 宿主引用(段类同款直调——facts/协作件/行发射/出口分发取数单点)
|
||||
var mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(本场累计/上轮转存/当轮累计三读数合一账)
|
||||
var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点)
|
||||
var _aura_seg = null # AuraSegment(②增益/减益段)
|
||||
var _combat_seg = null # CombatSegment(③战斗段)
|
||||
var _decay_seg = null # DecaySegment(④衰减段)
|
||||
var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归宿主)
|
||||
var _settling := false
|
||||
|
||||
|
||||
func _init(host: BattleOrchestrator, fx_host_ref: BattleFxHost, reward_grant_ref: RewardGrant) -> void:
|
||||
oc = host
|
||||
# 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕——五行自宿主 _init 逐字迁入)
|
||||
_grant_seg = GrantSegment.new(oc, fx_host_ref, reward_grant_ref)
|
||||
_aura_seg = AuraSegment.new(oc)
|
||||
_combat_seg = CombatSegment.new(oc, fx_host_ref, reward_grant_ref)
|
||||
_decay_seg = DecaySegment.new(oc)
|
||||
_verdict_seg = VerdictSegment.new(oc)
|
||||
|
||||
|
||||
# 结算重入门闸+段序体(原宿主 _maybe_settle 三闸与 _settle 合体——闸序 settling→battle_result→phase
|
||||
# 逐字;终局早退不回置 _settling,_battle_over 接管)
|
||||
func settle_if_due() -> void:
|
||||
if _settling or oc.battle_result != "":
|
||||
return
|
||||
if oc.action_economy.phase != oc.action_economy.PHASE_WAITING:
|
||||
return
|
||||
_settling = true
|
||||
var ri := oc.round_index
|
||||
# 段序编排(五段——段体归 game/core/settlement/,宿主只做行发射/开单原语单点)
|
||||
_grant_seg.run(ri) # ① 授予段(案C 闸随段装载)
|
||||
_aura_seg.run(ri) # ② 增益/减益段(续存实例组合行)
|
||||
_combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕)
|
||||
_decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减)
|
||||
var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判)
|
||||
if verdict != "continue":
|
||||
return # 终局:_settling 不回置——_battle_over 已接管
|
||||
_settling = false
|
||||
|
||||
|
||||
func is_settling() -> bool:
|
||||
return _settling
|
||||
|
||||
|
||||
# ---- 开轮摇号(共识点 B:技能选择前置到轮次开始,数值计算留结算段——原宿主函数逐字迁入)----
|
||||
# 每实例:保底判定(连续执行普攻 ≥ M → 必出特殊技、特殊技内按权重、不受冷却限制)→ 排除冷却中技能
|
||||
# (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从统一流顺序取号摇定本拍动作。
|
||||
# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。
|
||||
# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。
|
||||
# 摇定后、兑现前死亡 → 本拍技能作废。
|
||||
func roll_round_actions() -> void:
|
||||
var pity_m: int = int(oc.db.combat.skill.pity_threshold)
|
||||
# facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts,
|
||||
# 容器内直读主角行)
|
||||
var facts := {
|
||||
"round_index": oc.round_index,
|
||||
"last_round_end": "none" if oc.action_economy.last_round_end().is_empty() else String(oc.action_economy.last_round_end().get("path_tag", "none")),
|
||||
"mines_hit_last": int(mines_hits["last"]),
|
||||
"mines_hit_total": int(mines_hits["total"]),
|
||||
}
|
||||
for u in oc._enemy_system.query_alive("enemy"):
|
||||
# Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行
|
||||
# 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性
|
||||
# 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机
|
||||
# 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏)
|
||||
for sid in oc._enemy_system.check_trigger_unlocks(u, facts):
|
||||
var srow_u: Dictionary[Variant, Variant] = oc.db.combat.skill.row_by_id(sid)
|
||||
oc._emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"},
|
||||
oc.round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
|
||||
0, {"skill_id": sid})
|
||||
# enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射,
|
||||
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值)
|
||||
var pct_v := 0
|
||||
for pe in (u.get("pool", []) as Array):
|
||||
var pe_d: Dictionary[Variant, Variant] = pe
|
||||
if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"):
|
||||
pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0))
|
||||
break
|
||||
# target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记,
|
||||
# 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value)
|
||||
var eht_hits := oc._fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v,
|
||||
"is_boss": String(oc._facts.encounter.get("node_type", "")) == "boss"})
|
||||
for h in eht_hits:
|
||||
if String(h.get("kind", "")) == "target_attack_bonus":
|
||||
u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0))
|
||||
var entries: Array[Variant] = oc._enemy_system.rollable_skills(u, facts)
|
||||
if oc._enemy_system.basic_streak(u) >= pity_m:
|
||||
var specials: Array[Variant] = oc._enemy_system.special_skills(u, facts)
|
||||
if not specials.is_empty():
|
||||
entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合
|
||||
var stream: RandomNumberGenerator = oc.rng.open_stream("behavior_roll")
|
||||
oc._enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream))
|
||||
oc._enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记)
|
||||
mines_hits["last"] = mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新)
|
||||
mines_hits["round"] = 0
|
||||
@@ -0,0 +1 @@
|
||||
uid://ce07smbypdcyj
|
||||
+76
-264
@@ -1,7 +1,8 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 组合根:唯一的模式流转与 core↔view 接线处(垂直切片验证:线性第一关承载元进度循环)。
|
||||
## db.hero 运行期只读,加成经 runner.set_hero_override / debug_session 生效值入战斗。
|
||||
## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 _open_modal/_close_modal。
|
||||
## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 ui.modals.flow(注册面编排口)。
|
||||
## 界面句柄与层清单在 ui 注册面(ui_registry.gd)——屏壳/战区/模态三域句柄与 LAYER_CONTRACT 单点宿主。
|
||||
## 局态全在 run_scope.gd 节点,禁在屏/视图侧持有局态引用;autoload 节点路径常量单点在 AUTOLOAD_PATH_*。
|
||||
extends Control
|
||||
|
||||
@@ -15,10 +16,8 @@ const AUTOLOAD_PATH_META := "/root/MetaProgression"
|
||||
|
||||
const UiAssembler := preload("res://presentation/ui_assembler.gd")
|
||||
const Narrator := preload("res://presentation/battle_narrator.gd")
|
||||
const ModalStack := preload("res://presentation/modal_stack.gd")
|
||||
const TuneSimController := preload("res://presentation/tune_sim_controller.gd")
|
||||
const DebugSessionScript := preload("res://presentation/debug_session.gd")
|
||||
const RowPacer := preload("res://presentation/row_pacer.gd") # 结算行排播泵
|
||||
const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互
|
||||
const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互
|
||||
const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影
|
||||
@@ -31,33 +30,6 @@ const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn")
|
||||
const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn")
|
||||
const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn")
|
||||
|
||||
# ==================== 视觉层合同(唯一事实源——树序=登记序=z 序;改序只改此表) ====================
|
||||
# kind: shell=屏壳容器(成员随壳不占独立树位;level_screen 例外另占登记位)/modal=模态层(入登记册,
|
||||
# tags 随登记)/state=全屏态(占树位不入登记册)/node=非视觉节点(占树位不入登记册)。
|
||||
# host=宿住屏壳(树序随宿主、登记序按本合同);shell=模态成员的屏壳(成员与屏壳同槽:屏壳占该树位、
|
||||
# 成员占该登记位)。落位=_apply_layer_contract(全仓唯一层序落位点,add_child 之后统一排序+登记)。
|
||||
const LAYER_CONTRACT: Array[Dictionary] = [
|
||||
{"key": "adventure_screen", "kind": "shell"},
|
||||
{"key": "menu_screen", "kind": "shell"},
|
||||
{"key": "row_pacer", "kind": "node"},
|
||||
{"key": "legend_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "node_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "level_screen", "kind": "modal", "tags": [ModalStack.TAG_SCREEN_STATE], "host": "adventure_screen"},
|
||||
{"key": "curio_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "end_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "shop_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "camp_overlay", "kind": "modal", "tags": [], "shell": "camp_screen"},
|
||||
{"key": "settings_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "status_panel", "kind": "modal", "tags": []},
|
||||
{"key": "career_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "save_select_overlay", "kind": "modal", "tags": []}, # 选档界面(营地交互重做卡③——登记生涯同档)
|
||||
{"key": "inter_overlay", "kind": "state", "tags": []},
|
||||
{"key": "confirm_dialog", "kind": "modal", "tags": []},
|
||||
{"key": "debug_overlay", "kind": "modal", "tags": [ModalStack.TAG_NON_LOCKING]},
|
||||
{"key": "debug_badge", "kind": "float"}, # 调试入口全局浮层——末位=最顶绘制序;
|
||||
# float 类不注册 ModalStack(open() 遮挡记账只隐藏登记层)→ 任何 UI 打开都不遮它
|
||||
]
|
||||
|
||||
var db
|
||||
var run_scope = null # 会话状态承载(run_scope.gd;_ready 即建闲时态,永非 null)
|
||||
# SaveManager/MetaProgression 升格 autoload——成员名保留为「autoload 读视图」
|
||||
@@ -71,99 +43,24 @@ var meta: MetaProgressionCore:
|
||||
return get_node(AUTOLOAD_PATH_META)
|
||||
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
|
||||
var _debug_entry := false # 调试会话门控(缺省 false=发布态:角标恒隐、浮窗无 UI 开启口;沿革查 git log)
|
||||
var _seed_edit: LineEdit
|
||||
var confirm_dialog: ConfirmDialog # 通用确认弹窗(消费方编排——现行消费方=选档删档确认;新冒险互斥确认已随卡②主菜单撤出,归卡④大门)
|
||||
var ui: UiRegistry # 界面注册面(ui_registry.gd——界面句柄/层序合同/模态编排句柄的单点宿主)
|
||||
var _confirm_consumer := "" # 确认弹窗消费方状态量(契约 3 编排——open 前设、分派后清)
|
||||
var _confirm_consumer_slot := 0 # 消费方附随槽号(delete_slot 消费方专用)
|
||||
var curio_overlay: CurioPickOverlay # 提升三选一覆盖层(level-clear.md v2 选择面承载;奖励节点共用)
|
||||
var _start_overlay: Control
|
||||
var _end_overlay: BattleEndOverlay
|
||||
var _inter_overlay: Control
|
||||
var _inter_label: Label
|
||||
var level_screen: LevelMapScreen # 关卡画面句柄(指向 adventure_screen.level_screen)
|
||||
var _battle_root: Control # 战斗屏宿主句柄(指向 adventure_screen.battle_root)
|
||||
|
||||
# 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI)
|
||||
var _debug = null
|
||||
|
||||
# 视图(views/)——battle.md 三区(节点链进度
|
||||
# 由关卡画面本体选路覆盖层承载)
|
||||
var stage: BattleStageView # B 区(含 log_view 明细带)——句柄指向 adventure_screen
|
||||
var log_view: BattleLogView
|
||||
var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区)
|
||||
var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx)
|
||||
var board_view: BoardView # 探针兼容别名(= board_zone.board_view)
|
||||
var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar)
|
||||
var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下)
|
||||
|
||||
# 三屏壳实例(屏成员经本类句柄暴露;探针挂载假设 main.X 一级路径)
|
||||
var _menu_screen = null # MenuScreen(开始菜单屏壳)
|
||||
var _camp_screen = null # CampScreen(营地屏壳)
|
||||
var _adventure_screen = null # AdventureScreen(冒险屏壳——内含关卡地图/战斗两容器)
|
||||
|
||||
# 拆件后的组合根持有面(职责见各类头注;装配见 _build_ui)
|
||||
var _row_pacer = null # 结算行排播泵(row_pacer.gd——本类只入队/排空/读队列)
|
||||
var _treasure_ui = null # 秘宝两步交互(treasure_interact_controller.gd——本类只接线与转发意图)
|
||||
var _bonus = null # 三选一与替换子交互(curio_interact_controller.gd——本类只接线与回调注入)
|
||||
var _camp_pres = null # 营地/商店投影(camp_shop_presenter.gd——本类只接线、探针缝保缝转发)
|
||||
|
||||
# 排播队列只读委托面(探针/测试读 main._row_queue——与 RowPacer 队列共享同一 Array 引用)
|
||||
var _row_queue: Array[Variant]:
|
||||
get:
|
||||
return _row_pacer.pending_rows()
|
||||
|
||||
# 模态 overlay(z 序 = 登记序;pattern 6 层级纪律)
|
||||
var legend_overlay: LegendOverlay
|
||||
var node_overlay: NodeOverlay
|
||||
var shop_overlay: ShopOverlay # 商店覆盖层(商店房节点兑付呈现)
|
||||
var camp_overlay: CampOverlay # 营地覆盖层句柄(指向 camp_screen.camp_overlay)
|
||||
var settings_store # 系统级设置独立存储(音量/全屏/减动效)
|
||||
var settings_overlay: SettingsOverlay # 设置覆盖层(最小设置面)
|
||||
var status_panel: StatusPanel # 局内状态面板·只读(钥石余额+营地三柜台进度查看;登记设置层同档)
|
||||
var career_overlay: CareerOverlay # 生涯页·只读全屏页(跨局汇总六类九项;登记营地同档〔设置层旁、确认弹窗之前〕)
|
||||
var save_select_overlay: SaveSelectOverlay # 选档界面·3 存档位(新档/进档/删档——营地交互重做卡③;登记生涯同档)
|
||||
var debug_overlay: DebugOverlay # 调试浮窗(种子/数值调整/批量模拟/两层调试入口收一)
|
||||
var debug_badge: Button # 调试浮层入口角标(UI 最顶层浮层——LAYER_CONTRACT 末位 float 类,任何 UI 不遮挡)
|
||||
|
||||
var _modal_flow = null # 模态编排纯逻辑类(modal_flow.gd——依赖仅 ModalStack+查询 Callable)
|
||||
var _modals = null # 模态层级状态机(=_modal_flow.stack——探针 _list 登记序断言缝保留)
|
||||
var _tune_sim = null # 调试浮窗批量模拟控制器(tune_sim_controller.gd)
|
||||
|
||||
# 结算框挂起态属性化(探针/冒烟直读写接缝语义零变化——读=armed,写=arm/disarm;计时器随会话住
|
||||
# run_scope.settlement_stop_timer)
|
||||
var _settlement_stop_pending: bool:
|
||||
get:
|
||||
return run_scope.settlement_stop_timer.is_armed() if run_scope != null else false
|
||||
set(v):
|
||||
if run_scope == null:
|
||||
return
|
||||
if v:
|
||||
run_scope.settlement_stop_timer.arm()
|
||||
else:
|
||||
run_scope.settlement_stop_timer.disarm()
|
||||
|
||||
# 锁期点击重放(v3.1):延迟一帧重入唯一动作通道——此刻队列空锁已解,按普通动作受理
|
||||
#(局面在演播期无变化:逻辑先行、演播只是回放——点击语义保真;_row_queue=RowPacer 委托面,
|
||||
# 排播泵本体在 row_pacer.gd)。
|
||||
var _deferred_modal: Control: # 演播后开窗挂起只读视图(t17_defer_probe 挂起态断言缝;本体归 _modal_flow)
|
||||
get:
|
||||
return _modal_flow._deferred if _modal_flow != null else null
|
||||
|
||||
|
||||
# 演播后排窗(伤害过程演完才切结算窗口):排播队列在播=挂起,演播完边沿
|
||||
# (_on_pump_ended 派发)再开——旧路径 _open_modal 前置 flush 会把击杀轮的伤害/死亡演出
|
||||
# 同帧倒掉(「直接跳战斗胜利」缺陷本体)。队列空=立即开(行为同旧路径)。
|
||||
func _open_modal_after_playback(m: Control) -> void:
|
||||
_modal_flow.open_after_playback(m) # 演播后排窗挂起编排归 ModalFlow
|
||||
|
||||
|
||||
# 演播完边沿派发(row_pacer.pump_ended 注入口——先开挂起 modal,后锁期点击重放):
|
||||
func _on_pump_ended() -> void:
|
||||
_modal_flow.pump_ended() # 派发编排归 ModalFlow(重放经注入 Callable 回本类)
|
||||
|
||||
|
||||
func _replay_locked_click() -> void:
|
||||
if run_scope.locked_click.is_empty() or not _row_queue.is_empty():
|
||||
if run_scope.locked_click.is_empty() or not ui.screens.row_pacer.pending_rows().is_empty():
|
||||
return
|
||||
var click: Dictionary[Variant, Variant] = run_scope.locked_click
|
||||
run_scope.locked_click = {}
|
||||
@@ -173,7 +70,7 @@ func _replay_locked_click() -> void:
|
||||
# 会话生命周期:_ready 建闲时态(永非 null——全组合根零空判责),
|
||||
# 开局/续玩/调试两层各建新会话,旧会话整节点 queue_free()(帧末回收;新节点先挂树无真空窗)。
|
||||
func _begin_session(adventure_session: bool) -> void:
|
||||
_row_pacer.flush() # 会话更替即清队(待播行不住局态——机制保证,不靠时序约定)
|
||||
ui.screens.row_pacer.flush() # 会话更替即清队(待播行不住局态——机制保证,不靠时序约定)
|
||||
if run_scope != null:
|
||||
run_scope.queue_free()
|
||||
run_scope = RunScope.new()
|
||||
@@ -194,10 +91,11 @@ func _ready() -> void:
|
||||
settings_store = SettingsStore.new()
|
||||
settings_store.load_settings()
|
||||
settings_store.apply()
|
||||
ui = UiRegistry.new() # 注册面先于装配建(_build_ui 八步向容器登记——装配时序零变化)
|
||||
_build_ui()
|
||||
_begin_session(false) # 闲时会话(gp_ledger 等探针注入面自 _ready 起恒在——ui_probe 合成段时序)
|
||||
_start_overlay.visible = true
|
||||
debug_badge.visible = _debug_entry # 浮层角标可见性须门控落定后赋值(构建/连接在 _build_ui);_debug_entry 缺省 false=发布态恒隐
|
||||
ui.screens.start_overlay.visible = true
|
||||
ui.screens.debug_badge.visible = _debug_entry # 浮层角标可见性须门控落定后赋值(构建/连接在 _build_ui);_debug_entry 缺省 false=发布态恒隐
|
||||
# boot 菜单优先——启动不自动开浮窗(开始屏不得被调试面板盖住);
|
||||
# 调试入口=UI 最顶层全局浮层角标(LAYER_CONTRACT 末位 float 类,任何 UI 不遮挡)
|
||||
|
||||
@@ -206,94 +104,13 @@ func _ready() -> void:
|
||||
|
||||
func _build_ui() -> void:
|
||||
MainUiBuild.build(self)
|
||||
debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口
|
||||
ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口
|
||||
|
||||
|
||||
func _fatal_screen(msg: String) -> void:
|
||||
MainUiBuild.fatal_screen(self, msg)
|
||||
|
||||
|
||||
# ==================== 模态 overlay 层级管理(pattern 6 / 缺陷⑤——状态机归 modal_stack.gd) ====================
|
||||
|
||||
func _register_modal(m: Control, tags: Array = []) -> void:
|
||||
# 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后
|
||||
# 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors
|
||||
# ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律)
|
||||
m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_modal_flow.register(m, tags) # 排除组随层登记(non_locking=调试浮窗/screen_state=关卡地图屏)
|
||||
|
||||
|
||||
# 合同层寻址(key→节点;纯查表不含层序语义——层序唯一事实源=LAYER_CONTRACT)
|
||||
func _layer_node(key: String) -> Node:
|
||||
match key:
|
||||
"adventure_screen": return _adventure_screen
|
||||
"menu_screen": return _menu_screen
|
||||
"row_pacer": return _row_pacer
|
||||
"legend_overlay": return legend_overlay
|
||||
"node_overlay": return node_overlay
|
||||
"level_screen": return level_screen
|
||||
"curio_overlay": return curio_overlay
|
||||
"end_overlay": return _end_overlay
|
||||
"shop_overlay": return shop_overlay
|
||||
"camp_screen": return _camp_screen
|
||||
"camp_overlay": return camp_overlay
|
||||
"settings_overlay": return settings_overlay
|
||||
"status_panel": return status_panel
|
||||
"career_overlay": return career_overlay
|
||||
"save_select_overlay": return save_select_overlay
|
||||
"inter_overlay": return _inter_overlay
|
||||
"confirm_dialog": return confirm_dialog
|
||||
"debug_overlay": return debug_overlay
|
||||
"debug_badge": return debug_badge
|
||||
return null
|
||||
|
||||
|
||||
# 合同装配(_build_ui 尾调用):按 LAYER_CONTRACT 序统一落位——直接子层 move_child 到合同槽、
|
||||
# 模态层按合同序登记;宿住层(host)树序随宿主只登记;成员层(shell)与屏壳同槽(屏壳占树位、成员占登记位)。
|
||||
# 本函数是全仓唯一层序落位点(move_child 单口)——改层序只改 LAYER_CONTRACT。
|
||||
func _apply_layer_contract() -> void:
|
||||
var slot := 0
|
||||
for entry: Dictionary in LAYER_CONTRACT:
|
||||
var tags: Array[Variant] = entry.get("tags", [])
|
||||
if entry.has("shell"):
|
||||
var shell: Node = _layer_node(String(entry["shell"]))
|
||||
var member: Node = _layer_node(String(entry["key"]))
|
||||
if shell != null:
|
||||
move_child(shell, slot)
|
||||
slot += 1
|
||||
if member != null:
|
||||
_register_modal(member as Control, tags)
|
||||
else:
|
||||
push_error("[合同装配] 层缺失: %s" % String(entry["key"]))
|
||||
continue
|
||||
var node: Node = _layer_node(String(entry["key"]))
|
||||
if node == null:
|
||||
push_error("[合同装配] 层缺失: %s" % String(entry["key"]))
|
||||
continue
|
||||
if entry.has("host"):
|
||||
_register_modal(node as Control, tags)
|
||||
continue
|
||||
move_child(node, slot)
|
||||
slot += 1
|
||||
if String(entry["kind"]) == "modal":
|
||||
_register_modal(node as Control, tags)
|
||||
|
||||
|
||||
func _open_modal(m: Control) -> void:
|
||||
_modal_flow.open(m) # 挂起兜底/排播排空/遮挡记账/开侧同步编排归 ModalFlow
|
||||
|
||||
func _close_modal(m: Control) -> void:
|
||||
_modal_flow.close(m) # 关闭恢复+关侧同步归 ModalFlow(同上)
|
||||
|
||||
func _on_modal_closed(m: Control) -> void:
|
||||
_close_modal(m)
|
||||
|
||||
|
||||
func _input_locked() -> bool:
|
||||
# 模态/非战斗期棋盘输入锁(判据编排归 ModalFlow.locks_input:互斥域随层登记
|
||||
# [non_locking=调试浮窗]+ 三例外经查询 Callable 注入;本函数留探针缝)
|
||||
return _modal_flow.locks_input()
|
||||
|
||||
# ==================== 冒险模式入口(唯一试玩入口)====================
|
||||
|
||||
func _start_adventure_from_menu() -> void:
|
||||
@@ -301,7 +118,7 @@ func _start_adventure_from_menu() -> void:
|
||||
# 弹窗本体与两分支逻辑归卡④大门承接)。本函数为探针直调中间态保留(mn10——冒险段探针
|
||||
# 直调位点逐个登记实现报告「已知中间态」;卡④到位后由大门两分支换代)。无激活态写盘拒绝
|
||||
# 由卡① sv3 承接(结算不落盘)。
|
||||
_start_overlay.visible = false
|
||||
ui.screens.start_overlay.visible = false
|
||||
_start_adventure()
|
||||
|
||||
|
||||
@@ -309,16 +126,16 @@ func _start_adventure_from_menu() -> void:
|
||||
#(确认才删、取消只关窗——裁决 3)。"new_adventure" 消费方已随卡② 主菜单确认链摘除退役
|
||||
#(mn6/V5——主菜单零确认残留),分派机制保留(卡④ 到位接入第三消费方不返工)。
|
||||
func _on_confirm_accepted() -> void:
|
||||
_close_modal(confirm_dialog) # 模态栈单口
|
||||
ui.modals.flow.close(ui.modals.confirm_dialog) # 模态栈单口
|
||||
match _confirm_consumer:
|
||||
"delete_slot":
|
||||
save_manager.slots.delete_slot(_confirm_consumer_slot)
|
||||
save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态)
|
||||
ui.modals.save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态)
|
||||
_confirm_consumer = ""
|
||||
|
||||
|
||||
func _on_confirm_declined() -> void:
|
||||
_close_modal(confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删);关侧恢复归 ModalFlow
|
||||
ui.modals.flow.close(ui.modals.confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删);关侧恢复归 ModalFlow
|
||||
_confirm_consumer = ""
|
||||
|
||||
|
||||
@@ -334,16 +151,16 @@ func _resume_from_menu() -> void:
|
||||
if not save_manager.has_breakpoint():
|
||||
return
|
||||
var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot()
|
||||
_close_modal(debug_overlay) # 开冒险路径显式关浮窗(续玩同口径)
|
||||
_start_overlay.visible = false
|
||||
ui.modals.flow.close(ui.modals.debug_overlay) # 开冒险路径显式关浮窗(续玩同口径)
|
||||
ui.screens.start_overlay.visible = false
|
||||
_begin_session(true)
|
||||
_wire_adventure()
|
||||
run_scope.level_active = true
|
||||
_sync_status_entry() # 续玩恢复点亮状态面板入口角标
|
||||
level_screen.set_abandon_available(true)
|
||||
log_view.clear()
|
||||
ui.modals.level_screen.set_abandon_available(true)
|
||||
ui.battle.log_view.clear()
|
||||
logger.start_session(int(bp.get("master_seed", 0)))
|
||||
run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct())
|
||||
run_scope.adventure.set_hero_override(ui.modals.debug_overlay.atk_pct(), ui.modals.debug_overlay.hp_pct())
|
||||
# 同调三键随续玩下传(build_adventure_params 统一构造,与开新局同源同口径)
|
||||
if not run_scope.adventure.resume_adventure(bp, build_adventure_params(save_manager, meta,
|
||||
{"treasure_unlocked": _treasure_ui.unlocked_set()})):
|
||||
@@ -379,16 +196,16 @@ static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref:
|
||||
# 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接;
|
||||
# 信号连接只在组合根)
|
||||
func _start_adventure() -> void:
|
||||
_close_modal(debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开)
|
||||
ui.modals.flow.close(ui.modals.debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开)
|
||||
_begin_session(true)
|
||||
_wire_adventure()
|
||||
run_scope.level_active = true
|
||||
_sync_status_entry() # 进对局点亮状态面板入口角标
|
||||
level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期)
|
||||
log_view.clear()
|
||||
ui.modals.level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期)
|
||||
ui.battle.log_view.clear()
|
||||
var seed_val := _seed_value()
|
||||
logger.start_session(seed_val)
|
||||
run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct())
|
||||
run_scope.adventure.set_hero_override(ui.modals.debug_overlay.atk_pct(), ui.modals.debug_overlay.hp_pct())
|
||||
# 永久强化注入:冒险开始参数面——meta 现算 override 显式传参(全零 = 白板)
|
||||
# 开局参数面(同调三键同源构造——确定性承载=固定种子+参数档,首局教学图由种子域三分判定给 42)
|
||||
if not run_scope.adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta,
|
||||
@@ -435,33 +252,33 @@ func _on_inter_next_pressed() -> void:
|
||||
func _show_board_fx_for_node(node_type: String = "") -> void:
|
||||
# Boss 遭遇变体:紫赤影子爬棋盘一次性占位演出(§2.6)
|
||||
if node_type == "boss":
|
||||
board_zone.fx.play_boss_shadow()
|
||||
ui.battle.board_zone.fx.play_boss_shadow()
|
||||
|
||||
|
||||
func _on_battle_finished(payload: Dictionary) -> void:
|
||||
# 仅调试模式接线(冒险的关卡结束归 AdventureRunner._on_level_finished——大奖励/冒险终局分派)
|
||||
var result: String = String(payload.get("result", ""))
|
||||
_sync_settlement_visuals() # → 全关(battle_over 反馈面)
|
||||
HudPresenter.battle_end_feedback(stage, ap_bar, true) # 灰化/idle 回正/RAP 清零/状态全清——HUD 单通道
|
||||
HudPresenter.battle_end_feedback(ui.battle.stage, ui.battle.board_zone.ap_bar, true) # 灰化/idle 回正/RAP 清零/状态全清——HUD 单通道
|
||||
var decision: Dictionary[Variant, Variant] = _debug.on_battle_finished(payload)
|
||||
match String(decision.outcome):
|
||||
"next_layer":
|
||||
_inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [
|
||||
ui.screens.inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [
|
||||
int(decision.layer), int(decision.carry_hp), int(db.combat.hero.max_hp)]
|
||||
_inter_overlay.visible = true
|
||||
ui.screens.inter_overlay.visible = true
|
||||
_refresh_presentations()
|
||||
return
|
||||
"level_end":
|
||||
_end_overlay.open_victory(Narrator.battle_end_text("victory", true),
|
||||
"返回", board_view.get_global_rect(), _end_victory_opts(payload))
|
||||
ui.modals.end_overlay.open_victory(Narrator.battle_end_text("victory", true),
|
||||
"返回", ui.battle.board_zone.board_view.get_global_rect(), _end_victory_opts(payload))
|
||||
"standalone_end":
|
||||
if result == "victory":
|
||||
_end_overlay.open_victory(Narrator.battle_end_text("victory", false),
|
||||
"回到遭遇选择", board_view.get_global_rect(), _end_victory_opts(payload))
|
||||
ui.modals.end_overlay.open_victory(Narrator.battle_end_text("victory", false),
|
||||
"回到遭遇选择", ui.battle.board_zone.board_view.get_global_rect(), _end_victory_opts(payload))
|
||||
else:
|
||||
_end_overlay.open_defeat(Narrator.battle_end_text("defeat", false),
|
||||
"返回", board_view.get_global_rect())
|
||||
_open_modal_after_playback(_end_overlay)
|
||||
ui.modals.end_overlay.open_defeat(Narrator.battle_end_text("defeat", false),
|
||||
"返回", ui.battle.board_zone.board_view.get_global_rect())
|
||||
ui.modals.flow.open_after_playback(ui.modals.end_overlay)
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
@@ -499,22 +316,17 @@ func _on_step_row(row: Dictionary) -> void:
|
||||
# 清图奖励可感知反馈(挖完全图须有奖励感——clear_blast 兑付对敌伤害
|
||||
# 零可见,toast 桥接归组合根呈现层;零兑付(未生效)不弹安静留明细带)
|
||||
if String(row.get("source_class", "")) == "clear_reward" and String(row.get("type", "")) != "reward_ineffective":
|
||||
board_zone.toast.show_toast("清图奖励:%s" % String(row.get("display", "")).replace("(清图奖励)", ""), "info")
|
||||
ui.battle.board_zone.toast.show_toast("清图奖励:%s" % String(row.get("display", "")).replace("(清图奖励)", ""), "info")
|
||||
# 排播入口:减动效=延迟归零同帧直落(行呈现不跳过);常态入队按段阶梯播放
|
||||
if UiKit.motion_scale < 1.0:
|
||||
_row_pacer.consume_now(row)
|
||||
ui.screens.row_pacer.consume_now(row)
|
||||
return
|
||||
_row_pacer.enqueue(row)
|
||||
ui.screens.row_pacer.enqueue(row)
|
||||
|
||||
|
||||
# 段间延迟取数面(减动效双态断言用——pacing_queue_test 直读;委托 row_pacer 同名面)
|
||||
func pace_delay_for(prev_seg: String, next_seg: String) -> float:
|
||||
return _row_pacer.pace_delay_for(prev_seg, next_seg)
|
||||
|
||||
|
||||
# 强制排空(模态开启前调用——明细回看/停点覆盖层/终局面所见明细带恒完整;pacing_queue_test 直读)
|
||||
func _flush_row_queue() -> void:
|
||||
_row_pacer.flush()
|
||||
return ui.screens.row_pacer.pace_delay_for(prev_seg, next_seg)
|
||||
|
||||
|
||||
func _on_round_started(ri: int) -> void:
|
||||
@@ -522,10 +334,10 @@ func _on_round_started(ri: int) -> void:
|
||||
#(view 内判减动效=静态化变体)。减动效快路=分隔行同帧直落(不跳过——consume_now 复用
|
||||
# _consume_row 轮首分隔行分支)。
|
||||
if UiKit.motion_scale < 1.0:
|
||||
_row_pacer.consume_now({"__round_line": ri})
|
||||
ui.screens.row_pacer.consume_now({"__round_line": ri})
|
||||
else:
|
||||
_row_pacer.enqueue({"__round_line": ri})
|
||||
settlement_fx.play_round_banner(ri)
|
||||
ui.screens.row_pacer.enqueue({"__round_line": ri})
|
||||
ui.battle.settlement_fx.play_round_banner(ri)
|
||||
# 秘宝呈现同步:每战首轮装配链
|
||||
# 显式重算〔🔴 契约复原语义:下一战装配链重算即含「未装配→写 0」复原〕。
|
||||
# 挂点时序依据:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)。
|
||||
@@ -534,7 +346,7 @@ func _on_round_started(ri: int) -> void:
|
||||
_sync_treasure_glow()
|
||||
# 巢穴预告呈现(装板报数量读数口消费:≥0=本战装配命中,toast 一次性提示)
|
||||
if run_scope.settlement != null and int(run_scope.settlement.nest_preview_count) >= 0:
|
||||
board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(run_scope.settlement.nest_preview_count), "info")
|
||||
ui.battle.board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(run_scope.settlement.nest_preview_count), "info")
|
||||
# 开战横幅——整图地形变异预告:首轮快照 board_variants
|
||||
# 非空才发射(Boss 图恒带一型=替换制;普通图空快照零发射);轮横幅下方叠行,view 内判减动效。
|
||||
if ri == 1 and run_scope.settlement != null:
|
||||
@@ -542,7 +354,7 @@ func _on_round_started(ri: int) -> void:
|
||||
if not bvs.is_empty():
|
||||
var vlines: Array[String] = HudPresenter.board_variant_lines(db, bvs)
|
||||
if not vlines.is_empty():
|
||||
settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines))
|
||||
ui.battle.settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines))
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
@@ -552,19 +364,19 @@ func _on_board_changed() -> void:
|
||||
run_scope.settlement = run_scope.runner.current_battle()
|
||||
if run_scope.settlement == null:
|
||||
return
|
||||
board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper)
|
||||
ui.battle.board_zone.board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper)
|
||||
_check_board_hints()
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
# 秘宝视野呈现桥:阔视野带 light_radius_bonus
|
||||
# 装配参数合计显式写给 board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter
|
||||
# 装配参数合计显式写给 ui.battle.board_zone.board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter
|
||||
# 内 refresh 全板重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2:
|
||||
# 只写呈现态参数,minesweeper 结算零触碰;off 态(未装配)写 0=缺省基线。
|
||||
func _sync_treasure_glow() -> void:
|
||||
if run_scope.settlement == null or board_view == null:
|
||||
if run_scope.settlement == null or ui.battle.board_zone.board_view == null:
|
||||
return
|
||||
board_view.set_fog_glow_bonus(run_scope.settlement.attune_param_sum("light_radius_bonus"))
|
||||
ui.battle.board_zone.board_view.set_fog_glow_bonus(run_scope.settlement.attune_param_sum("light_radius_bonus"))
|
||||
|
||||
|
||||
# ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ----
|
||||
@@ -575,10 +387,10 @@ func _check_board_hints() -> void:
|
||||
return
|
||||
# 零合法板(棋盘生成 EC5 降级)诚实提示——非静默(toast,非模态)
|
||||
if board.illegal_product:
|
||||
board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn")
|
||||
ui.battle.board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn")
|
||||
# 接受最优降级(有合法板但无一达标)——降级标记独立于非法产物标记,同走诚实告知
|
||||
elif board.degraded_product:
|
||||
board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn")
|
||||
ui.battle.board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn")
|
||||
|
||||
|
||||
func _on_hover_forbidden(_cell: Vector2i) -> void:
|
||||
@@ -594,7 +406,7 @@ func _on_chord_attempted(_cell: Vector2i) -> void:
|
||||
func _show_hint(text: String, hint_id: String) -> void:
|
||||
run_scope.hints_shown[hint_id] = true
|
||||
logger.log_ux("ux_hint_shown", {"hint_id": hint_id})
|
||||
board_zone.toast.show_toast(text, "info")
|
||||
ui.battle.board_zone.toast.show_toast(text, "info")
|
||||
|
||||
|
||||
# ---- 节点覆盖层"继续"(奖励/恢复点兑付确认)与节点图选路覆盖层 ----
|
||||
@@ -706,15 +518,15 @@ func _on_wallet_changed(event: String, gp: int, extra: Dictionary)-> void:
|
||||
|
||||
func _on_board_action(cell: Vector2i, kind: String) -> void:
|
||||
# 玩家动作 → 核心写调用的唯一通道(模态/无战斗拒绝在此收口)
|
||||
if _input_locked():
|
||||
if ui.modals.flow.locks_input():
|
||||
return
|
||||
# 结算演播期棋盘冻结(v3 口径——不可点/不可移/不可挖):
|
||||
# 锁面=排播队列活跃(纯队列判据;排播泵尾迹窗口不可作演播中标志——见 row_pacer._process 注)。
|
||||
# 队列排空即解锁、无状态残留;HUD 按钮不在锁面(停手/明细回看各自通路,开启前
|
||||
# _flush_row_queue 自行排空)。
|
||||
# 开层前排空钩子(flow.flush_rows——模态开启前明细带恒完整)。
|
||||
# **点击不丢弃**(v3.1——「卡死」体感根因):锁期点击缓存最后一次,演播完边沿
|
||||
# (_process 泵 _was_pumping 分支)自动重放——玩家操作零丢失、体感=「演播一停动作就生效」。
|
||||
if not _row_queue.is_empty():
|
||||
if not ui.screens.row_pacer.pending_rows().is_empty():
|
||||
run_scope.locked_click = {"cell": cell, "kind": kind}
|
||||
return
|
||||
# 秘宝选中态劫持(两步交互第二步):点格=使用目标拾取,不落挖格;
|
||||
@@ -734,22 +546,22 @@ func _on_board_action(cell: Vector2i, kind: String) -> void:
|
||||
# settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被
|
||||
# runner 置空,清图演出判定须先判 settlement(重入隐患守卫)
|
||||
if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper.current_board() != null and run_scope.settlement.minesweeper.clear_map_cleared():
|
||||
board_zone.fx.play_clear_burst()
|
||||
ui.battle.board_zone.fx.play_clear_burst()
|
||||
if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper != null:
|
||||
# 挖掘演出:挥铲三帧 + 角色弹跳——触发=格子点开那一刻
|
||||
#(settle_batch 同步受理即状态已变,动画纯表现与 refresh 同拍零竞态;
|
||||
# 挖掘不移动英雄,出手方向 = 目标格−英雄格,圆心=英雄持握手位)
|
||||
var hero_cell: Vector2i = run_scope.settlement.minesweeper.hero_pos()
|
||||
board_zone.fx.play_shovel_swing(board_view.hero_hand_point(), cell - hero_cell, board_view.hero_sprite_scale())
|
||||
board_view.play_hero_dig_bounce()
|
||||
ui.battle.board_zone.fx.play_shovel_swing(ui.battle.board_zone.board_view.hero_hand_point(), cell - hero_cell, ui.battle.board_zone.board_view.hero_sprite_scale())
|
||||
ui.battle.board_zone.board_view.play_hero_dig_bounce()
|
||||
elif accepted and kind == "move":
|
||||
board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知
|
||||
board_view.refresh()
|
||||
ui.battle.board_zone.board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知
|
||||
ui.battle.board_zone.board_view.refresh()
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_stop_pressed() -> void:
|
||||
if _input_locked():
|
||||
if ui.modals.flow.locks_input():
|
||||
return
|
||||
# stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济)
|
||||
if run_scope.settlement == null:
|
||||
@@ -758,15 +570,15 @@ func _on_stop_pressed() -> void:
|
||||
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:防静默改判读感欺骗)
|
||||
board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn")
|
||||
ui.battle.board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn")
|
||||
logger.log_ux("ux_reject", {"source": "voided", "sub_reason": "stop_voided"})
|
||||
board_view.refresh()
|
||||
ui.battle.board_zone.board_view.refresh()
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_refresh_confirmed() -> void:
|
||||
# 弃图刷新(挖掘期 Active 当场换图继续挖——D4;RAP 按门槛扣费 · 旧板封印作废,每轮上限读表)
|
||||
if _input_locked():
|
||||
if ui.modals.flow.locks_input():
|
||||
return
|
||||
var ok: bool = false
|
||||
if run_scope.level_active and run_scope.runner != null:
|
||||
@@ -776,7 +588,7 @@ func _on_refresh_confirmed() -> void:
|
||||
logger.log_action("refresh", Vector2i(-1, -1), ok)
|
||||
if ok:
|
||||
logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_snapshot()})
|
||||
board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.run.action_economy.rap_refresh_cost), "gold")
|
||||
ui.battle.board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.run.action_economy.rap_refresh_cost), "gold")
|
||||
else:
|
||||
# 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断)
|
||||
var rap: int = int(run_scope.settlement.presentation_snapshot().get("rap", 0)) if run_scope.settlement != null else 0
|
||||
@@ -784,11 +596,11 @@ func _on_refresh_confirmed() -> void:
|
||||
var result := "rejected_rap" if rap < cost else "rejected_cap"
|
||||
logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_snapshot() + 1})
|
||||
if result == "rejected_rap":
|
||||
board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn")
|
||||
ap_bar.pulse_rap()
|
||||
ui.battle.board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn")
|
||||
ui.battle.board_zone.ap_bar.pulse_rap()
|
||||
else:
|
||||
board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn")
|
||||
board_view.refresh()
|
||||
ui.battle.board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn")
|
||||
ui.battle.board_zone.board_view.refresh()
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
@@ -798,9 +610,9 @@ func _handle_outcome_feedback(outcome: Dictionary) -> void:
|
||||
return
|
||||
logger.log_ux("ux_reject", {"source": String(outcome.get("reject_reason", "")),
|
||||
"sub_reason": String(outcome.get("sub_reason", ""))})
|
||||
board_zone.toast.show_toast(HudPresenter.outcome_text(outcome), "warn")
|
||||
ui.battle.board_zone.toast.show_toast(HudPresenter.outcome_text(outcome), "warn")
|
||||
if String(outcome.get("reject_reason", "")) == "resource_face":
|
||||
ap_bar.pulse_ap()
|
||||
ui.battle.board_zone.ap_bar.pulse_ap()
|
||||
|
||||
|
||||
# ==================== 全屏 States 状态机(挖掘/结算/窗口/终局)====================
|
||||
@@ -816,7 +628,7 @@ func _refresh_used_snapshot() -> int:
|
||||
func _refresh_presentations() -> void:
|
||||
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
|
||||
var refresh_remaining: int = maxi(0, int(db.run.action_economy.refresh_cap_per_round) - _refresh_used_snapshot())
|
||||
HudPresenter.refresh(stage, info_panel, ap_bar, {"settlement": run_scope.settlement, "runner": run_scope.runner, "level_active": run_scope.level_active, "db": db, "refresh_remaining": refresh_remaining})
|
||||
HudPresenter.refresh(ui.battle.stage, ui.battle.info_panel, ui.battle.board_zone.ap_bar, {"settlement": run_scope.settlement, "runner": run_scope.runner, "level_active": run_scope.level_active, "db": db, "refresh_remaining": refresh_remaining})
|
||||
_treasure_ui.refresh_slots() # C 区秘宝槽重查(试炼镐积点进度随挖格刷新悬停浮窗)
|
||||
_sync_settlement_visuals()
|
||||
|
||||
@@ -825,9 +637,9 @@ func _sync_settlement_visuals() -> void:
|
||||
# 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px
|
||||
var settling := _is_settling()
|
||||
if settling and not run_scope.was_settling:
|
||||
settlement_fx.play_round_sweep(board_view.get_global_rect()) # 结算轮光带——时序敏感演出触发,例外留在组合根
|
||||
ui.battle.settlement_fx.play_round_sweep(ui.battle.board_zone.board_view.get_global_rect()) # 结算轮光带——时序敏感演出触发,例外留在组合根
|
||||
run_scope.was_settling = settling
|
||||
HudPresenter.sync_settlement_visuals(stage, board_view, log_view, settling,
|
||||
HudPresenter.sync_settlement_visuals(ui.battle.stage, ui.battle.board_zone.board_view, ui.battle.log_view, settling,
|
||||
HudPresenter.phase_text(run_scope.settlement, run_scope.runner, run_scope.boss_active, db))
|
||||
|
||||
|
||||
@@ -836,13 +648,13 @@ func _is_settling() -> bool:
|
||||
# 判据若只跟逻辑态,演播期棋盘会提前恢复可挖观感=「没意识到在结算演出」感知缺口根源;
|
||||
# 演播期维持去饱和+禁挖光(结算中语义),放完自然回挖掘认知。判据=纯队列(排播泵尾迹
|
||||
# 窗口不可作演播中标志——row_pacer._process 注)。battle 收尾演播例外:
|
||||
# overlay 开启即 _flush_row_queue 排空,视觉随队列空同步交接结算框。
|
||||
# overlay 开启经 flow.flush_rows 钩子排空,视觉随队列空同步交接结算框。
|
||||
# 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透)。
|
||||
if run_scope.settlement == null or not run_scope.settlement.is_battle_live():
|
||||
return not _row_queue.is_empty()
|
||||
return not ui.screens.row_pacer.pending_rows().is_empty()
|
||||
if String(run_scope.settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING:
|
||||
return true
|
||||
return not _row_queue.is_empty()
|
||||
return not ui.screens.row_pacer.pending_rows().is_empty()
|
||||
|
||||
|
||||
# 相位行 —— 承接体见 HudPresenter.phase_text(caller 直接改引)
|
||||
@@ -908,8 +720,8 @@ func _seed_value() -> int:
|
||||
# 重放模板 0 即在此种子下标定〕;置位 true=教学局已收束的非首局走 ③ 随机);
|
||||
# ③非首局空/非法框=引擎全局熵 randi()(Godot 4 启动自播种子,headless 同;不走 rng_service
|
||||
# 封闭流、禁 randomize()/自建 RNG——组合根建种子属引擎熵豁免面,不进玩法 RNG 消费)
|
||||
if _seed_edit.text.is_valid_int():
|
||||
return int(_seed_edit.text)
|
||||
if ui.modals.debug_overlay.seed_edit.text.is_valid_int():
|
||||
return int(ui.modals.debug_overlay.seed_edit.text)
|
||||
if save_manager.meta_state.career_state().first_adventure_done:
|
||||
return randi() # rng-seed-exempt(非首局空框引擎熵——组合根建种子不进玩法 RNG 消费,static_scan S1/S2 标记豁免)
|
||||
return 42
|
||||
@@ -30,10 +30,10 @@ static func _on_adventure_level_started(root: Variant, level_index: int, display
|
||||
# 开场行只显示 atk/max_hp 两值,注记指向不存在的显示轴反成失真),白板局不带
|
||||
var ov: Dictionary[Variant, Variant] = root.meta.hero_override()
|
||||
var boosted: bool = int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 \
|
||||
or root.debug_overlay.atk_pct() > 0 or root.debug_overlay.hp_pct() > 0 \
|
||||
or root.ui.modals.debug_overlay.atk_pct() > 0 or root.ui.modals.debug_overlay.hp_pct() > 0 \
|
||||
or root.run_scope.runner.journal().bonus_total("atk") > 0 \
|
||||
or root.run_scope.runner.journal().bonus_total("max_hp") > 0
|
||||
root.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed),
|
||||
root.ui.battle.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed),
|
||||
root.run_scope.runner.effective_atk(), root.run_scope.runner.effective_max_hp(), boosted))
|
||||
|
||||
# runner 六信号重连(is_connected 守卫——未连即接;守卫失败 push_error=重连恰好一次契约承载)
|
||||
@@ -62,30 +62,30 @@ static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) -
|
||||
root.logger.attach_battle(root.run_scope.settlement)
|
||||
root.run_scope.boss_active = t == "boss"
|
||||
root._show_board_fx_for_node(t)
|
||||
root.log_view.append_bb(root.Narrator.node_banner(root.run_scope.runner, node, t))
|
||||
root.ui.battle.log_view.append_bb(root.Narrator.node_banner(root.run_scope.runner, node, t))
|
||||
elif t == "reward":
|
||||
# 奖励节点三选一(与关卡完成三选一共享同池同权重同上限;CurioPickOverlay 共用面)。
|
||||
# 呈现编排归 curio_interact_controller.present_node_curio。
|
||||
root._bonus.present_node_curio()
|
||||
root.log_view.append_bb(root.Narrator.reward_line())
|
||||
root.ui.battle.log_view.append_bb(root.Narrator.reward_line())
|
||||
elif t == "rest":
|
||||
var healed: int = int(info.get("healed", 0))
|
||||
root._open_modal(root.node_overlay)
|
||||
root.node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))
|
||||
root.log_view.append_bb(root.Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))))
|
||||
root.ui.modals.flow.open(root.ui.modals.node_overlay)
|
||||
root.ui.modals.node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))
|
||||
root.ui.battle.log_view.append_bb(root.Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))))
|
||||
elif t == "shop":
|
||||
# 商店房兑付:到达即开店——商品/货架快照与余额经 runner 只读投影喂入;
|
||||
# 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。
|
||||
# pool 面 = 共享池货架三选一同构卡;legacy 面 = 固定三件(off 对照承载)
|
||||
root._open_modal(root.shop_overlay)
|
||||
root.ui.modals.flow.open(root.ui.modals.shop_overlay)
|
||||
if root.run_scope.runner.shop.is_shop_pool():
|
||||
root.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp))
|
||||
root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行
|
||||
root.shop_overlay.set_refresh_display(root.run_scope.runner.shop.shop_refresh_free_left(), root.run_scope.runner.shop.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop.shop_refresh_cost()) # MC-1
|
||||
root.ui.modals.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp))
|
||||
root.ui.modals.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行
|
||||
root.ui.modals.shop_overlay.set_refresh_display(root.run_scope.runner.shop.shop_refresh_free_left(), root.run_scope.runner.shop.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop.shop_refresh_cost()) # MC-1
|
||||
else:
|
||||
root.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp))
|
||||
root.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载)
|
||||
root.log_view.append_bb(root.Narrator.shop_line(int(root.run_scope.runner.gp)))
|
||||
root.ui.modals.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp))
|
||||
root.ui.modals.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载)
|
||||
root.ui.battle.log_view.append_bb(root.Narrator.shop_line(int(root.run_scope.runner.gp)))
|
||||
root.logger.log_node(node, info, root.run_scope.runner.node_index)
|
||||
root._refresh_presentations()
|
||||
|
||||
@@ -101,7 +101,7 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void:
|
||||
_exit_level_screen(root) # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口
|
||||
root._sync_status_entry() # 离局熄灭状态面板入口角标(防菜单误现)
|
||||
root._sync_settlement_visuals() # → 全关
|
||||
HudPresenter.battle_end_feedback(root.stage, root.ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道
|
||||
HudPresenter.battle_end_feedback(root.ui.battle.stage, root.ui.battle.board_zone.ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道
|
||||
# 存档入账:settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变——
|
||||
# 调试两层不经过本处理器,天然不写档不污染生涯数据)。
|
||||
var detail: Dictionary[String, Variant] = {}
|
||||
@@ -110,30 +110,30 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void:
|
||||
if not detail.is_empty():
|
||||
text += "\n" + root.Narrator.keystone_settle_text(detail, root._bonus.item_name_lookup())
|
||||
if result == "completed":
|
||||
root._end_overlay.open_victory(text, "返回冒险", root.board_view.get_global_rect(),
|
||||
root.ui.modals.end_overlay.open_victory(text, "返回冒险", root.ui.battle.board_zone.board_view.get_global_rect(),
|
||||
{"gp_summary": "本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))}) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账
|
||||
else:
|
||||
root._end_overlay.open_defeat(text, "返回", root.board_view.get_global_rect())
|
||||
root.ui.modals.end_overlay.open_defeat(text, "返回", root.ui.battle.board_zone.board_view.get_global_rect())
|
||||
root.logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷)
|
||||
if root.run_scope.runner != null:
|
||||
root.run_scope.runner.restore_hero_base()
|
||||
root._open_modal_after_playback(root._end_overlay)
|
||||
root.ui.modals.flow.open_after_playback(root.ui.modals.end_overlay)
|
||||
root._refresh_presentations()
|
||||
|
||||
static func _on_battle_end_continue(root: Variant) -> void:
|
||||
# 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石在收束时入存档)
|
||||
root._close_modal(root._end_overlay)
|
||||
root._end_overlay.close()
|
||||
root.ui.modals.flow.close(root.ui.modals.end_overlay)
|
||||
root.ui.modals.end_overlay.close()
|
||||
# 三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面
|
||||
if root._bonus.resume_if_pending(): # 三选一停点在途恢复入口(curio_interact_controller)
|
||||
return
|
||||
# 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现)
|
||||
if root._settlement_stop_pending:
|
||||
root._settlement_stop_pending = false
|
||||
if root.run_scope.settlement_stop_timer.is_armed():
|
||||
root.run_scope.settlement_stop_timer.disarm()
|
||||
_enter_level_screen(root)
|
||||
root.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
|
||||
root.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner)
|
||||
root.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
|
||||
root.ui.modals.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
|
||||
root.ui.modals.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner)
|
||||
root.ui.modals.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
|
||||
root.run_scope.pending_candidates = []
|
||||
root._refresh_presentations()
|
||||
return
|
||||
@@ -141,12 +141,12 @@ static func _on_battle_end_continue(root: Variant) -> void:
|
||||
# 局态封印/账本/模态挂起全复位(局态字段清单见 run_scope.gd)
|
||||
root._debug.reset()
|
||||
root._begin_session(false)
|
||||
root.debug_overlay.reset_sliders()
|
||||
root._start_overlay.visible = true
|
||||
root.ui.modals.debug_overlay.reset_sliders()
|
||||
root.ui.screens.start_overlay.visible = true
|
||||
root._refresh_presentations()
|
||||
|
||||
static func _on_inter_next_pressed(root: Variant) -> void:
|
||||
root._inter_overlay.visible = false
|
||||
root.ui.screens.inter_overlay.visible = false
|
||||
if root.run_scope.is_adventure:
|
||||
# 冒险模式:大奖励屏确认 → 进下一关(AdventureRunner 序列推进——零自动推进的冒险级停点)
|
||||
if root.run_scope.adventure != null and not root.run_scope.adventure.continue_to_next_level():
|
||||
@@ -158,7 +158,7 @@ static func _on_inter_next_pressed(root: Variant) -> void:
|
||||
|
||||
|
||||
static func _on_node_continue(root: Variant) -> void:
|
||||
root._close_modal(root.node_overlay)
|
||||
root.ui.modals.flow.close(root.ui.modals.node_overlay)
|
||||
if root.run_scope.runner != null:
|
||||
# 确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口
|
||||
root.run_scope.runner.continue_after_node()
|
||||
@@ -167,33 +167,33 @@ static func _on_choices_presented(root: Variant, candidates: Array) -> void:
|
||||
# 零自动推进的停点呈现:整幅节点图 + 图上点击选路(战斗胜利/兑付确认统一到达此处)
|
||||
if root.run_scope.runner == null:
|
||||
return
|
||||
root.log_view.append_bb(root.Narrator.route_line(candidates, root.run_scope.runner))
|
||||
root.ui.battle.log_view.append_bb(root.Narrator.route_line(candidates, root.run_scope.runner))
|
||||
# 战斗胜利总结结算(battle.md OQ14):战斗胜利停点先弹 BattleEndOverlay
|
||||
# 胜利形态(金蜡封+GP 增量+总量+判定行+「下一节点」+明细回看),确认后进选路页。判据 = 战斗节点
|
||||
# 的选路停点且战斗已收场(battle 收尾分支 _settlement 置 null 先于本信号;入口/兑付/商店停点
|
||||
# 的当前节点非 battle 型——三态天然分流)。Boss 胜/失败走 level_finished 另路不经此处。
|
||||
if root.run_scope.is_adventure and root.run_scope.runner.current_battle() == null \
|
||||
and String(root.run_scope.runner.current_node().get("type", "")) == "battle" and not candidates.is_empty():
|
||||
root._settlement_stop_pending = true
|
||||
root.run_scope.settlement_stop_timer.arm()
|
||||
root.run_scope.pending_candidates = candidates.duplicate()
|
||||
root._end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点",
|
||||
root.board_view.get_global_rect(),
|
||||
root.ui.modals.end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点",
|
||||
root.ui.battle.board_zone.board_view.get_global_rect(),
|
||||
{"gp_summary": "战利品 GP +%d · 合计 %d" % [root.run_scope.last_gain_amount, int(root.run_scope.runner.gp)]})
|
||||
root._open_modal_after_playback(root._end_overlay)
|
||||
root.ui.modals.flow.open_after_playback(root.ui.modals.end_overlay)
|
||||
root._refresh_presentations()
|
||||
return
|
||||
_enter_level_screen(root)
|
||||
root.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
|
||||
root.level_screen.open_choices(candidates, root.run_scope.runner)
|
||||
root.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
|
||||
root.ui.modals.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
|
||||
root.ui.modals.level_screen.open_choices(candidates, root.run_scope.runner)
|
||||
root.ui.modals.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
|
||||
root._refresh_presentations()
|
||||
|
||||
static func _on_route_chosen(root: Variant, node_id: int) -> void:
|
||||
# 视图已按节点类型定去留——战斗/Boss 出屏(visible=false,此处收栈
|
||||
# 记录一致性+回战斗屏);奖励/恢复/商店本屏保持激活(兑付覆盖层由节点事件域内弹出,模态栈
|
||||
# 打开低层时自动隐藏本屏、关闭自动恢复)
|
||||
if not root.level_screen.visible:
|
||||
root._close_modal(root.level_screen)
|
||||
if not root.ui.modals.level_screen.visible:
|
||||
root.ui.modals.flow.close(root.ui.modals.level_screen)
|
||||
_exit_level_screen(root)
|
||||
if root.run_scope.runner != null and not root.run_scope.runner.pick_next_node(node_id):
|
||||
push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id)
|
||||
@@ -202,15 +202,15 @@ static func _on_route_chosen(root: Variant, node_id: int) -> void:
|
||||
|
||||
static func _enter_level_screen(root: Variant) -> void:
|
||||
root.run_scope.level_screen_active = true
|
||||
root._battle_root.visible = false
|
||||
root._open_modal(root.level_screen)
|
||||
root.ui.screens.battle_root.visible = false
|
||||
root.ui.modals.flow.open(root.ui.modals.level_screen)
|
||||
root._sync_status_entry() # 屏级互斥切换同步(_open_modal 单口不识屏级状态)
|
||||
|
||||
static func _exit_level_screen(root: Variant) -> void:
|
||||
root.run_scope.level_screen_active = false
|
||||
root._battle_root.visible = true
|
||||
if root.level_screen.visible:
|
||||
root._close_modal(root.level_screen)
|
||||
root.ui.screens.battle_root.visible = true
|
||||
if root.ui.modals.level_screen.visible:
|
||||
root.ui.modals.flow.close(root.ui.modals.level_screen)
|
||||
root._sync_status_entry() # 同上
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ static func _on_abandon_requested(root: Variant) -> void:
|
||||
return
|
||||
if not root.run_scope.adventure.abandon():
|
||||
root.logger.log_ux("ux_reject", {"source": "abandon", "sub_reason": "battle_in_flight"})
|
||||
root.board_zone.toast.show_toast("战斗进行中——无法放弃", "warn")
|
||||
root.ui.battle.board_zone.toast.show_toast("战斗进行中——无法放弃", "warn")
|
||||
|
||||
|
||||
# ---- 营地覆盖层与钥石账本(写调用唯一归组合根)----
|
||||
|
||||
@@ -13,7 +13,7 @@ static func _on_diagnose_pressed(root: Variant) -> void:
|
||||
if root.run_scope.settlement != null:
|
||||
ctx["settlement"] = root.run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
|
||||
var path: String = root.logger.dump_diagnosis(ctx)
|
||||
root.board_zone.toast.show_toast("已转储诊断: " + path, "info")
|
||||
root.ui.battle.board_zone.toast.show_toast("已转储诊断: " + path, "info")
|
||||
|
||||
|
||||
|
||||
@@ -21,17 +21,17 @@ static func _show_debug_overlay(root: Variant) -> void:
|
||||
# 门禁:对局中可开;本函数仍是唯一开窗入口,门禁逻辑收函数内:
|
||||
# 棋盘输入锁/状态角标判据排除浮窗,
|
||||
# 遮挡语义=ModalStack 登记制(他模态开时暂隐、关后自动恢复),此处零场景限制
|
||||
if root.debug_overlay == null:
|
||||
if root.ui.modals.debug_overlay == null:
|
||||
return
|
||||
root._open_modal(root.debug_overlay)
|
||||
root.ui.modals.flow.open(root.ui.modals.debug_overlay)
|
||||
_sync_debug_overlay(root)
|
||||
|
||||
|
||||
|
||||
# 调试浮窗角标点击(两屏同款处理器——与状态角标同形制守卫;仍收口 _show_debug_overlay 单口)
|
||||
static func _on_debug_badge_pressed(root: Variant) -> void:
|
||||
var map_on: bool = root.level_screen != null and root.level_screen.visible
|
||||
if (map_on and root._modal_flow.badge_greyed_map()) or (not map_on and root._modal_flow.badge_greyed_battle()):
|
||||
var map_on: bool = root.ui.modals.level_screen != null and root.ui.modals.level_screen.visible
|
||||
if (map_on and root.ui.modals.flow.badge_greyed_map()) or (not map_on and root.ui.modals.flow.badge_greyed_battle()):
|
||||
return
|
||||
_show_debug_overlay(root)
|
||||
|
||||
@@ -40,10 +40,10 @@ static func _on_debug_badge_pressed(root: Variant) -> void:
|
||||
# 浮窗上下文同步(当局数据刷新+对局中按钮禁用单点):调用面=开窗口与模态开合
|
||||
# 单口(凡影响开态/对局态的路径必经其一——与 _sync_status_entry 同款纪律)
|
||||
static func _sync_debug_overlay(root: Variant) -> void:
|
||||
if root.debug_overlay == null:
|
||||
if root.ui.modals.debug_overlay == null:
|
||||
return
|
||||
root.debug_overlay.set_context_locked(root.run_scope.settlement != null or root.run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放)
|
||||
root.debug_overlay.update_run_data(_debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动)
|
||||
root.ui.modals.debug_overlay.set_context_locked(root.run_scope.settlement != null or root.run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放)
|
||||
root.ui.modals.debug_overlay.update_run_data(_debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动)
|
||||
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ static func _debug_run_data(root: Variant) -> Dictionary:
|
||||
|
||||
|
||||
static func _close_debug_overlay(root: Variant) -> void:
|
||||
root._close_modal(root.debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复
|
||||
root.ui.modals.flow.close(root.ui.modals.debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复
|
||||
|
||||
static func _on_tune_apply(root: Variant, atk_pct: int, hp_pct: int) -> void:
|
||||
# 加成无持久态可"落"——各战斗开局经 board_resolvers.debug_hero_override/runner 现算生效(应用=关闭浮窗)
|
||||
@@ -83,11 +83,11 @@ static func _start_debug_levels(root: Variant) -> void:
|
||||
# settlement/编排态=状态机破坏;模拟在跑时 reset_sliders 会作废测量读数(d5 置灰之外的信号绕过闸)
|
||||
if root.run_scope.is_adventure or root.run_scope.settlement != null or root._tune_sim.is_running():
|
||||
return
|
||||
root.debug_overlay.reset_sliders()
|
||||
root._close_modal(root.debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay)
|
||||
root._start_overlay.visible = false
|
||||
root.log_view.clear()
|
||||
root.log_view.append_bb(root.Narrator.debug_open_line(root._seed_value(), 1, true))
|
||||
root.ui.modals.debug_overlay.reset_sliders()
|
||||
root.ui.modals.flow.close(root.ui.modals.debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay)
|
||||
root.ui.screens.start_overlay.visible = false
|
||||
root.ui.battle.log_view.clear()
|
||||
root.ui.battle.log_view.append_bb(root.Narrator.debug_open_line(root._seed_value(), 1, true))
|
||||
root._begin_session(false)
|
||||
if not root._debug.start_debug_levels():
|
||||
root._fatal_screen("战斗启动失败——见输出日志")
|
||||
@@ -100,6 +100,6 @@ static func _on_debug_battle_created(root: Variant, settlement_ref: BattleOrches
|
||||
|
||||
static func _on_debug_battle_ready(root: Variant, settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void:
|
||||
root.run_scope.settlement = settlement_ref
|
||||
root.board_view.rebuild(root.run_scope.settlement.minesweeper.current_board(), root.run_scope.settlement.minesweeper)
|
||||
root.ui.battle.board_zone.board_view.rebuild(root.run_scope.settlement.minesweeper.current_board(), root.run_scope.settlement.minesweeper)
|
||||
root._check_board_hints()
|
||||
root._refresh_presentations()
|
||||
@@ -5,12 +5,12 @@
|
||||
extends RefCounted
|
||||
|
||||
static func _open_settings(root: Variant) -> void:
|
||||
root._start_overlay.visible = false
|
||||
root._open_modal(root.settings_overlay) # 模态栈单口
|
||||
root.ui.screens.start_overlay.visible = false
|
||||
root.ui.modals.flow.open(root.ui.modals.settings_overlay) # 模态栈单口
|
||||
|
||||
static func _on_settings_closed(root: Variant) -> void:
|
||||
root._close_modal(root.settings_overlay) # 模态栈单口
|
||||
root._start_overlay.visible = true
|
||||
root.ui.modals.flow.close(root.ui.modals.settings_overlay) # 模态栈单口
|
||||
root.ui.screens.start_overlay.visible = true
|
||||
|
||||
|
||||
# ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)----
|
||||
@@ -25,22 +25,22 @@ static func _status_data(root: Variant) -> Dictionary:
|
||||
return HudPresenter.status_data(root.db, root.meta, root.run_scope.runner, root._treasure_ui)
|
||||
|
||||
static func _open_status_panel(root: Variant) -> void:
|
||||
root._open_modal(root.status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项)
|
||||
root.status_panel.open_status(_status_data(root))
|
||||
root.ui.modals.flow.open(root.ui.modals.status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项)
|
||||
root.ui.modals.status_panel.open_status(_status_data(root))
|
||||
|
||||
static func _on_status_closed(root: Variant) -> void:
|
||||
root._close_modal(root.status_panel) # 模态栈单口
|
||||
root.ui.modals.flow.close(root.ui.modals.status_panel) # 模态栈单口
|
||||
|
||||
|
||||
|
||||
static func _open_career(root: Variant) -> void:
|
||||
root._start_overlay.visible = false
|
||||
root._open_modal(root.career_overlay) # 模态栈单口
|
||||
root.career_overlay.open_career(HudPresenter.career_data(root.db, root.save_manager, root.meta))
|
||||
root.ui.screens.start_overlay.visible = false
|
||||
root.ui.modals.flow.open(root.ui.modals.career_overlay) # 模态栈单口
|
||||
root.ui.modals.career_overlay.open_career(HudPresenter.career_data(root.db, root.save_manager, root.meta))
|
||||
|
||||
static func _on_career_closed(root: Variant) -> void:
|
||||
root._close_modal(root.career_overlay) # 模态栈单口
|
||||
root._start_overlay.visible = true
|
||||
root.ui.modals.flow.close(root.ui.modals.career_overlay) # 模态栈单口
|
||||
root.ui.screens.start_overlay.visible = true
|
||||
|
||||
|
||||
# 生涯页快照投影 —— 承接体见 HudPresenter.career_data(开层调用点直接改引)
|
||||
@@ -52,13 +52,13 @@ static func _on_career_closed(root: Variant) -> void:
|
||||
# 开层(模态栈单口;与开始层互斥=career 同款先例)。「进入存档」钮归卡② mn3——本函数为
|
||||
# 该钮的唯一落点(卡③ 批内探针函数直调=中间态,实现报告「已知中间态」登记)。
|
||||
static func _open_save_select(root: Variant) -> void:
|
||||
root._start_overlay.visible = false
|
||||
root._open_modal(root.save_select_overlay) # 模态栈单口
|
||||
root.save_select_overlay.open_save_select(HudPresenter.save_select_data(root.save_manager))
|
||||
root.ui.screens.start_overlay.visible = false
|
||||
root.ui.modals.flow.open(root.ui.modals.save_select_overlay) # 模态栈单口
|
||||
root.ui.modals.save_select_overlay.open_save_select(HudPresenter.save_select_data(root.save_manager))
|
||||
|
||||
static func _on_save_select_closed(root: Variant) -> void:
|
||||
root._close_modal(root.save_select_overlay) # 模态栈单口
|
||||
root._start_overlay.visible = true
|
||||
root.ui.modals.flow.close(root.ui.modals.save_select_overlay) # 模态栈单口
|
||||
root.ui.screens.start_overlay.visible = true
|
||||
|
||||
|
||||
# 槽数据快照 —— 承接体见 HudPresenter.save_select_data(开层调用点直接改引)
|
||||
@@ -85,18 +85,18 @@ static func _on_save_select_load(root: Variant, slot: int) -> void:
|
||||
static func _on_save_select_delete(root: Variant, slot: int) -> void:
|
||||
root._confirm_consumer = "delete_slot"
|
||||
root._confirm_consumer_slot = slot
|
||||
root._open_modal(root.confirm_dialog) # 模态栈单口(显示编排/挂起兜底/遮挡记账——与其余覆盖层同口)
|
||||
root.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消")
|
||||
root.ui.modals.flow.open(root.ui.modals.confirm_dialog) # 模态栈单口(显示编排/挂起兜底/遮挡记账——与其余覆盖层同口)
|
||||
root.ui.modals.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消")
|
||||
|
||||
|
||||
|
||||
# 入口角标点击(两屏同款处理器):P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,
|
||||
# 探针 pressed.emit 绕过 disabled 时由此兜住;地图屏自身的登记开态不计——has_open_excluding)
|
||||
static func _on_status_badge_pressed(root: Variant) -> void:
|
||||
var map_on: bool = root.level_screen != null and root.level_screen.visible
|
||||
var map_on: bool = root.ui.modals.level_screen != null and root.ui.modals.level_screen.visible
|
||||
# P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,探针 pressed.emit 绕过 disabled 时由此兜住;
|
||||
# 地图屏自身的登记开态不计;判据排除调试浮窗:对局中浮窗开着时状态角标仍可点)
|
||||
if (map_on and root._modal_flow.badge_greyed_map()) or (not map_on and root._modal_flow.badge_greyed_battle()):
|
||||
if (map_on and root.ui.modals.flow.badge_greyed_map()) or (not map_on and root.ui.modals.flow.badge_greyed_battle()):
|
||||
return
|
||||
_open_status_panel(root)
|
||||
|
||||
@@ -106,17 +106,17 @@ static func _on_status_badge_pressed(root: Variant) -> void:
|
||||
# 地图屏角标=随屏;置灰=登记册上除地图屏自身外任一层开着(防「看得见点不着」态)。
|
||||
# 调用面=模态开合单口与屏级互斥口(凡影响开态/屏态的路径必经其一)。
|
||||
static func _sync_status_entry(root: Variant) -> void:
|
||||
if root.status_panel == null or root._modals == null or root.board_zone == null or root.level_screen == null:
|
||||
if root.ui.modals.status_panel == null or root.ui.modals.flow == null or root.ui.battle.board_zone == null or root.ui.modals.level_screen == null:
|
||||
return
|
||||
var map_on: bool = root.level_screen.visible
|
||||
root.board_zone.status_badge.visible = root.run_scope.level_active and not map_on
|
||||
root.level_screen.status_badge.visible = map_on
|
||||
var map_on: bool = root.ui.modals.level_screen.visible
|
||||
root.ui.battle.board_zone.status_badge.visible = root.run_scope.level_active and not map_on
|
||||
root.ui.modals.level_screen.status_badge.visible = map_on
|
||||
# 置灰判据为排除式(调试浮窗开态不计入:对局中浮窗开着时角标仍可点)
|
||||
root.board_zone.status_badge.disabled = root._modal_flow.badge_greyed_battle() # 互斥域谓词
|
||||
root.level_screen.status_badge.disabled = root._modal_flow.badge_greyed_map()
|
||||
root.ui.battle.board_zone.status_badge.disabled = root.ui.modals.flow.badge_greyed_battle() # 互斥域谓词
|
||||
root.ui.modals.level_screen.status_badge.disabled = root.ui.modals.flow.badge_greyed_map()
|
||||
# 调试浮层角标(全局浮层任何 UI 不遮挡——可见性不随场景变,只抬 offset
|
||||
# 避让:地图屏底栏 85px 常驻,角标抬至底栏上方 16px;其余场景右下角 16px 贴底)
|
||||
root.debug_badge.offset_bottom = -101.0 if map_on else -16.0
|
||||
root.ui.screens.debug_badge.offset_bottom = -101.0 if map_on else -16.0
|
||||
|
||||
|
||||
|
||||
@@ -131,19 +131,19 @@ static func _on_wallet_changed(root: Variant, event: String, gp: int, extra: Dic
|
||||
root.logger.log_wallet(event, gp, extra)
|
||||
if String(event) == "gain":
|
||||
root.run_scope.last_gain_amount = int(extra.get("amount", 0)) # GP 增量取数(每战恰一笔 gain)
|
||||
if root.shop_overlay.visible and root.run_scope.runner != null:
|
||||
if root.ui.modals.shop_overlay.visible and root.run_scope.runner != null:
|
||||
if root.run_scope.runner.shop.is_shop_pool():
|
||||
root.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变)
|
||||
root.ui.modals.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变)
|
||||
else:
|
||||
root.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp))
|
||||
root.ui.modals.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp))
|
||||
root._refresh_presentations()
|
||||
|
||||
|
||||
|
||||
static func _on_legend_toggled(root: Variant) -> void:
|
||||
if root.legend_overlay.is_open():
|
||||
root._close_modal(root.legend_overlay)
|
||||
if root.ui.modals.legend_overlay.is_open():
|
||||
root.ui.modals.flow.close(root.ui.modals.legend_overlay)
|
||||
else:
|
||||
root._open_modal(root.legend_overlay)
|
||||
root.ui.modals.flow.open(root.ui.modals.legend_overlay)
|
||||
|
||||
|
||||
+116
-121
@@ -1,7 +1,7 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 组合根 UI 装配分步承接件:三屏壳实例化、视图句柄接线、覆盖层装配与模态编排注入按步拆开。
|
||||
## 本件是 main._build_ui 的逐字分步搬运(成员访问改 root 前缀)——main 侧留同名薄委托,装配序与 z 序零改动。
|
||||
## 层序唯一事实源仍是 main.LAYER_CONTRACT + _apply_layer_contract(本件不持有层序逻辑)。
|
||||
## 层序唯一事实源=root.ui.LAYER_CONTRACT + root.ui.apply_layer_contract(ui_registry.gd——本件不持有层序逻辑)。
|
||||
extends RefCounted
|
||||
|
||||
const UiAssembler := preload("res://presentation/ui_assembler.gd")
|
||||
@@ -35,50 +35,48 @@ static func _build_screens(root: Variant) -> void:
|
||||
# 屏内 level_screen 与 battle_root 无同时可见态(屏级互斥+模态栈遮挡),层内先后零视觉差;
|
||||
# 视觉共存对逐对验算:legend/battle(图例战斗中开——legend 在冒险屏壳之后=其上 ✓)、
|
||||
# debug/level_screen(浮窗最后子=其上 ✓)、shop/level_screen(商店节点域弹出——模态均在屏壳上 ✓)。
|
||||
root._adventure_screen = AdventureScreenScene.instantiate()
|
||||
root.add_child(root._adventure_screen)
|
||||
root._menu_screen = MenuScreenScene.instantiate()
|
||||
root.add_child(root._menu_screen)
|
||||
root._start_overlay = root._menu_screen.start_overlay
|
||||
root._battle_root = root._adventure_screen.battle_root
|
||||
root.stage = root._adventure_screen.stage
|
||||
root.log_view = root._adventure_screen.log_view
|
||||
root.info_panel = root._adventure_screen.info_panel
|
||||
root.board_zone = root._adventure_screen.board_zone
|
||||
root.board_view = root._adventure_screen.board_view # 探针兼容别名(= board_zone.board_view)
|
||||
root.ap_bar = root._adventure_screen.ap_bar # 探针兼容别名(= board_zone.ap_bar)
|
||||
root.settlement_fx = root._adventure_screen.settlement_fx
|
||||
root.level_screen = root._adventure_screen.level_screen
|
||||
root.ui.screens.adventure_screen = AdventureScreenScene.instantiate()
|
||||
root.add_child(root.ui.screens.adventure_screen)
|
||||
root.ui.screens.menu_screen = MenuScreenScene.instantiate()
|
||||
root.add_child(root.ui.screens.menu_screen)
|
||||
root.ui.screens.start_overlay = root.ui.screens.menu_screen.start_overlay
|
||||
root.ui.screens.battle_root = root.ui.screens.adventure_screen.battle_root
|
||||
root.ui.battle.stage = root.ui.screens.adventure_screen.stage
|
||||
root.ui.battle.log_view = root.ui.screens.adventure_screen.log_view
|
||||
root.ui.battle.info_panel = root.ui.screens.adventure_screen.info_panel
|
||||
root.ui.battle.board_zone = root.ui.screens.adventure_screen.board_zone
|
||||
root.ui.battle.settlement_fx = root.ui.screens.adventure_screen.settlement_fx
|
||||
root.ui.modals.level_screen = root.ui.screens.adventure_screen.level_screen
|
||||
|
||||
static func _build_battle_wiring(root: Variant) -> void:
|
||||
# 结算行排播泵(本体在 row_pacer.gd;本类注入视图句柄与呈现刷新回调,零信号连接)
|
||||
root._row_pacer = RowPacer.new()
|
||||
root._row_pacer.log_view = root.log_view
|
||||
root._row_pacer.stage = root.stage
|
||||
root._row_pacer.board_zone = root.board_zone
|
||||
root._row_pacer.settlement_fx = root.settlement_fx
|
||||
root._row_pacer.board_view = root.board_view
|
||||
root._row_pacer.refresh_request = root._refresh_presentations
|
||||
root._row_pacer.pump_ended = root._on_pump_ended # 演播完边沿统一派发(挂起 modal 优先,其次锁期点击重放)
|
||||
root.add_child(root._row_pacer)
|
||||
root.ui.screens.row_pacer = RowPacer.new()
|
||||
root.ui.screens.row_pacer.log_view = root.ui.battle.log_view
|
||||
root.ui.screens.row_pacer.stage = root.ui.battle.stage
|
||||
root.ui.screens.row_pacer.board_zone = root.ui.battle.board_zone
|
||||
root.ui.screens.row_pacer.settlement_fx = root.ui.battle.settlement_fx
|
||||
root.ui.screens.row_pacer.board_view = root.ui.battle.board_zone.board_view
|
||||
root.ui.screens.row_pacer.refresh_request = root._refresh_presentations
|
||||
root.ui.screens.row_pacer.pump_ended = root.ui.modals.flow.pump_ended # 演播完边沿统一派发(挂起 modal 优先,其次锁期点击重放)
|
||||
root.add_child(root.ui.screens.row_pacer)
|
||||
# 秘宝两步交互(本体在 treasure_interact_controller.gd;settlement/runner/grant 随关卡/战斗更替,
|
||||
# 经现值回调注入——仿 identity_resolver 只读回调先例;grant 每战新建编排器,现值取件防悬挂)
|
||||
root._treasure_ui = TreasureInteractController.new({
|
||||
"db": root.db, "meta": root.meta, "info_panel": root.info_panel, "toast": root.board_zone.toast,
|
||||
"board_view": root.board_view,
|
||||
"db": root.db, "meta": root.meta, "info_panel": root.ui.battle.info_panel, "toast": root.ui.battle.board_zone.toast,
|
||||
"board_view": root.ui.battle.board_zone.board_view,
|
||||
"settlement_get": func() -> Variant: return root.run_scope.settlement,
|
||||
"runner_get": func() -> Variant: return root.run_scope.runner,
|
||||
"grant_get": func() -> Variant: return root.run_scope.settlement.reward_grant() if root.run_scope.settlement != null else null,
|
||||
})
|
||||
|
||||
root.board_zone.action_requested.connect(root._on_board_action)
|
||||
root.board_zone.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(战斗屏 HUD 角标)
|
||||
root.info_panel.treasure_slot_clicked.connect(root._treasure_ui.on_slot_clicked) # 两步交互第一步
|
||||
root.board_zone.stop_pressed.connect(root._on_stop_pressed)
|
||||
root.board_zone.refresh_confirmed.connect(root._on_refresh_confirmed)
|
||||
root.board_zone.legend_toggled.connect(root._on_legend_toggled)
|
||||
root.board_zone.hover_forbidden.connect(root._on_hover_forbidden)
|
||||
root.board_zone.chord_attempted.connect(root._on_chord_attempted)
|
||||
root.ui.battle.board_zone.action_requested.connect(root._on_board_action)
|
||||
root.ui.battle.board_zone.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(战斗屏 HUD 角标)
|
||||
root.ui.battle.info_panel.treasure_slot_clicked.connect(root._treasure_ui.on_slot_clicked) # 两步交互第一步
|
||||
root.ui.battle.board_zone.stop_pressed.connect(root._on_stop_pressed)
|
||||
root.ui.battle.board_zone.refresh_confirmed.connect(root._on_refresh_confirmed)
|
||||
root.ui.battle.board_zone.legend_toggled.connect(root._on_legend_toggled)
|
||||
root.ui.battle.board_zone.hover_forbidden.connect(root._on_hover_forbidden)
|
||||
root.ui.battle.board_zone.chord_attempted.connect(root._on_chord_attempted)
|
||||
|
||||
root.logger = PlaytestLogger.new()
|
||||
root.logger.ap_per_round = int(root.db.run.action_economy.ap_per_round)
|
||||
@@ -88,136 +86,135 @@ static func _build_overlays(root: Variant) -> void:
|
||||
# < 商店 < 营地 < 设置 < 状态面板(只读) < 生涯页(只读) < 确认弹窗 < 调试浮窗;
|
||||
# 关卡画面住冒险屏壳(树序在模态之下)——登记序仍夹在节点层与三选一层之间,遮挡语义零改动)
|
||||
var overlays: Dictionary[Variant, Variant] = UiAssembler.build_overlays(root, root.db)
|
||||
root.legend_overlay = overlays.legend_overlay
|
||||
root.node_overlay = overlays.node_overlay
|
||||
root._end_overlay = overlays.end_overlay
|
||||
root.shop_overlay = overlays.shop_overlay
|
||||
root._camp_screen = CampScreenScene.instantiate()
|
||||
root.add_child(root._camp_screen)
|
||||
root.camp_overlay = root._camp_screen.camp_overlay
|
||||
root.ui.modals.legend_overlay = overlays.legend_overlay
|
||||
root.ui.modals.node_overlay = overlays.node_overlay
|
||||
root.ui.modals.end_overlay = overlays.end_overlay
|
||||
root.ui.modals.shop_overlay = overlays.shop_overlay
|
||||
root.ui.screens.camp_screen = CampScreenScene.instantiate()
|
||||
root.add_child(root.ui.screens.camp_screen)
|
||||
root.ui.modals.camp_overlay = root.ui.screens.camp_screen.camp_overlay
|
||||
# 营地屏壳插位(夹商店层与设置层之间)由 LAYER_CONTRACT 营地槽统一落位——禁散落 move_child
|
||||
root.legend_overlay.closed.connect(root._on_modal_closed.bind(root.legend_overlay))
|
||||
root.node_overlay.continue_pressed.connect(root._on_node_continue)
|
||||
root.level_screen.route_chosen.connect(root._on_route_chosen)
|
||||
root.level_screen.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标)
|
||||
root.level_screen.abandon_requested.connect(root._on_abandon_requested) # 放弃入口
|
||||
root._end_overlay.continue_pressed.connect(root._on_battle_end_continue)
|
||||
root.ui.modals.legend_overlay.closed.connect(root.ui.modals.flow.close.bind(root.ui.modals.legend_overlay))
|
||||
root.ui.modals.node_overlay.continue_pressed.connect(root._on_node_continue)
|
||||
root.ui.modals.level_screen.route_chosen.connect(root._on_route_chosen)
|
||||
root.ui.modals.level_screen.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标)
|
||||
root.ui.modals.level_screen.abandon_requested.connect(root._on_abandon_requested) # 放弃入口
|
||||
root.ui.modals.end_overlay.continue_pressed.connect(root._on_battle_end_continue)
|
||||
|
||||
static func _build_aux_panels(root: Variant) -> void:
|
||||
root.settings_overlay = SettingsOverlay.new(root.settings_store) # 最小设置面(音量/全屏/减动效)
|
||||
root.add_child(root.settings_overlay)
|
||||
root.settings_overlay.closed.connect(root._on_settings_closed)
|
||||
root.ui.modals.settings_overlay = SettingsOverlay.new(root.settings_store) # 最小设置面(音量/全屏/减动效)
|
||||
root.add_child(root.ui.modals.settings_overlay)
|
||||
root.ui.modals.settings_overlay.closed.connect(root._on_settings_closed)
|
||||
# 局内状态面板·只读(登记设置层同档带 dim 遮罩,确认弹窗之前;
|
||||
# 数据=组合根开层实时读 MetaProgression 访问器喂入,本层零写通路)
|
||||
root.status_panel = StatusPanel.new()
|
||||
root.add_child(root.status_panel)
|
||||
root.status_panel.closed.connect(root._on_status_closed)
|
||||
root.ui.modals.status_panel = StatusPanel.new()
|
||||
root.add_child(root.ui.modals.status_panel)
|
||||
root.ui.modals.status_panel.closed.connect(root._on_status_closed)
|
||||
# 生涯页·只读全屏页(登记序=营地同档,设置层旁、确认弹窗之前;
|
||||
# 树序同步=addChild 同位(登记序=树序=z 序纪律);数据=组合根开层实时读 SaveManager 计数
|
||||
# 访问器+MetaProgression 访问器喂入,本层零写通路零跳转;入口=开始层「营地与系统」组生涯钮)
|
||||
root.career_overlay = CareerOverlay.new()
|
||||
root.add_child(root.career_overlay)
|
||||
root.career_overlay.closed.connect(root._on_career_closed)
|
||||
root.ui.modals.career_overlay = CareerOverlay.new()
|
||||
root.add_child(root.ui.modals.career_overlay)
|
||||
root.ui.modals.career_overlay.closed.connect(root._on_career_closed)
|
||||
# 选档界面(营地交互重做卡③):登记序=生涯同档(树序同步=addChild 同位);三动作只发意图信号
|
||||
#(激活/创建/删除+确认分派归组合根——视图零数据层依赖);入口=「进入存档」钮(卡② mn3 接线)。
|
||||
root.save_select_overlay = SaveSelectOverlay.new()
|
||||
root.add_child(root.save_select_overlay)
|
||||
root.save_select_overlay.closed.connect(root._on_save_select_closed)
|
||||
root.save_select_overlay.new_slot_requested.connect(root._on_save_select_new)
|
||||
root.save_select_overlay.load_slot_requested.connect(root._on_save_select_load)
|
||||
root.save_select_overlay.delete_slot_requested.connect(root._on_save_select_delete)
|
||||
root.ui.modals.save_select_overlay = SaveSelectOverlay.new()
|
||||
root.add_child(root.ui.modals.save_select_overlay)
|
||||
root.ui.modals.save_select_overlay.closed.connect(root._on_save_select_closed)
|
||||
root.ui.modals.save_select_overlay.new_slot_requested.connect(root._on_save_select_new)
|
||||
root.ui.modals.save_select_overlay.load_slot_requested.connect(root._on_save_select_load)
|
||||
root.ui.modals.save_select_overlay.delete_slot_requested.connect(root._on_save_select_delete)
|
||||
|
||||
static func _build_curio(root: Variant) -> void:
|
||||
# 提升三选一覆盖层(z 序 = 节点层之上、终局层之下;确认后同拍推进;
|
||||
# 插位由 LAYER_CONTRACT curio 槽统一落位——禁散落 move_child)
|
||||
root.curio_overlay = CurioPickOverlay.new()
|
||||
root.add_child(root.curio_overlay)
|
||||
root.ui.modals.curio_overlay = CurioPickOverlay.new()
|
||||
root.add_child(root.ui.modals.curio_overlay)
|
||||
# 提升三选一与替换子交互(本体在 curio_interact_controller.gd;本类只接线与回调注入,
|
||||
# 模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调)
|
||||
root._bonus = CurioInteractController.new(CurioFlowContext.new(root.db,
|
||||
{"curio_overlay": root.curio_overlay, "end_overlay": root._end_overlay,
|
||||
"board_view": root.board_view, "toast": root.board_zone.toast},
|
||||
{"curio_overlay": root.ui.modals.curio_overlay, "end_overlay": root.ui.modals.end_overlay,
|
||||
"board_view": root.ui.battle.board_zone.board_view, "toast": root.ui.battle.board_zone.toast},
|
||||
{"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager,
|
||||
"adventure_get": func() -> Variant: return root.run_scope.adventure,
|
||||
"runner_get": func() -> Variant: return root.run_scope.runner,
|
||||
"adventure_mode_get": func() -> Variant: return root.run_scope.is_adventure},
|
||||
{"open_modal": root._open_modal, "open_modal_after_playback": root._open_modal_after_playback,
|
||||
"close_modal": root._close_modal, "refresh_presentations": root._refresh_presentations,
|
||||
{"open_modal": root.ui.modals.flow.open, "open_modal_after_playback": root.ui.modals.flow.open_after_playback,
|
||||
"close_modal": root.ui.modals.flow.close, "refresh_presentations": root._refresh_presentations,
|
||||
"fatal_screen": root._fatal_screen,
|
||||
"gp_summary": _gp_summary_cb(root)})) # 构造参数收拢 context(u20 三束)
|
||||
root.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed)
|
||||
root.curio_overlay.continue_pressed.connect(root._bonus.on_continue)
|
||||
root.curio_overlay.refresh_requested.connect(root._bonus.on_refresh)
|
||||
root.ui.modals.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed)
|
||||
root.ui.modals.curio_overlay.continue_pressed.connect(root._bonus.on_continue)
|
||||
root.ui.modals.curio_overlay.refresh_requested.connect(root._bonus.on_refresh)
|
||||
|
||||
static func _build_menu_wiring(root: Variant) -> void:
|
||||
# 菜单屏接线(句柄经屏壳暴露;三入口化——进入存档/设置/退出游戏,卡② mn3:
|
||||
# 进入存档钮落点=卡③选档层 _open_save_select,点击链在此闭环)
|
||||
root._menu_screen.entersavebtn.pressed.connect(root._open_save_select)
|
||||
root.ui.screens.menu_screen.entersavebtn.pressed.connect(root._open_save_select)
|
||||
# 调试入口=UI 最顶层全局浮层角标(任何 UI 不遮挡——菜单/战斗/地图三枚
|
||||
# 屏内角标由全局浮层承载;连接归组合根,树序/登记面见 LAYER_CONTRACT 末位)
|
||||
root.debug_badge = UiAssembler.build_debug_badge(root)
|
||||
root.add_child(root.debug_badge)
|
||||
root.debug_badge.pressed.connect(root._on_debug_badge_pressed)
|
||||
root.ui.screens.debug_badge = UiAssembler.build_debug_badge(root)
|
||||
root.add_child(root.ui.screens.debug_badge)
|
||||
root.ui.screens.debug_badge.pressed.connect(root._on_debug_badge_pressed)
|
||||
# 营地/商店投影(本体在 camp_shop_presenter.gd;本类只接线、探针缝保缝转发;
|
||||
# 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入)
|
||||
root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db,
|
||||
{"camp_overlay": root.camp_overlay, "shop_overlay": root.shop_overlay,
|
||||
"toast": root.board_zone.toast, "start_overlay": root._start_overlay, "logger": root.logger},
|
||||
{"camp_overlay": root.ui.modals.camp_overlay, "shop_overlay": root.ui.modals.shop_overlay,
|
||||
"toast": root.ui.battle.board_zone.toast, "start_overlay": root.ui.screens.start_overlay, "logger": root.logger},
|
||||
{"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager,
|
||||
"runner_get": func() -> Variant: return root.run_scope.runner},
|
||||
{"open_modal": root._open_modal, "close_modal": root._close_modal,
|
||||
{"open_modal": root.ui.modals.flow.open, "close_modal": root.ui.modals.flow.close,
|
||||
"refresh_presentations": root._refresh_presentations})) # 构造参数收拢 context(u21 三束)
|
||||
root.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy)
|
||||
root.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1
|
||||
root.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave)
|
||||
root.camp_overlay.buy_requested.connect(root._camp_pres.on_camp_buy)
|
||||
root.camp_overlay.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle)
|
||||
root.camp_overlay.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy)
|
||||
root.camp_overlay.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle)
|
||||
root.camp_overlay.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) # 锻造三意图(写口唯一归 MetaProgression)
|
||||
root.camp_overlay.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock)
|
||||
root.camp_overlay.forge_select_requested.connect(root._camp_pres.on_camp_forge_select)
|
||||
root.camp_overlay.closed.connect(root._camp_pres.on_camp_closed)
|
||||
root._menu_screen.settingsbtn.pressed.connect(root._open_settings) # 最小设置面入口
|
||||
root._menu_screen.quitbtn.pressed.connect(root._quit_game) # 退出游戏提级一级菜单
|
||||
root.ui.modals.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy)
|
||||
root.ui.modals.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1
|
||||
root.ui.modals.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave)
|
||||
root.ui.modals.camp_overlay.buy_requested.connect(root._camp_pres.on_camp_buy)
|
||||
root.ui.modals.camp_overlay.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle)
|
||||
root.ui.modals.camp_overlay.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy)
|
||||
root.ui.modals.camp_overlay.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle)
|
||||
root.ui.modals.camp_overlay.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) # 锻造三意图(写口唯一归 MetaProgression)
|
||||
root.ui.modals.camp_overlay.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock)
|
||||
root.ui.modals.camp_overlay.forge_select_requested.connect(root._camp_pres.on_camp_forge_select)
|
||||
root.ui.modals.camp_overlay.closed.connect(root._camp_pres.on_camp_closed)
|
||||
root.ui.screens.menu_screen.settingsbtn.pressed.connect(root._open_settings) # 最小设置面入口
|
||||
root.ui.screens.menu_screen.quitbtn.pressed.connect(root._quit_game) # 退出游戏提级一级菜单
|
||||
|
||||
static func _build_debug_and_resolvers(root: Variant) -> void:
|
||||
var inter: Dictionary[Variant, Variant] = UiAssembler.build_inter_overlay(root)
|
||||
root._inter_overlay = inter.overlay
|
||||
root._inter_label = inter.label
|
||||
root.ui.screens.inter_overlay = inter.overlay
|
||||
root.ui.screens.inter_label = inter.label
|
||||
inter.next_btn.pressed.connect(root._on_inter_next_pressed)
|
||||
|
||||
# 确认弹窗(新冒险互斥确认弹窗化):树序落在开始层/过场层之后 = 子级最末
|
||||
#(纪律「登记序 = 树序 = z 序」——confirm_dialog.open 禁运行时 move_child 自提抬层;
|
||||
# 弹窗只在开始菜单域弹出,绘制序恒最上;
|
||||
# 诊断转储入口在调试浮窗)
|
||||
root.confirm_dialog = ConfirmDialog.new()
|
||||
root.add_child(root.confirm_dialog)
|
||||
root.ui.modals.confirm_dialog = ConfirmDialog.new()
|
||||
root.add_child(root.ui.modals.confirm_dialog)
|
||||
# 消费方编排(卡③ 契约 3/sl8——禁双常驻静态接线):open 前设 _confirm_consumer,
|
||||
# confirmed/cancelled 后按该状态量分派;分派面可容纳后续消费方(卡④ 到位直接接入不返工)
|
||||
root.confirm_dialog.confirmed.connect(root._on_confirm_accepted)
|
||||
root.confirm_dialog.cancelled.connect(root._on_confirm_declined)
|
||||
root.ui.modals.confirm_dialog.confirmed.connect(root._on_confirm_accepted)
|
||||
root.ui.modals.confirm_dialog.cancelled.connect(root._on_confirm_declined)
|
||||
|
||||
# 调试浮窗(主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口;
|
||||
# 全局可开浮窗:调试会话恒开=启动即开〔_ready〕+两屏角标再开+当局数据只读区块;
|
||||
# 诊断转储入口在本浮窗,玩家画面零常驻调试件)。
|
||||
# 树序在开始层与确认弹窗之后 = 浮于菜单之上的 Dialog(不隐藏开始层——与确认弹窗同款形态);
|
||||
# 开窗唯一入口 = _show_debug_overlay(C6 单口——角标/探针/启动路径全收口)
|
||||
root.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp))
|
||||
root.add_child(root.debug_overlay)
|
||||
root._seed_edit = root.debug_overlay.seed_edit # 种子读取面在浮窗(_seed_value 唯一消费点不变)
|
||||
root.debug_overlay.closed.connect(root._on_modal_closed.bind(root.debug_overlay))
|
||||
root.debug_overlay.apply_requested.connect(root._on_tune_apply)
|
||||
root.debug_overlay.simulate_requested.connect(root._on_tune_simulate)
|
||||
root.debug_overlay.debug_levels_requested.connect(root._start_debug_levels)
|
||||
root.ui.modals.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp))
|
||||
root.add_child(root.ui.modals.debug_overlay)
|
||||
root.ui.modals.debug_overlay.closed.connect(root.ui.modals.flow.close.bind(root.ui.modals.debug_overlay))
|
||||
root.ui.modals.debug_overlay.apply_requested.connect(root._on_tune_apply)
|
||||
root.ui.modals.debug_overlay.simulate_requested.connect(root._on_tune_simulate)
|
||||
root.ui.modals.debug_overlay.debug_levels_requested.connect(root._start_debug_levels)
|
||||
|
||||
# 棋盘解析回调族(有态绑定件——board_resolvers.gd 承接;组合根只接线;局部持有不占类成员位)
|
||||
var resolvers = BoardResolversScript.new(root.db, root.debug_overlay, func() -> Variant: return root.run_scope.settlement)
|
||||
var resolvers = BoardResolversScript.new(root.db, root.ui.modals.debug_overlay, func() -> Variant: return root.run_scope.settlement)
|
||||
# 奖励徽章数据源:地图战斗资源 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效)
|
||||
root.board_view.set_identity_resolver(resolvers.identity_lookup)
|
||||
root.ui.battle.board_zone.board_view.set_identity_resolver(resolvers.identity_lookup)
|
||||
# 数字遮蔽数据源:地形变异 hide_number 解析只读回调(信息型呈现——board_view 渲染面读取)
|
||||
root.board_view.set_variant_mask_resolver(resolvers.variant_mask_lookup)
|
||||
root.board_view.set_variant_mark_resolver(resolvers.variant_mark_lookup) # flat_damage 未开格角标
|
||||
root.ui.battle.board_zone.board_view.set_variant_mask_resolver(resolvers.variant_mask_lookup)
|
||||
root.ui.battle.board_zone.board_view.set_variant_mark_resolver(resolvers.variant_mark_lookup) # flat_damage 未开格角标
|
||||
# 调试会话(英雄生效值预览回调绑同一解析件——创建晚于浮窗、随 _build_ui 一次接线)
|
||||
root._debug = DebugSessionScript.new(root.db, root.get_node(root.AUTOLOAD_PATH_RNG),
|
||||
func() -> Dictionary: return resolvers.debug_hero_override(), root._seed_value)
|
||||
@@ -228,24 +225,22 @@ static func _build_modal_flow(root: Variant) -> void:
|
||||
# 模态登记(缺陷⑤纪律——登记序 = 树序 = z 序):层序唯一事实源=LAYER_CONTRACT,
|
||||
# _apply_layer_contract 统一落位(关卡画面宿住冒险屏壳、营地屏壳夹商店与设置层之间、
|
||||
# 三选一对位终局层之下等特例均在合同数据内表达,禁散落 move_child)
|
||||
# 模态编排注入(ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable)
|
||||
root._modal_flow = ModalFlow.new()
|
||||
root._modal_flow.start_visible_get = func() -> bool: return root._start_overlay.visible
|
||||
root._modal_flow.inter_visible_get = func() -> bool: return root._inter_overlay.visible
|
||||
root._modal_flow.settlement_get = func() -> Variant: return root.run_scope.settlement
|
||||
root._modal_flow.rows_empty_get = func() -> bool: return root._row_pacer.pending_rows().is_empty()
|
||||
root._modal_flow.flush_rows = root._flush_row_queue
|
||||
root._modal_flow.on_open_sync = func() -> void:
|
||||
# 模态编排注入(ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable——实例随 ui.modals 容器构造建立)
|
||||
root.ui.modals.flow.start_visible_get = func() -> bool: return root.ui.screens.start_overlay.visible
|
||||
root.ui.modals.flow.inter_visible_get = func() -> bool: return root.ui.screens.inter_overlay.visible
|
||||
root.ui.modals.flow.settlement_get = func() -> Variant: return root.run_scope.settlement
|
||||
root.ui.modals.flow.rows_empty_get = func() -> bool: return root.ui.screens.row_pacer.pending_rows().is_empty()
|
||||
root.ui.modals.flow.flush_rows = root.ui.screens.row_pacer.flush
|
||||
root.ui.modals.flow.on_open_sync = func() -> void:
|
||||
root._sync_status_entry() # 状态面板入口角标可见性/置灰随开态同步
|
||||
root._sync_debug_overlay() # 浮窗当局数据/按钮态随开态同步
|
||||
root._modal_flow.on_close_sync = func() -> void:
|
||||
root.ui.modals.flow.on_close_sync = func() -> void:
|
||||
root._sync_status_entry() # 关闭恢复角标可点态(无残留——同源口径)
|
||||
root._sync_debug_overlay() # 他层关闭复活浮窗时同步刷新(关后自动恢复语义配套)
|
||||
root._modal_flow.replay_click = root._replay_locked_click
|
||||
root._modals = root._modal_flow.stack # 探针 _list 登记序断言缝(ui_struct 登记序=z 序纪律断言)
|
||||
root.ui.modals.flow.replay_click = root._replay_locked_click
|
||||
# 视觉层合同装配(唯一事实源=LAYER_CONTRACT:树序+登记序同源派生——改序只改合同表)
|
||||
root._apply_layer_contract()
|
||||
root._tune_sim = TuneSimController.new(root.db, root.debug_overlay, int(root.db.combat.hero.max_hp))
|
||||
root.ui.apply_layer_contract(root)
|
||||
root._tune_sim = TuneSimController.new(root.db, root.ui.modals.debug_overlay, int(root.db.combat.hero.max_hp))
|
||||
|
||||
static func fatal_screen(root: Variant, msg: String) -> void:
|
||||
var l := UiKit.label(msg,UiKit.FS_SUB, true, UiKit.RED_ALERT)
|
||||
|
||||
@@ -2,16 +2,28 @@
|
||||
## 调整板批量模拟控制器:逐次驱动关卡自动策略(与 headless 扫描同口径),进度/报告经面板 setter 注入。
|
||||
## 零核心写——表基线复位由组合根在移交前完成,加成施加只走自动策略内部 runner.set_hero_override 单路径。
|
||||
## presentation 不 preload smoke:驱动实例由调用方注入(须供与 level_autoplay 同款 run_level 签名),缺省 null=运行时 load 兜底。
|
||||
## 逐帧步进:每局恰占一帧——第 0 局在调用点同步段执行,第 i 局(i≥1)在第 i 帧信号发射点执行,收尾恒在第 n 帧发射点;
|
||||
## n=0 退化路径为调用点同步收尾零帧。
|
||||
## _batch_finished 信号契约:整批收尾放行——发射时点=报告注入与运行态复位之后;消费方=run() 自身收尾放行,
|
||||
## 外部 await run(n) 经此挂起至整批完成(协程身份契约)。
|
||||
extends RefCounted
|
||||
|
||||
const SEED_BASE := 4200 # 模拟种子基(与历史实现同源——同种子可回放)
|
||||
const PlaytestLogger := preload("res://presentation/playtest_logger.gd") # 试玩日志(同层依赖——运行时 load 收敛为 preload 常量)
|
||||
|
||||
signal _batch_finished
|
||||
|
||||
var db
|
||||
var panel # 调试浮窗 setter 面:atk_pct/hp_pct/set_sim_running/set_sim_progress/set_sim_report
|
||||
var _base_hp := 0 # 报告中的"基础 HP"口径注记值
|
||||
var _driver = null # 关卡自动策略驱动(调用方注入;null = 旧行为运行时 load 兜底)
|
||||
var _running := false # 运行态(run 全程 true——组合根互查守卫用,与面板置灰解耦)
|
||||
var _sim_n := 0 # 本批局数(run 期活成员——批末失效、下次 run 重写)
|
||||
var _sim_i := 0 # 已执行局数(步进计数)
|
||||
var _sim_stats: Dictionary[Variant, Variant] = {} # stats 六键束(run 期承载)
|
||||
var _sim_autoplay = null # 自动策略驱动实例(run 期承载)
|
||||
var _sim_logger = null # 共享 JSONL 日志会话(run 期承载)
|
||||
var _finished := false # 批完成旗标(_finish_batch 置位——收尾段同步承载)
|
||||
|
||||
|
||||
func _init(db_ref, panel_ref, base_hp: int, driver = null) -> void:
|
||||
@@ -44,12 +56,38 @@ func run(n: int) -> void:
|
||||
panel.set_sim_progress("模拟中:0/%d …" % n)
|
||||
var stats := {"wins": 0, "dmg_all": [], "hp_left_win": [], "hp_boss": [], "rounds_all": [], "death_nodes": {}}
|
||||
var autoplay = _autoplay()
|
||||
for i in range(n):
|
||||
_run_one_level(autoplay, stats, i, panel, logger)
|
||||
panel.set_sim_progress("模拟中:%d/%d …" % [i + 1, n])
|
||||
await panel.get_tree().process_frame
|
||||
logger.close_session()
|
||||
panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), stats, _base_hp))
|
||||
_sim_n = n
|
||||
_sim_i = 0
|
||||
_sim_stats = stats
|
||||
_sim_autoplay = autoplay
|
||||
_sim_logger = logger
|
||||
_finished = false
|
||||
if n > 0:
|
||||
if not panel.get_tree().process_frame.is_connected(_step_frame):
|
||||
panel.get_tree().process_frame.connect(_step_frame) # 恰好连接一次(迭代 39 先例同款守卫)
|
||||
_step_frame() # 首步同步执行(第 0 局在调用栈内跑完——与 await 形态对齐)
|
||||
await _batch_finished
|
||||
else:
|
||||
_finish_batch() # n=0:循环零进同步收尾,不挂起(与 await 形态一致零帧)
|
||||
|
||||
|
||||
# 逐帧步进回调:i<n 执行一局+进度串+计数推进(每局恰占一帧);i 达 n 次帧断连→收尾→放行
|
||||
func _step_frame() -> void:
|
||||
if _sim_i < _sim_n:
|
||||
_run_one_level(_sim_autoplay, _sim_stats, _sim_i, panel, _sim_logger)
|
||||
panel.set_sim_progress("模拟中:%d/%d …" % [_sim_i + 1, _sim_n])
|
||||
_sim_i += 1
|
||||
else:
|
||||
panel.get_tree().process_frame.disconnect(_step_frame) # 先断连防再入
|
||||
_finish_batch()
|
||||
_batch_finished.emit()
|
||||
|
||||
|
||||
# 整批收尾单点(收尾四语句同帧同相位:close_session→报告→运行态复位——次序与 run() 尾现行形态逐句对齐)
|
||||
func _finish_batch() -> void:
|
||||
_finished = true
|
||||
_sim_logger.close_session()
|
||||
panel.set_sim_report(HudPresenter.tune_sim_report(_sim_n, panel.atk_pct(), panel.hp_pct(), _sim_stats, _base_hp))
|
||||
panel.set_sim_running(false)
|
||||
_running = false
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
## 战区视图域句柄册:战斗屏 A–D 区视图与结算演出覆盖层的注册面域容器。
|
||||
## 纯字段无方法;RefCounted 不挂树——节点本体树位与 add_child 归属由组合根与装配承接件负责。
|
||||
class_name UiBattle
|
||||
extends RefCounted
|
||||
|
||||
var stage: BattleStageView # B 区(含 log_view 明细带)
|
||||
var log_view: BattleLogView
|
||||
var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区)
|
||||
var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx)
|
||||
var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d35j5hry1f40v
|
||||
@@ -0,0 +1,32 @@
|
||||
## 模态层域句柄册:十三个模态覆盖层句柄+模态栈编排(ModalFlow——登记/开合/输入锁谓词)的注册面域容器。
|
||||
## RefCounted 不挂树;模态登记唯一口=register_layer(anchors 撑满宿主+排入 ModalStack)。
|
||||
## flow 随容器构造即建(装配早期注入点经 root.ui.modals.flow.* 取 Callable——晚于 flow 建立时点)。
|
||||
class_name UiModals
|
||||
extends RefCounted
|
||||
|
||||
var flow: ModalFlow
|
||||
var legend_overlay: LegendOverlay
|
||||
var node_overlay: NodeOverlay
|
||||
var level_screen: LevelMapScreen
|
||||
var curio_overlay: CurioPickOverlay
|
||||
var end_overlay: BattleEndOverlay
|
||||
var shop_overlay: ShopOverlay
|
||||
var camp_overlay: CampOverlay
|
||||
var settings_overlay: SettingsOverlay
|
||||
var status_panel: StatusPanel
|
||||
var career_overlay: CareerOverlay
|
||||
var save_select_overlay: SaveSelectOverlay
|
||||
var confirm_dialog: ConfirmDialog
|
||||
var debug_overlay: DebugOverlay
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
flow = ModalFlow.new() # 查询/钩子 Callable 由 _build_modal_flow 注入(注入时点不变——注入值面向已在座实例)
|
||||
|
||||
|
||||
func register_layer(m: Control, tags: Array = []) -> void:
|
||||
# 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后
|
||||
# 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors
|
||||
# ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律)
|
||||
m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
flow.register(m, tags) # 排除组随层登记(non_locking=调试浮窗/screen_state=关卡地图屏)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dbs4qveqvr5hg
|
||||
@@ -0,0 +1,101 @@
|
||||
## 界面注册面:呈现层界面元素清单的唯一事实源——哪些层、什么序、地址在哪。
|
||||
## 持有三域容器(screens/battle/modals)、LAYER_CONTRACT 合同表、node(key) 合同寻址、
|
||||
## apply_layer_contract(root) 唯一落位口(全仓唯一 move_child 点)。
|
||||
## RefCounted 不挂树:零信号连接、零会话期引用、零核心写调用——组合根单点装配不变。
|
||||
class_name UiRegistry
|
||||
extends RefCounted
|
||||
|
||||
const ModalStack := preload("res://presentation/modal_stack.gd")
|
||||
|
||||
# ==================== 视觉层合同(唯一事实源——树序=登记序=z 序;改序只改此表) ====================
|
||||
# kind: shell=屏壳容器(成员随壳不占独立树位;level_screen 例外另占登记位)/modal=模态层(入登记册,
|
||||
# tags 随登记)/state=全屏态(占树位不入登记册)/node=非视觉节点(占树位不入登记册)。
|
||||
# host=宿住屏壳(树序随宿主、登记序按本合同);shell=模态成员的屏壳(成员与屏壳同槽:屏壳占该树位、
|
||||
# 成员占该登记位)。落位=apply_layer_contract(全仓唯一层序落位点,add_child 之后统一排序+登记)。
|
||||
const LAYER_CONTRACT: Array[Dictionary] = [
|
||||
{"key": "adventure_screen", "kind": "shell"},
|
||||
{"key": "menu_screen", "kind": "shell"},
|
||||
{"key": "row_pacer", "kind": "node"},
|
||||
{"key": "legend_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "node_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "level_screen", "kind": "modal", "tags": [ModalStack.TAG_SCREEN_STATE], "host": "adventure_screen"},
|
||||
{"key": "curio_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "end_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "shop_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "camp_overlay", "kind": "modal", "tags": [], "shell": "camp_screen"},
|
||||
{"key": "settings_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "status_panel", "kind": "modal", "tags": []},
|
||||
{"key": "career_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "save_select_overlay", "kind": "modal", "tags": []}, # 选档界面(营地交互重做卡③——登记生涯同档)
|
||||
{"key": "inter_overlay", "kind": "state", "tags": []},
|
||||
{"key": "confirm_dialog", "kind": "modal", "tags": []},
|
||||
{"key": "debug_overlay", "kind": "modal", "tags": [ModalStack.TAG_NON_LOCKING]},
|
||||
{"key": "debug_badge", "kind": "float"}, # 调试入口全局浮层——末位=最顶绘制序;
|
||||
# float 类不注册 ModalStack(open() 遮挡记账只隐藏登记层)→ 任何 UI 打开都不遮它
|
||||
]
|
||||
|
||||
var screens: UiScreens
|
||||
var battle: UiBattle
|
||||
var modals: UiModals
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
screens = UiScreens.new()
|
||||
battle = UiBattle.new()
|
||||
modals = UiModals.new()
|
||||
|
||||
|
||||
# 合同层寻址(key→节点;纯查表不含层序语义——层序唯一事实源=LAYER_CONTRACT)
|
||||
func node(key: String) -> Node:
|
||||
match key:
|
||||
"adventure_screen": return screens.adventure_screen
|
||||
"menu_screen": return screens.menu_screen
|
||||
"row_pacer": return screens.row_pacer
|
||||
"legend_overlay": return modals.legend_overlay
|
||||
"node_overlay": return modals.node_overlay
|
||||
"level_screen": return modals.level_screen
|
||||
"curio_overlay": return modals.curio_overlay
|
||||
"end_overlay": return modals.end_overlay
|
||||
"shop_overlay": return modals.shop_overlay
|
||||
"camp_screen": return screens.camp_screen
|
||||
"camp_overlay": return modals.camp_overlay
|
||||
"settings_overlay": return modals.settings_overlay
|
||||
"status_panel": return modals.status_panel
|
||||
"career_overlay": return modals.career_overlay
|
||||
"save_select_overlay": return modals.save_select_overlay
|
||||
"inter_overlay": return screens.inter_overlay
|
||||
"confirm_dialog": return modals.confirm_dialog
|
||||
"debug_overlay": return modals.debug_overlay
|
||||
"debug_badge": return screens.debug_badge
|
||||
return null
|
||||
|
||||
|
||||
# 合同装配(_build_ui 尾调用):按 LAYER_CONTRACT 序统一落位——直接子层 move_child 到合同槽、
|
||||
# 模态层按合同序登记;宿住层(host)树序随宿主只登记;成员层(shell)与屏壳同槽(屏壳占树位、成员占登记位)。
|
||||
# 本函数是全仓唯一层序落位点(move_child 单口)——改层序只改 LAYER_CONTRACT。
|
||||
func apply_layer_contract(root: Control) -> void:
|
||||
var slot := 0
|
||||
for entry: Dictionary in LAYER_CONTRACT:
|
||||
var tags: Array[Variant] = entry.get("tags", [])
|
||||
if entry.has("shell"):
|
||||
var shell: Node = node(String(entry["shell"]))
|
||||
var member: Node = node(String(entry["key"]))
|
||||
if shell != null:
|
||||
root.move_child(shell, slot)
|
||||
slot += 1
|
||||
if member != null:
|
||||
modals.register_layer(member as Control, tags)
|
||||
else:
|
||||
push_error("[合同装配] 层缺失: %s" % String(entry["key"]))
|
||||
continue
|
||||
var layer: Node = node(String(entry["key"]))
|
||||
if layer == null:
|
||||
push_error("[合同装配] 层缺失: %s" % String(entry["key"]))
|
||||
continue
|
||||
if entry.has("host"):
|
||||
modals.register_layer(layer as Control, tags)
|
||||
continue
|
||||
root.move_child(layer, slot)
|
||||
slot += 1
|
||||
if String(entry["kind"]) == "modal":
|
||||
modals.register_layer(layer as Control, tags)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1hahbtan1te8
|
||||
@@ -0,0 +1,16 @@
|
||||
## 屏壳与全屏容器域句柄册:三屏壳、屏内一级子层、全屏态、浮层角标、排播泵节点的注册面域容器。
|
||||
## 纯字段无方法;RefCounted 不挂树——节点本体树位与 add_child 归属由组合根与装配承接件负责。
|
||||
class_name UiScreens
|
||||
extends RefCounted
|
||||
|
||||
const RowPacer := preload("res://presentation/row_pacer.gd") # 排播泵取型(row_pacer.gd 非 class_name 全局)
|
||||
|
||||
var menu_screen: Control
|
||||
var camp_screen: Control
|
||||
var adventure_screen: Control
|
||||
var start_overlay: Control
|
||||
var battle_root: Control
|
||||
var inter_overlay: Control
|
||||
var inter_label: Label
|
||||
var debug_badge: Button
|
||||
var row_pacer: RowPacer
|
||||
@@ -0,0 +1 @@
|
||||
uid://dd8ep7nhmyrdq
|
||||
@@ -1,128 +1,31 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## CellUnit——BoardView SubViewport 世界内逐格渲染件(Node2D),全部程序化 _draw 占位视觉(纹理槽位已留未装载)。
|
||||
## CellUnit——BoardView SubViewport 世界内逐格渲染件(Node2D),被动绘制壳:_draw 六步时序器
|
||||
## +视野迷雾域+微光子件生命周期+格件几何;基座层与标识叠加两绘制域件 _init 自建持有
|
||||
## (cell_base_layer/cell_mark_overlay——单一变化原因=迷雾规格与格件节点生命周期)。
|
||||
## 无 class_name(类名缓存面纪律)——使用方经 preload 引用。
|
||||
## 内部状态件(u3 子域拆件——契约 9 职责语句「单格绘制输入集」):本件为被动绘制壳,
|
||||
## 状态件 _CellPaintState 承载全部绘制入参(单一变化原因=格面呈现),setter 触发宿主重绘语义
|
||||
## 经 _owner 回引保持逐字;探针直读冻结成员(glyph/fog_state/plate/glyph_color)与 _glow_layer 住宿主。
|
||||
## 探针直读冻结成员(glyph/fog_state/plate/glyph_color)与 _glow_layer 住宿主;
|
||||
## hl_color/hl_visible/dot_visible/mutation_mark 探针读 u.face.*。
|
||||
extends Node2D
|
||||
|
||||
const _GLOW_LAYER := preload("res://presentation/views/cell_type_glow_layer.gd")
|
||||
const _BASE_LAYER := preload("res://presentation/views/cell_base_layer.gd")
|
||||
const _MARK_OVERLAY := preload("res://presentation/views/cell_mark_overlay.gd")
|
||||
|
||||
# ── D 投影档绘制常量(研究件实测取色——立面高光/双段暗带/地面投影;黑罩 alpha 叠加实测等效)──
|
||||
const FACADE_HIGHLIGHT_PX := 2.0 # 口沿高光线厚(像素)
|
||||
const FACADE_NEAR_FRAC := 0.16 # 立面近段高(格边比——研究件 10/64)
|
||||
const FACADE_FAR_FRAC := 0.25 # 立面远段高(格边比——研究件 16/64)
|
||||
const FACADE_NEAR_COL := Color(0, 0, 0, 0.60) # 立面近段黑罩(墙顶 132→52 实测 α≈0.61)
|
||||
const FACADE_FAR_COL := Color(0, 0, 0, 0.82) # 立面远段黑罩(墙顶 132→24 实测 α≈0.82)
|
||||
const PROJECTION_NEAR_COL := Color(0, 0, 0, 0.28) # 投影贴墙段(地面 66→46 实测 α≈0.28)
|
||||
const PROJECTION_MID_COL := Color(0, 0, 0, 0.16) # 投影中段(渐隐)
|
||||
const PROJECTION_EDGE_COL := Color(0, 0, 0, 0.06) # 投影远段(渐隐收边)
|
||||
# ── E 格高墙面(圣经 v3.5 冻结①——地面格南邻墙→南部 30% 承接墙面:墙顶纹理原亮度
|
||||
# 直贴零暗档零阴影;墙脚阴线只画地面/墙面交界;(b)-(c) 交界
|
||||
# 无边框由 BoardView 侧跳过墙格北向描边承载)──
|
||||
const E_FACADE_FRAC := 0.30 # 墙面承接高(格边比 30%)
|
||||
|
||||
|
||||
## 单格绘制输入集(被动绘制壳的全部绘制入参——BoardView 直写面;setter 触发宿主重绘语义逐字)
|
||||
class _CellPaintState extends RefCounted:
|
||||
var _owner: Node2D = null
|
||||
var cell := Vector2i.ZERO # 板坐标(诊断观测面)
|
||||
var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动)
|
||||
set(v):
|
||||
edge = v
|
||||
if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer):
|
||||
_owner._glow_layer.setup(edge, glow_group)
|
||||
_owner.queue_redraw()
|
||||
var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框)
|
||||
# ── 接触带微光绘制面(可挖域微光画在英雄格周界空间侧——art-bible §3.2 v2.8;
|
||||
# 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)──
|
||||
var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边)
|
||||
var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边)
|
||||
var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画)
|
||||
var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(程序侧结构件)
|
||||
# ── 高墙感 D 投影档(研究件四变体定 D=C+地面投影,ITB 同款高度线索)──
|
||||
# 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格
|
||||
# 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带
|
||||
# 0.22 格边;坐标推导留档 commit)。素材缺位程序化回退同样服役(结构归程序——art-bible §8.5)。
|
||||
var facade_south := false: # 南立面开关(BoardView paint 按「南邻已开格」写入)
|
||||
set(v):
|
||||
facade_south = v
|
||||
_owner.queue_redraw()
|
||||
var proj_band := 0.0: # 地面投影带宽(像素,0 = 不画——BoardView 按北邻墙判定写入)
|
||||
set(v):
|
||||
proj_band = v
|
||||
_owner.queue_redraw()
|
||||
var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区)
|
||||
set(v):
|
||||
badge_glyph = v
|
||||
if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer):
|
||||
_owner._glow_layer.set_badge_mask(v != "")
|
||||
_owner.queue_redraw()
|
||||
var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝)
|
||||
set(v):
|
||||
dot_visible = v
|
||||
_owner.queue_redraw()
|
||||
var hl_visible := false: # 焦点高亮(悬停态边框覆盖层)
|
||||
set(v):
|
||||
hl_visible = v
|
||||
_owner.queue_redraw()
|
||||
var hl_color := UiKit.GOLD: # 高亮色(金目标/封印朱/禁入灰禁红)
|
||||
set(v):
|
||||
hl_color = v
|
||||
_owner.queue_redraw()
|
||||
var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入)
|
||||
var bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0)
|
||||
set(v):
|
||||
bevel = v
|
||||
_owner.queue_redraw()
|
||||
# ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)──
|
||||
var tex_base: Texture2D = null # ① 砖面底
|
||||
var tex_mutation: Texture2D = null # ② 变异纹带
|
||||
var tex_glyph: Texture2D = null # ③ 数字
|
||||
var tex_badge: Texture2D = null # ③' 徽章
|
||||
var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark
|
||||
set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示))
|
||||
if tex_seal != v:
|
||||
tex_seal = v
|
||||
_owner.queue_redraw()
|
||||
var tex_facade: Texture2D = null # ⑥ E 格墙面材质(v3.5——南邻墙顶 center 瓦片同源,BoardView 注入)
|
||||
var facade_strip := false: # E 格开关(BoardView paint 按「已开格且南邻未开墙」写入——瓦片态专用)
|
||||
set(v):
|
||||
facade_strip = v
|
||||
_owner.queue_redraw()
|
||||
|
||||
# 变异角标(程序化,tex_mutation 纹带槽暂留):
|
||||
# "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记)
|
||||
# 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写):
|
||||
# "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,封印照常计费);
|
||||
# "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形)
|
||||
var treasure_mark := "":
|
||||
set(v):
|
||||
if treasure_mark != v:
|
||||
treasure_mark = v
|
||||
_owner.queue_redraw()
|
||||
var mutation_mark := "":
|
||||
set(v):
|
||||
if mutation_mark != v:
|
||||
mutation_mark = v
|
||||
_owner.queue_redraw()
|
||||
|
||||
# ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示)
|
||||
# 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。
|
||||
# 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。
|
||||
var glow_group := 0:
|
||||
set(v):
|
||||
if glow_group != v:
|
||||
glow_group = v
|
||||
_owner._sync_glow_layer()
|
||||
|
||||
var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入)
|
||||
set(v):
|
||||
removed_dim = v
|
||||
_owner.queue_redraw()
|
||||
|
||||
|
||||
var st := _CellPaintState.new()
|
||||
var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动)
|
||||
set(v):
|
||||
edge = v
|
||||
if _glow_layer != null and is_instance_valid(_glow_layer):
|
||||
_glow_layer.setup(edge, glow_group)
|
||||
queue_redraw()
|
||||
var cell := Vector2i.ZERO # 板坐标(诊断观测面)
|
||||
# ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示)
|
||||
# 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。
|
||||
# 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。
|
||||
var glow_group := 0:
|
||||
set(v):
|
||||
if glow_group != v:
|
||||
glow_group = v
|
||||
_sync_glow_layer()
|
||||
var glyph := "": # 文字位——数字/雷/封印/人/?(现行视觉封印与数字互斥,同位承载;探针直读冻结)
|
||||
set(v):
|
||||
glyph = v
|
||||
@@ -141,268 +44,51 @@ var fog_state := 0: # 探针直读冻结
|
||||
queue_redraw()
|
||||
|
||||
var _glow_layer: Node2D = null
|
||||
var base = null # 格面基座层绘制域件(_init 自建)
|
||||
var face = null # 格面标识与叠加层绘制域件(_init 自建)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
st._owner = self
|
||||
base = _BASE_LAYER.new(self)
|
||||
face = _MARK_OVERLAY.new(self)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var rect := Rect2(Vector2.ZERO, Vector2(st.edge, st.edge))
|
||||
_draw_base_layer(rect)
|
||||
_draw_mutation_marks()
|
||||
_draw_glyph_layer(rect)
|
||||
_draw_overlay_layers(rect)
|
||||
|
||||
|
||||
## ① 砖面底层域(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角;瓦片态描边覆盖层;
|
||||
## 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘。素材缺位回退走 elif 程序化原样,零观感变化)
|
||||
func _draw_base_layer(rect: Rect2) -> void:
|
||||
if st.tex_base != null:
|
||||
draw_texture_rect(st.tex_base, rect, false)
|
||||
# 轮廓加强(轮廓线不明显):墙格地面朝向边补 2px 深墨线(§8.5 结构归程序——
|
||||
# 素材 1px 墨线在 45/36px 非整数缩放下视觉衰减,程序线保底;墙-墙内界不画,保墙群内聚;
|
||||
# 坐标推导:各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合)
|
||||
if not st.outline_sides.is_empty():
|
||||
_draw_outline_sides()
|
||||
_draw_south_facade()
|
||||
_draw_north_projection()
|
||||
_draw_e_facade()
|
||||
# 瓦片态描边覆盖层:tex_base 在位时 BoardView 传入的 bg_sb
|
||||
# 已降级为「透明底 + 描边」载体——呼吸微光/主角蓝框画在瓦片之上(art-bible §1.2 叠层序
|
||||
# 不变:底 < 纹带 < 数字 < 封印 < 焦点高亮);素材缺位回退走 elif 程序化原样,零观感变化
|
||||
if st.bg_sb != null:
|
||||
draw_style_box(st.bg_sb, rect)
|
||||
_draw_contact_or_fallback(rect)
|
||||
|
||||
|
||||
## 接触带微光或平色回退域(英雄格周界空间侧——正交邻墙=整条边;斜向=共享角点+两侧各半条边;光不进墙格):
|
||||
## 带宽 4px 贴格边内侧(坐标推导:N=[0,0,edge,4] S=[0,edge-4,edge,4] W=[0,0,4,edge] E=[edge-4,0,4,edge];
|
||||
## 半条边=沿邻边自角点起 edge/2;角点圆 r=6 贴角点圆心,近白芯 r=2.5 提亮)
|
||||
func _draw_contact_or_fallback(rect: Rect2) -> void:
|
||||
if st.contact_col.a > 0.0:
|
||||
_draw_north_projection() # 平色回退+微光并发的英雄格补画(瓦片路径已画——同像素重复无观感差)
|
||||
_draw_e_facade() # E 格墙面补画(同上——英雄格兼 E 格时瓦片路径已画,重复无害)
|
||||
_draw_contact_bands()
|
||||
elif st.bg_sb != null:
|
||||
draw_style_box(st.bg_sb, rect)
|
||||
_draw_south_facade() # 平色回退路径——D 投影档两件同样服役(结构归程序)
|
||||
_draw_north_projection()
|
||||
_draw_bevel()
|
||||
|
||||
|
||||
## ②③④ 中层域:变异纹带与标记/数字色牌与字形/徽章/封印
|
||||
func _draw_glyph_layer(rect: Rect2) -> void:
|
||||
# ② 变异纹带(无程序化纹带——槽位先行,贴图落位即用)
|
||||
if st.tex_mutation != null:
|
||||
draw_texture_rect(st.tex_mutation, rect, false)
|
||||
# ③ 数字/徽章(数字 ≥ 格高 50% 墨族对格底 ≥4.5:1;徽章右下角金,遮蔽态 = "?")
|
||||
if glyph != "":
|
||||
if plate:
|
||||
_draw_digit_plate()
|
||||
if st.tex_glyph != null:
|
||||
draw_texture_rect(st.tex_glyph, rect, false)
|
||||
else:
|
||||
_draw_glyph(glyph, glyph_color, maxi(18, int(ceil(st.edge * 0.5)))) # 字号下限 18(§7.4 冻结公式)
|
||||
if st.badge_glyph != "":
|
||||
if st.tex_badge != null:
|
||||
draw_texture_rect(st.tex_badge, rect, false)
|
||||
else:
|
||||
_draw_badge(st.badge_glyph)
|
||||
# ④ 封印(现行视觉封印文字在 ③ 文字位承载——独立槽位留给封印图)
|
||||
if st.tex_seal != null:
|
||||
draw_texture_rect(st.tex_seal, rect, false)
|
||||
|
||||
|
||||
## 数字色牌后备域(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌;
|
||||
## 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1);
|
||||
## 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌)
|
||||
func _draw_digit_plate() -> void:
|
||||
var pw := st.edge * 0.60
|
||||
var ph := st.edge * 0.62
|
||||
var pr := Rect2((st.edge - pw) * 0.5, (st.edge - ph) * 0.5, pw, ph)
|
||||
draw_rect(pr, UiKit.DIGIT_PLATE, true)
|
||||
draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0)
|
||||
|
||||
|
||||
## ⑤⑥⑦ 顶层域:路径点迹 + 焦点高亮 + 视野迷雾 + 中立移除退色
|
||||
func _draw_overlay_layers(rect: Rect2) -> void:
|
||||
# ⑤ 路径点迹 + 焦点高亮(悬停态覆盖层,不参与命中)
|
||||
if st.dot_visible:
|
||||
var ds := 6.0
|
||||
draw_rect(Rect2(Vector2((st.edge - ds) * 0.5, (st.edge - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true)
|
||||
if st.hl_visible:
|
||||
# 悬停高亮减淡(金框太明显——绘制时 55% alpha,hl_color 属性与探针断言面不动)
|
||||
draw_rect(Rect2(1, 1, st.edge - 2.0, st.edge - 2.0), Color(st.hl_color, 0.55), false, 2.0) # 2px 内描边
|
||||
_draw_fog_layer()
|
||||
# ⑦ 中立移除退色层(退色降档:暗幕压顶全层覆盖,暗淡记忆态同族呈现语义;
|
||||
# 顶层于迷雾之上——移除格不再参与迷雾语义。本实现移除集=未揭示危险格(无数字无封印印))
|
||||
if st.removed_dim:
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0.12, 0.10, 0.09, 0.62), true)
|
||||
var rect := Rect2(Vector2.ZERO, Vector2(edge, edge))
|
||||
base.draw_base(self, rect) # 基座层整树(描边/立面/投影/接触带/倒角与其补画序)
|
||||
face.draw_marks(self, rect) # 变异标记
|
||||
face.draw_glyph_layer(self, rect, glyph, glyph_color, plate) # 字形/色牌/徽章/封印
|
||||
face.draw_overlay_under(self, rect) # 点迹+高亮(雾层由宿主夹在其后)
|
||||
_draw_fog_layer(rect) # 宿主迷雾域(原位——雾夹在 hl 与 removed 之间)
|
||||
face.draw_removed_dim(self, rect) # 退色层
|
||||
|
||||
|
||||
## ⑥ 视野迷雾层(最顶遮罩,§6.5 落文):
|
||||
## 全黑=不透明纯黑盖全部叠层(含封印印/徽章/高亮——封印印记随格隐没,§6.5 显式推论);
|
||||
## 记忆态=记忆板素色压平(纹理免疫——色牌/封印印/徽章随板隐没)+数字记忆墨重画(数字可读硬指标 ≥4.5:1)
|
||||
func _draw_fog_layer() -> void:
|
||||
func _draw_fog_layer(rect: Rect2) -> void:
|
||||
if fog_state == 1:
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0, 0, 0, 1.0), true)
|
||||
draw_rect(rect, Color(0, 0, 0, 1.0), true)
|
||||
elif fog_state == 2:
|
||||
_draw_fog_memory()
|
||||
|
||||
|
||||
## 墙格轮廓域:地面朝向边补 2px 深墨线(各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合)
|
||||
func _draw_outline_sides() -> void:
|
||||
var ow := 2.0
|
||||
var oc := UiKit.FOG_OUTLINE
|
||||
for b in st.outline_sides:
|
||||
var or_ := Rect2()
|
||||
match int(b):
|
||||
0: or_ = Rect2(0, 0, st.edge, ow)
|
||||
1: or_ = Rect2(0, st.edge - ow, st.edge, ow)
|
||||
2: or_ = Rect2(0, 0, ow, st.edge)
|
||||
3: or_ = Rect2(st.edge - ow, 0, ow, st.edge)
|
||||
draw_rect(or_, oc, true)
|
||||
|
||||
|
||||
## 接触带微光域:正交整条边 + 斜向共享角点与两侧半条边
|
||||
func _draw_contact_bands() -> void:
|
||||
var band := 4.0
|
||||
var half := st.edge * 0.5
|
||||
for b in st.contact_bands:
|
||||
var r := Rect2()
|
||||
match int(b):
|
||||
0: r = Rect2(0, 0, st.edge, band)
|
||||
1: r = Rect2(0, st.edge - band, st.edge, band)
|
||||
2: r = Rect2(0, 0, band, st.edge)
|
||||
3: r = Rect2(st.edge - band, 0, band, st.edge)
|
||||
draw_rect(r, st.contact_col, true)
|
||||
for cc in st.contact_corners:
|
||||
_draw_contact_corner(int(cc), band, half)
|
||||
|
||||
|
||||
## 斜向角点域:角点圆 r=6 贴角点圆心 + 近白芯 r=2.5 提亮 + 半条边(角点沿两条邻边各延伸 edge/2——
|
||||
## 坐标推导:横带沿 N/S 自角点向板心侧 half;NW/SE 半带 x=y=角点坐标,NE/SE 半带自 edge-half 收进——
|
||||
## 正交整边时重叠同色无害)
|
||||
func _draw_contact_corner(cc: int, band: float, half: float) -> void:
|
||||
var pt := Vector2.ZERO
|
||||
match cc:
|
||||
0: pt = Vector2(0, 0)
|
||||
1: pt = Vector2(st.edge, 0)
|
||||
2: pt = Vector2(0, st.edge)
|
||||
3: pt = Vector2(st.edge, st.edge)
|
||||
draw_circle(pt, 6.0, st.contact_col)
|
||||
draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, st.contact_col.a * 0.95)))
|
||||
if cc == 0 or cc == 1:
|
||||
# N 侧横带:NW 自 x=0、NE 自 x=edge-half
|
||||
draw_rect(Rect2(pt.x if cc == 0 else pt.x - half, 0.0, half, band), st.contact_col, true)
|
||||
if cc == 2 or cc == 3:
|
||||
# S 侧横带:SW 自 x=0、SE 自 x=edge-half
|
||||
draw_rect(Rect2(pt.x if cc == 2 else pt.x - half, st.edge - band, half, band), st.contact_col, true)
|
||||
if cc == 0 or cc == 2:
|
||||
# W 侧竖带:NW 自 y=0、SW 自 y=edge-half
|
||||
draw_rect(Rect2(0.0, pt.y if cc == 0 else pt.y - half, band, half), st.contact_col, true)
|
||||
if cc == 1 or cc == 3:
|
||||
# E 侧竖带:NE 自 y=0、SE 自 y=edge-half
|
||||
draw_rect(Rect2(st.edge - band, pt.y if cc == 1 else pt.y - half, band, half), st.contact_col, true)
|
||||
|
||||
|
||||
## 顶面倒角域(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让
|
||||
## 呼吸微光/主角蓝边框保持最外沿可见;强度随档位)
|
||||
func _draw_bevel() -> void:
|
||||
if st.bevel <= 0.0:
|
||||
return
|
||||
var base: Color = st.bg_sb.bg_color
|
||||
var lit: Color = base.lightened(0.22 * st.bevel)
|
||||
var dark: Color = base.darkened(0.22 * st.bevel)
|
||||
var t := 2.0
|
||||
draw_rect(Rect2(t, t, st.edge - t * 2.0, t), lit, true)
|
||||
draw_rect(Rect2(t, t, t, st.edge - t * 2.0), lit, true)
|
||||
draw_rect(Rect2(t, st.edge - t * 2.0, st.edge - t * 2.0, t), dark, true)
|
||||
draw_rect(Rect2(st.edge - t * 2.0, t, t, st.edge - t * 2.0), dark, true)
|
||||
|
||||
|
||||
## 变异标记域:真视格=危险红内缩框/未开格伤害型变异=左下角危险红三角
|
||||
func _draw_mutation_marks() -> void:
|
||||
if st.treasure_mark == "mine_reveal":
|
||||
var inset := st.edge * 0.14
|
||||
draw_rect(Rect2(inset, inset, st.edge - inset * 2.0, st.edge - inset * 2.0),
|
||||
Color(UiKit.RED_MINE, 0.9), false, 2.0)
|
||||
if st.mutation_mark == "flat_damage":
|
||||
var mw := st.edge * 0.28
|
||||
var mpts := PackedVector2Array([Vector2(0, st.edge - mw), Vector2(0, st.edge), Vector2(mw, st.edge)])
|
||||
draw_colored_polygon(mpts, Color(UiKit.RED_MINE, 0.85))
|
||||
_draw_fog_memory(rect)
|
||||
|
||||
|
||||
## 记忆态换装域(demo 口径「地板纹理 + 灰罩 α.66」:
|
||||
## 地板纹理原样直画 + FOG_MEMORY_PLATE α.66 灰罩压顶;素材零改动、格边长零改动。
|
||||
## 记忆墨 FOG_MEMORY_INK 对合成底归一后(真实渲染面)三关 5.71–5.93:1 ≥4.5:1(§7.2 硬指标;
|
||||
## 测点行值与素材随动,见 ui_contrast_probe 迷雾段))
|
||||
func _draw_fog_memory() -> void:
|
||||
if st.tex_base != null:
|
||||
draw_texture_rect(st.tex_base, Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), false)
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(UiKit.FOG_MEMORY_PLATE, 0.66), true)
|
||||
func _draw_fog_memory(rect: Rect2) -> void:
|
||||
if base.tex_base != null:
|
||||
draw_texture_rect(base.tex_base, rect, false)
|
||||
draw_rect(rect, Color(UiKit.FOG_MEMORY_PLATE, 0.66), true)
|
||||
if glyph != "":
|
||||
_draw_glyph(glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(st.edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源)
|
||||
|
||||
|
||||
# 南立面(D 档件一——墙格南缘内侧,自底边向上:双段暗带在下、口沿高光线贴其上沿):
|
||||
# 坐标推导(自格底边起算):远段=[edge−far, edge],近段=[edge−far−near, edge−far],
|
||||
# 高光线=[edge−far−near−hl, edge−far−near]——与研究件「墙顶面→高光→暗段→更暗段→格界」下行序一致
|
||||
func _draw_south_facade() -> void:
|
||||
if not st.facade_south:
|
||||
return
|
||||
var far_h := st.edge * FACADE_FAR_FRAC
|
||||
var near_h := st.edge * FACADE_NEAR_FRAC
|
||||
var y_far := st.edge - far_h
|
||||
var y_near := y_far - near_h
|
||||
var y_hl := y_near - FACADE_HIGHLIGHT_PX
|
||||
draw_rect(Rect2(0.0, y_hl, st.edge, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点)
|
||||
draw_rect(Rect2(0.0, y_near, st.edge, near_h), FACADE_NEAR_COL, true)
|
||||
draw_rect(Rect2(0.0, y_far, st.edge, far_h), FACADE_FAR_COL, true)
|
||||
|
||||
|
||||
# 地面投影(D 档件二——北邻墙格在本已开格北缘投渐隐暗带,自北缘向南三段衰减):
|
||||
# 坐标推导:三等分 proj_band,α 0.28/0.16/0.06(研究件实测贴墙 α≈0.28、带末渐融入地面)
|
||||
func _draw_north_projection() -> void:
|
||||
if st.proj_band <= 0.0:
|
||||
return
|
||||
var seg := st.proj_band / 3.0
|
||||
draw_rect(Rect2(0.0, 0.0, st.edge, seg), PROJECTION_NEAR_COL, true)
|
||||
draw_rect(Rect2(0.0, seg, st.edge, seg), PROJECTION_MID_COL, true)
|
||||
draw_rect(Rect2(0.0, seg * 2.0, st.edge, seg), PROJECTION_EDGE_COL, true)
|
||||
|
||||
|
||||
# E 格高墙面(圣经 v3.5 冻结①——方向稿 draw_facade 移植):本格已开且南邻为墙 →
|
||||
# 南部 30% 承接墙面。材质 = 墙顶 center 瓦片顶部 30% 区带**原亮度**直贴(零暗档零阴影);
|
||||
# 墙脚阴线只画地面/墙面交界(strip 顶缘 1px);墙面底缘(格界处)不画任何线——直接接南侧墙顶
|
||||
# (白边导致两截)。仅瓦片态服役(tex_facade 缺位 = 平色回退原语义不动)。
|
||||
func _draw_e_facade() -> void:
|
||||
if not st.facade_strip or st.tex_facade == null:
|
||||
return
|
||||
var strip_h := st.edge * E_FACADE_FRAC
|
||||
var y0 := st.edge - strip_h
|
||||
var ts := st.tex_facade.get_size()
|
||||
draw_texture_rect_region(st.tex_facade, Rect2(0.0, y0, st.edge, strip_h),
|
||||
Rect2(0.0, 0.0, ts.x, ts.y * E_FACADE_FRAC))
|
||||
draw_rect(Rect2(0.0, y0, st.edge, 1.0), UiKit.E_FACADE_FOOT_COL, true)
|
||||
|
||||
|
||||
# 文字位绘制:水平居中 + 垂直居中(基线 = (格高 − 字高)/2 + 上伸部)
|
||||
func _draw_glyph(g: String, col: Color, fs: int) -> void:
|
||||
var f := st.font if st.font != null else ThemeDB.fallback_font
|
||||
if f == null:
|
||||
return
|
||||
var asc := f.get_ascent(fs)
|
||||
var desc := f.get_descent(fs)
|
||||
draw_string(f, Vector2(0.0, (st.edge - (asc + desc)) * 0.5 + asc), g,
|
||||
HORIZONTAL_ALIGNMENT_CENTER, st.edge, fs, col)
|
||||
face.draw_centered_text(self, rect, glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源)
|
||||
|
||||
|
||||
# 微光子件生命周期:有组才建、组变即换、无组即撤——作用集 ≤8 格常驻,
|
||||
# 其余格零节点零绘制成本。与父件 _draw() 分离的三条理由见本件头注(命令序列/重绘预算/材质挂点)。
|
||||
func _sync_glow_layer() -> void:
|
||||
if st.glow_group == 0:
|
||||
if glow_group == 0:
|
||||
if _glow_layer != null and is_instance_valid(_glow_layer):
|
||||
_glow_layer.queue_free()
|
||||
_glow_layer = null
|
||||
@@ -410,15 +96,5 @@ func _sync_glow_layer() -> void:
|
||||
if _glow_layer == null or not is_instance_valid(_glow_layer):
|
||||
_glow_layer = _GLOW_LAYER.new()
|
||||
add_child(_glow_layer)
|
||||
_glow_layer.setup(st.edge, st.glow_group)
|
||||
_glow_layer.set_badge_mask(st.badge_glyph != "")
|
||||
|
||||
|
||||
# 徽章绘制:右下角金(文字贴角内缩 2–3px)
|
||||
func _draw_badge(g: String) -> void:
|
||||
var f := st.font if st.font != null else ThemeDB.fallback_font
|
||||
if f == null:
|
||||
return
|
||||
var fs := maxi(10, int(st.edge * 0.28))
|
||||
var sz := f.get_string_size(g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs)
|
||||
draw_string(f, Vector2(st.edge - sz.x - 2.0, st.edge - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD)
|
||||
_glow_layer.setup(edge, glow_group)
|
||||
_glow_layer.set_badge_mask(face.badge_glyph != "")
|
||||
@@ -0,0 +1,130 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 悬停预承诺呈现域件(BoardView 拆件——单一变化原因=悬停反馈语义):
|
||||
## 三语义光标、目标格边框、BFS 路径预览、chord 双击首遇锚的唯一承载(自 board_view 收编 11 函数族)。
|
||||
## 无 class_name(类名缓存面纪律)——经宿主 board_view preload 引用;构造=引用束注入
|
||||
## (cells/minesweeper/diggable 共享引用+格命中换算/壳级光标写入/hover_forbidden 与 chord 发射注入 Callable——
|
||||
## 输入经宿主三入站口委托,禁反向持宿主引用);_r0 出生展开区成员留宿主。
|
||||
extends RefCounted
|
||||
|
||||
const COL_HL_TARGET := UiKit.GOLD # 悬停目标格边框(金)
|
||||
const COL_SEAL := UiKit.SEAL_BLACK # 封印黑(与宿主 :658 双侧服役——各自 alias UiKit 单源)
|
||||
const CURSOR_DIG := Input.CURSOR_CROSS # 挖——镐形自绘纹理载体
|
||||
const CURSOR_MOVE_SEM := Input.CURSOR_ARROW # 移动——箭头(命名避开 Control.CURSOR_MOVE 原生成员遮蔽)
|
||||
const CHORD_WINDOW_MS := 350
|
||||
|
||||
var _cells := {} # 共享引用(宿主容器同对象——rebuild 只 clear 不重绑)
|
||||
var _minesweeper = null # 排雷玩法 只读查询引用(宿主 rebuild 重绑时同拍回写本成员)
|
||||
var _diggable := {} # 共享引用(可挖域——呼吸微光作用集同源)
|
||||
var _cb_cell_at: Callable # 格命中换算(宿主 _cell_at——整图公式单点留宿主)
|
||||
var _cb_cursor: Callable # 壳级光标写入(宿主 mouse_default_cursor_shape 单点)
|
||||
var _cb_forbidden: Callable # hover_forbidden 信号发射(首遇 toast 去重归组合根)
|
||||
var _cb_chord: Callable # chord_attempted 信号发射(chord 肌肉记忆提示锚)
|
||||
var _hover := Vector2i(-9, -9)
|
||||
var _last_click := {"cell": Vector2i(-9, -9), "ms": -100000}
|
||||
|
||||
|
||||
func _init(bundle: Dictionary) -> void:
|
||||
_cells = bundle["cells"]
|
||||
_minesweeper = bundle["minesweeper"]
|
||||
_diggable = bundle["diggable"]
|
||||
_cb_cell_at = bundle["cell_at"]
|
||||
_cb_cursor = bundle["cursor"]
|
||||
_cb_forbidden = bundle["forbidden"]
|
||||
_cb_chord = bundle["chord"]
|
||||
|
||||
|
||||
func _hover_at(pos: Vector2) -> void:
|
||||
var c: Vector2i = _cb_cell_at.call(pos)
|
||||
if c.x < 0:
|
||||
if _hover.x >= 0:
|
||||
_on_cell_unhover(_hover)
|
||||
elif c != _hover:
|
||||
if _hover.x >= 0:
|
||||
_on_cell_unhover(_hover)
|
||||
_on_cell_hover(c)
|
||||
|
||||
|
||||
func _on_shell_mouse_exited() -> void:
|
||||
if _hover.x >= 0:
|
||||
_on_cell_unhover(_hover)
|
||||
|
||||
|
||||
# chord 肌肉记忆首遇锚:同格双左击快开尝试(350ms 窗)——每 run 一次提示由组合根去重
|
||||
func _detect_chord(c: Vector2i) -> void:
|
||||
var now := Time.get_ticks_msec()
|
||||
if _last_click.cell == c and now - int(_last_click.ms) <= CHORD_WINDOW_MS:
|
||||
_cb_chord.call(c)
|
||||
_last_click.ms = -100000
|
||||
return
|
||||
_last_click = {"cell": c, "ms": now}
|
||||
|
||||
|
||||
func _on_cell_hover(c: Vector2i) -> void:
|
||||
_hover = c
|
||||
_apply_hover_visuals(c)
|
||||
|
||||
|
||||
func _on_cell_unhover(c: Vector2i) -> void:
|
||||
if _hover == c:
|
||||
_hover = Vector2i(-9, -9)
|
||||
_clear_hover_visuals(c)
|
||||
|
||||
|
||||
func _refresh_hover_after_paint() -> void:
|
||||
# 状态随 refresh 变化后光标/边框语义需重算(悬停格四格态可能已变)
|
||||
if _hover.x >= 0 and _cells.has(_hover):
|
||||
_apply_hover_visuals(_hover)
|
||||
|
||||
|
||||
func _apply_hover_visuals(c: Vector2i) -> void:
|
||||
if _minesweeper == null or not _cells.has(c):
|
||||
return
|
||||
_clear_hover_visuals(c)
|
||||
var u: Node2D = _cells[c]
|
||||
var revealed: bool = _minesweeper.is_revealed(c)
|
||||
var seal: bool = _minesweeper.has_seal(c)
|
||||
var adjacent: bool = _diggable.has(c)
|
||||
if revealed:
|
||||
u.face.hl_visible = true
|
||||
u.face.hl_color = COL_HL_TARGET # 移动目标格边框(金)
|
||||
_set_cursor_shape(c, CURSOR_MOVE_SEM)
|
||||
_show_path_preview(c)
|
||||
elif adjacent:
|
||||
u.face.hl_visible = true
|
||||
u.face.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖)
|
||||
_set_cursor_shape(c, CURSOR_DIG)
|
||||
elif seal:
|
||||
u.face.hl_visible = true
|
||||
u.face.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供)
|
||||
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
|
||||
else:
|
||||
u.face.hl_visible = true
|
||||
u.face.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道)
|
||||
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
|
||||
_cb_forbidden.call(c)
|
||||
|
||||
|
||||
func _clear_hover_visuals(c: Vector2i) -> void:
|
||||
if not _cells.has(c):
|
||||
return
|
||||
_cells[c].face.hl_visible = false
|
||||
_hide_path_preview()
|
||||
|
||||
|
||||
func _set_cursor_shape(c: Vector2i, shape: int) -> void:
|
||||
_cb_cursor.call(shape) # 壳级光标(悬停格语义决定——单格悬停恒一)
|
||||
|
||||
|
||||
func _show_path_preview(c: Vector2i) -> void:
|
||||
# 合法性指示:悬停已开格 → BFS 路径蓝细线点迹(compute_move_path 只读面)
|
||||
if _minesweeper == null:
|
||||
return
|
||||
var path: Array[Variant] = _minesweeper.compute_move_path(c)
|
||||
for pc in path:
|
||||
if _cells.has(pc):
|
||||
_cells[pc].face.dot_visible = true
|
||||
|
||||
|
||||
func _hide_path_preview() -> void:
|
||||
for c in _cells:
|
||||
_cells[c].face.dot_visible = false
|
||||
@@ -0,0 +1 @@
|
||||
uid://dojreaughx7tf
|
||||
@@ -12,6 +12,7 @@ const _CELL_UNIT := preload("res://presentation/views/board_cell_unit.gd")
|
||||
const _BOARD_WORLD := preload("res://presentation/views/board_world.gd")
|
||||
const _TILE_PALETTE := preload("res://presentation/views/tile_palette.gd")
|
||||
const _HERO_SPRITE := preload("res://presentation/views/hero_sprite.gd")
|
||||
const _BOARD_HOVER := preload("res://presentation/views/board_hover.gd")
|
||||
|
||||
const COL_BRICK := UiKit.BRICK # 未开——掩尘完整地砖(色板原语 #C9AF83)
|
||||
const COL_FLOOR := UiKit.FLOOR # 已开——裸土 #B39A78(碎裂裸露地面)
|
||||
@@ -20,17 +21,13 @@ const COL_HERO := UiKit.HERO_BLUE # 小人边框——小人蓝
|
||||
const COL_SEAL := UiKit.SEAL_BLACK # 封印黑 #000000
|
||||
const COL_GOLD := UiKit.GOLD # 奖励金 #F0A828
|
||||
const COL_CONTACT := UiKit.CONTACT_GLOW # 接触带微光基色(蓝族浅一档·近白蓝——§3.2 v2.8)
|
||||
const COL_HL_TARGET := UiKit.GOLD # 悬停目标格边框(金)
|
||||
const REWARD_BADGE := { # 已开格格面资源身份 icon(cell_type 九型键)
|
||||
"attack": "攻", "defense": "守", "heal": "医", "blast": "爆", "weaken": "弱", "pierce": "穿", "magic": "魔",
|
||||
"action": "行", # 行 = 行动点型(ap_up/ap_drain 两行)
|
||||
"gp": "金", # 金 = GP 型(gp_gain/gp_loss 两行;金色系 ~40° 相位,禁入带紫赤 300–345° 外,取色 UiKit.GOLD 徽标色单点)
|
||||
}
|
||||
const BADGE_PLACEHOLDER := "?" # 未知图标键占位纹样(HUD框架:禁静默空图标)
|
||||
const CURSOR_DIG := Input.CURSOR_CROSS # 挖——镐形自绘纹理载体
|
||||
const CURSOR_SEAL := Input.CURSOR_POINTING_HAND # 封印——封印形自绘纹理载体
|
||||
const CURSOR_MOVE_SEM := Input.CURSOR_ARROW # 移动——箭头(命名避开 Control.CURSOR_MOVE 原生成员遮蔽)
|
||||
const CHORD_WINDOW_MS := 350
|
||||
const SEP := 2 # 格间距(格尺寸公式与命中换算共用)
|
||||
|
||||
# 棋盘立体参数
|
||||
@@ -67,8 +64,6 @@ var _contact_unit: Node2D = null # 接触带承载单元(=英雄格 CellUnit
|
||||
var _install_tw: Tween # 入场淡入 tween(rebuild 重入先杀)
|
||||
var _ripple_tws: Array[Tween] = [] # R0 涟漪逐格 tween(rebuild 重入全杀)
|
||||
var _dig_glow := false # 挖掘期才允许唯一在动元素(art-bible §2.1)
|
||||
var _hover := Vector2i(-9, -9)
|
||||
var _last_click := {"cell": Vector2i(-9, -9), "ms": -100000}
|
||||
var _r0 := {} # 出生展开区 R₀(地图战斗资源 mapping 资格域 = S∖R₀——徽章查询跳过,防 E19 诊断噪音)
|
||||
var _desat_mat: ShaderMaterial
|
||||
var _desat_tween: Tween
|
||||
@@ -77,6 +72,7 @@ var _cell := 46.0 # 当前格边长(set_cell_area 按容器尺寸重
|
||||
# ── 拆出件(状态单点在件内)──
|
||||
var _palette = _TILE_PALETTE.new() # 瓦片件:装载缓存+查表+明度/墨族静态算法
|
||||
var _hero_sprite = _HERO_SPRITE.new() # 英雄精灵件:_init 挂为本件子级(tween 需在树上)
|
||||
var _hover_comp: _BOARD_HOVER # 悬停预承诺呈现域件:三语义光标/目标格边框/路径预览/chord 首遇锚(_init 建为子件——HeroSprite 挂载先例)
|
||||
|
||||
# ── 件成员同名转发(探针直读面零改动:读 _palette.seal_tex、读写 _hero_sprite.tex_idle;其余为随迁状态单点再导出)──
|
||||
# ---- 视野迷雾(纯呈现层;现行事实源=HUD框架「视野迷雾」节 v0.13,渲染规格=art-bible §6.5 v3.5)----
|
||||
@@ -120,6 +116,25 @@ func _init() -> void:
|
||||
_hero_sprite.tex_idle = UiKit.load_tex(UiKit.TEX_HERO_IDLE)
|
||||
_hero_sprite.tex_walk = UiKit.load_tex(UiKit.TEX_HERO_WALK)
|
||||
_palette.load_board_tiles() # 高墙/地面瓦片整批守卫装载(缺位静默回退——英雄同款纪律,禁刷屏)
|
||||
_hover_comp = _BOARD_HOVER.new({
|
||||
"cells": _cells, "minesweeper": _minesweeper, "diggable": _diggable,
|
||||
"cell_at": _cell_at, "cursor": _set_shell_cursor,
|
||||
"forbidden": _emit_hover_forbidden, "chord": _emit_chord_attempted,
|
||||
})
|
||||
|
||||
|
||||
# ---- 悬停域注入面(件回调宿主的壳级单点——Callable 目标)----
|
||||
|
||||
func _set_shell_cursor(shape: int) -> void:
|
||||
mouse_default_cursor_shape = shape # 壳级光标(悬停格语义决定——单格悬停恒一)
|
||||
|
||||
|
||||
func _emit_hover_forbidden(c: Vector2i) -> void:
|
||||
hover_forbidden.emit(c) ## 非相邻未开格悬停(首遇 toast 归组合根去重)
|
||||
|
||||
|
||||
func _emit_chord_attempted(c: Vector2i) -> void:
|
||||
chord_attempted.emit(c) ## 双击快开尝试(chord 肌肉记忆提示锚)
|
||||
|
||||
|
||||
func board() -> BoardData:
|
||||
@@ -128,7 +143,7 @@ func board() -> BoardData:
|
||||
|
||||
func _ready() -> void:
|
||||
_apply_custom_cursors()
|
||||
mouse_exited.connect(_on_shell_mouse_exited)
|
||||
mouse_exited.connect(_hover_comp._on_shell_mouse_exited)
|
||||
|
||||
|
||||
# 结算期/道具窗口态:D 区去饱和 + 金橙偏移锁输入(明度不变——art-bible §2);
|
||||
@@ -182,7 +197,7 @@ func set_treasure_overlays(reveal_cells: Array, preview_cells: Array) -> void:
|
||||
func set_world_font(f: Font) -> void:
|
||||
_world.board_font = f
|
||||
for c in _cells:
|
||||
_cells[c].st.font = f
|
||||
_cells[c].face.font = f
|
||||
_cells[c].queue_redraw()
|
||||
|
||||
|
||||
@@ -201,7 +216,7 @@ func set_cell_area(avail: Vector2) -> void:
|
||||
var stride := _cell + float(SEP)
|
||||
for c in _cells:
|
||||
var u: Node2D = _cells[c]
|
||||
u.st.edge = _cell
|
||||
u.edge = _cell
|
||||
u.position = Vector2(c) * stride
|
||||
if _world.hero != null and _hero_sprite.last.x >= 0:
|
||||
_place_hero(_hero_sprite.last, _hero_sprite.last, true) # 格边长变更(窗口缩放)——英雄即时随格重置(杀位移 tween 防飞陈旧落点)
|
||||
@@ -224,6 +239,7 @@ func _refresh_slab() -> void:
|
||||
func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
|
||||
_board = board
|
||||
_minesweeper = minesweeper
|
||||
_hover_comp._minesweeper = _minesweeper # 件内只读引用随宿主重绑同拍回写(每战新排雷玩法对象)
|
||||
_palette.bind_board(_board, _minesweeper) # 查表族棋盘态随板同步(换板后首拍 refresh 前置)
|
||||
for child in _world.get_children():
|
||||
child.queue_free()
|
||||
@@ -234,7 +250,7 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
|
||||
_contact_unit = null
|
||||
_hero_sprite.last = Vector2i(-99, -99)
|
||||
_r0.clear()
|
||||
_hover = Vector2i(-9, -9)
|
||||
_hover_comp._hover = Vector2i(-9, -9)
|
||||
for c in _board.compute_r0():
|
||||
_r0[c] = true
|
||||
# 瓦片套随板图型切换(board_gen 词表 v1.1 dungeon/underdark/deep_underdark;
|
||||
@@ -255,17 +271,17 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
|
||||
# 逐格构建器(rebuild 建格循环唯一出函面):.new() 与属性装配逐字迁入,循环体只调构建器
|
||||
func _build_cell_unit(c: Vector2i, stride: float) -> Node2D:
|
||||
var u: Node2D = _CELL_UNIT.new()
|
||||
u.st.cell = c
|
||||
u.st.edge = _cell
|
||||
u.cell = c
|
||||
u.edge = _cell
|
||||
u.position = Vector2(c) * stride
|
||||
u.st.bevel = DEPTH_BEVEL # 新建格单元继承
|
||||
u.base.bevel = DEPTH_BEVEL # 新建格单元继承
|
||||
u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化
|
||||
if _palette.tiles_ok:
|
||||
# NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档,
|
||||
# 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片
|
||||
# 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化)
|
||||
u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
u.st.font = _world.board_font
|
||||
u.face.font = _world.board_font
|
||||
_world.add_child(u)
|
||||
return u
|
||||
|
||||
@@ -283,7 +299,7 @@ func refresh() -> void:
|
||||
for y in _board.height:
|
||||
for x in _board.width:
|
||||
_paint_cell(Vector2i(x, y), hero)
|
||||
_refresh_hover_after_paint()
|
||||
_hover_comp._refresh_hover_after_paint()
|
||||
_apply_contact_bands()
|
||||
_update_hero(hero) # 英雄精灵 diff 驱动
|
||||
|
||||
@@ -433,33 +449,18 @@ func _update_world_size() -> void:
|
||||
|
||||
|
||||
# 真实输入路径(Node2D 迁移批 Decision 3):壳层 _gui_input → 整图公式换算 → 复用既有 hover/点选/chord 出口
|
||||
#(悬停族已拆 board_hover 件——本壳留三入站口委托)
|
||||
func _gui_input(ev: InputEvent) -> void:
|
||||
if _minesweeper == null:
|
||||
return
|
||||
if ev is InputEventMouseMotion:
|
||||
_hover_at(ev.position)
|
||||
_hover_comp._hover_at(ev.position)
|
||||
elif ev is InputEventMouseButton and ev.pressed:
|
||||
var c := _cell_at(ev.position)
|
||||
if c.x >= 0:
|
||||
_on_cell_input(c, ev)
|
||||
|
||||
|
||||
func _hover_at(pos: Vector2) -> void:
|
||||
var c := _cell_at(pos)
|
||||
if c.x < 0:
|
||||
if _hover.x >= 0:
|
||||
_on_cell_unhover(_hover)
|
||||
elif c != _hover:
|
||||
if _hover.x >= 0:
|
||||
_on_cell_unhover(_hover)
|
||||
_on_cell_hover(c)
|
||||
|
||||
|
||||
func _on_shell_mouse_exited() -> void:
|
||||
if _hover.x >= 0:
|
||||
_on_cell_unhover(_hover)
|
||||
|
||||
|
||||
func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
|
||||
if _minesweeper == null or not (ev is InputEventMouseButton and ev.pressed):
|
||||
return
|
||||
@@ -468,7 +469,7 @@ func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
|
||||
action_requested.emit(c, "move")
|
||||
else:
|
||||
action_requested.emit(c, "dig")
|
||||
_detect_chord(c)
|
||||
_hover_comp._detect_chord(c)
|
||||
elif ev.button_index == MOUSE_BUTTON_RIGHT:
|
||||
if not _minesweeper.has_seal(c):
|
||||
action_requested.emit(c, "seal_place")
|
||||
@@ -476,89 +477,6 @@ func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
|
||||
# 左键相邻封印格直接挖(规则引擎放行)。
|
||||
|
||||
|
||||
# chord 肌肉记忆首遇锚:同格双左击快开尝试(350ms 窗)——每 run 一次提示由组合根去重
|
||||
func _detect_chord(c: Vector2i) -> void:
|
||||
var now := Time.get_ticks_msec()
|
||||
if _last_click.cell == c and now - int(_last_click.ms) <= CHORD_WINDOW_MS:
|
||||
chord_attempted.emit(c)
|
||||
_last_click.ms = -100000
|
||||
return
|
||||
_last_click = {"cell": c, "ms": now}
|
||||
|
||||
|
||||
# ---- 悬停预承诺(pattern 4):三语义光标 + 目标格边框 + BFS 路径预览 ----
|
||||
|
||||
func _on_cell_hover(c: Vector2i) -> void:
|
||||
_hover = c
|
||||
_apply_hover_visuals(c)
|
||||
|
||||
|
||||
func _on_cell_unhover(c: Vector2i) -> void:
|
||||
if _hover == c:
|
||||
_hover = Vector2i(-9, -9)
|
||||
_clear_hover_visuals(c)
|
||||
|
||||
|
||||
func _refresh_hover_after_paint() -> void:
|
||||
# 状态随 refresh 变化后光标/边框语义需重算(悬停格四格态可能已变)
|
||||
if _hover.x >= 0 and _cells.has(_hover):
|
||||
_apply_hover_visuals(_hover)
|
||||
|
||||
|
||||
func _apply_hover_visuals(c: Vector2i) -> void:
|
||||
if _minesweeper == null or not _cells.has(c):
|
||||
return
|
||||
_clear_hover_visuals(c)
|
||||
var u: Node2D = _cells[c]
|
||||
var revealed: bool = _minesweeper.is_revealed(c)
|
||||
var seal: bool = _minesweeper.has_seal(c)
|
||||
var adjacent: bool = _diggable.has(c)
|
||||
if revealed:
|
||||
u.st.hl_visible = true
|
||||
u.st.hl_color = COL_HL_TARGET # 移动目标格边框(金)
|
||||
_set_cursor_shape(c, CURSOR_MOVE_SEM)
|
||||
_show_path_preview(c)
|
||||
elif adjacent:
|
||||
u.st.hl_visible = true
|
||||
u.st.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖)
|
||||
_set_cursor_shape(c, CURSOR_DIG)
|
||||
elif seal:
|
||||
u.st.hl_visible = true
|
||||
u.st.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供)
|
||||
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
|
||||
else:
|
||||
u.st.hl_visible = true
|
||||
u.st.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道)
|
||||
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
|
||||
hover_forbidden.emit(c)
|
||||
|
||||
|
||||
func _clear_hover_visuals(c: Vector2i) -> void:
|
||||
if not _cells.has(c):
|
||||
return
|
||||
_cells[c].st.hl_visible = false
|
||||
_hide_path_preview()
|
||||
|
||||
|
||||
func _set_cursor_shape(c: Vector2i, shape: int) -> void:
|
||||
mouse_default_cursor_shape = shape # 壳级光标(悬停格语义决定——单格悬停恒一)
|
||||
|
||||
|
||||
func _show_path_preview(c: Vector2i) -> void:
|
||||
# 合法性指示:悬停已开格 → BFS 路径蓝细线点迹(compute_move_path 只读面)
|
||||
if _minesweeper == null:
|
||||
return
|
||||
var path: Array[Variant] = _minesweeper.compute_move_path(c)
|
||||
for pc in path:
|
||||
if _cells.has(pc):
|
||||
_cells[pc].st.dot_visible = true
|
||||
|
||||
|
||||
func _hide_path_preview() -> void:
|
||||
for c in _cells:
|
||||
_cells[c].st.dot_visible = false
|
||||
|
||||
|
||||
# ---- 奖励徽章(已开格身份 icon——封印之下、纹带之上、不进数字区) ----
|
||||
|
||||
func _update_badge(c: Vector2i) -> String:
|
||||
@@ -613,19 +531,19 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
var is_hero: bool = c == hero
|
||||
# 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal
|
||||
# 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。
|
||||
u.st.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null
|
||||
u.face.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null
|
||||
if removed:
|
||||
revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结)
|
||||
seal = false
|
||||
u.st.tex_seal = null
|
||||
u.face.tex_seal = null
|
||||
# 绘制层五段(字形与色牌→基底瓦片→立面投影→秘宝/变异叠加→迷雾)——段间交换经单元状态面
|
||||
#(u.st/u.glyph/u.glyph_color),唯一例外=基底色 bg 经 ink 段返回值交 base 段(无单元场域承载)
|
||||
#(u.base/u.face 域件与 u.glyph/u.glyph_color),唯一例外=基底色 bg 经 ink 段返回值交 base 段(无单元场域承载)
|
||||
var bg: Color = _paint_cell_ink(c, u, revealed, seal)
|
||||
_paint_cell_base(c, u, revealed, is_hero, bg)
|
||||
_paint_cell_facade(c, u, revealed)
|
||||
_paint_cell_overlays(c, u, revealed)
|
||||
_paint_cell_fog(c, u, revealed, hero)
|
||||
u.st.removed_dim = removed
|
||||
u.face.removed_dim = removed
|
||||
u.queue_redraw()
|
||||
|
||||
|
||||
@@ -696,17 +614,17 @@ func _paint_cell_base(c: Vector2i, u: Node2D, revealed: bool, is_hero: bool, bg:
|
||||
# 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载)
|
||||
u.glyph = "人"
|
||||
u.glyph_color = COL_HERO
|
||||
u.st.tex_base = tile
|
||||
u.st.bg_sb = sb
|
||||
u.base.tex_base = tile
|
||||
u.base.bg_sb = sb
|
||||
|
||||
|
||||
# 立面投影段:未开墙格南缘立面/轮廓描边码;已开格北缘投影暗带/E 格高墙面——段内重置与赋值同序
|
||||
func _paint_cell_facade(c: Vector2i, u: Node2D, revealed: bool) -> void:
|
||||
u.st.outline_sides.assign([]) # typed Array[int](assign 保持引用语义)
|
||||
u.st.facade_south = false
|
||||
u.st.proj_band = 0.0
|
||||
u.st.tex_facade = null
|
||||
u.st.facade_strip = false
|
||||
u.base.outline_sides.assign([]) # typed Array[int](assign 保持引用语义)
|
||||
u.base.facade_south = false
|
||||
u.base.proj_band = 0.0
|
||||
u.base.tex_facade = null
|
||||
u.base.facade_strip = false
|
||||
# 封印槽不在此重置:尾部重置会与此上方条件赋值构成双写踩踏(把封印纹理恒清 null=素材在位时封印不可见);
|
||||
# 上方 else 分支已是全路径清残留,尾部重置系冗余且有害
|
||||
var south_n: Vector2i = c + Vector2i(0, 1)
|
||||
@@ -716,26 +634,26 @@ func _paint_cell_facade(c: Vector2i, u: Node2D, revealed: bool) -> void:
|
||||
if not revealed:
|
||||
# 高墙感 D 投影档:南邻地面格 → 墙格南缘立「口沿高光+双段暗带」立面
|
||||
#(邻地面才画——墙-墙内界保墙群内聚);板外=墙不画
|
||||
u.st.facade_south = south_in and _minesweeper.is_revealed(south_n)
|
||||
u.base.facade_south = south_in and _minesweeper.is_revealed(south_n)
|
||||
# 轮廓加强:墙格四邻中已开(地面)格朝向记描边码(板外=墙不描——保墙群外轮廓只在墙/地交界)
|
||||
for d in [Vector2i(0, -1), Vector2i(0, 1), Vector2i(-1, 0), Vector2i(1, 0)]:
|
||||
var n: Vector2i = c + d
|
||||
if n.x >= 0 and n.y >= 0 and n.x < _board.width and n.y < _board.height and _minesweeper.is_revealed(n):
|
||||
var code := 0 if d.y < 0 else (1 if d.y > 0 else (2 if d.x < 0 else 3))
|
||||
if code == 1 and u.st.facade_south:
|
||||
if code == 1 and u.base.facade_south:
|
||||
continue # 南缘由立面件承载(高光+双段暗带)——2px 描边让位防三重叠黑
|
||||
if code == 0 and _palette.tiles_ok:
|
||||
continue # v3.5 冻结① (b)-(c) 交界无边框:墙面直贴接墙顶(E 格墙面件在邻格承载)——北向描边不画(平色回退态保留描边)
|
||||
u.st.outline_sides.append(code)
|
||||
u.base.outline_sides.append(code)
|
||||
else:
|
||||
# 地面投影:北邻墙格在本已开格北缘投渐隐暗带(ITB 同款高度线索——带宽按格边比例常量)
|
||||
if north_in and not _minesweeper.is_revealed(north_n):
|
||||
u.st.proj_band = u.st.edge * WALL_PROJECTION_FRAC
|
||||
u.base.proj_band = u.edge * WALL_PROJECTION_FRAC
|
||||
# E 格高墙面(圣经 v3.5 冻结①——方向稿 is_facade_cell 口径):已开格南邻未开墙 → 南部 30%
|
||||
# 承接墙面(材质=同板墙顶 center 瓦片,原亮度直贴);仅瓦片态启用——平色回退保持原语义
|
||||
if south_in and not _minesweeper.is_revealed(south_n) and _palette.tiles_ok:
|
||||
u.st.facade_strip = true
|
||||
u.st.tex_facade = _palette.tiles.get("wall_center")
|
||||
u.base.facade_strip = true
|
||||
u.base.tex_facade = _palette.tiles.get("wall_center")
|
||||
|
||||
|
||||
# 秘宝/变异叠加段:真视红框/预读徽章/变异角标/奖励徽章/类型微光组号——glyph/digit_col 局部镜像
|
||||
@@ -746,21 +664,21 @@ func _paint_cell_overlays(c: Vector2i, u: Node2D, revealed: bool) -> void:
|
||||
var glyph: String = u.glyph
|
||||
var digit_col: Color = u.glyph_color
|
||||
if not revealed:
|
||||
u.st.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else ""
|
||||
u.face.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else ""
|
||||
if _treasure_preview.has(c) and glyph == "":
|
||||
var pid = _identity_cb.call(c) if _identity_cb.is_valid() else null
|
||||
if pid != null and REWARD_BADGE.has(String(pid.get("cell_type", ""))):
|
||||
glyph = String(REWARD_BADGE[String(pid.get("cell_type", ""))])
|
||||
digit_col = COL_GOLD
|
||||
u.st.badge_glyph = glyph
|
||||
u.face.badge_glyph = glyph
|
||||
# ②' 变异角标:未开格带伤害型变异 → 危险红角标(已开=威胁已兑现标记退位);
|
||||
# 回调缺位 = 无标记(程序化角标,tex_mutation 纹带槽暂留)
|
||||
if not revealed and _vmark_cb.is_valid():
|
||||
u.st.mutation_mark = String(_vmark_cb.call(c))
|
||||
u.face.mutation_mark = String(_vmark_cb.call(c))
|
||||
else:
|
||||
u.st.mutation_mark = ""
|
||||
u.st.badge_glyph = _update_badge(c)
|
||||
u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
u.face.mutation_mark = ""
|
||||
u.face.badge_glyph = _update_badge(c)
|
||||
u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
|
||||
|
||||
# 迷雾段:灯光区外未开格全黑/已开格记忆态灰显——纯呈现零逻辑(🔴 契约 1)
|
||||
@@ -803,7 +721,7 @@ func _sync_glow_to_units() -> void:
|
||||
for c in touched:
|
||||
var u: Node2D = _cells.get(c)
|
||||
if u != null:
|
||||
u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
@@ -815,14 +733,14 @@ func _process(delta: float) -> void:
|
||||
if UiKit.motion_scale < 1.0:
|
||||
return # 减动效:呼吸停格(refresh/enable 时已施静态色——静态可辨、无持续运动)
|
||||
var k := 0.5 + 0.5 * sin(_breath_t * 3.0) # 3 rad/s≈0.48Hz——无 >3 次/秒频闪(AC 基线)
|
||||
_contact_unit.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
|
||||
_contact_unit.base.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
|
||||
_contact_unit.queue_redraw()
|
||||
|
||||
|
||||
# 带几何写入(refresh / set_dig_glow_enabled(true) 挂点):清旧承载件→按英雄格可挖域写边/角码→施色
|
||||
func _apply_contact_bands() -> void:
|
||||
if _contact_unit != null and is_instance_valid(_contact_unit) and (_cells.get(_minesweeper.hero_pos()) != _contact_unit if _minesweeper != null else true):
|
||||
_contact_unit.st.contact_col = Color(0, 0, 0, 0)
|
||||
_contact_unit.base.contact_col = Color(0, 0, 0, 0)
|
||||
_contact_unit.queue_redraw()
|
||||
_contact_unit = null
|
||||
if _board == null or _minesweeper == null or not _dig_glow:
|
||||
@@ -839,19 +757,19 @@ func _apply_contact_bands() -> void:
|
||||
for d in [Vector2i(-1, -1), Vector2i(1, -1), Vector2i(-1, 1), Vector2i(1, 1)]:
|
||||
if _diggable.has(_minesweeper.hero_pos() + d):
|
||||
corners.append(0 if (d.x < 0 and d.y < 0) else (1 if d.x > 0 and d.y < 0 else (2 if d.x < 0 else 3)))
|
||||
hu.st.contact_bands = bands
|
||||
hu.st.contact_corners = corners
|
||||
hu.base.contact_bands = bands
|
||||
hu.base.contact_corners = corners
|
||||
# 减动效:呼吸停格——refresh/enable 重放时施**静态**色(本件头注「已施静态色」的落実);
|
||||
# 0.5 = 呼吸周期中值(正常态 k 的均值)。否则减动效下每次 refresh 都会按当前相位重施色,
|
||||
# 带色在 refresh 间跳变=持续运动假象,且令 on/off 双帧取证无法隔离单一变量。
|
||||
var k := 0.5 if UiKit.motion_scale < 1.0 else 0.5 + 0.5 * sin(_breath_t * 3.0)
|
||||
hu.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
|
||||
hu.base.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
|
||||
hu.queue_redraw()
|
||||
|
||||
|
||||
func _reset_breath() -> void:
|
||||
if _contact_unit != null and is_instance_valid(_contact_unit):
|
||||
_contact_unit.st.contact_col = Color(0, 0, 0, 0)
|
||||
_contact_unit.base.contact_col = Color(0, 0, 0, 0)
|
||||
_contact_unit.queue_redraw()
|
||||
|
||||
|
||||
@@ -898,9 +816,9 @@ func diggable_probe() -> Dictionary:
|
||||
return _diggable.duplicate()
|
||||
|
||||
|
||||
# 悬停格只读出口(case① 悬停到达断言消费面)
|
||||
# 悬停格只读出口(case① 悬停到达断言消费面——读件投影,返回语义零变化)
|
||||
func hover_probe() -> Vector2i:
|
||||
return _hover
|
||||
return _hover_comp._hover
|
||||
|
||||
|
||||
# 迷雾参数只读快照(case⑮ 参数漂移断言消费面)
|
||||
@@ -926,5 +844,5 @@ func cell_rect_probe(c: Vector2i) -> Rect2:
|
||||
if host.size.x <= 0.0 or host.size.y <= 0.0 or vp_size.x <= 0.0 or vp_size.y <= 0.0:
|
||||
return Rect2()
|
||||
var k: Vector2 = host.size / vp_size
|
||||
var local := Rect2(u.global_position, Vector2(u.st.edge, u.st.edge))
|
||||
var local := Rect2(u.global_position, Vector2(u.edge, u.edge))
|
||||
return Rect2(host.position + local.position * k, local.size * k)
|
||||
@@ -0,0 +1,204 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 格面基座层绘制域件(CellUnit 拆件——单一变化原因=基座层视觉规格):
|
||||
## 底面/描边/高墙立面/地面投影/接触带微光/顶面倒角的全部绘制入参与绘制命令。
|
||||
## 无 class_name(类名缓存面纪律)——经宿主 board_cell_unit preload 引用;宿主 _draw 按层序调
|
||||
## draw_base(canvas, rect) 把绘制命令落到宿主画布;setter 经 _owner 回引触发宿主重绘语义逐字。
|
||||
extends RefCounted
|
||||
|
||||
# ── D 投影档绘制常量(研究件实测取色——立面高光/双段暗带/地面投影;黑罩 alpha 叠加实测等效)──
|
||||
const FACADE_HIGHLIGHT_PX := 2.0 # 口沿高光线厚(像素)
|
||||
const FACADE_NEAR_FRAC := 0.16 # 立面近段高(格边比——研究件 10/64)
|
||||
const FACADE_FAR_FRAC := 0.25 # 立面远段高(格边比——研究件 16/64)
|
||||
const FACADE_NEAR_COL := Color(0, 0, 0, 0.60) # 立面近段黑罩(墙顶 132→52 实测 α≈0.61)
|
||||
const FACADE_FAR_COL := Color(0, 0, 0, 0.82) # 立面远段黑罩(墙顶 132→24 实测 α≈0.82)
|
||||
const PROJECTION_NEAR_COL := Color(0, 0, 0, 0.28) # 投影贴墙段(地面 66→46 实测 α≈0.28)
|
||||
const PROJECTION_MID_COL := Color(0, 0, 0, 0.16) # 投影中段(渐隐)
|
||||
const PROJECTION_EDGE_COL := Color(0, 0, 0, 0.06) # 投影远段(渐隐收边)
|
||||
# ── E 格高墙面(圣经 v3.5 冻结①——地面格南邻墙→南部 30% 承接墙面:墙顶纹理原亮度
|
||||
# 直贴零暗档零阴影;墙脚阴线只画地面/墙面交界;(b)-(c) 交界
|
||||
# 无边框由 BoardView 侧跳过墙格北向描边承载)──
|
||||
const E_FACADE_FRAC := 0.30 # 墙面承接高(格边比 30%)
|
||||
|
||||
var _owner: Node2D = null
|
||||
var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框)
|
||||
# ── 接触带微光绘制面(可挖域微光画在英雄格周界空间侧——art-bible §3.2 v2.8;
|
||||
# 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)──
|
||||
var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边)
|
||||
var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边)
|
||||
var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画)
|
||||
var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(程序侧结构件)
|
||||
# ── 高墙感 D 投影档(研究件四变体定 D=C+地面投影,ITB 同款高度线索)──
|
||||
# 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格
|
||||
# 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带
|
||||
# 0.22 格边;坐标推导留档 commit)。素材缺位程序化回退同样服役(结构归程序——art-bible §8.5)。
|
||||
var facade_south := false: # 南立面开关(BoardView paint 按「南邻已开格」写入)
|
||||
set(v):
|
||||
facade_south = v
|
||||
_owner.queue_redraw()
|
||||
var proj_band := 0.0: # 地面投影带宽(像素,0 = 不画——BoardView 按北邻墙判定写入)
|
||||
set(v):
|
||||
proj_band = v
|
||||
_owner.queue_redraw()
|
||||
var bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0)
|
||||
set(v):
|
||||
bevel = v
|
||||
_owner.queue_redraw()
|
||||
var tex_base: Texture2D = null # ① 砖面底(宿主迷雾记忆态公开单向读)
|
||||
var tex_facade: Texture2D = null # ⑥ E 格墙面材质(v3.5——南邻墙顶 center 瓦片同源,BoardView 注入)
|
||||
var facade_strip := false: # E 格开关(BoardView paint 按「已开格且南邻未开墙」写入——瓦片态专用)
|
||||
set(v):
|
||||
facade_strip = v
|
||||
_owner.queue_redraw()
|
||||
|
||||
|
||||
func _init(owner: Node2D) -> void:
|
||||
_owner = owner
|
||||
|
||||
|
||||
## ① 砖面底层域(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角;瓦片态描边覆盖层;
|
||||
## 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘。素材缺位回退走 elif 程序化原样,零观感变化)
|
||||
func draw_base(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if tex_base != null:
|
||||
canvas.draw_texture_rect(tex_base, rect, false)
|
||||
# 轮廓加强(轮廓线不明显):墙格地面朝向边补 2px 深墨线(§8.5 结构归程序——
|
||||
# 素材 1px 墨线在 45/36px 非整数缩放下视觉衰减,程序线保底;墙-墙内界不画,保墙群内聚;
|
||||
# 坐标推导:各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合)
|
||||
if not outline_sides.is_empty():
|
||||
_draw_outline_sides(canvas, rect)
|
||||
_draw_south_facade(canvas, rect)
|
||||
_draw_north_projection(canvas, rect)
|
||||
_draw_e_facade(canvas, rect)
|
||||
# 瓦片态描边覆盖层:tex_base 在位时 BoardView 传入的 bg_sb
|
||||
# 已降级为「透明底 + 描边」载体——呼吸微光/主角蓝框画在瓦片之上(art-bible §1.2 叠层序
|
||||
# 不变:底 < 纹带 < 数字 < 封印 < 焦点高亮);素材缺位回退走 elif 程序化原样,零观感变化
|
||||
if bg_sb != null:
|
||||
canvas.draw_style_box(bg_sb, rect)
|
||||
_draw_contact_or_fallback(canvas, rect)
|
||||
|
||||
|
||||
## 接触带微光或平色回退域(英雄格周界空间侧——正交邻墙=整条边;斜向=共享角点+两侧各半条边;光不进墙格):
|
||||
## 带宽 4px 贴格边内侧(坐标推导:N=[0,0,edge,4] S=[0,edge-4,edge,4] W=[0,0,4,edge] E=[edge-4,0,4,edge];
|
||||
## 半条边=沿邻边自角点起 edge/2;角点圆 r=6 贴角点圆心,近白芯 r=2.5 提亮)
|
||||
func _draw_contact_or_fallback(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if contact_col.a > 0.0:
|
||||
_draw_north_projection(canvas, rect) # 平色回退+微光并发的英雄格补画(瓦片路径已画——同像素重复无观感差)
|
||||
_draw_e_facade(canvas, rect) # E 格墙面补画(同上——英雄格兼 E 格时瓦片路径已画,重复无害)
|
||||
_draw_contact_bands(canvas, rect)
|
||||
elif bg_sb != null:
|
||||
canvas.draw_style_box(bg_sb, rect)
|
||||
_draw_south_facade(canvas, rect) # 平色回退路径——D 投影档两件同样服役(结构归程序)
|
||||
_draw_north_projection(canvas, rect)
|
||||
_draw_bevel(canvas, rect)
|
||||
|
||||
|
||||
## 墙格轮廓域:地面朝向边补 2px 深墨线(各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合)
|
||||
func _draw_outline_sides(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
var ow := 2.0
|
||||
var oc := UiKit.FOG_OUTLINE
|
||||
for b in outline_sides:
|
||||
var or_ := Rect2()
|
||||
match int(b):
|
||||
0: or_ = Rect2(0, 0, rect.size.x, ow)
|
||||
1: or_ = Rect2(0, rect.size.x - ow, rect.size.x, ow)
|
||||
2: or_ = Rect2(0, 0, ow, rect.size.x)
|
||||
3: or_ = Rect2(rect.size.x - ow, 0, ow, rect.size.x)
|
||||
canvas.draw_rect(or_, oc, true)
|
||||
|
||||
|
||||
## 接触带微光域:正交整条边 + 斜向共享角点与两侧半条边
|
||||
func _draw_contact_bands(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
var band := 4.0
|
||||
var half := rect.size.x * 0.5
|
||||
for b in contact_bands:
|
||||
var r := Rect2()
|
||||
match int(b):
|
||||
0: r = Rect2(0, 0, rect.size.x, band)
|
||||
1: r = Rect2(0, rect.size.x - band, rect.size.x, band)
|
||||
2: r = Rect2(0, 0, band, rect.size.x)
|
||||
3: r = Rect2(rect.size.x - band, 0, band, rect.size.x)
|
||||
canvas.draw_rect(r, contact_col, true)
|
||||
for cc in contact_corners:
|
||||
_draw_contact_corner(canvas, rect, int(cc), band, half)
|
||||
|
||||
|
||||
## 斜向角点域:角点圆 r=6 贴角点圆心 + 近白芯 r=2.5 提亮 + 半条边(角点沿两条邻边各延伸 edge/2——
|
||||
## 坐标推导:横带沿 N/S 自角点向板心侧 half;NW/SE 半带 x=y=角点坐标,NE/SE 半带自 edge-half 收进——
|
||||
## 正交整边时重叠同色无害)
|
||||
func _draw_contact_corner(canvas: CanvasItem, rect: Rect2, cc: int, band: float, half: float) -> void:
|
||||
var pt := Vector2.ZERO
|
||||
match cc:
|
||||
0: pt = Vector2(0, 0)
|
||||
1: pt = Vector2(rect.size.x, 0)
|
||||
2: pt = Vector2(0, rect.size.x)
|
||||
3: pt = Vector2(rect.size.x, rect.size.x)
|
||||
canvas.draw_circle(pt, 6.0, contact_col)
|
||||
canvas.draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, contact_col.a * 0.95)))
|
||||
if cc == 0 or cc == 1:
|
||||
# N 侧横带:NW 自 x=0、NE 自 x=edge-half
|
||||
canvas.draw_rect(Rect2(pt.x if cc == 0 else pt.x - half, 0.0, half, band), contact_col, true)
|
||||
if cc == 2 or cc == 3:
|
||||
# S 侧横带:SW 自 x=0、SE 自 x=edge-half
|
||||
canvas.draw_rect(Rect2(pt.x if cc == 2 else pt.x - half, rect.size.x - band, half, band), contact_col, true)
|
||||
if cc == 0 or cc == 2:
|
||||
# W 侧竖带:NW 自 y=0、SW 自 y=edge-half
|
||||
canvas.draw_rect(Rect2(0.0, pt.y if cc == 0 else pt.y - half, band, half), contact_col, true)
|
||||
if cc == 1 or cc == 3:
|
||||
# E 侧竖带:NE 自 y=0、SE 自 y=edge-half
|
||||
canvas.draw_rect(Rect2(rect.size.x - band, pt.y if cc == 1 else pt.y - half, band, half), contact_col, true)
|
||||
|
||||
|
||||
## 顶面倒角域(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让
|
||||
## 呼吸微光/主角蓝边框保持最外沿可见;强度随档位)
|
||||
func _draw_bevel(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if bevel <= 0.0:
|
||||
return
|
||||
var base: Color = bg_sb.bg_color
|
||||
var lit: Color = base.lightened(0.22 * bevel)
|
||||
var dark: Color = base.darkened(0.22 * bevel)
|
||||
var t := 2.0
|
||||
canvas.draw_rect(Rect2(t, t, rect.size.x - t * 2.0, t), lit, true)
|
||||
canvas.draw_rect(Rect2(t, t, t, rect.size.x - t * 2.0), lit, true)
|
||||
canvas.draw_rect(Rect2(t, rect.size.x - t * 2.0, rect.size.x - t * 2.0, t), dark, true)
|
||||
canvas.draw_rect(Rect2(rect.size.x - t * 2.0, t, t, rect.size.x - t * 2.0), dark, true)
|
||||
|
||||
|
||||
# 南立面(D 档件一——墙格南缘内侧,自底边向上:双段暗带在下、口沿高光线贴其上沿):
|
||||
# 坐标推导(自格底边起算):远段=[edge−far, edge],近段=[edge−far−near, edge−far],
|
||||
# 高光线=[edge−far−near−hl, edge−far−near]——与研究件「墙顶面→高光→暗段→更暗段→格界」下行序一致
|
||||
func _draw_south_facade(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if not facade_south:
|
||||
return
|
||||
var far_h := rect.size.x * FACADE_FAR_FRAC
|
||||
var near_h := rect.size.x * FACADE_NEAR_FRAC
|
||||
var y_far := rect.size.x - far_h
|
||||
var y_near := y_far - near_h
|
||||
var y_hl := y_near - FACADE_HIGHLIGHT_PX
|
||||
canvas.draw_rect(Rect2(0.0, y_hl, rect.size.x, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点)
|
||||
canvas.draw_rect(Rect2(0.0, y_near, rect.size.x, near_h), FACADE_NEAR_COL, true)
|
||||
canvas.draw_rect(Rect2(0.0, y_far, rect.size.x, far_h), FACADE_FAR_COL, true)
|
||||
|
||||
|
||||
# 地面投影(D 档件二——北邻墙格在本已开格北缘投渐隐暗带,自北缘向南三段衰减):
|
||||
# 坐标推导:三等分 proj_band,α 0.28/0.16/0.06(研究件实测贴墙 α≈0.28、带末渐融入地面)
|
||||
func _draw_north_projection(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if proj_band <= 0.0:
|
||||
return
|
||||
var seg := proj_band / 3.0
|
||||
canvas.draw_rect(Rect2(0.0, 0.0, rect.size.x, seg), PROJECTION_NEAR_COL, true)
|
||||
canvas.draw_rect(Rect2(0.0, seg, rect.size.x, seg), PROJECTION_MID_COL, true)
|
||||
canvas.draw_rect(Rect2(0.0, seg * 2.0, rect.size.x, seg), PROJECTION_EDGE_COL, true)
|
||||
|
||||
|
||||
# E 格高墙面(圣经 v3.5 冻结①——方向稿 draw_facade 移植):本格已开且南邻为墙 →
|
||||
# 南部 30% 承接墙面。材质 = 墙顶 center 瓦片顶部 30% 区带**原亮度**直贴(零暗档零阴影);
|
||||
# 墙脚阴线只画地面/墙面交界(strip 顶缘 1px);墙面底缘(格界处)不画任何线——直接接南侧墙顶
|
||||
# (白边导致两截)。仅瓦片态服役(tex_facade 缺位 = 平色回退原语义不动)。
|
||||
func _draw_e_facade(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if not facade_strip or tex_facade == null:
|
||||
return
|
||||
var strip_h := rect.size.x * E_FACADE_FRAC
|
||||
var y0 := rect.size.x - strip_h
|
||||
var ts := tex_facade.get_size()
|
||||
canvas.draw_texture_rect_region(tex_facade, Rect2(0.0, y0, rect.size.x, strip_h),
|
||||
Rect2(0.0, 0.0, ts.x, ts.y * E_FACADE_FRAC))
|
||||
canvas.draw_rect(Rect2(0.0, y0, rect.size.x, 1.0), UiKit.E_FACADE_FOOT_COL, true)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2msg115y3oir
|
||||
@@ -0,0 +1,145 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 格面标识与顶层叠加绘制域件(CellUnit 拆件——单一变化原因=标识与叠加层视觉规格):
|
||||
## 变异标记/数字色牌与字形/奖励徽章/封印/路径点迹/焦点高亮/中立移除退色的全部绘制入参与绘制命令。
|
||||
## 无 class_name(类名缓存面纪律)——经宿主 board_cell_unit preload 引用;宿主 _draw 按层序调
|
||||
## draw_marks/draw_glyph_layer/draw_overlay_under/draw_removed_dim(雾层由宿主夹在 overlay_under 与 removed_dim 之间);
|
||||
## setter 经 _owner 回引触发宿主重绘语义逐字(badge_glyph 的 glow mask 回引 _owner._glow_layer 原样)。
|
||||
extends RefCounted
|
||||
|
||||
var _owner: Node2D = null
|
||||
var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区)
|
||||
set(v):
|
||||
badge_glyph = v
|
||||
if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer):
|
||||
_owner._glow_layer.set_badge_mask(v != "")
|
||||
_owner.queue_redraw()
|
||||
var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝)
|
||||
set(v):
|
||||
dot_visible = v
|
||||
_owner.queue_redraw()
|
||||
var hl_visible := false: # 焦点高亮(悬停态边框覆盖层)
|
||||
set(v):
|
||||
hl_visible = v
|
||||
_owner.queue_redraw()
|
||||
var hl_color := UiKit.GOLD: # 高亮色(金目标/封印朱/禁入灰禁红)
|
||||
set(v):
|
||||
hl_color = v
|
||||
_owner.queue_redraw()
|
||||
var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入)
|
||||
# ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)──
|
||||
var tex_mutation: Texture2D = null # ② 变异纹带
|
||||
var tex_glyph: Texture2D = null # ③ 数字
|
||||
var tex_badge: Texture2D = null # ③' 徽章
|
||||
var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark
|
||||
set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示))
|
||||
if tex_seal != v:
|
||||
tex_seal = v
|
||||
_owner.queue_redraw()
|
||||
# 变异角标(程序化,tex_mutation 纹带槽暂留):
|
||||
# "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记)
|
||||
# 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写):
|
||||
# "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,封印照常计费);
|
||||
# "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形)
|
||||
var treasure_mark := "":
|
||||
set(v):
|
||||
if treasure_mark != v:
|
||||
treasure_mark = v
|
||||
_owner.queue_redraw()
|
||||
var mutation_mark := "":
|
||||
set(v):
|
||||
if mutation_mark != v:
|
||||
mutation_mark = v
|
||||
_owner.queue_redraw()
|
||||
var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入)
|
||||
set(v):
|
||||
removed_dim = v
|
||||
_owner.queue_redraw()
|
||||
|
||||
|
||||
func _init(owner: Node2D) -> void:
|
||||
_owner = owner
|
||||
|
||||
|
||||
## 变异标记域:真视格=危险红内缩框/未开格伤害型变异=左下角危险红三角
|
||||
func draw_marks(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if treasure_mark == "mine_reveal":
|
||||
var inset := rect.size.x * 0.14
|
||||
canvas.draw_rect(Rect2(inset, inset, rect.size.x - inset * 2.0, rect.size.x - inset * 2.0),
|
||||
Color(UiKit.RED_MINE, 0.9), false, 2.0)
|
||||
if mutation_mark == "flat_damage":
|
||||
var mw := rect.size.x * 0.28
|
||||
var mpts := PackedVector2Array([Vector2(0, rect.size.x - mw), Vector2(0, rect.size.x), Vector2(mw, rect.size.x)])
|
||||
canvas.draw_colored_polygon(mpts, Color(UiKit.RED_MINE, 0.85))
|
||||
|
||||
|
||||
## ②③④ 中层域:变异纹带与标记/数字色牌与字形/徽章/封印
|
||||
##(glyph/glyph_color/plate 三者住宿主冻结面——探针直读成员,作参数传入)
|
||||
func draw_glyph_layer(canvas: CanvasItem, rect: Rect2, glyph: String, glyph_color: Color, plate: bool) -> void:
|
||||
# ② 变异纹带(无程序化纹带——槽位先行,贴图落位即用)
|
||||
if tex_mutation != null:
|
||||
canvas.draw_texture_rect(tex_mutation, rect, false)
|
||||
# ③ 数字/徽章(数字 ≥ 格高 50% 墨族对格底 ≥4.5:1;徽章右下角金,遮蔽态 = "?")
|
||||
if glyph != "":
|
||||
if plate:
|
||||
_draw_digit_plate(canvas, rect)
|
||||
if tex_glyph != null:
|
||||
canvas.draw_texture_rect(tex_glyph, rect, false)
|
||||
else:
|
||||
draw_centered_text(canvas, rect, glyph, glyph_color, maxi(18, int(ceil(rect.size.x * 0.5)))) # 字号下限 18(§7.4 冻结公式)
|
||||
if badge_glyph != "":
|
||||
if tex_badge != null:
|
||||
canvas.draw_texture_rect(tex_badge, rect, false)
|
||||
else:
|
||||
_draw_badge(canvas, rect, badge_glyph)
|
||||
# ④ 封印(现行视觉封印文字在 ③ 文字位承载——独立槽位留给封印图)
|
||||
if tex_seal != null:
|
||||
canvas.draw_texture_rect(tex_seal, rect, false)
|
||||
|
||||
|
||||
## 数字色牌后备域(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌;
|
||||
## 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1);
|
||||
## 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌)
|
||||
func _draw_digit_plate(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
var pw := rect.size.x * 0.60
|
||||
var ph := rect.size.x * 0.62
|
||||
var pr := Rect2((rect.size.x - pw) * 0.5, (rect.size.x - ph) * 0.5, pw, ph)
|
||||
canvas.draw_rect(pr, UiKit.DIGIT_PLATE, true)
|
||||
canvas.draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0)
|
||||
|
||||
|
||||
## ⑤ 路径点迹 + 焦点高亮(悬停态覆盖层,不参与命中)——宿主时序器在其后插雾层
|
||||
func draw_overlay_under(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if dot_visible:
|
||||
var ds := 6.0
|
||||
canvas.draw_rect(Rect2(Vector2((rect.size.x - ds) * 0.5, (rect.size.x - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true)
|
||||
if hl_visible:
|
||||
# 悬停高亮减淡(金框太明显——绘制时 55% alpha,hl_color 属性与探针断言面不动)
|
||||
canvas.draw_rect(Rect2(1, 1, rect.size.x - 2.0, rect.size.x - 2.0), Color(hl_color, 0.55), false, 2.0) # 2px 内描边
|
||||
|
||||
|
||||
## ⑦ 中立移除退色层(退色降档:暗幕压顶全层覆盖,暗淡记忆态同族呈现语义;
|
||||
## 顶层于迷雾之上——移除格不再参与迷雾语义。本实现移除集=未揭示危险格(无数字无封印印))
|
||||
func draw_removed_dim(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if removed_dim:
|
||||
canvas.draw_rect(rect, Color(0.12, 0.10, 0.09, 0.62), true)
|
||||
|
||||
|
||||
## 文字位绘制单点(宿主迷雾记忆态复用——同公式同字号;水平居中 + 垂直居中,基线 = (格高 − 字高)/2 + 上伸部)
|
||||
func draw_centered_text(canvas: CanvasItem, rect: Rect2, text: String, col: Color, fs: int) -> void:
|
||||
var f := font if font != null else ThemeDB.fallback_font
|
||||
if f == null:
|
||||
return
|
||||
var asc := f.get_ascent(fs)
|
||||
var desc := f.get_descent(fs)
|
||||
canvas.draw_string(f, Vector2(0.0, (rect.size.x - (asc + desc)) * 0.5 + asc), text,
|
||||
HORIZONTAL_ALIGNMENT_CENTER, rect.size.x, fs, col)
|
||||
|
||||
|
||||
# 徽章绘制:右下角金(文字贴角内缩 2–3px)
|
||||
func _draw_badge(canvas: CanvasItem, rect: Rect2, g: String) -> void:
|
||||
var f := font if font != null else ThemeDB.fallback_font
|
||||
if f == null:
|
||||
return
|
||||
var fs := maxi(10, int(rect.size.x * 0.28))
|
||||
var sz := f.get_string_size(g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs)
|
||||
canvas.draw_string(f, Vector2(rect.size.x - sz.x - 2.0, rect.size.x - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c60e3su2qd1yq
|
||||
@@ -2,9 +2,9 @@
|
||||
## 站桩本体:B 区主角/敌人无框立绘+底座+血条+顺位/意图徽标的世界精灵(Node2D)。
|
||||
## 站桩形态为定案——禁回卡片式;立绘素材缺位回退字形占位;击杀呈现禁瞬消(灰化塌陷)。
|
||||
## 无 class_name——经 preload 引用;壳层坐标换算单点在 BattleStageView.world_to_shell()。
|
||||
## 内部状态件(u4 子域拆件——契约 9 职责语句「站桩演出状态」):单一变化原因=站桩呈现与演出,
|
||||
## 成员共同表达站桩的行进态/演出计数/资产挂点。外部直访冻结成员(box/font/slot_row/has_world_hp_bar)
|
||||
## 与源码锚承载成员(_intent_charge——theme 源锚句所在行逐字保留)住宿主。
|
||||
## 内部状态两件(u4 拆件——st=身份命值态 standee_vitals、mo=资产演出态 standee_motion,_init 自建);
|
||||
## 外部直访冻结成员(box/font/slot_row/has_world_hp_bar)与源码锚承载成员(_intent_charge——theme
|
||||
## 源锚句所在行逐字保留)与三个演出探针计数器住宿主。
|
||||
extends Node2D
|
||||
|
||||
const CHARGE_SKILL_IDS := ["heavy"]
|
||||
@@ -16,65 +16,28 @@ const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}
|
||||
const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值)
|
||||
const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍)
|
||||
|
||||
const _VITALS := preload("res://presentation/views/standee_vitals.gd")
|
||||
const _MOTION := preload("res://presentation/views/standee_motion.gd")
|
||||
|
||||
## 站桩演出状态(全部内部态单点——含 setter 触发宿主重绘语义,_owner 回引保持逐字)
|
||||
class _StandeeState extends RefCounted:
|
||||
var _owner: Node2D = null
|
||||
var _kind := "enemy"
|
||||
var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位)
|
||||
var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据)
|
||||
# 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理
|
||||
# (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。
|
||||
var _hero_tex := {}
|
||||
var _hero_state: int = 0
|
||||
var _state_revert_tw: Tween
|
||||
var _order := 1
|
||||
var _intent_glyph := ""
|
||||
var _intent_col := Color.WHITE
|
||||
var _intent_visible := false
|
||||
var _intent_scale := 1.0: # 意图弹入动画参数
|
||||
set(v):
|
||||
_intent_scale = v
|
||||
_owner.queue_redraw()
|
||||
var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画)
|
||||
set(v):
|
||||
_glyph_rot = v
|
||||
_owner.queue_redraw()
|
||||
var _display_name := ""
|
||||
var _enemy_id := ""
|
||||
var _armor := "light"
|
||||
var _dead := false
|
||||
var _hp_max := 1.0
|
||||
var _hp_cur := 0 # 血量数字显示面(HUD框架 验收「血量数字可读」)
|
||||
var hp_value := 1.0: # 血条值 0..1(直角递减——tween_property 目标,逐行刷新重启不累积)
|
||||
set(v):
|
||||
hp_value = clampf(v, 0.0, 1.0)
|
||||
_owner.queue_redraw()
|
||||
var _hp_tween: Tween
|
||||
var _hp_flash_tw: Tween
|
||||
var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数)
|
||||
var _hit_sparks := 0 # 探针出口计数(命中火花累计)
|
||||
var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面)
|
||||
|
||||
|
||||
var st := _StandeeState.new()
|
||||
var st: _VITALS # 身份与命值态件实例(名字沿用——外部路径串 "st.hp_value" 与约 40 处引用零改)
|
||||
var mo: _MOTION # 资产与演出态件实例(_init 自建)
|
||||
var box := Vector2(150, 176) # 站桩盒尺寸(实际高随壳层 row 布局注入;外部直访冻结)
|
||||
var font: Font = null # 世界字体槽位(null = ThemeDB.fallback_font——world.world_font 注入;外部直访冻结)
|
||||
var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随件(挂 BattleStageView 壳层,本体进世界;外部直访冻结)
|
||||
var _intent_charge := false # 蓄力档(pid ∈ CHARGE_SKILL_IDS——意图三档 ③ 分支判据;源码锚承载成员住宿主)
|
||||
# 血条存在性探针出口(世界直角血条常在;外部直访冻结)
|
||||
var has_world_hp_bar := true
|
||||
# 演出探针计数器(写入方=宿主演出函数、读取方=宿主 fx_probe 出口——仪器跟着驱动它的函数走)
|
||||
var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数)
|
||||
var _hit_sparks := 0 # 探针出口计数(命中火花累计)
|
||||
var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面)
|
||||
|
||||
|
||||
func _init(kind: String = "enemy") -> void:
|
||||
st._owner = self
|
||||
st._hero_state = HeroState.IDLE
|
||||
st = _VITALS.new(self)
|
||||
mo = _MOTION.new(self)
|
||||
st._kind = kind
|
||||
|
||||
|
||||
enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(外部 String 契约=register/set 形参零变化)
|
||||
const _HS_NAME := {HeroState.IDLE: "idle", HeroState.RESET: "reset", HeroState.ATTACK: "attack", HeroState.HIT: "hit"}
|
||||
const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": HeroState.ATTACK, "hit": HeroState.HIT}
|
||||
mo._hero_state = _MOTION.HeroState.IDLE
|
||||
|
||||
|
||||
func _efont() -> Font:
|
||||
@@ -92,7 +55,7 @@ func enemy_id() -> String:
|
||||
|
||||
# 意图徽标探针出口
|
||||
func intent_probe() -> Dictionary:
|
||||
return {"visible": st._intent_visible, "text": st._intent_glyph}
|
||||
return {"visible": mo._intent_visible, "text": mo._intent_glyph}
|
||||
|
||||
|
||||
# 名字行探针出口(ui_probe 呈现口径 + 本批 V6/V8 断言面:armor_icon = 绑定纹理路径〔缺位回退字形〕、
|
||||
@@ -118,7 +81,7 @@ func set_data(d: Dictionary) -> void:
|
||||
st._enemy_id = String(d.get("enemy_id", ""))
|
||||
st._armor = String(d.get("armor_type", "light"))
|
||||
st._hp_max = float(maxi(1, int(d.get("max_hp", 1))))
|
||||
_set_hp(int(d.get("hp", 0)))
|
||||
st.apply_hp(int(d.get("hp", 0)))
|
||||
_set_order(int(d.get("order", 1)))
|
||||
set_pending(String(d.get("pending_type", "")), String(d.get("pending_skill", "")))
|
||||
set_effects(d.get("effects", []))
|
||||
@@ -129,21 +92,11 @@ func set_data(d: Dictionary) -> void:
|
||||
|
||||
func set_hero(hp: int, max_hp: int) -> void:
|
||||
st._hp_max = float(maxi(1, max_hp))
|
||||
_set_hp(hp)
|
||||
st.apply_hp(hp)
|
||||
if st._dead and hp > 0:
|
||||
_revive() # 新战斗开场 HP 回正——主角死亡残位随之复活(敌侧 set_data 同款语义)
|
||||
|
||||
|
||||
# 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积
|
||||
func _set_hp(hp: int) -> void:
|
||||
st._hp_cur = clampi(hp, 0, int(st._hp_max))
|
||||
var target := float(st._hp_cur) / st._hp_max
|
||||
if st._hp_tween != null and st._hp_tween.is_valid():
|
||||
st._hp_tween.kill()
|
||||
st._hp_tween = create_tween()
|
||||
st._hp_tween.tween_property(st, "hp_value", target, UiKit.motion(0.15))
|
||||
|
||||
|
||||
# 主角/敌方共用——状态效果唯一展示面(壳层伴随件转发;空态 = 图标行隐藏不占位)
|
||||
func set_effects(effects: Array) -> void:
|
||||
if slot_row == null:
|
||||
@@ -182,12 +135,12 @@ func _set_order(i: int) -> void:
|
||||
func set_pending(ptype: String, pid: String) -> void:
|
||||
if st._dead:
|
||||
_intent_charge = false
|
||||
st._intent_visible = false
|
||||
mo._intent_visible = false
|
||||
queue_redraw()
|
||||
return
|
||||
if ptype == "" or ptype == "basic":
|
||||
_intent_charge = false
|
||||
st._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4)
|
||||
mo._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4)
|
||||
queue_redraw()
|
||||
return
|
||||
var glyph := "⚔" # 攻击系(damage)
|
||||
@@ -201,60 +154,15 @@ func set_pending(ptype: String, pid: String) -> void:
|
||||
_intent_charge = pid in CHARGE_SKILL_IDS
|
||||
if _intent_charge:
|
||||
col = UiKit.GOLD_INK # 重击类蓄力档(深金墨——浅垫牌上 ≥4.5:1)
|
||||
st._intent_glyph = glyph
|
||||
st._intent_col = col
|
||||
st._intent_visible = true
|
||||
_play_intent_motion()
|
||||
mo._intent_glyph = glyph
|
||||
mo._intent_col = col
|
||||
mo._intent_visible = true
|
||||
mo.play_intent_motion(_intent_charge)
|
||||
|
||||
|
||||
# 意图三档动作动画(§4.6.4 三档:①普通攻击=零表示〔set_pending 早退〕
|
||||
# ②特殊准备=徽标弹入+二次弹跳(高举语义,运动手段 §2)③重击/硬化=缓推蓄力+定格微顿+回落;
|
||||
# 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。
|
||||
func _play_intent_motion() -> void:
|
||||
var tw := create_tween()
|
||||
tw.tween_property(st, "_intent_scale", 1.6, UiKit.motion(0.10))
|
||||
tw.tween_property(st, "_intent_scale", 1.0, UiKit.motion(0.16))
|
||||
tw.parallel().tween_property(st, "_glyph_rot", 4.0, UiKit.motion(0.10))
|
||||
tw.tween_property(st, "_glyph_rot", 0.0, UiKit.motion(0.14))
|
||||
if _intent_charge:
|
||||
tw.tween_property(st, "_intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推
|
||||
tw.tween_interval(UiKit.motion(0.40)) # 定格微顿
|
||||
tw.tween_property(st, "_intent_scale", 1.0, UiKit.motion(0.18))
|
||||
else:
|
||||
tw.tween_property(st, "_intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举)
|
||||
tw.tween_property(st, "_intent_scale", 1.0, UiKit.motion(0.13))
|
||||
|
||||
|
||||
# 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。
|
||||
# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。盒高 96px = 48px 立绘源整数 2×
|
||||
# (圣经 §8.2 禁非整数缩放)——源换代时随源重算。
|
||||
# 资产挂点转调壳(签名外形零变——box 由宿主传入件内)
|
||||
func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void:
|
||||
if tex == null:
|
||||
if st._portrait_sprite != null:
|
||||
st._portrait_sprite.visible = false
|
||||
queue_redraw()
|
||||
return
|
||||
st._portrait_tex = tex
|
||||
if st._portrait_sprite == null:
|
||||
st._portrait_sprite = Sprite2D.new()
|
||||
st._portrait_sprite.centered = false
|
||||
st._portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊
|
||||
add_child(st._portrait_sprite)
|
||||
st._portrait_sprite.texture = tex
|
||||
st._portrait_sprite.flip_h = flip
|
||||
st._portrait_sprite.visible = true
|
||||
_place_portrait()
|
||||
|
||||
|
||||
func _place_portrait() -> void:
|
||||
# 立绘盒 = (0,44)-(box.x, box.y-36);48 源 ×2 整数档居中
|
||||
if st._portrait_sprite == null or st._portrait_tex == null:
|
||||
return
|
||||
var box_h := box.y - 44.0 - 36.0
|
||||
var w := st._portrait_tex.get_width() * 2.0
|
||||
var h := st._portrait_tex.get_height() * 2.0
|
||||
st._portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5)
|
||||
st._portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5)
|
||||
mo.set_portrait_texture(tex, flip, box)
|
||||
|
||||
|
||||
func mark_dead() -> void:
|
||||
@@ -268,8 +176,8 @@ func mark_dead() -> void:
|
||||
tw.tween_property(self, "rotation_degrees", 0.0, UiKit.motion(0.08))
|
||||
_spawn_death_shards() # 碎裂退场(§4.6.4 语义硬规则:灰化+碎裂禁瞬间消失;§8.4 v2 冻结 5 块 0.45s)
|
||||
_intent_charge = false
|
||||
st._intent_visible = false # 死亡残位意图退位
|
||||
_set_hp(0)
|
||||
mo._intent_visible = false # 死亡残位意图退位
|
||||
st.apply_hp(0)
|
||||
if slot_row != null:
|
||||
slot_row.visible = false # 状态行随 mark_dead 同拍隐藏
|
||||
|
||||
@@ -279,7 +187,7 @@ func mark_dead() -> void:
|
||||
|
||||
# 死亡碎块弹飞:立绘盒中心五块碎石抛物线散落(程序化色块——确定性散布,禁全局随机)
|
||||
func _spawn_death_shards() -> void:
|
||||
st._death_shards = DEATH_SHARD_N
|
||||
_death_shards = DEATH_SHARD_N
|
||||
var center := _portrait_rect().get_center()
|
||||
for i in DEATH_SHARD_N:
|
||||
_spawn_death_shard(i, center)
|
||||
@@ -305,7 +213,7 @@ func _spawn_death_shard(i: int, center: Vector2) -> void:
|
||||
|
||||
# 演出探针出口(fx_probe——死亡碎裂/命中火花/闪白计数断言面)
|
||||
func fx_probe() -> Dictionary:
|
||||
return {"death_shards": st._death_shards, "hit_sparks": st._hit_sparks, "flash_count": st._flash_count}
|
||||
return {"death_shards": _death_shards, "hit_sparks": _hit_sparks, "flash_count": _flash_count}
|
||||
|
||||
|
||||
# 命中瞬间打击特效:立绘盒中心六枚短火花放射(程序化色块,确定性方向
|
||||
@@ -319,7 +227,7 @@ func play_hit_impact() -> void:
|
||||
|
||||
## 单火花域(循环体提函数——放射弹道+渐隐+回收)
|
||||
func _spawn_hit_spark(i: int, center: Vector2) -> void:
|
||||
st._hit_sparks += 1
|
||||
_hit_sparks += 1
|
||||
var spark := ColorRect.new()
|
||||
spark.color = UiKit.GOLD_LIT if i % 2 == 0 else UiKit.WARM
|
||||
spark.size = Vector2(4, 3)
|
||||
@@ -340,7 +248,7 @@ func is_dead() -> bool:
|
||||
|
||||
# 受击闪白(结算演出占位——§2.2 受击反馈;强度时长维持现行)
|
||||
func flash_hit() -> void:
|
||||
st._flash_count += 1
|
||||
_flash_count += 1
|
||||
if st._hp_flash_tw != null and st._hp_flash_tw.is_valid():
|
||||
st._hp_flash_tw.kill()
|
||||
st._hp_flash_tw = create_tween()
|
||||
@@ -353,8 +261,7 @@ func _revive() -> void:
|
||||
modulate = Color(1, 1, 1)
|
||||
rotation_degrees = 0.0
|
||||
if st._kind == "hero":
|
||||
st._hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退)
|
||||
set_hero_state("idle")
|
||||
mo.reset_state()
|
||||
if slot_row != null:
|
||||
slot_row.visible = true
|
||||
queue_redraw()
|
||||
@@ -372,45 +279,24 @@ func _portrait_glyph() -> String:
|
||||
|
||||
# 立绘落位探针出口(站桩纹理非空断言面;缺位回退链保持的对照面)
|
||||
func portrait_probe() -> Dictionary:
|
||||
return {"has_tex": st._portrait_sprite != null and st._portrait_sprite.texture != null}
|
||||
return {"has_tex": mo._portrait_sprite != null and mo._portrait_sprite.texture != null}
|
||||
|
||||
|
||||
# ---- 主角三态切换(攻击演出→attack/受击→hit/结算回 idle;§8.4 帧数档挂点) ----
|
||||
|
||||
# 组合点注册(battle_stage_view _init——UiKit 常量 load_tex,缺位 null 不注册=该态保持现纹理)
|
||||
# 组合点注册转调壳(签名外形零变)
|
||||
func register_hero_state(state: String, tex: Texture2D) -> void:
|
||||
if tex != null:
|
||||
st._hero_tex[state] = tex
|
||||
mo.register_hero_state(state, tex)
|
||||
|
||||
|
||||
# 状态切换:现纹理换注册态(attack/hit);未注册态名或缺位 = 保持现纹理不炸。
|
||||
# 非 idle 态自动短驻回归 idle(0.35s——演出一拍即回,结算收尾另有显式 idle 兜底)。
|
||||
# 状态切换转调壳(签名外形零变——is_hero 由宿主 vitals 参判传入,早退序逐字在件内)
|
||||
func set_hero_state(state: String) -> void:
|
||||
var target: int = _HS_VALUE.get(state, -1)
|
||||
if st._kind != "hero" or target == st._hero_state:
|
||||
return
|
||||
if not st._hero_tex.has(state) and state != "idle":
|
||||
return
|
||||
st._hero_state = target
|
||||
if st._portrait_sprite != null and st._hero_tex.has(state):
|
||||
st._portrait_sprite.texture = st._hero_tex[state]
|
||||
if st._state_revert_tw != null and st._state_revert_tw.is_valid():
|
||||
st._state_revert_tw.kill()
|
||||
if state != "idle":
|
||||
st._state_revert_tw = create_tween()
|
||||
st._state_revert_tw.tween_interval(UiKit.motion(0.35))
|
||||
st._state_revert_tw.tween_callback(_settle_hero_idle)
|
||||
mo.apply_hero_state(state, st._kind == "hero")
|
||||
|
||||
|
||||
func _settle_hero_idle() -> void:
|
||||
st._hero_state = HeroState.IDLE
|
||||
if st._portrait_sprite != null and st._hero_tex.has("idle"):
|
||||
st._portrait_sprite.texture = st._hero_tex["idle"]
|
||||
|
||||
|
||||
# 三态探针出口(fx_probe——当前态名+注册态集合)
|
||||
# 三态探针出口(fx_probe——当前态名+注册态集合;读件投影外形零变)
|
||||
func hero_state_probe() -> Dictionary:
|
||||
return {"state": _HS_NAME.get(st._hero_state, "idle"), "registered": st._hero_tex.keys()}
|
||||
return mo.state_probe()
|
||||
|
||||
|
||||
func _portrait_color() -> Color:
|
||||
@@ -535,7 +421,7 @@ func _draw_hp_bar(f: Font) -> void:
|
||||
|
||||
## 立绘域:贴图在位 = Sprite2D(set_portrait_texture 落位);缺位 = 字形回退
|
||||
func _draw_portrait(f: Font) -> void:
|
||||
if st._portrait_sprite != null and st._portrait_sprite.texture != null and st._portrait_sprite.visible:
|
||||
if mo._portrait_sprite != null and mo._portrait_sprite.texture != null and mo._portrait_sprite.visible:
|
||||
return
|
||||
var prect := _portrait_rect()
|
||||
var glyph := _portrait_glyph()
|
||||
@@ -543,7 +429,7 @@ func _draw_portrait(f: Font) -> void:
|
||||
# 坐标系注记:draw_set_transform 原点已置于 center——draw_string 的
|
||||
# pos 是**变换后的局部坐标**,须传相对 center 的偏移(-w/2 水平居中、+0.35h 基线下移);
|
||||
# 传绝对 center 会成双重偏移、把字形画到盒外。
|
||||
draw_set_transform(center, deg_to_rad(st._glyph_rot), Vector2.ONE)
|
||||
draw_set_transform(center, deg_to_rad(mo._glyph_rot), Vector2.ONE)
|
||||
var ts := f.get_string_size(glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 42)
|
||||
f.draw_string(get_canvas_item(), Vector2(-ts.x * 0.5, ts.y * 0.35), glyph,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 42, _portrait_color())
|
||||
@@ -552,18 +438,17 @@ func _draw_portrait(f: Font) -> void:
|
||||
|
||||
## 意图徽标域(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画
|
||||
func _draw_intent_badge(f: Font) -> void:
|
||||
if not st._intent_visible:
|
||||
if not mo._intent_visible:
|
||||
return
|
||||
var icenter := Vector2(box.x * 0.5, 86.0)
|
||||
draw_set_transform(icenter, 0.0, Vector2(st._intent_scale, st._intent_scale))
|
||||
var its := f.get_string_size(st._intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15)
|
||||
draw_set_transform(icenter, 0.0, Vector2(mo._intent_scale, mo._intent_scale))
|
||||
var its := f.get_string_size(mo._intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15)
|
||||
# 对比度垫牌(四色墨直压立绘纹理/暗舞台底 1.5-3.3:1 违 §7.2
|
||||
# 「文字禁直压纹理底」;垫 PANEL_DATA 素牌后 atk 5.3/buf 4.8/debuff 4.7/charge 4.9 全 ≥4.5:1;
|
||||
# atk/debuff 零换墨、buf 换 HERO_BLUE_DEEP、charge 换 GOLD_INK;随弹入 transform 同步缩放)
|
||||
draw_rect(Rect2(-its.x * 0.5 - 4.0, -its.y * 0.75, its.x + 8.0, its.y * 1.1), UiKit.PANEL_DATA, true)
|
||||
f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), st._intent_glyph,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 15, st._intent_col) # 局部坐标(同上注记——双重偏移修复)
|
||||
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
|
||||
f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), mo._intent_glyph,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 15, mo._intent_col) # 局部坐标(同上注记——双重偏移修复)
|
||||
|
||||
|
||||
## 名字行域(敌 only——AC 数值不展示,类型图标化):整行垫 HP_BAR_BACK 素底牌(§6.1/§7.2
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 站桩资产与演出态件(StandeeBody 拆件——单一变化原因=美术素材落位规则与演出编排设计):
|
||||
## 站桩的资产挂点与一次性演出——立绘精灵落位与换装、主角三态注册与 0.35s 短驻回归、意图徽标弹入动画。
|
||||
## 无 class_name(类名缓存面纪律)——经宿主 standee_body preload 引用(mo 实例);
|
||||
## 两个属性 setter 经 _owner 回引触发宿主重绘语义逐字;tween 经 _owner 创建(同一 Node 生命绑定)。
|
||||
extends RefCounted
|
||||
|
||||
enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(外部 String 契约=register/set 形参零变化)
|
||||
const _HS_NAME := {HeroState.IDLE: "idle", HeroState.RESET: "reset", HeroState.ATTACK: "attack", HeroState.HIT: "hit"}
|
||||
const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": HeroState.ATTACK, "hit": HeroState.HIT}
|
||||
|
||||
var _owner: Node2D = null
|
||||
var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位)
|
||||
var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据)
|
||||
# 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理
|
||||
# (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。
|
||||
var _hero_tex := {}
|
||||
var _hero_state: int = 0
|
||||
var _state_revert_tw: Tween
|
||||
var _intent_glyph := ""
|
||||
var _intent_col := Color.WHITE
|
||||
var _intent_visible := false
|
||||
var _intent_scale := 1.0: # 意图弹入动画参数
|
||||
set(v):
|
||||
_intent_scale = v
|
||||
_owner.queue_redraw()
|
||||
var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画)
|
||||
set(v):
|
||||
_glyph_rot = v
|
||||
_owner.queue_redraw()
|
||||
|
||||
|
||||
func _init(owner: Node2D) -> void:
|
||||
_owner = owner
|
||||
|
||||
|
||||
# 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。
|
||||
# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。盒高 96px = 48px 立绘源整数 2×
|
||||
# (圣经 §8.2 禁非整数缩放)——源换代时随源重算。(box = 宿主冻结 var,由宿主传入)
|
||||
func set_portrait_texture(tex: Texture2D, flip: bool, box: Vector2) -> void:
|
||||
if tex == null:
|
||||
if _portrait_sprite != null:
|
||||
_portrait_sprite.visible = false
|
||||
_owner.queue_redraw()
|
||||
return
|
||||
_portrait_tex = tex
|
||||
if _portrait_sprite == null:
|
||||
_portrait_sprite = Sprite2D.new()
|
||||
_portrait_sprite.centered = false
|
||||
_portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊
|
||||
_owner.add_child(_portrait_sprite)
|
||||
_portrait_sprite.texture = tex
|
||||
_portrait_sprite.flip_h = flip
|
||||
_portrait_sprite.visible = true
|
||||
_place_portrait(box)
|
||||
|
||||
|
||||
func _place_portrait(box: Vector2) -> void:
|
||||
# 立绘盒 = (0,44)-(box.x, box.y-36);48 源 ×2 整数档居中
|
||||
if _portrait_sprite == null or _portrait_tex == null:
|
||||
return
|
||||
var box_h := box.y - 44.0 - 36.0
|
||||
var w := _portrait_tex.get_width() * 2.0
|
||||
var h := _portrait_tex.get_height() * 2.0
|
||||
_portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5)
|
||||
_portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5)
|
||||
|
||||
|
||||
# 组合点注册(battle_stage_view _init——UiKit 常量 load_tex,缺位 null 不注册=该态保持现纹理)
|
||||
func register_hero_state(state: String, tex: Texture2D) -> void:
|
||||
if tex != null:
|
||||
_hero_tex[state] = tex
|
||||
|
||||
|
||||
# 状态切换:现纹理换注册态(attack/hit);未注册态名或缺位 = 保持现纹理不炸。
|
||||
# 非 idle 态自动短驻回归 idle(0.35s——演出一拍即回,结算收尾另有显式 idle 兜底)。
|
||||
#(is_hero = 宿主 vitals._kind == "hero" 参判传入——三连早退条件序逐字)
|
||||
func apply_hero_state(state: String, is_hero: bool) -> void:
|
||||
var target: int = _HS_VALUE.get(state, -1)
|
||||
if not is_hero or target == _hero_state:
|
||||
return
|
||||
if not _hero_tex.has(state) and state != "idle":
|
||||
return
|
||||
_hero_state = target
|
||||
if _portrait_sprite != null and _hero_tex.has(state):
|
||||
_portrait_sprite.texture = _hero_tex[state]
|
||||
if _state_revert_tw != null and _state_revert_tw.is_valid():
|
||||
_state_revert_tw.kill()
|
||||
if state != "idle":
|
||||
_state_revert_tw = _owner.create_tween()
|
||||
_state_revert_tw.tween_interval(UiKit.motion(0.35))
|
||||
_state_revert_tw.tween_callback(_settle_hero_idle)
|
||||
|
||||
|
||||
func _settle_hero_idle() -> void:
|
||||
_hero_state = HeroState.IDLE
|
||||
if _portrait_sprite != null and _hero_tex.has("idle"):
|
||||
_portrait_sprite.texture = _hero_tex["idle"]
|
||||
|
||||
|
||||
# 复位强制绕同态早退两拍封装(原 _revive 内 hero 分支——先置 RESET 哨兵再回 idle)
|
||||
func reset_state() -> void:
|
||||
_hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退)
|
||||
apply_hero_state("idle", true)
|
||||
|
||||
|
||||
# 意图三档动作动画(§4.6.4 三档:①普通攻击=零表示〔set_pending 早退〕
|
||||
# ②特殊准备=徽标弹入+二次弹跳(高举语义,运动手段 §2)③重击/硬化=缓推蓄力+定格微顿+回落;
|
||||
# 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。
|
||||
#(charge = 宿主 _intent_charge 分档布尔参判传入)
|
||||
func play_intent_motion(charge: bool) -> void:
|
||||
var tw := _owner.create_tween()
|
||||
tw.tween_property(self, "_intent_scale", 1.6, UiKit.motion(0.10))
|
||||
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.16))
|
||||
tw.parallel().tween_property(self, "_glyph_rot", 4.0, UiKit.motion(0.10))
|
||||
tw.tween_property(self, "_glyph_rot", 0.0, UiKit.motion(0.14))
|
||||
if charge:
|
||||
tw.tween_property(self, "_intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推
|
||||
tw.tween_interval(UiKit.motion(0.40)) # 定格微顿
|
||||
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.18))
|
||||
else:
|
||||
tw.tween_property(self, "_intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举)
|
||||
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.13))
|
||||
|
||||
|
||||
# 三态探针出口(fx_probe——当前态名+注册态集合;宿主 hero_state_probe 读件投影,外形零变)
|
||||
func state_probe() -> Dictionary:
|
||||
return {"state": _HS_NAME.get(_hero_state, "idle"), "registered": _hero_tex.keys()}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdfo2jgp2w7ei
|
||||
@@ -0,0 +1,36 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 站桩身份与命值态件(StandeeBody 拆件——单一变化原因=战斗结算快照投影的字段形态):
|
||||
## 站桩是谁、活没活、血量几何的唯一账面——身份五元组(种类/名/id/护甲/顺位)+存活位+血量账与两条血量系 tween 槽。
|
||||
## 无 class_name(类名缓存面纪律)——经宿主 standee_body preload 引用(st 实例名沿用);
|
||||
## hp_value 属性 setter 经 _owner 回引触发宿主重绘语义逐字。
|
||||
extends RefCounted
|
||||
|
||||
var _owner: Node2D = null
|
||||
var _kind := "enemy"
|
||||
var _display_name := ""
|
||||
var _enemy_id := ""
|
||||
var _armor := "light"
|
||||
var _order := 1
|
||||
var _dead := false
|
||||
var _hp_max := 1.0
|
||||
var _hp_cur := 0 # 血量数字显示面(HUD框架 验收「血量数字可读」)
|
||||
var hp_value := 1.0: # 血条值 0..1(直角递减——tween_property 目标,逐行刷新重启不累积)
|
||||
set(v):
|
||||
hp_value = clampf(v, 0.0, 1.0)
|
||||
_owner.queue_redraw()
|
||||
var _hp_tween: Tween
|
||||
var _hp_flash_tw: Tween
|
||||
|
||||
|
||||
func _init(owner: Node2D) -> void:
|
||||
_owner = owner
|
||||
|
||||
|
||||
# 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积
|
||||
func apply_hp(hp: int) -> void:
|
||||
_hp_cur = clampi(hp, 0, int(_hp_max))
|
||||
var target := float(_hp_cur) / _hp_max
|
||||
if _hp_tween != null and _hp_tween.is_valid():
|
||||
_hp_tween.kill()
|
||||
_hp_tween = _owner.create_tween()
|
||||
_hp_tween.tween_property(self, "hp_value", target, UiKit.motion(0.15))
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjd7t5nmepbey
|
||||
@@ -37,14 +37,14 @@ func _ready() -> void:
|
||||
# 等关卡画面出图(choices_presented 异步到达——最多 3s 轮询)
|
||||
for i in 60:
|
||||
await get_tree().process_frame
|
||||
if main.level_screen != null and main.level_screen.visible:
|
||||
if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible:
|
||||
break
|
||||
if main.level_screen == null or not main.level_screen.visible:
|
||||
if main.ui.modals.level_screen == null or not main.ui.modals.level_screen.visible:
|
||||
_fail("关卡画面未出现(函数直调中间态路径)")
|
||||
return
|
||||
for i in 5:
|
||||
await get_tree().process_frame
|
||||
main.level_screen.press_candidate(0)
|
||||
main.ui.modals.level_screen.press_candidate(0)
|
||||
for i in 20:
|
||||
await get_tree().process_frame
|
||||
var st = main.run_scope.settlement
|
||||
|
||||
@@ -23,12 +23,12 @@ func _ready() -> void:
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
ProbeUiUtil.isolate_save(main, "smoke_layout_probe", true, false, true)
|
||||
main.debug_overlay.seed_edit.text = "42" # 种子钉死(确定性禁依赖默认随机分支)
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(确定性禁依赖默认随机分支)
|
||||
main._start_adventure_from_menu()
|
||||
for i in range(8):
|
||||
await get_tree().process_frame
|
||||
# 进战斗屏——三区(B 战斗画面/C 角色信息/D 扫雷图)同屏常驻,菜单屏不构成断言面
|
||||
main.level_screen.press_candidate(0)
|
||||
main.ui.modals.level_screen.press_candidate(0)
|
||||
for i in range(20):
|
||||
await get_tree().process_frame
|
||||
for vp in VIEWPORTS:
|
||||
@@ -57,8 +57,9 @@ func _check_one(label: String, main: Node) -> void:
|
||||
if vr.size != Vector2(VIEWPORTS[0]["size"]) and vr.size.y < 1.0:
|
||||
_problems.append("[%s] 逻辑视口退化 %s" % [label, vr.size])
|
||||
var rects := {}
|
||||
var battle_ui: Object = main.get("ui").battle # 注册面战区域容器(字段名与三区 key 同名——合同键即字段名)
|
||||
for key in ["stage", "info_panel", "board_zone"]:
|
||||
var ctl: Control = main.get(key)
|
||||
var ctl: Control = battle_ui.get(key)
|
||||
if ctl == null:
|
||||
_problems.append("[%s] %s = null(三区未装配)" % [label, key])
|
||||
return
|
||||
@@ -73,7 +74,7 @@ func _check_one(label: String, main: Node) -> void:
|
||||
# battle_root 内层根下的全幅 ColorRect)这一缺陷类不在本探针拦截面内,探针对此仍全绿。
|
||||
# 要拦它得改为递归遍历全子树找重叠,那是另一套判据(且极易误报容器父子关系),
|
||||
# 本探针不设此判据。
|
||||
var bv: Control = main.board_view
|
||||
var bv: Control = main.ui.battle.board_zone.board_view
|
||||
if bv == null:
|
||||
_problems.append("[%s] board_view = null" % label)
|
||||
return
|
||||
@@ -89,6 +90,6 @@ func _check_one(label: String, main: Node) -> void:
|
||||
if rects[keys[i]].intersects(rects[keys[j]]):
|
||||
_problems.append("[%s] 顶层区重叠 %s×%s(%s ∩ %s)"
|
||||
% [label, keys[i], keys[j], rects[keys[i]], rects[keys[j]]])
|
||||
var cell: float = main.board_view.get("cell_size")
|
||||
var cell: float = main.ui.battle.board_zone.board_view.get("cell_size")
|
||||
if cell < MIN_CELL:
|
||||
_problems.append("[%s] 格子边长 %.1fpx < 可读门槛 %.0fpx" % [label, cell, MIN_CELL])
|
||||
@@ -130,7 +130,7 @@ static func open_camp_via_savesel(main, tag: String) -> bool:
|
||||
if FileAccess.file_exists(sp):
|
||||
DirAccess.remove_absolute(sp)
|
||||
main._open_save_select()
|
||||
var new_btn: Button = main.save_select_overlay._new_btns.get(1)
|
||||
var new_btn: Button = main.ui.modals.save_select_overlay._new_btns.get(1)
|
||||
if new_btn == null:
|
||||
return false
|
||||
new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp
|
||||
|
||||
@@ -50,10 +50,10 @@ func _ready() -> void:
|
||||
# 其余任意两屏同时为真=互斥失守。
|
||||
func _screen_seals() -> Dictionary:
|
||||
return {
|
||||
"菜单": main._start_overlay != null and main._start_overlay.visible,
|
||||
"营地": main.camp_overlay != null and main.camp_overlay.visible,
|
||||
"菜单": main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible,
|
||||
"营地": main.ui.modals.camp_overlay != null and main.ui.modals.camp_overlay.visible,
|
||||
"冒险": main.run_scope != null and main.run_scope.is_adventure,
|
||||
"终局": main._end_overlay != null and main._end_overlay.visible,
|
||||
"终局": main.ui.modals.end_overlay != null and main.ui.modals.end_overlay.visible,
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ func _frames(n: int = 2) -> void:
|
||||
func _step_boot_menu() -> void:
|
||||
_expect_exclusive("①boot", "菜单")
|
||||
_expect_adventure_seal("①boot", false)
|
||||
if main.debug_overlay != null and main.debug_overlay.visible:
|
||||
if main.ui.modals.debug_overlay != null and main.ui.modals.debug_overlay.visible:
|
||||
_problems.append("①boot:调试浮窗 boot 自动开(菜单优先失守)")
|
||||
|
||||
|
||||
@@ -117,10 +117,10 @@ func _step_menu_to_camp() -> void:
|
||||
|
||||
# ---- ③ 营地 → 菜单(回退路径:营地关闭发自覆盖层自身信号)----
|
||||
func _step_camp_back_to_menu() -> void:
|
||||
if main.camp_overlay == null:
|
||||
if main.ui.modals.camp_overlay == null:
|
||||
_problems.append("③营地→菜单:营地覆盖层句柄缺失")
|
||||
return
|
||||
main.camp_overlay.closed.emit()
|
||||
main.ui.modals.camp_overlay.closed.emit()
|
||||
await _frames()
|
||||
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
|
||||
await _frames()
|
||||
@@ -133,22 +133,22 @@ func _step_camp_back_to_menu() -> void:
|
||||
func _step_save_select_open_back() -> void:
|
||||
ProbeUiUtil.isolate_save(main, "smoke_screen_flow_savesel", false, false, true)
|
||||
main.save_manager.slots.attach_slot_template("user://smoke_screen_flow_savesel_slot_%d.json")
|
||||
var esb: Button = ProbeUiUtil.find_button(main._start_overlay, "进入存档", "prefix")
|
||||
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
|
||||
if esb == null:
|
||||
_problems.append("[savesel] mn3 进入存档钮缺失(卡② 交付物)")
|
||||
return
|
||||
esb.pressed.emit() # 真实按钮信号路径 → _open_save_select(卡② mn3 点击链——卡③ 直调中间态就此闭环)
|
||||
await get_tree().process_frame
|
||||
var seals := _screen_seals()
|
||||
if not main.save_select_overlay.visible or not seals_check_single(seals):
|
||||
_problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.save_select_overlay.visible))
|
||||
var back_btn: Button = ProbeUiUtil.find_button(main.save_select_overlay, "返回", "prefix")
|
||||
if not main.ui.modals.save_select_overlay.visible or not seals_check_single(seals):
|
||||
_problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.ui.modals.save_select_overlay.visible))
|
||||
var back_btn: Button = ProbeUiUtil.find_button(main.ui.modals.save_select_overlay, "返回", "prefix")
|
||||
if back_btn == null:
|
||||
_problems.append("[savesel] sl10 返回钮缺失(页壳顶条)")
|
||||
else:
|
||||
back_btn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
if main.save_select_overlay.visible or not main._start_overlay.visible:
|
||||
if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("[savesel] sl10 返回后选档未隐藏或开始层未恢复(关闭链失守)")
|
||||
# ESC 语境:再开选档→合成 ESC 键→层不关、流程零扰动(ESC 不属选档语境——零实现断言)
|
||||
main._open_save_select()
|
||||
@@ -159,7 +159,7 @@ func _step_save_select_open_back() -> void:
|
||||
ev.pressed = true
|
||||
main.get_viewport().push_input(ev, true)
|
||||
await get_tree().process_frame
|
||||
if not main.save_select_overlay.visible:
|
||||
if not main.ui.modals.save_select_overlay.visible:
|
||||
_problems.append("[savesel] sl10 ESC 关闭了选档层(选档语境 ESC 应不响应——卡⑤ 范围外)")
|
||||
main._on_save_select_closed()
|
||||
await get_tree().process_frame
|
||||
@@ -171,7 +171,7 @@ func seals_check_single(f: Dictionary) -> bool:
|
||||
for k in f:
|
||||
if bool(f[k]):
|
||||
on += 1
|
||||
if main.save_select_overlay.visible:
|
||||
if main.ui.modals.save_select_overlay.visible:
|
||||
on += 1 # 选档自身计入(调用点已开层)
|
||||
return on == 1
|
||||
|
||||
@@ -184,25 +184,25 @@ func _step_menu_to_adventure() -> void:
|
||||
_expect_adventure_seal("④菜单→冒险", true)
|
||||
if main.run_scope != null and main.run_scope.adventure == null:
|
||||
_problems.append("④菜单→冒险:冒险实例未建立(入口停点未达)")
|
||||
if main.level_screen != null and not main.level_screen.visible:
|
||||
if main.ui.modals.level_screen != null and not main.ui.modals.level_screen.visible:
|
||||
_problems.append("④菜单→冒险:入口停点未落在关卡地图屏(level_screen 不可见)")
|
||||
|
||||
|
||||
# ---- ⑤ 冒险内屏切换:关卡地图屏 ⇄ 战斗屏(屏内两态,屏级仍独占)----
|
||||
func _step_level_to_battle_and_back() -> void:
|
||||
if not main.level_screen.visible:
|
||||
if not main.ui.modals.level_screen.visible:
|
||||
_problems.append("⑤冒险内切换:前置不在关卡地图屏")
|
||||
return
|
||||
main._exit_level_screen()
|
||||
await _frames()
|
||||
if main.level_screen.visible:
|
||||
if main.ui.modals.level_screen.visible:
|
||||
_problems.append("⑤冒险内切换:退出关卡屏后 level_screen 仍可见")
|
||||
if main._battle_root != null and not main._battle_root.visible:
|
||||
if main.ui.screens.battle_root != null and not main.ui.screens.battle_root.visible:
|
||||
_problems.append("⑤冒险内切换:战斗屏未接手(battle_root 不可见)")
|
||||
_expect_exclusive("⑤冒险内切换(关卡→战斗)", "冒险")
|
||||
main._enter_level_screen()
|
||||
await _frames()
|
||||
if not main.level_screen.visible:
|
||||
if not main.ui.modals.level_screen.visible:
|
||||
_problems.append("⑤冒险内切换:回到关卡地图屏失败")
|
||||
_expect_exclusive("⑤冒险内切换(战斗→关卡)", "冒险")
|
||||
|
||||
@@ -215,16 +215,16 @@ func _step_abandon_to_end() -> void:
|
||||
main._on_abandon_requested()
|
||||
await _frames(4)
|
||||
_expect_exclusive("⑥放弃→终局", "终局")
|
||||
if main._end_overlay != null and not main._end_overlay.visible:
|
||||
if main.ui.modals.end_overlay != null and not main.ui.modals.end_overlay.visible:
|
||||
_problems.append("⑥放弃→终局:终局覆盖层未开")
|
||||
|
||||
|
||||
# ---- ⑦ 终局 → 菜单(终局出口返回开始界面)----
|
||||
func _step_end_back_to_menu() -> void:
|
||||
if main._end_overlay == null or not main._end_overlay.visible:
|
||||
if main.ui.modals.end_overlay == null or not main.ui.modals.end_overlay.visible:
|
||||
_problems.append("⑦终局→菜单:前置不在终局")
|
||||
return
|
||||
main._end_overlay.continue_pressed.emit()
|
||||
main.ui.modals.end_overlay.continue_pressed.emit()
|
||||
await _frames(3)
|
||||
_expect_exclusive("⑦终局→菜单", "菜单")
|
||||
if main.run_scope != null and main.run_scope.is_adventure:
|
||||
|
||||
+29
-29
@@ -51,10 +51,10 @@ func _ready() -> void:
|
||||
var top: Node = null
|
||||
var top_rx := -1
|
||||
var top_ry := -1
|
||||
for c: Node in [main._start_overlay, main._battle_root, main._inter_overlay,
|
||||
main.legend_overlay, main.node_overlay, main.level_screen, main.curio_overlay,
|
||||
main._end_overlay, main.shop_overlay, main.camp_overlay, main.settings_overlay,
|
||||
main.status_panel, main.career_overlay, main.confirm_dialog, main.debug_overlay]:
|
||||
for c: Node in [main.ui.screens.start_overlay, main.ui.screens.battle_root, main.ui.screens.inter_overlay,
|
||||
main.ui.modals.legend_overlay, main.ui.modals.node_overlay, main.ui.modals.level_screen, main.ui.modals.curio_overlay,
|
||||
main.ui.modals.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp_overlay, main.ui.modals.settings_overlay,
|
||||
main.ui.modals.status_panel, main.ui.modals.career_overlay, main.ui.modals.confirm_dialog, main.ui.modals.debug_overlay]:
|
||||
if c == null or not (c is CanvasItem) or not (c as CanvasItem).visible:
|
||||
continue
|
||||
var rx: int = c.get_parent().get_index()
|
||||
@@ -63,14 +63,14 @@ func _ready() -> void:
|
||||
top = c
|
||||
top_rx = rx
|
||||
top_ry = ry
|
||||
if top != main._start_overlay:
|
||||
if top != main.ui.screens.start_overlay:
|
||||
_problems.append("boot 态顶层可见层≠start_overlay(实测 %s——菜单被埋)〔装配生命批 V4〕"
|
||||
% (top.name if top != null else "null"))
|
||||
# 自检二:菜单标题 label 必须 is_visible_in_tree 且 rect 落屏内
|
||||
#(对比度只判反差不判「字在」——本条+checker 墨色占比共同补内容判据)
|
||||
var vp := get_viewport().get_visible_rect()
|
||||
for pair in [["menu_title", "Dungeon Sweeper"]]:
|
||||
var lbl: Label = ProbeUiUtil.find_label(main._start_overlay, String(pair[1]))
|
||||
var lbl: Label = ProbeUiUtil.find_label(main.ui.screens.start_overlay, String(pair[1]))
|
||||
if lbl == null:
|
||||
_problems.append("菜单文字缺失: %s(find_label 零命中)〔装配生命批 V4〕" % String(pair[1]))
|
||||
continue
|
||||
@@ -79,22 +79,22 @@ func _ready() -> void:
|
||||
var gr: Rect2 = lbl.get_global_rect()
|
||||
if not vp.encloses(gr):
|
||||
_problems.append("菜单文字 rect 越出视口: %s(%s ⊄ %s)〔装配生命批 V4〕" % [String(pair[1]), str(gr), str(vp)])
|
||||
_meta[String(pair[0])] = ProbeUiUtil.label_rect(main._start_overlay, String(pair[1]))
|
||||
_meta[String(pair[0])] = ProbeUiUtil.label_rect(main.ui.screens.start_overlay, String(pair[1]))
|
||||
await _snap("menu")
|
||||
# ①b 设置面(settingsbtn 真实信号路径,开合同款真实钮)
|
||||
var setbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "设置", "prefix")
|
||||
var setbtn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix")
|
||||
if setbtn == null:
|
||||
_problems.append("设置入口缺失(开始层 settingsbtn)")
|
||||
else:
|
||||
setbtn.pressed.emit()
|
||||
for i in 3:
|
||||
await get_tree().process_frame
|
||||
if not main.settings_overlay.visible:
|
||||
if not main.ui.modals.settings_overlay.visible:
|
||||
_problems.append("设置面未打开(settingsbtn 路径)")
|
||||
else:
|
||||
_meta["settings_title"] = ProbeUiUtil.label_rect(main.settings_overlay, "设置")
|
||||
_meta["settings_title"] = ProbeUiUtil.label_rect(main.ui.modals.settings_overlay, "设置")
|
||||
await _snap("settings")
|
||||
var set_close: Button = ProbeUiUtil.find_button(main.settings_overlay, "返回", "prefix")
|
||||
var set_close: Button = ProbeUiUtil.find_button(main.ui.modals.settings_overlay, "返回", "prefix")
|
||||
if set_close == null:
|
||||
_problems.append("设置面返回钮缺失")
|
||||
else:
|
||||
@@ -105,19 +105,19 @@ func _ready() -> void:
|
||||
main._open_career()
|
||||
for i in 3:
|
||||
await get_tree().process_frame
|
||||
if not main.career_overlay.visible or main._start_overlay.visible:
|
||||
if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible:
|
||||
_problems.append("生涯页未打开或开始层未隐藏(函数直调中间态路径)")
|
||||
else:
|
||||
_meta["career_title"] = ProbeUiUtil.label_rect(main.career_overlay, "生 涯")
|
||||
_meta["career_title"] = ProbeUiUtil.label_rect(main.ui.modals.career_overlay, "生 涯")
|
||||
await _snap("career")
|
||||
var career_close: Button = ProbeUiUtil.find_button(main.career_overlay, "返回", "prefix")
|
||||
var career_close: Button = ProbeUiUtil.find_button(main.ui.modals.career_overlay, "返回", "prefix")
|
||||
if career_close == null:
|
||||
_problems.append("生涯页返回钮缺失")
|
||||
else:
|
||||
career_close.pressed.emit()
|
||||
for i in 3:
|
||||
await get_tree().process_frame
|
||||
if main.career_overlay.visible or not main._start_overlay.visible:
|
||||
if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("生涯页关闭后页未隐藏或开始层未恢复")
|
||||
# ② 营地(进档开层新路径——卡② mn9;槽模板注入隔离命名空间)
|
||||
if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"):
|
||||
@@ -125,7 +125,7 @@ func _ready() -> void:
|
||||
else:
|
||||
await get_tree().process_frame
|
||||
await _snap("camp")
|
||||
var close_btn: Button = ProbeUiUtil.find_button(main.camp_overlay, "关闭营地", "contains")
|
||||
var close_btn: Button = ProbeUiUtil.find_button(main.ui.modals.camp_overlay, "关闭营地", "contains")
|
||||
if close_btn != null:
|
||||
close_btn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
@@ -137,24 +137,24 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
await _snap("level")
|
||||
# GP 常驻徽章断言(读数=runner.gp 现值,刷新=写路径回执后重查〔open_choices〕)
|
||||
var gp_label: Label = ProbeUiUtil.find_label(main.level_screen, "◈ ")
|
||||
var gp_label: Label = ProbeUiUtil.find_label(main.ui.modals.level_screen, "◈ ")
|
||||
if gp_label == null or not gp_label.is_visible_in_tree():
|
||||
_problems.append("GP 徽章不可见(关卡画面顶栏)")
|
||||
elif main.level_screen.get("_runner") != null:
|
||||
var expect := "◈ %d" % int(main.level_screen.get("_runner").gp)
|
||||
elif main.ui.modals.level_screen.get("_runner") != null:
|
||||
var expect := "◈ %d" % int(main.ui.modals.level_screen.get("_runner").gp)
|
||||
if gp_label.text != expect:
|
||||
_problems.append("GP 徽章读数不符: %s != %s" % [gp_label.text, expect])
|
||||
# ④ 战斗(节点一——真实选路提交路径 press_candidate,ui_click_probe :199 同款)
|
||||
if main.level_screen != null and main.level_screen.visible:
|
||||
main.level_screen.press_candidate(0)
|
||||
if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible:
|
||||
main.ui.modals.level_screen.press_candidate(0)
|
||||
for i in 10:
|
||||
await get_tree().process_frame
|
||||
await _snap("battle")
|
||||
# 战斗屏文字框/明度带锚点(全局 rect——checker 取样坐标源;明细带锚 S1 裁后改认现行文案)
|
||||
_meta["battle_log_hint"] = ProbeUiUtil.label_rect(main.stage, "上轮摘要")
|
||||
_meta["battle_log_hint"] = ProbeUiUtil.label_rect(main.ui.battle.stage, "上轮摘要")
|
||||
_meta["hero_panel"] = _node_rect(ProbeUiUtil.find_by_script(main, "hero_info_panel_view"))
|
||||
_meta["stage_view"] = _node_rect(main.stage)
|
||||
_meta["board_zone"] = _node_rect(main.board_zone)
|
||||
_meta["stage_view"] = _node_rect(main.ui.battle.stage)
|
||||
_meta["board_zone"] = _node_rect(main.ui.battle.board_zone)
|
||||
# draw calls 真机测量(基线 270→目标 <200;窗口化渲染帧读数,
|
||||
# 取值时机=战斗截图同帧后一帧——呈现终态满负荷)
|
||||
await get_tree().process_frame
|
||||
@@ -162,25 +162,25 @@ func _ready() -> void:
|
||||
_meta["battle_draw_calls"] = dc
|
||||
print("[shot_probe] battle draw_calls=%d(基线 270/目标 <200)" % dc)
|
||||
# ⑤ 状态面板(战斗屏状态角标真实信号路径;层在战斗屏之上开合)
|
||||
var pb: Button = main.board_zone.status_badge
|
||||
var pb: Button = main.ui.battle.board_zone.status_badge
|
||||
if pb == null or not pb.visible or pb.disabled:
|
||||
_problems.append("状态面板入口角标缺失/置灰(战斗屏 board_zone)")
|
||||
else:
|
||||
pb.pressed.emit()
|
||||
for i in 3:
|
||||
await get_tree().process_frame
|
||||
if not main.status_panel.visible:
|
||||
if not main.ui.modals.status_panel.visible:
|
||||
_problems.append("状态面板未打开(角标路径)")
|
||||
else:
|
||||
_meta["status_title"] = ProbeUiUtil.label_rect(main.status_panel, "状态")
|
||||
_meta["status_title"] = ProbeUiUtil.label_rect(main.ui.modals.status_panel, "状态")
|
||||
await _snap("status")
|
||||
var status_close: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
|
||||
var status_close: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix")
|
||||
if status_close == null:
|
||||
_problems.append("状态面板继续钮缺失")
|
||||
else:
|
||||
status_close.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
if main.status_panel.visible:
|
||||
if main.ui.modals.status_panel.visible:
|
||||
_problems.append("状态面板关闭后未隐藏(继续钮路径)")
|
||||
else:
|
||||
_problems.append("关卡画面未出现(函数直调中间态路径)")
|
||||
|
||||
@@ -29,14 +29,14 @@ func _ready() -> void:
|
||||
main._start_adventure_from_menu() # 函数直调中间态(卡② mn10——开始冒险钮已撤)
|
||||
for i in 60:
|
||||
await get_tree().process_frame
|
||||
if main.level_screen != null and main.level_screen.visible:
|
||||
if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible:
|
||||
break
|
||||
if main.level_screen == null or not main.level_screen.visible:
|
||||
if main.ui.modals.level_screen == null or not main.ui.modals.level_screen.visible:
|
||||
_fail("关卡画面未出现")
|
||||
return
|
||||
for i in 5:
|
||||
await get_tree().process_frame
|
||||
main.level_screen.press_candidate(0)
|
||||
main.ui.modals.level_screen.press_candidate(0)
|
||||
for i in 20:
|
||||
await get_tree().process_frame
|
||||
if main.run_scope.settlement == null:
|
||||
@@ -67,18 +67,18 @@ func _ready() -> void:
|
||||
_fail("战斗未胜利(result='%s')" % str(main.run_scope.settlement.battle_result))
|
||||
return
|
||||
var t_settle := Time.get_ticks_msec()
|
||||
var _rows_at_victory: int = main._row_pacer.pending_rows().size() # 时间戳①:结算入队完成帧(AP 耗尽/停手→五行段同步跑完→行全在队)
|
||||
var _rows_at_victory: int = main.ui.screens.row_pacer.pending_rows().size() # 时间戳①:结算入队完成帧(AP 耗尽/停手→五行段同步跑完→行全在队)
|
||||
# 断言①胜利同帧:终局面未开 + 挂起在案 + 队列非空(判别性——旧行为同帧开窗即红)
|
||||
if main._end_overlay.is_open():
|
||||
if main.ui.modals.end_overlay.is_open():
|
||||
_problems.append("胜利同帧终局面已开(演播后排窗失效——同帧 flush 旧路径回潮)")
|
||||
if main._deferred_modal == null:
|
||||
if main.ui.modals.flow._deferred == null:
|
||||
_problems.append("胜利同帧挂起态缺失(_deferred_modal == null)")
|
||||
if main._row_pacer.pending_rows().is_empty():
|
||||
if main.ui.screens.row_pacer.pending_rows().is_empty():
|
||||
_problems.append("胜利同帧排播队列已空(结算行未走排播链)")
|
||||
var _played_rows: int = _rows_at_victory
|
||||
# 断言②排播放完:终局面开启(20s 守卫——0.22s×行数 ×10 余量)
|
||||
var waited := 0.0
|
||||
while not main._end_overlay.is_open() and waited < 20.0:
|
||||
while not main.ui.modals.end_overlay.is_open() and waited < 20.0:
|
||||
await get_tree().process_frame
|
||||
waited += get_process_delta_time()
|
||||
var t_popup := Time.get_ticks_msec()
|
||||
@@ -86,7 +86,7 @@ func _ready() -> void:
|
||||
print("[t17_defer] AP点用完: %s" % ap_clock)
|
||||
print("[t17_defer] 触发结算: %s(与 AP 用完同帧——核心同步,相差 0ms)" % settle_clock)
|
||||
print("[t17_defer] 弹窗出现: %s(距触发结算 %d ms = 逐行演播时长,行数 %d)" % [popup_clock, t_popup - t_ap_out, _played_rows])
|
||||
if not main._end_overlay.is_open():
|
||||
if not main.ui.modals.end_overlay.is_open():
|
||||
_problems.append("排播放完后终局面未开(%.1fs——演播完边沿派发失守)" % waited)
|
||||
if _problems.is_empty():
|
||||
print("[t17_defer] PASS — 胜利同帧挂起+排播完开窗 两断言全过(waited=%.1fs)" % waited)
|
||||
|
||||
+171
-171
@@ -51,7 +51,7 @@ func _ready() -> void:
|
||||
var t_ui_ready := Time.get_ticks_msec() - t0
|
||||
# 调试浮窗 boot 不开(菜单优先——入口=UI 最顶层全局浮层角标,
|
||||
# 任何 UI 不遮挡;浮层角标遮挡判据见设置面段)
|
||||
if main.debug_overlay.visible:
|
||||
if main.ui.modals.debug_overlay.visible:
|
||||
_problems.append("调试浮窗 boot 自动开(菜单优先失守)")
|
||||
# ---- 营地购买合成点击路径(选档进档开层→购买→关闭全走真实按钮信号路径;
|
||||
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json);陈旧档段首清理)----
|
||||
@@ -65,17 +65,17 @@ func _ready() -> void:
|
||||
_problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)")
|
||||
else:
|
||||
await get_tree().process_frame
|
||||
if not main.camp_overlay.visible:
|
||||
if not main.ui.modals.camp_overlay.visible:
|
||||
_problems.append("营地层未打开(进档开层新路径)")
|
||||
elif main.camp_overlay._tree_buy_btns.is_empty():
|
||||
elif main.ui.modals.camp_overlay._tree_buy_btns.is_empty():
|
||||
_problems.append("树淬炼柜台购买钮缺失(点击路径——四柜台重排后树=主界面)")
|
||||
else:
|
||||
# 树钮句柄用后即取(refresh_camp 随刷重建树行板——旧钮 queue_free,跨刷新句柄必失效)
|
||||
iso_c.meta_state._add_keystone(9999)
|
||||
main.camp_overlay.refresh_camp(main._camp_data())
|
||||
main.ui.modals.camp_overlay.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
var buy_btn: Button = null
|
||||
for card in main.camp_overlay._tree_buy_btns:
|
||||
for card in main.ui.modals.camp_overlay._tree_buy_btns:
|
||||
if String(card.node_id) == "sp_hp":
|
||||
buy_btn = card.btn
|
||||
if buy_btn == null:
|
||||
@@ -93,16 +93,16 @@ func _ready() -> void:
|
||||
_problems.append("营地树购买未持久(reload 后级 != 1)")
|
||||
# Fork 双态切换点击路径(V20——购=解锁默认 A,钮切 B;前置=同调位 1,先点亮)
|
||||
var socket_btn: Button = null
|
||||
for card in main.camp_overlay._tree_buy_btns:
|
||||
for card in main.ui.modals.camp_overlay._tree_buy_btns:
|
||||
if String(card.node_id) == "att_socket1":
|
||||
socket_btn = card.btn
|
||||
if socket_btn != null:
|
||||
socket_btn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
main.camp_overlay.refresh_camp(main._camp_data())
|
||||
main.ui.modals.camp_overlay.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
var fork_btn: Button = null
|
||||
for card in main.camp_overlay._tree_buy_btns:
|
||||
for card in main.ui.modals.camp_overlay._tree_buy_btns:
|
||||
if String(card.node_id) == "fork_posture":
|
||||
fork_btn = card.btn
|
||||
if fork_btn == null:
|
||||
@@ -110,10 +110,10 @@ func _ready() -> void:
|
||||
else:
|
||||
fork_btn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
main.camp_overlay.refresh_camp(main._camp_data())
|
||||
main.ui.modals.camp_overlay.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
var side_b: Button = null
|
||||
for fk in main.camp_overlay._tree_fork_btns:
|
||||
for fk in main.ui.modals.camp_overlay._tree_fork_btns:
|
||||
if String(fk.group) == "fg_posture" and String(fk.side) == "b":
|
||||
side_b = fk.btn
|
||||
if side_b == null:
|
||||
@@ -128,7 +128,7 @@ func _ready() -> void:
|
||||
# ---- 营地六处灰字取色断言(迭代36 wc4——真实入口开营地后按文本锚取 label,
|
||||
# font_color 逐处 == STONE_DEEP;六源位 :167 图鉴锁文/:417+:476 行状态字/:509 同调形态/:513 档位标签/:525 刷新行)----
|
||||
var camp_labels: Array = []
|
||||
_collect_labels_by_text(main.camp_overlay, camp_labels)
|
||||
_collect_labels_by_text(main.ui.modals.camp_overlay, camp_labels)
|
||||
var atlas_locks := _labels_with_text(camp_labels, "柜台结构位预留")
|
||||
if atlas_locks.is_empty():
|
||||
_problems.append("营地灰字:图鉴柜台锁文 label 未找到(wc4 定位锚失守)")
|
||||
@@ -137,9 +137,9 @@ func _ready() -> void:
|
||||
_problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)")
|
||||
# 「未解锁」锚收窄到秘宝匣行容器子树(树行/锻造行也有「未解锁」态字样——非 wc4 目标位)
|
||||
var locked_status: Array = []
|
||||
if main.camp_overlay.get("_workshop_holder") != null:
|
||||
if main.ui.modals.camp_overlay.get("_workshop_holder") != null:
|
||||
var ws_labels: Array = []
|
||||
_collect_labels_by_text(main.camp_overlay._workshop_holder, ws_labels)
|
||||
_collect_labels_by_text(main.ui.modals.camp_overlay._workshop_holder, ws_labels)
|
||||
locked_status = _labels_with_text(ws_labels, "未解锁")
|
||||
if locked_status.is_empty():
|
||||
_problems.append("营地灰字:秘宝匣行「未解锁」态 label 未找到(wc4 夹具前提失守——新档应有锁定秘宝行)")
|
||||
@@ -177,7 +177,7 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
|
||||
await get_tree().process_frame
|
||||
if main.camp_overlay.visible or not main._start_overlay.visible:
|
||||
if main.ui.modals.camp_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("营地关闭后未恢复开始层")
|
||||
# ---- 选档界面行为走查(迭代 33 卡③):sl5 空槽新档链/sl6 有档槽进档链/sl7 删档二次确认双路径
|
||||
# /sl8 消费方分派单口/sl12 动线序不回闪。开层=函数直调(「进入存档」钮归卡② mn3——中间态在案);
|
||||
@@ -196,11 +196,11 @@ func _ready() -> void:
|
||||
f_c2.close() # 槽2 有档异值(进档链夹具);槽1/3 空
|
||||
main._open_save_select()
|
||||
await get_tree().process_frame
|
||||
if not main.save_select_overlay.visible:
|
||||
if not main.ui.modals.save_select_overlay.visible:
|
||||
_problems.append("[savesel] 选档层未打开(click 段前置)")
|
||||
# sl12 动线序(新档链同拍采样):逐帧可见性次序——首帧选档隐藏时营地层必须已可见、开始层保持隐藏
|
||||
#(禁先关后开——_close_modal 会恢复开始层=回闪一帧)
|
||||
var new_btn: Button = main.save_select_overlay._new_btns.get(1)
|
||||
var new_btn: Button = main.ui.modals.save_select_overlay._new_btns.get(1)
|
||||
if new_btn == null:
|
||||
_problems.append("[savesel] 槽1 新档钮缺失(sl5/sl12 驱动中断)")
|
||||
else:
|
||||
@@ -208,7 +208,7 @@ func _ready() -> void:
|
||||
new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp
|
||||
for fi in 3:
|
||||
await get_tree().process_frame
|
||||
samples.append([main.save_select_overlay.visible, main.camp_overlay.visible, main._start_overlay.visible])
|
||||
samples.append([main.ui.modals.save_select_overlay.visible, main.ui.modals.camp_overlay.visible, main.ui.screens.start_overlay.visible])
|
||||
if samples[0][0]:
|
||||
_problems.append("[savesel] sl12 新档后选档层仍可见(落点开层未发生)")
|
||||
elif not samples[0][1]:
|
||||
@@ -217,8 +217,8 @@ func _ready() -> void:
|
||||
_problems.append("[savesel] sl12 动线序违约:开始层在落点帧恢复(回闪一帧)")
|
||||
if main.save_manager.slots.active_slot() != 1 or not FileAccess.file_exists(csel_tpl % 1):
|
||||
_problems.append("[savesel] sl5 新档链失守(active=%d 应=1 且槽1 白板应落盘)" % main.save_manager.slots.active_slot())
|
||||
var occluded: Array = main._modals._occluded.get(main.camp_overlay, [])
|
||||
if not occluded.has(main.save_select_overlay):
|
||||
var occluded: Array = main.ui.modals.flow.stack._occluded.get(main.ui.modals.camp_overlay, [])
|
||||
if not occluded.has(main.ui.modals.save_select_overlay):
|
||||
_problems.append("[savesel] sl12 选档层被关闭而非栈记账挂起(先关后开形态——遮挡挂起中间态钉死)")
|
||||
if int((JSON.parse_string(FileAccess.get_file_as_string(csel_tpl % 1)) as Dictionary).get("meta", {}).get("keystone_balance", -1)) != 0:
|
||||
_problems.append("[savesel] sl5 新档非白板(槽1 落盘应全零)")
|
||||
@@ -227,7 +227,7 @@ func _ready() -> void:
|
||||
# sl6 进档链:槽2 进档 → 激活=2(camp 数据随激活槽)
|
||||
main._open_save_select()
|
||||
await get_tree().process_frame
|
||||
var load_btn: Button = main.save_select_overlay._load_btns.get(2)
|
||||
var load_btn: Button = main.ui.modals.save_select_overlay._load_btns.get(2)
|
||||
if load_btn == null:
|
||||
_problems.append("[savesel] 槽2 进档钮缺失(sl6 驱动中断——夹具应有档)")
|
||||
else:
|
||||
@@ -235,51 +235,51 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
if main.save_manager.slots.active_slot() != 2:
|
||||
_problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.slots.active_slot())
|
||||
if not main.camp_overlay.visible or main.save_select_overlay.visible:
|
||||
if not main.ui.modals.camp_overlay.visible or main.ui.modals.save_select_overlay.visible:
|
||||
_problems.append("[savesel] sl6 落点未开营地层或选档未隐藏")
|
||||
main._on_camp_closed()
|
||||
await get_tree().process_frame
|
||||
# sl7 删档二次确认双路径:一击只开窗(文件仍在)→取消只关窗(文件仍在)→再开窗确认才删(槽回空)
|
||||
main._open_save_select()
|
||||
await get_tree().process_frame
|
||||
var del_btn: Button = main.save_select_overlay._delete_btns.get(2)
|
||||
var del_btn: Button = main.ui.modals.save_select_overlay._delete_btns.get(2)
|
||||
if del_btn == null:
|
||||
_problems.append("[savesel] 槽2 删档钮缺失(sl7 驱动中断)")
|
||||
else:
|
||||
del_btn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
if not main.confirm_dialog.is_open():
|
||||
if not main.ui.modals.confirm_dialog.is_open():
|
||||
_problems.append("[savesel] sl7 一击未只开确认窗(直删形态)")
|
||||
elif not FileAccess.file_exists(csel_tpl % 2):
|
||||
_problems.append("[savesel] sl7 确认前文件已被删(一次点击即删违契)")
|
||||
else:
|
||||
if not main._modal_flow.stack._occluded.has(main.confirm_dialog):
|
||||
if not main.ui.modals.flow.stack._occluded.has(main.ui.modals.confirm_dialog):
|
||||
_problems.append("[savesel] sl7 确认窗未经模态栈开(直开旁路——wr2 单口失守)")
|
||||
elif not main._input_locked():
|
||||
elif not main.ui.modals.flow.locks_input():
|
||||
_problems.append("[savesel] sl7 确认窗开时输入锁未生效(锁域失守——wr2)")
|
||||
main.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源)
|
||||
main.ui.modals.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源)
|
||||
await get_tree().process_frame
|
||||
if main.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2):
|
||||
if main.ui.modals.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2):
|
||||
_problems.append("[savesel] sl7 取消路径失守(应只关窗、文件仍在)")
|
||||
elif main._confirm_consumer != "":
|
||||
_problems.append("[savesel] sl8 取消后消费方状态量未清(分派面残留)")
|
||||
if main._modal_flow.stack._occluded.has(main.confirm_dialog):
|
||||
if main.ui.modals.flow.stack._occluded.has(main.ui.modals.confirm_dialog):
|
||||
_problems.append("[savesel] sl7 取消后确认窗遮挡账未清(关侧未走模态栈——wr2)")
|
||||
del_btn.pressed.emit() # 再开窗走确认路径
|
||||
await get_tree().process_frame
|
||||
main.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源)
|
||||
main.ui.modals.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源)
|
||||
await get_tree().process_frame
|
||||
if FileAccess.file_exists(csel_tpl % 2) or main.confirm_dialog.is_open():
|
||||
if FileAccess.file_exists(csel_tpl % 2) or main.ui.modals.confirm_dialog.is_open():
|
||||
_problems.append("[savesel] sl7 确认路径失守(应删档+关窗)")
|
||||
elif main.run_scope.is_adventure or main._start_overlay.visible:
|
||||
elif main.run_scope.is_adventure or main.ui.screens.start_overlay.visible:
|
||||
_problems.append("[savesel] sl8 删档确认误触发开局(双常驻接线形态——分派单口失守)")
|
||||
elif bool(main.save_select_overlay._occupied_flags.get(2, true)):
|
||||
elif bool(main.ui.modals.save_select_overlay._occupied_flags.get(2, true)):
|
||||
_problems.append("[savesel] sl7 删后选档面未就地刷新(槽2 应回空态)")
|
||||
elif main._confirm_consumer != "":
|
||||
_problems.append("[savesel] sl8 确认后消费方状态量未清(分派面残留)")
|
||||
main._on_save_select_closed()
|
||||
await get_tree().process_frame
|
||||
if main.save_select_overlay.visible or not main._start_overlay.visible:
|
||||
if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)")
|
||||
|
||||
# ---- 设置面段(隔离 store 换注禁碰真实 user://settings.cfg;真实按钮信号路径)----
|
||||
@@ -289,19 +289,19 @@ func _ready() -> void:
|
||||
var iso_s = load("res://presentation/settings_store.gd").new("user://smoke_click_probe_settings.cfg")
|
||||
iso_s.load_settings()
|
||||
main.settings_store = iso_s
|
||||
main.settings_overlay.retarget(iso_s)
|
||||
var settings_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "设置", "prefix")
|
||||
main.ui.modals.settings_overlay.retarget(iso_s)
|
||||
var settings_btn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix")
|
||||
if settings_btn == null:
|
||||
_problems.append("设置入口按钮缺失(开始层 settingsbtn)")
|
||||
else:
|
||||
settings_btn.pressed.emit() # 真实按钮信号路径 → _open_settings
|
||||
await get_tree().process_frame
|
||||
if not main.settings_overlay.visible or main._start_overlay.visible:
|
||||
if not main.ui.modals.settings_overlay.visible or main.ui.screens.start_overlay.visible:
|
||||
_problems.append("设置面未打开(点击路径)")
|
||||
else:
|
||||
# 减动效转正双态(卡④ AC③)——M4 适配:拨杆开关(_motion_btn)语义翻转 on=完整/off=快速,
|
||||
# 状态字随行 _motion_st;查找由文字改成员引用(拨杆无文字)
|
||||
var motion_btn: Button = main.settings_overlay._motion_btn
|
||||
var motion_btn: Button = main.ui.modals.settings_overlay._motion_btn
|
||||
if motion_btn == null:
|
||||
_problems.append("设置面减动效开关缺失")
|
||||
else:
|
||||
@@ -318,7 +318,7 @@ func _ready() -> void:
|
||||
if UiKit.motion_scale != 1.0:
|
||||
_problems.append("减动效关未恢复(motion_scale != 1.0)")
|
||||
# 音量实时生效(卡④ AC①):滑杆 30% → master bus db 与 store 双读
|
||||
var vol: HSlider = main.settings_overlay._volume
|
||||
var vol: HSlider = main.ui.modals.settings_overlay._volume
|
||||
vol.value = 30
|
||||
await get_tree().process_frame
|
||||
var master_idx := AudioServer.get_bus_index("Master")
|
||||
@@ -329,24 +329,24 @@ func _ready() -> void:
|
||||
_problems.append("音量未入 store(%f ≠ 0.3)" % iso_s.volume_linear)
|
||||
# 调试入口断言(卡⑦ c4 发布态换代——角标恒隐;原「最顶层浮层不被遮挡」
|
||||
# 回归语义随调试浮层发布态退役,申报见迭代 38 实现报告 §8.2)
|
||||
if main.debug_badge == null or main.debug_badge.visible:
|
||||
if main.ui.screens.debug_badge == null or main.ui.screens.debug_badge.visible:
|
||||
_problems.append("设置面开中调试角标可见(发布态恒隐失守)")
|
||||
var settings_close: Button = ProbeUiUtil.find_button(main.settings_overlay, "返回", "prefix")
|
||||
var settings_close: Button = ProbeUiUtil.find_button(main.ui.modals.settings_overlay, "返回", "prefix")
|
||||
if settings_close == null:
|
||||
_problems.append("设置面返回按钮缺失")
|
||||
else:
|
||||
settings_close.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
if main.settings_overlay.visible or not main._start_overlay.visible:
|
||||
if main.ui.modals.settings_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("设置面关闭后未恢复开始层")
|
||||
# 旧入口零残留断言(卡④ AC③——测试调整板不含减动效开关;14.5 起载体=调试浮窗)
|
||||
var legacy_motion = ProbeUiUtil.find_check_button(main.debug_overlay, "减动效")
|
||||
var legacy_motion = ProbeUiUtil.find_check_button(main.ui.modals.debug_overlay, "减动效")
|
||||
if legacy_motion != null:
|
||||
_problems.append("调整板减动效旧入口未退役(设置面唯一入口失守——卡④ AC③)")
|
||||
# D1 断言(二轮):退出游戏只在一级菜单——开始菜单有、设置面无
|
||||
if ProbeUiUtil.find_button(main._start_overlay, "退出游戏", "prefix") == null:
|
||||
if ProbeUiUtil.find_button(main.ui.screens.start_overlay, "退出游戏", "prefix") == null:
|
||||
_problems.append("开始菜单缺退出游戏按钮(二轮 D1)")
|
||||
if ProbeUiUtil.find_button(main.settings_overlay, "退出游戏", "prefix") != null:
|
||||
if ProbeUiUtil.find_button(main.ui.modals.settings_overlay, "退出游戏", "prefix") != null:
|
||||
_problems.append("设置面仍含退出游戏按钮(二轮 D1——只在一级菜单出现)")
|
||||
# ---- 生涯页段(入口点击链+关闭回返,真实按钮信号路径;本页只读零写,
|
||||
# 值断言归冒烟投影段与 ui_struct 结构族,此处只验开合路径与零跳转)----
|
||||
@@ -354,45 +354,45 @@ func _ready() -> void:
|
||||
if true:
|
||||
main._open_career()
|
||||
await get_tree().process_frame
|
||||
if not main.career_overlay.visible or main._start_overlay.visible:
|
||||
if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible:
|
||||
_problems.append("生涯页未打开(点击路径)")
|
||||
else:
|
||||
# 零跳转契约(提纲 P4 甲):页内除「返回」外零导航钮
|
||||
for nav in ["营地", "商店", "设置", "退出", "冒险"]:
|
||||
if ProbeUiUtil.find_button(main.career_overlay, nav, "contains") != null:
|
||||
if ProbeUiUtil.find_button(main.ui.modals.career_overlay, nav, "contains") != null:
|
||||
_problems.append("生涯页内出现导航入口「%s」(零跳转违约——生涯纯只读)" % nav)
|
||||
# 局内入口断言(提纲 P4 甲):生涯入口只在开始层——战斗屏/地图屏零生涯角标
|
||||
if main.board_zone.find_child("career_badge", true, false) != null \
|
||||
or main.level_screen.find_child("career_badge", true, false) != null:
|
||||
if main.ui.battle.board_zone.find_child("career_badge", true, false) != null \
|
||||
or main.ui.modals.level_screen.find_child("career_badge", true, false) != null:
|
||||
_problems.append("局内屏出现生涯入口件(局内不放生涯入口违约)")
|
||||
var career_close: Button = ProbeUiUtil.find_button(main.career_overlay, "返回", "prefix")
|
||||
var career_close: Button = ProbeUiUtil.find_button(main.ui.modals.career_overlay, "返回", "prefix")
|
||||
if career_close == null:
|
||||
_problems.append("生涯页返回按钮缺失")
|
||||
else:
|
||||
career_close.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
if main.career_overlay.visible or not main._start_overlay.visible:
|
||||
if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("生涯页关闭后未恢复开始层")
|
||||
# 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_click_probe_reset", true, false, true) # 零档换注(继续上局钮已随三入口化撤出——卡② mn8)
|
||||
main.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成——L2 岔路选路段只能经冒险推进到达(单关直入口退役)
|
||||
main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,行为走查确定性禁依赖默认分支)
|
||||
main.ui.modals.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成——L2 岔路选路段只能经冒险推进到达(单关直入口退役)
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,行为走查确定性禁依赖默认分支)
|
||||
var t1 := Time.get_ticks_msec()
|
||||
main._start_adventure_from_menu()
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
# 既有口径回归(迭代36 wc3——调试滑条 400/200=局外加成 pct>0,开场行应带「(含面板加成)」;
|
||||
# 本局为本探针首个冒险,日志无先前行,全文本判即可)
|
||||
var wc3_txt: String = main.log_view._log.get_parsed_text()
|
||||
var wc3_txt: String = main.ui.battle.log_view._log.get_parsed_text()
|
||||
if not wc3_txt.contains("种子 42") or not wc3_txt.contains("(含面板加成)"):
|
||||
_problems.append("开场行回归失守:局外加成(pct>0)局的开场行应带「(含面板加成)」(wc3)")
|
||||
# 关卡开始 = 入口选路停点——press_candidate 图上命中路径选入口候选进首战
|
||||
#(首格可点 AC 口径随之含入口选路一步:菜单点击 → 停点呈现 → pick → 棋盘安装)
|
||||
if main.level_screen.visible:
|
||||
if main.ui.modals.level_screen.visible:
|
||||
# 独立性断言:独立屏激活期战斗屏宿主整体隐藏
|
||||
if main._battle_root.visible:
|
||||
if main.ui.screens.battle_root.visible:
|
||||
_problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)")
|
||||
main.level_screen.press_candidate(0)
|
||||
main.ui.modals.level_screen.press_candidate(0)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var t_entry := Time.get_ticks_msec() - t1
|
||||
@@ -402,7 +402,7 @@ func _ready() -> void:
|
||||
var first_cell_ms := t_entry
|
||||
if first_cell_ms > 500:
|
||||
_problems.append("进入战斗屏首格可点超时 %dms > 500ms" % first_cell_ms)
|
||||
if main.board_view.cells_probe().is_empty():
|
||||
if main.ui.battle.board_zone.board_view.cells_probe().is_empty():
|
||||
_problems.append("首格未可点(cells=0)")
|
||||
print("[click_probe] 首格可点计时 %dms(进入战斗屏口径,≤500ms AC)· 场景冷启动 %dms(含 UI 构建 %dms,观测项)" % [
|
||||
first_cell_ms, t_total, t_ui_ready])
|
||||
@@ -413,68 +413,68 @@ func _ready() -> void:
|
||||
while not _done and action_budget > 0:
|
||||
action_budget -= 1
|
||||
await get_tree().process_frame
|
||||
if main.node_overlay.visible:
|
||||
main.node_overlay.press_continue()
|
||||
if main.ui.modals.node_overlay.visible:
|
||||
main.ui.modals.node_overlay.press_continue()
|
||||
_seals_placed.clear()
|
||||
continue
|
||||
if main.curio_overlay.visible:
|
||||
if main.ui.modals.curio_overlay.visible:
|
||||
# 三选一停点(CurioPickOverlay——奖励节点/关卡完成兑付共用面,须与硬锁态适配,
|
||||
# 未适配则走到即卡死)。两态分派:result 态(兑付反馈——点继续推进)/
|
||||
# 待选态(点首件+确认)。真实信号路径全线。〔无替换子态——装配不可变契约。〕
|
||||
# 三选一停点输入锁定断言(停点态断言面:待选/兑付两态共用此层)。
|
||||
if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty() and String(main.curio_overlay._header).contains("关卡奖励点"):
|
||||
if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty() and String(main.ui.modals.curio_overlay._header).contains("关卡奖励点"):
|
||||
# 停点态判据 = _node_curio_pending(奖励节点三选一停点住此——原条件查
|
||||
# _curio_pending〔关卡完成三选一态〕与奖励头部互斥、恒不可命中=拦截能力未证病根,
|
||||
# 卡⑤ d4 转正随修;头部校验串本体零触碰)
|
||||
_tested_curio_head = true
|
||||
var ch := String(main.curio_overlay._header)
|
||||
var ch := String(main.ui.modals.curio_overlay._header)
|
||||
if not ch.contains("冒险牌库") or not ch.contains("属性值提升与补给") or ch.contains("同权重") or ch.contains("同上限") or ch.contains("提升池"):
|
||||
_problems.append("三选一头部旧话残留或新话缺失(wa4——%s)" % ch.replace("
|
||||
", "|"))
|
||||
# 奖励节点战报文案换代断言(卡⑤ wr2——reward_line 同屏打印于停点拉起前,
|
||||
# 同一驱动路径承载;旧「内容待定」残留或新话缺失即红)
|
||||
var logtext: String = main.log_view.parsed_text()
|
||||
var logtext: String = main.ui.battle.log_view.parsed_text()
|
||||
if not logtext.contains("关卡奖励点(三选一——选毕推进)") or logtext.contains("内容待定"):
|
||||
_problems.append("奖励节点战报文案未换代(卡⑤ wr2——旧话残留或新话缺失)")
|
||||
if not main._input_locked():
|
||||
if not main.ui.modals.flow.locks_input():
|
||||
_problems.append("三选一停点打开时输入未锁定")
|
||||
if main.curio_overlay._continue_btn.visible:
|
||||
main.curio_overlay.press_continue()
|
||||
if main.ui.modals.curio_overlay._continue_btn.visible:
|
||||
main.ui.modals.curio_overlay.press_continue()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
|
||||
main.curio_overlay.press_card(0) # 奖励节点待选态:点首件+确认(真实按钮信号路径——与关卡完成停点同形)
|
||||
main.curio_overlay.press_confirm()
|
||||
main.ui.modals.curio_overlay.press_card(0) # 奖励节点待选态:点首件+确认(真实按钮信号路径——与关卡完成停点同形)
|
||||
main.ui.modals.curio_overlay.press_confirm()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
|
||||
main.curio_overlay.press_card(0)
|
||||
main.curio_overlay.press_confirm()
|
||||
main.ui.modals.curio_overlay.press_card(0)
|
||||
main.ui.modals.curio_overlay.press_confirm()
|
||||
else:
|
||||
main.curio_overlay.press_continue() # 停点已消费的残面——继续关屏
|
||||
main.ui.modals.curio_overlay.press_continue() # 停点已消费的残面——继续关屏
|
||||
_seals_placed.clear()
|
||||
continue
|
||||
if main.shop_overlay.visible:
|
||||
if main.ui.modals.shop_overlay.visible:
|
||||
# 商店房覆盖层(摆放约束:每终点前一位恰一店,真实路径恒必经一店):零购离开(真实 leave 信号路径)放行选路。
|
||||
# 分派链序:商店层=选路页上的模态覆盖(停点呈现为
|
||||
# level_screen+shop 双 visible)——必须先于 level_screen 分派,否则下层候选点击
|
||||
# 空转烧尽预算(实证:lv2 商店停点双 visible 循环 12000 帧 done=false)。
|
||||
main.shop_overlay.leave_pressed.emit()
|
||||
main.ui.modals.shop_overlay.leave_pressed.emit()
|
||||
_seals_placed.clear()
|
||||
continue
|
||||
if main.level_screen.visible:
|
||||
if main.ui.modals.level_screen.visible:
|
||||
# 节点图选路口(零自动推进停点)——图上命中路径交替选候选(岔路使用行为;布局决策 2 改造)
|
||||
# 地图屏角标一次性 case:选路前经角标开/关状态面板(层在地图屏之上开合,
|
||||
# 关闭后地图屏选路态零扰动——P2 甲层级语义)
|
||||
if not _tested_map_status and not main.status_panel.visible:
|
||||
if not _tested_map_status and not main.ui.modals.status_panel.visible:
|
||||
_tested_map_status = true
|
||||
var mpb: Button = main.level_screen.status_badge
|
||||
var mpb: Button = main.ui.modals.level_screen.status_badge
|
||||
if mpb == null or not mpb.visible or mpb.disabled:
|
||||
_problems.append("地图屏状态角标缺失/未点亮/被置灰(P1 丁)")
|
||||
else:
|
||||
mpb.pressed.emit()
|
||||
if not main.status_panel.visible or not main.level_screen.visible:
|
||||
if not main.ui.modals.status_panel.visible or not main.ui.modals.level_screen.visible:
|
||||
_problems.append("地图屏角标开状态面板失败或地图屏被误藏(P2 甲层级失守)")
|
||||
var rbtn2: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
|
||||
var rbtn2: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix")
|
||||
if rbtn2 != null:
|
||||
rbtn2.pressed.emit()
|
||||
if main.status_panel.visible or not main.level_screen.visible:
|
||||
if main.ui.modals.status_panel.visible or not main.ui.modals.level_screen.visible:
|
||||
_problems.append("状态面板关闭后地图屏态失守(恢复语义破)")
|
||||
# 地图屏加成帽换代断言(迭代36 卡A wa2——「特性」→「属性值提升」;帽 Label 常在树;
|
||||
# 独立旗标——_tested_map_status 在同拍前置块已置位,禁复用其门控)
|
||||
@@ -482,7 +482,7 @@ func _ready() -> void:
|
||||
_tested_map_cap = true
|
||||
var cap_found := false
|
||||
var ls_labels: Array = []
|
||||
_collect_labels_by_text(main.level_screen, ls_labels)
|
||||
_collect_labels_by_text(main.ui.modals.level_screen, ls_labels)
|
||||
for l in ls_labels:
|
||||
if l.text == "属性值提升":
|
||||
cap_found = true
|
||||
@@ -494,20 +494,20 @@ func _ready() -> void:
|
||||
if main.run_scope.runner != null:
|
||||
for nd in main.run_scope.runner.nodes():
|
||||
var nd_id: int = main.run_scope.runner.node_id_of(nd)
|
||||
if String(nd.get("type", "")) == "reward" and main.level_screen._cand_ids.has(nd_id):
|
||||
pick = main.level_screen._cand_ids.find(nd_id)
|
||||
if String(nd.get("type", "")) == "reward" and main.ui.modals.level_screen._cand_ids.has(nd_id):
|
||||
pick = main.ui.modals.level_screen._cand_ids.find(nd_id)
|
||||
break
|
||||
main.level_screen.press_candidate(pick)
|
||||
main.ui.modals.level_screen.press_candidate(pick)
|
||||
_route_clicks += 1
|
||||
_seals_placed.clear()
|
||||
continue
|
||||
if main._inter_overlay.visible:
|
||||
if main.ui.screens.inter_overlay.visible:
|
||||
# 大奖励屏(冒险级停点——非末关的关卡完成恰一次):首见确认推进 L2(真实按钮信号路径);再见 = L2 已关卡完成,收口
|
||||
if not main._input_locked():
|
||||
if not main.ui.modals.flow.locks_input():
|
||||
_problems.append("大奖励屏打开时输入未锁定")
|
||||
_inter_confirms += 1
|
||||
if _inter_confirms == 1:
|
||||
var next_btn: Button = ProbeUiUtil.find_button(main._inter_overlay, "进入下一层", "prefix")
|
||||
var next_btn: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
|
||||
if next_btn == null:
|
||||
_problems.append("大奖励屏确认按钮缺失")
|
||||
else:
|
||||
@@ -522,23 +522,23 @@ func _ready() -> void:
|
||||
break
|
||||
_seals_placed.clear()
|
||||
continue
|
||||
if main._end_overlay.is_open() and main._settlement_stop_pending:
|
||||
if main.ui.modals.end_overlay.is_open() and main.run_scope.settlement_stop_timer.is_armed():
|
||||
# 战斗胜利总结结算框(真实信号路径——每场普通胜利到达):断言已拍板面后点「下一节点」
|
||||
if not main._input_locked():
|
||||
if not main.ui.modals.flow.locks_input():
|
||||
_problems.append("胜利结算框打开时输入未锁定")
|
||||
var gp_line: String = "%s %s" % [main._end_overlay._gp_name.text, main._end_overlay._gp_val.text] # 两列账行(审计差距④)——名/值列拼回整串断言
|
||||
var gp_line: String = "%s %s" % [main.ui.modals.end_overlay._gp_name.text, main.ui.modals.end_overlay._gp_val.text] # 两列账行(审计差距④)——名/值列拼回整串断言
|
||||
if not gp_line.contains("GP +%d" % main.run_scope.last_gain_amount) or not gp_line.contains("合计 %d" % int(main.run_scope.runner.gp)):
|
||||
_problems.append("胜利结算框 GP 增量/总量与钱包实态不符(%s|实态 +%d/%d)" % [gp_line, main.run_scope.last_gain_amount, int(main.run_scope.runner.gp)])
|
||||
if main._end_overlay.verdict_text() == "":
|
||||
if main.ui.modals.end_overlay.verdict_text() == "":
|
||||
_problems.append("胜利结算框判定行缺失")
|
||||
_settlement_confirms += 1
|
||||
main._end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径
|
||||
main.ui.modals.end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if main._settlement_stop_pending or not main.level_screen.visible:
|
||||
if main.run_scope.settlement_stop_timer.is_armed() or not main.ui.modals.level_screen.visible:
|
||||
_problems.append("胜利结算框「下一节点」未进选路页(卡⑥ AC②)")
|
||||
continue
|
||||
if main._end_overlay.is_open():
|
||||
if main.ui.modals.end_overlay.is_open():
|
||||
# 冒险终局(失败/放弃/打穿)——无单关直入口,见终局即收口(模态断言照旧)
|
||||
# 三选一链适配:_adventure_mode 仍真且非选路挂起=中途结算面(Boss 胜利
|
||||
# 总结「继续」后挂三选一→真终局未到)——真实信号路径点继续推进。
|
||||
@@ -547,13 +547,13 @@ func _ready() -> void:
|
||||
# (_adventure_mode)在真终局仍 true——会把真终局当中途面点掉返回键(实证)。
|
||||
var _adv_running: bool = main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running"
|
||||
if main.run_scope.is_adventure and _adv_running:
|
||||
main._end_overlay._btn.pressed.emit()
|
||||
main.ui.modals.end_overlay._btn.pressed.emit()
|
||||
continue
|
||||
if not main._input_locked():
|
||||
if not main.ui.modals.flow.locks_input():
|
||||
_problems.append("终局 overlay 打开时输入未锁定")
|
||||
_done = true
|
||||
print("[click_probe] 冒险终局:%s · 选路口点击 %d 次" % [
|
||||
main._end_overlay.verdict_text().replace("\n", " "), _route_clicks])
|
||||
main.ui.modals.end_overlay.verdict_text().replace("\n", " "), _route_clicks])
|
||||
break
|
||||
if main.run_scope.settlement == null or main.run_scope.runner == null:
|
||||
continue
|
||||
@@ -566,16 +566,16 @@ func _ready() -> void:
|
||||
# 上轮摘要恢复断言(卡⑥ wr1-b——改前 _round_rows 类型错 append 全数失败、缓冲恒空、
|
||||
# 摘要自引入类型错以来从未显示):战中场观测点=round≥2 且排播队列已排空(轮首分隔行
|
||||
# 已消费=finish_settlement_round 已刷);不挂循环出口——收尾的续玩重建段会再走 log_view.clear()
|
||||
if not _tested_round_summary and int(st["round"]) >= 2 and main._row_queue.is_empty():
|
||||
if not _tested_round_summary and int(st["round"]) >= 2 and main.ui.screens.row_pacer.pending_rows().is_empty():
|
||||
_tested_round_summary = true
|
||||
if main.log_view._summary.text == "":
|
||||
if main.ui.battle.log_view._summary.text == "":
|
||||
_problems.append("上轮摘要恒空(卡⑥ wr1-b——结算行缓冲仍失真)")
|
||||
if JSON.stringify(st) == JSON.stringify(_last_state): # 排播演播期豁免(硬锁 v3——合法停顿态):演播期棋盘锁生效、probe 点击被吞、
|
||||
# 逻辑态恒定属预期。判据=纯队列(与 main 锁同源)。Waiting 收尾等待期**不豁免**——
|
||||
# 锁若把收尾链卡死,此处 stall 照报(探测面)。
|
||||
# T1 D1 处置=保留豁免+注记:冻结/缓存/重放语义不再靠豁免绕行,由 _locktest_tick
|
||||
# 脚本化断言正面承载(每 run 一次;逻辑态恒定本豁免继续作 stall 判据)。
|
||||
if main._row_queue.is_empty():
|
||||
if main.ui.screens.row_pacer.pending_rows().is_empty():
|
||||
_stall_frames += 1
|
||||
if _stall_frames == 30:
|
||||
_report_stall(st)
|
||||
@@ -595,36 +595,36 @@ func _ready() -> void:
|
||||
# ---- 预算耗尽收口(T17 硬锁批适配):选件变强后 probe 可能活过预算不再战败——断点段前提
|
||||
# (冒险终局面在案)须主动满足。收口驱动:任何停点层逐个推进(复用主循环分支语义)至
|
||||
# level_screen 选路停点→真实放弃二击→终局面。战斗中则继续 autoplay 打完。
|
||||
if not _done and not main._end_overlay.is_open():
|
||||
if not _done and not main.ui.modals.end_overlay.is_open():
|
||||
var close_guard := 8000
|
||||
var dbg_frame := 0 # T17 演播后排窗:每场胜利 +~400 帧演播等待(旧同帧 flush 零等待)——守卫按多场战斗放大
|
||||
while close_guard > 0:
|
||||
close_guard -= 1
|
||||
await get_tree().process_frame
|
||||
if main._end_overlay.is_open():
|
||||
if main.ui.modals.end_overlay.is_open():
|
||||
# 中途结算面(state=running)=点继续推进;真终局(settling)=断点段前提满足,收口。
|
||||
if main.run_scope.is_adventure and main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running":
|
||||
main._end_overlay._btn.pressed.emit()
|
||||
main.ui.modals.end_overlay._btn.pressed.emit()
|
||||
else:
|
||||
break
|
||||
elif main.curio_overlay.visible:
|
||||
if main.curio_overlay._continue_btn.visible:
|
||||
main.curio_overlay.press_continue()
|
||||
elif main.ui.modals.curio_overlay.visible:
|
||||
if main.ui.modals.curio_overlay._continue_btn.visible:
|
||||
main.ui.modals.curio_overlay.press_continue()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
|
||||
main.curio_overlay.press_card(0)
|
||||
main.curio_overlay.press_confirm()
|
||||
main.ui.modals.curio_overlay.press_card(0)
|
||||
main.ui.modals.curio_overlay.press_confirm()
|
||||
else:
|
||||
main.curio_overlay.press_continue()
|
||||
elif main.node_overlay.visible:
|
||||
main.node_overlay.press_continue()
|
||||
elif main.shop_overlay.visible:
|
||||
main.shop_overlay.leave_pressed.emit()
|
||||
elif main._inter_overlay.visible:
|
||||
var next_btn2: Button = ProbeUiUtil.find_button(main._inter_overlay, "进入下一层", "prefix")
|
||||
main.ui.modals.curio_overlay.press_continue()
|
||||
elif main.ui.modals.node_overlay.visible:
|
||||
main.ui.modals.node_overlay.press_continue()
|
||||
elif main.ui.modals.shop_overlay.visible:
|
||||
main.ui.modals.shop_overlay.leave_pressed.emit()
|
||||
elif main.ui.screens.inter_overlay.visible:
|
||||
var next_btn2: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
|
||||
if next_btn2 != null:
|
||||
next_btn2.pressed.emit()
|
||||
elif main.level_screen.visible:
|
||||
var abandon_btn: Button = ProbeUiUtil.find_button(main.level_screen, "放弃", "prefix")
|
||||
elif main.ui.modals.level_screen.visible:
|
||||
var abandon_btn: Button = ProbeUiUtil.find_button(main.ui.modals.level_screen, "放弃", "prefix")
|
||||
if abandon_btn != null:
|
||||
_closed_via_abandon = true
|
||||
abandon_btn.pressed.emit()
|
||||
@@ -652,14 +652,14 @@ func _ready() -> void:
|
||||
str(((bpf as Dictionary).get("bonus_deck", []) as Array).size()),
|
||||
str((bpf as Dictionary).get("bonus_pending"))])
|
||||
_problems.append("断点段:战败结算后断点未清(T3 三结局清断点失守)")
|
||||
if not main._end_overlay.is_open():
|
||||
if not main.ui.modals.end_overlay.is_open():
|
||||
_problems.append("断点段:战败后未出冒险终局面")
|
||||
if main._end_overlay._btn == null:
|
||||
if main.ui.modals.end_overlay._btn == null:
|
||||
_problems.append("断点段:终局面返回按钮缺失")
|
||||
else:
|
||||
main._end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄)
|
||||
main.ui.modals.end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄)
|
||||
await get_tree().process_frame
|
||||
if not main._start_overlay.visible:
|
||||
if not main.ui.screens.start_overlay.visible:
|
||||
# 豁免:放弃路径收口时终局「返回」后仍挂三选一/关卡推进残留态(停点链),
|
||||
# 战败态前提失配——记录不计 problem(返回路径已实机目检)。
|
||||
if not _closed_via_abandon:
|
||||
@@ -683,16 +683,16 @@ func _ready() -> void:
|
||||
if FileAccess.file_exists(p_ol):
|
||||
DirAccess.remove_absolute(p_ol)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_click_probe_openline", true, false, true)
|
||||
main.debug_overlay.reset_sliders() # ① 白板反向前提(_start_adventure 只关窗不清值——残值跨段存活防坑)
|
||||
main.debug_overlay.seed_edit.text = "42"
|
||||
main.ui.modals.debug_overlay.reset_sliders() # ① 白板反向前提(_start_adventure 只关窗不清值——残值跨段存活防坑)
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "42"
|
||||
main._start_adventure_from_menu()
|
||||
var ol_l1_wait := 0
|
||||
while ol_l1_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1:
|
||||
while ol_l1_wait < 24 and String(main.ui.battle.log_view._log.get_parsed_text()).count("· 种子 ") < 1:
|
||||
ol_l1_wait += 1
|
||||
await get_tree().process_frame
|
||||
if ol_l1_wait >= 24:
|
||||
_problems.append("开场行路径①:L1 开场行未达日志(开局清日志后 24 帧内未现种子行)")
|
||||
elif String(main.log_view._log.get_parsed_text()).count("(含面板加成)") > 0:
|
||||
elif String(main.ui.battle.log_view._log.get_parsed_text()).count("(含面板加成)") > 0:
|
||||
_problems.append("开场行路径①失守:白板局 L1 开场行带「(含面板加成)」(wc1 反向)")
|
||||
print("[click_probe] 开场行①判定 wait=%d 帧断言完成" % ol_l1_wait)
|
||||
# ②③ 断点续玩正向(既有快照喂入形态——代拍板 2 预授权;生产「继续上局」钮已随三入口化
|
||||
@@ -716,14 +716,14 @@ func _ready() -> void:
|
||||
# resume 同步重建开场行,返回时已在日志)
|
||||
main._resume_from_menu()
|
||||
var ol_r_wait := 0
|
||||
while ol_r_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1:
|
||||
while ol_r_wait < 24 and String(main.ui.battle.log_view._log.get_parsed_text()).count("· 种子 ") < 1:
|
||||
ol_r_wait += 1
|
||||
await get_tree().process_frame
|
||||
if main.run_scope.runner == null or main.run_scope.runner.journal().bonus_total("atk") != 5:
|
||||
_problems.append("开场行路径②③前提失守:续玩重建后加算区 atk!=5(断点回填链断)")
|
||||
if ol_r_wait >= 24:
|
||||
_problems.append("开场行路径②③失守:续玩重建未发开场行(选路停点形应重发——形态/驱动复核)")
|
||||
elif String(main.log_view._log.get_parsed_text()).count("(含面板加成)") < 1:
|
||||
elif String(main.ui.battle.log_view._log.get_parsed_text()).count("(含面板加成)") < 1:
|
||||
_problems.append("开场行路径②③失守:bonus 已入账(pct=0/ov=0)的续玩重建开场行未带「(含面板加成)」(wc1 正向/wc2)")
|
||||
else:
|
||||
print("[click_probe] 开场行②③判定:续玩重建开场行带后缀(bonus.atk=5、pct=0)— wait=%d 帧" % ol_r_wait)
|
||||
@@ -749,15 +749,15 @@ func _ready() -> void:
|
||||
|
||||
func _run_modal_probes_once() -> void:
|
||||
# ⓪ 玩家面文案真实入口断言(迭代36 卡A wa1——C 区两段头随战斗常驻)
|
||||
if not _tested_copy and main.info_panel != null and main.info_panel.visible:
|
||||
if not _tested_copy and main.ui.battle.info_panel != null and main.ui.battle.info_panel.visible:
|
||||
_tested_copy = true
|
||||
var feat_head: Label = null
|
||||
var curio_head: Label = null
|
||||
for child in main.info_panel._feat_section.get_children():
|
||||
for child in main.ui.battle.info_panel._feat_section.get_children():
|
||||
if child is Label:
|
||||
feat_head = child
|
||||
break
|
||||
for child in main.info_panel._curio_section.get_children():
|
||||
for child in main.ui.battle.info_panel._curio_section.get_children():
|
||||
if child is Label:
|
||||
curio_head = child
|
||||
break
|
||||
@@ -768,15 +768,15 @@ func _run_modal_probes_once() -> void:
|
||||
# ① 图例模态(pattern 6):打开 → 遮挡物隐藏 + 输入锁定;关闭 → 恢复
|
||||
if not _tested_legend:
|
||||
_tested_legend = true
|
||||
main.ap_bar._btn_legend.pressed.emit()
|
||||
if not main.legend_overlay.is_open():
|
||||
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit()
|
||||
if not main.ui.modals.legend_overlay.is_open():
|
||||
_problems.append("图例打开失败")
|
||||
if main._end_overlay.is_open() or main._start_overlay.visible:
|
||||
if main.ui.modals.end_overlay.is_open() or main.ui.screens.start_overlay.visible:
|
||||
_problems.append("图例打开时上方遮挡层未隐藏(缺陷⑤复发)")
|
||||
if not main._input_locked():
|
||||
if not main.ui.modals.flow.locks_input():
|
||||
_problems.append("图例打开时棋盘输入未锁定")
|
||||
var legend_has_seal := false
|
||||
for e in main.legend_overlay.ENTRIES:
|
||||
for e in main.ui.modals.legend_overlay.ENTRIES:
|
||||
if String(e[1]) == "封印":
|
||||
legend_has_seal = true
|
||||
# 图例封印行换代断言(迭代36 卡A wa5——真实 AP 条图例钮开层路径)
|
||||
@@ -785,31 +785,31 @@ func _run_modal_probes_once() -> void:
|
||||
", "|"))
|
||||
if not legend_has_seal:
|
||||
_problems.append("图例缺『封印』格态条目(格态图例基线)")
|
||||
main.legend_overlay.close()
|
||||
if main.legend_overlay.is_open() or main._input_locked():
|
||||
main.ui.modals.legend_overlay.close()
|
||||
if main.ui.modals.legend_overlay.is_open() or main.ui.modals.flow.locks_input():
|
||||
_problems.append("图例关闭后未恢复输入/可见性")
|
||||
# ②/②b 明细回看模态无探针(M9/P-C7——冒险中回看归「生涯」,06 登记)
|
||||
# ⑤ 状态面板(P1 丁角标真实信号路径 + P2 甲模态语义 + P1 守卫 + R3 锁恢复):
|
||||
# 角标打开 → 只读层可见+输入锁定+余额徽非空 → 「继续」关闭恢复;图例开着 → 角标置灰且点击被守卫兜住
|
||||
if not _tested_status and not main.legend_overlay.is_open() and not main.status_panel.visible:
|
||||
if not _tested_status and not main.ui.modals.legend_overlay.is_open() and not main.ui.modals.status_panel.visible:
|
||||
_tested_status = true
|
||||
var pbtn: Button = main.board_zone.status_badge
|
||||
var pbtn: Button = main.ui.battle.board_zone.status_badge
|
||||
if pbtn == null or not pbtn.visible or pbtn.disabled:
|
||||
_problems.append("战斗屏状态角标缺失/未点亮/被置灰(P1 丁入口失守 visible=%s disabled=%s)"
|
||||
% [str(pbtn != null and pbtn.visible), str(pbtn != null and pbtn.disabled)])
|
||||
else:
|
||||
pbtn.pressed.emit() # 真实按钮信号路径 → 组合根 _on_status_badge_pressed
|
||||
if not main.status_panel.visible:
|
||||
if not main.ui.modals.status_panel.visible:
|
||||
_problems.append("状态面板未随角标打开(组合根接线失守)")
|
||||
if not main._input_locked():
|
||||
if not main.ui.modals.flow.locks_input():
|
||||
_problems.append("状态面板打开时输入未锁定(R3)")
|
||||
var bal_txt: String = main.status_panel._balance_badge.value_label.text
|
||||
var bal_txt: String = main.ui.modals.status_panel._balance_badge.value_label.text
|
||||
if not bal_txt.begins_with("钥石 "):
|
||||
_problems.append("状态面板余额徽投影缺失(%s)" % bal_txt)
|
||||
# ---- 本局信息面板换代断言(迭代36 卡③——真实入口三断言)----
|
||||
# ① 面板子树零旧段文案(跨关概览退役零残留)
|
||||
var sv_labels: Array = []
|
||||
_collect_labels_by_text(main.status_panel, sv_labels)
|
||||
_collect_labels_by_text(main.ui.modals.status_panel, sv_labels)
|
||||
for l in sv_labels:
|
||||
if String(l.text).contains("道具工坊") or String(l.text).contains("天赋树"):
|
||||
_problems.append("状态面板子树残留旧段文案「%s」(sv6①——零旧段失守)" % String(l.text))
|
||||
@@ -821,8 +821,8 @@ func _run_modal_probes_once() -> void:
|
||||
var attr_expect := "生命 %d/%d · 攻击 %d · 护甲 AC %d · GP %d" % [
|
||||
int(rnr.hero_hp), rnr.effective_max_hp(), rnr.effective_atk(),
|
||||
rnr.effective_ac(), int(rnr.gp)]
|
||||
if String(main.status_panel._attrs_label.text) != attr_expect:
|
||||
_problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.status_panel._attrs_label.text])
|
||||
if String(main.ui.modals.status_panel._attrs_label.text) != attr_expect:
|
||||
_problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.ui.modals.status_panel._attrs_label.text])
|
||||
# ③ _status_data() 直调对拍(sv7——键集恰八键+curios/feats 独立 oracle)
|
||||
var sd: Dictionary = main._status_data()
|
||||
var keys := ["balance", "hero_hp", "max_hp", "atk", "ac", "gp", "curios", "feats"]
|
||||
@@ -846,25 +846,25 @@ func _run_modal_probes_once() -> void:
|
||||
var feats_expect: Array = load("res://presentation/hud_presenter.gd").feat_holdings_lines(main.db, rnr.feats_held())
|
||||
if str((sd as Dictionary).get("feats", [])) != str(feats_expect):
|
||||
_problems.append("_status_data().feats 对拍 feat_holdings_lines 失守(sv7)")
|
||||
var resume_btn: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
|
||||
var resume_btn: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix")
|
||||
if resume_btn == null:
|
||||
_problems.append("状态面板关闭钮缺失(「继续」)")
|
||||
else:
|
||||
resume_btn.pressed.emit()
|
||||
if main.status_panel.visible or main._input_locked():
|
||||
if main.ui.modals.status_panel.visible or main.ui.modals.flow.locks_input():
|
||||
_problems.append("状态面板关闭后未恢复(visible=%s locked=%s——R3 残留)"
|
||||
% [str(main.status_panel.visible), str(main._input_locked())])
|
||||
% [str(main.ui.modals.status_panel.visible), str(main.ui.modals.flow.locks_input())])
|
||||
# P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住(探针 emit 绕过 disabled)
|
||||
main.ap_bar._btn_legend.pressed.emit()
|
||||
if not main.board_zone.status_badge.disabled:
|
||||
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit()
|
||||
if not main.ui.battle.board_zone.status_badge.disabled:
|
||||
_problems.append("图例开着时战斗屏状态角标未置灰(P1 守卫失守)")
|
||||
main.board_zone.status_badge.pressed.emit()
|
||||
if main.status_panel.visible:
|
||||
main.ui.battle.board_zone.status_badge.pressed.emit()
|
||||
if main.ui.modals.status_panel.visible:
|
||||
_problems.append("置灰态仍可开状态面板(守卫兜底失守)")
|
||||
main.legend_overlay.close()
|
||||
main.ui.modals.legend_overlay.close()
|
||||
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发
|
||||
if not _tested_tooltip:
|
||||
var row = main.stage.hero_standee.slot_row # 状态 tooltip 载体(B 区站桩)
|
||||
var row = main.ui.battle.stage.hero_standee.slot_row # 状态 tooltip 载体(B 区站桩)
|
||||
for child in row.get_children():
|
||||
if child is PanelContainer and child.visible and child.has_meta("eff"):
|
||||
_tested_tooltip = true
|
||||
@@ -878,14 +878,14 @@ func _run_modal_probes_once() -> void:
|
||||
_problems.append("状态槽 tooltip 未随移出隐藏")
|
||||
break
|
||||
# ④ 弃图刷新确认链(pattern 7 异位:确认 chips 与常规确认不同位 + 显式代价文案)
|
||||
if not _tested_refresh_chain and main.ap_bar.is_refresh_ready():
|
||||
if not _tested_refresh_chain and main.ui.battle.board_zone.ap_bar.is_refresh_ready():
|
||||
_tested_refresh_chain = true
|
||||
var rap_before: int = int(main.run_scope.settlement.action_economy.rap)
|
||||
var board_before = main.run_scope.settlement.minesweeper.current_board()
|
||||
main.ap_bar.press_refresh_for_probe()
|
||||
if not main.ap_bar.confirm_visible():
|
||||
main.ui.battle.board_zone.ap_bar.press_refresh_for_probe()
|
||||
if not main.ui.battle.board_zone.ap_bar.confirm_visible():
|
||||
_problems.append("弃图刷新确认 chips 未出现(异位确认缺失)")
|
||||
main.ap_bar.press_confirm_for_probe()
|
||||
main.ui.battle.board_zone.ap_bar.press_confirm_for_probe()
|
||||
if int(main.run_scope.settlement.action_economy.rap) != rap_before - 8:
|
||||
_problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.run_scope.settlement.action_economy.rap)])
|
||||
if main.run_scope.settlement.action_economy.phase != "Active":
|
||||
@@ -897,8 +897,8 @@ func _run_modal_probes_once() -> void:
|
||||
if used_now != 1:
|
||||
_problems.append("刷新轮内计数错误(%d)" % used_now)
|
||||
var remain_expect: int = maxi(0, int(main.db.run.action_economy.refresh_cap_per_round) - used_now)
|
||||
if not main.ap_bar.refresh_remaining_text().contains(str(remain_expect)):
|
||||
_problems.append("HUD 刷新余量与快照推导失符(HUD='%s' 应含 %d)" % [main.ap_bar.refresh_remaining_text(), remain_expect])
|
||||
if not main.ui.battle.board_zone.ap_bar.refresh_remaining_text().contains(str(remain_expect)):
|
||||
_problems.append("HUD 刷新余量与快照推导失符(HUD='%s' 应含 %d)" % [main.ui.battle.board_zone.ap_bar.refresh_remaining_text(), remain_expect])
|
||||
|
||||
|
||||
# ---- 营地灰字取色断言辅助(迭代36 wc4——文本锚定位,全 Label 递归收集)----
|
||||
@@ -922,14 +922,14 @@ func _play_one_action() -> void:
|
||||
var settlement = main.run_scope.settlement
|
||||
if settlement == null:
|
||||
return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 settlement 可能已被置空(基线既有噪声——加固)
|
||||
if not main._row_queue.is_empty():
|
||||
if not main.ui.screens.row_pacer.pending_rows().is_empty():
|
||||
return # T1 D1——演播期不出手:此时点击只会进锁期缓存被「最后一次」覆盖(原逐帧连点语义无行为覆盖价值,且毁锁期断言注入的独占前提)
|
||||
var minesweeper = settlement.minesweeper
|
||||
# 行为①:随机封印(右键——未开相邻格),约 20% 轮次
|
||||
if _ap_spent_this_round == 0 and _rand.randf() < 0.2:
|
||||
var target := _random_unknown_adjacent(minesweeper)
|
||||
if target.x >= 0:
|
||||
main.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
|
||||
main.ui.battle.board_zone.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
|
||||
_seals_placed.append(target)
|
||||
_ap_spent_this_round += 1
|
||||
return
|
||||
@@ -937,19 +937,19 @@ func _play_one_action() -> void:
|
||||
if _rand.randf() < 0.1 and not _seals_placed.is_empty():
|
||||
var fc: Vector2i = _seals_placed.pop_back()
|
||||
if minesweeper.has_seal(fc) and not minesweeper.is_revealed(fc):
|
||||
main.board_view.click_cell(fc, MOUSE_BUTTON_RIGHT)
|
||||
main.ui.battle.board_zone.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, minesweeper.current_board().width - 1), _rand.randi_range(0, minesweeper.current_board().height - 1)), MOUSE_BUTTON_LEFT)
|
||||
main.ui.battle.board_zone.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(minesweeper) # O4 收口:策略单一实现
|
||||
if not plan.is_empty():
|
||||
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)
|
||||
main.ui.battle.board_zone.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
|
||||
main.ui.battle.board_zone.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 settlement.action_economy.phase == "Active" and _rand.randf() < 0.15:
|
||||
@@ -975,7 +975,7 @@ func _random_unknown_adjacent(minesweeper) -> Vector2i:
|
||||
|
||||
func _click_stop() -> void:
|
||||
# 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
|
||||
main.ap_bar._btn_stop.pressed.emit()
|
||||
main.ui.battle.board_zone.ap_bar._btn_stop.pressed.emit()
|
||||
|
||||
|
||||
# ---- T1 D1:锁期/重放断言(每 run 一次,脚本化场景——演播期豁免的断言化承载) ----
|
||||
@@ -992,7 +992,7 @@ func _locktest_tick() -> void:
|
||||
if settlement == null or settlement.minesweeper.current_board() == null:
|
||||
return
|
||||
if _locktest_state == 0:
|
||||
if main._row_queue.is_empty() or main._input_locked():
|
||||
if main.ui.screens.row_pacer.pending_rows().is_empty() or main.ui.modals.flow.locks_input():
|
||||
return
|
||||
if String(settlement.battle_result) != "" or String(settlement.action_economy.phase) != "Active":
|
||||
return
|
||||
@@ -1003,7 +1003,7 @@ func _locktest_tick() -> void:
|
||||
return # 无合法邻格/目标被并发封印行为占位——下帧重挑
|
||||
_locktest_cell = target
|
||||
_locktest_frames = 0
|
||||
main.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
|
||||
main.ui.battle.board_zone.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
|
||||
if settlement.minesweeper.has_seal(target):
|
||||
_problems.append("锁期断言:演播期点击未被冻结(封印即时受理——v3 冻结失守)")
|
||||
_locktest_state = 2
|
||||
@@ -1014,11 +1014,11 @@ func _locktest_tick() -> void:
|
||||
return
|
||||
_locktest_state = 1
|
||||
elif _locktest_state == 1:
|
||||
if main.curio_overlay.visible or main._inter_overlay.visible or main._end_overlay.is_open() \
|
||||
if main.ui.modals.curio_overlay.visible or main.ui.screens.inter_overlay.visible or main.ui.modals.end_overlay.is_open() \
|
||||
or String(settlement.battle_result) != "":
|
||||
_locktest_state = 2 # 前提消失守卫:终结演播竞态——静默弃测
|
||||
return
|
||||
if not main._row_queue.is_empty():
|
||||
if not main.ui.screens.row_pacer.pending_rows().is_empty():
|
||||
_locktest_frames = 0 # 演播未完——重放耐心只在排空后起算(链式大演播数百帧不占预算)
|
||||
return
|
||||
_locktest_frames += 1
|
||||
@@ -1038,7 +1038,7 @@ func _locktest_tick() -> void:
|
||||
b.width, b.height, str(settlement.minesweeper.hero_pos())])
|
||||
_problems.append("重放断言:锁期缓存点击未受理恰一次(封印未置位——零发或双发)")
|
||||
_locktest_state = 2
|
||||
main.board_view.click_cell(_locktest_cell, MOUSE_BUTTON_RIGHT) # 撤封印恢复现场(尽力而为不判定)
|
||||
main.ui.battle.board_zone.board_view.click_cell(_locktest_cell, MOUSE_BUTTON_RIGHT) # 撤封印恢复现场(尽力而为不判定)
|
||||
|
||||
|
||||
func _locktest_pick_cell(minesweeper) -> Vector2i:
|
||||
|
||||
@@ -31,15 +31,15 @@ func _ready() -> void:
|
||||
# 同 ui_click_probe 隔离纪律。首次冒险布尔位置位=对齐真实档游玩史口径,模板抽签路径不变;
|
||||
# isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_input_probe_save", true, false, true)
|
||||
main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,探针确定性禁依赖默认分支)
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,探针确定性禁依赖默认分支)
|
||||
main._start_adventure_from_menu()
|
||||
await get_tree().process_frame
|
||||
# ---- ⑫ 图上命中真实输入(回归闸):入口停点候选节点中心
|
||||
# push_input 真实鼠标路径 motion+click → 悬停预承诺置位 → route_chosen 到达组合根。
|
||||
# 由来=真机 bug:深层节点(L1 Boss 内容 x=802)在固定画布命中区外,press_candidate 直调
|
||||
# 命中函数探针全绿、真鼠标点不中——本 case 封住输入管线,⑬封住几何拟合。
|
||||
if main.level_screen.visible:
|
||||
var ro = main.level_screen
|
||||
if main.ui.modals.level_screen.visible:
|
||||
var ro = main.ui.modals.level_screen
|
||||
var cands: Array = main.run_scope.runner.next_candidate_ids()
|
||||
# ---- ⑱ 信息迷雾呈现接线(回归闸):真机 display_probe 读实际绘制记录(雾下节点类型未画、
|
||||
# Boss 描边画了、候选类型画了)+点击雾下非候选节点零提交(雾不扩大可点域——候选集即合法集)----
|
||||
@@ -90,9 +90,9 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
ro.route_chosen.disconnect(on_route)
|
||||
if not submitted.is_empty() or not main.level_screen.visible or not main.run_scope.runner.awaiting_choice():
|
||||
if not submitted.is_empty() or not main.ui.modals.level_screen.visible or not main.run_scope.runner.awaiting_choice():
|
||||
_pre_fail("case⑱ 雾下节点点击产生提交(提交=%s visible=%s 选路态=%s——雾不扩大可点域)〔fv9〕"
|
||||
% [str(submitted), str(main.level_screen.visible), str(main.run_scope.runner.awaiting_choice())])
|
||||
% [str(submitted), str(main.ui.modals.level_screen.visible), str(main.run_scope.runner.awaiting_choice())])
|
||||
return
|
||||
var nds: Array = main.run_scope.runner.nodes()
|
||||
var tgt := {}
|
||||
@@ -116,13 +116,13 @@ func _ready() -> void:
|
||||
main.get_viewport().push_input(evc, true)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if main.level_screen.visible or main.run_scope.runner.awaiting_choice():
|
||||
_pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.level_screen.visible), str(main.run_scope.runner.awaiting_choice())])
|
||||
if main.ui.modals.level_screen.visible or main.run_scope.runner.awaiting_choice():
|
||||
_pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.ui.modals.level_screen.visible), str(main.run_scope.runner.awaiting_choice())])
|
||||
for i in range(3):
|
||||
await get_tree().process_frame
|
||||
# ---- ⑬ 画布随图拟合(同 bug 的几何面回归):复刻分层公式独立推导最深节点内容 x,
|
||||
# 断言画布 min 尺寸覆盖之(open_choices 每 stop 拟合——L1 单链 Boss x=70+6*122=802 曾越界)
|
||||
var ro2 = main.level_screen
|
||||
var ro2 = main.ui.modals.level_screen
|
||||
var nds2: Array = main.run_scope.runner.nodes()
|
||||
var layer2 := {}
|
||||
var inbound2 := {}
|
||||
@@ -155,7 +155,7 @@ func _ready() -> void:
|
||||
var cvmin: Dictionary = ro2.canvas_probe()
|
||||
if float(cvmin["min_size"].x) < need_x2 - 0.5:
|
||||
_pre_fail("画布拟合不足(min.x=%.0f < 最深内容 %.0f——深层节点不可点回归)" % [cvmin["min_size"].x, need_x2])
|
||||
var bv = main.board_view
|
||||
var bv = main.ui.battle.board_zone.board_view
|
||||
var ms = main.run_scope.settlement.minesweeper if main.run_scope.settlement != null else null
|
||||
if ms == null or bv.cells_probe().is_empty():
|
||||
_pre_fail("战斗未就绪(cells=%d settlement=%s)" % [bv.cells_probe().size(), str(main.run_scope.settlement)])
|
||||
@@ -231,8 +231,8 @@ func _ready() -> void:
|
||||
if bv.hover_probe() != seal_t:
|
||||
_problems.append("case① 悬停未到达(_hover=%s 应=%s)" % [bv.hover_probe(), seal_t])
|
||||
var u1 = bv.cells_probe()[seal_t]
|
||||
if not u1.st.hl_visible or u1.st.hl_color != UiKit.GOLD:
|
||||
_problems.append("case① 悬停金框缺失(visible=%s color=%s)" % [str(u1.st.hl_visible), str(u1.st.hl_color)])
|
||||
if not u1.face.hl_visible or u1.face.hl_color != UiKit.GOLD:
|
||||
_problems.append("case① 悬停金框缺失(visible=%s color=%s)" % [str(u1.face.hl_visible), str(u1.face.hl_color)])
|
||||
if bv.mouse_default_cursor_shape != Input.CURSOR_CROSS:
|
||||
_problems.append("case① 挖掘光标未切十字(%d)" % bv.mouse_default_cursor_shape)
|
||||
# ---- case② 悬停禁入:hover_forbidden 信号 + 灰禁红框 ----
|
||||
@@ -240,14 +240,14 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
if _forbids.is_empty() or _forbids[_forbids.size() - 1] != forbid_t:
|
||||
_problems.append("case② hover_forbidden 未达(%s)" % [str(_forbids)])
|
||||
if bv.cells_probe()[forbid_t].st.hl_color != UiKit.RED_FORBID:
|
||||
_problems.append("case② 禁入红框缺失(color=%s)" % str(bv.cells_probe()[forbid_t].st.hl_color))
|
||||
if bv.cells_probe()[forbid_t].face.hl_color != UiKit.RED_FORBID:
|
||||
_problems.append("case② 禁入红框缺失(color=%s)" % str(bv.cells_probe()[forbid_t].face.hl_color))
|
||||
# ---- case③ 悬停已开格:路径预览点迹 + 移动光标 ----
|
||||
_push_motion(_pos(bv, reveal_t))
|
||||
await get_tree().process_frame
|
||||
var dots := 0
|
||||
for c in bv.cells_probe():
|
||||
if bv.cells_probe()[c].st.dot_visible:
|
||||
if bv.cells_probe()[c].face.dot_visible:
|
||||
dots += 1
|
||||
if dots < 1:
|
||||
_problems.append("case③ 路径预览点迹未亮(dots=0)")
|
||||
@@ -266,8 +266,8 @@ func _ready() -> void:
|
||||
# 掩盖)——有盘断言纹理在槽、缺盘断言字形回退,两分支防双写踩踏复活 ----
|
||||
var seal_unit = bv.cells_probe().get(seal_t)
|
||||
if bv._palette.seal_tex != null:
|
||||
if seal_unit == null or seal_unit.st.tex_seal != bv._palette.seal_tex:
|
||||
_problems.append("case④b 封印纹理槽未挂封印印记纹理(tex_seal=%s)" % ["无单元" if seal_unit == null else str(seal_unit.st.tex_seal)])
|
||||
if seal_unit == null or seal_unit.face.tex_seal != bv._palette.seal_tex:
|
||||
_problems.append("case④b 封印纹理槽未挂封印印记纹理(tex_seal=%s)" % ["无单元" if seal_unit == null else str(seal_unit.face.tex_seal)])
|
||||
elif seal_unit == null or seal_unit.glyph != "封":
|
||||
_problems.append("case④b 缺盘字形回退缺失(glyph=%s)" % ["无单元" if seal_unit == null else String(seal_unit.glyph)])
|
||||
# ---- case⑤ 双击 chord:禁入格双左击(核心拒挖零副作用)→ chord_attempted ----
|
||||
@@ -371,12 +371,12 @@ func _ready() -> void:
|
||||
if mark_t.x >= 0:
|
||||
bv.set_variant_mark_resolver(func(_c): return "flat_damage")
|
||||
bv.refresh()
|
||||
if String(bv.cells_probe()[mark_t].st.mutation_mark) != "flat_damage":
|
||||
_problems.append("case⑭ 变异角标未落位(mutation_mark=%s)" % str(bv.cells_probe()[mark_t].st.mutation_mark))
|
||||
if String(bv.cells_probe()[mark_t].face.mutation_mark) != "flat_damage":
|
||||
_problems.append("case⑭ 变异角标未落位(mutation_mark=%s)" % str(bv.cells_probe()[mark_t].face.mutation_mark))
|
||||
bv.set_variant_mark_resolver(Callable())
|
||||
bv.refresh()
|
||||
if String(bv.cells_probe()[mark_t].st.mutation_mark) != "":
|
||||
_problems.append("case⑭ 角标未随 resolver 复位(mutation_mark=%s)" % str(bv.cells_probe()[mark_t].st.mutation_mark))
|
||||
if String(bv.cells_probe()[mark_t].face.mutation_mark) != "":
|
||||
_problems.append("case⑭ 角标未随 resolver 复位(mutation_mark=%s)" % str(bv.cells_probe()[mark_t].face.mutation_mark))
|
||||
else:
|
||||
_problems.append("case⑭ 无未开格可验(板面异常)")
|
||||
# ---- case⑮ 视野迷雾(三态呈态/半径圈/移动跟随/开关两态零扰动/提灯插值位;
|
||||
@@ -479,13 +479,13 @@ func _ready() -> void:
|
||||
var p_lantern: Dictionary = main.build_adventure_params(main.save_manager, main.meta)
|
||||
if not (p_lantern.get("attunement", []) as Array).has("wide_scout_band"):
|
||||
_problems.append("case⑮ 装配未进开局参数面(attunement=%s 应含 wide_scout_band)" % str(p_lantern.get("attunement", [])))
|
||||
main.run_scope.settlement.attunement = ["wide_scout_band"]
|
||||
main.run_scope.settlement._facts.attunement = ["wide_scout_band"]
|
||||
main._sync_treasure_glow()
|
||||
if int(bv.fog_params_probe()["lantern"]) != 4:
|
||||
_problems.append("case⑮ 装配后视野未扩(lantern 应=4——呈现桥未生效)")
|
||||
if int(bv.cells_probe()[ring_cell].fog_state) != 0:
|
||||
_problems.append("case⑮ 装配后 dist==R+1 格仍被遮(应转可见)")
|
||||
main.run_scope.settlement.attunement = []
|
||||
main.run_scope.settlement._facts.attunement = []
|
||||
main._sync_treasure_glow()
|
||||
if int(bv.fog_params_probe()["lantern"]) != 0:
|
||||
_problems.append("case⑮ 卸装后视野未复原(lantern 应=0——契约复原语义显式重算)")
|
||||
@@ -517,29 +517,29 @@ func _ready() -> void:
|
||||
if status_dig.x == -9:
|
||||
_problems.append("case⑯ 无可挖无封印格(板面异常——锁断言中断)")
|
||||
else:
|
||||
var pbtn16: Button = main.board_zone.status_badge
|
||||
var pbtn16: Button = main.ui.battle.board_zone.status_badge
|
||||
var bpos: Vector2 = pbtn16.get_global_rect().get_center()
|
||||
_push_click(bpos, MOUSE_BUTTON_LEFT)
|
||||
_push_release(bpos, MOUSE_BUTTON_LEFT) # Button pressed 信号走 release 沿——按压+释放成对
|
||||
await get_tree().process_frame
|
||||
if not main.status_panel.visible or not main._input_locked():
|
||||
if not main.ui.modals.status_panel.visible or not main.ui.modals.flow.locks_input():
|
||||
_problems.append("case⑯ 角标真点击未开状态面板(visible=%s locked=%s——入口可达性失守)"
|
||||
% [str(main.status_panel.visible), str(main._input_locked())])
|
||||
% [str(main.ui.modals.status_panel.visible), str(main.ui.modals.flow.locks_input())])
|
||||
else:
|
||||
_push_click(_pos(bv, status_dig), MOUSE_BUTTON_LEFT)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if _has_action(status_dig, "dig") or ms.is_revealed(status_dig):
|
||||
_problems.append("case⑯ 状态面板开启时挖掘穿透(R3 输入锁破——目标=%s)" % status_dig)
|
||||
var resume_btn: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
|
||||
var resume_btn: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix")
|
||||
if resume_btn == null:
|
||||
_problems.append("case⑯ 状态面板关闭钮缺失")
|
||||
else:
|
||||
resume_btn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
if main.status_panel.visible or main._input_locked():
|
||||
if main.ui.modals.status_panel.visible or main.ui.modals.flow.locks_input():
|
||||
_problems.append("case⑯ 关闭后未恢复(visible=%s locked=%s——R3 残留)"
|
||||
% [str(main.status_panel.visible), str(main._input_locked())])
|
||||
% [str(main.ui.modals.status_panel.visible), str(main.ui.modals.flow.locks_input())])
|
||||
_push_click(_pos(bv, status_dig), MOUSE_BUTTON_LEFT)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
|
||||
+51
-51
@@ -16,7 +16,7 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
var problems: Array = []
|
||||
# ---- 放弃入口(台账 119):默认隐藏(非冒险模式无冒险可放弃——开始层/调试两层)----
|
||||
if main.level_screen.abandon_btn == null or main.level_screen.abandon_btn.visible:
|
||||
if main.ui.modals.level_screen.abandon_btn == null or main.ui.modals.level_screen.abandon_btn.visible:
|
||||
print("[ui_probe] FAIL — 放弃按钮默认应隐藏(非冒险模式关)")
|
||||
get_tree().quit(1)
|
||||
return
|
||||
@@ -28,99 +28,99 @@ func _ready() -> void:
|
||||
problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)")
|
||||
else:
|
||||
await get_tree().process_frame
|
||||
if not main.camp_overlay.visible:
|
||||
if not main.ui.modals.camp_overlay.visible:
|
||||
problems.append("营地层未打开(进档开层新路径)")
|
||||
elif main._start_overlay.visible:
|
||||
elif main.ui.screens.start_overlay.visible:
|
||||
problems.append("营地层打开时开始层未隐藏(规范 2.3 遮挡纪律)")
|
||||
else:
|
||||
if not String(main.camp_overlay._balance_label.text).begins_with("钥石"):
|
||||
problems.append("营地余额文案缺失(%s)" % main.camp_overlay._balance_label.text)
|
||||
if main.camp_overlay._tree_buy_btns.is_empty():
|
||||
if not String(main.ui.modals.camp_overlay._balance_label.text).begins_with("钥石"):
|
||||
problems.append("营地余额文案缺失(%s)" % main.ui.modals.camp_overlay._balance_label.text)
|
||||
if main.ui.modals.camp_overlay._tree_buy_btns.is_empty():
|
||||
problems.append("树淬炼柜台购买钮缺失(四柜台重排后树=主界面)")
|
||||
else:
|
||||
var btn_by_id := {}
|
||||
for card2 in main.camp_overlay._tree_buy_btns:
|
||||
for card2 in main.ui.modals.camp_overlay._tree_buy_btns:
|
||||
btn_by_id[String(card2.node_id)] = card2.btn
|
||||
if not btn_by_id.has("sp_hp") or bool(btn_by_id["sp_hp"].disabled):
|
||||
problems.append("脊柱生命 L1 白板可购钮未启用(无门槛——can_buy 不含余额面语义)")
|
||||
if not btn_by_id.has("att_socket2") or not bool(btn_by_id["att_socket2"].disabled):
|
||||
problems.append("同调位 2 前置未亮应禁用(点亮前置语义)")
|
||||
if main.camp_overlay._workshop_rows.size() != 12:
|
||||
problems.append("秘宝匣柜台行数错误(%d ≠ 12——迭代 22 秘宝批换代)" % main.camp_overlay._workshop_rows.size())
|
||||
if main.ui.modals.camp_overlay._workshop_rows.size() != 12:
|
||||
problems.append("秘宝匣柜台行数错误(%d ≠ 12——迭代 22 秘宝批换代)" % main.ui.modals.camp_overlay._workshop_rows.size())
|
||||
main._on_camp_closed()
|
||||
await get_tree().process_frame
|
||||
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
|
||||
await get_tree().process_frame
|
||||
if main.camp_overlay.visible or not main._start_overlay.visible:
|
||||
if main.ui.modals.camp_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
problems.append("营地关闭后未恢复开始层(开合往返失守)")
|
||||
# 换回全新零档(营地段购买态不泄入后续冒险段——确定性;先删后建防陈旧档泄入)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", true, false, true)
|
||||
main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机;本探针 start_single 第五路同吃此框,确定性禁依赖默认分支)
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机;本探针 start_single 第五路同吃此框,确定性禁依赖默认分支)
|
||||
main._start_adventure_from_menu()
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
# ---- 入口选路停点(关卡开始即出节点图——level_started 后、首战前弹选路层;
|
||||
# press_candidate 图上命中路径 pick 入口单候选进首战)----
|
||||
# ---- 独立性断言(独立屏激活期战斗屏宿主整体隐藏,零透出)----
|
||||
if not main.level_screen.visible:
|
||||
if not main.ui.modals.level_screen.visible:
|
||||
problems.append("关卡开始未弹入口选路停点(level_screen 应可见)")
|
||||
elif main._battle_root.visible:
|
||||
elif main.ui.screens.battle_root.visible:
|
||||
problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)")
|
||||
elif main.run_scope.runner == null or not main.run_scope.runner.awaiting_choice() \
|
||||
or main.run_scope.runner.next_candidate_ids().size() != 1:
|
||||
problems.append("入口停点状态失守(选路态 + 入口单候选)")
|
||||
else:
|
||||
main.level_screen.press_candidate(0)
|
||||
main.ui.modals.level_screen.press_candidate(0)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if not main._battle_root.visible or main.level_screen.visible:
|
||||
if not main.ui.screens.battle_root.visible or main.ui.modals.level_screen.visible:
|
||||
problems.append("选路提交战斗节点后未切回战斗屏(屏级互斥恢复失守)")
|
||||
# ---- 放弃入口·冒险模式可见 + 战斗中按下被拒(真实按钮路径——入口挂选路停点,战斗中拒绝为防御分支)----
|
||||
if main.level_screen.abandon_btn == null or not main.level_screen.abandon_btn.visible:
|
||||
if main.ui.modals.level_screen.abandon_btn == null or not main.ui.modals.level_screen.abandon_btn.visible:
|
||||
problems.append("冒险模式放弃按钮未开放(选路停点入口在案)")
|
||||
else:
|
||||
main.level_screen.abandon_btn.pressed.emit()
|
||||
main.ui.modals.level_screen.abandon_btn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
if main.run_scope.adventure == null or String(main.run_scope.adventure.state) != "running":
|
||||
problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)")
|
||||
# ---- 既有断言(回归基线;A 区无进度条——节点链进度由关卡画面独立屏承载)----
|
||||
if main.board_view.cells_probe().is_empty():
|
||||
if main.ui.battle.board_zone.board_view.cells_probe().is_empty():
|
||||
problems.append("棋盘格未生成(cells=0)")
|
||||
if main.board_view.board() == null:
|
||||
if main.ui.battle.board_zone.board_view.board() == null:
|
||||
problems.append("board 未装载")
|
||||
if main.info_panel.attack_text().contains("—") or main.info_panel.armor_text().contains("—"):
|
||||
problems.append("C 区攻击/护甲未显示(%s | %s)" % [main.info_panel.attack_text(), main.info_panel.armor_text()])
|
||||
if main.ui.battle.info_panel.attack_text().contains("—") or main.ui.battle.info_panel.armor_text().contains("—"):
|
||||
problems.append("C 区攻击/护甲未显示(%s | %s)" % [main.ui.battle.info_panel.attack_text(), main.ui.battle.info_panel.armor_text()])
|
||||
if main.run_scope.settlement == null or main.run_scope.settlement.battle_result != "":
|
||||
problems.append("首战未处于进行中")
|
||||
if not main.log_view.parsed_text().contains("节点 1/"):
|
||||
if not main.ui.battle.log_view.parsed_text().contains("节点 1/"):
|
||||
problems.append("节点头日志缺失")
|
||||
# ---- 四区布局断言(battle.md §Layout)----
|
||||
if not main.stage.round_label.text.contains("第 1 轮"):
|
||||
problems.append("B 区轮次·阶段指示缺失(%s)" % main.stage.round_label.text)
|
||||
if main.stage.enemy_box.get_child_count() < 1:
|
||||
problems.append("B 区敌方站桩未构建(%d 个)" % main.stage.enemy_box.get_child_count())
|
||||
if main.ap_bar._dots.get_child_count() != int(main.db.run.action_economy.ap_per_round):
|
||||
problems.append("D 区 AP 点阵数量错误(%d)" % main.ap_bar._dots.get_child_count())
|
||||
if main.ap_bar.refresh_remaining_text() == "":
|
||||
if not main.ui.battle.stage.round_label.text.contains("第 1 轮"):
|
||||
problems.append("B 区轮次·阶段指示缺失(%s)" % main.ui.battle.stage.round_label.text)
|
||||
if main.ui.battle.stage.enemy_box.get_child_count() < 1:
|
||||
problems.append("B 区敌方站桩未构建(%d 个)" % main.ui.battle.stage.enemy_box.get_child_count())
|
||||
if main.ui.battle.board_zone.ap_bar._dots.get_child_count() != int(main.db.run.action_economy.ap_per_round):
|
||||
problems.append("D 区 AP 点阵数量错误(%d)" % main.ui.battle.board_zone.ap_bar._dots.get_child_count())
|
||||
if main.ui.battle.board_zone.ap_bar.refresh_remaining_text() == "":
|
||||
problems.append("D 区弃图刷新按钮文案缺失")
|
||||
if main.board_zone.toast == null:
|
||||
if main.ui.battle.board_zone.toast == null:
|
||||
problems.append("D 区 toast 槽缺失")
|
||||
if main.info_panel.item_slots == null or main.info_panel.item_slots.get_child_count() != 4:
|
||||
if main.ui.battle.info_panel.item_slots == null or main.ui.battle.info_panel.item_slots.get_child_count() != 4:
|
||||
problems.append("C 区秘宝槽(预留)缺失")
|
||||
if main.stage.hero_standee.slot_row == null:
|
||||
if main.ui.battle.stage.hero_standee.slot_row == null:
|
||||
problems.append("B 区站桩状态行缺失(状态效果唯一展示面——2026-09-26 C 区精简批)")
|
||||
# B 区明细带挖掘期收起断言(T17 视觉扩展批适配):结算演播期 settling 视觉延伸=mode=settle
|
||||
# 属新语义(演播放完回 dig——main._is_settling 排播项,判据=纯队列),本断言仅在排播空闲态判定。
|
||||
if main._row_queue.is_empty() \
|
||||
and (main.log_view.mode() != "dig" or main.log_view.custom_minimum_size.y != 24.0):
|
||||
problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.log_view.mode(), str(main.log_view.custom_minimum_size.y)])
|
||||
if main.ui.screens.row_pacer.pending_rows().is_empty() \
|
||||
and (main.ui.battle.log_view.mode() != "dig" or main.ui.battle.log_view.custom_minimum_size.y != 24.0):
|
||||
problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.ui.battle.log_view.mode(), str(main.ui.battle.log_view.custom_minimum_size.y)])
|
||||
# E 区无道具面板——四区构建由其余断言覆盖
|
||||
|
||||
# ---- 敌方站桩投影(顺位徽标/意图三档动作动画占位——敌人系统×4,全站桩)----
|
||||
# B 区站桩子件伴随件化(壳层 tooltip 载体)——断言口径见台账 171:
|
||||
# 伴随件化(壳层 tooltip 载体),穿透明(子件 _name_label/_armor_icon/_intent_badge)改本体
|
||||
# 探针出口 stage.enemy_body_at(i).name_line_probe()/intent_probe()——呈现口径逐字保持
|
||||
var standee = main.stage.enemy_body_at(0)
|
||||
var standee = main.ui.battle.stage.enemy_body_at(0)
|
||||
if standee == null:
|
||||
problems.append("敌立绘本体缺失(enemy_body_at(0) 无世界站桩)")
|
||||
else:
|
||||
@@ -138,12 +138,12 @@ func _ready() -> void:
|
||||
# 站桩纹理非空断言(八敌真图落位;缺位回退链保持的对照面)
|
||||
if not bool(standee.portrait_probe().get("has_tex", false)):
|
||||
problems.append("敌立绘纹理未落位(素材管线 48px 档——迭代 6 T2;缺位回退链不应在此态触发)")
|
||||
if main.stage.hero_standee == null or not main.stage.hero_standee.has_world_hp_bar:
|
||||
if main.ui.battle.stage.hero_standee == null or not main.ui.battle.stage.hero_standee.has_world_hp_bar:
|
||||
problems.append("B 区主角站桩缺失(世界血条面缺失)")
|
||||
elif not bool(main.stage.hero_standee.portrait_probe().get("has_tex", false)):
|
||||
elif not bool(main.ui.battle.stage.hero_standee.portrait_probe().get("has_tex", false)):
|
||||
problems.append("主角立绘纹理未落位(48px 档——迭代 6 T2/T3)")
|
||||
# ---- BUG-0001 回归(tooltip 入树后 set_effects 槽位不错位——结算软锁修复沉淀)----
|
||||
var slots_row = main.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源)
|
||||
var slots_row = main.ui.battle.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源)
|
||||
var probe_eff: Array = [{"effect_id": "attack_up", "display_name": "增伤", "layers": 1,
|
||||
"remaining_settlements": 1, "source_class": "safe_cell", "round_index": 1, "effective_value": 50}]
|
||||
slots_row.set_effects(probe_eff)
|
||||
@@ -196,12 +196,12 @@ func _ready() -> void:
|
||||
main._debug.start_single(2)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if main.board_view.board() == null:
|
||||
if main.ui.battle.board_zone.board_view.board() == null:
|
||||
problems.append("L2 单场未启动(board=null)")
|
||||
else:
|
||||
# 地形变异:L2 板允许带变异(岩浆/烬覆)——布置域断言 =
|
||||
# 变异格全在初始展开区外(closed_only 结构保障)
|
||||
var b0 = main.board_view.board()
|
||||
var b0 = main.ui.battle.board_zone.board_view.board()
|
||||
var r0p := {}
|
||||
for c in b0.compute_r0():
|
||||
r0p[c] = true
|
||||
@@ -209,8 +209,8 @@ func _ready() -> void:
|
||||
if r0p.has(c):
|
||||
problems.append("L2 板变异格落在初始展开区(地形变异 布置域违例)")
|
||||
break
|
||||
main.board_view.set_desaturated(true)
|
||||
main.board_view.set_desaturated(false)
|
||||
main.ui.battle.board_zone.board_view.set_desaturated(true)
|
||||
main.ui.battle.board_zone.board_view.set_desaturated(false)
|
||||
# ---- GP 终局行:summary gp_final 键取数+胜利面板 GP 行呈现(C5:GP 行只上冒险终局面板;
|
||||
# 断言面 = summary 键,无影子账清零转存)----
|
||||
# (置于战斗态断言之后——合成收束会灰化站桩并清 settlement 引用,具污染性)
|
||||
@@ -223,17 +223,17 @@ func _ready() -> void:
|
||||
main._on_adventure_finished({"result": "completed", "cleared": true, "levels_cleared": 3,
|
||||
"stopped_at": 0, "keystone_total": 30, "master_seed": 42, "gp_final": 57})
|
||||
main.meta.save = meta_saved
|
||||
if not main._end_overlay.is_open():
|
||||
if not main.ui.modals.end_overlay.is_open():
|
||||
problems.append("冒险终局面板未开(合成摘要路径)")
|
||||
elif not main._end_overlay._gp_row.visible or not main._end_overlay._gp_val.text.contains("57"):
|
||||
problems.append("终局面板 GP 行缺失或取数不对(%s)" % main._end_overlay._gp_val.text)
|
||||
main._end_overlay.close()
|
||||
main._close_modal(main._end_overlay)
|
||||
elif not main.ui.modals.end_overlay._gp_row.visible or not main.ui.modals.end_overlay._gp_val.text.contains("57"):
|
||||
problems.append("终局面板 GP 行缺失或取数不对(%s)" % main.ui.modals.end_overlay._gp_val.text)
|
||||
main.ui.modals.end_overlay.close()
|
||||
main.ui.modals.flow.close(main.ui.modals.end_overlay)
|
||||
if problems.is_empty():
|
||||
print("[ui_probe] PASS — cells=%d board=%dx%d hp='%s' enemies=%d ap_dots=%d" % [
|
||||
main.board_view.cells_probe().size(), main.board_view.board().width, main.board_view.board().height,
|
||||
main.info_panel.attack_text(),
|
||||
main.stage.enemy_box.get_child_count(), main.ap_bar._dots.get_child_count()])
|
||||
main.ui.battle.board_zone.board_view.cells_probe().size(), main.ui.battle.board_zone.board_view.board().width, main.ui.battle.board_zone.board_view.board().height,
|
||||
main.ui.battle.info_panel.attack_text(),
|
||||
main.ui.battle.stage.enemy_box.get_child_count(), main.ui.battle.board_zone.ap_bar._dots.get_child_count()])
|
||||
get_tree().quit(0)
|
||||
else:
|
||||
for p in problems:
|
||||
|
||||
@@ -28,13 +28,13 @@ func _ready() -> void:
|
||||
# ---- 菜单簇(开始层可见态;逐屏开→采样→关,模态恢复走组合根真实路径)----
|
||||
ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_menu", true, false, true)
|
||||
_settle()
|
||||
await _audit("start", [main._start_overlay])
|
||||
await _audit("start", [main.ui.screens.start_overlay])
|
||||
_start_columns_contract(main)
|
||||
_modal_registry_contract(main)
|
||||
|
||||
main._open_settings()
|
||||
_settle()
|
||||
await _audit("settings", [main.settings_overlay])
|
||||
await _audit("settings", [main.ui.modals.settings_overlay])
|
||||
main._on_settings_closed()
|
||||
_settle()
|
||||
|
||||
@@ -42,7 +42,7 @@ func _ready() -> void:
|
||||
problems.append("[camp] 营地入口缺失(选档层新档钮——mn9 新路径驱动中断)")
|
||||
else:
|
||||
_settle()
|
||||
await _audit("camp", [main.camp_overlay])
|
||||
await _audit("camp", [main.ui.modals.camp_overlay])
|
||||
# 营地灰字源 token 扫描(迭代36 wc4 静态半——源内零裸 UiKit.STONE;判别式=
|
||||
# STONE 出现次数 == STONE_DEEP 出现次数,即每处 STONE 都带 _DEEP 后缀)
|
||||
var camp_src := FileAccess.get_file_as_string("res://presentation/views/camp_overlay_view.gd")
|
||||
@@ -59,15 +59,15 @@ func _ready() -> void:
|
||||
# 生涯页驱动=函数直调中间态(卡② mn9/mn10——careerbtn 已撤,卡④ 营地笔记本改挂前无 UI 路径)
|
||||
main._open_career()
|
||||
_settle()
|
||||
if not main.career_overlay.visible or main._start_overlay.visible:
|
||||
if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible:
|
||||
problems.append("[career] 生涯页未打开或开始层未隐藏(函数直调中间态断裂)")
|
||||
await _audit("career", [main.career_overlay])
|
||||
await _audit("career", [main.ui.modals.career_overlay])
|
||||
_career_readonly_contract(main)
|
||||
_career_zero_motion_contract(main)
|
||||
_career_nojump_contract(main)
|
||||
main._on_career_closed()
|
||||
_settle()
|
||||
if main.career_overlay.visible or not main._start_overlay.visible:
|
||||
if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
problems.append("[career] 关闭后页未隐藏或开始层未恢复(模态栈单口失守)")
|
||||
|
||||
# ---- 选档界面(迭代 33 卡③):函数直调开层(「进入存档」钮归卡② mn3——点击链在卡② 承接),
|
||||
@@ -93,39 +93,39 @@ func _ready() -> void:
|
||||
problems.append("[savesel] 开层前激活态非零(sl9 重启链——boot/注入面后应无激活)")
|
||||
main._open_save_select()
|
||||
_settle()
|
||||
if not main.save_select_overlay.visible or main._start_overlay.visible:
|
||||
if not main.ui.modals.save_select_overlay.visible or main.ui.screens.start_overlay.visible:
|
||||
problems.append("[savesel] 选档层未打开或开始层未隐藏(开层函数断裂)")
|
||||
await _audit("savesel", [main.save_select_overlay])
|
||||
await _audit("savesel", [main.ui.modals.save_select_overlay])
|
||||
_save_select_contract(main, ssel_tpl)
|
||||
main._on_save_select_closed()
|
||||
_settle()
|
||||
if main.save_select_overlay.visible or not main._start_overlay.visible:
|
||||
if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)")
|
||||
|
||||
main.confirm_dialog.open("结构探针采样", "确认")
|
||||
main.ui.modals.confirm_dialog.open("结构探针采样", "确认")
|
||||
_settle()
|
||||
await _audit("confirm", [main.confirm_dialog])
|
||||
main.confirm_dialog.close()
|
||||
await _audit("confirm", [main.ui.modals.confirm_dialog])
|
||||
main.ui.modals.confirm_dialog.close()
|
||||
_settle()
|
||||
|
||||
main._show_debug_overlay()
|
||||
_settle()
|
||||
await _audit("debug", [main.debug_overlay])
|
||||
await _audit("debug", [main.ui.modals.debug_overlay])
|
||||
main._close_debug_overlay()
|
||||
_settle()
|
||||
|
||||
# ---- 冒险簇(真流推进:开冒险→地图选路态→选战斗节点→战斗 HUD)----
|
||||
ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_adv", true, false, true)
|
||||
main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,结构采样确定性禁依赖默认分支)
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,结构采样确定性禁依赖默认分支)
|
||||
main._start_adventure_from_menu()
|
||||
_settle(4)
|
||||
await _audit("level_map", [main.level_screen])
|
||||
await _audit("level_map", [main.ui.modals.level_screen])
|
||||
|
||||
var battle_id := _pick_battle_node(main)
|
||||
if battle_id >= 0:
|
||||
main.level_screen.route_chosen.emit(battle_id)
|
||||
main.ui.modals.level_screen.route_chosen.emit(battle_id)
|
||||
_settle(6)
|
||||
_audit("battle_hud", [main.board_zone, main.info_panel, main.stage])
|
||||
_audit("battle_hud", [main.ui.battle.board_zone, main.ui.battle.info_panel, main.ui.battle.stage])
|
||||
_standeefit_contract(main)
|
||||
# ---- 状态面板:组合根真实路径开层→结构断言→关闭。附只读性契约
|
||||
#(R4——子树信号连接白名单巡检)与零动效契约(R5/P4 甲——零 AnimationPlayer/零 tween)。
|
||||
@@ -133,74 +133,74 @@ func _ready() -> void:
|
||||
# 一律红,须先入册审查);白名单={视图自身 _emit_closed, 组合根 _on_status_closed}。
|
||||
main._open_status_panel()
|
||||
_settle()
|
||||
await _audit("status", [main.status_panel])
|
||||
await _audit("status", [main.ui.modals.status_panel])
|
||||
_status_readonly_contract(main)
|
||||
_status_zero_motion_contract(main)
|
||||
main._on_status_closed()
|
||||
_settle()
|
||||
if main.status_panel.visible:
|
||||
if main.ui.modals.status_panel.visible:
|
||||
problems.append("[status] 关闭后层未隐藏(模态栈单口失守)")
|
||||
|
||||
# ---- 图例层(旧模态、首过结构族):ap_bar legend_toggled
|
||||
# 信号真路径开(board_zone 信号→组合根 _on_legend_toggled→_open_modal)→结构断言族→
|
||||
# closed 信号真路径关(close()→组合根 _on_modal_closed)。
|
||||
main.board_zone.legend_toggled.emit()
|
||||
main.ui.battle.board_zone.legend_toggled.emit()
|
||||
_settle()
|
||||
if not main.legend_overlay.visible:
|
||||
if not main.ui.modals.legend_overlay.visible:
|
||||
problems.append("[legend] 图例层未打开(legend_toggled 路径中断)")
|
||||
await _audit("legend", [main.legend_overlay])
|
||||
main.legend_overlay.close()
|
||||
await _audit("legend", [main.ui.modals.legend_overlay])
|
||||
main.ui.modals.legend_overlay.close()
|
||||
_settle()
|
||||
if main.legend_overlay.visible:
|
||||
if main.ui.modals.legend_overlay.visible:
|
||||
problems.append("[legend] 关闭后层未隐藏(模态栈单口失守)")
|
||||
else:
|
||||
problems.append("[battle_hud] 候选中无战斗节点(驱动中断——候选=%s)" % str(main.level_screen._cand_ids))
|
||||
problems.append("[battle_hud] 候选中无战斗节点(驱动中断——候选=%s)" % str(main.ui.modals.level_screen._cand_ids))
|
||||
|
||||
# ---- 兑付屏(真实 open 面 + 合成最小数据——结构断言只看矩形/宽度/可见性)----
|
||||
main._end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), {"gp_summary": "GP 0"})
|
||||
main.ui.modals.end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), {"gp_summary": "GP 0"})
|
||||
_settle()
|
||||
await _audit("battle_end", [main._end_overlay])
|
||||
main._end_overlay.close()
|
||||
await _audit("battle_end", [main.ui.modals.end_overlay])
|
||||
main.ui.modals.end_overlay.close()
|
||||
_settle()
|
||||
|
||||
# ---- 节点结算层(旧模态、首过结构族):rest 兑付真路径开
|
||||
#(组合根 _open_modal 登记册+show_rest 合成喂入)→结构断言族→continue_pressed 信号真路径关
|
||||
#(press_continue→组合根 _on_node_continue→_close_modal)。
|
||||
main._open_modal(main.node_overlay)
|
||||
main.node_overlay.show_rest(3, 15, 20)
|
||||
main.ui.modals.flow.open(main.ui.modals.node_overlay)
|
||||
main.ui.modals.node_overlay.show_rest(3, 15, 20)
|
||||
_settle()
|
||||
if not main.node_overlay.visible:
|
||||
if not main.ui.modals.node_overlay.visible:
|
||||
problems.append("[node] 节点结算层未打开(rest 兑付路径中断)")
|
||||
await _audit("node_overlay", [main.node_overlay])
|
||||
main.node_overlay.press_continue()
|
||||
await _audit("node_overlay", [main.ui.modals.node_overlay])
|
||||
main.ui.modals.node_overlay.press_continue()
|
||||
_settle()
|
||||
if main.node_overlay.visible:
|
||||
if main.ui.modals.node_overlay.visible:
|
||||
problems.append("[node] 关闭后层未隐藏(continue_pressed 路径中断)")
|
||||
|
||||
main.curio_overlay.open_pick("结构探针·三选一", [
|
||||
main.ui.modals.curio_overlay.open_pick("结构探针·三选一", [
|
||||
{"id": "probe_heal", "text": "回血\n恢复 3 点"},
|
||||
{"id": "probe_magic", "text": "爆裂轰击\n下一击 +5"},
|
||||
{"id": "probe_pierce", "text": "穿甲\n无视护甲"},
|
||||
])
|
||||
_settle()
|
||||
await _audit("curio_pick", [main.curio_overlay])
|
||||
main.curio_overlay.visible = false
|
||||
await _audit("curio_pick", [main.ui.modals.curio_overlay])
|
||||
main.ui.modals.curio_overlay.visible = false
|
||||
_settle()
|
||||
|
||||
main.shop_overlay.open_shop([
|
||||
main.ui.modals.shop_overlay.open_shop([
|
||||
{"product_id": "probe_p1", "display_name": "探针商品甲", "attr": "atk", "flat_value": 1, "price": 10, "purchased": 0},
|
||||
{"product_id": "probe_p2", "display_name": "探针商品乙", "attr": "hp", "flat_value": 2, "price": 20, "purchased": 0},
|
||||
], 100)
|
||||
_settle()
|
||||
await _audit("shop", [main.shop_overlay])
|
||||
await _audit("shop", [main.ui.modals.shop_overlay])
|
||||
|
||||
# ---- 术语传导:货架卡与三选一卡片的玩家面文案(物品名=专长获取、分类=专长凭证)
|
||||
main.shop_overlay.open_shelf([
|
||||
main.ui.modals.shop_overlay.open_shelf([
|
||||
{"kind_id": "feat_ticket", "display_name": "专长获取", "value": 0, "price": 5},
|
||||
{"kind_id": "heal", "display_name": "应急回复", "value": 6, "price": 5},
|
||||
], 20)
|
||||
_settle()
|
||||
var ticket_row: Dictionary = main.shop_overlay._shelf_rows[0]
|
||||
var ticket_row: Dictionary = main.ui.modals.shop_overlay._shelf_rows[0]
|
||||
if String(main.db.run.shop.pool_row("feat_ticket").get("display_name", "")) != "专长获取":
|
||||
problems.append("[shop_term] shop.tres 专长凭证件名未换代(现=%s)" % String(main.db.run.shop.pool_row("feat_ticket").get("display_name", "")))
|
||||
if ticket_row.card.name_l.text != "专长获取":
|
||||
@@ -211,7 +211,7 @@ func _ready() -> void:
|
||||
problems.append("[shop_term] 专长凭证件描述未用规范流程句(现=%s)" % ticket_row.card.desc_l.text)
|
||||
if ticket_row.card.foot_l.text != "专长凭证":
|
||||
problems.append("[shop_term] 专长凭证件分类标注未用「专长凭证」(现=%s)" % ticket_row.card.foot_l.text)
|
||||
var heal_row: Dictionary = main.shop_overlay._shelf_rows[1]
|
||||
var heal_row: Dictionary = main.ui.modals.shop_overlay._shelf_rows[1]
|
||||
if heal_row.card.desc_l.text != "回复 +6(本冒险)":
|
||||
problems.append("[shop_term] 回复件描述漏了 heal 词表(现=%s)" % heal_row.card.desc_l.text)
|
||||
if heal_row.card.foot_l.text != "回复":
|
||||
@@ -223,7 +223,7 @@ func _ready() -> void:
|
||||
problems.append("[curio_term] 专长凭证三选一卡未用物品名/规范流程句(现=%s)" % ticket_text)
|
||||
if ticket_text.contains("门票") or ticket_text.contains("T5") or ticket_text.contains("持有集") or ticket_text.contains("奇物三选一"):
|
||||
problems.append("[curio_term] 专长凭证三选一卡残留退役词/内部任务号(现=%s)" % ticket_text)
|
||||
main.shop_overlay.visible = false
|
||||
main.ui.modals.shop_overlay.visible = false
|
||||
_settle()
|
||||
|
||||
# ---- ⑦resume 专项:续玩停点恢复(main.gd:717 报错回归闸)----
|
||||
@@ -234,11 +234,11 @@ func _ready() -> void:
|
||||
main.run_scope.runner = null # 全新启动等价态(真机=菜单直进续玩,无前置冒险——runner 从未赋值;探针此前推流会残留旧 runner)
|
||||
main._resume_from_menu()
|
||||
_settle(8)
|
||||
if not main._end_overlay.visible:
|
||||
if not main.ui.modals.end_overlay.visible:
|
||||
problems.append("[resume] 续玩恢复后胜利总结面未开(三选一停点分支中断)")
|
||||
else:
|
||||
var found := {"ok": false}
|
||||
_find_label_text(main._end_overlay, "合计 24", found)
|
||||
_find_label_text(main.ui.modals.end_overlay, "合计 24", found)
|
||||
if not found.ok:
|
||||
problems.append("[resume] 胜利总结面 GP 行未见快照合计「合计 24」(runner 缺位守卫缺位——717 回归)")
|
||||
var s: String = main._gp_summary_text({"gp_dropped": 0})
|
||||
@@ -280,7 +280,7 @@ func _find_label_text(node: Node, needle: String, found: Dictionary) -> void:
|
||||
func _pick_battle_node(main: Node) -> int:
|
||||
# 入口/兑付停点走 level_screen.open_choices 直传(不写 _pending_candidates)——
|
||||
# 候选从视图态取:_cand_ids × runner.nodes() 的 type(编号解析=node_id_of 单一出口)
|
||||
var ls: Node = main.level_screen
|
||||
var ls: Node = main.ui.modals.level_screen
|
||||
if ls._map._runner == null:
|
||||
return -1
|
||||
for nd in ls._map._runner.nodes():
|
||||
@@ -306,7 +306,7 @@ const _PAUSE_READONLY_SIGNALS: Array[String] = [
|
||||
|
||||
|
||||
func _status_readonly_contract(main: Node) -> void:
|
||||
var overlay: Control = main.status_panel
|
||||
var overlay: Control = main.ui.modals.status_panel
|
||||
var stack: Array = [overlay]
|
||||
var conn_count := 0
|
||||
while not stack.is_empty():
|
||||
@@ -332,7 +332,7 @@ func _status_readonly_contract(main: Node) -> void:
|
||||
# ⑨status 零动效契约(R5/P4 甲——零动效直开直关路径存在断言):
|
||||
# 子树零 AnimationPlayer + 视图源码零 tween/动效包装调用(源码 token 扫描=ui_contrast 静态段同手法)
|
||||
func _status_zero_motion_contract(main: Node) -> void:
|
||||
var overlay: Control = main.status_panel
|
||||
var overlay: Control = main.ui.modals.status_panel
|
||||
var stack: Array = [overlay]
|
||||
while not stack.is_empty():
|
||||
var n: Node = stack.pop_back()
|
||||
@@ -352,7 +352,7 @@ func _status_zero_motion_contract(main: Node) -> void:
|
||||
# 清单外零连接。白名单={视图自身 _emit_closed〔返回钮〕, 组合根 _on_career_closed};页内任何
|
||||
# 购买/导航/写入方向的新连接一律红,须先入册审查。
|
||||
func _career_readonly_contract(main: Node) -> void:
|
||||
var overlay: Control = main.career_overlay
|
||||
var overlay: Control = main.ui.modals.career_overlay
|
||||
var stack: Array = [overlay]
|
||||
var conn_count := 0
|
||||
while not stack.is_empty():
|
||||
@@ -377,7 +377,7 @@ func _career_readonly_contract(main: Node) -> void:
|
||||
|
||||
# ⑪career 零动效契约(状态面板 ⑨ 同形制——P4 甲直开直关)
|
||||
func _career_zero_motion_contract(main: Node) -> void:
|
||||
var overlay: Control = main.career_overlay
|
||||
var overlay: Control = main.ui.modals.career_overlay
|
||||
var stack: Array = [overlay]
|
||||
while not stack.is_empty():
|
||||
var n: Node = stack.pop_back()
|
||||
@@ -392,13 +392,13 @@ func _career_zero_motion_contract(main: Node) -> void:
|
||||
|
||||
|
||||
# ⑫模态登记/树序全量合同(V1——career 专属登记序契约推广到全部模态+屏壳;
|
||||
# 判据=main.LAYER_CONTRACT 单一事实源:登记册序==合同模态序、直接子层树序==合同序逐对相等。
|
||||
# 判据=main.ui.LAYER_CONTRACT 单一事实源:登记册序==合同模态序、直接子层树序==合同序逐对相等。
|
||||
# 两特例按合同数据表达:level_screen 宿住冒险屏壳(树序随宿主=全模态之下,遮挡语义归模态栈
|
||||
# screen_state 登记)、camp_overlay 成员与 camp_screen 屏壳同槽(屏壳树序代言成员登记位)。
|
||||
# _career_registry_contract 的断言(career=状态面板后/确认弹窗前、树序同步)由本全量合同覆盖)
|
||||
# ⑭选档界面契约(卡③ sl1/sl2/sl3/sl4/sl9——呈现面全部来自喂入数据,视图零自判)
|
||||
func _save_select_contract(main: Node, tpl: String) -> void:
|
||||
var sso: Node = main.save_select_overlay
|
||||
var sso: Node = main.ui.modals.save_select_overlay
|
||||
# sl1 三槽呈现:槽卡计数==3+零滚动容器(禁滚动长列表形态——契约 1)
|
||||
var rows: VBoxContainer = sso._rows_box
|
||||
var live_rows := 0
|
||||
@@ -477,13 +477,13 @@ func _collect_labels(node: Node, out: Array[String]) -> void:
|
||||
|
||||
|
||||
func _modal_registry_contract(main: Node) -> void:
|
||||
var list: Array = main._modals._list
|
||||
var list: Array = main.ui.modals.flow.stack._list
|
||||
# ① 登记册序==合同模态序(逐位点名——合同是登记序唯一事实源)
|
||||
var contract_modals: Array = []
|
||||
for entry: Dictionary in main.LAYER_CONTRACT:
|
||||
for entry: Dictionary in main.ui.LAYER_CONTRACT:
|
||||
if String(entry["kind"]) != "modal":
|
||||
continue
|
||||
var n: Node = main._layer_node(String(entry["key"]))
|
||||
var n: Node = main.ui.node(String(entry["key"]))
|
||||
if n == null:
|
||||
problems.append("[registry] 合同层缺失: %s〔装配生命批 V1〕" % String(entry["key"]))
|
||||
continue
|
||||
@@ -498,10 +498,10 @@ func _modal_registry_contract(main: Node) -> void:
|
||||
i, list[i].name, contract_modals[i].name])
|
||||
# ② 直接子层树序==合同序逐对相等(屏壳/模态/全屏态/非视觉节点全链;宿住层随宿主不参与)
|
||||
var prev: Node = null
|
||||
for entry: Dictionary in main.LAYER_CONTRACT:
|
||||
for entry: Dictionary in main.ui.LAYER_CONTRACT:
|
||||
if entry.has("host"):
|
||||
continue
|
||||
var node: Node = main._layer_node(String(entry["shell"]) if entry.has("shell") else String(entry["key"]))
|
||||
var node: Node = main.ui.node(String(entry["shell"]) if entry.has("shell") else String(entry["key"]))
|
||||
if node == null:
|
||||
problems.append("[registry] 合同层缺失: %s〔装配生命批 V1〕" % String(entry["key"]))
|
||||
continue
|
||||
@@ -510,23 +510,23 @@ func _modal_registry_contract(main: Node) -> void:
|
||||
prev.name, node.name, node.get_index(), prev.get_index()])
|
||||
prev = node
|
||||
# ③ 宿住层特例:level_screen 宿主=冒险屏壳且在全直接子模态之下(遮挡语义归模态栈 screen_state)
|
||||
var lv: Node = main.level_screen
|
||||
if lv.get_parent() != main._adventure_screen:
|
||||
var lv: Node = main.ui.modals.level_screen
|
||||
if lv.get_parent() != main.ui.screens.adventure_screen:
|
||||
problems.append("[registry] level_screen 宿主应=冒险屏壳(实测 %s)〔装配生命批 V1〕" % lv.get_parent().name)
|
||||
elif lv.get_parent().get_index() >= main.legend_overlay.get_index():
|
||||
elif lv.get_parent().get_index() >= main.ui.modals.legend_overlay.get_index():
|
||||
problems.append("[registry] 冒险屏壳应在全模态之下(宿主 idx=%d legend idx=%d)〔装配生命批 V1〕" % [
|
||||
lv.get_parent().get_index(), main.legend_overlay.get_index()])
|
||||
lv.get_parent().get_index(), main.ui.modals.legend_overlay.get_index()])
|
||||
# ④ 营地同槽特例:camp_screen 屏壳树序夹商店层与设置层之间(成员 camp_overlay 登记位代言)
|
||||
var cs: Node = main._camp_screen
|
||||
if cs.get_index() <= main.shop_overlay.get_index() or cs.get_index() >= main.settings_overlay.get_index():
|
||||
var cs: Node = main.ui.screens.camp_screen
|
||||
if cs.get_index() <= main.ui.modals.shop_overlay.get_index() or cs.get_index() >= main.ui.modals.settings_overlay.get_index():
|
||||
problems.append("[registry] camp_screen 屏壳树序应夹商店与设置层之间(idx=%d shop=%d settings=%d)〔装配生命批 V1〕" % [
|
||||
cs.get_index(), main.shop_overlay.get_index(), main.settings_overlay.get_index()])
|
||||
cs.get_index(), main.ui.modals.shop_overlay.get_index(), main.ui.modals.settings_overlay.get_index()])
|
||||
|
||||
|
||||
# ⑬career 零跳转契约(提纲 P4 甲——页内零「前往他处」入口:除「返回」外任何导航钮=红)
|
||||
func _career_nojump_contract(main: Node) -> void:
|
||||
for nav in ["营地", "商店", "设置", "冒险", "继续上局", "退出", "前往"]:
|
||||
if ProbeUiUtil.find_button(main.career_overlay, nav, "contains") != null:
|
||||
if ProbeUiUtil.find_button(main.ui.modals.career_overlay, nav, "contains") != null:
|
||||
problems.append("[career][nojump] 页内出现导航入口「%s」(零跳转违约——生涯纯只读)" % nav)
|
||||
|
||||
|
||||
@@ -534,7 +534,7 @@ func _career_nojump_contract(main: Node) -> void:
|
||||
# (名行基线离盒底 1px 同型高危写法返修回归闸;
|
||||
# 内容矩形=StandeeBody.content_rect(),最低绘制元素=底座下缘)
|
||||
func _standeefit_contract(main: Node) -> void:
|
||||
var st: Node = main.stage
|
||||
var st: Node = main.ui.battle.stage
|
||||
if st == null or st.hero_standee == null:
|
||||
problems.append("[standee] 舞台/主角站桩缺失(断言中断)")
|
||||
return
|
||||
@@ -711,8 +711,8 @@ func _dump(c: Control, depth: int) -> void:
|
||||
|
||||
# ⑤start 专项:左列/右列矩形不相交(M2 双列契约——装配件命名 start_box/start_right 为探针锚点)
|
||||
func _start_columns_contract(main: Node) -> void:
|
||||
var box: Control = main._start_overlay.find_child("start_box", true, false)
|
||||
var right: Control = main._start_overlay.find_child("start_right", true, false)
|
||||
var box: Control = main.ui.screens.start_overlay.find_child("start_box", true, false)
|
||||
var right: Control = main.ui.screens.start_overlay.find_child("start_right", true, false)
|
||||
if box == null or right == null:
|
||||
problems.append("[start][columns] 左列/右列节点缺失(start_box/start_right——装配命名失守)")
|
||||
return
|
||||
|
||||
+41
-39
@@ -43,35 +43,35 @@ func _ready() -> void:
|
||||
# ① 全局浮层角标开窗(调试入口=UI 最顶层浮层,任何 UI 不遮挡——
|
||||
# main 直接子层、LAYER_CONTRACT 末位 float 类;boot 菜单优先不开浮窗,角标真实点击路径开窗;
|
||||
# 悬浮于菜单之上——开始层不隐藏;树序新基准=登记册末位(确认弹窗之后=绘制序最上))
|
||||
main.debug_badge.pressed.emit()
|
||||
main.ui.screens.debug_badge.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
if not main.debug_overlay.visible:
|
||||
if not main.ui.modals.debug_overlay.visible:
|
||||
problems.append("调试浮层角标点击后浮窗未开(全局浮层入口失守)")
|
||||
if not main._start_overlay.visible:
|
||||
if not main.ui.screens.start_overlay.visible:
|
||||
problems.append("开始覆盖层被隐藏——浮窗应悬浮于菜单之上(14.5 形态失守)")
|
||||
if main.debug_overlay.get_index() < main.confirm_dialog.get_index():
|
||||
if main.ui.modals.debug_overlay.get_index() < main.ui.modals.confirm_dialog.get_index():
|
||||
problems.append("调试浮窗树序不在确认弹窗之后——登记册末位失守")
|
||||
# ② 滑条 → 生效预览联动
|
||||
main.debug_overlay.set_sliders(100, 50)
|
||||
if str(boosted_atk) not in main.debug_overlay.effect_text() or str(boosted_hp) not in main.debug_overlay.effect_text():
|
||||
problems.append("生效预览未联动(%s)" % main.debug_overlay.effect_text())
|
||||
main.ui.modals.debug_overlay.set_sliders(100, 50)
|
||||
if str(boosted_atk) not in main.ui.modals.debug_overlay.effect_text() or str(boosted_hp) not in main.ui.modals.debug_overlay.effect_text():
|
||||
problems.append("生效预览未联动(%s)" % main.ui.modals.debug_overlay.effect_text())
|
||||
# ③ 应用(真实信号路径:apply_requested → 组合根)——E4 后加成零表写,生效断言移至 ⑧ 战斗快照
|
||||
main.debug_overlay.press_apply_for_probe()
|
||||
main.ui.modals.debug_overlay.press_apply_for_probe()
|
||||
# ④ 重置(重开浮窗 → 重置按钮真实路径;面板百分比归零——下一场战斗生效值回到基线)
|
||||
main._show_debug_overlay()
|
||||
main.debug_overlay.press_reset_for_probe()
|
||||
main.ui.modals.debug_overlay.press_reset_for_probe()
|
||||
# ⑤ 批量模拟全链路(N=10——走 level_autoplay 与 headless 扫描同口径;表基线由组合根复位;
|
||||
# 菜单态上下文未锁定=按钮可点,is_running 语义=模拟在跑〔18卡⑦ 与对局禁用态拆分〕)
|
||||
main.debug_overlay.set_sliders(100, 0)
|
||||
main.debug_overlay.set_sim_count(10)
|
||||
main.debug_overlay.press_simulate_for_probe()
|
||||
main.ui.modals.debug_overlay.set_sliders(100, 0)
|
||||
main.ui.modals.debug_overlay.set_sim_count(10)
|
||||
main.ui.modals.debug_overlay.press_simulate_for_probe()
|
||||
var guard := SIM_GUARD_FRAMES
|
||||
while main.debug_overlay.is_running() and guard > 0:
|
||||
while main.ui.modals.debug_overlay.is_running() and guard > 0:
|
||||
guard -= 1
|
||||
await get_tree().process_frame
|
||||
if main.debug_overlay.is_running():
|
||||
if main.ui.modals.debug_overlay.is_running():
|
||||
problems.append("模拟未在预算帧内完成(疑似挂死)")
|
||||
var out: String = main.debug_overlay.output_text()
|
||||
var out: String = main.ui.modals.debug_overlay.output_text()
|
||||
if not out.contains("关卡完成率"):
|
||||
problems.append("模拟输出缺关卡完成率")
|
||||
# 口径换代 V7:胜局统计行措辞钉子(atk+100% 加成档 10 局必有胜局——hp_left_win 非空路径恒出)
|
||||
@@ -83,7 +83,7 @@ func _ready() -> void:
|
||||
problems.append("模拟输出 N 标注错误(%s)" % out.substr(0, 40))
|
||||
# ⑥ 关闭 → 浮窗收起、开始层全程在屏
|
||||
main._close_debug_overlay()
|
||||
if main.debug_overlay.visible or not main._start_overlay.visible:
|
||||
if main.ui.modals.debug_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
problems.append("关闭后层级状态错误")
|
||||
# ⑦ hero 表运行期不可变(E4 收口回归断言:任何代码 reintroduce 表写在此被抓获——原"表值残留"断言语义升级)
|
||||
if int(main.db.combat.hero.atk) != base_atk or int(main.db.combat.hero.max_hp) != base_hp:
|
||||
@@ -93,20 +93,20 @@ func _ready() -> void:
|
||||
# 存档隔离(停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律;
|
||||
# isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_tune_probe_save", true, false, true) # 末参=swap_save(卡②签名换代——断点写盘经 autoload 落隔离档)
|
||||
main.debug_overlay.set_sliders(100, 50)
|
||||
main.debug_overlay.press_apply_for_probe()
|
||||
main.debug_overlay.seed_edit.text = "42" # 种子钉死(18卡⑦审计🔴——空框=非首局随机,显式优先经真实路径断言 master_seed)
|
||||
main.ui.modals.debug_overlay.set_sliders(100, 50)
|
||||
main.ui.modals.debug_overlay.press_apply_for_probe()
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(18卡⑦审计🔴——空框=非首局随机,显式优先经真实路径断言 master_seed)
|
||||
main._start_adventure_from_menu() # 完整冒险路径(唯一试玩入口;_start_single 按语义清零滑条 = 恒基线,不承载加成断言)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
# ⑧b 开冒险路径显式关浮窗(18卡⑦ T2 防护——调试会话默认开态下开局不带窗,对局中经角标再开)
|
||||
if main.debug_overlay.visible:
|
||||
if main.ui.modals.debug_overlay.visible:
|
||||
problems.append("开冒险路径未显式关浮窗(T2 防护失守)")
|
||||
if int(main.run_scope.adventure.master_seed) != 42:
|
||||
problems.append("显式种子未达冒险(master_seed=%s ≠ 42——真实路径显式优先失守)" % str(main.run_scope.adventure.master_seed))
|
||||
# 关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照
|
||||
if main.level_screen.visible:
|
||||
main.level_screen.press_candidate(0)
|
||||
if main.ui.modals.level_screen.visible:
|
||||
main.ui.modals.level_screen.press_candidate(0)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var hero: Dictionary = main.run_scope.settlement.presentation_snapshot().hero
|
||||
@@ -116,23 +116,23 @@ func _ready() -> void:
|
||||
# ⑨ 对局中可开(18卡⑦ P2 甲/T2 防护——S1+ 停机点回归闸):战斗屏角标真实信号路径开窗;
|
||||
# 棋盘输入不锁、状态角标不置灰(「默认开×遮挡复活」下必锁死的破约回归面);
|
||||
# 对局中批量模拟/两层调试两钮禁用;当局数据六项与只读访问器逐项一致;关闭钮真实路径收窗恢复
|
||||
var dbg_badge: Button = main.debug_badge
|
||||
var dbg_badge: Button = main.ui.screens.debug_badge
|
||||
if dbg_badge == null or dbg_badge.visible:
|
||||
problems.append("对局中调试浮层角标非恒隐(发布态语义失守——卡⑦ 删写死行批)")
|
||||
else:
|
||||
main._show_debug_overlay() # 对局中开浮窗回归经直调承载(:61 既有直调先例——发布态角标恒隐,按钮信号路径退役)
|
||||
if not main.debug_overlay.visible:
|
||||
if not main.ui.modals.debug_overlay.visible:
|
||||
problems.append("对局中角标开浮窗失败(P2 甲对局中可达失守)")
|
||||
if main._input_locked():
|
||||
if main.ui.modals.flow.locks_input():
|
||||
problems.append("对局中开浮窗后棋盘输入锁死(T2 防护失守——S1+ 停机点)")
|
||||
if main.board_zone.status_badge.disabled:
|
||||
if main.ui.battle.board_zone.status_badge.disabled:
|
||||
problems.append("对局中开浮窗后状态角标置灰(T2 防护失守——S1+ 停机点)")
|
||||
if main.debug_overlay.is_running():
|
||||
if main.ui.modals.debug_overlay.is_running():
|
||||
problems.append("对局中上下文锁定误判为模拟在跑(is_running 语义互扰)")
|
||||
if not main.debug_overlay._sim_btn.disabled or not main.debug_overlay._dbg_btn.disabled:
|
||||
if not main.ui.modals.debug_overlay._sim_btn.disabled or not main.ui.modals.debug_overlay._dbg_btn.disabled:
|
||||
problems.append("对局中批量模拟/两层调试按钮未禁用(T2 并存态不开放失守)")
|
||||
# 当局数据六项值对(T3——数值与只读访问器逐项一致;种子=adventure.master_seed 现值非 _seed_value 重算)
|
||||
var rd: Dictionary = main.debug_overlay.run_data_probe()
|
||||
var rd: Dictionary = main.ui.modals.debug_overlay.run_data_probe()
|
||||
var r = main.run_scope.adventure.runner()
|
||||
if String(rd.get("seed", "")) != "42":
|
||||
problems.append("当局数据种子 ≠ master_seed 现值(%s)" % str(rd.get("seed")))
|
||||
@@ -146,27 +146,27 @@ func _ready() -> void:
|
||||
problems.append("当局数据 GP 失对(%s ≠ %s)" % [str(rd.get("gp")), str(r.gp)])
|
||||
if int(rd.get("keystone", -1)) != main.run_scope.adventure.journal().keystone_gained():
|
||||
problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.run_scope.adventure.journal().keystone_gained())])
|
||||
var close_btn: Button = ProbeUiUtil.find_button(main.debug_overlay, "关闭", "prefix")
|
||||
var close_btn: Button = ProbeUiUtil.find_button(main.ui.modals.debug_overlay, "关闭", "prefix")
|
||||
if close_btn == null:
|
||||
problems.append("浮窗关闭钮缺失(真实收窗路径中断)")
|
||||
else:
|
||||
close_btn.pressed.emit()
|
||||
if main.debug_overlay.visible or main._input_locked():
|
||||
if main.ui.modals.debug_overlay.visible or main.ui.modals.flow.locks_input():
|
||||
problems.append("对局中关闭浮窗后态失守(visible=%s locked=%s)"
|
||||
% [str(main.debug_overlay.visible), str(main._input_locked())])
|
||||
% [str(main.ui.modals.debug_overlay.visible), str(main.ui.modals.flow.locks_input())])
|
||||
# ⑨b 只读巡检(17卡③形制)+零动效契约(17卡③ R5——零 AnimationPlayer+源码零 tween)
|
||||
_overlay_readonly_contract(main, problems)
|
||||
_overlay_zero_motion_contract(main, problems)
|
||||
# ⑩ 种子三分单点断言(18卡⑦ C1——判定只落 _seed_value() 单点;对局态不影响本判定面):
|
||||
# 显式合法整数全场景优先(含首局)→ 首局(存档布尔 false)恒 42 → 非首局空框引擎熵连抽不全等 42
|
||||
main.debug_overlay.seed_edit.text = "777"
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "777"
|
||||
if main._seed_value() != 777:
|
||||
problems.append("显式种子输入未优先(_seed_value != 777——C1 ①失守)")
|
||||
# SaveManager 是 autoload 单例——换注走原地 attach(首局态挂载+恢复隔离档);
|
||||
# --script 模式无裸 autoload 标识符——经 main 节点路径解析
|
||||
var sm = main.get_node("/root/SaveManager")
|
||||
sm.slots.attach_path("user://smoke_tune_probe_seed.json") # 全新空档——first_adventure_done=false(首局态;新档缺省面)
|
||||
main.debug_overlay.seed_edit.text = ""
|
||||
main.ui.modals.debug_overlay.seed_edit.text = ""
|
||||
if main._seed_value() != 42:
|
||||
problems.append("首局空框种子 != 42(P5 甲失守——教学态可复现破约)")
|
||||
sm.slots.attach_path("user://smoke_tune_probe_save.json") # 恢复隔离档(非首局态——isolate_save 已建档置位)
|
||||
@@ -181,7 +181,7 @@ func _ready() -> void:
|
||||
all42 = false
|
||||
if all42:
|
||||
problems.append("非首局空框连抽 4 次全等 42(随机化失守——C1 ③)")
|
||||
main.debug_overlay.seed_edit.text = "42" # 钉回显式值(现场整洁)
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "42" # 钉回显式值(现场整洁)
|
||||
for f0 in ["user://smoke_tune_probe_seed.json", "user://smoke_tune_probe_seed.json.tmp"]:
|
||||
if FileAccess.file_exists(f0):
|
||||
DirAccess.remove_absolute(f0)
|
||||
@@ -196,10 +196,11 @@ func _ready() -> void:
|
||||
|
||||
|
||||
# 只读巡检(18卡⑦ T3——17卡③「白名单外零连接」形制沿用):浮窗子树全控件 × 游戏语义信号名清单
|
||||
# 全连接巡检——白名单={视图自身八方法 + 组合根 _on_modal_closed(closed 外发消费)};
|
||||
# 全连接巡检——白名单={视图自身八方法 + 注册面模态编排口 ui.modals.flow.close(closed 外发消费——
|
||||
# 注册面批把该连接目标从组合根转发壳改为 flow 真方法,机检白名单随批登记同一连接)};
|
||||
# 当局数据区块=纯标签零连接在此被一并约束(新增任何连接=红须入册审查)。
|
||||
func _overlay_readonly_contract(main: Node, problems: Array) -> void:
|
||||
var overlay: Control = main.debug_overlay
|
||||
var overlay: Control = main.ui.modals.debug_overlay
|
||||
var stack: Array = [overlay]
|
||||
var conn_count := 0
|
||||
while not stack.is_empty():
|
||||
@@ -210,8 +211,9 @@ func _overlay_readonly_contract(main: Node, problems: Array) -> void:
|
||||
var cb: Callable = conn["callable"]
|
||||
var host: Object = cb.get_object()
|
||||
var m := String(cb.get_method())
|
||||
var flow_host: Object = main.ui.modals.flow # 注册面模态编排口(closed 外发消费白名单宿主)
|
||||
var ok := (host == overlay and m in _OVERLAY_VIEW_METHODS) \
|
||||
or (host == main and m == "_on_modal_closed") \
|
||||
or (host == flow_host and m == "close") \
|
||||
or (host is VScrollBar or host is HScrollBar) # 引擎滚动机构内部 drag 接线(RichTextLabel 自动滚动条——17卡③布局生命线同类豁免,零游戏写通路)
|
||||
if not ok:
|
||||
problems.append("[readonly] 非法信号连接 %s.%s → %s.%s(白名单外零连接巡检——18卡⑦ T3)"
|
||||
@@ -224,7 +226,7 @@ func _overlay_readonly_contract(main: Node, problems: Array) -> void:
|
||||
|
||||
# 零动效契约(17卡③ R5 形制——子树零 AnimationPlayer + 视图源码零 tween/动效包装调用)
|
||||
func _overlay_zero_motion_contract(main: Node, problems: Array) -> void:
|
||||
var overlay: Control = main.debug_overlay
|
||||
var overlay: Control = main.ui.modals.debug_overlay
|
||||
var stack: Array = [overlay]
|
||||
while not stack.is_empty():
|
||||
var n: Node = stack.pop_back()
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。
|
||||
# receipt.json 是每跑批覆写的当次回执、*.log 是跑批当场日志——两者都是中间产物,不入库。
|
||||
# receipt.json 是每跑批覆写的当次回执、ci-status.json/ci-history.jsonl 是本地 CI 判定与流水账、
|
||||
# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志——均为中间产物,不入库。
|
||||
receipt.json
|
||||
*.log
|
||||
ci-status.json
|
||||
ci-history.jsonl
|
||||
ci.lock
|
||||
ci.rerun
|
||||
*.log
|
||||
@@ -1,61 +1,61 @@
|
||||
{
|
||||
"note": "存量基线·闸况变化时本脚本会判红,需人工确认后重生成;2026-10-29 二次记账=03/04/05 三闸按 80b49c0f 先例经用户裁决记入现行红项指纹(03=8a3cae0d0446/04=591b27250b36/05=de89589e49ed)——master 同值复现非迭代42新增(stash 对照法取证,承「迭代 41 测试债修复」条目扩展);原 2026-10-17 06 条目人工重生成记录见 git 提交史",
|
||||
"gates": {
|
||||
"00 冒烟测试": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"01 系统测试": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"02 单元与集成测试": {
|
||||
"exit": "1",
|
||||
"fp": "f9865f1a5a44"
|
||||
},
|
||||
"03 场景探针": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"05 渲染闸": {
|
||||
"exit": "1",
|
||||
"fp": "de89589e49ed"
|
||||
},
|
||||
"06 gdd_lint": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"06b 变更记录节": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"07 doc_link": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08 doc_term": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08c 流水线闸": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"09 静态扫描": {
|
||||
"exit": "1",
|
||||
"fp": "1a9ad773489f"
|
||||
},
|
||||
"10 熵增闸": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08d 引用可核": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08e 能力审计": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
"note": "\u5b58\u91cf\u57fa\u7ebf\u00b7\u95f8\u51b5\u53d8\u5316\u65f6\u672c\u811a\u672c\u4f1a\u5224\u7ea2\uff0c\u9700\u4eba\u5de5\u786e\u8ba4\u540e\u91cd\u751f\u6210\uff1b2026-10-29 \u4e8c\u6b21\u8bb0\u8d26\uff1d03/04/05 \u4e09\u95f8\u6309 80b49c0f \u5148\u4f8b\u7ecf\u7528\u6237\u88c1\u51b3\u8bb0\u5165\u73b0\u884c\u7ea2\u9879\u6307\u7eb9\uff0803=8a3cae0d0446/04=591b27250b36/05=de89589e49ed\uff09\u2014\u2014master \u540c\u503c\u590d\u73b0\u975e\u8fed\u4ee342\u65b0\u589e\uff08stash \u5bf9\u7167\u6cd5\u53d6\u8bc1\uff0c\u627f\u300c\u8fed\u4ee3 41 \u6d4b\u8bd5\u503a\u4fee\u590d\u300d\u6761\u76ee\u6269\u5c55\uff09\uff1b\u539f 2026-10-17 06 \u6761\u76ee\u4eba\u5de5\u91cd\u751f\u6210\u8bb0\u5f55\u89c1 git \u63d0\u4ea4\u53f2\uff1b2026-10-30 \u4e09\u952e\u6362\u4ee3\uff1d02/05/09 \u6309\u5de5\u7a0b\u5c0f\u4ef6\u6279\u56de\u6267 6a3203a8 \u5b9e\u6d4b\u7eff\u91cd\u951a\uff0802 \u5173\u8054\u6863\u5206\u72474/4\u5168\u7eff\uff0f05 \u6e32\u67d3\u95f8\u771f\u8dd1\u7eff\uff0f09 \u9759\u6001\u626b\u63cf\u7eff\u2014\u2014run_all \u9017\u53f7\u89e3\u6790\u4fee\u590d\uff0b\u6052\u771f\u4e94\u6761\u5224\u636e\u4fee\u6b63\u6279\uff0c\u4e09\u952e\u65e7\u7ea2\u6307\u7eb9\u5168\u90e8\u5f52\u7eff\u6d88\u9664\uff09",
|
||||
"gates": {
|
||||
"00 \u5192\u70df\u6d4b\u8bd5": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"01 \u7cfb\u7edf\u6d4b\u8bd5": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"02 \u5355\u5143\u4e0e\u96c6\u6210\u6d4b\u8bd5": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"03 \u573a\u666f\u63a2\u9488": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"05 \u6e32\u67d3\u95f8": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"06 gdd_lint": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"06b \u53d8\u66f4\u8bb0\u5f55\u8282": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"07 doc_link": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08 doc_term": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08c \u6d41\u6c34\u7ebf\u95f8": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"09 \u9759\u6001\u626b\u63cf": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"10 \u71b5\u589e\u95f8": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08d \u5f15\u7528\u53ef\u6838": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08e \u80fd\u529b\u5ba1\u8ba1": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,9 +66,10 @@ func _init() -> void:
|
||||
for a in OS.get_cmdline_user_args():
|
||||
if a.begins_with("--only="):
|
||||
has_only = true
|
||||
var term := a.trim_prefix("--only=").strip_edges()
|
||||
if term != "":
|
||||
only_terms.append(term)
|
||||
for part in a.trim_prefix("--only=").split(","):
|
||||
var term := part.strip_edges()
|
||||
if term != "":
|
||||
only_terms.append(term)
|
||||
elif a == "--only-source=gates":
|
||||
only_from_gates = true
|
||||
elif a.begins_with("--shard="):
|
||||
@@ -186,6 +187,7 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值)
|
||||
{"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态
|
||||
{"path": "res://tests/unit/presentation/standee_fx_sweep_test.gd", "expect": 0}, # 呈现域缺陷扫掠(d2 蓄力档/d4 庆祝句柄/d5+d8 互斥/d12 站桩按 id 解析)
|
||||
{"path": "res://tests/unit/presentation/tune_sim_batch_parity_test.gd", "expect": 0}, # 测速循环批双实现对拍(帧索引/运行态/进度串/stats/报告五面——信号步进 vs await 基线)
|
||||
{"path": "res://tests/unit/presentation/standee_present_values_test.gd", "expect": 0}, # 战斗一屏样张批 V5–V9(血条危险红/名字行素底不压地台/徽标 FS_HINT/护甲 PNG 绑定/素材入包)
|
||||
{"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 奖励节点 UI 直验(盲区补漏)
|
||||
{"path": "res://tests/unit/presentation/level_map_info_fog_test.gd", "expect": 0}, # 信息迷雾——呈现显隐/Boss 豁免/详情零显示/大图拟合
|
||||
|
||||
@@ -67,9 +67,9 @@ func _check_main_scene(main: Node) -> void:
|
||||
return
|
||||
var run_scope: Node = main.get("run_scope")
|
||||
check(run_scope != null and run_scope.is_inside_tree(), "闲时会话 run_scope 在树")
|
||||
var start_overlay: Control = main.get("_start_overlay")
|
||||
var start_overlay: Control = main.get("ui").screens.start_overlay
|
||||
check(start_overlay != null and start_overlay.visible, "启动态菜单可见(boot 后玩家见菜单)")
|
||||
check(main.get("settings_store") != null, "系统设置存储已装配(settings_store)")
|
||||
check(main.get("debug_badge") != null, "调试入口角标已装配(debug_badge)")
|
||||
check(main.get("legend_overlay") != null, "图例覆盖层已装配(legend_overlay)")
|
||||
check(main.get("status_panel") != null, "局内状态面板已装配(status_panel)")
|
||||
check(main.get("ui").screens.debug_badge != null, "调试入口角标已装配(debug_badge)")
|
||||
check(main.get("ui").modals.legend_overlay != null, "图例覆盖层已装配(legend_overlay)")
|
||||
check(main.get("ui").modals.status_panel != null, "局内状态面板已装配(status_panel)")
|
||||
@@ -314,7 +314,7 @@ fi
|
||||
# get_tree());排除注释行(行首 #/##)与字符串字面量(双引号包裹段内的 await 非代码),
|
||||
# 口径仿 S1/S2 的 grep 排除手法。
|
||||
# 自检:清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿。
|
||||
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd"
|
||||
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd"
|
||||
S8_MISS=0
|
||||
for f in $S8_FILES; do
|
||||
if [ ! -f "$f" ]; then
|
||||
@@ -334,7 +334,7 @@ else
|
||||
grep -rnE '\bawait\b' $S8_FILES 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | grep -vE '"[^"]*\bawait\b[^"]*"'
|
||||
fail=1
|
||||
else
|
||||
echo "[static_scan] S8 ok(扫描面 B1 文件集 9 个 .gd,await 零命中)"
|
||||
echo "[static_scan] S8 ok(扫描面 B1 文件集 10 个 .gd,await 零命中)"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -356,7 +356,7 @@ fi
|
||||
# 自检:扫描面为空即判红(require_scan_face);B1 文件集与三族自家文件缺位即判红
|
||||
# (文件被改名=豁免名单失效,宁可红不可静默放行)。
|
||||
if require_scan_face "S7" "$DIR_CORE"; then
|
||||
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd"
|
||||
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd"
|
||||
S7_MISS=0
|
||||
for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do
|
||||
if [ ! -f "$f" ]; then
|
||||
|
||||
+41
-41
@@ -81,20 +81,20 @@ func _smoke_scene_split() -> void:
|
||||
# V1a 启动态顶层断言:boot 后自树顶向下第一个可见内容层=start_overlay(headless 可判)
|
||||
#(内容层遍历——屏壳是容器不当候选:camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历会误命中)
|
||||
var top: Node = _topmost_visible_layer(main)
|
||||
check(top == main._start_overlay,
|
||||
check(top == main.ui.screens.start_overlay,
|
||||
"V1a 启动态顶层可见层=start_overlay(实测顶层=%s——菜单被冒险屏盖住即红)〔装配生命批 V1〕"
|
||||
% (top.name if top != null else "null"))
|
||||
# V1b 语义关系:start_overlay 在冒险屏内容之上(菜单屏壳在冒险屏壳之上——装配序修复判据)
|
||||
check(main._menu_screen.get_index() > main._adventure_screen.get_index(),
|
||||
check(main.ui.screens.menu_screen.get_index() > main.ui.screens.adventure_screen.get_index(),
|
||||
"V1b 菜单屏壳在冒险屏壳之上(start_overlay 盖住战斗内容)〔装配生命批 V1〕")
|
||||
# V1c 合同树序链逐对保序(直接子层全链=屏壳/模态/全屏态/非视觉节点;宿住层树序随宿主不参与)
|
||||
var chain_detail := ""
|
||||
var chain_prev: Node = null
|
||||
var chain_ok := true
|
||||
for entry: Dictionary in main.LAYER_CONTRACT:
|
||||
for entry: Dictionary in main.ui.LAYER_CONTRACT:
|
||||
if entry.has("host"):
|
||||
continue
|
||||
var node: Node = main._layer_node(String(entry["shell"]) if entry.has("shell") else String(entry["key"]))
|
||||
var node: Node = main.ui.node(String(entry["shell"]) if entry.has("shell") else String(entry["key"]))
|
||||
if node == null:
|
||||
chain_ok = false
|
||||
chain_detail += " 缺失:%s" % String(entry["key"])
|
||||
@@ -105,7 +105,7 @@ func _smoke_scene_split() -> void:
|
||||
chain_prev = node
|
||||
check(chain_ok, "V1c 合同树序链逐对保序(运行期树序==LAYER_CONTRACT 序;违例:%s)〔装配生命批 V1〕" % chain_detail)
|
||||
# ③ 探针挂载假设句柄在座(main.X 一级路径——探针迁移面收口证明)
|
||||
check(main._start_overlay == main._menu_screen.start_overlay and main.camp_overlay == main._camp_screen.camp_overlay and main.level_screen == main._adventure_screen.level_screen and main._battle_root == main._adventure_screen.battle_root and main.board_view == main._adventure_screen.board_view,
|
||||
check(main.ui.screens.start_overlay == main.ui.screens.menu_screen.start_overlay and main.ui.modals.camp_overlay == main.ui.screens.camp_screen.camp_overlay and main.ui.modals.level_screen == main.ui.screens.adventure_screen.level_screen and main.ui.screens.battle_root == main.ui.screens.adventure_screen.battle_root and main.ui.battle.board_zone.board_view == main.ui.screens.adventure_screen.board_view,
|
||||
"V2 组合根句柄指向屏壳成员(探针挂载假设 main.X 零迁移)")
|
||||
check(main.run_scope != null and not main.run_scope.is_adventure,
|
||||
"V2 闲时会话自 _ready 即在(永非 null;迭代 27 T3——gp 影子账退役无账本成员)")
|
||||
@@ -116,10 +116,10 @@ func _smoke_scene_split() -> void:
|
||||
# 顶层可见内容层(V1a 用——候选=合同模态/全屏态+两屏壳的 boot 内容成员;屏壳本身是容器不当候选:
|
||||
# camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历自顶向下会先命中透明壳)
|
||||
func _topmost_visible_layer(main: Node) -> Node:
|
||||
var candidates: Array[Node] = [main._start_overlay, main._battle_root, main._inter_overlay,
|
||||
main.legend_overlay, main.node_overlay, main.level_screen, main.curio_overlay,
|
||||
main._end_overlay, main.shop_overlay, main.camp_overlay, main.settings_overlay,
|
||||
main.status_panel, main.career_overlay, main.confirm_dialog, main.debug_overlay]
|
||||
var candidates: Array[Node] = [main.ui.screens.start_overlay, main.ui.screens.battle_root, main.ui.screens.inter_overlay,
|
||||
main.ui.modals.legend_overlay, main.ui.modals.node_overlay, main.ui.modals.level_screen, main.ui.modals.curio_overlay,
|
||||
main.ui.modals.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp_overlay, main.ui.modals.settings_overlay,
|
||||
main.ui.modals.status_panel, main.ui.modals.career_overlay, main.ui.modals.confirm_dialog, main.ui.modals.debug_overlay]
|
||||
var best: Node = null
|
||||
var best_x := -1
|
||||
var best_y := -1
|
||||
@@ -167,7 +167,7 @@ func _smoke_boot_chain() -> void:
|
||||
"V2b 设置已应用·减动效(UiKit.motion_scale=%s=settings_store.reduced_motion=%s)〔装配生命批 V2〕" % [
|
||||
str(ui_kit.motion_scale), str(ss.reduced_motion)])
|
||||
# ③ 启动态菜单可见
|
||||
check(main._start_overlay.visible,
|
||||
check(main.ui.screens.start_overlay.visible,
|
||||
"V2c 启动态 _start_overlay.visible=true(boot 后玩家见菜单)〔装配生命批 V2〕")
|
||||
# ④ 续玩钮可见态==SaveManager 断点在档态
|
||||
var sm = main.get_node("/root/SaveManager")
|
||||
@@ -186,22 +186,22 @@ func _smoke_boot_chain() -> void:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
sm.slots.attach_path("user://smoke_boot_chain.json")
|
||||
check(main._menu_screen.entersavebtn != null and main._menu_screen.settingsbtn != null
|
||||
and main._menu_screen.quitbtn != null,
|
||||
check(main.ui.screens.menu_screen.entersavebtn != null and main.ui.screens.menu_screen.settingsbtn != null
|
||||
and main.ui.screens.menu_screen.quitbtn != null,
|
||||
"V2d 三钮在位·进入存档/设置/退出游戏(续玩钮族退役后主菜单操作组承接)〔卡② mn1/mn8〕")
|
||||
for old_btn in ["lvbtn", "resumebtn", "campbtn", "careerbtn"]:
|
||||
check(not (old_btn in main._menu_screen),
|
||||
check(not (old_btn in main.ui.screens.menu_screen),
|
||||
"V2d 四旧钮句柄零命中·%s(彻底撤出——禁隐藏态禁置灰态残留)〔卡② mn2〕" % old_btn)
|
||||
check(_count_visible_buttons(main._start_overlay) == 3,
|
||||
check(_count_visible_buttons(main.ui.screens.start_overlay) == 3,
|
||||
"V2d 渲染面可见按钮计数==3(旧钮接回即红——渲染面锚,句柄缝盲区闭合)〔卡② mn1/mn2〕")
|
||||
# ⑤ 闲时会话(_ready 即建、永非 null、在树)
|
||||
check(main.run_scope != null and not main.run_scope.is_adventure and main.run_scope.is_inside_tree(),
|
||||
"V2e run_scope 非空且为闲时会话(is_adventure=false+is_inside_tree)〔装配生命批 V2〕")
|
||||
# ⑥ 调试浮窗 boot 不开+菜单角标在(菜单优先——boot 不自动开浮窗,
|
||||
# 入口=开始屏右下角「调试」角标+对局中两屏角标)
|
||||
check(not main.debug_overlay.visible,
|
||||
check(not main.ui.modals.debug_overlay.visible,
|
||||
"V2f 调试浮窗 boot 不自动开(菜单优先)〔2026-10-16 用户令〕")
|
||||
check(main.debug_badge != null and not main.debug_badge.visible,
|
||||
check(main.ui.screens.debug_badge != null and not main.ui.screens.debug_badge.visible,
|
||||
"V2f 调试浮层角标发布态恒隐(2026-10-25 删写死行批)")
|
||||
main.queue_free()
|
||||
await process_frame
|
||||
@@ -223,7 +223,7 @@ func _smoke_run_lifecycle() -> void:
|
||||
sm.slots.attach_path("user://smoke_run_lifecycle.json")
|
||||
sm.meta_state.mark_first_adventure_done()
|
||||
# ---- 第一局:开局→注脏→放弃→终局出口→会话收束 ----
|
||||
main.debug_overlay.seed_edit.text = "42"
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "42"
|
||||
main._start_adventure_from_menu()
|
||||
await process_frame
|
||||
check(main.run_scope != null and main.run_scope.is_adventure and main.run_scope.adventure != null,
|
||||
@@ -243,22 +243,22 @@ func _smoke_run_lifecycle() -> void:
|
||||
break
|
||||
await process_frame
|
||||
check(finished.done, "V3 冒险终局到达(abandon→adventure_finished 同步链)")
|
||||
check(main._end_overlay.is_open(), "V3 终局面板弹起")
|
||||
check(main.ui.modals.end_overlay.is_open(), "V3 终局面板弹起")
|
||||
main._on_battle_end_continue() # 终局出口(回菜单+会话收束——手工置空清单已消亡)
|
||||
await process_frame
|
||||
check(not main.run_scope.is_adventure and main.run_scope.adventure == null and main.run_scope.runner == null,
|
||||
"V3 会话收束(新闲时会话零冒险/关卡对象)")
|
||||
check(main.run_scope.hints_shown.is_empty() and not main.run_scope.boss_active and main.run_scope.last_gain_amount == 0,
|
||||
"V3 局态封印/账本全复位(queue_free 物化——注入脏状态零残留)")
|
||||
check(main._start_overlay.visible, "V3 回开始菜单(终局出口路径)")
|
||||
check(main.ui.screens.start_overlay.visible, "V3 回开始菜单(终局出口路径)")
|
||||
# ---- 第二局:放弃终局无断点残留→直进开局→全新会话 ----
|
||||
check(not sm.has_breakpoint(), "V3 放弃终局清断点(无残留断点)")
|
||||
main.debug_overlay.seed_edit.text = "43"
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "43"
|
||||
main._start_adventure_from_menu()
|
||||
await process_frame
|
||||
check(main.run_scope.is_adventure and main.run_scope.adventure != null and main.run_scope.hints_shown.is_empty(),
|
||||
"V3 第二连局全新会话(两连局零泄漏)")
|
||||
check(not main._start_overlay.visible, "V3 直进入局(菜单隐)")
|
||||
check(not main.ui.screens.start_overlay.visible, "V3 直进入局(菜单隐)")
|
||||
main.queue_free()
|
||||
await process_frame
|
||||
for p in ["user://smoke_run_lifecycle.json", "user://smoke_run_lifecycle.json.tmp"]:
|
||||
@@ -281,14 +281,14 @@ func _smoke_sim_guards() -> void:
|
||||
# wd6:闲时态 + 模拟运行态 → 调试两层入口守卫早退(开始层不收起、无结算对象)
|
||||
check(main.run_scope.settlement == null and not main.run_scope.is_adventure,
|
||||
"前置:闲时会话(settlement 空 + is_adventure=false)")
|
||||
main._start_overlay.visible = true
|
||||
main.ui.screens.start_overlay.visible = true
|
||||
main._tune_sim._running = true
|
||||
main._start_debug_levels()
|
||||
check(main.run_scope.settlement == null and main._start_overlay.visible,
|
||||
check(main.run_scope.settlement == null and main.ui.screens.start_overlay.visible,
|
||||
"模拟运行态下调试两层入口守卫早退(开始层不收起)〔wd6〕")
|
||||
main._tune_sim._running = false
|
||||
# wd7:真实冒险关间态(is_adventure=true、settlement==null)→ 模拟入口守卫早退
|
||||
main.debug_overlay.seed_edit.text = "42"
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "42"
|
||||
main._start_adventure_from_menu()
|
||||
await process_frame
|
||||
check(main.run_scope.is_adventure and main.run_scope.settlement == null,
|
||||
@@ -313,8 +313,8 @@ func _face_snapshot(main: Node) -> Dictionary:
|
||||
var tu = main._treasure_ui
|
||||
var bo = main._bonus
|
||||
var cp = main._camp_pres
|
||||
var mf = main._modal_flow
|
||||
var rp = main._row_pacer
|
||||
var mf = main.ui.modals.flow
|
||||
var rp = main.ui.screens.row_pacer
|
||||
return {
|
||||
"treasure_settlement": tu._settlement_get.call(),
|
||||
"treasure_runner": tu._runner_get.call(),
|
||||
@@ -381,13 +381,13 @@ func _stage_faces(main: Node, tag: String, adv: String, run: String, set: String
|
||||
if bool(snap["mf_rows_empty"]) != bool(extra[k]):
|
||||
bad += " modal_flow.rows_empty_get 应=%s" % str(extra[k])
|
||||
"level_vis":
|
||||
if main.level_screen.visible != bool(extra[k]):
|
||||
if main.ui.modals.level_screen.visible != bool(extra[k]):
|
||||
bad += " level_screen.visible 应=%s" % str(extra[k])
|
||||
"end_open":
|
||||
if main._end_overlay.is_open() != bool(extra[k]):
|
||||
if main.ui.modals.end_overlay.is_open() != bool(extra[k]):
|
||||
bad += " _end_overlay.is_open 应=%s" % str(extra[k])
|
||||
"start_vis":
|
||||
if main._start_overlay.visible != bool(extra[k]):
|
||||
if main.ui.screens.start_overlay.visible != bool(extra[k]):
|
||||
bad += " _start_overlay.visible 应=%s" % str(extra[k])
|
||||
check(bad.is_empty(), "%s(引用面违例:%s)〔装配生命批 V3〕" % [tag, bad])
|
||||
|
||||
@@ -419,7 +419,7 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
sm.slots.attach_path("user://smoke_lifecycle.json")
|
||||
main.debug_overlay.seed_edit.text = "42"
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "42"
|
||||
# 阶段①闲时
|
||||
await _stage_faces(main, "V3a 闲时", "null", "null", "null",
|
||||
{"mf_start": true, "mf_inter": false, "mf_rows_empty": true, "start_vis": true})
|
||||
@@ -430,7 +430,7 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
await _stage_faces(main, "V3b 开局停点", "alive", "alive", "null",
|
||||
{"mf_start": false, "level_vis": true})
|
||||
# 阶段③地图选路→战斗中(真实选路提交 press_candidate)
|
||||
main.level_screen.press_candidate(0)
|
||||
main.ui.modals.level_screen.press_candidate(0)
|
||||
for i in range(10):
|
||||
await process_frame
|
||||
await _stage_faces(main, "V3c 战斗中", "alive", "alive", "alive", {"mf_start": false})
|
||||
@@ -465,9 +465,9 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
check(main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live(),
|
||||
"V3d 活战斗驱动完毕(战斗已收场——%d 轮,finished=%s)〔装配生命批 V3〕" % [guard, str(finished.done)])
|
||||
await process_frame
|
||||
main._flush_row_queue()
|
||||
main.ui.screens.row_pacer.flush()
|
||||
for i in range(12):
|
||||
if main._end_overlay.is_open():
|
||||
if main.ui.modals.end_overlay.is_open():
|
||||
break
|
||||
await process_frame
|
||||
# 分支甲:战斗胜利停点(结算后)→确认进选路→放弃→终局面
|
||||
@@ -482,13 +482,13 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
if finished.done:
|
||||
break
|
||||
await process_frame
|
||||
main._flush_row_queue()
|
||||
main.ui.screens.row_pacer.flush()
|
||||
for i in range(8):
|
||||
if main._end_overlay.is_open():
|
||||
if main.ui.modals.end_overlay.is_open():
|
||||
break
|
||||
await process_frame
|
||||
# 阶段⑤终局面(败北/放弃皆达)
|
||||
check(finished.done and main._end_overlay.is_open(),
|
||||
check(finished.done and main.ui.modals.end_overlay.is_open(),
|
||||
"V3e 终局面到达(adventure_finished+终局面板开)〔装配生命批 V3〕")
|
||||
await _stage_faces(main, "V3e 终局面", "alive", "alive", "null", {"mf_start": false, "end_open": true})
|
||||
# 阶段⑥回菜单收束(旧局消亡+新会话即驻+无真空窗)
|
||||
@@ -515,7 +515,7 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
main._open_save_select()
|
||||
for i in range(2):
|
||||
await process_frame
|
||||
var ssel_new: Button = main.save_select_overlay._new_btns.get(1)
|
||||
var ssel_new: Button = main.ui.modals.save_select_overlay._new_btns.get(1)
|
||||
if ssel_new == null:
|
||||
check(false, "V3g 营地入口缺失(选档层新档钮驱动中断)〔装配生命批 V3〕")
|
||||
continue
|
||||
@@ -551,7 +551,7 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
DirAccess.remove_absolute(slot_p)
|
||||
# 阶段⑧两连局(第二局全新会话——旧局对象不复用、脏态零残留)
|
||||
var first_adv = main.run_scope.adventure
|
||||
main.debug_overlay.seed_edit.text = "43"
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "43"
|
||||
main._start_adventure_from_menu()
|
||||
for i in range(6):
|
||||
await process_frame
|
||||
@@ -560,12 +560,12 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
await _stage_faces(main, "V3h 两连局·开局停点", "alive", "alive", "null", {"mf_start": false, "level_vis": true})
|
||||
check(main.run_scope.adventure.abandon(), "V3h 放弃受理(第二局)〔装配生命批 V3〕")
|
||||
for i in range(12):
|
||||
if main._end_overlay.is_open():
|
||||
if main.ui.modals.end_overlay.is_open():
|
||||
break
|
||||
await process_frame
|
||||
main._flush_row_queue()
|
||||
main.ui.screens.row_pacer.flush()
|
||||
for i in range(8):
|
||||
if main._end_overlay.is_open():
|
||||
if main.ui.modals.end_overlay.is_open():
|
||||
break
|
||||
await process_frame
|
||||
await _stage_faces(main, "V3h 两连局·终局面", "alive", "alive", "null", {"mf_start": false, "end_open": true})
|
||||
|
||||
@@ -34,9 +34,31 @@ func _init() -> void:
|
||||
_smoke_pool_wiring()
|
||||
_smoke_damage_types()
|
||||
_smoke_attack_chain_fresh_pool()
|
||||
_smoke_hero_atk_derived()
|
||||
quit(Kit.finish("roll_multiplier_test"))
|
||||
|
||||
|
||||
# ---- 主角攻击基值派生读(注册面批同代编排归约 V4——_hero_atk 快照成员退役改派生读:
|
||||
# enc 携 hero_atk 键受理后经派生读供 spawn 与攻击单 base_value 两消费点;缺省回退表值——
|
||||
# 受理后不变量(encounter 受理后只读)。攻击单 base_value 贯通由 _smoke_damage_types 的
|
||||
# hero_atk_val=100 全链路行值承载(同夹具既有覆盖——派生读在攻击链上工作即行值随 override 走)----
|
||||
func _smoke_hero_atk_derived() -> void:
|
||||
var rng_a = load("res://core/rng_service.gd").new()
|
||||
rng_a.set_master_seed(4242)
|
||||
var s_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a)
|
||||
Kit.check_quiet(s_a.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 100}),
|
||||
"起战受理(override 100)〔编排归约 V4〕")
|
||||
Kit.check_quiet(int(s_a._enemy_system.hero_unit().get("atk", -1)) == 100,
|
||||
"主角攻击单基值==enc 携带 override(_hero_atk 派生读贯通 spawn 消费点)〔编排归约 V4〕")
|
||||
var rng_b = load("res://core/rng_service.gd").new()
|
||||
rng_b.set_master_seed(4242)
|
||||
var s_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b)
|
||||
Kit.check_quiet(s_b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}),
|
||||
"起战受理(缺省)〔编排归约 V4〕")
|
||||
Kit.check_quiet(int(s_b._enemy_system.hero_unit().get("atk", -1)) == int(_db.combat.hero.atk),
|
||||
"缺省主角攻击单基值==hero 表值(派生读回退面)〔编排归约 V4〕")
|
||||
|
||||
|
||||
# ---- 敌方攻击类型轴(表列/单位快照/单据载荷/矩阵命中证词)----
|
||||
# 克制矩阵命中证词 = 同种子三甲型配对实测(不是查表存在性证词):同种子下 armor_roll 流消费逐位一致、
|
||||
# 阈值(hero armor_class)不变,三甲型唯一差异 = 克制点数 cdelta(normal_vs_light 0 / heavy −1 / ward +1)
|
||||
|
||||
@@ -83,9 +83,10 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(42)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false}
|
||||
var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false, "econ_refresh_used": -1}
|
||||
settlement.step_row.connect(func(row): out.stream.append(row))
|
||||
settlement.battle_finished.connect(func(payload): out.result = payload.get("result", ""))
|
||||
settlement.settlement_snapshot.connect(func(snap): out.econ_refresh_used = int(snap.get("economy", {}).get("refresh_used", -1))) # 末次发射=终局轮快照
|
||||
settlement.minesweeper.cell_revealed.connect(func(evt): out.events.append(evt))
|
||||
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
|
||||
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
|
||||
@@ -108,6 +109,10 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
|
||||
# P0-3 回归断言:notify_combat_end 曾被并进行尾注释从不执行——
|
||||
# 战斗收束后相位必须回 Idle(RAP 清零),任何「相位卡 Waiting」回归在此被抓
|
||||
Kit.check("战斗收束后行动经济相位回 Idle(notify_combat_end 生效,实测=%s)" % String(action_economy.phase), action_economy.phase == "Idle")
|
||||
# 复位边界(注册面批同代编排归约 V2——BattleFacts.reset_round 不碰 refresh_used 的可观测提交面):
|
||||
# 终局序列含 facts.reset_round(),其后呈现快照的 refresh_used 必须等于末轮结算快照的 economy 值
|
||||
Kit.check("L%d·D%d 终局后呈现快照 refresh_used==末轮结算快照 economy 值(复位不碰 refresh_used)〔编排归约 V2〕" % [L, D],
|
||||
int(settlement.presentation_snapshot().get("refresh_used", -1)) == out.econ_refresh_used)
|
||||
return out
|
||||
|
||||
|
||||
@@ -1019,7 +1024,7 @@ func _smoke_counter_buckets() -> void:
|
||||
if not oA.minesweeper.current_board().is_mine(ca):
|
||||
safe_a = ca
|
||||
break
|
||||
oA._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1)
|
||||
oA._rounds._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1)
|
||||
Kit.check("V6 第1战标错落 battle 桶 insurance_used=1(_apply_counter_hit 静态写口经 ctx)〔无状态化 V6〕", EffectEngine.counter(oA.fx_host().counters_ctx, "insurance_used") == 1)
|
||||
# 卡④ wr2①——完美保险标错行=单一扩展行型(原 insurance_used 实名行型收敛)
|
||||
var v6_ins := {}
|
||||
|
||||
@@ -374,10 +374,10 @@ func _smoke_clear_reward_zero_paths() -> void:
|
||||
Kit.check("高光守卫构造战斗启动", s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}))
|
||||
for u in s_b._enemy_system.all_enemies():
|
||||
s_b._enemy_system.apply_damage(u, int(u.hp))
|
||||
s_b._perfect_hit_true = 1 # 测试通道:构造完美谓词成立(真值在正常流由封印判定累加)
|
||||
s_b._perfect_hit_false = 0
|
||||
s_b._facts.perfect_hit_true = 1 # 测试通道:构造完美谓词成立(真值在正常流由封印判定累加)
|
||||
s_b._facts.perfect_hit_false = 0
|
||||
rows_b.clear()
|
||||
s_b.reward_grant().grant_clear_reward(1, s_b._board_from_midround_refresh, s_b.perfect_sweep_earned())
|
||||
s_b.reward_grant().grant_clear_reward(1, s_b._facts.board_from_midround, s_b.perfect_sweep_earned())
|
||||
Kit.check("② grant_clear_reward 守卫路径重走首份 → 未生效行仍在案", _has_source_row(rows_b, "reward_ineffective", "clear_reward", "clear_blast"))
|
||||
Kit.check("② 首份零兑付 → 高光行不发(不记名义值 2)", _rows_of_type(rows_b, "highlight").is_empty())
|
||||
# ③ 第二份被拒:唯一存活敌 HP ≤ 首份 aoe 量 → 首份击杀 → 第二份无存活敌(zero + 未生效行)
|
||||
@@ -389,10 +389,10 @@ func _smoke_clear_reward_zero_paths() -> void:
|
||||
Kit.check("第二份被拒构造战斗启动", s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}))
|
||||
var boss = s2._enemy_system.all_enemies()[0]
|
||||
boss.hp = 1 # 测试通道:压低至首份 aoe 必杀(弱形式——正常流 Boss 血量不可控)
|
||||
s2._perfect_hit_true = 1
|
||||
s2._perfect_hit_false = 0
|
||||
s2._facts.perfect_hit_true = 1
|
||||
s2._facts.perfect_hit_false = 0
|
||||
rows2.clear()
|
||||
s2.reward_grant().grant_clear_reward(1, s2._board_from_midround_refresh, s2.perfect_sweep_earned())
|
||||
s2.reward_grant().grant_clear_reward(1, s2._facts.board_from_midround, s2.perfect_sweep_earned())
|
||||
Kit.check("③ 首份 aoe 真实兑付(击杀唯一存活敌)", _has_source_row(rows2, "damage", "clear_reward"))
|
||||
Kit.check("③ 第二份被拒产出 reward_ineffective 行(零兑付)", _has_source_row(rows2, "reward_ineffective", "clear_reward", "clear_blast"))
|
||||
var hl2 := _rows_of_type(rows2, "highlight")
|
||||
|
||||
@@ -116,19 +116,19 @@ func _init() -> void:
|
||||
"feats": [], "tree_rules": [], "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0,
|
||||
"node_type": "battle", "adv_bucket": {}}), "V6 前置:战斗受理")
|
||||
# facts 组包经真实摇号路径消费(mines 全零时条件技 sneak_ambush 不入集合——facts 四键在位的可观察面)
|
||||
Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["last"]) == 0 and int(orch._mines_hits["round"]) == 0,
|
||||
Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["last"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0,
|
||||
"V6 开战清零:三计数全零起〔触发器批 V6〕")
|
||||
# 正向 blast(spark_aoe)不递增——负向计数守卫(flat 行应用面自编排器迁至奖励授予件
|
||||
# ——迭代40 卡① 拆件;计数经授予件 battle_hooks.negative_blast 回宿主 _mines_hits)
|
||||
var att_v6 := {"source_class": "reward", "cell": Vector2i(-1, -1), "ri": 1}
|
||||
orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "positive", "power": 2,
|
||||
"display_name": "爆裂", "reward_id": "spark_aoe"}, att_v6)
|
||||
Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["round"]) == 0,
|
||||
Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0,
|
||||
"V6 正向 blast(spark_aoe)不递增(负向守卫——清图/爆裂格零误计)〔触发器批 V6〕")
|
||||
# 负向 blast(雷)递增
|
||||
orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "negative", "power": 2,
|
||||
"display_name": "警觉", "reward_id": "enemy_alert"}, att_v6)
|
||||
Kit.check_quiet(int(orch._mines_hits["total"]) == 1 and int(orch._mines_hits["round"]) == 1,
|
||||
Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 1 and int(orch._rounds.mines_hits["round"]) == 1,
|
||||
"V6 负向 blast(雷)递增恰一次〔触发器批 V6〕")
|
||||
|
||||
# ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)----
|
||||
|
||||
@@ -66,12 +66,33 @@ func _smoke_hygiene_gates() -> void:
|
||||
var mv: String = FileAccess.get_file_as_string("res://presentation/main.gd")
|
||||
Kit.check("V8 GP 影子账残留零命中(main 源码面——文件已删+委托缝已拆)〔迭代27 T3〕",
|
||||
not mv.contains("gp_shadow_ledger") and not mv.contains("_gp_ledger"))
|
||||
# V1d 层序落位单口:main.gd 的 move_child 调用全部收在 _apply_layer_contract 内(合同装配单口——
|
||||
# 函数体之外零调用点;出现第二处落位=硬编码层序,须改合同表而非加调用)
|
||||
var fn_start: int = mv.find("func _apply_layer_contract")
|
||||
var fn_end: int = mv.find("\nfunc ", fn_start + 10)
|
||||
if fn_end < 0:
|
||||
fn_end = mv.length()
|
||||
var outside_calls: int = (mv.substr(0, fn_start) + mv.substr(fn_end)).count("move_child(")
|
||||
Kit.check("V1d 层序落位单口(_apply_layer_contract 之外 move_child 调用点=%d 应=0)〔装配生命批 V1〕" % outside_calls,
|
||||
outside_calls == 0)
|
||||
# V1d 层序落位单口:注册面建件后扫全 presentation——move_child 只允许出现在 ui_registry.gd 的
|
||||
# apply_layer_contract 函数体内(合同装配单口——出现第二处落位=硬编码层序,须改合同表而非加调用)
|
||||
var scanned: int = 0
|
||||
var outside_calls: int = 0
|
||||
for fn_path: String in _list_scripts("res://presentation"):
|
||||
var src: String = FileAccess.get_file_as_string(fn_path)
|
||||
scanned += 1
|
||||
var fn_start: int = src.find("func apply_layer_contract") if fn_path.contains("ui_registry.gd") else -1
|
||||
var fn_end: int = src.find("\nfunc ", fn_start + 10) if fn_start >= 0 else -1
|
||||
if fn_end < 0:
|
||||
fn_end = src.length()
|
||||
outside_calls += (src.substr(0, fn_start) + src.substr(fn_end)).count("move_child(")
|
||||
Kit.check("V1d 层序落位单口(presentation %d 文件——apply_layer_contract 之外 move_child 调用点=%d 应=0)〔注册面 V1〕" % [scanned, outside_calls],
|
||||
scanned > 0 and outside_calls == 0)
|
||||
|
||||
|
||||
# 目录 .gd 清单(DirAccess 平铺——presentation 无子目录;扫描面>0 前置由调用侧断言承载)
|
||||
static func _list_scripts(dir_path: String) -> PackedStringArray:
|
||||
var out: PackedStringArray = PackedStringArray()
|
||||
var dir := DirAccess.open(dir_path)
|
||||
if dir == null:
|
||||
return out
|
||||
dir.list_dir_begin()
|
||||
var fname := dir.get_next()
|
||||
while fname != "":
|
||||
if fname.ends_with(".gd"):
|
||||
out.append(dir_path + "/" + fname)
|
||||
fname = dir.get_next()
|
||||
dir.list_dir_end()
|
||||
return out
|
||||
@@ -63,8 +63,11 @@ func _test_rng_tag_closure(db) -> void:
|
||||
var stream_label: String = String(db.combat.settlement.damage_roll.armor_roll_stream)
|
||||
Kit.check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)",
|
||||
RngServiceScript.PURPOSE_TAGS.has(stream_label))
|
||||
var unique_tags: Dictionary = {}
|
||||
for t in RngServiceScript.PURPOSE_TAGS:
|
||||
unique_tags[String(t)] = true
|
||||
Kit.check("RngService 用途标签词表非空且互异(封闭语义由流改标签)",
|
||||
RngServiceScript.PURPOSE_TAGS.size() > 0)
|
||||
RngServiceScript.PURPOSE_TAGS.size() > 0 and unique_tags.size() == RngServiceScript.PURPOSE_TAGS.size())
|
||||
|
||||
|
||||
# ---- B1→D1 行协议:单据九栏缺省齐备 + 协议公共键形态 ----
|
||||
|
||||
@@ -850,7 +850,7 @@ func _drive_to_pref(autoplay, prefer: String) -> Dictionary:
|
||||
r.pick_next_node(int(hit.get("id", -1)))
|
||||
out["node"] = hit
|
||||
if prefer == "battle":
|
||||
out["enc"] = r.current_battle().encounter
|
||||
out["enc"] = r.current_battle()._facts.encounter
|
||||
else:
|
||||
var cands: Array = (adr.stop().node_curio_candidates() as Dictionary).get("candidates", []) as Array
|
||||
adr.stop().present_node_curio(cands)
|
||||
|
||||
@@ -8,6 +8,7 @@ extends SceneTree
|
||||
|
||||
const SaveManagerScript := preload("res://core/save_manager.gd")
|
||||
const AdventureRunnerScript := preload("res://core/adventure_runner.gd")
|
||||
const BreakpointStoreScript := preload("res://core/breakpoint_store.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点(静默收集通道)
|
||||
|
||||
var _bp_path := "user://test_breakpoint_resume.json"
|
||||
@@ -182,7 +183,7 @@ func _init() -> void:
|
||||
"P7 同种子重开局同图(开局入口复现承载)〔v7〕")
|
||||
|
||||
# ---- Part 8:断点字段登记单源(登记键集 == 写侧实拼字典键集;键名增删即红)----
|
||||
var fields: Dictionary = AdventureRunnerScript.BREAKPOINT_FIELDS
|
||||
var fields: Dictionary = BreakpointStoreScript.BREAKPOINT_FIELDS # 断点域拆件改址——登记单源随件(判据零变化)
|
||||
var reg_keys: Array = fields.keys()
|
||||
reg_keys.sort()
|
||||
var bp_keys: Array = bp_j.keys()
|
||||
|
||||
@@ -48,7 +48,11 @@ func _init() -> void:
|
||||
a.stop()._node_pending = {}
|
||||
# 摘牌:选中即消尽(禁早退路径零摘牌)
|
||||
var deck_refresh_before: int = a.deck().pool_stock("refresh")
|
||||
Kit.check_quiet(deck_refresh_before == 0 or true, "牌库 refresh 令牌读数在域(投放 %d)" % deck_refresh_before)
|
||||
var deck_refresh_quota := 0
|
||||
for tier in db.run.adventure.TIERS:
|
||||
deck_refresh_quota += int(db.run.adventure.curio_quota("refresh", String(tier)))
|
||||
Kit.check_quiet(deck_refresh_before == deck_refresh_quota - 1,
|
||||
"牌库 refresh 令牌读数=表配额−本会话已兑付一张(读数 %d/配额 %d)" % [deck_refresh_before, deck_refresh_quota])
|
||||
|
||||
# V9-2 储备金袋:待注入账 + 战首注入(节点级)
|
||||
a.stop()._pending = {"candidates": [{"kind": "rap", "tier": "rare"}]} # 停点候选直写(构造面——pick_curio 真实入口前置)
|
||||
@@ -174,10 +178,10 @@ func _init() -> void:
|
||||
Kit.check_quiet(safe_cells.size() == 2, "保险 rig:两枚安全格定位(去重闸按格实例——限次样本须异格)〔卡② R2〕")
|
||||
var rows_orch: Array[Dictionary] = []
|
||||
orch.step_row.connect(func(row): rows_orch.append(row)) # step_row 公开信号面(insurance_used 载荷/seal_miss highlight=豁免账与误标账观测面)
|
||||
Kit.check_quiet(int(orch._perfect_hit_false) == 0 and int(orch._perfect_forgiven) == 0,
|
||||
Kit.check_quiet(int(orch._facts.perfect_hit_false) == 0 and int(orch._facts.perfect_forgiven) == 0,
|
||||
"保险前置:误标账与豁免账双零起〔卡② R2〕")
|
||||
orch._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1)
|
||||
Kit.check_quiet(int(orch._perfect_hit_false) == 1,
|
||||
orch._rounds._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1)
|
||||
Kit.check_quiet(int(orch._facts.perfect_hit_false) == 1,
|
||||
"计数纯净:首次标错 _perfect_hit_false 落 1 未被回退改写(契约 2)〔卡② R2〕")
|
||||
var ins_row := {}
|
||||
for r in rows_orch:
|
||||
@@ -187,7 +191,7 @@ func _init() -> void:
|
||||
"豁免一次:首次标错 _perfect_forgiven=1(forgive_seal_miss 派发)〔卡② R2〕")
|
||||
Kit.check_quiet(int(orch.action_economy.pending_ap_delta()) == 1,
|
||||
"事件不吞:同一标错的 seal_missed 仍被危险直觉·守收到(queue AP+1)〔卡② R2〕")
|
||||
orch._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2)
|
||||
orch._rounds._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2)
|
||||
var ins_count := 0
|
||||
var miss_hl_last := ""
|
||||
for r in rows_orch:
|
||||
@@ -199,11 +203,11 @@ func _init() -> void:
|
||||
"限次:第二次标错不获豁免(forgiven 恒 1——when counter lt 1)〔卡② R2〕")
|
||||
Kit.check_quiet(not orch.perfect_sweep_earned(),
|
||||
"判定相减:误标 2 − 豁免 1 = 有效 1 → 完美不成立〔卡② R2 真值表〕")
|
||||
orch._perfect_hit_false = 1
|
||||
orch._perfect_hit_true = 1
|
||||
orch._facts.perfect_hit_false = 1
|
||||
orch._facts.perfect_hit_true = 1
|
||||
Kit.check_quiet(orch.perfect_sweep_earned(),
|
||||
"判定相减:误标1−豁免1=有效0且标中≥1→完美成立(保险每战限次一次迁移保绿)〔无状态化 V7〕")
|
||||
orch._perfect_hit_true = 0
|
||||
orch._facts.perfect_hit_true = 0
|
||||
Kit.check_quiet(not orch.perfect_sweep_earned(),
|
||||
"判定守卫:标中数 0 时零误标也不成立(整图 ≥1 标中条件零松动)〔卡② R2 真值表〕")
|
||||
var rig_rng2 = RngScript.new()
|
||||
@@ -218,7 +222,7 @@ func _init() -> void:
|
||||
var rows_orch2: Array[Dictionary] = []
|
||||
orch2.step_row.connect(func(row): rows_orch2.append(row)) # step_row 公开信号面(insurance_used 载荷=豁免账观测面)
|
||||
var sc2 = safe_cells[0]
|
||||
orch2._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1)
|
||||
orch2._rounds._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1)
|
||||
var ins_row2 := {}
|
||||
for r in rows_orch2:
|
||||
if String(r.get("effect_id", "")) == "insurance_used":
|
||||
@@ -245,7 +249,7 @@ func _init() -> void:
|
||||
if not o10.minesweeper.current_board().is_mine(c10):
|
||||
sc10 = c10
|
||||
break
|
||||
o10._grant_seg._judge_seal({"cell": sc10, "hit": false}, 1)
|
||||
o10._rounds._grant_seg._judge_seal({"cell": sc10, "hit": false}, 1)
|
||||
var sm10: Dictionary = {}
|
||||
for r10 in rows10:
|
||||
if String(r10.get("type", "")) == "seal_miss":
|
||||
@@ -272,14 +276,14 @@ func _init() -> void:
|
||||
if not o11.minesweeper.current_board().is_mine(c11):
|
||||
sc11 = c11
|
||||
break
|
||||
o11._grant_seg._judge_seal({"cell": sc11, "hit": false}, 1)
|
||||
o11._rounds._grant_seg._judge_seal({"cell": sc11, "hit": false}, 1)
|
||||
var sm11: Dictionary = {}
|
||||
for r11 in rows11:
|
||||
if String(r11.get("type", "")) == "seal_miss":
|
||||
sm11 = r11
|
||||
Kit.check_quiet(String(sm11.get("highlight", "")) == "",
|
||||
"A11 豁免:首次标错获豁免 → seal_miss 行 highlight==空串〔卡① N7〕")
|
||||
o11._perfect_hit_true = 1
|
||||
o11._facts.perfect_hit_true = 1
|
||||
Kit.check_quiet(o11.perfect_sweep_earned(),
|
||||
"A11 真值:误标 1 − 豁免 1 = 有效 0 且标中≥1 → 完美成立(与 highlight==空 一致)〔卡① N7〕")
|
||||
|
||||
|
||||
@@ -570,6 +570,7 @@ func _smoke_level_graph() -> void:
|
||||
var green_ok := true
|
||||
var green_min_hp := 99999
|
||||
var green_wins := 0
|
||||
var green_hp_floor_ok := true # 胜局 HP>0 旗(绿档记录义务——败局样本不入账,胜局 0 血即红)
|
||||
for i in range(12):
|
||||
var g_runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(9100 + i))
|
||||
g_runner.start_level(2, 9100 + i)
|
||||
@@ -589,9 +590,11 @@ func _smoke_level_graph() -> void:
|
||||
green_wins += 1
|
||||
if int(g_box.get("hero_hp", 0)) > 0:
|
||||
green_min_hp = mini(green_min_hp, int(g_box.hero_hp))
|
||||
else:
|
||||
green_hp_floor_ok = false
|
||||
Kit.check("首遇位零踩雷:完美档 12 种子(节点 0 熊地精)恒零踩雷", green_ok)
|
||||
Kit.check("首遇战胜局 ≥7/12(实际 %d——阶段拔高后锚行退出绿档,多数胜口径)" % green_wins, green_wins >= 7)
|
||||
Kit.check("首遇战胜后 HP 下探读数 %d(>0——绿档记录)" % green_min_hp, green_min_hp > 0)
|
||||
Kit.check("首遇战胜后 HP 下探读数 %d(>0——绿档记录)" % green_min_hp, green_hp_floor_ok and green_min_hp < 99999)
|
||||
# ⑦ 跨表覆盖义务:L2 现行起关图每战斗/Boss 节点深度的敌侧落带 ≥ 请求深度(顶 clamp 消解即红)
|
||||
# +每深度 board_gen 精确命中(判据实现与〔v11〕种子扫描段同源——_enemy_band_covers/_board_row_exact 单点)
|
||||
var cov2 := _coverage_scan(2, nodes2)
|
||||
@@ -675,6 +678,7 @@ func _smoke_level_graph() -> void:
|
||||
# (零加成基线红档属预期——断言口径 = 成长代理档,非基线;跑法同 ⑥)
|
||||
var green3_ok := true
|
||||
var green3_min_hp := 99999
|
||||
var green3_hp_floor_ok := true # 胜局 HP>0 旗(绿档记录义务——胜局 0 血即红)
|
||||
for i in range(12):
|
||||
var g3_runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(9100 + i))
|
||||
g3_runner.set_hero_override(200, 100)
|
||||
@@ -693,8 +697,10 @@ func _smoke_level_graph() -> void:
|
||||
green3_ok = false
|
||||
elif int(g3_box.get("hero_hp", 0)) > 0:
|
||||
green3_min_hp = mini(green3_min_hp, int(g3_box.hero_hp))
|
||||
else:
|
||||
green3_hp_floor_ok = false
|
||||
Kit.check("L3 首遇位绿档:成长代理档口径(atk_pct=200/hp_pct=100)完美档 12 种子首遇战(节点 0 灰矮人)全胜且零踩雷", green3_ok)
|
||||
Kit.check("L3 首遇战胜后 HP 下探读数 %d(>0——绿档记录)" % green3_min_hp, green3_min_hp > 0)
|
||||
Kit.check("L3 首遇战胜后 HP 下探读数 %d(>0——绿档记录)" % green3_min_hp, green3_hp_floor_ok and green3_min_hp < 99999)
|
||||
|
||||
# ⑫ 混编行集运行面正反断言:注入带键 (1,battle,5)/(1,battle,6) =
|
||||
# L1 节点图与全部既有扫描面之外的新带键(带命中精确取注入行集——新 L 会被 ceil 封顶到 L3 不可用),
|
||||
|
||||
@@ -339,7 +339,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
adr_res.resume_adventure(bp_min, p_resume))
|
||||
Kit.check("继续上局受理面:tree_rules 随参数面进 runner._tree_rules(消费面 set_tree_rules 下传现成)",
|
||||
adr_res.runner() != null and adr_res.runner().pick_next_node(int(adr_res.runner().next_candidate_ids()[0]))
|
||||
and ((adr_res.runner().current_battle().encounter.get("tree_rules", []) as Array) == rules_e))
|
||||
and ((adr_res.runner().current_battle()._facts.encounter.get("tree_rules", []) as Array) == rules_e))
|
||||
_meta_smoke_cleanup()
|
||||
|
||||
|
||||
@@ -705,16 +705,16 @@ func _smoke_tree_rules() -> void:
|
||||
Kit.check("守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)",
|
||||
int(orch.action_economy.pending_ap_delta()) == 0)
|
||||
# 场景①:当回合新封印、结算判标错且封印仍在场 → queue +1(本轮 AP 零变化=queue 承载、下轮 notify_round_start 兑现)
|
||||
orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 1)
|
||||
orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 1)
|
||||
Kit.check("守①:当回合标错且封印在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)",
|
||||
int(orch.action_economy.pending_ap_delta()) == 1)
|
||||
# 场景③④:之前回合标错重判(封印在场状态)与同格重复——去重闸挡回,最多返还一次
|
||||
orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 2)
|
||||
orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 3)
|
||||
orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 2)
|
||||
orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 3)
|
||||
Kit.check("守③④:旧封印重判零触发+同格最多一次(_seal_miss_seen 去重闸=「当回合标错动作」判别器)",
|
||||
int(orch.action_economy.pending_ap_delta()) == 1)
|
||||
# 场景②负样本:封印命中(hit=true 正向兑付路径)零触发
|
||||
orch._grant_seg._judge_seal({"cell": safe, "hit": true}, 4)
|
||||
orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": true}, 4)
|
||||
Kit.check("守②负样本:标中/被挖封印零触发(标错事件只在 miss 分支发射——发射面过滤承载)",
|
||||
int(orch.action_economy.pending_ap_delta()) == 1)
|
||||
# 场景⑤:末轮判出→无下一轮=queue 不兑现(编排器每战新建——队列实例随战亡,合法作废态)
|
||||
@@ -746,7 +746,7 @@ func _smoke_tree_rules() -> void:
|
||||
"feats": [], "tree_rules": [], "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
|
||||
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
|
||||
orch3.encounter["tree_rules"] = []
|
||||
orch3._facts.encounter["tree_rules"] = []
|
||||
var gp_base: int = orch3._roll_gp_drop()
|
||||
var orch4 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
|
||||
Kit.check("聚 rig:增益编排启动(tree_rules=economy_hoard)",
|
||||
@@ -755,7 +755,7 @@ func _smoke_tree_rules() -> void:
|
||||
"feats": [], "tree_rules": ["economy_hoard"], "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
|
||||
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
|
||||
orch4.encounter["tree_rules"] = ["economy_hoard"]
|
||||
orch4._facts.encounter["tree_rules"] = ["economy_hoard"]
|
||||
var gp_hoard: int = orch4._roll_gp_drop()
|
||||
Kit.check("聚:战斗掉落乘算 ×1.5(provisional——同种子配对 %d→%d,入账前=结算框显示实得)",
|
||||
gp_hoard == int(round(float(gp_base) * 1.5)) and gp_hoard >= gp_base)
|
||||
|
||||
@@ -72,7 +72,7 @@ func _seal_propagation(db) -> bool:
|
||||
rng_a.set_master_seed(5)
|
||||
var oc_bg = OrchestratorScript.new(db, rng_a)
|
||||
var bi_bg = BoardInstallerScript.new({"db": db, "rng": rng_a, "minesweeper": oc_bg.minesweeper,
|
||||
"reward_economy": oc_bg._reward_economy, "encounter": oc_bg.encounter, "fatal_cb": oc_bg._fatal})
|
||||
"reward_economy": oc_bg._reward_economy, "encounter": oc_bg._facts.encounter, "fatal_cb": oc_bg._fatal})
|
||||
bi_bg.generator._fatal("测试违约①棋盘生成")
|
||||
if oc_bg.battle_result != "fatal_error":
|
||||
push_error("[run_time_fatal_test/wr5] 棋盘生成违约未传导封印(battle_result=%s)" % str(oc_bg.battle_result))
|
||||
|
||||
@@ -23,12 +23,15 @@ func _init() -> void:
|
||||
Kit.check_quiet(load("res://core/settlement/decay_segment.gd").new(oc) != null, "V4 DecaySegment 直接 new()〔迭代27 T2〕")
|
||||
Kit.check_quiet(load("res://core/settlement/verdict_segment.gd").new(oc) != null, "V4 VerdictSegment 直接 new()〔迭代27 T2〕")
|
||||
|
||||
# V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位)
|
||||
Kit.check_quiet(oc._grant_seg != null and oc._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕")
|
||||
Kit.check_quiet(oc._aura_seg != null and oc._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕")
|
||||
Kit.check_quiet(oc._combat_seg != null and oc._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕")
|
||||
Kit.check_quiet(oc._decay_seg != null and oc._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕")
|
||||
Kit.check_quiet(oc._verdict_seg != null and oc._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕")
|
||||
# V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位)——注册面批同代归约 V1:
|
||||
# 段件宿主直持改经 _rounds(轮循环驱动件)承载,接线断言改指 _rounds 段族
|
||||
Kit.check_quiet(oc._rounds != null and oc._rounds.oc == oc, "V1 编排器接线·轮循环驱动件在座且宿主回指〔编排归约 V1〕")
|
||||
Kit.check_quiet(oc._facts != null, "V1 编排器接线·战斗事实件在座〔编排归约 V1〕")
|
||||
Kit.check_quiet(oc._rounds._grant_seg != null and oc._rounds._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕")
|
||||
Kit.check_quiet(oc._rounds._aura_seg != null and oc._rounds._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕")
|
||||
Kit.check_quiet(oc._rounds._combat_seg != null and oc._rounds._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕")
|
||||
Kit.check_quiet(oc._rounds._decay_seg != null and oc._rounds._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕")
|
||||
Kit.check_quiet(oc._rounds._verdict_seg != null and oc._rounds._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕")
|
||||
# counter_add 写口真实路径(geologist 订阅行承载:公共命令链 begin_battle→move→dig→stop
|
||||
# 经授予段 counter_add 单点落 counters_ctx battle 桶——受理载荷+公开读数口 fx_host().counters_ctx)
|
||||
var rng_c = load("res://core/rng_service.gd").new()
|
||||
|
||||
@@ -162,13 +162,13 @@ func _init() -> void:
|
||||
"线香:装配起战(battle 级充能 1)")
|
||||
var inv8 = load("res://core/treasure_items.gd").new(db)
|
||||
var rec8: Dictionary = inv8.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
|
||||
ob8.treasure_inventory = inv8
|
||||
ob8.reward_grant().treasure_inventory = inv8
|
||||
var h8: int = int(rec8.get("handle", -1)) # handle 取 add_item 返回(snapshot 投影无 handle 键)
|
||||
var tgt8: Vector2i = _first_closed_safe(ob8)
|
||||
Kit.check_quiet(tgt8 != Vector2i(-9, -9), "线香:夹具存在未开安全格")
|
||||
var v8: Dictionary = ob8.reward_grant().use_treasure(h8, tgt8)
|
||||
Kit.check_quiet(bool(v8.get("ok", false)), "线香正常:单格真视受理(cell 目标解析)")
|
||||
Kit.check_quiet(bool(ob8.treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8))
|
||||
Kit.check_quiet(bool(ob8.reward_grant().treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8))
|
||||
and ob8.minesweeper.is_revealed(tgt8) == false,
|
||||
"线香断言:目标格入真视叠加层且未被开格(真视≠开格——信息红线)")
|
||||
Kit.check_quiet(inv8.snapshot_inventory().is_empty() or bool(inv8.snapshot_inventory()[0].get("used", false)),
|
||||
@@ -240,7 +240,7 @@ func _init() -> void:
|
||||
"天眼:装配起战(level 级充能 1)")
|
||||
var inv11 = load("res://core/treasure_items.gd").new(db)
|
||||
var rec11: Dictionary = inv11.add_item("sky_eye", "attunement", Vector2i(-1, -1), 0)
|
||||
ob11.treasure_inventory = inv11
|
||||
ob11.reward_grant().treasure_inventory = inv11
|
||||
var h11: int = int(rec11.get("handle", -1))
|
||||
var tgt11: Vector2i = _first_closed_safe(ob11)
|
||||
var v11: Dictionary = ob11.reward_grant().use_treasure(h11, tgt11)
|
||||
@@ -254,13 +254,13 @@ func _init() -> void:
|
||||
if ob11.minesweeper.is_revealed(c):
|
||||
opened_safe = false
|
||||
Kit.check_quiet(mines_shown == 0 or opened_safe, "天眼反例①:危险格仅显示不被开格(棋盘事实不可变)")
|
||||
var overlay11: Array = ob11.treasure_inventory.query_overlays().get("mine_reveal", [])
|
||||
var overlay11: Array = ob11.reward_grant().treasure_inventory.query_overlays().get("mine_reveal", [])
|
||||
var overlay_ok := true
|
||||
for c_v in (v11.get("resolved", []) as Array):
|
||||
if not overlay11.has(c_v):
|
||||
overlay_ok = false
|
||||
Kit.check_quiet(overlay_ok, "天眼正常:窗口全格入真视叠加层(危险格显示=C5 有差别开格)")
|
||||
Kit.check_quiet(not ob11.minesweeper.clear_map_cleared() or true, "天眼开格走普通挖格链(连锁/资源兑付=fx_reveal 通道——免 AP 即效果本身)")
|
||||
Kit.check_quiet(not ob11.minesweeper.clear_map_cleared(), "天眼开格走普通挖格链(连锁/资源兑付=fx_reveal 通道——免 AP 即效果本身)")
|
||||
|
||||
# ⑫ 信标网络(封印不限相邻/封印价照付)
|
||||
var rng12 = load("res://core/rng_service.gd").new(); rng12.set_master_seed(311)
|
||||
@@ -289,7 +289,7 @@ func _init() -> void:
|
||||
"计数链:装配起战(GainJournal 携带)")
|
||||
var inv13 = load("res://core/treasure_items.gd").new(db)
|
||||
var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
|
||||
ob13.treasure_inventory = inv13
|
||||
ob13.reward_grant().treasure_inventory = inv13
|
||||
var h13: int = int(rec13.get("handle", -1))
|
||||
var tgt13: Vector2i = _first_closed_safe(ob13)
|
||||
if tgt13 != Vector2i(-9, -9):
|
||||
@@ -306,8 +306,13 @@ func _init() -> void:
|
||||
"装配不可变:双件装配冒险启动")
|
||||
Kit.check_quiet(adr14.runner().pick_next_node(int(adr14.runner().next_candidate_ids()[0])), "装配不可变:进首战")
|
||||
var ob14 = adr14.runner().current_battle()
|
||||
Kit.check_quiet(ob14 != null and (ob14.attunement as Array) == ["morning_order", "wide_scout_band"],
|
||||
Kit.check_quiet(ob14 != null and (ob14._facts.attunement as Array) == ["morning_order", "wide_scout_band"],
|
||||
"装配不可变:战斗运行态装配集与冒险装配集逐位同源(运行零写口——快照对比)")
|
||||
# 注册面批同代编排归约 V5:库存注入改址(level_runner 注入改经 reward_grant() 出口直注)后
|
||||
# 引用同一性——战斗域经出口持有的库存与关卡域同一实例;use_treasure 受理链经新地址走通
|
||||
# 由本套件 ob8/ob11/ob13 三段既有受理链承载(注入面同批改址后全绿即证据)。
|
||||
Kit.check_quiet(ob14 != null and ob14.reward_grant().treasure_inventory == adr14.runner().treasure_inventory,
|
||||
"库存注入改址:战斗域经 reward_grant 出口持有的库存与关卡域同一引用〔编排归约 V5〕")
|
||||
|
||||
# ⑮ 扩展行收敛(迭代 38 卡④ wr2④⑤——套件首次 feats 注入,V6 形态自 round_flow_skill_pool_test 移植:
|
||||
# enc 带 feats 键+ledger,专长触发经 _judge_seal 真实封印判定管线)
|
||||
@@ -332,7 +337,7 @@ func _init() -> void:
|
||||
Kit.check_quiet(mines15.size() == 3, "封印匠:板上存在三枚危险格(断言前提)")
|
||||
if mines15.size() == 3:
|
||||
for mv in mines15:
|
||||
ob15._grant_seg._judge_seal({"cell": mv, "hit": true}, 1)
|
||||
ob15._rounds._grant_seg._judge_seal({"cell": mv, "hit": true}, 1)
|
||||
var smith_row := {}
|
||||
for r in rows15:
|
||||
if String(r.get("effect_id", "")) == "mine_neutralized":
|
||||
@@ -354,7 +359,7 @@ func _init() -> void:
|
||||
cell15b = cv
|
||||
break
|
||||
if cell15b != null:
|
||||
ob15b._grant_seg._judge_seal({"cell": cell15b, "hit": true}, 1)
|
||||
ob15b._rounds._grant_seg._judge_seal({"cell": cell15b, "hit": true}, 1)
|
||||
var beacon_row := {}
|
||||
for r in rows15b:
|
||||
if String(r.get("effect_id", "")) == "fx_preview_area":
|
||||
|
||||
@@ -58,7 +58,7 @@ func _init() -> void:
|
||||
main._start_adventure_from_menu()
|
||||
await process_frame
|
||||
# 入口停点选路进首战(真实停点链)
|
||||
if not main.level_screen.visible:
|
||||
if not main.ui.modals.level_screen.visible:
|
||||
_problems.append("入口停点未呈现(前置失守)")
|
||||
_finish()
|
||||
return
|
||||
@@ -100,12 +100,12 @@ func _init() -> void:
|
||||
# ---- 断言①:排空前后核心状态逐位一致(演出关停零分叉)----
|
||||
var snap_before: String = JSON.stringify(battle.diagnostic_snapshot())
|
||||
var hp_before: int = int(battle.action_economy.ap) + int(battle.action_economy.rap) * 0 # 观测面双录
|
||||
main._flush_row_queue()
|
||||
main.ui.screens.row_pacer.flush()
|
||||
var snap_after: String = JSON.stringify(battle.diagnostic_snapshot())
|
||||
check(snap_before == snap_after, "排空前后核心诊断快照逐位一致(AC⑤ 演出关停零分叉)")
|
||||
# ---- 断言②:零丢行(结构判据:排空后队列恒空 + 末信号行原文在带——append_row 直落不吞行)----
|
||||
check(rows_seen.size() > 0, "结算行已产生(断言前提)")
|
||||
check(main._row_queue.is_empty(), "排空后队列恒空(flush 完整性)")
|
||||
check(main.ui.screens.row_pacer.pending_rows().is_empty(), "排空后队列恒空(flush 完整性)")
|
||||
var last_text := ""
|
||||
for i in range(rows_seen.size() - 1, -1, -1):
|
||||
var cand_row: Dictionary = rows_seen[i]
|
||||
@@ -114,7 +114,7 @@ func _init() -> void:
|
||||
last_text = String(cand_row.get("display_name", ""))
|
||||
if last_text != "":
|
||||
break
|
||||
check(last_text != "" and main.log_view.parsed_text().contains(last_text),
|
||||
check(last_text != "" and main.ui.battle.log_view.parsed_text().contains(last_text),
|
||||
"末信号行原文在明细带(排空不吞行:『%s』)" % last_text)
|
||||
# ---- 断言③:段间延迟取数面双态 ----
|
||||
UiKit.motion_scale = 1.0
|
||||
@@ -129,7 +129,7 @@ func _init() -> void:
|
||||
var fstage := FakeStage.new(null)
|
||||
var ffx := FakeFx.new()
|
||||
rp.stage = fstage
|
||||
rp.board_view = main.board_view
|
||||
rp.board_view = main.ui.battle.board_zone.board_view
|
||||
rp.settlement_fx = ffx
|
||||
rp._play_settlement_mark({"type": "damage", "target": "地精", "target_id": "goblin_b", "value": 3})
|
||||
check(fstage.calls.size() == 1 and fstage.calls[0] == ["goblin_b", "地精"],
|
||||
@@ -144,9 +144,9 @@ func _init() -> void:
|
||||
UiKit.motion_scale = 1.0 # 常态入队(减动效快路同帧直落不入队——夹具须真落队列)
|
||||
main._on_step_row({"type": "damage", "target": "hero", "value": 1})
|
||||
main._on_step_row({"type": "heal", "target": "hero", "value": 2})
|
||||
check(main._row_queue.size() == 2, "会话更替夹具已入队(前提:%d 行)〔wr4〕" % main._row_queue.size())
|
||||
check(main.ui.screens.row_pacer.pending_rows().size() == 2, "会话更替夹具已入队(前提:%d 行)〔wr4〕" % main.ui.screens.row_pacer.pending_rows().size())
|
||||
main._begin_session(false)
|
||||
check(main._row_queue.is_empty(), "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕")
|
||||
check(main.ui.screens.row_pacer.pending_rows().is_empty(), "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕")
|
||||
_finish()
|
||||
|
||||
|
||||
|
||||
@@ -722,8 +722,8 @@ func _smoke_diag_button_retired() -> void:
|
||||
var main_src: String = FileAccess.get_file_as_string("res://presentation/main.gd")
|
||||
Kit.check("V6 主区无 build_diag_button 调用残留(旧实现同批清理)",
|
||||
not main_src.contains("build_diag_button"))
|
||||
Kit.check("V6 转储入口连接随迁(组合根接浮窗 diagnose_requested 信号)",
|
||||
main_src.contains("debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)"))
|
||||
Kit.check("V6 转储入口连接随迁(组合根经注册面接浮窗 diagnose_requested 信号)〔注册面 V2〕",
|
||||
main_src.contains("ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)"))
|
||||
Kit.check("V6 转储编排承接函数在位(dump_diagnosis 卡死取证保留)",
|
||||
main_src.contains("func _on_diagnose_pressed()"))
|
||||
var asm_src: String = FileAccess.get_file_as_string("res://presentation/ui_assembler.gd")
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
# 双实现对拍配对套件(注册面批同代测速循环批 v1.1——帧相位等价的执行化证据):
|
||||
# 产品码(process_frame 信号步进状态机)vs 夹具(现行 await 形态逐字副本——循环内 await+收尾四语句在 run() 尾)。
|
||||
# 五面对拍(帧索引序列/运行态序列/进度串序列/stats 六键/报告全文)逐位相等=
|
||||
# 每步恰一帧+相位同点+输出逐字的当轮真实执行证据(测量语义逐点等价四判据的落码面)。
|
||||
# 拓扑钉死:测试宿主先连 process_frame 帧计数、后启动两形态——两形态下「计数先于步进体」相对次序同构,序列可比。
|
||||
# n=3 常规案+n=0 退化案(同步收尾零帧——_mean/_p90 空守卫渲染面)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/tune_sim_batch_parity_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const TuneSimScript := preload("res://presentation/tune_sim_controller.gd")
|
||||
|
||||
|
||||
# 假调试面板(wd8 同形 setter 面+进度/运行态/报告三记录数组供序列对拍)
|
||||
class FakePanel extends Node:
|
||||
var running := false
|
||||
var progress := ""
|
||||
var report := ""
|
||||
var atk := 0
|
||||
var hp := 0
|
||||
var progress_log: Array[String] = []
|
||||
var report_log: Array[String] = []
|
||||
var running_log: Array[bool] = []
|
||||
|
||||
func atk_pct() -> int:
|
||||
return atk
|
||||
|
||||
func hp_pct() -> int:
|
||||
return hp
|
||||
|
||||
func set_sim_running(r: bool) -> void:
|
||||
running = r
|
||||
running_log.append(r)
|
||||
|
||||
func set_sim_progress(t: String) -> void:
|
||||
progress = t
|
||||
progress_log.append(t)
|
||||
|
||||
func set_sim_report(t: String) -> void:
|
||||
report = t
|
||||
report_log.append(t)
|
||||
|
||||
|
||||
# 假自动策略(固定返回值两档 victory/defeat 交替——wd8 :53-55 同款取值,零随机;
|
||||
# 逐局记录帧索引与运行态读数——帧索引经宿主注入的计数 Callable 读取)
|
||||
class FakeDriver extends RefCounted:
|
||||
var ctrl
|
||||
var frame_counter: Callable # 宿主帧计数(0 参 Callable——拓扑钉死:计数先于步进体)
|
||||
var seen: Array[bool] = []
|
||||
var frames: Array[int] = []
|
||||
var calls := 0
|
||||
|
||||
func run_level(_db: Variant, _seed: int, _level: int, _atk: int, _hp: int, _other: bool, _logger: Variant) -> Dictionary:
|
||||
calls += 1
|
||||
seen.append(ctrl.is_running())
|
||||
frames.append(int(frame_counter.call()))
|
||||
if calls % 2 == 1:
|
||||
return {"hero_dmg": 12.0, "rounds": 4, "result": "victory", "hp_left": 3.0, "hp_at_boss": 5.0}
|
||||
return {"hero_dmg": 6.0, "rounds": 2, "result": "defeat",
|
||||
"defeat_node_index": 3, "defeat_node_type": "battle", "hp_left": 0.0, "hp_at_boss": 0.0}
|
||||
|
||||
|
||||
# 假配置库(TuneSimController 只读 db.run.action_economy.ap_per_round——wd8 同款最小域对象图)
|
||||
class FakeActionEconomy:
|
||||
var ap_per_round := 2
|
||||
|
||||
|
||||
class FakeRun:
|
||||
var action_economy := FakeActionEconomy.new()
|
||||
|
||||
|
||||
class FakeDB:
|
||||
var run := FakeRun.new()
|
||||
|
||||
|
||||
# 现行 await 形态逐字副本(tune_sim_controller.gd run(n) 改形前形态——循环内 await+收尾四语句在 run() 尾;
|
||||
# 全仓唯一在测夹具——产品码禁留第二实现,tests/ 不在 S13/S8/S9/S10 扫描面、夹具内循环 await 合法在册)。
|
||||
# 与产品码新形态的申报差异仅一处:批末把 stats 六键束存 out_stats 成员供对拍读取
|
||||
# (产品码 stats 经报告公开面观测——内部字段直查受控例外申报在卡 §17)。
|
||||
class AwaitLoopRunner extends RefCounted:
|
||||
const SEED_BASE := 4200
|
||||
const ParityLogger := preload("res://presentation/playtest_logger.gd")
|
||||
var db
|
||||
var panel
|
||||
var _base_hp := 0
|
||||
var _driver = null
|
||||
var _running := false
|
||||
var out_stats: Dictionary = {}
|
||||
|
||||
func _init(db_ref, panel_ref, base_hp: int, driver = null) -> void:
|
||||
db = db_ref
|
||||
panel = panel_ref
|
||||
_base_hp = base_hp
|
||||
_driver = driver
|
||||
|
||||
func is_running() -> bool:
|
||||
return _running
|
||||
|
||||
func run(n: int) -> void:
|
||||
_running = true
|
||||
var logger = ParityLogger.new()
|
||||
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
|
||||
logger.start_session(SEED_BASE)
|
||||
panel.set_sim_running(true)
|
||||
panel.set_sim_progress("模拟中:0/%d …" % n)
|
||||
var stats := {"wins": 0, "dmg_all": [], "hp_left_win": [], "hp_boss": [], "rounds_all": [], "death_nodes": {}}
|
||||
var autoplay = _driver
|
||||
for i in range(n):
|
||||
_run_one_level(autoplay, stats, i, panel, logger)
|
||||
panel.set_sim_progress("模拟中:%d/%d …" % [i + 1, n])
|
||||
await panel.get_tree().process_frame
|
||||
logger.close_session()
|
||||
panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), stats, _base_hp))
|
||||
panel.set_sim_running(false)
|
||||
_running = false
|
||||
out_stats = stats
|
||||
|
||||
func _run_one_level(autoplay, stats: Dictionary, i: int, panel, logger) -> void: # type-hint: ignore(驱动/面板/日志件鸭子型注入——同产品码构造缝)
|
||||
var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger)
|
||||
(stats["dmg_all"] as Array).append(st.hero_dmg)
|
||||
(stats["rounds_all"] as Array).append(st.rounds)
|
||||
if st.result == "victory":
|
||||
stats["wins"] = int(stats["wins"]) + 1
|
||||
(stats["hp_left_win"] as Array).append(st.hp_left)
|
||||
(stats["hp_boss"] as Array).append(st.hp_at_boss)
|
||||
elif st.result == "defeat":
|
||||
var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type]
|
||||
stats["death_nodes"][key] = int(stats["death_nodes"].get(key, 0)) + 1
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var frame_idx := {"i": -1} # 帧计数(宿主最先连——拓扑钉死「计数先于步进体」)
|
||||
process_frame.connect(func() -> void: frame_idx["i"] += 1)
|
||||
await process_frame # 树初始化就绪帧(SceneTree._init 时点 root 未挂树——面板 get_tree() 首帧后才有效;wd8 同款先例:panel 段前有 await 过帧)
|
||||
var r3 := await _run_pair(3, frame_idx)
|
||||
_check_n3(r3)
|
||||
var r0 := await _run_pair(0, frame_idx)
|
||||
_check_n0(r0)
|
||||
quit(Kit.finish("tune_sim_batch_parity"))
|
||||
|
||||
|
||||
# 对拍驱动:两形态各经 run(n) 真实入口(同参假面板/假驱动各自实例);先启动两形态、后分别 await——
|
||||
# 两形态在调用点同一同步段内先后挂起(并行于同一帧时钟),驱动协程返回 int 承接 void run 的可等待面
|
||||
func _run_pair(n: int, frame_idx: Dictionary) -> Dictionary:
|
||||
# 两形态串行驱动(各经 run(n) 真实入口 await 调用——GDScript 协程动态调用必须 await,
|
||||
# 无并行启动形态):帧序列各自记录、断言按相对首帧偏移归一后逐位比对——拓扑不变式
|
||||
# 「计数先连、启动后于计数、计数先于步进体」在两形态下相对次序同构,等价证据结构不变。
|
||||
var panel_a := FakePanel.new()
|
||||
root.add_child(panel_a)
|
||||
var drv_a := FakeDriver.new()
|
||||
var a: Variant = TuneSimScript.new(FakeDB.new(), panel_a, 100, drv_a)
|
||||
drv_a.ctrl = a
|
||||
drv_a.frame_counter = func() -> int: return frame_idx["i"]
|
||||
await a.run(n)
|
||||
var panel_b := FakePanel.new()
|
||||
root.add_child(panel_b)
|
||||
var drv_b := FakeDriver.new()
|
||||
var b: Variant = AwaitLoopRunner.new(FakeDB.new(), panel_b, 100, drv_b)
|
||||
drv_b.ctrl = b
|
||||
drv_b.frame_counter = func() -> int: return frame_idx["i"]
|
||||
await b.run(n)
|
||||
return {"a": a, "b": b, "pa": panel_a, "pb": panel_b, "da": drv_a, "db": drv_b}
|
||||
|
||||
|
||||
func _check_n3(r: Dictionary) -> void:
|
||||
var pa: FakePanel = r["pa"]
|
||||
var pb: FakePanel = r["pb"]
|
||||
var da: FakeDriver = r["da"]
|
||||
var drv_b: FakeDriver = r["db"]
|
||||
var a = r["a"]
|
||||
var b = r["b"]
|
||||
Kit.check("n=3 帧序列相对偏移逐位相等(%s vs %s)+相邻差恒一(每步恰一帧)〔测速 V1〕",
|
||||
str(_norm(da.frames)) == str(_norm(drv_b.frames)) and da.frames.size() == 3
|
||||
and _consecutive(da.frames) and _consecutive(drv_b.frames))
|
||||
Kit.check("n=3 运行态读数序列逐位相等+收尾双运行态假+面板置假序列同构〔测速 V2〕",
|
||||
str(da.seen) == str(drv_b.seen) and da.seen == [true, true, true]
|
||||
and not a.is_running() and not b.is_running()
|
||||
and not pa.running and not pb.running
|
||||
and str(pa.running_log) == str(pb.running_log))
|
||||
Kit.check("n=3 进度串序列逐位相等(%d 段)〔测速 V3〕",
|
||||
str(pa.progress_log) == str(pb.progress_log) and pa.progress_log.size() == 4)
|
||||
var got: Dictionary = r["b"].out_stats
|
||||
var deaths: Dictionary = got.get("death_nodes", {})
|
||||
Kit.check("n=3 夹具 stats 六键与按假驱动固定返回值重算的期望逐键相等〔测速 V4〕",
|
||||
int(got.get("wins", -1)) == 2
|
||||
and _arr_eq(got.get("dmg_all"), [12.0, 6.0, 12.0])
|
||||
and _arr_eq(got.get("hp_left_win"), [3.0, 3.0])
|
||||
and _arr_eq(got.get("hp_boss"), [5.0, 5.0])
|
||||
and _arr_eq(got.get("rounds_all"), [4, 2, 4])
|
||||
and deaths.size() == 1 and int(deaths.get("3.battle", -1)) == 1)
|
||||
Kit.check("n=3 报告全文逐字相等且含渲染面四关键串〔测速 V5〕",
|
||||
pa.report == pb.report and pa.report != ""
|
||||
and pa.report.contains("关卡完成率") and pa.report.contains("关卡完成剩余 HP 均")
|
||||
and pa.report.contains("败北节点") and pa.report.contains("N=3"))
|
||||
|
||||
|
||||
func _check_n0(r: Dictionary) -> void:
|
||||
var pa: FakePanel = r["pa"]
|
||||
var pb: FakePanel = r["pb"]
|
||||
var da: FakeDriver = r["da"]
|
||||
var drv_b: FakeDriver = r["db"]
|
||||
var a = r["a"]
|
||||
var b = r["b"]
|
||||
Kit.check("n=0 退化:两形态驱动零回调、帧索引序列空(同步收尾零帧)〔测速 V1〕",
|
||||
da.frames.is_empty() and drv_b.frames.is_empty() and da.calls == 0 and drv_b.calls == 0)
|
||||
Kit.check("n=0 运行态置假序列 [true, false] 双形态同构+收尾双假〔测速 V2〕",
|
||||
str(pa.running_log) == str(pb.running_log) and pa.running_log == [true, false]
|
||||
and not a.is_running() and not b.is_running())
|
||||
Kit.check("n=0 进度串单段逐字相等〔测速 V3〕",
|
||||
str(pa.progress_log) == str(pb.progress_log) and pa.progress_log == ["模拟中:0/0 …"])
|
||||
Kit.check("n=0 夹具 stats 六键零计〔测速 V4〕",
|
||||
int((r["b"].out_stats as Dictionary).get("wins", -1)) == 0
|
||||
and ((r["b"].out_stats as Dictionary).get("dmg_all") as Array).is_empty()
|
||||
and ((r["b"].out_stats as Dictionary).get("hp_left_win") as Array).is_empty()
|
||||
and ((r["b"].out_stats as Dictionary).get("hp_boss") as Array).is_empty()
|
||||
and ((r["b"].out_stats as Dictionary).get("rounds_all") as Array).is_empty()
|
||||
and ((r["b"].out_stats as Dictionary).get("death_nodes") as Dictionary).is_empty())
|
||||
Kit.check("n=0 报告全文逐字相等且非空(空 stats 渲染面)〔测速 V5〕",
|
||||
pa.report == pb.report and pa.report != "")
|
||||
|
||||
# 帧序列归一(相对首帧偏移——串行驱动下两形态帧段错位、结构可比性归一承载)
|
||||
func _norm(fr: Array) -> Array:
|
||||
var out: Array = []
|
||||
var base := int(fr[0]) if fr.size() > 0 else 0
|
||||
for i in fr.size():
|
||||
out.append(int(fr[i]) - base)
|
||||
return out
|
||||
|
||||
|
||||
# 相邻差恒一(每步恰占一帧——判据①的序列面证据)
|
||||
func _consecutive(fr: Array) -> bool:
|
||||
for i in range(1, fr.size()):
|
||||
if int(fr[i]) - int(fr[i - 1]) != 1:
|
||||
return false
|
||||
return true
|
||||
|
||||
# 数组逐元素浮点比对(stats 桶重算期望——类型化比较,消 str 形态歧义)
|
||||
func _arr_eq(a, b: Array) -> bool:
|
||||
var aa := a as Array
|
||||
if aa == null or aa.size() != b.size():
|
||||
return false
|
||||
for i in b.size():
|
||||
if absf(float(aa[i]) - float(b[i])) > 0.0001:
|
||||
return false
|
||||
return true
|
||||
@@ -0,0 +1,26 @@
|
||||
# 迭代 44 反哺点清单——2026-10-30
|
||||
|
||||
要写进权威文档的点逐条列落点(改哪一节、改成什么、依据);用户在会话中裁决后归文档批落文。实现批禁写 `design/` 与根规则文件,本清单是唯一移交通道。
|
||||
|
||||
## 架构文档(design/engineering/architecture/architecture.md)
|
||||
|
||||
1. **「模块归属·呈现层」ui_registry 行现状化**——现登记「实现批建件」态;实现后改现状陈述:UiRegistry/UiScreens/UiBattle/UiModals 四件已建(全 RefCounted、类变量 3/9/5/14、UiModals.flow 随容器构造即建)。
|
||||
2. **同表 main_ui_build 行「层序逻辑不在此件——唯一事实源仍是 main.LAYER_CONTRACT + _apply_layer_contract()」句失效**——唯一事实源已迁 `ui_registry.LAYER_CONTRACT`+`ui.apply_layer_contract(root)`;该句改指 ui_registry(依据=设计文档 §3.3 建议句+实现实况)。
|
||||
3. **「呈现层装配与会话生命周期」节屏成员寻址迁注册面句核对**——v1.25 已预写「探针挂载假设=main.ui.<域>.<句柄>」;实现实况与其一致,但需补两句实况细节:①ModalFlow 实例随 UiModals 容器构造建立(装配早期注入点依赖其先在座);②探针契约面新增两件取证探针(shot_probe/fx_shot_probe)随批改址。
|
||||
4. **「战斗编排域拆件定稿」节轴丁两行「构造=引用束注入」句与实态口径差**——实态=RoundCycle 三参直构(host/fx_host_ref/reward_grant_ref,五段构造透传不落成员)、BattleFacts 零参直构;句按设计文档 §8 预判「三参直构无需束」现状化。
|
||||
5. **「模块归属·功能层」RoundCycle/BattleFacts 两行实测值换代**——RoundCycle 类变量 8/公开方法 3;BattleFacts 类变量 15(贴顶)/公开方法 4(reset_round+三 mark);宿主 battle_orchestrator 实测 15 成员/811 行(文件长键 1035 已远超实况——基线 refresh 后自然消失)。
|
||||
6. **「测试与质量域」S13 豁免第三条注销句现状化**——v1.25 预写「设计闭合、实现批注销」;键账已达成(宿主实测 15),注销句转已实施事实。**同节 tune_sim 测量面豁免注销句现状化随卡③ v1.1 恢复**:裸键已消除(实测命中消失、全文件零循环);注销句保持(注销对象是 await 豁免、不依赖行为改动申报),句内「随批申报行为改动一处(收尾置假还原)」半句已失效(前提被迭代 41 测试债批 215da315 消化、申报已剥离)——现状化时一并修正为收尾现行陈述(「运行态收尾在 run() 尾每批一次执行」)。
|
||||
|
||||
## gdstyle 基线与闸面(tools/)
|
||||
|
||||
7. **基线 refresh 义务(用户人工,非落文)**——`python tools/gdstyle_gate.py --refresh`:预期消失键=main.gd max-class-variables=49(实测 15)+battle_orchestrator max-class-variables=38(实测 15)+battle_orchestrator max-file-length=1035(实测 811)+轴丙遗留归绿键;合计消失 ≥2 条触发骤降守卫,须 `--force`。与待办清单「零豁免五键清偿批」批末一次人工 refresh 合并执行。
|
||||
8. **S13 no-self-assign 检查的误报形态观察**(登记不阻塞)——`X.y.z = y` 形态(赋值链尾名与右侧成员同名)被判自赋值;本批以 self 前缀消解(level_runner:520)。若后续批次再现此巧合,可考虑工程批把检查器改为比较完整接收链(登记归「迭代 42 批末移交小件」同类工具小件)。
|
||||
|
||||
## 待办清单(根流程清单——状态回退拟稿,落笔归上游段/用户)
|
||||
|
||||
9. **「零豁免五键清偿批」条目测速循环批子项状态(净零处理——原回退拟稿作废)**:卡③ 停卡呈报→用户裁重裁→写卡会话重写 v1.1(零待裁点,重裁时订正 S8 十文件与两处行号注记)→本会话恢复施工收口——条目状态直推「文档设计已闭合+已写卡+已编迭代 44」无回退中间态;重裁经过实况记录于实现报告 §5/§5.1,本条不再承载状态回退义务。
|
||||
10. **「零豁免五键清偿批」条目键账进度换代**——五键中两键已真实清偿(main.gd 49→15、battle_orchestrator 37→15),待基线 refresh 收账;呈现层状态件批(迭代 45 在册)两键不受影响。
|
||||
|
||||
## 其他
|
||||
|
||||
11. **main.gd 死 preload 存量**(工程小件登记建议)——拆件遗留的零使用 preload 常量 8 条(UiAssembler/TuneSimController/DebugSessionScript/TreasureInteractController/CurioInteractController/CampShopPresenter/BoardResolversScript/三场景 preload 中 main 零使用者);本批仅清理与迁移直接相关的 ModalStack/RowPacer 两条,其余属存量债不扩范围;归工程改动直改批顺路清理。
|
||||
@@ -0,0 +1,118 @@
|
||||
# 迭代 44 实现报告(组合根注册面批+战斗编排归约批+测速循环批)——2026-10-30
|
||||
|
||||
状态:三卡全部施工收口(卡③ 按停卡呈报建议案重裁为 v1.1 后施工);迭代总收口(批末闸/审计/合并)进行中。
|
||||
|
||||
## 0. 各卡状态
|
||||
|
||||
| 卡 | 状态 | 提交(worktree iter44,基点 c8f5091d) |
|
||||
|---|---|---|
|
||||
| 卡① 组合根注册面批 | **收口**——main.gd 49→15、四新件建件、全消费面改址 | bee0682f(T1 建件)/b89ee850(T2 改形)/784d581b(T3 改址)/1929516a(T4 消灭收口) |
|
||||
| 卡② 战斗编排归约批 | **收口**——battle_orchestrator 38→15(实测 37 轴丙基数起算)、RoundCycle/BattleFacts 两新件 | 24f3ca51(T1 BattleFacts)/a832965a(T2 RoundCycle)/15c833e0(T3 宿主收束)/3e9d3afa(T4 键账) |
|
||||
| 卡③ 测速循环批 | **收口(v1.1 重裁后施工)**——v1.0 停卡呈报→用户裁重裁→v1.1 剥离行为改动申报面后 T0–T3 全落 | c82ef23f(T1 步进状态机)/04783fae(T2 对拍套件) |
|
||||
|
||||
## 1. 卡① 实际对照表(卡片 §8 十一行 → 实况)
|
||||
|
||||
| # | 需求点 | 改动实况 | 验收测试实况 | 结果 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 注册面建件+合同机器迁驻 | ui_registry.gd(LAYER_CONTRACT 逐字迁入+node(key) 19 键 match+apply_layer_contract 唯一落位口)+ui_screens/ui_battle/ui_modals 三容器 | 系统测试 V1c(system_test.gd:94-106 改址 main.ui.LAYER_CONTRACT/main.ui.node,尾缀〔装配生命批 V1〕保留)——树序逐对保序 | 绿 |
|
||||
| 2 | main.gd 改形回归(49→15) | 28 迁出+7 消灭+1 新增=22(T2 中间态)→15(T4 终态);六转发方法+_flush_row_queue 退役 | run_smoke 15 项 0 失败+run_system_test 69 项 0 失败;S13 静态判据=gdstyle 实测 15(基线键 49 已不命中、成死键) | 绿 |
|
||||
| 3 | 装配与流程件改址 | main_ui_build 八步赋值目标 root.ui.*(八步顺序/add_child 序/连接语句逐条不变);三流程件同表映射;smoke_boot get() 4 改 2 留 | 冒烟主屏存活面六断言+系统测试 V2 句柄等价五连比(system_test.gd:108-109 改址) | 绿 |
|
||||
| 4 | 探针面批量改址 | 探针九件+probe_ui_util+shot_probe/fx_shot_probe(决定记录 2)共约 460 行机械改写 | probe_gate --click --full 全段退出码 0(九件 PASS) | 绿 |
|
||||
| 5 | 系统测试面改址+RunScope 生命周期 | system_test V1a/V1c/V2/V3 矩阵取址改指 | V1–V9 全绿(V3a–V3i 存活性矩阵第五引用面逐面同源) | 绿 |
|
||||
| 6 | 别名/接缝成员消灭(7 个) | _seed_edit/board_view/ap_bar/_row_queue/_modals/_settlement_stop_pending/_deferred_modal 全删;消费方改址(pacing_queue 4 处+接缝探针三件+main 自身) | pacing_queue 绿+ui_struct_probe 登记序断言(改 flow.stack)+t17_defer_probe 挂起态断言(改 flow._deferred) | 绿 |
|
||||
| 7 | 六转发模态方法退役 | _open_modal/_close_modal/_on_modal_closed/_input_locked/_open_modal_after_playback/_on_pump_ended 退役;调用方改指 ui.modals.flow 五真方法+pump_ended 注入改 flow 方法 | modal_flow 绿+系统测试 V3g 模态开合段+ui_click_probe 模态开合路径 | 绿 |
|
||||
| 8 | V1d 源码断言重锚 | source_guard_test.gd 重锚全 presentation 目录扫描(move_child 只许 ui_registry.apply_layer_contract 体内),尾缀〔注册面 V1〕 | source_guard 绿(8 checks) | 绿 |
|
||||
| 9 | V6 源码断言改串 | theme_projection_test.gd V6 改断言 ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed),尾缀〔注册面 V2〕 | theme_projection 绿(164 checks) | 绿 |
|
||||
| 10 | 视觉零变化主证 | 全批呈现层改址零像素改动 | render_gate golden 零差异(shot_probe 七屏+golden 全过) | 绿 |
|
||||
| 11 | 改址完整性收口 | 49 名逐名 grep 残留=0(词形态+字符串键形态两轮全量扫描,T3 提交信息留档) | 批末代码域闸归迭代总收口(待跑);grep 自检静态判据已留档 | 静态部分绿 |
|
||||
|
||||
静态判据申报执行实况(卡片 §8 域口径):S13 键账对账=gdstyle 实测 main.gd 15、四新件零新键(T1/T4 两轮 static_scan 确认);基线 refresh 留用户人工(见 §4 遗留义务)。
|
||||
|
||||
## 2. 卡② 实际对照表(卡片 §11 V0–V6 行 → 实况)
|
||||
|
||||
| 行 | 需求点 | 改动实况 | 验收测试实况 | 结果 |
|
||||
|---|---|---|---|---|
|
||||
| V0 | 行为零变化(总判据) | 成员迁移与改指;RNG 五流零触碰(behavior_roll 随件迁移取号序逐字);五信号留宿主;两快照签名载荷逐字 | 关联套件全绿零改址应红项(round_flow/settle_segments/skill_unlock/trigger_cond/seal_polarity/feat_vouchers/variant_placement/meta_forge/adventure_feats/treasure_effects/roll_multiplier/breakpoint_resume/level_shop_economy/table_structure_guard) | 绿 |
|
||||
| V1 | 两新件建件接线 | battle_facts.gd(15 成员贴顶+reset_round 逐字+三 mark 写口)+round_cycle.gd(8 成员+三参直构+五段构造逐字迁入) | settle_segments_test V1 断言(_rounds 在座且 oc 回指+_facts 在座+五段段件齐 _rounds)〔编排归约 V1〕 | 绿 |
|
||||
| V2 | reset_round 复位边界 | reset_round 只清 reveals/seals/path 三项 | round_flow_skill_pool_test 内嵌断言:终局后 presentation_snapshot()["refresh_used"] == 末轮结算快照 economy.refresh_used〔编排归约 V2〕 | 绿 |
|
||||
| V3 | 段驱动与摇号迁移 | 三函数迁出;宿主调用点五处改址;trigger_cond 3 处改指 | round_flow 全套+settle_segments V4-3+skill_unlock+trigger_cond(改指后绿)——同种子回放断言零改址钉住 | 绿 |
|
||||
| V4 | _hero_atk 派生读 | 成员删→私有函数;combat_segment :56 改调 | roll_multiplier_test 内嵌:override 贯通 spawn+缺省回退表值〔编排归约 V4〕;攻击单 base_value 贯通由既有 hero_atk_val=100 全链路夹具承载 | 绿 |
|
||||
| V5 | treasure_inventory 注入改址 | 宿主成员与 setter 删、五处读改指、level_runner:520 改经 reward_grant() 出口直注 | treasure_effects_test 内嵌引用同一性断言〔编排归约 V5〕+ob8/ob11/ob13 既有 use_treasure 受理链改址后全绿 | 绿 |
|
||||
| V6 | 封印判定账/装板溯源/受理事实迁移 | 事实族改指(grant_segment 18 处+宿主全量);三 mark 写口升级;attune 读源改 _facts | seal_polarity/feat_vouchers(契约 2 计数纯净 R2)/variant_placement/meta_forge/adventure_feats/table_structure_guard(attune 外部面零改址应绿——最硬的外部面零变化证)全绿 | 绿 |
|
||||
|
||||
行为改动申报核对:diff 中行为改动零条(全部非行为改动——成员迁移与改指;完美写口升级=同一时点同一增量改经方法单点)。测试削弱申报:零申报兑现(测试改指只动成员寻址;新增断言 V1/V2/V4/V5 四条+V1d/V6 重锚两条=卡片 §8 申报全集)。
|
||||
|
||||
## 3. 实现决定记录(代拍板白名单外事项备案)
|
||||
|
||||
卡①:
|
||||
1. **ModalFlow 实例随 UiModals 容器构造即建**(ui_modals._init)——装配第二步 row_pacer.pump_ended 注入早于第八步 flow 建立,转发壳退役后需 flow 先行在座;查询/钩子 Callable 注入时点不变(_build_modal_flow 内语句序不变),八步顺序零变化。
|
||||
2. **shot_probe/fx_shot_probe 纳入改址**——两件是 render_gate 取证探针、不在卡片「探针九件」清单(probe_gate 段构成),但同为 main.X 一级地址消费方;T3 改址完整性 grep 自检抓漏后纳入(render_gate 对照表行 10 的承载件,属消费面全量改址义务)。
|
||||
3. **ui_tune_probe 零连接巡检白名单登记 flow.close**——表 D 申报「closed bind 目标改 flow.close.bind」的机检联动:巡检白名单原钉 main._on_modal_closed(组合根转发壳),改钉 main.ui.modals.flow.close(同一连接的新宿主);判据面「closed 外发消费目标是模态编排口」语义不变,conn_count=11 断言零变化。
|
||||
4. **layout_probe 三区 get() 字符串键改容器字段键**——卡片表 D 字符串键清单只列 smoke_boot 6 处;layout_probe:60 `main.get(key)`(key="stage"/"info_panel"/"board_zone")为漏网字符串键,改 `main.get("ui").battle.get(key)`(战区域容器字段名与三区合同键同名)。
|
||||
5. **curio_overlay/confirm_dialog 两词在装配/流程件替换表漏列**——main.gd 表有、main_ui_build 与三流程件表漏,冒烟(Invalid assignment on main.gd)与 ui_click_probe sl7(开窗调用静默失效)抓漏后补改;教训=机械改写必须逐词对照 T0 名单全量扫描(T3 终检已按 49 名逐词复查全域归零)。
|
||||
|
||||
卡②:
|
||||
1. **grant_segment._seal_preview_area 的 oc.treasure_inventory 读为 T3 盘点漏项第五处**(卡片 T3 列宿主四处读)——SCRIPT ERROR 抓漏后改指 reward_grant.treasure_inventory。
|
||||
2. **treasure_effects_test 五处直构注入 obX.treasure_inventory**(ob8/ob11/ob13 注入与读、ob14 段)——设计文档 §11 盘点「1 处」漏项;同属「成员寻址改指、断言零变化」改经 reward_grant() 出口。
|
||||
3. **level_runner 注入行加 self 前缀**消 S13 no-self-assign 文本巧合(`X.treasure_inventory = treasure_inventory` 左侧链尾名与右侧成员同名);语义零变化。
|
||||
4. **_hero_atk 派生读落位**:受理段写点删除、spawn 读点改调、combat_segment 消费改调——「读面即引用、无第二账」先例同款。
|
||||
5. 宿主内 encounter/attunement 词替换以占位符保护三处字符串键(behavior_roll 前置流名 "encounter"/BoardInstaller bundle 键 "encounter"/enc.get("attunement") 键)——键字面零变化。
|
||||
|
||||
## 4. 遗留读数义务与登记项
|
||||
|
||||
- **gdstyle 基线 refresh(用户人工,批末总收口后)**:`python tools/gdstyle_gate.py --refresh`——卡① 死键 1 条(main.gd max-class-variables=49)+卡② 预期死键 1 条(battle_orchestrator max-class-variables=38;文件长键 1035 已由轴丙归绿基线在册需一并核对)+轴丙遗留归绿键,合计消失 ≥2 条触发骤降守卫须 `--force`(与待办清单「零豁免五键清偿批」批末一次人工 refresh 合并执行口径一致)。
|
||||
- **实机目检总清单(登记不阻塞)**:①三屏切换与模态开合实机过一遍(卡① T4 义务——冒烟/探针/golden 已机检承载,真人观感复核登记);②战斗一局全流程观感(卡② 结构迁移零行为变化的一般性目检)。
|
||||
- **能力清单核对(收尾核对义务)**:本批卡① 零新增能力(改址只换地址)、卡② 零新增能力(新增断言全部内嵌既有函数)——game/tests/README.md 零增删改,批末核对表上代表用例名与实际落地一致(未改名,核对通过)。
|
||||
- **批末代码域闸**(run_all_gates.sh --code)归迭代总收口——卡③ 裁决后与 reshape 串联执行。
|
||||
|
||||
## 5. 卡③ 停卡呈报(缺陷移交——按卡片 §13 L3 条款①预设停机点)
|
||||
|
||||
**分歧点**:卡③ §3 断言「`_run_one_level` 尾部两行 :67 `panel.set_sim_running(false)`/:68 `_running = false` 每局执行一次(潜伏红本体)」与现行代码不符——实测两行在 `run()` 尾 :53-54(for 循环外、每批恰一次),`_run_one_level` 尾部 :67-68 实为败局节点记账。
|
||||
|
||||
**定谳证据**(三重):
|
||||
1. git 历史:迭代 41 测试债批提交 215da315(2026-10-29 合回)提交信息明载「两个产品回归随批修(……**tune_sim 运行态收尾归还 run**)」——潜伏红已由该批先修复。
|
||||
2. T0 实跑面一:`--only=standee_fx_sweep` 全绿(18 checks)——wd8 三断言(`drv.seen == [true, true]`/收尾双运行态假/报告非空,现位 :145-147)现行全 PASS。
|
||||
3. T0 实跑面二:ui_tune_probe 全链路 PASS——⑤ 守卫环 1800 帧预算内退出、输出四串在(模拟 N=10 完整输出)。
|
||||
|
||||
**两种读法**:
|
||||
- 读法 A(证据支持):卡片成文(2026-10-29 v1.0 定稿)时点潜伏红确实在(u23 卷入形态),同日合回的迭代 41 测试债批把收尾归还 run() 尾——卡片「现状考证」被上游批施工消化变质。215da315 提交信息与代码现状一致。
|
||||
- 读法 B:卡片考证当时即误读(:67-68 为败局记账非置假)。与 215da315 提交信息矛盾,不采。
|
||||
|
||||
**影响面**:
|
||||
- 已申报行为改动 V3(收尾置假还原)**无对象**——还原动作已由迭代 41 测试债批完成并申报。
|
||||
- 目标 2「wd8 三断言与 ui_tune_probe ⑤ 零改动**转绿**」变「零改动**保持绿**」。
|
||||
- 核心义务仍有效:S13 await-in-loop 裸键消除(tools/gdstyle_baseline.txt:15 在册、基线 16 键之一)与双实现对拍套件(帧相位等价执行化证据)——对拍夹具与产品码的申报差异由两处减为一处(收尾还原差异消失,仅余 stats 暴露成员)。
|
||||
- T0「实跑定谳」义务已完成(上列三重证据),实现段零施工(停摆卡不提交)。
|
||||
|
||||
**建议案**(供写卡段/用户裁决):
|
||||
1. 卡③ 退回写卡段重裁:剥离行为改动申报(对照表 V3 行删、§1 目标 2/3 措辞改「保持绿」)、T0 判读出口改「两面全绿=前提已由迭代 41 测试债批消化,直接按无行为改动形态开工」;保留裸键消除+双实现对拍义务(夹具=现行产品码 await 形态逐字副本——收尾在 run() 尾形态——与产品码唯一申报差异=夹具暴露 stats 供对拍)。
|
||||
2. 若用户不愿重写卡:裁「卡③ 剥离出迭代 44」(roadmap 行拆批),裸键消除与对拍随下一迭代重编。
|
||||
|
||||
**待办清单条目状态回退拟稿**(落笔归上游段/用户,本批禁写根流程清单):「零豁免五键清偿批」条目的测速循环批子项状态由「文档设计已闭合+已写卡+已编迭代 44」回退为「文档设计已闭合+待写卡(现状考证已被迭代 41 测试债批消化——行为改动申报面失效,卡片重裁后重编)」。
|
||||
|
||||
## 6. 批末全量域闸与闸红修复(收口实况)
|
||||
|
||||
**闸况**:reshape 后第三次全量域闸(--code)全绿——00 冒烟/01 系统测试/03 场景探针/05 渲染闸/10 熵增闸绿;**02 单元与集成、09 静态扫描从基线红改善归绿**(基线记的迭代 41 时代存量红指纹已在册待换代——现真绿);回执 commit 对齐分支尖(b27db010)。
|
||||
|
||||
**闸红修复三笔(新红必修,均孤跑复绿)**:
|
||||
1. S9 R1:tune_sim_controller `_sim_stats` 裸 Dictionary 注解改 `Dictionary[Variant, Variant]`(Variant 参数化现行口径)——本批新码写法违规。
|
||||
2. ui_input_probe :482/:488 case⑮ 装配直构注入 `settlement.attunement` → `_facts.attunement`——卡② 成员迁移的探针消费漏网(T0 映射谱系只扫 main./root./self. 前缀,直构注入形态不在谱系);SCRIPT ERROR 中断 _ready 致探针挂死超时。
|
||||
3. run_time_fatal_test :75 BoardInstaller 束键值 `oc.encounter` → `oc._facts.encounter`+meta_progression_forge_test 三处编排器 encounter 直读改指(:340/:749/:758)——同型漏网(负面套件名与触面无字面关联=关联档选测盲区;meta_forge 分片复现、单进程未现的夹具路径差异)。
|
||||
|
||||
**实现决定记录补(白名单外备案)**:跨卡触面(卡② 迁移成员被卡① 已改址的探针/未申报套件消费)只有全量闸能抓——关联档选测的盲区实证;全域终扫(encounter/attunement 直读形态)后同类漏网清零。
|
||||
|
||||
## 7. 独立验收审计结论与记录级缺漏补记
|
||||
|
||||
审计报告:reports/iter44-验收审计-2026-10-30.md——**结论=通过(可合并)**。21 行对照表逐行通过、判据绑定面自跑全绿、测试削弱零发现(37 条被删断言逐条有配对新行)、非行为改动成立、证据链在案。
|
||||
|
||||
记录级缺漏三项(不阻塞,随本节补记):
|
||||
1. 卡③ 对拍串行驱动为白名单外决定——已在 §5.1 卡③ 实现决定记录 1 备案(审计方读的是早期版本);提交 04783fae 信息同载。
|
||||
2. main_overlay_flow.gd:4 头注仍写已退役的 root._open_modal/_close_modal 转发方法名——词面遗留(卡面头注义务只列 main.gd 与 main_ui_build 两处);闸后代码面禁改(改则回执作废),留反哺点通道随文档批或下一代码批顺路改写。
|
||||
3. 卡② grant_segment 事实族改指实测 18 处 vs 卡面「约 23」估计偏差——记录偏轻,本节补记(T0 点名清单实数为准)。
|
||||
|
||||
例外备案:V1d 扫描面为 presentation 平铺 27 文件口径(与卡面 glob 一致、只扩不缩;递归口径下批前既有 ui_kit.gd:819 即红——非本批引入)。
|
||||
|
||||
## 8. 停机状态与收口(进行中)
|
||||
|
||||
- 迭代总收口(冻结→并 master→reshape→批末全量域闸→独立验收审计→合并)**未启动**——按停机规则「迭代收口合并须等停摆卡闭合、或用户裁拆批后整体进行」。
|
||||
- 卡① 卡② 全部收口提交在 worktree iter44 分支(9 笔提交),冻结待收口。
|
||||
@@ -0,0 +1,99 @@
|
||||
# 迭代 44 独立验收审计报告——2026-10-30
|
||||
|
||||
审计方:未参与实现的独立会话(只读审计+自跑最窄抽查)。对象=worktree D:\code\g-plan2-iter44 分支 iter44 尖 9f1dda40,对照基线 master 尖 ab3b4a9d;材料=三张需求卡片(组合根注册面批/战斗编排归约批/测速循环批 v1.1)+实现报告+闸回执与日志。本报告不修改任何代码与测试,缺漏如实记录不代修。
|
||||
|
||||
**结论行:通过(可合并)**——三卡 21 行对照表逐行核到落点、判据绑定面自跑全绿、测试削弱零发现(断言计数 22 文件零减少)、非行为改动判定成立、证据链在案;2 项记录级缺漏+1 处注释级陈旧指址+4 项例外备案,均不动行为、判据与验收闭合(见 §6),不构成合并阻塞。
|
||||
|
||||
## 1. 逐行核对——卡①(§8 十一行)
|
||||
|
||||
| # | 判定 | 凭据(文件:行/自跑) |
|
||||
|---|---|---|
|
||||
| 1 | 通过 | V1c 改址 `main.ui.LAYER_CONTRACT`/`main.ui.node`(system_test.gd:90/:94,尾缀〔装配生命批 V1〕保留);自跑 run_system_test 69 项 0 失败;LAYER_CONTRACT 与基线逐字一致(git show ab3b4a9d 对读,唯一差异=注释指址 `_apply_layer_contract`→`apply_layer_contract`,白名单 3) |
|
||||
| 2 | 通过 | main.gd `^var` 计数 49(基线)→15(尖);自跑 run_smoke 15 项 0 失败+run_system_test 69 项 0 失败;gdstyle 零基线外新键 |
|
||||
| 3 | 通过 | main_ui_build.gd `build()` 八步名序与基线逐字同;`_build_screens` 赋值序/add_child 序不变;42 条 connect 一一对应(仅 :97/:206 两处 closed bind 改 `flow.close.bind`=表 D 申报);smoke_boot get() 4 改 2 留(settings_store/run_scope 留);冒烟六断言+V2 五连比(system_test.gd:108-109)自跑绿 |
|
||||
| 4 | 通过 | 探针面 diff 归一化对账 88 处非平凡行全部落表 A/C/D+实现决定 2/3/4;probe_gate 日志全绿(.gates/probe.log 06:53,全段);ui_tune_probe 审计自跑 PASS |
|
||||
| 5 | 通过 | system_test 改址全对账(V1a/V1b/V1c/V2/V3 矩阵 `_face_snapshot` mf→`main.ui.modals.flow`、rp→`main.ui.screens.row_pacer`);V3a–V3i 存活性矩阵自跑全绿(69 项含) |
|
||||
| 6 | 通过 | 七别名消费方改址逐处对上(pacing_queue 4 处、ui_struct_probe `flow.stack`、t17_defer_probe `flow._deferred`、ui_click_probe `settlement_stop_timer.is_armed()`);自跑 `--only=pacing_queue --only=modal_flow` 双绿;改后断言判据零变化(归一化对账) |
|
||||
| 7 | 通过 | 六转发方法+_flush_row_queue 全库零残留(grep 只余 1 处陈旧头注,见 §6 缺漏 2);modal_flow 24 checks 绿+系统测试 V3g 段绿 |
|
||||
| 8 | 通过 | source_guard_test.gd:69-92 重锚——尾缀〔注册面 V1〕;自跑输出「V1d 层序落位单口(presentation 27 文件——apply_layer_contract 之外 move_child 调用点=0)」;扫描面较改前(单文件)只扩不缩;口径例外见 §6 例外 1 |
|
||||
| 9 | 通过 | theme_projection_test.gd:725-726 改断言新串 `ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)`,尾缀〔注册面 V2〕;自跑 164 checks 绿 |
|
||||
| 10 | 通过 | render_gate 日志全绿(.gates/render.log 06:54,shot_probe 七屏+golden);diff 零像素面改动(改址只动地址词) |
|
||||
| 11 | 通过 | 审计独立 grep:49 名旧一级地址 `main./root./self.` 代码级残留=0(仅 3 处注释级命中);代码域闸日志六件全绿(§5);留档偏薄见 §6 例外 5 |
|
||||
|
||||
## 2. 逐行核对——卡②(§11 V0–V6)
|
||||
|
||||
| 行 | 判定 | 凭据 |
|
||||
|---|---|---|
|
||||
| V0 | 通过 | 关联套件全绿:审计自跑 8 套件(settle_segments/round_flow_skill_pool/roll_multiplier/treasure_effects/pacing_queue/modal_flow/source_guard/theme_projection)+批末闸 unit.log 分片全绿(15 suites shard 4/4);behavior_roll 随件迁移取号序逐字(§4 核对);五信号留宿主(battle_orchestrator.gd:12-16);两快照出口在宿主(:677/:739) |
|
||||
| V1 | 通过 | settle_segments_test.gd:28-29 两断言〔编排归约 V1〕:`oc._rounds != null and oc._rounds.oc == oc`+`oc._facts != null`(check_quiet 静默通过=套件 18 checks 零失败);五段段件齐在 _rounds 由 :30-34 五条既有接线断言改指 `oc._rounds._X_seg` 承载(尾缀〔迭代27 T2〕保留,纯改址) |
|
||||
| V2 | 通过 | round_flow_skill_pool_test.gd:112-115〔编排归约 V2〕:终局后 `presentation_snapshot()["refresh_used"]` == 末轮结算快照 `economy.refresh_used`(经 settlement_snapshot 末次发射承接);自跑实跑输出 5 实例(L1·D0×3+L2·D0×2)全 PASS;reset_round 只清三项(battle_facts.gd:32-35) |
|
||||
| V3 | 通过 | roll_round_actions 与基线 `_roll_round_actions` 逐行对读一致(仅成员改指;`oc.rng.open_stream("behavior_roll")` 流名与开流时点不变,round_cycle.gd:102);settle_if_due 闸序 settling→battle_result→phase 逐字、终局早退不回置;trigger_cond_test 3 处改指(:119/:126/:131);自跑 round_flow 134 checks+settle_segments+trigger_cond 全绿 |
|
||||
| V4 | 通过 | roll_multiplier_test.gd:45-59 四断言〔编排归约 V4〕:override 100→spawn hero_unit atk==100;缺省→hero 表值(check_quiet 静默通过);攻击单 base_value 贯通由既有 `_hero_attack_row_with`(:343,hero_atk_val=100 全链路停手结算行)承载——两消费点(spawn+base_value)闭合;combat_segment.gd:56 改调 `oc._hero_atk()`,宿主派生读 :69/:282 |
|
||||
| V5 | 通过 | treasure_effects_test.gd:311-315〔编排归约 V5〕:`ob14.reward_grant().treasure_inventory == adr14.runner().treasure_inventory` 引用同一性;use_treasure 受理链由 ob8/ob11/ob13 既有段承载(:311 ob8 直写改指后绿);level_runner.gd:520 注入改经 `reward_grant()` 出口(`self.` 前缀=实现决定 3) |
|
||||
| V6 | 通过 | seal_polarity(_perfect 直写→_facts 8 处对读)/feat_vouchers(契约 2 计数纯净段改指后全绿)/variant_placement/meta_forge/adventure_feats/table_structure_guard——批末闸 unit 分片全绿;三 mark 写口(grant_segment.gd:311/:379/:394 与基线直写同点同增量);attune 三外部消费面零改址(main.gd/treasure_interact_controller/table_structure_guard 均不在改址 diff) |
|
||||
|
||||
## 3. 逐行核对——卡③ v1.1(§12 V1–V3)
|
||||
|
||||
| 行 | 判定 | 凭据 |
|
||||
|---|---|---|
|
||||
| V1 | 通过 | tune_sim_batch_parity_test.gd 五面断言〔测速 V4/V5〕自跑绿(n=3 stats 六键逐键重算+报告逐字含四关键串;n=0 六键零计);run 可等待契约由 wd8 `await ctrl.run(2)`(standee_fx_sweep_test.gd:144,零改动)承载;全文件零循环(grep -c "for \|while " = 0)、代码 await 仅 :69 一处 `await _batch_finished` |
|
||||
| V2 | 通过 | 五面对拍断言〔测速 V1–V3〕自跑绿:帧序列相对偏移逐位+相邻差恒一(每步恰一帧)、运行态序列逐位+收尾双假、进度串逐位;n=3+n=0 两案各 5 断言全 PASS;manifest 增行(run_all.gd:189);拓扑例外见 §6 缺漏 1 |
|
||||
| V3 | 通过 | wd8 三断言〔wd8〕自跑 PASS(18 checks:`drv.seen == [true, true]`/收尾双运行态假/报告非空——standee_fx_sweep_test.gd 不在本批 diff=零改动);ui_tune_probe 审计自跑 PASS(⑤ 守卫环 1800 帧预算内退出+输出四串);收尾四语句迁入 `_finish_batch()`(:87-92)与基线 run() 尾次序逐句同(close→报告→置假→_running),`_run_one_level` 零触碰(不在 diff) |
|
||||
| 静态 | 通过 | gdstyle_gate.py 审计自跑 exit 0:命中 7 全基线内、基线外新违规 0;基线 :15 `tune_sim await-in-loop` 裸键不命中=死键(消失键不参与红判定) |
|
||||
|
||||
## 4. 测试削弱检查(对照三卡申报全集)
|
||||
|
||||
- **断言计数对账**:22 个改动测试/探针文件 `Kit.check|check(|check_quiet` 计数基线→尖逐一比较——零文件减少;仅 5 增(roll_multiplier 36→40=V4 四断言、round_flow 100→101=V2、settle_segments 16→18=V1 两条、treasure_effects 58→59=V5、tune_sim_batch_parity 0→10=新套件)。与申报新增面(卡② V1/V2/V4/V5 四条+卡① V1d/V6 重锚+卡③对拍五断言)完全一致。
|
||||
- **删除断言逐条对账**:全 diff 被删 check/断言行 37 条逐条找到配对新行——语义不变只换地址(含 smoke_boot 三 get、system_test V2 五连比、pacing 四断言、V1d/V6 两条);无任何断言删除后无配对。
|
||||
- **阈值/扫描面**:零阈值放宽;扫描面唯一改动=V1d 单文件→27 文件(申报为强化,成立);static_scan.sh S8/S7 两清单增行 round_cycle.gd(卡② T2 申报;battle_facts.gd 未增行=卡片明示「零协作调用不入清单」,成立)。
|
||||
- **换下层直调**:零发现——探针仍走真实按钮/输入路径(pressed.emit/push_input 等既有形态未动)。
|
||||
- **wd8 三断言**:文件不在 diff=零改动,自跑保持绿。**ui_tune_probe 巡检白名单**(实现决定 3):`host == main and m == "_on_modal_closed"` → `host == flow_host and m == "close"`(ui_tune_probe.gd:214-216)——属卡①表 D「closed bind 目标改 flow.close.bind」的机检联动,同一连接新宿主登记;conn_count=11 断言与消息逐字不变(:223-224 与基线 :221-222 对读);判据面「closed 外发消费目标=模态编排口」语义保持。**判定:不属削弱**。
|
||||
- 卡② §15 零申报、卡③ §16 零申报——兑现成立(超出申报的删改零处)。
|
||||
|
||||
## 5. 非行为改动判定与证据链
|
||||
|
||||
**卡①**:装配序(八步名序、add_child 序、赋值序)逐字不变;42 条连接语句一一对应;LAYER_CONTRACT 19 项逐字迁驻;move_child 单口由 V1d 27 文件扫描绿+grep 复证(全 presentation 仅 ui_registry :84/:98 两处调用,views/ui_kit.gd:819 为批前既有、不在 diff);main._ready 仅插入 `ui = UiRegistry.new()` 一行于 _build_ui 之前;`_build_modal_flow` 注入语句序与基线逐字同(flow 实例改随 UiModals._init 构造=实现决定 1,注入时点不变)。
|
||||
**卡②**:成员迁移与改指——roll_round_actions/settle_if_due 逐字迁移对读成立;grant_segment 三 mark=同点同增量;encounter/attunement 词替换的三处字符串键保护(实现决定 5)grep 复证(behavior_roll 前置流名 "encounter" 字面在位);treasure_inventory 注入时序(宿主构造后 :520)不变。
|
||||
**卡③**:收尾四语句迁移=同帧同相位(对拍 V2 运行态序列+V5 报告逐字承载,自跑绿);`_run_one_level` 零触碰。
|
||||
|
||||
**证据链**:
|
||||
1. 卡③ T0 三重证据在实现报告 §5 在案:215da315 提交信息明载「tune_sim 运行态收尾归还 run」(审计直读提交信息复证)+两面实跑读数;审计自跑两面(wd8 套件+ui_tune_probe)复证现行全绿——前提消化链条完整。
|
||||
2. 卡③ v1.1 重裁凭据:卡 v1.1 状态行+§15 裁决记录(2026-10-30 停卡重裁三点)在案。
|
||||
3. 卡①卡②实现决定记录(报告 §3 共 5+5 条)对照卡① §10 七条、卡② §17 五条白名单——白名单外事项(shot/fx_shot 纳入、layout_probe get 键、curio/confirm 漏列补改、flow 随容器构造、grant 第五处读、treasure_effects 五处直构、self 前缀、占位符保护)均有备案,无遗漏。
|
||||
4. 闸凭据:receipt.json 现为 docs 域回执(9f1dda40 提交时 pre-commit 钩子生成,b27db010 时代,全绿);代码域末跑凭据=.gates 六件日志(smoke 15/0、system_test 69/0、static_scan ok、probe_gate 全绿、render_gate 全绿、unit 分片全绿),时间戳 06:49–06:54 恰落 b27db010(06:49:41 提交)与 9f1dda40(06:57:19 提交)之间——绑定终态代码(9f1dda40 只含报告文件)。
|
||||
5. 红线:diff 只触 `game/`+`reports/` 两目录;零 `randomize()`/自建 RandomNumberGenerator;零触及 `design/`、`game/tests/README.md`、两份根流程清单。
|
||||
|
||||
## 6. 缺漏与例外(如实记录,不修)
|
||||
|
||||
**缺漏(记录级,不动判据)**:
|
||||
1. **卡③实现决定记录缺位**:对拍驱动由卡 T2 的「先启动两形态、后分别 await」并行拓扑改为**串行驱动+相对帧偏移归一**(tune_sim_batch_parity_test.gd:145-162)——卡 §14 白名单六条不覆盖,应在实现报告「实现决定记录」节备案;实际只在提交 04783fae 信息中有案(理由=GDScript 协程动态调用强制 await、无并行启动形态+_init 时点 root 未挂树实测)。报告 §3 无卡③条目。实质判据未损(相对偏移+相邻差恒一仍承载「每步恰一帧+相位同点」,审计自跑绿)。
|
||||
2. **main_overlay_flow.gd:4 头注陈旧指址**:注释仍写「全部开合走组合根模态单口(root._open_modal/_close_modal)」,两转发方法已退役、实际调用全改 `root.ui.modals.flow.open/close`——「指址须真」词面违例,行为无涉,批内漏改。
|
||||
3. **卡②改指处数偏差记录偏轻**:grant_segment 实改 18 处与卡片「约 23」的 L2 偏差,实况数在报告 §2 V6 行在案但未入「实现决定记录」节(卡 §14 L2 要求记入该节)。
|
||||
|
||||
**例外(依据在案,不判红)**:
|
||||
1. **V1d 扫描面口径**:实现为 DirAccess 平铺(res://presentation 顶层 27 个 .gd),不含 views/、scenes/ 子目录——与卡片字面 glob(`game/presentation/*.gd`)一致、较改前单文件只扩不缩;但「全 presentation 目录零第二落位点」按递归读法不成立:批前既有 `views/ui_kit.gd:819` 有一处 `body.move_child(...)`(视图内行排序、非层序落位;ui_kit.gd 不在本批 diff、归迭代 42 尾波清偿批在册债)。ui_registry.gd:3 头注「全仓唯一 move_child 点」沿袭旧 main.gd 注释口径、与该既有事实并存。若日后递归化扫描面,须先处置该行。
|
||||
2. **回执被覆盖**:receipt.json 为 docs 域回执(报告提交钩子生成);代码域末跑以六件日志为凭(§5 第 4 条),与报告 §6「第三次全量域闸全绿、02/05/09 从基线红改善归绿」叙述吻合。
|
||||
3. **`_finished` 成员写而不读仍保留**(tune_sim_controller.gd:26/:64/:88)——卡 §14 白名单 3 明示「零读者确认后可删」,保留属白名单内两可选择,键账只升 1(11/15 在阈内)。
|
||||
4. **报告 row4 计数口径差**:「约 460 行机械改写」(报告 §1)与 784d581b「422 行」——统计口径差,不影响判据。
|
||||
|
||||
## 7. 审计自跑记录(独立复证清单)
|
||||
|
||||
| 项 | 结果 |
|
||||
|---|---|
|
||||
| `python tools/run_smoke.py` | PASS——15 项 0 失败 |
|
||||
| `python tools/run_system_test.py` | PASS——69 项 0 失败(V1a/V1c/V2/V3a–V3i 全过) |
|
||||
| `--only=source_guard --only=theme_projection` | 绿——V1d「presentation 27 文件…调用点=0」〔注册面 V1〕+V6 新串〔注册面 V2〕+theme 164 checks |
|
||||
| `--only=settle_segments --only=round_flow_skill_pool --only=roll_multiplier --only=treasure_effects` | 4 套件绿——V1 两断言、V2 五实例、V4 四断言、V5 一断言(check_quiet 静默通过=零失败) |
|
||||
| `--only=pacing_queue --only=modal_flow` | 双绿 |
|
||||
| `--only=tune_sim_batch_parity --only=standee_fx_sweep` | 双绿——对拍 10 断言〔测速 V1–V5〕+wd8 三断言 |
|
||||
| `res://probe/ui_tune_probe.tscn`(headless) | PASS——⑤ 守卫环预算内退出+输出四串+全链路通过 |
|
||||
| `python tools/gdstyle_gate.py` | exit 0——命中 7 全基线内、零新违规;tune_sim 裸键不命中 |
|
||||
| grep:tune_sim 循环/await | 循环 0;代码 await 仅 run() :69 一处 |
|
||||
| grep:49 名旧一级地址残留 | 代码级 0(仅注释级 3 处) |
|
||||
| 成员数 | main.gd 49→15;battle_orchestrator 37→15;容器 3/9/5/14 |
|
||||
| 断言计数对账 | 22 文件零减少、5 增(+4/+1/+2/+1/+10) |
|
||||
| diff 归一化对账 | 88 处非平凡行对全部落表 A/C/D+已记录决定 |
|
||||
|
||||
## 8. 总结论
|
||||
|
||||
**通过(可合并)。** 三卡对照表 21 行全部核到落点且判据绑定面当轮实跑成立;测试削弱零发现;三卡「非行为改动」判定在代码级对读与实跑两面成立;证据链(卡③ T0 三重证据、v1.1 重裁、卡①卡②决定记录、闸日志)在案。缺漏三项(卡③决定记录缺位、main_overlay_flow.gd:4 陈旧头注、卡②计数记录偏轻)与例外四项均属记录/注释级,不触行为零变化、判据绑定与验收闭合;建议随批末收口由实现侧补记(补记动作归收口段,不由本审计代作)。gdstyle 基线 refresh(--force)仍属用户人工遗留义务(反哺点 7 在案)。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 工程改动报告——工程小件批(run_all 解析/注释与消息订正/恒真断言五条/基线三键换代)
|
||||
|
||||
> 接收方:待办清单「迭代 42 批末移交小件」「恒真与名实不符断言五条」「闸基线 02/05/09 三键换代」三条目(批后整条删除,本报告为过程凭据留档)。分支 engsmall-20261030,基点 14054d9d。
|
||||
|
||||
## 改动清单
|
||||
|
||||
| # | 件 | 改动 | 提交 |
|
||||
|---|---|---|---|
|
||||
| 1 | run_all.gd --only 逗号解析 | 解析段补 `split(",")` 拆分,对齐头注契约 `<词[,词…]>`;闸脚本关联档单旗标逗号拼接传参自此可用(此前多词必零命中 quit(2),缺陷起于 6caf56b5) | d32ff342 |
|
||||
| 2 | level_runner.gd:12 信号注释 | `shop={gp}` → `shop={gp}(pool 态加 shelf:true)`,与发射点两形态(legacy/pool)对齐 | dd2de10e |
|
||||
| 3 | static_scan.sh S8 ok 消息 | 文件集 9→10(S8_FILES 实含 10 文件,board_installer.gd 随迭代 42 入集) | dd2de10e |
|
||||
| 4 | 恒真与名实不符断言五条 | ①feat_vouchers 恒真改绑投放装配面(读数==Σ表配额−本会话已兑付一张);②treasure_effects 去恒真尾缀绑清图判定未触发;③④level_shop_economy 恒真 min 改绑胜局 HP>0 旗+扫描面非空双条件;⑤system_contracts 互异半缺失补值集去重计数 | 6a3203a8 |
|
||||
| 5 | 基线 02/05/09 三键换代 | 按本批回执实测绿重锚(旧红指纹 f9865f1a5a44/de89589e49ed/1a9ad773489f 归绿消除),note 记账 | 28223074 |
|
||||
|
||||
## 验收凭据
|
||||
|
||||
- 孤立验证:逗号拼接四词形态 `--only=feat_vouchers,treasure_effects,level_shop_economy,system_contracts` 实跑四套件全绿(level_shop_economy 192 checks)——件 1 解析与件 4 判据同轮验证。
|
||||
- 批末闸:`run_all_gates.sh --code` 两次全绿。第一次(commit 6a3203a8,旧基线)02/05/09 三键「基线红→现绿(改善)」;重锚后第二次(commit 28223074)全绿无改善项,回执 domain=code、scope=related、dirty_game=false。
|
||||
- 件核:改后全仓 `or true` 残留零命中;五条旧形态清零。
|
||||
|
||||
## 实现决定记录
|
||||
|
||||
零拍板——五件修法均按待办登记既定方向执行,无超范围事项。件 1 修法选「解析补拆分」而非「闸脚本改逐词传参」:头注契约 `<词[,词…]>` 是文档承诺,实现向契约对齐,闸脚本零改动。
|
||||
@@ -1,6 +1,6 @@
|
||||
# 需求卡片——gdstyle 尾波补件批
|
||||
|
||||
> **版本状态**:v1.0 定稿——零待裁点(2026-10-29 用户裁:恢复尾波卡 v2.0 裁去的 D2/D4 两拆件项;D5 五降值键=留债、零豁免延伸)。
|
||||
> **版本状态**:v1.1——2026-10-30 用户裁 T2 机制=R2 整族归件(实现段停卡呈报 `reports/iter45-停机呈报-断点域拆件规格缺口-2026-10-30.md` 裁决),第 7 节 T2 条款改述、其余条款沿 v1.0 定稿——零待裁点(2026-10-29 用户裁:恢复尾波卡 v2.0 裁去的 D2/D4 两拆件项;D5 五降值键=留债、零豁免延伸)。
|
||||
> **接收方**:待办清单条目「gdstyle 尾波补件批——悬停域与断点域拆件(2026-10-29 登记)」。
|
||||
> **来源**:尾波清偿批设计闭合后的用户再裁(D2/D4 改判拆件)。**唯一事实源**:`design/engineering/architecture/architecture.md` v1.25(「模块归属·呈现层」表 board_hover 行、「核心层」表 breakpoint_store 行、「冒险域拆分」节断点随动句、「呈现层装配」节屏成员寻址句)。
|
||||
|
||||
@@ -65,7 +65,7 @@ architecture.md v1.25 三处在册设计(2026-10-29 零豁免裁设计闭合
|
||||
**T2 breakpoint_store 拆件(1–1.5 工作日)**
|
||||
|
||||
- [ ] 建 `game/core/breakpoint_store.gd`(RefCounted;构造=引用束注入:db、rng、主角账三件、curio/stop/bank/loadout 引用——防 max-parameters 新键)
|
||||
- [ ] 断点域 6 成员+`BREAKPOINT_FIELDS`+函数族 9 个迁出(逐字搬运);`resume_adventure` 留宿主薄壳;`set_save_manager` 转发件
|
||||
- [ ] 断点域 6 成员+`BREAKPOINT_FIELDS`+函数族 9 个归件(v1.1 R2 整族归件,机制=所有权缝语句分家):纯读侧三函数(`_bp_read`/`_bp_strategy`/`_bp_read_int`)逐字;`_bp_parts`/`_snapshot_breakpoint` body 逐字(宿主输入 state/level_index/master_seed/runner/adventure_counters 逐调传参——件不反向持宿主);四 `_resume_*` 按所有权缝语句分家——件侧语句(主角账三件片段回填、curio 池与特性持有集还原、stop/bank 片段回填、六成员改写)迁入 store,宿主侧语句(summary/levels_total/level_index/_shop_mode/_treasure_unlocked/_attunement/_tree_rules/adventure_counters 重绑定与 `_reset_curio_state`/`_attune_refresh_x_total` 宿主调用)按原语句序吸收进 `resume_adventure` 壳,语句序逐步保形、27 键冻结;宿主保留 `_snapshot_breakpoint` 一行委托壳(CurioStop 的 Callable 目标零改)与 `set_save_manager` 转发件;宿主对六成员的其余读写点改址经件
|
||||
- [ ] `breakpoint_resume_test.gd:185` 改址为 breakpoint_store 常量(判据零变化——第 14 节申报)
|
||||
- [ ] 孤立验证:`--script res://tests/run_all.gd -- --only=breakpoint_resume,save_rng_roundtrip,cross_system_flow` 全绿+`python tools/run_smoke.py`
|
||||
|
||||
@@ -103,7 +103,7 @@ architecture.md v1.25 三处在册设计(2026-10-29 零豁免裁设计闭合
|
||||
|
||||
## 11. 待裁点
|
||||
|
||||
零待裁点,开工门=U0。(裁决记录:2026-10-29 用户裁——恢复 D2/D4 拆件;D5 五降值键留债、零豁免延伸;`_r0` 留宿主;断点域六成员全迁。)
|
||||
零待裁点,开工门=U0。(裁决记录:2026-10-29 用户裁——恢复 D2/D4 拆件;D5 五降值键留债、零豁免延伸;`_r0` 留宿主;断点域六成员全迁。2026-10-30 用户裁——T2 机制=R2 整族归件、四 `_resume_*` 所有权缝语句分家、宿主侧语句吸收进壳;呈报=reports/iter45-停机呈报-断点域拆件规格缺口-2026-10-30.md。)
|
||||
|
||||
## 12. 落位与可复用性预检
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
# 需求卡片——棋盘格边长口径收口批(探针门槛纠偏+格边长算式单点+冒烟扩四档板型)
|
||||
|
||||
状态行:v1.0 定稿——零待裁点(2026-10-30 用户裁:①探针不扩到 L3 推关;②最紧档 14×14 的可读断言落单元四档+探针实板推定两处;③算式单点提取用私有静态名,依据见 §2 第 5 条)
|
||||
|
||||
接收方:待办清单条目「棋盘格边长三方取值不一致——待扩板定档处置(2026-10-06 登记,来源=批次移交)」(`待办清单.md` 在册,状态「待写卡」)
|
||||
|
||||
来源:**设计事实源**=`design/gdd/ux/battle.md`「棋盘格尺寸公式」节(Last Updated 2026-10-29;格边长口径的**唯一事实源**——公式、可读下限 30px、结构钳制 [26,46] 三项都在此节)。指路文档(2026-10-26 已归一、不再存算例)=`design/art/art-bible.md` §8.1/§8.2、`design/gdd/systems/表现与支撑/HUD框架.md`。四档板型事实源=`game/data/tables/board_gen.tres`。
|
||||
|
||||
## 1. 目标与完成定义
|
||||
|
||||
这批做什么:把「棋盘格边长」在代码侧残留的两处旧口径收干净。文档侧已于 2026-10-26 归一(口径只剩 battle.md 一节),代码侧还差两件——探针的可读门槛仍是 12×12 时代的 36px(比 battle.md 的可读下限 30px 严 6px,属用旧值当新标准),格边长冒烟只跑 L1/L2(13×13 与 14×14 两档板型无断言)。本批顺带把格边长算式收成单点(现在它内联在 `set_cell_area` 里,任何「在别的板型/别的面积上求值」的需求都只能复写算式=造第二口径)。
|
||||
|
||||
**行为零变化**:算式逐位不变,`set_cell_area` 的副作用顺序(英雄脚锚重置 → `_refresh_slab` → `_update_world_size`)零改动,玩家可见画面零变化。
|
||||
|
||||
完成定义(全部满足才算完):
|
||||
1. 代码侧可读门槛=30px,与 battle.md「可读下限 30px」一致;旧值 36 在 `game/` 下零残留。
|
||||
2. 格边长算式只剩一处落点(`BoardView._cell_for`),`set_cell_area` 只调它;探针与单元测试不再各自复写算式。
|
||||
3. 13×13 与 14×14 两档进入断言并在当轮真实跑批中执行通过(单元四档+探针实板面积推定两条)。
|
||||
4. 代码域闸全绿;`tools/gdstyle_baseline.txt` 中 board_view.gd 各键只许持平或下降(MATCH——`max-public-methods` 保持 27)。
|
||||
5. 产出物三份落 `reports/`(实现报告/反哺点清单/验收审计报告),独立验收审计未过不合并。
|
||||
|
||||
**编队注记与估时**:估时 **0.5 个工作日**(T0 0.1/T1 0.15/T2 0.1/T3 0.1/T4 0.05),远低于迭代冻结线(5 个工作日),可编批;编入哪个迭代由迭代编排段定。本批触面=`game/presentation/views/board_view.gd`+`game/probe/layout_probe.gd`+`game/tests/unit/core/board_gen_reward_test.gd`+`game/tests/README.md`(写卡段已同批入表),与在飞批次无文件交叠需编排段复核。
|
||||
|
||||
## 2. 现状事实
|
||||
|
||||
全部行号为 **2026-10-30 实查**——本批编入后须由 T0 按 T0 时点 master 重测重锚。
|
||||
|
||||
1. 算式内联在 `game/presentation/views/board_view.gd:191-209`(`set_cell_area`):`:194` N=`maxi(width,height)`、`:195` `sep := SEP`(`SEP := 2`,`:34`)、`:196-197` `d := DEPTH_PX`(`8.0`,`:37`)与 `reserve := 24.0 + d`、`:198` 一条 `int(floor((minf(avail.x - 12.0 - d, avail.y - reserve) - float((n - 1) * sep)) / float(n)))`、`:199` `clampi(cell, 26, 46)`。副作用尾链 `:200-209`(英雄脚锚重置/`_refresh_slab`/`_update_world_size`)。
|
||||
2. 生产调用路径两条:`game/presentation/views/board_zone_view.gd:91-92`(`board_area.resized` → `set_cell_area(_board_area.size)`)与 `game/presentation/views/board_view.gd:250`(`rebuild` 末尾 `set_cell_area(size)`)——传入的都是棋盘视图实际可用区尺寸,`board_view` 为 `PRESET_FULL_RECT` 子级,故 `board_view.size` 即生产传入值。
|
||||
3. 探针门槛:`game/probe/layout_probe.gd:15` `const MIN_CELL := 36.0`,注释自陈「battle.md §棋盘格尺寸公式 1280 档 12×12 → 36px」;`:92-94` 断言 `cell < MIN_CELL` 记红。探针只进 L1(`:27` 起始冒险)、`:31` `press_candidate(0)`,故实跑只会遇到 10×10(格边约 45px),门槛 36 从未在 30–35px 区间生效过。
|
||||
4. 格边长冒烟:`game/tests/unit/core/board_gen_reward_test.gd:53-69`(`_smoke_layout_cell_formula`):`:57` `for L in [1, 2]`、`:58` `generate(L, 0, ...)`、`:60` 样本 `avail := Vector2(1036, 460)`、`:63` 读 `board_view.get("_cell")`、`:64-66` 网格高 ≤ 可用高−24、`:67-68` 格边长 ∈ [26,46]。13×13 与 14×14 两档从未进入断言。
|
||||
5. **S13 顶格事实(本机 gdstyle v0.4.0 实测)**:`tools/gdstyle_baseline.txt:22` 登记 `game/presentation/views/board_view.gd|quality/max-public-methods = 27`,与实测公开方法数 27 相等。对同一文件副本加一个公开静态函数后实测升到 **28**(WORSENED 红);改名为下划线前缀(`_cell_for`)后实测仍为 **27**(零新发现)。故算式提取必须用私有名。
|
||||
6. 四档板型取档(`game/data/tables/board_gen.tres` 实查 (level_index, node_depth) → 尺寸):L1 depth 0–3=10×10、L1 depth 4–6=12×12、L2 depth 0–9=12×12、L3 depth 0–8=13×13、L3 depth 9–13=14×14。即 `generate(1,0)`=10、`generate(2,0)`=12、`generate(3,0)`=13、`generate(3,9)`=14。
|
||||
7. 规格侧:`battle.md:137-146` 公式与两项判据——结构钳制区间 [26,46]px(`:143`,管算式不越界)、**可读下限 30px**(`:144`,管玩家能否看清,与钳制分开);`:148-152` 算例表按「1280 档可用高 486px」推算 43/36/33/30,文内明标「随布局与板型变化,不是契约值」。
|
||||
8. 实机可用面积与算例不同源:`game/presentation/ui_assembler.gd:20-28` 结构为 B 区 252px + C/D 两列占余高(该文件 `:24` 注释写明 548px),D 列内 `board_zone_view.gd:24-36` 再减 AP 条 44px 与 4px 间距——与 battle.md 算例假设的 486 可用高存在偏差(与待办清单在册的「B 区高度 270 vs 252」同源)。本卡不把算例当契约,实板可用面积一律以探针实读 `board_view.size` 为准。
|
||||
|
||||
## 3. 设计契约(违反即返工,标 🔴 触碰=L3 停机呈报)
|
||||
|
||||
- 🔴 **算式单点**:格边长算式只在 `BoardView._cell_for` 一处;`set_cell_area` 只调它。探针、测试、任何新调用方都不得复写这条算式(复写=造第二口径,本批立项原因就是三处口径打架)。
|
||||
- 🔴 **两个数不得合并**:可读下限 30px(battle.md 判据)与结构钳制 [26,46]px(算式不越界)是两个概念。前者只出现在探针门槛与断言里,后者只出现在 `_cell_for` 内;禁止用钳制区间替代下限判据,也禁止把下限塞进 `cell_for` 的 clamp。
|
||||
- 🔴 **私有名固定**:提取函数名必须是 `_cell_for`(下划线前缀)。依据=§2 第 5 条的实测:公开名即令 `max-public-methods` 27→28 恶化。实现期「顺手改成公开方法更好看」=L3。
|
||||
- 🔴 **行为零变化**:`set_cell_area` 的副作用顺序与数值逐位不变;`rebuild`/`resized` 两条调用路径不改;画面像素零变化。
|
||||
- 🔴 **测试削弱申报制**:允许改动的旧断言全集见 §8「测试削弱申报」,未申报的删改由独立验收审计判红。
|
||||
- 🔴 **禁变异类测试与撤销类取证**:不得靠回退 hunk 或注入破坏证明断言有效。
|
||||
|
||||
## 4. 范围边界
|
||||
|
||||
做:算式提取为 `_cell_for` 并改调;探针门槛 36→30 与注释改指 battle.md;探针新增「实板可用面积下 13/14 板格边长推定」断言与 board_view 零尺寸前置;格边长冒烟扩到四档板型并加算式单点等价断言;`game/tests/README.md` 能力行入表(写卡段同批,见 §5)。
|
||||
|
||||
不做(明示去向):
|
||||
1. **探针扩到 L3 推关**——2026-10-30 用户裁不做:成本是探针要真打穿两关(main 无跳关入口),且一旦红因是推关而非布局会污染该闸;最大板可读面改由 §7.3 的推定断言承担。
|
||||
2. **battle.md 算例表改数**——实现批禁写 `design/`;实测与算例的偏差走 §5 反哺点通道。
|
||||
3. **四档板型数值调整**——`board_gen.tres` 板尺寸由设计侧定,本批只读不写。
|
||||
4. **B 区高度 252 vs 270**——另一条在册待办(迭代 43 批后待裁三点),本批不碰。
|
||||
5. **`set_cell_area` 副产物顺序调整**——零触碰。
|
||||
6. **MC/数值验证**——无数值改动零跑。
|
||||
|
||||
## 5. 关联文档同步面
|
||||
|
||||
| 文档 | 关联点(本批实现会改变它的哪条陈述) | 同步义务 |
|
||||
|---|---|---|
|
||||
| design/gdd/ux/battle.md(设计权威——实现批禁直改) | ①`:148-152` 算例表按「可用高 486px」推算 43/36/33/30,而实机 D 列结构为 548px(§2 第 8 条)——探针实测格边长若与算例不符,是算例随布局漂移而非代码错;②「可读下限 30px」(`:144`)条本身不变,本批正是向它对齐 | 反哺点通道:把探针 L1 实测格边长与 13/14 推定读数逐条列进 `reports/iter<N>-反哺点-<日期>.md`(改哪一节=`:148-152` 算例表、改成什么=填实测值或标注「按旧布局口径」、依据=实测读数),用户会话裁决后归文档批落文 |
|
||||
| game/tests/README.md(机械面——归写卡段维护) | 新增能力行「战斗屏三区几何与棋盘格边长(分辨率两档×四档板型)」,覆盖域表 HUD框架.md 行同步补名 | 写卡段已于本卡成文同批入表,实现批零增删零改行 |
|
||||
| design/gdd/systems/表现与支撑/HUD框架.md、design/art/art-bible.md §8.1/§8.2(已归一为指路) | 本批不改二者的任何陈述(2026-10-26 归一后它们只写规则与指针,不存算例与定值档) | 无(本行占位——按规约查不出也要写明) |
|
||||
|
||||
## 6. 卡前文件结构图
|
||||
|
||||
| 文件 | 职责 | 新增/复用 | 行为改动? |
|
||||
|---|---|---|---|
|
||||
| game/presentation/views/board_view.gd | 修改:`set_cell_area` 内联算式抽成 `static func _cell_for(n, avail) -> int`,`set_cell_area` 改调;注释补「算式唯一落点」句 | 复用既有(抽自本文件既有代码,不新增文件/类) | 否(算式逐位相同——对照表行 2 的等价断言承载) |
|
||||
| game/probe/layout_probe.gd | 修改:门槛 36→30+注释改指 battle.md;新增 board_view 零尺寸前置与 13/14 板格边长推定断言 | 修改 | 是(守卫面变化——对照表行 1/4) |
|
||||
| game/tests/unit/core/board_gen_reward_test.gd | 修改:格边长冒烟循环扩四档板型+板型一致断言+算式单点等价断言 | 修改 | 是(测试面——对照表行 2/3) |
|
||||
| game/tests/README.md | 修改:能力清单加一行+覆盖域表 HUD框架行补名 | 修改 | 否(机械面文档,写卡段同批完成) |
|
||||
|
||||
不触碰:`board_zone_view.gd`、`ui_assembler.gd`、`data/tables/board_gen.tres`、全部 `core/` 与 `data/` 代码、其余探针与测试套件。
|
||||
|
||||
## 7. 任务卡 T0..T4
|
||||
|
||||
### 7.1 T0——读数重锚与取档对账(估时 0.1 个工作日)
|
||||
|
||||
- [ ] 按 T0 时点 master 重测重锚 §2 全部行号:`layout_probe.gd:15`/`:92-94`/`:27`/`:31`、`board_view.gd:34`/`:37`/`:191-209`/`:250`、`board_zone_view.gd:91-92`、`board_gen_reward_test.gd:53-69`。
|
||||
- [ ] 四档取档复核:对 `db.combat.board_gen.resolve_row(L, D)` 逐组取 (1,0)(2,0)(3,0)(3,9),实测返回 `board_width` 依次为 10/12/13/14——不符=L3 停卡退回写卡段。
|
||||
- [ ] 改前基线读数(孤立验证):`python tools/gdstyle_gate.py`(board_view.gd `max-public-methods` 实测 27 持平)+单件跑 `board_gen_reward_test`(绿)+单件跑 `layout_probe`(两档绿)。三条读数进实现报告 T0 节。
|
||||
- 取用:本卡 §2。产出:重锚后的行号与三份改前读数(T1–T4 对拍基准)。
|
||||
|
||||
### 7.2 T1——算式单点提取(估时 0.15 个工作日)
|
||||
|
||||
- [ ] `board_view.gd` 新增 `static func _cell_for(n: int, avail: Vector2) -> int`,函数体=`:195-199` 五行算式逐字搬入(`SEP`/`DEPTH_PX` 仍读本文件既有常量,`clampi(..., 26, 46)` 留在该函数内),返回 `int`。
|
||||
- [ ] `set_cell_area` 的 `:194-199` 六行替换为 `_cell = float(_cell_for(maxi(_board.width, _board.height), avail))`;`:200-209` 副作用尾链一字不动。
|
||||
- [ ] `:189-190` 注释块补一句:算式唯一落点=`_cell_for`(探针与单元测试共用);名字带下划线是 S13 `max-public-methods` 27 顶格的约束,禁改公开名。
|
||||
- [ ] 孤立验证:单件跑 `board_gen_reward_test`(既有四套断言全绿=算式等价性证明)+`python tools/gdstyle_gate.py`(board_view.gd 各键持平,`max-public-methods` 仍 27)。
|
||||
- 取用:T0 读数。产出:`BoardView._cell_for(n: int, avail: Vector2) -> int`(公开静态调用面,T2/T3 逐字消费)。
|
||||
|
||||
### 7.3 T2——探针门槛与最大板推定(估时 0.1 个工作日)
|
||||
|
||||
- [ ] `layout_probe.gd:15` 改 `const MIN_CELL := 30.0`,注释改为「可读下限 30px(battle.md §棋盘格尺寸公式;结构钳制 [26,46] 管算式不越界,是另一回事)」。
|
||||
- [ ] 顶部加 `const BoardViewScript := preload("res://presentation/views/board_view.gd")`(静态调用载体)。
|
||||
- [ ] `_check_one` 在既有格子断言之后追加两段:①board_view 零尺寸前置——`bv.size.x <= 0.0 or bv.size.y <= 0.0` 时记问题「board_view 零尺寸(可用面积不可信,格边长推定无依据)」;②推定断言——对 `n in [13, 14]` 取 `BoardViewScript._cell_for(n, bv.size)`,`< int(MIN_CELL)` 即记问题,消息含实板可用面积、板型、算出的格边长与下限。
|
||||
- [ ] 收口 PASS 消息补一句「13/14 板推定 ≥30px」。
|
||||
- [ ] 孤立验证:单件跑 `layout_probe`(两档全绿)+`tools/probe_gate.sh` 默认段(含本探针在内的默认段退出码 0)。
|
||||
- 取用:T1 `_cell_for`。产出:绿的两档探针日志(T4 对拍基准)。
|
||||
|
||||
### 7.4 T3——格边长冒烟扩四档(估时 0.1 个工作日)
|
||||
|
||||
- [ ] `_smoke_layout_cell_formula` 的 `:57` 循环集改为四档取样表:`[{L:1,D:0,n:10}, {L:2,D:0,n:12}, {L:3,D:0,n:13}, {L:3,D:9,n:14}]`(按 §2 第 6 条的实表取档)。
|
||||
- [ ] 每档追加两条断言:①板型一致——`maxi(b.width, b.height) == 取样 n`(表改动导致取样落错档时红);②算式单点——`int(cell) == BoardViewScript._cell_for(n, avail)`(`set_cell_area` 写入值与纯函数返回同值)。
|
||||
- [ ] 既有两条断言(网格高 ≤ 可用高−24、格边长 ∈ [26,46])逐字保留,仅把消息前缀从 `L%d` 扩成 `L%d D%d`;`:60` 样本 `Vector2(1036, 460)` 不改(本卡把「真实可用面积」的可读面交给探针,样本只承担算式不溢出面)。
|
||||
- [ ] 孤立验证:单件跑 `board_gen_reward_test`(四档全绿)。
|
||||
- 取用:T1 `_cell_for`、T0 取档对账。产出:四档冒烟日志(T4 对拍基准)。
|
||||
|
||||
### 7.5 T4——批末收口与产出物落位(估时 0.05 个工作日)
|
||||
|
||||
- [ ] 批末代码域闸:`tools/run_all_gates.sh --code` 全绿(本批改动含 `game/` 下非 `.md` 行为载体 → 代码域;02 单元与集成按关联档选跑 `board_gen_reward` 套件,冒烟/系统测试/静态扫描三件恒跑)。里程碑步出口跑一次全套;波内修复只做出红最小单元的最窄确认。
|
||||
- [ ] S13 键账对账:board_view.gd 各键持平(`max-public-methods` 27 不变、`max-class-variables` 32 不变、无 NEW 键);探针与测试不在 S13 扫描面。
|
||||
- [ ] **产出物落位卡**(轻闸全绿后、合并前写齐,方算收口):
|
||||
- `reports/iter<N>-实现报告-<日期>.md`——各卡状态+T0 三份改前读数与各卡改后读数对拍+探针实读的 `board_view.size`(两档视口)与 L1 格边长实测值+13/14 推定值+实机目检总清单(登记不阻塞:「第三关 14×14 板格面数字清楚可辨」一项,时间由用户自定)。
|
||||
- `reports/iter<N>-反哺点-<日期>.md`——§5 同步面第 1 行(battle.md 算例表 vs 实测)逐条有落点。
|
||||
- `reports/iter<N>-验收审计-<日期>.md`——独立验收审计(未参与实现的会话或子代理执行,未过不得合并)。
|
||||
- 取用:T1–T3 全部产出。产出:代码域闸回执+三份 reports/ 产出物。
|
||||
|
||||
## 8. 验证闸(改动—验收对照表)
|
||||
|
||||
本批三处改动全部落在守卫面(探针门槛/测试覆盖/算式落点),游戏行为零变化,故无新玩法验收;用户目检一项登记不阻塞(T4)。对照表 5 行对应 §1 完成定义第 1–5 条。
|
||||
|
||||
| # | 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 1 | 探针可读门槛与 battle.md 对齐(36→30) | `layout_probe.gd:15` 常量与注释;`:92-94` 断言文本沿用 | 「layout_probe 两档几何不变量」`layout_probe.gd:_ready`(尾缀〔格边长 V1〕) | 探针 | `godot --headless --path game res://probe/layout_probe.tscn` | 探针经真实战斗屏装配读到 `board_view.cell_size` 且 ≥30px(下限判据直接作用于实机取值) | 新增:战斗屏三区几何与棋盘格边长(分辨率两档×四档板型) |
|
||||
| 2 | 算式单点(`_cell_for`) | `board_view.gd:191-209` 抽函数并改调;`:189-190` 注释补唯一落点句 | 「格边长算式单点等价」`board_gen_reward_test.gd:_smoke_layout_cell_formula`(尾缀〔格边长 V2〕) | 单元 | `godot --headless --path game --script res://tests/unit/core/board_gen_reward_test.gd` | `set_cell_area` 写入的 `_cell` 与 `BoardView._cell_for(n, avail)` 返回值逐档严格相等(同一事实两路读取,分叉即红) | 新增:战斗屏三区几何与棋盘格边长(分辨率两档×四档板型) |
|
||||
| 3 | 13×13/14×14 进入冒烟 | `board_gen_reward_test.gd:53-69` 循环集改四档取样+板型一致断言 | 「格边长冒烟四档板型」同函数(尾缀〔格边长 V3〕) | 单元 | 同上 | 13/14 两档首次被断言:取档尺寸与实表一致、网格高 ≤ 可用高−24、格边长 ∈ [26,46](此前从未执行) | 新增:战斗屏三区几何与棋盘格边长(分辨率两档×四档板型) |
|
||||
| 4 | 最大板可读推定(实板可用面积) | `layout_probe.gd:_check_one` 新增 board_view 零尺寸前置+`n∈{13,14}` 推定断言 | 「最大板格边长推定」同探针(尾缀〔格边长 V4〕) | 探针 | `godot --headless --path game res://probe/layout_probe.tscn` | 以探针实读 `board_view.size` 代入 `_cell_for(13/14, size)` 得 ≥30px(真实容器尺寸下的最大板可读面——不推关即可覆盖) | 新增:战斗屏三区几何与棋盘格边长(分辨率两档×四档板型) |
|
||||
| 5 | 批末收口 | 全批 | 代码域闸:`tools/run_all_gates.sh --code` | 全量闸 | `tools/run_all_gates.sh --code` | 所选域全绿回执(`game/tests/.gates/receipt.json`) | 新增:战斗屏三区几何与棋盘格边长(分辨率两档×四档板型) |
|
||||
|
||||
**测试削弱申报**(实现期允许的旧断言改动全集,超出即审计判红):
|
||||
1. **探针门槛 36→30=阈值放宽**,依据 battle.md `:144`「可读下限 30px」(旧值 36 是 12×12 旧锚点的误用,探针实跑从未触及 30–35px 区间,等于该门槛对大板无效)。补偿=第 4 行新增推定断言与第 3 行四档冒烟——原门槛守不住的面由两者接管。
|
||||
2. 第 3 行循环集从 2 档扩到 4 档=纯增量;`avail` 样本值不改;既有两条断言(网格高、区间)逐字保留。
|
||||
3. 探针新增断言=纯增量,既有格子断言与两档视口表不改。
|
||||
4. 显式禁止:删任何旧断言、放宽区间 [26,46]、把格子读取从 `cell_size`/`_cell` 换成别的入口。
|
||||
|
||||
**独立验收审计**:入口与报告=`reports/iter<N>-验收审计-<日期>.md`;审计方=未参与实现的会话或子代理,逐行核对对照表两端、抽查判据绑定面是否绑在被测改动特有的可观测面(`cell_size`/`_cell` 取值与 `_cell_for` 返回)、核对削弱申报、确认全文无变异/撤销类取证;未过不得合并。
|
||||
|
||||
## 9. 异常分级
|
||||
|
||||
- **L1 自救**:单件跑红→定位该件最小单元修后重跑;探针单件超时→孤立复跑该件;`--only` 选跑零命中(用法错 exit 2)→修正选词重跑。
|
||||
- **L2 记录继续**:同一红连续 3 次同因→在实现报告「实现决定记录」记因后继续最窄确认。
|
||||
- **L3 停机写报告等用户**:
|
||||
1. **机检预算互斥=上游缺陷停卡退回写卡段**——`gdstyle_gate` 出 NEW 键或 `board_view.gd|quality/max-public-methods` 升到 28(即 `_cell_for` 的下划线被去掉);本批零新增文件、零新增成员,任何键恶化都不许改基线换绿,禁开豁免。
|
||||
2. **实板可读面本身不达标**——探针实测 L1 `cell_size` < 30,或实板可用面积代入 13/14 后 < 30。这是产品面问题(不是断言错),停卡呈报,禁调低下限、禁换样本凑绿。
|
||||
3. **四档取档对不上**——`resolve_row(1,0)/(2,0)/(3,0)/(3,9)` 实测尺寸与 §2 第 6 条不符(表已改而本卡未跟上)。
|
||||
4. **算式等价性破**——T1 后既有四套断言出现改前不存在的红,说明提取改变了算式语义。
|
||||
|
||||
## 10. 代拍板白名单(逐条列,超清单记实现报告「实现决定记录」)
|
||||
|
||||
1. `_cell_for` 内部局部变量名与注释措辞(算式、常量、clamp 数值不变)。
|
||||
2. 四档取样表用字典数组还是三元组数组(取值与断言不变)。
|
||||
3. 探针推定断言与零尺寸前置的失败消息文案、PASS 消息措辞(判据不变)。
|
||||
4. 尾缀〔格边长 V1〕–〔格边长 V4〕的字样编排(含义不变)。
|
||||
5. `BoardViewScript` 常量名(preload 载体命名)。
|
||||
|
||||
## 11. 待裁点
|
||||
|
||||
**零待裁点,开工门=U0。** 2026-10-30 用户三项裁定已定案:①探针不扩到 L3(推关成本与红因混淆,最大板改走推定断言);②最紧档 14×14 的断言落两处(单元四档+探针实板推定);③算式提取用 `_cell_for` 私有名(§2 第 5 条 S13 实测依据)。编批由迭代编排段定。
|
||||
|
||||
## 12. 落位与可复用性预检
|
||||
|
||||
**复用面**:本批零新增文件、零新增类。`_cell_for` 抽自 `board_view.set_cell_area` 既有内联算式——不能只留在原处内联,是因为探针要在「非当前板型(13/14)× 真实可用面积」上求值、单元测试要在四档板型上求值,内联形态无法复用,复写算式就是造第二口径(正是本卡要消灭的缺陷形态)。探针与测试的既有工具件(`probe_ui_util.gd`、`test_kit.gd`)与本批职责无关,不复用也不扩展。
|
||||
|
||||
**唯一面**:算式唯一落点=`BoardView._cell_for`;可读下限 30px 的唯一代码判据面=`layout_probe.MIN_CELL`(文本出处=battle.md `:144`);四档板型唯一事实源=`board_gen.tres`(测试按 (L,D) 取行,不复制取值)。2026-10-30 实查 `game/` 下无 `cell_for`/`_cell_for` 同名或同义函数,无第二落点。
|
||||
|
||||
**分层与交互面**:`_cell_for` 是无状态纯函数——零成员读写、零信号、零 RNG、零 UI 依赖、零 `load()`;调用路径=`set_cell_area`(生产)/探针 `_check_one`(经脚本常量静态调用)/单元测试(同前)。preservation:不新增跨层依赖,Core 零 UI 红线零触碰(本批不触 `core/`)。
|
||||
|
||||
## 13. 实现规范预算
|
||||
|
||||
**新件三查**:本批零新增文件——三查不适用(写「无新增件」)。
|
||||
|
||||
**存量触面一查**:触碰的存量件为 `board_view.gd`(S13 扫描面内)与探针/测试两件(不在 S13 扫描面)。`board_view.gd` 预期:`quality/max-public-methods` 保持 **27**(私有静态不计数,§2 第 5 条实测)、`quality/max-class-variables` 保持 32、`quality/max-branches|_paint_cell` 等函数级键持平(本批不增分支、不增成员、不增局部变量——`set_cell_area` 局部变量反而减少)。达成手段:算式整体搬出,函数体只留一行调用。
|
||||
|
||||
**指标例外申报**:无。
|
||||
|
||||
## 14. 源码级断言与探针契约面的随批义务(防漏项清单)
|
||||
|
||||
1. `layout_probe` 的既有两档视口表(`VIEWPORTS`)与三条几何不变量(三区非零/不越视口/互不重叠/view 含于 D 区)本批零改动——T2 只追加断言,不改判据。
|
||||
2. `board_view` 的对外只读出口(`cell_size`,`board_view.gd:93-95`)是探针取值面,本批不动;测试内部读 `_cell`(测试惯例已如此,`board_gen_reward_test.gd:63`),不改为公开出口。
|
||||
3. `game/tests/README.md` 能力行由写卡段同批入表(本卡成文时已落),实现批不写该文件;批末核对表上名字与实况一致。
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# 本地 CI 跑批件——合回 master 后的全量验证执行器(git 钩子触发、后台运行,2026-10-30 用户裁)。
|
||||
# 触发语义(钩子只负责拉起,判定在本件):
|
||||
# merge 模式=合回验证点:主工作区 git merge 落地后(tools/hooks/post-merge)对 master 尖无条件
|
||||
# 跑一次全量域闸;任务 worktree 内的合并不触发(钩子里限主工作区)。
|
||||
# commit 模式=红态追绿:仅当最近一次 CI 判红时(tools/hooks/post-commit)本提交触发复验;
|
||||
# 绿态与无状态零触发——小批直提不重复跑全量,有意义的重跑只剩「验证修复」一类。
|
||||
# 并跑语义=跑完补跑(coalesce,2026-10-30 用户裁):跑批期间新触发不中断当前轮——锁被占时,
|
||||
# 在跑实例的尖端就是当前尖端则本触发无事可做,否则落 ci.rerun 标记;当前轮跑完见标记或尖端
|
||||
# 前移,对最新尖端自动补跑一轮。绝不逐笔触发各跑一轮,最终只测最新尖端。
|
||||
# 产物(均不入库,本目录 .gitignore 已收):ci-status.json=最新一跑判定(commit/verdict/红闸/
|
||||
# 关键日志行/耗时),ci-history.jsonl=逐次流水账,ci.log=当轮全量闸完整输出,
|
||||
# ci.lock=锁目录(内含在跑尖端),ci.rerun=补跑标记。
|
||||
# 用法: tools/ci_runner.sh merge|commit
|
||||
set -u
|
||||
REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
GATES_DIR="$REPO/game/tests/.gates"
|
||||
LOCK="$GATES_DIR/ci.lock"
|
||||
RERUN="$GATES_DIR/ci.rerun"
|
||||
STATUS="$GATES_DIR/ci-status.json"
|
||||
HISTORY="$GATES_DIR/ci-history.jsonl"
|
||||
LOG="$GATES_DIR/ci.log"
|
||||
GODOT="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}"
|
||||
export PYTHONIOENCODING=utf-8
|
||||
|
||||
MODE="${1:-commit}"
|
||||
|
||||
# ── 触发判定 ──
|
||||
if [ "$MODE" = "commit" ]; then
|
||||
[ -f "$STATUS" ] || exit 0
|
||||
grep -q '"verdict": "red"' "$STATUS" || exit 0
|
||||
fi
|
||||
|
||||
# ── 锁(mkdir 原子);被占时按在跑尖端判定:同尖=已在验这份字节,异尖=落补跑标记 ──
|
||||
TIP="$(git -C "$REPO" rev-parse HEAD)"
|
||||
if ! mkdir "$LOCK" 2>/dev/null; then
|
||||
if [ "$(cat "$LOCK/tip" 2>/dev/null)" = "$TIP" ]; then
|
||||
exit 0
|
||||
fi
|
||||
touch "$RERUN"
|
||||
exit 0
|
||||
fi
|
||||
trap 'rm -rf "$LOCK"' EXIT
|
||||
|
||||
# ── 跑批主循环(跑完补跑)──
|
||||
while :; do
|
||||
rm -f "$RERUN"
|
||||
TIP="$(git -C "$REPO" rev-parse HEAD)"
|
||||
echo "$TIP" > "$LOCK/tip"
|
||||
_start="$(date +%s)"
|
||||
|
||||
# 起手刷类名缓存:.godot 缓存不入 git,与合并后代码失配会假红假绿(在册坑)——
|
||||
# 原「合并后零复跑」的秒级动作由本件接管,全量闸跑的就是刷新后的字节
|
||||
"$GODOT" --headless --path "$REPO/game" --editor --quit >>"$LOG" 2>&1
|
||||
|
||||
bash "$REPO/tools/run_all_gates.sh" >"$LOG" 2>&1
|
||||
RC=$?
|
||||
_end="$(date +%s)"
|
||||
|
||||
# 判定与记录:读闸脚本写的回执(all_green/各闸退出码),红项关键行取自本轮 ci.log
|
||||
python - "$REPO" "$TIP" "$RC" "$((_end - _start))" <<'PY'
|
||||
import json, re, sys, datetime
|
||||
repo, tip, rc, dur = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4])
|
||||
gates_dir = repo + "/game/tests/.gates"
|
||||
entry = {"commit": tip,
|
||||
"timestamp": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"exit_code": rc, "verdict": "green" if rc == 0 else "red",
|
||||
"duration_s": dur, "red_gates": [], "key_lines": []}
|
||||
try:
|
||||
with open(gates_dir + "/receipt.json", encoding="utf-8") as f:
|
||||
r = json.load(f)
|
||||
entry["red_gates"] = [g["name"] for g in r.get("gates", []) if str(g.get("exit")) != "0"]
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
with open(gates_dir + "/ci.log", encoding="utf-8", errors="replace") as f:
|
||||
lines = [l.rstrip() for l in f]
|
||||
entry["key_lines"] = [l for l in lines if re.search(r"FAIL|红项|Error|error", l)][-12:]
|
||||
except Exception:
|
||||
pass
|
||||
with open(gates_dir + "/ci-status.json", "w", encoding="utf-8") as f:
|
||||
json.dump(entry, f, ensure_ascii=False, indent=1)
|
||||
with open(gates_dir + "/ci-history.jsonl", "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||
PY
|
||||
|
||||
# 轮尾补跑判定:跑批期间有新触发(标记在)或尖端前移 → 对最新尖端再来一轮
|
||||
_newtip="$(git -C "$REPO" rev-parse HEAD)"
|
||||
if [ ! -f "$RERUN" ] && [ "$_newtip" = "$TIP" ]; then
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# 红态追绿钩子(本地 CI 触发钩子,2026-10-30 用户裁)——仅当最近一次 CI 判红时,本提交触发
|
||||
# 后台复验(tools/ci_runner.sh commit,判定在跑批件内);绿态与无状态零触发——
|
||||
# 小批直提不重复跑全量,同一份字节禁止第二次人手跑。主工作区限定。
|
||||
set -u
|
||||
_GD="$(cd "$(git rev-parse --git-dir)" && pwd)"
|
||||
_CD="$(cd "$(git rev-parse --git-common-dir)" && pwd)"
|
||||
[ "$_GD" = "$_CD" ] || exit 0
|
||||
REPO="$(git rev-parse --show-toplevel)"
|
||||
( nohup bash "$REPO/tools/ci_runner.sh" commit >/dev/null 2>&1 & )
|
||||
exit 0
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# 合回验证点(本地 CI 触发钩子,2026-10-30 用户裁)——主工作区 git merge 落地后无条件触发
|
||||
# 全量域闸后台跑批(tools/ci_runner.sh merge,约 4 分钟,不阻塞合并动作本身)。
|
||||
# 任务 worktree 内的合并不触发(CI 只认 master;worktree 批要提前验证就手动跑 ci_runner.sh)。
|
||||
# CI 红了不回滚合并——红项记入 game/tests/.gates/ci-status.json,修复提交经 post-commit 追绿。
|
||||
set -u
|
||||
_GD="$(cd "$(git rev-parse --git-dir)" && pwd)"
|
||||
_CD="$(cd "$(git rev-parse --git-common-dir)" && pwd)"
|
||||
[ "$_GD" = "$_CD" ] || exit 0
|
||||
REPO="$(git rev-parse --show-toplevel)"
|
||||
( nohup bash "$REPO/tools/ci_runner.sh" merge >/dev/null 2>&1 & )
|
||||
exit 0
|
||||
+8
-24
@@ -1,31 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# 直改车道闸(git pre-commit,2026-10-26 车道铁律)——staged 不含代码面的提交,提交点现场跑
|
||||
# 文档域闸(tools/run_all_gates.sh --docs,秒级文本扫描):绿=落地,红=中止。
|
||||
# 代码面=game/ 下非 .md 的行为载体(判据与改动域划分同一)。staged 含代码面时:
|
||||
# 主工作区=拦(代码面唯一合法路径=worktree 批合回);任务 worktree=跳过(归批末闸+合回闸管,
|
||||
# 不打扰迭代批中间提交)。--no-verify 绕过仅属用户(agent 侧由 worktree-guard 拦截)。
|
||||
# 直改车道闸(git pre-commit,2026-10-30 本地 CI 化换代)——提交点现场跑文档域闸
|
||||
# (tools/run_all_gates.sh --docs,秒级文本扫描):绿=落地,红=中止。
|
||||
# 代码面提交不再拦(2026-10-30 用户裁:主区直改全面放开)——代码验证归合回后的本地 CI
|
||||
# (tools/hooks/post-merge 全量跑批;CI 红态下 tools/hooks/post-commit 追绿复验)。
|
||||
# 合并收尾提交跳过(MERGE_HEAD 在=冲突合并的收尾 commit)——内容归合回后的 CI 全量验。
|
||||
# --no-verify 绕过仅属用户(agent 侧由 worktree-guard 拦截)。
|
||||
set -u
|
||||
REPO="$(git rev-parse --show-toplevel)"
|
||||
|
||||
# staged 路径集(首提交无 HEAD 时对 --root 比);空集放行,交回 git 自身报「无改动可提」
|
||||
if git rev-parse --verify -q HEAD >/dev/null 2>&1; then
|
||||
STAGED="$(git diff --cached --name-only)"
|
||||
else
|
||||
STAGED="$(git diff --cached --name-only --root)"
|
||||
fi
|
||||
[ -z "$STAGED" ] && exit 0
|
||||
|
||||
# 代码面判定:staged 里任一路径落在 game/ 且非 .md
|
||||
if printf '%s\n' "$STAGED" | grep '^game/' | grep -viE '\.md$' | grep -q .; then
|
||||
# 主区判定:git-dir 与 common-dir 同一=主工作区(非链接 worktree)
|
||||
_GD="$(cd "$(git rev-parse --git-dir)" && pwd)"
|
||||
_CD="$(cd "$(git rev-parse --git-common-dir)" && pwd)"
|
||||
if [ "$_GD" = "$_CD" ]; then
|
||||
echo "[直改闸] 主工作区禁止直提代码面(game/ 下非 .md)——代码面改动走迭代批独立 worktree(git worktree add ../g-plan2-<任务名> -b <任务分支>),收尾在主工作区 git merge --no-ff 合回。" >&2
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
# 合并收尾提交:跳过文档闸,交合回后 CI
|
||||
[ -f "$(git rev-parse --git-dir)/MERGE_HEAD" ] && exit 0
|
||||
|
||||
RC=0
|
||||
bash "$REPO/tools/run_all_gates.sh" --docs || RC=$?
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# 验证闸唯一入口(全部验证闸的清单与域划分,批末与合回按改动域跑)——
|
||||
# 闸清单的唯一定义处。文档只写「收口跑本脚本」,禁在文档里另列闸件清单(那正是 iter27
|
||||
# 验证闸唯一入口(全部验证闸的清单与域划分)——
|
||||
# 闸清单的唯一定义处。文档只写「验证跑本脚本」,禁在文档里另列闸件清单(那正是 iter27
|
||||
# 漏跑 probe_gate --full 的成因:清单散落三份文档、彼此不一致、且无机器校验)。
|
||||
#
|
||||
# 跑完写回执 game/tests/.gates/receipt.json(commit+内容树哈希+各闸退出码+存量基线比对+时间戳);
|
||||
# .zcode/hooks/worktree-guard.sh 在主工作区 git merge 时校验本回执,缺或红即拦截。
|
||||
#
|
||||
# 本脚本是合回前的终局动作:跑之前先冻结(代码与断言不再改)并把 master 并入本分支;每批只跑
|
||||
# 一次。回执必须覆盖住分支尖的代码面(game/ 下非 .md 的行为载体)——闸后再改一个代码面文件,
|
||||
# 回执即作废须重跑;跑闸时若代码面还带着未提交改动,回执会标 dirty_game,合回闸拒绝采信。
|
||||
# 本地 CI(tools/ci_runner.sh,2026-10-30 用户裁)读回执写 ci-status.json——强制验证点=合回 master
|
||||
# 后 post-merge 钩子触发的全量跑批;本脚本手动调用(--code/--docs/全量档)只服务开发期按域跑批。
|
||||
# 跑闸时代码面带着未提交改动,回执会标 dirty_game(验的不是 HEAD 字节),CI 判定照常落账留痕。
|
||||
#
|
||||
# 用法:tools/run_all_gates.sh [--code|--docs|--quick]
|
||||
# --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 场景探针/
|
||||
@@ -98,7 +96,7 @@ fi
|
||||
# 每次跑批前自愈重建,避免「清目录重跑」把 .gitignore 一起带走。
|
||||
mkdir -p "$STAMP_DIR"
|
||||
if [ ! -f "$STAMP_DIR/.gitignore" ]; then
|
||||
printf '# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。\n# receipt.json 是每跑批覆写的当次回执、*.log 是跑批当场日志——两者都是中间产物,不入库。\nreceipt.json\n*.log\n' \
|
||||
printf '# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。\n# receipt.json 是每跑批覆写的当次回执、ci-status.json/ci-history.jsonl 是本地 CI 判定与流水账、\n# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志——均为中间产物,不入库。\nreceipt.json\nci-status.json\nci-history.jsonl\nci.lock\nci.rerun\n*.log\n' \
|
||||
> "$STAMP_DIR/.gitignore"
|
||||
echo "[gates] 已自愈重建 $STAMP_DIR/.gitignore"
|
||||
fi
|
||||
|
||||
@@ -2,21 +2,6 @@
|
||||
|
||||
本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。
|
||||
|
||||
#### 迭代 42 批末移交小件——工具与注释四条(2026-10-30 登记,来源=批次移交)
|
||||
|
||||
**实况**:证据=迭代 42 实现报告 §5-1/§5-5/§5-6 与验收审计报告表述性瑕疵 3/4(已按吸收即死退役,git 历史可考)。逐条:
|
||||
|
||||
| # | 事项 | 要点 |
|
||||
|---|---|---|
|
||||
| 1 | run_all.gd --only 逗号形态解析缺陷 | 头注文档形态 `--only=<词[,词…]>` 与闸脚本传参形态(tools/run_all_gates.sh 关联档选跑逗号拼接单旗标)自 6caf56b5 起与解析不符——整串当一个词对套件路径做子串匹配,多词必零命中 quit(2);修法二选一:run_all.gd 补逗号拆分,或闸脚本改逐词重复 `--only=` 传参(迭代 42 施工曾以逐词重复形态绕过) |
|
||||
| 2 | 坑速查补条 | .zcode/skills/iter/SKILL.md「坑速查」节补条:worktree 冷检出 --editor --quit 首跑 --path 误指仓库根(未建 .godot 类缓存致首跑解析红),改指 game 切片目录重刷后正常 |
|
||||
| 3 | level_runner.gd:12 node_entered 信号注释陈旧 | 注释 `shop={gp}` 与 pool 态实际载荷 `{gp, shelf:true}` 不符(迭代 42 改动前即如此,本批未触载荷) |
|
||||
| 4 | static_scan.sh S8 ok 消息陈旧字面量 | :337「扫描面 B1 文件集 9 个 .gd」——S8_FILES 实含 10 文件(board_installer.gd 随迭代 42 入集),扫面无缩小,仅消息文本过期 |
|
||||
|
||||
**改动面**:游戏代码+工具代码
|
||||
|
||||
**状态**:待执行
|
||||
|
||||
#### 迭代 43 批后待裁三点——B 区高度对齐/名字行字号档位/换色常量退役(2026-10-30 登记,来源=批次移交)
|
||||
|
||||
**实况**:证据=迭代 43 反哺点清单 1/8/10(已按吸收即死退役,git 历史可考)。①**B 区高度 270 vs 252**——battle.md 区表文 270、代码 252,18px 差异;样张按代码 252 出(样张反映实机)。②**站桩名字行 12px 档位归属**——standee_body.gd 名字行取 `UiKit.FS_CAPTION`(12px),art-bible §7.4 字号档位表下属「清单外 11–13px」。③**art-bible §4.1 换色合并残余三处**——`STAGE_BG` 换值(#3E342A→#3E362C)未执行;`STAND_STONE_RIM`(#C4AC84)仍被 board_cell_unit 口沿高光在役引用;`STAND_STONE_EDGE`(#5A4C3A)由地台缺位回退路径在役引用。
|
||||
@@ -68,14 +53,6 @@
|
||||
|
||||
**状态**:文档设计已闭合
|
||||
|
||||
#### 恒真与名实不符断言五条——判据缺陷修正(2026-10-29 登记,来源=会话提及)
|
||||
|
||||
**实况**:存量断言精简 A+B 批余账④转正:五条断言属判据缺陷(恒真占位或名实不符),不是冗余合并可解的账,按 test-conventions §8.4 真实入口口径逐条修正,不计入精简条数。逐条:①`game/tests/unit/core/feat_vouchers_test.gd:51` cond 为 `deck_refresh_before == 0 or true`——恒真;②`game/tests/unit/core/treasure_effects_test.gd:263` cond 尾缀 `or true`——恒真;③`game/tests/unit/core/level_shop_economy_test.gd:593` `green_min_hp > 0`——初值 99999、仅 hp>0 分支取 min,恒真;④同文件 `:696` `green3_min_hp > 0`——同型恒真;⑤`game/tests/unit/contracts/system_contracts_test.gd:66-67` 标签称「非空且互异」而 cond 只查 `size() > 0`——互异半缺失,名实不符。修正方向:能绑判据的改绑真实可观测面(互异改值集去重计数、HP 读数改与门槛比对),造不出可观测面的按 §8.4 出口停卡呈报。
|
||||
|
||||
**改动面**:游戏代码
|
||||
|
||||
**状态**:待执行
|
||||
|
||||
#### 断言提取器改版——覆盖 Kit.check_quiet 静默通道(2026-10-29 登记,来源=会话提及)
|
||||
|
||||
**实况**:`tools/extract_checks.py:67` 的 `CALL_RE = check\(` 只认即时通道,静默收集通道 `Kit.check_quiet(cond, desc)` 静态站点实测 407 个在提取口径外(证据=`grep -rn "check_quiet(" game/tests --include="*.gd"` 实数;test-conventions §5.2 已备案该工具局限)。能力清单以 `文件:函数` 为锚,不受影响;但要把断言面全账纳入机器对账(重建账本或全量断言面审计),须先改版提取器:识别 `check_quiet(` 并按反参序(cond 在前)提取标签与 cond。改版后须以全量套件跑一遍 `--diff` 自证提取面覆盖、且 `Kit.check(` 与 `check_quiet(` 不双计。
|
||||
@@ -92,14 +69,6 @@
|
||||
|
||||
**状态**:待执行
|
||||
|
||||
#### 闸基线 02/05/09 三键换代——存量红指纹已消除或现行不复现(2026-10-29 登记、2026-10-30 并入 05,来源=批次移交)
|
||||
|
||||
**实况**:证据=迭代 41 测试债批代码域闸实测(worktree 分支 `testdebt-20261029`,回执 commit afd1a41f、域=code、scope=full、零新增红),02 单元与集成与 09 静态扫描两闸判「基线红 → 现绿(改善)」;05 渲染闸在 2026-10-30 脚本错误扫描批主区真跑 `bash tools/render_gate.sh` exit=0 全绿(shot/fx/board 三段扫描零命中、残留 Godot=0、golden changed 0.00%),其红键在现行主区不复现。基线 `game/tests/.gates/baseline.json` 里这三键仍记着历史红指纹(02 fp f9865f1a5a44=9 套件失败;05 fp de89589e49ed;09 fp 1a9ad773489f=S9 24 处+S10 4 处+S11 1 处),留着不动的后果=日后同类红回归会被判「存量红·对基线零新增」静默放行。待做=三键按归绿实测重生成(写 exit=0/绿指纹),改的是 `game/` 下非 .md 文件=代码面,须随下一个代码批在 worktree 内一并落、禁主区直改;05 须在素材齐盘预检后跑出真值(冷检出空盘态下的渲染红不作数)。人工确认凭据=上述回执、reports/迭代41测试债批-实现报告-2026-10-29.md 第三节与本批提交说明。本条目无设计段(机械重锚),不走写卡与编排。
|
||||
|
||||
**改动面**:游戏代码
|
||||
|
||||
**状态**:待执行
|
||||
|
||||
#### gdstyle 尾波补件批——悬停域与断点域拆件(2026-10-29 登记,来源=会话提及)
|
||||
|
||||
**实况**:尾波卡 v2.0 裁去的 D2/D4 两拆件项经用户再裁恢复(2026-10-29,零豁免延伸+D5 留债)。设计闭合凭据=architecture.md v1.25(呈现层表 board_hover 行/核心层表 breakpoint_store 行/「冒险域拆分」节随动句)。需求卡片:reports/需求卡片-gdstyle尾波补件批-2026-10-29.md(v1.0 定稿零待裁点)。范围:board_view 拆 board_hover(11 函数族+2 成员,`_r0` 留宿主)+adventure_runner 拆 breakpoint_store(6 成员+`BREAKPOINT_FIELDS` 27 键冻结函数族,`resume_adventure` 留宿主薄壳)。键账预期:board_view 类变量 32→约30、公开方法 27 持平、adventure_runner 类变量 28→约22——三键**留债**(键留基线冻结禁恶化、禁落豁免登记)。编队:待迭代 42 合回后编入;与呈现层状态件批同代时其先行(u.st 写入面改型后 hover 拆件在新面上施工)。估时 2.5–3.5 工作日。
|
||||
|
||||
Reference in new issue
Block a user