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
+45
-6
@@ -28,10 +28,11 @@
|
||||
// (PRTS 状态机: an enemy's COMBAT state ends with its attack, not with its blocker); the candidates pass the
|
||||
// enemy's own rule (`e.profile.canTarget`) and are ordered
|
||||
// blocker → taunt → latest deployed (targeting.js sortAllyTargets). An enemy's damage type is its data's unless content
|
||||
// arms it (`e.profile.dmgType`: 转译基底's forms, whose data never attacks). Reaching the final leg's end = leak. A `fear` (恐惧) status suspends the route: the
|
||||
// enemy runs between random checkpoints away from the fear's source (fear.js moveFeared; a self-inflicted fear
|
||||
// flutters inside its own tile); an `attract` (诱导) status walks it to the status point instead (moveAttracted);
|
||||
// both re-plan the route when released (恐惧 outranks 诱导).
|
||||
// arms it (`e.profile.dmgType`: 转译基底's forms, whose data never attacks). Reaching the final leg's end = leak — on a
|
||||
// portal entrance it is a teleport to the far exit and a walk to that side's blue door instead (portalPickup). A `fear`
|
||||
// (恐惧) status suspends the route: the enemy runs between random checkpoints away from the fear's source (fear.js
|
||||
// moveFeared; a self-inflicted fear flutters inside its own tile); an `attract` (诱导) status walks it to the status
|
||||
// point instead (moveAttracted); both re-plan the route when released (恐惧 outranks 诱导).
|
||||
|
||||
import { ATTACK_PAUSE, ALLY_COLLIDER_RADIUS, MOVE_SCALE, PROJECTILE_SPEEDS, PROJECTILE_SPEED, BOOMERANG_RETURN_SPEED, COLS, CHAIN_RADIUS } from './constants.js';
|
||||
import { sortEnemyTargets, sortAllyTargets, canTargetEnemy, canTargetAlly, tileKeyOf } from './targeting.js';
|
||||
@@ -634,7 +635,10 @@ function advanceRoute(b, e, dt, R, standing = false) {
|
||||
let guard = 16;
|
||||
while (budget > 1e-9 && guard-- > 0 && e.alive) {
|
||||
const leg = R.legs[R.legIdx];
|
||||
if (!leg) { b.leak(e); return; }
|
||||
if (!leg) {
|
||||
if (!portalPickup(b, e)) { b.leak(e); return; }
|
||||
continue;
|
||||
}
|
||||
if (leg.t === 'wait') {
|
||||
if (R.waitLeft == null) R.waitLeft = leg.time;
|
||||
const use = Math.min(budget, R.waitLeft);
|
||||
@@ -684,13 +688,48 @@ function advanceRoute(b, e, dt, R, standing = false) {
|
||||
}
|
||||
budget = dist / speed;
|
||||
if (R.pts && R.ptIdx >= R.pts.length) {
|
||||
if (leg.final) { b.leak(e); return; }
|
||||
if (leg.final) {
|
||||
if (!portalPickup(b, e)) { b.leak(e); return; }
|
||||
}
|
||||
R.legIdx++;
|
||||
R.pts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boss-field portal pickup (tile_telin / tile_telout; GitHub #336, PR #337 by @2321Robin, both with a recording of the
|
||||
* official game): a route that ENDS on a portal entrance does not leak there — the enemy is teleported out of the far
|
||||
* exit and walks on to the blue door on the entrance's side, where it leaks. The boss / Hidden Core circuits end on
|
||||
* an entrance ([1,3] / [1,17]; 137 routes of data/waves.json); their mid-route crossings are the routes' own DISAPPEAR /
|
||||
* WAIT / APPEAR steps, every MOVE onto an entrance being followed by a DISAPPEAR, so only the route's end needs this.
|
||||
* Appends vanish → wait → reappear → walk-to-door legs to the live route and returns true (false: not on an entrance,
|
||||
* or no exit in the field). The exit is the telout farthest from the entrance, the door the end tile nearest to it:
|
||||
* every explicit pair of the data (95, routes and extra routes) reads [1,3] / [1,17] → [5,10] — the farther of the two
|
||||
* telouts [5,10] / [2,10] of the battle stages — and the doors [2,2] / [2,18] stand beside the entrances. Inside the
|
||||
* portal the enemy stays hidden PORTAL_WAIT s, the wait of the explicit crossings of the same tiles (WAIT 3 s in 85 of
|
||||
* the 95; 5 s in 9, 1 s in 1). [ASSUMED] the pairing (no tile carries a link) and the 3 s (no source times this one).
|
||||
*/
|
||||
const PORTAL_WAIT = 3;
|
||||
function portalPickup(b, e) {
|
||||
const r = Math.round(e.y), c = Math.round(e.x);
|
||||
if (!b.grid.inBounds(r, c) || b.grid.tile(r, c).special !== 'telin') return false;
|
||||
const outs = b.grid.specialTiles('telout');
|
||||
if (!outs.length) return false;
|
||||
const dist = (p) => hypot(p[0] - r, p[1] - c);
|
||||
const out = outs.reduce((a, x) => (dist(x) > dist(a) ? x : a));
|
||||
const R = e.route;
|
||||
R.legs.splice(R.legIdx + 1, 0, { t: 'disappear' }, { t: 'wait', time: PORTAL_WAIT }, { t: 'appear', r: out[0], c: out[1] });
|
||||
const ends = b.grid.specialTiles('end');
|
||||
if (ends.length) {
|
||||
const door = ends.reduce((a, x) => (dist(x) < dist(a) ? x : a));
|
||||
R.legs.push({ t: 'move', r: door[0], c: door[1], final: true });
|
||||
}
|
||||
R.pts = null;
|
||||
R.tailVersion = -1; // the appended tail invalidates the cached remaining-distance suffixes
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* How long an enemy stands for one attack: an unblocked ranged enemy for its whole clip (GitHub #58 — the owner's
|
||||
* decision of 2026-10-04 from first-hand memory of the official game: a ranged enemy stops for each attack's animation
|
||||
|
||||
@@ -640,8 +640,11 @@ function kitGun(ab, e, b) {
|
||||
});
|
||||
if (s2) list.push({
|
||||
cd: s2.cd, icd: s2.icd, cond: (b2) => b2.enemies.some((o) => o.alive && isSpring(o)),
|
||||
fire(b2, e2) { // 【末日布道】 springs dash to 铳, invulnerable, trampling operators
|
||||
// PRTS “碎铳之簧” 追逐模式 "不进行普通攻击": `disarm` for the dash (until 0.1.3 it kept shooting while it ran)
|
||||
fire(b2, e2) { // 【末日布道】 springs chase 铳 for the whole 5 s gain, invulnerable, trampling operators (kitSpring)
|
||||
// PRTS “碎铳之簧” 追逐模式 "不进行普通攻击": `disarm` for the dash (until 0.1.3 it kept shooting while it ran).
|
||||
// Each gun casts on its own data timer, and a cast takes every spring — on a pair field the later of the two
|
||||
// calls retargets them all to its gun (handbook 「持续召唤场上所有“碎铳之簧”向自身移动」; no stagger, no lock while
|
||||
// a chase runs: PR #347's pair coordination is not taken, the data has neither)
|
||||
for (const sp of b2.enemies) {
|
||||
if (!sp.alive || !isSpring(sp) || !sp.mem.ab) continue;
|
||||
sp.mem.ab.dash = { until: b2.time + (s2.bb.dog_duration ?? 0), mul: 1 + (s2.bb.move_speed ?? 0), gun: e2, hit: new Set() };
|
||||
@@ -709,11 +712,16 @@ function kitSpring(ab, e) {
|
||||
if (!P.up && P.downAt != null && b.time - P.downAt >= regen) raise(b);
|
||||
const d = e2.mem.ab.dash;
|
||||
if (!d) return;
|
||||
// 追逐模式 lasts the whole gain (PRTS 末日布道 "令全场的“碎铳之簧”获得5秒增益…增益期间切换为追逐模式"; “碎铳之簧”
|
||||
// 追逐模式 "持续追踪令其进入该形态的场上的假想敌:铳移动" — no arrival clause, unlike the 铳's own 冲锋模式): reaching
|
||||
// the gun does not end it, the spring keeps following the gun until the 5 s are over (PR #347 by @CXUtk; until
|
||||
// 0.2.1 it ended on arrival or within 1 tile). The casting gun dead: the nearest other gun; none left on the
|
||||
// field: it holds still, the gain running on.
|
||||
const g = d.gun && d.gun.alive ? d.gun : gun(b);
|
||||
const arrived = !g || stepToward(e2, g.x, g.y, e2.s.moveSpeed * d.mul * MOVE_SCALE * dt) || hypot(g.x - e2.x, g.y - e2.y) < 1;
|
||||
if (g) stepToward(e2, g.x, g.y, e2.s.moveSpeed * d.mul * MOVE_SCALE * dt);
|
||||
// 追逐模式 "对进入自身0.35半径范围内的我方单位(包括飞行单位)造成一次攻击力100%的物理普通伤害" (until 0.1.3: radius 0.5)
|
||||
for (const u of areaAllies(b, e2, e2.x, e2.y, CHARGE_RADIUS)) if (!d.hit.has(u)) { d.hit.add(u); hurt(b, e2, u, e2.s.atk, 'phys'); }
|
||||
if (arrived || b.time >= d.until) { e2.mem.ab.dash = null; b.removeBuff(e2, 'boss:dash'); if (e2.route) e2.route.pts = null; }
|
||||
if (b.time >= d.until) { e2.mem.ab.dash = null; b.removeBuff(e2, 'boss:dash'); if (e2.route) e2.route.pts = null; }
|
||||
},
|
||||
},
|
||||
// while shielded, every (spCost+1)-th attack (enemy SP +1 per attack, as 粉碎攻坚手's official text) adds the shield's
|
||||
|
||||
@@ -400,7 +400,10 @@ function applyReader(battle, it) {
|
||||
else battle.removeBuff(u, key);
|
||||
break;
|
||||
case 'act1autochess_gar_eff_respawnTimeByBond':
|
||||
if (k > 0) S.passiveBuff(battle, u, key, { redeployMul: Math.max(0.05, 1 + S.num(it.bb.respawn_time, 0) * k) });
|
||||
// 耀骑士临光's 144: −1.5 % / −3 % per 3 卡西米尔 layers with no minimum in the blackboard (divide_num, respawn_time
|
||||
// only) — clamped at 0 like every other redeploy multiplier, so a knock-out timer can reach 0 s (GitHub #370;
|
||||
// until 0.2.1 a 0.05 floor kept it at 5 % of the base time from 192 layers, 96 for the elite)
|
||||
if (k > 0) S.passiveBuff(battle, u, key, { redeployMul: Math.max(0, 1 + S.num(it.bb.respawn_time, 0) * k) });
|
||||
else battle.removeBuff(u, key);
|
||||
break;
|
||||
case 'act2autochess_gar_eff_attrByBond_add_onstart': {
|
||||
|
||||
+22
-11
@@ -26,8 +26,7 @@
|
||||
// 0.1.0: on 战场#04 活性源石 the lower-gate enemies must cross diagonally from row 9 into row 10 as officially, not
|
||||
// walk two tiles left of the gate and then straight up). A tile is non-blockable when it is walkable but not LOW
|
||||
// ground buildable for melee (floor / gate / goal / teleport / 深水区 tiles — `blockable()` false; no gate route of 战场#08
|
||||
// changed when its 深水区 became non-buildable, a few off-route smoothed steps of its boss field, rows 4–5, go around the
|
||||
// water instead of cutting across it). Each field holds two candidate
|
||||
// changed when its 深水区 became non-buildable). Each field holds two candidate
|
||||
// pointers per tile, both with the official `dist` (so the crate cost 1000 and every grid route length stay official):
|
||||
// * official: the plain algorithm above;
|
||||
// * preference (0.1.0's): the SPFA relaxes on (dist, pen) lexicographically, `pen` = non-blockable tiles on the chain
|
||||
@@ -36,7 +35,10 @@
|
||||
// only on the tile's own raw chain between its two ends, so it never slips past a road tile beside floor its grid
|
||||
// route does not walk.
|
||||
// `next[tile]` is the official pointer unless the preference pointer leads to a route CROSSING strictly fewer
|
||||
// non-blockable tiles (`cost`, counted to the destination in increasing `dist` order). A segment crosses the tiles
|
||||
// non-blockable tiles (`cost`, counted to the destination in increasing `dist` order) and no more 深水区 (`deep`): 深水区
|
||||
// is non-blockable itself, so without that test the preference waded into the pool to dodge floor (GitHub #375: on
|
||||
// 战场#08(下半) the patrolling 铳 / 卢西恩 took the lower road (3,9) → (2,3) through two water tiles, one floor tile fewer
|
||||
// than the official upper road with one). A segment crosses the tiles
|
||||
// whose interior it passes through (`crossTiles`, exact geometry); one it only touches at a corner point is not crossed
|
||||
// — the first version counted the corner tiles there too and so bent official diagonals into L shapes (D5). On equal
|
||||
// counts the official pointer stays, except where the preference one merely skips it (the official waypoint lies on
|
||||
@@ -166,9 +168,12 @@ export class Grid {
|
||||
}
|
||||
/** 1 = walkable terrain an operator cannot block on (floor, gate, goal, teleport…): the path tie-break penalty */
|
||||
this.unblockable = new Uint8Array(ROWS * COLS);
|
||||
/** 1 = walkable 深水区 (terrain 'deepsea': tile_deepsea / tile_deepwater): the tie-break never walks into more of it */
|
||||
this.deepWater = new Uint8Array(ROWS * COLS);
|
||||
for (let k = 0; k < ROWS * COLS; k++) {
|
||||
const t = this.tiles[k];
|
||||
this.unblockable[k] = t.pass === 'ALL' && !isBlockableTile(t) ? 1 : 0;
|
||||
this.deepWater[k] = t.pass === 'ALL' && t.terrain === 'deepsea' ? 1 : 0;
|
||||
}
|
||||
/** obstacle bits per tile (OB_BLOCK | OB_CRATE) */
|
||||
this.obstacle = new Uint8Array(ROWS * COLS);
|
||||
@@ -272,10 +277,10 @@ export class Grid {
|
||||
|
||||
_buildField(er, ec, allowDiagonal, ignore) {
|
||||
const N = ROWS * COLS;
|
||||
const f = { dest: -1, dist: null, parent: null, pen: null, next: null, official: null, cost: null, len: null, version: this.version, allowDiagonal, ignore };
|
||||
const f = { dest: -1, dist: null, parent: null, pen: null, next: null, official: null, cost: null, deep: null, len: null, version: this.version, allowDiagonal, ignore };
|
||||
if (!this.inBounds(er, ec)) {
|
||||
const none = new Int32Array(N).fill(-1);
|
||||
return Object.assign(f, { dist: none, parent: none, next: none, official: none, pen: new Int32Array(N), cost: new Int32Array(N) });
|
||||
return Object.assign(f, { dist: none, parent: none, next: none, official: none, pen: new Int32Array(N), cost: new Int32Array(N), deep: new Int32Array(N) });
|
||||
}
|
||||
const dest = er * COLS + ec;
|
||||
f.dest = dest;
|
||||
@@ -300,25 +305,30 @@ export class Grid {
|
||||
(n) => { for (let x = n, g = N; x >= 0 && g-- > 0; x = pref.parent[x]) onChain[x] = n; });
|
||||
// per tile, in increasing distance: the official pointer unless the preference one crosses strictly fewer
|
||||
// non-blockable tiles on the way to the destination — or as few while only skipping the official waypoint o, which
|
||||
// then lies on the straight line to it and leads there (the same walk, one waypoint fewer)
|
||||
// then lies on the straight line to it and leads there (the same walk, one waypoint fewer) — and no more 深水区
|
||||
// than the official one (GitHub #375)
|
||||
const order = [];
|
||||
for (let k = 0; k < N; k++) if (dist[k] >= 0) order.push(k);
|
||||
order.sort((a, b) => dist[a] - dist[b] || a - b);
|
||||
const through = (a, b) => { let n = -unb[a]; crossTiles(a, b, (r, c) => { n += unb[r * COLS + c]; }); return n; };
|
||||
const deepW = this.deepWater;
|
||||
const wet = (a, b) => { let n = -deepW[a]; crossTiles(a, b, (r, c) => { n += deepW[r * COLS + c]; }); return n; };
|
||||
const next = new Int32Array(N).fill(-1);
|
||||
const cost = new Int32Array(N);
|
||||
const deep = new Int32Array(N);
|
||||
for (const k of order) {
|
||||
const o = nextO[k], p = nextP[k];
|
||||
if (o < 0) continue; // the destination
|
||||
let use = o, best = through(k, o) + cost[o];
|
||||
let use = o, best = through(k, o) + cost[o], bestWet = wet(k, o) + deep[o];
|
||||
if (p >= 0 && p !== o) {
|
||||
const cp = through(k, p) + cost[p];
|
||||
if (cp < best || (cp === best && next[o] === p && onSegment(k, o, p))) { use = p; best = cp; }
|
||||
const cp = through(k, p) + cost[p], wp = wet(k, p) + deep[p];
|
||||
if (wp <= bestWet && (cp < best || (cp === best && next[o] === p && onSegment(k, o, p)))) { use = p; best = cp; bestWet = wp; }
|
||||
}
|
||||
next[k] = use;
|
||||
cost[k] = best;
|
||||
deep[k] = bestWet;
|
||||
}
|
||||
return Object.assign(f, { dist, parent: pref.parent, pen: pref.pen, next, official: nextO, cost });
|
||||
return Object.assign(f, { dist, parent: pref.parent, pen: pref.pen, next, official: nextO, cost, deep });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -434,8 +444,9 @@ export class Grid {
|
||||
* @property {Int32Array} pen non-blockable tiles on that raw chain (tile included, destination excluded)
|
||||
* @property {Int32Array} official smoothed parent of the pure official algorithm
|
||||
* @property {Int32Array} next the tile to walk straight toward (official, or the preference pointer when it crosses fewer
|
||||
* non-blockable tiles — see the module header)
|
||||
* non-blockable tiles and no more 深水区 — see the module header)
|
||||
* @property {Int32Array} cost non-blockable tiles the route of `next` crosses from the tile (excluded) to the destination
|
||||
* @property {Int32Array} deep 深水区 tiles the route of `next` crosses from the tile (excluded) to the destination
|
||||
* @property {Float64Array|null} len memoised smoothed length (fieldLength)
|
||||
*/
|
||||
|
||||
|
||||
@@ -394,6 +394,7 @@ export function normalizeRoute(r) {
|
||||
const t = String(st.t ?? st.type ?? 'move').toUpperCase();
|
||||
cps.push({ type: normCpType(t), pos: pairOf(st.p ?? st.pos) ?? [0, 0], time: num(st.s ?? st.time, 0) });
|
||||
}
|
||||
portalAppear(cps);
|
||||
return { motion, start, end, checkpoints: cps };
|
||||
}
|
||||
for (const cp of r.checkpoints ?? r.cp ?? []) {
|
||||
@@ -406,9 +407,29 @@ export function normalizeRoute(r) {
|
||||
cps.push({ type: normCpType(cp.type), pos: pairOf(cp.pos ?? cp.position ?? [cp.row, cp.col]) ?? [0, 0], time: num(cp.time ?? cp.t, 0) });
|
||||
}
|
||||
}
|
||||
portalAppear(cps);
|
||||
return { motion, start, end, checkpoints: cps };
|
||||
}
|
||||
|
||||
/**
|
||||
* The boss / Hidden Core templates' 传送门 (GitHub #336, PR #337 by @2321Robin): the twin routes spell a crossing as
|
||||
* DISAPPEAR → WAIT → APPEAR[exit], but four official routes leave the APPEAR out and go straight to MOVE-to-the-exit
|
||||
* (act1autochess_h07_02 #4, act1autochess_h08_02 #5 / #9, act2autochess_h07_05 #6; the level files themselves have no
|
||||
* APPEAR_AT_POS there). Taken literally, such a route walks the enemy's whole remaining run hidden (untargetable,
|
||||
* unblockable) and it leaks unseen — the crossing must read "reappear on the far side". Re-insert the exit APPEAR
|
||||
* ahead of that MOVE, at the MOVE's own target, where the twins appear ([5,10]; the leftover MOVE becomes a
|
||||
* zero-length hop, and the hop no longer counts as walking distance for the targeting order). A DISAPPEAR followed by
|
||||
* an APPEAR, or by no MOVE, stays as it is; idempotent.
|
||||
*/
|
||||
function portalAppear(cps) {
|
||||
for (let i = 0; i < cps.length; i++) {
|
||||
if (cps[i].type !== 'DISAPPEAR') continue;
|
||||
let j = i + 1;
|
||||
while (j < cps.length && cps[j].type === 'WAIT') j++;
|
||||
if (cps[j] && cps[j].type === 'MOVE') cps.splice(j, 0, { type: 'APPEAR', pos: cps[j].pos.slice(), time: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
function normCpType(t) {
|
||||
const s = String(t ?? 'MOVE').toUpperCase();
|
||||
if (s.includes('DISAPPEAR')) return 'DISAPPEAR';
|
||||
|
||||
Reference in new issue
Block a user