feedback5: merge CB2 — community reports (healers / summons): 刺玫's S2 heals the most injured ally again (the counter's heal alone goes to her protégé), 琳琅诗怀雅 heals herself and her 破财消灾 cost restarts at 5 DP with every deployment, 赫默's S2 casts at full SP so the drone takes the field at once, the countdown summons (医疗探机, 诅咒娃娃, 海嗣 and four 自选 ones) take no damage, are never healed and show the life left on their bar, stacked ground areas stay readable; §25.16 after CB5's §25.19 (renumbered with the later merges); golden: vs feedback5 CB2's own rules — roster-001/002/003/005/008/016/018/027/035/037/045/046/049, bond-deputShip-low/high, bond-suntShip-high, bond-victoriaShip-low/high, bond-visiShip-low/high, boss-boss_1/3/5/7-pair, hidden-boss_9-pair, unite-1, matches solo-FUNNY-2, coop2-HARD-4, coop3-ABYSS-5, coop4-ABYSS-7, coop2-NORMAL-8-serverrun, coop2-ABYSS-10-boosted, coop2-NORMAL-14-standins; vs CB2 exactly CB5's 18 bot matches
Merge fixes (the three tests the trial merge failed): - test/content/summon_loadout_conflicts.test.js (赫默's drone lifetime per module variant): it asserted "no cast yet" after the drone's redeploy time and then cast S2 by hand — with nobody hurt the old DEFAULT rule never cast. S2 now casts by itself the tick its SP is full (22 s), which is the official rule (client skill skchr_silent_2: no `_trigger`, `_allowNoTarget` 1, `_checkHasTargetBeforeDoCast` 0; CB264f8bf9), so the test waits for that cast and checks both drones' lifetimes from it. Failed on fb5-cb2 itself (the stream did not run the file). - test/sim/feedback1f-downed-tile.test.js (F5 rule 3): its "killed" case killed 浊心斯卡蒂's 海嗣 with 1e9 true damage; the 海嗣 is a countdown summon now — 无敌 + 禁疗, the official state (PRTS 海嗣 备注 「持有禁疗、无敌」) — so it only expires. The killed case takes 缪尔赛思's 流形 (a placed board summon that is attacked and killed and also comes back on its tile 25 s later); both cases now also check the summon's return. Failed on fb5-cb2 itself. - test/golden-standins.test.js (the 补位 match fields at least 3 different stand-ins): already failing on feedback537558f0— CB5's 调度中心 upgrade card changed the human seat's shop in coop2-NORMAL-14-standins, which then fielded only 2 of its 7 not-owned chess (忍冬 → Sharp, 安洁莉娜 → Raidian). tools/golden.mjs MATCH_NOT_OWNED adds 缄默德克萨斯 and 铃兰 (chess its AI fields now): 4 stand-ins (Sharp from round 4, Misery from 7, 预备干员-辅助 from 9, Raidian in 12); the test is unchanged, test/golden/README.md says 9 chess. This moves coop2-NORMAL-14-standins only. - docs/history/0.2.0.md: the §25 intro's two stream lines joined into one sentence (the merge had cut it). Golden moves outside CB2's commit list are CB2's rules on the paths CB5 gave the bots: coop2-HARD-4 fields 琳琅诗怀雅 from round 6, coop2-NORMAL-8-serverrun 刺玫 from round 7, coop2-ABYSS-10-boosted 刺玫 from round 2, coop2-NORMAL-14-standins 刺玫 from round 3 and 琳琅诗怀雅 in rounds 6-7 (instrumented); coop2-NORMAL-3 and coop4-FUNNY-6 (in CB2's list) still field 刺玫 and her S2 heals now reach other allies (36 / 205 heals), but no kill, leak, LP or end stat changes, so their digests stay. Gate: non-browser suites 4949 / 4949, npm run golden ok (283 scenarios), test/golden.test.js 9 / 9, lint 0 errors, check:imports ok, typecheck ok.
This commit is contained in:
commit
47079456be
34 files changed
+964
-368
No files matched your search
@@ -209,36 +209,41 @@ test('F5 "若干员被击倒的位置为其他干员或召唤物的初始位置
|
||||
// expired or been killed — and the removed summon was not counted, so the clause never applied: a 突袭 member that
|
||||
// landed on the free tile of 浊心斯卡蒂's 海嗣 and fell there lay on it and kept the 海嗣 (远古血亲: it comes back on its
|
||||
// tile) off the field. Every board piece's 初始位置 counts now, on the field or not.
|
||||
// Since 0.2.0 the 海嗣 is a countdown summon — 无敌 + 禁疗 (PRTS 海嗣 备注 「持有禁疗、无敌」; DESIGN §25.16): it only expires,
|
||||
// no damage kills it. The killed case takes 缪尔赛思's 流形, a board summon that is attacked and killed and also comes back
|
||||
// on its tile (「其被击败后会在25秒后自动刷新」).
|
||||
test('F5 rule 3: a body on the home of a summon that has expired or been killed goes back to its own; the summon comes back', () => {
|
||||
for (const how of ['expired', 'killed']) {
|
||||
const CASES = {
|
||||
expired: { owner: 'chess_char_6_04_a', tokenId: 'token_10017_skadi2_dedant', name: '海嗣' },
|
||||
killed: { owner: 'chess_char_6_11_a', tokenId: 'token_10030_mlyss_wtrman', name: '流形' },
|
||||
};
|
||||
for (const [how, { owner, tokenId, name }] of Object.entries(CASES)) {
|
||||
const h = makeBattle({
|
||||
stageId: 'flat', autoFinish: false, timeLimit: 400, bonds: RAID,
|
||||
units: [
|
||||
{ chessId: 'chess_char_6_04_a', row: 10, col: 3, uid: 1 },
|
||||
{ kind: 'token', tokenId: 'token_10017_skadi2_dedant', row: 10, col: 5, uid: 2, ownerUid: 1 },
|
||||
{ chessId: owner, row: 10, col: 3, uid: 1 },
|
||||
{ kind: 'token', tokenId, row: 10, col: 5, uid: 2, ownerUid: 1 },
|
||||
{ chessId: 'chess_char_1_18_a', row: 12, col: 3, uid: 3 },
|
||||
],
|
||||
});
|
||||
h.step();
|
||||
const b = h.b;
|
||||
const sea = b.allyUnits.find((u) => u.kind === 'token' && u.uid === 2);
|
||||
const sum = b.allyUnits.find((u) => u.kind === 'token' && u.uid === 2);
|
||||
const yan = h.unit(3);
|
||||
assert.ok(sea.alive && sea.tileR === 10 && sea.tileC === 5, 'the 海嗣 on its placed tile');
|
||||
if (how === 'killed') kill(b, sea);
|
||||
else h.runUntil(() => !sea.alive, 120);
|
||||
assert.ok(!sea.alive && sea.removed, `${how}: the 海嗣 is gone`);
|
||||
assert.ok(sum.alive && sum.tileR === 10 && sum.tileC === 5, `the ${name} on its placed tile`);
|
||||
if (how === 'killed') kill(b, sum);
|
||||
else h.runUntil(() => !sum.alive, 120);
|
||||
assert.ok(!sum.alive && sum.removed, `${how}: the ${name} is gone`);
|
||||
assert.equal(b.isReservedTile(10, 5), false, 'its home is free for an automatic placement');
|
||||
b.retreat(yan, { reason: 'raid' }); // the 突袭 landing (raidRedeploy: retreat + redeploy on a free tile)
|
||||
assert.ok(b.redeploy(yan, { free: true, tile: [10, 5], keepSp: true }), `${how}: 宴 lands on the 海嗣's home`);
|
||||
assert.ok(b.redeploy(yan, { free: true, tile: [10, 5], keepSp: true }), `${how}: 宴 lands on the ${name}'s home`);
|
||||
kill(b, yan);
|
||||
assert.deepEqual(b.restTile(yan), [12, 3], `${how}: 宴 lies on her own home, not on the 海嗣's`);
|
||||
assert.deepEqual(b.restTile(yan), [12, 3], `${how}: 宴 lies on her own home, not on the ${name}'s`);
|
||||
assert.deepEqual(bodyOf(b, yan), [12, 3], 'the client draws her there');
|
||||
assert.equal(b.isReservedTile(10, 5), false, 'the 海嗣\'s home stays free');
|
||||
if (how === 'expired') {
|
||||
const back = () => b.allyUnits.find((u) => u.kind === 'token' && u.alive && u.defId === sea.defId) ?? null;
|
||||
assert.ok(h.runUntil(() => back() != null, 60), 'the 海嗣 comes back (while 宴 still waits to redeploy)');
|
||||
assert.deepEqual(tileOf(back()), [10, 5], 'on its own tile');
|
||||
}
|
||||
assert.equal(b.isReservedTile(10, 5), false, `the ${name}'s home stays free`);
|
||||
const back = () => b.allyUnits.find((u) => u.kind === 'token' && u.alive && u.defId === tokenId) ?? null;
|
||||
assert.ok(h.runUntil(() => back() != null, 60), `${how}: the ${name} comes back`);
|
||||
assert.deepEqual(tileOf(back()), [10, 5], 'on its own tile');
|
||||
checkInvariants(b);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// test/sim/feedback5-countdown-summons.test.js — the countdown summons (community report of 2026-10-06 「赫默的无人机、浊心
|
||||
// 斯卡蒂的海嗣等这种召唤物不会受到外来伤害(包括源石地板),也不会被干员治疗,血量随时间减少(血量就类似倒计时条,减少完召唤物就
|
||||
// 消失)」): the summons that "不会受到攻击" and leave after a fixed time — 医疗探机, 诅咒娃娃, 斯卡蒂的海嗣 and the 自选 ones
|
||||
// (工程蓄水炮, 沙地兽, “打字机”, 本能的召唤) — hold 无敌 + 禁疗 (PRTS 海嗣 / 沙地兽 「持有禁疗、无敌」, 工程蓄水炮 禁疗; the others
|
||||
// [ASSUMED] from the report) and their bar (the snapshot's hp) shows the share of their life left (content/tokens.js
|
||||
// COUNTDOWN_SUMMONS / startCountdown). Until 0.2.0 a 海嗣 on 活性源石 lost 70 of its 100 HP every second and a hurt drone
|
||||
// drew the medics' heals. The 吟游者 trait (item 32) cannot refill such a bar either: their HP never moves.
|
||||
// Run: node --test test/sim/feedback5-countdown-summons.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { unitTuple } from '../../server/sim/snapshot.js';
|
||||
import { COUNTDOWN_SUMMONS } from '../../server/sim/content/tokens.js';
|
||||
|
||||
/** [label, owner entry (uid 1), token id, life in s] — every placed countdown summon of the pool and of the 自选 pool. */
|
||||
const CASES = [
|
||||
['赫默 医疗探机', { chessId: 'chess_char_2_02_a' }, 'token_10000_silent_healrb', 10],
|
||||
['巫恋 诅咒娃娃', { chessId: 'chess_char_3_15_a' }, 'token_10006_vodfox_doll', null],
|
||||
['浊心斯卡蒂 海嗣', { chessId: 'chess_char_6_04_a' }, 'token_10017_skadi2_dedant', 25],
|
||||
['温蒂 工程蓄水炮', { diy: { slot: 5, charId: 'char_400_weedy', skillIndex: 0, uniEquipId: null } }, 'token_10009_weedy_cannon', 20],
|
||||
['莱伊 沙地兽', { diy: { slot: 5, charId: 'char_4117_ray', skillIndex: 0, uniEquipId: null } }, 'token_10034_ray_sndbst', 25],
|
||||
['鸿雪 打字机', { diy: { slot: 5, charId: 'char_4055_bgsnow', skillIndex: 0, uniEquipId: null } }, 'token_10026_bgsnow_subbow', 25],
|
||||
];
|
||||
|
||||
function field(owner, tokenId) {
|
||||
const h = makeBattle({
|
||||
units: [{ uid: 1, row: 10, col: 3, ...owner }, { uid: 2, kind: 'token', tokenId, ownerUid: 1, row: 10, col: 4 }],
|
||||
autoFinish: false, timeLimit: 120, seed: 3, flags: { dpInit: 60, dpMax: 99 },
|
||||
});
|
||||
h.step();
|
||||
return { h, t: h.b.allyUnits.find((u) => u.kind === 'token' && u.defId === tokenId) };
|
||||
}
|
||||
const bar = (h, t) => unitTuple(t, h.b.time)[3] / unitTuple(t, h.b.time)[4];
|
||||
|
||||
for (const [label, owner, tokenId, lifeWant] of CASES) {
|
||||
test(`${label}: 无敌 + 禁疗, no outside damage (活性源石 tick), no heal, a bar that runs down with its life`, () => {
|
||||
assert.ok(COUNTDOWN_SUMMONS.has(tokenId));
|
||||
const { h, t } = field(owner, tokenId);
|
||||
assert.ok(t && t.alive, 'deployed from its piece');
|
||||
assert.ok(t.countdown, 'its life is counting down');
|
||||
const life = t.countdown.until - t.countdown.from;
|
||||
if (lifeWant != null) assert.ok(Math.abs(life - lifeWant) < 1e-6, `life ${life} s`);
|
||||
const f = t.s.flags;
|
||||
assert.ok(f.invulnerable && f.noHeal && f.healFree, '无敌 + 禁疗');
|
||||
// a 活性源石 tick (true, 无来源, 环境) and an enemy-style hit change nothing
|
||||
const hp0 = t.hp;
|
||||
h.b.dealDamage(null, t, { amount: 70, type: 'true', canDodge: false, tags: ['terrain'] });
|
||||
h.b.dealDamage(null, t, { amount: t.s.maxHp * 3, type: 'phys' });
|
||||
assert.equal(t.hp, hp0, 'no damage reaches it');
|
||||
assert.equal(h.b.heal(h.unit(1), t, 500), 0, 'no heal reaches it');
|
||||
// the bar: full at the start, ~3/4 then ~1/4 of the way through, gone at the end
|
||||
const from = t.countdown.from;
|
||||
h.run(from + life * 0.25 - h.b.time);
|
||||
assert.ok(Math.abs(bar(h, t) - 0.75) < 0.05, `a quarter in: ${bar(h, t).toFixed(3)}`);
|
||||
h.run(life * 0.5);
|
||||
assert.ok(Math.abs(bar(h, t) - 0.25) < 0.05, `three quarters in: ${bar(h, t).toFixed(3)}`);
|
||||
assert.equal(t.hp, t.s.maxHp, 'its HP itself never moves');
|
||||
h.run(life * 0.25 + 0.2);
|
||||
assert.ok(!t.alive, 'it leaves when the bar is empty');
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
});
|
||||
}
|
||||
|
||||
test('a summon that is attacked keeps its HP as HP: 伺夜\'s 狼群 takes damage, is no countdown summon', () => {
|
||||
const { h, t } = field({ chessId: 'chess_char_3_19_a' }, 'token_10028_vigil_wolf');
|
||||
assert.ok(!COUNTDOWN_SUMMONS.has(t.defId) && t.countdown == null && !t.s.flags.invulnerable);
|
||||
h.b.dealDamage(null, t, { amount: 100, type: 'true', canDodge: false, tags: ['terrain'] });
|
||||
assert.ok(t.hp < t.s.maxHp);
|
||||
assert.equal(unitTuple(t, h.b.time)[3], Math.ceil(t.hp), 'its bar is its HP');
|
||||
});
|
||||
Reference in new issue
Block a user