diff --git a/docs/ASSETS.md b/docs/ASSETS.md index 18835aaa..07e005e2 100644 --- a/docs/ASSETS.md +++ b/docs/ASSETS.md @@ -268,10 +268,17 @@ Other renderer rules from research 07 §5.4–5.5: - **Enemy aliases:** `enemies[id].spineAliasOf` means the model belongs to another enemy. Two cases: - `_2` variants whose official prefab is the base one (鸭爵, 高普尼克, 流泪小子, 圆仔, 假想敌:胄, 假想敌:铳): the base model, as in the game. - an enemy whose own model no dump carries: 灼热源石虫 / 炽焰源石虫 (`enemy_1305_mhslim` / `_2`) use the plain 源石虫 on - the web (`plan.mjs ENEMY_SPINE_ALIAS`). Their official skeletons only exist in the client's enemy art bundles - (`refs/arts/enm_art_*.ab`), so they are an optional **overlay**, `enemies[id].spineLocal` = `{ group, skel, atlas, - textures, pma, anims, animations, events, hits, bounds }` (file names in the `data/local-assets.json` group - `spine/enemy/{enemyId}`; the rest as a `spine` entry): + the web (`plan.mjs ENEMY_SPINE_ALIAS`). The reason is upstream: isHarryh/Ark-Models *indexes* `1305_mhslim` / + `1305_mhslim_2` but with an **empty `assetList`** — registered, never uploaded — so `arkModel()` finds no files for + them and the alias chain drops to `enemy_1007_slime`, a different enemy rather than a variant of it, which is why + the renderer tints that alias toward the slug's own colours. Their official skeletons only exist in the client's + enemy art bundles (`refs/arts/enm_art_*.ab`); the *mobile* build also ships them as + `enemy_spine//.{skel,atlas,png}` (straight-alpha pages: no `pma: true` line), but its only public + mirror is a community wiki rather than a GitHub dump, so it is deliberately **not** an asset source — a third-party + site is not added to `sources.mjs` for two models, and the tinted alias stays the web model until a GitHub dump + carries them. The same official model from the local client is the optional **overlay**, + `enemies[id].spineLocal` = `{ group, skel, atlas, textures, pma, anims, animations, events, hits, bounds }` (file + names in the `data/local-assets.json` group `spine/enemy/{enemyId}`; the rest as a `spine` entry): - `tools/local-extract/extract.py` writes the model to `public/assets/local/spine/enemy/{enemyId}/` (page textures with their `[alpha]` texture merged in: premultiplied RGB + A like Ark-Models; the atlas gets `size:` and `pma: true`) and lists its files in `data/local-assets.json`. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index c672134c..bd41ae84 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1259,7 +1259,7 @@ Eighteen workstreams (WA–WR, each on its own branch from master 7f13473) were ### 21.14 Special Originium slugs and 暴鸰's bomb (D3, D4) — `tools/local-extract/extract.py ENEMY_SPINES`, `tools/assets/local-enemy-spines.json`, `plan.mjs localEnemySpines`, `fetch-assets --local-spines`, `assets.js spineEntry`, `render/units.js ALIAS_TINT / FORMS`, `content/enemies.js kitBombd`, `snapshot.js form`, `style.js PROJ.droneBomb` -- **D3** ("所有特殊源石虫的模型全表现为普通源石虫"), partly real: 高能 / 冰爆 / 简饲源石虫 and “庞贝” always had their own models; 灼热 / 炽焰源石虫 (enemy_1305_mhslim / _2, the ELEMENT slugs, up to 10 a round) were the plain 源石虫 — no web dump has their models (the asset plan aliases them); the official skeletons exist only in the client (refs/arts/enm_art_12.ab). +- **D3** ("所有特殊源石虫的模型全表现为普通源石虫"), partly real: 高能 / 冰爆 / 简饲源石虫 and “庞贝” always had their own models; 灼热 / 炽焰源石虫 (enemy_1305_mhslim / _2, the ELEMENT slugs, up to 10 a round) were the plain 源石虫 — no web dump has their models (the asset plan aliases them); the official skeletons exist only in the client (refs/arts/enm_art_12.ab). Root cause located 2026-10-04: Ark-Models *indexes* `1305_mhslim` / `1305_mhslim_2` with an **empty `assetList`** (registered, never uploaded), so the alias chain falls to `enemy_1007_slime` — a different enemy rather than a variant of it. The *mobile* build ships their own model (`enemy_spine//.{skel,atlas,png}`, straight-alpha pages), but its only public mirror is a community wiki and not a GitHub dump, so it is deliberately **not** an asset source (docs/ASSETS.md "Enemy aliases"): the tinted alias stays the web model until a GitHub dump carries these two. - **D3 now**: an optional overlay — the manifest keeps the alias as the web model and `enemies[id].spineLocal` names the files of the data/local-assets.json group `spine/enemy/` with the metadata of the committed `tools/assets/local-enemy-spines.json` (data/assets.json is byte-identical with or without the extraction). The client draws the official model when the local manifest lists every file, else (or on a load failure) the web alias tinted toward the slug's own lava colours (`ALIAS_TINT`: 灼热 orange, 炽焰 red-orange, research 07 §5.6) [ASSUMED look]. The 0.1.1 release bundle carries the models (extracted with `node tools/setup.mjs --local`, the `spine/enemy` job). Audit (249 enemies vs the client's prefabs): no other enemy is drawn with another's model; 伊利昂的木驮兽 starts on skin `Full` in the game and is drawn with `default` (unchanged). `fetch-assets --prune` never deletes `public/assets/local` any more. - **D4** ("暴鸰的炸弹无法正常投放"), partly real: the bomb hit in the trigger tick, with no projectile, and stayed under the drone. **Official** (prefab enemy_1040_bombd / projectile_bombd; PRTS 暴鸰): Boomb plays Attack, the bomb leaves on OnAttack (frame 8) and flies homing at 5 tiles/s; the cast ends when the bomb is gone, ≥ 0.667 s after the release; buff bomb_s: the bomb-less mode S1 (*_2 clips) + speed ×2 ("技能结束后"); `_immuneStunWhenAffecting` 0. - **D4 now**: the drone holds still at the trigger; 8 ticks later (8/30 s) the release ('atk' droneBomb + the form 'bombed' — UnitInfo `form`); on landing the target 100 % ATK + the 8 tiles around 100 % splash (a target gone mid-flight: the bomb lands where it was; the drone's death after the release does not stop it); at max(landing, release + 0.667) ×2 speed and it flies on. A stun / freeze / sleep before the release interrupts the cast (no drop; the skill re-arms with its 1 s cooldown). diff --git a/test/feedback1d-models.test.js b/test/feedback1d-models.test.js index 326812ea..7e7d9966 100644 --- a/test/feedback1d-models.test.js +++ b/test/feedback1d-models.test.js @@ -11,6 +11,14 @@ // Without the extraction the web alias is drawn tinted toward the slug's own colours (render/units.js ALIAS_TINT, // research 07 §5.6 "a hue shift" [ASSUMED look]), so source installs still tell them apart from the plain slug. // 高能 / 冰爆 / 简饲源石虫 and “庞贝” always had their own models (checked in headless Chrome). +// +// Root cause (2026-10-04): Ark-Models *indexes* 1305_mhslim / 1305_mhslim_2 with an EMPTY `assetList` — registered, +// never uploaded — so `arkModel()` finds nothing and the alias chain drops to enemy_1007_slime, a *different* enemy +// rather than a variant of it. The mobile build does ship their own model (straight-alpha pages, no `pma: true` line), +// and it is reachable, but its only public mirror is a community wiki rather than a GitHub dump: adding it would put a +// non-GitHub host into `sources.mjs` for two models, which the reviewer of this change asked not to do. So the tinted +// alias stays the web model, the local extraction stays the overlay, and the last block below locks both halves +// (only these two enemies borrow a different enemy's skeleton; every asset source stays a GitHub dump). import { test, describe, before, after } from 'node:test'; import assert from 'node:assert/strict'; @@ -19,7 +27,8 @@ import { spawnSync } from 'node:child_process'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { buildPlan } from '../tools/assets/plan.mjs'; +import { buildPlan, ENEMY_SPINE_ALIAS } from '../tools/assets/plan.mjs'; +import { RAW } from '../tools/assets/sources.mjs'; import { resolveTemplate, collectLeaves } from '../tools/assets/manifest.mjs'; import { findLocalEnemyModels, localEnemySpineMeta, loadLocalEnemySpines, LOCAL_ENEMY_SPINES_FILE, localEnemySpineGroup } from '../tools/assets/spine.mjs'; import { indexAudio } from '../tools/assets/audio.mjs'; @@ -174,6 +183,34 @@ describe('D3: the official slug models are an optional overlay of the web alias' }); }); +describe('D3 root cause: only the two slugs borrow a different enemy\'s skeleton, and no non-GitHub source is added', () => { + test('plan: ENEMY_SPINE_ALIAS covers exactly the two slugs (Ark-Models indexes them with an empty assetList)', () => { + const p = plan(undefined); + assert.deepEqual(Object.keys(ENEMY_SPINE_ALIAS).sort(), [...SLUGS].sort(), 'exactly the slugs are aliased by hand'); + for (const id of SLUGS) assert.equal(ENEMY_SPINE_ALIAS[id], 'enemy_1007_slime', `${id}: the plain 源石虫`); + assert.ok(p.notes.some((n) => n.includes('enemy_1305_mhslim') && /aliased/.test(n)), + 'the plan says so in its notes, so a run without the model is explainable'); + }); + + test('data/assets.json: every other spineAliasOf stays inside its own variant family', () => { + const borrowed = []; + for (const [id, e] of Object.entries(MANIFEST.enemies)) { + if (!e.spineAliasOf) continue; + const base = /^(enemy_\d+_[a-z0-9]+?)_\d+$/i.exec(id); // enemy_2001_duckmi_2 → enemy_2001_duckmi + if (!SLUGS.includes(id) && (!base || base[1] !== e.spineAliasOf)) borrowed.push(`${id} → ${e.spineAliasOf}`); + } + assert.deepEqual(borrowed, [], + 'the six _2 aliases draw the base enemy their own prefab uses; only the two slugs borrow a different enemy'); + }); + + test('every asset source stays a GitHub dump (no third-party mirror for two models)', () => { + for (const [name, url] of Object.entries(RAW)) { + assert.match(url, /^https:\/\/raw\.githubusercontent\.com\//, `RAW.${name} is a raw.githubusercontent.com URL`); + } + assert.ok(!/prts|torappu/i.test(JSON.stringify(RAW)), 'no community-wiki host among the download sources'); + }); +}); + describe('D3 client: the official model only when data/local-assets.json lists it', () => { const webOf = (id) => MANIFEST.enemies[id].spine; // the plain 源石虫's model (spineAliasOf enemy_1007_slime) diff --git a/tools/assets/plan.mjs b/tools/assets/plan.mjs index 3fef0f21..d0f7c38c 100644 --- a/tools/assets/plan.mjs +++ b/tools/assets/plan.mjs @@ -29,6 +29,14 @@ import { EMOTE_CATALOG } from '../../shared/constants.js'; * models come from the local client only (tools/local-extract ENEMY_SPINES): `localEnemySpines` adds them as the * optional `spineLocal` overlay, which the client draws when data/local-assets.json lists its files (user feedback * after 0.1.0, D3: 灼热源石虫 / 炽焰源石虫 were drawn as the plain 源石虫 everywhere). + * + * Why no dump carries them: isHarryh/Ark-Models *indexes* `1305_mhslim` / `1305_mhslim_2` but with an EMPTY + * `assetList` — registered, never uploaded — so `arkModel()` returns nothing for them and the alias chain below falls + * back to `enemy_1007_slime`: a different enemy rather than a variant of it, which is why the renderer tints it + * (render/units.js ALIAS_TINT). The *mobile* build does ship their own model + * (`enemy_spine//.{skel,atlas,png}`), but the only public mirror of that build is a community wiki, + * not a GitHub dump, so downloading from it would add a source the project deliberately does not use (docs/ASSETS.md + * "Enemy aliases"). The tinted alias therefore stays the web model until a GitHub dump carries these two. */ export const ENEMY_SPINE_ALIAS = Object.freeze({ enemy_1305_mhslim: 'enemy_1007_slime',