PRTS 术语释义 寒冷 「友方寒冷始终需要两两一对产生友方冻结,持续时间取双方之中最高」, 异常效果 COLD 「在特定条件下转变为冻结」: applyStatus kept the older cold on for as long as the freeze, so every later cold froze again; the wind (every 25 s, 20 + 0.1 x layers s of cold) froze an enemy anywhere for the rest of the battle once its cold outlasted 25 s, never below. Now the pair turns into the freeze (max of the two, as before) and leaves no cold on an enemy; a cold on an operator (敌方寒冷) keeps the old rule. The wind alone now freezes 20 + 0.1 x L s of every 50 s from 51 layers (all of it from 300). Tests: test/sim/feedback5-kjerag-cold.test.js (fails on the parent); feedback3-cold-freeze and 灵知 S2 (kits_t4) updated to the pairing. Docs: SIM.md cold row, design/engine.md. Golden: every scenario where a second cold met an enemy still cold (灵知, 初雪, 雪猎, 耶拉, 圣聆初雪, 凛御银灰, items, the 谢拉格 wind) — the pair no longer leaves a cold: roster-005 / 009 / 010 / 012 / 014 / 015 / 016 / 018 / 026 / 029 / 031 / 036 / 040 / 042 / 046 / 047 / 049; bond-egirShip-high, bond-indomShip-low, bond-kjeragShip-high, bond-maniShip-high, bond-raidShip-low, bond-steadShip-high, bond-suntShip-low; boss-boss_2-pair / boss_3-pair / boss_4-pair / boss_6-pair, hidden-boss_8-pair / boss_10-pair; matches solo-HARD-1, solo-ABYSS-1, coop3-ABYSS-5, coop4-FUNNY-6, coop4-ABYSS-7.
137 lines
6.5 KiB
JavaScript
137 lines
6.5 KiB
JavaScript
// A second 寒冷 used to apply a fixed 3 s 冻结 (COLD_FREEZE_DURATION) whatever the two colds lasted.
|
||
// PRTS 术语释义 寒冷 (id ba.cold): 友方寒冷 pairs into 友方冻结, 「持续时间取双方之中最高」 — max(remaining, incoming). Since
|
||
// 0.2.0 the pair BECOMES the freeze (「两两一对产生友方冻结」, 异常效果 COLD 「转变为冻结」): no cold is left on the enemy
|
||
// (until then the older cold stayed on for as long as the freeze — test/sim/feedback5-kjerag-cold.test.js).
|
||
// 抵抗 shortens the incoming duration before that max. A frozen-immune enemy still gets no freeze.
|
||
// [ASSUMED] the engine's one cold uses that 友方 sentence for an enemy-applied cold too (PRTS states the max on
|
||
// the 友方 line only). gamedata_const ba.cold names the freeze and not its length.
|
||
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { makeBattle, enemyRec } from '../helpers/battleHarness.js';
|
||
import { hasGeneratedData } from '../../server/sim/simdata.js';
|
||
|
||
const REAL = { skip: !hasGeneratedData() };
|
||
const close = (a, b, eps, msg = '') => assert.ok(Math.abs(a - b) <= eps, `${msg} expected ${b} ± ${eps}, got ${a}`);
|
||
|
||
function coldBattle(extra = {}) {
|
||
return makeBattle({
|
||
defs: { enemies: { enemy_dummy: enemyRec({ key: 'enemy_dummy', hp: 1e6, speed: 0, res: 30 }) } },
|
||
enemies: [{ key: 'enemy_dummy', pos: [11, 8] }],
|
||
content: 'none', autoFinish: false, timeLimit: 60,
|
||
...extra,
|
||
});
|
||
}
|
||
|
||
test('cold 10s then immediately another cold 1s → freeze lasts about 10s, not 3', () => {
|
||
const h = coldBattle();
|
||
h.step();
|
||
const e = h.enemy('enemy_dummy');
|
||
h.b.applyStatus(e, 'cold', { duration: 10 });
|
||
h.b.applyStatus(e, 'cold', { duration: 1 });
|
||
assert.equal(e.findBuff('cold'), null, 'the pair became the freeze: no cold left');
|
||
close(e.findBuff('freeze').timeLeft, 10, 1e-6, 'freeze: the longer cold, not the shorter one');
|
||
assert.equal(e.s.res, 15, 'frozen: RES −15');
|
||
h.run(3.2);
|
||
assert.ok(e.s.flags.freeze, 'still frozen past the old fixed 3 s');
|
||
h.run(7);
|
||
assert.ok(!e.s.flags.freeze, 'ends with the 10 s cold');
|
||
assert.equal(e.s.res, 30);
|
||
});
|
||
|
||
test('cold 1s then cold 8s → freeze lasts about 8s, not 3', () => {
|
||
const h = coldBattle();
|
||
h.step();
|
||
const e = h.enemy('enemy_dummy');
|
||
h.b.applyStatus(e, 'cold', { duration: 1 });
|
||
h.b.applyStatus(e, 'cold', { duration: 8 });
|
||
assert.equal(e.findBuff('cold'), null);
|
||
close(e.findBuff('freeze').timeLeft, 8, 1e-6);
|
||
h.run(3.2);
|
||
assert.ok(e.s.flags.freeze, 'still frozen past 3 s');
|
||
h.run(5);
|
||
assert.ok(!e.s.flags.freeze, 'ends with the 8 s cold');
|
||
});
|
||
|
||
test('a shorter second cold does not shorten a longer remaining cold\'s freeze', () => {
|
||
const h = coldBattle();
|
||
h.step();
|
||
const e = h.enemy('enemy_dummy');
|
||
h.b.applyStatus(e, 'cold', { duration: 10 });
|
||
h.run(6);
|
||
const remain = e.findBuff('cold').timeLeft;
|
||
assert.ok(remain > 3.5 && remain < 4.5, `about 4 s left, got ${remain}`);
|
||
h.b.applyStatus(e, 'cold', { duration: 1 });
|
||
assert.equal(e.findBuff('cold'), null, 'both colds became the freeze');
|
||
close(e.findBuff('freeze').timeLeft, remain, 1e-6, 'freeze follows the remaining cold, not 1 s and not 3 s');
|
||
h.run(3.2);
|
||
assert.ok(e.s.flags.freeze, 'a 1 s or 3 s freeze would have ended');
|
||
h.run(1.2);
|
||
assert.ok(!e.s.flags.freeze);
|
||
});
|
||
|
||
test('抵抗 still shortens the incoming duration before the max', () => {
|
||
const h = coldBattle({ hooks: ['statusApplied'] });
|
||
h.step();
|
||
const e = h.enemy('enemy_dummy');
|
||
h.b.applyStatus(e, 'cold', { duration: 2 });
|
||
h.b.applyStatus(e, 'resist', { value: 0.5 });
|
||
h.b.applyStatus(e, 'cold', { duration: 10 });
|
||
// incoming 10 × (1 − 0.5) = 5, remaining 2 → the freeze is 5, not 10 and not 2.5 (and no cold is left)
|
||
assert.equal(e.findBuff('cold'), null, 'cold');
|
||
close(e.findBuff('freeze').timeLeft, 5, 1e-6, 'freeze');
|
||
const freeze = h.hooksOf('statusApplied').filter((c) => c.target === e && c.status === 'freeze');
|
||
assert.equal(freeze.length, 1);
|
||
close(freeze[0].duration, 5, 1e-9, 'the reported freeze duration');
|
||
h.run(3.2);
|
||
assert.ok(e.s.flags.freeze, 'not halved a second time down to 2.5 s');
|
||
h.run(2);
|
||
assert.ok(!e.s.flags.freeze, 'not the unresisted 10 s');
|
||
});
|
||
|
||
test('a frozen-immune enemy gets the second cold and no freeze', () => {
|
||
const h = makeBattle({
|
||
defs: { enemies: { enemy_icy: enemyRec({ key: 'enemy_icy', hp: 1e6, speed: 0, immunities: { frozen: true } }) } },
|
||
enemies: [{ key: 'enemy_icy', pos: [10, 8] }],
|
||
content: 'none', autoFinish: false,
|
||
});
|
||
h.step();
|
||
const icy = h.enemy('enemy_icy');
|
||
h.b.applyStatus(icy, 'cold', { duration: 10 });
|
||
h.b.applyStatus(icy, 'cold', { duration: 1 });
|
||
assert.ok(icy.s.flags.cold);
|
||
close(icy.findBuff('cold').timeLeft, 10, 1e-6, 'the colds still take the longer duration');
|
||
assert.equal(icy.findBuff('freeze'), null);
|
||
assert.ok(!icy.s.flags.freeze);
|
||
});
|
||
|
||
test('吉兆飞鳞: the stun when a freeze ends follows the longer or shorter cold, not a fixed 3 s', REAL, () => {
|
||
const run = (first, second) => {
|
||
const h = makeBattle({
|
||
autoFinish: false, timeLimit: 40, hooks: ['statusApplied'],
|
||
enemies: [{ key: 'enemy_10045_parrot', pos: [10, 6], mods: { speedMul: 0, hpMul: 100 } }],
|
||
});
|
||
h.step();
|
||
const e = h.enemy('enemy_10045_parrot');
|
||
h.b.applyStatus(e, 'cold', { duration: first });
|
||
h.b.applyStatus(e, 'cold', { duration: second });
|
||
const want = Math.max(first, second);
|
||
close(e.findBuff('freeze').timeLeft, want, 1e-6, `freeze ${first} then ${second}`);
|
||
h.run(3.2);
|
||
const early = h.hooksOf('statusApplied').filter((c) => c.target === e && c.status === 'stun');
|
||
if (want > 3.2) {
|
||
assert.ok(e.s.flags.freeze, 'still frozen at 3.2 s');
|
||
assert.equal(early.length, 0, '失温坠落 has not fired at the old 3 s mark');
|
||
}
|
||
h.run(want - 3.2 + 0.3);
|
||
assert.ok(!e.s.flags.freeze, 'the freeze has ended');
|
||
const stuns = h.hooksOf('statusApplied').filter((c) => c.target === e && c.status === 'stun');
|
||
// the 0.05 s 【失温坠落】 stun is what the freeze's end applies; its own status hook then grounds the bird
|
||
// (Stun.duration, 8 s). The harness hears the 8 s one first because that hook runs inside the 0.05 s emit.
|
||
assert.ok(stuns.some((c) => Math.abs(c.duration - 0.05) < 1e-6), `【失温坠落】 0.05 s stun, got ${stuns.map((c) => c.duration)}`);
|
||
return h;
|
||
};
|
||
run(1, 1); // shorter than the old 3 s: the stun is not held until 3 s
|
||
run(10, 1); // longer than 3 s: no stun at 3 s, stun when the 10 s freeze ends
|
||
});
|