Лобби: комнаты на сервере (C++) и UI на клиенте
Сервер: Lobby/Room/Player — create/join/leave/list_rooms, broadcast обновлений, проверка авторизации (hello) и состояния комнаты (waiting/full/in_progress). Клиент: полноценный UI лобби (создание, список, вход в комнату, выход), блокировка кнопок до установки WS-соединения. nginx: no-cache для JS/CSS (нет хеширования имён файлов).
This commit is contained in:
+90
-1
@@ -1 +1,90 @@
|
||||
/* TODO: лобби и игровое поле — этапы 3-6 */
|
||||
.lobby-panel {
|
||||
border: var(--border);
|
||||
background: var(--color-bg-raised);
|
||||
padding: 1.25rem 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.lobby-panel__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.prompt-inline {
|
||||
color: var(--color-accent-dim);
|
||||
}
|
||||
|
||||
.lobby-panel input {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-accent-dim);
|
||||
color: var(--color-fg);
|
||||
font-family: var(--font-mono);
|
||||
padding: 0.3rem 0.5rem;
|
||||
}
|
||||
|
||||
.lobby-panel button {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-accent-dim);
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
padding: 0.35rem 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lobby-panel button:hover:not(:disabled) {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 6px var(--color-accent-dim);
|
||||
}
|
||||
|
||||
.lobby-panel button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.lobby-error {
|
||||
color: var(--color-silver);
|
||||
border-left: 2px solid var(--color-accent-dim);
|
||||
padding-left: 0.6rem;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.room-list,
|
||||
.room-players {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.room-list__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
border-bottom: var(--border);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.room-list__name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.room-list__count,
|
||||
.room-list__state {
|
||||
color: var(--color-silver);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.room-list__empty {
|
||||
color: var(--color-silver);
|
||||
}
|
||||
|
||||
.room-players li::before {
|
||||
content: '- ';
|
||||
color: var(--color-accent-dim);
|
||||
}
|
||||
|
||||
+126
-11
@@ -1,21 +1,136 @@
|
||||
// TODO: полноценный UI лобби (список/создание/присоединение комнат) — этап 3.
|
||||
// Пока — проверка связки клиент <-> gameserver (hello -> lobby.list_rooms).
|
||||
import { Network, ServerMessage } from './network.js';
|
||||
|
||||
export function connectAndProbe() {
|
||||
const net = new Network();
|
||||
const container = document.getElementById('lobby');
|
||||
|
||||
net.on('_open', () => net.hello('cacto'));
|
||||
let net;
|
||||
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() {
|
||||
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}</li>`).join('')}
|
||||
</ul>
|
||||
<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 '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, () => {
|
||||
console.log('[gameserver] hello.ack received');
|
||||
connected = true;
|
||||
net.listRooms();
|
||||
});
|
||||
net.on(ServerMessage.LOBBY_ROOMS, (payload) => {
|
||||
console.log('[gameserver] rooms:', payload.rooms);
|
||||
});
|
||||
net.on(ServerMessage.ERROR, (payload) => {
|
||||
console.error('[gameserver] error:', payload);
|
||||
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.ERROR, (payload) => {
|
||||
console.error('[gameserver] error:', payload);
|
||||
errorMessage = payload.message;
|
||||
render();
|
||||
});
|
||||
|
||||
render();
|
||||
return net;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// TODO: инициализация Quintus и игрового цикла рендера — этапы 4-5
|
||||
import { connectAndProbe } from './lobby.js';
|
||||
import { initLobby } from './lobby.js';
|
||||
|
||||
connectAndProbe();
|
||||
initLobby();
|
||||
|
||||
@@ -33,6 +33,18 @@ export class Network {
|
||||
listRooms() {
|
||||
this.send(ClientMessage.LOBBY_LIST_ROOMS);
|
||||
}
|
||||
|
||||
createRoom(mode) {
|
||||
this.send(ClientMessage.LOBBY_CREATE_ROOM, { mode });
|
||||
}
|
||||
|
||||
joinRoom(roomId) {
|
||||
this.send(ClientMessage.LOBBY_JOIN_ROOM, { room_id: roomId });
|
||||
}
|
||||
|
||||
leaveRoom() {
|
||||
this.send(ClientMessage.LOBBY_LEAVE_ROOM);
|
||||
}
|
||||
}
|
||||
|
||||
export { ServerMessage };
|
||||
|
||||
Reference in New Issue
Block a user