Files
ark-stronghold-protocol/public/js/render/board3d/atlas.js
T
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

137 lines
6.8 KiB
JavaScript
Raw 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.
// render/board3d/atlas.js — which pixels of the official board textures dress each surface of the 3D board
// (DESIGN §15). Pure data + UV helpers (no three import): the geometry builder (layout.js) maps these rects straight
// onto the tiles, the three.js scene samples the textures of MT_autochess (TX_autochessi_D + _N + _M + _E, which
// share one UV layout) and MT_autochess_common (TX_autochessi_common_D/_E, device plates).
//
// Source of truth for the rects: this table. tools/crop-board-atlas.mjs validates every rect against the extracted
// images and writes it into public/assets/local/map/autochess/tiles.json → `board3d`, which the renderer prefers
// (so a re-crop needs no code change); `resolveUvTable(tiles)` merges it over these defaults.
//
// surface = { src: 'D' | 'common', rect: [x, y, w, h] (source px, y down), rot?: 0|90|180|270 (clockwise, of the
// texture on the face), flipX?: bool, tint?: '#rrggbb' (vertex colour multiply), glow?: number }
//
// Atlas pieces (TX_autochessi_D 2048², 256 px per floor cell; picked by eye, validated by the crop tool): gold-framed
// high-ground plates (row 0: strip L / M / R + single), concrete slabs (rows 1–2: rails, ✕ marker, hazard stripes,
// chevrons, red no-deploy stripes), wall panels (REST AREA / EVACUATION), REINFORCEMENTS / EQUIPMENTS bench pads
// (512² each), ribbed side panels (gold / grey), the hazard-framed glass hatch, the ▲▼ lift plate, grille slats,
// dark machinery hatch, gold ring hatch (objective), orange crate panels.
export const SOURCES = Object.freeze({
D: Object.freeze({ w: 2048, h: 2048 }),
common: Object.freeze({ w: 1024, h: 1024 }),
});
const S = (src, rect, o = {}) => Object.freeze({ src, rect: Object.freeze(rect), ...o });
/** Surface → atlas region (see header). Names are used by layout.js. */
export const SURFACES = Object.freeze({
// floors
concrete: S('D', [256, 512, 256, 256]),
concreteRailTL: S('D', [0, 256, 256, 256]),
concreteRailT: S('D', [256, 256, 256, 256]),
concreteRailL: S('D', [0, 512, 256, 256]),
concreteStripe: S('D', [512, 256, 256, 256]),
concreteArrow: S('D', [768, 256, 256, 256]),
concreteRed: S('D', [768, 512, 256, 256]),
hatch: S('D', [549, 1787, 255, 256]),
lift: S('D', [804, 1790, 234, 234]),
ringHatch: S('D', [1290, 1400, 190, 190]),
slats: S('D', [1036, 480, 404, 216]),
mech: S('D', [1088, 708, 356, 310]),
steel: S('D', [12, 776, 196, 196]),
// high ground (gold-framed plates; texture top = the tile's far edge)
plateL: S('D', [0, 0, 256, 256]),
plateM: S('D', [256, 0, 256, 256]),
plateR: S('D', [512, 0, 256, 256]),
plateS: S('D', [768, 0, 256, 256]),
// bench pads
padReinf: S('D', [0, 1024, 512, 512]),
padEquip: S('D', [512, 1024, 512, 512]),
// side panels (cropped to the face aspect at use: see sideRect)
goldSide: S('D', [0, 1664, 272, 384]),
graySide: S('D', [272, 1664, 272, 384]),
benchRail: S('D', [0, 1538, 512, 98]),
restPanel: S('D', [0, 768, 512, 208]),
evacPanel: S('D', [512, 768, 256, 208]),
pipePanel: S('D', [544, 1540, 480, 244]),
// devices
crateSide: S('D', [1087, 1938, 134, 108]),
crateTop: S('D', [1222, 1938, 134, 108], { rot: 90 }),
// common atlas plates (MT_autochess_common, alpha-tested decals)
hazardX: S('common', [2, 2, 250, 248]),
heal: S('common', [264, 2, 256, 250]),
shield: S('common', [538, 2, 256, 250]),
target: S('common', [2, 262, 250, 250]),
blast: S('common', [538, 262, 250, 250]),
enemyMark: S('common', [792, 258, 232, 262]),
fast: S('common', [2, 520, 250, 250]),
arrowUp: S('common', [508, 762, 244, 244]),
arrowDown: S('common', [776, 762, 244, 244]),
});
const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
/** Validate one surface record (from tiles.json) → a clean record or null. */
export function cleanSurface(v) {
if (!isObj(v) || !SOURCES[v.src] || !Array.isArray(v.rect) || v.rect.length !== 4) return null;
const [x, y, w, h] = v.rect.map(Number);
const src = SOURCES[v.src];
if (![x, y, w, h].every(Number.isFinite) || w < 4 || h < 4 || x < 0 || y < 0 || x + w > src.w || y + h > src.h) return null;
const rot = [0, 90, 180, 270].includes(v.rot) ? v.rot : 0;
const out = { src: v.src, rect: [x, y, w, h], rot, flipX: !!v.flipX };
if (typeof v.tint === 'string' && /^#[0-9a-f]{6}$/i.test(v.tint)) out.tint = v.tint;
return out;
}
/** Surface table: defaults overridden by a tiles.json `board3d` section (invalid entries ignored). */
export function resolveUvTable(tiles) {
const out = {};
for (const [k, v] of Object.entries(SURFACES)) out[k] = cleanSurface(v);
const extra = isObj(tiles) && isObj(tiles.board3d) ? tiles.board3d : null;
if (extra) for (const [k, v] of Object.entries(extra)) { const c = cleanSurface(v); if (c) out[k] = c; }
return out;
}
/**
* UV corners of a surface for a quad whose corners are given as [bottom-left, bottom-right, top-right, top-left]
* of the face as seen from outside (for a top face: near-left, near-right, far-right, far-left). Optional
* `sub` = [u0, v0, u1, v1] (fractions of the rect, v down) crops the rect (side panels, partial faces).
* Returns [u,v, u,v, u,v, u,v] in three's convention (v up, textures flipped on upload). `inset` (source px)
* keeps mip filtering inside the region.
*/
export function surfaceUV(surface, sub = null, inset = 2) {
const src = SOURCES[surface.src] || SOURCES.D;
let [x, y, w, h] = surface.rect;
if (sub) {
const [a, b, c, d] = sub;
x += w * a; y += h * b; const w2 = w * (c - a), h2 = h * (d - b); w = w2; h = h2;
}
const ix = Math.min(inset, w / 4), iy = Math.min(inset, h / 4);
const u0 = (x + ix) / src.w, u1 = (x + w - ix) / src.w;
const vTop = 1 - (y + iy) / src.h, vBot = 1 - (y + h - iy) / src.h;
// texture corners in face order BL, BR, TR, TL
let c = [[u0, vBot], [u1, vBot], [u1, vTop], [u0, vTop]];
if (surface.flipX) c = [c[1], c[0], c[3], c[2]];
const turns = ((surface.rot | 0) / 90) & 3;
// rotating the texture clockwise on the face = the face corner k samples texture corner k − turns
for (let t = 0; t < turns; t++) c = [c[1], c[2], c[3], c[0]];
return [c[0][0], c[0][1], c[1][0], c[1][1], c[2][0], c[2][1], c[3][0], c[3][1]];
}
/** Sub-rect (fractions) of a side panel that keeps the texture undistorted on a face `width` × `height` tiles. */
export function sideRect(surface, width, height) {
const [, , w, h] = surface.rect;
const faceAspect = Math.max(1e-3, height / Math.max(1e-3, width));
const texAspect = h / w;
if (faceAspect >= texAspect) return [0, 0, 1, 1];
const frac = faceAspect / texAspect;
return [0, 0, 1, frac];
}
/** '#rrggbb' → [r, g, b] (0..1); null → [1, 1, 1]. */
export function tintRgb(hex) {
if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return [1, 1, 1];
const n = parseInt(hex.slice(1), 16);
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
}