diff --git a/.gitignore b/.gitignore index 4de6ca16..6721a54d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,11 @@ node_modules/ public/vendor/ # --- game assets: (c) Hypergryph / Yostar, fetched or extracted locally, NEVER committed ---------- -# tools/fetch-assets.mjs → public/assets/** (~250 MB) and public/fonts/** (+ fonts.css) +# tools/fetch-assets.mjs → public/assets/** (~270 MB) and public/fonts/** (+ fonts.css) public/assets/ public/fonts/ # tools/local-extract/extract.py → public/assets/local/** and this per-machine manifest -# (optional: the client and the tests work without it) +# (optional: the client and the tests work without it; docs/DEPLOY.md §6 lists what falls back) data/local-assets.json # download caches, upstream tables, ledgers, reports, setup state .cache/ diff --git a/README.md b/README.md index d23c3018..abf36675 100644 --- a/README.md +++ b/README.md @@ -80,13 +80,13 @@ English summary: [below](#english). git clone https://github.com/sganggs/Stronghold-Protocol.git cd Stronghold-Protocol npm install # 安装依赖(postinstall 会把 pixi / preact / three 复制到 public/vendor) -npm run setup # 检查环境,并从公开镜像下载约 250 MB 美术 / 音频(可中断,再次运行会续传) +npm run setup # 检查环境,并从公开镜像下载约 270 MB 美术 / 音频(可中断,再次运行会续传) npm start # 启动服务器:http://localhost:3000 ``` 也可以直接运行启动脚本(Windows `scripts\start-windows.bat`,macOS / Linux `scripts/start.sh`):首次会自动安装依赖、下载素材,然后启动服务器并打开浏览器。 -- **官方 3D 棋盘**需要从本机的《明日方舟》PC 客户端提取贴图(Windows 原生客户端、macOS 的 CrossOver 或 PlayCover)。`npm run setup` 检测到客户端时会询问是否提取(需要 Python 3.8+,依赖装在项目内的 `.venv-extract`,不影响系统);之后可以用 `node tools/setup.mjs --local` 重新提取,或用 `--game "<…/StreamingAssets/AB/Windows>"` 指定路径。没有客户端时自动使用 2D 棋盘,其他功能不受影响。 +- **本地客户端素材(可选)**:官方 3D 棋盘、部分官方界面图标(交流按钮与表情面板的边框、模组类型图标等)和灼热 / 炽焰源石虫的官方模型需要从本机的《明日方舟》PC 客户端提取(Windows 原生客户端、macOS 的 CrossOver 或 PlayCover)。`npm run setup` 检测到客户端时会询问是否提取(需要 Python 3.8+,依赖装在项目内的 `.venv-extract`,不影响系统);之后可以用 `node tools/setup.mjs --local` 重新提取,或用 `--game "<…/StreamingAssets/AB/Windows>"` 指定路径。没有客户端时游戏照常运行,这几样换成替代样式:2D 棋盘、样式相近的图标、染色的普通源石虫。表情和「玩法说明」的教程图随上面的素材一起从公开镜像下载,不需要客户端。没有客户端的服务器(例如 Linux VPS)也可以从**同一版本**的整合包里复制 `public/assets/local/` 和 `data/local-assets.json`,见 [docs/DEPLOY.md](docs/DEPLOY.md) 的「本地客户端素材」。 - 素材下载优先使用 GitHub,失败时自动改用 jsDelivr 镜像。 - `npm run doctor`(即 `node tools/doctor.mjs`)可以随时诊断:Node 版本、素材是否完整、端口占用、局域网地址和防火墙。 @@ -232,7 +232,7 @@ RENDER_E2E=1 node --test 'test/render/*.browser.test.js' # 渲染测试,部 An **unofficial, non-commercial fan remake** of Arknights' seasonal auto-chess tower-defense mode *Stronghold Protocol: Alliance*, played in the browser: solo, or 1–4 player co-op (AI teammates can fill seats). Combat is simulated in each player's browser, so a low-power PC can host. -- **Run:** download the all-in-one bundle from [Releases](../../releases/latest), install Node.js 22 or 24, then double-click `scripts\start-windows.bat` (Windows) or run `./scripts/start.sh` (macOS / Linux) and open . From source: `npm install && npm run setup && npm start` (setup downloads ~250 MB of art from public mirrors; the official 3D board needs a local Arknights client to extract, otherwise the 2D board is used). +- **Run:** download the all-in-one bundle from [Releases](../../releases/latest), install Node.js 22 or 24, then double-click `scripts\start-windows.bat` (Windows) or run `./scripts/start.sh` (macOS / Linux) and open . From source: `npm install && npm run setup && npm start` (setup downloads ~270 MB of art from public mirrors, the emotes and the how-to-play pages included; the official 3D board, some official HUD icons and two enemy models are extracted from a local Arknights client — without one the game uses the 2D board and look-alike stand-ins, and a server can copy `public/assets/local/` and `data/local-assets.json` from the release bundle of the same version). - **Play with friends:** create a co-op room and share the 4-letter key or the `?room=KEY` link. On a LAN, use the address printed at start; otherwise use a virtual-LAN tool, a tunnel or a VPS — see [docs/DEPLOY.md](docs/DEPLOY.md). - **Disclaimer:** not affiliated with or endorsed by Hypergryph or Yostar. All Arknights names, art, audio, text and data are © their respective owners and are **not** covered by this project's GPL licence. For study and personal non-commercial use only — no selling, paid distribution, paid servers or monetisation of any kind. Content will be removed on request of the rights holders. Provided "as is", without warranty. - **License:** code GPL-3.0-or-later ([LICENSE](LICENSE)); game assets excluded. diff --git a/docs/ASSETS.md b/docs/ASSETS.md index ff1380b1..4e3d03c8 100644 --- a/docs/ASSETS.md +++ b/docs/ASSETS.md @@ -48,7 +48,7 @@ How downloads are fetched: - A manifest entry with fallbacks (for example an enemy icon that falls back to its base enemy's icon) only moves on to the next alternative after a **definitive 404**. When the primary fails transiently (network error, 5xx or an invalid payload after all retries), no fallback is fetched. The path is listed under `downloadErrors` in the report, and the next run retries the primary. - A skeleton that fails to parse is deleted and removed from the ledger, so the next online run downloads it again. -The first run downloads about **242 MiB in about 3,700 files**. It took 134 s on a ~3 MB/s link. A re-run takes about 1 s. +The first run downloads about **269 MiB in about 4,000 files** (it took 134 s on a ~3 MB/s link before the 55 emote and 玩法说明 files, 21.3 MiB, were added). A re-run takes about 1 s. Outputs: - `data/assets.json`: the manifest (committed). @@ -79,6 +79,7 @@ The research JSONs in `docs/research/` (03, 05, 07) define **which** ids are nee | Band (strategy) icons | AA2 `…/arts/bandicon/` | `band/{bandId}.png` | | Profession, sub-profession and battle-card icons | AA2 `arts/profession_hub`, `arts/ui/subprofessionicon`, `ui_battle_new/battlecard` | `prof/icon_{p}.png`, `prof/large_{p}.png`, `prof/battlecard_{p}.png`, `prof/sub/{subProfessionId}.png` | | UI sprites (see below) | AA2 `ui/autochess/**`, `arts/**`, `activity/[uc]act2autochess/**`, `battle/[pack]common/sprites` | `ui/{group}/{key}.png` | +| The 36 battle emotes and the 19 玩法说明 (tutorial) pages, which `tools/local-extract` also extracts (GitHub issue #42: without the local client a server showed default emote icons) | AA2 `cn` `ui/emoticon/theme/[uc]{themeId}/icon/{picId}.png` (`shared/constants.js EMOTE_CATALOG`) and `arts/guidebookpages/[pack]autochess/{key}.png` (1024², shown at 16:9 like the local copies) | `ui/emoticon/{dir}/{picId}.png`, `ui/guide/{key}.png` | | Operator battle Spine (Front, Back) | fexli/ArknightsResource `spine/{id}/{id}/{Front,Back}/` | `spine/op/{charId}/{front,back}/{stem}.{skel,atlas,png}` | | Token Spine | fexli: the default model, or else the first skin variant (`spine/{tokenId}/{variant}/Spine/`) | `spine/token/{tokenId}/{stem}.*` | | Enemy Spine (PC build, premultiplied alpha) | isHarryh/Ark-Models `models_enemies/{key}/`, file names from `models_data.json` | `spine/enemy/{enemyId}/{stem}.*` | @@ -107,7 +108,7 @@ The `stem` of a Spine model is the upstream file name. Two examples: `char_107_l - **UI:** - every group from `07-assets.json → autochessUi`: rarity, elite and chess-level sprites, the shop panel and cards, HUD, bond board, equip slot, round dialog, band choose, settlement, prepare backdrop; - `arts` (rarity stars, elite icons, the camp logos of pool nations, the loading illustrations used by the act2 modes, battle common sprites, act2 entry backdrops and season logo, item rarity frames); - - extras: mode choice art, battle-ready backdrops, battle UI (speed, pause, HP slider, attack range, boss avatar frame, skill ready), `empty_skill`, the 机变 panel and cards, the equip-replace dialog, the bond detail dialog, the prep-ready panel, stage-info titles. + - extras (`tools/assets/plan.mjs UI_EXTRAS`): mode choice art, battle-ready backdrops, battle UI (speed, pause, HP slider, attack range, boss avatar frame, skill ready), `empty_skill`, the 机变 panel and cards, the equip-replace dialog, the bond detail dialog, the prep-ready panel, stage-info titles, the 36 battle emotes (`emoticon/{dir}/{picId}`) and the 19 玩法说明 pages (`guide/{key}`) — these two keyed by the group and name of `data/local-assets.json`. ## Post-processing @@ -147,7 +148,9 @@ All paths are URL paths relative to the site root, for example `/assets/char/ava bands: { [bandId]: url }, skills: { [iconId]: url }, // iconId = skill_table iconId ?? skillId skillsById: { [skillId]: iconId }, - ui: { ['group/key']: url }, // e.g. 'hudPanel/icon_hp', 'shopCard/frame_lv1', 'loading/loading_ac_core' + ui: { ['group/key']: url }, // e.g. 'hudPanel/icon_hp', 'shopCard/frame_lv1', 'loading/loading_ac_core'; + // 'emoticon/basic/pic_happy_battle', 'guide/autochess_home_1': the data/local-assets.json + // group + name of the same picture (the client takes the local one first) prof: { icon: {caster…warrior}, large: {…}, battlecard: {…, token}, sub: {[subProfessionId]: url} }, audio: { bgm: { lobby, prep, combat, boss: { intro?, loop } }, // intro then crossfade to loop (1 s) @@ -268,6 +271,7 @@ Other renderer rules from research 07 §5.4–5.5: ### Other fallbacks +- **Emotes and 玩法说明 pages** (`public/js/data.js artUrls / nextArtUrl`): the local-client picture (`data/local-assets.json`) first, then the mirror copy (`ui['emoticon/…']`, `ui['guide/…']`), each tried in turn when one fails to load; with neither, the neutral emote glyph and the official tips text (`config.tips`). The rest of the local-client art (the 3D board, most official HUD sprites, module type icons, the two enemy models above) has no web copy: docs/DEPLOY.md §6 lists what falls back without it. - **Tokens:** - Without an avatar, use `chars[owner].avatar` with a 召唤物 badge, or `prof.battlecard.token`. - Without a Spine, draw the avatar sprite with a bob tween. diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 22109193..8efe4742 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -10,7 +10,7 @@ | 服务器 CPU | 战斗在各玩家浏览器里模拟(DESIGN §14),服务器只负责回合、经济和校验:**每个房间每个作战回合约 1 ms CPU**。AI 队友 / 掉线玩家的战场由服务器模拟:作战开始时 3 个 AI 战场在开发机上约 0.2–0.5 s CPU,小主机上可能要几秒(分成 8 ms 小片执行,不会卡住其他房间)。`SP_VERIFY=all` 会复算每个真人战场,CPU 明显增加,小主机建议保持 `off` 或 `sample`。 | | 服务器内存 | 空闲约 100 MB,每个进行中的对局再增加几 MB。 | | 网络 | 4 人对局中服务器每回合下行约 0.25 MB(DESIGN §14 实测)。首次进入游戏时浏览器要从主机下载所需的图片 / Spine 模型 / 音频(按需加载,之后走浏览器缓存),公网隧道带宽小时第一次会慢一些。 | -| 磁盘 | 素材约 250 MB(`public/assets`)+ 依赖约 125 MB(`node_modules`);可选的本地提取约 40 MB(`.venv-extract`)+ 70 MB 贴图。 | +| 磁盘 | 素材约 270 MB(`public/assets`)+ 依赖约 125 MB(`node_modules`);可选的本地提取约 40 MB(`.venv-extract`)+ 70 MB 贴图(见第 6 节)。 | | 玩家设备 | 支持 WebGL 的现代浏览器(Chrome / Edge / Firefox / Safari 最新版),电脑或手机平板(横屏)。老旧设备可在设置里调低画质或访问 `/?board=2d`。 | 服务器**无状态**:房间和对局只存在内存里,没有数据库和存档,**不需要备份**。重启服务器会结束正在进行的对局(包括断线后本可在 24 小时内回来继续的独立模拟)。 @@ -31,7 +31,7 @@ ```powershell git clone https://github.com/sganggs/Stronghold-Protocol.git C:\Stronghold-Protocol ``` -3. 双击 `C:\Stronghold-Protocol\scripts\start-windows.bat`。首次会:安装依赖(`npm ci`;完整包已含,跳过)→ 复制前端库 → 下载约 250 MB 素材(完整包已含,跳过;显示进度,中断后再次启动会续传)→ 若检测到本机的明日方舟客户端,询问是否提取官方贴图(可跳过)→ 启动服务器并打开浏览器。 +3. 双击 `C:\Stronghold-Protocol\scripts\start-windows.bat`。首次会:安装依赖(`npm ci`;完整包已含,跳过)→ 复制前端库 → 下载约 270 MB 素材(完整包已含,跳过;显示进度,中断后再次启动会续传)→ 若检测到本机的明日方舟客户端,询问是否提取官方贴图(可跳过)→ 启动服务器并打开浏览器。 4. 窗口里会打印朋友可用的地址,例如 `http://192.168.1.23:3000`。用另一台设备打开它确认能进入。关闭窗口即停止服务器。 等价的手动命令:`npm ci`、`node tools/setup.mjs`、`npm start`。 @@ -237,6 +237,23 @@ services: | 朋友打不开页面 | 防火墙规则 / 网络类型(1.2);确认用的是 `LAN` 地址而不是 `localhost`;访客 Wi-Fi 常开启「AP 隔离」;不在同一网络请看第 2 节 | | 画面是占位图、没有声音 | 素材没下完:重新运行 `node tools/setup.mjs`(会续传);缺失明细在 `.cache/assets-report.json`。GitHub 原始地址访问失败时会自动改用 jsDelivr 镜像 | | 素材下载很慢 / 失败 | 网络问题可随时中断,重新运行会跳过已完成的文件;`node tools/fetch-assets.mjs --concurrency=4` 降低并发。有文件没下载成功时,素材清单 `data/assets.json` 保持不变(脚本列出缺少的条目并以非零状态结束;游戏里缺的图片用占位图,缺的声音不播放),重新运行即可补齐 | -| 本地提取失败 | 不影响游戏。确认客户端已下载全部资源;Python 版本太新导致依赖安装失败时,安装 Python 3.12 后删除 `.venv-extract` 再运行 `node tools/setup.mjs --local` | -| 3D 棋盘没出现 | 需要本地提取的棋盘贴图(`node tools/doctor.mjs` 会显示「3D 棋盘可用」),以及支持 WebGL2 的浏览器 | +| 表情显示成默认图标、「玩法说明」只有文字要点 | 素材没下载完整:重新运行 `node tools/setup.mjs`(表情和教程图随其他素材一起从公开镜像下载,不需要客户端);缺失明细在 `.cache/assets-report.json` | +| 本地提取失败 | 游戏照常运行,只是第 6 节表格里的几样换成替代样式。确认客户端已下载全部资源;Python 版本太新导致依赖安装失败时,安装 Python 3.12 后删除 `.venv-extract` 再运行 `node tools/setup.mjs --local` | +| 3D 棋盘没出现 | 需要本地提取的棋盘贴图(`node tools/doctor.mjs` 会显示「3D 棋盘可用」),以及支持 WebGL2 的浏览器。没有客户端的服务器可以从同一版本的整合包复制本地素材(第 6 节) | | 断线 | 同盟模拟 10 分钟内、独立模拟 24 小时内(`config.constants.singleReconnectTime`)用同一浏览器重新打开页面,自动回到原座位。同盟掉线期间按原阵容自动作战、到时自动准备(不会代为购买;想让 AI 代打请用「离开模拟 → 暂离(AI 托管)」);独立模拟不计时,等你回来 | + +## 6. 本地客户端素材(可选) + +`public/assets/local/` 和 `data/local-assets.json` 是从本机安装的《明日方舟》客户端里提取的官方素材(`tools/local-extract`,DESIGN §13):`node tools/setup.mjs` 检测到客户端时会询问是否提取,之后可以用 `node tools/setup.mjs --local` 重新提取,或用 `--game "<…/StreamingAssets/AB/Windows>"` 指定客户端目录。setup 从公开镜像下载的素材不包含这部分,所以在没有客户端的电脑上(例如 Linux 服务器)从源码部署时不会有它;Releases 的完整包里已经带上了。 + +没有本地素材时游戏照常运行,只是下面几样换成替代样式: + +| 内容 | 没有本地素材时 | +|---|---| +| 官方 3D 棋盘(贴图、模型、地图特效) | 2D 棋盘,地块由程序绘制 | +| 部分官方界面图标与底板:交流按钮和表情面板的边框、暂停面板、装备替换窗口、干员调配界面、队友状态与漏怪标记、模组类型图标等 | 样式相近的替代图形、图标或文字 | +| 灼热 / 炽焰源石虫的官方模型 | 染成橙色 / 红橙色的普通源石虫 | + +表情(6 套 × 6 个)和「玩法说明」的 19 页教程图公开镜像也有:`node tools/setup.mjs` 会和其他素材一起下载(约 21 MB),不需要客户端;有本地素材时优先显示本地的。 + +**没有客户端的服务器**想要上表中的官方素材:从**同一版本**的完整包([Releases](https://github.com/sganggs/Stronghold-Protocol/releases))里,把 `public/assets/local/` 文件夹和 `data/local-assets.json` 复制到服务器项目目录下的相同位置。服务器每次请求都会重新读取这两处,不必重启,玩家刷新页面即可。一定要用与服务器代码相同版本的完整包:各版本提取的内容和清单可能不同(例如灼热 / 炽焰源石虫的模型是 0.1.0 之后才加入的),混用其他版本的文件会缺图或用错图。复制后 `node tools/doctor.mjs` 会显示本地素材的条目数和「3D 棋盘可用」。 diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 41c932cb..435ea0e3 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -516,11 +516,11 @@ 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 art that no web source has from a locally installed client (CrossOver Windows build or PlayCover iOS build) into `public/assets/local/**` and writes `data/local-assets.json` (`{ groups: { '': { 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). +`tools/local-extract/extract.py` (Python: UnityPy + lz4 + Pillow; decodes Arknights' custom **LZ4AK** bundle compression via `aklz4.py`) extracts art that no web source has from a locally installed client (CrossOver Windows build or PlayCover iOS build) into `public/assets/local/**` and writes `data/local-assets.json` (`{ groups: { '': { 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//']`, `ui['guide/']`) 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. - `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/{basic,basic_2}/pic__battle.png` — the 12 official battle emotes (`shared/constants.js → EMOTES`, `emoteArtPath`). -- `guide/autochess_{home,shop,handbook}_N.png` — the official tutorial pages; stored squashed to 1024² — **display at 16:9**. +- `emoticon//.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). - `projectiles/*` — battle projectile sprites. - `spine/enemy//` — enemy models no web dump carries (灼热 / 炽焰源石虫, `extract.py ENEMY_SPINES`): manifest `enemies[id].spineLocal` (metadata from the committed `tools/assets/local-enemy-spines.json`); drawn instead of the tinted web alias when every file is listed (§21.14). - `module/` — the official module (uniequip) TYPE icons (white glyphs; keys are the client's mixed-case file names, e.g. `PRI-X`, `mar-x`, `isw-a`), matched **case-insensitively** against a ModuleRecord's `typeName` by `assetUrls.moduleTypeIconUrl(local, typeName)` (Greek type letters map to the file's Latin letter: `ISW-α` → `isw-a`) — shown on the 干员调配 module cards / module info, the detail panel's 模组 row and the shop card's module tag; lettered tiles / type text without them. @@ -1482,9 +1482,17 @@ TBD-22.3 TBD-22.4 -### 22.5 Emotes and guide pages without a local client; deployment docs (#42); the English title (#38) +### 22.5 Emotes and guide pages without a local client; deployment docs (#42); the English title (#38) — `tools/assets/plan.mjs UI_EXTRAS / GUIDE_PAGES`, data/assets.json `ui['emoticon//']` / `ui['guide/']`, `data.js artUrls / nextArtUrl`, `ui/emotes.js emoteArtUrls / EmoteArt`, `ui/guide.js guidePages / GuideHost`, `tools/setup.mjs LOCAL_ART_FALLBACK / LOCAL_ART_COPY_HINT`, `tools/doctor.mjs`, README, docs/DEPLOY.md §6, `server/index.js` banner, package.json `name` -TBD-22.5 +- **#42 seen**: "游玩时左下角的六页表情全部显示为默认图标" — a Linux server deployed from source (git clone, setup with the asset download, Node 22); copying `public/assets/local/` and `data/local-assets.json` from the release bundle fixed it. **Real**: both are extracted from a locally installed client (§13; setup extracts only when it finds one), and the emote art and the 玩法说明 pages came from there only, so a source deploy without the client drew the neutral glyph for all 36 emotes and the official tips text instead of the 19 pages. README, DEPLOY.md, setup and doctor said the local art did not matter ("不影响游戏", "其他功能不受影响") — wrong: the 3D board, part of the official HUD art and two enemy models fall back too. +- **Source**: the mirror the pipeline already uses (ArknightsAssets2 `cn`, assets/dyn) has all 55 files — the six theme bundles' `ui/emoticon/theme/[uc]/icon/.png` (the 36 battle sprites, keyed by display_meta_table picId) and `arts/guidebookpages/[pack]autochess/autochess_{home 1–9, shop 1–6, handbook 1–4}.png`. Checked 2026-10-03: every URL answers 200 on raw.githubusercontent.com and on the jsDelivr fallback; the same pictures at the same sizes as the local extraction (emotes 120×120 / ≈113×115, pages 1024² squashed — still shown at 16:9), pixel differences only from the platforms' texture compression. +- **Now**: the asset plan downloads them like any other UI sprite (UI_EXTRAS, built from shared/constants.js EMOTE_CATALOG and GUIDE_PAGES — a test keeps GUIDE_PAGES equal to guide.js GUIDE_CHAPTERS), keyed by the data/local-assets.json group and name: `ui['emoticon//']` → `/assets/ui/emoticon//.png`, `ui['guide/']` → `/assets/ui/guide/.png` (55 files, 21.3 MiB; the download ≈ 270 MB, was ≈ 250 MB). The committed data/assets.json lists them (regenerated by a real fetch-assets run: only these 55 entries, `hash` and `stats` changed), so `npm run setup` — which the start scripts run on every start — on an install made before them finds 55 files missing and runs fetch-assets; §21.25's conventions hold: ledger, retries, the jsDelivr fallback, and a run where they fail keeps the committed manifest (shrink guard), so the next setup tries again. The client looks both manifests up by the same names (`data.js artUrls`): the local-client picture first, then the mirror copy; `EmoteArt` and the guide viewer move to the next URL when an image fails to load (`nextArtUrl`: a copied data/local-assets.json without its files falls through to the mirror copy), and with neither keep today's fallback (the glyph; the tips text). `localAsset` stays local-only. +- **Docs now**: README, docs/DEPLOY.md (new §6 本地客户端素材, the 排错 rows), docs/PLAYING.md, docs/ASSETS.md and the setup / doctor messages say what still needs the client — the official 3D board (→ the 2D board with procedural tiles), part of the official HUD art (the emote button and panel frames, the pause plate, the equip-replace dialog, the loadout screen, the team status and miss tags, module type icons … → CSS look-alikes, icons or text) and 灼热 / 炽焰源石虫's models (→ the tinted web alias, §21.14) — and that a server without the client can copy `public/assets/local/` and `data/local-assets.json` from the release bundle of the **same version** (both are read per request: no restart). The Docker parts are unchanged (deferred by the owner). +- **Not done here**: the mirror also has most sprites of the three autochess UI bundles that the local `ui/battle` / `ui/common` / `ui/outer` groups hold (by name 377 / 389, 137 / 160 and 659 / 659 under `ui/autochess/[uc]autochess{battle,common,outer}/**`; the emote panel frames, `matte_pause` and `bg_miss_enemy` checked at the local sizes), so the HUD row of DEPLOY §6 could shrink in a follow-up. +- **#38, the English title** — **official**: EN client data, activity_table `basicInfo.act2autochess.name` = "Stronghold Protocol: Alliance" (CN 卫戍协议:盟约; ArknightsAssets/ArknightsGamedata `en/gamedata/excel/activity_table.json`, 2026-09-29 — Kengxxiao/ArknightsGameData_YoStar stops at EN client 31.5.80, 2025-11-13, before this season, with act1vautochess "Stronghold Protocol" only). **Now**: the README title and English summary, the server start-up banner and the package name (`stronghold-protocol-alliance`, package.json and the lockfile) say Alliance instead of "Covenant"; the Chinese title 卫戍协议:盟约 and the repository name stay. "Covenant" remains only in 新约能天使's official appellation ("Exusiai the New Covenant") and its buff key `angel2:covenant`. +- **PR #27 follow-up**: tools/doctor.mjs's comment now says directly that tun0 / tap0 are matched first by VPN_IF (substring `tun\d` / `tap`) and classed as vpn — it spoke of no longer listing `^tun\d`, which master's VIRTUAL_IF never had; comment only. +- [ASSUMED]: none. +- Tests: `test/assets.test.js` (the 55 plan entries and URLs, a rebuilt manifest, the committed manifest lists them), `test/ui/localArt.test.js` (the lookup order, a server without the client, GUIDE_PAGES = GUIDE_CHAPTERS), `test/fetch-assets-shrink.test.js` (a failed mirror run keeps the manifest), `test/static-local-art.test.js` (the docs and messages), `test/version.test.js` (the English title), `test/doctor.test.js`. ### 22.6 钩索师 and 推击手 on 高台 (#32 item 4) diff --git a/docs/PLAYING.md b/docs/PLAYING.md index e0e57bad..8fa93cca 100644 --- a/docs/PLAYING.md +++ b/docs/PLAYING.md @@ -2,7 +2,7 @@ 「卫戍协议:盟约」是自走棋 + 塔防:休整期在调度中心招募干员、摆阵、配装备,作战期干员自动部署并迎击从红门涌来的敌人,漏过去的敌人扣目标生命值(LP)。同盟模拟中 1–4 人**合作**(没有 PvP),撑到第 14 回合打倒敌方领袖即胜利。 -本文数值来自官方数据表(`data/config.json` 等)与 [research](research/00-INDEX.md);游戏内左下角 📖「玩法说明」有官方教程图(需本地提取)。操作方式见 [README · 操作](../README.md#操作)。 +本文数值来自官方数据表(`data/config.json` 等)与 [research](research/00-INDEX.md);游戏内左下角 📖「玩法说明」有官方教程图(随素材一起下载;没下载到时显示文字要点)。操作方式见 [README · 操作](../README.md#操作)。 ## 1. 一局的流程 diff --git a/docs/research/09-ux-official.md b/docs/research/09-ux-official.md index e54865d8..7b937c3d 100644 --- a/docs/research/09-ux-official.md +++ b/docs/research/09-ux-official.md @@ -289,6 +289,8 @@ All in `StreamingAssets/AB/Windows/ui/emoticon/theme/`: The autochess **room** pics (pic_hello, pic_question, pic_waiting, pic_working, pic_sorry_2, …) are **not** in any local bundle. Keep room emotes out of scope, or reuse the battle glyphs. +The public mirror (ArknightsAssets2 `cn`, `ui/emoticon/theme/[uc]/icon/.png`) has the same 36 battle sprites, at the same sizes (checked 2026-10-03); since v0.1.2 `tools/fetch-assets.mjs` downloads them and the client uses them when the local art is absent (DESIGN §22.5, GitHub issue #42). + --- ## 5. Other prep interactions we may have missed diff --git a/test/static-local-art.test.js b/test/static-local-art.test.js index d8ce4ebe..3041ef3a 100644 --- a/test/static-local-art.test.js +++ b/test/static-local-art.test.js @@ -1,5 +1,6 @@ // The optional local-client art manifest (data/local-assets.json, DESIGN §13): an install without it must get an -// empty manifest (200) instead of a 404, and an install with it must get the real file. +// empty manifest (200) instead of a 404, and an install with it must get the real file. The docs and the setup / +// doctor messages say what falls back without it (GitHub issue #42, DESIGN §22.5). import { test } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; @@ -69,3 +70,22 @@ test('present data/local-assets.json is served as-is', async () => { fs.rmSync(dir, { recursive: true, force: true }); } }); + +test('docs and messages say what falls back without the local art and how a server without the client gets it (GitHub issue #42)', async () => { + const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8'); + const { LOCAL_ART_FALLBACK, LOCAL_ART_COPY_HINT } = await import('../tools/setup.mjs'); + for (const f of ['README.md', 'docs/DEPLOY.md', 'tools/setup.mjs', 'tools/doctor.mjs']) { + assert.ok(!/不影响游戏|其他功能不受影响|游戏不受影响/.test(read(f)), `${f}: never "the local art does not matter"`); + } + for (const re of [/3D 棋盘/, /界面图标/, /源石虫/]) assert.match(LOCAL_ART_FALLBACK, re); + assert.ok(!/表情|玩法说明/.test(LOCAL_ART_FALLBACK), 'the emotes and the 玩法说明 pages are downloaded, not local-only'); + assert.match(LOCAL_ART_COPY_HINT, /同一版本的整合包/); + const deploy = read('docs/DEPLOY.md'); + const s6 = deploy.slice(deploy.indexOf('## 6. 本地客户端素材')); + assert.ok(deploy.includes('## 6. 本地客户端素材') && s6.length > 200, 'DEPLOY §6'); + for (const re of [/同一版本/, /public\/assets\/local\//, /data\/local-assets\.json/, /3D 棋盘/, /源石虫/, /表情/, /玩法说明/]) assert.match(s6, re); + const readme = read('README.md'); + assert.match(readme, /表情和「玩法说明」的教程图随上面的素材一起从公开镜像下载/); + assert.match(readme, /\*\*同一版本\*\*的整合包/); + assert.ok(!/需本地提取/.test(read('docs/PLAYING.md')), 'PLAYING: the 玩法说明 pages come with the download'); +}); diff --git a/tools/doctor.mjs b/tools/doctor.mjs index c7c53add..7a602f6b 100755 --- a/tools/doctor.mjs +++ b/tools/doctor.mjs @@ -18,6 +18,7 @@ import { fileURLToPath } from 'node:url'; import { ROOT, MIN_NODE, IS_WIN, IS_MAC, c, mark, capture, padDisplay, displayWidth, checkNode, checkDeps, checkVendor, checkData, checkAssets, checkLocal, findClient, findPython, + LOCAL_ART_FALLBACK, LOCAL_ART_COPY_HINT, } from './setup.mjs'; // --------------------------------------------------------------------------------------------------- @@ -192,7 +193,7 @@ async function main() { const client = findClient(null); row(local.manifest ? 'ok' : 'skip', '本地客户端美术(可选)', local.manifest ? `${local.count} 项${local.board3d ? ',3D 棋盘可用' : ',无棋盘贴图(2D 棋盘)'}${local.board3d && !local.tiles ? ';缺 tiles.json → node tools/setup.mjs' : ''}` - : client ? `检测到 ${client.kind} 客户端 → node tools/setup.mjs --local` : '未提取(不影响游戏)'); + : client ? `检测到 ${client.kind} 客户端 → node tools/setup.mjs --local` : `未提取:${LOCAL_ART_FALLBACK}(${LOCAL_ART_COPY_HINT})`); if (client || local.manifest) { const py = findPython(); row(py ? 'ok' : 'skip', 'Python(仅提取用)', py ? `${py.cmd} ${py.version}` : '未找到 Python 3.8+'); diff --git a/tools/setup.mjs b/tools/setup.mjs index c88ea680..25590cee 100755 --- a/tools/setup.mjs +++ b/tools/setup.mjs @@ -8,7 +8,7 @@ // 2. Dependencies: `npm ci` (falls back to `npm install`) when node_modules is missing or incomplete. // 3. Client libraries in public/vendor (tools/vendor.mjs) when any is missing. // 4. Game data (data/*.json, committed) present and parseable. -// 5. Art/audio (tools/fetch-assets.mjs, ~250 MB into public/assets, resumable, mirror fallback) when public/assets +// 5. Art/audio (tools/fetch-assets.mjs, ~270 MB into public/assets, resumable, mirror fallback) when public/assets // is missing or data/assets.json lists files that are not on disk. A failure is a warning: the game still runs // with fallback visuals and the next run resumes. // 6. Optional: official board/UI art from a locally installed Arknights client (Windows native install, CrossOver @@ -189,6 +189,15 @@ export function checkAssets() { return { ok: present && missing.length === 0 && urls.length > 0, present, manifest: true, total: urls.length, missing: missing.length, sample: missing.slice(0, 5), bytes: Number(m.stats?.bytes) || 0 }; } +/** + * What the game draws instead when the local-client art is absent (DESIGN §13, docs/DEPLOY.md §6). The battle emotes and + * the 玩法说明 pages are not in the list: step 5 downloads them from the public mirror with the other assets (GitHub + * issue #42). Shown by setup and doctor. + */ +export const LOCAL_ART_FALLBACK = '3D 棋盘改用 2D,部分官方界面图标和灼热/炽焰源石虫模型用替代样式'; +/** Where a machine without the client gets the local art (docs/DEPLOY.md §6「本地客户端素材」). */ +export const LOCAL_ART_COPY_HINT = '没有客户端的服务器可以从同一版本的整合包复制 public/assets/local 和 data/local-assets.json'; + /** * Local-client art (optional): manifest entry count, whether the 3D board atlas is on disk and whether the extraction * has the enemy models only the client has (`spine/enemy/*` groups, extract.py ENEMY_SPINES — added after 0.1.0). @@ -400,7 +409,7 @@ async function main() { let assets = checkAssets(); if (!opts.assets) add(assets.ok ? 'ok' : 'skip', '美术/音频 public/assets', assets.ok ? `${assets.total} 个文件` : '已跳过(--no-assets)'); else if (!assets.ok && deps.ok && !opts.check) { - const what = !assets.present ? `首次下载约 ${assets.bytes ? mb(assets.bytes) : '250 MB'},可随时中断,重新运行会续传` + const what = !assets.present ? `首次下载约 ${assets.bytes ? mb(assets.bytes) : '270 MB'},可随时中断,重新运行会续传` : `补全缺失的 ${assets.missing} 个文件`; log(`\n${c.cyan('▶')} 下载美术与音频素材(${what})…`); const r = run(process.execPath, [path.join(ROOT, 'tools', 'fetch-assets.mjs')]); @@ -422,7 +431,8 @@ async function main() { const already = local.manifest && local.dirPresent; if (!client) { if (already && local.board3d && !local.tiles && !opts.check) cropBoardTiles(log); - add(already ? 'ok' : 'skip', '本地客户端美术(可选)', already ? `已提取 ${local.count} 项` : (opts.game ? `找不到 ${opts.game}` : '未检测到本机明日方舟客户端(不影响游戏)')); + add(already ? 'ok' : 'skip', '本地客户端美术(可选)', already ? `已提取 ${local.count} 项` + : `${opts.game ? `找不到 ${opts.game}` : '未检测到本机明日方舟客户端'}:${LOCAL_ART_FALLBACK}(${LOCAL_ART_COPY_HINT},见 docs/DEPLOY.md 第 6 节)`); } else if (!client.autochess) { add(already ? 'ok' : 'warn', '本地客户端美术(可选)', `${client.kind} 客户端缺少卫戍协议资源(请在游戏内下载全部资源):${client.path}`); } else if (already && opts.local !== 'force') { @@ -438,7 +448,7 @@ async function main() { let go = opts.local === 'force' || opts.yes; if (go || !state.localDeclined) { log(`\n${c.cyan('▶')} 检测到本机明日方舟客户端(${client.kind}):\n ${c.dim(client.path)}`); - log(' 可以从中提取官方棋盘贴图、UI 图标、表情等(仅本机使用;通常 1–5 分钟,Python 依赖约 40 MB,装在项目内的 .venv-extract)。'); + log(' 可以从中提取官方 3D 棋盘贴图、界面图标等(仅本机使用;通常 1–5 分钟,Python 依赖约 40 MB,装在项目内的 .venv-extract)。'); } let unattended = false; if (!go && !state.localDeclined) { @@ -457,7 +467,7 @@ async function main() { if (r.ok) cropBoardTiles(log); const after = checkLocal(); if (r.ok && after.manifest) { add('ok', '本地客户端美术(可选)', `提取 ${after.count} 项${after.board3d ? ',3D 棋盘可用' : ''}`); saveState({ localDeclined: false, localExtractedFrom: client.path }); } - else add('warn', '本地客户端美术(可选)', `提取未成功(退出码 ${r.code}),游戏不受影响;可稍后重试 node tools/setup.mjs --local`); + else add('warn', '本地客户端美术(可选)', `提取未成功(退出码 ${r.code}):游戏照常运行,${LOCAL_ART_FALLBACK};可稍后重试 node tools/setup.mjs --local`); } } }