- Stock: fixed trait gifts (缪尔赛思 / 变形者集群 变形同构体, 耶拉 不融冰, 蜜蜡 / 卡涅利安 迅捷作战粮, 诗怀雅 盟约之币) still occupy stock but are granted past the cap; an operator card already shown in the shop or a reward can be taken after the pool runs out and overdraws (GitHub #498, the owner's decision); item buys and item rewards keep the stock check. 洛洛 / 焰尾 sub-pools use the measured weights (PR #497 by @2321Robin; the occupancy idea of PR #496 by @2321Robin re-implemented differently). - Boss pairs are drawn at random once per match and fixed to battle start; the Hidden Core reuses them; a leaver does not split a full pair (the owner's decision). - 阿戈尔 in 联防 no longer re-applies the opening devour HP loss or a false revive; Touch's team medic stays for the match after its holder leaves (owner-approved, official lifetime unverified). - Auto-skill ranges: 司霆惊蛰 S2, 斩业星熊 S3 against air, 予愿安洁莉娜 S3; 自在's post-rebirth invulnerability is also untargetable (#510); Horn / Clementia / catapult / 忍冬 first hit (PR #502 by @Xinglan233); 不融冰 / tear gas trigger on real damage. - Bond layer-up sound in round start / draft / prep / battle (#482): one channel, a new cue replaces the old, no extra cooldown; a prep update that adds layers and activates a bond plays one cue. - Client: multi-swing fix (#506), refresh single-flight (PR #507 by @robll-v1), LP tower keeps the settled value with a −N tick (PR #504 by @robll-v1), export with only 潜能 / 练度 changes (PR #503 by @Xinglan233), spectator rosters list DIY members (PR #505 by @Xinglan233), banned members beside bond tooltips (PR #494 by @YuriRestia), recovery action above the away overlay (PR #511 by @RainoutOvO), the /dev/sim-check.html consistency page (PR #492 by @Cloudnyco), Korean (PR #490 by @D-Roote) and English Victoria wording (PR #499 by @YuriRestia). - golden: 45 of 287 scenarios move against 0.2.4 (roster 17, bonds 3, fields 6, matches 17, standins 2), each attributed in docs/history/0.2.5.md §30.9 / §30.16.
62 lines
3.0 KiB
JavaScript
62 lines
3.0 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
|
import { rotateOffset } from '../../server/sim/dir.js';
|
|
|
|
const CASES = [
|
|
['char_1043_leizi2', 1, [0, 3]],
|
|
['char_1044_hsgma2', 2, [0, 2]],
|
|
['char_1015_aglna2', 2, [1, -1]],
|
|
];
|
|
function field(charId, skillIndex, { tier = 6, elite = true, dir = 'RIGHT', cooldown = 0 } = {}) {
|
|
const h = makeBattle({ units: [{ uid: 1, diy: { slot: tier, charId, skillIndex }, elite, row: 10, col: 5, dir }],
|
|
defs: { enemies: { dummy: enemyRec({ key: 'dummy', hp: 1e9, atk: 0, speed: 0 }),
|
|
flyer: enemyRec({ key: 'flyer', hp: 1e9, atk: 0, speed: 0, motion: 'FLY' }) } },
|
|
flags: { startOpCooldown: cooldown }, autoFinish: false, timeLimit: 90 });
|
|
h.step();
|
|
return { h, u: h.unit(1) };
|
|
}
|
|
|
|
for (const [charId, skillIndex, delta] of CASES) {
|
|
test(`${charId}: all DIY forms cast for a target in the actual skill area outside normal targeting`, () => {
|
|
for (const tier of [5, 6]) for (const elite of [false, true]) for (const dir of ['UP', 'RIGHT', 'DOWN', 'LEFT']) {
|
|
for (const key of ['dummy', 'flyer']) {
|
|
const { h, u } = field(charId, skillIndex, { tier, elite, dir });
|
|
// Leizi's terrain flood and Hoshiguma's x-2 are symmetric; Angelina's rear diagonal follows her facing.
|
|
const [dr, dc] = charId === 'char_1015_aglna2' ? rotateOffset(...delta, dir) : delta;
|
|
h.spawn(key, { pos: [10 + dr, 5 + dc] });
|
|
u.skill.addCharge(1); h.step();
|
|
assert.equal(u.skill.activations, 1, `${charId}/${tier}/${elite}/${dir}/${key}`);
|
|
assert.equal(h.b.errors.length, 0); checkInvariants(h.b);
|
|
}
|
|
}
|
|
});
|
|
test(`${charId}: expanded auto-cast keeps targetability and operation cooldown`, () => {
|
|
for (const flag of ['stealth', 'untargetable', 'sleep']) {
|
|
const { h, u } = field(charId, skillIndex);
|
|
const e = h.spawn('flyer', { pos: [10 + delta[0], 5 + delta[1]] });
|
|
h.b.addBuff(e, { key: 'test:hidden', flags: { [flag]: true } });
|
|
u.skill.addCharge(1); h.run(0.5);
|
|
assert.equal(u.skill.activations, 0, flag);
|
|
h.b.removeBuff(e, 'test:hidden'); h.step();
|
|
assert.equal(u.skill.activations, 1, `${flag} removed`);
|
|
}
|
|
const { h, u } = field(charId, skillIndex, { cooldown: 3 });
|
|
h.spawn('flyer', { pos: [10 + delta[0], 5 + delta[1]] });
|
|
u.skill.addCharge(1); h.run(1);
|
|
assert.equal(u.skill.activations, 0);
|
|
h.runUntil(() => u.skill.activations > 0, 3);
|
|
assert.equal(u.skill.activations, 1);
|
|
assert.ok(u.skill.lastStart >= 3);
|
|
});
|
|
}
|
|
|
|
test('Leizi S2 uses low-terrain reachability and permanent range extension, without changing ordinary range', () => {
|
|
for (const extended of [false, true]) {
|
|
const { h, u } = field('char_1043_leizi2', 1);
|
|
if (extended) h.b.addBuff(u, { key: 'test:range', mods: { rangeExtend: 1 }, persist: true });
|
|
h.spawn('flyer', { pos: [10, 9] }); u.skill.addCharge(1); h.step();
|
|
assert.equal(u.skill.activations, extended ? 1 : 0);
|
|
}
|
|
});
|