From 2767adccc0362b00f422cd8f3e9852b2b9a15379 Mon Sep 17 00:00:00 2001 From: sganggs Date: Tue, 6 Oct 2026 22:05:47 +0800 Subject: [PATCH] =?UTF-8?q?feedback5=20WAA:=20DESIGN=20=C2=A725.14=20?= =?UTF-8?q?=E2=80=94=20the=20four=20community=20reports=20of=202026-10-06?= =?UTF-8?q?=20(two=20fixed,=20=E5=BF=8D=E5=86=AC's=20trigger=20and=20?= =?UTF-8?q?=E6=9E=AF=E6=9C=BD=E4=B9=8B=E7=A7=8D's=20bounty=20already=20ans?= =?UTF-8?q?wered=20and=20pinned)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/history/0.2.0.md §25.14 (also scratchpad draft 0.2.0.aa.md) in the §25 style: the prisoners' models and timing (§25.14.1), 忍冬 — her default S3 has cast on its 2-2 running range since §25.5's ACTIVE_RANGE (0.1.4 waited for her 1-tile range; §25.14.2), a bounty burned to death (§25.14.3), 枯朽萃聚使徒's seeds — fixed by 0.1.3 §23.25, every leaked seed paid the card again in 联防 on v0.1.2 (§25.14.4); the §25 intro list, the owner's decisions, the §25.10 table, what stays [ASSUMED] (the 联防 payee) and the open polish (enemy prefabs whose default clips differ from the manifest: 猎狗pro, 大鲍勃, 碎骨, “斩胄之剑” / “破胄之锤”). - test/sim/feedback5-community-pins.test.js pins the two reports this tree already answers (they pass on the parent): 忍冬 S3 casts with an enemy two tiles ahead and not three, S2 on its 3-12, S1 on her attack; a bounty 使徒's seeds carry no bounty and pay nothing, the body pays once, and in 联防 only its own leak carries the card. - Golden unchanged. --- docs/history/0.2.0.md | 92 ++++++++++++++++++++++- test/sim/feedback5-community-pins.test.js | 75 ++++++++++++++++++ 2 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 test/sim/feedback5-community-pins.test.js diff --git a/docs/history/0.2.0.md b/docs/history/0.2.0.md index ca399954..541d64b4 100644 --- a/docs/history/0.2.0.md +++ b/docs/history/0.2.0.md @@ -21,7 +21,8 @@ rules (GitHub #181, #187 / #170, follow-ups) → §25.7; WE2, data / match / UI what stays [ASSUMED] and the questions left to the owner → §25.10. The closing additions: the owner's decisions of 2026-10-06 (望's stock, skill triggers through summon areas, the 突袭 landing, GitHub #148) and the items the sources settled (泥岩's 闭锁; 忍冬, 深靛, 阿) → §25.11; release packaging and the asset manifest (the 自选 voices, the summons -with no Spine model) → §25.12. +with no Spine model) → §25.12; the community reports of 2026-10-06 (the 孤岛风云 prisoners drawn freed, 忍冬's trigger, a +bounty burned to death by the 活性源石, 枯朽之种 paying a bounty) → §25.14. Decisions of the owner: 2026-10-04 — the server stays on plain `node:http` + `ws`, no web framework (§25.1); 2026-10-05 — the 补位 plan (§25.3); the 自选编队 rules, the collab operators out of the pool, ISW-A refused (§25.4); @@ -30,7 +31,8 @@ Chinese by default with an English switch, official English game texts, PR #70's enemy in range, 纯烬艾雅法拉 S1 at full SP, 伺夜 S1 only with his pack on the field (§25.5); 2026-10-06 — a chess the player does not own shows as its stand-in everywhere (the owner's recall of the official mode, §25.3); the enemy damage frame kept (§25.7.2); 望's placed 棋子 count in her stock, skills that act through summons cast on an enemy in a summon's area, -a melee 突袭 member lands where it can block (§25.11). +a melee 突袭 member lands where it can block (§25.11); the bounty pays whenever its enemy dies (relayed with the community +reports, §25.14.3). ### 25.1 Refactor and tooling: the big files split, golden results, lint, types, import boundaries (refactor plan; owner's decision of 2026-10-04: no web framework) — `server/http/`, `server/match/match/`, `server/match/player/`, `server/sim/battle/`, `content/enemies/`, `kits/ops/`, `public/js/ui/gameLogic/`, `render/fx/`, `render/app/`, `screens/game/`, `tools/golden.mjs`, `tools/check-imports.mjs`, `eslint.config.js`, `jsconfig.json` @@ -658,6 +660,7 @@ its tile, §18.1; §25.9.2). Where a 0.1.4 change met 0.2.0 work: | 望's placed 棋子 take and return her stock, so S1 / S2 keep cycling (§25.11.1) | the owner, 2026-10-06 (deliberate deviation from 「不消耗持有数量」) | | Skills that act through summons cast on an enemy in a summon's area: 麦哲伦 S1, 令 S3, 电弧 S2 / S3 (§25.11.2) | the owner, 2026-10-06 (the larger-range rule) | | A melee 突袭 member lands on a tile where it can block before one where it cannot (GitHub #148, §25.11.3) | the owner, 2026-10-06 | +| A bounty pays whenever its enemy dies — terrain and other 无来源 deaths included, in 联防 too (§25.14.3) | the owner, 2026-10-06 (relayed with the community reports) | Settled by the sources in W5V, no longer questions: 泥岩 S3's dormancy is 闭锁 — she lets go and blocks nobody (PRTS, §25.11.4); 阿 S2 / S3 keep the data's DEFAULT, 忍冬 S2's and 深靛 S1's notes are what the kits do (§25.11.5). @@ -670,7 +673,8 @@ the moment a 棋子 leaves, a 棋子 over the cap lost, TRP-X's 7th placed 棋 redeployment; 电弧 S2's area = 赛柯's 3-2 range, standing summons only; the official 突袭 landing taken as random among the legal tiles, ranged members on the old order, the target's own tile counted as a blocking tile; 泥岩's 反制 during 闭锁 not modelled; the English of the remake's own strings; the lite zip without `public/fonts`, the zips' unchanged -package.json (maintainer scripts fail there); the stand-in and operator kit items of the tables above. +package.json (maintainer scripts fail there); the 联防 payee of a bounty death no operator caused — the helper of the half +it fell on (§25.14.3); the stand-in and operator kit items of the tables above. Left to the owner (none changed in 0.2.0): 淬羽赫默's 丰润羽翼 as 生命回复速度 (the client template, no PRTS note) and 遥's break heal on the ATK cached at the grant (both kept, §25.11.5); whether a passive counts as 技能就绪 for the whole @@ -682,7 +686,8 @@ sources: the ally condition for 阿 S2 / S3, 泥岩 S3's 闭锁. Open polish (no rule): 34 or more of the 38 summons of the 自选 kits have no Spine skeleton in the online sources, so the field draws them as the avatar placeholder (§25.12); `scripts/make-windows-bundle.mjs` still copies every tracked file but test/ (it could reuse the package selection); the THIRD-PARTY-NOTICES "In the release bundle" column does not tell -the lite zip (no fonts) apart. +the lite zip (no fonts) apart; the enemy prefabs whose default clips differ from the manifest's roles (猎狗pro, 大鲍勃, +碎骨, “斩胄之剑” / “破胄之锤”, §25.14.1). ### 25.11 The owner's decisions of 2026-10-06 and the items the sources settled (W5V) — `kits/ops/op-wang.js`, `kits/shared/summoner.js summonTriggerArea`, `content/bonds/addon/battle.js raidTile`, `kits/ops/chess_char_4_18-mudrok.js` @@ -829,3 +834,82 @@ answers and checked the remaining items against PRTS and the client (W5V's commi (no `diy.excluded` operator keeps art; one lower-case module icon per type; no two manifest URLs differ only in case; orphans compared case-insensitively), `test/assets.test.js` (the empty-array rule). Docs: docs/DEPLOY.md §7, the README download section (full and lite), docs/ASSETS.md. + +### 25.14 Community reports of 2026-10-06 (W5AA) — `content/enemies/archetypes.js prisoner`, `render/units.js FORMS`, `tools/assets/spine.mjs PREFAB_SPINE_ROLES`, `sim/battle/economy.js _bountyPayee` + +The owner collected four community reports on 2026-10-06. Each was reproduced on the 0.2.0 tree with a script (and, +where the report could be older, on the v0.1.2 / v0.1.4 sources), checked against the official data — the battle +prefabs of the local client, ArknightsGameData's buff templates, PRTS — and the earlier decisions. Two were real and are +fixed (W5AA's commits name every golden scenario moved); two were already fixed and are pinned. + +#### 25.14.1 「囚徒类敌人出门就是已解放的模型」: the 孤岛风云 prisoners leave the gate confined and change model as they are freed — `content/enemies/archetypes.js prisoner`, `render/units.js FORMS`, `render/fx/kinds.js SILENT_PHASES`, `tools/assets/spine.mjs PREFAB_SPINE_ROLES` + +- **Seen**: 普通囚犯 / 老练囚犯 walked out of the gate drawn freed (a red collar light); no prisoner changed its model when + freed (强壮囚犯, 拳师囚犯, 重犯 and 传奇重犯 stayed grey all battle), on the 2D and the 3D board alike. +- **Official**: each prisoner skeleton holds three clip sets whose collar light is grey, blinking orange and red. The + battle prefabs (local client `battle/enm_pfb_6.ab`, read with UnityPy: the Graphic component's anim key → clip table, + the modes' replace pairs, the clip the Spine starts on) draw mode Default — 【禁锢】 — on the grey set and start on it, + mode R on the orange set and mode L — 【解放】 — on the red one. The buff templates (ArknightsGameData + `battle/buff_template_data.json`): `enemy_confinement` starts `confinement.times` charges; `enemy_confinement[atk_cnt]` + spends one ON_BEFORE_ATTACK unless the prisoner has 麻痹 (PRTS 拳师囚犯 「在持有麻痹的情况下,攻击不计数」) and switches to + mode R when one is left; `enemy_liberty_listener[warning]` switches to mode L before the next attack — PRTS + 「进行第4次攻击前,切换至解放状态」. +- **Cause**: the client's name-based role resolver gave 普通 / 老练囚犯 their unnumbered clips, which are the red (freed) + set — their grey set is `*3`; FORMS had no prisoner entry and the sim sent no form. The sim also counted on the + 'attack' hook, after the damage, so the 4th attack still hit confined. +- **Now**: `prisoner()` counts in the `before` hook (an attack 麻痹 interrupts never reaches it), sets the form + 'warning' before attack times − 1 (fx 'phase', drawn silently — the blinking light is the model's) and 'liberty' + before attack times (fx 'liberate', the white blast as before), so that attack already hits with the liberty bonus; + 重犯's free-all and 杰斯顿's knock-out switch every freed prisoner too, and UnitInfo `form` gives a view built later the + right set. FORMS maps 'warning' / 'liberty' for the six skeletons (普通 / 老练囚犯 `*2` / unnumbered, 强壮囚犯 `*2` / + `*3`, 拳师囚犯 / 重犯 / 传奇重犯 `*_orange` / `*_red`). `PREFAB_SPINE_ROLES` gives 普通 / 老练囚犯 their grey `*3` set + as the manifest's roles: data/assets.json regenerated (`fetch-assets --offline --add-only`, JSON compare: those two + `anims` and the hash), data/enemies.json too (`build-data --offline`: their `attackAnim.clip` Attack → Attack3, the + same length and strike frame). The 3D board draws the same Pixi views, so both boards follow. +- [ASSUMED]: none (the modes and their timing come from the prefabs and the buff templates). +- Found by the same audit (all 242 enemy prefabs against the manifest's roles), not changed, for later: 猎狗pro moves on + `Run_Loop` (manifest `Move_Loop`), 大鲍勃 attacks on `Attack2`, 碎骨 moves on `Move_02`, “斩胄之剑” / “破胄之锤” idle on + `Idle_A` (a 0 s pose the resolver skips on purpose); 杰斯顿, 扎罗 and 再生 differ only in the death clip their FORMS + already set. +- Golden: 28 scenarios — the warning's 'phase' fx only: roster-001 / 003 / 007 / 024 / 033, bond-miraShip-low; the freed 4th + attack: roster-004 / 021 / 022 / 026, bond-kazimierzShip-low / raidShip-low / soloShip-low / victoriaShip-low / + visiShip-low, standin-01, diy-017 / 024 / 061 / 065 / 066 / 094 / 097 / 104 / 105 / 127, and the bot matches + solo-ABYSS-2, solo-HARD-9-boosted. Tests: `test/content/feedback5-prisoners.test.js`, + `test/render/feedback5-prisoners.test.js`, `test/render/feedback5-prisoners.browser.test.js` (RENDER_E2E=1, both boards). + Docs: docs/ASSETS.md, docs/SIM.md, DESIGN §9 (design/client.md). + +#### 25.14.2 「敌人在忍冬技能范围内没有开技能,在攻击范围才开」: already answered by the larger-range rule (§25.5) — no change + +- **Checked**: data/chess.json — S1 小施惩戒 DEFAULT (AUTO, no larger range), S2 坠刃拷问 SKILL_RANGE on its 技能范围 3-12 + (§25.11.5), S3 隐狐之艺 (her default) ACTIVE_RANGE on its running range 2-2 (`rawRule` DEFAULT); she is in neither + part of data/backups.json. On v0.1.4 S3 was on DEFAULT and cast only with an enemy in her 1-tile range — the report; + on this tree it casts with an enemy two tiles ahead (not three), S2 on any tile of the 3-12. +- Golden: unchanged. Pinned: `test/sim/feedback5-community-pins.test.js`. + +#### 25.14.3 「被源石地板烫死的悬赏没给赏金」: a bounty enemy that dies with no operator behind it pays in 联防 too — `sim/battle/economy.js _bountyPayee`, `sim/battle/deploy.js _remove` + +- **Seen**: a bounty enemy burned to death by the 活性源石 paid no bounty. +- **Checked**: on its own field it always paid its owner — all 96 walking kill-bounty enemies on 战场#04 with no + operator, on this tree and on v0.1.4: none unpaid. The loss is in 联防: a leaked bounty enemy re-enters carrying + `bounty.ownerPlayerId` = the leaker (unite.js planUnite), and a death without an operator killer fell back to that + owner, who has no per-player entry in the 联防 battle, so `addCoins` dropped the coins. Until 0.1.3 the 联防 field was + the round's map (GitHub #41), so 战场#04's 活性源石 burned leaked bounty enemies for nothing; the 联防 maps have no + terrain now, but a 无来源 tick, the enemy's own HP loss or another enemy's hit still paid nobody. +- **Official**: PRTS 卫戍协议:盟约 决策 「该敌人于对应玩家所属区域倒下时,使相应玩家获得额外资金」 — no exception for 环境伤害; + the owner's decision (relayed 2026-10-06): the bounty pays whenever the body dies. +- **Now**: `_bountyPayee` — an operator or summon of a player in the battle that dealt the blow pays its player (as + before, a 联防 helper included); any other death pays the card's owner when that player fights there (own field, Final + Assault / Hidden Core: unchanged), else the player whose half of the field the enemy fell on (`_ownerForTile`). +- [ASSUMED]: the 联防 payee of a death nobody caused (no source names one; PRTS's 「于…区域倒下」 reads as the area's + player). +- Golden: unchanged. Tests: `test/sim/feedback5-bounty-payee.test.js` (the 联防 cases fail on the parent; the own-field + 活性源石 death, a helper's operator on the other half and a pair boss field pinned). Docs: docs/SIM.md §1.2, + docs/META.md, docs/PLAYING.md. + +#### 25.14.4 「悬赏敌人枯朽萃聚使徒召唤的枯朽之种被击杀也会获得赏金」: already fixed in 0.1.3 (§23.25) — no change + +- **Checked**: the 使徒's BornBugs spawn its seeds through `spawnChildren`, which drops the card's `bountyId` / + `bountyCoins` (§23.25); a seed never had a `bounty` on the own field, so killing one never paid. On the v0.1.2 sources a + bounty 使徒 whose seeds leaked sent every seed into 联防 carrying the card — 15 of 16 re-entries paid it again — which + is the report; on this tree only the 使徒's own leak carries it. +- Golden: unchanged. Pinned: `test/sim/feedback5-community-pins.test.js`. diff --git a/test/sim/feedback5-community-pins.test.js b/test/sim/feedback5-community-pins.test.js new file mode 100644 index 00000000..a909ced8 --- /dev/null +++ b/test/sim/feedback5-community-pins.test.js @@ -0,0 +1,75 @@ +// test/sim/feedback5-community-pins.test.js — two community reports of 2026-10-06 that this tree already answers, pinned +// (DESIGN §25.14; they pass on the parent commit — nothing changed for them): +// 「敌人在忍冬技能范围内没有开技能,在攻击范围才开」 — 0.1.4 cast her default S3 隐狐之艺 on its official DEFAULT row, i.e. +// only with an enemy in her 1-tile attack range; 0.2.0's ACTIVE_RANGE (the owner's decision of 2026-10-05, §25.5) +// casts it with an enemy inside its running range 2-2. S2 坠刃拷问 casts on anything in its 技能范围 3-12 +// (SKILL_RANGE), S1 小施惩戒 (AUTO, no larger range) on her attack. +// 「悬赏敌人枯朽萃聚使徒召唤的枯朽之种被击杀也会获得赏金」 — the seeds come from spawnChildren, which drops the card's +// bountyId / bountyCoins (0.1.3 §23.25, GitHub #67 / #89-2); killing them never paid on the own field, and since 0.1.3 +// a leaked seed no longer re-enters 联防 carrying the card (≤ 0.1.2 every one paid it again there). +// Run: node --test test/sim/feedback5-community-pins.test.js + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js'; +import { getDefaultSource } from '../../server/sim/simdata.js'; +import { planUnite } from '../../server/match/unite.js'; + +const ds = getDefaultSource(); +const done = (h) => { checkInvariants(h.b); assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); }; +const dummy = (key) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0 }); + +test('忍冬: S3 casts with an enemy in its 2-2 running range (not only in her attack range), S2 on its 3-12, S1 on her attack', () => { + const casts = (id, skillId, pos) => { + const index = ds.rawChess(id).skills.find((s) => s.skillId === skillId).index; + const h = makeBattle({ seed: 3, autoFinish: false, timeLimit: 60, defs: { enemies: { e: dummy('e') } }, units: [{ chessId: id, row: 10, col: 4, skillIndex: index }] }); + h.step(); + const u = h.unit(id); + u.skill.gainSp(999, 'test'); + h.spawn('e', { pos }); + h.run(3); + done(h); + return u.skill.activations; + }; + for (const id of ['chess_char_3_18_a', 'chess_char_3_18_b']) { + assert.deepEqual(ds.rawChess(id).rangeGrid, [[0, 0], [0, 1]], `${id}: her own range is 1 tile ahead`); + assert.ok(casts(id, 'skchr_vulpis_3', [10, 6]) >= 1, `${id} S3: an enemy 2 tiles ahead`); + assert.equal(casts(id, 'skchr_vulpis_3', [10, 7]), 0, `${id} S3: not 3 tiles ahead`); + for (const pos of [[10, 6], [10, 7], [9, 5]]) assert.equal(casts(id, 'skchr_vulpis_2', pos), 1, `${id} S2 (3-12): ${pos}`); + assert.equal(casts(id, 'skchr_vulpis_1', [10, 6]), 0, `${id} S1: not out of her attack range`); + assert.ok(casts(id, 'skchr_vulpis_1', [10, 5]) >= 1, `${id} S1: on her attack`); + } +}); + +test('枯朽萃聚使徒 as a bounty: its 枯朽之种 carry no bounty and pay nothing; the body pays once; in 联防 only its own leak carries the card', () => { + const card = { id: 'enemyeffect_10_8', card: { payout: 'kill', coin: 2, enemyKey: 'enemy_1321_wdarft' } }; + const h = makeBattle({ + autoFinish: false, timeLimit: 200, + enemies: [{ key: 'enemy_1321_wdarft', route: 2, mods: { hpMul: 1, bountyId: card.id }, bounty: { coins: 2, ownerPlayerId: 'p1' }, tag: 'bounty', ownerPlayerId: 'p1' }], + }); + assert.ok(h.runUntil(() => h.b.enemies.some((e) => e.alive && e.defId === 'enemy_1269_nhfly'), 30), 'BornBugs cast'); + const seeds = h.b.enemies.filter((e) => e.alive && e.defId === 'enemy_1269_nhfly'); + for (const s of seeds) { + assert.equal(s.bounty, null); + assert.ok(!s.mods || (s.mods.bountyId == null && s.mods.bountyCoins == null)); + h.b.kill(s, null); + } + assert.deepEqual(h.eventsOf('bounty'), [], 'the seeds pay nothing'); + h.b.kill(h.enemy('enemy_1321_wdarft'), null); + assert.deepEqual(h.eventsOf('bounty'), [['bounty', 'p1', 2]], 'the body pays once'); + done(h); + // 联防: the 使徒 and its seeds leak (no operator); only the 使徒 re-enters with the card's bounty + const h2 = makeBattle({ + autoFinish: true, timeLimit: 200, + enemies: [{ key: 'enemy_1321_wdarft', route: 2, mods: { hpMul: 1, bountyId: card.id }, bounty: { coins: 2, ownerPlayerId: 'p1' }, tag: 'bounty', ownerPlayerId: 'p1' }], + }); + h2.runToEnd(200); + const r = h2.result().perPlayer.p1; + assert.ok(r.leaked.filter((l) => l.enemyKey === 'enemy_1269_nhfly').length >= 3, 'seeds leaked'); + const seat = (id, s, o = {}) => ({ playerId: id, seat: s, deployCount: o.deployCount ?? 1, bonds: {}, layers: {}, board: new Map(), bounties: o.bounties ?? [] }); + const leaker = seat('p1', 1, { bounties: [card] }); + const helperSeat = seat('H', 0, { deployCount: 3 }); + const m = { isSolo: false, alivePlayers: () => [leaker, helperSeat], gd: { unite: { maxHelpers: 2 }, enemy: (k) => ({ key: k }) } }; + const plan = planUnite(m, new Map([['p1', r], ['H', { perfect: true, leaked: [], unitsEnd: [] }]])); + assert.deepEqual(plan.leaked.filter((l) => l.bounty).map((l) => [l.enemyKey, l.bounty.coins]), [['enemy_1321_wdarft', 2]]); +});