Files
cactoz.su/frontend/js/game/lobby.js
T
cacto 2d0be9c287 Игровая логика PvP: сервер (C++) + клиент (Quintus.js)
Сервер: Game — authoritative-тик 20 Гц через uSockets timer, движение/коллизии
танков и пуль, разрушаемые кирпичные стены, respawn, победа/поражение/ничья,
технический проигрыш при дисконнекте. game.ready запускает матч, когда оба
игрока в pvp-комнате готовы.

Клиент: заведён Quintus.js (MIT, vendored) — Tank/Bullet/Wall спрайты,
рендер по снапшотам game.state, keyboard-инпут (WASD/стрелки/space),
экран результата матча.

Найден и исправлен use-after-free: колбэк onFinished_ синхронно удалял
Game изнутри его же метода — перенесено на uWS::Loop::defer.
2026-07-28 12:05:01 +05:00

153 lines
4.3 KiB
JavaScript

import { Network, ServerMessage } from './network.js';
import { ClientMessage } from './protocol.js';
import { startBattle } from './battle.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 readyBtn =
currentRoom.mode === 'pvp'
? '<button data-action="ready">готов</button>'
: '<p class="lobby-error">coop ещё не реализован (этап 5)</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>
${readyBtn}
<button data-action="leave">выйти в лобби</button>
</div>`;
}
function render() {
if (currentRoom) {
renderRoomView();
} else {
renderLobbyView();
}
}
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':
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;
}