Playtest #5 fixes: hovering enemies, target order, blocking hand-over, 联防 knocked-out, elements, huge-boss hit area, maps, gear icon, phone HUD, 至简 price
- #1 近地悬浮 enemies count as air units (Unit.isFlying): ground-only attacks, splash, terrain and traps skip them; PRTS 不可对空 skill effects made ground-only; 掠海漂移体 / 吉兆飞鳞 drop modes; the bot plans them as air - #2 an operator knocked out before 联防 is fielded and force-exited (PRTS 强制退场): it lies on its tile with the redeploy ring and redeploys (full timer [ASSUMED]); partners, observers and the server fallback included - #3 element audit of every operator, item and enemy: 海嗣 numbers are official; bursts are 无来源, 元素脆弱 only raises 元素伤害, 元素抗性 applied, 盟约·辅助干员 fills all three gauges, 菲莱 / 哈洛德 / 纯烬 / enemy kits fixed - #4 blocking by the official contact radius (0.7071): another operator in contact takes over at once when the blocker dies, withdraws or is stunned; melee blockers attack what they block - #5 enemy target order per PRTS 索敌: blocker, special priority, taunt, last deployed; initial deploy 从上到下>从左到右, operators before summons; special priorities break ties by taunt then deploy order - #6 act2 m01's normal-field blower machines (row 13) are drawn in the normal / 联防 views - #7 Final Assault prep legality reads the player's half of the boss field (fence tiles deployable); all 11 stages audited against the official level files - #8 regular settings gear; #9 the prep camera keeps the bench and temp rows clear of the shop bar on phones - #10 huge bosses have the official 4.95 × 2.95 hit area (sim/body.js) and are 自缚 / unblockable - #11 至简 costs 1: 购买价格为N's bb.price is a discount off the tier price, applied before other price effects - DESIGN §19 and the normative §3 / §5 / §6 / §7 lines, SIM / META / DATA / PLAYING updated; 2595 unit tests and every browser suite green; dev recordings regenerated
This commit is contained in:
1 parent
d4f868cd19
commit
15291c2293
104 files changed
+5539
-662
No files matched your search
@@ -0,0 +1,110 @@
|
||||
// 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';
|
||||
|
||||
/**
|
||||
* 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 Math.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 Math.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;
|
||||
}
|
||||
Reference in new issue
Block a user