From 686f7ecf37e1b903ebd8f9a6f650a256eacdb0c3 Mon Sep 17 00:00:00 2001 From: sganggs Date: Wed, 7 Oct 2026 01:51:18 +0800 Subject: [PATCH] =?UTF-8?q?feedback5=20CB4:=20three=20community=20reports?= =?UTF-8?q?=20already=20answered,=20pinned=20=E2=80=94=20the=20briefing=20?= =?UTF-8?q?types=20fill=207-9=20of=20the=2013=20normal=20waves,=20at=20mos?= =?UTF-8?q?t=203=20each=20(item=206,=20the=20official=20schedule;=20now=20?= =?UTF-8?q?stated=20in=20docs/PLAYING.md);=20=E6=96=B0=E7=A1=8E's=204=20?= =?UTF-8?q?=E6=96=AD=E5=88=83=20since=20GitHub=20#107=20(item=2053);=20a?= =?UTF-8?q?=20freed=20prisoner=20re-enters=20=E8=81=94=E9=98=B2=20confined?= =?UTF-8?q?=20(item=2057)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No product change. Item 6: 400 seeds per mode, 险境/绝境/终极 7 in 141, 8 in 192, 9 in 67, per type 1-3 (R14/R15 take slots of the 15). Item 53: v0.1.4 stacked a layer per attack (2430 -> 5670), this tree spends one blade per attack and ends the bonus with the 4th (client template enemy_dead_spawn[sfji]). Item 57: a leak re-enters as a new spawn (key, mods, LP, bounty only), confined. Golden unchanged. Tests: test/sim/feedback5-cb4-pins.test.js (2). Docs: DESIGN §25.18.3-§25.18.5, docs/PLAYING.md. --- docs/PLAYING.md | 2 +- docs/history/0.2.0.md | 38 ++++++++++++++- test/sim/feedback5-cb4-pins.test.js | 76 +++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 test/sim/feedback5-cb4-pins.test.js diff --git a/docs/PLAYING.md b/docs/PLAYING.md index 19b12245..f71c6261 100644 --- a/docs/PLAYING.md +++ b/docs/PLAYING.md @@ -16,7 +16,7 @@ | 阶段 | 说明 | |---|---| -| 确认本局信息 | 本局地图(标准模拟固定为「战场#01」;险境从 8 张地图中随机,绝境 / 终极从不含战场#01 的 7 张中随机)、敌方派系、**被禁用的盟约及其干员**、领袖预告。所有人确认后继续;同盟模拟最多 25 秒;只有你一名玩家时(独立模拟,或同盟模拟只带 AI 队友)不限时。 | +| 确认本局信息 | 本局地图(标准模拟固定为「战场#01」;险境从 8 张地图中随机,绝境 / 终极从不含战场#01 的 7 张中随机)、敌方派系(三种特训敌人类型,如折射、飞行:和官方一样,每种占全局 15 个回合位中的 3 个,第 14 回合领袖战和第 15 回合隐秘核心也各占一个位,所以险境 / 绝境 / 终极第 1–13 回合中有 7–9 波是这三种类型、每种最多 3 波,其余各波是「特异」类型;每波只有一种类型)、**被禁用的盟约及其干员**、领袖预告。所有人确认后继续;同盟模拟最多 25 秒;只有你一名玩家时(独立模拟,或同盟模拟只带 AI 队友)不限时。 | | 选择策略 | 40 名策略(分队长)决定**初始目标生命值(20–45)**和一条永久规则。同盟:随机顺序轮流选,**每人 30 秒**,右上角和「决策中」显示同一个倒计时;AI 队友立即选完;队友已选的不能重复,每人可**跳过一次**(排到最后);超时自动选你**当前选中**的策略(已被队友选走则选华法琳)。独立:自由选择、不限时;同盟模拟只有你一名玩家时也不限时。选的时候可以点左侧决策顺序下方的「查看禁用盟约与干员」,再看一遍「确认本局信息」里的盟约(置灰、禁用人数)和本局禁用干员:只能查看,倒计时照常走,底部也显示轮到谁和剩余秒数;点「关闭」、点窗口外面或按 Esc 关闭,轮到下一位时会自动关闭。 | | 机变 | 部分回合开始前出现:同盟共 6 张卡轮流各取 1 张(首位 30 秒,其余 16 秒;只有你一名玩家时不限时),独立 3 选 1。卡片包括装备、资金、干员、盟约层数、悬赏等;**悬赏决策**为你的作战添加悬赏敌人,卡上用蓝字标出持续多久:「下场作战」/「接下来两场作战」的只出现一场或两场,之后不再出现;官方的「多轮悬赏」(原文写「之后的每场作战」)在这里也按「接下来两场作战」处理,只出现两场(如教鞭的法术大师A2·多轮战术特训)。各回合的悬赏决策按官方对局截图来出:第 3 回合是 6 张「接下来两场作战」的悬赏(3 张 1 资金、2 张 2 资金、1 张 3 资金);第 9 回合是各首领的「下场作战」悬赏和「源石虫·特训」;绝境 / 终极的第 11 回合多半是悬赏决策(也可能是机密商店或战术决策),其中的悬赏通常有 1 只特异巨兽,其余是各阵营的「下场战斗」悬赏。击倒悬赏敌人的人(联防中的队友也算)获得资金;右侧效果栏会写「还剩 N 场作战」。**机密商店**的 6 件装备都免费,和官方一样可能出现两件相同的装备(各算一张卡,可以分别被选走);第 11 回合(绝境 / 终极)的机密商店按官方:2 件 VI 阶、至少 1 件 V 阶和 1 枚盟约之币,标准 / 险境较早回合的机密商店各阶装备都可能出现。**战术决策**也和官方一样可能出现两张相同的卡(官方截图里出现过两张「补给」;各算一张卡,可以分别被选走,每张只生效一次,两人各选一张全队卡则效果叠加);第 11 回合(绝境 / 终极)的战术决策按官方截图只出盟誓、驰援、列装 / 财富 / 补给 / 整备 / 升华等增益卡,不出削弱敌人的卡和战场演变卡。每张卡左上是图标和名称,下面写着完整效果(与详情卡相同的官方描述,手机上也显示);已被选走的卡变暗,右上角显示选走它的玩家头像(悬停可看名字)。选卡要**点两次**:第一次只选中(金框 +「确认选择 · 再次点击」),再点同一张或点右上的「确认选择」才生效;点别的卡换选,按 Esc 或点空白处取消。 | | 休整期 | 购买、摆阵、配装备、冻结 / 刷新、升级调度中心;同盟有倒计时(65–215 秒,随回合增加);独立模拟、以及只有你一名玩家(只带 AI 队友)的同盟模拟不限时。所有人「准备就绪」即提前开战,全员就绪前可取消。 | diff --git a/docs/history/0.2.0.md b/docs/history/0.2.0.md index 9fb00296..2a9fc164 100644 --- a/docs/history/0.2.0.md +++ b/docs/history/0.2.0.md @@ -27,7 +27,8 @@ a merged elite 拉普兰德 as a new one (GitHub #169), the leader pool per play model quirks (PR #211) → §25.13; 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; the second batch of community reports of 2026-10-06, stream CB4 (enemies, bosses, waves, movement: the 突袭 landing on the 临时整备区 row and the other half of the -联防 map, a silenced 萨卡兹枯朽战士's death pollution, …) → §25.18. +联防 map, a silenced 萨卡兹枯朽战士's death pollution; the briefing types' 7–9 waves, 新硎's 断刃 and a freed prisoner in 联防 +already answered, …) → §25.18. 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); @@ -1115,3 +1116,38 @@ The real ones are fixed, one commit each with its golden scenarios named; the re `test/content/enemies_bosses.test.js` (the silence rule, the two keys silenced and after a silence ran out; 3 fail on the parent). +#### 25.18.3 「每局游戏开始展示的敌人类型(折射、飞行等等),实际出现波次在7到9波,而且每个最多三波,其余波次用特异类型填充」 (item 6): already the official schedule — no change + +- **Checked**: `setupMatchWaves` (research 08 §2.1, decoded from the client's `RandomEnemyGenerater`): each of the three + briefing types owns 3 of the 15 round slots, SPECIAL (特异) the other 6, shuffled, one type per round. R14 (the leader) + and R15 (the hidden core) take slots too, so the 13 normal rounds of 险境 / 绝境 / 终极 hold 7–9 type waves — 400 seeds + per mode: 7 in 141 matches, 8 in 192, 9 in 67; per type 1–3 — and every other wave is SPECIAL (solo 标准: 8 normal + rounds, 3–7). Exactly what the player describes. +- **Official**: act2autochess `specialEnemyRandomTypeDict` (count 3 per random type, SPECIAL −1), constData + `maxLevelCnt` 15, `specialEnemyNum` 3, `enemyTypeIdentifierToFillRandom` 1 (SPECIAL). +- **Now**: docs/PLAYING.md states it at 确认本局信息 (the report: 「更新日志和机制描述也没写」). +- Golden: unchanged. Pinned: `test/sim/feedback5-cb4-pins.test.js` (the 7–9 and the 1–3), `test/match/waves.test.js` (3 of + the 15 slots each). + +#### 25.18.4 「敌人新硎提升攻击力的断刃好像无限消耗,实际应当攻击一次消耗一个,具体有几个复核」 (item 53): already fixed in 0.2.0 (§25.6.2, GitHub #107) — no change + +- **Checked**: on v0.1.4 新硎's ATK went 2430 → 3510 → 4590 → 5670 and stayed (a layer per attack, none removed) — the + report. On this tree: 4 attacks at 2430 (+80 %), the 4th spends the last 断刃 and ends the bonus, the next swing comes at + once at 1350; killed, max(1, 4 − spent) 铁矛头 (沉沙: +70 %, 铜矛头). +- **Official**: PRTS 天赋 (4 【断刃】, one spent per successful attack); the client's template `enemy_dead_spawn[sfji]` + (battle prefab enemy_1207_sfji_2): ON_OUTPUT_ATK_OR_HEAL — while `cnt` ≥ 1, `cnt` += `cnt_add` (−1) and one blade marker + goes; ON_ABILITY_FINISH — `cnt` ≤ 0 ends the ATK buff and switches to mode 1 (无断刃模式); ON_OWNER_KILLED (not 坠落) — + summons `cnt` (at least 1). +- [ASSUMED]: the client spends a blade per damage output; here every attack spends one, so an attack an operator dodges + spends one too. +- Golden: unchanged. Pinned: `test/content/enemies_bosses.test.js` (the #107 test). + +#### 25.18.5 「囚犯敌人的解放状态联防时不应继承」 (item 57): already answered (§25.14.1) — no change + +- **Checked**: a leak re-enters 联防 as a new spawn — `planUnite` passes on its key, spawn mods, LP and bounty only, and + `buildUniteWave` spawns it — so `prisoner()` starts it confined (the 禁锢 buff, no form) and only its own attacks there + free it; the client drops every view when a battle starts. The freed look was 普通 / 老练囚犯's red clip set, drawn from + the gate until §25.14.1. +- Golden: unchanged. Pinned: `test/sim/feedback5-cb4-pins.test.js` (freed in its own combat, leaked, confined in 联防, + freed at its 4th attack there). + diff --git a/test/sim/feedback5-cb4-pins.test.js b/test/sim/feedback5-cb4-pins.test.js new file mode 100644 index 00000000..e962ff4e --- /dev/null +++ b/test/sim/feedback5-cb4-pins.test.js @@ -0,0 +1,76 @@ +// test/sim/feedback5-cb4-pins.test.js — community reports of 2026-10-06 that this tree already answers, pinned (DESIGN +// §25.18; they pass on the parent commit — nothing changed for them): +// item 6 「每局游戏开始展示的敌人类型(折射、飞行等等),实际出现波次在7到9波,而且每个最多三波,其余波次用特异类型填充」 — the +// official type schedule (client RandomEnemyGenerater, research 08 §2.1; act2autochess specialEnemyRandomTypeDict count 3, +// constData maxLevelCnt 15 / specialEnemyNum 3 / enemyTypeIdentifierToFillRandom 1): each briefing type owns 3 of the +// 15 round slots, SPECIAL (特异) the other 6, shuffled — R14 (leader) and R15 (hidden core) take slots too, so the 13 +// normal rounds hold 7–9 type waves, at most 3 per type; +// item 57 「囚犯敌人的解放状态联防时不应继承」 — a leaked prisoner re-enters 联防 as a new spawn (unite.js planUnite → +// waves.js buildUniteWave: its key and spawn mods only), confined again (archetypes.js prisoner spawn); the freed look +// the report saw was 普通 / 老练囚犯's red clip set drawn from the gate, fixed with the prisoners' forms (§25.14.1). +// Run: node --test test/sim/feedback5-cb4-pins.test.js + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.js'; +import { GameData } from '../../server/match/gamedata.js'; +import { setupMatchWaves, buildUniteWave } from '../../server/match/waves.js'; +import { planUnite } from '../../server/match/unite.js'; +import { createRng } from '../../server/sim/rng.js'; +import { DATA } from '../match/harness.js'; + +test('item 6: the three briefing types fill 7–9 of the 13 normal waves (险境 / 绝境 / 终极), at most 3 each; the rest are SPECIAL', () => { + for (const modeId of ['mode_multi_normal', 'mode_single_hard', 'mode_multi_abyss']) { + const gd = new GameData(DATA, modeId); + assert.equal(gd.bossRound, 14); + const totals = new Set(); + for (let seed = 1; seed <= 200; seed++) { + const s = setupMatchWaves(gd, createRng(seed)); + const normal = s.typeSlots.slice(0, gd.bossRound - 1); + const per = s.factions.map((f) => normal.filter((t) => t === f).length); + for (const n of per) assert.ok(n >= 1 && n <= 3, `${modeId} seed ${seed}: ${per}`); + const total = per.reduce((a, b) => a + b, 0); + assert.ok(total >= 7 && total <= 9, `${modeId} seed ${seed}: ${total}`); + assert.equal(normal.filter((t) => t === 'SPECIAL').length, normal.length - total, 'every other wave is SPECIAL'); + totals.add(total); + } + assert.deepEqual([...totals].sort(), [7, 8, 9], `${modeId}: all three totals occur`); + } +}); + +test('item 57: a prisoner freed in its own combat leaks and re-enters 联防 confined (no freed form, the confinement buff, freed only by its own attacks there)', () => { + const KEY = 'enemy_1116_liprr'; // 普通囚犯 + const wall = chessRec({ id: 't_wall', profession: 'TANK', stats: { atk: 0, maxHp: 1e9, def: 0, blockCnt: 3 }, rangeGrid: [[0, 0]], skill: null }); + const kits = { t_wall: () => ({ trait: { noAttack: true } }) }; + const h = makeBattle({ autoFinish: false, timeLimit: 120, defs: { chess: { t_wall: wall } }, kits, units: [{ chessId: 't_wall', row: 9, col: 6 }], enemies: [{ key: KEY, route: 0 }] }); + assert.ok(h.runUntil(() => h.enemy(KEY) && h.enemy(KEY).form === 'liberty', 60), 'freed by its own attacks'); + const p = h.enemy(KEY); + assert.ok(p.findBuff('ab:liberty') && !p.findBuff('ab:confined')); + h.b.retreat(h.unit('t_wall')); + assert.ok(h.runUntil(() => !p.alive, 60), 'it walks on and leaks'); + const r = h.result().perPlayer.p1; + assert.deepEqual(r.leaked.filter((l) => l.counted !== false).map((l) => l.enemyKey), [KEY]); + checkInvariants(h.b); + // 联防: the leaker's counted leak re-enters on the escaped template of one helper + const gd = new GameData(DATA, 'mode_multi_normal'); + const seat = (id, s) => ({ playerId: id, seat: s, deployCount: 1, bonds: {}, layers: {}, board: new Map(), bounties: [] }); + const m = { isSolo: false, alivePlayers: () => [seat('p1', 1), seat('H', 0)], gd: { unite: { maxHelpers: 2 }, enemy: (k) => gd.enemy(k) } }; + const plan = planUnite(m, new Map([['p1', r], ['H', { perfect: true, leaked: [], unitsEnd: [] }]])); + assert.deepEqual(plan.leaked.map((l) => l.enemyKey), [KEY]); + assert.deepEqual(Object.keys(plan.leaked[0]).sort(), ['bounty', 'enemyKey', 'lpr', 'mods', 'sourcePlayerId', 'tag'], 'nothing of its state travels'); + const wave = buildUniteWave(gd, plan.leaked, 1, 60); + const u = makeBattle({ + kind: 'unite', stageId: 'act1autochess_escaped_single', autoFinish: false, timeLimit: 60, routes: wave.routes, + defs: { chess: { t_wall: wall } }, kits, units: [{ chessId: 't_wall', row: 9, col: 8 }], + enemies: wave.spawns.map((s) => ({ key: s.enemyKey, time: s.time, route: s.routeIndex, mods: s.mods, sourcePlayerId: s.sourcePlayerId })), + }); + assert.ok(u.runUntil(() => u.enemy(KEY), 30), 're-entered'); + const q = u.enemy(KEY); + assert.equal(q.form ?? null, null, 'drawn confined'); + assert.ok(q.findBuff('ab:confined') && !q.findBuff('ab:liberty'), 'confined again'); + assert.ok(u.runUntil(() => q.stats.attacks >= 1, 40), 'blocked, it attacks'); + assert.equal(q.form ?? null, null, 'still confined after its first 联防 attack'); + assert.ok(u.runUntil(() => q.form === 'liberty', 60), 'freed again only by its own attacks there'); + assert.equal(q.stats.attacks, 4, 'at its 4th attack in 联防 (confinement.times), as in a fresh spawn'); + checkInvariants(u.b); +});