Files
ark-stronghold-protocol/test/sim/feedback2-forced-exit-down.test.js
Sukvii ffdb1293c4 Deploy-timed skills run the normal duration lifecycle: one charge on deployment, a duration bar, a real end, again on the next deployment (#109)
宴 S2, 斯卡蒂 S2, 野鬃 S1, 砾 S1 / S2, 伊内丝 S3, 缄默德克萨斯 S1–S3 and 耀骑士临光 S2 used a separate timed buff while the skill stayed active; equipment and bond effects that read the skill state now see it end. 伊内丝's first sentry placement starts no skill. Contributed by @Sukvii.
2026-10-05 22:36:29 +08:00

104 lines
5.7 KiB
JavaScript
Raw Permalink 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.
// test/sim/feedback2-forced-exit-down.test.js — GitHub #60 「42突袭死后会消失」 (42 = 史尔特尔): every operator that leaves
// the field lies down on its tile and redeploys there — PRTS 卫戍协议/帮助 §作战阶段 单位部署: "干员退场后,将返回隐藏的待部署区,
// 并原地留下一个“倒地干员”以供查看信息,满足再部署条件时,移除场上的该倒地干员并自动部署至该位置。" (the owner's decision of
// 2026-10-04). A forced exit — 史尔特尔's 余烬 (8 s after the lethal blow), 耀骑士临光 S2's withdrawal, the 骑士戒律 + 竞技旗
// combo, 伊内丝 S3's first deployment, a 商人 that cannot pay — used to leave no body and no ring and to come back on the
// board tile (Battle.isDown took only 'killed' / FORCED_EXIT); it still is no kill. The 突袭 retreat ('raid') redeploys at
// once and leaves nothing; a real knock-out is unchanged.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { makeBattle, enemyRec } from '../helpers/battleHarness.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const chess = JSON.parse(readFileSync(path.join(ROOT, 'data/chess.json'), 'utf8'));
const SURTR = 'chess_char_5_07_a', NEARL2 = 'chess_char_6_17_a', INSIDE = 'chess_char_1_01_a';
const DUMMY = { enemy_far: enemyRec({ key: 'enemy_far', hp: 1e9, speed: 0, atk: 0 }) };
const approx = (a, b, msg, eps = 0.05) => assert.ok(Math.abs(a - b) <= eps, `${msg}: ${a} vs ${b}`);
const RAID = { raidShip: { count: 2, active: true, tier: 1, layers: 10 } };
function field(units, bonds = {}) {
return makeBattle({ defs: { enemies: DUMMY }, units, bonds, enemies: [{ key: 'enemy_far', pos: [12, 8] }], autoFinish: false, timeLimit: 300, hooks: ['death', 'kill', 'deploy'] });
}
const downOf = (h, u) => (h.b.snapshot().down || []).find((d) => d[0] === u.id) ?? null;
/** Lethal blow on 史尔特尔: 余烬 holds her at 1 HP, then forces her out 8 s later. */
function ember(h, u) {
h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
assert.ok(u.alive && u.hp >= 1, '余烬 holds her at 1 HP');
assert.ok(h.runUntil(() => !u.alive, 10), 'forced out');
assert.equal(u.removeReason, 'retreat');
}
test('史尔特尔 after 余烬 lies down on her tile (b.snap down, field meta) and redeploys there after 70 s — no kill', () => {
const h = field([{ chessId: SURTR, row: 10, col: 3, dir: 'RIGHT' }]);
const u = h.unit(SURTR);
h.run(1);
ember(h, u);
assert.ok(h.b.isDown(u));
const d = downOf(h, u);
assert.ok(d, 'the 倒地干员 and its redeploy ring');
assert.deepEqual(d.slice(4), [10, 3]);
approx(d[2], 70, 'her redeploy time');
assert.ok(h.b.fieldMeta().units.some((x) => x.id === u.id), 'a client joining late draws her down');
assert.equal(h.hooksOf('kill').filter((c) => c.victim === u).length, 0, 'no knock-out hooks');
assert.deepEqual(h.hooksOf('death').filter((c) => c.unit === u).map((c) => c.reason), ['retreat']);
assert.equal(h.b.result().perPlayer.p1.deaths, 0, 'no knock-down counted');
h.b.getPlayer(u.ownerId).dp = 99;
const out = u.deathAt;
assert.ok(h.runUntil(() => u.alive, 75));
approx(h.b.time - out, 70, 'back after her redeploy time', 0.1);
assert.deepEqual([u.tileR, u.tileC], [10, 3]);
assert.equal(downOf(h, u), null);
h.invariants();
});
test('史尔特尔 with 突袭: forced out after her jump she lies on the landing tile and comes back there, not on her board tile', () => {
const h = field([{ chessId: SURTR, row: 10, col: 3, dir: 'RIGHT' }], RAID);
const u = h.unit(SURTR);
assert.ok(h.runUntil(() => u.tileR !== u.homeR || u.tileC !== u.homeC, 30), 'jumped');
const at = [u.tileR, u.tileC];
ember(h, u);
assert.deepEqual(downOf(h, u)?.slice(4), at, 'down where she stood');
assert.ok(h.b.isReservedTile(at[0], at[1]), 'no ally lands on her body');
h.b.getPlayer(u.ownerId).dp = 99;
const n = h.hooksOf('deploy').filter((c) => c.unit === u).length;
assert.ok(h.runUntil(() => u.alive, 75));
assert.equal(h.hooksOf('deploy').filter((c) => c.unit === u).length, n + 1);
assert.deepEqual([u.tileR, u.tileC], at, 'redeployed on that tile (it used to be her board tile)');
assert.deepEqual([u.homeR, u.homeC], [10, 3], 'her home is unchanged');
});
test('耀骑士临光 S2: withdrawn at the skill end, she lies down on her tile with the ×1.25 redeploy ring and comes back there', () => {
const idx = chess[NEARL2].skills.findIndex((s) => (s.skillId ?? s.id) === 'skchr_nearl2_2');
assert.ok(idx >= 0);
const h = field([{ chessId: NEARL2, row: 10, col: 4, skillIndex: idx }]);
const u = h.unit(NEARL2);
h.step();
assert.ok(u.alive && u.skill.active, 'S2 on deploy');
assert.ok(h.runUntil(() => !u.alive, 40), 'withdraws when the skill ends');
assert.equal(u.removeReason, 'retreat');
const d = downOf(h, u);
assert.deepEqual(d?.slice(4), [10, 4]);
approx(d[2], u.base.respawnTime * 1.25, 'this redeploy ×1.25', 0.02);
assert.equal(h.hooksOf('kill').filter((c) => c.victim === u).length, 0);
h.b.getPlayer(u.ownerId).dp = 99;
assert.ok(h.runUntil(() => u.alive, d[2] + 2));
assert.deepEqual([u.tileR, u.tileC], [10, 4]);
});
test('a real knock-out is unchanged: down on its tile, the kill hooks and the knock-down count', () => {
const h = field([{ chessId: INSIDE, row: 10, col: 3, dir: 'RIGHT' }]);
const u = h.unit(INSIDE);
h.run(1);
h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
assert.ok(!u.alive);
assert.equal(u.removeReason, 'killed');
assert.deepEqual(downOf(h, u)?.slice(4), [10, 3]);
assert.equal(h.hooksOf('kill').filter((c) => c.victim === u).length, 1);
assert.equal(h.b.result().perPlayer.p1.deaths, 1);
});