diff --git a/README.md b/README.md index bec8a29e..5cbb23ac 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # 卫戍协议:盟约 · Stronghold Protocol: Alliance +> 开发版(dev 分支):不稳定,请勿用于公开服务器。最近正式版请从 Releases 下载。 + 《明日方舟》季节性自走棋塔防玩法「卫戍协议:盟约」的**非官方同人复刻**:浏览器即开即玩,单人或 1–4 人联机合作。 ![version](https://img.shields.io/badge/version-0.2.3--dev-2ea44f) diff --git a/docs/history/0.2.3.md b/docs/history/0.2.3.md index 4a26fff1..6048c88d 100644 --- a/docs/history/0.2.3.md +++ b/docs/history/0.2.3.md @@ -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. diff --git a/public/js/render/fx/kinds.js b/public/js/render/fx/kinds.js index b721a01a..6b14f9a4 100644 --- a/public/js/render/fx/kinds.js +++ b/public/js/render/fx/kinds.js @@ -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 }, diff --git a/server/sim/content/kits/README.md b/server/sim/content/kits/README.md index d1988a0f..3fee2fd6 100644 --- a/server/sim/content/kits/README.md +++ b/server/sim/content/kits/README.md @@ -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 diff --git a/test/backups.test.js b/test/backups.test.js index 3f98b320..0890b9ba 100644 --- a/test/backups.test.js +++ b/test/backups.test.js @@ -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)) { diff --git a/test/diy.test.js b/test/diy.test.js index 81f14f40..86632d38 100644 --- a/test/diy.test.js +++ b/test/diy.test.js @@ -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'); diff --git a/test/feedback7-voices.test.js b/test/feedback7-voices.test.js index 8b8fbac9..ded52f35 100644 --- a/test/feedback7-voices.test.js +++ b/test/feedback7-voices.test.js @@ -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'); diff --git a/test/full-potential.test.js b/test/full-potential.test.js index 50202a8c..791625e5 100644 --- a/test/full-potential.test.js +++ b/test/full-potential.test.js @@ -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); }); diff --git a/test/i18n-data.test.js b/test/i18n-data.test.js index a7cd29d6..bc638561 100644 --- a/test/i18n-data.test.js +++ b/test/i18n-data.test.js @@ -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'); diff --git a/test/potential.test.js b/test/potential.test.js index 66cb8fc3..a40eb3db 100644 --- a/test/potential.test.js +++ b/test/potential.test.js @@ -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' }, () => { diff --git a/test/sim/playtest6-skills.test.js b/test/sim/playtest6-skills.test.js index d42e952c..5a8e7a6a 100644 --- a/test/sim/playtest6-skills.test.js +++ b/test/sim/playtest6-skills.test.js @@ -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];