chore: W1 项目骨架 — 基线 v1.0 架构落盘(Godot 4.7.2 / gl_compatibility)
架构基线:docs/architecture/2026-10-19-F-architecture.md(v1.0) 范围:WRATH OF THE DEAD《怨返》单人挂机 · GDScript · 1280×720 · 12 班底数据驱动 - 骨架:project.godot(4.7 + GL Compatibility)、架构 §2.1 完整目录树(含占位说明)、 .gitignore/.gitattributes(LF 纪律)、README.md、CLAUDE.md、export_presets.cfg 占位、 pipeline/README.md(3ds Max 侧契约) - 数据管线段 1:data/sheets/generators.csv(GDD §3.1 十二班底逐值抄录) + data/schema/generators.schema.json(v0)+ src/tools/sheet_importer.gd (import_all/import_sheet/validate + ImportReport + .tres stub + 行号/列名/原因失败策略) + src/tools/import_main.gd(CI CLI 入口)+ data/build/generators.json(字节级确定性产物) - 探针:P1(SCREEN_TEXTURE/CanvasLayer 排序)与 P3(NoiseTexture2D seamless)场景代码与 截图/统计工具;实测手册与结论模板在工作区 docs/engineering/(随仓库外文档树) - CI:数据门激活(import_all + schema validate + build 漂移门);单测/sim/导出 W3+ 注释占位; 4.7.2 headless CLI 行为标注待本机验证 待用户侧:P1/P3 实测回填结论;P6 字体许可 / P7 GPU 实测;look-dev 游魂初版(Max 侧)
This commit is contained in:
commit
856326a766
42 files changed
+1814
No files matched your search
@@ -0,0 +1,18 @@
|
|||||||
|
# 行尾纪律:CI 的 build/*.json 漂移门依赖稳定行尾;文本文件一律 LF 入库
|
||||||
|
*.gd text eol=lf
|
||||||
|
*.gdshader text eol=lf
|
||||||
|
*.tscn text eol=lf
|
||||||
|
*.tres text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.csv text eol=lf
|
||||||
|
*.cfg text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.svg text eol=lf
|
||||||
|
.gitattributes text eol=lf
|
||||||
|
.gitignore text eol=lf
|
||||||
|
.gitkeep text eol=lf
|
||||||
|
project.godot text eol=lf
|
||||||
|
*.txt text eol=lf
|
||||||
|
*.ms text eol=lf
|
||||||
|
*.py text eol=lf
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# CI —— F《怨返》数据门(架构 §7.2;W1 激活部分)
|
||||||
|
#
|
||||||
|
# W1 激活:checkout → setup Godot 4.7.2 → headless import → SheetImporter
|
||||||
|
# import_all → schema validate → build 漂移门(git diff)
|
||||||
|
# W3 激活:GUT 单测(需先合入 gd-unit-testing addon) —— 见下方注释块
|
||||||
|
# W4 激活:headless sim 14 门报告(artifact) —— 见下方注释块
|
||||||
|
# W5 激活:Windows 导出 → artifact(templates + export_presets 补全)
|
||||||
|
#
|
||||||
|
# 待本机验证(4.7.x 超出训练数据):
|
||||||
|
# - `godot --headless --path . --import` 的 4.7.2 实际行为(若失效改 -e --quit-after 200)
|
||||||
|
# - setup-godot@v1 对 4.7.2 版本串/模板的支持
|
||||||
|
# 首跑后按实际更新本文件与 README §5。
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
data-gate:
|
||||||
|
name: data-gate(schema v0 数据门)
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Godot 4.7.2
|
||||||
|
uses: chickensoft-games/setup-godot@v1
|
||||||
|
with:
|
||||||
|
version: 4.7.2
|
||||||
|
export-templates: false # W5 导出步骤再置 true
|
||||||
|
|
||||||
|
- name: Godot version sanity
|
||||||
|
run: godot --headless --version
|
||||||
|
|
||||||
|
# 首次 import:生成全局类缓存 + 资源导入(SheetImporter 才能被解析)
|
||||||
|
- name: Headless import
|
||||||
|
run: godot --headless --path . --import
|
||||||
|
|
||||||
|
# 数据门 1:CSV → build/*.json(schema 违规 = 中止 + 行号/列名/原因,退出码 1)
|
||||||
|
- name: SheetImporter import_all
|
||||||
|
run: godot --headless --path . -s res://src/tools/import_main.gd
|
||||||
|
|
||||||
|
# 数据门 2:build/*.json 对 schema 合规性
|
||||||
|
- name: Schema validate
|
||||||
|
run: godot --headless --path . -s res://src/tools/import_main.gd -- --validate-only
|
||||||
|
|
||||||
|
# 数据门 3:漂移门——改了 CSV 没重新生成提交 build = 红("CI 生成并提交"纪律)
|
||||||
|
# 行尾由 .gitattributes 钉 LF,避免格式性误报
|
||||||
|
- name: Build drift check
|
||||||
|
run: git diff --exit-code -- data/build/
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# W3 激活(前置:合入 gd-unit-testing addon 到 addons/gut/):
|
||||||
|
# unit-tests:
|
||||||
|
# runs-on: windows-latest
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v4
|
||||||
|
# - uses: chickensoft-games/setup-godot@v1
|
||||||
|
# with: { version: "4.7.2", export-templates: false }
|
||||||
|
# - run: godot --headless --path . --import
|
||||||
|
# - run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd
|
||||||
|
# -gtest=res://tests/unit -gexit
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# W4 激活(sim 14 门;W6 起门 #5/#10 设硬门,此前 informational):
|
||||||
|
# sim-gates:
|
||||||
|
# runs-on: windows-latest
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v4
|
||||||
|
# - uses: chickensoft-games/setup-godot@v1
|
||||||
|
# with: { version: "4.7.2", export-templates: false }
|
||||||
|
# - run: godot --headless --path . --import
|
||||||
|
# - run: godot --headless --path . -s res://src/core/sim/sim_main.gd
|
||||||
|
# - uses: actions/upload-artifact@v4
|
||||||
|
# with: { name: sim-report, path: tests/sim_reports/ }
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# W5 激活(release tag:Windows 导出 → artifact → 人工上传 Steamworks):
|
||||||
|
# export-windows:
|
||||||
|
# if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
# runs-on: windows-latest
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v4
|
||||||
|
# - uses: chickensoft-games/setup-godot@v1
|
||||||
|
# with: { version: "4.7.2", export-templates: true }
|
||||||
|
# - run: godot --headless --path . --import
|
||||||
|
# - run: godot --headless --path . --export-release "Windows Desktop" build/wrath-of-the-dead.exe
|
||||||
|
# - uses: actions/upload-artifact@v4
|
||||||
|
# with: { name: windows-build, path: build/ }
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# ---- Godot 4 ----
|
||||||
|
.godot/
|
||||||
|
/android/
|
||||||
|
.import/
|
||||||
|
|
||||||
|
# ---- 导出产物(架构 §2.1:data/build 是数据产物不受影响,见下方排除只锚根目录)----
|
||||||
|
/build/
|
||||||
|
*.exe
|
||||||
|
*.pck
|
||||||
|
*.dmg
|
||||||
|
*.app
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
# ---- 系统垃圾 ----
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# ---- 编辑器临时 ----
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# CLAUDE.md —— F《WRATH OF THE DEAD / 怨返》工程偏好
|
||||||
|
|
||||||
|
> 由 `docs/architecture/2026-10-19-F-architecture.md`(v1.0)派生 · 2026-10-19 · W1
|
||||||
|
> 本文件为工程宪法摘要;**冲突时以架构文档为准**;架构更新须两边同步。目标 ≤100 行。
|
||||||
|
|
||||||
|
## 引擎版本纪律
|
||||||
|
|
||||||
|
- **钉 Godot 4.7.x 补丁线,当前 4.7.2**(`project.godot` config/features);小版本补丁可升,**跨 minor(4.8)上线后不追**;
|
||||||
|
- GDScript + GodotSteam **v4.23.1**(GDExtension 构建必须与 4.7.x 匹配;升级引擎 = 重新获取匹配构建);
|
||||||
|
- renderer:`gl_compatibility`(2D 合成 + 核显底线;改 renderer = ADR 级动作);
|
||||||
|
- ⚠ **知识缺口**:4.7.x 超出训练数据。API 不确定 → 先查官方文档/探针实测,**不臆造 API**;缺口持续标注,`docs/engine-reference/` 待补(架构 §11 P0)。
|
||||||
|
|
||||||
|
## 路径作用域编码标准(架构 §2.2)
|
||||||
|
|
||||||
|
| 作用域 | 纪律 |
|
||||||
|
|---|---|
|
||||||
|
| `src/core/` | 纯逻辑;禁 `extends Node`/访问场景树;`tick()` 零分配;100% 被 sim 与 GUT 无头复用 |
|
||||||
|
| `src/game/` | 唯一权威状态持有者;系统在此 tick;不直连 Steam(走 platform/);不碰渲染 |
|
||||||
|
| `src/presentation/` | **只读快照 + 发命令**;UI 不持有游戏状态(仅视图本地态);禁反向写 core |
|
||||||
|
| `src/platform/` | Steam API 唯一出口;`game/` 禁直调 `Steam.*` |
|
||||||
|
| `src/tools/` | `@tool` 标注;CI 可 headless;运行时场景禁引用 |
|
||||||
|
| `data/` | 零代码;稳定字符串 ID;**不重编号**;schema 违规 = CI 红 |
|
||||||
|
| `pipeline/` | 契约 = 命名 + 导出参数;唯一逐资产代码 = sprite 路径 |
|
||||||
|
| network | **不适用**:单人买断无网络代码 |
|
||||||
|
|
||||||
|
依赖方向:`presentation → game → core`;`game → platform`;`tools → data`。禁反向、禁跨层。
|
||||||
|
|
||||||
|
## 数据驱动红线(架构 §9.2 + ADR-3)
|
||||||
|
|
||||||
|
1. 零硬编码:一切数值进 `data/sheets/` → JSON → Resource;
|
||||||
|
2. **新资产 = 加行**;唯一逐资产代码 = `GeneratorDef.sprite_row`;
|
||||||
|
3. 新增资产若需改代码 = **设计失败,回退重做**;
|
||||||
|
4. 稳定 ID 永不重编号/复用;schema 违规 = CI 红(行号+列名+原因,禁跳过坏行);
|
||||||
|
5. 数值列禁千分位;`_cn/_en` 本地化后缀;引用列填目标 ID;
|
||||||
|
6. "AI 让加一个班底变便宜了"禁用作加范围理由。
|
||||||
|
|
||||||
|
## ADR 一句话(架构 §8,均已接受)
|
||||||
|
|
||||||
|
- **ADR-1**:预渲染 2 朝向 billboard,不用实时 3D(用户比较优势在批渲染);
|
||||||
|
- **ADR-2**:单趟 fused 全屏后处理 shader(posterize→LUT→纸纹→暗角),UI 层在其上;
|
||||||
|
- **ADR-3**:美术数据行纪律(新资产=加行;唯一逐资产代码 = sprite 路径);
|
||||||
|
- **ADR-4**:AI 仅内部使用(mood board/概念),不进包体、不出厂、商店页披露填 No。
|
||||||
|
|
||||||
|
## 探针状态(W1)
|
||||||
|
|
||||||
|
| 探针 | 状态 | 手册 / 结论 |
|
||||||
|
|---|---|---|
|
||||||
|
| P1 SCREEN_TEXTURE/CanvasLayer | ⏳ 待用户实测 | `docs/engineering/2026-10-19-f-w1-probe-p1-manual.md` → `docs/engineering/probes/P1-result.md` |
|
||||||
|
| P3 noise seamless 平铺 | ⏳ 待用户实测 | `docs/engineering/2026-10-19-f-w1-probe-p3-manual.md` → `docs/engineering/probes/P3-result.md` |
|
||||||
|
| P6 字体许可 / P7 用户 GPU | ⏳ 用户侧任务(W1) | 不在本仓库;结论回填 probes/ |
|
||||||
|
| P2/P4/P5/P8-P11 | 未启动 | 架构 §11 排期(W2-W5) |
|
||||||
|
|
||||||
|
**不过的回退**:P1 → SubViewport+BackBufferCopy(+2-4h);P3 → 脚本预烘焙无缝纹理(+0.5-1h)。
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 数据门(导入 + 校验)
|
||||||
|
godot --headless --path . --import
|
||||||
|
godot --headless --path . -s res://src/tools/import_main.gd # import_all
|
||||||
|
godot --headless --path . -s res://src/tools/import_main.gd -- --validate-only
|
||||||
|
# 运行:编辑器 F5(main scene W2 起);探针:打开 tests/probe_* 场景 F6
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据管线速查
|
||||||
|
|
||||||
|
`data/sheets/*.csv` →(SheetImporter,本仓库 `src/tools/`)→ `data/build/*.json`(提交)→(W3 `.tres`)→ DataRegistry。
|
||||||
|
单向流动:游戏永不写 `data/`;sim 与游戏读同一份 build。契约见 `README.md` + 架构 §3。
|
||||||
|
|
||||||
|
## 纪律提醒
|
||||||
|
|
||||||
|
- 写代码前**先写测试**(GUT W3 起);每个 Story 留测试证据路径;
|
||||||
|
- 高影响动作(提交/删除/上线)须用户审批;本仓库 `git` 提交信息写架构基线;
|
||||||
|
- 目录微调 = 架构 §2.1 变更 = ADR 流程,不擅自挪目录。
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# WRATH OF THE DEAD / 怨返(项目代号 F)
|
||||||
|
|
||||||
|
> 含冤而死,那就招一支阴兵,杀回人间。
|
||||||
|
> Wrongfully dead? Raise a ghost army and fight your way back to the living world.
|
||||||
|
|
||||||
|
单人挂机(idle)· Godot 4.7.2 + GDScript · Zad Archery 式横版 · 水墨绘卷视觉
|
||||||
|
架构基线:`docs/architecture/2026-10-19-F-architecture.md`(v1.0,2026-10-19)
|
||||||
|
工程宪法摘要:`CLAUDE.md`(必读)
|
||||||
|
|
||||||
|
## 1. 环境要求
|
||||||
|
|
||||||
|
| 项 | 要求 | 备注 |
|
||||||
|
|---|---|---|
|
||||||
|
| 引擎 | **Godot 4.7.2**(.NET 不需要,纯 GDScript) | 钉 4.7.x 补丁线;跨 minor 不追 |
|
||||||
|
| 导出模板 | 4.7.2 stable 模板(W5 CI 打包前安装) | Editor → Manage Export Templates |
|
||||||
|
| Steam | GodotSteam v4.23.1(W5 接入,W1 不依赖) | addon 二进制暂未入库 |
|
||||||
|
| GPU | 核显可跑(性能底线 30fps) | 用户 GPU 实测定预算(探针 P7) |
|
||||||
|
|
||||||
|
## 2. 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) 编辑器打开本目录(project.godot)→ 等待首次资源导入
|
||||||
|
# 2) 数据门验证(headless)
|
||||||
|
godot --headless --path . --import
|
||||||
|
godot --headless --path . -s res://src/tools/import_main.gd # import_all(期望 RESULT: PASS)
|
||||||
|
godot --headless --path . -s res://src/tools/import_main.gd -- --validate-only
|
||||||
|
# 3) 跑探针:编辑器打开 tests/probe_p1_screen_texture/probe_p1.tscn → F6
|
||||||
|
# (main scene 自 W2 起由 game/ 编排层提供,F5 直接运行)
|
||||||
|
git status # import_all 后 data/build 应无 diff(提交物已校准)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 仓库地图(架构 §2.1)
|
||||||
|
|
||||||
|
```
|
||||||
|
project.godot / export_presets.cfg / icon.svg / CLAUDE.md
|
||||||
|
data/sheets/*.csv 内容唯一源(策划维护;一行=一资产/一参数)
|
||||||
|
data/schema/*.json 每张表一份 JSON Schema(CI 数据门)
|
||||||
|
data/build/*.json SheetImporter 产物(提交;sim 与游戏共用)
|
||||||
|
src/core/ 纯逻辑:numbers / economy / save / sim(W3 起)
|
||||||
|
src/game/ 编排层:GameState / DataRegistry / 各 Director
|
||||||
|
src/platform/ SteamService(W5)
|
||||||
|
src/presentation/ 表现层:stage / hud / panels / modals / post
|
||||||
|
src/tools/ @tool:sheet_importer.gd + import_main.gd(CI 入口)
|
||||||
|
assets/ sprites / luts / shaders / themes / fonts
|
||||||
|
tests/ probe_*(W1)/ unit(W3 起)/ sim_reports(W4 起)
|
||||||
|
pipeline/ 3ds Max 侧契约(README.md;Max 脚本用户主场)
|
||||||
|
.github/workflows/ CI 数据门(W3+ 激活单测/sim/导出)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 数据管线("年更=加行"的地基,架构 §3)
|
||||||
|
|
||||||
|
```
|
||||||
|
data/sheets/*.csv ──SheetImporter(src/tools/,@tool/CI headless)──► data/build/*.json
|
||||||
|
│ schema 校验(违规=中止+行号/列名/原因)
|
||||||
|
▼(W3)
|
||||||
|
*.tres Resource ──► DataRegistry ──► 游戏 + sim(同一份,零漂移)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 三条铁律:① 单向流动(游戏永不写 `data/`);② sim 与游戏读同一份 build;③ 数值改动**只改 CSV 不动代码**;
|
||||||
|
- CSV 规范:稳定字符串 ID(不重编号);`_cn/_en` 后缀;数值列禁千分位;引用列填目标 ID;
|
||||||
|
- build JSON 由导入器的确定性序列化器生成(字节稳定)——**改 CSV 后必须重跑 import 并提交 build**,否则 CI 漂移门红;
|
||||||
|
- **W1 已知**:`display_en` 为工程侧临时译名(待文策渊按 GDD §8.4 glossary 确认);`unlock_hall/unlock_gate` 引用的 halls/upgrades 表 W2 落盘后激活跨表引用校验。
|
||||||
|
|
||||||
|
## 5. CI(架构 §7.2)
|
||||||
|
|
||||||
|
`.github/workflows/ci.yml`:push/PR → checkout → setup Godot 4.7.2 → headless import → `import_all` → schema `validate` → build 漂移门(git diff)。
|
||||||
|
单元测试(W3,GUT)、sim 14 门(W4)、Windows 导出(W5)为注释占位,标注了激活条件。
|
||||||
|
**待本机验证**:4.7.2 的 `--headless --import` CLI 行为(备选 `-e --quit-after 200`)——首跑后按实际更新本 README。
|
||||||
|
|
||||||
|
## 6. W1 探针(用户侧实测,30min/项)
|
||||||
|
|
||||||
|
| 探针 | 手册(工作区 docs/engineering/) | 回填 |
|
||||||
|
|---|---|---|
|
||||||
|
| P1 后处理纹理语义 | `2026-10-19-f-w1-probe-p1-manual.md` | `docs/engineering/probes/P1-result.md` |
|
||||||
|
| P3 noise 平铺 | `2026-10-19-f-w1-probe-p3-manual.md` | `docs/engineering/probes/P3-result.md` |
|
||||||
|
|
||||||
|
P6(字体许可)/P7(GPU)为用户侧任务,结论同样回填 probes/。
|
||||||
|
|
||||||
|
## 7. 纪律速览(详见 CLAUDE.md / 架构 §9)
|
||||||
|
|
||||||
|
- look-dev 冻结(W2 生死门):冻结前不加新资产类型;Max 侧不回炉;调色只在 Godot 侧;
|
||||||
|
- 水墨四色面积纪律:墨 ~65 / 骨白 ~25 / 朱砂 ≤8% / 靛 ≤7%;
|
||||||
|
- 终局 ≤1e14;三乘区加法优先禁乘性深度叠加;
|
||||||
|
- network 作用域不适用(单人无网络代码);AI 不进包体(ADR-4)。
|
||||||
|
|
||||||
|
## 8. W1 状态与待办
|
||||||
|
|
||||||
|
- [x] 项目骨架 + git 首提交(本 README 同步日期)
|
||||||
|
- [x] 数据管线段 1(generators.csv 12 行 + schema v0 + SheetImporter v0.1 + build 提交)
|
||||||
|
- [x] 探针 P1/P3 代码 + 实测手册 + 结论模板
|
||||||
|
- [ ] 用户:跑 P1/P3 实测并回填结论;P6/P7 用户侧任务
|
||||||
|
- [ ] 用户:look-dev 游魂初版(Max 侧)→ 导入验证 sprite_row 路径
|
||||||
|
- [ ] W2 计划:管线贯通 demo(CSV→12 槽 UI)、sim 骨架、军列带纸面原型
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# assets/fonts —— 子集化 OFL 字体(LXGW WenKai/Ma Shan Zheng 标题 + Noto Sans SC 正文 + JetBrains Mono 数字)。P6 许可复核后落盘;子集脚本 pipeline/fonts/(架构 §7.4)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# assets/luts —— per-hall LUT PNG(tools/lut_generator.gd 产物,64×1)。W4 起(架构 §5.3)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# assets/shaders —— 后处理 fused shader / 母题 shader / 单位描边兜底。探针 P1 通过后落盘(架构 §5.2)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# assets/sprites/units —— billboard 精灵:2 朝向×2 帧 + units_atlas.png(MultiMesh)。Max 侧批量渲染产物(pipeline/ 契约);look-dev 游魂 W1-2 首片。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# assets/sprites/vfx —— 墨点/笔触/斩弧(管线报告 §1.2)。W6 juice 最小集。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# assets/themes —— 水墨 Theme(界格/卷轴/印章语言;禁圆角卡片/默认组件蓝)。W4-W6(GDD §7.2④)。
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
{
|
||||||
|
"sheet": "generators",
|
||||||
|
"schema_version": 0,
|
||||||
|
"columns": [
|
||||||
|
"id",
|
||||||
|
"display_cn",
|
||||||
|
"display_en",
|
||||||
|
"base_cost",
|
||||||
|
"cost_ratio",
|
||||||
|
"base_rate",
|
||||||
|
"unlock_hall",
|
||||||
|
"unlock_gate",
|
||||||
|
"sprite_row"
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "gen_01_youhun",
|
||||||
|
"display_cn": "游魂",
|
||||||
|
"display_en": "Wandering Souls",
|
||||||
|
"base_cost": 15,
|
||||||
|
"cost_ratio": 1.13,
|
||||||
|
"base_rate": 0.1,
|
||||||
|
"unlock_hall": "none",
|
||||||
|
"unlock_gate": "none",
|
||||||
|
"sprite_row": "res://assets/sprites/units/gen_01_youhun"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen_02_yinbing",
|
||||||
|
"display_cn": "阴兵",
|
||||||
|
"display_en": "Ghost Soldiers",
|
||||||
|
"base_cost": 120,
|
||||||
|
"cost_ratio": 1.14,
|
||||||
|
"base_rate": 1,
|
||||||
|
"unlock_hall": "none",
|
||||||
|
"unlock_gate": "none",
|
||||||
|
"sprite_row": "res://assets/sprites/units/gen_02_yinbing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen_03_guizu",
|
||||||
|
"display_cn": "鬼卒班",
|
||||||
|
"display_en": "Ghost Constables",
|
||||||
|
"base_cost": 1200,
|
||||||
|
"cost_ratio": 1.15,
|
||||||
|
"base_rate": 8,
|
||||||
|
"unlock_hall": "hall_01_qinguang",
|
||||||
|
"unlock_gate": "up_gate_guizu",
|
||||||
|
"sprite_row": "res://assets/sprites/units/gen_03_guizu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen_04_biliao",
|
||||||
|
"display_cn": "勾魂笔僚",
|
||||||
|
"display_en": "Death-Scribe Clerks",
|
||||||
|
"base_cost": 12000,
|
||||||
|
"cost_ratio": 1.15,
|
||||||
|
"base_rate": 50,
|
||||||
|
"unlock_hall": "hall_02_chujiang",
|
||||||
|
"unlock_gate": "up_gate_biliao",
|
||||||
|
"sprite_row": "res://assets/sprites/units/gen_04_biliao"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen_05_tangpu",
|
||||||
|
"display_cn": "孟婆汤铺",
|
||||||
|
"display_en": "Meng Po's Soup Stall",
|
||||||
|
"base_cost": 120000,
|
||||||
|
"cost_ratio": 1.16,
|
||||||
|
"base_rate": 400,
|
||||||
|
"unlock_hall": "hall_03_songdi",
|
||||||
|
"unlock_gate": "up_gate_tangpu",
|
||||||
|
"sprite_row": "res://assets/sprites/units/gen_05_tangpu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen_06_nanmin",
|
||||||
|
"display_cn": "枉死城难民团",
|
||||||
|
"display_en": "Refugee Camp of the Wrongfully Dead",
|
||||||
|
"base_cost": 1200000,
|
||||||
|
"cost_ratio": 1.16,
|
||||||
|
"base_rate": 3000,
|
||||||
|
"unlock_hall": "hall_04_wuguan",
|
||||||
|
"unlock_gate": "up_gate_nanmin",
|
||||||
|
"sprite_row": "res://assets/sprites/units/gen_06_nanmin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen_07_shuban",
|
||||||
|
"display_cn": "阴律司书办",
|
||||||
|
"display_en": "Clerks of the Nether Court",
|
||||||
|
"base_cost": 12000000,
|
||||||
|
"cost_ratio": 1.17,
|
||||||
|
"base_rate": 24000,
|
||||||
|
"unlock_hall": "hall_05_yama",
|
||||||
|
"unlock_gate": "up_gate_shuban",
|
||||||
|
"sprite_row": "res://assets/sprites/units/gen_07_shuban"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen_08_bingfang",
|
||||||
|
"display_cn": "幽都兵坊",
|
||||||
|
"display_en": "Armory of the Dark Capital",
|
||||||
|
"base_cost": 120000000,
|
||||||
|
"cost_ratio": 1.17,
|
||||||
|
"base_rate": 180000,
|
||||||
|
"unlock_hall": "hall_06_biancheng",
|
||||||
|
"unlock_gate": "up_gate_bingfang",
|
||||||
|
"sprite_row": "res://assets/sprites/units/gen_08_bingfang"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen_09_wodi",
|
||||||
|
"display_cn": "十殿卧底",
|
||||||
|
"display_en": "Sleeper Agents in the Ten Courts",
|
||||||
|
"base_cost": 1200000000,
|
||||||
|
"cost_ratio": 1.18,
|
||||||
|
"base_rate": 1400000,
|
||||||
|
"unlock_hall": "hall_07_taishan",
|
||||||
|
"unlock_gate": "up_gate_wodi",
|
||||||
|
"sprite_row": "res://assets/sprites/units/gen_09_wodi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen_10_xingzhe",
|
||||||
|
"display_cn": "轮回裂隙行者",
|
||||||
|
"display_en": "Rift Walkers of the Wheel",
|
||||||
|
"base_cost": 12000000000,
|
||||||
|
"cost_ratio": 1.18,
|
||||||
|
"base_rate": 11000000,
|
||||||
|
"unlock_hall": "hall_08_dushi",
|
||||||
|
"unlock_gate": "up_gate_xingzhe",
|
||||||
|
"sprite_row": "res://assets/sprites/units/gen_10_xingzhe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen_11_yinhun",
|
||||||
|
"display_cn": "六道引魂人",
|
||||||
|
"display_en": "Soul Guides of the Six Paths",
|
||||||
|
"base_cost": 120000000000,
|
||||||
|
"cost_ratio": 1.19,
|
||||||
|
"base_rate": 85000000,
|
||||||
|
"unlock_hall": "hall_09_pingdeng",
|
||||||
|
"unlock_gate": "up_gate_yinhun",
|
||||||
|
"sprite_row": "res://assets/sprites/units/gen_11_yinhun"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen_12_tianzi",
|
||||||
|
"display_cn": "幽冥天子",
|
||||||
|
"display_en": "Emperor of the Netherworld",
|
||||||
|
"base_cost": 1200000000000,
|
||||||
|
"cost_ratio": 1.2,
|
||||||
|
"base_rate": 650000000,
|
||||||
|
"unlock_hall": "none",
|
||||||
|
"unlock_gate": "up_gate_tianzi",
|
||||||
|
"sprite_row": "res://assets/sprites/units/gen_12_tianzi"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "generators.schema.json",
|
||||||
|
"title": "generators 表 schema v0(十二班底发生器,数值抄录自 GDD §3.1)",
|
||||||
|
"description": "架构 §3.2 CSV 规范落盘。数值列禁千分位;引用列填目标 ID 或 none;ID 一旦分配永不重编号。v0 仅做单表校验:unlock_hall/unlock_gate 引用的 halls.csv / upgrades.csv 目标表 W2 落盘,届时激活跨表引用完整性校验。",
|
||||||
|
"x-supported-keywords": [
|
||||||
|
"type", "properties", "required", "items", "enum", "pattern",
|
||||||
|
"minimum", "maximum", "minLength", "minItems", "maxItems",
|
||||||
|
"additionalProperties", "const"
|
||||||
|
],
|
||||||
|
"x-unique": ["id"],
|
||||||
|
"type": "object",
|
||||||
|
"required": ["sheet", "schema_version", "columns", "rows"],
|
||||||
|
"properties": {
|
||||||
|
"sheet": { "const": "generators" },
|
||||||
|
"schema_version": { "const": 0 },
|
||||||
|
"columns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"minItems": 9
|
||||||
|
},
|
||||||
|
"rows": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 12,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id", "display_cn", "display_en", "base_cost",
|
||||||
|
"cost_ratio", "base_rate", "unlock_hall", "unlock_gate", "sprite_row"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^gen_[0-9]{2}_[a-z0-9]+$",
|
||||||
|
"description": "稳定字符串 ID;永不重编号、永不复用(年更=加行)"
|
||||||
|
},
|
||||||
|
"display_cn": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "简体名(GDD §3.1 班底名)"
|
||||||
|
},
|
||||||
|
"display_en": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "英文名——W1 工程侧临时译名,待文策渊按 GDD §8.4 glossary 口径确认后回填"
|
||||||
|
},
|
||||||
|
"base_cost": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "C0(GDD §3.1);整数写法,禁千分位与 K/M/B/T 后缀"
|
||||||
|
},
|
||||||
|
"cost_ratio": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 2,
|
||||||
|
"description": "r(1.13→1.20,GDD §3.1)"
|
||||||
|
},
|
||||||
|
"base_rate": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "rate/s(GDD §3.1);阴德/秒"
|
||||||
|
},
|
||||||
|
"unlock_hall": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^(none|hall_[0-9]{2}_[a-z]+)$",
|
||||||
|
"description": "殿门闸门 ID(halls.csv 行 ID)或 none"
|
||||||
|
},
|
||||||
|
"unlock_gate": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^(none|up_gate_[a-z0-9_]+)$",
|
||||||
|
"description": "路引闸门 ID(upgrades.csv 行 ID)或 none;gen_12 的敕封前置(宿慧≥60 且转世≥2)属 upgrades.csv 行内规则,W2 建模"
|
||||||
|
},
|
||||||
|
"sprite_row": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^res://assets/sprites/[a-z_]+/[a-z0-9_]+$",
|
||||||
|
"description": "★ ADR-3:唯一允许的逐资产代码 = sprite 路径前缀(Max 侧命名 <unit_id>_<l|r>_<frame>.png)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
id,display_cn,display_en,base_cost,cost_ratio,base_rate,unlock_hall,unlock_gate,sprite_row
|
||||||
|
gen_01_youhun,游魂,Wandering Souls,15,1.13,0.1,none,none,res://assets/sprites/units/gen_01_youhun
|
||||||
|
gen_02_yinbing,阴兵,Ghost Soldiers,120,1.14,1,none,none,res://assets/sprites/units/gen_02_yinbing
|
||||||
|
gen_03_guizu,鬼卒班,Ghost Constables,1200,1.15,8,hall_01_qinguang,up_gate_guizu,res://assets/sprites/units/gen_03_guizu
|
||||||
|
gen_04_biliao,勾魂笔僚,Death-Scribe Clerks,12000,1.15,50,hall_02_chujiang,up_gate_biliao,res://assets/sprites/units/gen_04_biliao
|
||||||
|
gen_05_tangpu,孟婆汤铺,Meng Po's Soup Stall,120000,1.16,400,hall_03_songdi,up_gate_tangpu,res://assets/sprites/units/gen_05_tangpu
|
||||||
|
gen_06_nanmin,枉死城难民团,Refugee Camp of the Wrongfully Dead,1200000,1.16,3000,hall_04_wuguan,up_gate_nanmin,res://assets/sprites/units/gen_06_nanmin
|
||||||
|
gen_07_shuban,阴律司书办,Clerks of the Nether Court,12000000,1.17,24000,hall_05_yama,up_gate_shuban,res://assets/sprites/units/gen_07_shuban
|
||||||
|
gen_08_bingfang,幽都兵坊,Armory of the Dark Capital,120000000,1.17,180000,hall_06_biancheng,up_gate_bingfang,res://assets/sprites/units/gen_08_bingfang
|
||||||
|
gen_09_wodi,十殿卧底,Sleeper Agents in the Ten Courts,1200000000,1.18,1400000,hall_07_taishan,up_gate_wodi,res://assets/sprites/units/gen_09_wodi
|
||||||
|
gen_10_xingzhe,轮回裂隙行者,Rift Walkers of the Wheel,12000000000,1.18,11000000,hall_08_dushi,up_gate_xingzhe,res://assets/sprites/units/gen_10_xingzhe
|
||||||
|
gen_11_yinhun,六道引魂人,Soul Guides of the Six Paths,120000000000,1.19,85000000,hall_09_pingdeng,up_gate_yinhun,res://assets/sprites/units/gen_11_yinhun
|
||||||
|
gen_12_tianzi,幽冥天子,Emperor of the Netherworld,1200000000000,1.20,650000000,none,up_gate_tianzi,res://assets/sprites/units/gen_12_tianzi
|
||||||
|
@@ -0,0 +1,46 @@
|
|||||||
|
; ============================================================================
|
||||||
|
; export_presets.cfg —— W1 占位版本
|
||||||
|
; 主预设:Windows Desktop x86_64(架构 §7.1);macOS/Linux 次预设 W5 补。
|
||||||
|
; 本文件为手工最小占位;W5 CI 打包前用编辑器 Project > Export 向导重新生成校准
|
||||||
|
; (4.7 选项键以编辑器实际导出为准——4.7.x 超出训练数据,此处仅保证可解析)。
|
||||||
|
; 导出模板:Godot 4.7.2 stable 导出模板(Editor > Manage Export Templates)。
|
||||||
|
; ============================================================================
|
||||||
|
|
||||||
|
[preset.0]
|
||||||
|
|
||||||
|
name="Windows Desktop"
|
||||||
|
platform="Windows Desktop"
|
||||||
|
runnable=true
|
||||||
|
advanced_settings=false
|
||||||
|
dedicated_server=false
|
||||||
|
custom_features=""
|
||||||
|
export_filter="all_resources"
|
||||||
|
include_filter=""
|
||||||
|
exclude_filter=""
|
||||||
|
export_path="build/wrath-of-the-dead.exe"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
encryption_include_filters=""
|
||||||
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
|
encrypt_pck=false
|
||||||
|
encrypt_directory=false
|
||||||
|
|
||||||
|
[preset.0.options]
|
||||||
|
|
||||||
|
custom_template/debug=""
|
||||||
|
custom_template/release=""
|
||||||
|
binary_format/architecture="x86_64"
|
||||||
|
texture_format/bptc=true
|
||||||
|
texture_format/s3tc=true
|
||||||
|
texture_format/etc=false
|
||||||
|
texture_format/etc2=false
|
||||||
|
application/modify_resources=true
|
||||||
|
application/icon=""
|
||||||
|
application/file_version=""
|
||||||
|
application/product_version=""
|
||||||
|
application/company_name=""
|
||||||
|
application/product_name="WRATH OF THE DEAD"
|
||||||
|
application/file_description=""
|
||||||
|
application/copyright=""
|
||||||
|
application/trademarks=""
|
||||||
|
ssh_remote_deploy/enabled=false
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||||
|
<rect width="128" height="128" fill="#1A1614"/>
|
||||||
|
<circle cx="64" cy="64" r="36" fill="none" stroke="#EDE6D8" stroke-width="7"/>
|
||||||
|
<path d="M64 22 v84 M22 64 h84" stroke="#B03A2E" stroke-width="5"/>
|
||||||
|
<circle cx="64" cy="64" r="6" fill="#B03A2E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 345 B |
@@ -0,0 +1,48 @@
|
|||||||
|
# pipeline/ —— 3ds Max 侧协作契约(工程侧规格,用户主场)
|
||||||
|
|
||||||
|
> 架构依据:`docs/architecture/2026-10-19-F-architecture.md` §7.3 | 撰写:程基岩(W1)
|
||||||
|
> **边界纪律**:本目录是**契约**不是实现——`batch_render.ms` / `asset_manifest.csv` 由用户(3ds Max 侧)实现;工程侧只消费资产不碰 Max。Max 侧交片后不回炉(除造型错误);所有调色动作只在 Godot 侧(ADR-1 / 架构 §9.1 C2)。
|
||||||
|
|
||||||
|
## 1. 命名约定(与 `data/sheets/generators.csv` 的 `sprite_row` 对齐)
|
||||||
|
|
||||||
|
```
|
||||||
|
<unit_id>_<朝向>_<帧>.png
|
||||||
|
```
|
||||||
|
|
||||||
|
- `<unit_id>` = `generators.csv` 的 `id` 列(如 `gen_01_youhun`)——**新单位先在 CSV 加行,再出图**(ADR-3:新资产 = 加行);
|
||||||
|
- `<朝向>`:`l`(面左)/ `r`(面右)——军列向右推进时用 `r`,殿王战主角提刀用 `l`;
|
||||||
|
- `<帧>`:`0`(站立)/ `1`(行军起伏)——MVP 每朝向 2 帧(架构 §5.1);
|
||||||
|
- 示例:`gen_01_youhun_r_0.png`、`gen_03_guizu_l_1.png`;
|
||||||
|
- 交付位置:`assets/sprites/units/`(本仓库);图集打包在 W2+ 由 `tools/` 侧脚本合 `units_atlas.png` + UV manifest。
|
||||||
|
|
||||||
|
## 2. 规格(每片必守)
|
||||||
|
|
||||||
|
| 项 | 规格 | 依据 |
|
||||||
|
|---|---|---|
|
||||||
|
| 世界尺度 | 统一身高尺度:单位 256px 画布 ≈ 统一世界高度(十殿单位同尺度) | 架构 §5.1 |
|
||||||
|
| 轴心 | 统一**脚底轴心**(bottom-center) | 架构 §5.1 |
|
||||||
|
| 尺寸档 | 单位 256×256 / 殿王·套件 512×512 / 商店 hero 1024×1024 | 架构 §5.1 |
|
||||||
|
| 格式 | PNG 32-bit **直通 alpha**(不做预乘、不烘背景) | 架构 §5.1 |
|
||||||
|
| 上色 | Ink'n'Paint **3 阶平色** + 墨黑 `#1A1614` 描边**一带**(粗细一致 ±1px @256) | 架构 §9.1 冻结评审 #6 |
|
||||||
|
| 描边写入 | 描边烘焙进 sprite(主方案);P2 探针复核 alpha 边缘 halo | 架构 §11 P2 |
|
||||||
|
| 面数/灯光 | primitive 摆件 + 三灯即可;不追求写实 | 管线报告 §1.2 |
|
||||||
|
| 剪影 | 可读剪影优先(评审 #3);刑典母题走剪影 + 文字暗示(GDD §0.3 IP 红线) | GDD §0.3 |
|
||||||
|
|
||||||
|
## 3. look-dev 三件套(W1-2 生死门,架构 §10.1)
|
||||||
|
|
||||||
|
1. **一单位**:游魂 `gen_01_youhun`,2 朝向 × 2 帧,256×256;
|
||||||
|
2. **一背景**:第 1 殿秦广王 band(远景水墨 shader 层 + 中景剪影套件 3-5 件 + 近景地面)——背景层为**程序化 shader**(架构 §5.3),Max 侧只出剪影套件;
|
||||||
|
3. **一殿王**:秦广王 512×512。
|
||||||
|
|
||||||
|
完整 shader 堆栈在线后走 §10.1 十项冻结评审(签字 + 参数快照 + 截图入 `docs/engineering/`)。
|
||||||
|
|
||||||
|
## 4. `asset_manifest.csv`(用户侧维护,建议列)
|
||||||
|
|
||||||
|
`id, name, variant, size, has_left, has_right, frames, notes` —— 与 `generators.csv` 同 ID 口径; batch_render.ms 从本清单读批量出图。
|
||||||
|
|
||||||
|
## 5. W1 状态
|
||||||
|
|
||||||
|
- [x] 契约文档(本文件);
|
||||||
|
- [ ] `batch_render.ms`(用户,2-4h 一次性投入);
|
||||||
|
- [ ] `asset_manifest.csv`(用户,随首单位建立);
|
||||||
|
- [ ] look-dev 首单位 `gen_01_youhun` 初版(用户)+ 导入 Godot 可见(工程侧验收:sprite_row 路径即)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# pipeline/maxscript —— batch_render.ms + asset_manifest.csv 属用户(3ds Max 侧)任务;本目录在 W1 留空。契约见 ../README.md(架构 §7.3)。
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
; Engine configuration file.
|
||||||
|
; ============================================================================
|
||||||
|
; F《WRATH OF THE DEAD / 怨返》—— Godot 4.7.x 补丁线(当前钉 4.7.2)
|
||||||
|
; 架构基线:docs/architecture/2026-10-19-F-architecture.md §7.1(W1 落盘)
|
||||||
|
; 纪律:小版本补丁可升;跨 minor(4.8)上线后不追。
|
||||||
|
; 知识缺口声明:4.7.x 超出训练数据范围——本文件键名均为 Godot 4.x 稳定键,
|
||||||
|
; 首次用编辑器打开时若有迁移/配置提示,记录到 docs/engineering/ 后同步更新本
|
||||||
|
; 文件与 CLAUDE.md;不要静默跨 minor。
|
||||||
|
; ============================================================================
|
||||||
|
|
||||||
|
config_version=5
|
||||||
|
|
||||||
|
[application]
|
||||||
|
|
||||||
|
config/name="WRATH OF THE DEAD"
|
||||||
|
config/description="Wrongfully dead? Raise a ghost army and fight your way back to the living world."
|
||||||
|
run/main_scene=""
|
||||||
|
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||||
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
|
[display]
|
||||||
|
|
||||||
|
window/size/viewport_width=1280
|
||||||
|
window/size/viewport_height=720
|
||||||
|
window/size/window_width_override=1280
|
||||||
|
window/size/window_height_override=720
|
||||||
|
window/stretch/mode="canvas_items"
|
||||||
|
window/stretch/aspect="expand"
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
|
||||||
|
renderer/rendering_method="gl_compatibility"
|
||||||
|
renderer/rendering_method.mobile="gl_compatibility"
|
||||||
|
2d/snap/snap_2d_transforms_to_pixel=true
|
||||||
|
2d/snap/snap_2d_vertices_to_pixel=true
|
||||||
|
textures/vram_compression/import_etc2_astc=true
|
||||||
|
textures/vram_compression/import_s3tc_bptc=true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# src/core/economy —— 权威经济核心:EconomyState.tick() 10Hz 确定性;GeneratorDef/UpgradeDef Resource 数据行;MultiplierSet 三乘区;PrestigeModel;OfflineModel。W3 落盘(架构 §4.2)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# src/core/numbers —— BigNumber(double 封装 + ≤1e14 断言) / number_format(K/M/B/T + tabular)。W3 落盘(架构 §2.1/§4.2)。纪律:core 无 Node 依赖、零热路径分配。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# src/core/save —— save_game.gd(SAVE_VERSION + read/write)+ migration_registry.gd(版本链迁移,每迁移函数配单测)。W5 落盘(架构 §3.5/§6)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# src/core/sim —— 无头平衡模拟器:sim_main.gd(extends SceneTree)+ simulator.gd + strategies/ + gates.gd(14 门)。W2 骨架 / W4 首轮(架构 §10.3)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# src/game —— 编排层(唯一权威状态):GameState / DataRegistry / EventDirector / UnlockDirector / BossBattle。W2-W6 落盘(架构 §2.3/§4)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# src/platform —— SteamService:GodotSteam v4.23.1 唯一封装。game/ 禁止直调 Steam.*。W5 落盘(架构 §6.4)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# src/presentation —— 表现层:只读快照 + 发命令;UI 不持有游戏状态。stage/hud/panels/modals/post 子域。W2 起落盘(架构 §5)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# src/presentation/hud —— HUD 条(阴德/秒、十殿小地图、下一目标进度条)。W4-W6(架构 §7.1/GDD §7.1)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# src/presentation/modals —— 绘卷语言模态:冤魂申诉竖卷 / 殿王战过场 / 转世 / 敕封。W6(架构 §7.1)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# src/presentation/panels —— 右滑抽屉:升级树(5 页签)/ 成就殿堂 / 冤案卡册。W4-W6(架构 §7.1)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# src/presentation/post —— post_process_rect.gd:单趟 fused 后处理载体(posterize→LUT→纸纹→暗角;UI 层其上)。P1 过后 W3-W4 落盘(架构 §5.2)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# src/presentation/stage —— stage_root.tscn / camera_rig.gd / hall_band.gd / army_band.gd / boss_arena.gd。W3 起落盘(架构 §4.3)。
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SheetImporter CLI 入口(CI headless / 编辑器外批量导入)
|
||||||
|
# 用法:
|
||||||
|
# godot --headless --path . -s res://src/tools/import_main.gd
|
||||||
|
# godot --headless --path . -s res://src/tools/import_main.gd -- --validate-only
|
||||||
|
# 退出码:0 = 无违规;1 = 有违规或异常(CI 数据门按此判红)
|
||||||
|
#
|
||||||
|
# 依赖:使用前先跑 `godot --headless --path . --import`(生成全局类缓存,
|
||||||
|
# 本脚本才能解析全局 class_name SheetImporter)。参见 .github/workflows/ci.yml。
|
||||||
|
#
|
||||||
|
# 注意:4.7.x 超出训练数据——`-s` + SceneTree 入口模式为 4.x 稳定模式;
|
||||||
|
# 若 4.7.2 行为变化,按实际报错修正并记录到 docs/engineering/。
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var user_args := OS.get_cmdline_user_args()
|
||||||
|
var all_args := OS.get_cmdline_args()
|
||||||
|
var validate_only := user_args.has("--validate-only") or all_args.has("--validate-only")
|
||||||
|
var code := 0
|
||||||
|
if validate_only:
|
||||||
|
code = _run_validate()
|
||||||
|
else:
|
||||||
|
code = _run_import()
|
||||||
|
quit(code)
|
||||||
|
|
||||||
|
|
||||||
|
func _run_import() -> int:
|
||||||
|
print("=== SheetImporter import_all:sheets → build/*.json(schema 校验 + 差异化报告)===")
|
||||||
|
var report := SheetImporter.import_all()
|
||||||
|
for s in report.sheets:
|
||||||
|
print(s.summary())
|
||||||
|
print(report.summary())
|
||||||
|
if report.ok():
|
||||||
|
print("RESULT: PASS(%d 表 / %d 行 / 0 违规)" % [report.sheets.size(), report.row_count])
|
||||||
|
return 0
|
||||||
|
push_error("RESULT: FAIL(%d 条违规;schema 违规 = 构建红,禁止跳过坏行静默导入)" % report.violations.size())
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
func _run_validate() -> int:
|
||||||
|
print("=== SheetImporter validate:data/build 对 schema 合规性 ===")
|
||||||
|
var violations := SheetImporter.validate(SheetImporter.BUILD_DIR)
|
||||||
|
if violations.is_empty():
|
||||||
|
print("RESULT: PASS(build 目录全部合规)")
|
||||||
|
return 0
|
||||||
|
for v in violations:
|
||||||
|
push_error(v)
|
||||||
|
push_error("RESULT: FAIL(%d 条违规)" % violations.size())
|
||||||
|
return 1
|
||||||
@@ -0,0 +1,636 @@
|
|||||||
|
@tool
|
||||||
|
class_name SheetImporter
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SheetImporter v0.1 —— CSV → JSON 单向数据管线(架构 §3)
|
||||||
|
#
|
||||||
|
# 铁律(架构 §3.1/§3.2/§3.3):
|
||||||
|
# ① 单向流动:本工具只写 data/build/;游戏永不写 data/
|
||||||
|
# ② schema 违规 = 导入中止 + 违规清单(行号 + 列名 + 原因);禁止跳过坏行静默导入
|
||||||
|
# ③ 一行 = 一条数据;首列 id 稳定字符串 ID,永不重编号、永不复用(年更=加行)
|
||||||
|
# ④ 数值列禁千分位/禁单位后缀;文本列空 = 回退(schema required 已挡空值)
|
||||||
|
#
|
||||||
|
# 入口(headless/CI):src/tools/import_main.gd
|
||||||
|
# godot --headless --path . -s res://src/tools/import_main.gd
|
||||||
|
# godot --headless --path . -s res://src/tools/import_main.gd -- --validate-only
|
||||||
|
#
|
||||||
|
# 范围声明(W1 骨架):
|
||||||
|
# - CSV → data/build/*.json:完整可用
|
||||||
|
# - → *.tres:接口在(export_tres),W3 GeneratorDef 落盘后启用(ADR-3)
|
||||||
|
# - 跨表引用完整性(halls/upgrades 等目标表):W2 落盘后激活(schema 已预留说明)
|
||||||
|
#
|
||||||
|
# 知识缺口:Godot 4.7.x 超出训练数据。本文件只用 4.x 稳定 API
|
||||||
|
# (DirAccess / FileAccess / JSON / RegEx / String)。本机首跑若有 API
|
||||||
|
# 报错,按 4.7.2 实际报错修正,并记录到 docs/engineering/。
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
const SHEETS_DIR := "res://data/sheets"
|
||||||
|
const SCHEMA_DIR := "res://data/schema"
|
||||||
|
const BUILD_DIR := "res://data/build"
|
||||||
|
|
||||||
|
## 本工具实现的 JSON Schema 子集白名单;schema 出现白名单外关键词 = schema 违规
|
||||||
|
const SUPPORTED_KEYWORDS: Array[String] = [
|
||||||
|
"type", "properties", "required", "items", "enum", "pattern",
|
||||||
|
"minimum", "maximum", "minLength", "minItems", "maxItems",
|
||||||
|
"additionalProperties", "const",
|
||||||
|
]
|
||||||
|
const META_KEYWORDS: Array[String] = ["$schema", "$id", "title", "description"]
|
||||||
|
|
||||||
|
|
||||||
|
class ImportReport extends RefCounted:
|
||||||
|
var sheet: String = ""
|
||||||
|
var csv_path: String = ""
|
||||||
|
var json_path: String = ""
|
||||||
|
var row_count: int = 0
|
||||||
|
var added: int = 0
|
||||||
|
var changed: int = 0
|
||||||
|
var removed: int = 0
|
||||||
|
var violations: Array[String] = []
|
||||||
|
var notes: Array[String] = []
|
||||||
|
var tres_export: Array[String] = []
|
||||||
|
var sheets: Array = [] ## import_all 聚合时的子报告
|
||||||
|
|
||||||
|
func ok() -> bool:
|
||||||
|
return violations.is_empty()
|
||||||
|
|
||||||
|
func summary() -> String:
|
||||||
|
var lines := PackedStringArray()
|
||||||
|
var head := "表 %s:%d 行(+%d 新增 / ~%d 变更 / -%d 删除)→ %s" % [
|
||||||
|
sheet, row_count, added, changed, removed,
|
||||||
|
json_path.get_file() if json_path != "" else "未写盘"
|
||||||
|
]
|
||||||
|
lines.append(head)
|
||||||
|
for v in violations:
|
||||||
|
lines.append(" [违规] " + v)
|
||||||
|
for n in notes:
|
||||||
|
lines.append(" [备注] " + n)
|
||||||
|
for t in tres_export:
|
||||||
|
lines.append(" [tres] " + t)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 公开接口(架构 §3.3 签名级)
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
## 全量导入:sheets → build/*.json。返回聚合报告(新增/变更/违规行)。
|
||||||
|
static func import_all() -> ImportReport:
|
||||||
|
var report := ImportReport.new()
|
||||||
|
report.sheet = "ALL"
|
||||||
|
if not DirAccess.dir_exists_absolute(SHEETS_DIR):
|
||||||
|
report.violations.append("表目录不存在:%s" % SHEETS_DIR)
|
||||||
|
return report
|
||||||
|
var csv_files := DirAccess.get_files_at(SHEETS_DIR)
|
||||||
|
csv_files.sort()
|
||||||
|
if csv_files.is_empty():
|
||||||
|
report.violations.append("表目录为空:%s" % SHEETS_DIR)
|
||||||
|
return report
|
||||||
|
var done := 0
|
||||||
|
for file in csv_files:
|
||||||
|
if not file.ends_with(".csv"):
|
||||||
|
continue
|
||||||
|
var csv_path := "%s/%s" % [SHEETS_DIR, file]
|
||||||
|
var sheet_name := file.get_basename()
|
||||||
|
var schema_path := "%s/%s.schema.json" % [SCHEMA_DIR, sheet_name]
|
||||||
|
var schema_v: Variant = _load_json_file(schema_path)
|
||||||
|
if not (schema_v is Dictionary):
|
||||||
|
report.violations.append("%s 的 schema 缺失或解析失败(每张表必须有 data/schema/<表名>.schema.json)" % file)
|
||||||
|
continue
|
||||||
|
var result: Dictionary = import_sheet(csv_path, schema_v)
|
||||||
|
done += 1
|
||||||
|
report.sheets.append(result)
|
||||||
|
report.added += int(result.get("added", 0))
|
||||||
|
report.changed += int(result.get("changed", 0))
|
||||||
|
report.removed += int(result.get("removed", 0))
|
||||||
|
var tr: Array = result.get("typed_rows", [])
|
||||||
|
report.row_count += tr.size()
|
||||||
|
var sub_v: Array = result.get("violations", [])
|
||||||
|
for v in sub_v:
|
||||||
|
report.violations.append(str(v))
|
||||||
|
var sub_n: Array = result.get("notes", [])
|
||||||
|
for n in sub_n:
|
||||||
|
report.notes.append(str(n))
|
||||||
|
var sub_t: Array = result.get("tres_export", [])
|
||||||
|
for t in sub_t:
|
||||||
|
report.tres_export.append(str(t))
|
||||||
|
if done == 0:
|
||||||
|
report.violations.append("未找到任何 CSV 表:%s" % SHEETS_DIR)
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
## 单表导入;typed_rows 为按 schema 转换后的强类型字典数组(建表头顺序键序)。
|
||||||
|
static func import_sheet(csv_path: String, schema: Dictionary) -> Dictionary:
|
||||||
|
var out := {
|
||||||
|
"ok": false,
|
||||||
|
"sheet": _sheet_name(schema),
|
||||||
|
"csv_path": csv_path,
|
||||||
|
"json_path": "",
|
||||||
|
"columns": [],
|
||||||
|
"typed_rows": [],
|
||||||
|
"added": 0, "changed": 0, "removed": 0,
|
||||||
|
"violations": [],
|
||||||
|
"notes": [],
|
||||||
|
"tres_export": [],
|
||||||
|
}
|
||||||
|
# 0) schema 自检:关键词白名单
|
||||||
|
var kw_err := _check_schema_keywords(schema)
|
||||||
|
if kw_err != "":
|
||||||
|
out.violations.append(kw_err)
|
||||||
|
return out
|
||||||
|
# 1) 读 CSV
|
||||||
|
var text := FileAccess.get_file_as_string(csv_path)
|
||||||
|
if text == "":
|
||||||
|
out.violations.append("%s:文件为空或读取失败" % csv_path.get_file())
|
||||||
|
return out
|
||||||
|
var parsed: Dictionary = _parse_csv(text)
|
||||||
|
if parsed.has("error"):
|
||||||
|
out.violations.append(str(parsed.error))
|
||||||
|
return out
|
||||||
|
var header: Array = parsed.header
|
||||||
|
var rows: Array = parsed.rows
|
||||||
|
# 2) 表头 对 schema 列声明
|
||||||
|
var items := _rows_items(schema)
|
||||||
|
var row_props := _row_props(schema)
|
||||||
|
if row_props.is_empty() or items.is_empty():
|
||||||
|
out.violations.append("schema 缺少 rows.items.properties 定义(schema 结构错误)")
|
||||||
|
return out
|
||||||
|
var required_v: Variant = items.get("required", [])
|
||||||
|
var required: Array = required_v if required_v is Array else []
|
||||||
|
var missing := PackedStringArray()
|
||||||
|
var extra := PackedStringArray()
|
||||||
|
for col in required:
|
||||||
|
if not header.has(col):
|
||||||
|
missing.append(str(col))
|
||||||
|
for col in header:
|
||||||
|
if not row_props.has(col):
|
||||||
|
extra.append(str(col))
|
||||||
|
if not missing.is_empty():
|
||||||
|
out.violations.append("%s:表头缺少必填列 %s(CSV 规范:列名与 schema 严格一致)" % [csv_path.get_file(), str(missing)])
|
||||||
|
if not extra.is_empty():
|
||||||
|
out.violations.append("%s:表头出现 schema 未声明列 %s(新增列 = schema 版本升级,勿静默加列)" % [csv_path.get_file(), str(extra)])
|
||||||
|
if header.size() != required.size() and missing.is_empty() and extra.is_empty():
|
||||||
|
out.violations.append("%s:表头列数 %d ≠ schema 声明 %d(疑似重复列名)" % [csv_path.get_file(), header.size(), required.size()])
|
||||||
|
if not out.violations.is_empty():
|
||||||
|
return out
|
||||||
|
# 3) 行级转换 + 校验(每个错误带 CSV 行号 + 列名 + 原因)
|
||||||
|
var typed_rows: Array = []
|
||||||
|
var row_lines: Array = []
|
||||||
|
for r in rows:
|
||||||
|
var line_no: int = int(r.line)
|
||||||
|
var cells: Array = r.cells
|
||||||
|
if cells.size() != header.size():
|
||||||
|
out.violations.append("%s 第 %d 行:列数 %d ≠ 表头 %d(检查是否漏/多逗号,或单元格内逗号未用引号包裹)" % [csv_path.get_file(), line_no, cells.size(), header.size()])
|
||||||
|
continue
|
||||||
|
var row := {}
|
||||||
|
for i in header.size():
|
||||||
|
var col: String = header[i]
|
||||||
|
var raw: String = str(cells[i])
|
||||||
|
var prop: Dictionary = row_props.get(col, {})
|
||||||
|
var conv: Dictionary = _convert(raw, prop)
|
||||||
|
if conv.error != "":
|
||||||
|
out.violations.append("%s 第 %d 行 列 '%s':%s" % [csv_path.get_file(), line_no, col, str(conv.error)])
|
||||||
|
else:
|
||||||
|
var sc := _check_scalar(prop, conv.value)
|
||||||
|
if sc != "":
|
||||||
|
out.violations.append("%s 第 %d 行 列 '%s':%s(得到 '%s')" % [csv_path.get_file(), line_no, col, sc, str(conv.value)])
|
||||||
|
row[col] = conv.value
|
||||||
|
typed_rows.append(row)
|
||||||
|
row_lines.append(line_no)
|
||||||
|
# 4) 行级规则:唯一 ID(schema x-unique)+ 最少行数(GDD 锁定 12)
|
||||||
|
var unique_v: Variant = schema.get("x-unique", [])
|
||||||
|
if unique_v is Array:
|
||||||
|
for uc in unique_v:
|
||||||
|
var ucs := str(uc)
|
||||||
|
var seen := {}
|
||||||
|
var idx := 0
|
||||||
|
for row in typed_rows:
|
||||||
|
var idv := str(row.get(ucs, "<missing>"))
|
||||||
|
if seen.has(idv):
|
||||||
|
out.violations.append("%s 第 %d 行 列 '%s':重复值 '%s'(稳定 ID 永不重编号/复用;前次出现约第 %d 行)" % [
|
||||||
|
csv_path.get_file(), int(row_lines[idx]), ucs, idv, int(seen[idv])])
|
||||||
|
seen[idv] = int(row_lines[idx])
|
||||||
|
idx += 1
|
||||||
|
if items.has("minItems") and typed_rows.size() < int(items.minItems):
|
||||||
|
out.violations.append("%s:数据行 %d < schema minItems %d(GDD 锁定 12 班底;删行 = 设计动作,走 ADR 流程)" % [csv_path.get_file(), typed_rows.size(), int(items.minItems)])
|
||||||
|
if not out.violations.is_empty():
|
||||||
|
return out # 失败策略:有违规不写 build,导入中止
|
||||||
|
# 5) 组装 build 文档(确定性 canonical JSON)+ 写盘
|
||||||
|
var doc := {
|
||||||
|
"sheet": _sheet_name(schema),
|
||||||
|
"schema_version": _schema_version(schema),
|
||||||
|
"columns": header,
|
||||||
|
"rows": typed_rows,
|
||||||
|
}
|
||||||
|
var json_path := "%s/%s.json" % [BUILD_DIR, _sheet_name(schema)]
|
||||||
|
# 先读旧 build(差异化对比必须在覆盖前读)
|
||||||
|
var prev: Variant = _load_json_file(json_path)
|
||||||
|
var prev_rows: Array = []
|
||||||
|
if prev is Dictionary:
|
||||||
|
prev_rows = prev.get("rows", [])
|
||||||
|
DirAccess.make_dir_recursive_absolute(BUILD_DIR)
|
||||||
|
var f := FileAccess.open(json_path, FileAccess.WRITE)
|
||||||
|
if f == null:
|
||||||
|
out.violations.append("写入失败:%s(%s)" % [json_path, error_string(FileAccess.get_open_error())])
|
||||||
|
return out
|
||||||
|
f.store_string(_canonical_json(doc, "") + "\n")
|
||||||
|
f.close()
|
||||||
|
out.json_path = json_path
|
||||||
|
out.columns = header
|
||||||
|
out.typed_rows = typed_rows
|
||||||
|
# 6) 与旧 build 对比:新增/变更/删除(按稳定 ID)
|
||||||
|
if prev_rows.is_empty():
|
||||||
|
out.added = typed_rows.size()
|
||||||
|
out.notes.append("首次生成 %s" % json_path.get_file())
|
||||||
|
else:
|
||||||
|
var new_by_id := {}
|
||||||
|
for row in typed_rows:
|
||||||
|
new_by_id[str(row.get("id", ""))] = row
|
||||||
|
var old_by_id := {}
|
||||||
|
for row in prev_rows:
|
||||||
|
old_by_id[str(row.get("id", ""))] = row
|
||||||
|
for id in new_by_id:
|
||||||
|
if not old_by_id.has(id):
|
||||||
|
out.added += 1
|
||||||
|
elif _canonical_json(new_by_id[id], "") != _canonical_json(old_by_id[id], ""):
|
||||||
|
out.changed += 1
|
||||||
|
for id in old_by_id:
|
||||||
|
if not new_by_id.has(id):
|
||||||
|
out.removed += 1
|
||||||
|
# 7) .tres 导出(v0.1 stub:接口在,W3 启用——见 export_tres)
|
||||||
|
var tres: Array[String] = export_tres(json_path, BUILD_DIR + "/tres")
|
||||||
|
out.tres_export = tres
|
||||||
|
out.ok = true
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## 校验 build/*.json 对 schema 的合规性;违规列表为空 = 通过。
|
||||||
|
## 行号约定:rows[i] 报为"第 i+2 行"(对齐 CSV 物理行,表头 = 第 1 行)。
|
||||||
|
static func validate(json_dir: String) -> Array[String]:
|
||||||
|
var violations: Array[String] = []
|
||||||
|
if not DirAccess.dir_exists_absolute(json_dir):
|
||||||
|
violations.append("build 目录不存在:%s(先运行 import_all)" % json_dir)
|
||||||
|
return violations
|
||||||
|
var schema_files := DirAccess.get_files_at(SCHEMA_DIR)
|
||||||
|
schema_files.sort()
|
||||||
|
for sf in schema_files:
|
||||||
|
if not sf.ends_with(".schema.json"):
|
||||||
|
continue
|
||||||
|
var sheet_name := sf.substr(0, sf.length() - ".schema.json".length())
|
||||||
|
var schema_v: Variant = _load_json_file("%s/%s" % [SCHEMA_DIR, sf])
|
||||||
|
if not (schema_v is Dictionary):
|
||||||
|
violations.append("schema 解析失败:%s" % sf)
|
||||||
|
continue
|
||||||
|
var json_path := "%s/%s.json" % [json_dir, sheet_name]
|
||||||
|
var doc: Variant = _load_json_file(json_path)
|
||||||
|
if doc == null:
|
||||||
|
violations.append("缺少 build 产物:%s(运行 import_all 生成)" % json_path)
|
||||||
|
continue
|
||||||
|
var where := "%s → %s" % [sheet_name, json_path.get_file()]
|
||||||
|
_validate_value(doc, schema_v, where, "", violations, -1)
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
## ★ ADR-3:唯一允许的逐资产代码 = sprite 路径(已是 CSV 列)。
|
||||||
|
## .tres 生成 v0.1 = stub:接口定义在此,W3 GeneratorDef 数据类落盘后启用。
|
||||||
|
static func export_tres(build_json_path: String, out_dir: String) -> Array[String]:
|
||||||
|
return ["[stub] %s → %s/*.tres:W3 GeneratorDef 落盘后启用(数据行 → Resource)" % [build_json_path.get_file(), out_dir]]
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# CSV 解析(RFC4180 简化版:引号包裹 + "" 转义;v0 不支持内嵌换行)
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static func _parse_csv(text: String) -> Dictionary:
|
||||||
|
if text.begins_with("\ufeff"):
|
||||||
|
text = text.substr(1)
|
||||||
|
var header: Array = []
|
||||||
|
var rows: Array = []
|
||||||
|
var cells: Array = []
|
||||||
|
var cell := ""
|
||||||
|
var quoted := false
|
||||||
|
var in_quotes := false
|
||||||
|
var line := 1
|
||||||
|
var i := 0
|
||||||
|
var n := text.length()
|
||||||
|
while i < n:
|
||||||
|
var c := text[i]
|
||||||
|
if in_quotes:
|
||||||
|
if c == '"':
|
||||||
|
if i + 1 < n and text[i + 1] == '"':
|
||||||
|
cell += '"'
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
in_quotes = false
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
cell += c
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if c == '"' and cell == "":
|
||||||
|
in_quotes = true
|
||||||
|
quoted = true
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if c == ",":
|
||||||
|
cells.append(_cell_value(cell, quoted))
|
||||||
|
cell = ""
|
||||||
|
quoted = false
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if c == "\r":
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if c == "\n":
|
||||||
|
cells.append(_cell_value(cell, quoted))
|
||||||
|
_push_line(cells, line, header, rows)
|
||||||
|
cells = []
|
||||||
|
cell = ""
|
||||||
|
quoted = false
|
||||||
|
line += 1
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
cell += c
|
||||||
|
i += 1
|
||||||
|
if in_quotes:
|
||||||
|
return {"error": "CSV 引号未闭合(v0 规范:单元格内双引号以 \"\" 转义;不支持内嵌换行)"}
|
||||||
|
if cell != "" or quoted or not cells.is_empty():
|
||||||
|
cells.append(_cell_value(cell, quoted))
|
||||||
|
_push_line(cells, line, header, rows)
|
||||||
|
return {"header": header, "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
static func _cell_value(cell: String, was_quoted: bool) -> String:
|
||||||
|
# v0 简化:非引号单元格去首尾空白;引号单元格原样保留(本项目无首尾空白敏感数据)
|
||||||
|
return cell if was_quoted else cell.strip_edges()
|
||||||
|
|
||||||
|
|
||||||
|
static func _push_line(cells: Array, line_no: int, header: Array, rows: Array) -> void:
|
||||||
|
if cells.size() == 1 and str(cells[0]).strip_edges() == "":
|
||||||
|
return # 跳过空行
|
||||||
|
if header.is_empty():
|
||||||
|
for c in cells:
|
||||||
|
if not (c is String):
|
||||||
|
return
|
||||||
|
header.append_array(cells)
|
||||||
|
return
|
||||||
|
rows.append({"line": line_no, "cells": cells})
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 类型转换 / 标量校验
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static func _convert(raw: String, prop: Dictionary) -> Dictionary:
|
||||||
|
var t := str(prop.get("type", "string"))
|
||||||
|
match t:
|
||||||
|
"string":
|
||||||
|
return {"value": raw, "error": ""}
|
||||||
|
"integer":
|
||||||
|
if _re_match("^[+-]?[0-9]+$", raw) == false:
|
||||||
|
return {"value": null, "error": "期望 integer,得到 '%s'(数值列禁千分位/禁单位后缀;示例:1200000)" % raw}
|
||||||
|
return {"value": int(raw), "error": ""}
|
||||||
|
"number":
|
||||||
|
if _re_match("^[+-]?([0-9]+(\\.[0-9]+)?|\\.[0-9]+)$", raw) == false:
|
||||||
|
return {"value": null, "error": "期望 number,得到 '%s'(数值列禁千分位与 K/M/B/T 后缀;示例:1.2K → 1200)" % raw}
|
||||||
|
if raw.contains("."):
|
||||||
|
return {"value": float(raw), "error": ""}
|
||||||
|
return {"value": int(raw), "error": ""}
|
||||||
|
"boolean":
|
||||||
|
var low := raw.to_lower()
|
||||||
|
if low in ["true", "1", "yes"]:
|
||||||
|
return {"value": true, "error": ""}
|
||||||
|
if low in ["false", "0", "no"]:
|
||||||
|
return {"value": false, "error": ""}
|
||||||
|
return {"value": null, "error": "期望 boolean,得到 '%s'(true/false)" % raw}
|
||||||
|
_:
|
||||||
|
return {"value": null, "error": "schema 声明了本工具不支持的类型 '%s'" % t}
|
||||||
|
return {"value": raw, "error": ""}
|
||||||
|
|
||||||
|
|
||||||
|
static func _re_match(pattern: String, text: String) -> bool:
|
||||||
|
var re := RegEx.new()
|
||||||
|
if re.compile(pattern) != OK:
|
||||||
|
push_warning("RegEx 编译失败:%s" % pattern)
|
||||||
|
return false
|
||||||
|
return re.search(text) != null
|
||||||
|
|
||||||
|
|
||||||
|
static func _check_scalar(prop: Dictionary, value: Variant) -> String:
|
||||||
|
if prop.has("const"):
|
||||||
|
var cv: Variant = prop.const
|
||||||
|
if value != cv:
|
||||||
|
return "必须等于常量 %s" % str(cv)
|
||||||
|
if prop.has("enum"):
|
||||||
|
var allowed: Variant = prop.enum
|
||||||
|
if allowed is Array and not allowed.has(value):
|
||||||
|
return "取值必须为 %s 之一" % str(allowed)
|
||||||
|
if prop.has("pattern") and value is String:
|
||||||
|
if _re_match(str(prop.pattern), value) == false:
|
||||||
|
return "不匹配模式 %s" % str(prop.pattern)
|
||||||
|
if value is int or value is float:
|
||||||
|
if prop.has("minimum") and float(value) < float(prop.minimum):
|
||||||
|
return "小于下限 %s" % str(prop.minimum)
|
||||||
|
if prop.has("maximum") and float(value) > float(prop.maximum):
|
||||||
|
return "大于上限 %s" % str(prop.maximum)
|
||||||
|
if prop.has("minLength") and value is String and value.length() < int(prop.minLength):
|
||||||
|
return "长度 %d < minLength %d" % [value.length(), int(prop.minLength)]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
static func _type_ok(value: Variant, t: String) -> bool:
|
||||||
|
match t:
|
||||||
|
"string":
|
||||||
|
return value is String
|
||||||
|
"integer":
|
||||||
|
if value is int:
|
||||||
|
return true
|
||||||
|
if value is float:
|
||||||
|
return is_finite(value) and is_equal_approx(value, float(int(value)))
|
||||||
|
return false
|
||||||
|
"number":
|
||||||
|
return value is int or value is float
|
||||||
|
"boolean":
|
||||||
|
return value is bool
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 文档校验(build/*.json 对 schema;schema 子集白名单可查)
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static func _validate_value(value: Variant, node: Dictionary, where: String, path: String, violations: Array[String], row_index: int) -> void:
|
||||||
|
var loc := _loc(where, path, row_index)
|
||||||
|
var declared := str(node.get("type", ""))
|
||||||
|
if declared != "" and not _type_ok(value, declared):
|
||||||
|
violations.append("%s类型期望 %s,得到 %s('%s')" % [loc, declared, type_string(typeof(value)), _brief(value)])
|
||||||
|
return
|
||||||
|
if not (value is Dictionary or value is Array):
|
||||||
|
var sc := _check_scalar(node, value)
|
||||||
|
if sc != "":
|
||||||
|
violations.append("%s%s(得到 '%s')" % [loc, sc, _brief(value)])
|
||||||
|
return
|
||||||
|
if value is Dictionary:
|
||||||
|
var props_v: Variant = node.get("properties", {})
|
||||||
|
var props: Dictionary = props_v if props_v is Dictionary else {}
|
||||||
|
var req_v: Variant = node.get("required", [])
|
||||||
|
if req_v is Array:
|
||||||
|
for rk in req_v:
|
||||||
|
if not value.has(rk):
|
||||||
|
violations.append("%s缺少必填列 '%s'" % [loc, str(rk)])
|
||||||
|
var add_v: Variant = node.get("additionalProperties", true)
|
||||||
|
var add_allowed := true
|
||||||
|
if add_v is bool:
|
||||||
|
add_allowed = add_v
|
||||||
|
for k in value.keys():
|
||||||
|
var ks := str(k)
|
||||||
|
if not props.has(ks):
|
||||||
|
if not add_allowed:
|
||||||
|
violations.append("%s出现 schema 未声明的列 '%s'(新增列 = schema 版本升级,勿静默加列)" % [loc, ks])
|
||||||
|
continue
|
||||||
|
_validate_value(value[k], props[ks], where, _join_path(path, ks), violations, row_index)
|
||||||
|
return
|
||||||
|
# value is Array
|
||||||
|
if node.has("minItems") and value.size() < int(node.minItems):
|
||||||
|
violations.append("%s数量 %d < minItems %d" % [loc, value.size(), int(node.minItems)])
|
||||||
|
if node.has("maxItems") and value.size() > int(node.maxItems):
|
||||||
|
violations.append("%s数量 %d > maxItems %d" % [loc, value.size(), int(node.maxItems)])
|
||||||
|
var items_v: Variant = node.get("items", null)
|
||||||
|
if items_v is Dictionary:
|
||||||
|
var idx := 0
|
||||||
|
for item in value:
|
||||||
|
var next_row := idx if path == "rows" else row_index
|
||||||
|
_validate_value(item, items_v, where, "%s[%d]" % [path, idx], violations, next_row)
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
|
||||||
|
static func _check_schema_keywords(schema: Dictionary) -> String:
|
||||||
|
var bad := PackedStringArray()
|
||||||
|
_walk_schema(schema, bad)
|
||||||
|
if bad.is_empty():
|
||||||
|
return ""
|
||||||
|
return "schema 使用了本工具未支持的关键词:%s(支持集:%s)" % [str(bad), str(SUPPORTED_KEYWORDS)]
|
||||||
|
|
||||||
|
|
||||||
|
static func _walk_schema(node: Dictionary, bad: PackedStringArray) -> void:
|
||||||
|
for k in node.keys():
|
||||||
|
var ks := str(k)
|
||||||
|
if not ks.begins_with("x-") and not SUPPORTED_KEYWORDS.has(ks) and not META_KEYWORDS.has(ks):
|
||||||
|
bad.append(ks)
|
||||||
|
for key in ["properties", "items", "additionalProperties"]:
|
||||||
|
if node.has(key):
|
||||||
|
var v: Variant = node[key]
|
||||||
|
if v is Dictionary:
|
||||||
|
_walk_schema(v, bad)
|
||||||
|
elif v is Array:
|
||||||
|
for item in v:
|
||||||
|
if item is Dictionary:
|
||||||
|
_walk_schema(item, bad)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 确定性 canonical JSON 序列化(build 产物字节稳定 = CI 漂移门可字节比对)
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static func _canonical_json(value: Variant, indent: String) -> String:
|
||||||
|
if value is Dictionary:
|
||||||
|
if value.is_empty():
|
||||||
|
return "{}"
|
||||||
|
var lines := PackedStringArray()
|
||||||
|
for k in value.keys():
|
||||||
|
lines.append("%s\t%s: %s" % [indent, JSON.stringify(str(k)), _canonical_json(value[k], indent + "\t")])
|
||||||
|
return "{\n" + ",\n".join(lines) + "\n" + indent + "}"
|
||||||
|
if value is Array:
|
||||||
|
if value.is_empty():
|
||||||
|
return "[]"
|
||||||
|
var lines := PackedStringArray()
|
||||||
|
for v in value:
|
||||||
|
lines.append("%s\t%s" % [indent, _canonical_json(v, indent + "\t")])
|
||||||
|
return "[\n" + ",\n".join(lines) + "\n" + indent + "]"
|
||||||
|
if value is String:
|
||||||
|
return JSON.stringify(value)
|
||||||
|
if value is bool:
|
||||||
|
return "true" if value else "false"
|
||||||
|
if value is int:
|
||||||
|
return str(value)
|
||||||
|
if value is float:
|
||||||
|
if is_finite(value) and is_equal_approx(value, float(int(value))) and absf(value) < 1e15:
|
||||||
|
return str(int(value))
|
||||||
|
return str(value)
|
||||||
|
return JSON.stringify(str(value))
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 小工具
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static func _rows_items(schema: Dictionary) -> Dictionary:
|
||||||
|
var props_v: Variant = schema.get("properties", {})
|
||||||
|
if not (props_v is Dictionary):
|
||||||
|
return {}
|
||||||
|
var rows_v: Variant = props_v.get("rows", {})
|
||||||
|
if not (rows_v is Dictionary):
|
||||||
|
return {}
|
||||||
|
var items_v: Variant = rows_v.get("items", {})
|
||||||
|
return items_v if items_v is Dictionary else {}
|
||||||
|
|
||||||
|
|
||||||
|
static func _row_props(schema: Dictionary) -> Dictionary:
|
||||||
|
var items := _rows_items(schema)
|
||||||
|
var props_v: Variant = items.get("properties", {})
|
||||||
|
return props_v if props_v is Dictionary else {}
|
||||||
|
|
||||||
|
|
||||||
|
static func _sheet_name(schema: Dictionary) -> String:
|
||||||
|
var props_v: Variant = schema.get("properties", {})
|
||||||
|
if props_v is Dictionary:
|
||||||
|
var sheet_v: Variant = props_v.get("sheet", {})
|
||||||
|
if sheet_v is Dictionary and sheet_v.has("const"):
|
||||||
|
return str(sheet_v.const)
|
||||||
|
var title := str(schema.get("title", "unknown"))
|
||||||
|
return title.split(" ")[0] if title != "" else "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
static func _schema_version(schema: Dictionary) -> int:
|
||||||
|
var props_v: Variant = schema.get("properties", {})
|
||||||
|
if props_v is Dictionary:
|
||||||
|
var ver_v: Variant = props_v.get("schema_version", {})
|
||||||
|
if ver_v is Dictionary and ver_v.has("const"):
|
||||||
|
return int(ver_v.const)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
static func _load_json_file(path: String) -> Variant:
|
||||||
|
if not FileAccess.file_exists(path):
|
||||||
|
return null
|
||||||
|
var text := FileAccess.get_file_as_string(path)
|
||||||
|
if text == "":
|
||||||
|
return null
|
||||||
|
var data: Variant = JSON.parse_string(text)
|
||||||
|
if data == null:
|
||||||
|
push_warning("JSON 解析失败:%s" % path)
|
||||||
|
return null
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
static func _loc(where: String, path: String, row_index: int) -> String:
|
||||||
|
if row_index >= 0:
|
||||||
|
if path == "":
|
||||||
|
return "%s 第 %d 行:" % [where, row_index + 2]
|
||||||
|
return "%s 第 %d 行 %s:" % [where, row_index + 2, path]
|
||||||
|
if path == "":
|
||||||
|
return "%s:" % where
|
||||||
|
return "%s %s:" % [where, path]
|
||||||
|
|
||||||
|
|
||||||
|
static func _join_path(path: String, key: String) -> String:
|
||||||
|
if path == "":
|
||||||
|
return key
|
||||||
|
return "%s.%s" % [path, key]
|
||||||
|
|
||||||
|
|
||||||
|
static func _brief(v: Variant) -> String:
|
||||||
|
var s := str(v)
|
||||||
|
if s.length() > 32:
|
||||||
|
s = s.substr(0, 32) + "…"
|
||||||
|
return s
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# 探针 P1 —— SCREEN_TEXTURE / CanvasLayer 排序语义
|
||||||
|
|
||||||
|
验证 Godot 4.7 后处理三问(架构 §11 P1 / §5.2 ADR-2):
|
||||||
|
① 背景色是否含入 SCREEN_TEXTURE 采样;② CanvasLayer 排序(L0 世界 < L1 后处理 < L2 UI);③ UI 层是否被后处理污染。
|
||||||
|
|
||||||
|
**完整实测手册(必读,约 30 分钟)**:
|
||||||
|
`docs/engineering/2026-10-19-f-w1-probe-p1-manual.md`(本仓库外的工作区文档树)
|
||||||
|
|
||||||
|
**跑法**:编辑器打开 `tests/probe_p1_screen_texture/probe_p1.tscn` → F6 运行当前场景。
|
||||||
|
键位:`1` 直通 / `2` 反色 / `3` 红染 / `4` 纯绿 / `V` 显隐后处理层 / `S` 截图到 `user://` / `Esc` 退出。
|
||||||
|
控制台会打印引擎版本 / rendering_method / 显卡适配器——回填结论时连同截图一起归档。
|
||||||
|
|
||||||
|
**结论回填**:`docs/engineering/probes/P1-result.md`
|
||||||
|
**不过的回退**:SubViewport + BackBufferCopy(架构预算 +2-4h)。
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 探针 P1 —— Godot 4.7 hint_screen_texture / SCREEN_TEXTURE 语义
|
||||||
|
# 架构依据:§11 P1 / §5.2(ADR-2 单趟 fused 后处理的 CanvasLayer 拓扑)
|
||||||
|
# 实测手册:docs/engineering/2026-10-19-f-w1-probe-p1-manual.md
|
||||||
|
# 结论回填:docs/engineering/probes/P1-result.md
|
||||||
|
#
|
||||||
|
# 三问:
|
||||||
|
# ① 背景/世界色是否含入 SCREEN_TEXTURE 采样(后处理 L1 能否看见 L0)
|
||||||
|
# ② CanvasLayer 排序语义(L1 后处理在 L0 世界之上、L2 UI 之下)
|
||||||
|
# ③ UI 层是否被后处理污染(UI 保持 crisp、不被 shader 调制)
|
||||||
|
#
|
||||||
|
# 场景在 _ready() 中代码化搭建(不手写 .tscn 降低出错面)。
|
||||||
|
# 键位:1 直通 | 2 反色 | 3 红染 | 4 纯绿 | V 显/隐后处理层 | S 截图 user:// | Esc 退出
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
enum PostMode { PASSTHROUGH, INVERT, RED_TINT, SOLID_GREEN }
|
||||||
|
|
||||||
|
var _post: ColorRect
|
||||||
|
var _post_visible := true
|
||||||
|
var _mode: int = PostMode.PASSTHROUGH
|
||||||
|
var _mat: ShaderMaterial
|
||||||
|
var _hud: Label
|
||||||
|
var _shot_n := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_build_world()
|
||||||
|
_build_post()
|
||||||
|
_build_ui()
|
||||||
|
_print_environment()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_world() -> void:
|
||||||
|
var world := CanvasLayer.new()
|
||||||
|
world.name = "L0_World"
|
||||||
|
world.layer = 0
|
||||||
|
add_child(world)
|
||||||
|
|
||||||
|
var bg := ColorRect.new()
|
||||||
|
bg.name = "Background"
|
||||||
|
bg.color = Color("#23384F") # 已知底色:后处理里能看见它 = 世界含入采样
|
||||||
|
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
world.add_child(bg)
|
||||||
|
|
||||||
|
var marker := ColorRect.new()
|
||||||
|
marker.name = "WorldMarker"
|
||||||
|
marker.color = Color("#D08A2E")
|
||||||
|
marker.position = Vector2(400, 240)
|
||||||
|
marker.size = Vector2(480, 240)
|
||||||
|
world.add_child(marker)
|
||||||
|
|
||||||
|
var world_label := Label.new()
|
||||||
|
world_label.name = "WorldLabel"
|
||||||
|
world_label.text = "L0 World: bg #23384F + marker #D08A2E"
|
||||||
|
world_label.position = Vector2(24, 180)
|
||||||
|
world_label.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
world.add_child(world_label)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_post() -> void:
|
||||||
|
var post_layer := CanvasLayer.new()
|
||||||
|
post_layer.name = "L1_PostProcess"
|
||||||
|
post_layer.layer = 1
|
||||||
|
add_child(post_layer)
|
||||||
|
|
||||||
|
var sh := load("res://tests/probe_p1_screen_texture/probe_p1.gdshader") as Shader
|
||||||
|
_mat = ShaderMaterial.new()
|
||||||
|
_mat.shader = sh
|
||||||
|
_mat.set_shader_parameter("mode", PostMode.PASSTHROUGH)
|
||||||
|
|
||||||
|
_post = ColorRect.new()
|
||||||
|
_post.name = "PostProcessRect"
|
||||||
|
_post.material = _mat
|
||||||
|
_post.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_post.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
post_layer.add_child(_post)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
var ui_layer := CanvasLayer.new()
|
||||||
|
ui_layer.name = "L2_UI"
|
||||||
|
ui_layer.layer = 2
|
||||||
|
add_child(ui_layer)
|
||||||
|
|
||||||
|
var root := Control.new()
|
||||||
|
root.name = "UIRoot"
|
||||||
|
root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
ui_layer.add_child(root)
|
||||||
|
|
||||||
|
# UI 样本:四色板 + 一块纯白。被后处理污染 = 颜色变了/出现着色/纸纹
|
||||||
|
var chip_colors := [
|
||||||
|
Color("#1A1614"), Color("#EDE6D8"), Color("#B03A2E"),
|
||||||
|
Color("#3A5A6B"), Color("#FFFFFF"),
|
||||||
|
]
|
||||||
|
var chip_names := ["墨 #1A1614", "骨白 #EDE6D8", "朱砂 #B03A2E", "靛 #3A5A6B", "纯白 #FFFFFF"]
|
||||||
|
for i in chip_colors.size():
|
||||||
|
var chip := ColorRect.new()
|
||||||
|
chip.name = "Chip%d" % i
|
||||||
|
chip.color = chip_colors[i]
|
||||||
|
chip.size = Vector2(200, 40)
|
||||||
|
chip.position = Vector2(960, 48 + i * 56)
|
||||||
|
root.add_child(chip)
|
||||||
|
var lbl := Label.new()
|
||||||
|
lbl.name = "ChipLabel%d" % i
|
||||||
|
lbl.text = chip_names[i]
|
||||||
|
lbl.position = Vector2(1010, 54 + i * 56)
|
||||||
|
if chip_colors[i].get_luminance() > 0.5:
|
||||||
|
lbl.add_theme_color_override("font_color", Color.BLACK)
|
||||||
|
else:
|
||||||
|
lbl.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
root.add_child(lbl)
|
||||||
|
|
||||||
|
_hud = Label.new()
|
||||||
|
_hud.name = "HUDHelp"
|
||||||
|
_hud.text = _help_text()
|
||||||
|
_hud.position = Vector2(24, 648)
|
||||||
|
_hud.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
root.add_child(_hud)
|
||||||
|
|
||||||
|
|
||||||
|
func _help_text() -> String:
|
||||||
|
var mode_names := ["PASS直通", "INVERT反色", "RED红染", "GREEN纯绿"]
|
||||||
|
var mode_name := mode_names[_mode] if _mode >= 0 and _mode < mode_names.size() else "?"
|
||||||
|
return "探针 P1 | 1 直通 2 反色 3 红染 4 纯绿 | V 显/隐后处理层(当前:%s)| S 截图到 user:// | Esc 退出 | 当前 mode=%s" % [
|
||||||
|
"显示" if _post_visible else "隐藏", mode_name]
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
var key_event := event as InputEventKey
|
||||||
|
if key_event != null and key_event.pressed and not key_event.echo:
|
||||||
|
match key_event.keycode:
|
||||||
|
KEY_1:
|
||||||
|
_set_mode(PostMode.PASSTHROUGH)
|
||||||
|
KEY_2:
|
||||||
|
_set_mode(PostMode.INVERT)
|
||||||
|
KEY_3:
|
||||||
|
_set_mode(PostMode.RED_TINT)
|
||||||
|
KEY_4:
|
||||||
|
_set_mode(PostMode.SOLID_GREEN)
|
||||||
|
KEY_V:
|
||||||
|
_post_visible = not _post_visible
|
||||||
|
_post.visible = _post_visible
|
||||||
|
_hud.text = _help_text()
|
||||||
|
KEY_S:
|
||||||
|
_screenshot()
|
||||||
|
KEY_ESCAPE:
|
||||||
|
get_tree().quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_mode(m: int) -> void:
|
||||||
|
_mode = m
|
||||||
|
_mat.set_shader_parameter("mode", m)
|
||||||
|
_hud.text = _help_text()
|
||||||
|
var mode_names := ["PASS直通", "INVERT反色", "RED红染", "GREEN纯绿"]
|
||||||
|
print("P1 mode → %d(%s)" % [m, mode_names[m] if m >= 0 and m < mode_names.size() else "?"])
|
||||||
|
|
||||||
|
|
||||||
|
func _screenshot() -> void:
|
||||||
|
var tex := get_viewport().get_texture()
|
||||||
|
if tex == null:
|
||||||
|
push_warning("P1: viewport texture 为空,回退:请用系统截图(Win+Shift+S)")
|
||||||
|
return
|
||||||
|
var img := tex.get_image()
|
||||||
|
if img == null:
|
||||||
|
push_warning("P1: get_image() 失败,回退:请用系统截图(Win+Shift+S)")
|
||||||
|
return
|
||||||
|
var path := "user://p1_mode_%d_%02d.png" % [_mode, _shot_n]
|
||||||
|
_shot_n += 1
|
||||||
|
var err := img.save_png(path)
|
||||||
|
print("P1 screenshot → ", path, " err=", err)
|
||||||
|
|
||||||
|
|
||||||
|
func _print_environment() -> void:
|
||||||
|
var v := Engine.get_version_info()
|
||||||
|
print("=== P1 probe 环境(截图请连同此段一起归档)===")
|
||||||
|
print("godot: %d.%d.%d %s" % [int(v.major), int(v.minor), int(v.patch), str(v.status)])
|
||||||
|
print("rendering_method: ", ProjectSettings.get_setting("rendering/renderer/rendering_method"))
|
||||||
|
print("adapter: ", RenderingServer.get_video_adapter_name())
|
||||||
|
print("viewport: ", get_viewport().get_visible_rect().size)
|
||||||
|
print("stretch: ", ProjectSettings.get_setting("display/window/stretch/mode"), " / ", ProjectSettings.get_setting("display/window/stretch/aspect"))
|
||||||
|
print("=== 键位:1 直通 | 2 反色 | 3 红染 | 4 纯绿 | V 显隐后处理 | S 截图 | Esc 退出 ===")
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
shader_type canvas_item;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 探针 P1 shader —— SCREEN_TEXTURE 语义(架构 §11 P1 / §5.2 ADR-2)
|
||||||
|
// 由 probe_p1.gd 以 ShaderMaterial 挂到 L1 全屏 ColorRect 上。
|
||||||
|
// mode(uniform int):
|
||||||
|
// 0 直通 —— 验证"背景/世界是否含入 SCREEN_TEXTURE 采样"
|
||||||
|
// 1 反色 —— 放大世界内容差异,便于肉眼确认采样到了 L0
|
||||||
|
// 2 红色染色 —— L1 全屏染色:验证 L2 UI 是否被后处理污染(UI 应保持原色 crisp)
|
||||||
|
// 3 纯绿 —— 顺序验证:整屏变绿 = CanvasLayer 排序 L0<L1<L2 符合预期
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
uniform int mode = 0;
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec4 screen = texture(SCREEN_TEXTURE, SCREEN_UV);
|
||||||
|
if (mode == 0) {
|
||||||
|
COLOR = screen;
|
||||||
|
} else if (mode == 1) {
|
||||||
|
COLOR = vec4(1.0 - screen.rgb, screen.a);
|
||||||
|
} else if (mode == 2) {
|
||||||
|
COLOR = vec4(screen.r, 0.0, 0.0, 1.0);
|
||||||
|
} else {
|
||||||
|
COLOR = vec4(0.0, 1.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/probe_p1_screen_texture/probe_p1.gd" id="1"]
|
||||||
|
|
||||||
|
[node type="Node" name="ProbeP1ScreenTexture"]
|
||||||
|
script = ExtResource("1")
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# 探针 P3 —— NoiseTexture2D / FastNoiseLite 平铺语义
|
||||||
|
|
||||||
|
验证 `seamless = true` 是否真正可平铺(架构 §11 P3;纸纹 pass 的 tileable 前提,管线报告 §6.3)。
|
||||||
|
屏上为 `seamless = true/false` 两个 2×2 平铺网格(红线 = tile 边界);按 `S` 生成边界差图 PNG(`user://p3_diff_*.png`)并打印 wrap/grad 比统计。
|
||||||
|
|
||||||
|
**完整实测手册(必读,约 30 分钟)**:
|
||||||
|
`docs/engineering/2026-10-19-f-w1-probe-p3-manual.md`(本仓库外的工作区文档树)
|
||||||
|
|
||||||
|
**跑法**:编辑器打开 `tests/probe_p3_noise_tiling/probe_p3.tscn` → F6 运行当前场景。
|
||||||
|
键位:`S` 生成检查图 + 打印统计 / `Esc` 退出。
|
||||||
|
|
||||||
|
**结论回填**:`docs/engineering/probes/P3-result.md`
|
||||||
|
**不过的回退**:脚本预烘焙无缝纹理(用 Image 环绕混合生成 256² 纸纹源,不进 NoiseTexture2D)。
|
||||||
|
|
||||||
|
**知识缺口标注**:`FastNoiseLite.TYPE_SIMPLEX` / `FRACTAL_FBM` / `NoiseTexture2D.seamless` / `seamless_blend_ratio`
|
||||||
|
为 4.x 稳定名;若 4.7.2 打开报错,按实际属性名修正并记录到 `docs/engineering/`。
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 探针 P3 —— NoiseTexture2D / FastNoiseLite 平铺语义(架构 §11 P3)
|
||||||
|
# 依据:管线报告 §6.3;作用于 §5.2 纸纹 pass(双尺度 noise 有序抖动的 tileable 前提)
|
||||||
|
# 实测手册:docs/engineering/2026-10-19-f-w1-probe-p3-manual.md
|
||||||
|
# 结论回填:docs/engineering/probes/P3-result.md
|
||||||
|
#
|
||||||
|
# 验证:seamless = true 是否真正可平铺
|
||||||
|
# - 屏上:左右两个 2×2 tile 网格(红线 = tile 边界 @256px)肉直视读接缝
|
||||||
|
# - 按 S:生成边界差图 PNG(user://p3_diff_*.png)+ 控制台统计
|
||||||
|
# * wrap_* = |左边缘-右边缘| / |上边缘-下边缘|(平铺接缝强度)
|
||||||
|
# * grad_mean = 相邻像素平均差(自然梯度基线)
|
||||||
|
# * seamless 若有效:wrap ≈ grad(同数量级);若无效:wrap >> grad
|
||||||
|
#
|
||||||
|
# 知识缺口:FastNoiseLite/NoiseTexture2D 属性名(seamless / seamless_blend_ratio /
|
||||||
|
# TYPE_SIMPLEX / FRACTAL_FBM)为 4.x 稳定名;4.7.2 若报错按实际属性名修正并记录。
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
const NOISE_SIZE := 256 # 每 tile 256×256;面板 512×512 = 2×2 平铺
|
||||||
|
const PANEL_SIZE := 512
|
||||||
|
|
||||||
|
var _textures: Array = []
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_textures = [_make_texture(true), _make_texture(false)]
|
||||||
|
var positions := [Vector2(96, 160), Vector2(672, 160)]
|
||||||
|
var titles := ["seamless = true", "seamless = false"]
|
||||||
|
for i in _textures.size():
|
||||||
|
var tr := TextureRect.new()
|
||||||
|
tr.name = "Panel%d" % i
|
||||||
|
tr.texture = _textures[i]
|
||||||
|
tr.position = positions[i]
|
||||||
|
tr.size = Vector2(PANEL_SIZE, PANEL_SIZE)
|
||||||
|
tr.stretch_mode = TextureRect.STRETCH_TILE
|
||||||
|
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(tr)
|
||||||
|
var overlay := SeamOverlay.new()
|
||||||
|
overlay.position = positions[i]
|
||||||
|
add_child(overlay)
|
||||||
|
var t := Label.new()
|
||||||
|
t.name = "PanelTitle%d" % i
|
||||||
|
t.text = "%s(红线 = tile 边界;面板内 2×2 平铺)" % titles[i]
|
||||||
|
t.position = Vector2(int(positions[i].x), 128)
|
||||||
|
t.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
add_child(t)
|
||||||
|
var hud := Label.new()
|
||||||
|
hud.name = "HUDHelp"
|
||||||
|
hud.text = "探针 P3 | 对比两侧红线处接缝(错位/亮度跳变=异常)| S 生成检查图到 user:// 并打印统计 | Esc 退出"
|
||||||
|
hud.position = Vector2(96, 648)
|
||||||
|
hud.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
add_child(hud)
|
||||||
|
_print_environment()
|
||||||
|
|
||||||
|
|
||||||
|
func _make_texture(seamless: bool) -> NoiseTexture2D:
|
||||||
|
var fnl := FastNoiseLite.new()
|
||||||
|
fnl.seed = 20261019
|
||||||
|
fnl.noise_type = FastNoiseLite.TYPE_SIMPLEX
|
||||||
|
fnl.frequency = 0.02
|
||||||
|
fnl.fractal_type = FastNoiseLite.FRACTAL_FBM
|
||||||
|
fnl.fractal_octaves = 3
|
||||||
|
fnl.fractal_lacunarity = 2.0
|
||||||
|
fnl.fractal_gain = 0.5
|
||||||
|
var nt := NoiseTexture2D.new()
|
||||||
|
nt.width = NOISE_SIZE
|
||||||
|
nt.height = NOISE_SIZE
|
||||||
|
nt.noise = fnl
|
||||||
|
nt.seamless = seamless
|
||||||
|
nt.seamless_blend_ratio = 0.25
|
||||||
|
nt.generate_mipmaps = false
|
||||||
|
return nt
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
var key_event := event as InputEventKey
|
||||||
|
if key_event != null and key_event.pressed and not key_event.echo:
|
||||||
|
match key_event.keycode:
|
||||||
|
KEY_S:
|
||||||
|
_generate_check_images()
|
||||||
|
KEY_ESCAPE:
|
||||||
|
get_tree().quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _generate_check_images() -> void:
|
||||||
|
var names := ["seamless", "periodic"]
|
||||||
|
print("=== P3 检查图与统计 ===")
|
||||||
|
for i in _textures.size():
|
||||||
|
var nt: NoiseTexture2D = _textures[i]
|
||||||
|
var img := nt.get_image()
|
||||||
|
if img == null:
|
||||||
|
push_warning("P3: NoiseTexture2D.get_image() 返回 null(%s)。回退:系统截图(Win+Shift+S)+ 肉眼判读。" % names[i])
|
||||||
|
continue
|
||||||
|
var w := img.get_width()
|
||||||
|
var h := img.get_height()
|
||||||
|
# 边界差图(×8 放大):wrap 接缝越亮 = 平铺越不连续
|
||||||
|
var strip_v := Image.create(w, h, false, Image.FORMAT_RGBA8)
|
||||||
|
var strip_h := Image.create(w, h, false, Image.FORMAT_RGBA8)
|
||||||
|
for y in h:
|
||||||
|
var dv := _amp(absf(img.get_pixel(0, y).r - img.get_pixel(w - 1, y).r))
|
||||||
|
for x in w:
|
||||||
|
strip_v.set_pixel(x, y, Color(dv, dv, dv, 1.0))
|
||||||
|
for x in w:
|
||||||
|
var dh := _amp(absf(img.get_pixel(x, 0).r - img.get_pixel(x, h - 1).r))
|
||||||
|
for y in h:
|
||||||
|
strip_h.set_pixel(x, y, Color(dh, dh, dh, 1.0))
|
||||||
|
strip_v.save_png("user://p3_diff_v_%s.png" % names[i])
|
||||||
|
strip_h.save_png("user://p3_diff_h_%s.png" % names[i])
|
||||||
|
var stats := _edge_stats(img)
|
||||||
|
print("%s: wrap_v mean=%.5f max=%.5f | wrap_h mean=%.5f max=%.5f | grad_mean=%.5f → wrap/grad v=%.2fx h=%.2fx" % [
|
||||||
|
names[i], stats.wrap_v_mean, stats.wrap_v_max, stats.wrap_h_mean, stats.wrap_h_max, stats.grad_mean,
|
||||||
|
stats.wrap_v_mean / maxf(stats.grad_mean, 1e-9), stats.wrap_h_mean / maxf(stats.grad_mean, 1e-9)])
|
||||||
|
print("PNG 位置:%APPDATA%/Godot/app_userdata/<项目名>/p3_diff_*.png")
|
||||||
|
print("判读:seamless 行 wrap/grad ≈ 1-3x = 可用;periodic 行 wrap/grad ≫ 1 = 对照组生效(说明指标有效)")
|
||||||
|
|
||||||
|
|
||||||
|
func _amp(d: float) -> float:
|
||||||
|
return clampf(d * 8.0, 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _edge_stats(img: Image) -> Dictionary:
|
||||||
|
var w := img.get_width()
|
||||||
|
var h := img.get_height()
|
||||||
|
var wrap_v := 0.0
|
||||||
|
var wrap_v_max := 0.0
|
||||||
|
for y in h:
|
||||||
|
var d := absf(img.get_pixel(0, y).r - img.get_pixel(w - 1, y).r)
|
||||||
|
wrap_v += d
|
||||||
|
wrap_v_max = maxf(wrap_v_max, d)
|
||||||
|
var wrap_h := 0.0
|
||||||
|
var wrap_h_max := 0.0
|
||||||
|
for x in w:
|
||||||
|
var d := absf(img.get_pixel(x, 0).r - img.get_pixel(x, h - 1).r)
|
||||||
|
wrap_h += d
|
||||||
|
wrap_h_max = maxf(wrap_h_max, d)
|
||||||
|
var grad := 0.0
|
||||||
|
for y in h:
|
||||||
|
for x in w - 1:
|
||||||
|
grad += absf(img.get_pixel(x, y).r - img.get_pixel(x + 1, y).r)
|
||||||
|
return {
|
||||||
|
"wrap_v_mean": wrap_v / h,
|
||||||
|
"wrap_v_max": wrap_v_max,
|
||||||
|
"wrap_h_mean": wrap_h / w,
|
||||||
|
"wrap_h_max": wrap_h_max,
|
||||||
|
"grad_mean": grad / float((w - 1) * h),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _print_environment() -> void:
|
||||||
|
var v := Engine.get_version_info()
|
||||||
|
print("=== P3 probe 环境 ===")
|
||||||
|
print("godot: %d.%d.%d %s" % [int(v.major), int(v.minor), int(v.patch), str(v.status)])
|
||||||
|
print("rendering_method: ", ProjectSettings.get_setting("rendering/renderer/rendering_method"))
|
||||||
|
print("adapter: ", RenderingServer.get_video_adapter_name())
|
||||||
|
print("noise: FastNoiseLite simplex fbm oct3 f0.02 | tile 256×256 | blend_ratio 0.25")
|
||||||
|
|
||||||
|
|
||||||
|
class SeamOverlay extends Node2D:
|
||||||
|
const TILE := 256
|
||||||
|
const PANEL := 512
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var c := Color(1.0, 0.2, 0.2, 0.6)
|
||||||
|
draw_line(Vector2(TILE, 0), Vector2(TILE, PANEL), c, 1.0)
|
||||||
|
draw_line(Vector2(0, TILE), Vector2(PANEL, TILE), c, 1.0)
|
||||||
|
draw_rect(Rect2(Vector2.ZERO, Vector2(PANEL, PANEL)), c, false, 1.0)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/probe_p3_noise_tiling/probe_p3.gd" id="1"]
|
||||||
|
|
||||||
|
[node type="Node2D" name="ProbeP3NoiseTiling"]
|
||||||
|
script = ExtResource("1")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# tests/sim_reports —— 模拟器 14 门报告 report-<date>.json(git 跟踪,月度对账证据)。W4 首轮(架构 §9.3/§10.3)。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# tests/unit —— GUT 单测(core 全覆盖:公式/迁移/格式化)。W3 激活;GUT addon 待合入(CI W3 占位)。
|
||||||
Reference in new issue
Block a user