diff --git a/.gitea/workflows/baseline-mac.yml b/.gitea/workflows/baseline-mac.yml new file mode 100644 index 00000000..89f3e62a --- /dev/null +++ b/.gitea/workflows/baseline-mac.yml @@ -0,0 +1,53 @@ +# mac 平台 golden 基线生成工作流(workflow_dispatch 手动触发型,2026-11-03)。 +# 用途:渲染基线按平台各存——mac 基线必须在 Mac 的图形会话里窗口化渲染产出(Windows 基线不可跨平台替代)。 +# 纪律(Q6):本工作流只「生成」候选基线文件(落在 runner 作业工作区,不提交不入库); +# 入库走人工通道:人工目检四张基线(体验验证清单㊾)→ 取回文件显式提交并写明触发原因。 +# 禁由 CI 自动提交基线(防静默重制掩盖视觉回归)。 +# 运行形态:runs-on=ci-mac(host 模式,作业活在 ci-runner 图形会话内——窗口可开、截图可用)。 +name: baseline-mac +on: + workflow_dispatch: + +jobs: + refresh: + runs-on: ci-mac + timeout-minutes: 30 + steps: + - name: 检出(纯 shell 克隆) + env: + TOKEN: ${{ github.token }} + run: | + git clone -q "https://${TOKEN}@git.kuaimeo.com/${GITHUB_REPOSITORY}.git" "$GITHUB_WORKSPACE/repo" + cd "$GITHUB_WORKSPACE/repo" && git log --oneline -1 + + - name: 导入缓存刷新 + run: | + cd "$GITHUB_WORKSPACE/repo" + /usr/local/bin/godot --headless --path game --editor --quit + + - name: 退出判别实验(quit-after 能否解 macOS 窗口化退出挂死) + run: | + cd "$GITHUB_WORKSPACE/repo" + mkdir -p /tmp/shots_diag + /usr/local/bin/godot --path game --quit-after 3000 res://probe/board_shot_probe.tscn -- /tmp/shots_diag + echo "diag_rc=$?" + ls -la /tmp/shots_diag/ | head -8 + + - name: 基线生成(棋盘三图) + run: | + cd "$GITHUB_WORKSPACE/repo" + GODOT_BIN=/usr/local/bin/godot bash tools/golden_baseline_refresh.sh "$GITHUB_WORKSPACE/repo/game" + ls -la game/tests/golden/mac/ || true + + - name: 基线生成(菜单全屏) + run: | + cd "$GITHUB_WORKSPACE/repo" + GODOT_BIN=/usr/local/bin/godot bash tools/golden_baseline_refresh.sh "$GITHUB_WORKSPACE/repo/game" --menu + ls -la game/tests/golden/mac/ || true + + - name: 基线导出(/Users/Shared 不随作业清理,供人工取回目检) + run: | + mkdir -p /Users/Shared/baseline_out + cp -f "$GITHUB_WORKSPACE/repo/game/tests/golden/mac/"*.png /Users/Shared/baseline_out/ + chmod 644 /Users/Shared/baseline_out/* + ls -la /Users/Shared/baseline_out/ diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 00000000..7237235d --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,51 @@ +# 远端 CI 工作流(2026-11-03 用户裁:CI 迁远端 Mac runner,host 模式直跑)。 +# 触发:master 收到 push(post-merge 钩子合回自动推,或人手推 master)——分支推送不触发 +# (开发分支不烧远端引擎时长;分支级验证=开发期手动跑批档)。 +# 跑什么由 tools/ci/domain_route.sh 按改动域分流(乙案)—— +# 游戏码批=两域全量(含渲染闸);文档批=文档域秒级闸;纯闸件/管线批=零触发。 +# 运行环境:Mac host 模式(tools/ci/mac_setup.sh 自举),GODOT_BIN/GDSTYLE_BIN 由自举件安装到位。 +# 检出用纯 shell 克隆:host 模式执行 JS 系动作(actions/checkout)需要 node 运行时, +# runner 机器无 node——首跑实证「Cannot find: node in PATH」即败(run 4),本仓禁用 JS 系动作, +# 引入新 Actions 前必须确认其执行器形态(shell/composite 可,node/docker 不可)。 +# 闸清单与域划分唯一出处 = tools/run_all_gates.sh(本文件只负责触发与分流,不另列闸件)。 +name: ci +on: + push: + branches: [master] + +jobs: + gates: + runs-on: ci-mac + timeout-minutes: 90 + steps: + - name: 检出(纯 shell 克隆) + env: + TOKEN: ${{ github.token }} + run: | + git clone -q "https://${TOKEN}@git.kuaimeo.com/${GITHUB_REPOSITORY}.git" "$GITHUB_WORKSPACE/repo" + cd "$GITHUB_WORKSPACE/repo" + git log --oneline -1 + + - name: 域分流(乙案) + id: route + run: | + cd "$GITHUB_WORKSPACE/repo" + bash tools/ci/domain_route.sh "${{ github.event.before }}" "${{ github.sha }}" + + - name: 导入缓存刷新(闸前置——.godot 不入 git,冷检出一付) + if: steps.route.outputs.mode != 'skip' + run: | + cd "$GITHUB_WORKSPACE/repo" + /usr/local/bin/godot --headless --path game --editor --quit + + - name: 跑闸 + if: steps.route.outputs.mode != 'skip' + env: + GODOT_BIN: /usr/local/bin/godot + GDSTYLE_BIN: /usr/local/bin/gdstyle + run: | + cd "$GITHUB_WORKSPACE/repo" + MODE="${{ steps.route.outputs.mode }}" + if [ "$MODE" = "full" ]; then ARGS=""; else ARGS="--docs"; fi + echo "[ci] 域=$MODE 跑闸参数=$ARGS" + bash tools/run_all_gates.sh $ARGS diff --git a/.gitignore b/.gitignore index ef6a5961..fa6e5311 100644 --- a/.gitignore +++ b/.gitignore @@ -53,10 +53,9 @@ archive/ # === 在途图像素材(2026-10-02 用户裁:artwork/ 整目录不入库,取消 md 例外——此前文本指引入库留痕的口子关闭)=== artwork/ -# === 游戏包美术素材(2026-09-29 裁定维持:素材不入 git,运行时资源本地生成不入库)=== -game/assets/art/** -!game/assets/art/**/*.md -!game/assets/art/**/ +# === 游戏包美术素材(2026-11-03 用户裁:素材回 git,推翻 2026-09-29 不入库裁定—— +# 仓库自包含原则:远端 CI 与任何新克隆必须拉到完整可跑素材)=== +!game/assets/art/**/*.import # === 模拟数据缓存层(2026-10-03 用户裁:sim-data/ 数据件不入库——最新 MC 锚常驻留盘作分析缓存,无数值调整不重跑;同日二裁:README+分析工具三件留库,其余留盘)=== sim-data/* diff --git a/AGENTS.md b/AGENTS.md index 9fdd17c3..47711831 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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-merge 钩子复验追绿(红态修复走新开修复 worktree,`fix-<任务名>` 分支,免重塑免数卡;post-commit 钩子保留为红态主区直提兜底触发,绿态与无状态零触发)——批内提交零钩子检查、合回只验一次,同一份字节禁止第二次人手跑。CI 红不回滚合并,红项在案、修复合回追绿;开工先读 CI 状态,红未修不开新批。跑批起手自动 `--editor --quit` 刷类名缓存(.godot 缓存不入 git,防合并后失配假红)。`tools/run_all_gates.sh` 仍是全部验证闸的**唯一入口与唯一清单,域划分与选闸算法只写在它这一个地方**,闸件增减与分域调整都只改它,文档禁另列清单;`--code`/`--docs`/`--quick` 手动档只服务开发期按域跑批(开发期窄测纪律见 process-conventions §6),不构成验收;全量回归由用户自发发起。**回执等待一律后台(2026-11-02 用户裁)**:合回落地先向用户呈报本批总结,随即 run_in_background 运行 `tools/ci_watch.sh <合并提交>` 等回执、唤醒后再续,禁前台 sleep/轮询等 CI(细则=process-conventions §1 第 3 条) +- **合回验证 = 远端 CI(2026-11-03 用户裁,取代 2026-10-30 本地 CI)**:git merge 合回 master 落地后,post-merge 钩子(`tools/hooks/post-merge`)自动 `git push origin master` 触发远端 CI(Gitea Actions,runner=Mac 本机 host 模式,环境自举件=`tools/ci/mac_setup.sh`),工作流(`.gitea/workflows/ci.yml`)按改动域分流(分流算法单点=`tools/ci/domain_route.sh`):游戏码批=两域全量含渲染闸,文档批=文档域秒级闸,纯闸件/管线批=零触发。回执由 `tools/ci_watch.sh remote` 轮询 Gitea API 写回 `game/tests/.gates/ci-status.json`(合同不变:commit/绿红/红闸(job 级)+job 日志链接/耗时)+`ci-history.jsonl` 流水账;**回执等待一律后台(2026-11-02 用户裁)**:合回落地先向用户呈报本批总结,随即 run_in_background 运行 `tools/ci_watch.sh remote <合并提交>`,禁前台 sleep/轮询等 CI(细则=process-conventions §1 第 3 条)。CI 红态不回滚合并——红项在案、修复批合回即再推再验追绿;开工先读 CI 状态,红未修不开新批。同一份字节禁止第二次人手跑。`tools/run_all_gates.sh` 仍是全部验证闸的**唯一入口与唯一清单,域划分与选闸算法只写在它这一个地方**,文档禁另列清单;`--code`/`--docs`/`--quick` 手动档只服务开发期按域跑批(开发期窄测纪律见 process-conventions §6),不构成验收;全量回归由用户自发发起。**runner 依赖**:Mac 需开机在线(关机=远端任务排队延后);换机/重装按 `tools/ci/mac_setup.sh` 自举 - 单件直跑(调试期最窄确认用,**不构成验收**):冒烟测试 `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`——探针标配 / 回归断言 / 提交前验证 / 验收清单落码 | @@ -103,11 +103,11 @@ ## 硬性红线(勿违反;操作细则在 process-conventions.md 与各规约) -- 车道铁律(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) +- 车道铁律(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(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 推导」并集套件、三件保险恒跑、横切面回落全量——算法只在闸脚本内定义),手动跑不构成验收;全量回归由用户自发发起。文档面批与代码面批验收同走合回 CI 全量(`--docs` 仅属开发期手动档,process-conventions §1) -- **改动四类**——**需求改动**(游戏代码,含随逻辑写/改的测试代码):唯一走「写需求卡片+编入迭代批次」通道;迭代实现批禁写 `design/` 整个目录、两份根流程清单(`待办清单.md`/`体验验证清单.md`)与 `game/tests/README.md`(能力清单,归写卡段维护)(含新增/修改/删除),设计文档对它只读、是写测试与实现代码的输入依据;两份流程清单的条目状态只由流水线各段推进,实现批禁止顺手改;产出物只准写 `reports/`;合回后由本地 CI 全量验证(红项修复合回追绿,process-conventions §1)。**工程改动**(tools/、闸件、模拟测量工具、纯测试维护——不改变游戏行为的配套代码)、**文档设计改动**、**美术素材改动**:不写需求卡片、不编迭代批次,由用户发令走独立 worktree 直改+合回(车道铁律,commit 点零钩子检查);大体量改动(含触及代码面的工程改动如纯测试维护)同车道无特殊通道;文档设计类写入走 doc-design 技能(登记条目格式由流水线闸强制) +- 合回后验证 = 远端 CI 闸全绿(唯一强制验证点;唯一入口、唯一闸清单、唯一域划分与选闸算法仍只在 `tools/run_all_gates.sh`,禁在文档另列;触发与追绿语义见「运行与验证」首条与 process-conventions §1)。开发期手动跑批按需选档(`--code` 关联档选测——02 跑「卡片申报 ∪ diff 推导」并集套件、三件保险恒跑、横切面回落全量——算法只在闸脚本内定义),手动跑不构成验收;全量回归由用户自发发起。远端按改动域分流:文档面批走文档域秒级闸,代码面批走两域全量(分流算法单点=`tools/ci/domain_route.sh`) +- **改动四类**——**需求改动**(游戏代码,含随逻辑写/改的测试代码):唯一走「写需求卡片+编入迭代批次」通道;迭代实现批禁写 `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) diff --git a/design/engineering/architecture/architecture.md b/design/engineering/architecture/architecture.md index 4fc7fcb2..087faa46 100644 --- a/design/engineering/architecture/architecture.md +++ b/design/engineering/architecture/architecture.md @@ -2,8 +2,8 @@ ## 文档状态 -- 版本:1.32 -- 最近更新:2026-11-03(架构审查 5.2 文档漂移订正——主角账三件与刷新账件四行补登记替代失效的 HeroLedger 行、MetaProgression 基础层补登记行(四 Autoload 唯一跨 Autoload 依赖件)、拆件定稿节冻结计数摘除(活体清单不冻结计数)、CurioStop 行落账指称随拆件改指;EC8 热重载规格经用户裁决保留原状(实装排期另立待办条目)、本批一度摘除已撤回,本文「抗热重载」快照句随规格保留;沿革查 git) +- 版本:1.35 +- 最近更新:2026-11-04(架构审查 5.2 文档漂移订正并入 P1 余项批之后——主角账三件与刷新账件四行补登记替代失效的 HeroLedger 行、MetaProgression 基础层补登记行(四 Autoload 唯一跨 Autoload 依赖件)、拆件定稿节冻结计数摘除(活体清单不冻结计数)、CurioStop 行落账指称随拆件改指;EC8 热重载规格经用户裁决保留原状(实装排期另立待办条目)、一度摘除已撤回,「抗热重载」快照句随规格保留;沿革查 git) - 引擎:Godot 4.7.2-stable(GDScript 单语言;2026-09-07 锁定) - 评审模式:solo(`review-mode.txt`,2026-09-12 用户拍板——TD/LP 门禁按 solo 跳过,用户授权架构产出自主推进、迭代期回调) - 覆盖 GDD:现行全集(根索引=`design/gdd/02-系统清单.md`,文档树构成以根索引为准) @@ -102,7 +102,7 @@ | 模块 | 独占职责 | 对外暴露 | 消费依赖 | 引擎 API | |---|---|---|---|---| | **board_generator**(A1) | 生成算法(五阶段管线)、BoardGenTable 回填、生成期指标集、合法性谓词(clearable / R₀≠S / min_safe)、可推理率度量(metric_version);生成姿态 = MVP 同步生成 + 加载幕(切片实证 10×10/12×12 同步可接受,WorkerThread 留扩展) | `generate(L, D, rng) -> Board`;度量独立入口×3(推理器 / F3 度量 / 合法性谓词——独立纯函数、同一实现) | ConfigDB(BoardGenTable/VariationTable);board_gen 流(一次 open_stream = 一张图);dig_ap_cost 只读 | 无 | -| **minesweeper_rules**(A2) | 棋盘状态(已开集 O / 封印集 G / 小人位)、三事件面、批次仲裁、install 四不变式 | `install_board(board)`;`settle_batch(intents)`(唯一提交入口);查询只读面(current_board / hero_pos / is_revealed / has_seal 等);`compute_move_path(cell)` 只读;`retire_board()`;三源拒绝(board_face / resource_face / phase)+ sub_reason 封闭枚举 | A1 产物(Board class_name 数据契约);A3 try_spend / last_round_end(迭代 41 由属性改方法)/ 相位 | 无(零表读取、零 UI——切片实证) | +| **minesweeper_rules**(A2) | 棋盘状态(已开集 O / 封印集 G / 小人位)、三事件面、批次仲裁、install 四不变式 | `install_board(board)`;`settle_batch(intents)`(唯一提交入口);查询只读面(current_board / hero_pos / is_revealed / has_seal 等);`compute_move_path(cell)` 只读;`preview_reveal(cell)` 只读预览口(零写入零事件——0 格连锁传播闭包;迭代 50 己案建口);`retire_board()`;三源拒绝(board_face / resource_face / phase)+ sub_reason 封闭枚举 | A1 产物(Board class_name 数据契约);A3 try_spend / last_round_end(迭代 41 由属性改方法)/ 相位 | 无(零表读取、零 UI——切片实证) | | **unit_container**(B2) | hero/enemy 单位实例(hp/atk/实例池:池快照+冷却+摇定动作+保底计数)、行值快照 | spawn / query_alive / rollable_skills / special_skills / check_trigger_unlocks / set_pending_action / pending_action / basic_streak / commit_action / tick_cooldowns / apply_damage / heal / default_target / check_skill_unlocks / despawn_all(2026-09-16 敌人技能池批:query_beat/query_attack/advance 随行为环退役;rollable_skills/special_skills/check_trigger_unlocks 带 facts 参——2026-10-27 情境条件触发器实装,事实缺键=条件假拦下,百分比事实走阈值式折算) | ConfigDB(EnemyTable/SkillTable/HeroTable) | 无 | | **reward_economy**(B4) | 格面资源身份映射(install 期预分配、Ready 只读、换图映射消亡)、极性路由字典面((类型, 极性)→ 字典行查询;路由执行归 B1 授予段)、F6 当量供给投影;装配期摇号面已拆出掷格件 RewardRoller(2026-11-02 轴戊设计闭合、迭代 49 卡① 建件——roll_board 装配序=守卫→复位→委托→收纳→ready;受理账面留守) | 封闭 API:roll_board / query_identity / query_full_snapshot / grant / grant_clear / retire_mapping / polarity_row / phase(weighted_pick 退役——返行选行算子迁掷格件);另有 is_mapped 静默布尔投影(批量工具用);实测类变量 11 | ConfigDB(RewardTable);BattleFxHost 经注入 Callable `luck_bias_query` 取偏运合计(reward_roll 发射与 bias_rewards 求和移至效果层完成);reward_roll 流(scope = 每次 install 批,取号经掷格件) | 无 | | **helper_items**(A5,切片已实装秘宝域) | HelperItemTable、探测显影语义 | (契约规范源:`../../gdd/systems/跨局循环/秘宝.md`——桥接位:A2 查询接口 + A4 counter_item_ids) | A2 / A4 / B4 | 无 | @@ -132,7 +132,7 @@ | **level_runner**(C1-lite,切片已建) | 关卡域生命周期(节点链推进、共享 B4 + HP 携带、关卡末 level_clear) | 节点推进接口;LevelTable(`level.tres`——节点 enum {battle,reward,rest,boss}、末节点必须 Boss、ConfigDB 跨表校验⑧) | B1(直构 BattleOrchestrator 接线战斗)、ConfigDB LevelTable | 无 | | **run_structure**(C2) | 冒险/关卡序列、失败重开、**master_seed 唯一接线方** | (契约规范源:`../../gdd/systems/冒险循环/冒险结构.md`) | RngService.set_master_seed、C3/D2 | 无 | | **difficulty**(C3) | DifficultyTable 回填、全部曲线数值调参所有权、战斗期望表(切片 monte_carlo 为工具原型) | 调参输出(经 D2 R1 解析/编排器传递,禁反向调用生成) | ConfigDB、B1 对账输出(调参约束清单)、A1 报告 C | 无 | -| **meta_progression**(C4,Alpha) | MetaTable、钥石账本、天赋树域(MetaTreeTable 12 节点 v0.8——Fork 一节点双态/row 字段退役/RULE_KEYS 六键/彩蛋特化觉醒,迭代 21 T1 换代)+ 同调位槽位判定(树点亮数单源——T4 槽位收编)。**存档内元进度数据读写原语归 MetaStateStore——已落地(迭代 41):本类纯转发读已退役(消费方直连 meta_state),业务规则与写口编排留本类;hero_override 内联锻造加值与树淬炼 flat;并组三组(forge_select(kind,route_id)/hero_type_axes()→{attack,armor}/tree_camp_status()→{rows,fork_groups});补 meta_state 成员装配** | 公开面 20 定稿:buy_treasure_unlock / refresh_treasure_unlocks / treasure_shop_status / forge_buy_level / forge_buy_unlock / forge_select / hero_type_axes / forge_shop_status / settle_adventure / hero_override / tree_can_buy / tree_buy / tree_toggle_fork / tree_buy_fork / tree_rule_keys / tree_camp_status / toggle_attunement / attune_camp_status / attune_refresh_charges / buy_attune_refresh | D3、C2、ConfigDB | 无 | +| **meta_progression**(C4,Alpha) | MetaTable、钥石账本、天赋树域(MetaTreeTable 12 节点 v0.8——Fork 一节点双态/row 字段退役/RULE_KEY_PARAMS 六键参数字典(迭代 50 键值字典化)/彩蛋特化觉醒,迭代 21 T1 换代)+ 同调位槽位判定(树点亮数单源——T4 槽位收编)。**存档内元进度数据读写原语归 MetaStateStore——已落地(迭代 41):本类纯转发读已退役(消费方直连 meta_state),业务规则与写口编排留本类;hero_override 内联锻造加值与树淬炼 flat;并组三组(forge_select(kind,route_id)/hero_type_axes()→{attack,armor}/tree_camp_status()→{rows,fork_groups});补 meta_state 成员装配** | 公开面 20 定稿:buy_treasure_unlock / refresh_treasure_unlocks / treasure_shop_status / forge_buy_level / forge_buy_unlock / forge_select / hero_type_axes / forge_shop_status / settle_adventure / hero_override / tree_can_buy / tree_buy / tree_toggle_fork / tree_buy_fork / tree_rules(返参数字典——迭代 50 R1 换代)/ tree_camp_status / toggle_attunement / attune_camp_status / attune_refresh_charges / buy_attune_refresh | D3、C2、ConfigDB | 无 | ### 战斗域装配现状(迭代 27——2026-10-17 用户裁写入;RunLedger 段已随迭代 39 拆件批退役,四账去向见下「冒险域拆分(现状登记)」节) @@ -271,7 +271,7 @@ |---|---|---|---| | **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 图 | -验证收拢为合回后本地 CI 全量(post-merge 触发),commit 点零执法;主工作区防污染由 ZCode 层守卫承担——只约束 agent,用户手工改动不受限。子代理覆盖面(2026-10-26 冷启动评估实测):子代理的工具调用不经过 ZCode 层守卫——ZCode 层实际只覆盖主会话工具调用;残余暴露面=子代理写主工作区与蓄意 `--no-verify`(该旗标整族跳过 git 层钩子),属故意越权而非遗忘型失误,与防遗忘的设计目标正交。 +验证收拢为合回后远端 CI(post-merge 钩子推送 origin master 触发 Gitea Actions,runner=Mac 本机 host 模式;按改动域分流,分流单点=tools/ci/domain_route.sh),commit 点零执法;主工作区防污染由 ZCode 层守卫承担——只约束 agent,用户手工改动不受限。子代理覆盖面(2026-10-26 冷启动评估实测):子代理的工具调用不经过 ZCode 层守卫——ZCode 层实际只覆盖主会话工具调用;残余暴露面=子代理写主工作区与蓄意 `--no-verify`(该旗标整族跳过 git 层钩子),属故意越权而非遗忘型失误,与防遗忘的设计目标正交。 ### 三条判定硬闸现行状态 @@ -283,22 +283,24 @@ 背景事实(run_smoke 头注在案):GDScript 运行时脚本错误不改进程退出码、不计入冒烟失败数、且静默中断所在函数剩余断言——2026-09-19 状态记录 83 实证「309 项 0 失败」实为断言未跑全。这是 SCRIPT ERROR 扫描必须存在于判定层的原因。 -### 静态扫描域 S1–S8 承接清单 +### 静态扫描域 S1–S13 承接清单 静态扫描域规则(2026-09-12 立,沿革见 git commit log;下表为现行事实登记): | # | 规则 | 状态 | |---|---|---| -| S1 | 玩法代码禁 `randomize()`、自建 `RandomNumberGenerator`(D2 Rule 5) | **在位**(硬失败;扫描面 game/core + game/presentation) | +| S1 | 玩法代码禁 `randomize()`、自建 `RandomNumberGenerator`(D2 Rule 5) | **在位**(硬失败;扫描面 game/core + game/presentation)。非产品码三目录(`game/tests`/`game/probe`/`game/sim`)不进本闸与 S2——它们自建且**显式定种**的随机源是合法测量口径(同种子对拍改前改后读数),禁的是不定种形态;适用域分档与无机器闸的理由见 code-conventions §4 第 13 条(2026-11-03 架构审查 P1 余项裁:不扩扫描面,人工评审承载) | | S2 | 禁全局随机函数 `randi()` / `randf()` / `randf_range()`——全局 RNG 绕开 RngService(specialist 补登) | **在位**(与 S1 同段硬失败;实例方法 `.randi()` 排除、`rng-seed-exempt` 豁免注记在案) | -| S3 | core/ 禁 ConfigDB 外 data 表加载(`load(` / `preload(` / `ResourceLoader.load`;schema class_name 直引豁免)(D2 Rule 2) | **在位**(warning 级——class_name 直引误报需人工复核;路径已由裸 `core` 修正为 `game/core`,此前恒零命中静默空转;豁免 config_db.gd 自身=数值单一事实源的正主) | +| S3 | core/ 禁 ConfigDB 外 data 表加载(`load(` / `preload(` / `ResourceLoader.load`;schema class_name 直引豁免)(D2 Rule 2) | **在位**(warning 级——class_name 直引误报需人工复核;路径已由裸 `core` 修正为 `game/core`,此前恒零命中静默空转;豁免 config_db.gd 自身=数值单一事实源的正主)。2026-11-03 架构审查 P1 余项裁**维持 warning 级不升硬**:升硬的前提是先有可靠的直引误报排除判据,该判据未立;本行状态即事实源,禁在别处另列 | | S4 | core/ 禁 `extends Control/Node2D`、禁 `.tscn` preload(D2 Rule 7) | **在位**(硬失败) | | S5 | core/ 禁裸引用 autoload 全局标识符(`ConfigDB` / `RngService` 直接写 = --script 模式 parse error)——构造注入 only(specialist 升格) | **在位**(硬失败;形态=标识符后紧跟 `.` 的成员访问,排除注释行与字符串字面量) | | S6 | core↔core 信号连接只许方法 Callable(禁 lambda 捕获 self);发射者禁持有订阅者反向引用 | **在位**(2026-10-25 落闸:内联 lambda 禁令机器闸——同行与跨行两形态、豁免注记 `s6-signal-exempt`、盘面零存量;「发射者禁反向引用」句静态判据不成熟,归人工评审面承接、不造弱判据) | | S7 | B1 编排唯一性:B2/B3/B4 API 符号命中文件 ⊆ B1 文件集;A2/B3/B4 零相互直调(B1 AC-43) | **在位**(2026-10-25 落闸:符号清单×B1 域文件集,族内自调用豁免、豁免注记 `s7-b1-exempt`、缺位自检;全符号形态盘面零存量) | -| S8 | 结算路径 `await` 零命中(B1 结算/编排文件集);字面量扫描 = warning 级 + 人工三要素复核(D2 AC-14 口径) | **在位**(2026-10-25 落闸:B1 域文件集 await 零命中,文件集缺位自检、排除注释与字符串;盘面零存量。本行后半句「字面量扫描=warning 级+人工三要素复核」为独立子项、仍归人工面) | +| S8 | 结算路径 `await` 零命中(B1 结算/编排文件集);字面量扫描 = warning 级 + 人工三要素复核(D2 AC-14 口径) | **在位**(2026-10-25 落闸:B1 域文件集 await 零命中,排除注释与字符串;盘面零存量。清单单点=`static_scan.sh` 的 `B1_SET`,S7 的豁免名单由它派生(原两份 14 文件抄本已并点,2026-11-03 架构审查 P1 裁)。自检两件:①清单内文件缺位即红;②**目录域反向对账**——`game/core/settlement/` 实有 `.gd` 未登记进 `B1_SET` 即红(新段类忘登记=await 面静默失效,2026-11-03 补)。反向对账只覆盖 settlement/:装板/授予/效果端口/掷格四件结算码住 `game/core` 根下,纯目录域覆盖不住,根下新增仍由本行「模块归属」B1 登记面+写卡段申报承接。本行后半句「字面量扫描=warning 级+人工三要素复核」为独立子项、仍归人工面 | -域外扩充两件(均硬失败在位):**S9** 类型闸(`tools/audit_type_hints.py`,迭代 27 T5——产品码无类型容器/有值返回缺类型/参数缺类型);**S10** 文件头闸(`tools/audit_file_headers.py`——头注 ≤8 行 + 日期/变更叙事黑名单)。 +域外扩充五件(S11/S12/S13 自 2026-09-29 起陆续落闸,本表原先只登记到 S10,2026-11-03 补齐登记面):**S9** 类型闸(`tools/audit_type_hints.py`,迭代 27 T5——产品码无类型容器/有值返回缺类型/参数缺类型;扫描面=产品码三目录);**S10** 文件头闸(`tools/audit_file_headers.py`——头注 `##` 载体+日期/变更叙事黑名单;扫描面=产品码三目录);**S11** 代码注释禁用词闸(扫描面=`game/` 全域 `.gd`/`.tres`/`.md`/`.json`/`.sh`,词表单点取 `design/gdd/术语表.md`);**S12** 效果引擎无状态化旧件零残留闸(扫描面=`game/` 全域 `.gd`/`.tres`,旧符号与旧 id 字面清单为脚本字面值);**S13** 第三方质量规则闸(`tools/gdstyle_gate.py`,钉 gdstyle v0.4.0;扫描面=产品码三目录,存量以 `tools/gdstyle_baseline.txt` 欠账簿登记,判定=基线外新键红、同键数值恶化红)。 + +S11/S12 扫描面覆盖全域,故 sim/probe/tests 的术语与旧件面**在册**;其余十一闸(S1–S10、S13)扫描面止于产品码,非产品码三目录不受约束——分档理由与各目录的运行期承接通道见 code-conventions §4 第 13 条。**防空转自检现行状态**(本文「三层承载与判定原则」要求扫描面为空即判红):S1–S8 与 S11/S12/S13 各自带空面或缺位自检;**S9/S10 两件已补齐同款自检**(`tools/audit_type_hints.py` 与 `tools/audit_file_headers.py` 缺目录或扫描面零个 `.gd` 一律判红,并绿时打印扫描面件数供核;2026-11-03 架构审查 P1 余项的工程改动批落地,本表此前登记的「静默零命中即 PASS」偏差已清偿)。 ### 三层承载与判定原则 diff --git a/design/engineering/code-conventions.md b/design/engineering/code-conventions.md index 1d3bafdc..0e56f8fe 100644 --- a/design/engineering/code-conventions.md +++ b/design/engineering/code-conventions.md @@ -72,12 +72,13 @@ 4. **lambda 捕获循环变量按创建时值**——循环内先落地局部变量再捕获,或用 `bind`。 5. **重命名脚本必须同步 `.uid` 文件**;新增 `class_name` 后必须跑一次编辑器扫描刷新全局类缓存,否则 `--headless --script` 模式解析失败。 6. **`print` 格式占位符与实参类型必须匹配**(`%d` 配 String = 静默输出字面量)。 -7. **语言纪律已机器化为 S9 类型闸**——`tools/audit_type_hints.py`,执行载体=静态扫描域 S9(`game/tests/static_scan.sh`),退出码 0=归零。扫描面=产品码(`game/core`+`game/data`+`game/presentation`;probe/sim/tests 不在扫描面——测量面容器直构是既有惯例)。三规则:R1 无类型容器(显式非参数化 `Dictionary`/`Array` 注解)/R2 有值返回缺返回类型(纯副作用缺 `-> void` 属写法面不入闸)/R3 参数缺类型(`_init` 豁免——构造注入面形态自由)。 +7. **语言纪律已机器化为 S9 类型闸**——`tools/audit_type_hints.py`,执行载体=静态扫描域 S9(`game/tests/static_scan.sh`),退出码 0=归零。扫描面=产品码三目录(`game/core`+`game/data`+`game/presentation`);非产品码三目录的适用域分档见本节第 13 条。三规则:R1 无类型容器(显式非参数化 `Dictionary`/`Array` 注解)/R2 有值返回缺返回类型(纯副作用缺 `-> void` 属写法面不入闸)/R3 参数缺类型(`_init` 豁免——构造注入面形态自由)。 8. **`:= {}` 推断形态不算债**——`var x := {}` 类赋值推断属类型推断合法面,不入 R1 扫描;只有显式写的非参数化容器注解才算无类型容器。 9. **行级白名单通道 `# type-hint: ignore`**——豁免该行不入 S9;豁免理由必须随行注明(现行例:`board_generator` 四函数的 db 参鸭子型——typed 参拒冒烟夹具 `_BoardRowStub` 注入)。 -10. **`# rng-seed-exempt` 标记豁免通道**——S1/S2 随机函数扫描对该行豁免;限定用途=非玩法 RNG 消费面的引擎熵(现行例:组合根种子编辑框空态取 `randi()`——建种子不进玩法封闭流)。 +10. **`# rng-seed-exempt` 标记豁免通道**——S1/S2 随机函数扫描对该行豁免;限定用途=非玩法 RNG 消费面的引擎熵(现行例:组合根种子编辑框空态取 `randi()`——建种子不进玩法封闭流);非产品码三目录自建随机源的定种要求见第 13 条。 11. **`--script` 模式禁裸 autoload 标识符**——`ConfigDB`/`RngService` 等 autoload 名在 `--headless --script` 模式编译器不解析(parse error);产品码与测试夹具一律经节点路径取用(`get_node("/root/…")`/`AUTOLOAD_PATH_*` 常量);静态扫描域 S5 硬失败承载在位。 12. **Variant 参数化现行口径**——账本类字典/数组标注 `Dictionary[Variant, Variant]`/`Array[Variant]` 即合格(满足 R1);元素类型精化列后续事项、不入硬纪律,口径变更须用户裁决后续批次另行写入。 +13. **静态扫描适用域按来源分档(2026-11-03 架构审查 P1 余项裁)**——S 系静态扫描(S1–S13)的扫描面一律止于产品码三目录,`game/tests`/`game/probe`/`game/sim` 不进任何 S 系扫描面;这三目录的可靠性由运行期通道承接,各通道是:`game/tests`=闸 00/01/02(冒烟·单元与集成·系统测试)、`game/probe`=闸 03/04/05(场景探针与渲染 golden;同目录内 `type_glow_shot_probe` 是人工取证件、`probe_ui_util` 是探针共用面,两者不入闸)、`game/sim`=不入 CI,属用户手动发起的调参与数值测量批量。三目录**允许**自建 `RandomNumberGenerator`,条件=**必须显式定种**(改前改后读数要用同种子对拍,这是全项目测量口径);三目录真正禁止的形态是不定种随机——`randomize()` 与全局 `randi()`/`randf()`/`randf_range()`,一旦命中那次运行结果不可复现,读数与取证一并失效。这条对三目录的部分目前**无机器闸**(S1/S2 扫描面不含它们):扩面需给每处合法定种挂 `rng-seed-exempt` 注记、只拦得住新增,2026-11-03 用户裁不扩,改由本条文字+批末独立验收审计的人工评审承载。术语口径:`tools/run_all_gates.sh` 里的「测量面」指 `game/tests`+`game/sim`(影响面选闸用语,`game/probe` 不在其中——改探针必须跑 03/05),与本条分档无关;本文不再用「测量面」统称这三个目录。 --- diff --git a/design/engineering/process-conventions.md b/design/engineering/process-conventions.md index 1efcf908..a5ce00c5 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -7,12 +7,12 @@ ## 1. 分支纪律(worktree 铁律) -主工作区(master 检出)只作合并落点,一切开发/设计改动禁止直接发生在主工作区(2026-10-31 用户裁恢复;2026-10-30 的「主区直改+直提」车道与 commit 点 pre-commit 直改闸随之退役;验证模型不动——合回后本地 CI 仍是唯一强制验证点): +主工作区(master 检出)只作合并落点,一切开发/设计改动禁止直接发生在主工作区(2026-10-31 用户裁恢复;2026-10-30 的「主区直改+直提」车道与 commit 点 pre-commit 直改闸随之退役;验证模型同构——合回后远端 CI 是唯一强制验证点,2026-11-03 用户裁,细则见本节第 3 条): 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 形)合回前执行 `python tools/reshape_cards.py`,把分支线性史按卡末态快照压成一卡一笔(可选追加一笔纯文档收尾),master 台账由此每卡一行——工具自带「重塑前后树逐字节一致」断言与 backup 分支,不改任何文件内容;非迭代批免重塑;然后主工作区 `git merge --no-ff <任务分支>`——这是主区唯一合法写动作(合回守卫对迭代分支数卡校验,未归卡提交拦下)。**合回落地即 `git worktree remove`+删分支(合回即删,2026-09-19 原裁);post-merge 钩子已在合回落地时自动触发 CI,红态不回滚合并——新开修复 worktree(`fix-<任务名>` 分支,非 iter 形免重塑免数卡)修复提交再合,落地复验追绿后删修复区**。游戏类测试(冒烟/单测/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 形免重塑免数卡;post-commit 钩子保留为红态主区直提兜底触发,绿态与无状态零触发)。重跑判据:同一份字节禁止第二次人手跑;有意义的重跑只剩「验证修复」一类。CI 红不回滚合并——红项记 `game/tests/.gates/ci-status.json`,开工先读 CI 状态,红未修不开新批。跑批起手 `--editor --quit` 刷类名缓存(在册坑:.godot 缓存不入 git,合并后失配假红假绿)。并跑语义=跑完补跑(coalesce):跑批期间新触发不中断当前轮,轮尾对最新尖端补跑,绝不逐笔各跑一轮。**回执等待纪律(2026-11-02 用户裁)**:合回落地即向用户呈报本批总结,回执等待一律以 run_in_background 后台任务运行 `tools/ci_watch.sh <合并提交>`(认领判据与容错细则=该件头注),禁前台 sleep/轮询循环等 CI;后台任务唤醒会话后按回执续行——绿即删区收口,红或超时即呈报走修复批。闸重跑等一切分钟级跑批等待同此纪律 -4. **执法分层(全机械,零自觉依赖)**:①`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:主工作区 Edit/Write 与主区非 merge `git commit` 拦截(豁免 `.zcode/`、`.qoder/`;worktree 内目标快速放行;合并收尾提交凭 MERGE_HEAD 豁免)、`git commit --no-verify` 拦截(该旗标仅属用户)、iter 分支合回的一卡一提交数卡校验、规则闸(写 design//game/ 前核对会话规约读取记录)。②post-merge/post-commit 钩子——本地 CI 触发(仅主工作区;任务 worktree 内的合并不触发)。③git 层不设车道钩子(pre-commit 直改闸已退役)——用户手工改动不受限;ZCode 层只约束 agent(子代理不经守卫层) +3. **合回后验证(远端 CI,2026-11-03 用户裁取代本地 CI)**:git merge 合回 master 落地后 post-merge 钩子(`tools/hooks/post-merge`)自动 `git push origin master` 触发远端 CI(Gitea Actions,runner=Mac 本机 host 模式),工作流按改动域分流(分流算法单点=`tools/ci/domain_route.sh`:游戏码批=两域全量含渲染闸,文档批=文档域秒级闸,纯闸件/管线批=零触发)——唯一强制验证点。回执由 `tools/ci_watch.sh remote` 轮询 Gitea API 写回 `game/tests/.gates/ci-status.json`(合同不变:commit/verdict/red_gates/key_lines/note),agent 开工先读 CI 状态的纪律不变,红未修不开新批。CI 红态不回滚合并——修复走新开修复 worktree(`fix-<任务名>` 分支,非 iter 形免重塑免数卡),修复合回落地即再推再验追绿(追绿同链;本地跑批件 `ci_runner.sh` 与本地追绿钩子 post-commit 已退役)。导入缓存刷新(.godot 不入 git 的在册坑)由工作流「导入缓存刷新」步接管。**回执等待纪律(2026-11-02 用户裁)**:合回落地即向用户呈报本批总结,回执等待一律以 run_in_background 后台任务运行 `tools/ci_watch.sh remote <合并提交>`(认领判据与容错细则=该件头注),禁前台 sleep/轮询循环等 CI;后台任务唤醒会话后按回执续行——绿即删区收口,红或超时即呈报走修复批。闸重跑等一切分钟级跑批等待同此纪律 +4. **执法分层(全机械,零自觉依赖)**:①`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:主工作区 Edit/Write 与主区非 merge `git commit` 拦截(豁免 `.zcode/`、`.qoder/`;worktree 内目标快速放行;合并收尾提交凭 MERGE_HEAD 豁免)、`git commit --no-verify` 拦截(该旗标仅属用户)、iter 分支合回的一卡一提交数卡校验、规则闸(写 design//game/ 前核对会话规约读取记录)。②post-merge 钩子——远端 CI 触发(推送+挂回执等待;仅主工作区,任务 worktree 内的合并不触发)。③git 层不设车道钩子(pre-commit 直改闸已退役)——用户手工改动不受限;ZCode 层只约束 agent(子代理不经守卫层) ## 2. 五段流水线 @@ -44,12 +44,12 @@ 5. **涉数值改动先采改前读数**——与改后同随机种子配对比较并写明逐项比对口径;无数值改动禁跑模拟 **实施** -6. **新功能必配新测试**——新增玩法/机制必须配套新测试套件进 `tests/run_all`;没有对应测试的变更不许合并;并按 test-conventions §8 挂进改动—验收对照表,没有对照表行的行为改动同样不许合并 +6. **新功能必配新测试**——新增玩法/机制必须配套新测试套件进 `tests/run_all`;没有对应测试的变更不许合并;并按 test-conventions §8 挂进改动—验收对照表,没有对照表行的行为改动同样不许合并。拆件/重构产出的新 core 件不属新增玩法,由 test-conventions §8.3「新增 core 件的覆盖登记义务」承接(被 tests 面点名,或在能力行申报「经宿主链覆盖」),核对动作归本清单第 10 条的收尾提交 7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补 **完成** -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 条),合并后无手工动作 +8. **合回后 CI 验证(唯一强制验证点)**——主工作区 `git merge --no-ff <任务分支>` 落地后 post-merge 钩子自动推送 origin master 触发远端 CI;远端按改动域分流(§1 第 3 条),游戏码批跑全量域闸(不带参数档,02 全套件),判定与红项由 `ci_watch.sh remote` 写回 `game/tests/.gates/ci-status.json`。全量是游戏码批的强制档:跨卡触面、探针与未申报套件的消费漏网只有全量闸能抓(迭代 44 实证),关联档选测的盲区由它兜底。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。`tools/run_all_gates.sh` 是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。开发期手动跑批按需选档(按文件性质判域:代码面→`--code`,文档件→`--docs`,两类都有→全量;`--code` 关联档选测——02 只跑「需求卡片申报 ∪ diff 推导」并集套件、00 冒烟/01 系统测试/09 静态扫描三件保险恒跑、横切面回落全量——算法见闸脚本),手动跑不构成验收。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占 CI 全量——其质量义务=落批时人工确认闸可跑通且判据口径未变;闸真崩溃时远端 CI 判红在案(job 失败=verdict 红,fail-closed 语义保留) +9. **重跑语义(2026-10-30 用户裁,取代原「每批一次/合并后零复跑」口径)**——同一份字节禁止第二次人手跑;允许的重跑只剩两类:① CI 红后的修复合回(post-merge 钩子再推再验,每轮修复至多触发一轮);② 用户自发发起的全量回归。「跑都跑了再跑一遍保险」「顺手改点再补一跑」都不构成重跑理由。导入缓存刷新由远端工作流「导入缓存刷新」步接管,合并后无手工动作 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-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter-反哺点-<日期>.md`(每条写清哪个文件哪一节、改成什么、依据是什么)。设计权威文档(GDD/art-bible)**任何批都不直改**:用户在会话中逐点裁决后,由工程/文档设计改动批(直改类)写入 `design/`(执行入口=doc-design 技能),并把反哺条目从 `reports/` 清掉(吸收即死;退役执行=batch-closure 技能)。机械面文档(roadmap 行/术语表/README 工具注)同样归工程/文档设计改动批在合并前更新。反哺点未落地期间相关 GDD 节视为待反哺,读前先查 `reports/` 与待办清单;需求卡片必列「关联文档同步面」按两层标注 12. **实机目检归用户,只登记不阻塞**——真人检测项登记进实现报告目检总清单与体验验证清单即完成批内义务;禁把等目检当停工理由或完成前置 @@ -62,7 +62,7 @@ **MC/verifier 触发细则**:仅在数值验证场景触发(改数值前后取证);normal 档为主、perfect 按需;**无数值改动的会话禁跑 MC**;结构位移类「存档锚」须先向用户请示定性再跑;需求卡片预设步骤的前提消失时(如「带值裁定后重锚」实际未投值)停问勿照字面执行。 -**分级周期口径**:全部验证闸收编在 `tools/run_all_gates.sh` 一个入口下(**脚本即清单,且即域划分的唯一出处**),分三档——`--code`/`--docs` 按本批改动域只跑该域闸(闸件清单与域划分只写在脚本头部注释,本文不重列);`--quick` 是全量档的中段自检形态(跳过渲染闸与静态扫描);**不带参数=两域全量**。强制验证点=合回后本地 CI 的全量档(§1 第 2 条);分档只服务开发期手动跑批。真人实机目检归用户不进自动闸。 +**分级周期口径**:全部验证闸收编在 `tools/run_all_gates.sh` 一个入口下(**脚本即清单,且即域划分的唯一出处**),分三档——`--code`/`--docs` 按本批改动域只跑该域闸(闸件清单与域划分只写在脚本头部注释,本文不重列);`--quick` 是全量档的中段自检形态(跳过渲染闸与静态扫描);**不带参数=两域全量**。强制验证点=合回后远端 CI(游戏码批走全量档,细则见 §1 第 3 条);分档只服务开发期手动跑批。真人实机目检归用户不进自动闸。 ## 5. MC 采集档位细则 diff --git a/design/engineering/test-conventions.md b/design/engineering/test-conventions.md index 039058c1..f9727317 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -25,9 +25,9 @@ 每个缺陷修复必须在探针或冒烟中留下能抓住该缺陷的断言(例:封印动作耗尽 AP → 必须进入第 2 轮; 选用道具 → 库存态必须变 Used)。没有回归断言的修复视为未完成。 -## 4. 验证时机(合回后本地 CI 制,2026-10-30 换代) +## 4. 验证时机(合回后远端 CI 制,2026-11-03 换代) -开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,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)。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。 +开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,process-conventions §6)。全量验证归合回后的远端 CI:git merge 合回 master 落地后 post-merge 钩子推送 origin master,远端按改动域分流跑 `tools/run_all_gates.sh`(游戏码批=不带参数档两域全量、02 全套件——跨卡触面与关联档选测的盲区由全量兜底;文档批=文档域秒级闸;分流算法单点=`tools/ci/domain_route.sh`),回执由 `tools/ci_watch.sh remote` 轮询写回 `game/tests/.gates/ci-status.json`;CI 红态下修复合回落地即再推再验追绿(修复走新开 `fix-<任务名>` 修复 worktree,process-conventions §1;本地跑批件 ci_runner.sh 与本地追绿钩子 post-commit 已退役)。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。 行为探针使用随机化玩家行为(封印/停手/刷新/无效点击)——自动玩家不做的动作就是测试盲区。 ## 5. 测试函数怎么写——验收清单条目的落码规范 @@ -99,6 +99,8 @@ **能力清单维护锚(2026-10-29 用户裁改版定稿)**:`game/tests/README.md`「能力清单」是**实现指导+阅读导航+义务声明**——只宣布「要验证什么」,不记「是否已验证/是否跑过」;落地账归现行验收闭合链(改动—验收对照表逐条挂断言+合回前独立验收审计),本清单不承载验证状态。表为四列:能力|系统|真实入口|代表用例(`文件:函数`)。**维护归写卡段,当场入表**:写需求卡片时,本批新增能力当场写入能力行——真实入口取自设计文档已定的入口,代表用例=卡里定死的计划测试(`文件:函数`);受影响存量行(入口/用例名变更)同批更新;能力退役删行。**实现照单落名**:测试函数名以清单为唯一依据,名实不符=卡片没写清=停卡退回。**收尾核对**:批末收尾只核对表上名字与实际落地一致(改名则回表),不刷任何批次字段;实现批禁写该文件的现行红线不变(process-conventions §3 第 11 条),核对义务归收尾提交。三层模型见 §8.9。改动—验收对照表纪律不变:新增或改写的每条断言仍须挂一行(上文「测试侧」);断言改判仍受 §8.4 真实入口口径约束。 +**新增 core 件的覆盖登记义务(2026-11-03 架构审查 P1 余项裁)**:拆件或重构产出的新 core 文件不触发 process-conventions §3 第 6 条「新功能必配新测试」(那条只管新增玩法/机制),因此单由本条承接——新 core 件要么被 `game/tests/` 某套件按文件名或 `class_name` 点名,要么在承接它的能力行「代表用例」里申报「经宿主链覆盖(宿主=<件>)」;两者都无=批末收尾核对时补登。本条**不是覆盖率要求**,与 §7「覆盖率不是目标」同轨:登记的是能力义务的归属,不做文件计数——「每个 core 件必须被点名一次」的机器闸已裁否(它逼出为点名而点名的空套件,且与 §7 正面冲突)。落笔面沿用现行分工:申报行由写卡段入能力清单(实现批禁写该文件),核对归批末收尾提交。 + ### 8.4 真实入口断言(验收闭合的主判据) - **唯一口径(2026-10-24/2026-10-28 用户两裁)**:每个需求点与每个行为改动的验收=一条从真实入口进入的断言,可 grep 定位、在当轮真实跑批中执行并通过。**禁变异类测试**(把生产码或数据表改成错值看断言抓不抓得到);**禁撤销类取证**(回退本批改动或该 hunk 看断言红不红)——两者都是人为制造红,属伪造证据,一律禁止。 diff --git a/design/gdd/systems/冒险循环/奇物.md b/design/gdd/systems/冒险循环/奇物.md index 025feff7..2226a8d5 100644 --- a/design/gdd/systems/冒险循环/奇物.md +++ b/design/gdd/systems/冒险循环/奇物.md @@ -11,7 +11,7 @@ > > **兑付频率(先看这条,它是三条渠道的共同前提)**:奇物**不是打一场战斗给一份**。关内的普通战斗节点打完了什么也不给;一关之内奇物**只兑付一次**,触发点是**关底 Boss 被击败**——而按 [冒险结构](冒险结构.md) 的定义,打掉关底 Boss 恰好就等于"通过这一关",这两件事是同一个时刻,不是一条链上的两步。 > -> 状态:**已实现**(2026-10-04 双层 BD 实施批落地);现行为 v1.4。本文件是奇物的唯一定义文档。 +> 状态:**已实现**(2026-10-04 双层 BD 实施批落地);现行为 v1.5。本文件是奇物的唯一定义文档。 > > 数值纪律:本文件只写概念、取值域与指路;每件给多少、效果是什么一律不进正文,事实源是 `data/tables/feats.tres` 与 `data/tables/adventure.tres`。 @@ -164,7 +164,7 @@ > 令牌牌库(奖励节点与商店共用)的组织与三选一抽取规则(2026-10-07 用户裁案D——归档打底做翼 + 曝光槽)。 - **令牌=出现次数上限**:每个成员在牌库里有 N 张令牌——拿了消耗、**没拿回池**(刷新重抽同样不消耗),所以「最多被拿 N 次」= 池里放 N 张,天然成立,不另设计数器;「只刷出、没拿」不计次数(现行 TP0 语义沿用)。 -- **概率与上限解耦**(设计约束,新内容必检):每个成员标注**两个独立属性**——概率位(档+权重/令牌数)与已拿上限(cap;拿了才计、刷出不计)。概率不决定上限、上限不影响概率。 +- **概率与上限解耦**(设计约束,新内容必检):每个成员标注**两个独立属性**——概率位(档+权重/令牌数)与已拿上限(cap;拿了才计、刷出不计。cap 必填——成员行缺 cap 声明=数据错误、拒绝加载,取值域 [1,99];2026-11-02 用户裁「不要任何缺省值」总判据)。概率不决定上限、上限不影响概率。 - **已知耦合问题(实装批选型注意)**:现行令牌模型里「N 张令牌」同时承担概率与上限两个职责,无法独立设定(例:档概率需 3 张 vs 单件限拿 2 次——3 张压不住上限、2 张保不住概率)。实现路径候选三案:①抽象权重表+独立已拿计数器(解耦彻底;代价=推翻 TP0 实体牌库模型、「池耗即上限」语义改写)②令牌池+提前退役计数器(保留模型;代价=名义概率与有效概率两层、对账复杂化)③维持耦合(设计自我设限——用户已否)。选型归实装批;影响面=MC 对账模型与断点存档(已拿计数器须进断点附随件)。 - **内容归档(打底+做翼)**:普通档=属性件+小额钱(每拿都有底的面粉);优秀/稀有档=功能凭证与改手感件(期待位);传说档=惊喜位。归档按决策影响力、不按数值大小;面值数值归调参批。 - **曝光槽**:每次三选一呈现 = **2 格按全池抽 + 1 格按「优秀档以上」子池抽**;高档子池空 → 该格回退按全池抽(池空直通同族兜底)。格数与档配比归调参批。 diff --git a/design/gdd/systems/战斗循环/增益状态.md b/design/gdd/systems/战斗循环/增益状态.md index f4abdd3b..c2e432cd 100644 --- a/design/gdd/systems/战斗循环/增益状态.md +++ b/design/gdd/systems/战斗循环/增益状态.md @@ -1,7 +1,7 @@ # 增益状态 > 一句话:增益/减益的状态池、叠层与衰减 -> 状态:已定稿(自旧体系迁移);现行为 v1.5 +> 状态:已定稿(自旧体系迁移);现行为 v1.6 ## 设计目的 @@ -112,7 +112,6 @@ | 合并匹配认个体 | 同名状态只在同一个体名下按合并策略结算;不同个体同名实例互不相干、互不可见 | | 个体死亡即清 | 个体死亡,其名下实例全部随体移除,不转移、不并回别处 | | 呈现各读各的 | 每只立绘的状态图标行只读它自己名下的实例;结算明细行归因照旧,单体授予写明目标是哪一只 | -| 实装随该批 | 本节为定稿口径;实装随敌方状态个体化实装批进游戏——合入前行为仍为旧共享态(对敌效果全体一条实例) | 举证:本节为结构规则与语义裁定,无计算。 diff --git a/game/README.md b/game/README.md index 962962a5..4ce89c8d 100644 --- a/game/README.md +++ b/game/README.md @@ -50,7 +50,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://sim/adv **试玩画像口径(2026-09-19 用户拍板固化——全项目试玩/扫描/面板模拟一律只用以下两档,随机策略口径退役)**:`policy="normal"` = **普通画像·基础定式**(单数字满足规则——满足即标/满足即开,无处可推盲挖边界格、不刷新;教学后正常玩家的下限近似);`policy="perfect"` = **完美画像**(single+subset 全推理零盲挖/卡死刷新——技能上界仪)。实现单一出口 = `sim/auto_player.gd`(novice_action / perfect_action)经 `sim/level_autoplay.gd`,调试浮窗批量模拟与各扫描工具自动同口径;战斗层冒烟夹具与调试转储的 `choose_dig` 直调是机制测试夹具、不承载画像语义。分级依据与新旧读数(乱点 0%/5% → 基础定式 65%/90%)见 auto_player.gd 普通策略档节与台账 77。 单关诊断工具(level_debug.gd)随单关直入口口径退役(2026-09-23)——败退归因走 balance_verifier 冒险段(层/节点/敌人人读归因)。 -战斗级工具(v1 遗产):`dump_battle.gd`(结算流水)/ `monte_carlo.gd`(战斗期望)/ `experiment_density.gd`(密度调参)。 +战斗级工具(v1 遗产):`dump_battle.gd`(结算流水)/ `monte_carlo.gd`(战斗期望)。 ## 范围(v1 范围 + v2 追加,用户 2026-09-10/11 确认) diff --git a/game/assets/art/board/seal_mark.png b/game/assets/art/board/seal_mark.png new file mode 100644 index 00000000..ddf875e1 Binary files /dev/null and b/game/assets/art/board/seal_mark.png differ diff --git a/game/assets/art/board/seal_mark.png.import b/game/assets/art/board/seal_mark.png.import new file mode 100644 index 00000000..769ad7d8 --- /dev/null +++ b/game/assets/art/board/seal_mark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://7mgi36hqrvej" +path="res://.godot/imported/seal_mark.png-4e00c69f08466c44d297a28c90752754.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/seal_mark.png" +dest_files=["res://.godot/imported/seal_mark.png-4e00c69f08466c44d297a28c90752754.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_deep_underdark_1.png b/game/assets/art/board/terrain_floor_deep_underdark_1.png new file mode 100644 index 00000000..79ee9353 Binary files /dev/null and b/game/assets/art/board/terrain_floor_deep_underdark_1.png differ diff --git a/game/assets/art/board/terrain_floor_deep_underdark_1.png.import b/game/assets/art/board/terrain_floor_deep_underdark_1.png.import new file mode 100644 index 00000000..4cb607d9 --- /dev/null +++ b/game/assets/art/board/terrain_floor_deep_underdark_1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bqvh7631mcuaj" +path="res://.godot/imported/terrain_floor_deep_underdark_1.png-c1b190b8e4b3895184bdd0ee6bd74f82.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_deep_underdark_1.png" +dest_files=["res://.godot/imported/terrain_floor_deep_underdark_1.png-c1b190b8e4b3895184bdd0ee6bd74f82.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_deep_underdark_2.png b/game/assets/art/board/terrain_floor_deep_underdark_2.png new file mode 100644 index 00000000..854ae1c9 Binary files /dev/null and b/game/assets/art/board/terrain_floor_deep_underdark_2.png differ diff --git a/game/assets/art/board/terrain_floor_deep_underdark_2.png.import b/game/assets/art/board/terrain_floor_deep_underdark_2.png.import new file mode 100644 index 00000000..ab154447 --- /dev/null +++ b/game/assets/art/board/terrain_floor_deep_underdark_2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://x6sl5ht6n2uc" +path="res://.godot/imported/terrain_floor_deep_underdark_2.png-c0469b1cf1af026328686618be34f5d3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_deep_underdark_2.png" +dest_files=["res://.godot/imported/terrain_floor_deep_underdark_2.png-c0469b1cf1af026328686618be34f5d3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_deep_underdark_3.png b/game/assets/art/board/terrain_floor_deep_underdark_3.png new file mode 100644 index 00000000..451109dc Binary files /dev/null and b/game/assets/art/board/terrain_floor_deep_underdark_3.png differ diff --git a/game/assets/art/board/terrain_floor_deep_underdark_3.png.import b/game/assets/art/board/terrain_floor_deep_underdark_3.png.import new file mode 100644 index 00000000..6693993a --- /dev/null +++ b/game/assets/art/board/terrain_floor_deep_underdark_3.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://blj41w52x8poi" +path="res://.godot/imported/terrain_floor_deep_underdark_3.png-f159b9c2d516f78ef7d19b05a4631d11.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_deep_underdark_3.png" +dest_files=["res://.godot/imported/terrain_floor_deep_underdark_3.png-f159b9c2d516f78ef7d19b05a4631d11.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_deep_underdark_4.png b/game/assets/art/board/terrain_floor_deep_underdark_4.png new file mode 100644 index 00000000..436dd4f0 Binary files /dev/null and b/game/assets/art/board/terrain_floor_deep_underdark_4.png differ diff --git a/game/assets/art/board/terrain_floor_deep_underdark_4.png.import b/game/assets/art/board/terrain_floor_deep_underdark_4.png.import new file mode 100644 index 00000000..8f1a2858 --- /dev/null +++ b/game/assets/art/board/terrain_floor_deep_underdark_4.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b3bwubcxsl5jq" +path="res://.godot/imported/terrain_floor_deep_underdark_4.png-f5f15b985679a255ff00e1b8688e15b2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_deep_underdark_4.png" +dest_files=["res://.godot/imported/terrain_floor_deep_underdark_4.png-f5f15b985679a255ff00e1b8688e15b2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_deep_underdark_5.png b/game/assets/art/board/terrain_floor_deep_underdark_5.png new file mode 100644 index 00000000..eb1ca36e Binary files /dev/null and b/game/assets/art/board/terrain_floor_deep_underdark_5.png differ diff --git a/game/assets/art/board/terrain_floor_deep_underdark_5.png.import b/game/assets/art/board/terrain_floor_deep_underdark_5.png.import new file mode 100644 index 00000000..e7cc8e97 --- /dev/null +++ b/game/assets/art/board/terrain_floor_deep_underdark_5.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cv07q7w556bwy" +path="res://.godot/imported/terrain_floor_deep_underdark_5.png-143a53136daea878fff67d8ac9e05c42.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_deep_underdark_5.png" +dest_files=["res://.godot/imported/terrain_floor_deep_underdark_5.png-143a53136daea878fff67d8ac9e05c42.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_dungeon_1.png b/game/assets/art/board/terrain_floor_dungeon_1.png new file mode 100644 index 00000000..4851e3bc Binary files /dev/null and b/game/assets/art/board/terrain_floor_dungeon_1.png differ diff --git a/game/assets/art/board/terrain_floor_dungeon_1.png.import b/game/assets/art/board/terrain_floor_dungeon_1.png.import new file mode 100644 index 00000000..9f14b819 --- /dev/null +++ b/game/assets/art/board/terrain_floor_dungeon_1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b65onwkof3wsy" +path="res://.godot/imported/terrain_floor_dungeon_1.png-40bb7fc26d6ced997a8f1cc9005b6f18.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_dungeon_1.png" +dest_files=["res://.godot/imported/terrain_floor_dungeon_1.png-40bb7fc26d6ced997a8f1cc9005b6f18.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_dungeon_2.png b/game/assets/art/board/terrain_floor_dungeon_2.png new file mode 100644 index 00000000..3211eb39 Binary files /dev/null and b/game/assets/art/board/terrain_floor_dungeon_2.png differ diff --git a/game/assets/art/board/terrain_floor_dungeon_2.png.import b/game/assets/art/board/terrain_floor_dungeon_2.png.import new file mode 100644 index 00000000..b14ebbf7 --- /dev/null +++ b/game/assets/art/board/terrain_floor_dungeon_2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ddqxia2sv5ktq" +path="res://.godot/imported/terrain_floor_dungeon_2.png-c4869c7b250ede2771251e1c17af6563.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_dungeon_2.png" +dest_files=["res://.godot/imported/terrain_floor_dungeon_2.png-c4869c7b250ede2771251e1c17af6563.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_dungeon_3.png b/game/assets/art/board/terrain_floor_dungeon_3.png new file mode 100644 index 00000000..bb53aa6e Binary files /dev/null and b/game/assets/art/board/terrain_floor_dungeon_3.png differ diff --git a/game/assets/art/board/terrain_floor_dungeon_3.png.import b/game/assets/art/board/terrain_floor_dungeon_3.png.import new file mode 100644 index 00000000..5f011ca0 --- /dev/null +++ b/game/assets/art/board/terrain_floor_dungeon_3.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bieystjeu21q4" +path="res://.godot/imported/terrain_floor_dungeon_3.png-148125c305ac1d93b24d178431bf294e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_dungeon_3.png" +dest_files=["res://.godot/imported/terrain_floor_dungeon_3.png-148125c305ac1d93b24d178431bf294e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_dungeon_4.png b/game/assets/art/board/terrain_floor_dungeon_4.png new file mode 100644 index 00000000..601aaedf Binary files /dev/null and b/game/assets/art/board/terrain_floor_dungeon_4.png differ diff --git a/game/assets/art/board/terrain_floor_dungeon_4.png.import b/game/assets/art/board/terrain_floor_dungeon_4.png.import new file mode 100644 index 00000000..4858a3c7 --- /dev/null +++ b/game/assets/art/board/terrain_floor_dungeon_4.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://q5eko1txuoq5" +path="res://.godot/imported/terrain_floor_dungeon_4.png-3fe81a46ece4b02dc031045919a12259.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_dungeon_4.png" +dest_files=["res://.godot/imported/terrain_floor_dungeon_4.png-3fe81a46ece4b02dc031045919a12259.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_dungeon_5.png b/game/assets/art/board/terrain_floor_dungeon_5.png new file mode 100644 index 00000000..b830793d Binary files /dev/null and b/game/assets/art/board/terrain_floor_dungeon_5.png differ diff --git a/game/assets/art/board/terrain_floor_dungeon_5.png.import b/game/assets/art/board/terrain_floor_dungeon_5.png.import new file mode 100644 index 00000000..f2e5f169 --- /dev/null +++ b/game/assets/art/board/terrain_floor_dungeon_5.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bvyjsdrcsysi1" +path="res://.godot/imported/terrain_floor_dungeon_5.png-de6614fc89049d19ea41225a836f9d75.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_dungeon_5.png" +dest_files=["res://.godot/imported/terrain_floor_dungeon_5.png-de6614fc89049d19ea41225a836f9d75.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_underdark_1.png b/game/assets/art/board/terrain_floor_underdark_1.png new file mode 100644 index 00000000..58bbef8f Binary files /dev/null and b/game/assets/art/board/terrain_floor_underdark_1.png differ diff --git a/game/assets/art/board/terrain_floor_underdark_1.png.import b/game/assets/art/board/terrain_floor_underdark_1.png.import new file mode 100644 index 00000000..d247f281 --- /dev/null +++ b/game/assets/art/board/terrain_floor_underdark_1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://tql72osnj7b4" +path="res://.godot/imported/terrain_floor_underdark_1.png-f600aa8d0e421e81abdeacc2ca1e684d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_underdark_1.png" +dest_files=["res://.godot/imported/terrain_floor_underdark_1.png-f600aa8d0e421e81abdeacc2ca1e684d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_underdark_2.png b/game/assets/art/board/terrain_floor_underdark_2.png new file mode 100644 index 00000000..bc81def6 Binary files /dev/null and b/game/assets/art/board/terrain_floor_underdark_2.png differ diff --git a/game/assets/art/board/terrain_floor_underdark_2.png.import b/game/assets/art/board/terrain_floor_underdark_2.png.import new file mode 100644 index 00000000..5d235be2 --- /dev/null +++ b/game/assets/art/board/terrain_floor_underdark_2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b5wep4y81o1jw" +path="res://.godot/imported/terrain_floor_underdark_2.png-af44e8e4b98a05a2d39075dbcd9d4e01.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_underdark_2.png" +dest_files=["res://.godot/imported/terrain_floor_underdark_2.png-af44e8e4b98a05a2d39075dbcd9d4e01.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_underdark_3.png b/game/assets/art/board/terrain_floor_underdark_3.png new file mode 100644 index 00000000..fed5e8fd Binary files /dev/null and b/game/assets/art/board/terrain_floor_underdark_3.png differ diff --git a/game/assets/art/board/terrain_floor_underdark_3.png.import b/game/assets/art/board/terrain_floor_underdark_3.png.import new file mode 100644 index 00000000..37166c30 --- /dev/null +++ b/game/assets/art/board/terrain_floor_underdark_3.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dui4ogsp407gh" +path="res://.godot/imported/terrain_floor_underdark_3.png-2b02c10af1a0cdae05a3e6c4b62383db.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_underdark_3.png" +dest_files=["res://.godot/imported/terrain_floor_underdark_3.png-2b02c10af1a0cdae05a3e6c4b62383db.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_underdark_4.png b/game/assets/art/board/terrain_floor_underdark_4.png new file mode 100644 index 00000000..827034ce Binary files /dev/null and b/game/assets/art/board/terrain_floor_underdark_4.png differ diff --git a/game/assets/art/board/terrain_floor_underdark_4.png.import b/game/assets/art/board/terrain_floor_underdark_4.png.import new file mode 100644 index 00000000..1b28f0fd --- /dev/null +++ b/game/assets/art/board/terrain_floor_underdark_4.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cmhm5txfhgwx3" +path="res://.godot/imported/terrain_floor_underdark_4.png-f1fdca02c60c236ff6c7656d2c63a42e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_underdark_4.png" +dest_files=["res://.godot/imported/terrain_floor_underdark_4.png-f1fdca02c60c236ff6c7656d2c63a42e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_floor_underdark_5.png b/game/assets/art/board/terrain_floor_underdark_5.png new file mode 100644 index 00000000..b9f29454 Binary files /dev/null and b/game/assets/art/board/terrain_floor_underdark_5.png differ diff --git a/game/assets/art/board/terrain_floor_underdark_5.png.import b/game/assets/art/board/terrain_floor_underdark_5.png.import new file mode 100644 index 00000000..e36416ea --- /dev/null +++ b/game/assets/art/board/terrain_floor_underdark_5.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://l2rq0unla3uv" +path="res://.godot/imported/terrain_floor_underdark_5.png-b581b01cb084ee52178ca96663e73933.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_floor_underdark_5.png" +dest_files=["res://.godot/imported/terrain_floor_underdark_5.png-b581b01cb084ee52178ca96663e73933.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_center_crack_deep_underdark.png b/game/assets/art/board/terrain_wall_center_crack_deep_underdark.png new file mode 100644 index 00000000..12ed78a6 Binary files /dev/null and b/game/assets/art/board/terrain_wall_center_crack_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_center_crack_deep_underdark.png.import b/game/assets/art/board/terrain_wall_center_crack_deep_underdark.png.import new file mode 100644 index 00000000..50457097 --- /dev/null +++ b/game/assets/art/board/terrain_wall_center_crack_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://rgaccyhfmihw" +path="res://.godot/imported/terrain_wall_center_crack_deep_underdark.png-2d56722bbd6050f6b795a625b3edaa6c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_center_crack_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_center_crack_deep_underdark.png-2d56722bbd6050f6b795a625b3edaa6c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_center_crack_dungeon.png b/game/assets/art/board/terrain_wall_center_crack_dungeon.png new file mode 100644 index 00000000..2aee016e Binary files /dev/null and b/game/assets/art/board/terrain_wall_center_crack_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_center_crack_dungeon.png.import b/game/assets/art/board/terrain_wall_center_crack_dungeon.png.import new file mode 100644 index 00000000..1c877644 --- /dev/null +++ b/game/assets/art/board/terrain_wall_center_crack_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cgr1rjgu7yw3v" +path="res://.godot/imported/terrain_wall_center_crack_dungeon.png-df45d525c0fd838550460540defe30f2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_center_crack_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_center_crack_dungeon.png-df45d525c0fd838550460540defe30f2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_center_crack_underdark.png b/game/assets/art/board/terrain_wall_center_crack_underdark.png new file mode 100644 index 00000000..d17ef75a Binary files /dev/null and b/game/assets/art/board/terrain_wall_center_crack_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_center_crack_underdark.png.import b/game/assets/art/board/terrain_wall_center_crack_underdark.png.import new file mode 100644 index 00000000..473ea274 --- /dev/null +++ b/game/assets/art/board/terrain_wall_center_crack_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bv36y7mva45uy" +path="res://.godot/imported/terrain_wall_center_crack_underdark.png-8ca77a6201e1431a0db830c2275ad51e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_center_crack_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_center_crack_underdark.png-8ca77a6201e1431a0db830c2275ad51e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_center_deep_underdark.png b/game/assets/art/board/terrain_wall_center_deep_underdark.png new file mode 100644 index 00000000..2c61cf83 Binary files /dev/null and b/game/assets/art/board/terrain_wall_center_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_center_deep_underdark.png.import b/game/assets/art/board/terrain_wall_center_deep_underdark.png.import new file mode 100644 index 00000000..26be9c37 --- /dev/null +++ b/game/assets/art/board/terrain_wall_center_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dvhyhx04wiouj" +path="res://.godot/imported/terrain_wall_center_deep_underdark.png-21dba86720ce186d7016925f8a498d80.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_center_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_center_deep_underdark.png-21dba86720ce186d7016925f8a498d80.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_center_dungeon.png b/game/assets/art/board/terrain_wall_center_dungeon.png new file mode 100644 index 00000000..796b1c84 Binary files /dev/null and b/game/assets/art/board/terrain_wall_center_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_center_dungeon.png.import b/game/assets/art/board/terrain_wall_center_dungeon.png.import new file mode 100644 index 00000000..4ec18e73 --- /dev/null +++ b/game/assets/art/board/terrain_wall_center_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://qxf2g7lov6th" +path="res://.godot/imported/terrain_wall_center_dungeon.png-658b0d351ec8a12f85a4143d159fb3fd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_center_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_center_dungeon.png-658b0d351ec8a12f85a4143d159fb3fd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_center_speckle_deep_underdark.png b/game/assets/art/board/terrain_wall_center_speckle_deep_underdark.png new file mode 100644 index 00000000..abf7f96f Binary files /dev/null and b/game/assets/art/board/terrain_wall_center_speckle_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_center_speckle_deep_underdark.png.import b/game/assets/art/board/terrain_wall_center_speckle_deep_underdark.png.import new file mode 100644 index 00000000..3ed807d9 --- /dev/null +++ b/game/assets/art/board/terrain_wall_center_speckle_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://l7y5j2ksy8ax" +path="res://.godot/imported/terrain_wall_center_speckle_deep_underdark.png-18c3524fcd66d9ed2ed95e2e87467104.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_center_speckle_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_center_speckle_deep_underdark.png-18c3524fcd66d9ed2ed95e2e87467104.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_center_speckle_dungeon.png b/game/assets/art/board/terrain_wall_center_speckle_dungeon.png new file mode 100644 index 00000000..cfb85b97 Binary files /dev/null and b/game/assets/art/board/terrain_wall_center_speckle_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_center_speckle_dungeon.png.import b/game/assets/art/board/terrain_wall_center_speckle_dungeon.png.import new file mode 100644 index 00000000..2a6ba912 --- /dev/null +++ b/game/assets/art/board/terrain_wall_center_speckle_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cf8sm7ryxvgph" +path="res://.godot/imported/terrain_wall_center_speckle_dungeon.png-2ccd555fef371b488c3dc218ae241295.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_center_speckle_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_center_speckle_dungeon.png-2ccd555fef371b488c3dc218ae241295.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_center_speckle_underdark.png b/game/assets/art/board/terrain_wall_center_speckle_underdark.png new file mode 100644 index 00000000..5bd9d774 Binary files /dev/null and b/game/assets/art/board/terrain_wall_center_speckle_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_center_speckle_underdark.png.import b/game/assets/art/board/terrain_wall_center_speckle_underdark.png.import new file mode 100644 index 00000000..c84df32c --- /dev/null +++ b/game/assets/art/board/terrain_wall_center_speckle_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://db0pevpne83kk" +path="res://.godot/imported/terrain_wall_center_speckle_underdark.png-a6e4207e2f82bef11f1ec867f81aeef8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_center_speckle_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_center_speckle_underdark.png-a6e4207e2f82bef11f1ec867f81aeef8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_center_underdark.png b/game/assets/art/board/terrain_wall_center_underdark.png new file mode 100644 index 00000000..1cd39d08 Binary files /dev/null and b/game/assets/art/board/terrain_wall_center_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_center_underdark.png.import b/game/assets/art/board/terrain_wall_center_underdark.png.import new file mode 100644 index 00000000..4670f55d --- /dev/null +++ b/game/assets/art/board/terrain_wall_center_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cvo0jytgh8bgh" +path="res://.godot/imported/terrain_wall_center_underdark.png-4586560d6659927249e74c4aef1c55db.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_center_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_center_underdark.png-4586560d6659927249e74c4aef1c55db.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_end_n_deep_underdark.png b/game/assets/art/board/terrain_wall_end_n_deep_underdark.png new file mode 100644 index 00000000..8d710978 Binary files /dev/null and b/game/assets/art/board/terrain_wall_end_n_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_end_n_deep_underdark.png.import b/game/assets/art/board/terrain_wall_end_n_deep_underdark.png.import new file mode 100644 index 00000000..72947459 --- /dev/null +++ b/game/assets/art/board/terrain_wall_end_n_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ebb36udrrlco" +path="res://.godot/imported/terrain_wall_end_n_deep_underdark.png-953a459c1d7d70cb54ff21d6e65e947e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_end_n_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_end_n_deep_underdark.png-953a459c1d7d70cb54ff21d6e65e947e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_end_n_dungeon.png b/game/assets/art/board/terrain_wall_end_n_dungeon.png new file mode 100644 index 00000000..bc126ab6 Binary files /dev/null and b/game/assets/art/board/terrain_wall_end_n_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_end_n_dungeon.png.import b/game/assets/art/board/terrain_wall_end_n_dungeon.png.import new file mode 100644 index 00000000..5ed9e401 --- /dev/null +++ b/game/assets/art/board/terrain_wall_end_n_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bjkoixrqxyfvi" +path="res://.godot/imported/terrain_wall_end_n_dungeon.png-0292ee08e0a9524fae5fce50c83e27a0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_end_n_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_end_n_dungeon.png-0292ee08e0a9524fae5fce50c83e27a0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_end_n_underdark.png b/game/assets/art/board/terrain_wall_end_n_underdark.png new file mode 100644 index 00000000..d2ba1c50 Binary files /dev/null and b/game/assets/art/board/terrain_wall_end_n_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_end_n_underdark.png.import b/game/assets/art/board/terrain_wall_end_n_underdark.png.import new file mode 100644 index 00000000..ac11cbac --- /dev/null +++ b/game/assets/art/board/terrain_wall_end_n_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dwisuhgfosirs" +path="res://.godot/imported/terrain_wall_end_n_underdark.png-623c49a4e05a066226c2dfb6e066d735.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_end_n_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_end_n_underdark.png-623c49a4e05a066226c2dfb6e066d735.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_end_s_deep_underdark.png b/game/assets/art/board/terrain_wall_end_s_deep_underdark.png new file mode 100644 index 00000000..36030068 Binary files /dev/null and b/game/assets/art/board/terrain_wall_end_s_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_end_s_deep_underdark.png.import b/game/assets/art/board/terrain_wall_end_s_deep_underdark.png.import new file mode 100644 index 00000000..f22138c3 --- /dev/null +++ b/game/assets/art/board/terrain_wall_end_s_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c3jin8ytakl8a" +path="res://.godot/imported/terrain_wall_end_s_deep_underdark.png-6c06ef145e41ecfc9bbdfc330ea28e29.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_end_s_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_end_s_deep_underdark.png-6c06ef145e41ecfc9bbdfc330ea28e29.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_end_s_dungeon.png b/game/assets/art/board/terrain_wall_end_s_dungeon.png new file mode 100644 index 00000000..13618f17 Binary files /dev/null and b/game/assets/art/board/terrain_wall_end_s_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_end_s_dungeon.png.import b/game/assets/art/board/terrain_wall_end_s_dungeon.png.import new file mode 100644 index 00000000..9bd24b30 --- /dev/null +++ b/game/assets/art/board/terrain_wall_end_s_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dxu6my20xmrcw" +path="res://.godot/imported/terrain_wall_end_s_dungeon.png-57e9f35af5ee5188529d19914d6ca77c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_end_s_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_end_s_dungeon.png-57e9f35af5ee5188529d19914d6ca77c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_end_s_underdark.png b/game/assets/art/board/terrain_wall_end_s_underdark.png new file mode 100644 index 00000000..60d01669 Binary files /dev/null and b/game/assets/art/board/terrain_wall_end_s_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_end_s_underdark.png.import b/game/assets/art/board/terrain_wall_end_s_underdark.png.import new file mode 100644 index 00000000..c0345477 --- /dev/null +++ b/game/assets/art/board/terrain_wall_end_s_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b4vqrhnwi6dt" +path="res://.godot/imported/terrain_wall_end_s_underdark.png-bc9e276e09623b0334da8ed6dfe0eab1.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_end_s_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_end_s_underdark.png-bc9e276e09623b0334da8ed6dfe0eab1.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_end_w_deep_underdark.png b/game/assets/art/board/terrain_wall_end_w_deep_underdark.png new file mode 100644 index 00000000..3901d0c2 Binary files /dev/null and b/game/assets/art/board/terrain_wall_end_w_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_end_w_deep_underdark.png.import b/game/assets/art/board/terrain_wall_end_w_deep_underdark.png.import new file mode 100644 index 00000000..55fec3be --- /dev/null +++ b/game/assets/art/board/terrain_wall_end_w_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bii82y3sk4grg" +path="res://.godot/imported/terrain_wall_end_w_deep_underdark.png-d8d8a5cb939f178b0b824311d090e725.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_end_w_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_end_w_deep_underdark.png-d8d8a5cb939f178b0b824311d090e725.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_end_w_dungeon.png b/game/assets/art/board/terrain_wall_end_w_dungeon.png new file mode 100644 index 00000000..34aa583f Binary files /dev/null and b/game/assets/art/board/terrain_wall_end_w_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_end_w_dungeon.png.import b/game/assets/art/board/terrain_wall_end_w_dungeon.png.import new file mode 100644 index 00000000..6ab2eb30 --- /dev/null +++ b/game/assets/art/board/terrain_wall_end_w_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b65l0bhfd558i" +path="res://.godot/imported/terrain_wall_end_w_dungeon.png-3aad2e0c1657cf6eebf27b020b395b2c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_end_w_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_end_w_dungeon.png-3aad2e0c1657cf6eebf27b020b395b2c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_end_w_underdark.png b/game/assets/art/board/terrain_wall_end_w_underdark.png new file mode 100644 index 00000000..56002e77 Binary files /dev/null and b/game/assets/art/board/terrain_wall_end_w_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_end_w_underdark.png.import b/game/assets/art/board/terrain_wall_end_w_underdark.png.import new file mode 100644 index 00000000..5f3c32d9 --- /dev/null +++ b/game/assets/art/board/terrain_wall_end_w_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cd5s6k3b7im7e" +path="res://.godot/imported/terrain_wall_end_w_underdark.png-9186bc905359de8487544281666215d5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_end_w_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_end_w_underdark.png-9186bc905359de8487544281666215d5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_hrun_deep_underdark.png b/game/assets/art/board/terrain_wall_hrun_deep_underdark.png new file mode 100644 index 00000000..7b9ea546 Binary files /dev/null and b/game/assets/art/board/terrain_wall_hrun_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_hrun_deep_underdark.png.import b/game/assets/art/board/terrain_wall_hrun_deep_underdark.png.import new file mode 100644 index 00000000..cfe6fe84 --- /dev/null +++ b/game/assets/art/board/terrain_wall_hrun_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b5p724r8nn4d0" +path="res://.godot/imported/terrain_wall_hrun_deep_underdark.png-0420654e445530a86d1d34034a31da6d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_hrun_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_hrun_deep_underdark.png-0420654e445530a86d1d34034a31da6d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_hrun_dungeon.png b/game/assets/art/board/terrain_wall_hrun_dungeon.png new file mode 100644 index 00000000..f5cd79b6 Binary files /dev/null and b/game/assets/art/board/terrain_wall_hrun_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_hrun_dungeon.png.import b/game/assets/art/board/terrain_wall_hrun_dungeon.png.import new file mode 100644 index 00000000..607a46e2 --- /dev/null +++ b/game/assets/art/board/terrain_wall_hrun_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cbf66ix2absgu" +path="res://.godot/imported/terrain_wall_hrun_dungeon.png-3c9b70e7ac0fb58d990f82e83c6e731f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_hrun_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_hrun_dungeon.png-3c9b70e7ac0fb58d990f82e83c6e731f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_hrun_underdark.png b/game/assets/art/board/terrain_wall_hrun_underdark.png new file mode 100644 index 00000000..90a99e66 Binary files /dev/null and b/game/assets/art/board/terrain_wall_hrun_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_hrun_underdark.png.import b/game/assets/art/board/terrain_wall_hrun_underdark.png.import new file mode 100644 index 00000000..e9e060ac --- /dev/null +++ b/game/assets/art/board/terrain_wall_hrun_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://svrupdcmhsfl" +path="res://.godot/imported/terrain_wall_hrun_underdark.png-4291043e05ce452299e340333ee8e872.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_hrun_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_hrun_underdark.png-4291043e05ce452299e340333ee8e872.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_in_nw_deep_underdark.png b/game/assets/art/board/terrain_wall_in_nw_deep_underdark.png new file mode 100644 index 00000000..c83f80eb Binary files /dev/null and b/game/assets/art/board/terrain_wall_in_nw_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_in_nw_deep_underdark.png.import b/game/assets/art/board/terrain_wall_in_nw_deep_underdark.png.import new file mode 100644 index 00000000..58d16d76 --- /dev/null +++ b/game/assets/art/board/terrain_wall_in_nw_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bo6xkwfjmxosn" +path="res://.godot/imported/terrain_wall_in_nw_deep_underdark.png-2d2512aed9946584c3b8dd0df540cb09.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_in_nw_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_in_nw_deep_underdark.png-2d2512aed9946584c3b8dd0df540cb09.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_in_nw_dungeon.png b/game/assets/art/board/terrain_wall_in_nw_dungeon.png new file mode 100644 index 00000000..5c1266e2 Binary files /dev/null and b/game/assets/art/board/terrain_wall_in_nw_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_in_nw_dungeon.png.import b/game/assets/art/board/terrain_wall_in_nw_dungeon.png.import new file mode 100644 index 00000000..6f89e9ea --- /dev/null +++ b/game/assets/art/board/terrain_wall_in_nw_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cdi3c5ap10ylv" +path="res://.godot/imported/terrain_wall_in_nw_dungeon.png-2f1ca8b7a692cfcb82277f1a1f644e81.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_in_nw_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_in_nw_dungeon.png-2f1ca8b7a692cfcb82277f1a1f644e81.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_in_nw_underdark.png b/game/assets/art/board/terrain_wall_in_nw_underdark.png new file mode 100644 index 00000000..cb06e7cb Binary files /dev/null and b/game/assets/art/board/terrain_wall_in_nw_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_in_nw_underdark.png.import b/game/assets/art/board/terrain_wall_in_nw_underdark.png.import new file mode 100644 index 00000000..e7591b0b --- /dev/null +++ b/game/assets/art/board/terrain_wall_in_nw_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cdw5m34cj1i7y" +path="res://.godot/imported/terrain_wall_in_nw_underdark.png-abdbd3fda6332dc184e23a71b3e64694.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_in_nw_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_in_nw_underdark.png-abdbd3fda6332dc184e23a71b3e64694.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_in_sw_deep_underdark.png b/game/assets/art/board/terrain_wall_in_sw_deep_underdark.png new file mode 100644 index 00000000..0b82e6c6 Binary files /dev/null and b/game/assets/art/board/terrain_wall_in_sw_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_in_sw_deep_underdark.png.import b/game/assets/art/board/terrain_wall_in_sw_deep_underdark.png.import new file mode 100644 index 00000000..0081da6c --- /dev/null +++ b/game/assets/art/board/terrain_wall_in_sw_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bfdvybhhsiab" +path="res://.godot/imported/terrain_wall_in_sw_deep_underdark.png-95a51c1cfaa299327d9521102d62a8fd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_in_sw_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_in_sw_deep_underdark.png-95a51c1cfaa299327d9521102d62a8fd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_in_sw_dungeon.png b/game/assets/art/board/terrain_wall_in_sw_dungeon.png new file mode 100644 index 00000000..4fd8d1c2 Binary files /dev/null and b/game/assets/art/board/terrain_wall_in_sw_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_in_sw_dungeon.png.import b/game/assets/art/board/terrain_wall_in_sw_dungeon.png.import new file mode 100644 index 00000000..bae01494 --- /dev/null +++ b/game/assets/art/board/terrain_wall_in_sw_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bm1ym8guy8hy7" +path="res://.godot/imported/terrain_wall_in_sw_dungeon.png-af8146ab3ec09f7ec81d311545df2db8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_in_sw_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_in_sw_dungeon.png-af8146ab3ec09f7ec81d311545df2db8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_in_sw_underdark.png b/game/assets/art/board/terrain_wall_in_sw_underdark.png new file mode 100644 index 00000000..b9981cc4 Binary files /dev/null and b/game/assets/art/board/terrain_wall_in_sw_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_in_sw_underdark.png.import b/game/assets/art/board/terrain_wall_in_sw_underdark.png.import new file mode 100644 index 00000000..28503b9f --- /dev/null +++ b/game/assets/art/board/terrain_wall_in_sw_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c0ucsu6l6goha" +path="res://.godot/imported/terrain_wall_in_sw_underdark.png-f6e0900dc4f816e4abc37c0f028e53b1.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_in_sw_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_in_sw_underdark.png-f6e0900dc4f816e4abc37c0f028e53b1.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_lone_deep_underdark.png b/game/assets/art/board/terrain_wall_lone_deep_underdark.png new file mode 100644 index 00000000..49d1c140 Binary files /dev/null and b/game/assets/art/board/terrain_wall_lone_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_lone_deep_underdark.png.import b/game/assets/art/board/terrain_wall_lone_deep_underdark.png.import new file mode 100644 index 00000000..26e03b63 --- /dev/null +++ b/game/assets/art/board/terrain_wall_lone_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://hvnun5n6056x" +path="res://.godot/imported/terrain_wall_lone_deep_underdark.png-1a6b6f4455c09948d8f791bff7c778f8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_lone_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_lone_deep_underdark.png-1a6b6f4455c09948d8f791bff7c778f8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_lone_dungeon.png b/game/assets/art/board/terrain_wall_lone_dungeon.png new file mode 100644 index 00000000..550efaa0 Binary files /dev/null and b/game/assets/art/board/terrain_wall_lone_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_lone_dungeon.png.import b/game/assets/art/board/terrain_wall_lone_dungeon.png.import new file mode 100644 index 00000000..18cca459 --- /dev/null +++ b/game/assets/art/board/terrain_wall_lone_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bm2h8q6lnsj2o" +path="res://.godot/imported/terrain_wall_lone_dungeon.png-52df3c8260010c5129ad3213cd9b9a43.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_lone_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_lone_dungeon.png-52df3c8260010c5129ad3213cd9b9a43.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_lone_underdark.png b/game/assets/art/board/terrain_wall_lone_underdark.png new file mode 100644 index 00000000..8a5d5468 Binary files /dev/null and b/game/assets/art/board/terrain_wall_lone_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_lone_underdark.png.import b/game/assets/art/board/terrain_wall_lone_underdark.png.import new file mode 100644 index 00000000..d159e400 --- /dev/null +++ b/game/assets/art/board/terrain_wall_lone_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://77v8wirdmy68" +path="res://.godot/imported/terrain_wall_lone_underdark.png-7edaf1e8452f7900b29c17db52c0e2fb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_lone_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_lone_underdark.png-7edaf1e8452f7900b29c17db52c0e2fb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_n_deep_underdark.png b/game/assets/art/board/terrain_wall_n_deep_underdark.png new file mode 100644 index 00000000..32e215ea Binary files /dev/null and b/game/assets/art/board/terrain_wall_n_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_n_deep_underdark.png.import b/game/assets/art/board/terrain_wall_n_deep_underdark.png.import new file mode 100644 index 00000000..d2711691 --- /dev/null +++ b/game/assets/art/board/terrain_wall_n_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://82hovv8uc8x8" +path="res://.godot/imported/terrain_wall_n_deep_underdark.png-fbb2006ac471691867c3bfc9bc5f8f4a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_n_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_n_deep_underdark.png-fbb2006ac471691867c3bfc9bc5f8f4a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_n_dungeon.png b/game/assets/art/board/terrain_wall_n_dungeon.png new file mode 100644 index 00000000..da17d322 Binary files /dev/null and b/game/assets/art/board/terrain_wall_n_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_n_dungeon.png.import b/game/assets/art/board/terrain_wall_n_dungeon.png.import new file mode 100644 index 00000000..19900fdd --- /dev/null +++ b/game/assets/art/board/terrain_wall_n_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://xi6qqpfssh4f" +path="res://.godot/imported/terrain_wall_n_dungeon.png-92bed658d0d444b4b3af2c7099283ac6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_n_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_n_dungeon.png-92bed658d0d444b4b3af2c7099283ac6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_n_underdark.png b/game/assets/art/board/terrain_wall_n_underdark.png new file mode 100644 index 00000000..a5cf1f1b Binary files /dev/null and b/game/assets/art/board/terrain_wall_n_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_n_underdark.png.import b/game/assets/art/board/terrain_wall_n_underdark.png.import new file mode 100644 index 00000000..c68b7144 --- /dev/null +++ b/game/assets/art/board/terrain_wall_n_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dmdw5yu8tl5bo" +path="res://.godot/imported/terrain_wall_n_underdark.png-67d67be8ad6c55a96d817a8401a23591.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_n_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_n_underdark.png-67d67be8ad6c55a96d817a8401a23591.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_nw_deep_underdark.png b/game/assets/art/board/terrain_wall_nw_deep_underdark.png new file mode 100644 index 00000000..02d6e498 Binary files /dev/null and b/game/assets/art/board/terrain_wall_nw_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_nw_deep_underdark.png.import b/game/assets/art/board/terrain_wall_nw_deep_underdark.png.import new file mode 100644 index 00000000..a318cb9c --- /dev/null +++ b/game/assets/art/board/terrain_wall_nw_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cc4ywbm165883" +path="res://.godot/imported/terrain_wall_nw_deep_underdark.png-79c09758be59b4560293fecb9c18ed05.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_nw_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_nw_deep_underdark.png-79c09758be59b4560293fecb9c18ed05.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_nw_dungeon.png b/game/assets/art/board/terrain_wall_nw_dungeon.png new file mode 100644 index 00000000..bcb184e8 Binary files /dev/null and b/game/assets/art/board/terrain_wall_nw_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_nw_dungeon.png.import b/game/assets/art/board/terrain_wall_nw_dungeon.png.import new file mode 100644 index 00000000..4b90ce8b --- /dev/null +++ b/game/assets/art/board/terrain_wall_nw_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bg8eus334o3xy" +path="res://.godot/imported/terrain_wall_nw_dungeon.png-6b9f23693b836a61cdad264892b98848.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_nw_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_nw_dungeon.png-6b9f23693b836a61cdad264892b98848.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_nw_underdark.png b/game/assets/art/board/terrain_wall_nw_underdark.png new file mode 100644 index 00000000..208d3fd1 Binary files /dev/null and b/game/assets/art/board/terrain_wall_nw_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_nw_underdark.png.import b/game/assets/art/board/terrain_wall_nw_underdark.png.import new file mode 100644 index 00000000..f64fd9ed --- /dev/null +++ b/game/assets/art/board/terrain_wall_nw_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dglh6jyhow4c" +path="res://.godot/imported/terrain_wall_nw_underdark.png-fe712f58bebec5aa7c5efd1227f781c3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_nw_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_nw_underdark.png-fe712f58bebec5aa7c5efd1227f781c3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_s_deep_underdark.png b/game/assets/art/board/terrain_wall_s_deep_underdark.png new file mode 100644 index 00000000..71a223db Binary files /dev/null and b/game/assets/art/board/terrain_wall_s_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_s_deep_underdark.png.import b/game/assets/art/board/terrain_wall_s_deep_underdark.png.import new file mode 100644 index 00000000..cc153f56 --- /dev/null +++ b/game/assets/art/board/terrain_wall_s_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c438agpdmwdu3" +path="res://.godot/imported/terrain_wall_s_deep_underdark.png-3c678f69636db64772c2edc372d7c317.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_s_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_s_deep_underdark.png-3c678f69636db64772c2edc372d7c317.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_s_dungeon.png b/game/assets/art/board/terrain_wall_s_dungeon.png new file mode 100644 index 00000000..5071d740 Binary files /dev/null and b/game/assets/art/board/terrain_wall_s_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_s_dungeon.png.import b/game/assets/art/board/terrain_wall_s_dungeon.png.import new file mode 100644 index 00000000..4c862571 --- /dev/null +++ b/game/assets/art/board/terrain_wall_s_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://domdlufjlq6nq" +path="res://.godot/imported/terrain_wall_s_dungeon.png-037dfed667cc59ec4668e80a9794926a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_s_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_s_dungeon.png-037dfed667cc59ec4668e80a9794926a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_s_underdark.png b/game/assets/art/board/terrain_wall_s_underdark.png new file mode 100644 index 00000000..34467477 Binary files /dev/null and b/game/assets/art/board/terrain_wall_s_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_s_underdark.png.import b/game/assets/art/board/terrain_wall_s_underdark.png.import new file mode 100644 index 00000000..701b0721 --- /dev/null +++ b/game/assets/art/board/terrain_wall_s_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b0ql3ac3up57t" +path="res://.godot/imported/terrain_wall_s_underdark.png-0100fbd8ef0488e937caa72aa208a5fd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_s_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_s_underdark.png-0100fbd8ef0488e937caa72aa208a5fd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_sw_deep_underdark.png b/game/assets/art/board/terrain_wall_sw_deep_underdark.png new file mode 100644 index 00000000..e3b8cda0 Binary files /dev/null and b/game/assets/art/board/terrain_wall_sw_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_sw_deep_underdark.png.import b/game/assets/art/board/terrain_wall_sw_deep_underdark.png.import new file mode 100644 index 00000000..e0172639 --- /dev/null +++ b/game/assets/art/board/terrain_wall_sw_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b2ystne6fwwdq" +path="res://.godot/imported/terrain_wall_sw_deep_underdark.png-2f4d16adc4d6eb4d0098ac25924a2ef3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_sw_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_sw_deep_underdark.png-2f4d16adc4d6eb4d0098ac25924a2ef3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_sw_dungeon.png b/game/assets/art/board/terrain_wall_sw_dungeon.png new file mode 100644 index 00000000..73499d81 Binary files /dev/null and b/game/assets/art/board/terrain_wall_sw_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_sw_dungeon.png.import b/game/assets/art/board/terrain_wall_sw_dungeon.png.import new file mode 100644 index 00000000..8f709c27 --- /dev/null +++ b/game/assets/art/board/terrain_wall_sw_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dq8peiidl8sat" +path="res://.godot/imported/terrain_wall_sw_dungeon.png-330552816fb6b7432c219d2680a44e5c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_sw_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_sw_dungeon.png-330552816fb6b7432c219d2680a44e5c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_sw_underdark.png b/game/assets/art/board/terrain_wall_sw_underdark.png new file mode 100644 index 00000000..0b371a34 Binary files /dev/null and b/game/assets/art/board/terrain_wall_sw_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_sw_underdark.png.import b/game/assets/art/board/terrain_wall_sw_underdark.png.import new file mode 100644 index 00000000..5c1ba34c --- /dev/null +++ b/game/assets/art/board/terrain_wall_sw_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://6sm7bqq31s7s" +path="res://.godot/imported/terrain_wall_sw_underdark.png-f36c6615ac6d087a1c50a968712353a2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_sw_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_sw_underdark.png-f36c6615ac6d087a1c50a968712353a2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_vrun_deep_underdark.png b/game/assets/art/board/terrain_wall_vrun_deep_underdark.png new file mode 100644 index 00000000..a75fe6a8 Binary files /dev/null and b/game/assets/art/board/terrain_wall_vrun_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_vrun_deep_underdark.png.import b/game/assets/art/board/terrain_wall_vrun_deep_underdark.png.import new file mode 100644 index 00000000..b3e269f5 --- /dev/null +++ b/game/assets/art/board/terrain_wall_vrun_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://xaxgkg0r3au8" +path="res://.godot/imported/terrain_wall_vrun_deep_underdark.png-152a77f4602a5de6311c4f3dbff9d2cb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_vrun_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_vrun_deep_underdark.png-152a77f4602a5de6311c4f3dbff9d2cb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_vrun_dungeon.png b/game/assets/art/board/terrain_wall_vrun_dungeon.png new file mode 100644 index 00000000..7864db33 Binary files /dev/null and b/game/assets/art/board/terrain_wall_vrun_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_vrun_dungeon.png.import b/game/assets/art/board/terrain_wall_vrun_dungeon.png.import new file mode 100644 index 00000000..a0bf908a --- /dev/null +++ b/game/assets/art/board/terrain_wall_vrun_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://mx6jscg4t428" +path="res://.godot/imported/terrain_wall_vrun_dungeon.png-e4381c9b2b4333532791f5d6ab9b27b8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_vrun_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_vrun_dungeon.png-e4381c9b2b4333532791f5d6ab9b27b8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_vrun_underdark.png b/game/assets/art/board/terrain_wall_vrun_underdark.png new file mode 100644 index 00000000..31ad5d66 Binary files /dev/null and b/game/assets/art/board/terrain_wall_vrun_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_vrun_underdark.png.import b/game/assets/art/board/terrain_wall_vrun_underdark.png.import new file mode 100644 index 00000000..6fd7bbf7 --- /dev/null +++ b/game/assets/art/board/terrain_wall_vrun_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cu6160orutoe6" +path="res://.godot/imported/terrain_wall_vrun_underdark.png-210db3728378ea55e8051118f89e1acf.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_vrun_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_vrun_underdark.png-210db3728378ea55e8051118f89e1acf.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_w_deep_underdark.png b/game/assets/art/board/terrain_wall_w_deep_underdark.png new file mode 100644 index 00000000..3ab8db0e Binary files /dev/null and b/game/assets/art/board/terrain_wall_w_deep_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_w_deep_underdark.png.import b/game/assets/art/board/terrain_wall_w_deep_underdark.png.import new file mode 100644 index 00000000..d252c609 --- /dev/null +++ b/game/assets/art/board/terrain_wall_w_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cryoqyhs63ppr" +path="res://.godot/imported/terrain_wall_w_deep_underdark.png-721382e265020c9dcd3b916ab1b0a301.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_w_deep_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_w_deep_underdark.png-721382e265020c9dcd3b916ab1b0a301.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_w_dungeon.png b/game/assets/art/board/terrain_wall_w_dungeon.png new file mode 100644 index 00000000..a793e266 Binary files /dev/null and b/game/assets/art/board/terrain_wall_w_dungeon.png differ diff --git a/game/assets/art/board/terrain_wall_w_dungeon.png.import b/game/assets/art/board/terrain_wall_w_dungeon.png.import new file mode 100644 index 00000000..135d7a21 --- /dev/null +++ b/game/assets/art/board/terrain_wall_w_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://k5utfbnxinx4" +path="res://.godot/imported/terrain_wall_w_dungeon.png-8efa491fd34ca8272667bd9d146f8bcd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_w_dungeon.png" +dest_files=["res://.godot/imported/terrain_wall_w_dungeon.png-8efa491fd34ca8272667bd9d146f8bcd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/board/terrain_wall_w_underdark.png b/game/assets/art/board/terrain_wall_w_underdark.png new file mode 100644 index 00000000..63432296 Binary files /dev/null and b/game/assets/art/board/terrain_wall_w_underdark.png differ diff --git a/game/assets/art/board/terrain_wall_w_underdark.png.import b/game/assets/art/board/terrain_wall_w_underdark.png.import new file mode 100644 index 00000000..42ca69e4 --- /dev/null +++ b/game/assets/art/board/terrain_wall_w_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://2iu5pro6yicu" +path="res://.godot/imported/terrain_wall_w_underdark.png-b4cc22a68fe225462ee73f38fb2f59bc.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/board/terrain_wall_w_underdark.png" +dest_files=["res://.godot/imported/terrain_wall_w_underdark.png-b4cc22a68fe225462ee73f38fb2f59bc.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_enemy_bugbear.png b/game/assets/art/character/character_enemy_bugbear.png new file mode 100644 index 00000000..1459c927 Binary files /dev/null and b/game/assets/art/character/character_enemy_bugbear.png differ diff --git a/game/assets/art/character/character_enemy_bugbear.png.import b/game/assets/art/character/character_enemy_bugbear.png.import new file mode 100644 index 00000000..c5cd3bd7 --- /dev/null +++ b/game/assets/art/character/character_enemy_bugbear.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d1aq8my5rbybh" +path="res://.godot/imported/character_enemy_bugbear.png-b85db95696ea170d3a02378c1b9cb1b6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_enemy_bugbear.png" +dest_files=["res://.godot/imported/character_enemy_bugbear.png-b85db95696ea170d3a02378c1b9cb1b6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_enemy_drider.png b/game/assets/art/character/character_enemy_drider.png new file mode 100644 index 00000000..eacafc76 Binary files /dev/null and b/game/assets/art/character/character_enemy_drider.png differ diff --git a/game/assets/art/character/character_enemy_drider.png.import b/game/assets/art/character/character_enemy_drider.png.import new file mode 100644 index 00000000..c9b344ca --- /dev/null +++ b/game/assets/art/character/character_enemy_drider.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d1b3u3566fv0o" +path="res://.godot/imported/character_enemy_drider.png-89d380427d47e32b55ca302d2158beaa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_enemy_drider.png" +dest_files=["res://.godot/imported/character_enemy_drider.png-89d380427d47e32b55ca302d2158beaa.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_enemy_duergar.png b/game/assets/art/character/character_enemy_duergar.png new file mode 100644 index 00000000..fa34b7e9 Binary files /dev/null and b/game/assets/art/character/character_enemy_duergar.png differ diff --git a/game/assets/art/character/character_enemy_duergar.png.import b/game/assets/art/character/character_enemy_duergar.png.import new file mode 100644 index 00000000..33bb84b1 --- /dev/null +++ b/game/assets/art/character/character_enemy_duergar.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dspobuhs2bv1o" +path="res://.godot/imported/character_enemy_duergar.png-c02537548b2ea756962fa93566ead2ca.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_enemy_duergar.png" +dest_files=["res://.godot/imported/character_enemy_duergar.png-c02537548b2ea756962fa93566ead2ca.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_enemy_duergar_king.png b/game/assets/art/character/character_enemy_duergar_king.png new file mode 100644 index 00000000..b70daebd Binary files /dev/null and b/game/assets/art/character/character_enemy_duergar_king.png differ diff --git a/game/assets/art/character/character_enemy_duergar_king.png.import b/game/assets/art/character/character_enemy_duergar_king.png.import new file mode 100644 index 00000000..6ba43975 --- /dev/null +++ b/game/assets/art/character/character_enemy_duergar_king.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://7uvsmevmehhj" +path="res://.godot/imported/character_enemy_duergar_king.png-d5e31106329eda800f48ca51edbed39d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_enemy_duergar_king.png" +dest_files=["res://.godot/imported/character_enemy_duergar_king.png-d5e31106329eda800f48ca51edbed39d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_enemy_giant_spider.png b/game/assets/art/character/character_enemy_giant_spider.png new file mode 100644 index 00000000..6dca3321 Binary files /dev/null and b/game/assets/art/character/character_enemy_giant_spider.png differ diff --git a/game/assets/art/character/character_enemy_giant_spider.png.import b/game/assets/art/character/character_enemy_giant_spider.png.import new file mode 100644 index 00000000..1e792c0b --- /dev/null +++ b/game/assets/art/character/character_enemy_giant_spider.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dpyl4psib7iq3" +path="res://.godot/imported/character_enemy_giant_spider.png-172e2248fcfbc494429b6e3fd1b999c7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_enemy_giant_spider.png" +dest_files=["res://.godot/imported/character_enemy_giant_spider.png-172e2248fcfbc494429b6e3fd1b999c7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_enemy_goblin.png b/game/assets/art/character/character_enemy_goblin.png new file mode 100644 index 00000000..c6630f68 Binary files /dev/null and b/game/assets/art/character/character_enemy_goblin.png differ diff --git a/game/assets/art/character/character_enemy_goblin.png.import b/game/assets/art/character/character_enemy_goblin.png.import new file mode 100644 index 00000000..fbf9332c --- /dev/null +++ b/game/assets/art/character/character_enemy_goblin.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://btp31vbcf4cqo" +path="res://.godot/imported/character_enemy_goblin.png-61e8f4ed9f4bde5c1ebad41aac0e2b30.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_enemy_goblin.png" +dest_files=["res://.godot/imported/character_enemy_goblin.png-61e8f4ed9f4bde5c1ebad41aac0e2b30.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_enemy_hook_horror.png b/game/assets/art/character/character_enemy_hook_horror.png new file mode 100644 index 00000000..fb11697a Binary files /dev/null and b/game/assets/art/character/character_enemy_hook_horror.png differ diff --git a/game/assets/art/character/character_enemy_hook_horror.png.import b/game/assets/art/character/character_enemy_hook_horror.png.import new file mode 100644 index 00000000..452f714f --- /dev/null +++ b/game/assets/art/character/character_enemy_hook_horror.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b2a3gq6llqrbl" +path="res://.godot/imported/character_enemy_hook_horror.png-f7f019e67911860bfd4be9701c6e9b2c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_enemy_hook_horror.png" +dest_files=["res://.godot/imported/character_enemy_hook_horror.png-f7f019e67911860bfd4be9701c6e9b2c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_enemy_ogre_chief.png b/game/assets/art/character/character_enemy_ogre_chief.png new file mode 100644 index 00000000..c1d89406 Binary files /dev/null and b/game/assets/art/character/character_enemy_ogre_chief.png differ diff --git a/game/assets/art/character/character_enemy_ogre_chief.png.import b/game/assets/art/character/character_enemy_ogre_chief.png.import new file mode 100644 index 00000000..b7418654 --- /dev/null +++ b/game/assets/art/character/character_enemy_ogre_chief.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cxhjjf70qlwko" +path="res://.godot/imported/character_enemy_ogre_chief.png-cb3ca5d1a22cd8e99e0e0e67f0f8798c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_enemy_ogre_chief.png" +dest_files=["res://.godot/imported/character_enemy_ogre_chief.png-cb3ca5d1a22cd8e99e0e0e67f0f8798c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_hero_attack.png b/game/assets/art/character/character_hero_attack.png new file mode 100644 index 00000000..b3ede0cf Binary files /dev/null and b/game/assets/art/character/character_hero_attack.png differ diff --git a/game/assets/art/character/character_hero_attack.png.import b/game/assets/art/character/character_hero_attack.png.import new file mode 100644 index 00000000..91f91984 --- /dev/null +++ b/game/assets/art/character/character_hero_attack.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bntadl65b6jnw" +path="res://.godot/imported/character_hero_attack.png-93b08f476e3a009b89e396fa1e46e6df.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_hero_attack.png" +dest_files=["res://.godot/imported/character_hero_attack.png-93b08f476e3a009b89e396fa1e46e6df.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_hero_hit.png b/game/assets/art/character/character_hero_hit.png new file mode 100644 index 00000000..ba469fae Binary files /dev/null and b/game/assets/art/character/character_hero_hit.png differ diff --git a/game/assets/art/character/character_hero_hit.png.import b/game/assets/art/character/character_hero_hit.png.import new file mode 100644 index 00000000..af346185 --- /dev/null +++ b/game/assets/art/character/character_hero_hit.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dimscg7o8etlj" +path="res://.godot/imported/character_hero_hit.png-568efe9a72fdfea9f5ec51af2bef96cb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_hero_hit.png" +dest_files=["res://.godot/imported/character_hero_hit.png-568efe9a72fdfea9f5ec51af2bef96cb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_hero_idle.png b/game/assets/art/character/character_hero_idle.png new file mode 100644 index 00000000..70262f12 Binary files /dev/null and b/game/assets/art/character/character_hero_idle.png differ diff --git a/game/assets/art/character/character_hero_idle.png.import b/game/assets/art/character/character_hero_idle.png.import new file mode 100644 index 00000000..cb027532 --- /dev/null +++ b/game/assets/art/character/character_hero_idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://blpk0rxpsdbbp" +path="res://.godot/imported/character_hero_idle.png-3fa7e01fe1358bc085b861a92c27385a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_hero_idle.png" +dest_files=["res://.godot/imported/character_hero_idle.png-3fa7e01fe1358bc085b861a92c27385a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_hero_portrait.png b/game/assets/art/character/character_hero_portrait.png new file mode 100644 index 00000000..2f0c44ba Binary files /dev/null and b/game/assets/art/character/character_hero_portrait.png differ diff --git a/game/assets/art/character/character_hero_portrait.png.import b/game/assets/art/character/character_hero_portrait.png.import new file mode 100644 index 00000000..0cd3a046 --- /dev/null +++ b/game/assets/art/character/character_hero_portrait.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://2cqiu1gqhpdl" +path="res://.godot/imported/character_hero_portrait.png-c00d1ca3a380578f93578e5f57469a61.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_hero_portrait.png" +dest_files=["res://.godot/imported/character_hero_portrait.png-c00d1ca3a380578f93578e5f57469a61.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/character/character_hero_walk.png b/game/assets/art/character/character_hero_walk.png new file mode 100644 index 00000000..44be323c Binary files /dev/null and b/game/assets/art/character/character_hero_walk.png differ diff --git a/game/assets/art/character/character_hero_walk.png.import b/game/assets/art/character/character_hero_walk.png.import new file mode 100644 index 00000000..404633a3 --- /dev/null +++ b/game/assets/art/character/character_hero_walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bkatg15m1umhu" +path="res://.godot/imported/character_hero_walk.png-4b44c893a70d6eb9fcf8e28d1af6b0b7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/character/character_hero_walk.png" +dest_files=["res://.godot/imported/character_hero_walk.png-4b44c893a70d6eb9fcf8e28d1af6b0b7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/fx/shovel_swing_f0.png b/game/assets/art/fx/shovel_swing_f0.png new file mode 100644 index 00000000..ab3b0b38 Binary files /dev/null and b/game/assets/art/fx/shovel_swing_f0.png differ diff --git a/game/assets/art/fx/shovel_swing_f0.png.import b/game/assets/art/fx/shovel_swing_f0.png.import new file mode 100644 index 00000000..35b86297 --- /dev/null +++ b/game/assets/art/fx/shovel_swing_f0.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bci06atuxoj2l" +path="res://.godot/imported/shovel_swing_f0.png-06ffc540d3ad1550957a48f3c889b190.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/fx/shovel_swing_f0.png" +dest_files=["res://.godot/imported/shovel_swing_f0.png-06ffc540d3ad1550957a48f3c889b190.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/fx/shovel_swing_f1.png b/game/assets/art/fx/shovel_swing_f1.png new file mode 100644 index 00000000..769e539d Binary files /dev/null and b/game/assets/art/fx/shovel_swing_f1.png differ diff --git a/game/assets/art/fx/shovel_swing_f1.png.import b/game/assets/art/fx/shovel_swing_f1.png.import new file mode 100644 index 00000000..c571bb92 --- /dev/null +++ b/game/assets/art/fx/shovel_swing_f1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://e47aiashtdlk" +path="res://.godot/imported/shovel_swing_f1.png-84017c125b45d615a7dc6035e51c95ad.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/fx/shovel_swing_f1.png" +dest_files=["res://.godot/imported/shovel_swing_f1.png-84017c125b45d615a7dc6035e51c95ad.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/fx/shovel_swing_f2.png b/game/assets/art/fx/shovel_swing_f2.png new file mode 100644 index 00000000..278353ed Binary files /dev/null and b/game/assets/art/fx/shovel_swing_f2.png differ diff --git a/game/assets/art/fx/shovel_swing_f2.png.import b/game/assets/art/fx/shovel_swing_f2.png.import new file mode 100644 index 00000000..801f740f --- /dev/null +++ b/game/assets/art/fx/shovel_swing_f2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cesyve5qba48u" +path="res://.godot/imported/shovel_swing_f2.png-90ae5cc0450c344e9a910083341d2225.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/fx/shovel_swing_f2.png" +dest_files=["res://.godot/imported/shovel_swing_f2.png-90ae5cc0450c344e9a910083341d2225.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_camp_bg.png b/game/assets/art/scene/scene_camp_bg.png new file mode 100644 index 00000000..00d5b88c Binary files /dev/null and b/game/assets/art/scene/scene_camp_bg.png differ diff --git a/game/assets/art/scene/scene_camp_bg.png.import b/game/assets/art/scene/scene_camp_bg.png.import new file mode 100644 index 00000000..58570a8b --- /dev/null +++ b/game/assets/art/scene/scene_camp_bg.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bwlimsh4e0u1r" +path="res://.godot/imported/scene_camp_bg.png-ad8da76e848772a9fd1f8364d5ec2282.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_camp_bg.png" +dest_files=["res://.godot/imported/scene_camp_bg.png-ad8da76e848772a9fd1f8364d5ec2282.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_camp_board.png b/game/assets/art/scene/scene_camp_board.png new file mode 100644 index 00000000..ee9c77dc Binary files /dev/null and b/game/assets/art/scene/scene_camp_board.png differ diff --git a/game/assets/art/scene/scene_camp_board.png.import b/game/assets/art/scene/scene_camp_board.png.import new file mode 100644 index 00000000..d902dcad --- /dev/null +++ b/game/assets/art/scene/scene_camp_board.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://biak0b4j1om3a" +path="res://.godot/imported/scene_camp_board.png-2bb40fea7e76e8223a8ee46d42f0564d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_camp_board.png" +dest_files=["res://.godot/imported/scene_camp_board.png-2bb40fea7e76e8223a8ee46d42f0564d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_camp_campfire.png b/game/assets/art/scene/scene_camp_campfire.png new file mode 100644 index 00000000..b7adeaf0 Binary files /dev/null and b/game/assets/art/scene/scene_camp_campfire.png differ diff --git a/game/assets/art/scene/scene_camp_campfire.png.import b/game/assets/art/scene/scene_camp_campfire.png.import new file mode 100644 index 00000000..5767ac0d --- /dev/null +++ b/game/assets/art/scene/scene_camp_campfire.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ndp0uhptgqky" +path="res://.godot/imported/scene_camp_campfire.png-de9b03b71e5cc4cdda7044a26e14a348.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_camp_campfire.png" +dest_files=["res://.godot/imported/scene_camp_campfire.png-de9b03b71e5cc4cdda7044a26e14a348.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_camp_chest.png b/game/assets/art/scene/scene_camp_chest.png new file mode 100644 index 00000000..84713a52 Binary files /dev/null and b/game/assets/art/scene/scene_camp_chest.png differ diff --git a/game/assets/art/scene/scene_camp_chest.png.import b/game/assets/art/scene/scene_camp_chest.png.import new file mode 100644 index 00000000..764c758b --- /dev/null +++ b/game/assets/art/scene/scene_camp_chest.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://k7p13sma4aov" +path="res://.godot/imported/scene_camp_chest.png-8a14e84aaa4c16615ee7c9caf7907074.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_camp_chest.png" +dest_files=["res://.godot/imported/scene_camp_chest.png-8a14e84aaa4c16615ee7c9caf7907074.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_camp_dummy.png b/game/assets/art/scene/scene_camp_dummy.png new file mode 100644 index 00000000..277a206e Binary files /dev/null and b/game/assets/art/scene/scene_camp_dummy.png differ diff --git a/game/assets/art/scene/scene_camp_dummy.png.import b/game/assets/art/scene/scene_camp_dummy.png.import new file mode 100644 index 00000000..a2ead307 --- /dev/null +++ b/game/assets/art/scene/scene_camp_dummy.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bvx11fd8xi8v6" +path="res://.godot/imported/scene_camp_dummy.png-0e3dc9dfccca2e79204bfe1b987ba3ca.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_camp_dummy.png" +dest_files=["res://.godot/imported/scene_camp_dummy.png-0e3dc9dfccca2e79204bfe1b987ba3ca.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_camp_forge.png b/game/assets/art/scene/scene_camp_forge.png new file mode 100644 index 00000000..817fb421 Binary files /dev/null and b/game/assets/art/scene/scene_camp_forge.png differ diff --git a/game/assets/art/scene/scene_camp_forge.png.import b/game/assets/art/scene/scene_camp_forge.png.import new file mode 100644 index 00000000..f4f0661f --- /dev/null +++ b/game/assets/art/scene/scene_camp_forge.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bho1gh7ryoeno" +path="res://.godot/imported/scene_camp_forge.png-bdd0ec03bb168d33247f81916c3e43c3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_camp_forge.png" +dest_files=["res://.godot/imported/scene_camp_forge.png-bdd0ec03bb168d33247f81916c3e43c3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_camp_gate.png b/game/assets/art/scene/scene_camp_gate.png new file mode 100644 index 00000000..7ea25a60 Binary files /dev/null and b/game/assets/art/scene/scene_camp_gate.png differ diff --git a/game/assets/art/scene/scene_camp_gate.png.import b/game/assets/art/scene/scene_camp_gate.png.import new file mode 100644 index 00000000..ec103ad2 --- /dev/null +++ b/game/assets/art/scene/scene_camp_gate.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cjpvw3h4jb5ot" +path="res://.godot/imported/scene_camp_gate.png-26be8f559f484cb9444347d25ce1dd04.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_camp_gate.png" +dest_files=["res://.godot/imported/scene_camp_gate.png-26be8f559f484cb9444347d25ce1dd04.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_camp_notebook.png b/game/assets/art/scene/scene_camp_notebook.png new file mode 100644 index 00000000..34ffacf2 Binary files /dev/null and b/game/assets/art/scene/scene_camp_notebook.png differ diff --git a/game/assets/art/scene/scene_camp_notebook.png.import b/game/assets/art/scene/scene_camp_notebook.png.import new file mode 100644 index 00000000..7dfbb7ab --- /dev/null +++ b/game/assets/art/scene/scene_camp_notebook.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b13td544tmy2k" +path="res://.godot/imported/scene_camp_notebook.png-ee3dec562e61ac5cee5ec0c7acb3a357.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_camp_notebook.png" +dest_files=["res://.godot/imported/scene_camp_notebook.png-ee3dec562e61ac5cee5ec0c7acb3a357.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_camp_tent.png b/game/assets/art/scene/scene_camp_tent.png new file mode 100644 index 00000000..006d1f3b Binary files /dev/null and b/game/assets/art/scene/scene_camp_tent.png differ diff --git a/game/assets/art/scene/scene_camp_tent.png.import b/game/assets/art/scene/scene_camp_tent.png.import new file mode 100644 index 00000000..73a19401 --- /dev/null +++ b/game/assets/art/scene/scene_camp_tent.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b8xj8p66yfvwv" +path="res://.godot/imported/scene_camp_tent.png-b89dbea860fc9c6d13bdb53af0533642.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_camp_tent.png" +dest_files=["res://.godot/imported/scene_camp_tent.png-b89dbea860fc9c6d13bdb53af0533642.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_camp_wall.png b/game/assets/art/scene/scene_camp_wall.png new file mode 100644 index 00000000..0fff6eba Binary files /dev/null and b/game/assets/art/scene/scene_camp_wall.png differ diff --git a/game/assets/art/scene/scene_camp_wall.png.import b/game/assets/art/scene/scene_camp_wall.png.import new file mode 100644 index 00000000..8a50f5f5 --- /dev/null +++ b/game/assets/art/scene/scene_camp_wall.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b4o3do4bo7sum" +path="res://.godot/imported/scene_camp_wall.png-e8871b9de80a29c065a661f23f1a8cd4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_camp_wall.png" +dest_files=["res://.godot/imported/scene_camp_wall.png-e8871b9de80a29c065a661f23f1a8cd4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_stage_bg_deep_underdark.png b/game/assets/art/scene/scene_stage_bg_deep_underdark.png new file mode 100644 index 00000000..7ccae636 Binary files /dev/null and b/game/assets/art/scene/scene_stage_bg_deep_underdark.png differ diff --git a/game/assets/art/scene/scene_stage_bg_deep_underdark.png.import b/game/assets/art/scene/scene_stage_bg_deep_underdark.png.import new file mode 100644 index 00000000..3e238d75 --- /dev/null +++ b/game/assets/art/scene/scene_stage_bg_deep_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://80psivqtlgsp" +path="res://.godot/imported/scene_stage_bg_deep_underdark.png-40c9b4152fe37e62dc2d2b61533b0383.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_stage_bg_deep_underdark.png" +dest_files=["res://.godot/imported/scene_stage_bg_deep_underdark.png-40c9b4152fe37e62dc2d2b61533b0383.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_stage_bg_dungeon.png b/game/assets/art/scene/scene_stage_bg_dungeon.png new file mode 100644 index 00000000..44c42148 Binary files /dev/null and b/game/assets/art/scene/scene_stage_bg_dungeon.png differ diff --git a/game/assets/art/scene/scene_stage_bg_dungeon.png.import b/game/assets/art/scene/scene_stage_bg_dungeon.png.import new file mode 100644 index 00000000..9be40600 --- /dev/null +++ b/game/assets/art/scene/scene_stage_bg_dungeon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://v6708buxtf2k" +path="res://.godot/imported/scene_stage_bg_dungeon.png-5db883a26511a741d808c2594192d27a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_stage_bg_dungeon.png" +dest_files=["res://.godot/imported/scene_stage_bg_dungeon.png-5db883a26511a741d808c2594192d27a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_stage_bg_underdark.png b/game/assets/art/scene/scene_stage_bg_underdark.png new file mode 100644 index 00000000..3cad45e7 Binary files /dev/null and b/game/assets/art/scene/scene_stage_bg_underdark.png differ diff --git a/game/assets/art/scene/scene_stage_bg_underdark.png.import b/game/assets/art/scene/scene_stage_bg_underdark.png.import new file mode 100644 index 00000000..2c30034f --- /dev/null +++ b/game/assets/art/scene/scene_stage_bg_underdark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bsjqmmc10n67o" +path="res://.godot/imported/scene_stage_bg_underdark.png-ac0b308b34e6bf255f0748a026ef267d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_stage_bg_underdark.png" +dest_files=["res://.godot/imported/scene_stage_bg_underdark.png-ac0b308b34e6bf255f0748a026ef267d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_stage_ground.png b/game/assets/art/scene/scene_stage_ground.png new file mode 100644 index 00000000..8f4803d3 Binary files /dev/null and b/game/assets/art/scene/scene_stage_ground.png differ diff --git a/game/assets/art/scene/scene_stage_ground.png.import b/game/assets/art/scene/scene_stage_ground.png.import new file mode 100644 index 00000000..2fca347b --- /dev/null +++ b/game/assets/art/scene/scene_stage_ground.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b3aaglen1alw7" +path="res://.godot/imported/scene_stage_ground.png-a11b6a0430bcb2f048b2a1eb9cdf66ea.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_stage_ground.png" +dest_files=["res://.godot/imported/scene_stage_ground.png-a11b6a0430bcb2f048b2a1eb9cdf66ea.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/scene/scene_stage_plaza.png b/game/assets/art/scene/scene_stage_plaza.png new file mode 100644 index 00000000..c2f89c2b Binary files /dev/null and b/game/assets/art/scene/scene_stage_plaza.png differ diff --git a/game/assets/art/scene/scene_stage_plaza.png.import b/game/assets/art/scene/scene_stage_plaza.png.import new file mode 100644 index 00000000..105de203 --- /dev/null +++ b/game/assets/art/scene/scene_stage_plaza.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://oyw1sn4cupc0" +path="res://.godot/imported/scene_stage_plaza.png-a3a92d12d3b121d65eed1a1d2d147f31.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/scene/scene_stage_plaza.png" +dest_files=["res://.godot/imported/scene_stage_plaza.png-a3a92d12d3b121d65eed1a1d2d147f31.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/backdrop_stone.png b/game/assets/art/ui/backdrop_stone.png new file mode 100644 index 00000000..e79fd30d Binary files /dev/null and b/game/assets/art/ui/backdrop_stone.png differ diff --git a/game/assets/art/ui/backdrop_stone.png.import b/game/assets/art/ui/backdrop_stone.png.import new file mode 100644 index 00000000..b4012907 --- /dev/null +++ b/game/assets/art/ui/backdrop_stone.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c2tfcanic03yg" +path="res://.godot/imported/backdrop_stone.png-799c672c1695c92b2aff3adcca263380.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/backdrop_stone.png" +dest_files=["res://.godot/imported/backdrop_stone.png-799c672c1695c92b2aff3adcca263380.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/border_stone.png b/game/assets/art/ui/border_stone.png new file mode 100644 index 00000000..26427291 Binary files /dev/null and b/game/assets/art/ui/border_stone.png differ diff --git a/game/assets/art/ui/border_stone.png.import b/game/assets/art/ui/border_stone.png.import new file mode 100644 index 00000000..b5a66ea3 --- /dev/null +++ b/game/assets/art/ui/border_stone.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bfeovg2xhf4ox" +path="res://.godot/imported/border_stone.png-12d43548af52199d50f7855c5e304610.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/border_stone.png" +dest_files=["res://.godot/imported/border_stone.png-12d43548af52199d50f7855c5e304610.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/button_stone.png b/game/assets/art/ui/button_stone.png new file mode 100644 index 00000000..904f1497 Binary files /dev/null and b/game/assets/art/ui/button_stone.png differ diff --git a/game/assets/art/ui/button_stone.png.import b/game/assets/art/ui/button_stone.png.import new file mode 100644 index 00000000..54a4bc38 --- /dev/null +++ b/game/assets/art/ui/button_stone.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://70c70lsrbc8v" +path="res://.godot/imported/button_stone.png-5ab18541546b60db4ddb0a308313bba7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/button_stone.png" +dest_files=["res://.godot/imported/button_stone.png-5ab18541546b60db4ddb0a308313bba7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/button_stone_pressed.png b/game/assets/art/ui/button_stone_pressed.png new file mode 100644 index 00000000..161091a4 Binary files /dev/null and b/game/assets/art/ui/button_stone_pressed.png differ diff --git a/game/assets/art/ui/button_stone_pressed.png.import b/game/assets/art/ui/button_stone_pressed.png.import new file mode 100644 index 00000000..855f5b1d --- /dev/null +++ b/game/assets/art/ui/button_stone_pressed.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://tprx4ie1y2nv" +path="res://.godot/imported/button_stone_pressed.png-8dc823b8e3152939770cc37218c41a97.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/button_stone_pressed.png" +dest_files=["res://.godot/imported/button_stone_pressed.png-8dc823b8e3152939770cc37218c41a97.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/gen/checkbox_checked.png b/game/assets/art/ui/gen/checkbox_checked.png new file mode 100644 index 00000000..8bf34558 Binary files /dev/null and b/game/assets/art/ui/gen/checkbox_checked.png differ diff --git a/game/assets/art/ui/gen/checkbox_checked.png.import b/game/assets/art/ui/gen/checkbox_checked.png.import new file mode 100644 index 00000000..db693b2b --- /dev/null +++ b/game/assets/art/ui/gen/checkbox_checked.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c37vc3lk8j46e" +path="res://.godot/imported/checkbox_checked.png-be331ca5bc2f4bbbb7d8d9f3f5aa38c7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/gen/checkbox_checked.png" +dest_files=["res://.godot/imported/checkbox_checked.png-be331ca5bc2f4bbbb7d8d9f3f5aa38c7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/gen/checkbox_checked_disabled.png b/game/assets/art/ui/gen/checkbox_checked_disabled.png new file mode 100644 index 00000000..76862b12 Binary files /dev/null and b/game/assets/art/ui/gen/checkbox_checked_disabled.png differ diff --git a/game/assets/art/ui/gen/checkbox_checked_disabled.png.import b/game/assets/art/ui/gen/checkbox_checked_disabled.png.import new file mode 100644 index 00000000..f75394a6 --- /dev/null +++ b/game/assets/art/ui/gen/checkbox_checked_disabled.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://csa8nxs5rd1ck" +path="res://.godot/imported/checkbox_checked_disabled.png-0b82c6a6e76a9ac90dc9e7aab1acb5d0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/gen/checkbox_checked_disabled.png" +dest_files=["res://.godot/imported/checkbox_checked_disabled.png-0b82c6a6e76a9ac90dc9e7aab1acb5d0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/gen/checkbox_unchecked.png b/game/assets/art/ui/gen/checkbox_unchecked.png new file mode 100644 index 00000000..080ab93c Binary files /dev/null and b/game/assets/art/ui/gen/checkbox_unchecked.png differ diff --git a/game/assets/art/ui/gen/checkbox_unchecked.png.import b/game/assets/art/ui/gen/checkbox_unchecked.png.import new file mode 100644 index 00000000..474e5cfc --- /dev/null +++ b/game/assets/art/ui/gen/checkbox_unchecked.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bcxl6v0tuddrx" +path="res://.godot/imported/checkbox_unchecked.png-2d74fc2e148b67340bc15301d7ab0b20.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/gen/checkbox_unchecked.png" +dest_files=["res://.godot/imported/checkbox_unchecked.png-2d74fc2e148b67340bc15301d7ab0b20.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/gen/checkbox_unchecked_disabled.png b/game/assets/art/ui/gen/checkbox_unchecked_disabled.png new file mode 100644 index 00000000..93d0bc6d Binary files /dev/null and b/game/assets/art/ui/gen/checkbox_unchecked_disabled.png differ diff --git a/game/assets/art/ui/gen/checkbox_unchecked_disabled.png.import b/game/assets/art/ui/gen/checkbox_unchecked_disabled.png.import new file mode 100644 index 00000000..d59d97d7 --- /dev/null +++ b/game/assets/art/ui/gen/checkbox_unchecked_disabled.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://s77irc8u3vb5" +path="res://.godot/imported/checkbox_unchecked_disabled.png-3a548f6b94bb18167d5607a58a16bf4a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/gen/checkbox_unchecked_disabled.png" +dest_files=["res://.godot/imported/checkbox_unchecked_disabled.png-3a548f6b94bb18167d5607a58a16bf4a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/gen/option_arrow.png b/game/assets/art/ui/gen/option_arrow.png new file mode 100644 index 00000000..25bae828 Binary files /dev/null and b/game/assets/art/ui/gen/option_arrow.png differ diff --git a/game/assets/art/ui/gen/option_arrow.png.import b/game/assets/art/ui/gen/option_arrow.png.import new file mode 100644 index 00000000..88c9efb6 --- /dev/null +++ b/game/assets/art/ui/gen/option_arrow.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bcxxt11qv3dbk" +path="res://.godot/imported/option_arrow.png-137be1c806e28e86518a9ef13f8aeee9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/gen/option_arrow.png" +dest_files=["res://.godot/imported/option_arrow.png-137be1c806e28e86518a9ef13f8aeee9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/gen/slider_grabber.png b/game/assets/art/ui/gen/slider_grabber.png new file mode 100644 index 00000000..f53cba64 Binary files /dev/null and b/game/assets/art/ui/gen/slider_grabber.png differ diff --git a/game/assets/art/ui/gen/slider_grabber.png.import b/game/assets/art/ui/gen/slider_grabber.png.import new file mode 100644 index 00000000..124fdd5d --- /dev/null +++ b/game/assets/art/ui/gen/slider_grabber.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bntrcomrrsrwd" +path="res://.godot/imported/slider_grabber.png-ba18ff33ac0e2d8187625d5afd88333a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/gen/slider_grabber.png" +dest_files=["res://.godot/imported/slider_grabber.png-ba18ff33ac0e2d8187625d5afd88333a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/gen/slider_grabber_disabled.png b/game/assets/art/ui/gen/slider_grabber_disabled.png new file mode 100644 index 00000000..8e53ce84 Binary files /dev/null and b/game/assets/art/ui/gen/slider_grabber_disabled.png differ diff --git a/game/assets/art/ui/gen/slider_grabber_disabled.png.import b/game/assets/art/ui/gen/slider_grabber_disabled.png.import new file mode 100644 index 00000000..05ed4cc5 --- /dev/null +++ b/game/assets/art/ui/gen/slider_grabber_disabled.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bld83qtfy5cx6" +path="res://.godot/imported/slider_grabber_disabled.png-9f95594f132d7e05607289d40168c3b3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/gen/slider_grabber_disabled.png" +dest_files=["res://.godot/imported/slider_grabber_disabled.png-9f95594f132d7e05607289d40168c3b3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/gen/slider_grabber_highlight.png b/game/assets/art/ui/gen/slider_grabber_highlight.png new file mode 100644 index 00000000..2f485579 Binary files /dev/null and b/game/assets/art/ui/gen/slider_grabber_highlight.png differ diff --git a/game/assets/art/ui/gen/slider_grabber_highlight.png.import b/game/assets/art/ui/gen/slider_grabber_highlight.png.import new file mode 100644 index 00000000..251c2072 --- /dev/null +++ b/game/assets/art/ui/gen/slider_grabber_highlight.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dvwgrhmraj2pi" +path="res://.godot/imported/slider_grabber_highlight.png-3fac15354c02e1eabb51c47480b8ba38.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/gen/slider_grabber_highlight.png" +dest_files=["res://.godot/imported/slider_grabber_highlight.png-3fac15354c02e1eabb51c47480b8ba38.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/gen/spinbox_updown.png b/game/assets/art/ui/gen/spinbox_updown.png new file mode 100644 index 00000000..0c8080ce Binary files /dev/null and b/game/assets/art/ui/gen/spinbox_updown.png differ diff --git a/game/assets/art/ui/gen/spinbox_updown.png.import b/game/assets/art/ui/gen/spinbox_updown.png.import new file mode 100644 index 00000000..42944b62 --- /dev/null +++ b/game/assets/art/ui/gen/spinbox_updown.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://brppa0ay8e8nr" +path="res://.godot/imported/spinbox_updown.png-79ed9fabd79d665c14c0e5d154733884.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/gen/spinbox_updown.png" +dest_files=["res://.godot/imported/spinbox_updown.png-79ed9fabd79d665c14c0e5d154733884.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_atlas.png b/game/assets/art/ui/icon_atlas.png new file mode 100644 index 00000000..eafff2c6 Binary files /dev/null and b/game/assets/art/ui/icon_atlas.png differ diff --git a/game/assets/art/ui/icon_atlas.png.import b/game/assets/art/ui/icon_atlas.png.import new file mode 100644 index 00000000..21f89db1 --- /dev/null +++ b/game/assets/art/ui/icon_atlas.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://doev3s0bcuq5j" +path="res://.godot/imported/icon_atlas.png-e89438fa2c7f3a01bc829f65a369f953.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_atlas.png" +dest_files=["res://.godot/imported/icon_atlas.png-e89438fa2c7f3a01bc829f65a369f953.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_blast.png b/game/assets/art/ui/icon_blast.png new file mode 100644 index 00000000..712cdd9d Binary files /dev/null and b/game/assets/art/ui/icon_blast.png differ diff --git a/game/assets/art/ui/icon_blast.png.import b/game/assets/art/ui/icon_blast.png.import new file mode 100644 index 00000000..48e5f76a --- /dev/null +++ b/game/assets/art/ui/icon_blast.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://o58nlv6hk2ys" +path="res://.godot/imported/icon_blast.png-04064899f4aae105e5ecc26444884109.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_blast.png" +dest_files=["res://.godot/imported/icon_blast.png-04064899f4aae105e5ecc26444884109.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_coinbag.png b/game/assets/art/ui/icon_coinbag.png new file mode 100644 index 00000000..336fe7d0 Binary files /dev/null and b/game/assets/art/ui/icon_coinbag.png differ diff --git a/game/assets/art/ui/icon_coinbag.png.import b/game/assets/art/ui/icon_coinbag.png.import new file mode 100644 index 00000000..a239604c --- /dev/null +++ b/game/assets/art/ui/icon_coinbag.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dullklfv7xlte" +path="res://.godot/imported/icon_coinbag.png-7ad26f492e92674b894095789857da38.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_coinbag.png" +dest_files=["res://.godot/imported/icon_coinbag.png-7ad26f492e92674b894095789857da38.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_forge.png b/game/assets/art/ui/icon_forge.png new file mode 100644 index 00000000..a3877919 Binary files /dev/null and b/game/assets/art/ui/icon_forge.png differ diff --git a/game/assets/art/ui/icon_forge.png.import b/game/assets/art/ui/icon_forge.png.import new file mode 100644 index 00000000..af40eaf3 --- /dev/null +++ b/game/assets/art/ui/icon_forge.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cq67vqun43yjs" +path="res://.godot/imported/icon_forge.png-9a2687f121c998d16d7dec611e0254a9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_forge.png" +dest_files=["res://.godot/imported/icon_forge.png-9a2687f121c998d16d7dec611e0254a9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_heart.png b/game/assets/art/ui/icon_heart.png new file mode 100644 index 00000000..e1316523 Binary files /dev/null and b/game/assets/art/ui/icon_heart.png differ diff --git a/game/assets/art/ui/icon_heart.png.import b/game/assets/art/ui/icon_heart.png.import new file mode 100644 index 00000000..50c6a7a5 --- /dev/null +++ b/game/assets/art/ui/icon_heart.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://tnxge3jvk55l" +path="res://.godot/imported/icon_heart.png-3e14321ca98d58e5ee89247177dc2a6c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_heart.png" +dest_files=["res://.godot/imported/icon_heart.png-3e14321ca98d58e5ee89247177dc2a6c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_keyshard.png b/game/assets/art/ui/icon_keyshard.png new file mode 100644 index 00000000..7654bd4d Binary files /dev/null and b/game/assets/art/ui/icon_keyshard.png differ diff --git a/game/assets/art/ui/icon_keyshard.png.import b/game/assets/art/ui/icon_keyshard.png.import new file mode 100644 index 00000000..778da476 --- /dev/null +++ b/game/assets/art/ui/icon_keyshard.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b5e36vq0sakx3" +path="res://.godot/imported/icon_keyshard.png-cff99cec261e28b5051430eb78979325.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_keyshard.png" +dest_files=["res://.godot/imported/icon_keyshard.png-cff99cec261e28b5051430eb78979325.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_keystone.png b/game/assets/art/ui/icon_keystone.png new file mode 100644 index 00000000..97b85243 Binary files /dev/null and b/game/assets/art/ui/icon_keystone.png differ diff --git a/game/assets/art/ui/icon_keystone.png.import b/game/assets/art/ui/icon_keystone.png.import new file mode 100644 index 00000000..51cca41c --- /dev/null +++ b/game/assets/art/ui/icon_keystone.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bnd8yq1gwlsn" +path="res://.godot/imported/icon_keystone.png-3763a32207cfae2f45fe72d471b0bb5f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_keystone.png" +dest_files=["res://.godot/imported/icon_keystone.png-3763a32207cfae2f45fe72d471b0bb5f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_medkit.png b/game/assets/art/ui/icon_medkit.png new file mode 100644 index 00000000..424985e0 Binary files /dev/null and b/game/assets/art/ui/icon_medkit.png differ diff --git a/game/assets/art/ui/icon_medkit.png.import b/game/assets/art/ui/icon_medkit.png.import new file mode 100644 index 00000000..c43fe502 --- /dev/null +++ b/game/assets/art/ui/icon_medkit.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://jefpnnfyt2f" +path="res://.godot/imported/icon_medkit.png-08fc2ed48cfc456328d0642154fa4558.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_medkit.png" +dest_files=["res://.godot/imported/icon_medkit.png-08fc2ed48cfc456328d0642154fa4558.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_pouch.png b/game/assets/art/ui/icon_pouch.png new file mode 100644 index 00000000..38d8a71a Binary files /dev/null and b/game/assets/art/ui/icon_pouch.png differ diff --git a/game/assets/art/ui/icon_pouch.png.import b/game/assets/art/ui/icon_pouch.png.import new file mode 100644 index 00000000..1f0bb711 --- /dev/null +++ b/game/assets/art/ui/icon_pouch.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dvrxoke5t81od" +path="res://.godot/imported/icon_pouch.png-7af57e57864a8872b241790365ccc158.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_pouch.png" +dest_files=["res://.godot/imported/icon_pouch.png-7af57e57864a8872b241790365ccc158.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_refresh.png b/game/assets/art/ui/icon_refresh.png new file mode 100644 index 00000000..bc4ffc0f Binary files /dev/null and b/game/assets/art/ui/icon_refresh.png differ diff --git a/game/assets/art/ui/icon_refresh.png.import b/game/assets/art/ui/icon_refresh.png.import new file mode 100644 index 00000000..2beda547 --- /dev/null +++ b/game/assets/art/ui/icon_refresh.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c5fpw50gjad0u" +path="res://.godot/imported/icon_refresh.png-84a3ff5804304796b72f7b94b917da31.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_refresh.png" +dest_files=["res://.godot/imported/icon_refresh.png-84a3ff5804304796b72f7b94b917da31.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_shield.png b/game/assets/art/ui/icon_shield.png new file mode 100644 index 00000000..c016682f Binary files /dev/null and b/game/assets/art/ui/icon_shield.png differ diff --git a/game/assets/art/ui/icon_shield.png.import b/game/assets/art/ui/icon_shield.png.import new file mode 100644 index 00000000..8d3a42f8 --- /dev/null +++ b/game/assets/art/ui/icon_shield.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cqjotgk88l820" +path="res://.godot/imported/icon_shield.png-5447c9828a7dde676476f6e9d9c0b8af.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_shield.png" +dest_files=["res://.godot/imported/icon_shield.png-5447c9828a7dde676476f6e9d9c0b8af.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_shovel.png b/game/assets/art/ui/icon_shovel.png new file mode 100644 index 00000000..e40b213e Binary files /dev/null and b/game/assets/art/ui/icon_shovel.png differ diff --git a/game/assets/art/ui/icon_shovel.png.import b/game/assets/art/ui/icon_shovel.png.import new file mode 100644 index 00000000..340e40d2 --- /dev/null +++ b/game/assets/art/ui/icon_shovel.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://clajmy4i3l0pf" +path="res://.godot/imported/icon_shovel.png-64bba6b461a2f5988db8da6e65838ed7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_shovel.png" +dest_files=["res://.godot/imported/icon_shovel.png-64bba6b461a2f5988db8da6e65838ed7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_sword.png b/game/assets/art/ui/icon_sword.png new file mode 100644 index 00000000..35143e66 Binary files /dev/null and b/game/assets/art/ui/icon_sword.png differ diff --git a/game/assets/art/ui/icon_sword.png.import b/game/assets/art/ui/icon_sword.png.import new file mode 100644 index 00000000..3d9d12ad --- /dev/null +++ b/game/assets/art/ui/icon_sword.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dd8y2oo0wv0j6" +path="res://.godot/imported/icon_sword.png-9cb55873ed9a5812cea24af598654bae.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_sword.png" +dest_files=["res://.godot/imported/icon_sword.png-9cb55873ed9a5812cea24af598654bae.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/icon_workshop.png b/game/assets/art/ui/icon_workshop.png new file mode 100644 index 00000000..c5ba3fd2 Binary files /dev/null and b/game/assets/art/ui/icon_workshop.png differ diff --git a/game/assets/art/ui/icon_workshop.png.import b/game/assets/art/ui/icon_workshop.png.import new file mode 100644 index 00000000..64406186 --- /dev/null +++ b/game/assets/art/ui/icon_workshop.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bfpqjx3j80p52" +path="res://.godot/imported/icon_workshop.png-c0b603c8dccfe9069e64d051eabd765e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/icon_workshop.png" +dest_files=["res://.godot/imported/icon_workshop.png-c0b603c8dccfe9069e64d051eabd765e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/node_battle.png b/game/assets/art/ui/node_battle.png new file mode 100644 index 00000000..18884e94 Binary files /dev/null and b/game/assets/art/ui/node_battle.png differ diff --git a/game/assets/art/ui/node_battle.png.import b/game/assets/art/ui/node_battle.png.import new file mode 100644 index 00000000..11dea5c4 --- /dev/null +++ b/game/assets/art/ui/node_battle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c4tyr775ctd3r" +path="res://.godot/imported/node_battle.png-d897da35cd6e09c738bf96a9e2ebf5c8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/node_battle.png" +dest_files=["res://.godot/imported/node_battle.png-d897da35cd6e09c738bf96a9e2ebf5c8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/node_boss.png b/game/assets/art/ui/node_boss.png new file mode 100644 index 00000000..170482cd Binary files /dev/null and b/game/assets/art/ui/node_boss.png differ diff --git a/game/assets/art/ui/node_boss.png.import b/game/assets/art/ui/node_boss.png.import new file mode 100644 index 00000000..8e9e3246 --- /dev/null +++ b/game/assets/art/ui/node_boss.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ckhdl5ow2ilwy" +path="res://.godot/imported/node_boss.png-880d6b2c5f9777b66597a0689ca9db6a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/node_boss.png" +dest_files=["res://.godot/imported/node_boss.png-880d6b2c5f9777b66597a0689ca9db6a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/node_rest.png b/game/assets/art/ui/node_rest.png new file mode 100644 index 00000000..972d5c89 Binary files /dev/null and b/game/assets/art/ui/node_rest.png differ diff --git a/game/assets/art/ui/node_rest.png.import b/game/assets/art/ui/node_rest.png.import new file mode 100644 index 00000000..6159e0b7 --- /dev/null +++ b/game/assets/art/ui/node_rest.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://8mab8uhryjga" +path="res://.godot/imported/node_rest.png-87d435876759d192ffecf3a9464f3fba.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/node_rest.png" +dest_files=["res://.godot/imported/node_rest.png-87d435876759d192ffecf3a9464f3fba.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/node_reward.png b/game/assets/art/ui/node_reward.png new file mode 100644 index 00000000..4a26c963 Binary files /dev/null and b/game/assets/art/ui/node_reward.png differ diff --git a/game/assets/art/ui/node_reward.png.import b/game/assets/art/ui/node_reward.png.import new file mode 100644 index 00000000..2418b791 --- /dev/null +++ b/game/assets/art/ui/node_reward.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://0nr8gywio7vn" +path="res://.godot/imported/node_reward.png-5668979de50aef5ad53942d485aafdff.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/node_reward.png" +dest_files=["res://.godot/imported/node_reward.png-5668979de50aef5ad53942d485aafdff.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/node_shop.png b/game/assets/art/ui/node_shop.png new file mode 100644 index 00000000..c6bbe080 Binary files /dev/null and b/game/assets/art/ui/node_shop.png differ diff --git a/game/assets/art/ui/node_shop.png.import b/game/assets/art/ui/node_shop.png.import new file mode 100644 index 00000000..a1756609 --- /dev/null +++ b/game/assets/art/ui/node_shop.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dakv3mhvwv1h2" +path="res://.godot/imported/node_shop.png-5ab84123a44a3396f6201cd801023df4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/node_shop.png" +dest_files=["res://.godot/imported/node_shop.png-5ab84123a44a3396f6201cd801023df4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/assets/art/ui/panel_stone.png b/game/assets/art/ui/panel_stone.png new file mode 100644 index 00000000..14417b61 Binary files /dev/null and b/game/assets/art/ui/panel_stone.png differ diff --git a/game/assets/art/ui/panel_stone.png.import b/game/assets/art/ui/panel_stone.png.import new file mode 100644 index 00000000..5adbff2d --- /dev/null +++ b/game/assets/art/ui/panel_stone.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ffg6ebgqxxdy" +path="res://.godot/imported/panel_stone.png-c2b330d1b6552bd7ff83ec9fcce267d9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/art/ui/panel_stone.png" +dest_files=["res://.godot/imported/panel_stone.png-c2b330d1b6552bd7ff83ec9fcce267d9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index b7cc5b57..2ee4af3d 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -36,7 +36,7 @@ var _refresh_bank: RefreshBank = null # 刷新账件(_init 建——三源 var _loadout: AttuneLoadout = null # 同调配装件(_init 建;充能单源化——物化行唯一事实源) var _bp_store: BreakpointStore # 断点组装与回放件(_init 建;六成员+27 键登记单源+读写侧机组随件——宿主读写经件) var adventure_inventory: TreasureItems # 冒险级秘宝库存(单源化——同调未用行跨关存活的承载面;关卡域语义归 level 使用面) -var _tree_rules: Array[Variant] = [] # 树 rule 键集(Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面) +var _tree_rules: Dictionary[Variant, Variant] = {} # 树规则参数字典(Fork 激活侧 effects_* 透传;下传 enc.tree_rules 消费面) var _shop_mode := "legacy" # 商店模式("legacy"=固定三件加定价〔off 对照承载,缺省零扰动〕/"pool"=共享池抽 3 买 1 重抽;真实入口 main.gd 显式投 pool) @@ -123,7 +123,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: # 特性开局注入(MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派 # 各 2 件档一成型——测量面专用;真实局规则改写型特性只走局内随机渠道,红线②层级纪律不破〕) var feat_seed: Array[Variant] = params.get("relics_seed", []) if params.get("relics_seed", []) is Array else [] - _tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # A/B 面 rule 键集 + _tree_rules = params.get("tree_rules", {}) if params.get("tree_rules", {}) is Dictionary else {} # A/B 面规则键参数字典 if not feat_seed.is_empty(): for r in feat_seed: _stats.feats.append(String(r)) @@ -163,7 +163,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: _attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive _refresh_bank.set_refresh_x_cap(_attune_refresh_x_total()) # 刷新账 X 基数预算注入(续玩参数面同 start) _bp_store.restore_stop_bank_fragments(bp, params) # 停点件/刷新账片段回填(件侧——凭证计数;刷新存量/Y 池缺键回退参数面) - _tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # A/B 面 rule 键集(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传) + _tree_rules = params.get("tree_rules", {}) if params.get("tree_rules", {}) is Dictionary else {} # A/B 面规则键参数字典(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传) # 续玩计数器回填(adventure 域计数——缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先——桶归宿主字典) var bp_counters = _bp_store._bp_read("adventure_counters", bp) if bp_counters is Dictionary: diff --git a/game/core/battle_fx_host.gd b/game/core/battle_fx_host.gd index 22eaa6b9..67478a1d 100644 --- a/game/core/battle_fx_host.gd +++ b/game/core/battle_fx_host.gd @@ -179,7 +179,7 @@ func flush_pending_reveals(ri: int) -> void: # 效果引擎订阅重建(每战从持有集/装备集重建,中途获得下一战生效;树规则合成订阅同面装载。 # 装载序=feats 表序→treasure 表序→树规则,写死保种子复放。计数器四桶经 counters_ctx 引用宿主容器,无回灌时点) -func rebuild_subscriptions(feats: Array, attunement: Array, tree_rules: Array) -> void: +func rebuild_subscriptions(feats: Array, attunement: Array, tree_rules: Dictionary[Variant, Variant]) -> void: engine.rebuild(feats, attunement, tree_rules) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 9b75544f..56e121e0 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -115,7 +115,7 @@ func _fx_host_fact(fact: String) -> Variant: match fact: "nest_count": var n := 0 - for eff in _status_effects.query_active("enemy"): + for eff in _status_effects.query_active_side("enemy"): if String(eff.effect_id) == "weaken": n += 1 return n @@ -348,7 +348,7 @@ func _begin_battle_fx_state(enc: Dictionary) -> void: # 树 rule 键消费(Fork 激活侧效果=效果引擎合成 # 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键 # =stat 面投影不入战斗(T8 表内容 provisional 口径披露)) - var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array + var tree_rules: Dictionary[Variant, Variant] = enc.get("tree_rules", {}) as Dictionary # 效果引擎订阅重建(装载序=feats 表序→treasure 表序→树规则,写死保种子复放——细则见 battle_fx_host.rebuild_subscriptions) _fx_host.rebuild_subscriptions(feat_ids, _facts.attunement, tree_rules) @@ -510,19 +510,15 @@ func _defender_ac(unit: Dictionary) -> int: return _damage.defender_ac(unit) -func _pool_effective(effect_id: String) -> int: - var side := "hero" - var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(effect_id) - if not row.is_empty(): - side = String(row.target_side) - for eff in _status_effects.query_active(side): +func _pool_effective(effect_id: String, owner_key: String) -> int: + for eff in _status_effects.query_active(owner_key): if eff.effect_id == effect_id: return eff.effective_value return 0 func _emit_composition_effects(side: String, ri: int) -> void: - for eff in _status_effects.query_active(side): + for eff in _status_effects.query_active_side(side): if eff.round_index < ri: # 续存实例(前轮授予)——每结算兑付一次的可见面(合并实例只持首授归因——增益状态 义务⑦) _emit_row({"type": "effect", "source_class": eff.source_class, "origin_cell": eff.origin_cell}, ri, @@ -536,10 +532,10 @@ func _emit_settlement_snapshot(verdict: String) -> void: for u in _enemy_system.query_alive("enemy"): enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "pending_skill": _enemy_system.pending_action(u), "armor": String(u.get("armor_type", "light")), "armor_class": int(u.get("armor_class", 0))}) var buffs: Array[Dictionary] = [] - for eff in _status_effects.query_active("hero"): - buffs.append({"id": eff.effect_id, "side": "hero", "layers": eff.layers, "left": eff.remaining_settlements}) - for eff in _status_effects.query_active("enemy"): - buffs.append({"id": eff.effect_id, "side": "enemy", "layers": eff.layers, "left": eff.remaining_settlements}) + for eff in _status_effects.query_active_side("hero"): + buffs.append({"id": eff.effect_id, "side": "hero", "owner": String(eff.owner_key), "layers": eff.layers, "left": eff.remaining_settlements}) + for eff in _status_effects.query_active_side("enemy"): + buffs.append({"id": eff.effect_id, "side": "enemy", "owner": String(eff.owner_key), "layers": eff.layers, "left": eff.remaining_settlements}) var mines_hit := 0 var safe_n := 0 for evt in _facts.reveals: @@ -651,8 +647,9 @@ func _roll_gp_drop() -> int: # 财富之道·聚(战斗掉落 GP 乘算·入账前:只乘战斗掉落两成分〔掷定+关卡加成〕, # 店内 escrow 不乘〔范围=仅战斗掉落渠道〕;本函数返回值=结算框「GP 增量」行 # 显示源,落在入账前=显示即实得;乘数 provisional 150%) - if (_facts.encounter.get("tree_rules", []) as Array).has("economy_hoard"): - drop = int(round(float(drop) * 1.5)) + var hoard: Dictionary[Variant, Variant] = (_facts.encounter.get("tree_rules", {}) as Dictionary).get("economy_hoard", {}) + if not hoard.is_empty(): + drop = int(round(float(drop) * float(hoard["mult_pct"]) / 100.0)) # 乘数读规则键参数 mult_pct return drop + _reward_grant.escrow @@ -666,12 +663,6 @@ func _roll_gp_drop() -> int: func presentation_snapshot() -> Dictionary: var enemies: Array[Dictionary] = [] var all: Array[Variant] = _enemy_system.all_enemies() - # 敌方增益投影(side-wide 状态——蓄力/硬化/削弱/暴走 percent 行 + 警觉 ac 行) - var enemy_effects: Array[Dictionary] = [] - for eff in _status_effects.query_active("enemy"): - var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id) - if not erow.is_empty() and String(erow.unit) in ["percent", "ac"]: - enemy_effects.append({"effect_id": eff.effect_id, "display_name": String(eff.display_name), "layers": int(eff.layers)}) for i in all.size(): var u: Dictionary[Variant, Variant] = all[i] var alive: bool = bool(u.get("alive", false)) @@ -689,7 +680,7 @@ func presentation_snapshot() -> Dictionary: "pending_skill": pending, "pending_type": String(pending_row.get("type", "")) if not pending_row.is_empty() else "", "pending_name": String(pending_row.get("display_name", "")) if not pending_row.is_empty() else "", - "effects": enemy_effects, + "effects": _enemy_own_effects(u), "alive": alive, }) var hero := {} @@ -705,7 +696,7 @@ func presentation_snapshot() -> Dictionary: "refresh_used": _facts.refresh_used, # 受理计数入呈现成文面(HUD 余量单源读此处) "hero": hero, "enemies": enemies, - "hero_effects": _status_effects.query_active("hero").duplicate(true), # 值拷贝出户(快照消费方直改不渗入状态容器) + "hero_effects": _status_effects.query_active(StatusContainer.OWNER_HERO).duplicate(true), # 值拷贝出户(快照消费方直改不渗入状态容器) # 整图变异投影(呈现横幅/常驻角标唯一读取面; # 空数组 = 无全局型合法态:普通图与全局池空回落图恒空) "board_variants": (minesweeper.current_board().board_variant_ids().duplicate() @@ -713,6 +704,18 @@ func presentation_snapshot() -> Dictionary: } +# 敌方增益逐卡投影(呈现各读各的——增益状态 v1.5):每卡只读它自己名下实例 +# (蓄力/硬化/削弱/暴走 percent 行 + 警觉 ac 行;死亡个体桶已清,投影恒空) +func _enemy_own_effects(u: Dictionary) -> Array[Dictionary]: + var own: Array[Dictionary] = [] + if bool(u.get("alive", false)): + for eff in _status_effects.query_active(StatusContainer.owner_key_for(u)): + var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id) + if not erow.is_empty() and String(erow.unit) in ["percent", "ac"]: + own.append({"effect_id": eff.effect_id, "display_name": String(eff.display_name), "layers": int(eff.layers)}) + return own + + # 战斗进行中谓词(hero 在场且存活)——组合根高频相位判断的低成本面 func is_battle_live() -> bool: return battle_result == "" and not _enemy_system.hero_unit().is_empty() diff --git a/game/core/board_data.gd b/game/core/board_data.gd index c1539e62..fa7651b9 100644 --- a/game/core/board_data.gd +++ b/game/core/board_data.gd @@ -4,9 +4,7 @@ class_name BoardData extends RefCounted -# 出生 3×3 区格数(W×H−9 布雷域基的双落点收口单点: -# 棋盘生成 _attempt/_finalize 与 BoardGenTable 布雷数下限校验共用;与 spawn_cells() 的 3×3 几何同源) -const SPAWN_AREA_CELLS := 9 +var spawn_edge: int = 0 # 出生区边长(奇数——表头 board_gen.spawn_edge 经生成器注入;spawn_cells() 几何与 W×H−edge² 布雷域基同源导出) var width: int = 0 var height: int = 0 @@ -69,9 +67,10 @@ func neighbors8(c: Vector2i) -> Array: func spawn_cells() -> Array: + var half := spawn_edge / 2 var out: Array[Vector2i] = [] - for dy in range(-1, 2): - for dx in range(-1, 2): + for dy in range(-half, half + 1): + for dx in range(-half, half + 1): var c := Vector2i(spawn_center.x + dx, spawn_center.y + dy) if in_bounds(c): out.append(c) diff --git a/game/core/board_generator.gd b/game/core/board_generator.gd index accfd631..e6d17342 100644 --- a/game/core/board_generator.gd +++ b/game/core/board_generator.gd @@ -14,26 +14,26 @@ var fatal_handler: Callable = Callable() # ==================== 生成入口 ==================== -# 目标雷数 N = round(d×(W×H−9))(params 快照键:density/w/h)——布雷定数与产物断言两处的单点公式 +# 目标雷数 N = round(d×(W×H−edge²))(params 快照键:density/w/h/spawn_edge)——布雷定数与产物断言两处的单点公式 static func target_mine_count(params: Dictionary) -> int: - return int(round(float(params.density) * float(int(params.w) * int(params.h) - BoardData.SPAWN_AREA_CELLS))) + return int(round(float(params.density) * float(int(params.w) * int(params.h) - int(params.spawn_edge) * int(params.spawn_edge)))) # rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件)。 -# db 参数不标具体类型原因:冒烟工具注入 FakeDB 桩(experiment_density.gd 等)——typed 形参会拒收桩表 +# db 参数不标具体类型原因:冒烟工具注入 FakeDB 桩(calibrate_boards.gd 等)——typed 形参会拒收桩表 func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData: # type-hint: ignore(测量面 _BoardRowStub 冒烟夹具鸭子型注入——typed 参拒 stub) var row: Dictionary[Variant, Variant] = db.combat.board_gen.resolve_row(L, D) if row.is_empty(): _fatal("BoardGen 行缺失 (%d,%d)" % [L, D]) return null - var params := _board_params(row, L, D) + var params := _board_params(row, L, D, int(db.combat.board_gen.spawn_edge)) # 表头直索引——缺成员即崩=fail-fast var outcome := _attempt_loop(params, rng_stream, db) _finalize(outcome["board"], params, outcome["attempts"], outcome["mode"], outcome["r0_rej"]) return outcome["board"] ## 生成期表值单次读取快照(防热重载撕裂 attempt 语义;零 RNG) -func _board_params(row: Dictionary, L: int, D: int) -> Dictionary: +func _board_params(row: Dictionary, L: int, D: int, spawn_edge: int) -> Dictionary: return { "L": L, "D": D, "w": int(row.board_width), "h": int(row.board_height), @@ -47,6 +47,7 @@ func _board_params(row: Dictionary, L: int, D: int) -> Dictionary: # r0_cap=0 闸关。正式表行全行投值,加载期校验在 BoardGenTable.validate) "shave_r": int(row.get("shave_ring_radius", 1)), "r0_cap": int(row.get("r0_max_cap", 0)), + "spawn_edge": spawn_edge, # 出生区边长(表头单点——经 generate 直索引注入) } @@ -100,11 +101,12 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar var b := BoardData.new() b.width = params.w b.height = params.h + b.spawn_edge = int(params.spawn_edge) b.spawn_center = Vector2i(int((params.w - 1) / 2.0), int((params.h - 1) / 2.0)) b.board_type = str(params.board_type) b.level_index = params.L b.node_depth = params.D - # ② 布雷:N = round(d×(W×H−9)) 先定数;域 = 全格 − 出生 3×3;均匀布雷后"先布后删"(_populate_mines 承载——Fisher-Yates 取号段整体随迁) + # ② 布雷:N = round(d×(W×H−edge²)) 先定数;域 = 全格 − 出生区(edge²);均匀布雷后"先布后删"(_populate_mines 承载——Fisher-Yates 取号段整体随迁) var removed := _populate_mines(b, params, rng_stream) # ③ 变异布置(地形变异 框架面):布置约束与算法随首批变异类型定稿(生成期布置接口结构位), # 定稿前零布置——清单为空是合法态(P0),静默产出无变异图。 @@ -148,7 +150,7 @@ func _populate_mines(b: BoardData, params: Dictionary, rng_stream: RandomNumberG func _shave_ring(b: BoardData, shave_r: int) -> int: var ring := {} for c in b.all_cells(): - if maxi(absi(c.x - b.spawn_center.x), absi(c.y - b.spawn_center.y)) <= 1 + shave_r: + if maxi(absi(c.x - b.spawn_center.x), absi(c.y - b.spawn_center.y)) <= (int(b.spawn_edge) - 1) / 2 + shave_r: ring[c] = true var removed := 0 for c in ring.keys(): diff --git a/game/core/board_installer.gd b/game/core/board_installer.gd index f8b7c123..88c300d8 100644 --- a/game/core/board_installer.gd +++ b/game/core/board_installer.gd @@ -122,7 +122,7 @@ func _place_variants(board: BoardData) -> void: func _place_variants_apply(board: BoardData, candidates: Array[Vector2i], stream: RandomNumberGenerator, vrow: Dictionary, total: int) -> void: var placed := {} while placed.size() < total: - var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.combat.variation.pack_max))) + var pack_size: int = mini(total - placed.size(), stream.randi_range(int(db.combat.variation.pack_min), int(db.combat.variation.pack_max))) var free: Array[Vector2i] = [] for c in candidates: # 新片种子/生长格禁与其他已布片 8 邻贴接——否则两片粘连成一个超 pack_max 的连通分量 diff --git a/game/core/config_db_resolver.gd b/game/core/config_db_resolver.gd index 37668e44..f8fb164b 100644 --- a/game/core/config_db_resolver.gd +++ b/game/core/config_db_resolver.gd @@ -107,7 +107,8 @@ func _warn_resolve(table_id: String, category: String, message: String, l_key: i _diag.call("warning", category, message) -# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故) +# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数, +# 越域值 clamp(域常量读 DifficultyTable.MULT_MIN/MULT_MAX 单点——使用时 clamp 口径,warning 预登记同族) func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary: var dt = _difficulty_get.call() if dt == null: @@ -115,8 +116,8 @@ func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary: for row in dt.rows: if int(row.get("level_index", 0)) == L and int(row.get("node_depth", -1)) == D: return { - "hp_mult": clampf(float(row.get("hp_mult", 1.0)), 0.1, 10.0), - "atk_mult": clampf(float(row.get("atk_mult", 1.0)), 0.1, 10.0), + "hp_mult": clampf(float(row.get("hp_mult", 1.0)), DifficultyTable.MULT_MIN, DifficultyTable.MULT_MAX), + "atk_mult": clampf(float(row.get("atk_mult", 1.0)), DifficultyTable.MULT_MIN, DifficultyTable.MULT_MAX), } return {} diff --git a/game/core/curio_deck.gd b/game/core/curio_deck.gd index 4df30645..d7cb9623 100644 --- a/game/core/curio_deck.gd +++ b/game/core/curio_deck.gd @@ -95,14 +95,15 @@ func consume_deck_token(kind: String, tier: String, feat_id: String = "") -> boo # 已拿上限判定(身份维度:kind 走 curio_cap、relic 走 feats 行 cap 字段; # taken 只在兑付时 +1〔拿了才计〕、刷出不计)。缺省放行=白名单外 kind 兜底。 func cap_ok(kind: String, feat_id: String = "") -> bool: - var cap := 99 if kind == "relic": var row: Dictionary[Variant, Variant] = db.crossrun.feats.row_by_id(feat_id) - cap = int(row.get("cap", 1)) if not row.is_empty() else 1 - return taken.get("relic:" + feat_id, 0) < cap + if row.is_empty(): + push_error("[奇物牌库] relic 令牌引用悬空 feat_id=%s(feats 表无此行——配置违约,禁兜底)" % feat_id) + return false + return taken.get("relic:" + feat_id, 0) < int(row["cap"]) # 直索引——schema 必填保证在键 if not db.run.adventure.CURIO_KINDS.has(kind): return true - cap = int(db.run.adventure.curio_cap(kind)) + var cap: int = int(db.run.adventure.curio_cap(kind)) return int(taken.get(kind, 0)) < cap diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index 3ebf3137..09f9344c 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -89,10 +89,11 @@ func _per_value(name: String, payload: Dictionary) -> int: return 0 -# 巢穴控制数(fact nest_count 取数面——敌方 weaken 实例数,猎巢人订阅面) +# 巢穴控制数(fact nest_count 取数面——敌方名下 weaken 实例总数,猎巢人订阅面; +# 个体化后削弱为累加行、每体各持一条——总数 = 被削弱个体数) func _nest_count() -> int: var n := 0 - for eff in effects.query_active("enemy"): + for eff in effects.query_active_side("enemy"): if String(eff.effect_id) == "weaken": n += 1 return n @@ -116,8 +117,8 @@ func submit(t: Dictionary) -> int: # board_flat_hero 的键并入行载荷,board_blast/board_typed 带 batch_key 供未生效行归因)。 # attack_commit 发射点(我方伤害单据开立入口; # 命中挂单据 context 随行——施加面在 _resolve_hero_attack 管线算术前逐族施加,开单时点=主角攻击 -# 单恒为段内首单(结账现场与开单现场一致)。载荷 atk/target/matchup + 挖掘 streak 事实〔连珠矢/ -# 执拗订阅面〕+巢穴控制数〔猎巢人〕) +# 单恒为段内首单(结账现场与开单现场一致)。载荷 atk/target/matchup + 挖掘 streak 事实 +# (执拗订阅面)+巢穴控制数〔猎巢人〕) # spec 束必载键(缺载 assert——fail-fast):channel/base_value/target_rule/ri; # 可选键缺省=原形缺省:origin_cell=(-1,-1)/attack_type=""/rider=0/highlight=""/context={}。 # 返回单据字典九栏目+requires/source_unit 逐字不变(契约 8)。 @@ -196,6 +197,7 @@ func _on_unit_died(u: Dictionary) -> void: for t in _queue: if is_same(t.source_unit, u): t["cancelled"] = true # 死亡即不动手——该敌未结单据作废(静默零行零 commit) + effects.purge_owner(StatusContainer.owner_key_for(u)) # 个体死亡即清名下实例(增益状态 v1.5——不转移不并回) # ---- 兑付管线(单点——所有伤害经过同一道门)---- @@ -356,7 +358,7 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float: # 掷点兑现(rolled 分量/payload 归因/池乘区/下限/英雄侧 rider 族/落地扣血/击杀标记; -# 击杀发射面拆 _kill_overflow)。返回 {landed, payload, hero_rider}(交付步消费)。 +# 击杀发射面拆 _kill_effects)。返回 {landed, payload, hero_rider}(交付步消费)。 func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> Dictionary: var target = ctx["target"] var ac_hits: Array[Variant] = ctx["ac_hits"] @@ -370,7 +372,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D payload["tier"] = int(ctx["discount"]) if bool(ctx["crit"]): payload["crit"] = true - var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense"), clamped_pool("enemy_defense")) + var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense", StatusContainer.OWNER_HERO), clamped_pool("enemy_defense", StatusContainer.owner_key_for(target))) var landed: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), pooled) # 加法族:英雄侧 rider(enemy_skill rider 对称位补齐口径)——后置必中附加,独立于掷点与乘区 # (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成 @@ -391,31 +393,16 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D units.apply_damage(target, landed) payload["killed"] = not target.alive # 击杀标记在扣血后取 if bool(payload.get("killed", false)): - _kill_overflow(t, target, landed) + _kill_effects(t, target) return {"landed": landed, "payload": payload, "hero_rider": hero_rider} -# kill 发射+施加(敌人死亡确认;贯穿射击溢出弹射订阅面:对下一存活敌 -# 以穿刺类型弹射本体 value%〔无次敌=弹射落空〕。B 模式入队段尾 flush 兑现;A 模式递归 resolve) -func _kill_overflow(t: Dictionary, target: Dictionary, landed: int) -> void: - for h in _fx_hits("kill", {"target": String(target.display_name), "target_id": String(target.get("enemy_id", "")), "overflow": landed}): +# kill 发射+施加(敌人死亡确认;订阅面=reveal_tiles 击杀揭示——斩首学者通道。 +# B 模式入队段尾 flush 兑现;A 模式端口路由宿主入队) +func _kill_effects(t: Dictionary, target: Dictionary) -> void: + for h in _fx_hits("kill", {"target": String(target.display_name), "target_id": String(target.get("enemy_id", ""))}): if String(h.get("kind", "")) == "reveal_tiles": _port.reveal_effect() # 斩首学者——击杀揭示 1 格(端口路由宿主入队,授予段末兑付) - continue - if String(h.get("kind", "")) != "overflow": - continue - var next_target = units.default_target() - if next_target != null: - # source_class 用中性名(fx_overflow 泛用通道,不挂件名) - var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "fx_overflow", "贯穿弹射", - {"channel": "board", - "base_value": maxi(int(db.combat.settlement.damage_roll.damage_floor), int(landed * int(h.get("value", 50)) / 100)), - "target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": int(t.round_index), - "origin_cell": t.origin_cell, "attack_type": "pierce"}) - if policy == POLICY_SEGMENT: - _queue.append(of_t) - else: - _resolve(of_t) # 交付:即时执行族(碎甲震击打重甲附加「护甲裂解」——复用 off_guard,零新状态行; @@ -424,8 +411,8 @@ func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary var payload: Dictionary[Variant, Variant] = redemption["payload"] for h in (ctx["ac_hits"] as Array): if String(h.get("kind", "")) == "apply_status": - # source_class 用中性名(fx_apply_status 泛用通道,不挂件名) - effects.apply_effect(String(h.get("status_id", "")), {"source_class": "fx_apply_status"}, -1) + # source_class 用中性名(fx_apply_status 泛用通道,不挂件名);落点 = 被命中者个体(增益状态 v1.5) + effects.apply_effect(String(h.get("status_id", "")), StatusContainer.owner_key_for(ctx["target"]), {"source_class": "fx_apply_status"}, -1) payload["cracked"] = true var label := _attack_label(String(t.display), bool(ctx["crit"]), int(ctx["discount"])) if int(redemption["hero_rider"]) > 0: @@ -473,7 +460,7 @@ func _enemy_roll(t: Dictionary) -> Dictionary: var discount: int = _roll_discount(int(roll_read.roll), threshold) var cdelta: int = _counter_delta(String(t.attack_type), String(hero.get("armor_type", "light"))) var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.combat.settlement.damage_roll.crit_multiplier), cdelta) - var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_defense"), clamped_pool("enemy_output")) + var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_defense", StatusContainer.OWNER_HERO), clamped_pool("enemy_output", StatusContainer.owner_key_for(t.source_unit))) var total: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区 return {"roll": int(roll_read.roll), "threshold": threshold, "crit": crit, "discount": discount, "total": total} @@ -593,13 +580,13 @@ func defender_ac(unit: Dictionary) -> int: var total: int = _hero_ac_override if total < 0: total = int(db.combat.hero.armor_class) - for eff in effects.query_active("hero"): + for eff in effects.query_active(StatusContainer.OWNER_HERO): var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id) if not row.is_empty() and String(row.unit) == "ac": total += int(eff.effective_value) return total var total_e: int = int(unit.get("armor_class", 0)) - for eff_e in effects.query_active("enemy"): + for eff_e in effects.query_active(StatusContainer.owner_key_for(unit)): var erow_ac: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff_e.effect_id) if not erow_ac.is_empty() and String(erow_ac.unit) == "ac": total_e += int(eff_e.effective_value) @@ -625,10 +612,12 @@ func _discount_shift_up(discount: int, shift: int) -> int: return int(tbl[mini(tbl.size() - 1, idx + shift)]) -# 池内加算 → clamp [-100,+100](池间乘算归 DamageMath.pool_damage 调用侧) -func clamped_pool(pool: String) -> int: +# 池内加算 → clamp [-100,+100](池间乘算归 DamageMath.pool_damage 调用侧); +# 池按个体读(增益状态 v1.5):主角侧池传 OWNER_HERO,敌方侧池传被打者/施放者个体键—— +# 乘区只合计该个体名下实例(B 敌的削弱不进 A 敌的账) +func clamped_pool(pool: String, owner_key: String) -> int: var total := 0 - for eff in effects.query_active("hero") + effects.query_active("enemy"): + for eff in effects.query_active(owner_key): var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id) if row.is_empty(): continue diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index c2d3a331..5b66a023 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -26,7 +26,7 @@ const MINE_EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "seal_se ## grant_gp/bias_wallet、counter_add/counter_spend、reveal_tiles/remove_mines/refresh_pick、 ## bias_rewards/preview。需求卡片标题计 23 系笔误,落码按行内容全收——commit 留痕) const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open_ticket", - "attack_bonus", "attack_segments", "yield_mult", "spawn_spell", "overflow", "grant_material", + "attack_bonus", "yield_mult", "spawn_spell", "grant_material", "grant_ap", "grant_gp", "bias_wallet", "shield", "spike", "apply_status", "counter_add", "counter_spend", "reveal_tiles", "remove_mines", "refresh_pick", "bias_rewards", "preview", @@ -100,7 +100,7 @@ func _init(db_ref: ConfigDb = null) -> void: ## 集过滤)→树规则合成行。同一数据行的多个 trigger 按行内数组序。计数器四桶由宿主持有、随宿主生灭, ## 本方法只重建订阅集(运行时可变状态=零;battle/level 桶的清零由宿主 new 天然承载,round 桶清零 ## 在 dispatch 的 round_start 特判内整桶 clear,adventure 桶随宿主字典引用跨战持久——无副本搬运)。 -func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void: +func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Dictionary[Variant, Variant] = {}) -> void: _triggers.clear() _load_feat_triggers(feat_ids) _load_attune_triggers(item_ids) @@ -134,42 +134,42 @@ func _load_attune_triggers(item_ids: Array) -> void: ## sense 两组=每关限次〔level 桶计数器 lt 上限闸 + 命中即 counter_add 记账双行——同事件双行同 ## 预评估态见闸同记,序=装载序确定;寿命随 LevelRunner.level_counters 每关 new 天然按关清零〕; ## economy 两组零订阅行——消费点=GP 掉落乘算与商品计价折价的 tree_rules 成员判定。数值全 provisional 归 M4。 -func _push_tree_rules(tree_rules: Array) -> void: - for key_v in tree_rules: - _push_tree_rule(String(key_v)) +func _push_tree_rules(tree_rules: Dictionary[Variant, Variant]) -> void: + for rk in tree_rules: + _push_tree_rule(String(rk), tree_rules[rk] as Dictionary) -## 单树规则合成行 push(六键 match 分派——词表封闭,未知键零订阅零扰动) -func _push_tree_rule(rk: String) -> void: +## 单树规则合成行 push(六键 match 分派——词表封闭,未知键零订阅零扰动;参数全读规则键参数字典) +func _push_tree_rule(rk: String, params: Dictionary[Variant, Variant]) -> void: match rk: "posture_first_strike": _push_trigger("tree", rk, {"on": "round_start", "when": [{"fact": "battle_index", "op": "eq", "value": 1}, {"fact": "round_index", "op": "eq", "value": 1}], - "do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {}) + "do": {"kind": "grant_ap", "mode": "queue", "value": int(params["ap_bonus"])}}, {}) "posture_bulwark": _push_trigger("tree", rk, {"on": "damage_taken", "when": [{"fact": "battle_index", "op": "eq", "value": 1}, {"fact": "round_index", "op": "eq", "value": 1}], - "do": {"kind": "shield", "value": 1}}, {}) + "do": {"kind": "shield", "value": int(params["shield"])}}, {}) "sense_sharp": # 锋=踩中危险格伤害减半·每关限次——mine_hit 挂点现成(damage_mult 收集→halve_pct, # 与惊巢反制同路径;伤害在结算战斗段末兑付,探索阶段零改动) _push_trigger("tree", rk, {"on": "mine_hit", - "when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}], - "do": {"kind": "damage_mult", "value": 50}}, {}) + "when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": int(params["cap_per_level"])}], + "do": {"kind": "damage_mult", "value": int(params["halve_pct"])}}, {}) _push_trigger("tree", rk, {"on": "mine_hit", - "when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}], + "when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": int(params["cap_per_level"])}], "do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_sharp_used", "value": 1}}, {}) "sense_guard": # 守=标错返还行动点·每关限次(契约 12 失误补偿——seal_missed 由 _judge_seal 去重闸后 # 发射;queue=下一轮开轮兑现,末轮判出无下一轮=合法作废自动成立) _push_trigger("tree", rk, {"on": "seal_missed", - "when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}], - "do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {}) + "when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": int(params["cap_per_level"])}], + "do": {"kind": "grant_ap", "mode": "queue", "value": int(params["ap_refund"])}}, {}) _push_trigger("tree", rk, {"on": "seal_missed", - "when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}], + "when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": int(params["cap_per_level"])}], "do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_guard_used", "value": 1}}, {}) @@ -461,7 +461,7 @@ static func _vtr_from_refs(rid: String, tr: Dictionary, row: Dictionary, errs: A ## 加法族(数值相加,可交换):add_ticket_rider、attack_bonus、grant_material、grant_ap(非 transfer)、 ## counter_add、shield——发射点逐条累加收集器。 ## 乘法族(收集后一次施加,正负分桶):yield_mult、damage_mult。 -## 任意真族(任一命中即生效):spawn_spell、overflow、spike。 +## 任意真族(任一命中即生效):spawn_spell、spike。 ## 按序改写族(收集→单点按声明序施加):modify_ticket 及一切改写窗口效果。 ## 按序消耗族(按订阅序消耗共享资源,耗尽即止;可选 fallback):counter_spend。 ## 即时执行族(每条命中各自执行一次,执行序=声明序):open_ticket、grant_ap(mode:transfer)、 diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 502c6d0a..46619890 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -91,7 +91,7 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary = {}) -> v treasure_inventory = ctx["treasure_inventory"] # 冒险级库存共享(同调未用行跨关存活——单源化承载面) _treasure_unlocked = ctx.get("treasure_unlocked", {}) if ctx.get("treasure_unlocked", {}) is Dictionary else {} _attunement = ctx.get("attunement", []) if ctx.get("attunement", []) is Array else [] # 活引用注入(契约 6——禁快照) - _tree_rules = ctx.get("tree_rules", []) if ctx.get("tree_rules", []) is Array else [] # 活引用注入 + _tree_rules = ctx.get("tree_rules", {}) if ctx.get("tree_rules", {}) is Dictionary else {} # 活引用注入(规则键参数字典) _hero_attack_type = String(ctx.get("hero_attack_type", "")) _hero_armor_type = String(ctx.get("hero_armor_type", "")) _voucher_rap = maxi(0, int(ctx.get("voucher_rap", 0))) @@ -661,8 +661,8 @@ func current_battle() -> Variant: # 特性持有集(begin_battle enc.feats 求值面;**共享引用不复制**——关内奖励节点兑付的 relic 须当关生效) var _feats: Array[Variant] = [] -# 树 rule 键集(begin_battle enc.tree_rules 携带;Fork 激活侧 effects——构造注入活引用) -var _tree_rules: Array[Variant] = [] +# 树规则参数字典(begin_battle enc.tree_rules 携带;Fork 激活侧 effects_*——构造注入活引用) +var _tree_rules: Dictionary[Variant, Variant] = {} # 特性持有集读面(HUD C 区特性段数据源) diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index e762d613..abf252ff 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -384,9 +384,10 @@ func tree_buy_fork(node_id: String) -> bool: return save.write_to_disk() -# 树规则键投影(激活 Fork 侧 effects 并入) -func tree_rule_keys() -> Array: - var out: Array[Variant] = [] +# 树规则参数字典投影(激活 Fork 侧 effects_* 参数字典并入——键=规则键,值=该键参数字典原样透传; +# 首遇优先去重语义保持) +func tree_rules() -> Dictionary[Variant, Variant]: + var out: Dictionary[Variant, Variant] = {} var sides: Dictionary[Variant, Variant] = meta_state.tree_state().fork_sides for row in db.crossrun.meta_tree.rows: if String(row.get("kind", "")) != "fork": @@ -397,7 +398,7 @@ func tree_rule_keys() -> Array: var fx: Dictionary[Variant, Variant] = row.get("effects_" + active, {}) as Dictionary for k in fx.keys(): if not out.has(String(k)): - out.append(String(k)) + out[String(k)] = fx[k] return out diff --git a/game/core/minesweeper_rules.gd b/game/core/minesweeper_rules.gd index 24163cf8..c6c4b518 100644 --- a/game/core/minesweeper_rules.gd +++ b/game/core/minesweeper_rules.gd @@ -368,6 +368,19 @@ func fx_reveal(c: Vector2i) -> bool: return true +## 挖一格→真实翻开集只读预览(sim 信息面出口——与 _classify 同口径:本格封印视为已隐式退役、 +## 其余封印照挡;复用 _chain_closure 闭包;零状态写入、零事件发射) +func preview_reveal(c: Vector2i) -> Array[Vector2i]: + var sim_seals := _seals.duplicate() + sim_seals.erase(c) + var closure := _chain_closure(c, sim_seals) + var out: Array[Vector2i] = [] + for cc in closure.keys(): + if cc != c and not _revealed.has(cc): + out.append(cc) # F2 口径:新开格集——不含首格与已开格 + return out + + # 连锁闭包:μX.[O∪{c} ⊆ X ∧ ∀x∈X:(x∈S ∧ num(x)=0 ⇒ N₈(x)∩S∖G ⊆ X)]——连锁不越封印 func _chain_closure(start: Vector2i, seal_set: Dictionary) -> Dictionary: var chain := {start: true} diff --git a/game/core/node_map_generator.gd b/game/core/node_map_generator.gd index e07c67a5..bcf3044e 100644 --- a/game/core/node_map_generator.gd +++ b/game/core/node_map_generator.gd @@ -9,7 +9,6 @@ extends RefCounted const NODE_TYPES := ["battle", "boss", "reward", "rest", "shop"] const FILLER_TYPES := ["battle", "reward", "rest"] -const MAX_WIDTH := 4 # 产出约束·宽度=分叉生长(同深度节点数由岔口长出 1→2→3→4,禁摆平行层) # 多终点图的支路互斥段必须各有独有类型(契约 5③),而商店与 Boss 是每条支路共有件, # 独有类型只能来自 battle/reward/rest 三件 ⇒ 归属大岔口各区域的填充节点取互斥签名类型。 # 入口与主链段(大岔口上游,不属任何支路互斥段)恒为战斗节点——承载战斗保底与岔口下一跳互异基准形。 @@ -66,40 +65,11 @@ func _setup_and_build(params: Dictionary, seed_val: int) -> Dictionary: ## 参数档可行性判定(执行边界③:参数无解=配置错误)——域校验外加深度预算与战斗保底可放性。 static func check_params(params: Dictionary) -> Array: - var errs: Array[Dictionary] = _cp_domain(params) - if not errs.is_empty(): - return errs + # 域校验并单点(R7——参数档域合法性唯一在 LevelGenParams.validate 加载期; + # 生成器只留可行性判定 _cp_structural——schema 无此判定的生成器职责) return _cp_structural(params) -## 域校验段:五旋钮值域/岔口去路数锁值/配比四类合计 1 -static func _cp_domain(params: Dictionary) -> Array[Dictionary]: - var errs: Array[Dictionary] = [] - var L := int(params.get("layers", 0)) - var e := int(params.get("exit_count", 0)) - var en := int(params.get("entry_count", 0)) - var bf := int(params.get("battle_floor", -1)) - var bd := float(params.get("branch_density", -1.0)) - if L < 6: - errs.append({"category": "domain", "message": "层深 %d 低于下限 6(层高已锁)" % L}) - if e < 1 or e > 3: - errs.append({"category": "domain", "message": "出口个数 %d 越域 [1,3]" % e}) - if en < 1: - errs.append({"category": "domain", "message": "入口个数 %d 越域(≥1)" % en}) - if bd < 0.0 or bd > 1.0: - errs.append({"category": "domain", "message": "分支密度 %f 越域 [0,1]" % bd}) - if bf < 0: - errs.append({"category": "domain", "message": "战斗保底 %d 越域(≥0)" % bf}) - if int(params.get("fork_ways_min", 2)) < 2 or int(params.get("fork_ways_max", 3)) > 3: - errs.append({"category": "domain", "message": "岔口去路数域越界(锁 2/3)"}) - var sum := 0.0 - for rk in ["battle_ratio", "reward_ratio", "rest_ratio", "shop_ratio"]: - sum += float(params.get(rk, -1.0)) - if not is_equal_approx(sum, 1.0): - errs.append({"category": "domain", "message": "配比四类合计 %f ≠ 1" % sum}) - return errs - - ## 结构可行性段:主链段容量与战斗保底可放性(多出口按区域签名累加可放槽) static func _cp_structural(params: Dictionary) -> Array[Dictionary]: var errs: Array[Dictionary] = [] @@ -271,8 +241,8 @@ func _vo_width_rules(nodes: Array, errs: Array) -> void: var d2: int = int(n["depth"]) per_depth[d2] = int(per_depth.get(d2, 0)) + 1 for d3 in per_depth: - if int(per_depth[d3]) > MAX_WIDTH: - errs.append({"category": "structural", "message": "深度 %d 节点数 %d 超分叉生长上限 4(平行层摆放)" % [d3, per_depth[d3]]}) + if int(per_depth[d3]) > int(_params["width_max"]): + errs.append({"category": "structural", "message": "深度 %d 节点数 %d 超分叉生长上限(width_max 表值——平行层摆放)" % [d3, per_depth[d3]]}) ## 计数规则③·参数档对照:战斗保底/入口数/终点数/层高(params 为空时跳过) @@ -358,7 +328,7 @@ func _build_region(r: int, e: int, L: int, prevs: Array, terminals: Array) -> vo ## 侧支节点与主去路同深(保持「深度=自入口最长路径步数」不变量),两支各自汇入主去路的后继, ## 于是支路互斥段={主去路节点} 与 {侧支节点}——两支类型互异即同时满足「下一跳互异」与「各有独有类型」。 func _add_forks(rg: RandomNumberGenerator, L: int, t: int, e: int) -> void: - var wanted := clampi(int(round(float(_params["branch_density"]) * float(L))), 1, 24) + var wanted := maxi(int(round(float(_params["branch_density"]) * float(L))), 1) # 死上限 24 删(不可达值——下限 1 保留防零岔口) var anchors := _collect_fork_anchors(L, t, e) anchors = _shuffle(rg, anchors) var selected := {} # 已扩岔节点 id @@ -377,10 +347,10 @@ func _add_forks(rg: RandomNumberGenerator, L: int, t: int, e: int) -> void: continue if (v["next"] as Array).is_empty(): continue # 主去路无后继(无可汇入点)——该锚点弃用 - var extra := 2 if (int(_params["fork_ways_max"]) >= 3 and rg.randf() < 0.35) else 1 + var extra := 2 if (int(_params["fork_ways_max"]) >= 3 and rg.randf() < float(_params["fork_third_way_pct"]) / 100.0) else 1 var made := 0 for k in range(extra): - if _depth_count(int(v["depth"])) >= MAX_WIDTH: + if _depth_count(int(v["depth"])) >= int(_params["width_max"]): break var wid := _mk("battle", int(v["depth"])) (a["next"] as Array).append(wid) @@ -452,7 +422,7 @@ func _fill_ratios(rg: RandomNumberGenerator) -> void: free = _shuffle(rg, free) var want := int(round(float(_pool.size()) * (float(_params["reward_ratio"]) + float(_params["rest_ratio"])))) var floor_n := int(_params["battle_floor"]) - var rest_first := rg.randf() < 0.5 + var rest_first := rg.randf() < float(_params["fill_rest_first_pct"]) / 100.0 for i in range(want): if i >= free.size(): break diff --git a/game/core/reward_grant.gd b/game/core/reward_grant.gd index ae265754..0e468f10 100644 --- a/game/core/reward_grant.gd +++ b/game/core/reward_grant.gd @@ -223,8 +223,9 @@ func _dispatch_row(row: Dictionary, att: Dictionary) -> bool: return false -# percent 行兑付:GP 型行走战斗内缓冲记账;其余 apply_effect 入容器; -# 满层拒收 = "奖励未生效"行(零静默) +# percent 行兑付:GP 型行走战斗内缓冲记账;其余 apply_effect 入容器(逐个体——enemy 侧行 = +# 全体存活敌逐只各发一条实例,hero 侧行 = 主角单桶,增益状态 v1.5 目标规则「全体存活敌」); +# 满层拒收 = "奖励未生效"行(零静默——全体皆拒才记一行;部分个体满层拒由该个体容器内原子拒绝) func _apply_percent_row(row: Dictionary, att: Dictionary) -> bool: var eff := String(row.effect_id) if eff == "gp_gain" or eff == "gp_loss": @@ -236,11 +237,20 @@ func _apply_percent_row(row: Dictionary, att: Dictionary) -> bool: var eff_mag: int = int(srow.magnitude) if prem_pct != 100: eff_mag = int(round(eff_mag * prem_pct / 100.0)) - var ok: bool = status_effects.apply_effect(eff, _grant_src(att, prem_pct, eff_mag)) - if ok: + var owner_keys: Array[Variant] = [] + if String(srow.target_side) == "enemy": + for u in units.query_alive("enemy"): + owner_keys.append(StatusContainer.owner_key_for(u)) + else: + owner_keys.append(StatusContainer.OWNER_HERO) + var granted := 0 + for okey in owner_keys: + if status_effects.apply_effect(eff, String(okey), _grant_src(att, prem_pct, eff_mag)): + granted += 1 + if granted > 0: emit_cb.call({"type": "effect", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1))}, int(att.get("ri", 0)), String(srow.display_name) + String(att.get("label_suffix", "")), eff_mag, - {"effect_id": eff, "pool_value": int(pool_effective_cb.call(eff))}) + {"effect_id": eff, "pool_value": int(pool_effective_cb.call(eff, String(owner_keys[0])))}) return true emit_cb.call({"type": "reward_ineffective", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))}, int(att.get("ri", 0)), "奖励未生效(满层)%s" % String(att.get("label_suffix", "")), 0, diff --git a/game/core/settlement/combat_segment.gd b/game/core/settlement/combat_segment.gd index cf1c7b29..0c047327 100644 --- a/game/core/settlement/combat_segment.gd +++ b/game/core/settlement/combat_segment.gd @@ -1,8 +1,8 @@ class_name CombatSegment extends RefCounted # ③ 战斗段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 -# 职责:案C 闸复位 + 本轮扫雷事实推导(执拗 streak/连珠矢段数)+ 蓄力层注入 + 主角先手 -# (含连珠矢分段开单)+ 敌方行动兑现 + 地图伤害账殿后(补提交统一结账)。 +# 职责:案C 闸复位 + 本轮扫雷事实推导(执拗 streak 取数面)+ 蓄力层注入 + 主角先手 +# + 敌方行动兑现 + 地图伤害账殿后(补提交统一结账)。 # 调用序:主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕;hero hp=0 短路。 # 行发射/开单单点归宿主(oc)——案C 暂存闸归 RewardGrant(段持三引用直调,零转发壳)—— # 本函数体为原编排器 _combat_segment 逐字搬运(🔴 零行为变更契约)。 @@ -21,7 +21,7 @@ func _init(host, fx_host_ref: BattleFxHost, reward_grant_ref: RewardGrant) -> vo func run(ri: int) -> void: reward_grant.defer["on"] = false # 案C 闸复位——战斗段起开单即入队(主角/敌方/补提交的地图单按开单序统一结账) - # 本轮扫雷事实推导(执拗 streak/连珠矢段数共用取数面):本轮 safe 事件数= + # 本轮扫雷事实推导(执拗 streak 取数面):本轮 safe 事件数= # streak;踩雷即清零(mine_hit 监听同口——执拗「踩雷清零」承载)。攻击在战斗段=授予段后 # 同一次 _settle 内,facts 已定。 var round_safe := 0 @@ -56,18 +56,6 @@ func _hero_strike(ri: int) -> void: "ri": ri, "origin_cell": Vector2i(-1, -1), "attack_type": String(oc._enemy_system.hero_unit().get("attack_type", "normal"))}) oc._damage.submit(ht) - # 连珠矢(内容数据驱动扩展批 T3——attack_commit 订阅施加面:开格节奏变攻击段数,本轮每开 - # ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位 - # 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致) - for h in oc._hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"): - var segments := mini(int(h.get("cap", 2)), int(oc.fx_host().facts_inject["dig_streak"]) / maxi(1, int(h.get("divisor", 3)))) - for s in segments: - # source_class 用中性名;attack_segments 兑付通道保留泛用面 - var ct := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments", "连击分段", - {"channel": "board", "base_value": int(h.get("value", 0)), - "target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri, - "origin_cell": oc.minesweeper.hero_pos(), "attack_type": "pierce"}) - oc._damage.submit(ct) # 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。 @@ -83,9 +71,11 @@ func _enemy_actions(ri: int) -> void: continue if String(srow.type) == "buff" or String(srow.type) == "debuff": # 增益/妨害技 = 0 伤害施放行:不掷不折;状态授予即数值兑现,技能冷却与增益持续两计时器分账。 - # 落点由状态行 target_side 路由(buff → 敌方侧 / debuff → 主角侧——容器零改动,proposal-status-vocabulary §1)。 + # 落点按目标规则(增益状态 v1.5〈目标规则与个体归属〉):buff → 施放者个体(「自己」—— + # 敌方增益技只强化施放者自己,不再全体受益);debuff → 主角(off_guard/sapped 同义主角侧)。 var dur: int = int(srow.get("buff_duration", 1)) - oc._status_effects.apply_effect(String(srow.buff_ref), {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur) + var owner_key: String = StatusContainer.owner_key_for(u) if String(srow.type) == "buff" else StatusContainer.OWNER_HERO + oc._status_effects.apply_effect(String(srow.buff_ref), owner_key, {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur) oc._emit_row({"type": "cast", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1)}, ri, "%s 施放 %s" % [u.display_name, String(srow.display_name)], 0, {"skill_id": skill_id, "effect_id": String(srow.buff_ref), "duration": dur}) oc._enemy_system.commit_action(u) continue diff --git a/game/core/shop_session.gd b/game/core/shop_session.gd index be90aa47..8c1ceaac 100644 --- a/game/core/shop_session.gd +++ b/game/core/shop_session.gd @@ -53,12 +53,14 @@ func current_price(prow: Dictionary) -> int: + int(prow.get("price_increment", 0)) * purchased_tier(String(prow.get("product_id", "")))) -# 散折价单点(财富之道·散 economy_spend 激活时商品购买价 ×spend_discount〔表 knob,缺省 0.8〕, +# 散折价单点(财富之道·散 economy_spend 激活时商品购买价 ×discount_pct/100〔规则键参数字典—— +# 折率读 economy_spend 参数,零表字段〕, # floor 取整偏向玩家、下限 1 防 1 元商品折成 0;范围=商品标价定义边界〔legacy 商品面 current_price # + 池货架面 pool_price 同折〕——商店刷新价 shop_refresh_cost 与三选一付费刷新不折) func _spend_discounted(price: int) -> int: if _rules().has("economy_spend"): - return maxi(1, int(floor(float(price) * float(db.run.shop.spend_discount)))) + var spend_params: Dictionary[Variant, Variant] = _rules()["economy_spend"] as Dictionary + return maxi(1, int(floor(float(price) * float(spend_params["discount_pct"]) / 100.0))) return price @@ -152,7 +154,7 @@ func _draw_shelf() -> void: continue seen.append(k) shelf.append(tok) - if shelf.size() >= 3: + if shelf.size() >= int(db.run.shop.shelf_size): break # T5 专长凭证注入(非 Boss 渠道唯一专长入口——货架与节点共享保底计数;判定含 pity 记账,替换末位占一席。 # 取数复用本函数 shop_draw scope 的 stream——零新 scope,确定性序列不漂移) @@ -329,8 +331,8 @@ func _level_index() -> int: return int((_refs["level_row"] as Dictionary).get("level_index", 0)) -func _rules() -> Array: - return _refs["tree_rules"] as Array +func _rules() -> Dictionary[Variant, Variant]: + return _refs["tree_rules"] as Dictionary func _attunement() -> Array: diff --git a/game/core/status_container.gd b/game/core/status_container.gd index 391f2bd0..4a40a1c5 100644 --- a/game/core/status_container.gd +++ b/game/core/status_container.gd @@ -1,12 +1,25 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION -## 增益状态统一容器:三方效果同构实例、单一时限量纲(先兑付后衰减)、按行叠层与授予时快照抗热重载(垂直切片验证:增益减益逐行归因)。 +## 增益状态统一容器:三方效果同构实例、单一时限量纲(先兑付后衰减)、按行叠层与授予时快照抗热重载。 +## 实例按个体归属(增益状态 v1.5):主角一桶+每只敌各一桶——全体授予=授予路径逐只各发一条实例, +## 合并只在同体内(同名状态不同个体互不相干),个体死亡即清名下实例(purge_owner 单点)。 ## 零随机零信号——纯被动服务;授予唯一入口 apply_effect,满层拒绝零写入(拒收记未生效行归调用方)。 class_name StatusContainer extends RefCounted +const OWNER_HERO := "hero" # 主角个体键(单桶——主角全体状态唯一的归属键) + var db: ConfigDb var _open := false -var _instances: Array[Dictionary] = [] # 插入序;元素 = {effect_id, layers, remaining, source_class, origin_cell, round_index, snap_m, snap_unit, snap_display} +var _buckets: Dictionary[Variant, Variant] = {} # owner_key -> Array[Dictionary](桶内插入序);元素 = {effect_id, layers, remaining, source_class, origin_cell, round_index, snap_m, snap_unit, snap_display} + + +# 个体键约定单点(拼写禁散落第二处——授予/死亡清/消费面一律经此导出): +# 主角 = OWNER_HERO;敌人 = "e<生成序>"(生成序 = 开战逐只编号 handle_id 的 1 基序,沿顺位轴)。 +static func owner_key_for(u: Dictionary) -> String: + if bool(u.get("is_hero", false)): + return OWNER_HERO + assert(u.has("handle_id"), "[增益状态] 敌单位缺 handle_id(个体键导出沿生成序轴——单位违约)") + return "e%d" % (int(u.get("handle_id", -1)) + 1) func _init(db_ref: ConfigDb) -> void: @@ -18,35 +31,62 @@ func notify_battle_start() -> bool: push_warning("[增益状态] Active 下重复 notify_battle_start(EC11)") return false _open = true - _instances.clear() + _buckets.clear() return true # 唯一授予入口(挖掘期零直写——战斗结算 结算编排期统一调用)。 +# owner_key(个体归属——增益状态 v1.5):实例挂到该个体桶,合并/衰减/查询全在桶内; # duration_override(敌人技能池批——增益技施放):技能行 buff_duration 覆盖状态行时长(-1 = 状态表值)。 # 施放是持续量的 owner(proposal §2.2 字段词汇表"buff_duration 增益持续回合数");其他授予路径传缺省。 -func apply_effect(effect_id: String, source: Dictionary, duration_override: int = -1) -> bool: +func apply_effect(effect_id: String, owner_key: String, source: Dictionary = {}, duration_override: int = -1) -> bool: if not _open: push_warning("[增益状态] 域闭授予(EC1)") return false + if owner_key == "": + push_warning("[增益状态] 空 owner_key 授予(个体归属违约)") + return false + return _apply_open(effect_id, owner_key, source, duration_override) + + +# 开域授予主路径(守卫已过——未知行警告与三臂受理;单出口布尔受理) +func _apply_open(effect_id: String, owner_key: String, source: Dictionary, duration_override: int) -> bool: + var accepted := false var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(effect_id) if row.is_empty(): push_warning("[增益状态] 未知 effect_id(EC2): %s" % effect_id) return false var policy: String = row.stack_policy var dur: int = int(row.duration_settlements) if duration_override < 1 else duration_override - var max_stack: int = int(row.max_stack) var snap := _grant_snapshot(row, source) + var bucket: Array[Dictionary] = _bucket_of(owner_key) + var hit: Dictionary[Variant, Variant] = {} + for inst in bucket: + if inst.effect_id == effect_id: + hit = inst + break match policy: "independent": - _instances.append(_new_instance(effect_id, 1, dur, source, snap)) - return true + bucket.append(_new_instance(effect_id, 1, dur, source, snap)) + accepted = true "accumulate": - return _apply_accumulate(effect_id, dur, snap, source) + # 命中加时长未命中追加(R += D 取本次授予生效时长;L 不变;快照沿首授) + if hit.is_empty(): + bucket.append(_new_instance(effect_id, 1, dur, source, snap)) + else: + hit.remaining = int(hit.remaining) + dur + accepted = true "stack": - return _apply_stack(effect_id, dur, snap, source, max_stack) - push_warning("[增益状态] 未知 stack_policy") - return false + # 满层拒零写入·未满加层·未命中追加(stack 授予不触碰 R;拒收由战斗结算记"奖励未生效"行) + if hit.is_empty(): + bucket.append(_new_instance(effect_id, 1, dur, source, snap)) + accepted = true + elif int(hit.layers) < int(row.max_stack): + hit.layers = int(hit.layers) + 1 + accepted = true + _: + push_warning("[增益状态] 未知 stack_policy") + return accepted ## 授予时快照域(抗表重载——EC9;封印溢价贯穿——迭代14② T4 percent 形态同乘快照、缺省路径零扰动) @@ -57,27 +97,13 @@ func _grant_snapshot(row: Dictionary, source: Dictionary) -> Dictionary: return {"m": snap_m, "unit": String(row.unit), "display": String(row.display_name)} -## 累加臂:命中加时长未命中追加(R += D 取本次授予生效时长——覆盖值 ≥1 按覆盖值;L 不变;快照沿首授;每次授予必有剩余次数变化) -func _apply_accumulate(effect_id: String, dur: int, snap: Dictionary, source: Dictionary) -> bool: - for inst in _instances: - if inst.effect_id == effect_id: - inst.remaining = int(inst.remaining) + dur - return true - _instances.append(_new_instance(effect_id, 1, dur, source, snap)) - return true - - -## stack 臂:满层拒零写入·未满加层·未命中追加(stack 授予不触碰 R——spec 决议; -## 满层拒绝原子——容器零写入,拒收由战斗结算记"奖励未生效"行) -func _apply_stack(effect_id: String, dur: int, snap: Dictionary, source: Dictionary, max_stack: int) -> bool: - for inst in _instances: - if inst.effect_id == effect_id: - if int(inst.layers) >= max_stack: - return false - inst.layers = int(inst.layers) + 1 - return true - _instances.append(_new_instance(effect_id, 1, dur, source, snap)) - return true +## 个体桶取用(缺桶新建——实例只在桶内合并,不同体永不互见) +func _bucket_of(owner_key: String) -> Array[Dictionary]: + if not _buckets.has(owner_key): + var fresh: Array[Dictionary] = [] + _buckets[owner_key] = fresh + return fresh + return _buckets[owner_key] func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary, snap: Dictionary) -> Dictionary: @@ -94,16 +120,15 @@ func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary, } -# 只读投影(插入序稳定枚举;六归因字段 + display_name + 授予时快照) -func query_active(side: String) -> Array: +# 只读投影(个体桶插入序稳定枚举;owner_key 随行出户 + 六归因字段 + display_name + 授予时快照) +func query_active(owner_key: String) -> Array: var out: Array[Dictionary] = [] - for inst in _instances: + for inst in _buckets.get(owner_key, [] as Array[Dictionary]): var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(inst.effect_id) if row.is_empty(): continue - if String(row.target_side) != side: - continue out.append({ + "owner_key": owner_key, "effect_id": inst.effect_id, "layers": inst.layers, "remaining_settlements": inst.remaining, @@ -118,26 +143,47 @@ func query_active(side: String) -> Array: return out -# 轮末衰减:先兑付后衰减;每实例恰 −1;归 0 移除;返回移除数 k(恰一次执行由 战斗结算 编排信任承载) +# 侧汇总投影(主角侧 = hero 桶;敌方侧 = 各存活个体桶并集——死亡个体桶已随 purge_owner 删除, +# 并集即全部在册实例;数值与旧阵营共享读法等价,服务计数类汇总消费面) +func query_active_side(side: String) -> Array: + if side == OWNER_HERO: + return query_active(OWNER_HERO) + var out: Array[Dictionary] = [] + for key in _buckets.keys(): + if String(key) != OWNER_HERO: + out.append_array(query_active(String(key))) + return out + + +# 轮末衰减:先兑付后衰减;逐桶各 −1(每实例每结算恰一次);归 0 移除;返回移除数 k(恰一次执行由 战斗结算 编排信任承载) func decay_round() -> int: if not _open: return 0 # EC13 域闭兜底读法——容器恒空,无独立分支不诊断 var k := 0 - var survivors: Array[Dictionary] = [] - for inst in _instances: - inst.remaining = int(inst.remaining) - 1 - if inst.remaining <= 0: - k += 1 - else: - survivors.append(inst) - _instances = survivors + for key in _buckets.keys(): + var bucket: Array[Dictionary] = _buckets[key] + var survivors: Array[Dictionary] = [] + for inst in bucket: + inst.remaining = int(inst.remaining) - 1 + if inst.remaining <= 0: + k += 1 + else: + survivors.append(inst) + _buckets[key] = survivors return k +# 个体死亡即清(死亡清唯一通道——死亡个体名下实例全部移除,不转移、不并回别处;返回移除数) +func purge_owner(owner_key: String) -> int: + var removed := (_buckets.get(owner_key, [] as Array[Dictionary]) as Array).size() + _buckets.erase(owner_key) + return removed + + func notify_battle_end() -> bool: if not _open: push_warning("[增益状态] BattleClosed 下重复 notify_battle_end(EC11)") return false _open = false - _instances.clear() # 全清不跨战斗 + _buckets.clear() # 全清不跨战斗 return true diff --git a/game/data/schemas/action_economy_table.gd b/game/data/schemas/action_economy_table.gd index 9621c45b..c51819c6 100644 --- a/game/data/schemas/action_economy_table.gd +++ b/game/data/schemas/action_economy_table.gd @@ -4,7 +4,7 @@ class_name ActionEconomyTable extends Resource -@export var ap_per_round: int = 6 # [1,32] knob(默认值 = 现行表值基线,随 AP 压速调整) +@export var ap_per_round: int # [1,32] knob(零哨兵——表行缺字段越域即加载致命) @export var dig_ap_cost: int = 1 # [1,8] knob(域下限 1 = 零预算系统失效拒绝) @export var seal_place_ap_cost: int = 1 # [0,8] knob(无 seal_pull_ap_cost 字段——拔印不占 AP) @export var rap_refresh_cost: int = 6 # [1,64] knob(默认值随 AP 压速调整) diff --git a/game/data/schemas/board_gen_table.gd b/game/data/schemas/board_gen_table.gd index a8322475..7212dc2d 100644 --- a/game/data/schemas/board_gen_table.gd +++ b/game/data/schemas/board_gen_table.gd @@ -5,6 +5,7 @@ class_name BoardGenTable extends Resource @export var rows: Array[Dictionary] = [] # Array[Dictionary] +@export var spawn_edge: int = 0 # 出生区边长(奇数 [3,5]——零哨兵:表头缺字段=加载期域校验拒) const SOLVER_RULES := ["single", "single_subset"] const COST_BUDGET := 16384 # F2c provisional 量级护栏(禁墙钟逃生) const TARGET_ABS_FLOOR := 0.85 # 战斗结算 修订——target 域下限(P1 绝对锚) @@ -13,6 +14,8 @@ const BOARD_TYPE_VOCABULARY := ["dungeon", "underdark", "deep_underdark"] # func validate() -> Array: var errs: Array[Dictionary] = [] + if spawn_edge < 3 or spawn_edge > 5 or spawn_edge % 2 == 0: + errs.append({"category": "domain", "message": "spawn_edge 越域(奇数 [3,5]——零缺省): %d" % spawn_edge}) if rows.is_empty(): errs.append({"category": "empty_table", "message": "BoardGenTable 为空"}) return errs @@ -73,8 +76,8 @@ func _validate_row_constraints(row: Dictionary) -> Array[Dictionary]: var att: int = int(row.get("max_gen_attempts", 0)) if att < 1 or att > 32: errs.append({"category": "domain", "message": "(%d,%d) max_gen_attempts 越域 [1,32]" % [L, D]}) - # 布雷数下限(R3)——零雷板拒绝(−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS) - var n_mines := int(round(d * float(w * h - BoardData.SPAWN_AREA_CELLS))) + # 布雷数下限(R3)——零雷板拒绝(−spawn_edge² = 出生区格数,表头单点导出) + var n_mines := int(round(d * float(w * h - spawn_edge * spawn_edge))) if n_mines < 1: errs.append({"category": "cross_field", "message": "(%d,%d) 布雷数下限违例 round(d×(W×H−9)) ≥ 1" % [L, D]}) # F2b min_safe 可行性 @@ -102,9 +105,9 @@ func _validate_shave_params(row: Dictionary) -> Array[Dictionary]: errs.append({"category": "domain", "message": "(%d,%d) shave_ring_radius 越域 [0,2]: %d" % [L, D, sr]}) var r0_cap: int = int(row.get("r0_max_cap", 0)) if r0_cap > 0: - # 护栏(需求卡片 §二):cap ≥ 出生格数 9(BoardData.SPAWN_AREA_CELLS)、cap ≤ 板格数一半 - if r0_cap < BoardData.SPAWN_AREA_CELLS: - errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 低于出生保障面 %d: %d" % [L, D, BoardData.SPAWN_AREA_CELLS, r0_cap]}) + # 护栏(需求卡片 §二):cap ≥ 出生格数(spawn_edge² 表头导出)、cap ≤ 板格数一半 + if r0_cap < spawn_edge * spawn_edge: + errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 低于出生保障面 %d: %d" % [L, D, spawn_edge * spawn_edge, r0_cap]}) if r0_cap > (w * h) / 2: errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 超板格数一半 %d: %d" % [L, D, (w * h) / 2, r0_cap]}) return errs diff --git a/game/data/schemas/difficulty_table.gd b/game/data/schemas/difficulty_table.gd index f7ff7cf5..6b4a4e7d 100644 --- a/game/data/schemas/difficulty_table.gd +++ b/game/data/schemas/difficulty_table.gd @@ -22,7 +22,7 @@ func validate() -> Array: seen[key] = true if L < 1 or D < 0: errs.append({"category": "domain", "message": "难度键越域 (%d,%d)" % [L, D]}) - # 乘数越域 = warning 级(使用时 clamp——组合事故非配置事故,难度曲线与Boss GDD Edge Case 预登记) + # 乘数越域 = warning 级(使用时 clamp 到 MULT_MIN/MULT_MAX——组合事故非配置事故,warning 预登记) for k in ["hp_mult", "atk_mult"]: var v: float = float(row.get(k, 1.0)) if v < MULT_MIN or v > MULT_MAX: diff --git a/game/data/schemas/feat_table.gd b/game/data/schemas/feat_table.gd index 4aa81b2e..1b36ad62 100644 --- a/game/data/schemas/feat_table.gd +++ b/game/data/schemas/feat_table.gd @@ -1,4 +1,5 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION + ## 专长(奇物·Feat)定义表 schema:逐 feat_id 行订阅式效果声明的类型化读取(空表合法态;垂直切片验证:订阅行经效果引擎统一解释执行)。 ## 🔴 层级纪律:专长只走局内随机渠道(Boss 三选一/凭证开启)——本表行永不入同调位表/局外树。 ## triggers 订阅行四词表封闭校验单点在 EffectEngine.validate_trigger_row——本表不自建第二套校验。 @@ -36,8 +37,7 @@ func validate() -> Array: errs.append({"category": "domain", "message": "%s display_name 空" % rid}) if not RARITIES.has(String(row.get("rarity", ""))): errs.append({"category": "enum", "message": "%s rarity 出域 %s" % [rid, str(RARITIES)]}) - if row.has("cap") and int(row.get("cap", 1)) < 1: - errs.append({"category": "domain", "message": "%s cap 出域 [1,∞): %d" % [rid, int(row.get("cap", 1))]}) + errs.append_array(_validate_cap(row, rid)) if not row.has("triggers"): errs.append({"category": "domain", "message": "%s 缺 triggers(订阅形态唯一合法行形)" % rid}) else: @@ -48,6 +48,16 @@ func validate() -> Array: return errs +## cap 域(必填零兜底——缺行拒、越域 [1,99] 拒;域=config_db_cross_validator 在册口径) +func _validate_cap(row: Dictionary, rid: String) -> Array[Dictionary]: + var errs: Array[Dictionary] = [] + if not row.has("cap"): + errs.append({"category": "domain", "message": "%s 缺 cap(必填——零兜底)" % rid}) + elif int(row.get("cap", 0)) < 1 or int(row.get("cap", 0)) > 99: + errs.append({"category": "domain", "message": "%s cap 越域 [1,99]: %d" % [rid, int(row.get("cap", 0))]}) + return errs + + func row_by_id(feat_id: String) -> Dictionary: for row in rows: if String(row.get("feat_id", "")) == feat_id: diff --git a/game/data/schemas/level_gen_params.gd b/game/data/schemas/level_gen_params.gd index 53284bcd..40563cc5 100644 --- a/game/data/schemas/level_gen_params.gd +++ b/game/data/schemas/level_gen_params.gd @@ -7,7 +7,8 @@ extends Resource const RATIO_KEYS := ["battle_ratio", "reward_ratio", "rest_ratio", "shop_ratio"] const ROW_KEYS := ["level_index", "layers", "branch_density", "entry_count", "exit_count", - "battle_floor", "fork_ways_min", "fork_ways_max"] + RATIO_KEYS + "battle_floor", "fork_ways_min", "fork_ways_max", + "fork_third_way_pct", "fill_rest_first_pct", "width_max"] + RATIO_KEYS @export var rows: Array[Dictionary] = [] # 每行 = ROW_KEYS 键集(level_index 唯一正整数) @@ -65,6 +66,16 @@ func _validate_row_shape(row: Dictionary, L: int, key: String, layers_by_level: var bd: float = float(row.get("branch_density", -1.0)) if bd < 0.0 or bd > 1.0: errs.append({"category": "domain", "message": "%s 分支密度 %f 越域 [0,1]" % [key, bd]}) + # 域并单点(关卡节点图生成.md v1.4 调参表——生成器侧域校验退役,唯此一处) + var ftw: float = float(row.get("fork_third_way_pct", -1.0)) + if ftw < 0.0 or ftw > 100.0: + errs.append({"category": "domain", "message": "%s fork_third_way_pct %f 越域 [0,100]" % [key, ftw]}) + var frf: float = float(row.get("fill_rest_first_pct", -1.0)) + if frf < 0.0 or frf > 100.0: + errs.append({"category": "domain", "message": "%s fill_rest_first_pct %f 越域 [0,100]" % [key, frf]}) + var wmax: int = int(row.get("width_max", 0)) + if wmax < 2 or wmax > 6: + errs.append({"category": "domain", "message": "%s width_max %d 越域 [2,6]" % [key, wmax]}) var ratio_sum := 0.0 for rk in RATIO_KEYS: var v: float = float(row.get(rk, -1.0)) diff --git a/game/data/schemas/meta_tree_table.gd b/game/data/schemas/meta_tree_table.gd index e88ceae2..4d91f524 100644 --- a/game/data/schemas/meta_tree_table.gd +++ b/game/data/schemas/meta_tree_table.gd @@ -23,6 +23,16 @@ const SPINE_ROW_GATE := 4 ## 无 fg_mastery 克制专精组——类型表达归锻造工坊) const FORK_GROUPS := ["fg_posture", "fg_sense", "fg_economy"] +## 规则键参数域表(键集与取值域逐字=天赋树.md v0.12 规则键参数表——文档定域、schema 执行) +const RULE_KEY_PARAMS := { + "posture_first_strike": {"ap_bonus": [1, 4]}, + "posture_bulwark": {"shield": [1, 9]}, + "sense_sharp": {"halve_pct": [1, 99], "cap_per_level": [1, 9]}, + "sense_guard": {"ap_refund": [1, 4], "cap_per_level": [1, 9]}, + "economy_hoard": {"mult_pct": [101, 300]}, + "economy_spend": {"discount_pct": [1, 99]}, +} + func validate() -> Array: var errs: Array[Variant] = [] @@ -42,6 +52,7 @@ func validate() -> Array: errs.append_array(_validate_row_head(row, nid)) errs.append_array(_validate_row_numbers(row, nid)) errs.append_array(_validate_row_effects(row, nid)) + errs.append_array(_validate_rule_key_params(row, nid)) errs.append_array(_validate_row_fork(row, nid, fork_seen)) errs.append_array(_validate_requires_dangling(rows, seen)) errs.append_array(_validate_requires_level(rows, seen)) @@ -101,6 +112,43 @@ func _validate_row_effects(row: Dictionary, nid: String) -> Array[Variant]: return errs +## 规则键参数域校验:effects/effects_a/effects_b 中凡 RULE_KEYS 成员的值必须是 Dictionary、 +## 键集恰等 RULE_KEY_PARAMS[键] 的键集(缺参数/多参数即拒)、每值 int 且在闭域内(零缺省——缺字段=加载期报错) +func _validate_rule_key_params(row: Dictionary, nid: String) -> Array[Variant]: + var errs: Array[Variant] = [] + for fx_key in ["effects", "effects_a", "effects_b"]: + var fx: Dictionary[Variant, Variant] = row.get(fx_key, {}) as Dictionary + for rk in fx.keys(): + if not RULE_KEYS.has(String(rk)): + continue + errs.append_array(_validate_one_rule_key_params(nid, fx_key, String(rk), fx[rk])) + return errs + + +## 单规则键参数字典校验(值须为 Dictionary、键集恰等参数域表、每值 int 且在闭域内) +func _validate_one_rule_key_params(nid: String, fx_key: String, rk: String, params_v: Variant) -> Array[Variant]: + var errs: Array[Variant] = [] + if not (params_v is Dictionary): + errs.append({"category": "domain", "message": "%s %s.%s 缺参数(规则键值须为参数字典——零缺省)" % [nid, fx_key, rk]}) + return errs + var params: Dictionary[Variant, Variant] = params_v + var expect: Dictionary[Variant, Variant] = RULE_KEY_PARAMS[rk] + for pk in params.keys(): + if not expect.has(String(pk)): + errs.append({"category": "domain", "message": "%s %s.%s 多参数: %s(参数域表封闭)" % [nid, fx_key, rk, String(pk)]}) + elif not (params[pk] is int): + errs.append({"category": "domain", "message": "%s %s.%s.%s 越域(须 int)" % [nid, fx_key, rk, String(pk)]}) + else: + var dom: Array[Variant] = expect[String(pk)] + var v := int(params[pk]) + if v < int(dom[0]) or v > int(dom[1]): + errs.append({"category": "domain", "message": "%s %s.%s.%s 越域 [%d,%d]: %d" % [nid, fx_key, rk, String(pk), int(dom[0]), int(dom[1]), v]}) + for pk in expect.keys(): + if not params.has(String(pk)): + errs.append({"category": "domain", "message": "%s %s.%s 缺参数: %s(参数域表封闭)" % [nid, fx_key, rk, String(pk)]}) + return errs + + ## Fork 结构域:一节点双态结构判据(fork_group 词表/组唯一/主 effects 须空/a/b 双键规则面) func _validate_row_fork(row: Dictionary, nid: String, fork_seen: Dictionary) -> Array[Variant]: var errs: Array[Variant] = [] diff --git a/game/data/schemas/shop_table.gd b/game/data/schemas/shop_table.gd index 768493e2..973979c0 100644 --- a/game/data/schemas/shop_table.gd +++ b/game/data/schemas/shop_table.gd @@ -13,7 +13,7 @@ extends Resource @export var price_a: float = 0.0 # 进店已购次数爬价因子(provisional——0 = 未投值即致命域) @export var price_b: float = 0.0 # 跨关递增因子(provisional) @export var relic_base_price: float = 15.0 # relic 保底卡基价(provisional——池令牌非货架种,词表外专枝) -@export var spend_discount: float = 0.8 # 财富之道·散 economy_spend 激活时的购买折扣(provisional) +@export var shelf_size: int = 0 # 货架位数(零哨兵——缺字段越域 [1,6] 即加载致命) const GP_TIERS := ["normal", "elite", "boss"] const PRODUCT_ATTRS := ["atk", "max_hp", "armor_class"] @@ -131,8 +131,8 @@ func _validate_price_factors(errs: Array[Dictionary]) -> void: errs.append({"category": "domain", "message": "price_b 越域 [1.0,2.0]: %f" % price_b}) if relic_base_price < 1.0 or relic_base_price > 99.0: errs.append({"category": "domain", "message": "relic_base_price 越域 [1.0,99.0]: %f" % relic_base_price}) - if spend_discount <= 0.0 or spend_discount > 1.0: - errs.append({"category": "domain", "message": "spend_discount 越域 (0.0,1.0]: %f" % spend_discount}) + if shelf_size < 1 or shelf_size > 6: + errs.append({"category": "domain", "message": "shelf_size 越域 [1,6]: %d" % shelf_size}) # ---- 行族访问器(自 ConfigDB 归位——行集解析单点;消费面 db.run.shop. 直连)---- diff --git a/game/data/schemas/variation_table.gd b/game/data/schemas/variation_table.gd index 1b4af099..537fa9bf 100644 --- a/game/data/schemas/variation_table.gd +++ b/game/data/schemas/variation_table.gd @@ -9,9 +9,10 @@ extends Resource # 布置参数(表头字段;数量公式 = round(候选池格数 × percent_of_cells / 100) + (关卡序号 − 1) × per_level_bonus, # 连通片生长、单片 ≤ pack_max——图规模百分比 × 关卡递增,全连通、单片不超 4; # 百分比基数 = 候选池(安全格 − 初始展开区,即实际可挖格)而非总格数) -@export var percent_of_cells: int = 5 -@export var per_level_bonus: int = 2 -@export var pack_max: int = 4 +@export var percent_of_cells: int = 0 +@export var per_level_bonus: int = 0 +@export var pack_max: int = 0 +@export var pack_min: int = 0 # 成包下界(零哨兵——缺字段越域即加载致命) const SCOPES := ["region", "global"] # resource_face = 资源面偏斜(全局作用域变异——整图修摇号分布:生存族按 effect_value% @@ -24,6 +25,7 @@ const PLACEMENTS := ["closed_only", "any"] # 布置约束 func validate() -> Array: # 空清单是合法状态(地形变异 P0)——零行即零变体格,配置加载零错误、零警告 var errs: Array[Dictionary] = [] + errs.append_array(_validate_header_domains()) var seen := {} for row in rows: var head := _validate_row(row, seen) @@ -36,6 +38,20 @@ func validate() -> Array: return errs +## 表头域校验单点(地形变异.md v1.2 调参表 owner 域——零缺省,缺字段/越域即拒) +func _validate_header_domains() -> Array[Dictionary]: + var errs: Array[Dictionary] = [] + if percent_of_cells < 0 or percent_of_cells > 50: + errs.append({"category": "domain", "message": "percent_of_cells 越域 [0,50]: %d" % percent_of_cells}) + if per_level_bonus < 0 or per_level_bonus > 9: + errs.append({"category": "domain", "message": "per_level_bonus 越域 [0,9]: %d" % per_level_bonus}) + if pack_min < 1 or pack_min > pack_max: + errs.append({"category": "domain", "message": "pack_min 越域 [1,pack_max=%d]: %d" % [pack_max, pack_min]}) + if pack_max < pack_min or pack_max > 9: + errs.append({"category": "domain", "message": "pack_max 越域 [pack_min=%d,9]: %d" % [pack_min, pack_max]}) + return errs + + ## 行域:variant_id 非空与查重/display_name/unlock_level/四词表(scope/effect/trigger/placement) func _validate_row(row: Dictionary, seen: Dictionary) -> Dictionary: var errs: Array[Dictionary] = [] diff --git a/game/data/tables/board_gen.tres b/game/data/tables/board_gen.tres index 15d9393b..6154d94b 100644 --- a/game/data/tables/board_gen.tres +++ b/game/data/tables/board_gen.tres @@ -4,6 +4,7 @@ [resource] script = ExtResource("1") +spawn_edge = 3 rows = [{ "level_index": 1, "node_depth": 0, @@ -411,7 +412,7 @@ rows = [{ ; 切片调参登记(SLICE-TUNED provisional,调参所有权仍归 C3): ; A1 GDD 参考行(d0.10/0.12)经"先布后删"环内移除后仅幸存 2-3 雷、R0/|S|约0.97、残余挖掘 2-4 格—— -; 掘近退化板(A2 反例 B 机制的低密度放大)。实验数据(8 种子均值,smoke/experiment_density.gd): +; 掘近退化板(A2 反例 B 机制的低密度放大)。实验数据(8 种子均值,出处=退役件 experiment_density.gd(git 历史可考)): ; d0.25 → L1 幸存雷 11/残余 13/inf 0.925/att 2.9 · L2 雷 19/残余 21/inf 0.908/att 7.9(seed42 实测 inf 0.889→target 定 0.88 首过),零降级告警。 ; target 收窄至 0.88/0.88(不低于 0.85 绝对锚)为密度上移的对价——B1 条款③⑤对账重跑义务随调参触发。 ; 2026-09-11 关卡扩展批:L1 增 D1–D4 行(Boss=Dmax=4)。 diff --git a/game/data/tables/feats.tres b/game/data/tables/feats.tres index 6bb2bf5c..28687a22 100644 --- a/game/data/tables/feats.tres +++ b/game/data/tables/feats.tres @@ -20,6 +20,7 @@ rows = [ "feat_id": "twin_pick", "display_name": "双管镐", "rarity": "common", + "cap": 1, "desc": "每第 2 次付费开格返 1 AP", "triggers": [ { @@ -52,6 +53,7 @@ rows = [ "feat_id": "geologist", "display_name": "地质学家", "rarity": "common", + "cap": 1, "desc": "每首开一种新类型资源格集 1 点,满 4 种→揭示 1 格", "triggers": [ { @@ -106,6 +108,7 @@ rows = [ "feat_id": "hard_hat", "display_name": "安全帽", "rarity": "excellent", + "cap": 1, "desc": "开安全格攒 1 层;触陷阱耗层,每层减 1 伤害(一次清零)", "triggers": [ { @@ -145,6 +148,7 @@ rows = [ "feat_id": "skip_mining", "display_name": "跳格采掘", "rarity": "excellent", + "cap": 1, "desc": "挖开空白格攒 1 点,3 点→揭示 1 格", "triggers": [ { @@ -199,6 +203,7 @@ rows = [ "feat_id": "blast_accounting", "display_name": "爆破会计", "rarity": "common", + "cap": 1, "desc": "轮末:本轮开格花费 AP≥5 → +1 GP", "triggers": [ { @@ -231,6 +236,7 @@ rows = [ "feat_id": "frontier_miner", "display_name": "边陲矿工", "rarity": "common", + "cap": 1, "desc": "挖开棋盘边缘格:产出 +1", "triggers": [ { @@ -253,6 +259,7 @@ rows = [ "feat_id": "seal_echo", "display_name": "封印回响", "rarity": "excellent", + "cap": 1, "desc": "封印命中计数;轮末计数≥2 → 返 1 AP(下轮)", "triggers": [ { @@ -301,6 +308,7 @@ rows = [ "feat_id": "warning_beacon", "display_name": "示警烟火", "rarity": "common", + "cap": 1, "desc": "封印命中:揭示周围 3×3 的资源类型(不揭安危)", "triggers": [ { @@ -316,6 +324,7 @@ rows = [ "feat_id": "seal_smith", "display_name": "封印匠", "rarity": "rare", + "cap": 1, "desc": "封印命中每第 3 次 → 该危险格中立移除", "triggers": [ { @@ -348,6 +357,7 @@ rows = [ "feat_id": "seal_stake", "display_name": "封印桩", "rarity": "common", + "cap": 1, "desc": "封印命中:本轮 AC +1", "triggers": [ { @@ -364,6 +374,7 @@ rows = [ "feat_id": "pain_memory", "display_name": "疼痛记忆", "rarity": "rare", + "cap": 1, "desc": "触陷阱攒 1 层;下次攻击附加层值伤害,用后清零", "triggers": [ { @@ -411,6 +422,7 @@ rows = [ "feat_id": "fuse_wire", "display_name": "保险丝", "rarity": "rare", + "cap": 1, "desc": "单次受伤 ≥5 攒 1 点,2 点→护盾 1 层", "triggers": [ { @@ -465,6 +477,7 @@ rows = [ "feat_id": "blood_insight", "display_name": "见血开窍", "rarity": "common", + "cap": 1, "desc": "触陷阱:揭示 1 格", "triggers": [ { @@ -480,6 +493,7 @@ rows = [ "feat_id": "trap_instinct", "display_name": "陷阱本能", "rarity": "excellent", + "cap": 1, "desc": "触陷阱:本轮攻击 +2(走攻击面板乘区)", "triggers": [ { @@ -503,6 +517,7 @@ rows = [ "feat_id": "chain_traps", "display_name": "连环陷阱", "rarity": "excellent", + "cap": 1, "desc": "同轮第 2 次及以后的触陷阱:每次额外 +1 层(疼痛记忆同账)", "triggers": [ { @@ -535,6 +550,7 @@ rows = [ "feat_id": "trap_burst", "display_name": "陷阱爆发", "rarity": "rare", + "cap": 1, "desc": "触陷阱:全体存活敌受陷阱伤×1.5 直伤", "triggers": [ { @@ -550,6 +566,7 @@ rows = [ "feat_id": "blood_honey", "display_name": "血蜜", "rarity": "common", + "cap": 1, "desc": "触陷阱:回复 2 生命(陷阱伤仍净亏血)", "triggers": [ { @@ -565,6 +582,7 @@ rows = [ "feat_id": "stone_skin", "display_name": "石肤", "rarity": "excellent", + "cap": 1, "desc": "受伤:AC +1(本场累计,上限 3)", "triggers": [ { @@ -582,6 +600,7 @@ rows = [ "feat_id": "last_stand", "display_name": "背水", "rarity": "rare", + "cap": 1, "desc": "生命≤50%:受伤额外 −2", "triggers": [ { @@ -604,6 +623,7 @@ rows = [ "feat_id": "breaker_strike", "display_name": "破封一击", "rarity": "excellent", + "cap": 1, "desc": "挖开带封印的危险格:+1 层(疼痛记忆同账)", "triggers": [ { @@ -627,6 +647,7 @@ rows = [ "feat_id": "map_exchange", "display_name": "弃图兑换", "rarity": "excellent", + "cap": 1, "desc": "停手:剩余 AP≥3 → 折为攻击伤害", "triggers": [ { @@ -682,6 +703,7 @@ rows = [ "feat_id": "last_drop", "display_name": "一滴不剩", "rarity": "common", + "cap": 1, "desc": "上轮 AP 耗尽 → +1 AP", "triggers": [ { @@ -705,6 +727,7 @@ rows = [ "feat_id": "market_close", "display_name": "收盘", "rarity": "common", + "cap": 1, "desc": "轮末:本轮开格 ≥8 → +1 GP", "triggers": [ { @@ -736,6 +759,7 @@ rows = [ "feat_id": "execution_scholar", "display_name": "斩首学者", "rarity": "common", + "cap": 1, "desc": "击杀:揭示 1 格", "triggers": [ { @@ -751,6 +775,7 @@ rows = [ "feat_id": "executioner", "display_name": "处刑台", "rarity": "excellent", + "cap": 1, "desc": "敌血量≤30%:主角对其伤害 +2", "triggers": [ { @@ -773,6 +798,7 @@ rows = [ "feat_id": "field_dressing", "display_name": "战地包扎", "rarity": "common", + "cap": 1, "desc": "战斗胜利:回复 3 生命", "triggers": [ { @@ -788,6 +814,7 @@ rows = [ "feat_id": "blood_tax", "display_name": "血税", "rarity": "rare", + "cap": 1, "desc": "本局每触一次陷阱,最大生命 −2;每封印命中一次,攻击 +1(下一战起生效)", "triggers": [ { @@ -812,6 +839,7 @@ rows = [ "feat_id": "venom_edge", "display_name": "淬毒刃纹", "rarity": "rare", + "cap": 1, "desc": "克制命中追加 +2 伤害", "triggers": [ { @@ -834,6 +862,7 @@ rows = [ "feat_id": "seal_burst", "display_name": "封印爆裂", "rarity": "rare", + "cap": 1, "desc": "封印命中:全体存活敌 3 点群伤", "triggers": [ { @@ -850,6 +879,7 @@ rows = [ "feat_id": "perfect_insurance", "display_name": "完美保险", "rarity": "rare", + "cap": 1, "desc": "本场首次封印落空,不计入完美封印判定", "triggers": [ { @@ -891,6 +921,7 @@ rows = [ "display_name": "省力周转", "rarity": "excellent", "cap": 1, + "cap": 1, "desc": "刷新半价:弃图刷新成本为基线 50%", "triggers": [ { @@ -908,6 +939,7 @@ rows = [ "display_name": "崩壁冲击", "rarity": "excellent", "cap": 1, + "cap": 1, "desc": "刷后群伤:成功刷新后全体存活敌各受 2 点", "triggers": [ { @@ -925,6 +957,7 @@ rows = [ "display_name": "矿道红利", "rarity": "rare", "cap": 1, + "cap": 1, "desc": "刷后GP:每次成功刷新固定 +1 GP", "triggers": [ { @@ -941,6 +974,7 @@ rows = [ "display_name": "石层缓冲", "rarity": "common", "cap": 1, + "cap": 1, "desc": "当轮减伤:本轮成功刷新后,每次受伤固定 −2(多刷不叠加)", "triggers": [ { @@ -973,6 +1007,7 @@ rows = [ "display_name": "蓄力周转", "rarity": "excellent", "cap": 1, + "cap": 1, "desc": "刷后蓄力:每次刷新 +1 层(上限 3),下一次攻击消费全部层数", "triggers": [ { @@ -1029,6 +1064,7 @@ rows = [ "display_name": "坑道小憩", "rarity": "common", "cap": 1, + "cap": 1, "desc": "当轮恢复:每次刷新累计 1 点(上限 3),本轮轮末合计回复", "triggers": [ { diff --git a/game/data/tables/level_gen_params.tres b/game/data/tables/level_gen_params.tres index 2aa43701..6245764b 100644 --- a/game/data/tables/level_gen_params.tres +++ b/game/data/tables/level_gen_params.tres @@ -16,7 +16,10 @@ rows = [{ "exit_count": 1, "battle_floor": 4, "fork_ways_min": 2, -"fork_ways_max": 3 +"fork_ways_max": 3, +"fork_third_way_pct": 35, +"fill_rest_first_pct": 50, +"width_max": 4 }, { "level_index": 2, "layers": 9, @@ -29,7 +32,10 @@ rows = [{ "exit_count": 2, "battle_floor": 6, "fork_ways_min": 2, -"fork_ways_max": 3 +"fork_ways_max": 3, +"fork_third_way_pct": 35, +"fill_rest_first_pct": 50, +"width_max": 4 }, { "level_index": 3, "layers": 13, @@ -42,7 +48,10 @@ rows = [{ "exit_count": 3, "battle_floor": 8, "fork_ways_min": 2, -"fork_ways_max": 3 +"fork_ways_max": 3, +"fork_third_way_pct": 35, +"fill_rest_first_pct": 50, +"width_max": 4 }] ; 节点图生成器核心批 卡① §8.1 参数档 provisional 取值表逐行填列(定值批落终值,数值全 provisional)。 diff --git a/game/data/tables/meta_tree.tres b/game/data/tables/meta_tree.tres index e91f16f5..27e4d049 100644 --- a/game/data/tables/meta_tree.tres +++ b/game/data/tables/meta_tree.tres @@ -113,10 +113,14 @@ rows = [{ "name_a": "先手压制", "name_b": "稳健拓荒", "effects_a": { - "posture_first_strike": 1 + "posture_first_strike": { + "ap_bonus": 1 + } }, "effects_b": { - "posture_bulwark": 1 + "posture_bulwark": { + "shield": 1 + } } }, { "node_id": "fork_sense", @@ -131,10 +135,16 @@ rows = [{ "name_a": "锋", "name_b": "守", "effects_a": { - "sense_sharp": 1 + "sense_sharp": { + "halve_pct": 50, + "cap_per_level": 2 + } }, "effects_b": { - "sense_guard": 1 + "sense_guard": { + "ap_refund": 1, + "cap_per_level": 2 + } } }, { "node_id": "fork_economy", @@ -149,10 +159,14 @@ rows = [{ "name_a": "聚", "name_b": "散", "effects_a": { - "economy_hoard": 1 + "economy_hoard": { + "mult_pct": 150 + } }, "effects_b": { - "economy_spend": 1 + "economy_spend": { + "discount_pct": 80 + } } }] diff --git a/game/data/tables/shop.tres b/game/data/tables/shop.tres index 60b6f6a1..c6afa528 100644 --- a/game/data/tables/shop.tres +++ b/game/data/tables/shop.tres @@ -4,6 +4,7 @@ [resource] script = ExtResource("1") +shelf_size = 3 gp_drops = [{ "tier": "normal", "display_name": "普通战斗", @@ -93,7 +94,6 @@ pool_shelf = [{ price_a = 1.8 price_b = 1.1 relic_base_price = 15.0 -spend_discount = 0.8 ; GP+商店房实施批(2026-09-20 用户拍板,设计权威 design/research/proposal-gp-shop-room-2026-09-20.md 裁决记录): ; 掉落=均值±方差掷定(gp_drop 封闭流)+关级加值;精英行为预留行(当前无消费方,随精英节点内容批启用); diff --git a/game/data/tables/variation.tres b/game/data/tables/variation.tres index 5f7b2636..d3bc68b6 100644 --- a/game/data/tables/variation.tres +++ b/game/data/tables/variation.tres @@ -7,6 +7,7 @@ script = ExtResource("1") percent_of_cells = 5 per_level_bonus = 0 pack_max = 4 +pack_min = 2 rows = [{ "variant_id": "thorn", "display_name": "荆棘丛", diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 41c9713c..051ecd60 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -182,7 +182,7 @@ func _wire_adventure() -> void: # 冒险开局参数面构造(主菜单开新/续玩两入口共用:同调三键随开局下传。 # 取值口径与营地投影完全同源:装配集=save.attunement_equipped(save_manager.gd 访问器)、Y 刷新池= -# meta.attune_refresh_charges(当前等级行表值)、树 rule 键集=meta.tree_rule_keys(Fork 激活侧 effects); +# meta.attune_refresh_charges(当前等级行表值)、树规则参数字典=meta.tree_rules(Fork 激活侧 effects_*); # hero_override 沿用既有 meta 现算口径。static 形态=冒烟直呼单测本函数,免实例化整菜单场景)。 # 两根类型轴随开局下传(attack_type/armor_type——契约 2 所有权移交+契约 8 冒险开始快照; # 取值=meta.hero_type_axes()=存档选中路线类型轴(attack/armor 两键),未解锁/异常选中态回退锻造配置默认值)。 @@ -195,7 +195,7 @@ static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref: "armor_type": meta_ref.hero_type_axes()["armor"] if meta_ref != null else "", "attunement": save_manager_ref.meta_state.attune_state().equipped if save_manager_ref != null else [], "attune_refresh_y": meta_ref.attune_refresh_charges() if meta_ref != null else 0, - "tree_rules": meta_ref.tree_rule_keys() if meta_ref != null else [], + "tree_rules": meta_ref.tree_rules() if meta_ref != null else {}, } params.merge(extra, true) return params @@ -630,10 +630,10 @@ func _on_refresh_confirmed() -> void: var result := "rejected_rap" if rap < cost else "rejected_cap" logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_snapshot() + 1}) if result == "rejected_rap": - ui.battle.board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn") + ui.battle.board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 %d)" % [cost - rap, cost], "warn") ui.battle.board_zone.ap_bar.pulse_rap() else: - ui.battle.board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn") + ui.battle.board_zone.toast.show_toast("本轮刷新次数已用完(每轮 %d 次)" % int(db.run.action_economy.refresh_cap_per_round), "warn") ui.battle.board_zone.board_view.refresh() _refresh_presentations() diff --git a/game/presentation/playtest_logger.gd b/game/presentation/playtest_logger.gd index 4182051f..b8fb64b4 100644 --- a/game/presentation/playtest_logger.gd +++ b/game/presentation/playtest_logger.gd @@ -13,12 +13,15 @@ var _seed := 0 var _recent_steps: Array[Dictionary] = [] # 环形缓冲:最近结算行 var _recent_actions: Array[Dictionary] = [] # 环形缓冲:最近玩家动作 var _snap_count := 0 -var ap_per_round := 8 # 组合根注入(AP 消耗口径分母——行动经济 无推送信号) +var ap_per_round: int = 0 # 必填注入(零缺省——组合根 start_session 前显式设定,守卫拒未注入) var _ux_inputs := 0 # 本结算轮原始输入动作数(log_action 计数,快照后归零) # 开启会话(每次 start_level/start_battle 调用——按种子分文件)。返回日志路径。 func start_session(seed_val: int) -> String: + if ap_per_round < 1 or ap_per_round > 32: + push_error("[试玩日志器] ap_per_round 未注入或越域: %d——组合根必填注入" % ap_per_round) + return "" _seed = seed_val _recent_steps.clear() _recent_actions.clear() diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index 07ac8753..7c17035f 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -77,7 +77,7 @@ func name_line_probe() -> Dictionary: # ---- setter 面(presenter/组合根零改排) ---- # data = 战斗结算 presentation_snapshot enemies[] 投影:{order, display_name, hp, max_hp, enemy_id, -# pending_skill/pending_type/pending_name, effects[](敌方 side 增益——各立绘同值呈现)} +# pending_skill/pending_type/pending_name, effects[](该敌自己名下增益——各立绘各读各的,增益状态 v1.5 个体投影)} func set_data(d: Dictionary) -> void: st._display_name = String(d.get("display_name", "敌人")) st._enemy_id = String(d.get("enemy_id", "")) diff --git a/game/sim/adventure_monte_carlo.gd b/game/sim/adventure_monte_carlo.gd index 64277218..a669679d 100644 --- a/game/sim/adventure_monte_carlo.gd +++ b/game/sim/adventure_monte_carlo.gd @@ -47,6 +47,11 @@ func _initialize() -> void: var feat_mode := "off" if args.size() >= 7 and args[6] in ["off", "formed", "ab_a", "ab_b", "rules"]: feat_mode = args[6] + var db = load("res://core/config_db.gd").new() + if not db.load_tables("res://data/tables", false): + print("表加载失败") + quit(1) + return var feat_profile := {} if feat_mode == "formed": # 成型画像=五事件族覆盖 14 件 @@ -58,16 +63,11 @@ func _initialize() -> void: "last_drop", "market_close", "execution_scholar"]} elif feat_mode == "ab_a": - feat_profile = {"mode": "off", "tree_rules": ["posture_first_strike"]} + feat_profile = {"mode": "off", "tree_rules": _tree_rule_dict(db, ["posture_first_strike"])} elif feat_mode == "ab_b": - feat_profile = {"mode": "off", "tree_rules": ["posture_bulwark"]} + feat_profile = {"mode": "off", "tree_rules": _tree_rule_dict(db, ["posture_bulwark"])} elif feat_mode == "rules": - feat_profile = {"mode": "off", "tree_rules": Array(String(args[7] if args.size() >= 8 else "").split(",", false))} # V19——任意键集配对注入(Array 包裹防 PackedStringArray 被 adventure_runner is Array 守卫整组丢弃) - var db = load("res://core/config_db.gd").new() - if not db.load_tables("res://data/tables", false): - print("表加载失败") - quit(1) - return + feat_profile = {"mode": "off", "tree_rules": _tree_rule_dict(db, Array(String(args[7] if args.size() >= 8 else "").split(",", false)))} # V19——任意键集配对注入(规则键参数字典形态) var attune_profile := {} if attune_mode == "on": # 贪心画像:Slots=3(槽位判定源=树同调位节点点亮数,模拟画像=三节点 @@ -153,3 +153,21 @@ func _initialize() -> void: print("BATCH %s N=%d 终局计数=%s 日志=%s" % [policy, n, JSON.stringify(tally), log_path]) print("(口径注:零加成 = atk/hp 天赋格百分比均 0;关间大奖励(HP 上限提升+回满)按冒险结构设计自动兑付,未额外剥离 · 全程日志见 user://logs/ · 总耗时 %.1f 秒)" % [float(Time.get_ticks_msec() - t0) / 1000.0]) quit(0) + + +## 树规则参数字典构造(A/B 面注入单点——扫 meta_tree Fork 行 effects_a/b,命中 keys 集的键取其 +## 参数字典原样透传;db 参鸭子型沿 board_generator 先例——sim 面不在 S9 扫描面) +static func _tree_rule_dict(db, keys: Array[Variant]) -> Dictionary[Variant, Variant]: + var out: Dictionary[Variant, Variant] = {} + var keyset := {} + for k in keys: + keyset[String(k)] = true + for row in db.crossrun.meta_tree.rows: + if String(row.get("kind", "")) != "fork": + continue + for skey in ["effects_a", "effects_b"]: + var fx: Dictionary = row.get(skey, {}) as Dictionary + for rk in fx.keys(): + if keyset.has(String(rk)) and not out.has(String(rk)): + out[String(rk)] = fx[rk] + return out diff --git a/game/sim/auto_player.gd b/game/sim/auto_player.gd index a2d3b5b6..377dd8cd 100644 --- a/game/sim/auto_player.gd +++ b/game/sim/auto_player.gd @@ -146,6 +146,16 @@ static func _deduce(minesweeper) -> Dictionary: if not mines.has(c): mines[c] = true changed = true + # 0 格连锁算子(架构整改批补齐——与棋盘生成 _apply_chain_rule 对位):safe 集中未揭示格 + # 挖开必触发 0 格连锁(连锁只经安全格扩散=闭包集全安全,真实玩家挖 c 免费获得的信息面), + # 经 preview_reveal 真实闭包取新开格集并入 safe——双口径显式登记:unknown 不含 safe_set oracle + # 〔口径差异=board_generator 版含 oracle,本版 no-peek——:80-83 注释在账〕 + for c in (safe.keys() as Array).duplicate(): + if board.is_safe(c) and not minesweeper.is_revealed(c): + for rc in minesweeper.preview_reveal(c): + if not safe.has(rc): + safe[rc] = true + changed = true return {"safe": _row_major(safe), "mines": _row_major(mines)} diff --git a/game/sim/battle_driver.gd b/game/sim/battle_driver.gd index 9e0a908a..5081ac47 100644 --- a/game/sim/battle_driver.gd +++ b/game/sim/battle_driver.gd @@ -11,26 +11,84 @@ const AutoPlayer := preload("res://sim/auto_player.gd") const GUARD_ACTIONS := 400 # 单轮动作步防挂死上限(被收口探针原值逐一同值) -# 驱动一整轮挖掘。still_running = 调用方战斗活性谓词(缺省恒真): +# 驱动一整轮(驱动循环单点——通用签名)。still_running = 调用方战斗活性谓词(缺省恒真): # monte_carlo / skill_rhythm 传 `func(): return out.result == ""`(battle_finished 已发的轮内不再动手), # damage_probe 关卡驱动传 runner 绑定 + battle_result 谓词,battle 级探针传空即可。 -static func drive_round(settlement, still_running: Callable = Callable()) -> void: +# act_provider 契约(策略回调——func(settlement) -> Dictionary): +# {"kind":"stop"}/空字典 = 停手出环;单步 {"kind":"move"|"dig"|"seal"|"refresh","cell":V}(refresh 无 cell); +# 合步 {"kind":"move","cell":X,"next":{"kind":"dig","cell":Y}}——move 提交后查相位/轮次、变更即断弃 next。 +# on_action 契约(日志回调——func(submit_kind:String, cell:Vector2i, ok:bool, round_index:int)): +# submit_kind ∈ {move, dig, seal_place, refresh, stop}——轮末统一 stop 也经此回调(accept 取 outcomes[0].accepted); +# 空 Callable = 不记日志。 +# max_rejects:策略回调被拒连续上限(≤0 = 不设防——缺省形态零防线语义保持);ok 清零否则累加。 +# 缺省两参形态(act_provider 空)= AutoPlayer.choose_dig 原语义逐位:guard 400、异格先移、合步 dig、 +# 移位后相位/轮次离场断弃 next、轮末 stop——四调用方(dump_battle/monte_carlo/damage_probe/skill_rhythm)两参零改动。 +static func drive_round(settlement, still_running: Callable = Callable(), + act_provider: Callable = Callable(), on_action: Callable = Callable(), + max_rejects: int = 0) -> void: var start_round: int = settlement.round_index var minesweeper = settlement.minesweeper var guard := GUARD_ACTIONS + var rejects := 0 + var using_provider: bool = not act_provider.is_null() while _alive(still_running) and settlement.action_economy.phase == "Active" \ - and settlement.round_index == start_round and guard > 0: + and settlement.round_index == start_round and guard > 0 \ + and (max_rejects <= 0 or rejects < max_rejects): guard -= 1 - var plan := AutoPlayer.choose_dig(minesweeper) - if plan.is_empty(): + var act: Dictionary = act_provider.call(settlement) if using_provider else _default_act(minesweeper) + var kind := String(act.get("kind", "stop")) + if kind == "stop" or act.is_empty(): break - if plan.hero_cell != minesweeper.hero_pos(): - minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) - if settlement.action_economy.phase != "Active" or settlement.round_index != start_round: - break - minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) + if _submit_act(settlement, act, on_action, start_round): + rejects = 0 + else: + rejects += 1 if _alive(still_running) and settlement.action_economy.phase == "Active" and settlement.round_index == start_round: - minesweeper.settle_batch([{"action": "stop"}]) + var out_s: Array = settlement.minesweeper.settle_batch([{"action": "stop"}]) + _on(on_action, "stop", Vector2i(-1, -1), _accepted(out_s), start_round) + + +# 缺省策略(AutoPlayer.choose_dig 原生 plan 专用):同位直接 dig、异位先移合步 next=dig +static func _default_act(minesweeper) -> Dictionary: + var plan := AutoPlayer.choose_dig(minesweeper) + if plan.is_empty(): + return {"kind": "stop"} + if plan.hero_cell == minesweeper.hero_pos(): + return {"kind": "dig", "cell": plan.dig_cell} + return {"kind": "move", "cell": plan.hero_cell, "next": {"kind": "dig", "cell": plan.dig_cell}} + + +# 单步提交分派(move/dig/seal→settle_batch——seal 提交动作名 seal_place;refresh→player_request_refresh; +# 合步 move→next 递归提交——相位/轮次离场断弃,与原内联逐位同) +static func _submit_act(settlement, act: Dictionary, on_action: Callable, ri: int) -> bool: + var kind := String(act.get("kind", "stop")) + var cell: Vector2i = act.get("cell", Vector2i(-1, -1)) + var ok := false + if kind == "refresh": + ok = bool(settlement.player_request_refresh()) + _on(on_action, "refresh", Vector2i(-1, -1), ok, ri) + return ok + var action_name := "seal_place" if kind == "seal" else kind + var out_s: Array = settlement.minesweeper.settle_batch([{"action": action_name, "cell": cell}]) + ok = _accepted(out_s) + _on(on_action, action_name, cell, ok, ri) + if kind == "move": + var next: Dictionary = act.get("next", {}) + if next.is_empty(): + return ok + if settlement.action_economy.phase != "Active" or settlement.round_index != ri: + return ok # 移位后相位/轮次离场——断弃 next + _submit_act(settlement, next, on_action, ri) + return ok + + +static func _on(on_action: Callable, kind: String, cell: Vector2i, ok: bool, ri: int) -> void: + if not on_action.is_null(): + on_action.call(kind, cell, ok, ri) + + +static func _accepted(outcomes: Array) -> bool: + return not outcomes.is_empty() and bool(outcomes[0].accepted) static func _alive(still_running: Callable) -> bool: diff --git a/game/sim/calibrate_boards.gd b/game/sim/calibrate_boards.gd index 71daea40..86c04a8c 100644 --- a/game/sim/calibrate_boards.gd +++ b/game/sim/calibrate_boards.gd @@ -11,6 +11,7 @@ extends SceneTree class FakeDB: class _FakeBoardGen: var row: Dictionary + var spawn_edge: int = 0 # 表头成员(generate 直索引面——真值由 _initialize 逐行注入) func resolve_row(_L: int, _D: int) -> Dictionary: return row class _FakeCombat: @@ -42,9 +43,11 @@ func _initialize() -> void: # 初始展开收窄两参数透传(.get 缺省与生成器防御口径一致) "shave_ring_radius": int(row.get("shave_ring_radius", 1)), "r0_max_cap": int(row.get("r0_max_cap", 0)), + "spawn_edge": int(db.combat.board_gen.spawn_edge), # 出生区边长透传(读真表头、零字面量——组E FakeDB 模式不动) } var fdb := FakeDB.new() fdb.row = rd + fdb.combat.board_gen.spawn_edge = int(db.combat.board_gen.spawn_edge) # 表头注入(零字面量——读真表头) var mines: Array = [] var safes: Array = [] var r0s: Array = [] diff --git a/game/sim/experiment_density.gd b/game/sim/experiment_density.gd deleted file mode 100644 index 26879000..00000000 --- a/game/sim/experiment_density.gd +++ /dev/null @@ -1,72 +0,0 @@ -# VERTICAL SLICE - NOT FOR PRODUCTION -# 一次性实验:不同 mine_density 下的 R₀ 占比 / 幸存雷数 / 残余挖掘量 / 推理率——为切片临时调参提供数据。 -extends SceneTree - -func _initialize() -> void: - var db = load("res://core/config_db.gd").new() - db.load_tables("res://data/tables", true) - var board_generator = load("res://core/board_generator.gd").new() - print("=== 密度实验(L1 10×10 target0.88 / L2 12×12 target0.90,各 8 种子)===") - print("d\t|M|/N\tR₀\tR₀/|S|\t残余挖掘\tinf\tatt") - for d in [0.22, 0.25, 0.28, 0.30, 0.33]: - for cfg in [[10, 10, 0.88, 50], [12, 12, 0.90, 60]]: - var w: int = cfg[0] - var h: int = cfg[1] - var t: float = cfg[2] - var ms: int = cfg[3] - var agg := {"m": 0, "r0": 0, "rem": 0, "inf": 0.0, "att": 0, "n": 0} - for s in range(8): - var row := { - "level_index": 1, "node_depth": 0, "board_width": w, "board_height": h, - "mine_density": d, "inferability_target": t, "solver_rules": "single_subset", - "min_safe_count": ms, "max_gen_attempts": 16, - "board_type": "dungeon", - } - _patch_row(row, d) - var params := _params_from(row) - var rg := RandomNumberGenerator.new() - rg.seed = 1000 + s - var b = board_generator.generate(1, 0, rg, _db(db, row)) - if b == null: - continue - agg.m += b.mines.size() - agg.r0 += int(b.metrics.r0_count) - agg.rem += b.safe_cells().size() - int(b.metrics.r0_count) - agg.inf += float(b.metrics.inferability) - agg.att += int(b.metrics.attempts_used) - agg.n += 1 - if agg.n > 0: - var n := float(agg.n) - var n_placed: int = int(round(d * float(w * h - 9))) - print("%.2f %dx%d\t%d/%d\t%d\t%.2f\t%d\t%.3f\t%.1f" % [d, w, h, int(agg.m / n + 0.5), n_placed, int(agg.r0 / n + 0.5), agg.r0 / n / (w * h - int(agg.m / n + 0.5)), int(agg.rem / n + 0.5), agg.inf / n, agg.att / n]) - quit(0) - -# --- 简易 db 替身:只供给本实验的行 --- -var _row_override: Dictionary = {} - -func _patch_row(row: Dictionary, _d: float) -> void: - _row_override = row - -func _params_from(row: Dictionary) -> Dictionary: - return { - "L": 1, "D": 0, "w": int(row.board_width), "h": int(row.board_height), - "density": float(row.mine_density), "target": float(row.inferability_target), - "solver_rules": "single_subset", "min_safe": int(row.min_safe_count), - "max_attempts": int(row.max_gen_attempts), "board_type": "dungeon", - } - -class FakeDB: - class _FakeBoardGen: - var row: Dictionary - func resolve_row(_L: int, _D: int) -> Dictionary: - return row - class _FakeCombat: - var board_gen := _FakeBoardGen.new() - var combat := _FakeCombat.new() - func status_effect_row(_id: String) -> Dictionary: - return {} - -func _db(_real, row: Dictionary): - var f := FakeDB.new() - f.combat.board_gen.row = row - return f diff --git a/game/sim/experiment_density.gd.uid b/game/sim/experiment_density.gd.uid deleted file mode 100644 index 87cad61f..00000000 --- a/game/sim/experiment_density.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cg7jkrsc03rar diff --git a/game/sim/level_autoplay.gd b/game/sim/level_autoplay.gd index 60f23e6b..13f34021 100644 --- a/game/sim/level_autoplay.gd +++ b/game/sim/level_autoplay.gd @@ -20,9 +20,8 @@ # 运行前提:db 已 load_tables;run_level/run_adventure 均须全新 RngService(同种子 ⇒ 全程逐步可回放)。 # 实现注:信号处理体全部为静态方法 + bind 传状态——GDScript 多行 lambda 内嵌 match/控制流的解析不可靠。 extends RefCounted - const AutoPlayer := preload("res://sim/auto_player.gd") # 自动玩家策略单一实现 - +const BattleDriver := preload("res://sim/battle_driver.gd") # 庚案收敛:驱动循环单点(策略仍归 _decide_act 单点分派) # ── 普通档 v3 失误注入参数(踩雷期望=完美+2/板、每板少挖几个正确格)── # 标雷→误挖转化率:主踩雷源=卡死赌挖(不确定性概率命中),误挖只作偶发真手滑—— # 真人几乎不挖自己封印的格(p 过高会让首轮已证封印变保底命中,非失误形态) @@ -32,14 +31,12 @@ const NORMAL_SKIP_DIG_P := 0.05 # 空闲提前赌挖率:已证安全格在手时小概率先赌一格——完美系把不确定性攒到残局 # (残局 hazard→1=扫雷),真人早赌滚信息(低 hazard 期吃掉不确定性);p×~40 安全 AP/板 ≈ 4 次 const NORMAL_IDLE_GAMBLE_P := 0.10 - - static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, logger = null, policy: String = "normal", route: String = "min") -> Dictionary: var own_session := logger == null if own_session: logger = load("res://presentation/playtest_logger.gd").new() + logger.ap_per_round = int(db.run.action_economy.ap_per_round) # 注入序前置(守卫拒后开——R5 必填) logger.start_session(seed_val) - logger.ap_per_round = int(db.run.action_economy.ap_per_round) logger.log_run({"seed": seed_val, "level_index": level_index, "atk_pct": atk_pct, "hp_pct": hp_pct, "policy": policy, "route": route}) var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed_val) @@ -77,8 +74,6 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: if own_session: logger.close_session() return st - - # 单关驱动段(run_level 抽取——零自动推进停点 + 战斗自动玩家;policy 经 st 入 _autoplay_battle) # 入口停点(node_index 尚 -1 = 进节点前无 current_node)也经本分支驱动—— # 读 next_candidate_ids 候选(入口单候选)→ pick_next_node,同一 _enter_node 等价(零 RNG 漂移); @@ -100,8 +95,6 @@ static func _drive_level(runner, st: Dictionary, logger, policy: String, route: if this_settlement == null: break # level_finished 已发(settlement 置 null)而信号尚未置 result 的防御 _autoplay_battle(runner, this_settlement, st, logger) - - # 三关连探冒险驱动——AdventureRunner 编排:主种子恰一次 # (start_adventure 唯一 new_adventure 调用点)、大奖励立即确认、冒险终局置 result/summary。 # 返回结构 = run_level 同族 + summary / wallet_events / encounter_max_hp_seq / reward_events / @@ -116,8 +109,8 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_ var own_session := logger == null if own_session: logger = load("res://presentation/playtest_logger.gd").new() + logger.ap_per_round = int(db.run.action_economy.ap_per_round) # 注入序前置(守卫拒后开——R5 必填) logger.start_session(seed_val) - logger.ap_per_round = int(db.run.action_economy.ap_per_round) logger.log_run({"seed": seed_val, "adventure": true, "atk_pct": atk_pct, "hp_pct": hp_pct, "policy": policy, "route": route, "hero_override": hero_override}) var rng = load("res://core/rng_service.gd").new() @@ -137,7 +130,6 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_ "seal_placed": 0, "seal_hit": 0, "seal_miss": 0, "seal_hit_gain": 0, "seal_miss_penalty": 0, "prng": _policy_rng(seed_val), "mine_slips": 0, "skip_digs": 0, "shop_buys": 0, "adv": true, # v3 档位行为开关(选路评分/失误注入/商店购买只挂冒险测量面——单关夹具语义不变) - "level_maps": [], # 逐关节点图标识(生成图 = 关卡序号+本局主种子——MC 分层出数) } # 信号直连(拆件批 T3):runner 信号在 level_started 重取点重连(转发族退役——重取点前移到本回调) @@ -158,9 +150,9 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_ # 特性注入(formed 画像=开局注入七流派各 2 件档一成型集;测量面专用) if relic.has("seed_ids"): adv_params["relics_seed"] = relic.get("seed_ids", []) - # 树规则键注入(A/B 面读数载体:fork 激活侧 effects rule 键集透传) + # 树规则参数字典注入(A/B 面读数载体:fork 激活侧 effects_* 参数字典透传) if relic.has("tree_rules"): - adv_params["tree_rules"] = relic.get("tree_rules", []) + adv_params["tree_rules"] = relic.get("tree_rules", {}) if relic.get("tree_rules", {}) is Dictionary else {} # 商店分层注入(缺省 legacy 与既有调用方逐位一致零行为变更;pool=共享池货架面) var pool_shop: bool = String(shop.get("mode", "")) == "pool" if pool_shop: @@ -189,8 +181,6 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_ if own_session: logger.close_session() return st - - # 每关开始(autoplay 直连面):重取 runner 并重连三信号(恰好一次——同关守卫: # level_started 每关恰一次发射,bind Callable 每次新建故以关卡号守卫防重入) static func _on_autoplay_level_started(level_index: int, _display_name: String, adr, st: Dictionary, collect_rows: bool, logger) -> void: @@ -203,8 +193,6 @@ static func _on_autoplay_level_started(level_index: int, _display_name: String, r.step_row.connect(_on_step_row.bind(adr, st, collect_rows)) r.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger)) r.wallet_changed.connect(_on_adventure_wallet.bind(st, logger)) - - # 冒险模式节点采集(_on_node_entered 的 AdventureRunner 门面版——runner 逐关更替,经 adr.runner() 重取; # encounter_max_hp_seq 抓 effective_max_hp:node_entered 先于 begin_battle,encounter 尚为空字典, # 该值 = begin_battle 将收到 hero_max_hp 同一表达式〔规划书抓取点与代码事实冲突,按代码为准已登记〕) @@ -215,7 +203,6 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr, if not st.has("level_maps"): st["level_maps"] = [] st["level_maps"].append("%d#%d" % [int(r.level_row.get("level_index", 0)), int(adr.master_seed)]) # MC 分层出数(关卡×生成图标识) - if t == "battle" or t == "boss": st.battles += 1 st.last_battle_enemies = [] # 末场战斗敌方名序列按战斗重置(失败局终值 = 死亡遭遇) @@ -259,8 +246,6 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr, r.continue_after_node() if logger != null: logger.log_node(node, info, r.node_index) - - # 冒险钱包事件采集(wallet 行族——GP 跨关延续与冒险收束 clear 的验收取数面;同日志合规转发) static func _on_adventure_wallet(event: String, gp_val: int, extra: Dictionary, st: Dictionary, logger) -> void: st.wallet_events.append({"event": String(event), "gp": int(gp_val), @@ -268,8 +253,6 @@ static func _on_adventure_wallet(event: String, gp_val: int, extra: Dictionary, "source": String(extra.get("source", ""))}) if logger != null: logger.log_wallet(event, gp_val, extra) - - # 三选一停点驱动(确定性策略;TP0 起 316-f 判读:1 级最稀有、同档并列取最左; # 奖励事件随 reward_events 留档) static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> void: @@ -283,8 +266,6 @@ static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> voi var pick: Dictionary = _pick_token(candidates, String(st.get("feat_mode", "off"))) if not adr.stop().pick_curio(String(pick.get("kind", "")), String(pick.get("tier", "")), String(pick.get("feat_id", ""))): st.result = "advance_failed" - - # 三选一确定性选件(TP0 316-f——1 级优先度=最稀有〔四级:legendary>rare>excellent>common〕、2 级=最左; # 候选并列同档取最左=稳定序保证。空候选/未知档位 = 回退最左防御口径。 # 成型画像(relic 模式 on:候选含 relic 令牌时优先在 relic 集内按 316-f 择一 @@ -302,22 +283,11 @@ static func _pick_token(candidates: Array, feat_mode: String = "off") -> Diction var best_rank := -1 for tok in pool: var t: Dictionary = tok as Dictionary - var rank := 0 - match String(t.get("tier", "")): - "legendary": - rank = 4 - "rare": - rank = 3 - "excellent": - rank = 2 - "common": - rank = 1 + var rank: int = int(TreasureItemTable.TIER_RANK.get(String(t.get("tier", "")), 0)) # 档位排序读表(未知档 0=原 match 落空同语义) if best.is_empty() or rank > best_rank: best = t best_rank = rank return best - - # 三选一兑付完成 → 上下文分派(关卡完成停点=推进下一关 / 奖励节点=回图选路) static func _on_adventure_curio_resolved(result: Dictionary, st: Dictionary, adr) -> void: st.reward_events.append({"curio_resolved": result.duplicate(true)}) @@ -328,8 +298,6 @@ static func _on_adventure_curio_resolved(result: Dictionary, st: Dictionary, adr return if not adr.continue_to_next_level(): st.result = "advance_failed" - - # 商店贪心买齐:GP 局内死钱——买得起就买( affordable 集里挑最便宜的 # 反复买到达不上任何商品为止,guard 防死循环)。确定性(零随机流);购买出账/加算区入账全走 # runner.shop.buy_product 核心编排(与呈现层购买同一出账路径,wallet_changed 信号照常采集)。 @@ -350,8 +318,6 @@ static func _autoplay_shop(r, st: Dictionary) -> void: if not r.shop.buy_product(String(pick.get("product_id", ""))): return st.shop_buys = int(st.get("shop_buys", 0)) + 1 - - # 商店共享池贪心:货架三选一同构面贪心买齐——affordable 集挑最便宜反复买 # (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.shop.buy_pool_item # 核心编排(与呈现层购买同一出账+池消费路径,wallet_changed 信号照常采集)。确定性(零随机流)。 @@ -372,24 +338,18 @@ static func _autoplay_shop_pool(r, st: Dictionary) -> void: if not r.shop.buy_pool_item(String(pick.get("kind_id", ""))): return st.shop_buys = int(st.get("shop_buys", 0)) + 1 - - # 冒险终局(completed/failed/abandoned——result 载 summary.result,外层驱动循环随之收敛) static func _on_adventure_finished(summary: Dictionary, st: Dictionary, logger) -> void: st.summary = summary.duplicate(true) st.result = String(summary.get("result", "")) if logger != null: logger.log_level_end(summary) - - # 策略流 RNG:种子派生自局种子 × 黄金比率常数 + 偏移——与核心统一流 # (rng_service 单盐派生)零共享零扰动;逐局确定性保持(同种子 ⇒ 同决策序列 ⇒ 同回放) static func _policy_rng(seed_val: int) -> RandomNumberGenerator: var prng := RandomNumberGenerator.new() prng.seed = seed_val * 2654435761 + 912931 return prng - - # 路径轴选路(目标价值评分制):候选按节点类型评分取最高, # 同分 tie-break 沿 route 方向(min=小句柄/max=大句柄——verifier 双窗轴保持)。 # 评分语义(确定性,零随机流):rest=伤重高值(缺口比例×100)/shop=买得起高值(GP 局内死钱, @@ -438,8 +398,6 @@ static func _route_pick(runner, route: String, st: Dictionary) -> int: if better: best = c return int(best.id) - - static func _on_step_row(row: Dictionary, _runner, st: Dictionary, collect_rows: bool) -> void: if row.type == "damage" and row.source_class == "enemy_action": st.hero_dmg += int(row.value) @@ -470,8 +428,6 @@ static func _on_step_row(row: Dictionary, _runner, st: Dictionary, collect_rows: st.seal_miss_penalty += int(row.value) if collect_rows: st.rows.append(row) - - # 敌方显示名采集:敌方行动/施放行文本首词 = 单位显示名 #(攻击行「地精 撕咬(暴击)」、施放行「地精 施放 毒雾」——显示名不含空格,去重连续重复) static func _note_enemy(text: String, st: Dictionary) -> void: @@ -483,8 +439,6 @@ static func _note_enemy(text: String, st: Dictionary) -> void: var arr: Array = st.last_battle_enemies if arr.is_empty() or String(arr[arr.size() - 1]) != ename: arr.append(ename) - - static func _on_node_entered(node: Dictionary, info: Dictionary, runner, st: Dictionary, logger) -> void: var t := String(node.get("type", "")) if t == "battle" or t == "boss": @@ -506,8 +460,6 @@ static func _on_node_entered(node: Dictionary, info: Dictionary, runner, st: Dic # 不掺购买行为;冒险口径(run_adventure)已在 _on_adventure_node_entered 改贪心买齐 if logger != null: logger.log_node(node, info, runner.node_index) - - static func _on_level_finished(p: Dictionary, st: Dictionary, logger) -> void: st.result = String(p.get("result", "")) st.hp_left = int(p.get("hp", 0)) @@ -516,8 +468,6 @@ static func _on_level_finished(p: Dictionary, st: Dictionary, logger) -> void: st.defeat_node_type = String(p.get("node_type", "")) if logger != null: logger.log_level_end(p) - - static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void: # 策略档经 _decide_act 单点分派(单循环承载三档——三档决策语义逐位不变,见 _decide_act)。 # 普通策略档 v3:完美策略 + 受控失误注入(标定依据=真人实证: @@ -534,40 +484,27 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void round_guard -= 1 st.rounds += 1 var start_round: int = settlement.round_index - var action_guard := 400 - var rejects := 0 - while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \ - and settlement.round_index == start_round and action_guard > 0 and rejects < 3: - action_guard -= 1 - var act: Dictionary = _decide_act(st, settlement) - var kind := String(act.get("kind", "stop")) - if kind == "stop": - break - var ok := false - if kind == "move": - ok = _submit_intent(settlement, "move", act.get("cell"), logger, start_round) - elif kind == "dig": - ok = _submit_intent(settlement, "dig", act.get("cell"), logger, start_round) - elif kind == "seal": - ok = _submit_intent(settlement, "seal_place", act.get("cell"), logger, start_round) - elif kind == "refresh": - ok = bool(settlement.player_request_refresh()) - if logger != null: - logger.log_action("refresh", Vector2i(-1, -1), ok, {"round": start_round}) - if ok: - if kind == "refresh": - st.refreshes = int(st.refreshes) + 1 - elif kind == "seal": - st.seal_placed = int(st.get("seal_placed", 0)) + 1 # 封印数(η 分母=全部封印数〔Q5〕;失误注入转化封印不计数=转化后实为挖) - rejects = 0 - else: - rejects += 1 - if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round: - var out_s: Array = settlement.minesweeper.settle_batch([{"action": "stop"}]) - if logger != null: - logger.log_action("stop", Vector2i(-1, -1), _accepted(out_s), {"round": start_round}) + # 内层单轮循环委托 BattleDriver.drive_round(驱动循环单点——庚案收敛:本类禁再持第二轮驱动循环; + # 策略分派/失误注入全保留在 _decide_act 原地,日志与计数经 _action_logger 回调承载) + BattleDriver.drive_round(settlement, + func() -> bool: return runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round, + _act_provider(st), _action_logger(st, logger), 3) +# 策略回调适配器(drive_round act_provider 契约——三档分派 perfect/novice/slip 全在 _decide_act 原地) +static func _act_provider(st: Dictionary) -> Callable: + return func(s) -> Dictionary: return _decide_act(st, s) + + +# 日志回调适配器(drive_round on_action 契约——log_action 带轮号 + refresh/seal 计数语义逐位同原内联) +static func _action_logger(st: Dictionary, logger) -> Callable: + return func(kind: String, cell: Vector2i, ok: bool, ri: int) -> void: + if logger != null: + logger.log_action(kind, cell, ok, {"round": ri}) + if ok and kind == "refresh": + st.refreshes = int(st.refreshes) + 1 + elif ok and kind == "seal_place": + st.seal_placed = int(st.get("seal_placed", 0)) + 1 # 封印数(η 分母=全部封印数〔Q5〕;失误注入转化封印不计数=转化后实为挖) # 普通档 v3 失误注入:三类—— # ①标雷误挖:把「封印已证雷」动作直接转成「挖该格」(真人手滑原型:知道是雷、位置点错, # 踩雷吃伤且该雷奖励报废);期望踩雷增量 ≈ 每板已证雷数 × NORMAL_MINE_SLIP_P(目标完美+2/板) @@ -598,8 +535,6 @@ static func _apply_normal_slips(act: Dictionary, settlement, st: Dictionary) -> if not plan.is_empty(): return _gamble_action(plan, settlement) return act - - # 赌挖动作组装(v3.4 抽出):目标邻接=直接挖,否则先移到目标的已开邻格(下一拍再挖) static func _gamble_action(plan: Dictionary, settlement) -> Dictionary: var ms = settlement.minesweeper @@ -609,8 +544,6 @@ static func _gamble_action(plan: Dictionary, settlement) -> Dictionary: if maxi(d.x, d.y) == 1: return {"kind": "dig", "cell": target} return {"kind": "move", "cell": plan.hero_cell} - - # 战斗决策单行分派: # perfect 档 = 完美策略单步决策(上界口径); # normal 档 v3 = 完美+受控失误注入(冒险测量面 adv=true);单关机制夹具保持 v2 新手画像(adv 缺省)。 @@ -621,15 +554,3 @@ static func _decide_act(st: Dictionary, settlement) -> Dictionary: if bool(st.get("adv", false)): return _apply_normal_slips(AutoPlayer.perfect_action(settlement.minesweeper, settlement), settlement, st) # v3:完美+失误 return AutoPlayer.novice_action(settlement.minesweeper) # 单关机制夹具:v2 新手画像保持 - - -static func _submit_intent(settlement, action: String, cell, logger, round_index: int) -> bool: - var out: Array = settlement.minesweeper.settle_batch([{"action": action, "cell": cell}]) - if logger != null: - logger.log_action(action, cell, _accepted(out), {"round": round_index}) - return _accepted(out) - - -static func _accepted(outcomes: Array) -> bool: - return not outcomes.is_empty() and bool(outcomes[0].accepted) - diff --git a/game/tests/.gates/baseline.json b/game/tests/.gates/baseline.json index bbac70c8..657cbc6d 100644 --- a/game/tests/.gates/baseline.json +++ b/game/tests/.gates/baseline.json @@ -1,61 +1,65 @@ { - "note": "\u5b58\u91cf\u57fa\u7ebf\u00b7\u95f8\u51b5\u53d8\u5316\u65f6\u672c\u811a\u672c\u4f1a\u5224\u7ea2\uff0c\u9700\u4eba\u5de5\u786e\u8ba4\u540e\u91cd\u751f\u6210\uff1b2026-10-29 \u4e8c\u6b21\u8bb0\u8d26\uff1d03/04/05 \u4e09\u95f8\u6309 80b49c0f \u5148\u4f8b\u7ecf\u7528\u6237\u88c1\u51b3\u8bb0\u5165\u73b0\u884c\u7ea2\u9879\u6307\u7eb9\uff0803=8a3cae0d0446/04=591b27250b36/05=de89589e49ed\uff09\u2014\u2014master \u540c\u503c\u590d\u73b0\u975e\u8fed\u4ee342\u65b0\u589e\uff08stash \u5bf9\u7167\u6cd5\u53d6\u8bc1\uff0c\u627f\u300c\u8fed\u4ee3 41 \u6d4b\u8bd5\u503a\u4fee\u590d\u300d\u6761\u76ee\u6269\u5c55\uff09\uff1b\u539f 2026-10-17 06 \u6761\u76ee\u4eba\u5de5\u91cd\u751f\u6210\u8bb0\u5f55\u89c1 git \u63d0\u4ea4\u53f2\uff1b2026-10-30 \u4e09\u952e\u6362\u4ee3\uff1d02/05/09 \u6309\u5de5\u7a0b\u5c0f\u4ef6\u6279\u56de\u6267 6a3203a8 \u5b9e\u6d4b\u7eff\u91cd\u951a\uff0802 \u5173\u8054\u6863\u5206\u72474/4\u5168\u7eff\uff0f05 \u6e32\u67d3\u95f8\u771f\u8dd1\u7eff\uff0f09 \u9759\u6001\u626b\u63cf\u7eff\u2014\u2014run_all \u9017\u53f7\u89e3\u6790\u4fee\u590d\uff0b\u6052\u771f\u4e94\u6761\u5224\u636e\u4fee\u6b63\u6279\uff0c\u4e09\u952e\u65e7\u7ea2\u6307\u7eb9\u5168\u90e8\u5f52\u7eff\u6d88\u9664\uff09", - "gates": { - "00 \u5192\u70df\u6d4b\u8bd5": { - "exit": "0", - "fp": "green" - }, - "01 \u7cfb\u7edf\u6d4b\u8bd5": { - "exit": "0", - "fp": "green" - }, - "02 \u5355\u5143\u4e0e\u96c6\u6210\u6d4b\u8bd5": { - "exit": "0", - "fp": "green" - }, - "03 \u573a\u666f\u63a2\u9488": { - "exit": "0", - "fp": "green" - }, - "05 \u6e32\u67d3\u95f8": { - "exit": "0", - "fp": "green" - }, - "06 gdd_lint": { - "exit": "0", - "fp": "green" - }, - "06b \u53d8\u66f4\u8bb0\u5f55\u8282": { - "exit": "0", - "fp": "green" - }, - "07 doc_link": { - "exit": "0", - "fp": "green" - }, - "08 doc_term": { - "exit": "0", - "fp": "green" - }, - "08c \u6d41\u6c34\u7ebf\u95f8": { - "exit": "0", - "fp": "green" - }, - "09 \u9759\u6001\u626b\u63cf": { - "exit": "0", - "fp": "green" - }, - "10 \u71b5\u589e\u95f8": { - "exit": "0", - "fp": "green" - }, - "08d \u5f15\u7528\u53ef\u6838": { - "exit": "0", - "fp": "green" - }, - "08e \u80fd\u529b\u5ba1\u8ba1": { - "exit": "0", - "fp": "green" - } + "note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成", + "gates": { + "00 冒烟测试": { + "exit": "0", + "fp": "green" + }, + "01 系统测试": { + "exit": "0", + "fp": "green" + }, + "02 单元与集成测试": { + "exit": "0", + "fp": "green" + }, + "03 场景探针": { + "exit": "0", + "fp": "green" + }, + "05 渲染闸": { + "exit": "0", + "fp": "green" + }, + "05b 色板闸": { + "exit": "0", + "fp": "green" + }, + "06 gdd_lint": { + "exit": "0", + "fp": "green" + }, + "06b 变更记录节": { + "exit": "0", + "fp": "green" + }, + "07 doc_link": { + "exit": "0", + "fp": "green" + }, + "08 doc_term": { + "exit": "0", + "fp": "green" + }, + "08c 流水线闸": { + "exit": "0", + "fp": "green" + }, + "08d 引用可核": { + "exit": "0", + "fp": "green" + }, + "08e 能力审计": { + "exit": "0", + "fp": "green" + }, + "09 静态扫描": { + "exit": "0", + "fp": "green" + }, + "10 熵增闸": { + "exit": "0", + "fp": "green" } + } } \ No newline at end of file diff --git a/game/tests/README.md b/game/tests/README.md index 208619ee..88a00411 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -183,7 +183,7 @@ tests/ | 同名状态合并策略(叠加/累加/并存)与剩余次数语义 | 战斗循环×增益状态 | `StatusContainer.apply_effect()`(编排宿主 begin_battle 链) | `roll_multiplier_test.gd:_smoke_armor_roll` | | 状态效果结算链(逐值衰减/到期移除/战斗结束全清/满层拒绝/独立并存/首授快照) | 增益状态 | `StatusContainer.apply_effect()`/`decay_round()`/`notify_battle_end()` | `system_contracts_test.gd:_test_status_decay_and_clear`;`system_contracts_test.gd:_test_status_stack_full_reject`;`system_contracts_test.gd:_test_status_independent_branch`;`system_contracts_test.gd:_test_status_snapshot_first_grant` | | 状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 | 增益状态 | `StatusContainer.apply_effect()`(个体键)/`purge_owner()` | `system_contracts_test.gd:_test_status_individual_isolation`;`system_contracts_test.gd:_test_status_grant_all_per_unit`;`system_contracts_test.gd:_test_status_death_clears`;`system_contracts_test.gd:_test_status_merge_per_unit` | -| 敌方增益自体落点与乘区池按个体 | 敌人系统×战斗结算×增益状态 | 技能兑付编排链/`DamageController.clamped_pool()` | `round_flow_skill_pool_test.gd:_test_enemy_buff_targets_self`;`damage_controller_test.gd:_test_pool_consumption_per_unit`;`damage_controller_test.gd:_test_defender_ac_per_unit`;`battle_narrator_test.gd:_test_snapshot_effects_per_unit` | +| 敌方增益自体落点与乘区池按个体 | 敌人系统×战斗结算×增益状态 | 技能兑付编排链/`DamageController.clamped_pool()` | `round_flow_skill_pool_test.gd:_smoke_enemy_buff_targets_self`;`damage_controller_test.gd:_test_pool_consumption_per_unit`;`damage_controller_test.gd:_test_defender_ac_per_unit`;`battle_narrator_test.gd:_test_snapshot_effects_per_unit` | | 敌人技能池跨表链接校验 | 数据配置×敌人系统 | `ConfigDbCrossValidator.validate_skill_pool_links()` | `config_db_cross_validator_test.gd:_test_all_links_valid`;`config_db_cross_validator_test.gd:_test_dangling_skill_id`;`config_db_cross_validator_test.gd:_test_cast_side_mismatch` | **覆盖域表**(`design/gdd/systems/` 逐篇对照——状态三档:已覆盖/部分覆盖/未覆盖;部分与未覆盖行的缺口指待办清单条目,覆盖面提升时同批改本表): @@ -208,7 +208,7 @@ tests/ | 表现与支撑 | 存档序列化.md | 存档写盘⇄读回往返保真;三槽位隔离与激活失败回滚;槽位摘要窥视与坏槽三态;启动态无激活不写盘;旧档迁移链与断点续玩零跳号(集成);存档⇄RNG 快照往返保真(集成) | 已覆盖 | — | | 表现与支撑 | 教学引导.md | — | 未覆盖 | 教学形态与首遇时机待设计;见待办清单「教学形态未定」条 | | 表现与支撑 | 数据配置.md | 全表装载、就绪态与表清单目录对账;进度坐标查行:精确命中/向上取整/封顶/警告去重;运行期越域致命与进程非 0 退出;加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域;表结构守卫:域校验/行序键/跨字段互锁/配额锚 | 已覆盖 | — | -| 表现与支撑 | 设置与本地化.md | 减动效开关存储契约(持久化回读/缺省值/引擎动效档应用) | 部分覆盖 | 存储契约断言由「断言补齐批」承接;多语言切换面未建(P1);见待办清单「能力清单覆盖缺口补齐」条 | +| 表现与支撑 | 设置与本地化.md | 减动效开关存储契约(持久化回读/缺省值/引擎动效档应用) | 已覆盖 | 存储契约三断言已落地(迭代 50 断言补齐批);多语言切换面未建(P1,归设置与本地化待设计问题清单) | | 表现与支撑 | 音频.md | — | 未覆盖 | MVP 无音频(P1 后置,无断言面);见待办清单「能力清单覆盖缺口补齐」条 | | 跨局循环 | 元进度.md | 元进度入账与锻造两轴 | 已覆盖 | — | | 跨局循环 | 天赋树.md | 天赋树购买与六键合成 | 已覆盖 | — | diff --git a/game/tests/golden/README.md b/game/tests/golden/README.md index 05913c31..51cb1287 100644 --- a/game/tests/golden/README.md +++ b/game/tests/golden/README.md @@ -1,7 +1,12 @@ # 视觉 golden 基线(渲染闸的比对基准) -五张基线:棋盘三图(`board_L1_dungeon` / `board_L2_underdark` / `board_L3_deep_underdark`, -裁剪区 900×760)+ 启动态菜单(`menu_boot`,全屏)+ 关卡画面(`level_map`,全屏)。 +基线**按平台各存**(2026-11-03 用户裁:跨显卡/字形渲染机制差异使跨平台逐像素必失配—— +`windows/` 与 `mac/` 子目录各持一套,平台内部照常严格比对,跨平台不互比; +平台选择实现单点 = `tools/gate_platform.sh` 的 `gate_platform_id`,比较器 `tools/golden_compare.py` +自动拼平台子目录)。本目录现持:`windows/`(棋盘三图 `board_L1_dungeon` / `board_L2_underdark` / +`board_L3_deep_underdark`,裁剪区 900×760 + 启动态菜单 `menu_boot`,全屏)、 +`level_map`(关卡画面全屏,历史基线留根——现行闸件未对拍它)、 +`palette_baseline/`(色板基线,纯文件内容比对与平台无关)。 比对判据与工具见 `tools/golden_compare.py` (逐像素灰度差 >16 的像素占比 ≤2%)。 diff --git a/game/tests/golden/mac/board_L1_dungeon.png b/game/tests/golden/mac/board_L1_dungeon.png new file mode 100644 index 00000000..887eeed2 Binary files /dev/null and b/game/tests/golden/mac/board_L1_dungeon.png differ diff --git a/game/tests/golden/mac/board_L2_underdark.png b/game/tests/golden/mac/board_L2_underdark.png new file mode 100644 index 00000000..db635703 Binary files /dev/null and b/game/tests/golden/mac/board_L2_underdark.png differ diff --git a/game/tests/golden/mac/board_L3_deep_underdark.png b/game/tests/golden/mac/board_L3_deep_underdark.png new file mode 100644 index 00000000..7e1f377a Binary files /dev/null and b/game/tests/golden/mac/board_L3_deep_underdark.png differ diff --git a/game/tests/golden/mac/menu_boot.png b/game/tests/golden/mac/menu_boot.png new file mode 100644 index 00000000..b81a1dc6 Binary files /dev/null and b/game/tests/golden/mac/menu_boot.png differ diff --git a/game/tests/golden/board_L1_dungeon.png b/game/tests/golden/windows/board_L1_dungeon.png similarity index 100% rename from game/tests/golden/board_L1_dungeon.png rename to game/tests/golden/windows/board_L1_dungeon.png diff --git a/game/tests/golden/board_L2_underdark.png b/game/tests/golden/windows/board_L2_underdark.png similarity index 100% rename from game/tests/golden/board_L2_underdark.png rename to game/tests/golden/windows/board_L2_underdark.png diff --git a/game/tests/golden/board_L3_deep_underdark.png b/game/tests/golden/windows/board_L3_deep_underdark.png similarity index 100% rename from game/tests/golden/board_L3_deep_underdark.png rename to game/tests/golden/windows/board_L3_deep_underdark.png diff --git a/game/tests/golden/menu_boot.png b/game/tests/golden/windows/menu_boot.png similarity index 100% rename from game/tests/golden/menu_boot.png rename to game/tests/golden/windows/menu_boot.png diff --git a/game/tests/integration/cross_system_flow_test.gd b/game/tests/integration/cross_system_flow_test.gd index c4593ae5..165ae097 100644 --- a/game/tests/integration/cross_system_flow_test.gd +++ b/game/tests/integration/cross_system_flow_test.gd @@ -142,6 +142,7 @@ func _smoke_playtest_logger() -> void: rng.set_master_seed(20260912) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) var logger = load("res://presentation/playtest_logger.gd").new() + logger.ap_per_round = 8 # 必填注入(R5 守卫——夹具沿组合根表值 8) var path: String = logger.start_session(20260912) logger.attach_battle(settlement) var snaps: Array = [] diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index bd198255..4e803b0a 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -149,6 +149,10 @@ func _init() -> void: return if failures > 0: push_error("[run_all] %d 个套件失败" % failures) + for fp in failed_paths: + push_error("[run_all] 失败套件: %s" % fp) + for tp in timed_out_paths: + push_error("[run_all] 超时套件: %s" % tp) quit(1) return var tag := ",filtered" if filtered else "" @@ -186,6 +190,12 @@ func _suites() -> Array: {"path": "res://tests/unit/core/curio_refresh_paid_test.gd", "expect": 0}, # 三选一刷新免费双源+GP 付费档(对齐商店 MC-1:价曲线/优先序/停点清零/钱包单口) {"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值) {"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态 + {"path": "res://tests/unit/presentation/settings_store_test.gd", "expect": 0}, # 减动效开关存储契约(持久化回读/缺省值/引擎动效档应用)〔断言补齐批〕 + {"path": "res://tests/unit/battle_settlement/boss_gate_test.gd", "expect": 0}, # Boss 节点开战必选 Boss 行集(行类路由+端到端)〔断言补齐批〕 + {"path": "res://tests/unit/data_config/effect_kind_retired_test.gd", "expect": 1, "negative": true}, # 甲案词表退役——overflow/attack_segments 误写加载致命〔架构整改〕 + {"path": "res://tests/unit/core/playtest_logger_guard_test.gd", "expect": 0}, # R5 必填注入守卫——未注入拒绝开会话〔架构整改〕 + {"path": "res://tests/unit/core/battle_driver_drive_test.gd", "expect": 0}, # R12 驱动循环契约——缺省确定性/provider/rejects/合步〔架构整改〕 + {"path": "res://tests/unit/core/auto_player_deduce_test.gd", "expect": 0}, # R15 0 格连锁算子——远端格经真实闭包入 safe〔架构整改〕 {"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–V12(血条危险红/名字行素底不压地台与提档 FS_HINT/徽标/护甲 PNG 绑定/素材入包/地台色合并退役) @@ -269,9 +279,18 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> Dictiona DirAccess.make_dir_recursive_absolute(proj + "/tests/.gates") # /V:ON=延迟展开:!ERRORLEVEL! 到 echo 执行时(套件已退出)才取值; # 延迟展开会吞字面 !,命令串各段(引擎路径/工程路径/套件路径/日志路径)不得含 !。 - var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1 & echo !ERRORLEVEL! > "%s"' % [bin, proj, path, log, codefile] + var pid: int = -1 var out := PackedStringArray() - var pid: int = OS.create_process("cmd.exe", PackedStringArray(["/V:ON", "/C", cmd])) + if OS.get_name() == "Windows": + # Windows 形态(原实现逐位保留):/V:ON 延迟展开,!ERRORLEVEL! 到 echo 时(套件已退出)才取值; + # 延迟展开会吞字面 !,命令串各段(引擎路径/工程路径/套件路径/日志路径)不得含 !。 + var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1 & echo !ERRORLEVEL! > "%s"' % [bin, proj, path, log, codefile] + pid = OS.create_process("cmd.exe", PackedStringArray(["/V:ON", "/C", cmd])) + else: + # POSIX 形态(mac/Linux runner):/bin/sh -c 同步串行——sh 前台等引擎退出再落 $?, + # 无延迟展开问题;create_process 拿 sh 的 pid(存活轮询=sh 前台等待期),截止杀走 kill 树。 + var sh_cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1; echo $? > "%s"' % [bin, proj, path, log, codefile] + pid = OS.create_process("/bin/sh", PackedStringArray(["-c", sh_cmd])) if pid < 0: push_error("[run_all] FAIL %s — cmd 子进程启动失败(pid=%d)" % [path, pid]) return {"fail": 1, "timed_out": false} @@ -289,7 +308,12 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> Dictiona var negative := bool(suite.get("negative", false)) if timed_out: var kill_out := PackedStringArray() - OS.execute("taskkill", PackedStringArray(["/F", "/T", "/PID", str(pid)]), kill_out, true) + if OS.get_name() == "Windows": + OS.execute("taskkill", PackedStringArray(["/F", "/T", "/PID", str(pid)]), kill_out, true) + else: + # POSIX:先杀 sh 的直接子进程(引擎),再杀 sh 自身(顺序要紧——先杀父会让子成孤儿续跑) + OS.execute("pkill", PackedStringArray(["-9", "-P", str(pid)]), kill_out, true) + OS.execute("kill", PackedStringArray(["-9", str(pid)]), kill_out, true) # taskkill 落刀到进程对象消失有间隔:等死确认再放行下一套件(10s 宽限), # 确认不了也如实报红留 PID 供手工清杀,不静默放行。 var reap_deadline := Time.get_ticks_msec() + 10000 diff --git a/game/tests/static_scan.sh b/game/tests/static_scan.sh old mode 100644 new mode 100755 index e17aaa93..b8f9c873 --- a/game/tests/static_scan.sh +++ b/game/tests/static_scan.sh @@ -20,6 +20,16 @@ fail=0 DIR_CORE="game/core" DIR_PRES="game/presentation" +# python 解释器解析:PATH 有 python 用 python,只有 python3(runner 常态)回落 python3—— +# 硬编码 python 在无别名机器上=127 command not found,三审计零输出静默判红(CI run15 实证)。 +PY_BIN="" +if command -v python >/dev/null 2>&1; then PY_BIN="python" +elif command -v python3 >/dev/null 2>&1; then PY_BIN="python3" +else + echo "[static_scan] FAIL: 无 python 解释器(PATH 上既无 python 也无 python3)" + exit 1 +fi + scan_files() { # scan_files <目录> — 该目录下 .gd 文件数 find "$1" -name '*.gd' 2>/dev/null | wc -l } @@ -36,7 +46,7 @@ require_scan_face() { # require_scan_face <规则名> <目录> — 扫描面 } # S9 — 类型闸(code-conventions §4 机器闸:产品码无类型容器/有值返回缺类型/参数缺类型) -if python tools/audit_type_hints.py; then +if "$PY_BIN" tools/audit_type_hints.py; then echo "[static_scan] S9 ok" else echo "[static_scan] S9 FAIL: 类型闸存量未归零(R1/R2/R3 见上清单)" @@ -44,7 +54,7 @@ else fi # S10 — 文件头闸(code-conventions §1.4.1:## 载体+日期/变更叙事黑名单;行数上限已停用) -if python tools/audit_file_headers.py; then +if "$PY_BIN" tools/audit_file_headers.py; then echo "[static_scan] S10 ok" else echo "[static_scan] S10 FAIL: 文件头存量见上清单" @@ -140,7 +150,7 @@ S11_TABLE="design/gdd/术语表.md" S11_FAIL_LOCAL=0 if [ ! -f "$S11_TABLE" ]; then - echo "[static_scan] S11 FAIL: 术语表文件不存在($S11_TABLE)" + echo "[static_scan] S11 FAIL: 术语表文件不存在(${S11_TABLE})" S11_FAIL_LOCAL=1 else # 解析词表:「退役词对照」第一列→去括号注→斜杠拆分 @@ -296,7 +306,7 @@ fi # 簿,登记值只准随还债下降,禁抬高到现状。基线重生成=人工专用(python tools/gdstyle_gate.py # --refresh,附对账;键数骤降逾 10% 或登记值被抬高均拒写,须 --force),禁清基线换绿。二进制本机固定路径 # D:/code/gdstyle/gdstyle.exe(不入库),GDSTYLE_BIN 环境变量覆盖;版本与钉定版不符即红(升级须重测)。 -if python tools/gdstyle_gate.py; then +if "$PY_BIN" tools/gdstyle_gate.py; then echo "[static_scan] S13 ok" else echo "[static_scan] S13 FAIL: 基线外新违规/指标恶化或二进制/版本/扫描面问题(清单见上)" @@ -308,13 +318,15 @@ fi # 可回放纯逻辑步骤流(同种子同输入逐位复现),await 会打断确定性——「模块归属」B1 行已写明 # 「禁 await——切片 88 冒烟实证」。本闸为防回归防线(2026-10-24 人工 grep 盘面干净)。 # 扫描面:B1 域文件集(与 architecture.md「模块归属」B1 行同口径)=battle_orchestrator.gd -# + battle_fx_host.gd + reward_grant.gd + board_installer.gd 三拆件 + settlement/ 五段段类 + damage_controller.gd,共 14 文件,显式清单见 S8_FILES。 +# + battle_fx_host.gd + reward_grant.gd + board_installer.gd 三拆件 + settlement/ 五段段类 + damage_controller.gd,清单单点=B1_SET。 # 执行侧字面值豁免:脚本必须带字面值才能工作;文件集与架构文档冲突时以 B1 域文件集为准。 # 判据:文件集内出现 await 关键字即红(词边界 \bawait\b,任何形态——await 信号/协程/ # 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 game/core/settlement/battle_refresh_flow.gd game/core/effect_port.gd game/core/reward_roller.gd" +# 自检:①清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿;②settlement/ 目录域反向对账——新段类未登记即判红(见下)。 +# B1 域文件集=单点清单(S8 await 禁扫面与 S7 豁免名单共用;改成员先改 architecture.md「模块归属」B1 行再改此处,2026-11-03 架构审查 P1 裁并点) +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 game/core/effect_port.gd game/core/reward_roller.gd" +S8_FILES="$B1_SET" S8_MISS=0 for f in $S8_FILES; do if [ ! -f "$f" ]; then @@ -322,6 +334,17 @@ for f in $S8_FILES; do S8_MISS=1 fi done +# 自检二(2026-11-03 架构审查 P1 裁):目录域反向对账——settlement/ 实有 .gd 未登记进 B1_SET=该扫的没扫,判红。 +# 只反向对 settlement/ 一个目录域:装板/授予/效果端口/掷格四件结算码住 game/core 根下,纯目录域覆盖不住, +# 根下新增仍由「模块归属」B1 行+写卡段申报面承接(禁把目录判据当成全域判据)。 +S8_UNREG="" +for f in $(find game/core/settlement -name '*.gd' 2>/dev/null | sort); do + case " $B1_SET " in *" $f "*) ;; *) S8_UNREG="$S8_UNREG $f" ;; esac +done +if [ -n "$S8_UNREG" ]; then + echo "[static_scan] S8 FAIL: settlement/ 有结算段类未登记进 B1 域文件集——await 禁扫面对它静默失效:$S8_UNREG(先改 architecture.md「模块归属」B1 行,再补 B1_SET)" + S8_MISS=1 +fi if [ "$S8_MISS" -eq 1 ]; then fail=1 else @@ -334,7 +357,7 @@ else grep -rnE '\bawait\b' $S8_FILES 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | grep -vE '"[^"]*\bawait\b[^"]*"' fail=1 else - echo "[static_scan] S8 ok(扫描面 B1 文件集 10 个 .gd,await 零命中)" + echo "[static_scan] S8 ok(扫描面 B1 文件集 $(echo $S8_FILES | wc -w) 个 .gd+settlement/ 目录域已反向对账,await 零命中)" fi fi @@ -358,7 +381,7 @@ fi if require_scan_face "S7" "$DIR_CORE"; then # 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 game/core/effect_port.gd game/core/reward_roller.gd" +S7_B1_SET="$B1_SET" # 与 S8 同一单点清单(原第二份抄本,2026-11-03 并点——两份靠人工同步必漂) S7_MISS=0 for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do if [ ! -f "$f" ]; then diff --git a/game/tests/unit/battle_settlement/boss_gate_test.gd b/game/tests/unit/battle_settlement/boss_gate_test.gd new file mode 100644 index 00000000..bf32d8c4 --- /dev/null +++ b/game/tests/unit/battle_settlement/boss_gate_test.gd @@ -0,0 +1,48 @@ +# tests/unit/battle_settlement/boss_gate_test.gd — Boss 节点开战必选 Boss 行集(端到端) +# 断言面 = 行类路由(resolver:boss 行集全行 boss / battle 行集零 boss 行)+端到端 +# (begin_battle(node_type="boss") → 实例化敌人全部出自 boss 行集)。固定种子经 RngService 流口。 +# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/boss_gate_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") + + +func _init() -> void: + _test_boss_node_spawns_boss_rowset() + quit(Kit.finish("boss_gate_test")) + + +func _test_boss_node_spawns_boss_rowset() -> void: + var db = load("res://core/config_db.gd").new() + var load_ok: bool = db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0 + Kit.check("前置:表加载零 error", load_ok) + if not load_ok: + return + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(20261103) # 固定种子经 RngService 流口(RNG 单点) + # 行类路由面:resolver 预解析(boss 行集全行 row_kind=="boss";对照 battle 行集零 boss 行) + var boss_ids := {} + var battle_ids := {} + var boss_kind_ok := true + for row in db.combat.enemy.resolve_rowset(1, 4, "boss"): + boss_ids[String(row["enemy_id"])] = true + if String(row["row_kind"]) != "boss": + boss_kind_ok = false + var battle_kind_ok := true + for row in db.combat.enemy.resolve_rowset(1, 4, "battle"): + battle_ids[String(row["enemy_id"])] = true + if String(row["row_kind"]) == "boss": + battle_kind_ok = false + Kit.check("行类路由:boss 行集非空全行 boss、battle 行集非空零 boss 行〔断言补齐批 V10〕", + boss_kind_ok and battle_kind_ok and boss_ids.size() > 0 and battle_ids.size() > 0) + # 端到端:begin_battle(node_type="boss") → 实例化敌人行键全部出自 boss 行集 + var oc = load("res://core/battle_orchestrator.gd").new(db, rng) + var ok_boss: bool = oc.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.combat.hero.max_hp), "node_type": "boss"}) + Kit.check("前置:boss 节点开局受理(L1 D4 Boss 行集)", ok_boss) + if not ok_boss: + return + var all_from_boss := true + for u in oc._enemy_system.all_enemies(): + if not boss_ids.has(String(u["enemy_id"])): + all_from_boss = false + Kit.check("Boss 节点开战必选 Boss 行集(实例化敌人全部出自 boss 行集——跨宿主真实入口链)〔断言补齐批 V10〕", all_from_boss) diff --git a/game/tests/unit/battle_settlement/boss_gate_test.gd.uid b/game/tests/unit/battle_settlement/boss_gate_test.gd.uid new file mode 100644 index 00000000..548c836c --- /dev/null +++ b/game/tests/unit/battle_settlement/boss_gate_test.gd.uid @@ -0,0 +1 @@ +uid://dkmph2dfqjqer diff --git a/game/tests/unit/battle_settlement/damage_controller_test.gd b/game/tests/unit/battle_settlement/damage_controller_test.gd index b42bde9b..6430d0ac 100644 --- a/game/tests/unit/battle_settlement/damage_controller_test.gd +++ b/game/tests/unit/battle_settlement/damage_controller_test.gd @@ -35,6 +35,8 @@ func _init() -> void: _test_hero_death_halt(db) _test_ac_merge(db) _test_pool_clamp(db) + _test_pool_consumption_per_unit(db) + _test_defender_ac_per_unit(db) quit(Kit.finish("damage_controller_test")) @@ -45,7 +47,7 @@ func _on_row(head, ri, display, value, extra) -> void: "round_index": ri, "display": display, "value": value, "extra": extra, "highlight": head.get("highlight", "")}) -func _harness(db): +func _harness(db, depth: int = 0): var rng = RngServiceScript.new() rng.set_master_seed(TEST_SEED) var units = UnitContainerScript.new(db) @@ -53,7 +55,7 @@ func _harness(db): var ctrl = DamageControllerScript.new(db, rng, units, effects) ctrl.emit_row_cb = Callable(self, "_on_row") effects.notify_battle_start() - var spawn_ok: bool = units.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)}) + var spawn_ok: bool = units.spawn({"level_index": 1, "node_depth": depth, "hero_hp": int(db.combat.hero.max_hp)}) return {"db": db, "rng": rng, "units": units, "effects": effects, "ctrl": ctrl, "spawn_ok": spawn_ok} @@ -219,6 +221,7 @@ func _test_source_death_void(db) -> void: # 单位字典键集复刻(剥离 handle_id:生产键集含句柄,`==` 误命的可达条件被破坏,夹具刻意不携句柄) func _unit_fixture() -> Dictionary: return { + "handle_id": 0, # 生成序轴(个体化批——个体键导出沿 handle_id;手搭夹具同契约) "enemy_id": "fixture_enemy", "display_name": "地精", "hp": 5, "max_hp": 5, "atk": 1, @@ -342,7 +345,7 @@ func _test_ac_merge(db) -> void: Kit.check("前置:状态表存在 hero 侧 ac 型行", false) return var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0} - h.effects.apply_effect(String(ac_eff.effect_id), src) + h.effects.apply_effect(String(ac_eff.effect_id), StatusContainerScript.OWNER_HERO, src) var expected: int = override_v + int(ac_eff.magnitude) Kit.check("ac 型效果叠层入 AC(层数×幅度,不入池)", h.ctrl.defender_ac(h.units.hero_unit()) == expected) @@ -356,7 +359,7 @@ func _test_pool_clamp(db) -> void: return var lo: int = int(h.db.combat.settlement.core.pool_clamp_lo) var hi: int = int(h.db.combat.settlement.core.pool_clamp_hi) - Kit.check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0) + Kit.check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense", StatusContainerScript.OWNER_HERO) == 0 and lo <= 0 and hi >= 0) # 找 hero_offense 池的 percent 型 hero 侧效果行 var pool_eff := {} for row in h.db.combat.status_effects.rows: @@ -369,12 +372,12 @@ func _test_pool_clamp(db) -> void: Kit.check("前置:hero_offense 池存在 percent 型 hero 侧行", false) return var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0} - var first_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), src) + var first_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), StatusContainerScript.OWNER_HERO, src) var expected1: int = clampi(int(pool_eff.magnitude), lo, hi) - Kit.check("池值 = 单实例 L×m 加算后 clamp", first_ok and h.ctrl.clamped_pool("hero_offense") == expected1) - var second_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), src) + Kit.check("池值 = 单实例 L×m 加算后 clamp", first_ok and h.ctrl.clamped_pool("hero_offense", StatusContainerScript.OWNER_HERO) == expected1) + var second_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), StatusContainerScript.OWNER_HERO, src) Kit.check("累加政策重复授予受理且层数恒 1(同名重复授予加时长)——池值不变", - second_ok and h.ctrl.clamped_pool("hero_offense") == expected1) + second_ok and h.ctrl.clamped_pool("hero_offense", StatusContainerScript.OWNER_HERO) == expected1) # ac 型效果不入 percent 池 var ac_eff2 := {} for row2 in h.db.combat.status_effects.rows: @@ -382,9 +385,9 @@ func _test_pool_clamp(db) -> void: ac_eff2 = row2 break if not ac_eff2.is_empty(): - var before: int = h.ctrl.clamped_pool("hero_offense") - h.effects.apply_effect(String(ac_eff2.effect_id), src) - Kit.check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense") == before) + var before: int = h.ctrl.clamped_pool("hero_offense", StatusContainerScript.OWNER_HERO) + h.effects.apply_effect(String(ac_eff2.effect_id), StatusContainerScript.OWNER_HERO, src) + Kit.check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense", StatusContainerScript.OWNER_HERO) == before) # stack 政策效果:全新容器授予至满层 → 再授拒绝(满层拒绝原子——容器零写入) var stack_eff := {} for row3 in h.db.combat.status_effects.rows: @@ -399,6 +402,47 @@ func _test_pool_clamp(db) -> void: var granted := 0 var max_n: int = int(stack_eff.max_stack) for i in range(max_n + 1): - if effects2.apply_effect(String(stack_eff.effect_id), src): + if effects2.apply_effect(String(stack_eff.effect_id), StatusContainerScript.OWNER_HERO, src): granted += 1 Kit.check("stack 政策恰授予 max_stack 层后拒绝(满层拒绝原子)", granted == max_n) + + +# ---- 个体池读数 / 个体 AC 合流(增益状态 v1.5——敌方状态个体化批)---- + +func _test_pool_consumption_per_unit(db) -> void: + var h = _harness(db, 3) # depth 3 = hobgoblin 行集(unit_count 2-3——个体对照面需 ≥2 敌) + if not h.spawn_ok: + return + var enemies: Array = h.units.all_enemies() + Kit.check("V6 前置:行集 ≥2 敌(个体池对照面)", enemies.size() >= 2) + if enemies.size() < 2: + return + var wmag: int = int(db.combat.status_effects.row_by_id("weaken").magnitude) + var lo: int = int(db.combat.settlement.core.pool_clamp_lo) + var hi: int = int(db.combat.settlement.core.pool_clamp_hi) + var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} + var k1: String = StatusContainerScript.owner_key_for(enemies[0]) + var k2: String = StatusContainerScript.owner_key_for(enemies[1]) + Kit.check("V6 前置:e1 受理削弱", h.effects.apply_effect("weaken", k1, src)) + Kit.check("B 敌的削弱不进 A 敌的账:e1 池含 weaken 值(clamp 域内)、e2 恒 0〔个体化批 V6〕", + h.ctrl.clamped_pool("enemy_output", k1) == clampi(wmag, lo, hi) and h.ctrl.clamped_pool("enemy_output", k2) == 0) + + +func _test_defender_ac_per_unit(db) -> void: + var h = _harness(db, 3) + if not h.spawn_ok: + return + var enemies: Array = h.units.all_enemies() + Kit.check("V7 前置:行集 ≥2 敌(个体 AC 对照面)", enemies.size() >= 2) + if enemies.size() < 2: + return + var amag: int = int(db.combat.status_effects.row_by_id("alert").magnitude) + var src := {"source_class": "mine_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} + var u1: Dictionary = enemies[0] + var u2: Dictionary = enemies[1] + var k1: String = StatusContainerScript.owner_key_for(u1) + var base1: int = int(u1.get("armor_class", 0)) + var base2: int = int(u2.get("armor_class", 0)) + Kit.check("V7 前置:e1 受理警觉", h.effects.apply_effect("alert", k1, src)) + Kit.check("alert 各算各的:e1 AC = 自身基数 + 警觉幅度、e2 AC = 自身基数(该敌自己名下 ac 型)〔个体化批 V7〕", + h.ctrl.defender_ac(u1) == base1 + amag and h.ctrl.defender_ac(u2) == base2) diff --git a/game/tests/unit/battle_settlement/effect_port_test.gd b/game/tests/unit/battle_settlement/effect_port_test.gd index 85a0afbb..acb419c8 100644 --- a/game/tests/unit/battle_settlement/effect_port_test.gd +++ b/game/tests/unit/battle_settlement/effect_port_test.gd @@ -33,7 +33,7 @@ func _init() -> void: func _enc(feats: Array) -> Dictionary: return {"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), - "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "feats": feats, "tree_rules": [], + "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "feats": feats, "tree_rules": {}, "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "boss", "adv_bucket": {}} @@ -113,7 +113,7 @@ func _test_biz_fact_face_closed() -> void: var ctrl = orch.fx_host().damage # 阳性对照(载荷携带事实——harness 能见命中):合成行独占订阅面(rebuild 持有 id 过滤—— # 只挂阳性行),board_flat_hero 真实单据流,base_value 足量避开伤害下限保底 - orch.fx_host().rebuild_subscriptions([pos_id], [], []) + orch.fx_host().rebuild_subscriptions([pos_id], [], {}) var hp0 := int(ctrl.units.hero_unit().get("hp", 0)) var t_a: Dictionary = ctrl.open_ticket(ctrl.KIND_BOARD_FLAT_HERO, "probe_port", "闭面探测甲", {"channel": "board", "base_value": 10, "target_rule": "hero", "ri": 1, "origin_cell": orch.minesweeper.hero_pos()}) @@ -121,7 +121,7 @@ func _test_biz_fact_face_closed() -> void: var hp1 := int(ctrl.units.hero_unit().get("hp", 0)) Kit.check("V3 阳性对照:hp_ratio 载荷 when 通过——承伤减量恰 1(10→9)", hp0 - hp1 == 9) # 阴性对照(闭面外事实——宿主链答而业务面不答):订阅面换代为阴性合成行独占,同路径零减量=F10 现状形态守卫 - orch.fx_host().rebuild_subscriptions([neg_id], [], []) + orch.fx_host().rebuild_subscriptions([neg_id], [], {}) var t_b: Dictionary = ctrl.open_ticket(ctrl.KIND_BOARD_FLAT_HERO, "probe_port", "闭面探测乙", {"channel": "board", "base_value": 10, "target_rule": "hero", "ri": 1, "origin_cell": orch.minesweeper.hero_pos()}) ctrl.flush_submit(t_b) diff --git a/game/tests/unit/battle_settlement/refresh_feats_test.gd b/game/tests/unit/battle_settlement/refresh_feats_test.gd index 2c8850c5..7b7932c9 100644 --- a/game/tests/unit/battle_settlement/refresh_feats_test.gd +++ b/game/tests/unit/battle_settlement/refresh_feats_test.gd @@ -48,7 +48,7 @@ func _init() -> void: 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", + "feat_id": "probe_dt_counter", "display_name": "承伤计数探针", "rarity": "common", "cap": 1, "desc": "测试夹具:每次主角承伤计数 +1", "triggers": [{"on": "damage_taken", "do": {"kind": "counter_add", "counter_id": "dt_probe_hits", "value": 1}}], }) @@ -62,7 +62,7 @@ func _orch(seed_v: int): 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": [], + "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": {}} @@ -361,7 +361,7 @@ func test_no_feats_baseline() -> void: 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", + "feat_id": "probe_br_counter", "display_name": "刷新事实探针", "rarity": "common", "cap": 1, "desc": "测试夹具:每次成功刷新计数 +1", "triggers": [{"on": "board_refreshed", "do": {"kind": "counter_add", "counter_id": "br_probe_hits", "value": 1}}], }) diff --git a/game/tests/unit/battle_settlement/roll_multiplier_test.gd b/game/tests/unit/battle_settlement/roll_multiplier_test.gd index df8438c4..723f8129 100644 --- a/game/tests/unit/battle_settlement/roll_multiplier_test.gd +++ b/game/tests/unit/battle_settlement/roll_multiplier_test.gd @@ -231,24 +231,24 @@ func _weaken_accumulate_probe() -> Dictionary: if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp)}): return {} var src: Dictionary = {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index} - settlement._status_effects.apply_effect("weaken", src) + settlement._status_effects.apply_effect("weaken", "e1", src) settlement._status_effects.decay_round() # remaining 2 → 1(先兑付后衰减——只借衰减器,不结算) - settlement._status_effects.apply_effect("weaken", src) # 累加:remaining 1+2=3,层数不动 + settlement._status_effects.apply_effect("weaken", "e1", src) # 累加:remaining 1+2=3,层数不动 var regrant := _weaken_inst(settlement) - settlement._status_effects.apply_effect("weaken", src, 3) # 覆盖时长按覆盖值累加:3+3=6 + settlement._status_effects.apply_effect("weaken", "e1", src, 3) # 覆盖时长按覆盖值累加:3+3=6 var override_grant := _weaken_inst(settlement) - settlement._status_effects.apply_effect("weaken", src) # 同轮再授:6+2=8 + settlement._status_effects.apply_effect("weaken", "e1", src) # 同轮再授:6+2=8 var extra := _weaken_inst(settlement) if regrant.is_empty() or override_grant.is_empty() or extra.is_empty(): return {} # 走伤害控制器公有算术面 clamped_pool - return {"layers": int(extra.layers), "pool": int(settlement._damage.clamped_pool("enemy_output")), + return {"layers": int(extra.layers), "pool": int(settlement._damage.clamped_pool("enemy_output", "e1")), "after_regrant": int(regrant.remaining_settlements), "after_override": int(override_grant.remaining_settlements), "after_extra": int(extra.remaining_settlements), "snap_magnitude": int(regrant.snap_magnitude)} func _weaken_inst(settlement: Object) -> Dictionary: - for eff in settlement._status_effects.query_active("enemy"): + for eff in settlement._status_effects.query_active("e1"): if String(eff.effect_id) == "weaken": return eff return {} @@ -269,7 +269,7 @@ func _ac11_tier_samples() -> Array: var guard := 12 while settlement.battle_result == "" and guard > 0: guard -= 1 - settlement._status_effects.apply_effect("ac_up", {"source_class": "safe_cell", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index}) + settlement._status_effects.apply_effect("ac_up", "hero", {"source_class": "safe_cell", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index}) settlement.minesweeper.settle_batch([{"action": "stop"}]) var tiers: Array = [] for row in rows: @@ -368,7 +368,10 @@ func _hero_attack_row_with(opts: Dictionary, hero_atk_val: int, seed_val: int) - if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": hero_atk_val}): return {} for eff in opts.get("effects", []): - settlement._status_effects.apply_effect(String(eff), {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index}) + # 个体键读数(个体化批):按状态行 target_side 路由个体——主角行归主角桶、敌方行归首敌(被打者) + var erow: Dictionary = _db.combat.status_effects.row_by_id(String(eff)) + var okey: String = "e1" if String(erow.get("target_side", "")) == "enemy" else "hero" + settlement._status_effects.apply_effect(String(eff), okey, {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index}) settlement.minesweeper.settle_batch([{"action": "stop"}]) for row in rows: if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "hero_attack": diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index 2542adac..c89a7401 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -39,6 +39,7 @@ func _init() -> void: _smoke_skill_pool() _smoke_counter_buckets() _smoke_extended_rows() + _smoke_enemy_buff_targets_self() quit(Kit.finish("round_flow_skill_pool_test")) @@ -420,7 +421,7 @@ func _smoke_settlement_segments() -> void: if decays != 1 or verdicts != 1: decay_verdict_ok = false # 衰减/判定每结算恰一次 Kit.check("每轮行相位单调(授予→增益/减益→战斗→衰减→判定)", order_ok) - # 案C 段内序:段内伤害序 = 主角先手 →〔连珠矢随先手段〕→ 敌方逐个 → 地图账殿后。 + # 案C 段内序:段内伤害序 = 主角先手 → 敌方逐个 → 地图账殿后。 # 地图账 = 全部棋盘来源伤害行 + 主角倒下不兑留痕行;违序(先于先手或插进敌方还手中间)即失败。 var mapdmg_ok := true for ri_key in by_round.keys(): @@ -439,13 +440,13 @@ func _smoke_settlement_segments() -> void: var rw: Dictionary = rows_r[i] var sc := String(rw.get("source_class", "")) var is_board_acct: bool = (String(rw.get("type", "")) == "damage" \ - and not ["hero_attack", "enemy_action", "fx_streak_segments"].has(sc)) \ + and not ["hero_attack", "enemy_action"].has(sc)) \ or String(rw.get("forfeit", "")) == "hero_down" if not is_board_acct: continue if (first_hero >= 0 and i < first_hero) or (last_enemy >= 0 and i < last_enemy): mapdmg_ok = false - Kit.check("案C 段内序:地图伤害账殿后(主角先手→敌方逐个→地图账;连珠矢随先手段)", mapdmg_ok) + Kit.check("案C 段内序:地图伤害账殿后(主角先手→敌方逐个→地图账)", mapdmg_ok) Kit.check("衰减/判定段每结算恰一段(先兑付后衰减——含末轮)", decay_verdict_ok) @@ -994,7 +995,7 @@ func _smoke_counter_buckets() -> void: var RngS = load("res://core/rng_service.gd") var enc1 = {"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": ["twin_pick"], "tree_rules": [], "battle_index": 1, + "feats": ["twin_pick"], "tree_rules": {}, "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle", "adv_bucket": led} var o1 = RigS.new(_db, _fresh_rng(779301)) @@ -1013,7 +1014,7 @@ func _smoke_counter_buckets() -> void: # V6——battle 桶随宿主销毁:第1战标错落 insurance_used(battle 桶无前缀),次战新编排器读 0。 var encA = {"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": ["perfect_insurance"], "tree_rules": [], "battle_index": 1, + "feats": ["perfect_insurance"], "tree_rules": {}, "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle", "adv_bucket": {}} var oA = RigS.new(_db, _fresh_rng(779401)) @@ -1052,7 +1053,7 @@ func _smoke_extended_rows() -> void: var RigS = load("res://core/battle_orchestrator.gd") # 局部装配别名(_smoke_counter_buckets 同款——跨函数不共享局部量) var enc_fx = {"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": ["geologist"], "tree_rules": [], "battle_index": 1, + "feats": ["geologist"], "tree_rules": {}, "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle", "adv_bucket": {}} # ② 兑付行:有未揭示安全格 → 掷格揭示成功 → 扩展行带真实格与 fx_reveal 身份 @@ -1086,3 +1087,50 @@ func _smoke_extended_rows() -> void: Kit.check("卡④ wr2③:揭示落空行 type=extended 且 effect_id=fx_reveal 且 origin_cell=越界哨值〔明细收敛 V2〕", not missed.is_empty() and String(missed.get("type", "")) == "extended" and Vector2i(missed["origin_cell"][0], missed["origin_cell"][1]).x == -1) + + +# ---- 敌方增益技自体落点(增益状态 v1.5 目标规则「自己」——敌方状态个体化批)---- +# 真实入口=战斗结算编排链(技能施放):begin_battle → stop 结算推进一轮(战斗段施放+轮末衰减, +# 蓄力表值 2 拍衰减后剩 1 拍仍在池)→ 逐敌读敌方输出池。判据=施放者与被他敌的池读数差。 + +func _smoke_enemy_buff_targets_self() -> void: + var emp_mag: int = int(_db.combat.status_effects.row_by_id("empower").magnitude) + var sc_script = load("res://core/status_container.gd") + var found_seed := -1 + var found_ok := false + var carrier_pool := -999 + for s in range(1, 401): + var rg = load("res://core/rng_service.gd").new() + rg.set_master_seed(s) + var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg) + var rows: Array = [] + settlement.step_row.connect(func(row): rows.append(row)) + # L1 depth 3(hobgoblin 行集 2-3 只——depth 0 单 goblin 常被主角先手一击秒杀,死亡即不动手、cast 不发生) + if not settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(_db.combat.hero.max_hp)}): + continue + for turn in range(4): + if settlement.battle_result != "": + break + settlement.minesweeper.settle_batch([{"action": "stop"}]) # 推进一轮:施放→衰减(蓄力仍持 1 拍) + if settlement.battle_result != "": + break + var charge_casts := 0 + for row in rows: + if String(row.get("type", "")) == "cast" and String(row.get("skill_id", "")) == "charge": + charge_casts += 1 + if charge_casts != 1: + continue # 只取恰一次蓄力施放的时刻——判据单义 + var carriers: Array = [] + for u in settlement._enemy_system.query_alive("enemy"): + var okey: String = sc_script.owner_key_for(u) + if settlement._damage.clamped_pool("enemy_output", okey) != 0: + carriers.append(okey) + if carriers.size() == 1: + found_seed = s + carrier_pool = settlement._damage.clamped_pool("enemy_output", String(carriers[0])) + found_ok = carrier_pool == emp_mag + break + if found_seed > 0: + break + Kit.check("蓄力施放种子可达(恰一次 cast=charge,seed=%d)" % found_seed, found_seed > 0) + Kit.check("敌方增益技自体落点:施放者 output 池 = 蓄力幅度(%d,实测 %d)、他敌恒 0〔个体化批 V5〕" % [emp_mag, carrier_pool], found_ok) diff --git a/game/tests/unit/battle_settlement/seal_polarity_test.gd b/game/tests/unit/battle_settlement/seal_polarity_test.gd index 862711b1..547f394a 100644 --- a/game/tests/unit/battle_settlement/seal_polarity_test.gd +++ b/game/tests/unit/battle_settlement/seal_polarity_test.gd @@ -748,7 +748,7 @@ func _smoke_negative_dictionary() -> void: var snap_b: Dictionary = settlement._reward_economy.query_full_snapshot(by_type["blast"]) settlement.reward_grant().grant_routed(snap_b, "negative", {"source_class": "mine_hit", "cell": by_type["blast"], "ri": 1}) var alert_ok: bool = false - for eff_a in settlement._status_effects.query_active("enemy"): + for eff_a in settlement._status_effects.query_active_side("enemy"): if String(eff_a.effect_id) == "alert": alert_ok = true Kit.check("blast 型负向 → 警觉(敌方护甲等级 +1 状态在位)", alert_ok) diff --git a/game/tests/unit/battle_settlement/trigger_cond_test.gd b/game/tests/unit/battle_settlement/trigger_cond_test.gd index 026b88d6..fc6eb2c7 100644 --- a/game/tests/unit/battle_settlement/trigger_cond_test.gd +++ b/game/tests/unit/battle_settlement/trigger_cond_test.gd @@ -113,7 +113,7 @@ func _init() -> void: var orch = RigS.new(db, _rng(700001)) Kit.check_quiet(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], - "feats": [], "tree_rules": [], "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0, + "feats": [], "tree_rules": {}, "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle", "adv_bucket": {}}), "V6 前置:战斗受理") # facts 组包经真实摇号路径消费(mines 全零时条件技 sneak_ambush 不入集合——facts 四键在位的可观察面) Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["last"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0, diff --git a/game/tests/unit/battle_settlement/variant_placement_test.gd b/game/tests/unit/battle_settlement/variant_placement_test.gd index 7ac34fce..415c86de 100644 --- a/game/tests/unit/battle_settlement/variant_placement_test.gd +++ b/game/tests/unit/battle_settlement/variant_placement_test.gd @@ -28,6 +28,7 @@ func _init() -> void: _smoke_variant_placement(1) _smoke_variant_placement(2) _smoke_global_variant() + _test_pack_min_from_table() quit(Kit.finish("variant_placement_test")) @@ -331,3 +332,36 @@ func _errs_has_cross_field(errs: Array) -> bool: # ——互锁负样本/门控三维/资源面偏斜守恒与合成序/快照投影不变)---- # 取号计数桩(variant_place 取号断言——子类拦截计数,派生与取数语义零改动) + + +# ---- 成包下界读表(架构整改——pack_min 表头单点)---- +# 观测面局限申报(实现期裁定,详见实现报告/对照表):pack_min 消费面=randi_range 下界(软目标—— +# 8 邻贴接禁令下生长早停为合法态〔board_installer 注释明示〕,候选池碎片化使「产物零 void: + # ① 静态:布置面零字面量下界(randi_range(2, 退役——下界读 pack_min 表头单点) + var src := FileAccess.get_file_as_string("res://core/board_installer.gd") + Kit.check("布置面零字面量下界(randi_range(2, 退役、下界读表单点在位)〔架构整改 V10·静态〕", + not src.contains("randi_range(2,") and src.contains("int(db.combat.variation.pack_min)")) + # ② 同种子改值产物互异(扫种子取首个差异局——真实入口 begin_battle 布置段) + var vt = _db.combat.variation + var saved_min: int = int(vt.pack_min) + var found_diff := false + for s in range(42, 102): + var sigs := {} + for pm in [3, 1]: + vt.pack_min = pm + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(s) + var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) + if not settlement.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}): + continue + var b = settlement.minesweeper.current_board() + sigs[pm] = str(b.variants.keys()) + vt.pack_min = saved_min + if String(sigs.get(3, "")) != "" and String(sigs.get(3, "")) != String(sigs.get(1, "")): + found_diff = true + break + vt.pack_min = saved_min + Kit.check("同种子 pack_min=3 vs 1 布置产物互异(下界进 randi_range——改值可观测)〔架构整改 V10〕", found_diff) \ No newline at end of file diff --git a/game/tests/unit/contracts/system_contracts_test.gd b/game/tests/unit/contracts/system_contracts_test.gd index 2d172f7c..4c212e80 100644 --- a/game/tests/unit/contracts/system_contracts_test.gd +++ b/game/tests/unit/contracts/system_contracts_test.gd @@ -25,6 +25,14 @@ func _init() -> void: _test_rng_tag_closure(db) _test_ticket_nine_columns() _test_status_boolean_acceptance(db) + _test_status_individual_isolation(db) + _test_status_grant_all_per_unit(db) + _test_status_death_clears(db) + _test_status_merge_per_unit(db) + _test_status_decay_and_clear(db) + _test_status_stack_full_reject(db) + _test_status_independent_branch(db) + _test_status_snapshot_first_grant(db) quit(Kit.finish("system_contracts_test")) @@ -112,13 +120,174 @@ func _test_status_boolean_acceptance(db) -> void: return var probe_id: String = String(any_row.effect_id) var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0} - Kit.check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, src) == false) - Kit.check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", src) == false) + Kit.check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, StatusContainerScript.OWNER_HERO, src) == false) + Kit.check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", StatusContainerScript.OWNER_HERO, src) == false) effects.notify_battle_start() - Kit.check("开域后受理已知 id(布尔受理口径 true 通道)", effects.apply_effect(probe_id, src) == true) - var active: Array = effects.query_active(String(any_row.target_side)) + Kit.check("开域后受理已知 id(布尔受理口径 true 通道)", effects.apply_effect(probe_id, StatusContainerScript.OWNER_HERO, src) == true) + var active: Array = effects.query_active(StatusContainerScript.OWNER_HERO) var projected := false for a in active: if String(a.effect_id) == probe_id: projected = int(a.effective_value) == int(a.layers) * int(a.snap_magnitude) Kit.check("受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)", projected) + + +# ---- B4 个体归属契约(增益状态 v1.5——敌方状态个体化批):个体隔离/全体逐体发/死亡即清/同体合并 ---- +# 个体键直传字面量("e1"/"e2"/"e3"——拼写约定单点在 StatusContainer.owner_key_for,测试面模拟授予路径逐只授) + +func _test_status_individual_isolation(db) -> void: + var effects = StatusContainerScript.new(db) + effects.notify_battle_start() + var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} + var wrow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("weaken") + Kit.check("前置:weaken 行在册(累加策略——个体合并语义载体)", not wrow.is_empty() and String(wrow.stack_policy) == "accumulate") + Kit.check("个体隔离:A 敌受理", effects.apply_effect("weaken", "e1", src) == true) + Kit.check("个体隔离:B 敌受理(同名不同体互不合并)", effects.apply_effect("weaken", "e2", src) == true) + Kit.check("个体隔离:A/B 各持一条(各体桶独立)〔个体化批 V1〕", + effects.query_active("e1").size() == 1 and effects.query_active("e2").size() == 1) + effects.decay_round() # A/B 各自 −1——先验证互不干扰后验证独立倒数 + var a_after: Array = effects.query_active("e1") + var b_after: Array = effects.query_active("e2") + var a_rem: int = int(a_after[0].remaining_settlements) if a_after.size() == 1 else -1 + var b_rem: int = int(b_after[0].remaining_settlements) if b_after.size() == 1 else -1 + Kit.check("个体隔离:各体独立倒数(A=表值−1、B=表值−1 同步各减)〔个体化批 V1〕", + a_rem == int(wrow.duration_settlements) - 1 and b_rem == int(wrow.duration_settlements) - 1) + Kit.check("个体隔离:投影携带 owner_key(逐个体承载出户)〔个体化批 V1〕", + a_after.size() == 1 and String(a_after[0].owner_key) == "e1" and String(b_after[0].owner_key) == "e2") + + +func _test_status_grant_all_per_unit(db) -> void: + var effects = StatusContainerScript.new(db) + effects.notify_battle_start() + var src := {"source_class": "mine_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} + var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("enrage") + Kit.check("前置:enrage 行在册(叠加策略——逐体叠层载体)", not erow.is_empty() and String(erow.stack_policy) == "stack") + # 模拟授予路径全体规则(reward_grant._apply_percent_row 同构):循环三敌逐只各发一条 + var keys: Array[String] = ["e1", "e2", "e3"] + var granted := 0 + for k in keys: + if effects.apply_effect("enrage", k, src): + granted += 1 + Kit.check("全体授予逐体发:三敌各受理一条(对全体=逐只各发)〔个体化批 V2〕", granted == 3) + var e2_ok: bool = effects.apply_effect("enrage", "e2", src) # e2 再授一层——各体独立合并 + Kit.check("全体授予逐体发:同体叠加至 2 层、异体层数不动〔个体化批 V2〕", + e2_ok and int(effects.query_active("e2")[0].layers) == 2 + and int(effects.query_active("e1")[0].layers) == 1 and int(effects.query_active("e3")[0].layers) == 1) + effects.decay_round() + var all_decayed := true + for k in keys: + for eff in effects.query_active(k): + if int(eff.remaining_settlements) != int(erow.duration_settlements) - 1: + all_decayed = false + Kit.check("全体授予逐体发:各体同轮独立衰减 −1(逐桶各减)〔个体化批 V2〕", all_decayed) + + +func _test_status_death_clears(db) -> void: + var effects = StatusContainerScript.new(db) + effects.notify_battle_start() + var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} + effects.apply_effect("weaken", "e1", src) + effects.apply_effect("weaken", "e2", src) + Kit.check("死亡即清:purge_owner 返回移除数(e1 名下 1 条)〔个体化批 V3〕", effects.purge_owner("e1") == 1) + Kit.check("死亡即清:e1 桶清零(重 purge 零移除)〔个体化批 V3〕", effects.query_active("e1").is_empty() and effects.purge_owner("e1") == 0) + Kit.check("死亡即清:e2 不受影响(不转移不并回)〔个体化批 V3〕", effects.query_active("e2").size() == 1) + var side_all: Array = effects.query_active_side("enemy") + Kit.check("死亡即清:侧汇总只含存活个体实例〔个体化批 V3〕", side_all.size() == 1 and String(side_all[0].owner_key) == "e2") + + +func _test_status_merge_per_unit(db) -> void: + var effects = StatusContainerScript.new(db) + effects.notify_battle_start() + var src := {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": 0} + var wrow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("weaken") + var w_dur: int = int(wrow.duration_settlements) + effects.apply_effect("weaken", "e1", src) + effects.apply_effect("weaken", "e1", src) + Kit.check("同体合并照旧:累加行同体再授时长累加(%d+%d——仍单实例)〔个体化批 V4〕" % [w_dur, w_dur], + effects.query_active("e1").size() == 1 and int(effects.query_active("e1")[0].remaining_settlements) == w_dur * 2) + var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("enrage") + var max_stack: int = int(erow.max_stack) + var granted_n := 0 + for i in range(max_stack + 1): + if effects.apply_effect("enrage", "e2", src): + granted_n += 1 + Kit.check("同体合并照旧:叠加行满层拒绝(恰授 max_stack=%d 层)〔个体化批 V4〕" % max_stack, granted_n == max_stack) + Kit.check("同体合并照旧:满层时异体仍受理(拒绝只在同体内原子)〔个体化批 V4〕", effects.apply_effect("enrage", "e3", src) == true) + + +# ---- B4 结算链契约(断言补齐批——①增益状态结算链):衰减逐值/战斗结束全清/满层拒绝/独立并存/首授快照 ---- + +func _test_status_decay_and_clear(db) -> void: + var effects = StatusContainerScript.new(db) + effects.notify_battle_start() + var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} + var wrow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("weaken") + var w_dur: int = int(wrow.duration_settlements) + Kit.check("V1 前置:weaken 时长 ≥2(两拍衰减观测面)", w_dur >= 2) + Kit.check("V1 前置:e1 受理", effects.apply_effect("weaken", "e1", src)) + Kit.check("衰减逐值:首次 decay 返回移除数 0(未归零)〔断言补齐批 V1〕", effects.decay_round() == 0) + Kit.check("衰减逐值:每结算恰 −1(remaining=%d−1)〔断言补齐批 V1〕" % (w_dur - 1), + effects.query_active("e1").size() == 1 and int(effects.query_active("e1")[0].remaining_settlements) == w_dur - 1) + Kit.check("到期移除:第二次 decay 返回移除数 1〔断言补齐批 V2〕", effects.decay_round() == 1) + Kit.check("到期移除:归零实例当轮销毁(投影零行)〔断言补齐批 V2〕", effects.query_active("e1").is_empty()) + effects.apply_effect("weaken", "e2", src) + effects.apply_effect("attack_up", StatusContainerScript.OWNER_HERO, src) + Kit.check("V2 前置:两桶各持实例", effects.query_active_side("enemy").size() == 1 and effects.query_active(StatusContainerScript.OWNER_HERO).size() == 1) + Kit.check("战斗结束全清:notify_battle_end 返回 true 且全实例清空(两桶零行)〔断言补齐批 V2〕", + effects.notify_battle_end() and effects.query_active_side("enemy").is_empty() and effects.query_active(StatusContainerScript.OWNER_HERO).is_empty()) + + +func _test_status_stack_full_reject(db) -> void: + var effects = StatusContainerScript.new(db) + effects.notify_battle_start() + var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} + var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("enrage") + var max_stack: int = int(erow.max_stack) + for i in range(max_stack): + effects.apply_effect("enrage", "e1", src) + Kit.check("V3 前置:满层(恰 %d 层)" % max_stack, + effects.query_active("e1").size() == 1 and int(effects.query_active("e1")[0].layers) == max_stack) + var before_layers: int = int(effects.query_active("e1")[0].layers) + var before_rem: int = int(effects.query_active("e1")[0].remaining_settlements) + Kit.check("满层拒绝:再授返 false〔断言补齐批 V3〕", effects.apply_effect("enrage", "e1", src) == false) + Kit.check("满层拒绝:层数与剩余零写入(容器逐字不变——原子拒绝)〔断言补齐批 V3〕", + int(effects.query_active("e1")[0].layers) == before_layers and int(effects.query_active("e1")[0].remaining_settlements) == before_rem) + + +func _test_status_independent_branch(db) -> void: + var effects = StatusContainerScript.new(db) + effects.notify_battle_start() + var probe := { + "effect_id": "probe_independent_v4", + "display_name": "并存试验", + "target_side": "enemy", + "magnitude": 7, + "unit": "flat", + "duration_settlements": 2, + "stack_policy": "independent", + "max_stack": 1, + } + db.combat.status_effects.rows.append(probe) # 直改已加载表(运行时注入试验行——并存分支零内容行,夹具面) + var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} + Kit.check("independent 并存:第一次授予受理〔断言补齐批 V4〕", effects.apply_effect("probe_independent_v4", "e1", src)) + Kit.check("independent 并存:第二次授予受理(并存——无合并无满层语义)〔断言补齐批 V4〕", effects.apply_effect("probe_independent_v4", "e1", src)) + var both: Array = effects.query_active("e1") + Kit.check("independent 并存:两实例各持时长 2 独立倒数〔断言补齐批 V4〕", + both.size() == 2 and int(both[0].remaining_settlements) == 2 and int(both[1].remaining_settlements) == 2) + db.combat.status_effects.rows.pop_back() # 测毕还原(表行零残留) + Kit.check("independent 试验行测毕还原(row_by_id 零命中)", db.combat.status_effects.row_by_id("probe_independent_v4").is_empty()) + + +func _test_status_snapshot_first_grant(db) -> void: + var effects = StatusContainerScript.new(db) + effects.notify_battle_start() + var src_first := {"source_class": "seal_hit", "origin_cell": Vector2i(3, 4), "round_index": 1} + var src_second := {"source_class": "mine_hit", "origin_cell": Vector2i(8, 9), "round_index": 5} + effects.apply_effect("weaken", "e1", src_first) + effects.apply_effect("weaken", "e1", src_second) # 累加:时长加、归因与快照沿首授 + var inst: Array = effects.query_active("e1") + Kit.check("首授快照:累加合并仍单实例〔断言补齐批 V5〕", inst.size() == 1) + Kit.check("首授快照:origin_cell/round_index/source_class 沿首授(合并实例只持首授归因)〔断言补齐批 V5〕", + String(inst[0].source_class) == "seal_hit" and inst[0].origin_cell == Vector2i(3, 4) and int(inst[0].round_index) == 1) + Kit.check("首授快照:幅度快照 = 首授时表值(抗热重载域)〔断言补齐批 V5〕", + int(inst[0].snap_magnitude) == int(db.combat.status_effects.row_by_id("weaken").magnitude)) diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index e362e441..6437559d 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -589,7 +589,7 @@ func _smoke_feats() -> void: var sv_c = h.get("set", null) EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if sv_c == null else int(sv_c), sv_c != null) # 开格系 - fx.rebuild(["twin_pick"], [], []) + fx.rebuild(["twin_pick"], [], {}) for i in 4: var hits_dp: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_paid", {"ap_cost": 1}) var refund := false @@ -601,7 +601,7 @@ func _smoke_feats() -> void: Kit.check("双管镐:第 %d 次付费挖格返 1 AP(每第 2 次——mod_eq 预评估口径)" % (i + 1), refund) else: Kit.check("双管镐反例:第 %d 次不返(未满 2 次)" % (i + 1), not refund) - fx.rebuild(["geologist"], [], []) + fx.rebuild(["geologist"], [], {}) var geo_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": true}) apply_fx.call(geo_hits) Kit.check("地质学家:首开新类型集 1 点〔专长批 V3〕", _hits_kind(geo_hits, "counter_add").size() == 1) @@ -611,7 +611,7 @@ func _smoke_feats() -> void: Kit.check("地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕", _hits_kind(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0) var geo_hits3: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": false}) Kit.check("地质学家反例:同类型重复首开不叠点〔专长批 V3〕", _hits_kind(geo_hits3, "counter_add").is_empty()) - fx.rebuild(["hard_hat"], [], []) + fx.rebuild(["hard_hat"], [], {}) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {})) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {})) Kit.check("安全帽:开安全格攒层 ×2〔专长批 V3〕", EffectEngine.counter(orch.fx_host().counters_ctx, "helm_layers") == 2) @@ -621,7 +621,7 @@ func _smoke_feats() -> void: if String(h.get("kind", "")) == "damage_reduce": hh_reduce = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", ""))) Kit.check("安全帽:踩雷耗层减伤=层值 2(一次清零随施加面 set 行)〔专长批 V3〕", hh_reduce == 2) - fx.rebuild(["skip_mining"], [], []) + fx.rebuild(["skip_mining"], [], {}) var sk_zero: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": true}) apply_fx.call(sk_zero) Kit.check("跳格采掘:空白 0 格攒 1 点〔专长批 V3〕", _hits_kind(sk_zero, "counter_add").size() == 1) @@ -630,24 +630,24 @@ func _smoke_feats() -> void: apply_fx.call(sk_full) Kit.check("跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕", _hits_kind(sk_full, "reveal_tiles").size() == 1) Kit.check("跳格采掘反例:普通开格不计点〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty()) - fx.rebuild(["blast_accounting"], [], []) + fx.rebuild(["blast_accounting"], [], {}) for i in 5: var ba_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_paid", {"ap_cost": 1}) for h in ba_hits: if String(h.get("kind", "")) == "counter_add": EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), 1) Kit.check("爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1) - fx.rebuild(["frontier_miner"], [], []) + fx.rebuild(["frontier_miner"], [], {}) Kit.check("边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1 and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty()) # 封印系 - fx.rebuild(["seal_echo"], [], []) + fx.rebuild(["seal_echo"], [], {}) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {})) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {})) var se_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}) Kit.check("封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕", _hits_kind(se_hits, "grant_ap").size() == 1 and String((_hits_kind(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue") - fx.rebuild(["seal_smith"], [], []) + fx.rebuild(["seal_smith"], [], {}) var sm_remove := 0 for i in 3: var sm_dispatch: Array = fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}) @@ -656,14 +656,14 @@ func _smoke_feats() -> void: if String(h.get("kind", "")) == "remove_mines": sm_remove += 1 Kit.check("封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕", sm_remove == 1) - fx.rebuild(["seal_stake"], [], []) + fx.rebuild(["seal_stake"], [], {}) Kit.check("封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1 and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round") - fx.rebuild(["warning_beacon"], [], []) + fx.rebuild(["warning_beacon"], [], {}) Kit.check("示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1) # 踩雷/受伤系 - fx.rebuild(["pain_memory"], [], []) + fx.rebuild(["pain_memory"], [], {}) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {})) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {})) var pm_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}) @@ -671,7 +671,7 @@ func _smoke_feats() -> void: for h in orch._hits_of(pm_hits, "attack_bonus"): pm_bonus = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", ""))) Kit.check("疼痛记忆:攻击附加层值伤害=层值 2·用后清零(set 行随施加面)〔专长批 V3〕", pm_bonus == 2) - fx.rebuild(["fuse_wire"], [], []) + fx.rebuild(["fuse_wire"], [], {}) var fw_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 6}) var fw_shield := 0 for h in fw_hits: @@ -682,43 +682,43 @@ func _smoke_feats() -> void: if String(h.get("kind", "")) == "counter_add": EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0))) Kit.check("保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕", fw_shield == 0) - fx.rebuild(["blood_insight"], [], []) + fx.rebuild(["blood_insight"], [], {}) Kit.check("见血开窍:踩雷揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1) - fx.rebuild(["trap_instinct"], [], []) + fx.rebuild(["trap_instinct"], [], {}) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {})) var ti_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}) var ti_val := 0 for h in orch._hits_of(ti_hits, "attack_bonus"): ti_val = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", ""))) Kit.check("陷阱本能:踩雷本轮攻击 +2(attack_bonus 通道——面板加算)〔专长批 V3〕", ti_val == 2) - fx.rebuild(["chain_traps", "pain_memory"], [], []) + fx.rebuild(["chain_traps", "pain_memory"], [], {}) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {})) var ct_before: int = EffectEngine.counter(orch.fx_host().counters_ctx, "pain_layers") apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {})) Kit.check("连环陷阱:同轮第 2 颗起每颗额外 +1 层(疼痛记忆同账)〔专长批 V3〕", EffectEngine.counter(orch.fx_host().counters_ctx, "pain_layers") == ct_before + 2) - fx.rebuild(["trap_burst"], [], []) + fx.rebuild(["trap_burst"], [], {}) var tb_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"damage": 8}) Kit.check("陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕", _hits_kind(tb_hits, "blast_scaled").size() == 1 and float((_hits_kind(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0) - fx.rebuild(["blood_honey"], [], []) + fx.rebuild(["blood_honey"], [], {}) Kit.check("血蜜:踩雷回复 N 生命〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1) - fx.rebuild(["stone_skin"], [], []) + fx.rebuild(["stone_skin"], [], {}) var ss_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1}) Kit.check("石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕", _hits_kind(ss_hits, "ac_bonus").size() == 1 and int((_hits_kind(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3) - fx.rebuild(["last_stand"], [], []) + fx.rebuild(["last_stand"], [], {}) Kit.check("背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1 and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty()) # 跨族桥 - fx.rebuild(["breaker_strike"], [], []) + fx.rebuild(["breaker_strike"], [], {}) Kit.check("破封一击:挖开带封印危险格 +1 层·无封印反例零触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1 and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty()) # 节奏系 - fx.rebuild(["map_exchange"], [], []) + fx.rebuild(["map_exchange"], [], {}) var me_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 4}) apply_fx.call(me_hits) EffectEngine.counter_add(orch.fx_host().counters_ctx, "stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账 @@ -728,41 +728,41 @@ func _smoke_feats() -> void: me_atk = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", ""))) Kit.check("弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕", me_atk == 4) Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty()) - fx.rebuild(["last_drop"], [], []) + fx.rebuild(["last_drop"], [], {}) var ld_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 2, "ap_exhausted_last": true}) Kit.check("一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕", _hits_kind(ld_hits, "grant_ap").size() == 1) Kit.check("一滴不剩反例:上轮有余不发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty()) - fx.rebuild(["market_close"], [], []) + fx.rebuild(["market_close"], [], {}) for i in 8: for h in fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {}): if String(h.get("kind", "")) == "counter_add": EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), 1) Kit.check("收盘:本轮开格≥8→+1 GP〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1) # 战斗系 - fx.rebuild(["execution_scholar"], [], []) - Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1) - fx.rebuild(["executioner"], [], []) + fx.rebuild(["execution_scholar"], [], {}) + Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌"}), "reveal_tiles").size() == 1) + fx.rebuild(["executioner"], [], {}) var ex_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 30, "is_boss": false}) Kit.check("处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕", _hits_kind(ex_hits, "target_attack_bonus").size() == 1 and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty()) - fx.rebuild(["field_dressing"], [], []) + fx.rebuild(["field_dressing"], [], {}) Kit.check("战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1) # 契约尖点 - fx.rebuild(["blood_tax"], [], []) + fx.rebuild(["blood_tax"], [], {}) Kit.check("血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1 and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk") # 迁入 2 件 - fx.rebuild(["venom_edge"], [], []) + fx.rebuild(["venom_edge"], [], {}) Kit.check("淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1 and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty()) - fx.rebuild(["seal_burst"], [], []) + fx.rebuild(["seal_burst"], [], {}) Kit.check("封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1) # 零持有 var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74)) - orch0.fx_host().rebuild_subscriptions([], [], []) + orch0.fx_host().rebuild_subscriptions([], [], {}) Kit.check("零持有订阅=零命中(relic 关着零扰动——伤害管线既有行为逐位不变)", orch0.fx_host().fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}).is_empty() and orch0.fx_host().fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": true, "terrain_kind": []}).is_empty() diff --git a/game/tests/unit/core/auto_player_deduce_test.gd b/game/tests/unit/core/auto_player_deduce_test.gd new file mode 100644 index 00000000..380e7865 --- /dev/null +++ b/game/tests/unit/core/auto_player_deduce_test.gd @@ -0,0 +1,70 @@ +# tests/unit/core/auto_player_deduce_test.gd — 模拟玩家 0 格连锁算子〔架构整改 V15〕 +# 断言面 = AutoPlayer._deduce 补齐后:safe 集含「与所有已开格 8 邻不接」的远端格(该形态格 +# single/subset 约束不可达——唯一来源=0 格连锁经 preview_reveal 真实闭包);零盲挖语义回归 +# (mines 集零安全格)。真实入口=AutoPlayer._deduce(真板真流——begin_battle 装配链)。 +# 运行:godot --headless --path game --script res://tests/unit/core/auto_player_deduce_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const AutoPlayer := preload("res://sim/auto_player.gd") +const BattleDriver := preload("res://sim/battle_driver.gd") + + +func _init() -> void: + _smoke_zero_chain_rule() + quit(Kit.finish("auto_player_deduce_test")) + + +func _smoke_zero_chain_rule() -> void: + var db = load("res://core/config_db.gd").new() + var load_ok: bool = db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0 + Kit.check("前置:表加载零 error", load_ok) + if not load_ok: + return + var found := false + var found_seed := -1 + var remote_ok := false + var mines_clean := false + for s in range(42, 142): + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(s) + var settlement = load("res://core/battle_orchestrator.gd").new(db, rng) + if not settlement.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)}): + continue + var ms = settlement.minesweeper + BattleDriver.drive_round(settlement) # 真实挖一轮(0 连锁实挖触发——信息面展开) + if ms.current_board() == null: + continue + var dd: Dictionary = AutoPlayer._deduce(ms) + var safe: Array = dd["safe"] + var mines: Array = dd["mines"] + # 远端格:safe 中与所有已开格 8 邻距离 >1(single/subset 约束只覆盖已开数字格邻域—— + # 此形态格唯一来源=0 格连锁算子) + var revealed: Array = ms.revealed_cells() + var remote := false + for rc_v in safe: + var rc: Vector2i = rc_v + var touches := false + for rv in revealed: + var d: Vector2i = rc - rv + if maxi(absi(d.x), absi(d.y)) <= 1: + touches = true + break + if not touches: + remote = true + break + # 零盲挖:mines 集全落真雷(板面雷集) + var board = ms.current_board() + var clean := true + for mv in mines: + if not board.is_mine(mv): + clean = false + if remote and clean: + found = true + found_seed = s + remote_ok = true + mines_clean = true + break + Kit.check("0 格连锁种子可达(远端 safe 形态在案,seed=%d)〔架构整改 V15〕" % found_seed, found) + Kit.check("远端格入 safe 集(桥接 0 区域传播——改前约束模型必缺)〔架构整改 V15〕", remote_ok) + Kit.check("零盲挖语义保持(mines 集零安全格)〔架构整改 V15〕", mines_clean) diff --git a/game/tests/unit/core/auto_player_deduce_test.gd.uid b/game/tests/unit/core/auto_player_deduce_test.gd.uid new file mode 100644 index 00000000..d7c3b2c4 --- /dev/null +++ b/game/tests/unit/core/auto_player_deduce_test.gd.uid @@ -0,0 +1 @@ +uid://cggeuptv8skhl diff --git a/game/tests/unit/core/battle_driver_drive_test.gd b/game/tests/unit/core/battle_driver_drive_test.gd new file mode 100644 index 00000000..43ba5e85 --- /dev/null +++ b/game/tests/unit/core/battle_driver_drive_test.gd @@ -0,0 +1,150 @@ +# tests/unit/core/battle_driver_drive_test.gd — drive_round 驱动循环契约〔架构整改 V13〕 +# 断言面 = BattleDriver.drive_round 通用签名四形态: +# ① 缺省两参(真编排器同种子两跑逐位——确定性自证;四调用方零改动回归由全套承担); +# ② provider 形态(桩 settlement——策略回调产出 dig/seal_place/refresh 各一、日志回调收四元组); +# ③ max_rejects(策略恒返被拒动作——第 3 拒后停手出环); +# ④ 合步 next(move 后相位变更即断弃 dig)。 +# 桩形态沿 seal_polarity_test 状态袋先例(手搭 settlement——引用步骤不复制代码)。 +# 运行:godot --headless --path game --script res://tests/unit/core/battle_driver_drive_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const BattleDriver := preload("res://sim/battle_driver.gd") + +const Vector2iArray := [] + + +class _StubEcon: + var phase := "Active" + + +class _StubMS: + var log: Array = [] + var accept := true + var reject_cells := {} + var on_submit: Callable = Callable() # 提交后钩子(④ 相位变更注入) + + func settle_batch(actions: Array) -> Array: + var out: Array = [] + for a in actions: + var cell: Vector2i = a.get("cell", Vector2i(-1, -1)) + var ok := accept and not reject_cells.has(cell) + log.append({"action": String(a.get("action", "")), "cell": cell, "ok": ok}) + out.append({"accepted": ok, "outcome": {}, "reject_reason": "" if ok else "stub_reject"}) + if on_submit.is_valid(): + on_submit.call(String(a.get("action", ""))) + return out + + +class _StubSettlement: + var round_index := 1 + var battle_result := "" + var action_economy := _StubEcon.new() + var minesweeper := _StubMS.new() + var refresh_calls := 0 + + func player_request_refresh() -> bool: + refresh_calls += 1 + return true + + +func _init() -> void: + _smoke_drive_round_contract() + quit(Kit.finish("battle_driver_drive_test")) + + +# ① 缺省两参(真编排器):同种子两跑整轮驱动逐位(确定性自证——动作序列快照经 cell_revealed 事件流) +func _smoke_drive_round_contract() -> void: + var db = load("res://core/config_db.gd").new() + var load_ok: bool = db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0 + Kit.check("前置:表加载零 error", load_ok) + if not load_ok: + return + var sigs: Array = [] + for run in range(2): + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(777) + var settlement = load("res://core/battle_orchestrator.gd").new(db, rng) + if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)}): + Kit.check("前置:开局受理(run %d)" % run, false) + return + var moves: Array = [] + settlement.minesweeper.cell_revealed.connect(func(evt): moves.append(str(evt.get("cell", "")) + ":" + String(evt.get("kind", "")))) + BattleDriver.drive_round(settlement) + sigs.append(str(moves)) + Kit.check("缺省两参:真编排器同种子两跑事件流逐位(驱动语义确定性自证)〔架构整改 V13〕", sigs[0] == sigs[1] and String(sigs[0]) != "") + _provider_shapes() + _max_rejects() + _combined_step_next() + + +# ② provider 形态:策略回调产出 dig/seal_place/refresh 各一——提交与日志回调四元组在案 +func _provider_shapes() -> void: + var st := _StubSettlement.new() + var script := [ {"kind": "dig", "cell": Vector2i(1, 1)}, {"kind": "seal", "cell": Vector2i(2, 2)}, {"kind": "refresh"}, {"kind": "stop"} ] + var logs: Array = [] + var provider := func(_s) -> Dictionary: + return script.pop_front() if script.size() > 0 else {"kind": "stop"} + var logger := func(kind: String, cell: Vector2i, ok: bool, ri: int) -> void: + logs.append({"kind": kind, "cell": cell, "ok": ok, "ri": ri}) + BattleDriver.drive_round(st, Callable(), provider, logger, 0) + Kit.check("provider 形态:dig 提交在案(桩日志 action=dig cell=(1,1))〔架构整改 V13〕", + logs.size() >= 1 and String(logs[0]["kind"]) == "dig" and logs[0]["cell"] == Vector2i(1, 1) and bool(logs[0]["ok"])) + Kit.check("provider 形态:seal 提交动作名 seal_place(提交分派换名)〔架构整改 V13〕", + logs.size() >= 2 and String(logs[1]["kind"]) == "seal_place" and logs[1]["cell"] == Vector2i(2, 2)) + Kit.check("provider 形态:refresh 经 player_request_refresh(计数面外——桩 refresh_calls=1)+回调四元组 round_index 同轮〔架构整改 V13〕", + st.refresh_calls == 1 and logs.size() >= 3 and String(logs[2]["kind"]) == "refresh" and int(logs[2]["ri"]) == 1) + Kit.check("provider 形态:轮末 stop 统一回调(submit_kind=stop)〔架构整改 V13〕", + logs.size() >= 4 and String(logs[logs.size() - 1]["kind"]) == "stop") + + +# ③ max_rejects:策略恒返被拒动作——第 3 拒后停手出环(循环次数=3) +func _max_rejects() -> void: + var st := _StubSettlement.new() + st.minesweeper.accept = false + st.minesweeper.reject_cells = {Vector2i(9, 9): true} + var provider := func(_s) -> Dictionary: + return {"kind": "dig", "cell": Vector2i(9, 9)} + var logs: Array = [] + var logger := func(kind: String, _cell: Vector2i, _ok: bool, _ri: int) -> void: + logs.append(kind) + BattleDriver.drive_round(st, Callable(), provider, logger, 3) + var dig_rejects := 0 + for k in logs: + if String(k) == "dig": + dig_rejects += 1 + Kit.check("max_rejects=3:第 3 拒后停手出环(被拒 dig 日志恰 3 行——轮末 stop 另行在案)〔架构整改 V13〕", dig_rejects == 3) + Kit.check("max_rejects=3:轮末 stop 回调在案〔架构整改 V13〕", logs.size() >= 1 and String(logs[logs.size() - 1]) == "stop") + + +# ④ 合步 next:move 后相位变更即断弃 dig(桩 on_submit 钩子把 phase 推离 Active) +func _combined_step_next() -> void: + var st := _StubSettlement.new() + st.minesweeper.on_submit = func(action: String) -> void: + if action == "move": + st.action_economy.phase = "Settling" # 移位触发结算相位变更 + var provider := func(_s) -> Dictionary: + return {"kind": "move", "cell": Vector2i(3, 3), "next": {"kind": "dig", "cell": Vector2i(3, 4)}} + BattleDriver.drive_round(st, Callable(), provider, Callable(), 0) + var dig_count := 0 + for entry in st.minesweeper.log: + if String(entry["action"]) == "dig": + dig_count += 1 + Kit.check("合步 next:move 后相位变更断弃 dig(提交日志零 dig——与原内联逐位同)〔架构整改 V13〕", dig_count == 0) + # 对照:相位不变时合步 dig 照常提交 + var st2 := _StubSettlement.new() + var fired := {"once": false} + var provider2 := func(_s) -> Dictionary: + if bool(fired["once"]): + return {"kind": "stop"} + fired["once"] = true + return {"kind": "move", "cell": Vector2i(3, 3), "next": {"kind": "dig", "cell": Vector2i(3, 4)}} + BattleDriver.drive_round(st2, Callable(), provider2, Callable(), 0) + var dig2 := 0 + var move2 := 0 + for entry in st2.minesweeper.log: + if String(entry["action"]) == "dig": + dig2 += 1 + if String(entry["action"]) == "move": + move2 += 1 + Kit.check("合步 next:相位不变时 dig 照常提交(move+dig 两行)〔架构整改 V13〕", dig2 == 1 and move2 == 1) diff --git a/game/tests/unit/core/battle_driver_drive_test.gd.uid b/game/tests/unit/core/battle_driver_drive_test.gd.uid new file mode 100644 index 00000000..f02efce4 --- /dev/null +++ b/game/tests/unit/core/battle_driver_drive_test.gd.uid @@ -0,0 +1 @@ +uid://dj6k6mmyk26pc diff --git a/game/tests/unit/core/board_gen_reward_test.gd b/game/tests/unit/core/board_gen_reward_test.gd index 441314fc..e7dd0c4f 100644 --- a/game/tests/unit/core/board_gen_reward_test.gd +++ b/game/tests/unit/core/board_gen_reward_test.gd @@ -130,6 +130,7 @@ func _smoke_reward_identity_unit() -> void: class _BoardRowStub: class _StubBoardGen: var row: Dictionary + var spawn_edge: int = 3 # 表头成员(generate 直索引面——现行边长 3) func resolve_row(_L: int, _D: int) -> Dictionary: return row class _StubCombat: @@ -200,22 +201,23 @@ func _smoke_board_generator() -> void: # ③ ring_removed 量级:≤ 域内候选危险格数(5×5−出生 9=16——无全域剃雷); # ④ 闸压线行为:cap=9(出生保障面下限)时出图必 r0≤9 或 accept_worst 非法标记(三分出口诚实降级,玩家永不卡死) var cap_val: int = int(row.get("r0_max_cap", 0)) - Kit.check("L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 9——板面 30%% 锚)" % [L, cap_val], - cap_val >= BoardData.SPAWN_AREA_CELLS) + var spawn_cells_n: int = int(_db.combat.board_gen.spawn_edge) * int(_db.combat.board_gen.spawn_edge) + Kit.check("L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 %d——板面 30%% 锚)" % [L, cap_val, spawn_cells_n], + cap_val >= spawn_cells_n) Kit.check("L%d 结构断言 R₀=%d ≤ cap=%d(R₀≤cap 重掷闸承载)" % [L, int(b1.metrics.r0_count), cap_val], int(b1.metrics.r0_count) <= cap_val) Kit.check("L%d r0_cap_rejected 尝试事实指标在位(本图 %d 次触发)" % [L, int(b1.metrics.get("r0_cap_rejected", -1))], int(b1.metrics.get("r0_cap_rejected", -1)) >= 0) - Kit.check("L%d ring_removed=%d ≤ 剃雷域候选危险格数((3+2r)²−9——全域剃雷退役)" % [L, int(b1.metrics.get("ring_removed", 99))], - int(b1.metrics.get("ring_removed", 99)) <= (2 + int(row.get("shave_ring_radius", 1))) * (2 + int(row.get("shave_ring_radius", 1))) * 4 - 5) + Kit.check("L%d ring_removed=%d ≤ 剃雷域候选危险格数((edge+2r)²−edge²——全域剃雷退役)" % [L, int(b1.metrics.get("ring_removed", 99))], + int(b1.metrics.get("ring_removed", 99)) <= (int(_db.combat.board_gen.spawn_edge) + 2 * int(row.get("shave_ring_radius", 1))) * (int(_db.combat.board_gen.spawn_edge) + 2 * int(row.get("shave_ring_radius", 1))) - int(_db.combat.board_gen.spawn_edge) * int(_db.combat.board_gen.spawn_edge)) var row9: Dictionary = row.duplicate() - row9["r0_max_cap"] = BoardData.SPAWN_AREA_CELLS + row9["r0_max_cap"] = int(_db.combat.board_gen.spawn_edge) * int(_db.combat.board_gen.spawn_edge) # cap 压线=出生保障面(spawn_edge² 推导——语义 9 不减) row9["max_gen_attempts"] = 4 var stub9 := _BoardRowStub.new() stub9.combat.board_gen.row = row9 var b9 = board_generator.generate(1, 0, _mk_stream("board_gen", 909), stub9) - Kit.check("L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" % [int(b9.metrics.r0_count), "≤9" if int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS else ">9→illegal_product"], - b9 != null and (int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS or b9.illegal_product)) + Kit.check("L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" % [int(b9.metrics.r0_count), "≤9" if int(b9.metrics.r0_count) <= spawn_cells_n else ">9→illegal_product"], + b9 != null and (int(b9.metrics.r0_count) <= spawn_cells_n or b9.illegal_product)) Kit.check("L1 cap 压线出图指标集完整(accept_mode=%s / 闸触发 %d 次)" % [String(b9.metrics.get("accept_mode", "")), int(b9.metrics.get("r0_cap_rejected", 0))], int(b9.metrics.get("r0_cap_rejected", 0)) >= 0 and String(b9.metrics.get("accept_mode", "")) != "") diff --git a/game/tests/unit/core/board_generator_test.gd b/game/tests/unit/core/board_generator_test.gd index e4340644..4acd338d 100644 --- a/game/tests/unit/core/board_generator_test.gd +++ b/game/tests/unit/core/board_generator_test.gd @@ -6,6 +6,7 @@ extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") const BoardGeneratorScript := preload("res://core/board_generator.gd") +const BoardDataScript := preload("res://core/board_data.gd") const RngServiceScript := preload("res://core/rng_service.gd") var _db: ConfigDb @@ -19,6 +20,7 @@ func _init() -> void: return _test_generate_shape_and_mines() _test_target_mine_count_single_source() + _test_spawn_edge_from_table() _test_digits_equal_neighbor_mine_count() _test_same_seed_bit_identical() _test_clearable_and_inferability_selfconsistent() @@ -66,9 +68,11 @@ func _test_target_mine_count_single_source() -> void: var params := { "w": int(row.board_width), "h": int(row.board_height), "density": float(row.mine_density), + "spawn_edge": int(_db.combat.board_gen.spawn_edge), } var from_fn: int = BoardGeneratorScript.target_mine_count(params) - var from_expr: int = int(round(float(params.density) * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS))) + var edge: int = int(params.spawn_edge) + var from_expr: int = int(round(float(params.density) * float(params.w * params.h - edge * edge))) checked += 1 if from_fn != from_expr: bad += 1 @@ -80,7 +84,8 @@ func _test_target_mine_count_single_source() -> void: var row1: Dictionary = _db.combat.board_gen.resolve_row(1, 0) Kit.check("产物断言读数 = target_mine_count 同源(metrics.target_mines)〔wd14〕", int(b.metrics.get("target_mines", -1)) == BoardGeneratorScript.target_mine_count({ - "w": int(row1.board_width), "h": int(row1.board_height), "density": float(row1.mine_density)})) + "w": int(row1.board_width), "h": int(row1.board_height), "density": float(row1.mine_density), + "spawn_edge": int(_db.combat.board_gen.spawn_edge)})) func _test_digits_equal_neighbor_mine_count() -> void: @@ -124,3 +129,36 @@ func _test_clearable_and_inferability_selfconsistent() -> void: var safe_total := b.width * b.height - b.mines.size() Kit.check("清图判定与安全格总数同在(安全格 %d 个,可清=%s)" % [safe_total, str(clearable)], safe_total > 0) + + +# ---- V8 出生区边长表单点(架构整改——spawn_edge 表头驱动布雷域基/几何/护栏全导出)---- + +func _test_spawn_edge_from_table() -> void: + # ① 纯函数对拍:edge=5 夹具 → target = round(d×(W×H−25))(25 格布雷域基可观测) + var p5 := {"w": 12, "h": 12, "density": 0.15, "spawn_edge": 5} + Kit.check("V8 纯函数:edge=5 布雷域基 25 格(target=round(d×(144−25)))〔架构整改 V8〕", + BoardGeneratorScript.target_mine_count(p5) == int(round(0.15 * (144 - 25)))) + # ② BoardData 几何:spawn_cells() 按 spawn_edge 导出(5→25 / 3→9) + var b5 = BoardDataScript.new() + b5.width = 12 + b5.height = 12 + b5.spawn_center = Vector2i(5, 5) + b5.spawn_edge = 5 + Kit.check("V8 几何:edge=5 → spawn_cells 25 格(in_bounds 过滤前满方)〔架构整改 V8〕", b5.spawn_cells().size() == 25) + var b3 = BoardDataScript.new() + b3.width = 12 + b3.height = 12 + b3.spawn_center = Vector2i(5, 5) + b3.spawn_edge = 3 + Kit.check("V8 几何:edge=3 → spawn_cells 9 格(与退役常量同值——逐位回归)〔架构整改 V8〕", b3.spawn_cells().size() == 9) + # ③ 真路径回归:真表头 spawn_edge=3 驱动出图(spawn_cells 9 格——生成器注入面) + var b := _generate(1, 0, 74000) + Kit.check("V8 真路径:真表头 edge=3 出图 spawn_cells 9 格〔架构整改 V8〕", b != null and b.spawn_cells().size() == 9) + # ④ 表头域校验:edge=5 时 r0_max_cap=9 低于出生保障面 25 → validate 拒(护栏读表头) + var tbl = load("res://data/schemas/board_gen_table.gd").new() + tbl.spawn_edge = 5 + var probe_row := {"level_index": 1, "node_depth": 0, "board_width": 12, "board_height": 12, "mine_density": 0.15, + "inferability_target": 0.95, "solver_rules": "single_subset", "min_safe_count": 3, "max_gen_attempts": 8, + "board_type": "dungeon", "shave_ring_radius": 1, "r0_max_cap": 9} + tbl.rows.append(probe_row) # typed Array[Dictionary]——append 面 + Kit.check("V8 护栏:edge=5 表头下 r0_cap=9 低于出生保障面 25 被拒〔架构整改 V8〕", not tbl.validate().is_empty()) diff --git a/game/tests/unit/core/effect_words_t1_test.gd b/game/tests/unit/core/effect_words_t1_test.gd index 12a17964..0b1a196b 100644 --- a/game/tests/unit/core/effect_words_t1_test.gd +++ b/game/tests/unit/core/effect_words_t1_test.gd @@ -59,7 +59,7 @@ func _init() -> void: var fx = EffectEngineScript.new(null) var ctx_f := EffectContext.new() var hits: Array = [] - fx.rebuild([], [], []) + fx.rebuild([], [], {}) fx._push_trigger("test", "v1_row", {"on": "dig_stop", "when": [{"fact": "ap_left", "op": "gte", "value": 3}], "do": {"kind": "counter_add", "counter_id": "stop_swap", "value_event": "ap_left"}}, {}) @@ -73,7 +73,7 @@ func _init() -> void: # ⑤ mod_eq 算子(双管镐落法——预评估口径每第 2 次判定;battle 桶无前缀) var fx2 = EffectEngineScript.new(null) var ctx2 := EffectContext.new() - fx2.rebuild([], [], []) + fx2.rebuild([], [], {}) fx2._push_trigger("test", "mod_row", {"on": "dig_paid", "when": [{"fact": "counter", "id": "dbl_digs", "op": "mod_eq", "value": 2, "ref": 1}], "do": {"kind": "grant_ap", "value": 1}}, {}) @@ -108,7 +108,7 @@ func _init() -> void: # ⑦ V4——前缀归桶清零/消耗语义(round_ 桶随 round_start 整桶清空;battle 无前缀计数跨 round_start 存活) var fx5 = EffectEngineScript.new(null) var ctx5 := EffectContext.new() - fx5.rebuild([], [], []) + fx5.rebuild([], [], {}) fx5._push_trigger("test", "round_row", {"on": "mine_hit", "do": {"kind": "counter_add", "counter_id": "round_mines", "value": 1}}, {}) for h in fx5.dispatch(ctx5, "mine_hit", {}): @@ -126,7 +126,7 @@ func _init() -> void: # battle 桶对照:无前缀 helm_layers 不受 round_start 影响(跨 round_start 存活) var fx6 = EffectEngineScript.new(null) var ctx6 := EffectContext.new() - fx6.rebuild([], [], []) + fx6.rebuild([], [], {}) fx6._push_trigger("test", "battle_row", {"on": "mine_hit", "do": {"kind": "counter_add", "counter_id": "helm_layers", "value": 1}}, {}) for h in fx6.dispatch(ctx6, "mine_hit", {}): diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index c17b8881..04839ab6 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -165,7 +165,7 @@ func _init() -> void: var orch = RigScript.new(db, rig_rng) Kit.check_quiet(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": ["perfect_insurance"], "tree_rules": ["sense_guard"], "battle_index": 1, + "feats": ["perfect_insurance"], "tree_rules": {"sense_guard": {"ap_refund": 1, "cap_per_level": 2}}, "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}), "保险 rig:战斗启动(feats=perfect_insurance 与 tree_rules=sense_guard 双订阅 seal_missed)〔卡② R2〕") @@ -215,7 +215,7 @@ func _init() -> void: var orch2 = RigScript.new(db, rig_rng2) Kit.check_quiet(orch2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": ["perfect_insurance"], "tree_rules": [], "battle_index": 2, + "feats": ["perfect_insurance"], "tree_rules": {}, "battle_index": 2, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}), "次战 rig:第二战编排启动(feats 同件)〔卡② R2〕") @@ -240,7 +240,7 @@ func _init() -> void: o10.step_row.connect(func(row): rows10.append(row)) Kit.check_quiet(o10.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": [], "tree_rules": [], "battle_index": 1, + "feats": [], "tree_rules": {}, "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}), "A10 rig:无保险战斗启动〔卡① N7〕") @@ -267,7 +267,7 @@ func _init() -> void: o11.step_row.connect(func(row): rows11.append(row)) Kit.check_quiet(o11.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": ["perfect_insurance"], "tree_rules": [], "battle_index": 1, + "feats": ["perfect_insurance"], "tree_rules": {}, "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}), "A11 rig:带保险战斗启动〔卡① N7〕") diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index 45069651..2fcac170 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -251,6 +251,8 @@ func _init() -> void: _smoke_shop_graph() _smoke_multi_terminal_settlement() _smoke_shop_pool() + _test_shelf_size_from_table() + _test_spend_discount_from_rule_key() _test_shop_node_entered_payload() _smoke_generated_coverage() quit(Kit.finish("level_shop_economy_test")) @@ -261,7 +263,6 @@ func _init() -> void: func _smoke_table_migration() -> void: print("--- 数值入表(shop 两 knob + hero 显示名 · 原值迁移)---") Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.run.shop.relic_base_price == 15.0) - Kit.check("shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕", _db.run.shop.spend_discount == 0.8) Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.combat.hero.display_name == "探险者") # 消费端读表:spawn 真实构造链的英雄单位显示名与表值同 var units = load("res://core/unit_container.gd").new(_db) @@ -1554,3 +1555,42 @@ func _has_err_category(errs: Array, category: String) -> bool: # ⑦ 实装探针(妨害技蛛网束缚 + 毒爆必中附加分量,实装面 = L2 Boss 卓尔): # 停手轮驱动 (2,9)——零挖掘零封印 → 零池,伤害行可逐拍精确复算;种子 hunt 至少各命一粒毒爆伤害行 # 与蛛网束缚施放行;毒分量随开战掷定的攻击基线缩放(atk 区间 2–4 → rider 1–3)。 + + +# ---- 货架位数读表(架构整改——shelf_size 表头单点:进店与重抽同源)---- + +func _test_shelf_size_from_table() -> void: + var autoplay = load("res://sim/level_autoplay.gd") + var st = _db.run.shop + var saved: int = int(st.shelf_size) + st.shelf_size = 5 # 运行时表头改值夹具(测毕还原——真表行值零触碰) + var got5 := -1 + for s in range(42, 92): + var a9 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s)) + a9.set_hero_override(900, 400) + if not a9.start_adventure(s, {"shop_mode": "pool"}): + continue + if not _adv_drive_to_shop(a9, _adv_st(), autoplay): + continue + got5 = (a9.runner().shop.shop_shelf() as Array).size() + break + st.shelf_size = saved + Kit.check("夹具 shelf_size=5 → 货架五件可观测(实测 %d;真表值 3 回归=既有「进店抽 3」断言承载)〔架构整改 V9〕" % got5, got5 == 5) + + +# ---- 散折价收回规则键(架构整改 R2——shop.spend_discount 表字段退役,折价读 economy_spend.discount_pct)---- + +func _test_spend_discount_from_rule_key() -> void: + # 字段真删:shop.tres 加载零诊断 + spend_discount 属性不在位 + Kit.check("shop.tres 加载零诊断(spend_discount 字段真删——schema 与表行同卸)〔架构整改 V2〕", + _db.diagnostics_count("error") == 0 and _db.run.shop.get("spend_discount") == null) + # 80% 折价值逐位同(真实入口=LevelRunner 装配链 shop_session.current_price,economy_spend 注入) + var bonus: int = int(_db.run.shop.price_level_bonus_at(0)) + var spend_dict: Dictionary = {"economy_spend": {"discount_pct": 80}} + var lr_on = load("res://core/level_runner.gd").new(_db, _fresh_rng(777005), {"tree_rules": spend_dict}) + var lr_off = load("res://core/level_runner.gd").new(_db, _fresh_rng(777006), {"tree_rules": {}}) + var p_on: int = int(lr_on.shop.current_price({"base_price": 100, "price_increment": 0})) + var p_off: int = int(lr_off.shop.current_price({"base_price": 100, "price_increment": 0})) + var expect_on: int = maxi(1, int(floor(float(100 + bonus) * float(spend_dict["economy_spend"]["discount_pct"]) / 100.0))) + Kit.check("散激活:折后价 = (基价+关卡加成)×discount_pct/100 floor(%d vs 期望 %d)〔架构整改 V2〕" % [p_on, expect_on], p_on == expect_on) + Kit.check("散未激活:原价(无 economy_spend 键——%d)〔架构整改 V2〕" % p_off, p_off == 100 + bonus) diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index 0a3f5373..7048c521 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -37,6 +37,10 @@ func _init() -> void: _smoke_forge_axes() _smoke_tree() _smoke_tree_rules() + _test_forge_buy_reject() + _test_blessing_no_forge_leak() + _test_default_axes_pre_key() + _test_armor_switch_dual_axes() quit(Kit.finish("meta_progression_forge_test")) @@ -50,7 +54,8 @@ const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry "user://smoke_meta_tree.json", "user://smoke_meta_career.json", "user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json", "user://smoke_meta_treasure_blank.json", "user://smoke_meta_forge.json", "user://smoke_meta_forge2.json", "user://smoke_meta_forge3.json", - "user://smoke_meta_forge4.json", "user://smoke_meta_forge5.json"] + "user://smoke_meta_forge4.json", "user://smoke_meta_forge5.json", + "user://smoke_meta_reject.json", "user://smoke_meta_blessing.json", "user://smoke_meta_axesw.json"] func _meta_smoke_cleanup() -> void: @@ -309,7 +314,7 @@ func _meta_settle_and_workshop() -> void: sv_e.meta_state.set_attune_refresh_level(2) sv_e.meta_state.set_fork_side("fg_posture", "a") var mp_e = load("res://core/meta_progression.gd").new(_db, sv_e) - var rules_e: Array = mp_e.tree_rule_keys() + var rules_e: Dictionary[Variant, Variant] = mp_e.tree_rules() Kit.check("开局传参夹具前置:Fork 侧 a 激活 rule 键非空 + Y=Lv2 行表值 2(防断言空转)", rules_e.has("posture_first_strike") and mp_e.attune_refresh_charges() == 2) var p_new: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e, @@ -319,7 +324,7 @@ func _meta_settle_and_workshop() -> void: Kit.check("主菜单开新冒险参数面:Y 刷新池 = 当前等级行表值(营地升几档开局就给几次)", int(p_new.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges()) Kit.check("主菜单开新冒险参数面:树 rule 键集 = Fork 激活侧 effects(点哪侧带哪侧)", - (p_new.get("tree_rules", []) as Array) == rules_e) + (p_new.get("tree_rules", {}) as Dictionary) == rules_e) Kit.check("主菜单开新冒险参数面:入口特有键(treasure_unlocked/shop_mode)随 extra 原样并入", bool((p_new.get("treasure_unlocked", {}) as Dictionary).get("wide_scout_band", false)) and String(p_new.get("shop_mode", "")) == "pool") var p_resume: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e, @@ -327,7 +332,7 @@ func _meta_settle_and_workshop() -> void: Kit.check("主菜单继续上局参数面:三键与开新冒险同源同值(续玩不吃白板开局)", (p_resume.get("attunement", []) as Array) == (sv_e.meta_state.attune_state().equipped as Array) and int(p_resume.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges() - and (p_resume.get("tree_rules", []) as Array) == rules_e) + and (p_resume.get("tree_rules", {}) as Dictionary) == rules_e) # 续玩路径 tree_rules 受理断言:直呼 runner 续玩参数面 # (v2 旧断点夹具同式最小断点——smoke ⑫ 受理路径同形)验 _tree_rules 与传入一致。 var adr_res = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(19)) @@ -339,7 +344,7 @@ func _meta_settle_and_workshop() -> void: adr_res.resume_adventure(bp_min, p_resume)) Kit.check("继续上局受理面:tree_rules 随参数面进 runner._tree_rules(消费面 set_tree_rules 下传现成)", adr_res.runner() != null and adr_res.runner().pick_next_node(int(adr_res.runner().next_candidate_ids()[0])) - and ((adr_res.runner().current_battle()._facts.encounter.get("tree_rules", []) as Array) == rules_e)) + and ((adr_res.runner().current_battle()._facts.encounter.get("tree_rules", {}) as Dictionary) == rules_e)) _meta_smoke_cleanup() @@ -418,6 +423,12 @@ func _smoke_forge_axes() -> void: Kit.check("长剑二级购:扣 30、等级→2;轻甲独立零扰动(六路线各练各的)〔锻造工坊批 V2〕", mp2.forge_buy_level("sword") and sv2.meta_state.forge_state().levels.get("sword", 0) == 2 and sv2.meta_state.keystone_balance() == 50 and sv2.meta_state.forge_state().levels.get("light", 0) == 0) + # V12:长剑购买链走至 3 级(45 档扣款——余额 50−45=5,几何价第三档);链后续夹具段余额基准 + # 由 add_keystone 补回(注入口径——直写存档域构造状态,下游 V3 段 50−20 语义不变) + Kit.check("长剑三级购:扣 45、等级→3(余额 50−45=5)〔断言补齐批 V12〕", + mp2.forge_buy_level("sword") and sv2.meta_state.forge_state().levels.get("sword", 0) == 3 + and sv2.meta_state.keystone_balance() == 5) + sv2.meta_state._add_keystone(45) # V2③:余额不足拒且零副作用(等级/选中/解锁三面零变化) var sv2d = _meta_new_save(_META_SMOKE_PATHS[14]) var mp2d = load("res://core/meta_progression.gd").new(_db, sv2d) @@ -605,10 +616,10 @@ func _smoke_tree() -> void: Kit.check("零币切态 b(购买解锁后 A/B 可切)", mp.tree_toggle_fork("fg_posture", "b") and sv.meta_state.tree_state().fork_sides.get("fg_posture", "") == "b") Kit.check("切态零消费(钥石余额差分不动)", int(sv.meta_state.keystone_balance()) == bal_pre_toggle) - Kit.check("规则键投影=激活态效果键(B 态稳健拓荒——tree_rule_keys 随切态换键集)", - (mp.tree_rule_keys() as Array).has("posture_bulwark") and not (mp.tree_rule_keys() as Array).has("posture_first_strike")) + Kit.check("规则键投影=激活态效果键(B 态稳健拓荒——tree_rules 随切态换键集)", + (mp.tree_rules() as Dictionary).has("posture_bulwark") and not (mp.tree_rules() as Dictionary).has("posture_first_strike")) Kit.check("切回 A 态键集随换(六键词表在册——V11 校验面归单元段)", - mp.tree_toggle_fork("fg_posture", "a") and (mp.tree_rule_keys() as Array).has("posture_first_strike")) + mp.tree_toggle_fork("fg_posture", "a") and (mp.tree_rules() as Dictionary).has("posture_first_strike")) # 彩蛋特化觉醒(V8) Kit.check("彩蛋不经购买(手动购亮=红——觉醒唯一入口=特化判定)", not mp.tree_can_buy("easter_ley")) Kit.check("觉醒前置未满足不亮(两线未拉满)", sv.meta_state.tree_state().nodes.get("easter_ley", 0) == 0) @@ -629,7 +640,7 @@ func _smoke_tree_rules() -> void: print("--- 天赋树六键行为(V10:窗/限次/聚散/守侧)---") # 开局姿态组:每关首轮窗(battle_index 事实——引擎直构 + 自建 ctx) var fx = load("res://core/effect_engine.gd").new(null) - fx.rebuild([], [], ["posture_first_strike", "posture_bulwark"]) + fx.rebuild([], [], {"posture_first_strike": {"ap_bonus": 1}, "posture_bulwark": {"shield": 1}}) var ctx_p := EffectContext.new() Kit.check("先手压制:每关首战首轮窗命中(battle_index=1 且 round=1 → grant_ap queue)〔天赋树批 V10〕", (fx.dispatch(ctx_p, "round_start", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1) @@ -641,7 +652,7 @@ func _smoke_tree_rules() -> void: and (fx.dispatch(ctx_p, "damage_taken", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty()) # 危险直觉·锋:限次闸(引擎直构——mine_hit 挂点现成;限次计数住 level 桶,跨战持久=共享同一字典引用) var fz = load("res://core/effect_engine.gd").new(null) - fz.rebuild([], [], ["sense_sharp"]) + fz.rebuild([], [], {"sense_sharp": {"halve_pct": 50, "cap_per_level": 2}}) var lvl_shared := {} # 同一关的 level 桶(两编排器共享引用 = 跨战持久,替代旧副本搬运) var ctx_fz := EffectContext.new() ctx_fz.level_bucket = lvl_shared @@ -658,7 +669,7 @@ func _smoke_tree_rules() -> void: Kit.check("锋:限次耗尽全额(counter 2 不<2 闸关——每关限次 provisional 2)〔无状态化 V5〕", (fz.dispatch(ctx_fz, "mine_hit", {}, Callable()) as Array).is_empty()) var fz2 = load("res://core/effect_engine.gd").new(null) - fz2.rebuild([], [], ["sense_sharp"]) + fz2.rebuild([], [], {"sense_sharp": {"halve_pct": 50, "cap_per_level": 2}}) var ctx_fz2 := EffectContext.new() ctx_fz2.level_bucket = lvl_shared # 挂同一 level 桶引用(新形态跨战持久:无 export/seed,同字典即持久) Kit.check("锋:限次跨战持久(level 桶共享引用——替代旧副本搬运链)〔无状态化 V4〕", @@ -668,7 +679,7 @@ func _smoke_tree_rules() -> void: var orch_nc = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777003)) orch_nc.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": [], "tree_rules": [], "battle_index": 1, + "feats": [], "tree_rules": {}, "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}) # 真实战斗上下文(状态容器开域——dispatch 链受理) var nc_rows: Array = [] # step_row 采集盒(lambda 捕引用容器——捕获变量赋值不出闭包) @@ -692,7 +703,7 @@ func _smoke_tree_rules() -> void: Kit.check("守侧 rig:战斗编排启动(tree_rules=sense_guard 携带)", orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": [], "tree_rules": ["sense_guard"], "battle_index": 1, + "feats": [], "tree_rules": {"sense_guard": {"ap_refund": 1, "cap_per_level": 2}}, "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"})) var board = orch.minesweeper.current_board() @@ -722,7 +733,7 @@ func _smoke_tree_rules() -> void: Kit.check("守⑤ rig:次战编排启动(新战新队列实例)", orch2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": [], "tree_rules": ["sense_guard"], "battle_index": 1, + "feats": [], "tree_rules": {"sense_guard": {"ap_refund": 1, "cap_per_level": 2}}, "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"})) Kit.check("守⑤:末轮判出不返(queue 只在 notify_round_start 兑现、战亡即弃——无跨战泄漏)", @@ -730,33 +741,103 @@ func _smoke_tree_rules() -> void: # 树限次按关清零改由 LevelRunner.level_counters 随 new 承载(旧硬编码名单冒烟断言随函数删除——换代同生死;新语义由 V4/V5 段承载) # 财富之道·散:商品标价折价(level_runner 计价单点) var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004), - {"tree_rules": []}) # 构造注入(拆件批 T3——setter 群退役) + {"tree_rules": {}}) # 构造注入(拆件批 T3——setter 群退役) Kit.check("散未激活:商品标价原价(无 economy_spend 键)", int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0))) + var spend_dict: Dictionary = {"economy_spend": {"discount_pct": 80}} lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777005), - {"tree_rules": ["economy_spend"]}) # 构造注入(拆件批 T3——setter 群退役) + {"tree_rules": spend_dict}) # 构造注入(拆件批 T3——setter 群退役) Kit.check("散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)", - int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount)))) - and int(lr.shop.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount))))) + int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(spend_dict["economy_spend"]["discount_pct"]) / 100.0))) + and int(lr.shop.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(spend_dict["economy_spend"]["discount_pct"]) / 100.0)))) # 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×) var orch3 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005)) Kit.check("聚 rig:基线编排启动(无增益侧)", orch3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": [], "tree_rules": [], "battle_index": 1, + "feats": [], "tree_rules": {}, "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"})) - orch3._facts.encounter["tree_rules"] = [] + orch3._facts.encounter["tree_rules"] = {} var gp_base: int = orch3._roll_gp_drop() var orch4 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005)) Kit.check("聚 rig:增益编排启动(tree_rules=economy_hoard)", orch4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": [], "tree_rules": ["economy_hoard"], "battle_index": 1, + "feats": [], "tree_rules": {"economy_hoard": {"mult_pct": 150}}, "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"})) - orch4._facts.encounter["tree_rules"] = ["economy_hoard"] + orch4._facts.encounter["tree_rules"] = {"economy_hoard": {"mult_pct": 150}} var gp_hoard: int = orch4._roll_gp_drop() Kit.check("聚:战斗掉落乘算 ×1.5(provisional——同种子配对 %d→%d,入账前=结算框显示实得)", - gp_hoard == int(round(float(gp_base) * 1.5)) and gp_hoard >= gp_base) + gp_hoard == int(round(float(gp_base) * float({"economy_hoard": {"mult_pct": 150}}["economy_hoard"]["mult_pct"]) / 100.0)) and gp_hoard >= gp_base) _meta_smoke_cleanup() + + +# ---- 锻造契约补齐(断言补齐批——⑥锻造工坊):拒购零副作用/祝福件零泄漏/默认值解耦/甲型双轴 ---- + +func _test_forge_buy_reject() -> void: + var sv = _meta_new_save("user://smoke_meta_reject.json") + var mp = load("res://core/meta_progression.gd").new(_db, sv) + sv.meta_state._add_keystone(10) # 低于首购价 20——余额不足面 + var fs0: Dictionary = sv.meta_state.forge_state().duplicate(true) + var bal0: int = sv.meta_state.keystone_balance() + Kit.check("余额不足拒购:sword false、等级与余额零变化〔断言补齐批 V13〕", + not mp.forge_buy_level("sword") and sv.meta_state.forge_state() == fs0 and sv.meta_state.keystone_balance() == bal0) + sv.meta_state._add_keystone(1000) # 充值后未解锁面不变(spear 直购未走/练满未达) + Kit.check("未解锁拒购:spear false、等级与余额零变化〔断言补齐批 V13〕", + not mp.forge_buy_level("spear") and sv.meta_state.forge_state() == fs0 + and sv.meta_state.keystone_balance() == bal0 + 1000) + + +func _test_blessing_no_forge_leak() -> void: + var sv = _meta_new_save("user://smoke_meta_blessing.json") + var mp = load("res://core/meta_progression.gd").new(_db, sv) + var fs0: Dictionary = sv.meta_state.forge_state().duplicate(true) + # 真实入口=商店购买口(LevelRunner 装配的 shop_session.buy_product——祝福件走 GP 钱包+局内加算区) + var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(80)) + var lvl_ok: bool = runner.start_level(1, 80) + Kit.check("V14 前置:关卡启动(L1 行集)", lvl_ok) + if not lvl_ok: + return + runner.gp = 100 + Kit.check("V14 前置:攻击祝福受理", runner.shop.buy_product("atk_up")) + Kit.check("V14 前置:护甲祝福受理", runner.shop.buy_product("armor_up")) + Kit.check("祝福件零泄漏:购买后 forge_state 投影逐位不变(levels/选中/解锁三面)〔断言补齐批 V14〕", + sv.meta_state.forge_state() == fs0) + Kit.check("祝福件住局内加算区(atk +1 在加算日记——通道分离实证)", runner.journal().bonus_total("atk") == 1) + + +func _test_default_axes_pre_key() -> void: + var sv = _meta_new_save("user://smoke_meta_axesw.json") + var mp = load("res://core/meta_progression.gd").new(_db, sv) + var ax: Dictionary = mp.hero_type_axes() + Kit.check("树钥匙未点亮:hero_type_axes 返回锻造配置默认 normal/light(默认值常驻解耦)〔断言补齐批 V15〕", + String(ax["attack"]) == "normal" and String(ax["armor"]) == "light") + + +func _test_armor_switch_dual_axes() -> void: + var sv = _meta_new_save("user://smoke_meta_axesw.json") + sv.meta_state._add_keystone(1000) + var mp = load("res://core/meta_progression.gd").new(_db, sv) + Kit.check("V16 前置:重甲直购解锁+首购+选中(200+20 钥石)", + mp.forge_buy_unlock("heavy") and mp.forge_buy_level("heavy") and mp.forge_select("armor", "heavy")) + var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(81)) + var p: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv, mp) + var adv_ok: bool = adr.start_adventure(81, p) + Kit.check("V16 前置:参数面冒险启动", adv_ok) + if not adv_ok: + return + Kit.check("甲型双轴:heavy 选中 → AC 轴 12 基础+路线 Lv1=13(light 同级 11 的 +2 档差)〔断言补齐批 V16〕", + int(adr.runner().effective_ac()) == 13 and String(mp.hero_type_axes()["armor"]) == "heavy") + Kit.check("甲型双轴:挨普通攻击修正 −1(克制矩阵 normal→heavy)〔断言补齐批 V16〕", + int(_db.combat.settlement.damage_roll.armor_effectiveness["normal"]["heavy"]) == -1) + Kit.check("V16 前置:切回 light(初始持有已解锁可切)", mp.forge_select("armor", "light")) + var adr2 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(82)) + var p2b: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv, mp) + var adv2_ok: bool = adr2.start_adventure(82, p2b) + Kit.check("V16 前置:切回后新参数面冒险启动(快照冻结契约——新 runner 读新选中)", adv2_ok) + if not adv2_ok: + return + Kit.check("甲型双轴:切回 light 复原(AC 10+0)且 heavy 等级保留(Lv1 不丢)〔断言补齐批 V16〕", + int(adr2.runner().effective_ac()) == 10 and sv.meta_state.forge_state().levels.get("heavy", 0) == 1) diff --git a/game/tests/unit/core/minesweeper_rules_test.gd b/game/tests/unit/core/minesweeper_rules_test.gd index 3552ff8d..0adad56d 100644 --- a/game/tests/unit/core/minesweeper_rules_test.gd +++ b/game/tests/unit/core/minesweeper_rules_test.gd @@ -40,6 +40,7 @@ func _init() -> void: _test_reveal_advances() _test_seal_place() _test_reject_out_of_catalog() + _test_preview_reveal_readonly() quit(Kit.finish("minesweeper_rules_test")) @@ -110,3 +111,44 @@ func _test_reject_out_of_catalog() -> void: var o: Dictionary = (out[0] as Dictionary) if not out.is_empty() else {} Kit.check("目录外意图被拒(不静默吞掉)——实际载荷 %s" % str(o).substr(0, 80), out.size() == 1 and String(o.get("outcome", o.get("result", ""))) == "rejected") + + +# ---- V14 棋盘揭示预览只读口(架构整改——preview_reveal 与实挖揭示集同口径、零状态写入零事件)---- + +func _test_preview_reveal_readonly() -> void: + var events := 0 + _rules.cell_revealed.connect(func(_evt): events += 1) # 零发射哨兵(preview 全程不触发揭示事件) + # 形态 A:封印在场——preview(未开格) 不越封印且零状态写入 + var before_revealed := _rules.revealed_cells().size() + var before_seals := _rules.seal_count() + var c_open := _unrevealed_safe() + var prev_a: Array = _rules.preview_reveal(c_open) + Kit.check("V14 形态A:preview 返回集(真板形态——%d 格)" % prev_a.size(), prev_a.size() >= 0) + Kit.check("V14 形态A:调用前后零状态写入(revealed/seal 计数零变化)〔架构整改 V14〕", + _rules.revealed_cells().size() == before_revealed and _rules.seal_count() == before_seals) + # 形态 B:挖封印格——本格封印隐式退役只入模拟集,真实封印计数不动 + var seal_c := _unrevealed_safe() + _rules.settle_batch([{"action": "seal_place", "cell": seal_c}]) + var sealed_before := _rules.seal_count() + var prev_b: Array = _rules.preview_reveal(seal_c) + Kit.check("V14 形态B:preview(封印格) 零状态写入(seal_count 不变——隐式退役只入模拟集)〔架构整改 V14〕", + _rules.seal_count() == sealed_before) + # 同口径对照:preview(c) = settle_batch(dig c) 后 revealed 差集 ∖ {c}(F2——不含首格与已开格) + var dig_c := _unrevealed_safe() + var prev_c: Array = _rules.preview_reveal(dig_c) + var before := {} + for rc in _rules.revealed_cells(): + before[rc] = true + _rules.settle_batch([{"action": "dig", "cell": dig_c}]) + var after_diff := [] + for rc in _rules.revealed_cells(): + if not before.has(rc) and rc != dig_c: + after_diff.append(rc) + Kit.check("V14 同口径:preview(c) = 实挖揭示差集 ∖ {c}(F2——%d vs %d 格逐位)〔架构整改 V14〕" % [prev_c.size(), after_diff.size()], + str(prev_c) == str(after_diff)) + var events_before_previews := events # 三次 preview 调用前快照(此前增量=夹具自身实挖——与 preview 无关) + var _pa: Array = _rules.preview_reveal(_unrevealed_safe()) + var _pb: Array = _rules.preview_reveal(_unrevealed_safe()) + var _pc: Array = _rules.preview_reveal(_unrevealed_safe()) + Kit.check("V14 零事件:preview 全程 cell_revealed 零发射(前后增量 0——实测 %d→%d)〔架构整改 V14〕" % [events_before_previews, events], + events == events_before_previews) diff --git a/game/tests/unit/core/node_map_generator_test.gd b/game/tests/unit/core/node_map_generator_test.gd index 57054f1f..1c1e8acb 100644 --- a/game/tests/unit/core/node_map_generator_test.gd +++ b/game/tests/unit/core/node_map_generator_test.gd @@ -29,6 +29,8 @@ func _init() -> void: _test_shop_and_attribution() _test_battle_floor() _test_exit_structure() + _test_boss_exit_per_region() + _test_gen_params_table_driven() _test_rng_single_point() _test_negative_fixtures() quit(Kit.finish("node_map_generator_test")) @@ -303,6 +305,41 @@ func _terminals_reachable(g: Dictionary, id: int) -> int: return c +# ---- V9 Boss 出场门控(难度曲线与Boss——每区域恰一 Boss 终点)---- + +func _test_boss_exit_per_region() -> void: + # 真实生成多区域图:全部终点 type=="boss"、boss 终点数==exit_count、深度=L−r(r∈[0,exit_count) + # ——每区域恰一 boss、深度互异即分属不同区域) + # 节点 depth 字段=入口起最长路径深(生成器校验轴);区域序号 r 的可观测形态=最深 boss(r=0) + # 与该 boss 的深度差(同层 boss 深度 = d0 − r——区域段 L−r 的 1 步差) + var all_ok := true + for L in [1, 2, 3]: + for s in [42, 43, 44]: + var g := _graph(L, s) + var by_id := {} + for n in g["nodes"]: + by_id[int(n["id"])] = n + var exits := int(_params(L)["exit_count"]) + var d0 := 0 + for tid in g["terminals"]: + d0 = maxi(d0, int(by_id[int(tid)]["depth"])) + var per_region := {} + for tid in g["terminals"]: + var tn: Dictionary = by_id[int(tid)] + if String(tn["type"]) != "boss": + all_ok = false + var r := d0 - int(tn["depth"]) + if r < 0 or r >= exits: + all_ok = false + per_region[r] = int(per_region.get(r, 0)) + 1 + if per_region.size() != exits: + all_ok = false + for r in per_region: + if int(per_region[r]) != 1: + all_ok = false + Kit.check("每区域恰一 Boss 终点(type=boss × exit_count 个、深度=d0−r 互异分域——区域段 L−r 的可观测形态)〔断言补齐批 V9〕", all_ok) + + # ---- V10 RNG 单点(纯流面 counter 不变)---- func _test_rng_single_point() -> void: @@ -441,3 +478,57 @@ func _has_msg(errs: Array, needle: String) -> bool: if String(e["message"]).contains(needle): return true return false + + +# ---- V6 节点图三参数表值驱动(架构整改——fork_third_way_pct/fill_rest_first_pct/width_max 入 level_gen_params)---- + +func _test_gen_params_table_driven() -> void: + # ① 同种子真实表行产图逐位(表值驱动回归锚) + var g1 := _graph(2, 42) + var g2 := _graph(2, 42) + Kit.check("表值驱动:同种子产图逐位复现〔架构整改 V6〕", _sig(g1) == _sig(g2)) + # ② width_max=2 夹具 → 各深度节点数 ≤2(宽度上限读表可观测) + var p2: Dictionary = _params(2).duplicate(true) + p2["width_max"] = 2 + var gw: Dictionary = _gen.generate(p2, 42) + var per_depth := {} + for n in gw["nodes"]: + var d2: int = int(n["depth"]) + per_depth[d2] = int(per_depth.get(d2, 0)) + 1 + var width_ok := true + for d3 in per_depth: + if int(per_depth[d3]) > 2: + width_ok = false + Kit.check("width_max=2 夹具:各深度节点数 ≤2(宽度上限读表可观测)〔架构整改 V6〕", width_ok) + # ③ fork_third_way_pct=0 夹具 → 无节点去路数超 fork_ways_min(三去路概率读表可观测) + var p0: Dictionary = _params(2).duplicate(true) + p0["fork_third_way_pct"] = 0 + var g0: Dictionary = _gen.generate(p0, 42) + var ways_ok := true + for n in g0["nodes"]: + if (n["next"] as Array).size() > int(p0["fork_ways_min"]): + ways_ok = false + Kit.check("fork_third_way_pct=0 夹具:无节点去路数超 fork_ways_min(三去路概率读表可观测)〔架构整改 V6〕", ways_ok) + # ④ fill_rest_first_pct=100 vs 0 → 填充交替起点可观测变化(交替起始骰读表)。 + # 夹具=L1 真实表行改 branch_density=0/battle_floor=0:fill 段只在单出口图跑(e==1——_assign_types)、 + # 岔口修型段会覆盖侧链头填充位——无岔口+零保底组合让填充差异直达产物(多种子扫过 diff 恒真) + var p100: Dictionary = _params(1).duplicate(true) + p100["branch_density"] = 0.0 + p100["battle_floor"] = 0 + p100["fill_rest_first_pct"] = 100 + var g100: Dictionary = _gen.generate(p100, 40) + var p000: Dictionary = _params(1).duplicate(true) + p000["branch_density"] = 0.0 + p000["battle_floor"] = 0 + p000["fill_rest_first_pct"] = 0 + var g000: Dictionary = _gen.generate(p000, 40) + var rest100 := 0 + for n in g100["nodes"]: + if String(n["type"]) == "rest": + rest100 += 1 + var rest000 := 0 + for n in g000["nodes"]: + if String(n["type"]) == "rest": + rest000 += 1 + Kit.check("fill_rest_first_pct=100 vs 0 夹具:填充交替起点产物互异(%d vs %d rest)〔架构整改 V6〕" % [rest100, rest000], + _sig(g100) != _sig(g000) and rest100 > rest000) diff --git a/game/tests/unit/core/node_map_growth_test.gd b/game/tests/unit/core/node_map_growth_test.gd index 86d3227f..3ab25ad9 100644 --- a/game/tests/unit/core/node_map_growth_test.gd +++ b/game/tests/unit/core/node_map_growth_test.gd @@ -66,11 +66,13 @@ func _test_fail_fast_static(gen, db) -> void: and NodeMapGenerator.check_params(db.run.level_gen_params.row_by_level_index(3)).is_empty()) var unsolvable := {"layers": 6, "branch_density": 0.0, "battle_ratio": 0.5, "reward_ratio": 0.2, "rest_ratio": 0.2, "shop_ratio": 0.1, "entry_count": 1, "exit_count": 3, "battle_floor": 4, - "fork_ways_min": 2, "fork_ways_max": 3} + "fork_ways_min": 2, "fork_ways_max": 3, + "fork_third_way_pct": 35, "fill_rest_first_pct": 50, "width_max": 4} Kit.check("层深 6 放三出口无解 → check_params 拦截〔fv4〕", not NodeMapGenerator.check_params(unsolvable).is_empty()) var overfloor := {"layers": 6, "branch_density": 0.0, "battle_ratio": 0.5, "reward_ratio": 0.2, "rest_ratio": 0.2, "shop_ratio": 0.1, "entry_count": 1, "exit_count": 1, "battle_floor": 99, - "fork_ways_min": 2, "fork_ways_max": 3} + "fork_ways_min": 2, "fork_ways_max": 3, + "fork_third_way_pct": 35, "fill_rest_first_pct": 50, "width_max": 4} Kit.check("战斗保底超可放数无解 → check_params 拦截〔fv4〕", not NodeMapGenerator.check_params(overfloor).is_empty()) @@ -96,7 +98,8 @@ func _trigger_fatal() -> void: # 域内合法但深度预算无解:层深 6 放三出口(check_params 拦下 → 致命中止) var unsolvable := {"layers": 6, "branch_density": 0.0, "battle_ratio": 0.5, "reward_ratio": 0.2, "rest_ratio": 0.2, "shop_ratio": 0.1, "entry_count": 1, "exit_count": 3, "battle_floor": 4, - "fork_ways_min": 2, "fork_ways_max": 3} + "fork_ways_min": 2, "fork_ways_max": 3, + "fork_third_way_pct": 35, "fill_rest_first_pct": 50, "width_max": 4} gen.generate(unsolvable, 42) # 致命封印未置位=fail-fast 静默化(缺陷)→ 显式退出码 2 与致命停机码 1 区分 if gen.is_fatal(): diff --git a/game/tests/unit/core/playtest_logger_guard_test.gd b/game/tests/unit/core/playtest_logger_guard_test.gd new file mode 100644 index 00000000..1f1ec9eb --- /dev/null +++ b/game/tests/unit/core/playtest_logger_guard_test.gd @@ -0,0 +1,36 @@ +# tests/unit/core/playtest_logger_guard_test.gd — 试玩日志器 ap_per_round 必填注入守卫〔架构整改 V5〕 +# 断言面 = PlaytestLogger.start_session():未注入(0)返回空串+零文件产出;注入 8 返回非空路径。 +# 真实入口=PlaytestLogger.start_session(R5 必填注入——组合根注入序前置的守卫面)。 +# 运行:godot --headless --path game --script res://tests/unit/core/playtest_logger_guard_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") + + +func _init() -> void: + _smoke_required_injection() + quit(Kit.finish("playtest_logger_guard_test")) + + +func _smoke_required_injection() -> void: + # 未注入(缺省 0):start_session 返回空串、零文件产出 + var lg0 = load("res://presentation/playtest_logger.gd").new() + var p0: String = lg0.start_session(42) + Kit.check("未注入拒绝开会话:start_session 返回空串〔架构整改 V5〕", p0 == "") + var f0 := FileAccess.file_exists(String(lg0._path)) + Kit.check("未注入零文件产出(_path 留空、盘上无会话件)〔架构整改 V5〕", not f0 and String(lg0._path) == "") + # 注入 8:返回非空路径(会话开启) + var lg8 = load("res://presentation/playtest_logger.gd").new() + lg8.ap_per_round = 8 + var p8: String = lg8.start_session(42) + Kit.check("注入 8 返回非空路径(守卫放行——会话开启)〔架构整改 V5〕", p8 != "") + if lg8._file != null: + lg8._file.close() + var f8 := FileAccess.file_exists(p8) + Kit.check("注入后会话件在盘〔架构整改 V5〕", f8) + if f8: + DirAccess.remove_absolute(ProjectSettings.globalize_path(p8)) + # 越域拒绝(33>32 上界) + var lg33 = load("res://presentation/playtest_logger.gd").new() + lg33.ap_per_round = 33 + Kit.check("越域 33 拒绝开会话(守卫上界 [1,32])〔架构整改 V5〕", lg33.start_session(42) == "") diff --git a/game/tests/unit/core/playtest_logger_guard_test.gd.uid b/game/tests/unit/core/playtest_logger_guard_test.gd.uid new file mode 100644 index 00000000..6feabbe6 --- /dev/null +++ b/game/tests/unit/core/playtest_logger_guard_test.gd.uid @@ -0,0 +1 @@ +uid://cyaq5yjgwr0ng diff --git a/game/tests/unit/core/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd index c2b0d23e..6a6c34cf 100644 --- a/game/tests/unit/core/treasure_effects_test.gd +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -318,7 +318,7 @@ func _init() -> void: # enc 带 feats 键+ledger,专长触发经 _judge_seal 真实封印判定管线) var enc_feats = {"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, - "feats": ["seal_smith"], "tree_rules": [], "battle_index": 1, + "feats": ["seal_smith"], "tree_rules": {}, "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle", "adv_bucket": {}} var rng15 = load("res://core/rng_service.gd").new(); rng15.set_master_seed(341) diff --git a/game/tests/unit/core/tree_rules_v11_test.gd b/game/tests/unit/core/tree_rules_v11_test.gd index 8ecd5030..8c791eb7 100644 --- a/game/tests/unit/core/tree_rules_v11_test.gd +++ b/game/tests/unit/core/tree_rules_v11_test.gd @@ -14,34 +14,45 @@ extends SceneTree func _init() -> void: var eng = load("res://core/effect_engine.gd") var tree_tbl = load("res://data/schemas/meta_tree_table.gd") + var db = load("res://core/config_db.gd").new() + db.load_tables("res://data/tables", false) var errs: Array = [] + # 规则键参数字典夹具值源(R1 换代——synth 参数读 meta_tree Fork 行 effects_*,字面量退役) + var fork_params := {} + for row in db.crossrun.meta_tree.rows: + if String(row.get("kind", "")) != "fork": + continue + for skey in ["effects_a", "effects_b"]: + var fx: Dictionary = row.get(skey, {}) as Dictionary + for rk in fx.keys(): + fork_params[String(rk)] = fx[rk] # 四键合成行夹具(与 rebuild 树规则段同构——逐字段对照,实现漂移在此转红) var synth := { "posture_first_strike": [{"on": "round_start", "when": [{"fact": "battle_index", "op": "eq", "value": 1}, {"fact": "round_index", "op": "eq", "value": 1}], - "do": {"kind": "grant_ap", "mode": "queue", "value": 1}}], + "do": {"kind": "grant_ap", "mode": "queue", "value": int(fork_params["posture_first_strike"]["ap_bonus"])}}], "posture_bulwark": [{"on": "damage_taken", "when": [{"fact": "battle_index", "op": "eq", "value": 1}, {"fact": "round_index", "op": "eq", "value": 1}], - "do": {"kind": "shield", "value": 1}}], + "do": {"kind": "shield", "value": int(fork_params["posture_bulwark"]["shield"])}}], "sense_sharp": [{"on": "mine_hit", - "when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}], - "do": {"kind": "damage_mult", "value": 50}}, + "when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": int(fork_params["sense_sharp"]["cap_per_level"])}], + "do": {"kind": "damage_mult", "value": int(fork_params["sense_sharp"]["halve_pct"])}}, {"on": "mine_hit", - "when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}], + "when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": int(fork_params["sense_sharp"]["cap_per_level"])}], "do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_sharp_used", "value": 1}}], "sense_guard": [{"on": "seal_missed", - "when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}], - "do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, + "when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": int(fork_params["sense_guard"]["cap_per_level"])}], + "do": {"kind": "grant_ap", "mode": "queue", "value": int(fork_params["sense_guard"]["ap_refund"])}}, {"on": "seal_missed", - "when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}], + "when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": int(fork_params["sense_guard"]["cap_per_level"])}], "do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_guard_used", "value": 1}}], } var six := ["posture_first_strike", "posture_bulwark", "sense_sharp", "sense_guard", "economy_hoard", "economy_spend"] # rebuild 实装面(db=null→feat/attune 两段空转,_triggers 只含树规则合成行)——行数对照载体 var eng_inst = eng.new() - eng_inst.rebuild([], [], six) + eng_inst.rebuild([], [], fork_params) for key in six: if not tree_tbl.RULE_KEYS.has(key): errs.append("规则键出域 RULE_KEYS 词表: %s" % key) @@ -64,10 +75,58 @@ func _init() -> void: eng.validate_trigger_row("bad_kind", {}, [{"on": "round_start", "do": {"kind": "no_such_kind"}}], bad_errs) if bad_errs.is_empty(): errs.append("反例护栏失效:出域事件/出域效果类型未被 validate_trigger_row 拒绝") + errs.append_array(_test_rule_key_params(eng, tree_tbl, db)) if errs.is_empty(): - print("[tree_rules_v11] all green(四键合成行零违例 + 聚散两键无行成立 + 反例被拒——契约 5 闭集在册词表承载)") + print("[tree_rules_v11] all green(四键合成行零违例 + 聚散两键无行成立 + 反例被拒 + 六键参数域全过——契约 5 闭集在册词表承载)") quit(0) return for e in errs: printerr("[tree_rules_v11] %s" % str(e)) quit(1) + + +# ---- 规则键参数域校验(架构整改批 R1——六键参数字典):正例全过/缺参数拒/多参数拒/越域拒/非字典拒 ---- + +func _test_rule_key_params(eng, tree_tbl, db) -> Array: + var errs: Array = [] + var params_tbl: Dictionary = tree_tbl.RULE_KEY_PARAMS + # 正例:meta_tree.tres 现行 Fork 行逐键过 validate(加载期已验——此处显式复核) + var probe_ok := {"node_id": "probe_rule", "effects": {}, "effects_a": {}, "effects_b": {}} + for row in db.crossrun.meta_tree.rows: + if String(row.get("kind", "")) != "fork": + continue + for skey in ["effects_a", "effects_b"]: + for rk in (row.get(skey, {}) as Dictionary).keys(): + probe_ok[skey][String(rk)] = (row.get(skey, {}) as Dictionary)[rk] + errs.append_array(db.crossrun.meta_tree._validate_rule_key_params(probe_ok, "probe_ok")) + # 负例构造:取 sense_sharp 行(双参数键——缺/多/越域/非字典四形态) + var base_params: Dictionary = {"halve_pct": 50, "cap_per_level": 2} + var mk := func(mod: Dictionary) -> Dictionary: + var row := {"node_id": "probe_neg", "effects_a": {"sense_sharp": base_params.duplicate(true)}, "effects_b": {}} + for k in mod.keys(): + row["effects_a"]["sense_sharp"] = mod[k] + return row + var cases := [ + {"name": "缺参数", "row": {"node_id": "probe_neg", "effects_a": {"sense_sharp": {"halve_pct": 50}}, "effects_b": {}}}, + {"name": "多参数", "row": {"node_id": "probe_neg", "effects_a": {"sense_sharp": {"halve_pct": 50, "cap_per_level": 2, "ghost": 1}}, "effects_b": {}}}, + {"name": "越域", "row": {"node_id": "probe_neg", "effects_a": {"sense_sharp": {"halve_pct": 50, "cap_per_level": 99}}, "effects_b": {}}}, + {"name": "非字典", "row": {"node_id": "probe_neg", "effects_a": {"sense_sharp": 1}, "effects_b": {}}}, + ] + for c in cases: + var got: Array = db.crossrun.meta_tree._validate_rule_key_params(c["row"], "probe_neg") + if got.is_empty(): + errs.append("参数域负例未被拒(%s)" % String(c["name"])) + # 越域判别词在 message 内(断言定位面) + var oor: Array = db.crossrun.meta_tree._validate_rule_key_params(cases[2]["row"], "probe_neg") + var oor_hit := false + for e in oor: + if str(e).contains("越域"): + oor_hit = true + if not oor_hit: + errs.append("越域 message 缺判别词(断言定位面违约)") + # R1 对照表落点:_tree_rule_dict 注入构造面最小断言(sim 单点) + var mc = load("res://sim/adventure_monte_carlo.gd") + var td: Dictionary = mc._tree_rule_dict(db, ["posture_first_strike"]) + if not (td.size() == 1 and (td.get("posture_first_strike", {}) as Dictionary).get("ap_bonus", -1) == 1): + errs.append("_tree_rule_dict 注入构造面失配(应={posture_first_strike:{ap_bonus:1}}): %s" % str(td)) + return errs diff --git a/game/tests/unit/data_config/effect_kind_retired_test.gd b/game/tests/unit/data_config/effect_kind_retired_test.gd new file mode 100644 index 00000000..fe9819cb --- /dev/null +++ b/game/tests/unit/data_config/effect_kind_retired_test.gd @@ -0,0 +1,69 @@ +# 致命类用例(独立进程,期望退出码 1) +# 甲案词表退役〔架构整改 V3〕——overflow/attack_segments 两死效果通道摘出 EFFECT_KINDS: +# 向 feats.tres 注入 do.kind="overflow" 与 do.kind="attack_segments" 探针行 → validate_trigger_row +# 须以 EFFECT_KINDS 出域拒绝(词表闭面收窄——误写即加载致命)。任一违规被放行即回归(退出码 2)。 +# 运行:godot --headless --path game --script res://tests/unit/data_config/effect_kind_retired_test.gd +extends SceneTree + +const ConfigDBScript := preload("res://core/config_db.gd") +const Kit := preload("res://tests/unit/test_kit.gd") + +const SRC_DIR := "res://data/tables" +const BAD_OVERFLOW_DIR := "user://effect_kind_fixture_overflow" +const BAD_SEGMENTS_DIR := "user://effect_kind_fixture_segments" + +# 注入探针:死通道 do.kind(甲案摘名两族——各一探针行) +const PROBE_OVERFLOW := ' {\n "feat_id": "kind_overflow_probe",\n "display_name": "通道探针·溢出",\n "rarity": "common",\n "triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "overflow", "value": 50 } } ],\n "desc": "夹具探针——甲案退役词 overflow"\n},\n' +const PROBE_SEGMENTS := ' {\n "feat_id": "kind_segments_probe",\n "display_name": "通道探针·连珠",\n "rarity": "common",\n "triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "attack_segments", "cap": 2, "divisor": 3, "value": 0 } } ],\n "desc": "夹具探针——甲案退役词 attack_segments"\n},\n' + + +func _inject(dir: String, probe: String) -> bool: + var path := dir + "/feats.tres" + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + push_error("注入失败:读不到 %s" % path) + return false + var s := f.get_as_text() + f.close() + var anchor := "rows = [" + var idx := s.find(anchor) + if idx < 0: + push_error("注入失败:锚点 '%s' 未找到(len=%d)" % [anchor, s.length()]) + return false + var cut := idx + anchor.length() + s = s.substr(0, cut) + "\n" + probe + s.substr(cut) + var w := FileAccess.open(path, FileAccess.WRITE) + if w == null: + push_error("注入失败:写不开 %s" % path) + return false + w.store_string(s) + w.close() + return true + + +func _assert_rejected(bad_dir: String, probe: String, label: String) -> bool: + # 返回 true=正确拒绝;false=放行或夹具失败(调用方据此退出 2) + if not Kit.copy_tables(SRC_DIR, bad_dir) or not _inject(bad_dir, probe): + push_error("[%s] 夹具目录准备失败" % label) + return false + var db = ConfigDBScript.new() + var ok: bool = db.load_tables(bad_dir, false) + if ok: + push_error("[%s] 违规表被放行——EFFECT_KINDS 出域校验失守(回归)" % label) + return false + if db.diagnostics_count("error") < 1: + push_error("[%s] 加载被拒但零 error 诊断——校验通道静默化(回归)" % label) + return false + print("[%s] 正确拒绝(error 诊断在案——EFFECT_KINDS 出域)" % label) + return true + + +func _init() -> void: + if not _assert_rejected(BAD_OVERFLOW_DIR, PROBE_OVERFLOW, "effect_kind_retired_test/①overflow 出域"): + quit(2) + return + if not _assert_rejected(BAD_SEGMENTS_DIR, PROBE_SEGMENTS, "effect_kind_retired_test/②attack_segments 出域"): + quit(2) + return + print("[effect_kind_retired_test] 双探针均正确拒绝(甲案词表退役——死通道误写加载致命)〔架构整改 V3〕") + quit(1) diff --git a/game/tests/unit/data_config/effect_kind_retired_test.gd.uid b/game/tests/unit/data_config/effect_kind_retired_test.gd.uid new file mode 100644 index 00000000..bb97b0c6 --- /dev/null +++ b/game/tests/unit/data_config/effect_kind_retired_test.gd.uid @@ -0,0 +1 @@ +uid://pgo3ndugo2w1 diff --git a/game/tests/unit/data_config/forge_table_test.gd b/game/tests/unit/data_config/forge_table_test.gd index 370bc4b5..226a5114 100644 --- a/game/tests/unit/data_config/forge_table_test.gd +++ b/game/tests/unit/data_config/forge_table_test.gd @@ -97,6 +97,21 @@ func _test_price_ladder(db) -> void: prices == [20, 30, 45, 67, 101]) Kit.check("已满级(owned=5)再购价照算 151(cap 闸归写口,价格函数纯几何)〔锻造工坊批 V2〕", ForgeTableScript.level_price(row, 5) == 151) + # 六路线全量梯(断言补齐批 V11——floor 公式按表值推算不写死表值、逐级严格递增、满级档价=上限档推算值) + var all_ladder_ok := true + for frow in db.crossrun.forge.routes: + var base: int = int(frow["base_price"]) + var ratio: int = int(frow["price_ratio_pct"]) + var cap: int = int(frow["cap"]) + var prev := 0 + for owned in range(cap + 1): + var expect: int = int(floor(float(base) * pow(float(ratio) / 100.0, float(owned)))) + if ForgeTableScript.level_price(frow, owned) != expect: + all_ladder_ok = false + if owned > 0 and expect <= prev: + all_ladder_ok = false # 逐级严格递增(ratio>100 几何保证) + prev = expect + Kit.check("六路线全量梯:floor 公式表值推算逐级严格递增、满级档价=上限档推算值〔断言补齐批 V11〕", all_ladder_ok) # ---- 负向:出域行逐类构造各一行全部被拒(V1)---- @@ -199,7 +214,7 @@ func _test_negative_cases() -> void: # 现行 31;本套件零扩签,钉现行数(将来任何加词即转红提示复核)。 func _test_effect_kinds_closed() -> void: - Kit.check("效果引擎效果类型闭集 34 种(迭代 46 换图系扩签 grant_reserve_ap/apply_discount 换代)〔锻造工坊批 V11〕", - EffectEngineScript.EFFECT_KINDS.size() == 34) + Kit.check("效果引擎效果类型闭集 32 种(甲案词表退役——overflow/attack_segments 摘名)〔锻造工坊批 V11〕", + EffectEngineScript.EFFECT_KINDS.size() == 32) Kit.check("效果引擎事件词表在册 22 种(迭代 46 换图系扩签 refresh_request/board_refreshed 换代)〔锻造工坊批 V11〕", EffectEngineScript.EVENTS.size() == 22) diff --git a/game/tests/unit/data_config/level_gen_params_test.gd b/game/tests/unit/data_config/level_gen_params_test.gd index 28ac1aba..116cee43 100644 --- a/game/tests/unit/data_config/level_gen_params_test.gd +++ b/game/tests/unit/data_config/level_gen_params_test.gd @@ -26,7 +26,8 @@ func _row(L: int, layers: int, bd: float, ratios: Array, entry: int, exit_c: int return {"level_index": L, "layers": layers, "branch_density": bd, "battle_ratio": ratios[0], "reward_ratio": ratios[1], "rest_ratio": ratios[2], "shop_ratio": ratios[3], "entry_count": entry, "exit_count": exit_c, "battle_floor": floor_n, - "fork_ways_min": 2, "fork_ways_max": 3} + "fork_ways_min": 2, "fork_ways_max": 3, + "fork_third_way_pct": 35, "fill_rest_first_pct": 50, "width_max": 4} func _table(rows: Array) -> Variant: diff --git a/game/tests/unit/data_config/table_structure_guard_test.gd b/game/tests/unit/data_config/table_structure_guard_test.gd index ff0d7f8b..f1d5bfeb 100644 --- a/game/tests/unit/data_config/table_structure_guard_test.gd +++ b/game/tests/unit/data_config/table_structure_guard_test.gd @@ -42,6 +42,9 @@ func _init() -> void: _smoke_ac_swap_guard() _smoke_enemy_bands() _smoke_variation_framework() + _test_level_gen_new_fields() + _test_variation_header_domains() + _test_feat_cap_required() _smoke_difficulty_table() _smoke_nine_types_gp_stream() _smoke_adventure_table() @@ -529,3 +532,87 @@ func _conversion_row_of(rows: Array, attr: String) -> Dictionary: if String(r.get("attr", "")) == attr: return r return {} + + +# ---- V7 节点图三新键域校验(架构整改——缺新键行拒/width_max 越域拒/两 pct 越域拒/表值全过)---- + +func _test_level_gen_new_fields() -> void: + var tbl = _db.run.level_gen_params + var mk := func(mod: Dictionary) -> Dictionary: + var row := {"level_index": 1, "layers": 6, "branch_density": 0.3, "battle_ratio": 0.5, "reward_ratio": 0.2, + "rest_ratio": 0.2, "shop_ratio": 0.1, "entry_count": 1, "exit_count": 1, "battle_floor": 4, + "fork_ways_min": 2, "fork_ways_max": 3, "fork_third_way_pct": 35, "fill_rest_first_pct": 50, "width_max": 4} + for k in mod.keys(): + if bool(mod[k]): + row.erase(k) + else: + row[k] = mod[k] + return row + var inst = load("res://data/schemas/level_gen_params.gd").new() + # 表值 35/50/4 全过 + inst.rows.append(mk.call({})) + Kit.check("V7 表值行全过(35/50/4)〔架构整改 V7〕", inst.validate().is_empty()) + # 缺新键行拒(structural 键集闭集) + var missing: Dictionary = mk.call({"fork_third_way_pct": true, "fill_rest_first_pct": true, "width_max": true}) + inst.rows = [] as Array[Dictionary] + inst.rows.append(missing) + Kit.check("V7 缺新键行拒(ROW_KEYS 闭集)〔架构整改 V7〕", not inst.validate().is_empty()) + # width_max 越域拒 + inst.rows = [] as Array[Dictionary] + inst.rows.append(mk.call({"width_max": 7})) + Kit.check("V7 width_max=7 越域拒〔架构整改 V7〕", not inst.validate().is_empty()) + # 两 pct 越域拒 + inst.rows = [] as Array[Dictionary] + inst.rows.append(mk.call({"fork_third_way_pct": 101})) + Kit.check("V7 fork_third_way_pct=101 越域拒〔架构整改 V7〕", not inst.validate().is_empty()) + inst.rows = [] as Array[Dictionary] + inst.rows.append(mk.call({"fill_rest_first_pct": -1})) + Kit.check("V7 fill_rest_first_pct=-1 越域拒〔架构整改 V7〕", not inst.validate().is_empty()) + + +# ---- 变异表头域校验 / feat cap 必填(架构整改——丁案三件的数据面守卫)---- + +func _test_variation_header_domains() -> void: + var inst = load("res://data/schemas/variation_table.gd").new() + inst.percent_of_cells = 5 + inst.per_level_bonus = 0 + inst.pack_max = 4 + inst.pack_min = 2 + Kit.check("V11 表值头全过(5/0/4/2)〔架构整改 V11〕", inst.validate().is_empty()) + inst.pack_min = 5 # > pack_max=4 → 拒 + Kit.check("V11 pack_min>pack_max 拒〔架构整改 V11〕", not inst.validate().is_empty()) + inst.pack_min = 2 + inst.percent_of_cells = 51 # > 50 → 拒 + Kit.check("V11 percent_of_cells=51 拒〔架构整改 V11〕", not inst.validate().is_empty()) + inst.percent_of_cells = 5 + inst.pack_max = 10 # > 9 → 拒 + Kit.check("V11 pack_max=10 拒〔架构整改 V11〕", not inst.validate().is_empty()) + + +func _test_feat_cap_required() -> void: + var inst = load("res://data/schemas/feat_table.gd").new() + # 生产表全行 cap 在场(加载期已验——此处显式复核 37 行) + var all_cap := true + for row in _db.crossrun.feats.rows: + if not row.has("cap"): + all_cap = false + elif int(row["cap"]) < 1 or int(row["cap"]) > 99: + all_cap = false + Kit.check("V12 生产 feats 表全行 cap 在场且在域 [1,99]〔架构整改 V12〕", all_cap and _db.crossrun.feats.rows.size() > 0) + # 缺 cap 行拒 + var mk := {"feat_id": "probe_cap", "display_name": "探针", "rarity": "common", "triggers": []} + var no_cap := mk.duplicate(true) + inst.rows.append(no_cap) + Kit.check("V12 缺 cap 行拒(必填——零兜底)〔架构整改 V12〕", not inst.validate().is_empty()) + # 越域拒 + var oor := mk.duplicate(true) + oor["cap"] = 100 + inst.rows = [] as Array[Dictionary] + inst.rows.append(oor) + Kit.check("V12 cap=100 越域拒〔架构整改 V12〕", not inst.validate().is_empty()) + # cap_ok 直索引零兜底:在场行读 cap(relic 面真实入口) + var deck = load("res://core/curio_deck.gd").new(_db, load("res://core/rng_service.gd").new()) + var some_feat: Dictionary = _db.crossrun.feats.rows[0] + var cap_v: int = int(some_feat["cap"]) + Kit.check("V12 cap_ok 在场行直索引读 cap(首行 %s cap=%d——零兜底路径)〔架构整改 V12〕" % [String(some_feat["feat_id"]), cap_v], + deck.cap_ok("relic", String(some_feat["feat_id"])) == (int(deck.taken.get("relic:" + String(some_feat["feat_id"]), 0)) < cap_v)) diff --git a/game/tests/unit/presentation/battle_narrator_test.gd b/game/tests/unit/presentation/battle_narrator_test.gd index 13362dd7..494ef192 100644 --- a/game/tests/unit/presentation/battle_narrator_test.gd +++ b/game/tests/unit/presentation/battle_narrator_test.gd @@ -23,6 +23,7 @@ func _init() -> void: _test_phase_text_nonempty() _test_boss_name_uses_boss_rowset() _test_level_clear_wording() + _test_snapshot_effects_per_unit() quit(Kit.finish("battle_narrator_test")) @@ -99,3 +100,39 @@ func _test_level_clear_wording() -> void: {"cleared": false, "result": "failed", "levels_cleared": 1, "stopped_at": 2, "keystone_total": 4, "master_seed": 42}) Kit.check("冒险摘要完成关数行(实际「%s」)〔口径换代 V3〕" % summary, summary.contains("完成 1 关 · 止步第 2 层") and not summary.contains("通关")) + + +# ---- 呈现各读各的(增益状态 v1.5——敌方状态个体化批)---- +# 真实入口=呈现状态快照函数 presentation_snapshot:A 有削弱 B 无 → 两卡投影各现各的(零共享引用) + +func _test_snapshot_effects_per_unit() -> void: + var db = load("res://core/config_db.gd").new() + Kit.check("V8 前置:表加载零 error", db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0) + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(20261103) + var settlement = load("res://core/battle_orchestrator.gd").new(db, rng) + # depth 3 = hobgoblin 行集 2-3 只——两卡对照面 + var open_ok: bool = settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(db.combat.hero.max_hp)}) + Kit.check("V8 前置:开局(L1 depth3 双敌行集)", open_ok) + if not open_ok: + return + var enemies: Array = settlement._enemy_system.all_enemies() + var dual_ok: bool = enemies.size() >= 2 + Kit.check("V8 前置:≥2 敌(两卡对照面)", dual_ok) + if not dual_ok: + return + var sc = load("res://core/status_container.gd") + var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} + Kit.check("V8 前置:A 敌受理削弱(B 敌不授)", settlement._status_effects.apply_effect("weaken", sc.owner_key_for(enemies[0]), src)) + var snap: Dictionary = settlement.presentation_snapshot() + var e1_effects: Array = snap.enemies[0].effects + var e2_effects: Array = snap.enemies[1].effects + var e1_has := false + for e in e1_effects: + if String(e.effect_id) == "weaken": + e1_has = true + var e2_has := false + for e in e2_effects: + if String(e.effect_id) == "weaken": + e2_has = true + Kit.check("A 有削弱 B 无 → 两卡投影各现各的(A 卡现削弱、B 卡无同款)〔个体化批 V8〕", e1_has and not e2_has) diff --git a/game/tests/unit/presentation/player_copy_sweep_test.gd b/game/tests/unit/presentation/player_copy_sweep_test.gd index 40fa7d12..b0b5db89 100644 --- a/game/tests/unit/presentation/player_copy_sweep_test.gd +++ b/game/tests/unit/presentation/player_copy_sweep_test.gd @@ -36,6 +36,7 @@ func _init() -> void: _sweep_source_names() _sweep_board_glyph() _sweep_token_scans() + _sweep_refresh_toast_no_literals() quit(Kit.finish("player_copy_sweep_test")) @@ -156,6 +157,27 @@ func _sweep_token_scans() -> void: orch_hits == 0) +# 刷新两 toast 文案零数字字面量(架构整改 R4——格式串零数字、插值位与表字段一一对应; +# 静态判据+评审注记观测面局限:真机文案随表值由实机目检兜——T10 清单两项) +func _sweep_refresh_toast_no_literals() -> void: + var main_src := FileAccess.get_file_as_string("res://presentation/main.gd") + var rap_toast := "" + var cap_toast := "" + for line in main_src.split(" +"): + if "弃图刷新需" in line: + rap_toast = line + if "本轮刷新次数已用完" in line: + cap_toast = line + Kit.check("V4 前置:两 toast 行在场(源锚)", rap_toast != "" and cap_toast != "") + # RAP 不足 toast:格式串零数字字面量(「需 %d」插值读 rap_refresh_cost 表值) + var rap_fmt_bad := "(弃图刷新需 8)" in rap_toast or "%d 点(弃图刷新需 %d)" not in rap_toast + Kit.check("RAP 不足 toast:格式串零数字字面量、插值位=cost 表值拼接〔架构整改 V4〕", not rap_fmt_bad) + # 轮上限 toast:文案零数字字面量(「每轮 %d 次」插值读 refresh_cap_per_round 表值) + Kit.check("轮上限 toast:零数字字面量、插值位=refresh_cap_per_round 表值拼接〔架构整改 V4〕", + "(每轮 2 次)" not in cap_toast and "每轮 %d 次" in cap_toast) + + func _first_label(node: Node) -> Label: for child in node.get_children(): if child is Label: diff --git a/game/tests/unit/presentation/settings_store_test.gd b/game/tests/unit/presentation/settings_store_test.gd new file mode 100644 index 00000000..91a7e4a0 --- /dev/null +++ b/game/tests/unit/presentation/settings_store_test.gd @@ -0,0 +1,63 @@ +# tests/unit/presentation/settings_store_test.gd — 设置存储单元契约(回读/缺省/应用链) +# 断言面 = SettingsStore 公开方法:save_settings/load_settings 持久化回读、缺文件出厂默认、 +# apply() 引擎档应用(UiKit.motion_scale 双态)。隔离卫生:一律经 _init path 注入指向临时 cfg, +# 禁碰真实 user://settings.cfg(ui_click_probe 设置面段同款纪律)。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/settings_store_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const StoreScript := preload("res://presentation/settings_store.gd") + +const ISO_PATH := "user://settings_store_unit_test.cfg" # 隔离注路径(临时件——测毕清理) + + +func _init() -> void: + _test_settings_roundtrip() + _test_settings_default() + _test_settings_apply_chain() + _cleanup() + quit(Kit.finish("settings_store_test")) + + +func _cleanup() -> void: + DirAccess.remove_absolute(ProjectSettings.globalize_path(ISO_PATH)) + + +# ---- 持久化回读(写 true→存→新实例回读 true;节/键 motion/reduced)---- + +func _test_settings_roundtrip() -> void: + var w = StoreScript.new(ISO_PATH) + w.reduced_motion = true + Kit.check("前置:save_settings 落盘成功", w.save_settings()) + var r = StoreScript.new(ISO_PATH) + r.load_settings() + Kit.check("持久化回读:新实例回读 reduced_motion=true(motion/reduced 节键)〔断言补齐批 V6〕", r.reduced_motion == true) + w.reduced_motion = false + Kit.check("回写往返:改 false 再存再读得 false〔断言补齐批 V6〕", w.save_settings() and (func(): + var r2 = StoreScript.new(ISO_PATH) + r2.load_settings() + return r2.reduced_motion == false).call()) + + +# ---- 缺省值(无文件缺省 false)---- + +func _test_settings_default() -> void: + var absent_path := "user://settings_store_unit_absent_%d.cfg" % (Time.get_ticks_msec()) + DirAccess.remove_absolute(ProjectSettings.globalize_path(absent_path)) # 确保无文件 + var d = StoreScript.new(absent_path) + d.load_settings() + Kit.check("无文件缺省:reduced_motion=false(缺文件=出厂默认合法态)〔断言补齐批 V7〕", d.reduced_motion == false) + + +# ---- 引擎档应用链(reduced→apply()→UiKit.motion_scale==0.5,false→1.0;测毕还原静态初值)---- + +func _test_settings_apply_chain() -> void: + var saved_scale: float = UiKit.motion_scale # 静态初值留底——测毕还原 + var a = StoreScript.new(ISO_PATH) + a.reduced_motion = true + a.apply() + Kit.check("应用链:reduced=true → UiKit.motion_scale==0.5〔断言补齐批 V8〕", UiKit.motion_scale == 0.5) + a.reduced_motion = false + a.apply() + Kit.check("应用链:reduced=false → UiKit.motion_scale==1.0〔断言补齐批 V8〕", UiKit.motion_scale == 1.0) + UiKit.motion_scale = saved_scale # 还原(静态共享面——不渗入其他套件) diff --git a/game/tests/unit/presentation/settings_store_test.gd.uid b/game/tests/unit/presentation/settings_store_test.gd.uid new file mode 100644 index 00000000..32168909 --- /dev/null +++ b/game/tests/unit/presentation/settings_store_test.gd.uid @@ -0,0 +1 @@ +uid://bqscskbjj026u diff --git a/reports/审计-全库架构审查-2026-10-29.md b/reports/审计-全库架构审查-2026-10-29.md index d8964eb8..9c51594a 100644 --- a/reports/审计-全库架构审查-2026-10-29.md +++ b/reports/审计-全库架构审查-2026-10-29.md @@ -3,6 +3,7 @@ > **性质**:设计讨论段过程稿(审计读数)。`game/` 全目录只读架构审查的发现清单与证据,作为写卡段、工程改动批与文档设计批的输入材料;结论被吸收进权威文档与修复批后按「吸收即死」退役。 > **范围**:core 44 件、presentation 71 件、data 24 schema + 19 表、sim 15 件、probe 19 件、tests 69 件、`tools/` 闸件。判据双轨=项目自有规范(`design/engineering/architecture/architecture.md` v1.24 + 五份工程规约)+ Godot 4.7 最佳实践。 > **方式**:分层依赖方向/数据管线/测试覆盖三路并行静态取证+机械 grep 复核。**零测试执行、零引擎运行、零闸执行**(测试两入口铁规),全部结论来自静态读盘;下文每条命中均给 文件:行号,抽查原文已核。 +> **处置状态(2026-11-03 更新)**:§三 P0(3.1/3.2)已随迭代 50 三卡合回(合并提交 8d3a12ec)。**§四 P1 四条全部闭合**,含三处子项一并落笔:4.1 表 presentation 行(零动作,豁免通道限定用途已在册)、4.2 表 S7 行的私有成员直取面(缺口在册、移交 §5.1,不属 §四)、4.4 末「低覆盖另计四件」(零动作,能力清单已承接)——裁决、读数订正与已落地闸件见各节「2026-11-03」标注。**§五 P2 四项未处置**、§六待裁点⑦未落文,**本报告禁退役**(「吸收即死」判据未满足:结论未全吸收进权威文档)。登记位现不在原「架构审查优化讨论」条目(该条随迭代 50 收尾按状态机出口删除),改在待办清单「架构审查余项——闸面缺口、豁免存废、零测试点名与架构文档漂移」(2026-11-03 批次移交新立)。 --- @@ -102,12 +103,18 @@ core 内干净,绕过点全在 core 之外: | 层 | 位置 | 形态 | 是否被闸扫到 | |---|---|---|---| | presentation | `main.gd:914` | `randi()` 建种子,行内标 `rng-seed-exempt` | 扫到但被豁免标记排除 | + +**上表 presentation 行的处置(2026-11-03)**:零动作、维持现状。该处在产品码扫描面**内**,已被行内注记在册豁免,且豁免通道的限定用途已由 code-conventions §4 第 10 条写死=非玩法 RNG 消费面的引擎熵(组合根建种子不进玩法封闭流),与本节「三目录定种自建属合法测量口径」同一条理,不需再动。 | probe | `ui_click_probe.gd:20` 等 8 处 | `RandomNumberGenerator.new()` | **不扫** | | sim | `calibrate_boards.gd:54`、`experiment_density.gd:27`、`damage_probe.gd:274`、`level_autoplay.gd:388` | 自建 RNG + 显式定种 | **不扫** | | tests | 3 处测试夹具 | 自建 RNG | **不扫** | 全部绕过点均显式定种、无 `randomize()`(全库零命中),确定性尚存;问题在于**没有任何闸防它扩散**——S1/S2 扫描面只有 `DIR_CORE` + `DIR_PRES`(`static_scan.sh:20-21, 88-91`)。 +**2026-11-03 裁(不扩扫描面,改为写说明)**: +- **裁决**:S1/S2 扫描面**保持产品码两目录**,不扩 sim/probe/tests。这三目录自建且显式定种的随机源是**合法测量口径**(改前改后读数要用同种子对拍,属全项目测量前提),把它们判进「玩法代码禁自建 RNG」的闸面,等于给每处合法用法挂 `rng-seed-exempt` 注记、只拦得住新增——不值其成本。落文=`design/engineering/code-conventions.md` §4 新增第 13 条(按来源分档:三目录各由哪条运行期通道承接、允许自建的条件是不定种禁止、以及该禁止目前无机器闸只由文字纪律+批末独立验收审计人工评审承载),架构文档 S 表 S1 行加同口径注。 +- **读数订正(2026-11-03 复核)**:①自建点由本节记的「8 处」降到**7 处**——`game/sim/experiment_density.gd` 已随迭代 50 卡片 T9 删除(sim 现 3 处:calibrate_boards/damage_probe/level_autoplay;probe 1 处 ui_click_probe;tests 3 处 board_gen_reward/player_copy_sweep/theme_projection)。②「无 `randomize()`」要加限定:`randomize()` 在 `game/` 字面命中 2 处,两处均在注释里(`core/rng_service.gd:3`、`presentation/main.gd:756`),**实际调用零命中**,结论不变。③按 S1 原判据(裸 `randi()`/`randf()`/`randf_range()`)扫这三目录,非注释命中 **0 处**。 + ### 4.2 静态闸扫描面结构性缺口 `game/tests/static_scan.sh` 的 S1–S13 扫描面实况: @@ -127,6 +134,17 @@ core 内干净,绕过点全在 core 之外: 净效果:**sim / probe / tests 三层除 S11/S12 外,零类型闸、零质量闸、零 RNG 闸、零 await 闸。** +**2026-11-03 裁(三面不扩;两处机制修与两处本轮新查)**: +- **S9/S10/S13 不扩到三层**(裁决)。2026-11-03 只读数实跑(三个静态扫描脚本对三层,未修文件、未写基线)给出现存总量 **≈2162 处**:S9 类型 1070(sim 158/probe 130/tests 782,其中无类型容器 859)、S10 文件头 117、S13 质量 975(其中「禁调试打印」274+「循环内 await」70——这两类在测试与探针码里是功能需要,不是债)。产品码面 S9/S10 现行**归零**,扩面即把「归零」这一执法前提换成 1187 项存量红;为三层另建欠账基线会把账从 1 份变 3 份。落文=code-conventions §4 第 13 条写明适用域止于产品码并给出三目录的运行期承接通道。 +- **读数是**:S8 现行 **14 文件**显式清单(`static_scan.sh:317`),非本节所记 9 文件;同口径 14 文件清单在 S7 另抄一份(`:361`),两份靠人工同步;绿文案仍报「B1 文件集 10 个 .gd」(`:337`)与头注 `:311` 的「共 14 文件」自相矛盾;S8 自检只防「清单内文件缺位」(`:319-324`)不防「该进的没进」——纯目录域方案已证伪:装板/授予/效果端口/掷格四件结算码在 `game/core` 根下而非 `settlement/` 目录内。 +- **S7 行缺口的尾巴(本表 §4.2 里唯一指向 §五的一格)**:该行写「S7 不管私有成员直取面(见 5.1)」。§四 对 S7 只裁它自己的扫描面与清单形态——扫描面维持 `$DIR_CORE`、豁免名单与 S8 并成单点 `B1_SET`(已落地);私有成员直取面**不在本节处置范围**:它无静态判据可造(GDScript 无编译期访问检查,误报面大),且真要做是改 `core/settlement/` 的协作契约=游戏代码=需求改动通道,归 §5.1 与待办清单「架构审查余项」条目之后的讨论段。本节对此格的结论=**缺口在册、移交 §5.1,不在 §四 闭合**。 +- **本轮新查两处**(审计未列):①S9/S10 **无扫描面空转自检**(`tools/audit_type_hints.py:82-83`、`tools/audit_file_headers.py:64-65` 目录不存在即静默 PASS),违背本文「三层承载与判定原则」的零命中自检要求与脚本前言自立的防空转纪律(`static_scan.sh:18-19`);②S3 是半闸(命中只打 WARNING、不置 fail,`static_scan.sh:110`)——**但这条不是洞**:架构文档 S 表 S3 行早已登记「warning 级——class_name 直引误报需人工复核」,属已在册状态。2026-11-03 裁**维持 warning 不升硬**(升硬前提是先有可靠的直引误报排除判据,该判据未立)。 +- **待执行清单(2026-11-03 同日执行完毕,凭据见下)**: + 1. `game/tests/static_scan.sh`:S8 新增「`game/core/settlement/` 实有 `.gd` ⊆ 清单」反向对账(新段类未登记即红)+S7 的 `S7_B1_SET` 与 `S8_FILES` 两份 14 文件抄本合一为单点 `B1_SET`+绿文案「10 个 .gd」改为派生计数(现打「14 个 .gd+settlement/ 目录域已反向对账」)——**已落地**; + 2. `tools/audit_type_hints.py`/`tools/audit_file_headers.py`:补扫描面空转自检(缺目录或零个 `.gd` 即红,绿时打印扫描面件数供核)——**已落地**(负证:把面清单改成不存在目录,两件各返回退出码 1;正证:真实面 158 个 `.gd` 归零 PASS); + 3. 架构文档 S 表同步——S8 行登记单点与反向对账判据、原「S9/S10 缺自检」偏差句改判为已清偿——**已落地**(凭据=`design/engineering/architecture/architecture.md` v1.34)。 +- **验证口径**:`bash game/tests/static_scan.sh` 全量 13 条规则绿、退出码 0;车道=会话内独立 worktree 直改+合回远端 CI(零需求卡片,纯闸件面属工程改动)。§四 P1 至此四条全部闭合。 + ### 4.3 S13 豁免常态化 三处豁免全落在体量最大、风险最高的文件上: @@ -137,6 +155,8 @@ core 内干净,绕过点全在 core 之外: | `presentation/main.gd` | 46 个句柄成员 | 同上 | | `presentation/tune_sim_controller.gd` | await 使用 | 同上 | +**2026-11-03 结案(待裁点⑤已无对象,读数过期)**:本节所记三处豁免已于 **2026-10-31 迭代 44 批后收尾**清偿(commit 9eedf896「gdstyle 基线 refresh 收账 16→5 键:消失=main.gd49/orchestrator38+文件长 1035/tune_sim await…五键清偿」)。现行基线只剩 5 条键,全在 `adventure_runner`/`level_runner`/`board_view`(`tools/gdstyle_baseline.txt:8-12`),判定机制是「基线外新键红、同键数值恶化红、只准因还债下降」(同文件 :5)。零动作,不再叠清偿期限——「只降不升+refresh 骤降/抬升双守卫」已在位,加期限属重复建设。 + ### 4.4 四个 core 文件零测试点名 判定口径:文件名与 `class_name` 在 `game/tests/` 全域 grep 均零命中。 @@ -152,6 +172,14 @@ core 内干净,绕过点全在 core 之外: 低覆盖(仅 1 个测试文件点名)另计:`hero_stats`、`skill_lottery`、`shop_session`、`curio_deck`。 +**2026-11-03 裁(定性改判+读数订正为 11 件;落文=规约登记义务,不建闸不补空套件)**: +- **定性改判**:本节「零测试点名」的风险叙述不成立——四件都有有针对性断言,只是不经文件名的通道:MetaStateStore 经 `meta_state.` 键散布 14 个测试文件、SlotStore 经 `.slots.attach_path/attach_slot_template` 在 system_test 与 save_slot_isolation/summary/boot_no_activate 四套件、BattleFxHost 经 `fx_host()` 在 adventure_feats_test.gd:584-661、AttuneLoadout 经 `_loadout.set_attunement` 在 feat_vouchers_test.gd:80-83。真缺口是**没有「按文件登记覆盖」的惯例**,不是没测。 +- **读数订正**:按本节同口径(`game/tests/**/*.gd` 全域 grep basename 与 class_name)复跑,core 现 52 件里零点名是 **11 件**,不是 4 件。分三档:纯零 6 件(attune_loadout 117 行/slot_store 189 行/config_domain_combat·crossrun·presentation·run 四件 7–12 行纯持有容器);只在闸清单出现 4 件(battle_fx_host 232 行〔本节记 179,已涨〕/reward_roller 245/round_cycle 106/battle_refresh_flow 43);只在能力清单文字出现 1 件(meta_state_store 176)。**成因读数**:后一档三件全是迭代 44/46/49 的拆件产物——「拆件产新件不登记」是复发模式,不是一次性欠账。 +- **裁否两案**:①「立每个 core 件必须被点名一次的覆盖闸」——与本文档族的 `test-conventions.md` §7「覆盖率不是目标:不引入行覆盖率度量;现行补洞=能力清单覆盖域表+人工判定」正面冲突,且会把为点名而点名的空套件逼出来;②「给 11 件补点名套件」——同既有断言重复,属为登记而写码。 +- **落文**:`design/engineering/test-conventions.md` §8.3 新增「新增 core 件的覆盖登记义务」(新 core 件要么被 tests 面点名、要么在能力行申报「经宿主链覆盖(宿主=<件>)」,两者都无=收尾核对补登;明示本条不是覆盖率要求),`design/engineering/process-conventions.md` §3 第 6 条加承接指向(拆件套的覆盖登记归该义务,核对动作归收尾提交)。 +- **落笔约束**:能力清单 `game/tests/README.md` 本体归写卡段维护、实现批禁写(`test-conventions.md` §8.3),所以申报行由写卡段入表,本轮规约文字不代写清单内容。 +- **「低覆盖另计四件」的处置(本本节最后一句未落笔项)**:`hero_stats`/`skill_lottery`/`shop_session`/`curio_deck` 四件**零动作**。判据:低覆盖≠无覆盖,四件在能力清单各有代表用例承接(现值:`hero_stats`→hero_ledger_test 直构断言 rest_heal;`skill_lottery` 已由审计时点的 1 件升到 2 件——round_flow_skill_pool_test:798/830 与 trigger_cond_test:85/90;`shop_session`→level_shop_economy_test 经 `LevelRunner.shop.` 真实入口;`curio_deck`→pool_case_d 经 AdventureRunner),已满足新落的 §8.3 登记义务(点名或申报经宿主链覆盖二者有一即可)。要提覆盖得由写卡段在后续批次按能力行增补,不属本节闸件范围。 + --- ## 五、P2 — 封装与漂移 @@ -190,6 +218,8 @@ GDScript 无编译期访问检查,私有成员改名不会有任何闸报错 以下均需用户裁定,Agent 不代裁: +**2026-11-03 状态**:①②③ 已裁、已写卡、已随迭代 50 三卡合回(凭据=`reports/处置-架构审查3.1+3.2整改-2026-11-02.md` 与合并提交 8d3a12ec);④⑤⑥ 已裁,裁决、读数订正与待执行清单见 §四 的 4.1/4.2/4.3/4.4 各节「2026-11-03」标注;⑦ **未处置**——属 §五 P2,讨论段未开工,因此本报告整体尚未闭合。 + 1. **A 类硬编码 18 处怎么修**——按改动四类归属,其中 `core/` 与 `presentation/` 的玩法数值入表属**需求改动**(写卡 + 编排),`sim/` 两处模拟口径常量属**工程改动**。两类不能混一批。 2. **B 类结构常量要不要入表**——`MAX_WIDTH` / `CHARGE_LAYER_CAP` / `SPAWN_AREA_CELLS` / `node_map_generator` 的域字面量四组,代码自标「非玩法表值」,但 `SPAWN_AREA_CELLS` 已被 data 侧校验反向引用,是否算玩法数值需裁。 3. **sim 平行实现的处置口径**——是收敛(sim 调 core 的推理与掷点)还是承认双口径并显式登记(避免读数混用)。四组里①②影响数值验证可信度,③④影响可维护性。 diff --git a/reports/需求卡片-敌方状态个体化批-2026-11-02.md b/reports/需求卡片-敌方状态个体化批-2026-11-02.md deleted file mode 100644 index 3ef4cdc7..00000000 --- a/reports/需求卡片-敌方状态个体化批-2026-11-02.md +++ /dev/null @@ -1,138 +0,0 @@ -# 需求卡片:敌方状态个体化批(2026-11-02) - -> 一句话:把增益/减益状态实例从「阵营共享一条」改为「每只敌人各自持有」——全体授予=逐只各发一条实例,敌方增益技只强化施放者自己,个体死亡即清名下实例。 -> 来源:待办清单条目「敌方状态个体化——状态实例归属到敌人个体」(2026-11-02 登记,来源=会话提及)。 -> 审裁决:godot-master 终审通过(2026-11-02——架构四查全过,P3 一处 S9 类型标注句折入 T0-1);grilling 零待裁点(断言换代申报照卡执行、估时归迭代行)。 - -## 设计事实源 - -| 文档 | 依据 | 版本 | -|---|---|---| -| `design/gdd/systems/战斗循环/增益状态.md` | 硬规矩「实例按个体归属」+〈目标规则与个体归属〉节(词表四词/合并认个体/死亡即清/呈现各读各的) | v1.5(2026-11-02) | -| `design/gdd/systems/战斗循环/敌人系统.md` | 增益技落点=施放者自己;警觉授予随动 | v1.5(2026-11-02) | -| `design/gdd/systems/战斗循环/战斗结算.md` | 警觉按个体;AC 合流=该敌自己名下的 ac 型状态 | v1.6(2026-11-02) | - -与待办清单条目凭据一致。 - -## 目标与完成定义 - -状态容器从「按阵营两桶」换为「按个体多桶」(主角一桶+每只敌各一桶),授予/合并/衰减/清除/兑付/呈现全链随动;敌方增益技从「全体受益」纠正为「施放者自己」。完成定义=全部断言当轮跑批执行并通过+代码域闸全绿+合回后本地 CI 全量域闸全绿+主角侧行为零变化(回归全绿)。 - -## 现状事实(代码级考证 2026-11-02,全仓审计) - -- **容器本体**:`StatusContainer` 全战斗单例(battle_orchestrator.gd:217),实例按 `target_side` 两桶;`apply_effect` :28-49、`query_active(side)` :98-118、`decay_round` :121-134(全池统一 −1)、`notify_battle_end` :137-143。 -- **授予写点 3 处**:敌方技能增益(combat_segment.gd:84-90,落点由状态行 target_side 路由到敌方侧——现行全体共享的根);奖励格(reward_grant.gd:226-239,含 enemy_alert);直伤附带(damage_controller.gd:451 fx_apply_status)。 -- **读点**:乘区池合计 `clamped_pool` damage_controller.gd:629-637(两 side 实例混算);defender_ac 敌分支 :601-606(side 级 ac 型加到每一只敌);nest_count battle_orchestrator.gd:117-121 与 damage_controller.gd:93-98;池兑付 _pool_effective :521-527;结算快照 buffs :525-535;呈现投影 :670-692(**同一数组引用塞进每张敌卡**,standee_body.gd:80 自认「各立绘同值」)。 -- **死亡**:unit_died → damage_controller._on_unit_died :192-198(未结单据作废);状态实例不随死亡清,活到衰减或战斗终局。 -- **已正确的个体面(不动)**:技能冷却/半血闩/保底计数(unit_container.gd:99-153、:259-272);hp/armor/intent;死亡清脸呈现;顺位取靶(:358-362)。 -- **主角侧**:单单位,全链语义不受影响。 -- **中途入场**:不存在该路径(敌人开战一次性实例化,unit_container._spawn_rowset :81-118),「后援继承」问题消解。 - -## 设计契约红线 - -- 🔴 **GDD v1.5 语义逐条照搬**:实例按个体归属/全体授予=逐只各发一条/敌方增益技归施放者自己/个体死亡即清/呈现各读各的/合并只在同体内——任何偏离=L3 停卡。 -- 🔴 **status_effects.tres 行值零触碰**:合并策略、词表、时长、幅度全照旧——本批只改归属结构,不改任何状态定义。 -- 🔴 **断言换代申报制**:下方「测试削弱申报」清单之外,禁删禁改任何既有断言;实现轮撞红的侧级共享读数断言须挂对照表行换代,静默放宽=独立审计判红。 -- 🔴 **主角侧行为零变化**:attack_up/ac_up 全套既有断言回归绿;无类型键改动泄漏到主角调用面以外的公开接口。 -- 🔴 RNG 单点/Core 零 UI 依赖/fail-fast 照旧;全程零随机(禁随机摇目标)。 -- 🔴 **实现照单落名**:README 代表用例名=测试函数名唯一依据;断言尾缀〔个体化批 Vn〕全仓可 grep。 - -## 测试削弱申报(唯一允许的换代清单) - -| 既有断言 | 换代方式 | 依据 | -|---|---|---| -| `roll_multiplier_test.gd:_smoke_armor_roll` V1/V2/V3(迭代 48 卡①削弱累加三断言) | 重写为个体键读数——累加/续时/覆盖时长的语义断言原样保留,只把「敌方侧查询」换成「目标个体查询」 | GDD v1.5 合并匹配认个体 | -| `round_flow_skill_pool_test.gd:_smoke_settlement_segments` 衰减相位断言(:413-421、:449) | 全池衰减改逐桶衰减(每个存活个体恰 −1、归 0 移除),断言语义等价换代 | GDD v1.5 实例按个体归属 | - -## 范围边界 - -**做**:容器个体化+3 写点+全部读点随动+呈现投影换代+上表申报断言换代+新断言 V1-V8。 - -**不做**:种类级目标词(不预先建面);伤害侧新目标规则(现行三词够用);主角侧任何语义变化;status_effects.tres 行值;多语言/音频(P1);mid-battle 增援(路径不存在)。 - -## 关联文档同步面 - -| 文档 | 关联点 | 同步义务 | -|---|---|---| -| `design/gdd/systems/战斗循环/增益状态.md` v1.5 | 〈目标规则与个体归属〉节「实装随该批」规矩行 | 实装合入后该行已过时——反哺点清单登记,用户裁决后文档批删除(实现批禁写 design/) | -| `game/tests/README.md` | 能力行增改 | 写卡段当场落(本批卡内已列),实现照单落名 | - -## 任务卡 T0..T3 - -**T0 容器个体化+授予路径换代(≈1 工作日)** - -- [ ] T0-1 `status_container.gd`:实例桶键从 side 换个体——个体键约定单点定义(常量:主角=`"hero"`,敌人=`"e<生成序>"`,生成序=开战逐只编号沿用顺位轴);新桶键结构按 S9 全类型标注(godot-master 终审折入);`apply_effect(effect_id, owner_key, duration_override)`、`query_active(owner_key)`、`decay_round()`(逐桶各 −1)、`purge_owner(owner_key)` 新增(死亡清)、`notify_battle_end` 照旧全清 -- [ ] T0-2 授予写点 3 处换代:combat_segment.gd 增益技落点=施放者自己;reward_grant.gd 全体规则=循环存活敌逐只发(封印削弱/踩雷/警觉);damage_controller.fx_apply_status=被命中者个体 -- [ ] T0-3 死亡清接线:unit_died → purge_owner(死亡个体名下实例全清) -- [ ] T0-4 断言落地 `system_contracts_test.gd`:`_test_status_individual_isolation`〔V1〕(两敌各授同名实例,层数/时长互不干扰);`_test_status_grant_all_per_unit`〔V2〕(全体授予=每只各一条、各自衰减);`_test_status_death_clears`〔V3〕(死体实例清零、他体不受影响);`_test_status_merge_per_unit`〔V4〕(同体合并照旧:累加/叠加/满层拒) -- [ ] T0-5 换代申报落地:armor_roll V1-V3 重写(个体键读数,语义保留);`round_flow_skill_pool_test.gd` 新增 `_test_enemy_buff_targets_self`〔V5〕(蓄力只强化施放者,他敌 output 池读数不变) -- [ ] T0-6 单元套全绿——里程碑步出口跑一次全套闸;波内修复只做出红最小单元的最窄确认 - -**T1 消费面随动(≈0.5 工作日)** - -- [ ] T1-1 `damage_controller.gd`:`clamped_pool` 带 owner_key(主角打敌=该敌防御池;敌打主角=施放敌自己的输出池);defender_ac 敌分支只读被打者名下 ac 型实例;nest_count 两处(battle_orchestrator.gd:117-121、damage_controller.gd:93-98)按个体;_pool_effective 与结算快照 buffs 逐个体 -- [ ] T1-2 断言落地 `damage_controller_test.gd`:`_test_pool_consumption_per_unit`〔V6〕(B 敌的削弱不进 A 敌的账);`_test_defender_ac_per_unit`〔V7〕(alert 各算各的) -- [ ] T1-3 单元套全绿(同 T0-6 措辞) - -**T2 呈现投影+主角侧回归(≈0.5 工作日)** - -- [ ] T2-1 battle_orchestrator.gd:670-692:enemies[].effects 每卡读自己桶(删同引用共享);结算明细归因照旧 -- [ ] T2-2 断言落地 `battle_narrator_test.gd`:`_test_snapshot_effects_per_unit`〔V8〕(A 有削弱 B 无 → 两卡投影各现各的) -- [ ] T2-3 主角侧回归:attack_up/ac_up 全套既有断言绿;冒烟 15 项 0 失败 -- [ ] T2-4 单元套全绿(同 T0-6 措辞) - -**T3 产出物落位卡(≈0.25 工作日)** - -- [ ] T3-1 合并前写齐 `reports/iterN-实现报告-<日期>.md`(各卡状态+实机目检总清单〔本批玩家可见面=敌方增益图标各现各的、蓄力不再全体受益——两项入目检清单,登记不阻塞〕+实现决定记录) -- [ ] T3-2 合并前写齐 `reports/iterN-反哺点-<日期>.md`(增益状态.md「实装随该批」行删除义务,逐条落点与依据;实现批禁写 design/ 与根规则文件) - -## 验证闸(改动—验收对照表) - -| 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 | -|---|---|---|---|---|---|---| -| 实例按个体归属 | status_container.gd 键换代 | `_test_status_individual_isolation`〔V1〕 | 单元 | StatusContainer.apply_effect()/query_active() | 各体投影读数互不影响 | 新增:状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 | -| 全体授予逐体发 | reward_grant.gd 循环授予 | `_test_status_grant_all_per_unit`〔V2〕 | 单元 | 同上 | 逐体实例数与各自衰减 | 新增:状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 | -| 个体死亡即清 | unit_died→purge_owner | `_test_status_death_clears`〔V3〕 | 单元 | 同上+purge_owner() | 死体桶清零、他体不变 | 新增:状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 | -| 同体合并照旧 | 合并匹配认个体 | `_test_status_merge_per_unit`〔V4〕 | 单元 | StatusContainer.apply_effect() | 同体累加/叠加/满层拒读数 | 新增:状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 | -| 增益技归施放者自己 | combat_segment.gd 落点换代 | `_test_enemy_buff_targets_self`〔V5〕 | 单元 | 战斗结算编排链(技能施放) | 施放者与被他敌的池读数差 | 新增:敌方增益自体落点与乘区池按个体 | -| 乘区池按个体 | clamped_pool 带 owner_key | `_test_pool_consumption_per_unit`〔V6〕 | 单元 | DamageController 兑付 | 被击者专属池合计 | 新增:敌方增益自体落点与乘区池按个体 | -| AC 合流按个体 | defender_ac 敌分支 | `_test_defender_ac_per_unit`〔V7〕 | 单元 | DamageController 兑付 | 被击者自身 ac 型合计 | 新增:敌方增益自体落点与乘区池按个体 | -| 呈现各读各的 | 投影换代 battle_orchestrator.gd | `_test_snapshot_effects_per_unit`〔V8〕 | 单元 | 呈现状态快照函数 | 两卡 effects 投影差 | 新增:状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 | -| 申报换代两件 | armor_roll/settlement_segments 重写 | 原函数名保留(语义等价换代,尾缀随原批) | 单元 | 原真实入口 | 原判据个体化 | 既有:同名状态合并策略(叠加/累加/并存)与剩余次数语义 | - -## 独立验收审计 - -执行体=迭代 loop 内启动的未参与实现子代理(2026-11-02 用户裁,test-conventions §8.7 默认通道);报告落 `reports/iterN-验收审计-<日期>.md`,逐行写「通过/缺漏/例外及依据」。审计四件事:逐行核对对照表(测试存在、当轮执行、尾缀可 grep、真实入口未绕改动点);抽查判据绑定面;检查测试削弱(对照本卡「测试削弱申报」两件清单);核对内部字段直查受控例外(本卡零直查行)。未过不得合并。 - -## 异常分级 - -- **L1 自救**:断言消息、局部夹具命名、循环授予的实现次序(入列顺序稳定可读照 GDD)。 -- **L2 记录继续**:撞红的其余侧级共享读数断言——按申报制挂对照表行换代,记实现决定记录。 -- **L3 停机呈报**:呈现快照无个体键承载(投影改不动=上游缺陷);purge 与断点续玩时序冲突;GDD 语义与现役观测面失配;机检键与本卡契约互斥——一律停卡退回写卡段。 - -## 代拍板白名单 - -- 测试内局部命名、常量、断言消息文本。 -- 循环授予的实现编排(按调用方给定次序入列,保证稳定可读)。 -- 个体键字符串格式的内部拼写(约定语义不变)。 - -## 待裁点 - -零待裁点,开工门=U0。 - -## 落位与可复用性预检 - -**零新增文件**:全部改造/扩展现有件——status_container.gd(本体)、combat_segment.gd/reward_grant.gd/damage_controller.gd(写点)、unit_container.gd(死亡信号既有)、battle_orchestrator.gd(读点/投影);测试扩展 system_contracts_test.gd/round_flow_skill_pool_test.gd/damage_controller_test.gd/battle_narrator_test.gd 四个既有宿主。 - -**唯一面**:个体键约定单点定义于 StatusContainer 头部(禁散落第二处拼写);purge 单点(死亡清唯一通道);无 helper 外散。 - -**分层与交互**:全链在 core 层,零 UI 依赖(headless 可跑通);呈现只经呈现状态快照函数读取,不直捅容器;RNG 零新增(本批零摇号)。 - -## 实现规范预算 - -- status_container.gd:公开方法 +1(purge_owner)、成员 +1(桶键结构)——公开方法 ≤20、成员 ≤15 内。 -- 存量触面 S13 预期:battle 域触碰文件(combat_segment/damage_controller/battle_orchestrator/reward_grant)键较登记值只降不升(clamped_pool 加参不增分支路径,投影换代删共享引用行);T0 读数步骤核对。 - -## 拆分边界纪律 - -本卡为行为重构(归属结构换代),非拆件批:禁为迎合机检保留或新增无效成员/别名;机检键与本卡契约互斥=停卡呈报,不自造第三条路。 diff --git a/reports/需求卡片-断言补齐批-2026-11-02.md b/reports/需求卡片-断言补齐批-2026-11-02.md deleted file mode 100644 index 042e3259..00000000 --- a/reports/需求卡片-断言补齐批-2026-11-02.md +++ /dev/null @@ -1,174 +0,0 @@ -# 需求卡片:断言补齐批(2026-11-02) - -> 一句话:为①增益状态结算链、②减动效开关存储、⑤Boss 出场门控、⑥锻造工坊结构四域补自动化断言——零行为改动、纯测试补齐,同批刷新能力清单缺口口径。 -> 来源:待办清单条目「能力清单覆盖缺口补齐——六类无断言面或设计未定项」(2026-10-29 登记,来源=会话提及)。 - -## 设计事实源 - -| 域 | 设计依据(唯一事实源) | 版本凭据 | -|---|---|---| -| ⑥ 锻造工坊 | `design/gdd/systems/跨局循环/锻造工坊.md` 验收标准节九条判据 | v0.11(2026-11-02) | -| ① 增益状态 | `design/gdd/systems/战斗循环/增益状态.md` 结算链规则 | v1.4 | -| ⑤ 难度曲线与Boss | `design/gdd/systems/冒险循环/难度曲线与Boss.md` 核心规则(机制面) | v1.2 | -| ② 减动效开关 | `design/gdd/systems/表现与支撑/设置与本地化.md`(开关承诺)+ `design/gdd/ux/battle.md`(减动效双态承诺:延迟归零同帧直落+横幅静态化、可加速不可整体跳过) | v1.1 | - -与待办清单条目 8 凭据(实况关联文档清单)一致;增益状态.md 以本表 v1.4 为准,待办条目实况中的 v1.1 记法由本批订正。 - -## 目标与完成定义 - -为条目 8 收窄后的四域剩余真缺口补 **13 个新断言函数+2 处既有函数扩展**;断言全数通过;能力清单(`game/tests/README.md`)同步刷新(5 行新增能力行+5 行覆盖域表缺口句订正)。 -完成定义=全部断言在当轮真实跑批中执行并通过+代码域闸全绿+合回后本地 CI 全量域闸全绿。 - -## 现状事实(代码级考证,2026-11-02) - -### ① 增益状态结算链(game/core/status_container.gd) - -- 授予路由:`apply_effect` :28-49 按 stack_policy 三臂路由(independent :41-43/accumulate :44-45/stack :46-47)。 -- 叠层/续时:`_apply_accumulate` :60-67(命中只加时长、层数恒 1、快照沿首授);`_apply_stack` :70-80(满层拒返 false、层数与时长零写入)。 -- 衰减:`decay_round` :121-134(先兑付后衰减、每实例每结算恰 −1、归 0 移除、返回移除数 k)。 -- 清除:`notify_battle_end` :137-143(战斗结束全清);`notify_battle_start` :16-22(开域+清残留)。 -- 可观测面:返回值 bool/int+只读投影 `query_active(side)`(effect_id/layers/remaining_settlements/effective_value=L×m)。 -- 已盖面(不动):迭代 48 卡①(git 1e35ab94)已盖叠层续时 V1/V2/V3(`roll_multiplier_test.gd:_smoke_armor_roll` :160-167);五段全序与衰减相位断言(`round_flow_skill_pool_test.gd:_smoke_settlement_segments` :382-449)。 -- 剩余缺口:衰减逐值与到期移除数、战斗结束全清、满层拒绝、independent 分支、快照沿首授——全部零断言(notify_battle_end 全测试目录零调用,grep 证实)。 - -### ② 减动效开关存储(game/presentation/settings_store.gd) - -- 独立设置存储(不进存档):user://settings.cfg 节/键 motion/reduced(读 :26、写 :33);`reduced_motion` 默认 false(:12)。 -- 引擎档应用:`apply()` :38-45(reduced → UiKit.motion_scale=0.5,否则 1.0)。 -- 已盖面(不动):探针全链(`ui_click_probe.gd` 设置面段 :310-327:开→0.5、持久化回读、关→1.0);系统测试 V2b(`system_test.gd` :166-168);单元侧排播双态(`pacing_queue_test.gd` :120-125,直拨 UiKit.motion_scale 不经 store)。 -- 剩余缺口:store 本体零单元断言(持久化回读/缺省值/应用链);README :194 缺口句仍写「仅系统测试与冒烟覆盖」,与现役探针覆盖不符——本批订正。 - -### ⑤ Boss 出场门控(机制面) - -- 门控链:`node_map_generator.gd` `_build_region` :338-352(每区域 boss 深度=L−r、恰一终点)→ `level_runner.gd:546` node_type 入参 → `battle_orchestrator.gd:283` row_kind 传参 → `config_db_resolver.gd:33-67` 行类路由(越域 fatal :41-43)→ boss 行集选出。 -- Σ×r_boss 无运行时承载(Boss hp 行值直填 enemy.tres;difficulty_table.gd:2-3 头注成文口径「Boss 数值走行值直填不经本表」)——本批不造运行时公式断言。 -- 已盖面(不动):resolver 路由与 clamp(`config_db_resolver_test.gd` 四函数);表侧 Boss 行集带键覆盖(`table_structure_guard_test.gd:_smoke_enemy_bands` :242-259);半血门与闩(`skill_unlock_test.gd`)。 -- 剩余缺口:每区域恰一 Boss 终点无专项断言(`node_map_generator_test.gd` 只断终点数与入口数 :79-89);「Boss 节点开战必选 boss 行集」端到端无直接断言。 - -### ⑥ 锻造工坊(meta_progression.gd+forge_table.gd+shop_session.gd) - -- 解锁双通道单点:`_forge_route_unlocked` meta_progression.gd:126-139(初始持有∨直购∨练满前一档);直购写口 `forge_buy_unlock` :160-170(已解锁拒绝零收费),解锁价 200(forge.tres)。 -- 定价:`forge_table.gd:24-28` level_price = floor(基价 × (公比/100)^已购级数);表值基价 20/公比 150(forge.tres)。 -- 注入:`hero_override` :270-293(树淬炼 :280-288、分层相加 :289-293、树攻防恒 0);`_forge_axis_bonus` :297-302(只认选中路线、异常回退 0);参数面下传 main.gd:191-201;入账 adventure_runner.gd:89-97。 -- 默认值常驻:`hero_type_axes` :188-198(未点亮照常返回 normal/light)+ forge.tres:7-8 默认轴。 -- 拒购:`forge_buy_level` :144-155;余额闸零副作用(`meta_state_store.gd:18-22` spend_keystone 余额不足 return false 零写入)。 -- 存档:save_manager v7 forge_routes 域 :267-272(默认 weapon=sword/armor=light)+normalize 补齐 :295-311。 -- 祝福件通道:局内商店购买(shop_session.gd:71-91 legacy/:272-293 pool)写局内加算区(GP 币),与 forge_routes 代码零交集;绑定负断言零测试。 -- 已盖面(不动):双通道/锁死/公式对拍/树零攻防(`meta_progression_forge_test.gd` :432-455、:476-541);锁定态/两区三行/钮态(`forge_workbench_test.gd` :42-99);长剑价格梯纯函数(`forge_table_test.gd:_test_price_ladder` :91-99);购买链首两级(:411-420)。 -- 剩余缺口:全路线价格梯(非长剑四路线零梯断言)、购买链 45 档、拒购零副作用、祝福件零泄漏、默认值解耦、甲型双轴切换。 - -## 设计契约红线 - -- 🔴 **零行为改动**:本批只增测试断言与 README 能力行;禁触产品码、禁触 data/tables 行值——断言需要观测面而现役无 → 停卡呈报,禁为可测性改产品码(需求改动另走卡)。 -- 🔴 **判据绑可观测提交面**(test-conventions §8.4);禁变异类测试、禁撤销类取证。 -- 🔴 **RNG 单点**:⑤ 集成断言用固定种子经 RngService 封闭流,禁 randomize()/自建 RNG。 -- 🔴 **实现照单落名**:README 代表用例名=测试函数名唯一依据,名实不符=停卡退回写卡段;断言失败消息尾缀〔断言补齐批 Vn〕全仓可 grep。 -- 🔴 **测试削弱申报**:本批不删不改任何既有断言(迭代 48 V1-V3 零触碰);无允许削弱项。 -- 🔴 **Forge 表行值零触碰**(provisional 形态归 M4 数值批)。 - -## 范围边界 - -**做**:13 个新断言函数+2 处既有函数扩展+README 能力行增行与覆盖域表缺口句订正。 - -**不做**:②多语言(P1 功能缺口);③音频(P1 后置);⑤数值落带断言(provisional 归 M4);④死亡灰化/碎裂断言(2026-11-02 用户裁除名,探针+人工目检为终态);Σ×r_boss 运行时公式断言(无运行时承载,不造);满级特效(本期不做);为可测性改产品码(需求改动另走卡)。 - -## 关联文档同步面 - -| 文档 | 关联点 | 同步义务 | -|---|---|---| -| `game/tests/README.md` 覆盖域表 | 断言落地后覆盖状态变化(:180/:182/:184/:194/:200 五行) | 状态列翻转登记进反哺点清单,由用户裁决后归文档批落文(实现批禁写 README);缺口句订正属写卡段维护面,本批当场落 | -| 两份 GDD(难度曲线与Boss/锻造工坊) | 断言绑定其验收标准节/规则节 | 无义务——零触面 | - -## 任务卡 T0..T4 - -**T0 ①增益状态结算链断言(≈0.5 工作日)** - -- [ ] T0-1 `game/tests/unit/contracts/system_contracts_test.gd` 新增四函数:`_test_status_decay_and_clear`〔V1/V2〕(衰减每结算恰 −1、归 0 移除并返回移除数 k;notify_battle_end 后 query_active 零行);`_test_status_stack_full_reject`〔V3〕(满层再授返 false、层数与 remaining 零写入);`_test_status_independent_branch`〔V4〕(直改已加载表 rows 追加 independent 试验行→授予两实例→并存独立结算,测毕还原);`_test_status_snapshot_first_grant`〔V5〕(accumulate 再授只加时长,origin_cell/round_index 沿首授) -- [ ] T0-2 单元套全绿——里程碑步出口跑一次全套闸;波内修复只做出红最小单元的最窄确认 - -**T1 ②设置存储契约断言(≈0.5 工作日)** - -- [ ] T1-1 新建 `game/tests/unit/presentation/settings_store_test.gd` 三函数:`_test_settings_roundtrip`〔V6〕(写 true→存→新实例回读 true,节/键 motion/reduced);`_test_settings_default`〔V7〕(无文件缺省 false);`_test_settings_apply_chain`〔V8〕(reduced→apply()→UiKit.motion_scale==0.5,false→1.0,测毕还原静态初值);隔离卫生:经 retarget() 指向临时 cfg,禁碰真实 user://settings.cfg(ui_click_probe 同款纪律) -- [ ] T1-2 单元套全绿(同 T0-2 措辞) - -**T2 ⑤Boss 门控断言(≈0.5 工作日)** - -- [ ] T2-1 `node_map_generator_test.gd` 新增 `_test_boss_exit_per_region`〔V9〕(真实生成多区域图,每区域终点集恰一 type=="boss"、深度=区域深度 L−r) -- [ ] T2-2 新建 `game/tests/unit/battle_settlement/boss_gate_test.gd`:`_test_boss_node_spawns_boss_rowset`〔V10〕(begin_battle(node_type="boss")→实例化敌人行键全部 row_kind=="boss";对照入参 node_type="battle" 预解析行集零 boss 行;固定种子经 RngService 流口) -- [ ] T2-3 单元套全绿(同 T0-2 措辞) - -**T3 ⑥锻造工坊断言(≈1 工作日)** - -- [ ] T3-1 扩展 `forge_table_test.gd:_test_price_ladder`〔V11〕:六路线全量梯(逐级严格递增、floor 公式按表值推算不写死表值、满级档价=上限档) -- [ ] T3-2 扩展 `meta_progression_forge_test.gd:_meta_pricing_and_buy`〔V12〕:长剑购买链走至 3 级(45 档扣款与余额递减断言) -- [ ] T3-3 新增 `_test_forge_buy_reject`〔V13〕(余额不足与未解锁两路→false,等级与余额零变化) -- [ ] T3-4 新增 `_test_blessing_no_forge_leak`〔V14〕(商店买攻击祝福+护甲祝福→forge_state 投影逐位不变) -- [ ] T3-5 新增 `_test_default_axes_pre_key`〔V15〕(树钥匙未点亮→hero_type_axes 返回 normal/light) -- [ ] T3-6 新增 `_test_armor_switch_dual_axes`〔V16〕(forge_select("heavy")→AC 轴 +2 且挨普通攻击修正 −1;切回 light 复原且等级保留) -- [ ] T3-7 单元套全绿(同 T0-2 措辞)——里程碑步出口跑一次全套闸;波内修复只做出红最小单元的最窄确认 - -**T4 产出物落位卡(≈0.25 工作日)** - -- [ ] T4-1 合并前写齐 `reports/iterN-实现报告-<日期>.md`(各卡状态+遗留读数义务+实现决定记录;本批零实机目检项——纯断言批无玩家可见面) -- [ ] T4-2 合并前写齐 `reports/iterN-反哺点-<日期>.md`(覆盖域表五行状态列翻转与缺口句收敛,逐条写明落点与依据;实现批禁写 design/ 与根规则文件) - -## 验证闸(改动—验收对照表) - -| 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 | -|---|---|---|---|---|---|---| -| 增益衰减逐值与到期移除+战斗结束全清 | system_contracts_test.gd 新增一函数 | `_test_status_decay_and_clear`〔V1/V2〕 | 单元 | StatusContainer.apply_effect()/decay_round()/notify_battle_end() | 返回值+query_active 投影 | 新增:状态效果结算链(逐值衰减/到期移除/战斗结束全清/满层拒绝/独立并存/首授快照) | -| 增益满层拒绝 | 同上文件新增一函数 | `_test_status_stack_full_reject`〔V3〕 | 单元 | StatusContainer.apply_effect() | 返回 false+投影层数/时长零变化 | 新增:状态效果结算链(逐值衰减/到期移除/战斗结束全清/满层拒绝/独立并存/首授快照) | -| 增益独立并存+首授快照 | 同上文件新增两函数 | `_test_status_independent_branch`〔V4〕/`_test_status_snapshot_first_grant`〔V5〕 | 单元 | StatusContainer.apply_effect()+直改已加载表行(试验行) | 返回值+投影快照域 | 新增:状态效果结算链(逐值衰减/到期移除/战斗结束全清/满层拒绝/独立并存/首授快照) | -| 减动效存储回读/缺省/应用链 | settings_store_test.gd 新文件三函数 | `_test_settings_roundtrip`〔V6〕/`_test_settings_default`〔V7〕/`_test_settings_apply_chain`〔V8〕 | 单元 | SettingsStore.load_settings()/save_settings()/apply() | ConfigFile 回读值+UiKit.motion_scale | 新增:减动效开关存储契约(持久化回读/缺省值/引擎动效档应用) | -| 每区域恰一 Boss 终点 | node_map_generator_test.gd 新增一函数 | `_test_boss_exit_per_region`〔V9〕 | 单元 | NodeMapGenerator.generate() | 生成图节点 type/depth 投影 | 新增:Boss 出场门控(每区域恰一 Boss 终点/Boss 节点开战必选 Boss 行集) | -| Boss 节点开战必选 Boss 行集 | boss_gate_test.gd 新文件一函数 | `_test_boss_node_spawns_boss_rowset`〔V10〕 | 集成 | BattleOrchestrator.begin_battle() | 实例化敌人行键 row_kind 投影 | 新增:Boss 出场门控(每区域恰一 Boss 终点/Boss 节点开战必选 Boss 行集) | -| 全路线价格梯+多级购买扣款 | forge_table_test 扩展+meta_progression_forge_test 扩展 | `_test_price_ladder`(扩)〔V11〕/`_meta_pricing_and_buy`(扩)〔V12〕 | 单元 | ForgeTable.level_price()/MetaProgression.forge_buy_level() | 表值推算序列+扣款与余额递减 | 新增:锻造定价与购买闭环(全路线价格梯/多级购买扣款/拒购零副作用) | -| 拒购零副作用 | meta_progression_forge_test 新增一函数 | `_test_forge_buy_reject`〔V13〕 | 单元 | MetaProgression.forge_buy_level() | 返回 false+余额/等级零变化 | 新增:锻造定价与购买闭环(全路线价格梯/多级购买扣款/拒购零副作用) | -| 祝福件零泄漏 | meta_progression_forge_test 新增一函数 | `_test_blessing_no_forge_leak`〔V14〕 | 单元 | 商店购买口(shop_session 两条购买路径) | forge_state 投影逐位不变 | 新增:锻造与相邻通道隔离(祝福件购买锻造零变化/默认轴与钥匙解耦/甲型切换双轴可感) | -| 默认值解耦+甲型双轴 | meta_progression_forge_test 新增两函数 | `_test_default_axes_pre_key`〔V15〕/`_test_armor_switch_dual_axes`〔V16〕 | 单元 | MetaProgression.hero_type_axes()/forge_select() | 返回值+hero_override 投影(AC 轴+matchup 修正) | 新增:锻造与相邻通道隔离(祝福件购买锻造零变化/默认轴与钥匙解耦/甲型切换双轴可感) | - -表外备注:`game/tests/README.md` 五行缺口句订正=非行为改动(写卡段维护面,本批当场落,见「关联文档同步面」节)。 - -## 独立验收审计 - -执行体=迭代 loop 内启动的未参与实现子代理(2026-11-02 用户裁,test-conventions §8.7 默认通道);报告落 `reports/iterN-验收审计-<日期>.md`,逐行写「通过/缺漏/例外及依据」。审计四件事:逐行核对对照表(测试存在、当轮执行、尾缀可 grep、真实入口未绕改动点);抽查判据绑定面;检查测试削弱(本卡申报=无允许削弱项);核对内部字段直查受控例外(本卡零直查行)。未过不得合并。 - -## 异常分级 - -- **L1 自救**:断言消息文本、局部夹具命名、扩展函数内步序。 -- **L2 记录继续**:README 新行在旅程段内的插入行位(系统归属相邻处)——记入实现决定记录。 -- **L3 停机呈报**:①independent 试验行注入不可行→停卡呈报;⑤boss_gate 集成夹具不可行→停卡呈报;⑥甲型双轴观测面不足(AC 修正不随 select 变化)→停卡呈报——观测面不足=上游缺陷,禁自造注入破坏;机检键与本卡契约互斥→停卡退回写卡段。 - -## 代拍板白名单 - -- 测试内局部命名、常量、断言消息文本。 -- 扩展函数内步序编排。 -- README 新行的旅程段内插入行位。 - -## 待裁点 - -零待裁点,开工门=U0。 - -## 落位与可复用性预检 - -**新增文件 2 个:** - -| 文件 | 职责 | 为什么不能复用既有 | -|---|---|---| -| `game/tests/unit/presentation/settings_store_test.gd` | SettingsStore 单元契约(回读/缺省/应用链) | pacing_queue_test 主题是排播节奏(直拨 UiKit 不经 store);ui_click_probe 是探针承载非单元套件(不入 run_all 单元登记);store 单元契约无既有唯一落点 | -| `game/tests/unit/battle_settlement/boss_gate_test.gd` | Boss 节点开战必选 Boss 行集端到端 | node_map_generator_test 只测图结构;config_db_resolver_test/table_structure_guard_test 只测表面路由与带键覆盖;battle_narrator_test 只测呈现投影命名——端到端链(generator→begin_battle→spawn)无既有承载 | - -**复用既有(全部扩展,不新增文件):** system_contracts_test.gd(增益状态单元契约既有宿主)、forge_table_test.gd/meta_progression_forge_test.gd(锻造既有宿主)、node_map_generator_test.gd(图结构既有宿主)。 - -**唯一面**:每条断言唯一函数承载;README 新行唯一;各新函数复用所在文件既有夹具(harness 不外散);零产品码触面。 - -**分层与交互**:全部新件在测试层(probe/sim/tests 不在 S9 扫描面,测量面直构惯例);Core 域断言经领域类公开方法进出(StatusContainer/NodeMapGenerator/MetaProgression/ForgeTable);集成断言实例化真实宿主链(battle_orchestrator),禁裸替身。 - -## 实现规范预算 - -- 新增两文件均为单元测试宿主:公开面仅 ` _init`(各文件 ≤6 个私有断言函数、零公开方法增长、成员 ≤5)——远低于 gdstyle 默认阈值(公开方法 ≤20/成员 ≤15)。 -- 存量触面:产品码零触面,S13 基线键零新增;扩展的四份存量测试件不入 gdstyle 基线(测试面直构惯例),无对账键。 - -## 拆分边界纪律 - -本卡非拆分/重构类(纯测试增补),不适用拆出单元条款;零删码,无「为机检保留无效成员」面。 diff --git a/reports/需求卡片-架构审查3.1+3.2整改批-2026-11-02.md b/reports/需求卡片-架构审查3.1+3.2整改批-2026-11-02.md deleted file mode 100644 index dc9a6b7c..00000000 --- a/reports/需求卡片-架构审查3.1+3.2整改批-2026-11-02.md +++ /dev/null @@ -1,460 +0,0 @@ -# 需求卡片:架构审查 3.1+3.2 整改批(2026-11-02) - -来源:待办清单「架构审查优化讨论——全库架构审计读数的逐条裁决与整改分批(2026-10-29 登记,来源=审计发现)」条目 3.1+3.2 部分(**唯一事实源**:reports/处置-架构审查3.1+3.2整改-2026-11-02.md 总判据六条+裁决台账+设计方案甲~庚+取证事实 18 处现址表;设计闭合凭据=该处置记录+五份已落文 GDD,worktree 提交 15352170,文档域闸全绿 cb92ab10)。 -状态:已写卡(v1.2,2026-11-03 成文;v1.1=grilling 对抗复核 9 阻断+10 建议全采纳修订;v1.2=终审核 3 阻断+1 建议折入——测试适配面闭合判据+S9 参数化泛型形态)。改动面:游戏代码+工具代码(sim 面随批,用户裁完整性优先)。 - -## 1. 目标与完成定义 - -**这批做什么**:把 2026-10-29 全库架构审计 §3.1(数值单一事实源被 18 处硬编码击穿)与 §3.2(sim 与 core 四组平行实现)的已裁决事项全部落码——七条设计方案: - -| 方案 | 内容 | 一句话 | -|---|---|---| -| 甲 | 死效果通道退役 | EFFECT_KINDS 摘除 overflow/attack_segments 两名+两处分支删除,50/2/3 兜底随之消失 | -| 乙 | 天赋树规则键参数字典 | 六规则键值从开关 1 改参数字典,效果引擎零字面量,economy_spend 折价自 shop 表收回 | -| 丙 | 关卡节点图参数入表+域并单点 | fork_third_way_pct/fill_rest_first_pct/width_max 入 level_gen_params 逐关行;生成器侧域校验删除并回 schema | -| 丁 | 经济三件 | shop 货架位数 shelf_size 入表、variation pack_min 入表+表头三域校验、feat cap 改必填 | -| 戊 | 出生区入表 | board_gen 表头 spawn_edge 入表,SPAWN_AREA_CELLS 常量退役,几何与护栏全由表值导出 | -| 己 | sim 信息面出口 | MinesweeperRules 新增 preview_reveal 只读口;auto_player 补 0 格连锁算子经真实闭包 | -| 庚 | 驱动循环收敛 | battle_driver.drive_round 升级通用签名(策略回调+日志回调),level_autoplay 驱动循环抄本删除;_pick_token 改读 TIER_RANK | - -外加:A类① 双写 4 处(main.gd 提示文案两处、playtest_logger ap_per_round、config_db_resolver clamp 域);A类② 入表与兜底全删;sim 组 A/B/D 落地、组 C 零改动只确认、组 E 零动作。 - -**什么状态算完**:上表七方案与 A类① 的每一条改动都落码、都有改动—验收对照表(第 8 节)配对的验收断言在当轮真实跑批中通过;代码域闸全绿(冒烟+单元+探针+渲染/静态);改前改后读数对拍零非预期漂移(行为零变化项逐位同,己案预期漂移项按第 5 节申报);两份 reports/ 产出物(实现报告+反哺点清单)写齐。 - -## 2. 设计事实源 - -- **处置记录(输入合同,逐条一致)**:`reports/处置-架构审查3.1+3.2整改-2026-11-02.md`——总判据六条(§一)、裁决台账(§二)、设计方案甲~庚(§三)、取证事实 18 处现址表(§四)、GDD 落文凭据(§五)、不在本卡范围(§六)。 -- **五份已落文 GDD(设计事实源)**: - - `design/gdd/systems/跨局循环/天赋树.md` v0.12——「规则键参数表」节=乙案字段权威(六键参数名与取值域); - - `design/gdd/systems/冒险循环/关卡节点图生成.md` v1.4——丙案三参数 owner(调参表三行); - - `design/gdd/systems/冒险循环/冒险结构.md` v1.4——丁① 货架位数 owner(调参表「货架位数」行); - - `design/gdd/systems/排雷循环/地形变异.md` v1.2——丁② pack_min owner(调参表「连通片下限」行); - - `design/gdd/systems/排雷循环/棋盘生成.md` v1.2——戊案出生区边长 owner(调参表「出生区边长」行+布雷公式指针形态)。 -- **审计背景**:`reports/审计-全库架构审查-2026-10-29.md`(行号已漂移,以处置记录取证现址与本卡第 3 节实查为准)。 - -## 3. 现状事实(2026-11-03 worktree 分支 arch-review-decisions 实查,开工自行复核) - -取证表 18 处现址逐条复核结果(与处置记录差异处已标注): - -| # | 现址(实查) | 值 | 处置方案 | -|---|---|---|---| -| 1 | core/battle_orchestrator.gd:655-656 | `if tree_rules.has("economy_hoard"): drop = int(round(float(drop) * 1.5))` | 乙 | -| 2 | core/node_map_generator.gd:380 | `rg.randf() < 0.35`(岔口第三去路) | 丙 | -| 3 | core/node_map_generator.gd:455 | `rg.randf() < 0.5`(恢复/奖励交替起始骰) | 丙 | -| 4 | core/node_map_generator.gd:361 | `clampi(int(round(branch_density*L)), 1, 24)`(上限 24 不可达) | 丙 | -| 5 | core/effect_engine.gd:154 | `"do": {"kind": "shield", "value": 1}`(posture_bulwark) | 乙 | -| 6 | core/effect_engine.gd:160 | `"do": {"kind": "damage_mult", "value": 50}`(sense_sharp) | 乙 | -| 7 | core/effect_engine.gd:159/162/169/172 | `"value": 2`(两键 cap_per_level ×4 行) | 乙 | -| 8 | core/damage_controller.gd:412 | `int(h.get("value", 50))`(overflow 死通道兜底) | 甲 | -| 9 | core/board_installer.gd:125 | `stream.randi_range(2, int(...pack_max))`(成包下界) | 丁② | -| 10 | core/settlement/combat_segment.gd:63 | `mini(int(h.get("cap", 2)), ... / maxi(1, int(h.get("divisor", 3))))`(attack_segments 死通道;处置记录写 :64,实查 :63——漂移 1 行) | 甲 | -| 11 | core/shop_session.gd:155 | `if shelf.size() >= 3:`(货架位) | 丁① | -| 12 | core/curio_deck.gd:98-106 | `var cap := 99` 死变量+`row.get("cap", 1)`/`else 1` 双兜底 | 丁③ | -| 13 | core/config_db_resolver.gd:118-119 | `clampf(..., 0.1, 10.0)`;失实「GDD Edge Case」注释实查在 :110(处置记录写 :25——漂移;difficulty_table.gd schema 内同款失实注释同族) | A类① | -| 14 | presentation/playtest_logger.gd:16 | `var ap_per_round := 8`(三副本另两处=action_economy.tres=8、action_economy_table.gd schema `= 6`) | A类① | -| 15 | core/node_map_generator.gd:12 | `const MAX_WIDTH := 4` | 丙 | -| 16 | core/node_map_generator.gd:76-99 | `check_params` → `_cp_domain`(域校验 :76-99;`_cp_structural` :104-122 可行性判定 schema 无此判定、**保留**) | 丙 | -| 17 | core/board_data.gd:9 | `const SPAWN_AREA_CELLS := 9`(被 board_gen_table.gd:77/:106 反向引用=跨目录回边) | 戊 | -| 18 | presentation/main.gd:633 与 :636 | 「弃图刷新需 8」「每轮 2 次」(处置记录写 ~:783-790,实查 :633/:636——iter46/48 漂移,正确读表先例在同函数 :625/:629/:664) | A类① | - -其他现状事实(写 T 卡依据): - -- **tree_rules 流**(乙案改造路径):meta_progression.gd:388-401 `tree_rule_keys()` 返回键数组 ← main.gd:198 注入参数面 ← adventure_runner.gd:39/:126/:166/:361(`Array[Variant]`+`is Array` 守卫)→ level_runner.gd:94/:542/:664-665/:733 → battle_orchestrator.gd:351-353 传 `battle_fx_host.gd:182-183`(`rebuild_subscriptions(feats: Array, attunement: Array, tree_rules: Array)` 第三参 typed Array——乙案必须同步改 Dictionary,漏改即每次 begin_battle 运行期崩)→ effect_engine.gd:103-107/:137-139 `_push_tree_rule(rk)` 四分支(:145-174,economy 两键零合成行不入 match)硬编码参数;economy 两键零订阅行,消费点=battle_orchestrator.gd:654 与 shop_session.gd:58-61(`db.run.shop.spend_discount`=0.8,shop.tres:96)+:333 `_rules() -> Array`。meta_tree.tres 三 Fork 行 `effects_a/b` 现为开关值 1。测试侧键数组注入:feat_vouchers_test.gd:168 `["sense_guard"]`、meta_progression_forge_test.gd:737 `["economy_spend"]`、refresh_feats_test.gd:65、round_flow_skill_pool_test.gd:997/1016/1055、effect_port_test.gd:36、trigger_cond_test.gd:116、treasure_effects_test.gd:321(全 `[]` 或键数组);effect_words_t1_test.gd:62/76/111/129 四处 `rebuild([], [], [])`;tree_rules_v11_test.gd:40-77 夹具镜像硬编码值;meta_progression_forge_test.gd:312 `tree_rule_keys()`。sim 侧:adventure_monte_carlo.gd:61-65 三档键数组注入(db 在 :67 才加载——注入序需调整)、level_autoplay.gd:162-163 透传。 -- **甲案死通道**:EFFECT_KINDS(effect_engine.gd:28-66)含 `"attack_segments"`(:29)与 `"overflow"`(:29);`_vtr_do_keys`(effect_engine.gd:430-443)对 do.kind 强校验 EFFECT_KINDS——词表摘名即加载致命。全库 .tres 零行使用此两名(grep 实核)。overflow 施加面=damage_controller.gd:400-415 `_kill_overflow`(调用点 :394,:359 注释提及);attack_segments 消费面=combat_segment.gd:62-70(source_class 名 `fx_streak_segments`,:66 开票)。断言面 grep 全量:tests/unit/core/adventure_feats_test.gd:743 用 kill 事件载荷键 `"overflow": 0`(**事件数据键,非效果类型词**);tests/unit/battle_settlement/round_flow_skill_pool_test.gd:442 排除表含 `"fx_streak_segments"`、:423/:448 注释提「连珠矢」。effect_engine.gd:464「任意真族」注释列 overflow。 -- **戊案面**:SPAWN_AREA_CELLS 引用共 5 处——board_generator.gd:19(target_mine_count 静态公式)、board_data.gd:9(定义)、board_gen_table.gd:77/:106/:107(schema 反向引用);测试引用 4 处——tests/unit/core/board_gen_reward_test.gd:204/:212/:217/:218、board_generator_test.gd:71。board_data.gd:71-79 `spawn_cells()` 硬编码 `range(-1, 2)` 3×3 几何;board_generator.gd:148-152 `_shave_ring` 硬编码 `<= 1 + shave_r`(1=(3−1)/2);board_gen.tres 现无表头字段(:5 起直接 rows)。board_gen_table.gd 已有零哨兵先例:无 @export 初始化即 0 出域。board_generator.gd:24-25 注释声明 db 鸭子型(测量桩注入);sim/calibrate_boards.gd:10-19 FakeDB 桩 `_FakeBoardGen` 只暴露 `resolve_row`,:45-54 rd 行字典透传表行字段(shave/r0_cap 两参数已有同款透传先例)。 -- **丁案面**:shop_table.gd:16 `spend_discount: float = 0.8`+validate :134 域校验;零哨兵先例同文件 price_a/price_b(`= 0.0`,"0 = 未投值即致命域")。variation_table.gd 表头三字段 percent_of_cells/per_level_bonus/pack_max 均 @export 带**非零默认**(5/2/4)且 validate() 零表头校验(grep 实核)。feat_table.gd:41 cap 可选(`if row.has("cap") and ... < 1`),feats.tres 37 行仅 7 行写 cap;config_db_cross_validator.gd:91 注释已登记 cap ∈ [1,99]。curio_deck.gd:30-39 `build()` 的 relic 令牌 feat_id 直源自 feats 活行(常态悬空不可达,断点续玩跨版本时可达)。 -- **庚案面**:battle_driver.gd 全文 38 行——`drive_round(settlement, still_running: Callable = Callable())`(:17)单轮循环:guard=GUARD_ACTIONS(400)、策略=AutoPlayer.choose_dig(plan 一次算定、move+dig 合步提交、move 后查相位/轮次变更即弃 dig)、轮末统一 stop;四调用方全两参形态——dump_battle.gd:30、monte_carlo.gd:74、damage_probe.gd:102/:346、skill_rhythm_probe.gd:58(升级后零改动)。level_autoplay.gd:521-568 `_autoplay_battle(runner, settlement, st, logger)` 外层 round_guard=60 轮循环+内层 action_guard=400/rejects<3 单轮循环,策略=`_decide_act`(:618-624 三档分派 perfect/adv 失误注入/novice),提交=`_submit_intent`(:626-631,kind seal→settle_batch 动作名 seal_place,logger.log_action 带轮号),refresh 走 `player_request_refresh()`,计数 st.rounds/refreshes/seal_placed。logger 会话装配序=先 `start_session` 后设 `ap_per_round`(run_level :39-42、run_adventure :117-120——R5 删默认后守卫即拒、两处会话静默不开,须注入序前置)。测试直接调用 `_autoplay_battle(r, b, st, logger)` 13 处(adventure_feats_test×4、level_shop_economy_test×9)——签名不变则零波及。`_pick_token`(:292-318)字面量 rank match(legendary 4/rare 3/excellent 2/common 1,未知档 0);单点常量=data/schemas/treasure_item_table.gd:44 `TIER_RANK`;消费先例=meta_progression.gd:499。damage_probe.gd 复算面(:196-201 一带)维持,头注自陈「诊断工具·非验收件」,run_all.gd 与 run_all_gates.sh 零引用(grep 实核)。 -- **己案面**:minesweeper_rules.gd `_chain_closure`(:372-384)与 `_chain_reveal_order`(:386-399)私有;唯一公开揭示口 settle_batch(:215)带扣费与事件;`_classify`(:243-262)已示范「挖封印格同口径」模拟封印集形态(sim_seals.erase(c))。auto_player.gd 全 static 无持久成员,查询面=is_revealed/has_seal/reveal_record/current_board/compute_move_path;`_deduce`(:114-152)single+subset 不动点,缺 0 格连锁算子(core 侧 board_generator.gd:204-215 `_apply_chain_rule` 有——口径差异=unknown 不含 safe_set oracle,:80-83 注释在账);novice 档 `_deduce_single`(:55-85)单规则画像。 -- **组 D 面**:experiment_density 引用 6 处——sim/board_generator.gd 无(注释在 **core**/board_generator.gd:23)、data/tables/board_gen.tres:414 调参注释、game/README.md:53 工具行、game/tests/README.md:304 在册行(写卡段已删,见第 7 节 T9)、审计报告两处(过程件不动)+文件本体与 .uid。 -- **能力清单**:`game/tests/README.md` 现行 30 行能力表+覆盖域表在役;本批增改行见第 7 节 T10 与第 8 节对照表「能力行」列。 - -## 4. 设计契约(违反即返工红线) - -- 🔴 **逐条一致**:本卡一切内容与处置记录逐条一致;实现段发现卡面与处置记录冲突=上游缺陷,停卡退回写卡段(L3)。 -- 🔴 **零缺省**(总判据 2):本批新入表字段全部必填、无缺省——表行缺字段=加载期报错;代码禁默默顶默认值。schema 侧落法=@export 零哨兵(int 0 / float 0.0)+validate 域校验出域即拒(shop_table price_a/price_b 先例)。 -- 🔴 **死代码不保留**(总判据 3):甲案两名摘除后禁留兼容别名、禁留注释死分支、禁保留指向死通道的载荷键与排除表成员。 -- 🔴 **sim 零规则副本**(总判据 4):庚案收敛后 `level_autoplay.gd` 禁再持第二轮驱动循环;己案后 sim 禁自算连锁闭包——一律经 `preview_reveal`。 -- 🔴 **数值单一事实源**:本批触碰的每处字面量按处置方案入表或读单点;效果引擎落成后全文件零玩法数值字面量(六键参数全读节点行)。 -- 🔴 **行为零变化面逐位同**:甲/乙/丙/丁/戊/庚六案的现行值迁移(150/1/50/2/0.35/0.5/4/9/3/2/1/80/3/8/2/1.5)与己案的 preview 口径,除己案 auto_player 算子补齐(预期漂移、第 5 节申报)外,全部同种子逐位回归——冒烟与既有测试套零非预期转红;MC 旧锚不重跑(第 5 节)。 -- 🔴 **Core 零 UI 依赖不破**:minesweeper_rules.preview_reveal 为纯查询口(零状态写入、零事件发射);所有改动件保持 `--headless --script` 可跑通。 -- 🔴 **fail-fast**:数据校验错误一律致命中止,无软回退;新增校验全落 schema validate()(加载期)。 -- 🔴 **S9 参数化形态**(终审 B12):本批全部新增/改型注解一律参数化泛型(`Dictionary[Variant, Variant]`/`Array[Variant]`,code-conventions §4.12 现行口径)——裸 `Dictionary`/`Array` 注解=S9 R1A 红;参数禁缺类型(R3,`_init` 豁免除外);db 参鸭子型沿 board_generator.gd:24 先例(sim 面不在 S9 扫描面)。 -- 🔴 **拆分边界纪律**(结构类卡必填,判据权威=code-conventions §1.1): - - 本批收敛类改动两处,逐件职责语句:①`BattleDriver.drive_round` 通用签名——职责语句「单轮挖掘动作循环的唯一实现:按调用方策略取动作、按统一规则提交、按统一口径记日志与拒绝计数」;单一变化原因=驱动循环语义变化;集体身份=战斗驱动循环单点(battle_driver.gd 文件头自陈身份随之更新)。②节点图域校验并单点——职责语句「参数档合法性判定唯一在 LevelGenParams.validate(加载期)+生成器只留可行性判定 _cp_structural(schema 无此判定的生成器职责)」;单一变化原因=参数域变化;集体身份=参数档校验面(schema 域校验+生成器可行性两级各管一段,禁第三份)。 - - 「过阈值」不构成任何一件的拆分或收拢动机;按纪律拆不出内聚单元 → 停卡呈报走豁免/还债通道,禁硬拆凑绿(写进第 9 节 L3)。 - - 禁为机检文本保留无效成员(第 13 节条款全卡适用)。 - -## 5. 范围边界 - -**收件**(=处置记录全部事项): -- A类①:#1/#2 main.gd 提示文案两处读表拼接(取证表 #18);#13 config_db_resolver clamp 读 DifficultyTable.MULT_MIN/MULT_MAX+失实注释订正;#14 playtest_logger 删默认改必填注入+action_economy_table.gd schema 默认删除(取证异常清单三副本项,卡裁=删默认)。 -- A类②+B类入表:乙(六键参数字典+economy_spend 收回)、丙(三参数入表+域并单点+死上限删除)、丁(三件)、戊(spawn_edge)、#4 死上限 24 删除。 -- 甲:死效果通道整段退役(EFFECT_KINDS 两名+两分支+兜底 50/2/3+载荷键与排除表成员等死面清理)。 -- sim:组 A(庚 案)、组 B(己 案)、组 C(零代码改动,T10 只写确认项)、组 D(experiment_density 删除+引用面处理);组 E 零动作(calibrate_boards FakeDB 模式维持——戊案连带仅 rd 透传一行,见 T3)。 -- 乙案连带(处置记录未点名但不可避免的编译级波及,全列 T1):tree_rules 面从键数组换代参数字典波及的 core/presentation/sim/tests 全部触点。 - -**不收件**(去向已指认): -- B类 CHARGE_LAYER_CAP 死框架——**待办清单专条**(2026-11-02 登记「EffectPort biz_fact 键 charge_layers——无属主数据游离出领域实体的处置」,涉及迭代 49 端口契约,独立走卡)。本卡零触碰 action_economy.gd。 -- sim 标定常量(失误三概率/选路评分)——用户裁接受现状零动作。 -- 审计 P1/P2 四项(闸面缺口/S13 豁免存废/零测试点名四件/架构文档漂移三处)——留待后续讨论段(待办清单条目「本条未闭合余项」)。 -- damage_probe 复算面——按需诊断件维持,零改动(T10 确认项)。 -- MC 重跑——零收件。己案 0 格连锁补齐改变模拟玩家行为(通关率预期上调、旧 MC 锚漂移):重跑归用户按 `sim-data/README.md` MC 采集档位表发起(一档只能由用户发起);本批只在实现报告登记「锚漂移预期+旧锚失效范围」。 -- 组 C 确认以外的 damage_probe / defense_scan / rhythm_batch 等测量件改版。 - -## 6. 关联文档同步面 - -实现批禁写 `design/` 整个目录、两份根流程清单与 `game/tests/README.md`;下表每条的落文义务全部进 `reports/iter-反哺点-<日期>.md`(N=编排段定),由用户裁决后归文档批落文: - -| 文档 | 关联点(本批实现会改变它的哪条陈述) | 同步义务(反哺点清单逐条承载) | -|---|---|---| -| design/engineering/architecture/architecture.md | 己案新增 `MinesweeperRules.preview_reveal` 公开只读口(core 公开 API 面);戊案 SPAWN_AREA_CELLS 常量退役(件表/常量登记面如有登记);庚案 battle_driver 通用签名 | 反哺点清单逐条列:件表行增改内容与依据(处置记录己案「architecture.md 件表行随反哺点落文」)——**机械面文档禁直改** | -| design/gdd/systems/跨局循环/天赋树.md v0.12 | 无陈述变化(规则键参数表已是本批字段权威)——若实现段发现参数表与卡面不一致=上游缺陷停卡 | 反哺点清单写「零反哺」确认行 | -| design/gdd/systems/冒险循环/关卡节点图生成.md v1.4 | 无陈述变化(调参表三行已落) | 同上确认行 | -| design/gdd/systems/冒险循环/冒险结构.md v1.4 | 无陈述变化(货架位数行已落 v1.4;「财富之道两参数归天赋树规则键参数表」句已落) | 同上确认行 | -| design/gdd/systems/排雷循环/地形变异.md v1.2 | 无陈述变化(pack_min 行已落) | 同上确认行 | -| design/gdd/systems/排雷循环/棋盘生成.md v1.2 | 无陈述变化(出生区边长行已落 v1.2) | 同上确认行 | -| design/gdd/systems/冒险循环/奇物.md | 丁③ cap 字段清单行(处置记录丁案明示「奇物.md(cap 字段清单行随卡申报反哺)」——feats 行 cap 改必填后,奇物文档对 cap 的现行陈述(若有「可缺省/缺省按 1」类句)需订正 | 反哺点清单列明:奇物.md 中 cap 相关句的现址行号与订正内容 | -| 术语表 | 无新术语(本批沿用处置记录既定用词:参数字典/只读口/驱动循环单点等仓库现行词) | 确认行「零新增术语」 | - -(机检口径:本表同步义务格无一条写「随批」;所有 design/ 与根规则文件目标格的义务都是「反哺点清单落文」。) - -## 7. 任务卡 T0..T10 - -### 卡前·文件结构图 - -行为改动行标【行为】+对照表行号(第 8 节 R 行);非行为改动标【非行为】。测试件全部为随逻辑写/改的测试代码(需求改动合法面)。 - -``` -数据面(表+schema) - game/data/tables/meta_tree.tres 【行为 R1】三 Fork 行 effects_a/b 开关值 1 → 参数字典 - game/data/schemas/meta_tree_table.gd 【行为 R1】RULE_KEY_PARAMS 常量+_validate_rule_key_params - game/data/tables/shop.tres 【行为 R2/R9】表头 +shelf_size=3、-spend_discount=0.8 - game/data/schemas/shop_table.gd 【行为 R2/R9】+shelf_size(零哨兵+[1,6])/-spend_discount 及其域校验 - game/data/tables/level_gen_params.tres 【行为 R7】三行 × +fork_third_way_pct=35/+fill_rest_first_pct=50/+width_max=4 - game/data/schemas/level_gen_params.gd 【行为 R7】ROW_KEYS+三键+三域校验 - game/data/tables/board_gen.tres 【行为 R8】表头 +spawn_edge=3;:414 调参注释改「出处=退役件 experiment_density.gd(git 历史可考)」【非行为】 - game/data/schemas/board_gen_table.gd 【行为 R8】+spawn_edge(零哨兵+[3,5]奇数)+三处引用改表值导出 - game/data/tables/variation.tres 【行为 R10】表头 +pack_min=2 - game/data/schemas/variation_table.gd 【行为 R10】+pack_min([1,pack_max])+表头三字段域校验 - game/data/tables/feats.tres 【行为 R11】30 行补 cap: 1(7 行已写保持) - game/data/schemas/feat_table.gd 【行为 R11】cap 必填+域 [1,99] - game/data/schemas/action_economy_table.gd 【行为 R5】ap_per_round 删默认(零哨兵化) - game/data/schemas/difficulty_table.gd 【非行为】失实「GDD Edge Case」注释订正(同 #13 族) - -core 层 - game/core/effect_engine.gd 【行为 R1/R3】rebuild/_push_tree_rules/_push_tree_rule 参数化(四分支读参数——economy 两键零合成行不入 match);EFFECT_KINDS 摘两名;:464 族注释改【非行为】 - game/core/battle_orchestrator.gd 【行为 R1】tree_rules 收参 Dictionary;:654-656 economy_hoard 读 mult_pct - game/core/battle_fx_host.gd 【行为 R1 连带】:182 rebuild_subscriptions 第三参 typed Array → Dictionary - game/core/shop_session.gd 【行为 R1/R9】_rules() 返 Dictionary;_spend_discounted 读规则键参数;:155 货架位读表 - game/core/adventure_runner.gd 【行为 R1】_tree_rules 换 Dictionary+两处守卫改 is Dictionary - game/core/level_runner.gd 【行为 R1】同上(:94/:664-665/:542/:733) - game/core/meta_progression.gd 【行为 R1】tree_rule_keys() → tree_rules() -> Dictionary - game/core/node_map_generator.gd 【行为 R7】MAX_WIDTH 删、:361 死上限删、:380/:382/:455 三处读表、_cp_domain 删并回 schema - game/core/board_data.gd 【行为 R8】SPAWN_AREA_CELLS 删;spawn_cells() 几何改 spawn_edge 成员导出;compute_r0 同源 - game/core/board_generator.gd 【行为 R8】target_mine_count/_board_params/_attempt 读 spawn_edge;_shave_ring 半径=(edge−1)/2+r;:23 注释改举例名 calibrate_boards【非行为】 - game/core/board_installer.gd 【行为 R10】:125 成包下界读 pack_min - game/core/curio_deck.gd 【行为 R11】:98-106 死变量删+relic 行悬空 push_error+return false(兜底 1 删) - game/core/damage_controller.gd 【行为 R3】_kill_overflow 改名 _kill_effects+overflow 分支删(reveal_tiles 分支保留)+:359 注释同步 - game/core/settlement/combat_segment.gd 【行为 R3】attack_segments 分支删(:59-70) - game/core/config_db_resolver.gd 【行为 R6】:118-119 clamp 读 DifficultyTable.MULT_MIN/MULT_MAX;:110 失实注释订正【非行为】 - game/core/minesweeper_rules.gd 【行为 R14】+preview_reveal(c) -> Array[Vector2i] - -presentation 层 - game/presentation/main.gd 【行为 R1/R4】:198 tree_rules() 调用(:185 注释同步);:633/:636 两处文案读表拼接 - game/presentation/playtest_logger.gd 【行为 R5】:16 删默认=0;start_session 必填注入守卫 - -sim 层 - game/sim/battle_driver.gd 【行为 R12】drive_round 通用签名(策略回调+日志回调+max_rejects;缺省零变化) - game/sim/level_autoplay.gd 【行为 R12/R13】_autoplay_battle 内层委托 drive_round;_pick_token 改读 TIER_RANK;:39-42/:117-120 注入序前置〔R5 连带〕;:162-163 透传守卫 Dictionary〔R1 类型适配·非行为,T10 确认读数〕 - game/sim/adventure_monte_carlo.gd 【行为 R1 连带·对照表落点=R1】三档注入改参数字典(db 加载序前置)+_tree_rule_dict 断言挂 tree_rules_v11_test〔V1〕 - game/sim/auto_player.gd 【行为 R15】_deduce 补 0 格连锁算子经 preview_reveal;口径注释保持【非行为】 - game/sim/calibrate_boards.gd 【行为 R8 连带】rd 透传一行 spawn_edge(读真表头,零字面量) - game/sim/experiment_density.gd 【删除·组D】连同 .uid - -测试件(随逻辑写/改) - tests/unit/core/tree_rules_v11_test.gd 【行为 R1】夹具参数化++_test_rule_key_params 检查段 - tests/unit/core/meta_progression_forge_test.gd 【行为 R1】全文件触点 17 处适配::312 tree_rules()、:322/:330/:340/:342 resume 断言、:608-611 tree_rule_keys() 三行、:632/:644/:661 rebuild 第三参、:695/:725/:755 begin_battle 键数组、:749/:758 encounter 直改、:671/:746 空形态、:737 键数组注入改字典、:739-740 期望算式改 discount_pct 推导(申报 8) - tests/unit/core/feat_vouchers_test.gd 【行为 R1】:168 键数组 → 参数字典+:218/:243/:270 空数组 stale → {} - tests/unit/battle_settlement/refresh_feats_test.gd 【非行为】tree_rules: [] → {} - tests/unit/core/treasure_effects_test.gd 【非行为】:321 tree_rules: [] → {} - tests/unit/battle_settlement/round_flow_skill_pool_test.gd 【非行为 R3】[]→{} ×3+:442 排除表 fx_streak_segments 成员删+:423/:448 注释订正 - tests/unit/battle_settlement/effect_port_test.gd 【非行为】[] → {} - tests/unit/battle_settlement/trigger_cond_test.gd 【非行为】[] → {} - tests/unit/core/effect_words_t1_test.gd 【非行为】rebuild 第三参 [] → {} ×4(:62/:76/:111/:129) - tests/unit/core/adventure_feats_test.gd 【行为 R3】:743 载荷键 overflow 删(断言内容不动)+rebuild 第三参 [] → {} 共 30 处(:592-761 grep 定位逐处)〔形态迁移·申报 9〕 - tests/unit/core/node_map_generator_test.gd 【行为 R7】+_test_gen_params_table_driven 检查段 - tests/unit/core/node_map_growth_test.gd 【非行为】手搭夹具补三新键(结构拦截断言不动) - tests/unit/core/board_generator_test.gd 【行为 R8】+_test_spawn_edge_from_table 段+:71 公式对账改 spawn_edge² - tests/unit/core/board_gen_reward_test.gd 【非行为】夹具表补 spawn_edge=3;:204-218 断言改 spawn_edge² 推导(断言强度不减) - tests/unit/core/level_shop_economy_test.gd 【行为 R2/R9】+_test_shelf_size_from_table+_test_spend_discount_from_rule_key 段+:264 迁移断言删(申报 7) - tests/unit/battle_settlement/variant_placement_test.gd 【行为 R10】+_test_pack_min_from_table 段 - tests/unit/data_config/level_gen_params_test.gd 【行为 R7】_row() 构造器补三新键+负样本夹具同步 - tests/unit/core/table_structure_guard_test.gd 【行为 R7/R10/R11】+_test_level_gen_new_fields+_test_variation_header_domains+_test_feat_cap_required - tests/unit/core/minesweeper_rules_test.gd 【行为 R14】+_test_preview_reveal_readonly 段 - tests/unit/presentation/player_copy_sweep_test.gd 【行为 R4】+_sweep_refresh_toast_no_literals 段 - tests/unit/data_config/effect_kind_retired_test.gd 【行为 R3·新增·负面】甲案词表退役加载致命 - tests/unit/core/playtest_logger_guard_test.gd 【行为 R5·新增】未注入拒绝开会话 - tests/unit/core/battle_driver_drive_test.gd 【行为 R12·新增】drive_round 契约 - tests/unit/core/auto_player_deduce_test.gd 【行为 R15·新增】0 格连锁算子 - tests/run_all.gd 【非行为】manifest +4 行(一负面三常规:effect_kind_retired/playtest_logger_guard/battle_driver_drive/auto_player_deduce) - -文档件 - game/README.md:53 【非行为·组D】工具行删 experiment_density - game/tests/README.md 写卡段已同批增改(能力行新增 10 行+既有行更新 5 处+experiment_density 在册行删 1 行)——实现批禁写,批末只核对表上名字与实际落地一致 -``` - -边界说明:一起改的按职责放一起——表+schema 同卡、消费面同卡、验收随对应卡同轮落地。库内大文件(main.gd/battle_orchestrator.gd)不擅自拆。 - ---- - -**T0 基线读数与机检对账**(估时 0.25–0.5 工作日) -取用:无。产出:基线读数入实现报告(后续卡对拍用);本批全部断言面清点确认。 - -- [ ] S13 对账基线:对结构图全部 core/data/presentation 存量触件(25 件,gdstyle 扫描面内;实触存量生产件共 30 件,其余 5 件在 sim/tests 非扫描面)现场跑 gdstyle 记读数(branches/returns/函数长/成员数)为对账基线——`tools/gdstyle_baseline.txt` 既有登记仅 5 键 3 文件,多数件无登记值可读,现场读数即基线(既有登记 2 件照登对账)——第 14 节「只降不升」以此为准 -- [ ] grep 清点四件:`overflow`/`attack_segments`/`fx_streak_segments`/`experiment_density` 全库命中行号清单落实现报告(甲案与 T9 的改前底账) -- [ ] 跑一次冒烟基线:`python tools/run_smoke.py` 全绿读数落实现报告(后续每卡改后对拍基准;不构成验收,验收归第 8 节对照表) -- [ ] 复核取证表 18 处现址与本卡第 3 节逐条一致(漂移即停卡呈报 L3——上游缺陷退写卡段) - -**T1 乙案·规则键参数字典**(估时 1–1.5 工作日) -取用:无(本卡起点)。产出:tree_rules 参数字典形态全库贯通(后续 T7 的 level_autoplay relic 透传消费此形态)。 - -- [ ] meta_tree.tres 三 Fork 行 effects_a/effects_b 值改参数字典——精确行内容(六键全列,对照天赋树.md v0.12 规则键参数表): - - fork_posture:`"effects_a": {"posture_first_strike": {"ap_bonus": 1}}`、`"effects_b": {"posture_bulwark": {"shield": 1}}` - - fork_sense:`"effects_a": {"sense_sharp": {"halve_pct": 50, "cap_per_level": 2}}`、`"effects_b": {"sense_guard": {"ap_refund": 1, "cap_per_level": 2}}` - - fork_economy:`"effects_a": {"economy_hoard": {"mult_pct": 150}}`、`"effects_b": {"economy_spend": {"discount_pct": 80}}` - - (语义注记:mult_pct=150 与现码 1.5 同值、halve_pct=50 与现码 50 同值、discount_pct=80 与 shop 表 0.8 同值——全部行为零变化) -- [ ] meta_tree_table.gd 新增常量与校验——精确签名: - - `const RULE_KEY_PARAMS := {"posture_first_strike": {"ap_bonus": [1, 4]}, "posture_bulwark": {"shield": [1, 9]}, "sense_sharp": {"halve_pct": [1, 99], "cap_per_level": [1, 9]}, "sense_guard": {"ap_refund": [1, 4], "cap_per_level": [1, 9]}, "economy_hoard": {"mult_pct": [101, 300]}, "economy_spend": {"discount_pct": [1, 99]}}`(键集与取值域逐字=天赋树.md v0.12 规则键参数表) - - 新私有校验段 `_validate_rule_key_params(row: Dictionary[Variant, Variant], nid: String) -> Array[Variant]`:effects/effects_a/effects_b 中凡 RULE_KEYS 成员的值,必须是 Dictionary、键集恰等 RULE_KEY_PARAMS[键] 的键集(缺参数/多参数即拒)、每值 int 且在闭域内——违例 message 带「缺参数/多参数/越域」判别词 -- [ ] effect_engine.gd:`rebuild(feat_ids, item_ids, tree_rules: Dictionary[Variant, Variant] = {})`;`_push_tree_rules(tree_rules: Dictionary[Variant, Variant])` 逐键调 `_push_tree_rule(rk: String, params: Dictionary[Variant, Variant])`;四分支参数化——posture_first_strike `value: int(params["ap_bonus"])`、posture_bulwark `value: int(params["shield"])`、sense_sharp 两行 when `value: int(params["cap_per_level"])`+do `value: int(params["halve_pct"])`、sense_guard 两行 when `value: int(params["cap_per_level"])`+do `value: int(params["ap_refund"])`(economy 两键零合成行不入 match,维持)——效果引擎零字面量 -- [ ] battle_fx_host.gd:182 `rebuild_subscriptions(feats: Array, attunement: Array, tree_rules: Dictionary[Variant, Variant]) -> void` 第三参类型改(漏改即每次 begin_battle 运行期崩——B1) -- [ ] battle_orchestrator.gd:351-353 收 `Dictionary[Variant, Variant]`;:654-656 改 `var hoard: Dictionary[Variant, Variant] = (_facts.encounter.get("tree_rules", {}) as Dictionary).get("economy_hoard", {})` 命中即 `drop = int(round(float(drop) * float(hoard["mult_pct"]) / 100.0))`(直索引缺键即崩=fail-fast,schema 已保证在键);注释「乘数 provisional 150%」改「乘数读规则键参数 mult_pct」 -- [ ] shop_session.gd:`_rules() -> Dictionary[Variant, Variant]`;`_spend_discounted` 改 `maxi(1, int(floor(float(price) * float(_rules()["economy_spend"]["discount_pct"]) / 100.0)))`(has 判定保留);:50/:56 注释同步 -- [ ] shop_table.gd 删 `spend_discount` 字段+validate :134 域校验;shop.tres 删 :96 行 -- [ ] adventure_runner.gd:39/:126/:166(守卫 `is Dictionary`)/:361+:39 注释同步(树 rule 键集→键集参数字典);level_runner.gd:94/:664-665(类型换 `Dictionary[Variant, Variant]`)/:542/:733+:69/:664 注释同步(tree_rules:Array → 参数字典);main.gd:198 改 `meta_ref.tree_rules()`(:185 注释同步) -- [ ] meta_progression.gd:388-401:`tree_rule_keys()` 改名 `tree_rules() -> Dictionary[Variant, Variant]`(键=规则键,值=激活侧 effects_* 参数字典原样透传,首遇优先去重语义保持) -- [ ] sim 连带:adventure_monte_carlo.gd——db 加载段(:67-70)前移至 feat_profile 构造之前,新增 `static func _tree_rule_dict(db, keys: Array[Variant]) -> Dictionary[Variant, Variant]`(db 参鸭子型沿 board_generator:24 先例——sim 面不在 S9 扫描面;扫 meta_tree Fork 行 effects_a/b,命中 keys 集的键取其参数字典),三档注入改 `{"mode": "off", "tree_rules": _tree_rule_dict(db, [...])}`;level_autoplay.gd:162-163 透传守卫改 `is Dictionary`(类型适配·非行为——透传语义经 T10 MC 冒烟确认读数核) -- [ ] 测试适配一(键注入面与消费面四件):meta_progression_forge_test.gd:312 改 `tree_rules()`+:737 `["economy_spend"]` 改 `{"economy_spend": {"discount_pct": 80}}`+:739-740 期望算式 `float(_db.run.shop.spend_discount)` 改 `float(params 字典 discount_pct) / 100.0` 推导(申报 8——断言强度不减,折后价逐位同值);feat_vouchers_test.gd:168 改 `{"sense_guard": {"ap_refund": 1, "cap_per_level": 2}}`;tree_rules_v11_test.gd 夹具从参数字典构造期望合成行+rebuild 第三参传字典 -- [ ] 测试适配二(空形态与 rebuild 第三参面 11 处):refresh_feats_test.gd:65/round_flow_skill_pool_test.gd:997/1016/1055/effect_port_test.gd:36/trigger_cond_test.gd:116/treasure_effects_test.gd:321/effect_words_t1_test.gd:62/76/111/129 的 `tree_rules` 空数组或 rebuild 第三参改空字典 `{}` -- [ ] 测试适配三(闭合判据=按文件 grep 全量触点,B10/B11):meta_progression_forge_test.gd 全文件 `tree_rules|tree_rule_keys` 触点 17 处(:312/:322/:330/:340/:342/:608/:609/:611/:632/:644/:661/:695/:725/:755/:749/:758/:671/:746/:737 逐一入上两步);adventure_feats_test.gd rebuild 第三参 `[]` → `{}` 共 30 处(:592-761 区间 grep 定位逐处改,断言内容零改动——申报 9);feat_vouchers_test.gd:218/:243/:270 空数组 stale 改 `{}`;收尾核:`grep -c "tree_rule_keys" game/` 全库零命中+`grep -n "tree_rules" game/tests --include="*.gd"` 逐件过目零键数组残留 -- [ ] tree_rules_v11_test.gd 新增 `_test_rule_key_params` 检查段:六键参数域正例全过/缺参数拒/多参数拒/越域拒/非字典拒;并增检 `_tree_rule_dict(db, ["posture_first_strike"])` 返回 `{"posture_first_strike": {"ap_bonus": 1}}`(R1 对照表落点——sim 注入构造面最小断言,B9) -- [ ] 孤立验证新增段 → 出红只跑红件最小单元 → 并入全套(回归措辞:里程碑卡出口跑一次全套闸;卡内修复只做出红最小单元的最窄确认) - -**T2 丙案·节点图参数入表+域并单点**(估时 0.5–0.75 工作日) -取用:无。产出:level_gen_params 三新字段+生成器域校验并单点。 - -- [ ] level_gen_params.gd:ROW_KEYS 追加 `"fork_third_way_pct", "fill_rest_first_pct", "width_max"`;_validate_row_shape 追加三域——fork_third_way_pct float [0,100]、fill_rest_first_pct float [0,100]、width_max int [2,6](域=关卡节点图生成.md v1.4 调参表) -- [ ] level_gen_params.tres 三行各补 `"fork_third_way_pct": 35, "fill_rest_first_pct": 50, "width_max": 4`(percent 形态按 v1.4 调参表域 [0,100]——35/50 与现码 0.35/0.5 同概率语义,消费除 100.0) -- [ ] node_map_generator.gd:删 `const MAX_WIDTH := 4`(:12);:361 `clampi(..., 1, 24)` 改 `maxi(int(round(float(_params["branch_density"]) * float(L))), 1)`(死上限 24 删、下限 1 保留防零岔口与现行为漂移);:380 `< 0.35` 改 `< float(_params["fork_third_way_pct"]) / 100.0`;:382 `>= MAX_WIDTH` 改 `>= int(_params["width_max"])`;:455 `< 0.5` 改 `< float(_params["fill_rest_first_pct"]) / 100.0` -- [ ] node_map_generator.gd:`check_params` 删 `_cp_domain` 调用(改直接 `return _cp_structural(params)`)+删 `_cp_domain` 整函数(:76-99)——域校验唯一落 LevelGenParams.validate(加载期),生成器保留可行性判定 `_cp_structural`(schema 无此判定) -- [ ] node_map_generator_test.gd 追加 `_test_gen_params_table_driven` 检查段:真实表行(row_by_level_index)产图回归同种子逐位;三参数改值夹具(width_max 2→岔口锚点限宽可观测、fork_third_way_pct 0→无三去路图、fill_rest_first_pct 100→填充首节点恒恢复)产出可观测变化断言 -- [ ] table_structure_guard_test.gd 追加 `_test_level_gen_new_fields` 检查段:缺新键行拒(structural 键集闭集)、width_max 越域拒、两 pct 越域拒、表值 35/50/4 全过 -- [ ] tests/unit/data_config/level_gen_params_test.gd:`_row()` 构造器(:24-27)补三新键(`fork_third_way_pct: 35, fill_rest_first_pct: 50, width_max: 4`)——否则 ROW_KEYS 闭集校验拒全部夹具行、:89 起「合法三行」断言整组转红;负样本夹具(bad_keys_row 多塞键等)同步核——多塞键负样本仍须拒、合法行须全过 -- [ ] node_map_growth_test.gd 手搭夹具(:70/:74/:96 一带)补三新键——结构拦截断言与 fatal 子进程不动(回归) - -**T3 戊案·出生区入表**(估时 0.5–1 工作日) -取用:T0 基线读数。产出:spawn_edge 表头单点,SPAWN_AREA_CELLS 退役,跨目录回边(board_gen_table→board_data)解除。 - -- [ ] board_gen_table.gd:`@export var spawn_edge: int = 0`(零哨兵——表头缺字段越域即加载致命);validate 新增域校验 `spawn_edge < 3 or spawn_edge > 5 or spawn_edge % 2 == 0` → domain 错;`_validate_row_constraints` :77 与 `_validate_shave_params` :106-107 的 `BoardData.SPAWN_AREA_CELLS` 全改 `spawn_edge * spawn_edge`(含 :76/:105 注释) -- [ ] board_gen.tres 表头(rows 前)加 `spawn_edge = 3`;:414 调参注释「smoke/experiment_density.gd」改「出处=退役件 experiment_density.gd(git 历史可考)」(不写提交哈希——哈希查 git log,注释只留可读指针) -- [ ] board_data.gd:删 `const SPAWN_AREA_CELLS := 9`(:7-9 注释随行删);新增成员 `var spawn_edge: int = 0`;`spawn_cells()`(:71-79)改 `var half := spawn_edge / 2`+`for dy in range(-half, half + 1)`(spawn_edge=3 时与现 3×3 逐位同——含 in_bounds 过滤);compute_r0()(:98-115)经 spawn_cells 自动同源;:7-8 文件头注释同步 -- [ ] board_generator.gd:`target_mine_count`(:18-20)改 `int(round(float(params.density) * float(int(params.w) * int(params.h) - int(params.spawn_edge) * int(params.spawn_edge))))`;`_board_params`(:36-51)加 `"spawn_edge": int(row.spawn_edge)`(直索引——缺键即崩=fail-fast,schema 与 resolve 注入保证在场);`_attempt`(:103 一带)加 `b.spawn_edge = int(params.spawn_edge)`;`_shave_ring`(:148-152)`<= 1 + shave_r` 改 `<= (int(b.spawn_edge) - 1) / 2 + shave_r`(spawn_edge=3 时逐位同——出生区半径同一事实不二处定义);:22-23 注释「experiment_density.gd 等」改「calibrate_boards.gd」 -- [ ] calibrate_boards.gd(组 E 零动作例外:一行编译级连带)——rd 字典(:45-54 透传段)加 `"spawn_edge": int(db.combat.board_gen.spawn_edge),`(读真表头、零字面量,FakeDB 模式与桩结构不动) -- [ ] 测试:board_generator_test.gd 追加 `_test_spawn_edge_from_table` 检查段(夹具表 spawn_edge=5 → 布雷域基、R₀ 下限、剃雷域半径按 25 格可观测变化;=3 回归逐位同);:71 `from_expr` 对账改 spawn_edge² 推导(夹具 params 补 spawn_edge 键);board_gen_reward_test.gd 夹具表补 `spawn_edge = 3` 成员赋值、:204/:212/:217/:218 的 `BoardData.SPAWN_AREA_CELLS` 改夹具表 `spawn_edge * spawn_edge` 推导(断言强度不减:cap 压线语义保持 9 格) - -**T4 丁案·经济三件**(估时 0.5–1 工作日) -取用:无。产出:shelf_size/pack_min/feat cap 三事实源入表。 - -- [ ] shop_table.gd:`@export var shelf_size: int = 0`(零哨兵)+validate 域校验 `shelf_size < 1 or shelf_size > 6` → domain 错;shop.tres 表头加 `shelf_size = 3` -- [ ] shop_session.gd:155 `if shelf.size() >= 3:` 改 `>= int(db.run.shop.shelf_size)` -- [ ] variation_table.gd:`@export var pack_min: int = 0`(零哨兵)+validate 新增 `_validate_header_domains` 段——pack_min ∈ [1, pack_max]、percent_of_cells ∈ [0, 50](地形变异.md v1.2 调参表)、per_level_bonus ∈ [0, 9]、pack_max ∈ [pack_min, 9](owner 域表述对齐;现表头三字段零校验补齐);variation.tres 表头加 `pack_min = 2` -- [ ] board_installer.gd:125 `stream.randi_range(2, ...)` 改 `stream.randi_range(int(db.combat.variation.pack_min), ...)`(pack_min=2 时逐位同) -- [ ] feat_table.gd :41 cap 校验改必填:`if not row.has("cap")` → domain 错「缺 cap(必填——零兜底)」;`elif int(row.get("cap", 0)) < 1 or > 99` → domain 错(域 [1,99]=config_db_cross_validator.gd:91 在册口径) -- [ ] feats.tres 30 行补 `cap: 1`——批量手法:python 脚本逐行在 `rarity` 行后插 `cap: 1`(仅对无 cap 行;7 行已写保持原值),脚本用后即删(吸收即死);校验=feat_table.validate 全过+`grep -c 'cap' feats.tres` = 37 -- [ ] curio_deck.gd:98-106:删 `var cap := 99` 死变量;relic 分支改 `var row := db.crossrun.feats.row_by_id(feat_id)`+`if row.is_empty(): push_error("[奇物牌库] relic 令牌引用悬空 feat_id=%s(feats 表无此行——配置违约,禁兜底)" % feat_id); return false`,在场行 `return taken.get("relic:" + feat_id, 0) < int(row["cap"])`(直索引——schema 必填保证在键) -- [ ] table_structure_guard_test.gd 追加 `_test_variation_header_domains`+`_test_feat_cap_required` 检查段(域正反例+feats.tres 全行 cap 在场断言);variant_placement_test.gd 追加 `_test_pack_min_from_table` 检查段(夹具 pack_min=3 → 布置产物零 1–2 格连通片可观测断言,真实入口 begin_battle 布置段);level_shop_economy_test.gd 追加 `_test_shelf_size_from_table` 检查段(夹具 shelf_size=5 → 货架五件可观测,真实入口 shop 货架构建)+:264 `spend_discount == 0.8` 迁移断言行删除(申报 7——字段随 T1 退役,表值迁移断言使命终结,等价判据由 `_test_spend_discount_from_rule_key`〔V2〕承接) - -**T5 甲案·死效果通道退役**(估时 0.75–1 工作日) -取用:T0 grep 底账。产出:EFFECT_KINDS 闭面收窄,两名误写加载致命(负面套件)。 - -- [ ] effect_engine.gd EFFECT_KINDS(:29)摘 `"attack_segments"` 与 `"overflow"` 两名;:464「任意真族」注释删 overflow 成员 -- [ ] damage_controller.gd:`_kill_overflow`(:400-415)改名 `_kill_effects(t: Dictionary, target: Dictionary, landed: int) -> void`——`overflow` 分支(:404-415:`if String(h.get("kind", "")) != "overflow": continue` 起至 open_ticket/入队/递归段)删除,reveal_tiles 分支(斩首学者)保留;调用点 :394 改名;:359 注释「击杀发射面拆 _kill_overflow」同步;kill 事件载荷键 `"overflow": landed` 删除(死通道整体退役——全库零订阅行消费此键,grep 实核) -- [ ] combat_segment.gd::59-70 attack_segments 消费段整删(注释 :59-61+for 循环 :62-70)——cap 2/divisor 3/value 0 兜底随之消失;fx_attack_hits 载荷键自此零写零读 -- [ ] 测试清理:adventure_feats_test.gd:743 dispatch 载荷删 `"overflow": 0` 键(断言内容「斩首学者击杀揭示 1 格」不动);round_flow_skill_pool_test.gd :442 排除表删 `"fx_streak_segments"` 成员、:423/:448 注释删「连珠矢」句(段内序断言不动) -- [ ] 新增负面套件 tests/unit/data_config/effect_kind_retired_test.gd(extends SceneTree,沿 counter_prefix_fatal_test.gd 既有形态:user:// 夹具目录复制 feats.tres → 注入 do.kind="overflow" 探针行与 do.kind="attack_segments" 探针行各一 → `ConfigDB.load_tables` → 两探针均被 validate_trigger_row 拒绝(EFFECT_KINDS 出域)→ 违规被放行即 `quit(2)`;全拒 `quit(1)` 语义与既有负面件一致——运行命令注释在文件头) -- [ ] run_all.gd manifest 追加:`{"path": "res://tests/unit/data_config/effect_kind_retired_test.gd", "expect": 1, "negative": true}, # 甲案词表退役——overflow/attack_segments 误写加载致命〔架构整改〕` - -**T6 A类①·双写四处置**(估时 0.5–0.75 工作日) -取用:无。产出:提示文案与 clamp 域读单点,logger 必填注入。 - -- [ ] main.gd:633 `"...(弃图刷新需 8)" % (cost - rap)` 改 `"...(弃图刷新需 %d)" % [cost - rap, cost]`(cost=:629 表读值);:636 `"本轮刷新次数已用完(每轮 2 次)"` 改 `"本轮刷新次数已用完(每轮 %d 次)" % int(db.run.action_economy.refresh_cap_per_round)`(同函数 :625 正确读表先例同款) -- [ ] config_db_resolver.gd:118-119 两处 `0.1, 10.0` 改 `DifficultyTable.MULT_MIN, DifficultyTable.MULT_MAX`(单点=data/schemas/difficulty_table.gd:9-:10);:110 注释「(GDD Edge Case——组合事故非配置事故)」失实句删(GDD 无此节);difficulty_table.gd 同款失实注释(mult_oor_warning 校验处)同步订正 -- [ ] level_autoplay.gd:39-42 与 :117-120 注入序前置——`logger.ap_per_round = int(db.run.action_economy.ap_per_round)` 移到 `logger.start_session(seed_val)` 之前(R5 删默认后守卫拒后开=两处会话静默不开) -- [ ] playtest_logger.gd:16 `var ap_per_round := 8` 改 `var ap_per_round: int = 0`(零缺省);`start_session`(:24)开头加守卫 `if ap_per_round < 1 or ap_per_round > 32: push_error("[试玩日志器] ap_per_round 未注入或越域: %d——组合根必填注入" % ap_per_round); return ""`(:9 返回契约注释同步;_append 空文件守卫已兜住未开会话的 log_* 调用——实核 :172-176) -- [ ] action_economy_table.gd:9 `@export var ap_per_round: int = 6` 删默认(`@export var ap_per_round: int`——零哨兵,表行缺字段越域 [1,32] 加载致命;action_economy.tres 已显式写 8,加载零影响) -- [ ] 新增 tests/unit/core/playtest_logger_guard_test.gd:未注入调 start_session → 返回空串+零文件产出断言;注入 8 → 返回非空路径断言(真实入口=PlaytestLogger.start_session) -- [ ] run_all.gd manifest 追加:`{"path": "res://tests/unit/core/playtest_logger_guard_test.gd", "expect": 0}, # R5 必填注入守卫〔架构整改〕` -- [ ] config_db_resolver_test.gd `_test_difficulty_clamp` 回归(clamp 值 0.1/10.0 不变,来源换常量) - -**T7 庚案·驱动循环收敛+档位排序读表**(估时 0.75–1 工作日) -取用:无。产出:drive_round 驱动循环单点(缺省零变化),level_autoplay 抄本删除。 - -- [ ] battle_driver.gd `drive_round` 升级通用签名——精确签名与契约: - ``` - static func drive_round(settlement, still_running: Callable = Callable(), - act_provider: Callable = Callable(), on_action: Callable = Callable(), - max_rejects: int = 0) -> void - ``` - - act_provider 契约:`func(settlement) -> Dictionary`——返回 `{"kind": "stop"}` 或空字典=停手出环;单步 `{"kind": "move"|"dig"|"seal"|"refresh", "cell": Vector2i}`(refresh 无 cell);合步 `{"kind": "move", "cell": X, "next": {"kind": "dig", "cell": Y}}`(缺省策略适配 AutoPlayer.choose_dig 原生 plan 专用——move 提交后查相位/轮次、变更即断弃 next,与现 :30-33 逐位同) - - 提交分派:move/dig/seal→`minesweeper.settle_batch([{...}])`(seal 提交动作名 seal_place);refresh→`settlement.player_request_refresh()` - - on_action 契约:`func(submit_kind: String, cell: Vector2i, ok: bool, round_index: int)`——submit_kind ∈ {move, dig, seal_place, refresh, stop};轮末统一 stop 也经此回调(accept 取 outcomes[0].accepted);空 Callable=不记日志 - - 循环条件:现 :22-23 四项(活性谓词/Active 相位/同轮/guard>0)+`(max_rejects <= 0 or rejects < max_rejects)`;guard 语义保持「策略回调调用次数 ≤ GUARD_ACTIONS」;ok 时 rejects=0 否则 +1 - - 缺省形态(act_provider 空)=现 :24-34 内联块逐位:`AutoPlayer.choose_dig` plan 一次算定、hero_cell 异位先 move、合步 next=dig、plan 空 break——四调用方(dump_battle:30/monte_carlo:74/damage_probe:102/:346/skill_rhythm_probe:58)两参调用零改动 -- [ ] level_autoplay.gd `_autoplay_battle`(:521-568):外层 round_guard=60 轮循环与 st.rounds 计数保留(属调用方);内层单轮循环整体删,改调 `BattleDriver.drive_round(settlement, func() -> bool: return runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round, _act_provider(st), _action_logger(st, logger), 3)`——新增两个静态适配器:`_act_provider(st) -> Callable`(闭包捕获 st:`func(s) -> Dictionary: return _decide_act(st, s)`——策略分派 perfect/novice/slip 全保留在 _decide_act/_apply_normal_slips 原地);`_action_logger(st, logger) -> Callable`(闭包捕获:`func(kind, cell, ok, ri): if logger != null: logger.log_action(kind, cell, ok, {"round": ri}); if ok and kind == "refresh": st.refreshes += 1; elif ok and kind == "seal_place": st.seal_placed += 1`——计数语义与现 :553-561 逐位同);轮末 stop 日志与现 :566-568 同构 -- [ ] level_autoplay.gd `_pick_token`(:292-318):字面量 match rank 表删,改 `var rank: int = int(TreasureItemTable.TIER_RANK.get(String(t.get("tier", "")), 0))`(未知档 0=现 match 落空同语义;316-f 最左 tie-break 逻辑不动) -- [ ] 新增 tests/unit/core/battle_driver_drive_test.gd `_smoke_drive_round_contract` 检查段:①缺省两参调用——同种子同 settlement 下动作序列快照与改前逐位对拍(手搭 settlement 桩沿 seal_polarity_test.gd:240 状态袋先例);②provider 形态——策略回调产出 dig/move/seal_place 各一验证提交与日志回调收到(kind,cell,ok,round)四元组;③max_rejects——策略恒返回被拒动作时第 3 拒后停手出环;④合步 next——move 后相位变更即弃 dig -- [ ] run_all.gd manifest 追加 battle_driver_drive_test(expect 0);adventure_feats_test/level_shop_economy_test 现有 `_autoplay_battle` 调用面回归(签名未变零改动) - -**T8 己案·sim 信息面出口**(估时 0.5–1 工作日) -取用:无。产出:core 只读揭示口+auto_player 0 格连锁算子。 - -- [ ] minesweeper_rules.gd 新增公开只读口——精确签名与语义: - ``` - ## 挖一格→真实翻开集只读预览(sim 信息面出口——与 _classify 同口径:本格封印视为已隐式退役、 - ## 其余封印照挡;复用 _chain_closure 闭包;零状态写入、零事件发射) - func preview_reveal(c: Vector2i) -> Array[Vector2i]: - ``` - 实现:`sim_seals := _seals.duplicate(); sim_seals.erase(c)` → `_chain_closure(c, sim_seals)` → 取闭包中 `cc != c and not _revealed.has(cc)` 的格(F2 口径:新开格集,不含首格与已开格) -- [ ] auto_player.gd `_deduce`(:114-152)补 0 格连锁算子:while-changed 每轮 single+subset 扫描后追加一段——遍历 safe 集中未揭示格,逐格 `for rc in minesweeper.preview_reveal(c): if not safe.has(rc): safe[rc] = true; changed = true`(连锁只经安全格扩散=闭包集全安全,真实玩家挖 c 免费获得的信息面;`_deduce_single` novice 画像不动);:80-83「口径差异=unknown 不含 oracle」注释保持,双口径显式登记补一句「0 格连锁经 preview_reveal 真实闭包(架构整改批补齐——与棋盘生成 _apply_chain_rule 对位)」 -- [ ] 新增 tests/unit/core/auto_player_deduce_test.gd `_smoke_zero_chain_rule` 检查段:手搭棋盘夹具——0 数字区域经一格桥接,远端格与任何已开数字格不邻接(改前约束模型必不证其安全)→ `AutoPlayer._deduce` 后 safe 集含远端格断言;零盲挖语义回归(mines 集不含安全格);真实入口=AutoPlayer._deduce(经 perfect_action 装配链取 minesweeper 实例) -- [ ] minesweeper_rules_test.gd 追加 `_test_preview_reveal_readonly` 检查段:同板同格断言 `preview_reveal(c)` = `settle_batch(dig c)` 后 `revealed_cells()` 差集 ∖ {c}(F2 同口径——不含首格与已开格;封印格在场/挖封印格两形态各一例);调用前后 revealed_cells() 与 seal_count() 零变化(零状态写入);cell_revealed 事件零发射 -- [ ] run_all.gd manifest 追加:`{"path": "res://tests/unit/core/auto_player_deduce_test.gd", "expect": 0}, # R15 0 格连锁算子〔架构整改〕` -- [ ] MC 锚漂移登记:实现报告写明「0 格连锁补齐后 perfect/adv 档通关率预期上调、旧 MC 锚失效;重跑归用户按 MC 采集档位表发起」 - -**T9 组D·experiment_density 退役**(估时 0.25–0.5 工作日) -取用:T0 grep 底账。产出:死测量件清出,引用面归零。 - -- [ ] 删 game/sim/experiment_density.gd 与 experiment_density.gd.uid -- [ ] game/README.md:53 工具行删 experiment_density 引用(`dump_battle.gd(结算流水)/ monte_carlo.gd(战斗期望)` 保留) -- [ ] core/board_generator.gd:23 注释举例名已于 T3 改 calibrate_boards(本卡核对零残留);board_gen.tres:414 注释已于 T3 改「出处=退役件 experiment_density.gd(git 历史可考)」(本卡核对零残留) -- [ ] 全库 grep `experiment_density` 零命中(审计报告 reports/ 两处除外——过程件不动) -- [ ] game/tests/README.md:304 在册行——写卡段已删(见下);实现批批末核对该行确已不在 - -**T10 组C 确认+产出物落位卡**(估时 0.5–0.75 工作日) -取用:全部前卡。产出:批收口两件。 - -- [ ] 组 C 确认项(零代码改动):grep `damage_probe` 于 tests/run_all.gd 与 tools/run_all_gates.sh 零引用;damage_probe.gd 头注「诊断工具·非验收件」自陈在位——两项读数落实现报告 -- [ ] 实机目检总清单(登记不阻塞):①RAP 不足 toast「弃图刷新需 N」显示表值(改 rap_refresh_cost 表值目检文案随动)②本轮刷新用尽 toast「每轮 N 次」同上③散折价 80% 购买价目检(economy_spend 激活,回归性质) -- [ ] 遗留读数义务登记:MC 锚漂移预期(T8)+重跑归用户档位发起 -- [ ] 确认读数:level_autoplay.gd:162-163 透传守卫(R1 类型适配·非行为,B9 申报)——MC 冒烟跑一次 run_adventure relic 模式(tree_rules 字典透传)零报错即透传生效,读数落实现报告 -- [ ] **实现报告** `reports/iter-实现报告-<日期>.md`:各卡状态+实机目检总清单+遗留读数义务+实现决定记录(代拍板白名单逐条回填)+S13 键改前改后对账表(T0 现场基线 25 件逐件核——baseline 既有登记 2 件照登,第 14 节承诺逐件核) -- [ ] **反哺点清单** `reports/iter-反哺点-<日期>.md`:第 6 节同步面表逐条落点(architecture.md 件表行内容、奇物.md cap 句订正、五份 GDD 零反哺确认行、术语零新增确认行) -- [ ] 批末核对:game/tests/README.md 能力行与实际落地名实一致(改名则回表);独立验收审计(见第 8 节末)未过不合并 -- [ ] .uid 收口(终审 S11):`--editor --quit` 一次后确认 4 件新测试件 .uid 已生成随批提交(仓库 .uid 全量入库先例——防合回主区长期 untracked);删除件 experiment_density.gd.uid 确已随 T9 移除 - ---- - -### 估时汇总 - -T0 0.25–0.5|T1 1–1.5|T2 0.5–0.75|T3 0.5–1|T4 0.5–1|T5 0.75–1|T6 0.5–0.75|T7 0.75–1|T8 0.5–1|T9 0.25–0.5|T10 0.5–0.75。 -区间合计 6.0–9.75 工作日;**精确汇总 8 工作日**(切分预案:若 T1 连带测试适配超 1.5 工日,把 sim 连带两件〔adventure_monte_carlo/level_autoplay 透传〕切出为 T1b 独立半日卡,其余不动;合计超 10 工作日=超软盒,须用户特批标记后方可续批)。 - -## 8. 验证闸(改动—验收对照表) - -测试方式两分法:下表全部走**调试接口验证**(单元/集成套直构夹具与真实入口调用,沿现役 DebugOverlay/DS_DEBUG 门控先例不适用——本批零 UI 装配面);**正常流程体验验证**=实机目检三项(T10 清单,登记不阻塞)。判据全部绑被测改动特有的可观测提交面(合成行参数值/表值驱动的产物差异/驱动序列快照/揭示集逐位);纯静态判据两处(R6/R13)已注记观测面局限。 - -| 需求点 | 改动(文件:行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 | -|---|---|---|---|---|---|---| -| R1 六键参数字典 | meta_tree.tres 三 Fork 行;meta_tree_table.gd 校验;effect_engine 四分支读参;battle_fx_host.rebuild_subscriptions 收 Dictionary;battle_orchestrator/shop_session 消费;meta_progression.tree_rules();adventure_runner/level_runner/main 传参;sim 注入面(adventure_monte_carlo db 前移+_tree_rule_dict+三档换字典;level_autoplay :162-163 透传守卫·类型适配) | tree_rules_v11_test.gd:_test_rule_key_params〔架构整改 V1〕(含 `_tree_rule_dict(db, ["posture_first_strike"])` 最小断言);既有 _smoke_tree_rules/feat_vouchers R2 面回归 | 单元 | `MetaTreeTable.validate()`/`EffectEngine.rebuild()`/`MetaProgression.tree_rules()`/`AdventureMonteCarlo._tree_rule_dict()` | 合成行 do.value=参数字典值(非硬编码);缺参数/越域 validate 返回非空;tree_rules() 返回字典含激活侧参数;_tree_rule_dict 返回 {规则键: 参数字典} | 新增:规则键参数读表(六键参数字典校验与激活侧透传);既有:天赋树购买与六键合成 | -| R2 散折价收回规则键 | shop.tres/-spend_discount;shop_table.gd 删字段;shop_session._spend_discounted 读 discount_pct | level_shop_economy_test.gd:_test_spend_discount_from_rule_key〔架构整改 V2〕 | 单元 | `shop_session.current_price()`(经 LevelRunner 装配链,economy_spend 注入) | 80% 折价值逐位同(表字段退役前后对拍);shop.tres 加载零诊断(字段真删) | 既有:GP 钱包/escrow/商店收口/共享池 | -| R3 死效果通道退役 | effect_engine.gd EFFECT_KINDS 摘两名;damage_controller overflow 分支删;combat_segment attack_segments 分支删;载荷键/排除表成员清理 | effect_kind_retired_test.gd:_assert_rejected〔架构整改 V3〕(负面套件 expect=1 negative=true);adventure_feats_test :743 面与 round_flow 段内序回归 | 单元(负面独立进程) | `ConfigDB.load_tables()`(夹具注入探针行) | 两 kind 探针行加载被拒(EFFECT_KINDS 出域报错文本);round_flow 五段全序断言零转红 | 既有:加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 | -| R4 刷新提示文案读表 | main.gd:633/:636 两处拼接读表 | player_copy_sweep_test.gd:_sweep_refresh_toast_no_literals〔架构整改 V4〕+实机目检两项(T10) | 单元(静态扫描)+目检 | `player_copy_sweep` 源扫描(现役 sweep 机制) | 两 toast 格式串零数字字面量、插值位与表字段一一对应(静态判据+评审注记观测面局限:真机文案由目检兜) | 新增:刷新提示文案读表拼接(RAP 不足/轮上限两 toast 零字面量) | -| R5 ap_per_round 必填注入 | playtest_logger.gd 删默认+start_session 守卫;action_economy_table.gd 删 schema 默认;level_autoplay.gd:39-42/:117-120 注入序前置(先设 ap_per_round 再 start_session——守卫拒后开=会话静默不开) | playtest_logger_guard_test.gd:_smoke_required_injection〔架构整改 V5〕 | 单元 | `PlaytestLogger.start_session()` | 未注入返回空串+零文件;注入后返回路径;action_economy.tres 缺该字段夹具 validate 非空 | 新增:试玩日志器 ap_per_round 必填注入守卫(未注入拒绝开会话) | -| R6 clamp 域读单点 | config_db_resolver.gd:118-119 读 MULT_MIN/MULT_MAX;:110 与 difficulty_table.gd 失实注释订正 | config_db_resolver_test.gd:_test_difficulty_clamp(既有回归)+静态判据:resolver 源零 0.1/10.0 字面量(评审注记) | 单元+静态 | `ConfigDbResolver.resolve_difficulty_modifiers()` | clamp 输出域 [0.1,10.0] 逐位同(常量来源切换零值变) | 既有:行集寻址、行序保序与难度乘数 clamp | -| R7 节点图三参数入表+域并单点 | level_gen_params.gd/tres;node_map_generator.gd 四处读表+_cp_domain 删 | node_map_generator_test.gd:_test_gen_params_table_driven〔架构整改 V6〕;table_structure_guard_test.gd:_test_level_gen_new_fields〔架构整改 V7〕;node_map_growth_test 回归 | 单元 | `NodeMapGenerator.generate()`/`LevelGenParams.validate()` | 三参数改值产物可观测变化;缺键/越域 validate 拒;同种子产图回归逐位同 | 新增:节点图参数读表与域单点(三参数驱动产物/生成器侧可行性保留) | -| R8 出生区边长入表 | board_gen.tres+spawn_edge;board_gen_table.gd 零哨兵+三处导出;board_data.gd 常量删+几何导出;board_generator.gd 读表+剃雷域导出;calibrate_boards 一行透传 | board_generator_test.gd:_test_spawn_edge_from_table〔架构整改 V8〕;_test_target_mine_count_single_source 更新回归;board_gen_reward_test 夹具回归 | 单元 | `BoardGenerator.generate()`/`BoardGenTable.validate()` | spawn_edge=5 夹具布雷域基/R₀ 下限/剃雷域半径按 25 格可观测;spawn_edge=3 逐位回归;BoardData.SPAWN_AREA_CELLS 全库零引用 | 新增:出生区边长表单点(布雷域基/R₀ 下限/剃雷域半径全由 spawn_edge 导出);既有:棋盘按参数行出图、数字恒真 | -| R9 货架位数入表 | shop.tres+shelf_size;shop_table.gd 零哨兵+[1,6];shop_session.gd:155 读表 | level_shop_economy_test.gd:_test_shelf_size_from_table〔架构整改 V9〕 | 单元 | `shop_session` 货架构建(经 LevelRunner 装配链) | 夹具 shelf_size=5 → 货架五件可观测;=3 回归逐位同 | 新增:货架位数读表(进店与重抽同源) | -| R10 变异成包下界入表+表头域校验 | variation.tres+pack_min;variation_table.gd 零哨兵+三域校验;board_installer.gd:125 读表 | variant_placement_test.gd:_test_pack_min_from_table〔架构整改 V10〕;table_structure_guard_test.gd:_test_variation_header_domains〔架构整改 V11〕 | 单元 | `BattleOrchestrator.begin_battle()`(变异布置段)/`VariationTable.validate()` | 夹具 pack_min=3 → 产物零 1–2 格片;pack_min 越域/三表头字段越域 validate 拒 | 既有:变异布置门控;新增:变异表头域校验(地形变异) | -| R11 feat cap 必填 | feat_table.gd 必填+[1,99];feats.tres 30 行补 cap:1;curio_deck.gd 死变量删+悬空报错 | table_structure_guard_test.gd:_test_feat_cap_required〔架构整改 V12〕;adventure_feats_test relic 面回归 | 单元 | `FeatTable.validate()`/`curio_deck.cap_ok()` | 缺 cap 行拒/越域拒/37 行全在场;cap_ok 在场行直索引零兜底(悬空 push_error 路径断言) | 既有:表结构守卫:域校验/行序键/跨字段互锁/配额锚 | -| R12 驱动循环收敛 | battle_driver.gd 通用签名;level_autoplay._autoplay_battle 内层委托;抄本删除 | battle_driver_drive_test.gd:_smoke_drive_round_contract〔架构整改 V13〕;adventure_feats_test/level_shop_economy_test _autoplay_battle 面回归 | 单元+集成 | `BattleDriver.drive_round()`(两参缺省与五参回调两形态) | 缺省两参动作序列快照逐位同;provider 形态提交与日志回调四元组;max_rejects=3 第三拒停手;合步 next 相位变更弃 dig | 新增:sim 驱动循环单点(缺省零变化与策略/日志回调形态) | -| R13 档位排序读表 | level_autoplay._pick_token 改读 TreasureItemTable.TIER_RANK | adventure_feats_test 316-f 选件既有断言回归+静态判据:_pick_token 源零 rank 字面量(评审注记) | 单元+静态 | `level_autoplay._pick_token()`(经 run_adventure 三选一链) | 最稀有优先/最左 tie-break 逐位同(读表前后对拍) | 既有:奖励三选一候选/刷新付费/呈现 | -| R14 棋盘揭示预览只读口 | minesweeper_rules.gd +preview_reveal | minesweeper_rules_test.gd:_test_preview_reveal_readonly〔架构整改 V14〕 | 单元 | `MinesweeperRules.preview_reveal()` | preview(c) = settle_batch 挖 c 后 revealed_cells 差集 ∖ {c}(F2 同口径——不含首格与已开格,封印在场/挖封印格两形态同);调用前后 revealed_cells/seal_count 零变化;cell_revealed 零发射 | 新增:棋盘揭示预览只读口(零状态写入/与实挖揭示集同口径);既有:挖/封印合法性与可辨拒绝(拒绝零副作用) | -| R15 0 格连锁算子经真实闭包 | auto_player.gd _deduce 补算子 | auto_player_deduce_test.gd:_smoke_zero_chain_rule〔架构整改 V15〕 | 单元 | `AutoPlayer._deduce()`(经 perfect_action 装配链) | 桥接 0 区域远端格入 safe 集(改前必缺);mines 集零安全格(零盲挖语义保持) | 新增:模拟玩家 0 格连锁算子(经真实闭包,零盲挖语义保持) | - -**零行为改动面的证据义务(不设对照表行的申报)**:上表 R1–R15 覆盖本卡全部带运行期提交面的需求点。两项零行为改动——组 D(experiment_density 删除+引用面清理)与组 C(damage_probe 零改动确认)——无运行期提交面、无能力行可锚,验收=静态判据与确认读数(全库 grep 零命中/闸脚本零引用/头注自陈在位),证据落 T9/T10 步骤与实现报告,由独立验收审计按第 8 节末口径核读数;不为其虚设对照表行与能力行。 - -**闸口径**:重闸=`tools/run_all_gates.sh` 按本批改动域选档(改动含 game/ 下非 .md 行为载体 → `--code`;文档件触面并入同一合并验证——两类都有按闸脚本算法定档);所选域全绿才合并。卡内不写死闸件名单。回归措辞:里程碑卡(T 段)出口跑一次全套闸;卡内修复只做出红最小单元的最窄确认;新增测试/工具先行孤立验证再并入全套。 - -**测试削弱申报**(允许改/删的旧断言全清单;未申报的删改由独立验收审计判红): -1. tree_rules_v11_test.gd 夹具 synth 硬编码值(1/50/2)改参数字典构造——断言结构不变(四键行数对照+validate_trigger_row 零违例),值来源从字面量改参数(R1 的被测面本身)。 -2. board_gen_reward_test.gd:204-218 的 `BoardData.SPAWN_AREA_CELLS` 引用改夹具表 spawn_edge² 推导——cap 压线 9 语义不减(R8)。 -3. board_generator_test.gd:71 from_expr 公式对账改 spawn_edge²——对账强度不减(R8)。 -4. adventure_feats_test.gd:743 dispatch 载荷删 `"overflow": 0` 键——断言内容(斩首学者揭示 1 格)零松动(R3)。 -5. round_flow_skill_pool_test.gd:442 排除表删 `"fx_streak_segments"` 成员——排除面缩窄至死通道不可达成员,段内序断言零松动(R3)。 -6. node_map_growth_test.gd 手搭夹具补三新键——结构拦截与 fatal 断言零松动(R7)。 -7. level_shop_economy_test.gd:264 `spend_discount == 0.8` 迁移断言行删除——字段随 R2 退役后无可断言对象;表值迁移断言的历史使命终结,等价判据(折价 80% 行为)由新增 `_test_spend_discount_from_rule_key`〔V2〕承接,验收强度不减(R2)。 -8. meta_progression_forge_test.gd:739-740 期望值算式 `float(_db.run.shop.spend_discount)` 改经规则键参数 `discount_pct / 100.0` 推导——断言对象从表字段换规则键参数,折后价期望逐位同值,断言强度不减(R1)。 -9. adventure_feats_test.gd:592-761 rebuild 第三参 `[]` → `{}` 共 30 处+meta_progression_forge_test.gd:632/:644/:661 同款 3 处——typed 形参迁移的形态适配,断言内容与期望值零改动(R1)。 -以上 9 条均为「随被测改动同步的夹具/引用形态迁移」,无一条删除断言或放宽阈值(第 7 条删的是断言对象已消失的迁移断言,行为判据由同值新断言承接)。 - -**独立验收审计**:入口=批末 T10 后、合并前;报告=`reports/iter-验收审计-<日期>.md`;由没参与实现的会话或子代理执行,核对:对照表逐行断言真实存在且在当轮跑批通过、测试削弱申报 9 条之外零删改、第 13 节无效成员零残留、S13 键只降不升对账。未过不合并。 - -## 9. 异常分级 - -- **L1 自救**:测试断言笔误、夹具键名拼写、注释错字——就地改,记录进实现报告。 -- **L2 记录继续**:非行为漂移类小事——表注释与本卡行号 ±3 行内的漂移;测试失败经排查属既有缺陷且与本批改动无因果——登记跳过并报用户。 -- **L3 停机写报告等用户**:①卡面与处置记录/五份 GDD 冲突(上游缺陷,退写卡段);②取证表 18 处现址复核不符(同上);③乙案连带触点发现处置记录未列的第五处以上消费面(行为语义不明,禁代裁);④机检预算互斥=新增件超第 14 节预算且无例外申报=上游缺陷,停卡退回写卡段,禁实现段自行拆件或砍面;⑤拆分边界按第 4 节纪律拆不出内聚单元(凑计数袋禁)=停卡呈报走豁免/还债通道;⑥schema 校验与卡片契约互斥(含第 13 节机检键冲突)=一律停卡呈报,不得自造第三条路;⑦己案 preview 口径与「settle_batch 实挖揭示集差集 ∖ {c}」(B6 定死的 F2 同口径)出现非预期不一致。 - -## 10. 代拍板白名单 - -- 测试内部夹具的命名与组织(状态袋字段、夹具目录名、探针行 desc 文案)。 -- toast/注释中文措辞的微调(数值与字段名不动)。 -- validate 错误 message 的具体文案(判别词「缺参数/多参数/越域/出域」必须保留在 message 内供断言定位)。 -- 表行插入的物理顺序与对齐(.tres 字段顺序不改变语义)。 -- feats.tres 批量补 cap 的脚本实现方式(用后即删)。 -- 测试函数内部辅助函数的拆分(不新增公开面)。 -- 逐卡内「孤立验证→出红最小单元→并入全套」的执行顺序细节。 -超清单的决断记入实现报告「实现决定记录」节。 - -## 11. 待裁点 - -**零待裁点,开工门=U0**——设计方案甲~庚与全部取证处置均已在 2026-11-02 讨论会话由用户逐项裁决(处置记录 §二/§三「已裁或按总判据导出,零待裁点」),五份 GDD 落文闭合。本卡未新增任何备选案。 - -## 12. 落位与可复用性预检 - -**新增文件(4 件,全部测试件)**: -- tests/unit/data_config/effect_kind_retired_test.gd——为什么不能复用:甲案词表退役的负面断言必须独立进程断言退出码(test-conventions §5.6,负面套件禁落现役非负面套件——headless assert 错误扫描假红);counter_prefix_fatal_test.gd 是 feats.tres 注入探针的同形态先例,**复用其夹具模式但不并件**(该件断言的是 counter 前缀与引用闭合,注入探针内容与本件不同,并件会让负面 manifest 语义混乱)。唯一落点:data_config 目录(夹具对象=feats.tres 加载路径);grep 同名零命中。 -- tests/unit/core/playtest_logger_guard_test.gd——playtest_logger 现无任何测试件(grep 零命中);守卫行为是 R5 特有提交面,无法挂靠既有套件的既有能力行。唯一落点 tests/unit/core。 -- tests/unit/core/battle_driver_drive_test.gd——drive_round 契约测试需手搭 settlement 桩与动作序列快照对拍;smoke 族共用面(battle_driver 头注声明)明确「smoke_all 本体不经本面、含回归断言禁并入」——契约测试落单元层新件,不污染 smoke。状态袋复用 seal_polarity_test.gd 的手搓先例模式(引用步骤,不复制代码)。 -- tests/unit/core/auto_player_deduce_test.gd——0 格连锁算子需特构棋盘夹具(0 区域+桥接格),adventure_feats_test 的冒险装配链夹具过重且断言面不同。唯一落点 tests/unit/core。 - -**新增函数/常量(逐条唯一性核查)**: -- `MinesweeperRules.preview_reveal`——grep `preview` 于 minesweeper_rules.gd:现公开面只有 fx_reveal(带状态写入的事件版,:355)——两者语义不同(零写入 vs 真挖),不能复用;命名与 effect_engine PREVIEW 主动件无冲突(跨类)。core 公开 API 净增 1 口,反哺点清单承载。 -- `MetaTreeTable.RULE_KEY_PARAMS`+`_validate_rule_key_params`——schema 内唯一;仓库无第二份参数域表(天赋树.md 规则键参数表=owner,schema=机械承载,两处同源不违规:文档定域、schema 执行)。 -- `MetaProgression.tree_rules()`——`tree_rule_keys()` 改名而非并存(禁别名,第 13 节);grep 全库唯一调用点 main.gd:198+测试两处,全量适配。 -- `LevelAutoplay._act_provider`/`_action_logger`——level_autoplay 文件内静态适配器,唯一;通用形态只在 battle_driver.drive_round 一处。 -- `AdventureMonteCarlo._tree_rule_dict`——sim 内唯一;不散落(level_autoplay 透传不重建字典)。 -- `BoardData.spawn_edge` 成员——唯一出生区边长实例承载;表头单点 board_gen_table.spawn_edge 经行快照注入,无第二处几何定义(`_shave_ring` 半径推导同源)。 -- `VariationTable._validate_header_domains`——表头域校验唯一段;不与行校验混写。 - -**复用既有(不新增)**:负面套件形态=counter_prefix_fatal_test 模式;驱动桩=seal_polarity_test 状态袋模式;能力清单挂靠=既有行追加用例为主(8/16 行新增能力仅 6 行全新);TIER_RANK/TIER 词表/状态袋/gdstyle 基线全部既有件。 - -**分层与交互面**:sim→core 单向(auto_player→preview_reveal;battle_driver→settle_batch/player_request_refresh);core→data schema 方向(config_db_resolver→DifficultyTable.MULT_MIN——与既有 db.combat.* 消费同向,禁反向);presentation→core 只经既有出口(main.gd 读 db.run.action_economy 现役口径不变);RngService 红线零触碰(本批零新增随机源——variation/board_gen 参数全为读表非取数);Core 零 UI 红线零触碰(minesweeper_rules 新口纯查询)。 - -## 13. 禁为机检文本保留无效成员 - -本卡为重构类卡,全卡适用以下条款(code-conventions §1.1 同款):禁止为迎合机检消息文本(含计数、message 判别词、grep 面)而保留或新增无效成员/别名/死分支;`tree_rule_keys()` 改名后旧名禁留兼容壳;EFFECT_KINDS 摘名后禁留注释化成员;`_kill_overflow` 改名后旧名禁留转发壳;`_cp_domain` 删除后禁留空壳调用。机检键与卡片契约互斥时(如 pipeline_gate/gdstyle 消息判据与本卡签名冲突),实现段一律停卡呈报,不得自造第三条路。 - -## 14. 实现规范预算 - -新增 .gd 文件仅 4 个测试件(SceneTree 测试脚本,无公开方法面、无常量成员超限面——gdstyle 对测试件的接口块预算全部余量充足,逐件实数:公开方法 0、类变量 0–2)。新增生产代码为零新文件(全部落在既有件),按「存量触面一查」逐件对账: - -- 接口块预算(改动后公开方法数 ≤20 / 成员变量 ≤15):minesweeper_rules.gd 公开方法 +1(现数远低于 20,T0 读数确认);board_data.gd 成员 +1(spawn_edge);battle_driver.gd 公开方法 +0(单函数签名扩展);meta_tree_table.gd 常量 +1(RULE_KEY_PARAMS——常量不计类变量阈值);其余件公开面零净增(改名/收参不增数)。 -- 单函数分支 ≤8:`_push_tree_rule` 四分支 match(economy 两键零合成行不入 match)——参数化后分支数不变为 4;`drive_round` 分支数:5 kind 分派+合步 next+守卫 ≈7——在限内,T0 读数锁定基线;`_validate_rule_key_params` 分支 ≤6。 -- 迁移函数对账:本批无「自存量文件迁入」函数(改名件 `_kill_overflow`→`_kill_effects` 按同键迁移查 `tools/gdstyle_baseline.txt`——源键 branches/returns 超登记值即随迁拆函数,拆分不改语义)。 -- 存量触面 S13 预期:本批实触存量生产件 30 件,其中 gdstyle 扫描面(core/data/presentation)内 25 件为对账面——S13 键较 T0 现场基线**只降不升**(net 删代码为主:两死分支/域校验函数/字面量表;净增面仅 preview_reveal 与校验段——降不达即检查是否夹带超预算成员,L3 ④兜底);扫描面外 5 件(sim/tests)随批闸口径另计。 -- 指标例外申报:无(零新件超默认阈值;若实现中发现例外,走本卡异常分级 L3 ④停卡,禁就地豁免)。 - -## 15. 拆分边界纪律 - -见第 4 节末「拆分边界纪律」条款(结构类卡必填,卡内写条款、判据定义以 code-conventions §1.1 为权威):两个拆出/收敛单元(drive_round 通用签名、参数域校验并单点)各带职责语句、单一变化原因与集体身份;「过阈值」只作发现信号;拆不出内聚单元停卡呈报(第 9 节 L3 ⑤)。先例=迭代 41 两卡 v1.2(核心数据面契约 7/呈现层契约 9)。 diff --git a/roadmap.md b/roadmap.md index 3d322a76..83f1d566 100644 --- a/roadmap.md +++ b/roadmap.md @@ -5,8 +5,8 @@ ## 当前位(只回答「现在该做什么」;三行内随批次换代,禁止在此扩写成状态快照) - **阶段**:M3——Alpha 版本的完整实现(2026-10-09 立规:未明令宣布完成前停留 M3,迭代只排 M3 需求,M4 不排期;根目录 stage.txt 为旧阶段开关仅存档)。 -- **迭代位**:迭代 1–49 全部已完成——迭代 49(效果端口与掷格分件+模态句柄分域+营地五界面真窗口化三卡)2026-11-02 合回(合并提交 549683a5,独立验收审计通过〔缺漏 0、例外 1 在案〕),批后收尾闭合(2026-11-02):反哺点九条处置闭合(architecture v1.31 轴戊两件与功能层四行换代已建件现状〔EffectPort 六 Callable 路由/RewardRoller 掷格面迁出、B4 公开面 weighted_pick 退役、roll_board 装配序五步、五件类变量实数在案〕+ui_registry 行三域句柄分域换代〔run/menu 两子容器新建、ModalFlow 升格注册面直驻、寻址 `modals.run.*`/`modals.menu.*` 换代〕/术语表 v1.26 立「效果端口件」「掷格件」两条〔用户裁〕/camp.md v2.6 叠层与开合补窗壳形态已实装注记/08-Alpha v1.25 三·界面营地行补窗壳形态在役注记/体验验证清单 ㊽ 登记目检两项/static_scan.sh S8 头注计数句订正 10→14)、产出物退役六件(三需求卡片+反哺点+实现报告+验收审计——吸收即死+证据只留最新)、待办清单三卡条目整条删除(状态机出口)。迭代 48 收尾记录见 git 提交 aa396eb9,迭代 47 见 bd54d1f4。 -- **下一步**:迭代 50 编排待发令(`/iter-plan`)——候选=断言补齐批卡(V1–V16 四域断言补齐,已写卡 2026-11-02)。待编池:冒险前资源配比预设(冻结备选)、迭代 30 原卡②③(2026-10-17 已剔出迭代 30 规划、退回待设计,待美术/设计定稿后重新立卡)。 +- **迭代位**:迭代 1–50 全部已完成——迭代 50(敌方状态个体化+断言补齐+架构审查 3.1+3.2 整改三卡)2026-11-03 合回(合并提交 8d3a12ec,独立验收审计两轮终判通过〔首轮缺漏三条→整改笔 6cb1cf5e→复验通过〕),批后收尾闭合(2026-11-03):反哺点十条处置闭合(增益状态 v1.6 删「实装随该批」过时行/architecture v1.32 排雷规则件 preview_reveal 只读预览口登记+天赋树 tree_rules 参数字典 API 换代/奇物 v1.5 cap 必填与取值域写入/覆盖域表减动效行翻已覆盖;SPAWN_AREA_CELLS、shop.spend_discount 表字段、battle_driver 签名三处零登记零落点,天赋树 v0.12 等五份与术语表零反哺确认)、实机目检五项登记体验验证清单 ㊿〔实现报告原编㊾与 CI 迁远端批撞号,改用㊿〕、MC 锚代表性注记落 sim-data/README(重跑归一档用户发起)、产出物退役六件(三需求卡片+反哺点+实现报告+验收审计——吸收即死+证据只留最新)、待办清单三卡条目整条删除(架构审查余项〔审计待裁点④⑤⑥⑦〕按未决先迁再死另立新条登记)。迭代 49 收尾记录见 git 提交 aa396eb9,迭代 48 见 bd54d1f4。 +- **下一步**:迭代 51 编排待发令(`/iter-plan`)。待编池:架构审查余项(闸面缺口/豁免存废/零测试点名/架构文档漂移——待办清单在册,待讨论段逐项裁决)、冒险前资源配比预设(冻结备选)、迭代 30 原卡②③(2026-10-17 已剔出迭代 30 规划、退回待设计,待美术/设计定稿后重新立卡)。 ## 里程碑时间线 @@ -83,7 +83,7 @@ | 迭代 47 | 已完成(2026-11-01) | 已退役(git 历史可考) | 2026-11-01 | ≈15 个工作日〔用户特批 2026-11-01 超 10 个工作日〕 | 已退役(git 历史可考) | | 迭代 48 | 已完成(2026-11-02) | 已退役(git 历史可考) | 2026-11-01 | ≈6.1 个工作日 | 已退役(git 历史可考) | | 迭代 49 | 已完成(2026-11-02) | 已退役(git 历史可考) | 2026-11-02 | ≈7.25 个工作日 | 已退役(git 历史可考) | -| 迭代 50 | 待开工 | reports/需求卡片-敌方状态个体化批-2026-11-02.md
reports/需求卡片-断言补齐批-2026-11-02.md
reports/需求卡片-架构审查3.1+3.2整改批-2026-11-02.md | 2026-11-03 | ≈12.875 个工作日〔用户特批 2026-11-03 超 10 个工作日〕 | — | +| 迭代 50 | 已完成(2026-11-03) | 已退役(git 历史可考) | 2026-11-03 | ≈12.875 个工作日〔用户特批 2026-11-03 超 10 个工作日〕 | 已退役(git 历史可考) | - 迭代 38 收口为带红合并〔用户特批 2026-10-26〕:09 静态扫描 S13 gdstyle 恶化 2 条经批准记入基线存量红(git 提交 c4c69701),卡② 停卡退上游,处方已于 2026-10-26 修订定稿 v1.3(git 提交 b3ea7532),随「gdstyle 存量债全量清偿」条目波 7 待编排。 - 迭代 39 收口为带红合并〔用户特批 2026-10-27〕:02 EV 锚 goblin 0.9148∉[1.00,1.20](触发器批有意行为改动,fp f74b9a48ef4a)已随 EV 锚重锚批换带归绿(git 提交 ba5105aa);08d 生涯.md:55 行号漂移已随 2026-10-28 批后收尾反哺落文修复归绿。三卡拆分后终局 gdstyle 恶化 2 条(battle_orchestrator max-file-length、level_map_screen max-class-variables)归迭代 40 卡①还债与迭代 41 存量债序列。 diff --git a/sim-data/README.md b/sim-data/README.md index 98b3028d..526b0db5 100644 --- a/sim-data/README.md +++ b/sim-data/README.md @@ -7,6 +7,7 @@ - 锚:`runsum_m3iter9exit_81000_n400_20261003.jsonl`(N=400/档=普通 v3.4+perfect;种子 81000 起逐局独立;route=min;taken=hero_dmg+mine_dmg,口径对齐真人分析端) - 读数:`读数_m3iter9exit81000_20261003.md`(k=0.60、完美战役全通 59.0%、结构位移存档) - 换代义务:M-4 数值批投值时本锚随批作废,批内自取改前/改后锚(77000/78000 先例) +- 代表性注记(2026-11-03,迭代 50 合并提交 8d3a12ec):0 格连锁算子补齐改变模拟玩家行为(推理面增强),perfect/adv 档通关率预期上调——本锚读数对现行代码代表性下降,带值结论须注明「旧行为基线」口径;重跑归一档(用户发起),发起时批内自取改前/改后锚 ## 历史锚(只读不判) diff --git a/tools/audit_file_headers.py b/tools/audit_file_headers.py index 5d8b1072..433d0ac7 100644 --- a/tools/audit_file_headers.py +++ b/tools/audit_file_headers.py @@ -59,19 +59,23 @@ def audit_path(path): def main(): + # 扫描面空转自检:目录缺失或零个 .gd 一律判红——原实现 `if not is_dir(): continue` + # 会让路径写错静默零命中换绿,违背 test-conventions §7「零命中自检为辅证」。 + missing = [d for d in SCAN_DIRS if not (ROOT / d).is_dir()] + face = [p for d in SCAN_DIRS for p in sorted((ROOT / d).rglob("*.gd"))] + if missing or not face: + print("[audit_file_headers] FAIL——扫描面为空(缺目录:%s;面清单:%s)" + % ("、".join(missing) if missing else "无", "、".join(SCAN_DIRS))) + return 1 violations = 0 - for d in SCAN_DIRS: - base = ROOT / d - if not base.is_dir(): - continue - for path in sorted(base.rglob("*.gd")): - for lineno, msg in audit_path(path): - print("%s:%d: %s" % (path.relative_to(ROOT), lineno, msg)) - violations += 1 + for path in face: + for lineno, msg in audit_path(path): + print("%s:%d: %s" % (path.relative_to(ROOT), lineno, msg)) + violations += 1 if violations: print("[audit_file_headers] FAIL: 文件头存量 %d 处(存量按 code-conventions §1.4.1 白名单清偿)" % violations) return 1 - print("[audit_file_headers] ok: 头注零黑名单命中(R0 载体/R2 日期/R3 变更叙事)") + print("[audit_file_headers] ok: 头注零黑名单命中(R0 载体/R2 日期/R3 变更叙事;扫描面 %d 个 .gd)" % len(face)) return 0 diff --git a/tools/audit_type_hints.py b/tools/audit_type_hints.py index 968c1d4b..91384022 100644 --- a/tools/audit_type_hints.py +++ b/tools/audit_type_hints.py @@ -78,16 +78,23 @@ def audit_file(path): def main(): if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8", errors="replace") + # 扫描面空转自检:目录缺失或零个 .gd 一律判红——路径写错不得静默零命中换绿 + # (test-conventions §7「零命中自检为辅证」;game/tests/static_scan.sh 前言同款纪律)。 + missing = [d for d in SCAN_DIRS if not (ROOT / d).is_dir()] + face = [p for d in SCAN_DIRS for p in sorted((ROOT / d).rglob("*.gd"))] + if missing or not face: + print("[audit_type_hints] FAIL——扫描面为空(缺目录:%s;面清单:%s)" + % ("、".join(missing) if missing else "无", "、".join(SCAN_DIRS))) + return 1 total = 0 - for d in SCAN_DIRS: - for p in sorted((ROOT / d).rglob("*.gd")): - for line, rule, text in audit_file(p): - print("[%s] %s:%d: %s" % (rule, p.relative_to(ROOT).as_posix(), line, text)) - total += 1 + for p in face: + for line, rule, text in audit_file(p): + print("[%s] %s:%d: %s" % (rule, p.relative_to(ROOT).as_posix(), line, text)) + total += 1 if total: print("[audit_type_hints] FAIL——存量 %d 处(R1 无类型容器/R2 有值返回缺类型/R3 参数缺类型)" % total) return 1 - print("[audit_type_hints] PASS——产品码类型闸归零(R1/R2/R3)") + print("[audit_type_hints] PASS——产品码类型闸归零(R1/R2/R3;扫描面 %d 个 .gd)" % len(face)) return 0 diff --git a/tools/capability_audit.py b/tools/capability_audit.py index 8eb96e9b..8d29555d 100644 --- a/tools/capability_audit.py +++ b/tools/capability_audit.py @@ -10,8 +10,8 @@ r"""capability_audit.py — 能力清单、覆盖域表与 GDD 纯度机检( manifest 内且该文件有 `func 函数名(`,或该文件是 game/probe/ 下的探针件且件内有 `func 函数名(`—— 探针不进 run_all manifest,单独认探针面)或卡在(该文件名或函数名能在 reports/需求卡片*.md 中找到, 供写卡时当场入表的计划用例锚定);两头都无=红(清单挂了无凭证义务)。 - E 真实入口可核(双通道):能力行「真实入口」的每个 `类.方法()`——产品码(game/ 除 tests//probe//sim/) - 有 `func 方法(`,或该方法名能在 reports/需求卡片*.md 中找到(计划入口同 D 口径);两头都无=红。 + E 真实入口可核(双通道):能力行「真实入口」的每个 `类.方法()`——产品码+sim 测量件(game/ 除 + tests//probe/)有 `func 方法(`,或该方法名能在 reports/需求卡片*.md 中找到(计划入口同 D 口径);两头都无=红。 F 覆盖域表:systems 每篇一行;状态∈{已覆盖,部分覆盖,未覆盖};非未覆盖行的「能力行」非空且每项都能 对上能力表里的行名;部分/未覆盖行的缺口非空且指待办清单。 条件闸行豁免(代表用例含「条件闸」字样)不参与 D/E。 @@ -121,10 +121,12 @@ def manifest(): def product_funcs(): + # 「代码在」锚面=产品码(game/ 除 tests//probe/)+sim 测量件:调参测量与模拟驱动件 + # (battle_driver/auto_player 等)是合法的真实入口宿主,卡片退役后其入口只能锚代码面。 names = set() for p in (REPO / "game").rglob("*.gd"): rel = p.relative_to(REPO).as_posix() - if rel.startswith(("game/tests/", "game/probe/", "game/sim/")): + if rel.startswith(("game/tests/", "game/probe/")): continue names.update(re.findall(r"^(?:static\s+)?func\s+([A-Za-z_]\w*)\s*\(", read(p), re.M)) return names @@ -251,7 +253,7 @@ def check_entry(no, name, cell, funcs, cards): for _, method in tokens: if method not in funcs and not (cards and method in cards): red(README.relative_to(REPO), no, "真实入口", - "「%s」方法 %s() 无凭证——产品码无定义且需求卡片未载" % (name[:16], method)) + "「%s」方法 %s() 无凭证——产品码与 sim 测量件无定义且需求卡片未载" % (name[:16], method)) def check_coverage(cov_tables, names): diff --git a/tools/ci/chunk_dl.py b/tools/ci/chunk_dl.py new file mode 100644 index 00000000..79d6e11d --- /dev/null +++ b/tools/ci/chunk_dl.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +r"""chunk_dl.py — 分片并行下载器(断点续传),2026-11-03 CI 迁移批收编。 + +背景:GitHub 直连在本项目两台机器均不可用,加速镜像站速度慢且会掐连接/回错误页。 +本件把大文件切成 N 片经镜像并行拉取(单片断了自动续传重试),全部齐后拼合。 +已实证:74MB Godot Linux 包约 15 分钟拉完(2026-11-03 mac_setup 依赖此件拉 Godot macOS 包)。 + +用法: python3 tools/ci/chunk_dl.py <输出路径> [分片数=16] +镜像回落顺序:直连 → gh-proxy.net → gh-proxy.com(HEAD 探测 Content-Length 决定实际用的源)。 +校验:分片按字节范围落盘、按序拼合;zip 结构校验由调用方(mac_setup.sh 的 unzip -tq)承担。 +退出码:0=成功 1=失败。 +""" +import concurrent.futures as cf +import os +import subprocess +import sys + +MIRRORS = ["", "https://gh-proxy.net/", "https://gh-proxy.com/"] + + +def pick_mirror(url: str): + """HEAD 探测取 Content-Length + 前 4 字节 zip 魔数校验(镜像站会回 200 错误页体, + 只看 Content-Length 会把 195 字节的假体当真——2026-11-03 gh-proxy.net 实证)。 + 返回(实际可用 URL, 字节数)或 (None, 0)。非 zip 目标禁用本件。""" + for m in MIRRORS: + u = m + url + r = subprocess.run(["curl", "-sIL", "-m", "30", u], capture_output=True, text=True) + size = 0 + for line in r.stdout.splitlines(): + if line.lower().startswith("content-length:"): + try: + size = int(line.split(":", 1)[1].strip()) + except ValueError: + size = 0 + if size <= 65536: + continue + head = subprocess.run( + ["curl", "-sL", "-m", "20", "-r", "0-3", "-o", "-", u], + capture_output=True) + if head.stdout[:2] != b"PK": + continue + return u, size + return None, 0 + + +def fetch(job) -> bool: + i, s, e, url, out = job + p = "%s.part%02d" % (out, i) + want = e - s + 1 + for _attempt in range(200): + have = os.path.getsize(p) if os.path.exists(p) else 0 + if have >= want: + return True + r0 = s + have + subprocess.run( + ["curl", "-sL", "--speed-limit", "1024", "--speed-time", "30", + "-m", "600", "-r", "%d-%d" % (r0, e), "-o", p + ".new", url], + capture_output=True) + if os.path.exists(p + ".new"): + with open(p, "ab") as f, open(p + ".new", "rb") as g: + f.write(g.read()) + os.remove(p + ".new") + return False + + +def main() -> int: + if len(sys.argv) < 3: + print("用法: python3 chunk_dl.py <输出路径> [分片数=16]") + return 1 + url, out = sys.argv[1], sys.argv[2] + n = int(sys.argv[3]) if len(sys.argv) > 3 else 16 + real, size = pick_mirror(url) + if not real: + print("全部下载源失败") + return 1 + print("size=%d url=%s" % (size, real), flush=True) + chunk = (size + n - 1) // n + jobs = [(i, i * chunk, min((i + 1) * chunk, size) - 1, real, out) + for i in range(n) if i * chunk < size] + with cf.ThreadPoolExecutor(n) as ex: + for ok in ex.map(fetch, jobs): + if not ok: + print("分片重试耗尽") + return 1 + with open(out, "wb") as f: + for i, _, _, _, _ in jobs: + with open("%s.part%02d" % (out, i), "rb") as g: + f.write(g.read()) + for i, _, _, _, _ in jobs: + os.remove("%s.part%02d" % (out, i)) + print("done %d" % os.path.getsize(out)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/ci/domain_route.sh b/tools/ci/domain_route.sh new file mode 100644 index 00000000..98f2803e --- /dev/null +++ b/tools/ci/domain_route.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# 乙案域分流——push 触发的远端 CI 按改动域选闸(2026-11-03 用户裁)。 +# 用法: tools/ci/domain_route.sh +# 输出: stdout 打印 full|docs|skip;GITHUB_OUTPUT 在位时另写 mode= 行(Actions 步骤输出)。 +# 规则(唯一事实源,乙案语义): +# 1. diff 含 game/ 下非 .md 文件(行为载体:代码/场景/数据表/素材)→ full(两域全量,渲染闸随 code 域) +# 2. 否则 diff 非空(design/ 文档、reports/、根 md、game/ 下 .md 等一切非代码面)→ docs +# (乙案:文档批只跑文档域秒级闸——gdd_lint/doc_link/doc_term/pipeline/doc_ref/capability+熵增) +# 例外:diff 只含 tools/ 与 .gitea/(闸件与管线自身改动)→ skip——闸件改动不占合回全量, +# 质量义务=落批人工确认可跑通(run_all_gates.sh 头注既有口径,不在远端重复烧引擎)。 +# 3. diff 空 → skip。 +# 判定面说明:game/tests/** 与 game/sim/** 属 game/ 行为载体(.md 除外)→ full, +# 与 run_all_gates.sh 关联档口径一致(测量面批 02 照跑全套件)。 +set -u +base="${1:-}" +head="${2:-HEAD}" +[ -n "$base" ] || { echo "用法: domain_route.sh " >&2; exit 2; } +head="$(git rev-parse --verify "${head}^{commit}" 2>/dev/null)" || { echo "坏 head: $head" >&2; exit 2; } +# base 不可用(新分支 zero-sha/被 force-push 掉)→ 回落 HEAD~1;孤提交(无父)→ docs 保守最小跑。 +git cat-file -e "$base^{commit}" 2>/dev/null || base="$(git rev-parse "$head~1" 2>/dev/null || true)" +if [ -z "$base" ]; then + mode="docs" +else + files="$(git diff --name-only "$base" "$head" 2>/dev/null)" + if [ -z "$files" ]; then + mode="skip" + elif echo "$files" | grep -vE '\.md$' | grep -qE '^game/'; then + mode="full" + else + non_tools="$(echo "$files" | grep -vE '^(tools/|\.gitea/)' || true)" + if [ -n "$non_tools" ]; then mode="docs"; else mode="skip"; fi + fi +fi +if [ -n "${GITHUB_OUTPUT:-}" ]; then printf 'mode=%s\n' "$mode" >> "$GITHUB_OUTPUT"; fi +echo "$mode" +exit 0 diff --git a/tools/ci/mac_setup.sh b/tools/ci/mac_setup.sh new file mode 100644 index 00000000..a8ba6a36 --- /dev/null +++ b/tools/ci/mac_setup.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +# mac_setup.sh — CI runner 自举件(Mac 本机 host 模式+专用账户图形会话;2026-11-03 用户裁)。 +# 一条命令把一台 macOS 变成 gplan 的远端 CI runner:专用账户+自动登录图形会话+Godot+gdstyle+ +# Pillow+act_runner 用户级常驻(LaunchAgent)。 +# 形态要点(渲染闸的硬需求):渲染闸要真窗口 = 必须有图形会话(WindowServer 只存在于已登录会话)。 +# 方案 = ci-runner 账户开机自动登录(owns 控制台图形会话)→ 该会话内 LaunchAgent 拉起 runner → +# 测试作业全部以 ci-runner 身份运行(无 admin、可写面限定);日常使用经「快速用户切换」到本人账户, +# 两会话并存互不干扰(后台会话保留约 1-2GB 内存;后台会话渲染行为在端到端验收第一轮实证)。 +# 用法(sudo 必须——建账户/写 /etc/kcpassword/装 /usr/local/bin 都要 root): +# sudo bash tools/ci/mac_setup.sh [--godot-zip <本地已下载的 macOS 版 zip 路径>] +# 环境前提: +# ① macOS(Intel/Apple Silicon 皆可,Godot 走 universal 包)② Xcode Command Line Tools 已装 +# (python3/git 在位;缺则先 xcode-select --install)③ 调用账户的 ~/gitea-runner/ 下已有 +# act_runner(darwin 包,gitea.com gitea/runner releases)与已注册的 .runner(一次性注册动作)。 +# 幂等:重复执行只补缺件;账户密码首次生成后存 /var/db/gplan-ci-runner.pw(root 600),重跑复用。 +set -euo pipefail + +GODOT_VER="4.7.2" +GODOT_URL="https://github.com/godotengine/godot/releases/download/${GODOT_VER}-stable/Godot_v${GODOT_VER}-stable_macos.universal.zip" +GDSTYLE_VER="0.4.0" +GDSTYLE_URL="https://github.com/atelico/gdstyle/releases/download/v${GDSTYLE_VER}/gdstyle-x86_64-apple-darwin.tar.gz" +MIRRORS=("" "https://gh-proxy.net/" "https://gh-proxy.com/") # 直连优先,镜像回落 +CI_USER="ci-runner" +CI_HOME="/Users/${CI_USER}" +PW_FILE="/var/db/gplan-ci-runner.pw" + +log() { echo "[mac_setup] $*"; } +die() { echo "[mac_setup] ✗ $*" >&2; exit 1; } + +[ "$(uname -s)" = "Darwin" ] || die "仅支持 macOS" +[ "$(id -u)" -eq 0 ] || die "请用 sudo 运行(建账户/写 /etc/kcpassword/装 /usr/local/bin 需要 root)" +for t in git curl python3 unzip dscl; do command -v "$t" >/dev/null 2>&1 || die "缺 ${t}(先 xcode-select --install)"; done +INVOKER="${SUDO_USER:-$(whoami)}" +RUNNER_SRC="/Users/${INVOKER}/gitea-runner" +[ -x "${RUNNER_SRC}/act_runner" ] || die "缺 ${RUNNER_SRC}/act_runner——先从 gitea.com/gitea/runner releases 下载 darwin 包" +[ -f "${RUNNER_SRC}/.runner" ] || die "缺 ${RUNNER_SRC}/.runner——act_runner 尚未注册(register 子命令,见 Gitea 文档)" + +GODOT_ZIP="" +while [ $# -gt 0 ]; do + case "$1" in + --godot-zip) GODOT_ZIP="${2:-}"; shift 2 ;; + *) die "未知参数: $1" ;; + esac +done +rm -f /tmp/godot_mac_XXXXXX.zip /tmp/gdstyle_XXXXXX.tar.gz 2>/dev/null || true # 早期 BSD-mktemp 缺陷的字面残留清理 + +# ── 1. 专用账户+密码落盘(自动登录需要密码编码进 /etc/kcpassword,root 600)── +if dscl . -read "${CI_HOME}" NFSHomeDirectory >/dev/null 2>&1; then + log "账户 ${CI_USER} 已存在" + [ -f "$PW_FILE" ] || die "账户已存在但 $PW_FILE 缺失(无法配置自动登录)——删除账户重跑,或手工补密码文件" + CI_PW="$(cat "$PW_FILE")" +else + CI_PW="$(uuidgen | tr -d '-')" + /usr/sbin/sysadminctl -addUser "$CI_USER" -password "$CI_PW" -home "$CI_HOME" \ + || die "账户创建失败(sysadminctl)" + printf '%s' "$CI_PW" > "$PW_FILE" + chmod 600 "$PW_FILE" + log "账户 ${CI_USER} 已创建(密码仅存 ${PW_FILE},root 600,不对外告知)" +fi +# 家目录兜底:sysadminctl 只登记不创建(首次运行输出 "not created!" 在案)——家目录缺失时 +# 自动登录与任何以该用户执行的操作都会失败(Pillow --user 失败即此根因)。 +if [ ! -d "${CI_HOME}" ]; then + mkdir -p "${CI_HOME}" + chown -R "${CI_USER}:staff" "${CI_HOME}" + createhomedir -c -u "$CI_USER" >/dev/null 2>&1 || true + log "家目录 ${CI_HOME} 已补建" +fi + +# ── 2. 自动登录(ci-runner 拥有控制台图形会话——渲染闸的窗口来源)── +_KCP="/etc/kcpassword" +_need_kcp=1 +[ -f "$_KCP" ] && _need_kcp=0 +if defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser 2>/dev/null | grep -q "$CI_USER"; then + log "自动登录已指向 ${CI_USER},跳过" + _need_kcp=0 +fi +if [ "$_need_kcp" = 1 ]; then + python3 - "$CI_PW" <<'PYEOF' +import sys +pwd = sys.argv[1].encode() +key = bytes([0x7D, 0x89, 0x52, 0x23, 0xD2, 0xBC, 0xDD, 0xEA, 0xA3, 0xB9, 0x1F]) +enc = bytes(b ^ key[i % len(key)] for i, b in enumerate(pwd)) +enc += b"\x00" * ((12 - len(enc) % 12) % 12 + 1) +open("/etc/kcpassword", "wb").write(enc) +PYEOF + chmod 600 /etc/kcpassword + defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser "$CI_USER" + log "自动登录已配置:开机直接进入 ${CI_USER} 图形会话(日常使用=快速用户切换到本人账户)" +fi + +# ── 3. Godot macOS 版 → 完整 .app 装 /usr/local/lib + /usr/local/bin/godot 包装转发 ── +# 纪律:官方签名封 bundle 的 Info.plist——从 .app 抽裸二进制会被 taskgated 判 +# 「Code Signature Invalid」SIGKILL(2026-11-03 实证),必须整 bundle 安装保持签名校验路径。 +if /usr/local/bin/godot --version >/dev/null 2>&1 && /usr/local/bin/godot --version 2>/dev/null | grep -q "$GODOT_VER"; then + log "Godot ${GODOT_VER} 已在位,跳过" +else + ZIP="" + SRC_ZIP="/Users/${INVOKER}/godot-linux/Godot_v${GODOT_VER}-stable_macos.universal.zip" + if [ -n "$GODOT_ZIP" ] && [ -s "$GODOT_ZIP" ]; then + ZIP="$GODOT_ZIP"; log "使用本地 zip: $ZIP" + elif [ -z "$GODOT_ZIP" ] && [ -s "$SRC_ZIP" ]; then + ZIP="$SRC_ZIP"; log "复用已下载 zip: $ZIP" + else + ZIP="$(mktemp "${TMPDIR:-/tmp}/godot_mac_XXXXXX")" # macOS BSD mktemp 禁 X 后带后缀 + _ok=0 + for m in "${MIRRORS[@]}"; do + log "下载 Godot(${m:+经 $m})" + # 镜像站可能回 200 的错误页体——下载后必须验 zip 结构,假成功当失败处理 + if curl -fL --retry 2 -m 1800 -o "$ZIP" "${m}${GODOT_URL}" && unzip -tq "$ZIP" >/dev/null 2>&1; then + _ok=1; break + fi + log "该源失败,换下一源" + done + [ "$_ok" = 1 ] || die "全部下载源失败——手动下载 ${GODOT_URL} 后重跑并加 --godot-zip <路径>;或用 tools/ci/chunk_dl.py 分片拉取" + fi + TMPD="$(mktemp -d)" + unzip -tq "$ZIP" >/dev/null # 双保险:本地复用的 zip 同样验结构 + unzip -q -o "$ZIP" -d "$TMPD" + mkdir -p /usr/local/lib + rm -rf "/usr/local/lib/Godot_ci.app" + cp -R "${TMPD}/Godot.app" "/usr/local/lib/Godot_ci.app" + xattr -dr com.apple.quarantine "/usr/local/lib/Godot_ci.app" 2>/dev/null || true + cat > /usr/local/bin/godot <<'EOF' +#!/bin/bash +exec /usr/local/lib/Godot_ci.app/Contents/MacOS/Godot "$@" +EOF + chmod 0755 /usr/local/bin/godot + rm -rf "$TMPD" + log "Godot 已装:$(/usr/local/bin/godot --version 2>/dev/null | head -1)" +fi + +# ── 4. gdstyle v0.4.0 darwin → /usr/local/bin/gdstyle(S13 静态扫描闸件,版本钉死)── +if /usr/local/bin/gdstyle --version >/dev/null 2>&1; then + log "gdstyle 已在位,跳过" +else + TGZ="$(mktemp "${TMPDIR:-/tmp}/gdstyle_XXXXXX")" # macOS BSD mktemp 禁 X 后带后缀 + SRC_TGZ="/Users/${INVOKER}/godot-linux/gdstyle-x86_64-apple-darwin.tar.gz" + if [ -s "$SRC_TGZ" ] && tar -tzf "$SRC_TGZ" >/dev/null 2>&1; then + TGZ="$SRC_TGZ"; log "复用已下载 gdstyle 包: $TGZ" + else + _ok=0 + for m in "${MIRRORS[@]}"; do + log "下载 gdstyle v${GDSTYLE_VER} darwin(${m:+经 $m})" + if curl -fL --retry 2 -m 600 -o "$TGZ" "${m}${GDSTYLE_URL}" && tar -tzf "$TGZ" >/dev/null 2>&1; then _ok=1; break; fi + done + [ "$_ok" = 1 ] || die "gdstyle 下载失败——手动下载 ${GDSTYLE_URL} 解压放 /usr/local/bin/gdstyle 后重跑" + fi + TMPD="$(mktemp -d)" + tar -xzf "$TGZ" -C "$TMPD" + BIN="$(find "$TMPD" -type f -name 'gdstyle*' | head -1)" + [ -n "$BIN" ] || die "压缩包内未找到 gdstyle 二进制" + install -m 0755 "$BIN" /usr/local/bin/gdstyle + xattr -dr com.apple.quarantine /usr/local/bin/gdstyle 2>/dev/null || true + rm -rf "$TMPD" + log "gdstyle 已装:/usr/local/bin/gdstyle" +fi + +# ── 5. Pillow(渲染闸依赖)——系统级安装(/Library/Python,全账户可导入), +# 不用 --user:ci-runner 家目录时序敏感,系统级绕开全部家目录问题 ── +if python3 -c "import PIL" >/dev/null 2>&1; then + log "Pillow 已在位(系统级),跳过" +else + log "系统级安装 Pillow(阿里源)" + python3 -m pip install pillow \ + -i https://mirrors.aliyun.com/pypi/simple/ --quiet \ + || die "Pillow 系统级安装失败" +fi + +# ── 6. runner 程序与注册文件就位(复制既有注册身份,免重新注册)── +mkdir -p "${CI_HOME}/gitea-runner" "${CI_HOME}/Library/LaunchAgents" +cp -f "${RUNNER_SRC}/act_runner" "${CI_HOME}/gitea-runner/act_runner" +cp -f "${RUNNER_SRC}/.runner" "${CI_HOME}/gitea-runner/.runner" +cp -f "${RUNNER_SRC}/config.yaml" "${CI_HOME}/gitea-runner/config.yaml" 2>/dev/null || true +python3 - "${CI_HOME}/gitea-runner/.runner" <<'PYEOF' +import json, sys +p = sys.argv[1] +d = json.load(open(p)) +d["labels"] = ["ci-mac:host"] +json.dump(d, open(p, "w")) +print("labels ->", d["labels"]) +PYEOF +sed -i '' 's/^ level: debug/ level: info/' "${CI_HOME}/gitea-runner/config.yaml" 2>/dev/null || true +chown -R "${CI_USER}:staff" "${CI_HOME}/gitea-runner" "${CI_HOME}/Library/LaunchAgents" + +# ── 7. LaunchAgent(用户级常驻,挂在 ci-runner 图形会话内——渲染闸窗口的来源)── +PLIST="${CI_HOME}/Library/LaunchAgents/com.gitea.act-runner.plist" +cat > "$PLIST" < + + + + Labelcom.gitea.act-runner + ProgramArguments + + ${CI_HOME}/gitea-runner/act_runner + daemon + + WorkingDirectory${CI_HOME}/gitea-runner + RunAtLoad + KeepAlive + StandardOutPath${CI_HOME}/gitea-runner/daemon.log + StandardErrorPath${CI_HOME}/gitea-runner/daemon.log + + +EOF +chown "${CI_USER}:staff" "$PLIST" +_uid="$(id -u "$CI_USER")" +if launchctl asuser "$_uid" launchctl bootstrap "gui/$_uid" "$PLIST" 2>/dev/null; then + log "LaunchAgent 已在 ${CI_USER} 会话内启动" + _started=1 +else + _started=0 + log "当前无 ${CI_USER} 图形会话——LaunchAgent 已就位,重启后随自动登录自动拉起" +fi + +# ── 8. 退役 Docker 版 runner(如存在)── +if command -v docker >/dev/null 2>&1; then + docker rm -f gitea-runner >/dev/null 2>&1 && log "Docker 版 runner 容器已退役" || log "无 Docker 版 runner 容器(或 docker 未运行)" +fi + +# ── 9. 验证与收尾提示 ── +if [ "$_started" = 1 ]; then + sleep 5 + grep -q "declare successfully" "${CI_HOME}/gitea-runner/daemon.log" 2>/dev/null \ + && log "runner 已向 Gitea 声明成功" || log "runner 已启动,声明确认见 ${CI_HOME}/gitea-runner/daemon.log" +else + log "⚠ 重启 Mac 后自动登录生效、runner 随会话拉起——重启后验证第①条" +fi +log "全部完成。验证清单:" +log " ① Gitea 网页 → 仓库设置 → Actions → Runners 应显示 mac-ci 在线(Idle,标签 ci-mac)" +log " ② sudo -u ${CI_USER} /usr/local/bin/godot --headless --version" +log " ③ sudo -u ${CI_USER} python3 -c 'import PIL'" +log " ④ 日常使用:登录窗/快速用户切换到本人账户(ci-runner 会话在后台保持)" +log " ⑤ 端到端:下一次合回 master 的推送自动触发跑批" diff --git a/tools/ci_runner.sh b/tools/ci_runner.sh deleted file mode 100644 index 855e2d59..00000000 --- a/tools/ci_runner.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -# 本地 CI 跑批件——合回 master 后的全量验证执行器(git 钩子触发、后台运行,2026-10-30 用户裁)。 -# 触发语义(钩子只负责拉起,判定在本件): -# merge 模式=合回验证点:主工作区 git merge 落地后(tools/hooks/post-merge)对 master 尖无条件 -# 跑一次全量域闸;任务 worktree 内的合并不触发(钩子里限主工作区)。 -# commit 模式=红态追绿:仅当最近一次 CI 判红时(tools/hooks/post-commit)本提交触发复验; -# 绿态与无状态零触发——小批直提不重复跑全量,有意义的重跑只剩「验证修复」一类。 -# 并跑语义=跑完补跑(coalesce,2026-10-30 用户裁):跑批期间新触发不中断当前轮——锁被占时, -# 在跑实例的尖端就是当前尖端则本触发无事可做,否则落 ci.rerun 标记;当前轮跑完见标记或尖端 -# 前移,对最新尖端自动补跑一轮。绝不逐笔触发各跑一轮,最终只测最新尖端。 -# 产物(均不入库,本目录 .gitignore 已收):ci-status.json=最新一跑判定(commit/verdict/红闸/ -# 关键日志行/耗时),ci-history.jsonl=逐次流水账,ci.log=当轮全量闸完整输出, -# ci.lock=锁目录(内含在跑尖端),ci.rerun=补跑标记。 -# 用法: tools/ci_runner.sh merge|commit -set -u -REPO="$(cd "$(dirname "$0")/.." && pwd)" -GATES_DIR="$REPO/game/tests/.gates" -LOCK="$GATES_DIR/ci.lock" -RERUN="$GATES_DIR/ci.rerun" -STATUS="$GATES_DIR/ci-status.json" -HISTORY="$GATES_DIR/ci-history.jsonl" -LOG="$GATES_DIR/ci.log" -GODOT="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}" -export PYTHONIOENCODING=utf-8 - -MODE="${1:-commit}" - -# ── 触发判定 ── -if [ "$MODE" = "commit" ]; then - [ -f "$STATUS" ] || exit 0 - grep -q '"verdict": "red"' "$STATUS" || exit 0 -fi - -# ── 锁(mkdir 原子);被占时按在跑尖端判定:同尖=已在验这份字节,异尖=落补跑标记 ── -TIP="$(git -C "$REPO" rev-parse HEAD)" -if ! mkdir "$LOCK" 2>/dev/null; then - if [ "$(cat "$LOCK/tip" 2>/dev/null)" = "$TIP" ]; then - exit 0 - fi - touch "$RERUN" - exit 0 -fi -trap 'rm -rf "$LOCK"' EXIT - -# ── 跑批主循环(跑完补跑)── -while :; do - rm -f "$RERUN" - TIP="$(git -C "$REPO" rev-parse HEAD)" - echo "$TIP" > "$LOCK/tip" - _start="$(date +%s)" - - # 起手刷类名缓存:.godot 缓存不入 git,与合并后代码失配会假红假绿(在册坑)—— - # 原「合并后零复跑」的秒级动作由本件接管,全量闸跑的就是刷新后的字节 - "$GODOT" --headless --path "$REPO/game" --editor --quit >>"$LOG" 2>&1 - - bash "$REPO/tools/run_all_gates.sh" >"$LOG" 2>&1 - RC=$? - _end="$(date +%s)" - - # 判定与记录:读闸脚本写的回执(all_green/各闸退出码),红项关键行取自本轮 ci.log - python - "$REPO" "$TIP" "$RC" "$((_end - _start))" <<'PY' -import json, re, sys, datetime -repo, tip, rc, dur = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4]) -gates_dir = repo + "/game/tests/.gates" -entry = {"commit": tip, - "timestamp": datetime.datetime.now().astimezone().isoformat(timespec="seconds"), - "exit_code": rc, "verdict": "green" if rc == 0 else "red", - "duration_s": dur, "red_gates": [], "key_lines": []} -try: - with open(gates_dir + "/receipt.json", encoding="utf-8") as f: - r = json.load(f) - # 回执只有确属本跑(commit 对齐)才采信——闸在起手自检即中止时不写新回执, - # 盘上旧回执是上一跑的读数,当作本次红项会撒谎(2026-10-31 实跑缺陷修正) - if r.get("commit") == tip: - entry["red_gates"] = [g["name"] for g in r.get("gates", []) if str(g.get("exit")) != "0"] - else: - entry["note"] = "闸未跑完(起手自检或早期中止,回执为上一跑旧件)——红项读数不可用,看 key_lines 与 ci.log" -except Exception: - pass -try: - with open(gates_dir + "/ci.log", encoding="utf-8", errors="replace") as f: - lines = [l.rstrip() for l in f] - entry["key_lines"] = [l for l in lines if re.search(r"FAIL|红项|Error|error", l)][-12:] -except Exception: - pass -with open(gates_dir + "/ci-status.json", "w", encoding="utf-8") as f: - json.dump(entry, f, ensure_ascii=False, indent=1) -with open(gates_dir + "/ci-history.jsonl", "a", encoding="utf-8") as f: - f.write(json.dumps(entry, ensure_ascii=False) + "\n") -PY - - # 轮尾补跑判定:跑批期间有新触发(标记在)或尖端前移 → 对最新尖端再来一轮 - _newtip="$(git -C "$REPO" rev-parse HEAD)" - if [ ! -f "$RERUN" ] && [ "$_newtip" = "$TIP" ]; then - exit 0 - fi -done diff --git a/tools/ci_watch.sh b/tools/ci_watch.sh index 79fe2701..13b2b5e9 100755 --- a/tools/ci_watch.sh +++ b/tools/ci_watch.sh @@ -1,57 +1,126 @@ #!/usr/bin/env bash -# 回执等待件——合回后等 CI 回执的后台轮询器(2026-11-02 用户裁;规则面=process-conventions §1 第 3 条「回执等待纪律」)。 -# 用法: tools/ci_watch.sh <合并提交hash> [超时秒,默认 900] -# 调用面: 合回落地先向用户呈报本批总结,随即以 run_in_background 后台任务运行本件;命令结束时 -# harness 唤醒会话——绿→删区收口;红→呈报红闸名单走修复批;超时→呈报「回执未落」(CI 可能崩溃)。 -# 「本次回执已落」认领判据(三条全满足,防认错轮): -# ① 回执可整读——CI 写回执非原子,半截/读空静默重试; -# ② 回执 commit 为传入提交或其后代——另一会话合回推进尖端触发补跑覆盖时按后代认领(coalesce 语义见 ci_runner.sh); -# ③ 回执 timestamp 晚于合并提交时刻——排除合并前盘上的旧回执(旧尖端是合并提交的祖先,只查②会误领旧回执)。 +# 回执等待件——合回后等远端 CI 回执的后台轮询器(2026-11-03 用户裁:CI 迁远端,本件改轮询 Gitea API)。 +# 用法: tools/ci_watch.sh remote <推送提交hash> [超时秒,默认 3000] +# 调用面: post-merge 钩子推送 master 成功后自动后台挂起;人手等待亦可(agent 会话以 run_in_background 运行, +# 命令结束时 harness 唤醒——绿→删区收口;红→呈报红闸名单走修复批;超时→呈报「回执未落」)。 +# 「本次回执已落」判据:Gitea commit status 的 state 离开 pending(success/failure/error), +# 且 status 的 target_url 指向本仓库 Actions(防认错仓)。远端跑批 20-35 分钟量级,默认超时给足 50 分钟。 +# 产物:沿用本地 CI 时代的回执合同——game/tests/.gates/ci-status.json(commit/verdict/timestamp/ +# duration_s/red_gates/key_lines/note)覆写 + ci-history.jsonl 追加;红态 red_gates=失败的 job 名单 +# (来自 runs//jobs API),job 日志链接在 key_lines,细粒度红闸清单以 job 日志为准。 +# 授权:GITEA_TOKEN 环境变量优先,否则读 git 凭据库(host=git.kuaimeo.com)。 # 退出码: 0=绿 1=红 2=超时/回执未落 3=参数错误 set -u -# 主工作区定位:CI 只在主工作区跑(post-merge 钩子限主区),从任务 worktree 内调用也要读主区回执—— -# 经 git 公共目录(.git)的上级定位主仓根,主区调用与 worktree 调用同一读数 COMMON="$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" \ || { echo "不在 git 仓库内"; exit 3; } REPO="$(dirname "$COMMON")" -STATUS="$REPO/game/tests/.gates/ci-status.json" -BASE="${1:-}" -TIMEOUT="${2:-900}" +GATES_DIR="$REPO/game/tests/.gates" +STATUS="$GATES_DIR/ci-status.json" +HISTORY="$GATES_DIR/ci-history.jsonl" + +MODE="${1:-}" +BASE="${2:-}" +TIMEOUT="${3:-3000}" INTERVAL=30 -[ -n "$BASE" ] || { echo "用法: ci_watch.sh <合并提交hash> [超时秒]"; exit 3; } +case "$MODE" in + remote) ;; + *) + echo "用法: ci_watch.sh remote <推送提交hash> [超时秒](本地 CI 模式已退役——回执现由远端产生)" + exit 3 ;; +esac +[ -n "$BASE" ] || { echo "用法: ci_watch.sh remote <推送提交hash> [超时秒]"; exit 3; } BASE="$(git -C "$REPO" rev-parse --verify "${BASE}^{commit}")" || { echo "提交不存在: $BASE"; exit 3; } -BASE_CT="$(git -C "$REPO" log -1 --format=%ct "$BASE")" -_deadline=$(( $(date +%s) + TIMEOUT )) -while :; do - _read="$(python -I -c " -import json, sys, datetime -try: - d = json.load(open(sys.argv[1], encoding='utf-8')) - ts = int(datetime.datetime.fromisoformat(d['timestamp']).timestamp()) - print(d.get('commit', ''), d.get('verdict', ''), ts) -except Exception: - pass -" "$STATUS")" - _com=""; _verdict=""; _ts="" - if [ -n "$_read" ]; then - set -- $_read - [ $# -eq 3 ] && { _com="$1"; _verdict="$2"; _ts="$3"; } - fi - if [ -n "$_com" ] && [ -n "$_verdict" ] && [ -n "$_ts" ] \ - && [ "$_ts" -gt "$BASE_CT" ] \ - && git -C "$REPO" merge-base --is-ancestor "$BASE" "$_com" 2>/dev/null; then - python -I -c " -import json, sys -d = json.load(open(sys.argv[1], encoding='utf-8')) -print('CI回执:', d.get('verdict'), '| commit', d.get('commit', '')[:8], '| 耗时', d.get('duration_s'), 's') -if d.get('note'): print('note:', d['note']) -for g in d.get('red_gates', []): print('红闸:', g) -for l in d.get('key_lines', []): print('key:', l) -" "$STATUS" - [ "$_verdict" = "green" ] && exit 0 - exit 1 - fi - _now=$(date +%s); _left=$((_deadline - _now)) - [ "$_left" -le 0 ] && { echo "超时(${TIMEOUT}s)回执未落——CI 可能崩溃或未触发,查 game/tests/.gates/ 的 ci.lock 与 ci.log"; exit 2; } - sleep $(( _left < INTERVAL ? _left : INTERVAL )) -done + +# 仓库全名(owner/repo)从 origin URL 解析(HTTPS 与 SSH 形式都兼容) +_origin="$(git -C "$REPO" remote get-url origin 2>/dev/null || true)" +REPO_SLUG="$(printf '%s' "$_origin" | grep -oE '[^/:]+/[^/]+\.git$' | sed 's/\.git$//')" +[ -n "$REPO_SLUG" ] || { echo "无法从 origin 解析 owner/repo: $_origin"; exit 3; } + +# 令牌:环境变量优先,回落 git 凭据库 +TOKEN="${GITEA_TOKEN:-}" +if [ -z "$TOKEN" ]; then + TOKEN="$(printf 'protocol=https\nhost=git.kuaimeo.com\n\n' | git credential fill 2>/dev/null | sed -n 's/^password=//p' | head -1)" +fi +[ -n "$TOKEN" ] || { echo "取不到 Gitea 令牌(设 GITEA_TOKEN 或先 git credential approve 存储)"; exit 3; } +API="https://git.kuaimeo.com/api/v1/repos/$REPO_SLUG" + +export PYTHONIOENCODING=utf-8 +python -I - "$API" "$BASE" "$TOKEN" "$TIMEOUT" "$INTERVAL" "$STATUS" "$HISTORY" <<'PYEOF' +import json, os, re, subprocess, sys, time, urllib.request, urllib.error, datetime + +api, base, token, timeout, interval, status_path, history_path = sys.argv[1:8] +timeout, interval = int(timeout), int(interval) +deadline = time.time() + timeout +start = time.time() +H = {"Authorization": "token " + token} + + +def get(url, t=20): + req = urllib.request.Request(url, headers=H) + return json.load(urllib.request.urlopen(req, timeout=t)) + + +def combined_status(sha): + return get(api + "/commits/%s/status" % sha) + + +def write_receipt(verdict, note, red_gates, key_lines): + now = datetime.datetime.now().astimezone().isoformat(timespec="seconds") + dur = int(time.time() - start) + d = {"commit": base, "verdict": verdict, "timestamp": now, + "duration_s": dur, "note": note, "red_gates": red_gates, "key_lines": key_lines} + os.makedirs(os.path.dirname(status_path), exist_ok=True) + tmp = status_path + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(d, f, ensure_ascii=False, indent=1) + os.replace(tmp, status_path) + with open(history_path, "a", encoding="utf-8") as f: + f.write(json.dumps(d, ensure_ascii=False) + "\n") + return d + + +def emit(d): + print("CI回执:", d.get("verdict"), "| commit", d.get("commit", "")[:8], "| 耗时", d.get("duration_s"), "s") + if d.get("note"): + print("note:", d["note"]) + for g in d.get("red_gates", []): + print("红闸:", g) + for l in d.get("key_lines", []): + print("key:", l) + + +last_err = "" +while True: + try: + s = combined_status(base) + state = s.get("state", "") + statuses = s.get("statuses", []) + target = next((x.get("target_url", "") for x in statuses if x.get("target_url")), "") + if state in ("success", "failure", "error", "warning"): + red = state != "success" + red_gates, run_id = [], None + m = re.search(r"/actions/runs/(\d+)", target) + if m and red: + run_id = m.group(1) + try: + jobs = get(api + "/actions/runs/%s/jobs" % run_id) + red_gates = sorted({j["name"] for j in jobs.get("jobs", []) if j.get("status") not in ("success", "skipped")}) + except Exception as e: + red_gates = ["" % e] + note = "远端CI state=%s%s" % (state, ("(run %s)" % run_id) if run_id else "") + key_lines = ["job日志: %s" % target] if target else [] + d = write_receipt("green" if not red else "red", note, red_gates, key_lines) + emit(d) + sys.exit(0 if not red else 1) + # pending:继续等 + except urllib.error.HTTPError as e: + last_err = "HTTP %s" % e.code + except Exception as e: + last_err = str(e) + if time.time() > deadline: + note = "超时(%ds)远端回执未落——最后错误 %s;查 runner 在线态与 %s/actions" % (timeout, last_err or "无", api.rsplit("/repos/", 1)[0]) + d = write_receipt("red", note, ["<回执未落>"], []) + emit(d) + sys.exit(2) + time.sleep(interval) +PYEOF diff --git a/tools/gate_platform.sh b/tools/gate_platform.sh new file mode 100644 index 00000000..8847cae3 --- /dev/null +++ b/tools/gate_platform.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# 闸件跨平台件——平台探测/路径形式/带超时执行/Godot 孤儿清杀,Git Bash 与 macOS/Linux 双栖。 +# 单点定义:run_all_gates.sh / probe_gate.sh / render_gate.sh / golden_baseline_refresh.sh 共用(source 引入)。 +# 纪律:闸脚本禁裸调 coreutils timeout 与 Windows 专用件(taskkill/PowerShell/ps -W/tasklist/cygpath)—— +# 一律走本件封装;macOS 为 BSD 工具链(无 coreutils timeout,sed/stat 语义不同),裸调必断 +# (2026-11-03 远端 CI 迁移裁决:runner=Mac 本机 host 模式,禁再增 Windows 专用直调)。 +# 平台判定以 uname 为准(Git Bash=MINGW*,macOS=Darwin,Linux=Linux)。 + +gate_is_win() { + case "$(uname -s 2>/dev/null)" in + MINGW*|MSYS*|CYGWIN*|Windows_NT) return 0 ;; + *) return 1 ;; + esac +} + +# Godot --path 接受的项目路径形式:Windows 需 cygpath -m 混合斜杠,其他平台原样。 +gate_game_path() { + if gate_is_win; then cygpath -m "$1"; else printf '%s' "$1"; fi +} + +# 杀整棵进程树()——递归杀子进程后杀本进程。macOS/Linux 走 pgrep -P; +# Windows 侧 MSYS kill 对其可见进程有效,带 wrapper 树的场景仍由调用方原 taskkill /T 逻辑负责。 +gate_kill_tree() { + local p c + kill -9 "$1" 2>/dev/null + for c in $(pgrep -P "$1" 2>/dev/null); do gate_kill_tree "$c"; done +} + +# 带超时执行(coreutils timeout 的纯 bash 替代,双平台同语义): +# gate_run_timeout <秒> <日志路径> <命令...> +# 命令输出重定向到日志;退出码=命令退出码,超时=124(与 coreutils 语义一致,判据与既有闸一致)。 +# 实现=后台启动+守卫落标志文件(probe_gate run_suite 同款手法:taskkill 强杀后 wait 报 0 的 +# 假绿以标志文件纠偏);超时先杀整树再回 124。 +gate_run_timeout() { + local t="$1" log="$2"; shift 2 + local kf + kf="$(mktemp "${TMPDIR:-/tmp}/gatetime_XXXXXX")" + "$@" >"$log" 2>&1 & + local pid=$! + ( sleep "$t" 2>/dev/null + touch "$kf" + gate_kill_tree "$pid" + true ) & + local guard=$! + wait "$pid"; local st=$? + kill "$guard" 2>/dev/null; wait "$guard" 2>/dev/null + if [ -f "$kf" ]; then + gate_kill_tree "$pid" + st=124 + fi + rm -f "$kf" + return "$st" +} + +# 列命令行含 的 Godot 进程 PID(孤儿清杀与批末残留清点共用)。 +# Windows:PowerShell WMI 按进程名+命令行匹配(console wrapper+真引擎都算); +# macOS/Linux:ps 全表按「可执行含 godot+命令行含本 game 路径」过滤(ps 两平台同形)。 +gate_orphans() { + local gp="$1" + if gate_is_win; then + local tmpd tmpf + tmpd="$(mktemp -d "${TMPDIR:-/tmp}/gateorph_XXXXXX")" # macOS BSD mktemp 禁 X 后带后缀(本分支 Windows 专用,模板统一防复用) + tmpf="$tmpd/k.ps1" + local gp_win + gp_win="$(cygpath -m "$gp")" + printf "Get-CimInstance Win32_Process -Filter \"Name like 'Godot%%'\" | Where-Object { \$_.CommandLine -like '*%s*' } | ForEach-Object { \$_.ProcessId }\n" "$gp_win" > "$tmpf" + powershell -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$tmpf")" 2>/dev/null | tr -d '\r' + rm -rf "$tmpd" + else + ps -axo pid=,command= 2>/dev/null | awk -v g="$gp" 'tolower($0) ~ /godot/ && index($0, g) {print $1}' + fi +} + +gate_kill_orphans() { # gate_kill_orphans + local p + for p in $(gate_orphans "$1"); do + if gate_is_win; then taskkill //F //PID "$p" >/dev/null 2>&1 + else kill -9 "$p" 2>/dev/null; fi + done +} + +# 全局 Godot 进程计数(其他会话的仅报数不算账——批末残留清点的对照面)。 +gate_godot_count_all() { + if gate_is_win; then tasklist 2>/dev/null | grep -ci '^Godot' || true + else ps -axo comm= 2>/dev/null | grep -ci 'godot' || true + fi +} + +# golden 基线平台子目录名——渲染基线按平台各存一份(Windows 显卡字形/抗锯齿与 macOS CoreText +# 差异使跨平台逐像素比对必失配,2026-11-03 用户裁:按平台各存基线,平台内部照常严格比对)。 +gate_platform_id() { + case "$(uname -s 2>/dev/null)" in + MINGW*|MSYS*|CYGWIN*|Windows_NT) echo "windows" ;; + Darwin) echo "mac" ;; + *) echo "linux" ;; + esac +} diff --git a/tools/gdstyle_gate.py b/tools/gdstyle_gate.py index b93d54cb..49c375a5 100644 --- a/tools/gdstyle_gate.py +++ b/tools/gdstyle_gate.py @@ -48,7 +48,10 @@ CONFIG = ROOT / "tools" / "gdstyle.toml" BASELINE = ROOT / "tools" / "gdstyle_baseline.txt" SCAN_DIRS = ["game/core", "game/data", "game/presentation"] PINNED_VERSION = "0.4.0" -DEFAULT_BIN = "D:/code/gdstyle/gdstyle.exe" +# 二进制定位:GDSTYLE_BIN 环境变量优先;Windows 本机固定路径保底,macOS/Linux 走 /usr/local/bin +# (mac_setup.sh 自举件装钉定版 v0.4.0 darwin 包至此,远端 CI 与本机同判据)。 +DEFAULT_BIN = ("D:/code/gdstyle/gdstyle.exe" if sys.platform == "win32" + else "/usr/local/bin/gdstyle") MAX_REPORT = 50 MAX_VANISHED_REPORT = 20 SHRINK_GUARD = 0.9 # 新键数 < 旧键数 * 本系数=骤降,refresh 拒写须 --force diff --git a/tools/golden_baseline_refresh.sh b/tools/golden_baseline_refresh.sh old mode 100644 new mode 100755 index 93ad7d7f..422d5a4f --- a/tools/golden_baseline_refresh.sh +++ b/tools/golden_baseline_refresh.sh @@ -15,6 +15,7 @@ set -u G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}" REPO="$(cd "$(dirname "$0")/.." && pwd)" +. "$REPO/tools/gate_platform.sh" # 跨平台件(平台探测/路径形式/带超时执行) GAME="" MODE="board" for a in "$@"; do @@ -24,7 +25,8 @@ for a in "$@"; do esac done GAME="${GAME:-$REPO/game}" -GAME_WIN="$(cygpath -m "$GAME")" +GAME_WIN="$(gate_game_path "$GAME")" # Godot --path 接受形式(Windows 混合斜杠,其他平台原样) +PLAT="$(gate_platform_id)" # 基线落点=本平台子目录(game/tests/golden//) TMP="$(mktemp -d /tmp/goldenrefresh_XXXXXX)" trap 'rm -rf "$TMP"' EXIT @@ -32,20 +34,22 @@ if [ "$MODE" = "menu" ]; then for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \ assets/art/ui/button_stone.png; do if [ ! -f "$GAME/$a" ]; then - echo "[golden_refresh] 拒跑 — 菜单素材缺位: $a(S3 停机点口径:基线禁锚回退态,先补素材+导入边车)" + echo "[golden_refresh] 拒跑 — 菜单素材缺位: ${a}(S3 停机点口径:基线禁锚回退态,先补素材+导入边车)" exit 1 fi done - timeout 240 "$G" --path "$GAME_WIN" res://probe/shot_probe.tscn -- "$TMP/shots" >"$TMP/run.log" 2>&1 + gate_run_timeout 420 "$TMP/run.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/shot_probe.tscn -- "$TMP/shots" st=$? grep -E "\[shot_probe\]" "$TMP/run.log" | tail -3 - if [ "$st" -ne 0 ] || [ ! -s "$TMP/shots/shot_menu.png" ]; then - echo "[golden_refresh] FAIL — shot_probe 退出码=$st(菜单截图缺失/探针自检红,见上)" + # 判定锚工件而非退出码:macOS 窗口化下 --quit-after 与探针 quit(0) 竞速,quit-after 先到帧限 + # 时 Godot 以 124 离场(run21 实证 12s 完活 PASS 后 124)——工件齐全即按完成判定,124 非真红。 + if [ ! -s "$TMP/shots/shot_menu.png" ] || ! grep -q "问题=0" "$TMP/run.log"; then + echo "[golden_refresh] FAIL — 菜单截图缺失或 shot_probe 自检红(st=$st,见上)" exit 1 fi - mkdir -p "$REPO/game/tests/golden" - cp "$TMP/shots/shot_menu.png" "$REPO/game/tests/golden/menu_boot.png" || exit 1 - echo "[golden_refresh] 完成 — 菜单全屏基线已代入 game/tests/golden/menu_boot.png(提交说明须写明触发原因)" + mkdir -p "$REPO/game/tests/golden/$PLAT" + cp "$TMP/shots/shot_menu.png" "$REPO/game/tests/golden/$PLAT/menu_boot.png" || exit 1 + echo "[golden_refresh] 完成 — 菜单全屏基线已代入 game/tests/golden/$PLAT/menu_boot.png(提交说明须写明触发原因)" exit 0 fi @@ -56,21 +60,22 @@ for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \ assets/art/board/terrain_wall_n_underdark.png assets/art/board/terrain_floor_underdark_1.png \ assets/art/board/terrain_wall_n_deep_underdark.png assets/art/board/terrain_floor_deep_underdark_1.png; do if [ ! -f "$GAME/$a" ]; then - echo "[golden_refresh] 拒跑 — 素材缺位: $a(S3 停机点口径:基线禁锚字形回退态,先补素材+导入边车)" + echo "[golden_refresh] 拒跑 — 素材缺位: ${a}(S3 停机点口径:基线禁锚字形回退态,先补素材+导入边车)" exit 1 fi done -timeout 180 "$G" --path "$GAME_WIN" res://probe/board_shot_probe.tscn -- "$TMP/shots" >"$TMP/run.log" 2>&1 +gate_run_timeout 360 "$TMP/run.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/board_shot_probe.tscn -- "$TMP/shots" st=$? grep -E "\[board_shot\]" "$TMP/run.log" | tail -6 -if [ "$st" -ne 0 ] || [ ! -s "$TMP/shots/board_meta.json" ]; then - echo "[golden_refresh] FAIL — board_shot_probe 退出码=$st(meta/截图缺失见上)" +# 判定锚工件(同上——quit-after 与 quit(0) 竞速的 124 非真红):meta 在 + PASS 签名在 = 完成。 +if [ ! -s "$TMP/shots/board_meta.json" ] || ! grep -q "PASS — 三关双帧取证" "$TMP/run.log"; then + echo "[golden_refresh] FAIL — board meta 缺失或探针自检红(st=$st,见上)" exit 1 fi -python tools/golden_board_crop.py "$TMP/shots/board_meta.json" "$TMP/shots" "$TMP/crop" || exit 1 -mkdir -p "$REPO/game/tests/golden" +python3 tools/golden_board_crop.py "$TMP/shots/board_meta.json" "$TMP/shots" "$TMP/crop" || exit 1 +mkdir -p "$REPO/game/tests/golden/$PLAT" for b in board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png; do - cp "$TMP/crop/$b" "$REPO/game/tests/golden/$b" || exit 1 + cp "$TMP/crop/$b" "$REPO/game/tests/golden/$PLAT/$b" || exit 1 done -echo "[golden_refresh] 完成 — 三张基线已换代入 game/tests/golden/(提交说明须写明触发原因)" +echo "[golden_refresh] 完成 — 三张基线已换代入 game/tests/golden/$PLAT/(提交说明须写明触发原因)" diff --git a/tools/golden_compare.py b/tools/golden_compare.py index c98701df..c87decc8 100644 --- a/tools/golden_compare.py +++ b/tools/golden_compare.py @@ -19,6 +19,14 @@ from PIL import Image, ImageChops TOLERANCE = 16 # 灰度差容差(单通道) FAIL_RATIO = 0.02 # changed 占比失败线 +# 基线平台子目录:<黄金图根目录>/——渲染基线按平台各存(gate_platform_id 同口径), +# 平台内部照常逐像素严格比对;跨平台不互比(字形/抗锯齿机制差异,2026-11-03 用户裁)。 +_PLAT = {"win32": "windows", "darwin": "mac"}.get(sys.platform, "linux") + + +def plat_missing(base: Path) -> bool: + return not (base / _PLAT).is_dir() + def compare(base: Path, curr: Path, name: str, failures: list) -> None: pb, pc = base / name, curr / name @@ -48,6 +56,11 @@ def main() -> int: print("用法: python tools/golden_compare.py <黄金图目录> <新截图目录> [屏清单...]") return 1 base, curr = Path(sys.argv[1]), Path(sys.argv[2]) + base = base / _PLAT + if not base.is_dir(): + print("[golden] FAIL — 本平台(%s)基线目录未建:%s" % (_PLAT, base)) + print(" 处置:在该平台跑 tools/golden_baseline_refresh.sh 重制基线并提交(基线换代权限 Q6)") + return 1 shots = sys.argv[3:] or ["shot_menu.png", "shot_camp.png", "shot_level.png", "shot_battle.png"] failures = [] for name in shots: diff --git a/tools/hooks/post-commit b/tools/hooks/post-commit deleted file mode 100644 index dc986266..00000000 --- a/tools/hooks/post-commit +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -# 红态追绿钩子(本地 CI 触发钩子,2026-10-30 用户裁)——仅当最近一次 CI 判红时,本提交触发 -# 后台复验(tools/ci_runner.sh commit,判定在跑批件内);绿态与无状态零触发—— -# 小批直提不重复跑全量,同一份字节禁止第二次人手跑。主工作区限定。 -set -u -_GD="$(cd "$(git rev-parse --git-dir)" && pwd)" -_CD="$(cd "$(git rev-parse --git-common-dir)" && pwd)" -[ "$_GD" = "$_CD" ] || exit 0 -REPO="$(git rev-parse --show-toplevel)" -( nohup bash "$REPO/tools/ci_runner.sh" commit >/dev/null 2>&1 & ) -exit 0 diff --git a/tools/hooks/post-merge b/tools/hooks/post-merge index 13298b9c..f0ca94c6 100644 --- a/tools/hooks/post-merge +++ b/tools/hooks/post-merge @@ -1,12 +1,27 @@ #!/usr/bin/env bash -# 合回验证点(本地 CI 触发钩子,2026-10-30 用户裁)——主工作区 git merge 落地后无条件触发 -# 全量域闸后台跑批(tools/ci_runner.sh merge,约 4 分钟,不阻塞合并动作本身)。 -# 任务 worktree 内的合并不触发(CI 只认 master;worktree 批要提前验证就手动跑 ci_runner.sh)。 -# CI 红了不回滚合并——红项记入 game/tests/.gates/ci-status.json,修复提交经 post-commit 追绿。 +# 合回验证点(远端 CI 触发钩子,2026-11-03 用户裁)——主工作区 git merge 落地后: +# ① push origin master(触发远端 CI:Gitea 收到 push → 派发 mac-ci runner 按改动域跑闸) +# ② 后台挂 ci_watch.sh remote 等回执(回执落 ci-status.json,agent 下次开工读态零改动) +# 推送失败=远端验证不可能发生——大声呈报并落 push_failed 判定进 ci-status.json(防静默,下游开工读态即见)。 +# 任务 worktree 内的合并不触发(CI 只认 master);CI 红了不回滚合并——修复批合回后本钩子再推再验(追绿同链)。 set -u _GD="$(cd "$(git rev-parse --git-dir)" && pwd)" _CD="$(cd "$(git rev-parse --git-common-dir)" && pwd)" [ "$_GD" = "$_CD" ] || exit 0 REPO="$(git rev-parse --show-toplevel)" -( nohup bash "$REPO/tools/ci_runner.sh" merge >/dev/null 2>&1 & ) +TIP="$(git -C "$REPO" rev-parse HEAD)" +export PYTHONIOENCODING=utf-8 +if ! git -C "$REPO" push origin master >/dev/null 2>&1; then + echo "[post-merge] ✗ push origin master 失败——远端 CI 无法触发!先修推送(凭据/网络)后手动 git push,再手动挂 tools/ci_watch.sh remote $TIP" + python -I -c " +import json, os, datetime +p = r'$REPO/game/tests/.gates/ci-status.json' +d = {'commit': r'$TIP', 'verdict': 'push_failed', 'timestamp': datetime.datetime.now().astimezone().isoformat(timespec='seconds'), + 'duration_s': 0, 'note': 'post-merge 推送失败,远端 CI 未触发——修复推送后手动 git push + ci_watch remote', 'red_gates': [], 'key_lines': []} +os.makedirs(os.path.dirname(p), exist_ok=True) +json.dump(d, open(p, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) +" 2>/dev/null + exit 0 +fi +( nohup bash "$REPO/tools/ci_watch.sh" remote "$TIP" >/dev/null 2>&1 & ) exit 0 diff --git a/tools/probe_gate.sh b/tools/probe_gate.sh old mode 100644 new mode 100755 index 839caa7d..dcb7f2fb --- a/tools/probe_gate.sh +++ b/tools/probe_gate.sh @@ -17,10 +17,12 @@ # 四件带断言探针;默认段断言面零删减零改动(--full 只增不改)。 # 渲染面归 render_gate.sh 正主(窗口化:shot_probe 七屏+fx_shot/board_shot 取证+棋盘 golden 逐像素)。 # 用法: tools/probe_gate.sh [game目录] [--click] [--full] 缺省 game 目录=脚本所在仓库的 game/(worktree 内自动指向该 worktree) -# 依赖: Git Bash(coreutils timeout/taskkill) + PowerShell;Godot 路径可用环境变量 GODOT_BIN 覆盖。 +# 依赖: Git Bash(coreutils timeout/taskkill) + PowerShell(Windows 侧);macOS/Linux 走 tools/gate_platform.sh +# 共享跨平台件(Godot 路径两平台都可用 GODOT_BIN 环境变量覆盖)。 set -u G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}" REPO="$(cd "$(dirname "$0")/.." && pwd)" +. "$REPO/tools/gate_platform.sh" # 跨平台件(平台探测/路径形式/带超时执行/孤儿清杀) GAME=""; WITH_CLICK=0; WITH_FULL=0 for a in "$@"; do case "$a" in @@ -30,7 +32,7 @@ for a in "$@"; do esac done GAME="${GAME:-$REPO/game}" -GAME_WIN="$(cygpath -m "$GAME")" # Godot --path 需 Windows 形式 +GAME_WIN="$(gate_game_path "$GAME")" # Godot --path 接受形式(Windows 混合斜杠,其他平台原样) TMP="$(mktemp -d /tmp/probegate_XXXXXX)" trap 'rm -rf "$TMP"' EXIT _rc=0 @@ -52,10 +54,16 @@ fi verdict() { # verdict <探针名> <日志路径> <退出码> [签名标记] local name="$1" log="$2" st="$3" mark="${4:-PASS}" if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then + # macOS quit(0) 挂死受控例外:守卫回收后若 PASS 签名在位且错误扫描净=探针已完成判定, + # 挂死只发生在退出段(run26 实证:headless 探针同样挂)——按完成处理,签名缺失仍判红。 + if [ -n "$PY" ] && "$PY" "$REPO/tools/error_scan.py" "$log" >"$TMP/err.txt" 2>&1 && grep -q "$mark" "$log"; then + echo "[probe_gate] OK — $name 判定完成(exit=$st 为 macOS 退出段挂死回收,签名+扫描双证齐全)" + return 0 + fi echo "[probe_gate] FAIL — $name 超时挂死(exit=$st),孤儿已清"; return 1 fi if [ "$st" -ne 0 ]; then - echo "[probe_gate] FAIL — $name 退出码=$st(退出码为唯一权威判据,PASS 串不抵)"; return 1 + echo "[probe_gate] FAIL — $name 退出码=${st}(退出码为唯一权威判据,PASS 串不抵)"; return 1 fi if [ -n "$PY" ] && ! "$PY" "$REPO/tools/error_scan.py" "$log" >"$TMP/err.txt" 2>&1; then echo "[probe_gate] FAIL — $name 脚本错误扫描命中:" @@ -63,17 +71,16 @@ verdict() { # verdict <探针名> <日志路径> <退出码> [签名标记] return 1 fi if ! grep -q "$mark" "$log"; then - echo "[probe_gate] FAIL — $name 未见签名标记「$mark」(exit=$st——退出码 0 但零输出亦判红)"; return 1 + echo "[probe_gate] FAIL — $name 未见签名标记「${mark}」(exit=${st}——退出码 0 但零输出亦判红)"; return 1 fi return 0 } -orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(console wrapper+真引擎都算) - printf "Get-CimInstance Win32_Process -Filter \"Name like 'Godot%%'\" | Where-Object { \$_.CommandLine -like '*%s*' } | ForEach-Object { \$_.ProcessId }\n" "$GAME_WIN" > "$TMP/k.ps1" - powershell -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$TMP/k.ps1")" 2>/dev/null | tr -d '\r' +orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(实现单点在 gate_platform.sh,双平台) + gate_orphans "$GAME_WIN" } kill_orphans() { - local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done + gate_kill_orphans "$GAME_WIN" } # 每段跑批统一走本函数:后台启动记 MSYS PID→WINPID,守卫进程到点先 taskkill /T 整树回收 @@ -81,6 +88,14 @@ kill_orphans() { # 2026-10-25 在册挂死根源),后 kill -9 收 MSYS 侧。正常退出不动进程树;段后 kill_orphans 照跑兜底。 run_suite() { # run_suite <日志> <场景 res:// 路径> local t="$1" log="$2" scene="$3" + if ! gate_is_win; then + # macOS/Linux:无 console wrapper(Godot 单进程),走共享带超时执行+孤儿清杀兜底。 + local st + gate_run_timeout "$t" "$log" "$G" --headless --path "$GAME_WIN" "$scene" + st=$? + gate_kill_orphans "$GAME_WIN" + return "$st" + fi local kf="$TMP/killed.$RANDOM" rm -f "$kf" "$G" --headless --path "$GAME_WIN" "$scene" >"$log" 2>&1 & @@ -108,7 +123,7 @@ run_suite() { # run_suite <日志> <场景 res:// 路径> # 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付) if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then echo "[probe_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)" - timeout 300 "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1 + gate_run_timeout 300 "$TMP/refresh.log" "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1 kill_orphans fi @@ -183,8 +198,8 @@ fi echo "======== 残留检查(批末闸) ========" left="$(orphans | wc -l | tr -d ' ')" -total="$(tasklist 2>/dev/null | grep -ci '^Godot' || true)" -echo "本闸残留 Godot=$left(必须0) 全局 Godot 进程=$total(其他会话的仅报数不算账)" +total="$(gate_godot_count_all)" +echo "本闸残留 Godot=${left}(必须0) 全局 Godot 进程=${total}(其他会话的仅报数不算账)" if [ "$left" -ne 0 ]; then echo "[probe_gate] FAIL — 本闸残留未清零"; kill_orphans; _rc=1; fi if [ "$_rc" -eq 0 ]; then echo "[probe_gate] 全绿"; else echo "[probe_gate] 存在 FAIL,见上"; fi exit "$_rc" diff --git a/tools/render_gate.sh b/tools/render_gate.sh old mode 100644 new mode 100755 index d6443f25..00db59da --- a/tools/render_gate.sh +++ b/tools/render_gate.sh @@ -27,27 +27,28 @@ # 窗口化运行会短暂弹 Godot 窗口(本机可开窗口径);禁改 --headless 跑本闸 # (headless 无渲染面,探针内 fail-fast 护栏会红——那是护栏生效不是闸红)。 # 用法: tools/render_gate.sh [game目录] 缺省 game 目录=脚本所在仓库的 game/(worktree 内自动指向该 worktree) -# 依赖: Git Bash(coreutils timeout/taskkill) + PowerShell + python(PIL);Godot 路径可用环境变量 GODOT_BIN 覆盖。 +# 依赖: tools/gate_platform.sh(双平台封装)+ python(PIL);Windows 侧另用 taskkill/PowerShell,macOS/Linux 走共享件;Godot 路径可用环境变量 GODOT_BIN 覆盖。 set -u G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}" REPO="$(cd "$(dirname "$0")/.." && pwd)" +. "$REPO/tools/gate_platform.sh" # 跨平台件(平台探测/路径形式/带超时执行/孤儿清杀) GAME="${1:-$REPO/game}" -GAME_WIN="$(cygpath -m "$GAME")" # Godot --path 需 Windows 形式 +GAME_WIN="$(gate_game_path "$GAME")" # Godot --path 接受形式(Windows 混合斜杠,其他平台原样) +GOLDEN="$REPO/game/tests/golden/$(gate_platform_id)" # 本平台基线目录(按平台各存,比较器同口径) TMP="$(mktemp -d /tmp/rendergate_XXXXXX)" trap 'rm -rf "$TMP"' EXIT _rc=0 -orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(console wrapper+真引擎都算) - printf "Get-CimInstance Win32_Process -Filter \"Name like 'Godot%%'\" | Where-Object { \$_.CommandLine -like '*%s*' } | ForEach-Object { \$_.ProcessId }\n" "$GAME_WIN" > "$TMP/k.ps1" - powershell -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$TMP/k.ps1")" 2>/dev/null | tr -d '\r' +orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(实现单点在 gate_platform.sh,双平台) + gate_orphans "$GAME_WIN" } kill_orphans() { - local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done + gate_kill_orphans "$GAME_WIN" } scan_log() { # scan_log <段名> <日志>——脚本错误扫描;日志不可读(error_scan exit 2)同判红 local name="$1" log="$2" - if ! python "$REPO/tools/error_scan.py" "$log" >"$TMP/err_scan.txt" 2>&1; then + if ! python3 "$REPO/tools/error_scan.py" "$log" >"$TMP/err_scan.txt" 2>&1; then echo "[render_gate] FAIL — $name 脚本错误扫描未通过(命中或日志不可读):" sed -n '1,6p' "$TMP/err_scan.txt" | sed 's/^/ /' _rc=1 @@ -57,7 +58,7 @@ scan_log() { # scan_log <段名> <日志>——脚本错误扫描;日志不 # 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付) if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then echo "[render_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)" - timeout 300 "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1 + gate_run_timeout 300 "$TMP/refresh.log" "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1 kill_orphans fi @@ -66,16 +67,15 @@ if [ "$pre" -gt 0 ]; then echo "[render_gate] 预清本闸残留 Godot×$pre"; k echo "======== shot_probe 七屏截图 → shot_checker A-D ========" SHOT="$TMP/shots" -timeout 180 "$G" --path "$GAME_WIN" res://probe/shot_probe.tscn -- "$SHOT" >"$TMP/shot.log" 2>&1 +gate_run_timeout 360 "$TMP/shot.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/shot_probe.tscn -- "$SHOT" st=$?; kill_orphans grep -E "\[shot_probe\]" "$TMP/shot.log" | tail -12 -if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then - echo "[render_gate] FAIL — shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1 -elif [ "$st" -ne 0 ] || ! grep -q "问题=0" "$TMP/shot.log"; then - echo "[render_gate] FAIL — shot_probe 未干净完成(exit=$st,缺失屏/空文件/路径断裂见上)"; _rc=1 +# shot 判定锚工件:问题=0 签名在位=完成(124 为 macOS 退出段挂死回收/quit-after 竞速,非真红——run21/26 实证) +if ! grep -q "问题=0" "$TMP/shot.log"; then + echo "[render_gate] FAIL — shot_probe 未干净完成(exit=${st},缺失屏/空文件/路径断裂见上)"; _rc=1 fi if [ "$_rc" -eq 0 ]; then - python tools/shot_checker.py "$SHOT" shot_menu.png shot_settings.png shot_career.png shot_camp.png shot_level.png shot_battle.png shot_status.png | sed 's/^/[render_gate] /' + python3 tools/shot_checker.py "$SHOT" shot_menu.png shot_settings.png shot_career.png shot_camp.png shot_level.png shot_battle.png shot_status.png | sed 's/^/[render_gate] /' _rc="${PIPESTATUS[0]}" [ "$_rc" -ne 0 ] && echo "[render_gate] FAIL — shot_checker A-D 断言存在红(S1 停机点:首跑存量 FAIL 全量呈报登记待办清单)" fi @@ -83,14 +83,12 @@ scan_log "shot_probe" "$TMP/shot.log" echo "======== fx_shot_probe 三时刻取证(Q7) ========" FX="$TMP/fxshots" -timeout 180 "$G" --path "$GAME_WIN" res://probe/fx_shot_probe.tscn -- "$FX" >"$TMP/fx.log" 2>&1 +gate_run_timeout 360 "$TMP/fx.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/fx_shot_probe.tscn -- "$FX" st=$?; kill_orphans grep -E "\[fx_shot_probe\]" "$TMP/fx.log" | tail -4 -if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then - echo "[render_gate] FAIL — fx_shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1 -elif [ "$st" -ne 0 ]; then - echo "[render_gate] FAIL — fx_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1 -fi +# fx 判定锚工件:取证双检(fx_evidence)+ 错误扫描(scan_log)已覆盖真红——macOS 下 +# quit-after 与 quit(0) 竞速的 124 退出非真红(run21 实证),不单独判闸。 + scan_log "fx_shot_probe" "$TMP/fx.log" fx_evidence() { # 取证双检:模式文件存在且非零字节(零字节/缺失任一即 FAIL——Q7) local pat="$1" min="$2" n=0 f @@ -108,22 +106,19 @@ fx_evidence "fx_final.png" 1 echo "======== board_shot_probe L1-L3 取证(Q7) ========" BD="$TMP/boardshots" -timeout 180 "$G" --path "$GAME_WIN" res://probe/board_shot_probe.tscn -- "$BD" >"$TMP/bd.log" 2>&1 +gate_run_timeout 360 "$TMP/bd.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/board_shot_probe.tscn -- "$BD" st=$?; kill_orphans grep -E "\[board_shot\]" "$TMP/bd.log" | tail -6 -if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then - echo "[render_gate] FAIL — board_shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1 -elif [ "$st" -ne 0 ]; then - echo "[render_gate] FAIL — board_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1 -fi +# board 判定锚工件:board_meta.json 非零 + 错误扫描覆盖真红;124 竞速退出非真红(同 fx)。 + scan_log "board_shot_probe" "$TMP/bd.log" for b in board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png board_meta.json; do if [ ! -s "$BD/$b" ]; then echo "[render_gate] FAIL — 取证缺失/零字节: $b"; _rc=1; fi done echo "======== 棋盘 golden 逐像素(T2——裁剪→对比,容差 provisional) ========" -if [ ! -d "$REPO/game/tests/golden" ]; then - echo "[render_gate] FAIL — game/tests/golden/ 基线目录缺失(基线在库=验收断言②)"; _rc=1 +if [ ! -d "$GOLDEN" ]; then + echo "[render_gate] FAIL — 本平台($(gate_platform_id))基线目录缺失:${GOLDEN}(基线按平台各存、在库=验收断言②)"; _rc=1 elif [ ! -s "$BD/board_meta.json" ]; then echo "[render_gate] SKIP — board_meta.json 缺失(取证段已红),golden 对比不判" else @@ -136,15 +131,15 @@ else assets/art/board/terrain_wall_n_dungeon.png assets/art/board/terrain_floor_dungeon_1.png \ assets/art/board/terrain_wall_n_underdark.png assets/art/board/terrain_floor_underdark_1.png \ assets/art/board/terrain_wall_n_deep_underdark.png assets/art/board/terrain_floor_deep_underdark_1.png; do - [ -f "$GAME/$a" ] || { echo "[render_gate] FAIL — 棋盘素材缺位: $a(Q5:基线锚素材齐盘态,缺盘不得出验收结论)"; _asset_miss=1; } + [ -f "$GAME/$a" ] || { echo "[render_gate] FAIL — 棋盘素材缺位: ${a}(Q5:基线锚素材齐盘态,缺盘不得出验收结论)"; _asset_miss=1; } done if [ "$_asset_miss" -eq 0 ]; then CROP="$TMP/crop" - python tools/golden_board_crop.py "$BD/board_meta.json" "$BD" "$CROP" | sed 's/^/[render_gate] /' + python3 tools/golden_board_crop.py "$BD/board_meta.json" "$BD" "$CROP" | sed 's/^/[render_gate] /' if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo "[render_gate] FAIL — 棋盘区裁剪失败"; _rc=1 else - python tools/golden_compare.py "$REPO/game/tests/golden" "$CROP" \ + python3 tools/golden_compare.py "$REPO/game/tests/golden" "$CROP" \ board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png | sed 's/^/[render_gate] /' if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo "[render_gate] FAIL — golden 逐像素超容差(S2 停机点:字体/平台误红呈报后定容差)"; _rc=1 @@ -154,8 +149,8 @@ else fi echo "======== 菜单 golden 逐像素(迭代 28 T4——启动态全屏基线)========" -if [ ! -s "$REPO/game/tests/golden/menu_boot.png" ]; then - echo "[render_gate] FAIL — game/tests/golden/menu_boot.png 基线缺失(菜单 golden 未建——tools/golden_baseline_refresh.sh --menu)"; _rc=1 +if [ ! -s "$GOLDEN/menu_boot.png" ]; then + echo "[render_gate] FAIL — $GOLDEN/menu_boot.png 基线缺失(菜单 golden 未建——本平台跑 tools/golden_baseline_refresh.sh --menu)"; _rc=1 elif [ ! -s "$SHOT/shot_menu.png" ]; then echo "[render_gate] SKIP — shot_menu.png 缺失(shot_probe 段已红),菜单 golden 不判" elif [ ! -f "$GAME/assets/fonts/ui_font_fusion_pixel_zh_hans.ttf" ] || [ ! -f "$GAME/assets/art/ui/button_stone.png" ]; then @@ -164,7 +159,7 @@ else MENUG="$TMP/menugolden" mkdir -p "$MENUG" cp "$SHOT/shot_menu.png" "$MENUG/menu_boot.png" - python tools/golden_compare.py "$REPO/game/tests/golden" "$MENUG" menu_boot.png | sed 's/^/[render_gate] /' + python3 tools/golden_compare.py "$REPO/game/tests/golden" "$MENUG" menu_boot.png | sed 's/^/[render_gate] /' if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo "[render_gate] FAIL — 菜单 golden 超容差(S2 停机点口径:核对热区——按钮换墨/布局变更属预期面=显式重制基线+呈报;超预期面=L3)"; _rc=1 fi @@ -172,8 +167,8 @@ fi echo "======== 残留检查(批末闸) ========" left="$(orphans | wc -l | tr -d ' ')" -total="$(tasklist 2>/dev/null | grep -ci '^Godot' || true)" -echo "本闸残留 Godot=$left(必须0) 全局 Godot 进程=$total(其他会话的仅报数不算账)" +total="$(gate_godot_count_all)" +echo "本闸残留 Godot=${left}(必须0) 全局 Godot 进程=${total}(其他会话的仅报数不算账)" if [ "$left" -ne 0 ]; then echo "[render_gate] FAIL — 本闸残留未清零"; kill_orphans; _rc=1; fi if [ "$_rc" -eq 0 ]; then echo "[render_gate] 全绿"; else echo "[render_gate] 存在 FAIL,见上"; fi exit "$_rc" diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh old mode 100644 new mode 100755 index 341b4efe..d2af73fc --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -4,8 +4,8 @@ # 漏跑 probe_gate --full 的成因:清单散落三份文档、彼此不一致、且无机器校验)。 # # 跑完写回执 game/tests/.gates/receipt.json(commit+内容树哈希+各闸退出码+存量基线比对+时间戳); -# 本地 CI(tools/ci_runner.sh,2026-10-30 用户裁)读回执写 ci-status.json——强制验证点=合回 master -# 后 post-merge 钩子触发的全量跑批;本脚本手动调用(--code/--docs/全量档)只服务开发期按域跑批。 +# 远端 CI(2026-11-03 用户裁)读回执判定,回执经 tools/ci_watch.sh remote 轮询 Gitea API 写 ci-status.json—— +# 强制验证点=合回 master 后 post-merge 钩子推送触发的远端跑批;本脚本手动调用(--code/--docs/全量档)只服务开发期按域跑批。 # 跑闸时代码面带着未提交改动,回执会标 dirty_game(验的不是 HEAD 字节),CI 判定照常落账留痕。 # # 用法:tools/run_all_gates.sh [--code|--docs|--quick] @@ -44,6 +44,7 @@ GAME="$REPO/game" STAMP_DIR="$GAME/tests/.gates" RECEIPT="$STAMP_DIR/receipt.json" BASELINE="$STAMP_DIR/baseline.json" +. "$REPO/tools/gate_platform.sh" # 跨平台件(平台探测/路径形式/带超时执行) QUICK=0 DOMAIN="full" @@ -93,8 +94,10 @@ fi # ── python 解释器解析:优先带 PIL 的 gamedev venv(渲染闸的 shot_checker/golden_compare 依赖), # 回落 PATH 上的 python。渲染闸的三个脚本要 PIL,用错解释器会假红。 PY="" -for cand in "/d/dev/python/envs/gamedev/Scripts/python.exe" "$(command -v python 2>/dev/null)" "python"; do +for cand in "/d/dev/python/envs/gamedev/Scripts/python.exe" "$(command -v python 2>/dev/null)" "python3" "python"; do [ -z "$cand" ] && continue + # 存在性守卫先行:Windows venv 路径在非 Windows 平台不存在,误选会让 PY 指向幽灵路径整批崩。 + command -v "$cand" >/dev/null 2>&1 || [ -x "$cand" ] || continue if "$cand" -c "import PIL" >/dev/null 2>&1; then PY="$cand"; break; fi [ -z "$PY" ] && PY="$cand" done @@ -282,7 +285,7 @@ if in_domain code; then echo "[gates] 关联档选闸·申报:未找到在册需求卡片(diff 与 roadmap 进行中行均无),申报集为空——仅按 diff 推导" fi if [ "$SCOPE" = related ]; then - echo "[gates] 关联档选闸:02 选套件跑(--only=$ONLY_TERMS)" + echo "[gates] 关联档选闸:02 选套件跑(--only=${ONLY_TERMS})" fi fi fi @@ -319,9 +322,10 @@ gate() { # gate <名称> <日志名> <命令...> echo "════════════════════════════════════════════" "$@" >"$STAMP_DIR/$log" 2>&1 local rc=$? - # 失败时把关键行回显到终端(红必须看得见) + # 失败时把关键行回显到终端(红必须看得见——明细行形态 [Rx]/[Sxx]/违规/存量 同回显, + # 远端 runner 只回传终端日志,折叠过窄=红因滞留 runner 侧不可诊断) if [ $rc -ne 0 ]; then - grep -E "FAIL|红|Error|error" "$STAMP_DIR/$log" | tail -12 | sed 's/^/ | /' + grep -E "FAIL|红|Error|error|违规|存量|^\[|\] FAIL" "$STAMP_DIR/$log" | tail -60 | sed 's/^/ | /' fi NAMES+=("$name"); EXITS+=("$rc"); LOGS+=("$log") return 0 @@ -338,7 +342,7 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分 local _shards="${GATE_SHARDS:-4}" echo "" echo "════════════════════════════════════════════" - echo " $name(--script 模式,退出码为唯一判据)" + echo " ${name}(--script 模式,退出码为唯一判据)" if [ "$SCOPE" = related ] && [ -n "$ONLY_TERMS" ]; then echo " 关联档选跑:--only=$ONLY_TERMS" fi @@ -348,7 +352,7 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分 echo "════════════════════════════════════════════" local rc=0 if [ "$_shards" -le 1 ]; then - local -a _cmd=(--headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd) + local -a _cmd=(--headless --path "$(gate_game_path "$GAME")" --script res://tests/run_all.gd) [ ${#_user_args[@]} -gt 0 ] && _cmd+=(-- "${_user_args[@]}") ( cd "$REPO" && "$GODOT" "${_cmd[@]}" ) \ >"$STAMP_DIR/unit.log" 2>&1 @@ -360,8 +364,8 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分 local -a _pids=() local k for k in $(seq 1 "$_shards"); do - ( cd "$REPO" && "$GODOT" --headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd \ - -- "${_user_args[@]}" "--shard=$k" "--shard-count=$_shards" ) \ + ( cd "$REPO" && "$GODOT" --headless --path "$(gate_game_path "$GAME")" --script res://tests/run_all.gd \ + -- ${_user_args[@]:+"${_user_args[@]}"} "--shard=$k" "--shard-count=$_shards" ) \ >"$STAMP_DIR/unit_shard_$k.log" 2>&1 & _pids+=($!) done @@ -385,7 +389,9 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分 rc=1 tail -12 "$STAMP_DIR/unit.log" | sed 's/^/ | /' fi - [ $rc -ne 0 ] && tail -12 "$STAMP_DIR/unit.log" | sed 's/^/ | /' + if [ $rc -ne 0 ]; then + grep -E "FAIL res://|超时|ABORT|个套件失败|套件未跑|SCRIPT ERROR|Parse Error" "$STAMP_DIR/unit.log" | tail -80 | sed 's/^/ | /' + fi NAMES+=("$name"); EXITS+=("$rc"); LOGS+=("unit.log") return 0 } diff --git a/tools/run_smoke.py b/tools/run_smoke.py index f6b756cd..cbf34fa8 100644 --- a/tools/run_smoke.py +++ b/tools/run_smoke.py @@ -18,13 +18,15 @@ r"""run_smoke.py — 冒烟测试运行器(脚本错误外部门禁) ③ 输出无「SCRIPT ERROR」「Parse Error」标记行(标记清单供 tools/error_scan.py 复用,禁另写一份)。 """ import argparse +import os import re import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent -DEFAULT_GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe" +# 引擎定位:GODOT_BIN 环境变量优先(远端 CI 与非 Windows 平台的注入口),Windows 本机固定路径保底。 +DEFAULT_GODOT = os.environ.get("GODOT_BIN") or r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe" DEFAULT_SLICE = ROOT / "game" DEFAULT_SCENE = "res://tests/smoke_boot.tscn" SUMMARY_RE = re.compile(r"\[smoke\] 摘要:(\d+) 项检查,(\d+) 项失败") diff --git a/tools/run_system_test.py b/tools/run_system_test.py index 9d49bbe2..43711a3c 100644 --- a/tools/run_system_test.py +++ b/tools/run_system_test.py @@ -19,13 +19,15 @@ r"""run_system_test.py — 系统测试运行器(脚本错误外部门禁) ③ 输出无「SCRIPT ERROR」「Parse Error」标记行(标记清单经 tools/error_scan.py 复用,禁另写一份)。 """ import argparse +import os import re import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent -DEFAULT_GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe" +# 引擎定位:GODOT_BIN 环境变量优先(远端 CI 与非 Windows 平台的注入口),Windows 本机固定路径保底。 +DEFAULT_GODOT = os.environ.get("GODOT_BIN") or r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe" DEFAULT_SLICE = ROOT / "game" DEFAULT_SCRIPT = "res://tests/system_test.gd" # 系统测试宿主 SUMMARY_RE = re.compile(r"(?:冒烟完成|\[system_test\] 摘要):(\d+) 项检查,(\d+) 项失败") diff --git a/tools/setup_worktree_userdir.sh b/tools/setup_worktree_userdir.sh old mode 100644 new mode 100755 diff --git a/tools/sync_worktree_assets.sh b/tools/sync_worktree_assets.sh old mode 100644 new mode 100755 diff --git a/体验验证清单.md b/体验验证清单.md index 7c36e08b..ba705294 100644 --- a/体验验证清单.md +++ b/体验验证清单.md @@ -590,3 +590,32 @@ - **红灯信号**:窗壳挤压屏面、场景几乎不可见、滚动卡滞或内容溢出窗板即报缺陷。 - **结论回流**:观感校准意见回填 camp.md 终值(普通设计决策);呈现缺陷归待办清单登记新条目。 + +## ㊾ CI 迁远端工程批——Mac 端渲染基线目检(2026-11-03 随工程批登记) + +> CI 迁移远端(Mac 本机 host 模式)工程批:渲染基线按平台各存(windows/ 已有,mac/ 待 Mac 端重制入库)。 +> 基线重制与目检在批次端到端验收时执行(Mac 端跑 tools/golden_baseline_refresh.sh 生成后人工判定)。 +> 登记不阻塞(2026-10-12 真人检测立规)。 + +| # | 目检项 | 判据 | +|---|---|---| +| 1 | Mac 端 golden 基线正确性 | mac/ 子目录四张基线(棋盘三图+menu_boot)重制生成后人工判定:布局骨架与现网一致、无素材缺位字形回退、无渲染异常(错位/透明块/花屏);判定通过后经下一批提交入库,此后渲染闸在 Mac 上以该套基线逐像素比对 | +| 2 | Mac 端渲染闸全链目检 | 远端游戏码批跑渲染闸时 Mac 桌面短暂弹 Godot 窗口属预期(窗口化截图取证);七屏截图与三段取证文件齐全、shot_checker A–D 无红 | + +- **红灯信号**:基线帧出现素材缺位字形、明显错位或花屏;渲染闸报「取证缺失/零字节」。 +- **结论回流**:基线异常=登记待办清单(或当场重制复核);渲染缺陷归待办清单登记新条目。 + +## ㊿ 迭代 50——敌方状态个体化与文案读表目检(2026-11-03 随批后收尾登记) + +> 三卡合回(2026-11-03,合并提交 8d3a12ec):卡① 敌方状态个体化、卡② 断言补齐、卡③ 架构审查 3.1+3.2 整改。独立验收审计两轮终判通过(首轮缺漏三条→整改笔 6cb1cf5e→复验通过);机检读数与论证详见迭代 50 实现批提交(合并提交 8d3a12ec 一线可考)。目检清单原编㊾-A~E 与在册㊾〔CI 迁远端批〕撞号,登记改用㊿。登记不阻塞(2026-10-12 真人检测立规)。 + +| # | 目检项 | 判据 | +|---|---|---| +| 1 | 敌方增益图标个体投影 | 增益图标各立绘各现各的——敌 A 有削弱时敌 B 不出现同款图标(各读各的桶,不再全体同现) | +| 2 | 敌方增益技自体落点 | 蓄力/硬化/护身只强化施放者自己,不再全体受益 | +| 3 | RAP 不足 toast 读表值 | 「弃图刷新需 N」的 N 随 rap_refresh_cost 表值显示,改表文案随动 | +| 4 | 每轮刷新上限 toast 读表值 | 「每轮 N 次」的 N 随 refresh_cap_per_round 表值显示,同上 | +| 5 | 散折价 80% 购买价(回归性质) | economy_spend 激活时商店购买价按 80% 折后价显示与扣款 | + +- **红灯信号**:图标全体同现、增益技全体受益、toast 数字与表值不符、折后价未生效即报缺陷。 +- **结论回流**:呈现缺陷归待办清单登记新条目。 diff --git a/待办清单.md b/待办清单.md index f185a3f8..f1f07c99 100644 --- a/待办清单.md +++ b/待办清单.md @@ -12,17 +12,17 @@ **状态**:待文档设计 -#### 架构审查优化讨论——全库架构审计读数的逐条裁决与整改分批(2026-10-29 登记,来源=审计发现) +#### 架构审查余项——闸面缺口、豁免存废、零测试点名与架构文档漂移(2026-11-03 登记,来源=批次移交) -**实况**:2026-10-29 全库架构静态审计完成,读数见 reports/审计-全库架构审查-2026-10-29.md(七节:总评/健康面/P0/P1/P2/待裁点/取证口径;零测试执行零引擎运行,全部结论带 文件:行号)。健康面已逐条核干净:Core 零 UI 八项判据全零命中、core 内 RNG 单点 100% 成立、字符串式信号连接全库 0 处、core 层零超长函数、19 表 ↔ 19 schema 一一对应、无循环 preload。问题集中在三条「写了没闸守」的纪律被实际破坏:①数值单一事实源被 18 处硬编码击穿,最危险两处是 `presentation/main.gd:787` 与 `:790` 把 `rap_refresh_cost=8` 与 `refresh_cap_per_round=2` 写死进提示文案(紧邻 783 行是正确读表的),表改文案不改、玩家直接受错误数字;另有 4 组代码自标「机制结构常量/产出约束」的字面量(`node_map_generator.gd:12` `MAX_WIDTH`、`action_economy.gd:14` `CHARGE_LAYER_CAP`、`board_data.gd:9` `SPAWN_AREA_CELLS`、`node_map_generator.gd:83-94` 域字面量)是否入表待裁。②`game/sim/` 与 `game/core/` 四组平行实现——扫雷推理引擎双份(`board_generator.gd` `measure_inferability` 族 vs `auto_player.gd` `_deduce` 族,输入集口径不同:前者含 safe_set oracle、后者 no-peek)、伤害掷点期望重算(`damage_probe.gd:196-201`)、档位排序与驱动循环双份(`level_autoplay.gd` vs `treasure_item_table.gd:44` 与 `battle_driver.gd:17`)、FakeDB 伪造表喂生成器——直接威胁「模拟读数 = 游戏真实数值」这一数值验证前提。③RNG 单点纪律在 core 外失守(sim 4 处/probe 1 处/tests 3 处自建 `RandomNumberGenerator`,均显式定种、确定性尚存),而 `static_scan.sh` 的 S1/S2 扫描面只含 `DIR_CORE` + `DIR_PRES`。守卫面另有三缺口:S8 是 9 文件显式清单(新增结算文件忘登记=静默不扫,缺位自检只防删改不防增)、S9 类型闸与 S13 质量闸扫描面止步于 core+data+presentation(sim/probe/tests 三层除 S11/S12 外零闸)、S13 三处豁免常态化到最大户文件(`battle_orchestrator`/`main.gd`/`tune_sim_controller`);另有四个 core 文件零测试点名(`attune_loadout`/`battle_fx_host`/`meta_state_store`/`slot_store`)。P2 级:`core/settlement/` 五段类直读写编排器私有成员 20+ 处(`oc._damage.*`/`oc._enemy_system.*`,GDScript 无编译期检查、改名静默断裂);架构文档三处漂移(`architecture.md:110` 登记的 `core/hero_ledger.gd` 盘上不存在且 `hero_ledger_test.gd:171` 显式断言它必须不存在、`:135` 行数过期、EC8 热重载门后 `ConfigDb.reload()` 零调用方且走 `load()` 拿不到磁盘新值);class_name 级双向依赖(`core/config_db` → `data/schemas/board_gen_table` → `core/board_data.SPAWN_AREA_CELLS`)与 `NODE_TYPES` 词表双份;`MetaProgression` 是唯一用 `get_node("/root/...")` 取依赖、唯一跨 Autoload 直调的单例。 +**实况**:2026-10-29 全库架构静态审计的四组待裁余项,自「架构审查优化讨论」条目迁入(该条目的 3.1+3.2 部分已随迭代 50 卡③实现合回〔合并提交 8d3a12ec〕,余项不在本卡,按未决先迁再死迁立本条)。四组余项(审计待裁点 ④⑤⑥⑦,读数为审计时点,逐项现值以仓库为准):①守卫闸面缺口——S1/S2 随机函数扫描面止步 core+presentation(sim 4 处/probe 1 处/tests 3 处自建 RNG 不在扫)、S9 类型闸与 S13 质量闸扫描面不含 sim/probe/tests、S8 是 9 文件显式清单(缺位自检只防删改不防增);②S13 质量闸豁免存废——审计时点三处豁免常态化到最大户文件(battle_orchestrator 已随拆件批收窄,余量以闸件为准);③零测试点名四件(attune_loadout/battle_fx_host/meta_state_store/slot_store);④架构文档漂移三处(architecture.md 登记的 core/hero_ledger.gd 盘上不存在且测试断言它必须不存在、行数过期、EC8 热重载门后 ConfigDb.reload() 零调用方且走 load() 拿不到磁盘新值)。凭据:reports/审计-全库架构审查-2026-10-29.md(待裁点节)+reports/处置-架构审查3.1+3.2整改-2026-11-02.md(3.1+3.2 闭合凭据,随迭代 50 合回)。 -**2026-11-02 设计闭合(3.1+3.2 部分,用户逐项裁决)**:凭据=reports/处置-架构审查3.1+3.2整改-2026-11-02.md(总判据六条+逐项裁决记录+设计方案甲~庚+取证事实 18 处现址表)。用户总判据:机制数据一律长在所属封装实体的内部属性(表行字段)里,全局常量与无主账本清零;不要任何缺省值(缺字段=加载期报错);死代码不保留;sim 零规则副本;伤害期望独立复算=按需诊断件不进闸。GDD 已落文五份(天赋树 v0.12/关卡节点图生成 v1.4/冒险结构 v1.4/地形变异 v1.2/棋盘生成 v1.2,worktree 提交 15352170,文档域闸全绿)。全部改动(含 sim 面)归同一张需求卡片(用户裁,完整性优先)。**本条未闭合余项**(审计待裁点 ④⑤⑥⑦,不在本卡):闸面缺口(S1/S2/S9/S13 扫描面、S8 目录域)、S13 三处豁免存废、零测试点名四件、架构文档漂移三处——留待后续讨论段,不阻塞本卡。 - -**2026-11-02 写卡(2026-11-03 修订闭合)**:需求卡片 reports/需求卡片-架构审查3.1+3.2整改批-2026-11-02.md(v1.2——子代理写卡+grilling 一轮 19 条折入+godot-master 式终审 4 条折入,pipeline_gate PASS,worktree 提交 2f43b81d);11 张 T 卡估时合计 8 工作日(超 10 须特批提示在卡);能力行 10 增 5 改 1 删已入 game/tests/README.md。 +**缺口**:四组余项中①②③ 已裁完并落地(见下 2026-11-03 记录),还差④**架构文档漂移三处**的裁决与落文(`architecture.md` 的 HeroLedger 登记行、battle_orchestrator 行数与成员数、EC8 热重载门后 `ConfigDb.reload()` 零调用方)——属审计 §5.2/待裁点⑦,需再走一次设计讨论;同族的审计 §5.1(settlement 私有成员直取,实测 105 处)/§5.3(NODE_TYPES 词表双份)/§5.4(MetaProgression 取依赖形态)尚未登记本条,随④同批处置时一并入册。 **改动面**:文档+游戏代码+工具代码 -**状态**:文档设计已闭合+已写卡+已编迭代 50 +**状态**:文档设计中 + +**2026-11-03 设计讨论裁决(①②③ 三组,用户逐项点头)**:凭据=reports/审计-全库架构审查-2026-10-29.md §四 各节「2026-11-03」标注。①**闸面缺口**——S9 类型闸/S10 文件头闸/S13 质量闸**不扩**到 sim/probe/tests(三层存量只读数实跑≈2162 处,其中禁调试打印 274+循环内 await 70 属工具码功能需要;产品码两面现归零,扩面即毁执法前提),改为在 code-conventions §4 新增第 13 条把适用域按来源写清(tests→闸00/01/02、probe→闸03/04/05〔含两件不入闸的人工取证件与共用面〕、sim→不入 CI 的手动调参批量),并停用「测量面」对该三目录的统称(保留 run_all_gates 选闸原义);**S1/S2 随机闸同样不扩**——三目录自建随机源显式定种属合法测量口径,禁的不定种形态改由文字纪律+独立验收审计承载(用户令:说明写规约,不写 README)。②**S13 豁免存废**——结案销号:审计时点三处豁免已于 2026-10-31 迭代 44 批后收尾清偿(基线现 5 键只降不升,commit 9eedf896),本组无对象可裁、不再叠清偿期限。③**零测试点名**——定性改判为「无按文件登记的惯例」而非「没测」(11 件零点名件全有行为断言,只是不经类名;读数由审计的 4 件订正为 11 件,其中三件是迭代 44/46/49 拆件产物=复发模式);立每 core 件点名闸与补点名套件两案裁否(撞 test-conventions §7「覆盖率不是目标」),改为落 test-conventions §8.3「新增 core 件的覆盖登记义务」+process-conventions §3 第 6 条承接指向。**已落地闸件(工程改动,零卡片,用户令在本会话车道内直改)**:static_scan.sh 的 S8 加 settlement/ 目录域反向对账+S7/S8 两份 14 文件清单并成单点 `B1_SET`+「10 个 .gd」文案改派生计数;audit_type_hints.py/audit_file_headers.py 各补扫描面空转自检;architecture S 表 S8 行与 S9/S10 偏差句同批同步。验证=static_scan 全量 13 条规则绿退出码 0、两件负证返回 1、合回后远端 CI `66407117` green。 #### 三关色相 × 五组微光互辨抽测——三关图类型色板终值冻结的前置抽测(2026-10-28 登记,来源=会话提及) @@ -47,20 +47,6 @@ **状态**:文档设计已闭合 -#### 能力清单覆盖缺口补齐——六类无断言面或设计未定项(2026-10-29 登记,来源=会话提及) - -**实况**:实际情况=2026-11-02 设计讨论闭合——④除名(用户裁:死亡灰化/碎裂属纯表现锦上添花,探针+人工目检已是合规终态,呈现与 battle.md 承诺行零触碰);范围收窄:④除名后余五类,其中②多语言半边(P1 功能缺口,归 owner 文档待设计问题清单)、③音频(P1 后置)、⑤数值落带半边(归 M4 数值批)三块延期不在本条,可写卡面=①增益状态结算链、②减动效开关、⑤机制门控、⑥锻造工坊结构四处。关联文档: -- reports/需求卡片-断言补齐批-2026-11-02.md(2026-11-02,本条需求卡片——已写卡凭据) -- `game/tests/README.md` 覆盖域表对照行(2026-10-29 立,缺口证据;2026-11-02 随卡刷新五行能力行与覆盖域口径) -- `design/gdd/systems/跨局循环/锻造工坊.md` v0.11(2026-11-02,验收标准节九条判据落文——⑥写卡输入合同) -- `design/gdd/systems/战斗循环/增益状态.md` v1.4(①结算链规则定稿) -- `design/gdd/systems/冒险循环/难度曲线与Boss.md` v1.2(⑤机制门控规则定稿;数值带 provisional 归 M4) -- `design/gdd/systems/表现与支撑/设置与本地化.md` v1.1(②减动效开关与呈现承诺现行口径) - -**改动面**:游戏代码+工具代码 - -**状态**:文档设计已闭合+已写卡+已编迭代 50 - #### 数值标定窗口总挂账——原九条数值类条目合并(2026-10-28 登记,来源=会话提及) **实况**:2026-10-28 用户令把九条数值类挂账合并为一条:削弱档位的最终数值/迭代 10 奖励重构联动欠账/调参批数值点评登记/三方向各自的解锁清单与定价/GP 经济换算失衡/天赋树实装批登记欠账余两件/商店独立刷新机制两参数/基线通关率越带加难杠杆扫描/成长系数节奏——原九条同步删除,内容全量迁入汇总文档,git 历史可考(本提交)。共同背景:M4 数值批卡已随 2026-10-16 删卡令退役;2026-10-18 用户裁=各条留待办清单、未设计未启动、不另行归属,未来数值标定窗口重开时重走设计段再写卡——本条沿用该裁定,重启条件=用户发令开窗口。合并动机=九条共享同一重启条件与同一执行形态(窗口内一次 MC 批量统一覆盖各条对比需求:全冒险期望+奖励期望),分散登记徒增维护面。逐条挂账明细、裁定史与相关文档见汇总文档。 @@ -164,19 +150,6 @@ **状态**:待文档设计 -#### 敌方状态个体化——状态实例归属到敌人个体(2026-11-02 登记,来源=会话提及) - -**实况**:实际情况=用户裁敌方侧共享设计推翻——敌人是个体,状态实例必须按个体归属("对全体"=对每个存活敌各发一条实例);附带裁决:敌方增益技目标=施放者自己(现行一只施放全体受益属实现错误)、目标规则词表收四词(全体存活敌/自己/单体/顺位最前,种类级等首个内容批再加词)、个体死亡即清名下实例、呈现各读各的。存储方案=逐个体授予独立实例(用户裁 A 案;权威调研十余案例同构——Slay the Spire/Into the Breach/Cataclysm DDA/Unreal GAS 等,阵营桶仅服务换人继承类需求,本项目敌人开战一次性入场无此路径)。原「单体寻址的接口扩展位」条目(2026-10-18 登记)随本条并入退役——其五条语义被个体归属通则吸收。关联文档: -- `design/gdd/systems/战斗循环/增益状态.md` v1.5(2026-11-02,硬规矩改「实例按个体归属」+〈目标规则与个体归属〉节定稿) -- `design/gdd/systems/战斗循环/敌人系统.md` v1.5(2026-11-02,增益技落点=施放者自己+警觉授予随动) -- `design/gdd/systems/战斗循环/战斗结算.md` v1.6(2026-11-02,警觉与 AC 合流句个体化随动) -- `design/gdd/ux/battle.md`(2026-11-02,敌方增益呈现行个体化随动) -- reports/需求卡片-敌方状态个体化批-2026-11-02.md(2026-11-02,本条需求卡片——已写卡凭据) - -**改动面**:游戏代码 - -**状态**:文档设计已闭合+已写卡+已编迭代 50 - #### 冒险前资源配比预设(构筑提案)(2026-09-18 登记,来源=会话提及) **实况**:构筑提案 2026-09-18 立项、八项裁决全裁(2026-10-04),设计闭合机制=[元进度](design/gdd/systems/跨局循环/元进度.md)「冒险开始参数面与覆盖值通道」(零新机制);2026-10-05 挂起——移出 roadmap 迭代 14,八项裁决与需求卡片冻结保留,重启须用户另行发令;全案与淘汰依据原载已退役提案(git 历史可考),裁决细节冻结在卡片。关联文档:[元进度](design/gdd/systems/跨局循环/元进度.md) v1.0(闭合凭据;现行 v1.25 已复核在役,2026-10-26 文档体检第 10 项);需求卡片 reports/需求卡片-冒险前资源配比预设批-2026-10-04.md(文件头挂冻结横幅)。