chore: sync ammo readout with upstream dev (#552)
This commit is contained in:
222 files changed
+10419
-2026
No files matched your search
@@ -3,6 +3,11 @@
|
||||
Owner: `tools/fetch-assets.mjs` and `tools/assets/*`. Research background: `docs/research/07-assets.md`.
|
||||
|
||||
All official game art, Spine models and audio are **downloaded at install time**. They are never committed; `public/assets/` is git-ignored.
|
||||
|
||||
The UI sound `audio.sfx.ui.bondUp` is the official `ON_ACT1AUTOCHESS_ADD_BOND` bank's
|
||||
`act1autochess_b_ui_buffup`. Live prep/battle layer gains use it as well as bond activation. Its bank settings are
|
||||
`maxSoundAllowed: 1` and `popOldest: true`: one dedicated SFX channel, replacing the previous sound without an extra
|
||||
cooldown. It respects the SFX/mute settings; a stale asynchronous decode cannot restart a replaced cue (#482).
|
||||
All downloaded game assets the client needs are listed in **`data/assets.json`**. Game-art requests must use that manifest.
|
||||
The original project icons below are shipped static files, not downloaded game assets.
|
||||
|
||||
@@ -160,6 +165,7 @@ The research JSONs in `docs/research/` (03, 05, 07) define **which** ids are nee
|
||||
| Enemy Spine (PC build, premultiplied alpha) | isHarryh/Ark-Models `models_enemies/{key}/`, file names from `models_data.json` | `spine/enemy/{enemyId}/{stem}.*` |
|
||||
| Enemy Spine that no dump carries (灼热源石虫 / 炽焰源石虫) | the local client only (`tools/local-extract/extract.py ENEMY_SPINES`, optional); never downloaded and never required: an overlay of the web alias (`enemies[id].spineLocal`) | `local/spine/enemy/{enemyId}/{stem}.*` (listed in `data/local-assets.json`) |
|
||||
| Original token Spine overlays (40 summons: most 自选 summons, 凯瑟琳's 爬行号·防护单元, 凛御银灰's 风雪之眼, 香槟炸弹) | local-client extraction (`tools/local-extract/extract.py TOKEN_SPINES`, optional); the original overlay is preferred, with any listed web model retained as fallback | `local/spine/token/{tokenId}/{stem}.*` (listed in `data/local-assets.json`) |
|
||||
| Official battlefield scenes (one per battlefield: meshes, lightmaps, albedo textures; DESIGN §15, DESIGN §30.21) | the scene bundles `scenes/activities/<season>/level_<stage>/…` (`tools/local-extract/extract.py --only scene`, optional). The client carries them only while the season runs; off season `tools/local-extract/fetch_scenes.py` downloads the bundles of the in-season version `26-04-13-12-31-42_e9b560` from the official CDN into `.cache/official-scenes/` (33 bundles, 22,801,308 bytes compressed on the Android list, md5-checked), and the materials / textures still come from the local client's theme bundles. The 0.2.5 full release zip includes the extracted files under `public/assets/local/scene/` when the maintainer extracted them before packaging; a lite zip and a source install may fetch and extract them (`node tools/setup.mjs --scenes`). The repository never stores the bundles or the extracted files. Without a scene the 3D board draws its procedural metal tiles | `local/scene/{stageId}/scene.json`, `scene.bin`, `lm{i}.*`; `local/scene/tex/{texture}.*` (listed in `data/local-assets.json`) |
|
||||
| BGM | AA2 `voice` branch `audio/sound_beta_2/music/**` (大厅/休整期 `act1autochess`, 开战 `act13side/m_bat_kazimierz2_{1,2}` — 骑士之日 / 无畏者; the 开战 track follows the round: `_2` 无畏者 rounds 1–7, `_1` 骑士之日 from round 8) | `audio/bgm/{file}.mp3` |
|
||||
| SFX (UI, battle, per unit) | AA2 `voice` `audio/sound_beta_2/**`, mapped from `audio_data.json` banks | `audio/sfx/{same sub-path}.mp3` |
|
||||
| 干员战斗语音 | AA2 `voice` `audio/sound_beta_2/voice_cn/{charId}/cn_nn.mp3` (Chinese, `audio.voice`) and `voice/{charId}/cn_nn.mp3` (Japanese, `audio.voiceJp`: the same file names) — the lines `charword_table.json` lists (`placeType` = when the game plays one, `voiceAsset` = the path); `--voice-lang=jp|en|kr` fills `audio.voice` from `voice/`, `voice_en/`, `voice_kr/` instead | `audio/voice/cn/{charId}/{cn_nn}.mp3`, `audio/voice/jp/{charId}/{cn_nn}.mp3` (`audio/voice/{lang}/…` for another `--voice-lang`) |
|
||||
|
||||
+13
-3
@@ -112,7 +112,7 @@ Top level: `{ season, seasonName, modes, economy, lpCapPerRound, bossOvertimeAft
|
||||
| `poolCopies[tier]` + `poolCopiesOverrides` | 12/14/18/16/8/5 plus measured low-tier identity exceptions (DESIGN §29.8) | single-player copies; `coopPoolMultiplier: 2` doubles shared operators, not private DIY stock |
|
||||
| `itemPoolCopies[tier]` + `itemPoolCopiesOverrides` | 4/6/7/8/7/3 plus eight measured exceptions (DESIGN §29.8) | shared shop-equipment ownership: normal 1, upgraded 2; non-shop effect-only equipment unlimited |
|
||||
| `goldenCopies`, `mergeCount`, `mergeCountOverrides`, `itemMergeCount` | `3`, `3`, `{"chess_char_2_11_a":2}` (风丸), `2` | |
|
||||
| `rewardOffer` | `{"count":3,"tierOffset":1,"maxTier":6,"price":0,…}` | merge reward: 3 **different** free chess of tier `min(shopLevel+1,6)` (a short tier tops up from the tier below; `PlayerState.pushRewardOffer`, user playtest #6 item 19) |
|
||||
| `rewardOffer` | `{"count":3,"tierOffset":1,"maxTier":6,"price":0,…}` | merge reward: 3 **different** free chess of tier `min(shopLevel+1,6)` (a short tier tops up from the tier below; the player's stocked 自选 of that tier joins the roll, DESIGN §30.23; `PlayerState.pushRewardOffer`, user playtest #6 item 19) |
|
||||
| `handFillOrder` | `"rightToLeft"` | |
|
||||
| `shopOdds` | `{"model":"copyWeighted",…}` | [ASSUMED] roll model |
|
||||
| `borrowCount`, `fallbackBondId`, `defaultBandId`, `defaultStartLp` | `20`, `"emptyShip"`, `"band_bldsk"`, `28` | |
|
||||
@@ -642,8 +642,9 @@ prototype. Built by `tools/build-data.mjs buildBackups`; `shared/standIn.js` com
|
||||
`shared/diy.js` adds the 自选 rules on top. The sim fields both (docs/SIM.md §12). Played since 0.2.0: the player fills
|
||||
the four slots on the 自选编队 tab (`room.diy { picks }`, docs/PLAYING.md §3; shared/protocol.js checkDiyPicks keeps the
|
||||
legal picks), the match takes the picks the seat had at its start (`PlayerState.diy`), and each slotted piece is sold in
|
||||
that player's shop only — its own stock (the tier's pool copies, 8 / 5 [ASSUMED]), from the 调度中心 level `shopLevel`,
|
||||
none when every bond of it is banned this match — and is the operator for every rule of that player (its data view:
|
||||
that player's shop only — its own stock (the tier's pool copies, 8 / 5 [ASSUMED]). The ordinary shop draws it from the
|
||||
调度中心 level `shopLevel`; the promotion reward draws 自选 of the reward tier, one shop level earlier (DESIGN §30.23).
|
||||
It has no stock when every bond of it is banned this match, and it is the operator for every rule of that player (its data view:
|
||||
server/match/player/diy.js, docs/META.md §3). The rules in the data (activity_table act2autochess `charShopChessDatas`; PRTS 卫戍协议, 卫戍协议:盟约
|
||||
下半/PRTS盟约记录):
|
||||
|
||||
@@ -737,3 +738,12 @@ the DIY stock (per player in the match, `PlayerState.diyStock`: `config.economy.
|
||||
field), 助战 borrows (`borrowCount` 20), and a player's potential / 练度 per operator (the 干员调配 settings sent with
|
||||
`room.loadout`; the forms are built at full potential and carry the lower ranks, §2.3; 练度 = effects.json
|
||||
`aceffect_char_1…4`).
|
||||
|
||||
### Acquisition subsets corrected in 0.2.5 (§30.2)
|
||||
|
||||
`tools/build-data.mjs` owns the measured Rockrock/Flametail subset weights from
|
||||
[路标月报#2](https://www.bilibili.com/video/BV1eLXXBqEgF/). Rockrock: ordinary hammer 10; tremble, undying, haste,
|
||||
burn hammers 2 each; limited accelerator 1; shield drone and laser emitter 4 each. Flametail: Wild Mane 10,
|
||||
Ashlock 10, Fartooth 3. Rebuild `choices.json` offline; these weights do not exempt draws from shared availability.
|
||||
The runtime derives unlimited fixed-gift families from `SERVER_GAIN_EQUIP`; this is a grant-source exemption,
|
||||
not a zero-occupancy flag or a manually maintained second list in generated data.
|
||||
+7
-1
@@ -26,7 +26,7 @@
|
||||
```
|
||||
装完**关闭并重新打开**终端,`node -v` 应显示 v22 或更高(winget 的 LTS 目前是 v24.x,同样可用)。没有 winget 时从 <https://nodejs.org/zh-cn/download> 和 <https://git-scm.com/download/win> 下载安装。
|
||||
2. 下载,三选一。建议放在一个固定、短、**不在 OneDrive 同步范围内**的目录,例如 `C:\Stronghold-Protocol`:
|
||||
- **完整包(推荐)**:在仓库的 [Releases](https://github.com/sganggs/Stronghold-Protocol/releases) 页面下载最新版本的 `Stronghold-Protocol-v<版本>.zip`(约 505 MB,解压后约 710 MB;已含运行依赖、前端库和全部素材,包括中文、日文两套干员语音和官方 3D 棋盘等本地客户端素材),解压后把里面的 `Stronghold-Protocol` 文件夹放到上述位置。不需要 Git,首次启动也不用再下载素材。素材版权归上海鹰角网络 / Yostar,仅限非商业使用,见 [NOTICE.md](../NOTICE.md)。
|
||||
- **完整包(推荐)**:在仓库的 [Releases](https://github.com/sganggs/Stronghold-Protocol/releases) 页面下载最新版本的 `Stronghold-Protocol-v<版本>.zip`(约 550 MB,解压后约 780 MB;已含运行依赖、前端库和全部素材,包括中文、日文两套干员语音和官方 3D 棋盘、各战场官方场景等本地客户端素材),解压后把里面的 `Stronghold-Protocol` 文件夹放到上述位置。不需要 Git,首次启动也不用再下载素材。素材版权归上海鹰角网络 / Yostar,仅限非商业使用,见 [NOTICE.md](../NOTICE.md)。
|
||||
- **精简包**:同一页面的 `Stronghold-Protocol-v<版本>-lite.zip`(约 22 MB)。代码、运行依赖和前端库与完整包相同,但不带素材:美术、Spine 模型、音频(含两套干员语音)、字体、表情和「玩法说明」教程图在首次启动时由 setup 从公开镜像下载(约 550 MB,显示进度,可中断续传;镜像设置见下面的「国内镜像下载」)。官方 3D 棋盘等本地客户端素材需要用本机客户端提取,或从同一版本的完整包复制(第 6 节)。适合下载大文件不方便、或想先下一个小包的情况;放置方式同完整包。
|
||||
- **源码**:
|
||||
```powershell
|
||||
@@ -300,6 +300,7 @@ services:
|
||||
| 内容 | 没有本地素材时 |
|
||||
|---|---|
|
||||
| 官方 3D 棋盘(贴图、模型、地图特效) | 2D 棋盘,地块由程序绘制 |
|
||||
| 各战场的官方场景(沙地等 11 个战场,0.2.5 素材包;完整包在提取后带上,精简包和源码可在本机下载并提取,见下) | 3D 棋盘用程序生成的金属地块 |
|
||||
| 部分官方界面图标与底板:交流按钮和表情面板的边框、暂停面板、装备替换窗口、干员调配界面、队友状态与漏怪标记、模组类型图标等 | 样式相近的替代图形、图标或文字 |
|
||||
| 灼热 / 炽焰源石虫的官方模型 | 染成橙色 / 红橙色的普通源石虫 |
|
||||
| 39 个召唤物的官方模型(多数自选召唤物,以及凯瑟琳的爬行号·防护单元、凛御银灰的风雪之眼;公开镜像没有) | 召唤物头像(菱形底板) |
|
||||
@@ -310,6 +311,8 @@ services:
|
||||
|
||||
**0.2.0 之前提取过的**:召唤物模型是 0.2.0 新增的提取项,旧的提取结果里没有(`node tools/setup.mjs` 会提示「缺少新版的自选召唤物模型」)。有客户端的电脑运行 `node tools/setup.mjs --local` 重新提取即可,只想补这一项也可以在提取用的 Python 环境里运行 `tools/local-extract/extract.py --only spine/token`(新文件写入 `public/assets/local/spine/token/`,清单里其他条目保持不变)。
|
||||
|
||||
**各战场的官方场景**(0.2.5 素材包,维护者 2026-10-12:可以进完整包;精简包和源码安装可以在本机下载):官方棋盘是每个战场一个做好的场景(11 个战场,其中 9 个是沙地)。场景包只在赛季开放期间随客户端分发,赛季外的客户端里没有。仓库不存放这些官方文件。有客户端的电脑可以 `node tools/setup.mjs --scenes`(询问或强制下载并提取;`--no-scenes` 跳过)。也可以分两步:`python3 tools/local-extract/fetch_scenes.py`(从官方 CDN 下载赛季版本 `26-04-13-12-31-42_e9b560` 的 33 个场景包,共 22,801,308 字节,放进 `.cache/official-scenes/`;`--list` 只看清单不下载),再用提取环境的 Python 运行 `tools/local-extract/extract.py --only scene`(材质和贴图仍取自本机客户端;`--game` 可指定客户端目录)。没有 Python 3.8+,或 UnityPy / lz4 / Pillow 没装好时,setup 只提示并继续,3D 棋盘用金属地块。没有客户端时贴图取不出来,从同一版本的完整包复制 `public/assets/local/` 和 `data/local-assets.json`。作者估计每个场景在进对局时另下载约 2–7 MB,各场景共用贴图约 3 MB;本机没有跑过提取,完整包因此增加的解压体积没有实测。
|
||||
|
||||
**3D 棋盘贴图的下载量**:每位玩家进入对局时都要从开服的电脑下载 3D 棋盘的 12 张贴图。提取时会给这 12 张各写一份 WebP(颜色贴图有损、质量 95,法线和数据贴图无损),清单里列的是 WebP,同名 PNG 留在旁边给裁切工具和 setup 用。这部分下载量从约 6.7 MB 降到约 2 MB,网速慢的远程联机最明显。只有 PNG 的本地素材(例如在这一改动之前提取的)可以用提取时的 Python 环境运行 `tools/local-extract/extract.py --webp` 就地补上,只需要 Pillow,不需要客户端。
|
||||
|
||||
## 7. 打包发布(维护者)
|
||||
@@ -323,9 +326,12 @@ npm run package:lite -- --out <目录> # 精简包 Stronghold-Protocol-v<版
|
||||
npm run package -- --update --from <旧版本的完整包>[,<…>] --out <目录> # 更新包 Stronghold-Protocol-v<版本>-update.zip
|
||||
```
|
||||
|
||||
完整包在打包机已经提取战场场景时带上 `public/assets/local/scene/`(`--dry-run` 摘要里的 `battlefield scenes` 一行)。还没有提取时完整包仍能打,棋盘用金属地块。要带上 11 个官方场景,先运行 `python3 tools/local-extract/fetch_scenes.py`,再用提取环境的 Python 运行 `tools/local-extract/extract.py --only scene`(或 `node tools/setup.mjs --scenes`),然后再 `npm run package`。精简包不带场景。`.cache/official-scenes/` 在拒绝名单里,不会打进任何包。`tools/local-extract/scenes.py` 和 `fetch_scenes.py` 必须先提交:打包读的是 `git ls-files`,未提交的脚本不会进玩家包。
|
||||
|
||||
- **打进去的**:`git ls-files` 里的 `server/`、`shared/`、`data/`、`public/`(不含 `public/dev/`)、`packs/`(随仓库提交的内容包;只在本机安装、没提交的不打进去)、启动脚本、玩家会运行的工具(setup、vendor、fetch-assets 与 `tools/assets/`、doctor,以及 setup 调用的 `tools/local-extract/` 和 `crop-board-atlas.mjs`)、服务器和 fetch-assets 读取的 4 张研究数据表(`docs/research/` 的 `03-operators`、`05-enemies`、`05-maps`、`07-assets` 四个 JSON)、`package.json` / `package-lock.json`、许可证与说明(`LICENSE`、`NOTICE.md`、`THIRD-PARTY-NOTICES.md`、`README.md`、`CHANGELOG.md`)、`docs/PLAYING.md` 和本文;然后在临时目录里生成 `packs/index.json`(打进去的语言包和内容包的列表,供纯静态托管使用;服务器自己会实时列出,见 [PACKS.md](PACKS.md)),再 `npm ci --omit=dev` 装上运行依赖和 `public/vendor`。完整包再加上 `data/assets.json` 列出的素材、`public/fonts`,以及本地提取的 `public/assets/local/` 和 `data/local-assets.json`。磁盘上有、清单却没列出的文件不打进去(例如 0.2.0 移出自选的焰狐龙梓兰的旧素材)。日文干员语音(`audio.voiceJp`,`public/assets/audio/voice/jp/` 的 2674 个文件,约 85 MB,zip 后约 76 MB)默认也打进完整包;`tools/package.mjs` 里的开关 `FULL_ZIP_JP_VOICE` 改成 `false` 时,完整包(以及由它比较出的更新包)不带这些文件:玩家首次启动时 setup 会像精简包那样下载它们,下载完成前选「日本語」会播中文语音,更新包也不会删除玩家已有的日文语音。
|
||||
- **不打进去的**:`test/`、维护用的工具(数据构建、golden、botbench、i18n、导入检查、本工具等)、`scripts/make-windows-bundle.mjs`(Windows 便携包,见 [WINDOWS.md](WINDOWS.md))、其他文档、研究笔记和 `docs/img/`、`handoff/`、`.github/`、`types/`、lint / 编辑器 / Docker 配置。和 0.1.x 的整树打包(全部跟踪文件加上 `public/assets` 的全部内容)相比,0.2.0 的完整包少了约 640 个文件、解压后小约 26 MB,zip 小约 8 MB。
|
||||
- **打包前的检查**(`--dry-run` 也全部做一遍):拒绝名单(`pv`、`review`、`.cache`、`.claude`、`.git`、`logs`、`.env`、`scripts/service.env.cmd`、`handoff`、`test` 等);每个打进去的模块的相对导入、玩家用的 npm 脚本(start / setup / doctor / launch / postinstall / vendor / assets)都指向包里的文件;完整包里 `data/assets.json` 和 `data/local-assets.json` 列出的文件都在(缺了先运行 `node tools/fetch-assets.mjs`);没有只差大小写的两个路径;包里的文件(二进制素材也查)不含个人目录路径(`/Users/…`、`C:\Users\…`、`/home/…`)或本机的账户名(运行时从系统读取;`SP_PACKAGE_SCAN_NAMES=a,b` 可以再加名字);打进去的已跟踪文件没有未提交的改动(重新生成的 `data/assets.json` 要先提交)。有任何问题都会列出原因、以非零状态结束,不写 zip;正式打包时还会核对临时目录里的文件和计划完全一致。
|
||||
- **玩家包里的脚本和文档链接**:临时目录里的 `package.json` 只留下每条 `node <文件>` 都指向包内文件、并且至少指出一个文件的脚本(`start`、`dev`、`setup`、`doctor`、`launch`、`postinstall`、`vendor`、`assets`)。`test`、`golden`、数据构建、lint、本工具等不留,避免玩家运行 `npm test` 时在没有测试文件的情况下得到「0 个测试、成功退出」。`README.md`、`CHANGELOG.md`、`NOTICE.md`、`THIRD-PARTY-NOTICES.md`、`docs/PLAYING.md` 和本文里,指向包内没有的文件的相对链接改成该版本 GitHub 标签上的同一路径(版本号去掉末尾的 `-dev` 后加 `v`;图片用 `raw`,其余用 `blob`);`../../releases/latest` 改为仓库的 Releases 地址。源码仓库里的这些文件仍是相对链接。
|
||||
- **MANIFEST.json**:三种包的根目录都有(`npm ci` 之后写入):除素材(`public/assets/`、`public/fonts/`、`data/assets.json`、`data/local-assets.json`,归 setup 管)以外每个文件的大小和 sha256,同一版本的三种包内容相同。`npm run doctor` 和更新包的启动检查(`server/update.js`)用它核对安装。
|
||||
- **更新包**(0.2.1 起,每个版本都发):`--from` 后面列出**之前每个 0.2.x 版本的完整包**(Releases 上的 zip,或它解压出来、没动过的文件夹;逗号分隔或写多个 `--from`),例如发布 0.2.2 时 `--from Stronghold-Protocol-v0.2.0.zip,Stronghold-Protocol-v0.2.1.zip`。工具照常构建完整包的临时目录(`npm ci` 等),逐个文件(大小 + sha256)和每个旧版本比较:和任何一个旧版本不同、或旧版本没有的文件都打进去,所以一个更新包能覆盖在列出的每个版本上;旧版本有、新版本没有的文件记进 `UPDATE.json` 的 `removed`(连同各旧版本里的 sha256,玩家那边只删内容一致的文件;只差大小写的同名文件和 `.env` 之类的本机文件名不删,摘要里会列出)。旧版本必须比当前版本旧、带素材(不能是精简包或更新包)、每个版本只给一次,`--dry-run` 只读取并检查旧版本。更新包里还有新版本的 `MANIFEST.json` 和 `UPDATE.json`(适用的旧版本、文件数、字节数、文件列表和 `removed`),同样经过拒绝名单和个人信息检查;摘要列出和每个旧版本相比改动 / 新增 / 删除的文件数和更新包里各类文件的数量。发布时在 Releases 说明里写明更新包适用的版本(即 `--from` 列出的版本)。`--keep-stage` 保留更新包目录和完整包的临时目录(`<目录>/Stronghold-Protocol-v<版本>-update/` 里的 `Stronghold-Protocol/` 和 `.full/`)。
|
||||
- **需要**:已下载素材的仓库(完整包)——打包前先联网运行一次 `node tools/fetch-assets.mjs`,补齐清单计划但本机还没有的素材(清单只列出磁盘上有的文件,打包工具看不出缺了哪些;`data/assets.json` 有变化就先提交);能访问 npm 的网络(`npm ci`);`zip`(或 bsdtar 的 `tar`,Windows 10 起自带);更新包还要之前各版本的完整包(工具自己读 zip,不需要 `unzip`;Releases 上可以重新下载)。`--out` 默认是系统临时目录下的 `stronghold-protocol-release`,不能在仓库里面;`--force` 覆盖已有的 zip,`--keep-stage` 保留打包用的目录供检查。
|
||||
+2
-1
@@ -4,7 +4,7 @@ This is the **single source of truth** for every implementer. Research lives in
|
||||
|
||||
Language: player-facing text is **Simplified Chinese** by default, with an English switch since 0.2.0 (§25.2, docs/I18N.md: UI strings through `t('中文')`, game texts from the official Chinese data or the official EN client's). Code, comments and identifiers are English.
|
||||
|
||||
Versions: the first public release was **0.1.0** (2026-10-02, the state of §0–§20.15); the releases after it add the player feedback and GitHub reports — 0.1.1 §21, 0.1.2 §22, 0.1.3 §23, 0.1.4 §24 — and **0.2.0** (2026-10-07; `CHANGELOG.md`) the maintainability refactor, two languages, 补位, 自选编队 and their fidelity work — §25 — **0.2.1** (2026-10-07) the 联防 battlefield restored, full potential and the GitHub fixes after it — §26 — and **0.2.2** (2026-10-09) per-operator 潜能 / 练度, Japanese voices, the statistics page, 失衡, whole-frame attack timing and the GitHub fixes after 0.2.1 — §27. **0.2.3** (2026-10-10; `package.json`, `shared/constants.js APP_VERSION`) 克莱门莎 and new modules, community fixes, text size, per-operator voices, installation, the opening reroll and local match recovery — §28. **0.2.4** (2026-10-11) the official shared card pool and item stock, Raid revival and #475, and the community fixes after 0.2.3 — §29, the state described by this document. The labels v1 / v2 / v2.1–v2.5.2 in §0, §14–§20 and in the BALANCE / SIM comparisons name the design generations and the private playtest builds that came before it; they are kept as history.
|
||||
Versions: the first public release was **0.1.0** (2026-10-02, the state of §0–§20.15); the releases after it add the player feedback and GitHub reports — 0.1.1 §21, 0.1.2 §22, 0.1.3 §23, 0.1.4 §24 — and **0.2.0** (2026-10-07; `CHANGELOG.md`) the maintainability refactor, two languages, 补位, 自选编队 and their fidelity work — §25 — **0.2.1** (2026-10-07) the 联防 battlefield restored, full potential and the GitHub fixes after it — §26 — and **0.2.2** (2026-10-09) per-operator 潜能 / 练度, Japanese voices, the statistics page, 失衡, whole-frame attack timing and the GitHub fixes after 0.2.1 — §27. **0.2.3** (2026-10-10; `package.json`, `shared/constants.js APP_VERSION`) 克莱门莎 and new modules, community fixes, text size, per-operator voices, installation, the opening reroll and local match recovery — §28. **0.2.4** (2026-10-11) the official shared card pool and item stock, Raid revival and #475, and the community fixes after 0.2.3 — §29. **0.2.5** (2026-10-12) fixed gifts and already displayed operator offers under the shared stock, random boss pairing, the official battlefield scenes on the 3D board (optional local art: the full bundle includes them when extracted, a missing scene keeps the metal board; §15, §30.21), the official damage-number option, the 驭械术师 / hit-tile / bot ports and the community fixes after 0.2.4 — §30, the state described by this document; its rules supersede the corresponding 0.2.4 ones. The labels v1 / v2 / v2.1–v2.5.2 in §0, §14–§20 and in the BALANCE / SIM comparisons name the design generations and the private playtest builds that came before it; they are kept as history.
|
||||
|
||||
---
|
||||
|
||||
@@ -36,3 +36,4 @@ normative lines it rewrote).
|
||||
| §27 | [history/0.2.2.md](history/0.2.2.md) | 0.2.2 — after the 0.2.1 release (2026-10-09) |
|
||||
| §28 | [history/0.2.3.md](history/0.2.3.md) | 0.2.3 — after the 0.2.2 release (2026-10-10) |
|
||||
| §29 | [history/0.2.4.md](history/0.2.4.md) | 0.2.4 — after the 0.2.3 release (2026-10-11) |
|
||||
| §30 | [history/0.2.5.md](history/0.2.5.md) | 0.2.5 — after the 0.2.4 release (2026-10-12) |
|
||||
@@ -156,6 +156,11 @@ rebuilt.
|
||||
the source on the record's own ids, then the 下半 season (`act2autochess`). Rich-text tags and placeholders are the
|
||||
same as in Chinese, so `ui/richText.js` renders the English unchanged (a few official EN texts leave a tag unclosed;
|
||||
the parser runs it to the end of the text, as the game does).
|
||||
- **Corrections**: `tools/i18n/corrections-<lang>.json` (the pack's code: `en`, `ja`, `ko`, `zh-TW`; optional) is
|
||||
tried before the official data of that language, for an official translation that contradicts the Chinese text and
|
||||
the engine (exact match on the raw Chinese text; the plain twin is stripped from it; every entry carries a `_why:`
|
||||
note; `_`-keys are never written to the overlay). Only `corrections-en.json` exists. Counted as `correction` in the
|
||||
coverage report.
|
||||
- **Fallbacks**, in order: `tools/i18n/fallback-remake.json` (the remake's own strings: stage-name parts, item notes,
|
||||
the escaped levels' map names, the season name), `tools/i18n/fallback-pr70.json` (PR #70's translations of texts the EN client
|
||||
lacks: modules of CN-only operators, the 甄选 slots, 结算称号 criteria, emote labels), then `public/i18n/en.json`;
|
||||
|
||||
+34
-11
@@ -64,7 +64,7 @@ LOBBY → INFO_CHECK (co-op 25 s; solo and single-human matches untimed; all hum
|
||||
```
|
||||
|
||||
**Deploy field (user playtest #5 item 7).** From the ROUND_START of a boss round (最终攻势 / 隐秘核心) a player deploys on
|
||||
its half of the boss field (`Match.deployFieldOf` → `'bossL'`, or `'bossR'` for the second player of a seat pair — the
|
||||
its half of the boss field (`Match.deployFieldOf` → `'bossL'`, or `'bossR'` for the second player of a drawn pair — the
|
||||
same pairing as the fields, `bossWaves` / `Match.bossGroupOf`, planned in `startRound` BEFORE the players' round start
|
||||
so R14 → R15 never re-checks a boss-half board against the normal field, and re-paired — with an immediate re-check —
|
||||
when a teammate quits before the fight): `board.js buildDeployMap`
|
||||
@@ -411,14 +411,14 @@ keeps it; `PlayerState.pieceRoundCount`; prefix keys with your module too) —
|
||||
first manual refresh that copy witnesses (player feedback after 0.1.0: "获得该干员后该回合的首次刷新" also stacks; a copy
|
||||
bought after selling one this round is a new copy and fires on its own first refresh [ASSUMED]).
|
||||
|
||||
Writes (all validated, never throw on bad input, never make funds / pools negative):
|
||||
Writes (validated, never throw on bad input; funds and available stock stay non-negative; authorized operator overdraw records an internal deficit):
|
||||
|
||||
| helper | effect |
|
||||
|---|---|
|
||||
| `addFunds(n, reason?)` / `addPendingFunds(n)` / `spendFunds(n)` | funds now / at the next round start / pay (false when short) |
|
||||
| `addLayers(bondId, n, { requireActive })` | layer gain (`requireActive` = "使已激活的…"); returns layers added — at most the room left under `BOND_LAYER_CAP` (999, shared/constants.js `layerGainRoom`; the battle gains merged at SETTLE too); fires onLayers unless it added 0 |
|
||||
| `addLayers(bondId, n, { requireActive })` | layer gain (`requireActive` = "使已激活的…"); returns layers added — at most the room left under `BOND_LAYER_CAP` (999, shared/constants.js `layerGainRoom`; the battle gains merged at SETTLE too); fires onLayers unless it added 0. During ROUND_START / SP_DRAFT / PREP, a positive gain also sends the owner an ephemeral `m.layer {playerId, bondId, add}` audio notification; no replay on resync or SETTLE backfill |
|
||||
| `grantChess(id, { toTemp, golden, requirePool=true, fromPool=true })` | acquire a chess (takes pool copies; with `requirePool` a pool chess with no copy left fails → `null`); merges; fires onGain. `toTemp` puts it into temp, but a free hand slot pulls it in at the next recompute (temp holds overflow only, `PlayerState._fillHandFromTemp`) |
|
||||
| `grantItem(id, { toTemp })` | acquire an item (merges with an identical normal item); `toTemp` as for grantChess |
|
||||
| `grantItem(id, { toTemp, requirePool=true })` | acquire an item (merges with an identical normal item); `toTemp` as for grantChess. `requirePool:false` bypasses stock only for fixed `SERVER_GAIN_EQUIP` gift families; every copy still counts in occupancy (§30.1). |
|
||||
| `rollChess({ maxTier, tier, bond, filter })` / `rollItem({ pool, tier, maxTier })` | copy-weighted chess id from the shared pool / item id (choices.json pools) |
|
||||
| `grantFreeRefresh(n)` | free refreshes (stack) |
|
||||
| `modifyPrice(delta)` / `setPrice(v)` | onPrice only: edit `ev.price` |
|
||||
@@ -436,6 +436,7 @@ Writes (all validated, never throw on bad input, never make funds / pools negati
|
||||
| `offerBountyChoice(cards, sourceItemId)` | 教鞭 only: open a PERSONAL choice of up to three of `cards` (cards.bounty entries) for this player — shuffled once with the meta rng, kept in `m.private.personalChoice` until the owner confirms one (`g.choice { idx, choiceId }`) or the prep's deadline / an AI seat picks (§2.5); returns the intent result (`{ ok }`, or `{ error, detail }` for a phase that is not PREP, a Ready player, a choice already open or no card — before any rng draw or id, so the Art stays in the hand) |
|
||||
| `toast(text, kind)` / `ticker(text)` / `giftTicker(fromName, chessId)` | messages |
|
||||
| `teammates()` / `player(playerId)` | ctx objects of other alive players (team effects) |
|
||||
| `teamMedicGranted()` | Whether this match has a confirmed Touch selection, including an eliminated/departed holder; only the retained 外勤医疗 grant uses this exception ([ASSUMED], §30.14) |
|
||||
|
||||
### 2.5 Items: consume-on-equip and Arts
|
||||
Items whose data `kind` starts with `consume_on_equip` resolve through their `item:` handler's `onEquip` and are
|
||||
@@ -559,9 +560,14 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
|
||||
stock (`ps.diyStock`: `poolCopies` of the slot's tier, 8 / 5 [ASSUMED]; none — `diyBanned` — when every bond of it is
|
||||
off this match; `initDiyStock` once the bans are drawn); `poolOf(baseId)` routes its copy accounting (buy, reward
|
||||
picks, merges, promotions, sells, temp, elimination; `invariants.js` checks `left + held = cap` per player). The
|
||||
shop's chess slots and the reward offers' temporary refreshes roll it with the shared pool (`pool.roll({ extra })`,
|
||||
copy-weighted like any chess of its tier) once the 调度中心 reaches the slot's `shopLevel` (5 / 6) [ASSUMED for the
|
||||
reward offers]; an effect, reward pool or 机变 card that grants the player a random operator draws its stock too
|
||||
shop's chess slots roll it with the shared pool (`pool.roll({ extra: diyRollEntries() })`, copy-weighted like any
|
||||
chess of its tier) once the 调度中心 reaches the slot's `shopLevel` (5 / 6). The promotion reward's temporary refresh
|
||||
(`pushRewardOffer` with no explicit ids) draws the same stock through `diyStockEntries()` under the reward tier
|
||||
`min(shopLevel + 1, 6)`, so a tier-5 自选 piece can appear when the 调度中心 reaches 4 and a tier-6 piece when it
|
||||
reaches 5 — one shop level before the ordinary shop, and not guaranteed. 卫戍协议/帮助 干员晋升 names one refresh one
|
||||
tier above the 调度中心 and no separate pool; the owner decided on 2026-10-12 that the refresh includes 自选 of that
|
||||
tier (0.2.0 had [ASSUMED] the shop-level gate for the reward too, DESIGN §25; §30.23 reverses it). An effect, reward
|
||||
pool or 机变 card that grants the player a random operator draws its stock too
|
||||
(effectsMeta `rollChess` / `rollPool`, the 驰援 fallback in choices.js: `ps.diyStockEntries()` — the roll's own tier
|
||||
rules, no 调度中心 gate, bonds through the player's view; 「自选干员放入后模拟中的补给池随机范围也将被相应扩大」, 0.2.0), and
|
||||
信标 never sends one to a teammate [ASSUMED]. `battleInput` carries `diy` (the pick); `m.private.diy` / `diyBanned`; `prepFieldMeta` and
|
||||
@@ -674,6 +680,9 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
|
||||
(own phase perfect) go to pending funds; bounty rounds decrement; LP ≤ 0 ⇒ eliminated (all copies back to the pool).
|
||||
* **Final Assault / Hidden Core**: finalAssault.js header. Boards are passed in board coordinates; the sim maps board
|
||||
rows 9–12 onto boss rows 2–5 (`BOSS_ROW_OFFSET` −7, matching every stage's boss rows) and mirrors the right side.
|
||||
Pairing uses a separate seeded shuffle once per match, before Final Assault prep; `bossPairs` retains partners and
|
||||
sides into Hidden Core. A departure preserves intact pairs and joins the remaining single players. The published
|
||||
`m.public.bossPairs` follows the preparation plan; camera, placement and green team frames use it, never new draws.
|
||||
Pool (`finalAssault.js bossPoolHp` → `GameData.bossPoolShare` → `gamedata.js bossPoolShareOf`, DESIGN §20.10,
|
||||
§25.13.4): one pool shared by every boss field (official tip "最终攻势中,所有人将一起对敌方领袖造成伤害"; notice 5114's
|
||||
"敌方领袖的总生命值不变" is about the mirrored copies of a pair field sharing it) = `bloodPoint[difficulty]` × the players
|
||||
@@ -731,6 +740,12 @@ smallest stat (坚若磐石 "目标生命值损失最少" = least `lpLost`). doc
|
||||
---
|
||||
|
||||
## 4. 联防 (Unite)
|
||||
|
||||
HP/SP restoration is completed after deployment and all `battleStart` effects. Living carry ratios remain exact,
|
||||
including below 1% or fractional HP under a smaller new maximum. As approved for the reported Egir bug, opening
|
||||
devour rebuilds attributes/mark chains without repeating HP loss or death/revive effects on a carried survivor.
|
||||
Forced-down carry still participates in attribute chains and stays down without using a revive slot. Fresh units
|
||||
without carry and other battle kinds retain normal devour damage; later redeploys do not reuse the old carry.
|
||||
Planned after the normal combats, at the end of the COMBAT_END pause from the players still in (`Match._afterCombat`, so a
|
||||
player who quit during the last field or the pause is neither a helper nor a leaker whose enemies re-enter; unite.js):
|
||||
helpers = `unite.js helperOrder` (research 08 §5, PRTS 卫戍协议/帮助 §联防阶段): ≤ `unite.maxHelpers` (2) perfect players
|
||||
@@ -768,6 +783,8 @@ leaker's own capsule / row (`ui/hud.js uniteRemaining`) and the leakers' team ro
|
||||
|
||||
## 5. Views (DESIGN §8.2 / §8.3) and deviations
|
||||
|
||||
The client's stored loadout and `m.private.loadout` / `ops` must agree after a successful sync. In `public/js/ui/loadoutSync.js`, edits and `welcome` invalidate preparation still awaiting game data, so an older payload cannot overwrite a newer reset on the session or seat, or enter the next match (DESIGN §16, DESIGN §30.17). Request reply ordering, debounce, close flush and retry/lock handling remain unchanged.
|
||||
|
||||
`m.public` (throttled ≤ 10/s, only sent when it changed) carries the DESIGN fields plus: `drawnDisabledBonds` (the 3+4
|
||||
drawn set; `disabledBonds` = drawn ∪ the mode's static list), `hiddenBossId`, `bossRound`, `hiddenRound`, `spRound`,
|
||||
`combatMode` (`'client'` | `'server'`), `fields[].progress { killed, total, done }` (teammates' progress UI), `paused`
|
||||
@@ -927,13 +944,19 @@ choices are explicitly [ASSUMED]; DESIGN §28.15–16 records the source and val
|
||||
|
||||
`SharedItemPool` derives equipment availability from current ownership across all players: ordinary 1, upgraded 2.
|
||||
Equips, merges and returns cannot lose reservations. Consumptions/destruction release ownership [ASSUMED removal
|
||||
interpretation]. Shop/reward displays reserve nothing; rejected exhausted purchases retain funds and offers.
|
||||
interpretation]. Shop/reward displays reserve nothing; rejected exhausted **item** purchases retain funds and offers.
|
||||
整备 upgrades the purchased item only when `left` still has the golden's second copy; otherwise the item stays
|
||||
normal and the charge remains.
|
||||
`m.public.sp.cards[].soldOut` disables duplicates when a preceding choice used the final copy; timer/bot choices
|
||||
use the same availability. Non-shop effect-only equipment is unlimited.
|
||||
|
||||
Operator capacities use measured single-player exceptions and twice those counts in co-op; DIY remains private.
|
||||
`take(..., { overdraw: true })` records full occupation of a granted/promoted elite or a Mimic copy beyond capacity;
|
||||
normal takes remain bounded. Negative internal balance is a deficit, never negative available stock. Existing
|
||||
item selection probabilities and explicit grant weights remain unchanged.
|
||||
`take(..., { overdraw: true })` records full occupation, including authorized acquisitions beyond capacity.
|
||||
Already displayed operator shop offers remain purchasable after exhaustion (路标月报#2 2:50–2:57, #498);
|
||||
operator reward offers follow the same rule by the owner's 2026-10-11 decision. Future rolls still use available stock.
|
||||
Negative internal balance is a deficit, never negative available stock; selling first repays the deficit.
|
||||
|
||||
Fixed equipment gifts from `SERVER_GAIN_EQUIP` (卡涅利安、蜜蜡、缪尔赛思、耶拉、诗怀雅) may exceed the pool,
|
||||
including the same families awarded by 变形者. They still occupy ordinary/advanced 1/2 copies and can leave availability
|
||||
at zero after a removal. Ordinary shop, reward and random grants retain their limits. `requirePool:false` is not a
|
||||
universal equipment bypass. Rockrock/Flametail's measured weighted subsets remain subject to shared availability (§30.2).
|
||||
+10
-4
@@ -24,7 +24,7 @@
|
||||
| 休整期 | 购买、摆阵、配装备、冻结 / 刷新、升级调度中心;同盟有倒计时(65–215 秒,随回合增加);独立模拟、以及只有你一名玩家(只带 AI 队友)的同盟模拟不限时。所有人「准备就绪」即提前开战,全员就绪前可取消;点「准备就绪」时商店自动收起、棋盘放大到全场,取消准备时商店再展开(资金旁的「收起」和商店标签「展开商店」随时可以手动切换)。 |
|
||||
- **特殊地形点一下就有说明**:战场上的**活性源石**、**沼泽**、**排气格栅**、**深水区**,以及**红门 / 蓝门 / 传送点**,点一下就会弹出它自己的说明卡——名称、机制,以及**这一关自己的数值**(比如战场#04 的活性源石:每秒 70 点真实伤害、攻击力 +20%、攻击速度 +20、持续 300 秒)。休整期和作战期都能点。普通地板 / 道路 / 高台点了不会弹东西,所以点一下仍然只是「取消选中、关掉卡片」。
|
||||
- **场地装置点一下也有说明**:**阻隔工事**(挡路:地面敌人绕着走,无路可走才撞碎它;作战中说明卡上有它的实时血量,被摧毁后点不出来)、**“双眼皮”炮台**(自动法术攻击、命中附加脆弱,按当前层数最高的盟约强化;还没被机械援助或机变卡启用的炮台不显示、点不出来)、**射击台**(远程位可部署的高台位)和**源石流发生装置**(吹风方向,以及对干员攻击力、敌人移动速度的加减)点一下同样弹出说明卡,数值都取自所在关卡的装置数据,装置名称按当前语言显示官方译名;被机变卡移除的箱子同样不显示。休整期和作战期都能点(GitHub #228,PR #229,感谢 @rickylxw)。
|
||||
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**逐列从左到右、每列从上到下**的顺序(官方「从上到下>从左到右」:最左一列最先)自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。顶栏中间的胶囊是官方的「已解决 / 本回合敌人数」:打死一个、漏掉一个都算已解决(开局 0/3,漏一个 1/3,打死一个 2/3,打死会分裂的那只 3/3);分裂出来的小怪、召唤物和领袖召唤的敌人不进分子也不进分母,但它们漏掉时仍然各自扣目标生命值、也会破坏完美作战(4 只磨砻全被打死、分裂出的 8 只全漏进蓝门:胶囊 4/4,扣 8 点),所以会召唤的领袖关胶囊也能填满。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在倒下的格子上**(倒地姿势、略微变灰),之后也在这一格再部署(与官方一样;倒在其他干员或召唤物初始位置上的,会回到自己的初始位置躺下,召唤物已经消失也一样;唯一的例外是乌尔比安:三技能把他移到船锚处期间被击倒时,他回到自己的部署位躺下、再部署——这是项目维护者 2026-10-07 应社区反馈有意做出的改动,与官方「原地倒下」不同);因自身效果强制退场的干员(史尔特尔「余烬」结束、耀骑士临光 2 技能结束等)同样倒在原地、在原地再部署;倒地干员所在的格子**不会被占用**,突袭盟约的再部署、援军、召唤物都会避开它。头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
|
||||
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**逐列从左到右、每列从上到下**的顺序(官方「从上到下>从左到右」:最左一列最先)自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。顶栏中间的胶囊是官方的「已解决 / 本回合敌人数」:打死一个、漏掉一个都算已解决(开局 0/3,漏一个 1/3,打死一个 2/3,打死会分裂的那只 3/3);分裂出来的小怪、召唤物和领袖召唤的敌人不进分子也不进分母,但它们漏掉时仍然各自扣目标生命值、也会破坏完美作战(4 只磨砻全被打死、分裂出的 8 只全漏进蓝门:胶囊 4/4,扣 8 点),所以会召唤的领袖关胶囊也能填满。有敌人进入蓝门时,顶栏的目标生命值**先不变**(红字 + 「−N」标记待扣的点数,每回合至多 10 点),结算时才显示扣除后的数值(与官方一致,GitHub #479;0.2.4 及之前是立即显示扣除后的数值);联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在倒下的格子上**(倒地姿势、略微变灰),之后也在这一格再部署(与官方一样;倒在其他干员或召唤物初始位置上的,会回到自己的初始位置躺下,召唤物已经消失也一样;唯一的例外是乌尔比安:三技能把他移到船锚处期间被击倒时,他回到自己的部署位躺下、再部署——这是项目维护者 2026-10-07 应社区反馈有意做出的改动,与官方「原地倒下」不同);因自身效果强制退场的干员(史尔特尔「余烬」结束、耀骑士临光 2 技能结束等)同样倒在原地、在原地再部署;倒地干员所在的格子**不会被占用**,突袭盟约的再部署、援军、召唤物都会避开它。头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
|
||||
| 联防 | 见第 5 节。 |
|
||||
| 结算 | 扣 LP、发放悬赏资金、结算层数;LP ≤ 0 的玩家淘汰(其干员回到公共卡池)。结算开始时屏幕正中跳出官方的「**作战结束**」框(约 3 秒后自己消失,不挡操作),下面一行只写**你自己**这一回合被扣的目标生命值:没扣是「**全员无伤!**」,扣了是「**生命值减少 −N**」;同时播官方的战斗结束音效(扣了、联防里没扣、普通回合没扣各一种)。联防回合见第 5 节。最终攻势和隐秘核心没有这一框(之后直接是结算称号或隐秘核心);已淘汰的玩家和观战者不弹框。 |
|
||||
|
||||
@@ -59,13 +59,15 @@
|
||||
## 3. 招募、整备区与晋升
|
||||
|
||||
- **购买点两次**:第一次点商店卡片只选中(卡片抬起并打开详情,底部出现「确认购买 · 再次点击」),再点这张卡片的**任意位置**(包括金色的确认条)才购买;点别处或按 Esc 取消。升级调度中心、晋升奖励同样两次;策略或装备带来的免费挑选(如凯瑟琳【定向投放】升级后刷新的 3 件装备、娜仁图亚【见者有份】的 2 件装备、寻呼模块的 3 名干员)也一样出现在商店卡片的位置,左侧标题写着它的来源,装备显示为装备卡(图标、名称、说明、FREE),选 1 个免费获得;同时有多个免费挑选时依次出现,标题下和「稍后」后的提示按钮会写还有几项(如「+1」)。不能购买时(例如已准备就绪)点卡片只打开详情。
|
||||
- **公共卡池**(同盟):每名干员的副本数有限,I–VI 阶分别 12 / 14 / 18 / 16 / 8 / 5 份,所有玩家共用;持有的干员(棋盘 + 整备区)占用副本,商店里展示的不占用。一名干员被大家拿光后就刷不出来了;淘汰玩家的干员会还回卡池。
|
||||
- **刷新等待**:按下「刷新」后按钮会变暗,本次请求结束(成功或失败)后恢复可点;等待期间再点按钮或按 R 都不会重复发送,避免高延迟时连点多次扣费(GitHub #501)。
|
||||
- **公共卡池**:独立模拟 I–VI 阶的基础副本数分别为 12 / 14 / 18 / 16 / 8 / 5,少数干员有例外;同盟的公共干员库存是独立的两倍,自选干员仍用各自的私人库存。持有的普通/精锐干员占 1 / 3 份,商店展示不占份数。拿光后不能再刷到,但已经展示的干员商店牌和奖励牌仍可领取;超额部分也占库,卖出先抵消超额,淘汰时归还所持份数。
|
||||
- **装备库存**:普通/进阶装备通常占共享库存 1 / 2 份,道具商店和奖励领取时仍检查余量。缪尔赛思、耶拉等固定特质赠品仍占份数,但不会因库存已满停止赠送;拥有量降回上限以下后商店才有余量。商业包装方案的 2 份是同时库存,不是整局限买两次。
|
||||
- **位置上限**:棋盘最多部署 **8** 名;整备区 **10** 格。整备区满时**不能购买或领取**任何干员、装备(包括能凑齐 3 名合成精锐的那一张,和官方一样),先出售、部署或配发腾出格子;新获得的单位(合成的精锐、奖励、退回的装备等)进入**临时整备区**(整备区前面的 5 格),整备区一空出格子,临时整备区的单位就**自动补进去**(从右往左)。临时整备区有单位时**不能准备就绪**,棋盘上会用红色虚线框标出这一排并说明规则:把它们放进整备区或战场、配发或使用,或者出售 / 销毁。休整期中进入临时整备区的单位,若到**本休整期结束**时仍留在那里就会被自动处理(干员返还卡池、装备销毁);作战、结算期间或准备就绪之后才进入的(例如战斗奖励、结算时合成的精锐),会保留到**下一个休整期**结束,你一定有机会处理它们。
|
||||
- **晋升精锐**:3 名同名干员(无论是否部署)自动合成**精锐**(等级、技能、模组大幅提升;风丸只需 2 名),精锐不会再合成。如果被合成的干员里有**已经部署在战场上**的,精锐会直接出现在**那名干员的位置**(保持原来的朝向,不多占部署位;有两三名都在场上时,取最先部署的那名——最左一列优先,同一列从上到下);否则精锐进入整备区(满了进入临时整备区)。原先配发的装备都退回整备区,需要重新配发;被合成干员的召唤物随之移除,场上的精锐会把自己的召唤物送进整备区。点选一张能合成的商店卡片时,精锐将出现的那一格会亮起金色边框,同时打开的详情卡里也会写明去向(「可晋升:精锐干员将出现在作战区原位置 / 将进入整备区」;详情卡会自动停靠在不遮挡这一格的一侧)。在回合开始或结算时因策略、奖励等完成的合成,精锐出现时同样会播放金色的晋升特效。
|
||||
- **晋级奖励**:每次晋升后获得一次特殊招募:从 3 名 **高一阶**(最高 VI)、**互不相同**的随机干员中**免费**挑 1 名(该阶剩余干员不足 3 名时由低一阶补足)。它不能刷新、不能冻结、本回合结束失效。
|
||||
- **干员调配(技能、模组、潜能与练度)**:大厅、房间和「确认本局信息」里的「干员调配」按钮打开的界面有三个标签页:「干员调配」「干员持有」「自选编队」。「干员调配」把干员列成一张表,每名干员一行,各列是**技能**、精锐的**模组**和**潜能 · 练度**:直接点行里的技能图标、模组图标(「不装备」在最前,模组只在精锐时生效)或改两个下拉框就设好了;点头像或名字在右边打开详情,依次是潜能与练度、特质、技能(普通 Lv.4 / 精锐 Lv.7 两档数值可切换)、局内数值和模组(窄屏——宽度 1000 像素以下,例如横屏的手机——详情盖住列表,点「干员列表」返回)。**潜能**(1–6)和**练度**(未精英化 / 精英阶段1 / 精英阶段2 / 精英阶段2-60级)按干员设置,同一名干员的普通、精锐和自选共用,默认潜能 6、精英阶段2-60级(维护者 2026-10-08 的决定:默认满潜、满加成)。官方规则:潜能取你自己持有的这名干员的潜能;练度是调度手册「加成情况」里的**自持有加成**——未精英化 +0%,精英1 攻击、防御 +5%,精英2 攻击 +10%、防御和生命 +5%,精英2 60 级三项 +10%,单独乘算,不和其他「攻击力 +x%」相加;未持有的特许干员在官方按潜能 1、没有加成,想照官方打就把它设成潜能 1、未精英化。补位的替补干员和自选的原型干员没有潜能和练度,设置对它们不生效。商店卡片、详情卡和「局内数值」都按你的设置显示;同盟模拟中队友的干员按队友的设置。和技能、模组一样是**局外设置**:「确认本局信息」结束前修改对本局生效,之后只保存到下一局。「恢复默认」「全部恢复默认」同时重置潜能与练度,「导出 / 导入」会带上它们,「干员调配」按钮上的数字也算上改过潜能或练度的干员。
|
||||
- **干员持有(补位)**:同一个界面的第二个标签页「**干员持有**」可以把你没有的干员**下掉**(点一下卡片切换「持有 / 未持有」)。和官方一样只有 55 张「普通」棋子能下掉(界面按阶级列出本赛季商店里的 53 名,每张写着由谁替补、用几技能);74 张「预设」棋子(多是 1–2 阶的常见干员)无论是否持有都由本人上场,不在其中。默认全部持有。下掉的干员和官方一样**显示为替补干员**:商店和晋升奖励的卡片、整备区和临时整备区里的单位、作战区、结算界面的阵容,都是替补干员的头像、名字和模型(例如银灰显示为 Sharp),带一个小小的「替补」标记;详情卡显示替补干员的立绘、名字、技能、天赋和模组,名字下面写着「银灰的替补」。**盟约、特质、阶级、价格、合成仍按原干员**(卡片和详情卡上的盟约、特质就是原干员的),作战时的属性、技能、天赋、模组、攻击范围、近战位 / 远程位都是替补干员的(例如银灰 → Sharp 的 2 技能「亮剑」)。同一名替补干员可能替补好几名干员(Sharp 替补忍冬、银灰、隐德来希等),它们按各自的原干员分别合成:看阶级和盟约区分,点开详情卡能看到替补的是谁。同盟模拟中队友查看你的整备区和作战区、你的盟约成员时,看到的也是替补干员。替补的技能和模组由棋子固定,「干员调配」对它不生效;替补干员没有召唤物(下掉缪尔赛思就没有流形)。这是**局外设置**:下一局开始时生效,对局中修改只保存到下一局;同盟模拟中只影响你自己的棋子,队友的棋子按队友的设置。没有官方的好友借用:想用本人上场,改回「持有」即可。「全部持有」一键恢复,「导出 / 导入」可以把设置带到别的浏览器;大厅的「干员调配」按钮上会显示「替补 N」提醒你有几名干员被下掉。
|
||||
- **自选编队**:同一个界面的第三个标签页「**自选编队**」有 4 个名额:5 阶两个、6 阶两个。点名额打开选择列表,每个名额可以放一名**你持有的 6★ 干员**(棋子池里没有的那些,标「已持有」:3 个技能任选其一,精锐的模组任选其一或不装备;只在集成战略或生息演算里生效的模组不能选:ISW-A、SO-A / SO-B、RA-A)或一名**原型干员**(标「原型」:Sharp、Mechanist 等 9 名 6★,5 阶另有 6 名 4★ 预备干员;技能和模组与补位时相同,不能更改)。同一名原型干员可以同时放进一个 5 阶和一个 6 阶名额;你持有的干员只能放一个名额;同一阶的两个名额不能放同一人。列表只列出已经做好战斗逻辑的干员(原型干员全部可选,持有的 6★ 随战斗逻辑完成陆续加入)。对局中,自选干员**只出现在你自己的商店里**:调度中心升到 5 级后才会刷出 5 阶名额的干员,6 级后才会刷出 6 阶的;刷新概率和同阶干员一样,每个名额有自己的库存(5 阶 8 张、6 阶 5 张,不与队友共用),价格与同阶干员相同,晋升奖励里也可能出现;效果、奖励或机变卡「获得一名随机干员」时,也可能抽到你自己的自选干员(按那个效果自己的阶级规则,不看调度中心等级)。三张合成精锐,精锐时所选模组生效(5 阶 1 级、6 阶 3 级)。你持有的干员的潜能与练度在名额卡片上设置(默认潜能 6、精英阶段2-60级,规则同「干员调配」);原型干员没有潜能和练度。自选干员**没有特质**,盟约按干员的实际阵营分配(推进之王是维多利亚,煌同时是炎和维多利亚,没有对应阵营的是协防干员),和普通棋子一样计入盟约人数;本局禁用了它全部的盟约时,它不会出现在商店里(协防不会被禁用),开局的「确认本局信息」、选策略时的「本局信息」和对局中的「本局信息」都会在「本局禁用干员」下写明是哪一名。商店卡片、整备区、作战区和详情卡上都标着「自选」,详情卡显示所选的技能和模组。这同样是**局外设置**:下一局开始时生效,对局中修改只保存到下一局;AI 队友不使用自选;你开启 AI 托管时,AI 会优先买你的自选干员,并在快能刷出自选名额时提早一回合升级调度中心;信标不会把自选干员送给队友。「导出 / 导入」「全部清空」可用,大厅的「干员调配」按钮上会显示「自选 N」。
|
||||
- **自选编队**:同一个界面的第三个标签页「**自选编队**」有 4 个名额:5 阶两个、6 阶两个。点名额打开选择列表,每个名额可以放一名**你持有的 6★ 干员**(棋子池里没有的那些,标「已持有」:3 个技能任选其一,精锐的模组任选其一或不装备;只在集成战略或生息演算里生效的模组不能选:ISW-A、SO-A / SO-B、RA-A)或一名**原型干员**(标「原型」:Sharp、Mechanist 等 9 名 6★,5 阶另有 6 名 4★ 预备干员;技能和模组与补位时相同,不能更改)。同一名原型干员可以同时放进一个 5 阶和一个 6 阶名额;你持有的干员只能放一个名额;同一阶的两个名额不能放同一人。列表只列出已经做好战斗逻辑的干员(原型干员全部可选,持有的 6★ 随战斗逻辑完成陆续加入)。对局中,自选干员**只出现在你自己的商店里**:调度中心升到 5 级后才会刷出 5 阶名额的干员,6 级后才会刷出 6 阶的;刷新概率和同阶干员一样,每个名额有自己的库存(5 阶 8 张、6 阶 5 张,不与队友共用),价格与同阶干员相同。晋升奖励按奖励自己的阶级抽取,所以该阶自选可以比普通商店早一级出现:调度中心 4 级的五阶奖励可以出现五阶自选,5 级的六阶奖励可以出现六阶自选,不保证刷出;效果、奖励或机变卡「获得一名随机干员」时,也可能抽到你自己的自选干员(按那个效果自己的阶级规则,不看调度中心等级)。三张合成精锐,精锐时所选模组生效(5 阶 1 级、6 阶 3 级)。你持有的干员的潜能与练度在名额卡片上设置(默认潜能 6、精英阶段2-60级,规则同「干员调配」);原型干员没有潜能和练度。自选干员**没有特质**,盟约按干员的实际阵营分配(推进之王是维多利亚,煌同时是炎和维多利亚,没有对应阵营的是协防干员),和普通棋子一样计入盟约人数;本局禁用了它全部的盟约时,它不会出现在商店里(协防不会被禁用),开局的「确认本局信息」、选策略时的「本局信息」和对局中的「本局信息」都会在「本局禁用干员」下写明是哪一名。商店卡片、整备区、作战区和详情卡上都标着「自选」,详情卡显示所选的技能和模组。这同样是**局外设置**:下一局开始时生效,对局中修改只保存到下一局;AI 队友不使用自选;你开启 AI 托管时,AI 会优先买你的自选干员,并在快能刷出自选名额时提早一回合升级调度中心;信标不会把自选干员送给队友。「导出 / 导入」「全部清空」可用,大厅的「干员调配」按钮上会显示「自选 N」。
|
||||
- 每回合商店会自动刷新未冻结的格子。
|
||||
- **「本回合首次主动刷新」按每名干员各自计算**(拉普兰德):她在本回合遇到的第一次主动刷新生效,在整备区里也算——先刷新、再买下拉普兰德,下一次刷新她仍会叠层;本回合新合成的精锐算作一名新的拉普兰德——即使合成用的几名本回合已经生效,精锐在本回合的下一次主动刷新仍会 +8。
|
||||
|
||||
@@ -124,7 +126,7 @@
|
||||
|
||||
## 7. 最终攻势与隐秘核心
|
||||
|
||||
- **最终攻势**(第 14 回合;独立标准为第 9 回合):按座位两两配对,每对共享一个战场,一人守左半边、一人守右半边(右侧玩家的棋盘左右镜像)。所有人**共同削减同一条领袖血条**(血量 = 该难度的官方领袖生命值 × 开战时存活的玩家人数:领袖生命值取自当前版本的游戏数据,例如终极「假想敌:胄」3 600 000,4 人存活时为 14 400 000、2 人为 7 200 000;AI 队友和掉线托管的玩家都算,已被淘汰或中途退出的不算;独立模拟按 1 人计。这是作者 2026-10-06 采纳社区 PR #209 的决定,取代之前「保持固定血量」的做法),看不到另一组的战场。全队 LP 合并成一个池,不再有每回合 10 点上限。顶部的 120 秒倒计时结束后战斗**继续**(不会超时判负);从开战第 150 秒起(实际秒数)全队每秒扣 1 点 LP。领袖血量归零即胜利(血条显示 0 时领袖已倒下,战斗立即结束),LP 归零即失败——以先归零的一方为准,LP 耗尽后打出的伤害不再计入。此阶段盟约不叠层。
|
||||
- **最终攻势**(第 14 回合;独立标准为第 9 回合):每局开始时随机两两配对(准备期到开战固定,隐秘核心沿用同一分组),每对共享一个战场,一人守左半边、一人守右半边(右侧玩家的棋盘左右镜像)。所有人**共同削减同一条领袖血条**(血量 = 该难度的官方领袖生命值 × 开战时存活的玩家人数:领袖生命值取自当前版本的游戏数据,例如终极「假想敌:胄」3 600 000,4 人存活时为 14 400 000、2 人为 7 200 000;AI 队友和掉线托管的玩家都算,已被淘汰或中途退出的不算;独立模拟按 1 人计。这是作者 2026-10-06 采纳社区 PR #209 的决定,取代之前「保持固定血量」的做法),看不到另一组的战场。全队 LP 合并成一个池,不再有每回合 10 点上限。顶部的 120 秒倒计时结束后战斗**继续**(不会超时判负);超过开战 120 秒后(实际秒数)全队每秒扣 1 点 LP;仅开战时有 AI 队友的队伍延后至超过 150 秒。领袖血量归零即胜利(血条显示 0 时领袖已倒下,战斗立即结束),LP 归零即失败——以先归零的一方为准,LP 耗尽后打出的伤害不再计入。此阶段盟约不叠层。
|
||||
- **限伤**(最终攻势与隐秘核心,与官方相同):对领袖的**单次伤害达到 300000 或以上时整次作废**——这一下造成 0 点伤害,不计入领袖血条,也不显示伤害数字;不是截到 300000,而是一点都不算(299999 照常生效)。只针对领袖本体(含两人战场里的另一个领袖),领袖的部件、护卫、无人机和其他小怪不受影响,普通回合和联防也不受影响;部件把伤害等量转给领袖时,这一下也按一次伤害检查;无人机死亡令领袖失去的 2% 血条不算一次伤害,不受限伤影响(隐秘核心终极「假想敌:胄」3 或 4 人存活时每架为 43.2 万 / 57.6 万,按限伤会整次作废,本作仍然计入,属推断)。层数极高时领袖仍可能几秒内倒下(例如每名玩家的两个盟约都在 999 层,或所有激活的盟约都在 999 层),这来自官方按层数计算、没有上限的伤害倍率(谢拉格、奥术、精准等),官方社区也有「boss一秒死」的说法。
|
||||
- 最终攻势 / 隐秘核心的休整期和战斗中一样,**同组两人在同一个关底战场上**:你在自己的半场摆位(右侧玩家看到自己在右边,摆位左右镜像,干员相对领袖的朝向不变),队友当前的阵容显示在另一半场(只能看,随他调整实时更新);左侧成员栏里同组两人的头像带**绿框**,从回合开始一直到战斗结束。观战者查看其中一人时也会看到两人的阵容和站在关底的领袖。
|
||||
- 最终攻势 / 隐秘核心的休整期按你在**关底战场上的半场**判定可部署地块(右侧玩家左右镜像),拖动时高亮的就是可放置的格子:例如「源石流发生装置」关底图中被 1×3 橙色管子围起的地块可以部署。在最终攻势放在这些格子上的干员进入隐秘核心时会留在原位。
|
||||
@@ -174,10 +176,14 @@
|
||||
|
||||
刷新、冻结、升级、撤退、出售和准备只在休整期有效,撤退 / 出售要先点选一名干员。方向轮盘(方向键预览、`Enter` 确认、`Esc` 取消;按 Tab 先到轮盘上的「点击取消」,在它上面按 `Enter` 是取消,Tab 到其他按钮上按 `Enter` 没有反应)、「玩法说明」和「干员调配」的翻页键是这些界面自己的按键,不能修改。
|
||||
|
||||
**伤害数字**:标题画面、大厅、房间和对局里的「设置」有「显示伤害数字」(默认打开,每一次命中和治疗都跳出数字)和「官方伤害数字」(默认关闭)。打开「官方伤害数字」后,只在伤害达到 [PRTS 作战机制](https://prts.wiki/w/作战机制)「伤害红字(暴击)」的条件时显示红色整数(最终伤害不低于期望伤害的 1.5 倍;闪避了本应显示红字的一击时显示黄色 MISS)。关掉「显示伤害数字」则什么数字都不显示。无敌时官方客户端会显示红色 0,本作目前不会为这种未击中补一条伤害事件,因此这种红色 0 暂时看不到。
|
||||
|
||||
**修改快捷键**:标题画面的「设置」按钮、大厅和房间顶栏的「设置」(玩法说明左边)或对局左下角的「设置」→「快捷键」。点一下要改的按键(只用键盘时按 Tab 移到它上面再按 Enter),出现「按下新按键…」后按新的键;按 `Esc` 取消,设置窗口不会关闭。可以用字母、数字、空格、标点符号键和 Delete / Insert / Home / End / PageUp / PageDown;`Esc`、`Tab`、`Enter`、方向键和 F1–F12 留给界面使用,也不能搭配 Ctrl / Alt / ⌘。新按键已被另一个快捷键使用时两者互换,列表下方会写明(例如把「撤退选中干员」改成 `X`,「出售选中干员」就换成原来的 `Q`)。「恢复默认」改回上表的按键。修改立即生效,商店栏、准备按钮和单位底部「撤退 / 出售」按钮上显示的按键也跟着变;设置和音量等一起保存在当前浏览器里(换浏览器或清除网站数据后回到默认)。按键按键帽上印的字符识别(AZERTY 等键盘按印着的字母),俄文等非拉丁布局按键位识别。手机等纯触屏设备要连接键盘才能用快捷键,设置里的快捷键列表默认折叠。
|
||||
|
||||
## 12. 反馈问题
|
||||
|
||||
标题画面页脚的「项目仓库」在新标签页打开本项目的 [GitHub 仓库](https://github.com/sganggs/Stronghold-Protocol)。
|
||||
|
||||
遇到报错、卡住或结果不对时,打开「设置」→「问题反馈」,点「复制诊断信息」,再到 [GitHub issue](https://github.com/sganggs/Stronghold-Protocol/issues) 里描述发生了什么并粘贴进去。诊断信息包括游戏版本、浏览器和设备、当前所在的阶段与回合,以及打开页面以来记录到的错误(最多保留最近 40 条)。战斗中复制时,默认附上这场战斗的数据,开发者可以用它重现这场战斗;不想附上可以关掉「附上本场战斗」。诊断信息只在你的浏览器里生成,不会自动上传;你和其他玩家的名字、房间号会被替换成 `<me>`、`<player2>`、`<room>` 这样的占位符。剪贴板不可用时(部分 App 内置浏览器),内容会显示在下方的文本框里,手动全选复制即可。
|
||||
|
||||
## 13. 统计(本机)
|
||||
|
||||
+59
-11
@@ -81,7 +81,15 @@ summon that came in during the initial deployment (also one an operator's deploy
|
||||
the operators in the aggro order (`unit.aggroSeq`, §1.2). On a shared field (联防, the boss field)
|
||||
the players' fields deploy side by side: the i-th operators of all players come in together (in `players` order), then
|
||||
the summons the same way, and the summons rank after the operators of all players **[ASSUMED]** (PRTS describes one
|
||||
field — units deploy one after another with a fixed delay from the battle start — and "两处阵地将前后拼接为一处阵地"). Register hooks before the first step (e.g. `opts.setup(battle)` or right after `new Battle`).
|
||||
field — units deploy one after another with a fixed delay from the battle start — and "两处阵地将前后拼接为一处阵地").
|
||||
|
||||
外勤医疗's prepared slot is a preparation view record, not a combat `Unit`. The `battleStart` handler creates either
|
||||
预备干员-医疗 or Touch after counting deployed elites. [ASSUMED] Its effective targeting order precedes ordinary
|
||||
initial deployments (`preBattleAggroSeq`); its actual `deploySeq` still identifies the later runtime spawn. Prepared
|
||||
slots on a shared field follow player order [ASSUMED]. A later `spawnMapChar` without this option keeps its ordinary
|
||||
summon order (§30.22).
|
||||
|
||||
Register hooks before the first step (e.g. `opts.setup(battle)` or right after `new Battle`).
|
||||
|
||||
**Tick order:** scheduled callbacks → spawns → DP → buffs (+HP regen) → enemies (attack, move, block) →
|
||||
enemy tile index → allies (skill tick, attack) → projectiles → auto-redeploys → boss pool sync → `tick` hook →
|
||||
@@ -164,14 +172,18 @@ the old tile-key order), so a rotated layout plays the same (test/sim/facing-inv
|
||||
directions on an open field). 余 S3's fire wall runs through his tile perpendicular to his direction (his column facing
|
||||
RIGHT / LEFT, his row facing UP / DOWN; fx `firewall.axis` = `'col'|'row'`).
|
||||
`carryState: { hpPct, sp }` restores unite helpers' operators (HP ratio, and the SP = the official 技力, stored charges
|
||||
included: `unitsEnd` reports `skill.spTotal`, reset rebuilds the charges from it, and `skill.setSpTotal` sets it again after
|
||||
the `deploy` hook — PRTS "部署完成后…技力修改至与上一阶段结束时相同", so 独行 / 黄沙罗盘's deploy-time SP does not add to it;
|
||||
included: `unitsEnd` reports `skill.spTotal`; after all initial deployments and `battleStart` hooks, HP is restored
|
||||
against the new maximum and `skill.setSpTotal` restores the charges — PRTS "部署完成后…技力修改至与上一阶段结束时相同",
|
||||
so 独行 / 黄沙罗盘's opening SP does not add to it. Positive ratios are not raised to 1% or 1 HP;
|
||||
later redeploys keep those gifts); `carryState: { sp }` a board summon
|
||||
piece's SP only (PRTS "召唤物仅修改技力"). Nothing else is carried: a skill running at the end of the own combat enters
|
||||
switched off with the SP it had left — spent at its activation, 0 for a one-charge skill (`unitsEnd.skillActive` is
|
||||
reported, never carried; community report #34 — it used to restart for free).
|
||||
Timed deployment skills (`activateOnDeploy`) trigger once from the new deployment, consuming their deployment charge;
|
||||
they do not resume the previous skill state.
|
||||
The approved Egir carry policy rebuilds opening devour attributes while skipping its repeated HP loss on a living
|
||||
carry target; this prevents artificial knock-outs and revival-slot consumption before restoration. A fresh target
|
||||
without a previous state still takes that opening damage. Later combat, HP loss and redeploys follow the normal rules.
|
||||
`carryState: { down: true }` = an operator knocked out at the end of the helper's own combat (PRTS 卫戍协议/帮助 "上一阶段为
|
||||
退场状态的干员强制退场", user playtest #5 item 2): `start()` deploys it with everyone (initial `deploy` fires), then — before
|
||||
`battleStart` — withdraws it with reason `FORCED_EXIT` (constants.js `'forcedExit'`) and HP 0 (the end-of-phase HP ratio, as after
|
||||
@@ -370,7 +382,7 @@ enemy: 隐匿 kept out, 起飞 not), 【污染秽蚀】 on every ally inside (fl
|
||||
`alliesInRadius`) — through `dealDamage`, so shields absorb a damage
|
||||
tick, damage-taken modifiers scale it and it counts for 受击回复 SP and TAKE_DAMAGE skills like any hit (§4; element fills
|
||||
excepted). 【污染秽蚀】 (萨卡兹枯朽战车's 秽蚀轰击,
|
||||
萨卡兹枯朽战士's death — none when it dies silenced, its client template checks 沉默, DESIGN §25.18) is **true** damage, 50 / 25 per second on low / high ground (PRTS "每秒受到50/25点真实普通伤害 …
|
||||
萨卡兹枯朽战士 / 组长's death — none when either dies while silenced. The owner's official-play recall of 2026-10-12 is that a silenced death creates no fog, which contradicts the PRTS / in-game text 「不可沉默」; only `enemy_1267_nhpbr` and `enemy_1267_nhpbr_2` share this PollutedDie ability, DESIGN §30.23) is **true** damage, 50 / 25 per second on low / high ground (PRTS "每秒受到50/25点真实普通伤害 …
|
||||
同名效果不叠加", user playtest #6): a unit covered by several zones takes one tick per second (`unit.mem.pollutedAt`), so
|
||||
a crowd of dying 萨卡兹枯朽战士 totals 50 / s, not 50 × n. It is "可对空,无视无法选择": it also burns an airborne 起飞 ally
|
||||
(`ignoreSelect`), at the low-ground rate on a low tile. The other zones are no exception to 对地规避: a ground enemy's
|
||||
@@ -533,7 +545,8 @@ Getters: `s` (aggregated stats), `maxHp`, `atk`, `hpRatio`, `sp`, `spMax`, `canA
|
||||
flag `float` (近地悬浮, PRTS 术语释义 "算作空中单位") or `levitate` (浮空 "变为空中单位") — never an enemy under 缚地 (flag
|
||||
`groundbind`, "目标变为地面单位") unless a 浮空 lifts it again; movement and pathing read `motion` (a hovering enemy keeps
|
||||
walking the ground path). `deploySeq` counts deployments (and identifies one: `seq === u.deploySeq`);
|
||||
`aggroSeq` is the aggro order (= deploySeq, except the summons of the initial deployment, §1).
|
||||
`aggroSeq` is the aggro order (= deploySeq, except the summons of the initial deployment and prepared 外勤医疗 map
|
||||
characters, §1).
|
||||
|
||||
克莱门莎 S2's kit stores its cabin in `unit.mem.clemntCabin`. Before boarding each tick, it removes passengers
|
||||
that died, became hidden / uncarryable or cannot path to the next cabin position, removes their carry buff and
|
||||
@@ -541,7 +554,7 @@ returns their current `weight` (including modifiers). The same tick's count and
|
||||
list and budget (DESIGN §28.27); this is local to the kit, not a change to general displacement.
|
||||
|
||||
**Hit areas (`body.js`, user playtest #5 item 10).** A regular enemy is a point: in a grid range when the tile of its
|
||||
position (`round(y)`, `round(x)`) is a range tile, in a radius when its position is (DESIGN §3). A huge enemy (巨型单位:
|
||||
position (`roundHalfEven(y)`, `roundHalfEven(x)` — PRTS 作战机制 "四舍六入五成双"; `Math.round` would take 2.5 to 3) is a range tile, in a radius when its position is (DESIGN §3). An enemy held on the field edge is inset by `FIELD_EDGE_INSET` (1/1048576) [ASSUMED] so that rounding keeps it on the edge tile; movement, knockback and a unit's standing tile for range keys still use `Math.round` (§30.27). A huge enemy (巨型单位:
|
||||
`enemy.hitArea` from data/enemies.json `hitArea` — 假想敌:胄 / 管 / 盐风主教昆图斯 / 阿利斯泰尔 / “萨米的意志”, PRTS
|
||||
"受击判定区域为长4.95、宽2.95的长方形,向上偏移1.0") is hit anywhere on a rectangle `w` × `h` tiles centred on its position
|
||||
moved `dx` columns right / `dy` rows up: it occupies every tile the rectangle overlaps (`bodyKeys` — 5 × 3 tiles; PRTS
|
||||
@@ -891,7 +904,7 @@ instance) and skip `'counter'` / `'reflect'` damage. When the guard trips, the l
|
||||
| `applyStatus(tgt, key, {duration, source, value, force, point, stackAs})`, `removeStatus(tgt, key)`, `resistOf(unit)` | §3 |
|
||||
| `applyStrongest(tgt, key, {duration, value, mods, source})` | §3 — "同名效果取最高" for a non-catalogue effect |
|
||||
| `addBuff(unit, buff)`, `removeBuff(unit, key)` | §3 |
|
||||
| `spawnToken(ownerUnit | playerId, tokenId, row, col, { def, stats, hp, duration, untargetable, dir, kit, force, anySource })` | field tiles; def from data/tokens.json `variants[ownerChessId]` for the owner unit's selected skill / module (`tokenDef`); `dir` defaults to the owner unit's (else the player's: RIGHT, mirrored side LEFT; a legacy `facing` ±1 is still read); returns the token or null (tile busy; or the owner runs a **non-default** skill that does not produce the token — `producesToken` — unless `anySource`: kit install hooks written for the default skill run under every skill). `spawnDevice(key, row, col, { …, dir })` likewise |
|
||||
| `spawnToken(ownerUnit | playerId, tokenId, row, col, { def, stats, hp, duration, untargetable, dir, kit, force, anySource, preBattleAggroSeq })` | field tiles; def from data/tokens.json `variants[ownerChessId]` for the owner unit's selected skill / module (`tokenDef`); `dir` defaults to the owner unit's (else the player's: RIGHT, mirrored side LEFT; a legacy `facing` ±1 is still read); `preBattleAggroSeq` is a negative priority reserved for a prepared stage map character, before ordinary initial units; returns the token or null (tile busy; or the owner runs a **non-default** skill that does not produce the token — `producesToken` — unless `anySource`: kit install hooks written for the default skill run under every skill). `spawnDevice(key, row, col, { …, dir })` likewise |
|
||||
| `tokenDef(tokenId, ownerUnit | chessId)`, `producesToken(ownerUnit, tokenId)` | the token def a summon of that owner gets — `getToken(id, owner.defId, owner.def.loadout)`, exact even when two players of one field give the same chess different loadouts (prefer it over an id-only `battle.data.getToken(id, unit.defId)` for summon stats / blackboards); whether the owner's loadout makes the token (DATA.md §14 `sources` has 'skill' or 'talent'; true when the data does not tell: no own variant, player-owned summons) |
|
||||
| `spawnEnemy(enemyKey, { routeIndex, route, pos, mods, tag, sourcePlayerId, ownerPlayerId, bounty, countInTotal, inTotal, def })` | returns the enemy, or null past `MAX_ALIVE_ENEMIES`; `def` = inline record (enemies.json shape or normalised) for keys missing from data. A runtime spawn is never `inTotal` (§1.3 — the HUD capsule's own set) unless content passes `inTotal: true` explicitly |
|
||||
| `spawnDevice(key, row, col, { hp, obstacle, blockCnt, name, def, res, atk, bat, aspd })`, `setObstacle(r, c, on)` | obstacles re-path enemies; spawnDevice returns null outside the rect, on a living unit or on a knocked-out operator's tile |
|
||||
@@ -1065,6 +1078,11 @@ has the generic spec. Summons: `battle.tokenDef(tokenId, unit)` / `battle.spawnT
|
||||
(token skill of that slot: `variants[owner].bySkill[i]`, module attributes: `.byModule[id]`). Coverage of the selectable
|
||||
skills: `node tools/kit-coverage.mjs --missing [--tier N] [--strict]`.
|
||||
|
||||
忍冬's 追凶 records the per-enemy start time and applies its arts addition in the same `damaged` callback,
|
||||
including the first instance (PRTS talent remarks; DESIGN §30.3). It does not refresh the ten-second window on
|
||||
repeated hits. Its `vulpisHunt` / `addition` tags and re-entrancy guard prevent the rider from feeding itself.
|
||||
`test/content/vulpis-first-hit.test.js` checks actual HP loss and damage events with naturally ready skills.
|
||||
|
||||
Timed deployment kits: 宴 S2 uses `bb.duration` and skill ATK mods, loses current HP once on landing, and converts normal
|
||||
physical attacks to arts only while S2 is active; S1 has no such conversion. 斯卡蒂 S2 uses its own record's
|
||||
`s.bb.duration` / `s.bb.atk`; DRE-Y revival in place does not restart it. 野鬃 S1 uses its record's `duration` /
|
||||
@@ -1114,7 +1132,7 @@ Element conventions of the kits (user playtest #5 #3; official term dictionary:
|
||||
targeting: { maxTargets, rangeGrid, rangeExtend, priority, allInRange, canHitFly,
|
||||
noRangeExtend /* the skill's range ignores the unit's 攻击距离 (rangeExtend): 信仰搅拌机 S3, PRTS 备注 */,
|
||||
showOwnRange /* rangeGrid only selects targets, no official range change: the card keeps the unit's own range */ },
|
||||
attack: { dmgType, atkScale, healScale, splashRadius, splashScale, hits, projectile, maxTargets, dmgMul,
|
||||
attack: { dmgType, atkScale, healScale, splashRadius, splashScale, hits, funnelMul, funnelBodyHits, projectile, maxTargets, dmgMul,
|
||||
chain: {count, falloff, radius, sluggish}, heal: {mode, count, falloff, hpAtMost /* targets at or below this HP ratio only */}, onHitStatus: {key, duration, value},
|
||||
onHit(ctx), // once per attack (main target; target may be null for a splash landing)
|
||||
onEachHit(ctx) }, // per victim: main, every splash / chain victim — ctx += { dealt, kind:'main'|'splash'|'chain', isSplash, isChain, main, attackId }
|
||||
@@ -1199,10 +1217,11 @@ chess_char_2_07_a: (bb) => ({
|
||||
```
|
||||
|
||||
**3. Instant burst caster — 至简 `chess_char_3_13_a` 神工意匠** (bb `atk_scale 1.3, ct 2`; next attack 130 % arts, hits
|
||||
twice; 2 charges; the funnel profile already ramps damage on the same target):
|
||||
twice; 2 charges; the funnel profile's unreleased drones are additional damage that ramps on the same target, and this
|
||||
charged attack sets two body hits):
|
||||
```js
|
||||
chess_char_3_13_a: (bb) => ({
|
||||
skill: { kind: 'charges', attack: { atkScale: bb.atk_scale, hits: 2, dmgType: 'arts' } },
|
||||
skill: { kind: 'charges', attack: { atkScale: bb.atk_scale, hits: 2, funnelBodyHits: 2, dmgType: 'arts' } },
|
||||
}),
|
||||
```
|
||||
An AoE instant (德克萨斯 剑雨, bb `stun 2`: two hits of 120 % arts around her + stun) uses `onStart` instead:
|
||||
@@ -1341,7 +1360,7 @@ header of professions.js).
|
||||
| loopshooter | 回环射手 (user playtest #3): every attack throws a boomerang (`ai.js throwBoomerang`, projectile `'boomerang'`) out to the target at 15 tiles/s — it hits on arrival — and back to the thrower's current position at 3.75 tiles/s without damage (PRTS 跃跃 "投射物飞行速度15,返回时飞行速度3.75"); attacks only while holding it (every boomerang thrown caught — "必须回收全部回旋投掷物才可以进行下一次攻击", `unit.trait.boomerangsOut`) and with the attack cooldown ready, so the real interval is the longer of the two; a target dead mid-flight is not hit (it still flies to the last position and back); knocked out / withdrawn ⇒ lost, a redeployed thrower holds a fresh one; 跃跃 S2's extra boomerangs share the one flight (cnt hits); each catch fires `boomerangCaught`, and an attack profile's `boomerangOnward(ctx)` flies it on after the first hit before it turns back — ctx `hit` (a hit of the same attack), `comeBack(x, y)` once (a content error sends it back from the hit point): 娜仁图亚 S1 bounces, S2 dashes on and hits on its way back (kits/ops/op-narant.js) |
|
||||
| reaperrange | hits every enemy in range; ×1.5 (bb atk_scale) on the trait front grid (or its own line ahead) — both along its direction |
|
||||
| chain | chain N (trait text/bb max_target) with −15 % per jump (bb chain.atk_scale), 1.7-tile jumps (constants.js CHAIN_RADIUS, PRTS 溅射半径一览: 链术师 1.7; 1.8 until 0.1.1), sluggish on each hit |
|
||||
| funnel | drone damage 20 % → +15 %/hit on the same target → 110 % (bb init/delta/max) |
|
||||
| funnel | normal attack: one 100 % ATK body hit plus separate drone additional damage, 20 % → +15 %/hit on the same target → 110 % (bb init/delta/max; modules can change init/cap). `funnelMul` holds the drone ramp; `dmgMul` applies to both. Extra drones (`hits`) do not duplicate the body; an unreleased skill attack can set `funnelBodyHits` (至简 S2: two body hits and two additions). Body and additions share an `attackId`, but only the body is `isAttack`; additions carry `addition, funnel`. [ASSUMED] body instances first at the existing impact, one ramp per volley. A skill that only buffs or retargets the normal attack keeps the body; `isSkill` does not remove it. `funnelBodyHits: 0` is drone-only (荒芜拉普兰德 S2). 耶拉 S2 detaches `funnelMul`: one 100 % `isAttack` body in range, the unreleased addition off, and `1 + attack@cnt` arts skill drones (`kjera:drone`, `isSkill`, not `isAttack`) that keep the lock until the enemy dies or the skill ends. [ASSUMED] Those drones use the trait ramp; her blackboard has no drone scale. The base form continues the per-target ramp (one step per volley on a shared target) and writes it back; the hidden form restarts each drone at init and does not write the scales back. [ASSUMED] No flight delay; an untargetable lock is dropped and retargeted; stun and disarm do not pause the drones. 澄闪's skills and 荒芜拉普兰德 S3 stay on their own `dealDamage` paths. A normal deployment resets target, scale and any per-target ramp map to init, including revival against the same living target. A free move (`moveRedeploy`, the same carry as a 秘术师's stored energy and 乌尔比安 S3) keeps the ramp. An in-flight drone recomputes the ramp on impact. 洛洛 S2 detaches this multiplier from new body shots and inherits the trait ramp in an independent skill tick: `rockr:drone`, arts, `isSkill:true`, `isAttack:false`. Older projectiles cannot change its local ramp. A living selectable lock need not remain in body range; the drone returns on a kill or when the skill ends, and ending hands the ramp back and restores the normal multiplier (§30.26) |
|
||||
| mystic | 秘术师 (PRTS 分支特性信息 秘术师, GitHub #181): at its attack check (the attack ready, able to act, not disarmed) with no valid target — no target, or its kit's `canAttack` false (深靛 never picks a bound enemy) — it stores one energy (`storeEnergy`, up to bb times: 3, 深靛's MSC-X 4), an attack action: the attack interval restarts; a full store idles with the attack ready. The energies leave with its next attack that happens (`releaseEnergy` in `performAttack`, after `beforeAttack`: an attack cancelled before its shot keeps them) and land with the main hit, one arts attack hit each (`hitsFn` reads the hit's `info.energy`); a redeployment holds none [ASSUMED]. 维伊 / 黑键 plug their own `storeEnergy` (转置能量, elite energies) into the same check |
|
||||
| phalanx | no attack & DEF +200 %, RES +20 (bb) while the skill is off; while on, `rangeAoe`: each attack strikes every selectable enemy on its range at once (blocked enemies included; a stealthed one only when revealed or blocked), the same damage near and far, instant (`'beam'`) — "群体法术伤害" (secondary: Terra Wiki, Phalanx Caster: "attacks hit all enemies within their range"; supporting: PRTS 林 S3 备注 "单次普攻最多触发1次效果" — one normal attack can kill several — the same 锁定攻击范围 shape as the 轰击术师, and PRTS 溅射半径一览 documents no splash radius for it (it omits the 撼地者 too, so this is not proof); no primary source states a target cap; community report E3: it used to be one bolt + a 1.1 splash). 卡涅利安's charged S1 keeps the skill-off trait, 不攻击 included (kit `canAttack`, PRTS 备注) |
|
||||
| physician | heal the lowest HP% injured ally in range (a skill `targeting.maxTargets` widens any heal profile) |
|
||||
@@ -1486,6 +1505,11 @@ objects), and call `checkInvariants` at the end. Run: `node --test test/sim/*.te
|
||||
|
||||
## 11. Tools
|
||||
|
||||
Equipment/fortress corrections (§30.3): Kjerag Ice and Tear Gas use actual enemy damage events, including DoT,
|
||||
not attack declarations or element-gauge/HP-loss events. Fortress fallback targeting and skill automation include
|
||||
the operator's own tile after blocked/ranged candidates; only explicitly enabled skills retain melee splash.
|
||||
`performAttack` captures each shot's `fortressMelee` flag for impact resolution, independent of later target changes.
|
||||
|
||||
`node tools/simrun.mjs --mode mode_multi_normal --round 5 --stage act2autochess_m01 --lineup "角峰@9,7 隐现@10,4 艾丝黛尔*@12,5" --ascii 10`
|
||||
prints a per-unit damage/DPS/kills/heal table (+ ASCII frames: letters = operators, digits = ground enemies per tile,
|
||||
`^` = flyers, `■` = crates). `--wave <template>`, `--seed`, `--content generic`, `--hpMul/--atkMul/--speedMul`,
|
||||
@@ -1496,6 +1520,30 @@ skill × module, every bond at its threshold and at 999 layers, every leader fie
|
||||
reduced to digests in `test/golden/*.json`; `test/golden.test.js` fails, naming the scenario and the field, when a
|
||||
digest moves. A refactor must leave them unchanged; `npm run golden:update` records an intended gameplay change.
|
||||
|
||||
Cross-engine check (`tools/sim-check.mjs`, `public/dev/sim-check.html`): a browser simulates its battles and the server
|
||||
re-simulates them (SP_VERIFY, takeovers), so every engine must give the bits Node gives. `node tools/sim-check.mjs`
|
||||
builds the golden battle corpus (roster, bonds, fields, standins, diy — 267 BattleSpecs) and traces it in Node into
|
||||
`public/dev/simcheck/data/` (git-ignored, about a minute): after every tick the state (battle time, RNG state and per
|
||||
unit alive / removed / deployed, x, y, hp, atkCd, SP, the five element gauges, damage dealt / taken, healing, blocking
|
||||
count) hashed to the exact IEEE-754 bits, one hash per 30 ticks; every 30 ticks `b.snapshot()` (what viewers are sent);
|
||||
every 150 ticks a bounded traversal of unit-reachable state (depth 8; definitions, profile/kit, battle pointers, functions,
|
||||
accessors and `seq` left out); and `resultDigest(b.result())`. `/dev/sim-check.html` — on any device that reaches the server, a phone
|
||||
included — runs the corpus (or its fast subset, or a family) with the client's own loader (`loadBrowserSim`) and
|
||||
compares block by block: a battle that parts is reported with the first interval where a difference was detected, the level (the outcome, the
|
||||
snapshot, the state, or the hidden state only), and a download of that block's state vectors;
|
||||
`node tools/sim-check.mjs --diff <file>` replays the same ticks in Node and names differing recorded vector entries
|
||||
(with both values' bits). It cannot locate view-only/deep-only differences absent from those vectors.
|
||||
Special float values, including negative zero, survive JSON export. `--engines chromium,firefox,webkit [--subset fast|all|<family>]` runs the page headless after
|
||||
the trace (Firefox and WebKit through the `playwright` package when it is installed, `npx playwright install firefox
|
||||
webkit`; Chrome through puppeteer-core otherwise) and exits 1 on a difference. The tracing, comparison and page
|
||||
(`public/dev/simcheck/core.js`, `ui.js`, `node.mjs`) have no dependency on this repository; `stronghold.js` is the part
|
||||
that knows the sim. `--engines chrome` uses the local executable selected by `CHROME_PATH` (including Edge).
|
||||
The reference, corpus and dump share a content identity over scenarios, served sim/shared code and data. Dirty changes
|
||||
invalidate the reference. The page rechecks inputs before runs/dumps; `--diff` rejects stale or mismatched inputs.
|
||||
An empty/unknown selection, max-tick truncation or runtime exception is an error, never a zero-battle success.
|
||||
All-green means the recorded fields, sampled snapshots and result hashes agree: it does not prove equality of every
|
||||
internal field, event, projectile queue or rendered frame. Verification results belong to the tested revision.
|
||||
|
||||
## 12. Data notes (simdata.js)
|
||||
|
||||
Defs are normalised from data/*.json (DATA.md) with research JSON as a fallback. Chess: `stats {maxHp, atk, def, res, aspd,
|
||||
|
||||
+38
-1
@@ -4,6 +4,12 @@ Part of [DESIGN.md](../DESIGN.md) (the index; section numbers are global).
|
||||
|
||||
## 9. Client rendering contract (public/js/render)
|
||||
|
||||
Bond-layer audio (#482): live own preparation gains arrive as `m.layer`; the displayed battle's `layer` tuples
|
||||
play the same `bondUp` cue. A gain of several layers plays once, including gains to inactive bonds. The official
|
||||
`ON_ACT1AUTOCHESS_ADD_BOND` bank allows one sound and replaces the oldest; there is no added cooldown. Existing
|
||||
bond activation cues use this same channel. State snapshots, settlement backfill and buffered field-entry events
|
||||
do not generate layer cues. Field entry, leaving the game and muting cancel playing or still-decoding layer audio.
|
||||
|
||||
`import { createFieldView } from './render/app.js'`
|
||||
|
||||
```js
|
||||
@@ -55,6 +61,12 @@ A bond the mode never activates (标准: official `inactiveBondIdList`) reads
|
||||
|
||||
Interactions (research 09 §1.2 / §5, which supersede the first draft): shop card, upgrade and 机变 card are two-tap (select → confirm, §18.2) — a tap anywhere on the card, its confirm strip included, is the card's tap (shop cards have no ⓘ corner; the armed bar sits above the detail card; a shop card that cannot be bought right now opens its detail on tap, §20.7); a press / drop goes by the tile under the pointer (§18.1); drag bench↔board — over a legal tile the dragged unit stands on it, as in the official deploy drag, held under the pointer elsewhere (§27.62); a legal board drop (also onto the piece's own tile) opens the 4-direction deploy wheel (swipe UP/RIGHT/DOWN/LEFT with the rotated range striped orange; release in the centre, ✕ 点击取消 or Esc cancels); tap a unit's tile ⇒ underframe with **出售 +N** (and **撤退** on the board; items/Arts in the hand / temp: **销毁** — equipped items are locked, §6.2) plus its rotated range — the range follows the open detail card (`screens/game/range.js inspectRange`, GitHub PR #281): an own board piece in prep, and in battle or on a scouted board any ally operator / summon on the field with its current range (the live stats' `range` and `dir`, the snapshot's tile), never an enemy or a bench unit (UnitInfo `area` hand / temp); drag, the wheel and the pen hide it; no drag-to-sell; drag an item onto a unit's tile ⇒ equip that unit (both slots used ⇒ the replace dialog `ui/equipReplace.js` shows the incoming and the two equipped items; the player picks the one to replace — it is destroyed — `g.equip replaceUid`; cancel changes nothing); Arts are dropped on a tile and get the direction step; right-click or long-press ⇒ detail (or a tap on a shop card while the bar is locked); keyboard shortcuts — the defaults, which each player rebinds in 设置 → 快捷键 (`ui/gameLogic/shortcuts.js`, saved with the settings; §25.15) — `R` refresh, `F` freeze, `D` level-up, `Q` retreat / `X` sell the selected operator, `Space` ready (in a solo battle: pause / resume, §14 Solo pause); `Esc` closes (fixed); none acts while a text field has the focus or a dialog is open. All actions optimistic-free: UI waits for server state (latency is small).
|
||||
|
||||
The owner's `prepMapChars` (外勤医疗's 预备干员-医疗, or another displayed map-character variant) are read-only
|
||||
preparation views: tapping one opens its existing operator-style detail card and highlights its attack range from
|
||||
its board-space position and facing, including on a mirrored boss half. They cannot be dragged, sold or retreated
|
||||
from this preview. The inspection affordance does not make the preview a combat unit; the battle sim still creates
|
||||
the selected variant at battle start (§30.22).
|
||||
|
||||
Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer), per-unit attack/hit SFX (throttled; an impact plays for a hostile attack's phys / arts / true damage only, never an operator's skill-mode file for a normal attack, ≤ 2 copies of one battle sound — §18.4; a skill cue that reaches the client before its unit — the sim emits a deploy-tick cast ahead of the unit's `['spawn']` — is held and sounds once when the unit is tracked, `AudioManager.pendingSkill`, GitHub PR #292 by @LimitlessHPPK; entering a field late hands the sound the buffered `spawn` tuples only, `screens/game/early.js audioEarly`, so no old death, deploy or cast replays), boss music in R14/15.
|
||||
|
||||
---
|
||||
@@ -62,7 +74,8 @@ Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer
|
||||
## 13. Local-client art (optional, host-side)
|
||||
|
||||
`tools/local-extract/extract.py` (Python: UnityPy + lz4 + Pillow; decodes Arknights' custom **LZ4AK** bundle compression via `aklz4.py`) extracts the art the web download does not include (some of it, most official HUD sprites among it, is on the public mirror too — §22.5) from a locally installed client (CrossOver Windows build or PlayCover iOS build) into `public/assets/local/**` and writes `data/local-assets.json` (`{ groups: { '<subdir>': { name: { path, w, h, kind } } } }`). Everything here is **optional**: clients must render correctly when the file or any entry is missing (fall back to the web assets / procedural art). The emotes and the guide pages below are on the public mirror too (§22.5): fetch-assets downloads them (data/assets.json `ui['emoticon/<dir>/<picId>']`, `ui['guide/<key>']`) and the client shows the local copy first, then that one (`data.js artUrls`); docs/DEPLOY.md §6 lists what still needs the client.
|
||||
- `map/autochess/TX_autochessi_*` — the real autochess board atlas (`_D` diffuse 2048² with floor tiles, gold-framed high-ground plates, REINFORCEMENTS bench pads, EQUIPMENTS pads, crates, gates; `_N/_M/_E` normal/mask/emission; `common_D` device icons; `BG` backdrop). `map/autochesssand/*` — the sand-theme variant.
|
||||
- `map/autochess/TX_autochessi_*` — the real autochess board atlas (`_D` diffuse 2048² with floor tiles, gold-framed high-ground plates, REINFORCEMENTS bench pads, EQUIPMENTS pads, crates, gates; `_N/_M/_E` normal/mask/emission; `common_D` device icons; `BG` backdrop). `map/autochesssand/*` — the textures and materials of the sand theme (the theme of nine of the eleven battlefield scenes, §15).
|
||||
- `scene/<stageId>/` and `scene/tex/` — the official scene of a battlefield (§15 "Official battlefield scenes", DESIGN §30.21): `scene.json` (materials, key light, chunk records), `scene.bin` (the meshes in board space), the lightmap pages `lm<i>`, and the materials' albedo textures shared by the scenes. Read from the scene bundles `scenes/activities/<season>/level_<stage>/…`, which the client carries only while the season runs: `extract.py --only scene` takes them from the client or from `--scenes <dir>` / `.cache/official-scenes/*` (`tools/local-extract/fetch_scenes.py` downloads them from the official CDN's in-season version). The 0.2.5 full release zip includes the extracted files when the maintainer extracted them before packaging; a lite zip and a source install may fetch and extract them (`node tools/setup.mjs --scenes`). The repository never stores the bundles or the extracted files.
|
||||
- `ui/{common,battle,outer}/*` — every sprite of the three official autochess UI bundles (tier chips I–VI, bond disc rings, frames, banners, HUD pieces, badges, backgrounds, mode art).
|
||||
- `emoticon/<dir>/<picId>.png` — the 36 official battle emotes, 6 themes (`shared/constants.js → EMOTE_THEMES / EMOTES`, `emoteArtPath`; data/emotes.json); also downloaded from the mirror (§22.5).
|
||||
- `guide/autochess_{home,shop,handbook}_N.png` — the 19 official tutorial pages; stored squashed to 1024² — **display at 16:9** (the mirror copies too, §22.5).
|
||||
@@ -77,6 +90,10 @@ Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer
|
||||
|
||||
Findings (2026-09-28): 卫戍协议 levels have no scene bundle (`mapId` null; the official hot-update list contains none). The client **builds the board at runtime** from the stage grid using the map theme `arts/maps/map_autochess` (materials `MT_autochess` (albedo `TX_autochessi_D` 2048², normal `_N`, metallic/gloss `_M`, emission `_E`), `MT_autochess_common` (`common_D/_E`: device/gate icons), `MT_autochess_Transparent` (`Transparent_D/N/M/E`), `MT_autochessi_BG` (`TX_autochessi_BG`)), the background meshes `arts/maps/map_autochess/bkg_mesh.ab` (`S_Background_common`, `S_Background_shadow`) and `arts/maps/common/meshes/s_background_common.ab`, device meshes (`s_common_box_01` crate, `s_wind_device` blower, …), plus the standard gate/objective effects. The level's `options.configBlackBoard` carries the official camera framing (`default_pos_x/y`, `default_size`, `enemy_pos_x/y`, …).
|
||||
|
||||
Correction (2026-10-11, GitHub #532; PR #533 by @andrew05060414): the first finding above was made off season and is wrong. While a season runs, the official hot-update list carries **one prebuilt scene per battlefield** — `scenes/activities/act1autochess/level_act1autochess_m01…m07` and `scenes/activities/act2autochess/level_act2autochess_m01…m04` (each with `…/lightingdata.ab` and `…_lighting_settings.ab`; Android versions `25-11-21-15-21-44_ee1197`, `26-03-13-06-11-47_14fb99`, `26-04-13-12-31-42_e9b560`); the lists of 2026-09-13 and 2026-10-08 have none, and no 卫戍协议 level data either. The asset index of those versions (`<hash>.idx`: each bundle's dependencies) gives each scene's art: act1 m01–m07 and act2 m02 / m04 depend on `arts/maps/map_autochesssand` (m05 and act2 m04 also on `map_doss_shore`), act2 m01 on `map_lakegarden` + `map_syracuse_metro`, act2 m03 on `map_kxmr` + `map_lm_center`. **No battlefield scene depends on `arts/maps/map_autochess`** (the metal theme the procedural board below draws; in the current client the 矢量突破 scenes `act3break` use it). A scene holds the logic tiles (`(row,col)#tileKey` at anchor-local (col, row, −height), high ground 0.4), the art under `GraphicHolder` (static parts merged into "Combined Mesh (root: scene)" objects, lightmapped: Shadowmask mode, one directional key light), and three sub-graphics `HIDDEN@1…3` (`MapSubGraphicHolder`) that the game switches on: the left normal field, the right normal field and the boss field — the areas of `layout.js AREAS`.
|
||||
|
||||
Official battlefield scenes (`board3d/load.js loadScenePack`, `scene.js setScenePack`, `materials.js sceneMaterial`): when `data/local-assets.json` lists `scene/<stageId>`, the 3D board draws that scene instead of the procedural tiles, cliffs and background plane; the devices, gate / objective boxes, field edges and terrain overlays stay as below. The base part is always drawn; a `HIDDEN@n` part is drawn when its centre lies in one of the built areas. Chunks of those shown parts that share a material and a lightmap page are one mesh when the area is built (`load.js mergeSceneChunks`); water shares a mesh across pages because its shader has no lightmap. A switched-off part stays out of that mesh. Without the scene (not extracted, a load failure, an unknown `version`) the procedural metal board is drawn as before. Approximations: the official shaders are not reproduced — a lit material is albedo × (the lightmap's baked light + key light · N·L · the baked shadow mask), in linear light; normal, metallic and emission maps, the vertex-colour blend (`_HG_VERTEX_COLOR_BLEND_ON`), the sky box, the map effects (`mapeff_*`) and the post-processing profile are not drawn; `Torappu/Scene/StylizedWater` is the board's water shader in the material's colours; crates cast no shadow on a scene. **[ASSUMED]** `SCENE_LIGHT` (`direct` 0.8, `indirect` 0.4, `ambient` 0.2: tuned by eye against the PRTS screenshot of 战场#06, the official post-processing being unknown) and the RGBM reading of the lightmap pages (`scenes.py decode_rgbm`: Unity's linear-space decode, range 5). Owner decision (2026-10-12): these extracted files belong to the 0.2.5 asset pack and may ship in the full release zip; a lite package or a source install may download and extract them on that machine. Missing extractor dependencies keep the metal board. The repository still never stores the official files.
|
||||
|
||||
Target: a **real 3D board layer** rendered with three.js (vendored ESM, no CDN) underneath the existing Pixi layer:
|
||||
- Geometry generated from `data/stages.json` like the official builder: LOW tiles = top quads; HIGH tiles = raised blocks with side walls; forbidden/void, fences, gates (red), objective (blue), hand/temp pads, special terrain; UVs mapped onto the official atlas regions (reuse/extend `public/assets/local/map/autochess/tiles.json`); normal + emission + metallic maps; lighting tuned to match official screenshots (key directional light, ambient/hemisphere, soft contact shadows); official background meshes & texture; device meshes (OBJ exported by `tools/local-extract`) with their textures; animated gate/objective effects.
|
||||
- One camera model drives both layers: `public/js/render/projection.js` keeps its public API (`worldToScreen`, `screenToWorld`/tile picking, presets, transitions) but derives it from a three.js `PerspectiveCamera` initialised from the official configBlackBoard framing, so Spine units, FX, drag/drop and hit-testing stay aligned with the 3D board.
|
||||
@@ -155,3 +172,23 @@ It does not diagnose a particular user's GPU or persist a disabled renderer pref
|
||||
Community UI and recovery (§29.6): DIY reuses roster stats and skill previews; Swire's coins show current/cap. The lobby player chip opens statistics and the responsive header keeps optional controls from overlapping its title. Settlement's summary scrolls separately from Return. Failed board art can retry, and failed three.js imports use two bounded fresh URLs. Failed engine initialization can remount on online/visibility, at most twice consecutively, cleaning up each previous view; explicit fallback/2D stays selected.
|
||||
|
||||
Replacing a field renderer also resets the game screen's entered-view identity. Frames are buffered while the replacement loads; after subscribing, the local runner re-shows full current metadata/snapshot, or the server resends the watched field. Real local and server-streamed battles preserve the same simulation object and clock through recovery, including enemies spawned before the new renderer existed.
|
||||
|
||||
§30.4: the HP tower retains current HP until settlement and shows pending loss separately. Shop refresh is single
|
||||
flight per client until its promise settles (success or error); the button and R key share that state. DIY-only
|
||||
potential/cultivation changes are exportable; spectator member resolution includes the owner's DIY selection.
|
||||
Bond tooltips show banned visible members in one clamped scrollable compound box, stacked on narrow screens.
|
||||
An open list updates on member changes; long-press lists persist for reading and close on outside tap/Escape.
|
||||
The developer simulation checker and its bounded verification claims are specified in SIM §11 and history §30.5.
|
||||
|
||||
Operator attack clips play once per actual attack (§30.7), retaining their interval-based speed. A prepared wind-up
|
||||
is consumed without replay; a later queued attack may start during the previous recovery. Without another attack
|
||||
the actor returns to base/skill idle. Late operator events still start at the strike frame; no sim timing changes.
|
||||
|
||||
Connection recovery sits above the AI-away overlay and below global modals (§30.8), so a replaced session can
|
||||
be reclaimed by pointer while away. This does not change the stacking behavior of modals nested inside a screen.
|
||||
|
||||
§30.20: 设置 →「官方伤害数字」 defaults off. Off, damage numbers stay the typed every-hit popups. On, and only
|
||||
while 「显示伤害数字」 is also on, the field draws a red integer for a `dmg` tuple marked `'r'` and a yellow MISS
|
||||
for a dodge fx whose payload is `num: 'm'` (PRTS 作战机制 「伤害红字(暴击)」). Ordinary hits and heals draw no
|
||||
digits in that mode. The mark is computed in the sim; the random offset above the model is client-side. The
|
||||
simplified board uses the same mark with a fixed slot. The title footer links the project repository in a new tab.
|
||||
@@ -4,6 +4,19 @@ Part of [DESIGN.md](../DESIGN.md) (the index; section numbers are global).
|
||||
|
||||
## 5. Simulation (server/sim) — engine contract
|
||||
|
||||
联防 restoration (§30.13): the initial setup establishes buffs and devour attributes before restoring living
|
||||
operators' exact HP ratio and total SP; summons restore SP only. Opening Egir devour does not damage a carried
|
||||
survivor a second time. Forced-down carry stays out without spending an Egir revival; subsequent real knock-outs
|
||||
and redeploys are unchanged. No 1% / 1-HP floor is added to a positive carried ratio.
|
||||
|
||||
Expanded automatic skill areas (§30.11) include 司霆惊蛰 S2's current low-terrain flood when it strictly contains the
|
||||
initial area, 斩业星熊 S3's selectable air targets, and 予愿安洁莉娜 S3's initial 3-9 ∪ x-4.
|
||||
Permanent range extension, control/operation cooldown and targetability still apply; normal attacks are unchanged.
|
||||
|
||||
Prepared 外勤医疗 map characters (§30.22) keep an earlier aggro order than ordinary initial operators. Preparation
|
||||
can open their detail card and board-space range. Their `deploySeq` is still the later runtime spawn. The retained
|
||||
team grant (§30.14) is unchanged.
|
||||
|
||||
### 5.1 Battle API (implemented by the sim-core owner; consumed by match & content)
|
||||
|
||||
```js
|
||||
@@ -117,6 +130,12 @@ Engine helpers (content must use these, never touch internals): `battle.dealDama
|
||||
|
||||
### 5.5 Combat rules (engine)
|
||||
|
||||
- **Prepared 外勤医疗 slot**: 预备干员-医疗 is visible on the field during preparation. At `battleStart`, the elite count
|
||||
chooses either that medic or its Touch replacement for the same slot. [ASSUMED] This replacement does not move the
|
||||
slot behind the ordinary starting operators in effective deployment priority: both variants receive an earlier
|
||||
`aggroSeq`, while their actual `deploySeq` still records the later runtime creation and guards deployment-scoped
|
||||
effects. On a shared field, prepared slots follow player order [ASSUMED]. An ad-hoc map-character spawn during
|
||||
combat retains ordinary summon priority. See §30.22 and SIM.md §1.
|
||||
- **Initial deployment** at t=0, free: operators by column — the left column first, top to bottom within a column (right-side boss player: from its own left, the highest field column) — then the summons (PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物", read as a scan: down each column, the columns left to right — the order PRTS gives the 阿戈尔 devour, "更靠左和靠上"; row-major until 0.1.3, §23.23; `aggroSeq` ranks every start-of-battle summon after all operators) — a skill's placed summon (赫默 医疗探机, 巫恋 诅咒娃娃) included, once and free ("所有手动部署的召唤物,无视所属干员的持有状态…作战开始时立即部署一次"; afterwards it re-appears on its tile each time the owner's skill gives one, §20.1); on a shared field (联防, boss) the players' i-th operators deploy together [ASSUMED]. Deploy effects fire (`deploy` with `initial: true`). Then operators carried into 联防 knocked out (`carryState.down`) are forced out (`FORCED_EXIT`, HP 0, §19.3). Then `battleStart`, after which the forced-out operators' timers are re-read (redeploy-time effects that start with the battle, e.g. 机变 征召, cover them too).
|
||||
- **DP**: per player, start 10, +1/s, cap 99; used only for auto-redeploy (dead op: after `respawnTime`, if the tile it lies on is free and DP ≥ cost ⇒ redeploy there with full HP, SP = initSp). A knocked-out operator (and, since 0.1.2, one forced out by its own effects, §22.15) stays **down** on the tile it fell on — its own home when it fell on another board piece's home (that piece on the field or not) and its home is free, and 乌尔比安 knocked out while his S3 has moved him always on his deployment tile when it is free (the owner's deviation of 2026-10-07, `Unit.downAtHome`, §25.17.3) — until it redeploys there, and no ally deploys or moves onto that tile meanwhile (PRTS 卫戍协议/帮助 §作战阶段 单位部署; `Battle.downOn` / `restTile` / `_layBody`, §21.24; b.snap `down`, §18.3: counting / waiting for DP / its tile taken, a safeguard) — also one that entered 联防 knocked out (`FORCED_EXIT`, its full redeploy timer — confirmed by the user — with the battle-start redeploy effects, §19.3); summons, devices and enemies leave at once.
|
||||
- **Blocking** (contact radius, PRTS 游戏数据基础 §阻挡半径 / 作战机制 "中点判定…案例:阻挡"; `constants.js BLOCK_RADIUS`, `Battle._checkBlock`, §19.2): an operator with free capacity blocks an unblocked, blockable enemy whose position lies within its block radius of its centre — ground 0.7071 (squared 0.49999037), 0.8944 × the blocker's 阻挡半径倍率 for blockFly units vs flyers (起飞; mod `blockRadiusScale` = 倍率 − 1 — PRTS 游戏数据基础 "飞行阻挡半径 = 0.8944 × 阻挡半径倍率", 凯尔希·思衡托 +0.23), devices 0.4472 (阻隔工事 / 障碍物); air units (FLY, 近地悬浮, 浮空) only by blockFly units. A ground enemy is never blocked by a unit standing on a tile ground units cannot pass (围墙 / 围栏: PRTS 地形机制 "部署在其中的单位,若当前阻挡类型为'地面阻挡'则无法阻挡敌人" — an enemy pushed or pulled against the fence walks on; air blocking there stays [ASSUMED], §21.23) nor by an airborne (起飞) unit, which blocks flyers only (§21.22). An enemy holding 不可阻挡 (PRTS 异常效果 BLOCK_FREE 「无法阻挡/被阻挡,自动解除阻挡」) — `unblockable` (恐惧, 诱导, enemy abilities), 浮空, and 沉睡 (SLEEPING = 无法行动+无敌+不可阻挡) — is never blocked and is released at once: a sleeping enemy frees its blocker's slot, stays where it is and, once awake, is blocked again only by a blocker with room (§24.9). The nearest such operator wins (ties: scan order [ASSUMED]). It is re-checked every tick, moving or not, so another operator in contact with room takes the enemy over when its blocker dies, is withdrawn or is stunned, and an operator whose capacity frees up grabs an overlapping enemy; one that finds no room walks on. A stunned or frozen enemy is checked too — 晕眩 / 冻结 hold no 不可阻挡 (PRTS 异常效果: the STUN / FROZEN state machines stop its moves and attacks) —, so an operator redeployed beside a held enemy blocks it where it stands and lifts its 隐匿; one held short of contact is blocked once it walks in after the status (GitHub #232; until 0.2.0 the check waited for the status to end). A head-on enemy stops at contact (≈ 0.71 tile), outside the blocker's tile. Enemy uses `blockCnt` capacity (default 1). Blocked enemies stop moving and attack their blocker if melee (or anyone in range if ranged, blocker first). **Every blocker** — melee units (重装要塞 / 领主 / 哨戒铁卫 included), summons and a ranged operator on a melee tile alike — whose attack hits enemies may always target the enemies it blocks, in range or not, whatever its facing, and targets them first — a 速射手's air priority (优先攻击空中单位) excepted, which comes before the enemy it blocks (the owner's decision of 2026-10-08, GitHub #220) — ("可以选择且优先选择阻挡单位", PRTS 选择器; the user's rule after playtest #6, "阻挡了就一定要能打到": officially the collision pushes a blocked enemy to its blocker's front; `Battle.blockedTargets`, `targeting.js sortEnemyTargets`; §20.3 — it replaces the playtest #5 QA's melee-only restriction of §19.2). A healer (dmgType `heal`, or a skill attack turned into a heal) selects injured allies only and keeps healing while it blocks (PRTS 卫戍协议/帮助 "对于医疗干员(咒愈师分支除外),攻击目标为需要治疗的单位"; §21.17); 咒愈师 follow the blocker rule. The huge leaders are 自缚 + unblockable (§7).
|
||||
@@ -167,11 +186,40 @@ Profession traits (default behaviours implemented once in the engine, keyed by `
|
||||
|
||||
---
|
||||
|
||||
Unreleased funnel drones deal additional arts damage alongside the ordinary attack (PRTS 分支特性信息 驭械术师, §30.24).
|
||||
The engine keeps a 100 % ATK body instance and a separate drone instance, each through mitigation and shields.
|
||||
`funnelMul` holds only the drone ramp; the attack's `dmgMul` (至简's talent) is shared once. Extra attached drones
|
||||
raise `hits`, not the body count; 至简's charged S2 sets `funnelBodyHits` to two. Both share one `attackId`, attack
|
||||
hook and SP / charge use; additions have `addition` and `funnel` tags and do not count as `isAttack`. [ASSUMED] Body
|
||||
instances precede additions at the existing projectile impact; consecutive hits keep one ramp per volley. A skill
|
||||
that only buffs or retargets the normal attack keeps the body; `isSkill` is not the switch. `funnelBodyHits === 0`
|
||||
is drone-only (荒芜拉普兰德 S2): the ramp stays inside those hits. 耶拉 S2 detaches `funnelMul` the way 洛洛 S2 does:
|
||||
one 100 % body inside her range, the unreleased addition off, and the released drones as separate skill hits that
|
||||
keep the lock until the enemy dies or the skill ends (§30.24). 澄闪's skills stay on their own `dealDamage` paths.
|
||||
|
||||
Funnel ramps return to the initial scale on a normal deployment, including automatic revival against the same living
|
||||
target (§30.25). The reset clears the current target, the scale and any per-target ramp map, and leaves other trait
|
||||
state alone. A free move (`moveRedeploy`, the same carry as a 秘术师's stored energy and 乌尔比安 S3) keeps the ramp.
|
||||
An in-flight shot recomputes the ramp when it lands, so a projectile launched before the reset can still step the new
|
||||
ramp.
|
||||
|
||||
洛洛 S2 releases its trait drone into an independent skill tick (§30.26). Acquisition uses the body's normal target
|
||||
selection; a living selectable lock stays valid outside the body's range and returns only when that enemy dies or the
|
||||
skill ends. The body retains its own ranged ordinary attacks. The released ramp inherits the trait state but is
|
||||
isolated from older projectiles, and while it is out the unreleased addition is off. Ending the skill returns that
|
||||
ramp to the trait and restores the normal multiplier and the overload cap. S1 and other operators' funnel attacks keep
|
||||
their existing paths. [ASSUMED] There is no flight delay; an untargetable lock is dropped and retargeted; body control
|
||||
does not pause the released drone.
|
||||
|
||||
---
|
||||
|
||||
## 7. Content modules (server/sim/content + match/effectsMeta handlers)
|
||||
|
||||
Each domain file `server/sim/content/{tokens,bonds,garrisons,items,bands,enemies,bosses,devices,choices}.js` exports `install(battle)` (battle side; called once per Battle by `content/index.js → installContent`) and `registerMeta(registry)` (prep side; called once at server boot by `match/effectsMeta.js`, API documented in `docs/META.md`). Kits live one per file in `content/kits/ops/` (`export default { [baseChessId]: (bb, chess, def) => Kit }`), listed in `content/kits/index.js` (guide: `content/kits/README.md`). Content must be **data-driven from blackboards** (numbers never hard-coded when a blackboard key exists). Every effect gets at least one unit test in `test/content/*.test.js` using the `test/helpers/battleHarness.js` harness (provided by sim-core).
|
||||
|
||||
- kits: all 112 visible chess + hidden ones used by effects (盟约·辅助干员), default skill + talents + trait specifics; tokens. The 72 non-collaboration 6★ DIY operators include 克莱门莎 (§28.1); her launch-day provisional mechanics are local to her kit. 黍 GUA-Y and 乌尔比安 CRU-Y follow the generated module blackboards and existing module-level rules (§28.2).
|
||||
忍冬's 追凶 adds arts damage on the first qualifying instance too; the per-enemy window starts on that instance,
|
||||
and repeated hits do not extend it. The addition keeps its own tag and re-entrancy guard (§30.3).
|
||||
克莱门莎 S2 removes dead, hidden, self-bound / teleport-immune and unreachable passengers before checking new
|
||||
boarding in the same tick. Each released passenger returns its current weight to the cabin's remaining budget
|
||||
and frees its slot (§28.27); neither corpses nor failed carries reserve capacity.
|
||||
@@ -233,3 +281,14 @@ tile and attack-reach checks. Hand/temp rows, empty halves in single-player mode
|
||||
The general `onOwnBoard`/`onFieldBoard` contracts, other movement, summons and preparation placement remain unchanged.
|
||||
|
||||
香槟炸弹 also rejects ordinary allied auras and inspiration as a device, through its shared internal selection barrier (§29.11). This does not isolate other unhealable summons or alter its detonation.
|
||||
|
||||
§30.3 corrections: fortress targeting is blocked → ordinary range → own tile (selectable ground targets only).
|
||||
Automatic skill checks include that extra tile. Melee attacks are single-target unless the skill explicitly gives
|
||||
melee splash (Horn S2). Each shot retains its launch-time melee mode while flying. The bounty catapult's normal
|
||||
triple attack splashes at radius 1.5; 0.5 belongs to its 入戏 override. Clementia S3 accepts enemy erosion bursts only.
|
||||
Kjerag Ice and Tear Gas proc on actual carrier damage to enemies, including DoT/skill/splash; elemental gauge fill,
|
||||
HP loss, bare attack events and zero damage do not qualify. These changes do not alter other items' attack hooks.
|
||||
|
||||
Xi alone opts into hostile selection immunity during its ten-second post-rebirth invincibility (§30.8).
|
||||
The generic `invulnerable` flag remains damage protection only; other rebirth kits retain their existing choices.
|
||||
Presence-only and SP-only skill triggers are not suppressed merely because their enemies are unselectable.
|
||||
+18
-2
@@ -27,7 +27,7 @@ LOBBY(room) → INFO_CHECK (co-op 25 s; solo and any single-human match untimed
|
||||
PRTS "上一阶段为退场状态的干员强制退场", its full redeploy timer — confirmed by the user (§19.3))
|
||||
SETTLE (LP −min(leaks,10) per player (unite survivors to their source); eliminations; layer gains applied; coins; broadcast;
|
||||
after a 联防 the outcome and every player's charge ride the SETTLE m.public as `uniteResult`, GitHub #235)
|
||||
R14 FINAL_ASSAULT (merged team LP; pairs by seat; shared boss pool; 120 real s countdown, no hard stop; overtime −1 team LP per
|
||||
R14 FINAL_ASSAULT (merged team LP; random pairs fixed for this match; shared boss pool; 120 real s countdown, no hard stop; overtime −1 team LP per
|
||||
real s after 120 real s, or 150 with an AI teammate at fight start (§29.1, m.public.overtimeAt); no layer gains; team LP 0 ⇒ defeat at once (PRTS 直接失败),
|
||||
pool 0 ⇒ victory — the first end the server registers decides, §20.5)
|
||||
R15 HIDDEN_CORE (if difficulty ≥ NORMAL and Σ activated layers > 350 solo / 1200 co-op and team LP > 1)
|
||||
@@ -60,7 +60,7 @@ Summons (§20.1): an operator on the board sends its manually deployable summons
|
||||
|
||||
Equipment (research 04 §2 + addendum, 09 §1.2): 2 slots per chess; a third item on a full carrier replaces the equipped item `replaceUid` names (the replace dialog's pick, official `UseEquipUp.unloadInstId`; absent ⇒ the oldest; a uid not equipped on the target ⇒ `BAD_TARGET`, nothing changes) and the replaced item is destroyed. A consume-on-equip item (盟约之币, 博士投影 …) on a full carrier replaces the same way before it resolves, so the carrier is left with a free slot (the normal 博士投影 takes it until the next round start); a target its effect refuses (博士投影 on an elite) keeps both items — PRTS 卫戍协议/帮助 "达到上限强行佩戴会改为替换装备" names no exception (GitHub #263; the client opens the replace dialog for it, `equipReplaces`). Equipped items are otherwise **locked**: `destroy` (销毁) accepts hand / temp items only (`BAD_TARGET 'equipped items are locked'`); equipment goes back to the hand only when its carrier merges, is sold, or is destroyed by 突变细胞's transformation (the cell itself returns too; PRTS 帮助 "在失去该干员(干员出售、销毁、合并等)…时自动卸除", §21.1) — a sale is refused with `HAND_FULL` when there is no room for it; returned equipment is stowed before the operator a transformation gains and before a merge elite's summon cards, and every path that hands the player an item — returned equipment included — ends with the auto-merge, so no player holds two identical mergeable normal items (§21.7).
|
||||
|
||||
Economy/shop/pool/merge/hand rules: exactly as research 00-INDEX §3–§4 (income `min(3+r,12)`; prices by tier; refresh 1; freeze toggles all; sell +1; items not sellable; upgrade prices per mode with −1/round; single-player pool copies 12/14/18/16/8/5 plus measured exceptions; co-op shared operators use twice those counts, while equipment has separate shared stock (DESIGN §29.8); elite = 3 copies; odds = copy-weighted draw over unbanned visible chess with tier ≤ level; merge 3 (风丸 2) → elite onto the consumed deployed copy's tile, else to the hand (§20.11) + reward offer of 3 **different** free chess of tier `min(level+1, 6)` (copy-weighted; a tier short of different chess tops up from the tier below [ASSUMED], §20.6); full hand blocks every buy / reward pick, a merge-completing one too (PRTS 卫戍协议/帮助 §手牌区, GitHub #82); overflow to temp; a free hand slot pulls the temp pieces back in at once, right → left (`PlayerState._fillHandFromTemp`); temp blocks ready).
|
||||
Economy/shop/pool/merge/hand rules: exactly as research 00-INDEX §3–§4 (income `min(3+r,12)`; prices by tier; refresh 1; freeze toggles all; sell +1; items not sellable; upgrade prices per mode with −1/round; single-player pool copies 12/14/18/16/8/5 plus measured exceptions; co-op shared operators use twice those counts, while equipment has separate shared stock (DESIGN §29.8); elite = 3 copies; odds = copy-weighted draw over unbanned visible chess with tier ≤ level; merge 3 (风丸 2) → elite onto the consumed deployed copy's tile, else to the hand (§20.11) + reward offer of 3 **different** free chess of tier `min(level+1, 6)` (copy-weighted, including the player's stocked 自选 of that tier — one shop level before the ordinary shop, §30.23; a tier short of different chess tops up from the tier below [ASSUMED], §20.6); full hand blocks every buy / reward pick, a merge-completing one too (PRTS 卫戍协议/帮助 §手牌区, GitHub #82); overflow to temp; a free hand slot pulls the temp pieces back in at once, right → left (`PlayerState._fillHandFromTemp`); temp blocks ready).
|
||||
|
||||
**Temp overflow (临时整备区, row 8; user playtest #3 item 3).** Passive gains that find the hand full (merge elites, grants — the operator a 突变细胞 transformation gains included, §21.1 —, battle-result rewards, equipment returned by a merged / sold / transformed carrier) overflow into the 5 temp slots, and Ready is refused while any is occupied (official "直到溢出情况排除才可开始进行作战"). A temp piece is resolved — a chess sold back to the pool with its summons removed, items destroyed, a summon stack removed (it comes back at the next round start, PRTS "干员所属召唤物会于下一回合返还", §20.1) — only at the deadline of the **first prep in which its player could act on it** (`PlayerState.tempDue(piece)` vs `prepsEnded`; every write into a temp slot goes through `_putTemp`, which records it): a piece that arrived during a prep before Ready is due at that prep's end; one that arrived after Ready or during the `<休整期结束时>` effects, or outside PREP (COMBAT, 联防, SETTLE — battle-result grants, a SETTLE merge's elite, returned equipment —, the next ROUND_START, 机变) is kept, visible and usable (move to a free hand slot, place, equip, use, destroy, sell) through the **next** prep. Un-readying (`setReady(false)`) makes whatever arrived while ready due at the current prep. ROUND_START no longer wipes temp (it used to destroy what SETTLE had put there before the player ever saw it). A merge's elite whose merge consumed a deployed copy takes that copy's tile and facing (§20.11); otherwise it goes to the hand, overflowing into temp (also outside PREP). `server/match/audit.js` (`tools/matchrun.mjs --check`) tracks the arrivals itself and flags any temp piece that outlives its due prep. The UI frames the row on the board with the rule (§17.5).
|
||||
|
||||
@@ -106,6 +106,8 @@ Official rule (bwiki 盟约, 更新公告 5114): before a match the player canno
|
||||
|
||||
**Loadout wire & state:** the client keeps a per-browser loadout `{ [baseChessId]: { skill: index, module: uniEquipId | 'none' } }` (localStorage) and sends it with C2S `room.loadout { entries }` (validated: known visible chess, legal skill index for that chess's normal AND elite status, legal module id). The lobby stores it on the seat; at match start `Match` receives `seats[].loadout`; `PlayerState` keeps it immutable for the match; board pieces are resolved to `{chessId, skillIndex, moduleId}` when building `PlayerBattleInput.units[]` (fields `skillIndex`, `moduleId`; BattleSpec carries them). Bots use defaults. m.private exposes the effective loadout so the UI (shop cards, detail panel) shows the chosen skill/module.
|
||||
|
||||
Client sync preparation may await chess/backups data. A later edit (including restoring defaults) or `welcome` invalidates the earlier preparation: its payload, missing-data result or failure cannot send or change sync status. The current edit keeps the existing debounce, close flush, ordered requests and retry/lock handling (`public/js/ui/loadoutSync.js`).
|
||||
|
||||
**潜能 / 练度 (0.2.2, the owner's decision of 2026-10-08: 「调配干员里自己设置吧,默认满潜满加成」):** official — an operator fights at the player's own potential (调度手册 「卫戍协议中干员潜能由自身已持有干员潜能决定」; an unowned 特许 operator at potential 1) and an owned operator of the same name gets the 自持有 练度 bonus (activity_table `cultivateEffectList`: 未精英化 +0 %, 精英1 ATK / DEF +5 %, 精英2 ATK +10 % DEF / HP +5 %, 精英2 Lv.60 +10 % each — a multiplier of its own, never summed with the 直接乘算 bonuses). The 干员调配 screen sets both per operator (by charId: initial, elite and 自选 forms share them; default 潜能 6 and 精英2 Lv.60); they travel as `room.loadout.ops` (`{ [charId]: { potential, cultivate } }`, defaults dropped; shared/protocol.js `checkLoadoutOps`, strict like the entries), `seats[].ops` → `PlayerState.ops` (re-checked; bots none = the defaults), and every operator the player owns states them in its `PlayerBattleInput` entry (`potential`, `cultivate`; shared/potential.js `cultivationOf`) — never a 补位 stand-in (a not-owned NORMAL chess) or a prototype 自选 pick. The sim composes the def at the potential (data/*.json carry every potential, DATA.md §2.3; the summons' talents at the owner's) and multiplies ATK / DEF / max HP by the tier's `char_attribute_mul` (effects.json `aceffect_char_1…4`, SIM.md §12). The cards (局内数值, shop and detail cards, a teammate's unit from UnitInfo `potential` / `cultivate`) show the numbers at the settings.
|
||||
|
||||
**Sim:** `simdata` resolves a unit def for `(chessId, skillIndex, moduleId)` (cached): `skill` = the selected SkillRecord; stats = `statsBase` + module attr (elite) ; trait/talents = base with the selected module's overrides. Kit contract (backward compatible): a kit function `(bb, chess) => { skill?, skills?: { [skillId]: SkillSpec }, talents, trait }` receives `chess.skill` = the **selected** skill and `bb` = its blackboard; the loader uses `skills[selectedSkillId]` if present, else `skill` only when the selected skill is the chess's default, else the **generic** skill spec — talents/trait from the kit always apply. Every visible chess must get hand-authored specs for all of its selectable skills (tracked by a coverage test).
|
||||
@@ -113,6 +115,10 @@ Official rule (bwiki 盟约, 更新公告 5114): before a match the player canno
|
||||
**UI:** a 干员调配 screen reachable from the lobby, room and briefing (INFO_CHECK): the roster is one list, a row per operator with aligned columns under a head row — portrait / name / bonds (opens the detail), the skills and the elite's modules to tap (不装备 first; the quick choices of PR #301 by @farridge, laid out as one list instead of its three columns of cards, the owner's call of 2026-10-08), the 潜能 / 练度 selects (`screens/loadout.js RosterRow`, `screens/cultivation.js`); the detail beside it (below 1000 px wide it slides over the list, a quick choice keeps the list) opens with the 潜能与练度 section (潜能 1–6, the four official 练度 tiers with their effect texts); the detail then shows the operator's 特质 — the in-match `GarrisonBlock` / RichText of the form its 普通 / 精锐 skill toggle shows, triggers such as 「<进入休整期时>」 kept (0.2.2, PR #301 by @farridge); filter by tier/bond/class, per chess choose skill (icon, name, SP info, desc at Lv4 and Lv7) and elite module (official type icon from the local-client art when extracted (§13), else a lettered tile; name/type, stat bonus, trait/talent changes), 恢复默认, persisted; a 局内数值 section between the skills and the modules (普通 / 精锐 toggle, 精锐 first) draws the detail card's own stats block, 特性 and 天赋 for the chosen skill / module (§22.16); the chosen skill/module shown on shop cards and in the detail panel (a teammate's unit — 前往查看 in prep, 联防 / 最终攻势 fields, an observed battle — shows **its owner's** skill/module from its UnitInfo `skillIndex` / `moduleId`, `gameLogic.js unitLoadout`) — which also shows the stats, 特性, talents and attack range the unit fights with (`shared/loadoutRecord.js` `loadoutRecord` / `attackRangeGrid`, the same composition the sim's `getChess(id, loadout)` uses; the deploy wheel's range preview too) — the range it is deployed with: a selected passive range skill (引星棘刺 S3 3-9) and the 特性's permanent 攻击距离 (信仰搅拌机 SPT-Y +1) included, over all 1284 loadouts equal to the battle's range at deployment; in battle and prep the card draws the live `range` (§21.16). In battle the renderer plays the equipped skill's own Spine clip and ON_SKILL_START sound (`UnitInfo.skillIndex`; `data/assets.json` lists icons, clips and sounds for every skill index).
|
||||
|
||||
**导出 / 导入 a loadout:** 导出 hands the current loadout out as a versioned envelope `{ kind: 'stronghold.loadout', v, exportedAt, count, entries, ops }` (`ops`: the 潜能 / 练度 settings, 0.2.2 — an import without them leaves the current settings alone) — copied through `ui/clipboard.js copyText` or downloaded as a file. 导入 accepts that envelope, the stored `{ v, entries }` form and a bare `{ [chessId]: { skill, module } }` map (serialised text too), and refuses another tool's `kind`, junk, an empty result and a **newer** `v` (`parseImport` — a newer payload is never mis-read). Both go through `loadoutSync.applyLoadoutEntries` → `sanitizeEntries`, so a preset from another build never sends the server an entry it would refuse, an import that keeps nothing changes nothing, and the result is reported as N imported / M not (`ui/loadoutModel.js`, `screens/loadout.js`).
|
||||
A payload that carries `ops`, including `ops: {}`, is refused before any write when chess.json or backups.json is missing (`loadoutOpsImportRefusal` in `screens/loadout.js`, the same check as a 自选编队 import). An older file with no `ops` still imports skills and modules once chess is ready and leaves the current 练度 in place.
|
||||
Export remains available for non-default `ops` alone, including operators outside the normal roster. The roster's
|
||||
adjusted count and reset scope stay limited to its own operators; the DIY export still carries picks only.
|
||||
|
||||
**干员持有 (0.2.0 补位):** the overlay's second tab marks operators as not owned (the 55 NORMAL chess; per-browser `sp.pref.ownership` = `{ v, notOwned }`, synced as `room.ownership { notOwned }` by the same sync engine as the loadout, `ui/loadoutSync.js installPrefSync`; 导出 / 导入 as `{ kind: 'stronghold.ownership', v, exportedAt, count, notOwned }`). A not-owned chess fights as its official stand-in with the backup skill / module — the loadout never applies to it ("对于补位干员其技能不可更改") — and is shown as the stand-in wherever the player's copy appears (shop / reward cards, hand and 临时整备区, board, detail card, result lineup, a teammate's scouting and bond popups: portrait, name, model, with a small 「替补」 mark; the owner's recall of the official mode, 2026-10-06), while its bonds, 特质, tier, price and merge stay the chess's; the 0.2.0 补位 design subsections have the rules and sources.
|
||||
**自选编队 (0.2.0):** the overlay's third tab fills the four DIY slots (5阶 ×2, 6阶 ×2) with an owned 6★ outside the chess pool (any of its three skills, any module of its elite form but a 集成战略 one) or a prototype (its locked skill / module); only operators with a kit are offered (`welcome.diyKitted`). Per-browser `sp.pref.diy` = `{ v, picks }`, synced as `room.diy { picks }` by the same sync engine; 导出 / 导入 as `{ kind: 'stronghold.diy', v, exportedAt, count, picks }` (`ui/diyModel.js`, `screens/diy.js`). In the match a slotted piece is the operator for its player (name, art, class, bonds from its factions, no 特质, the pick's skill and module; the slot's tier, price and merge), sold only in that player's shop from the slot's 调度中心 level with its own stock; the shop / reward / detail cards and the own pieces carry a 「自选」 badge (`ui/gameLogic/diy.js`); the 0.2.0 自选编队 design subsection has the rules and sources.
|
||||
|
||||
@@ -123,6 +129,10 @@ Official rule (bwiki 盟约, 更新公告 5114): before a match the player canno
|
||||
[ASSUMED] Bot purchasing applies one soft 18-point penalty to non-elite, non-DIY tier V/VI shared-pool cards whose
|
||||
active bonds overlap another living teammate's opening strategy. Human and AI teammates are treated equally; it
|
||||
neither reserves stock nor prevents a stronger purchase. Existing strategy presets and rehearsal counts are unchanged.
|
||||
A living teammate's selected strategy also soft-reserves its bonds for bot strategy selection and focus planning
|
||||
before any member is deployed (§30.28). The reservation ends when that teammate is eliminated. It does not hard-exclude
|
||||
a strategy, reserve stock, or stop an immediate merge. A reserved strategy keeps a quarter of its unreserved draw
|
||||
weight (`SOFT_BAND_RESERVE` = 0.25), so it can still be chosen. Mode-inactive bonds stay weight 0 (DESIGN §21.26).
|
||||
|
||||
[ASSUMED] AI reactions are cosmetic social choices, enabled by default and disabled with `SP_BOT_EMOTES=0`. Only
|
||||
mixed human/AI matches emit them, through the existing whitelist and cooldown. A human emote gets at most one eligible
|
||||
@@ -141,3 +151,9 @@ before mutation; data preparation failure leaves the old opening intact. This is
|
||||
mode rule. Unrelated result-screen changes from PR #408 are not included.
|
||||
|
||||
The shared DIY detail preview (stats, range, trait, talents, potential, cultivation and matching normal/elite skill descriptions) follows roster loadout resolution. Jessica shield placement is limited to her four orthogonal neighbors. The two Ptilopsis prep-gift copy exceptions are defined in §29.3 and META; no generic change to all copied conditions is implied.
|
||||
|
||||
Shared-stock corrections (§30.1–2): fixed `SERVER_GAIN_EQUIP` gifts still occupy ordinary/advanced 1/2 copies but
|
||||
may exceed capacity; `requirePool:false` bypasses equipment limits only for these fixed families. Already displayed
|
||||
operator shop and reward offers remain valid after exhaustion and record the deficit. Future rolls exclude empty
|
||||
stock; selling repays the deficit first. Item shop/reward claims and 整备 remain stock-limited. Rockrock and Flametail
|
||||
use the measured weighted subsets described in DATA and §30.2.
|
||||
+18
-2
@@ -40,6 +40,9 @@ S→C:
|
||||
interpolate linearly; nothing is inferred from the attack animation.
|
||||
- `b.ev` — `{ fieldId, ev: [ ... ] }` event tuples: `['spawn', UnitInfo]`, `['atk', srcId, tgtId, projKind]`, `['dmg', tgtId, amount, type]`, `['heal', tgtId, amount]`, `['skill', id, on:0|1]`, `['die', id, reason]` (reason `'killed'`, `'retreat'`, `'expired'` …; only `'killed'` plays an operator's knock-down sound), `['status', id, key, on]`, `['fx', kind, x, y, extra]` (`'hitCap' {id, n}` = a cancelled 限伤 hit, drawn as nothing, §20.12; `extra.form` = the unit's model form from then on (an enemy's mode, a 傀儡师's 替身 §22.11) — `shared/protocol.js fxForm`, state like a spawn: never dropped by the client, §21.4), `['layer', playerId, bondId, n]`, `['bounty', playerId, coins]`, `['deploy', id]`
|
||||
- `m.toast {kind, text}` · `m.ticker {text, id, type, priority, playerId}` (the strip plays the highest priority first, §21.10) · `m.emote {playerId, id}` · `m.result {...}`
|
||||
- `m.layer {playerId, bondId, add}` — a positive prep layer gain, sent only to that player in ROUND_START / SP_DRAFT /
|
||||
PREP for audio feedback. No replay on reconnect/resync and no settlement backfill; battle audio uses `layer` tuples
|
||||
from the displayed field. Older clients can ignore this notification without changing gameplay.
|
||||
- `m.unitStats {seq, round, units: [unitStatsEntry]}` — the answer to `g.unitStats` (`seq` echoed): every own board operator / summon by uid with `{id, uid, defId, hp, alive, maxHp, atk, def, res, interval, blockCnt, moveSpeed, range?, base: {…same}}` and `dir?` (shared/protocol.js `unitStatsEntry`; `range` = an ally's live range grid, facing RIGHT, never a target-selection grid, §21.16; `dir` = the unit's facing now, UP|RIGHT|DOWN|LEFT — the snapshot tuples carry none and UnitInfo `dir` is the facing at send time — which the detail card's range overlay rotates by, GitHub PR #281) as its next battle starts (§18.5)
|
||||
|
||||
`UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, skillIndex?, moduleId?, items?, form? }` (`dir` = the ally's deploy direction; the renderer picks the model (Back for UP, mirrored for LEFT) and the ground wedge from it; `skillIndex` = the ally's equipped skill (§16): the renderer plays that skill's Spine clip (`anims.skills[i]`) and the audio its own ON_SKILL_START sound; `moduleId` = an elite ally's equipped module (uniEquipId | 'none'); `items` = an ally operator's equipped item ids (absent without any; never on enemies or summons): the bond popup lists a 变形同构体 wearer from them and the detail card shows its 同构 chip and 装备 (§21.11); all three also on the prep-scouting `m.field` units (`Match.prepFieldMeta`, the held pieces included), so a teammate's unit shows its owner's loadout in the detail card and its equipped items as item pips on the unit (prep surfaces; user playtest #2 item 3); `form` = the unit's current model form (an enemy's mode §21.4, a 傀儡师's 替身 §22.11; a view built mid-battle starts in it)).
|
||||
@@ -137,6 +140,19 @@ codes are UI metadata only, never authentication. No new network message or nick
|
||||
|
||||
Optional `coins: [[id, balance, cap]]` is an integer skill-currency readout (§29.6), separate from ammo. It includes zero, is stepped between snapshots and clears when omitted. Result-validation names for board operators are resolved from the server's DIY/stand-in selections, never a supplied display name.
|
||||
|
||||
Preparation state carries read-only `prepMapChars` for 外勤医疗. Boss-mate and scouting metadata include them in
|
||||
displayed field coordinates. Draft cards expose optional `soldOut` when equipment stock is unavailable; the client
|
||||
Preparation state carries read-only `prepMapChars` for 外勤医疗. The owner's entries can be clicked for a detail card
|
||||
and board-space range preview without adding a placement intent or a combat entity. Boss-mate and scouting metadata
|
||||
include them in displayed field coordinates. Draft cards expose optional `soldOut` when equipment stock is unavailable; the client
|
||||
disables picking and the server checks again before consumption.
|
||||
|
||||
`m.public.bossPairs: [[playerId, playerId?], …] | null` publishes the preparation pairing, first member left and
|
||||
second right. It is drawn once per match and retained for Hidden Core; intact pairs survive departures, singles
|
||||
pair up. Clients use it for preparation camera/placement/team frames; only older servers lacking the field use
|
||||
the legacy seat-order fallback. Viewing or reconnecting never draws a new pair.
|
||||
|
||||
[ASSUMED] Touch's confirmed initial team grant persists for this match after its holder is eliminated or leaves.
|
||||
The maintainer explicitly approved retaining this policy on 2026-10-12; its official-game lifetime remains unverified.
|
||||
The preparation medic and future battle's medic/Touch use the same entitlement; the receiving player must still be
|
||||
alive. A mere draft focus grants nothing; another match starts without it. Only `band_amedic` is retained in
|
||||
`contentInfo.matchBands`; `teammates` and other team strategies still filter eliminated players. The official text
|
||||
and initial/unlimited-round data do not explicitly settle the holder-elimination case (§30.14).
|
||||
@@ -274,10 +274,3 @@ enemy: picking emitted a unit click with no definition, instead of the tile clic
|
||||
engine-canvas regression retains the card/100 HP assertions and also covers another field refresh, live HP loss and
|
||||
closing the card when the crate is destroyed. Device metadata supplied by the new entry still wins; changing fields
|
||||
does not carry old devices across. Gameplay and allied targeting rules are unchanged.
|
||||
|
||||
### 29.15 Ammunition readout
|
||||
|
||||
Active ammunition skills show remaining rounds and magazine capacity below the segmented bar (§10), including
|
||||
Laterano's extra rounds and refills. The renderer reads the existing exact snapshot counts rather than estimating
|
||||
them from SP. Missing ammunition snapshots, preparation and death hide the number. Dense magazines retain their
|
||||
numeric readout when the renderer omits cell separators. Combat rules and golden results are unchanged.
|
||||
@@ -0,0 +1,713 @@
|
||||
# DESIGN §30 — 0.2.5
|
||||
|
||||
Part of [DESIGN.md](../DESIGN.md). This chapter records 0.2.5, released on 2026-10-12.
|
||||
|
||||
## 30. Community follow-up
|
||||
|
||||
### 30.1 Fixed gifts and already displayed operator offers
|
||||
|
||||
Reports #488/#491/#495, PR #496 by @2321Robin, and the owner's Muelsyse/Yela report: the shared equipment
|
||||
cap added in 0.2.4 incorrectly prevented fixed acquisition gifts. [路标档案馆:路标月报#2](https://www.bilibili.com/video/BV1eLXXBqEgF/)
|
||||
states both that equipment from any source occupies stock and that these operators' fixed gifts are unlimited.
|
||||
The owner confirmed on 2026-10-11: gifts still occupy stock, but exhaustion must never prevent the gift.
|
||||
|
||||
`GameData.isUnlimitedGiftItem` derives the families from `SERVER_GAIN_EQUIP`. The actual gift passes
|
||||
`requirePool:false` through `effectsMeta` to `acquireItem`; only those families bypass availability. Ordinary and
|
||||
advanced copies still count as 1/2. This covers Carnelian, Beeswax, Muelsyse, Kjera and Swire, and the same fixed
|
||||
family awarded by the Damazti strategy. It does not make every strategy's equipment unlimited. Shop and item
|
||||
reward purchases, random grants and 整备 upgrades keep their availability checks. When ownership exceeds the cap,
|
||||
removing one copy need not immediately reopen stock. The proposed PR's zero-occupancy model was not adopted.
|
||||
|
||||
#498: the same video's approximately 2:50–2:57 example describes nine owned 溯光星源 plus one
|
||||
already displayed card in each of four shops, allowing thirteen owned copies. Shop operator purchases therefore
|
||||
do not recheck availability; `acquireChess` records the full deficit and later rolls still exclude exhausted stock.
|
||||
The owner explicitly extended this behavior to already displayed **operator reward** offers; that extension is a
|
||||
maintainer decision, not footage directly establishing every reward type. Item purchases/rewards remain bounded.
|
||||
Funds, full-hand checks, per-offer sold flags, merge accounting and selling returns retain their ordinary rules.
|
||||
|
||||
Tests: `test/match/community-round30.test.js`, `shared-stock.test.js`, `garrisons_meta.test.js`.
|
||||
Normative rules: META shared inventory and helper contract; design/match economy.
|
||||
|
||||
### 30.2 Measured acquisition subsets
|
||||
|
||||
PR #497 by @2321Robin, same original research video's rule table: Rockrock's subset weights are ordinary hammer
|
||||
10; tremble/undying/haste/burn hammer 2 each; limited accelerator 1; shield drone 4; laser emitter 4. Flametail's
|
||||
subset is Wild Mane 10, Ashlock 10, Fartooth 3. These are weighted subsets, still constrained by shared stock.
|
||||
The generator owns these tables; `data/choices.json` is regenerated offline. Tests cover the table and exhausted
|
||||
eligibility rather than asserting that subset draws ignore ownership.
|
||||
|
||||
### 30.3 Damage and target-selection corrections
|
||||
|
||||
- Clementia S3's erosion-burst listener now requires an enemy target. The generic targetability helper does not
|
||||
check allegiance, so a friendly erosion burst previously scheduled a shot. Enemy bursts retain the same behavior;
|
||||
the mode's existing boss erosion immunity is unchanged.
|
||||
- Horn follows [PRTS 号角](https://prts.wiki/w/号角): blocked targets first, ordinary ranged targets next, then a
|
||||
selectable ground target on her own tile. Her skills' automatic target check includes that tile. Other adjacent
|
||||
blind tiles do not become valid targets. Ordinary/S1/S3 melee attacks are single-target; S2 explicitly retains
|
||||
melee splash. Each shot captures its melee mode, so switching targets cannot remove splash from a shell in flight.
|
||||
- The bounty [自制投石机](https://prts.wiki/w/自制投石机) has normal splash radius 1.5 and three hits. The old
|
||||
radius 0.5 came from its 入戏 override, not its ordinary attack. Tests cover adjacent/diagonal allies and an outside
|
||||
target. No claim is made here about unrelated attack cadence.
|
||||
- PR #502 by @Xinglan233: Vulpisfoglia's first qualifying damage instance marks the enemy and also deals 追凶's arts
|
||||
addition. Repeated damage does not extend the original window. Source: [PRTS 忍冬](https://prts.wiki/w/忍冬).
|
||||
- #489: [PRTS 盟约记录](https://prts.wiki/w/卫戍协议:盟约_下半/PRTS盟约记录) corrects both Kjerag Ice and Tear Gas
|
||||
from “攻击时” to “造成伤害时”. Only these handlers move to actual carrier damage, including DoT, skill and splash
|
||||
instances; bare attack events, elemental gauge fill, HP loss, zero damage and friendly targets do not trigger them.
|
||||
Existing probabilities/durations remain data-driven. Regression uses real Ascalon poison while she is disarmed.
|
||||
|
||||
Tests: `community-round30.test.js`, `item-damage-procs.test.js`, `vulpis-first-hit.test.js` under test/content,
|
||||
plus existing item/operator/fortress suites. Normative rules: SIM and design/engine.
|
||||
|
||||
### 30.4 Client information and input
|
||||
|
||||
- PR #503 by @Xinglan233: a loadout containing only operator potential/cultivation changes is exportable.
|
||||
- PR #505 by @Xinglan233: spectator `ownerOps` includes resolved DIY selection, so bond members have correct identity.
|
||||
- #479 / PR #504 by @robll-v1, owner approved: before settlement, the HP tower shows the current HP and a separate
|
||||
pending-loss marker; settlement updates the number. This supersedes the earlier live pre-deduction display choice.
|
||||
- #501 / PR #507 by @robll-v1, owner approved: a refresh request in flight disables another button/R request until
|
||||
it settles, including errors. This is a client interaction guard, not server-wide request idempotency.
|
||||
- #484 / PR #494 by @YuriRestia, owner approved: bond tooltips list this match's banned visible members. Main text
|
||||
and list share one viewport-clamped, scrollable container; narrow screens stack the sections. Dynamic member
|
||||
changes update an open tooltip even if the displayed count stays equal. Long-press lists persist for reading and
|
||||
close on outside tap or Escape; ordinary tooltips keep their short behavior.
|
||||
- PR #490 by @D-Roote: Korean UI corrections; the obsolete 3D failure key from the proposal is excluded in favor of
|
||||
the current recovery message. PR #499 by @YuriRestia: English Victoria text explicitly applies its bonus per item,
|
||||
through a narrow correction layer in the translation generator, retaining all placeholders.
|
||||
|
||||
Tests: refresh unit/E2E, loadout export E2E, match-info, watch-bonds, round30-tools E2E and i18n checks.
|
||||
Normative rules: PLAYING, I18N and design/client.
|
||||
|
||||
### 30.5 Developer simulation checker
|
||||
|
||||
PR #492 by @Cloudnyco, owner approved: `/dev/sim-check.html` replays generated BattleSpecs with the real browser
|
||||
loader against Node traces. The corpus is the golden battle families, excluding full matches. The CLI generates
|
||||
local ignored reference data and can run browser engines or compare an exported state block.
|
||||
|
||||
Review repairs: reject unknown/empty selections and unfinished runs; recover controls after start/step/result/dump
|
||||
errors; preserve negative zero and other special floats across JSON; verify shared content identity for specs,
|
||||
reference, runtime and data; reject stale inputs; escape reference text; allow long mobile result lists to scroll.
|
||||
Reports state the first **detected interval**, not an exact first divergent frame. State dumps can diagnose only
|
||||
their recorded fields; snapshot/deep-only differences may not expose a differing field. The bounded unit-state
|
||||
traversal and sampled snapshots do not prove equality of all memory, events or rendered frames. See SIM §11.
|
||||
|
||||
### 30.6 Rechecked reports without a gameplay change
|
||||
|
||||
Real browser loading verifies seven different Yan operators plus ordinary/elite Muelsyse carrying the converter
|
||||
and Yan item: count 9, tier 3 and two actual 炎佑. All 36 normal/advanced conversion-item pairings retain battle
|
||||
membership. The preparation/combat conversion feedback was not reproduced in these cases; this does not exclude
|
||||
an unreported configuration. Normal/hidden Helm missile tests confirm the existing shell and its 3×3 stun.
|
||||
|
||||
Lost Colossus and Zaaro exist in the source catalog, but the fixed bounty draw groups remain based on the earlier
|
||||
22-match evidence. The owner chose to retain those groups until direct grouping evidence is found; catalog
|
||||
presence alone is insufficient to assign a group or probability. No AI formation change from #452, WebRTC #487
|
||||
or built-in HTTPS #478 is included.
|
||||
|
||||
### 30.7 One visual clip per operator attack
|
||||
|
||||
#506: a single attack formerly kept an operator's animation looping for 1.4 attack intervals, allowing a second
|
||||
visual swing without another simulated attack. The owner approved playing one clip per real attack, retaining
|
||||
operator rhythm scaling, enemy speed rules, skill selection and damage timing. A queued wind-up can restart during
|
||||
the preceding attack's recovery; its actual attack consumes that preparation rather than replaying the clip.
|
||||
After recovery the actor returns to its base/skill-idle clip. With no look-ahead, operators still start at the strike
|
||||
frame: completing a late wind-up would delay the visual hit, so that separate policy is deliberately unchanged.
|
||||
Tests cover single/continuous fast attacks, real Provence metadata, skill idle and attack interpolation.
|
||||
|
||||
### 30.8 Xi invincibility and session recovery
|
||||
|
||||
#510: [PRTS “自在”](https://prts.wiki/w/“自在”) specifies ten seconds of invincibility after its five-second rebirth;
|
||||
the linked status also excludes hostile selection. Xi opts into `reborn.invincibleUntargetable` for that same
|
||||
window. The general `invulnerable` flag and other enemies' rebirth policies are unchanged. Attacks and
|
||||
target-requiring automatic skills wait; presence-only `SKILL_RANGE` and SP-only skills retain their existing
|
||||
trigger rules. In-flight damage and elemental fill still deal zero during invincibility. Tests cover all four
|
||||
trigger types, projectile arrival and resumed attacks after expiry.
|
||||
|
||||
PR #511 by @RainoutOvO: the connection recovery banner now sits above the AI-away overlay, so “continue here”
|
||||
remains clickable after another page replaces the session. It remains below global modals. Tests use actual
|
||||
pointer hit-testing and clicks, including a global confirmation dialog over the recovery button.
|
||||
|
||||
### 30.9 Golden changes
|
||||
|
||||
Before the follow-up changes below (§30.11 onward), all 287 scenarios were regenerated, with no additions or deletions;
|
||||
45 differed from 0.2.4. This table records that initial candidate. Follow-up changes are recorded separately below.
|
||||
Each cause below was checked by an isolated counterfactual rerun: reverting the named change or combination
|
||||
restores the old digest exactly. Shared RNG and later economy/target choices can amplify a local correction.
|
||||
The two co-op paths involving Rockrock can mask a damage-item effect when each is reverted alone; their combined
|
||||
rollback, not mere operator presence, establishes the attribution.
|
||||
|
||||
| Cause / combination | Changed scenarios |
|
||||
|---|---|
|
||||
| catapult normal splash radius | `roster-013`, `standin-06` |
|
||||
| catapult normal splash radius; Kjerag Ice / Tear Gas damage triggers and resulting RNG order | `roster-040` |
|
||||
| Kjerag Ice / Tear Gas damage triggers and resulting RNG order | `bond-maniShip-high`, `boss-boss_2-pair`, `boss-boss_3-pair`, `boss-boss_5-pair`, `coop2-ABYSS-10-boosted`, `coop2-NORMAL-70-diy`, `hidden-boss_8-pair`, `hidden-boss_9-solo`, `roster-005`, `roster-012`, `roster-019`, `roster-033`, `roster-047`, `solo-ABYSS-1`, `solo-FUNNY-1`, `solo-FUNNY-2`, `solo-HARD-1`, `solo-NORMAL-1`, `solo-NORMAL-2`, `standin-07` |
|
||||
| Kjerag Ice / Tear Gas damage triggers and resulting RNG order; Fortress own-tile targeting / melee splash | `coop2-HARD-4` |
|
||||
| Kjerag Ice / Tear Gas damage triggers and resulting RNG order; Fortress own-tile targeting / melee splash; Rockrock acquisition-subset weights; Vulpisfoglia first-hit addition | `coop3-ABYSS-5`, `coop4-ABYSS-7` |
|
||||
| Kjerag Ice / Tear Gas damage triggers and resulting RNG order; Rockrock acquisition-subset weights | `coop2-NORMAL-8-serverrun`, `coop4-FUNNY-6` |
|
||||
| Kjerag Ice / Tear Gas damage triggers and resulting RNG order; Rockrock acquisition-subset weights; Vulpisfoglia first-hit addition | `coop2-NORMAL-1-coverage-standins`, `coop2-NORMAL-3` |
|
||||
| Kjerag Ice / Tear Gas damage triggers and resulting RNG order; Vulpisfoglia first-hit addition | `coop2-NORMAL-5-coverage-diy`, `roster-026` |
|
||||
| Fortress own-tile targeting / melee splash (including Horn and Ashlock) | `boss-boss_5-solo`, `roster-004`, `roster-023`, `roster-032`, `roster-034`, `roster-043`, `solo-HARD-9-boosted` |
|
||||
| Vulpisfoglia first-hit addition | `bond-soloShip-low`, `bond-yanShip-low`, `roster-007`, `roster-009`, `roster-039` |
|
||||
| Xi post-rebirth hostile selection immunity | `roster-011` |
|
||||
|
||||
The fixed-gift, displayed-offer overdraw, Flametail-subset and Clementia changes do not move these corpus
|
||||
digests individually; their dedicated regressions cover the reported cases. UI, translation and developer-tool
|
||||
changes do not alter simulation rules. The 140 DIY-only corpus cases retain their previous digests.
|
||||
|
||||
### 30.10 Bond-layer audio
|
||||
|
||||
#482: the client only sounded an increase in active bond count, missing actual layer gains in prep and battle.
|
||||
The official audio table maps `ON_ACT1AUTOCHESS_ADD_BOND` to `act1autochess_b_ui_buffup`, with
|
||||
`maxSoundAllowed=1` and `popOldest=true`. The supplied [match recording](https://www.bilibili.com/video/BV1VxwNzZEqK/)
|
||||
also contains the cue around Matterhorn acquisition and battle layer gains. The maintainer approved that bank's
|
||||
single-channel replacement policy with no extra cooldown; the recording alone does not settle every timing edge.
|
||||
|
||||
Own live prep gains now send an ephemeral `m.layer`; the displayed battle's existing `layer` tuples use the same
|
||||
audio channel. One multi-layer gain is one cue, inactive bonds included; capped/zero gains are silent. New cues stop
|
||||
the previous source and invalidate its pending decode. Field entry, exit and mute cancel pending/playing cues.
|
||||
Snapshots, resync and settlement bookkeeping do not synthesize gains. Existing activation audio shares the channel.
|
||||
The change does not affect layer amounts, simulation events or RNG. Tests cover server unicast/phase/cap behavior,
|
||||
asynchronous replacement and cancellation, muted/missing audio and late field entry.
|
||||
|
||||
### 30.11 Automatic skill-area follow-up
|
||||
|
||||
Reports described Kroos the Keen Glint and Degenbrecher waiting for enemies to reach a smaller range. With full SP,
|
||||
normal/elite forms and all four facings, Kroos S1/S2 cast at the outer normal range and Degenbrecher S2/S3 cast in
|
||||
their own skill range outside normal attack range. The supplied still image does not establish SP, cooldown or
|
||||
the running client version, so those reports are not marked fixed. The documented operation cooldown and Kroos's
|
||||
normal-attack timing still apply.
|
||||
|
||||
The broader real-record audit covered 1,850 manual-skill configurations and 2,232 directional trigger cases.
|
||||
It found three independent omissions under the previously approved expanded auto-cast policy:
|
||||
|
||||
- 司霆惊蛰 S2 has a terrain-derived range with no static rangeId. Its extra trigger now uses the same low-tile
|
||||
flood and permanent range extension as the running skill, only when it strictly contains the initial range.
|
||||
High ground, field bounds, normal attacks and its SEARCH rule remain unchanged.
|
||||
- 斩业星熊 S3 can attack flyers while active, but its trigger inherited the inactive ground-only profile. Its
|
||||
actual expanded area now accepts selectable flyers, like the existing 陈 S3 exception.
|
||||
- 予愿安洁莉娜 S3's opening area is 3-9 plus the surrounding x-4. Its ACTIVE_RANGE trigger now includes that same
|
||||
union, without extending it to a future flight destination or changing its anchored post-move attack area.
|
||||
|
||||
These changes preserve SP, control and operation cooldown gates and do not admit stealth/sleep/untargetable enemies
|
||||
to an ACTIVE_RANGE condition. Tests cover all DIY forms, facings, ground/air targets, permanent extension and gates.
|
||||
|
||||
### 30.12 Random boss partners, retained for Hidden Core
|
||||
|
||||
The maintainer requested random pairing each match and explicitly chose to retain partners for the hidden boss.
|
||||
A separate seeded shuffle assigns pairs and left/right sides before Final Assault preparation. The same plan
|
||||
drives prep previews, legal deployment maps and the actual battle. Hidden Core retains it; a departure keeps intact
|
||||
pairs and pairs up the remaining single players. A new Match starts a fresh draw; a repeated seed remains reproducible.
|
||||
|
||||
`m.public.bossPairs` supplies the actual plan to the client camera, placement and green partner frames; seat-order
|
||||
inference survives only as compatibility with older servers missing this field. The audit checks coverage, saved
|
||||
pair stability and actual fields instead of recomputing seat pairs. Lowest-seat connected-human authority and
|
||||
other-pair watch restrictions are unchanged. Regression includes 2/3/4 players, departures, hidden continuation,
|
||||
mirror coordinates, reconnects and real-browser prep-to-battle views.
|
||||
|
||||
### 30.13 联防 HP/SP restoration and Egir opening devour
|
||||
|
||||
The report was reproduced through normal BattleResult → `uniteBattleOpts` → a new battle: an Egir survivor's
|
||||
carried HP was charged for opening devour again. Some survivors died and revived at full HP, consuming a revival
|
||||
slot and resetting SP; others lost HP twice. Two 1% clamps also raised legal low HP, and a 1-HP floor changed the
|
||||
ratio when the new maximum was smaller.
|
||||
|
||||
The maintainer approved reconstructing Egir marks/base-ATK/block attributes without repeating opening devour HP
|
||||
loss on a carried survivor. Deployment and battle-start effects establish the new stats, then the living operator's
|
||||
exact positive HP ratio and total SP are restored. Forced-down carry still contributes to the attribute chain and
|
||||
stays down without spending a revival slot. Summons carry SP only; previous active skills are not resumed. Normal,
|
||||
boss and fresh no-carry units keep their usual devour damage, and later knock-outs/redeploys work normally.
|
||||
|
||||
[PRTS 帮助](https://prts.wiki/w/卫戍协议/帮助) specifies restoring HP ratio and SP after deployment. The detailed
|
||||
suppression of repeated opening death/revive effects is the approved implementation of that contract, not a claim
|
||||
that the wiki documents each internal callback. Tests include real carry construction, prior full-health food that
|
||||
would otherwise die anew, remaining revival slots, values below 1%/1 HP, and ordinary/boss behavior.
|
||||
|
||||
### 30.14 Touch's team medic after its holder is eliminated
|
||||
|
||||
On 2026-10-12 the maintainer explicitly approved retaining the implementation, including both elimination and
|
||||
voluntary departure after a confirmed selection. This resolves the local policy choice, not the official-evidence gap.
|
||||
|
||||
The observed loss happens in newly constructed fields and the next preparation, not by removing an already spawned
|
||||
medic from a teammate's ongoing battle. Both prep and battle eligibility formerly searched only living holders.
|
||||
|
||||
[ASSUMED] A confirmed Touch selection grants the team medic for this match even after that player is eliminated
|
||||
or leaves. Official data marks the effect initial and unlimited in rounds and describes a medic on every player's
|
||||
field, but does not explicitly document the holder's elimination. This local candidate adopts the persistent grant
|
||||
under the maintainer's request to make evidence-based local choices pending review. A draft focus is insufficient;
|
||||
a confirmed choice followed by departure counts. New matches have no inherited grant. Recipients must be alive.
|
||||
|
||||
Only Touch is retained in `contentInfo.matchBands`; ordinary teammate contexts and other eliminated strategies
|
||||
stay excluded. Prep still displays the reserve medic, with the battle replacing it by Touch at the existing elite
|
||||
threshold. Tests cover LP elimination, departure, both medic variants, draft confirmation, fresh matches and the
|
||||
normal/unite/boss/hidden BattleSpec paths.
|
||||
|
||||
### 30.15 Other reports rechecked without changing their rules
|
||||
|
||||
- 联防 order: [PRTS 帮助](https://prts.wiki/w/卫戍协议/帮助) includes downed operators in deployed headcount. The
|
||||
implementation reads deployed count, not deployment capacity. Nine deployed with casualties may rank ahead of
|
||||
eight survivors; the active-bond and layer tie-breaks retain their existing documented uncertainty.
|
||||
- 乌尔比安 S3: [PRTS 乌尔比安](https://prts.wiki/w/乌尔比安) gives the next tile beyond an occupied anchor landing
|
||||
as a fallback before his own tile. The pictured report alone does not contradict this; his existing own-board,
|
||||
occupied/downed reservation and return rules remain unchanged.
|
||||
- 叙拉古: fixed-layer cases last exactly `32 + 0.4L` game seconds, which is half as many real seconds at 2×. The
|
||||
formula tooltip reads current layers while an already deployed buff retains its initial duration/ASPD; a new
|
||||
deployment reads the new layers. No direct source settled whether official in-battle gains should extend the old
|
||||
buff, so that separate uncertainty is retained; durations are not doubled globally.
|
||||
- 塑心: hidden-layer bonds can still receive layers under the documented PRTS rule (§25.21.6). The old exclusion
|
||||
was specific to 余's strategy (§27.21), not a global ban. Selecting the highest hidden layer count is retained.
|
||||
- Push/pull: 176 force/mass cases and real Degenbrecher boss cases did not reproduce a weight bypass. Mode leaders
|
||||
retain their existing displacement guard; an ordinary `rank=BOSS` enemy is not automatically the same as a
|
||||
protected mode leader. Weight, static-body and immunity checks remain. 余 S2 can teleport 铳 in one step under its
|
||||
separate teleport rules; this is a possible source of the reported movement, not an identification of the screenshot.
|
||||
Boss trigger-edge tests distinguish a large rendered model from its actual hit area; the still image alone does
|
||||
not establish a missed auto-cast.
|
||||
|
||||
### 30.16 Follow-up golden changes
|
||||
|
||||
All 287 scenarios were recomputed after §30.11–30.14. Only five match digests change from the preceding candidate:
|
||||
`coop2-NORMAL-3`, `coop4-FUNNY-6`, `coop2-ABYSS-10-boosted`, `coop2-NORMAL-70-diy`, and
|
||||
`coop2-NORMAL-5-coverage-diy`. In each, restoring only the old ordered boss pairing in an isolated process reproduces
|
||||
the preceding digest exactly; the changed partner/side also changes preparation choices and later combat totals.
|
||||
All five already differed from 0.2.4, so the final release-relative total remains 45 scenarios (§30.9 plus these
|
||||
additional pairing effects). There are no new corpus cases or removals. The other follow-up fixes do not move this
|
||||
corpus; their dedicated regressions cover the reported and newly found cases.
|
||||
|
||||
### 30.17 Loadout sync preparation
|
||||
|
||||
PR #540 by @Xinglan233. Restoring defaults while a loadout sync was still waiting on backups.json could put the
|
||||
previous skill back on the server. `prepare()` assigned its request number only after the await, so the older
|
||||
payload was treated as the newer one. A stale missing-data result or a failed download could also replace a sync
|
||||
the reset had already finished.
|
||||
|
||||
`installPrefSync` now keeps a preparation revision. An edit of a watched field, including restoring defaults, and
|
||||
`welcome` increment it before the new flush is scheduled. A preparation that started at an older revision does not
|
||||
send and does not change sync status when it finishes, whether it built a payload, found the data missing, or
|
||||
failed. Debounce, the close flush, request ordering and the RATE / lock retry stay as they are. The same engine
|
||||
covers 干员持有 and 自选编队. Golden unchanged. Tests: `test/ui/loadout-sync-stale.test.js`.
|
||||
|
||||
### 30.18 Loadout import with ops while backups are missing
|
||||
|
||||
PR #538 by @Xinglan233. Importing a loadout file that includes `ops` checked only that chess.json was loaded.
|
||||
`cultivationCharIds` then dropped 自选 operators, which only backups.json names, and the import still saved. That
|
||||
cleared a 练度 the file was not meant to erase. `ops: {}` is the same replacement.
|
||||
|
||||
After a successful parse, `loadoutOpsImportRefusal` refuses the whole import with the 自选编队 data check
|
||||
(`importRefusal('diy')`) before any write. A file with no `ops` still imports skills and modules once chess.json is
|
||||
ready and leaves the current 练度 in place. Golden unchanged. Tests: `test/ui/loadout-ops-import.test.js` (node, no
|
||||
browser).
|
||||
|
||||
### 30.19 Player package scripts and doc links
|
||||
|
||||
GitHub #531 and #530. The player zip copied the repository `package.json`. `npm test` is `node --test`, and `test/`
|
||||
is not in the zip, so it reported 0 tests and exited 0. Nine other scripts name files or tools the zip does not
|
||||
contain (`golden`, `golden:update`, `build-data`, `check:imports`, `ci`, `package`, `package:lite`, `lint`,
|
||||
`typecheck`). README, CHANGELOG, NOTICE, THIRD-PARTY-NOTICES, PLAYING and DEPLOY also keep relative links to
|
||||
developer docs, pictures and `../../releases/latest`. Twenty-three of those, in README, PLAYING and DEPLOY, point
|
||||
at developer files.
|
||||
|
||||
The stage rewrites `package.json` only. A script stays when every `node <file>` it names is in the zip and it names
|
||||
at least one (`start`, `dev`, `setup`, `doctor`, `launch`, `postinstall`, `vendor`, `assets`). The others are
|
||||
removed, so `npm test` is a missing script and exits non-zero. The repository `package.json` is unchanged. The same
|
||||
stage rewrites those six Markdown files: a relative link whose target ships stays relative; a picture becomes
|
||||
`https://github.com/sganggs/Stronghold-Protocol/raw/v<tag>/<path>`; another file becomes `.../blob/v<tag>/<path>`;
|
||||
`../../releases/latest` becomes `.../releases/latest`. The tag is `v` plus the package version with a trailing
|
||||
`-dev` removed. [ASSUMED] an unreleased `0.2.5-dev` build points those links at `v0.2.5`, the tag that version will
|
||||
use. Source copies in the git checkout stay relative. Golden unchanged. Tests: `test/package-player.test.js`,
|
||||
`test/package.test.js`, `test/docs-paths.test.js`.
|
||||
|
||||
### 30.20 Official damage numbers and the title-page repository link
|
||||
|
||||
Issues #412 and #543. #412's font swap, element-colour pass and bold crit type are not part of this change.
|
||||
The new setting 「官方伤害数字」 (`officialDamageNumbers`) defaults off. While it is off, every rounded hit and
|
||||
heal still pops the existing typed number (merge, colours, 90-number ceiling, sparks and slashes unchanged).
|
||||
Turning 「显示伤害数字」 off still hides every digit, including official ones. The value is sanitised with the
|
||||
other settings (a missing or non-boolean saved value stays off) and copied into the live field view.
|
||||
|
||||
The official rule is PRTS [作战机制](https://prts.wiki/w/作战机制) 「伤害红字(暴击)」 (fetched 2026-10-12): show a
|
||||
red integer of the final damage when 最终伤害 ≥ 期望伤害 × 1.5, or when a skill forces red. Expected damage follows
|
||||
the normal formula except that DEF/RES penetration, post-mitigation bonuses (temporary attack-scale increases,
|
||||
damage-scale increases such as 脆弱, amount rewrites, barrier absorption), final-damage resets and special HP
|
||||
mechanisms are not counted. Invulnerable targets multiply expected damage by 0, and 0 ≥ 0 × 1.5 still shows red 0.
|
||||
A dodge of a hit that would have been red shows yellow MISS (`#ff9000`) instead. After 卫戍协议:盟约 the official
|
||||
client no longer caps red numbers at 10; this client does not restore that cap. The existing 90-number ceiling
|
||||
stays as a renderer limit [ASSUMED: 「无限制」 means the old cap of 10 is not brought back].
|
||||
|
||||
In this sim the comparison runs inside `dealDamage` and does not change HP, shields, hooks or RNG:
|
||||
|
||||
- Expected damage is mitigation of `dmg.amount` with penetration forced to 0, times `dmg.mul` only.
|
||||
- The compared total is the pre-shield amount after source and target multipliers. The integer on the existing
|
||||
`dmg` tuple — what both modes display — is what remains after shields and an `hpDamage` rewrite.
|
||||
- A qualifying tuple that already fires (`amount >= 0.5`, not silent) gains a fifth element `'r'`. Other tuples
|
||||
stay four elements. Element gauge fills are not marked. Heals are not marked, so official mode hides them.
|
||||
- A dodge that already fires sets `num: 'm'` on its fx payload only when the same test says the landed hit would
|
||||
be red. That test does not roll RNG again and does not refresh a dirty attacker's stats (a dirty `source.s`
|
||||
read rescales HP).
|
||||
|
||||
[ASSUMED] gaps, left unmarked rather than guessed:
|
||||
|
||||
- `dmg.amount` already includes the attack's stated ATK × 攻击倍率 (`atkScaleMul` is baked in by callers). It is
|
||||
not divided back out. `dmg.mul` stays on both sides of the comparison.
|
||||
- Cached-ATK expected damage (raw ATK, ignoring the attack scale and the target's DEF/RES), forced red, blue
|
||||
numbers and yellow BLOCK are not represented on DamageInfo. The three wiki examples are not hard-coded.
|
||||
- An invulnerable or sleeping hit, and a qualifying total below 0.5, still emit no `dmg` event, so official mode
|
||||
cannot show that red 0. Emitting one would increment `events.dmg` and could play an impact. The renderer can
|
||||
draw a red 0 when a tuple `['dmg', id, 0, type, 'r']` is supplied. Liftoff and a cancelled or hit-capped attack
|
||||
are not red 0.
|
||||
- PRTS does not say that ordinary hits are silent. That is the requested setting: official mode shows only the
|
||||
marked red integer and the yellow MISS. The sentence in the issue comment is not a PRTS sentence.
|
||||
- The random position is about ±0.35 tiles across and 0.15–0.55 tiles above the head. The simplified board keeps
|
||||
its fixed slot and only changes the colour / the MISS label.
|
||||
- Sparks and the melee slash stay in both modes.
|
||||
|
||||
#543: the title footer carries a text link labelled 「项目仓库」 to
|
||||
`https://github.com/sganggs/Stronghold-Protocol`, `target="_blank"` and `rel="noopener noreferrer"`. It does not
|
||||
replace 设置 → 问题反馈.
|
||||
|
||||
Tests: `test/sim/official-damage-numbers.test.js`, `test/ui/official-damage-numbers.test.js`,
|
||||
`test/render/official-numbers.test.js`. Golden digests do not move: no new event kind is emitted, and the extra
|
||||
tuple field is not part of the digest. Player notes: PLAYING; client notes: design/client.md.
|
||||
|
||||
### 30.21 Official battlefield scenes (#532)
|
||||
|
||||
Report: the web board draws the metal theme for every battlefield. The reporter asked for the sand theme.
|
||||
PR #533 by @andrew05060414.
|
||||
|
||||
Findings (2026-10-11; the official hot-update lists and asset indexes, read from the CDN, and the scene bundles of
|
||||
version `26-04-13-12-31-42_e9b560`):
|
||||
|
||||
- The official board is not assembled at run time. Every battlefield is a prebuilt scene bundle, shipped only while
|
||||
the season runs. DESIGN §15 said otherwise from an off-season list and is corrected there.
|
||||
- Which art a battlefield uses is the dependency list of its scene bundle: the sand theme for act1 m01–m07 and act2
|
||||
m02 / m04, `map_lakegarden` + `map_syracuse_metro` for act2 m01, `map_kxmr` + `map_lm_center` for act2 m03. The
|
||||
same in the 上半 version of 2025-11 and the 下半 versions of 2026-03 / 04. No battlefield uses the metal theme.
|
||||
- The sand theme bundle holds no mesh because the models are baked into each scene ("Combined Mesh (root: scene)").
|
||||
- A scene's three sub-graphics `HIDDEN@1…3` are the left normal field, the right normal field and the boss field.
|
||||
- `python3 tools/local-extract/fetch_scenes.py --list` against that Android hot-update list names 33 bundles and
|
||||
22,801,308 bytes. That is the compressed CDN download, not the extracted size in the full zip.
|
||||
|
||||
Change: `tools/local-extract/scenes.py` exports a battlefield's scene (meshes in board space, materials, key light,
|
||||
lightmap pages with the baked shadow mask in alpha, albedo textures) to `public/assets/local/scene/<stageId>/`;
|
||||
`extract.py --only scene` runs it, reading the scene bundles from the client, `--scenes <dir>` or
|
||||
`.cache/official-scenes/*`; `tools/local-extract/fetch_scenes.py` downloads the in-season bundles from the official
|
||||
CDN (md5-checked against that version's hot-update list). The 3D board draws a stage's scene when the local-art
|
||||
manifest lists it (`board3d/load.js loadScenePack`, `scene.js setScenePack`); the procedural metal board stays the
|
||||
fallback and still draws the devices, gates, field edges and terrain overlays. Chunks of the parts on screen that
|
||||
share a material and a lightmap page are one mesh (`mergeSceneChunks`), so a battlefield such as act2 m02 (36 chunks,
|
||||
5 lightmap pages) stays inside the board's draw-call budget. A switched-off HIDDEN part is left out of that mesh.
|
||||
|
||||
Owner decision (2026-10-12): the eleven scenes belong to the 0.2.5 asset pack. The maintainer's full release zip
|
||||
includes the extracted files. A lite package or a source install may download and extract them on that machine
|
||||
(`node tools/setup.mjs --scenes`; `--no-scenes` skips). Missing Python 3.8+ or UnityPy / lz4 / Pillow is a warning,
|
||||
and the metal board remains. The repository never stores the bundles or the extracted files (`.gitignore` already
|
||||
ignores `public/assets/`, `data/local-assets.json` and `.cache/`, and names `.cache/official-scenes/`). Packaging
|
||||
does not fail when the scenes are absent; the summary line `battlefield scenes` says whether this machine has them.
|
||||
`scenes.py` and `fetch_scenes.py` ship in a player zip only after they are committed, because the packager reads
|
||||
`git ls-files`.
|
||||
|
||||
No gameplay change: the scene is art under the same grid, camera and unit layer. Golden results are not moved by
|
||||
this change.
|
||||
|
||||
**[ASSUMED]** the light gains `SCENE_LIGHT` (`range` 8.0, `ambient` 0.2, `direct` 0.8, `indirect` 0.4) and the RGBM
|
||||
reading of the lightmap pages (Unity linear decode, range 5), both in DESIGN §15. Not done: the scene's normal /
|
||||
metallic / emission maps, map effects, sky box and post-processing; the tile graphics a level switches at run time
|
||||
(mire, tide) still use the remake's overlays. The extracted byte size inside a full zip was not measured here.
|
||||
|
||||
Tests: `test/render/board3d-scene.test.js` (the scene table against data/stages.json, board-space conversion, chunk
|
||||
layout shared by scenes.py and load.js, sub-graphic groups, RGBM decode, the loader with a fake store, BoardScene
|
||||
with a stub renderer: scene on / areas / fallback). `test/scene-assets.test.js` (setup's scene step, the full plan
|
||||
ships extracted scene files, the lite plan ships none, git ignores the bundles).
|
||||
|
||||
### 30.22 Prepared 外勤医疗 target order and preparation inspection
|
||||
|
||||
PR #523 by @YinqiMargatroid, continuing #451. PR #451 made 预备干员-医疗 visible in preparation and left its initial
|
||||
aggro order unchanged. The owner decided on 2026-10-12 to take both halves of this report as the author describes them.
|
||||
The retained team grant in §30.14 is unchanged: a confirmed Touch selection still supplies the medic after the holder
|
||||
is eliminated or leaves.
|
||||
|
||||
In the recording [卫戍协议](https://b23.tv/RQLI8WS), the right-side 联防 board at 19:13 has Touch and 哈洛德 inside the
|
||||
same flying 守墓石像's attack range when it locks its target, and the attack selects 哈洛德. The 17:01 preparation
|
||||
frame shows 哈洛德 with no equipment, and his position makes him the right player's first ordinary deployment under
|
||||
the current column scan. The 19:36 clip supplies another targeting comparison. [PRTS 卫戍协议/帮助](https://prts.wiki/w/卫戍协议/帮助)
|
||||
gives the column order and places operators before summons; [PRTS 作战机制](https://prts.wiki/w/作战机制) gives 索敌 as
|
||||
blocker, then special priority, then 仇恨, then earliest appearance. These observations support an effective priority
|
||||
earlier than ordinary operators for the prepared 外勤医疗 slot. They do not expose the official unit identity or the
|
||||
exact deployment counter.
|
||||
|
||||
The battle sim still creates the selected map-character variant at `battleStart`, after the board deployment.
|
||||
[ASSUMED] The prepared slot keeps an earlier effective targeting order whether it stays 预备干员-医疗 or is replaced
|
||||
by Touch. Only `aggroSeq` receives a negative pre-initial value (`i - chars.length` across every prepared character
|
||||
on the field); `deploySeq` remains the actual spawn's identity for deployment hooks and scheduled effects. `0` is not
|
||||
used, because `aggroCmp` treats a falsy `aggroSeq` as `deploySeq`. [ASSUMED] When several players have prepared slots,
|
||||
their relative order follows the existing player iteration, the same way other initial placements do. A map character
|
||||
spawned ad hoc after battle start, without `preBattleAggroSeq`, keeps ordinary summon order. Redeploy and device
|
||||
spawn still assign `aggroSeq` from the new `deploySeq`. The flying 守墓石像 tests check both variants at target-lock
|
||||
time, a right-side 联防 Touch ahead of the left medic and behind no ordinary operator, and the higher-taunt exception.
|
||||
|
||||
The further recording [准备阶段](https://b23.tv/4JXAp12) at 0:17 shows 预备干员-医疗 selected, with its detail card and
|
||||
attack range visible before combat. This shows an inspection affordance. It does not establish that the official game
|
||||
already simulates the medic as a combat entity during preparation. The client lets the existing read-only preparation
|
||||
view open its operator-style card and board-space range by tap, including on the mirrored boss half and in the flat
|
||||
fallback renderer. The original board-space record is kept for the card; only the rendered view is transformed. The
|
||||
preview stays immovable and is not sold or retreated. The battle creation point is unchanged. The same click path
|
||||
accepts another map-character variant if one is included in `prepMapChars`.
|
||||
|
||||
Normative lines: SIM.md §1 and §2 (`aggroSeq`, `spawnToken` `preBattleAggroSeq`), design/engine.md §5.5, design/client.md
|
||||
Interactions, design/network.md preparation state. Tests: `test/content/bands.test.js`, `test/content/tokens_devices.test.js`,
|
||||
`test/ui/inspect-range.test.js`, `test/ui/playtest6_summons.test.js`, `test/render/browser.test.js` (needs `RENDER_E2E=1`).
|
||||
|
||||
`npm run golden:update` recomputed all 287 scenarios against the preceding 0.2.5 candidate. No scenario was added or
|
||||
removed. Only `coop2-NORMAL-14-standins` moved. Its human seat holds `band_amedic`, so the team grant places the
|
||||
prepared map character on both fields; the earlier `aggroSeq` changes who those enemies strike, and the later bond
|
||||
and combat totals follow. The match stays a round-14 victory with team LP 31. The partner (`band_chiave`) first
|
||||
differs at round 11: activated layers 149 → 161, with 拉特兰 5 → 11 and 精准 24 → 30 in that settle row. Round 12
|
||||
continues the same layer gap (191 → 206). The human settle row first differs at round 13: kills 21 → 22, leaks
|
||||
36 → 35. Final human totals: kills 272 → 273, leaks 44 → 43, damage dealt 2,564,165 → 2,565,610, boss damage
|
||||
193,091 → 189,876. Partner boss damage 586,909 → 590,124, damage dealt 3,838,927 → 3,843,408, activated layers
|
||||
267 → 282; final bonds move 奥术 27 → 30, 拉特兰 17 → 23 and 精准 53 → 59. This scenario was not among the 45 that
|
||||
already differed from 0.2.4 (§30.9, §30.16), so the release-relative total is now 46. The other 286 digests match
|
||||
the preceding candidate.
|
||||
|
||||
### 30.23 Silenced 枯朽 death fog, and 自选 in the promotion reward
|
||||
|
||||
Two owner decisions of 2026-10-12. Neither is a new pool or a change to any other silence rule.
|
||||
|
||||
**#470.** `enemy_1267_nhpbr` (萨卡兹枯朽战士) and `enemy_1267_nhpbr_2` (萨卡兹枯朽战士组长) share one death ability,
|
||||
PollutedDie 【污染秽蚀】 (`kitPolluted`). A death while `flags.silence` is set creates no zone. The owner's official-play
|
||||
recall is that killing one which already shows the silence icon leaves no fog. That recall contradicts the PRTS /
|
||||
in-game text 「不可沉默」. Both handbook lines are `format: "NORMAL"` ("被击倒时释放污染秽蚀,使周围我方持续损失生命"),
|
||||
not `SILENCE`. 0.2.0 already withheld the fog (§25.18.2), from the general 死亡爆炸 line "如无特殊说明,此能力默认可沉默"
|
||||
and a client-template reading of `projectile_on_killed`. That section stays as the 0.2.0 reading. The reason recorded
|
||||
now is the owner's recall, against 「不可沉默」. The local gamedata cache does not contain the string 「不可沉默」; the
|
||||
contradiction is the issue title and the owner's statement.
|
||||
|
||||
The same ability is only those two keys. Checked and left unchanged: 萨卡兹枯朽战车 / 尖端 (`enemy_1272_nhtank`, `_2`)
|
||||
秽蚀轰击 is an attack skill with `_ignoreSilence` and `silenceImmune`; 枯朽之种 (`enemy_1269_nhfly`) self-destructs and
|
||||
is silence-immune; 枯朽萃聚使徒 (`enemy_1321_wdarft`) summons seeds and is silence-immune; 萨卡兹枯朽前锋 / 辟路前锋
|
||||
(`enemy_1422_lrsldr`, `_2`) deal arts while in a 源石污染区; 假想敌:蚀裂 (`enemy_9006_actoxi`) has a different death
|
||||
cloud that was already silenceable; 高能源石虫's 死亡爆炸 is a different ability. A silence that has expired does not
|
||||
stop the fog. A leak (`reason !== 'killed'`) still creates none. A killing blow that would apply silence only if the
|
||||
target is still alive is not this case: the report is the icon already showing.
|
||||
|
||||
**#515.** The promotion reward (three different operators, one tier above the 调度中心, at most 6, price 0) includes
|
||||
自选 operators of the newly reached tier, the way the ordinary shop includes them once the 调度中心 is at that tier.
|
||||
`pushRewardOffer` with no explicit `ids` now passes `diyStockEntries()` into `pool.roll`. The tier filter is unchanged,
|
||||
including the downward walk when a tier has no further distinct chess (playtest #6 item 19). Shop level 4 rolls reward
|
||||
tier 5, so a stocked tier-5 自选 piece can appear and a tier-6 piece cannot. Shop level 5 rolls reward tier 6, so a
|
||||
stocked tier-6 piece can appear; a tier-5 piece appears only if that roll walks down. Shop level 6 still can roll tier
|
||||
6. Shop level 3 rolls tier 4 and has no tier-4 自选 piece. The ordinary shop (`economy.js`, `diyRollEntries`) still
|
||||
waits for `shopLevel` 5 / 6. Effect offers that pass explicit `ids` do not roll. A 自选 piece is not guaranteed. A slot
|
||||
whose every bond is banned still has no stock. `left <= 0` stays out. Copy counts 8 / 5 stay [ASSUMED] and unchanged.
|
||||
|
||||
Seen: 卫戍协议/帮助 干员的获得与精锐化 describes a temporary refresh of three operators one tier above the 调度中心 and
|
||||
does not say that 自选 are excluded. The next section's note "仅在调度中心等级 ≥ 干员所在等阶时,该干员才有可能出现在栏位中"
|
||||
is the persistent shop: binding it to the refresh would leave a level-4 调度中心 with no tier-5 operator.
|
||||
`shopLevelDisplayDataDict` lists the tier-5 DIY slots at level 5 and the tier-6 slots at level 6; those rows are visible
|
||||
shop chess, and no second pool excludes them. No parsed page names 自选 inside the refresh. The inclusion is the
|
||||
owner's decision of 2026-10-12, not a new [ASSUMED]. 0.2.0 §25 had [ASSUMED] that the reward used the slot's shop
|
||||
level; that sentence stays there, and this section reverses it.
|
||||
|
||||
Normative lines: `server/match/player/acquire.js` `pushRewardOffer`, `server/match/player/diy.js`, META.md §3,
|
||||
DATA.md `rewardOffer` and §18, design/match.md economy, PLAYING.md 自选编队, SIM.md enemy damage zones,
|
||||
`server/sim/content/enemies/archetypes.js` `kitPolluted`. Tests: `test/match/diy-shop.test.js` (the reward tallies;
|
||||
the level-4 ordinary shop still rolls no tier-5 自选 piece), `test/content/enemies_bosses.test.js` (both keys, silenced
|
||||
and after the silence runs out).
|
||||
|
||||
`npm run golden:update` recomputed all 287 scenarios. No scenario was added or removed, and no digest moved.
|
||||
#470 does not change the sim: the fog was already withheld. #515 can change a promotion reward only when the seat
|
||||
has stocked 自选 of the reward tier and the 调度中心 is still below that slot's shop level. The two DIY matches start
|
||||
at shop level 5. `coop2-NORMAL-5-coverage-diy` rolls its only reward after the shop is already 6, where both iterators
|
||||
include the tier-6 stock, so the offer is the same. `coop2-NORMAL-70-diy` rolls four tier-6 rewards while the shop is
|
||||
still 5. On this seed the card the seat takes is the same operator as before. Rounds 8–10 differ only in an untaken
|
||||
card (round 8 swaps the other two; rounds 9 and 10 replace the third). The other level-5 offer is unchanged. Both
|
||||
level-6 offers match the previous roll, including the one that shows the tier-6 自选 piece, which the shop gate already
|
||||
allowed. The digest records
|
||||
shop draws, fielded pieces, board hashes and the rng state, not an untaken reward card, so the stored scenario does
|
||||
not move. The release-relative total against 0.2.4 stays 46.
|
||||
|
||||
### 30.24 Funnel normal attacks keep a body hit
|
||||
|
||||
PR #537 by @Xinglan233. [PRTS 分支特性信息 驭械术师](https://prts.wiki/w/分支特性信息/data) says a drone that has not
|
||||
been released by a skill deals additional damage on top of the operator's attack: 20 % ATK at first, +15 on the same
|
||||
target, cap 110 %. A skill-released drone deals skill damage.
|
||||
|
||||
The profession used to return that ramp as the whole normal-attack multiplier, so the body hit was missing.
|
||||
`resolveHit` now keeps a 100 % ATK body instance and separate drone instances. `funnelMul` holds only the drone ramp;
|
||||
an attack's `dmgMul` (至简's talent) is applied once to both. Extra attached drones raise `hits`, not the body count.
|
||||
至简's charged S2 sets `funnelBodyHits`, so two body hits and two additions share one ramp step. Both share one
|
||||
`attackId` and one attack hook; only the body is `isAttack`. Additions carry `addition` and `funnel` and are not also
|
||||
emitted as a normal attack.
|
||||
|
||||
[ASSUMED] Body instances precede additions at the existing projectile impact, and a multi-hit volley takes one ramp
|
||||
step. Ordinary attacks and any skill that only buffs or retargets the normal attack keep the body. `info.isSkill` is
|
||||
not the switch: every attack or targeting override sets it. `funnelBodyHits === 0` means the attack is drone-only
|
||||
(the ramp stays inside those hits and no body is added). 荒芜拉普兰德 S2 sets that. 至简 S2 sets `funnelBodyHits`
|
||||
to two, so two body hits and two additions still share one ramp step.
|
||||
|
||||
耶拉 S2, base and hidden, follows the 洛洛 S2 split. `skchr_kjera_2` (docs/research/03-operators.md) says 自身与浮游单元
|
||||
攻击, and the lock lasts until the enemy dies or the skill ends. While it runs, `funnelMul` is detached, so her own
|
||||
shot is one 100 % `isAttack` hit inside her range and the unreleased addition is off. `1 + attack@cnt` drones tick
|
||||
on their own (`kjera:drone`, arts, `isSkill`, not `isAttack`) and may stay outside her range. Cold is rolled once
|
||||
on the body and once on each drone hit. Ending the skill restores the multiplier.
|
||||
|
||||
[ASSUMED] Those released drones use the trait ramp (20 % → +15 % on the same target → the cap). Her blackboard has
|
||||
no drone scale; the branch page gives that ramp to drones that were not released. The base form continues its
|
||||
per-target ramp, and two drones on one enemy in the same tick take one step. That map is written back when the skill
|
||||
ends. The hidden form restarts each drone at the initial scale, and those scales are not written back onto the single
|
||||
trait ramp. There is no flight delay. A lock that can no longer be targeted is dropped and the drone retargets. Stun
|
||||
and disarm do not pause the released drones. An in-flight shot from before the skill can still step the live ramp;
|
||||
the skill keeps its own copy, and a shot that lands after the skill ends can step the restored ramp.
|
||||
|
||||
[ASSUMED] 荒芜拉普兰德 S1 says 释放浮游单元 and does not say she stops attacking. The toggle is read as a retarget of
|
||||
the unreleased attack: one body hit plus the two additions, at the raised ATK. A 洛洛-style release is not used.
|
||||
澄闪's skills and 荒芜拉普兰德 S3 stay on their own `dealDamage` paths. 澄闪 S3 and the skill-drone simulator on
|
||||
淬羽 are unchanged.
|
||||
|
||||
Kits that stored the ramp on `dmgMul` (至简, both 耶拉 forms, 澄闪 S2) now store it on `funnelMul`.
|
||||
|
||||
Tests: `test/sim/funnel-body.test.js`, and the funnel cases in `kits_t3`, `kits_t4`, `kits_t6`, `kits_alt_t6`,
|
||||
`kits_alt_t2` and `op_gdglow`. The golden moves from this correction, measured against the candidate after §30.29,
|
||||
are §30.30.
|
||||
|
||||
### 30.25 Redeploy resets the drone ramp
|
||||
|
||||
PR #536 by @Xinglan233. The same branch page says that changing target, or redeploying the operator, restores the
|
||||
drone damage to its initial value at once.
|
||||
|
||||
`installFunnel` resets `funnelTarget`, `funnelScale` and the per-target `funnelMap` (耶拉 / 至简) on a normal
|
||||
deployment, including automatic revival against the same living target. A free move (`moveRedeploy`, the same carry
|
||||
as a 秘术师's stored energy and 乌尔比安 S3) keeps the ramp. An in-flight drone still applies the ramp it computes on
|
||||
impact, so a shot launched before the reset can step the new ramp. That limit is documented and not changed.
|
||||
|
||||
Tests: `test/sim/funnel-redeploy.test.js`, `test/match/funnel-redeploy.test.js`.
|
||||
|
||||
### 30.26 洛洛 S2 keeps its lock outside her range
|
||||
|
||||
PR #535 by @Xinglan233. The official skill table `skchr_rockr_2`, as recorded in `docs/research/03-operators.md`,
|
||||
says the released drone stays locked until the enemy is killed or the skill ends, then returns. Leaving her attack
|
||||
range is not a return condition. The branch page says a skill-released drone deals skill damage, and that additional
|
||||
damage belongs to drones that were not released.
|
||||
|
||||
S2 now runs one independent skill tick (`rockr:drone`, one drone at both ranks). While the skill is up the trait
|
||||
multiplier is detached, so new body shots are ordinary attacks and the unreleased addition is off. The drone's hits
|
||||
are arts skill damage (`isSkill`, not `isAttack`). It inherits the trait ramp on its own copy, so an older projectile
|
||||
cannot change that ramp. A living selectable lock stays valid outside the body's range. Ending the skill writes the
|
||||
ramp back and restores the multiplier and the overload cap. S1 and other operators' funnel attacks keep their
|
||||
existing paths. 立于磐石 is unchanged.
|
||||
|
||||
[ASSUMED] There is no flight delay. A lock that becomes untargetable is dropped and the drone retargets. Body control
|
||||
(stun and the like) does not pause the released drone.
|
||||
|
||||
Tests: `test/content/rockrock-s2-lock.test.js`, and the 洛洛 case in `kits_t1t2`.
|
||||
|
||||
### 30.27 Hit tiles use half-even rounding
|
||||
|
||||
PR #520 by @Cloudnyco. PRTS 作战机制 (oldid 433935, 「碰撞体积与位置识别」) says the struck unit's coordinates are
|
||||
rounded half to even (四舍六入五成双) onto the tile they occupy. `Math.round` sends an exact .5 toward +∞, so an enemy
|
||||
held on an outer half-tile was indexed off the board and dropped from the hit index.
|
||||
|
||||
`roundHalfEven` is used for hit tiles only: `body.js` `posKey`, the point cases of `bodyOnTile` and `bodyTileReach`,
|
||||
the point-enemy index in `battle/queries.js`, and `targeting.js` `tileKeyOf`. Movement, knockback, `ai.js`, a unit's
|
||||
standing tile for range keys, and `onFrontLine` still use `Math.round`. #519's signed-zero case in `powi` was already
|
||||
on this candidate and is left as it is.
|
||||
|
||||
[ASSUMED] `FIELD_EDGE_INSET` = 1/1048576 pulls a clamped enemy just inside the outer half-tile, so half-even rounding
|
||||
keeps it on the edge tile. An inset of 1/1024 was rejected because it moved unbalance steps. The inset has no
|
||||
official source.
|
||||
|
||||
Tests: `test/sim/hit-tile-round.test.js`, `test/sim/detmath.test.js`, the edge landing in
|
||||
`feedback5-raid-own-board`, the 连星 probe in `op_wang`, and the 麦哲伦 probe in `feedback5-wv-summon-triggers`.
|
||||
|
||||
### 30.28 Bots soft-reserve a living teammate's opening bonds
|
||||
|
||||
PR #518 by @IceCodeNew, for issue #514. This is not an official rule. It extends the accepted #407 card penalty.
|
||||
|
||||
Opening-band choice and focus planning used to ignore a living teammate's selected strategy until someone from it was
|
||||
deployed, so a bot could take that faction's high cards. A living teammate's strategy now soft-reserves its bonds for
|
||||
both choices, before any member is deployed. The reservation ends when that teammate is eliminated (`alivePlayers`).
|
||||
It does not hard-exclude a strategy, reserve stock, or stop an immediate merge. A reserved band's draw weight is a
|
||||
quarter of its unreserved weight (`SOFT_BAND_RESERVE` = 0.25) and stays above zero, so the strategy can still be
|
||||
drawn. Focus planning applies the existing −6 teammate penalty to those bonds as well. Mode-inactive bonds stay
|
||||
weight 0 (DESIGN §21.26). A solo match has no teammate to reserve against. The shared-pool −18 card penalty is
|
||||
unchanged, and a stronger merge can still win it.
|
||||
|
||||
[ASSUMED] The quarter weight is a project choice. The PR text called the reservation soft; its diff hard-zeroed the
|
||||
weight. This candidate follows the text. The reservation is equal for human and AI teammates.
|
||||
|
||||
Tests: the two `#514` cases in `test/match/bot-smarter.test.js`. One proves the focus leaves the reserved bond while
|
||||
the teammate lives and returns after elimination. The other proves a reserved strategy is still drawn, and drawn more
|
||||
often once that teammate is eliminated. The existing card test still shows a merge outscoring the −18 penalty.
|
||||
|
||||
### 30.29 Golden changes after the funnel, hit-tile and bot-reservation ports
|
||||
|
||||
All 287 scenarios were recomputed once after §30.24–§30.28 and compared with this candidate before those ports
|
||||
(`HEAD` `7fd0478d`, `git show HEAD:test/golden/*.json`). No scenario was added or removed. 43 digests differ from
|
||||
that candidate. They are additional to §30.9 and §30.16.
|
||||
|
||||
Against released 0.2.4 the total is now 64. Of the previous 45, 21 are byte-for-byte the digests already on this
|
||||
candidate and are not repeated below: `roster-011`, `roster-012`, `roster-013`, `roster-032`, `roster-033`,
|
||||
`roster-034`, `roster-039`, `roster-040`, `roster-043`, `bond-maniShip-high`, `bond-yanShip-low`,
|
||||
`boss-boss_2-pair`, `boss-boss_5-solo`, `hidden-boss_8-pair`, `hidden-boss_9-solo`, `solo-NORMAL-2`,
|
||||
`coop2-NORMAL-3`, `solo-HARD-9-boosted`, `coop2-ABYSS-10-boosted`, `standin-06`, `standin-07`. The other 24 of
|
||||
those 45 moved again, and 19 scenarios that still matched 0.2.4 on this candidate now differ.
|
||||
|
||||
Opening strategy ids do not change in any of the 20 match digests. A solo match has no teammate, so §30.28 cannot
|
||||
move one. Co-op seeds keep the same `bandId` and the same bot-draw counts except `coop4-ABYSS-7`. The recorded
|
||||
match moves are battle totals from §30.24–§30.27, and the later gold and LP those totals change. No isolated
|
||||
counterfactual was re-run; the groups below follow who is actually on the board.
|
||||
|
||||
| Cause | Changed scenarios |
|
||||
|---|---|
|
||||
| Funnel body hit plus redeploy reset, and 洛洛 S2's independent lock (§30.24–§30.26). These boards include 洛洛 | `roster-004`, `roster-005`, `roster-023`, `roster-024`, `bond-victoriaShip-low`, `bond-victoriaShip-high` |
|
||||
| Funnel body hit plus redeploy reset (§30.24–§30.25). 至简 and/or 耶拉, not 洛洛 | `roster-007`, `roster-009`, `roster-026`, `roster-028`, `bond-deputShip-low`, `bond-emptyShip-low`, `bond-kjeragShip-high`, `bond-maniShip-low`, `bond-miraShip-low`, `bond-sargonShip-high`, `bond-soloShip-low`, `bond-visiShip-low` |
|
||||
| Same body hit and redeploy reset on 荒芜拉普兰德 | `roster-017`, `roster-019`, `roster-036`, `roster-046`, `roster-047`, `boss-boss_3-pair`, `boss-boss_5-pair`, `boss-boss_7-pair`, `hidden-boss_9-pair` |
|
||||
| 澄闪 DIY: the normal attack is a body hit plus one drone addition (§30.24) | `diy-098`, `diy-099` |
|
||||
| Hit-tile half-even rounding and the edge inset (§30.27). No funnel operator is deployed; the damage total stays 1388100 and the hook counts and the snapshot sample at index 27 move | `standin-04` |
|
||||
| Full matches. Opening bands are unchanged. Final lineups include a funnel operator, except `coop2-HARD-4` (both seats eliminated, lineup empty) and `coop3-ABYSS-5` (the surviving lineup has none), whose first divergence is round 7 and round 5; those two are the hit-tile change, or a funnel operator who is not left in the stored lineup | `solo-FUNNY-1`, `solo-FUNNY-2`, `solo-NORMAL-1`, `solo-HARD-1`, `solo-ABYSS-1`, `coop2-HARD-4`, `coop2-NORMAL-1-coverage-standins`, `coop2-NORMAL-5-coverage-diy`, `coop2-NORMAL-8-serverrun`, `coop2-NORMAL-70-diy`, `coop3-ABYSS-5`, `coop4-FUNNY-6`, `coop4-ABYSS-7` |
|
||||
|
||||
Result flips inside that match list: `solo-NORMAL-1` defeat → victory (team LP 0 → 2); `solo-FUNNY-1` team LP 15 → 23
|
||||
and `solo-FUNNY-2` team LP 8 → 23, both still victories, with every settled round row unchanged; `coop2-NORMAL-8-serverrun`
|
||||
team LP 26 → 27; `coop4-ABYSS-7` eliminated at round 12 → defeat at round 13, and its shop and bot draw counts change.
|
||||
The other matches keep the same end reason. `standin-04` is the only battle in this set with no funnel operator on
|
||||
the board.
|
||||
|
||||
### 30.30 Golden changes after the skill-only funnel body correction
|
||||
|
||||
All 287 scenarios were recomputed after the §30.24 correction (a skill that only buffs or retargets keeps the body;
|
||||
耶拉 S2 uses the 洛洛 split) and compared with this candidate after §30.29 (`HEAD` `e0f37718`,
|
||||
`git show HEAD:test/golden/*.json`). No scenario was added or removed. `fields`, `standins` and `diy` are unchanged
|
||||
(澄闪 stays on `dealDamage`). 19 digests differ. They are additional to §30.9, §30.16 and §30.29. Seventeen of them
|
||||
already moved in §30.29 and move again. `roster-038` and `coop2-NORMAL-14-standins` did not move in §30.29.
|
||||
|
||||
Opening strategy ids do not change in any of the 20 match digests. No isolated counterfactual was re-run. The roster
|
||||
and bond rows name the skill index the corpus assigns: 耶拉 index 1 is 心随意动, 荒芜拉普兰德 index 0 is 慵怠者悲鸣.
|
||||
至简 is on two of those roster boards at index 1; her S2 already kept two body hits, and her totals move because the
|
||||
fight around 耶拉 changed.
|
||||
|
||||
| Cause | Changed scenarios |
|
||||
|---|---|
|
||||
| 耶拉 S2, base form, skill index 1. `roster-007` also fields 至简 S2 | `roster-007`, `roster-009` |
|
||||
| 耶拉 S2, skill index 1. No 荒芜拉普兰德 on the board | `bond-emptyShip-low`, `bond-kjeragShip-high`, `bond-miraShip-low`, `bond-soloShip-low` |
|
||||
| 荒芜拉普兰德 S1, skill index 0. The toggle keeps one body hit and both unreleased additions | `roster-036`, `roster-038` |
|
||||
|
||||
Battle-total flips in that table: `roster-007` killed 132 → 137, leaks 41 → 37, coins 4 → 6, deaths 11 → 10, damage
|
||||
234357 → 243212 (耶拉's own damage 9204 → 17443; 至简 8936 → 9558). `roster-009` keeps the same kills and leaks;
|
||||
耶拉's damage 2 → 3 and 至简's kills 28 → 29, damage 7594 → 7168. `roster-036` killed 75 → 76, leaks 55 → 54,
|
||||
damage 1160465 → 1189341 (荒芜拉普兰德 60604 → 99946, kills 2 → 3). `roster-038` damage 3467641 → 3468742
|
||||
(荒芜拉普兰德 kills 4 → 14, damage 106163 → 187571). The four bonds keep the same kills and leaks. `bond-emptyShip-low`
|
||||
healing 381 → 362; 耶拉's damage 6585 → 11450 (kills 1 → 3), `bond-kjeragShip-high` 6927 → 16312,
|
||||
`bond-miraShip-low` 10165 → 18617 (kills 2 → 4), `bond-soloShip-low` 1963 → 2764.
|
||||
|
||||
Match end and the final lineup. A bot's chosen skill is not stored. Where the final lineup still contains 耶拉, her
|
||||
default skill is S2, which is the skill this correction changes.
|
||||
|
||||
| Final lineup | Changed scenarios and the recorded end |
|
||||
|---|---|
|
||||
| 耶拉, lineup unchanged | `solo-FUNNY-1` (`3_20_b`): victory, team LP 23 → 27; only round 8's bond layers move (steadShip 34 → 35). `solo-FUNNY-2` (`3_20_a`): victory, team LP 23 → 29; all eight stored round rows are identical, so the LP change is the boss round. `solo-NORMAL-1` (`3_20_b`): victory, team LP 2 → 11; first divergence round 10 (kjeragShip 13 → 14). `solo-HARD-1` (`3_20_a` and 至简 `3_13_a`): still a defeat, team LP 0, boss pool 215042 → 212054; first divergence round 8, the board hash. `coop2-NORMAL-8-serverrun` (ai_1 `3_20_a`): victory, team LP 27 → 37 (ai_0 LP 10 → 19, ai_1 LP 17 → 18); first divergence round 8, ai_1 killed 22 → 23, leaks 1 → 0. `coop2-NORMAL-5-coverage-diy` (ai_0 `3_20_a`): victory, team LP 33 → 34 (h_0 LP 27 → 28); first divergence round 13, ai_0's board hash, killed 26 → 21, leaks 17 → 22 |
|
||||
| 耶拉 was on the pre-change lineup; that seat is then eliminated and the stored lineup is empty | `solo-ABYSS-1`: defeat at round 13 → eliminated at round 12, team LP 0 → null. The pre-change lineup had 耶拉 `3_20_a` and 至简 `3_13_a`. First divergence round 8: LP 27 → 18, killed 19 → 10, leaks 0 → 9. `coop4-ABYSS-7`: still a defeat at round 13, team LP 0, boss pool 4821366 → 3417504. ai_1 is eliminated at round 13; the pre-change lineup had 至简 `3_13_a` and 耶拉 `3_20_a`. First divergence round 8: ai_1 LP 19 → 20, killed 2 → 3, leaks 7 → 6. ai_0's lineup also changes and has no funnel chess id before or after |
|
||||
| 耶拉 plus other funnel operators, lineup unchanged, same victory and the same team LP 130 | `coop4-FUNNY-6`: ai_0 耶拉 `3_20_a`, ai_1 荒芜拉普兰德 `6_18_a`, ai_3 洛洛 `2_10_a`. First divergence round 11, ai_0's kjeragShip layers 26 → 23 |
|
||||
| No 耶拉 on the final lineup. The diverging round's pieces and skill choice are not stored | `coop2-NORMAL-1-coverage-standins`: still a defeat, team LP 0, boss pool 25040 unchanged. ai_0's final lineup includes 荒芜拉普兰德 `6_18_a`. First divergence round 7, the other seat's miraShip layer 5 → 6. `coop2-NORMAL-14-standins`: victory, team LP 31 → 40 (ai_0 LP 13 → 22). Neither final lineup contains a funnel chess id. First divergence round 10 bond layers (raidShip 26 → 28, steadShip 16 → 18); that round's board hash is unchanged |
|
||||
|
||||
### 30.31 Ammunition readout
|
||||
|
||||
Active ammunition skills show remaining rounds and magazine capacity below the segmented bar (§10), including
|
||||
Laterano's extra rounds and refills. The renderer reads the existing exact snapshot counts rather than estimating
|
||||
them from SP. Missing ammunition snapshots, preparation and death hide the number. Dense magazines retain their
|
||||
numeric readout when the renderer omits cell separators. Combat rules and golden results are unchanged.
|
||||
Reference in new issue
Block a user