- server/sim/detmath.js: hypot (sqrt of the squares, rescaled outside 2^±500), powi (integer powers in double-double, one rounding), sin / cos (fdlibm 5.3) and atan2 (fdlibm / FreeBSD msun) built only from correctly rounded IEEE operations; the sim's Math.hypot / sin / cos / atan2 / pow calls go through it (65 files); eslint.config.js forbids the approximated Math functions under server/sim and test/sim/detmath.test.js scans for them. - sin / cos / atan2 are the algorithms V8 runs in Node, so the server's results stay; only hypot differs from V8's. Golden: 2 moved — diy-027 (a hypot distance, damage +80) and coop3-ABYSS-5 (a later card pick from round 9, same end). - Applied unchanged after review (Grok static review: MERGE; the diff read in full: no network, no new dependency). - Tests: broad node --test 5155 / 5155 with the new file tracked (detmath 7 / 7, package 10 / 10); lint 0 errors, typecheck, check:imports.
112 lines
5.9 KiB
JavaScript
112 lines
5.9 KiB
JavaScript
// server/sim/body.js — where a unit can be hit: the ONE helper behind every range test that targets enemies (user
|
||
// playtest #5 item 10: "大体型的关底boss实际受击判定范围很小…原版是很大的一片区域").
|
||
//
|
||
// Regular units are a point: in a grid range when the tile containing their position (round(y), round(x)) is a range
|
||
// tile, in a radius when their position is (DESIGN §3). Huge units (巨型单位 — enemy data `hitArea`, tools/build-data.mjs
|
||
// HIT_AREAS, from PRTS "巨型单位:受击判定区域为长4.95、宽2.95的长方形,向上偏移1.0") are hit anywhere on a rectangle
|
||
// `w` tiles along the columns × `h` along the rows, centred on their position moved `dx` columns right and `dy` rows
|
||
// up:
|
||
// * grid ranges / tile rules: the unit occupies every tile its rectangle overlaps (PRTS 作战机制 "巨型BOSS单位的每一个
|
||
// 占据的格子都可以让其本身通过格子判定"; an operator's range — 1×1 tile squares — touches the rectangle exactly then):
|
||
// a 4.95 × 2.95 area centred on a tile covers that 5 × 3 block;
|
||
// * radius rules (auras, AoE around a point or a unit): the distance from the point to the rectangle (0 inside it)
|
||
// [ASSUMED: PRTS 作战机制 calls collider tests "the most common"];
|
||
// * splash around a struck / marked target is a 中点判定 (PRTS 作战机制: "中点判定…案例:阻挡,酒神1天赋的1.3溅射半径"):
|
||
// every enemy, a huge one too, counts by its position (Battle.enemiesInRadius(x, y, r, true) — the engine's
|
||
// profession splash and aftershocks, and the kits' splash centred on an enemy).
|
||
// The unit's own position stays its 判定中心: projectiles fly to it, its own attacks and radii measure from it, and it
|
||
// is the centre of splash on it (PRTS 分支特性信息: 投掷物会抵达目标的判定中心…巨型单位的模型中心具有一定偏移). Body size
|
||
// plays no part in blocking or movement — every huge unit here is 自缚 + 无法被阻挡 (PRTS 天赋; content/bosses.js
|
||
// SELF_BOUND: persistent noMove + unblockable from spawn). Also by position: movement, pathing and terrain on the
|
||
// enemy's tile, tile-entry tracking (雪境 tiles), placement heuristics and distance-scaled numbers, and the range keys
|
||
// taken from blocked enemies (a huge unit is never blocked).
|
||
// Pure functions of (x, y, hitArea); no battle state — also used for the client's picking (render/pick.js mirrors
|
||
// `hitRect`).
|
||
|
||
import { ROWS, COLS } from './constants.js';
|
||
import { hypot } from './detmath.js';
|
||
|
||
/**
|
||
* Validated hit area of a data record (`{ w, h, dx, dy }`, tiles) or null for a regular (point) unit.
|
||
* @param {any} a
|
||
*/
|
||
export function normHitArea(a) {
|
||
if (!a || typeof a !== 'object') return null;
|
||
const w = Number(a.w), h = Number(a.h), dx = Number(a.dx ?? 0), dy = Number(a.dy ?? 0);
|
||
if (!(w > 0) || !(h > 0) || !Number.isFinite(w) || !Number.isFinite(h) || !Number.isFinite(dx) || !Number.isFinite(dy)) return null;
|
||
return Object.freeze({ w, h, dx, dy });
|
||
}
|
||
|
||
/** The world rectangle `{ x0, x1, y0, y1 }` of a huge unit, or null for a point unit (no `hitArea`). */
|
||
export function hitRect(u) {
|
||
const a = u && u.hitArea;
|
||
if (!a) return null;
|
||
const cx = u.x + a.dx, cy = u.y + a.dy;
|
||
return { x0: cx - a.w / 2, x1: cx + a.w / 2, y0: cy - a.h / 2, y1: cy + a.h / 2 };
|
||
}
|
||
|
||
/** Tile key of a unit's position (-1 off the grid). */
|
||
function posKey(u) {
|
||
const r = Math.round(u.y), c = Math.round(u.x);
|
||
return r < 0 || r >= ROWS || c < 0 || c >= COLS ? -1 : r * COLS + c;
|
||
}
|
||
|
||
/**
|
||
* Tile keys (row × COLS + col) the unit occupies: a point unit the tile of its position ([] off the grid), a huge unit
|
||
* every tile its rectangle overlaps (open intervals: a tile only touched along an edge is not overlapped).
|
||
*/
|
||
export function bodyKeys(u) {
|
||
const R = hitRect(u);
|
||
if (!R) { const k = posKey(u); return k < 0 ? [] : [k]; }
|
||
const out = [];
|
||
const r0 = Math.max(0, Math.floor(R.y0 + 0.5 + 1e-9)), r1 = Math.min(ROWS - 1, Math.ceil(R.y1 - 0.5 - 1e-9));
|
||
const c0 = Math.max(0, Math.floor(R.x0 + 0.5 + 1e-9)), c1 = Math.min(COLS - 1, Math.ceil(R.x1 - 0.5 - 1e-9));
|
||
for (let r = r0; r <= r1; r++) for (let c = c0; c <= c1; c++) out.push(r * COLS + c);
|
||
return out;
|
||
}
|
||
|
||
/** Does the unit occupy tile (r, c)? */
|
||
export function bodyOnTile(u, r, c) {
|
||
const R = hitRect(u);
|
||
if (!R) return Math.round(u.y) === r && Math.round(u.x) === c;
|
||
return c + 0.5 > R.x0 + 1e-9 && c - 0.5 < R.x1 - 1e-9 && r + 0.5 > R.y0 + 1e-9 && r - 0.5 < R.y1 - 1e-9;
|
||
}
|
||
|
||
/** Does the unit occupy any tile of `keys` (tile keys: a Set / Map — anything with `has` — or an array)? */
|
||
export function bodyInKeys(u, keys) {
|
||
if (!u || !keys) return false;
|
||
const has = typeof keys.has === 'function' ? (k) => keys.has(k) : (k) => keys.includes(k);
|
||
if (!u.hitArea) { const k = posKey(u); return k >= 0 && has(k); }
|
||
for (const k of bodyKeys(u)) if (has(k)) return true;
|
||
return false;
|
||
}
|
||
|
||
/** Distance (tiles) from point (x, y) to the unit's body: its position, or its rectangle (0 inside). */
|
||
export function bodyDist(u, x, y) {
|
||
const R = hitRect(u);
|
||
if (!R) return hypot(u.x - x, u.y - y);
|
||
const dx = x < R.x0 ? R.x0 - x : x > R.x1 ? x - R.x1 : 0;
|
||
const dy = y < R.y0 ? R.y0 - y : y > R.y1 ? y - R.y1 : 0;
|
||
return hypot(dx, dy);
|
||
}
|
||
|
||
/** Is the unit's body within `r` tiles of point (x, y)? (the same ≤ r + 1e-9 test as Battle.enemiesInRadius) */
|
||
export function bodyInRadius(u, x, y, r) {
|
||
return bodyDist(u, x, y) <= r + 1e-9;
|
||
}
|
||
|
||
/**
|
||
* Chebyshev reach in tiles from tile (r, c) to the unit's body (0 = it occupies that tile; 1 = one of the 8 tiles
|
||
* around it; …): "周围八格"-style square areas centred on a tile.
|
||
*/
|
||
export function bodyTileReach(u, r, c) {
|
||
if (!u.hitArea) return Math.max(Math.abs(Math.round(u.y) - r), Math.abs(Math.round(u.x) - c));
|
||
let best = Infinity;
|
||
for (const k of bodyKeys(u)) {
|
||
const kr = Math.floor(k / COLS), kc = k % COLS;
|
||
const d = Math.max(Math.abs(kr - r), Math.abs(kc - c));
|
||
if (d < best) best = d;
|
||
}
|
||
return best;
|
||
}
|