Playtest #4 fixes: tile picking, knocked-down state, element gauges, draft timer, live stats, shop items, drones, 赫默, 炎佑, UI flash
- #1 picking by the tile under the pointer (prep, battle, pen, equipment drops); the dragged model stays under the pointer (mouse and touch); pixel probe removed; phone ghost-click on the underframe fixed - #2 two-tap confirm on 机变 cards (like the shop) - #3 a 同盟 room with one human is untimed like 独立模拟 (no countdown outside battles) - #4 strategy draft: one synced countdown, 30 s per human turn, timeout takes the highlighted band - #5 special 维式重锤 (4) and 突变细胞 no longer sold; item card shows where they come from - #6 operator skill-mode sounds no longer play on normal attacks (纯烬艾雅法拉 and 55 others) - #7 detail card shows live battle stats and the prep-time battle-start stats (g.unitStats) - #8 official element gauges on units; burst cooldown (阻回) per PRTS - #9 knocked-down operators stay on their tile with a redeploy countdown ring - #10 drone behaviour per PRTS (暴鸰, 先兆者 shells, 黑云, 萨科塔之眼) - #11 赫默's drone only from her skill; placeable summons limited to talent tokens - #12 炎佑 stays where it is with no enemies; 祛恶之焰 as a continuous channel - #13 the occasional giant bond icon (loading texture scaled ×46) fixed - docs (DESIGN §18), recordings, data and tests updated (2467 unit tests, all browser suites green)
This commit is contained in:
1 parent
0aba602ecf
commit
d4f868cd19
107 files changed
+4510
-1653
No files matched your search
+78
-207
@@ -1,224 +1,95 @@
|
||||
// render/pick.js — which unit the pointer is on (user playtest #3 item 7: "clicking a unit on the map often selects
|
||||
// the one a row below it; dropping equipment too"). Pure screen-space geometry (no PIXI, no DOM), the ONE rule behind
|
||||
// every picking path: prep press / drag start / right-click detail and equipment dropped on an operator
|
||||
// (render/app.js pieceAt / pieceUnder, and ui/gameLogic.js pickPieceAt over view.pieceScreenRect shapes), battle
|
||||
// clicks and hover (battleUnitAt) and the enemy preview pen (penUnitAt).
|
||||
//
|
||||
// Why "the front-most rectangle under the pointer" failed: a chibi is an upright billboard ~1.27 tiles tall (median
|
||||
// of the 138 official operators' Front / Back skeletons in their idle pose, p10–p90 1.17–1.41) standing on its tile,
|
||||
// while the rows of the official 30° cameras are only ~0.84 (prep) – 0.91 (battle) of a tile apart on screen. The unit
|
||||
// in front covers the legs / feet of the unit behind it and its head sits over that unit's tile; its 0.7-tile-wide
|
||||
// rectangle also covered the ground and the legs showing beside its head (a head is only ~0.5 tile wide), and a
|
||||
// pressed tile was only looked at when no rectangle at all was hit — so a press on the lower part of a unit, on its
|
||||
// feet or on its tile went to the unit in front of it: the one a row lower on screen.
|
||||
//
|
||||
// Body of a unit (all px on one screen; `x`, `y` = its feet, `s` = px per tile at the feet, `h` = model height in
|
||||
// tiles, `w` = width factor (1 for operators; enemies scale with their size), `flip` = +1 facing right / −1 left). The
|
||||
// measured opaque profile of the idle chibis is ~±0.3 tile wide through the body (the back side a little wider: hair,
|
||||
// capes) and ~±0.2 at the crown; a taller model is taller hair / ears / hats — its face stays ~0.86 tile up:
|
||||
// outline head ellipse (centre h − 0.33·w above the feet, radii 0.27·w × 0.33·w) ∪ body box (0.08 below the feet …
|
||||
// the shoulders at min(0.62·h, 0.78·w); −0.34·w … +0.28·w facing right) ∪ the unit's HUD (tier chip /
|
||||
// bars) — what is drawn
|
||||
// core face (ellipse 0.21·w × 0.15·w centred min(0.86·w, h − 0.36·w) above the feet) ∪ torso / legs (±0.2·w, from
|
||||
// 0.1 above the feet to the shoulders) — what a player aims at
|
||||
// 'diamond' (the avatar diamond shown until the Spine model is there): the rhombus of side `d` tiles above the feet
|
||||
// 'plate' (hand items): a square of half-size `r` tiles centred on (x, y)
|
||||
// Rule (pickBody):
|
||||
// 1. the pointer is on units' cores → the front-most of them (drawn on top: the face of the unit in front is its own;
|
||||
// the torso of the unit behind is its own above that face)
|
||||
// 2. else a unit stands on the tile under the pointer → that unit (its feet, its tile — also where the hair or a
|
||||
// weapon of the unit in front hangs over it)
|
||||
// 3. else the pointer is on outlines only (hair, hands, feet, weapon edges) → the unit whose core is nearest (px / its
|
||||
// own scale); ties → the front-most
|
||||
// The feet of a unit hidden behind the face of the unit in front are not pickable (nothing of them shows there); its
|
||||
// tile beside that face and everything of it above that face are.
|
||||
//
|
||||
// Pixel probe (optional `probe(body)` → true / false / null, render/app.js drawnAt): the shapes above are a model of
|
||||
// the drawn chibi, and where the bodies of two units overlap the model cannot tell whose arm, hair or weapon is drawn
|
||||
// on top. Measured on the mock match against the rendered pixels (1920×1080, every opaque unit pixel sampled, the
|
||||
// front-most drawn unit = the truth): the rule alone put 7 % of the prep presses (18 % in battle) on the wrong unit —
|
||||
// mostly the unit BEHIND: the hair crown and the sides (arms, clothes, weapons) of the unit in front lie over the legs /
|
||||
// tile of the one behind — against 2 % / 11 % for the old front-most rectangle; with the probe 0.7 % / 1.7 % (the rest:
|
||||
// anti-aliased edges). So wherever the probe can change the answer (two or more bodies within their outline +
|
||||
// PROBE_MARGIN of the pointer, or one the rule did not pick), the front-most body whose model really draws the pixel
|
||||
// under the pointer wins — what the player sees. Only when none of them draws there (a gap, the bare tile) or the probe
|
||||
// cannot tell (null) does the rule above decide.
|
||||
// render/pick.js — which unit the pointer is on (user playtest #4 item 1: "地上都画好了一个一个方格,点击对应方格就选中那个
|
||||
// 方格的人物就行" — the ground is drawn as tiles: a press on a tile selects the unit on that tile). Pure logic (no PIXI, no
|
||||
// DOM): render/app.js hands in the units and the ground under the pointer (render/projection.js pickTile, raised tops
|
||||
// first, in the tiles the views are drawn on). The one rule behind every picking path — prep press / click / right-click
|
||||
// and long-press detail / drag start / hover (app.js pieceAt), battle clicks and hover (battleUnitAt), the enemy preview
|
||||
// pen (penUnitAt); a dragged piece (an item too) drops on the tile under the pointer (render/drag.js):
|
||||
// * prep pieces (board tiles, bench row 7, temp row 8), battle allies and the figures of the enemy pen stand on
|
||||
// tiles: the unit on the tile under the pointer. An empty tile selects nothing — a press on a unit's head where it is
|
||||
// drawn over the tile behind it is a press on that tile. Several on one tile (the pen's clusters of up to 3, a piece
|
||||
// held for a moment on an occupied tile) → the one nearest to the pointer's point on the tile, ties → the front-most.
|
||||
// * battle enemies walk between tiles: the one within ENEMY_REACH tile of the pointer — its ground position from the
|
||||
// pointer's point on the ground, or its drawn body (on screen: the upright line from its feet to its head, in its px
|
||||
// per tile), whichever is nearer, so a press on a tall enemy's / a boss's body picks it too; a flying enemy (drawn in
|
||||
// the air, over whatever tile it crosses) by its drawn body alone.
|
||||
// * an ally's tile and an enemy both qualify → the nearer one (distance in tiles to the pointer's point), ties → the
|
||||
// ally, then the front-most.
|
||||
// (Replaces the drawn-body shapes and the 1-px render probe of v2.2, DESIGN §17.2: the player aims at the grid.)
|
||||
|
||||
/** Median model height of the official chibis (tiles above the feet), when the model's own is not known. */
|
||||
export const BODY_H = 1.27;
|
||||
/** Shape constants in tiles (see the header). */
|
||||
export const BODY = Object.freeze({
|
||||
headRx: 0.27, headRy: 0.33, faceRx: 0.21, faceRy: 0.15, face: 0.86, faceTop: 0.36, back: 0.34, front: 0.28,
|
||||
below: 0.08, shoulder: 0.62, neck: 0.78, torso: 0.2, torsoFrom: 0.1, lean: 0.03,
|
||||
});
|
||||
/** Battle enemies are picked within this many tiles of the pointer's ground point or of their drawn body on screen. */
|
||||
export const ENEMY_REACH = 0.6;
|
||||
|
||||
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
||||
const inRect = (r, px, py) => !!r && px >= r.x0 && px <= r.x1 && py >= r.y0 && py <= r.y1;
|
||||
const inEllipse = (e, px, py) => ((px - e.cx) / e.rx) ** 2 + ((py - e.cy) / e.ry) ** 2 <= 1;
|
||||
const rectDist = (r, px, py) => Math.hypot(Math.max(r.x0 - px, 0, px - r.x1), Math.max(r.y0 - py, 0, py - r.y1));
|
||||
function ellipseDist(e, px, py) {
|
||||
const k = Math.hypot((px - e.cx) / e.rx, (py - e.cy) / e.ry);
|
||||
return k > 1 ? (k - 1) * Math.min(e.rx, e.ry) : 0;
|
||||
const EPS = 1e-9;
|
||||
const isTile = (t) => !!t && Number.isInteger(t.row) && Number.isInteger(t.col);
|
||||
const onTile = (u, t) => isTile(u.tile) && isTile(t) && u.tile.row === t.row && u.tile.col === t.col;
|
||||
const depthOf = (u) => (u && Number.isFinite(u.depth) ? u.depth : 0);
|
||||
/** Tiles between a unit's ground position and the pointer's point on the ground (0 when either is unknown). */
|
||||
function groundDist(u, g) {
|
||||
if (!g || !Number.isFinite(g.x) || !Number.isFinite(g.y) || !Number.isFinite(u.x) || !Number.isFinite(u.y)) return 0;
|
||||
return Math.hypot(u.x - g.x, u.y - g.y);
|
||||
}
|
||||
/** Rhombus |dx|/a + |dy|/a ≤ k around (cx, cy). */
|
||||
const inRhombus = (d, px, py, k = 1) => Math.abs(px - d.cx) / d.a + Math.abs(py - d.cy) / d.a <= k;
|
||||
|
||||
/**
|
||||
* @typedef {{ x: number, y: number, s: number, h?: number, w?: number, flip?: number, kind?: 'chibi'|'diamond'|'plate',
|
||||
* d?: number, r?: number, depth?: number, hud?: { x0: number, y0: number, x1: number, y1: number } | null,
|
||||
* tile?: { row: number, col: number } | null }} PickBody
|
||||
* `depth`: draw order (larger = nearer the camera, drawn on top); `tile`: the tile it stands on (null: moving units)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Screen geometry of a body: `{ outline: [parts], core: [parts] }`, parts `{ rect }` / `{ ellipse }` / `{ rhombus }`.
|
||||
* @param {PickBody} b
|
||||
*/
|
||||
export function bodyParts(b) {
|
||||
const s = b.s, x = b.x, y = b.y;
|
||||
const hud = b.hud && [b.hud.x0, b.hud.y0, b.hud.x1, b.hud.y1].every(Number.isFinite) ? { rect: b.hud } : null;
|
||||
if (b.kind === 'plate') {
|
||||
const a = (Number(b.r) > 0 ? b.r : 0.31) * s;
|
||||
return { outline: [{ rect: { x0: x - a, x1: x + a, y0: y - a, y1: y + a } }], core: [{ rect: { x0: x - a * 0.7, x1: x + a * 0.7, y0: y - a * 0.7, y1: y + a * 0.7 } }] };
|
||||
}
|
||||
if (b.kind === 'diamond') {
|
||||
const half = ((Number(b.d) > 0 ? b.d : 0.78) / 2) * s;
|
||||
const rh = { cx: x, cy: y - BODY.below * s - half, a: half };
|
||||
return { outline: [{ rhombus: rh, k: 1 }, ...(hud ? [hud] : [])], core: [{ rhombus: rh, k: 0.7 }] };
|
||||
}
|
||||
const w = Number(b.w) > 0 ? b.w : 1;
|
||||
const h = clamp(Number(b.h) > 0 ? b.h : BODY_H, 0.3, 4);
|
||||
const flip = b.flip === -1 ? -1 : 1;
|
||||
const cx = x - BODY.lean * flip * w * s;
|
||||
const head = { cx, cy: y - (h - BODY.headRy * w) * s, rx: BODY.headRx * w * s, ry: BODY.headRy * w * s };
|
||||
const face = { cx, cy: y - Math.min(BODY.face * w, h - BODY.faceTop * w) * s, rx: BODY.faceRx * w * s, ry: BODY.faceRy * w * s };
|
||||
const top = y - Math.min(BODY.shoulder * h, BODY.neck * w) * s;
|
||||
const box = { x0: x - (flip > 0 ? BODY.back : BODY.front) * w * s, x1: x + (flip > 0 ? BODY.front : BODY.back) * w * s, y0: top, y1: y + BODY.below * s };
|
||||
const torso = { x0: cx - BODY.torso * w * s, x1: cx + BODY.torso * w * s, y0: top, y1: y - BODY.torsoFrom * Math.min(1, h) * s };
|
||||
// the HUD (tier chip / bars) is drawn above every unit, but over the unit behind: an outline, never a core
|
||||
return { outline: [{ ellipse: head }, { rect: box }, ...(hud ? [hud] : [])], core: [{ ellipse: face }, { rect: torso }] };
|
||||
}
|
||||
|
||||
const partHit = (p, px, py) => (p.rect ? inRect(p.rect, px, py) : p.ellipse ? inEllipse(p.ellipse, px, py) : inRhombus(p.rhombus, px, py, p.k));
|
||||
function partDist(p, px, py) {
|
||||
if (p.rect) return rectDist(p.rect, px, py);
|
||||
if (p.ellipse) return ellipseDist(p.ellipse, px, py);
|
||||
const r = p.rhombus, k = (Math.abs(px - r.cx) + Math.abs(py - r.cy)) / r.a;
|
||||
return k > p.k ? (k - p.k) * r.a * Math.SQRT1_2 : 0;
|
||||
}
|
||||
|
||||
/** 2 = on the body's core, 1 = on its outline only, 0 = off it. */
|
||||
export function hitBody(b, px, py) {
|
||||
if (!b || !(b.s > 0) || !Number.isFinite(b.x) || !Number.isFinite(b.y)) return 0;
|
||||
// cheap reject (battle hover tests every unit on every pointer move): far outside the body and its HUD
|
||||
if (b.kind !== 'plate' && !(b.hud && inRect(b.hud, px, py))) {
|
||||
const w = Number(b.w) > 0 ? b.w : 1, reach = Math.max(BODY.back * w, (Number(b.d) || 0) / 2) * b.s + 1;
|
||||
const top = b.y - (b.kind === 'diamond' ? (Number(b.d) || 0.78) + BODY.below : clamp(Number(b.h) > 0 ? b.h : BODY_H, 0.3, 4)) * b.s - 1;
|
||||
if (Math.abs(px - b.x) > reach || py < top || py > b.y + BODY.below * b.s + 1) return 0;
|
||||
}
|
||||
const parts = bodyParts(b);
|
||||
if (parts.core.some((p) => partHit(p, px, py))) return 2;
|
||||
return parts.outline.some((p) => partHit(p, px, py)) ? 1 : 0;
|
||||
}
|
||||
|
||||
/** Distance (px) from a point to the body's core, divided by the body's scale (0 on the core). */
|
||||
export function coreDistance(b, px, py) {
|
||||
const parts = bodyParts(b);
|
||||
let d = Infinity;
|
||||
for (const p of parts.core) d = Math.min(d, partDist(p, px, py));
|
||||
return d / b.s;
|
||||
}
|
||||
|
||||
/** The screen bounding box of a body's outline (px): `{ x, y, width, height }`. */
|
||||
export function bodyBounds(b) {
|
||||
const parts = bodyParts(b);
|
||||
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
||||
for (const p of [...parts.outline, ...parts.core]) {
|
||||
if (p.rect) { x0 = Math.min(x0, p.rect.x0); x1 = Math.max(x1, p.rect.x1); y0 = Math.min(y0, p.rect.y0); y1 = Math.max(y1, p.rect.y1); }
|
||||
else if (p.ellipse) { const e = p.ellipse; x0 = Math.min(x0, e.cx - e.rx); x1 = Math.max(x1, e.cx + e.rx); y0 = Math.min(y0, e.cy - e.ry); y1 = Math.max(y1, e.cy + e.ry); }
|
||||
else { const r = p.rhombus, a = r.a * p.k; x0 = Math.min(x0, r.cx - a); x1 = Math.max(x1, r.cx + a); y0 = Math.min(y0, r.cy - a); y1 = Math.max(y1, r.cy + a); }
|
||||
}
|
||||
if (!Number.isFinite(x0)) return { x: b.x, y: b.y, width: 0, height: 0 };
|
||||
return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
||||
}
|
||||
|
||||
const depthOf = (b) => (Number.isFinite(b.depth) ? b.depth : 0);
|
||||
|
||||
/** Margin (tiles) around a body's outline in which the pixel probe still looks (weapons, capes, hair, big enemies past
|
||||
* the modelled outline; 0.25 left 4 % of the battle presses wrong, 0.5 1.7 %). */
|
||||
export const PROBE_MARGIN = 0.5;
|
||||
/** At most this many overlapping bodies are probed (front-most first). */
|
||||
const PROBE_MAX = 6;
|
||||
|
||||
/** Bodies whose outline (+ PROBE_MARGIN) contains the point, front-most first. */
|
||||
export function probeCandidates(bodies, px, py) {
|
||||
const out = [];
|
||||
for (const b of bodies) {
|
||||
if (!b || !(b.s > 0) || !Number.isFinite(b.x) || !Number.isFinite(b.y)) continue;
|
||||
const r = bodyBounds({ ...b, hud: null }), m = PROBE_MARGIN * b.s;
|
||||
if (px >= r.x - m && px <= r.x + r.width + m && py >= r.y - m && py <= r.y + r.height + m) out.push(b);
|
||||
}
|
||||
return out.sort((a, b) => depthOf(b) - depthOf(a));
|
||||
/** Tiles (its px per tile) between screen point (px, py) and an enemy's drawn body — the line from its feet up to its head. */
|
||||
function bodyDist(b, px, py) {
|
||||
if (!b || !(b.s > 0) || ![b.x, b.top, b.feet, px, py].every(Number.isFinite)) return Infinity;
|
||||
const y = Math.max(Math.min(b.top, b.feet), Math.min(Math.max(b.top, b.feet), py));
|
||||
return Math.hypot(px - b.x, py - y) / b.s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pixel probe step of pickBody: the front-most body around the point whose model draws the pixel under it. Asked only
|
||||
* when it can change the answer: two or more bodies there, or one that the shape rule did not pick (`rule`: e.g. its
|
||||
* arm over the tile of a unit standing behind it). null when none of them draws there or the probe cannot tell for one
|
||||
* in front of the first that does (then the shape rule decides).
|
||||
* @typedef {{ row: number, col: number, x?: number, y?: number }} PickGround the tile under the pointer and the pointer's
|
||||
* point on its top (world tiles: x = col, y = row), or null off the grid
|
||||
* @typedef {{ tile?: { row: number, col: number } | null, x?: number, y?: number, depth?: number, fly?: boolean,
|
||||
* body?: { x: number, top: number, feet: number, s: number } | null }} PickUnit
|
||||
* `tile`: the tile it stands on (null: it walks — a battle enemy); `x`, `y`: its ground position (world tiles);
|
||||
* `body`: an enemy's drawn body on screen (px: x, head `top`, `feet`; `s` px per tile there); `fly`: in the air — its
|
||||
* ground position does not count; `depth`: draw order (larger = in front)
|
||||
*/
|
||||
function probeBodies(bodies, px, py, probe, rule) {
|
||||
const cand = probeCandidates(bodies, px, py);
|
||||
if (!cand.length || (cand.length === 1 && cand[0] === rule)) return null;
|
||||
for (const b of cand.slice(0, PROBE_MAX)) {
|
||||
let d = null;
|
||||
try { d = probe(b); } catch { d = null; }
|
||||
if (d === true) return b;
|
||||
if (d !== false) return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const onTile = (b, t) => !!t && !!b.tile && b.tile.row === t.row && b.tile.col === t.col;
|
||||
function nearest(list, px, py) {
|
||||
|
||||
/**
|
||||
* The unit standing on the tile under the pointer (several → the nearest to the pointer's point, ties → the front-most).
|
||||
* @param {Array<PickUnit|null>} units
|
||||
* @param {PickGround|null} ground
|
||||
* @returns {PickUnit|null} one of `units`
|
||||
*/
|
||||
export function pickOnTile(units, ground) {
|
||||
if (!Array.isArray(units) || !isTile(ground)) return null;
|
||||
let best = null, bd = Infinity;
|
||||
for (const b of list) {
|
||||
const d = coreDistance(b, px, py);
|
||||
if (d < bd - 1e-9 || (Math.abs(d - bd) <= 1e-9 && best && depthOf(b) > depthOf(best))) { best = b; bd = d; }
|
||||
for (const u of units) {
|
||||
if (!u || !onTile(u, ground)) continue;
|
||||
const d = groundDist(u, ground);
|
||||
if (d < bd - EPS || (Math.abs(d - bd) <= EPS && depthOf(u) > depthOf(best))) { best = u; bd = d; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* The body the pointer is on (see the header for the rule), or null.
|
||||
* @param {Array<PickBody|null>} bodies
|
||||
* @param {number} px
|
||||
* Battle pick (see the header): allies on the tile under the pointer, enemies within ENEMY_REACH of it; the nearer wins.
|
||||
* @param {Array<PickUnit|null>} units allies with `tile`, enemies with `tile: null` and their drawn `body`
|
||||
* @param {PickGround|null} ground
|
||||
* @param {number} px pointer on screen (enemies' drawn bodies)
|
||||
* @param {number} py
|
||||
* @param {{ row: number, col: number } | null} [ground] the tile under the pointer (same space as the bodies' `tile`)
|
||||
* @param {((body: PickBody) => (boolean|null)) | null} [probe] does this body's model draw the pixel under the pointer
|
||||
* (null: cannot tell)? See the header.
|
||||
* @returns {PickBody|null} one of `bodies`
|
||||
* @returns {PickUnit|null} one of `units`
|
||||
*/
|
||||
export function pickBody(bodies, px, py, ground = null, probe = null) {
|
||||
if (!Number.isFinite(px) || !Number.isFinite(py) || !Array.isArray(bodies)) return null;
|
||||
const rule = ruleBody(bodies, px, py, ground);
|
||||
if (typeof probe === 'function') {
|
||||
const drawn = probeBodies(bodies, px, py, probe, rule);
|
||||
if (drawn) return drawn;
|
||||
export function pickBattle(units, ground, px, py) {
|
||||
if (!Array.isArray(units)) return null;
|
||||
const onGround = !!ground && Number.isFinite(ground.x) && Number.isFinite(ground.y);
|
||||
let best = null, bd = Infinity;
|
||||
const better = (u, d) => {
|
||||
if (d < bd - EPS) return true;
|
||||
if (Math.abs(d - bd) > EPS) return false;
|
||||
if (!!u.tile !== !!best.tile) return !!u.tile; // a tie: the ally standing there
|
||||
return depthOf(u) > depthOf(best);
|
||||
};
|
||||
for (const u of units) {
|
||||
if (!u) continue;
|
||||
let d = Infinity;
|
||||
if (u.tile) { if (onTile(u, ground)) d = groundDist(u, ground); }
|
||||
else {
|
||||
d = bodyDist(u.body, px, py);
|
||||
if (!u.fly && onGround && Number.isFinite(u.x) && Number.isFinite(u.y)) d = Math.min(d, Math.hypot(u.x - ground.x, u.y - ground.y));
|
||||
if (!(d <= ENEMY_REACH)) continue;
|
||||
}
|
||||
if (d < Infinity && better(u, d)) { best = u; bd = d; }
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
/** The shape rule of pickBody (the header's steps 1–3). */
|
||||
function ruleBody(bodies, px, py, ground) {
|
||||
let core = null;
|
||||
const rims = [];
|
||||
for (const b of bodies) {
|
||||
const hit = hitBody(b, px, py);
|
||||
if (hit === 2) { if (!core || depthOf(b) > depthOf(core)) core = b; }
|
||||
else if (hit === 1) rims.push(b);
|
||||
}
|
||||
if (core) return core;
|
||||
const standing = ground ? bodies.filter((b) => b && b.s > 0 && onTile(b, ground)) : [];
|
||||
if (standing.length) return nearest(standing, px, py);
|
||||
return rims.length ? nearest(rims, px, py) : null;
|
||||
return best;
|
||||
}
|
||||
Reference in new issue
Block a user