feedback2 WB: 突袭 jumps only to a tile from which its target is in range, on the idle trigger too (GitHub issue #51)
Up to 0.1.1 only the 技能就绪 trigger required a landing tile that covers the target; the 10 s idle trigger took raidTile's fallback (a tile that covers nothing), so a member with nothing reachable hopped every 10 s between useless tiles and never went for a second ground enemy it could have hit. Now both triggers walk the candidates in the existing order (own field first, most advanced first, at most 8) and jump to the first one with a covering tile; none -> the member stays, its idle time keeps counting and the 0.25 s poll looks again. [ASSUMED]: the official text only says "再部署至一名地面敌人周围". raidTile now enumerates only the tiles a range offset leads back to from the enemy's body (same choice as the old 25-tile scan whenever a covering tile existed: 200 000 random cases), and a player's members share one sorted candidate list per poll (rebuilt after a jump), so a search that finds nothing stays cheap: +5.8 us per tick on the heavy boss field with 18 members that never reach anything, against +5.5 us for 0.1.1 (the plain requirement on the old scan: +29.6 us). Tests: two "突袭 #51" cases in test/content/bonds_addon.test.js.
This commit is contained in:
1 parent
2a03467897
commit
76e60a186b
2 files changed
+148
-40
No files matched your search
@@ -31,14 +31,16 @@
|
|||||||
// + 脆弱 ×damage_scale for weak[limit] s
|
// + 脆弱 ×damage_scale for weak[limit] s
|
||||||
// 助力 deputShip all operators DEF +(base + per·L), redeploy time ×(1 + respawn_time)
|
// 助力 deputShip all operators DEF +(base + per·L), redeploy time ×(1 + respawn_time)
|
||||||
// 突袭 raidShip member idle ≥ no_attack_duration s (or skill ready) with no enemy in range → "保留技力立即再部署"
|
// 突袭 raidShip member idle ≥ no_attack_duration s (or skill ready) with no enemy in range → "保留技力立即再部署"
|
||||||
// next to the most advanced ground enemy, on a free tile its position may be deployed on
|
// next to the most advanced ground enemy it can reach: on a free tile its position may be deployed
|
||||||
// (grid.canStand: never the 深水区 — player report #3 after 0.1.0, members dropped into 战场#08's
|
// on from which its range covers that enemy (GitHub issue #51 [ASSUMED]: the first of the 8 most
|
||||||
// pool after an enemy wading in it; Battle.isReservedTile: never a tile a knocked-out operator
|
// advanced that has such a tile; none → it stays and the next poll looks again, never a jump that
|
||||||
// lies on — player report F5, members landed on a fallen teammate): a real redeployment (retreat +
|
// hits nothing); grid.canStand: never the 深水区 (player report #3 after 0.1.0, members dropped
|
||||||
// free redeploy on the landing tile, full HP, `deploy` fires — 部署时 traits such as 史尔特尔,
|
// into 战场#08's pool after an enemy wading in it); Battle.isReservedTile: never a tile a knocked-out
|
||||||
// 突袭手雷, 卡西米尔, 叙拉古) with its SP / charges kept (engine redeploy tile + keepSp); ATK/HP
|
// operator lies on (player report F5, members landed on a fallen teammate). A real redeployment
|
||||||
// +(base + per·L) until it leaves the field; knocked out after a jump it lies where it fell and
|
// (retreat + free redeploy on the landing tile, full HP, `deploy` fires — 部署时 traits such as
|
||||||
// comes back there (the engine's rest tile, Battle._layBody — PRTS 卫戍协议/帮助 "原地留下一个
|
// 史尔特尔, 突袭手雷, 卡西米尔, 叙拉古) with its SP / charges kept (engine redeploy tile + keepSp);
|
||||||
|
// ATK/HP +(base + per·L) until it leaves the field; knocked out after a jump it lies where it fell
|
||||||
|
// and comes back there (the engine's rest tile, Battle._layBody — PRTS 卫戍协议/帮助 "原地留下一个
|
||||||
// “倒地干员”…自动部署至该位置"; its own home when it fell on another board piece's home);
|
// “倒地干员”…自动部署至该位置"; its own home when it fell on another board piece's home);
|
||||||
// L ≥ power_bond_stack_cnt: every operator ASPD +power_attack_speed
|
// L ≥ power_bond_stack_cnt: every operator ASPD +power_attack_speed
|
||||||
// 不屈 indomShip ground operator knocked out → p = min(1, base + per·L) immediate free redeploy where it lies
|
// 不屈 indomShip ground operator knocked out → p = min(1, base + per·L) immediate free redeploy where it lies
|
||||||
@@ -52,12 +54,13 @@
|
|||||||
// Hooks registered only when a bond needs them. Priorities: `hit` −20 (坚守 redirect, after other modifiers had their
|
// Hooks registered only when a bond needs them. Priorities: `hit` −20 (坚守 redirect, after other modifiers had their
|
||||||
// say), `death` 10 (不屈, the bond slot of the revive/redeploy convention); everything else 0.
|
// say), `death` 10 (不屈, the bond slot of the revive/redeploy convention); everything else 0.
|
||||||
|
|
||||||
import { absoluteRangeKeys, canTargetEnemy } from '../../../targeting.js';
|
import { canTargetEnemy, extendedGrid } from '../../../targeting.js';
|
||||||
import { localOrder, localBefore } from '../../../dir.js';
|
import { normDir, rotateOffset, localOrder, localBefore } from '../../../dir.js';
|
||||||
|
import { bodyKeys } from '../../../body.js';
|
||||||
import { isHpLoss } from '../../../damage.js';
|
import { isHpLoss } from '../../../damage.js';
|
||||||
import {
|
import {
|
||||||
num, bondRecord, buffParams, bondTier, bondLayers, isMember, isElite, isGroundOp, onField, playerOps, passiveBuff,
|
num, bondRecord, buffParams, bondTier, bondLayers, isMember, isElite, isGroundOp, onField, playerOps, passiveBuff,
|
||||||
fxOn, N4, N8, bodyInKeys, directMods,
|
fxOn, N4, N8, directMods, COLS,
|
||||||
} from '../../support/index.js';
|
} from '../../support/index.js';
|
||||||
|
|
||||||
export const ID = Object.freeze({
|
export const ID = Object.freeze({
|
||||||
@@ -75,6 +78,7 @@ const STEAD_CD = 'bond:steadShip:cd';
|
|||||||
const AURA_POLL = 0.25;
|
const AURA_POLL = 0.25;
|
||||||
const RAID_POLL = 0.25;
|
const RAID_POLL = 0.25;
|
||||||
const RAID_SEARCH = 2; // landing tiles within this Chebyshev distance of the target enemy
|
const RAID_SEARCH = 2; // landing tiles within this Chebyshev distance of the target enemy
|
||||||
|
const RAID_TARGETS = 8; // candidate enemies a poll tries per member, the most advanced first (raidTargets)
|
||||||
|
|
||||||
/** 奥术 vulnerability mods for a multiplier (battle.applyStrongest). */
|
/** 奥术 vulnerability mods for a multiplier (battle.applyStrongest). */
|
||||||
const arcaneMods = (v) => ({ artsTakenMul: v });
|
const arcaneMods = (v) => ({ artsTakenMul: v });
|
||||||
@@ -235,29 +239,59 @@ function updateAura(battle, st) {
|
|||||||
|
|
||||||
// =====================================================================================================================
|
// =====================================================================================================================
|
||||||
// 突袭 relocation
|
// 突袭 relocation
|
||||||
|
//
|
||||||
|
// Either trigger (技能就绪, or no attack for no_attack_duration s) jumps only to a landing tile from which the member's
|
||||||
|
// attack range covers the enemy it jumps to: the first candidate (raidTargets order, the RAID_TARGETS most advanced)
|
||||||
|
// that has one; when none has, the member stays where it is and the next poll looks again (its idle time keeps
|
||||||
|
// counting, so it jumps as soon as one can be reached). The official text only says "再部署至一名地面敌人周围" —
|
||||||
|
// "never a useless landing" is [ASSUMED] after GitHub issue #51 (up to 0.1.1 the idle trigger landed where nothing was
|
||||||
|
// in range, hopped between such tiles every 10 s and ignored a second enemy it could have reached; DESIGN §22.2).
|
||||||
|
|
||||||
function raidTile(battle, u, e) {
|
/**
|
||||||
const er = Math.round(e.y), ec = Math.round(e.x);
|
* The member's attack range as offsets from its tile: its own grid + rangeExtend (targeting.js extendedGrid, the
|
||||||
const ranged = u.def?.position === 'RANGED';
|
* relative form of absoluteRangeKeys) turned by its direction — flat [dRow, dCol, dRow, dCol, …].
|
||||||
const grid = u.rangeGrid || [[0, 0]];
|
*/
|
||||||
const ext = num(u.s.rangeExtend);
|
function raidReach(u) {
|
||||||
let best = null, bs = null;
|
const d = normDir(u.dir);
|
||||||
for (let dr = -RAID_SEARCH; dr <= RAID_SEARCH; dr++) {
|
const out = [];
|
||||||
for (let dc = -RAID_SEARCH; dc <= RAID_SEARCH; dc++) {
|
for (const [dr, dc] of extendedGrid(u.rangeGrid || [[0, 0]], num(u.s.rangeExtend))) out.push(...rotateOffset(dr, dc, d));
|
||||||
const r = er + dr, c = ec + dc;
|
return out;
|
||||||
if (!battle.grid.inRect(r, c) || battle.isReservedTile(r, c)) continue;
|
|
||||||
if (!battle.grid.canStand(r, c, { ranged })) continue;
|
|
||||||
const covers = bodyInKeys(e, absoluteRangeKeys(grid, r, c, u.dir, ext)) ? 0 : 1;
|
|
||||||
const d = Math.max(Math.abs(dr), Math.abs(dc)) + 0.01 * (Math.abs(dr) + Math.abs(dc));
|
|
||||||
// last tie-break: the offset in the unit's facing-RIGHT frame (sim/dir.js localOrder; for a RIGHT-facing unit the
|
|
||||||
// plain tile-key order), so the landing tile turns with its direction
|
|
||||||
const s0 = covers, s1 = d, s2 = localOrder(dr, dc, u.dir);
|
|
||||||
if (!bs || s0 < bs[0] || (s0 === bs[0] && (s1 < bs[1] - 1e-9 || (Math.abs(s1 - bs[1]) <= 1e-9 && localBefore(s2, bs[2]))))) { best = [r, c, covers === 0]; bs = [s0, s1, s2]; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best; // [row, col, targetInRange]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Landing tile [row, col] of a jump to enemy `e`, or null: a tile from which the member's range (`reach`, raidReach)
|
||||||
|
* covers the enemy's body (a huge enemy: any tile it occupies — body.js), within RAID_SEARCH tiles (Chebyshev) of the
|
||||||
|
* enemy, inside the field rect, that the member's position may be deployed on (grid.canStand: never the 深水区 —
|
||||||
|
* player report #3 after 0.1.0) and that is free (Battle.isReservedTile: no living unit, no knocked-out operator's
|
||||||
|
* body — player report F5 —, no waiting piece's tile). The nearest first (Chebyshev, then 0.01·Manhattan); last
|
||||||
|
* tie-break the offset in the member's facing-RIGHT frame (sim/dir.js localOrder; for a RIGHT-facing unit the plain
|
||||||
|
* tile-key order), so the landing tile turns with its direction. Only the tiles a range offset leads back to from a
|
||||||
|
* body tile are looked at, so a search that finds nothing stays cheap.
|
||||||
|
*/
|
||||||
|
function raidTile(battle, u, e, reach) {
|
||||||
|
const er = Math.round(e.y), ec = Math.round(e.x);
|
||||||
|
const ranged = u.def?.position === 'RANGED';
|
||||||
|
let best = null, bd = Infinity, bo = null;
|
||||||
|
for (const k of bodyKeys(e)) {
|
||||||
|
const br = Math.floor(k / COLS), bc = k - br * COLS;
|
||||||
|
for (let i = 0; i < reach.length; i += 2) {
|
||||||
|
const r = br - reach[i], c = bc - reach[i + 1], dr = r - er, dc = c - ec;
|
||||||
|
if (Math.abs(dr) > RAID_SEARCH || Math.abs(dc) > RAID_SEARCH) continue;
|
||||||
|
if (!battle.grid.inRect(r, c) || !battle.grid.canStand(r, c, { ranged }) || battle.isReservedTile(r, c)) continue;
|
||||||
|
const d = Math.max(Math.abs(dr), Math.abs(dc)) + 0.01 * (Math.abs(dr) + Math.abs(dc));
|
||||||
|
const o = localOrder(dr, dc, u.dir);
|
||||||
|
if (d < bd - 1e-9 || (Math.abs(d - bd) <= 1e-9 && localBefore(o, bo))) { best = [r, c]; bd = d; bo = o; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jump candidates of player `pid`, in priority order: the ground enemies an operator may target (not flying,
|
||||||
|
* canTargetEnemy) — those of the player's own field (`ownerId`), the others only when it has none —, the most advanced
|
||||||
|
* first (least remaining path distance, then the earliest spawned) [ASSUMED, research 02 §3.18]. The same list for
|
||||||
|
* every member of the player (canTargetEnemy reads the enemy, not the attacker).
|
||||||
|
*/
|
||||||
function raidTargets(battle, u, pid) {
|
function raidTargets(battle, u, pid) {
|
||||||
const own = [], other = [];
|
const own = [], other = [];
|
||||||
for (const e of battle.enemies) {
|
for (const e of battle.enemies) {
|
||||||
@@ -273,6 +307,7 @@ function raidTargets(battle, u, pid) {
|
|||||||
function raidPoll(battle, st) {
|
function raidPoll(battle, st) {
|
||||||
const bb = st.bb[ID.raid];
|
const bb = st.bb[ID.raid];
|
||||||
const idle = num(bb.no_attack_duration, 10);
|
const idle = num(bb.no_attack_duration, 10);
|
||||||
|
let targets = null; // the player's candidates (raidTargets), shared by its members until a jump changes the field
|
||||||
for (const u of st.members[ID.raid]) {
|
for (const u of st.members[ID.raid]) {
|
||||||
if (!onField(u) || !u.canAct) continue;
|
if (!onField(u) || !u.canAct) continue;
|
||||||
const since = Math.max(u.lastAttackAt ?? -Infinity, u.deployedAt ?? -Infinity, u.mem[KEY.raid] ?? -Infinity);
|
const since = Math.max(u.lastAttackAt ?? -Infinity, u.deployedAt ?? -Infinity, u.mem[KEY.raid] ?? -Infinity);
|
||||||
@@ -280,19 +315,22 @@ function raidPoll(battle, st) {
|
|||||||
const idleOk = battle.time - since >= idle - 1e-9;
|
const idleOk = battle.time - since >= idle - 1e-9;
|
||||||
if (!(ready || idleOk)) continue;
|
if (!(ready || idleOk)) continue;
|
||||||
if (battle.enemiesInKeys(u.rangeKeys || [], u, u.profile).length) continue;
|
if (battle.enemiesInKeys(u.rangeKeys || [], u, u.profile).length) continue;
|
||||||
const targets = raidTargets(battle, u, st.pid);
|
const list = (targets ??= raidTargets(battle, u, st.pid));
|
||||||
for (let i = 0; i < targets.length && i < 8; i++) {
|
if (!list.length) continue;
|
||||||
const tile = raidTile(battle, u, targets[i]);
|
// either trigger: raidTile only offers tiles with the target in range (without that a ready skill that finds no
|
||||||
|
// target would redeploy — firing every 部署时 effect — at every poll; the idle trigger, which lacked it up to
|
||||||
|
// 0.1.1, hopped every 10 s: issue #51)
|
||||||
|
const reach = raidReach(u);
|
||||||
|
for (let i = 0; i < list.length && i < RAID_TARGETS; i++) {
|
||||||
|
const tile = raidTile(battle, u, list[i], reach);
|
||||||
if (!tile) continue;
|
if (!tile) continue;
|
||||||
// the "skill ready" path has no idle gate: only jump where the target is in range, else a ready skill that never
|
targets = null; // the retreat / redeploy handlers (部署时 effects) may change the enemies: the next member re-sorts
|
||||||
// finds a target would redeploy (and fire every 部署时 effect) every poll
|
|
||||||
if (!idleOk && !tile[2]) continue;
|
|
||||||
const res = raidRedeploy(battle, u, tile[0], tile[1]);
|
const res = raidRedeploy(battle, u, tile[0], tile[1]);
|
||||||
if (!res) continue;
|
if (!res) continue;
|
||||||
u.mem[KEY.raid] = battle.time;
|
u.mem[KEY.raid] = battle.time; // with deployedAt: the idle time starts again from the landing
|
||||||
if (res === 'raid') {
|
if (res === 'raid') {
|
||||||
battle.addBuff(u, { key: KEY.raid, mods: st.raidMods });
|
battle.addBuff(u, { key: KEY.raid, mods: st.raidMods });
|
||||||
fxOn(battle, 'blink', u, KEY.raid, 'relocate', { target: targets[i].id });
|
fxOn(battle, 'blink', u, KEY.raid, 'relocate', { target: list[i].id });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// 绝技 and the prep side of 助力 远见 奇迹 投资人 调和 (numbers from data/bonds.json, research 02 §3.9–§3.23).
|
// 绝技 and the prep side of 助力 远见 奇迹 投资人 调和 (numbers from data/bonds.json, research 02 §3.9–§3.23).
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
import { makeBattle, chessRec, enemyRec, checkInvariants, flatStage } from '../helpers/battleHarness.js';
|
||||||
import { gainLayers, inRange } from '../../server/sim/content/support/index.js';
|
import { gainLayers, inRange } from '../../server/sim/content/support/index.js';
|
||||||
import { bondBb, procChance } from '../../server/sim/content/bonds/addon/battle.js';
|
import { bondBb, procChance } from '../../server/sim/content/bonds/addon/battle.js';
|
||||||
import { registerMeta } from '../../server/sim/content/bonds/addon.js';
|
import { registerMeta } from '../../server/sim/content/bonds/addon.js';
|
||||||
@@ -342,6 +342,76 @@ test('突袭: the jump is a redeployment — deploy fires (部署时 effects), f
|
|||||||
checkInvariants(h.b);
|
checkInvariants(h.b);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GitHub issue #51 (reported on 0.1.0, still so in 0.1.1): the 10 s idle jump landed where no enemy was in range and
|
||||||
|
// then hopped between such tiles every 10 s, and it ignored a second enemy it could have reached. Either trigger now jumps only to a landing tile
|
||||||
|
// with its target in range — the first candidate that has one — else the member stays and the next poll looks again
|
||||||
|
// [ASSUMED: the text says only "再部署至一名地面敌人周围"]. Row 9 here: cols 8–9 plain floor (not deployable); a
|
||||||
|
// RIGHT-facing melee member (its own tile + the tile in front) reaches a speed-0 enemy on (9,9) only from (9,9) or
|
||||||
|
// (9,8) — neither deployable.
|
||||||
|
const RAID51_STAGE = flatStage({ rows: { 9: '##Errrrrff' + 'S' + 'rrrrrrr' + 'S##' } });
|
||||||
|
const RAID51_ENEMY = { enemy_raid51: enemyRec({ key: 'enemy_raid51', hp: 1e7, speed: 0 }) };
|
||||||
|
const raidJumps = (h) => h.hooksOf('deploy').filter((c) => c.unit.kind === 'op' && !c.initial); // (an enemy spawn is a deploy too)
|
||||||
|
|
||||||
|
test('突袭 #51: no landing tile reaches the enemy → neither trigger jumps (no hopping); once one can, the jump comes at once', () => {
|
||||||
|
const defs = {
|
||||||
|
chess: { r_m: op('r_m', ['raidShip']), r_s: chessRec({ id: 'r_s', bonds: ['raidShip'], skill: { spCost: 10, initSp: 10 } }) },
|
||||||
|
enemies: RAID51_ENEMY,
|
||||||
|
};
|
||||||
|
const h = makeBattle({
|
||||||
|
stage: RAID51_STAGE, defs, bonds: { raidShip: bond(1, 10) }, enemies: [{ key: 'enemy_raid51', pos: [9, 9] }],
|
||||||
|
units: [{ chessId: 'r_m', row: 12, col: 3 }, { chessId: 'r_s', row: 11, col: 3 }], hooks: ['deploy', 'death'],
|
||||||
|
autoFinish: false, timeLimit: 120,
|
||||||
|
});
|
||||||
|
const m = h.unit('r_m'), s = h.unit('r_s');
|
||||||
|
h.run(45);
|
||||||
|
assert.equal(m.lastAttackAt, -Infinity, 'r_m idle the whole time (the 10 s trigger, four times over)');
|
||||||
|
assert.ok(s.skill.ready, 'r_s: its skill stays ready the whole time (the 技能就绪 trigger)');
|
||||||
|
assert.deepEqual(raidJumps(h), [], 'no jump: no landing tile has the enemy in range');
|
||||||
|
assert.deepEqual(h.hooksOf('death').filter((c) => c.reason === 'raid'), [], 'no 突袭 retreat');
|
||||||
|
assert.deepEqual([m.tileR, m.tileC, s.tileR, s.tileC], [12, 3, 11, 3], 'both stay where they are');
|
||||||
|
// a ground enemy that can be reached: both jump at the next poll (the idle time kept counting) and land with it in range
|
||||||
|
const t0 = h.b.time;
|
||||||
|
const e2 = h.spawn('enemy_raid51', { pos: [12, 7] });
|
||||||
|
h.run(0.3);
|
||||||
|
assert.equal(raidJumps(h).length, 2, 'both members jumped');
|
||||||
|
for (const c of raidJumps(h)) assert.ok(c.t - t0 <= 0.25 + 1e-6, `within one poll (${(c.t - t0).toFixed(2)} s)`);
|
||||||
|
for (const u of [m, s]) {
|
||||||
|
assert.ok(inRange(u, e2), `${u.defId}: the new enemy in range after the jump (${u.tileR},${u.tileC})`);
|
||||||
|
assert.ok(Math.max(Math.abs(u.tileR - 12), Math.abs(u.tileC - 7)) <= 2, `${u.defId}: next to it`);
|
||||||
|
}
|
||||||
|
checkInvariants(h.b);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('突袭 #51: the most advanced enemy out of reach → the jump goes to the next one it can reach and fights there; idle time restarts', () => {
|
||||||
|
const defs = { chess: { r_m: op('r_m', ['raidShip']) }, enemies: RAID51_ENEMY };
|
||||||
|
const idle = bondBb('raidShip').no_attack_duration;
|
||||||
|
const h = makeBattle({
|
||||||
|
stage: RAID51_STAGE, defs, bonds: { raidShip: bond(1, 10) }, hooks: ['deploy', 'death'], autoFinish: false, timeLimit: 120,
|
||||||
|
enemies: [{ key: 'enemy_raid51', pos: [9, 9] }, { key: 'enemy_raid51', pos: [12, 9] }], units: [{ chessId: 'r_m', row: 12, col: 3 }],
|
||||||
|
});
|
||||||
|
h.step();
|
||||||
|
const [e1, e2] = h.enemies();
|
||||||
|
assert.deepEqual([e1.y, e1.x, e2.y, e2.x], [9, 9, 12, 9]);
|
||||||
|
assert.ok(h.b.remainingDistance(e1) < h.b.remainingDistance(e2), 'the one out of reach is the more advanced (first candidate)');
|
||||||
|
const u = h.unit('r_m');
|
||||||
|
assert.ok(h.runUntil(() => raidJumps(h).length > 0, idle + 1), 'jumped after the idle time');
|
||||||
|
const t1 = h.b.time;
|
||||||
|
assert.ok(inRange(u, e2) && !inRange(u, e1), `landed on ${u.tileR},${u.tileC} with the second enemy in range`);
|
||||||
|
h.run(5);
|
||||||
|
assert.ok(u.lastAttackAt > t1 && e2.hp < e2.s.maxHp, 'it attacks the enemy it jumped to');
|
||||||
|
assert.equal(raidJumps(h).length, 1, 'busy there: no further jump');
|
||||||
|
// its enemy gone, a reachable one elsewhere: the next jump comes the idle time after its last attack, not at once
|
||||||
|
h.b.dealDamage(null, e2, { amount: 1e9, type: 'true' });
|
||||||
|
const last = u.lastAttackAt;
|
||||||
|
const e3 = h.spawn('enemy_raid51', { pos: [10, 5] });
|
||||||
|
assert.ok(!inRange(u, e3));
|
||||||
|
assert.ok(h.runUntil(() => raidJumps(h).length === 2, idle + 1), 'jumped again');
|
||||||
|
const dt = h.b.time - last;
|
||||||
|
assert.ok(dt >= idle - 1e-6 && dt <= idle + 0.25 + 1e-6, `${dt.toFixed(2)} s after its last attack`);
|
||||||
|
assert.ok(inRange(u, e3), 'the new enemy in range');
|
||||||
|
checkInvariants(h.b);
|
||||||
|
});
|
||||||
|
|
||||||
test('不屈: knocked-out ground operator redeploys (p=1 at high L); tier 2 every operator +5 SP; inactive / elevated → no', () => {
|
test('不屈: knocked-out ground operator redeploys (p=1 at high L); tier 2 every operator +5 SP; inactive / elevated → no', () => {
|
||||||
const bb = bondBb('indomShip');
|
const bb = bondBb('indomShip');
|
||||||
const sk = { spCost: 50, initSp: 0 };
|
const sk = { spCost: 50, initSp: 0 };
|
||||||
|
|||||||
Reference in new issue
Block a user