feedback7: merge fb7-fields — PR #337 boss-field portals teleport (route-end portals: hidden 3 s, out of [5,10], leak at the side's door), PR #347 碎铳之簧 chases the full 5 s, #370 耀骑士临光's redeploy reduction floored at 0, #375 the water-map patrol takes the official upper road
- Grok review: MERGE AFTER FIXES — the end-portal wait (0 s → 3 s, the data's modal crossing) fixed on the branch
(1205336a). solo-NORMAL-1 still fails the Hidden Core (explained in the branch commits).
- Golden union OK; broad node --test 5517 (5516 pass, 0 fail, 1 skipped); lint 0 errors.
This commit is contained in:
commit
31fee55c6c
18 files changed
+819
-336
No files matched your search
@@ -247,7 +247,9 @@ test('E2 audit: healers on the lane tiles of every boss / hidden field deal no d
|
||||
const lead = (e) => e.isBoss || e.tag === 'boss' || e.tag === 'part' || LUCIEN.test(e.defId);
|
||||
let healsOnLead = 0;
|
||||
h.b.on('heal', (c) => { if (healers.includes(c.source) && c.amount > 0 && c.source.blocking.some(lead)) healsOnLead++; });
|
||||
for (let i = 0; i < 120 * 30; i++) {
|
||||
// 200 s on the fields whose leaders / parts a healer must block (the solo 铳 field's springs first meet a healer at
|
||||
// 145 s since their 末日布道 chase lasts its whole 5 s — PR #347), 120 s elsewhere
|
||||
for (let i = 0; i < (EXPECT[key] ? 200 : 120) * 30; i++) {
|
||||
h.step();
|
||||
// operators break the 碎铳之簧 shields (unblockable while shielded): a hit of the kind each shield yields to
|
||||
for (const e of h.b.enemies) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Blockable-ground preference of the ground pathing (grid.js header; user playtest #2: on 战场#01 the lower-gate enemies
|
||||
// walked up the col-9 floor lane, where no operator can stand, instead of the col-8 road). The flow field keeps the
|
||||
// official route unless the preference route — the fewest non-blockable tiles among equal-length chains, smoothing
|
||||
// that never cuts across floor its grid route does not walk — crosses fewer non-blockable tiles. "Crosses" = the
|
||||
// that never cuts across floor its grid route does not walk — crosses fewer non-blockable tiles and no more 深水区
|
||||
// (GitHub #375). "Crosses" = the
|
||||
// segment passes through the tile's interior (grid.js segmentTiles; brushing a corner is not crossing — the D5 report
|
||||
// after 0.1.0, test/sim/pathing-official.test.js). Audit: every active stage × gate × field (normal, 联防 both halves,
|
||||
// boss both halves incl. the solo `_s` templates) — the non-blockable tiles an enemy still crosses are listed and
|
||||
@@ -69,27 +70,36 @@ function nbCrossed(g, s, e) {
|
||||
|
||||
/**
|
||||
* Fewest non-blockable tiles (start and goal excluded) over all 4-connected crate-free routes s → e of length ≤ the
|
||||
* shortest + `slack`; { best, min } (min = the minimum over those lengths).
|
||||
* shortest + `slack` that cross at most `maxWet` 深水区 tiles (grid.js: the tie-break never walks into more 深水区 than
|
||||
* the official route — GitHub #375); { best, min } (min = the minimum over those lengths).
|
||||
*/
|
||||
function fewestNb(g, s, e, slack = 2) {
|
||||
function fewestNb(g, s, e, slack = 2, maxWet = Infinity) {
|
||||
const f = g.flowField(e[0], e[1]);
|
||||
const best = f.dist[s[0] * COLS + s[1]];
|
||||
if (best < 0 || best >= 1000) return null;
|
||||
const ek = e[0] * COLS + e[1];
|
||||
const cost = (k) => (k === ek ? 0 : g.unblockable[k]);
|
||||
let cur = new Map([[s[0] * COLS + s[1], 0]]);
|
||||
const wetOf = Number.isFinite(maxWet) ? (k) => (k === ek ? 0 : g.deepWater[k]) : () => 0;
|
||||
// tile → (深水区 so far → fewest non-blockable so far)
|
||||
let cur = new Map([[s[0] * COLS + s[1], new Map([[0, 0]])]]);
|
||||
let min = Infinity;
|
||||
for (let L = 0; L <= best + slack; L++) {
|
||||
if (cur.has(ek)) min = Math.min(min, cur.get(ek));
|
||||
if (cur.has(ek)) for (const p of cur.get(ek).values()) min = Math.min(min, p);
|
||||
const nx = new Map();
|
||||
for (const [k, p] of cur) {
|
||||
for (const [k, byWet] of cur) {
|
||||
if (k === ek) continue;
|
||||
const r = (k / COLS) | 0, c = k % COLS;
|
||||
for (const [dr, dc] of [[1, 0], [0, 1], [-1, 0], [0, -1]]) {
|
||||
const nr = r + dr, nc = c + dc;
|
||||
if (!g.walkable(nr, nc) || g.isCrate(nr, nc)) continue;
|
||||
const nk = nr * COLS + nc, np = p + cost(nk);
|
||||
if (!nx.has(nk) || nx.get(nk) > np) nx.set(nk, np);
|
||||
const nk = nr * COLS + nc;
|
||||
let m = nx.get(nk);
|
||||
for (const [w, p] of byWet) {
|
||||
const nw = w + wetOf(nk), np = p + cost(nk);
|
||||
if (nw > maxWet) continue;
|
||||
if (!m) nx.set(nk, m = new Map());
|
||||
if (!m.has(nw) || m.get(nw) > np) m.set(nw, np);
|
||||
}
|
||||
}
|
||||
}
|
||||
cur = nx;
|
||||
@@ -97,6 +107,14 @@ function fewestNb(g, s, e, slack = 2) {
|
||||
return { best, min };
|
||||
}
|
||||
|
||||
/** 深水区 tiles crossed from s to e (start and goal excluded). */
|
||||
function wetCrossed(g, s, e) {
|
||||
const wp = g.waypoints(s[0], s[1], e[0], e[1]);
|
||||
const seen = new Set();
|
||||
for (const [r, c] of crossed(wp)) if (!(r === s[0] && c === s[1]) && !(r === e[0] && c === e[1]) && g.deepWater[r * COLS + c]) seen.add(`${r},${c}`);
|
||||
return seen.size;
|
||||
}
|
||||
|
||||
/** Tiles an enemy of `route` visits (rounded position per tick) and whether `u` blocked it. */
|
||||
function walk(h) {
|
||||
const seen = [];
|
||||
@@ -293,12 +311,15 @@ test('audit: every WALK leg of every wave template (normal / 联防 / boss / hid
|
||||
if (!grids.has(key)) grids.set(key, stageGrid(sid, rect));
|
||||
const g = grids.get(key);
|
||||
if (!g.walkable(from[0], from[1]) || !g.walkable(to[0], to[1])) continue;
|
||||
const eq = fewestNb(g, from, to, 0);
|
||||
if (!eq) continue;
|
||||
if (!fewestNb(g, from, to, 0)) continue;
|
||||
const got = nbCrossed(g, from, to);
|
||||
// compared with the routes that wade through no more 深水区 than ours (#375: 战场#08(下半)'s patrol legs to (2,3) /
|
||||
// (2,17) keep the official upper road, not the lower one through two water tiles that crosses one floor tile less)
|
||||
const wet = wetCrossed(g, from, to);
|
||||
const eq = fewestNb(g, from, to, 0, wet);
|
||||
const what = `${sid} ${id} (${from}) → (${to}) crosses ${got.join(' ')}`;
|
||||
assert.ok(got.length <= eq.min, `${what}, an equal-length route only ${eq.min}`);
|
||||
const near = fewestNb(g, from, to, 2);
|
||||
const near = fewestNb(g, from, to, 2, wet);
|
||||
if (near.min < got.length) {
|
||||
assert.ok(rect === GEO.BOSS_RECT && got.every(arena), `${what}: a route of length ≤ ${near.best}+2 crosses only ${near.min}`);
|
||||
accepted.push(what);
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
// preference field = 0.1.0's — equal-length ties to the chain with the fewest non-blockable tiles, a line of sight that
|
||||
// covers non-blockable tiles, diagonal-step corners included, only on the own raw chain — whose pointer replaces the
|
||||
// official one where its route crosses fewer non-blockable tiles, or as few while only skipping the official waypoint
|
||||
// (on the straight line to it, leading there); "crosses" = positive-length intersection with the tile, found here by
|
||||
// clipping the segment against each tile, independently of grid.js crossTiles):
|
||||
// (on the straight line to it, leading there), and no more 深水区 than the official route (GitHub #375); "crosses" =
|
||||
// positive-length intersection with the tile, found here by clipping the segment against each tile, independently of
|
||||
// grid.js crossTiles):
|
||||
// * the 8 active stages with their match-start devices: the smoothed chain from EVERY walkable tile of the normal,
|
||||
// 联防 and boss rects to every goal equals the full-map reference one (the sim's rect limit changes nothing);
|
||||
// * 400 random crate / block layouts: identical chains to the same algorithm limited to the rect;
|
||||
@@ -56,6 +57,8 @@ function official(sid, { crates: extraCrates = [], blocks: extraBlocks = [], rec
|
||||
if (t.tileKey === 'tile_deepsea') return true;
|
||||
return !(t.heightType === 'LOWLAND' && (t.buildableType === 'ALL' || t.buildableType === 'MELEE'));
|
||||
};
|
||||
/** 深水区 — the preference may not cross more of it than the official route (#375) */
|
||||
const deep = (r, c) => ['tile_deepsea', 'tile_deepwater'].includes(tile(r, c).tileKey);
|
||||
const bres = ([r0, c0], [r1, c1], clear) => {
|
||||
const dr = Math.abs(r1 - r0), dc = Math.abs(c1 - c0), sr = r1 > r0 ? 1 : -1, sc = c1 > c0 ? 1 : -1;
|
||||
let err = dc - dr, r = r0, c = c0;
|
||||
@@ -140,12 +143,15 @@ function official(sid, { crates: extraCrates = [], blocks: extraBlocks = [], rec
|
||||
const off = spfa(false);
|
||||
const dist = off.dist;
|
||||
const segNb = (a, b) => through(a, b).filter(([y, x]) => !(y === a[0] && x === a[1]) && nb(y, x)).length;
|
||||
const segWet = (a, b) => through(a, b).filter(([y, x]) => !(y === a[0] && x === a[1]) && deep(y, x)).length;
|
||||
let nxt = smooth(off, false);
|
||||
if (prefer) {
|
||||
// per tile in increasing distance: the preference pointer when its route crosses fewer non-blockable tiles (or as
|
||||
// few while it only skips the official waypoint: o strictly inside the segment a → p, and o's chosen pointer is p)
|
||||
// and no more 深水区 than the official pointer's route
|
||||
const nxtP = smooth(spfa(true), true);
|
||||
const cost = new Map([[D, 0]]);
|
||||
const wet = new Map([[D, 0]]);
|
||||
const chosen = new Map();
|
||||
const skips = (a, o, p) => {
|
||||
const t = through(a, p);
|
||||
@@ -155,9 +161,11 @@ function official(sid, { crates: extraCrates = [], blocks: extraBlocks = [], rec
|
||||
if (k === D) continue;
|
||||
const a = unkey(k), o = nxt.get(k), p = nxtP.get(k);
|
||||
const co = segNb(a, o) + cost.get(key(o)), cp = segNb(a, p) + cost.get(key(p));
|
||||
const useP = cp < co || (cp === co && key(o) !== key(p) && key(chosen.get(key(o)) ?? []) === key(p) && skips(a, o, p));
|
||||
const wo = segWet(a, o) + wet.get(key(o)), wp = segWet(a, p) + wet.get(key(p));
|
||||
const useP = wp <= wo && (cp < co || (cp === co && key(o) !== key(p) && key(chosen.get(key(o)) ?? []) === key(p) && skips(a, o, p)));
|
||||
chosen.set(k, useP ? p : o);
|
||||
cost.set(k, useP ? cp : co);
|
||||
wet.set(k, useP ? wp : wo);
|
||||
}
|
||||
nxt = chosen;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Official route shapes (community report after 0.1.0, D5: on 战场#04 活性源石 the lower-gate enemies walked two tiles
|
||||
// straight left of the red gate and then straight up, instead of crossing diagonally from the 4th row to the 3rd as in
|
||||
// the official game). grid.js keeps the official flow field (research 08 §3.1) unless the blockable-ground preference
|
||||
// route of user playtest #2 (0.1.0's) crosses strictly fewer non-blockable tiles (floor, gates) — a segment that only
|
||||
// route of user playtest #2 (0.1.0's) crosses strictly fewer non-blockable tiles (floor, gates) and no more 深水区
|
||||
// (GitHub #375) — a segment that only
|
||||
// brushes a floor tile's corner does not cross it. Audit: every stage (active or not) × gate × field against the pure
|
||||
// official algorithm; the routes that still differ are listed, each avoiding floor the official route walks over.
|
||||
import { test } from 'node:test';
|
||||
@@ -127,6 +128,43 @@ test('audit: every route of every stage (11, active or not) × gate × field is
|
||||
assert.equal(official, stages.length * legs.length - seen.size);
|
||||
});
|
||||
|
||||
// GitHub #375 (FrogThai: 「水图boss会从下面这条路往蓝门走,实际应该从上路走」): on 战场#08(下半) 涨潮控制 the patrolling leaders
|
||||
// (铳 — level_act1autochess_h07_02 route 6 —, 卢西恩 — act2autochess_h07_05 route 8: PATROL_MOVE (3,9) → (2,3) → (5,9))
|
||||
// took the lower road (3,9) → (2,3) through the two 深水区 tiles (3,6) / (4,6) — one floor tile fewer than the official
|
||||
// upper road along row 5, which crosses one water tile. The tie-break may not wade into more 深水区 than the official
|
||||
// route (grid.js), so these legs are official again.
|
||||
test('#375 战场#08(下半): the (3,9) → (2,3) patrol leg stays on the official upper road', REAL, () => {
|
||||
const sid = 'act2autochess_m04';
|
||||
const g = stageGrid(sid, GEO.BOSS_RECT), pure = stageGrid(sid, GEO.BOSS_RECT, true);
|
||||
for (const [s, e, wp] of [
|
||||
[[3, 9], [2, 3], '3,9 5,9 5,4 2,4 2,3'],
|
||||
[[3, 11], [2, 17], '3,11 5,11 5,16 2,16 2,17'],
|
||||
[[3, 10], [2, 3], '3,10 5,9 5,4 2,4 2,3'],
|
||||
[[4, 10], [3, 9], '4,10 3,9'],
|
||||
]) {
|
||||
assert.deepEqual(g.waypoints(s[0], s[1], e[0], e[1]), pure.waypoints(s[0], s[1], e[0], e[1]), `(${s}) → (${e})`);
|
||||
assert.equal(str(g.waypoints(s[0], s[1], e[0], e[1])), wp);
|
||||
}
|
||||
});
|
||||
|
||||
test('#375 real battle (act2 m04, 铳 on act1autochess_h07_02): the patrol walks row 5 to (2,3) and steps on one 深水区 only', REAL, () => {
|
||||
const h = makeBattle({ kind: 'boss', stageId: 'act2autochess_m04', waveTemplate: 'act1autochess_h07_02', units: [], autoFinish: false, timeLimit: 120,
|
||||
sharedBoss: { hp: 1e12, maxHp: 1e12, damage(pid, n) { this.hp -= n; } } });
|
||||
const rows = getDefaultSource().getStage('act2autochess_m04').raw.rows;
|
||||
const tiles = [];
|
||||
h.b.on('tick', () => {
|
||||
const e = h.b.enemies.find((u) => u.alive && u.defId === 'enemy_9017_achunt');
|
||||
if (!e) return;
|
||||
const k = `${Math.round(e.y)},${Math.round(e.x)}`;
|
||||
if (tiles[tiles.length - 1] !== k) tiles.push(k);
|
||||
});
|
||||
assert.ok(h.runUntil(() => tiles.includes('2,4'), 60), `reaches (2,4): ${tiles.join(' ')}`);
|
||||
const upTo = tiles.slice(0, tiles.indexOf('2,4') + 1);
|
||||
assert.deepEqual(upTo.slice(0, 4), ['4,10', '3,9', '4,9', '5,9'], `up to row 5 first: ${upTo.join(' ')}`);
|
||||
const water = upTo.filter((k) => { const [r, c] = k.split(',').map(Number); return rows[r][c] === 'd'; });
|
||||
assert.deepEqual(water, ['5,6'], `深水区 stepped on: ${water.join(' ')} (route ${upTo.join(' ')})`);
|
||||
});
|
||||
|
||||
test('D5: an operator on (9,9), (9,8) or (10,8) blocks the lower-gate enemy on the official diagonal', REAL, () => {
|
||||
const guard = chessRec({ id: 't_guard', profession: 'WARRIOR', stats: { atk: 0, blockCnt: 3, maxHp: 1e6 }, skill: null });
|
||||
const walker = enemyRec({ key: 'enemy_walker', hp: 1e6, speed: 1 });
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// The 传送门 (portal) of the boss / hidden-core templates (GitHub #336, PR #337 by @2321Robin): an enemy flies or walks
|
||||
// into the portal and reappears on the far side. Four official routes spell the crossing as DISAPPEAR → WAIT → MOVE-to-the-exit with no APPEAR step
|
||||
// (act1autochess_h07_02 route #4, act1autochess_h08_02 routes #5/#9, act2autochess_h07_05 route #6), while their twin
|
||||
// routes in the same templates write it out as DISAPPEAR → WAIT → APPEAR[exit]. Taken literally, an appear-less route
|
||||
// made the enemy walk its whole remaining route invisible and then leak unseen — the blue door takes the leak and the
|
||||
// team never sees who or how (user report: 打 boss 时小怪进传送门后被判定进蓝门,全程隐身). normalizeRoute re-inserts
|
||||
// the exit APPEAR ahead of that MOVE — at the MOVE's own target, which is exactly where the twins appear.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { normalizeRoute, hasGeneratedData } from '../../server/sim/simdata.js';
|
||||
import { makeBattle, enemyRec } from '../helpers/battleHarness.js';
|
||||
|
||||
const REAL = { skip: !hasGeneratedData() && 'no generated data' };
|
||||
|
||||
/** Leg signature of a normalized route: `MOVE[r,c]` / `APPEAR[r,c]` / `DISAPPEAR` / `WAIT`. */
|
||||
const sig = (r) => r.checkpoints.map((c) => c.type + (c.type === 'MOVE' || c.type === 'APPEAR' ? `[${c.pos[0]},${c.pos[1]}]` : ''));
|
||||
/** The test enemy's leaks (the templates' own spawns leak too). */
|
||||
const leaksOf = (h) => h.hooksOf('enemyLeak').filter((l) => l.enemy.defId === 'enemy_test_portal');
|
||||
/** The test enemy's hidden spans: [{ from: [r,c], to: [r,c], dt }] (vanish tile, reappear tile, seconds hidden). */
|
||||
function hiddenSpans(h) {
|
||||
const spans = [];
|
||||
let open = null;
|
||||
const set = h.b._setHidden.bind(h.b);
|
||||
h.b._setHidden = (e, on) => {
|
||||
if (e.defId === 'enemy_test_portal' && on !== !!e.hidden) {
|
||||
const at = [Math.round(e.y), Math.round(e.x)];
|
||||
if (on) open = { at, t: h.b.time };
|
||||
else if (open) { spans.push({ from: open.at, to: at, dt: h.b.time - open.t }); open = null; }
|
||||
}
|
||||
return set(e, on);
|
||||
};
|
||||
return spans;
|
||||
}
|
||||
|
||||
test('normalizeRoute: DISAPPEAR → WAIT → MOVE gains the twin APPEAR at the move target', () => {
|
||||
const r = normalizeRoute({
|
||||
motion: 'FLY', start: [2, 10], end: [1, 3],
|
||||
steps: [
|
||||
{ t: 'move', p: [2, 11] }, { t: 'move', p: [1, 17] }, { t: 'disappear' }, { t: 'wait', s: 3 },
|
||||
{ t: 'move', p: [5, 10] }, { t: 'move', p: [5, 9] },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(sig(r), ['MOVE[2,11]', 'MOVE[1,17]', 'DISAPPEAR', 'WAIT', 'APPEAR[5,10]', 'MOVE[5,10]', 'MOVE[5,9]']);
|
||||
});
|
||||
|
||||
test('normalizeRoute: a DISAPPEAR the data does spell out with an APPEAR is kept as the only one', () => {
|
||||
const r = normalizeRoute({
|
||||
motion: 'FLY', start: [2, 10], end: [1, 17],
|
||||
steps: [
|
||||
{ t: 'move', p: [1, 3] }, { t: 'disappear' }, { t: 'wait', s: 3 }, { t: 'appear', p: [5, 10] }, { t: 'move', p: [5, 13] },
|
||||
],
|
||||
});
|
||||
assert.equal(r.checkpoints.filter((c) => c.type === 'APPEAR').length, 1);
|
||||
assert.deepEqual(sig(r), ['MOVE[1,3]', 'DISAPPEAR', 'WAIT', 'APPEAR[5,10]', 'MOVE[5,13]']);
|
||||
});
|
||||
|
||||
test('normalizeRoute: idempotent — a second pass over a fixed route changes nothing', () => {
|
||||
const once = normalizeRoute({
|
||||
motion: 'FLY', start: [2, 10], end: [1, 3],
|
||||
steps: [{ t: 'move', p: [1, 17] }, { t: 'disappear' }, { t: 'wait', s: 3 }, { t: 'move', p: [5, 10] }],
|
||||
});
|
||||
assert.deepEqual(normalizeRoute(once), once);
|
||||
});
|
||||
|
||||
// The four official appear-less routes on their real templates: after the portal the enemy must be back in the open
|
||||
// at the exit [5,10] (where the twins appear) and the route's leak at the blue door must happen VISIBLE.
|
||||
const PORTAL_EXIT = [5, 10];
|
||||
const CASES = [
|
||||
['act1autochess_h07_02', 4, 'FLY'],
|
||||
['act1autochess_h08_02', 5, 'WALK'],
|
||||
['act1autochess_h08_02', 9, 'WALK'],
|
||||
['act2autochess_h07_05', 6, 'FLY'],
|
||||
];
|
||||
|
||||
for (const [tplId, routeIdx, motion] of CASES) {
|
||||
test(`${tplId} route #${routeIdx}: portal crossing reappears at [${PORTAL_EXIT}] and leaks visible`, REAL, () => {
|
||||
const h = makeBattle({
|
||||
kind: 'boss', waveTemplate: tplId, content: 'none',
|
||||
defs: { enemies: { enemy_test_portal: enemyRec({ key: 'enemy_test_portal', hp: 1e6, speed: 3, motion }) } },
|
||||
enemies: [{ key: 'enemy_test_portal', route: routeIdx }],
|
||||
autoFinish: true, timeLimit: 400, hooks: ['enemyLeak'],
|
||||
});
|
||||
const e = () => h.enemy('enemy_test_portal');
|
||||
assert.ok(h.runUntil(() => e() && e().hidden, 400), 'reaches the portal (disappears)');
|
||||
assert.ok(h.runUntil(() => e() && !e().hidden, 400), 'reappears after the portal');
|
||||
assert.deepEqual([Math.round(e().y), Math.round(e().x)], PORTAL_EXIT, 'reappears at the portal exit');
|
||||
h.runToEnd(400);
|
||||
const [leak, ...more] = leaksOf(h);
|
||||
assert.ok(leak, 'the route still ends in its leak');
|
||||
assert.equal(more.length, 0, 'one leak');
|
||||
assert.equal(leak.enemy.hidden, false, 'the leak is visible');
|
||||
});
|
||||
}
|
||||
|
||||
// A route that ENDS on a portal entrance does not leak there: the entrance teleports the enemy out of the far exit
|
||||
// ([5,10] — the same pairing every explicit DISAPPEAR / APPEAR pair in the data reads) and it walks on to the blue
|
||||
// door on that side of the field, where the leak happens (GitHub #336: 打 boss 小怪第一次进传送门就被判定进蓝门,
|
||||
// 官方是传送后走向蓝门). The solo boss routes are exactly this shape: straight from the centre into the corner portal.
|
||||
// Inside the portal it stays hidden for 3 s, as the explicit crossings of the same tiles (WAIT 3 in 85 of the 95).
|
||||
test('solo boss: a route ending on the left portal entrance leaks at the left blue door, not the portal', REAL, () => {
|
||||
const h = makeBattle({
|
||||
kind: 'boss', stageId: 'act1autochess_m01', rect: { r0: 0, r1: 5, c0: 0, c1: 20 },
|
||||
waveTemplate: 'act1autochess_h07_02_s', content: 'none',
|
||||
defs: { enemies: { enemy_test_portal: enemyRec({ key: 'enemy_test_portal', hp: 1e6, speed: 3 }) } },
|
||||
enemies: [{ key: 'enemy_test_portal', route: 0 }],
|
||||
autoFinish: true, timeLimit: 300, hooks: ['enemyLeak'],
|
||||
});
|
||||
const spans = hiddenSpans(h);
|
||||
const e = () => h.enemy('enemy_test_portal');
|
||||
assert.ok(h.runUntil(() => e() && Math.round(e().y) === 1 && Math.round(e().x) === 3, 300), 'reaches the portal entrance [1,3]');
|
||||
assert.ok(h.runUntil(() => e() && Math.round(e().y) === 5 && Math.round(e().x) === 10, 300), 'teleports out of the exit [5,10]');
|
||||
h.runToEnd(400);
|
||||
const [leak, ...more] = leaksOf(h);
|
||||
assert.ok(leak, 'leaks eventually');
|
||||
assert.deepEqual([Math.round(leak.enemy.y), Math.round(leak.enemy.x)], [2, 2], 'the leak is at the left blue door [2,2]');
|
||||
assert.equal(more.length, 0, 'stepping into the portal entrance is no leak of its own');
|
||||
assert.equal(spans.length, 1, 'one portal crossing');
|
||||
assert.deepEqual([spans[0].from, spans[0].to], [[1, 3], [5, 10]], 'hidden from the entrance to the exit');
|
||||
assert.ok(Math.abs(spans[0].dt - 3) < h.TICK * 1.5, `hidden 3 s inside the portal (${spans[0].dt.toFixed(3)} s)`);
|
||||
});
|
||||
|
||||
test('a route ending on the right portal entrance mirrors: leak at the right blue door', REAL, () => {
|
||||
const h = makeBattle({
|
||||
kind: 'boss', stageId: 'act1autochess_m01', rect: { r0: 0, r1: 5, c0: 0, c1: 20 }, content: 'none',
|
||||
defs: { enemies: { enemy_test_portal: enemyRec({ key: 'enemy_test_portal', hp: 1e6, speed: 3 }) } },
|
||||
enemies: [{ key: 'enemy_test_portal', route: { motion: 'WALK', start: [2, 10], end: [1, 17], checkpoints: [] } }],
|
||||
autoFinish: true, timeLimit: 300, hooks: ['enemyLeak'],
|
||||
});
|
||||
const spans = hiddenSpans(h);
|
||||
h.runToEnd(400);
|
||||
const [leak, ...more] = leaksOf(h);
|
||||
assert.ok(leak, 'leaks eventually');
|
||||
assert.deepEqual([Math.round(leak.enemy.y), Math.round(leak.enemy.x)], [2, 18], 'the leak is at the right blue door [2,18]');
|
||||
assert.equal(more.length, 0, 'stepping into the portal entrance is no leak of its own');
|
||||
assert.deepEqual(spans.map((x) => [x.from, x.to]), [[[1, 17], [5, 10]]], 'one crossing, entrance to exit');
|
||||
assert.ok(Math.abs(spans[0].dt - 3) < h.TICK * 1.5, `hidden 3 s inside the portal (${spans[0].dt.toFixed(3)} s)`);
|
||||
});
|
||||
Reference in new issue
Block a user