feedback5: merge W5C — 琳琅诗怀雅 S2 bomb on x-6 (#116), 新硎 / 沉沙 one 断刃 layer (#107), the owner's ACTIVE_RANGE trigger rule, AUTO-skill audit (伺夜 S2, 浊心斯卡蒂 S2)
# Conflicts: # data/backups.json # tools/build-data.mjs
This commit is contained in:
commit
65ac4afbd8
32 files changed
+2432
-1959
No files matched your search
@@ -290,22 +290,37 @@ for (const [key, n] of Object.entries(DEATH_SPAWN)) {
|
||||
}
|
||||
|
||||
for (const key of ['enemy_1207_sfji', 'enemy_1207_sfji_2']) {
|
||||
test(`${nm(key)}: each attack spends a blade (+ATK); unspent blades become 矛头 on death (at least 1)`, () => {
|
||||
// PRTS 天赋 "攻击力+X%,持有4个【断刃】 / 每次成功攻击后消耗1个【断刃】,攻击结束时若已耗尽【断刃】,则立刻切换为无断刃模式并失去攻击力
|
||||
// 加成" (popup: "清空当次攻击间隔" — it attacks again at once); GitHub #107: until 0.2.0 each attack stacked another layer
|
||||
test(`${nm(key)}: one ATK layer while it holds a blade, gone with the 4th attack (which is followed at once by the next); unspent blades become 矛头 on death (at least 1)`, () => {
|
||||
const per = tb(key, 'Atkup.atk') ?? tb(key, 'AtkUp.atk'), cnt = tb(key, 'DeadSpawn.cnt');
|
||||
const h = arena({ units: [{ chessId: 't_wall', row: 9, col: 5 }] });
|
||||
h.step();
|
||||
const e = put(h, key, [9, 5]);
|
||||
const base = e.s.atk;
|
||||
const base = e.base.atk;
|
||||
approx(e.s.atk, base * (1 + per), 1e-6, 'holding 4 blades: one layer from the spawn');
|
||||
h.runUntil(() => e.stats.attacks >= 2, 20);
|
||||
const per = tb(key, 'Atkup.atk') ?? tb(key, 'AtkUp.atk');
|
||||
approx(e.s.atk, base * (1 + 2 * per));
|
||||
killed(h, e, null);
|
||||
approx(e.s.atk, base * (1 + per), 1e-6, 'two blades spent: still one layer (never stacked)');
|
||||
h.runUntil(() => e.stats.attacks >= cnt, 20);
|
||||
approx(e.s.atk, base, 1e-6, 'the last blade spent: 无断刃模式, no bonus');
|
||||
const tLast = h.b.time;
|
||||
assert.ok(h.runUntil(() => e.stats.attacks >= cnt + 1, 0.2), 'the mode switch clears the attack interval: the next attack at once');
|
||||
assert.ok(h.b.time - tLast < e.s.interval / 2, `${h.b.time - tLast} s after the 4th attack`);
|
||||
const child = E[key].talents.bbStr['DeadSpawn.enemy_key'];
|
||||
assert.equal(alive(h, child).length, tb(key, 'DeadSpawn.cnt') + tb(key, 'DeadSpawn.cnt_add') * 2);
|
||||
const h2 = arena();
|
||||
killed(h, e, null);
|
||||
assert.equal(alive(h, child).length, 1, 'no blade left: still one 矛头');
|
||||
// killed after two attacks: the two unspent blades; untouched: all four
|
||||
const h2 = arena({ units: [{ chessId: 't_wall', row: 9, col: 5 }] });
|
||||
h2.step();
|
||||
const e2 = put(h2, key, [10, 7]);
|
||||
const e2 = put(h2, key, [9, 5]);
|
||||
h2.runUntil(() => e2.stats.attacks >= 2, 20);
|
||||
killed(h2, e2, null);
|
||||
assert.equal(alive(h2, child).length, tb(key, 'DeadSpawn.cnt'));
|
||||
assert.equal(alive(h2, child).length, cnt + tb(key, 'DeadSpawn.cnt_add') * 2);
|
||||
const h3 = arena();
|
||||
h3.step();
|
||||
const e3 = put(h3, key, [10, 7]);
|
||||
killed(h3, e3, null);
|
||||
assert.equal(alive(h3, child).length, cnt);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -352,16 +352,18 @@ test('2_12 砾 S1 影袭: at deployment DEF +def decaying to 0 over `duration` s
|
||||
}
|
||||
});
|
||||
|
||||
test('2_13 蒂比 S1 专业喷绘技巧: DEFAULT trigger, takes off (skill range, ATK +atk, blocks flyers), single shots; hits never set it off', () => {
|
||||
test('2_13 蒂比 S1 专业喷绘技巧: ACTIVE_RANGE trigger (an enemy in its 2-3 only), takes off (skill range, ATK +atk, blocks flyers), single shots; hits never set it off', () => {
|
||||
for (const id of both('chess_char_2_13')) {
|
||||
const bb = bbAlt(id);
|
||||
const h = run({ defs: { enemies: { e: dummy('e'), f: dummy('f', { motion: 'FLY' }) } }, units: [U(id, 9, 5, { carryState: READY })], enemies: [{ key: 'e', pos: [9, 6] }] });
|
||||
// (10,5) = [1,0]: inside S1's 2-3, outside her own 2-2 — the owner's rule of 2026-10-05 (data trigger ACTIVE_RANGE)
|
||||
const h = run({ defs: { enemies: { e: dummy('e'), f: dummy('f', { motion: 'FLY' }) } }, units: [U(id, 9, 5, { carryState: READY })], enemies: [{ key: 'e', pos: [10, 5] }] });
|
||||
const u = h.unit(id);
|
||||
usesAlt(u, id);
|
||||
h.step();
|
||||
const range0 = u.baseRangeKeys.length;
|
||||
assert.ok(!u.baseRangeKeys.includes(h.b.grid.key(10, 5)), 'the enemy is outside her own range');
|
||||
h.runUntil(() => u.skill.active, 3);
|
||||
assert.equal(started(h, u)[0].reason, 'DEFAULT');
|
||||
assert.equal(started(h, u)[0].reason, 'ACTIVE_RANGE');
|
||||
assert.equal(u.s.flags.liftoff, true, 'airborne (起飞)');
|
||||
assert.equal(u.ground, true, 'still a ground unit on her low tile');
|
||||
approx(u.s.atk, u.base.atk * (1 + bb.atk));
|
||||
|
||||
@@ -829,6 +829,32 @@ test('3_19 伺夜 S2 领袖的馈赠: +DP, the pack recovers HP, its next attack
|
||||
}
|
||||
});
|
||||
|
||||
test('3_19 伺夜 S2 领袖的馈赠 (自动触发): no enemy needed — cast at full SP while the pack stands and holds no unused gift (PRTS 备注), never without the pack', () => {
|
||||
for (const id of BOTH('chess_char_3_19_a')) {
|
||||
const b = SB(id, 'skchr_vigil_2');
|
||||
const h = makeBattle({ defs: { chess: noGarrison(id) }, timeLimit: 60, flags: { dpPerSec: 0 }, units: [U(id, 'skchr_vigil_2', 10, 3)] });
|
||||
const u = h.unit(id), p = h.b.getPlayer('p1');
|
||||
h.run(1);
|
||||
const w = u.trait.reinforcement;
|
||||
assert.ok(w && w.alive, 'the pack stands');
|
||||
const dp0 = p.dp;
|
||||
fill(u);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), 'cast with nobody on the field');
|
||||
approx(p.dp, dp0 + b.cost, 1e-6, '+cost DP at once');
|
||||
assert.ok(w.mem.vigilGift, 'the pack holds the gift for its next attack');
|
||||
fill(u);
|
||||
h.run(3);
|
||||
assert.equal(u.skill.activations, 1, '"狼群未获得此技能的充能时可触发": no second cast while the gift is unused');
|
||||
// the gift spent (as by a bite), the pack gone: still no cast until a pack stands again
|
||||
w.mem.vigilGift = null;
|
||||
for (let i = 0; i < 10 && w.alive; i++) h.b.dealDamage(null, w, { amount: 1e9, type: 'true' }); // a wolf is lost per KO
|
||||
assert.ok(!u.trait.reinforcement?.alive, 'no pack on the field');
|
||||
h.step(2);
|
||||
assert.equal(u.skill.activations, 1, '"仅场上存在狼群…时可触发": no pack, no cast');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('3_19 伺夜 精锐 module TAC-Y: ×165 % on pack-blocked enemies; "援军阻挡的敌人更容易受到我方的攻击" = those enemies taunt +1 (not the pack); TAC-X guard not applied', () => {
|
||||
const id = 'chess_char_3_19_b', mod = 'uniequip_003_vigil';
|
||||
const tb = ds.getChess(id, { moduleId: mod }).traitBb;
|
||||
|
||||
@@ -366,6 +366,23 @@ test('6_04 浊心斯卡蒂 S2 同葬无光之愿: toggle; 鼓舞 ATK and DEF (at
|
||||
}
|
||||
});
|
||||
|
||||
test('6_04 浊心斯卡蒂 S2 同葬无光之愿 (自动触发, effects on her allies only): on as soon as its SP is full — no enemy needed', () => {
|
||||
for (const id of both('chess_char_6_04')) {
|
||||
const sid = 'skchr_skadi2_2';
|
||||
const h = run({
|
||||
defs: { chess: { ally_a: plain('ally_a', { stats: { maxHp: 1e5, atk: 400, def: 100 } }) } },
|
||||
units: [U(id, sid, 10, 4, { carryState: READY }), { chessId: 'ally_a', row: 10, col: 5 }],
|
||||
});
|
||||
const u = h.unit(id), a = h.unit('ally_a');
|
||||
usesSkill(u, sid);
|
||||
assert.equal(u.skill.rule, 'SP_FULL');
|
||||
assert.ok(h.runUntil(() => u.skill.active, 2), 'on with nobody on the field (the data DEFAULT waited for an enemy in her range)');
|
||||
h.run(1.1);
|
||||
assert.ok(a.findBuff('inspire') && a.findBuff('inspire:def'), '鼓舞 on the ally beside her');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
// =================================================================================================================
|
||||
// 6_05 异客
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.
|
||||
import { getDefaultSource } from '../../server/sim/simdata.js';
|
||||
import { effectiveProfile } from '../../server/sim/ai.js';
|
||||
import KITS from '../../server/sim/content/kits/tier3.js';
|
||||
import { COLS } from '../../server/sim/constants.js';
|
||||
|
||||
const ds = getDefaultSource();
|
||||
const D = (id) => ds.getChess(id);
|
||||
@@ -110,23 +111,38 @@ test('3_03 诗怀雅: 近距离作战指导 melee ATK aura, ×talent_scale durin
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('3_04 琳琅诗怀雅: coins → champagne bomb (ATK% + 停顿), 大买家 coin/ATK per DP payment, 破财消灾 DP revive', () => {
|
||||
/** 琳琅诗怀雅 S2's placement range x-6 (range_table; PRTS 备注 "香槟炸弹放置范围:x-6") — symmetric, so facing does not matter. */
|
||||
const SWIRE_X6 = [[2, 0], [1, 0], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, 0], [-2, 0]];
|
||||
const TRAP = 'token_10031_swire2_gdtrap';
|
||||
const trapsOf = (h) => h.b.allyUnits.filter((t) => t.alive && t.defId === TRAP);
|
||||
const x6Keys = (r, c) => new Set(SWIRE_X6.map(([dr, dc]) => (r + dr) * COLS + c + dc));
|
||||
/** Flat stage where the only free placeable tile of x-6 around (9,5) is (9,6): the others are walkable 'R' road. */
|
||||
const ONE_TILE = { rows: { 9: '##ERRrrRrrSrrrrrrrS##', 10: '##hrrRrrrrfrrrrrrrf##', 11: '##hrrRrrrrfrrrrrrrf##' } };
|
||||
|
||||
test('3_04 琳琅诗怀雅: coins → champagne bombs on range x-6 with no enemy needed (ATK% + 停顿), 大买家 coin/ATK per DP payment, 破财消灾 DP revive', () => {
|
||||
const id = 'chess_char_3_04_a', bb = BB(id), t0 = TB(id, 0), t1 = TB(id, 1);
|
||||
const booms = [];
|
||||
const h = makeBattle({
|
||||
defs: { enemies: { enemy_w: enemyRec({ key: 'enemy_w', hp: 1e6, speed: 1, def: 0, atk: 0 }) } }, timeLimit: 60, hooks: ['damaged', 'deploy'], captureNoisy: true,
|
||||
units: [{ chessId: id, row: 9, col: 5 }], enemies: [{ key: 'enemy_w', route: 0 }],
|
||||
units: [{ chessId: id, row: 9, col: 5 }], enemies: [{ key: 'enemy_w', route: 0, time: 8 }],
|
||||
setup: (b) => b.on('damaged', (c) => { if (c.dmg?.tags?.includes('trap')) booms.push({ amount: c.amount, atk: c.source.s.atk, t: b.time }); }),
|
||||
});
|
||||
const u = h.unit(id);
|
||||
const x6 = x6Keys(9, 5);
|
||||
h.step();
|
||||
assert.equal(u.mem.coins, t0.sp, '开启技能 coin');
|
||||
// the 开启技能 coin (大买家) is thrown at once — her attack's turn, nobody on the field (client charpack: the S2 attack is
|
||||
// [SpawnToken if a free x-6 tile and a coin, else Attack]; PRTS 备注 "否则随机放置香槟至可部署的地块")
|
||||
assert.equal(trapsOf(h).length, t0.sp, 'the deployment coin became a bomb with no enemy around');
|
||||
assert.equal(u.mem.coins, 0);
|
||||
h.run(3.1);
|
||||
assert.equal(u.mem.coins, t0.sp + t0.trait_sp, 'coin from the first trait payment');
|
||||
assert.equal(u.findBuff('talent:swire2_buyer')?.stacks, 1);
|
||||
assert.equal(u.findBuff('talent:swire2_buyer')?.stacks, 1, 'first trait payment: an ATK stack');
|
||||
approx(u.s.atk, u.base.atk * (1 + t0.atk));
|
||||
assert.ok(h.runUntil(() => booms.length > 0, 20), 'bomb exploded');
|
||||
assert.ok(h.hooksOf('deploy').some((c) => c.unit.defId === 'token_10031_swire2_gdtrap'), 'bomb token placed');
|
||||
assert.equal(trapsOf(h).length, t0.sp + t0.trait_sp, '… and its coin another bomb');
|
||||
for (const t of trapsOf(h)) {
|
||||
assert.ok(x6.has(t.tileR * COLS + t.tileC), `bomb (${t.tileR},${t.tileC}) on range x-6`);
|
||||
assert.ok(h.b.grid.groundPassable(t.tileR, t.tileC) && h.b.grid.canStand(t.tileR, t.tileC), 'on walkable ground a melee unit may stand on');
|
||||
}
|
||||
assert.ok(h.runUntil(() => booms.length > 0, 30), 'bomb exploded');
|
||||
approx(booms[0].amount, booms[0].atk * bb.atk_scale, 1e-6, 'bomb damage');
|
||||
const e = h.enemy('enemy_w');
|
||||
assert.ok(e.findBuff('sluggish'), '停顿');
|
||||
@@ -846,33 +862,37 @@ test('3_02 断崖: 浮游刃 blades hit enemies blocked by the allies around her
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('3_04 琳琅诗怀雅 精锐: MER-X drain, coin cap, armed bomb hits twice; bombs never land on a dead operator’s tile', () => {
|
||||
test('3_04 琳琅诗怀雅 精锐: MER-X drain; no enemy ⇒ coins fill every free tile of x-6, then pile up to the cap; armed bomb hits twice; never on a dead operator’s tile', () => {
|
||||
const gid = 'chess_char_3_04_b', bb = BB(gid), t0 = TB(gid, 0), tb = TR(gid);
|
||||
// drain + coin cap (no enemy: no attack spends a coin)
|
||||
const q = makeBattle({ units: [{ chessId: gid, row: 9, col: 5 }], timeLimit: 60, flags: { dpPerSec: 0 } });
|
||||
// drain; no enemy: every coin becomes a bomb on a free tile of x-6 until none is left, then the coins pile up to the cap
|
||||
const q = makeBattle({ units: [{ chessId: gid, row: 9, col: 5 }], timeLimit: 90, flags: { dpPerSec: 0 } });
|
||||
const s = q.unit(gid);
|
||||
q.step();
|
||||
q.b.getPlayer('p1').dp = 50;
|
||||
q.b.getPlayer('p1').dp = 90;
|
||||
q.run(3.1);
|
||||
approx(q.b.getPlayer('p1').dp, 50 - Math.abs(tb.cost), 1e-6, 'module: 2 DP per payment');
|
||||
q.run(tb.interval * (bb.sp + 1));
|
||||
approx(q.b.getPlayer('p1').dp, 90 - Math.abs(tb.cost), 1e-6, 'module: 2 DP per payment');
|
||||
// the flat field around (9,5): x-6 has 6 free placeable tiles (row 9 cols 3/4/6/7, (10,5), (11,5); rows 7/8 are no field)
|
||||
const free = [...x6Keys(9, 5)].filter((k) => { const r = (k / COLS) | 0, c = k % COLS; return !(r === 9 && c === 5) && q.b.grid.groundPassable(r, c) && q.b.grid.canStand(r, c); });
|
||||
assert.equal(free.length, 6);
|
||||
q.run(tb.interval * (free.length + bb.sp + 1));
|
||||
assert.deepEqual(trapsOf(q).map((t) => t.tileR * COLS + t.tileC).sort((a, b) => a - b), free.sort((a, b) => a - b), 'a bomb on every free tile of x-6');
|
||||
const paid = Math.floor((q.b.time + 1e-9) / tb.interval);
|
||||
assert.equal(s.mem.coins, bb.sp, 'coin cap = bb.sp');
|
||||
assert.equal(s.mem.coins, bb.sp, 'x-6 full: coin cap = bb.sp');
|
||||
assert.equal(s.findBuff('talent:swire2_buyer').stacks, Math.min(t0.max_stack_cnt, paid), 'ATK stack per payment (coins capped, stacks not)');
|
||||
done(q);
|
||||
|
||||
// a bomb that stayed duration_switch s on the field hits twice
|
||||
// a bomb that stayed duration_switch s on the field hits twice (only (9,6) is free: the walker comes to it after 3 s)
|
||||
const booms = [];
|
||||
const h = makeBattle({
|
||||
flat: ONE_TILE,
|
||||
defs: { enemies: { enemy_d: dummy('enemy_d'), enemy_w: enemyRec({ key: 'enemy_w', hp: 1e6, speed: 1, def: 0, atk: 0 }) } }, timeLimit: 60,
|
||||
units: [{ chessId: gid, row: 9, col: 5 }], enemies: [{ key: 'enemy_d', pos: [9, 5] }, { key: 'enemy_w', route: 0, time: 1 }],
|
||||
setup: (b) => b.on('damaged', (c) => { if (c.dmg?.tags?.includes('trap')) booms.push({ target: c.target.defId, amount: c.amount, atk: c.source.s.atk, t: b.time }); }),
|
||||
});
|
||||
const u = h.unit(gid);
|
||||
const switchT = ds.getToken('token_10031_swire2_gdtrap', gid).skill.bb.duration_switch;
|
||||
assert.ok(h.runUntil(() => h.b.allyUnits.some((t) => t.alive && t.defId === 'token_10031_swire2_gdtrap'), 5), 'bomb placed in front of her');
|
||||
const bomb = h.b.allyUnits.find((t) => t.alive && t.defId === 'token_10031_swire2_gdtrap');
|
||||
assert.deepEqual([bomb.tileR, bomb.tileC], [9, 6]);
|
||||
const switchT = ds.getToken(TRAP, gid).skill.bb.duration_switch;
|
||||
assert.ok(h.runUntil(() => trapsOf(h).length > 0, 5), 'bomb placed');
|
||||
const bomb = trapsOf(h)[0];
|
||||
assert.deepEqual([bomb.tileR, bomb.tileC], [9, 6], 'the one free tile of x-6');
|
||||
assert.ok(h.runUntil(() => booms.length > 0, 30), 'the walker triggered it');
|
||||
assert.ok(booms[0].t - bomb.deployedAt >= switchT);
|
||||
assert.equal(booms.length, 2, 'armed bomb: one extra hit');
|
||||
@@ -880,8 +900,9 @@ test('3_04 琳琅诗怀雅 精锐: MER-X drain, coin cap, armed bomb hits twice;
|
||||
assert.ok(h.enemy('enemy_w').findBuff('sluggish'));
|
||||
done(h);
|
||||
|
||||
// the tile in front belongs to a dead operator waiting to redeploy: no bomb there (no coin spent)
|
||||
// that one tile belongs to a dead operator waiting to redeploy: no bomb anywhere, no coin spent, and she attacks on
|
||||
const k = makeBattle({
|
||||
flat: ONE_TILE,
|
||||
defs: { enemies: { enemy_d: dummy('enemy_d') } }, timeLimit: 30,
|
||||
units: [{ chessId: gid, row: 9, col: 5 }, { chessId: 'chess_char_3_16_a', row: 9, col: 6 }], enemies: [{ key: 'enemy_d', pos: [9, 5], time: 0.5 }],
|
||||
});
|
||||
@@ -892,11 +913,36 @@ test('3_04 琳琅诗怀雅 精锐: MER-X drain, coin cap, armed bomb hits twice;
|
||||
const coins = v.mem.coins;
|
||||
k.run(5);
|
||||
assert.ok(v.stats.attacks >= 2, 'she kept attacking');
|
||||
assert.equal(k.b.allyUnits.filter((t) => t.defId === 'token_10031_swire2_gdtrap').length, 0, 'no bomb on the dead operator’s tile');
|
||||
assert.equal(k.b.allyUnits.filter((t) => t.defId === TRAP).length, 0, 'no bomb on the dead operator’s tile');
|
||||
assert.ok(v.mem.coins >= coins, 'no coin spent');
|
||||
done(k);
|
||||
});
|
||||
|
||||
test('3_04 琳琅诗怀雅 S2 见面礼: an enemy on a free tile of x-6 gets the bomb on its tile — ground only, in her attack target order; outside her 1-1 attack range too', () => {
|
||||
const id = 'chess_char_3_04_a';
|
||||
const bombAt = (enemies) => {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: { enemy_d: dummy('enemy_d'), enemy_f: dummy('enemy_f', { motion: 'FLY' }) } }, timeLimit: 10, autoFinish: false,
|
||||
units: [{ chessId: id, row: 9, col: 5 }], enemies,
|
||||
});
|
||||
const u = h.unit(id);
|
||||
h.step();
|
||||
for (const t of trapsOf(h)) h.b.retreat(t, { reason: 'expired', permanent: true }); // the deployment coin's bomb
|
||||
h.step(2);
|
||||
u.mem.coins = 1;
|
||||
assert.ok(h.runUntil(() => trapsOf(h).length > 0, 2), 'a bomb');
|
||||
const t = trapsOf(h)[0];
|
||||
done(h);
|
||||
return [t.tileR, t.tileC];
|
||||
};
|
||||
// (9,7): two tiles ahead — x-6 but outside her attack range 1-1
|
||||
assert.deepEqual(bombAt([{ key: 'enemy_d', pos: [9, 7] }]), [9, 7]);
|
||||
// two ground enemies: the one nearer the goal (9,2) first (her attack target order: blocked → … → remaining distance)
|
||||
assert.deepEqual(bombAt([{ key: 'enemy_d', pos: [9, 7] }, { key: 'enemy_d', pos: [9, 4] }]), [9, 4]);
|
||||
// a flyer is no target of the selector (its `_targetMotion` WALK): the ground enemy farther from the goal gets it
|
||||
assert.deepEqual(bombAt([{ key: 'enemy_d', pos: [9, 7] }, { key: 'enemy_f', pos: [9, 4], route: 2 }]), [9, 7]);
|
||||
});
|
||||
|
||||
test('3_07 见行者: 惊爆射击 fires on an enemy inside the skill range only; 精锐 refund only on a ranged tile', () => {
|
||||
const id = 'chess_char_3_07_a', bb = BB(id);
|
||||
const h = makeBattle({
|
||||
|
||||
@@ -801,10 +801,13 @@ test('百炼嘉维尔 S2: drags unblocked enemies in front of her; T1 ATK/DEF +1
|
||||
u.hp = u.s.maxHp * 0.3;
|
||||
approx(h.b.heal(null, u, 100), 100 * t1.heal_scale_2);
|
||||
for (const e of h.b.enemies) h.b.dealDamage(null, e, { amount: 1e9, type: 'true' });
|
||||
h.run(0.3); // 战地巨斧 refreshes every 0.2 s: back to no extra blocked enemy before the cast
|
||||
const near = h.spawn('enemy_dummy', { pos: [9, 5] });
|
||||
const far = h.spawn('enemy_dummy', { pos: [9, 6] });
|
||||
u.skill.gainSp(1000);
|
||||
// S2's 2-5 strictly contains her 1-1: ACTIVE_RANGE (the owner's rule, 2026-10-05) casts it at once, no attack needed
|
||||
assert.ok(h.runUntil(() => u.skill.active, 3));
|
||||
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u)?.reason, 'ACTIVE_RANGE');
|
||||
approx(u.s.atk, u.base.atk * (1 + t0.atk + bb.atk));
|
||||
h.b.dealDamage(null, near, { amount: 1e9, type: 'true' });
|
||||
h.run(1.5);
|
||||
|
||||
@@ -125,8 +125,10 @@ test('琳琅诗怀雅: a 香槟炸弹 arms after its owner\'s module variant of
|
||||
chessId: SWIRE, lo, tokens, row: 9, col: 5, enemies: { enemy_d: dummy('enemy_d') },
|
||||
spawns: [{ key: 'enemy_d', pos: [9, 5] }, { key: 'enemy_d', pos: [9, 13] }],
|
||||
});
|
||||
for (const u of ops) { u.mem.coins = 5; u.player.dp = 99; }
|
||||
// the moment a bomb stands, an enemy steps on it (the bomb is < 1 tick old)
|
||||
// each threw its deployment coin's bomb in duel's first step (S2 needs no enemy); no other coin before the 3 s payment,
|
||||
// so every trap hit below is that bomb's
|
||||
for (const u of ops) { u.mem.coins = 0; u.player.dp = 99; }
|
||||
// the moment a bomb stands, an enemy steps on it (the bomb is a tick old)
|
||||
const bombs = [null, null];
|
||||
for (let i = 0; i < 30 * 10 && bombs.some((x) => !x); i++) {
|
||||
h.step();
|
||||
|
||||
Reference in new issue
Block a user