feat(player): animate authoritative hedge breakthroughs
This commit is contained in:
+51
-1
@@ -355,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, 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}};
|
||||
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, travelTimer: 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) {
|
||||
@@ -375,10 +375,14 @@
|
||||
windowObject.clearTimeout(state.effects.impactTimer);
|
||||
windowObject.clearTimeout(state.effects.rateTimer);
|
||||
windowObject.clearTimeout(state.effects.breakthroughTimer);
|
||||
windowObject.clearTimeout(state.effects.travelTimer);
|
||||
state.effects.impactTimer = null;
|
||||
state.effects.rateTimer = null;
|
||||
state.effects.breakthroughTimer = null;
|
||||
state.effects.travelTimer = null;
|
||||
state.hedge.openedKeys.clear();
|
||||
documentObject.querySelectorAll(".breakthrough-runner").forEach(runner => runner.remove());
|
||||
byId("avatar-control").classList.remove("breakthrough-travelling");
|
||||
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"));
|
||||
@@ -418,6 +422,7 @@
|
||||
byId("match-screen").hidden = name !== "match";
|
||||
byId("observer-screen").hidden = name !== "observer";
|
||||
documentObject.body.classList.toggle("match-active", name === "match");
|
||||
if (name !== "match") stopBreakthroughTravel();
|
||||
}
|
||||
|
||||
function renderWelcomeHeadline(headline) {
|
||||
@@ -524,6 +529,45 @@
|
||||
return edge.opened ? `hedge-breach-${hedgeVariant(edge, 3)}` : `hedge-intact-${hedgeVariant(edge, 6)}`;
|
||||
}
|
||||
|
||||
function breakthroughDirection(previous, snapshot) {
|
||||
if (!previous || previous.role === "observer" || snapshot.role === "observer" || previous.width !== snapshot.width) return null;
|
||||
const delta = snapshot.self - previous.self;
|
||||
const directions = {[-snapshot.width]: "north", [1]: "east", [snapshot.width]: "south", [-1]: "west"};
|
||||
const direction = directions[delta];
|
||||
if (!direction || (direction === "east" && previous.self % snapshot.width === snapshot.width - 1) || (direction === "west" && previous.self % snapshot.width === 0)) return null;
|
||||
const key = hedgeKeyForMove(previous.self, snapshot.width, direction);
|
||||
return projectedHedgeEdges(snapshot.cells, snapshot.width).some(edge => edge.key === key && edge.opened) ? direction : null;
|
||||
}
|
||||
|
||||
function stopBreakthroughTravel() {
|
||||
windowObject.clearTimeout(state.effects.travelTimer);
|
||||
state.effects.travelTimer = null;
|
||||
documentObject.querySelectorAll(".breakthrough-runner").forEach(runner => runner.remove());
|
||||
byId("avatar-control").classList.remove("breakthrough-travelling");
|
||||
}
|
||||
|
||||
function showBreakthroughTravel(direction) {
|
||||
stopBreakthroughTravel();
|
||||
if (!direction || state.reducedMotion) return;
|
||||
const mapShell = byId("map-shell");
|
||||
const cellSize = Number.parseFloat(byId("maze").style.getPropertyValue("--cell"));
|
||||
if (!Number.isFinite(cellSize)) return;
|
||||
const offsets = {north: {x: 0, y: cellSize}, east: {x: -cellSize, y: 0}, south: {x: 0, y: -cellSize}, west: {x: cellSize, y: 0}};
|
||||
const runner = icon("player", `breakthrough-runner player-${state.session && state.session.slot === 2 ? "two" : "one"}`);
|
||||
runner.style.setProperty("--breakthrough-start-x", `${offsets[direction].x}px`);
|
||||
runner.style.setProperty("--breakthrough-start-y", `${offsets[direction].y}px`);
|
||||
runner.style.setProperty("--breakthrough-mid-x", `${offsets[direction].x * .42}px`);
|
||||
runner.style.setProperty("--breakthrough-mid-y", `${offsets[direction].y * .42}px`);
|
||||
for (let index = 0; index < 4; index++) {
|
||||
const accent = documentObject.createElement("span");
|
||||
accent.className = `breakthrough-accent accent-${index + 1}`;
|
||||
runner.appendChild(accent);
|
||||
}
|
||||
mapShell.appendChild(runner);
|
||||
byId("avatar-control").classList.add("breakthrough-travelling");
|
||||
state.effects.travelTimer = windowObject.setTimeout(stopBreakthroughTravel, 480);
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -636,6 +680,8 @@
|
||||
renderObserverSnapshot(snapshot);
|
||||
return;
|
||||
}
|
||||
const previous = state.snapshot;
|
||||
const direction = breakthroughDirection(previous, snapshot);
|
||||
state.snapshot = snapshot;
|
||||
setScreen("match");
|
||||
const slot = state.session && state.session.slot ? state.session.slot : 1;
|
||||
@@ -657,6 +703,8 @@
|
||||
byId("compass-stat").dataset.direction = hint;
|
||||
byId("compass-arrow").style.transform = `rotate(${HINT_ANGLES[hint]}deg)`;
|
||||
renderMap(snapshot);
|
||||
if (direction) showBreakthroughTravel(direction);
|
||||
else stopBreakthroughTravel();
|
||||
if (Number.isInteger(snapshot.stateSequence)) {
|
||||
windowObject.requestAnimationFrame(() => state.latency.painted(snapshot.stateSequence));
|
||||
}
|
||||
@@ -849,6 +897,7 @@
|
||||
function resetPlayerControls() {
|
||||
resetAvatarControl();
|
||||
resetDirectionalPad();
|
||||
stopBreakthroughTravel();
|
||||
}
|
||||
|
||||
function resetAvatarControl() {
|
||||
@@ -911,6 +960,7 @@
|
||||
|
||||
function setReducedMotion(reduced) {
|
||||
state.reducedMotion = reduced;
|
||||
if (reduced) stopBreakthroughTravel();
|
||||
documentObject.documentElement.dataset.reducedMotion = String(reduced);
|
||||
byId("motion-toggle").setAttribute("aria-pressed", String(reduced));
|
||||
byId("motion-toggle").setAttribute("aria-label", reduced ? "Включить движение" : "Уменьшить движение");
|
||||
|
||||
Reference in New Issue
Block a user