- Shared card pool and item stock as in the official mode (the owner's request, from the 路标档案馆 video and public notes): solo low-tier exceptions, co-op operators x2, shared item stock with special-item exceptions, normal / advanced occupancy 1 / 2, re-check on buy, 机变 exhaustion, release on consume / destroy / elimination; 整备 never exceeds stock; invariant held <= cap. - 突袭: one taunt-first reselect right after a revive (the owner's rule); ordinary polls and redeploys keep the 0.2.3 rule; a revive on an enemy's tile stays. #475: online boss / hidden-boss Raid may land on the empty half (owner-limited scope). - 浊心斯卡蒂 S1 x 坚守 no longer bounce redirected damage, and the two shares conserve the hit (idea of PR #464 by @HappyTanuki). - Operator fixes adapted from PRs by @Xinglan233 (#459 哈洛德 S2 elemental trigger, #460 涤火杰西卡 shield on the four adjacent tiles, #461 家族徽章 first hit after stealth, #462 松果 S2 regrow per deploy); 克莱门莎 S2 splash 1.1 / 100% ATK and air auto-trigger (#467); 白面 copying 歌蕾蒂娅 / 余 skips the three-member condition (#473, the owner's video). - 香槟炸弹 is not chosen by friendly auras / heals (PR #468 / #476 by @Siornya); 琳琅诗怀雅 coin balance (PR #450 by @IceCodeNew); Touch prep display (PR #451 by @YinqiMargatroid); lobby header (PR #471 by @YuriRestia); Japanese UI terms (PR #448 by @scutkane). - AI-only boss countdown grace, render-failure cache / recovery without restarting a battle, device units kept on a same-field re-entry, DIY detail / skill previews, large-text result buttons. - golden: battle families equal 0.2.3 except diy-135 / diy-136 (克莱门莎); all 18 release matches move through the shared pool, every flip named in docs/history/0.2.4.md §29.12; two coverage scenarios added.
88 lines
5.1 KiB
JavaScript
88 lines
5.1 KiB
JavaScript
// test/content/feedback7-champagne-hold.test.js — 琳琅诗怀雅 S2's 香槟炸弹 (token_10031_swire2_gdtrap) under the owner's
|
|
// decision D2 of 2026-10-08: it holds 禁疗, loses no HP (活性源石, damage, a 流失) and never leaves because of its HP; its
|
|
// 见面礼 is unchanged (the first ground enemy that touches it, then it is gone). Both kits: 琳琅诗怀雅's own bomb and the
|
|
// token kit (content/tokens.js champagne, a bomb spawned without her kit).
|
|
// Run: node --test test/content/feedback7-champagne-hold.test.js
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
|
|
|
const TRAP = 'token_10031_swire2_gdtrap';
|
|
const SW = 'chess_char_3_04_a';
|
|
for (const path of ['owner', 'token']) test(`香槟炸弹 (${path}) rejects ordinary allied auras and inspiration without isolating normal summons (#476)`, () => {
|
|
const { h, bomb: ownBomb } = herBomb({ units: [
|
|
{ uid: 2, chessId: 'chess_char_5_20_a', row: 10, col: 4, skillIndex: 0 },
|
|
{ uid: 3, chessId: 'chess_char_6_04_a', row: 10, col: 5, skillIndex: 1 },
|
|
] });
|
|
const angelina = h.unit(2), skadi = h.unit(3);
|
|
if (path === 'token') h.b.retreat(ownBomb, { reason: 'expired', permanent: true });
|
|
const bomb = path === 'owner' ? ownBomb : h.b.spawnToken(h.unit(1), TRAP, 9, 6);
|
|
const control = h.b.spawnToken(skadi, 'token_10017_skadi2_dedant', 9, 4);
|
|
assert.ok(bomb && control);
|
|
assert.ok(skadi.skill.activate('test', { free: true })); h.run(0.7);
|
|
assert.equal(h.b.allySelectable(bomb, angelina), false);
|
|
assert.equal(bomb.s.aspd, bomb.base.aspd);
|
|
assert.equal(bomb.s.hpRegen, 0);
|
|
assert.equal(bomb.findBuff('inspire'), null);
|
|
assert.equal(bomb.findBuff('inspire:def'), null);
|
|
assert.ok(h.b.allySelectable(control, angelina), 'a normal unhealable summon is still selectable');
|
|
assert.ok(control.s.aspd > control.base.aspd, 'Angelina still affects the ordinary summon');
|
|
assert.ok(bomb.alive && bomb.hp === bomb.s.maxHp); checkInvariants(h.b);
|
|
});
|
|
/** 琳琅诗怀雅 on (9,5) of a field whose only free x-6 tile is (9,6) — 活性源石 when `infected`; she throws one bomb there. */
|
|
function herBomb({ infected = false, units = [], enemies = [] } = {}) {
|
|
const row9 = infected ? '##ERRriRrrSrrrrrrrS##' : '##ERRrrRrrSrrrrrrrS##';
|
|
const h = makeBattle({
|
|
flat: { rows: { 9: row9, 10: '##hrrRrrrrfrrrrrrrf##', 11: '##hrrRrrrrfrrrrrrrf##' } },
|
|
defs: { enemies: { e_w: enemyRec({ key: 'e_w', hp: 1e6, speed: 1, def: 0, atk: 0 }) } },
|
|
timeLimit: 60, autoFinish: false, hooks: ['heal', 'death', 'damaged'], captureNoisy: true,
|
|
units: [{ uid: 1, chessId: SW, row: 9, col: 5 }, ...units], enemies,
|
|
});
|
|
h.step();
|
|
h.unit(1).mem.coins = 1;
|
|
assert.ok(h.runUntil(() => h.b.allyUnits.some((t) => t.alive && t.defId === TRAP), 6), 'she threw a bomb');
|
|
const bomb = h.b.allyUnits.find((t) => t.alive && t.defId === TRAP);
|
|
assert.deepEqual([bomb.tileR, bomb.tileC], [9, 6]);
|
|
return { h, bomb, sw: h.unit(1) };
|
|
}
|
|
|
|
test('香槟炸弹 (her kit): no HP loss on 活性源石, 禁疗, a lethal 流失 or hit leaves it at full HP and on the field', () => {
|
|
const { h, bomb } = herBomb({ infected: true });
|
|
assert.ok(bomb.s.flags.untargetable && bomb.s.flags.noHeal && bomb.s.flags.healFree, '不可选中 + 禁疗');
|
|
assert.ok(!bomb.s.flags.invulnerable, 'no 无敌 flag (no badge on the client)');
|
|
h.run(3.2);
|
|
assert.equal(bomb.hp, bomb.s.maxHp, '活性源石 takes nothing');
|
|
assert.equal(h.b.heal(h.unit(1), bomb, 500), 0, 'no heal reaches it');
|
|
assert.equal(h.b.injuredAlliesInKeys([9 * 21 + 6], h.unit(1)).length, 0, 'no healer picks it');
|
|
h.b.dealDamage(null, bomb, { amount: 1e6, type: 'true', tags: ['test'] });
|
|
h.b.loseHp(bomb, 1e6, { tags: ['test'] });
|
|
h.step();
|
|
assert.ok(bomb.alive && bomb.deployed, 'not gone because of its HP');
|
|
assert.equal(bomb.hp, bomb.s.maxHp);
|
|
assert.equal(h.hooksOf('death').filter((c) => c.unit === bomb).length, 0);
|
|
checkInvariants(h.b);
|
|
});
|
|
|
|
test('香槟炸弹 (her kit): its 见面礼 is unchanged — the first ground enemy touching it is hit and slowed, then the bomb is gone', () => {
|
|
const { h, bomb } = herBomb({ enemies: [{ key: 'e_w', route: 0, time: 1 }] });
|
|
assert.ok(h.runUntil(() => !bomb.alive, 30), 'set off');
|
|
const e = h.b.enemies.find((x) => x.alive);
|
|
assert.ok(e && e.hp < 1e6 && e.findBuff('sluggish'), 'hit + 停顿');
|
|
assert.ok(h.hooksOf('death').some((c) => c.unit === bomb && c.reason === 'expired'), 'used up');
|
|
checkInvariants(h.b);
|
|
});
|
|
|
|
test('香槟炸弹 (the token kit, spawned without her kit): the same 禁疗, no HP loss, no knock-out', () => {
|
|
const h = makeBattle({ timeLimit: 30, autoFinish: false, hooks: ['death'], units: [{ uid: 1, chessId: SW, row: 12, col: 3 }] });
|
|
h.step();
|
|
const bomb = h.b.spawnToken(h.unit(1), TRAP, 9, 6);
|
|
assert.ok(bomb && bomb.s.flags.noHeal && bomb.s.flags.healFree && bomb.s.flags.untargetable);
|
|
h.b.loseHp(bomb, 1e6, { tags: ['test'] });
|
|
h.b.dealDamage(null, bomb, { amount: 1e6, type: 'true', tags: ['test'] });
|
|
h.step();
|
|
assert.ok(bomb.alive && bomb.hp === bomb.s.maxHp);
|
|
assert.equal(h.b.heal(h.unit(1), bomb, 500), 0);
|
|
checkInvariants(h.b);
|
|
});
|