The contributor's code (status.js _applyValuedStatus and both test files, applied as written from PR #348), so they are the author of this commit; the maintainer side added the 同名效果取最高 label back to the JSDoc and a docs/SIM.md line. buff.data.tail held ONE waiting application (the later-ending one): a third, weaker but longer one replaced a middle one that was still running, so when the strongest ended the effect jumped to the weakest (#342: 30 % / 25 % / 20 % 减防 — DEF 1000 → 700 → 800 instead of 750 → 800 → 1000). The tail is now a chain ordered by strength (decreasing) and end (increasing): a weaker application is inserted in it, a stronger one takes over with the old value and its chain behind it, an equal refresh trims waiters that end before it, and an expiry resumes the next live waiter with the rest of the chain. removeStatus / removeBuff still drop the whole chain; two-application data shapes are unchanged. Golden: six 自选 battles that stack three same-name applications on one unit — diy-059 / diy-122 / diy-123 / diy-124 (庇护 from several operators), diy-133 (Mon3tr's ASPD talent buff talent:monstr:aspd), diy-138 (脆弱) — the middle application now resumes.
126 lines
5.0 KiB
JavaScript
126 lines
5.0 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { makeBattle, enemyRec } from '../helpers/battleHarness.js';
|
|
|
|
function setup() {
|
|
const h = makeBattle({
|
|
defs: { enemies: { enemy_test: enemyRec({ key: 'enemy_test', hp: 1e6, atk: 100, def: 1000, speed: 0 }) } },
|
|
enemies: [{ key: 'enemy_test', pos: [11, 8] }], content: 'none', autoFinish: false,
|
|
});
|
|
h.step();
|
|
return { h, e: h.enemy('enemy_test') };
|
|
}
|
|
|
|
const orders = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]];
|
|
const approx = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-6, `${actual} ≈ ${expected}`);
|
|
|
|
for (const plain of [false, true]) {
|
|
for (const order of orders) {
|
|
test(`${plain ? 'applyStrongest' : 'fragile'}: three effects expire in strength order after application order ${order}`, () => {
|
|
const { h, e } = setup();
|
|
const effects = [{ value: 0.8, duration: 2 }, { value: 0.6, duration: 5 }, { value: 0.3, duration: 10 }];
|
|
const key = plain ? 'test:strongest' : 'fragile';
|
|
for (const i of order) {
|
|
if (plain) h.b.applyStrongest(e, key, { ...effects[i], mods: (v) => ({ dmgTakenMul: 1 + v }) });
|
|
else h.b.applyStatus(e, key, effects[i]);
|
|
}
|
|
for (const [elapsed, multiplier, damage] of [[0, 1.8, 180], [2.1, 1.6, 160], [3, 1.3, 130], [5, 1, 100]]) {
|
|
if (elapsed) h.run(elapsed);
|
|
approx(e.s.dmgTakenMul, multiplier);
|
|
assert.equal(h.b.dealDamage(null, e, { amount: 100, type: 'true' }), damage);
|
|
assert.equal(e.buffs.filter((b) => b.key === key).length, multiplier === 1 ? 0 : 1);
|
|
}
|
|
assert.deepEqual(h.b.errors, []);
|
|
});
|
|
}
|
|
}
|
|
|
|
test('strongest statuses: a weaker effect that expires under the winner never resumes', () => {
|
|
const { h, e } = setup();
|
|
h.b.applyStatus(e, 'fragile', { value: 0.6, duration: 3 });
|
|
h.b.applyStatus(e, 'fragile', { value: 0.3, duration: 10 });
|
|
h.b.applyStatus(e, 'fragile', { value: 0.8, duration: 5 });
|
|
h.run(5.1);
|
|
approx(e.s.dmgTakenMul, 1.3);
|
|
h.run(5);
|
|
assert.equal(e.findBuff('fragile'), null);
|
|
});
|
|
|
|
test('strongest statuses: equal-strength refresh skips expired fallbacks but keeps the later one', () => {
|
|
const { h, e } = setup();
|
|
for (const [value, duration] of [[0.8, 2], [0.6, 5], [0.3, 10]]) h.b.applyStatus(e, 'fragile', { value, duration });
|
|
h.run(1);
|
|
h.b.applyStatus(e, 'fragile', { value: 0.8, duration: 6 });
|
|
h.b.applyStatus(e, 'fragile', { value: 0.8, duration: 1 });
|
|
h.run(5.1);
|
|
approx(e.s.dmgTakenMul, 1.8);
|
|
h.run(1);
|
|
approx(e.s.dmgTakenMul, 1.3);
|
|
h.run(3);
|
|
assert.equal(e.findBuff('fragile'), null);
|
|
});
|
|
|
|
test('strongest statuses: refreshing a waiting effect preserves the weaker later fallback', () => {
|
|
const { h, e } = setup();
|
|
for (const [value, duration] of [[0.8, 2], [0.6, 5], [0.3, 10]]) h.b.applyStatus(e, 'fragile', { value, duration });
|
|
h.run(1);
|
|
h.b.applyStatus(e, 'fragile', { value: 0.6, duration: 6 });
|
|
h.run(4.1);
|
|
approx(e.s.dmgTakenMul, 1.6);
|
|
h.run(2);
|
|
approx(e.s.dmgTakenMul, 1.3);
|
|
h.run(3);
|
|
assert.equal(e.findBuff('fragile'), null);
|
|
});
|
|
|
|
test('strongest statuses: stackAs controls every fallback without changing its actual effect', () => {
|
|
const { h, e } = setup();
|
|
h.b.applyStatus(e, 'weaken', { value: 0.1, stackAs: 0.9, duration: 2 });
|
|
h.b.applyStatus(e, 'weaken', { value: 0.2, stackAs: 0.8, duration: 5 });
|
|
h.b.applyStatus(e, 'weaken', { value: 0.3, duration: 10 });
|
|
approx(e.s.atk, 90);
|
|
h.run(2.1);
|
|
approx(e.s.atk, 80);
|
|
h.run(3);
|
|
approx(e.s.atk, 70);
|
|
h.run(5);
|
|
approx(e.s.atk, 100);
|
|
});
|
|
|
|
test('strongest statuses: equal-priority waiting effects keep the longest-lived application value', () => {
|
|
const { h, e } = setup();
|
|
h.b.applyStatus(e, 'weaken', { value: 0.9, duration: 2 });
|
|
h.b.applyStatus(e, 'weaken', { value: 0.1, stackAs: 0.8, duration: 5 });
|
|
h.b.applyStatus(e, 'weaken', { value: 0.3, duration: 10 });
|
|
h.b.applyStatus(e, 'weaken', { value: 0.2, stackAs: 0.8, duration: 7 });
|
|
h.b.applyStatus(e, 'weaken', { value: 0.4, stackAs: 0.8, duration: 6 });
|
|
h.b.applyStatus(e, 'weaken', { value: 0.5, stackAs: 0.8, duration: 7 });
|
|
h.run(2.1);
|
|
approx(e.s.atk, 80);
|
|
h.run(3);
|
|
approx(e.s.atk, 80);
|
|
h.run(2);
|
|
approx(e.s.atk, 70);
|
|
h.run(3);
|
|
approx(e.s.atk, 100);
|
|
});
|
|
|
|
for (const plain of [false, true]) {
|
|
test(`${plain ? 'removeBuff' : 'removeStatus'} discards every waiting strongest effect`, () => {
|
|
const { h, e } = setup();
|
|
const key = plain ? 'test:strongest' : 'fragile';
|
|
for (const [value, duration] of [[0.8, 2], [0.6, 5], [0.3, 10]]) {
|
|
if (plain) h.b.applyStrongest(e, key, { value, duration, mods: (v) => ({ dmgTakenMul: 1 + v }) });
|
|
else h.b.applyStatus(e, key, { value, duration });
|
|
}
|
|
assert.equal(plain ? h.b.removeBuff(e, key) : h.b.removeStatus(e, key), 1);
|
|
h.b.applyStatus(e, 'fragile', { value: 0.1, duration: 1 });
|
|
h.run(2.1);
|
|
approx(e.s.dmgTakenMul, 1);
|
|
h.run(8);
|
|
approx(e.s.dmgTakenMul, 1);
|
|
assert.equal(e.findBuff(key), null);
|
|
assert.deepEqual(h.b.errors, []);
|
|
});
|
|
}
|