// 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 });