Клиент: audio.js синтезирует эффекты без внешних файлов (выстрел, взрыв, разрушение кирпича, победа/поражение) — вписывается в "чистый HTML/CSS/JS" стек. AudioContext разблокируется по клику "готов" (браузеры блокируют автовоспроизведение без жеста). Сервер: 3 варианта раскладки coop-карты (случайный выбор при старте матча) вместо одной фиксированной. Fix: сервер разрушал кирпичи в своей копии карты, но никогда не сообщал об этом клиенту — стена оставалась видимой навсегда, хотя сквозь неё уже можно было стрелять. Теперь game.state включает destroyed_tiles, клиент убирает соответствующий спрайт стены.
174 lines
5.2 KiB
JavaScript
174 lines
5.2 KiB
JavaScript
import { Network, ServerMessage } from './network.js';
|
|
import { ClientMessage } from './protocol.js';
|
|
import { startBattle } from './battle.js';
|
|
import { unlockAudio } from './audio.js';
|
|
|
|
const container = document.getElementById('lobby');
|
|
|
|
let net;
|
|
let myPlayerId = null;
|
|
let rooms = [];
|
|
let currentRoom = null;
|
|
let errorMessage = '';
|
|
let connected = false;
|
|
let nickname = 'cacto-' + Math.floor(Math.random() * 1000);
|
|
|
|
function roomRow(room) {
|
|
const full = room.players >= room.max_players;
|
|
return `
|
|
<li class="room-list__item">
|
|
<span class="room-list__name">${room.name}</span>
|
|
<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>
|
|
</li>`;
|
|
}
|
|
|
|
function errorBanner() {
|
|
return errorMessage ? `<p class="lobby-error">! ${errorMessage}</p>` : '';
|
|
}
|
|
|
|
function renderLobbyView() {
|
|
const dis = connected ? '' : 'disabled';
|
|
container.innerHTML = `
|
|
<div class="lobby-panel">
|
|
${errorBanner()}
|
|
${connected ? '' : '<p class="lobby-error">подключение...</p>'}
|
|
<div class="lobby-panel__row">
|
|
<label class="prompt-inline">nickname</label>
|
|
<input id="nickname-input" type="text" value="${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>
|
|
</div>
|
|
<ul class="room-list">
|
|
${rooms.length === 0 ? '<li class="room-list__empty">пока пусто — создай комнату</li>' : rooms.map(roomRow).join('')}
|
|
</ul>
|
|
</div>`;
|
|
}
|
|
|
|
function renderRoomView() {
|
|
const full = currentRoom.players.length >= 2;
|
|
let statusLine = '';
|
|
let readyBtn = '<button data-action="ready">готов</button>';
|
|
if (currentRoom.mode === 'pvp' && !full) {
|
|
statusLine = '<p class="lobby-error">жду второго игрока (1/2) — открой комнату во второй вкладке/другим человеком</p>';
|
|
readyBtn = '<button data-action="ready" disabled>готов</button>';
|
|
} else if (currentRoom.mode === 'coop' && !full) {
|
|
statusLine = '<p class="lobby-error">можно начать соло, второй игрок сможет подключиться позже</p>';
|
|
}
|
|
container.innerHTML = `
|
|
<div class="lobby-panel">
|
|
${errorBanner()}
|
|
<h3 class="prompt">room ${currentRoom.name}</h3>
|
|
<p>режим: <span class="tag">${currentRoom.mode}</span></p>
|
|
<ul class="room-players">
|
|
${currentRoom.players.map((p) => `<li>${p.nickname}${p.ready ? ' — готов' : ''}</li>`).join('')}
|
|
</ul>
|
|
${statusLine}
|
|
${readyBtn}
|
|
<button data-action="leave">выйти в лобби</button>
|
|
</div>`;
|
|
}
|
|
|
|
function render() {
|
|
if (currentRoom) {
|
|
renderRoomView();
|
|
} else {
|
|
renderLobbyView();
|
|
}
|
|
}
|
|
|
|
// Вызывается из battle.js по кнопкам экрана победы/поражения.
|
|
export function returnToLobbyContainer() {
|
|
document.getElementById('battlecity-canvas-container').style.display = 'none';
|
|
container.style.display = '';
|
|
render();
|
|
}
|
|
|
|
export function leaveRoomAndReturn() {
|
|
currentRoom = null;
|
|
net.leaveRoom();
|
|
returnToLobbyContainer();
|
|
}
|
|
|
|
container?.addEventListener('click', (e) => {
|
|
const btn = e.target.closest('[data-action]');
|
|
if (!btn) return;
|
|
errorMessage = '';
|
|
switch (btn.dataset.action) {
|
|
case 'create':
|
|
net.createRoom(btn.dataset.mode);
|
|
break;
|
|
case 'join':
|
|
net.joinRoom(btn.dataset.roomId);
|
|
break;
|
|
case 'ready':
|
|
unlockAudio();
|
|
net.send(ClientMessage.GAME_READY);
|
|
break;
|
|
case 'leave':
|
|
currentRoom = null;
|
|
net.leaveRoom();
|
|
render();
|
|
break;
|
|
case 'refresh':
|
|
net.listRooms();
|
|
break;
|
|
}
|
|
});
|
|
|
|
container?.addEventListener('change', (e) => {
|
|
if (e.target.id === 'nickname-input') {
|
|
nickname = e.target.value.trim() || nickname;
|
|
}
|
|
});
|
|
|
|
export function initLobby() {
|
|
net = new Network();
|
|
|
|
net.on('_open', () => net.hello(nickname));
|
|
net.on(ServerMessage.HELLO_ACK, (payload) => {
|
|
connected = true;
|
|
myPlayerId = payload.player_id;
|
|
net.listRooms();
|
|
});
|
|
net.on('_close', () => {
|
|
connected = false;
|
|
render();
|
|
});
|
|
|
|
net.on(ServerMessage.LOBBY_ROOMS, (payload) => {
|
|
rooms = payload.rooms;
|
|
render();
|
|
});
|
|
|
|
net.on(ServerMessage.LOBBY_ROOM_JOINED, (payload) => {
|
|
currentRoom = payload;
|
|
render();
|
|
});
|
|
|
|
net.on(ServerMessage.LOBBY_ROOM_UPDATED, (payload) => {
|
|
if (currentRoom) {
|
|
currentRoom.players = payload.players;
|
|
render();
|
|
}
|
|
});
|
|
|
|
net.on(ServerMessage.GAME_START, (payload) => {
|
|
startBattle(net, payload, myPlayerId);
|
|
});
|
|
|
|
net.on(ServerMessage.ERROR, (payload) => {
|
|
console.error('[gameserver] error:', payload);
|
|
errorMessage = payload.message;
|
|
render();
|
|
});
|
|
|
|
render();
|
|
return net;
|
|
}
|