feat(ui): replace gameplay icons with local SVGs

This commit is contained in:
2026-09-10 21:47:15 +03:00
parent 8214e7f7ed
commit abb3ff27b7
11 changed files with 162 additions and 74 deletions
+19 -23
View File
@@ -19,11 +19,11 @@
const ONBOARDING_STEPS = Object.freeze([
Object.freeze({icon: "hand", title: "Потяни героя", text: "Потяни героя — лабиринт поедет под ним."}),
Object.freeze({icon: "motion", title: "Отпусти — герой ждёт", text: "Рука остановилась — шаги тоже."}),
Object.freeze({icon: "maze", title: "Стена не пускает", text: "Упрись в стену — попробуй другую сторону."}),
Object.freeze({icon: "spikes", title: "Стена не пускает", text: "Упрись в стену — попробуй другую сторону."}),
Object.freeze({icon: "key", title: "Найди ключ", text: "Ключ открывает твой выход."}),
Object.freeze({icon: "eye", title: "Глаз расширяет карту", text: "Возьми глаз — увидишь дальше."}),
Object.freeze({icon: "crystal", title: "Кристалл расширяет карту", text: "Возьми кристалл — увидишь дальше."}),
Object.freeze({icon: "arrow", title: "Стрелка знает путь", text: "Компас показывает сторону выхода."}),
Object.freeze({icon: "exit", title: "Выходи с ключом", text: "Коснись своего выхода с ключом."}),
Object.freeze({icon: "exit-closed", title: "Выходи с ключом", text: "Коснись своего выхода с ключом."}),
Object.freeze({icon: "friends", title: "Встретил друга", text: "Другой герой может отдать найденный ключ."})
]);
const NAME_ADJECTIVES = Object.freeze(["Бодрый", "Хитрый", "Тихий", "Смелый", "Ловкий", "Сонный", "Шустрый", "Лунный", "Яркий", "Дикий", "Вечный", "Храбрый"]);
@@ -421,29 +421,28 @@
function addObject(cell, object) {
const [kind, owner, variant] = object;
let marker;
const ownerVariant = owner === 1 ? "two" : "one";
if (kind === "key") {
marker = icon("key", "object object-key"); marker.dataset.owner = owner === 1 ? "two" : "one";
marker = icon("key", "object object-key"); marker.dataset.owner = ownerVariant;
} else if (kind === "exit") {
marker = documentObject.createElement("span"); marker.className = "object object-exit"; marker.dataset.owner = owner === 1 ? "two" : "one"; marker.dataset.open = String(variant);
marker = icon(variant ? "exit-open" : "exit-closed", "object object-exit"); marker.dataset.owner = ownerVariant; marker.dataset.open = String(variant);
} else if (kind === "compass") {
marker = icon("arrow", "object object-compass");
} else if (kind === "vision") {
marker = icon("eye", "object object-vision");
marker = icon("crystal", "object object-vision");
} else {
marker = documentObject.createElement("span"); marker.className = "object object-landmark"; marker.textContent = ["♣", "✿", "♠", "⌂"][variant % 4] || "♣";
}
cell.appendChild(marker);
}
function addPawn(cell, label, opponent) {
const pawn = documentObject.createElement("span");
pawn.className = opponent ? "pawn pawn-opponent" : "pawn pawn-self";
const text = documentObject.createElement("span"); text.textContent = label;
pawn.appendChild(text); cell.appendChild(pawn);
function addPawn(cell, slot, opponent) {
const pawn = icon("player", `pawn ${opponent ? "pawn-opponent" : "pawn-self"} pawn-owner-${slot === 2 ? "two" : "one"}`);
cell.appendChild(pawn);
}
function characterForSlot(slot) {
return slot === 2 ? {symbol: "◆", name: "Мышонок"} : {symbol: "●", name: "Лисёнок"};
return slot === 2 ? {name: "Синий герой", keyName: "синего игрока", owner: "two"} : {name: "Жёлтый герой", keyName: "жёлтого игрока", owner: "one"};
}
function renderMap(snapshot) {
@@ -477,7 +476,7 @@
if (!(data.passages & 8)) cell.classList.add("wall-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, characterForSlot(slot === 1 ? 2 : 1).symbol, true);
if (data.index === snapshot.opponent) addPawn(cell, slot === 1 ? 2 : 1, true);
maze.appendChild(cell);
});
byId("map-placeholder").hidden = true;
@@ -492,15 +491,13 @@
setScreen("match");
const slot = state.session && state.session.slot ? state.session.slot : 1;
const character = characterForSlot(slot);
byId("self-slot").textContent = character.symbol;
byId("self-slot").setAttribute("aria-label", character.name);
byId("avatar-handle-symbol").textContent = character.symbol;
byId("avatar-control").setAttribute("aria-label", `Тянуть героя ${character.name} для движения`);
byId("self-name").textContent = playerName();
byId("identity").className = `identity player-${slot === 1 ? "one" : "two"}`;
byId("match-screen").classList.toggle("player-two-mode", slot === 2);
byId("match-state").textContent = PHASES[snapshot.phase] || "Гонка идёт";
byId("inventory").textContent = snapshot.inventory == null ? "Пусто" : `Ключ ${characterForSlot(snapshot.inventory + 1).symbol}`;
byId("inventory").textContent = snapshot.inventory == null ? "Пусто" : `Ключ ${characterForSlot(snapshot.inventory + 1).keyName}`;
byId("inventory-stat").dataset.filled = String(snapshot.inventory != null);
byId("drop-key").disabled = snapshot.inventory == null;
setDirectionalPadEnabled(snapshot.phase === "active" && state.socket && state.socket.readyState === windowObject.WebSocket.OPEN);
@@ -561,10 +558,8 @@
(objects.get(index) || []).forEach(object => addObject(cell, object));
players.forEach((player, playerIndex) => {
if (player.cell === index && !player.exited) {
const pawn = documentObject.createElement("span");
pawn.className = `pawn pawn-player-${playerIndex === 0 ? "one" : "two"}`;
const label = documentObject.createElement("span"); label.textContent = `P${playerIndex + 1}`;
pawn.appendChild(label); cell.appendChild(pawn);
const pawn = icon("player", `pawn pawn-player-${playerIndex === 0 ? "one" : "two"} pawn-owner-${playerIndex === 0 ? "one" : "two"}`);
cell.appendChild(pawn);
}
});
maze.appendChild(cell);
@@ -582,7 +577,8 @@
else if (snapshot.phase !== "paused") hideModal();
}
function showModal(title, text, kicker, countdown) {
function showModal(title, text, kicker, countdown, iconName) {
byId("modal-icon").querySelector("use").setAttribute("href", `/icons.svg#${iconName || "hourglass"}`);
byId("modal-title").textContent = title; byId("modal-text").textContent = text; byId("modal-kicker").textContent = kicker;
byId("countdown").hidden = !countdown; byId("modal").hidden = false;
}
@@ -616,7 +612,7 @@
title = slot ? (winner === slot ? "Победа!" : "Победил соперник") : `Победил Игрок ${winner}`;
text = slot ? (winner === slot ? "Вы официально быстрее очень большого куста." : "Соперник нашёл выход. Лабиринт требует реванш.") : "Зрительный зал фиксирует героический побег без вмешательства в результат.";
}
showModal(title, text, "ГОНКА ЗАВЕРШЕНА", false);
showModal(title, text, "ГОНКА ЗАВЕРШЕНА", false, "exit-open");
resetPlayerControls();
byId("modal-action").textContent = state.session && state.session.role === "player" && state.session.slot === 1 ? "Новая гонка" : "Вернуться в лобби";
byId("modal-action").hidden = false;
@@ -658,7 +654,7 @@
} else if (message.type === "error") {
state.moveFlow.complete(message.requestId);
if (message.payload.code === "invalid_token") { windowObject.localStorage.removeItem(TOKEN_KEY); state.session = null; setScreen("welcome"); }
if (message.payload.code === "blocked") { const bump = byId("wall-bump"); bump.textContent = localError("blocked"); bump.classList.remove("show"); void bump.offsetWidth; bump.classList.add("show"); }
if (message.payload.code === "blocked") { const bump = byId("wall-bump"); bump.querySelector(".wall-bump-text").textContent = localError("blocked"); bump.classList.remove("show"); void bump.offsetWidth; bump.classList.add("show"); }
showNotice(localError(message.payload.code), "error"); feedback(false);
}
}