Update expanded roster checks and register Clementia impact visuals

This commit is contained in:
sganggs committed 2026-10-09 17:46:07 +08:00
1 parent c166479ff1
commit 0b0ce4fbd7
11 files changed
+34 -18

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
# 卫戍协议:盟约 · Stronghold Protocol: Alliance
> 开发版(dev 分支):不稳定,请勿用于公开服务器。最近正式版请从 Releases 下载。
《明日方舟》季节性自走棋塔防玩法「卫戍协议:盟约」的**非官方同人复刻**:浏览器即开即玩,单人或 1–4 人联机合作。
![version](https://img.shields.io/badge/version-0.2.3--dev-2ea44f)
+11
View File
@@ -40,6 +40,9 @@ with mechanism notes from PRTS checked on 2026-10-09. Launch-day descriptions ma
defence, S1 cadence, S2 capacity / immunity / terrain / air vortex / target cap, and S3 ammo / global marks /
mixed damage. Golden changes and the final integration verification are recorded below once complete.
Clementia S3 currently uses the shared blast visual as an explicit `[ASSUMED]` placeholder;
this preserves the previous fallback appearance, not a claim to reproduce her official effect assets.
### 28.2 New modules — 黍「好时节」and 乌尔比安「前路」
- **Sources:** [PRTS 黍](https://prts.wiki/w/黍), [PRTS 乌尔比安](https://prts.wiki/w/乌尔比安), official
@@ -167,3 +170,11 @@ Sleep, silence, no-heal targets, charge refunds, cooperative healing and S2 targ
Golden: all 285 scenarios recomputed; `diy-059`, `diy-133` change because
charged heals now act without enemy targets and S2 can open for enemies. Other scenarios remain byte-identical.
### 28.11 Integration checks for the expanded roster
The fixed roster assertions now include 72 owned DIY operators, 259 forms, 192 voiced operators and 2688 Japanese
lines; the official-table comparison still checks every potential, skill and module. Clementia adds one 阿戈尔 pick.
The official EN overlay has no launch-day Clementia talents yet: overall coverage remains ≥97%, with the measured
talent floor explicitly updated to 94.9%; untranslated text keeps its Chinese fallback. The old Saria no-enemy
assertion is corrected to require actual healing, and development builds retain the standard top-of-README notice.
+2
View File
@@ -7,6 +7,8 @@
*/
export const FX_KINDS = Object.freeze({
// blasts
// [ASSUMED] Clementia's S3 uses the shared blast placeholder until her official effect is ported.
blast: { a: 'blast', c: 0xffb35c },
aoe: { a: 'blast', c: 0xffb35c }, explode: { a: 'blast', c: 0xff7a33 }, explosion: { a: 'blast', c: 0xff7a33 },
// `pt`: always at the event's (x, y) (its `id` is the shooter); `heavy`: debris + scorch
bombard: { a: 'blast', c: 0xffa04a, r: 1.5, pt: true, heavy: true }, bombardShell: { a: 'shell', c: 0xff5a3a, r: 1.5, pt: true },
+1 -1
View File
@@ -148,7 +148,7 @@ or a prototype (DATA.md §18, `shared/diy.js`). The prototypes run their stand-i
an owned 6★ can be picked only once it has a kit of its own: one file per operator, the same rules as every kit above.
Contributions are welcome — one operator per pull request is easiest to review.
**Who.** The owned-6★ picks are `data/backups.json diy.ownedPool` (71 operators; the collab operators are not included,
**Who.** The owned-6★ picks are `data/backups.json diy.ownedPool` (72 operators; the collab operators are not included,
the owner's decision of 2026-10-05). The ones still without a kit:
```sh
+6 -6
View File
@@ -1,7 +1,7 @@
// test/backups.test.js — the data of 补位 (stand-ins) and 自选 (DIY slots): chess.json `backup` and data/backups.json
// (docs/DATA.md §18; tools/build-data.mjs buildBackups; shared/standIn.js, shared/diy.js). These tests pin the data the
// gameplay reads: the 133 base chess and their types, the 17 stand-in characters and their forms, every NORMAL chess
// resolving to its stand-in, the 4 DIY slots, the legal picks (the 71 owned 6★ with their forms and summons, the
// resolving to its stand-in, the 4 DIY slots, the legal picks (the 72 owned 6★ with their forms and summons, the
// prototypes and their locked selections) and the faction bonds.
// With the official-data cache (.cache/gamedata) the backup fields, the stand-in numbers and the bond derivation are
// re-derived from the raw tables.
@@ -237,9 +237,9 @@ test('DIY: prototype picks — the 9 elites at tiers 5 and 6, six 4★ reserves
assert.equal(diyRecordOf(chess.chess_char_5_01_a, { charId: 'char_608_acpion' }, data), null, 'not a DIY slot');
});
test('DIY: the 71 owned 6★ picks — a form at every slot status with all three skills, every module at stage 1 and 3, their summons', () => {
test('DIY: the 72 owned 6★ picks — a form at every slot status with all three skills, every module at stage 1 and 3, their summons', () => {
const { ownedPool } = backups.diy;
assert.equal(ownedPool.length, 71);
assert.equal(ownedPool.length, 72);
let summoners = 0;
for (const id of ownedPool) {
const u = backups.units[id];
@@ -311,7 +311,7 @@ test('DIY: prototype picks carry the skill / module of their 补位 rows at the
test('DIY: the owned-6★ pool and the faction → bond rule (mainPower + every subPower vs powerIdList, else 协防干员)', () => {
const { ownedPool, operators, prototypes } = backups.diy;
const roster = new Set(base.map((c) => c.charId).filter(Boolean));
assert.equal(ownedPool.length, 71);
assert.equal(ownedPool.length, 72);
for (const id of ownedPool) {
assert.ok(!roster.has(id), `${id}: a roster operator is never a pick`);
assert.deepEqual([operators[id].rarity, operators[id].obtainable], [6, true], id);
@@ -325,7 +325,7 @@ test('DIY: the owned-6★ pool and the faction → bond rule (mainPower + every
}
const tally = {};
for (const id of ownedPool) { const k = operators[id].bonds.join('+'); tally[k] = (tally[k] || 0) + 1; }
assert.deepEqual(tally, { emptyShip: 38, yanShip: 14, victoriaShip: 8, sargonShip: 4, siracusaShip: 4, lateranoShip: 1, kazimierzShip: 1, 'yanShip+victoriaShip': 1 });
assert.deepEqual(tally, { emptyShip: 38, yanShip: 14, victoriaShip: 8, sargonShip: 4, siracusaShip: 4, lateranoShip: 1, kazimierzShip: 1, egirShip: 1, 'yanShip+victoriaShip': 1 });
// the collab operators are out of the data and the pool (the owner's decision of 2026-10-05: copyright)
assert.deepEqual(backups.diy.excluded.map((id) => [id, backups.units[id], operators[id]]),
['char_456_ash', 'char_1029_yato2', 'char_1048_orchd2', 'char_4123_ela', 'char_4141_marcil', 'char_4182_oblvns', 'char_4217_makoto'].map((id) => [id, undefined, undefined]),
@@ -341,7 +341,7 @@ test('DIY: powers and the owned pool re-derived from character_table', { skip: !
const collab = (c) => [c.mainPower, ...(c.subPower || [])].some((p) => ['rainbow', 'action4', 'mujica', 'sees', 'laios'].includes(p?.teamId))
|| /^(?:MH|RS|AM|PS|DD)\d/.test(c.displayNumber || '');
const legal = Object.entries(CT).filter(([id, c]) => id.startsWith('char_') && c.rarity === 'TIER_6' && !['TOKEN', 'TRAP'].includes(c.profession) && !c.isNotObtainable && !roster.has(id));
assert.equal(legal.length, 78, 'the excel\'s 78 obtainable 6★ outside the chess pool');
assert.equal(legal.length, 79, 'the excel\'s 79 obtainable 6★ outside the chess pool');
assert.deepEqual([...backups.diy.ownedPool].sort(), legal.filter(([, c]) => !collab(c)).map(([id]) => id).sort());
assert.deepEqual([...backups.diy.excluded].sort(), legal.filter(([, c]) => collab(c)).map(([id]) => id).sort());
for (const [id, o] of Object.entries(backups.diy.operators)) {
+2 -2
View File
@@ -35,11 +35,11 @@ test('slots: two per tier (5, 6), each with its elite twin', () => {
assert.equal(diyTokenOwner(SIEGE, chess.chess_char_6_diy1_b.status), 'char_112_siege@2/60/7/3');
});
test('diyPool: tier 5 = 15 prototypes + 71 owned 6★, tier 6 = 9 + 71; no preset, no collab; with the kit registry only kitted operators', () => {
test('diyPool: tier 5 = 15 prototypes + 72 owned 6★, tier 6 = 9 + 72; no preset, no collab; with the kit registry only kitted operators', () => {
const p5 = diyPool(5, { data: DATA }), p6 = diyPool(6, { data: DATA });
assert.deepEqual(p5, [...RESERVES5, ...ELITES, ...ownedPool]);
assert.deepEqual(p6, [...ELITES, ...ownedPool]);
assert.deepEqual([p5.length, p6.length], [86, 80]);
assert.deepEqual([p5.length, p6.length], [87, 81]);
const roster = new Set(Object.values(chess).map((c) => c.charId).filter(Boolean));
for (const id of p5) assert.ok(!roster.has(id) && !COLLAB.includes(id), id);
assert.deepEqual(diyPool(4, { data: DATA }), [], 'no 自选 slot at tier 4');
+3 -3
View File
@@ -62,7 +62,7 @@ test('plan: audio.voiceJp is the JP dub (ArknightsAssets2 voice/) of the very sl
assert.ok(plan({ voiceSlots: null }).audio.voiceJp.char_263_skadi.gacha);
});
test('data/assets.json: voiceJp gives every voiced operator the JP twin of each Chinese line (191 operators, 2674 files); stand-ins, 盟约·辅助干员 and summons have neither', () => {
test('data/assets.json: voiceJp gives every voiced operator the JP twin of each Chinese line (192 operators, 2688 files); stand-ins, 盟约·辅助干员 and summons have neither', () => {
const m = readJson('data/assets.json');
const cn = m.audio.voice;
const jp = m.audio.voiceJp;
@@ -70,13 +70,13 @@ test('data/assets.json: voiceJp gives every voiced operator the JP twin of each
assert.deepEqual(Object.keys(jp), Object.keys(cn), 'the same operators');
assert.equal(m.stats.voiceJpChars, Object.keys(jp).length);
assert.equal(m.stats.voiceChars, Object.keys(cn).length);
assert.equal(Object.keys(jp).length, 191);
assert.equal(Object.keys(jp).length, 192);
// the JP tree is the Chinese one with the folder swapped: same slots, same lines in the same order, same file names
assert.deepEqual(JSON.parse(JSON.stringify(cn).replaceAll('/assets/audio/voice/cn/', '/assets/audio/voice/jp/')), jp);
const lines = [];
const walk = (x) => { if (typeof x === 'string') lines.push(x); else if (Array.isArray(x)) x.forEach(walk); else if (x && typeof x === 'object') Object.values(x).forEach(walk); };
walk(jp);
assert.equal(lines.length, 2674);
assert.equal(lines.length, 2688);
for (const u of lines) assert.match(u, /^\/assets\/audio\/voice\/jp\/char_[^/]+\/cn_\d+\.mp3$/);
// no voice: the 17 stand-ins of data/backups.json (预备干员 / 原型干员), the mode's 盟约·辅助干员, every summon
const backups = readJson('data/backups.json');
+1 -1
View File
@@ -93,7 +93,7 @@ test('every chess, 自选 form and stand-in form: the raw keyframes plus every p
for (const [charId, u] of Object.entries(backups.units)) {
for (const f of Object.values(u.forms)) { check(`${charId}@${f.status.phase}/${f.status.level}`, charId, f.status, f.stats); forms++; }
}
assert.equal(forms, 256);
assert.equal(forms, 259);
// the 原型干员 stand-ins have no potential ranks: their numbers are the plain keyframes
for (const id of ['char_600_cpione', 'char_602_cdfend', 'char_608_acpion', 'char_617_sharp2']) assert.equal((CT[id].potentialRanks || []).length, 0, id);
});
+3 -3
View File
@@ -95,13 +95,13 @@ test('data/i18n/en.json: shape, the 盟约 season of the EN build, coverage and
assert.equal(OVERLAY.version, 1);
assert.equal(OVERLAY.lang, 'en');
assert.equal(OVERLAY.meta.source.season, 'act2autochess', 'built from an EN client that has 卫戍协议:盟约 下半');
// the 自选 data (0.2.0) brings 71 owned 6★, some of them (and some newer modules) not in the EN client yet: their
// names, skills, talents and modules stay Chinese — measured 97.6 % in all, skills 95.9 / talents 95.1 % (2026-10-05)
// 0.2.3 adds Clementia before her EN release: official untranslated names/skills/talents remain Chinese.
// Measured 2026-10-09: total 97.4%, skills 95.9%, talents 94.9%; other category floors stay unchanged.
assert.ok(OVERLAY.meta.totals.pct >= 97, `coverage ${OVERLAY.meta.totals.pct} %`);
for (const kind of ['operators', 'skills', 'talents', 'modules', 'traits', 'enemies', 'bonds', 'items', 'effects', 'bands', 'garrisons', 'stages', 'tokens', 'choices', 'config']) {
const c = OVERLAY.meta.coverage[kind];
assert.ok(c && c.texts > 0, `coverage of ${kind}`);
assert.ok(c.pct >= (['operators', 'skills', 'talents', 'modules'].includes(kind) ? 95 : 98), `${kind}: ${c.pct} %`);
assert.ok(c.pct >= (kind === 'talents' ? 94.9 : ['operators', 'skills', 'modules'].includes(kind) ? 95 : 98), `${kind}: ${c.pct} %`);
}
assert.deepEqual(Object.keys(OVERLAY.files).filter((f) => !DATA_FILES.includes(f)), []);
assert.equal(OVERLAY.names['琳琅诗怀雅'], 'Swire the Elegant Wit');
+1 -1
View File
@@ -102,7 +102,7 @@ test('every chess and 自选 form at every potential: the raw keyframes plus the
forms++;
}
}
assert.equal(forms, 256);
assert.equal(forms, 259);
});
test('talent candidates at every potential: each talent is the last one unlocked with requiredPotentialRank ≤ the rank', { skip: !HAS_CACHE && 'no .cache/gamedata' }, () => {
+2 -1
View File
@@ -247,7 +247,8 @@ test('#16 塞雷娅 S1 急救 (audit): checked at her attack — an ally of her
fill(u);
m.hp = 4000;
h.run(2);
assert.equal(u.skill.activations, 0, 'no enemy, no attack: no cast');
assert.equal(u.skill.activations, 1, 'eligible injured ally supplies the heal attack without an enemy (#406)');
assert.ok(m.hp > 4000, 'the charged attack actually heals');
done(h);
[h, u, m] = setup(true);
const e = h.enemies()[0];