222 lines
7.3 KiB
JavaScript
222 lines
7.3 KiB
JavaScript
class SnakeUtils {
|
|
static snakeColor(index) {
|
|
return `var(--snake-${((index % 10) + 1)})`;
|
|
}
|
|
|
|
static stableColorIndexFromId(snakeId) {
|
|
const raw = String(snakeId || "");
|
|
let hash = 0;
|
|
for (let i = 0; i < raw.length; i += 1) {
|
|
hash = ((hash * 31) + raw.charCodeAt(i)) >>> 0;
|
|
}
|
|
return hash % 10;
|
|
}
|
|
|
|
static resolveSnakeColor(snakeId, isYou, colorById) {
|
|
if (snakeId && colorById && colorById.has(snakeId)) {
|
|
return colorById.get(snakeId);
|
|
}
|
|
if (isYou) return "var(--you)";
|
|
return SnakeUtils.snakeColor(SnakeUtils.stableColorIndexFromId(snakeId));
|
|
}
|
|
|
|
static extractSnakeColor(rawSnake) {
|
|
if (!rawSnake || typeof rawSnake !== "object") return null;
|
|
const direct = rawSnake.color;
|
|
const custom = rawSnake.customizations && rawSnake.customizations.color;
|
|
const appearance = rawSnake.appearance && rawSnake.appearance.color;
|
|
const color = direct || custom || appearance;
|
|
if (!color) return null;
|
|
return String(color).trim();
|
|
}
|
|
|
|
static buildSnakeColorById(turnData, replay) {
|
|
const colorById = new Map();
|
|
const replayTurns = replay && Array.isArray(replay.turns) ? replay.turns : [];
|
|
const boardSnakes = turnData && turnData.board && Array.isArray(turnData.board.snakes)
|
|
? turnData.board.snakes
|
|
: [];
|
|
const replaySnakes = Array.isArray(turnData && turnData.snakes) ? turnData.snakes : [];
|
|
|
|
const historicalSnakes = [];
|
|
for (const replayTurn of replayTurns) {
|
|
if (replayTurn && Array.isArray(replayTurn.snakes)) {
|
|
historicalSnakes.push(...replayTurn.snakes);
|
|
}
|
|
if (replayTurn && replayTurn.board && Array.isArray(replayTurn.board.snakes)) {
|
|
historicalSnakes.push(...replayTurn.board.snakes);
|
|
}
|
|
}
|
|
|
|
for (const snake of [...historicalSnakes, ...boardSnakes, ...replaySnakes]) {
|
|
if (!snake) continue;
|
|
const snakeId = snake.id || snake.snake_id;
|
|
if (!snakeId) continue;
|
|
const color = SnakeUtils.extractSnakeColor(snake);
|
|
if (color) colorById.set(snakeId, color);
|
|
}
|
|
return colorById;
|
|
}
|
|
|
|
static buildSnakeCustomizationById(turnData, replay) {
|
|
const customById = new Map();
|
|
const replayTurns = replay && Array.isArray(replay.turns) ? replay.turns : [];
|
|
const sources = [];
|
|
|
|
for (const replayTurn of replayTurns) {
|
|
if (replayTurn && replayTurn.board && Array.isArray(replayTurn.board.snakes)) {
|
|
sources.push(...replayTurn.board.snakes);
|
|
}
|
|
}
|
|
|
|
if (turnData && turnData.board && Array.isArray(turnData.board.snakes)) {
|
|
sources.push(...turnData.board.snakes);
|
|
}
|
|
|
|
for (const snake of sources) {
|
|
if (!snake) continue;
|
|
const snakeId = snake.id || snake.snake_id;
|
|
if (!snakeId) continue;
|
|
const custom = snake.customizations || {};
|
|
const head = custom.head || null;
|
|
const tail = custom.tail || null;
|
|
if (head || tail) {
|
|
customById.set(snakeId, { head, tail });
|
|
}
|
|
}
|
|
|
|
return customById;
|
|
}
|
|
|
|
static buildCustomizationIconUrl(kind, value) {
|
|
const raw = String(value || "").trim().toLowerCase();
|
|
if (!raw) return null;
|
|
if (!/^[a-z0-9-]+$/.test(raw)) return null;
|
|
return `/dashboard/customizations/${kind}/${raw}.svg`;
|
|
}
|
|
|
|
static parseSnakeColor(color) {
|
|
if (!color) return null;
|
|
const value = String(color).trim();
|
|
if (value.startsWith("#")) {
|
|
const hex = value.slice(1);
|
|
if (hex.length === 3) {
|
|
const r = parseInt(hex[0] + hex[0], 16);
|
|
const g = parseInt(hex[1] + hex[1], 16);
|
|
const b = parseInt(hex[2] + hex[2], 16);
|
|
return Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b) ? null : { r, g, b };
|
|
}
|
|
if (hex.length === 6) {
|
|
const r = parseInt(hex.slice(0, 2), 16);
|
|
const g = parseInt(hex.slice(2, 4), 16);
|
|
const b = parseInt(hex.slice(4, 6), 16);
|
|
return Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b) ? null : { r, g, b };
|
|
}
|
|
}
|
|
const rgb = value.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
|
|
if (rgb) {
|
|
return {
|
|
r: Math.max(0, Math.min(255, Number(rgb[1]))),
|
|
g: Math.max(0, Math.min(255, Number(rgb[2]))),
|
|
b: Math.max(0, Math.min(255, Number(rgb[3]))),
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static snakeRowBackground(color) {
|
|
const parsed = SnakeUtils.parseSnakeColor(color);
|
|
if (!parsed) return "transparent";
|
|
const isDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
const alpha = isDark ? 0.26 : 0.16;
|
|
return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${alpha})`;
|
|
}
|
|
|
|
static inferHeadDirection(snake) {
|
|
const body = Array.isArray(snake && snake.body) ? snake.body : [];
|
|
if (body.length >= 2) {
|
|
const head = body[0];
|
|
const neck = body[1];
|
|
if (head && neck) {
|
|
const dx = Number(head.x) - Number(neck.x);
|
|
const dy = Number(head.y) - Number(neck.y);
|
|
if (dx > 0) return "right";
|
|
if (dx < 0) return "left";
|
|
if (dy > 0) return "up";
|
|
if (dy < 0) return "down";
|
|
}
|
|
}
|
|
|
|
const inferred = String(snake && snake.inferred_move ? snake.inferred_move : "").toLowerCase();
|
|
if (["up", "down", "left", "right"].includes(inferred)) return inferred;
|
|
|
|
if (body.length < 2) return "right";
|
|
const head = body[0];
|
|
const neck = body[1];
|
|
if (!head || !neck) return "right";
|
|
const dx = Number(head.x) - Number(neck.x);
|
|
const dy = Number(head.y) - Number(neck.y);
|
|
if (dx > 0) return "right";
|
|
if (dx < 0) return "left";
|
|
if (dy > 0) return "up";
|
|
if (dy < 0) return "down";
|
|
return "right";
|
|
}
|
|
|
|
static inferTailDirection(snake) {
|
|
const body = Array.isArray(snake && snake.body) ? snake.body : [];
|
|
if (body.length < 2) return "right";
|
|
const tail = body[body.length - 1];
|
|
if (!tail) return "right";
|
|
|
|
let beforeTail = null;
|
|
for (let idx = body.length - 2; idx >= 0; idx -= 1) {
|
|
const candidate = body[idx];
|
|
if (!candidate) continue;
|
|
if (Number(candidate.x) !== Number(tail.x) || Number(candidate.y) !== Number(tail.y)) {
|
|
beforeTail = candidate;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!beforeTail) {
|
|
const inferred = String(snake && snake.inferred_move ? snake.inferred_move : "").toLowerCase();
|
|
if (["up", "down", "left", "right"].includes(inferred)) return inferred;
|
|
return "right";
|
|
}
|
|
|
|
const dx = Number(beforeTail.x) - Number(tail.x);
|
|
const dy = Number(beforeTail.y) - Number(tail.y);
|
|
if (dx > 0) return "right";
|
|
if (dx < 0) return "left";
|
|
if (dy > 0) return "up";
|
|
if (dy < 0) return "down";
|
|
return "right";
|
|
}
|
|
|
|
static directionToHeadTransform(direction) {
|
|
if (direction === "left") return "scaleX(-1)";
|
|
if (direction === "up") return "rotate(270deg)";
|
|
if (direction === "down") return "rotate(90deg)";
|
|
return "rotate(0deg)";
|
|
}
|
|
|
|
static directionToTailTransform(direction) {
|
|
if (direction === "right") return "scaleX(-1)";
|
|
if (direction === "left") return "rotate(0deg)";
|
|
if (direction === "up") return "rotate(270deg) scaleX(-1)";
|
|
if (direction === "down") return "rotate(90deg) scaleX(-1)";
|
|
return "scaleX(-1)";
|
|
}
|
|
|
|
static stableVariantFromString(value) {
|
|
const raw = String(value || "");
|
|
if (!raw) return 1;
|
|
let hash = 0;
|
|
for (let i = 0; i < raw.length; i += 1) {
|
|
hash = ((hash * 33) + raw.charCodeAt(i)) >>> 0;
|
|
}
|
|
return (hash % 5) + 1;
|
|
}
|
|
}
|