Files
sganggs 01a44cd288 卫戍协议:盟约 web remake — initial import
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).
2026-09-29 16:21:31 +08:00

93 lines
4.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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]);