Редизайн под новую Matrix-стилистику + RU/EN переключатель языка
Новая шапка (sticky, blur, мигающий курсор), нумерованные секции резюме с полным опытом работы и новым разделом «Образование», карточки проектов в стиле drwxr-xr-x, более яркий дождь на фоне. Добавлен i18n-модуль (frontend/js/i18n.js) с переключателем EN/RU: резюме и проекты полностью переведены, в игре переведены статичные подписи интерфейса лобби и боя (данные от сервера остаются на русском).
This commit is contained in:
+54
-15
@@ -1,6 +1,38 @@
|
||||
import { ClientMessage, ServerMessage } from './protocol.js';
|
||||
import { returnToLobbyContainer, leaveRoomAndReturn } from './lobby.js';
|
||||
import { playShot, playWallHit, playExplosion, playWin, playLose } from './audio.js';
|
||||
import { getLang } from '../i18n.js';
|
||||
|
||||
const STR = {
|
||||
ru: {
|
||||
controlsHint: 'WASD / стрелки + пробел тоже работают',
|
||||
fire: 'огонь',
|
||||
titles: { win: 'ПОБЕДА', lose: 'ПОРАЖЕНИЕ', draw: 'НИЧЬЯ' },
|
||||
rematch: 'играть ещё',
|
||||
toLobby: 'в лобби',
|
||||
},
|
||||
en: {
|
||||
controlsHint: 'WASD / arrow keys + space also work',
|
||||
fire: 'fire',
|
||||
titles: { win: 'VICTORY', lose: 'DEFEAT', draw: 'DRAW' },
|
||||
rematch: 'play again',
|
||||
toLobby: 'to lobby',
|
||||
},
|
||||
};
|
||||
|
||||
function t() {
|
||||
return STR[getLang()];
|
||||
}
|
||||
|
||||
// Регистрируем один раз на модуль: обновляем статичные подписи в уже
|
||||
// отрисованной панели управления и оверлей победы/поражения, если он открыт.
|
||||
window.addEventListener('cacto:langchange', () => {
|
||||
const hint = document.querySelector('.battle-controls__hint');
|
||||
if (hint) hint.textContent = t().controlsHint;
|
||||
const fireBtn = document.querySelector('.fire-btn');
|
||||
if (fireBtn) fireBtn.textContent = t().fire;
|
||||
if (lastOverPayload) renderOverlay(lastOverPayload);
|
||||
});
|
||||
|
||||
const TILE_PX = 32;
|
||||
// Quintus транслирует и вращает контекст сам (Sprite.render -> matrix.setContextTransform)
|
||||
@@ -282,6 +314,23 @@ function onState(payload) {
|
||||
}
|
||||
}
|
||||
|
||||
let lastOverPayload = null;
|
||||
|
||||
function renderOverlay(payload) {
|
||||
const title = t().titles[payload.result] || payload.result.toUpperCase();
|
||||
const overlay = document.getElementById('battle-result-overlay');
|
||||
overlay.innerHTML = `
|
||||
<div class="battle-result battle-result--${payload.result}">
|
||||
<h2 class="glitch" data-text="${title}">${title}</h2>
|
||||
<p>${payload.reason}</p>
|
||||
<div class="battle-result__actions">
|
||||
<button data-action="rematch">${t().rematch}</button>
|
||||
<button data-action="to-lobby">${t().toLobby}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
overlay.style.display = 'flex';
|
||||
}
|
||||
|
||||
function onOver(payload) {
|
||||
polling = false;
|
||||
if (payload.result === 'win') {
|
||||
@@ -289,19 +338,8 @@ function onOver(payload) {
|
||||
} else {
|
||||
playLose();
|
||||
}
|
||||
const titles = { win: 'ПОБЕДА', lose: 'ПОРАЖЕНИЕ', draw: 'НИЧЬЯ' };
|
||||
const title = titles[payload.result] || payload.result.toUpperCase();
|
||||
const overlay = document.getElementById('battle-result-overlay');
|
||||
overlay.innerHTML = `
|
||||
<div class="battle-result battle-result--${payload.result}">
|
||||
<h2 class="glitch" data-text="${title}">${title}</h2>
|
||||
<p>${payload.reason}</p>
|
||||
<div class="battle-result__actions">
|
||||
<button data-action="rematch">играть ещё</button>
|
||||
<button data-action="to-lobby">в лобби</button>
|
||||
</div>
|
||||
</div>`;
|
||||
overlay.style.display = 'flex';
|
||||
lastOverPayload = payload;
|
||||
renderOverlay(payload);
|
||||
}
|
||||
|
||||
function onOverlayClick(e) {
|
||||
@@ -310,6 +348,7 @@ function onOverlayClick(e) {
|
||||
const overlay = document.getElementById('battle-result-overlay');
|
||||
overlay.style.display = 'none';
|
||||
overlay.innerHTML = '';
|
||||
lastOverPayload = null;
|
||||
if (btn.dataset.action === 'rematch') {
|
||||
returnToLobbyContainer();
|
||||
} else {
|
||||
@@ -331,14 +370,14 @@ export function startBattle(networkInstance, payload, ownPlayerId) {
|
||||
<div class="battle-layout">
|
||||
<div id="battle-canvas-slot"></div>
|
||||
<div class="battle-controls">
|
||||
<p class="battle-controls__hint">WASD / стрелки + пробел тоже работают</p>
|
||||
<p class="battle-controls__hint">${t().controlsHint}</p>
|
||||
<div class="dpad">
|
||||
<button class="dpad__up" data-input="up">↑</button>
|
||||
<button class="dpad__left" data-input="left">←</button>
|
||||
<button class="dpad__right" data-input="right">→</button>
|
||||
<button class="dpad__down" data-input="down">↓</button>
|
||||
</div>
|
||||
<button class="fire-btn" data-input="fire">огонь</button>
|
||||
<button class="fire-btn" data-input="fire">${t().fire}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="battle-result-overlay"></div>`;
|
||||
|
||||
+56
-14
@@ -2,9 +2,49 @@ import { Network, ServerMessage } from './network.js';
|
||||
import { ClientMessage } from './protocol.js';
|
||||
import { startBattle } from './battle.js';
|
||||
import { unlockAudio } from './audio.js';
|
||||
import { getLang } from '../i18n.js';
|
||||
|
||||
const container = document.getElementById('lobby');
|
||||
|
||||
// Статичные подписи UI лобби переводим; данные из сервера (ники, room id,
|
||||
// текст ошибок) остаются как есть — они генерируются на сервере только на русском.
|
||||
const STR = {
|
||||
ru: {
|
||||
connecting: 'подключение...',
|
||||
join: 'войти',
|
||||
createCoop: 'создать комнату (coop)',
|
||||
createPvp: 'создать комнату (pvp)',
|
||||
refresh: 'обновить список',
|
||||
empty: 'пока пусто — создай комнату',
|
||||
ready: 'готов',
|
||||
readySuffix: ' — готов',
|
||||
waitingForSecond: 'жду второго игрока (1/2) — открой комнату во второй вкладке/другим человеком',
|
||||
soloHint: 'можно начать соло, второй игрок сможет подключиться позже',
|
||||
room: 'комната',
|
||||
mode: 'режим:',
|
||||
leave: 'выйти в лобби',
|
||||
},
|
||||
en: {
|
||||
connecting: 'connecting...',
|
||||
join: 'join',
|
||||
createCoop: 'create room (coop)',
|
||||
createPvp: 'create room (pvp)',
|
||||
refresh: 'refresh list',
|
||||
empty: 'nothing here yet — create a room',
|
||||
ready: 'ready',
|
||||
readySuffix: ' — ready',
|
||||
waitingForSecond: 'waiting for a second player (1/2) — open the room in another tab or have someone else join',
|
||||
soloHint: 'you can start solo, a second player can join later',
|
||||
room: 'room',
|
||||
mode: 'mode:',
|
||||
leave: 'leave to lobby',
|
||||
},
|
||||
};
|
||||
|
||||
function t() {
|
||||
return STR[getLang()];
|
||||
}
|
||||
|
||||
let net;
|
||||
let myPlayerId = null;
|
||||
let rooms = [];
|
||||
@@ -33,7 +73,7 @@ function roomRow(room) {
|
||||
<span class="tag">${room.mode}</span>
|
||||
<span class="room-list__count">${room.players}/${room.max_players}</span>
|
||||
<span class="room-list__state">${room.state}</span>
|
||||
<button data-action="join" data-room-id="${room.id}" ${full || room.state !== 'waiting' || !connected ? 'disabled' : ''}>войти</button>
|
||||
<button data-action="join" data-room-id="${room.id}" ${full || room.state !== 'waiting' || !connected ? 'disabled' : ''}>${t().join}</button>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
@@ -46,18 +86,18 @@ function renderLobbyView() {
|
||||
container.innerHTML = `
|
||||
<div class="lobby-panel">
|
||||
${errorBanner()}
|
||||
${connected ? '' : '<p class="lobby-error">подключение...</p>'}
|
||||
${connected ? '' : `<p class="lobby-error">${t().connecting}</p>`}
|
||||
<div class="lobby-panel__row">
|
||||
<label class="prompt-inline">nickname</label>
|
||||
<input id="nickname-input" type="text" value="${escapeHtml(nickname)}" maxlength="20" ${dis} />
|
||||
</div>
|
||||
<div class="lobby-panel__row">
|
||||
<button data-action="create" data-mode="coop" ${dis}>создать комнату (coop)</button>
|
||||
<button data-action="create" data-mode="pvp" ${dis}>создать комнату (pvp)</button>
|
||||
<button data-action="refresh" ${dis}>обновить список</button>
|
||||
<button data-action="create" data-mode="coop" ${dis}>${t().createCoop}</button>
|
||||
<button data-action="create" data-mode="pvp" ${dis}>${t().createPvp}</button>
|
||||
<button data-action="refresh" ${dis}>${t().refresh}</button>
|
||||
</div>
|
||||
<ul class="room-list">
|
||||
${rooms.length === 0 ? '<li class="room-list__empty">пока пусто — создай комнату</li>' : rooms.map(roomRow).join('')}
|
||||
${rooms.length === 0 ? `<li class="room-list__empty">${t().empty}</li>` : rooms.map(roomRow).join('')}
|
||||
</ul>
|
||||
</div>`;
|
||||
}
|
||||
@@ -65,24 +105,24 @@ function renderLobbyView() {
|
||||
function renderRoomView() {
|
||||
const full = currentRoom.players.length >= 2;
|
||||
let statusLine = '';
|
||||
let readyBtn = '<button data-action="ready">готов</button>';
|
||||
let readyBtn = `<button data-action="ready">${t().ready}</button>`;
|
||||
if (currentRoom.mode === 'pvp' && !full) {
|
||||
statusLine = '<p class="lobby-error">жду второго игрока (1/2) — открой комнату во второй вкладке/другим человеком</p>';
|
||||
readyBtn = '<button data-action="ready" disabled>готов</button>';
|
||||
statusLine = `<p class="lobby-error">${t().waitingForSecond}</p>`;
|
||||
readyBtn = `<button data-action="ready" disabled>${t().ready}</button>`;
|
||||
} else if (currentRoom.mode === 'coop' && !full) {
|
||||
statusLine = '<p class="lobby-error">можно начать соло, второй игрок сможет подключиться позже</p>';
|
||||
statusLine = `<p class="lobby-error">${t().soloHint}</p>`;
|
||||
}
|
||||
container.innerHTML = `
|
||||
<div class="lobby-panel">
|
||||
${errorBanner()}
|
||||
<h3 class="prompt">room ${currentRoom.name}</h3>
|
||||
<p>режим: <span class="tag">${currentRoom.mode}</span></p>
|
||||
<h3 class="prompt">${t().room} ${currentRoom.name}</h3>
|
||||
<p>${t().mode} <span class="tag">${currentRoom.mode}</span></p>
|
||||
<ul class="room-players">
|
||||
${currentRoom.players.map((p) => `<li>${escapeHtml(p.nickname)}${p.ready ? ' — готов' : ''}</li>`).join('')}
|
||||
${currentRoom.players.map((p) => `<li>${escapeHtml(p.nickname)}${p.ready ? t().readySuffix : ''}</li>`).join('')}
|
||||
</ul>
|
||||
${statusLine}
|
||||
${readyBtn}
|
||||
<button data-action="leave">выйти в лобби</button>
|
||||
<button data-action="leave">${t().leave}</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -180,6 +220,8 @@ export function initLobby() {
|
||||
render();
|
||||
});
|
||||
|
||||
window.addEventListener('cacto:langchange', render);
|
||||
|
||||
render();
|
||||
return net;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
// TODO: инициализация Quintus и игрового цикла рендера — этапы 4-5
|
||||
import { initLobby } from './lobby.js';
|
||||
import { initLangToggle } from '../i18n.js';
|
||||
|
||||
initLobby();
|
||||
// lobby.js/battle.js сами слушают cacto:langchange и перерисовывают свои
|
||||
// статичные подписи — здесь достаточно включить саму кнопку в шапке.
|
||||
initLangToggle();
|
||||
|
||||
Reference in New Issue
Block a user