feat(web): render canonical living hedge maze walls
This commit is contained in:
+100
-19
@@ -302,6 +302,48 @@
|
||||
return snapshot && Array.isArray(snapshot.cells) ? snapshot.cells.map(cell => cell[0]) : [];
|
||||
}
|
||||
|
||||
function hedgeEdgeKey(x, y, vertical) {
|
||||
return `${vertical ? "v" : "h"}:${x}:${y}`;
|
||||
}
|
||||
|
||||
function hedgeVariant(edge, count) {
|
||||
const seed = ((edge.x + 1) * 1103515245 + (edge.y + 1) * 12345 + (edge.vertical ? 97 : 13)) >>> 0;
|
||||
return seed % count + 1;
|
||||
}
|
||||
|
||||
function projectedHedgeEdges(cells, width) {
|
||||
if (!Array.isArray(cells) || !Number.isInteger(width) || width < 1) return [];
|
||||
const present = new Set(cells.map(cell => cell[0]));
|
||||
const edges = new Map();
|
||||
const add = (x, y, vertical, opened) => {
|
||||
const key = hedgeEdgeKey(x, y, vertical);
|
||||
const existing = edges.get(key);
|
||||
edges.set(key, {key, x, y, vertical, opened: !!opened || !!(existing && existing.opened)});
|
||||
};
|
||||
cells.forEach(cell => {
|
||||
if (!Array.isArray(cell) || cell.length < 3 || !Number.isInteger(cell[0])) return;
|
||||
const index = cell[0]; const passages = cell[1]; const opened = cell[2];
|
||||
const x = index % width; const y = Math.floor(index / width);
|
||||
const eastVisible = present.has(index + 1) && (index + 1) % width !== 0;
|
||||
const southVisible = present.has(index + width);
|
||||
if (!(passages & 1) || (opened & 1)) add(x, y, false, opened & 1);
|
||||
if (!(passages & 8) || (opened & 8)) add(x, y, true, opened & 8);
|
||||
if ((!eastVisible && !(passages & 2)) || (opened & 2)) add(x + 1, y, true, opened & 2);
|
||||
if ((!southVisible && !(passages & 4)) || (opened & 4)) add(x, y + 1, false, opened & 4);
|
||||
});
|
||||
return Array.from(edges.values());
|
||||
}
|
||||
|
||||
function hedgeKeyForMove(cellIndex, width, direction) {
|
||||
if (!Number.isInteger(cellIndex) || !Number.isInteger(width) || width < 1) return null;
|
||||
const x = cellIndex % width; const y = Math.floor(cellIndex / width);
|
||||
if (direction === "north") return hedgeEdgeKey(x, y, false);
|
||||
if (direction === "east") return hedgeEdgeKey(x + 1, y, true);
|
||||
if (direction === "south") return hedgeEdgeKey(x, y + 1, false);
|
||||
if (direction === "west") return hedgeEdgeKey(x, y, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
function clampObserverZoom(value) {
|
||||
return Math.min(6, Math.max(.25, value));
|
||||
}
|
||||
@@ -313,7 +355,7 @@
|
||||
|
||||
function boot(documentObject, windowObject) {
|
||||
const byId = id => documentObject.getElementById(id);
|
||||
const state = {socket: null, clientSequence: 0, serverSequence: 0, viewSequence: -1, session: null, lobby: null, snapshot: null, reconnectTimer: null, noticeTimer: null, rail: {priority: 0, key: null, persistent: false}, effects: {impactTimer: null, rateTimer: null}, moveDirections: new Map(), touchStart: null, pending: new Map(), assembler: new SnapshotAssembler(), latency: new LatencyTracker(() => windowObject.performance.now()), audio: null, feedbackReady: false, muted: false, reducedMotion: false, onboardingSeen: false, onboardingStep: 0, avatar: {pointerId: null, startX: null, startY: null, direction: null, repeatTimer: null, repeatMs: null}, dpad: {pointerId: null, direction: null, repeatTimer: null}, observer: {scale: 1, x: 0, y: 0, dragging: null}};
|
||||
const state = {socket: null, clientSequence: 0, serverSequence: 0, viewSequence: -1, session: null, lobby: null, snapshot: null, reconnectTimer: null, noticeTimer: null, rail: {priority: 0, key: null, persistent: false}, effects: {impactTimer: null, rateTimer: null, breakthroughTimer: null}, hedge: {openedKeys: new Set()}, moveDirections: new Map(), touchStart: null, pending: new Map(), assembler: new SnapshotAssembler(), latency: new LatencyTracker(() => windowObject.performance.now()), audio: null, feedbackReady: false, muted: false, reducedMotion: false, onboardingSeen: false, onboardingStep: 0, avatar: {pointerId: null, startX: null, startY: null, direction: null, repeatTimer: null, repeatMs: null}, dpad: {pointerId: null, direction: null, repeatTimer: null}, observer: {scale: 1, x: 0, y: 0, dragging: null}};
|
||||
state.moveFlow = new LatestIntentFlow((direction, element) => sendMoveNow(direction, element));
|
||||
|
||||
function showNotice(text, kind, priority, persistent, key) {
|
||||
@@ -332,8 +374,11 @@
|
||||
function clearEffects() {
|
||||
windowObject.clearTimeout(state.effects.impactTimer);
|
||||
windowObject.clearTimeout(state.effects.rateTimer);
|
||||
windowObject.clearTimeout(state.effects.breakthroughTimer);
|
||||
state.effects.impactTimer = null;
|
||||
state.effects.rateTimer = null;
|
||||
state.effects.breakthroughTimer = null;
|
||||
state.hedge.openedKeys.clear();
|
||||
byId("maze").classList.remove("impact-north", "impact-east", "impact-south", "impact-west", "rate-limited");
|
||||
byId("wall-bump").classList.remove("show", "impact-north", "impact-east", "impact-south", "impact-west");
|
||||
documentObject.querySelectorAll(".cell.bush-impact").forEach(cell => cell.classList.remove("bush-impact", "bush-stage-1", "bush-stage-2", "bush-stage-3", "bush-stage-4"));
|
||||
@@ -353,7 +398,11 @@
|
||||
const stage = Math.min(4, Math.max(1, Number.isInteger(hits) ? hits : 1));
|
||||
const target = state.snapshot && documentObject.querySelector(`.cell[data-cell-index="${state.snapshot.self}"]`);
|
||||
if (target && Number.isInteger(hits) && hits > 0) target.classList.add("bush-impact", `bush-stage-${stage}`);
|
||||
state.effects.impactTimer = windowObject.setTimeout(() => { maze.classList.remove("impact-north", "impact-east", "impact-south", "impact-west"); bump.classList.remove("show", "impact-north", "impact-east", "impact-south", "impact-west"); if (target) target.classList.remove("bush-impact", `bush-stage-${stage}`); }, 340);
|
||||
const hedgeKey = state.snapshot && hedgeKeyForMove(state.snapshot.self, state.snapshot.width, direction);
|
||||
const hedge = hedgeKey && maze.querySelector(`[data-hedge-key="${hedgeKey}"]`);
|
||||
const hedgeUse = hedge && hedge.querySelector("use");
|
||||
if (hedgeUse && Number.isInteger(hits) && hits > 0) hedgeUse.setAttribute("href", `/hedges.svg#hedge-impact-${stage}`);
|
||||
state.effects.impactTimer = windowObject.setTimeout(() => { maze.classList.remove("impact-north", "impact-east", "impact-south", "impact-west"); bump.classList.remove("show", "impact-north", "impact-east", "impact-south", "impact-west"); if (target) target.classList.remove("bush-impact", `bush-stage-${stage}`); if (hedgeUse) hedgeUse.setAttribute("href", `/hedges.svg#${hedge.dataset.hedgeSymbol}`); }, 340);
|
||||
}
|
||||
|
||||
function showRateLimit() {
|
||||
@@ -470,6 +519,51 @@
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function hedgeSymbol(edge) {
|
||||
return edge.opened ? `hedge-breach-${hedgeVariant(edge, 3)}` : `hedge-intact-${hedgeVariant(edge, 6)}`;
|
||||
}
|
||||
|
||||
function renderHedgeEdges(maze, cells, width, minX, minY) {
|
||||
const edges = projectedHedgeEdges(cells, width);
|
||||
const openedKeys = new Set(edges.filter(edge => edge.opened).map(edge => edge.key));
|
||||
const newOpenedKeys = new Set([...openedKeys].filter(key => !state.hedge.openedKeys.has(key)));
|
||||
const layer = documentObject.createElement("span");
|
||||
layer.className = "hedge-layer";
|
||||
edges.forEach(edge => {
|
||||
const wrapper = documentObject.createElement("span");
|
||||
const symbol = hedgeSymbol(edge);
|
||||
wrapper.className = `hedge-edge ${edge.vertical ? "hedge-vertical" : "hedge-horizontal"}`;
|
||||
wrapper.dataset.hedgeKey = edge.key;
|
||||
wrapper.dataset.hedgeSymbol = symbol;
|
||||
wrapper.style.setProperty("--hedge-x", String(edge.x - minX));
|
||||
wrapper.style.setProperty("--hedge-y", String(edge.y - minY));
|
||||
const svg = documentObject.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("viewBox", "0 0 160 40");
|
||||
svg.setAttribute("preserveAspectRatio", "none");
|
||||
const use = documentObject.createElementNS("http://www.w3.org/2000/svg", "use");
|
||||
use.setAttribute("href", `/hedges.svg#${symbol}`);
|
||||
svg.appendChild(use);
|
||||
wrapper.appendChild(svg);
|
||||
layer.appendChild(wrapper);
|
||||
});
|
||||
maze.appendChild(layer);
|
||||
state.hedge.openedKeys = openedKeys;
|
||||
if (newOpenedKeys.size && !state.reducedMotion) {
|
||||
newOpenedKeys.forEach(key => {
|
||||
const edge = layer.querySelector(`[data-hedge-key="${key}"]`);
|
||||
const use = edge && edge.querySelector("use");
|
||||
if (use) use.setAttribute("href", "/hedges.svg#hedge-breakthrough");
|
||||
});
|
||||
windowObject.clearTimeout(state.effects.breakthroughTimer);
|
||||
state.effects.breakthroughTimer = windowObject.setTimeout(() => {
|
||||
layer.querySelectorAll(".hedge-edge").forEach(edge => {
|
||||
const use = edge.querySelector("use");
|
||||
if (use) use.setAttribute("href", `/hedges.svg#${edge.dataset.hedgeSymbol}`);
|
||||
});
|
||||
}, 340);
|
||||
}
|
||||
}
|
||||
|
||||
function addObject(cell, object) {
|
||||
const [kind, owner, variant] = object;
|
||||
let marker;
|
||||
@@ -527,19 +621,12 @@
|
||||
cell.className = "cell";
|
||||
cell.dataset.cellIndex = String(data.index);
|
||||
cell.style.gridColumn = String(data.x - minX + 1); cell.style.gridRow = String(data.y - minY + 1);
|
||||
if (!(data.passages & 1)) cell.classList.add("wall-n");
|
||||
if (!(data.passages & 2)) cell.classList.add("wall-e");
|
||||
if (!(data.passages & 4)) cell.classList.add("wall-s");
|
||||
if (!(data.passages & 8)) cell.classList.add("wall-w");
|
||||
if (data.opened & 1) cell.classList.add("trampled-n");
|
||||
if (data.opened & 2) cell.classList.add("trampled-e");
|
||||
if (data.opened & 4) cell.classList.add("trampled-s");
|
||||
if (data.opened & 8) cell.classList.add("trampled-w");
|
||||
(objects.get(data.index) || []).forEach(object => addObject(cell, object));
|
||||
const slot = state.session ? state.session.slot : 1;
|
||||
if (data.index === snapshot.opponent) addPawn(cell, slot === 1 ? 2 : 1, true);
|
||||
maze.appendChild(cell);
|
||||
});
|
||||
renderHedgeEdges(maze, snapshot.cells, snapshot.width, minX, minY);
|
||||
byId("map-placeholder").hidden = true;
|
||||
}
|
||||
|
||||
@@ -601,6 +688,7 @@
|
||||
setScreen("observer");
|
||||
const maze = byId("observer-maze");
|
||||
maze.replaceChildren();
|
||||
maze.style.setProperty("--cell", `${OBSERVER_CELL_SIZE}px`);
|
||||
maze.style.gridTemplateColumns = `repeat(${snapshot.width}, ${OBSERVER_CELL_SIZE}px)`;
|
||||
maze.style.gridTemplateRows = `repeat(${snapshot.height}, ${OBSERVER_CELL_SIZE}px)`;
|
||||
const objects = new Map();
|
||||
@@ -612,14 +700,6 @@
|
||||
cell.className = "cell";
|
||||
cell.style.gridColumn = String(index % snapshot.width + 1);
|
||||
cell.style.gridRow = String(Math.floor(index / snapshot.width) + 1);
|
||||
if (!(passages & 1)) cell.classList.add("wall-n");
|
||||
if (!(passages & 2)) cell.classList.add("wall-e");
|
||||
if (!(passages & 4)) cell.classList.add("wall-s");
|
||||
if (!(passages & 8)) cell.classList.add("wall-w");
|
||||
if (opened & 1) cell.classList.add("trampled-n");
|
||||
if (opened & 2) cell.classList.add("trampled-e");
|
||||
if (opened & 4) cell.classList.add("trampled-s");
|
||||
if (opened & 8) cell.classList.add("trampled-w");
|
||||
(objects.get(index) || []).forEach(object => addObject(cell, object));
|
||||
players.forEach((player, playerIndex) => {
|
||||
if (player.cell === index && !player.exited) {
|
||||
@@ -629,6 +709,7 @@
|
||||
});
|
||||
maze.appendChild(cell);
|
||||
});
|
||||
renderHedgeEdges(maze, snapshot.cells, snapshot.width, 0, 0);
|
||||
players.forEach((player, index) => {
|
||||
const key = player.key == null ? "без ключа" : `ключ P${player.key + 1}`;
|
||||
byId(`observer-p${index + 1}`).textContent = !player.connected ? "нет связи" : player.exited ? "снаружи" : key;
|
||||
@@ -935,7 +1016,7 @@
|
||||
return {state, handleMessage, move, resetAvatarControl, latencySamples: () => state.latency.snapshot(), pointerFeedbackSamples: () => state.latency.pointerSnapshot()};
|
||||
}
|
||||
|
||||
const api = Object.freeze({TOKEN_KEY, MAX_VIEW_CHUNKS, NAME_ADJECTIVES, NAME_NOUNS, NAME_COMBINATION_COUNT, LOGIN_HEADLINES, ONBOARDING_STEPS, LATENCY_SAMPLE_LIMIT, AVATAR_TOUCH_TARGET_SIZE, AVATAR_MAX_DEFLECTION, AVATAR_DEAD_ZONE, AVATAR_DIRECTION_HYSTERESIS, AVATAR_SLOW_REPEAT_MS, AVATAR_FAST_REPEAT_MS, DIRECTIONAL_PAD_REPEAT_MS, LatestIntentFlow, LatencyTracker, directionFromKey, directionFromSwipe, avatarDirection, avatarRepeatMs, avatarVector, playerCellSize, playerMapOffset, playerFocalPoint, playerFocalMapOffset, nextOnboardingStep, randomIndex, randomSuggestedName, randomLoginHeadline, createEnvelope, snapshotIsValid, SnapshotAssembler, projectedCellIndices, clampObserverZoom, observerFitScale, boot});
|
||||
const api = Object.freeze({TOKEN_KEY, MAX_VIEW_CHUNKS, NAME_ADJECTIVES, NAME_NOUNS, NAME_COMBINATION_COUNT, LOGIN_HEADLINES, ONBOARDING_STEPS, LATENCY_SAMPLE_LIMIT, AVATAR_TOUCH_TARGET_SIZE, AVATAR_MAX_DEFLECTION, AVATAR_DEAD_ZONE, AVATAR_DIRECTION_HYSTERESIS, AVATAR_SLOW_REPEAT_MS, AVATAR_FAST_REPEAT_MS, DIRECTIONAL_PAD_REPEAT_MS, LatestIntentFlow, LatencyTracker, directionFromKey, directionFromSwipe, avatarDirection, avatarRepeatMs, avatarVector, playerCellSize, playerMapOffset, playerFocalPoint, playerFocalMapOffset, nextOnboardingStep, randomIndex, randomSuggestedName, randomLoginHeadline, createEnvelope, snapshotIsValid, SnapshotAssembler, projectedCellIndices, hedgeEdgeKey, hedgeKeyForMove, hedgeVariant, projectedHedgeEdges, clampObserverZoom, observerFitScale, boot});
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
||||
root.Maze32Player = api;
|
||||
if (root.document) root.document.addEventListener("DOMContentLoaded", () => { root.maze32PlayerApp = boot(root.document, root); });
|
||||
|
||||
Reference in New Issue
Block a user