Online co-op browser remake of Arknights' Stronghold Protocol: Alliance (client-simulated combat, official waves/bonds/items/strategies, 3D board, facing wheel, operator loadout, official emotes, deployment scripts).
93 lines
4.7 KiB
JavaScript
93 lines
4.7 KiB
JavaScript
// server/sim/dir.js — the 4 deploy directions (DESIGN §3 "Facing (corrected)", research 09 §1.2). Pure ESM, shared by
|
||
// the sim, the match (server/match) and browsers (/sim/dir.js).
|
||
//
|
||
// Every board piece / ally unit has `dir ∈ UP | RIGHT | DOWN | LEFT` (default RIGHT, toward the gates). Row 0 is the
|
||
// BOTTOM row, so UP = +row. Forward vectors [dRow, dCol]: UP (1,0), RIGHT (0,1), DOWN (−1,0), LEFT (0,−1).
|
||
// Range grids / relative offsets `[dRow, dCol]` are authored facing RIGHT and rotated:
|
||
// RIGHT (dr, dc) · UP (dc, −dr) · LEFT (−dr, −dc) · DOWN (−dc, dr)
|
||
// (a quarter turn per step: the "up" side of a right-facing unit — its left hand — becomes the left side when it faces
|
||
// UP). `rangeExtend` is applied along +dCol BEFORE rotating (targeting.absoluteRangeKeys). The right-hand Final
|
||
// Assault player is mirrored: col c → 20 − c with RIGHT ↔ LEFT (UP / DOWN unchanged) — `mirrorDir`.
|
||
// The legacy scalar facing (+1 / −1) survives only as the derived horizontal sign `hSign(dir)` (sprite flip).
|
||
|
||
export const DIRS = Object.freeze(['UP', 'RIGHT', 'DOWN', 'LEFT']);
|
||
export const DEFAULT_DIR = 'RIGHT';
|
||
/** Forward vector [dRow, dCol] per direction. */
|
||
export const DIR_VEC = Object.freeze({
|
||
UP: Object.freeze([1, 0]), RIGHT: Object.freeze([0, 1]), DOWN: Object.freeze([-1, 0]), LEFT: Object.freeze([0, -1]),
|
||
});
|
||
const OPPOSITE = Object.freeze({ UP: 'DOWN', DOWN: 'UP', RIGHT: 'LEFT', LEFT: 'RIGHT' });
|
||
const MIRROR = Object.freeze({ UP: 'UP', DOWN: 'DOWN', RIGHT: 'LEFT', LEFT: 'RIGHT' });
|
||
|
||
export const isDir = (d) => d === 'UP' || d === 'RIGHT' || d === 'DOWN' || d === 'LEFT';
|
||
|
||
/**
|
||
* A direction from anything a caller may hold: a direction name (any case), a legacy facing sign (−1 ⇒ LEFT, +1 ⇒
|
||
* RIGHT) or junk (⇒ `fallback`).
|
||
*/
|
||
export function normDir(d, fallback = DEFAULT_DIR) {
|
||
if (isDir(d)) return d;
|
||
if (typeof d === 'string') { const u = d.trim().toUpperCase(); if (isDir(u)) return u; }
|
||
if (typeof d === 'number' && Number.isFinite(d) && d !== 0) return d < 0 ? 'LEFT' : 'RIGHT';
|
||
return isDir(fallback) ? fallback : DEFAULT_DIR;
|
||
}
|
||
|
||
/** Forward vector [dRow, dCol] of a direction (junk ⇒ RIGHT). */
|
||
export const dirVec = (d) => DIR_VEC[normDir(d)];
|
||
|
||
/** Rotate a facing-RIGHT offset [dr, dc] into absolute [dRow, dCol] for direction `d`. */
|
||
export function rotateOffset(dr, dc, d) {
|
||
// `0 - x` (not `-x`): never a negative zero
|
||
switch (normDir(d)) {
|
||
case 'UP': return [dc, 0 - dr];
|
||
case 'LEFT': return [0 - dr, 0 - dc];
|
||
case 'DOWN': return [0 - dc, dr];
|
||
default: return [dr, dc];
|
||
}
|
||
}
|
||
|
||
/** Inverse of rotateOffset: an absolute delta [dRow, dCol] in the facing-RIGHT frame of direction `d`. */
|
||
export function toLocal(ar, ac, d) {
|
||
switch (normDir(d)) {
|
||
case 'UP': return [0 - ac, ar];
|
||
case 'LEFT': return [0 - ar, 0 - ac];
|
||
case 'DOWN': return [ac, 0 - ar];
|
||
default: return [ar, ac];
|
||
}
|
||
}
|
||
|
||
/** Horizontal sign for sprite flipping (LEFT ⇒ −1, everything else +1). */
|
||
export const hSign = (d) => (normDir(d) === 'LEFT' ? -1 : 1);
|
||
/** RIGHT ↔ LEFT (the Final Assault right-side mirror); UP / DOWN unchanged. */
|
||
export const mirrorDir = (d) => MIRROR[normDir(d)];
|
||
export const oppositeDir = (d) => OPPOSITE[normDir(d)];
|
||
/** Are two directions perpendicular? */
|
||
export const perpendicular = (a, b) => { const [ar, ac] = dirVec(a), [br, bc] = dirVec(b); return ar * br + ac * bc === 0; };
|
||
/** Direction of a (row, col) delta: the dominant axis (ties → horizontal); a zero delta ⇒ `fallback`. */
|
||
export function dirFromDelta(dRow, dCol, fallback = DEFAULT_DIR) {
|
||
const r = Number(dRow) || 0, c = Number(dCol) || 0;
|
||
if (r === 0 && c === 0) return normDir(fallback);
|
||
if (Math.abs(c) >= Math.abs(r)) return c > 0 ? 'RIGHT' : 'LEFT';
|
||
return r > 0 ? 'UP' : 'DOWN';
|
||
}
|
||
|
||
/** Tile [r, c] reached from (r, c) by the facing-RIGHT offset [dr, dc] of a unit facing `d`. */
|
||
export function offsetTile(r, c, dr, dc, d) {
|
||
const [a, b] = rotateOffset(dr, dc, d);
|
||
return [r + a, c + b];
|
||
}
|
||
/** Tile `k` steps in front of (k < 0: behind) (r, c) for direction `d`. */
|
||
export function frontOf(r, c, d, k = 1) {
|
||
const [fr, fc] = dirVec(d);
|
||
return [r + fr * k, c + fc * k];
|
||
}
|
||
|
||
/**
|
||
* Tie-break order of a tile relative to a unit, in the unit's facing-RIGHT frame: [localRow, localCol] = toLocal of the
|
||
* absolute delta. Comparing it lexicographically (localBefore) instead of the absolute tile key keeps "first tile"
|
||
* choices turning with the unit; for a RIGHT-facing unit it is exactly the tile-key order (row, then col).
|
||
*/
|
||
export const localOrder = (dRow, dCol, d) => toLocal(dRow, dCol, d);
|
||
/** Lexicographic `a` < `b` of two localOrder pairs (`b` null ⇒ true). */
|
||
export const localBefore = (a, b) => !b || a[0] < b[0] || (a[0] === b[0] && a[1] < b[1]);
|