Files
ark-stronghold-protocol/test/sim/feedback3-cold-freeze.test.js
T
sganggs e47b098d43 feedback5 CB1: a pair of colds on an enemy becomes the freeze, no cold is left — the 谢拉格 wind no longer freezes for good from 51 layers on (community reports 「谢拉格盟约冰冻时间没有随层数正确成长」 and 「…被在无法被任何干员攻击到的地方永控」, items 7 and 8)
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.
2026-10-07 01:39:50 +08:00

137 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
});