feedback2 WK: an operator forced off the field (史尔特尔's 余烬, 耀骑士临光 S2, 骑士戒律 + 竞技旗, 伊内丝 S3, a 商人 that cannot pay) lies down on its tile and redeploys there like a knocked-out one, still no kill (PRTS 卫戍协议/帮助; GitHub #60)
This commit is contained in:
1 parent
68da23e8ed
commit
42d9c3744b
6 files changed
+154
-32
No files matched your search
@@ -50,23 +50,25 @@ test('a knocked-out operator is `down` until it redeploys: its timer, then waiti
|
||||
assert.ok(h.eventsOf('deploy').some((e) => e[1] === a.id));
|
||||
});
|
||||
|
||||
test('summons, devices, enemies and withdrawn / removed operators are never `down`', () => {
|
||||
test('summons, devices, enemies, operators removed for good and the 突袭 retreat are never `down`; an operator forced out is (GitHub #60)', () => {
|
||||
const h = makeBattle({
|
||||
defs: { chess: { t_a: op('t_a'), t_b: op('t_b'), t_c: op('t_c') }, enemies: { enemy_dummy: enemyRec({ key: 'enemy_dummy', hp: 100, speed: 0 }) } },
|
||||
units: [{ chessId: 't_a', row: 9, col: 5 }, { chessId: 't_b', row: 10, col: 5 }, { chessId: 't_c', row: 11, col: 5 }],
|
||||
defs: { chess: { t_a: op('t_a'), t_b: op('t_b'), t_c: op('t_c'), t_d: op('t_d') }, enemies: { enemy_dummy: enemyRec({ key: 'enemy_dummy', hp: 100, speed: 0 }) } },
|
||||
units: [{ chessId: 't_a', row: 9, col: 5 }, { chessId: 't_b', row: 10, col: 5 }, { chessId: 't_c', row: 11, col: 5 }, { chessId: 't_d', row: 9, col: 6 }],
|
||||
enemies: [{ key: 'enemy_dummy', pos: [11, 8] }], content: 'none', autoFinish: false, timeLimit: 120,
|
||||
});
|
||||
h.step();
|
||||
const tok = h.b.spawnToken('p1', 'token_test', 12, 5, { def: TOKEN });
|
||||
h.b.kill(tok);
|
||||
h.b.retreat(h.unit('t_a'), { reason: 'retreat' });
|
||||
h.b.retreat(h.unit('t_b'), { reason: 'expired', permanent: true });
|
||||
h.b.retreat(h.unit('t_d'), { reason: 'raid' }); // 突袭: redeployed at once on its landing tile (here: nowhere)
|
||||
h.b.kill(h.enemy('enemy_dummy'));
|
||||
assert.equal(h.snapshot().down, undefined);
|
||||
// PRTS 卫戍协议/帮助 "干员退场后…原地留下一个“倒地干员”…": a forced exit (史尔特尔's 余烬 …) lies down like a knock-out
|
||||
h.b.retreat(h.unit('t_a'), { reason: 'retreat' });
|
||||
h.b.kill(h.unit('t_c'));
|
||||
assert.deepEqual(h.snapshot().down.map((d) => d[0]), [h.unit('t_c').id], 'only the knocked-out operator');
|
||||
assert.deepEqual(h.snapshot().down.map((d) => d[0]), [h.unit('t_a').id, h.unit('t_c').id], 'the forced-out and the knocked-out operator');
|
||||
const ids = new Set(h.b.fieldMeta().units.map((u) => u.id));
|
||||
assert.ok(ids.has(h.unit('t_c').id) && !ids.has(h.unit('t_a').id) && !ids.has(tok.id));
|
||||
assert.ok(ids.has(h.unit('t_c').id) && ids.has(h.unit('t_a').id) && !ids.has(h.unit('t_b').id) && !ids.has(h.unit('t_d').id) && !ids.has(tok.id));
|
||||
});
|
||||
|
||||
test('carryState.down (联防): deployed with everyone, forced out at once — down on its tile without the knock-out hooks — then back after its timer', () => {
|
||||
|
||||
@@ -392,7 +392,8 @@ test('redeploy: { tile } lands on another tile (home unchanged; knocked out ther
|
||||
approx(atDeploy, sp, 1e-9, 'SP restored before the deploy hook');
|
||||
approx(u.hp, u.s.maxHp);
|
||||
// plain redeploy: initSp; knocked out there later → the auto-redeploy comes back on that tile (PRTS 卫戍协议/帮助
|
||||
// "…原地留下一个“倒地干员”…满足再部署条件时…自动部署至该位置"; player report F5 after 0.1.0); a retreat → home
|
||||
// "…原地留下一个“倒地干员”…满足再部署条件时…自动部署至该位置"; player report F5 after 0.1.0); a retreat too (every 退场:
|
||||
// GitHub #60)
|
||||
h.b.retreat(u);
|
||||
h.b.redeploy(u, { tile: [12, 8] });
|
||||
approx(u.skill.sp, 2, 1e-9, 'without keepSp: initial SP');
|
||||
@@ -402,8 +403,9 @@ test('redeploy: { tile } lands on another tile (home unchanged; knocked out ther
|
||||
assert.ok(h.runUntil(() => u.alive, u.base.respawnTime + 1));
|
||||
assert.deepEqual([u.tileR, u.tileC], [12, 8], 'auto-redeploy on the tile it lay on');
|
||||
h.b.retreat(u);
|
||||
assert.deepEqual(h.b.snapshot().down?.map((d) => d.slice(4)), [[12, 8]], 'a retreated operator lies where it stood');
|
||||
assert.equal(h.b.redeploy(u), true);
|
||||
assert.deepEqual([u.tileR, u.tileC], [10, 4], 'a retreated unit comes back on its home tile');
|
||||
assert.deepEqual([u.tileR, u.tileC], [12, 8], 'and comes back there, not on its home tile (GitHub #60)');
|
||||
// paid redeploy on a tile
|
||||
h.b.retreat(u);
|
||||
const dp = p.dp;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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.findBuff('nearl2:night'), '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);
|
||||
});
|
||||
Reference in new issue
Block a user