feedback7: merge fb7-tick — whole-frame attack intervals and time-SP costs take exactly that many frames, 1 s = 30 not 31 (PR #402 by @Cloudnyco, ported onto 0.2.2: 1e-9 on the ally / enemy attack countdown, the 双眼皮 turret and 寒檀's icicles, gainSp / _normalize; PRTS 作战机制 帧对齐); golden: every family moves (281 of 283, end-result flips in §27.61); union OK
This commit is contained in:
commit
e1b75e1308
21 files changed
+5008
-4776
No files matched your search
@@ -421,8 +421,9 @@ test('2_16 拉普兰德 S1 日晷: toggle (持续时间无限) ATK +atk; blocks
|
||||
const bb = bbAlt(id);
|
||||
for (const roll of [true, false]) {
|
||||
const seen = [];
|
||||
// 120 hits a minute at bat 0.5, each at the 5 % floor (12.5): 1500 in the 60 s, short of the normal form's 1791 HP
|
||||
const h = run({
|
||||
defs: { enemies: { e: dummy('e', { atk: 300, bat: 0.5 }) } }, units: [U(id, 9, 5, { carryState: READY })], enemies: [{ key: 'e', pos: [9, 5] }],
|
||||
defs: { enemies: { e: dummy('e', { atk: 250, bat: 0.5 }) } }, units: [U(id, 9, 5, { carryState: READY })], enemies: [{ key: 'e', pos: [9, 5] }],
|
||||
setup: (b) => { b.rng.chance = (p) => { seen.push(p); return roll; }; },
|
||||
});
|
||||
const u = h.unit(id);
|
||||
|
||||
@@ -78,6 +78,8 @@ test('every tier-6 skill × module choice (normal + elite) survives a real wave
|
||||
units: [{ chessId: id, row: 10, col: 4, skillIndex: s.index, moduleId, carryState: READY }, { chessId: 'chess_char_1_01_a', row: 9, col: 4 }, { chessId: 'chess_char_2_06_a', row: 11, col: 5 }],
|
||||
enemies: [{ key: 'enemy_1007_slime', count: 8, interval: 1.5 }, { key: 'enemy_1007_slime', route: 1, count: 8, interval: 1.5, time: 3 }],
|
||||
});
|
||||
// (a medic acts on the hurt only, and the slimes may fall before they reach anyone: the team starts at half HP)
|
||||
if (base.profession === 'MEDIC') { h.step(); for (const a of h.b.allyUnits) a.hp = a.s.maxHp / 2; }
|
||||
h.runToEnd(90);
|
||||
done(h);
|
||||
const u = h.b.allyUnits.find((x) => x.defId === id);
|
||||
@@ -258,7 +260,7 @@ test('6_03 余 S1 今日做东: taunt +1 while carried; TAKE_DAMAGE cast, HP/DEF
|
||||
const sid = 'skchr_yu_1', bb = bbOf(id, sid);
|
||||
const h = run({
|
||||
defs: { enemies: { hit: enemyRec({ key: 'hit', hp: 1e7, speed: 0, atk: 300, bat: 1 }) } },
|
||||
units: [U(id, sid, 10, 4, { carryState: READY })], enemies: [{ key: 'hit', pos: [10, 4] }],
|
||||
units: [U(id, sid, 10, 4, { carryState: READY })], enemies: [{ key: 'hit', pos: [10, 4] }], hooks: [...HOOKS, 'elementBurst'],
|
||||
});
|
||||
const u = h.unit(id);
|
||||
usesSkill(u, sid);
|
||||
@@ -270,8 +272,10 @@ test('6_03 余 S1 今日做东: taunt +1 while carried; TAKE_DAMAGE cast, HP/DEF
|
||||
approx(skillBuff(u).mods.defPct, bb.def);
|
||||
const t0 = h.b.time;
|
||||
h.run(4);
|
||||
const burns = h.hooksOf('elementHit').filter((c) => c.source === u && c.dmg.element === 'burn' && c.t > t0 && (c.dmg.tags || []).includes('skill'));
|
||||
const taken = h.hooksOf('damaged').filter((c) => c.target === u && c.dmg?.isAttack && c.t > t0);
|
||||
// the burns build a 燃烧 burst on the attacker, which refuses same-element fills while it runs: compare up to it
|
||||
const end = h.hooksOf('elementBurst').find((c) => c.element === 'burn' && c.t > t0)?.t ?? Infinity;
|
||||
const burns = h.hooksOf('elementHit').filter((c) => c.source === u && c.dmg.element === 'burn' && c.t > t0 && c.t <= end && (c.dmg.tags || []).includes('skill'));
|
||||
const taken = h.hooksOf('damaged').filter((c) => c.target === u && c.dmg?.isAttack && c.t > t0 && c.t <= end);
|
||||
assert.ok(burns.length >= 1 && burns.length === taken.length, `one burn per attack taken (${burns.length}/${taken.length})`);
|
||||
approx(burns[0].dmg.amount, u.s.atk * bb.ep_damage_ratio, 'ep_damage_ratio × ATK');
|
||||
done(h);
|
||||
|
||||
@@ -212,8 +212,9 @@ test('S1 坚守阵线 (AUTO, SP_FULL): at full SP with no enemy, 持续时间无
|
||||
const life = tokenTalent(tier, elite, mod).duration;
|
||||
assert.ok(h.runUntil(() => u.skill.active, sk.spCost + 1), `${label(f)}: cast at ${sk.spCost} s with no enemy`);
|
||||
approx(h.b.time, sk.spCost, `${label(f)}: at full SP`, 0.02);
|
||||
// the first shield expired before (life < cost) and came back at life + 30: it gets the 20 s now
|
||||
assert.ok(t.alive);
|
||||
// the first shield expired before (life < cost) and came back at life + 30: it gets the 20 s now — or, T6 (60 + 30 =
|
||||
// the 90 s cost), on its return a tick after the cast (its timers count from its deployment on the first tick)
|
||||
assert.ok(h.runUntil(() => t.alive, 0.1), `${label(f)}: the shield is on the field`);
|
||||
h.step();
|
||||
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `${label(f)}: ATK`);
|
||||
approx(u.s.def, u.base.def * (1 + sk.bb.def + 0.18), `${label(f)}: DEF (+18 % T1)`);
|
||||
|
||||
@@ -207,7 +207,9 @@ test('S2 驱凶辟邪: passive ASPD +15 / +20; cast on his attack, the target is
|
||||
approx(h.b.time - t0, sk.bb.paper_duration, `T${tier}: after 5 s`, 0.05);
|
||||
const dmg = h.hooksOf('damaged').slice(n0);
|
||||
const burst = dmg.filter((c) => c.source === u && c.dmg.tags.includes('lmlee:burst'));
|
||||
const hits = h.hooksOf('damaged').filter((c) => c.target === t && c.source === u && !c.dmg.tags.includes('lmlee:burst') && h.b.time >= t0).length;
|
||||
// his hits on it before the burst (one of his hits in the burst's own tick lands after it and is not counted)
|
||||
const all = h.hooksOf('damaged');
|
||||
const hits = all.slice(0, all.indexOf(burst[0])).filter((c) => c.target === t && c.source === u && !c.dmg.tags.includes('lmlee:burst')).length;
|
||||
assert.deepEqual(burst.map((c) => c.target).sort((a, b) => a.id - b.id), [t, near].sort((a, b) => a.id - b.id), `T${tier}: radius ${MARK_RADIUS}, the flyer too, not 2 tiles away`);
|
||||
assert.ok(!burst.some((c) => c.target === far));
|
||||
for (const c of burst) {
|
||||
|
||||
@@ -248,9 +248,10 @@ test('S3 延异视阈: enemy shots on his range fly at 5 % of their speed (back
|
||||
for (const g of h.enemies()) h.b.applyStatus(g, 'stun', { duration: 99, source: null }); // no new shot
|
||||
const held = h.b.projectiles.list.filter((p) => p.source && p.source.side === 'enemy').length;
|
||||
assert.ok(held >= 1, `${held} shots in the air as it ends`);
|
||||
const taken = u.stats.taken;
|
||||
h.runUntil(() => !u.skill.active, 2);
|
||||
h.step();
|
||||
// (until the end a held shot still reaches him at its 5 % speed — one a second from the gun 2 tiles away)
|
||||
const taken = u.stats.taken;
|
||||
const left = h.b.projectiles.list.filter((p) => p.source && p.source.side === 'enemy');
|
||||
if (!disarm) {
|
||||
assert.equal(left.length, 0, 'cleared');
|
||||
|
||||
@@ -124,10 +124,11 @@ test('S1 旋刃 (切换: on until she leaves; data DEFAULT): range −1 (the far
|
||||
const b = h.spawn('enemy_dummy', { pos: [11, 5] }), c = h.spawn('enemy_dummy', { pos: [9, 6] });
|
||||
const far = h.spawn('enemy_dummy', { pos: [12, 5] }); // 2 from A, 1 from B: reachable from B only
|
||||
h.run(0.1);
|
||||
const n0 = atkHits(h, u).length;
|
||||
const before0 = atkHits(h, u), n0 = before0.length;
|
||||
h.run(4);
|
||||
const after = atkHits(h, u).slice(n0);
|
||||
const id = after[0].dmg.attackId;
|
||||
// the first attack thrown after that (one in flight then has already hit A before the others came)
|
||||
const id = after.find((x) => !before0.some((y) => y.dmg.attackId === x.dmg.attackId)).dmg.attackId;
|
||||
const chain = after.filter((x) => x.dmg.attackId === id);
|
||||
assert.equal(chain.length, 4, `T${tier}: the hit + 3 bounces`);
|
||||
assert.equal(chain[0].target, lone, `T${tier}: her target`);
|
||||
|
||||
Reference in new issue
Block a user