卫戍协议:盟约 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).
This commit is contained in:
sganggs committed 2026-09-29 16:21:31 +08:00
commit 01a44cd288
408 files changed
+308986

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
// 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]);