Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a113331b6 | ||
|
|
4e4232d804 | ||
|
|
52296872e3 | ||
|
|
1243ffe632 | ||
|
|
5a0e7069c1 | ||
|
|
98470adc4d | ||
|
|
4b5e3f27e8 | ||
|
|
aa4aa1dd20 | ||
|
|
3a9c6ff453 | ||
|
|
5ad72f44a0 | ||
|
|
c585a6bd73 | ||
|
|
c009cf7b1e | ||
|
|
b991f52ac2 | ||
|
|
cd121b18c2 | ||
|
|
83a4fbf5c5 | ||
|
|
70d071f850 | ||
|
|
8be5d425ca | ||
|
|
4554e0e702 | ||
|
|
b75688c6ca | ||
|
|
ab5a45af00 | ||
|
|
afee92476d | ||
|
|
b5433285b4 | ||
|
|
546ed1d611 | ||
|
|
37ed6ab426 | ||
|
|
5269ed4080 | ||
|
|
5ecfabc251 | ||
|
|
57084e0bd3 | ||
|
|
12af4fe00c | ||
|
|
22809f9b79 | ||
|
|
94dfa7e79b | ||
|
|
7fd50fc485 | ||
|
|
e7090bc9c1 | ||
|
|
3a8634ab92 | ||
|
|
92e9c70a8d | ||
|
|
4a2ed96182 | ||
|
|
33525d19de | ||
|
|
3499638dee | ||
|
|
e075f92e0f | ||
|
|
34bb62a55b | ||
|
|
ceb816cd39 | ||
|
|
94628fca83 | ||
|
|
870b2f2bf6 | ||
|
|
3a1878ff80 | ||
|
|
d216ae0498 | ||
|
|
e090ea8af2 | ||
|
|
263050adf1 | ||
|
|
eb5dc1e7cb | ||
|
|
aba54ab228 | ||
|
|
3fa99583fb |
No files matched your search
@@ -16,7 +16,7 @@
|
||||
|
||||
## 运行与验证(工作目录 = 含 project.godot 的切片目录)
|
||||
|
||||
- **合回验证 = 本地 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),不构成验收;全量回归由用户自发发起
|
||||
- **合回验证 = 本地 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-merge 钩子复验追绿(红态修复走新开修复 worktree,`fix-<任务名>` 分支,免重塑免数卡;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)、合回后本地 CI、五段流水线、批次清单、测试两入口与 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`——探针标配 / 回归断言 / 提交前验证 / 验收清单落码 |
|
||||
@@ -83,7 +83,7 @@
|
||||
|
||||
**判据**:描述含多个实体、实体间有明确关系(先后顺序、包含、依赖、状态转移、布局对比)的内容时,直接画图说明,不写长段文字;单点事实(一句装得下)不画,纯数据对照用表格。
|
||||
|
||||
**载体**:默认用 visualize 技能出 HTML/SVG(会话内联渲染,表现力优先),流程/状态/结构/实体关系与空间对比、像素级示意、界面草图同一通道;查看端不支持内联渲染时退回 Mermaid 代码块,仍不行用字符画。
|
||||
**载体**:用 diagram-design 技能出 HTML/SVG 内联交付(流程/状态/结构/实体关系与空间对比、像素级示意、界面草图同一通道),大图精修与导出用 archify 技能;查看端不支持内联渲染时用字符画。
|
||||
|
||||
**边界**:硬数值与前提条件留在文字里,图不承载唯一数值依据;图与文禁重复说同一件事,文字只留图说不清的部分。
|
||||
|
||||
@@ -93,12 +93,12 @@
|
||||
|
||||
## 硬性红线(勿违反;操作细则在 process-conventions.md 与各规约)
|
||||
|
||||
- 车道铁律(判据与改动域划分同一,按文件性质切;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)
|
||||
- 车道铁律(2026-10-31 用户裁恢复)——主工作区只作合并落点,一切开发/设计改动走独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`(建区即跑 `tools/setup_worktree_userdir.sh`),收口=冻结→件核→并 master→重塑→主区 `git merge --no-ff`→删区(合回即删,2026-09-19 原裁),CI 红不回滚合并——新开修复 worktree(`fix-<任务名>` 分支,免重塑免数卡)修复合回追绿;主区唯一合法写动作=合并,agent 侧由 worktree-guard 拦主区 Edit/Write 与非 merge 提交;豁免面仅 `.zcode/`、`.qoder/`(不入 git,主区直改);commit 点零钩子检查(pre-commit 直改闸已退役),全量验证归合回后本地 CI(`tools/hooks/post-merge` 触发,见「运行与验证」首条);`--no-verify` 仅属用户,agent 禁用(细则见 process-conventions §1)
|
||||
- **仓库根禁止设会话状态文件**(`active.md` 已废除)——现状看 roadmap「当前位」,待办看待办清单,过程看 git 提交信息,产出物看 `reports/`;任何文档禁止另设「当前状态快照」节,两处记同一件事必然漂移(细则见 doc-conventions 第 10 节)
|
||||
- 测试执行两入口铁规——只有迭代 loop 开发与用户点名两个触发入口;其余会话零测试,需测试支撑先请示并注明未跑测
|
||||
- 合回后验证 = 本地 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)
|
||||
- 合回后验证 = 本地 CI 全量域闸全绿(唯一强制验证点;唯一入口、唯一闸清单、唯一域划分与选闸算法仍只在 `tools/run_all_gates.sh`,禁在文档另列;触发与追绿语义见「运行与验证」首条与 process-conventions §1)。开发期手动跑批按需选档(`--code` 关联档选测——02 跑「卡片申报 ∪ diff 推导」并集套件、三件保险恒跑、横切面回落全量——算法只在闸脚本内定义),手动跑不构成验收;全量回归由用户自发发起。文档面批与代码面批验收同走合回 CI 全量(`--docs` 仅属开发期手动档,process-conventions §1)
|
||||
- **改动四类**——**需求改动**(游戏代码,含随逻辑写/改的测试代码):唯一走「写需求卡片+编入迭代批次」通道;迭代实现批禁写 `design/` 整个目录、两份根流程清单(`待办清单.md`/`体验验证清单.md`)与 `game/tests/README.md`(能力清单,归写卡段维护)(含新增/修改/删除),设计文档对它只读、是写测试与实现代码的输入依据;两份流程清单的条目状态只由流水线各段推进,实现批禁止顺手改;产出物只准写 `reports/`;合回后由本地 CI 全量验证(红项修复合回追绿,process-conventions §1)。**工程改动**(tools/、闸件、模拟测量工具、纯测试维护——不改变游戏行为的配套代码)、**文档设计改动**、**美术素材改动**:不写需求卡片、不编迭代批次,由用户发令走独立 worktree 直改+合回(车道铁律,commit 点零钩子检查);大体量改动(含触及代码面的工程改动如纯测试维护)同车道无特殊通道;文档设计类写入走 doc-design 技能(登记条目格式由流水线闸强制)
|
||||
- 五段流水线铁律——**需求改动**(游戏代码)按「登记 → 文档设计/美术设计 → 写需求卡片 → 迭代编排 → 实现」五段推进,段间不自动流转、每段转进由用户发令;编入迭代的卡片必须设计闭合+零待裁点——需求卡片任何工作量皆可写、皆可编入迭代,工作量限制只属迭代层(迭代估时总量 <5 个工作日=「冻结待凑批」、不可开工);流水线闸 `tools/pipeline_gate.py` 强制(已注册进 run_all_gates.sh 文档域);实现段零裁断——卡片没写清=上游缺陷,停该卡退回上游,禁内联求裁(process-conventions §2,格式与状态机=doc-conventions §12)。工程/文档设计/美术素材三类改动不走写卡与编排段:(如需设计讨论)→ 用户发令 → 独立 worktree 直改 → 合回 CI(车道细则=process-conventions §1)
|
||||
- MC/模拟:无数值改动零跑;档位硬闸见 `sim-data/README.md`「MC 采集档位表」,一档只能由用户发起
|
||||
- 闸机轮次纪律:新码先孤立验证 → 出红只跑红件最小单元 → 长批先最小参数试车 → 开工扫坑册(process-conventions §6)
|
||||
- **验收闭合**——每个需求点与每个行为改动都要有从真实入口进入的验收断言,可 grep 定位、在当轮真实跑批中执行并通过,判据绑被测改动特有的可观测提交面;**禁变异类测试与撤销类取证**(2026-10-24/2026-10-28 用户裁——人为制造红属伪造证据),证明做不出=停卡呈报;禁删断言/放宽阈值换绿;合回前独立验收审计未过则禁止合并(细则=test-conventions §8+iter 技能第 5 步)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 美术圣经(Art Bible)— Dungeon Sweeper
|
||||
|
||||
> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.30,历次修订明细见 git 提交史
|
||||
> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.31,历次修订明细见 git 提交史
|
||||
>
|
||||
> **Art Director Sign-Off(AD-ART-BIBLE)**:历次批均未签核——此为 Lean/Solo 模式(非 PHASE-GATE)下的现行治理状态;full 门禁签核时机待用户表态。
|
||||
> 风格基调:地牢题材但**非暗黑风**——轻快明亮的幻想冒险风,全年龄;像素风;角色 2~3 头身。
|
||||
@@ -395,7 +395,7 @@
|
||||
- **交互四态(零新素材,全部程序化)**:悬停=**轮廓亮边 + 浮动**(浮动须整像素吸附);按下=位移 1px;**禁用/未解锁=降明度 + 去饱和**(§2 已缩窄至情绪域,状态表达不受限),**仅锻炉(锻造工坊)与宝箱(秘宝匣)两件会有此态**;已解锁=常态。
|
||||
- **钥石余额**:常驻画面顶部的一行小 HUD 栏(高 48px,**浮层覆盖,不挤压画面**)。
|
||||
- **事实源边界**:解锁条件/价格/存档写入归元进度 GDD 与 `MetaProgression`;素材命名与路径归 `asset-path-map`;清单登记归 `art-assets-checklist` §3.5。
|
||||
- **待裁(本节不填数,由作者定)**:① **营地环境子板色值**——色彩脚本只给定色方向,落地色值属 §4.1 单点,须过 §4.2 语义检查与 RGB 三通道差 ≥8/255 两道门;② **门扇与门洞的高宽比例**——中景带自底部往上约 2/3 处起之后高度有限,门扇高度远超围墙带,两者关系待作者重定;③ **帐篷的落地尺寸**——帐篷体量大于被替换的旧承载件,尺寸待作者重定;坐标草案的现行出口见 `camp.md` 的指路句。
|
||||
- **已裁(2026-11-01 用户裁,随迭代 47 卡④ 开工门销号)**:① **营地环境子板色值**——不前置拍板:T1 量化场景实际用色后按 §4.1 场景环境子板条款登记(每屏一套、≤32 色、过 §4.2 语义检查与 RGB 三通道差 ≥8/255 两道门);② **门扇与门洞的高宽比例**——门扇 240×220 追认现值,坐标按门洞 x520–760 对齐(`camp-scene-prompts.md` §2.3);③ **帐篷的落地尺寸**——300×170 追认现值(各素材第一张样本稿已过审为终稿,重定即触发重出,不裁)。
|
||||
|
||||
### 6.3 三关图类型色板候选(board_type 换挡方向)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 营地场景 · 分层生图提示词卡 v1.2
|
||||
# 营地场景 · 分层生图提示词卡 v1.3
|
||||
|
||||
> **性质**:营地场景图(1280×800,一张可点击场景图 + 十张分层:背景层 1 + 围墙层 1 + 八个物件层)的出图执行侧文件。**场景级资产,与角色级 [art-prompts](art-prompts.md) 并列,两文件互不覆盖**。
|
||||
> **依据**:色彩与氛围 = `reports/提案-营地色彩与氛围脚本-2026-10-23.md`(本卡不复述色值);坐标与层序 = `artwork/2026-10-23-营地场景/布局草案-1280x800.html`;物件命名与消费链 = [asset-path-map](asset-path-map.md);画法与交付形态 = [art-bible](art-bible.md) §6.2。冲突时色彩以色彩脚本为准,画法以 art-bible 为准。
|
||||
@@ -171,10 +171,10 @@ A campfire made of a ring of grey stones around crossed stacked logs with a warm
|
||||
|
||||
---
|
||||
|
||||
## 5. 待裁(不阻塞出图,阻塞坐标表与定稿)
|
||||
## 5. 已裁(2026-11-01 用户裁,随迭代 47 卡④ 开工门销号;不再阻塞坐标表与定稿)
|
||||
|
||||
1. **帐篷尺寸**:现 300×170 待重定,重定后按比例重出
|
||||
2. **门扇高宽**:现 240×220,围墙带只有 43px 高;围墙独立成层后门扇叠在门洞上,两者高宽关系待裁
|
||||
3. **布告板位置**:现 y330 悬空在围墙下方,建议 `(845, 268, 130, 140)` 挂回围墙带
|
||||
4. **描边颜色追认**:本卡按奶油色(去底技术前提);若改参照物的深灰褐,需换去底方案
|
||||
5. **营地环境子板色值**:art-bible §4.1 为数值单点,色值待作者拍板,本卡不复述
|
||||
1. **帐篷尺寸**:300×170 追认现值(各素材第一张样本稿已过审为终稿;重定即按比例重出、不裁)
|
||||
2. **门扇高宽**:240×220 追认现值,坐标按门洞 x520–760 对齐(§2.3);围墙带桩身高 43px、门扇叠在门洞上的叠放关系随此锁定
|
||||
3. **布告板位置**:采纳建议值 `(845, 268, 130, 140)` 挂回围墙带(y330 悬空案弃);终值随坐标表(卡④ `camp_scene_layout.tres`)落盘
|
||||
4. **描边颜色**:奶油色追认——去底脚本「奶油描边膨胀 3px 当屏障」的技术前提随现值锁定(§1 注记同源);改深灰褐须换去底方案全批重出,不裁
|
||||
5. **营地环境子板色值**:不前置拍板——T1 量化场景实际用色后按 art-bible §4.1 两道门登记(语义检查+RGB 三通道差 ≥8/255),本卡仍不复述色值
|
||||
@@ -249,18 +249,17 @@
|
||||
| **文档域** | gdd_lint、doc_link_audit、doc_term_audit、流水线闸 | `game/` 内的 `.md` 与 `game/` 外的一切路径:`design/`、根规则文件(`AGENTS.md`/`roadmap.md`/`待办清单.md`/`体验验证清单.md`)、`tools/` 工具脚本、`.zcode/` 配置、`sim-data/`、`reports/` |
|
||||
| **两域都含** | repo_structure_audit(熵增闸:顶层目录/根文件/图像白名单/退役路径/design 子目录五查——文档批新增或挪动 `.md` 同样会绊它;纯文本扫描秒级) | — |
|
||||
|
||||
口径三条:**域划分的唯一出处是 `tools/run_all_gates.sh`**(`--code`/`--docs`/不带参数=全量),本表是它的现状登记、不是第二份清单;改闸件本身或一次改动跨两域 → 跑全量档(§6.2 共享件例外);合回闸按 merge diff 的文件性质推出应验的域(`game/` 内 `.md` 归文档域),再校验回执的 `domain` 字段与新增红。代码域跑前先过**素材齐盘预检**——`game/assets/art/**` 与一切 `*.import` 边车都不入 git(2026-09-29 裁定),worktree 冷检出必空盘,空盘态冒烟 V1 成片假红、渲染闸更是一段 golden 都不比。
|
||||
口径三条:**域划分的唯一出处是 `tools/run_all_gates.sh`**(`--code`/`--docs`/不带参数=全量),本表是它的现状登记、不是第二份清单;改闸件本身或一次改动跨两域 → 跑全量档(§6.2 共享件例外);开发期手动档按本批改动域选 `--code`/`--docs`,合回后 CI 恒跑全量档。代码域跑前先过**素材齐盘预检**——`game/assets/art/**` 与一切 `*.import` 边车都不入 git(2026-09-29 裁定),worktree 冷检出必空盘,空盘态冒烟 V1 成片假红、渲染闸更是一段 golden 都不比。
|
||||
|
||||
### 车道分层执法(2026-10-26 用户裁「按路径切车道,取代一切改动走 worktree」)
|
||||
### 车道分层执法(2026-10-31 用户裁恢复「一切改动走 worktree」)
|
||||
|
||||
改动车道与改动域同一判据(文件性质):代码面=worktree 批车道,文档面=主工作区直改车道。执法全机械——判据只有文件路径性质与 git 图,不含「改动大小」等语义判断;「预期大改」由用户开工点名自愿开 worktree,不进规则。
|
||||
一切改动走独立 worktree,主工作区只作合并落点。执法全机械——判据只有 git 图与路径(豁免面 `.zcode/`、`.qoder/` 不入 git,主区直改),不含「改动大小」等语义判断。
|
||||
|
||||
| 执法层 | 载体 | 职责 | 判据 |
|
||||
|---|---|---|---|
|
||||
| **git pre-commit 钩子** | `tools/hooks/pre-commit`(版本化本体,`core.hooksPath` 挂载;在位性由 run_all_gates.sh 跑批自检——失位即红) | 直改车道闸机检:staged 不含代码面的提交,提交点现场跑 `run_all_gates.sh --docs`(秒级文本扫描),绿落地红拦;staged 含代码面时主工作区=拦(代码面唯一合法路径=worktree 批合回)、任务 worktree=跳过(归批末闸+合回闸,不打扰迭代批中间提交) | staged 文件性质+主区/worktree 判定(git-dir 与 common-dir 同一=主区) |
|
||||
| **worktree-guard**(ZCode PreToolUse) | `.zcode/hooks/worktree-guard.sh`(本机件,不入 git) | agent 侧车道纪律:主工作区代码面的 Edit/Write 与 staged 含代码面的 commit 拦截;`git commit --no-verify` 拦截(该旗标仅属用户);合回闸回执六况校验 | 路径性质+分支名+回执 JSON+git 图 |
|
||||
| **worktree-guard**(ZCode PreToolUse) | `.zcode/hooks/worktree-guard.sh`(本机件,不入 git) | agent 侧车道纪律:主工作区 Edit/Write 与主区非 merge `git commit` 拦截(豁免 `.zcode/`、`.qoder/`;worktree 内目标快速放行;合并收尾提交凭 MERGE_HEAD 豁免);`git commit --no-verify` 拦截(该旗标仅属用户);iter 分支合回一卡一提交数卡校验;规则闸 | 路径性质+分支名+MERGE_HEAD+git 图 |
|
||||
|
||||
取舍依据:失败代价不对称——docs 红是死链/术语/格式,下次任何跑批对基线比对必暴露、秒级修复,故文档面轻执法(commit 点闸、无回执流程);代码面红是运行时行为坏,保留「worktree+批末域闸+回执硬绑定」全套。git 层钩子对一切提交者生效(含用户手工提交,紧急出口 `--no-verify`);ZCode 层只约束 agent。子代理覆盖面(2026-10-26 冷启动评估实测):子代理的工具调用不经过 ZCode 层守卫(代码面 Edit 未被拦、`--no-verify` 探针未被拦)——ZCode 层实际只覆盖主会话工具调用,git 层因此是唯一覆盖一切提交者的执法层;残余暴露面=蓄意 `--no-verify`(pre-commit 族钩子被该旗标整族跳过,git 层无法自防),属故意越权而非遗忘型失误,与防遗忘的设计目标正交。
|
||||
验证收拢为合回后本地 CI 全量(post-merge 触发),commit 点零执法;主工作区防污染由 ZCode 层守卫承担——只约束 agent,用户手工改动不受限。子代理覆盖面(2026-10-26 冷启动评估实测):子代理的工具调用不经过 ZCode 层守卫——ZCode 层实际只覆盖主会话工具调用;残余暴露面=子代理写主工作区与蓄意 `--no-verify`(该旗标整族跳过 git 层钩子),属故意越权而非遗忘型失误,与防遗忘的设计目标正交。
|
||||
|
||||
### 三条判定硬闸现行状态
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# 流程规范(Dungeon Sweeper)
|
||||
|
||||
> 约束范围:一切批次的流程纪律——车道(代码面 worktree 批/其余主区直改)、五段流水线、批次工作流、测试执行细则、像素件工作纪律。
|
||||
> 约束范围:一切批次的流程纪律——车道(一切改动走独立 worktree,主区只作合并落点)、五段流水线、批次工作流、测试执行细则、像素件工作纪律。
|
||||
> 分工:硬性红线的一句话索引在 AGENTS.md「硬性红线」;本文承载其操作细则;批次开工前通读本文。
|
||||
|
||||
**开工任何批次前必须通读本文。**
|
||||
|
||||
## 1. 分支纪律(车道铁律)
|
||||
## 1. 分支纪律(worktree 铁律)
|
||||
|
||||
一切面主工作区直改+直提(2026-10-30 用户裁本地 CI 化:验证挪到合回后,主区直提不再拦任何面;判据与 §3 第 8 条改动域划分同一):
|
||||
主工作区(master 检出)只作合并落点,一切开发/设计改动禁止直接发生在主工作区(2026-10-31 用户裁恢复;2026-10-30 的「主区直改+直提」车道与 commit 点 pre-commit 直改闸随之退役;验证模型不动——合回后本地 CI 仍是唯一强制验证点):
|
||||
|
||||
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
|
||||
1. **worktree 车道(一切面)**:每批先建独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`;建区后立刻跑 `tools/setup_worktree_userdir.sh`——给该区独立 Godot user 目录(测试夹具、试玩日志、截图产物不与其他区共用;主区不生成,真实档保持原位)。所有改动与提交都在 worktree 内自由进行——commit 点零钩子检查(`tools/hooks/pre-commit` 已删除)。豁免面仅 `.zcode/`(钩子与配置)与 `.qoder/`(本机 harness 配置)允许主工作区直改——两份实测均不入 git(走 worktree 反而带不进去,改动只对本机生效)
|
||||
2. **收口顺序(固定)**:**冻结 → 件核 → 并 master → 重塑 → 主区合回 → 删区**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先在 worktree 内 `git merge master` 解冲突(必须在合回之前,合回后才并=CI 验的是中间态);**重塑**=迭代分支(iter<N> 形)合回前执行 `python tools/reshape_cards.py`,把分支线性史按卡末态快照压成一卡一笔(可选追加一笔纯文档收尾),master 台账由此每卡一行——工具自带「重塑前后树逐字节一致」断言与 backup 分支,不改任何文件内容;非迭代批免重塑;然后主工作区 `git merge --no-ff <任务分支>`——这是主区唯一合法写动作(合回守卫对迭代分支数卡校验,未归卡提交拦下)。**合回落地即 `git worktree remove`+删分支(合回即删,2026-09-19 原裁);post-merge 钩子已在合回落地时自动触发 CI,红态不回滚合并——新开修复 worktree(`fix-<任务名>` 分支,非 iter<N> 形免重塑免数卡)修复提交再合,落地复验追绿后删修复区**。游戏类测试(冒烟/单测/MC/探针)批内默认零执行(两入口铁规,见 §4),用户点名或当场问才跑
|
||||
3. **合回后验证(本地 CI,2026-10-30 用户裁)**:git merge 合回 master 落地后 post-merge 钩子(`tools/hooks/post-merge`)后台触发 `tools/ci_runner.sh merge` 对 master 尖跑一次全量域闸——唯一强制全量验证点;CI 红态下修复合回落地即由 post-merge 钩子复验追绿(红态修复走新开修复 worktree,`fix-<任务名>` 分支,非 iter<N> 形免重塑免数卡;post-commit 钩子保留为红态主区直提兜底触发,绿态与无状态零触发)。重跑判据:同一份字节禁止第二次人手跑;有意义的重跑只剩「验证修复」一类。CI 红不回滚合并——红项记 `game/tests/.gates/ci-status.json`,开工先读 CI 状态,红未修不开新批。跑批起手 `--editor --quit` 刷类名缓存(在册坑:.godot 缓存不入 git,合并后失配假红假绿)。并跑语义=跑完补跑(coalesce):跑批期间新触发不中断当前轮,轮尾对最新尖端补跑,绝不逐笔各跑一轮
|
||||
4. **执法分层(全机械,零自觉依赖)**:①`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:主工作区 Edit/Write 与主区非 merge `git commit` 拦截(豁免 `.zcode/`、`.qoder/`;worktree 内目标快速放行;合并收尾提交凭 MERGE_HEAD 豁免)、`git commit --no-verify` 拦截(该旗标仅属用户)、iter<N> 分支合回的一卡一提交数卡校验、规则闸(写 design//game/ 前核对会话规约读取记录)。②post-merge/post-commit 钩子——本地 CI 触发(仅主工作区;任务 worktree 内的合并不触发)。③git 层不设车道钩子(pre-commit 直改闸已退役)——用户手工改动不受限;ZCode 层只约束 agent(子代理不经守卫层)
|
||||
|
||||
## 2. 五段流水线
|
||||
|
||||
**需求改动**(游戏代码,含随逻辑写/改的测试)按五段流水线走:**登记 → 文档设计/美术设计(两条设计线)→ 写需求卡片 → 迭代编排 → 实现**;工程/文档设计/美术素材三类改动不走写卡与编排段,走「(如需设计讨论)→ 用户发令直改 → 主区直改+commit 点闸」(车道细则见 §1)。两层口径(2026-10-25 用户立):需求卡片=需求单位的实施文档,任何工作量皆可写、皆可编入迭代;工作量限制只属迭代层——迭代估时总量 <5 个工作日=「冻结待凑批」,不可开工。段间不自动流转,每段转进由用户发令;每段一个技能入口(五者均装于项目 `.zcode/skills/`:doc-design/asset-ingest/req-card/iter-plan/iter),登记不立技能(条目格式由流水线闸强制)。设计工作在会话内进行、用户主导、禁无监管自跑。
|
||||
**需求改动**(游戏代码,含随逻辑写/改的测试)按五段流水线走:**登记 → 文档设计/美术设计(两条设计线)→ 写需求卡片 → 迭代编排 → 实现**;工程/文档设计/美术素材三类改动不走写卡与编排段,走「(如需设计讨论)→ 用户发令 → 独立 worktree 直改 → 合回 CI」(车道细则见 §1)。两层口径(2026-10-25 用户立):需求卡片=需求单位的实施文档,任何工作量皆可写、皆可编入迭代;工作量限制只属迭代层——迭代估时总量 <5 个工作日=「冻结待凑批」,不可开工。段间不自动流转,每段转进由用户发令;每段一个技能入口(五者均装于项目 `.zcode/skills/`:doc-design/asset-ingest/req-card/iter-plan/iter),登记不立技能(条目格式由流水线闸强制)。设计工作在会话内进行、用户主导、禁无监管自跑。
|
||||
|
||||
| 段 | 入口技能 | 产出 | 出入口铁律 |
|
||||
|---|---|---|---|
|
||||
@@ -40,7 +40,7 @@
|
||||
3. **用户发开工令才动手**——需求卡片经用户批准(U0)后方开工
|
||||
|
||||
**环境与基线**
|
||||
4. **代码面批独立 worktree+拷导入缓存**——触及代码面的批次按 §1 建 worktree;先拷主区 `.godot/` 目录与字体 `.import` 边车再跑测试,否则全量重导入资源近似卡死。检出工作区零测试,直接开工(2026-10-22 频率规则);闸壳每次跑批先自检/自愈本区 user 目录隔离(§1 第 1 条),建不起来即拒跑;直改车道(非代码面)无此项
|
||||
4. **一切批独立 worktree+拷导入缓存**——按 §1 建 worktree;涉代码域跑闸或测试的批先拷主区 `.godot/` 目录与字体 `.import` 边车(纯文档批免拷),否则全量重导入资源近似卡死。检出工作区零测试,直接开工(2026-10-22 频率规则);闸壳每次跑批先自检/自愈本区 user 目录隔离(§1 第 1 条),建不起来即拒跑
|
||||
5. **涉数值改动先采改前读数**——与改后同随机种子配对比较并写明逐项比对口径;无数值改动禁跑模拟
|
||||
|
||||
**实施**
|
||||
@@ -48,8 +48,8 @@
|
||||
7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补
|
||||
|
||||
**完成**
|
||||
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 条),合并后无手工动作
|
||||
8. **合回后 CI 验证(唯一强制验证点)**——主工作区 `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-merge 钩子复验,每轮修复至多触发一轮);② 用户自发发起的全量回归。「跑都跑了再跑一遍保险」「顺手改点再补一跑」都不构成重跑理由。类名缓存刷新与开机确认由 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. **实机目检归用户,只登记不阻塞**——真人检测项登记进实现报告目检总清单与体验验证清单即完成批内义务;禁把等目检当停工理由或完成前置
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
## 4. 验证时机(合回后本地 CI 制,2026-10-30 换代)
|
||||
|
||||
开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,process-conventions §6);主区小改孤跑验证后直提。全量验证归合回后的本地 CI:git merge 合回 master 触发 post-merge 钩子跑 `tools/run_all_gates.sh` 不带参数档(两域全量,02 全套件——跨卡触面与关联档选测的盲区由全量兜底),结果落 `game/tests/.gates/ci-status.json`;CI 红态下修复提交经 post-commit 钩子自动追绿。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。
|
||||
开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,process-conventions §6)。全量验证归合回后的本地 CI:git merge 合回 master 触发 post-merge 钩子跑 `tools/run_all_gates.sh` 不带参数档(两域全量,02 全套件——跨卡触面与关联档选测的盲区由全量兜底),结果落 `game/tests/.gates/ci-status.json`;CI 红态下修复合回落地即由 post-merge 钩子复验追绿(修复走新开 `fix-<任务名>` 修复 worktree,process-conventions §1)。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。
|
||||
行为探针使用随机化玩家行为(封印/停手/刷新/无效点击)——自动玩家不做的动作就是测试盲区。
|
||||
|
||||
## 5. 测试函数怎么写——验收清单条目的落码规范
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
| `术语表.md` | 全库专用术语的唯一对照表——危险格/安全格/同调位/RAP/GP/真视等定义+退役词对照(2026-10-11 建立,v1.23) |
|
||||
| `待办清单.md`(仓库根,不属本树) | 全项目待办事项的唯一登记处;各系统文档的「待办」节只留指路存根 |
|
||||
| `体验验证清单.md`(仓库根,不属本树) | 需要真人试玩观测的验证项登记处(与待办清单分工:待办清单是"要拍板的事",本篇是"要玩过才知道的事");每条注明结论回流路径 |
|
||||
| `08-Alpha版本标准.md` | Alpha 版本包含哪些东西的清单——里程碑 M3 的完成标准;只列条目不写内容,定义一律指路各权威文档(**已定稿 v1.20**,2026-10-13) |
|
||||
| `08-Alpha版本标准.md` | Alpha 版本包含哪些东西的清单——里程碑 M3 的完成标准;只列条目不写内容,定义一律指路各权威文档(**已定稿 v1.21**,2026-10-13) |
|
||||
|
||||
## systems/ 五个循环
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **一句话定位**:本文定义这个项目的 **Alpha 版本包含哪些东西**——它是里程碑 M3 的完成标准,也是判断"还差什么"的对照表。
|
||||
> **本文只列条目,不写内容**:每样东西是什么、规则怎么走,一律归各自的权威文档,本文只给指路链接。想知道某样东西的定义,请点链接,不要在本文里找解释。
|
||||
> **状态**:**已定稿(v1.20)**——2026-10-13 用户令定稿,历次升版明细见 git 提交史。Alpha 的内容范围自此冻结:增删条目一律须经用户裁决。
|
||||
> **状态**:**已定稿(v1.21)**——2026-10-13 用户令定稿,历次升版明细见 git 提交史。Alpha 的内容范围自此冻结:增删条目一律须经用户裁决。
|
||||
> **成文**:2026-10-13。
|
||||
|
||||
## 本文怎么用
|
||||
@@ -104,7 +104,7 @@ Alpha 内以玩法循环本身的内容与表现为主(棋盘、关卡、战
|
||||
| 开始覆盖层 | —— | 在役 |
|
||||
| 选档界面(3 存档位) | —— | 在役(迭代 33 卡③——空槽新档/有档进档/删档二次确认+摘要三项;2026-10-22 用户裁增行〔反哺 #8〕) |
|
||||
| 营地(天赋树 / 锻造工坊 / 秘宝匣 / 图鉴) | [camp](ux/camp.md) | 在役(场景图与七物件点选随营地交互重做列入 Alpha,见下行) |
|
||||
| 营地交互重做(五卡:存档制数据层/主菜单三入口/选档界面/营地场景图/Esc 浮窗与营地顶栏) | [camp](ux/camp.md)+[需求澄清](../../reports/需求澄清-营地交互重做-2026-10-18.md) | 部分在役(卡①②③ 已随迭代 33 实装合回,2026-10-22;卡④营地场景图 2026-10-21 用户令退回美术设计段,美术生成方案提案 2026-10-22 成文,余三个用户侧前置未销号:像素规格执行(Q6 甲过渡口径已裁)、点选反馈设计稿、参照物;卡⑤ Esc 浮窗随卡④ 代码波同行、未编排。2026-10-21 用户裁:五卡整体列入 Alpha。出口=五卡各自验收标准通过+营地屏用户目检通过) |
|
||||
| 营地交互重做(五卡:存档制数据层/主菜单三入口/选档界面/营地场景图/Esc 浮窗与营地顶栏) | [camp](ux/camp.md)+[需求澄清](../../reports/需求澄清-营地交互重做-2026-10-18.md) | 部分在役(卡①②③ 已随迭代 33 实装合回,2026-10-22;卡④营地场景图美术设计已闭合(样张过审 2026-10-31)、v3.0 现行有效卡重立,与卡⑤ Esc 浮窗同编迭代 47(待开工)。2026-10-21 用户裁:五卡整体列入 Alpha。出口=五卡各自验收标准通过+营地屏用户目检通过) |
|
||||
| 生涯页 | [生涯](systems/跨局循环/生涯.md) | 在役(期一) |
|
||||
| 关卡地图屏 | [level-map](ux/level-map.md) | 在役 |
|
||||
| 战斗屏 | [battle](ux/battle.md) | 在役 |
|
||||
@@ -135,7 +135,7 @@ Alpha 内以玩法循环本身的内容与表现为主(棋盘、关卡、战
|
||||
| 机器可读的呈现断言(对比度 / 结构) | ——(清单唯一出处=process-conventions 运行与验证节) | 在役 |
|
||||
| 分辨率两档(1280×800 / 1920×1080) | [HUD框架](systems/表现与支撑/HUD框架.md) | 在役 |
|
||||
| 绘制调用数达标 | —— | 在役 |
|
||||
| 真人试玩轮次 | [体验验证清单](../../体验验证清单.md) | 在役(义务维持;当前轮次未清:㉚/㉛ 未核销,迭代 34 三条目检项待登记) |
|
||||
| 真人试玩轮次 | [体验验证清单](../../体验验证清单.md) | 在役(义务维持;当前轮次未清=㉚–㊸ 各条待用户核销——迭代 32 起各批实机目检项登记不阻塞,迭代 42 无玩家可见改动未登记) |
|
||||
|
||||
---
|
||||
|
||||
@@ -157,7 +157,7 @@ Alpha 内以玩法循环本身的内容与表现为主(棋盘、关卡、战
|
||||
| 站桩帧动画 | [待办清单](../../待办清单.md) | 待做(列入 Alpha 定版前置清单:定版前完成最小版,路线预选拆件驱动;未完成则顺延 Beta;不占功能迭代) |
|
||||
| 全局纹理过滤设为邻近采样 | [待办清单](../../待办清单.md) | 在役(迭代 26 · 2026-10-14 入库:default_texture_filter=0+平滑渐变件三处显式 LINEAR 白名单,翻转面全枚举闭合) |
|
||||
| UI 字体(全局像素字,Fusion Pixel 单套单字重) | [美术圣经](../art/art-bible.md) §7.4 | 在役(2026-10-24 用户裁现状即终稿——拉丁沿用自带字形不另接专件、不上第二字重;信息层级手段=字号档差+墨色深浅) |
|
||||
| 战斗一屏样张(界面美术的终稿闸) | [美术圣经](../art/art-bible.md) §1.4 | 待做(Alpha 硬前置:站桩帧动画的定版前置与主角+三 Boss 重产都依赖它;出口=用户目检通过的战斗屏样张,并产出尺寸档/色板/明度档清单) |
|
||||
| 战斗一屏样张(界面美术的终稿闸) | [美术圣经](../art/art-bible.md) §1.4 | 部分在役(规格半已闭合=目标规格清单已落 art-bible 五张规格表〔2026-10-30 合回〕,五项实装定值与样张机检随迭代 43 入库;未结半=出口未达——方向稿用户目检(体验验证清单㊶)先行,样张整体过闸随其后。站桩帧动画的定版前置与主角+三 Boss 重产依赖照旧) |
|
||||
| UI 氛围层与九宫格切片观感修 | [待办清单](../../待办清单.md) | 部分在役(光柱硬边已修=连续 alpha 渐变;未结半=九宫格切片错位——Alpha 内做最小修复:核对素材尺寸,必要时重出九宫格素材,全套素材重出排 Beta;出口=结算屏与图例屏错位消失,渲染闸+用户目检通过) |
|
||||
| UI 元素细节修(滑轨填充 / 图标垫块 / 留白 / 开发件外泄) | [待办清单](../../待办清单.md) | 在役(迭代 26 · 2026-10-14 入库:①滑轨填充随迭代 25 注销;余②图标垫块明度差降档/③商店+营地布局修/④诊断钮迁调试浮窗 全落地) |
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
>
|
||||
> **兑付频率(先看这条,它是三条渠道的共同前提)**:奇物**不是打一场战斗给一份**。关内的普通战斗节点打完了什么也不给;一关之内奇物**只兑付一次**,触发点是**关底 Boss 被击败**——而按 [冒险结构](冒险结构.md) 的定义,打掉关底 Boss 恰好就等于"通过这一关",这两件事是同一个时刻,不是一条链上的两步。
|
||||
>
|
||||
> 状态:**已实现**(2026-10-04 双层 BD 实施批落地);现行为 v1.2。本文件是奇物的唯一定义文档。
|
||||
> 状态:**已实现**(2026-10-04 双层 BD 实施批落地);现行为 v1.3。本文件是奇物的唯一定义文档。
|
||||
>
|
||||
> 数值纪律:本文件只写概念、取值域与指路;每件给多少、效果是什么一律不进正文,事实源是 `data/tables/feats.tres` 与 `data/tables/adventure.tres`。
|
||||
|
||||
@@ -99,25 +99,25 @@
|
||||
|
||||
### 换图系(7 件,设计定稿,待实现)
|
||||
|
||||
换图系把「弃图刷新」从坏图逃生改成主动构筑行为。两件基座分别解决 RAP 的来源与价格;后续专长让成功刷新产生输出、生存或资源收益。七件不要求一次拿齐,不设强制先拿基座、集件标签或集件奖励。
|
||||
换图系把「弃图刷新」从坏图逃生改成主动构筑行为。两件基座分别解决 RAP 的来源与价格;后续专长让成功刷新产生输出、生存或 GP 资源收益。七件不要求一次拿齐,不设强制先拿基座、集件标签或集件奖励。
|
||||
|
||||
| 层 | 代号 | 规则 | 行为职责 |
|
||||
|---|---|---|---|
|
||||
| 基座 | 掘进储能 | 每次付费挖格后额外获得 RAP;安全格与危险格同口径 | 持续挖图也能准备刷新,不再必须少挖停手 |
|
||||
| 基座 | 折价换图 | 弃图刷新的 RAP 成本减半 | 同一份储备支持更多刷新 |
|
||||
| 输出 | 弃图轰击 | 成功刷新后,在本轮战斗段对全体存活敌人造成一次伤害 | 刷新成为直接进攻行为 |
|
||||
| 输出 | 换图蓄势 | 成功刷新攒层,层数有上限;下一次攻击附加全部层数对应的伤害后清零,战斗结束清空 | 连续换图蓄成一次单点爆发 |
|
||||
| 生存 | 换图庇护 | 成功刷新后,在本轮授予段获得护盾 | 用刷新预防后续伤害 |
|
||||
| 生存 | 弃图休整 | 成功刷新后,在本轮授予段回复生命,封顶生命上限 | 放弃当前图换取恢复 |
|
||||
| 资源 | 弃图回收 | 成功刷新后,按旧图探索进度在本轮授予段回收清图奖励 | 先探索再主动弃图变现 |
|
||||
| 基座 | 矿脉储能 | 每次付费挖格被受理后,额外获得 RAP;安全格与危险格同口径 | 持续挖图也能准备刷新,不再必须少挖停手 |
|
||||
| 基座 | 省力周转 | 弃图刷新的 RAP 成本减半 | 同一份储备支持更多刷新 |
|
||||
| 输出 | 崩壁冲击 | 成功刷新后,在本轮战斗段末对全体存活敌人造成一次固定伤害 | 刷新成为直接进攻行为 |
|
||||
| 输出 | 蓄力周转 | 成功刷新攒层,最多 3 层;本轮下一次主角攻击消费全部层数后清零,战斗结束不跨战保留 | 连续换图蓄成一次单点爆发 |
|
||||
| 生存 | 石层缓冲 | 本轮至少成功刷新一次后,主角每次受到伤害固定减少 2 点;不叠加 | 刷新形成当轮全局防御姿态 |
|
||||
| 生存 | 坑道小憩 | 每次成功刷新累计 1 点恢复,最多 3 点;本轮授予段合计回复生命,封顶生命上限 | 放弃当前图换取恢复 |
|
||||
| 资源 | 矿道红利 | 每次成功刷新固定获得 1 GP | 刷新动作自身带少量GP收益 |
|
||||
|
||||
共同规则:
|
||||
|
||||
- **双基座叠加**:掘进储能与折价换图可以同时持有,产 RAP 与成本减半分别生效;每轮刷新次数上限继续限频,不设互斥规则。
|
||||
- **成功才触发**:后续五件只认成功换图;刷新被拒绝时不产生效果。同一轮多次成功刷新按发生顺序逐次登记。
|
||||
- **轮末兑付**:刷新不新增结算段。群伤、护盾、回血与回收按[战斗结算](../战斗循环/战斗结算.md)的既有段序兑付,刷新瞬间不直接改这些战斗结果。
|
||||
- **回收看旧图**:弃图回收读取被放弃旧图的探索进度,不把旧图或新图判为清图;具体口径归[地图战斗资源](../战斗循环/地图战斗资源.md)。
|
||||
- **基线不变**:没有这些专长时,RAP 来源、刷新成本、清图奖励与刷新结果保持原规则。数值与稀有度归 `feats.tres`,落表前本节只作设计声明。
|
||||
- **双基座叠加**:矿脉储能与省力周转可以同时持有,产 RAP 与成本减半分别生效;每轮刷新次数上限继续限频,不设互斥规则。
|
||||
- **成功才触发**:后续五件只认成功换图;刷新被拒绝时不产生事实,也不给输出、生存或资源收益。同一轮多次成功刷新按发生顺序逐次登记。
|
||||
- **轮末兑付**:刷新不新增结算段。崩壁冲击走本轮战斗段末结账,石层缓冲在每次主角受伤时消费,坑道小憩与矿道红利走本轮授予段兑付。
|
||||
- **全局受伤面**:石层缓冲覆盖敌方攻击、危险格、错误封印资源格与地形造成的主角伤害;临时生命值不在本流派设计范围内。
|
||||
- **基线不变**:没有这些专长时,RAP 来源、刷新成本、清图奖励与伤害结果保持原规则。数值与稀有度归 `feats.tres`,落表前本节只作设计声明。
|
||||
|
||||
### 迁入登记(秘宝域迁出件;实装批落地后并入件表)
|
||||
|
||||
@@ -479,7 +479,7 @@
|
||||
11. 凭证拿到即开,不进持有集,玩家无法囤积——选中或买下的同一时刻就开专长三选一。
|
||||
12. 凭证带有计步保底:连续 5 次三选一展示未出现则第 6 次必出;计数跨奖励节点与商店共享,刷新后的重新展示计入次数,限购用满后保底不再触发。
|
||||
13. 被称为流派基座的专长必须能说明拿到前后的操作差异与新行为的持续资源来源;只有同一操作后的泛用数值增长,不得登记为基座。
|
||||
14. 换图系两件基座同时持有时都生效;后续五件只在成功刷新后触发,无专长基线不变;弃图回收按旧图探索进度结算,同一旧图至多回收一次。
|
||||
14. 换图系两件基座同时持有时都生效;后续五件只在成功刷新后触发,无专长基线不变;石层缓冲覆盖所有主角受伤路径且同轮不叠加,矿道红利按成功刷新次数固定获得 GP。
|
||||
|
||||
> 实装批落地前,现役代码按旧 27 件专长目录与旧资源成员在同框架下运行;新成员口径(27 件基础件表 / 凭证件表)自实装批起验。
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 地图战斗资源
|
||||
|
||||
> 一句话:格面资源的产出、极性字典与清图奖励封装
|
||||
> 状态:已定稿(自旧体系迁移);现行为 v1.2
|
||||
> 状态:已定稿(自旧体系迁移);现行为 v1.3
|
||||
|
||||
## 设计目的
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
| defense 护甲 | ac_up 盾牌加护(AC 掷点阈值轴——2026-09-16 换装,不入池)——幅度/上限/权重见 `reward.tres` | off_guard 失衡(AC 掷点阈值轴负向——2026-09-18 换装,不入池、退出主角防御池)——幅度/上限见 `status_effects.tres` off_guard 行 |
|
||||
| heal 回血 | heal_minor(满血零兑付照旧)——威力/权重见 `reward.tres` | heal_backlash 直伤:战斗结算直接扣血,不经增益状态——威力见 `reward.tres` |
|
||||
| blast 群伤 | spark_aoe 全体存活敌各吃一笔——威力/权重见 `reward.tres` | enemy_alert 警觉:敌方全体护甲等级 +1/层(`status_effects.tres` alert 行)——见 `reward.tres` |
|
||||
| weaken 削弱 | weaken(敌方输出池;同名重复授予只刷新剩余次数——2026-09-18 续时定案)——幅度见 `status_effects.tres` weaken 行 | sapped(主角输出池)——幅度见 `status_effects.tres` sapped 行 |
|
||||
| weaken 削弱 | weaken(敌方输出池;同名重复授予只加时长)——幅度见 `status_effects.tres` weaken 行 | sapped(主角输出池)——幅度见 `status_effects.tres` sapped 行 |
|
||||
| action 行动点 | ap_up:下轮行动点 +1——见 `reward.tres` | ap_drain 迟滞:下轮行动点 −1——见 `reward.tres` |
|
||||
| gp 财货 | gp_gain GP 袋:战斗内 GP 缓冲 +power,战斗完成随 GP 掉落统一入钱包账本(兑付编排归[战斗结算](战斗结算.md)、账本归[冒险结构](../冒险循环/冒险结构.md))——威力/权重见 `reward.tres` | gp_loss 破财:战斗内 GP 缓冲 −power,weight=0 纯字典不参与摇号——见 `reward.tres` |
|
||||
| pierce 穿刺 | pierce_strike 穿刺弹:单目标克制直伤(attack_type=pierce 查克制矩阵,必中全额不掷点;目标 = 固定顺位最前存活敌)——威力/权重见 `reward.tres` | pierce_snare 穿刺陷阱:主角直伤,不经增益状态——威力见 `reward.tres` |
|
||||
@@ -158,29 +158,8 @@
|
||||
| 可配置可替换 | 内容 = 池行值;允许即时类(当场兑付,不入库) |
|
||||
| 图类型差异化是预留插槽 | 现行只有默认槽;结构位已留,启用走棋盘生成词表落定 |
|
||||
|
||||
持有弃图回收专长时,同一清图奖励对象也可按旧图探索进度部分回收;这不改变正常清图与清图奖励的定义,例外口径见下节。
|
||||
|
||||
举证:本节为封装与流程规则,无计算。
|
||||
|
||||
### 弃图回收:按旧图探索进度兑付(设计定稿,待实现)
|
||||
|
||||
弃图回收让玩家不必挖完最后几个安全格,也能把已经推进的探索换成一部分清图奖励。它只读取被放弃的旧图,不把旧图判为清图,也不把新图判为已清图。
|
||||
|
||||
旧图退役前记录三项读数:装板初始已开安全格数、装板初始未开安全格数、退役时已开安全格数。探索进度定义为:
|
||||
|
||||
```
|
||||
探索进度 =(退役时已开安全格数 − 装板初始已开安全格数)÷ 装板初始未开安全格数
|
||||
```
|
||||
|
||||
进度限制在 0~1。玩家付费挖开与连锁展开的安全格都真实推进探索,计入分子;装板时白送的初始格、封锁格与危险格不计入。没有新探索就刷新,进度为 0,不能获得回收奖励。
|
||||
|
||||
回收兑付的是旧图在装板期已经摇定的清图奖励对象,按探索进度取对应份额;具体比例、档位、取整与数值归调参。它只发基础奖励的进度份额:
|
||||
|
||||
- 不触发完美封印加倍,不产生清图事件,不执行正常清图的整板兑付。
|
||||
- 同一旧图只在成功退役时回收一次;新图不继承旧图探索进度。
|
||||
- 刷新被拒绝时旧图不退役,也不回收;成功后的兑付时机归[战斗结算](战斗结算.md)本轮授予段。
|
||||
- 正常清图仍发完整清图奖励对象;弃图回收不得让同等探索投入的收益无条件高于正常清图。
|
||||
|
||||
## 依赖与输出
|
||||
|
||||
本系统被两侧夹住:事实与数值从上游来,调用与呈现往下游去。下表就是删除任何一方时的影响面清单。
|
||||
@@ -260,9 +239,6 @@
|
||||
- 类型封闭与字典完整:表里出现九型之外的类型、或任一型缺正向/负向行,加载拒绝。
|
||||
- 未生效可见:满层增益、满血正向治疗、无存活敌的群伤,各自在结算明细记"奖励未生效"行,零折算、零补偿。
|
||||
- 加载护栏:池空、清图池无正当量、增益引用悬空——任一命中即加载时返回错误并中止;默认内容加载通过。
|
||||
- 弃图回收进度:付费挖开与连锁展开的安全格计入探索,初始格、封锁格与危险格不计;无新探索时进度为 0,不能获得回收奖励。
|
||||
- 弃图回收恰一次:成功退役的旧图按当次探索进度回收其预摇清图奖励对象,新图不继承进度,同一旧图不能重复回收;被拒绝的刷新零兑付。
|
||||
- 回收不是清图:弃图回收不触发完美封印加倍、清图事件或正常清图整板兑付;正常清图仍发完整清图奖励对象。
|
||||
- 表实一致:数据表字段与奖励表字段定义逐字段一致。
|
||||
|
||||
## 待办
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 增益状态
|
||||
|
||||
> 一句话:增益/减益的状态池、叠层与衰减
|
||||
> 状态:已定稿(自旧体系迁移);现行为 v1.2
|
||||
> 状态:已定稿(自旧体系迁移);现行为 v1.3
|
||||
|
||||
## 设计目的
|
||||
|
||||
战斗里有一类效果不在当场一次性用掉,而要管上几次结算:挖开安全格拿到的主角增益、封印标中带来的怪物削弱、踩雷喂给怪物的增强(警觉——blast 型负向)、标错封印招来的负向兑付,都是这类。本文把它们统称"状态":发给主角的有益效果叫增益(失衡与乏力是例外——它们是发给主角的减益),发给敌人的负面效果叫减益;敌方侧的增强同样是状态——几个方向共用同一个状态池。
|
||||
|
||||
增益状态就是这一个状态池:它收下全部状态,用同一套规矩管理——发入、合并(同一种效果重复拿到时按表行策略处理:叠层加一,或只刷新剩余次数——读 `status_effects.tres` 逐行 stack_policy)、衰减(每次结算后剩余次数减一,减到零效果消失)、清空(战斗结束全部清除)。没有它,几类效果得各写一套计时逻辑,结算明细也无法逐行指回"这效果是我哪步棋带来的"。状态池管机制、不管数值从哪来:某个效果该多大,由各收益系统的表去定,状态池只照章入列、按次衰减。
|
||||
增益状态就是这一个状态池:它收下全部状态,用同一套规矩管理——发入、合并(同一种效果重复拿到时按表行策略处理:叠层加一,或把剩余次数累加上本次授予的时长——读 `status_effects.tres` 逐行 stack_policy)、衰减(每次结算后剩余次数减一,减到零效果消失)、清空(战斗结束全部清除)。没有它,几类效果得各写一套计时逻辑,结算明细也无法逐行指回"这效果是我哪步棋带来的"。状态池管机制、不管数值从哪来:某个效果该多大,由各收益系统的表去定,状态池只照章入列、按次衰减。
|
||||
|
||||
## 系统范围
|
||||
|
||||
@@ -76,14 +76,14 @@
|
||||
|
||||
举证:本节为授予路径与登记规则,无计算。
|
||||
|
||||
### 同名状态再拿到:叠层、刷新、并存
|
||||
### 同名状态再拿到:叠层、累加、并存
|
||||
|
||||
同一种状态还没消失又拿到一份,怎么合并由状态表每行的合并策略字段说了算,共三种。合并在授予那一刻由状态池完成;多个状态在结算里按什么先后起作用,是战斗结算的编排——两件事互不干扰。
|
||||
|
||||
| 合并策略 | 重复授予时发生什么 | 说明 |
|
||||
|---|---|---|
|
||||
| 叠加 | 层数加一;剩余结算次数不动——只有首次授予定钟 | 现行内容行多数用叠加(逐行合并策略读 `status_effects.tres`) |
|
||||
| 刷新 | 剩余结算次数重置为该实例首次授予时的初始值,层数不变 | 刷新类内容行已就位——削弱行 weaken(2026-09-18 续时定案)、增益技行 empower / fortify、**主角增益行 attack_up / ac_up(2026-09-20 用户拍板「只续时不叠层」,全状态表增益/减益两侧合并语义就此统一为:主角可重复获取的增益与减益一律续时、不叠层)**;逐行读 `status_effects.tres` |
|
||||
| 累加 | 剩余结算次数加上本次授予的时长——技能施放行带覆盖时长时按覆盖值加;层数不变 | 现行内容行六行用累加(削弱行 weaken、增益技行 empower / fortify、主角增益行 attack_up / ac_up);重复拿一次就多攒一段时长,同一轮拿几份攒几段,停手后按剩余次数逐轮耗完;逐行读 `status_effects.tres` |
|
||||
| 并存 | 新建一份独立实例,两份各算各的 | 同上 |
|
||||
|
||||
并存分支没有满层上限,但状态池规模天然有界:实例数的上界就是受理过的授予数——多受理一份就多一份实例,多少份在授予那一刻就完全确定,不会无限膨胀。
|
||||
@@ -110,7 +110,7 @@
|
||||
|
||||
### 剩余次数怎么倒数:按结算次数衰减
|
||||
|
||||
每个状态实例自带一个倒数计数——剩余结算次数(本文说的"衰减"就是它逐次递减、归零消失)。它不承诺"活几轮",而是数"经历过几次轮末结算":每轮结算先让状态起作用,随后计数减一,减到零状态即被移除。持续 1 的状态就是"一次性":在授予它的那轮结算里生效一次,轮末消失。MVP 时期六行内容均为一次性(削弱后改续时——见 git 提交史;2026-09-20 起主角增益两行也走续时、duration 表值仍为 1——重复拿等于重新获得一次当轮增益);跨轮持续的状态,寿命由表行初始值表达,给几就活几次结算。
|
||||
每个状态实例自带一个倒数计数——剩余结算次数(本文说的"衰减"就是它逐次递减、归零消失)。它不承诺"活几轮",而是数"经历过几次轮末结算":每轮结算先让状态起作用,随后计数减一,减到零状态即被移除。持续 1 的状态就是"一次性":在授予它的那轮结算里生效一次,轮末消失。现行累加行里主角增益两行持续 1、削弱与敌方增益技行持续 2——累加语义下重复拿一次就按本次授予时长多攒一段,同一轮拿几份攒几段;跨轮持续的状态,寿命由表行初始值表达,给几就活几次结算。
|
||||
|
||||
```
|
||||
一轮结算:查询生效清单 → 逐行兑付(层数 × 行值)→ 轮末衰减(全部实例剩余次数 −1)→ 归零实例移除
|
||||
@@ -140,8 +140,8 @@
|
||||
|
||||
| 例子 | 状态池输出 | 消费侧读法 |
|
||||
|---|---|---|
|
||||
| 暴走叠两层 | +(2 × enrage 行幅度)(读表;越出单行取值域,原样输出) | 压在天花板 +100% 上:敌方输出乘区封顶 ×2(削弱已改续时——"叠两层压地板 −100% 归零"的读法随 2026-09-18 结构修复批退役) |
|
||||
| 主角盾牌加护再拿一份 | 层数恒 1、剩余结算次数重置(refresh——2026-09-20 主角增益续时批) | 不经任何池:掷点判定阈值 = 主角基准(读 hero 表)+ 层数(现行 +1——上限 2 层叠读取法退役,局内可达 AC 上限 11) |
|
||||
| 暴走叠两层 | +(2 × enrage 行幅度)(读表;越出单行取值域,原样输出) | 压在天花板 +100% 上:敌方输出乘区封顶 ×2(削弱非叠层行——"叠两层压地板 −100% 归零"的读法不成立) |
|
||||
| 主角盾牌加护再拿一份 | 层数恒 1、剩余结算次数累加本次授予的时长 | 不经任何池:掷点判定阈值 = 主角基准(读 hero 表)+ 层数(现行 +1——上限 2 层叠读取法退役,局内可达 AC 上限 11) |
|
||||
|
||||
状态池还有一条快照纪律:实例的幅度、初始次数、量纲在授予那一刻从表行拷贝。战斗中配置表即使被重载,已授予实例不受任何影响,只影响后续授予。
|
||||
|
||||
@@ -162,15 +162,15 @@
|
||||
|
||||
| 状态 | 效果一句话 | 发给谁 | 幅度(现行值) | 时限 / 合并 | 来源类别 |
|
||||
|---|---|---|---|---|---|
|
||||
| attack_up 增伤 | 主角攻击提升 | 主角 | 见 `data/tables/status_effects.tres` attack_up 行 | 时限/上限读表 / 刷新(2026-09-20 续时批 stack→refresh,层数恒 1) | 安全格 |
|
||||
| ac_up 盾牌加护 | 主角护甲等级提升(掷点折扣判定阈值——AC 轴,不入池) | 主角 | 见 `status_effects.tres` ac_up 行(2026-09-16 换装,原 defense_up 行已退役) | 时限/上限读表 / 刷新(2026-09-20 续时批 stack→refresh,层数恒 1) | 安全格 |
|
||||
| attack_up 增伤 | 主角攻击提升 | 主角 | 见 `data/tables/status_effects.tres` attack_up 行 | 时限/上限读表 / 累加(层数恒 1) | 安全格 |
|
||||
| ac_up 盾牌加护 | 主角护甲等级提升(掷点折扣判定阈值——AC 轴,不入池) | 主角 | 见 `status_effects.tres` ac_up 行(2026-09-16 换装,原 defense_up 行已退役) | 时限/上限读表 / 累加(层数恒 1) | 安全格 |
|
||||
| alert 警觉 | 敌方护甲等级 +1/层 | 敌方 | 见 `status_effects.tres` alert 行 | 1 结算 / 叠加读表 | 踩雷 / 标空封印(blast 型负向) |
|
||||
| weaken 削弱 | 敌方攻击被削弱(同名重复标中只刷新剩余次数、层数恒 1) | 敌方 | 见 `status_effects.tres` weaken 行(取值域 [60,85],取负生效) | 时限读表 / 刷新(2026-09-18 续时定案) | 封印标中 |
|
||||
| weaken 削弱 | 敌方攻击被削弱(同名重复标中只加时长、层数恒 1) | 敌方 | 见 `status_effects.tres` weaken 行(取值域 [60,85],取负生效) | 时限读表 / 累加 | 封印标中 |
|
||||
| enrage 暴走 | 敌方攻击提升 | 敌方 | 见 `status_effects.tres` enrage 行 | 时限/上限读表 / 叠加 | 标空封印反噬 / 踩雷(attack 型负向) |
|
||||
| off_guard 失衡 | 主角护甲等级降低(AC 轴——掷点判定阈值下调、更易被命中;2026-09-18 换装,原百分比承伤池已退) | 主角 | 见 `status_effects.tres` off_guard 行 | 时限/上限读表 / 叠加 | 标空封印反噬 / 踩雷(defense 型负向)/ 敌方妨害技(2026-09-19 新效应词汇批——蛛网束缚载体,duration 由技能行覆盖) |
|
||||
| sapped 乏力 | 主角输出降低(weaken 型负向,主角输出池) | 主角 | 见 `status_effects.tres` sapped 行 | 时限/上限读表 / 叠加 | 危险格负向 / 标空封印(2026-09-15 格面资源批)/ 敌方妨害技(大地脉动——L3 Boss 石君王独占,2026-09-20 第三关内容批) |
|
||||
| empower 蓄力 | 敌方输出倍率增益(敌方输出池——蓄力技载体;幅度读表) | 敌方 | 见 `status_effects.tres` empower 行 | duration 读表 = 1 个有效增益拍 / 刷新 | 敌方技能施放(2026-09-16 敌人技能池批) |
|
||||
| fortify 硬化 | 敌方防御提升(**敌方防御池**——硬化技载体;负值入池 = 主角攻击折半,2026-09-18 符号纠正——旧正值实际翻倍) | 敌方 | 见 `status_effects.tres` fortify 行 | duration 读表 = 1 个有效增益拍 / 刷新 | 敌方技能施放(2026-09-16 敌人技能池批) |
|
||||
| empower 蓄力 | 敌方输出倍率增益(敌方输出池——蓄力技载体;幅度读表) | 敌方 | 见 `status_effects.tres` empower 行 | duration 读表 = 1 个有效增益拍 / 累加 | 敌方技能施放(2026-09-16 敌人技能池批) |
|
||||
| fortify 硬化 | 敌方防御提升(**敌方防御池**——硬化技载体;负值入池 = 主角攻击折半,2026-09-18 符号纠正——旧正值实际翻倍) | 敌方 | 见 `status_effects.tres` fortify 行 | duration 读表 = 1 个有效增益拍 / 累加 | 敌方技能施放(2026-09-16 敌人技能池批) |
|
||||
|
||||
empower 与 fortify 是敌方技能池增益技在状态池侧的数据落点(技能行 buff_ref 引用,施放时长由技能行覆盖——技能冷却与增益持续是两个计时器):两行 duration 表值(读表)的口径是"先兑付后衰减含施放当轮"——施放发生在第 N 结算战斗段,存活到第 N+1 结算战斗段才兑付"下一拍",即"持续 1 个有效结算"(proposal 语义的状态池落地)。敌方防御池是第四个乘区池:敌方作为防守方的百分比修正池,消费面 = [战斗结算](战斗结算.md)主角攻击的池乘区——四池对称(敌打主角 = 主角防御池 × 敌方输出池,主角打敌 = 主角输出池 × 敌方防御池),状态池语义零延伸:仍是同构实例、side-wide 加算、不封顶不保底。
|
||||
|
||||
@@ -195,7 +195,7 @@ empower 与 fortify 是敌方技能池增益技在状态池侧的数据落点(
|
||||
|
||||
1. 战斗结算通知战斗开始:状态池初始化,空池进入可用状态。
|
||||
2. 轮末结算时,战斗结算把这一轮的扫雷事实兑换成授予:挖开安全格给主角上增益、封印标中削弱敌人、踩雷给敌方上增强、标空封印触发反噬——授予入口是唯一进货口。
|
||||
3. 状态池按表行合并策略受理(新建 / 叠一层 / 刷新),实例立即生效并携带归因记录。
|
||||
3. 状态池按表行合并策略受理(新建 / 叠一层 / 累加时长),实例立即生效并携带归因记录。
|
||||
4. 战斗结算查询生效清单,逐行兑付进结算公式(有效值 = 层数 × 行值)。
|
||||
5. 兑付完做轮末衰减:全部实例剩余次数减一,归零移除——先兑付、后衰减。
|
||||
6. 战斗外授予、未知状态、满层授予一律被拒且状态池不变;满层的"奖励未生效"行归战斗结算的结算明细。
|
||||
@@ -207,7 +207,7 @@ empower 与 fortify 是敌方技能池增益技在状态池侧的数据落点(
|
||||
|
||||
| 优先级 | 内容 | 后置原因 |
|
||||
|---|---|---|
|
||||
| P1 | 刷新、并存两类合并策略的后续内容行 | 语义已定义、状态池已承载;刷新类已有内容(empower/fortify 2026-09-16 敌人技能池批、weaken 2026-09-18 续时定案),并存仍空,首个并存内容行上线时顺带补验收 |
|
||||
| P1 | 累加、并存两类合并策略的后续内容行 | 语义已定义、状态池已承载;累加类已有内容(weaken/empower/fortify/attack_up/ac_up),并存仍空,首个并存内容行上线时顺带补验收 |
|
||||
| P1 | 清图奖励含增益的授予路径 | 来源类别已预留;内容与档位归[地图战斗资源](地图战斗资源.md)定稿 |
|
||||
| P1 | 单体寻址扩展位(接口已定稿——五条语义见核心规则〈单体寻址扩展位〉节) | 实现随首个单体效果的内容批进游戏,真实入口验收随该批闭合 |
|
||||
| P2 | 驱散 / 延时 / 转移接口 | 状态一旦生效,玩家没有任何手段摘掉、延后或挪给别人——省掉一整类界面复杂度是明确的设计决定;要加属于新增接口的修订,不是调参 |
|
||||
@@ -230,12 +230,12 @@ empower 与 fortify 是敌方技能池增益技在状态池侧的数据落点(
|
||||
| sapped 幅度 | 百分比 [−100,100] 且非 0 | percent 行 magnitude | 乏力;危险格负向 weaken 型,进主角输出修正池(2026-09-15 格面资源批补登记) |
|
||||
| empower 幅度 | 百分比 [−100,100] 且非 0 | percent 行 magnitude | 蓄力;敌方技能池增益技载体,进敌方输出修正池 |
|
||||
| fortify 幅度 | 百分比 [−100,100] 且非 0 | percent 行 magnitude | 硬化;敌方技能池增益技载体,进**敌方防御池**(负值 = 减伤方向——2026-09-18 符号纠正) |
|
||||
| 初始剩余次数 | 整数 [1,99] | duration_settlements | 一次性语义行逐行读表;续时行 = weaken(2026-09-18 定案,duration 2)+ empower/fortify(1 个有效增益拍——先兑付后衰减含施放当轮)+ **attack_up / ac_up(2026-09-20 主角增益续时批,duration 表值 1 维持——续时语义在 duration=1 下表现为重复拿重置当轮倒数;跨轮延续随将来 duration 拉长自然显形,数值归状态层重设计批)**;拉长 = 恢复跨轮延续、规划变简单 |
|
||||
| 叠层上限 | 整数 [1,16] | max_stack | 仅叠加策略消费;刷新行层数恒 1——weaken / attack_up / ac_up 表值上限在续时语义下不消费(2026-09-18 / 2026-09-20),empower/fortify ×1(冷却闸下不可能自叠加) |
|
||||
| 初始剩余次数 | 整数 [1,99] | duration_settlements | 累加行 = weaken(duration 2)+ empower/fortify(表值 2 = 先兑付后衰减含施放当轮的 1 个有效增益拍)+ attack_up / ac_up(duration 1);累加语义下重复拿一次就按本次授予时长加一段;拉长表值 = 每次授予攒更多段、规划变简单 |
|
||||
| 叠层上限 | 整数 [1,16] | max_stack | 仅叠加策略消费;累加行层数恒 1、表值上限不消费,empower/fortify ×1(冷却闸下不可能自叠加) |
|
||||
|
||||
非调参的冻结项:三类合并策略语义、两种量纲语义、来源类别枚举、"先兑付后衰减"时序、拒绝原子性、零随机——改动走本文修订流程,不走调参。
|
||||
|
||||
联动纪律:增伤与削弱是当量锚的两端;盾牌加护与[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)的承伤预期联动(掷点折扣轴——AC 行值变更须复算生存链区间;2026-09-20 主角增益续时批后 ac_up 层数恒 1、局内可达 AC 上限 11,复算随调参批);削弱改续时后"单轮层数上限受[行动经济](../排雷循环/行动经济.md)行动点约束"的联动随之失效(2026-09-18 续时定案——同名重复标中只刷新剩余次数、层数恒 1;2026-09-20 起主角侧 attack_up / ac_up 同口径)。这些联动值禁止独立调参,修订须重跑锚对照并同步登记册。
|
||||
联动纪律:增伤与削弱是当量锚的两端;盾牌加护与[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)的承伤预期联动(掷点折扣轴——AC 行值变更须复算生存链区间;ac_up 层数恒 1、局内可达 AC 上限 11,复算随调参批);削弱与主角增益行同名重复授予只加时长、层数恒 1,"单轮层数上限受[行动经济](../排雷循环/行动经济.md)行动点约束"的联动不成立。这些联动值禁止独立调参,修订须重跑锚对照并同步登记册。
|
||||
|
||||
## 验收标准
|
||||
|
||||
@@ -243,7 +243,7 @@ empower 与 fortify 是敌方技能池增益技在状态池侧的数据落点(
|
||||
- 多向同构:主角侧增益与减益、敌方侧增强与削弱共用同一套受理、合并、倒数、清除语义,不存在第二套计时逻辑。
|
||||
- 授予即生效:状态发入当轮结算即兑付第一次,无"待生效"中间态。
|
||||
- 只登记不去重:同一事实被转化两次授予,登记两层——去重责任在战斗结算。
|
||||
- 叠层不减钟:叠加策略行叠加只加层数,剩余次数保持首次授予定下的轨迹不变(刷新策略行则相反——层数不变、只重置剩余次数;两策略逐行读表)。
|
||||
- 叠层不减钟:叠加策略行叠加只加层数,剩余次数保持首次授予定下的轨迹不变(累加策略行则相反——层数不变、剩余次数加上本次授予的时长;两策略逐行读表)。
|
||||
- 满层拒绝原子且留痕:叠满后再授予返回失败、状态池逐字不变;结算明细多一行"奖励未生效"(含来源)。
|
||||
- 先兑付后衰减:每轮结算恰做一次衰减,归零实例当轮移除;停手轮、弃图刷新轮照常衰减。
|
||||
- 合成不设上下限:状态池输出的有效值恒等于层数 × 行值,越出百分比域不封顶不保底;地板在战斗结算侧。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 战斗结算
|
||||
|
||||
> 一句话:轮末结算的编排与逐行兑现
|
||||
> 状态:已定稿(自旧体系迁移);现行为 v1.3
|
||||
> 状态:已定稿(自旧体系迁移);现行为 v1.4
|
||||
|
||||
## 设计目的
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
| 判定行 | 胜 / 负 / 继续各一行;终局判定行同时是胜负画面的数据锚 |
|
||||
| 高光行 | 完美封印成立时一行,只记实际有效兑付 |
|
||||
|
||||
每行还携带公共字段:来源类别、来源格、轮次、显示名、数值,以及可空的高光标记(用于强调封印标中、清图奖励这类值得高亮的结果)。没生效也要留痕:两类"未生效"行如实记录合法例外,呈现时聚合计数,不逐行刷屏。
|
||||
每行还携带公共字段:来源类别、来源格、轮次、显示名、数值,以及可空的高光标记(用于强调封印标中、清图奖励这类值得高亮的结果)。奖励直伤行的数值字段沿用 `effectiveness` 键名、值为整数点数——键名与量纲是数据导出与分析工具的兼容契约,改动须连动分析工具面。没生效也要留痕:两类"未生效"行如实记录合法例外,呈现时聚合计数,不逐行刷屏。
|
||||
|
||||
任何一行找不到来源即断言失败——宁可返回错误,也不让"合并到看不出来源"的呈现混过数据层。但有些行天然没有棋盘来源,断言为此留了两层豁免,豁免集合是封闭清单,改动同样走本文修订。
|
||||
|
||||
@@ -268,21 +268,21 @@ EV 参考数不入正文(派生读数,需要时按判定式与现行表值
|
||||
|
||||
### 换图系专长:刷新事实与本轮兑付(设计定稿,待实现)
|
||||
|
||||
持有换图系后续专长时,成功刷新除执行上述换图编排外,还产生一次 `board_refreshed` 事实。旧图退役前保存它的探索进度与清图奖励对象身份;新板就位后登记事实。刷新被拒绝时不产生事实,任何后续效果都不触发。同一轮多次成功刷新按发生顺序逐次登记,不合并成一次。
|
||||
持有换图系后续专长时,成功刷新除执行上述换图编排外,还产生一次 `board_refreshed` 事实。事实只在新板装板成功后登记;刷新被拒绝时不产生事实,任何后续效果都不触发。同一轮多次成功刷新按发生顺序逐次登记,不合并成一次。
|
||||
|
||||
刷新只登记事实,不当场插入结算。各效果在既有五段内兑付:
|
||||
刷新只登记事实,不当场插入结算。各效果在既有五段与对应消费点兑付:
|
||||
|
||||
| 专长 | 兑付规则 |
|
||||
|---|---|
|
||||
| 弃图轰击 | 每次刷新事实开一张全体敌人伤害单据,在本轮战斗段末经伤害控制器统一结账;目标按结账时的存活状态解析 |
|
||||
| 换图蓄势 | 每次刷新事实当场累计一层,受表值上限约束;下一次主角攻击附加全部层数对应的伤害,随后清零;战斗结束清空 |
|
||||
| 换图庇护 | 每次刷新事实在本轮授予段经既有护盾通道授予一次,不新开挖掘期状态入口 |
|
||||
| 弃图休整 | 每次刷新事实在本轮授予段回复生命,按既有治疗规则封顶生命上限;满血时记未生效行 |
|
||||
| 弃图回收 | 每次刷新事实在本轮授予段按旧图探索进度回收其清图奖励对象;进度与奖励口径归[地图战斗资源](地图战斗资源.md) |
|
||||
| 崩壁冲击 | 每次刷新事实开一张全体敌人伤害单据,在本轮战斗段末经伤害控制器统一结账;目标按结账时的存活状态解析 |
|
||||
| 蓄力周转 | 每次刷新事实累计一层,最多 3 层;本轮下一次主角攻击消费全部层数对应的攻击加成,随后清零;战斗结束不跨战保留 |
|
||||
| 石层缓冲 | 每次刷新事实把当轮全局减伤状态置为生效;本轮主角每次受到伤害都固定减少 2 点,同一轮多次刷新不继续叠加 |
|
||||
| 坑道小憩 | 每次刷新事实累计 1 点恢复量,最多 3 点;本轮授予段合计回复生命,封顶生命上限,随后清零;满血时记未生效行 |
|
||||
| 矿道红利 | 每次刷新事实固定获得 1 GP,走战斗内 GP 缓冲并在授予段留痕;不按旧图探索进度折算,也不与清图奖励绑定 |
|
||||
|
||||
弃图轰击若击杀最后敌人,仍由本轮判定段按既有顺序判胜,不新增挖掘期即时胜利出口。换图系效果使用扩展行留痕:来源写明专长与被弃旧图,弃图轰击走伤害单据,护盾、回血与回收各保留自己的兑付记录;它们不指向单一棋盘格,使用明细流水的专长刷新效果来源豁免,不伪造来源格。
|
||||
全局受伤事件统一:`damage_taken` 是主角受到伤害的唯一效果事件,覆盖敌方攻击、危险格、错误封印资源格与地形造成的主角伤害。石层缓冲订阅该事件并在现行伤害下限之前消费;旧有订阅同一事件的专长也按这一完整语义生效。临时生命值不在此机制内。
|
||||
|
||||
弃图回收不是清图:不触发完美封印加倍,不发清图事件,不改变「刷新图不发清图奖励」。它兑的是被放弃旧图按探索进度回收的一部分奖励。
|
||||
崩壁冲击若击杀最后敌人,仍由本轮判定段按既有顺序判胜,不新增挖掘期即时胜利出口。换图系效果使用扩展行留痕:崩壁冲击走伤害单据,石层缓冲在受伤消费行体现减免,坑道小憩与矿道红利分别走治疗与 GP 入账记录。刷新事实来源不指向单一棋盘格,使用明细流水的专长刷新效果来源豁免,不伪造来源格。
|
||||
|
||||
战斗开始按固定序列开张:关上配置热重载的闸门、初始化状态池、掷骰定敌方实际数量、生成敌人、装板并预分配资源恰一次、变异布置([地形变异](../排雷循环/地形变异.md)——棋盘设置顺序第 4 步:生成棋盘 → 打开状态初始化 → 战斗资源格设置 → 变异格设置,消费 `variant_place` 封闭流)、发开轮通知并执行开轮摇号(摇定首轮各敌本拍动作)。上一关的大奖励(如血上限提升)经战斗开始入参的覆盖值通道生效,内容归[冒险结构](../冒险循环/冒险结构.md)。
|
||||
|
||||
@@ -379,8 +379,9 @@ EV 参考数不入正文(派生读数,需要时按判定式与现行表值
|
||||
- 行型封闭:流水行型只出现本文登记的基础 9 类与扩展行;行型词表改动必须走本文修订并同步呈现层行格式化,未登记行型禁止落行。
|
||||
- 伤害单据管线(2026-09-24):全部伤害(主角先手、敌方伤害技、棋盘克制直伤、群伤、反噬、地形变异)经伤害控制器单据管线兑现,兑付配方封闭五种;目标在结账时刻照规则解析;死亡事件由单位自行宣布——怪死其未结单据作废(零行、不进冷却不进保底计数)、主角死兑付终止;段界结账为现行口径、开单即结为回切结构位,两模式各自同种子同输入逐位复现。
|
||||
- 换图系刷新事实:成功刷新保存旧图探索进度并登记一次事实,被拒绝时不登记;同一轮多次刷新按发生顺序逐次兑付,刷新本身不新增结算段或轮次结束。
|
||||
- 换图系输出:弃图轰击在本轮战斗段末经伤害控制器结算;换图蓄势受层数上限约束,下一次主角攻击消费全部层数,战斗结束清空;群伤致敌清空时仍由本轮判定段判胜。
|
||||
- 换图系生存与回收:护盾、回血与弃图回收在本轮授予段兑付并留下可归因扩展行;回复封顶生命上限;回收不触发清图事件或完美封印加倍。
|
||||
- 换图系输出:崩壁冲击在本轮战斗段末经伤害控制器结算;蓄力周转受 3 层上限约束,本轮下一次主角攻击消费全部层数,战斗结束清空;群伤致敌清空时仍由本轮判定段判胜。
|
||||
- 换图系生存与资源:石层缓冲经全局 `damage_taken` 对敌方攻击、危险格、错误封印与地形伤害各减 2,同轮不叠加;坑道小憩在授予段按累计值回血并封顶;矿道红利每次成功刷新固定入账 1 GP。
|
||||
- 全局受伤事件:`damage_taken` 只在主角实际承伤消费点发射一次,所有伤害路径均经同一效果引擎减免面,禁止同一张单重复发射或只让部分伤害来源订阅生效。
|
||||
- 条款3 对账(2026-10-05 判式换代批 T3——与行动经济 F1 条款3 重立同步):①聚合健康式 (1−ρ̂)·E_dig(u) − ρ̂·V_mine(u) ≥ 0 与 β 比值带 [1.2, 2.5] 判读逐 u 检算(清图支配性审计为常设重跑项,读数快照登记归 clause3 重立条);②封印溢价兑付正确:封印标中正向兑付 = 同型正向 ×(1+δ)(δ 读 `settlement.tres` flag_hit_premium,100=无溢价直通零扰动),三类形态全覆盖(flat power / percent 幅度 / gp 缓冲)、标空与挖格兑付不乘溢价、危险格与安全格同池结构零触碰。
|
||||
|
||||
## 待办
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
| trigger(触发条件) | 全部,可省 | {fact, op, value, latch} 四键——fact 与 op 取「触发条件」节封闭词表,value 类型与域随 fact,出域加载期拒绝;缺省无字段=无条件技,旧行为零变化(2026-10-26 情境条件触发器批立列、2026-10-27 迭代 39 实装落地;原池条目的血量百分比字段收编为本字段闩型首用例) |
|
||||
| P1 字段位 | — | 命中段数、目标数——随对应后置批加列(触发条件位已由 trigger 立列收编——2026-10-26 立列、2026-10-27 实装;原"固定值分量"位已由 rider_mult 实装收编——2026-09-19;多段命中「命中段数」保留以防复活) |
|
||||
|
||||
普通攻击也是一行技能行,两条域校验锁死它的特殊性:倍率恒 1、冷却恒 0——想配倍率或冷却的就不是普攻,是伤害技。技能伤害存倍率、不存固定值:伤害 = 施放者攻击基线 × 技能倍率——同一行"重击",不同敌种按各自攻击基线缩放 = 基线 A × 技能倍率 M(行值读 enemy.tres / skills.tres),深层强化行自动缩放,这是共享技能表成立的前提;**固定的绝对值伤害不满足该前提**(不随基线缩放、跨关稀释——Brotato 教训),"固定值伤害"的合法形态是**必中附加分量 rider_mult**(×攻击基线的必中小倍率,2026-09-19 实装)。增益技带效果引用与持续回合,百分比天然按施放者同池生效,无缩放问题;妨害技同构,落点由状态行 target_side 路由到主角。
|
||||
普通攻击也是一行技能行,两条域校验锁死它的特殊性:倍率恒 1、冷却恒 0——想配倍率或冷却的就不是普攻,是伤害技。技能伤害存倍率、不存固定值:伤害 = 施放者攻击基线 × 技能倍率——同一行"重击",不同敌种按各自攻击基线缩放 = 基线 A × 技能倍率 M(行值读 enemy.tres / skills.tres),深层强化行自动缩放,这是共享技能表成立的前提;**固定的绝对值伤害不满足该前提**(不随基线缩放、跨关稀释——Brotato 教训),"固定值伤害"的合法形态是**必中附加分量 rider_mult**(×攻击基线的必中小倍率,2026-09-19 实装)。增益技带效果引用与持续回合,百分比天然按施放者同池生效,无缩放问题;妨害技同构,落点由状态行 target_side 路由到主角。技能伤害基线的取整:攻击基线 × 倍率按四舍五入、正好半格时一律向上取——全伤害管线唯一不用向下取整的点(半点上取保住低攻击档特殊技的辨识度:攻击 1 配 ×1.5 的突刺落到 2 而不是 1)。
|
||||
|
||||
池构成纪律(加载校验,违规拒绝入表):**每池恰一行普通攻击**(普攻冷却恒 0 → 可摇集合恒非空,"空池不可能"由结构保证,最坏情况怪物平砍等待——那是玩家的反打窗口而非死拍);**每池增益类技能 ≤1**(非 RPG,怪物连出不同增益显得呆);**每池妨害类技能 ≤1**(2026-09-19 新效应词汇批同构纪律);**每池至少一行不带触发条件的特殊技**(普攻不算——保底特殊技集合恒非空的结构保证,2026-10-26 触发条件批);池条目 skill_id 悬空致命;权重 > 0、冷却 ≥ 0。
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 行动经济
|
||||
|
||||
> 一句话:每轮行动点怎么花、停手与刷新的代价
|
||||
> 状态:已定稿(自旧体系迁移);现行为 v1.2
|
||||
> 状态:已定稿(自旧体系迁移);现行为 v1.3
|
||||
|
||||
## 设计目的
|
||||
|
||||
@@ -97,12 +97,12 @@
|
||||
|
||||
| 专长 | 规则 | 不变边界 |
|
||||
|---|---|---|
|
||||
| 掘进储能 | 每次付费挖格被受理后,额外获得表值 RAP;安全格与危险格同口径 | 连锁展开只随首格的一次付费动作结算,不逐格追加;封印、移动、刷新不产 RAP |
|
||||
| 折价换图 | 弃图刷新的 RAP 成本为基线的 50% | 只改价格;AP、轮次、每轮刷新上限与换图流程不变 |
|
||||
| 矿脉储能 | 每次付费挖格被受理后,额外获得表值 RAP;安全格与危险格同口径 | 连锁展开只随首格的一次付费动作结算,不逐格追加;封印、移动、刷新不产 RAP |
|
||||
| 省力周转 | 弃图刷新的 RAP 成本为基线的 50% | 只改价格;AP、轮次、每轮刷新上限与换图流程不变 |
|
||||
|
||||
两件基座可以同时持有并叠加:付费挖格照常产 RAP,刷新同时按半价扣费。两条规则作用在不同账户入口,不互相覆盖,也不设互斥。刷新仍须储备达到折后门槛才受理;储备不足或超过每轮次数上限时拒绝,状态不变。RAP 仍在战斗结束清零,刷新本身不返 RAP。
|
||||
|
||||
无掘进储能时,挖格不新增 RAP;无折价换图时,刷新成本仍读全局基值。后续输出、生存与资源专长如何使用成功刷新这个事实,归[战斗结算](../战斗循环/战斗结算.md)。
|
||||
无矿脉储能时,挖格不新增 RAP;无省力周转时,刷新成本仍读全局基值。后续输出、生存与资源专长如何使用成功刷新这个事实,归[战斗结算](../战斗循环/战斗结算.md)。
|
||||
|
||||
### 下轮行动点修正(格面资源八型批 2026-09-20)
|
||||
|
||||
@@ -249,8 +249,8 @@
|
||||
| move_ap_cost | int | 表值(冻结 0) | 移动免费——冻结,非调参项 |
|
||||
| chain_ap_cost | int | 表值(冻结 0) | 连锁免费——冻结,非调参项 |
|
||||
| refresh_cap_per_round | int | 表值(provisional,结构位预留) | 每轮刷新上限,开轮重置计数 |
|
||||
| (跨表)掘进储能产 RAP | int [1, 64] | 设计定稿,待入 `feats.tres` | 每次受理的付费挖格兑付一次;连锁不逐格追加 |
|
||||
| (跨表)折价换图成本率 | percent | 50%(结构已裁,待入 `feats.tres`) | 只改持有者的刷新价格;双基座叠加时不覆盖掘进储能 |
|
||||
| (跨表)矿脉储能产 RAP | int [1, 64] | 设计定稿,待入 `feats.tres` | 每次受理的付费挖格兑付一次;连锁不逐格追加 |
|
||||
| (跨表)省力周转成本率 | percent | 50%(结构已裁,待入 `feats.tres`) | 只改持有者的刷新价格;双基座叠加时不覆盖矿脉储能 |
|
||||
| (跨表)跨冒险行动点加成 | int [0,10] / int [0,99],冒险表 | adventure_table bonus_ap_flat / bonus_cap_ap(provisional) | 三选一 AP 件兑现值与全冒险次数上限;加成轴语义见「跨冒险行动点加成轴」节 |
|
||||
|
||||
字段名注记:2026-09-18 措辞完成批已全链对齐正式名——表字段与本文行名统一为 `refresh_cap_per_round`,刷新入口命名与表字段口径对齐(命名统一登记兑现,切片临时名零残留)。
|
||||
@@ -265,8 +265,8 @@
|
||||
- 仲裁唯一:最后 1 点行动点挖开最后一个安全格时,判清图(剩余清零 + 换图 + 清图奖励),不判耗尽;输出恰一条。
|
||||
- 停手 1:1:剩余 3 点停手,储备池加 3,下轮同图继续。
|
||||
- 刷新规则:储备不足时刷新被拒绝且状态不变;同一轮第 3 次刷新被拒绝;刷新不重置行动点、不发清图奖励、旧图上的封印印记失效。
|
||||
- 掘进储能:持有专长时,每次受理的付费挖格额外产一次 RAP,安全格与危险格同口径,连锁展开不逐格追加;无专长时挖格不新增 RAP。
|
||||
- 折价换图与双基座:持有折价换图时按基线的 50% 扣费;与掘进储能同时持有则两条规则都生效,刷新仍受储备门槛与每轮次数上限约束。
|
||||
- 矿脉储能:持有专长时,每次受理的付费挖格额外产一次 RAP,安全格与危险格同口径,连锁展开不逐格追加;无专长时挖格不新增 RAP。
|
||||
- 省力周转与双基座:持有省力周转时按基线的 50% 扣费;与矿脉储能同时持有则两条规则都生效,刷新仍受储备门槛与每轮次数上限约束。
|
||||
- 储备清零:战斗结束信号到达后储备池归 0,行动点进入休眠。
|
||||
- 跨字段校验:任一动作成本配置超过每轮预算时,加载期致命中止。
|
||||
- 零随机:同一动作序列驱动两个独立行动点实例,轮次结束记录与储备轨迹逐项相同。
|
||||
|
||||
+29
-12
@@ -93,11 +93,29 @@ func request_dig(chain_size: int, clears_map: bool) -> bool:
|
||||
_reject("phase")
|
||||
return false
|
||||
var cost := _cost("dig")
|
||||
# dig_paid 发射+施加(付费开掘发起、动作经济闸收对价前:
|
||||
# dig_paid 发射+施加(付费开掘发起、动作经济闸收对价前——施加单点 _apply_dig_paid_hits):
|
||||
# counter_spend 命中=尝试加收对价并积计数;资源不足走 fallback/耗尽即止=只付常规基础对价。
|
||||
# counter_add〔双管镐 付费挖格计数〕/ grant_ap〔每第 2 次返 1 AP——对价减免,
|
||||
# mod_eq 闸在行 when〕同面施加)
|
||||
var apply_res := _apply_dig_paid_hits(cost)
|
||||
cost = int(apply_res["cost"])
|
||||
if _ap < cost:
|
||||
_reject("resource_face")
|
||||
return false # 拒绝轮零入池(矿脉储能只在扣费受理后兑付——C2 成功才登记同款纪律)
|
||||
if int(apply_res["refund"]) > 0:
|
||||
cost = maxi(0, cost - int(apply_res["refund"])) # 返还=对价减免(净费不低于 0——免费开格不产生负对价)
|
||||
var accepted := _spend_cost_and_arbitrate(cost, clears_map)
|
||||
if accepted and int(apply_res["rap_gain"]) > 0:
|
||||
grant_reserve_ap(int(apply_res["rap_gain"])) # 每次受理的付费挖格产 RAP(一次性入池——每动作恰一挖,连锁不二次)
|
||||
return accepted
|
||||
|
||||
|
||||
# dig_paid 命中施加单点(迭代 46 卡A T3 自 request_dig 迁出——分支本体逐字保留:
|
||||
# counter_spend 加收对价并积计数/counter_add 施加〔双管镐 付费挖格计数〕/
|
||||
# grant_ap 对价减免〔每第 2 次返 1 AP——mod_eq 闸在行 when〕/grant_reserve_ap 矿脉储能累计;
|
||||
# 命中序=声明序不变。返回 {cost, refund, rap_gain} 交受理面消费)
|
||||
func _apply_dig_paid_hits(base_cost: int) -> Dictionary:
|
||||
var cost := base_cost
|
||||
var dig_refund := 0
|
||||
var rap_gain := 0
|
||||
if _fx != null:
|
||||
for h in _fx.dispatch(_ctx, "dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}):
|
||||
if String(h.get("kind", "")) == "counter_spend":
|
||||
@@ -110,12 +128,9 @@ func request_dig(chain_size: int, clears_map: bool) -> bool:
|
||||
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
elif String(h.get("kind", "")) == "grant_ap":
|
||||
dig_refund += int(h.get("value", 0))
|
||||
if _ap < cost:
|
||||
_reject("resource_face")
|
||||
return false
|
||||
if dig_refund > 0:
|
||||
cost = maxi(0, cost - dig_refund) # 返还=对价减免(净费不低于 0——免费开格不产生负对价)
|
||||
return _spend_cost_and_arbitrate(cost, clears_map)
|
||||
elif String(h.get("kind", "")) == "grant_reserve_ap":
|
||||
rap_gain += int(h.get("value", 0))
|
||||
return {"cost": cost, "refund": dig_refund, "rap_gain": rap_gain}
|
||||
|
||||
|
||||
func _spend_cost_and_arbitrate(cost: int, clears_map: bool) -> bool:
|
||||
@@ -196,13 +211,15 @@ func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void:
|
||||
# 轮内刷新:唯一入口 =
|
||||
# Active 相位——仅扣 RAP,相位与 AP 均不动。刷新不打断轮次、不触发结算;换图编排归 战斗结算
|
||||
# (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 HUD框架发起)纪律不变。
|
||||
func request_refresh() -> bool:
|
||||
# cost_override=折后价入参(迭代 46 卡A T3——BattleRefreshFlow 折价读数传入;缺省 -1=表基线逐位不变)
|
||||
func request_refresh(cost_override: int = -1) -> bool:
|
||||
if _phase != Phase.ACTIVE:
|
||||
_reject("phase")
|
||||
return false
|
||||
if _rap < db.run.action_economy.rap_refresh_cost:
|
||||
var cost := int(db.run.action_economy.rap_refresh_cost) if cost_override < 0 else cost_override
|
||||
if _rap < cost:
|
||||
return false # EC13 资源不足拒绝——合法相位内资源拒绝,非编排错误
|
||||
_rap -= db.run.action_economy.rap_refresh_cost
|
||||
_rap -= cost
|
||||
return true
|
||||
|
||||
|
||||
|
||||
@@ -31,8 +31,9 @@ signal settlement_snapshot(snap: Dictionary) ## 每结算末尾的全量状态
|
||||
const SOURCE_EXEMPT_TYPES := ["decay", "verdict"]
|
||||
# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图而非棋盘格;
|
||||
# fx_reveal=专长揭示掷格(落空行无真实格);
|
||||
# fx_grant_gp=专长·GP 入账(钱包面而非棋盘面,行无格子可指)
|
||||
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp"]
|
||||
# fx_grant_gp=专长·GP 入账(钱包面而非棋盘面,行无格子可指);
|
||||
# fx_board_refresh=成功刷新事实族(迭代 46 卡A——崩壁冲击群伤行源=换图动作而非棋盘格)
|
||||
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp", "fx_board_refresh"]
|
||||
|
||||
var _facts: BattleFacts # 战斗事实件(battle_facts.gd——事实族 15 成员单点)
|
||||
var _rounds: RoundCycle # 轮循环驱动件(settlement/round_cycle.gd——五段持有/段序/重入闸/摇号)
|
||||
@@ -596,26 +597,17 @@ func _exit_distribution() -> void:
|
||||
round_started.emit(round_index)
|
||||
|
||||
|
||||
# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道
|
||||
# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道(折价成本同源:判据归 BattleRefreshFlow)
|
||||
func can_refresh() -> bool:
|
||||
return battle_result == "" \
|
||||
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)
|
||||
return BattleRefreshFlow.can_refresh(self)
|
||||
|
||||
|
||||
# 玩家经 HUD框架发起、战斗结算 代理调用(禁止 RAP 满自动刷)。
|
||||
# 挖掘期唯一通道:Active 相位立即换图
|
||||
# 继续挖(RAP -8;AP 保留、相位不动、不触发结算);非挖掘期请求一律拒绝——非清图轮恒同图。
|
||||
# 挖掘期唯一通道:Active 相位立即换图继续挖(RAP 按折后价扣;AP 保留、相位不动、不触发结算);
|
||||
# 非挖掘期请求一律拒绝——非清图轮恒同图。刷新门/折价扣费/换图/成功事实归 BattleRefreshFlow
|
||||
# (迭代 46 卡A T3 拆出——编排器入口与唯一通道不变,C10 结构预算只降不升)。
|
||||
func player_request_refresh() -> bool:
|
||||
if not can_refresh():
|
||||
return false
|
||||
if not action_economy.request_refresh():
|
||||
return false
|
||||
_facts.refresh_used += 1
|
||||
_reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提)
|
||||
minesweeper.retire_board(true) # 旧板退役(轮内通道)
|
||||
return _install_and_reset(true) # 当场换图——失败即编排违例 fail-fast
|
||||
return BattleRefreshFlow.execute(self)
|
||||
|
||||
|
||||
func _battle_over(result: String) -> void:
|
||||
|
||||
@@ -30,12 +30,13 @@ const TABLE_MANIFEST := {
|
||||
"feats": "feats.tres", # FeatTable 规则改写型特性(定义表 Rule 4 豁免——TreasureItemTable 先例;空表合法态=主时间线零扰动)
|
||||
"forge": "forge.tres", # ForgeTable——两根类型轴默认值+六路线行族;攻防所有权切分唯一数据面
|
||||
"battle_layout": "battle_layout.tres", # BattleLayoutTable——战斗屏分区几何与明细带高度单行
|
||||
"camp_scene_layout": "camp_scene_layout.tres", # CampSceneLayoutTable——营地场景 10 层贴图/命中矩形与门洞缺口单行
|
||||
}
|
||||
|
||||
var combat: ConfigDomainCombat # 战斗编排域七表(board_gen/variation/status_effects/enemy/skill/hero/settlement)
|
||||
var run: ConfigDomainRun # 冒险 runner 域七表(level/difficulty/level_gen_params/reward/adventure/action_economy/shop)
|
||||
var crossrun: ConfigDomainCrossrun # 跨局元进度域五表(meta/meta_tree/treasure_items/feats/forge)
|
||||
var presentation: ConfigDomainPresentation # 表现层布局域一表(battle_layout)
|
||||
var presentation: ConfigDomainPresentation # 表现层布局域两表(battle_layout/camp_scene_layout)
|
||||
|
||||
var is_ready := false
|
||||
var _diagnostics: Array[Dictionary] = [] # {severity, category, message}
|
||||
@@ -149,6 +150,9 @@ func _assign_loaded(loaded: Dictionary) -> void:
|
||||
crossrun.feats = loaded.get("feats")
|
||||
crossrun.forge = loaded.get("forge")
|
||||
presentation.battle_layout = loaded.get("battle_layout")
|
||||
presentation.camp_scene_layout = loaded.get("camp_scene_layout")
|
||||
for problem: String in presentation.camp_scene_layout.validate():
|
||||
_fatal("camp_scene_layout", problem) # 布局几何红线(命中区越画面/相交/侵顶栏)=数据致命
|
||||
# resolver 协作件注入三表(resolve 委托承载——resolver 文件与键零搬移)
|
||||
combat.board_gen._resolver = _resolver
|
||||
combat.enemy._resolver = _resolver
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
## 表现层布局域容器(D2 域容器——config_db 表持有按域收敛):战斗屏布局一表类型化持有。
|
||||
## 表现层布局域容器(D2 域容器——config_db 表持有按域收敛):战斗屏与营地场景布局两表类型化持有。
|
||||
## 零方法零行为——纯持有结构;「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。
|
||||
class_name ConfigDomainPresentation
|
||||
extends Resource
|
||||
|
||||
var battle_layout: BattleLayoutTable
|
||||
var camp_scene_layout: CampSceneLayoutTable
|
||||
@@ -460,8 +460,8 @@ func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary
|
||||
# 敌方伤害技:掷点 → 折扣/暴击 → 克制点数(attack_type vs 主角护甲类型——
|
||||
# 无类型恒中性 delta 0,写法同 hero_attack 的 _counter_delta 消费位)→ hero_defense×enemy_output
|
||||
# 池乘区 → 下限 → +rider(后置必中附加)→ 交付 → commit_action(作废单不 commit——死亡即不动手)。
|
||||
# 掷点算术归 _enemy_roll;damage_taken 施加面归 _enemy_shield_apply(发射在本函数——
|
||||
# 与减伤落快照的先后序固定)。
|
||||
# 掷点算术归 _enemy_roll;damage_taken 统一发射+消费归 _emit_hero_damage_taken(发射在本函数——
|
||||
# 与减伤落快照的先后序固定;「掷点后、交付前」消费时点不变)。
|
||||
func _resolve_enemy_skill(t: Dictionary) -> void:
|
||||
var hero: Dictionary[Variant, Variant] = units.hero_unit()
|
||||
var roll := _enemy_roll(t)
|
||||
@@ -469,9 +469,8 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
|
||||
# damage_taken 发射(kind=enemy_skill;铁壁整备减伤订阅面:shield 加法族合计 value×per(charge_layers),
|
||||
# 稳健拓荒树规则同面(rebuild 合成订阅);减后过下限保底。damage_reduce〔背水 hp_ratio when〕/
|
||||
# counter_add〔保险丝/石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 _fx_fact 补充事实面回落提供)
|
||||
var dt_hits := _fx_hits("damage_taken", {"kind": "enemy_skill", "amount": total,
|
||||
"source": String(t.context.get("skill_id", "")), "round": int(t.round_index)})
|
||||
total = int(_enemy_shield_apply(dt_hits, total)["total"])
|
||||
var dt_res := _emit_hero_damage_taken(total, "enemy_skill", String(t.context.get("skill_id", "")), t.origin_cell, int(t.round_index))
|
||||
total = int(dt_res["total"])
|
||||
units.apply_damage(hero, total)
|
||||
var payload: Dictionary[Variant, Variant] = {"target": "hero", "skill_id": String(t.context.get("skill_id", "")), "baseline": int(t.context.get("baseline", 0)),
|
||||
"roll": int(roll["roll"]), "ac": int(roll["threshold"]), "tier": int(roll["discount"]), "attack_type": String(t.attack_type)}
|
||||
@@ -502,9 +501,22 @@ func _enemy_roll(t: Dictionary) -> Dictionary:
|
||||
return {"roll": int(roll_read.roll), "threshold": threshold, "crit": crit, "discount": discount, "total": total}
|
||||
|
||||
|
||||
# damage_taken 施加面(shield 加法族合计;damage_reduce 逐条减量〔背水〕;ac_bonus 转编排器单点
|
||||
# 〔石肤 AC 累积归 set_hero_ac_override 消费面〕;counter_add 施加)。返回 {total, shield}。
|
||||
func _enemy_shield_apply(hits: Array, total: int) -> Dictionary:
|
||||
# 全局受伤发射单点(迭代 46 卡A T2——所有主角承伤路径唯一发射口:敌方攻击/地形/危险格/错误封印
|
||||
# 资源直伤(后三者经 board_flat_hero 单据进入)都经此 dispatch 一次 damage_taken 并消费一次;
|
||||
# 同一伤害单据重复发射由 context.hero_damage_consumed 标记闸拦(地形路径在开单点标记)。
|
||||
# payload 必含 kind/amount/source/cell/round/hp_ratio;返回 {total, hits} 交调用侧按减后值开单/交付)。
|
||||
func _emit_hero_damage_taken(amount: int, damage_kind: String, source: String, cell: Vector2i, ri: int) -> Dictionary:
|
||||
var hu: Dictionary[Variant, Variant] = units.hero_unit()
|
||||
var payload: Dictionary[Variant, Variant] = {"kind": damage_kind, "amount": amount, "source": source,
|
||||
"cell": cell, "round": ri, "hp_ratio": int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1))))))}
|
||||
var hits := _fx_hits("damage_taken", payload)
|
||||
return {"total": int(_apply_hero_damage_effects(hits, amount)["total"]), "hits": hits}
|
||||
|
||||
|
||||
# damage_taken 唯一消费族(组合次序冻结:shield 加法族合计→damage_reduce 逐条减量〔背水/石层缓冲〕→
|
||||
# ac_bonus 转编排器单点〔石肤 AC 累积归 set_hero_ac_override 消费面〕→counter_add 施加〔保险丝/石肤/
|
||||
# 换图系临时计数夹具〕。返回 {total, shield})。
|
||||
func _apply_hero_damage_effects(hits: Array, total: int) -> Dictionary:
|
||||
var shield := 0
|
||||
for h in hits:
|
||||
match String(h.get("kind", "")):
|
||||
@@ -565,9 +577,15 @@ func _resolve_board_blast(t: Dictionary) -> void:
|
||||
_outcomes[int(t.ticket_id)] = "redeemed"
|
||||
|
||||
|
||||
# 棋盘主角直伤(反噬/治疗包负向/地形变异):无类型必中,下限保底;context 键并入行载荷
|
||||
# 棋盘主角直伤(反噬/治疗包负向/危险格/错误封印资源直伤):无类型必中,下限保底;context 键并入行载荷。
|
||||
# damage_taken 全局发射点(迭代 46 卡A T2——单据未标 hero_damage_consumed 时在下限前发射并消费一次;
|
||||
# 地形路径已在授予段开单点发射并标记,此处跳过防重复)
|
||||
func _resolve_board_flat_hero(t: Dictionary) -> void:
|
||||
var dealt: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源
|
||||
var dealt: int = int(t.base_value)
|
||||
if not bool((t.get("context", {}) as Dictionary).get("hero_damage_consumed", false)):
|
||||
var res_flat := _emit_hero_damage_taken(dealt, "board_flat_hero", String(t.get("source_class", "")), t.origin_cell, int(t.round_index))
|
||||
dealt = int(res_flat["total"])
|
||||
dealt = maxi(int(db.combat.settlement.damage_roll.damage_floor), dealt) # 伤害下限全来源(减后过下限)
|
||||
units.apply_damage(units.hero_unit(), dealt)
|
||||
var extra: Dictionary[Variant, Variant] = {"target": "hero"}
|
||||
for k in t.context.keys():
|
||||
|
||||
@@ -12,11 +12,15 @@ extends RefCounted
|
||||
const EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "attack_commit", "clamp_check",
|
||||
"armor_roll", "kill", "damage_taken", "enemy_hp_threshold", "round_start", "seal_settle",
|
||||
"round_end", "reward_roll", "treasure_used", "charge_full", "dig_stop", "battle_won", "seal_missed",
|
||||
"board_installed"]
|
||||
"board_installed",
|
||||
# 换图系双事件(refresh_request=玩家请求刷新〔省力周转 apply_discount 订阅面——折扣只改门槛
|
||||
# 与扣费不改拒绝判据〕;board_refreshed=成功刷新事实〔换图系七件订阅主挂点——装新板后才发射〕)
|
||||
"refresh_request", "board_refreshed"]
|
||||
|
||||
## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥1 行订阅本集事件——schema 加载校验。
|
||||
## 原 MINE_HOOKS 的同义迁移:挖掘/封印/轮末结算面事件)
|
||||
const MINE_EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "seal_settle", "round_end", "reward_roll"]
|
||||
## 原 MINE_HOOKS 的同义迁移:挖掘/封印/轮末结算面事件+轮内刷新面〔换图系〕)
|
||||
const MINE_EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "seal_settle", "round_end", "reward_roll",
|
||||
"refresh_request", "board_refreshed"]
|
||||
|
||||
## 效果类型词汇(解释器 switch 闭集——词表实数以 EFFECT_KINDS 常量为准,含共享行拆型:
|
||||
## grant_gp/bias_wallet、counter_add/counter_spend、reveal_tiles/remove_mines/refresh_pick、
|
||||
@@ -40,7 +44,11 @@ const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open
|
||||
"mine_count_preview",
|
||||
# forgive_seal_miss=完美保险(本场首次封印落空不计入完美
|
||||
# 封印判定——订阅 seal_missed 的豁免效果,消费点在授予段;feats.tres 首例 domain=battle 行)
|
||||
"forgive_seal_miss"]
|
||||
"forgive_seal_miss",
|
||||
# 换图系两效果(grant_reserve_ap=矿脉储能·付费挖格产 RAP〔消费点在行动经济 dig_paid
|
||||
# 受理后一次性入池〕;apply_discount=省力周转·刷新成本折扣〔mode=refresh cost_mult 百分比,
|
||||
# floor 取整——只改门槛与扣费、不改拒绝判据〕)
|
||||
"grant_reserve_ap", "apply_discount"]
|
||||
|
||||
## fact 条件词汇(封闭枚举——payload 同名键直读面〔fact 取值序 payload
|
||||
## 优先——effect_engine._fact_value 既有机制〕。new_resource_type=首开资源类型〔地质学家〕;
|
||||
|
||||
@@ -365,6 +365,12 @@ func outcome_of(ticket_id: int) -> String:
|
||||
return "redeemed" if ticket_id < 0 else damage.outcome(ticket_id)
|
||||
|
||||
|
||||
# 延迟单据入队(迭代 46 卡A T4——成功刷新的崩壁冲击群伤单走案C 同队列:授予段闸外唯一入口,
|
||||
# 战斗段末 _board_deferred_settle 按开单序补提交统一结账;类变量零增员——复用 defer.queue)
|
||||
func defer_ticket(t: Dictionary) -> void:
|
||||
(defer.queue as Array).append(t)
|
||||
|
||||
|
||||
# 主角直伤单据(反噬/治疗包负向——无类型必中,下限保底在控制器管线内)
|
||||
func _submit_flat_hero(display: String, power: int, att: Dictionary, sync: bool) -> String:
|
||||
var t := damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, String(att.get("source_class", "")), display,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
class_name BattleRefreshFlow
|
||||
extends RefCounted
|
||||
## 换图刷新流程件(迭代 46 卡A T3/T4 自编排器拆出):刷新可用性门、折价扣费、换图执行与成功事实登记。
|
||||
## 拆分边界(需求卡片 C13):本件只负责可用性、扣费、换图与成功事实登记——七件效果的算法留在
|
||||
## 效果引擎、授予段与伤害控制器。调用路径=呈现层→编排器单行转发→本件→行动经济/装板壳/授予段。
|
||||
## 反自动刷新(C11):只经玩家入口发起,禁 RAP 满自动刷——编排器公开面单行转发,无自动调用点。
|
||||
## 成功序钉死(T4):折价扣 RAP→refresh_used+1→旧资源退役→旧板退役→装新板→授予段成功事实发射;
|
||||
## 装板失败返回 false 且零效果——成功事实只在新板就位后发射。
|
||||
|
||||
|
||||
## 折价成本读数(refresh_request 命中省力周转 apply_discount——只认 kind==apply_discount 且
|
||||
## mode==refresh,取声明序第一命中,floor(base×cost_mult/100);无命中=表基线逐位不变。
|
||||
## can_refresh 与 execute 同源取数〔V3——can 与执行同源判断〕;折扣只改变门槛与扣费,
|
||||
## 不改变拒绝判据〔C2——每轮上限与相位判据照旧〕)
|
||||
static func _effective_refresh_cost(oc: BattleOrchestrator) -> int:
|
||||
var base := int(oc.db.run.action_economy.rap_refresh_cost)
|
||||
for h in oc.fx_host().fx_emit("refresh_request", {"mode": "refresh", "base": base}):
|
||||
if String(h.get("kind", "")) == "apply_discount" and String(h.get("mode", "")) == "refresh":
|
||||
return int(floor(float(base) * float(int(h.get("cost_mult", 100))) / 100.0))
|
||||
return base
|
||||
|
||||
|
||||
## 可用性门(HUD 刷新按钮同源判据):战斗未终局、每轮上限未满、Active 相位、RAP 抵折后价。
|
||||
static func can_refresh(oc: BattleOrchestrator) -> bool:
|
||||
return oc.battle_result == "" \
|
||||
and oc._facts.refresh_used < int(oc.db.run.action_economy.refresh_cap_per_round) \
|
||||
and oc.action_economy.phase == oc.action_economy.PHASE_ACTIVE \
|
||||
and oc.action_economy.rap >= _effective_refresh_cost(oc)
|
||||
|
||||
|
||||
## 执行(玩家请求刷新唯一通道体):扣费受理后继续;扣费拒绝/门拒零效果零事实。
|
||||
static func execute(oc: BattleOrchestrator) -> bool:
|
||||
if not can_refresh(oc):
|
||||
return false
|
||||
if not oc.action_economy.request_refresh(_effective_refresh_cost(oc)):
|
||||
return false
|
||||
oc._facts.refresh_used += 1
|
||||
oc._reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提)
|
||||
oc.minesweeper.retire_board(true) # 旧板退役(轮内通道)
|
||||
if not oc._install_and_reset(true): # 当场换图——失败即编排违例 fail-fast(零成功事实)
|
||||
return false
|
||||
oc._rounds._grant_seg.apply_board_refreshed(oc.round_index) # 成功事实:新板就位后一次发射
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://d2kjxlt5b2eqv
|
||||
@@ -6,7 +6,8 @@ extends RefCounted
|
||||
# 效果施加归 BattleFxHost(段持三引用直调,零转发壳)——
|
||||
# 本段只搬运时序,零算法改动(🔴 零行为变更契约:同种子完整局输出逐位一致)。
|
||||
# run 编排骨架+批私有族(_reveal_batch/_terrain_batch/_seal_batch/_round_end_apply,段内施加面
|
||||
# 再拆 _reveal_safe_apply/_reveal_mine_apply/_terrain_damage_reduce/_seal_hit_effects 等单面函数);
|
||||
# 再拆 _reveal_safe_apply/_reveal_mine_apply/_seal_hit_effects 等单面函数);地形 damage_taken
|
||||
# 消费随迭代 46 卡A T2 并入伤害控制器全局消费族(本段零本地减伤函数);
|
||||
# kind 分派单点化(P1-1 同批):counter_add 五位三行组 / grant_material 三位兑付块
|
||||
# 收 _apply_counter_hit / _apply_material_hit 两助手(分支本体逐字保留在助手内)。
|
||||
|
||||
@@ -32,6 +33,27 @@ func run(ri: int) -> void:
|
||||
_round_end_apply(ri)
|
||||
|
||||
|
||||
# 成功刷新事实发射+施加(迭代 46 卡A T4——BattleRefreshFlow 装新板后单点调用,每成功刷新恰一次;
|
||||
# 拒绝刷新零事实〔C2〕。命中消费三面:counter_add→既有单点落账〔蓄力周转/石层缓冲/坑道小憩 计数〕;
|
||||
# grant_gp→战斗内缓冲 escrow 入账+fx_grant_gp 行〔矿道红利——不绑清图奖励,C7〕;
|
||||
# open_ticket channel=blast→崩壁冲击群伤单延迟入队〔案C 同队列,战斗段末按存活目标结账〕)
|
||||
func apply_board_refreshed(ri: int) -> void:
|
||||
var br_hits := fx_host.fx_emit("board_refreshed", {"round": ri})
|
||||
for h in br_hits:
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
_apply_counter_hit(h)
|
||||
for h in br_hits:
|
||||
if String(h.get("kind", "")) == "grant_gp":
|
||||
reward_grant.escrow += int(h.get("value", 0))
|
||||
oc._emit_row({"type": "effect", "source_class": "fx_grant_gp", "origin_cell": Vector2i(-1, -1)}, ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": reward_grant.escrow})
|
||||
for h in br_hits:
|
||||
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast":
|
||||
var bb_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_board_refresh", "崩壁冲击",
|
||||
{"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE,
|
||||
"ri": ri, "origin_cell": Vector2i(-1, -1)})
|
||||
reward_grant.defer_ticket(bb_t)
|
||||
|
||||
|
||||
# 开格/踩雷兑付批(轮 facts_reveals 资源事件序:safe 正向乘区兑付 / mine_hit 减半负向兑付)
|
||||
func _reveal_batch(ri: int) -> void:
|
||||
for evt in oc._facts.reveals:
|
||||
@@ -167,37 +189,22 @@ func _terrain_batch(ri: int) -> void:
|
||||
if String(vrow.get("effect", "")) != "flat_damage":
|
||||
continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面)
|
||||
# 地形变异伤害单据——无类型必中,下限保底在控制器管线内
|
||||
# damage_taken 发射在单据开立前:damage_reduce〔背水——hp_ratio when
|
||||
# 面〕先收集再按减后值开单;counter_add〔保险丝/石肤〕+ ac_bonus〔石肤〕照常施加;
|
||||
# grant_material〔荆棘/地脉〕移到单据提交后(兑付值序一致——收益不因减伤变化)
|
||||
# damage_taken 统一发射(迭代 46 卡A T2——地形路径并入全局消费族 _emit_hero_damage_taken:
|
||||
# 发射在单据开立前、减后值开单,组合消费含 shield〔全局受伤语义〕;
|
||||
# hero_damage_consumed 标记入单据 context——战斗段末 _resolve_board_flat_hero 跳过重复发射;
|
||||
# grant_material〔荆棘/地脉〕移到单据提交后(兑付值序一致——收益不因减伤变化))
|
||||
var dt_amount := int(vrow.get("effect_value", 0))
|
||||
var dt_hits := fx_host.fx_emit("damage_taken", {"kind": "terrain", "amount": dt_amount,
|
||||
"source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid),
|
||||
"hp_ratio": int(round(100.0 * float(int(oc._enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(oc._enemy_system.hero_unit().get("max_hp", 1))))))})
|
||||
dt_amount = _terrain_damage_reduce(dt_hits, dt_amount)
|
||||
var dt_res := oc._damage._emit_hero_damage_taken(dt_amount, "terrain", String(vid), evt.cell, ri)
|
||||
dt_amount = int(dt_res["total"])
|
||||
var vt := oc._damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)),
|
||||
{"channel": "board", "base_value": dt_amount, "target_rule": DamageController.TARGET_HERO,
|
||||
"ri": ri, "origin_cell": evt.cell, "context": {"variant_id": String(vid)}})
|
||||
"ri": ri, "origin_cell": evt.cell, "context": {"variant_id": String(vid), "hero_damage_consumed": true}})
|
||||
reward_grant.submit(vt, false)
|
||||
for h in dt_hits:
|
||||
for h in (dt_res["hits"] as Array):
|
||||
if String(h.get("kind", "")) == "grant_material":
|
||||
_apply_material_hit(h, evt.cell, ri)
|
||||
|
||||
|
||||
# damage_taken 施加面(damage_reduce 减量收集〔背水——定值〕/ac_bonus〔石肤〕/counter_add〔保险丝/石肤〕;
|
||||
# 返回减后 amount 交开单消费)
|
||||
func _terrain_damage_reduce(dt_hits: Array, dt_amount: int) -> int:
|
||||
for h in dt_hits:
|
||||
match String(h.get("kind", "")):
|
||||
"damage_reduce":
|
||||
dt_amount = maxi(0, dt_amount - int(h.get("value", 0)))
|
||||
"ac_bonus":
|
||||
fx_host.apply_ac_bonus(h)
|
||||
"counter_add":
|
||||
_apply_counter_hit(h)
|
||||
return dt_amount
|
||||
|
||||
|
||||
# 封印判定批+战封印军规开单段(封印流:seal_judged 批后统一开单:轮末每面封印命中对最前存活敌
|
||||
# 结算 board_typed 必中伤害〔复用零新配方;「封印所在巢穴敌」简化为 default_target 最前存活敌——
|
||||
# 台账披露待追认〕;完美布防=本轮封印批全中(hit>0 且 miss==0)时封印伤 ×mult——全对推理奖励)。
|
||||
@@ -259,6 +266,7 @@ func _seal_strike_value(fs_fx: Array, fs_hits: int, fs_misses: int) -> Dictionar
|
||||
func _round_end_apply(ri: int) -> void:
|
||||
var re_fx := fx_host.fx_emit("round_end", {"round": ri})
|
||||
_round_end_blast_tickets(re_fx, ri)
|
||||
_round_end_heal_value_counter(re_fx, ri) # 迭代 46 卡A T5——坑道小憩恢复兑付(先于清零行消费层值,声明序=heal 行在 counter set 行前)
|
||||
for h in re_fx:
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
_apply_counter_hit(h)
|
||||
@@ -278,6 +286,24 @@ func _round_end_apply(ri: int) -> void:
|
||||
oc._damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界)
|
||||
|
||||
|
||||
# round_end heal value_counter 兑付(迭代 46 卡A T5——坑道小憩:层值=本轮成功刷新累计恢复点,
|
||||
# units.heal 封顶、满血发未生效行;层清零由同事件 counter_add set 行承载〔声明序在后〕——本面
|
||||
# 先于 counter_add 遍历消费层值即保序。行源=成功刷新事实族(无棋盘格——fx_board_refresh 豁免类))
|
||||
func _round_end_heal_value_counter(re_fx: Array, ri: int) -> void:
|
||||
for h in re_fx:
|
||||
if String(h.get("kind", "")) != "heal" or String(h.get("value_counter", "")) == "":
|
||||
continue
|
||||
var layers := EffectEngine.counter(fx_host.counters_ctx, String(h.get("value_counter", "")))
|
||||
if layers <= 0:
|
||||
continue
|
||||
var hu: Dictionary[Variant, Variant] = oc._enemy_system.hero_unit()
|
||||
if int(hu.get("hp", 0)) >= int(hu.get("max_hp", 1)):
|
||||
oc._emit_row({"type": "reward_ineffective", "source_class": "fx_board_refresh", "origin_cell": Vector2i(-1, -1)}, ri, "坑道小憩 未生效(满血)", 0, {"batch_key": String(h.get("value_counter", ""))})
|
||||
continue
|
||||
var healed: int = oc._enemy_system.heal(hu, layers)
|
||||
oc._emit_row({"type": "heal", "source_class": "fx_board_refresh", "origin_cell": Vector2i(-1, -1)}, ri, "坑道小憩 回复", healed, {"target": "hero"})
|
||||
|
||||
|
||||
# round_end blast 通道反打单(per-counter 积层读数×value=反打总量;source_class 用中性名,
|
||||
# per-counter 反打通道保留泛用面;案C 暂存战斗段末兑付)
|
||||
func _round_end_blast_tickets(re_fx: Array, ri: int) -> void:
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
## 营地场景单层规格(camp_scene_layout.tres 行件):层键+贴图矩形+命中矩形+命中键。
|
||||
## z 序=表内数组序(视图子节点序同源派生);坐标一律终稿画布 1280×800 像素坐标。
|
||||
class_name CampSceneLayerSpec
|
||||
extends Resource
|
||||
|
||||
@export var key: StringName = &"" # 层键(对齐 UiKit.TEX_CAMP_* 常量后缀)
|
||||
@export var texture_rect: Rect2 = Rect2() # 贴图矩形(1:1 像素贴图落位)
|
||||
@export var hotspot: Rect2 = Rect2() # 命中矩形(零矩形=不可点层——bg/wall/tent)
|
||||
@export var hotspot_key: StringName = &"" # object_pressed 载荷键(可点层必填)
|
||||
|
||||
|
||||
func is_clickable() -> bool:
|
||||
return hotspot.size.x > 0.0 and hotspot.size.y > 0.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://bll8d0s1nmarf
|
||||
@@ -0,0 +1,67 @@
|
||||
## 营地场景布局表 schema(camp_scene_layout.tres):10 层贴图矩形/命中矩形/门洞缺口/围墙带包络/顶栏禁区。
|
||||
## 坐标数值唯一事实源=数据行本表——视图、探针、单测一律读表取值,禁另写坐标字面量。
|
||||
class_name CampSceneLayoutTable
|
||||
extends Resource
|
||||
|
||||
@export var canvas := Vector2(1280, 800) # 终稿画布(工程档同幅面)
|
||||
@export var hud_band_h := 48 # 顶栏禁区高度(命中不侵)
|
||||
@export var gate_gap := Vector2i(520, 760) # 门洞缺口 x 范围(透出背景层小路,禁填死)
|
||||
@export var wall_band := Rect2(0, 240, 1280, 70) # 围墙带包络(门柱顶 240—桩底 310;围墙贴图须落在其内)
|
||||
@export var layers: Array[CampSceneLayerSpec] = []
|
||||
|
||||
|
||||
## 加载期 fail-fast 校验(ConfigDB 装载即查,问题非空=数据致命)。
|
||||
## 校验面按件拆分:层身份与贴图矩形/命中区边界/命中区互斥/围墙带包络。
|
||||
func validate() -> Array[String]:
|
||||
var problems: Array[String] = []
|
||||
var clickables: Array[CampSceneLayerSpec] = _collect_clickables(problems)
|
||||
_check_hotspot_bounds(clickables, problems)
|
||||
_check_hotspot_disjoint(clickables, problems)
|
||||
_check_wall_band(problems)
|
||||
return problems
|
||||
|
||||
|
||||
func _collect_clickables(problems: Array[String]) -> Array[CampSceneLayerSpec]:
|
||||
var seen: Dictionary[StringName, bool] = {}
|
||||
var clickables: Array[CampSceneLayerSpec] = []
|
||||
for layer in layers:
|
||||
if layer.key == &"":
|
||||
problems.append("层键为空")
|
||||
continue
|
||||
if seen.has(layer.key):
|
||||
problems.append("层键重复: %s" % layer.key)
|
||||
seen[layer.key] = true
|
||||
if _out_of_canvas(layer.texture_rect):
|
||||
problems.append("贴图矩形越画面: %s %s" % [layer.key, layer.texture_rect])
|
||||
if layer.is_clickable():
|
||||
if layer.hotspot_key == &"":
|
||||
problems.append("可点层缺命中键: %s" % layer.key)
|
||||
clickables.append(layer)
|
||||
elif layer.hotspot_key != &"":
|
||||
problems.append("不可点层带命中键: %s" % layer.key)
|
||||
return clickables
|
||||
|
||||
|
||||
func _check_hotspot_bounds(clickables: Array[CampSceneLayerSpec], problems: Array[String]) -> void:
|
||||
for h in clickables:
|
||||
if h.hotspot.position.x < 0.0 or h.hotspot.position.y < hud_band_h \
|
||||
or _out_of_canvas(h.hotspot):
|
||||
problems.append("命中区越画面或侵顶栏禁区: %s %s" % [h.key, h.hotspot])
|
||||
|
||||
|
||||
func _check_hotspot_disjoint(clickables: Array[CampSceneLayerSpec], problems: Array[String]) -> void:
|
||||
for i in clickables.size():
|
||||
for j in range(i + 1, clickables.size()):
|
||||
if clickables[i].hotspot.intersects(clickables[j].hotspot):
|
||||
problems.append("命中区相交: %s × %s" % [clickables[i].key, clickables[j].key])
|
||||
|
||||
|
||||
func _check_wall_band(problems: Array[String]) -> void:
|
||||
for layer in layers:
|
||||
if layer.key == &"wall" and not wall_band.encloses(layer.texture_rect):
|
||||
problems.append("围墙贴图越围墙带包络: %s" % layer.texture_rect)
|
||||
|
||||
|
||||
func _out_of_canvas(rect: Rect2) -> bool:
|
||||
return rect.position.x < 0.0 or rect.position.y < 0.0 \
|
||||
or rect.end.x > canvas.x or rect.end.y > canvas.y
|
||||
@@ -0,0 +1 @@
|
||||
uid://c6p8wyvcc5upp
|
||||
@@ -0,0 +1,82 @@
|
||||
[gd_resource type="Resource" script_class="CampSceneLayoutTable" load_steps=13 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/camp_scene_layout_table.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://data/schemas/camp_scene_layer_spec.gd" id="2"]
|
||||
|
||||
[sub_resource type="Resource" id="1"]
|
||||
script = ExtResource("2")
|
||||
key = &"bg"
|
||||
texture_rect = Rect2(0, 0, 1280, 800)
|
||||
hotspot = Rect2(0, 0, 0, 0)
|
||||
hotspot_key = &""
|
||||
|
||||
[sub_resource type="Resource" id="2"]
|
||||
script = ExtResource("2")
|
||||
key = &"wall"
|
||||
texture_rect = Rect2(0, 261, 1280, 49)
|
||||
hotspot = Rect2(0, 0, 0, 0)
|
||||
hotspot_key = &""
|
||||
|
||||
[sub_resource type="Resource" id="3"]
|
||||
script = ExtResource("2")
|
||||
key = &"board"
|
||||
texture_rect = Rect2(845, 268, 130, 121)
|
||||
hotspot = Rect2(845, 268, 130, 140)
|
||||
hotspot_key = &"board"
|
||||
|
||||
[sub_resource type="Resource" id="4"]
|
||||
script = ExtResource("2")
|
||||
key = &"gate"
|
||||
texture_rect = Rect2(520, 250, 240, 211)
|
||||
hotspot = Rect2(520, 250, 240, 220)
|
||||
hotspot_key = &"gate"
|
||||
|
||||
[sub_resource type="Resource" id="5"]
|
||||
script = ExtResource("2")
|
||||
key = &"dummy"
|
||||
texture_rect = Rect2(340, 520, 120, 180)
|
||||
hotspot = Rect2(340, 520, 120, 180)
|
||||
hotspot_key = &"dummy"
|
||||
|
||||
[sub_resource type="Resource" id="6"]
|
||||
script = ExtResource("2")
|
||||
key = &"forge"
|
||||
texture_rect = Rect2(40, 467, 240, 193)
|
||||
hotspot = Rect2(40, 420, 240, 240)
|
||||
hotspot_key = &"forge"
|
||||
|
||||
[sub_resource type="Resource" id="7"]
|
||||
script = ExtResource("2")
|
||||
key = &"tent"
|
||||
texture_rect = Rect2(982, 520, 251, 170)
|
||||
hotspot = Rect2(0, 0, 0, 0)
|
||||
hotspot_key = &""
|
||||
|
||||
[sub_resource type="Resource" id="8"]
|
||||
script = ExtResource("2")
|
||||
key = &"chest"
|
||||
texture_rect = Rect2(1097, 700, 106, 90)
|
||||
hotspot = Rect2(1085, 700, 130, 90)
|
||||
hotspot_key = &"chest"
|
||||
|
||||
[sub_resource type="Resource" id="9"]
|
||||
script = ExtResource("2")
|
||||
key = &"notebook"
|
||||
texture_rect = Rect2(818, 704, 100, 66)
|
||||
hotspot = Rect2(818, 690, 100, 80)
|
||||
hotspot_key = &"notebook"
|
||||
|
||||
[sub_resource type="Resource" id="10"]
|
||||
script = ExtResource("2")
|
||||
key = &"campfire"
|
||||
texture_rect = Rect2(560, 571, 160, 149)
|
||||
hotspot = Rect2(560, 640, 160, 80)
|
||||
hotspot_key = &"campfire"
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
canvas = Vector2(1280, 800)
|
||||
hud_band_h = 48
|
||||
gate_gap = Vector2i(520, 760)
|
||||
wall_band = Rect2(0, 240, 1280, 70)
|
||||
layers = [SubResource("1"), SubResource("2"), SubResource("3"), SubResource("4"), SubResource("5"), SubResource("6"), SubResource("7"), SubResource("8"), SubResource("9"), SubResource("10")]
|
||||
@@ -869,5 +869,215 @@ rows = [
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"feat_id": "vein_reserve",
|
||||
"display_name": "矿脉储能",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "挖格储能:每次受理的付费挖格 +1 储备点(RAP)",
|
||||
"triggers": [
|
||||
{
|
||||
"on": "dig_paid",
|
||||
"do": {
|
||||
"kind": "grant_reserve_ap",
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"feat_id": "easy_swap",
|
||||
"display_name": "省力周转",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "刷新半价:弃图刷新成本为基线 50%",
|
||||
"triggers": [
|
||||
{
|
||||
"on": "refresh_request",
|
||||
"do": {
|
||||
"kind": "apply_discount",
|
||||
"mode": "refresh",
|
||||
"cost_mult": 50
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"feat_id": "wall_blast",
|
||||
"display_name": "崩壁冲击",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "刷后群伤:成功刷新后全体存活敌各受 2 点",
|
||||
"triggers": [
|
||||
{
|
||||
"on": "board_refreshed",
|
||||
"do": {
|
||||
"kind": "open_ticket",
|
||||
"channel": "blast",
|
||||
"value": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"feat_id": "mine_profit",
|
||||
"display_name": "矿道红利",
|
||||
"rarity": "rare",
|
||||
"cap": 1,
|
||||
"desc": "刷后GP:每次成功刷新固定 +1 GP",
|
||||
"triggers": [
|
||||
{
|
||||
"on": "board_refreshed",
|
||||
"do": {
|
||||
"kind": "grant_gp",
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"feat_id": "stone_mitigation",
|
||||
"display_name": "石层缓冲",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "当轮减伤:本轮成功刷新后,每次受伤固定 −2(多刷不叠加)",
|
||||
"triggers": [
|
||||
{
|
||||
"on": "board_refreshed",
|
||||
"do": {
|
||||
"kind": "counter_add",
|
||||
"counter_id": "round_swap_guard",
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"on": "damage_taken",
|
||||
"when": [
|
||||
{
|
||||
"fact": "counter",
|
||||
"id": "round_swap_guard",
|
||||
"op": "gte",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"do": {
|
||||
"kind": "damage_reduce",
|
||||
"value": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"feat_id": "charge_rotation",
|
||||
"display_name": "蓄力周转",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "刷后蓄力:每次刷新 +1 层(上限 3),下一次攻击消费全部层数",
|
||||
"triggers": [
|
||||
{
|
||||
"on": "board_refreshed",
|
||||
"when": [
|
||||
{
|
||||
"fact": "counter",
|
||||
"id": "swap_charge_layers",
|
||||
"op": "lt",
|
||||
"value": 3
|
||||
}
|
||||
],
|
||||
"do": {
|
||||
"kind": "counter_add",
|
||||
"counter_id": "swap_charge_layers",
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"on": "attack_commit",
|
||||
"when": [
|
||||
{
|
||||
"fact": "counter",
|
||||
"id": "swap_charge_layers",
|
||||
"op": "gte",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"do": {
|
||||
"kind": "attack_bonus",
|
||||
"value_counter": "swap_charge_layers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"on": "attack_commit",
|
||||
"when": [
|
||||
{
|
||||
"fact": "counter",
|
||||
"id": "swap_charge_layers",
|
||||
"op": "gte",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"do": {
|
||||
"kind": "counter_add",
|
||||
"counter_id": "swap_charge_layers",
|
||||
"set": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"feat_id": "tunnel_rest",
|
||||
"display_name": "坑道小憩",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "当轮恢复:每次刷新累计 1 点(上限 3),本轮轮末合计回复",
|
||||
"triggers": [
|
||||
{
|
||||
"on": "board_refreshed",
|
||||
"when": [
|
||||
{
|
||||
"fact": "counter",
|
||||
"id": "round_swap_rest",
|
||||
"op": "lt",
|
||||
"value": 3
|
||||
}
|
||||
],
|
||||
"do": {
|
||||
"kind": "counter_add",
|
||||
"counter_id": "round_swap_rest",
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"on": "round_end",
|
||||
"when": [
|
||||
{
|
||||
"fact": "counter",
|
||||
"id": "round_swap_rest",
|
||||
"op": "gte",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"do": {
|
||||
"kind": "heal",
|
||||
"value_counter": "round_swap_rest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"on": "round_end",
|
||||
"when": [
|
||||
{
|
||||
"fact": "counter",
|
||||
"id": "round_swap_rest",
|
||||
"op": "gte",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"do": {
|
||||
"kind": "counter_add",
|
||||
"counter_id": "round_swap_rest",
|
||||
"set": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -7,7 +7,7 @@ extends RefCounted
|
||||
## 三束按域收拢(u21——同 u20 形;views=视图句柄/getters=查询 Callable 活引用/flow=编排 Callable):
|
||||
## 原 12 参长签名收 4 参——字段即原构造参数逐一同名迁移,键面=消费方改址合同。
|
||||
var db
|
||||
var views: Dictionary = {} # {camp_overlay, shop_overlay, toast, start_overlay, logger} # type-hint: ignore(三束异构混载——视图句柄/查询 Callable/编排 Callable)
|
||||
var views: Dictionary = {} # {camp_scene, forge_view, treasure_view, tree_view, codex_view, shop_overlay, toast, start_overlay, save_select_overlay, logger, refresh_bus} # type-hint: ignore(三束异构混载——视图句柄/刷新单口/查询 Callable/编排 Callable;卡④ T3 营地域改挂)
|
||||
var getters: Dictionary = {} # {meta_get, save_get, runner_get} # type-hint: ignore(同上——Callable 束)
|
||||
var flow: Dictionary = {} # {open_modal, close_modal, refresh_presentations} # type-hint: ignore(同上——Callable 束)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ extends RefCounted
|
||||
var db
|
||||
var _meta_get: Callable # () → 元进度账本现值(组合根 lambda——探针 isolate_save 换注面)
|
||||
var _save_get: Callable # () → 存档承载现值(同上——营地 recent 摘要读数面)
|
||||
var camp_overlay: CampOverlay
|
||||
var views: Dictionary = {} # {camp_scene, forge_view, treasure_view, tree_view, codex_view, shop_overlay, toast, start_overlay, save_select_overlay, logger, refresh_bus} # type-hint: ignore(三束异构混载——视图句柄/单口件;营地域句柄经 ctx.views 取用——组合根 ui.modals.camp 子容器同源)
|
||||
var shop_overlay: ShopOverlay
|
||||
var toast: ToastStack
|
||||
var start_overlay: Control # 开始层互斥面(开合仿设置面先例——非模态且树序在后,规范 2.3)
|
||||
@@ -23,7 +23,7 @@ func _init(ctx: CampFlowContext) -> void:
|
||||
db = ctx.db
|
||||
_meta_get = ctx.getters.meta_get
|
||||
_save_get = ctx.getters.save_get
|
||||
camp_overlay = ctx.views.camp_overlay
|
||||
views = ctx.views # 营地域五目标+刷新单口+关层收闭面(卡④ T3——CampOverlay 退役后投影目标改线)
|
||||
shop_overlay = ctx.views.shop_overlay
|
||||
toast = ctx.views.toast
|
||||
start_overlay = ctx.views.start_overlay
|
||||
@@ -34,21 +34,54 @@ func _init(ctx: CampFlowContext) -> void:
|
||||
_refresh_presentations = ctx.flow.refresh_presentations
|
||||
|
||||
|
||||
# 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后,
|
||||
# 规范 2.3 遮挡纪律;打开时先刷条件解锁——达标未解锁就地入账)
|
||||
# 打开营地场景根(进档落点——卡④ T3 改挂:开层=交互式场景图,四子界面经场景点选进)
|
||||
func open_camp() -> void:
|
||||
var meta = _meta_get.call()
|
||||
if meta == null:
|
||||
return
|
||||
meta.refresh_treasure_unlocks() # 条件解锁刷新
|
||||
start_overlay.visible = false
|
||||
_open_modal.call(camp_overlay) # 模态栈单口(登记层开合一律走栈)
|
||||
camp_overlay.open_camp(camp_data())
|
||||
_open_modal.call(views.camp_scene) # 模态栈单口(场景根开——选档层被栈遮挡挂起)
|
||||
views.refresh_bus.refresh(camp_data()) # 进档动线尾显式刷新一闸(卡⑤ 契约 5 时机②——HUD 读数重读)
|
||||
_refresh_presentations.call()
|
||||
|
||||
|
||||
# ---- 营地四子界面开层(场景点选路由分派——组合根 _on_camp_object_pressed;数据投喂=快照投影)----
|
||||
|
||||
|
||||
func open_camp_forge() -> void:
|
||||
_open_modal.call(views.forge_view)
|
||||
views.forge_view.open_camp(camp_data())
|
||||
|
||||
|
||||
func open_camp_treasure() -> void:
|
||||
_open_modal.call(views.treasure_view)
|
||||
views.treasure_view.open_camp(camp_data())
|
||||
|
||||
|
||||
func open_camp_tree() -> void:
|
||||
_open_modal.call(views.tree_view)
|
||||
views.tree_view.open_camp(camp_data())
|
||||
|
||||
|
||||
func open_camp_codex() -> void:
|
||||
_open_modal.call(views.codex_view)
|
||||
views.codex_view.open_camp(camp_data())
|
||||
|
||||
|
||||
# 子界面返回钮(视图只发 closed——关层归本口;场景根留底复现,开始层不动)
|
||||
func on_camp_sub_closed(view: Control) -> void:
|
||||
_close_modal.call(view)
|
||||
_refresh_presentations.call()
|
||||
|
||||
|
||||
# 关闭整个营地(关层收闭——卡④ T3:营地层与选档层同收+开始层恢复,禁只关营地留下选档回闪;
|
||||
# 触发面=篝火/ESC 回主菜单动线,卡④ T4 接线)
|
||||
func on_camp_closed() -> void:
|
||||
_close_modal.call(camp_overlay) # 模态栈单口
|
||||
for v: Control in [views.camp_scene, views.forge_view, views.treasure_view, views.tree_view, views.codex_view]:
|
||||
if v.visible:
|
||||
_close_modal.call(v)
|
||||
_close_modal.call(views.save_select_overlay)
|
||||
start_overlay.visible = true
|
||||
_refresh_presentations.call() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面)
|
||||
|
||||
@@ -79,7 +112,7 @@ func on_camp_buy(attr: String) -> void:
|
||||
if not meta.buy_treasure_unlock(attr):
|
||||
toast.show_toast("购买被拒(余额不足或不可购买)", "warn")
|
||||
return
|
||||
camp_overlay.refresh_camp(camp_data())
|
||||
views.refresh_bus.refresh(camp_data())
|
||||
_refresh_presentations.call()
|
||||
return
|
||||
# Y 刷新升级行购买(双入口之一——树上 ks_refresh 同账;
|
||||
@@ -87,7 +120,7 @@ func on_camp_buy(attr: String) -> void:
|
||||
if not meta.buy_attune_refresh():
|
||||
toast.show_toast("购买被拒", "warn")
|
||||
return
|
||||
camp_overlay.refresh_camp(camp_data())
|
||||
views.refresh_bus.refresh(camp_data())
|
||||
_refresh_presentations.call()
|
||||
|
||||
|
||||
@@ -99,7 +132,7 @@ func on_camp_attune_toggle(item_id: String) -> void:
|
||||
if not meta.toggle_attunement(item_id):
|
||||
toast.show_toast("同调位已满——先卸下再装配", "warn")
|
||||
return
|
||||
camp_overlay.refresh_camp(camp_data())
|
||||
views.refresh_bus.refresh(camp_data())
|
||||
|
||||
|
||||
# 天赋树购买(写口唯一归 MetaProgression.tree_buy/tree_buy_fork;Fork 节点购=激活侧)
|
||||
@@ -111,7 +144,7 @@ func on_camp_tree_buy(node_id: String) -> void:
|
||||
if not ok:
|
||||
toast.show_toast("购买被拒——前置/行门槛/余额不足", "warn")
|
||||
return
|
||||
camp_overlay.refresh_camp(camp_data())
|
||||
views.refresh_bus.refresh(camp_data())
|
||||
_refresh_presentations.call()
|
||||
|
||||
|
||||
@@ -123,7 +156,7 @@ func on_camp_fork_toggle(group: String, side: String) -> void:
|
||||
if not meta.tree_toggle_fork(group, side):
|
||||
toast.show_toast("切换被拒——未激活组请先购买", "warn")
|
||||
return
|
||||
camp_overlay.refresh_camp(camp_data())
|
||||
views.refresh_bus.refresh(camp_data())
|
||||
_refresh_presentations.call()
|
||||
|
||||
|
||||
@@ -136,7 +169,7 @@ func on_camp_forge_buy(route_id: String) -> void:
|
||||
if not meta.forge_buy_level(route_id):
|
||||
toast.show_toast("锻造被拒——钥石不足或路线不可锻造", "warn")
|
||||
return
|
||||
camp_overlay.refresh_camp(camp_data())
|
||||
views.refresh_bus.refresh(camp_data())
|
||||
_refresh_presentations.call()
|
||||
|
||||
|
||||
@@ -148,7 +181,7 @@ func on_camp_forge_unlock(route_id: String) -> void:
|
||||
if not meta.forge_buy_unlock(route_id):
|
||||
toast.show_toast("解锁被拒——钥石不足或已解锁", "warn")
|
||||
return
|
||||
camp_overlay.refresh_camp(camp_data())
|
||||
views.refresh_bus.refresh(camp_data())
|
||||
_refresh_presentations.call()
|
||||
|
||||
|
||||
@@ -162,7 +195,7 @@ func on_camp_forge_select(route_id: String) -> void:
|
||||
if not ok:
|
||||
toast.show_toast("切换被拒——路线未解锁", "warn")
|
||||
return
|
||||
camp_overlay.refresh_camp(camp_data())
|
||||
views.refresh_bus.refresh(camp_data())
|
||||
_refresh_presentations.call()
|
||||
|
||||
|
||||
|
||||
@@ -123,19 +123,37 @@ func _start_adventure_from_menu() -> void:
|
||||
|
||||
|
||||
# 确认弹窗分派(卡③ 契约 3):按 _confirm_consumer 路由——"delete_slot"=卡③ 删档确认
|
||||
#(确认才删、取消只关窗——裁决 3)。"new_adventure" 消费方已随卡② 主菜单确认链摘除退役
|
||||
#(mn6/V5——主菜单零确认残留),分派机制保留(卡④ 到位接入第三消费方不返工)。
|
||||
#(确认才删、取消只关窗——裁决 3)。"gate"/"gate_new"=卡④ 大门两分支消费方(有/无断点),
|
||||
# "campfire_leave"=篝火二次确认(卡④ T4 接入——分派机制扩展第三/第四消费方)。
|
||||
func _on_confirm_accepted() -> void:
|
||||
ui.modals.flow.close(ui.modals.confirm_dialog) # 模态栈单口
|
||||
match _confirm_consumer:
|
||||
"delete_slot":
|
||||
save_manager.slots.delete_slot(_confirm_consumer_slot)
|
||||
ui.modals.save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态)
|
||||
"gate":
|
||||
_camp_pres.on_camp_closed() # 冒险发起后营地场景屏隐藏(选档层同收——卡④ T4 关层收闭)
|
||||
_resume_from_menu() # 继续上一段冒险(现役续玩编排同源——卡④ 契约 4)
|
||||
"gate_new":
|
||||
_camp_pres.on_camp_closed()
|
||||
_start_adventure() # 开始新冒险(现役开局编排同源)
|
||||
"campfire_leave":
|
||||
_camp_pres.on_camp_closed() # 篝火确认离场=营地与选档层同收+开始层恢复(回主菜单)
|
||||
_confirm_consumer = ""
|
||||
|
||||
|
||||
# 次确认分派(三钮形态中钮——大门「开始新冒险」出口;卡④ T4)
|
||||
func _on_confirm_secondary() -> void:
|
||||
ui.modals.flow.close(ui.modals.confirm_dialog) # 模态栈单口
|
||||
match _confirm_consumer:
|
||||
"gate":
|
||||
_camp_pres.on_camp_closed()
|
||||
_start_adventure()
|
||||
_confirm_consumer = ""
|
||||
|
||||
|
||||
func _on_confirm_declined() -> void:
|
||||
ui.modals.flow.close(ui.modals.confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删);关侧恢复归 ModalFlow
|
||||
ui.modals.flow.close(ui.modals.confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删/卡④:退回营地);关侧恢复归 ModalFlow
|
||||
_confirm_consumer = ""
|
||||
|
||||
|
||||
@@ -500,12 +518,35 @@ func _sync_status_entry() -> void:
|
||||
MainOverlayFlow._sync_status_entry(self)
|
||||
|
||||
|
||||
# ---- 探针缝保缝(click/ui_probe 营地段直呼组合根面;实现归 camp_shop_presenter.gd)----
|
||||
# ---- 探针缝保缝(click/ui_probe 营地段直呼组合根面;实现归 camp_shop_presenter.gd / main_overlay_flow.gd)----
|
||||
|
||||
func _on_camp_closed() -> void:
|
||||
MainOverlayFlow._on_camp_closed(self)
|
||||
|
||||
|
||||
func _on_camp_object_pressed(key: StringName) -> void:
|
||||
MainOverlayFlow._on_camp_object_pressed(self, key) # 场景点选路由(卡④ T3——薄委托,实现归覆盖层流程承接件)
|
||||
|
||||
|
||||
# Esc 输入入口(产品码首个键盘入口——卡⑤ T1 契约 9 接线定稿:事件驱动 _unhandled_input、
|
||||
# KEY_ESCAPE 键值判定配 physical_keycode、pressed 且非 echo、禁 Input 单例轮询、不挂 ui_cancel)
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and not event.echo and (event.keycode == KEY_ESCAPE or event.physical_keycode == KEY_ESCAPE):
|
||||
MainEscFlow.on_esc_input(self)
|
||||
|
||||
|
||||
func _on_esc_return() -> void:
|
||||
MainEscFlow._on_return_to_menu(self)
|
||||
|
||||
|
||||
func _on_esc_settings() -> void:
|
||||
MainEscFlow._on_settings(self)
|
||||
|
||||
|
||||
func _on_esc_quit() -> void:
|
||||
MainEscFlow._on_quit(self)
|
||||
|
||||
|
||||
func _camp_data() -> Dictionary:
|
||||
return MainOverlayFlow._camp_data(self)
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
## 组合根 Esc 流程承接件(产品码首个键盘输入入口——main._unhandled_input 同名薄委托落地):
|
||||
## Esc 四态路由(契约 9 判据单点=ModalStack 登记册现开集合,禁手枚名单)+三出口动线。
|
||||
## 静态承接件零成员态(仿 main_overlay_flow 形态——main 侧留同名薄委托,探针 main.X 路径零改动)。
|
||||
class_name MainEscFlow
|
||||
extends RefCounted
|
||||
|
||||
|
||||
## 营地语境判据(Q1 甲——场景根 visible;子界面开态抑制由契约 9① 现开集合承载,不并入本谓词)
|
||||
static func is_camp_context(root: Variant) -> bool:
|
||||
return root.ui.modals.camp.scene != null and root.ui.modals.camp.scene.visible
|
||||
|
||||
|
||||
## Esc 四态优先序(契约 9——①先于②:设置开在浮窗之上态必须先命中「无反馈」,浮窗不隔层关闭):
|
||||
## ①除浮窗外任一登记模态开→无反馈(子界面开态/设置开在浮窗之上)→②浮窗开→close(同键单义切换,
|
||||
## Q2 甲)→③营地语境→open→④非营地→无反馈(选档/冒险/菜单语境 Esc 零响应——契约 2)
|
||||
static func on_esc_input(root: Variant) -> void:
|
||||
var esc: Control = root.ui.modals.camp.esc_overlay
|
||||
if _other_modal_open(root, esc):
|
||||
return # ① 除浮窗外任一登记模态开 → 无反馈
|
||||
if esc.visible:
|
||||
root.ui.modals.flow.close(esc) # ② 再按 Esc 关窗
|
||||
return
|
||||
if is_camp_context(root):
|
||||
root.ui.modals.flow.open(esc) # ③ 营地语境唤起
|
||||
# ④ 非营地语境 → 无反馈
|
||||
|
||||
|
||||
# 登记册现开集合查询(除浮窗与营地场景根本身——场景根=语境层非抑制对象;
|
||||
# 迭代登记册遍历=契约 9「判据单点」 sanctioned 读法,禁按层名手枚名单)
|
||||
static func _other_modal_open(root: Variant, esc: Control) -> bool:
|
||||
for m in root.ui.modals.flow.stack._list:
|
||||
if m != esc and m != root.ui.modals.camp.scene and is_instance_valid(m) and m.visible:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## 返回主菜单动线(契约 4):首步 close 浮窗(缺此步浮窗残留主菜单上方)→营地与选档层同收+开始层恢复
|
||||
static func _on_return_to_menu(root: Variant) -> void:
|
||||
root.ui.modals.flow.close(root.ui.modals.camp.esc_overlay)
|
||||
root._camp_pres.on_camp_closed() # 关层收闭单口(卡④ T3——营地层与选档层同收+开始层恢复)
|
||||
|
||||
|
||||
## 设置出口(浮窗登记在设置层之下——open 只隐上方层,浮窗由树序遮挡非属性隐藏;关后重现=栈语义)
|
||||
static func _on_settings(root: Variant) -> void:
|
||||
root.ui.modals.flow.open(root.ui.modals.settings_overlay)
|
||||
|
||||
|
||||
## 退出出口(_quit_game 原样复用——settings_store.save_settings+quit,零语义副作用)
|
||||
static func _on_quit(root: Variant) -> void:
|
||||
root._quit_game()
|
||||
@@ -0,0 +1 @@
|
||||
uid://caa868un7dnt4
|
||||
@@ -10,7 +10,11 @@ static func _open_settings(root: Variant) -> void:
|
||||
|
||||
static func _on_settings_closed(root: Variant) -> void:
|
||||
root.ui.modals.flow.close(root.ui.modals.settings_overlay) # 模态栈单口
|
||||
root.ui.screens.start_overlay.visible = true
|
||||
# 语境分支(卡⑤ 契约 3——谓词=camp_esc_overlay.visible,谁开的设置谁收):
|
||||
# 营地浮窗路径关设置回浮窗不回主菜单(栈语义:浮窗登记在下、由树序遮挡非属性隐藏);
|
||||
# 菜单路径保留开始层恢复硬副作用。
|
||||
if not root.ui.modals.camp.esc_overlay.visible:
|
||||
root.ui.screens.start_overlay.visible = true
|
||||
|
||||
|
||||
# ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)----
|
||||
@@ -40,7 +44,43 @@ static func _open_career(root: Variant) -> void:
|
||||
|
||||
static func _on_career_closed(root: Variant) -> void:
|
||||
root.ui.modals.flow.close(root.ui.modals.career_overlay) # 模态栈单口
|
||||
root.ui.screens.start_overlay.visible = true
|
||||
if not root.ui.modals.camp.scene.visible: # 营地语境关生涯不回开始层(笔记本入口——卡④ T3;栈已复现场景根)
|
||||
root.ui.screens.start_overlay.visible = true
|
||||
|
||||
|
||||
# ---- 营地场景点选路由(卡④ T3/T4——场景根 object_pressed 组合根分派)----
|
||||
# 大门两分支(裁决 5,Q5 甲):点大门永远先出确认不直接进冒险——有断点=三出口弹窗
|
||||
#(继续上一段冒险/开始新冒险/取消),无断点=是否开始新冒险(是/否);cancelled 恒=无变更退回营地。
|
||||
# 篝火回主菜单必须二次确认(camp.md v2.2 裁定);确认后营地层与选档层同收+开始层恢复(关层收闭)。
|
||||
|
||||
|
||||
static func _on_camp_object_pressed(root: Variant, key: StringName) -> void:
|
||||
match key:
|
||||
&"forge":
|
||||
root._camp_pres.open_camp_forge()
|
||||
&"chest":
|
||||
root._camp_pres.open_camp_treasure()
|
||||
&"dummy":
|
||||
root._camp_pres.open_camp_tree()
|
||||
&"board":
|
||||
root._camp_pres.open_camp_codex()
|
||||
&"notebook":
|
||||
_open_career(root) # 生涯页入口改挂帐篷旁笔记本(CareerOverlay 本体零改——卡④ T3)
|
||||
&"gate":
|
||||
_open_gate_confirm(root)
|
||||
&"campfire":
|
||||
root._confirm_consumer = "campfire_leave"
|
||||
root.ui.modals.confirm_dialog.open("确定离开营地回到主菜单?", "离开", "留下")
|
||||
|
||||
|
||||
static func _open_gate_confirm(root: Variant) -> void:
|
||||
if root.save_manager.has_breakpoint():
|
||||
root._confirm_consumer = "gate"
|
||||
root.ui.modals.confirm_dialog.open_three("有未完成的冒险——继续上一段冒险,还是开始新冒险?",
|
||||
"继续上一段冒险", "开始新冒险", "取消")
|
||||
return
|
||||
root._confirm_consumer = "gate_new"
|
||||
root.ui.modals.confirm_dialog.open("是否开始新冒险?", "是", "否")
|
||||
|
||||
|
||||
# 生涯页快照投影 —— 承接体见 HudPresenter.career_data(开层调用点直接改引)
|
||||
@@ -65,7 +105,7 @@ static func _on_save_select_closed(root: Variant) -> void:
|
||||
|
||||
|
||||
|
||||
# 空槽新档链(Q3 裁乙暂行落点=现役 CampOverlay;卡④ 重立卡后改挂场景图):卡① 创建=激活=白板落盘,
|
||||
# 空槽新档链(卡④ T3 换代:落点=营地场景根——CampOverlay 随四柜台拆解退役):卡① 创建=激活=白板落盘,
|
||||
# 落点开层。动线序(禁先关后开):先落点开层(开始层保持隐藏),选档层被模态栈遮挡隐藏、不关闭。
|
||||
static func _on_save_select_new(root: Variant, slot: int) -> void:
|
||||
root.save_manager.slots.create_slot(slot)
|
||||
@@ -73,7 +113,7 @@ static func _on_save_select_new(root: Variant, slot: int) -> void:
|
||||
|
||||
|
||||
|
||||
# 有档槽进档链(契约 8 进档即激活):卡① 激活+读盘(兜底/拒读链按槽成立)→暂行落点 CampOverlay。
|
||||
# 有档槽进档链(契约 8 进档即激活):卡① 激活+读盘(兜底/拒读链按槽成立)→落点=营地场景根(卡④ T3)。
|
||||
static func _on_save_select_load(root: Variant, slot: int) -> void:
|
||||
root.save_manager.slots.activate_slot(slot)
|
||||
root._camp_pres.open_camp()
|
||||
|
||||
@@ -83,7 +83,7 @@ static func _build_battle_wiring(root: Variant) -> void:
|
||||
|
||||
static func _build_overlays(root: Variant) -> void:
|
||||
# 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局
|
||||
# < 商店 < 营地 < 设置 < 状态面板(只读) < 生涯页(只读) < 确认弹窗 < 调试浮窗;
|
||||
# < 商店 < 营地场景根 < 四子界面 < 设置 < 状态面板(只读) < 生涯页(只读) < 确认弹窗 < 调试浮窗;
|
||||
# 关卡画面住冒险屏壳(树序在模态之下)——登记序仍夹在节点层与三选一层之间,遮挡语义零改动)
|
||||
var overlays: Dictionary[Variant, Variant] = UiAssembler.build_overlays(root, root.db)
|
||||
root.ui.modals.legend_overlay = overlays.legend_overlay
|
||||
@@ -92,8 +92,34 @@ static func _build_overlays(root: Variant) -> void:
|
||||
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
|
||||
# 营地场景根装配(布局表经 ConfigDB 单点注入——视图禁自载表路径;CampOverlay 随四柜台拆解退役)
|
||||
var camp_layout: CampSceneLayoutTable = root.db.presentation.camp_scene_layout
|
||||
root.ui.screens.camp_screen.camp_scene.setup_layers(camp_layout)
|
||||
root.ui.screens.camp_screen.camp_scene.bind_hotspots(camp_layout)
|
||||
root.ui.modals.camp.scene = root.ui.screens.camp_screen.camp_scene
|
||||
# 营地四子界面(独立全屏页——场景根留底形态;句柄落 ui.modals.camp 子容器——禁平铺回 ui_modals)
|
||||
var forge := ForgeView.new()
|
||||
root.add_child(forge)
|
||||
root.ui.modals.camp.forge = forge
|
||||
var treasure := TreasureView.new()
|
||||
root.add_child(treasure)
|
||||
root.ui.modals.camp.treasure = treasure
|
||||
var tree_view := TreeView.new()
|
||||
root.add_child(tree_view)
|
||||
root.ui.modals.camp.tree = tree_view
|
||||
var codex := CodexView.new()
|
||||
root.add_child(codex)
|
||||
root.ui.modals.camp.codex = codex
|
||||
# Esc 浮窗(卡⑤——登记于设置层之前的模态;三钮意图经 MainEscFlow 路由)
|
||||
var esc_overlay := CampEscOverlay.new()
|
||||
esc_overlay.setup_buttons()
|
||||
esc_overlay.bind_signals()
|
||||
root.add_child(esc_overlay)
|
||||
root.ui.modals.camp.esc_overlay = esc_overlay
|
||||
# 营地顶栏 HUD 栏(卡⑤——钥石余额常驻;挂卡④场景根顶栏预留位+入刷新单口目标册)
|
||||
var hud_band := CampHudBand.new()
|
||||
root.ui.modals.camp.scene.set_hud_band(hud_band)
|
||||
# 屏壳/子界面插位由 LAYER_CONTRACT 槽统一落位——禁散落 move_child
|
||||
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)
|
||||
@@ -158,9 +184,18 @@ static func _build_menu_wiring(root: Variant) -> void:
|
||||
root.ui.screens.debug_badge.pressed.connect(root._on_debug_badge_pressed)
|
||||
# 营地/商店投影(本体在 camp_shop_presenter.gd;本类只接线、探针缝保缝转发;
|
||||
# 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入)
|
||||
# 营地刷新单口(各写口尾→现开界面+场景根 HUD 一闸完成——卡④ T3;卡⑤ HUD band 随卡⑤挂册)
|
||||
var camp_bus := CampRefreshBus.new()
|
||||
camp_bus.bind_targets([root.ui.modals.camp.forge, root.ui.modals.camp.treasure,
|
||||
root.ui.modals.camp.tree, root.ui.modals.camp.codex,
|
||||
root.ui.screens.camp_screen.camp_scene.hud_band] as Array[Control])
|
||||
root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db,
|
||||
{"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},
|
||||
{"camp_scene": root.ui.modals.camp.scene, "forge_view": root.ui.modals.camp.forge,
|
||||
"treasure_view": root.ui.modals.camp.treasure, "tree_view": root.ui.modals.camp.tree,
|
||||
"codex_view": root.ui.modals.camp.codex, "shop_overlay": root.ui.modals.shop_overlay,
|
||||
"toast": root.ui.battle.board_zone.toast, "start_overlay": root.ui.screens.start_overlay,
|
||||
"save_select_overlay": root.ui.modals.save_select_overlay, "logger": root.logger,
|
||||
"refresh_bus": camp_bus},
|
||||
{"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.ui.modals.flow.open, "close_modal": root.ui.modals.flow.close,
|
||||
@@ -168,14 +203,19 @@ static func _build_menu_wiring(root: Variant) -> void:
|
||||
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)
|
||||
# 营地信号接线(CampOverlay 退役——四子界面意图信号+场景点选组合根路由+closed 经 bind 携视图)
|
||||
root.ui.modals.camp.forge.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy)
|
||||
root.ui.modals.camp.forge.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock)
|
||||
root.ui.modals.camp.forge.forge_select_requested.connect(root._camp_pres.on_camp_forge_select)
|
||||
root.ui.modals.camp.treasure.buy_requested.connect(root._camp_pres.on_camp_buy)
|
||||
root.ui.modals.camp.treasure.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle)
|
||||
root.ui.modals.camp.tree.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy)
|
||||
root.ui.modals.camp.tree.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle)
|
||||
root.ui.modals.camp.forge.closed.connect(root._camp_pres.on_camp_sub_closed.bind(root.ui.modals.camp.forge))
|
||||
root.ui.modals.camp.treasure.closed.connect(root._camp_pres.on_camp_sub_closed.bind(root.ui.modals.camp.treasure))
|
||||
root.ui.modals.camp.tree.closed.connect(root._camp_pres.on_camp_sub_closed.bind(root.ui.modals.camp.tree))
|
||||
root.ui.modals.camp.codex.closed.connect(root._camp_pres.on_camp_sub_closed.bind(root.ui.modals.camp.codex))
|
||||
root.ui.modals.camp.scene.object_pressed.connect(root._on_camp_object_pressed)
|
||||
root.ui.screens.menu_screen.settingsbtn.pressed.connect(root._open_settings) # 最小设置面入口
|
||||
root.ui.screens.menu_screen.quitbtn.pressed.connect(root._quit_game) # 退出游戏提级一级菜单
|
||||
|
||||
@@ -194,7 +234,12 @@ static func _build_debug_and_resolvers(root: Variant) -> void:
|
||||
# 消费方编排(卡③ 契约 3/sl8——禁双常驻静态接线):open 前设 _confirm_consumer,
|
||||
# confirmed/cancelled 后按该状态量分派;分派面可容纳后续消费方(卡④ 到位直接接入不返工)
|
||||
root.ui.modals.confirm_dialog.confirmed.connect(root._on_confirm_accepted)
|
||||
root.ui.modals.confirm_dialog.secondary_confirmed.connect(root._on_confirm_secondary) # 三钮次确认(卡④ 大门「开始新冒险」出口)
|
||||
root.ui.modals.confirm_dialog.cancelled.connect(root._on_confirm_declined)
|
||||
# Esc 浮窗三出口接线(卡⑤——意图信号经薄委托归 MainEscFlow 动线;输入入口=main._unhandled_input)
|
||||
root.ui.modals.camp.esc_overlay.return_requested.connect(root._on_esc_return)
|
||||
root.ui.modals.camp.esc_overlay.settings_requested.connect(root._on_esc_settings)
|
||||
root.ui.modals.camp.esc_overlay.quit_requested.connect(root._on_esc_quit)
|
||||
|
||||
# 调试浮窗(主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口;
|
||||
# 全局可开浮窗:调试会话恒开=启动即开〔_ready〕+两屏角标再开+当局数据只读区块;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
class_name CampScreen
|
||||
extends Control
|
||||
# 营地屏壳(营地覆盖层的屏壳容器)。
|
||||
# 营地覆盖层视图原样构建(CampOverlay 类视图继续代码构建、零 .tscn 化改造——契约 2);
|
||||
# 购买/开合接线归组合根经 CampShopPresenter(信号连接只在组合根)。
|
||||
# 可独立实例化(F6/headless 挂载)——V2 探针断言载体。
|
||||
# 营地屏壳(营地场景根的屏壳容器)。
|
||||
# 场景图视图代码构建(CampSceneView 继续代码构建、零 .tscn 化改造);布局表经组合根注入
|
||||
# (main_ui_build 装配段 setup_layers/bind_hotspots——视图禁自载表路径);购买/开合接线归组合根经
|
||||
# CampShopPresenter(信号连接只在组合根)。可独立实例化(F6/headless 挂载)——V2 探针断言载体。
|
||||
|
||||
var camp_overlay: CampOverlay # 营地覆盖层(原 build_overlays 营地段迁入——main 组合根句柄指向本屏成员)
|
||||
var camp_scene: CampSceneView # 营地场景图(交互式营地场景根——main 组合根句柄指向本屏成员)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE # 屏壳=纯容器(ui_input 实证:全屏 STOP 壳会吃掉下层模态鼠标输入)
|
||||
camp_overlay = CampOverlay.new()
|
||||
add_child(camp_overlay)
|
||||
camp_scene = CampSceneView.new()
|
||||
add_child(camp_scene)
|
||||
@@ -0,0 +1,12 @@
|
||||
## 营地交互域模态句柄子容器:持营地域全部模态句柄(终态六枚=营地场景根+锻造/秘宝/天赋树/图鉴四子界面+Esc 浮窗)。
|
||||
## RefCounted 不挂树;随 UiModals._init 构造(与 flow 同拍)。Esc 浮窗枚随卡⑤落——禁空占位凑结构。
|
||||
## 新营地句柄禁平铺回 ui_modals:营地域句柄寻址一律 ui.modals.camp.X 二层(模态句柄册分域纪律——全量分家另立批)。
|
||||
class_name UiCampModals
|
||||
extends RefCounted
|
||||
|
||||
var scene: CampSceneView # 营地场景根(交互式营地场景图——camp_screen 屏壳成员住宿)
|
||||
var forge: ForgeView # 锻造工坊子界面
|
||||
var treasure: TreasureView # 秘宝匣子界面
|
||||
var tree: TreeView # 天赋树子界面
|
||||
var codex: CodexView # 奖励格图鉴子界面(占位挂起态)
|
||||
var esc_overlay: CampEscOverlay # Esc 浮窗(卡⑤——终态六枚落满)
|
||||
@@ -0,0 +1 @@
|
||||
uid://goaqtequmh8s
|
||||
@@ -1,4 +1,4 @@
|
||||
## 模态层域句柄册:十三个模态覆盖层句柄+模态栈编排(ModalFlow——登记/开合/输入锁谓词)的注册面域容器。
|
||||
## 模态层域句柄册:十二个平铺模态句柄+营地交互域子容器(camp)+模态栈编排(ModalFlow——登记/开合/输入锁谓词)的注册面域容器。
|
||||
## RefCounted 不挂树;模态登记唯一口=register_layer(anchors 撑满宿主+排入 ModalStack)。
|
||||
## flow 随容器构造即建(装配早期注入点经 root.ui.modals.flow.* 取 Callable——晚于 flow 建立时点)。
|
||||
class_name UiModals
|
||||
@@ -11,7 +11,7 @@ var level_screen: LevelMapScreen
|
||||
var curio_overlay: CurioPickOverlay
|
||||
var end_overlay: BattleEndOverlay
|
||||
var shop_overlay: ShopOverlay
|
||||
var camp_overlay: CampOverlay
|
||||
var camp: UiCampModals
|
||||
var settings_overlay: SettingsOverlay
|
||||
var status_panel: StatusPanel
|
||||
var career_overlay: CareerOverlay
|
||||
@@ -22,6 +22,7 @@ var debug_overlay: DebugOverlay
|
||||
|
||||
func _init() -> void:
|
||||
flow = ModalFlow.new() # 查询/钩子 Callable 由 _build_modal_flow 注入(注入时点不变——注入值面向已在座实例)
|
||||
camp = UiCampModals.new() # 营地域句柄子容器(分域纪律——营地域句柄禁平铺回本册)
|
||||
|
||||
|
||||
func register_layer(m: Control, tags: Array = []) -> void:
|
||||
|
||||
@@ -22,7 +22,12 @@ const LAYER_CONTRACT: Array[Dictionary] = [
|
||||
{"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": "camp_scene", "kind": "modal", "tags": [], "shell": "camp_screen"}, # 营地场景根(交互式场景图——卡④)
|
||||
{"key": "forge_view", "kind": "modal", "tags": []}, # 营地四子界面(独立全屏页——场景根留底形态,卡④ T3)
|
||||
{"key": "treasure_view", "kind": "modal", "tags": []},
|
||||
{"key": "tree_view", "kind": "modal", "tags": []},
|
||||
{"key": "codex_view", "kind": "modal", "tags": []},
|
||||
{"key": "camp_esc_overlay", "kind": "modal", "tags": []}, # Esc 浮窗(卡⑤——登记于营地层与设置层之间,契约 3 栈语义正解)
|
||||
{"key": "settings_overlay", "kind": "modal", "tags": []},
|
||||
{"key": "status_panel", "kind": "modal", "tags": []},
|
||||
{"key": "career_overlay", "kind": "modal", "tags": []},
|
||||
@@ -58,7 +63,12 @@ func node(key: String) -> Node:
|
||||
"end_overlay": return modals.end_overlay
|
||||
"shop_overlay": return modals.shop_overlay
|
||||
"camp_screen": return screens.camp_screen
|
||||
"camp_overlay": return modals.camp_overlay
|
||||
"camp_scene": return modals.camp.scene
|
||||
"forge_view": return modals.camp.forge
|
||||
"treasure_view": return modals.camp.treasure
|
||||
"tree_view": return modals.camp.tree
|
||||
"codex_view": return modals.camp.codex
|
||||
"camp_esc_overlay": return modals.camp.esc_overlay
|
||||
"settings_overlay": return modals.settings_overlay
|
||||
"status_panel": return modals.status_panel
|
||||
"career_overlay": return modals.career_overlay
|
||||
|
||||
@@ -203,15 +203,11 @@ func set_world_font(f: Font) -> void:
|
||||
|
||||
# D 区可用面积(AP 条之外)→ 格边长 = floor((min(宽, 高) − 预留 − (N−1)×格间距) / N)(battle.md 格尺寸公式)
|
||||
# 公式须补算 (N−1)×2px 网格间距与垂直预留 24px,否则 12×12 板实高溢出可用区 ~6px,下缘被 clip_contents 裁掉。
|
||||
# 算式唯一落点 = _cell_for(迭代 46 卡C——探针与单元测试共用该静态函数求值,禁复写算式造第二口径)。
|
||||
func set_cell_area(avail: Vector2) -> void:
|
||||
if _board == null or _board.width == 0:
|
||||
return
|
||||
var n: int = maxi(_board.width, _board.height)
|
||||
var sep := SEP # 格间距(_init 同值)
|
||||
var d := DEPTH_PX
|
||||
var reserve := 24.0 + d # 垂直总预留(上 12 + 下 12 + 悬崖外延——悬崖占预算防溢出)
|
||||
var cell := int(floor((minf(avail.x - 12.0 - d, avail.y - reserve) - float((n - 1) * sep)) / float(n)))
|
||||
_cell = float(clampi(cell, 26, 46))
|
||||
_cell = float(_cell_for(maxi(_board.width, _board.height), avail))
|
||||
_hero_sprite.set_cell_size(_cell, float(SEP))
|
||||
var stride := _cell + float(SEP)
|
||||
for c in _cells:
|
||||
@@ -224,6 +220,17 @@ func set_cell_area(avail: Vector2) -> void:
|
||||
_update_world_size()
|
||||
|
||||
|
||||
# 格边长算式唯一落点(迭代 46 卡C T1 自 set_cell_area 抽出——逐位同式:SEP/DEPTH_PX 读本文件
|
||||
# 既有常量、clampi [26,46] 留本函数内;名字带下划线是 S13 max-public-methods 27 顶格的约束,禁改公开名。
|
||||
# 探针(非当前板型 13/14×实板可用面积)与单元测试(四档板型)经本函数求值,复写算式即造第二口径)
|
||||
static func _cell_for(n: int, avail: Vector2) -> int:
|
||||
var sep := SEP # 格间距(_init 同值)
|
||||
var d := DEPTH_PX
|
||||
var reserve := 24.0 + d # 垂直总预留(上 12 + 下 12 + 悬崖外延——悬崖占预算防溢出)
|
||||
var cell := int(floor((minf(avail.x - 12.0 - d, avail.y - reserve) - float((n - 1) * sep)) / float(n)))
|
||||
return clampi(cell, 26, 46)
|
||||
|
||||
|
||||
# 悬崖参数下发(网格像素尺寸/步距/列数/材质锚色——BoardWorld._draw 消费)
|
||||
func _refresh_slab() -> void:
|
||||
if _board == null or _board.width == 0:
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
## Esc 浮窗视图(营地语境 Esc 唤起):返回主菜单/设置/退出游戏三钮(裁决 9——三项且仅三项)。
|
||||
## 壳=UiKit.modal_shell(居中模态壳,ui_kit 工厂单点);退出钮=UiKit.danger_button;零新素材零新色值。
|
||||
## Q2 再按 Esc 关窗=组合根 Esc 路由直呼 flow.close(本视图无自关通路);三钮 pressed 外发意图不内含动线。
|
||||
## 零动效(契约 6——直开直关,子树零 AnimationPlayer、源码零动效令牌)。
|
||||
class_name CampEscOverlay
|
||||
extends Control
|
||||
|
||||
signal return_requested ## 返回主菜单意图(动线归 MainEscFlow._on_return_to_menu——首步 close 浮窗)
|
||||
signal settings_requested ## 设置意图(开设置层——浮窗登记在其下,栈语义关后重现)
|
||||
signal quit_requested ## 退出游戏意图(_quit_game 原样复用)
|
||||
|
||||
var _return_btn: Button
|
||||
var _settings_btn: Button
|
||||
var _quit_btn: Button
|
||||
|
||||
|
||||
func setup_buttons() -> void:
|
||||
var box: VBoxContainer = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(360, 0), 12,
|
||||
UiKit.panel_skinned(Vector4(14, 12, 14, 12))).box
|
||||
_return_btn = UiKit.button("返回主菜单")
|
||||
box.add_child(_return_btn)
|
||||
_settings_btn = UiKit.button("设置")
|
||||
box.add_child(_settings_btn)
|
||||
_quit_btn = UiKit.danger_button("退出游戏")
|
||||
box.add_child(_quit_btn)
|
||||
visible = false # 登记模态初始隐(开合走栈——模态纪律)
|
||||
|
||||
|
||||
func bind_signals() -> void:
|
||||
_return_btn.pressed.connect(func() -> void: return_requested.emit())
|
||||
_settings_btn.pressed.connect(func() -> void: settings_requested.emit())
|
||||
_quit_btn.pressed.connect(func() -> void: quit_requested.emit())
|
||||
@@ -0,0 +1 @@
|
||||
uid://col25elh1utw5
|
||||
@@ -0,0 +1,27 @@
|
||||
## 营地顶栏小 HUD 栏(裁决 10——钥石余额常驻,最小集形态):承载 UiKit.keystone_badge(工厂单点)。
|
||||
## 读数只经 camp_data() 投影单口(禁第二账本、禁视图自算);刷新挂卡④ CampRefreshBus 单口
|
||||
## (bus 目标册成员——显式触发才变,禁 _ready 自刷/自持第二入口,契约 5);载体=PRESET_TOP_WIDE 48px 浮层。
|
||||
class_name CampHudBand
|
||||
extends Control
|
||||
|
||||
var _badge: PanelContainer
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||||
custom_minimum_size = Vector2(0, 48) # 高 48px 浮层覆盖、不挤压画面(camp.md §顶栏)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE # HUD 栏不挡场景命中区(浮层覆盖语义)
|
||||
_badge = UiKit.keystone_badge("钥石 0")
|
||||
_badge.set_anchors_preset(Control.PRESET_CENTER_RIGHT)
|
||||
_badge.position = Vector2(-160, 8) # 顶栏右位次(UiKit 现行件域内——代拍板 3)
|
||||
add_child(_badge)
|
||||
|
||||
|
||||
## 余额写入(格式串=既有消费面同款「钥石 %d」——badge 工厂约定单点,禁第二格式串)
|
||||
func set_balance(v: int) -> void:
|
||||
_badge.value_label.text = "钥石 %d" % v
|
||||
|
||||
|
||||
## 刷新单口落点(CampRefreshBus 目标册成员——显式触发才变,契约 5 刷新时机①②③同一闸)
|
||||
func refresh_camp(camp_data: Dictionary) -> void:
|
||||
set_balance(int(camp_data.get("balance", 0)))
|
||||
@@ -0,0 +1 @@
|
||||
uid://by6ugjfdicgud
|
||||
@@ -1,655 +0,0 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 营地覆盖层:树淬炼/锻造工坊/秘宝匣/奖励格图鉴四柜台只读投影,购买与切换意图经信号外发。
|
||||
## 柜台锁定态(钥匙未点亮)开/关零副作用——零写档零报错、锁定行纯文案无可交互购买路径。
|
||||
## 开合与开始层互斥归组合根承接(打开前隐藏开始层)。
|
||||
## 内部状态件(u1 子域拆件——契约 9 职责轴):_TreeState=树淬炼柜台内容容器(单一变化原因=树行板结构);
|
||||
## _ForgeState=锻造工坊与秘宝匣两柜台行板与锁定门(钥匙门同构);_AttuneState=同调装配段节点与钮册;
|
||||
## _SideState=Y 刷新升级行与最近冒险账面。探针直读冻结成员(_balance_label/_tree_buy_btns/
|
||||
## _tree_fork_btns/_workshop_holder/_workshop_rows)住宿主不进状态件。
|
||||
class_name CampOverlay
|
||||
extends Control
|
||||
|
||||
signal buy_requested(attr: String)
|
||||
signal closed
|
||||
signal attune_toggled(item_id: String) ## 同调装配切换意图(写口唯一归 MetaProgression.toggle_attunement)
|
||||
signal tree_buy_requested(node_id: String) ## 天赋树购买意图(写口唯一归 MetaProgression.tree_buy)
|
||||
signal tree_fork_toggled(group: String, side: String) ## 双态切换意图(一节点双态——零币切态)
|
||||
signal forge_buy_requested(route_id: String) ## 锻造路线购买意图(写口唯一归 MetaProgression.forge_buy_level)
|
||||
signal forge_unlock_requested(route_id: String) ## 锻造路线解锁意图(写口唯一归 MetaProgression.forge_buy_unlock)
|
||||
signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select)
|
||||
|
||||
# 方向稿取色(柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit
|
||||
const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND
|
||||
const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE
|
||||
|
||||
|
||||
class _TreeState extends RefCounted:
|
||||
var _tree_holder: VBoxContainer # 树淬炼柜台内容(主界面——脊柱两行+钥匙/socket 行板+Fork 双态钮)
|
||||
|
||||
|
||||
class _ForgeState extends RefCounted:
|
||||
var _forge_lock_l: Label # 锻造工坊柜台锁定态行(钥匙未点亮)
|
||||
var _forge_workbench: VBoxContainer # 锻造工坊工作间(钥匙点亮后展开——两区六路线行板)
|
||||
var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线)
|
||||
var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线)
|
||||
var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费)
|
||||
var _ws_scroll: ScrollContainer # 秘宝匣柜台内容容器(钥匙点亮后可见)
|
||||
var _ws_lock_l: Label # 秘宝匣柜台锁定态行
|
||||
|
||||
|
||||
class _AttuneState extends RefCounted:
|
||||
var _attune_header_l: Label # 同调装配段(Slots 点亮呈现)
|
||||
var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷)
|
||||
var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行)
|
||||
|
||||
|
||||
class _SideState extends RefCounted:
|
||||
var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账)
|
||||
var _refresh_btn: Button
|
||||
var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建)
|
||||
|
||||
|
||||
var _balance_label: Label # 探针直读冻结(S17①——住宿主)
|
||||
var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费;住宿主)
|
||||
var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费;住宿主)
|
||||
var _workshop_holder: VBoxContainer # 秘宝匣柜台行板(六行+同调装配段+Y 升级行;探针直读冻结——住宿主)
|
||||
var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}](探针消费;住宿主)
|
||||
var _tree := _TreeState.new()
|
||||
var _forge := _ForgeState.new()
|
||||
var _attune := _AttuneState.new()
|
||||
var _side := _SideState.new()
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
# 底=不透明暖亮档(半透 dim 会透出底下棋盘/战斗提示条,故须实底;demo #v-camp=bg-bright 实底,
|
||||
# 遮蔽语义由组合根输入锁兜底)
|
||||
var dim := UiKit._modal_dim(self, UiKit.BG_BRIGHT)
|
||||
# 石纹弱质感+暗角(demo .tilebg 0.14/.vignette;缺位不挂即回退平色)
|
||||
var bd := UiKit.backdrop_tile()
|
||||
if bd != null:
|
||||
dim.add_child(bd)
|
||||
dim.add_child(UiKit.vignette_rect())
|
||||
|
||||
var root := MarginContainer.new()
|
||||
root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
root.add_theme_constant_override("margin_left", 16)
|
||||
root.add_theme_constant_override("margin_right", 16)
|
||||
root.add_theme_constant_override("margin_top", 12)
|
||||
root.add_theme_constant_override("margin_bottom", 12)
|
||||
dim.add_child(root)
|
||||
var col := VBoxContainer.new()
|
||||
col.add_theme_constant_override("separation", 8)
|
||||
root.add_child(col)
|
||||
|
||||
# ── 顶栏:左铭牌(标题+副题)/ 右钥石徽 ──
|
||||
_build_top_bar(col)
|
||||
|
||||
# ── 四柜台并排(程序占位重排)──
|
||||
# 宽度不足横向滚动(四柜 300 min 宽 > 窄窗可用宽时横滚兜底;纵向撑满余高)
|
||||
var camp_scroll := ScrollContainer.new()
|
||||
camp_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
camp_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
||||
camp_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
col.add_child(camp_scroll)
|
||||
var mid := HBoxContainer.new()
|
||||
mid.add_theme_constant_override("separation", 14)
|
||||
mid.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
camp_scroll.add_child(mid)
|
||||
_build_tree_counter(mid)
|
||||
_build_forge_counter(mid)
|
||||
_build_workshop_counter(mid)
|
||||
_build_atlas_counter(mid)
|
||||
_build_bottom_bar(col)
|
||||
visible = false
|
||||
|
||||
|
||||
## 顶栏域:铭牌(标题+副题)+钥石徽章
|
||||
func _build_top_bar(col: VBoxContainer) -> void:
|
||||
var top := HBoxContainer.new()
|
||||
top.add_theme_constant_override("separation", 10)
|
||||
col.add_child(top)
|
||||
var plaque := PanelContainer.new()
|
||||
plaque.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(14, 8, 14, 8)))
|
||||
top.add_child(plaque)
|
||||
var plaque_col := VBoxContainer.new()
|
||||
plaque.add_child(plaque_col)
|
||||
var title := UiKit.label("营地 · 天赋树",UiKit.FS_TITLE, true, UiKit.INK_TITLE)
|
||||
plaque_col.add_child(title)
|
||||
var sub := UiKit.label("营地本身即树——钥石逐节点点亮,属性/规则/工作间都在树上",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # STONE 对铭牌板 3.59:1 越带,换灰石深化 5.98:1
|
||||
plaque_col.add_child(sub)
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
top.add_child(spacer)
|
||||
# 钥石徽章烛光黄底+深墨字(demo ②)。UiKit.keystone_badge 工厂单点
|
||||
var badge := UiKit.keystone_badge("钥石 0")
|
||||
top.add_child(badge)
|
||||
_balance_label = badge.value_label
|
||||
|
||||
|
||||
## 柜一·树淬炼(树脊柱界面形态=主界面:一线一节点行板内升级+Fork 双态切换钮)
|
||||
func _build_tree_counter(mid: HBoxContainer) -> void:
|
||||
_tree._tree_holder = VBoxContainer.new()
|
||||
_tree._tree_holder.add_theme_constant_override("separation", 6)
|
||||
var tree_scroll := ScrollContainer.new()
|
||||
tree_scroll.custom_minimum_size = Vector2(300, 336)
|
||||
tree_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
tree_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
tree_scroll.add_child(_tree._tree_holder)
|
||||
var tree_panel := _counter_panel("天赋树 · 淬炼", "", tree_scroll, 300)
|
||||
tree_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
tree_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸
|
||||
mid.add_child(tree_panel)
|
||||
|
||||
|
||||
## 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间两区六路线行板)
|
||||
func _build_forge_counter(mid: HBoxContainer) -> void:
|
||||
_forge._forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)",UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
_forge._forge_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中
|
||||
_forge._forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_forge._forge_lock_l.custom_minimum_size = Vector2(280, 0)
|
||||
_forge._forge_workbench = VBoxContainer.new()
|
||||
_forge._forge_workbench.add_theme_constant_override("separation", 6)
|
||||
_forge._forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建)
|
||||
_forge._forge_zone_weapon = VBoxContainer.new()
|
||||
_forge._forge_zone_weapon.add_theme_constant_override("separation", 4)
|
||||
_forge._forge_zone_armor = VBoxContainer.new()
|
||||
_forge._forge_zone_armor.add_theme_constant_override("separation", 4)
|
||||
var wb_wh := UiKit.label("── 武器锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK) # 同款深金墨(对柜台板 5.21:1)
|
||||
_forge._forge_workbench.add_child(wb_wh)
|
||||
_forge._forge_workbench.add_child(_forge._forge_zone_weapon)
|
||||
var wb_ah := UiKit.label("── 护甲锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK)
|
||||
_forge._forge_workbench.add_child(wb_ah)
|
||||
_forge._forge_workbench.add_child(_forge._forge_zone_armor)
|
||||
var forge_holder := VBoxContainer.new()
|
||||
forge_holder.add_theme_constant_override("separation", 6)
|
||||
forge_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(与树/秘宝匣 scroll 336 min 对齐——锁定态仅锁文也撑住栏高)
|
||||
forge_holder.add_child(_forge._forge_lock_l)
|
||||
forge_holder.add_child(_forge._forge_workbench)
|
||||
var forge_panel := _counter_panel("锻造工坊", UiKit.TEX_ICON_FORGE, forge_holder, 300)
|
||||
forge_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
forge_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸
|
||||
mid.add_child(forge_panel)
|
||||
|
||||
|
||||
## 柜三·秘宝匣(钥匙门——ks_workshop 点亮前锁定态;点亮=现工坊内容整体迁入:六行+同调装配段+Y 升级行)
|
||||
func _build_workshop_counter(mid: HBoxContainer) -> void:
|
||||
_forge._ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)",UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
_forge._ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中
|
||||
_forge._ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_forge._ws_lock_l.custom_minimum_size = Vector2(280, 0)
|
||||
_forge._ws_scroll = ScrollContainer.new()
|
||||
_forge._ws_scroll.custom_minimum_size = Vector2(300, 336)
|
||||
_forge._ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_forge._ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
_workshop_holder = VBoxContainer.new()
|
||||
_workshop_holder.add_theme_constant_override("separation", 6)
|
||||
_forge._ws_scroll.add_child(_workshop_holder)
|
||||
var ws_holder := VBoxContainer.new()
|
||||
ws_holder.add_theme_constant_override("separation", 6)
|
||||
ws_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(锁定态 scroll 隐、仅锁文也撑住栏高)
|
||||
ws_holder.add_child(_forge._ws_lock_l)
|
||||
ws_holder.add_child(_forge._ws_scroll)
|
||||
var ws_panel := _counter_panel("秘宝匣", UiKit.TEX_ICON_WORKSHOP, ws_holder, 300)
|
||||
ws_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
ws_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸
|
||||
mid.add_child(ws_panel)
|
||||
|
||||
|
||||
## 柜四·奖励格图鉴(常开独立位——结构位预留,内容排 Alpha)
|
||||
func _build_atlas_counter(mid: HBoxContainer) -> void:
|
||||
var atlas_holder := VBoxContainer.new()
|
||||
atlas_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(图鉴仅锁文也撑住栏高)
|
||||
var atlas_panel := _counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 300)
|
||||
atlas_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
atlas_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸
|
||||
mid.add_child(atlas_panel)
|
||||
var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)",UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中
|
||||
lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
lock_l.custom_minimum_size = Vector2(280, 0)
|
||||
atlas_holder.add_child(lock_l)
|
||||
|
||||
|
||||
## 底栏域:关闭钮(左)/ 操作注记(右)+ 最近冒险条带
|
||||
func _build_bottom_bar(col: VBoxContainer) -> void:
|
||||
var close_btn := UiKit.button("关闭营地(回开始界面)")
|
||||
close_btn.pressed.connect(_emit_closed)
|
||||
var bottom := HBoxContainer.new()
|
||||
bottom.add_theme_constant_override("separation", 10)
|
||||
bottom.add_child(close_btn)
|
||||
var spacer2 := Control.new()
|
||||
spacer2.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
bottom.add_child(spacer2)
|
||||
var caption := UiKit.label("树上点亮 · 工作间随钥匙展开 · 图鉴常开",UiKit.FS_HINT, false, UiKit.WARM_SOFT)
|
||||
bottom.add_child(caption)
|
||||
col.add_child(bottom)
|
||||
_side._recent_list = UiKit.label("",UiKit.FS_HINT, false, UiKit.WARM_SOFT)
|
||||
_side._recent_list.clip_text = true
|
||||
_side._recent_list.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
col.add_child(_side._recent_list)
|
||||
|
||||
|
||||
# 柜台列面板(方向稿同构):PanelContainer 米板身 + 头带深棕(图标+标题暖白)
|
||||
func _counter_panel(title_text: String, icon_path: String, holder: Control, min_w: int) -> PanelContainer:
|
||||
var panel := PanelContainer.new()
|
||||
panel.custom_minimum_size = Vector2(min_w, 0)
|
||||
panel.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 10, 10, 10)))
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.add_theme_constant_override("separation", 8)
|
||||
panel.add_child(vbox)
|
||||
var band := PanelContainer.new()
|
||||
band.add_theme_stylebox_override("panel", UiKit.panel_flat(HEADER_BAND_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 5, 10, 5)))
|
||||
vbox.add_child(band)
|
||||
var head := HBoxContainer.new()
|
||||
head.add_theme_constant_override("separation", 6)
|
||||
band.add_child(head)
|
||||
if icon_path != "":
|
||||
var ic := UiKit.icon_rect(icon_path, 32) # 柜台头带 64 源 0.5×;缺位 null → 纯文本回退
|
||||
if ic != null:
|
||||
head.add_child(ic)
|
||||
var l := UiKit.label(title_text,UiKit.FS_BODY, true, UiKit.WARM)
|
||||
head.add_child(l)
|
||||
vbox.add_child(holder)
|
||||
return panel
|
||||
|
||||
|
||||
# ---- 柜台行构建(open_camp 重建;行结构仿 ShopOverlay 商品卡——亮板双色墨制)----
|
||||
|
||||
# 天赋树段重建(树淬炼柜台主界面:脉分组行板:节点名/等级/价/购买钮 + Fork 双态切换钮;
|
||||
# 树数据 = 组合根 _camp_data().tree 投影 [{node_id, display_name, vein, kind, level, price, can_buy,
|
||||
# currency}...] + fork_groups [{group, node_id, active_side, owned, can_buy, price, sides:[{side, display_name, effects}]}])
|
||||
func _rebuild_tree(data: Dictionary) -> void:
|
||||
for c in _tree._tree_holder.get_children():
|
||||
c.queue_free()
|
||||
_tree_buy_btns.clear()
|
||||
_tree_fork_btns.clear()
|
||||
var tree: Array[Variant] = data.get("tree", []) as Array
|
||||
var forks: Array[Variant] = data.get("fork_groups", []) as Array
|
||||
var vein_names := {"spine": "·脊柱淬炼·", "forge": "·锻造脉·", "keystone": "·钥石脉·", "attune": "·同调脉·"}
|
||||
var last_vein := ""
|
||||
for row in tree:
|
||||
var vein := String(row.get("vein", ""))
|
||||
if vein != last_vein:
|
||||
last_vein = vein
|
||||
var vh := UiKit.label(String(vein_names.get(vein, vein)),UiKit.FS_HINT, true, UiKit.GOLD_INK) # GOLD_DEEP 对柜台板 2.67:1 越带,换深金墨 5.21:1
|
||||
_tree._tree_holder.add_child(vh)
|
||||
if String(row.get("kind", "")) == "fork":
|
||||
continue # Fork 行去行板化——由 fork_groups 段承载双态切换钮
|
||||
_tree._tree_holder.add_child(_tree_row_line(row))
|
||||
for fg in forks:
|
||||
_tree._tree_holder.add_child(_tree_fork_line(fg))
|
||||
|
||||
|
||||
## 树行板构建域:节点名(含门槛标记)+购买钮(追加序与锚句随段整体搬迁)
|
||||
func _tree_row_line(row: Dictionary) -> HBoxContainer:
|
||||
var hb := HBoxContainer.new()
|
||||
hb.add_theme_constant_override("separation", 6)
|
||||
var lvl := int(row.get("level", 0))
|
||||
var cap := int(row.get("cap", 1))
|
||||
var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL)
|
||||
name_l.custom_minimum_size = Vector2(150, 0)
|
||||
name_l.clip_text = true
|
||||
var gate := String(row.get("gate_text", ""))
|
||||
if gate != "":
|
||||
name_l.text += " 🔒" # 等级阈值前置未达标标记(R1 机制位——悬停看门槛;达标后随 refresh 消失)
|
||||
name_l.tooltip_text = "等级门槛:%s" % gate
|
||||
hb.add_child(name_l)
|
||||
var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准
|
||||
btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", ""))))
|
||||
btn.disabled = not bool(row.get("can_buy", false))
|
||||
hb.add_child(btn)
|
||||
_tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn})
|
||||
return hb
|
||||
|
||||
|
||||
## Fork 双态行板构建域:组状态文案+解锁钮或双态切换钮
|
||||
func _tree_fork_line(fg: Dictionary) -> HBoxContainer:
|
||||
var group := String(fg.get("group", ""))
|
||||
var active := String(fg.get("active_side", ""))
|
||||
var owned := bool(fg.get("owned", false))
|
||||
var fb := HBoxContainer.new()
|
||||
fb.add_theme_constant_override("separation", 6)
|
||||
var state_txt := "未解锁"
|
||||
if owned:
|
||||
state_txt = "激活:%s" % active
|
||||
var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL)
|
||||
fb.add_child(gl)
|
||||
if not owned:
|
||||
var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免
|
||||
bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", ""))))
|
||||
bb.disabled = not bool(fg.get("can_buy", false))
|
||||
fb.add_child(bb)
|
||||
_tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb})
|
||||
else:
|
||||
for side_row in (fg.get("sides", []) as Array):
|
||||
var sd := String(side_row.get("side", ""))
|
||||
var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免
|
||||
sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd))
|
||||
fb.add_child(sb)
|
||||
_tree_fork_btns.append({"group": group, "side": sd, "btn": sb})
|
||||
return fb
|
||||
|
||||
|
||||
# 工作间钥匙门(锻造工坊/秘宝匣柜台锁定态切换:节点点亮=展开、未点亮=锁定行;
|
||||
# 图鉴常开无门槛。判据读树投影行 level——零核心访问,纯视图)
|
||||
func _refresh_key_gates(data: Dictionary) -> void:
|
||||
var lvl_by_id := {}
|
||||
for row in (data.get("tree", []) as Array):
|
||||
lvl_by_id[String(row.get("node_id", ""))] = int(row.get("level", 0))
|
||||
var forge_open := int(lvl_by_id.get("forge_workshop", 0)) >= 1
|
||||
var ws_open := int(lvl_by_id.get("ks_workshop", 0)) >= 1
|
||||
_forge._forge_lock_l.visible = not forge_open
|
||||
_forge._forge_workbench.visible = forge_open
|
||||
_forge._ws_lock_l.visible = not ws_open
|
||||
_forge._ws_scroll.visible = ws_open
|
||||
|
||||
|
||||
# 锻造工坊行板重建(两区六路线;open_camp/refresh_camp 调用。行结构仿秘宝匣行板:
|
||||
# 件名+等级+解锁态+购买/解锁/选中钮。UI 只发意图信号〔契约 9——零核心写调用、零 MetaProgression
|
||||
# 持有〕;钮禁用=常态防线(余额/未解锁/满级/已选中),兜底拒绝可见归 presenter pattern 1)
|
||||
func _rebuild_forge(data: Dictionary) -> void:
|
||||
for zone in [_forge._forge_zone_weapon, _forge._forge_zone_armor]:
|
||||
for c in zone.get_children():
|
||||
c.queue_free()
|
||||
_forge._forge_rows.clear()
|
||||
if not _forge._forge_workbench.visible:
|
||||
return # 锁定态(钥匙未点亮):零行板零购买路径——占位纪律延续
|
||||
var bal := int(data.get("balance", 0))
|
||||
for row in data.get("forge", []):
|
||||
var kind := String(row.get("kind", ""))
|
||||
var zone: VBoxContainer = _forge._forge_zone_weapon if kind == "weapon" else _forge._forge_zone_armor
|
||||
var card := _forge_route_card(row, bal)
|
||||
zone.add_child(card["card"])
|
||||
_forge._forge_rows.append(card)
|
||||
|
||||
|
||||
## 锻造行板构建域:单路线卡(件名+等级+状态+购买/解锁/选中钮——并联双通道呈现面)
|
||||
func _forge_route_card(row: Dictionary, bal: int) -> Dictionary:
|
||||
var d := _forge_route_scalars(row)
|
||||
var card := PanelContainer.new()
|
||||
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4)))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 6)
|
||||
card.add_child(line)
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK)
|
||||
name_l.custom_minimum_size = Vector2(104, 0)
|
||||
name_l.clip_text = true
|
||||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
line.add_child(name_l)
|
||||
var lvl_l := UiKit.label("Lv%d/%d" % [d["lvl"], d["cap_v"]], 13, false, UiKit.INK_LABEL)
|
||||
lvl_l.custom_minimum_size = Vector2(44, 0)
|
||||
line.add_child(lvl_l)
|
||||
var status_l := UiKit.label(_forge_status_text(d, row), UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
status_l.clip_text = true
|
||||
status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
line.add_child(status_l)
|
||||
var btns := _forge_route_buttons(d, row, bal, line)
|
||||
return {"card": card, "route_id": d["rid"], "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l,
|
||||
"buy_btn": btns["buy_btn"], "unlock_btn": btns["unlock_btn"], "select_btn": btns["select_btn"],
|
||||
"next_price": d["next_price"], "unlock_price": d["unlock_price"]}
|
||||
|
||||
|
||||
## 锻造行标量束:路线 id/解锁态/初始持有/等级/cap/几何价/解锁价一次取数
|
||||
func _forge_route_scalars(row: Dictionary) -> Dictionary:
|
||||
return {
|
||||
"rid": String(row.get("route_id", "")),
|
||||
"unlocked": bool(row.get("unlocked", false)),
|
||||
"initial": bool(row.get("initial_owned", false)),
|
||||
"lvl": int(row.get("level", 0)),
|
||||
"cap_v": int(row.get("cap", 0)),
|
||||
"next_price": int(row.get("next_price", 0)),
|
||||
"unlock_price": int(row.get("unlock_price", 0)),
|
||||
}
|
||||
|
||||
|
||||
## 状态文案域(并联双通道:练满前一档 or 一次性解锁价直购;已满级态)
|
||||
func _forge_status_text(d: Dictionary, row: Dictionary) -> String:
|
||||
var st_text := "已解锁"
|
||||
if not d["unlocked"]:
|
||||
if d["initial"]:
|
||||
st_text = "未解锁"
|
||||
else:
|
||||
st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), d["unlock_price"]]
|
||||
elif d["lvl"] >= d["cap_v"]:
|
||||
st_text = "已满级"
|
||||
return st_text
|
||||
|
||||
|
||||
## 三钮域:购买(已解锁未满级——余额不足禁用)/解锁(待解锁非初始——余额不足禁用)/选中(已解锁——当前=禁用态标记)
|
||||
func _forge_route_buttons(d: Dictionary, row: Dictionary, bal: int, line: HBoxContainer) -> Dictionary:
|
||||
var buy_btn: Button = null
|
||||
if d["unlocked"] and d["lvl"] < d["cap_v"]:
|
||||
buy_btn = UiKit.button("锻造 %d" % d["next_price"], 0.0) # 行板紧凑钮豁免
|
||||
buy_btn.pressed.connect(forge_buy_requested.emit.bind(d["rid"]))
|
||||
buy_btn.disabled = bal < d["next_price"]
|
||||
line.add_child(buy_btn)
|
||||
var unlock_btn: Button = null
|
||||
if not d["unlocked"] and not d["initial"]:
|
||||
unlock_btn = UiKit.button("解锁 %d" % d["unlock_price"], 0.0) # 行板紧凑钮豁免
|
||||
unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(d["rid"]))
|
||||
unlock_btn.disabled = bal < d["unlock_price"]
|
||||
line.add_child(unlock_btn)
|
||||
var select_btn: Button = null
|
||||
if d["unlocked"]:
|
||||
var sel := bool(row.get("selected", false))
|
||||
select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免
|
||||
select_btn.pressed.connect(forge_select_requested.emit.bind(d["rid"]))
|
||||
select_btn.disabled = sel
|
||||
line.add_child(select_btn)
|
||||
return {"buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn}
|
||||
|
||||
|
||||
# 打开营地(全量重建四柜台行 + 同调装配段 + 最近冒险;树段与钥匙门随 refresh_camp 重建——
|
||||
# 数据 = 组合根 _camp_data() 投影)
|
||||
func open_camp(data: Dictionary) -> void:
|
||||
_rebuild_workshop(data) # 段尾链 _rebuild_attune(同调装配段+Y 升级行随秘宝匣柜台内容)
|
||||
_rebuild_forge(data) # 锻造工坊两区六路线行板
|
||||
_rebuild_recent(data)
|
||||
refresh_camp(data)
|
||||
visible = true
|
||||
|
||||
|
||||
# 状态重刷(购买后调用——余额/树行板/钥匙门/工坊状态就地重建或改文案;树行板随刷重建=按钮
|
||||
# 禁用态与门槛/cap 同步,12 行重建开销可忽略)
|
||||
func refresh_camp(data: Dictionary) -> void:
|
||||
var balance := int(data.get("balance", 0))
|
||||
_balance_label.text = "钥石 %d" % balance
|
||||
_rebuild_tree(data)
|
||||
_refresh_key_gates(data)
|
||||
_rebuild_forge(data) # 行板随刷重建(购买/解锁/选中后等级/价/禁用态同步)
|
||||
_refresh_workshop_rows(data, balance)
|
||||
_refresh_attune_segment(data, balance)
|
||||
|
||||
|
||||
## 秘宝匣行板刷新域:进度列(解锁混合制三形态)与状态色(亮板双色墨制)与购买钮余额联动
|
||||
func _refresh_workshop_rows(data: Dictionary, balance: int) -> void:
|
||||
var ws_by_id := {}
|
||||
for row in data.get("workshop", []):
|
||||
ws_by_id[String(row.get("item_id", ""))] = row
|
||||
for card in _workshop_rows:
|
||||
var row: Dictionary[Variant, Variant] = ws_by_id.get(String(card.item_id), {})
|
||||
if row.is_empty():
|
||||
continue
|
||||
var unlocked := bool(row.get("unlocked", false))
|
||||
card.progress_l.text = _workshop_progress_text(row, unlocked)
|
||||
# 状态色用亮板双色墨制(深墨族替代暖白族——亮板上暖白不可读)
|
||||
var st_text := "未解锁"
|
||||
var st_col: Color = UiKit.STONE_DEEP
|
||||
if unlocked:
|
||||
st_text = "已解锁"
|
||||
st_col = UiKit.GOLD_INK # GOLD_DEEP 对行板 3.00:1 越带,换深金墨 5.84:1
|
||||
card.status_l.text = st_text
|
||||
card.status_l.add_theme_color_override("font_color", st_col)
|
||||
card.status_l.tooltip_text = st_text # 截断兜底(状态列 EXPAND_FILL 吸余宽——悬停看全文)
|
||||
# 购买钮余额联动(禁用态=余额不足,常态防线;pattern 1 兜底拒绝可见归 presenter)
|
||||
if card.get("buy_btn") != null:
|
||||
card.buy_btn.disabled = balance < int(card.get("price", 0))
|
||||
|
||||
|
||||
## 进度列文案域(解锁混合制三形态:钥石购买价/条件进度拼接/已入手)
|
||||
func _workshop_progress_text(row: Dictionary, unlocked: bool) -> String:
|
||||
if unlocked:
|
||||
return "已入手"
|
||||
var mode := String(row.get("mode", ""))
|
||||
if mode == "keystone":
|
||||
return "钥石 %d" % int(row.get("price", 0))
|
||||
if mode == "condition":
|
||||
var parts: Array[String] = []
|
||||
for req_v in (row.get("reqs", []) as Array):
|
||||
var req: Dictionary[Variant, Variant] = req_v as Dictionary
|
||||
var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件"))
|
||||
parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))])
|
||||
return "、".join(parts)
|
||||
return ""
|
||||
|
||||
|
||||
## 同调装配段刷新域(Slots 点亮/满位禁选/Y 升级行余额联动——就地刷)
|
||||
func _refresh_attune_segment(data: Dictionary, balance: int) -> void:
|
||||
var att: Dictionary[Variant, Variant] = data.get("attune", {})
|
||||
if _attune._attune_header_l != null:
|
||||
var slots_n := int(att.get("slots", 0))
|
||||
var eq: Array[Variant] = att.get("equipped", [])
|
||||
_attune._attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n]
|
||||
var eq_set := {}
|
||||
for v in eq:
|
||||
eq_set[String(v)] = true
|
||||
for card2 in _attune._attune_btns:
|
||||
var on: bool = eq_set.has(String(card2.item_id))
|
||||
card2.btn.text = "卸下" if on else "装配"
|
||||
# 满位禁选+未解锁禁装(已装件恒可卸下;unlocked 快照随 rebuild 刷新)
|
||||
card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false)))
|
||||
if _side._refresh_l != null:
|
||||
var rf: Dictionary[Variant, Variant] = att.get("refresh", {})
|
||||
_side._refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))]
|
||||
_side._refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0))
|
||||
_side._refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0))
|
||||
|
||||
|
||||
func _rebuild_workshop(data: Dictionary) -> void:
|
||||
_workshop_rows.clear()
|
||||
for child in _workshop_holder.get_children():
|
||||
child.queue_free()
|
||||
_attune._attune_nodes.clear()
|
||||
_attune._attune_btns.clear()
|
||||
_attune._attune_header_l = null
|
||||
_side._refresh_l = null
|
||||
_side._refresh_btn = null
|
||||
for row in data.get("workshop", []):
|
||||
var card := _workshop_card(row)
|
||||
_workshop_holder.add_child(card["card"])
|
||||
_workshop_rows.append(card)
|
||||
_rebuild_attune(data)
|
||||
|
||||
|
||||
## 秘宝匣行板构建域:件名(分档后缀+效果摘要悬停)+进度列+状态列+钥石购买钮(购买件未解锁在列)
|
||||
func _workshop_card(row: Dictionary) -> Dictionary:
|
||||
var card := PanelContainer.new()
|
||||
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4)))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 12)
|
||||
card.add_child(line)
|
||||
var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), ""))
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK)
|
||||
name_l.custom_minimum_size = Vector2(132, 0)
|
||||
name_l.clip_text = true
|
||||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀)
|
||||
line.add_child(name_l)
|
||||
var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 同行价格墨同案
|
||||
progress_l.custom_minimum_size = Vector2(150, 0)
|
||||
progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜
|
||||
progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
line.add_child(progress_l)
|
||||
# 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式
|
||||
var status_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.STONE_DEEP)
|
||||
status_l.clip_text = true # 同上——「已解锁 · 能力封存中」不撑柜(余宽吸收者)
|
||||
status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
line.add_child(status_l)
|
||||
# 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock,
|
||||
# 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足)
|
||||
var buy_btn: Button = null
|
||||
if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone":
|
||||
buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免
|
||||
buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", ""))))
|
||||
line.add_child(buy_btn)
|
||||
return {"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false)), "card": card}
|
||||
|
||||
|
||||
# 同调装配段重建(秘宝匣柜台尾部段;Slots 点亮+
|
||||
# 九件行板+Y 升级行,程序占位面)
|
||||
func _rebuild_attune(data: Dictionary) -> void:
|
||||
var att: Dictionary[Variant, Variant] = data.get("attune", {})
|
||||
_attune._attune_header_l = UiKit.label("",UiKit.FS_HINT, true, UiKit.INK)
|
||||
_workshop_holder.add_child(_attune._attune_header_l)
|
||||
_attune._attune_nodes.append(_attune._attune_header_l)
|
||||
for row in att.get("items", []):
|
||||
var line := _attune_item_line(row)
|
||||
_workshop_holder.add_child(line)
|
||||
_attune._attune_nodes.append(line)
|
||||
_refresh_attune_tail(att)
|
||||
|
||||
|
||||
## 同调件行板构建域:件名+形态+档位+装配切换钮(未解锁禁装——toggle 写口同守卫)
|
||||
func _attune_item_line(row: Dictionary) -> HBoxContainer:
|
||||
var iid := String(row.get("item_id", ""))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 6)
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK)
|
||||
name_l.custom_minimum_size = Vector2(96, 0)
|
||||
name_l.clip_text = true
|
||||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
line.add_child(name_l)
|
||||
var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||||
form_l.custom_minimum_size = Vector2(58, 0)
|
||||
line.add_child(form_l)
|
||||
# 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边)
|
||||
var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||||
tier_l.custom_minimum_size = Vector2(36, 0)
|
||||
line.add_child(tier_l)
|
||||
var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免
|
||||
btn.pressed.connect(attune_toggled.emit.bind(iid))
|
||||
btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫)
|
||||
line.add_child(btn)
|
||||
_attune._attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))})
|
||||
return line
|
||||
|
||||
|
||||
## Y 升级尾行构建域(双入口之一——树上 ks_refresh 同账)
|
||||
func _refresh_attune_tail(att: Dictionary) -> void:
|
||||
var refresh_line := HBoxContainer.new()
|
||||
refresh_line.add_theme_constant_override("separation", 6)
|
||||
_side._refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||||
refresh_line.add_child(_side._refresh_l)
|
||||
_side._refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免
|
||||
_side._refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh"))
|
||||
refresh_line.add_child(_side._refresh_btn)
|
||||
_workshop_holder.add_child(refresh_line)
|
||||
_attune._attune_nodes.append(refresh_line)
|
||||
|
||||
|
||||
func _rebuild_recent(data: Dictionary) -> void:
|
||||
var lines: Array[String] = []
|
||||
var recent: Array[Variant] = data.get("recent", [])
|
||||
if recent.is_empty():
|
||||
lines.append(" 最近冒险:(暂无记录——完成或结束一次冒险后入账)")
|
||||
for s in recent:
|
||||
var verdict := "失败"
|
||||
if String(s.get("result", "")) == "completed":
|
||||
verdict = "完成"
|
||||
elif String(s.get("result", "")) == "abandoned":
|
||||
verdict = "放弃"
|
||||
lines.append("最近冒险:%s · 完成 %d 关 · 钥石 +%d · 种子 %d" % [verdict,
|
||||
int(s.get("levels_cleared", 0)), int(s.get("keystone_total", 0)), int(s.get("master_seed", 0))])
|
||||
_side._recent_list.text = " | ".join(lines)
|
||||
|
||||
|
||||
func _emit_closed() -> void:
|
||||
closed.emit()
|
||||
|
||||
|
||||
func _emit_buy_requested(attr: String) -> void:
|
||||
buy_requested.emit(attr)
|
||||
@@ -1 +0,0 @@
|
||||
uid://dvg2wmjh87p8a
|
||||
@@ -0,0 +1,17 @@
|
||||
## 营地刷新单口:各写口尾→现开界面刷新+场景根 HUD 刷新一闸完成(卡⑤ HUD 同口挂接)。
|
||||
## 非 autoload、非信号广播——组合根单点推送(组合根建件绑册,presenter 经 context 持引用;
|
||||
## 卡④ 契约 4:写口语义零改,只改路由形态)。
|
||||
class_name CampRefreshBus
|
||||
extends RefCounted
|
||||
|
||||
var _targets: Array[Control] = [] # 可刷面册(四子界面+卡⑤ HUD 栏——均带 refresh_camp(data))
|
||||
|
||||
|
||||
func bind_targets(targets: Array[Control]) -> void:
|
||||
_targets = targets
|
||||
|
||||
|
||||
func refresh(camp_data: Dictionary) -> void:
|
||||
for t in _targets:
|
||||
if t != null and t.visible and t.has_method("refresh_camp"):
|
||||
t.refresh_camp(camp_data) # 现开界面就地重刷(未开的零开销——留底形态下场景根恒开)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdfmyxf87n6f6
|
||||
@@ -0,0 +1,309 @@
|
||||
## 营地场景图视图(交互式营地):10 层贴图铺底+七物件命中区+点选反馈四态+顶栏预留位。
|
||||
## 布局唯一事实源 = db.presentation.camp_scene_layout(构造注入,本视图禁自载表路径);
|
||||
## 子节点序 = 表内数组序 = z 序(Control 根+TextureRect 层——禁 CanvasLayer/z_index/Sprite2D)。
|
||||
## 输入单点:根 _gui_input 集中命中判定(层节点全 IGNORE),点击只发 object_pressed 信号,路由归组合根。
|
||||
## 零核心写调用:视图不读 MetaProgression,禁用态由组合根经 set_feedback_state 推送。
|
||||
class_name CampSceneView
|
||||
extends Control
|
||||
|
||||
signal object_pressed(key: StringName) ## 命中区点击确认(press+release 同区且非禁用;组合根路由开界面/动线)
|
||||
|
||||
## 名牌文案(camp.md §场景构成绑定表——七可点件按界面名逐字)
|
||||
const _HOTSPOT_NAMES := {
|
||||
&"gate": "冒险", &"forge": "锻造工坊", &"chest": "秘宝匣", &"dummy": "天赋树",
|
||||
&"board": "奖励格图鉴", &"notebook": "生涯记录", &"campfire": "离开营地",
|
||||
}
|
||||
## 层键→纹理路径(单点 = UiKit.TEX_CAMP_*,本表只引用不自写路径;缺位 load_tex→null→程序化占位)
|
||||
const _TEX_BY_KEY := {
|
||||
&"bg": UiKit.TEX_CAMP_BG, &"wall": UiKit.TEX_CAMP_WALL, &"board": UiKit.TEX_CAMP_BOARD,
|
||||
&"gate": UiKit.TEX_CAMP_GATE, &"dummy": UiKit.TEX_CAMP_DUMMY, &"forge": UiKit.TEX_CAMP_FORGE,
|
||||
&"tent": UiKit.TEX_CAMP_TENT, &"chest": UiKit.TEX_CAMP_CHEST,
|
||||
&"notebook": UiKit.TEX_CAMP_NOTEBOOK, &"campfire": UiKit.TEX_CAMP_CAMPFIRE,
|
||||
}
|
||||
## 禁用态去饱和着色器(modulate 只能调明度——降明度+去饱和走 canvas_item shader,零新素材)
|
||||
const _DESAT_SHADER_SRC := "
|
||||
shader_type canvas_item;
|
||||
uniform float strength : hint_range(0.0, 1.0) = 1.0;
|
||||
void fragment() {
|
||||
vec4 tex = texture(TEXTURE, UV);
|
||||
float g = dot(tex.rgb, vec3(0.299, 0.587, 0.114));
|
||||
tex.rgb = mix(tex.rgb, vec3(g) * 0.72, strength);
|
||||
COLOR = tex * COLOR;
|
||||
}
|
||||
"
|
||||
|
||||
enum FeedbackState { NORMAL, HOVER, PRESSED, DISABLED } ## 点选反馈四态(camp.md §点选反馈)
|
||||
|
||||
var _layout: CampSceneLayoutTable # 布局表(坐标唯一事实源)
|
||||
var _layer_nodes: Dictionary[StringName, Control] = {} # 层键→TextureRect/ColorRect 占位(子节点序=z 序)
|
||||
var _hotspot_rects: Dictionary[StringName, Rect2] = {} # 命中键→Rect2(视图局部坐标=表坐标)
|
||||
var _hotspot_layer: Dictionary[StringName, StringName] = {} # 命中键→层键
|
||||
var _base_states: Dictionary[StringName, int] = {} # 命中键→FeedbackState 值(持久基态——组合根置 DISABLED/NORMAL)
|
||||
var _outline: Panel # 悬停轮廓亮边
|
||||
var _name_label: Label # 物件名牌(下方 8px 居中,画面内钳制)
|
||||
var _hover_key := &"" # 当前悬停命中键(&""=无)
|
||||
var _press_key := &"" # 当前按住命中键
|
||||
var _float_tween: Tween # 悬停浮动补间(逐级整像素吸附,禁 _process 逐帧移 UI)
|
||||
var _desat_mat: ShaderMaterial # 禁用态共享材质(strength 恒 1,材质挂/摘承载开关)
|
||||
var _hud_slot: Control # 顶栏预留位(卡⑤ set_hud_band 填值)
|
||||
var hud_band: Control # 顶栏 HUD 栏句柄(卡⑤ CampHudBand——装配后 bus 目标册取用面)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
visible = false # 登记模态初始隐(开合走栈——模态纪律;boot 态场景根不得盖菜单)
|
||||
|
||||
|
||||
func setup_layers(layout: CampSceneLayoutTable) -> void:
|
||||
if layout == null:
|
||||
push_error("[CampSceneView] 布局表缺位——组合根须注入 db.presentation.camp_scene_layout")
|
||||
return
|
||||
_layout = layout
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP # 场景图=营地语境主交互面(模态层在兄弟位先截事件)
|
||||
_build_desat_material()
|
||||
for spec in layout.layers:
|
||||
var node: Control = _build_layer_node(spec)
|
||||
add_child(node)
|
||||
_layer_nodes[spec.key] = node
|
||||
_build_outline()
|
||||
_build_name_label()
|
||||
_build_hud_slot(layout)
|
||||
mouse_exited.connect(_on_mouse_exited) # 悬出根控件=清 hover/press(防态悬挂)
|
||||
|
||||
|
||||
func bind_hotspots(layout: CampSceneLayoutTable) -> void:
|
||||
if layout == null:
|
||||
return
|
||||
for spec in layout.layers:
|
||||
if spec.is_clickable():
|
||||
_hotspot_rects[spec.hotspot_key] = spec.hotspot
|
||||
_hotspot_layer[spec.hotspot_key] = spec.key
|
||||
_base_states[spec.hotspot_key] = FeedbackState.NORMAL
|
||||
|
||||
|
||||
func get_hotspot_rect(key: StringName) -> Rect2:
|
||||
var rect: Rect2 = _hotspot_rects.get(key, Rect2())
|
||||
return rect
|
||||
|
||||
|
||||
func set_feedback_state(key: StringName, state: FeedbackState) -> void:
|
||||
if not _base_states.has(key):
|
||||
return
|
||||
_base_states[key] = state # 持久基态只收 NORMAL/DISABLED;HOVER/PRESSED 为视图内瞬态
|
||||
_apply_state(key, _base_state(key))
|
||||
|
||||
|
||||
func set_hud_band(band: Control) -> void:
|
||||
if _hud_slot != null:
|
||||
_hud_slot.add_child(band)
|
||||
hud_band = band
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseMotion:
|
||||
_handle_motion(event as InputEventMouseMotion)
|
||||
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_handle_button(event as InputEventMouseButton)
|
||||
accept_event()
|
||||
|
||||
|
||||
func _handle_motion(event: InputEventMouseMotion) -> void:
|
||||
var key: StringName = _topmost_hotspot_at(event.position)
|
||||
if key != _hover_key:
|
||||
if _hover_key != &"":
|
||||
_apply_state(_hover_key, _base_state(_hover_key))
|
||||
_hover_key = key
|
||||
if key != &"":
|
||||
_apply_state(key, FeedbackState.HOVER)
|
||||
if _press_key != &"" and key != _press_key:
|
||||
_apply_state(_press_key, _base_state(_press_key)) # 按住拖出=取消按压
|
||||
_press_key = &""
|
||||
|
||||
|
||||
func _handle_button(event: InputEventMouseButton) -> void:
|
||||
var hit: StringName = _topmost_hotspot_at(event.position)
|
||||
if event.pressed:
|
||||
if hit != &"":
|
||||
_press_key = hit
|
||||
_apply_state(hit, FeedbackState.PRESSED)
|
||||
return
|
||||
if _press_key == &"":
|
||||
return
|
||||
if hit == _press_key and not _is_disabled(_press_key):
|
||||
object_pressed.emit(_press_key)
|
||||
_apply_state(_press_key, _base_state(_press_key))
|
||||
_press_key = &""
|
||||
|
||||
|
||||
func _build_layer_node(spec: CampSceneLayerSpec) -> Control:
|
||||
var node: Control = null
|
||||
var tex: Texture2D = UiKit.load_tex(_TEX_BY_KEY.get(spec.key, ""))
|
||||
if tex != null:
|
||||
var tr := TextureRect.new()
|
||||
tr.texture = tex
|
||||
tr.stretch_mode = TextureRect.STRETCH_SCALE
|
||||
node = tr
|
||||
else:
|
||||
var ph := ColorRect.new() # 程序化占位(素材缺位=冷检出半同步态,结构在位冒烟安全)
|
||||
ph.color = Color(1.0, 1.0, 1.0, 0.06)
|
||||
node = ph
|
||||
node.name = "Layer_%s" % spec.key
|
||||
node.position = spec.texture_rect.position
|
||||
node.size = spec.texture_rect.size
|
||||
node.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
return node
|
||||
|
||||
|
||||
func _build_desat_material() -> void:
|
||||
_desat_mat = ShaderMaterial.new()
|
||||
var shader := Shader.new()
|
||||
shader.code = _DESAT_SHADER_SRC
|
||||
_desat_mat.shader = shader
|
||||
|
||||
|
||||
func _build_outline() -> void:
|
||||
_outline = Panel.new()
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.draw_center = false
|
||||
sb.border_color = UiKit.CANDLE
|
||||
sb.set_border_width_all(2)
|
||||
sb.set_corner_radius_all(4)
|
||||
_outline.add_theme_stylebox_override("panel", sb)
|
||||
_outline.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_outline.visible = false
|
||||
add_child(_outline)
|
||||
|
||||
|
||||
func _build_name_label() -> void:
|
||||
_name_label = UiKit.label("", UiKit.FS_HINT, false, UiKit.INK_ON_DEEP)
|
||||
var plate := StyleBoxFlat.new()
|
||||
plate.bg_color = UiKit.BG_ROOT
|
||||
plate.set_corner_radius_all(4)
|
||||
plate.content_margin_left = 8.0
|
||||
plate.content_margin_right = 8.0
|
||||
plate.content_margin_top = 2.0
|
||||
plate.content_margin_bottom = 2.0
|
||||
_name_label.add_theme_stylebox_override("normal", plate)
|
||||
_name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE # 名牌不挡命中区
|
||||
_name_label.visible = false
|
||||
add_child(_name_label)
|
||||
|
||||
|
||||
func _build_hud_slot(layout: CampSceneLayoutTable) -> void:
|
||||
_hud_slot = Control.new()
|
||||
_hud_slot.name = "HudBandSlot"
|
||||
_hud_slot.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||||
_hud_slot.custom_minimum_size = Vector2(0, layout.hud_band_h) # 48px 浮层覆盖,不挤压画面
|
||||
_hud_slot.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_hud_slot)
|
||||
|
||||
|
||||
func _topmost_hotspot_at(pos: Vector2) -> StringName:
|
||||
if _layout == null:
|
||||
return &""
|
||||
var idx: int = _layout.layers.size() - 1 # 逆序=自最上 z 向下
|
||||
while idx >= 0:
|
||||
var spec: CampSceneLayerSpec = _layout.layers[idx]
|
||||
if spec.is_clickable() and _hotspot_rects.get(spec.hotspot_key, Rect2()).has_point(pos):
|
||||
return spec.hotspot_key
|
||||
idx -= 1
|
||||
return &""
|
||||
|
||||
|
||||
func _base_state(key: StringName) -> FeedbackState:
|
||||
var st: int = _base_states.get(key, FeedbackState.NORMAL)
|
||||
return FeedbackState.DISABLED if st == FeedbackState.DISABLED else FeedbackState.NORMAL
|
||||
|
||||
|
||||
func _is_disabled(key: StringName) -> bool:
|
||||
return _base_state(key) == FeedbackState.DISABLED
|
||||
|
||||
|
||||
func _apply_state(key: StringName, state: FeedbackState) -> void:
|
||||
var layer_key: StringName = _hotspot_layer.get(key, &"")
|
||||
var node: Control = _layer_nodes.get(layer_key, null)
|
||||
if node == null:
|
||||
return
|
||||
var rect: Rect2 = _hotspot_rects.get(key, Rect2())
|
||||
var base_y: float = node.position.y - _float_offset_of(key) # 还原到基准位再施新态
|
||||
_set_float_offset(key, 0)
|
||||
if _float_tween != null:
|
||||
_float_tween.kill()
|
||||
_float_tween = null
|
||||
match state:
|
||||
FeedbackState.NORMAL:
|
||||
node.material = null
|
||||
_outline.visible = false
|
||||
_name_label.visible = false
|
||||
FeedbackState.HOVER:
|
||||
_apply_hover(key, node, rect)
|
||||
FeedbackState.PRESSED:
|
||||
node.position.y = base_y + 1.0 # 按下=位移 1px
|
||||
FeedbackState.DISABLED:
|
||||
_apply_disabled(node)
|
||||
|
||||
|
||||
func _apply_hover(key: StringName, node: Control, rect: Rect2) -> void:
|
||||
if _is_disabled(key):
|
||||
_apply_disabled(node)
|
||||
return
|
||||
node.material = null
|
||||
_outline.position = rect.position
|
||||
_outline.size = rect.size
|
||||
_outline.visible = true
|
||||
_show_name_plate(key, rect)
|
||||
_run_float(key) # 悬停浮动(整像素逐级吸附)
|
||||
|
||||
|
||||
func _apply_disabled(node: Control) -> void:
|
||||
node.material = _desat_mat # 降明度+去饱和(shader;仅锻炉/宝箱会有此态)
|
||||
_outline.visible = false
|
||||
_name_label.visible = false
|
||||
|
||||
|
||||
func _run_float(key: StringName) -> void:
|
||||
_float_tween = create_tween()
|
||||
for step in 4:
|
||||
var off := -1 - step
|
||||
_float_tween.tween_callback(_set_float_offset.bind(key, off)).set_delay(UiKit.motion(0.03))
|
||||
# 悬离时由 _apply_state(NORMAL) 先归零位——本补间只承载升浮
|
||||
|
||||
|
||||
func _set_float_offset(key: StringName, off: int) -> void:
|
||||
var layer_key: StringName = _hotspot_layer.get(key, &"")
|
||||
var node: Control = _layer_nodes.get(layer_key, null)
|
||||
if node == null or _layout == null:
|
||||
return
|
||||
for spec in _layout.layers:
|
||||
if spec.key == layer_key:
|
||||
node.position.y = spec.texture_rect.position.y + float(off)
|
||||
return
|
||||
|
||||
|
||||
func _float_offset_of(key: StringName) -> float:
|
||||
var layer_key: StringName = _hotspot_layer.get(key, &"")
|
||||
var node: Control = _layer_nodes.get(layer_key, null)
|
||||
if node == null or _layout == null:
|
||||
return 0.0
|
||||
for spec in _layout.layers:
|
||||
if spec.key == layer_key:
|
||||
return node.position.y - spec.texture_rect.position.y
|
||||
return 0.0
|
||||
|
||||
|
||||
func _show_name_plate(key: StringName, rect: Rect2) -> void:
|
||||
_name_label.text = String(_HOTSPOT_NAMES.get(key, ""))
|
||||
_name_label.visible = true
|
||||
_name_label.reset_size()
|
||||
var pos := Vector2.ZERO
|
||||
pos.x = clampf(rect.get_center().x - _name_label.size.x / 2.0, 0.0, _layout.canvas.x - _name_label.size.x)
|
||||
pos.y = minf(rect.end.y + 8.0, _layout.canvas.y - _name_label.size.y) # 贴底物件防出画(画面内钳制)
|
||||
_name_label.position = pos
|
||||
|
||||
|
||||
func _on_mouse_exited() -> void:
|
||||
if _hover_key != &"":
|
||||
_apply_state(_hover_key, _base_state(_hover_key))
|
||||
_hover_key = &""
|
||||
if _press_key != &"":
|
||||
_apply_state(_press_key, _base_state(_press_key))
|
||||
_press_key = &""
|
||||
@@ -0,0 +1 @@
|
||||
uid://btsrctv5ddx16
|
||||
@@ -0,0 +1,92 @@
|
||||
## 营地子界面模态壳基类(锻造工坊/秘宝匣/天赋树/奖励格图鉴四子界面公共形态):
|
||||
## 实底全屏暖亮档+石纹暗角+顶栏(铭牌+钥石徽)+内容列+底栏返回钮。
|
||||
## 开合可见性归模态栈(登记层开合一律走栈);返回钮只发 closed——关层编排归 presenter(组合根单口)。
|
||||
## 余额徽随子类 refresh_camp 刷新(钥石读数=投影单口,禁第二账本)。
|
||||
class_name CampSubViewBase
|
||||
extends Control
|
||||
|
||||
signal closed
|
||||
|
||||
var _balance_label: Label # 钥石徽数值行(探针直读面——ui_probe 余额文案断言改指新界面)
|
||||
|
||||
|
||||
## 壳构建(子类 _init 调用一次):content 由子类先建好传入(列容器/滚动容器皆可)
|
||||
func _build_shell(title: String, icon_path: String, content: Control) -> void:
|
||||
# 实底暖亮档(半透 dim 会透出底下场景图——CampOverlay 同案;遮蔽语义由组合根输入锁兜底)
|
||||
var dim := UiKit._modal_dim(self, UiKit.BG_BRIGHT)
|
||||
var bd := UiKit.backdrop_tile()
|
||||
if bd != null:
|
||||
dim.add_child(bd)
|
||||
dim.add_child(UiKit.vignette_rect())
|
||||
var margin := MarginContainer.new()
|
||||
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
margin.add_theme_constant_override("margin_left", 16)
|
||||
margin.add_theme_constant_override("margin_right", 16)
|
||||
margin.add_theme_constant_override("margin_top", 12)
|
||||
margin.add_theme_constant_override("margin_bottom", 12)
|
||||
dim.add_child(margin)
|
||||
var col := VBoxContainer.new()
|
||||
col.add_theme_constant_override("separation", 8)
|
||||
margin.add_child(col)
|
||||
_build_top_bar(col, title, icon_path)
|
||||
content.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
# 内容米板身(柜台行板对比底=PanelData——旧柜台面板形态延续,行板墨色对比表不脱锚)
|
||||
var body := PanelContainer.new()
|
||||
body.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 10, 10, 10)))
|
||||
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
body.add_child(content)
|
||||
col.add_child(body)
|
||||
_build_bottom_bar(col)
|
||||
visible = false # 登记模态初始隐(开合走栈)
|
||||
|
||||
|
||||
## 顶栏域:铭牌(标题)+钥石徽(余额徽单点——子类 refresh_camp 刷新数值行)
|
||||
func _build_top_bar(col: VBoxContainer, title: String, icon_path: String) -> void:
|
||||
var top := HBoxContainer.new()
|
||||
top.add_theme_constant_override("separation", 10)
|
||||
col.add_child(top)
|
||||
var plaque := PanelContainer.new()
|
||||
plaque.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(14, 8, 14, 8)))
|
||||
top.add_child(plaque)
|
||||
var head := HBoxContainer.new()
|
||||
head.add_theme_constant_override("separation", 6)
|
||||
plaque.add_child(head)
|
||||
if icon_path != "":
|
||||
var ic := UiKit.icon_rect(icon_path, 32) # 头带图标缺位 null → 纯文本回退
|
||||
if ic != null:
|
||||
head.add_child(ic)
|
||||
head.add_child(UiKit.label(title, UiKit.FS_TITLE, true, UiKit.INK_TITLE))
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
top.add_child(spacer)
|
||||
var badge := UiKit.keystone_badge("钥石 0") # UiKit 工厂单点(烛光黄底深墨字)
|
||||
top.add_child(badge)
|
||||
_balance_label = badge.value_label
|
||||
|
||||
|
||||
## 底栏域:返回营地钮(closed 信号——关层编排归 presenter)
|
||||
func _build_bottom_bar(col: VBoxContainer) -> void:
|
||||
var bottom := HBoxContainer.new()
|
||||
bottom.add_theme_constant_override("separation", 10)
|
||||
col.add_child(bottom)
|
||||
var back := UiKit.button("返回营地")
|
||||
back.pressed.connect(_emit_closed)
|
||||
bottom.add_child(back)
|
||||
var spacer2 := Control.new()
|
||||
spacer2.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
bottom.add_child(spacer2)
|
||||
|
||||
|
||||
func open_camp(data: Dictionary) -> void:
|
||||
refresh_camp(data)
|
||||
visible = true # 开可见性由栈承载(本行为开层数据投喂的收尾拍)
|
||||
|
||||
|
||||
func refresh_camp(data: Dictionary) -> void:
|
||||
pass # 子类覆写(余额徽刷新+各自行板/门判据)
|
||||
|
||||
|
||||
func _emit_closed() -> void:
|
||||
closed.emit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dv0gyjyrtoxkh
|
||||
@@ -173,7 +173,7 @@ func _draw_south_facade(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
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_hl, rect.size.x, FACADE_HIGHLIGHT_PX), UiKit.BRICK, true) # 口沿高光(合并归掩尘枚 BRICK——art-bible §4.1 换色合并①同值单点)
|
||||
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)
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
## 奖励格图鉴子界面(自 CampOverlay 图鉴柜台抽出)——占位壳:
|
||||
## 内容保持挂起(用户令冻结,重启须用户另行发令——卡④ 契约 5),点进看占位、不是禁用态。
|
||||
## 锁文照录原柜台锁文并随形态订正『柜台』称谓(L2 记录——零新文案)。
|
||||
class_name CodexView
|
||||
extends CampSubViewBase
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var holder := VBoxContainer.new()
|
||||
holder.custom_minimum_size = Vector2(0, 336)
|
||||
var lock_l := UiKit.label(" —— 结构位预留:内容排 Alpha(地图战斗资源差异化批)", UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
lock_l.custom_minimum_size = Vector2(280, 0)
|
||||
holder.add_child(lock_l)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.custom_minimum_size = Vector2(300, 336)
|
||||
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
scroll.add_child(holder)
|
||||
_build_shell("奖励格图鉴", UiKit.TEX_ICON_ATLAS, scroll)
|
||||
|
||||
|
||||
func refresh_camp(data: Dictionary) -> void:
|
||||
_balance_label.text = "钥石 %d" % int(data.get("balance", 0)) # 内容挂起——余额徽随刷(占位态无行板)
|
||||
@@ -0,0 +1 @@
|
||||
uid://m1bcfjiceyiq
|
||||
@@ -1,14 +1,18 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 通用确认弹窗:open(text, confirm_label, cancel_label) 双钮外发 confirmed/cancelled。
|
||||
## 通用确认弹窗:open(text, confirm_label, cancel_label) 双钮外发 confirmed/cancelled;
|
||||
## open_three(text, confirm_label, secondary_label, cancel_label) 三钮增设 secondary_confirmed
|
||||
##(营地大门两分支——卡④ T4;双钮调用方零改动:次钮随 open 隐藏)。
|
||||
## 文案与按钮语义装配归调用方——本类零业务触碰。
|
||||
class_name ConfirmDialog
|
||||
extends Control
|
||||
|
||||
signal confirmed
|
||||
signal secondary_confirmed ## 次确认(三钮形态中钮——营地大门「开始新冒险」出口;卡④ T4)
|
||||
signal cancelled
|
||||
|
||||
var _text: Label
|
||||
var _confirm_btn: Button
|
||||
var _secondary_btn: Button
|
||||
var _cancel_btn: Button
|
||||
|
||||
|
||||
@@ -27,6 +31,9 @@ func _init() -> void:
|
||||
_cancel_btn = UiKit.button("取消")
|
||||
_cancel_btn.pressed.connect(_emit_cancelled)
|
||||
btn_row.add_child(_cancel_btn)
|
||||
_secondary_btn = UiKit.button("次选项")
|
||||
_secondary_btn.pressed.connect(_emit_secondary)
|
||||
btn_row.add_child(_secondary_btn)
|
||||
_confirm_btn = UiKit.button("确认")
|
||||
_confirm_btn.pressed.connect(_emit_confirmed)
|
||||
btn_row.add_child(_confirm_btn)
|
||||
@@ -37,12 +44,20 @@ func open(text: String, confirm_label: String, cancel_label: String = "取消")
|
||||
_text.text = text
|
||||
_confirm_btn.text = confirm_label
|
||||
_cancel_btn.text = cancel_label
|
||||
_secondary_btn.visible = false # 双钮形态(次钮隐藏——既有调用方零改动)
|
||||
# z 序纪律:层序唯一事实源=main.LAYER_CONTRACT(树序与登记序同源派生);开合单口
|
||||
# 归组合根 ModalFlow(挂起兜底/遮挡记账/开侧同步)——本类只管文案与显示,不自行抬层
|
||||
|
||||
|
||||
visible = true
|
||||
|
||||
|
||||
## 三钮形态(营地大门有断点分支——继续上一段冒险/开始新冒险/取消;Q5 裁甲)
|
||||
func open_three(text: String, confirm_label: String, secondary_label: String, cancel_label: String) -> void:
|
||||
open(text, confirm_label, cancel_label)
|
||||
_secondary_btn.text = secondary_label
|
||||
_secondary_btn.visible = true
|
||||
|
||||
|
||||
func close() -> void:
|
||||
visible = false
|
||||
|
||||
@@ -55,5 +70,9 @@ func _emit_confirmed() -> void:
|
||||
confirmed.emit()
|
||||
|
||||
|
||||
func _emit_secondary() -> void:
|
||||
secondary_confirmed.emit()
|
||||
|
||||
|
||||
func _emit_cancelled() -> void:
|
||||
cancelled.emit()
|
||||
@@ -0,0 +1,159 @@
|
||||
## 锻造工坊子界面(自 CampOverlay 锻造柜台抽出——承载面迁移,界面逻辑复用零重设计):
|
||||
## 钥匙门(树上 forge_workshop 点亮前锁定态零行板零购买路径;点亮=两区六路线行板)。
|
||||
## UI 只发意图信号(写口唯一归 MetaProgression 锻造写口族);钮禁用=常态防线(余额/未解锁/满级/已选中)。
|
||||
class_name ForgeView
|
||||
extends CampSubViewBase
|
||||
|
||||
signal forge_buy_requested(route_id: String) ## 锻造路线购买意图(写口唯一归 MetaProgression.forge_buy_level)
|
||||
signal forge_unlock_requested(route_id: String) ## 锻造路线解锁意图(写口唯一归 MetaProgression.forge_buy_unlock)
|
||||
signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select)
|
||||
|
||||
const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE
|
||||
|
||||
var _forge_lock_l: Label # 锁定态行(钥匙未点亮)
|
||||
var _forge_workbench: VBoxContainer # 工作间(钥匙点亮后展开——两区六路线行板)
|
||||
var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线)
|
||||
var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线)
|
||||
var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var holder := VBoxContainer.new()
|
||||
holder.add_theme_constant_override("separation", 6)
|
||||
holder.custom_minimum_size = Vector2(0, 336)
|
||||
_forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)", UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
_forge_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
_forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_forge_lock_l.custom_minimum_size = Vector2(280, 0)
|
||||
_forge_workbench = VBoxContainer.new()
|
||||
_forge_workbench.add_theme_constant_override("separation", 6)
|
||||
_forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建)
|
||||
_forge_zone_weapon = VBoxContainer.new()
|
||||
_forge_zone_weapon.add_theme_constant_override("separation", 4)
|
||||
_forge_zone_armor = VBoxContainer.new()
|
||||
_forge_zone_armor.add_theme_constant_override("separation", 4)
|
||||
var wb_wh := UiKit.label("── 武器锻造区 ──", UiKit.FS_HINT, true, UiKit.GOLD_INK)
|
||||
_forge_workbench.add_child(wb_wh)
|
||||
_forge_workbench.add_child(_forge_zone_weapon)
|
||||
var wb_ah := UiKit.label("── 护甲锻造区 ──", UiKit.FS_HINT, true, UiKit.GOLD_INK)
|
||||
_forge_workbench.add_child(wb_ah)
|
||||
_forge_workbench.add_child(_forge_zone_armor)
|
||||
holder.add_child(_forge_lock_l)
|
||||
holder.add_child(_forge_workbench)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.custom_minimum_size = Vector2(300, 336)
|
||||
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
scroll.add_child(holder)
|
||||
_build_shell("锻造工坊", UiKit.TEX_ICON_FORGE, scroll)
|
||||
|
||||
|
||||
func refresh_camp(data: Dictionary) -> void:
|
||||
_balance_label.text = "钥石 %d" % int(data.get("balance", 0))
|
||||
_refresh_key_gate(data)
|
||||
_rebuild_forge(data)
|
||||
|
||||
|
||||
# 工作间钥匙门(树上 forge_workshop 行 level 判据——零核心访问,纯视图读投影)
|
||||
func _refresh_key_gate(data: Dictionary) -> void:
|
||||
var lvl := 0
|
||||
for row in (data.get("tree", []) as Array):
|
||||
if String(row.get("node_id", "")) == "forge_workshop":
|
||||
lvl = int(row.get("level", 0))
|
||||
var open := lvl >= 1
|
||||
_forge_lock_l.visible = not open
|
||||
_forge_workbench.visible = open
|
||||
|
||||
|
||||
# 锻造行板重建(两区六路线;refresh_camp 调用。行结构仿秘宝匣行板:件名+等级+状态+购买/解锁/选中钮)
|
||||
func _rebuild_forge(data: Dictionary) -> void:
|
||||
for zone in [_forge_zone_weapon, _forge_zone_armor]:
|
||||
for c in zone.get_children():
|
||||
c.queue_free()
|
||||
_forge_rows.clear()
|
||||
if not _forge_workbench.visible:
|
||||
return # 锁定态(钥匙未点亮):零行板零购买路径——占位纪律延续
|
||||
var bal := int(data.get("balance", 0))
|
||||
for row in data.get("forge", []):
|
||||
var kind := String(row.get("kind", ""))
|
||||
var zone: VBoxContainer = _forge_zone_weapon if kind == "weapon" else _forge_zone_armor
|
||||
var card := _forge_route_card(row, bal)
|
||||
zone.add_child(card["card"])
|
||||
_forge_rows.append(card)
|
||||
|
||||
|
||||
## 锻造行板构建域:单路线卡(件名+等级+状态+购买/解锁/选中钮——并联双通道呈现面)
|
||||
func _forge_route_card(row: Dictionary, bal: int) -> Dictionary:
|
||||
var d := _forge_route_scalars(row)
|
||||
var card := PanelContainer.new()
|
||||
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4)))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 6)
|
||||
card.add_child(line)
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK)
|
||||
name_l.custom_minimum_size = Vector2(104, 0)
|
||||
name_l.clip_text = true
|
||||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
line.add_child(name_l)
|
||||
var lvl_l := UiKit.label("Lv%d/%d" % [d["lvl"], d["cap_v"]], 13, false, UiKit.INK_LABEL)
|
||||
lvl_l.custom_minimum_size = Vector2(44, 0)
|
||||
line.add_child(lvl_l)
|
||||
var status_l := UiKit.label(_forge_status_text(d, row), UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
status_l.clip_text = true
|
||||
status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
line.add_child(status_l)
|
||||
var btns := _forge_route_buttons(d, row, bal, line)
|
||||
return {"card": card, "route_id": d["rid"], "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l,
|
||||
"buy_btn": btns["buy_btn"], "unlock_btn": btns["unlock_btn"], "select_btn": btns["select_btn"],
|
||||
"next_price": d["next_price"], "unlock_price": d["unlock_price"]}
|
||||
|
||||
|
||||
## 锻造行标量束:路线 id/解锁态/初始持有/等级/cap/几何价/解锁价一次取数
|
||||
func _forge_route_scalars(row: Dictionary) -> Dictionary:
|
||||
return {
|
||||
"rid": String(row.get("route_id", "")),
|
||||
"unlocked": bool(row.get("unlocked", false)),
|
||||
"initial": bool(row.get("initial_owned", false)),
|
||||
"lvl": int(row.get("level", 0)),
|
||||
"cap_v": int(row.get("cap", 0)),
|
||||
"next_price": int(row.get("next_price", 0)),
|
||||
"unlock_price": int(row.get("unlock_price", 0)),
|
||||
}
|
||||
|
||||
|
||||
## 状态文案域(并联双通道:练满前一档 or 一次性解锁价直购;已满级态)
|
||||
func _forge_status_text(d: Dictionary, row: Dictionary) -> String:
|
||||
var st_text := "已解锁"
|
||||
if not d["unlocked"]:
|
||||
if d["initial"]:
|
||||
st_text = "未解锁"
|
||||
else:
|
||||
st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), d["unlock_price"]]
|
||||
elif d["lvl"] >= d["cap_v"]:
|
||||
st_text = "已满级"
|
||||
return st_text
|
||||
|
||||
|
||||
## 三钮域:购买(已解锁未满级——余额不足禁用)/解锁(待解锁非初始——余额不足禁用)/选中(已解锁——当前=禁用态标记)
|
||||
func _forge_route_buttons(d: Dictionary, row: Dictionary, bal: int, line: HBoxContainer) -> Dictionary:
|
||||
var buy_btn: Button = null
|
||||
if d["unlocked"] and d["lvl"] < d["cap_v"]:
|
||||
buy_btn = UiKit.button("锻造 %d" % d["next_price"], 0.0) # 行板紧凑钮豁免
|
||||
buy_btn.pressed.connect(forge_buy_requested.emit.bind(d["rid"]))
|
||||
buy_btn.disabled = bal < d["next_price"]
|
||||
line.add_child(buy_btn)
|
||||
var unlock_btn: Button = null
|
||||
if not d["unlocked"] and not d["initial"]:
|
||||
unlock_btn = UiKit.button("解锁 %d" % d["unlock_price"], 0.0) # 行板紧凑钮豁免
|
||||
unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(d["rid"]))
|
||||
unlock_btn.disabled = bal < d["unlock_price"]
|
||||
line.add_child(unlock_btn)
|
||||
var select_btn: Button = null
|
||||
if d["unlocked"]:
|
||||
var sel := bool(row.get("selected", false))
|
||||
select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免
|
||||
select_btn.pressed.connect(forge_select_requested.emit.bind(d["rid"]))
|
||||
select_btn.disabled = sel
|
||||
line.add_child(select_btn)
|
||||
return {"buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b77xh0e3txmh5
|
||||
@@ -59,7 +59,8 @@ func intent_probe() -> Dictionary:
|
||||
|
||||
|
||||
# 名字行探针出口(ui_probe 呈现口径 + 本批 V6/V8 断言面:armor_icon = 绑定纹理路径〔缺位回退字形〕、
|
||||
# 几何 = 素底牌矩形与文字基线、素底色——V6 判据 = baseline_y < plinth_top_y)
|
||||
# 几何 = 素底牌矩形与文字基线、素底色——V6 判据 = baseline_y < plinth_top_y;
|
||||
# name_font_size = 名字行字号档实读(迭代 46 卡B——提档 FS_HINT 14 的几何单点出口))
|
||||
func name_line_probe() -> Dictionary:
|
||||
var f := _efont()
|
||||
var tex := UiKit.load_tex(_armor_tex_path(st._armor))
|
||||
@@ -69,7 +70,8 @@ func name_line_probe() -> Dictionary:
|
||||
"armor_tex_bound": tex != null,
|
||||
"plate": plate, "baseline_y": _name_baseline(plate).y,
|
||||
"plinth_top_y": _pedestal_rect().position.y,
|
||||
"plate_color": UiKit.HP_BAR_BACK}
|
||||
"plate_color": UiKit.HP_BAR_BACK,
|
||||
"name_font_size": UiKit.FS_HINT}
|
||||
|
||||
|
||||
# ---- setter 面(presenter/组合根零改排) ----
|
||||
@@ -332,15 +334,16 @@ func _name_plate_rect(f: Font, tex: Texture2D) -> Rect2:
|
||||
var icon_w := 24.0
|
||||
if tex == null:
|
||||
icon_w = f.get_string_size(String(ARMOR_ICONS.get(st._armor, "◇")),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x
|
||||
var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT).x
|
||||
var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT).x
|
||||
var total_w := icon_w + 3.0 + name_w
|
||||
return Rect2((box.x - total_w - 8.0) * 0.5, box.y - 18.0 - 26.0, total_w + 8.0, 26.0)
|
||||
|
||||
|
||||
# 名字行文字基线(牌内几何单点——图标 24px 垂直居中牌内〔中心=牌顶+13〕,12px 字形基线=中心+4)
|
||||
# 名字行文字基线(牌内几何单点——图标 24px 垂直居中牌内〔中心=牌顶+13〕,14px 字形基线=中心+5
|
||||
# (0.35h 规则按字号档推导 14×0.35≈4.9→5,T0 实测校验——迭代 46 卡B §7.4 提档))
|
||||
func _name_baseline(plate: Rect2) -> Vector2:
|
||||
return plate.position + Vector2(4.0, 17.0)
|
||||
return plate.position + Vector2(4.0, 18.0)
|
||||
|
||||
|
||||
# 绘制内容矩形(世界局部;ui_struct_probe ⑥standee「不出框+下余量≥8px」断言读数面——
|
||||
@@ -362,8 +365,8 @@ func _draw() -> void:
|
||||
## 地面域:椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台
|
||||
## 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半)
|
||||
## + 石板地台(§6.1 近景地台独立件:TEX_STAGE_PLAZA 源 48×12 九宫格横向拉伸、顶缘受光线 2px
|
||||
## 不随拉伸变形、跨三关恒定;素材缺位回退程序化三段绘制〔STAND_STONE 族方向稿取色,几何同源
|
||||
## _pedestal_rect——与 panel_skinned 缺位回退同构〕)
|
||||
## 不随拉伸变形、跨三关恒定;素材缺位回退程序化三段绘制〔板身 STAND_STONE/描边 PLAZA_EDGE/
|
||||
## 顶缘线 BRICK——art-bible §4.1 换色合并①②改指目标色,几何同源 _pedestal_rect〕)
|
||||
func _draw_ground(f: Font) -> void:
|
||||
var sh_center := Vector2(box.x * 0.5, box.y - 24.0)
|
||||
var sh_rx := (box.x - 36.0) * 0.5
|
||||
@@ -379,8 +382,8 @@ func _draw_ground(f: Font) -> void:
|
||||
_draw_plinth_stretched(plaza, ped)
|
||||
else:
|
||||
draw_rect(ped, UiKit.STAND_STONE, true)
|
||||
draw_rect(ped, UiKit.STAND_STONE_EDGE, false, 1.0)
|
||||
draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.STAND_STONE_RIM, true)
|
||||
draw_rect(ped, UiKit.PLAZA_EDGE, false, 1.0)
|
||||
draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.BRICK, true)
|
||||
|
||||
|
||||
## 地台九宫格横向拉伸域(循环体提函数——左右 4px 端头切片 + 中段拉伸;纵向恒 12px 源档,
|
||||
@@ -464,13 +467,13 @@ func _draw_armor_name_line(f: Font) -> void:
|
||||
draw_texture_rect(tex, Rect2(baseline.x, plate.position.y + 1.0, 24.0, 24.0), false)
|
||||
else:
|
||||
var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇"))
|
||||
icon_w = f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x
|
||||
icon_w = f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT).x
|
||||
var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制)
|
||||
match st._armor:
|
||||
"ward":
|
||||
col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档
|
||||
"heavy":
|
||||
col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档
|
||||
f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION, col)
|
||||
f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, col)
|
||||
f.draw_string(get_canvas_item(), baseline + Vector2(icon_w + 3.0, 0.0), st._display_name,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION, UiKit.WARM) # v3.5 暗底双色墨制
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.WARM) # v3.5 暗底双色墨制
|
||||
@@ -0,0 +1,238 @@
|
||||
## 秘宝匣子界面(自 CampOverlay 秘宝匣柜台抽出——承载面迁移,界面逻辑复用零重设计):
|
||||
## 钥匙门(树上 ks_workshop 点亮前锁定态;点亮=12 件行板+同调装配段+Y 刷新升级行整体迁入)。
|
||||
## UI 只发意图信号(购买写口唯一归 MetaProgression.buy_treasure_unlock / toggle_attunement / buy_attune_refresh)。
|
||||
class_name TreasureView
|
||||
extends CampSubViewBase
|
||||
|
||||
signal buy_requested(attr: String) ## 秘宝购买/Y 刷新升级意图(presenter 按 attr 分派)
|
||||
signal attune_toggled(item_id: String) ## 同调装配切换意图(写口唯一归 MetaProgression.toggle_attunement)
|
||||
|
||||
const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE
|
||||
|
||||
var _ws_lock_l: Label # 锁定态行(ks_workshop 未点亮)
|
||||
var _ws_scroll: ScrollContainer # 秘宝匣内容容器(钥匙点亮后可见)
|
||||
var _workshop_holder: VBoxContainer # 秘宝匣行板(六行+同调装配段+Y 升级行;探针直读冻结)
|
||||
var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l, buy_btn, price, unlocked, card}](探针消费)
|
||||
var _attune_header_l: Label # 同调装配段(Slots 点亮呈现)
|
||||
var _attune_btns: Array[Dictionary] = [] # [{item_id, btn, unlocked, equipped}]——装配/卸下切换钮(满位禁选随刷)
|
||||
var _attune_nodes: Array[Control] = [] # 段内节点册(open 重建时定向清理,不扰工坊行板)
|
||||
var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账)
|
||||
var _refresh_btn: Button
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var holder := VBoxContainer.new()
|
||||
holder.add_theme_constant_override("separation", 6)
|
||||
holder.custom_minimum_size = Vector2(0, 336)
|
||||
_ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)", UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
_ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
_ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_ws_lock_l.custom_minimum_size = Vector2(280, 0)
|
||||
_ws_scroll = ScrollContainer.new()
|
||||
_ws_scroll.custom_minimum_size = Vector2(300, 336)
|
||||
_ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
_workshop_holder = VBoxContainer.new()
|
||||
_workshop_holder.add_theme_constant_override("separation", 6)
|
||||
_ws_scroll.add_child(_workshop_holder)
|
||||
holder.add_child(_ws_lock_l)
|
||||
holder.add_child(_ws_scroll)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.custom_minimum_size = Vector2(300, 336)
|
||||
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
scroll.add_child(holder)
|
||||
_build_shell("秘宝匣", UiKit.TEX_ICON_WORKSHOP, scroll)
|
||||
|
||||
|
||||
func open_camp(data: Dictionary) -> void:
|
||||
_rebuild_workshop(data) # 行板+同调装配段+Y 尾行随开层全量重建(段尾链 _rebuild_attune)
|
||||
super.open_camp(data)
|
||||
|
||||
|
||||
func refresh_camp(data: Dictionary) -> void:
|
||||
var balance := int(data.get("balance", 0))
|
||||
_balance_label.text = "钥石 %d" % balance
|
||||
_refresh_key_gate(data)
|
||||
_refresh_workshop_rows(data, balance)
|
||||
_refresh_attune_segment(data, balance)
|
||||
|
||||
|
||||
# 秘宝匣钥匙门(树上 ks_workshop 行 level 判据——零核心访问,纯视图读投影)
|
||||
func _refresh_key_gate(data: Dictionary) -> void:
|
||||
var lvl := 0
|
||||
for row in (data.get("tree", []) as Array):
|
||||
if String(row.get("node_id", "")) == "ks_workshop":
|
||||
lvl = int(row.get("level", 0))
|
||||
var open := lvl >= 1
|
||||
_ws_lock_l.visible = not open
|
||||
_ws_scroll.visible = open
|
||||
|
||||
|
||||
func _rebuild_workshop(data: Dictionary) -> void:
|
||||
_workshop_rows.clear()
|
||||
for child in _workshop_holder.get_children():
|
||||
child.queue_free()
|
||||
_attune_nodes.clear()
|
||||
_attune_btns.clear()
|
||||
_attune_header_l = null
|
||||
_refresh_l = null
|
||||
_refresh_btn = null
|
||||
for row in data.get("workshop", []):
|
||||
var card := _workshop_card(row)
|
||||
_workshop_holder.add_child(card["card"])
|
||||
_workshop_rows.append(card)
|
||||
_rebuild_attune(data)
|
||||
|
||||
|
||||
## 秘宝匣行板构建域:件名(分档后缀+效果摘要悬停)+进度列+状态列+钥石购买钮(购买件未解锁在列)
|
||||
func _workshop_card(row: Dictionary) -> Dictionary:
|
||||
var card := PanelContainer.new()
|
||||
card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4)))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 12)
|
||||
card.add_child(line)
|
||||
var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), ""))
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""), UiKit.FS_HINT, true, UiKit.INK)
|
||||
name_l.custom_minimum_size = Vector2(132, 0)
|
||||
name_l.clip_text = true
|
||||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀)
|
||||
line.add_child(name_l)
|
||||
var progress_l := UiKit.label("", UiKit.FS_HINT, false, UiKit.STONE_DEEP)
|
||||
progress_l.custom_minimum_size = Vector2(150, 0)
|
||||
progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜
|
||||
progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
line.add_child(progress_l)
|
||||
# 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式
|
||||
var status_l := UiKit.label("", UiKit.FS_HINT, true, UiKit.STONE_DEEP)
|
||||
status_l.clip_text = true # 「已解锁 · 能力封存中」不撑柜(余宽吸收者)
|
||||
status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
line.add_child(status_l)
|
||||
# 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock,
|
||||
# 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足)
|
||||
var buy_btn: Button = null
|
||||
if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone":
|
||||
buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免
|
||||
buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", ""))))
|
||||
line.add_child(buy_btn)
|
||||
return {"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false)), "card": card}
|
||||
|
||||
|
||||
# 同调装配段重建(秘宝匣尾部段:Slots 头行+九件行板+Y 升级尾行)
|
||||
func _rebuild_attune(data: Dictionary) -> void:
|
||||
var att: Dictionary[Variant, Variant] = data.get("attune", {})
|
||||
_attune_header_l = UiKit.label("", UiKit.FS_HINT, true, UiKit.INK)
|
||||
_workshop_holder.add_child(_attune_header_l)
|
||||
_attune_nodes.append(_attune_header_l)
|
||||
for row in att.get("items", []):
|
||||
var line := _attune_item_line(row)
|
||||
_workshop_holder.add_child(line)
|
||||
_attune_nodes.append(line)
|
||||
_refresh_attune_tail()
|
||||
|
||||
|
||||
## 同调件行板构建域:件名+形态+档位+装配切换钮(未解锁禁装——toggle 写口同守卫)
|
||||
func _attune_item_line(row: Dictionary) -> HBoxContainer:
|
||||
var iid := String(row.get("item_id", ""))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 6)
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK)
|
||||
name_l.custom_minimum_size = Vector2(96, 0)
|
||||
name_l.clip_text = true
|
||||
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
line.add_child(name_l)
|
||||
var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能", UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||||
form_l.custom_minimum_size = Vector2(58, 0)
|
||||
line.add_child(form_l)
|
||||
# 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边)
|
||||
var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")), UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||||
tier_l.custom_minimum_size = Vector2(36, 0)
|
||||
line.add_child(tier_l)
|
||||
var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免
|
||||
btn.pressed.connect(attune_toggled.emit.bind(iid))
|
||||
btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫)
|
||||
line.add_child(btn)
|
||||
_attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))})
|
||||
return line
|
||||
|
||||
|
||||
## Y 升级尾行构建域(双入口之一——树上 ks_refresh 同账)
|
||||
func _refresh_attune_tail() -> void:
|
||||
var refresh_line := HBoxContainer.new()
|
||||
refresh_line.add_theme_constant_override("separation", 6)
|
||||
_refresh_l = UiKit.label("", UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)
|
||||
refresh_line.add_child(_refresh_l)
|
||||
_refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免
|
||||
_refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh"))
|
||||
refresh_line.add_child(_refresh_btn)
|
||||
_workshop_holder.add_child(refresh_line)
|
||||
_attune_nodes.append(refresh_line)
|
||||
|
||||
|
||||
## 秘宝匣行板刷新域:进度列(解锁混合制三形态)与状态色(亮板双色墨制)与购买钮余额联动
|
||||
func _refresh_workshop_rows(data: Dictionary, balance: int) -> void:
|
||||
var ws_by_id := {}
|
||||
for row in data.get("workshop", []):
|
||||
ws_by_id[String(row.get("item_id", ""))] = row
|
||||
for card in _workshop_rows:
|
||||
var row: Dictionary[Variant, Variant] = ws_by_id.get(String(card.item_id), {})
|
||||
if row.is_empty():
|
||||
continue
|
||||
var unlocked := bool(row.get("unlocked", false))
|
||||
card.progress_l.text = _workshop_progress_text(row, unlocked)
|
||||
var st_text := "未解锁"
|
||||
var st_col: Color = UiKit.STONE_DEEP
|
||||
if unlocked:
|
||||
st_text = "已解锁"
|
||||
st_col = UiKit.GOLD_INK # GOLD_DEEP 对行板 3.00:1 越带,换深金墨 5.84:1
|
||||
card.status_l.text = st_text
|
||||
card.status_l.add_theme_color_override("font_color", st_col)
|
||||
card.status_l.tooltip_text = st_text # 截断兜底(状态列 EXPAND_FILL 吸余宽——悬停看全文)
|
||||
# 购买钮余额联动(禁用态=余额不足,常态防线;pattern 1 兜底拒绝可见归 presenter)
|
||||
if card.get("buy_btn") != null:
|
||||
card.buy_btn.disabled = balance < int(card.get("price", 0))
|
||||
|
||||
|
||||
## 进度列文案域(解锁混合制三形态:钥石购买价/条件进度拼接/已入手)
|
||||
func _workshop_progress_text(row: Dictionary, unlocked: bool) -> String:
|
||||
if unlocked:
|
||||
return "已入手"
|
||||
var mode := String(row.get("mode", ""))
|
||||
if mode == "keystone":
|
||||
return "钥石 %d" % int(row.get("price", 0))
|
||||
if mode == "condition":
|
||||
var parts: Array[String] = []
|
||||
for req_v in (row.get("reqs", []) as Array):
|
||||
var req: Dictionary[Variant, Variant] = req_v as Dictionary
|
||||
var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件"))
|
||||
parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))])
|
||||
return "、".join(parts)
|
||||
return ""
|
||||
|
||||
|
||||
## 同调装配段刷新域(Slots 点亮/满位禁选/Y 升级行余额联动——就地刷)
|
||||
func _refresh_attune_segment(data: Dictionary, balance: int) -> void:
|
||||
var att: Dictionary[Variant, Variant] = data.get("attune", {})
|
||||
if _attune_header_l != null:
|
||||
var slots_n := int(att.get("slots", 0))
|
||||
var eq: Array[Variant] = att.get("equipped", [])
|
||||
_attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n]
|
||||
var eq_set := {}
|
||||
for v in eq:
|
||||
eq_set[String(v)] = true
|
||||
for card2 in _attune_btns:
|
||||
var on: bool = eq_set.has(String(card2.item_id))
|
||||
card2.btn.text = "卸下" if on else "装配"
|
||||
# 满位禁选+未解锁禁装(已装件恒可卸下;unlocked 快照随 rebuild 刷新)
|
||||
card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false)))
|
||||
if _refresh_l != null:
|
||||
var rf: Dictionary[Variant, Variant] = att.get("refresh", {})
|
||||
_refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))]
|
||||
_refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0))
|
||||
_refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0))
|
||||
|
||||
|
||||
func _emit_buy_requested(attr: String) -> void:
|
||||
buy_requested.emit(attr)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dcakccwlacegq
|
||||
@@ -0,0 +1,105 @@
|
||||
## 天赋树子界面(自 CampOverlay 树淬炼柜台抽出——承载面迁移,界面逻辑复用零重设计):
|
||||
## 树脊柱界面形态=主界面(脉分组行板:节点名/等级/价/购买钮+Fork 双态切换钮段)。
|
||||
## UI 只发意图信号(写口唯一归 MetaProgression.tree_buy/tree_buy_fork/tree_toggle_fork)。
|
||||
class_name TreeView
|
||||
extends CampSubViewBase
|
||||
|
||||
signal tree_buy_requested(node_id: String) ## 天赋树购买意图(写口唯一归 MetaProgression.tree_buy)
|
||||
signal tree_fork_toggled(group: String, side: String) ## 双态切换意图(一节点双态——零币切态)
|
||||
|
||||
var _tree_holder: VBoxContainer # 树淬炼内容(脊柱两行+钥匙/socket 行板+Fork 双态钮段)
|
||||
var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费)
|
||||
var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_tree_holder = VBoxContainer.new()
|
||||
_tree_holder.add_theme_constant_override("separation", 6)
|
||||
var tree_scroll := ScrollContainer.new()
|
||||
tree_scroll.custom_minimum_size = Vector2(300, 336)
|
||||
tree_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
tree_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
tree_scroll.add_child(_tree_holder)
|
||||
var content := VBoxContainer.new()
|
||||
content.add_child(tree_scroll)
|
||||
_build_shell("天赋树 · 淬炼", "", content)
|
||||
|
||||
|
||||
func refresh_camp(data: Dictionary) -> void:
|
||||
_balance_label.text = "钥石 %d" % int(data.get("balance", 0))
|
||||
_rebuild_tree(data)
|
||||
|
||||
|
||||
# 天赋树段重建(脉分组行板:节点名/等级/价/购买钮 + Fork 双态切换钮;
|
||||
# 树数据 = 组合根 _camp_data().tree 投影 [{node_id, display_name, vein, kind, level, price, can_buy,
|
||||
# currency}...] + fork_groups [{group, node_id, active_side, owned, can_buy, price, sides:[{side, display_name, effects}]}])
|
||||
func _rebuild_tree(data: Dictionary) -> void:
|
||||
for c in _tree_holder.get_children():
|
||||
c.queue_free()
|
||||
_tree_buy_btns.clear()
|
||||
_tree_fork_btns.clear()
|
||||
var tree: Array[Variant] = data.get("tree", []) as Array
|
||||
var forks: Array[Variant] = data.get("fork_groups", []) as Array
|
||||
var vein_names := {"spine": "·脊柱淬炼·", "forge": "·锻造脉·", "keystone": "·钥石脉·", "attune": "·同调脉·"}
|
||||
var last_vein := ""
|
||||
for row in tree:
|
||||
var vein := String(row.get("vein", ""))
|
||||
if vein != last_vein:
|
||||
last_vein = vein
|
||||
var vh := UiKit.label(String(vein_names.get(vein, vein)), UiKit.FS_HINT, true, UiKit.GOLD_INK) # GOLD_DEEP 对柜台板 2.67:1 越带,换深金墨 5.21:1
|
||||
_tree_holder.add_child(vh)
|
||||
if String(row.get("kind", "")) == "fork":
|
||||
continue # Fork 行去行板化——由 fork_groups 段承载双态切换钮
|
||||
_tree_holder.add_child(_tree_row_line(row))
|
||||
for fg in forks:
|
||||
_tree_holder.add_child(_tree_fork_line(fg))
|
||||
|
||||
|
||||
## 树行板构建域:节点名(含门槛标记)+购买钮(追加序与锚句随段整体搬迁)
|
||||
func _tree_row_line(row: Dictionary) -> HBoxContainer:
|
||||
var hb := HBoxContainer.new()
|
||||
hb.add_theme_constant_override("separation", 6)
|
||||
var lvl := int(row.get("level", 0))
|
||||
var cap := int(row.get("cap", 1))
|
||||
var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL)
|
||||
name_l.custom_minimum_size = Vector2(150, 0)
|
||||
name_l.clip_text = true
|
||||
var gate := String(row.get("gate_text", ""))
|
||||
if gate != "":
|
||||
name_l.text += " 🔒" # 等级阈值前置未达标标记(R1 机制位——悬停看门槛;达标后随 refresh 消失)
|
||||
name_l.tooltip_text = "等级门槛:%s" % gate
|
||||
hb.add_child(name_l)
|
||||
var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准
|
||||
btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", ""))))
|
||||
btn.disabled = not bool(row.get("can_buy", false))
|
||||
hb.add_child(btn)
|
||||
_tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn})
|
||||
return hb
|
||||
|
||||
|
||||
## Fork 双态行板构建域:组状态文案+解锁钮或双态切换钮
|
||||
func _tree_fork_line(fg: Dictionary) -> HBoxContainer:
|
||||
var group := String(fg.get("group", ""))
|
||||
var active := String(fg.get("active_side", ""))
|
||||
var owned := bool(fg.get("owned", false))
|
||||
var fb := HBoxContainer.new()
|
||||
fb.add_theme_constant_override("separation", 6)
|
||||
var state_txt := "未解锁"
|
||||
if owned:
|
||||
state_txt = "激活:%s" % active
|
||||
var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL)
|
||||
fb.add_child(gl)
|
||||
if not owned:
|
||||
var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免
|
||||
bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", ""))))
|
||||
bb.disabled = not bool(fg.get("can_buy", false))
|
||||
fb.add_child(bb)
|
||||
_tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb})
|
||||
else:
|
||||
for side_row in (fg.get("sides", []) as Array):
|
||||
var sd := String(side_row.get("side", ""))
|
||||
var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免
|
||||
sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd))
|
||||
fb.add_child(sb)
|
||||
_tree_fork_btns.append({"group": group, "side": sd, "btn": sb})
|
||||
return fb
|
||||
@@ -0,0 +1 @@
|
||||
uid://mjjrdelhi7yg
|
||||
@@ -185,8 +185,7 @@ const TIP_BG := Color(0.16, 0.12, 0.09, 0.96) # tooltip 深底
|
||||
const STAGE_BG := Color8(62, 52, 42) # B 区舞台暗底(方向稿 render_battle_screen——battle_stage_view 面板底)
|
||||
const BEAM_LIGHT := Color8(255, 236, 200) # B 区顶光柱暖光(用时乘 alpha 渐隐——battle_stage_view 光柱层)
|
||||
const STAND_STONE := Color8(150, 130, 100) # 站桩石板地台·板身(standee_body 方向稿取色)
|
||||
const STAND_STONE_EDGE := Color8(90, 76, 58) # 站桩石板地台·描边
|
||||
const STAND_STONE_RIM := Color8(196, 172, 132) # 站桩石板地台·顶缘受光线
|
||||
const PLAZA_EDGE := Color8(76, 64, 56) # 小石台描边(art-bible §4.1 换色合并②合并目标——贴图已落此值,缺位回退同值)
|
||||
const CAMP_HEADER_BAND := Color8(110, 69, 38) # 营地柜台头带深棕 #6E4526(4-camp.png 方向稿取色;对齐 demo --wood-deep)
|
||||
const CAMP_ROW_PLATE := Color8(240, 230, 206) # 营地行板暖米(同上)
|
||||
const CAMP_BADGE_PANEL := Color8(40, 32, 24) # 营地钥石徽暗底(同上)
|
||||
@@ -248,6 +247,19 @@ const TEX_ENEMY_HOOK_HORROR := "res://assets/art/character/character_enemy_hook_
|
||||
const TEX_ENEMY_DUERGAR_KING := "res://assets/art/character/character_enemy_duergar_king.png" # 灰矮人石君王
|
||||
# 近景地台独立件(§6.1——源 48×12 九宫格横向拉伸、顶缘受光线 2px 不随拉伸变形;跨三关恒定不吃 wall_ambient tint)
|
||||
const TEX_STAGE_PLAZA := "res://assets/art/scene/scene_stage_plaza.png"
|
||||
# 营地场景十层(art-bible §6.2——z0 bg 不透明全幅+z1 围墙透明细条+八物件透明层;布局矩形/命中矩形/z 序
|
||||
# 唯一事实源 = data/tables/camp_scene_layout.tres;素材名跟本组常量走(先定常量防断链);
|
||||
# 缺位 load_tex→null→程序化占位,冒烟安全)
|
||||
const TEX_CAMP_BG := "res://assets/art/scene/scene_camp_bg.png" # z0 背景层(不透明全幅 1280×800)
|
||||
const TEX_CAMP_WALL := "res://assets/art/scene/scene_camp_wall.png" # z1 围墙层(透明细条,门洞留缺口)
|
||||
const TEX_CAMP_BOARD := "res://assets/art/scene/scene_camp_board.png" # z2 布告板
|
||||
const TEX_CAMP_GATE := "res://assets/art/scene/scene_camp_gate.png" # z3 门扇
|
||||
const TEX_CAMP_DUMMY := "res://assets/art/scene/scene_camp_dummy.png" # z4 木桩
|
||||
const TEX_CAMP_FORGE := "res://assets/art/scene/scene_camp_forge.png" # z4 锻炉铁砧
|
||||
const TEX_CAMP_TENT := "res://assets/art/scene/scene_camp_tent.png" # z4 帐篷(承载件不可点)
|
||||
const TEX_CAMP_CHEST := "res://assets/art/scene/scene_camp_chest.png" # z5 宝箱
|
||||
const TEX_CAMP_NOTEBOOK := "res://assets/art/scene/scene_camp_notebook.png" # z5 笔记本
|
||||
const TEX_CAMP_CAMPFIRE := "res://assets/art/scene/scene_camp_campfire.png" # z6 篝火(命中只取下半火堆)
|
||||
# 护甲类型图标三键(art-bible §5.3 工单:对象键 = 甲档变体;源 24×24 显示 24px = 1× 甲类整数倍;
|
||||
# 缺位 load_tex→null→字形回退,冒烟安全)
|
||||
const TEX_ARMOR_LIGHT := "res://assets/ui/icons/ui_armor_light.png"
|
||||
|
||||
@@ -6,15 +6,16 @@
|
||||
extends Node
|
||||
|
||||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # 隔离样板单点
|
||||
const BoardViewScript := preload("res://presentation/views/board_view.gd") # 算式单点静态调用载体(迭代 46 卡C)
|
||||
|
||||
# 分辨率档位表(逻辑视口尺寸;数据侧——增删档位只改这里,断言代码零改动)
|
||||
const VIEWPORTS := [
|
||||
{"label": "基座 1280x800", "size": Vector2i(1280, 800)},
|
||||
{"label": "16:9 全屏 1920x1080", "size": Vector2i(1920, 1080)},
|
||||
]
|
||||
const MIN_CELL := 36.0 # 格子可读门槛(battle.md §棋盘格尺寸公式 1280 档 12×12 → 36px)
|
||||
|
||||
var _problems: PackedStringArray = []
|
||||
var _cell_reads: Dictionary = {} # 逐档格边长实读留痕(label→cell_size;PASS 行引基座值)
|
||||
var _est_reads: Dictionary = {} # 逐档 13/14 板推定读数留痕(label→{13,14,avail};PASS 后打印对拍)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -41,8 +42,12 @@ func _ready() -> void:
|
||||
# 复原本项目基座(探针不该把进程留在末档尺寸)
|
||||
get_tree().root.content_scale_size = Vector2i(1280, 800)
|
||||
if _problems.is_empty():
|
||||
print("[layout_probe] PASS — %d 档几何不变量全过(三区非零/不越视口/互不重叠/view 含于 D 区/格子 ≥%.0fpx)"
|
||||
% [VIEWPORTS.size(), MIN_CELL])
|
||||
print("[layout_probe] PASS — %d 档几何不变量全过+格边长等价成立(基座实读 %.0fpx)〔格边长 V1〕"
|
||||
% [VIEWPORTS.size(), float(_cell_reads.get("基座 1280x800", -1.0))])
|
||||
for label in _est_reads:
|
||||
var e: Dictionary = _est_reads[label]
|
||||
print("[layout_probe] [%s] 13 板推定 %dpx/14 板推定 %dpx(实板可用面积 %s)〔格边长 V4〕"
|
||||
% [label, int(e["13"]), int(e["14"]), str(e["avail"])])
|
||||
get_tree().quit(0)
|
||||
else:
|
||||
for p in _problems:
|
||||
@@ -91,5 +96,25 @@ func _check_one(label: String, main: Node) -> void:
|
||||
_problems.append("[%s] 顶层区重叠 %s×%s(%s ∩ %s)"
|
||||
% [label, keys[i], keys[j], rects[keys[i]], rects[keys[j]]])
|
||||
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])
|
||||
_cell_reads[label] = cell
|
||||
# 格边长接线守卫——判据=等价(生产写入值与单点算式严格相等,生产接线断开即红;
|
||||
# 全仓禁任何人造格边长可读阈值)。
|
||||
# 板型 N 无公开出口→`bv.get("_board")` 内部字段直查=受控例外(改动—验收对照表行 1 注明理由)。
|
||||
var b = bv.get("_board")
|
||||
if b == null:
|
||||
_problems.append("[%s] _board 为 null(板型 N 无从取值,等价断言无依据)〔格边长 V1〕" % label)
|
||||
elif cell != float(BoardViewScript._cell_for(maxi(int(b.width), int(b.height)), bv.size)):
|
||||
_problems.append("[%s] 格边长等价破——生产写入 %.1fpx ≠ _cell_for(n=%d, avail=%s) 返回 %.1fpx(实板可用面积 %s)〔格边长 V1〕"
|
||||
% [label, cell, maxi(int(b.width), int(b.height)), str(bv.size),
|
||||
float(BoardViewScript._cell_for(maxi(int(b.width), int(b.height)), bv.size)), str(bv.size)])
|
||||
# 最大板推定留痕(迭代 46 卡C T2——实板可用面积代入 13/14 板型求算式单点,读数打印不作判红:
|
||||
# 锚点=用户验收的现行渲染值;对拍归实现报告,对拍不一致=卡 §9 L3 #2 呈报,禁实现侧改锚;
|
||||
# 零尺寸前置=可用面积不可信时推定无依据)
|
||||
if bv.size.x <= 0.0 or bv.size.y <= 0.0:
|
||||
_problems.append("[%s] board_view 零尺寸(可用面积不可信,格边长推定无依据)〔格边长 V4〕" % label)
|
||||
else:
|
||||
_est_reads[label] = {
|
||||
"13": BoardViewScript._cell_for(13, bv.size),
|
||||
"14": BoardViewScript._cell_for(14, bv.size),
|
||||
"avail": str(bv.size),
|
||||
}
|
||||
@@ -27,6 +27,7 @@ func _ready() -> void:
|
||||
await _step_boot_menu()
|
||||
await _step_menu_to_camp()
|
||||
await _step_camp_back_to_menu()
|
||||
await _step_camp_esc_and_hud() # 卡⑤——Esc 四态路由/三出口/设置语境分支/HUD 读数
|
||||
await _step_save_select_open_back() # 迭代 33 卡③ sl10——选档开合+返回路径+ESC 语境不响应
|
||||
await _step_menu_to_adventure()
|
||||
await _step_level_to_battle_and_back()
|
||||
@@ -51,7 +52,7 @@ func _ready() -> void:
|
||||
func _screen_seals() -> Dictionary:
|
||||
return {
|
||||
"菜单": 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.ui.modals.camp.scene != null and main.ui.modals.camp.scene.visible,
|
||||
"冒险": main.run_scope != null and main.run_scope.is_adventure,
|
||||
"终局": main.ui.modals.end_overlay != null and main.ui.modals.end_overlay.visible,
|
||||
}
|
||||
@@ -115,14 +116,14 @@ func _step_menu_to_camp() -> void:
|
||||
_expect_exclusive("②菜单→营地", "营地")
|
||||
|
||||
|
||||
# ---- ③ 营地 → 菜单(回退路径:营地关闭发自覆盖层自身信号)----
|
||||
# ---- ③ 营地 → 菜单(回退路径:篝火二次确认动线——卡④ 营地唯一回菜单出口;选档层随关层收闭同收)----
|
||||
func _step_camp_back_to_menu() -> void:
|
||||
if main.ui.modals.camp_overlay == null:
|
||||
_problems.append("③营地→菜单:营地覆盖层句柄缺失")
|
||||
if main.ui.modals.camp.scene == null:
|
||||
_problems.append("③营地→菜单:营地场景根句柄缺失〔句柄子容器批 V1〕")
|
||||
return
|
||||
main.ui.modals.camp_overlay.closed.emit()
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"campfire")
|
||||
await _frames()
|
||||
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
|
||||
main.ui.modals.confirm_dialog.confirmed.emit() # 二次确认离场(回主菜单——卡④ T4)
|
||||
await _frames()
|
||||
_expect_exclusive("③营地→菜单(回退)", "菜单")
|
||||
|
||||
@@ -229,3 +230,72 @@ func _step_end_back_to_menu() -> void:
|
||||
_expect_exclusive("⑦终局→菜单", "菜单")
|
||||
if main.run_scope != null and main.run_scope.is_adventure:
|
||||
_problems.append("⑦终局→菜单:冒险态未收束(run_scope 仍标 is_adventure)")
|
||||
|
||||
|
||||
# ---- ③' 营地 Esc 浮窗+HUD(卡⑤——真实键盘合成路径 push_input;四态路由+三出口+语境分支+余额单口)----
|
||||
func _step_camp_esc_and_hud() -> void:
|
||||
ProbeUiUtil.isolate_save(main, "smoke_screen_flow_esc", false, false, true)
|
||||
if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_esc"):
|
||||
_problems.append("[esc] 营地入口缺失(选档层新档钮驱动中断)〔ESC批 V1〕")
|
||||
return
|
||||
await _frames()
|
||||
_push_esc()
|
||||
await _frames()
|
||||
if not main.ui.modals.camp.esc_overlay.visible:
|
||||
_problems.append("[esc] 营地根态 Esc 未唤起浮窗(契约 9③)〔ESC批 V1〕")
|
||||
_push_esc()
|
||||
await _frames()
|
||||
if main.ui.modals.camp.esc_overlay.visible:
|
||||
_problems.append("[esc] 再按 Esc 未关浮窗(Q2 同键单义切换失守)〔ESC批 V1〕")
|
||||
_push_esc(true) # echo 连发(真实键盘长按形态)
|
||||
await _frames()
|
||||
if main.ui.modals.camp.esc_overlay.visible:
|
||||
_problems.append("[esc] echo 事件唤起浮窗(触发沿 pressed 且非 echo 失守)〔ESC批 V1〕")
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 子界面开态
|
||||
await _frames()
|
||||
_push_esc()
|
||||
await _frames()
|
||||
if main.ui.modals.camp.esc_overlay.visible or not main.ui.modals.camp.tree.visible:
|
||||
_problems.append("[esc] 子界面开态 Esc 反馈(契约 9① 无反馈失守)〔ESC批 V1〕")
|
||||
main.ui.modals.camp.tree.closed.emit()
|
||||
await _frames()
|
||||
# HUD 余额(契约 5 时机③——夹具直写后显式触发才变;禁 band 自刷假设)
|
||||
main.meta.meta_state._add_keystone(9999)
|
||||
main._camp_pres.views.refresh_bus.refresh(main._camp_data()) # 显式触发刷新单口
|
||||
var band: Control = main.ui.modals.camp.scene.get_node("HudBandSlot").get_child(0)
|
||||
if not String(band._badge.value_label.text).begins_with("钥石 9999"):
|
||||
_problems.append("[esc] HUD 余额读数≠存档单口现值(直写+显式触发未变)〔ESC批 V6〕")
|
||||
# 设置开在浮窗之上→Esc 无反馈且浮窗不误关(契约 9①);关设置回浮窗不回主菜单(契约 3 语境分支)
|
||||
_push_esc()
|
||||
await _frames()
|
||||
main._on_esc_settings()
|
||||
await _frames()
|
||||
if not main.ui.modals.settings_overlay.visible:
|
||||
_problems.append("[esc] 浮窗设置出口断裂〔ESC批 V4〕")
|
||||
_push_esc()
|
||||
await _frames()
|
||||
if not main.ui.modals.camp.esc_overlay.visible:
|
||||
_problems.append("[esc] 设置开态 Esc 误关浮窗(契约 9①——浮窗不隔层关闭)〔ESC批 V1〕")
|
||||
main._on_settings_closed()
|
||||
await _frames()
|
||||
if not main.ui.modals.camp.esc_overlay.visible or main.ui.screens.start_overlay.visible:
|
||||
_problems.append("[esc] 关设置后未回浮窗或回弹主菜单(契约 3 语境分支失守)〔ESC批 V4〕")
|
||||
# 返回主菜单动线(契约 4——浮窗自收+营地隐+选档收+菜单显)
|
||||
main.ui.modals.camp.esc_overlay.return_requested.emit()
|
||||
await _frames()
|
||||
if main.ui.modals.camp.esc_overlay.visible or main.ui.modals.camp.scene.visible or main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("[esc] 返回主菜单动线失守(浮窗自收/营地隐/选档收/菜单显)〔ESC批 V3〕")
|
||||
# 非营地语境(boot 菜单)Esc→无响应(契约 9④ 回归面)
|
||||
_push_esc()
|
||||
await _frames()
|
||||
if main.ui.modals.camp.esc_overlay.visible:
|
||||
_problems.append("[esc] 非营地语境 Esc 唤起浮窗(契约 9④ 失守)〔ESC批 V1〕")
|
||||
|
||||
|
||||
func _push_esc(echo := false) -> void:
|
||||
var ev := InputEventKey.new()
|
||||
ev.keycode = KEY_ESCAPE
|
||||
ev.physical_keycode = KEY_ESCAPE
|
||||
ev.pressed = true
|
||||
ev.echo = echo
|
||||
main.get_viewport().push_input(ev, true) # 真实键盘合成路径(:153 先例——push_input 不更新 Input 单例动作态)
|
||||
@@ -53,7 +53,7 @@ func _ready() -> void:
|
||||
var top_ry := -1
|
||||
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.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp.scene, 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
|
||||
@@ -119,17 +119,15 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("生涯页关闭后页未隐藏或开始层未恢复")
|
||||
# ② 营地(进档开层新路径——卡② mn9;槽模板注入隔离命名空间)
|
||||
# ② 营地场景图(进档开层新路径——卡② mn9;截图=可点击场景图形态——卡④ 契约 9 V1 验收载体)
|
||||
if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"):
|
||||
_problems.append("营地入口缺失(选档层新档钮——新路径驱动中断)")
|
||||
_problems.append("营地入口缺失(选档层新档钮——新路径驱动中断)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
await get_tree().process_frame
|
||||
await _snap("camp")
|
||||
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
|
||||
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态)
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"campfire") # 收闭改接篝火动线(营地根态无关闭钮——卡④ T5)
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.confirm_dialog.confirmed.emit() # 二次确认离场(关层收闭——选档层随收)
|
||||
await get_tree().process_frame
|
||||
# ③ 关卡画面(开始冒险→停点出图——入口停点语义;函数直调中间态 mn10)
|
||||
main._start_adventure_from_menu()
|
||||
|
||||
+106
-53
@@ -62,123 +62,131 @@ func _ready() -> void:
|
||||
# 原 swap=false 形态的写盘走 iso 实例旧路径,与槽文件断言不同源——卡② mn9 换代)
|
||||
var iso_c = ProbeUiUtil.isolate_save(main, "smoke_click_probe_camp", false, false, true)
|
||||
if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_click_probe_camp"):
|
||||
_problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)")
|
||||
_problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.camp_overlay.visible:
|
||||
_problems.append("营地层未打开(进档开层新路径)")
|
||||
elif main.ui.modals.camp_overlay._tree_buy_btns.is_empty():
|
||||
_problems.append("树淬炼柜台购买钮缺失(点击路径——四柜台重排后树=主界面)")
|
||||
if not main.ui.modals.camp.scene.visible:
|
||||
_problems.append("营地场景根未打开(进档开层新路径)〔句柄子容器批 V1〕")
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 场景点选真实路由(木桩→天赋树子界面——卡④ T5)
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.camp.tree._tree_buy_btns.is_empty():
|
||||
_problems.append("树淬炼购买钮缺失(四柜台拆解后树=独立子界面)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
# 树钮句柄用后即取(refresh_camp 随刷重建树行板——旧钮 queue_free,跨刷新句柄必失效)
|
||||
iso_c.meta_state._add_keystone(9999)
|
||||
main.ui.modals.camp_overlay.refresh_camp(main._camp_data())
|
||||
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
var buy_btn: Button = null
|
||||
for card in main.ui.modals.camp_overlay._tree_buy_btns:
|
||||
for card in main.ui.modals.camp.tree._tree_buy_btns:
|
||||
if String(card.node_id) == "sp_hp":
|
||||
buy_btn = card.btn
|
||||
if buy_btn == null:
|
||||
_problems.append("脊柱生命行缺失(点击路径)")
|
||||
_problems.append("脊柱生命行缺失(点击路径)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
var bal_before: int = iso_c.meta_state.keystone_balance()
|
||||
var price: int = int(main.db.crossrun.meta_tree.row_by_id("sp_hp").get("price", 0))
|
||||
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
|
||||
await get_tree().process_frame
|
||||
if int(iso_c.meta_state.keystone_balance()) != bal_before - price:
|
||||
_problems.append("营地树购买未出账(%d→%d 应 −%d)" % [bal_before, iso_c.meta_state.keystone_balance(), price])
|
||||
_problems.append("营地树购买未出账(%d→%d 应 −%d)〔句柄子容器批 V1〕" % [bal_before, iso_c.meta_state.keystone_balance(), price])
|
||||
var iso_chk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp_slot_1.json")
|
||||
iso_chk.load_from_disk()
|
||||
if int(iso_chk.meta_state.tree_state().nodes.get("sp_hp", 0)) != 1:
|
||||
_problems.append("营地树购买未持久(reload 后级 != 1)")
|
||||
_problems.append("营地树购买未持久(reload 后级 != 1)〔句柄子容器批 V1〕")
|
||||
# Fork 双态切换点击路径(V20——购=解锁默认 A,钮切 B;前置=同调位 1,先点亮)
|
||||
var socket_btn: Button = null
|
||||
for card in main.ui.modals.camp_overlay._tree_buy_btns:
|
||||
for card in main.ui.modals.camp.tree._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.ui.modals.camp_overlay.refresh_camp(main._camp_data())
|
||||
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
var fork_btn: Button = null
|
||||
for card in main.ui.modals.camp_overlay._tree_buy_btns:
|
||||
for card in main.ui.modals.camp.tree._tree_buy_btns:
|
||||
if String(card.node_id) == "fork_posture":
|
||||
fork_btn = card.btn
|
||||
if fork_btn == null:
|
||||
_problems.append("Fork 解锁钮缺失(点击路径)")
|
||||
_problems.append("Fork 解锁钮缺失(点击路径)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
fork_btn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp_overlay.refresh_camp(main._camp_data())
|
||||
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
var side_b: Button = null
|
||||
for fk in main.ui.modals.camp_overlay._tree_fork_btns:
|
||||
for fk in main.ui.modals.camp.tree._tree_fork_btns:
|
||||
if String(fk.group) == "fg_posture" and String(fk.side) == "b":
|
||||
side_b = fk.btn
|
||||
if side_b == null:
|
||||
_problems.append("Fork 双态切换钮缺失(解锁后应展开 a/b 钮)")
|
||||
_problems.append("Fork 双态切换钮缺失(解锁后应展开 a/b 钮)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
side_b.pressed.emit() # 合成切态点击 → _on_camp_fork_toggle → meta.tree_toggle_fork
|
||||
await get_tree().process_frame
|
||||
var iso_fk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp_slot_1.json")
|
||||
iso_fk.load_from_disk()
|
||||
if String(iso_fk.meta_state.tree_state().fork_sides.get("fg_posture", "")) != "b":
|
||||
_problems.append("Fork 切态未持久(reload 后激活态 != b)")
|
||||
# ---- 营地六处灰字取色断言(迭代36 wc4——真实入口开营地后按文本锚取 label,
|
||||
# font_color 逐处 == STONE_DEEP;六源位 :167 图鉴锁文/:417+:476 行状态字/:509 同调形态/:513 档位标签/:525 刷新行)----
|
||||
var camp_labels: Array = []
|
||||
_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 定位锚失守)")
|
||||
for l in atlas_locks:
|
||||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||||
_problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)")
|
||||
# 「未解锁」锚收窄到秘宝匣行容器子树(树行/锻造行也有「未解锁」态字样——非 wc4 目标位)
|
||||
var locked_status: Array = []
|
||||
if main.ui.modals.camp_overlay.get("_workshop_holder") != null:
|
||||
var ws_labels: Array = []
|
||||
_collect_labels_by_text(main.ui.modals.camp_overlay._workshop_holder, ws_labels)
|
||||
locked_status = _labels_with_text(ws_labels, "未解锁")
|
||||
_problems.append("Fork 切态未持久(reload 后激活态 != b)〔句柄子容器批 V1〕")
|
||||
# ---- 营地灰字取色断言(迭代36 wc4——四子界面拆分后按视图分扫;卡④ T5 迁移):
|
||||
# 图鉴锁文→codex 视图(锚随形态订正去『柜台』);秘宝行未解锁/同调形态/档位/刷新行→treasure 视图
|
||||
# _workshop_holder 子树。font_color 逐处 == STONE_DEEP;树链段已闭树界面。
|
||||
main.ui.modals.camp.tree.closed.emit() # 树界面返回(关层归 presenter 单口)
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"chest") # 宝箱→秘宝匣(灰字四源位宿主)
|
||||
await get_tree().process_frame
|
||||
var ws_labels: Array = []
|
||||
_collect_labels_by_text(main.ui.modals.camp.treasure._workshop_holder, ws_labels)
|
||||
var locked_status: Array = _labels_with_text(ws_labels, "未解锁")
|
||||
if locked_status.is_empty():
|
||||
_problems.append("营地灰字:秘宝匣行「未解锁」态 label 未找到(wc4 夹具前提失守——新档应有锁定秘宝行)")
|
||||
_problems.append("营地灰字:秘宝匣行「未解锁」态 label 未找到(wc4 夹具前提失守——新档应有锁定秘宝行)〔句柄子容器批 V1〕")
|
||||
for l in locked_status:
|
||||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||||
_problems.append("营地灰字:秘宝行「未解锁」态未用 STONE_DEEP(wc4 :417/:476)")
|
||||
_problems.append("营地灰字:秘宝行「未解锁」态未用 STONE_DEEP(wc4 :417/:476)〔句柄子容器批 V1〕")
|
||||
var form_found := false
|
||||
for l in camp_labels:
|
||||
for l in ws_labels:
|
||||
if l.text == "被动" or l.text == "主动·充能":
|
||||
form_found = true
|
||||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||||
_problems.append("营地灰字:同调形态标签未用 STONE_DEEP(wc4 :509)")
|
||||
_problems.append("营地灰字:同调形态标签未用 STONE_DEEP(wc4 :509)〔句柄子容器批 V1〕")
|
||||
if not form_found:
|
||||
_problems.append("营地灰字:同调行形态标签未找到(wc4 定位锚失守——同调段应有行)")
|
||||
_problems.append("营地灰字:同调行形态标签未找到(wc4 定位锚失守——同调段应有行)〔句柄子容器批 V1〕")
|
||||
var tier_vals := {}
|
||||
for v in UiKit.TREASURE_TIER_LABELS.values():
|
||||
tier_vals[String(v)] = true
|
||||
var tier_found := false
|
||||
for l in camp_labels:
|
||||
for l in ws_labels:
|
||||
if tier_vals.has(l.text):
|
||||
tier_found = true
|
||||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||||
_problems.append("营地灰字:同调档位标签未用 STONE_DEEP(wc4 :513)")
|
||||
_problems.append("营地灰字:同调档位标签未用 STONE_DEEP(wc4 :513)〔句柄子容器批 V1〕")
|
||||
if not tier_found:
|
||||
_problems.append("营地灰字:同调档位标签未找到(wc4 定位锚失守)")
|
||||
_problems.append("营地灰字:同调档位标签未找到(wc4 定位锚失守)〔句柄子容器批 V1〕")
|
||||
var refresh_inked := false
|
||||
for l in camp_labels:
|
||||
for l in ws_labels:
|
||||
if String(l.text).contains("三选一刷新"):
|
||||
refresh_inked = true
|
||||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||||
_problems.append("营地灰字:刷新行未用 STONE_DEEP(wc4 :525)")
|
||||
_problems.append("营地灰字:刷新行未用 STONE_DEEP(wc4 :525)〔句柄子容器批 V1〕")
|
||||
if not refresh_inked:
|
||||
_problems.append("营地灰字:刷新行 label 未找到(wc4 定位锚失守)")
|
||||
main._on_camp_closed()
|
||||
_problems.append("营地灰字:刷新行 label 未找到(wc4 定位锚失守)〔句柄子容器批 V1〕")
|
||||
main.ui.modals.camp.treasure.closed.emit()
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"board") # 布告板→图鉴占位(锁文宿主)
|
||||
await get_tree().process_frame
|
||||
var codex_labels: Array = []
|
||||
_collect_labels_by_text(main.ui.modals.camp.codex, codex_labels)
|
||||
var atlas_locks := _labels_with_text(codex_labels, "结构位预留")
|
||||
if atlas_locks.is_empty():
|
||||
_problems.append("营地灰字:图鉴锁文 label 未找到(wc4 定位锚失守——锚随形态订正去『柜台』)〔句柄子容器批 V1〕")
|
||||
for l in atlas_locks:
|
||||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||||
_problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)〔句柄子容器批 V1〕")
|
||||
main.ui.modals.camp.codex.closed.emit()
|
||||
await get_tree().process_frame
|
||||
main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3)
|
||||
await get_tree().process_frame
|
||||
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.camp_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("营地关闭后未恢复开始层")
|
||||
if main.ui.modals.camp.scene.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("营地关闭后未恢复开始层〔句柄子容器批 V1〕")
|
||||
# ---- 选档界面行为走查(迭代 33 卡③):sl5 空槽新档链/sl6 有档槽进档链/sl7 删档二次确认双路径
|
||||
# /sl8 消费方分派单口/sl12 动线序不回闪。开层=函数直调(「进入存档」钮归卡② mn3——中间态在案);
|
||||
# 隔离=attach_slot_template 注入命名空间,夹具按槽直写(卡① 注入口)。----
|
||||
@@ -208,7 +216,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.ui.modals.save_select_overlay.visible, main.ui.modals.camp_overlay.visible, main.ui.screens.start_overlay.visible])
|
||||
samples.append([main.ui.modals.save_select_overlay.visible, main.ui.modals.camp.scene.visible, main.ui.screens.start_overlay.visible])
|
||||
if samples[0][0]:
|
||||
_problems.append("[savesel] sl12 新档后选档层仍可见(落点开层未发生)")
|
||||
elif not samples[0][1]:
|
||||
@@ -217,7 +225,7 @@ 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.ui.modals.flow.stack._occluded.get(main.ui.modals.camp_overlay, [])
|
||||
var occluded: Array = main.ui.modals.flow.stack._occluded.get(main.ui.modals.camp.scene, [])
|
||||
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:
|
||||
@@ -235,7 +243,7 @@ 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.ui.modals.camp_overlay.visible or main.ui.modals.save_select_overlay.visible:
|
||||
if not main.ui.modals.camp.scene.visible or main.ui.modals.save_select_overlay.visible:
|
||||
_problems.append("[savesel] sl6 落点未开营地层或选档未隐藏")
|
||||
main._on_camp_closed()
|
||||
await get_tree().process_frame
|
||||
@@ -668,6 +676,51 @@ func _ready() -> void:
|
||||
# 卡④大门 T5 接收,两头登记禁静默删除;续玩钮已退役,主菜单确认链已摘 mn6)。
|
||||
# ① 战斗收口断点写盘断言保留(上方——冒险流直调中间态 mn10)。
|
||||
|
||||
# ---- 大门两分支断言段(卡④ T5 接收——卡② 移交落点;V3 断言面)----
|
||||
# 驱动=营地场景根点选信号(真实路由)→组合根 _on_camp_object_pressed→确认弹窗。
|
||||
# 无断点=两出口(是/否);有断点=三出口(set_breakpoint 注入选路停点形——同下方开场行③先例)。
|
||||
# 判据绑可观测提交面:点大门不直接进冒险(is_adventure 翻转只在 confirmed 后);cancelled 恒=
|
||||
# 无变更退回营地(写口零调用——余额读数不变+营地根 visible);开局后营地场景屏隐藏。
|
||||
ProbeUiUtil.isolate_save(main, "smoke_click_probe_gate", true, false, true)
|
||||
main._camp_pres.open_camp()
|
||||
await get_tree().process_frame
|
||||
var bal_gate: int = main._camp_data()["balance"]
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"gate")
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.confirm_dialog.is_open():
|
||||
_problems.append("大门无断点:确认弹窗未开(点大门应先出确认不直接进冒险)〔营地图批 V3〕")
|
||||
if main.run_scope.is_adventure:
|
||||
_problems.append("大门无断点:未确认即进冒险〔营地图批 V3〕")
|
||||
if main.ui.modals.confirm_dialog._secondary_btn.visible:
|
||||
_problems.append("大门无断点:次钮应隐藏(两出口形态)〔营地图批 V3〕")
|
||||
main.ui.modals.confirm_dialog.cancelled.emit()
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.camp.scene.visible or main.run_scope.is_adventure or main._camp_data()["balance"] != bal_gate:
|
||||
_problems.append("大门取消:未无变更退回营地(写口零调用/营地根 visible 失守)〔营地图批 V3〕")
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"gate")
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.confirm_dialog.confirmed.emit() # 「是」→开始新冒险
|
||||
await get_tree().process_frame
|
||||
if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible:
|
||||
_problems.append("大门确认:冒险未发起或营地场景屏未隐藏〔营地图批 V3〕")
|
||||
# 有断点三出口(选路停点形注入——同开场行③ set_breakpoint 先例)
|
||||
main.save_manager.set_breakpoint({
|
||||
"master_seed": 42, "rng_counter": 0, "hero_hp": 30, "gp": 10, "keystone": 0,
|
||||
"level_index": 1, "bonus": {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0},
|
||||
"shop_purchased": {}, "relics": [], "keystone_drops": [], "bonus_taken": {},
|
||||
"node_id": -1, "visited_ids": [], "graph_gen_v2": 1,
|
||||
})
|
||||
main._camp_pres.open_camp()
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"gate")
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.confirm_dialog.is_open() or not main.ui.modals.confirm_dialog._secondary_btn.visible:
|
||||
_problems.append("大门有断点:三出口形态失守(次钮应现形)〔营地图批 V3〕")
|
||||
main.ui.modals.confirm_dialog.secondary_confirmed.emit() # 「开始新冒险」出口
|
||||
await get_tree().process_frame
|
||||
if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible:
|
||||
_problems.append("大门有断点次确认:冒险未发起或营地未隐藏〔营地图批 V3〕")
|
||||
|
||||
# ---- 开场行三路径段(迭代36 wc1/wc2——真实入口驱动;隔离新档)----
|
||||
# 驱动设计(0/0 白板局生存性不成立的实证处置——L2 记录进实现报告):
|
||||
# ① 菜单开新局·白板反向=独立短局:0/0 滑条开局断言 L1 开场行零后缀(不开驱动——
|
||||
|
||||
@@ -45,15 +45,11 @@ func _pairs() -> Array:
|
||||
["商店·爬价规则墨/暖亮页底", UiKit.WARM_SOFT, UiKit.BG_BRIGHT, 1.0],# 货架面爬价规则行(page_shell 亮底)
|
||||
["商店·货架卡名墨/货架板", UiKit.INK_TITLE, UiKit.PANEL_DATA, 1.0], # bcard 货架卡商品名(换 TG 带后未机检位)
|
||||
["商店·货架价签墨/货架板", UiKit.GOLD_INK, UiKit.PANEL_DATA, 1.0], # bcard 价签「N ◈」(深金墨 5.21:1)
|
||||
["营地·等级章字/头带深棕", UiKit.INK_ON_DEEP, UiKit.CAMP_HEADER_BAND, 1.0],# 锻造行 Lv 章(木底小章亮墨)
|
||||
["营地·柜台头带字/头带深棕", UiKit.WARM, UiKit.CAMP_HEADER_BAND, 1.0], # 三柜台头带标题(暖白 bold)
|
||||
["营地·铭牌副题墨/铭牌板", UiKit.STONE_DEEP, UiKit.PANEL_DATA, 1.0],# 营地铭牌副题「跨局成长账本…」(灰石深化 5.98:1)
|
||||
["营地·行名墨/行板", UiKit.INK, UiKit.CAMP_ROW_PLATE, 1.0], # 锻造/工坊行名(墨族压暖米行板)
|
||||
["营地·行价格墨/行板", UiKit.STONE_DEEP, UiKit.CAMP_ROW_PLATE, 1.0],# 锻造行「N 钥石」价签/工坊进度行(灰石深化 6.70:1)
|
||||
["营地·树分组头墨/柜台板", UiKit.GOLD_INK, UiKit.PANEL_DATA, 1.0],# 天赋树脉分组头「·锻造脉·」(深金墨 5.21:1)
|
||||
["营地·树行名墨/柜台板", UiKit.INK_LABEL, UiKit.PANEL_DATA, 1.0], # 天赋树行「名 Lv0/1」(标签深墨)
|
||||
["营地·工坊状态墨/行板", UiKit.GOLD_INK, UiKit.CAMP_ROW_PLATE, 1.0],# 工坊状态字「已解锁」(深金墨 5.84:1·最不利现役态)
|
||||
["营地·最近冒险墨/暖亮底", UiKit.WARM_SOFT, UiKit.BG_BRIGHT, 1.0], # 底栏最近冒险条带(暖白次级)
|
||||
["设置·警示文/设置板", UiKit.INK_HINT, UiKit.PANEL_DATA, 1.0], # 设置警示文/后果说明(暖棕提示墨)
|
||||
["设置·选项标签/设置板", UiKit.INK_LABEL, UiKit.PANEL_DATA, 1.0], # 设置行标签/状态字(标签深墨)
|
||||
["三选一·停点头部墨/暗页底", UiKit.WARM_SOFT, UiKit.BG_ROOT, 1.0], # 三选一停点头部/回血反馈行(page_shell 根底档)
|
||||
|
||||
+28
-18
@@ -25,34 +25,44 @@ func _ready() -> void:
|
||||
# 营地先行段只换 meta 不换 save_manager——冒险段换注零档在前)----
|
||||
ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", false, false, false)
|
||||
if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_probe_camp"):
|
||||
problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)")
|
||||
problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.camp_overlay.visible:
|
||||
problems.append("营地层未打开(进档开层新路径)")
|
||||
if not main.ui.modals.camp.scene.visible:
|
||||
problems.append("营地场景根未打开(进档开层新路径)〔句柄子容器批 V1〕")
|
||||
elif main.ui.screens.start_overlay.visible:
|
||||
problems.append("营地层打开时开始层未隐藏(规范 2.3 遮挡纪律)")
|
||||
problems.append("营地层打开时开始层未隐藏(规范 2.3 遮挡纪律)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
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("树淬炼柜台购买钮缺失(四柜台重排后树=主界面)")
|
||||
# 天赋树子界面(场景点选真实路由——木桩命中区进树界面;卡④ T5 深员改指)
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy")
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.camp.tree.visible:
|
||||
problems.append("天赋树子界面未打开(场景点选路由中断)〔营地图批 V2〕")
|
||||
if not String(main.ui.modals.camp.tree._balance_label.text).begins_with("钥石"):
|
||||
problems.append("营地余额文案缺失(%s)〔句柄子容器批 V1〕" % main.ui.modals.camp.tree._balance_label.text)
|
||||
if main.ui.modals.camp.tree._tree_buy_btns.is_empty():
|
||||
problems.append("树淬炼购买钮缺失(四柜台拆解后树=独立子界面)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
var btn_by_id := {}
|
||||
for card2 in main.ui.modals.camp_overlay._tree_buy_btns:
|
||||
for card2 in main.ui.modals.camp.tree._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 不含余额面语义)")
|
||||
problems.append("脊柱生命 L1 白板可购钮未启用(无门槛——can_buy 不含余额面语义)〔句柄子容器批 V1〕")
|
||||
if not btn_by_id.has("att_socket2") or not bool(btn_by_id["att_socket2"].disabled):
|
||||
problems.append("同调位 2 前置未亮应禁用(点亮前置语义)")
|
||||
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()
|
||||
problems.append("同调位 2 前置未亮应禁用(点亮前置语义)〔句柄子容器批 V1〕")
|
||||
main.ui.modals.camp.tree.closed.emit()
|
||||
await get_tree().process_frame
|
||||
# 秘宝匣子界面(场景点选真实路由——宝箱命中区)
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"chest")
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.camp.treasure._workshop_rows.size() != 12:
|
||||
problems.append("秘宝匣行数错误(%d ≠ 12——迭代 22 秘宝批换代)〔句柄子容器批 V1〕" % main.ui.modals.camp.treasure._workshop_rows.size())
|
||||
main.ui.modals.camp.treasure.closed.emit()
|
||||
await get_tree().process_frame
|
||||
main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3)
|
||||
await get_tree().process_frame
|
||||
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.camp_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
problems.append("营地关闭后未恢复开始层(开合往返失守)")
|
||||
if main.ui.modals.camp.scene.visible or not main.ui.screens.start_overlay.visible:
|
||||
problems.append("营地关闭后未恢复开始层(开合往返失守)〔句柄子容器批 V1〕")
|
||||
# 换回全新零档(营地段购买态不泄入后续冒险段——确定性;先删后建防陈旧档泄入)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", true, false, true)
|
||||
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机;本探针 start_single 第五路同吃此框,确定性禁依赖默认分支)
|
||||
|
||||
@@ -39,36 +39,105 @@ func _ready() -> void:
|
||||
_settle()
|
||||
|
||||
if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_struct_probe_menu"):
|
||||
problems.append("[camp] 营地入口缺失(选档层新档钮——mn9 新路径驱动中断)")
|
||||
problems.append("[camp] 营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
_settle()
|
||||
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")
|
||||
if camp_src.count("UiKit.STONE") != camp_src.count("UiKit.STONE_DEEP"):
|
||||
problems.append("[camp] camp_overlay_view.gd 存在裸 UiKit.STONE(非 _DEEP 后缀——wc4 源扫描)")
|
||||
main._on_camp_closed()
|
||||
await _audit("camp", [main.ui.modals.camp.scene]) # 审计根=营地场景图(卡④ T5——CampOverlay 退役)
|
||||
# 营地灰字源 token 扫描(迭代36 wc4 静态半——营地域视图源内零裸 UiKit.STONE;判别式=
|
||||
# STONE 出现次数 == STONE_DEEP 出现次数;卡④ T5 改扫场景根+四子界面+公共壳六件)
|
||||
var camp_stone := 0
|
||||
var camp_stone_deep := 0
|
||||
for vf in ["camp_scene_view", "forge_view", "treasure_view", "tree_view", "codex_view", "camp_sub_view_base"]:
|
||||
var vf_src := FileAccess.get_file_as_string("res://presentation/views/%s.gd" % vf)
|
||||
camp_stone += vf_src.count("UiKit.STONE")
|
||||
camp_stone_deep += vf_src.count("UiKit.STONE_DEEP")
|
||||
if camp_stone != camp_stone_deep:
|
||||
problems.append("[camp] 营地域视图存在裸 UiKit.STONE(非 _DEEP 后缀——wc4 源扫描)〔句柄子容器批 V1〕")
|
||||
# ---- 营地场景层序段(卡④ V1①——子节点序=layout 表 z 序逐层相等、各层纹理矩形与表逐值一致;
|
||||
# 锻炉可达(V2 场景点选路由)+顶栏位在座(V8 结构)+根态反参照(V9——根态零文字按钮))----
|
||||
var layout: CampSceneLayoutTable = main.db.presentation.camp_scene_layout
|
||||
var order_ok := true
|
||||
var rect_ok := true
|
||||
for i in layout.layers.size():
|
||||
var layer_node: Control = main.ui.modals.camp.scene.get_child(i)
|
||||
if String(layer_node.name) != "Layer_%s" % layout.layers[i].key:
|
||||
order_ok = false
|
||||
if layer_node.position != layout.layers[i].texture_rect.position or layer_node.size != layout.layers[i].texture_rect.size:
|
||||
rect_ok = false
|
||||
if not order_ok:
|
||||
problems.append("[camp] 场景子节点序≠layout 表 z 序(逐层相等失守)〔营地图批 V1〕")
|
||||
if not rect_ok:
|
||||
problems.append("[camp] 层贴图矩形≠layout 表(逐值一致失守)〔营地图批 V1〕")
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"forge")
|
||||
_settle()
|
||||
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
|
||||
if not main.ui.modals.camp.forge.visible:
|
||||
problems.append("[camp] 点选锻炉→锻造子界面未开(场景点选路由中断)〔营地图批 V2〕")
|
||||
var hud_slot: Control = main.ui.modals.camp.scene.get_node_or_null("HudBandSlot")
|
||||
if hud_slot == null or hud_slot.custom_minimum_size.y != 48:
|
||||
problems.append("[camp] 顶栏 HUD 预留位缺失或高≠48(浮层覆盖不挤压画面)〔营地图批 V8〕")
|
||||
var scene_buttons := 0
|
||||
for n in main.ui.modals.camp.scene.get_children():
|
||||
if n is Button:
|
||||
scene_buttons += 1
|
||||
if scene_buttons != 0:
|
||||
problems.append("[camp] 场景根态存在文字按钮(反参照纪律——禁文字按钮列表形态)〔营地图批 V9〕")
|
||||
main.ui.modals.camp.forge.closed.emit()
|
||||
_settle()
|
||||
main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3)
|
||||
_settle()
|
||||
|
||||
# ---- 生涯页:入口点击链(合成点击真路径=pressed.emit→组合根 _open_career)→
|
||||
# ---- 生涯页:入口=帐篷旁笔记本(卡④ T3 改挂——场景点选信号真实路由→组合根 _open_career)→
|
||||
# 结构断言族→只读巡检(契约 3)+零动效契约+零跳转契约(提纲 P4 甲)→组合根真实路径关闭。
|
||||
#(登记序/树序契约统一在全模态 _modal_registry_contract——boot 段跑)----
|
||||
# 生涯页驱动=函数直调中间态(卡② mn9/mn10——careerbtn 已撤,卡④ 营地笔记本改挂前无 UI 路径)
|
||||
main._open_career()
|
||||
# 营地语境:开始层全程隐藏(笔记本入口在营地场景根——关生涯回营地不回开始层,卡④ T3 语境分支)
|
||||
main._camp_pres.open_camp() # 重开营地场景根(笔记本入口语境)
|
||||
_settle()
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"notebook")
|
||||
_settle()
|
||||
if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible:
|
||||
problems.append("[career] 生涯页未打开或开始层未隐藏(函数直调中间态断裂)")
|
||||
problems.append("[career] 生涯页未打开或开始层未隐藏(笔记本改挂路径断裂)〔营地图批 V5〕")
|
||||
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.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
problems.append("[career] 关闭后页未隐藏或开始层未恢复(模态栈单口失守)")
|
||||
if main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible:
|
||||
problems.append("[career] 关闭后页未隐藏或开始层回弹(营地语境分支失守)〔营地图批 V5〕")
|
||||
# ---- Esc 浮窗结构段(卡⑤——三钮在位且仅三+零动效契约+登记序在册+debug_badge 不波及)----
|
||||
main._camp_pres.open_camp()
|
||||
_settle()
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"gate")
|
||||
_settle()
|
||||
main.ui.modals.confirm_dialog.cancelled.emit() # 关大门弹窗(清场——浮窗段前提)
|
||||
_settle()
|
||||
var push_esc := func() -> void:
|
||||
var ev := InputEventKey.new()
|
||||
ev.keycode = KEY_ESCAPE
|
||||
ev.physical_keycode = KEY_ESCAPE
|
||||
ev.pressed = true
|
||||
main.get_viewport().push_input(ev, true)
|
||||
push_esc.call()
|
||||
_settle()
|
||||
if not main.ui.modals.camp.esc_overlay.visible:
|
||||
problems.append("[esc] 浮窗未唤起(结构段前提——营地语境 Esc 路由)〔ESC批 V1〕")
|
||||
var esc_btns := _buttons(main.ui.modals.camp.esc_overlay).size()
|
||||
if esc_btns != 3:
|
||||
problems.append("[esc] 浮窗三钮在位且仅三(实得 %d——禁第四钮)〔ESC批 V2〕" % esc_btns)
|
||||
var esc_static := FileAccess.get_file_as_string("res://presentation/views/camp_esc_overlay.gd") + FileAccess.get_file_as_string("res://presentation/main_esc_flow.gd")
|
||||
if esc_static.contains("create_tween") or esc_static.contains("UiKit.motion"):
|
||||
problems.append("[esc] 浮窗双件源码存在动效令牌(契约 6 零动效——直开直关)〔ESC批 V7〕")
|
||||
var esc_anim := _count_animplayers(main.ui.modals.camp.esc_overlay)
|
||||
if esc_anim != 0:
|
||||
problems.append("[esc] 浮窗子树 AnimationPlayer 非零(契约 6 零动效)〔ESC批 V7〕")
|
||||
if main.ui.screens.debug_badge.visible:
|
||||
problems.append("[esc] 浮窗开态 debug_badge 可见(发布态恒隐——float 类不注册 ModalStack 应零波及)〔ESC批 V8〕")
|
||||
push_esc.call()
|
||||
_settle()
|
||||
if main.ui.modals.camp.esc_overlay.visible:
|
||||
problems.append("[esc] 再按 Esc 未关浮窗(结构段收尾——Q2 单义切换)〔ESC批 V1〕")
|
||||
main._on_camp_closed() # 关营地复归菜单语境(后续选档段前提)
|
||||
_settle()
|
||||
|
||||
# ---- 选档界面(迭代 33 卡③):函数直调开层(「进入存档」钮归卡② mn3——点击链在卡② 承接),
|
||||
# sl1 三槽呈现/sl2 摘要三项逐槽读数/sl3 摘要禁项/sl4 操作集二分/sl9 重启链不预选。
|
||||
@@ -394,7 +463,7 @@ func _career_zero_motion_contract(main: Node) -> void:
|
||||
# ⑫模态登记/树序全量合同(V1——career 专属登记序契约推广到全部模态+屏壳;
|
||||
# 判据=main.ui.LAYER_CONTRACT 单一事实源:登记册序==合同模态序、直接子层树序==合同序逐对相等。
|
||||
# 两特例按合同数据表达:level_screen 宿住冒险屏壳(树序随宿主=全模态之下,遮挡语义归模态栈
|
||||
# screen_state 登记)、camp_overlay 成员与 camp_screen 屏壳同槽(屏壳树序代言成员登记位)。
|
||||
# screen_state 登记)、camp_scene 成员与 camp_screen 屏壳同槽(屏壳树序代言成员登记位)。
|
||||
# _career_registry_contract 的断言(career=状态面板后/确认弹窗前、树序同步)由本全量合同覆盖)
|
||||
# ⑭选档界面契约(卡③ sl1/sl2/sl3/sl4/sl9——呈现面全部来自喂入数据,视图零自判)
|
||||
func _save_select_contract(main: Node, tpl: String) -> void:
|
||||
@@ -516,7 +585,7 @@ func _modal_registry_contract(main: Node) -> void:
|
||||
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.ui.modals.legend_overlay.get_index()])
|
||||
# ④ 营地同槽特例:camp_screen 屏壳树序夹商店层与设置层之间(成员 camp_overlay 登记位代言)
|
||||
# ④ 营地同槽特例:camp_screen 屏壳树序夹商店层与设置层之间(成员 camp_scene 登记位代言)
|
||||
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〕" % [
|
||||
@@ -719,3 +788,13 @@ func _start_columns_contract(main: Node) -> void:
|
||||
if box.get_global_rect().intersects(right.get_global_rect()):
|
||||
problems.append("[start][columns] 左列 %s 与右列 %s 相交(双列叠放回归——M2 缺陷闸)" % [
|
||||
box.get_global_rect(), right.get_global_rect()])
|
||||
|
||||
|
||||
# AnimationPlayer 全子树收集(零动效契约扫描面——_buttons 同款递归形态)
|
||||
func _count_animplayers(node: Node) -> int:
|
||||
var out := 0
|
||||
if node is AnimationPlayer:
|
||||
out += 1
|
||||
for child in node.get_children():
|
||||
out += _count_animplayers(child)
|
||||
return out
|
||||
@@ -139,6 +139,15 @@ tests/
|
||||
| 调参测速批量模拟(逐帧步进与统计读数) | 调参测量 | `TuneSimController.run()`(调试浮窗批量模拟入口) | `tune_sim_batch_parity_test.gd:_init`(配对对拍套件,随测速循环批建) |
|
||||
| 战斗叙述与终局文案(数值带入/胜负互异) | HUD框架 | `BattleNarrator.battle_end_text()`/`BattleNarrator.boss_name()` | `battle_narrator_test.gd:_test_numeric_lines_carry_values`;`battle_narrator_test.gd:_test_end_text_differs_by_result`;`battle_narrator_test.gd:_test_boss_name_uses_boss_rowset` |
|
||||
| 玩家面文案与危险格格面字形 | HUD框架 | `BoardView.rebuild()`/`StatusSlotRow.set_effects()` | `player_copy_sweep_test.gd:_sweep_board_glyph`;`player_copy_sweep_test.gd:_sweep_legend`;`player_copy_sweep_test.gd:_sweep_hero_panel` |
|
||||
| 营地场景图十层铺底与门洞留空呈现 | 营地场景视图 | 进入存档→选档→进档→营地场景屏(`CampSceneView.setup_layers()` 驱动);单元测试载入 `camp_scene_layout.tres` | `shot_probe.gd:_ready`(camp 截图段)〔营地图批 V1〕;`camp_scene_layout_test.gd:_test_layout_hotspot_geometry`〔营地图批 V14〕 |
|
||||
| 营地七物件命中区点选与分界面可达 | 营地场景视图 | 七物件命中区 `_gui_input()` → `CampSceneView.object_pressed()` → 组合根开界面 | `ui_click_probe.gd:_ready`(七物件合成点击段)〔营地图批 V2〕 |
|
||||
| 营地大门两分支确认与断点续玩接线 | 营地交互流 | 大门命中区 → ConfirmDialog(三出口)→ `main._on_confirm_new_adventure()`/`_resume_from_menu()` | `ui_click_probe.gd:_ready`(大门两分支段/断点确认段 :500-528)〔营地图批 V3〕 |
|
||||
| 四柜台拆解后五界面入口改挂与写口零回归 | 营地五界面 | 五界面意图信号 → `CampShopPresenter` → `MetaProgression.tree_buy()` 写口 | `forge_workbench_test.gd:_test_forge_rows`〔营地图批 V4〕;`ui_click_probe.gd:_ready`(写口链段) |
|
||||
| 营地篝火回主菜单与笔记本改挂动线 | 营地交互流 | 笔记本命中区 → `CareerOverlay.open()`;篝火命中区 → ConfirmDialog 二次确认 → 主菜单 | `ui_struct_probe.gd:_ready`(career 段)〔营地图批 V5〕;`screen_flow_probe.gd:_ready`(篝火/开合段)〔营地图批 V7〕 |
|
||||
| 营地图鉴占位挂起态与反参照结构纪律 | 营地场景视图 | 布告板命中区 → `CampSceneView.object_pressed()` → 图鉴占位视图;营地根态场景树审计 | `ui_struct_probe.gd:_ready`(占位禁词/禁行段)〔营地图批 V6〕;`ui_struct_probe.gd:_ready`(结构禁词段)〔营地图批 V9〕 |
|
||||
| 营地刷新单口与场景根 HUD 同步 | 营地刷新总线 | 写口尾 → `CampRefreshBus.refresh()` → 现开界面刷新+场景根 HUD 刷新 | `ui_struct_probe.gd:_ready`(顶栏位段)〔营地图批 V8〕;`ui_click_probe.gd:_ready`(写口尾刷新段) |
|
||||
| Esc 浮窗唤起与三出口动线 | 营地 Esc 流 | 营地场景根态合成 Esc 键事件→ `main_esc_flow.on_esc_input()` → `ui.modals.flow.open/close`;浮窗三钮 `pressed` → 三出口 | `screen_flow_probe.gd:_ready`(Esc 唤起/子界面开态不唤起/再按关窗/非营地不唤起段)〔ESC批 V1〕;`screen_flow_probe.gd:_ready`(返回动线/设置双路径段)〔ESC批 V3/V4〕 |
|
||||
| 营地顶栏 HUD 钥石读数与刷新单口同步 | 营地 HUD 栏 | 隔离存档实例 `meta_state._add_keystone()`→ 显式触发 `CampRefreshBus.refresh(camp_data)` → `camp_hud_band.refresh()` → `keystone_badge` 读数 | `screen_flow_probe.gd:_ready`(HUD 余额读数/刷新段)〔ESC批 V6〕;`ui_struct_probe.gd:_ready`(顶栏位结构段) |
|
||||
|
||||
**旅程段七:数据配置与跨系统契约**
|
||||
|
||||
|
||||
@@ -188,10 +188,11 @@ func _suites() -> Array:
|
||||
{"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/standee_present_values_test.gd", "expect": 0}, # 战斗一屏样张批 V5–V12(血条危险红/名字行素底不压地台与提档 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 豁免/详情零显示/大图拟合
|
||||
{"path": "res://tests/unit/presentation/forge_workbench_test.gd", "expect": 0}, # 工作间两区六路线 UI 直验(V13 自动化前置面)
|
||||
{"path": "res://tests/unit/presentation/camp_scene_layout_test.gd", "expect": 0}, # 营地场景布局数据件校验(V14——fail-fast 判据+七命中区几何红线)
|
||||
{"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/battle_settlement/roll_multiplier_test.gd", "expect": 0}, # 掷点与乘区宿主(敌攻击类型轴/护甲掷点行为面/乘区接线/伤害类型克制与标错/fresh 池攻击链——批 B 自 smoke_all 迁入)
|
||||
|
||||
@@ -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/settlement/round_cycle.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 game/core/settlement/battle_refresh_flow.gd"
|
||||
S8_MISS=0
|
||||
for f in $S8_FILES; do
|
||||
if [ ! -f "$f" ]; then
|
||||
@@ -356,7 +356,9 @@ 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/settlement/round_cycle.gd game/core/damage_controller.gd"
|
||||
# B1 域文件集(迭代 46 卡A 增补 battle_refresh_flow——换图刷新流程件=C13 拆分边界的 B1 域拆件,
|
||||
# 承载旧资源退役/装板编排调用;architecture.md「模块归属」BattleRefreshFlow 行随反哺点落文)
|
||||
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 game/core/settlement/battle_refresh_flow.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
|
||||
|
||||
@@ -64,8 +64,8 @@ func _smoke_scene_split() -> void:
|
||||
var camp: Node = load("res://presentation/scenes/camp_screen.tscn").instantiate()
|
||||
root.add_child(camp)
|
||||
await process_frame
|
||||
check(camp.camp_overlay != null, "V2 营地屏壳独立实例化(camp_overlay 在位)")
|
||||
check(not camp.camp_overlay.visible, "V2 营地层默认隐藏(模态纪律)")
|
||||
check(camp.camp_scene != null, "V2 营地屏壳独立实例化(camp_scene 在位)")
|
||||
check(not camp.camp_scene.visible, "V2 营地层默认隐藏(模态纪律)")
|
||||
camp.queue_free()
|
||||
var adv: Node = load("res://presentation/scenes/adventure_screen.tscn").instantiate()
|
||||
root.add_child(adv)
|
||||
@@ -103,10 +103,10 @@ func _smoke_scene_split() -> void:
|
||||
chain_ok = false
|
||||
chain_detail += " %s(%d)≤%s(%d)" % [chain_prev.name, chain_prev.get_index(), node.name, node.get_index()]
|
||||
chain_prev = node
|
||||
check(chain_ok, "V1c 合同树序链逐对保序(运行期树序==LAYER_CONTRACT 序;违例:%s)〔装配生命批 V1〕" % chain_detail)
|
||||
check(chain_ok, "V1c 合同树序链逐对保序(运行期树序==LAYER_CONTRACT 序;违例:%s)〔装配生命批 V1〕〔句柄子容器批 V2〕" % chain_detail)
|
||||
# ③ 探针挂载假设句柄在座(main.X 一级路径——探针迁移面收口证明)
|
||||
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.ui.screens.start_overlay == main.ui.screens.menu_screen.start_overlay and main.ui.modals.camp.scene == main.ui.screens.camp_screen.camp_scene 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 零迁移)〔句柄子容器批 V1〕")
|
||||
check(main.run_scope != null and not main.run_scope.is_adventure,
|
||||
"V2 闲时会话自 _ready 即在(永非 null;迭代 27 T3——gp 影子账退役无账本成员)")
|
||||
main.queue_free()
|
||||
@@ -118,7 +118,7 @@ func _smoke_scene_split() -> void:
|
||||
func _topmost_visible_layer(main: Node) -> Node:
|
||||
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.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp.scene, 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
|
||||
|
||||
@@ -0,0 +1,650 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 换图系双基座七件专项宿主(迭代 46 卡A——对照表 V1–V12 按轮落地:V5=T2 全局受伤链路、
|
||||
# V2/V3=T3 两基座、V4/V10=T4 成功事实与 GP、V6–V9=T5 五件后续、V11=T6 数据与池;
|
||||
# V12 无专长基线回归归 round_flow/seal_polarity 既有断言承载)。
|
||||
# 临时承伤计数专长经现行 ConfigDB 注入(表行追加+持有集传 id——正常数据配置路径,卡片 §12);
|
||||
# 真实入口驱动:AutoPlayer 盲挖循环+停手/封印动作;判据绑可观测提交面(damage_taken 命中计数器
|
||||
# 与 target=hero 明细行)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/refresh_feats_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const ConfigDBScript = preload("res://core/config_db.gd")
|
||||
const EffectEngineScript = preload("res://core/effect_engine.gd")
|
||||
const AutoPlayer := preload("res://sim/auto_player.gd")
|
||||
|
||||
# 直接伤型资源类型(负向极性兑付=主角直伤单据的三型——reward 表 pierce/magic/heal 负向行)
|
||||
const DIRECT_TYPES := ["pierce", "magic", "heal"]
|
||||
|
||||
var _db
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_db = ConfigDBScript.new()
|
||||
if not _db.load_tables("res://data/tables", true):
|
||||
print("[refresh_feats_test] FAIL — 数值表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
_install_dt_probe()
|
||||
test_all_hero_damage_paths_emit_once()
|
||||
test_no_duplicate_damage_taken_event()
|
||||
test_reserve_on_accepted_dig()
|
||||
test_refresh_discount_gate_and_charge()
|
||||
test_double_base_stack()
|
||||
test_no_feats_baseline()
|
||||
test_board_refresh_fact_only_on_success()
|
||||
test_mine_profit_gp()
|
||||
test_stone_mitigation_all_damage()
|
||||
test_rest_rotation_cap_and_clear()
|
||||
test_charge_layers_consumed_same_round()
|
||||
test_blast_defers_to_combat_segment()
|
||||
test_seven_feats_validate_and_pool()
|
||||
quit(Kit.finish("refresh_feats_test"))
|
||||
|
||||
|
||||
# ---- 夹具族(真实入口驱动——settle_batch/stop/封印动作;直构只用于编排器受理)----
|
||||
|
||||
# 临时承伤计数专长(damage_taken → counter_add battle 桶——每张主角承伤单据恰 +1)
|
||||
func _install_dt_probe() -> void:
|
||||
if _db.crossrun.feats.row_by_id("probe_dt_counter").is_empty():
|
||||
(_db.crossrun.feats.rows as Array).append({
|
||||
"feat_id": "probe_dt_counter", "display_name": "承伤计数探针", "rarity": "common",
|
||||
"desc": "测试夹具:每次主角承伤计数 +1",
|
||||
"triggers": [{"on": "damage_taken", "do": {"kind": "counter_add", "counter_id": "dt_probe_hits", "value": 1}}],
|
||||
})
|
||||
|
||||
|
||||
func _orch(seed_v: int):
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_v)
|
||||
return load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
|
||||
|
||||
func _enc(feats: Array, voucher_rap: int = 0, armor: int = 10, hero_hp: int = 200) -> Dictionary:
|
||||
return {"level_index": 1, "node_depth": 4, "hero_hp": hero_hp, "hero_atk": 8, "hero_max_hp": 200,
|
||||
"hero_armor_class": armor, "hero_ap_bonus": 0, "attunement": [], "feats": feats, "tree_rules": [],
|
||||
"battle_index": 1, "voucher_rap": voucher_rap, "voucher_time_sand": 0, "node_type": "boss", "adv_bucket": {}}
|
||||
|
||||
|
||||
func _probe_count(orch) -> int:
|
||||
return EffectEngineScript.counter(orch.fx_host().counters_ctx, "dt_probe_hits")
|
||||
|
||||
|
||||
# 主角承伤明细行收集(type=damage 且 target=hero——敌方技与棋盘主角直伤两类单据的行面)
|
||||
func _collect_hero_rows(orch) -> Array:
|
||||
var rows: Array = []
|
||||
orch.step_row.connect(func(row):
|
||||
if String(row.get("type", "")) == "damage" and String(row.get("target", "")) == "hero":
|
||||
rows.append(row))
|
||||
return rows
|
||||
|
||||
|
||||
# 按来源类分桶计数(rows 收集器的只读后处理)
|
||||
func _count_by_source(rows: Array, source: String) -> int:
|
||||
var n := 0
|
||||
for r_v in rows:
|
||||
if String((r_v as Dictionary).get("source_class", "")) == source:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
# AutoPlayer 盲挖循环直到 cell_revealed 谓词命中(真实挖掘入口;返回 {"cell","evt"} 或空)
|
||||
func _dig_until(orch, pred: Callable) -> Dictionary:
|
||||
var ms = orch.minesweeper
|
||||
var hit := {}
|
||||
ms.cell_revealed.connect(func(evt):
|
||||
if hit.is_empty() and bool(pred.call(evt)):
|
||||
hit["cell"] = evt.cell
|
||||
hit["evt"] = evt)
|
||||
var guard := 40
|
||||
while hit.is_empty() and guard > 0:
|
||||
guard -= 1
|
||||
var plan = AutoPlayer.choose_dig(ms)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != ms.hero_pos():
|
||||
ms.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
ms.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
return hit
|
||||
|
||||
|
||||
# 直接伤型快照判定(未揭示格可查——reward_economy 装板期摇定身份,示警烟火同源读面)
|
||||
func _is_direct_type(orch, c: Vector2i) -> bool:
|
||||
var snap_v = orch.reward_grant().reward_economy.query_full_snapshot(c)
|
||||
return snap_v is Dictionary and DIRECT_TYPES.has(String((snap_v as Dictionary).get("cell_type", "")))
|
||||
|
||||
|
||||
# 可封印的直接伤型安全格扫描(未揭示+非雷+直接伤型+邻接可达 revealed 格——封印动作真实落点位)
|
||||
func _find_sealable_direct(orch) -> Vector2i:
|
||||
var ms = orch.minesweeper
|
||||
var b = ms.current_board()
|
||||
var revealed := {}
|
||||
for c_v in ms.revealed_cells():
|
||||
revealed[c_v] = true
|
||||
for y in b.height:
|
||||
for x in b.width:
|
||||
var c := Vector2i(x, y)
|
||||
if ms.is_revealed(c) or b.is_mine(c) or ms.has_seal(c) or not _is_direct_type(orch, c):
|
||||
continue
|
||||
for n_v in b.neighbors8(c):
|
||||
var n := Vector2i(n_v)
|
||||
if revealed.has(n) and (ms.compute_move_path(n).size() > 0 or n == ms.hero_pos()):
|
||||
return c
|
||||
return Vector2i(-1, -1)
|
||||
|
||||
|
||||
# 盲挖扩板直至出现可封印直接伤型安全格(有界;返回格位或 (-1,-1))
|
||||
func _seal_direct_with_expansion(orch) -> Vector2i:
|
||||
var c := _find_sealable_direct(orch)
|
||||
var guard := 12
|
||||
while c.x < 0 and guard > 0:
|
||||
guard -= 1
|
||||
var plan = AutoPlayer.choose_dig(orch.minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != orch.minesweeper.hero_pos():
|
||||
orch.minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
orch.minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
c = _find_sealable_direct(orch)
|
||||
return c
|
||||
|
||||
|
||||
# ---- V5 全局受伤链路(迭代 46 卡A T2)----
|
||||
|
||||
# 四条真实路径(敌方攻击/危险格/错误封印/地形)逐路驱动:每条至少一张主角承伤单据在案,
|
||||
# 且承伤计数器 == 主角承伤单据行数(每单据恰发射一次的全量判据——重复发射即计数器>行数)。
|
||||
func test_all_hero_damage_paths_emit_once() -> void:
|
||||
print("--- V5 全局 damage_taken:四路径逐路恰一次 ---")
|
||||
# ① 敌方攻击路径(Boss 战零挖掘连停三轮——干净样本:零地形零资源单据;多轮保证伤害技在案)
|
||||
var orch1 = _orch(46001)
|
||||
var rows1 := _collect_hero_rows(orch1)
|
||||
Kit.check("V5① 前置:战斗受理", orch1.begin_battle(_enc(["probe_dt_counter"])))
|
||||
for _i in 3:
|
||||
orch1.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var enemy_rows := _count_by_source(rows1, "enemy_action")
|
||||
Kit.check("V5① 敌方攻击路径:承伤单据在案且逐单恰一次(行 %d=计数 %d)〔换图系批 V5〕" % [enemy_rows, _probe_count(orch1)],
|
||||
enemy_rows >= 1 and _probe_count(orch1) == enemy_rows)
|
||||
# ② 地形路径(AutoPlayer 真挖至伤害型变异格开出——发射在授予段开单点、单据标 hero_damage_consumed)
|
||||
var orch2 = _orch(46002)
|
||||
var rows2 := _collect_hero_rows(orch2)
|
||||
Kit.check("V5② 前置:战斗受理", orch2.begin_battle(_enc(["probe_dt_counter"])))
|
||||
var terr := _dig_until(orch2, func(evt):
|
||||
for vid_v in (evt.get("variant_ids", []) as Array):
|
||||
if String(_db.combat.variation.row_by_id(String(vid_v)).get("effect", "")) == "flat_damage":
|
||||
return true
|
||||
return false)
|
||||
Kit.check("V5② 前置:伤害型变异格已挖开(%s)" % str(terr.get("cell", Vector2i(-1, -1))), not terr.is_empty())
|
||||
orch2.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var terrain_rows := _count_by_source(rows2, "地形变异")
|
||||
Kit.check("V5② 地形路径:地形单据在案且全单据逐张恰一次(行 %d=计数 %d——盲挖途经单据一并计入)〔换图系批 V5〕" % [rows2.size(), _probe_count(orch2)],
|
||||
terrain_rows >= 1 and _probe_count(orch2) == rows2.size())
|
||||
# ③ 危险格路径(AutoPlayer 真挖直接伤型危险格——负向直伤单据)
|
||||
var orch3 = _orch(46003)
|
||||
var rows3 := _collect_hero_rows(orch3)
|
||||
Kit.check("V5③ 前置:战斗受理", orch3.begin_battle(_enc(["probe_dt_counter"])))
|
||||
var mine_hit := _dig_until(orch3, func(evt):
|
||||
return String(evt.get("kind", "")) == "mine" and _is_direct_type(orch3, evt.cell))
|
||||
Kit.check("V5③ 前置:直接伤型危险格已挖开(%s)" % str(mine_hit.get("cell", Vector2i(-1, -1))), not mine_hit.is_empty())
|
||||
orch3.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var danger_rows := _count_by_source(rows3, "mine_hit")
|
||||
Kit.check("V5③ 危险格路径:直伤单据在案且全单据逐张恰一次(行 %d=计数 %d)〔换图系批 V5〕" % [rows3.size(), _probe_count(orch3)],
|
||||
danger_rows >= 1 and _probe_count(orch3) == rows3.size())
|
||||
# ④ 错误封印路径(扩板→封直接伤型安全格不挖→停手轮末判标错——负向直伤单据)
|
||||
var orch4 = _orch(46004)
|
||||
var rows4 := _collect_hero_rows(orch4)
|
||||
Kit.check("V5④ 前置:战斗受理", orch4.begin_battle(_enc(["probe_dt_counter"])))
|
||||
var scell := _seal_direct_with_expansion(orch4)
|
||||
Kit.check("V5④ 前置:可封直接伤型安全格在位(%s)" % str(scell), scell.x >= 0)
|
||||
if scell.x >= 0:
|
||||
var plan4 = AutoPlayer.choose_dig(orch4.minesweeper)
|
||||
# 封印动作真实落点:先就位到目标邻接可达格(封印距离同挖掘距离)
|
||||
var seal_plan := _plan_seal(orch4, scell)
|
||||
if not seal_plan.is_empty():
|
||||
if seal_plan["hero_cell"] != orch4.minesweeper.hero_pos():
|
||||
orch4.minesweeper.settle_batch([{"action": "move", "cell": seal_plan["hero_cell"]}])
|
||||
var seal_out: Array = orch4.minesweeper.settle_batch([{"action": "seal_place", "cell": scell}])
|
||||
Kit.check("V5④ 前置:封印受理", not seal_out.is_empty() and bool((seal_out[0] as Dictionary).get("accepted", false)))
|
||||
orch4.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var sealwrong_rows := _count_by_source(rows4, "seal_miss")
|
||||
Kit.check("V5④ 错误封印路径:直伤单据在案且全单据逐张恰一次(行 %d=计数 %d)〔换图系批 V5〕" % [rows4.size(), _probe_count(orch4)],
|
||||
sealwrong_rows >= 1 and _probe_count(orch4) == rows4.size())
|
||||
|
||||
|
||||
# 封印落点计划(目标格邻接可达 revealed 位——AutoPlayer._plan_on 同构最小私有形)
|
||||
func _plan_seal(orch, target: Vector2i) -> Dictionary:
|
||||
var ms = orch.minesweeper
|
||||
for n_v in ms.current_board().neighbors8(target):
|
||||
var n := Vector2i(n_v)
|
||||
if ms.is_revealed(n) and (ms.compute_move_path(n).size() > 0 or n == ms.hero_pos()):
|
||||
return {"hero_cell": n}
|
||||
return {}
|
||||
|
||||
|
||||
# 同一伤害不得出现两次消费(重复发射=计数器>行数):敌方路径与地形路径两样本逐位核对——
|
||||
# 地形单据带 hero_damage_consumed 标记,战斗段末 _resolve_board_flat_hero 跳过二次发射由此钉住。
|
||||
func test_no_duplicate_damage_taken_event() -> void:
|
||||
print("--- V5 全局 damage_taken:无重复消费 ---")
|
||||
var orch1 = _orch(46011)
|
||||
var rows1 := _collect_hero_rows(orch1)
|
||||
Kit.check("ND 前置:战斗受理", orch1.begin_battle(_enc(["probe_dt_counter"])))
|
||||
for _i in 2:
|
||||
orch1.minesweeper.settle_batch([{"action": "stop"}])
|
||||
Kit.check("ND 敌方路径零重复(行 %d=计数 %d——重复消费即计数>行)〔换图系批 V5〕" % [rows1.size(), _probe_count(orch1)],
|
||||
rows1.size() >= 1 and _probe_count(orch1) == rows1.size())
|
||||
var orch2 = _orch(46012)
|
||||
var rows2 := _collect_hero_rows(orch2)
|
||||
Kit.check("ND 前置:战斗受理", orch2.begin_battle(_enc(["probe_dt_counter"])))
|
||||
var terr2 := _dig_until(orch2, func(evt):
|
||||
for vid_v in (evt.get("variant_ids", []) as Array):
|
||||
if String(_db.combat.variation.row_by_id(String(vid_v)).get("effect", "")) == "flat_damage":
|
||||
return true
|
||||
return false)
|
||||
Kit.check("ND 前置:伤害型变异格已挖开(%s)" % str(terr2.get("cell", Vector2i(-1, -1))), not terr2.is_empty())
|
||||
orch2.minesweeper.settle_batch([{"action": "stop"}])
|
||||
Kit.check("ND 地形路径零重复(行 %d=计数 %d——context 标记闸失效即计数翻倍)〔换图系批 V5〕" % [rows2.size(), _probe_count(orch2)],
|
||||
rows2.size() >= 1 and _probe_count(orch2) == rows2.size())
|
||||
|
||||
|
||||
# ---- V2/V3 两件基座(迭代 46 卡A T3)----
|
||||
|
||||
# 表驱动停手攒 RAP(每轮 1:1 转存——无转存率订阅缺省)
|
||||
func _bank_rap(orch, rounds_n: int) -> void:
|
||||
for _i in rounds_n:
|
||||
orch.minesweeper.settle_batch([{"action": "stop"}])
|
||||
|
||||
|
||||
# AutoPlayer 单次付费挖格(真实挖掘入口——移位+dig 一动作)
|
||||
func _auto_dig(orch) -> void:
|
||||
var plan = AutoPlayer.choose_dig(orch.minesweeper)
|
||||
if not plan.is_empty():
|
||||
if plan.hero_cell != orch.minesweeper.hero_pos():
|
||||
orch.minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
orch.minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
|
||||
|
||||
# V2 矿脉储能:受理的付费挖格 RAP 恰 +1、AP 按表扣、板资源不变
|
||||
func test_reserve_on_accepted_dig() -> void:
|
||||
print("--- V2 矿脉储能:付费挖格产 RAP ---")
|
||||
var orch = _orch(46021)
|
||||
Kit.check("V2 前置:战斗受理", orch.begin_battle(_enc(["vein_reserve"])))
|
||||
_bank_rap(orch, 1)
|
||||
var rap_before: int = orch.action_economy.rap
|
||||
var ap_before: int = orch.action_economy.ap
|
||||
_auto_dig(orch)
|
||||
var dig_cost: int = int(_db.run.action_economy.dig_ap_cost)
|
||||
Kit.check("V2 受理付费挖格 RAP 恰 +1(%d→%d)〔换图系批 V2〕" % [rap_before, orch.action_economy.rap],
|
||||
orch.action_economy.rap == rap_before + 1)
|
||||
Kit.check("V2 AP 按表扣(%d→%d,dig 价 %d)〔换图系批 V2〕" % [ap_before, orch.action_economy.ap, dig_cost],
|
||||
orch.action_economy.ap == ap_before - dig_cost)
|
||||
Kit.check("V2 受理记账闭合:AP 耗尽轮 RAP 增量恰等于受理挖数(AP 消耗数——每受理恰 +1,未受理零入池)〔换图系批 V2〕",
|
||||
_drain_round_rap_accounting(orch))
|
||||
|
||||
|
||||
# V2 辅证:挖到 AP 耗尽——RAP 增量 == 受理挖数(每受理恰 +1;移位耗 AP 不入账故按挖计数)
|
||||
func _drain_round_rap_accounting(orch) -> bool:
|
||||
var rap0: int = orch.action_economy.rap
|
||||
var digs := 0
|
||||
var guard := 24
|
||||
while orch.action_economy.phase == "Active" and guard > 0:
|
||||
guard -= 1
|
||||
var plan = AutoPlayer.choose_dig(orch.minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != orch.minesweeper.hero_pos():
|
||||
orch.minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if orch.action_economy.phase != "Active":
|
||||
break # 移位耗尽 AP 收轮——本轮不再有受理挖
|
||||
orch.minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
digs += 1
|
||||
return digs > 0 and orch.action_economy.rap - rap0 == digs
|
||||
|
||||
|
||||
# V3 省力周转:折价门槛与扣费(can 与执行同源;base 8→扣 4)
|
||||
func test_refresh_discount_gate_and_charge() -> void:
|
||||
print("--- V3 省力周转:折价门槛与扣费 ---")
|
||||
var orch = _orch(46022)
|
||||
Kit.check("V3 前置:战斗受理", orch.begin_battle(_enc(["easy_swap"])))
|
||||
_bank_rap(orch, 1)
|
||||
var base_cost: int = int(_db.run.action_economy.rap_refresh_cost)
|
||||
var discounted: int = int(floor(float(base_cost) * 50.0 / 100.0))
|
||||
var rap0: int = orch.action_economy.rap
|
||||
Kit.check("V3 门同源:can_refresh 与折后价同源(RAP %d ≥ 折后 %d)〔换图系批 V3〕" % [rap0, discounted],
|
||||
orch.can_refresh())
|
||||
Kit.check("V3 第 1 次刷新受理", orch.player_request_refresh())
|
||||
Kit.check("V3 折后单次扣费(%d→%d,−%d)〔换图系批 V3〕" % [rap0, orch.action_economy.rap, discounted],
|
||||
orch.action_economy.rap == rap0 - discounted)
|
||||
Kit.check("V3 第 2 次刷新受理(RAP=%d)" % orch.action_economy.rap, orch.player_request_refresh())
|
||||
Kit.check("V3 两次折后扣费累计(RAP=%d=%d−2×%d)〔换图系批 V3〕" % [orch.action_economy.rap, rap0, discounted],
|
||||
orch.action_economy.rap == rap0 - 2 * discounted)
|
||||
Kit.check("V3 门槛拒:RAP 不足折后价零受理(can=false 且执行拒)〔换图系批 V3〕",
|
||||
orch.action_economy.rap < discounted and not orch.can_refresh() and not orch.player_request_refresh())
|
||||
|
||||
|
||||
# V3 双基座叠加:挖格产 RAP 与刷新折价同场生效
|
||||
func test_double_base_stack() -> void:
|
||||
print("--- V3 双基座叠加 ---")
|
||||
var orch = _orch(46023)
|
||||
Kit.check("V3 前置:战斗受理", orch.begin_battle(_enc(["vein_reserve", "easy_swap"])))
|
||||
_bank_rap(orch, 1)
|
||||
var rap_before: int = orch.action_economy.rap
|
||||
var base_cost: int = int(_db.run.action_economy.rap_refresh_cost)
|
||||
_auto_dig(orch)
|
||||
Kit.check("V3 双基座:挖格产 RAP +1(%d→%d)〔换图系批 V3〕" % [rap_before, orch.action_economy.rap],
|
||||
orch.action_economy.rap == rap_before + 1)
|
||||
var rap_after_dig: int = orch.action_economy.rap
|
||||
Kit.check("V3 双基座:刷新按折后价扣(−%d)〔换图系批 V3〕" % int(base_cost / 2),
|
||||
orch.player_request_refresh() and orch.action_economy.rap == rap_after_dig - int(base_cost / 2))
|
||||
|
||||
|
||||
# V12(refresh_feats 侧):无专长基线——挖格零产 RAP、刷新按表基线扣、AP 保留、上限照旧
|
||||
func test_no_feats_baseline() -> void:
|
||||
print("--- V12 无专长基线(refresh_feats 侧) ---")
|
||||
var orch = _orch(46024)
|
||||
Kit.check("V12 前置:战斗受理", orch.begin_battle(_enc([])))
|
||||
_bank_rap(orch, 3)
|
||||
var rap_before: int = orch.action_economy.rap
|
||||
var ap_before: int = orch.action_economy.ap
|
||||
var base_cost: int = int(_db.run.action_economy.rap_refresh_cost)
|
||||
_auto_dig(orch)
|
||||
Kit.check("V12 无基座:挖格零产 RAP(%d 不变)〔换图系批 V12〕" % rap_before, orch.action_economy.rap == rap_before)
|
||||
Kit.check("V12 刷新受理并按表基线扣(−%d)〔换图系批 V12〕" % base_cost,
|
||||
orch.player_request_refresh() and orch.action_economy.rap == rap_before - base_cost)
|
||||
Kit.check("V12 AP 保留、相位 Active(基线刷新不重置)〔换图系批 V12〕",
|
||||
orch.action_economy.ap == ap_before - int(_db.run.action_economy.dig_ap_cost) and orch.action_economy.phase == "Active")
|
||||
Kit.check("V12 每轮上限照旧:第 3 次拒(RAP 足额仍拒)〔换图系批 V12〕",
|
||||
orch.player_request_refresh() and not orch.player_request_refresh())
|
||||
|
||||
|
||||
# ---- V4/V10 成功刷新事实与命中施加(迭代 46 卡A T4)----
|
||||
|
||||
# 成功刷新事实探针专长安装(board_refreshed → counter_add——成功事实的可观测计数面)
|
||||
func _install_br_probe() -> void:
|
||||
if _db.crossrun.feats.row_by_id("probe_br_counter").is_empty():
|
||||
(_db.crossrun.feats.rows as Array).append({
|
||||
"feat_id": "probe_br_counter", "display_name": "刷新事实探针", "rarity": "common",
|
||||
"desc": "测试夹具:每次成功刷新计数 +1",
|
||||
"triggers": [{"on": "board_refreshed", "do": {"kind": "counter_add", "counter_id": "br_probe_hits", "value": 1}}],
|
||||
})
|
||||
|
||||
|
||||
func _br_count(orch) -> int:
|
||||
return EffectEngineScript.counter(orch.fx_host().counters_ctx, "br_probe_hits")
|
||||
|
||||
|
||||
# V4 成功事实只在新板就位后发射:成功恰一次;非 Active/每轮上限/RAP 不足三拒面全零事实零换图
|
||||
func test_board_refresh_fact_only_on_success() -> void:
|
||||
print("--- V4 成功刷新事实:拒绝零事实 ---")
|
||||
_install_br_probe()
|
||||
var orch = _orch(46031)
|
||||
var swaps := {"n": 0}
|
||||
var waiting_rejected := {"v": false}
|
||||
orch.board_changed.connect(func(): swaps["n"] += 1)
|
||||
# 非 Active 探针(round_flow 同款:借衰减行在结算段内同步探测一次——Waiting 相位刷新必拒)
|
||||
orch.step_row.connect(func(row):
|
||||
if String(row.get("type", "")) == "decay" and not bool(waiting_rejected["v"]):
|
||||
waiting_rejected["v"] = not orch.player_request_refresh())
|
||||
Kit.check("V4 前置:战斗受理", orch.begin_battle(_enc(["probe_br_counter"])))
|
||||
_bank_rap(orch, 3) # RAP=3×AP;首轮结算衰减行触发 Waiting 探针
|
||||
var swaps0: int = swaps["n"]
|
||||
Kit.check("V4 非 Active 拒零事实(结算段内探测拒绝)〔换图系批 V4〕",
|
||||
bool(waiting_rejected["v"]) and _br_count(orch) == 0 and int(swaps["n"]) == swaps0)
|
||||
# 成功 ×2:事实计数与换图信号逐次 +1
|
||||
Kit.check("V4 第 1 次成功(事实 +1 换图 +1)〔换图系批 V4〕",
|
||||
orch.player_request_refresh() and _br_count(orch) == 1 and int(swaps["n"]) == swaps0 + 1)
|
||||
Kit.check("V4 第 2 次成功(事实 +1 换图 +1)〔换图系批 V4〕",
|
||||
orch.player_request_refresh() and _br_count(orch) == 2 and int(swaps["n"]) == swaps0 + 2)
|
||||
# 每轮上限拒:used=2 满门、RAP 足额——零事实零换图
|
||||
Kit.check("V4 上限拒零事实(RAP %d 足额仍拒)〔换图系批 V4〕" % orch.action_economy.rap,
|
||||
not orch.can_refresh() and not orch.player_request_refresh() and _br_count(orch) == 2 and int(swaps["n"]) == swaps0 + 2)
|
||||
# RAP 不足拒:停手进新轮(used 重置、停手再入 RAP)→ 消费至 RAP 0 → 拒(零事实零换图;
|
||||
# 纯 RAP 不足拒绝面已由 V3 门槛拒单测钉住,此处并拦形态)
|
||||
orch.minesweeper.settle_batch([{"action": "stop"}])
|
||||
Kit.check("V4 新轮受理两次(used 重置、RAP 复入)〔换图系批 V4〕",
|
||||
orch.action_economy.phase == "Active" and orch.player_request_refresh() and orch.player_request_refresh() and _br_count(orch) == 4)
|
||||
Kit.check("V4 RAP 不足拒零事实(RAP %d < 价 %d)〔换图系批 V4〕" % [orch.action_economy.rap, int(_db.run.action_economy.rap_refresh_cost)],
|
||||
orch.action_economy.rap < int(_db.run.action_economy.rap_refresh_cost) and not orch.can_refresh() and not orch.player_request_refresh() and _br_count(orch) == 4 and int(swaps["n"]) == swaps0 + 4)
|
||||
|
||||
|
||||
# V10 矿道红利:每次成功刷新 escrow 恰 +1 且 fx_grant_gp 行在案;拒绝零变化
|
||||
func test_mine_profit_gp() -> void:
|
||||
print("--- V10 矿道红利:刷新固定 GP ---")
|
||||
var orch = _orch(46032)
|
||||
var gp_rows := {"n": 0}
|
||||
orch.step_row.connect(func(row):
|
||||
if String(row.get("type", "")) == "effect" and String(row.get("effect_id", "")) == "fx_grant_gp":
|
||||
gp_rows["n"] += 1)
|
||||
Kit.check("V10 前置:战斗受理", orch.begin_battle(_enc(["mine_profit"])))
|
||||
_bank_rap(orch, 2)
|
||||
var escrow0: int = orch.reward_grant().escrow
|
||||
Kit.check("V10 第 1 次成功 escrow +1(%d→%d)〔换图系批 V10〕" % [escrow0, orch.reward_grant().escrow],
|
||||
orch.player_request_refresh() and orch.reward_grant().escrow == escrow0 + 1)
|
||||
Kit.check("V10 fx_grant_gp 行在案(%d 行)〔换图系批 V10〕" % int(gp_rows["n"]), int(gp_rows["n"]) == 1)
|
||||
Kit.check("V10 第 2 次成功 escrow +1(%d→%d)〔换图系批 V10〕" % [orch.reward_grant().escrow, orch.reward_grant().escrow + 1],
|
||||
orch.player_request_refresh() and orch.reward_grant().escrow == escrow0 + 2)
|
||||
# 拒绝零变化:RAP 不足
|
||||
Kit.check("V10 拒绝零变化(escrow=%d)〔换图系批 V10〕" % orch.reward_grant().escrow,
|
||||
not orch.player_request_refresh() and orch.reward_grant().escrow == escrow0 + 2 and int(gp_rows["n"]) == 2)
|
||||
|
||||
|
||||
# ---- V6/V8/V9/V7 五件后续效果(迭代 46 卡A T5)----
|
||||
|
||||
func _counter(orch, counter_id: String) -> int:
|
||||
return EffectEngineScript.counter(orch.fx_host().counters_ctx, counter_id)
|
||||
|
||||
|
||||
# 伤害明细行泛收集(type=damage 全量——按来源类与值断言)
|
||||
func _collect_damage_rows(orch) -> Array:
|
||||
var rows: Array = []
|
||||
orch.step_row.connect(func(row):
|
||||
if String(row.get("type", "")) == "damage":
|
||||
rows.append(row))
|
||||
return rows
|
||||
|
||||
|
||||
func _damage_rows_by_source(rows: Array, source: String) -> Array:
|
||||
var out: Array = []
|
||||
for r_v in rows:
|
||||
var r := r_v as Dictionary
|
||||
if String(r.get("source_class", "")) == source:
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
|
||||
# V6 石层缓冲:全局固定减伤(危险格 pierce 8→6 两档钉死:单刷减 2·同轮多刷不叠加·未刷新轮不减;
|
||||
# 地形路径减后值=max(下限, effect_value−2)——含下限保持;敌方路径照常单次发射)
|
||||
func test_stone_mitigation_all_damage() -> void:
|
||||
print("--- V6 石层缓冲:全局固定减伤 ---")
|
||||
# S1 同轮双刷 → 危险格 pierce 8 减 2=6(叠加即 4——多刷不增钉死)
|
||||
var orch1 = _orch(46041)
|
||||
var rows1 := _collect_damage_rows(orch1)
|
||||
Kit.check("V6 S1 前置:战斗受理", orch1.begin_battle(_enc(["stone_mitigation"])))
|
||||
_bank_rap(orch1, 3)
|
||||
Kit.check("V6 S1 同轮双刷受理", orch1.player_request_refresh() and orch1.player_request_refresh())
|
||||
Kit.check("V6 S1 guard 累计 2(同轮双刷)", _counter(orch1, "round_swap_guard") == 2)
|
||||
var mine1 := _dig_until(orch1, func(evt):
|
||||
return String(evt.get("kind", "")) == "mine" and _is_direct_type(orch1, evt.cell))
|
||||
Kit.check("V6 S1 前置:直接伤型危险格已挖开(%s)" % str(mine1.get("cell", Vector2i(-1, -1))), not mine1.is_empty())
|
||||
orch1.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var pierce_rows1 := _damage_rows_by_source(rows1, "mine_hit")
|
||||
Kit.check("V6 S1 危险格减伤恰 2 且多刷不叠加(行值 %s∈[6]——叠加态应为 4)〔换图系批 V6〕" % str(pierce_rows1.map(func(r): return int(r.value))),
|
||||
pierce_rows1.size() >= 1 and int((pierce_rows1[0] as Dictionary).get("value", -1)) == 6)
|
||||
# S2 未刷新轮:guard 随 round_start 清空 → 同型危险格不减(pierce/magic 表幂 8——heal 型幂 6 不同锚勿混)
|
||||
var mine2 := _dig_until(orch1, func(evt):
|
||||
var snap_v2 = orch1.reward_grant().reward_economy.query_full_snapshot(evt.cell)
|
||||
return String(evt.get("kind", "")) == "mine" and snap_v2 is Dictionary \
|
||||
and ["pierce", "magic"].has(String((snap_v2 as Dictionary).get("cell_type", ""))))
|
||||
Kit.check("V6 S2 前置:第二直接伤型危险格已挖开(%s)" % str(mine2.get("cell", Vector2i(-1, -1))), not mine2.is_empty())
|
||||
orch1.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var pierce_rows2 := _damage_rows_by_source(rows1, "mine_hit")
|
||||
Kit.check("V6 S2 未刷新轮不减伤(新轮行值 %s 含 8——round 桶轮始清空)〔换图系批 V6〕" % str(pierce_rows2.map(func(r): return int(r.value))),
|
||||
pierce_rows2.size() >= 2 and int((pierce_rows2[pierce_rows2.size() - 1] as Dictionary).get("value", -1)) == 8)
|
||||
# S3 地形路径:减后值 = max(下限, effect_value−2)——下限保持与定值减一并钉死
|
||||
# (种子 46002=V5② 已证含 flat_damage 变异板;刷新换板后变异重布—— digging guard 内可达)
|
||||
var orch3 = _orch(46002)
|
||||
var rows3 := _collect_damage_rows(orch3)
|
||||
Kit.check("V6 S3 前置:战斗受理", orch3.begin_battle(_enc(["stone_mitigation"])))
|
||||
_bank_rap(orch3, 2)
|
||||
Kit.check("V6 S3 单刷受理", orch3.player_request_refresh())
|
||||
var terr3 := _dig_until(orch3, func(evt):
|
||||
for vid_v in (evt.get("variant_ids", []) as Array):
|
||||
if String(_db.combat.variation.row_by_id(String(vid_v)).get("effect", "")) == "flat_damage":
|
||||
return true
|
||||
return false)
|
||||
Kit.check("V6 S3 前置:伤害型变异格已挖开(%s)" % str(terr3.get("cell", Vector2i(-1, -1))), not terr3.is_empty())
|
||||
orch3.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var terrain_rows := _damage_rows_by_source(rows3, "地形变异")
|
||||
var floor_v: int = int(_db.combat.settlement.damage_roll.damage_floor)
|
||||
var ev_ok := true
|
||||
for tr_v in terrain_rows:
|
||||
var tr := tr_v as Dictionary
|
||||
var vid := String(tr.get("variant_id", ""))
|
||||
var ev: int = int(_db.combat.variation.row_by_id(vid).get("effect_value", 0))
|
||||
if int(tr.get("value", -1)) != maxi(floor_v, ev - 2):
|
||||
ev_ok = false
|
||||
Kit.check("V6 S3 地形减伤=max(下限,effect_value−2) 逐单闭合(%d 单)〔换图系批 V6〕" % terrain_rows.size(),
|
||||
terrain_rows.size() >= 1 and ev_ok)
|
||||
|
||||
|
||||
# V9 坑道小憩:轮末合计回复(白盒预置承载上限 3——计数器直改属数据面注入;
|
||||
# 自然累计 2 与满血未生效两形态分战场钉死)
|
||||
func test_rest_rotation_cap_and_clear() -> void:
|
||||
print("--- V9 坑道小憩:轮末恢复 ---")
|
||||
# R1 白盒预置 3 层 + 刷新闸(when<3 拒第 4 层)→ 停手回复 min(3, 缺血)——护甲降档保缺血 ≥3
|
||||
var orch1 = _orch(46051)
|
||||
var rows1: Array = []
|
||||
orch1.step_row.connect(func(row): rows1.append(row))
|
||||
Kit.check("V9 R1 前置:战斗受理", orch1.begin_battle(_enc(["tunnel_rest"], 0, 10, 100)))
|
||||
_bank_rap(orch1, 1) # 开局血差 100(hero_hp 100/max 200)——缺血 ≥3 恒成立,回复面确定
|
||||
var missing: int = 200 - int(orch1._enemy_system.hero_unit().get("hp", 0))
|
||||
EffectEngineScript.counter_add(orch1.fx_host().counters_ctx, "round_swap_rest", 3)
|
||||
Kit.check("V9 R1 预置 3 层后刷新被闸(when<3——上限 3)", orch1.player_request_refresh() and _counter(orch1, "round_swap_rest") == 3)
|
||||
orch1.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var heal_rows1: Array = []
|
||||
var idle_rows1 := 0
|
||||
for r_v in rows1:
|
||||
var r := r_v as Dictionary
|
||||
if String(r.get("type", "")) == "heal" and String(r.get("source_class", "")) == "fx_board_refresh":
|
||||
heal_rows1.append(r)
|
||||
if String(r.get("type", "")) == "reward_ineffective" and String(r.get("batch_key", "")) == "round_swap_rest":
|
||||
idle_rows1 += 1
|
||||
Kit.check("V9 R1 轮末回复=min(3,缺血%d) 且层清 0(heal 行值 %s)〔换图系批 V9〕" % [missing, str(heal_rows1.map(func(r): return int(r.value)))],
|
||||
missing >= 3 and heal_rows1.size() == 1 and int((heal_rows1[0] as Dictionary).get("value", -1)) == 3 and _counter(orch1, "round_swap_rest") == 0)
|
||||
# R2 满血未生效:首轮刷新后停手——授予段 heal 时点主角仍满血(combat 段还手在其后)
|
||||
var orch2 = _orch(46052)
|
||||
var rows2: Array = []
|
||||
orch2.step_row.connect(func(row): rows2.append(row))
|
||||
Kit.check("V9 R2 前置:战斗受理", orch2.begin_battle(_enc(["tunnel_rest"], 8)))
|
||||
Kit.check("V9 R2 首轮刷新受理(voucher_rap 入池)", orch2.player_request_refresh())
|
||||
orch2.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var heal_rows2 := 0
|
||||
var idle2 := 0
|
||||
for r2_v in rows2:
|
||||
var r2 := r2_v as Dictionary
|
||||
if String(r2.get("type", "")) == "heal" and String(r2.get("source_class", "")) == "fx_board_refresh":
|
||||
heal_rows2 += 1
|
||||
if String(r2.get("type", "")) == "reward_ineffective" and String(r2.get("batch_key", "")) == "round_swap_rest":
|
||||
idle2 += 1
|
||||
Kit.check("V9 R2 满血未生效行在案且零 heal 行(未生效 %d·heal %d)〔换图系批 V9〕" % [idle2, heal_rows2],
|
||||
idle2 == 1 and heal_rows2 == 0 and _counter(orch2, "round_swap_rest") == 0)
|
||||
# R3 自然累计:同轮双刷 → 回复 2(上限 3 的闸面由 R1 白盒钉住,此处证自然路径)
|
||||
var orch3 = _orch(46053)
|
||||
var rows3: Array = []
|
||||
orch3.step_row.connect(func(row): rows3.append(row))
|
||||
Kit.check("V9 R3 前置:战斗受理", orch3.begin_battle(_enc(["tunnel_rest"])))
|
||||
_bank_rap(orch3, 2)
|
||||
Kit.check("V9 R3 同轮双刷受理", orch3.player_request_refresh() and orch3.player_request_refresh())
|
||||
Kit.check("V9 R3 自然累计 2 层", _counter(orch3, "round_swap_rest") == 2)
|
||||
orch3.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var heal3 := 0
|
||||
for r3_v in rows3:
|
||||
if String((r3_v as Dictionary).get("type", "")) == "heal" and String((r3_v as Dictionary).get("source_class", "")) == "fx_board_refresh":
|
||||
heal3 += 1
|
||||
Kit.check("V9 R3 轮末 heal 行值 2(自然累计路径)〔换图系批 V9〕",
|
||||
heal3 == 1 and _counter(orch3, "round_swap_rest") == 0)
|
||||
|
||||
|
||||
# V8 蓄力周转:当轮攻击消费全部层数(上限 3 闸=白盒预置;战斗新建自然清空)
|
||||
func test_charge_layers_consumed_same_round() -> void:
|
||||
print("--- V8 蓄力周转:攻击消费层 ---")
|
||||
var orch1 = _orch(46061)
|
||||
var rows1 := _collect_damage_rows(orch1)
|
||||
Kit.check("V8 前置:战斗受理", orch1.begin_battle(_enc(["charge_rotation"], 16)))
|
||||
# 上限 3 闸:白盒预置 3 层后刷新被拒层(when<3)
|
||||
EffectEngineScript.counter_add(orch1.fx_host().counters_ctx, "swap_charge_layers", 3)
|
||||
Kit.check("V8 上限 3 闸(预置 3 刷新不加层)", orch1.player_request_refresh() and _counter(orch1, "swap_charge_layers") == 3)
|
||||
# 当轮攻击消费:停手 → 主角先手攻击 → 层数全消费清零
|
||||
orch1.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var attack_rows := 0
|
||||
for r_v in rows1:
|
||||
if String((r_v as Dictionary).get("source_class", "")) == "hero_attack":
|
||||
attack_rows += 1
|
||||
Kit.check("V8 当轮攻击消费全部层(攻击行在案 %d·层清 0)〔换图系批 V8〕" % attack_rows,
|
||||
attack_rows >= 1 and _counter(orch1, "swap_charge_layers") == 0)
|
||||
# 自然累计:双刷 2 层(voucher_rap 16 入池)→ 停手消费归零
|
||||
var orch2 = _orch(46062)
|
||||
Kit.check("V8 前置:战斗受理", orch2.begin_battle(_enc(["charge_rotation"], 16)))
|
||||
Kit.check("V8 战斗新建层 0(引擎重建自然清空)", _counter(orch2, "swap_charge_layers") == 0)
|
||||
Kit.check("V8 双刷累计 2 层", orch2.player_request_refresh() and orch2.player_request_refresh() and _counter(orch2, "swap_charge_layers") == 2)
|
||||
orch2.minesweeper.settle_batch([{"action": "stop"}])
|
||||
Kit.check("V8 攻击后层清 0(消费面闭合)〔换图系批 V8〕", _counter(orch2, "swap_charge_layers") == 0)
|
||||
|
||||
|
||||
# V7 崩壁冲击:延迟群伤单(刷新时点零伤害行——战斗段末按存活目标结账)
|
||||
func test_blast_defers_to_combat_segment() -> void:
|
||||
print("--- V7 崩壁冲击:延迟群伤 ---")
|
||||
var orch = _orch(46071)
|
||||
var rows := _collect_damage_rows(orch)
|
||||
Kit.check("V7 前置:战斗受理", orch.begin_battle(_enc(["wall_blast"], 8)))
|
||||
Kit.check("V7 刷新受理(voucher_rap 入池)", orch.player_request_refresh())
|
||||
Kit.check("V7 刷新时点零伤害行(授予段不结账——案C 延迟)〔换图系批 V7〕",
|
||||
_damage_rows_by_source(rows, "fx_board_refresh").is_empty())
|
||||
orch.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var blast_rows := _damage_rows_by_source(rows, "fx_board_refresh")
|
||||
Kit.check("V7 战斗段末群伤单恰一单(全体存活敌 ×2——行值 2 count≥1)〔换图系批 V7〕",
|
||||
blast_rows.size() == 1 and int((blast_rows[0] as Dictionary).get("value", -1)) == 2
|
||||
and int((blast_rows[0] as Dictionary).get("count", 0)) >= 1)
|
||||
|
||||
|
||||
# ---- V11 数据与池(迭代 46 卡A T6)----
|
||||
|
||||
# 七件 validate/在册/稀有度 cap 对表/跨表计数器引用闭合/desc 禁词
|
||||
func test_seven_feats_validate_and_pool() -> void:
|
||||
print("--- V11 七件数据与池 ---")
|
||||
var seven := {"vein_reserve": "excellent", "easy_swap": "excellent", "wall_blast": "excellent",
|
||||
"charge_rotation": "excellent", "stone_mitigation": "common", "tunnel_rest": "common", "mine_profit": "rare"}
|
||||
Kit.check("V11 feats.validate 零违例(七行全过词表/行键白名单/前缀校验)〔换图系批 V11〕",
|
||||
_db.crossrun.feats.validate().is_empty())
|
||||
var all_in := true
|
||||
var desc_bad := 0
|
||||
for fid in seven.keys():
|
||||
var row: Dictionary = _db.crossrun.feats.row_by_id(String(fid))
|
||||
if row.is_empty() or String(row.get("rarity", "")) != String(seven[fid]) or int(row.get("cap", 0)) != 1:
|
||||
all_in = false
|
||||
if String(row.get("desc", "")).contains("雷") or String(row.get("desc", "")).contains("旗"):
|
||||
desc_bad += 1
|
||||
Kit.check("V11 七行在册且稀有度/cap 逐件对表〔换图系批 V11〕", all_in)
|
||||
Kit.check("V11 七件 desc 禁词零命中(雷/旗字面)〔换图系批 V11〕", desc_bad == 0)
|
||||
# 跨表计数器引用闭合(判据域=换图系七件自身:value_counter/when-id 读取全部有 counter_add 写入方;
|
||||
# 预存行的消费点记账形态〔如完美保险 insurance_used 由授予段落账〕不在此判据域)
|
||||
var written := {}
|
||||
for row_v in (_db.crossrun.feats.rows as Array):
|
||||
for tv in ((row_v as Dictionary).get("triggers", []) as Array):
|
||||
var do_d: Dictionary = (tv as Dictionary).get("do", {})
|
||||
if String(do_d.get("kind", "")) == "counter_add":
|
||||
written[String(do_d.get("counter_id", ""))] = true
|
||||
var refs_ok := true
|
||||
for fid2 in seven.keys():
|
||||
var row2: Dictionary = _db.crossrun.feats.row_by_id(String(fid2))
|
||||
for tv2 in ((row2.get("triggers", []) as Array)):
|
||||
var tr2 := tv2 as Dictionary
|
||||
var do2: Dictionary = tr2.get("do", {})
|
||||
for reader in [String(do2.get("value_counter", "")), String(do2.get("value_from", ""))]:
|
||||
if reader != "" and not written.has(reader):
|
||||
refs_ok = false
|
||||
for c_v in (tr2.get("when", []) as Array):
|
||||
var cid := String((c_v as Dictionary).get("id", ""))
|
||||
if (c_v as Dictionary).get("fact", "") == "counter" and cid != "" and not written.has(cid):
|
||||
refs_ok = false
|
||||
Kit.check("V11 七件跨表计数器引用闭合(value_counter/when-id 全部有 counter_add 写入方)〔换图系批 V11〕", refs_ok)
|
||||
Kit.check("V11 换图系三计数器在写入集(swap_charge_layers/round_swap_guard/round_swap_rest)〔换图系批 V11〕",
|
||||
written.has("swap_charge_layers") and written.has("round_swap_guard") and written.has("round_swap_rest"))
|
||||
@@ -0,0 +1 @@
|
||||
uid://bmsh0idfmpw3j
|
||||
@@ -461,8 +461,8 @@ func _smoke_adventure() -> void:
|
||||
|
||||
func _smoke_feats() -> void:
|
||||
print("--- 专长重设计实装(feat_table 四级 · 专长带池接线 · 订阅面逐件)---")
|
||||
# ① 现行表牌库构成:30 件全量入池——102 数值令牌+30 relic 令牌(四级 rarity 直映射)
|
||||
Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.crossrun.feats != null and (_db.crossrun.feats.rows as Array).size() == 30)
|
||||
# ① 现行表牌库构成:37 件全量入池——102 数值令牌+37 relic 令牌(四级 rarity 直映射)
|
||||
Kit.check("feat_table 已加载(37 件=基础 27+迁入 2+完美保险 1+迭代 46 换图系七件——换代)", _db.crossrun.feats != null and (_db.crossrun.feats.rows as Array).size() == 37)
|
||||
Kit.check("feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)", _db.crossrun.feats.validate().is_empty())
|
||||
var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71))
|
||||
Kit.check("专长带场景:冒险启动(seed 71)", a_r0.start_adventure(71))
|
||||
@@ -473,7 +473,7 @@ func _smoke_feats() -> void:
|
||||
feat_tokens += 1
|
||||
var tk := String((t as Dictionary).get("tier", ""))
|
||||
tier_counts[tk] = int(tier_counts.get(tk, 0)) + 1
|
||||
Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0.deck().deck.size() == 132)
|
||||
Kit.check("专长带入池:37 件四级直映射(普通 14/优秀 13/稀有 10/传说 0)——牌库 102+37=139〔专长批 V2+迭代 46 换图系七件换代〕", feat_tokens == 37 and int(tier_counts.get("common", 0)) == 14 and int(tier_counts.get("excellent", 0)) == 13 and int(tier_counts.get("rare", 0)) == 10 and a_r0.deck().deck.size() == 139)
|
||||
# ② 旧 27 件零残留钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归静态核查)
|
||||
Kit.check("旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕", _db.crossrun.feats.row_by_id("arcane_ember").is_empty() and _db.crossrun.feats.row_by_id("magma_body").is_empty())
|
||||
Kit.check("迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕", _db.crossrun.treasure_items.row_by_id("venom_edge").is_empty() and _db.crossrun.treasure_items.row_by_id("seal_burst").is_empty())
|
||||
|
||||
@@ -51,21 +51,29 @@ func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator:
|
||||
# 公式 N×cell ≤ 可用高−12 必须计入 (N−1)×2px 网格间距,否则网格实高溢出被 clip_contents 裁掉;
|
||||
# 现公式 = 计入间距 + 垂直预留 24px〔上 12/下 12——底部不贴 D 区边框〕)----
|
||||
func _smoke_layout_cell_formula() -> void:
|
||||
print("--- 棋盘布局格边长公式(底部预留回归)---")
|
||||
print("--- 棋盘布局格边长公式(底部预留回归·四档板型)---")
|
||||
var board_generator = load("res://core/board_generator.gd").new()
|
||||
var board_view = load("res://presentation/views/board_view.gd").new()
|
||||
for L in [1, 2]:
|
||||
var b = board_generator.generate(L, 0, _mk_stream("board_gen", 555), _db)
|
||||
var board_view_script := load("res://presentation/views/board_view.gd")
|
||||
# 四档取样表(board_gen.tres 实表取档——(L,D)→尺寸:10/12/13/14;13×13 与 14×14 首次入断言)
|
||||
var tiers := [{"L": 1, "D": 0, "n": 10}, {"L": 2, "D": 0, "n": 12}, {"L": 3, "D": 0, "n": 13}, {"L": 3, "D": 9, "n": 14}]
|
||||
for t_v in tiers:
|
||||
var t: Dictionary = t_v
|
||||
var b = board_generator.generate(int(t["L"]), int(t["D"]), _mk_stream("board_gen", 555), _db)
|
||||
board_view.rebuild(b, null)
|
||||
var avail := Vector2(1036, 460) # 受约束 D 区可用面积样本(1280×800 档量级——用户实测裁切场景)
|
||||
board_view.set_cell_area(avail)
|
||||
var n: int = maxi(b.width, b.height)
|
||||
var cell: float = board_view.get("_cell")
|
||||
var grid_h := n * int(cell) + (n - 1) * 2
|
||||
Kit.check("L%d %dx%d 板网格高 %d ≤ 可用高 %d − 预留 24(格边 %dpx——下缘不贴边不裁切)" % [L, b.width, b.height, grid_h, int(avail.y), int(cell)],
|
||||
Kit.check("L%d D%d 板型一致(%dx%d = 取样 %d——表改动致取样落错档即红)〔格边长 V3〕" % [int(t["L"]), int(t["D"]), b.width, b.height, int(t["n"])],
|
||||
n == int(t["n"]))
|
||||
Kit.check("L%d D%d %dx%d 板网格高 %d ≤ 可用高 %d − 预留 24(格边 %dpx——下缘不贴边不裁切)〔格边长 V3〕" % [int(t["L"]), int(t["D"]), b.width, b.height, grid_h, int(avail.y), int(cell)],
|
||||
grid_h <= int(avail.y) - 24)
|
||||
Kit.check("L%d 格边在 26–46 档内(%dpx)" % [L, int(cell)],
|
||||
Kit.check("L%d D%d 格边在 26–46 档内(%dpx)〔格边长 V3〕" % [int(t["L"]), int(t["D"]), int(cell)],
|
||||
cell >= 26.0 and cell <= 46.0)
|
||||
Kit.check("L%d D%d 算式单点等价(set_cell_area 写入 %d == _cell_for 返回 %d——同一事实两路读取)〔格边长 V2〕" % [int(t["L"]), int(t["D"]), int(cell), board_view_script._cell_for(n, avail)],
|
||||
int(cell) == board_view_script._cell_for(n, avail))
|
||||
board_view.free()
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ func _init() -> void:
|
||||
# ① 词表在册断言(扩签面:2 事件+8 事实+7 效果类型+1 算子)
|
||||
Kit.check_quiet(EffectEngineScript.EVENTS.has("dig_stop") and EffectEngineScript.EVENTS.has("battle_won"),
|
||||
"新事件在册:dig_stop/battle_won〔专长批 V1〕")
|
||||
# ①-a 换图系两事件两效果在册(迭代 46 扩签——旧出域负例全部保留)
|
||||
Kit.check_quiet(EffectEngineScript.EVENTS.has("refresh_request") and EffectEngineScript.EVENTS.has("board_refreshed"),
|
||||
"换图系新事件在册:refresh_request/board_refreshed〔换图系批 V1〕")
|
||||
Kit.check_quiet(EffectEngineScript.EFFECT_KINDS.has("grant_reserve_ap") and EffectEngineScript.EFFECT_KINDS.has("apply_discount"),
|
||||
"换图系新效果在册:grant_reserve_ap/apply_discount〔换图系批 V1〕")
|
||||
Kit.check_quiet(EffectEngineScript.FACTS.has("hp_ratio") and EffectEngineScript.FACTS.has("is_edge_cell")
|
||||
and EffectEngineScript.FACTS.has("new_resource_type") and EffectEngineScript.FACTS.has("dug_under_seal")
|
||||
and EffectEngineScript.FACTS.has("ap_exhausted_last") and EffectEngineScript.FACTS.has("ap_left")
|
||||
|
||||
@@ -199,7 +199,7 @@ func _test_negative_cases() -> void:
|
||||
# 现行 31;本套件零扩签,钉现行数(将来任何加词即转红提示复核)。
|
||||
|
||||
func _test_effect_kinds_closed() -> void:
|
||||
Kit.check("效果引擎效果类型闭集 32 种(锻造工坊批零新增口径随迭代 31 卡② 扩签 forgive_seal_miss 换代)〔锻造工坊批 V11〕",
|
||||
EffectEngineScript.EFFECT_KINDS.size() == 32)
|
||||
Kit.check("效果引擎事件词表在册 20 种(本批零扩签参照面)〔锻造工坊批 V11〕",
|
||||
EffectEngineScript.EVENTS.size() == 20)
|
||||
Kit.check("效果引擎效果类型闭集 34 种(迭代 46 换图系扩签 grant_reserve_ap/apply_discount 换代)〔锻造工坊批 V11〕",
|
||||
EffectEngineScript.EFFECT_KINDS.size() == 34)
|
||||
Kit.check("效果引擎事件词表在册 22 种(迭代 46 换图系扩签 refresh_request/board_refreshed 换代)〔锻造工坊批 V11〕",
|
||||
EffectEngineScript.EVENTS.size() == 22)
|
||||
@@ -228,16 +228,14 @@ func _smoke_enemy_bands() -> void:
|
||||
int(row.get("depth_band", -1)), String(row.get("enemy_id", ""))])
|
||||
Kit.check("敌人表 28 行且键序 = 覆盖表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕",
|
||||
_db.combat.enemy.rows.size() == 28 and got_keys == expect_keys)
|
||||
# ② hobgoblin 缺口行留任随迁(契约 9:不删行不补立绘——缺口登记随行)
|
||||
# ② hobgoblin 缺口行留任随迁(不删行不补立绘——补齐归敌种内容批)
|
||||
var hob := {}
|
||||
for row in _db.combat.enemy.rows:
|
||||
if String(row.get("enemy_id", "")) == "hobgoblin":
|
||||
hob = row
|
||||
Kit.check("hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕〔fv25〕",
|
||||
Kit.check("hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕",
|
||||
int(hob.get("level_index", -1)) == 1 and String(hob.get("row_kind", "")) == "battle" \
|
||||
and int(hob.get("depth_band", -1)) == 3)
|
||||
Kit.check("hobgoblin 缺口登记随行(表文件注释在位——缺口不静默)〔fv25〕",
|
||||
FileAccess.get_file_as_string("res://data/tables/enemy.tres").contains("hobgoblin 行为缺口行"))
|
||||
# ③ Boss 行集带键 = 覆盖表(旧判据「各恰一行=图 Boss 节点深度」属单终点模板时代;
|
||||
# 生成图多终点要求每终点深度解析到互异带键 ⇒ 每关 Boss 行必然多于一行——
|
||||
# 迭代 35 覆盖批随动改写为带键清单,「带键互异」的运行时把关在 level_shop_economy_test〔v11〕)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
extends SceneTree
|
||||
# 营地场景布局数据件校验套件(V14——camp_scene_layout.tres 加载期 fail-fast 判据+七命中区几何红线机检)
|
||||
# 覆盖:校验函数通过(键唯一/贴图在画布/命中成对/围墙带包络)· 七可点件键集 · 两两不相交 ·
|
||||
# 不越画面 · 不侵顶栏禁区 · 篝火命中只取下半(关系谓词)· 门扇对齐门洞缺口 · 布告板挂围墙带。
|
||||
# 断言全部为表内关系谓词——禁坐标字面量(数值单一事实源 = 数据行本表)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/camp_scene_layout_test.gd(expect 0)
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
const _EXPECTED_KEYS: Array[StringName] = [&"gate", &"forge", &"dummy", &"chest", &"notebook", &"board", &"campfire"]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_test_layout_hotspot_geometry")
|
||||
|
||||
|
||||
func _test_layout_hotspot_geometry() -> void:
|
||||
var table: CampSceneLayoutTable = load("res://data/tables/camp_scene_layout.tres")
|
||||
Kit.check("V14 布局表加载且 schema 类在位〔营地图批 V14〕", table is CampSceneLayoutTable and table != null)
|
||||
if table == null:
|
||||
quit(Kit.finish("camp_scene_layout_test"))
|
||||
return
|
||||
var problems: Array[String] = table.validate()
|
||||
Kit.check("V14 校验函数通过(问题清单空,实得 %s)〔营地图批 V14〕" % [problems], problems.is_empty())
|
||||
var clickables: Array[CampSceneLayerSpec] = []
|
||||
for spec in table.layers:
|
||||
if spec.is_clickable():
|
||||
clickables.append(spec)
|
||||
Kit.check("V14 可点件数=7(实得 %d)〔营地图批 V14〕" % clickables.size(), clickables.size() == 7)
|
||||
var got_keys: Array[StringName] = []
|
||||
for spec in clickables:
|
||||
got_keys.append(spec.hotspot_key)
|
||||
got_keys.sort()
|
||||
var want_keys: Array[StringName] = []
|
||||
for k in _EXPECTED_KEYS:
|
||||
want_keys.append(k)
|
||||
want_keys.sort()
|
||||
Kit.check("V14 命中键集=七物件绑定表(实得 %s)〔营地图批 V14〕" % [got_keys], got_keys == want_keys)
|
||||
# 两两不相交(Rect2.intersects——贴边相触不算相交)
|
||||
var disjoint := true
|
||||
var pair := ""
|
||||
for i in clickables.size():
|
||||
for j in range(i + 1, clickables.size()):
|
||||
if clickables[i].hotspot.intersects(clickables[j].hotspot):
|
||||
disjoint = false
|
||||
pair = "%s×%s" % [clickables[i].key, clickables[j].key]
|
||||
Kit.check("V14 七命中区两两不相交(首违例 %s)〔营地图批 V14〕" % pair, disjoint)
|
||||
# 不越画面+不侵顶栏禁区
|
||||
var in_bounds := true
|
||||
var offender := ""
|
||||
for spec in clickables:
|
||||
if spec.hotspot.position.x < 0.0 or spec.hotspot.position.y < table.hud_band_h \
|
||||
or spec.hotspot.end.x > table.canvas.x or spec.hotspot.end.y > table.canvas.y:
|
||||
in_bounds = false
|
||||
offender = String(spec.key)
|
||||
Kit.check("V14 命中区不越画面且不侵顶栏禁区(y≥%d,违例 %s)〔营地图批 V14〕" % [table.hud_band_h, offender], in_bounds)
|
||||
# 篝火命中只取下半:命中区贴贴图底、严格小于贴图、起点在贴图内(火焰上段不可点)
|
||||
var fire: CampSceneLayerSpec = null
|
||||
var gate: CampSceneLayerSpec = null
|
||||
var board: CampSceneLayerSpec = null
|
||||
var wall: CampSceneLayerSpec = null
|
||||
for spec in table.layers:
|
||||
match String(spec.key):
|
||||
"campfire": fire = spec
|
||||
"gate": gate = spec
|
||||
"board": board = spec
|
||||
"wall": wall = spec
|
||||
var fire_ok := fire != null and fire.hotspot.end.y == fire.texture_rect.end.y \
|
||||
and fire.hotspot.size.y < fire.texture_rect.size.y \
|
||||
and fire.hotspot.position.y > fire.texture_rect.position.y
|
||||
Kit.check("V14 篝火命中只取下半(火堆贴底、火焰不点)〔营地图批 V14〕", fire_ok)
|
||||
# 门扇命中对齐门洞缺口(表内 gate_gap 字段关系谓词)
|
||||
var gate_ok := gate != null and gate.hotspot.position.x == float(table.gate_gap.x) \
|
||||
and gate.hotspot.end.x == float(table.gate_gap.y)
|
||||
Kit.check("V14 门扇命中对齐门洞缺口 %s〔营地图批 V14〕" % [table.gate_gap], gate_ok)
|
||||
# 布告板挂围墙带:命中顶落在围墙贴图带内
|
||||
var board_ok := board != null and wall != null \
|
||||
and board.hotspot.position.y >= wall.texture_rect.position.y \
|
||||
and board.hotspot.position.y <= wall.texture_rect.end.y
|
||||
Kit.check("V14 布告板命中挂围墙带〔营地图批 V14〕", board_ok)
|
||||
# 层序结构:10 层且 bg/wall 压前两位(z0/z1——子节点序=表序先验)
|
||||
var order_ok := table.layers.size() == 10 \
|
||||
and String(table.layers[0].key) == "bg" and String(table.layers[1].key) == "wall"
|
||||
Kit.check("V14 层数=10 且 bg/wall 占 z0/z1〔营地图批 V14〕", order_ok)
|
||||
# ConfigDB 装载链:表经单点注册进表现域容器且装载期校验过(fail-fast 钩子在位)
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true)
|
||||
Kit.check("V14 ConfigDB 装载含营地布局表且校验过(is_ready=%s)〔营地图批 V14〕" % db.is_ready,
|
||||
load_ok and db.is_ready and db.presentation.camp_scene_layout != null)
|
||||
quit(Kit.finish("camp_scene_layout_test"))
|
||||
@@ -0,0 +1 @@
|
||||
uid://ckyd57l11408
|
||||
@@ -15,7 +15,7 @@ func _initialize() -> void:
|
||||
|
||||
func _row_by_id(ov) -> Dictionary:
|
||||
var out := {}
|
||||
for r in ov._forge._forge_rows:
|
||||
for r in ov._forge_rows:
|
||||
out[String(r.route_id)] = r
|
||||
return out
|
||||
|
||||
@@ -30,7 +30,7 @@ func _run() -> void:
|
||||
var sv = load("res://core/save_manager.gd").new(_TEST_SAVE)
|
||||
sv.load_from_disk()
|
||||
var mp = load("res://core/meta_progression.gd").new(db, sv)
|
||||
var ov = load("res://presentation/views/camp_overlay_view.gd").new()
|
||||
var ov = load("res://presentation/views/forge_view.gd").new()
|
||||
root.add_child(ov)
|
||||
var buys := []
|
||||
var unlocks := []
|
||||
@@ -42,16 +42,16 @@ func _run() -> void:
|
||||
# ① 锁定态:树钥匙未点亮 → 工作间不可见、两区零行板(无任何可交互购买路径)
|
||||
ov.open_camp({"balance": 999, "forge": mp.forge_shop_status(), "tree": []})
|
||||
Kit.check("锁定态:工作间不可见(forge_workshop 钥匙未点亮)〔锻造工坊批 V13〕",
|
||||
not ov._forge._forge_workbench.visible and ov._forge._forge_lock_l.visible)
|
||||
not ov._forge_workbench.visible and ov._forge_lock_l.visible)
|
||||
Kit.check("锁定态:两区零行板(占位纪律——零购买路径)〔锻造工坊批 V13〕",
|
||||
ov._forge._forge_zone_weapon.get_child_count() == 0 and ov._forge._forge_zone_armor.get_child_count() == 0)
|
||||
ov._forge_zone_weapon.get_child_count() == 0 and ov._forge_zone_armor.get_child_count() == 0)
|
||||
|
||||
# ② 解锁态:钥匙点亮 → 两区各三路线
|
||||
ov.open_camp({"balance": 500, "forge": mp.forge_shop_status(),
|
||||
"tree": [{"node_id": "forge_workshop", "level": 1}]})
|
||||
Kit.check("解锁态:工作间可见(钥匙点亮展开)〔锻造工坊批 V13〕", ov._forge._forge_workbench.visible)
|
||||
Kit.check("解锁态:工作间可见(钥匙点亮展开)〔锻造工坊批 V13〕", ov._forge_workbench.visible)
|
||||
Kit.check("两区各三路线(武器锻造区 3 + 护甲锻造区 3)〔锻造工坊批 V13〕",
|
||||
ov._forge._forge_zone_weapon.get_child_count() == 3 and ov._forge._forge_zone_armor.get_child_count() == 3)
|
||||
ov._forge_zone_weapon.get_child_count() == 3 and ov._forge_zone_armor.get_child_count() == 3)
|
||||
var rows: Dictionary = _row_by_id(ov)
|
||||
Kit.check("六路线行册恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V13〕",
|
||||
rows.has("sword") and rows.has("spear") and rows.has("staff")
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 战斗一屏样张批 V5–V9 断言面(2026-10-29 迭代 43)——五项实装定值的真实入口断言:
|
||||
# 战斗一屏样张批 V5–V12 断言面(2026-10-29 迭代 43 立五项;2026-10-31 迭代 46 卡B 增 V10–V12)——
|
||||
# 实装定值的真实入口断言:
|
||||
# V5 血条填充 = 危险红(UiKit.HP_BAR_FILL 值单点 + 绘制点源码锚:全仓零 #689CE8 与零裸
|
||||
# Color(0.41,0.61,0.91);条底收 HP_BAR_TRACK 常量);
|
||||
# V6 站桩名字行垫 HP_BAR_BACK 素底且不压地台(名字行基线 y < 地台顶 y——name_line_probe 几何出口);
|
||||
# V7 顺位徽标提档 UiKit.FS_HINT(值断言 + 绘制点源码锚,§7.4 清单外 11–13px 提档);
|
||||
# V8 护甲图标三键(light/ward/heavy)绑对应 24×24 PNG(ResourceLoader 装载结果 + name_line_probe
|
||||
# 绑定面);
|
||||
# V9 地台件 scene_stage_plaza.png + 护甲三枚入包可加载且尺寸 48×12 / 24×24(§5.2/§5.3 工单)。
|
||||
# V9 地台件 scene_stage_plaza.png + 护甲三枚入包可加载且尺寸 48×12 / 24×24(§5.2/§5.3 工单);
|
||||
# V10 名字行提档 FS_HINT 14(探针 name_font_size 实读 + 两绘制点源码锚 + FS_CAPTION 计数锚==1);
|
||||
# V11 退役常量不在册 + 掩尘 BRICK 逐位 + 顶缘线/口沿高光两绘制点改指源码锚;
|
||||
# V12 PLAZA_EDGE 逐位 #4C4038 + 回退描边改指源码锚。
|
||||
# headless 无渲染面:像素采样以常量单点 + 源码锚 + 探针几何承载(观测面注记,验收对照表 V5 行)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/standee_present_values_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
@@ -48,14 +52,39 @@ func _init() -> void:
|
||||
Rect2(nl.plate).end.y <= float(nl.plinth_top_y) + 0.01)
|
||||
Kit.check("名字行素底 = HP_BAR_BACK 深档 #282220(挂载底色口径)〔样张批 V6〕",
|
||||
Color(nl.plate_color).to_html(false) == "282220")
|
||||
Kit.check("基线读数与实装几何同源(基线 = 地台顶上方 9px 档:149 < 158)〔样张批 V6〕",
|
||||
absf(float(nl.baseline_y) - 149.0) < 0.51 and absf(float(nl.plinth_top_y) - 158.0) < 0.51)
|
||||
Kit.check("基线读数与实装几何同源(基线 = 地台顶上方 8px 档:150 < 158——14px 档牌顶+18)〔样张批 V6〕",
|
||||
absf(float(nl.baseline_y) - 150.0) < 0.51 and absf(float(nl.plinth_top_y) - 158.0) < 0.51)
|
||||
|
||||
# ---- V7 顺位徽标提档 FS_HINT ----
|
||||
Kit.check("FS_HINT = 14(§7.4 六档)〔样张批 V7〕", UiKit.FS_HINT == 14)
|
||||
Kit.check("顺位徽标绘制点取 UiKit.FS_HINT(源码锚)〔样张批 V7〕",
|
||||
body_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT"))
|
||||
|
||||
# ---- V10 名字行提档 FS_HINT 14(迭代 46 卡B——五取值点+基线随档联动) ----
|
||||
Kit.check("名字行探针字号实读 = FS_HINT(几何单点出口 name_font_size)〔名字行 V10〕",
|
||||
int(nl.name_font_size) == UiKit.FS_HINT)
|
||||
Kit.check("名字与护甲两绘制点取 FS_HINT(源码锚——沿 V7 先例)〔名字行 V10〕",
|
||||
body_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.WARM")
|
||||
and body_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, col"))
|
||||
Kit.check("全文件 FS_CAPTION 仅剩消散提示一处(计数锚==1——三处宽度测量点强制换代)〔名字行 V10〕",
|
||||
body_src.count("UiKit.FS_CAPTION") == 1)
|
||||
|
||||
# ---- V11/V12 地台色合并(迭代 46 卡B——常量面载体=Script 常量表,源码锚正证改指目标色) ----
|
||||
var uikit_script: Script = load("res://presentation/views/ui_kit.gd")
|
||||
var cmap: Dictionary = uikit_script.get_script_constant_map()
|
||||
var cell_src := _read_source("res://presentation/views/cell_base_layer.gd")
|
||||
Kit.check("退役常量不在册(STAND_STONE_RIM/STAND_STONE_EDGE 零键)〔名字行 V11〕",
|
||||
not cmap.has("STAND_STONE_RIM") and not cmap.has("STAND_STONE_EDGE"))
|
||||
Kit.check("掩尘 BRICK 值逐位 #C9AF83(合并目标①)〔名字行 V11〕",
|
||||
UiKit.BRICK.to_html(false) == "c9af83")
|
||||
Kit.check("顶缘受光线与口沿高光两绘制点改指 BRICK(源码锚)〔名字行 V11〕",
|
||||
body_src.contains("ped.size.x, 2.0), UiKit.BRICK, true")
|
||||
and cell_src.contains("FACADE_HIGHLIGHT_PX), UiKit.BRICK, true"))
|
||||
Kit.check("PLAZA_EDGE 值逐位 #4C4038(合并目标②)〔名字行 V12〕",
|
||||
UiKit.PLAZA_EDGE.to_html(false) == "4c4038")
|
||||
Kit.check("回退描边绘制点改指 PLAZA_EDGE(源码锚)〔名字行 V12〕",
|
||||
body_src.contains("UiKit.PLAZA_EDGE"))
|
||||
|
||||
# ---- V8 护甲图标三键绑 24×24 PNG ----
|
||||
var armor_paths := {"light": UiKit.TEX_ARMOR_LIGHT, "ward": UiKit.TEX_ARMOR_WARD,
|
||||
"heavy": UiKit.TEX_ARMOR_HEAVY}
|
||||
|
||||
@@ -52,7 +52,6 @@ func _init() -> void:
|
||||
_smoke_diag_button_retired()
|
||||
_status_panel_projection()
|
||||
_career_overlay_projection()
|
||||
_camp_recent_projection()
|
||||
quit(Kit.finish("theme_projection_test"))
|
||||
|
||||
|
||||
@@ -708,12 +707,6 @@ func _smoke_element_details() -> void:
|
||||
var shop_src: String = FileAccess.get_file_as_string("res://presentation/views/shop_overlay_view.gd")
|
||||
Kit.check("T5③ 商店卡片区垂直居中(ALIGNMENT_CENTER)",
|
||||
shop_src.contains("_cards_box.alignment = BoxContainer.ALIGNMENT_CENTER"))
|
||||
var camp_src: String = FileAccess.get_file_as_string("res://presentation/views/camp_overlay_view.gd")
|
||||
var camp_min := camp_src.count("custom_minimum_size = Vector2(0, 336)")
|
||||
Kit.check("T5③ 营地三 holder 统一 min 高 336(实测 %d 处)" % camp_min, camp_min == 3)
|
||||
var camp_fill := camp_src.count("size_flags_vertical = Control.SIZE_FILL")
|
||||
Kit.check("T5③ 营地四栏面板纵向 FILL 等高(实测 %d 处)" % camp_fill, camp_fill == 4)
|
||||
Kit.check("T5③ 三处锁文垂直居中在位", camp_src.count("SIZE_SHRINK_CENTER") >= 3)
|
||||
|
||||
|
||||
# ---- 诊断转储钮冒烟(角落钮不在玩家画面+转储入口在调试浮窗;V6)----
|
||||
@@ -796,20 +789,6 @@ func _all_labels(root: Node) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
# ---- 营地最近冒险行投影(口径换代 V6:完成关数措辞钉子——open_camp 全量重建路径直调;
|
||||
# 最小档 data=recent 单条 completed 局,其余键缺省走 .get 兜底——与组合根 _camp_data 投影同键面)----
|
||||
func _camp_recent_projection() -> void:
|
||||
print("--- 营地·最近冒险行投影(open_camp 最小档)---")
|
||||
var camp = load("res://presentation/views/camp_overlay_view.gd").new()
|
||||
camp.open_camp({"balance": 0, "recent": [
|
||||
{"result": "completed", "levels_cleared": 2, "keystone_total": 5, "master_seed": 42}]})
|
||||
var recent_text: String = String(camp._side._recent_list.text)
|
||||
Kit.check("营地最近冒险行完成关数措辞(实际「%s」)〔口径换代 V6〕" % recent_text,
|
||||
recent_text.contains("完成 2 关") and not recent_text.contains("通关"))
|
||||
camp.visible = false
|
||||
camp.free()
|
||||
|
||||
|
||||
# ---- 生涯页只读投影一致性(汇总段六类九项逐行 == SaveManager 计数访问器/
|
||||
# MetaProgression 访问器读数;投影拼装与 HudPresenter.career_data 同构(同访问器族同序)。
|
||||
# 只读性连接巡检与零动效断言归 ui_struct_probe 呈现侧,本段只管数据面+旧档载入显示
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# 迭代 46 GDD 反哺点清单(2026-10-31)
|
||||
|
||||
> 接收方:待办清单「反哺点专条」通道——用户会话逐条裁决后由文档批写入 `design/`,写入后本清单条目清掉(吸收即死)。
|
||||
> 依据:迭代 46 实现报告(同目录)与实测评读数。实现批禁写 `design/`,以下为待裁决落文面。
|
||||
|
||||
## 卡A 换图系双基座七件批
|
||||
|
||||
1. **[奇物.md]** 基础件表(或专长实装状态句):七件(矿脉储能/省力周转/崩壁冲击/蓄力周转/石层缓冲/坑道小憩/矿道红利)由设计声明改为**已实装**(feats.tres 37 行、稀有度 14/13/10/0、cap 均 1);「双基座叠加、后续效果本轮五段结算内兑付」已在案零改述。依据=实现报告 §二 V2–V11。
|
||||
2. **[行动经济.md]** 两件基座由「待实现」改为在册:付费挖格 +1 RAP(受理后一次性入池、拒绝轮零入池)、刷新成本折价 50%(floor 取整、can 与执行同源、只改门槛与扣费不改拒绝判据)。依据=V2/V3。
|
||||
3. **[战斗结算.md]** 全局 `damage_taken` 落码状态补记:敌方攻击/地形/危险格/错误封印资源直伤四路径统一经唯一发射口与唯一消费族;**地形路径消费面自三件扩为五件(+shield)——铁壁自此对地形伤害生效;危险格/错误封印直伤路径自此纳入 damage_taken 订阅面**(用户已裁全局语义,实现报告 §六);成功刷新事实 `board_refreshed` 为七件订阅主挂点(装新板后恰一次发射)。依据=V5/V4。
|
||||
4. **[地图战斗资源.md]** 清图奖励语义确认句:矿道红利不绑清图奖励(每次成功刷新固定 +1 GP 走战斗内缓冲,不读旧图探索进度、不触发清图事件;刷新图零清图奖励守卫不变)——若现行正文已含此意则零改(本行核对性反哺)。依据=V10/V12。
|
||||
5. **[architecture.md]** 「模块归属」增补/现状化三行:①**BattleRefreshFlow**(新 B1 域拆件——刷新可用性门/折价扣费/换图执行/成功事实登记,调用路径呈现层→编排器单行转发→流程件→行动经济/装板壳/授予段;static_scan S7_B1_SET 已随批增补该文件,判据口径不变);②**damage_controller** 行现状化(damage_taken 唯一发射口 `_emit_hero_damage_taken`+唯一消费族 `_apply_hero_damage_effects`(原名 _enemy_shield_apply 改名)+board_flat_hero 结账前统一发射);③**action_economy** 行现状化(dig_paid 命中施加抽私有 `_apply_dig_paid_hits` 单点;request_refresh 收折后价入参)+**grant_segment** 行现状化(apply_board_refreshed 成功事实消费+round_end heal value_counter 兑付面)。依据=实现报告 §五/§八.6。
|
||||
|
||||
## 卡B 站桩名字行提档与地台色合并批
|
||||
|
||||
6. **[art-bible.md §4.1]** 换色合并登记①②句的代码指称换代:①「代码 `STAND_STONE_RIM`」②「代码 `STAND_STONE_EDGE`」两源常量已删除(全仓零引用——唯一残留=退役断言测试串,预期例外);②合并目标 `#4C4038` 补代码名 **`PLAZA_EDGE`**(ui_kit 唯一落点)。依据=卡B 对照表 3/4 行+grep 取证(实现报告 §三)。
|
||||
7. **[art-bible.md §8.2]** 高墙瓦片体系条口沿高光行:「Color8(196,172,132)」值随换值换代为 **`#C9AF83`(201,175,131)**(并档掩尘枚 BRICK 单点);程序化回退顶缘线与口沿高光两绘制点已改指。依据=对照表 3 行。
|
||||
|
||||
## 卡C 棋盘格边长口径收口批(停摆——随裁决落文)
|
||||
|
||||
8. **[battle.md §棋盘格尺寸公式]** 算例表(:148-152)与实机偏差实测落点:1280 档实板可用面积 **(1022, 454)**(算例假设 486 可用高)→ 实测 L1 格边 40px、13×13 推定 30px(恰达标)、**14×14 推定 28px(低于可读下限 30)**;1080 档 (1662, 734) → L1=46、13/14 推定=46(钳制顶格)。落文方向随「30px 下限适用域」裁决(读法甲/乙见停机呈报 §三):读法乙需补「下限按视口档分级」条+算例表填实测值或标注「按旧布局口径」。依据=停机呈报 §一/§四。
|
||||
|
||||
## 术语表
|
||||
|
||||
9. 零新增术语——本批新名(BattleRefreshFlow/_cell_for/PLAZA_EDGE/counter_id 三枚)均属代码标识符与数据行键,不入术语表(登记口径=概念规范名)。
|
||||
@@ -0,0 +1,12 @@
|
||||
# 迭代 46 GDD 反哺点清单——卡C(2026-11-01)
|
||||
|
||||
> 接收方:待办清单「反哺点专条」通道——用户会话逐条裁决后由文档批写入 `design/`,写入后本清单条目清掉(吸收即死)。
|
||||
> 卡A/卡B 反哺条见同目录 `iter46-反哺点-2026-10-31.md`;本文件只收卡C v2.0。
|
||||
|
||||
1. **[battle.md §棋盘格尺寸公式 :144]** 「可读下限 30px——与结构钳制分开:钳制管算式不越界,下限管玩家能否看清」整句**退役**:整句删除,或改写为「格边长可读性以用户验收的现行画面为基准;结构钳制 [26,46] 管算式不越界」(措辞由文档批裁)。依据=2026-11-01 用户三项裁定(锚点=用户验收的现行渲染值,基座档 14×14=28px 达标;30 无实测背书)+停机呈报读数链(`reports/停机呈报-棋盘格边长口径收口批-2026-10-31.md`)+实现报告 §四锚点留档(`iter46-实现报告-卡C-2026-11-01.md`)。随句建议同节载明判据现行形态:全域禁人造格边长可读阈值,接线守卫=生产写入值与单点算式严格相等,最大板推定读数留痕不作判红。
|
||||
|
||||
2. **作废声明**:`iter46-反哺点-2026-10-31.md` 第 8 条(battle.md 算例表改数——随「30px 下限适用域」读法甲/乙裁决落文)**作废**——`:148-152` 算例表已随布局几何单点化批(1cdef6ba)退役,无落点;读法甲/乙之争已被 2026-11-01 用户三项裁定取代(下限句退役,不再有适用域问题)。文档批落文时以本条为准,10-31 清单第 8 条不再执行。
|
||||
|
||||
## 术语表
|
||||
|
||||
零新增术语——本批新名 `_cell_for` 属代码标识符,不入术语表(登记口径=概念规范名)。
|
||||
@@ -0,0 +1,109 @@
|
||||
# 迭代 46 实现报告(2026-10-31)
|
||||
|
||||
> 批次:迭代 46 三卡——卡A 换图系双基座七件批 · 卡B 站桩名字行提档与地台色合并批 · 卡C 棋盘格边长口径收口批。
|
||||
> 工作区:`D:\code\g-plan2-iter46`(分支 iter46,自 b5b2c555 检出)。
|
||||
> **批内状态总览:卡A 收口 · 卡B 收口 · 卡C 停摆(L3 #2 预设停机点,呈报见主区 `reports/停机呈报-棋盘格边长口径收口批-2026-10-31.md`)**。合并阻塞等裁决。
|
||||
|
||||
## 一、各卡状态
|
||||
|
||||
| 卡 | 状态 | 出口证据 |
|
||||
|---|---|---|
|
||||
| 卡A 换图系双基座七件批 | **收口**(T0–T8 全落) | `tools/run_all_gates.sh --code` 7 闸全绿(回执 commit 45b53d68 时点);此后 S9/S7 修正与卡B 提交待迭代级复跑 |
|
||||
| 卡B 站桩名字行提档与地台色合并批 | **收口**(T0–T3 全落) | standee_present_values_test 26 checks 绿;golden 板面三张 0.00% 留痕;gdstyle 零新增键 |
|
||||
| 卡C 棋盘格边长口径收口批 | **停摆**(T0 完成;T1–T3 改动已验未提交;T4 未走) | 卡C §9 L3 #2:实板可用面积代入 14×14 得 28px < 30px(1280 档)——停卡呈报等裁决 |
|
||||
|
||||
## 二、卡A 实际对照表(改动—验收)
|
||||
|
||||
| 需求点 | 改动(文件:行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 结果 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| V1 词表扩签 | effect_engine.gd EVENTS 20→22/MINE_EVENTS 同步换图刷新面/EFFECT_KINDS 32→34;forge_table/effect_words 计数换代 | `effect_words_t1_test.gd:_init`(新增①-a 两断言);`forge_table_test.gd:_test_effect_kinds_closed` | 单元 | validate 与整表加载 | EVENTS=22、EFFECT_KINDS=34、出域仍拒(旧负例全保留) | 绿(20/41 checks) |
|
||||
| V2 矿脉储能 | action_economy.gd dig_paid 命中环累计 grant_reserve_ap(扣费受理后一次性入池·拒绝轮零入池);feats.tres vein_reserve 行 | `refresh_feats_test.gd:test_reserve_on_accepted_dig` | 接口 | `settle_batch(dig)`(AutoPlayer 真实挖掘) | RAP 恰+1(8→9)、AP 按表扣(8→7)、耗尽轮增量==受理挖数 | 绿 |
|
||||
| V3 省力周转 | BattleRefreshFlow `_effective_refresh_cost`(声明序第一命中 floor(base×mult/100),can 与执行同源);request_refresh 收折后价入参;easy_swap 行 | `test_refresh_discount_gate_and_charge`;`test_double_base_stack` | 接口 | `player_request_refresh()` | base 8→扣 4;两次折后扣尽 8→0;门槛拒零受理;双基座同场生效 | 绿 |
|
||||
| V4 成功事实 | battle_refresh_flow execute 成功序(折价扣RAP→used+1→旧资源退役→旧板退役→装新板→apply_board_refreshed);编排器两公开方法单行转发 | `test_board_refresh_fact_only_on_success` | 集成 | `player_request_refresh()` | 非 Active/每轮上限/RAP 不足三拒面零事实零换图(probe_br_counter 计数+board_changed 双观测);成功恰一次 | 绿 |
|
||||
| V5 全局受伤 | damage_controller `_emit_hero_damage_taken` 唯一发射口+`_apply_hero_damage_effects` 唯一消费族(改名自 _enemy_shield_apply,组合次序冻结);敌方攻击/棋盘直伤/地形三路径接入;grant_segment 删本地 _terrain_damage_reduce | `test_all_hero_damage_paths_emit_once`;`test_no_duplicate_damage_taken_event` | 集成 | 敌方攻击、dig danger、seal wrong、terrain 四真实入口 | 每路径承伤单据在案且计数器==行数(逐单恰一次);context 标记闸防地形二次发射 | 绿 |
|
||||
| V6 石层缓冲 | round_swap_guard 计数+damage_taken when≥1 damage_reduce 2(round 桶轮始自清) | `test_stone_mitigation_all_damage` | 集成 | 刷新→危险格(pierce 8)/地形两真实受伤路径 | pierce 8→6(双刷不叠加)、未刷新轮不减(8)、地形 max(下限, effect_value−2) 逐单闭合 | 绿 |
|
||||
| V7 崩壁冲击 | apply_board_refreshed open_ticket blast→reward_grant.defer_ticket(案C 同队列);SOURCE_EXEMPT_CLASSES+fx_board_refresh;wall_blast 行 | `test_blast_defers_to_combat_segment` | 集成 | 刷新→停手 | 刷新时点零伤害行;战斗段末恰一单 ×2 全体存活敌 | 绿 |
|
||||
| V8 蓄力周转 | swap_charge_layers 战斗桶+attack_bonus value_counter+set0(既有消费面零新码) | `test_charge_layers_consumed_same_round` | 接口 | 两次刷新→停手(主角先手攻击) | 双刷 2 层→攻击后 0;白盒预置 3 层时刷新被闸(上限 3);战斗新建 0 | 绿 |
|
||||
| V9 坑道小憩 | round_swap_rest+round_end heal value_counter(grant_segment 新增 `_round_end_heal_value_counter`——先于 set0 行消费层值) | `test_rest_rotation_cap_and_clear` | 集成 | 刷新→停手(轮末授予段) | 回复 min(3,缺血)==3、层清 0、满血发未生效行、自然累计 2 路径 | 绿 |
|
||||
| V10 矿道红利 | board_refreshed grant_gp→escrow+fx_grant_gp 行(apply_board_refreshed 消费);mine_profit 行 | `test_mine_profit_gp` | 集成 | `player_request_refresh()` | escrow 每成功+1(0→1→2)、fx_grant_gp 行在案、拒绝零变化 | 绿 |
|
||||
| V11 数据与池 | feats.tres 七行(rarity 14/13/10/0·cap 1);adventure_feats 30→37/牌库 132→139 | `test_seven_feats_validate_and_pool`;adventure_feats `_smoke_feats` | 单元 | ConfigDB 整表与冒险牌库 | rows 37、relic 37、14/13/10、deck 139、validate 空、禁词 0、七件计数器引用闭合 | 绿(85/138 checks) |
|
||||
| V12 无专长回归 | round_flow/seal 旧基线零改动(刷新体迁移编排器单行转发) | `_smoke_active_refresh`;`_smoke_refresh_provenance_guard`;`test_no_feats_baseline` | 单元+集成 | 现行刷新/清图入口 | RAP8、AP 保留、上限 2、刷新图零清图、挖格零产 RAP、表基线扣费 | 绿(134/70 checks) |
|
||||
|
||||
**窄测实跑记录**:refresh_feats 85 · adventure_feats 138 · round_flow 134 · seal_polarity 70 · damage_controller 44 · effect_words 20 · forge_table 41 · config_db 29——全绿。批末 `--code` 闸 00/01/02/03/05/09/10 全绿(回执 45b53d68)。
|
||||
|
||||
## 三、卡B 实际对照表(改动—验收)
|
||||
|
||||
| # | 需求点 | 改动 | 验收测试 | 结果 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 名字行字号提档 FS_HINT 14 | standee_body.gd 五取值点+name_line_probe 增 name_font_size | 〔名字行 V10〕探针实读==FS_HINT+两绘制点源码锚+FS_CAPTION 计数锚==1 | 绿 |
|
||||
| 2 | 名字行几何随档联动 | _name_baseline 偏移 17→18+注释换代 | V6 换代读数 150±0.51(8px 档)且 < plinth_top_y | 绿 |
|
||||
| 3 | 受光线常量退役改指 BRICK | ui_kit 删 STAND_STONE_RIM;standee_body:386+cell_base_layer:176 改指 | 〔名字行 V11〕常量表零键+BRICK==#C9AF83+两绘制点源码锚;grep 反证(仅剩测试断言串——预期例外) | 绿 |
|
||||
| 4 | 描边常量退役改指新 token | ui_kit 删 STAND_STONE_EDGE+增 PLAZA_EDGE=Color8(76,64,56);standee_body:385 改指 | 〔名字行 V12〕PLAZA_EDGE 逐位+常量表零旧键+描边源码锚 | 绿 |
|
||||
| 5 | 板面 golden 与换值一致 | (预期重锚——实际无需) | render_gate 板面三张 0.00%(改后跑,基线未动) | 绿(见实现决定 #4) |
|
||||
| 6 | 批末收口 | 全批 | 迭代级 --code 闸(待卡C 裁决后统一走) | 待迭代出口 |
|
||||
|
||||
卡B 改前读数(T0):standee_present_values_test 18 checks 绿 · gdstyle 5 键全基线内 · 渲染闸绿(--code 回执 45b53d68 内 05 闸)· 字体度量 ts(14px)=(14,20)。
|
||||
|
||||
## 四、卡C 停摆实况(改动已验未提交)
|
||||
|
||||
- T0 完成:行号重锚(board_view set_cell_area 现 :206-224、layout_probe MIN_CELL :15、冒烟 :53-69);取档对账 resolve_row(1,0)/(2,0)/(3,0)/(3,9)=10/12/13/14 全符;改前读数 board_gen_reward_test 62 绿+layout_probe 两档绿+gdstyle board_view max-public-methods 27 持平。
|
||||
- T1 算式单点:`BoardView._cell_for(n, avail)` 提取(逐位同式)+set_cell_area 改调——等价性由 board_gen_reward_test 既有断言承载(62 checks 绿)+gdstyle 持平。
|
||||
- T2 探针:MIN_CELL 36→30+注释改指 battle.md+BoardViewScript preload+零尺寸前置+13/14 推定断言——**推定断言触发 L3 #2(14×14=28<30)**。
|
||||
- T3 冒烟四档:取样表 [{1,0,10},{2,0,10→12},{3,0,13},{3,9,14}]+板型一致断言+算式单点等价断言——74 checks 绿(单元面全过:钳制 [26,46] 面无越界)。
|
||||
- 三件改动**未提交**(停摆卡不提交),随裁决处置(可复用——行为零变化)。
|
||||
- 停机呈报:主区 `reports/停机呈报-棋盘格边长口径收口批-2026-10-31.md`(分歧读法/建议案/待办回退拟稿)。
|
||||
|
||||
## 五、结构预算(S13 前后)
|
||||
|
||||
| 触面 | 前测 | 后测 | 判定 |
|
||||
|---|---|---|---|
|
||||
| battle_orchestrator.gd | 811 行/类变量 15(卡A 写 38 系笔误) | 803 行(−8 达标)/类变量 15 | 只降不升 ✓ |
|
||||
| reward_grant.gd | 类变量 15 | 15(defer_ticket 为方法零增员) | ✓ |
|
||||
| grant_segment.gd | 类变量 3/公开方法 1 | 3/2(apply_board_refreshed);私有新增 1(_round_end_heal_value_counter)≤4 | ✓ |
|
||||
| damage_controller.gd | 公开方法 9 | 9(私有净增 1:_emit_hero_damage_taken)≤2 | ✓ |
|
||||
| action_economy.gd | 公开方法 15 | 15(命中环抽私有 _apply_dig_paid_hits——S13 分支超限重构,分支本体逐字迁入) | ✓ |
|
||||
| battle_refresh_flow.gd(新件) | — | 公开函数 2(can_refresh/execute)/类变量 0 | ≤20/≤15 ✓ |
|
||||
| gdstyle_gate | 5 键全基线内 | 5 键全基线内零新增零恶化 | ✓ |
|
||||
|
||||
## 六、旧 damage_taken 专长覆盖面变化
|
||||
|
||||
全局统一后,订阅 `damage_taken` 的既有专长(背水 hp_ratio when/石肤 ac_bonus+counter_add/保险丝 counter_add)与换图系石层缓冲的覆盖面变化:
|
||||
|
||||
- **敌方攻击路径**:不变(原即经 _enemy_shield_apply 全组合消费)。
|
||||
- **地形路径**:消费族从本地三件(damage_reduce/ac_bonus/counter_add)扩为全局五件(+shield)——**铁壁(shield)自此对地形伤害生效**(用户已裁全局语义;卡A §十二削弱申报第 2 条在案)。
|
||||
- **危险格/错误封印资源直伤路径(board_flat_hero)**:原零事件,现统一发射——上述专长自此对这些路径生效(同上申报面)。
|
||||
- 无专长基线逐位零漂移(V12 全绿;同一伤害恰一次由 V5 钉死)。
|
||||
|
||||
## 七、实机目检总清单(登记不阻塞)
|
||||
|
||||
1. 〔卡A〕玩家持基座与收益件后,是否主动规划刷新而非只在坏图刷新(行为观感)。
|
||||
2. 〔卡B〕站桩名字行 14px 清晰可读且长名不截字(验收底线句)。
|
||||
3. 〔卡B〕墙格口沿亮线换值后观感无异(#C4AC84→#C9AF83)。
|
||||
4. 〔卡B〕贴图缺位回退描边与顶缘线观感无异(PLAZA_EDGE/BRICK 程序化回退)。
|
||||
5. 〔卡C·停摆批登记〕第三关 14×14 板格面数字在基座档的可辨读性(28px 实况——随裁决)。
|
||||
|
||||
## 八、实现决定记录(超白名单/偏差备案)
|
||||
|
||||
1. **卡A 自洽扫描笔误三处(不改合同)**:「类变量 38」实为 15;gdstyle_baseline.txt:9 引用实无 battle_orchestrator 键(该文件现无 S13 键);稀有度断言实挂 :476 非 :465。T0 按实数重锚记录,合同(只降不升/换代判据)不受影响。
|
||||
2. **卡A T0 工具名**:卡写 `bash tools/gdstyle.sh game`——该文件不存在,实际工具 `python tools/gdstyle_gate.py`(与卡B/C 一致),按实跑。
|
||||
3. **MINE_EVENTS 同步口径**:卡A T1「同步 :19 的 MINE_EVENTS」取「两新事件入集」读法——弃图刷新属扫雷侧动作面,流派铁律订阅集随之认列(零机械消费面差异,mine_covered 生产无强制点)。
|
||||
4. **卡B golden 预期红未现**:卡预期「板面三张红→重锚」,实测三张 0.00%——口沿高光换值 #C4AC84→#C9AF83 每通道差 ≤5,低于逐像素容差(≤16 差/≤2%),golden 判定零变化;重锚步骤无需执行(基线未动、零差留痕达成 V5 判据;禁借重锚夹带红线零触碰)。
|
||||
5. **S9 修正**:四处显式非参数化容器注解改 `:=` 推断(R1)——零行为改动。
|
||||
6. **S7 域集增补**:`static_scan.sh` S7_B1_SET 增补 battle_refresh_flow.gd——换图刷新流程件=卡A C13 拆分边界的 B1 域拆件(承载 retire_mapping/装板编排调用,卡A §八明列新结算族件);判据口径不变(B2/B3/B4 对外 API 仍限 B1 域调用)。**architecture.md「模块归属」需补 BattleRefreshFlow 行——已入反哺点清单**。
|
||||
7. **V11 引用闭合判据域**:全表扫描发现预存行完美保险 when 读 insurance_used 无 counter_add 写入方(消费点落账合法形态——feats.tres 注记在案)——判据域收窄为「换图系七件自身引用闭合」,预存形态不入判据。
|
||||
8. **apply_board_refreshed 访问形态**:BattleRefreshFlow 经 `oc._rounds._grant_seg` 直调(round_cycle 不在卡A §八改动文件清单内,不加转发壳——沿结算族直调惯例)。
|
||||
9. **卡A T8「代拍板追认单」不另立**:iter 技能与 process-conventions §2 定「超白名单记实现报告实现决定记录,不另立追认单」——本批全部决定在上列备案,白名单内决定零追认面。
|
||||
10. **测试断言口径修正(开发期自纠)**:V5②③④ 聚合判据从「两来源行合计」修正为「全 hero 行数」(盲挖途经的其他直伤单据一并入账);V2 辅证改「RAP 增量==受理挖数」(移位耗 AP 不入账);V4 时序修正(停手再入 RAP)——均为测试侧修正,生产码零回退。
|
||||
11. **V9 上限 3 承载形态**:每轮刷新上限 2(表值)使自然路径层值至多 2——「上限 3」判据由白盒计数器预置+真实刷新入口承载(test-conventions §5.4 计数器直改属数据面注入),自然累计 2 另设战场钉死。
|
||||
|
||||
## 九、独立验收审计结论
|
||||
|
||||
- **审计方**:未参与实现的子代理(独立会话上下文),2026-10-31 执行。
|
||||
- **报告**:`reports/iter46-验收审计-2026-10-31.md`(本目录)。
|
||||
- **结论**:**审计通过(零缺漏)**——卡A V1–V12 与卡B 6 行对照表逐行通过;尾缀全可 grep;真实入口零绕过(V2 真挖/V3·V4·V10 经 player_request_refresh/V5 四路径真实战斗驱动);自跑复核 refresh_feats 85/standee 26/adventure_feats 138/round_flow 134/seal_polarity 70/effect_words 20/forge_table 41 全绿+gdstyle 5 键零新增+render_gate 三张 0.00% 实跑;削弱核对全部落在两卡申报面内(round_flow/seal 零进 diff,禁删面原样在案);无变异/撤销类取证。
|
||||
- **审计附加发现处置**:①两枚新件 .uid 边车未随批提交——已补提交(.gitignore 要求);②static_scan S8_FILES 与 S7_B1_SET 未同步增补新件——已同步(新件零 await,扫描面实跑绿);③V10 断言消息在求值前格式化的显示瑕疵(判据本体正确,零处置);④V9 私有读前置守卫附注(审计方判例外不阻塞——判据主面走公共 step_row 信号)。
|
||||
|
||||
## 十、遗留读数义务
|
||||
|
||||
- 卡C 裁决后:探针两档 bv.size/L1 格边/13·14 推定读数已录本报告第四节与停机呈报——重写卡可直接引用,无需重采。
|
||||
- 无 MC/数值验证义务(本批无数值改动,feats.tres 七行为卡内 provisional 值——归调参批)。
|
||||
@@ -0,0 +1,70 @@
|
||||
# 迭代 46 实现报告——卡C 棋盘格边长口径收口批 v2.0(2026-11-01)
|
||||
|
||||
> 批次:迭代 46 卡C(v2.0 重立回编——2026-11-01 用户三项裁定后写卡批重立,合并提交 e7090bc9)。卡A/卡B 已于 2026-10-31 收口合回,见同目录 `iter46-实现报告-2026-10-31.md`。
|
||||
> 工作区:`D:\code\g-plan2-iter46`(分支 iter46,卡C 段基点=master 5269ed40,WIP 三件按 v2 卡 §12 判定复用两件返工一件)。
|
||||
> 需求卡片:`reports/需求卡片-棋盘格边长口径收口批-2026-10-30.md`(v2.0 定稿,零待裁点)。
|
||||
|
||||
## 一、卡C 状态
|
||||
|
||||
**收口**(T0–T4 全落)。游戏行为零变化:算式逐位同式、`set_cell_area` 副作用尾链零动、两条生产调用路径不改、画面像素零变化。本批改动全部落在守卫面=**人造阈值退役+算式单点+13/14 两档入卫**。
|
||||
|
||||
## 二、T0 改前基线读数(2026-11-01,master 5269ed40 态实测)
|
||||
|
||||
| 读数 | 结果 |
|
||||
|---|---|
|
||||
| `python tools/gdstyle_gate.py` | 扫描面 142 件、命中 5 条全在基线、基线外新违规 0、较登记值恶化 0(board_view.gd `max-public-methods=27`/`max-class-variables=31` 持平) |
|
||||
| 单件跑 `board_gen_reward_test` | all green(**62 checks**——改前基线值) |
|
||||
| 单件跑 `layout_probe` | 2 档 PASS(旧阈值形态「≥36px」消息,即本批退役对象) |
|
||||
| 四档取档对账(`resolve_row(1,0)/(2,0)/(3,0)/(3,9)`,临时对账件跑后即删) | 10×10/12×12/13×13/14×14,与卡 §2 第 6 条全符 |
|
||||
| `battle_layout.tres` 五字段 | stage_h=252/ap_bar_h=44/info_panel_w=240/log_band_h_dig=24/log_band_h_settle=84,与卡 §2 第 8 条全符 |
|
||||
|
||||
行号重锚:卡 §2 行号在 5269ed40 上逐一核实(`layout_probe.gd:15` MIN_CELL/`board_view.gd:206-215` 内联算式/`board_zone_view.gd:91-92` 生产调用路径+`board_view.rebuild` 末尾调用/`board_gen_reward_test.gd:53-69` 冒烟)——卡成文时点(3499638d)至 T0 时点间零漂移。`tools/gdstyle_baseline.txt` 两键登记值与卡 §2 第 5 条一致(:11=31、:22=27)。
|
||||
|
||||
## 三、实际对照表(改动—验收)
|
||||
|
||||
| # | 需求点 | 改动 | 验收测试(名称+实际落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 结果 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 1 | 探针阈值退役+格边长接线守卫 | `layout_probe.gd` MIN_CELL 常量整行删除;阈值断言换等价断言(:106-109) | 「两档几何不变量+格边长等价」`layout_probe.gd:_ready`(PASS 行 :45 尾缀〔格边长 V1〕;失败路径 :107) | 探针 | `godot --headless --path game res://probe/layout_probe.tscn` | 实读 `cell_size`(生产经 `set_cell_area` 真实写入)与 `_cell_for(n, bv.size)` 同输入严格相等——生产停止调单点算式即红;`_board` 内部字段直查=受控例外(板型 N 无公开出口,卡 §14.2);`grep -rn "MIN_CELL\|可读门槛\|可读下限" game/` 零命中(已跑,exit=1 零命中) | 绿(两档全过,基座实读 40px) |
|
||||
| 2 | 算式单点(`_cell_for`) | `board_view.gd:210` 改调 `static _cell_for`(:226,逐位同式);:206 注释补唯一落点句 | 「算式单点等价」`board_gen_reward_test.gd:75-76`(尾缀〔格边长 V2〕) | 单元 | `godot --headless --path game --script res://tests/unit/core/board_gen_reward_test.gd` | `set_cell_area` 写入的 `_cell` 与 `BoardView._cell_for(n, avail)` 返回值逐档严格相等(同一事实两路读取,分叉即红) | 绿(四档 4 断言全过;既有四套断言全绿=等价性证明,62→74 checks) |
|
||||
| 3 | 13×13/14×14 进入冒烟 | `board_gen_reward_test.gd:58` 循环集改四档取样表 [(1,0,10),(2,0,12),(3,0,13),(3,9,14)];:69-74 板型一致+既有两断言(前缀 L%d D%d) | 「格边长冒烟四档板型」同函数(尾缀〔格边长 V3〕) | 单元 | 同上 | 13/14 两档首次被断言:取档尺寸与实表一致、网格高 ≤ 可用高−24、格边 ∈ [26,46](既有断言逐字保留) | 绿(四档 12 断言全过,10/12/13/14 实测 41/33/31/28px) |
|
||||
| 4 | 最大板推定守卫(留痕型) | `layout_probe.gd:_check_one` 零尺寸前置(:112-114 记红)+13/14 推定留痕(:116-119 采集,:47-50 PASS 后打印) | 「最大板推定留痕」同探针(尾缀〔格边长 V4〕) | 探针 | 同行 1 | ①零尺寸前置(size ≤ 0 记红);②推定读数打印并与本报告 §四留档锚点读数逐值对拍,不一致=卡 §9 L3 #2 呈报(禁实现侧改锚) | 绿(基座 30/28、全屏 46/46——与锚点逐值一致,零对拍偏差) |
|
||||
| 5 | 批末收口 | 全批 | 代码域闸 `tools/run_all_gates.sh --code` | 全量闸 | `tools/run_all_gates.sh --code` | 所选域全绿回执(`game/tests/.gates/receipt.json`) | 绿(`all_green=true`,七闸 00/01/02/03/05/09/10,commit e3e206d0,域=code·scope=related[presentation],`changed_vs_baseline=[]`) |
|
||||
|
||||
**测试削弱申报核对**(卡 §8 申报全集四条,实际执行逐条对上):①探针阈值断言删除=申报 1 的执行面,补偿三件全落(等价断言/四档结构钳制/推定留痕对拍);②PASS 消息改写=申报 2;③循环集 2→4 档纯增量、样本值 (1036,460) 未改、既有两断言逐字保留=申报 3;④未发生恢复阈值、删旧断言、放宽区间、换读取入口任一项=申报 4 成立。另:既有几何不变量(三区非零/不越视口/互不重叠/view 含于 D 区)与视口档位表零改动(卡 §14.1 核对通过)。
|
||||
|
||||
## 四、锚点读数留档(两档视口 × 四板型——未来批次对照基准)
|
||||
|
||||
锚点=用户验收的现行渲染值(2026-11-01 用户裁,卡 §2 第 7 条)。本批探针实跑复核:
|
||||
|
||||
| 视口 | board_view.size(实读) | 10 板 | 12 板 | 13 板 | 14 板 |
|
||||
|---|---|---|---|---|---|
|
||||
| 基座 1280×800 | (1022.0, 454.0) | **40px(实读)** | 33px(单点算式读) | **30px(推定打印)** | **28px(推定打印)** |
|
||||
| 16:9 全屏 1920×1080 | (1662.0, 734.0) | 46px(实读·钳制顶格) | 46px(单点算式读) | **46px(推定打印)** | **46px(推定打印)** |
|
||||
|
||||
基座档 10 板实读经等价断言与 `_cell_for(10, (1022,454))` 严格相等(V1 守卫当轮执行);13/14 推定由探针 V4 打印(当轮留痕);12 板与全屏 10/12 三格由单点算式读数件在两档真实可用面积上读出(一次性临时件,当轮跑批留痕后即删):基座 12 板=33px、全屏 10/12=46px。全表与卡 §2 第 7 条锚点逐值一致,零对拍偏差。
|
||||
|
||||
## 五、实机目检总清单(登记不阻塞)
|
||||
|
||||
1. **第三关 14×14 板格面数字清楚可辨**——2026-11-01 用户已实际背书现行画面(锚点裁定),登记照留(卡 T4)。
|
||||
|
||||
## 六、实现决定记录
|
||||
|
||||
代拍板白名单 6 项全部按卡执行(`_cell_for` 局部变量名与注释措辞/四档取样表用字典数组/探针消息文案与 PASS 措辞/尾缀〔格边长 V1–V4〕编排/`BoardViewScript` 常量名/`bv.get("_board")` 取值形态),零超范围裁决。超白名单备案三件(均属测量面承载,判据零扩张):
|
||||
|
||||
1. **探针 `_board == null` 前置**(`layout_probe.gd:104-105`)——卡片明列判据只有零尺寸前置与等价断言;null 前置防「内部直查遇 null 即脚本错误静默跳过剩余断言」(test-conventions §7 失败模式),属测量面防御。撤销方式=删该分支(等价断言改回无条件求值)。
|
||||
2. **探针新增两成员 `_cell_reads`/`_est_reads`**(`layout_probe.gd:17-18`)——PASS 行基座实读与推定留痕打印的承载面;探针不在 S13 扫描面,无预算约束。撤销方式=删成员与两处打印(PASS 行退回无读数形态)。
|
||||
3. **单点算式锚点全表读数件**(本报告 §四 12 板与全屏 10/12 三格补读)——卡 §2 第 7 条锚点表含四板型,探针打印只覆盖 13/14(卡定死),全表留档用一次性临时脚本对两档实读可用面积逐板型读 `_cell_for`(当轮跑批留痕后即删)。撤销方式=无(纯读数,不落码)。
|
||||
|
||||
## 七、遗留读数义务
|
||||
|
||||
零。本批无数值改动,MC/verifier 零跑(卡 §4 范围边界 6)。§四全表读数已当轮留痕,无待补。
|
||||
|
||||
## 八、S13 结构预算与批末闸对账
|
||||
|
||||
- `board_view.gd`:`max-public-methods` **27→27**(`_cell_for` 私有名不计数)、`max-class-variables` **31→31**(零新增成员——钳制界未外提)、无 NEW 键;gdstyle 全批「基线外新违规 0、较登记值恶化 0」。
|
||||
- 探针与测试件不在 S13 扫描面(扫描面=产品码,测量面直构为既有惯例)。
|
||||
- 批末代码域闸:`tools/run_all_gates.sh --code` 全绿(回执落 `game/tests/.gates/receipt.json`,T4 当轮执行)。
|
||||
|
||||
## 九、独立验收审计结论
|
||||
|
||||
独立验收审计由未参与实现的子代理执行,报告=`reports/iter46-验收审计-卡C-2026-11-01.md`。结论:**通过,可合并**——五项全过(对照表五行逐行核对/自跑最窄测试实读:探针基座实读 40px·推定 30/28·46/46·单元 74 checks 全绿/判据绑定面钉生产写入值/测试削弱逐处对上卡 §8 申报全集/全仓无人造阈值回归且无变异·撤销取证)。一处非阻断备注已处置:实现报告 §三 行 1 判据绑定面格补「受控例外」句(审计报告原文备注定格于补句前)。审计方自跑读数与本实现会话当轮实跑逐值一致。未过不得合并红线未触发。
|
||||
@@ -0,0 +1,130 @@
|
||||
# 迭代 46 独立验收审计报告
|
||||
|
||||
> 审计方:独立验收审计子代理(未参与本批实现——迭代 loop 内启动,符合 test-conventions §8.7 两入口铁规)。
|
||||
> 日期:2026-10-31。
|
||||
> 工作区:`D:\code\g-plan2-iter46`(分支 iter46,基点 b5b2c555)。Godot:`Godot_v4.7.2-stable_win64_console.exe`。
|
||||
> 审计范围:合并候选=卡A(换图系双基座七件批)+卡B(站桩名字行提档与地台色合并批)已提交 diff=`git diff b5b2c555..c10bbc11`(10 笔提交=9 实现+1 收尾文档;20 文件,+1231/−96)。卡C(棋盘格边长)停摆不计。
|
||||
> 审计方式:不采信实现报告「已全绿」自述——逐行核对对照表两端、自跑最窄套件与闸件、对照 diff 逐处核对削弱申报。本审计只写本报告,零源码/测试/数据文件改动。
|
||||
|
||||
## 一、审计自跑记录(当轮真实执行)
|
||||
|
||||
| 套件/闸 | 结果 | 与报告申报对照 |
|
||||
|---|---|---|
|
||||
| refresh_feats_test | **all green (85 checks)**,exit 0 | 与报告§二 申报 85 一致 |
|
||||
| standee_present_values_test | **all green (26 checks)**,exit 0 | 与报告§三 申报 26 一致 |
|
||||
| adventure_feats_test | **all green (138 checks)**,exit 0 | 与报告§二 申报 138 一致 |
|
||||
| round_flow_skill_pool_test | **all green (134 checks)**,exit 0 | 与报告§二 申报 134 一致 |
|
||||
| seal_polarity_test(附加) | all green (70 checks),exit 0 | 与报告§二 申报 70 一致 |
|
||||
| effect_words_t1_test(附加,V1) | all green (20 checks),exit 0 | 与报告§二 申报 20 一致 |
|
||||
| forge_table_test(附加,V1) | all green (41 checks),exit 0 | 与报告§二 申报 41 一致 |
|
||||
| gdstyle_gate | 5 键全基线内、基线外新违规 0、恶化 0 | 与报告§五「零新增零恶化」一致 |
|
||||
| static_scan.sh | exit 0——S7 ok(B2/B3/B4 符号收敛 B1 文件集)/S8 ok/S13 ok | S7 新域集生效见§六① |
|
||||
| render_gate.sh(窗口化实跑) | 板面三张+menu_boot 全 **changed=0.00%**,全绿 | 与报告§八.4 一致,见§三行 5 |
|
||||
|
||||
## 二、卡A 对照表逐行核对(V1–V12)
|
||||
|
||||
| 需求点 | 测试存在 | 尾缀可 grep | 真实入口无绕过 | 判定 |
|
||||
|---|---|---|---|---|
|
||||
| V1 词表扩签 | `effect_words_t1_test.gd:_init` 新增①-a 两断言+`forge_table_test.gd:_test_effect_kinds_closed` 22/34 换代 | 〔换图系批 V1〕×2+〔锻造工坊批 V11〕×2 | ConfigDB 整表加载+词表常量直读 | **通过**(实跑 20/41 checks 绿;diff 核对 effect_words 纯增量 5 行,旧出域负例零删) |
|
||||
| V2 矿脉储能 | `test_reserve_on_accepted_dig` | 〔换图系批 V2〕×3 | 经 `_auto_dig`→`settle_batch([move]+[dig])` 真实付费挖格;辅证 `_drain_round_rap_accounting` 仍走 settle_batch 逐挖计数 | **通过**(RAP 8→9、AP 按表扣、耗尽轮增量==受理挖数三面全钉) |
|
||||
| V3 省力周转 | `test_refresh_discount_gate_and_charge`+`test_double_base_stack` | 〔换图系批 V3〕×6 | 经 `orch.player_request_refresh()` 唯一入口;can 与执行同源(`_effective_refresh_cost` 单点,diff 核实编排器两公开方法单行转发) | **通过**(折后 −4×2、门槛拒 can=false 且执行拒、双基座同场生效) |
|
||||
| V4 成功事实 | `test_board_refresh_fact_only_on_success` | 〔换图系批 V4〕×6 | 经 `player_request_refresh()`;非 Active 拒=decay 行内同步探测(round_flow_skill_pool_test:161 先例同款),上限拒/RAP 不足拒均在 Active 相位真入口 | **通过**(三拒面 probe_br_counter==0+board_changed 计数冻结双观测;成功恰一次×2+新轮×2) |
|
||||
| V5 全局受伤 | `test_all_hero_damage_paths_emit_once`+`test_no_duplicate_damage_taken_event` | 〔换图系批 V5〕×8 | **四路径全真实战斗驱动、零直调 `_emit`**:①敌方攻击=boss 战零挖掘连停三轮(enemy_action 行在案);②地形=AutoPlayer 盲挖 flat_damage 变异格+stop;③危险格=真挖直接伤型雷格;④错误封印=真 `seal_place`+stop | **通过**(每路径「承伤行数==计数器」恰一次+无重复消费两样本;实跑读数 行2=计2/行9=计9/行2=计2/行2=计2) |
|
||||
| V6 石层缓冲 | `test_stone_mitigation_all_damage` | 〔换图系批 V6〕×3 | 刷新经 `player_request_refresh`;危险格/地形经真挖+stop 结算 | **通过**(pierce 8→6 双刷不叠=叠加态 4 反证钉死;未刷轮不减=round 桶轮始清空;地形 max(下限, effect_value−2) 逐单闭合) |
|
||||
| V7 崩壁冲击 | `test_blast_defers_to_combat_segment` | 〔换图系批 V7〕×2 | 刷新+停手(轮末授予段→战斗段末) | **通过**(刷新时点零伤害行=授予段不结账;停手后恰一单 value 2 全体存活敌) |
|
||||
| V8 蓄力周转 | `test_charge_layers_consumed_same_round` | 〔换图系批 V8〕×2 | 双刷+停手(主角先手攻击)真实消费 | **通过**(白盒预置 3 层上限闸=实现决定 #11 申报面;双刷 2 层攻击后清 0;战斗新建 0) |
|
||||
| V9 坑道小憩 | `test_rest_rotation_cap_and_clear` | 〔换图系批 V9〕×3 | 刷新经真入口;轮末恢复在授予段(stop 触发) | **通过(附未注例外,见§五)**(回复 min(3,缺血)==3 且层清 0;满血未生效行+零 heal 行;自然累计 2 路径) |
|
||||
| V10 矿道红利 | `test_mine_profit_gp` | 〔换图系批 V10〕×4 | 经 `player_request_refresh()`;拒绝=RAP 不足真拒 | **通过**(escrow 每次+1、fx_grant_gp 行恰 1、拒绝零变化;消息文案时点瑕疵见§七.3,不涉判据) |
|
||||
| V11 数据与池 | `test_seven_feats_validate_and_pool`+adventure_feats `_smoke_feats` | 〔换图系批 V11〕×5+〔专长批 V2〕换代句 | ConfigDB 整表+冒险牌库真实启动(seed 71) | **通过**(validate 空/七行稀有度 cap 对表/desc 禁词/七件计数器引用闭合/三计数器在册;rows 37、14/13/10/0、deck 139;feats.tres 七行 trigger 结构逐行对照卡§十.T6 零出入) |
|
||||
| V12 无专长回归 | `test_no_feats_baseline`+round_flow `_smoke_active_refresh`+seal `_smoke_refresh_provenance_guard` | 〔换图系批 V12〕×4 | 挖格/刷新/停手全真入口 | **通过**(挖格零 RAP、表基线扣 8、AP 保留、第 3 次拒;round_flow/seal 两文件**零进 diff**——RAP8/上限2/AP保留/刷新图零清图/五段序/回放断言原样在案,134/70 checks 实跑绿) |
|
||||
|
||||
## 三、卡B 对照表逐行核对(6 行)
|
||||
|
||||
| # | 需求点 | 判定 |
|
||||
|---|---|---|
|
||||
| 1 | 名字行字号提档 FS_HINT 14 | **通过**——diff 核对五取值点(`:334-336` 两测量+`:467` 测量+`:474` 护甲绘制+`:476` 名字绘制)全改 `UiKit.FS_HINT`;`:114` 消散提示零触碰(FS_CAPTION 计数锚==1 断言在案且实跑绿);`name_line_probe` 增 `name_font_size` 键(探针实读==FS_HINT 绿) |
|
||||
| 2 | 名字行几何随档联动 | **通过**——`_name_baseline` 偏移 17→18+注释换代(diff 核实);V6 读数 150±0.51 与「8px 档」句换代=卡B§8 申报第 1 条面内;`baseline_y < plinth_top_y` 文字不压地台判据零改动且绿 |
|
||||
| 3 | 受光线常量退役改指 BRICK | **通过**——ui_kit 真删 `STAND_STONE_RIM` 无别名占位;`standee_body` 顶缘线与 `cell_base_layer:176` 口沿高光两绘制点改指 `BRICK`;V11=常量表零键+BRICK 逐位 c9af83+两处源码锚全绿;grep 全仓退役常量仅剩测试断言串(与报告§三「预期例外」申报一致) |
|
||||
| 4 | 描边常量退役改指新 token | **通过**——ui_kit 删 `STAND_STONE_EDGE`+原位增 `PLAZA_EDGE := Color8(76, 64, 56)`;`standee_body` 回退描边改指;V12=PLAZA_EDGE 逐位 4c4038+源码锚全绿 |
|
||||
| 5 | 板面 golden 与换值一致 | **通过**——审计方自跑 `bash tools/render_gate.sh`(窗口化):board_L1/L2/L3 与 menu_boot 全 0.00%;golden 五文件 git 零改动=基线未动、重锚未执行(与实现决定 #4 备案一致——#C4AC84→#C9AF83 灰度差约 2,低于容差 ≤16,0.00% 为必然);level_map.png 不在 render_gate 对比域、git 零改动 |
|
||||
| 6 | 批末收口 | **通过(遗留义务明示)**——迭代级 `--code` 闸未跑(卡B 行 6 申报「待迭代出口」);批内证据=commit 点 docs 域回执(fde1795d)全绿含 08c 流水线闸+审计方补跑 gdstyle/static_scan/render_gate/五套件全绿。合回后本地 CI 全量域闸为唯一强制验证点(process-conventions §1),本行遗留义务随合回 CI 承接 |
|
||||
|
||||
## 四、抽查断言原样输出行(当轮实跑摘录)
|
||||
|
||||
```
|
||||
[refresh_feats_test] all green (85 checks)
|
||||
[PASS] V5① 敌方攻击路径:承伤单据在案且逐单恰一次(行 2=计数 2)〔换图系批 V5〕
|
||||
[PASS] V5② 地形路径:地形单据在案且全单据逐张恰一次(行 9=计数 9——盲挖途经单据一并计入)〔换图系批 V5〕
|
||||
[PASS] V5③ 危险格路径:直伤单据在案且全单据逐张恰一次(行 2=计数 2)〔换图系批 V5〕
|
||||
[PASS] V5④ 错误封印路径:直伤单据在案且全单据逐张恰一次(行 2=计数 2)〔换图系批 V5〕
|
||||
[PASS] V2 受理付费挖格 RAP 恰 +1(8→9)〔换图系批 V2〕
|
||||
[PASS] V3 折后单次扣费(8→4,−4)〔换图系批 V3〕
|
||||
[PASS] V4 第 1 次成功(事实 +1 换图 +1)〔换图系批 V4〕
|
||||
[PASS] V6 S1 危险格减伤恰 2 且多刷不叠加(行值 [6]∈[6]——叠加态应为 4)〔换图系批 V6〕
|
||||
[PASS] V9 R1 轮末回复=min(3,缺血102) 且层清 0(heal 行值 [3])〔换图系批 V9〕
|
||||
[PASS] V10 第 1 次成功 escrow +1(0→0)〔换图系批 V10〕
|
||||
[PASS] V12 刷新受理并按表基线扣(−8)〔换图系批 V12〕
|
||||
[standee_present_values] all green (26 checks)
|
||||
[PASS] 基线读数与实装几何同源(基线 = 地台顶上方 8px 档:150 < 158——14px 档牌顶+18)〔样张批 V6〕
|
||||
[PASS] 名字行探针字号实读 = FS_HINT(几何单点出口 name_font_size)〔名字行 V10〕
|
||||
[PASS] 全文件 FS_CAPTION 仅剩消散提示一处(计数锚==1——三处宽度测量点强制换代)〔名字行 V10〕
|
||||
[PASS] 退役常量不在册(STAND_STONE_RIM/STAND_STONE_EDGE 零键)〔名字行 V11〕
|
||||
[PASS] PLAZA_EDGE 值逐位 #4C4038(合并目标②)〔名字行 V12〕
|
||||
[adventure_feats_test] all green (138 checks)
|
||||
[PASS] 专长带入池:37 件四级直映射(普通 14/优秀 13/稀有 10/传说 0)——牌库 102+37=139〔专长批 V2+迭代 46 换图系七件换代〕
|
||||
[round_flow_skill_pool_test] all green (134 checks)
|
||||
[seal_polarity_test] all green (70 checks)
|
||||
[effect_words_t1] all green (20 checks)
|
||||
[forge_table_test] all green (41 checks)
|
||||
```
|
||||
|
||||
```
|
||||
[render_gate] [golden] board_L1_dungeon.png — changed=0.00%(容差 ≤16 差/≤2%)OK
|
||||
[render_gate] [golden] board_L2_underdark.png — changed=0.00%(容差 ≤16 差/≤2%)OK
|
||||
[render_gate] [golden] board_L3_deep_underdark.png — changed=0.00%(容差 ≤16 差/≤2%)OK
|
||||
[render_gate] [golden] menu_boot.png — changed=0.00%(容差 ≤16 差/≤2%)OK
|
||||
[render_gate] 全绿
|
||||
```
|
||||
|
||||
注:V10 消息「escrow +1(0→0)」的两个读数均在 `player_request_refresh()` 求值前格式化——显示瑕疵;断言本体绑刷新后 `escrow == escrow0 + 1`,判据面正确(绿即真值)。
|
||||
|
||||
## 五、内部字段直查受控例外核对(test-conventions §80/§8.7 第 4 条)
|
||||
|
||||
| 直查行 | 性质 | 判定 |
|
||||
|---|---|---|
|
||||
| `orch.fx_host().counters_ctx`(refresh_feats_test 5 处:读计数器×3+白盒预置×2) | `counters_ctx` 是 BattleFxHost **公开成员**(无下划线),读经公共 API `EffectEngine.counter` | 非私有直查;存量套件 adventure_feats_test 同款先例 |
|
||||
| `EffectEngineScript.counter_add(...)` 白盒预置(V9/V8 上限 3 闸) | 计数器直改=数据面注入 | **已申报**——实现决定 #11(test-conventions §5.4 引用)+对照表 V8/V9 行「白盒预置」在案 |
|
||||
| `orch1._enemy_system.hero_unit()`(refresh_feats_test:518,V9 缺血读数) | **私有面直查 1 处** | **例外(手续缺注,不阻塞)**——对照表 V9 行与实现决定 #11 均未注该行理由。依据:①判据主面(heal 行/未生效行)经公共 `step_row` 信号在案,私有读只作前置量(`missing >= 3` 守卫),行为并非钉在私有面上;②存量套件 roll_multiplier_test.gd:51 同款先例;③公开替代面存在(`presentation_snapshot()` 含 hero.hp)。建议实现报告 V9 行补注一句(随合回流程,本审计不代改) |
|
||||
| 生产码 `oc._damage._emit_hero_damage_taken`/`oc._rounds._grant_seg` 直调 | 结算族内部协作 | 实现决定 #8 备案(结算族直调惯例)——非测试面,不在本节判域 |
|
||||
|
||||
## 六、附加核对(实现决定复核三项+卡C 脏面)
|
||||
|
||||
1. **S7_B1_SET 增补(实现决定 #6)——判据口径未变,通过**:static_scan.sh diff 仅 1 行清单+2 行注释;B2/B3/B4 三族符号交替式(14/5/9 符号)逐字未动;豁免语义(自家文件、注释/字符串排除、s7-b1-exempt)未动;`battle_refresh_flow.gd` 调 `oc._reward_economy.retire_mapping()` 命中 B4 符号,入 B1 域集与 C13 拆分边界自洽。审计实跑 static_scan:S7 ok(50 文件扫描面)。
|
||||
2. **S9 修正(f498d2ec)——零行为变化,通过**:4 处显式非参数化容器注解(`damage_controller.gd` 3 处 `: Dictionary`/`: Array`、`grant_segment.gd` 1 处)改 `:=` 推断,静态注解改写,无逻辑分支变化。
|
||||
3. **卡C 停摆脏面核对——申报一致,另见§七.1**:`git status` 修改面恰为申报的三件(`board_view.gd`/`layout_probe.gd`/`board_gen_reward_test.gd`),均未提交、不在审计 diff 内;另有 2 个未跟踪 `.uid` 文件(§七.1)。
|
||||
4. **golden 申报制核对**:golden 五文件(三张板面+level_map+menu_boot)git 零改动=重锚申报制零触发(实现决定 #4「无需重锚」与实跑 0.00% 互证);不存在借重锚夹带。
|
||||
|
||||
## 七、测试削弱核对清单(diff 被改/被删旧断言逐处 → 申报面)
|
||||
|
||||
| 位置 | 改动 | 申报依据 | 判定 |
|
||||
|---|---|---|---|
|
||||
| adventure_feats_test :464/:476 | 行数 30→37、稀有度 12/9/9→14/13/10、牌库 132→139 | 卡A§十二第 1 条(计数换代申报) | **通过**——零放宽:validate/禁词/退役钉面零删(diff 核对) |
|
||||
| forge_table_test :202 附近 | EFFECT_KINDS 32→34、EVENTS 20→22 | 卡A§十二第 1 条 | **通过** |
|
||||
| effect_words_t1_test :17-21 | 纯增量 5 行(两事件两效果在册) | 增量非削弱 | **通过**(旧负例全保留,diff 核对) |
|
||||
| standee_present_values_test V6 | 读数 149→150+「9px 档→8px 档」句 | 卡B§8 申报第 1 条 | **通过**(文字不压地台判据零改动) |
|
||||
| standee_present_values_test V10–V12 | 纯增量 8 断言+文件头注释换代 | 卡B§8 申报第 2 条 | **通过**(V5/V7/V8/V9 判据零改动,diff 核对) |
|
||||
| run_all.gd :191 | 套件注释 V5–V9→V5–V12 | 非行为改动(卡B§6 预告) | **通过** |
|
||||
| 禁删面(卡A§十二):RAP8 基线/每轮上限 2/AP 保留/刷新图零清图/伤害下限/五段序/同种子回放 | round_flow/seal 两文件零进 diff;伤害下限在 `_resolve_board_flat_hero` 保位(`maxi(下限, dealt)` 减后过下限=卡C4 同款) | 卡A§十二禁删令 | **通过**(134/70 checks 实跑绿承载) |
|
||||
| 生产面删除项 | `_terrain_damage_reduce` 删/编排器旧刷新体删/`_enemy_shield_apply` 改名/ui_kit 两常量删 | 卡A T2/T3 步骤明列+卡B§1/§3 | **通过**(全部卡面内,无未申报删改) |
|
||||
| 变异/撤销类取证 | 全 diff 无回退 hunk/注入破坏证明 | 卡A§十二+卡B§3 | **通过**(临时专长经 ConfigDB 表行追加=卡§十二「正常数据配置路径」申报一致) |
|
||||
|
||||
## 八、附加发现(不属验收对照面、不阻塞合并——处置建议随合回流程)
|
||||
|
||||
1. **批内新件两枚 `.uid` 未随批提交**:`game/core/settlement/battle_refresh_flow.gd.uid` 与 `game/tests/unit/battle_settlement/refresh_feats_test.gd.uid` 未跟踪(mtime 07:49,早于 T3 提交 07:52——非审计跑测所生成)。`.gitignore` 第 6 行明文「Godot 4 的 .uid 文件必须提交,不要忽略」,全仓其余 `.gd` 均有 `.uid` 入库伴件。已核零 UID 引用(grep 两 uid 串零命中),风险低;建议合回前补提交(本审计受只读约束不代提交)。
|
||||
2. **S8_FILES 未与 S7_B1_SET 同步增补**:await 零命中扫描集仍为 10 文件,新 B1 拆件不在其内。实查新件零 await(grep 证),当日无漏扫;但两清单同源 B1 域集出现分叉,建议 architecture「模块归属」BattleRefreshFlow 行落文(反哺点 #5)时同批补 S8_FILES。
|
||||
3. **V10 消息文案时点**(§四注):纯文案瑕疵,判据不受影响。
|
||||
4. **V9 私有读未注理由**(§五):建议补注。
|
||||
|
||||
## 九、结论
|
||||
|
||||
**审计通过(零缺漏)**——卡A 对照表 V1–V12 与卡B 对照表 6 行逐行通过(例外 1 处=V9 私有读手续缺注,附依据不阻塞);四套件+三附加套件+gdstyle+static_scan+render_gate 当轮自跑全绿;测试削弱全部在两卡申报面内,无未申报删改,无变异/撤销类取证;结构预算申报(803 行/类变量 15/各件增员)经实数核对全部相符。§八附加发现四条均不属验收对照面,随合回流程处置。
|
||||
@@ -0,0 +1,91 @@
|
||||
# 迭代 46 验收审计——卡C 棋盘格边长口径收口批 v2.0(2026-11-01)
|
||||
|
||||
> 审计方:独立子代理(未参与实现,只读证据+自跑最窄测试,不采信实现报告转述)。
|
||||
> 审计对象:commit `e3e206d0`(代码三件)+`8eaf7d87`(收尾文档两件);卡C 段基点 master `5269ed40`。
|
||||
> 依据:需求卡片 v2.0(§8 改动—验收对照表+测试削弱申报全集)、实现报告 §三 实际对照表、批末闸回执。
|
||||
> 审计工作区:`D:\code\g-plan2-iter46`(分支 iter46)。审计全程只读,唯一写入=本报告。
|
||||
|
||||
## 一、对照表逐行核对——通过
|
||||
|
||||
实现报告 §三 五行逐行核对,每行的验收测试真实存在、尾缀可 grep、真实入口未绕过改动点。
|
||||
|
||||
行 1(探针阈值退役+接线守卫):
|
||||
- 测试真实存在:`game/probe/layout_probe.gd:98-109`(读 `cell_size` → 等价断言),失败路径 `:107`,PASS 行 `:45`。
|
||||
- 尾缀 grep:`grep -n "格边长 V1" game/probe/layout_probe.gd` → `:45`/`:105`/`:107` 三处。
|
||||
- 真实入口:`layout_probe.tscn:6` 挂载 `res://probe/layout_probe.gd`(场景进,非脚本直调);探针断言对象=`main.tscn` 实例化的生产 UI 链(`main.ui.battle.board_zone.board_view`),非测试替身。
|
||||
- 报告附带的 grep 读数(MIN_CELL 零命中)由审计方独立复跑证实(见第五节)。
|
||||
|
||||
行 2(算式单点 V2):
|
||||
- 测试真实存在:`game/tests/unit/core/board_gen_reward_test.gd:75-76`,断言条件 `int(cell) == board_view_script._cell_for(n, avail)`。
|
||||
- 改动点行号核实:`board_view.gd:210`=`_cell = float(_cell_for(maxi(_board.width, _board.height), avail))`、`:226`=`static func _cell_for`——与报告所记 `:210`/`:226` 逐一相符。
|
||||
- 真实入口:`--script res://tests/unit/core/board_gen_reward_test.gd` 直跑(文件头 `:10` 自述同入口)。
|
||||
|
||||
行 3(四档冒烟 V3):
|
||||
- 测试真实存在:`board_gen_reward_test.gd:59` 四档取样表 `[(1,0,10),(2,0,12),(3,0,13),(3,9,14)]`,`:69`/`:71`/`:73` 三条 V3 断言。
|
||||
- 真实入口同行 2(同一宿主 `_init:33` 调 `_smoke_layout_cell_formula`)。
|
||||
|
||||
行 4(推定留痕 V4):
|
||||
- 测试真实存在:零尺寸前置 `layout_probe.gd:113-114`,推定采集 `:116-119`,PASS 后打印 `:47-50`,尾缀 `:49`/`:114`。
|
||||
- 真实入口同行 1。
|
||||
|
||||
行 5(批末收口):
|
||||
- `game/tests/.gates/receipt.json` 实读:`all_green=true`、`commit=e3e206d0f5646ecb8ece22ac08018c26e5591e1d`、七闸(00/01/02/03/05/09/10)exit 全 `0`、`domain=code`、`scope=related[presentation]`、`changed_vs_baseline=[]`——与报告行 5 所记一致。审计方未重跑全量闸(按审计约束,回执承载)。
|
||||
|
||||
## 二、自跑最窄测试抽查——通过
|
||||
|
||||
a. 探针两档(工作目录 `D:\code\g-plan2-iter46\game`):
|
||||
- 命令:`/d/code/godot/Godot_v4.7.2-stable_win64_console.exe --headless --path . res://probe/layout_probe.tscn`
|
||||
- 实测 exit 0。关键输出原文:
|
||||
- `[layout_probe] PASS — 2 档几何不变量全过+格边长等价成立(基座实读 40px)〔格边长 V1〕`
|
||||
- `[layout_probe] [基座 1280x800] 13 板推定 30px/14 板推定 28px(实板可用面积 (1022.0, 454.0))〔格边长 V4〕`
|
||||
- `[layout_probe] [16:9 全屏 1920x1080] 13 板推定 46px/14 板推定 46px(实板可用面积 (1662.0, 734.0))〔格边长 V4〕`
|
||||
- 读数与卡 §2 第 7 条锚点(基座 30/28、全屏 46/46,可用面积 1022×454/1662×734)逐值一致,零对拍偏差。审计方另按公式手工复核:基座 13 板 `(422−24)/13=30.6→30`、14 板 `(422−26)/14=28.28→28`、全屏 14 板 `(702−26)/14=48.3→钳制 46`——打印值与单点算式自洽。
|
||||
|
||||
b. 单元四档(同工作目录):
|
||||
- 命令:`…console.exe --headless --path . --script res://tests/unit/core/board_gen_reward_test.gd`
|
||||
- 实测 exit 0,收尾行 `[board_gen_reward_test] all green (74 checks)`。
|
||||
- 四档各四断言全 PASS:L1D0(41px)/L2D0(33px)/L3D0(31px)/L3D9(28px)——每档「板型一致〔格边长 V3〕」「网格高 ≤ 可用高−24〔格边长 V3〕」「格边 ∈ 26–46〔格边长 V3〕」「算式单点等价〔格边长 V2〕」各一条,16 条尾缀齐全。62→74 增量=每档净增 3 条 × 4 档,账目相符。
|
||||
|
||||
c. gdstyle(工作目录 `D:\code\g-plan2-iter46`):
|
||||
- 命令:`python tools/gdstyle_gate.py`
|
||||
- 实测 exit 0:`扫描面 142 个 .gd;命中 5 条,基线内 5 条,基线外新违规 0 条,较登记值恶化 0 条`。
|
||||
- 基线两键实读:`tools/gdstyle_baseline.txt:11`=`board_view.gd|quality/max-class-variables = 31`、`:12`=`board_view.gd|quality/max-public-methods = 27`——持平,无 NEW 键。
|
||||
|
||||
## 三、判据绑定面核查——通过(一处非阻断措辞备注)
|
||||
|
||||
1. 绑定的是生产写入值,不是多层防御后的终态:
|
||||
- 探针取值面=`cell_size` 公开只读属性(`board_view.gd:89-91` getter 直返 `_cell`),`_cell` 由 `set_cell_area` 在 `:210` 直接写入;生产写入路径两条未动(`board_zone_view.gd:92` resized 信号→`set_cell_area(_board_area.size)`、`board_view.gd:273` rebuild 末尾)。等价断言 `layout_probe.gd:106` 对 `bv.size`(实板可用面积)严格相等(`!=` 记红)——生产停调单点算式即红(基座档默认值 46 与算式值 40 分叉可捕)。
|
||||
- 单元取值面=真实调用 `set_cell_area(avail)` 后直读 `_cell`(`board_gen_reward_test.gd:65,67`),与同输入 `_cell_for(n, avail)` 逐档 int 严格相等(`:76`)——同一事实两路读取。
|
||||
2. 内部字段直查两处的例外依据:
|
||||
- 探针读 `_board`(`layout_probe.gd:103`):理由「板型 N 无公开出口」已载于代码注释 `:102`、卡 §7.3 T2 与卡 §14.2、报告 §六 白名单第 6 项。
|
||||
- 测试读 `_cell`(`board_gen_reward_test.gd:67`):批前既有取值面(卡 §2 第 4 条记载改前即如此),卡 §8 申报 4 明列 `cell_size`/`_cell` 为两合法入口、禁换别的入口——属守住的旧面,非新开直查。
|
||||
3. 非阻断措辞备注:卡 §14.2 与代码注释 `layout_probe.gd:102` 均称例外理由「对照表行 1 注明」,但实现报告 §三 行 1 判据绑定面格(以及卡 §8 行 1)未逐字带「N 无公开出口/受控例外」句——交叉引用指针落空。例外事实与理由已在他处完整承载(本节第 2 点所列四处),断言零削弱、无申报外改动,不判红;建议实现会话顺手把该句补进报告 §三 行 1(可选,不阻塞合并)。
|
||||
|
||||
## 四、测试削弱核查——通过
|
||||
|
||||
e3e206d0 负向行(删除行)全集逐处对账,全部落进卡 §8 申报四条,无申报外删改:
|
||||
|
||||
| 删除内容 | 对应申报 |
|
||||
|---|---|
|
||||
| `const MIN_CELL := 36.0` 整行 | 申报 1(阈值常量退役) |
|
||||
| `if cell < MIN_CELL:` +「< 可读门槛」记红两行 | 申报 1(阈值断言删除) |
|
||||
| 旧 PASS 消息(「格子 ≥36px」形态)两行 | 申报 2(文案随判据换代) |
|
||||
| `for L in [1, 2]` 与 `generate(L, 0, …)` 两行 | 申报 3(循环集 2→4 档)+白名单第 2 项(字典数组形态) |
|
||||
| 两条旧 Kit.check 的消息行 | 申报 3(前缀 L%d→L%d D%d——卡 §7.4 明许)+白名单第 4 项(尾缀编排) |
|
||||
| 冒烟 print 头行(「底部预留回归」→「·四档板型」) | 申报 3 同批日志头随档数换代(非断言,无条件面) |
|
||||
| `board_view.gd` 内联算式六行 | 非断言——逐字搬入 `_cell_for`(`board_view.gd:226-231` 对拍:SEP/DEPTH_PX 同源、reserve 24.0+d 同式、clampi(26,46) 留函数内),行为零变化由单元 74 checks 全绿承载 |
|
||||
|
||||
补偿三件全在位:等价断言(V1)、结构钳制四档(V3)、推定留痕(V4)。既有两断言条件逐字保留(`grid_h <= int(avail.y) - 24`、`cell >= 26.0 and cell <= 46.0`),样本 `Vector2(1036, 460)` 未改。既有几何不变量与视口档位表零改动(diff 负向行中无其一)。申报 4(禁恢复阈值/删旧断言/放宽区间/换读取入口)成立——四处均未发生。
|
||||
|
||||
## 五、全仓无人造阈值回归——通过
|
||||
|
||||
- `grep -rn "MIN_CELL\|可读门槛\|可读下限" game/` → 零命中(exit 1);扩到 `tools/`、`design/` 查 `MIN_CELL` 亦零命中。
|
||||
- 30/36 阈值形态穷举:三件改动文件中 30 仅两处且均非格边长判据(`board_gen_reward_test.gd:203`「板面 30% 锚」=雷数比例锚、`board_view.gd:659`「南部 30%」=墙面口径注释);36 零出现。全 `game/` 断言形态 grep(`(>|>=|<|<=)\s*(30|36)`)命中皆为无关域(RNG 位移 `>> 30`、商店 `1 << 30` 哨兵价、护甲 `<= 30` 属性域检);`clampi` 无 30/36 形态。格边长唯一区间判据仍是公式自带 `clampi(26, 46)`。
|
||||
- 变异类测试与撤销类取证:diff 负向行无「人为改错值验抓取」形态;无回退 hunk;测试代码无注入破坏分支(探针 `_board == null` 前置属测量面防御,报告 §六 第 1 项已备案,非变异)。
|
||||
- design/ 现状:`battle.md:144`「可读下限 30px」句仍在位=预期(实现批禁写 `design/`,退役走反哺点通道——`reports/iter46-反哺点-卡C-2026-11-01.md` 第 1 条已登记,卡 §5 明示不阻塞本批)。
|
||||
- 禁写面核查:卡C 段全区间 `5269ed40..8eaf7d87` 触碰文件恰为三件代码+两件 reports,`待办清单.md`/`体验验证清单.md`/`game/tests/README.md`/`design/` 零触碰。
|
||||
- 工作区漂移核查:三件代码文件 `git status` 干净——审计实跑的即 e3e206d0 提交的字节。
|
||||
|
||||
## 六、总结论
|
||||
|
||||
**通过。** 五项审计全过:对照表五行真实入口与尾缀齐全;自跑探针两档绿(基座实读 40px、推定 30/28 与 46/46 合锚点)、单元 all green 74 checks(四档 41/33/31/28px,V2/V3 全 PASS)、gdstyle 基线外新违规 0 恶化 0;判据绑生产写入值与 `_cell_for` 严格相等;删改逐处对上申报四条;全仓无人造阈值回归、无变异/撤销形态。唯一遗留=第三节第 3 点的交叉引用措辞备注(不判红、不阻塞合并,补句归实现会话可选动作)。
|
||||
@@ -0,0 +1,19 @@
|
||||
# 迭代 47 反哺点清单(实现批未写 design/ 与根规则文件——逐条待用户裁决后由文档批落文)
|
||||
|
||||
- 接收方:待办清单「营地交互重做·五张需求卡片」条目(裁决后逐条清掉)+各 owner 文档
|
||||
- 依据:卡⑥/卡④/卡⑤ §5 关联文档同步面全表义务格=反哺点·列清单
|
||||
|
||||
| # | 文档 | 位置 | 原句/现状 | 拟改 | 依据 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | architecture.md | 呈现层模块清单/测试与质量域 | 无 UiCampModals/四子界面/CampRefreshBus/MainEscFlow/CampHudBand/CampSceneView 六新件行 | 增六件归属行(呈现层/组合根拆件)+LAYER_CONTRACT 槽变化(camp_overlay→camp_scene+四子界面+camp_esc_overlay)+产品码首个键盘输入入口(main._unhandled_input 契约 9 接线)注记 | 卡⑥/④/⑤ 落地实况 |
|
||||
| 2 | camp.md | §顶栏 :39-41 / §叠层与开合 :43-45 | 「刷新入口=营地刷新单口(卡⑤同口挂接)」未记实装;承载形态「归卡④ §9 代拍板域,本 spec 不预设值」 | 回填实装状态(单口=CampRefreshBus 已挂/时机清单三条)+承载形态=单一内页切换(场景根留底)拍板值落文 | 卡④ T2/T3+卡⑤ 实装 |
|
||||
| 3 | art-bible.md | §6.2 :393 围墙细条句 / §4.1 场景环境子板 | 「围墙层落盘按细条 (0,240,1280,70) 裁」;环境子板色值待登记 | 细条句补实装值 (0,261,1280,49)(Y 压缩桩身 43 锁定值精确,备选 A′ 在档);登记营地 32 色主色板(色值清单=artwork/2026-11-01-营地终稿后处理/换算式登记.txt,两道门合规零违例对) | 卡④ T1 实测+§0.3-3⑤ |
|
||||
| 4 | camp-scene-prompts.md | §2.3 坐标表 / §4 验收 | 贴图矩形=规划值(130×140 等);单张 ≤32 量化后 | 八物件 contain 实测尺寸表(board 130×121 等——呈报备案追认值)+量化实况注记 | 卡④ T1 换算式登记 |
|
||||
| 5 | 生涯.md | :72 营地行(P6 条带去向) | 「营地条带的去留是本提纲必答项——见 §五 P6」 | P6 注销(Q4=甲:条带随四柜台拆解撤下,逐局历史回看归生涯——本批已撤) | 卡④ Q4 裁定落地 |
|
||||
| 6 | 08-Alpha版本标准.md | :193 「键盘 / 手柄输入(排 Beta)」 | Beta 行未注记 Esc 已落地 | 行内加注「例外:营地语境 Esc 浮窗已落地(用户明示裁决,澄清⑤裁决 9;键盘面仍排 Beta——本批只落 Esc 一项)」 | 卡⑤ 契约 2/9 |
|
||||
| 7 | 待办清单.md | 营地交互重做条+UI 贴图重构条+图鉴挂起 | 条目状态「已编迭代 47」;贴图重构条对象注记;图鉴挂起 | 状态推进(完成即整条删除)+实况换代(三卡收口凭据);贴图重构条「四柜台对象变化」注记更新(柜台形态已退役);图鉴挂起条入口改挂注记 | 三卡实装 |
|
||||
| 8 | 体验验证清单.md | 新增目检条 | — | 登记 §实现报告 §6 五条(场景观感/点选反馈/大门篝火/Esc HUD/四子界面)——登记不阻塞 | 卡④ V11+卡⑤ V9 |
|
||||
| 9 | roadmap.md | 迭代 47 行+当前位 | 状态「待开工」 | 行换代(三卡实现报告列+状态)+当前位三行换代 | 迭代收口 |
|
||||
| 10 | shot_checker 色数判据 | tools/shot_checker.py A 段 | camp 屏唯一色数 ≥64 | L3 呈报在案(读法 A/B 待裁)——裁决后判据修正随批+待办清单登记条 | 停机呈报 2026-11-01 |
|
||||
| 11 | asset-path-map.md §3 / art-assets-checklist.md §3.5 | 营地 10 张路径行 | 登记超前注记 | 实装入库确认(scene_camp_* 十件在役+TEX_CAMP_* 十键消费)+终稿置换待用户游戏内终审注记 | 卡④ T1 |
|
||||
| 12 | ui_kit CAMP_HEADER_BAND 令牌 | ui_kit.gd :178 邻域+ui_contrast 表 | 令牌在役(新界面零消费) | 令牌退役评估(现役消费面=ui_contrast 表已注销四行后零消费——随令牌批处置,非本批强删) | 卡④ T5 核签 |
|
||||
@@ -0,0 +1,117 @@
|
||||
# 迭代 47 实现报告(卡⑥ 营地模态句柄子容器批+卡④ 营地场景图新建批+卡⑤ Esc 浮窗与营地顶栏批)
|
||||
|
||||
- 接收方:roadmap.md 迭代 47 行(实现报告列)+待办清单「营地交互重做·五张需求卡片」条目
|
||||
- 性质:代码实现批产出物(实现批唯一可写目录=reports/;design/ 与根规则文件零写入——反哺点清单另立)
|
||||
- 分支:iter47(worktree g-plan2-iter47)· 基线 5ad72f44 · 收口提交史见 §9
|
||||
|
||||
## 1. 各卡状态
|
||||
|
||||
| 卡 | 状态 | 提交 | 出口验证 |
|
||||
|---|---|---|---|
|
||||
| 卡⑥ 句柄子容器批 | 收口 | 3a9c6ff4 | --code 全套闸 all_green;V1/V2/V3 留痕齐 |
|
||||
| 卡④ 营地场景图批 | 施工完成(T6 出口闸等 shot_checker 裁决重跑) | aa4aa1dd/4b5e3f27/98470adc/5a0e7069/1243ffe6/52296872 | 七探针+system_test 全绿;V14 11 项绿;--code 闸 01-04/09/10 绿、05 渲染闸红=L3 呈报在案 |
|
||||
| 卡⑤ Esc 浮窗与顶栏批 | 施工完成(T4 出口闸同上) | 4e4232d8 | 冒烟 PASS+system_test 69/0+七探针全绿 |
|
||||
|
||||
## 2. 卡⑥ 实际改动—验收对照表(尾缀〔句柄子容器批 Vn〕)
|
||||
|
||||
| 需求点 | 改动 | 验收测试 | 层级 | 真实入口 | 判据绑定面 | 结果 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| V1 句柄迁移零行为漂移 | ui_camp_modals.gd 新件;ui_modals 持 camp 删平铺;main_ui_build 装配 10 处;改指面 8 文件 43 处 | 五探针营地段+system_test V1c:94-106+句柄同一性链 :108 | 探针+系统 | 现役营地断言逐件复跑(断言零改寻址新) | 全段绿=零漂移;全仓 grep 零 modals.camp.overlay 残留 | 绿(V1 尾缀 34 处 grep 在位) |
|
||||
| V2 层序合同零漂移 | ui_registry node() 返回链改写(key 平铺不改名) | system_test V1c 合同树序链 | 系统 | LAYER_CONTRACT 单一事实源 | 树序链绿=合同落位零漂移 | 绿(V2 尾缀 :106 在位) |
|
||||
| V3 S13 读数 | ui_modals 类变量 14→14 | gdstyle gate+grep -c '^var ' 计数 | 静态 | static_scan.sh | gate 绿零 NEW+计数=14 留痕(观测面局限:闸对 14/15 同零命中,人工计数承载) | 绿(143 件零 NEW;计数 14) |
|
||||
|
||||
## 3. 卡④ 实际改动—验收对照表(尾缀〔营地图批 Vn/Bn〕)
|
||||
|
||||
| 需求点 | 改动 | 验收测试 | 层级 | 真实入口 | 判据绑定面 | 结果 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| V1 场景图形态 | camp_scene_view 10 层铺底+layout.tres+camp_screen 改挂 | shot_probe camp 段(截图=场景图)+ui_struct 场景层序段+shot_checker 结构段 | 探针 | 进存档→选档→进档→营地场景屏 | 截图实拍=场景图形态(十层合成/门洞透路/七物件就位);子节点序=表 z 序逐层;贴图矩形逐值 | 绿(截图目检+层序段 11 层断言) |
|
||||
| V2 七物件可达+叠层退避 | 七命中区+object_pressed+组合根路由 | ui_probe/ui_click/ui_struct 场景点选段 | 探针 | 命中区→_gui_input→object_pressed→路由 | 锻炉/宝箱/木桩/布告板/笔记本/篝火/大门逐一 object_pressed 驱动可见性 | 绿(七件路由断言分散六探针全绿) |
|
||||
| V3 大门两分支+开局收闭 | confirm_dialog 三钮化+消费方编排扩展+main_overlay_flow 路由 | ui_click 大门两分支段 | 探针 | 场景点选 gate→确认弹窗→分派 | 点大门不直接进冒险(is_adventure 翻转只在 confirmed 后);cancelled 写口零调用+营地根 visible;开局后营地隐藏 | 绿(无断点两出口+有断点三出口 set_breakpoint 注入全过) |
|
||||
| V4 五界面功能+写口零回归 | 四 view 抽出+presenter 承接面+bus 单口 | forge_workbench_test(承载类同迁断言零改)+ui_click 购买链 | 单元+探针 | 意图信号→presenter→MetaProgression 写口 | 树购买出账+持久(reload 后级=1)/Fork 持久/锻造三意图——全链复跑绿 | 绿(12 项+点击链 0 问题) |
|
||||
| V5 生涯只读契约 | career 入口改挂笔记本(本体零改) | ui_struct career 段(笔记本真实路由) | 探针 | 笔记本命中→object_pressed→_open_career | 生涯页可达+三契约不破+营地语境关生涯不回开始层 | 绿 |
|
||||
| V6 图鉴挂起态 | codex_view 占位壳 | ui_click 灰字段(锚=结构位预留) | 探针 | 布告板命中→codex 开 | 占位锁文锚定+零激活禁行+非禁用态 | 绿 |
|
||||
| V7 篝火动线 | 路由 campfire→二次确认→on_camp_closed | screen_flow 回退段+ui_click? | 探针 | 篝火命中(只下半)→确认→主菜单 | 二次确认 confirmed 先于关层;确认后营地+选档同收+菜单显 | 绿(screen_flow ③步+T4 孤检) |
|
||||
| V8 顶栏位+刷新单口 | set_hud_band 预留位+camp_refresh_bus 新件 | ui_struct 顶栏位段+卡⑤ HUD 段 | 探针 | 写口尾→bus.refresh→现开界面+HUD | 顶栏位 48px 在座;写口尾一闸刷新(八尾改路由) | 绿 |
|
||||
| V9 反参照纪律 | 场景根态零按钮 | ui_struct 反参照段 | 探针 | 场景根子树 Button 计数 | 根态零文字按钮(禁文字按钮列表形态) | 绿 |
|
||||
| V10 美术约束 | shot_checker camp 骨架段+素材二器 | ui_contrast 令牌表+palette_check 读数 | 机检 | camp 截图+素材文件 | 见 §7 遗留读数义务(A 段色数判据=L3 呈报在案) | 部分(令牌 36 对绿;A 段红待裁) |
|
||||
| V11 目检登记 | 实机目检总清单 | 件核对(§6) | 目检 | 用户实机 | 登记即义务了结(不阻塞) | 已登记(§6) |
|
||||
| V12 探针迁移留痕 | F8 全量位点 | 件核对(本表+§8 迁移留痕) | 件核对 | 探针 diff 对账 | 逐点改前→改后 | 绿(§8) |
|
||||
| V13 反哺点清单 | §5 同步面 | 件核对 | 件核对 | 反哺点文档逐条 | 每条有落点 | 绿(反哺点文档) |
|
||||
| V14 layout 数据件校验 | schema validate+fail-fast 钩子 | camp_scene_layout_test.gd:_test_layout_hotspot_geometry(manifest 登记) | 单元 | 单测载入 .tres+ConfigDB 装载链 | 校验通过+七命中区几何红线+关系谓词零坐标字面量 | 绿(11 项) |
|
||||
| B1 组合根接线总改造 | main.gd 路由/三钮编排/篝火/收闭+bus | ui_click 大门段+screen_flow 篝火段 | 集成 | 确认分派/_resume/_start/bus.refresh | 三钮消费方编排 open 前设后分派;cancelled 零写口;篝火确认离场 | 绿 |
|
||||
| B2 CampOverlay 退役+四界面抽出 | overlay 删除+四 view+五句柄子容器 | forge_workbench_test+全仓件核 | 集成 | 五界面 open/close→模态栈单口 | 旧件零残留(件核一);承载类同迁断言零改 | 绿 |
|
||||
|
||||
## 4. 卡⑤ 实际改动—验收对照表(尾缀〔ESC批 Vn/Bn〕)
|
||||
|
||||
| 需求点 | 改动 | 验收测试 | 层级 | 真实入口 | 判据绑定面 | 结果 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| V1 Esc 唤起四态 | camp_esc_overlay+main_esc_flow+main._unhandled_input | screen_flow Esc 步 | 探针 | push_input 合成 Esc(真实键盘路径) | 根态唤起/再按关窗/echo 零响应/子界面无反馈/设置开态不误关/菜单语境不唤起 | 绿(10 项) |
|
||||
| V2 浮窗三项 | setup_buttons 三钮 | ui_struct 浮窗段 | 探针 | 浮窗视图实例化 | 三钮在位且仅三(_buttons 递归计数) | 绿 |
|
||||
| V3 返回主菜单 | _on_return_to_menu(首步 close 浮窗) | screen_flow 返回动线段 | 探针 | return_requested→动线 | 浮窗自收+营地隐+选档收+菜单显(四可见性) | 绿 |
|
||||
| V4 设置双路径 | _on_settings+语境分支 | screen_flow 设置段 | 探针 | settings_requested→open→closed | 设置开浮窗恒 visible(树序遮挡非属性隐藏);关后回浮窗不回主菜单 | 绿 |
|
||||
| V5 退出项 | _on_quit→_quit_game 复用 | 连接链断言(main_ui_build 接线在位) | 静态 | quit_requested→_on_esc_quit→_quit_game | headless 不可真退=静态判据+观测面局限申报(本表) | 绿(接线段随装配绿证) |
|
||||
| V6 HUD 余额 | camp_hud_band+bus 挂册+进档尾刷新 | screen_flow HUD 段 | 探针 | 隔离档直写 _add_keystone(9999)→显式触发 bus.refresh | badge 文本=「钥石 9999」(单口现值,显式触发才变) | 绿 |
|
||||
| V7 零动效 | 直开直关 | ui_struct 浮窗段 | 探针 | 双件源码+子树扫描 | 零动效令牌+AnimPlayer=0(扫描面>0 非空前置) | 绿 |
|
||||
| V8 层序与遮挡 | contract 第六槽+node() | system_test V1c 复跑+ui_struct badge 段 | 系统+探针 | 合同表单一事实源 | 树序链含新槽逐对保序;badge 零波及 | 绿 |
|
||||
| V9 目检登记 | 实机目检总清单 | 件核对(§6) | 目检 | 用户实机 | 登记即义务了结 | 已登记(§6) |
|
||||
| V10 反哺点清单 | §5 同步面 | 件核对 | 件核对 | 反哺点文档 | 每条有落点 | 绿 |
|
||||
| B1 Esc 接线与浮窗 | 输入入口/薄委托/视图/新槽 | screen_flow+ui_struct | 集成 | 合成 Esc→路由→flow.open/close | 四态路由绑现开集合与调用记录 | 绿 |
|
||||
| B2 HUD 落位与刷新 | band 新建/set_hud_band/bus 挂册 | screen_flow HUD 段+ui_struct 顶栏段 | 集成 | bus.refresh→band.refresh_camp→badge | badge=单口取值;顶栏位结构在座 | 绿 |
|
||||
| B3 设置语境分支与三出口 | _on_settings_closed 分支+三出口动线 | screen_flow 设置段+返回段 | 集成 | 三钮 pressed→MainEscFlow | 关设置回浮窗禁弹主菜单(反向断言);退出连接链 | 绿 |
|
||||
|
||||
## 5. 实现决定记录(代拍板追认单——随 commit 留痕汇总)
|
||||
|
||||
1. **承载形态=单一内页切换(场景根留底)**(卡④ §9-2 域内):模态栈 open 只隐上方登记层(modal_stack.gd:27 实测),四子界面登记于场景根之上且实底遮挡——双开互斥靠遮挡天然成立;V2 断言按留底派生(子界面开=场景根 visible 保持)。
|
||||
2. **围墙层处理路线 A″**(用户裁 A 后处理实现):去底(边界泛洪+奶油描边屏障 BG_TOL=50)→门洞手术(门柱内移+克隆桩列,缺口 675→480 源=240 终稿精确)→Y 轴压缩(桩身 609→86 源=43 终稿锁定值精确,整带 692→98 落盘 (0,261,1280,49) 底锚 y310);备选 A′(带高 70/桩身 62)存档 artwork 同目录。竖向克隆接缝 3× 放大可辨、1× 游戏尺度不可辨(申报)。
|
||||
3. **八物件 contain 落位尺寸**(呈报备案追认):board 130×121/gate 240×211/dummy 120×180/forge 240×193/tent 251×170/chest 106×90/notebook 100×66/campfire 160×149(锁定矩形 contain,命中矩形按锁定值);贴图锚定=底中心(帐篷/锻炉/宝箱/篝火/笔记本)+顶锚(门扇悬挂/布告板挂墙)。
|
||||
4. **营地主色板 32 色**(§4.1 场景环境子板两道门合规):各层独立量化→并集 272→最近对合并 120 轮→32 色零违例对;十层按终版色板重量化回写。色值清单=换算式登记.txt,登记 art-bible §4.1 走反哺点。
|
||||
5. **codex 锁文称谓订正**(L2):「柜台结构位预留」→「结构位预留」(零新文案——『柜台』随形态退役)。
|
||||
6. **set_close_path 未落独立方法**(L2 API 面):Q2 关闭路径=组合根 Esc 路由直呼 flow.close(浮窗),视图无自关通路——卡面「关闭由组合根路由」直译;bind_signals 承载三钮意图外发。
|
||||
7. **契约 9 分支序实证修正**:①先于②(设置开在浮窗之上态先命中无反馈)——首装②先被 screen_flow Esc 段实机场景证伪(2 红),按卡面编号归位后全绿。
|
||||
8. **CampSceneView 初始隐**:_init visible=false(登记模态初始隐——boot 态场景根盖菜单三红同源修复)。
|
||||
9. **ui_modals 头注/迁移面**:卡⑥ 头注换代(十二平铺+营地域子容器)✓;ui_struct 层序注释换代(camp_scene 代言)✓。
|
||||
10. **卡④ T0 预算预案兑现**:main.gd 15/15 恰贴上限——T3/T4/T5 实现零新增成员(路由走 MainOverlayFlow 静态件+薄委托)✓。
|
||||
|
||||
## 6. 实机目检总清单(登记不阻塞——体验验证清单入册条目由文档批落)
|
||||
|
||||
1. 营地场景观感:十层构图/围墙门洞/纵深(V11——纵深稿「中景 2/3」可辨性)。
|
||||
2. 七物件辨识与点选反馈:悬停轮廓+浮动+名牌/按下位移/锻炉宝箱禁用态(未解锁档)。
|
||||
3. 大门两分支弹窗(三出口文案与位次)+篝火二次确认动线。
|
||||
4. Esc 浮窗三动线+HUD 余额观感+浮窗视觉面(卡⑤ V9)。
|
||||
5. 四子界面视觉(米板身壳/行板/返回钮位次)。
|
||||
|
||||
## 7. 遗留读数义务
|
||||
|
||||
1. **shot_checker A 段色数判据互斥**=L3 呈报在案(reports/停机呈报-shotchecker色数判据互斥-2026-11-01.md)——读法 A/B 待裁;裁决后判据修正随批+闸重跑追绿。
|
||||
2. palette_check 读数(V10 素材级):15 张 0 红灯 0 新增色(只读模式)——48 令牌 baseline 目录在库缺位,双门机检化=工程批事项(反哺点登记)。
|
||||
3. 用户在游戏内终审十层素材(2026-10-31 裁定预留)——终稿置换(若有)走反哺点。
|
||||
|
||||
## 8. 探针迁移留痕表(V12——F8 全量位点改前→改后)
|
||||
|
||||
| 位点 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| screen_flow :54 营地旗标 | modals.camp.overlay | modals.camp.scene |
|
||||
| screen_flow 回退段 :119-125 | overlay.closed.emit()+手动关选档 | 篝火动线(object_pressed campfire+confirmed) |
|
||||
| screen_flow Esc 段(新增) | —(选档语境零响应段保留) | 十断言步 _step_camp_esc_and_hud |
|
||||
| ui_probe 营地段 :26-57 | overlay 余额/树钮/秘宝行 | 场景点选路由→tree/treasure 视图成员 |
|
||||
| ui_click 营地段 :60-250 | overlay 树链/灰字六源位 | 树链 tree view+灰字分扫 treasure/codex(锚去『柜台』)+savesel 三处场景根 |
|
||||
| ui_click 断点段(卡② 移交) | lvbtn 驱动(已随卡② 撤) | 大门两分支断言段(V3 落位——无断点/有断点 set_breakpoint 注入) |
|
||||
| ui_struct :45 audit | audit("camp",[overlay]) | audit("camp",[camp.scene]) |
|
||||
| ui_struct 源扫描 :48-50 | camp_overlay_view.gd 单件 | 营地域六件合计(场景根+四子界面+公共壳) |
|
||||
| ui_struct 生涯段 :54-69 | 函数直调 _open_career | 笔记本真实路由(open_camp 重入+object_pressed notebook+营地语境分支断言) |
|
||||
| ui_struct 场景段(新增) | — | 层序/矩形逐值+锻炉可达+顶栏位+反参照+Esc 浮窗段 |
|
||||
| ui_contrast 令牌表 :48-55 | 头带两行+铭牌副题+最近冒险 | 四行随形态注销(CAMP_HEADER_BAND 零消费随退);ROW_PLATE/PANEL_DATA 行保留 |
|
||||
| shot_probe :120-135 | 关闭营地钮收闭 | 截图=场景图+篝火动线收闭 |
|
||||
| system_test V2/:108/:121 | camp_overlay 成员/句柄 | camp_scene 三处机械随迁(T3) |
|
||||
| system_test V3g camp 分支 | overlay 开合 | 场景根开合自适配(on_camp_closed 关层收闭) |
|
||||
| forge_workbench_test :33 | 直载 camp_overlay_view+._forge._X | 直载 forge_view+扁平成员(断言零改 12 项绿) |
|
||||
| theme_projection :711-716/:801-810 | 等高源码断言+最近冒险投影 | 注销(卡面削弱申报授权——旧形态断言失去对象/条带 Q4 甲撤下) |
|
||||
|
||||
## 9. 提交史与闸况
|
||||
|
||||
3a9c6ff4 卡⑥T1 → aa4aa1dd 卡④T1 进度 → 4b5e3f27 卡④T2 → 98470adc 卡④T3 → 5a0e7069 卡④T4 → 1243ffe6 卡④T5 → 52296872 卡④T5 补 → 4e4232d8 卡⑤T1-T3。
|
||||
闸况:卡⑥ 出口 --code all_green(3a9c6ff4 对齐);卡④ T6 出口闸 01-04/09/10 绿、05 渲染闸红=shot_checker 判据互斥(L3 呈报在案——合回前重跑)。
|
||||
|
||||
## 10. 独立验收审计结论
|
||||
|
||||
(独立审计报告=reports/iter47-验收审计-2026-11-01.md——结论待审计回填:通过/缺漏/例外及依据。)
|
||||
@@ -0,0 +1,55 @@
|
||||
# 停机呈报:棋盘格边长口径收口批(迭代 46 卡C)——L3 #2 实板可读面不达标
|
||||
|
||||
> 日期:2026-10-31 · 呈报人:迭代 46 实现会话(iter 技能执行中)
|
||||
> 接收方:待办清单条目「棋盘格边长三方取值不一致——待扩板定档处置(2026-10-06 登记)」
|
||||
> 性质:实现段零裁断——需求卡片预设停机点触发(需求卡片-棋盘格边长口径收口批-2026-10-30.md §9 异常分级 L3 第 2 条),停该卡呈报退回上游,等用户裁决。
|
||||
|
||||
## 一、触发了什么
|
||||
|
||||
需求卡片 §7.3 T2 规定探针新增「实板可用面积下 13/14 板格边长推定」断言:以探针实读 `board_view.size` 代入算式单点 `BoardView._cell_for(n, size)`,`< 30px`(battle.md 可读下限)即记问题。§9 L3 第 2 条预设:实板可用面积代入 13/14 后 < 30 = 产品面问题,**停卡呈报,禁调低下限、禁换样本凑绿**。
|
||||
|
||||
当轮真实跑批实测(worktree iter46,种子 42,两档视口):
|
||||
|
||||
| 视口档 | board_view.size(实板可用面积) | L1 实测格边 | 13×13 推定 | 14×14 推定 |
|
||||
|---|---|---|---|---|
|
||||
| 基座 1280×800 | (1022.0, 454.0) | 40px ✓ | 30px(恰达标) | **28px < 30 ✗** |
|
||||
| 16:9 1920×1080 | (1662.0, 734.0) | 46px(钳制顶格) | 46px ✓ | 46px ✓ |
|
||||
|
||||
代入算式复核(1280 档):`min(1022−12−8, 454−32) = 422`;`(422 − 13×2) / 14 = 28.28 → floor 28` → 钳制 [26,46] 内 → 28px。**14×14 板在基座档的实板格边长低于 battle.md「可读下限 30px」**。
|
||||
|
||||
## 二、事实定性(这不是本批制造的回归)
|
||||
|
||||
1. 算式逐位未动(卡C 行为零变化,单元四档冒烟 74 checks 全绿——结构钳制 [26,46] 面全过)。
|
||||
2. 28px 是**存量事实**:现行生产公式对 14×14 板(board_gen.tres:L3 depth 9–13)在基座档一向算出 28px;本批新增的推定断言只是首次把它量出来。
|
||||
3. battle.md 算例表的「14×14 → 30px」按「可用高 486px」推算,而实机 D 列结构给出 454px——卡片 §2.8 已登记该偏差(「实机可用面积与算例不同源」),本读数即该偏差的实测落点。
|
||||
4. 同样数据下 13×13=30 恰好达标,仅 14×14 一档越线下。
|
||||
|
||||
## 三、分歧点与两种以上读法
|
||||
|
||||
**分歧点**:battle.md「可读下限 30px」(:144)的适用域——是否按视口档/板型分级适用。
|
||||
|
||||
- **读法甲(全域硬下限)**:30px 对一切视口档×板型组合生效 → 14×14 板在基座档不达标=产品缺陷,需要设计动作(改布局可用面积/改板型表/缩 14×14 适用域)。
|
||||
- **读法乙(按档分级)**:30px 下限以 1920 档为守卫面(推定断言只判 1080 档),基座档按实测算例记录(battle.md 算例本标注「随布局与板型变化,不是契约值」)→ 推定断言判据域改判 1080 档,基座档记录实测 28px 不下断言。
|
||||
- 两读法都属设计裁决(owner 文档 battle.md 的判据域修订),**实现段无权自裁**——读法乙虽不是「调低下限」(30 数值不动),但改变断言的适用域,同样超出卡片明列范围。
|
||||
|
||||
## 四、建议案(供裁决,禁代裁)
|
||||
|
||||
1. **建议主案=拆批**:迭代 46 以卡A(换图系七件批)+卡B(名字行提档与地台色合并批)先行合回——两卡已收口、代码域闸全绿(回执 45b53d68,commit 45b53d68 后另有 S9/S7 修正与卡B 提交);卡C 停摆退回写卡段,待 30px 下限适用域裁决后重立卡编入后续迭代。拆批合回前置=roadmap 迭代 46 行与待办清单条目状态换代(归文档批,本批禁写)。
|
||||
2. **裁决后的卡C 重写方向**(按所选读法):
|
||||
- 读法甲:重写卡范围收窄为「算式单点+门槛 36→30+四档冒烟+13 板推定」,14×14 不达标另立缺陷条目走设计段(布局或板型表动作)。
|
||||
- 读法乙:卡C 原范围保留,推定断言判据域改「1080 档 ≥30」+基座档实测记录句;battle.md 反哺点补「下限适用域分级」条。
|
||||
3. **卡C 已完成的可保留面**(worktree iter46 未提交改动,停摆卡不提交——随裁决处置):`board_view.gd` 算式单点提取 `_cell_for`(等价性已证:board_gen_reward_test 62→74 checks 全绿)、`layout_probe.gd` 门槛 36→30 与推定断言、`board_gen_reward_test.gd` 四档扩样。三件改动行为零变化,裁决后可直接复用。
|
||||
|
||||
## 五、待办清单条目状态回退+缺口补记(拟稿——落笔归上游段/用户)
|
||||
|
||||
条目「棋盘格边长三方取值不一致——待扩板定档处置(2026-10-06 登记,来源=批次移交)」:
|
||||
|
||||
- **状态**(现「已写卡+已编迭代 46」)→ 回退拟稿:`已写卡(卡C 停摆退回——L3 #2 实板可读面不达标)`。
|
||||
- **缺口补记拟稿**:`缺口:14×14 板基座档(1280×800)实板格边 28px 低于 battle.md 可读下限 30px 的适用域待设计裁决——1280 档实板可用面积 (1022,454)→28px、1080 档 (1662,734)→46px;13×13 两档均达标(30/46)。裁决点=读法甲(全域硬下限→产品缺陷走设计动作)vs 读法乙(下限按视口档分级→断言判据域改 1080 档)。裁决后重写卡C(算式单点/门槛对齐/四档冒烟三件已验可复用)。`
|
||||
|
||||
## 六、本批实况速览
|
||||
|
||||
- 卡A 换图系双基座七件批:**收口**(T0–T8 全落,--code 闸 7 闸全绿)。
|
||||
- 卡B 站桩名字行提档与地台色合并批:**收口**(26 checks 绿,golden 三张 0.00% 留痕)。
|
||||
- 卡C 棋盘格边长口径收口批:**停摆**(T0 完成、T1–T3 改动已验未提交、T4 未走)——本呈报。
|
||||
- 迭代合并:**阻塞**,等本呈报裁决(拆批合回 A+B,或卡C 闭合后整体合回)。
|
||||
@@ -0,0 +1,60 @@
|
||||
# 复核报告:迭代 47 双卡 grilling+godot-master 双审(2026-11-01)
|
||||
|
||||
- **接收方**:`reports/需求卡片-营地场景图新建批-2026-10-18.md`(卡④,v3.0→v3.1)+ `reports/需求卡片-ESC浮窗与营地顶栏批-2026-10-18.md`(卡⑤,v1.3→v1.4)。本报告是双审过程稿,裁断与订正已全部吸收进两卡,批末收尾按吸收即死退役。
|
||||
- **审查构成**:四个独立子代理——卡④ grilling/卡⑤ grilling/卡④ godot-master/卡⑤ godot-master(全程只读、未落盘、未跑测试)。
|
||||
- **发起**:用户令(开工前审查:零待裁点、零阻塞点、零灰色地带、符合 Godot 设计规范)。
|
||||
- **裁决**:用户 2026-11-01 裁「都可以按照推荐来」——五裁点照推荐采纳、P3/P5 销号口径采纳、卡⑤ v1.4 补句、卡④ 漂移就地订正+Godot 风险项落卡。
|
||||
|
||||
## 一、双审结论
|
||||
|
||||
| 卡 | grilling | godot-master |
|
||||
|---|---|---|
|
||||
| 卡④ | 有条件可开工——卡内设计待裁点确为零、重立凭据属实、与卡⑤ 接口名实闭合;但 §0.3 四道开工门实证全部未销号(会卡 T1/T2/T5),另有成片现状事实漂移 | 基本符合有风险项——分层/信号/单点纪律全对齐、零硬违反;风险=1024×1024 源档降采样算式断裂+核心视觉不变量机器验收零覆盖 |
|
||||
| 卡⑤ | 有条件可开工——零真待裁点(Q1/Q2/Q3 留痕完整);但三处现行文本会指导出错实现(浮窗残留缺陷级/V4 判据与契约 3 矛盾/Esc 第四态未定义) | 基本符合有风险项——浮窗选型/层序/拆件与现役先例同构、零硬违反;风险=文件表漏列 ui_modals/main_ui_build 两必改件+Esc 第四态连带语境分支谓词二义 |
|
||||
|
||||
## 二、裁断与销号落点(2026-11-01 用户裁)
|
||||
|
||||
| 项 | 裁断 | 卡内落点 |
|
||||
|---|---|---|
|
||||
| camp-scene-prompts §5 五裁点 | ①帐篷 300×170 追认现值②门扇 240×220 追认③布告板 (845,268,130,140) 挂围墙带④描边奶油色追认⑤环境子板色值不前置(T1 量化后按 art-bible §4.1 两道门登记;T5 色计数改条件步) | 卡④ §0.3-3/§9-4/§10;反哺点(§5 落文归文档批) |
|
||||
| P3 点选反馈设计稿 | 销号口径=camp.md v2.2:35-37 四态规格即设计稿;名牌文案与可点区域视觉归 T2 域内 | 卡④ §0.2/§0.3-1 |
|
||||
| P5 GDD/art-bible 落文 | 销号口径=形态落文完成;实装回填归 T6 反哺点义务,非停机条件 | 卡④ §0.2/§0.3-2 |
|
||||
| T1 素材后处理发令 | /iter 开工令即含授权,不再两次发令 | 卡④ §0.3-4/T1 |
|
||||
| 1024×1024 档降采样 | 逐张登记换算式;1024 档非整数缩放显式特批登记(L2+实现报告申报局限),不回炉(追认已过审终稿优先) | 卡④ T1 |
|
||||
| Esc 第四态与接线 | 契约 9 四态优先序(判据单点=ModalStack 现开集合)+接线定稿=事件驱动 `_unhandled_input`+KEY_ESCAPE、禁 Input 单例轮询、不挂 ui_cancel、pressed 且非 echo | 卡⑤ 契约 9/T0/T1/§9-1 |
|
||||
| 设置语境分支谓词 | =`camp_esc_overlay.visible`(谁开的设置谁收),与 Esc 守卫谓词分立 | 卡⑤ 契约 3 |
|
||||
| 返回主菜单浮窗残留 | 动线首步 close 浮窗;V3 断言补浮窗 closed | 卡⑤ 契约 4/T3/V3 |
|
||||
| camp 截图排序约束死信 | 废止原句;按迭代 47 执行序=卡④ 整卡先、卡⑤ 后,卡⑤ T2 限 HUD 区显式更新兜底 | 卡⑤ §0/卡④ T0 |
|
||||
|
||||
## 三、双审独立发现、交叉确证的灰色地带(全部落卡)
|
||||
|
||||
1. Esc 第四态(设置开在浮窗之上再按 Esc)——两审独立发现 → 契约 9①。
|
||||
2. 返回主菜单后浮窗残留(缺陷级)——grilling 发现、godot-master 同域印证 → 契约 4。
|
||||
3. V4 判据与契约 3 自相矛盾(浮窗恒 visible 由栈语义保证,非属性隐藏)→ V4 改述。
|
||||
4. V6 badge 文本格式字面不等(带「钥石 」前缀)→ V6 改格式串比较。
|
||||
5. CampRefreshBus「Bus」命名 vs 全局事件总线否决史(architecture.md:353)→ 卡④ T3 头注自陈+卡⑤ 措辞「挂接/接收推送」。
|
||||
6. 文件表漏列 `ui_modals.gd`/`main_ui_build.gd`(S13 触面失察;ui_modals 类变量 14→15 恰贴上限)→ 两卡文件表与存量触面一查补列。
|
||||
7. 「卡④ T5 晚于卡⑤ T2」排序约束单侧声明且按执行序天然不可满足 → 死信改述(见 §二)。
|
||||
8. V7 零动效扫描面漏开合编排件 → 扫描面扩两新件;V5 补「禁对退出钮 pressed.emit」;V1 补冒险语境段与 echo 断言;「进档重读」触发主体钉组合根动线尾。
|
||||
|
||||
## 四、卡④ 成片事实漂移订正(T0 级,v3.1 就地订正)
|
||||
|
||||
`game/smoke/` 已退役(git 16c6f136)→ 探针全在 `game/probe/`;smoke_all.gd 已退役(git 957391e1)→ system_test.gd(V2 :67-68/V3g :520-541)+theme_projection_test.gd:710-717;`main.camp_overlay` 句柄已退役 → `main.ui.modals.camp_overlay`(ui_modals.gd:14);F3/F7 行号簇换 2026-11-01 实测;F6「断点弹窗现居主菜单」失实(main.gd:118-120 注释)——T4 承接;`_on_confirm_new_adventure` 已随卡②摘除;`UiKit.TEX_CAMP_*` 尚不存在(本批 ui_kit.gd 新增十键);`tools/gdstyle.sh` 不存在(→ static_scan.sh/gdstyle_gate.py);presenters/core 子目录路径订正;`design/gdd/06-待办清单.md` 已迁仓库根;契约 4 行号换实数(八处 :82/90/102/114/126/139/151/165=七口+on_camp_buy 双尾);终局落点 main.gd:703 → main_adventure_flow.gd:123-143。
|
||||
|
||||
## 五、卡⑤ F1–F10 抽验
|
||||
|
||||
10 条全抽:F1/F2/F3/F5/F6/F7/F8/F9/F10 属实;F4 消费点全对、行号三处系统性 ±1(keystone_badge :633→:632、modal_shell :487→:486、danger_button :700→:699)——已订正。卡外小漂:screen_flow_probe 营地旗标在 :54(:53 是菜单行)、待办清单五卡总条 :447 起——已订正。
|
||||
|
||||
## 六、Godot 规范审查补立(卡④)
|
||||
|
||||
- 节点选型钉死:Control 根+TextureRect 层+子节点序即 z 序;禁 CanvasLayer/z_index/Sprite2D 根(LAYER_CONTRACT 树序落位兼容)。
|
||||
- V1 扩判据:camp_scene_view 子节点序=layout 表 z 序逐层相等、纹理矩形与表逐值一致;新增 V14 layout 数据件校验(fail-fast 命中区几何)——核心视觉不变量机器验收面补齐。
|
||||
- T2 实现约束:浮动经 Tween 禁 `_process`;名牌 Label mouse_filter=IGNORE;禁用态去饱和经 ShaderMaterial;名牌画面内钳制;HUD 载体循 page_shell `PRESET_TOP_WIDE` 先例。
|
||||
- layout.tres 接 ConfigDB schema 类(先例 battle_layout.tres)+加载期 fail-fast 校验。
|
||||
|
||||
## 七、遗留与开工判定
|
||||
|
||||
- **零待裁点、零阻塞点**:卡④ §0.3 四道开工门全部裁断销号;卡⑤ 零真待裁点、开工按序随卡④ 代码波(迭代内排序,非跨迭代阻塞)。
|
||||
- 反哺点落文已完成(2026-11-01 本会话文档批,同批合回):camp-scene-prompts.md v1.3 §5 改已裁+art-bible.md v3.31 §6.2 待裁行改已裁;camp.md/art-bible 实装状态回填归迭代实现批 T6 义务。
|
||||
- 机检预算互斥裁决(2026-11-01 用户裁,停机呈报=iter47 worktree):实现段实测两卡平铺合计 ui_modals.gd 类变量 19>S13 上限 15——裁**最小改动**=营地六句柄落 `ui_camp_modals.gd` 子容器(新立卡⑥ 前置基建,迭代 47 执行序第一位;卡④ v3.2/卡⑤ v1.5 随批改口径,卡⑤ v1.4「14→15 恰贴」算术缺陷一并订正);全量分域句柄分家登记待办清单另立批。双审预算审为单卡口径、未做跨卡同文件累加核对,已在裁决中注记。
|
||||
- **判定:迭代 47 可开工**(卡⑥ → 卡④ → 卡⑤ 按序,`/iter` 发令即起 T0)。
|
||||
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user