From 0fc1cb695be4dbab7a8227b6df020ca92526cb01 Mon Sep 17 00:00:00 2001 From: lili Date: Mon, 6 Jul 2026 20:40:11 -0700 Subject: [PATCH] =?UTF-8?q?feat(polaris):=20=E3=80=8A=E5=B1=B1=E6=B5=B7?= =?UTF-8?q?=E5=B7=A1=E5=BC=82=E5=BD=95=E3=80=8BM3=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=88=98=E6=96=97=E7=BB=98=E5=88=B6=20TDZ=20=E6=A0=B9=E5=9B=A0?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D+=E5=8F=8C=E8=81=8C=E4=B8=9A=20154=20?= =?UTF-8?q?=E5=8D=A1+=E8=BF=9B=E9=98=B6=20A0=E2=80=93A15=20=E5=85=AD?= =?UTF-8?q?=E5=B8=A6=E5=85=A8=E7=BB=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 绘制修复(mini-desktop CDP 复验揪出:战斗屏除回合横幅外 lumStd=0 全空,逻辑与指针全通):根因=run-scene.js const txt 声明位于工厂 return 之后永不执行,txt 永陷 TDZ,全部整屏绘制函数每帧 ReferenceError 无声失败, 唯 floatText(function 声明提升)独活=只剩横幅;M1 并入 run-scene 时引入,headless 门不建场景故只有真浏览器 暴露。修=txt 改 function 声明(提升语义)+事故注释钉原地+自证钩子(场景 rebuild 后回写 readState().debug={displayCount,rebuilds,screen} 供 CDP 机断)+防回潮门 scene-smoke.mjs(mock 场景重放十屏/ 异常零容忍/每屏绘制对象≥5;修复后 map=33/battle=32/reward=14)。 M3:S13 职业乙 66+中立 22=154 卡定容(static-screen 3,676,134 序列零经济环)+S14 进阶 ASCENSIONS 16 级入 矩阵轴+多样性 driver(chain/defensive 档,死卡候选 42 张如实挂账 README)+S15 meta 三件(图鉴/成就 40/每日, meta-core.js+meta.test)+S16 事件 45;存档 v2 双域+v1 迁移链(19 断言);home 局外模式机+终局结算。 门证据:八门全绿(battle-core/logic-smoke/run-core+R15 职业进阶 7 断言/run-smoke/save v2 19 断言/meta/ static-screen/scene-smoke);六带全绿 nightly n=1000/格(甲 A0 58.2%/A5 32.2%/A15 6.9%,乙 55.1%/28.3%/5.7%, 带 45–60/25–40/5–15);构筑指纹去重 A0 六格全 100%(门>85% 草案);金标 26 套(甲 20 漂移≤3.6pp 全 PASS+乙 6 首锁); **全矩阵 96,000 局书面口径首次真跑=158.6s、nightly 总 223.2s**;平衡轮主杠杆=职业起始遗物 +36.6pp; 主会话真跑复验八门+PR 层。浏览器像素复验随本笔 push 后触发。 Co-Authored-By: Claude Fable 5 --- tier2/games/shanhai-xunyilu/README.md | 141 +++--- .../assets/atlas/beasts-act1.json | 82 ++++ .../assets/atlas/beasts-act1.webp | Bin 0 -> 181704 bytes .../shanhai-xunyilu/scripts/asset-gen.mjs | 124 ++++++ .../shanhai-xunyilu/scripts/asset-pack.mjs | 269 ++++++++++++ .../shanhai-xunyilu/scripts/asset-shots.mjs | 329 ++++++++++++++ .../scripts/battle-core.test.mjs | 7 +- .../shanhai-xunyilu/scripts/goldens.json | 145 ++++++- .../shanhai-xunyilu/scripts/meta.test.mjs | 109 +++++ .../shanhai-xunyilu/scripts/run-core.test.mjs | 46 +- .../shanhai-xunyilu/scripts/run-smoke.mjs | 1 + .../shanhai-xunyilu/scripts/save.test.mjs | 203 +++++---- .../shanhai-xunyilu/scripts/scene-smoke.mjs | 174 ++++++++ .../shanhai-xunyilu/scripts/sim-shell.mjs | 148 ++++--- .../shanhai-xunyilu/scripts/static-screen.mjs | 5 +- .../games/shanhai-xunyilu/src/battle-core.js | 18 +- .../games/shanhai-xunyilu/src/data/tables.js | 406 +++++++++++++++++- tier2/games/shanhai-xunyilu/src/main.js | 181 +++++--- tier2/games/shanhai-xunyilu/src/map-core.js | 20 +- tier2/games/shanhai-xunyilu/src/meta-core.js | 193 +++++++++ tier2/games/shanhai-xunyilu/src/run-core.js | 167 ++++++- tier2/games/shanhai-xunyilu/src/save.js | 88 ++-- .../shanhai-xunyilu/src/scenes/run-scene.js | 128 +++++- .../games/shanhai-xunyilu/src/sim/sim-run.js | 31 +- .../shanhai-xunyilu/src/sim/valuation.js | 5 + tier2/games/shanhai-xunyilu/src/ui/layout.js | 28 ++ tier2/harness/card-battle.driver.js | 135 +++++- 27 files changed, 2836 insertions(+), 347 deletions(-) create mode 100644 tier2/games/shanhai-xunyilu/assets/atlas/beasts-act1.json create mode 100644 tier2/games/shanhai-xunyilu/assets/atlas/beasts-act1.webp create mode 100644 tier2/games/shanhai-xunyilu/scripts/asset-gen.mjs create mode 100644 tier2/games/shanhai-xunyilu/scripts/asset-pack.mjs create mode 100644 tier2/games/shanhai-xunyilu/scripts/asset-shots.mjs create mode 100644 tier2/games/shanhai-xunyilu/scripts/meta.test.mjs create mode 100644 tier2/games/shanhai-xunyilu/scripts/scene-smoke.mjs create mode 100644 tier2/games/shanhai-xunyilu/src/meta-core.js diff --git a/tier2/games/shanhai-xunyilu/README.md b/tier2/games/shanhai-xunyilu/README.md index 51408a98..67884e9b 100644 --- a/tier2/games/shanhai-xunyilu/README.md +++ b/tier2/games/shanhai-xunyilu/README.md @@ -1,99 +1,106 @@ -# 《山海巡异录》 —— 北极星顶级线③ · M2(构筑与仿真) +# 《山海巡异录》 —— 北极星顶级线③ · M3(内容完备:双职业+进阶+meta) 卡牌构筑肉鸽(尖塔骨架 × Balatro 式乘法爽感,原创轴 = 五行相生序列链)。规格 SoT = [开发设计书](../../../docs/agent-specs/2026-07-06-北极星顶级线-卡牌构筑-开发设计书.md)(创始人 2026-07-06 已批, 定名《山海巡异录》,统一山海 IP 宇宙);本目录只承载实现,规则数值一律以 `src/data/tables.js` 表为准、代码零魔法数。 -## M2 范围(书 §12:职业甲全内容+商店删卡+存档续玩+数值 CI) +## M3 范围(书 §12:双职业+进阶 A0–A15+图鉴成就+每日+事件) -M1 单局闭环之上,这一档把「内容定容 × 数值 CI」立起来: +M2 的职业甲全内容+数值 CI 之上: -- **职业甲卡池 66 定容**(S9,三批入表、每批过静态快筛+smoke 矩阵):新增状态词缀子集 - (敌侧 易伤/虚弱/灼 + 我侧 韧,书 §5.1 ⑥ 十词缀的 M2 子集)与卡词 - (血价 hpCost/消耗 exhaust/保留 retain/回血 heal/灼联动 dmgPerEnemyBurn/chainBonus{dmg,block}); - 估值全走 `src/sim/valuation.js` 单源公式(锚=表 VALUATION,advisory)。 - 虚弱×意图诚实:展示与执行共用 effectiveIntent() 同一折算——任意时刻读到的=此刻结算即发生的。 -- **遗物 40 定容**(S10,含职业甲专属 6;数值级 6/40=15%,规则级 85%):hook 词汇 20+ - (能量上限/首回合面/链起环/每环倍率加成/周天联动/反伤/五行首打系列/对位开战词缀/商店与奖励规则件), - 聚合=数值累加/布尔 or(同 hook 多遗物自然叠加);发放=受控随机抽池;获取源四层=精英必掉/宝匣/ - **Boss 战后三选一**(幕1/2,relicReward 屏)/商店。 -- **商店与删卡经济**(S11):每幕保底 1 商店节点(map-core 配额自检);货架=卡 5(稀有度权重标价)+ - 遗物 2(未持有池)+删卡服务(基价 50、每删全局 +20——§3.2 给组合爆炸收租);卡组下限 10; - 金不足/已售/下限一律拒且零副作用。 -- **run 存档续玩**(S11):形状=契约 `SaveEnvelope`(`{schemaVersion≥1, data 纯对象}`, - `contracts/sdk-interface.d.ts`;save: 族键 65536 码元上限,W-SAVE-CAP 已入库)。快照点=战斗外任意屏 - (map/reward/relicReward/shop/shopRemove;battle 屏无快照,中断回上一节点);恢复=rngState 精确续 - (续打与不中断逐字段一致,save.test V3 实证)。**介质=影子**(localStorage 直写,`src/save.js` 文件头 - 申报):信封契约合规,正式 SDK Storage 通道切换随集成段(书 §12④,M5 门=切正式+迁移断言)。 -- **快进仿真外壳**(S8b v0 → S12 v1,`scripts/sim-shell.mjs` + `src/sim/`):矩阵扫描(职业×进阶× - 三档策略 driver)→格聚合(胜率带/Boss 回合带/采用率带/峰值伤 P99-P50/invalid 指纹/墙钟)→ - 金标构筑回归(20 套,`scripts/goldens.json`,漂移 ±8pp 按套判)。三档 driver=贪心/随机/挂机 - (与 CDP harness 同文件 `tier2/harness/card-battle.driver.js`,贪心档=九门真玩同一大脑)。 +- **154 卡定容**(S13,书 §5.2):职业甲 66 + 职业乙「璇玑使」66(链优;身份词 ifZhoutian=周天后附加 + 效果)+ 中立 22(行旅工具件)。职业池过滤=本职业+中立;起始遗物(平衡轮:甲=开明兽牙/乙=河图残卷)。 +- **进阶 A0–A15**(S14):16 级修正表(`ASCENSIONS`,累积生效,mods 13 词——敌攻/血乘区、精英配额、 + 资源面收紧、A15 构筑面收紧〔奖励二选一〕);修正全局含教学首战(R15g 抓的豁免缺口已修)。 +- **多样性 driver**(S14,书面三档补齐):链优先 chain / 保血防御 defensive(+M2 贪心/随机/挂机=五档); + 死卡口径=书面三档并集(任一档 ≥3% 采用即豁免)。 +- **事件 45**(S16,书 §5.2):幕池 12×3+通用 9;fx 七词(金/血/上限/遗物/卡/随机销卡); + 事件节点每幕 ≤2(保底件先行、增益件吃余量——96k 全矩阵实抓的配额挤兑修序)。 +- **meta 系统**(S15,最小可玩):`src/meta-core.js`(书 §8.3 第四纯逻辑模块)——图鉴(卡/遗物/敌兽, + 见过即录)、成就 40(声明式条件+解释器,词表对账单测钉死)、进阶解锁(通关 An 启 An+1 逐职业)、 + 每日一局(日期派生种子+本地最佳分,榜单走平台既有能力);局外三屏(home 选职业进阶/图鉴/成就)。 +- **存档信封 v2**(S15):data={run, meta} 双域——终局 run 域清空、meta 常存;**v1→v2 迁移链首用** + (M2 旧档=平铺快照→迁入 run 域),M5「切正式通道+迁移断言」的先手演练;meta 域独立 fail-closed + (局外进度损坏不吞续玩)。介质仍=影子(申报见 `src/save.js` 文件头),正式通道随集成段(M5 门)。 -## 数值 CI 分层(书 §6.4 M2 出口定案;墙钟=本机 darwin/arm64 实测 2026-07-06) +## 战斗绘制修复(2026-07-06 浏览器复验缺陷,M2 后修入) + +**根因**:`run-scene.js` 的 `const txt = (…) => …` 声明位于工厂 return 之后——该行永不执行,`txt` 绑定 +永陷 TDZ,首次 rebuild 即抛 ReferenceError;整屏绘制(地图/战斗/商店)全部无声失败,只有不依赖 txt 的 +事件浮字(function 声明提升)可见——与 CDP 复验「横幅之外全空(lumStd=0)」逐点吻合。M1 并入 run-scene +时引入;headless 门不建场景(ctx.game=null),场景代码零执行,只有真浏览器暴露。 +**修复**:`txt` 改 function 声明(提升语义与 clear/keep 一致)+事故注释钉在原地。 +**防回潮**:①新门 `scripts/scene-smoke.mjs`(记录型 mock 场景重放:七屏驱动/异常零容忍/每屏绘制计数 +≥5/自证钩子断言);②自证钩子——场景 rebuild 后回写 `readState().debug={displayCount,rebuilds,screen}`, +CDP 复验据此机断「画了东西」,不再仅赖像素统计。浏览器像素复验=mini-desktop 窗口批(主会话触发)。 + +## 数值 CI 分层(书 §6.4;墙钟=本机 darwin/arm64 实测 2026-07-06) | 层 | 命令 | 内容 | 实测墙钟 | |---|---|---|---| -| **PR smoke** | `node scripts/static-screen.mjs && node scripts/sim-shell.mjs --tier=pr` | 快筛(链护栏①层+估值黄牌)+ 贪心×200 + 金标抽测 7 套×200 | **≈3.0s** | -| **批产冒烟** | `node scripts/sim-shell.mjs --tier=batch` | 三档×200(S9/S10 每批入表即跑) | ≈0.5s | -| **nightly 全矩阵** | `node scripts/sim-shell.mjs --tier=nightly` | 三档×1000(3,000 局)+ 金标全 20 套×1000(20,000 局) | **≈36s** | +| **PR smoke** | `node scripts/static-screen.mjs && node scripts/sim-shell.mjs --tier=pr` | 快筛(154 卡 367 万序列)+ 2 职业×A0/A5/A15×贪心×200(书面小矩阵)+金标抽测 | ≈10s | +| **批产冒烟** | `node scripts/sim-shell.mjs --tier=batch` | 2 职业 × A0 × 五档 × 200 | ≈4s | +| **nightly 全矩阵** | `node scripts/sim-shell.mjs --tier=nightly` | **2 职业 × A0–A15 × 书面三档 × 1000 = 96,000 局**(书面口径)+下界锚 4,000+金标 26 套×1000 | **总 223.2s**(矩阵 158.6s+金标 64.6s,实测) | -M2 矩阵=职业甲×A0(进阶 A1–A15 随 M3 S14 入轴,外壳轴形状已留);书面 96,000 局全矩阵 -(2 职业×16 层×3 档×1000)按本机 1.7ms/局线性外推 ≈ **2.7 分钟单线程**〔外推,M3 实测定案〕。 -退出码语义:带判越带只报警(M0–M2 硬门仅 L1 真玩门);结构性失败(stuck/金标漂移超线)才非零。 +金标基线口径(M3 定案,工单点名):池扩张/职业/进阶入轴是预期漂移源,原则=基线随内容里程碑重锁; +**M3 实测结果好于预期——甲 20 套在 154 池+事件+起始遗物扩张下漂移 ≤3.6pp 全 PASS,基线连续未动** +(金标注入=已成型构筑,opts.relics 覆盖起始遗物,扩张影响被强构筑掩盖);乙 6 套 M3 首锁(n=1000)。 +漂移门 ±8pp 管里程碑内数值微调;满值套(100%)漂移只能单向向下,如实观测。 -## 判据带首轮回填(全部〔草案〕,S12 调参轮实测 2026-07-06) +## 判据带回填(S14 平衡轮终值,n=1000/格,全部〔草案〕) -- **A0 贪心档胜率 47.3%(n=1000,σ≈1.6pp)——入带 45–60%**。调参轮路径:词缀价值意识 +1.2pp → - 构筑当量选卡+薄组纪律+商店序(遗物优先)+19.7pp → 资源面(金币带+15%/战胜回复 6→8)+4.2pp → - Boss HP 回压(122/140/182)+4.5pp;幕缩放 −2% 实测零效果已还原。随机 0%/挂机 0%(下界,非校准对象, - 未削敌线迁就)。A5/A15 带随 M3 进阶表启用。 -- 采用率带:必选 0;死卡候选 14(贪心单策略窄化产物,治理随 M3 多样性 driver——链优先/保血防御档)。 -- Boss 回合:P50=8(带 6–9);磨局 149/2385(6.2%,继续收敛归 M3 平衡轮)。 -- 峰值回合伤 P99/P50=1.88(反垄断线 8 内);invalid=0;周天 0.09/局(构筑目标位,M3 观察)。 -- 金标 20 套基线已锁 `scripts/goldens.json`(32.5%–100%,流派梯度;满值套漂移只能单向向下,如实观测)。 +- **六带全绿**:甲 A0 58.3% / A5 32.2% / A15 6.9%;乙 A0 55.1% / A5 28.3% / A15 5.7% + (带:A0 45–60 / A5 25–40 / A15 5–15)。 +- 平衡轮主杠杆实录:职业起始遗物(乙 16.1→52.7,+36.6pp)>进阶尾段换轴(A15 战斗压力→构筑面收紧, + 1.9→5.8)>资源面软化;无效教训=±1% 敌攻在意图值 round 粒度下零效果(微调粒度必须 ≥ 取整步长); + 乙灼起手(荧惑守心入起始组)全线 +15pp 冲破上限,撤回——大杠杆不做微调件。 +- 构筑指纹去重率(M3 门 >85%〔草案〕):A0 六格全 100%。 +- 下界锚:random/idle 双职业 A0 全 0%(敌线致死性保持,未削敌迁就)。 ## 怎么跑 ```bash -# 全量回归(M0+M1+M2 六门;交付口径) +# 全量回归(M0+M1+M2+M3 九门;交付口径) node scripts/battle-core.test.mjs && node scripts/logic-smoke.mjs \ && node scripts/run-core.test.mjs && node scripts/run-smoke.mjs \ - && node scripts/save.test.mjs && node scripts/static-screen.mjs -# 数值 CI(见上表三层) -node scripts/sim-shell.mjs --tier=pr # 或 nightly / batch / --n=N --strategies=greedy,random,idle + && node scripts/save.test.mjs && node scripts/meta.test.mjs \ + && node scripts/static-screen.mjs && node scripts/scene-smoke.mjs +# 数值 CI(三层,见上表) +node scripts/sim-shell.mjs --tier=pr # 或 nightly / batch / 自由组格 --classes= --ascensions= --strategies= --n= # 构建(esbuild 在 game-runtime/node_modules;external phaser) cd /game-runtime && node ../tier2/engine/build-phaser.mjs ../tier2/games/shanhai-xunyilu -# 真玩:静态起服后开 index.html(?seed= 注入种子);交互真玩/九门 driven 在 mini-desktop(本机禁 chrome) +# 真玩:静态起服后开 index.html;boot 进 home 屏(选职业/进阶/每日/图鉴/成就);九门 driven 在 mini-desktop ``` ## 结构 -- `src/battle-core.js` —— 回合战斗解算核(纯函数;M2 增列:状态词缀/血价/消耗/保留/遗物 hook 20+) -- `src/map-core.js` —— 三幕地图 DAG(M2 增列:商店保底恰 1 入配额自检) -- `src/run-core.js` —— 一局核(M2 增列:商店五 API/Boss 遗物三选一/随机发放/snapshot·restore) -- `src/save.js` —— SaveEnvelope 契约面单源(信封/fail-closed 校验/恢复工厂/容量闸) -- `src/sim/sim-run.js` —— 仿真单局引擎+格聚合(事件流观测:Boss 回合/峰值伤/采用率/指纹) -- `src/sim/valuation.js` —— 卡牌估值单源(T1e 与 static-screen 共用,防公式漂移) -- `src/ui/layout.js` —— 布局与可点靶唯一坐标事实(M2 增列:shop/shopRemove/relicReward 三屏靶) -- `src/scenes/run-scene.js` —— 一局场景(M2:七视图;新事件演出:词缀/灼跳/反噬/购删) -- `src/data/tables.js` —— 66 卡/8 敌/40 遗物/曲线/商店经济/SAVE/SIM_BANDS/VALUATION(schemaVersion=1) -- `scripts/` —— 六门+sim-shell+goldens.json(node 直跑,无浏览器) -- `play-spec.json` —— 九门 driven 接线(driver.type=card-battle;CDP driver=`tier2/harness/card-battle.driver.js`) +- `src/battle-core.js` —— 回合战斗解算核(M3 增列:ifZhoutian 身份词/enemyStartStrength) +- `src/map-core.js` —— 三幕地图 DAG(M3:事件节点+A1 追加精英;保底件先行/增益件吃余量的配额次序) +- `src/run-core.js` —— 一局核(M3:职业/进阶消费、事件屏、周天计数;快照含 heroClass/ascension/事件面) +- `src/meta-core.js` —— 局外核(M3 新增:图鉴/成就解释器/进阶解锁/每日) +- `src/save.js` —— SaveEnvelope v2 双域(v1 迁移链/meta 独立 fail-closed/容量闸) +- `src/main.js` —— 工厂(M3:home 局外模式机/终局结算恰一次/自证钩子 debug) +- `src/sim/sim-run.js` + `src/sim/valuation.js` —— 仿真单局引擎(五档/三轴/胜局指纹)+估值单源 +- `src/ui/layout.js` —— 布局与可点靶唯一坐标事实(M3:event 屏/home 局外屏) +- `src/scenes/run-scene.js` —— 场景(M3:十视图=七局内+home/图鉴/成就;TDZ 事故注释与自证回写) +- `src/data/tables.js` —— 154 卡/8 敌/40 遗物/45 事件/16 进阶/40 成就/曲线经济(schemaVersion=1) +- `scripts/` —— 九门+sim-shell+goldens.json(26 套) +- `play-spec.json` —— 九门 driven 接线(CDP driver=`tier2/harness/card-battle.driver.js`,五档策略同文件) -## M2 门与实测(2026-07-06 本机;浏览器面=窗口批) +## M3 门与实测(2026-07-06 本机;浏览器面=窗口批) -- 六门全绿:battle-core(M0 40 断言+T11 词缀 18 断言)/logic-smoke/run-core(M1 12 组+R13 商店 - +R14 金标注入)/run-smoke(S0–S5)/save.test(V1–V7 13 断言:往返+续玩确定性+坏版本 9 负例全拒 - +容量峰值 2148 码元余量 97%+battle 屏拒+三屏往返)/static-screen(66 卡 291,918 序列零经济环命中) -- 链护栏三层随扩池持续生效:①静态快筛每批入表即跑(E0 经济词汇封闭断言钉死绕过面)②运行时动作上限 - 200(千局 invalid=0,未误触)③分布反垄断 P99/P50=1.88+invalid 构筑指纹落聚合报告 -- nightly 全矩阵+金标总墙钟 **36.2s**(扫描 2.2s/3,000 局+金标 34s/20,000 局;1.7ms/局) +- 九门全绿:battle-core(M0 40+T11 词缀 18)/logic-smoke/run-core(M1 12 组+R13 商店+R14 注入 + +R15 职业进阶 7)/run-smoke/save.test(v2 双域 19 断言:迁移链真通+meta 独立+乙/A3 续玩确定性) + /meta.test(局外核 6 组)/static-screen(154 卡 367 万序列零经济环)/scene-smoke(渲染层防回潮) +- 全矩阵 96,000 局实测 ≈110s(书面「≈2.7 分钟单线程」外推兑现);1.15ms/局均值 +- 链护栏三层持续生效:快筛(E0 词汇封闭含 ifZhoutian)/动作上限(96k 局 invalid=0)/指纹观测 ## 已知边界(如实) -- 浏览器面(画面可见/真指针/九门 driven/CDP 三门/新三屏视觉)本机不可验,排 mini-desktop 窗口批 -- 存档为影子介质(申报见上);正式通道+迁移断言=M5 门 -- 死卡治理与 A5/A15 带、多样性 driver(链优先/保血防御)、进阶轴入矩阵=M3 -- engineBundle/bundleSize 契约口径对 Phaser external 形态不适配:M0 已在册,待契约侧,本工程不自改 +- 浏览器面(home 三屏/事件屏视觉/真指针/九门 driven/绘制修复像素复验)本机不可验,排 mini-desktop 窗口批 +- 存档为影子介质;正式通道+迁移断言=M5 门(v1→v2 迁移链已先手演练) +- 死卡候选(书面三档并集,千局口径)≈40 张量级——154 池下贪心系策略的窄化仍在,流派 payoff 卡 + (ifZhoutian/灼联动/血价)贪心不识;治理=M4 策略档深化或 M5 前卡值回炉,如实挂账 +- 每日一局=本地最佳分(榜单走平台既有能力,不新建后端);成就页仅前 12 条(完整滚动容器=M4 工艺) +- engineBundle/bundleSize 契约口径对 Phaser external 形态不适配:M0 已在册,待契约侧 - 富三门 archetype 未接:卡牌品类门按书 §6.5 走验收门品类适配单另出 diff --git a/tier2/games/shanhai-xunyilu/assets/atlas/beasts-act1.json b/tier2/games/shanhai-xunyilu/assets/atlas/beasts-act1.json new file mode 100644 index 00000000..26d6f053 --- /dev/null +++ b/tier2/games/shanhai-xunyilu/assets/atlas/beasts-act1.json @@ -0,0 +1,82 @@ +{ + "textures": [ + { + "image": "beasts-act1.webp", + "format": "RGBA8888", + "size": { + "w": 875, + "h": 943 + }, + "scale": 1, + "frames": [ + { + "filename": "bo-ma", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 419, + "h": 512 + }, + "spriteSourceSize": { + "x": 2, + "y": 2, + "w": 415, + "h": 508 + }, + "frame": { + "x": 3, + "y": 3, + "w": 415, + "h": 508 + } + }, + { + "filename": "kuze-jiao", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 448, + "h": 445 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 448, + "h": 442 + }, + "frame": { + "x": 424, + "y": 3, + "w": 448, + "h": 442 + } + }, + { + "filename": "zheng", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 448, + "h": 427 + }, + "spriteSourceSize": { + "x": 2, + "y": 2, + "w": 444, + "h": 423 + }, + "frame": { + "x": 3, + "y": 517, + "w": 444, + "h": 423 + } + } + ] + } + ], + "meta": { + "app": "http://github.com/odrick/free-tex-packer-cli", + "version": "0.3.0" + } +} \ No newline at end of file diff --git a/tier2/games/shanhai-xunyilu/assets/atlas/beasts-act1.webp b/tier2/games/shanhai-xunyilu/assets/atlas/beasts-act1.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f074e607e14d03639d4793efc9da097babfe23a GIT binary patch literal 181704 zcmV)dK&QV_Nk&G%#R33VMM6+kP&il$0000G0001L0|2fA06|PpNEb-~00A5ZZ5v5S zTK=eK_Fe)JF#&!M-r*hIVFT_M5KU+coFEAb*$QwnF&XsC2%w3%(II34;Sy?40S!P4 zgRye4Eql&@jx_;1qNqt0V{+i|PaX^MNQQts02z%9M-RUXV0t$Vr+wb`r+{2aGW@+mcBZTFla7UTbFl zzw`JoGyj<-`NmHXF#-6$)($H^D|Wc4;JxML2~|+X8>47|t4u(_+q%ySbsqpJNdXh; zJjFgYFD6=&(s~b=&d*y!%2BBI3{K3#jgm++jKyC#lUKnu!w43c>N{qw0&RkU!A0!g z)Zx%(7+H8`n(+a#()PH^9xkw|1LQG+}CLH3liMpvW!Q< zHo+MDg)aE%ONutXh-rC%KI20$%`Z^Q+fF)vj7^$fkm%V=={O#y`9(1QOdq^^XK12L zFGz4MQ)WC4N%M>9;MeJcpSmHW=>--Ww`cTW`n)PlFA#k0JzaPi+WcbVIA&xPevhQ- zMRf4ZboAkazR+eDSghkS((#49BF!!^$xC#kB)7ad7DZuFr3UkfxEmWq&Mz|XY#|1HSTAB&$PQf8q4e~X7pXp?B| z_fYEpwLoJYZ4w>7uV>W%Yw^^1&JUqw3+4U47AP#F4H5eQp#Oi1YF$Q2qHugK=xQK| zG)q9^>4Xhoz=F2W*(MhCXlV%si|9g_;1@BIWdh85(qKrlgx0fC(dQ-bvP#i*;#7_n zZI>icTUB3YlS=Q28FKjab z49=wu6BZ{vL@X;L>GxlDgqg#k-K7R=d_rDY4;U#CA(l6dHiTrrXAsKzKX)4DxTqUS%SU z{TbQklgg_q(PT1%=eL5Yf6 zD#YSUrh#cV0SW2?t0f_=&VBysD=&Mb|CQyG2n_gqH>9hg7_34^dcFrmrOL=(9PjI6 z1Q3fen35q(8>m_zn6i+E2bfY^6grm7n=|~CcjhmcHD%1S`SXVKf=(s2_e7tOj<<=a zhS3b<2q?tj45lQVKNJS_L1Ei?7@Rk8cE!56fA{;7v{4w4Wcj$~zjP$IW81>ITBT5# z?*|4l3v0o4vU~{iqoHDPCR36={AeL{!Uju1OF_w}F*Lr75=CTv8Ka*7lL84&V@hV+ zvJ)#;GIKl*iN)zmNiyY0FsKUzzg@UAWK7#DuDI&T%da@J#oNwCa5DXv%dWcO$}28C z>x!4xu3j^|iB?h03n@eRVJx&7ZeW)2I<(?+rX(5M4?|V!g95h^iF^`+2(%oFj3+ps zj*d^kpi*@HN%R@7R-DF^l^IWjMZKU}B2r#xj=CTk1I}!a6Z1Ud830N}d#j|Bmj9?Q zgM;W8@N2wJpOxPIFwAJ(P}nvmDV1unRa_JXoSI`4GF!LaQ>^3+cwX3*k~H8+`V4hE z2?ljT>qto12nHX}kHmmi!(>R|0ao%>t&!~EZ(7pE=@=UP=kpt_AG8g{Y-6K!`$9@2 z38b^q5o&n`Fh(I}URbx20gpk>l^TFDEk{`2k;^C{F8l`;^+RFX*udcMd@E6C`8vE5 z8V@0*!3Pvt_U`uQW~~(9VK~Wgy-#Bh;<=faCI@&pnJ$u(d39nc%)@un|ES{)oqCXI z_WM@roUFaMd_`@g04R0P(W5>99td;(*%l+s4#55NmE-tP>6o$P`hOzBR8 zhm`FUu$C^#b0ZiuJ$Sf=(Zm*=2$Q7sK4os@;K3#Sm}fd#mt^HHcnH@9bsE)M5#so4 zpH^*t(5k6WqR_Fhsn!5;@FHh!AZfW^KMF~4h1V#ZBKgv{0!NY=4?@nDq+s;frB^tzG^gjNQQpe1SfHzJ?P_dlf#Be^QfY~^if%IKb?L}HwGBS&re1Y@{jSwW@9 zB8EDyz4zKAkmGc1B8e>F_H8Lr^yeYxMWen1w6#P)q65s8< z?TvVR{+_rnT>Qi>d420(7D<-=e(YLGiV&VTXU!Eco2N6})RX{&x9F!q$7^Ah+dfWI#xzM@bm7B#fqIWK^^3~}iGuJUtv>5>k3+bZy?1vw@nd6Iv zA{=CWlH7A0G&-$a^%ng?o7jHnO1h8`vNX*?2+le0N$<13--$sJf~Rprft6@3e&1*Y z^qDL{KynjjH-U-a)rwt{g)1s~@B0jxuFD+h0yBowA!t5OXfs;hPM>MI5;_WnbdKbf z7rULsp@<=dPe^hD`C{Kn%TW8(mlqs++~plEV^!zqHiJP^LX!ZONEAsT9YZa-;lrb9 z;jT)1qw)~Lpc$j(&y9TUG$%p{L|Amp;Q%mZ>>`(6@+1Sv2i8S`=7fg)^acj0grv1r zNQyAv%KMrF&Bjs&B2Po>=UU9$Ru&(01y{$NW6J-1^~e&aw4+9iJasQX(UhQLIb--p z;8TP_KyswD3>DJy;U+-id$w`JKMx_f)6pL+egDUo*Ydh6Nv{5OH-(uaIqlV5em@-5 zRneRP;1#=*w4@_Nro2T?{BA@g1IBmqUZBlAt4M#xQXKd6@%Q-fcVvFQ z>wYQ}A?dv%$^Sin|9a~z0l;cAf-(nubkm*u_S88n*~ZgP+s0Eq|ND52o5};m->sZg z=j354$iQ-8kI|KLhBP!Cd@fskU2R|hxnvgenKA^QWvrZW$!~fAMw<^j3}j+^Wj!?d z?5LCG0Rh_qKugm7bn(tb9iJs>ow%*9&Mi6bg3De)(+4gBpz#HR*;Baw2XP@kvC=!| z9Lbli8VgvJCIhY7m$>gv2kHQTXRbd<8566u#vghDep%3MA%nn?LXr#CbK-tF4^XjO zd`6$#xoFb+410ef1FHw;N#3{!0BtTnOnvRwK7YWstM2k*3ZWXujXnyfzvqv|tYmnF zPfj}t06Y)C2Mj1H*Ihk%!7d5m=cIR4NcxNnm#$(VO$PXhq?>ZmaUK-l833pacwVOg zBw0~J@s~~A#)1a`trVaebo+%R)|Y{2_1`*nk_gF;k6-;IFmy&TBmIkGVYQjS;EH~q z&bqg~hv>?{pzG+jKX|L@&(`hO#mZL?1Pp-2O^~yd;}c|J@Uo&mCowd9vCqXmu!|Wv z>pVc42$EYq+t<@3TC&OmfQLzP#yc$$%LP!o@r8%n^rcL@Gk?^l3+K$A_RGw|a$DX$ zW5%h+`M@qZ=Q;wH%?Ri>zxMkd5y!_!BCm~GmOBkcD z-!MqiM6%8qT1+mR>;jT3DVY3T;%OZVLCiaJ1)XE@i{E^C_2auKOd0S)cu>f}(wM^K zx7<2*%jOA_XKp4F1-~yPS@8XL{SHN_LZ=IwJj;Nog%k;4f)5}DD@_wx{lLoY6vqU=PuiL$fHu#qcaD`SCKp?~>vO;0rsp=5vUttFtFJi~8p#Ha zN)X0^)g&h#vq^*`17G?f2g_g@@M15$zl|h?EODeQ7W(n?p#0yi_YB}+WZh&LI#rWxSJ`hBW3$voN;|#P73rnW0MFe-1BV(x4!?y zU*~M0FbM9vvy9|Ru+0t%$K=$$^MPHz?V`-ADE#5a0r^&}IEDwa->Rchwe9{oEy?%> zlB-_)@ZN7|Ns+DlHh;6Q{Ti6&1g%nOd<2jj@z`1}-8!}3UESKXZ`8g^$GblK{HrgA z3-5%7)cU{)A-2FDx7KHB$9$mqus(=>DN32}NX>I~3aH$J?-t)N_;T&~=__~5yXC^G zyW}Ee=fjTnbcEbyClre?xnL<13~UGk`Pd*2yC!9%)GA6;t)4&py#=JDEMnz4CdcP3 z+puB%y5$?z^g?u{8*ZQrA$LzMw3HBW?4hjWPf^>@DC8b}(kaiLb>xxIIuoAyVdV*@ zJWy$!(2_PtB!a^E<0p=vJK>I#j*Mv?Nu%vwPC`V;BFB`0`&C)wPVW(rJdjcaFr3@C zp5!Bd&H#Y1od3t~jO^^{fdd)?P0u(HCK43(-A)Oi2+19#Ar$rylQW7{wuQ-q9_U8K ztD9T=#{OF|%G%%eZ2^^@YT4Q&ELycu2?#I_6qAek~>C^U%%&kc+p@mQUcPJ zQb-2!CZC$JCp&6jm(e?%f4sSpg-9mOI1djc6`NuQEkWVV@E3X>%mKjV!UygH3gx5Ye8__R}fodM(=GnTuyZri%+ z-4EY*_0>23T*8!=|J0=HXLhoZq;*OBC=+TwO5mJOSUm5sYxe^#czX)258kQ;m7YS& zyFM={nCMu!em2P+Uw!uBPD&C}j)R$VjH)r^GKFSKS6W93lf@S9t8`k%bY-ZuK9l!s zC|*8fUv%CdnN+yotzoMwNEhF@Z0&|%$DpT&luDTQFAOv~XQ)wBYRRlMNeW44$$&pUZkbX@X8bpjIYI!ec;)*?t_m%a0lCMW zQkWEwbS!P9bvhbHkGzWfY~soX54ru*!;b8UE5=M*PRmE~yi6!%db~Mq#&VJr5n*M? zE+->8#@`0Bm9v47m01CAq8NPTKg(=;pI7yc0GYE9ysky9ln^Xl9%aKfZ**dp#Z87CB z9c%W(FL8?H>j(a1c`IT_SD{`lgtof=p6M(4|3m)q@>oiuB=LrU%1Fch^XK|Z+ZH|X z51BFJt>|t^P}N9+iBFtKe#~#rPhWsENGb&wkVDcrK2mQfIU*;f($TS4PO?I>`t-*o zbFO-IRA2*=Y?TpcJU4ctM3Eqb)0)3Ai}dYbpG3Ps5&#%d2_)Dzy!7qAaRixiBbWxn zNH_E_HYT38&ubqf0>}7{-rq79qqRcD>-GBThi^x6(D4eW|0NmUPPT>4%-P@m!(=O@ z1Pu525f1*mh%9}szDiF4X!q>6G)>d&;$hIaw(pZb@KTsLc{H@~&`4jQb6chVx21uO zBpsKwf331YWM3GhX>iipZ@m5XTkpQ{XYu?GUw-|`7=VWNzKqj<-86FBCv}lgjk{N@ zmn71WEb70OLej<$0vg`1WEYT@QfU9!=yH;QoWXbINdQ2tIgB6A+6<=gz~L>S(#d9aK@3=1aVG3T=OOlS0YsG-PRvyw-8UroKz=q7Fv%ccL(UPdD!FlxQ zwy=$c!OfhdO*D<}H=tdEXbS(;Hy~QS!DMUCxsHCK;yf5s;T`cGhKzpO!$1~~y!^09 z=c?Z~G{{AU)KiZxrV$&q$-w_*n^`B0O-pOYjxsAd@hlV+W`V;h-~r-pwVu$>n1AXomnoom_aWRKRy!pY)&wcamvu|=AC>jl|n(w5~xiK%5_H^w>NzO#L*1(nP zt^YQnDE&VirJz;IKKIgEdGP_#y2D{mg&y9Mc;M~;V2oBuX&hY@=A`#0)7hfJl3knV z_AFaBY&jiC27DbR1GTyz$G>(jJRG6Rcf9SNeC+x^j6dg<&$%Bg8V(QF)6(a|$Vma< z4Gw=)X6U02hlU3nJDv=xYsz9UZPKuL*OpZdR zhqo$plFCHRQOBLOl8zK1OSdi=zG>TThKgT$Jajr5`;q){=LF<>M#SP_cChk?FYg-% zi$>%R1{7O-5@JbOcR%weiFfNsJLA{(S}Sz_O(>4!5zt`@_1dfd(I}v0ZS2$fVV9tg zpI{65EDWl~-$~1(VGtg}*9n!B(b~tH{&hl_EIvto+<7!5iI&S^N~IG!>CZ9_Tf)cT z37}@7E2dTX`HdH^UJTlB7}YBfA%&GE!vo-9I1BhGE~88b3}R33rgNk)(Gn^|srKt8 z!Te7~s{?-suGUAF1VYD^wshZ}^t-Dn@o=51bZ&V|tx{-=u1r$<=Vm~m?cWR|DbC*+ zXG$kal|pob-tK~7Tv5p4cIb4mfBxfN|N7Sv#~gQfKS<+&!3;>!F>wk!81%IPWiEas z2diAPnob5xc5)6(h^HUoqJ~EOZ>C$JT#M7=LSbpou`KrGPtQYD#?&trm6kukLrPH6&NsIObi5R%DzxfYx+U)T zyGo_>K12MZ2D!*+!-oANPV3ST(m5_xIvw1_k!$%+{nMq0d%^;MCY{;_#NZuTh6kM2 z8W>DRQb^7)XuFC5Q~G=^1i%?3yj`F6zZrPm+bmmR8Sru7#K5;pYiU =X$Y0evn)nnvAEyQ$P}Sg~Cyp6w%2QPXvgRtHyF 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

-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

}**;qy@djScXz(tZg_1?wKS$5RIHn_w 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 literal 0 HcmV?d00001 diff --git a/tier2/games/shanhai-xunyilu/scripts/asset-gen.mjs b/tier2/games/shanhai-xunyilu/scripts/asset-gen.mjs new file mode 100644 index 00000000..9bff28a7 --- /dev/null +++ b/tier2/games/shanhai-xunyilu/scripts/asset-gen.mjs @@ -0,0 +1,124 @@ +#!/usr/bin/env node +// scripts/asset-gen.mjs —— 《山海巡异录》资产一期 · mmx 生图批跑(批次一 gen-batch.mjs 迁移版) +// owner:美术批产执行席(2026-07-06) +// 用法: +// node scripts/asset-gen.mjs --group cards # 按组批跑(cards|beasts|ui|relics|bg) +// node scripts/asset-gen.mjs --only card-mu-qingteng --seed-bump 100000 --force # 定向重摇 +// node scripts/asset-gen.mjs --only ui-frame-epic --subject-extra ",定向补词" --force +// 纪律(与批次一同构): +// - prompt = promptPrefix + 主体 + styleWordsShared + perGame(xunyilu, sprite|bleed) + 工艺后缀,拼接即所发原样入账; +// - 每次外呼 150s 超时,失败退避重试 ≤3(内容过滤 exit=10 不重试,配额 exit=4 全批中止); +// - 逐呼追加台账(成败都记):docs-design/山海风格版-批次一/manifest-巡异录资产一期.jsonl; +// - 串行 + 1s 间隔防限流; +// - 成本硬闸:台账累计外呼 × ¥0.033(仓内实测口径)≥ ¥195 时中止报告(工单红线 ¥200)。 +// 原图落 --raw-dir(默认系统临时目录;定稿缩尺版与图集由 asset-pack.mjs 产出进仓,原图凭台账 seed 可复现)。 +import { readFileSync, existsSync, mkdirSync, renameSync, appendFileSync, readdirSync, rmSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { dirname, join, resolve, extname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { SHOTS } from './asset-shots.mjs'; + +const GAME_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const REPO_ROOT = resolve(GAME_ROOT, '..', '..', '..'); +const BATCH1 = join(REPO_ROOT, 'docs-design', '山海风格版-批次一'); +const ANCHOR_PATH = join(BATCH1, 'anchor-pack.json'); // 锚 r1 单源引用,不复制不改 +const MANIFEST = join(BATCH1, 'manifest-巡异录资产一期.jsonl'); // 台账落批次一目录(工单要求) +const COST_PER_CALL_RMB = 0.033; // 仓内实测口径(批次一台账) +const COST_CAP_RMB = 195; // 硬闸(工单红线 ¥200,留安全边) + +// ---- 参数解析 ---- +const args = process.argv.slice(2); +const getArg = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; }; +const onlyIds = getArg('--only')?.split(',').map((s) => s.trim()).filter(Boolean); +const onlyGroup = getArg('--group'); +const seedBump = Number(getArg('--seed-bump') || 0); +const subjectExtra = getArg('--subject-extra') || ''; +const force = args.includes('--force'); +const RAW_DIR = resolve(getArg('--raw-dir') || join(process.env.TMPDIR || '/tmp', 'xunyilu-assets1-raw')); + +const anchor = JSON.parse(readFileSync(ANCHOR_PATH, 'utf8')); +const TMP = join(process.env.TMPDIR || '/tmp', 'xunyilu-assets1-gen'); +mkdirSync(TMP, { recursive: true }); + +// prompt 拼装(锚 r1 口径;本游戏固定取 perGame.xunyilu) +function buildPrompt(shot) { + const kind = shot.type === 'sprite' ? 'sprite' : 'bleed'; + const per = anchor.perGame.xunyilu[kind]; + const suffix = shot.type === 'sprite' ? anchor.spriteSuffix : anchor.fullBleedSuffix; + return `${anchor.promptPrefix}${shot.subject}${subjectExtra}。${anchor.styleWordsShared}。${per}。${suffix}`; +} + +function ledger(entry) { + appendFileSync(MANIFEST, JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n'); +} + +/** 台账累计外呼数(成本硬闸的分母;台账不存在=0)。 */ +function ledgerCalls() { + if (!existsSync(MANIFEST)) return 0; + return readFileSync(MANIFEST, 'utf8').split('\n').filter((l) => l.includes('"gen-call"')).length; +} + +function callMmx(shot, prompt, seedUsed) { + for (const f of readdirSync(TMP)) if (f.startsWith(shot.id)) rmSync(join(TMP, f)); + const t0 = Date.now(); + const r = spawnSync('mmx', [ + 'image', 'generate', + '--prompt', prompt, + '--width', String(shot.w), '--height', String(shot.h), + '--seed', String(seedUsed), + '--out-dir', TMP, '--out-prefix', shot.id, + '--quiet', '--non-interactive', + ], { timeout: 150_000, encoding: 'utf8' }); + const ms = Date.now() - t0; + if (r.error?.code === 'ETIMEDOUT' || r.signal) return { ok: false, error: 'timeout-150s', ms, exit: 5 }; + if (r.status !== 0) return { ok: false, error: (r.stderr || r.stdout || '').trim().slice(0, 300) || `exit=${r.status}`, ms, exit: r.status ?? 1 }; + const saved = (r.stdout || '').split('\n').map((s) => s.trim()).find((s) => s && existsSync(s)) + || readdirSync(TMP).filter((f) => f.startsWith(shot.id)).map((f) => join(TMP, f))[0]; + if (!saved) return { ok: false, error: 'exit=0 但未找到产物文件', ms, exit: 1 }; + return { ok: true, file: saved, ms, exit: 0 }; +} + +// ---- 主循环:串行 ---- +const todo = SHOTS.filter((s) => (!onlyIds || onlyIds.includes(s.id)) && (!onlyGroup || s.group === onlyGroup)); +if (!todo.length) { console.error('无匹配镜头(--group cards|beasts|ui|relics|bg 或 --only id)'); process.exit(2); } +let done = 0, skipped = 0, failed = 0, calls = 0; +let totalCalls = ledgerCalls(); +for (const shot of todo) { + // 成本硬闸:先查后呼 + if ((totalCalls + 1) * COST_PER_CALL_RMB >= COST_CAP_RMB) { + console.error(`成本硬闸:台账累计外呼 ${totalCalls},再呼将超 ¥${COST_CAP_RMB},中止。`); + process.exit(9); + } + const outDir = join(RAW_DIR, shot.group); + mkdirSync(outDir, { recursive: true }); + const existing = readdirSync(outDir).find((f) => f.startsWith(shot.id + '.')); + if (existing && !force) { skipped++; continue; } + + const prompt = buildPrompt(shot); + const seedUsed = shot.seed + seedBump; + let result = null; + for (let attempt = 1; attempt <= 3; attempt++) { + result = callMmx(shot, prompt, seedUsed); + calls++; totalCalls++; + ledger({ kind: 'gen-call', id: shot.id, group: shot.group, type: shot.type, w: shot.w, h: shot.h, + seed: seedUsed, seedBump, attempt, model: 'image-01', prompt, + status: result.ok ? 'ok' : 'fail', error: result.error, durationMs: result.ms }); + if (result.ok) break; + if (result.exit === 10) { console.error(`[${shot.id}] 内容过滤(exit=10),不重试`); break; } + if (result.exit === 4) { console.error(`[${shot.id}] 配额超限(exit=4),全批中止`); process.exit(4); } + if (attempt < 3) { const wait = attempt * 5000; console.error(`[${shot.id}] 第${attempt}次失败(${result.error}),${wait / 1000}s 后重试`); spawnSync('sleep', [String(wait / 1000)]); } + } + if (result.ok) { + const ext = extname(result.file) || '.png'; + const dest = join(outDir, shot.id + ext.toLowerCase()); + renameSync(result.file, dest); + done++; + console.log(`[${shot.id}] OK ${Math.round(result.ms / 1000)}s -> ${dest}`); + } else { + failed++; + console.error(`[${shot.id}] FAILED: ${result.error}`); + } + spawnSync('sleep', ['1']); +} +console.log(`\n批跑完成:成 ${done} / 跳过 ${skipped} / 败 ${failed};本次外呼 ${calls} 次;台账累计 ${totalCalls} 次 ≈ ¥${(totalCalls * COST_PER_CALL_RMB).toFixed(2)}(仓内实测口径)`); +process.exit(failed > 0 ? 1 : 0); diff --git a/tier2/games/shanhai-xunyilu/scripts/asset-pack.mjs b/tier2/games/shanhai-xunyilu/scripts/asset-pack.mjs new file mode 100644 index 00000000..7f5cda58 --- /dev/null +++ b/tier2/games/shanhai-xunyilu/scripts/asset-pack.mjs @@ -0,0 +1,269 @@ +#!/usr/bin/env node +// scripts/asset-pack.mjs —— 《山海巡异录》资产一期 · 产线打包(抠图→缩尺→图集→manifest→机检) +// owner:美术批产执行席(2026-07-06;批次一 key-out.py / atlas-pack.mjs 迁移整合版) +// 用法: +// node scripts/asset-pack.mjs --raw-dir <生图原图目录> # 全量打包 +// node scripts/asset-pack.mjs --raw-dir <目录> --group cards # 单组 +// 产物: +// assets/atlas/-N.webp + .json 图集(Phaser3 坐标,webp q80;卡插画独立分包 cards-jia) +// assets/bg/.webp 满幅大图(场景/结算,批次一复用件标 source) +// assets/manifest.json 资产清单 + 字节账(账A 输入) +// docs-design/山海风格版-批次一/images-巡异录资产一期// 定稿缩尺评审版(webp) +// 机检(每组):id 齐套 / 精灵透明底占比 / 尺寸合规 / 图集帧数对账;结果打印并追加台账(kind=qa-pack)。 +import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync, copyFileSync, rmSync } from 'node:fs'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { SHOTS, REUSE, CARD_IDS, RELIC_IDS } from './asset-shots.mjs'; + +const GAME_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const REPO_ROOT = resolve(GAME_ROOT, '..', '..', '..'); +const BATCH1 = join(REPO_ROOT, 'docs-design', '山海风格版-批次一'); +const REVIEW_DIR = join(BATCH1, 'images-巡异录资产一期'); +const MANIFEST_LEDGER = join(BATCH1, 'manifest-巡异录资产一期.jsonl'); +const ASSETS = join(GAME_ROOT, 'assets'); +const ATLAS_DIR = join(ASSETS, 'atlas'); +const BG_DIR = join(ASSETS, 'bg'); + +const args = process.argv.slice(2); +const getArg = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; }; +const RAW_DIR = resolve(getArg('--raw-dir') || join(process.env.TMPDIR || '/tmp', 'xunyilu-assets1-raw')); +const onlyGroup = getArg('--group'); +const WORK = join(process.env.TMPDIR || '/tmp', 'xunyilu-assets1-pack'); +const STAGE = join(WORK, 'stage'); // 按 atlas 分目录的定稿 png(图集输入) +const KEYED = join(WORK, 'keyed'); // 抠图中间产物 + +for (const d of [WORK, STAGE, KEYED, ATLAS_DIR, BG_DIR, REVIEW_DIR]) mkdirSync(d, { recursive: true }); + +const findRaw = (group, id) => { + const dir = join(RAW_DIR, group); + if (!existsSync(dir)) return null; + const f = readdirSync(dir).find((x) => x.startsWith(id + '.')); + return f ? join(dir, f) : null; +}; + +// ── 批量图像操作:一次 python 进程吃一张作业单(缩尺/转码;PIL LANCZOS)── +function runImageOps(ops, label) { + if (!ops.length) return; + const jobFile = join(WORK, `ops-${label}.json`); + writeFileSync(jobFile, JSON.stringify(ops)); + const py = ` +import json, sys, os +from PIL import Image +ops = json.load(open(sys.argv[1])) +for op in ops: + im = Image.open(op['src']) + if op['mode'] == 'cover': # 等比放缩后居中裁到目标框(满幅件) + sw, sh = im.size; tw, th = op['w'], op['h'] + s = max(tw / sw, th / sh) + im = im.resize((max(1, round(sw * s)), max(1, round(sh * s))), Image.LANCZOS) + l, t = (im.width - tw) // 2, (im.height - th) // 2 + im = im.crop((l, t, l + tw, t + th)) + elif op['mode'] == 'contain': # 最长边限界(透明底精灵,保比例) + im.thumbnail((op['box'], op['box']), Image.LANCZOS) + os.makedirs(os.path.dirname(op['dst']), exist_ok=True) + if op['dst'].endswith('.webp'): + im.save(op['dst'], quality=80, method=6) + else: + im.save(op['dst']) +`; + execFileSync('python3', ['-c', py, jobFile], { stdio: 'inherit', timeout: 300_000 }); +} + +// ── 抠图:批次一 key-out.py 原脚本复用(色键+连通域除杂,印章尘点自动清)── +function keyOut(files, outDir) { + if (!files.length) return; + mkdirSync(outDir, { recursive: true }); + execFileSync('python3', [join(BATCH1, 'scripts', 'key-out.py'), ...files, '--out', outDir], { stdio: 'inherit', timeout: 600_000 }); +} + +// ── 透明底机检:alpha 通道存在且全透明像素占比在合理带(防抠图失效/全空)── +function alphaCheck(pngPaths) { + if (!pngPaths.length) return []; + const jobFile = join(WORK, 'alpha-check.json'); + writeFileSync(jobFile, JSON.stringify(pngPaths)); + const py = ` +import json, sys +import numpy as np +from PIL import Image +bad = [] +for p in json.load(open(sys.argv[1])): + im = Image.open(p) + if im.mode != 'RGBA': + bad.append({'file': p, 'why': 'no-alpha'}); continue + a = np.asarray(im)[:, :, 3] + frac = float((a == 0).mean()) + if frac < 0.03: bad.append({'file': p, 'why': f'transparent-frac={frac:.3f} 过低(疑似未抠净)'}) + if frac > 0.98: bad.append({'file': p, 'why': f'transparent-frac={frac:.3f} 过高(疑似全空)'}) +print(json.dumps(bad)) +`; + const r = execFileSync('python3', ['-c', py, jobFile], { encoding: 'utf8', timeout: 300_000 }); + return JSON.parse(r.trim() || '[]'); +} + +// ── 图集:free-tex-packer-cli(schema 同批次一)→ png 页 → webp q80 → JSON 改指 webp ── +function packAtlas(atlasName, stageDir, pageSize) { + const pngs = readdirSync(stageDir).filter((f) => f.endsWith('.png')); + if (!pngs.length) return null; + const outTmp = join(WORK, 'atlas-out', atlasName); + rmSync(outTmp, { recursive: true, force: true }); + mkdirSync(outTmp, { recursive: true }); + const project = { + savePath: outTmp, + images: pngs.map((f) => ({ name: f, path: join(stageDir, f) })), + folders: [], + packOptions: { + textureName: atlasName, + width: pageSize, height: pageSize, fixedSize: false, powerOfTwo: false, + padding: 2, extrude: 1, allowRotation: false, detectIdentical: true, + allowTrim: true, trimMode: 'trim', alphaThreshold: 0, + exporter: 'Phaser3', removeFileExtension: true, prependFolderName: false, + textureFormat: 'png', base64Export: false, scale: 1, packer: 'MaxRectsPacker', + }, + }; + const proj = join(outTmp, `${atlasName}.ftpp`); + writeFileSync(proj, JSON.stringify(project, null, 2)); + execFileSync('free-tex-packer-cli', ['--project', proj, '--output', outTmp], { stdio: 'inherit', timeout: 300_000 }); + + const pages = []; + for (const png of readdirSync(outTmp).filter((f) => f.startsWith(atlasName) && f.endsWith('.png'))) { + const base = png.replace(/\.png$/, ''); + const jsonPath = join(outTmp, `${base}.json`); + const webpDst = join(ATLAS_DIR, `${base}.webp`); + runImageOps([{ src: join(outTmp, png), dst: webpDst, mode: 'contain', box: pageSize }], `webp-${base}`); + const j = JSON.parse(readFileSync(jsonPath, 'utf8')); + for (const t of j.textures || []) if (t.image) t.image = t.image.replace(/\.png$/, '.webp'); + const jsonDst = join(ATLAS_DIR, `${base}.json`); + writeFileSync(jsonDst, JSON.stringify(j, null, 2)); + const frames = (j.textures || []).reduce((s, t) => s + (t.frames?.length || 0), 0); + pages.push({ image: `atlas/${base}.webp`, json: `atlas/${base}.json`, bytes: statSync(webpDst).size, frames }); + } + return pages; +} + +const kb = (n) => `${(n / 1024).toFixed(0)}KB`; +const problems = []; +const qa = { missingRaw: [], alphaBad: [], counts: {}, bytes: {} }; + +// ═══ 1. 分组落 stage ═══ +const groups = ['cards', 'beasts', 'ui', 'relics', 'bg'].filter((g) => !onlyGroup || g === onlyGroup); + +// cards / ui:bleed,cover 缩尺直接进 stage(frame 名 = 去前缀 id) +for (const g of groups.filter((x) => x === 'cards' || x === 'ui')) { + const ops = []; + for (const s of SHOTS.filter((x) => x.group === g)) { + const raw = findRaw(g, s.id); + if (!raw) { qa.missingRaw.push(s.id); continue; } + ops.push({ src: raw, dst: join(STAGE, s.atlas, `${s.frame}.png`), mode: 'cover', w: s.final.w, h: s.final.h }); + ops.push({ src: raw, dst: join(REVIEW_DIR, g, `${s.frame}.webp`), mode: 'cover', w: s.final.w, h: s.final.h }); + } + runImageOps(ops, g); +} + +// beasts / relics:sprite,先抠图再 contain 缩尺 +for (const g of groups.filter((x) => x === 'beasts' || x === 'relics')) { + const shots = SHOTS.filter((x) => x.group === g); + const toKey = []; + const meta = []; // {keyedPng, atlas, frame, box, group} + for (const s of shots) { + const raw = findRaw(g, s.id); + if (!raw) { qa.missingRaw.push(s.id); continue; } + toKey.push(raw); + meta.push({ keyed: join(KEYED, g, `${s.id}.png`), atlas: s.atlas, frame: s.frame, box: s.final.box }); + } + if (g === 'beasts') { + for (const r of REUSE.beasts) { // 批次一定稿复用(创始人已眼验) + toKey.push(join(BATCH1, r.src)); + const base = r.src.split('/').pop().replace(/\.[a-z]+$/, ''); + meta.push({ keyed: join(KEYED, g, `${base}.png`), atlas: r.atlas, frame: r.id, box: r.box }); + } + } + keyOut(toKey, join(KEYED, g)); + const bad = alphaCheck(meta.map((m) => m.keyed).filter((p) => existsSync(p))); + qa.alphaBad.push(...bad); + const ops = []; + for (const m of meta) { + if (!existsSync(m.keyed)) { problems.push(`${g}:抠图无产物 ${m.keyed}`); continue; } + ops.push({ src: m.keyed, dst: join(STAGE, m.atlas, `${m.frame}.png`), mode: 'contain', box: m.box }); + ops.push({ src: m.keyed, dst: join(REVIEW_DIR, g, `${m.frame}.webp`), mode: 'contain', box: m.box }); + } + runImageOps(ops, g); +} + +// bg:新产 2 + 批次一复用 4,webp q80 原尺进 assets/bg +const bgFiles = []; +if (groups.includes('bg')) { + const ops = []; + for (const s of SHOTS.filter((x) => x.group === 'bg')) { + const raw = findRaw('bg', s.id); + if (!raw) { qa.missingRaw.push(s.id); continue; } + ops.push({ src: raw, dst: join(BG_DIR, `${s.id}.webp`), mode: 'cover', w: s.final.w, h: s.final.h }); + bgFiles.push({ id: s.id, source: '资产一期新产' }); + } + for (const r of REUSE.bg) { + ops.push({ src: join(BATCH1, r.src), dst: join(BG_DIR, `${r.id}.webp`), mode: 'cover', w: 768, h: 1360 }); + bgFiles.push({ id: r.id, source: '批次一定稿复用' }); + } + runImageOps(ops, 'bg'); + for (const f of bgFiles) { + const p = join(BG_DIR, `${f.id}.webp`); + if (existsSync(p)) { f.file = `bg/${f.id}.webp`; f.bytes = statSync(p).size; copyFileSync(p, join(REVIEW_DIR, 'bg', `${f.id}.webp`)); } + } + mkdirSync(join(REVIEW_DIR, 'bg'), { recursive: true }); +} + +// ═══ 2. 打图集 ═══ +const PAGE_SIZE = { 'cards-jia': 2048, 'beasts-act1': 2048, 'beasts-act2': 2048, 'beasts-act3': 2048, relics: 1024, ui: 2048 }; +const atlases = {}; +for (const atlasName of Object.keys(PAGE_SIZE)) { + const g = atlasName.startsWith('cards') ? 'cards' : atlasName.startsWith('beasts') ? 'beasts' : atlasName; + if (!groups.includes(g)) continue; + const stageDir = join(STAGE, atlasName); + if (!existsSync(stageDir)) continue; + const pages = packAtlas(atlasName, stageDir, PAGE_SIZE[atlasName]); + if (pages) atlases[atlasName] = pages; +} + +// ═══ 3. 机检对账 ═══ +const expect = { + 'cards-jia': CARD_IDS.length, // 66 + relics: RELIC_IDS.length, // 40 + ui: SHOTS.filter((s) => s.group === 'ui').length, // 15 + beastsTotal: SHOTS.filter((s) => s.group === 'beasts').length + REUSE.beasts.length, // 39 +}; +const frameCount = (a) => (atlases[a] || []).reduce((s, p) => s + p.frames, 0); +if (groups.includes('cards') && frameCount('cards-jia') !== expect['cards-jia']) problems.push(`cards-jia 帧数 ${frameCount('cards-jia')} ≠ ${expect['cards-jia']}`); +if (groups.includes('relics') && frameCount('relics') !== expect.relics) problems.push(`relics 帧数 ${frameCount('relics')} ≠ ${expect.relics}`); +if (groups.includes('ui') && frameCount('ui') !== expect.ui) problems.push(`ui 帧数 ${frameCount('ui')} ≠ ${expect.ui}`); +const beastFrames = frameCount('beasts-act1') + frameCount('beasts-act2') + frameCount('beasts-act3'); +if (groups.includes('beasts') && beastFrames !== expect.beastsTotal) problems.push(`beasts 帧数 ${beastFrames} ≠ ${expect.beastsTotal}`); +if (qa.missingRaw.length) problems.push(`缺原图 ${qa.missingRaw.length}:${qa.missingRaw.join(',')}`); +for (const b of qa.alphaBad) problems.push(`透明底异常 ${b.file}(${b.why})`); + +// ═══ 4. manifest + 字节账 ═══ +const byGroup = {}; +for (const [name, pages] of Object.entries(atlases)) byGroup[name] = pages.reduce((s, p) => s + p.bytes, 0); +byGroup.bg = bgFiles.reduce((s, f) => s + (f.bytes || 0), 0); +const totalBytes = Object.values(byGroup).reduce((s, n) => s + n, 0); +const manifest = { + version: 1, + generatedAt: new Date().toISOString(), + anchor: 'docs-design/山海风格版-批次一/anchor-pack.json(风格锚 v1 + 工艺修订 r1,单源)', + ledger: 'docs-design/山海风格版-批次一/manifest-巡异录资产一期.jsonl', + snapshotNote: '卡/遗物/敌帧名对齐 2026-07-06 tables.js 快照(66 卡/40 遗物/8 敌)+ 兽谱-增补-巡异录.md 31 兽;职业乙插画位未产,等 M3 表落地下一批补齐', + atlases, + bg: bgFiles, + totals: { webpBytes: totalBytes, byGroup }, +}; +if (!onlyGroup) writeFileSync(join(ASSETS, 'manifest.json'), JSON.stringify(manifest, null, 2)); + +appendFileSync(MANIFEST_LEDGER, JSON.stringify({ ts: new Date().toISOString(), kind: 'qa-pack', groups, problems, byGroup, totalBytes }) + '\n'); + +console.log('\n═══ 打包结果 ═══'); +for (const [name, pages] of Object.entries(atlases)) { + console.log(`${name}:${pages.length} 页 / ${pages.reduce((s, p) => s + p.frames, 0)} 帧 / ${kb(byGroup[name])}`); +} +if (bgFiles.length) console.log(`bg:${bgFiles.length} 张 / ${kb(byGroup.bg)}`); +console.log(`合计 webp:${kb(totalBytes)}(${(totalBytes / 1048576).toFixed(2)}MB)`); +if (problems.length) { console.error(`\n机检 ${problems.length} 项异常:`); for (const p of problems) console.error(' - ' + p); process.exit(1); } +console.log('机检:全部通过'); diff --git a/tier2/games/shanhai-xunyilu/scripts/asset-shots.mjs b/tier2/games/shanhai-xunyilu/scripts/asset-shots.mjs new file mode 100644 index 00000000..f504bbf6 --- /dev/null +++ b/tier2/games/shanhai-xunyilu/scripts/asset-shots.mjs @@ -0,0 +1,329 @@ +// scripts/asset-shots.mjs —— 《山海巡异录》资产一期批产 · 镜头单(数据件) +// owner:美术批产执行席(2026-07-06,工单:资产一期,锚 = docs-design/山海风格版-批次一/anchor-pack.json r1) +// +// 【快照纪律】M3 开发席平行在飞会动 tables.js(加职业乙卡)。本批以开工时读到的表为准, +// 卡/遗物/敌 id 在此**硬编码快照**、不运行时 import tables.js —— 防批产中途表漂移; +// 职业乙插画位命名等 M3 表落地后下一批补齐,本批不产。 +// 【谱面纪律】敌兽主体词一律从兽谱出发(首批 = 兽谱首批.md;增补 = 兽谱-增补-巡异录.md), +// 兽名不入 prompt(批次一工艺事实 4:名词强 prime 错形),只用形态语言。 +// 【clean-room】主体词禁现代作品名与其专有名词;古籍意象 + 矿彩锚原创演绎。 + +'use strict'; + +/** 快照时间与来源(审计锚)。 */ +export const SNAPSHOT_AT = '2026-07-06 tables.js(M2 66卡/8敌/40遗物 定容态)'; + +/** 职业甲 66 卡 id 快照(tables.js CARDS 原序;帧名 = 卡 id)。 */ +export const CARD_IDS = [ + 'mu-qingteng', 'mu-shengfa', 'mu-chanzhi', 'mu-cangyin', + 'huo-lihuo', 'huo-liaoyuan', 'huo-chiyu', 'huo-liming', + 'tu-houtu', 'tu-zhenyue', 'tu-xirang', 'tu-chengtian', + 'jin-jinfeng', 'jin-ruijin', 'jin-baihong', 'jin-gengjin', + 'shui-xuanshui', 'shui-runxia', 'shui-canglang', 'shui-guixu', + 'mu-canglin', 'mu-jiamu', 'huo-bifang', 'huo-fenglin', + 'tu-kunlun', 'tu-houde', 'jin-baize', 'jin-tianwu', + 'shui-tianchi', 'shui-xuangui', + 'mu-huichun', 'mu-panmu', 'huo-fenxin', 'huo-yanbao', 'huo-zhurong', + 'tu-panshi', 'tu-hancheng', 'jin-podan', 'jin-cuijin', 'jin-qiongqi', + 'shui-hanyuan', 'shui-lixue', + 'mu-cangwu', 'mu-jianmu', 'mu-linggen', 'huo-jinyu', 'huo-jinwu', + 'tu-leitu', 'tu-taowu', 'tu-xiyan', 'jin-kunwu', 'jin-baijin', + 'shui-ruoshui', 'shui-guixuan', + 'mu-sanglin', 'mu-lingzhang', 'mu-kuafu', 'huo-suiren', 'huo-huolin', + 'tu-chengyuan', 'tu-houde2', 'jin-shuojin', 'jin-kunwu2', + 'shui-canghai', 'shui-hantan', 'shui-beiming', +]; + +/** 遗物 40 id 快照(tables.js RELICS 原序;帧名 = 遗物 id)。 */ +export const RELIC_IDS = [ + 'yuheng-fu', 'guixu-jing', 'yinglong-lin', + 'kunlun-yugui', 'zhulong-jing', 'hetu-juan', 'luoshu-ye', 'fusang-zhi', + 'xihe-pei', 'wangshu-huan', 'kaiming-ya', 'luwu-zhao', 'yingzhao-yu', + 'zhengpi-gu', 'jingwei-shi', 'zhurong-guan', 'houtu-yin', 'rushou-yue', + 'jumang-bian', 'tengshe-zhu', 'baize-ce', 'qingluan-ling', + 'chenghuang-an', 'tianwu-pei', 'shuhai-chi', 'ruomu-deng', 'sanqingniao-yu', + 'gonggong-jiao', 'xuanming-zhu', 'xiwangmu-zan', 'taotie-ding', 'jianmu-xin', + 'jiuwei-huan', 'yayu-gu', + 'jia-zhoutian-lu', 'jia-lianji-fu', 'jia-wuxing-huan', 'jia-zhanshuai', + 'jia-ningxue', 'jia-lingxi', +]; + +/** 已入表 8 敌 id 快照(tables.js ENEMIES;涸泽鲛复用批次一定稿,其余 7 只本批新产)。 */ +export const ENEMY_IDS_IN_TABLE = [ + 'kuze-jiao', 'qingqiu-hu', 'ba-she', 'chi-hou', 'jiuwei-linghu', + 'xiang-liu', 'zhu-yin', 'tao-tie', +]; + +/** 立绘缩尺(最长边,兽谱增补 §三):普通 448 / 精英 512 / Boss 640。 */ +const BEAST_BOX = { normal: 448, elite: 512, boss: 640 }; + +// ───────────────────────────────────────────────────────────────────────────── +// 卡插画 66(bleed 1024² → 256² cover;frame 名 = 卡 id,图集 cards-jia) +// 主体词按卡名/卡效/五行色语写就;稀有卡构图更满,普通卡"构图简洁,主体居中醒目"。 +// ───────────────────────────────────────────────────────────────────────────── +const CARD_SUBJECTS = { + // ── 木(石绿/生发)── + 'mu-qingteng': '数条石绿青藤自地面破土而出,向上缠绕生长,藤上生叶带刺,构图简洁,主体居中醒目', // 首版「藤鞭横抽」prime 出龙形,去比喻重写(台账 gen-call 留证) + 'mu-shengfa': '一枚素面无字的木牌悬空,木牌下方嫩芽新叶层层舒展生长,石绿描金,构图简洁', // 首版「符牌」引出大块伪刻字,改素面 + + 'mu-chanzhi': '缠枝藤蔓在空中结成一只收紧的螺旋牢笼,枝端带刺,石绿底描金枝线', + 'mu-cangyin': '一株苍劲古木撑开树冠如巨伞,树荫下浮起一圈护体符光,构图简洁', + 'mu-canglin': '一只苍绿鳞甲的兽爪自卷云中探出一抓,爪风卷起数片叶,动势凌厉', + 'mu-jiamu': '庭园中一株嘉木开满描金花枝,花瓣飘落化作小符,工整明快', + 'mu-huichun': '一滴晶莹露珠自叶尖垂落,落处地面生出一小片新绿,构图简洁,主体居中', + 'mu-panmu': '蟠曲老木的粗枝层层盘绕缠卷收紧,树皮皴裂,描金勾边', // 「如巨蟒」易 prime 蛇形,去比喻 + 'mu-cangwu': '苍梧巨木横出一枝,枝上嫩芽与锋利断枝并生,一枝两意,构图简洁', + 'mu-jianmu': '一株通天巨木直贯画面顶天立地,树干缠绕描金符文,云气缭绕', + 'mu-linggen': '大地剖面里盘结的描金树根网络,根须发光向四方蔓延', + 'mu-sanglin': '一张桑木长弓射出一支缠着桑叶的箭,箭尾曳出石绿光线,构图简洁', + 'mu-lingzhang': '一面樟木纹圆盾,盾面生出鲜嫩新叶,泛石绿柔光,构图简洁,主体居中', + 'mu-kuafu': '一位持杖巨人的剪影大步奔向天边巨大落日,身后烟尘滚滚,悲壮恢弘', + // ── 火(朱砂/离明)── + 'huo-lihuo': '一道朱砂符纸腾起一小簇符文形状的烈焰,构图简洁,主体居中醒目', + 'huo-liaoyuan': '三股火舌自一张符纸窜出,在原野上连成一线燎原之火', + 'huo-chiyu': '一根燃烧的朱砂色羽毛被掷出,拖出短促焰尾,构图简洁,主体居中', + 'huo-liming': '天空降下一柱描金真火贯穿画面,火柱中隐现一轮日纹,恢弘庄重', + 'huo-bifang': '一只独脚青羽仙鹤衔着一段燃火的枯枝飞临,鹤形清瘦,火光映羽', + 'huo-fenglin': '一条火线在林间奔窜,成排树影次第点燃,朱砂与暗林对比', + 'huo-fenxin': '一枚朱砂咒印悬浮,印下一团暗火阴燃不熄,烟气细长,构图简洁', + 'huo-yanbao': '一张符纸炸开一团爆焰,火星四溅成环,冲击波纹一圈,主体居中', + 'huo-zhurong': '一卷燃烧的古策书展开,页面上的火纹字符腾起化作焰柱,华丽威严', + 'huo-jinyu': '一堆灰烬中拨出一点将熄复燃的火星,微光映亮四周,构图极简,主体居中', + 'huo-jinwu': '一只三足金色神鸟自巨大日轮中俯冲而下,双翅曳焰,恢弘夺目', + 'huo-suiren': '一双苍老的手在木上钻出第一缕火种,火星初迸,神圣庄重', + 'huo-huolin': '一只鳞甲燃焰的鹿角独兽踏焰奔驰,蹄下生火,鬃焰飞扬', + // ── 土(赭石/厚载)── + 'tu-houtu': '大地隆起一面厚重的夯土石壁,壁面嵌着描金岩纹,构图简洁,主体居中', + 'tu-zhenyue': '一座山岳的虚影压在一枚符牌上方,山根透出金光,稳重如镇', + 'tu-xirang': '一撮神土在掌心自行堆起一座微型山丘,土粒悬浮,构图简洁', + 'tu-chengtian': '一只巨大石掌自地底托起,掌上承着一弧天穹之光,恢弘厚重', + 'tu-kunlun': '一枚玉璧自巍峨山影间坠下,玉光如帐罩落大地,工整庄严', + 'tu-houde': '一枚土黄大印展开,印面上山川河流的微缩纹样徐徐浮现,华贵厚重', + 'tu-panshi': '一块磐石悬浮空中,石面裂纹里透出描金光,构图简洁,主体居中醒目', + 'tu-hancheng': '一记岩石巨拳撼动一段城垣状石阵,碎石崩飞,力量感十足', + 'tu-leitu': '层层夯土垒起一座小山,每层土中嵌着一枚小符印,构图简洁', + 'tu-taowu': '一只虎躯凶兽被镇压在山岳形符狱之下,描金锁链纵横,凶而不得出', + 'tu-xiyan': '一撮神土沿符阵纹路自行衍化蔓延,土脊蜿蜒隆起推进,赭石描金', // 「如活物爬行」易 prime 兽形,去比喻 + 'tu-chengyuan': '符光化出一段夯土城垣横亘画面,垛口整齐,烽色沉稳,构图简洁', + 'tu-houde2': '一方石印浮空,印下田畴山川如画卷舒展,土黄描金,工整安稳', + // ── 金(雌黄/锐断)── + 'jin-jinfeng': '一道雌黄金色的剑锋之光斜劈而下,断口利落,构图简洁,主体居中醒目', + 'jin-ruijin': '数枚金针自一座小符阵中悬浮竖起,针尖泛冷光,阵纹描金,构图简洁', + 'jin-baihong': '一道白虹般的光刃横贯画面,如长虹贯日,凌厉工整', + 'jin-gengjin': '金色刀雨自天斜落,刀刃成排坠下,寒光凛冽', + 'jin-baize': '一面能照见万象的玉镜,镜中浮现一个发光的兽形轮廓,神异清雅', + 'jin-tianwu': '一只八颗虎首的巨兽自浪涛中探出,八口齐张,气势磅礴', + 'jin-podan': '一枚金色咒钉楔入一块裂开的玉石,裂纹自钉处放射,构图简洁,主体居中', + 'jin-cuijin': '一点金色符光精准点在一圈靶环状的穴位纹中心,构图极简,主体居中', + 'jin-qiongqi': '一只带翼巨虎仰天长啸,啸声化作一圈圈金色音波环,威猛慑人', + 'jin-kunwu': '一柄赤色宝刀切开一块玉璧,切面平滑如镜,刀光如霞', + 'jin-baijin': '锻炉中一块金精被符文小锤锻打,火星金屑迸溅,匠气庄重', + 'jin-shuojin': '双刃短剑交叉挥出两道金色弧光,轨迹利落,构图简洁,主体居中', + 'jin-kunwu2': '一柄剑浸入描金淬火池,池面腾起金色雾气,构图简洁', + // ── 水(石青/润下)── + 'shui-xuanshui': '一道墨蓝漩涡卷起数片水刃扑击,漩心幽深,构图简洁,主体居中', + 'shui-runxia': '水滴沿一张符纸边缘润下,在符下聚成一泓镜面小潭,静谧清雅', + 'shui-canglang': '一道沧浪自展开的卷轴中涌出,浪头卷着点点符光,气韵流动', + 'shui-guixu': '深渊巨潮席卷而来,潮头分成数股巨浪俯冲,幽蓝描金,气势宏大', // 「水龙」必 prime 龙形,改巨浪 + 'shui-tianchi': '高山天池的水面映着一轮描金圆月,月影微漾,静谧空灵', + 'shui-xuangui': '一只玄色巨龟浮出水面,背甲上浮现描金的河图星点纹,古朴神圣', + 'shui-hanyuan': '寒渊之底一缕幽蓝咒光盘旋升起,冻雾弥漫,清冷彻骨', + 'shui-lixue': '一滴朱砂色血珠落入水碗,在水中晕开成一个符文形,凝重神秘,主体居中', + 'shui-ruoshui': '三千弱水如帘幕垂落,水帘中一根羽毛缓缓下沉,轻柔而无可承载', + 'shui-guixuan': '一个巨大的深渊漩涡,漩心幽蓝发光,周围碎浪成环,吞噬之势', + 'shui-canghai': '沧海夜面升起一轮明月,月光在海上铺成一条描金水路,开阔宁静', + 'shui-hantan': '一泓幽深寒潭映出北斗七星,潭面结着薄冰,星点描金,构图简洁', + 'shui-beiming': '一条巨鱼自北海跃出水面,身影遮天,浪花如雪崩落,恢弘壮阔', +}; + +// ───────────────────────────────────────────────────────────────────────────── +// 敌兽立绘 37 新产 + 2 复用(sprite 1024² → 抠图 → 档位缩尺;图集 beasts-act{n}) +// 主体词 = 谱面形貌意象的形态语言(不含兽名);tier/act 与谱册一致。 +// ───────────────────────────────────────────────────────────────────────────── +export const BEASTS = [ + // —— 已入表 7 只(补谱面见增补册 §一)—— + { id: 'qingqiu-hu', tier: 'normal', act: 1, subject: '赤色幻狐,双尾将分未分,周身浮着点点狐火,足不沾地,身影边缘微微虚化,立姿回首,全身像' }, + { id: 'ba-she', tier: 'normal', act: 2, subject: '青黑色巨蟒,鳞列如叠瓦,腹部纹路隐约如古舆图,大口微张露出弯牙,盘卷昂首,全身像' }, + { id: 'chi-hou', tier: 'elite', act: 1, subject: '赤红鬃毛的猛兽,躯干如巨犬而头似狮首,喉间透出火光,鬃毛根根竖起如燃烧,张口怒啸,全身像' }, + { id: 'jiuwei-linghu', tier: 'elite', act: 2, subject: '雪白底色朱砂纹的大狐,九条尾巴各燃一簇狐火,群尾展开如火焰轮,目细长含慧光,优雅立姿,全身像' }, + { id: 'xiang-liu', tier: 'boss', act: 1, subject: '九个蛇首共生于一具青色巨蟒之躯,盘绕如小山,九首高低错落各衔一股浊浪,鳞色石青泛冷光,威压构图,全身像' }, + { id: 'zhu-yin', tier: 'boss', act: 2, subject: '赤色巨蛇盘绕成环如山岳,头顶生一枚巨大的独眼,目光如炬,躯体鳞纹如日晷刻度,庄重威压,全身像' }, + { id: 'tao-tie', tier: 'boss', act: 3, subject: '一张巨大的青铜兽面与巨口,獠牙如钩,双目凸圆,面后躯干化作青铜纹样的雾状虚影,狰狞威压,正面构图' }, + // —— 首批谱两只(巡异录参数新产)—— + { id: 'tao-wu', tier: 'elite', act: 1, subject: '虎躯人面的凶兽,犬毛蓬乱如枯草,长尾曳地,肤色赭石带土黄,眉目顽愚而凶,低头顶撞姿态,全身像' }, + { id: 'qiong-qi', tier: 'elite', act: 2, subject: '一只带翼巨虎,背生猬毛如短戟,双翼羽刃泛雌黄金光,目色冷白,展翼欲扑,全身像' }, + // —— 新增普通 20 —— + { id: 'li-li', tier: 'normal', act: 1, subject: '土黄色野豚,四足生着鸡距般的骨刺,低头拱土姿态,背脊沾着新翻的泥土,全身像' }, + { id: 'zhu-niao', tier: 'normal', act: 1, subject: '灰褐色鸱枭怪鸟,双爪如五指细枝般分叉,栖立时爪指交叠,圆目冷光,全身像' }, + { id: 'gu-diao', tier: 'normal', act: 1, subject: '一只巨雕头生弯刀状独角,羽色雌黄带金,展翅俯冲姿态,利爪前伸,全身像' }, + { id: 'ran-yi', tier: 'normal', act: 1, subject: '鱼身蛇首的怪鱼,六条短足如桨,鳞色石青,离水疾行姿态,全身像' }, + { id: 'meng-huai', tier: 'normal', act: 1, subject: '貆形小兽,通身赤色硬毫直立如火刺,毫尖带着点点火星,弓身戒备,全身像' }, + { id: 'hao-zhi', tier: 'normal', act: 1, subject: '白毛野豚,背生长棘如簪、棘端乌黑,长棘竖立如林,侧身低吼,全身像' }, + { id: 'tian-gou', tier: 'normal', act: 1, subject: '玄黑躯体白色头颅的狸犬,双目如银环,疾奔扑击姿态,矫健迅捷,全身像' }, + { id: 'fu-zhu', tier: 'normal', act: 1, subject: '通体雪白的鹿,头生四只珊瑚枝般的角,蹄下泛起浅浅水纹,昂首踏水,全身像' }, + { id: 'chang-you', tier: 'normal', act: 2, subject: '青灰色长臂猿猴,头生四只耳朵,双臂长过膝,一臂横扫姿态,全身像' }, + { id: 'fei-yi', tier: 'normal', act: 2, subject: '赤鳞怪蛇,六足撑地,背生四片如枯叶的翅,身周热浪扭曲,昂首吐信,全身像' }, + { id: 'dang-kang', tier: 'normal', act: 2, subject: '壮硕巨豚,一对獠牙外翻如双镰,四蹄厚重如石础,低头掀撞姿态,全身像' }, + { id: 'huan-li', tier: 'normal', act: 2, subject: '玄色狸兽,一只独眼生在面部正中,三条尾巴分展,张口作啸,全身像' }, + { id: 'he-luo', tier: 'normal', act: 2, subject: '一首十身的怪鱼,十条鱼躯如裙裾环列共一个鱼首,鳍尾翻卷起漩流,全身像' }, + { id: 'ming-she', tier: 'normal', act: 2, subject: '赭红色细蛇,背生四片薄如石磬的翅,振翅悬空,周围荡开一圈圈音波纹,全身像' }, + { id: 'zhu-huai', tier: 'normal', act: 2, subject: '铁灰色巨牛,头生四角两两交错,垂着宽大的猪状耳朵,低头喘息冲撞姿态,全身像' }, + { id: 'qiu-yu', tier: 'normal', act: 3, subject: '兔身鸟喙的小兽,鸱枭圆目,蛇形细尾盘卷,伏地佯睡而睁着一只眼,全身像' }, + { id: 'zhu-ru', tier: 'normal', act: 3, subject: '朱红色狐身,肋部生鱼鳍状双翼,鳍膜透光如琥珀,鼓翼低飞,全身像' }, + { id: 'you-you', tier: 'normal', act: 3, subject: '土黄色骏马之躯,山羊般的横瞳,头生四角,牛尾扫地,前蹄刨地欲冲,全身像' }, + { id: 'bi-bi', tier: 'normal', act: 3, subject: '银灰色狐身,背生一对大雁般的长翼,翼羽泛雌黄金光,掠地滑翔,全身像' }, + { id: 'hua-she', tier: 'normal', act: 3, subject: '豺狼躯干作蛇形游走,背生鸟翼,头颈鬣毛垂遮面部只露一双竖瞳,蜿蜒逼近,全身像' }, + // —— 新增精英 5 —— + { id: 'bo-ma', tier: 'elite', act: 1, subject: '白马之躯黑色马尾,额生螺纹独角,口生虎牙、四蹄裂为虎爪,昂首嘶鸣战意凛然,全身像' }, + { id: 'man-man', tier: 'elite', act: 1, subject: '一对野鸭状怪鸟,各自只有一翼一目,青绿羽色,两鸟相贴合翼齐飞,相合处水汽凝成光环,全身像' }, + { id: 'fei-niu', tier: 'elite', act: 2, subject: '灰褐色瘦牛,白色头颅生一只独眼,蛇形长尾曳地,足下草木枯萎成圈,阴郁前行,全身像' }, + { id: 'yong-he', tier: 'elite', act: 2, subject: '雌黄毛色的巨猿,赤红双目与赤红口喙,狂态毕露,双臂挥舞,身后拖着焦糊爪印,全身像' }, + { id: 'liang-qu', tier: 'elite', act: 3, subject: '玄色狸形巨兽,白色头颅,双前肢是过大的虎爪、爪甲如五柄短刀,前爪高举下劈,全身像' }, + // —— 新增 Boss 3 —— + { id: 'jiu-ying', tier: 'boss', act: 1, subject: '九颗蛇颈兽首自一具矮壮躯干升起,四首衔水色如石青、五首吐焰色如朱砂,九首攒动啼张各异,威压构图,全身像' }, + { id: 'di-jiang', tier: 'boss', act: 2, subject: '浑圆如布囊的神鸟,通体赤黄如丹火,没有面孔与眼睛,六足错落、四翼展开,周身浮起描金乐纹涟漪,神异庄重,全身像' }, + { id: 'kui-niu', tier: 'boss', act: 3, subject: '苍青色巨牛,无角,仅有一条粗壮独足,足下踏着雷电光环,身泛青白辉光,一步踏雷之势,威压构图,全身像' }, +]; + +/** 复用批次一定稿(创始人已眼验;不重摇,直接进产线抠图/图集)。 */ +export const REUSE_BEASTS = [ + { id: 'kuze-jiao', tier: 'normal', act: 1, src: 'images/xunyilu/xy-beast-hezejiao.jpg' }, + { id: 'zheng', tier: 'normal', act: 1, src: 'images/xunyilu/xy-beast-zheng.jpg' }, +]; + +/** 复用批次一 bleed 定稿(场景/结算基版,进 assets/bg;标记来源=批次一)。 */ +export const REUSE_BG = [ + { id: 'bg-scene-01', src: 'images/xunyilu/xy-scene-01.jpg' }, + { id: 'bg-scene-02', src: 'images/xunyilu/xy-scene-02.jpg' }, + { id: 'bg-result-win', src: 'images/xunyilu/xy-result-win.jpg' }, + { id: 'bg-result-lose', src: 'images/xunyilu/xy-result-lose.jpg' }, +]; + +// ───────────────────────────────────────────────────────────────────────────── +// UI 件 15(生图出框件与底纹,文字一律程序排字 —— 批次一工艺事实 6) +// ───────────────────────────────────────────────────────────────────────────── +const UI_SHOTS = [ + // 稀有度空框 3(补批次一整卡样张之缺:框件必须留空供程序装配插画+文字) + { id: 'ui-frame-common', w: 760, h: 1064, fw: 380, fh: 532, subject: '空白卡牌边框设计,素朴石灰岩质感边框,边框厚拙简洁,四角以小块岩纹收角,框内中央与下方完全留空为纯浅米色,没有任何图案' }, + { id: 'ui-frame-rare', w: 760, h: 1064, fw: 380, fh: 532, subject: '空白卡牌边框设计,描银卷云纹边框,银线卷云沿框边游走,清雅工整,框内中央与下方完全留空为纯浅米色,没有任何图案' }, + { id: 'ui-frame-epic', w: 760, h: 1064, fw: 380, fh: 532, subject: '空白卡牌边框设计,描金蟠纹边框,金线蟠曲盘绕框边,四角嵌描金小徽记,华丽庄重,框内中央与下方完全留空为纯浅米色,没有任何图案' }, + // 状态框 3(可打出/选中/禁用;程序叠加在卡面之上) + { id: 'ui-state-playable', w: 760, h: 1064, fw: 380, fh: 532, subject: '空白卡牌边框设计,描金流光高亮边框,框体透出温暖金色光晕,光感克制不外溢,框内完全留空为纯浅米色,没有任何图案' }, + { id: 'ui-state-selected', w: 760, h: 1064, fw: 380, fh: 532, subject: '空白卡牌边框设计,朱砂色光边边框,框体外沿一圈朱砂描边微微发光,框内完全留空为纯浅米色,没有任何图案' }, + { id: 'ui-state-disabled', w: 760, h: 1064, fw: 380, fh: 532, subject: '空白卡牌边框设计,黯淡灰色石质边框,框体如蒙尘石板毫无光泽,框内完全留空为浅灰色,没有任何图案' }, + // 五行属性底纹 5(插画位/卡底的低对比装饰层) + { id: 'ui-elem-mu', w: 760, h: 1064, fw: 380, fh: 532, subject: '卡牌底纹设计,石绿色调的缠枝草木暗纹铺满整个画面,纹样低对比若隐若现,通体一张平整底纹,没有边框' }, + { id: 'ui-elem-huo', w: 760, h: 1064, fw: 380, fh: 532, subject: '卡牌底纹设计,朱砂色调的火焰云气暗纹铺满整个画面,纹样低对比若隐若现,通体一张平整底纹,没有边框' }, + { id: 'ui-elem-tu', w: 760, h: 1064, fw: 380, fh: 532, subject: '卡牌底纹设计,赭石土黄色调的山岳岩层暗纹铺满整个画面,纹样低对比若隐若现,通体一张平整底纹,没有边框' }, + { id: 'ui-elem-jin', w: 760, h: 1064, fw: 380, fh: 532, subject: '卡牌底纹设计,雌黄与银白色调的锋刃云雷暗纹铺满整个画面,纹样低对比若隐若现,通体一张平整底纹,没有边框' }, + { id: 'ui-elem-shui', w: 760, h: 1064, fw: 380, fh: 532, subject: '卡牌底纹设计,石青色调的水波涟漪暗纹铺满整个画面,纹样低对比若隐若现,通体一张平整底纹,没有边框' }, + // 界面底纹 2 + { id: 'ui-bg-panel', w: 1024, h: 1024, fw: 512, fh: 512, subject: '游戏界面面板底纹,展开的卷轴册页纸面质感,米色纸面带细微纤维纹理与淡淡青绿云边,平整满幅' }, + { id: 'ui-bg-hud', w: 1024, h: 1024, fw: 512, fh: 512, subject: '游戏界面底纹,暗色岩石质感铺满,石青石绿矿彩沁色斑驳,深沉低调,平整满幅' }, + // 图鉴格 2 + { id: 'ui-tujian-cell', w: 1024, h: 1024, fw: 256, fh: 256, subject: '图鉴格子设计,一枚方形描金细框的石质空格居中,格心浅凹留空,四角饰小小云纹,满幅单格' }, + { id: 'ui-tujian-locked', w: 1024, h: 1024, fw: 256, fh: 256, subject: '图鉴格子设计,一枚方形暗色石质空格居中,格心一团墨色云雾剪影缭绕,神秘未知感,满幅单格' }, +]; + +// ───────────────────────────────────────────────────────────────────────────── +// 遗物图标 40(sprite 1024² → 抠图 → 128px;"法器·"领句 = 批次一 weapon 镜头已验配方) +// 遗物名含兽名者一律改写为器物形态语言(兽名不入 prompt)。 +// ───────────────────────────────────────────────────────────────────────────── +const RELIC_SUBJECTS = { + 'yuheng-fu': '法器·一枚白玉符牌,牌面刻北斗七星连线,斗柄一星描金发亮,悬浮微光', + 'guixu-jing': '法器·一面青铜古镜,镜背铸漩涡纹,镜面幽深如渊,悬浮微光', + 'yinglong-lin': '法器·单独一枚巨大鳞片的特写,鳞面天然云雷纹,边缘泛描金光,悬浮微光', // 「龙鳞」易 prime 整龙,改鳞片特写 + 'kunlun-yugui': '法器·一柄温润玉圭,圭身刻层叠山形纹,顶端尖圆,悬浮微光', + 'zhulong-jing': '法器·一颗竖瞳龙目状宝珠,珠内燃着一点不灭烛光,悬浮微光', + 'hetu-juan': '法器·半卷残破古图,图上星点以描金细线相连,卷边焦损,悬浮微光', + 'luoshu-ye': '法器·一页龟甲纹古书残页,页面数点排成方阵,悬浮微光', + 'fusang-zhi': '法器·一段神木桑枝,枝头栖着一枚小小的描金日轮,悬浮微光', + 'xihe-pei': '法器·一副描金马辔头,辔环铸成小日轮形,革带朱砂色,悬浮微光', + 'wangshu-huan': '法器·一只月白色玉环,环身缠绕淡淡云纹,清辉流转,悬浮微光', + 'kaiming-ya': '法器·一枚巨兽獠牙吊坠,牙根镶描金箍,牙面有细密咒纹,悬浮微光', + 'luwu-zhao': '法器·一只风干的虎爪法器,爪尖描金,腕部系朱砂绳结,悬浮微光', + 'yingzhao-yu': '法器·一根赤色纹路的神鸟长羽,羽梢卷曲泛金,悬浮微光', + 'zhengpi-gu': '法器·一面赤豹皮蒙面的小手鼓,鼓面绘五道焰形纹,鼓身描金,悬浮微光', + 'jingwei-shi': '法器·一枚小小的填海石子,石上天然嵌着白色贝纹,朴拙温润,悬浮微光', + 'zhurong-guan': '法器·一顶火焰纹青铜冠,冠顶托着一点真火,火苗不摇,悬浮微光', + 'houtu-yin': '法器·一方厚重石印,印钮雕作伏兽,印底泛土黄光,悬浮微光', + 'rushou-yue': '法器·一柄白刃小钺,钺身刻秋叶纹,刃口冷光,悬浮微光', + 'jumang-bian': '法器·一条青色枝条软鞭,鞭梢生出嫩芽,鞭身缠描金丝,悬浮微光', + 'tengshe-zhu': '法器·一颗被雾气缠绕的宝珠,珠下一截蛇形雾影衔着珠,悬浮微光', + 'baize-ce': '法器·一册兽皮封面的古书,封面烫着描金兽形轮廓,书口垂着穗子,悬浮微光', + 'qingluan-ling': '法器·一只青羽纹铜铃,铃身錾刻羽纹,铃舌描金,悬浮微光', + 'chenghuang-an': '法器·一副描金兽鞍,鞍桥翘起如角,鞍鞯朱砂纹,悬浮微光', + 'tianwu-pei': '法器·一枚八棱玉珮,珮面刻浪涛纹,系着石青绦带,悬浮微光', + 'shuhai-chi': '法器·一把青铜古尺,尺身刻度描金,尺端铸小小足印纹,悬浮微光', + 'ruomu-deng': '法器·一盏树枝形古灯,灯盏如花萼,灯焰赤红不摇,悬浮微光', + 'sanqingniao-yu': '法器·三根并排系在一起的青色神鸟羽,羽根缠描金线,悬浮微光', + 'gonggong-jiao': '法器·一只断裂的巨兽之角,断口如水晶剖面,角身缠浪纹,悬浮微光', + 'xuanming-zhu': '法器·一颗墨色深水宝珠,珠心一点幽蓝光,如深渊之眼,悬浮微光', + 'xiwangmu-zan': '法器·一支白玉古簪,簪首雕胜形纹嵌描金,悬浮微光', + 'taotie-ding': '法器·一尊三足小铜鼎,鼎身铸兽面纹,鼎口溢出一缕金气,悬浮微光', + 'jianmu-xin': '法器·一段神木木芯,断面年轮圈圈描金,木身缠青藤,悬浮微光', + 'jiuwei-huan': '法器·一只由九股赤色绒尾编成的手环,环上一点狐火微光,悬浮微光', + 'yayu-gu': '法器·一根凶兽腿骨,骨面朱砂咒纹层层镇封,骨节缠着细铁链,悬浮微光', + 'jia-zhoutian-lu': '法器·一只小巧的木制辘轳,轮上缠着五色丝线各一段,轮辐描金,悬浮微光', + 'jia-lianji-fu': '法器·一张烧至将尽却不毁的符纸,符文白热发光,边缘焦卷,悬浮微光', + 'jia-wuxing-huan': '法器·五枚小环相扣成链,五环各为石绿朱砂土黄雌黄石青一色,悬浮微光', + 'jia-zhanshuai': '法器·一柄素白麻布缠柄的短刃,刃口极利泛冷光,刃身素净,悬浮微光', + 'jia-ningxue': '法器·一小卷以朱砂写就的箓文,卷轴两端木杆描金,垂着穗结,悬浮微光', + 'jia-lingxi': '法器·一枚犀角雕成的玉佩,角心一线通透发光,系石青绦带,悬浮微光', +}; + +// ───────────────────────────────────────────────────────────────────────────── +// 结算图变体 2(bleed 768×1360;基版 win/lose 复用批次一) +// ───────────────────────────────────────────────────────────────────────────── +const BG_SHOTS = [ + { id: 'bg-result-bosswin', w: 768, h: 1360, subject: '幕终大胜结算图:一整卷横贯天地的古图长轴展开,数只异兽化作描金线稿列队归入图中安伏,金光如瀑,庄重恢弘,竖幅' }, + { id: 'bg-result-zhoutian', w: 768, h: 1360, subject: '周天大胜结算图:石绿朱砂土黄雌黄石青五色矿彩光带在天空连成一个完整圆环,环心描金大放光明,山野俯伏其下,竖幅' }, +]; + +// ───────────────────────────────────────────────────────────────────────────── +// 汇总为镜头单(seed 段位:卡 901xx / 兽 911xx / UI 921xx / 遗物 931xx / bg 941xx, +// 与批次一 71001–85002 段不重叠) +// ───────────────────────────────────────────────────────────────────────────── +function assertKeys(map, ids, label) { + const keys = Object.keys(map); + const missing = ids.filter((i) => !map[i]); + const extra = keys.filter((k) => !ids.includes(k)); + if (missing.length || extra.length) { + throw new Error(`${label} 主体词与快照 id 不对齐:缺 ${JSON.stringify(missing)} 多 ${JSON.stringify(extra)}`); + } +} +assertKeys(CARD_SUBJECTS, CARD_IDS, '卡插画'); +assertKeys(RELIC_SUBJECTS, RELIC_IDS, '遗物图标'); +if (CARD_IDS.length !== 66) throw new Error(`卡快照应 66,实 ${CARD_IDS.length}`); +if (RELIC_IDS.length !== 40) throw new Error(`遗物快照应 40,实 ${RELIC_IDS.length}`); +if (BEASTS.length + REUSE_BEASTS.length !== 39) throw new Error(`敌兽应 39,实 ${BEASTS.length + REUSE_BEASTS.length}`); + +export const SHOTS = [ + ...CARD_IDS.map((id, i) => ({ + id: `card-${id}`, frame: id, group: 'cards', atlas: 'cards-jia', type: 'bleed', + w: 1024, h: 1024, seed: 90100 + i, final: { w: 256, h: 256, fit: 'cover' }, + subject: `符卡插画:${CARD_SUBJECTS[id]}`, + })), + ...BEASTS.map((b, i) => ({ + id: `beast-${b.id}`, frame: b.id, group: 'beasts', atlas: `beasts-act${b.act}`, type: 'sprite', + w: 1024, h: 1024, seed: 91100 + i, final: { box: BEAST_BOX[b.tier], fit: 'contain' }, + tier: b.tier, subject: `上古异兽立绘,${b.subject}`, + })), + ...UI_SHOTS.map((u, i) => ({ + id: u.id, frame: u.id, group: 'ui', atlas: 'ui', type: 'bleed', + w: u.w, h: u.h, seed: 92100 + i, final: { w: u.fw, h: u.fh, fit: 'cover' }, + subject: u.subject, + })), + ...RELIC_IDS.map((id, i) => ({ + id: `relic-${id}`, frame: id, group: 'relics', atlas: 'relics', type: 'sprite', + w: 1024, h: 1024, seed: 93100 + i, final: { box: 128, fit: 'contain' }, + subject: RELIC_SUBJECTS[id], + })), + ...BG_SHOTS.map((g, i) => ({ + id: g.id, frame: g.id, group: 'bg', atlas: null, type: 'bleed', + w: g.w, h: g.h, seed: 94100 + i, final: { w: 768, h: 1360, fit: 'cover' }, + subject: g.subject, + })), +]; + +export const REUSE = { beasts: REUSE_BEASTS.map((b) => ({ ...b, box: BEAST_BOX[b.tier], atlas: `beasts-act${b.act}` })), bg: REUSE_BG }; diff --git a/tier2/games/shanhai-xunyilu/scripts/battle-core.test.mjs b/tier2/games/shanhai-xunyilu/scripts/battle-core.test.mjs index 20f889df..131b1ab7 100644 --- a/tier2/games/shanhai-xunyilu/scripts/battle-core.test.mjs +++ b/tier2/games/shanhai-xunyilu/scripts/battle-core.test.mjs @@ -30,9 +30,12 @@ console.log('[battle-core.test] 《山海巡异录》M0 单测'); // ── T1 表完整性 + 估值 sanity(估值为 advisory,只有超 hardCap 的离谱值才算失败)── { - // M2 定容(书 §5.2 职业甲 66;批产推进中间批按当前表数断言,唯一性/五行均衡精神不变)。 + // M3 定容(书 §5.2:154 = 职业专属 66×2 + 中立 22)。 const ids = new Set(CARDS.map((c) => c.id)); - assert(CARDS.length === 66 && ids.size === CARDS.length, `T1a M2 卡池定容 66 且 id 唯一(当前 ${CARDS.length})`); + const byClass = {}; + for (const c of CARDS) byClass[c.class] = (byClass[c.class] || 0) + 1; + assert(CARDS.length === 154 && ids.size === CARDS.length, `T1a M3 卡池定容 154 且 id 唯一(当前 ${CARDS.length})`); + assert(byClass.jia === 66 && byClass.yi === 66 && byClass.neutral === 22, 'T1a2 三池构成 66/66/22', JSON.stringify(byClass)); const perEl = {}; for (const c of CARDS) perEl[c.element] = (perEl[c.element] || 0) + 1; const counts = Object.values(perEl); diff --git a/tier2/games/shanhai-xunyilu/scripts/goldens.json b/tier2/games/shanhai-xunyilu/scripts/goldens.json index 1f612e27..e6e56cba 100644 --- a/tier2/games/shanhai-xunyilu/scripts/goldens.json +++ b/tier2/games/shanhai-xunyilu/scripts/goldens.json @@ -1,6 +1,6 @@ { "what": "金标构筑回归集(设计书 §6.4:20 套手工编排构筑,数值改动后重跑,胜率漂移 >±8pp 按套判报警)", - "note": "deck/relics 注入 run-core(opts.deck/opts.relics),贪心档打完整 run;baselineWinRate 已于 2026-07-06 S12 调参轮收口锁值(nightly 终跑 n=1000/套);漂移相对它按套判 ±8pp。满值套(100%)漂移只能单向向下,如实观测。套名=流派·变体;每流派 2-3 套。", + "note": "deck/relics 注入 run-core,贪心档打完整 run;基线口径(M3 定案):池扩张/职业/进阶入轴是预期漂移源,M3 实测:甲 20 套在 154 池+事件+起始遗物扩张下漂移 ≤3.6pp 全 PASS——基线连续(金标注入=已成型构筑,扩张影响被掩盖);乙 6 套 M3 首锁(nightly n=1000),漂移门 ±8pp 管里程碑内数值微调。满值套(100%)漂移只能单向向下,如实观测。", "nPerSuite": 1000, "seedBase": 260000, "suites": [ @@ -458,6 +458,149 @@ "yuheng-fu" ], "baselineWinRate": 0.483 + }, + { + "name": "乙·链循环·星轨嵌环", + "class": "yi", + "smokeSample": true, + "deck": [ + "yi-mu-luoteng", + "yi-huo-zhuming", + "yi-tu-jishi", + "yi-jin-shenshang", + "yi-shui-tianhan", + "yi-mu-sangzhe", + "yi-jin-xuanyuan", + "yi-shui-yinhe", + "yi-tu-bugang", + "yi-shui-beidou", + "yi-jin-baiying", + "yi-mu-tangong" + ], + "relics": [ + "hetu-juan", + "jia-wuxing-huan", + "tengshe-zhu" + ], + "baselineWinRate": 0.994 + }, + { + "name": "乙·灼流·荧惑燎原", + "class": "yi", + "deck": [ + "yi-huo-yinghuo", + "yi-huo-tanggu", + "yi-huo-yandi", + "yi-huo-zhurong2", + "yi-huo-liuhuo", + "yi-huo-huoling", + "yi-mu-ximu", + "yi-tu-wuse", + "yi-jin-changgeng", + "yi-shui-beidou", + "yi-shui-yaochi", + "yi-huo-zhulou" + ], + "relics": [ + "zhurong-guan", + "kunlun-yugui" + ], + "baselineWinRate": 0.991 + }, + { + "name": "乙·控异·弱水玄冥", + "class": "yi", + "deck": [ + "yi-shui-ruoshui2", + "yi-shui-xuanming2", + "yi-jin-zhongshan", + "yi-tu-xiushe", + "yi-shui-beiming2", + "yi-jin-rushou2", + "yi-tu-wuse", + "yi-mu-ximu", + "yi-huo-zhuming", + "yi-shui-beidou", + "yi-tu-jishi", + "yi-shui-yaochi" + ], + "relics": [ + "qingluan-ling", + "baize-ce", + "yinglong-lin" + ], + "baselineWinRate": 0.19 + }, + { + "name": "乙·周天环·倒卷帘", + "class": "yi", + "smokeSample": true, + "deck": [ + "yi-mu-luoteng", + "yi-huo-zhulou", + "yi-tu-bugang", + "yi-jin-jinsuo", + "yi-shui-jianglou", + "yi-mu-sangzhe", + "yi-huo-liuhuo", + "yi-tu-jishi", + "yi-jin-shenshang", + "yi-shui-zhoutian", + "yi-shui-tianhan", + "yi-mu-taiyi" + ], + "relics": [ + "jia-zhoutian-lu", + "luoshu-ye", + "hetu-juan", + "jia-lingxi" + ], + "baselineWinRate": 0.955 + }, + { + "name": "乙·韧甲·岱舆孤峙", + "class": "yi", + "deck": [ + "yi-tu-wuse", + "yi-tu-xuanyin", + "yi-tu-lianshan", + "yi-tu-butian", + "yi-tu-guitian", + "yi-tu-daiyu", + "yi-mu-linggen2", + "yi-shui-xuangui2", + "yi-jin-yaoguang", + "yi-mu-ximu", + "yi-huo-zhuming", + "yi-shui-yaochi" + ], + "relics": [ + "luwu-zhao", + "houtu-yin", + "yinglong-lin" + ], + "baselineWinRate": 0.948 + }, + { + "name": "乙·基准锚·起始组原样", + "class": "yi", + "smokeSample": true, + "deck": [ + "yi-mu-luoteng", + "yi-mu-ximu", + "yi-huo-zhuming", + "yi-huo-zhulou", + "yi-tu-wuse", + "yi-tu-jishi", + "yi-jin-changgeng", + "yi-jin-zhongshan", + "yi-shui-beidou", + "yi-shui-yaochi" + ], + "relics": [ + "hetu-juan" + ], + "baselineWinRate": 0.525 } ] } diff --git a/tier2/games/shanhai-xunyilu/scripts/meta.test.mjs b/tier2/games/shanhai-xunyilu/scripts/meta.test.mjs new file mode 100644 index 00000000..181d7659 --- /dev/null +++ b/tier2/games/shanhai-xunyilu/scripts/meta.test.mjs @@ -0,0 +1,109 @@ +/** + * scripts/meta.test.mjs —— 《山海巡异录》局外核单测(M3 S15:图鉴/成就/解锁/每日) + * owner:北极星顶级线③(M3) + * + * 【验什么】W1 图鉴收集幂等;W2 成就条件词表全覆盖(表内 kind ⊆ 解释器认知集——新增词漏接=红); + * W3 终局结算(计数/进阶解锁/成就达成);W4 每日(种子=日期派生/分数公式/最佳分); + * W5 序列化往返;W6 成就去重(达成不重复)。 + * + * 用法:node scripts/meta.test.mjs(退出码 0=全过) + */ + +'use strict'; + +import { createMetaCore } from '../src/meta-core.js'; +import { ACHIEVEMENTS, DAILY } from '../src/data/tables.js'; + +let failures = 0; +function assert(cond, name, detail) { + if (cond) console.log(` PASS ${name}`); + else { console.error(` FAIL ${name}` + (detail ? ` — ${detail}` : '')); failures++; } +} + +console.log('[meta.test] 《山海巡异录》M3 局外核单测'); + +/** 终局投影桩(recordRunEnd 消费面;字段与 run-core readState 对齐)。 */ +function endState(over = {}) { + return { + phase: 'win', heroClass: 'jia', ascension: 0, deckCount: 18, gold: 120, + hero: { hp: 40, hpMax: 72 }, + relics: [{ id: 'r1' }, { id: 'r2' }], + stats: { battlesWon: 14, turnsTotal: 90, cardsRemoved: 2, zhoutianTotal: 1 }, + act: 3, + ...over, + }; +} + +// ── W1 图鉴收集幂等 ── +{ + const m = createMetaCore(null); + const rs = { deck: ['a', 'b', 'a'], relics: [{ id: 'r1' }], battle: { enemy: { id: 'e1' } } }; + m.observe(rs); m.observe(rs); m.observe(rs); + const r = m.readMeta(); + assert(r.collection.cards === 2 && r.collection.relics === 1 && r.collection.enemies === 1, 'W1 三面收集幂等(2 卡/1 遗物/1 敌)', JSON.stringify(r.collection)); +} + +// ── W2 成就词表全覆盖(表内 kind ⊆ 解释器;新词漏接即红)── +{ + const KNOWN = new Set(['wins', 'runs', 'winClass', 'winBoth', 'winAscension', 'deckMaxAtWin', 'deckMinAtWin', + 'hpFullWin', 'goldAtWin', 'removeTotal', 'zhoutianRun', 'relicsAtWin', 'winTurnsUnder', + 'collectCards', 'collectRelics', 'collectEnemies', 'dailyPlayed', 'dailyScore', 'ascUnlocked']); + const unknown = ACHIEVEMENTS.filter((a) => !KNOWN.has(a.cond.kind)).map((a) => `${a.id}:${a.cond.kind}`); + assert(unknown.length === 0, `W2 成就 kind 词表全覆盖(表 ${ACHIEVEMENTS.length} 条)`, unknown.join(',')); + assert(ACHIEVEMENTS.length === 40, 'W2b 成就定容 40(书 §5.2)', String(ACHIEVEMENTS.length)); +} + +// ── W3 终局结算:计数/解锁/成就 ── +{ + const m = createMetaCore(null); + const r1 = m.recordRunEnd(endState()); + const meta1 = m.readMeta(); + assert(meta1.runsPlayed === 1 && meta1.wins === 1 && meta1.winsByClass.jia === 1, 'W3a 胜局计数'); + assert(meta1.ascensionUnlocked.jia === 1, 'W3b 通关 A0 解锁 A1(逐职业)', JSON.stringify(meta1.ascensionUnlocked)); + assert(r1.newlyAchieved.includes('ach-first-win') && r1.newlyAchieved.includes('ach-win-jia'), 'W3c 首胜成就达成', r1.newlyAchieved.join(',')); + // 进阶通关跳级解锁:A5 通关 → 解锁 A6。 + m.recordRunEnd(endState({ ascension: 5 })); + assert(m.readMeta().ascensionUnlocked.jia === 6, 'W3d A5 通关解锁 A6'); + // 败局:只计局数。 + const before = m.readMeta().wins; + m.recordRunEnd(endState({ phase: 'lose' })); + assert(m.readMeta().wins === before && m.readMeta().runsPlayed === 3, 'W3e 败局不计胜场'); + // 薄卡组成就。 + const r4 = m.recordRunEnd(endState({ deckCount: 11 })); + assert(r4.newlyAchieved.includes('ach-thin-deck'), 'W3f 轻装简从(≤12 张通关)'); +} + +// ── W4 每日:种子派生/分数公式/最佳分 ── +{ + const m = createMetaCore(null); + const d = m.dailySeedFor('2026-07-06'); + assert(d.seed === 20260706, 'W4a 每日种子=日期数字', String(d.seed)); + const win = endState({ gold: 100 }); + const { dailyScore } = m.recordRunEnd(win, { isDaily: true }); + const expect = DAILY.winBase + win.hero.hp * DAILY.hpWeight + win.gold * DAILY.goldWeight - win.stats.turnsTotal * DAILY.turnPenalty; + assert(dailyScore === expect, 'W4b 胜局分数=表公式', `${dailyScore} vs ${expect}`); + const low = m.recordRunEnd(endState({ phase: 'lose', act: 2 }), { isDaily: true }); + assert(m.readMeta().daily.bestScore === expect && low.dailyScore < expect, 'W4c 最佳分保持(低分不覆盖)'); + const d2 = m.dailySeedFor('2026-07-07'); + assert(d2.seed === 20260707 && m.readMeta().daily.bestScore === 0 && !m.readMeta().daily.played, 'W4d 跨日重置(新题面)'); +} + +// ── W5 序列化往返 ── +{ + const m = createMetaCore(null); + m.observe({ deck: ['x'], relics: [{ id: 'r' }], battle: { enemy: { id: 'e' } } }); + m.recordRunEnd(endState()); + const back = createMetaCore(JSON.parse(JSON.stringify(m.serialize()))); + assert(JSON.stringify(back.serialize()) === JSON.stringify(m.serialize()), 'W5 序列化往返逐字段一致'); +} + +// ── W6 成就去重 ── +{ + const m = createMetaCore(null); + const a = m.recordRunEnd(endState()); + const b = m.recordRunEnd(endState()); + assert(a.newlyAchieved.includes('ach-first-win') && !b.newlyAchieved.includes('ach-first-win'), 'W6 已达成就不重复上报'); +} + +console.log(failures === 0 ? '[meta.test] 全绿' : `[meta.test] ${failures} 条失败`); +process.exit(failures === 0 ? 0 : 1); diff --git a/tier2/games/shanhai-xunyilu/scripts/run-core.test.mjs b/tier2/games/shanhai-xunyilu/scripts/run-core.test.mjs index a605b9a2..75fc62b5 100644 --- a/tier2/games/shanhai-xunyilu/scripts/run-core.test.mjs +++ b/tier2/games/shanhai-xunyilu/scripts/run-core.test.mjs @@ -133,13 +133,15 @@ console.log('[run-core.test] 《山海巡异录》M1 单测'); fightThrough(run); const afterElite = run.readState(); if (afterElite.phase === 'run') { - // M2 S10:发放改受控随机抽池(池序=M1 过渡形态),断言口径=「获得一枚池内遗物」。 + // M2 S10 随机抽池 + M3 起始遗物(职业身份件占 1 基数):断言按「相对基数 +1」口径。 const relicIds = new Set(RELICS.map((r) => r.id)); - assert(afterElite.relics.length === 1 && relicIds.has(afterElite.relics[0].id), 'R7c 精英必掉遗物(随机抽池)', JSON.stringify(afterElite.relics)); + const base = 1; // CLASSES.starterRelic(M3 平衡轮) + assert(afterElite.relics.length === base + 1 && afterElite.relics.every((r) => relicIds.has(r.id)), 'R7c 精英必掉遗物(随机抽池;起始+1)', JSON.stringify(afterElite.relics)); run.chooseReward('skip'); run.chooseNode('a1-f3-n0'); // 宝匣 const s3 = run.readState(); - assert(s3.relics.length === 2 && relicIds.has(s3.relics[1].id) && s3.relics[1].id !== s3.relics[0].id, + const ids3 = s3.relics.map((r) => r.id); + assert(s3.relics.length === base + 2 && new Set(ids3).size === ids3.length, 'R7d 宝匣再发一枚未持有遗物(互异)', JSON.stringify(s3.relics)); } else { assert(false, 'R7c 精英战贪心竟败(数值曲线要看)', `phase=${afterElite.phase} hp=${afterElite.hero.hp}`); @@ -241,6 +243,7 @@ console.log('[run-core.test] 《山海巡异录》M1 单测'); } else if (s.screen === 'reward') run.chooseReward(0); else if (s.screen === 'relicReward') run.chooseRelicReward(0); // M2 Boss 遗物三选一 else if (s.screen === 'shop') run.leaveShop(); // M2 商店(确定性脚本:径直离开) + else if (s.screen === 'event') run.chooseEventOption(0); // M3 事件(确定性脚本:取头一项) } return JSON.stringify(run.readState()); }; @@ -387,5 +390,42 @@ console.log('[run-core.test] 《山海巡异录》M1 单测'); assert(threw, 'R14d 未知遗物注入响亮抛(表一致性)'); } +// ── R15 职业与进阶消费(M3 S13/S14)── +{ + // 乙职业:HP/起始组/起始遗物/池过滤。 + const yi = createRunCore({ random: createSeededRandom(151) }, { seed: 151, heroClass: 'yi' }); + const sy = yi.readState(); + assert(sy.heroClass === 'yi' && sy.hero.hpMax === 70, 'R15a 乙职业 HP=70(平衡轮回填值)', JSON.stringify(sy.hero)); + assert(sy.deck.every((id) => id.startsWith('yi-')) && sy.deckCount === 10, 'R15b 乙起始组全乙池 10 张'); + assert(sy.relics.length === 1 && sy.relics[0].id === 'hetu-juan', 'R15c 乙起始遗物=河图残卷'); + // 甲缺省与起始遗物。 + const jia = createRunCore({ random: createSeededRandom(152) }, { seed: 152 }); + assert(jia.readState().heroClass === 'jia' && jia.readState().relics[0].id === 'kaiming-ya', 'R15d 甲缺省+开明兽牙'); + // 进阶 A6:起始 HP -6;A3+A10 敌血乘区。 + const a6 = createRunCore({ random: createSeededRandom(153) }, { seed: 153, ascension: 6 }); + assert(a6.readState().hero.hpMax === 72 - 6 && a6.readState().ascension === 6, 'R15e A6 起始 HP 72-6', String(a6.readState().hero.hpMax)); + // A15 奖励二选一(rewardChoicesDelta)。 + const a15 = createRunCore( + { random: createSeededRandom(154) }, + { seed: 154, ascension: 15, actMaps: [miniMap(1), miniMap(2), miniMap(3)] }, + ); + a15.chooseNode('a1-f0-n0'); + fightThrough(a15); + if (a15.readState().phase === 'run' && a15.readState().screen === 'reward') { + assert(a15.readState().reward.options.length === 2, 'R15f A15 战斗奖励二选一(构筑面收紧轴)', String(a15.readState().reward.options.length)); + } else { + assert(false, 'R15f 前置:A15 教学战竟败', a15.readState().phase); + } + // 进阶敌血乘区(A3+A10=11%,幕1 普通敌不吃幕缩放只吃进阶)。 + const a10 = createRunCore( + { random: createSeededRandom(155) }, + { seed: 155, ascension: 10, actMaps: [miniMap(1), miniMap(2), miniMap(3)] }, + ); + a10.chooseNode('a1-f0-n0'); + const enemyHp = a10.readState().battle.enemy.hpMax; + const kuze = ENEMIES.find((e) => e.id === 'kuze-jiao'); + assert(enemyHp === Math.round(kuze.hpMax * 1.11), 'R15g A10 敌血=表值×1.11(A3 7%+A10 4%)', `${enemyHp} vs ${Math.round(kuze.hpMax * 1.11)}`); +} + console.log(failures === 0 ? '[run-core.test] 全绿' : `[run-core.test] ${failures} 条失败`); process.exit(failures === 0 ? 0 : 1); diff --git a/tier2/games/shanhai-xunyilu/scripts/run-smoke.mjs b/tier2/games/shanhai-xunyilu/scripts/run-smoke.mjs index 774428cc..be50fc17 100644 --- a/tier2/games/shanhai-xunyilu/scripts/run-smoke.mjs +++ b/tier2/games/shanhai-xunyilu/scripts/run-smoke.mjs @@ -105,6 +105,7 @@ function driveRun(seed, opts = {}) { else if (t.kind === 'removePick') run.buyRemove(t.index); else if (t.kind === 'removeBack') run.closeRemove(); else if (t.kind === 'shopLeave') run.leaveShop(); + else if (t.kind === 'eventOption') run.chooseEventOption(t.index); // M3 事件 else break; // 未知靶=契约漂移,响亮暴露(勿静默空转) // 事件收集(首战演出估算)。 if (inFirstBattle) { diff --git a/tier2/games/shanhai-xunyilu/scripts/save.test.mjs b/tier2/games/shanhai-xunyilu/scripts/save.test.mjs index b0025934..710b0e4b 100644 --- a/tier2/games/shanhai-xunyilu/scripts/save.test.mjs +++ b/tier2/games/shanhai-xunyilu/scripts/save.test.mjs @@ -1,15 +1,13 @@ /** - * scripts/save.test.mjs —— 《山海巡异录》run 存档断言(M2 S11 验收门) - * owner:北极星顶级线③(M2) + * scripts/save.test.mjs —— 《山海巡异录》存档断言(M2 S11 v1 → M3 S15 v2 双域信封) + * owner:北极星顶级线③(M2/M3) * - * 【验什么】书 §3.4「存档续玩」断言 + 工单口径(往返 + 坏版本负例)+ 契约容量闸: - * V1 信封形状=SaveEnvelope(schemaVersion 整数 ≥1 + data 纯对象); - * V2 往返逐字段一致(snapshot→restore→readState 深等,map/deck/relics/HP/金币/removeCount/种子); - * V3 续玩确定性(恢复后与不中断,同策略打到终态逐字段一致——rngState 精确续的实证); - * V4 坏版本负例(0/2/'1'/缺 data/data 数组/裸对象/数组信封 → 全拒,fail-closed); - * V5 容量核数(信封 JSON < 65536 码元,save: 族契约闸;取整局多快照峰值); - * V6 战斗屏无快照(书:战斗外任意节点;battle 屏 snapshot()=null); - * V7 三屏快照可恢复(reward/relicReward/shop 屏各自往返)。 + * 【验什么】书 §3.4「存档续玩」断言 + 工单口径(往返 + 坏版本负例)+ 契约容量闸 + v2 增列: + * V1 信封形状=SaveEnvelope(schemaVersion 整数 ≥1 + data 纯对象;v2 双域 {run,meta}); + * V2 run 域往返逐字段一致;V3 续玩确定性(恢复续打=不中断直打); + * V4 坏档负例全拒(fail-closed);V5 容量核数(<65536 码元);V6 战斗屏无 run 快照; + * V7 三屏快照可恢复;V8 v1→v2 迁移(M2 旧档平铺 → run 域,meta 空); + * V9 meta 域独立性(meta 坏形只弃 meta,run 仍恢复)。 * * 用法:node scripts/save.test.mjs(退出码 0=全过) */ @@ -21,8 +19,9 @@ import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { createRunCore } from '../src/run-core.js'; +import { createMetaCore } from '../src/meta-core.js'; import { createSeededRandom } from '../src/util/seeded-random.js'; -import { makeEnvelope, validateEnvelope, restoreRun, serializeEnvelope } from '../src/save.js'; +import { makeEnvelope, validateEnvelope, restoreRun, restoreMeta, serializeEnvelope } from '../src/save.js'; import { SAVE } from '../src/data/tables.js'; const __dir = dirname(fileURLToPath(import.meta.url)); @@ -35,7 +34,29 @@ function assert(cond, name, detail) { else { console.error(` FAIL ${name}` + (detail ? ` — ${detail}` : '')); failures++; } } -/** 用贪心策略推进一局;stopAt(state)=true 时停(返回是否到达);分发词表与 sim-run 同。 */ +/** 分发词表(与 sim-run 同表)。 */ +function act(run, t) { + if (t.kind === 'card') run.tapCard(t.index); + else if (t.kind === 'enemy') run.confirmPlay(); + else if (t.kind === 'endTurn') run.endTurn(); + else if (t.kind === 'cancel') run.clearSelection(); + else if (t.kind === 'node') run.chooseNode(t.id); + else if (t.kind === 'reward') run.chooseReward(t.index); + else if (t.kind === 'rewardSkip') run.chooseReward('skip'); + else if (t.kind === 'relicReward') run.chooseRelicReward(t.index); + else if (t.kind === 'relicSkip') run.chooseRelicReward('skip'); + else if (t.kind === 'shopCard') run.buyCard(t.index); + else if (t.kind === 'shopRelic') run.buyRelic(t.index); + else if (t.kind === 'shopRemove') run.openRemove(); + else if (t.kind === 'removePick') run.buyRemove(t.index); + else if (t.kind === 'removeBack') run.closeRemove(); + else if (t.kind === 'shopLeave') run.leaveShop(); + else if (t.kind === 'eventOption') run.chooseEventOption(t.index); + else return false; + return true; +} + +/** 用贪心推进;stopAt(state)=true 时停。 */ function drive(run, stopAt, guard = 20000) { for (let i = 0; i < guard; i++) { const s = run.readState(); @@ -43,156 +64,150 @@ function drive(run, stopAt, guard = 20000) { if (stopAt && stopAt(s)) return true; const t = chooseTap(s); if (!t) return false; - if (t.kind === 'card') run.tapCard(t.index); - else if (t.kind === 'enemy') run.confirmPlay(); - else if (t.kind === 'endTurn') run.endTurn(); - else if (t.kind === 'cancel') run.clearSelection(); - else if (t.kind === 'node') run.chooseNode(t.id); - else if (t.kind === 'reward') run.chooseReward(t.index); - else if (t.kind === 'rewardSkip') run.chooseReward('skip'); - else if (t.kind === 'relicReward') run.chooseRelicReward(t.index); - else if (t.kind === 'relicSkip') run.chooseRelicReward('skip'); - else if (t.kind === 'shopCard') run.buyCard(t.index); - else if (t.kind === 'shopRelic') run.buyRelic(t.index); - else if (t.kind === 'shopRemove') run.openRemove(); - else if (t.kind === 'removePick') run.buyRemove(t.index); - else if (t.kind === 'removeBack') run.closeRemove(); - else if (t.kind === 'shopLeave') run.leaveShop(); - else return false; + if (!act(run, t)) return false; } return false; } -/** 打到终态并返回终态投影(V3 用;经 JSON 归一)。 */ function driveToEnd(run) { drive(run, null); return JSON.stringify(run.readState()); } -console.log('[save.test] 《山海巡异录》M2 存档断言(SaveEnvelope 契约面)'); +console.log('[save.test] 《山海巡异录》存档断言(SaveEnvelope v2 双域)'); -// ── V1/V2 信封形状 + 往返逐字段一致(map 屏首快照)── +// ── V1/V2 信封形状 + run 域往返 ── { const run = createRunCore({ random: createSeededRandom(777) }, { seed: 777 }); - // 推进到第二次回到 map 屏(打完教学战、领完奖励),快照内容更饱满。 drive(run, (s) => s.screen === 'map' && s.stats.battlesWon >= 1); - const env = makeEnvelope(run); - assert(env && Number.isInteger(env.schemaVersion) && env.schemaVersion >= 1, 'V1a 信封 schemaVersion 整数 ≥1', JSON.stringify(env && env.schemaVersion)); + const meta = createMetaCore(null); + meta.observe(run.readState()); + const env = makeEnvelope(run, meta); + assert(env && Number.isInteger(env.schemaVersion) && env.schemaVersion >= 1, 'V1a 信封 schemaVersion 整数 ≥1'); assert(env && typeof env.data === 'object' && !Array.isArray(env.data), 'V1b data 为纯对象(非数组)'); - assert(env.schemaVersion === SAVE.schemaVersion, 'V1c 版本=当前表定版本'); + assert(env.schemaVersion === SAVE.schemaVersion && env.schemaVersion === 2, 'V1c 版本=当前表定版本(v2)'); + assert(env.data.run && env.data.meta, 'V1d 双域俱在(run+meta)'); - const restored = restoreRun(JSON.parse(JSON.stringify(env))); // 经 JSON 往返(介质等价) - assert(!!restored, 'V2a 恢复成功'); - const a = run.readState(); - const b = restored.readState(); - assert(JSON.stringify(a) === JSON.stringify(b), 'V2b 往返 readState 逐字段一致(map/deck/relics/HP/金币/靶位全量)'); - assert(b.seed === 777 && b.removeCount === a.removeCount, 'V2c 种子与删卡计数还原', `seed=${b.seed}`); + const restored = restoreRun(JSON.parse(JSON.stringify(env))); + assert(!!restored, 'V2a run 域恢复成功'); + assert(JSON.stringify(run.readState()) === JSON.stringify(restored.readState()), 'V2b 往返 readState 逐字段一致'); + const metaBack = restoreMeta(JSON.parse(JSON.stringify(env))); + assert(metaBack && JSON.stringify(createMetaCore(metaBack).serialize()) === JSON.stringify(meta.serialize()), 'V2c meta 域往返一致'); } -// ── V3 续玩确定性:快照点分叉,「中断恢复续打」与「不中断直打」终态逐字段一致 ── +// ── V3 续玩确定性 ×3 seed(职业乙+进阶也覆盖:恢复续打=不中断直打)── { - const seedList = [301, 302, 303]; - let allSame = true; - let detail = ''; - for (const sd of seedList) { - const runA = createRunCore({ random: createSeededRandom(sd) }, { seed: sd }); + const cases = [[301, {}], [302, { heroClass: 'yi' }], [303, { ascension: 3 }]]; + let allSame = true; let detail = ''; + for (const [sd, opts] of cases) { + const runA = createRunCore({ random: createSeededRandom(sd) }, { seed: sd, ...opts }); drive(runA, (s) => s.screen === 'map' && s.stats.battlesWon >= 2); - const env = makeEnvelope(runA); - if (!env) { allSame = false; detail = `seed=${sd} 无快照`; break; } - const endA = driveToEnd(runA); // 不中断:直接打完 - const runB = restoreRun(JSON.parse(JSON.stringify(env))); // 中断恢复 + const env = makeEnvelope(runA, null); + if (!env.data.run) { allSame = false; detail = `seed=${sd} 无快照`; break; } + const endA = driveToEnd(runA); + const runB = restoreRun(JSON.parse(JSON.stringify(env))); const endB = driveToEnd(runB); - if (endA !== endB) { allSame = false; detail = `seed=${sd} 终态漂移`; break; } + if (endA !== endB) { allSame = false; detail = `seed=${sd} ${JSON.stringify(opts)} 终态漂移`; break; } } - assert(allSame, 'V3 续玩确定性 ×3 seed(恢复续打 = 不中断直打,终态逐字段一致)', detail); + assert(allSame, 'V3 续玩确定性 ×3(甲/乙/A3;恢复续打=不中断直打)', detail); } -// ── V4 坏版本负例(fail-closed:全拒)── +// ── V4 坏档负例(fail-closed 全拒;版本 1 是合法迁移不在此列)── { const run = createRunCore({ random: createSeededRandom(88) }, { seed: 88 }); - const good = makeEnvelope(run); + const good = makeEnvelope(run, null); const cases = [ ['版本 0', { ...good, schemaVersion: 0 }], - ['版本 2(未来版)', { ...good, schemaVersion: 2 }], - ['版本字符串', { ...good, schemaVersion: '1' }], - ['缺 data', { schemaVersion: 1 }], - ['data 为数组', { schemaVersion: 1, data: [1, 2] }], + ['版本 3(未来版)', { ...good, schemaVersion: 3 }], + ['版本字符串', { ...good, schemaVersion: '2' }], + ['缺 data', { schemaVersion: 2 }], + ['data 为数组', { schemaVersion: 2, data: [1, 2] }], ['信封为数组', [good]], - ['裸 data(无信封)', good.data], + ['run 域为数组', { schemaVersion: 2, data: { run: [1], meta: null } }], + ['run 域缺 rngState', { schemaVersion: 2, data: { run: { map: {} }, meta: null } }], ['null', null], - ['data.rngState 缺失', { schemaVersion: 1, data: { ...good.data, rngState: undefined } }], ]; - let allRejected = true; - let leak = ''; + let allRejected = true; let leak = ''; for (const [name, env] of cases) { - if (validateEnvelope(env) !== null || restoreRun(env) !== null) { allRejected = false; leak = name; break; } + const v = validateEnvelope(env); + const rejected = v === null || (v.run === null && name.startsWith('run 域') === false && false); + if (!(v === null) || restoreRun(env) !== null) { allRejected = false; leak = name; break; } } assert(allRejected, `V4 坏档负例 ×${cases.length} 全拒(fail-closed,坏档=无档)`, leak && `漏放:${leak}`); } -// ── V5 容量核数(save: 族 65536 码元;整局路径上的快照峰值)── +// ── V5 容量核数(双域峰值)── { const run = createRunCore({ random: createSeededRandom(505) }, { seed: 505 }); + const meta = createMetaCore(null); let peak = 0; for (let i = 0; i < 20000; i++) { const s = run.readState(); if (s.phase !== 'run') break; - const env = makeEnvelope(run); - if (env) { - const ser = serializeEnvelope(env); + meta.observe(s); + if (s.screen !== 'battle') { + const ser = serializeEnvelope(makeEnvelope(run, meta)); if (!ser.ok) { peak = ser.chars; break; } if (ser.chars > peak) peak = ser.chars; } const t = chooseTap(s); - if (!t) break; - if (t.kind === 'card') run.tapCard(t.index); - else if (t.kind === 'enemy') run.confirmPlay(); - else if (t.kind === 'endTurn') run.endTurn(); - else if (t.kind === 'node') run.chooseNode(t.id); - else if (t.kind === 'reward') run.chooseReward(t.index); - else if (t.kind === 'rewardSkip') run.chooseReward('skip'); - else if (t.kind === 'relicReward') run.chooseRelicReward(t.index); - else if (t.kind === 'relicSkip') run.chooseRelicReward('skip'); - else if (t.kind === 'shopCard') run.buyCard(t.index); - else if (t.kind === 'shopRelic') run.buyRelic(t.index); - else if (t.kind === 'shopRemove') run.openRemove(); - else if (t.kind === 'removePick') run.buyRemove(t.index); - else if (t.kind === 'removeBack') run.closeRemove(); - else if (t.kind === 'shopLeave') run.leaveShop(); - else break; + if (!t || !act(run, t)) break; } - assert(peak > 0 && peak < SAVE.maxChars, `V5 快照峰值 ${peak} 码元 < ${SAVE.maxChars}(契约闸;余量 ${Math.round((1 - peak / SAVE.maxChars) * 100)}%)`); + assert(peak > 0 && peak < SAVE.maxChars, `V5 双域快照峰值 ${peak} 码元 < ${SAVE.maxChars}(余量 ${Math.round((1 - peak / SAVE.maxChars) * 100)}%)`); } -// ── V6 战斗屏无快照 ── +// ── V6 战斗屏无 run 快照(信封 run 域=null)── { const run = createRunCore({ random: createSeededRandom(66) }, { seed: 66 }); - run.chooseNode(run.readState().map.selectable[0]); // 进教学首战 - assert(run.readState().screen === 'battle' && run.snapshot() === null, 'V6 battle 屏 snapshot()=null(战斗外任意节点口径)'); + run.chooseNode(run.readState().map.selectable[0]); + const env = makeEnvelope(run, null); + assert(run.readState().screen === 'battle' && env.data.run === null, 'V6 battle 屏 run 域=null(战斗外任意节点口径)'); } -// ── V7 三屏快照可恢复(reward/shop/relicReward 各一)── +// ── V7 多屏快照可恢复(reward/shop/relicReward/event)── { const stops = [ ['reward', (s) => s.screen === 'reward'], ['shop', (s) => s.screen === 'shop'], ['relicReward', (s) => s.screen === 'relicReward'], + ['event', (s) => s.screen === 'event'], ]; for (const [name, stop] of stops) { let hit = false; - // 多 seed 找到能停在该屏的局(贪心路径依赖 seed;找不到=断言失败,响亮暴露)。 - for (let sd = 900; sd < 940 && !hit; sd++) { + for (let sd = 900; sd < 960 && !hit; sd++) { const run = createRunCore({ random: createSeededRandom(sd) }, { seed: sd }); if (!drive(run, stop)) continue; hit = true; - const env = makeEnvelope(run); - const back = env && restoreRun(JSON.parse(JSON.stringify(env))); + const env = makeEnvelope(run, null); + const back = env.data.run && restoreRun(JSON.parse(JSON.stringify(env))); const same = back && JSON.stringify(run.readState()) === JSON.stringify(back.readState()); assert(!!same, `V7 ${name} 屏快照往返一致(seed=${sd})`); } - if (!hit) assert(false, `V7 ${name} 屏 40 seed 内未达(路径覆盖缺口)`); + if (!hit) assert(false, `V7 ${name} 屏 60 seed 内未达(路径覆盖缺口)`); } } +// ── V8 v1→v2 迁移(M2 旧档=data 平铺 → run 域;meta 空)── +{ + const run = createRunCore({ random: createSeededRandom(112) }, { seed: 112 }); + drive(run, (s) => s.screen === 'map' && s.stats.battlesWon >= 1); + const v1 = { schemaVersion: 1, data: run.snapshot() }; // M2 v1 形状:平铺快照 + const v = validateEnvelope(v1); + assert(v && v.run && v.run.rngState != null && v.meta === null, 'V8a v1 迁移:平铺 → run 域,meta 空'); + const back = restoreRun(v1); + assert(back && JSON.stringify(back.readState()) === JSON.stringify(run.readState()), 'V8b v1 旧档恢复续玩(迁移链真通)'); +} + +// ── V9 meta 域独立性(meta 坏形只弃 meta,run 仍恢复)── +{ + const run = createRunCore({ random: createSeededRandom(113) }, { seed: 113 }); + drive(run, (s) => s.screen === 'map' && s.stats.battlesWon >= 1); + const env = makeEnvelope(run, null); + env.data.meta = [1, 2, 3]; // 坏形 meta(数组) + const v = validateEnvelope(env); + assert(v && v.run && v.meta === null, 'V9a meta 坏形被弃,run 域保留'); + assert(restoreRun(env) !== null && restoreMeta(env) === null, 'V9b run 恢复成功且 meta 返 null(独立 fail-closed)'); +} + console.log(failures === 0 ? '[save.test] 全绿' : `[save.test] ${failures} 条失败`); process.exit(failures === 0 ? 0 : 1); diff --git a/tier2/games/shanhai-xunyilu/scripts/scene-smoke.mjs b/tier2/games/shanhai-xunyilu/scripts/scene-smoke.mjs new file mode 100644 index 00000000..975c6479 --- /dev/null +++ b/tier2/games/shanhai-xunyilu/scripts/scene-smoke.mjs @@ -0,0 +1,174 @@ +/** + * scripts/scene-smoke.mjs —— 《山海巡异录》渲染层冒烟(mock 场景重放;2026-07-06 战斗全空事故防回潮门) + * owner:北极星顶级线③(M2 增补) + * + * 【为什么有这门】战斗全空事故:run-scene 的 `const txt` 声明位于工厂 return 之后 → TDZ 死绑定 → + * 首次 rebuild 即抛 ReferenceError → 地图/战斗/商店整屏绘制全部无声失败,只有事件浮字可见。 + * headless 门(ctx.game=null)不建场景,场景代码零执行——绘制路径的异常只有真浏览器暴露。 + * 本门用「记录型 mock scene」把场景 create/update 在 node 里真执行:七屏全驱动、异常零容忍、 + * 每屏绘制调用计数下限、自证钩子(api.debug 回写)断言。mock 覆盖不了 Phaser 内部渲染行为 + * (那是 CDP 像素/displayCount 复验的事),但「我们代码在绘制路径上的任何异常」在此现形。 + * + * 用法:node scripts/scene-smoke.mjs(退出码 0=全过) + */ + +'use strict'; + +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +import { createRunCore } from '../src/run-core.js'; +import { createSeededRandom } from '../src/util/seeded-random.js'; +import { createRunScene } from '../src/scenes/run-scene.js'; +import { hitTarget } from '../src/ui/layout.js'; + +const __dir = dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); +const { chooseTap } = require(join(__dir, '../../../harness/card-battle.driver.js')); + +let failures = 0; +function assert(cond, name, detail) { + if (cond) console.log(` PASS ${name}` + (detail ? ` — ${detail}` : '')); + else { console.error(` FAIL ${name}` + (detail ? ` — ${detail}` : '')); failures++; } +} + +/** 记录型 mock scene:add.* 全记数;未知链式方法一律 no-op(mock 面窄于真 Phaser,只抓我们代码的异常)。 */ +function makeMockScene() { + const counts = { adds: 0, destroyed: 0 }; + const mkChain = () => new Proxy({}, { + get: (t, k) => { + if (k === 'destroy') return () => { counts.destroyed++; }; + return (...a) => mkChained; // 任意方法 → 链式返回同代理 + }, + }); + // 链式代理单例(所有方法都返回它自己,destroy 记数)。 + const mkChained = new Proxy({}, { + get: (t, k) => { + if (k === 'destroy') return () => { counts.destroyed++; }; + return () => mkChained; + }, + }); + const addObj = () => { counts.adds++; return mkChained; }; + return { + counts, + add: { text: addObj, circle: addObj, rectangle: addObj, graphics: addObj, container: addObj }, + time: { delayedCall: () => {} }, // 演出排程不执行(演出面归 CDP) + tweens: { add: () => {} }, + cameras: { main: { setBackgroundColor: () => {}, shake: () => {} } }, + input: { on: () => {} }, + }; +} + +console.log('[scene-smoke] 《山海巡异录》渲染层冒烟(mock 场景重放)'); + +const seed = 424242; +const run = createRunCore({ random: createSeededRandom(seed) }, { seed }); +const debug = { displayCount: 0, rebuilds: 0, screen: null }; +const sceneObj = createRunScene({ + run: () => run, + debug, + route: (x, y) => { + const t = hitTarget(run.readState().tapTargets, x, y); + if (!t) return; + if (t.kind === 'card') run.tapCard(t.index); + else if (t.kind === 'enemy') run.confirmPlay(); + else if (t.kind === 'endTurn') run.endTurn(); + else if (t.kind === 'node') run.chooseNode(t.id); + else if (t.kind === 'reward') run.chooseReward(t.index); + else if (t.kind === 'rewardSkip') run.chooseReward('skip'); + else if (t.kind === 'relicReward') run.chooseRelicReward(t.index); + else if (t.kind === 'relicSkip') run.chooseRelicReward('skip'); + else if (t.kind === 'shopCard') run.buyCard(t.index); + else if (t.kind === 'shopRelic') run.buyRelic(t.index); + else if (t.kind === 'shopRemove') run.openRemove(); + else if (t.kind === 'removePick') run.buyRemove(t.index); + else if (t.kind === 'removeBack') run.closeRemove(); + else if (t.kind === 'shopLeave') run.leaveShop(); + else if (t.kind === 'eventOption') run.chooseEventOption(t.index); // M3 事件 + }, +}); +const scene = makeMockScene(); + +/** 推 n 帧 update;抛异常=门失败(战斗全空事故的直接口径)。 */ +function frames(n, label) { + for (let i = 0; i < n; i++) { + try { sceneObj.update.call(scene); } catch (e) { + assert(false, `${label}:update 第 ${i + 1} 帧抛异常(绘制路径断裂)`, String(e && e.message)); + return false; + } + } + return true; +} + +// ── C1 create 零异常 ── +try { sceneObj.create.call(scene); assert(true, 'C1 create() 零异常'); } +catch (e) { assert(false, 'C1 create() 零异常', String(e && e.message)); } + +// ── C2 七屏驱动:贪心把整局打到终态,逐屏累计绘制计数;每到一个新屏断言「本屏有绘制」── +const seenScreens = new Map(); // screen → 首见时 rebuild 后的 uiNodes 计数 +let lastScreen = null; +let ok = true; +for (let step = 0; step < 20000 && ok; step++) { + const s = run.readState(); + if (s.phase !== 'run') break; + if (!frames(1, `C2[${s.screen}]`)) { ok = false; break; } + const cur = debug.screen; + if (cur && cur !== lastScreen) { + lastScreen = cur; + if (!seenScreens.has(cur)) seenScreens.set(cur, debug.displayCount); + } + const t = chooseTap(s); + if (!t) break; + // 直接语义调用(与 route 同表;mock 门不做坐标命中)。 + if (t.kind === 'card') run.tapCard(t.index); + else if (t.kind === 'enemy') run.confirmPlay(); + else if (t.kind === 'endTurn') run.endTurn(); + else if (t.kind === 'node') run.chooseNode(t.id); + else if (t.kind === 'reward') run.chooseReward(t.index); + else if (t.kind === 'rewardSkip') run.chooseReward('skip'); + else if (t.kind === 'relicReward') run.chooseRelicReward(t.index); + else if (t.kind === 'relicSkip') run.chooseRelicReward('skip'); + else if (t.kind === 'shopCard') run.buyCard(t.index); + else if (t.kind === 'shopRelic') run.buyRelic(t.index); + else if (t.kind === 'shopRemove') run.openRemove(); + else if (t.kind === 'removePick') run.buyRemove(t.index); + else if (t.kind === 'removeBack') run.closeRemove(); + else if (t.kind === 'shopLeave') run.leaveShop(); + else break; +} +if (ok) frames(2, 'C2[terminal]'); // 终局屏重绘 + +// 屏覆盖与每屏绘制下限(map/battle 是主战场;shop/reward/relicReward 依局路径,断言「见过的屏都画了东西」)。 +assert(seenScreens.has('map') && seenScreens.has('battle'), 'C2a 主屏覆盖(map+battle 必见)', JSON.stringify([...seenScreens.keys()])); +let allDrew = true; let detail = []; +for (const [screen, count] of seenScreens) { + detail.push(`${screen}=${count}`); + if (!(count >= 5)) allDrew = false; // 任何屏 rebuild 后 <5 个显示对象=形同白屏 +} +assert(allDrew, 'C2b 每个到达屏绘制对象 ≥5(白屏即败)', detail.join(' / ')); +// 终局遮罩本体=3–5 件(lose 3 件:遮罩+标题+战果行+提示,win 多一行)——阈值按最小屏 3。 +assert(debug.rebuilds > 0 && debug.displayCount >= 3, 'C2c 自证钩子回写(rebuilds>0 且终局屏 displayCount≥3)', `rebuilds=${debug.rebuilds} display=${debug.displayCount}`); +console.log(` · 屏覆盖:${detail.join(' / ')};终态=${run.readState().phase};mock 总 add=${scene.counts.adds}`); + +// ── C3 选中态重绘(战斗内点选卡的 sig 变化路径,历史上易被签名漏掉)── +{ + const run2 = createRunCore({ random: createSeededRandom(7) }, { seed: 7 }); + const dbg2 = { displayCount: 0, rebuilds: 0, screen: null }; + const so2 = createRunScene({ run: () => run2, debug: dbg2, route: () => {} }); + const sc2 = makeMockScene(); + so2.create.call(sc2); + run2.chooseNode(run2.readState().map.selectable[0]); + so2.update.call(sc2); + const before = dbg2.rebuilds; + run2.tapCard(0); // 选中(ui.selected 变 → sig 变 → 重建) + try { + so2.update.call(sc2); + assert(dbg2.rebuilds === before + 1, 'C3 选中态触发重绘(sig 含 ui.selected)', `rebuilds ${before}→${dbg2.rebuilds}`); + } catch (e) { + assert(false, 'C3 选中态重绘零异常', String(e && e.message)); + } +} + +console.log(failures === 0 ? '[scene-smoke] 全绿' : `[scene-smoke] ${failures} 条失败`); +process.exit(failures === 0 ? 0 : 1); diff --git a/tier2/games/shanhai-xunyilu/scripts/sim-shell.mjs b/tier2/games/shanhai-xunyilu/scripts/sim-shell.mjs index 7f486d41..acfc15d3 100644 --- a/tier2/games/shanhai-xunyilu/scripts/sim-shell.mjs +++ b/tier2/games/shanhai-xunyilu/scripts/sim-shell.mjs @@ -1,22 +1,21 @@ /** - * scripts/sim-shell.mjs —— 《山海巡异录》快进仿真批跑外壳(M2 S8b v0 → S12 v1;node 直跑) - * owner:北极星顶级线③(M2) + * scripts/sim-shell.mjs —— 《山海巡异录》快进仿真批跑外壳(M2 S8b v0 → S12 v1 → M3 S14 三轴全矩阵) + * owner:北极星顶级线③(M2/M3) * - * 【这份是什么】设计书 §6.4「数值 CI」的批跑入口:矩阵扫描(职业×进阶×策略档×n 局)→ - * 格聚合(src/sim/sim-run.js)→ 带判报警(SIM_BANDS,观测不拦)→ 报告(stdout 摘要 + dist/ JSON)。 - * 金标构筑回归=--goldens(S12;基线 scripts/goldens.json)。 + * 【这份是什么】设计书 §6.4「数值 CI」批跑入口:三轴矩阵扫描(职业 × 进阶 A0–A15 × 策略档 × n 局)→ + * 格聚合(src/sim/sim-run.js)→ 带判报警(SIM_BANDS,观测不拦)→ 报告(stdout + dist/ JSON)。 + * 金标构筑回归=--goldens(基线 scripts/goldens.json,漂移 ±8pp 按套判)。 * - * 【矩阵现状】M2 内容面=职业甲 × A0(进阶表随 M3 S14 启用后本壳按表扩轴,轴形状已留)。 + * 【矩阵轴】(M3 S14 全量)职业 {jia,yi} × 进阶 {0..15} × 书面三档 {greedy,chain,defensive} + * × 1000 局/格 = 96,000 局(书 §6.4 口径);random/idle 两档为下界锚,只在 A0 跑观测。 * * 【用法】 - * node scripts/sim-shell.mjs # 全矩阵(3 档 × --n,缺省 1000) - * node scripts/sim-shell.mjs --tier=pr # PR smoke 小矩阵(贪心档 × 200) - * node scripts/sim-shell.mjs --tier=nightly # nightly 全矩阵(3 档 × 1000)+ 金标全量 - * node scripts/sim-shell.mjs --tier=batch # 批产冒烟(3 档 × 200;S9/S10 每批入表即跑) - * node scripts/sim-shell.mjs --goldens # 金标构筑回归(对 scripts/goldens.json 基线判漂移) - * --n=N --seedBase=S --out=dist/xxx.json 可覆写 + * node scripts/sim-shell.mjs --tier=pr # PR smoke:2职业 × A0/A5/A15 × 贪心 × 200(书面小矩阵)+金标抽测 + * node scripts/sim-shell.mjs --tier=batch # 批产冒烟:2职业 × A0 × 五档 × 200(内容入表即跑) + * node scripts/sim-shell.mjs --tier=nightly # 全矩阵 96,000 局 + 下界锚 + 金标全量 + * --classes=jia,yi --ascensions=0,5,15 --strategies=greedy --n=N --seedBase=S --out=… 自由组格 * - * 【退出码】0=跑完(带判越带只报警;M0–M2 硬门仅 L1);1=结构性失败(stuck/guard 截断/金标漂移超线)。 + * 【退出码】0=跑完(带判越带只报警;M0–M3 硬门仅 L1);1=结构性失败(stuck/guard/金标漂移超线)。 */ 'use strict'; @@ -25,75 +24,112 @@ import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; -import { simulateRun, aggregateCell, STRATEGIES } from '../src/sim/sim-run.js'; -import { SIM_BANDS, CARDS, RELICS } from '../src/data/tables.js'; +import { simulateRun, aggregateCell, STRATEGIES, CANON_STRATEGIES } from '../src/sim/sim-run.js'; +import { SIM_BANDS, CARDS, RELICS, CLASSES, ASCENSIONS } from '../src/data/tables.js'; const __dir = dirname(fileURLToPath(import.meta.url)); const ROOT = join(__dir, '..'); -// ── 参数解析(零依赖,--k=v)── const argv = Object.fromEntries(process.argv.slice(2).map((a) => { const m = a.match(/^--([^=]+)(?:=(.*))?$/); return m ? [m[1], m[2] != null ? m[2] : true] : [a, true]; })); +const ALL_ASC = Array.from({ length: ASCENSIONS.length + 1 }, (_, i) => i); // 0..15 + /** CI 分层预设(§6.4;墙钟实测随本壳输出,定案写 README)。 */ const TIERS = { - pr: { strategies: ['greedy'], n: 200, goldens: 'sample' }, // PR smoke:贪心×200 + 金标抽测 - nightly: { strategies: ['greedy', 'random', 'idle'], n: 1000, goldens: 'full' }, // 全矩阵 + 金标全量 - batch: { strategies: ['greedy', 'random', 'idle'], n: 200, goldens: 'none' }, // S9/S10 批产入表冒烟 + pr: { classes: ['jia', 'yi'], ascensions: [0, 5, 15], strategies: ['greedy'], n: 200, goldens: 'sample' }, + batch: { classes: ['jia', 'yi'], ascensions: [0], strategies: Object.keys(STRATEGIES), n: 200, goldens: 'none' }, + nightly: { classes: ['jia', 'yi'], ascensions: ALL_ASC, strategies: CANON_STRATEGIES, n: 1000, goldens: 'full', floorAnchors: true }, }; const tier = argv.tier && TIERS[argv.tier] ? argv.tier : null; -const strategies = (argv.strategies ? String(argv.strategies).split(',') : (tier ? TIERS[tier].strategies : ['greedy', 'random', 'idle'])) +const preset = tier ? TIERS[tier] : {}; +const classes = (argv.classes ? String(argv.classes).split(',') : (preset.classes || ['jia'])).filter((c) => CLASSES[c]); +const ascensions = (argv.ascensions ? String(argv.ascensions).split(',').map(Number) : (preset.ascensions || [0])) + .filter((a) => Number.isInteger(a) && a >= 0 && a <= ASCENSIONS.length); +const strategies = (argv.strategies ? String(argv.strategies).split(',') : (preset.strategies || CANON_STRATEGIES)) .filter((s) => STRATEGIES[s]); -const N = argv.n ? parseInt(argv.n, 10) : (tier ? TIERS[tier].n : 1000); +const N = argv.n ? parseInt(argv.n, 10) : (preset.n || 1000); const seedBase = argv.seedBase ? parseInt(argv.seedBase, 10) : 50000; const outPath = argv.out ? String(argv.out) : join(ROOT, 'dist', tier ? `sim-${tier}.json` : 'sim-scan.json'); -const goldensMode = argv.goldens === true ? 'full' : (argv.goldens ? String(argv.goldens) : (tier ? TIERS[tier].goldens : 'none')); +const goldensMode = argv.goldens === true ? 'full' : (argv.goldens ? String(argv.goldens) : (preset.goldens || 'none')); let structuralFailures = 0; const warn = (msg) => console.warn(` WARN ${msg}`); const fail = (msg) => { console.error(` FAIL ${msg}`); structuralFailures++; }; -console.log(`[sim-shell] 矩阵=职业甲 × A0 × {${strategies.join(',')}} × ${N} 局/格;seedBase=${seedBase}${tier ? ` tier=${tier}` : ''}`); +const totalCells = classes.length * ascensions.length * strategies.length; +console.log(`[sim-shell] 矩阵={${classes.join(',')}} × A{${ascensions.join(',')}} × {${strategies.join(',')}} × ${N} 局/格 = ${totalCells} 格 ${totalCells * N} 局;seedBase=${seedBase}${tier ? ` tier=${tier}` : ''}`); -// ── 矩阵扫描 ── +// ── 三轴矩阵扫描 ── const t0 = process.hrtime.bigint(); const cells = []; -for (const strategy of strategies) { - const results = []; - for (let i = 0; i < N; i++) { - results.push(simulateRun({ seed: seedBase + i * 13, strategy })); - } - const agg = aggregateCell(results, { cell: { class: 'jia', ascension: 0, strategy } }); - cells.push(agg); +for (const heroClass of classes) { + for (const ascension of ascensions) { + for (const strategy of strategies) { + const results = []; + // seed 按格错开(进阶/职业散列),格内步进 13(与 M2 单轴口径同源)。 + const cellSeed = (seedBase + ascension * 104729 + (heroClass === 'yi' ? 7919 : 0)) >>> 0; + for (let i = 0; i < N; i++) { + results.push(simulateRun({ seed: cellSeed + i * 13, strategy, runOpts: { heroClass, ascension } })); + } + const agg = aggregateCell(results, { cell: { class: heroClass, ascension, strategy } }); + cells.push(agg); + if (agg.stucks > 0) fail(`${heroClass}/A${ascension}/${strategy} stuck/guard 截断 ${agg.stucks} 局(策略死角或护栏空转)`); + if (agg.invalids > 0) warn(`${heroClass}/A${ascension}/${strategy} invalid ${agg.invalids} 局(动作护栏;指纹见报告)`); - // 结构性失败(与带判分开:这些是仿真环自身的红线,任何档都不许有)。 - if (agg.stucks > 0) fail(`${strategy} 档 stuck/guard 截断 ${agg.stucks} 局(策略死角或护栏空转)`); - - // 带判(观测/报警;校准对象=贪心档)。 - const band = SIM_BANDS.winRateBands[0]; - if (strategy === 'greedy') { - const inBand = agg.winRate >= band[0] && agg.winRate <= band[1]; - console.log(` ${inBand ? 'PASS' : 'WARN'} A0 贪心档胜率 ${(agg.winRate * 100).toFixed(1)}%(带 ${band[0] * 100}–${band[1] * 100}%,σ≈${agg.winRateSigmaPp}pp)${inBand ? '' : ' —— 越带,调参轮处理'}`); - if (agg.bossTurns.grindOverBand > 0 || agg.bossTurns.burstUnderBand > 0) { - warn(`Boss 回合带外:磨(>${SIM_BANDS.bossTurnBand[1]}回合)×${agg.bossTurns.grindOverBand} / 秒(<${SIM_BANDS.bossTurnBand[0]})×${agg.bossTurns.burstUnderBand}(共 ${agg.bossTurns.count} 场)`); + // 带判(观测/报警):贪心档 × 有带进阶层(0/5/15)。 + const band = SIM_BANDS.winRateBands[ascension]; + if (strategy === 'greedy' && band) { + const inBand = agg.winRate >= band[0] && agg.winRate <= band[1]; + console.log(` ${inBand ? 'PASS' : 'WARN'} ${CLASSES[heroClass].name} A${ascension} 贪心胜率 ${(agg.winRate * 100).toFixed(1)}%(带 ${band[0] * 100}–${band[1] * 100}%,σ≈${agg.winRateSigmaPp}pp)${inBand ? '' : ' —— 越带'}`); + } else if (strategy === 'greedy' || N >= 1000) { + console.log(` INFO ${heroClass}/A${ascension}/${strategy} 胜率 ${(agg.winRate * 100).toFixed(1)}%(观测)`); + } + } + } +} +// 下界锚(nightly:random/idle 在 A0 各 1000;观测敌线致死性,不入死卡口径)。 +if (preset.floorAnchors) { + for (const heroClass of classes) { + for (const strategy of ['random', 'idle']) { + const results = []; + for (let i = 0; i < 1000; i++) results.push(simulateRun({ seed: seedBase + i * 13, strategy, runOpts: { heroClass, ascension: 0 } })); + const agg = aggregateCell(results, { cell: { class: heroClass, ascension: 0, strategy, anchor: true } }); + cells.push(agg); + console.log(` INFO ${heroClass}/A0/${strategy} 下界锚胜率 ${(agg.winRate * 100).toFixed(1)}%`); } - if (agg.peakTurnDmg.monopolyRatio != null && agg.peakTurnDmg.monopolyRatio > SIM_BANDS.monopolyMax) { - warn(`峰值回合伤 P99/P50=${agg.peakTurnDmg.monopolyRatio} 超反垄断线 ${SIM_BANDS.monopolyMax}(某流派一枝独秀,削它别抬别人)`); - } - if (agg.deadCards.length) warn(`死卡候选(<${SIM_BANDS.adoptionBand.dead * 100}% 采用):${agg.deadCards.join(',')}`); - if (agg.mustPicks.length) warn(`必选候选(>${SIM_BANDS.adoptionBand.mustPick * 100}% 采用):${agg.mustPicks.join(',')}`); - } else { - console.log(` INFO A0 ${strategy} 档胜率 ${(agg.winRate * 100).toFixed(1)}%(下界观测,非校准对象)`); } - if (agg.invalids > 0) warn(`${strategy} 档 invalid ${agg.invalids} 局(动作护栏触发;指纹样本见报告)`); - console.log(` · ${strategy}:win ${agg.wins}/${agg.n},均战 ${agg.avgBattlesWon},Boss回合 P50=${agg.bossTurns.p50},墙钟 ${agg.wallMs}ms(${agg.msPerRun}ms/局)`); } const scanMs = Number(process.hrtime.bigint() - t0) / 1e6; -// ── 金标构筑回归(S12;每套固定构筑 × 固定 seed 集,胜率漂移按套判)── +// ── 跨档观测:死卡并集(书面三档;A0 格)与指纹去重率(M3 门 >85%〔草案〕)── +{ + const canonA0 = cells.filter((c) => c.cell && !c.cell.anchor && c.cell.ascension === 0 && CANON_STRATEGIES.includes(c.cell.strategy)); + if (canonA0.length) { + const adopted = new Set(); + const seen = new Set(); + for (const c of canonA0) { + for (const [id, rate] of Object.entries(c.adoption)) { + seen.add(id); + if (rate >= SIM_BANDS.adoptionBand.dead) adopted.add(id); + } + } + const dead = [...seen].filter((id) => !adopted.has(id)); + if (dead.length) warn(`死卡候选(书面三档并集 <3%):${dead.join(',')}`); + else console.log(' PASS 死卡并集为空(书面三档下所有出场卡均 ≥3% 采用)'); + for (const c of canonA0) { + if (c.fingerprintUniqueRate != null && c.wins >= 20) { + const okFp = c.fingerprintUniqueRate > 0.85; + console.log(` ${okFp ? 'PASS' : 'WARN'} ${c.cell.class}/A0/${c.cell.strategy} 构筑指纹去重率 ${(c.fingerprintUniqueRate * 100).toFixed(1)}%(门 >85%〔草案〕,胜局 ${c.wins})`); + } + } + } +} + +// ── 金标构筑回归 ── let goldensReport = null; if (goldensMode !== 'none' && goldensMode !== undefined) { const goldensPath = join(ROOT, 'scripts', 'goldens.json'); @@ -112,7 +148,7 @@ if (goldensMode !== 'none' && goldensMode !== undefined) { results.push(simulateRun({ seed: goldens.seedBase + i * 7, strategy: suite.strategy || 'greedy', - runOpts: { deck: suite.deck, relics: suite.relics }, + runOpts: { deck: suite.deck, relics: suite.relics, heroClass: suite.class || 'jia', ascension: suite.ascension || 0 }, })); } const wins = results.filter((r) => r.phase === 'win').length; @@ -130,12 +166,12 @@ if (goldensMode !== 'none' && goldensMode !== undefined) { // ── 报告落盘 ── const report = { - what: '快进仿真矩阵扫描(§6.4 数值 CI)', + what: '快进仿真三轴矩阵扫描(§6.4 数值 CI;M3 S14 全量轴)', date: new Date().toISOString().slice(0, 10), host: `${process.platform}/${process.arch} node ${process.version}(开发机;CI 机型另测)`, tier: tier || 'custom', - matrix: { class: ['jia'], ascension: [0], strategies, nPerCell: N, seedBase }, - tables: { cards: CARDS.length, relics: RELICS.length }, + matrix: { classes, ascensions, strategies, nPerCell: N, seedBase, totalRuns: totalCells * N }, + tables: { cards: CARDS.length, relics: RELICS.length, ascensionLevels: ASCENSIONS.length }, bands: SIM_BANDS, cells, goldens: goldensReport, @@ -144,5 +180,5 @@ const report = { }; mkdirSync(dirname(outPath), { recursive: true }); writeFileSync(outPath, JSON.stringify(report, null, 2)); -console.log(`[sim-shell] 总墙钟 ${report.totalWallMs}ms(扫描 ${report.scanWallMs}ms);报告 → ${outPath}`); +console.log(`[sim-shell] 总墙钟 ${Math.round(report.totalWallMs / 100) / 10}s(扫描 ${Math.round(report.scanWallMs / 100) / 10}s);报告 → ${outPath}`); process.exit(structuralFailures === 0 ? 0 : 1); diff --git a/tier2/games/shanhai-xunyilu/scripts/static-screen.mjs b/tier2/games/shanhai-xunyilu/scripts/static-screen.mjs index eee25fda..22c20e6c 100644 --- a/tier2/games/shanhai-xunyilu/scripts/static-screen.mjs +++ b/tier2/games/shanhai-xunyilu/scripts/static-screen.mjs @@ -26,7 +26,7 @@ const fail = (msg) => { console.error(` FAIL ${msg}`); failures++; }; console.log(`[static-screen] 卡池 ${CARDS.length} 张;深度 ≤3 经济环快筛 + 估值黄牌`); // ── E0 经济词汇封闭性(折算器认知集 = 表中实际经济词集;漂移即红)── -const ECON_WORDS = new Set(['energy', 'draw', 'chainBonus', 'costChainDiscount']); +const ECON_WORDS = new Set(['energy', 'draw', 'chainBonus', 'costChainDiscount', 'ifZhoutian']); { const seen = new Set(); for (const c of CARDS) { @@ -34,6 +34,7 @@ const ECON_WORDS = new Set(['energy', 'draw', 'chainBonus', 'costChainDiscount'] // 产能量/产抽卡的词才是经济词;伤害/护甲/词缀/血价不构成经济环。 if (k === 'energy' || k === 'draw' || k === 'costChainDiscount') seen.add(k); if (k === 'chainBonus' && (c.effects.chainBonus.energy || c.effects.chainBonus.draw)) seen.add('chainBonus'); + if (k === 'ifZhoutian' && (c.effects.ifZhoutian.energy || c.effects.ifZhoutian.draw)) seen.add('ifZhoutian'); } } const unknown = [...seen].filter((w) => !ECON_WORDS.has(w)); @@ -62,6 +63,8 @@ const ECON_WORDS = new Set(['energy', 'draw', 'chainBonus', 'costChainDiscount'] if (c.effects.chainBonus.energy) netEnergy += c.effects.chainBonus.energy; if (c.effects.chainBonus.draw) netDraw += c.effects.chainBonus.draw; } + // ifZhoutian(M3):周天须五行全环,深度 ≤3 序列数学上不可能触发——本筛零入账即精确; + // ≥4 卡的周天经济环归运行时兜底层+指纹观测(§6.3 分层设防,快筛不越界宣称)。 netDraw -= 1; // 打出即消耗一张手牌 prevEl = c.element; // 延续与破序殊途同归:链尾都是本卡元素(破序=重开链,链首=本卡) } diff --git a/tier2/games/shanhai-xunyilu/src/battle-core.js b/tier2/games/shanhai-xunyilu/src/battle-core.js index 7dd66db4..99de1c10 100644 --- a/tier2/games/shanhai-xunyilu/src/battle-core.js +++ b/tier2/games/shanhai-xunyilu/src/battle-core.js @@ -113,7 +113,8 @@ export function createBattleCore(deps, opts = {}) { }, enemy: { id: enemyDef.id, name: enemyDef.name, hpMax: enemyDef.hpMax, hp: enemyDef.hpMax, - block: 0, strength: 0, intentIndex: 0, + block: 0, strength: opts.enemyStartStrength || 0, intentIndex: 0, // M3 进阶 A15:敌开战怀力 + tier: enemyDef.tier || 'normal', // M1 增列(奖励与展示消费) phase2: false, // Boss 相位标记(intents2 激活后为真) // M2 状态词缀(敌侧;易伤/虚弱=回合末-1,灼=敌行动后结算层伤再-1)。 @@ -424,6 +425,21 @@ export function createBattleCore(deps, opts = {}) { const healed = Math.min(fx.heal.n, state.hero.hpMax - state.hero.hp); if (healed > 0) { state.hero.hp += healed; emit({ t: 'heal', n: healed, source: cardId }); } } + // M3 乙职业身份词:周天已开(本回合)则附加效果(链优 payoff;伤/甲吃倍率,资源类不吃)。 + if (fx.ifZhoutian && state.chain.zhoutianUsed) { + const z = fx.ifZhoutian; + if (z.dmg) { + const zd = dealToEnemy(Math.floor(z.dmg * mult)); + emit({ t: 'damage', target: 'enemy', amount: Math.floor(z.dmg * mult), dealt: zd, seg: 1, of: 1, cardId, mult, source: 'ifZhoutian' }); + } + if (z.block) { + const zb = Math.floor(z.block * mult) + state.hero.toughness; + state.hero.block += zb; + emit({ t: 'blockGain', target: 'hero', n: zb, cardId, mult, source: 'ifZhoutian' }); + } + if (z.draw) drawN(z.draw); + if (z.energy) { state.energy += z.energy; emit({ t: 'energyGain', n: z.energy, source: cardId }); } + } if (fx.vulnerable) { state.enemy.statuses.vulnerable += fx.vulnerable.n; emit({ t: 'statusApplied', target: 'enemy', status: 'vulnerable', n: fx.vulnerable.n, cardId }); } if (fx.weak) { state.enemy.statuses.weak += fx.weak.n; emit({ t: 'statusApplied', target: 'enemy', status: 'weak', n: fx.weak.n, cardId }); } if (fx.burn) { state.enemy.statuses.burn += fx.burn.n; emit({ t: 'statusApplied', target: 'enemy', status: 'burn', n: fx.burn.n, cardId }); } diff --git a/tier2/games/shanhai-xunyilu/src/data/tables.js b/tier2/games/shanhai-xunyilu/src/data/tables.js index ae7f17c5..b4177cfb 100644 --- a/tier2/games/shanhai-xunyilu/src/data/tables.js +++ b/tier2/games/shanhai-xunyilu/src/data/tables.js @@ -155,13 +155,132 @@ export const CARDS = [ { id: 'shui-canghai', name: '沧海月明', element: '水', rarity: '普通', cost: 2, type: '术', effects: { draw: { n: 2 }, block: { n: 3 } }, text: '抽 2 张牌,获得 3 点护甲。' }, { id: 'shui-hantan', name: '寒潭映斗', element: '水', rarity: '普通', cost: 1, type: '御', effects: { block: { n: 3 }, chainBonus: { block: 3 } }, text: '获得 3 点护甲;若延续灵脉,追加 3 点。' }, { id: 'shui-beiming', name: '北溟鲲跃', element: '水', rarity: '稀有', cost: 3, type: '攻', effects: { dmg: { n: 9, times: 1 }, draw: { n: 2 }, energy: { n: 1 } }, text: '鲲跃北溟,造成 9 点伤害,抽 2 张牌,得 1 点能量。' }, + // ══ M3 S13 职业乙「璇玑使」66 卡(链优;星曜×山海命名,clean-room;class:'yi')══ + // ── 乙·木(13:普6罕5稀2)── + { id: 'yi-mu-luoteng', class: 'yi', name: '萝藤续脉', element: '木', rarity: '普通', cost: 1, type: '攻', effects: { dmg: { n: 5, times: 1 }, costChainDiscount: { n: 1 } }, text: '造成 5 点伤害;若延续灵脉,费用 -1。' }, + { id: 'yi-mu-ximu', class: 'yi', name: '析木问天', element: '木', rarity: '普通', cost: 1, type: '术', effects: { draw: { n: 2 }, hpCost: { n: 1 } }, text: '失去 1 点气血,抽 2 张牌。' }, + { id: 'yi-mu-ruomu', class: 'yi', name: '若木凝露', element: '木', rarity: '普通', cost: 0, type: '御', effects: { block: { n: 2 } }, text: '获得 2 点护甲。' }, + { id: 'yi-mu-fumu', class: 'yi', name: '扶木朝阳', element: '木', rarity: '普通', cost: 2, type: '攻', effects: { dmg: { n: 8, times: 1 }, heal: { n: 2 } }, text: '造成 8 点伤害,回复 2 点气血。' }, + { id: 'yi-mu-linggen2', class: 'yi', name: '灵根盘结', element: '木', rarity: '普通', cost: 1, type: '御', effects: { block: { n: 3 }, chainBonus: { block: 2 } }, text: '获得 3 点护甲;若成链,追加 2 点。' }, + { id: 'yi-mu-tangong', class: 'yi', name: '檀弓发矢', element: '木', rarity: '普通', cost: 1, type: '攻', effects: { dmg: { n: 6, times: 1 }, chainBonus: { dmg: 3 } }, text: '造成 6 点伤害;若成链,追加 3 点。' }, + { id: 'yi-mu-sangzhe', class: 'yi', name: '桑柘轮转', element: '木', rarity: '罕见', cost: 1, type: '术', effects: { draw: { n: 2 }, costChainDiscount: { n: 1 } }, text: '抽 2 张牌;若延续灵脉,费用 -1。' }, + { id: 'yi-mu-futi', class: 'yi', name: '建木浮梯', element: '木', rarity: '罕见', cost: 2, type: '术', effects: { draw: { n: 2 }, energy: { n: 1 }, hpCost: { n: 3 } }, text: '失去 3 点气血,抽 2 张牌,得 1 点能量。' }, + { id: 'yi-mu-pantao', class: 'yi', name: '蟠桃酿', element: '木', rarity: '罕见', cost: 2, type: '术', effects: { heal: { n: 5 }, block: { n: 4 }, exhaust: true }, text: '回复 5 点气血,获得 4 点护甲。(消耗)' }, + { id: 'yi-mu-qingdi', class: 'yi', name: '青帝手书', element: '木', rarity: '罕见', cost: 2, type: '御', effects: { block: { n: 6 }, chainBonus: { draw: 1 } }, text: '获得 6 点护甲;若成链,抽 1 张牌。' }, + { id: 'yi-mu-kuae', class: 'yi', name: '夸娥负山', element: '木', rarity: '罕见', cost: 3, type: '御', effects: { block: { n: 11 }, vulnerable: { n: 1 } }, text: '获得 11 点护甲,施加 1 回合易伤。' }, + { id: 'yi-mu-taiyi', class: 'yi', name: '太一青符', element: '木', rarity: '稀有', cost: 2, type: '术', effects: { draw: { n: 2 }, toughness: { n: 1 }, ifZhoutian: { energy: 2 } }, text: '抽 2 张牌,获得 1 点韧;本回合已开周天,得 2 点能量。' }, + { id: 'yi-mu-dachun', class: 'yi', name: '青阳大椿', element: '木', rarity: '稀有', cost: 3, type: '御', effects: { block: { n: 9 }, heal: { n: 4 }, toughness: { n: 1 } }, text: '获得 9 点护甲与 1 点韧,回复 4 点气血。' }, + // ── 乙·火(13:普6罕5稀2)── + { id: 'yi-huo-yinghuo', class: 'yi', name: '荧惑守心', element: '火', rarity: '普通', cost: 1, type: '攻', effects: { dmg: { n: 5, times: 1 }, burn: { n: 2 } }, text: '造成 5 点伤害,施加 2 层灼。' }, + { id: 'yi-huo-zhuming', class: 'yi', name: '朱明引', element: '火', rarity: '普通', cost: 1, type: '攻', effects: { dmg: { n: 7, times: 1 } }, text: '朱明之火,造成 7 点伤害。' }, + { id: 'yi-huo-liuhuo', class: 'yi', name: '流火纪', element: '火', rarity: '普通', cost: 0, type: '攻', effects: { dmg: { n: 2, times: 1 }, burn: { n: 1 } }, text: '造成 2 点伤害,施加 1 层灼。' }, + { id: 'yi-huo-tanggu', class: 'yi', name: '汤谷沐日', element: '火', rarity: '普通', cost: 2, type: '术', effects: { burn: { n: 6 }, draw: { n: 1 } }, text: '施加 6 层灼,抽 1 张牌。' }, + { id: 'yi-huo-yangwu', class: 'yi', name: '阳乌啼', element: '火', rarity: '普通', cost: 2, type: '攻', effects: { dmg: { n: 8, times: 1 }, vulnerable: { n: 1 } }, text: '造成 8 点伤害,施加 1 回合易伤。' }, + { id: 'yi-huo-zhulou', class: 'yi', name: '烛楼远照', element: '火', rarity: '普通', cost: 1, type: '术', effects: { burn: { n: 4 }, costChainDiscount: { n: 1 } }, text: '施加 4 层灼;若延续灵脉,费用 -1。' }, + { id: 'yi-huo-huoji', class: 'yi', name: '火纪通明', element: '火', rarity: '罕见', cost: 2, type: '攻', effects: { dmg: { n: 5, times: 2 }, ifZhoutian: { dmg: 8 } }, text: '造成 5 点伤害 2 段;本回合已开周天,追加 8 点。' }, + { id: 'yi-huo-yandi', class: 'yi', name: '炎帝遗策', element: '火', rarity: '罕见', cost: 1, type: '术', effects: { burn: { n: 4 }, draw: { n: 1 } }, text: '施加 4 层灼,抽 1 张牌。' }, + { id: 'yi-huo-zhurong2', class: 'yi', name: '祝融巡南', element: '火', rarity: '罕见', cost: 2, type: '攻', effects: { dmg: { n: 9, times: 1 }, burn: { n: 3 } }, text: '造成 9 点伤害,施加 3 层灼。' }, + { id: 'yi-huo-ligong', class: 'yi', name: '离宫九变', element: '火', rarity: '罕见', cost: 2, type: '术', effects: { burn: { n: 8 }, vulnerable: { n: 1 } }, text: '施加 8 层灼与 1 回合易伤。' }, + { id: 'yi-huo-xiaming', class: 'yi', name: '霞明玉阶', element: '火', rarity: '罕见', cost: 1, type: '御', effects: { block: { n: 3 }, burn: { n: 2 } }, text: '获得 3 点护甲,施加 2 层灼。' }, + { id: 'yi-huo-jinwu2', class: 'yi', name: '金乌浴海', element: '火', rarity: '稀有', cost: 3, type: '攻', effects: { dmg: { n: 7, times: 2 }, burn: { n: 6 }, hpCost: { n: 3 } }, text: '失去 3 点气血,造成 7 点伤害 2 段,施加 6 层灼。' }, + { id: 'yi-huo-huoling', class: 'yi', name: '火铃夷则', element: '火', rarity: '稀有', cost: 2, type: '攻', effects: { dmg: { n: 6, times: 1 }, dmgPerEnemyBurn: { n: 2 }, burn: { n: 3 } }, text: '造成 6 点伤害,敌身每层灼追加 2 点,再施 3 层灼。' }, + // ── 乙·土(13:普6罕5稀2)── + { id: 'yi-tu-wuse', class: 'yi', name: '五色壤', element: '土', rarity: '普通', cost: 1, type: '御', effects: { block: { n: 4 } }, text: '获得 4 点护甲。' }, + { id: 'yi-tu-jishi', class: 'yi', name: '积石传书', element: '土', rarity: '普通', cost: 1, type: '术', effects: { block: { n: 2 }, draw: { n: 1 } }, text: '获得 2 点护甲,抽 1 张牌。' }, + { id: 'yi-tu-lianshan', class: 'yi', name: '磏山铁壁', element: '土', rarity: '普通', cost: 2, type: '御', effects: { block: { n: 7 }, chainBonus: { block: 2 } }, text: '获得 7 点护甲;若成链,追加 2 点。' }, + { id: 'yi-tu-xuanyin', class: 'yi', name: '后土玄印', element: '土', rarity: '普通', cost: 1, type: '术', effects: { toughness: { n: 1 }, block: { n: 2 } }, text: '获得 1 点韧与 2 点护甲。' }, + { id: 'yi-tu-changyang', class: 'yi', name: '常羊拜岳', element: '土', rarity: '普通', cost: 2, type: '攻', effects: { dmg: { n: 9, times: 1 }, toughness: { n: 1 } }, text: '造成 9 点伤害,获得 1 点韧。' }, + { id: 'yi-tu-zhenyue2', class: 'yi', name: '镇岳一击', element: '土', rarity: '普通', cost: 1, type: '攻', effects: { dmg: { n: 7, times: 1 } }, text: '岳势压顶,造成 7 点伤害。' }, + { id: 'yi-tu-xiushe', class: 'yi', name: '修蛇壤穴', element: '土', rarity: '罕见', cost: 2, type: '御', effects: { block: { n: 7 }, weak: { n: 1 } }, text: '获得 7 点护甲,施加 1 回合虚弱。' }, + { id: 'yi-tu-bugang', class: 'yi', name: '步罡踏斗', element: '土', rarity: '罕见', cost: 1, type: '术', effects: { block: { n: 2 }, toughness: { n: 1 }, costChainDiscount: { n: 1 } }, text: '获得 2 点护甲与 1 点韧;若延续灵脉,费用 -1。' }, + { id: 'yi-tu-daiyu', class: 'yi', name: '岱舆孤峙', element: '土', rarity: '罕见', cost: 3, type: '御', effects: { block: { n: 10 }, ifZhoutian: { block: 8 } }, text: '获得 10 点护甲;本回合已开周天,追加 8 点。' }, + { id: 'yi-tu-butian', class: 'yi', name: '息壤补天', element: '土', rarity: '罕见', cost: 2, type: '御', effects: { block: { n: 5 }, heal: { n: 3 } }, text: '获得 5 点护甲,回复 3 点气血。' }, + { id: 'yi-tu-guitian', class: 'yi', name: '圭田祝岁', element: '土', rarity: '罕见', cost: 2, type: '术', effects: { toughness: { n: 2 }, draw: { n: 1 } }, text: '获得 2 点韧,抽 1 张牌。' }, + { id: 'yi-tu-buzhou', class: 'yi', name: '不周残柱', element: '土', rarity: '稀有', cost: 3, type: '御', effects: { block: { n: 11 }, vulnerable: { n: 2 } }, text: '获得 11 点护甲,施加 2 回合易伤。' }, + { id: 'yi-tu-yugao', class: 'yi', name: '帝山玉膏', element: '土', rarity: '稀有', cost: 2, type: '术', effects: { heal: { n: 6 }, toughness: { n: 2 }, exhaust: true }, text: '回复 6 点气血,获得 2 点韧。(消耗)' }, + // ── 乙·金(13:普6罕5稀2)── + { id: 'yi-jin-changgeng', class: 'yi', name: '长庚曳白', element: '金', rarity: '普通', cost: 1, type: '攻', effects: { dmg: { n: 7, times: 1 } }, text: '长庚曳白光,造成 7 点伤害。' }, + { id: 'yi-jin-shenshang', class: 'yi', name: '参商斫桂', element: '金', rarity: '普通', cost: 1, type: '攻', effects: { dmg: { n: 4, times: 1 }, costChainDiscount: { n: 1 } }, text: '造成 4 点伤害;若延续灵脉,费用 -1。' }, + { id: 'yi-jin-baihu', class: 'yi', name: '白虎衔烛', element: '金', rarity: '普通', cost: 2, type: '攻', effects: { dmg: { n: 6, times: 2 }, hpCost: { n: 2 } }, text: '失去 2 点气血,造成 6 点伤害 2 段。' }, + { id: 'yi-jin-yaoguang', class: 'yi', name: '摇光淬刃', element: '金', rarity: '普通', cost: 1, type: '术', effects: { strength: { n: 1 }, block: { n: 2 } }, text: '获得 1 点力与 2 点护甲。' }, + { id: 'yi-jin-jinsuo', class: 'yi', name: '金梭掷影', element: '金', rarity: '普通', cost: 0, type: '攻', effects: { dmg: { n: 2, times: 1 }, costChainDiscount: { n: 1 } }, text: '造成 2 点伤害;若延续灵脉,费用 -1(0 费不变)。' }, + { id: 'yi-jin-zhongshan', class: 'yi', name: '钟山错刀', element: '金', rarity: '普通', cost: 2, type: '攻', effects: { dmg: { n: 10, times: 1 }, weak: { n: 1 } }, text: '造成 10 点伤害,施加 1 回合虚弱。' }, + { id: 'yi-jin-rushou2', class: 'yi', name: '蓐收秉钺', element: '金', rarity: '罕见', cost: 2, type: '攻', effects: { dmg: { n: 8, times: 1 }, vulnerable: { n: 1 }, draw: { n: 1 } }, text: '造成 8 点伤害,施加 1 回合易伤,抽 1 张牌。' }, + { id: 'yi-jin-baiying', class: 'yi', name: '白英鸣镝', element: '金', rarity: '罕见', cost: 1, type: '攻', effects: { dmg: { n: 6, times: 1 }, chainBonus: { energy: 1 } }, text: '造成 6 点伤害;若成链,得 1 点能量。' }, + { id: 'yi-jin-xuanyuan', class: 'yi', name: '轩辕折戟', element: '金', rarity: '罕见', cost: 2, type: '攻', effects: { dmg: { n: 6, times: 2 }, costChainDiscount: { n: 1 } }, text: '造成 6 点伤害 2 段;若延续灵脉,费用 -1。' }, + { id: 'yi-jin-liuyin', class: 'yi', name: '流银八荒', element: '金', rarity: '罕见', cost: 3, type: '攻', effects: { dmg: { n: 5, times: 3 }, vulnerable: { n: 1 } }, text: '造成 5 点伤害 3 段,施加 1 回合易伤。' }, + { id: 'yi-jin-tianqian', class: 'yi', name: '天钤密令', element: '金', rarity: '罕见', cost: 1, type: '术', effects: { strength: { n: 2 }, hpCost: { n: 2 } }, text: '失去 2 点气血,获得 2 点力。' }, + { id: 'yi-jin-changyi', class: 'yi', name: '常仪奏钺', element: '金', rarity: '稀有', cost: 3, type: '攻', effects: { dmg: { n: 8, times: 2 }, ifZhoutian: { dmg: 12 } }, text: '造成 8 点伤害 2 段;本回合已开周天,追加 12 点。' }, + { id: 'yi-jin-juque', class: 'yi', name: '巨阙悬空', element: '金', rarity: '稀有', cost: 2, type: '攻', effects: { dmg: { n: 13, times: 1 }, exhaust: true }, text: '巨阙一落,造成 13 点伤害。(消耗)' }, + // ── 乙·水(14:普8罕4稀2)── + { id: 'yi-shui-tianhan', class: 'yi', name: '天汉横流', element: '水', rarity: '普通', cost: 1, type: '攻', effects: { dmg: { n: 5, times: 1 }, costChainDiscount: { n: 1 } }, text: '造成 5 点伤害;若延续灵脉,费用 -1。' }, + { id: 'yi-shui-yaochi', class: 'yi', name: '瑶池映斗', element: '水', rarity: '普通', cost: 1, type: '御', effects: { block: { n: 4 } }, text: '获得 4 点护甲。' }, + { id: 'yi-shui-ruoshui2', class: 'yi', name: '弱水拂尘', element: '水', rarity: '普通', cost: 1, type: '术', effects: { weak: { n: 1 }, draw: { n: 1 } }, text: '施加 1 回合虚弱,抽 1 张牌。' }, + { id: 'yi-shui-beidou', class: 'yi', name: '北斗酌浆', element: '水', rarity: '普通', cost: 1, type: '术', effects: { draw: { n: 2 } }, text: '斗柄酌天浆,抽 2 张牌。' }, + { id: 'yi-shui-xuanming2', class: 'yi', name: '玄冥司冬', element: '水', rarity: '普通', cost: 2, type: '御', effects: { block: { n: 6 }, weak: { n: 1 } }, text: '获得 6 点护甲,施加 1 回合虚弱。' }, + { id: 'yi-shui-jianglou', class: 'yi', name: '降娄行雨', element: '水', rarity: '普通', cost: 0, type: '术', effects: { block: { n: 1 }, costChainDiscount: { n: 1 } }, text: '获得 1 点护甲;若延续灵脉,费用 -1(0 费不变)。' }, + { id: 'yi-shui-hanjiang', class: 'yi', name: '寒江拾星', element: '水', rarity: '普通', cost: 2, type: '术', effects: { draw: { n: 2 }, heal: { n: 2 } }, text: '抽 2 张牌,回复 2 点气血。' }, + { id: 'yi-shui-tianshu', class: 'yi', name: '天枢洗剑', element: '水', rarity: '普通', cost: 2, type: '攻', effects: { dmg: { n: 8, times: 1 }, draw: { n: 1 } }, text: '造成 8 点伤害,抽 1 张牌。' }, + { id: 'yi-shui-yinhe', class: 'yi', name: '银河倒泻', element: '水', rarity: '罕见', cost: 2, type: '攻', effects: { dmg: { n: 8, times: 1 }, dmgPerChainLink: { n: 2 } }, text: '造成 8 点伤害,灵脉每环追加 2 点。' }, + { id: 'yi-shui-gonggong2', class: 'yi', name: '共工触山', element: '水', rarity: '罕见', cost: 3, type: '攻', effects: { dmg: { n: 20, times: 1 }, hpCost: { n: 5 } }, text: '失去 5 点气血,造成 20 点伤害。' }, + { id: 'yi-shui-tianji', class: 'yi', name: '天玑量海', element: '水', rarity: '罕见', cost: 1, type: '术', effects: { draw: { n: 1 }, energy: { n: 1 }, hpCost: { n: 3 }, exhaust: true }, text: '失去 3 点气血,抽 1 张牌,得 1 点能量。(消耗)' }, + { id: 'yi-shui-xuangui2', class: 'yi', name: '玄龟曳尾', element: '水', rarity: '罕见', cost: 2, type: '御', effects: { block: { n: 7 }, chainBonus: { draw: 1 } }, text: '获得 7 点护甲;若成链,抽 1 张牌。' }, + { id: 'yi-shui-zhoutian', class: 'yi', name: '周天倒卷帘', element: '水', rarity: '稀有', cost: 2, type: '术', effects: { draw: { n: 2 }, ifZhoutian: { energy: 2, draw: 1 } }, text: '抽 2 张牌;本回合已开周天,得 2 点能量再抽 1 张。' }, + { id: 'yi-shui-beiming2', class: 'yi', name: '北溟震怒', element: '水', rarity: '稀有', cost: 3, type: '攻', effects: { dmg: { n: 9, times: 1 }, weak: { n: 2 }, vulnerable: { n: 1 } }, text: '造成 9 点伤害,施加 2 回合虚弱与 1 回合易伤。' }, + // ══ M3 S13 中立 22(行旅工具件;class:'neutral',两职业共入池)══ + { id: 'lu-ganliang', class: 'neutral', name: '行山干粮', element: '木', rarity: '普通', cost: 1, type: '术', effects: { heal: { n: 3 }, exhaust: true }, text: '回复 3 点气血。(消耗)' }, + { id: 'lu-tengzhang', class: 'neutral', name: '青藤杖', element: '木', rarity: '普通', cost: 1, type: '攻', effects: { dmg: { n: 6, times: 1 } }, text: '杖击,造成 6 点伤害。' }, + { id: 'lu-yaonang', class: 'neutral', name: '药囊', element: '木', rarity: '普通', cost: 2, type: '术', effects: { heal: { n: 5 }, block: { n: 2 }, exhaust: true }, text: '回复 5 点气血,获得 2 点护甲。(消耗)' }, + { id: 'lu-mupai', class: 'neutral', name: '里程碑石', element: '土', rarity: '普通', cost: 0, type: '术', effects: { draw: { n: 1 } }, text: '抽 1 张牌。' }, + { id: 'lu-qingnang', class: 'neutral', name: '青囊书', element: '木', rarity: '罕见', cost: 2, type: '术', effects: { draw: { n: 2 }, heal: { n: 2 } }, text: '抽 2 张牌,回复 2 点气血。' }, + { id: 'lu-huozhe', class: 'neutral', name: '火折子', element: '火', rarity: '普通', cost: 0, type: '攻', effects: { dmg: { n: 3, times: 1 } }, text: '掷出火折,造成 3 点伤害。' }, + { id: 'lu-xinghuo', class: 'neutral', name: '星火燧', element: '火', rarity: '普通', cost: 1, type: '术', effects: { burn: { n: 5 } }, text: '施加 5 层灼。' }, + { id: 'lu-nuanlu', class: 'neutral', name: '暖炉', element: '火', rarity: '普通', cost: 1, type: '御', effects: { block: { n: 3 }, heal: { n: 1 } }, text: '获得 3 点护甲,回复 1 点气血。' }, + { id: 'lu-fengsui', class: 'neutral', name: '烽燧急报', element: '火', rarity: '罕见', cost: 2, type: '攻', effects: { dmg: { n: 7, times: 1 }, burn: { n: 4 } }, text: '造成 7 点伤害,施加 4 层灼。' }, + { id: 'lu-tunang', class: 'neutral', name: '土囊', element: '土', rarity: '普通', cost: 1, type: '御', effects: { block: { n: 4 } }, text: '获得 4 点护甲。' }, + { id: 'lu-shipan', class: 'neutral', name: '罗盘石', element: '土', rarity: '普通', cost: 1, type: '术', effects: { block: { n: 2 }, draw: { n: 1 } }, text: '获得 2 点护甲,抽 1 张牌。' }, + { id: 'lu-zhenwen', class: 'neutral', name: '镇纹符', element: '土', rarity: '普通', cost: 2, type: '御', effects: { block: { n: 6 }, vulnerable: { n: 1 } }, text: '获得 6 点护甲,施加 1 回合易伤。' }, + { id: 'lu-anying', class: 'neutral', name: '安营桩', element: '土', rarity: '罕见', cost: 2, type: '御', effects: { block: { n: 5 }, toughness: { n: 1 } }, text: '获得 5 点护甲与 1 点韧。' }, + { id: 'lu-xiaodao', class: 'neutral', name: '行囊小刀', element: '金', rarity: '普通', cost: 1, type: '攻', effects: { dmg: { n: 7, times: 1 } }, text: '出刀,造成 7 点伤害。' }, + { id: 'lu-moshi', class: 'neutral', name: '磨石', element: '金', rarity: '普通', cost: 1, type: '术', effects: { strength: { n: 1 }, block: { n: 2 } }, text: '获得 1 点力与 2 点护甲。' }, + { id: 'lu-lingdang', class: 'neutral', name: '惊鸟铃', element: '金', rarity: '普通', cost: 1, type: '术', effects: { weak: { n: 2 } }, text: '施加 2 回合虚弱。' }, + { id: 'lu-duanzhen', class: 'neutral', name: '断阵刀', element: '金', rarity: '罕见', cost: 2, type: '攻', effects: { dmg: { n: 9, times: 1 }, weak: { n: 1 } }, text: '造成 9 点伤害,施加 1 回合虚弱。' }, + { id: 'lu-shuinang', class: 'neutral', name: '皮水囊', element: '水', rarity: '普通', cost: 1, type: '术', effects: { heal: { n: 2 }, draw: { n: 1 } }, text: '回复 2 点气血,抽 1 张牌。' }, + { id: 'lu-suoyi', class: 'neutral', name: '蓑衣斗笠', element: '水', rarity: '普通', cost: 1, type: '御', effects: { block: { n: 4 } }, text: '获得 4 点护甲。' }, + { id: 'lu-yudi', class: 'neutral', name: '雨笛', element: '水', rarity: '普通', cost: 2, type: '术', effects: { draw: { n: 2 }, weak: { n: 1 } }, text: '抽 2 张牌,施加 1 回合虚弱。' }, + { id: 'lu-luopan', class: 'neutral', name: '水罗盘', element: '水', rarity: '罕见', cost: 1, type: '术', effects: { draw: { n: 1 }, costChainDiscount: { n: 1 }, block: { n: 2 } }, text: '抽 1 张牌,获得 2 点护甲;若延续灵脉,费用 -1。' }, + { id: 'lu-xingtu', class: 'neutral', name: '荧惑星图', element: '火', rarity: '罕见', cost: 2, type: '术', effects: { draw: { n: 3 } }, text: '抽 3 张牌。' }, ]; +/** class 标定收尾(M3 S13):数组前 66 张=M0–M2 存量职业甲卡,统一补 class:'jia'(乙/中立卡对象自带)。 */ +for (const c of CARDS) { if (!c.class) c.class = 'jia'; } + /** M0 教学战卡组:首批 20 卡各 1 份(§11 M0 交付;M0 单战路与回归测试继续用它,字面不动)。 */ export const STARTER_DECK = CARDS.slice(0, 20).map((c) => c.id); /** M1 一局起始卡组(卡组下限 10,§3.2;全普通、五行齐备;攻 5/御 2/术 3 的谱系标准起手配比—— * 首版 攻3 使贪心 driver 面对幕1 Boss dps 不足,回填提攻击密度〔草案〕)。 */ +/** 职业表(M3 S13;书 §5.2 双职业:甲=攻优 HP72 / 乙=链优 HP66;§6.2 数值锚)。 + * 卡池过滤口径:职业池 = class===本职业 + class==='neutral'(中立); + * CARDS 里 class 缺省=甲(M2 存量 66 张,文件尾统一标定);乙/中立卡对象自带 class。 */ +export const CLASSES = { + // starterRelic(M3 S14 平衡轮):职业起始遗物——谱系标准结构;乙=河图残卷(链自带 1 环,身份件), + // 甲=开明兽牙(战始力 1)。乙 HP 66→68:链优税在贪心档无镜像收益,首轮 16.1% 回填〔草案〕。 + jia: { id: 'jia', name: '巡罡使', hpMax: 72, starterDeckKey: 'jia', starterRelic: 'kaiming-ya' }, // 攻优(M0–M2 主角) + yi: { id: 'yi', name: '璇玑使', hpMax: 70, starterDeckKey: 'yi', starterRelic: 'hetu-juan' }, // 链优(M3 S13;66→68→70 平衡轮两轮回填:A15 尾段乙贪心 3.1% 出带的最后一格〔草案〕) +}; + +/** 乙(璇玑使)起始卡组(链优配比 攻3/御2/术5;全普通、五行齐备〔草案〕)。 */ +export const YI_STARTER_DECK = [ + // S14 平衡轮:①shenshang/jianglou→zhongshan/yaochi(16.1%→52.7%);②曾试 ximu→yinghuo 抬 A15, + // 灼起手全线 +15pp 冲破 A0/A5 上限,撤回——尾段缺口改走进阶表全局微软化(A0 不吃修正,零涟漪)〔草案〕 + 'yi-mu-luoteng', 'yi-mu-ximu', + 'yi-huo-zhuming', 'yi-huo-zhulou', + 'yi-tu-wuse', 'yi-tu-jishi', + 'yi-jin-changgeng', 'yi-jin-zhongshan', + 'yi-shui-beidou', 'yi-shui-yaochi', +]; + export const RUN_STARTER_DECK = [ 'mu-qingteng', 'mu-cangyin', 'mu-shengfa', 'huo-lihuo', @@ -370,6 +489,7 @@ export const MAP = { restFloor: 5, // 憩点固定层(单节点) bossFloor: 6, // Boss 层(单节点) shopFloors: [2, 3, 4], // M2 S11:商店保底可落层(每幕恰 1,从仍为 battle 的节点改型) + eventsPerAct: 2, // M3 S16:每幕事件节点数(1–4 层 battle 改型;候选 ≤2 时保战斗密度让位)〔草案〕 // 幕缩放(逐幕表,普通/精英吃、Boss 定幕直调不吃):书 §6.2 锚「幕间 ×2.2」以玩家引擎 // 幕2末 ×4–5 为前提(职业全量卡池/升级面=M2 S9+)。M1 玩家成长面只有加卡+3 遗物, // 首轮仿真回填(贪心 driver ×20)三轮迭代:2.2^n → 1.8^n → 1.4^n 均团灭,幂次复利是主凶, @@ -453,13 +573,297 @@ export const VALUATION = { hardCap: 1.5, // 离谱线(单测硬线,超过=表失误) }; + +/** + * 事件表(M3 S16;书 §5.2:45 = 幕池 12×3 + 通用 9。低成本内容密度主源 §3.2: + * 一段文字+两三个选项+一个代价交换)。acts:适用幕(null=通用池)。 + * 选项 fx 词汇(run-core 唯一消费方,收敛七词): + * gold{n}(±金)/ heal{n}(回血)/ loseHp{n}(掉血,不吃甲)/ hpMax{n}(上限±,现血同步)/ + * relic:true(随机未持有遗物)/ card{rarity}(随机该稀有度职业池卡入组)/ removeRandomCommon:true(随机删一张普通,组>下限才生效) + * 数值全部〔草案·S14 平衡轮回填〕。 + */ +export const EVENTS = [ + // ── 幕 1 池(12)── + { id: 'ev-qiaofu', name: '迷途樵夫', acts: [1], text: '山道旁,樵夫指着两条岔路,愿以酒钱换你带他一程。', options: [ + { label: '带他走大路(得 25 金)', fx: { gold: 25 } }, + { label: '抄小路(失 6 气血,得 45 金)', fx: { loseHp: 6, gold: 45 } }, + ] }, + { id: 'ev-gubei', name: '斑驳古碑', acts: [1], text: '碑文半没于苔藓,依稀是前代巡使的手记。', options: [ + { label: '拓印研读(得罕见符)', fx: { card: '罕见' } }, + { label: '清整碑座(回复 8 气血)', fx: { heal: 8 } }, + ] }, + { id: 'ev-shanci', name: '山神小祠', acts: [1], text: '荒祠无人,香炉尚温。', options: [ + { label: '上香(花 20 金,回复 12 气血)', fx: { gold: -20, heal: 12 } }, + { label: '搜刮供品(得 30 金,失 4 气血)', fx: { gold: 30, loseHp: 4 } }, + { label: '离开', fx: {} }, + ] }, + { id: 'ev-shoushang', name: '受伤的异兽', acts: [1], text: '一只幼兽困在猎户的陷阱里,兽目含泪。', options: [ + { label: '救它(失 5 气血,得遗物)', fx: { loseHp: 5, relic: true } }, + { label: '绕开', fx: {} }, + ] }, + { id: 'ev-caiyao', name: '采药人', acts: [1], text: '背篓里的灵草还带着露水,采药人愿意匀你一株。', options: [ + { label: '买灵草(花 30 金,气血上限 +4)', fx: { gold: -30, hpMax: 4 } }, + { label: '闲聊几句(回复 5 气血)', fx: { heal: 5 } }, + ] }, + { id: 'ev-yeyu', name: '夜雨破庙', acts: [1], text: '雨夜借宿,庙中壁画里的兽影似乎会动。', options: [ + { label: '守夜观画(得普通符)', fx: { card: '普通' }, }, { label: '倒头就睡(回复 10 气血)', fx: { heal: 10 } }, + ] }, + { id: 'ev-duanjian', name: '断剑残冢', acts: [1], text: '剑冢无主,断剑上残留符纹。', options: [ + { label: '拔剑(失 8 气血,得稀有符)', fx: { loseHp: 8, card: '稀有' } }, + { label: '祭拜后离开(得 15 金)', fx: { gold: 15 } }, + ] }, + { id: 'ev-shanxi', name: '山溪淘金', acts: [1], text: '溪水里闪着碎金,也漂着不明的红藻。', options: [ + { label: '下水淘金(得 35 金,失 5 气血)', fx: { gold: 35, loseHp: 5 } }, + { label: '取水解渴(回复 6 气血)', fx: { heal: 6 } }, + ] }, + { id: 'ev-mubei2', name: '无名坟茔', acts: [1], text: '坟前供着半坛酒,碑上无字。', options: [ + { label: '添土祭拜(随机销一张普通符,心境澄明)', fx: { removeRandomCommon: true } }, + { label: '饮那半坛酒(回复 8 气血,失 15 金)', fx: { heal: 8, gold: -15 } }, + ] }, + { id: 'ev-luying', name: '猎户营地', acts: [1], text: '猎户们围着篝火分食烤肉,招手唤你同坐。', options: [ + { label: '同坐分食(回复 10 气血)', fx: { heal: 10 } }, + { label: '换购兽皮(花 25 金,得罕见符)', fx: { gold: -25, card: '罕见' } }, + ] }, + { id: 'ev-tongren', name: '同道巡使', acts: [1], text: '一位同门巡使正整理行囊,准备下山。', options: [ + { label: '交换心得(得普通符)', fx: { card: '普通' } }, + { label: '接济盘缠(失 20 金,气血上限 +3)', fx: { gold: -20, hpMax: 3 } }, + { label: '拱手道别', fx: {} }, + ] }, + { id: 'ev-guquan', name: '枯泉眼', acts: [1], text: '泉眼将枯,一尾小鱼在浅洼里挣扎。', options: [ + { label: '放生小鱼(得 20 金——鱼衔来的谢礼)', fx: { gold: 20 } }, + { label: '疏通泉眼(失 4 气血,回复上限 +4)', fx: { loseHp: 4, hpMax: 4 } }, + ] }, + // ── 幕 2 池(12)── + { id: 'ev-shiqiao', name: '石桥赌局', acts: [2], text: '桥头老者摆着三只倒扣的碗,笑而不语。', options: [ + { label: '押 30 金(五五之数,得 70 或血本无归——按天机定)', fx: { gold: 40 }, weightNote: '简化:期望正,定额结算' }, + { label: '不赌,付 10 金过桥', fx: { gold: -10 } }, + ] }, + { id: 'ev-yunlou', name: '云中楼阁', acts: [2], text: '半山云雾里浮着一座楼阁,梯口悬着铜铃。', options: [ + { label: '登楼(失 10 气血,得稀有符)', fx: { loseHp: 10, card: '稀有' } }, + { label: '摇铃即走(得 25 金)', fx: { gold: 25 } }, + ] }, + { id: 'ev-qimiao', name: '弃庙藏经', acts: [2], text: '经匣上的封条写着「勿启」。', options: [ + { label: '开匣(得罕见符,失 6 气血)', fx: { card: '罕见', loseHp: 6 } }, + { label: '原样供回(回复 8 气血)', fx: { heal: 8 } }, + ] }, + { id: 'ev-yaoshi', name: '妖市夜集', acts: [2], text: '雾里灯笼次第亮起,摊主们都戴着兽面。', options: [ + { label: '以血易宝(失 8 气血,得遗物)', fx: { loseHp: 8, relic: true } }, + { label: '以金易符(花 35 金,得罕见符)', fx: { gold: -35, card: '罕见' } }, + { label: '速速离开', fx: {} }, + ] }, + { id: 'ev-hanyu', name: '寒玉矿洞', acts: [2], text: '洞壁寒玉莹莹,凿声在深处回响。', options: [ + { label: '采玉(得 50 金,失 7 气血)', fx: { gold: 50, loseHp: 7 } }, + { label: '在洞口打坐(回复 12 气血)', fx: { heal: 12 } }, + ] }, + { id: 'ev-duanya', name: '断崖索道', acts: [2], text: '索道年久失修,崖底云海翻涌。', options: [ + { label: '走索道(失 9 气血,拾得 40 金)', fx: { loseHp: 9, gold: 40 } }, + { label: '绕远路(体力消耗,失 3 气血)', fx: { loseHp: 3 } }, + ] }, + { id: 'ev-guishu', name: '归墟商人', acts: [2], text: '商人自称来自海眼归墟,货物泛着咸腥的光。', options: [ + { label: '倾囊购宝(花 60 金,得遗物)', fx: { gold: -60, relic: true } }, + { label: '只买小件(花 25 金,得罕见符)', fx: { gold: -25, card: '罕见' } }, + ] }, + { id: 'ev-wushi', name: '巫祝问卜', acts: [2], text: '巫祝掷出龟甲,卦象晦暗不明。', options: [ + { label: '求解厄(花 30 金,随机销一张普通符)', fx: { gold: -30, removeRandomCommon: true } }, + { label: '求康健(花 30 金,气血上限 +5)', fx: { gold: -30, hpMax: 5 } }, + { label: '不问天命', fx: {} }, + ] }, + { id: 'ev-canjuan', name: '残卷飘零', acts: [2], text: '风里飘来半页符经,字迹被雨水洇开。', options: [ + { label: '拾起研读(得普通符)', fx: { card: '普通' } }, + { label: '任它去(心无挂碍,回复 6 气血)', fx: { heal: 6 } }, + ] }, + { id: 'ev-shou-qun', name: '兽群迁徙', acts: [2], text: '兽群踏过山谷,扬尘蔽日,队伍里有落单的幼崽。', options: [ + { label: '护送幼崽归群(失 6 气血,得遗物)', fx: { loseHp: 6, relic: true } }, + { label: '拾荒(兽群踩落的杂物,得 30 金)', fx: { gold: 30 } }, + ] }, + { id: 'ev-tishen', name: '替身草人', acts: [2], text: '道旁草人穿着巡使的旧袍,眉目被雨打得模糊。', options: [ + { label: '换上新袍(失 20 金,气血上限 +4)', fx: { gold: -20, hpMax: 4 } }, + { label: '取走旧符(得普通符,失 3 气血)', fx: { card: '普通', loseHp: 3 } }, + ] }, + { id: 'ev-yinquan', name: '饮泉辨毒', acts: [2], text: '双泉并涌,一甘一苦,苔色却相反。', options: [ + { label: '饮甘泉(回复 14 气血)', fx: { heal: 14 } }, + { label: '饮苦泉(失 5 气血,气血上限 +6)', fx: { loseHp: 5, hpMax: 6 } }, + ] }, + // ── 幕 3 池(12)── + { id: 'ev-guixu-yan', name: '归墟之眼', acts: [3], text: '海眼漩涡低鸣,万物之水在此归还。', options: [ + { label: '投金问渊(失 50 金,得遗物)', fx: { gold: -50, relic: true } }, + { label: '汲一瓢归墟水(失 8 气血,气血上限 +8)', fx: { loseHp: 8, hpMax: 8 } }, + ] }, + { id: 'ev-tianti', name: '天梯残基', acts: [3], text: '建木折断处只余残基,直上云霄的路早已不通。', options: [ + { label: '攀残基采灵材(失 10 气血,得稀有符)', fx: { loseHp: 10, card: '稀有' } }, + { label: '在基座冥思(回复 15 气血)', fx: { heal: 15 } }, + ] }, + { id: 'ev-zhouye', name: '昼夜倒悬', acts: [3], text: '此地烛阴的余息未散,日月同悬,影子朝着两个方向。', options: [ + { label: '借阴阳淬体(失 12 气血,气血上限 +10)', fx: { loseHp: 12, hpMax: 10 } }, + { label: '速速穿过(失 4 气血)', fx: { loseHp: 4 } }, + ] }, + { id: 'ev-wangchuan', name: '忘川渡口', acts: [3], text: '摆渡人不收金银,只收「记忆」。', options: [ + { label: '交出一段记忆(随机销一张普通符,回复 12 气血)', fx: { removeRandomCommon: true, heal: 12 } }, + { label: '泅渡(失 10 气血)', fx: { loseHp: 10 } }, + ] }, + { id: 'ev-fengshen', name: '封神残坛', acts: [3], text: '坛上符阵尚有余温,似乎还封着什么。', options: [ + { label: '续燃符阵(花 40 金,得遗物)', fx: { gold: -40, relic: true } }, + { label: '拆阵取材(得 45 金,失 6 气血)', fx: { gold: 45, loseHp: 6 } }, + ] }, + { id: 'ev-jinglong', name: '惊龙一瞥', acts: [3], text: '云层裂开一瞬,你与某只巨大的眼睛对视了。', options: [ + { label: '不退反进(失 10 气血,得稀有符)', fx: { loseHp: 10, card: '稀有' } }, + { label: '垂首避让(得 20 金——龙鳞落地成金)', fx: { gold: 20 } }, + ] }, + { id: 'ev-xumi', name: '须弥芥子', acts: [3], text: '一粒芥子悬在半空,里面隐约有山河。', options: [ + { label: '吞下芥子(失 8 气血,气血上限 +8)', fx: { loseHp: 8, hpMax: 8 } }, + { label: '装入行囊(得 55 金)', fx: { gold: 55 } }, + ] }, + { id: 'ev-canglong', name: '苍龙衔烛', acts: [3], text: '烛火自天而降,落在你的掌心不灭。', options: [ + { label: '并入符箓(得稀有符)', fx: { card: '稀有' } }, + { label: '借火疗伤(回复 16 气血)', fx: { heal: 16 } }, + ] }, + { id: 'ev-shanhaitu', name: '山海图残角', acts: [3], text: '图上兽形游走,一角空白正等着新的名字。', options: [ + { label: '以血描图(失 10 气血,得遗物)', fx: { loseHp: 10, relic: true } }, + { label: '收起残角(得 40 金)', fx: { gold: 40 } }, + ] }, + { id: 'ev-kunpeng', name: '鲲鹏过境', acts: [3], text: '天色骤暗,巨翼掠过,落下几枚风羽。', options: [ + { label: '拾羽炼符(得罕见符)', fx: { card: '罕见' } }, + { label: '乘风歇息(回复 12 气血)', fx: { heal: 12 } }, + ] }, + { id: 'ev-mingzhong', name: '暝钟自鸣', acts: [3], text: '古钟无人自鸣,声波荡开处尘埃浮空。', options: [ + { label: '以掌抵钟(失 8 气血,随机销一张普通符)', fx: { loseHp: 8, removeRandomCommon: true } }, + { label: '静听钟纹(得普通符)', fx: { card: '普通' } }, + ] }, + { id: 'ev-xingchen', name: '星辰坠地', acts: [3], text: '一枚星子砸出浅坑,余温灼人。', options: [ + { label: '徒手取星(失 9 气血,得 60 金)', fx: { loseHp: 9, gold: 60 } }, + { label: '围坑取暖(回复 10 气血)', fx: { heal: 10 } }, + ] }, + // ── 通用池(9;任意幕)── + { id: 'ev-xingjiao', name: '行脚商人', acts: null, text: '骡铃叮当,商人打开包袱:「都是山里淘换来的。」', options: [ + { label: '买符(花 28 金,得罕见符)', fx: { gold: -28, card: '罕见' } }, + { label: '卖旧物(得 22 金)', fx: { gold: 22 } }, + ] }, + { id: 'ev-gouhuo', name: '无主篝火', acts: null, text: '火堆还旺,旁边摆着烤好的干粮,主人却不见踪影。', options: [ + { label: '吃了再说(回复 9 气血)', fx: { heal: 9 } }, + { label: '添柴等主人(得 15 金——归来的猎人相谢)', fx: { gold: 15 } }, + ] }, + { id: 'ev-shanhong', name: '山洪断路', acts: null, text: '浊浪卷着断木冲下山谷,吊桥摇摇欲坠。', options: [ + { label: '抢渡(失 7 气血)', fx: { loseHp: 7 } }, + { label: '等水退(错过商机,失 15 金)', fx: { gold: -15 } }, + ] }, + { id: 'ev-qishou', name: '弃兽幼崽', acts: null, text: '兽穴空了,只剩一只幼崽蜷在枯草里。', options: [ + { label: '喂食安置(失 12 金,回复 6 气血——心安)', fx: { gold: -12, heal: 6 } }, + { label: '带去山神祠(得 18 金——祠祝的谢仪)', fx: { gold: 18 } }, + ] }, + { id: 'ev-duanfu', name: '断符残页', acts: null, text: '半张符纸粘在岩壁上,符胆尚存。', options: [ + { label: '补全符纸(得普通符)', fx: { card: '普通' } }, + { label: '取符胆换钱(得 20 金)', fx: { gold: 20 } }, + ] }, + { id: 'ev-jiuweng', name: '埋酒老翁', acts: null, text: '老翁在树下埋酒:「十年后再来挖。」他看了看你:「或者现在就喝。」', options: [ + { label: '对饮(回复 11 气血)', fx: { heal: 11 } }, + { label: '帮他埋酒(得 16 金)', fx: { gold: 16 } }, + ] }, + { id: 'ev-shibei', name: '试胆石碑', acts: null, text: '碑上刻着:「敢者留名。」碑下散落着前人的供钱。', options: [ + { label: '留名取钱(得 32 金,失 5 气血)', fx: { gold: 32, loseHp: 5 } }, + { label: '拱手离开', fx: {} }, + ] }, + { id: 'ev-yishi', name: '遗失的行囊', acts: null, text: '行囊挂在枝头,里面有干粮和一封没寄出的家书。', options: [ + { label: '物归原主(得 25 金——失主的谢礼)', fx: { gold: 25 } }, + { label: '取干粮充饥(回复 8 气血)', fx: { heal: 8 } }, + ] }, + { id: 'ev-wenquan', name: '野汤温泉', acts: null, text: '硫磺味的热气从石缝里涌出来。', options: [ + { label: '泡个痛快(回复 13 气血)', fx: { heal: 13 } }, + { label: '取泉华卖钱(得 24 金)', fx: { gold: 24 } }, + ] }, +]; + + +/** + * 成就表(M3 S15;书 §5.2 成就 40。声明式条件,meta-core 解释器消费——代码零成就逻辑字面量): + * winClass{cls} 用某职业通关 / winAscension{n} 任职业通关 An / winBoth 双职业通关 / + * wins{n} 累计通关数 / runs{n} 累计局数 / deckMaxAtWin{n} 胜局卡组 ≤n / deckMinAtWin{n} 胜局卡组 ≥n / + * hpFullWin 满血通关 / goldAtWin{n} 通关时金 ≥n / removeTotal{n} 累计删卡 / zhoutianRun{n} 单局周天 ≥n / + * collectCards{n} 图鉴卡 ≥n / collectRelics{n} 图鉴遗物 ≥n / collectEnemies{n} 图鉴敌兽 ≥n / + * relicsAtWin{n} 通关时遗物 ≥n / winTurnsUnder{n} 通关总回合 ≤n / dailyPlayed 每日首战 / dailyScore{n} 每日分 ≥n + */ +export const ACHIEVEMENTS = [ + { id: 'ach-first-win', name: '初巡功成', cond: { kind: 'wins', n: 1 }, text: '首次通关三幕。' }, + { id: 'ach-win-jia', name: '罡气纵横', cond: { kind: 'winClass', cls: 'jia' }, text: '用巡罡使通关。' }, + { id: 'ach-win-yi', name: '璇玑妙算', cond: { kind: 'winClass', cls: 'yi' }, text: '用璇玑使通关。' }, + { id: 'ach-win-both', name: '双璧巡天', cond: { kind: 'winBoth' }, text: '两位巡使都完成通关。' }, + { id: 'ach-wins-5', name: '五巡不怠', cond: { kind: 'wins', n: 5 }, text: '累计通关 5 局。' }, + { id: 'ach-wins-10', name: '十巡功德', cond: { kind: 'wins', n: 10 }, text: '累计通关 10 局。' }, + { id: 'ach-wins-25', name: '巡遍山海', cond: { kind: 'wins', n: 25 }, text: '累计通关 25 局。' }, + { id: 'ach-runs-10', name: '行者无疆', cond: { kind: 'runs', n: 10 }, text: '出巡 10 局。' }, + { id: 'ach-runs-30', name: '山海常客', cond: { kind: 'runs', n: 30 }, text: '出巡 30 局。' }, + { id: 'ach-runs-100', name: '百巡老手', cond: { kind: 'runs', n: 100 }, text: '出巡 100 局。' }, + { id: 'ach-asc1', name: '初登异阶', cond: { kind: 'winAscension', n: 1 }, text: '通关进阶一。' }, + { id: 'ach-asc3', name: '三阶不惧', cond: { kind: 'winAscension', n: 3 }, text: '通关进阶三。' }, + { id: 'ach-asc5', name: '五阶巡异', cond: { kind: 'winAscension', n: 5 }, text: '通关进阶五。' }, + { id: 'ach-asc8', name: '八阶履险', cond: { kind: 'winAscension', n: 8 }, text: '通关进阶八。' }, + { id: 'ach-asc10', name: '十阶问天', cond: { kind: 'winAscension', n: 10 }, text: '通关进阶十。' }, + { id: 'ach-asc15', name: '倾覆亦渡', cond: { kind: 'winAscension', n: 15 }, text: '通关进阶十五。' }, + { id: 'ach-thin-deck', name: '轻装简从', cond: { kind: 'deckMaxAtWin', n: 12 }, text: '以不超过 12 张的卡组通关。' }, + { id: 'ach-fat-deck', name: '有备无患', cond: { kind: 'deckMinAtWin', n: 30 }, text: '以 30 张以上的卡组通关。' }, + { id: 'ach-full-hp', name: '毫发无损', cond: { kind: 'hpFullWin' }, text: '满血状态下通关。' }, + { id: 'ach-rich', name: '腰缠万贯', cond: { kind: 'goldAtWin', n: 300 }, text: '通关时持金 300 以上。' }, + { id: 'ach-remove-10', name: '断舍离', cond: { kind: 'removeTotal', n: 10 }, text: '累计销卡 10 张。' }, + { id: 'ach-remove-30', name: '大道至简', cond: { kind: 'removeTotal', n: 30 }, text: '累计销卡 30 张。' }, + { id: 'ach-zhoutian-3', name: '周天三转', cond: { kind: 'zhoutianRun', n: 3 }, text: '单局触发周天 3 次。' }, + { id: 'ach-zhoutian-6', name: '周天六合', cond: { kind: 'zhoutianRun', n: 6 }, text: '单局触发周天 6 次。' }, + { id: 'ach-relics-6', name: '异宝盈袖', cond: { kind: 'relicsAtWin', n: 6 }, text: '通关时持有 6 枚以上遗物。' }, + { id: 'ach-relics-9', name: '百宝行囊', cond: { kind: 'relicsAtWin', n: 9 }, text: '通关时持有 9 枚以上遗物。' }, + { id: 'ach-fast-win', name: '兵贵神速', cond: { kind: 'winTurnsUnder', n: 70 }, text: '总回合数不超过 70 通关。' }, + { id: 'ach-cards-40', name: '符箓初集', cond: { kind: 'collectCards', n: 40 }, text: '图鉴收录 40 种符箓。' }, + { id: 'ach-cards-80', name: '符箓半壁', cond: { kind: 'collectCards', n: 80 }, text: '图鉴收录 80 种符箓。' }, + { id: 'ach-cards-120', name: '符海拾贝', cond: { kind: 'collectCards', n: 120 }, text: '图鉴收录 120 种符箓。' }, + { id: 'ach-cards-154', name: '万符归宗', cond: { kind: 'collectCards', n: 154 }, text: '图鉴收录全部 154 种符箓。' }, + { id: 'ach-relics-20', name: '鉴宝行家', cond: { kind: 'collectRelics', n: 20 }, text: '图鉴收录 20 枚遗物。' }, + { id: 'ach-relics-40', name: '宝录大成', cond: { kind: 'collectRelics', n: 40 }, text: '图鉴收录全部 40 枚遗物。' }, + { id: 'ach-enemies-4', name: '初识异类', cond: { kind: 'collectEnemies', n: 4 }, text: '图鉴收录 4 种异兽。' }, + { id: 'ach-enemies-8', name: '兽谱全览', cond: { kind: 'collectEnemies', n: 8 }, text: '图鉴收录全部 8 种异兽。' }, + { id: 'ach-daily-first', name: '每日一巡', cond: { kind: 'dailyPlayed' }, text: '完成一次每日一局。' }, + { id: 'ach-daily-1000', name: '当日魁首', cond: { kind: 'dailyScore', n: 1000 }, text: '每日一局得分 1000 以上。' }, + { id: 'ach-daily-1500', name: '名动山海', cond: { kind: 'dailyScore', n: 1500 }, text: '每日一局得分 1500 以上。' }, + { id: 'ach-asc-unlock-5', name: '阶梯行者', cond: { kind: 'ascUnlocked', n: 5 }, text: '任一职业解锁进阶五。' }, + { id: 'ach-asc-unlock-10', name: '阶梯尽头', cond: { kind: 'ascUnlocked', n: 10 }, text: '任一职业解锁进阶十。' }, +]; + +/** 每日一局常量(M3 S15;书 §5.1 ⑭:全服同种子、本地最佳分)。 */ +export const DAILY = { + // 分数公式常量(〔草案〕):胜=基石 1000 + 余血×10 + 金×1 - 总回合×2;负=战数×40 + 幕×100。 + winBase: 1000, hpWeight: 10, goldWeight: 1, turnPenalty: 2, battleWeight: 40, actWeight: 100, +}; + +/** + * 进阶修正表(M3 S14;书 §3.2「进阶阶梯」:逐级叠加负面修正,每级独立成表、可组合、累积生效—— + * AN = A1..AN 全部 mods 叠加;A0=无修正基线。数值全部〔草案·S14 平衡轮回填〕。 + * mods 词汇(run-core 聚合消费;Pct 键=百分比累加后一次乘,Delta 键=加区累加): + * enemyAtkPct/enemyHpPct 普通+精英 / eliteHpPct 精英血 / bossHpPct·bossAtkPct Boss / + * elitesPerActDelta 每幕精英配额 / restHealPctDelta 憩点回复比例 / startHpDelta 起始 HP / + * goldPct 战斗金币 / shopPricePct 商店与删卡价 / winHealDelta 战胜回复 / firstTurnEnemyStrength 敌开战力 / + * rewardChoicesDelta 战斗奖励选项数增减(A15:构筑面收紧轴——战斗压力面 A13 已到顶,尾级换轴) + */ +export const ASCENSIONS = [ + { level: 1, text: '异动加剧:每幕精英 +1', mods: { elitesPerActDelta: 1 } }, + { level: 2, text: '兽爪更利:敌攻 +7%', mods: { enemyAtkPct: 7 } }, + { level: 3, text: '兽躯更韧:敌血 +7%', mods: { enemyHpPct: 7 } }, + { level: 4, text: '灵泉枯涩:憩点回复 -12%', mods: { restHealPctDelta: -0.12 } }, + { level: 5, text: '镇主凝形:首领血 +10%', mods: { bossHpPct: 10 } }, + { level: 6, text: '披挂出巡:起始气血 -6', mods: { startHpDelta: -6 } }, + { level: 7, text: '山货歉收:战斗金币 -10%', mods: { goldPct: -10 } }, + { level: 8, text: '兽爪愈利:敌攻再 +6%', mods: { enemyAtkPct: 6 } }, + { level: 9, text: '市价腾贵:商店与销卡价 +15%', mods: { shopPricePct: 15 } }, + { level: 10, text: '兽躯愈韧:敌血再 +4%', mods: { enemyHpPct: 4 } }, + { level: 11, text: '血气难续:战胜回复 -3', mods: { winHealDelta: -3 } }, + { level: 12, text: '精怪成群:精英血 +8%', mods: { eliteHpPct: 8 } }, + { level: 13, text: '镇主暴怒:首领攻 +6%', mods: { bossAtkPct: 6 } }, + { level: 14, text: '灵泉近涸:憩点回复再 -8%', mods: { restHealPctDelta: -0.08 } }, + { level: 15, text: '山海倾覆:机缘稀薄,战斗奖励改为二选一', mods: { rewardChoicesDelta: -1 } }, +]; + /** * run 存档常量(M2 S11;形状=contracts/sdk-interface.d.ts SaveEnvelope:{schemaVersion≥1, data 纯对象}, * 键族='save:'+gameId+':'+versionId、上限 65536 UTF-16 码元、宿主 fail-closed——W-SAVE-CAP 已入库)。 * M2 介质=影子(localStorage 直写/内存),正式 SDK Storage 通道切换随集成段(书 §12④,M5 硬门)。 */ export const SAVE = { - schemaVersion: 1, // 存档版本(信封 schemaVersion;表结构破坏性变更时升并配迁移) + schemaVersion: 2, // v2(M3 S15):data={run,meta} 双域;v1(M2,data=run 平铺)经迁移链升入——首个真实迁移 shadowKey: 'save:shanhai-xunyilu:dev', // 影子键(正式 versionId 随发布集成段注入) maxChars: 65536, // save: 族上限(UTF-16 码元;契约口径) }; diff --git a/tier2/games/shanhai-xunyilu/src/main.js b/tier2/games/shanhai-xunyilu/src/main.js index 91169f0f..127daab7 100644 --- a/tier2/games/shanhai-xunyilu/src/main.js +++ b/tier2/games/shanhai-xunyilu/src/main.js @@ -1,93 +1,164 @@ /** * src/main.js —— 《山海巡异录》入口(A3 entry · PhaserGameFactory 默认导出) - * owner:北极星顶级线③(M1;host 集成修复版) + * owner:北极星顶级线③(M1 host 修复版 → M2 存档续玩 → M3 S15 局外席:home/图鉴/成就/每日) * * 【装载契约】(tier2/contracts/boot-phaser-host.d.ts + boot-phaser-host.js;夜市款同构基准) - * 宿主(bootPhaserHost)自建唯一 Phaser.Game(canvas#tier2-game 挂 opts.parent),调本工厂建实例, - * boot(ctx) 收 { game, canvas, seed, assets, caps, recHook }: - * - real 通道:ctx.game=宿主 Phaser.Game → 本工厂把场景 add 进**宿主实例**并启动; - * - headless/node 通道:ctx.game=null → 只建纯逻辑核,不触渲染(node import-safe)。 + * 宿主(bootPhaserHost)自建唯一 Phaser.Game,调本工厂建实例,boot(ctx) 收 { game, canvas, seed, … }: + * real 通道把场景 add 进宿主实例;headless(ctx.game=null)只建逻辑核,node import-safe。 * - * 【红线 · M0 事故复盘】M0 版曾在 boot 里自建第二个 Phaser.Game 实例且未挂宿主 parent → - * 页面出现游离 canvas 渲到视口外、宿主可见画布全黑、真指针打不到游戏(mini-desktop CDP 取证坐实)。 - * 本版修法:**工厂永不自建引擎实例**(全文件零 Phaser import),场景一律挂 ctx.game; - * run-smoke.mjs S0 静态自查断言钉住此红线防回潮。 + * 【红线 · M0 事故】工厂永不自建引擎实例(全文件零 Phaser import);run-smoke S0 静态自查钉死。 * - * 【输入单一路径】injectInput('tap') 与场景真指针都走 route(x,y) → readState().tapTargets - * 命中序(布局层唯一坐标事实)→ run-core 语义动作。driver/CDP/真人三路同因果。 + * 【M3 模式机】mode='home'|'run':boot 后进 home(选职业/进阶/每日/图鉴/成就;meta 由存档 v2 恢复); + * home 屏 tapTargets 由本工厂产(局外交互不属于一局,不进 run-core);开局/续玩切 'run'。 + * 终局:恰一次 recordRunEnd(成就/解锁/每日分)→ 点遮罩回 home;meta 常存(信封 v2 run=null 仍写)。 + * + * 【输入单一路径】injectInput('tap') 与场景真指针都走 route(x,y) → tapTargets 命中序(home=工厂算, + * run=run-core/layout 算)→ 语义动作。driver/CDP/真人三路同因果。 */ 'use strict'; import { createRunCore } from './run-core.js'; +import { createMetaCore } from './meta-core.js'; import { createSeededRandom } from './util/seeded-random.js'; -import { hitTarget } from './ui/layout.js'; +import { hitTarget, computeHomeTargets } from './ui/layout.js'; import { createRunScene } from './scenes/run-scene.js'; -import { SAVE_KEY, makeEnvelope, restoreRun, serializeEnvelope } from './save.js'; +import { SAVE_KEY, makeEnvelope, restoreRun, restoreMeta, serializeEnvelope } from './save.js'; +import { ASCENSIONS } from './data/tables.js'; const SCENE_KEY = 'shanhai-run'; /** - * 影子存档介质(M2 S11 口径,src/save.js 文件头已申报):浏览器径直写 localStorage 同名键; - * headless/node 无 localStorage 则整段静默跳过(仿真与单测走 run.snapshot() 纯数据面,不经介质)。 - * 正式 SDK Storage 通道(postMessage storage 消息)切换随发布集成段(书 §12④,M5 门)。 + * 影子存档介质(M2 S11 口径;src/save.js 文件头申报):浏览器径直写 localStorage 同名键; + * headless/node 无 localStorage 整段静默跳过。正式 SDK Storage 切换随发布集成段(书 §12④,M5 门)。 */ const shadowStore = { read() { try { const raw = globalThis.localStorage && globalThis.localStorage.getItem(SAVE_KEY); - return raw ? JSON.parse(raw) : null; // 影子介质在游戏侧解析;正式通道由宿主受信边界解析(契约 StorageResultPayload) + return raw ? JSON.parse(raw) : null; // 影子介质游戏侧解析;正式通道由宿主受信边界解析 } catch (e) { return null; } // 坏 JSON/无介质 = 无档(fail-closed) }, write(envelope) { try { if (!globalThis.localStorage) return; const ser = serializeEnvelope(envelope); - if (!ser.ok) { console.error('[save] 存档超容弃写:', ser.chars); return; } // 超容响亮留痕,不静默截断 + if (!ser.ok) { console.error('[save] 存档超容弃写:', ser.chars); return; } // 响亮留痕,不静默截断 globalThis.localStorage.setItem(SAVE_KEY, ser.json); } catch (e) { /* 介质失败不阻游戏(存档是增强,不是主链路) */ } }, - clear() { - try { globalThis.localStorage && globalThis.localStorage.removeItem(SAVE_KEY); } catch (e) { /* 同上 */ } - }, }; /** * PhaserGameFactory:宿主调它建 PhaserGameInstance(boot/readState/injectInput/destroy)。 - * @param {Record} [factoryOpts] 工厂选项(M1 未用) */ export default function createGame(factoryOpts) { /** @type {ReturnType|null} */ let run = null; + /** @type {ReturnType} */ + let metaCore = createMetaCore(null); /** @type {any} 宿主 Phaser.Game(real 通道;headless 为 null)。 */ let hostGame = null; let seed = 12345; + /** 局外模式机(M3):home=局外(main/collection/achievements 三子屏),run=一局。 */ + let mode = 'home'; + let homeScreen = 'main'; + const homeSel = { class: 'jia', ascension: 0 }; + let isDaily = false; + let runEnded = false; // recordRunEnd 去重旗(每局恰一次) + /** 场景自证面(战斗全空事故增补):rebuild 回写 displayCount/rebuilds/screen;headless 恒 null。 */ + let sceneDebug = null; - /** 建/重建一局(seed 派生受控随机;重开局 seed+1 保持确定性谱系)。 */ - function newRun() { - run = createRunCore({ random: createSeededRandom(seed >>> 0) }, { seed: seed >>> 0 }); - } - function restart() { - seed = (seed + 1) >>> 0; - shadowStore.clear(); // 重开=旧档作废(单槽语义) - newRun(); + /** 开一局(职业/进阶/每日种子;M3 局外入口)。 */ + function startRun(cls, ascension, daily) { + isDaily = !!daily; + let runSeed = seed; + if (daily) { + runSeed = metaCore.dailySeedFor().seed; // 全服同种子(日期派生;榜单走平台既有能力) + } else { + seed = (seed + 1) >>> 0; // 普通局:种子步进(同会话重开不同局) + runSeed = seed; + } + run = createRunCore({ random: createSeededRandom(runSeed >>> 0) }, { seed: runSeed >>> 0, heroClass: cls, ascension }); + mode = 'run'; + runEnded = false; + persist(); } - /** 存档节拍:战斗外屏落定即快照(书 §3.4「战斗外任意节点杀进程重进」);终局清档。 */ + /** 终局收口(恰一次):meta 结算(成就/解锁/每日分)→ 存档(run 域清空,meta 常存)。 */ + function settleRunEnd() { + if (!run || runEnded) return; + const end = run.readState(); + if (end.phase === 'run') return; + runEnded = true; + metaCore.recordRunEnd(end, { zhoutianCount: (end.stats && end.stats.zhoutianTotal) || 0, isDaily }); + persist(); + } + + /** 存档节拍:home=只存 meta;run=战斗外屏快照+meta(battle 屏 makeEnvelope 内自然产 run:null—— + * 注意:battle 屏不覆写上一节点快照,跳过写)。 */ function persist() { - if (!run) return; - const s = run.readState(); - if (s.phase !== 'run') { shadowStore.clear(); return; } - if (s.screen === 'battle') return; // 战斗内不存:中断按上一节点快照续 - const env = makeEnvelope(run); - if (env) shadowStore.write(env); + if (mode === 'run' && run) { + const s = run.readState(); + if (s.phase !== 'run') { shadowStore.write(makeEnvelope(null, metaCore)); return; } // 终局:run 域清 + if (s.screen === 'battle') return; // 战斗内不写:保上一节点快照(中断回节点语义) + shadowStore.write(makeEnvelope(run, metaCore)); + } else { + shadowStore.write(makeEnvelope(null, metaCore)); + } + } + + /** home 屏投影(readState home 分支;tapTargets=computeHomeTargets 同源)。 */ + function homeState() { + const meta = metaCore.readMeta(); + const maxAsc = Math.max(0, Math.min(ASCENSIONS.length, meta.ascensionUnlocked[homeSel.class] || 0)); + if (homeSel.ascension > maxAsc) homeSel.ascension = maxAsc; + const daily = metaCore.dailySeedFor(); + const projection = { + schemaVersion: 2, + phase: 'home', + screen: homeScreen, + home: { + sel: { ...homeSel }, + maxAscension: maxAsc, + ascensions: ASCENSIONS.map((a) => ({ level: a.level, text: a.text })), + daily, + meta, + }, + debug: sceneDebug, + }; + projection.tapTargets = computeHomeTargets(projection); + return projection; + } + + /** home 屏语义路由(局外交互;工厂级)。 */ + function routeHome(t) { + if (t.kind === 'homeClass') { homeSel.class = t.class; return; } + if (t.kind === 'ascMinus') { homeSel.ascension = Math.max(0, homeSel.ascension - 1); return; } + if (t.kind === 'ascPlus') { + const meta = metaCore.readMeta(); + const maxAsc = meta.ascensionUnlocked[homeSel.class] || 0; + homeSel.ascension = Math.min(maxAsc, homeSel.ascension + 1); + return; + } + if (t.kind === 'homeStart') { startRun(homeSel.class, homeSel.ascension, false); return; } + if (t.kind === 'homeDaily') { startRun('jia', 0, true); return; } // 每日一局:固定甲 A0(全服同题面〔草案〕) + if (t.kind === 'homeCollection') { homeScreen = 'collection'; return; } + if (t.kind === 'homeAchievements') { homeScreen = 'achievements'; return; } + if (t.kind === 'homeBack') { homeScreen = 'main'; return; } } /** - * 语义路由:坐标 → tapTargets 命中(数组序=优先级)→ run-core 动作。 + * 语义路由:坐标 → tapTargets 命中(数组序=优先级)→ 语义动作。 * 真指针(场景 pointerdown)与 injectInput(driver/CDP)共用,输入因果单源。 */ function route(x, y) { + if (mode === 'home') { + const t = hitTarget(homeState().tapTargets, x, y); + if (t) routeHome(t); + persist(); + return; + } if (!run) return; const t = hitTarget(run.readState().tapTargets, x, y); if (!t) return; @@ -106,39 +177,41 @@ export default function createGame(factoryOpts) { else if (t.kind === 'removePick') run.buyRemove(t.index); else if (t.kind === 'removeBack') run.closeRemove(); else if (t.kind === 'shopLeave') run.leaveShop(); - else if (t.kind === 'overlay') restart(); - persist(); // 每次语义动作后按节拍存档(battle 屏内为 no-op,零热路径开销) + else if (t.kind === 'eventOption') run.chooseEventOption(t.index); // M3 事件 + else if (t.kind === 'overlay') { settleRunEnd(); mode = 'home'; homeScreen = 'main'; persist(); return; } // 终局遮罩:回局外 + // 巡览观测(图鉴收集,幂等)+ 终局结算 + 存档节拍。 + metaCore.observe(run.readState()); + settleRunEnd(); + persist(); } return { /** - * 一次性启动:接宿主 boot 上下文。real 通道把场景挂进宿主 Phaser.Game(装载契约红线: - * 不自建实例、不游离挂载);headless(ctx.game=null)只建逻辑核。 - * @param {{ game?: any, canvas?: HTMLCanvasElement|null, seed?: number }} ctx + * 一次性启动:接宿主 boot 上下文;M3=恢复 meta+续玩(run 域有档直接进局),否则进 home。 */ async boot(ctx) { seed = (ctx && ctx.seed != null ? ctx.seed : 12345) >>> 0; - // M2 S11 续玩:有合法影子档即恢复(fail-closed:坏档=开新局);?seed= 显式注入时视为要开新局。 const saved = shadowStore.read(); + metaCore = createMetaCore(saved ? restoreMeta(saved) : null); const resumed = saved ? restoreRun(saved) : null; - if (resumed) run = resumed; - else newRun(); + if (resumed) { run = resumed; mode = 'run'; runEnded = false; } + else { mode = 'home'; } hostGame = (ctx && ctx.game) || null; if (!hostGame) return true; // headless/node 通道(宿主契约:不真渲染) - // 场景挂宿主实例并自启(宿主 HostScene 常驻管 latch 轮询,本场景只做游戏渲染与输入)。 - hostGame.scene.add(SCENE_KEY, createRunScene({ run: () => run, route }), true); + sceneDebug = { displayCount: 0, rebuilds: 0, screen: null }; // 真渲染通道才有自证面 + hostGame.scene.add(SCENE_KEY, createRunScene({ run: () => run, mode: () => mode, homeState, route, debug: sceneDebug }), true); return true; }, - /** 语义状态(宿主逐帧轮询 phase 做 latch 门;直通 run-core,含 tapTargets)。 */ + /** 语义状态(宿主逐帧轮询 phase 做 latch 门;home 态=局外投影,run 态=run-core 直通+debug)。 */ readState() { - return run ? run.readState() : { phase: 'booting' }; + if (mode === 'home' || !run) return homeState(); + const s = run.readState(); + s.debug = sceneDebug; // 附加取证键(不进 run-core 状态树;场景签名不读它) + return s; }, - /** - * 归一化输入注入(driver/CDP 通道):{type:'tap',x,y} 走与真指针相同的语义路由。 - * @param {{type:string,x?:number,y?:number}} ev - */ + /** 归一化输入注入(driver/CDP 通道):与真指针同一语义路由。 */ injectInput(ev) { if (ev && ev.type === 'tap') route(ev.x || 0, ev.y || 0); }, diff --git a/tier2/games/shanhai-xunyilu/src/map-core.js b/tier2/games/shanhai-xunyilu/src/map-core.js index 2971c705..9a356a9d 100644 --- a/tier2/games/shanhai-xunyilu/src/map-core.js +++ b/tier2/games/shanhai-xunyilu/src/map-core.js @@ -23,7 +23,7 @@ import { MAP } from './data/tables.js'; * @param {number} act 幕号(1..MAP.acts) * @returns {{nodes:Array<{id:string,floor:number,idx:number,type:string}>, edges:Array<[string,string]>}} */ -export function generateActMap(rng, act) { +export function generateActMap(rng, act, genOpts = {}) { const nodes = []; const edges = []; const floors = []; @@ -46,7 +46,7 @@ export function generateActMap(rng, act) { nodes.push(...row); } - // ── 配额:精英每幕 ≥1(2–4 层随机一位);宝匣恰 1(3 层随机一位,且不与精英同节点)── + // ── 配额:精英每幕 ≥1(2–4 层随机一位)+ 进阶 A1 追加(M3 genOpts.extraElites);宝匣恰 1 ── const eliteFloor = MAP.eliteFloors[rng.range(0, MAP.eliteFloors.length - 1)]; const eliteRow = floors[eliteFloor]; const eliteIdx = rng.range(0, eliteRow.length - 1); @@ -86,6 +86,22 @@ export function generateActMap(rng, act) { cand[rng.range(0, cand.length - 1)].type = 'shop'; } + // ── 增益件配额(保底件之后按余量;96k 全矩阵 A1 实抓「事件+追加精英挤兑商店保底」的修序)── + // 事件节点(M3 S16:每幕 ≤2,1–4 层仍为 battle 的随机改型;候选 ≤2 让位保战斗密度)。 + for (let ev = 0; ev < (MAP.eventsPerAct || 0); ev++) { + const cand = []; + for (let f = 1; f <= 4; f++) floors[f].forEach((nd) => { if (nd.type === 'battle') cand.push(nd); }); + if (cand.length <= 2) break; + cand[rng.range(0, cand.length - 1)].type = 'event'; + } + // 进阶 A1 追加精英(M3 S14):2–4 层仍为 battle 的随机改型;无候选跳过(不挤占任何保底)。 + for (let extra = 0; extra < (genOpts.extraElites || 0); extra++) { + const cand = []; + for (const f of MAP.eliteFloors) floors[f].forEach((nd) => { if (nd.type === 'battle') cand.push(nd); }); + if (cand.length === 0) break; + cand[rng.range(0, cand.length - 1)].type = 'elite'; + } + // ── 连边(近邻,保证双向覆盖:上层每节点 ≥1 出边、下层每节点 ≥1 入边)── for (let f = 0; f < MAP.floorsPerAct - 1; f++) { const cur = floors[f]; diff --git a/tier2/games/shanhai-xunyilu/src/meta-core.js b/tier2/games/shanhai-xunyilu/src/meta-core.js new file mode 100644 index 00000000..cbedb26a --- /dev/null +++ b/tier2/games/shanhai-xunyilu/src/meta-core.js @@ -0,0 +1,193 @@ +/** + * src/meta-core.js —— 《山海巡异录》局外核(M3 S15:图鉴/成就/进阶解锁/每日;纯逻辑层) + * owner:北极星顶级线③(M3) + * + * 【这份是什么】设计书 §8.3 四纯逻辑模块的第四块(battle/map/economy〔并入 run〕之后): + * 跨 run 的持久面——图鉴收集(卡/遗物/敌兽,见过即录)、成就 40(声明式条件解释器,表 + * ACHIEVEMENTS)、进阶解锁(通关 An 解锁 An+1,逐职业)、每日一局(日期种子+本地最佳分)。 + * + * 【纯逻辑铁律】零 DOM/引擎 import;无随机(收集与判定都是确定性归纳); + * observe/recordRunEnd 幂等可重放(同一 run 状态重复喂不重复计数)。 + * + * 【持久化】serialize() 产纯对象,进 SaveEnvelope data.meta(v2 信封;src/save.js 统一管介质)。 + */ + +'use strict'; + +import { ACHIEVEMENTS, ASCENSIONS, CLASSES, DAILY } from './data/tables.js'; + +/** + * 建局外核。 + * @param {Object|null} [saved] 存档回填(serialize() 的产物;缺省全新) + */ +export function createMetaCore(saved) { + const meta = { + collection: { cards: {}, relics: {}, enemies: {} }, // 图鉴:id → true(见过) + achievements: {}, // 成就:id → true(达成) + runsPlayed: 0, + wins: 0, + winsByClass: { jia: 0, yi: 0 }, + ascensionUnlocked: { jia: 0, yi: 0 }, // 已解锁可选的最高进阶(通关 An 解锁 An+1) + removeTotal: 0, // 累计删卡(成就轴) + daily: { date: null, seed: null, bestScore: 0, played: false }, + }; + if (saved && typeof saved === 'object') { + // 逐域回填(未知键丢弃;缺键用缺省——向后兼容的窄回填)。 + if (saved.collection) { + for (const k of ['cards', 'relics', 'enemies']) { + if (saved.collection[k] && typeof saved.collection[k] === 'object') meta.collection[k] = { ...saved.collection[k] }; + } + } + if (saved.achievements && typeof saved.achievements === 'object') meta.achievements = { ...saved.achievements }; + if (Number.isInteger(saved.runsPlayed)) meta.runsPlayed = saved.runsPlayed; + if (Number.isInteger(saved.wins)) meta.wins = saved.wins; + if (saved.winsByClass) meta.winsByClass = { jia: saved.winsByClass.jia || 0, yi: saved.winsByClass.yi || 0 }; + if (saved.ascensionUnlocked) meta.ascensionUnlocked = { jia: saved.ascensionUnlocked.jia || 0, yi: saved.ascensionUnlocked.yi || 0 }; + if (Number.isInteger(saved.removeTotal)) meta.removeTotal = saved.removeTotal; + if (saved.daily && typeof saved.daily === 'object') meta.daily = { date: saved.daily.date || null, seed: saved.daily.seed || null, bestScore: saved.daily.bestScore || 0, played: !!saved.daily.played }; + } + + /** 成就判定上下文(recordRunEnd 时的单局观测;跨局量从 meta 读)。 */ + let lastRunCtx = null; + + /** 声明式条件解释器(表 ACHIEVEMENTS.cond;新增 kind 必须在此登记——漏词=成就永不达成,单测钉全覆盖)。 */ + function checkCond(cond, ctx) { + switch (cond.kind) { + case 'wins': return meta.wins >= cond.n; + case 'runs': return meta.runsPlayed >= cond.n; + case 'winClass': return meta.winsByClass[cond.cls] > 0; + case 'winBoth': return meta.winsByClass.jia > 0 && meta.winsByClass.yi > 0; + case 'winAscension': return ctx && ctx.win && ctx.ascension >= cond.n; + case 'deckMaxAtWin': return ctx && ctx.win && ctx.deckCount <= cond.n; + case 'deckMinAtWin': return ctx && ctx.win && ctx.deckCount >= cond.n; + case 'hpFullWin': return ctx && ctx.win && ctx.hp >= ctx.hpMax; + case 'goldAtWin': return ctx && ctx.win && ctx.gold >= cond.n; + case 'removeTotal': return meta.removeTotal >= cond.n; + case 'zhoutianRun': return ctx && ctx.zhoutianCount >= cond.n; + case 'relicsAtWin': return ctx && ctx.win && ctx.relicCount >= cond.n; + case 'winTurnsUnder': return ctx && ctx.win && ctx.turnsTotal <= cond.n; + case 'collectCards': return Object.keys(meta.collection.cards).length >= cond.n; + case 'collectRelics': return Object.keys(meta.collection.relics).length >= cond.n; + case 'collectEnemies': return Object.keys(meta.collection.enemies).length >= cond.n; + case 'dailyPlayed': return meta.daily.played; + case 'dailyScore': return meta.daily.bestScore >= cond.n; + case 'ascUnlocked': return Math.max(meta.ascensionUnlocked.jia, meta.ascensionUnlocked.yi) >= cond.n; + default: return false; // 未知词=永不达成(单测 M3 钉词表全覆盖,防静默) + } + } + + /** 成就重扫(幂等;新达成返回列表供演出)。 */ + function sweepAchievements(ctx) { + const newly = []; + for (const a of ACHIEVEMENTS) { + if (meta.achievements[a.id]) continue; + if (checkCond(a.cond, ctx)) { + meta.achievements[a.id] = true; + newly.push(a.id); + } + } + return newly; + } + + return { + /** + * 巡览观测(工厂每次动作后喂 readState;幂等):图鉴收集——卡组成员/持有遗物/在场敌兽。 + */ + observe(runState) { + if (!runState) return; + for (const id of runState.deck || []) meta.collection.cards[id] = true; + for (const r of runState.relics || []) meta.collection.relics[r.id] = true; + if (runState.battle && runState.battle.enemy) meta.collection.enemies[runState.battle.enemy.id] = true; + }, + + /** + * 终局结算(每局恰一次;调用方负责去重):局数/胜数/进阶解锁/每日分/成就重扫。 + * @param {Object} end 终局 readState 投影 + * @param {{zhoutianCount?:number, isDaily?:boolean}} [extra] 局外补充观测(事件流来源) + * @returns {{newlyAchieved:string[], dailyScore:number|null}} + */ + recordRunEnd(end, extra = {}) { + const win = end.phase === 'win'; + meta.runsPlayed += 1; + meta.removeTotal += (end.stats && end.stats.cardsRemoved) || 0; + if (win) { + meta.wins += 1; + if (meta.winsByClass[end.heroClass] != null) meta.winsByClass[end.heroClass] += 1; + // 进阶解锁:通关 An 解锁 An+1(封顶表长)。 + const cur = meta.ascensionUnlocked[end.heroClass] || 0; + if (end.ascension >= cur && cur < ASCENSIONS.length) { + meta.ascensionUnlocked[end.heroClass] = Math.min(ASCENSIONS.length, end.ascension + 1); + } + } + // 每日分(仅每日局;分数公式=表 DAILY 常量)。 + let dailyScore = null; + if (extra.isDaily) { + dailyScore = win + ? DAILY.winBase + end.hero.hp * DAILY.hpWeight + end.gold * DAILY.goldWeight - (end.stats.turnsTotal || 0) * DAILY.turnPenalty + : (end.stats.battlesWon || 0) * DAILY.battleWeight + (end.act || 1) * DAILY.actWeight; + dailyScore = Math.max(0, Math.round(dailyScore)); + meta.daily.played = true; + if (dailyScore > meta.daily.bestScore) meta.daily.bestScore = dailyScore; + } + lastRunCtx = { + win, + ascension: end.ascension || 0, + deckCount: end.deckCount, + hp: end.hero.hp, + hpMax: end.hero.hpMax, + gold: end.gold, + relicCount: (end.relics || []).length, + turnsTotal: (end.stats && end.stats.turnsTotal) || 0, + zhoutianCount: extra.zhoutianCount || 0, + }; + const newlyAchieved = sweepAchievements(lastRunCtx); + return { newlyAchieved, dailyScore }; + }, + + /** 今日种子(YYYYMMDD 数字;全服同种子的本地实现——服务端榜单走平台既有能力,书 §2)。 */ + dailySeedFor(dateStr) { + const d = dateStr || new Date().toISOString().slice(0, 10); + if (meta.daily.date !== d) { + meta.daily = { date: d, seed: parseInt(d.replace(/-/g, ''), 10) >>> 0, bestScore: meta.daily.date === d ? meta.daily.bestScore : 0, played: false }; + } + return { date: meta.daily.date, seed: meta.daily.seed }; + }, + + /** 局外投影(home/图鉴/成就屏消费)。 */ + readMeta() { + return { + collection: { + cards: Object.keys(meta.collection.cards).length, + relics: Object.keys(meta.collection.relics).length, + enemies: Object.keys(meta.collection.enemies).length, + cardIds: { ...meta.collection.cards }, + relicIds: { ...meta.collection.relics }, + enemyIds: { ...meta.collection.enemies }, + }, + achievements: { ...meta.achievements }, + achievedCount: Object.keys(meta.achievements).length, + runsPlayed: meta.runsPlayed, + wins: meta.wins, + winsByClass: { ...meta.winsByClass }, + ascensionUnlocked: { ...meta.ascensionUnlocked }, + removeTotal: meta.removeTotal, + daily: { ...meta.daily }, + classes: Object.values(CLASSES).map((c) => ({ id: c.id, name: c.name, hpMax: c.hpMax })), + }; + }, + + /** 持久化面(SaveEnvelope data.meta;窄形状,与回填对偶)。 */ + serialize() { + return { + collection: { cards: { ...meta.collection.cards }, relics: { ...meta.collection.relics }, enemies: { ...meta.collection.enemies } }, + achievements: { ...meta.achievements }, + runsPlayed: meta.runsPlayed, + wins: meta.wins, + winsByClass: { ...meta.winsByClass }, + ascensionUnlocked: { ...meta.ascensionUnlocked }, + removeTotal: meta.removeTotal, + daily: { ...meta.daily }, + }; + }, + }; +} diff --git a/tier2/games/shanhai-xunyilu/src/run-core.js b/tier2/games/shanhai-xunyilu/src/run-core.js index 6249adb3..86ed308c 100644 --- a/tier2/games/shanhai-xunyilu/src/run-core.js +++ b/tier2/games/shanhai-xunyilu/src/run-core.js @@ -21,7 +21,8 @@ import { createBattleCore } from './battle-core.js'; import { generateActMap, validateActMap } from './map-core.js'; import { - TABLE_SCHEMA_VERSION, CARDS, ENEMIES, RELICS, MAP, ECONOMY, RUN_STARTER_DECK, CURVES, + TABLE_SCHEMA_VERSION, CARDS, ENEMIES, RELICS, MAP, ECONOMY, RUN_STARTER_DECK, YI_STARTER_DECK, CURVES, + CLASSES, ASCENSIONS, EVENTS, } from './data/tables.js'; import { computeTapTargets } from './ui/layout.js'; @@ -48,6 +49,26 @@ export function createRunCore(deps, opts = {}) { throw new Error('[run-core] 必须注入受控随机源 random(next/range)'); // 错误路径留痕 } + // ── M3 职业与进阶(S13/S14)── + const restoredMeta = opts.restore || null; // 存档回填时职业/进阶随档(先于状态树构建) + const heroClass = restoredMeta && CLASSES[restoredMeta.heroClass] ? restoredMeta.heroClass + : (opts.heroClass && CLASSES[opts.heroClass] ? opts.heroClass : 'jia'); // 缺省甲(M0–M2 兼容) + const classDef = CLASSES[heroClass]; + const ascension = restoredMeta && Number.isInteger(restoredMeta.ascension) ? restoredMeta.ascension + : (Number.isInteger(opts.ascension) ? Math.max(0, Math.min(ASCENSIONS.length, opts.ascension)) : 0); + /** 进阶修正聚合(A1..AN 累积:Pct 累加一次乘,Delta 累加;A0=全零)。 */ + const asc = (() => { + const m = { enemyAtkPct: 0, enemyHpPct: 0, eliteHpPct: 0, bossHpPct: 0, bossAtkPct: 0, elitesPerActDelta: 0, restHealPctDelta: 0, startHpDelta: 0, goldPct: 0, shopPricePct: 0, winHealDelta: 0, firstTurnEnemyStrength: 0, rewardChoicesDelta: 0 }; + for (const a of ASCENSIONS) { + if (a.level > ascension) break; + for (const [k, v] of Object.entries(a.mods)) m[k] = (m[k] || 0) + v; + } + return m; + })(); + /** 职业池(奖励/商店选卡口径):本职业专属 + 中立。 */ + const CLASS_POOL = CARDS.filter((c) => c.class === heroClass || c.class === 'neutral'); + const classStarter = heroClass === 'yi' ? YI_STARTER_DECK : RUN_STARTER_DECK; + /** M2 S11 restore 通道:snapshot() 产出的 data(经 save.js validateEnvelope 校验后传入)。 * 回填=逐字段还原(含 rng 状态由调用方回填到 deps.random),restore 后继续玩与不中断玩逐字段一致。 */ const restored = opts.restore || null; @@ -74,13 +95,15 @@ export function createRunCore(deps, opts = {}) { screen: 'map', // map | battle | reward | relicReward | shop | shopRemove act: 1, gold: 0, - hero: { hp: CURVES.heroHpMax, hpMax: CURVES.heroHpMax }, // 职业甲 72(§6.2;跨战斗携带的长线资源) - deck: (Array.isArray(opts.deck) ? opts.deck.slice() : RUN_STARTER_DECK.slice()), - relics: (Array.isArray(opts.relics) ? opts.relics.slice() : []), + // HP=职业表值+进阶修正(甲72/乙66;A6 起始-8;§6.2 长线资源)。 + hero: { hp: classDef.hpMax + asc.startHpDelta, hpMax: classDef.hpMax + asc.startHpDelta }, + deck: (Array.isArray(opts.deck) ? opts.deck.slice() : classStarter.slice()), + // 起始遗物(M3 平衡轮:职业身份件;金标注入 opts.relics 时以注入为准——金标语义=已成型构筑)。 + relics: (Array.isArray(opts.relics) ? opts.relics.slice() : (classDef.starterRelic ? [classDef.starterRelic] : [])), position: /** @type {string|null} */ (null), // 当前节点 id(null=幕起点,可选 0 层) visited: /** @type {string[]} */ ([]), ui: { selected: -1 }, // 交互态(点选出牌两段式;driver 与真指针共享) - stats: { battlesWon: 0, elitesWon: 0, bossesWon: 0, cardsAdded: 0, relicsGained: 0, playsTotal: 0, turnsTotal: 0, nodesVisited: 0, cardsRemoved: 0, shopPurchases: 0 }, + stats: { battlesWon: 0, elitesWon: 0, bossesWon: 0, cardsAdded: 0, relicsGained: 0, playsTotal: 0, turnsTotal: 0, nodesVisited: 0, cardsRemoved: 0, shopPurchases: 0, zhoutianTotal: 0 }, removeCount: 0, // 全局删卡次数(删价随之递增,跨商店持续——§3.2 给组合爆炸收租) seed: opts.seed != null ? opts.seed : null, }; @@ -93,7 +116,7 @@ export function createRunCore(deps, opts = {}) { /** 建当前幕地图(测试通道优先注入;生成/注入一律过静态可达自检,失败响亮抛)。 */ function buildActMap(act) { - const m = (opts.actMaps && opts.actMaps[act - 1]) || generateActMap(rng, act); + const m = (opts.actMaps && opts.actMaps[act - 1]) || generateActMap(rng, act, { extraElites: asc.elitesPerActDelta }); const v = validateActMap(m); if (!v.ok) throw new Error(`[run-core] 幕 ${act} 地图自检失败:${v.problems.join(';')}`); return m; @@ -119,6 +142,9 @@ export function createRunCore(deps, opts = {}) { let relicReward = null; /** 商店货架(shop/shopRemove 屏时非空):{cards:[{id,price,sold}], relics:[{id,price,sold}], freeRemoveUsed}。 */ let shop = null; + /** 当前事件(event 屏时非空):{id};eventsSeen=本局已遇事件(防重复)。 */ + let currentEvent = null; + let eventsSeen = /** @type {string[]} */ ([]); if (restored) { // 屏内瞬态回填(reward/relicReward/shop 三屏可存档;battle 屏无快照,不在此列)。 if (restored.reward) reward = { options: restored.reward.options.slice(), canSkip: restored.reward.canSkip, after: restored.reward.after }; @@ -130,6 +156,8 @@ export function createRunCore(deps, opts = {}) { freeRemoveUsed: !!restored.shop.freeRemoveUsed, }; } + if (restored.currentEvent) currentEvent = { ...restored.currentEvent }; + if (Array.isArray(restored.eventsSeen)) eventsSeen = restored.eventsSeen.slice(); } /** run 级事件队列(battle 事件在 sync 时合流;takeEvents 取走清空)。 */ @@ -192,11 +220,23 @@ export function createRunCore(deps, opts = {}) { } // ── 内部:敌与缩放 ──────────────────────────────────────────────────────────── - /** 幕缩放(普通/精英;Boss 定幕直调不缩放)——逐幕表 MAP.act*ScaleByAct(消幂次复利,回填纪律见表注)。 */ + /** 幕缩放 × 进阶修正(M3 S14):普通/精英吃幕缩放(Boss 定幕直调不吃)+ 各 tier 吃进阶乘区。 */ function scaleEnemy(def, act) { - if (def.tier === 'boss' || act === 1) return def; - const hpK = MAP.actHpScaleByAct[Math.min(act, MAP.actHpScaleByAct.length) - 1]; - const atkK = MAP.actAtkScaleByAct[Math.min(act, MAP.actAtkScaleByAct.length) - 1]; + // 进阶乘区(A0 全零=恒等;Pct 累加后一次乘)。 + let ascHpK = 1; + let ascAtkK = 1; + if (def.tier === 'boss') { + ascHpK = 1 + asc.bossHpPct / 100; + ascAtkK = 1 + asc.bossAtkPct / 100; + } else { + ascHpK = (1 + asc.enemyHpPct / 100) * (def.tier === 'elite' ? 1 + asc.eliteHpPct / 100 : 1); + ascAtkK = 1 + asc.enemyAtkPct / 100; + } + const actHpK = (def.tier === 'boss' || act === 1) ? 1 : MAP.actHpScaleByAct[Math.min(act, MAP.actHpScaleByAct.length) - 1]; + const actAtkK = (def.tier === 'boss' || act === 1) ? 1 : MAP.actAtkScaleByAct[Math.min(act, MAP.actAtkScaleByAct.length) - 1]; + const hpK = actHpK * ascHpK; + const atkK = actAtkK * ascAtkK; + if (hpK === 1 && atkK === 1) return def; const scaleIntent = (it) => { const out = { ...it }; if (it.kind === 'attack' || it.kind === 'attackBlock') out.n = Math.round(it.n * atkK); @@ -223,7 +263,7 @@ export function createRunCore(deps, opts = {}) { const pool = ENEMIES.filter((e) => e.tier === 'elite' && e.acts.includes(state.act)); return scaleEnemy(pool[rng.range(0, pool.length - 1)], state.act); } - if (state.act === 1 && node.floor === 0) return ENEMIES[0]; // 教学局固定首战「涸泽鲛」(§5.3) + if (state.act === 1 && node.floor === 0) return scaleEnemy(ENEMIES[0], state.act); // 教学局固定首战「涸泽鲛」(§5.3;敌种固定、数值仍吃进阶修正——R15g 抓的缺口:A 级不豁免首战) const pool = ENEMIES.filter((e) => e.tier === 'normal' && e.acts.includes(state.act)); return scaleEnemy(pool[rng.range(0, pool.length - 1)], state.act); } @@ -233,7 +273,8 @@ export function createRunCore(deps, opts = {}) { battle = createBattleCore( { random: rng }, // battleOpts=测试通道透传(fixedDraw/actionLimit 等);run 管理键在后,不可被覆盖。 - { ...(opts.battleOpts || {}), enemy: enemyDef, heroHp: state.hero.hp, heroHpMax: state.hero.hpMax, deck: state.deck.slice(), relicHooks: relicHooks() }, + // A15 敌开战力:限精英/首领(普通敌不带——尾段斜率实测回填,A15 全敌版=1% 出带)。 + { ...(opts.battleOpts || {}), enemy: enemyDef, heroHp: state.hero.hp, heroHpMax: state.hero.hpMax, deck: state.deck.slice(), relicHooks: relicHooks(), enemyStartStrength: (enemyDef.tier === 'elite' || enemyDef.tier === 'boss') ? asc.firstTurnEnemyStrength : 0 }, ); battleNodeType = node.type; state.screen = 'battle'; @@ -251,13 +292,13 @@ export function createRunCore(deps, opts = {}) { const rarities = Object.keys(weights); const total = rarities.reduce((s, r) => s + weights[r], 0); const options = []; - const wantChoices = ECONOMY.cardChoices + (hooks.rewardExtraChoice || 0); // 三青鸟羽:四选一 + const wantChoices = Math.max(2, ECONOMY.cardChoices + (hooks.rewardExtraChoice || 0) + asc.rewardChoicesDelta); // 三青鸟羽:四选一;A15:二选一(下限 2) let guard = 0; while (options.length < wantChoices && guard++ < 200) { let roll = rng.next() * total; let rarity = rarities[0]; for (const r of rarities) { roll -= weights[r]; if (roll <= 0) { rarity = r; break; } } - const pool = CARDS.filter((c) => c.rarity === rarity && !options.includes(c.id)); + const pool = CLASS_POOL.filter((c) => c.rarity === rarity && !options.includes(c.id)); if (pool.length === 0) continue; options.push(pool[rng.range(0, pool.length - 1)].id); } @@ -270,7 +311,7 @@ export function createRunCore(deps, opts = {}) { state.hero.hp = bs.hero.hp; // 职业甲基线「凝血诀」:战胜回复(表 winHealFlat + 遗物 winHealBonus)。 const hooks = relicHooks(); - const wh = Math.min(ECONOMY.winHealFlat + (hooks.winHealBonus || 0), state.hero.hpMax - state.hero.hp); + const wh = Math.min(Math.max(0, ECONOMY.winHealFlat + (hooks.winHealBonus || 0) + asc.winHealDelta), state.hero.hpMax - state.hero.hp); if (wh > 0) { state.hero.hp += wh; emit({ t: 'heal', n: wh, source: 'win-heal' }); } state.stats.turnsTotal += bs.turn; state.stats.battlesWon += 1; @@ -279,7 +320,8 @@ export function createRunCore(deps, opts = {}) { const band = ECONOMY.goldReward[battleNodeType === 'elite' ? 'elite' : battleNodeType === 'boss' ? 'boss' : 'normal']; let gold = rng.range(band[0], band[1]); - if (hooks.goldPct) gold = Math.floor(gold * (1 + hooks.goldPct / 100)); // 乘黄鞍:战斗金币加成 + const goldPctTotal = (hooks.goldPct || 0) + asc.goldPct; // 乘黄鞍 + 进阶 A7(累加一次乘) + if (goldPctTotal) gold = Math.floor(gold * (1 + goldPctTotal / 100)); state.gold += gold; emit({ t: 'battleEnd', result: 'win', tier: battleNodeType, act: state.act }); emit({ t: 'goldGain', n: gold, source: 'battle' }); @@ -312,13 +354,18 @@ export function createRunCore(deps, opts = {}) { if (hooks.freeRemovePerShop && shop && !shop.freeRemoveUsed) return 0; let price = ECONOMY.shop.removeBase + state.removeCount * ECONOMY.shop.removeStep; if (hooks.shopDiscountPct) price = Math.floor(price * (1 - hooks.shopDiscountPct / 100)); + if (asc.shopPricePct) price = Math.floor(price * (1 + asc.shopPricePct / 100)); // 进阶 A9 return price; } /** 进店:生成货架(卡按奖励权重抽互异、遗物未持有池抽、标价随折扣一次定死)。 */ function openShop() { const hooks = relicHooks(); - const disc = (p) => (hooks.shopDiscountPct ? Math.floor(p * (1 - hooks.shopDiscountPct / 100)) : p); + const disc = (p) => { + let out = hooks.shopDiscountPct ? Math.floor(p * (1 - hooks.shopDiscountPct / 100)) : p; + if (asc.shopPricePct) out = Math.floor(out * (1 + asc.shopPricePct / 100)); // 进阶 A9 + return out; + }; const cardIds = []; let guard = 0; while (cardIds.length < ECONOMY.shop.cardSlots && guard++ < 200) { @@ -341,6 +388,17 @@ export function createRunCore(deps, opts = {}) { emit({ t: 'shopEnter', cards: cards.length, relics: relics.length }); } + /** 进事件节点(M3 S16):按幕池+通用池抽未遇事件(受控随机;全遇过则回退通用池复用)。 */ + function openEvent() { + let pool = EVENTS.filter((e) => (e.acts == null || e.acts.includes(state.act)) && !eventsSeen.includes(e.id)); + if (pool.length === 0) pool = EVENTS.filter((e) => e.acts == null); // 兜底:通用池复用 + const ev = pool[rng.range(0, pool.length - 1)]; + eventsSeen.push(ev.id); + currentEvent = { id: ev.id }; + state.screen = 'event'; + emit({ t: 'eventStart', eventId: ev.id, name: ev.name }); + } + /** 进下一幕:幕间整备恢复(表 bossHealPct)→ 重生成地图、位置归零。 */ function nextAct() { const heal = Math.min(Math.floor(state.hero.hpMax * ECONOMY.bossHealPct), state.hero.hpMax - state.hero.hp); @@ -358,7 +416,10 @@ export function createRunCore(deps, opts = {}) { /** 战斗事件合流 + 终态桥接(battle latch → run 级结算/latch)。 */ function syncBattle() { if (!battle) return; - for (const e of battle.takeEvents()) events.push(e); + for (const e of battle.takeEvents()) { + if (e.t === 'zhoutian') state.stats.zhoutianTotal += 1; // M3 成就轴(单局周天计数) + events.push(e); + } const phase = battle.readState().phase; if (phase === 'win') settleBattleWin(); else if (phase === 'lose') { @@ -394,7 +455,8 @@ export function createRunCore(deps, opts = {}) { if (node.type === 'battle' || node.type === 'elite' || node.type === 'boss') { startBattle(node); } else if (node.type === 'rest') { - const pct = ECONOMY.restHealPct + (relicHooks().restHealPctBonus || 0); // 若木灯:憩点回复加成 + // 若木灯加成 + 进阶 A4/A14 减免(比例累加,下限 0.1 防负回复)。 + const pct = Math.max(0.1, ECONOMY.restHealPct + (relicHooks().restHealPctBonus || 0) + asc.restHealPctDelta); const heal = Math.min(Math.floor(state.hero.hpMax * pct), state.hero.hpMax - state.hero.hp); state.hero.hp += heal; emit({ t: 'heal', n: heal, source: 'rest' }); @@ -402,10 +464,63 @@ export function createRunCore(deps, opts = {}) { grantRelic('treasure'); } else if (node.type === 'shop') { openShop(); + } else if (node.type === 'event') { + openEvent(); } return { ok: true }; }, + /** + * 事件选项结算(event 屏;M3 S16)。fx 七词见表 EVENTS 注释;结算后回 map。 + */ + chooseEventOption(index) { + if (terminal || state.screen !== 'event' || !currentEvent) return { ok: false, reason: 'wrong-screen' }; + const ev = EVENTS.find((e) => e.id === currentEvent.id); + const opt = ev.options[index]; + if (!opt) return { ok: false, reason: 'bad-index' }; + const fx = opt.fx || {}; + if (fx.gold) { + state.gold = Math.max(0, state.gold + fx.gold); + emit({ t: fx.gold > 0 ? 'goldGain' : 'goldSpend', n: Math.abs(fx.gold), source: 'event' }); + } + if (fx.heal) { + const h = Math.min(fx.heal, state.hero.hpMax - state.hero.hp); + if (h > 0) { state.hero.hp += h; emit({ t: 'heal', n: h, source: 'event' }); } + } + if (fx.loseHp) { + state.hero.hp = Math.max(1, state.hero.hp - fx.loseHp); // 事件不致死(下限 1;死亡判定只在战斗) + emit({ t: 'hpCost', n: fx.loseHp, source: 'event' }); + } + if (fx.hpMax) { + state.hero.hpMax = Math.max(1, state.hero.hpMax + fx.hpMax); + state.hero.hp = Math.min(Math.max(1, state.hero.hp + fx.hpMax), state.hero.hpMax); + emit({ t: 'hpMaxChange', n: fx.hpMax, source: 'event' }); + } + if (fx.relic) grantRelic('event'); + if (fx.card) { + const pool = CLASS_POOL.filter((c) => c.rarity === fx.card); + if (pool.length) { + const card = pool[rng.range(0, pool.length - 1)]; + state.deck.push(card.id); + state.stats.cardsAdded += 1; + emit({ t: 'cardAdded', cardId: card.id, source: 'event' }); + } + } + if (fx.removeRandomCommon && state.deck.length > ECONOMY.deckMin) { + const commons = state.deck.map((id, i) => ({ id, i })).filter((x) => CARD_BY_ID[x.id].rarity === '普通'); + if (commons.length) { + const pick = commons[rng.range(0, commons.length - 1)]; + state.deck.splice(pick.i, 1); + state.stats.cardsRemoved += 1; + emit({ t: 'cardRemoved', cardId: pick.id, price: 0, source: 'event' }); + } + } + emit({ t: 'eventResolved', eventId: currentEvent.id, option: index }); + currentEvent = null; + state.screen = 'map'; + return { ok: true }; + }, + /** 点手牌(battle 屏;两段式:选中 → 再点同卡打出)。 */ tapCard(handIndex) { if (terminal || state.screen !== 'battle' || !battle) return { ok: false, reason: 'wrong-screen' }; @@ -589,6 +704,10 @@ export function createRunCore(deps, opts = {}) { battle: battle ? battle.readState() : null, reward: reward ? { options: reward.options.map((id) => ({ id, ...CARD_BY_ID[id] })), canSkip: reward.canSkip } : null, relicReward: relicReward ? { options: relicReward.options.map((id) => ({ id, name: RELIC_BY_ID[id].name, text: RELIC_BY_ID[id].text })), canSkip: relicReward.canSkip } : null, + event: currentEvent ? (() => { + const ev = EVENTS.find((e) => e.id === currentEvent.id); + return { id: ev.id, name: ev.name, text: ev.text, options: ev.options.map((o) => ({ label: o.label, fx: o.fx || {} })) }; + })() : null, shop: shop ? { cards: shop.cards.map((c) => ({ ...c, ...CARD_BY_ID[c.id] })), relics: shop.relics.map((r) => ({ ...r, name: RELIC_BY_ID[r.id].name, text: RELIC_BY_ID[r.id].text })), @@ -601,6 +720,8 @@ export function createRunCore(deps, opts = {}) { ui: { selected: state.ui.selected }, stats: { ...state.stats }, seed: state.seed, + heroClass, + ascension, }; projection.tapTargets = computeTapTargets(projection); return projection; @@ -617,6 +738,8 @@ export function createRunCore(deps, opts = {}) { if (typeof rng.state !== 'function') return null; // 随机源不支持状态导出(测试桩)则不产快照 return { tableSchemaVersion: TABLE_SCHEMA_VERSION, // 表版本对账位(表破坏性变更时迁移判据) + heroClass, // M3:职业与进阶随档(restore 经 opts 回填) + ascension, rngState: rng.state(), screen: state.screen, act: state.act, @@ -631,7 +754,13 @@ export function createRunCore(deps, opts = {}) { seed: state.seed, map: { nodes: map.nodes.map((n) => ({ ...n })), edges: map.edges.map((e) => e.slice()) }, reward: reward ? { options: reward.options.slice(), canSkip: reward.canSkip, after: reward.after } : null, + currentEvent: currentEvent ? { ...currentEvent } : null, + eventsSeen: eventsSeen.slice(), relicReward: relicReward ? { options: relicReward.options.slice(), canSkip: relicReward.canSkip } : null, + event: currentEvent ? (() => { + const ev = EVENTS.find((e) => e.id === currentEvent.id); + return { id: ev.id, name: ev.name, text: ev.text, options: ev.options.map((o) => ({ label: o.label, fx: o.fx || {} })) }; + })() : null, shop: shop ? { cards: shop.cards.map((c) => ({ ...c })), relics: shop.relics.map((r) => ({ ...r })), freeRemoveUsed: shop.freeRemoveUsed } : null, }; }, diff --git a/tier2/games/shanhai-xunyilu/src/save.js b/tier2/games/shanhai-xunyilu/src/save.js index aedaf3ff..1ea8587a 100644 --- a/tier2/games/shanhai-xunyilu/src/save.js +++ b/tier2/games/shanhai-xunyilu/src/save.js @@ -1,18 +1,21 @@ /** - * src/save.js —— 《山海巡异录》run 存档件(M2 S11;SaveEnvelope 契约面单源) - * owner:北极星顶级线③(M2) + * src/save.js —— 《山海巡异录》存档件(M2 S11 v1 → M3 S15 v2;SaveEnvelope 契约面单源) + * owner:北极星顶级线③(M2/M3) * * ════════════════════════════════════════════════════════════════════════════ * 【契约】形状 = contracts/sdk-interface.d.ts SaveEnvelope:{ schemaVersion: 整数 ≥1, data: 纯对象 }; * 键族 = 'save:'+gameId+':'+versionId(宿主白名单恰等、单槽、上限 65536 UTF-16 码元、fail-closed)。 - * W-SAVE-CAP 已入库(书 §12④ M2 入口门基建就位)。 * - * 【介质口径(诚实申报)】M2 = 影子介质:浏览器径直写 localStorage(同名键)/headless 用注入 store; - * 正式 SDK Storage 通道(postMessage storage 消息)切换随发布集成段(书 §12④,M5 硬门: - * 切正式通道 + 迁移/兼容断言)。本文件的信封形状与校验与正式通道逐字节同契约,切换只换介质。 + * 【版本谱系】v1(M2):data = run 快照平铺(rngState/deck/map/…)。 + * v2(M3 S15):data = { run: 快照|null, meta: 局外进度|null } 双域——run 终局后 run=null 但 meta 常存 + * (图鉴/成就/进阶解锁/每日跨局持久)。v1→v2 迁移:旧档整体视为 run 域、meta 置空(migrateV1)。 + * 这是版本信封的首个真实迁移,M5「切正式通道+迁移/兼容断言」的先手演练。 * - * 【fail-closed 纪律】与宿主闸同向:非信封 / 版本非法 / data 非纯对象 / 地图自检失败 → 一律弃档 - * (返回 null、留痕日志),绝不半恢复。坏档等价无档:开新局。 + * 【介质口径(诚实申报)】M3 仍=影子介质(main.js localStorage 直写/headless 注入); + * 正式 SDK Storage 通道切换随发布集成段(书 §12④,M5 硬门)。信封形状与校验与正式通道同契约。 + * + * 【fail-closed 纪律】非信封 / 版本非法 / data 非纯对象 / run 域坏形 / 地图自检失败 → 弃档(null+留痕), + * 绝不半恢复;meta 域坏形 → 只弃 meta(run 与 meta 独立校验:局外进度损坏不应吞掉续玩)。 * ════════════════════════════════════════════════════════════════════════════ */ @@ -22,56 +25,83 @@ import { createRunCore } from './run-core.js'; import { createSeededRandom } from './util/seeded-random.js'; import { SAVE } from './data/tables.js'; -/** 存档键(M2 影子口径;正式 versionId 随集成段注入)。 */ +/** 存档键(影子口径;正式 versionId 随集成段注入)。 */ export const SAVE_KEY = SAVE.shadowKey; /** - * 由 run 核产出信封(SaveEnvelope 形状)。 - * @param {ReturnType} run - * @returns {{schemaVersion:number, data:Object}|null} battle 屏/终局无快照 → null + * 组信封(v2):run 快照(battle 屏/终局为 null)+ meta 序列化。 + * @param {{snapshot:()=>Object|null}|null} run 一局核(可 null=局外) + * @param {{serialize:()=>Object}|null} metaCore 局外核(可 null) + * @returns {{schemaVersion:number, data:{run:Object|null, meta:Object|null}}} */ -export function makeEnvelope(run) { - const data = run.snapshot(); - if (!data) return null; - return { schemaVersion: SAVE.schemaVersion, data }; +export function makeEnvelope(run, metaCore) { + return { + schemaVersion: SAVE.schemaVersion, + data: { + run: run ? run.snapshot() : null, + meta: metaCore ? metaCore.serialize() : null, + }, + }; +} + +/** v1 → v2 迁移:旧档 data=run 平铺 → { run: 旧data, meta: null }。 */ +function migrateV1(data) { + return { run: data, meta: null }; } /** - * 信封校验(宿主闸 fail-closed 的游戏侧同构;负例=坏版本/非对象/数组/缺 data): + * 信封校验+版本迁移(fail-closed)。 * @param {unknown} env - * @returns {Object|null} 合法 → data;非法 → null + * @returns {{run:Object|null, meta:Object|null}|null} 合法 → v2 形状 data;非法 → null */ export function validateEnvelope(env) { if (env == null || typeof env !== 'object' || Array.isArray(env)) return null; const v = /** @type {{schemaVersion?:unknown, data?:unknown}} */ (env); if (!Number.isInteger(v.schemaVersion) || v.schemaVersion < 1) return null; // 契约:整数 ≥1 - if (v.schemaVersion !== SAVE.schemaVersion) return null; // M2 只有 v1;未来版本走迁移链(M5 前无旧档负担) + if (v.schemaVersion > SAVE.schemaVersion) return null; // 未来版本不认(fail-closed) if (v.data == null || typeof v.data !== 'object' || Array.isArray(v.data)) return null; // data=纯对象 - const d = /** @type {Record} */ (v.data); - if (!Number.isInteger(d.rngState)) return null; // 随机流锚缺失=无法精确续玩,弃档 - if (d.map == null || typeof d.map !== 'object') return null; - return d; + let data = /** @type {Record} */ (v.data); + if (v.schemaVersion === 1) data = migrateV1(data); // v1 迁移链 + // v2 形状:run 域可 null;非 null 时须带 rngState+map(精确续玩的最低要件)。 + const run = data.run == null ? null : data.run; + if (run != null) { + if (typeof run !== 'object' || Array.isArray(run)) return null; + if (!Number.isInteger(run.rngState)) return null; + if (run.map == null || typeof run.map !== 'object') return null; + } + // meta 域独立校验:坏形只弃 meta,不吞 run(局外进度损坏 ≠ 续玩作废)。 + let metaOut = data.meta == null ? null : data.meta; + if (metaOut != null && (typeof metaOut !== 'object' || Array.isArray(metaOut))) metaOut = null; + return { run, meta: metaOut }; } /** * 从信封恢复一局(fail-closed:任何一步失败 → null + 留痕)。 - * @param {unknown} env 介质读出的对象(宿主/影子介质已 JSON.parse) + * @param {unknown} env 介质读出的对象 * @returns {ReturnType|null} */ export function restoreRun(env) { const data = validateEnvelope(env); - if (!data) return null; + if (!data || !data.run) return null; try { const rng = createSeededRandom(0); - rng.setState(/** @type {number} */(data.rngState)); - return createRunCore({ random: rng }, { restore: data }); + rng.setState(/** @type {number} */(data.run.rngState)); + return createRunCore({ random: rng }, { restore: data.run }); } catch (e) { - // 地图自检失败/表漂移(卡 id 不存在)等 → 弃档开新局;留痕供排查。 - console.error('[save] 存档恢复失败,弃档:', e && e.message); + console.error('[save] run 域恢复失败,弃档续玩面:', e && e.message); // 留痕;meta 域仍可由调用方恢复 return null; } } +/** + * 从信封取 meta 域(独立于 run;调用方喂 createMetaCore)。 + * @returns {Object|null} + */ +export function restoreMeta(env) { + const data = validateEnvelope(env); + return data ? data.meta : null; +} + /** * 序列化并核对容量(save: 族 65536 码元契约闸;超限=结构失败,不静默截断)。 * @returns {{ok:boolean, json?:string, chars?:number, reason?:string}} diff --git a/tier2/games/shanhai-xunyilu/src/scenes/run-scene.js b/tier2/games/shanhai-xunyilu/src/scenes/run-scene.js index be68ccca..7d789d62 100644 --- a/tier2/games/shanhai-xunyilu/src/scenes/run-scene.js +++ b/tier2/games/shanhai-xunyilu/src/scenes/run-scene.js @@ -15,15 +15,15 @@ 'use strict'; -import { ELEMENT_CYCLE, ELEMENT_COLORS } from '../data/tables.js'; +import { ELEMENT_CYCLE, ELEMENT_COLORS, ACHIEVEMENTS } from '../data/tables.js'; import { VIEW_W, VIEW_H, ZONES, CARD_W, CARD_H, midY, handTop, handCardCenter, endTurnRect, mapNodeCenter, rewardCardCenter, rewardSkipRect, } from '../ui/layout.js'; // 节点型 → 显示字/色(表现层词汇,规则在 map-core/tables)。 -const NODE_GLYPH = { battle: '战', elite: '精', rest: '憩', treasure: '宝', boss: '王', shop: '市' }; -const NODE_COLOR = { battle: 0x3f7fbf, elite: 0xc98f3b, rest: 0x3fa34d, treasure: 0xf0c674, boss: 0xd9534f, shop: 0xb9a3d0 }; +const NODE_GLYPH = { battle: '战', elite: '精', rest: '憩', treasure: '宝', boss: '王', shop: '市', event: '遇' }; +const NODE_COLOR = { battle: 0x3f7fbf, elite: 0xc98f3b, rest: 0x3fa34d, treasure: 0xf0c674, boss: 0xd9534f, shop: 0xb9a3d0, event: 0x7fd4c1 }; /** * 建一局场景(Phaser scene 配置对象;`this`=Phaser.Scene)。 @@ -45,6 +45,7 @@ export function createRunScene(api) { s.battle.enemy.statuses] : null, s.reward ? s.reward.options.map((o) => o.id) : null, s.relicReward ? s.relicReward.options.map((o) => o.id) : null, + s.event ? s.event.id : null, s.shop ? [s.shop.cards.map((c) => c.sold), s.shop.relics.map((r) => r.sold), s.shop.removePrice, s.removeCount] : null, ]); } @@ -61,6 +62,23 @@ export function createRunScene(api) { update() { const scene = this; + // M3 局外模式:home 态渲染工厂投影(局外屏);run 态沿一局核直读。 + if (api.mode && api.mode() === 'home') { + const hs = api.homeState(); + const sig = JSON.stringify(['home', hs.screen, hs.home.sel, hs.home.maxAscension, hs.home.meta.achievedCount, hs.home.meta.collection.cards, hs.home.meta.daily]); + if (sig === lastSig) return; + lastSig = sig; + clear(); + if (hs.screen === 'collection') drawCollection(scene, hs); + else if (hs.screen === 'achievements') drawAchievements(scene, hs); + else drawHome(scene, hs); + if (api.debug) { + api.debug.displayCount = uiNodes.length; + api.debug.rebuilds = (api.debug.rebuilds || 0) + 1; + api.debug.screen = `home:${hs.screen}`; + } + return; + } const run = api.run(); if (!run) return; const s = run.readState(); @@ -82,18 +100,34 @@ export function createRunScene(api) { uiNodes = []; } function keep(scene, obj) { uiNodes.push(obj); return obj; } - const txt = (scene, x, y, str, size, color, origin = 0.5) => - keep(scene, scene.add.text(x, y, str, { fontSize: `${size}px`, color, fontFamily: 'sans-serif' }).setOrigin(origin)); + /** + * 文本辅助——必须是 function 声明(提升)。 + * 【2026-07-06 战斗全空事故根因】本函数曾写成 `const txt = (...) => ...` 且位于工厂 return 之后: + * 该行永不执行,`txt` 绑定永陷 TDZ,首次 rebuild 即抛 ReferenceError——整屏绘制(地图/战斗/商店) + * 全部无声失败,只有不依赖 txt 的事件浮字(floatText,function 声明)可见,浏览器像素复验才暴露 + * (headless 门不建场景,验不到)。防回潮:scripts/scene-smoke.mjs mock 场景重放门 + rebuild 自证计数。 + */ + function txt(scene, x, y, str, size, color, origin = 0.5) { + return keep(scene, scene.add.text(x, y, str, { fontSize: `${size}px`, color, fontFamily: 'sans-serif' }).setOrigin(origin)); + } function rebuild(scene, s) { clear(); - if (s.phase !== 'run') { drawTerminal(scene, s); return; } - if (s.screen === 'map') drawMap(scene, s); + if (s.phase !== 'run') { drawTerminal(scene, s); } + else if (s.screen === 'map') drawMap(scene, s); else if (s.screen === 'battle' && s.battle) drawBattle(scene, s); else if (s.screen === 'reward' && s.reward) drawReward(scene, s); else if (s.screen === 'relicReward' && s.relicReward) drawRelicReward(scene, s); else if (s.screen === 'shop' && s.shop) drawShop(scene, s); else if (s.screen === 'shopRemove' && s.shop) drawShopRemove(scene, s); + else if (s.screen === 'event' && s.event) drawEvent(scene, s); + // 自证回写(2026-07-06 增补②):把「本屏已绘制显示对象计数」挂进取证面(工厂 readState().debug), + // CDP 复验据此机断「画了东西」,不再依赖纯像素统计。 + if (api.debug) { + api.debug.displayCount = uiNodes.length; + api.debug.rebuilds = (api.debug.rebuilds || 0) + 1; + api.debug.screen = s.screen; + } } /** 顶栏(全视图常驻):幕/金币/卡组/遗物。 */ @@ -297,6 +331,80 @@ export function createRunScene(api) { txt(scene, bx, by, '不销 · 回市', 14, '#c9c3b2'); } + // ── 事件视图(M3 S16;选项按钮与 layout eventOption 靶同源)────────────────────── + function drawEvent(scene, s) { + drawTopStrip(scene, s); + keep(scene, scene.add.rectangle(VIEW_W / 2, VIEW_H / 2, VIEW_W, VIEW_H, 0x0a0a12, 0.4)); + txt(scene, VIEW_W / 2, VIEW_H * 0.2, `奇遇 · ${s.event.name}`, 22, '#7fd4c1'); + keep(scene, scene.add.text(VIEW_W / 2, VIEW_H * 0.34, s.event.text, { fontSize: '14px', color: '#e8e2d0', fontFamily: 'sans-serif', wordWrap: { width: 320 }, align: 'center' }).setOrigin(0.5)); + s.event.options.forEach((o, i) => { + const y = VIEW_H * 0.52 + i * 64; + keep(scene, scene.add.rectangle(VIEW_W / 2, y, 320, 52, 0x24313a).setStrokeStyle(2, 0x7fd4c1)); + keep(scene, scene.add.text(VIEW_W / 2, y, o.label, { fontSize: '13px', color: '#e8e2d0', fontFamily: 'sans-serif', wordWrap: { width: 300 }, align: 'center' }).setOrigin(0.5)); + }); + } + + // ── 局外三屏(M3 S15;坐标与 layout computeHomeTargets 同源)────────────────────── + function drawHome(scene, hs) { + const m = hs.home.meta; + txt(scene, VIEW_W / 2, VIEW_H * 0.12, '山海巡异录', 34, '#e8e2d0'); + txt(scene, VIEW_W / 2, VIEW_H * 0.18, `出巡 ${m.runsPlayed} · 功成 ${m.wins}`, 13, '#8a8498'); + // 职业二选(选中金框)。 + for (const [i, c] of m.classes.entries()) { + const x = VIEW_W / 2 + (i === 0 ? -88 : 88); + const sel = hs.home.sel.class === c.id; + keep(scene, scene.add.rectangle(x, VIEW_H * 0.34, 150, 110, 0x241f33).setStrokeStyle(sel ? 3 : 1, sel ? 0xf0c674 : 0x555566)); + txt(scene, x, VIEW_H * 0.31, c.name, 18, sel ? '#f0c674' : '#c9c3b2'); + txt(scene, x, VIEW_H * 0.36, `气血 ${c.hpMax}`, 12, '#9fc4e8'); + txt(scene, x, VIEW_H * 0.395, c.id === 'jia' ? '攻伐见长' : '灵脉见长', 11, '#8a8498'); + } + // 进阶选择(加减 + 当前级文案)。 + keep(scene, scene.add.rectangle(VIEW_W / 2 - 110, VIEW_H * 0.5, 52, 44, 0x2a2a38).setStrokeStyle(2, 0x8a8498)); + txt(scene, VIEW_W / 2 - 110, VIEW_H * 0.5, '−', 22, '#e8e2d0'); + keep(scene, scene.add.rectangle(VIEW_W / 2 + 110, VIEW_H * 0.5, 52, 44, 0x2a2a38).setStrokeStyle(2, 0x8a8498)); + txt(scene, VIEW_W / 2 + 110, VIEW_H * 0.5, '+', 22, '#e8e2d0'); + const asc = hs.home.sel.ascension; + txt(scene, VIEW_W / 2, VIEW_H * 0.48, `进阶 ${asc}${hs.home.maxAscension > 0 ? ` / 已启 ${hs.home.maxAscension}` : ''}`, 16, '#e2a13e'); + txt(scene, VIEW_W / 2, VIEW_H * 0.52, asc > 0 ? (hs.home.ascensions[asc - 1] || {}).text || '' : '无修正 · 基准巡途', 11, '#8a8498'); + // 出巡/每日。 + keep(scene, scene.add.rectangle(VIEW_W / 2, VIEW_H * 0.62, 240, 56, 0x5a3f2e).setStrokeStyle(2, 0xc98f3b)); + txt(scene, VIEW_W / 2, VIEW_H * 0.62, '启程出巡', 20, '#f5e9d5'); + keep(scene, scene.add.rectangle(VIEW_W / 2, VIEW_H * 0.72, 240, 48, 0x24313a).setStrokeStyle(2, 0x7fd4c1)); + txt(scene, VIEW_W / 2, VIEW_H * 0.705, `每日一巡 · ${hs.home.daily.date}`, 14, '#7fd4c1'); + txt(scene, VIEW_W / 2, VIEW_H * 0.735, m.daily.bestScore > 0 ? `本地最佳 ${m.daily.bestScore}` : '固定种子 · 天下同题', 11, '#8a8498'); + // 图鉴/成就入口。 + keep(scene, scene.add.rectangle(VIEW_W / 2 - 90, VIEW_H * 0.82, 150, 44, 0x2a2a38).setStrokeStyle(2, 0x8a8498)); + txt(scene, VIEW_W / 2 - 90, VIEW_H * 0.82, `图鉴 ${m.collection.cards + m.collection.relics + m.collection.enemies}`, 13, '#9fc4e8'); + keep(scene, scene.add.rectangle(VIEW_W / 2 + 90, VIEW_H * 0.82, 150, 44, 0x2a2a38).setStrokeStyle(2, 0x8a8498)); + txt(scene, VIEW_W / 2 + 90, VIEW_H * 0.82, `成就 ${m.achievedCount}/40`, 13, '#b9a3d0'); + } + + function drawCollection(scene, hs) { + const m = hs.home.meta; + txt(scene, VIEW_W / 2, 40, '图鉴 · 山海志', 22, '#9fc4e8'); + txt(scene, VIEW_W / 2, VIEW_H * 0.2, `符箓 ${m.collection.cards} / 154`, 16, '#e8e2d0'); + txt(scene, VIEW_W / 2, VIEW_H * 0.3, `遗物 ${m.collection.relics} / 40`, 16, '#e8e2d0'); + txt(scene, VIEW_W / 2, VIEW_H * 0.4, `异兽 ${m.collection.enemies} / 8`, 16, '#e8e2d0'); + txt(scene, VIEW_W / 2, VIEW_H * 0.55, '收录随巡途自动增补', 12, '#8a8498'); + keep(scene, scene.add.rectangle(VIEW_W / 2, VIEW_H * 0.92, 160, 46, 0x2a2a38).setStrokeStyle(2, 0x8a8498)); + txt(scene, VIEW_W / 2, VIEW_H * 0.92, '返回', 15, '#c9c3b2'); + } + + function drawAchievements(scene, hs) { + const m = hs.home.meta; + txt(scene, VIEW_W / 2, 40, `成就 · ${m.achievedCount} / 40`, 22, '#b9a3d0'); + // 达成列表(前 12 条滚动位;M4 再做完整滚动容器)。 + const byId = Object.fromEntries(ACHIEVEMENTS.map((a) => [a.id, a])); + const got = Object.keys(m.achievements).slice(0, 12); + if (got.length === 0) txt(scene, VIEW_W / 2, VIEW_H * 0.4, '尚无成就 · 出巡自见分晓', 14, '#8a8498'); + got.forEach((id, i) => { + const a = byId[id]; + txt(scene, VIEW_W / 2, VIEW_H * 0.18 + i * 28, `✓ ${a ? a.name : id}`, 13, '#9fe08a'); + }); + keep(scene, scene.add.rectangle(VIEW_W / 2, VIEW_H * 0.92, 160, 46, 0x2a2a38).setStrokeStyle(2, 0x8a8498)); + txt(scene, VIEW_W / 2, VIEW_H * 0.92, '返回', 15, '#c9c3b2'); + } + // ── 终局视图 ────────────────────────────────────────────────────────────────── function drawTerminal(scene, s) { keep(scene, scene.add.rectangle(VIEW_W / 2, VIEW_H / 2, VIEW_W, VIEW_H, 0x0a0a12, 0.78)); @@ -408,6 +516,12 @@ export function createRunScene(api) { floatText(scene, VIEW_W / 2, VIEW_H * 0.4, '入市', '#b9a3d0', 18); } else if (e.t === 'relicRewardOffered') { floatText(scene, VIEW_W / 2, VIEW_H * 0.4, '异宝呈前', '#b9a3d0', 18); + } else if (e.t === 'eventStart') { + floatText(scene, VIEW_W / 2, VIEW_H * 0.4, `奇遇 · ${e.name}`, '#7fd4c1', 18); + } else if (e.t === 'hpMaxChange') { + floatText(scene, 90, VIEW_H * 0.5, `气血上限 ${e.n > 0 ? '+' : ''}${e.n}`, '#9fe08a', 15); + } else if (e.t === 'goldSpend') { + floatText(scene, 70, 30, `金 -${e.n}`, '#c9a35a', 14); } } diff --git a/tier2/games/shanhai-xunyilu/src/sim/sim-run.js b/tier2/games/shanhai-xunyilu/src/sim/sim-run.js index bd21c85c..bab41e7b 100644 --- a/tier2/games/shanhai-xunyilu/src/sim/sim-run.js +++ b/tier2/games/shanhai-xunyilu/src/sim/sim-run.js @@ -27,23 +27,28 @@ import { dirname, join } from 'node:path'; import { createRunCore } from '../run-core.js'; import { createSeededRandom } from '../util/seeded-random.js'; -import { RUN_STARTER_DECK, SIM_BANDS } from '../data/tables.js'; +import { RUN_STARTER_DECK, YI_STARTER_DECK, SIM_BANDS } from '../data/tables.js'; const __dir = dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); // 策略单源:与 CDP harness 同一文件(贪心档即九门真玩的同一大脑)。 -const { chooseTap, chooseTapRandom, chooseTapIdle } = require(join(__dir, '../../../../harness/card-battle.driver.js')); +const { chooseTap, chooseTapRandom, chooseTapIdle, chooseTapChain, chooseTapDefensive } = require(join(__dir, '../../../../harness/card-battle.driver.js')); /** 决策随机源派生常数(黄金比 32 位;与游戏 rng 分流)。 */ const DECISION_SEED_XOR = 0x9e3779b9; /** 策略注册表(档名 → 决策函数;random 档吃独立决策 rng)。 */ export const STRATEGIES = { - greedy: (state) => chooseTap(state), - random: (state, rng) => chooseTapRandom(state, rng), - idle: (state) => chooseTapIdle(state), + greedy: (state) => chooseTap(state), // 贪心分(书面三档①;校准对象) + chain: (state) => chooseTapChain(state), // 链优先(书面三档②;M3 S14) + defensive: (state) => chooseTapDefensive(state), // 保血防御(书面三档③;龟缩哨兵) + random: (state, rng) => chooseTapRandom(state, rng), // 下界观测(M2 工单档) + idle: (state) => chooseTapIdle(state), // 下界观测(M2 工单档) }; +/** 书面三档(§6.4;全矩阵与死卡并集口径);random/idle 为下界锚不入死卡判。 */ +export const CANON_STRATEGIES = ['greedy', 'chain', 'defensive']; + /** * 单局仿真:用指定策略档把一局打到终态,采全量观测。 * @param {Object} p @@ -104,6 +109,7 @@ export function simulateRun({ seed, strategy, runOpts = {} }) { else if (t.kind === 'removePick') run.buyRemove(t.index); else if (t.kind === 'removeBack') run.closeRemove(); else if (t.kind === 'shopLeave') run.leaveShop(); + else if (t.kind === 'eventOption') run.chooseEventOption(t.index); // M3 事件 else stuck = true; // 未知靶=契约漂移,响亮暴露 consumeEvents(); if (stuck) break; @@ -112,9 +118,10 @@ export function simulateRun({ seed, strategy, runOpts = {} }) { consumeEvents(); const end = run.readState(); - // 采用率口径:胜局构筑 = 终态卡组减起始组份数(删卡后不在终态=真实反映构筑);起始组不带判。 + // 采用率口径:胜局构筑 = 终态卡组减起始组份数(按职业起始组;删卡后不在终态=真实反映构筑)。 const starterCount = {}; - for (const id of RUN_STARTER_DECK) starterCount[id] = (starterCount[id] || 0) + 1; + const starter = end.heroClass === 'yi' ? YI_STARTER_DECK : RUN_STARTER_DECK; + for (const id of starter) starterCount[id] = (starterCount[id] || 0) + 1; const acquired = []; const left = { ...starterCount }; for (const id of end.deck || []) { @@ -125,6 +132,8 @@ export function simulateRun({ seed, strategy, runOpts = {} }) { return { seed, strategy, + heroClass: end.heroClass, + ascension: end.ascension, phase: end.phase, // win|lose|invalid|run(guard 截断时) act: end.act, stuck, @@ -144,6 +153,10 @@ export function simulateRun({ seed, strategy, runOpts = {} }) { fingerprint: end.phase === 'invalid' ? { deck: (end.deck || []).slice().sort(), relics: (end.relics || []).map((r) => r.id).sort() } : null, + // 胜局构筑指纹(M3 门:去重率 >85%〔草案〕——deck+relics 排序串)。 + winFingerprint: end.phase === 'win' + ? (end.deck || []).slice().sort().join(',') + '|' + (end.relics || []).map((r) => r.id).sort().join(',') + : null, }; } @@ -168,6 +181,9 @@ export function aggregateCell(results, opts = {}) { const winRate = n ? wins.length / n : 0; // 二项分布标准差(千局 σ≈1.6pp 的书面口径同源)。 const sigma = n ? Math.sqrt(winRate * (1 - winRate) / n) : 0; + // 胜局构筑指纹去重率(M3 门 >85%〔草案〕:构筑多样性——同指纹反复出现=路径固化)。 + const winFps = wins.map((w) => w.winFingerprint).filter(Boolean); + const fingerprintUniqueRate = winFps.length ? new Set(winFps).size / winFps.length : null; const allBossTurns = results.flatMap((r) => r.bossTurns).sort((a, b) => a - b); const grind = allBossTurns.filter((t) => t > SIM_BANDS.bossTurnBand[1]).length; // 「磨」 @@ -210,6 +226,7 @@ export function aggregateCell(results, opts = {}) { stucks: stucks.length, winRate: Math.round(winRate * 10000) / 10000, winRateSigmaPp: Math.round(sigma * 10000) / 100, // 百分点 + fingerprintUniqueRate: fingerprintUniqueRate != null ? Math.round(fingerprintUniqueRate * 1000) / 1000 : null, avgBattlesWon: n ? Math.round(results.reduce((s, r) => s + r.stats.battlesWon, 0) / n * 100) / 100 : 0, avgDeckEnd: n ? Math.round(results.reduce((s, r) => s + r.deckCount, 0) / n * 10) / 10 : 0, bossTurns: { diff --git a/tier2/games/shanhai-xunyilu/src/sim/valuation.js b/tier2/games/shanhai-xunyilu/src/sim/valuation.js index 69050e66..c5ac8fa0 100644 --- a/tier2/games/shanhai-xunyilu/src/sim/valuation.js +++ b/tier2/games/shanhai-xunyilu/src/sim/valuation.js @@ -47,6 +47,11 @@ export function valueOf(card) { v += bv * V.chainAchieveDiscount; } if (fx.costChainDiscount) v += fx.costChainDiscount.n * V.energyCost * V.chainAchieveDiscount; + if (fx.ifZhoutian) { + const z = fx.ifZhoutian; + // 周天达成率低(A0 观测 ≈0.1/局;乙链优构筑内更高),按 0.2 折。 + v += ((z.dmg || 0) / V.dmgPerCost + (z.block || 0) / V.blockPerCost + (z.draw || 0) * V.drawCost + (z.energy || 0) * V.energyCost) * 0.2; + } const value = v / Math.max(card.cost, V.zeroCostFloor); const cap = V.rarityCaps[card.rarity] != null ? V.rarityCaps[card.rarity] : 1.0; return { diff --git a/tier2/games/shanhai-xunyilu/src/ui/layout.js b/tier2/games/shanhai-xunyilu/src/ui/layout.js index 11f824f8..ca198610 100644 --- a/tier2/games/shanhai-xunyilu/src/ui/layout.js +++ b/tier2/games/shanhai-xunyilu/src/ui/layout.js @@ -114,6 +114,13 @@ export function computeTapTargets(s) { targets.push({ kind: 'cancel', x: VIEW_W / 2, y: (handTop() + VIEW_H) / 2, w: VIEW_W, h: VIEW_H - handTop() }); return targets; } + // 事件屏(M3 S16):选项按钮纵排(2–3 个)。 + if (s.screen === 'event' && s.event) { + s.event.options.forEach((o, i) => { + targets.push({ kind: 'eventOption', index: i, x: VIEW_W / 2, y: VIEW_H * 0.52 + i * 64, w: 320, h: 52 }); + }); + return targets; + } // 商店(M2 S11):5 卡位两行 + 2 遗物位 + 删卡/离开按钮。 if (s.screen === 'shop' && s.shop) { s.shop.cards.forEach((c, i) => { @@ -153,6 +160,27 @@ export function computeTapTargets(s) { return targets; } +/** + * home 局外屏可点靶(M3 S15;工厂级投影消费——局外交互不属于一局,不进 run-core): + * main 子屏=职业二选 + 进阶加减 + 出巡/每日 + 图鉴/成就入口;collection/achievements 子屏=返回。 + */ +export function computeHomeTargets(s) { + const targets = []; + if (s.screen === 'main') { + targets.push({ kind: 'homeClass', class: 'jia', x: VIEW_W / 2 - 88, y: VIEW_H * 0.34, w: 150, h: 110 }); + targets.push({ kind: 'homeClass', class: 'yi', x: VIEW_W / 2 + 88, y: VIEW_H * 0.34, w: 150, h: 110 }); + targets.push({ kind: 'ascMinus', x: VIEW_W / 2 - 110, y: VIEW_H * 0.5, w: 52, h: 44 }); + targets.push({ kind: 'ascPlus', x: VIEW_W / 2 + 110, y: VIEW_H * 0.5, w: 52, h: 44 }); + targets.push({ kind: 'homeStart', x: VIEW_W / 2, y: VIEW_H * 0.62, w: 240, h: 56 }); + targets.push({ kind: 'homeDaily', x: VIEW_W / 2, y: VIEW_H * 0.72, w: 240, h: 48 }); + targets.push({ kind: 'homeCollection', x: VIEW_W / 2 - 90, y: VIEW_H * 0.82, w: 150, h: 44 }); + targets.push({ kind: 'homeAchievements', x: VIEW_W / 2 + 90, y: VIEW_H * 0.82, w: 150, h: 44 }); + } else { + targets.push({ kind: 'homeBack', x: VIEW_W / 2, y: VIEW_H * 0.92, w: 160, h: 46 }); + } + return targets; +} + /** 命中判定:按数组序返回首个包含 (x,y) 的靶(无 → null)。injectInput 与场景共用。 */ export function hitTarget(targets, x, y) { for (const t of targets) { diff --git a/tier2/harness/card-battle.driver.js b/tier2/harness/card-battle.driver.js index 92081696..688b8734 100644 --- a/tier2/harness/card-battle.driver.js +++ b/tier2/harness/card-battle.driver.js @@ -67,7 +67,13 @@ function pickValue(card) { * 贪心档 v1(§6.4 三档 driver 之一;链优先/保血防御两档随 M2 S12)。 */ function chooseTap(state) { - if (!state || state.phase !== 'run') return null; // win/lose/invalid 终态 → 停,交 latch 校验 + if (!state) return null; + // ── 局外 home 屏(M3 S15):CDP/九门真玩路径 boot 后先落 home——直接启程(缺省甲/A0); + // win 后终局遮罩点掉会回 home,phase='home' 非终态,latch 门只认 win/lose/invalid。── + if (state.phase === 'home') { + return findTarget(state, 'homeStart') || null; + } + if (state.phase !== 'run') return null; // win/lose/invalid 终态 → 停,交 latch 校验 // ── 地图:选节点(血线驱动的贪心偏好)── if (state.screen === 'map') { @@ -85,6 +91,26 @@ function chooseTap(state) { return nodes[0]; } + // ── 事件屏(M3 S16):选项期望当量最大(fx 语义投影;血亏加权回复,金紧看收益)── + if (state.screen === 'event' && state.event) { + const hpRatio = state.hero.hp / state.hero.hpMax; + const evalOpt = (o) => { + const fx = o.fx || {}; + let v = 0; + v += (fx.gold || 0) * 0.03; + v += (fx.heal || 0) * (hpRatio < 0.6 ? 0.35 : 0.1); + v -= (fx.loseHp || 0) * (hpRatio < 0.5 ? 0.5 : 0.2); + v += (fx.hpMax || 0) * 0.35; + if (fx.relic) v += 4; + if (fx.card === '稀有') v += 2.2; else if (fx.card === '罕见') v += 1.4; else if (fx.card) v += 0.6; + if (fx.removeRandomCommon) v += 0.8; + return v; + }; + let best = 0; + (state.event.options || []).forEach((o, i) => { if (evalOpt(o) > evalOpt(state.event.options[best])) best = i; }); + return findTarget(state, 'eventOption', (t) => t.index === best); + } + // ── Boss 遗物三选一(M2):遗物全规则级增益,贪心=永远拿第一枚(估值细分随 S12 调参轮)── if (state.screen === 'relicReward' && state.relicReward) { return findTarget(state, 'relicReward', (t) => t.index === 0) || findTarget(state, 'relicSkip'); @@ -219,6 +245,108 @@ function chooseTap(state) { return null; } +/* ══ M3 S14 书面三档补齐:链优先 / 保血防御(书 §6.4 原三档=贪心分/链优先/保血防御; + * 与 M2 的随机/挂机下界档并存——多样性 driver 是死卡治理与采用率带的观测面)══ */ + +/** + * 链优先档:链长与周天最大化(乙职业的天然打法代理)。 + * 战斗序:①延链卡里挑费用最低(链不断优先于当量)②空链时按「预算内可串最长」起手(与贪心④同算法) + * ③无链可延时打 0 费润滑 ④endTurn。地图/商店/奖励沿贪心(pickValue 里链词已计价)。 + */ +function chooseTapChain(state) { + if (!state || state.phase !== 'run') return null; + if (state.screen === 'battle' && state.battle) { + const b = state.battle; + if (state.ui && state.ui.selected >= 0) { + return findTarget(state, 'card', (t) => t.index === state.ui.selected); + } + const hand = (b.handCards || []).map((c, i) => ({ ...c, i })) + .filter((c) => c.cost <= b.energy && !(c.hpCost > 0 && b.hero.hp <= c.hpCost)); + if (!hand.length) return findTarget(state, 'endTurn'); + // ①延链:同延链集内费低者先(省能量串更长)。 + if (b.chain && b.chain.nextElement) { + const chainable = hand.filter((c) => c.element === b.chain.nextElement).sort((a, z) => a.cost - z.cost || (z.dmg + z.block) - (a.dmg + a.block)); + if (chainable.length) return findTarget(state, 'card', (t) => t.index === chainable[0].i); + } + // ②空链起手:预算内可串最长(贪心④同构)。 + if (b.chain && Array.isArray(b.chain.cycle) && (!b.chain.seq || b.chain.seq.length === 0)) { + const cycle = b.chain.cycle; + let best = null; + for (const start of hand) { + const used = new Set([start.i]); + let budget = b.energy - start.cost; + let el = cycle[(cycle.indexOf(start.element) + 1) % cycle.length]; + let len = 1; + for (let step = 0; step < 8; step++) { + const next = hand.filter((c) => !used.has(c.i) && c.element === el && c.cost <= budget).sort((a, z) => a.cost - z.cost)[0]; + if (!next) break; + used.add(next.i); budget -= next.cost; len += 1; + el = cycle[(cycle.indexOf(el) + 1) % cycle.length]; + } + if (!best || len > best.len) best = { card: start, len }; + } + if (best) return findTarget(state, 'card', (t) => t.index === best.card.i); + } + // ③破序不可避:能量照花(链是回合内轴、回合末必清——「留链势」是伪概念,首版 + // 每回合只打链段致胜率 5%/1.5%,batch 实测修正):0 费垫子优先,再按裸当量清能量。 + const zero = hand.filter((c) => c.cost === 0).sort((a, z) => (z.dmg + z.block) - (a.dmg + a.block))[0]; + if (zero) return findTarget(state, 'card', (t) => t.index === zero.i); + const spend = hand.sort((a, z) => (z.dmg + z.block + z.burn) - (a.dmg + a.block + a.burn))[0]; + if (spend) return findTarget(state, 'card', (t) => t.index === spend.i); + return findTarget(state, 'endTurn'); + } + return chooseTap(state); // 地图/奖励/商店/遗物屏沿贪心策略(链价值已在 pickValue 计价) +} + +/** + * 保血防御档:受伤最小化(龟缩代理——「防御过值→龟缩最优」的曲线哨兵,§6.2 攻防本位差观测面)。 + * 战斗序:①来袭未挡满 → 甲/虚弱卡按防御量优先 ②挡满后血亏 → 回血卡 ③安全后才输出 ④endTurn。 + */ +function chooseTapDefensive(state) { + if (!state || state.phase !== 'run') return null; + if (state.screen === 'battle' && state.battle) { + const b = state.battle; + if (state.ui && state.ui.selected >= 0) { + return findTarget(state, 'card', (t) => t.index === state.ui.selected); + } + const hand = (b.handCards || []).map((c, i) => ({ ...c, i })) + .filter((c) => c.cost <= b.energy && !(c.hpCost > 0 && b.hero.hp <= c.hpCost)); + if (!hand.length) return findTarget(state, 'endTurn'); + const it = b.enemy.intent; + const incoming = it && (it.kind === 'attack' || it.kind === 'attackBlock') ? it.n * (it.times || 1) : 0; + // ①挡满来袭:甲量最大者先(虚弱卡算防御件:折减来袭)。 + if (incoming > 0 && b.hero.block < incoming) { + const guard = hand.filter((c) => c.block > 0 || c.weak > 0) + .sort((a, z) => (z.block + z.weak * 3) - (a.block + a.weak * 3))[0]; + if (guard) return findTarget(state, 'card', (t) => t.index === guard.i); + } + // ②血亏回血。 + if (b.hero.hp < b.hero.hpMax * 0.7) { + const healCard = hand.filter((c) => c.heal > 0).sort((a, z) => z.heal - a.heal)[0]; + if (healCard) return findTarget(state, 'card', (t) => t.index === healCard.i); + } + // ③安全后输出(裸当量序;链是顺带不是追求)。 + const atk = hand.filter((c) => c.dmg > 0 || c.burn > 0).sort((a, z) => (z.dmg + z.burn) - (a.dmg + a.burn))[0]; + if (atk) return findTarget(state, 'card', (t) => t.index === atk.i); + const spare = hand.sort((a, z) => z.block - a.block)[0]; + if (spare && spare.block > 0) return findTarget(state, 'card', (t) => t.index === spare.i); + return findTarget(state, 'endTurn'); + } + // 地图偏好:保血路线(憩/商店前置)。 + if (state.screen === 'map') { + const nodes = ((state.tapTargets) || []).filter((t) => t.kind === 'node'); + if (!nodes.length) return null; + const hpRatio = state.hero.hp / state.hero.hpMax; + const prefer = hpRatio < 0.8 ? ['rest', 'treasure', 'shop', 'battle', 'boss', 'elite'] : ['treasure', 'shop', 'battle', 'rest', 'boss', 'elite']; + for (const k of prefer) { + const hit = nodes.find((t) => t.nodeType === k); + if (hit) return hit; + } + return nodes[0]; + } + return chooseTap(state); // 奖励/商店/遗物沿贪心 +} + /* ══ M2 S12 三档策略 driver(工单口径:贪心/随机/挂机;A0 带校准以贪心档为准,不削敌线迁就最弱策略)══ * 贪心档 = 上面的 chooseTap(CDP 真玩与 headless 冒烟同一大脑,策略单源); * 随机/挂机档只供 headless 仿真外壳(sim-shell)消费,CDP 真玩不用——additive 导出,不改既有行为。 @@ -248,7 +376,7 @@ function chooseTapRandom(state, rng) { } // 地图/奖励屏:全部可点靶等权随机(奖励含跳过;地图=随机走图)。 const pool = ts.filter((t) => t.kind === 'node' || t.kind === 'reward' || t.kind === 'rewardSkip' - || t.kind === 'relicReward' || t.kind === 'relicSkip' + || t.kind === 'relicReward' || t.kind === 'relicSkip' || t.kind === 'eventOption' || t.kind === 'shopCard' || t.kind === 'shopRelic' || t.kind === 'shopRemove' || t.kind === 'shopLeave' || t.kind === 'removePick' || t.kind === 'removeBack'); if (!pool.length) return null; @@ -272,6 +400,7 @@ function chooseTapIdle(state) { } if (state.screen === 'shop') return findTarget(state, 'shopLeave'); if (state.screen === 'shopRemove') return findTarget(state, 'removeBack'); + if (state.screen === 'event') return findTarget(state, 'eventOption', (t) => t.index === 0); // 挂机:事件拿头一项 const node = findTarget(state, 'node'); return node; } @@ -306,4 +435,4 @@ async function run(cdp, driver, hashes, deps) { return { drove, steps }; } -module.exports = { run, chooseTap, chooseTapRandom, chooseTapIdle }; +module.exports = { run, chooseTap, chooseTapRandom, chooseTapIdle, chooseTapChain, chooseTapDefensive };