Павел. Изменение интерфейса.

This commit is contained in:
PsiMagistr 2025-05-13 16:27:37 +03:00
parent 6d3d4ef6f6
commit 0d6d224a99
7 changed files with 999 additions and 801 deletions

99
bc.js
View File

@ -1,42 +1,50 @@
// bc.js (или server.js - ваш основной файл сервера) // bc.js (или server.js - ваш основной файл сервера)
// КОД ОСТАЕТСЯ БЕЗ ИЗМЕНЕНИЙ ОТНОСИТЕЛЬНО ПРЕДЫДУЩЕЙ ВЕРСИИ,
// ГДЕ УЖЕ БЫЛА ДОБАВЛЕНА ПЕРЕДАЧА characterKey В GameManager
const express = require('express'); const express = require('express');
const http = require('http'); const http = require('http');
const socketIo = require('socket.io'); const socketIo = require('socket.io');
const path = require('path'); const path = require('path');
// Серверные модули // Серверные модули
const GameManager = require('./server_modules/gameManager'); // GameManager будет изменен const GameManager = require('./server_modules/gameManager');
const authController = require('./server_modules/auth'); const authController = require('./server_modules/auth'); // Ваш модуль аутентификации
// const GAME_CONFIG = require('./server_modules/config'); // Не используется напрямую здесь, но может быть полезен для отладки
const hostname = 'localhost'; const hostname = 'localhost'; // или '0.0.0.0' для доступа извне
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
const io = socketIo(server); const io = socketIo(server, {
cors: {
origin: "*", // Разрешить все источники для простоты разработки. В продакшене укажите конкретный домен клиента.
methods: ["GET", "POST"]
}
});
const PORT = process.env.PORT || 3200; const PORT = process.env.PORT || 3200;
// Статическое обслуживание файлов из папки 'public'
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));
const gameManager = new GameManager(io); // GameManager будет содержать новую логику // Создание экземпляра GameManager
const gameManager = new GameManager(io);
io.on('connection', (socket) => { io.on('connection', (socket) => {
console.log('[Server] New client connected:', socket.id); console.log(`[Server BC.JS] New client connected: ${socket.id}`);
// При подключении нового клиента, отправляем ему текущий список доступных PvP игр
const availableGames = gameManager.getAvailablePvPGamesListForClient(); const availableGames = gameManager.getAvailablePvPGamesListForClient();
socket.emit('availablePvPGamesList', availableGames); socket.emit('availablePvPGamesList', availableGames);
// Обработчик запроса на обновление списка PvP игр
socket.on('requestPvPGameList', () => { socket.on('requestPvPGameList', () => {
const currentAvailableGames = gameManager.getAvailablePvPGamesListForClient(); const currentAvailableGames = gameManager.getAvailablePvPGamesListForClient();
socket.emit('availablePvPGamesList', currentAvailableGames); socket.emit('availablePvPGamesList', currentAvailableGames);
}); });
// --- Аутентификация ---
socket.on('register', async (data) => { socket.on('register', async (data) => {
console.log(`[Server BC.JS] Received 'register' event from ${socket.id} with username: ${data?.username}`); console.log(`[Server BC.JS] Received 'register' event from ${socket.id} with username: ${data?.username}`);
if (!data || typeof data.username !== 'string' || typeof data.password !== 'string') { if (!data || typeof data.username !== 'string' || typeof data.password !== 'string') {
socket.emit('registerResponse', { success: false, message: 'Некорректные данные запроса.' }); socket.emit('registerResponse', { success: false, message: 'Некорректные данные запроса для регистрации.' });
return; return;
} }
const result = await authController.registerUser(data.username, data.password); const result = await authController.registerUser(data.username, data.password);
@ -46,12 +54,13 @@ io.on('connection', (socket) => {
socket.on('login', async (data) => { socket.on('login', async (data) => {
console.log(`[Server BC.JS] Received 'login' event from ${socket.id} with username: ${data?.username}`); console.log(`[Server BC.JS] Received 'login' event from ${socket.id} with username: ${data?.username}`);
if (!data || typeof data.username !== 'string' || typeof data.password !== 'string') { if (!data || typeof data.username !== 'string' || typeof data.password !== 'string') {
socket.emit('loginResponse', { success: false, message: 'Некорректные данные запроса.' }); socket.emit('loginResponse', { success: false, message: 'Некорректные данные запроса для входа.' });
return; return;
} }
const result = await authController.loginUser(data.username, data.password); const result = await authController.loginUser(data.username, data.password);
if (result.success) { if (result.success) {
socket.userData = { userId: result.userId, username: result.username }; // Сохраняем userId // Сохраняем данные пользователя в объекте сокета для последующего использования
socket.userData = { userId: result.userId, username: result.username };
console.log(`[Server BC.JS] User ${result.username} (ID: ${result.userId}) associated with socket ${socket.id}. Welcome!`); console.log(`[Server BC.JS] User ${result.username} (ID: ${result.userId}) associated with socket ${socket.id}. Welcome!`);
} }
socket.emit('loginResponse', result); socket.emit('loginResponse', result);
@ -61,21 +70,24 @@ io.on('connection', (socket) => {
const username = socket.userData?.username || socket.id; const username = socket.userData?.username || socket.id;
console.log(`[Server BC.JS] Received 'logout' event from ${username}.`); console.log(`[Server BC.JS] Received 'logout' event from ${username}.`);
if (socket.userData) { if (socket.userData) {
gameManager.handleDisconnect(socket.id); // Обработает выход из игр // При выходе пользователя, обрабатываем его возможное участие в играх
delete socket.userData; gameManager.handleDisconnect(socket.id, socket.userData.userId); // Используем userId для более точной обработки
delete socket.userData; // Удаляем данные пользователя из сокета
console.log(`[Server BC.JS] User data cleared for ${username}.`); console.log(`[Server BC.JS] User data cleared for ${username}.`);
} }
// Можно отправить подтверждение выхода, если нужно
// socket.emit('logoutResponse', { success: true, message: 'Вы успешно вышли.' });
}); });
// --- Управление Играми ---
socket.on('createGame', (data) => { socket.on('createGame', (data) => {
if (!socket.userData) { if (!socket.userData) {
socket.emit('gameError', { message: "Ошибка: Вы не авторизованы для создания игры." }); socket.emit('gameError', { message: "Ошибка: Вы не авторизованы для создания игры." });
return; return;
} }
const mode = data?.mode || 'ai'; const mode = data?.mode || 'ai'; // 'ai' или 'pvp'
const characterKey = (data?.characterKey === 'almagest') ? 'almagest' : 'elena'; const characterKey = (data?.characterKey === 'almagest') ? 'almagest' : 'elena'; // По умолчанию Елена
console.log(`[Server BC.JS] User ${socket.userData.username} (socket: ${socket.id}) requests createGame. Mode: ${mode}, Character: ${characterKey}`); console.log(`[Server BC.JS] User ${socket.userData.username} (socket: ${socket.id}) requests createGame. Mode: ${mode}, Character: ${characterKey}`);
// Передаем socket.userData.userId в GameManager, если он нужен для идентификации пользователя между сессиями
gameManager.createGame(socket, mode, characterKey, socket.userData.userId); gameManager.createGame(socket, mode, characterKey, socket.userData.userId);
}); });
@ -86,7 +98,6 @@ io.on('connection', (socket) => {
} }
console.log(`[Server BC.JS] User ${socket.userData.username} (socket: ${socket.id}) requests joinGame for ID: ${data?.gameId}`); console.log(`[Server BC.JS] User ${socket.userData.username} (socket: ${socket.id}) requests joinGame for ID: ${data?.gameId}`);
if (data && typeof data.gameId === 'string') { if (data && typeof data.gameId === 'string') {
// Передаем socket.userData.userId
gameManager.joinGame(socket, data.gameId, socket.userData.userId); gameManager.joinGame(socket, data.gameId, socket.userData.userId);
} else { } else {
socket.emit('gameError', { message: 'Ошибка присоединения: неверный формат ID игры.' }); socket.emit('gameError', { message: 'Ошибка присоединения: неверный формат ID игры.' });
@ -100,38 +111,56 @@ io.on('connection', (socket) => {
} }
const characterKey = (data?.characterKey === 'almagest') ? 'almagest' : 'elena'; const characterKey = (data?.characterKey === 'almagest') ? 'almagest' : 'elena';
console.log(`[Server BC.JS] User ${socket.userData.username} (socket: ${socket.id}) requests findRandomGame. Preferred Character: ${characterKey}`); console.log(`[Server BC.JS] User ${socket.userData.username} (socket: ${socket.id}) requests findRandomGame. Preferred Character: ${characterKey}`);
// Передаем socket.userData.userId
gameManager.findAndJoinRandomPvPGame(socket, characterKey, socket.userData.userId); gameManager.findAndJoinRandomPvPGame(socket, characterKey, socket.userData.userId);
}); });
// --- Игровые Действия ---
socket.on('playerAction', (data) => { socket.on('playerAction', (data) => {
if (!socket.userData) return; if (!socket.userData) {
// Если пользователь не авторизован, но пытается совершить действие (маловероятно при правильной логике клиента)
socket.emit('gameError', { message: "Ошибка: Вы не авторизованы для совершения этого действия." });
return;
}
// GameManager сам проверит, принадлежит ли этот сокет к активной игре
gameManager.handlePlayerAction(socket.id, data); gameManager.handlePlayerAction(socket.id, data);
}); });
socket.on('requestRestart', (data) => { // Обработчик 'requestRestart' удален, так как эта функциональность заменена на "возврат в меню"
if (!socket.userData) {
socket.emit('gameError', { message: "Ошибка: Вы не авторизованы для запроса рестарта." });
return;
}
console.log(`[Server BC.JS] User ${socket.userData.username} (socket: ${socket.id}) requests restart for game ID: ${data?.gameId}`);
if (data && typeof data.gameId === 'string') {
gameManager.requestRestart(socket.id, data.gameId);
} else {
socket.emit('gameError', { message: 'Ошибка рестарта: неверный формат ID игры.' });
}
});
// --- Отключение Клиента ---
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
const username = socket.userData?.username || socket.id; const username = socket.userData?.username || socket.id;
console.log(`[Server] Client ${username} disconnected. Reason: ${reason}. Socket ID: ${socket.id}`); console.log(`[Server BC.JS] Client ${username} disconnected. Reason: ${reason}. Socket ID: ${socket.id}`);
// Передаем userId, если он есть, для более точной обработки в GameManager // Передаем userId, если он есть, для более точной обработки в GameManager
// (например, для удаления его ожидающих игр или корректного завершения активной игры)
const userId = socket.userData?.userId; const userId = socket.userData?.userId;
gameManager.handleDisconnect(socket.id, userId); gameManager.handleDisconnect(socket.id, userId);
// socket.userData автоматически очистится для этого объекта socket // socket.userData автоматически очистится для этого объекта socket при его удалении из io.sockets
}); });
// Для отладки: вывод списка активных игр каждые N секунд
// setInterval(() => {
// console.log("--- Active Games ---");
// const activeGames = gameManager.getActiveGamesList();
// if (activeGames.length > 0) {
// activeGames.forEach(game => {
// console.log(`ID: ${game.id}, Mode: ${game.mode}, Players: ${game.playerCount}, GameOver: ${game.isGameOver}, P1: ${game.playerSlot}, P2: ${game.opponentSlot}, Owner: ${game.ownerUserId}, Pending: ${game.pending}`);
// });
// } else {
// console.log("No active games.");
// }
// console.log("--- Pending PvP Games IDs ---");
// console.log(gameManager.pendingPvPGames.map(id => id.substring(0,8)));
// console.log("--- User to Pending Game Map ---");
// console.log(gameManager.userToPendingGame);
// console.log("---------------------");
// }, 30000); // Каждые 30 секунд
}); });
server.listen(PORT, hostname, () => { server.listen(PORT, hostname, () => {
console.log(`Server listening on http://${hostname}:${PORT}`); console.log(`==== Medieval Clash Server ====`);
console.log(` Listening on http://${hostname}:${PORT}`);
console.log(` Public files served from: ${path.join(__dirname, 'public')}`);
console.log(` Waiting for connections...`);
console.log(`===============================`);
}); });

View File

@ -9,196 +9,6 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=MedievalSharp&family=Roboto:wght@300;400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=MedievalSharp&family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
/* --- Стили для элементов настройки игры и аутентификации --- */
.auth-game-setup-wrapper {
width: 100%;
max-width: 700px;
margin: 20px auto;
padding: 25px 30px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 10px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5);
color: var(--text-light);
text-align: center;
}
/* ... (остальные стили .auth-game-setup-wrapper как были) ... */
.auth-game-setup-wrapper h2,
.auth-game-setup-wrapper h3 {
font-family: var(--font-fancy);
color: var(--text-heading);
margin-bottom: 1em;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding-bottom: 0.5em;
}
.auth-game-setup-wrapper h3 {
font-size: 1.2em;
margin-top: 1.5em;
}
.auth-game-setup-wrapper button,
#auth-section form button {
font-family: var(--font-main);
background: var(--button-bg);
color: var(--button-text);
border: 1px solid rgba(0,0,0,0.3);
border-radius: 6px;
padding: 10px 18px;
margin: 8px 5px;
cursor: pointer;
transition: all 0.15s ease;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.auth-game-setup-wrapper button:hover:enabled,
#auth-section form button:hover:enabled {
background: var(--button-hover-bg);
transform: translateY(-2px) scale(1.02);
box-shadow: 0 4px 8px rgba(0,0,0,0.4);
}
.auth-game-setup-wrapper button:active:enabled,
#auth-section form button:active:enabled {
transform: translateY(0px) scale(1);
box-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.auth-game-setup-wrapper button:disabled,
#auth-section form button:disabled {
background: var(--button-disabled-bg);
color: var(--button-disabled-text);
cursor: not-allowed;
opacity: 0.7;
}
.auth-game-setup-wrapper input[type="text"],
#auth-section input[type="text"],
#auth-section input[type="password"] {
padding: 10px;
border-radius: 5px;
border: 1px solid var(--panel-border);
background-color: var(--bar-bg);
color: var(--text-light);
margin: 5px 5px 10px 5px;
font-size: 0.9em;
width: calc(100% - 22px); /* Учитываем padding и border */
max-width: 300px; /* Ограничиваем ширину инпутов */
box-sizing: border-box;
}
#available-games-list {
margin-top: 20px;
text-align: left;
max-height: 250px;
overflow-y: auto;
padding: 10px 15px;
background-color: rgba(0,0,0,0.25);
border: 1px solid var(--log-border);
border-radius: 6px;
}
#available-games-list ul { list-style: none; padding: 0; }
#available-games-list li {
padding: 10px;
border-bottom: 1px solid rgba(var(--log-border), 0.7);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9em;
}
#available-games-list li:last-child { border-bottom: none; }
#available-games-list li button {
padding: 6px 10px;
font-size: 0.8em;
margin-left: 10px;
}
#status-container { /* Для game-status-message и auth-message */
min-height: 2.5em; /* Чтобы не прыгал layout */
margin-bottom: 15px;
}
#game-status-message,
#auth-message {
color: var(--turn-color);
font-weight: bold;
font-size: 1.1em;
padding: 5px;
background-color: rgba(0,0,0,0.1);
border-radius: 4px;
display: block; /* Чтобы занимал всю ширину и не мешал */
margin-bottom: 5px; /* Небольшой отступ между сообщениями, если оба видны */
}
#auth-message.success { color: var(--heal-color, green); }
#auth-message.error { color: var(--damage-color, red); }
#auth-section form {
margin-bottom: 20px;
}
#user-info {
padding: 10px;
background-color: rgba(255,255,255,0.05);
border-radius: 5px;
margin-bottom: 20px;
}
#user-info p {
margin: 0 0 10px 0;
font-size: 1.1em;
}
#logout-button {
background: linear-gradient(145deg, #8c3a3a, #6b2b2b) !important; /* Красный для выхода */
}
#logout-button:hover {
background: linear-gradient(145deg, #a04040, #8c3a3a) !important;
}
/* --- Стили для выбора персонажа --- */
.character-selection {
margin-top: 15px;
margin-bottom: 15px;
padding: 15px;
background-color: rgba(0,0,0,0.2);
border-radius: 6px;
border: 1px solid rgba(var(--panel-border), 0.5);
}
.character-selection h4 {
font-size: 1.1em;
color: var(--text-muted);
margin-bottom: 10px;
border: none; /* Убираем нижнюю границу */
padding: 0;
}
.character-selection label {
display: inline-block;
margin: 0 15px;
cursor: pointer;
font-size: 1.05em;
padding: 5px 10px;
border-radius: 4px;
transition: background-color 0.2s ease, color 0.2s ease;
}
.character-selection input[type="radio"] {
display: none; /* Скрываем стандартные радиокнопки */
}
.character-selection input[type="radio"]:checked + label {
background-color: var(--accent-player); /* Цвет Елены для выбранного */
color: #fff;
font-weight: bold;
box-shadow: 0 0 8px rgba(108, 149, 255, 0.5);
}
/* Стилизация лейбла Альмагест при выборе */
.character-selection input[type="radio"][value="almagest"]:checked + label {
background-color: var(--accent-opponent); /* Цвет Альмагест (как у Баларда) */
box-shadow: 0 0 8px rgba(255, 108, 108, 0.5);
}
.character-selection label:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.character-selection i { /* Иконки в лейблах */
margin-right: 8px;
vertical-align: middle; /* Лучше выравнивает иконку и текст */
}
/* Цвета иконок в лейблах */
label[for="char-elena"] i { color: var(--accent-player); }
label[for="char-almagest"] i { color: var(--accent-opponent); }
</style>
</head> </head>
<body> <body>
@ -233,7 +43,7 @@
<div id="game-setup" style="display: none;"> <!-- Секция Настройки Игры (после логина) --> <div id="game-setup" style="display: none;"> <!-- Секция Настройки Игры (после логина) -->
<h2>Настройка Игры</h2> <h2>Настройка Игры</h2>
<div> <div>
<button id="create-ai-game">Играть против AI (Балард)</button> <!-- Уточнили против кого AI --> <button id="create-ai-game">Играть против AI (Балард)</button>
</div> </div>
<hr style="margin: 15px 0;"> <hr style="margin: 15px 0;">
<div> <div>
@ -246,7 +56,7 @@
<label for="char-elena"><i class="fas fa-hat-wizard"></i> Елена</label> <label for="char-elena"><i class="fas fa-hat-wizard"></i> Елена</label>
<input type="radio" id="char-almagest" name="pvp-character" value="almagest"> <input type="radio" id="char-almagest" name="pvp-character" value="almagest">
<label for="char-almagest"><i class="fas fa-staff-aesculapius"></i> Альмагест</label> <!-- Иконка посоха --> <label for="char-almagest"><i class="fas fa-staff-aesculapius"></i> Альмагест</label>
</div> </div>
<!-- === Конец блока выбора персонажа === --> <!-- === Конец блока выбора персонажа === -->
@ -267,7 +77,6 @@
<div class="game-wrapper" style="display: none;"> <!-- Игровая арена, изначально скрыта --> <div class="game-wrapper" style="display: none;"> <!-- Игровая арена, изначально скрыта -->
<header class="game-header"> <header class="game-header">
<!-- Заголовок будет обновляться из ui.js -->
<h1><span class="title-player">Игрок 1</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="title-opponent">Игрок 2</span></h1> <h1><span class="title-player">Игрок 1</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="title-opponent">Игрок 2</span></h1>
</header> </header>
<main class="battle-arena-container"> <main class="battle-arena-container">
@ -276,10 +85,8 @@
<section id="player-panel" class="fighter-panel"> <section id="player-panel" class="fighter-panel">
<div class="panel-header"> <div class="panel-header">
<div class="character-visual"> <div class="character-visual">
<!-- Аватар будет обновляться из ui.js -->
<img src="images/elena_avatar.webp" alt="Аватар игрока 1" class="avatar-image player-avatar"> <img src="images/elena_avatar.webp" alt="Аватар игрока 1" class="avatar-image player-avatar">
</div> </div>
<!-- Имя будет обновляться из ui.js -->
<h2 id="player-name" class="fighter-name"> <h2 id="player-name" class="fighter-name">
<i class="fas fa-hat-wizard icon-player"></i> Елена <i class="fas fa-hat-wizard icon-player"></i> Елена
</h2> </h2>
@ -294,7 +101,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Тип ресурса (mana/stamina/dark-energy) и иконка будут обновляться из ui.js -->
<div class="stat-bar-container resource mana"> <div class="stat-bar-container resource mana">
<div class="bar-icon"><i class="fas fa-flask"></i></div> <div class="bar-icon"><i class="fas fa-flask"></i></div>
<div class="bar-wrapper"> <div class="bar-wrapper">
@ -323,7 +129,6 @@
</section> </section>
<section id="controls-panel" class="controls-panel-new"> <section id="controls-panel" class="controls-panel-new">
<!-- Индикатор хода будет обновляться из ui.js -->
<h3 id="turn-indicator">Ход: Игрок 1</h3> <h3 id="turn-indicator">Ход: Игрок 1</h3>
<div class="controls-layout"> <div class="controls-layout">
<div class="control-group basic-actions"> <div class="control-group basic-actions">
@ -332,7 +137,6 @@
</div> </div>
<div class="control-group ability-list"> <div class="control-group ability-list">
<h4><i class="fas fa-book-sparkles"></i> Способности</h4> <h4><i class="fas fa-book-sparkles"></i> Способности</h4>
<!-- Способности будут загружены из client.js -->
<div id="abilities-grid" class="abilities-grid"> <div id="abilities-grid" class="abilities-grid">
<p class="placeholder-text">Загрузка способностей...</p> <p class="placeholder-text">Загрузка способностей...</p>
</div> </div>
@ -346,10 +150,8 @@
<section id="opponent-panel" class="fighter-panel"> <section id="opponent-panel" class="fighter-panel">
<div class="panel-header"> <div class="panel-header">
<div class="character-visual"> <div class="character-visual">
<!-- Аватар будет обновляться из ui.js -->
<img src="images/balard_avatar.jpg" alt="Аватар игрока 2" class="avatar-image opponent-avatar"> <img src="images/balard_avatar.jpg" alt="Аватар игрока 2" class="avatar-image opponent-avatar">
</div> </div>
<!-- Имя будет обновляться из ui.js -->
<h2 id="opponent-name" class="fighter-name"> <h2 id="opponent-name" class="fighter-name">
<i class="fas fa-khanda icon-opponent"></i> Балард <i class="fas fa-khanda icon-opponent"></i> Балард
</h2> </h2>
@ -364,7 +166,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Тип ресурса и иконка будут обновляться из ui.js -->
<div class="stat-bar-container resource stamina"> <div class="stat-bar-container resource stamina">
<div class="bar-icon"><i class="fas fa-fire-alt"></i></div> <div class="bar-icon"><i class="fas fa-fire-alt"></i></div>
<div class="bar-wrapper"> <div class="bar-wrapper">
@ -405,13 +206,16 @@
<div id="game-over-screen" class="modal hidden"> <div id="game-over-screen" class="modal hidden">
<div class="modal-content"> <div class="modal-content">
<h2 id="result-message">Победа!</h2> <h2 id="result-message">Победа!</h2>
<button id="restart-game-button"><i class="fas fa-redo"></i> Новая Игра / Готов к рестарту</button> <!-- ИЗМЕНЕНА КНОПКА ЗДЕСЬ: добавлен class="modal-action-button" -->
<button id="return-to-menu-button" class="modal-action-button">
<i class="fas fa-arrow-left"></i> В меню выбора игры
</button>
</div> </div>
</div> </div>
</div> <!-- Конец .game-wrapper --> </div> <!-- Конец .game-wrapper -->
<script src="/socket.io/socket.io.js"></script> <script src="/socket.io/socket.io.js"></script>
<script src="./js/ui.js"></script> <!-- ui.js должен быть до client.js --> <script src="./js/ui.js"></script>
<script src="./js/client.js"></script> <script src="./js/client.js"></script>
</body> </body>
</html> </html>

View File

@ -7,14 +7,14 @@ document.addEventListener('DOMContentLoaded', () => {
// --- Состояние клиента --- // --- Состояние клиента ---
let currentGameState = null; let currentGameState = null;
let myPlayerId = null; // Технический ID слота ('player' или 'opponent') let myPlayerId = null; // Технический ID слота, который занимает ЭТОТ клиент ('player' или 'opponent')
let myCharacterKey = null; // Ключ моего персонажа ('elena' или 'almagest') let myCharacterKey = null;
let opponentCharacterKey = null; // Ключ персонажа оппонента let opponentCharacterKey = null;
let currentGameId = null; let currentGameId = null;
let playerBaseStatsServer = null; // Статы персонажа, которым УПРАВЛЯЕТ этот клиент let playerBaseStatsServer = null; // Статы персонажа, которым УПРАВЛЯЕТ этот клиент (приходят от сервера как data.playerBaseStats)
let opponentBaseStatsServer = null; // Статы персонажа-противника этого клиента let opponentBaseStatsServer = null; // Статы персонажа-оппонента этого клиента (приходят от сервера как data.opponentBaseStats)
let playerAbilitiesServer = null; // Способности персонажа, которым УПРАВЛЯЕТ этот клиент let playerAbilitiesServer = null;
let opponentAbilitiesServer = null; // Способности персонажа-противника let opponentAbilitiesServer = null;
let isLoggedIn = false; let isLoggedIn = false;
let loggedInUsername = ''; let loggedInUsername = '';
@ -42,7 +42,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Игровая Арена // Игровая Арена
const gameWrapper = document.querySelector('.game-wrapper'); const gameWrapper = document.querySelector('.game-wrapper');
const attackButton = document.getElementById('button-attack'); const attackButton = document.getElementById('button-attack');
const restartGameButton = document.getElementById('restart-game-button'); const returnToMenuButton = document.getElementById('return-to-menu-button');
const gameOverScreen = document.getElementById('game-over-screen'); const gameOverScreen = document.getElementById('game-over-screen');
console.log('Client.js DOMContentLoaded. Initializing elements...'); console.log('Client.js DOMContentLoaded. Initializing elements...');
@ -91,11 +91,26 @@ document.addEventListener('DOMContentLoaded', () => {
function hideGameOverModal() { function hideGameOverModal() {
const hiddenClass = (window.GAME_CONFIG && window.GAME_CONFIG.CSS_CLASS_HIDDEN) ? window.GAME_CONFIG.CSS_CLASS_HIDDEN : 'hidden'; const hiddenClass = (window.GAME_CONFIG && window.GAME_CONFIG.CSS_CLASS_HIDDEN) ? window.GAME_CONFIG.CSS_CLASS_HIDDEN : 'hidden';
if (gameOverScreen && !gameOverScreen.classList.contains(hiddenClass)) { if (gameOverScreen && !gameOverScreen.classList.contains(hiddenClass)) {
console.log('[Client.js DEBUG] Hiding GameOver Modal.');
gameOverScreen.classList.add(hiddenClass); gameOverScreen.classList.add(hiddenClass);
if (window.gameUI && gameUI.uiElements && gameUI.uiElements.gameOver && gameUI.uiElements.gameOver.modalContent) { if (window.gameUI && gameUI.uiElements && gameUI.uiElements.gameOver && gameUI.uiElements.gameOver.modalContent) {
gameUI.uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)'; gameUI.uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)';
gameUI.uiElements.gameOver.modalContent.style.opacity = '0'; gameUI.uiElements.gameOver.modalContent.style.opacity = '0';
} }
if (window.gameUI && window.gameUI.uiElements && window.gameUI.uiElements.opponent && window.gameUI.uiElements.opponent.panel) {
const opponentPanel = window.gameUI.uiElements.opponent.panel;
if (opponentPanel.classList.contains('dissolving')) {
console.log('[Client.js DEBUG] Removing .dissolving from opponent panel during hideGameOverModal.');
opponentPanel.classList.remove('dissolving');
const originalTransition = opponentPanel.style.transition;
opponentPanel.style.transition = 'none';
opponentPanel.style.opacity = '1';
opponentPanel.style.transform = 'scale(1) translateY(0)';
requestAnimationFrame(() => {
opponentPanel.style.transition = originalTransition || '';
});
}
}
} }
} }
@ -162,6 +177,9 @@ document.addEventListener('DOMContentLoaded', () => {
loggedInUsername = ''; loggedInUsername = '';
currentGameId = null; currentGameState = null; myPlayerId = null; currentGameId = null; currentGameState = null; myPlayerId = null;
myCharacterKey = null; opponentCharacterKey = null; myCharacterKey = null; opponentCharacterKey = null;
playerBaseStatsServer = null; opponentBaseStatsServer = null;
playerAbilitiesServer = null; opponentAbilitiesServer = null;
window.gameState = null; window.gameData = null; window.myPlayerId = null;
showAuthScreen(); showAuthScreen();
setGameStatusMessage("Вы вышли из системы."); setGameStatusMessage("Вы вышли из системы.");
}); });
@ -227,24 +245,32 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
if (restartGameButton) { if (returnToMenuButton) {
restartGameButton.addEventListener('click', () => { returnToMenuButton.addEventListener('click', () => {
if (currentGameId && currentGameState && currentGameState.isGameOver && isLoggedIn) { if (!isLoggedIn) {
socket.emit('requestRestart', { gameId: currentGameId });
setGameStatusMessage("Запрос на рестарт отправлен...");
restartGameButton.disabled = true;
} else {
if (!currentGameId && isLoggedIn) {
alert("Ошибка: ID текущей игры не определен. Невозможно запросить рестарт.");
showGameSelectionScreen(loggedInUsername);
} else if (!isLoggedIn) {
showAuthScreen(); showAuthScreen();
return;
} }
}
console.log('[Client] Return to menu button clicked.');
currentGameId = null;
currentGameState = null;
myPlayerId = null;
myCharacterKey = null;
opponentCharacterKey = null;
playerBaseStatsServer = null;
opponentBaseStatsServer = null;
playerAbilitiesServer = null;
opponentAbilitiesServer = null;
window.gameState = null;
window.gameData = null;
window.myPlayerId = null;
showGameSelectionScreen(loggedInUsername);
}); });
} }
// --- Функции для UI игры ---
function initializeAbilityButtons() { function initializeAbilityButtons() {
const abilitiesGrid = document.getElementById('abilities-grid'); const abilitiesGrid = document.getElementById('abilities-grid');
if (!abilitiesGrid || !window.gameUI || !window.GAME_CONFIG) { if (!abilitiesGrid || !window.gameUI || !window.GAME_CONFIG) {
@ -254,8 +280,8 @@ document.addEventListener('DOMContentLoaded', () => {
abilitiesGrid.innerHTML = ''; abilitiesGrid.innerHTML = '';
const config = window.GAME_CONFIG; const config = window.GAME_CONFIG;
const abilitiesToDisplay = playerAbilitiesServer; const abilitiesToDisplay = playerAbilitiesServer; // Используем данные, сохраненные при gameStarted
const baseStatsForResource = playerBaseStatsServer; const baseStatsForResource = playerBaseStatsServer; // Используем данные, сохраненные при gameStarted
if (!abilitiesToDisplay || abilitiesToDisplay.length === 0 || !baseStatsForResource) { if (!abilitiesToDisplay || abilitiesToDisplay.length === 0 || !baseStatsForResource) {
abilitiesGrid.innerHTML = '<p class="placeholder-text">Нет доступных способностей.</p>'; abilitiesGrid.innerHTML = '<p class="placeholder-text">Нет доступных способностей.</p>';
@ -271,7 +297,7 @@ document.addEventListener('DOMContentLoaded', () => {
let descriptionText = ability.description; let descriptionText = ability.description;
if (typeof ability.descriptionFunction === 'function') { if (typeof ability.descriptionFunction === 'function') {
const targetStatsForDesc = opponentBaseStatsServer; const targetStatsForDesc = opponentBaseStatsServer; // Используем данные, сохраненные при gameStarted
descriptionText = ability.descriptionFunction(config, targetStatsForDesc); descriptionText = ability.descriptionFunction(config, targetStatsForDesc);
} }
@ -336,6 +362,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (!isLoggedIn) { if (!isLoggedIn) {
showAuthScreen(); showAuthScreen();
} else { } else {
console.log(`[Client] Reconnected as ${loggedInUsername}. Requesting state or showing game selection.`);
showGameSelectionScreen(loggedInUsername); showGameSelectionScreen(loggedInUsername);
} }
}); });
@ -343,15 +370,6 @@ document.addEventListener('DOMContentLoaded', () => {
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
console.log('[Client] Disconnected from server:', reason); console.log('[Client] Disconnected from server:', reason);
setGameStatusMessage(`Отключено от сервера: ${reason}. Попробуйте обновить страницу.`, true); setGameStatusMessage(`Отключено от сервера: ${reason}. Попробуйте обновить страницу.`, true);
if (currentGameId) {
currentGameState = null; currentGameId = null; myPlayerId = null;
myCharacterKey = null; opponentCharacterKey = null;
if (isLoggedIn && gameWrapper && gameWrapper.style.display !== 'none') {
showGameSelectionScreen(loggedInUsername);
} else if (!isLoggedIn){
showAuthScreen();
}
}
hideGameOverModal(); hideGameOverModal();
}); });
@ -373,7 +391,7 @@ document.addEventListener('DOMContentLoaded', () => {
socket.on('gameCreated', (data) => { socket.on('gameCreated', (data) => {
if (!isLoggedIn) return; if (!isLoggedIn) return;
currentGameId = data.gameId; currentGameId = data.gameId;
myPlayerId = data.yourPlayerId; myPlayerId = data.yourPlayerId; // Запоминаем наш технический ID слота
console.log(`[Client] Game created/joined: ${currentGameId}, Mode: ${data.mode}, You (${loggedInUsername}) are in slot: ${myPlayerId}`); console.log(`[Client] Game created/joined: ${currentGameId}, Mode: ${data.mode}, You (${loggedInUsername}) are in slot: ${myPlayerId}`);
if (data.mode === 'pvp') { if (data.mode === 'pvp') {
if (gameIdInput) gameIdInput.value = currentGameId; if (gameIdInput) gameIdInput.value = currentGameId;
@ -386,18 +404,39 @@ document.addEventListener('DOMContentLoaded', () => {
socket.on('gameStarted', (data) => { socket.on('gameStarted', (data) => {
if (!isLoggedIn) return; if (!isLoggedIn) return;
console.log('[Client] Event "gameStarted" received:', data); console.log('[Client] Event "gameStarted" received:', data);
if (window.gameUI && window.gameUI.uiElements && window.gameUI.uiElements.opponent && window.gameUI.uiElements.opponent.panel) {
const opponentPanel = window.gameUI.uiElements.opponent.panel;
opponentPanel.classList.remove('dissolving');
const originalTransition = opponentPanel.style.transition;
opponentPanel.style.transition = 'none';
opponentPanel.style.opacity = '1';
opponentPanel.style.transform = 'scale(1) translateY(0)';
requestAnimationFrame(() => {
opponentPanel.style.transition = originalTransition || '';
});
console.log('[Client RESTART FIX Improved] Opponent panel styles explicitly reset for new game.');
}
currentGameId = data.gameId; currentGameId = data.gameId;
myPlayerId = data.yourPlayerId; myPlayerId = data.yourPlayerId; // Сервер присылает ID слота, который занимает ЭТОТ клиент
currentGameState = data.initialGameState; currentGameState = data.initialGameState;
// Сервер присылает playerBaseStats и opponentBaseStats ОТНОСИТЕЛЬНО этого клиента
// То есть, data.playerBaseStats - это статы персонажа, которым управляет этот клиент
// data.opponentBaseStats - это статы персонажа-оппонента для этого клиента
playerBaseStatsServer = data.playerBaseStats; playerBaseStatsServer = data.playerBaseStats;
opponentBaseStatsServer = data.opponentBaseStats; opponentBaseStatsServer = data.opponentBaseStats;
playerAbilitiesServer = data.playerAbilities; playerAbilitiesServer = data.playerAbilities;
opponentAbilitiesServer = data.opponentAbilities; opponentAbilitiesServer = data.opponentAbilities;
myCharacterKey = playerBaseStatsServer?.characterKey; myCharacterKey = playerBaseStatsServer?.characterKey; // Ключ персонажа этого клиента
opponentCharacterKey = opponentBaseStatsServer?.characterKey; opponentCharacterKey = opponentBaseStatsServer?.characterKey; // Ключ персонажа оппонента этого клиента
console.log(`[Client] Game started! My Slot ID: ${myPlayerId}, My Character: ${myCharacterKey}, Opponent Character: ${opponentCharacterKey}`);
console.log(`[Client gameStarted] My Slot ID (technical): ${myPlayerId}`);
console.log(`[Client gameStarted] My Character: ${myCharacterKey} (Name: ${playerBaseStatsServer?.name})`);
console.log(`[Client gameStarted] Opponent Character: ${opponentCharacterKey} (Name: ${opponentBaseStatsServer?.name})`);
if (data.clientConfig) { if (data.clientConfig) {
window.GAME_CONFIG = { ...data.clientConfig }; window.GAME_CONFIG = { ...data.clientConfig };
@ -405,14 +444,15 @@ document.addEventListener('DOMContentLoaded', () => {
window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' }; window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' };
} }
// Глобальные переменные для ui.js
window.gameState = currentGameState; window.gameState = currentGameState;
window.gameData = { window.gameData = { // Эти данные используются в ui.js для отображения панелей
playerBaseStats: playerBaseStatsServer, playerBaseStats: playerBaseStatsServer, // Статы "моего" персонажа
opponentBaseStats: opponentBaseStatsServer, opponentBaseStats: opponentBaseStatsServer, // Статы "моего оппонента"
playerAbilities: playerAbilitiesServer, playerAbilities: playerAbilitiesServer, // Способности "моего" персонажа
opponentAbilities: opponentAbilitiesServer opponentAbilities: opponentAbilitiesServer // Способности "моего оппонента"
}; };
window.myPlayerId = myPlayerId; window.myPlayerId = myPlayerId; // Технический ID слота этого клиента
showGameScreen(); showGameScreen();
initializeAbilityButtons(); initializeAbilityButtons();
@ -423,14 +463,17 @@ document.addEventListener('DOMContentLoaded', () => {
if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) { if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) {
data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type)); data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type));
} }
requestAnimationFrame(() => {
if (window.gameUI && typeof gameUI.updateUI === 'function') { if (window.gameUI && typeof gameUI.updateUI === 'function') {
console.log('[Client] Calling gameUI.updateUI() after style reset and rAF in gameStarted.');
gameUI.updateUI(); gameUI.updateUI();
} }
});
hideGameOverModal(); hideGameOverModal();
if (restartGameButton) { if (returnToMenuButton) {
restartGameButton.disabled = true; returnToMenuButton.disabled = true;
restartGameButton.dataset.gameIdForRestart = '';
} }
setGameStatusMessage(""); setGameStatusMessage("");
}); });
@ -438,7 +481,7 @@ document.addEventListener('DOMContentLoaded', () => {
socket.on('gameStateUpdate', (data) => { socket.on('gameStateUpdate', (data) => {
if (!isLoggedIn || !currentGameId) return; if (!isLoggedIn || !currentGameId) return;
currentGameState = data.gameState; currentGameState = data.gameState;
window.gameState = currentGameState; window.gameState = currentGameState; // ui.js использует это для обновления
if (window.gameUI && typeof gameUI.updateUI === 'function') { if (window.gameUI && typeof gameUI.updateUI === 'function') {
gameUI.updateUI(); gameUI.updateUI();
@ -457,23 +500,35 @@ document.addEventListener('DOMContentLoaded', () => {
socket.on('gameOver', (data) => { socket.on('gameOver', (data) => {
if (!isLoggedIn || !currentGameId) return; if (!isLoggedIn || !currentGameId) return;
console.log('[Client] Game over:', data);
console.log(`[Client gameOver] Received. My technical slot ID (myPlayerId): ${myPlayerId}, Winner's slot ID from server (data.winnerId): ${data.winnerId}`);
const playerWon = data.winnerId === myPlayerId; // Определяем, выиграл ли ЭТОТ клиент
console.log(`[Client gameOver] Calculated playerWon for this client: ${playerWon}`);
currentGameState = data.finalGameState; currentGameState = data.finalGameState;
window.gameState = currentGameState; window.gameState = currentGameState;
// Логи для отладки имен, которые будут использоваться в ui.js
if (window.gameData) {
console.log(`[Client gameOver] For ui.js, myName will be: ${window.gameData.playerBaseStats?.name}, opponentName will be: ${window.gameData.opponentBaseStats?.name}`);
}
if (window.gameUI && typeof gameUI.updateUI === 'function') gameUI.updateUI(); if (window.gameUI && typeof gameUI.updateUI === 'function') gameUI.updateUI();
if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) { if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) {
data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type)); data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type));
} }
if (window.gameUI && typeof gameUI.showGameOver === 'function') { if (window.gameUI && typeof gameUI.showGameOver === 'function') {
const playerWon = data.winnerId === myPlayerId; // opponentCharacterKeyFromClient передается, чтобы ui.js знал, какой персонаж был оппонентом
gameUI.showGameOver(playerWon, data.reason); // и мог применить, например, анимацию .dissolving к правильному типу оппонента (Балард/Альмагест)
if (restartGameButton) { const opponentKeyForModal = window.gameData?.opponentBaseStats?.characterKey;
restartGameButton.disabled = false; gameUI.showGameOver(playerWon, data.reason, opponentKeyForModal);
restartGameButton.dataset.gameIdForRestart = currentGameId;
if (returnToMenuButton) {
returnToMenuButton.disabled = false;
} }
} }
setGameStatusMessage("Игра окончена. " + (data.winnerId === myPlayerId ? "Вы победили!" : "Вы проиграли.")); setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли."));
}); });
socket.on('waitingForOpponent', () => { socket.on('waitingForOpponent', () => {
@ -485,37 +540,18 @@ document.addEventListener('DOMContentLoaded', () => {
if (!isLoggedIn || !currentGameId) return; if (!isLoggedIn || !currentGameId) return;
const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system'; const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system';
if (window.gameUI && typeof gameUI.addToLog === 'function') { if (window.gameUI && typeof gameUI.addToLog === 'function') {
gameUI.addToLog("Противник отключился.", systemLogType); gameUI.addToLog(`Противник (${data.disconnectedCharacterName || 'Игрок'}) отключился.`, systemLogType);
} }
if (currentGameState && !currentGameState.isGameOver) { if (currentGameState && !currentGameState.isGameOver) {
setGameStatusMessage("Противник отключился. Игра завершена. Вы можете начать новую.", true); setGameStatusMessage("Противник отключился. Игра может быть завершена сервером.", true);
} // Сервер должен прислать 'gameOver', если игра действительно завершается
});
socket.on('turnNotification', (data) => {
// console.log("[Client] Turn notification for slot:", data.currentTurn, "My Slot ID is:", myPlayerId);
});
socket.on('waitingForRestartVote', (data) => {
if (!isLoggedIn || !currentGameId) return;
const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system';
const voterName = data.voterCharacterName || 'Игрок';
if (window.gameUI && typeof gameUI.addToLog === 'function') {
gameUI.addToLog(
`${voterName} (${data.voterRole}) проголосовал(а) за рестарт. Нужно еще ${data.votesNeeded} голосов.`,
systemLogType
);
}
setGameStatusMessage(`Игрок ${voterName} проголосовал за рестарт. Ожидание вашего решения или решения другого игрока.`);
if (restartGameButton && currentGameState?.isGameOver) {
restartGameButton.disabled = false;
} }
}); });
socket.on('gameError', (data) => { socket.on('gameError', (data) => {
console.error('[Client] Server error:', data.message); console.error('[Client] Server error:', data.message);
const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system'; const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system';
if (isLoggedIn && window.gameUI && typeof gameUI.addToLog === 'function' && currentGameId && currentGameState && !currentGameState.isGameOver) { if (isLoggedIn && currentGameId && currentGameState && !currentGameState.isGameOver && window.gameUI && typeof gameUI.addToLog === 'function') {
gameUI.addToLog(`Ошибка: ${data.message}`, systemLogType); gameUI.addToLog(`Ошибка: ${data.message}`, systemLogType);
} }
setGameStatusMessage(`Ошибка: ${data.message}`, true); setGameStatusMessage(`Ошибка: ${data.message}`, true);
@ -532,7 +568,7 @@ document.addEventListener('DOMContentLoaded', () => {
updateAvailableGamesList([]); updateAvailableGamesList([]);
if (data.gameId) { if (data.gameId) {
currentGameId = data.gameId; currentGameId = data.gameId;
myPlayerId = data.yourPlayerId; myPlayerId = data.yourPlayerId; // Запоминаем наш технический ID слота
if (gameIdInput) gameIdInput.value = currentGameId; if (gameIdInput) gameIdInput.value = currentGameId;
console.log(`[Client] New game ${currentGameId} created after no pending games found. My slot: ${myPlayerId}`); console.log(`[Client] New game ${currentGameId} created after no pending games found. My slot: ${myPlayerId}`);
} }

View File

@ -30,7 +30,7 @@
controls: { controls: {
turnIndicator: document.getElementById('turn-indicator'), turnIndicator: document.getElementById('turn-indicator'),
buttonAttack: document.getElementById('button-attack'), buttonAttack: document.getElementById('button-attack'),
buttonBlock: document.getElementById('button-block'), buttonBlock: document.getElementById('button-block'), // Защита пока не активна
abilitiesGrid: document.getElementById('abilities-grid'), abilitiesGrid: document.getElementById('abilities-grid'),
}, },
log: { log: {
@ -39,7 +39,8 @@
gameOver: { gameOver: {
screen: document.getElementById('game-over-screen'), screen: document.getElementById('game-over-screen'),
message: document.getElementById('result-message'), message: document.getElementById('result-message'),
restartButton: document.getElementById('restart-game-button'), // restartButton: document.getElementById('restart-game-button'), // Старый ID, заменен
returnToMenuButton: document.getElementById('return-to-menu-button'), // Новый ID
modalContent: document.getElementById('game-over-screen')?.querySelector('.modal-content') modalContent: document.getElementById('game-over-screen')?.querySelector('.modal-content')
}, },
gameHeaderTitle: document.querySelector('.game-header h1'), gameHeaderTitle: document.querySelector('.game-header h1'),
@ -54,106 +55,146 @@
if (!logListElement) return; if (!logListElement) return;
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = message; li.textContent = message;
const config = window.GAME_CONFIG || {}; const config = window.GAME_CONFIG || {}; // Получаем конфиг из глобальной области
// Формируем класс для лога на основе типа
const logTypeClass = config[`LOG_TYPE_${type.toUpperCase()}`] ? `log-${config[`LOG_TYPE_${type.toUpperCase()}`]}` : `log-${type}`; const logTypeClass = config[`LOG_TYPE_${type.toUpperCase()}`] ? `log-${config[`LOG_TYPE_${type.toUpperCase()}`]}` : `log-${type}`;
li.className = logTypeClass; li.className = logTypeClass;
logListElement.appendChild(li); logListElement.appendChild(li);
// Прокрутка лога вниз
requestAnimationFrame(() => { logListElement.scrollTop = logListElement.scrollHeight; }); requestAnimationFrame(() => { logListElement.scrollTop = logListElement.scrollHeight; });
} }
function updateFighterPanelUI(panelRole, fighterState, fighterBaseStats, isControlledByThisClient) { function updateFighterPanelUI(panelRole, fighterState, fighterBaseStats, isControlledByThisClient) {
const elements = uiElements[panelRole]; const elements = uiElements[panelRole]; // 'player' или 'opponent'
const config = window.GAME_CONFIG || {}; const config = window.GAME_CONFIG || {};
if (!elements || !elements.hpFill || !elements.hpText || !elements.resourceFill || !elements.resourceText || !elements.status || !fighterState || !fighterBaseStats) { if (!elements || !elements.hpFill || !elements.hpText || !elements.resourceFill || !elements.resourceText || !elements.status || !fighterState || !fighterBaseStats) {
console.warn(`updateFighterPanelUI: Отсутствуют элементы/состояние/статы для панели ${panelRole}.`); // console.warn(`updateFighterPanelUI: Отсутствуют элементы UI, состояние бойца или базовые статы для панели ${panelRole}.`);
// Если панель должна быть видима, но нет данных, можно ее скрыть или показать плейсхолдер
if (elements && elements.panel && elements.panel.style.display !== 'none' && (!fighterState || !fighterBaseStats)) {
// console.warn(`updateFighterPanelUI: Нет данных для видимой панели ${panelRole}.`);
// elements.panel.style.opacity = '0.3'; // Пример: сделать полупрозрачной
}
return; return;
} }
// Если панель была полупрозрачной (из-за отсутствия данных), а теперь данные есть, делаем ее полностью видимой
// if (elements.panel && elements.panel.style.opacity !== '1' && fighterState && fighterBaseStats) {
// elements.panel.style.opacity = '1';
// }
// Обновление имени и иконки персонажа
if (elements.name) { if (elements.name) {
let iconClass = 'fa-question'; let accentColor = 'var(--text-muted)'; let iconClass = 'fa-question'; // Иконка по умолчанию
let accentColor = 'var(--text-muted)'; // Цвет по умолчанию
const characterKey = fighterBaseStats.characterKey; const characterKey = fighterBaseStats.characterKey;
if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-player'; accentColor = 'var(--accent-player)'; } if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-player'; accentColor = 'var(--accent-player)'; }
else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; accentColor = 'var(--accent-almagest)'; } // Используем новый цвет else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; accentColor = 'var(--accent-almagest)'; }
else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-opponent'; accentColor = 'var(--accent-opponent)'; } else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-opponent'; accentColor = 'var(--accent-opponent)'; }
let nameHtml = `<i class="fas ${iconClass}"></i> ${fighterBaseStats.name}`; else { /* console.warn(`updateFighterPanelUI: Неизвестный characterKey "${characterKey}" для иконки/цвета имени.`); */ }
let nameHtml = `<i class="fas ${iconClass}"></i> ${fighterBaseStats.name || 'Неизвестно'}`;
if (isControlledByThisClient) nameHtml += " (Вы)"; if (isControlledByThisClient) nameHtml += " (Вы)";
elements.name.innerHTML = nameHtml; elements.name.style.color = accentColor; elements.name.innerHTML = nameHtml;
elements.name.style.color = accentColor;
} }
// Обновление аватара
if (elements.avatar && fighterBaseStats.avatarPath) elements.avatar.src = fighterBaseStats.avatarPath; if (elements.avatar && fighterBaseStats.avatarPath) elements.avatar.src = fighterBaseStats.avatarPath;
else if (elements.avatar) elements.avatar.src = 'images/default_avatar.png'; else if (elements.avatar) elements.avatar.src = 'images/default_avatar.png'; // Запасной аватар
const maxHp = Math.max(1, fighterBaseStats.maxHp); // Обновление полос здоровья и ресурса
const maxHp = Math.max(1, fighterBaseStats.maxHp); // Избегаем деления на ноль
const maxRes = Math.max(1, fighterBaseStats.maxResource); const maxRes = Math.max(1, fighterBaseStats.maxResource);
const currentHp = Math.max(0, fighterState.currentHp); const currentHp = Math.max(0, fighterState.currentHp);
const currentRes = Math.max(0, fighterState.currentResource); const currentRes = Math.max(0, fighterState.currentResource);
elements.hpFill.style.width = `${(currentHp / maxHp) * 100}%`; elements.hpFill.style.width = `${(currentHp / maxHp) * 100}%`;
elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`; elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`;
elements.resourceFill.style.width = `${(currentRes / maxRes) * 100}%`; elements.resourceFill.style.width = `${(currentRes / maxRes) * 100}%`;
elements.resourceText.textContent = `${Math.round(currentRes)} / ${fighterBaseStats.maxResource}`; elements.resourceText.textContent = `${Math.round(currentRes)} / ${fighterBaseStats.maxResource}`;
const resourceBarContainer = elements[`${panelRole}ResourceBarContainer`]; // Обновление типа ресурса и иконки (mana/stamina/dark-energy)
const resourceIconElement = elements[`${panelRole}ResourceTypeIcon`]; const resourceBarContainerToUpdate = (panelRole === 'player') ? uiElements.playerResourceBarContainer : uiElements.opponentResourceBarContainer;
if (resourceBarContainer && resourceIconElement) { const resourceIconElementToUpdate = (panelRole === 'player') ? uiElements.playerResourceTypeIcon : uiElements.opponentResourceTypeIcon;
resourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
let resourceClass = 'mana'; let iconClass = 'fa-flask'; if (resourceBarContainerToUpdate && resourceIconElementToUpdate) {
resourceBarContainerToUpdate.classList.remove('mana', 'stamina', 'dark-energy'); // Сначала удаляем все классы ресурсов
let resourceClass = 'mana'; let iconClass = 'fa-flask'; // Значения по умолчанию (для Елены)
if (fighterBaseStats.resourceName === 'Ярость') { resourceClass = 'stamina'; iconClass = 'fa-fire-alt'; } if (fighterBaseStats.resourceName === 'Ярость') { resourceClass = 'stamina'; iconClass = 'fa-fire-alt'; }
else if (fighterBaseStats.resourceName === 'Темная Энергия') { resourceClass = 'dark-energy'; iconClass = 'fa-skull'; } else if (fighterBaseStats.resourceName === 'Темная Энергия') { resourceClass = 'dark-energy'; iconClass = 'fa-skull'; }
resourceBarContainer.classList.add(resourceClass); resourceBarContainerToUpdate.classList.add(resourceClass);
resourceIconElement.className = `fas ${iconClass}`; resourceIconElementToUpdate.className = `fas ${iconClass}`;
} }
// Обновление статуса (Готов/Защищается)
const statusText = fighterState.isBlocking ? (config.STATUS_BLOCKING || 'Защищается') : (config.STATUS_READY || 'Готов(а)'); const statusText = fighterState.isBlocking ? (config.STATUS_BLOCKING || 'Защищается') : (config.STATUS_READY || 'Готов(а)');
elements.status.textContent = statusText; elements.status.textContent = statusText;
elements.status.classList.toggle(config.CSS_CLASS_BLOCKING || 'blocking', fighterState.isBlocking); elements.status.classList.toggle(config.CSS_CLASS_BLOCKING || 'blocking', fighterState.isBlocking);
// Обновление подсветки и рамки панели
if (elements.panel) { if (elements.panel) {
let glowColorVar = '--panel-glow-opponent'; let borderColorVar = '--accent-opponent'; let glowColorVar = '--panel-glow-opponent';
if (fighterBaseStats.characterKey === 'elena') { glowColorVar = '--panel-glow-player'; borderColorVar = '--accent-player'; } let borderColorVar = 'var(--accent-opponent)'; // По умолчанию для оппонента
else if (fighterBaseStats.characterKey === 'almagest') { glowColorVar = '--panel-glow-opponent'; borderColorVar = 'var(--accent-almagest)'; } // Цвет рамки Альмагест if (fighterBaseStats.characterKey === 'elena') { glowColorVar = '--panel-glow-player'; borderColorVar = 'var(--accent-player)'; }
elements.panel.style.borderColor = borderColorVar; // Прямое присвоение, т.к. var() не сработает для accent-almagest если он не в :root else if (fighterBaseStats.characterKey === 'almagest') { glowColorVar = 'var(--panel-glow-opponent)'; borderColorVar = 'var(--accent-almagest)'; } // Для Альмагест используется ее цвет рамки
elements.panel.style.boxShadow = `0 0 15px var(${glowColorVar}), inset 0 0 10px rgba(0, 0, 0, 0.3)`; else if (fighterBaseStats.characterKey === 'balard') { glowColorVar = 'var(--panel-glow-opponent)'; borderColorVar = 'var(--accent-opponent)'; }
else { borderColorVar = 'var(--panel-border)'; } // Фоллбэк
elements.panel.style.borderColor = borderColorVar;
// Убедимся, что переменная glowColorVar существует, иначе тень может не примениться или вызвать ошибку
elements.panel.style.boxShadow = glowColorVar ? `0 0 15px ${glowColorVar}, inset 0 0 10px rgba(0, 0, 0, 0.3)` : `0 0 15px rgba(0,0,0,0.4), inset 0 0 10px rgba(0,0,0,0.3)`;
} }
} }
function generateEffectsHTML(effectsArray) { function generateEffectsHTML(effectsArray) {
const config = window.GAME_CONFIG || {}; const config = window.GAME_CONFIG || {};
if (!effectsArray || effectsArray.length === 0) return 'Нет'; if (!effectsArray || effectsArray.length === 0) return 'Нет';
return effectsArray.map(eff => { return effectsArray.map(eff => {
let effectClasses = config.CSS_CLASS_EFFECT || 'effect'; let effectClasses = config.CSS_CLASS_EFFECT || 'effect';
const title = `${eff.name}${eff.description ? ` - ${eff.description}` : ''} (Осталось: ${eff.turnsLeft} х.)`; const title = `${eff.name}${eff.description ? ` - ${eff.description}` : ''} (Осталось: ${eff.turnsLeft} х.)`;
const displayText = `${eff.name} (${eff.turnsLeft} х.)`; const displayText = `${eff.name} (${eff.turnsLeft} х.)`;
if (eff.type === config.ACTION_TYPE_DISABLE || eff.isFullSilence || eff.id.startsWith('playerSilencedOn_')) effectClasses += ' effect-stun';
else if (eff.type === config.ACTION_TYPE_DEBUFF || (eff.power && eff.power < 0) || eff.id.startsWith('effect_')) effectClasses += ' effect-debuff'; if (eff.isFullSilence || eff.id.startsWith('playerSilencedOn_') || (eff.type === config.ACTION_TYPE_DISABLE && !eff.grantsBlock) ) {
else if (eff.grantsBlock) effectClasses += ' effect-block'; effectClasses += ' effect-stun'; // Стан/безмолвие
else effectClasses += ' effect-buff'; } else if (eff.grantsBlock) {
effectClasses += ' effect-block'; // Эффект блока
} else if (eff.type === config.ACTION_TYPE_DEBUFF || (eff.power && eff.power < 0 && eff.type !== config.ACTION_TYPE_HEAL )) {
effectClasses += ' effect-debuff'; // Ослабления, DoT
} else { // ACTION_TYPE_BUFF или положительные эффекты (например, HoT)
effectClasses += ' effect-buff';
}
return `<span class="${effectClasses}" title="${title}">${displayText}</span>`; return `<span class="${effectClasses}" title="${title}">${displayText}</span>`;
}).join(' '); }).join(' ');
} }
function updateEffectsUI(currentGameState) { function updateEffectsUI(currentGameState) {
if (!currentGameState || !uiElements.player.buffsList || !uiElements.opponent.buffsList) return; if (!currentGameState || !window.GAME_CONFIG) { return; }
const mySlotId = window.myPlayerId; // Наш слот ('player' или 'opponent') const mySlotId = window.myPlayerId; // Технический ID слота этого клиента
if (!mySlotId) { return; }
const opponentSlotId = mySlotId === window.GAME_CONFIG.PLAYER_ID ? window.GAME_CONFIG.OPPONENT_ID : window.GAME_CONFIG.PLAYER_ID; const opponentSlotId = mySlotId === window.GAME_CONFIG.PLAYER_ID ? window.GAME_CONFIG.OPPONENT_ID : window.GAME_CONFIG.PLAYER_ID;
const myState = currentGameState[mySlotId]; const myState = currentGameState[mySlotId]; // Состояние персонажа этого клиента
if (uiElements.player && myState && myState.activeEffects) { if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList && myState && myState.activeEffects) {
uiElements.player.buffsList.innerHTML = generateEffectsHTML(myState.activeEffects.filter(e => e.type === window.GAME_CONFIG.ACTION_TYPE_BUFF || e.grantsBlock)); uiElements.player.buffsList.innerHTML = generateEffectsHTML(myState.activeEffects.filter(e => e.type === window.GAME_CONFIG.ACTION_TYPE_BUFF || e.grantsBlock || (e.type === window.GAME_CONFIG.ACTION_TYPE_HEAL && e.turnsLeft > 0) ));
uiElements.player.debuffsList.innerHTML = generateEffectsHTML(myState.activeEffects.filter(e => e.type !== window.GAME_CONFIG.ACTION_TYPE_BUFF && !e.grantsBlock)); uiElements.player.debuffsList.innerHTML = generateEffectsHTML(myState.activeEffects.filter(e => e.type !== window.GAME_CONFIG.ACTION_TYPE_BUFF && !e.grantsBlock && !(e.type === window.GAME_CONFIG.ACTION_TYPE_HEAL && e.turnsLeft > 0) ));
} }
const opponentState = currentGameState[opponentSlotId]; const opponentState = currentGameState[opponentSlotId]; // Состояние оппонента этого клиента
if (uiElements.opponent && opponentState && opponentState.activeEffects) { if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList && opponentState && opponentState.activeEffects) {
uiElements.opponent.buffsList.innerHTML = generateEffectsHTML(opponentState.activeEffects.filter(e => e.type === window.GAME_CONFIG.ACTION_TYPE_BUFF || e.grantsBlock)); uiElements.opponent.buffsList.innerHTML = generateEffectsHTML(opponentState.activeEffects.filter(e => e.type === window.GAME_CONFIG.ACTION_TYPE_BUFF || e.grantsBlock || (e.type === window.GAME_CONFIG.ACTION_TYPE_HEAL && e.turnsLeft > 0) ));
uiElements.opponent.debuffsList.innerHTML = generateEffectsHTML(opponentState.activeEffects.filter(e => e.type !== window.GAME_CONFIG.ACTION_TYPE_BUFF && !e.grantsBlock)); uiElements.opponent.debuffsList.innerHTML = generateEffectsHTML(opponentState.activeEffects.filter(e => e.type !== window.GAME_CONFIG.ACTION_TYPE_BUFF && !e.grantsBlock && !(e.type === window.GAME_CONFIG.ACTION_TYPE_HEAL && e.turnsLeft > 0) ));
} }
} }
function updateUI() { function updateUI() {
const currentGameState = window.gameState; const currentGameState = window.gameState; // Глобальное состояние игры
const gameDataGlobal = window.gameData; const gameDataGlobal = window.gameData; // Глобальные данные (статы, абилки) для этого клиента
const configGlobal = window.GAME_CONFIG; const configGlobal = window.GAME_CONFIG; // Глобальный конфиг
const myActualPlayerId = window.myPlayerId; // Слот, который занимает ЭТОТ клиент ('player' или 'opponent') const myActualPlayerId = window.myPlayerId; // Технический ID слота этого клиента
if (!currentGameState || !gameDataGlobal || !configGlobal || !myActualPlayerId) { if (!currentGameState || !gameDataGlobal || !configGlobal || !myActualPlayerId) {
console.warn("updateUI: Отсутствуют глобальные gameState, gameData, GAME_CONFIG или myActualPlayerId."); console.warn("updateUI: Отсутствуют глобальные gameState, gameData, GAME_CONFIG или myActualPlayerId.");
@ -164,35 +205,64 @@
return; return;
} }
// Определяем ID слота того, кто сейчас ходит // Определяем, чей сейчас ход по ID слота
const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID; const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID;
// Определяем ID слота оппонента для этого клиента
// Обновление панелей бойцов
const opponentActualSlotId = myActualPlayerId === configGlobal.PLAYER_ID ? configGlobal.OPPONENT_ID : configGlobal.PLAYER_ID; const opponentActualSlotId = myActualPlayerId === configGlobal.PLAYER_ID ? configGlobal.OPPONENT_ID : configGlobal.PLAYER_ID;
// Обновление панели "моего" персонажа
if (gameDataGlobal.playerBaseStats && currentGameState[myActualPlayerId]) {
if (uiElements.player.panel && uiElements.player.panel.style.opacity !== '1') uiElements.player.panel.style.opacity = '1';
updateFighterPanelUI('player', currentGameState[myActualPlayerId], gameDataGlobal.playerBaseStats, true); updateFighterPanelUI('player', currentGameState[myActualPlayerId], gameDataGlobal.playerBaseStats, true);
} else {
if (uiElements.player.panel) uiElements.player.panel.style.opacity = '0.5';
}
// Обновление панели "моего оппонента"
if (gameDataGlobal.opponentBaseStats && currentGameState[opponentActualSlotId]) {
if (uiElements.opponent.panel && uiElements.opponent.panel.style.opacity !== '1' && currentGameState.isGameOver === false ) {
// Если панель оппонента была "затемнена" и игра не окончена, восстанавливаем видимость
uiElements.opponent.panel.style.opacity = '1';
}
// Перед обновлением, если игра НЕ окончена, а панель "тает", отменяем это
if (uiElements.opponent.panel && uiElements.opponent.panel.classList.contains('dissolving') && currentGameState.isGameOver === false) {
console.warn("[UI UPDATE DEBUG] Opponent panel has .dissolving but game is NOT over. Forcing visible.");
const panel = uiElements.opponent.panel;
panel.classList.remove('dissolving');
const originalTransition = panel.style.transition; panel.style.transition = 'none';
panel.style.opacity = '1'; panel.style.transform = 'scale(1) translateY(0)';
requestAnimationFrame(() => { panel.style.transition = originalTransition || ''; });
}
updateFighterPanelUI('opponent', currentGameState[opponentActualSlotId], gameDataGlobal.opponentBaseStats, false); updateFighterPanelUI('opponent', currentGameState[opponentActualSlotId], gameDataGlobal.opponentBaseStats, false);
} else {
if (uiElements.opponent.panel) uiElements.opponent.panel.style.opacity = '0.5';
}
updateEffectsUI(currentGameState); updateEffectsUI(currentGameState);
// Обновление заголовка игры (Имя1 vs Имя2)
if (uiElements.gameHeaderTitle && gameDataGlobal.playerBaseStats && gameDataGlobal.opponentBaseStats) { if (uiElements.gameHeaderTitle && gameDataGlobal.playerBaseStats && gameDataGlobal.opponentBaseStats) {
const myName = gameDataGlobal.playerBaseStats.name; const myName = gameDataGlobal.playerBaseStats.name; // Имя моего персонажа
const opponentName = gameDataGlobal.opponentBaseStats.name; const opponentName = gameDataGlobal.opponentBaseStats.name; // Имя моего оппонента
const myKey = gameDataGlobal.playerBaseStats.characterKey; const myKey = gameDataGlobal.playerBaseStats.characterKey;
const opponentKey = gameDataGlobal.opponentBaseStats.characterKey; const opponentKey = gameDataGlobal.opponentBaseStats.characterKey;
let myClass = 'title-player'; let myClass = 'title-player';
if (myKey === 'elena') myClass = 'title-enchantress'; if (myKey === 'elena') myClass = 'title-enchantress'; else if (myKey === 'almagest') myClass = 'title-sorceress';
else if (myKey === 'almagest') myClass = 'title-sorceress';
let opponentClass = 'title-opponent'; let opponentClass = 'title-opponent';
if (opponentKey === 'elena') opponentClass = 'title-enchantress'; if (opponentKey === 'elena') opponentClass = 'title-enchantress'; else if (opponentKey === 'almagest') opponentClass = 'title-sorceress'; else if (opponentKey === 'balard') opponentClass = 'title-knight';
else if (opponentKey === 'almagest') opponentClass = 'title-sorceress';
else if (opponentKey === 'balard') opponentClass = 'title-knight';
uiElements.gameHeaderTitle.innerHTML = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="${opponentClass}">${opponentName}</span>`; uiElements.gameHeaderTitle.innerHTML = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="${opponentClass}">${opponentName}</span>`;
} }
// Обновление индикатора хода
if (uiElements.controls.turnIndicator) { if (uiElements.controls.turnIndicator) {
const currentTurnActorState = currentGameState[actorSlotWhoseTurnItIs]; // Имя того, чей ход, берем из gameState по ID слота
const currentTurnActorState = currentGameState[actorSlotWhoseTurnItIs]; // 'player' или 'opponent'
const currentTurnName = currentTurnActorState?.name || 'Неизвестно'; const currentTurnName = currentTurnActorState?.name || 'Неизвестно';
uiElements.controls.turnIndicator.textContent = `Ход: ${currentTurnName}`; uiElements.controls.turnIndicator.textContent = `Ход: ${currentTurnName}`;
const currentTurnCharacterKey = currentTurnActorState?.characterKey; const currentTurnCharacterKey = currentTurnActorState?.characterKey;
let turnColor = 'var(--turn-color)'; let turnColor = 'var(--turn-color)';
if (currentTurnCharacterKey === 'elena') turnColor = 'var(--accent-player)'; if (currentTurnCharacterKey === 'elena') turnColor = 'var(--accent-player)';
@ -201,31 +271,35 @@
uiElements.controls.turnIndicator.style.color = turnColor; uiElements.controls.turnIndicator.style.color = turnColor;
} }
const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId; // Управление активностью кнопок
const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId; // Может ли ЭТОТ клиент ходить
const isGameActive = !currentGameState.isGameOver; const isGameActive = !currentGameState.isGameOver;
// Кнопка атаки
if (uiElements.controls.buttonAttack) { if (uiElements.controls.buttonAttack) {
uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive); uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive);
const myCharKey = gameDataGlobal.playerBaseStats.characterKey; const myCharKey = gameDataGlobal.playerBaseStats?.characterKey;
const myState = currentGameState[myActualPlayerId]; const myStateForAttackBuff = currentGameState[myActualPlayerId]; // Состояние моего персонажа
let attackBuffId = null; let attackBuffId = null;
if (myCharKey === 'elena') attackBuffId = configGlobal.ABILITY_ID_NATURE_STRENGTH; if (myCharKey === 'elena') attackBuffId = configGlobal.ABILITY_ID_NATURE_STRENGTH;
else if (myCharKey === 'almagest') attackBuffId = configGlobal.ABILITY_ID_ALMAGEST_BUFF_ATTACK; else if (myCharKey === 'almagest') attackBuffId = configGlobal.ABILITY_ID_ALMAGEST_BUFF_ATTACK;
if (attackBuffId && myState) {
const isAttackBuffReady = myState.activeEffects.some(eff => eff.id === attackBuffId && !eff.justCast); if (attackBuffId && myStateForAttackBuff && myStateForAttackBuff.activeEffects) {
const isAttackBuffReady = myStateForAttackBuff.activeEffects.some(eff => eff.id === attackBuffId && !eff.justCast);
uiElements.controls.buttonAttack.classList.toggle(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed', isAttackBuffReady && canThisClientAct && isGameActive); uiElements.controls.buttonAttack.classList.toggle(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed', isAttackBuffReady && canThisClientAct && isGameActive);
} else { } else {
uiElements.controls.buttonAttack.classList.remove(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed'); uiElements.controls.buttonAttack.classList.remove(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed');
} }
} }
if (uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true; if (uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true; // Пока не используется
const actingPlayerState = currentGameState[myActualPlayerId]; // Кнопки способностей
const actingPlayerAbilities = gameDataGlobal.playerAbilities; const actingPlayerState = currentGameState[myActualPlayerId]; // Состояние моего персонажа
const actingPlayerResourceName = gameDataGlobal.playerBaseStats.resourceName; const actingPlayerAbilities = gameDataGlobal.playerAbilities; // Способности моего персонажа
const actingPlayerResourceName = gameDataGlobal.playerBaseStats?.resourceName;
uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => { uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => {
if (!(button instanceof HTMLButtonElement) || !actingPlayerState || !actingPlayerAbilities) { if (!(button instanceof HTMLButtonElement) || !actingPlayerState || !actingPlayerAbilities || !actingPlayerResourceName) {
if(button instanceof HTMLButtonElement) button.disabled = true; if(button instanceof HTMLButtonElement) button.disabled = true;
return; return;
} }
@ -234,17 +308,21 @@
if (!ability) { button.disabled = true; return; } if (!ability) { button.disabled = true; return; }
const hasEnoughResource = actingPlayerState.currentResource >= ability.cost; const hasEnoughResource = actingPlayerState.currentResource >= ability.cost;
const isBuffAlreadyActive = ability.type === configGlobal.ACTION_TYPE_BUFF && actingPlayerState.activeEffects.some(eff => eff.id === ability.id); const isBuffAlreadyActive = ability.type === configGlobal.ACTION_TYPE_BUFF && actingPlayerState.activeEffects?.some(eff => eff.id === ability.id);
const isOnCooldown = (actingPlayerState.abilityCooldowns?.[ability.id] || 0) > 0; const isOnCooldown = (actingPlayerState.abilityCooldowns?.[ability.id] || 0) > 0;
const isGenerallySilenced = actingPlayerState.activeEffects.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
const isSpecificallySilenced = actingPlayerState.disabledAbilities?.some(dis => dis.abilityId === abilityId && dis.turnsLeft > 0); const isGenerallySilenced = actingPlayerState.activeEffects?.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
const specificSilenceEffect = actingPlayerState.disabledAbilities?.find(dis => dis.abilityId === abilityId && dis.turnsLeft > 0);
const isSpecificallySilenced = !!specificSilenceEffect;
const isSilenced = isGenerallySilenced || isSpecificallySilenced; const isSilenced = isGenerallySilenced || isSpecificallySilenced;
const silenceTurnsLeft = isGenerallySilenced ? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0) const silenceTurnsLeft = isGenerallySilenced
: (isSpecificallySilenced ? (actingPlayerState.disabledAbilities.find(dis => dis.abilityId === abilityId)?.turnsLeft || 0) : 0); ? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0)
: (specificSilenceEffect?.turnsLeft || 0);
let isDisabledByDebuffOnTarget = false; let isDisabledByDebuffOnTarget = false;
const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId]; const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId]; // Состояние моего оппонента
if ((ability.id === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF) && opponentStateForDebuffCheck) { if (opponentStateForDebuffCheck && opponentStateForDebuffCheck.activeEffects &&
(ability.id === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF)) {
const effectIdForDebuff = 'effect_' + ability.id; const effectIdForDebuff = 'effect_' + ability.id;
isDisabledByDebuffOnTarget = opponentStateForDebuffCheck.activeEffects.some(e => e.id === effectIdForDebuff); isDisabledByDebuffOnTarget = opponentStateForDebuffCheck.activeEffects.some(e => e.id === effectIdForDebuff);
} }
@ -265,10 +343,11 @@
button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', isBuffAlreadyActive && !isDisabledByDebuffOnTarget); button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', isBuffAlreadyActive && !isDisabledByDebuffOnTarget);
} }
// Обновление title (всплывающей подсказки)
let titleText = `${ability.name} (${ability.cost} ${actingPlayerResourceName})`; let titleText = `${ability.name} (${ability.cost} ${actingPlayerResourceName})`;
let descriptionText = ability.description; let descriptionText = ability.description;
if (typeof ability.descriptionFunction === 'function') { if (typeof ability.descriptionFunction === 'function') {
descriptionText = ability.descriptionFunction(configGlobal, gameDataGlobal.opponentBaseStats); descriptionText = ability.descriptionFunction(configGlobal, gameDataGlobal.opponentBaseStats); // Для описания используем статы оппонента этого клиента
} }
if (descriptionText) titleText += ` - ${descriptionText}`; if (descriptionText) titleText += ` - ${descriptionText}`;
let abilityBaseCooldown = ability.cooldown; let abilityBaseCooldown = ability.cooldown;
@ -279,45 +358,87 @@
if (isOnCooldown) titleText = `${ability.name} - На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[ability.id]} х.`; if (isOnCooldown) titleText = `${ability.name} - На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[ability.id]} х.`;
else if (isSilenced) titleText = `Безмолвие! Осталось: ${silenceTurnsLeft} х.`; else if (isSilenced) titleText = `Безмолвие! Осталось: ${silenceTurnsLeft} х.`;
else if (isBuffAlreadyActive) { else if (isBuffAlreadyActive) {
const activeEffect = actingPlayerState.activeEffects.find(eff => eff.id === abilityId); const activeEffect = actingPlayerState.activeEffects?.find(eff => eff.id === abilityId);
titleText = `Эффект "${ability.name}" уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}`; titleText = `Эффект "${ability.name}" уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}`;
} else if (isDisabledByDebuffOnTarget && opponentStateForDebuffCheck) { } else if (isDisabledByDebuffOnTarget && opponentStateForDebuffCheck) {
const activeDebuff = opponentStateForDebuffCheck.activeEffects.find(e => e.id === 'effect_' + ability.id); const activeDebuff = opponentStateForDebuffCheck.activeEffects?.find(e => e.id === 'effect_' + ability.id);
titleText = `Эффект "${ability.name}" уже наложен на ${opponentStateForDebuffCheck.name}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}`; titleText = `Эффект "${ability.name}" уже наложен на ${opponentStateForDebuffCheck.name}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}`;
} }
button.setAttribute('title', titleText); button.setAttribute('title', titleText);
}); });
} }
function showGameOver(playerWon, reason = "") { function showGameOver(playerWon, reason = "", opponentCharacterKeyFromClient = null) {
const config = window.GAME_CONFIG || {}; const config = window.GAME_CONFIG || {};
const gameDataGlobal = window.gameData || {}; // Используем gameData, сохраненное в client.js, так как оно отражает перспективу этого клиента
const currentGameState = window.gameState; const clientSpecificGameData = window.gameData;
const currentActualGameState = window.gameState; // Самое актуальное состояние игры
const gameOverScreenElement = uiElements.gameOver.screen; const gameOverScreenElement = uiElements.gameOver.screen;
if (!gameOverScreenElement || !currentGameState) return;
console.log(`[UI.JS DEBUG] showGameOver CALLED. PlayerWon: ${playerWon}, Reason: ${reason}`);
console.log(`[UI.JS DEBUG] Current window.gameState.isGameOver: ${currentActualGameState?.isGameOver}`);
console.log(`[UI.JS DEBUG] Opponent Character Key (from client via param): ${opponentCharacterKeyFromClient}`);
console.log(`[UI.JS DEBUG] My Character Name (from window.gameData): ${clientSpecificGameData?.playerBaseStats?.name}`);
console.log(`[UI.JS DEBUG] Opponent Character Name (from window.gameData): ${clientSpecificGameData?.opponentBaseStats?.name}`);
if (!gameOverScreenElement) {
console.warn("[UI.JS DEBUG] showGameOver: gameOverScreenElement not found.");
return;
}
const resultMsgElement = uiElements.gameOver.message; const resultMsgElement = uiElements.gameOver.message;
const opponentPanelElement = uiElements.opponent.panel; // Имена для сообщения берем из clientSpecificGameData, т.к. они уже "перевернуты" для каждого клиента
const myName = gameDataGlobal.playerBaseStats?.name || "Игрок"; const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок";
const opponentName = gameDataGlobal.opponentBaseStats?.name || "Противник"; const opponentNameForResult = clientSpecificGameData?.opponentBaseStats?.name || "Противник";
const opponentCharacterKey = gameDataGlobal.opponentBaseStats?.characterKey;
if (resultMsgElement) { if (resultMsgElement) {
let winText = `Победа! ${myName} празднует!`; let winText = `Победа! ${myNameForResult} празднует!`;
let loseText = `Поражение! ${opponentName} оказался(лась) сильнее!`; let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`;
if (reason === 'opponent_disconnected') winText = `${opponentName} покинул(а) игру. Победа присуждается вам!`; if (reason === 'opponent_disconnected') {
winText = `${opponentNameForResult} покинул(а) игру. Победа присуждается вам!`;
// Если оппонент отключился, а мы проиграли (технически такое возможно, если сервер так решит)
// То текст поражения можно оставить стандартным или специфичным.
// Пока оставим стандартный, если playerWon = false и reason = opponent_disconnected.
}
resultMsgElement.textContent = playerWon ? winText : loseText; resultMsgElement.textContent = playerWon ? winText : loseText;
resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)'; resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)';
} }
const opponentPanelElement = uiElements.opponent.panel;
if (opponentPanelElement) { if (opponentPanelElement) {
opponentPanelElement.classList.remove('dissolving'); opponentPanelElement.classList.remove('dissolving');
if (playerWon && reason !== 'opponent_disconnected' && (opponentCharacterKey === 'balard' || opponentCharacterKey === 'almagest')) { console.log(`[UI.JS DEBUG] Opponent panel classList after initial remove .dissolving: ${opponentPanelElement.className}`);
// Используем opponentCharacterKeyFromClient, так как это ключ реального персонажа оппонента
const keyForDissolveEffect = opponentCharacterKeyFromClient;
if (currentActualGameState && currentActualGameState.isGameOver === true && playerWon && reason !== 'opponent_disconnected') {
if (keyForDissolveEffect === 'balard' || keyForDissolveEffect === 'almagest') {
console.log(`[UI.JS DEBUG] ADDING .dissolving to opponent panel. Conditions: isGameOver=${currentActualGameState.isGameOver}, playerWon=${playerWon}, opponentKeyForEffect=${keyForDissolveEffect}`);
opponentPanelElement.classList.add('dissolving'); opponentPanelElement.classList.add('dissolving');
} else {
console.log(`[UI.JS DEBUG] NOT adding .dissolving (opponent key mismatch for dissolve effect). Key for effect: ${keyForDissolveEffect}`);
} }
} else {
console.log(`[UI.JS DEBUG] NOT adding .dissolving. Conditions NOT met: isGameOver=${currentActualGameState?.isGameOver}, playerWon=${playerWon}, reason=${reason}`);
if (!currentActualGameState || currentActualGameState.isGameOver === false) {
console.log(`[UI.JS DEBUG] Ensuring opponent panel is visible because game is not 'isGameOver=true' or gameState missing.`);
const originalTransition = opponentPanelElement.style.transition;
opponentPanelElement.style.transition = 'none';
opponentPanelElement.style.opacity = '1';
opponentPanelElement.style.transform = 'scale(1) translateY(0)';
requestAnimationFrame(() => {
opponentPanelElement.style.transition = originalTransition || '';
});
}
}
console.log(`[UI.JS DEBUG] Opponent panel classList FINAL in showGameOver: ${opponentPanelElement.className}`);
} }
setTimeout(() => { setTimeout(() => {
if (window.gameState && window.gameState.isGameOver === true) { // Перепроверяем перед показом
console.log(`[UI.JS DEBUG] Showing gameOverScreen modal (isGameOver is true).`);
gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden'); gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden');
requestAnimationFrame(() => { requestAnimationFrame(() => {
gameOverScreenElement.style.opacity = '0'; gameOverScreenElement.style.opacity = '0';
@ -329,6 +450,9 @@
} }
}, config.MODAL_TRANSITION_DELAY || 10); }, config.MODAL_TRANSITION_DELAY || 10);
}); });
} else {
console.log(`[UI.JS DEBUG] NOT showing gameOverScreen modal (isGameOver is now false or gameState missing).`);
}
}, config.DELAY_BEFORE_VICTORY_MODAL || 1500); }, config.DELAY_BEFORE_VICTORY_MODAL || 1500);
} }

View File

@ -46,7 +46,7 @@
--scrollbar-track: #10121c; --scrollbar-track: #10121c;
--shake-duration: 0.4s; --shake-duration: 0.4s;
--cast-duration: 0.6s; --cast-duration: 0.6s;
--dissolve-duration: 6.0s; --dissolve-duration: 6.0s; /* Убедитесь, что это значение используется в JS для синхронизации, если нужно */
--log-panel-fixed-height: 280px; --log-panel-fixed-height: 280px;
} }
@ -59,11 +59,11 @@ body {
color: var(--text-light); color: var(--text-light);
line-height: 1.5; line-height: 1.5;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden; /* Предотвращает прокрутку основного body, если контент не помещается */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center; /* Центрирует auth-game-setup-wrapper */
padding: 10px; padding: 10px;
} }
h1, h2, h3, h4 { h1, h2, h3, h4 {
@ -86,7 +86,7 @@ i { margin-right: 6px; color: var(--icon-color); width: 1.2em; text-align: cente
.auth-game-setup-wrapper { .auth-game-setup-wrapper {
width: 100%; width: 100%;
max-width: 700px; max-width: 700px;
margin: 20px auto; margin: 20px auto; /* auto для центрирования, если flex в body изменится */
padding: 25px 30px; padding: 25px 30px;
background: var(--panel-bg); background: var(--panel-bg);
border: 1px solid var(--panel-border); border: 1px solid var(--panel-border);
@ -94,6 +94,9 @@ i { margin-right: 6px; color: var(--icon-color); width: 1.2em; text-align: cente
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5); box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5);
color: var(--text-light); color: var(--text-light);
text-align: center; text-align: center;
/* Добавим overflow-y: auto для случаев, когда контент не помещается */
max-height: calc(100vh - 40px); /* Чтобы не вылезал за экран */
overflow-y: auto;
} }
.auth-game-setup-wrapper h2, .auth-game-setup-wrapper h2,
.auth-game-setup-wrapper h3 { .auth-game-setup-wrapper h3 {
@ -104,6 +107,8 @@ i { margin-right: 6px; color: var(--icon-color); width: 1.2em; text-align: cente
padding-bottom: 0.5em; padding-bottom: 0.5em;
} }
.auth-game-setup-wrapper h3 { font-size: 1.2em; margin-top: 1.5em; } .auth-game-setup-wrapper h3 { font-size: 1.2em; margin-top: 1.5em; }
/* Общие стили для кнопок в .auth-game-setup-wrapper и форм аутентификации */
.auth-game-setup-wrapper button, .auth-game-setup-wrapper button,
#auth-section form button { #auth-section form button {
font-family: var(--font-main); font-family: var(--font-main);
@ -138,6 +143,8 @@ i { margin-right: 6px; color: var(--icon-color); width: 1.2em; text-align: cente
cursor: not-allowed; cursor: not-allowed;
opacity: 0.7; opacity: 0.7;
} }
/* Стили для инпутов */
.auth-game-setup-wrapper input[type="text"], .auth-game-setup-wrapper input[type="text"],
#auth-section input[type="text"], #auth-section input[type="text"],
#auth-section input[type="password"] { #auth-section input[type="password"] {
@ -148,10 +155,12 @@ i { margin-right: 6px; color: var(--icon-color); width: 1.2em; text-align: cente
color: var(--text-light); color: var(--text-light);
margin: 5px 5px 10px 5px; margin: 5px 5px 10px 5px;
font-size: 0.9em; font-size: 0.9em;
width: calc(100% - 22px); width: calc(100% - 22px); /* Учитываем padding и border */
max-width: 300px; max-width: 300px;
box-sizing: border-box; box-sizing: border-box;
} }
/* Стили для списка доступных игр */
#available-games-list { #available-games-list {
margin-top: 20px; margin-top: 20px;
text-align: left; text-align: left;
@ -165,7 +174,13 @@ i { margin-right: 6px; color: var(--icon-color); width: 1.2em; text-align: cente
#available-games-list ul { list-style: none; padding: 0; } #available-games-list ul { list-style: none; padding: 0; }
#available-games-list li { #available-games-list li {
padding: 10px; padding: 10px;
border-bottom: 1px solid rgba(var(--log-border), 0.7); border-bottom: 1px solid rgba(var(--log-border), 0.7); /* Некорректно, var() не работает в rgba() так */
/* Исправлено: */
border-bottom: 1px solid var(--log-border); /* Используем напрямую, или задаем цвет с альфа-каналом в переменной */
/* Или, если нужен именно альфа-канал от существующего цвета: */
/* border-bottom: 1px solid hsla(hue(var(--log-border)), saturation(var(--log-border)), lightness(var(--log-border)), 0.7); /* Это сложно, лучше отдельная переменная */
/* Простой вариант - чуть светлее основной границы: */
/* border-bottom: 1px solid #5a6082; /* Пример */
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -173,6 +188,8 @@ i { margin-right: 6px; color: var(--icon-color); width: 1.2em; text-align: cente
} }
#available-games-list li:last-child { border-bottom: none; } #available-games-list li:last-child { border-bottom: none; }
#available-games-list li button { padding: 6px 10px; font-size: 0.8em; margin-left: 10px; } #available-games-list li button { padding: 6px 10px; font-size: 0.8em; margin-left: 10px; }
/* Контейнер для статусных сообщений */
#status-container { min-height: 2.5em; margin-bottom: 15px; } #status-container { min-height: 2.5em; margin-bottom: 15px; }
#game-status-message, #auth-message { #game-status-message, #auth-message {
color: var(--turn-color); color: var(--turn-color);
@ -186,20 +203,26 @@ i { margin-right: 6px; color: var(--icon-color); width: 1.2em; text-align: cente
} }
#auth-message.success { color: var(--heal-color); } #auth-message.success { color: var(--heal-color); }
#auth-message.error { color: var(--damage-color); } #auth-message.error { color: var(--damage-color); }
/* Формы аутентификации */
#auth-section form { margin-bottom: 20px; } #auth-section form { margin-bottom: 20px; }
/* Информация о пользователе */
#user-info { padding: 10px; background-color: rgba(255,255,255,0.05); border-radius: 5px; margin-bottom: 20px; } #user-info { padding: 10px; background-color: rgba(255,255,255,0.05); border-radius: 5px; margin-bottom: 20px; }
#user-info p { margin: 0 0 10px 0; font-size: 1.1em; } #user-info p { margin: 0 0 10px 0; font-size: 1.1em; }
#logout-button { background: linear-gradient(145deg, #8c3a3a, #6b2b2b) !important; } #logout-button { background: linear-gradient(145deg, #8c3a3a, #6b2b2b) !important; } /* !important чтобы перебить общий стиль кнопки */
#logout-button:hover { background: linear-gradient(145deg, #a04040, #8c3a3a) !important; } #logout-button:hover { background: linear-gradient(145deg, #a04040, #8c3a3a) !important; }
/* Стили для выбора персонажа (перенесены из index.html) */ /* Стили для выбора персонажа */
.character-selection { .character-selection {
margin-top: 15px; margin-top: 15px;
margin-bottom: 15px; margin-bottom: 15px;
padding: 15px; padding: 15px;
background-color: rgba(0,0,0,0.2); background-color: rgba(0,0,0,0.2);
border-radius: 6px; border-radius: 6px;
border: 1px solid rgba(var(--panel-border), 0.5); border: 1px solid rgba(var(--panel-border), 0.5); /* Тоже может быть проблемой с var() в rgba() */
/* Исправлено: */
/* border: 1px solid #353a52; /* Пример полупрозрачного panel-border */
} }
.character-selection h4 { .character-selection h4 {
font-size: 1.1em; font-size: 1.1em;
@ -222,10 +245,12 @@ i { margin-right: 6px; color: var(--icon-color); width: 1.2em; text-align: cente
color: #fff; color: #fff;
font-weight: bold; font-weight: bold;
} }
/* Стилизация для Елены */
.character-selection input[type="radio"][value="elena"]:checked + label { .character-selection input[type="radio"][value="elena"]:checked + label {
background-color: var(--accent-player); background-color: var(--accent-player);
box-shadow: 0 0 8px rgba(108, 149, 255, 0.5); box-shadow: 0 0 8px rgba(108, 149, 255, 0.5);
} }
/* Стилизация для Альмагест */
.character-selection input[type="radio"][value="almagest"]:checked + label { .character-selection input[type="radio"][value="almagest"]:checked + label {
background-color: var(--accent-almagest); /* Новый цвет для Альмагест */ background-color: var(--accent-almagest); /* Новый цвет для Альмагест */
box-shadow: 0 0 8px rgba(199, 108, 255, 0.5); /* Тень для Альмагест */ box-shadow: 0 0 8px rgba(199, 108, 255, 0.5); /* Тень для Альмагест */
@ -268,15 +293,15 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
gap: 10px; gap: 10px;
overflow: hidden; overflow: hidden; /* Предотвращает выпадение контента */
} }
.player-column, .opponent-column { .player-column, .opponent-column {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
min-width: 0; min-width: 0; /* Для корректной работы flex с overflow */
overflow: hidden; overflow: hidden; /* Если контент внутри колонок может быть больше */
} }
/* --- Стили Панелей Персонажей, Управления, Лога --- */ /* --- Стили Панелей Персонажей, Управления, Лога --- */
@ -288,7 +313,7 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
padding: 15px; padding: 15px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden; /* Контент внутри панелей не должен выходить за их пределы */
transition: box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease-out, transform 0.3s ease-out; transition: box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease-out, transform 0.3s ease-out;
} }
@ -300,40 +325,40 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
gap: 10px; gap: 10px;
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1); border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 0; margin-bottom: 0; /* Убираем лишний отступ, если panel-content имеет свой margin-top */
} }
.fighter-name { font-size: 1.6em; margin: 0; flex-grow: 1; text-align: left; } .fighter-name { font-size: 1.6em; margin: 0; flex-grow: 1; text-align: left; }
.fighter-name .icon-player { color: var(--accent-player); } /* Елена */ .fighter-name .icon-player { color: var(--accent-player); }
.fighter-name .icon-opponent { color: var(--accent-opponent); } /* Балард */ .fighter-name .icon-opponent { color: var(--accent-opponent); }
.fighter-name .icon-almagest { color: var(--accent-almagest); } /* Альмагест */ .fighter-name .icon-almagest { color: var(--accent-almagest); }
.character-visual { flex-shrink: 0; margin-bottom: 0; } .character-visual { flex-shrink: 0; margin-bottom: 0; }
.avatar-image { .avatar-image {
display: block; display: block;
max-width: 50px; max-width: 50px; /* Фиксируем или делаем адаптивным */
height: auto; height: auto; /* Для сохранения пропорций */
border-radius: 50%; border-radius: 50%;
border: 2px solid var(--panel-border); /* Цвет рамки будет изменен JS */ border: 2px solid var(--panel-border); /* Цвет рамки будет изменен JS или специфичным классом */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
} }
.panel-content { .panel-content {
flex-grow: 1; flex-grow: 1;
overflow-y: auto; overflow-y: auto; /* Позволяет прокручивать контент, если он не помещается */
padding-right: 5px; padding-right: 5px; /* Для отступа от скроллбара */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
min-height: 0; min-height: 0; /* Для корректной работы flex с overflow */
padding-top: 10px; padding-top: 10px; /* Отступ от panel-header */
margin-top: 0; margin-top: 0; /* Убрали margin-bottom у panel-header, добавили padding-top сюда */
} }
.stat-bar-container { display: flex; align-items: center; gap: 10px; flex-shrink: 0; } .stat-bar-container { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.stat-bar-container .bar-icon { flex-shrink: 0; font-size: 1.4em; } .stat-bar-container .bar-icon { flex-shrink: 0; font-size: 1.4em; }
/* Цвета иконок ресурсов */ /* Цвета иконок ресурсов */
.stat-bar-container.health .bar-icon { color: var(--hp-color); } .stat-bar-container.health .bar-icon { color: var(--hp-color); }
.stat-bar-container.mana .bar-icon { color: var(--mana-color); } /* Мана Елены */ .stat-bar-container.mana .bar-icon { color: var(--mana-color); }
.stat-bar-container.stamina .bar-icon { color: var(--stamina-color); } /* Ярость Баларда */ .stat-bar-container.stamina .bar-icon { color: var(--stamina-color); }
.stat-bar-container.dark-energy .bar-icon { color: var(--dark-energy-color); } /* Темная Энергия Альмагест */ .stat-bar-container.dark-energy .bar-icon { color: var(--dark-energy-color); }
.bar-wrapper { flex-grow: 1; } .bar-wrapper { flex-grow: 1; }
.bar { .bar {
@ -342,7 +367,7 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
position: relative; background-color: var(--bar-bg); position: relative; background-color: var(--bar-bg);
} }
.bar-fill { .bar-fill {
display: block; height: 100%; border-radius: 3px; display: block; height: 100%; border-radius: 3px; /* чуть меньше, чем у родителя */
position: relative; z-index: 2; transition: width 0.4s ease-out; position: relative; z-index: 2; transition: width 0.4s ease-out;
} }
.bar-text { .bar-text {
@ -350,7 +375,7 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
z-index: 3; display: flex; justify-content: center; align-items: center; z-index: 3; display: flex; justify-content: center; align-items: center;
font-size: 0.75em; font-weight: bold; color: #fff; font-size: 0.75em; font-weight: bold; color: #fff;
text-shadow: 1px 1px 1px rgba(0,0,0,0.9); padding: 0 5px; text-shadow: 1px 1px 1px rgba(0,0,0,0.9); padding: 0 5px;
white-space: nowrap; pointer-events: none; white-space: nowrap; pointer-events: none; /* Чтобы текст не мешал кликам, если они есть */
} }
/* Цвета Заливки Баров */ /* Цвета Заливки Баров */
.health .bar-fill { background-color: var(--hp-color); } .health .bar-fill { background-color: var(--hp-color); }
@ -362,30 +387,30 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
/* Статус и Эффекты */ /* Статус и Эффекты */
.status-area { .status-area {
font-size: 0.9em; display: flex; align-items: baseline; gap: 5px; font-size: 0.9em; display: flex; align-items: baseline; gap: 5px;
flex-shrink: 0; min-height: 1.5em; flex-shrink: 0; min-height: 1.5em; /* Чтобы не прыгал layout */
} }
.status-area .icon-status { font-size: 1em; flex-shrink: 0; margin-top: 0.1em; } .status-area .icon-status { font-size: 1em; flex-shrink: 0; margin-top: 0.1em; /* Подгонка выравнивания */ }
.status-area strong { color: var(--text-muted); font-weight: normal; flex-shrink: 0; margin-right: 3px; } .status-area strong { color: var(--text-muted); font-weight: normal; flex-shrink: 0; margin-right: 3px; }
.status-area span { font-weight: bold; } .status-area span { font-weight: bold; }
.status-area span.blocking { color: var(--block-color); font-style: italic; } .status-area span.blocking { color: var(--block-color); font-style: italic; }
.effects-area { .effects-area {
font-size: 0.9em; display: flex; flex-direction: column; gap: 8px; font-size: 0.9em; display: flex; flex-direction: column; gap: 8px;
flex-shrink: 0; min-height: 3em; flex-shrink: 0; min-height: 3em; /* Резервируем место */
} }
.effect-category { display: flex; align-items: baseline; gap: 5px; } .effect-category { display: flex; align-items: baseline; gap: 5px; }
.effect-category strong { .effect-category strong {
color: var(--text-muted); font-weight: normal; font-family: var(--font-main); color: var(--text-muted); font-weight: normal; font-family: var(--font-main); /* Убедимся, что шрифт основной */
font-size: 0.9em; flex-shrink: 0; margin-right: 3px; font-size: 0.9em; flex-shrink: 0; margin-right: 3px;
} }
.effect-category .icon-effects-buff, .effect-category .icon-effects-debuff { .effect-category .icon-effects-buff, .effect-category .icon-effects-debuff {
font-size: 1em; flex-shrink: 0; margin-top: 0.1em; font-size: 1em; flex-shrink: 0; margin-top: 0.1em; /* Подгонка выравнивания */
width: 1.2em; text-align: center; width: 1.2em; text-align: center; /* Для иконок */
} }
.effect-category .icon-effects-buff { color: var(--heal-color); } .effect-category .icon-effects-buff { color: var(--heal-color); }
.effect-category .icon-effects-debuff { color: var(--damage-color); } .effect-category .icon-effects-debuff { color: var(--damage-color); }
.effect-list { display: inline; line-height: 1.4; min-width: 0; font-weight: bold; } .effect-list { display: inline; line-height: 1.4; min-width: 0; /* Для переноса, если нужно */ font-weight: bold; }
.effect { .effect {
display: inline-block; margin: 2px 3px 2px 0; padding: 1px 6px; display: inline-block; margin: 2px 3px 2px 0; padding: 1px 6px;
font-size: 0.8em; border-radius: 10px; border: 1px solid; font-size: 0.8em; border-radius: 10px; border: 1px solid;
@ -394,8 +419,8 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
} }
.effect-buff { border-color: var(--heal-color); color: var(--heal-color); } .effect-buff { border-color: var(--heal-color); color: var(--heal-color); }
.effect-debuff { border-color: var(--damage-color); color: var(--damage-color); } .effect-debuff { border-color: var(--damage-color); color: var(--damage-color); }
.effect-stun { border-color: var(--turn-color); color: var(--turn-color); } .effect-stun { border-color: var(--turn-color); color: var(--turn-color); } /* Для безмолвия/стана */
.effect-block { border-color: var(--block-color); color: var(--block-color); } .effect-block { border-color: var(--block-color); color: var(--block-color); } /* Для эффектов блока */
/* --- Панель Управления --- */ /* --- Панель Управления --- */
@ -424,11 +449,13 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
box-shadow: 0 3px 6px rgba(0,0,0,0.5); box-shadow: 0 3px 6px rgba(0,0,0,0.5);
} }
.action-button.basic:active:enabled { transform: translateY(0px); box-shadow: 0 1px 2px rgba(0,0,0,0.4); } .action-button.basic:active:enabled { transform: translateY(0px); box-shadow: 0 1px 2px rgba(0,0,0,0.4); }
/* Стиль для бафнутой атаки */
#button-attack.attack-buffed:enabled { #button-attack.attack-buffed:enabled {
border: 2px solid var(--heal-color) !important; border: 2px solid var(--heal-color) !important;
box-shadow: 0 0 10px 2px rgba(144, 238, 144, 0.6), 0 3px 6px rgba(0,0,0,0.5); box-shadow: 0 0 10px 2px rgba(144, 238, 144, 0.6), 0 3px 6px rgba(0,0,0,0.5);
background: linear-gradient(145deg, #70c070, #5a9a5a); background: linear-gradient(145deg, #70c070, #5a9a5a); /* Зеленый градиент */
transform: translateY(-1px); transform: translateY(-1px); /* Небольшой подъем */
} }
.ability-list { flex-grow: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; } .ability-list { flex-grow: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
@ -437,9 +464,9 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
display: grid; grid-template-columns: repeat(auto-fit, minmax(75px, 1fr)); display: grid; grid-template-columns: repeat(auto-fit, minmax(75px, 1fr));
gap: 8px; padding: 8px; background-color: rgba(0,0,0,0.2); gap: 8px; padding: 8px; background-color: rgba(0,0,0,0.2);
border-radius: 4px; overflow-y: auto; border: 1px solid rgba(0,0,0,0.3); border-radius: 4px; overflow-y: auto; border: 1px solid rgba(0,0,0,0.3);
flex-grow: 1; position: relative; flex-grow: 1; position: relative; /* Для псевдоэлемента, если нужен */
} }
.abilities-grid::after { content: ''; display: block; height: 10px; width: 100%; } .abilities-grid::after { content: ''; display: block; height: 10px; width: 100%; } /* Отступ снизу для скролла */
.abilities-grid .placeholder-text { .abilities-grid .placeholder-text {
grid-column: 1 / -1; text-align: center; color: var(--text-muted); grid-column: 1 / -1; text-align: center; color: var(--text-muted);
align-self: center; font-size: 0.9em; padding: 15px 0; align-self: center; font-size: 0.9em; padding: 15px 0;
@ -458,7 +485,7 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
.ability-button .ability-desc { .ability-button .ability-desc {
font-size: 0.65em; font-weight: normal; color: #aaccce; opacity: 0.8; font-size: 0.65em; font-weight: normal; color: #aaccce; opacity: 0.8;
text-shadow: none; max-height: 2em; overflow: hidden; width: 95%; text-shadow: none; max-height: 2em; overflow: hidden; width: 95%;
display: block; margin-top: auto; display: block; margin-top: auto; /* Прижимает описание вниз */
} }
.ability-button:hover:enabled { .ability-button:hover:enabled {
transform: scale(1.03) translateY(-1px); background: var(--button-ability-hover-bg); transform: scale(1.03) translateY(-1px); background: var(--button-ability-hover-bg);
@ -470,27 +497,40 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
box-shadow: inset 0 1px 2px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.3); box-shadow: inset 0 1px 2px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.3);
filter: brightness(0.9); filter: brightness(0.9);
} }
/* Общие стили для неактивных кнопок (базовых и способностей) */
.ability-button:disabled, .action-button.basic:disabled { .ability-button:disabled, .action-button.basic:disabled {
background: var(--button-disabled-bg) !important; border-color: transparent !important; background: var(--button-disabled-bg) !important; border-color: transparent !important;
color: var(--button-disabled-text) !important; cursor: not-allowed !important; color: var(--button-disabled-text) !important; cursor: not-allowed !important;
transform: none !important; box-shadow: inset 0 1px 3px rgba(0,0,0,0.4) !important; transform: none !important; box-shadow: inset 0 1px 3px rgba(0,0,0,0.4) !important;
opacity: 0.6; text-shadow: none !important; filter: grayscale(50%) !important; opacity: 0.6; text-shadow: none !important; filter: grayscale(50%) !important;
} }
/* Нехватка ресурса */
.ability-button.not-enough-resource:not(:disabled) { .ability-button.not-enough-resource:not(:disabled) {
border: 2px dashed var(--damage-color) !important; border: 2px dashed var(--damage-color) !important;
box-shadow: inset 0 0 8px rgba(255, 80, 80, 0.3), 0 3px 6px rgba(0,0,0,0.4) !important; box-shadow: inset 0 0 8px rgba(255, 80, 80, 0.3), 0 3px 6px rgba(0,0,0,0.4) !important;
animation: pulse-red-border 1s infinite ease-in-out; animation: pulse-red-border 1s infinite ease-in-out;
} }
.ability-button.buff-is-active:disabled { filter: grayscale(80%) brightness(0.8) !important; opacity: 0.5 !important; } /* Бафф уже активен (для кнопки способности) */
.ability-button.buff-is-active:disabled { /* :disabled потому что кнопка будет задизейблена JS */
filter: grayscale(80%) brightness(0.8) !important; opacity: 0.5 !important;
}
@keyframes pulse-red-border { @keyframes pulse-red-border {
0%, 100% { border-color: var(--damage-color); } 0%, 100% { border-color: var(--damage-color); }
50% { border-color: #ffb3b3; } 50% { border-color: #ffb3b3; }
} }
.ability-button.is-on-cooldown, .ability-button.is-silenced { filter: grayscale(70%) brightness(0.8) !important; } /* Состояния на КД или под безмолвием */
.ability-button.is-on-cooldown, .ability-button.is-silenced {
filter: grayscale(70%) brightness(0.8) !important;
}
.ability-button.is-on-cooldown .ability-name, .ability-button.is-on-cooldown .ability-desc, .ability-button.is-on-cooldown .ability-name, .ability-button.is-on-cooldown .ability-desc,
.ability-button.is-silenced .ability-name, .ability-button.is-silenced .ability-desc { opacity: 0.6; } .ability-button.is-silenced .ability-name, .ability-button.is-silenced .ability-desc {
.ability-button.is-on-cooldown .ability-desc, .ability-button.is-silenced .ability-desc { display: none; } opacity: 0.6;
}
/* Скрываем описание для КД и безмолвия, чтобы освободить место для таймера/иконки */
.ability-button.is-on-cooldown .ability-desc, .ability-button.is-silenced .ability-desc {
display: none;
}
.ability-cooldown-display { /* Также используется для отображения безмолвия */ .ability-cooldown-display { /* Также используется для отображения безмолвия */
position: absolute; bottom: 5px; left: 0; width: 100%; text-align: center; position: absolute; bottom: 5px; left: 0; width: 100%; text-align: center;
@ -499,16 +539,7 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
display: none; line-height: 1; display: none; line-height: 1;
} }
.ability-button.is-on-cooldown .ability-cooldown-display, .ability-button.is-on-cooldown .ability-cooldown-display,
.ability-button.is-silenced .ability-cooldown-display { display: block !important; } .ability-button.is-silenced .ability-cooldown-display { display: block !important; } /* !important для переопределения display:none */
.ability-button.is-silenced { /* Дополнительные стили для безмолвия, если нужны */
/* background: repeating-linear-gradient( -45deg, var(--button-disabled-bg), var(--button-disabled-bg) 8px, #4d3f50 8px, #4d3f50 16px ) !important; */
/* border-color: #6a5f6b !important; */
}
.ability-button.is-silenced::after { /* Иконка замка (опционально) */
/* content: '\f023'; font-family: 'Font Awesome 6 Free'; font-weight: 900; */
/* ... (стили для иконки замка, если используется) ... */
}
/* --- Панель Лога --- */ /* --- Панель Лога --- */
@ -520,22 +551,23 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
#log-list { #log-list {
list-style: none; flex-grow: 1; overflow-y: auto; background-color: var(--log-bg); list-style: none; flex-grow: 1; overflow-y: auto; background-color: var(--log-bg);
border: 1px solid var(--log-border); font-size: 0.85em; border-radius: 6px; border: 1px solid var(--log-border); font-size: 0.85em; border-radius: 6px;
color: var(--log-text); padding: 10px; min-height: 0; color: var(--log-text); padding: 10px; min-height: 0; /* Для корректной работы flex-grow и overflow */
} }
#log-list li { #log-list li {
padding: 4px 8px; border-bottom: 1px solid rgba(74, 80, 114, 0.5); padding: 4px 8px; border-bottom: 1px solid rgba(74, 80, 114, 0.5); /* Полупрозрачная граница */
line-height: 1.35; word-break: break-word; transition: background-color 0.3s; line-height: 1.35; word-break: break-word; transition: background-color 0.3s;
} }
#log-list li:last-child { border-bottom: none; } #log-list li:last-child { border-bottom: none; }
#log-list li:hover { background-color: rgba(255,255,255,0.03); } #log-list li:hover { background-color: rgba(255,255,255,0.03); } /* Легкая подсветка при наведении */
/* Стили для типов логов */
.log-damage { color: var(--damage-color); font-weight: 500; } .log-damage { color: var(--damage-color); font-weight: 500; }
.log-heal { color: var(--heal-color); font-weight: 500; } .log-heal { color: var(--heal-color); font-weight: 500; }
.log-block { color: var(--block-color); font-style: italic; } .log-block { color: var(--block-color); font-style: italic; }
.log-info { color: #b0c4de; } .log-info { color: #b0c4de; } /* Светло-голубой для общей информации */
.log-turn { .log-turn {
font-weight: bold; color: var(--turn-color); margin-top: 6px; font-weight: bold; color: var(--turn-color); margin-top: 6px;
border-top: 1px solid rgba(255, 215, 0, 0.3); padding-top: 6px; border-top: 1px solid rgba(255, 215, 0, 0.3); padding-top: 6px;
font-size: 1.05em; display: block; font-size: 1.05em; display: block; /* Чтобы занимал всю строку */
} }
.log-system { font-weight: bold; color: var(--system-color); font-style: italic; opacity: 0.8; } .log-system { font-weight: bold; color: var(--system-color); font-style: italic; opacity: 0.8; }
.log-effect { font-style: italic; color: var(--effect-color); } .log-effect { font-style: italic; color: var(--effect-color); }
@ -548,8 +580,9 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
align-items: center; z-index: 1000; backdrop-filter: blur(4px) brightness(0.7); align-items: center; z-index: 1000; backdrop-filter: blur(4px) brightness(0.7);
opacity: 0; pointer-events: none; transition: opacity 0.4s ease-out; opacity: 0; pointer-events: none; transition: opacity 0.4s ease-out;
} }
.modal.hidden { display: none !important; } .modal.hidden { display: none !important; } /* Используем !important для переопределения display: flex */
.modal:not(.hidden) { opacity: 1; pointer-events: auto; } .modal:not(.hidden) { opacity: 1; pointer-events: auto; }
.modal-content { .modal-content {
background: var(--modal-content-bg); padding: 40px 50px; border-radius: 10px; background: var(--modal-content-bg); padding: 40px 50px; border-radius: 10px;
text-align: center; border: 1px solid var(--panel-border); text-align: center; border: 1px solid var(--panel-border);
@ -560,41 +593,73 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
} }
.modal:not(.hidden) .modal-content { transform: scale(1) translateY(0); opacity: 1; } .modal:not(.hidden) .modal-content { transform: scale(1) translateY(0); opacity: 1; }
.modal-content h2#result-message { margin-bottom: 25px; font-family: var(--font-fancy); font-size: 2.5em; line-height: 1.2; } .modal-content h2#result-message { margin-bottom: 25px; font-family: var(--font-fancy); font-size: 2.5em; line-height: 1.2; }
#restart-game-button {
padding: 12px 30px; font-size: 1.1em; cursor: pointer; background: var(--button-bg); /* Стили для кнопки "В меню выбора игры" - ОБЩИЙ КЛАСС */
color: var(--button-text); border: 1px solid rgba(0,0,0,0.3); border-radius: 6px; .modal-action-button {
margin-top: 20px; font-weight: bold; text-transform: uppercase; padding: 12px 30px;
letter-spacing: 1px; transition: all 0.2s ease; box-shadow: 0 4px 8px rgba(0,0,0,0.4); font-size: 1.1em;
cursor: pointer;
background: var(--button-bg);
color: var(--button-text);
border: 1px solid rgba(0,0,0,0.3);
border-radius: 6px;
margin-top: 20px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.2s ease;
box-shadow: 0 4px 8px rgba(0,0,0,0.4);
outline: none; outline: none;
} }
#restart-game-button:hover:enabled { .modal-action-button:hover:enabled {
background: var(--button-hover-bg); transform: scale(1.05) translateY(-1px); background: var(--button-hover-bg);
transform: scale(1.05) translateY(-1px);
box-shadow: 0 6px 12px rgba(0,0,0,0.5); box-shadow: 0 6px 12px rgba(0,0,0,0.5);
} }
#restart-game-button:active:enabled { transform: scale(1) translateY(0); box-shadow: 0 3px 6px rgba(0,0,0,0.4); } .modal-action-button:active:enabled {
#restart-game-button:disabled { background: var(--button-disabled-bg); color: var(--button-disabled-text); cursor: not-allowed; opacity: 0.7; } transform: scale(1) translateY(0);
#restart-game-button i { margin-right: 8px; } box-shadow: 0 3px 6px rgba(0,0,0,0.4);
}
.modal-action-button:disabled {
background: var(--button-disabled-bg);
color: var(--button-disabled-text);
cursor: not-allowed;
opacity: 0.7;
}
.modal-action-button i {
margin-right: 8px;
}
/* --- Анимации --- */ /* --- Анимации --- */
@keyframes flash-effect { @keyframes flash-effect {
0%, 100% { box-shadow: var(--initial-box-shadow, inherit); border-color: var(--initial-border-color, inherit); transform: scale(1); } 0%, 100% {
box-shadow: var(--initial-box-shadow, 0 0 15px rgba(0, 0, 0, 0.4), inset 0 0 10px rgba(0, 0, 0, 0.3)); /* Возвращаем к исходной тени панели */
border-color: var(--initial-border-color, var(--panel-border)); /* Возвращаем к исходному цвету рамки */
transform: scale(1);
}
50% { 50% {
box-shadow: 0 0 25px 10px var(--flash-color-outer, rgba(255, 255, 255, 0.7)), box-shadow: 0 0 25px 10px var(--flash-color-outer, rgba(255, 255, 255, 0.7)),
inset 0 0 15px var(--flash-color-inner, rgba(255, 255, 255, 0.4)), inset 0 0 15px var(--flash-color-inner, rgba(255, 255, 255, 0.4)),
0 0 15px rgba(0, 0, 0, 0.4); 0 0 15px rgba(0, 0, 0, 0.4); /* Сохраняем базовую тень */
border-color: var(--flash-border-color, #ffffff); border-color: var(--flash-border-color, #ffffff);
transform: scale(1.005); transform: scale(1.005); /* Легкое увеличение */
} }
} }
/* Применение анимации каста к панели игрока (добавляется через JS) */ /* Применение анимации каста к панели игрока (добавляется через JS) */
#player-panel[class*="is-casting-"] { animation: flash-effect var(--cast-duration) ease-out; } #player-panel[class*="is-casting-"] { /* Селектор для любого класса, начинающегося с is-casting- */
animation: flash-effect var(--cast-duration) ease-out;
/* Сохраняем исходные значения для возврата в keyframes */
--initial-box-shadow: 0 0 15px rgba(0, 0, 0, 0.4), inset 0 0 10px rgba(0, 0, 0, 0.3);
/* --initial-border-color: var(--panel-border); /* JS должен будет установить правильный исходный цвет рамки */
/* Или, если мы знаем, что для player-panel это всегда --accent-player */
--initial-border-color: var(--accent-player); /* Предполагая, что #player-panel всегда Елена */
}
/* Цвета для разных кастов (переменные для keyframes) - могут быть адаптированы или расширены */ /* Цвета для разных кастов (переменные для keyframes) - могут быть адаптированы или расширены */
#player-panel.is-casting-heal { --flash-color-outer: rgba(144, 238, 144, 0.7); --flash-color-inner: rgba(144, 238, 144, 0.4); --flash-border-color: var(--heal-color); } #player-panel.is-casting-heal { --flash-color-outer: rgba(144, 238, 144, 0.7); --flash-color-inner: rgba(144, 238, 144, 0.4); --flash-border-color: var(--heal-color); }
#player-panel.is-casting-fireball { --flash-color-outer: rgba(255, 100, 100, 0.7); --flash-color-inner: rgba(255, 100, 100, 0.4); --flash-border-color: var(--damage-color); } #player-panel.is-casting-fireball { --flash-color-outer: rgba(255, 100, 100, 0.7); --flash-color-inner: rgba(255, 100, 100, 0.4); --flash-border-color: var(--damage-color); }
#player-panel.is-casting-shadowBolt { --flash-color-outer: rgba(138, 43, 226, 0.6); --flash-color-inner: rgba(138, 43, 226, 0.3); --flash-border-color: var(--dark-energy-color); }
/* ... Добавить для других способностей Елены и Альмагест, если нужна анимация каста ... */ /* ... Добавить для других способностей Елены и Альмагест, если нужна анимация каста ... */
/* Например, для Теневого Сгустка Альмагест (если она в слоте игрока) */
/* #player-panel.is-casting-shadowBolt { --flash-color-outer: rgba(138, 43, 226, 0.6); --flash-color-inner: rgba(138, 43, 226, 0.3); --flash-border-color: var(--dark-energy-color); } */
@keyframes shake-opponent { @keyframes shake-opponent {
@ -604,13 +669,24 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
} }
#opponent-panel.is-shaking { #opponent-panel.is-shaking {
animation: shake-opponent var(--shake-duration) cubic-bezier(.36,.07,.19,.97) both; animation: shake-opponent var(--shake-duration) cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0); backface-visibility: hidden; perspective: 1000px; /* Дополнительные свойства для лучшей производительности анимации */
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
} }
#opponent-panel.dissolving { #opponent-panel.dissolving {
opacity: 0; transform: scale(0.9) translateY(20px); /* opacity: 0; */ /* Управляется через transition */
/* transform: scale(0.9) translateY(20px); */ /* Управляется через transition */
transition: opacity var(--dissolve-duration) ease-in, transform var(--dissolve-duration) ease-in; transition: opacity var(--dissolve-duration) ease-in, transform var(--dissolve-duration) ease-in;
pointer-events: none; pointer-events: none; /* Чтобы нельзя было взаимодействовать во время исчезновения */
} }
/* Состояние после завершения анимации dissolving, если класс остается */
#opponent-panel.dissolved-state { /* Этот класс можно добавлять по завершению анимации через JS, если нужно */
opacity: 0;
transform: scale(0.9) translateY(20px);
}
@keyframes shake-short { @keyframes shake-short {
0%, 100% { transform: translateX(0); } 0%, 100% { transform: translateX(0); }
25% { transform: translateX(-3px); } 25% { transform: translateX(-3px); }
@ -622,20 +698,38 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
/* --- Отзывчивость (Медиа-запросы) --- */ /* --- Отзывчивость (Медиа-запросы) --- */
@media (max-width: 900px) { @media (max-width: 900px) {
body { height: auto; overflow-y: auto; padding: 5px 0; font-size: 15px; justify-content: flex-start; } body {
height: auto; /* Позволяем body расти по контенту */
overflow-y: auto; /* Включаем прокрутку для body, если нужно */
padding: 5px 0; /* Уменьшаем отступы */
font-size: 15px;
justify-content: flex-start; /* Чтобы контент не пытался всегда быть по центру */
}
.auth-game-setup-wrapper {
max-height: none; /* Убираем ограничение по высоте, body будет скроллиться */
}
.game-wrapper { padding: 5px; gap: 5px; height: auto; } .game-wrapper { padding: 5px; gap: 5px; height: auto; }
.game-header h1 { font-size: 1.5em; } .game-header h1 { font-size: 1.5em; }
.battle-arena-container { flex-direction: column; height: auto; overflow: visible; } .battle-arena-container { flex-direction: column; height: auto; overflow: visible; }
.player-column, .opponent-column { width: 100%; height: auto; overflow: visible; } .player-column, .opponent-column { width: 100%; height: auto; overflow: visible; }
.fighter-panel, .controls-panel-new, .battle-log-new { min-height: auto; height: auto; padding: 10px; flex-grow: 0; flex-shrink: 1; }
.controls-panel-new { min-height: 200px; } .fighter-panel, .controls-panel-new, .battle-log-new {
.battle-log-new { height: auto; min-height: 150px; } min-height: auto; /* Убираем min-height, пусть контент определяет */
#log-list { max-height: 200px; } height: auto; /* Высота по контенту */
padding: 10px;
flex-grow: 0; /* Панели не должны растягиваться */
flex-shrink: 1; /* Но могут сжиматься, если нужно */
}
.controls-panel-new { min-height: 200px; /* Сохраняем для удобства клика */ }
.battle-log-new { height: auto; min-height: 150px; } /* Лог тоже по контенту */
#log-list { max-height: 200px; } /* Ограничиваем высоту списка логов */
.abilities-grid { max-height: none; overflow-y: visible; padding-bottom: 8px; } .abilities-grid { max-height: none; overflow-y: visible; padding-bottom: 8px; }
.abilities-grid::after { display: none; } .abilities-grid::after { display: none; } /* Убираем псевдоэлемент, т.к. нет скролла */
.ability-list, .controls-layout { overflow: visible; } .ability-list, .controls-layout { overflow: visible; }
.fighter-name { font-size: 1.3em; } .fighter-name { font-size: 1.3em; }
.panel-content { margin-top: 10px; } .panel-content { margin-top: 10px; } /* Восстанавливаем отступ, если был убран */
.stat-bar-container .bar-icon { font-size: 1.2em; } .stat-bar-container .bar-icon { font-size: 1.2em; }
.bar { height: 18px; } .bar { height: 18px; }
.effects-area, .effect { font-size: 0.85em; } .effects-area, .effect { font-size: 0.85em; }
@ -645,15 +739,18 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
.ability-button { font-size: 0.75em; padding: 5px; } .ability-button { font-size: 0.75em; padding: 5px; }
.ability-button .ability-name { margin-bottom: 2px; } .ability-button .ability-name { margin-bottom: 2px; }
.ability-button .ability-desc { font-size: 0.65em; } .ability-button .ability-desc { font-size: 0.65em; }
.modal-content { padding: 25px 30px; width: 90%; max-width: 400px; } .modal-content { padding: 25px 30px; width: 90%; max-width: 400px; }
.modal-content h2#result-message { font-size: 1.8em; } .modal-content h2#result-message { font-size: 1.8em; }
#restart-game-button { font-size: 1em; padding: 10px 20px; } .modal-action-button { font-size: 1em; padding: 10px 20px; } /* Адаптируем кнопку в модалке */
/* Стили для auth-game-setup на планшетах */
#game-setup { max-width: 95%; padding: 15px; } #game-setup { max-width: 95%; padding: 15px; }
#game-setup h2 { font-size: 1.6em; } #game-setup h3 { font-size: 1.1em; } #game-setup h2 { font-size: 1.6em; } #game-setup h3 { font-size: 1.1em; }
#game-setup button { padding: 8px 12px; font-size: 0.9em; } #game-setup button { padding: 8px 12px; font-size: 0.9em; }
#game-setup input[type="text"] { width: calc(100% - 90px); max-width: 200px; padding: 8px;} #game-setup input[type="text"] { width: calc(100% - 90px); max-width: 200px; padding: 8px;}
#available-games-list { max-height: 180px; } #available-games-list { max-height: 180px; }
.character-selection label { margin: 0 10px; font-size: 1em; } /* Чуть меньше отступы для выбора на планшетах */ .character-selection label { margin: 0 10px; font-size: 1em; }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
@ -663,23 +760,34 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
.abilities-grid { grid-template-columns: repeat(auto-fit, minmax(65px, 1fr)); gap: 5px; padding: 5px; } .abilities-grid { grid-template-columns: repeat(auto-fit, minmax(65px, 1fr)); gap: 5px; padding: 5px; }
.ability-button { font-size: 0.7em; padding: 4px; } .ability-button { font-size: 0.7em; padding: 4px; }
.ability-button .ability-name { margin-bottom: 1px; } .ability-button .ability-name { margin-bottom: 1px; }
.ability-button .ability-desc { display: none; } .ability-button .ability-desc { display: none; } /* Скрываем описание на маленьких экранах */
#log-list { font-size: 0.8em; max-height: 150px; } #log-list { font-size: 0.8em; max-height: 150px; }
.modal-content { padding: 20px; } .modal-content { padding: 20px; }
.modal-content h2#result-message { font-size: 1.6em; } .modal-content h2#result-message { font-size: 1.6em; }
#restart-game-button { font-size: 0.9em; padding: 8px 16px; } .modal-action-button { font-size: 0.9em; padding: 8px 16px; } /* Адаптируем кнопку в модалке */
.stat-bar-container .bar-icon { font-size: 1.1em; } .stat-bar-container .bar-icon { font-size: 1.1em; }
.bar { height: 16px; } .bar-text { font-size: 0.7em; } .bar { height: 16px; } .bar-text { font-size: 0.7em; }
.effects-area, .effect { font-size: 0.8em; } .effects-area, .effect { font-size: 0.8em; }
/* Стили для auth-game-setup на мобильных */
.auth-game-setup-wrapper { padding: 15px; }
#game-setup { padding: 10px; } #game-setup { padding: 10px; }
#game-setup h2 { font-size: 1.4em; } #game-setup h2 { font-size: 1.4em; }
#game-setup button { padding: 7px 10px; font-size: 0.85em; margin: 5px; } #game-setup button { padding: 7px 10px; font-size: 0.85em; margin: 5px; }
#game-setup input[type="text"] { width: 100%; max-width: none; margin-bottom: 10px; } #game-setup input[type="text"] { width: 100%; max-width: none; margin-bottom: 10px; }
#game-setup div > button, #game-setup div > input[type="text"] { display: block; width: 100%; margin-left:0; margin-right:0; } /* Делаем кнопки и инпуты в game-setup блочными для лучшего отображения на мобильных */
#game-setup div > input[type="text"] + button { margin-top: 5px;} #game-setup div > button,
#game-setup div > input[type="text"] {
display: block; width: 100%; margin-left:0; margin-right:0;
}
#game-setup div > input[type="text"] + button { margin-top: 5px;} /* Отступ для кнопки после инпута */
#available-games-list { max-height: 120px; } #available-games-list { max-height: 120px; }
#available-games-list li button { font-size: 0.75em; padding: 5px 8px;} #available-games-list li button { font-size: 0.75em; padding: 5px 8px;}
.character-selection { padding: 10px; } .character-selection { padding: 10px; }
.character-selection label { margin: 0 5px; font-size: 0.9em; display: block; margin-bottom: 5px; } /* Лейблы в столбик */ .character-selection label { margin: 0 5px 5px 5px; font-size: 0.9em; display: block; } /* Лейблы в столбик */
.character-selection label i { margin-right: 5px;} .character-selection label i { margin-right: 5px;}
} }

View File

@ -7,22 +7,22 @@ class GameInstance {
constructor(gameId, io, mode = 'ai') { constructor(gameId, io, mode = 'ai') {
this.id = gameId; this.id = gameId;
this.io = io; this.io = io;
this.mode = mode; this.mode = mode; // 'ai' или 'pvp'
this.players = {}; // { socket.id: { id: 'player'/'opponent', socket: socketObject, chosenCharacterKey?: 'elena'/'almagest' } } this.players = {}; // { socket.id: { id: 'player'/'opponent', socket: socketObject, chosenCharacterKey?: 'elena'/'almagest' } }
this.playerSockets = {}; // { 'player': socketObject, 'opponent': socketObject } this.playerSockets = {}; // { 'player': socketObject, 'opponent': socketObject } - для быстрого доступа к сокету по роли
this.playerCount = 0; this.playerCount = 0;
this.gameState = null; this.gameState = null; // Хранит текущее состояние игры (HP, ресурсы, эффекты, чей ход и т.д.)
this.aiOpponent = (mode === 'ai'); this.aiOpponent = (mode === 'ai');
this.logBuffer = []; this.logBuffer = []; // Буфер для сообщений лога боя
this.restartVotes = new Set(); // this.restartVotes = new Set(); // Удалено, так как рестарт той же сессии убран
this.playerCharacterKey = null; // Ключи персонажей для текущей игры
this.opponentCharacterKey = null; this.playerCharacterKey = null; // Ключ персонажа в слоте 'player' (Елена или Альмагест)
this.ownerUserId = null; // userId создателя игры this.opponentCharacterKey = null; // Ключ персонажа в слоте 'opponent' (Балард, Елена или Альмагест)
this.ownerUserId = null; // userId создателя игры (важно для PvP ожидающих игр)
} }
addPlayer(socket, chosenCharacterKey = 'elena') { addPlayer(socket, chosenCharacterKey = 'elena') {
// Проверка, не пытается ли игрок присоединиться к игре, в которой он уже есть
if (this.players[socket.id]) { if (this.players[socket.id]) {
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре.' }); socket.emit('gameError', { message: 'Вы уже находитесь в этой игре.' });
console.warn(`[Game ${this.id}] Игрок ${socket.id} попытался присоединиться к игре, в которой уже состоит.`); console.warn(`[Game ${this.id}] Игрок ${socket.id} попытался присоединиться к игре, в которой уже состоит.`);
@ -34,29 +34,30 @@ class GameInstance {
return false; return false;
} }
let assignedPlayerId; let assignedPlayerId; // 'player' или 'opponent' (технический ID слота)
let actualCharacterKey; let actualCharacterKey; // 'elena', 'almagest', 'balard'
if (this.mode === 'ai') { if (this.mode === 'ai') {
if (this.playerCount > 0) { // В AI игру может войти только один реальный игрок if (this.playerCount > 0) {
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' }); socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
return false; return false;
} }
assignedPlayerId = GAME_CONFIG.PLAYER_ID; assignedPlayerId = GAME_CONFIG.PLAYER_ID;
actualCharacterKey = 'elena'; actualCharacterKey = 'elena'; // В AI режиме игрок всегда Елена
if (socket.userData?.userId) { if (socket.userData?.userId) {
this.ownerUserId = socket.userData.userId; this.ownerUserId = socket.userData.userId; // Запоминаем создателя
} }
} else { // PvP } else { // PvP режим
if (this.playerCount === 0) { // Первый игрок PvP if (this.playerCount === 0) { // Первый игрок в PvP
assignedPlayerId = GAME_CONFIG.PLAYER_ID; assignedPlayerId = GAME_CONFIG.PLAYER_ID;
actualCharacterKey = (chosenCharacterKey === 'almagest') ? 'almagest' : 'elena'; actualCharacterKey = (chosenCharacterKey === 'almagest') ? 'almagest' : 'elena';
if (socket.userData?.userId) { if (socket.userData?.userId) {
this.ownerUserId = socket.userData.userId; this.ownerUserId = socket.userData.userId; // Запоминаем создателя
} }
} else { // Второй игрок PvP } else { // Второй игрок в PvP
assignedPlayerId = GAME_CONFIG.OPPONENT_ID; assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
const firstPlayerInfo = Object.values(this.players)[0]; // Информация о первом игроке const firstPlayerInfo = Object.values(this.players)[0];
// Второй игрок автоматически получает "зеркального" персонажа
actualCharacterKey = (firstPlayerInfo.chosenCharacterKey === 'elena') ? 'almagest' : 'elena'; actualCharacterKey = (firstPlayerInfo.chosenCharacterKey === 'elena') ? 'almagest' : 'elena';
} }
} }
@ -64,15 +65,14 @@ class GameInstance {
this.players[socket.id] = { this.players[socket.id] = {
id: assignedPlayerId, id: assignedPlayerId,
socket: socket, socket: socket,
chosenCharacterKey: actualCharacterKey chosenCharacterKey: actualCharacterKey // Запоминаем ключ выбранного/назначенного персонажа
}; };
this.playerSockets[assignedPlayerId] = socket; this.playerSockets[assignedPlayerId] = socket;
this.playerCount++; this.playerCount++;
socket.join(this.id); socket.join(this.id); // Присоединяем сокет к комнате игры
const characterData = this._getCharacterBaseData(actualCharacterKey); const characterData = this._getCharacterBaseData(actualCharacterKey);
console.log(`[Game ${this.id}] Игрок ${socket.id} (userId: ${socket.userData?.userId || 'N/A'}) (${characterData?.name || 'Неизвестно'}) присоединился как ${assignedPlayerId} (персонаж: ${actualCharacterKey}). Всего игроков: ${this.playerCount}. Owner: ${this.ownerUserId || 'N/A'}`); console.log(`[Game ${this.id}] Игрок ${socket.userData?.username || socket.id} (userId: ${socket.userData?.userId || 'N/A'}) (${characterData?.name || 'Неизвестно'}) присоединился как ${assignedPlayerId} (персонаж: ${actualCharacterKey}). Всего игроков: ${this.playerCount}. Owner: ${this.ownerUserId || 'N/A'}`);
if (this.mode === 'pvp' && this.playerCount < 2) { if (this.mode === 'pvp' && this.playerCount < 2) {
socket.emit('waitingForOpponent'); socket.emit('waitingForOpponent');
@ -80,12 +80,12 @@ class GameInstance {
// Если игра готова к старту (2 игрока в PvP, или 1 в AI) // Если игра готова к старту (2 игрока в PvP, или 1 в AI)
if ((this.mode === 'ai' && this.playerCount === 1) || (this.mode === 'pvp' && this.playerCount === 2)) { if ((this.mode === 'ai' && this.playerCount === 1) || (this.mode === 'pvp' && this.playerCount === 2)) {
this.initializeGame(); this.initializeGame(); // Инициализируем состояние игры
if (this.gameState) { if (this.gameState) {
this.startGame(); this.startGame(); // Запускаем игру
} else { } else {
// Ошибка инициализации уже должна была быть залогирована и отправлена клиенту console.error(`[Game ${this.id}] Не удалось запустить игру: gameState не был инициализирован.`);
console.error(`[Game ${this.id}] Не удалось запустить игру, так как gameState не был инициализирован.`); // Ошибка должна была быть отправлена клиенту из initializeGame
} }
} }
return true; return true;
@ -94,20 +94,23 @@ class GameInstance {
removePlayer(socketId) { removePlayer(socketId) {
const playerInfo = this.players[socketId]; const playerInfo = this.players[socketId];
if (playerInfo) { if (playerInfo) {
const playerRole = playerInfo.id; const playerRole = playerInfo.id; // 'player' or 'opponent'
let characterKeyToRemove = playerInfo.chosenCharacterKey; let characterKeyOfLeavingPlayer = playerInfo.chosenCharacterKey;
const userIdOfLeavingPlayer = playerInfo.socket?.userData?.userId; const userIdOfLeavingPlayer = playerInfo.socket?.userData?.userId;
const usernameOfLeavingPlayer = playerInfo.socket?.userData?.username || socketId;
if (this.mode === 'ai' && playerRole === GAME_CONFIG.OPPONENT_ID) { // AI оппонент не имеет chosenCharacterKey в this.players // Для AI оппонента, у него нет записи в this.players, но его ключ 'balard'
characterKeyToRemove = 'balard'; if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) { // Если уходит игрок из AI игры
} else if (!characterKeyToRemove && this.gameState) { // Фоллбэк, если ключ не был в playerInfo // AI оппонент не имеет 'chosenCharacterKey' в this.players, так как он не сокет
characterKeyToRemove = (playerRole === GAME_CONFIG.PLAYER_ID) } else if (!characterKeyOfLeavingPlayer && this.gameState) {
// Фоллбэк, если ключ не был в playerInfo (маловероятно для реальных игроков)
characterKeyOfLeavingPlayer = (playerRole === GAME_CONFIG.PLAYER_ID)
? this.gameState.player?.characterKey ? this.gameState.player?.characterKey
: this.gameState.opponent?.characterKey; : this.gameState.opponent?.characterKey;
} }
const characterData = this._getCharacterBaseData(characterKeyToRemove); const characterData = this._getCharacterBaseData(characterKeyOfLeavingPlayer);
console.log(`[Game ${this.id}] Игрок ${socketId} (userId: ${userIdOfLeavingPlayer || 'N/A'}) (${characterData?.name || 'Неизвестно'}, роль: ${playerRole}, персонаж: ${characterKeyToRemove || 'N/A'}) покинул игру.`); console.log(`[Game ${this.id}] Игрок ${usernameOfLeavingPlayer} (socket: ${socketId}, userId: ${userIdOfLeavingPlayer || 'N/A'}) (${characterData?.name || 'Неизвестно'}, роль: ${playerRole}, персонаж: ${characterKeyOfLeavingPlayer || 'N/A'}) покинул игру.`);
if (this.playerSockets[playerRole] && this.playerSockets[playerRole].id === socketId) { if (this.playerSockets[playerRole] && this.playerSockets[playerRole].id === socketId) {
delete this.playerSockets[playerRole]; delete this.playerSockets[playerRole];
@ -115,17 +118,19 @@ class GameInstance {
delete this.players[socketId]; delete this.players[socketId];
this.playerCount--; this.playerCount--;
// Если создатель PvP игры вышел, и остался один игрок, обновляем ownerUserId
if (this.mode === 'pvp' && this.ownerUserId === userIdOfLeavingPlayer && this.playerCount === 1) { if (this.mode === 'pvp' && this.ownerUserId === userIdOfLeavingPlayer && this.playerCount === 1) {
const remainingPlayerSocketId = Object.keys(this.players)[0]; const remainingPlayerSocketId = Object.keys(this.players)[0];
const remainingPlayerSocket = this.players[remainingPlayerSocketId]?.socket; const remainingPlayerSocket = this.players[remainingPlayerSocketId]?.socket;
this.ownerUserId = remainingPlayerSocket?.userData?.userId || null; this.ownerUserId = remainingPlayerSocket?.userData?.userId || null; // Новый владелец - userId оставшегося или null
console.log(`[Game ${this.id}] Owner left. New potential owner for pending game: ${this.ownerUserId || remainingPlayerSocketId}`); console.log(`[Game ${this.id}] Owner left PvP game. New potential owner for pending game: ${this.ownerUserId || remainingPlayerSocketId}`);
} else if (this.playerCount === 0) { } else if (this.playerCount === 0) {
this.ownerUserId = null; this.ownerUserId = null; // Если игра пуста, нет владельца
} }
// Если игра была активна, завершаем ее из-за дисконнекта
if (this.gameState && !this.gameState.isGameOver) { if (this.gameState && !this.gameState.isGameOver) {
this.endGameDueToDisconnect(playerRole, characterKeyToRemove); this.endGameDueToDisconnect(playerRole, characterKeyOfLeavingPlayer || (playerRole === GAME_CONFIG.PLAYER_ID ? this.playerCharacterKey : this.opponentCharacterKey) );
} }
} }
} }
@ -135,12 +140,15 @@ class GameInstance {
this.gameState.isGameOver = true; this.gameState.isGameOver = true;
const winnerRole = disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const winnerRole = disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const disconnectedCharacterData = this._getCharacterBaseData(disconnectedCharacterKey); const disconnectedCharacterData = this._getCharacterBaseData(disconnectedCharacterKey);
const winnerCharacterKey = (winnerRole === GAME_CONFIG.PLAYER_ID) ? this.playerCharacterKey : this.opponentCharacterKey;
const winnerCharacterData = this._getCharacterBaseData(winnerCharacterKey);
this.addToLog(`Игрок ${disconnectedCharacterData?.name || 'Неизвестный'} покинул игру.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
this.io.to(this.id).emit('opponentDisconnected', { disconnectedPlayerId: disconnectedPlayerRole }); this.addToLog(`Игрок ${disconnectedCharacterData?.name || 'Неизвестный'} покинул игру. Победа присуждается ${winnerCharacterData?.name || winnerRole}!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
this.io.to(this.id).emit('opponentDisconnected', { disconnectedPlayerId: disconnectedPlayerRole, disconnectedCharacterName: disconnectedCharacterData?.name });
this.io.to(this.id).emit('gameOver', { this.io.to(this.id).emit('gameOver', {
winnerId: (this.mode === 'pvp' || winnerRole === GAME_CONFIG.OPPONENT_ID) ? winnerRole : GAME_CONFIG.OPPONENT_ID, winnerId: winnerRole,
reason: 'opponent_disconnected', reason: 'opponent_disconnected',
finalGameState: this.gameState, finalGameState: this.gameState,
log: this.consumeLogBuffer() log: this.consumeLogBuffer()
@ -152,52 +160,59 @@ class GameInstance {
console.log(`[Game ${this.id}] Initializing game state for (re)start... Mode: ${this.mode}`); console.log(`[Game ${this.id}] Initializing game state for (re)start... Mode: ${this.mode}`);
if (this.mode === 'ai') { if (this.mode === 'ai') {
this.playerCharacterKey = 'elena'; this.playerCharacterKey = 'elena'; // Игрок в AI всегда Елена
this.opponentCharacterKey = 'balard'; this.opponentCharacterKey = 'balard'; // AI всегда Балард
} else { // pvp } else { // pvp
const playerSocketInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); const playerSocketInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
const opponentSocketInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID); const opponentSocketInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
this.playerCharacterKey = playerSocketInfo?.chosenCharacterKey || 'elena'; // Игрок 1 (слот 'player') this.playerCharacterKey = playerSocketInfo?.chosenCharacterKey || 'elena'; // Фоллбэк, если что-то пошло не так
if (this.playerCount === 2 && opponentSocketInfo) { // Если есть второй игрок (слот 'opponent')
if (this.playerCount === 2 && opponentSocketInfo) {
this.opponentCharacterKey = opponentSocketInfo.chosenCharacterKey; this.opponentCharacterKey = opponentSocketInfo.chosenCharacterKey;
// Убедимся, что персонажи разные // Дополнительная проверка, чтобы персонажи были разными, если вдруг оба выбрали одного
if (this.playerCharacterKey === this.opponentCharacterKey) { if (this.playerCharacterKey === this.opponentCharacterKey) {
this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena'; this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena';
opponentSocketInfo.chosenCharacterKey = this.opponentCharacterKey; // Обновляем и в информации о сокете // Обновляем ключ у второго игрока, если он был изменен
console.warn(`[Game ${this.id}] Corrected character selection in PvP. Opponent for slot ${GAME_CONFIG.OPPONENT_ID} is now ${this.opponentCharacterKey}`); if (opponentSocketInfo.chosenCharacterKey !== this.opponentCharacterKey) {
opponentSocketInfo.chosenCharacterKey = this.opponentCharacterKey;
console.warn(`[Game ${this.id}] PvP character conflict resolved. Opponent in slot '${GAME_CONFIG.OPPONENT_ID}' is now ${this.opponentCharacterKey}.`);
} }
} else if (this.playerCount === 1) { // Только один игрок в PvP }
this.opponentCharacterKey = null; // Оппонент еще не определен } else if (this.playerCount === 1) { // Только один игрок в PvP, оппонент еще не определен
} else { this.opponentCharacterKey = null;
} else { // Неожиданная ситуация
console.error(`[Game ${this.id}] Unexpected playerCount (${this.playerCount}) or missing socketInfo during PvP character key assignment.`); console.error(`[Game ${this.id}] Unexpected playerCount (${this.playerCount}) or missing socketInfo during PvP character key assignment.`);
this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena'; // Фоллбэк this.opponentCharacterKey = (this.playerCharacterKey === 'elena') ? 'almagest' : 'elena'; // Аварийный фоллбэк
} }
} }
console.log(`[Game ${this.id}] Finalizing characters - Player Slot ('${GAME_CONFIG.PLAYER_ID}'): ${this.playerCharacterKey}, Opponent Slot ('${GAME_CONFIG.OPPONENT_ID}'): ${this.opponentCharacterKey || 'N/A (Waiting)'}`);
console.log(`[Game ${this.id}] Finalizing characters - Player Slot: ${this.playerCharacterKey}, Opponent Slot: ${this.opponentCharacterKey || 'N/A'}`);
const playerBase = this._getCharacterBaseData(this.playerCharacterKey); const playerBase = this._getCharacterBaseData(this.playerCharacterKey);
const playerAbilities = this._getCharacterAbilities(this.playerCharacterKey); const playerAbilities = this._getCharacterAbilities(this.playerCharacterKey);
let opponentBase = null; let opponentBase = null;
let opponentAbilities = null; let opponentAbilities = null;
// Загружаем данные оппонента, только если он определен (т.е. PvP игра с 2 игроками или AI игра)
if (this.opponentCharacterKey) { if (this.opponentCharacterKey) {
opponentBase = this._getCharacterBaseData(this.opponentCharacterKey); opponentBase = this._getCharacterBaseData(this.opponentCharacterKey);
opponentAbilities = this._getCharacterAbilities(this.opponentCharacterKey); opponentAbilities = this._getCharacterAbilities(this.opponentCharacterKey);
} }
// Проверяем, готовы ли мы к созданию полного игрового состояния
const isReadyForFullGameState = (this.mode === 'ai') || (this.mode === 'pvp' && this.playerCount === 2 && opponentBase && opponentAbilities); const isReadyForFullGameState = (this.mode === 'ai') || (this.mode === 'pvp' && this.playerCount === 2 && opponentBase && opponentAbilities);
if (!playerBase || !playerAbilities || (!isReadyForFullGameState && !(this.mode === 'pvp' && this.playerCount === 1))) { if (!playerBase || !playerAbilities || (!isReadyForFullGameState && !(this.mode === 'pvp' && this.playerCount === 1))) {
console.error(`[Game ${this.id}] CRITICAL ERROR: Failed to load necessary character data for initialization! Player: ${this.playerCharacterKey}, Opponent: ${this.opponentCharacterKey}, PlayerCount: ${this.playerCount}, Mode: ${this.mode}`); console.error(`[Game ${this.id}] CRITICAL ERROR: Failed to load necessary character data for initialization! PlayerKey: ${this.playerCharacterKey}, OpponentKey: ${this.opponentCharacterKey}, PlayerCount: ${this.playerCount}, Mode: ${this.mode}`);
this.logBuffer = []; this.logBuffer = []; // Очищаем лог
this.addToLog('Критическая ошибка сервера при инициализации персонажей!', GAME_CONFIG.LOG_TYPE_SYSTEM); this.addToLog('Критическая ошибка сервера при инициализации персонажей!', GAME_CONFIG.LOG_TYPE_SYSTEM);
this.io.to(this.id).emit('gameError', { message: 'Критическая ошибка сервера при инициализации игры.' }); // Уведомляем игроков в комнате об ошибке
this.gameState = null; this.io.to(this.id).emit('gameError', { message: 'Критическая ошибка сервера при инициализации игры. Не удалось загрузить данные персонажей.' });
this.gameState = null; // Не создаем gameState
return; return;
} }
// Создаем gameState
this.gameState = { this.gameState = {
player: { player: {
id: GAME_CONFIG.PLAYER_ID, characterKey: this.playerCharacterKey, name: playerBase.name, id: GAME_CONFIG.PLAYER_ID, characterKey: this.playerCharacterKey, name: playerBase.name,
@ -206,53 +221,64 @@ class GameInstance {
resourceName: playerBase.resourceName, attackPower: playerBase.attackPower, resourceName: playerBase.resourceName, attackPower: playerBase.attackPower,
isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {} isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {}
}, },
opponent: { opponent: { // Данные оппонента, если он есть, иначе плейсхолдеры
id: GAME_CONFIG.OPPONENT_ID, characterKey: this.opponentCharacterKey, id: GAME_CONFIG.OPPONENT_ID, characterKey: this.opponentCharacterKey,
name: opponentBase?.name || 'Ожидание...', name: opponentBase?.name || 'Ожидание игрока...',
currentHp: opponentBase?.maxHp || 1, maxHp: opponentBase?.maxHp || 1, currentHp: opponentBase?.maxHp || 1, maxHp: opponentBase?.maxHp || 1,
currentResource: opponentBase?.maxResource || 0, maxResource: opponentBase?.maxResource || 0, currentResource: opponentBase?.maxResource || 0, maxResource: opponentBase?.maxResource || 0,
resourceName: opponentBase?.resourceName || 'Неизвестно', attackPower: opponentBase?.attackPower || 0, resourceName: opponentBase?.resourceName || 'Неизвестно', attackPower: opponentBase?.attackPower || 0,
isBlocking: false, activeEffects: [], isBlocking: false, activeEffects: [],
// Специальные кулдауны для Баларда (AI)
silenceCooldownTurns: this.opponentCharacterKey === 'balard' ? 0 : undefined, silenceCooldownTurns: this.opponentCharacterKey === 'balard' ? 0 : undefined,
manaDrainCooldownTurns: this.opponentCharacterKey === 'balard' ? 0 : undefined, manaDrainCooldownTurns: this.opponentCharacterKey === 'balard' ? 0 : undefined,
abilityCooldowns: {} abilityCooldowns: {}
}, },
isPlayerTurn: Math.random() < 0.5, isGameOver: false, turnNumber: 1, gameMode: this.mode isPlayerTurn: Math.random() < 0.5, // Случайный первый ход
isGameOver: false,
turnNumber: 1,
gameMode: this.mode
}; };
// Инициализация кулдаунов способностей
playerAbilities.forEach(ability => { playerAbilities.forEach(ability => {
if (typeof ability.cooldown === 'number' && ability.cooldown > 0) this.gameState.player.abilityCooldowns[ability.id] = 0; if (typeof ability.cooldown === 'number' && ability.cooldown > 0) {
this.gameState.player.abilityCooldowns[ability.id] = 0;
}
}); });
if (opponentAbilities) { if (opponentAbilities) {
opponentAbilities.forEach(ability => { opponentAbilities.forEach(ability => {
let cd = 0; let cd = 0;
if (ability.cooldown) cd = ability.cooldown; if (ability.cooldown) cd = ability.cooldown;
else if (this.opponentCharacterKey === 'balard') { else if (this.opponentCharacterKey === 'balard') { // Специальные внутренние КД для AI Баларда
if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) cd = GAME_CONFIG[ability.internalCooldownFromConfig]; if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) {
else if (typeof ability.internalCooldownValue === 'number') cd = ability.internalCooldownValue; cd = GAME_CONFIG[ability.internalCooldownFromConfig];
} else if (typeof ability.internalCooldownValue === 'number') {
cd = ability.internalCooldownValue;
}
}
if (cd > 0) {
this.gameState.opponent.abilityCooldowns[ability.id] = 0;
} }
if (cd > 0) this.gameState.opponent.abilityCooldowns[ability.id] = 0;
}); });
} }
this.restartVotes.clear(); const isRestart = this.logBuffer.length > 0 && isReadyForFullGameState; // Проверяем, был ли лог до этого (признак рестарта)
const isFullGameReadyForLog = (this.mode === 'ai' && this.playerCount === 1) || (this.mode === 'pvp' && this.playerCount === 2 && this.opponentCharacterKey); this.logBuffer = []; // Очищаем лог перед новой игрой/рестартом
const isRestart = this.logBuffer.length > 0 && isFullGameReadyForLog; if (isReadyForFullGameState) { // Лог о начале битвы только если игра полностью готова
this.logBuffer = [];
if (isFullGameReadyForLog) {
this.addToLog(isRestart ? '⚔️ Игра перезапущена! ⚔️' : '⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM); this.addToLog(isRestart ? '⚔️ Игра перезапущена! ⚔️' : '⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
} }
console.log(`[Game ${this.id}] Game state initialized. isGameOver: ${this.gameState.isGameOver}. First turn: ${this.gameState.isPlayerTurn ? this.gameState.player.name : (this.gameState.opponent?.name || 'Оппонент')}`); console.log(`[Game ${this.id}] Game state initialized. isGameOver: ${this.gameState.isGameOver}. First turn: ${this.gameState.isPlayerTurn ? this.gameState.player.name : (this.gameState.opponent?.name || 'Оппонент')}`);
} }
startGame() { startGame() {
if (!this.gameState || !this.gameState.player || !this.gameState.opponent || !this.opponentCharacterKey || this.gameState.opponent.name === 'Ожидание...') { // Проверяем, что игра полностью готова к запуску (оба игрока есть и gameState инициализирован)
if (!this.gameState || !this.gameState.player || !this.gameState.opponent || !this.opponentCharacterKey || this.gameState.opponent.name === 'Ожидание игрока...') {
if (this.mode === 'pvp' && this.playerCount === 1 && !this.opponentCharacterKey) { if (this.mode === 'pvp' && this.playerCount === 1 && !this.opponentCharacterKey) {
console.log(`[Game ${this.id}] startGame: Waiting for opponent in PvP game.`); console.log(`[Game ${this.id}] startGame: PvP игра ожидает второго игрока.`);
} else if (!this.gameState) { } else if (!this.gameState) {
console.error(`[Game ${this.id}] Game cannot start: gameState is null.`); console.error(`[Game ${this.id}] Game cannot start: gameState is null.`);
} else { } else {
console.warn(`[Game ${this.id}] Game not fully ready to start. OpponentKey: ${this.opponentCharacterKey}, OpponentName: ${this.gameState.opponent.name}, PlayerCount: ${this.playerCount}`); console.warn(`[Game ${this.id}] Game not fully ready to start. OpponentKey: ${this.opponentCharacterKey}, OpponentName: ${this.gameState.opponent?.name}, PlayerCount: ${this.playerCount}`);
} }
return; return;
} }
@ -263,84 +289,65 @@ class GameInstance {
if (!playerCharData || !opponentCharData) { if (!playerCharData || !opponentCharData) {
console.error(`[Game ${this.id}] CRITICAL ERROR: startGame - Failed to load character data! PlayerKey: ${this.playerCharacterKey}, OpponentKey: ${this.opponentCharacterKey}`); console.error(`[Game ${this.id}] CRITICAL ERROR: startGame - Failed to load character data! PlayerKey: ${this.playerCharacterKey}, OpponentKey: ${this.opponentCharacterKey}`);
this.io.to(this.id).emit('gameError', { message: 'Критическая ошибка сервера при старте игры (данные персонажей).' }); this.io.to(this.id).emit('gameError', { message: 'Критическая ошибка сервера при старте игры (не удалось загрузить данные персонажей).' });
return; return;
} }
Object.values(this.players).forEach(pInfo => { // Отправляем каждому игроку его персональные данные для игры
Object.values(this.players).forEach(playerInfo => {
let dataForThisClient; let dataForThisClient;
if (pInfo.id === GAME_CONFIG.PLAYER_ID) { if (playerInfo.id === GAME_CONFIG.PLAYER_ID) { // Этот клиент играет за слот 'player'
dataForThisClient = { dataForThisClient = {
gameId: this.id, yourPlayerId: pInfo.id, initialGameState: this.gameState, gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
playerBaseStats: playerCharData.baseStats, opponentBaseStats: opponentCharData.baseStats, playerBaseStats: playerCharData.baseStats, opponentBaseStats: opponentCharData.baseStats,
playerAbilities: playerCharData.abilities, opponentAbilities: opponentCharData.abilities, playerAbilities: playerCharData.abilities, opponentAbilities: opponentCharData.abilities,
log: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG } log: this.consumeLogBuffer(), // Первый игрок получает весь накопленный лог
clientConfig: { ...GAME_CONFIG } // Копия конфига для клиента
}; };
} else { } else { // Этот клиент играет за слот 'opponent'
dataForThisClient = { dataForThisClient = {
gameId: this.id, yourPlayerId: pInfo.id, initialGameState: this.gameState, gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
// Меняем местами статы и абилки, чтобы клиент видел себя как 'player', а противника как 'opponent'
playerBaseStats: opponentCharData.baseStats, opponentBaseStats: playerCharData.baseStats, playerBaseStats: opponentCharData.baseStats, opponentBaseStats: playerCharData.baseStats,
playerAbilities: opponentCharData.abilities, opponentAbilities: playerCharData.abilities, playerAbilities: opponentCharData.abilities, opponentAbilities: playerCharData.abilities,
log: [], clientConfig: { ...GAME_CONFIG } log: [], // Второй игрок не получает стартовый лог, чтобы избежать дублирования
clientConfig: { ...GAME_CONFIG }
}; };
} }
pInfo.socket.emit('gameStarted', dataForThisClient); playerInfo.socket.emit('gameStarted', dataForThisClient);
}); });
const firstTurnName = this.gameState.isPlayerTurn ? this.gameState.player.name : this.gameState.opponent.name; const firstTurnName = this.gameState.isPlayerTurn ? this.gameState.player.name : this.gameState.opponent.name;
this.addToLog(`--- ${firstTurnName} ходит первым! ---`, GAME_CONFIG.LOG_TYPE_TURN); this.addToLog(`--- ${firstTurnName} ходит первым! (Ход ${this.gameState.turnNumber}) ---`, GAME_CONFIG.LOG_TYPE_TURN);
this.broadcastGameStateUpdate(); this.broadcastGameStateUpdate(); // Отправляем начальное состояние и лог
// Если ход AI, запускаем его логику
if (!this.gameState.isPlayerTurn) { if (!this.gameState.isPlayerTurn) {
if (this.aiOpponent && this.opponentCharacterKey === 'balard') { if (this.aiOpponent && this.opponentCharacterKey === 'balard') {
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN); setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
} else { } else { // PvP, ход второго игрока
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID }); this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID });
} }
} else { } else { // Ход первого игрока (реального)
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID }); this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID });
} }
} }
handleVoteRestart(requestingSocketId) { // Метод handleVoteRestart удален
if (!this.gameState || !this.gameState.isGameOver) {
const playerSocket = this.players[requestingSocketId]?.socket || this.io.sockets.sockets.get(requestingSocketId);
if(playerSocket) playerSocket.emit('gameError', {message: "Нельзя рестартовать игру, которая не завершена."});
return;
}
if (!this.players[requestingSocketId]) return;
this.restartVotes.add(requestingSocketId);
const voterInfo = this.players[requestingSocketId];
const voterCharacterKey = voterInfo.id === GAME_CONFIG.PLAYER_ID ? this.gameState.player.characterKey : this.gameState.opponent.characterKey;
const voterCharacterData = this._getCharacterBaseData(voterCharacterKey);
this.addToLog(`Игрок ${voterCharacterData?.name || 'Неизвестный'} (${voterInfo.id}) голосует за рестарт.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
this.broadcastLogUpdate();
const requiredVotes = this.playerCount > 0 ? this.playerCount : 1;
if (this.restartVotes.size >= requiredVotes) {
this.initializeGame();
if (this.gameState) this.startGame();
else console.error(`[Game ${this.id}] Failed to restart: gameState is null after re-initialization.`);
} else if (this.mode === 'pvp') {
this.io.to(this.id).emit('waitingForRestartVote', {
voterCharacterName: voterCharacterData?.name || 'Неизвестный',
voterRole: voterInfo.id,
votesNeeded: requiredVotes - this.restartVotes.size
});
}
}
processPlayerAction(requestingSocketId, actionData) { processPlayerAction(requestingSocketId, actionData) {
if (!this.gameState || this.gameState.isGameOver) return; if (!this.gameState || this.gameState.isGameOver) return;
const actingPlayerInfo = this.players[requestingSocketId]; const actingPlayerInfo = this.players[requestingSocketId];
if (!actingPlayerInfo) { console.error(`[Game ${this.id}] Action from unknown socket ${requestingSocketId}`); return; } if (!actingPlayerInfo) { console.error(`[Game ${this.id}] Action from unknown socket ${requestingSocketId}`); return; }
const actingPlayerRole = actingPlayerInfo.id; const actingPlayerRole = actingPlayerInfo.id; // 'player' или 'opponent'
const isCorrectTurn = (this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.PLAYER_ID) || const isCorrectTurn = (this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.PLAYER_ID) ||
(!this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.OPPONENT_ID); (!this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.OPPONENT_ID);
if (!isCorrectTurn) { actingPlayerInfo.socket.emit('gameError', { message: "Сейчас не ваш ход!" }); return; }
if (!isCorrectTurn) {
actingPlayerInfo.socket.emit('gameError', { message: "Сейчас не ваш ход!" });
return;
}
const attackerState = this.gameState[actingPlayerRole]; const attackerState = this.gameState[actingPlayerRole];
const defenderRole = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const defenderRole = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
@ -350,196 +357,203 @@ class GameInstance {
const defenderData = this._getCharacterData(defenderState.characterKey); const defenderData = this._getCharacterData(defenderState.characterKey);
if (!attackerData || !defenderData) { if (!attackerData || !defenderData) {
this.addToLog('Критическая ошибка сервера при обработке действия (данные персонажа)!', GAME_CONFIG.LOG_TYPE_SYSTEM); this.addToLog('Критическая ошибка сервера при обработке действия (не найдены данные персонажа)!', GAME_CONFIG.LOG_TYPE_SYSTEM);
this.broadcastLogUpdate(); return; this.broadcastLogUpdate(); return;
} }
const attackerBaseStats = attackerData.baseStats; let actionValid = true; // Флаг валидности действия
const defenderBaseStats = defenderData.baseStats;
const attackerAbilities = attackerData.abilities;
let actionValid = true;
// Обработка атаки
if (actionData.actionType === 'attack') { if (actionData.actionType === 'attack') {
let taunt = "";
if (attackerState.characterKey === 'elena') {
taunt = serverGameLogic.getElenaTaunt('playerBasicAttack', {}, GAME_CONFIG, gameData, this.gameState);
}
const attackBuffAbilityId = attackerState.characterKey === 'elena' ? GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH
: (attackerState.characterKey === 'almagest' ? GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK : null);
let attackBuffEffect = null;
if (attackBuffAbilityId) {
attackBuffEffect = attackerState.activeEffects.find(eff => eff.id === attackBuffAbilityId);
}
if (attackerState.characterKey === 'elena' && taunt && taunt !== "(Молчание)") {
this.addToLog(`${attackerState.name} атакует: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
} else {
this.addToLog(`${attackerState.name} атакует ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO);
}
if (attackBuffEffect && !attackBuffEffect.justCast) {
this.addToLog(`✨ Эффект "${attackBuffEffect.name}" активен! Атака восстановит ${attackerState.resourceName}!`, GAME_CONFIG.LOG_TYPE_EFFECT);
}
serverGameLogic.performAttack( serverGameLogic.performAttack(
attackerState, defenderState, attackerBaseStats, defenderBaseStats, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats,
this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData
); );
// Логика для "Силы Природы" и аналогов - бафф применяется после атаки
if (attackBuffEffect) { const attackBuffAbilityId = attackerState.characterKey === 'elena' ? GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH
const actualRegen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerBaseStats.maxResource - attackerState.currentResource); : (attackerState.characterKey === 'almagest' ? GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK : null);
if (attackBuffAbilityId) {
const attackBuffEffect = attackerState.activeEffects.find(eff => eff.id === attackBuffAbilityId);
if (attackBuffEffect && !attackBuffEffect.justCast) { // Эффект должен быть активен и не только что применен
const actualRegen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerData.baseStats.maxResource - attackerState.currentResource);
if (actualRegen > 0) { if (actualRegen > 0) {
attackerState.currentResource += actualRegen; attackerState.currentResource += actualRegen;
this.addToLog(`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от эффекта "${attackBuffEffect.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL); this.addToLog(`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от эффекта "${attackBuffEffect.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL);
} }
// Эффект НЕ удаляется здесь для многоразового действия // Не удаляем эффект, если он многоразовый. Если одноразовый - удалить тут.
// В текущей реализации Сила Природы имеет duration, поэтому управляется через processEffects.
}
} }
// Обработка способности
} else if (actionData.actionType === 'ability' && actionData.abilityId) { } else if (actionData.actionType === 'ability' && actionData.abilityId) {
const ability = attackerAbilities.find(ab => ab.id === actionData.abilityId); const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId);
if (!ability) { actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." }); return; } if (!ability) { actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." }); return; }
// Проверки валидности использования способности
if (attackerState.currentResource < ability.cost) { this.addToLog(`${attackerState.name} пытается применить "${ability.name}", но не хватает ${attackerState.resourceName}!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } if (attackerState.currentResource < ability.cost) { this.addToLog(`${attackerState.name} пытается применить "${ability.name}", но не хватает ${attackerState.resourceName}!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
if (actionValid && attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0) { this.addToLog(`"${ability.name}" еще не готова (КД: ${attackerState.abilityCooldowns[ability.id]} х.).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } if (actionValid && attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0) { this.addToLog(`"${ability.name}" еще на перезарядке (${attackerState.abilityCooldowns[ability.id]} х.).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
// Специальные КД для Баларда
if (actionValid && attackerState.characterKey === 'balard') { if (actionValid && attackerState.characterKey === 'balard') {
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && (attackerState.silenceCooldownTurns > 0 || (attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0))) { this.addToLog(`"${ability.name}" еще не готова (спец. КД или общий КД).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && attackerState.silenceCooldownTurns > 0) { this.addToLog(`"${ability.name}" еще не готова (спец. КД).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && (attackerState.manaDrainCooldownTurns > 0 || (attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0))) { this.addToLog(`"${ability.name}" еще не готова (спец. КД или общий КД).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && attackerState.manaDrainCooldownTurns > 0) { this.addToLog(`"${ability.name}" еще не готова (спец. КД).`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
} }
// Нельзя кастовать бафф, если он уже активен
if (actionValid && ability.type === GAME_CONFIG.ACTION_TYPE_BUFF && attackerState.activeEffects.some(e => e.id === ability.id)) { this.addToLog(`Эффект "${ability.name}" уже активен!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } if (actionValid && ability.type === GAME_CONFIG.ACTION_TYPE_BUFF && attackerState.activeEffects.some(e => e.id === ability.id)) { this.addToLog(`Эффект "${ability.name}" уже активен!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
const isDebuffAbility = ability.id === GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_DEBUFF; // Нельзя кастовать дебафф на цель, если он уже на ней (для определенных дебаффов)
if (actionValid && isDebuffAbility) { const isTargetedDebuff = ability.id === GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_DEBUFF;
if (defenderState.activeEffects.some(e => e.id === 'effect_' + ability.id)) { this.addToLog(`Эффект "${ability.name}" уже наложен на ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; } if (actionValid && isTargetedDebuff) {
if (defenderState.activeEffects.some(e => e.id === 'effect_' + ability.id)) { // Ищем эффект с префиксом effect_
this.addToLog(`Эффект "${ability.name}" уже наложен на ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO);
actionValid = false;
}
} }
if (actionValid) { if (actionValid) {
attackerState.currentResource -= ability.cost; attackerState.currentResource -= ability.cost;
// Установка кулдауна
let baseCooldown = 0; let baseCooldown = 0;
if (ability.cooldown) baseCooldown = ability.cooldown; if (ability.cooldown) baseCooldown = ability.cooldown;
else if (attackerState.characterKey === 'balard') { else if (attackerState.characterKey === 'balard') { // Специальные внутренние КД для AI
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) { attackerState.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN;} if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) { attackerState.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN;}
else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && ability.internalCooldownValue) { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue; } else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && ability.internalCooldownValue) { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue; }
else { if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) baseCooldown = GAME_CONFIG[ability.internalCooldownFromConfig]; else if (typeof ability.internalCooldownValue === 'number') baseCooldown = ability.internalCooldownValue; } else { if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) baseCooldown = GAME_CONFIG[ability.internalCooldownFromConfig]; else if (typeof ability.internalCooldownValue === 'number') baseCooldown = ability.internalCooldownValue; }
} }
if (baseCooldown > 0 && attackerState.abilityCooldowns) attackerState.abilityCooldowns[ability.id] = baseCooldown + 1; if (baseCooldown > 0 && attackerState.abilityCooldowns) attackerState.abilityCooldowns[ability.id] = baseCooldown + 1; // +1, т.к. уменьшится в конце этого хода
let logMessage = `${attackerState.name} колдует "${ability.name}" (-${ability.cost} ${attackerState.resourceName})`; serverGameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
if (attackerState.characterKey === 'elena') {
const taunt = serverGameLogic.getElenaTaunt('playerActionCast', { abilityId: ability.id }, GAME_CONFIG, gameData, this.gameState);
if (taunt && taunt !== "(Молчание)") logMessage += `: "${taunt}"`;
} }
const logType = ability.type === GAME_CONFIG.ACTION_TYPE_HEAL ? GAME_CONFIG.LOG_TYPE_HEAL : ability.type === GAME_CONFIG.ACTION_TYPE_DAMAGE ? GAME_CONFIG.LOG_TYPE_DAMAGE : GAME_CONFIG.LOG_TYPE_EFFECT; } else { actionValid = false; } // Неизвестный тип действия
this.addToLog(logMessage, logType);
const targetForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL || ability.type === GAME_CONFIG.ACTION_TYPE_BUFF) ? attackerState : defenderState; if (!actionValid) { this.broadcastLogUpdate(); return; } // Если действие невалидно, просто отправляем лог и выходим
const targetBaseStatsForAbility = (targetForAbility.id === defenderState.id ? defenderBaseStats : attackerBaseStats);
serverGameLogic.applyAbilityEffect(ability, attackerState, targetForAbility, attackerBaseStats, targetBaseStatsForAbility, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
}
} else { actionValid = false; }
if (!actionValid) { this.broadcastLogUpdate(); return; } if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } // Проверяем конец игры после действия
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } setTimeout(() => { this.switchTurn(); }, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); // Переключаем ход с задержкой
setTimeout(() => { this.switchTurn(); }, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
} }
switchTurn() { switchTurn() {
if (!this.gameState || this.gameState.isGameOver) return; if (!this.gameState || this.gameState.isGameOver) return;
const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
const endingTurnActorState = this.gameState[endingTurnActorRole]; const endingTurnActorState = this.gameState[endingTurnActorRole];
const endingTurnCharacterData = this._getCharacterData(endingTurnActorState.characterKey); const endingTurnCharacterData = this._getCharacterData(endingTurnActorState.characterKey);
if (!endingTurnCharacterData) { console.error(`SwitchTurn Error: No char data for ${endingTurnActorState.characterKey}`); return; } if (!endingTurnCharacterData) { console.error(`SwitchTurn Error: No char data for ${endingTurnActorState.characterKey}`); return; }
// Обработка эффектов в конце хода (DoT, HoT, истечение баффов/дебаффов)
serverGameLogic.processEffects(endingTurnActorState.activeEffects, endingTurnActorState, endingTurnCharacterData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData); serverGameLogic.processEffects(endingTurnActorState.activeEffects, endingTurnActorState, endingTurnCharacterData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
serverGameLogic.updateBlockingStatus(this.gameState.player); serverGameLogic.updateBlockingStatus(this.gameState.player); // Обновляем статус блока для обоих
serverGameLogic.updateBlockingStatus(this.gameState.opponent); serverGameLogic.updateBlockingStatus(this.gameState.opponent);
// Уменьшение кулдаунов способностей
if (endingTurnActorState.abilityCooldowns) { if (endingTurnActorState.abilityCooldowns) {
serverGameLogic.processPlayerAbilityCooldowns(endingTurnActorState.abilityCooldowns, endingTurnCharacterData.abilities, endingTurnActorState.name, this.addToLog.bind(this)); serverGameLogic.processPlayerAbilityCooldowns(endingTurnActorState.abilityCooldowns, endingTurnCharacterData.abilities, endingTurnActorState.name, this.addToLog.bind(this));
} }
// Специальные КД для Баларда
if (endingTurnActorState.characterKey === 'balard') { if (endingTurnActorState.characterKey === 'balard') {
if (endingTurnActorState.silenceCooldownTurns !== undefined && endingTurnActorState.silenceCooldownTurns > 0) endingTurnActorState.silenceCooldownTurns--; if (endingTurnActorState.silenceCooldownTurns !== undefined && endingTurnActorState.silenceCooldownTurns > 0) endingTurnActorState.silenceCooldownTurns--;
if (endingTurnActorState.manaDrainCooldownTurns !== undefined && endingTurnActorState.manaDrainCooldownTurns > 0) endingTurnActorState.manaDrainCooldownTurns--; if (endingTurnActorState.manaDrainCooldownTurns !== undefined && endingTurnActorState.manaDrainCooldownTurns > 0) endingTurnActorState.manaDrainCooldownTurns--;
} }
if (endingTurnActorRole === GAME_CONFIG.OPPONENT_ID) { // Уменьшение длительности безмолвия на конкретные абилки (если это ход оппонента)
const playerStateInGame = this.gameState.player; if (endingTurnActorRole === GAME_CONFIG.OPPONENT_ID) { // Если это был ход оппонента (AI или PvP)
const playerStateInGame = this.gameState.player; // Игрок, на которого могло быть наложено безмолвие
if (playerStateInGame.disabledAbilities?.length > 0) { if (playerStateInGame.disabledAbilities?.length > 0) {
const playerCharAbilities = this._getCharacterAbilities(playerStateInGame.characterKey); const playerCharAbilities = this._getCharacterAbilities(playerStateInGame.characterKey);
if (playerCharAbilities) serverGameLogic.processDisabledAbilities(playerStateInGame.disabledAbilities, playerCharAbilities, playerStateInGame.name, this.addToLog.bind(this)); if (playerCharAbilities) serverGameLogic.processDisabledAbilities(playerStateInGame.disabledAbilities, playerCharAbilities, playerStateInGame.name, this.addToLog.bind(this));
} }
} }
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; }
this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn; if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } // Проверяем конец игры после эффектов
if (this.gameState.isPlayerTurn) this.gameState.turnNumber++;
this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn; // Меняем ход
if (this.gameState.isPlayerTurn) this.gameState.turnNumber++; // Новый ход игрока - увеличиваем номер хода
const currentTurnActorState = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent; const currentTurnActorState = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent;
this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN); this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
this.broadcastGameStateUpdate(); this.broadcastGameStateUpdate();
// Если ход AI, запускаем его логику
if (!this.gameState.isPlayerTurn) { if (!this.gameState.isPlayerTurn) {
if (this.aiOpponent && this.opponentCharacterKey === 'balard') { if (this.aiOpponent && this.opponentCharacterKey === 'balard') {
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN); setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
} else { } else { // PvP, ход второго игрока
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID }); this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.OPPONENT_ID });
} }
} else { } else { // Ход первого игрока
this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID }); this.io.to(this.id).emit('turnNotification', { currentTurn: GAME_CONFIG.PLAYER_ID });
} }
} }
processAiTurn() { processAiTurn() {
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent || this.opponentCharacterKey !== 'balard') { if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent || this.opponentCharacterKey !== 'balard') {
if(!this.gameState || this.gameState.isGameOver) return; if(!this.gameState || this.gameState.isGameOver) return; // Если игра закончена, ничего не делаем
// Если не ход AI или это не AI Балард, выходим (хотя эта проверка должна быть раньше)
return; return;
} }
const aiDecision = serverGameLogic.decideAiAction(this.gameState, gameData, GAME_CONFIG, this.addToLog.bind(this)); const aiDecision = serverGameLogic.decideAiAction(this.gameState, gameData, GAME_CONFIG, this.addToLog.bind(this));
const attackerState = this.gameState.opponent; const attackerState = this.gameState.opponent; // AI всегда в слоте 'opponent' в AI режиме
const defenderState = this.gameState.player; const defenderState = this.gameState.player;
const attackerData = this._getCharacterData('balard'); const attackerData = this._getCharacterData('balard');
const defenderData = this._getCharacterData(defenderState.characterKey); const defenderData = this._getCharacterData(defenderState.characterKey); // Обычно 'elena'
if (!attackerData || !defenderData) { this.switchTurn(); return; }
if (!attackerData || !defenderData) { this.addToLog("AI не может действовать: ошибка данных персонажа.", GAME_CONFIG.LOG_TYPE_SYSTEM); this.switchTurn(); return; }
let actionValid = true; let actionValid = true;
if (aiDecision.actionType === 'attack') { if (aiDecision.actionType === 'attack') {
this.addToLog(`${attackerState.name} атакует ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO); // Лог атаки уже будет в performAttack
serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData); serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
} else if (aiDecision.actionType === 'ability' && aiDecision.ability) { } else if (aiDecision.actionType === 'ability' && aiDecision.ability) {
const ability = aiDecision.ability; const ability = aiDecision.ability;
// Проверки валидности (ресурс, КД) для AI
if (attackerState.currentResource < ability.cost || if (attackerState.currentResource < ability.cost ||
(attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0) || (attackerState.abilityCooldowns && attackerState.abilityCooldowns[ability.id] > 0) ||
(ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && attackerState.silenceCooldownTurns > 0) || (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE && attackerState.silenceCooldownTurns > 0) ||
(ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && attackerState.manaDrainCooldownTurns > 0) (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && attackerState.manaDrainCooldownTurns > 0)
) { ) {
actionValid = false; actionValid = false;
this.addToLog(`AI ${attackerState.name} не смог применить "${ability.name}" (ресурс/КД).`, GAME_CONFIG.LOG_TYPE_INFO); this.addToLog(`AI ${attackerState.name} не смог применить "${ability.name}" (недостаточно ресурса или на перезарядке). Решил атаковать.`, GAME_CONFIG.LOG_TYPE_INFO);
// Если выбранная способность невалидна, AI по умолчанию атакует
serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
} }
if (actionValid) {
if (actionValid) { // Если способность все еще валидна
attackerState.currentResource -= ability.cost; attackerState.currentResource -= ability.cost;
// Установка кулдауна для AI
let baseCooldown = 0; let baseCooldown = 0;
if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) { attackerState.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN;} if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_SILENCE) { attackerState.silenceCooldownTurns = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN; baseCooldown = GAME_CONFIG.BALARD_SILENCE_INTERNAL_COOLDOWN;}
else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && ability.internalCooldownValue) { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue;} else if (ability.id === GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN && ability.internalCooldownValue) { attackerState.manaDrainCooldownTurns = ability.internalCooldownValue; baseCooldown = ability.internalCooldownValue;}
else { if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) baseCooldown = GAME_CONFIG[ability.internalCooldownFromConfig]; else if (typeof ability.internalCooldownValue === 'number') baseCooldown = ability.internalCooldownValue; } else { if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) baseCooldown = GAME_CONFIG[ability.internalCooldownFromConfig]; else if (typeof ability.internalCooldownValue === 'number') baseCooldown = ability.internalCooldownValue; }
if (baseCooldown > 0 && attackerState.abilityCooldowns) attackerState.abilityCooldowns[ability.id] = baseCooldown + 1; if (baseCooldown > 0 && attackerState.abilityCooldowns) attackerState.abilityCooldowns[ability.id] = baseCooldown + 1;
this.addToLog(`${attackerState.name} применяет "${ability.name}"...`, GAME_CONFIG.LOG_TYPE_EFFECT); serverGameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
const targetForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL) ? attackerState : defenderState;
const targetBaseStatsForAbility = (targetForAbility.id === defenderState.id ? defenderData.baseStats : attackerData.baseStats);
serverGameLogic.applyAbilityEffect(ability, attackerState, targetForAbility, attackerData.baseStats, targetBaseStatsForAbility, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
} }
} else if (aiDecision.actionType === 'pass') { } else if (aiDecision.actionType === 'pass') { // Если AI решил пропустить ход
if (aiDecision.logMessage) this.addToLog(aiDecision.logMessage.message, aiDecision.logMessage.type); if (aiDecision.logMessage) this.addToLog(aiDecision.logMessage.message, aiDecision.logMessage.type);
else this.addToLog(`${attackerState.name} пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO); else this.addToLog(`${attackerState.name} обдумывает свой следующий ход...`, GAME_CONFIG.LOG_TYPE_INFO);
} else actionValid = false; } else { // Неизвестное решение AI или ошибка
actionValid = false;
this.addToLog(`AI ${attackerState.name} не смог выбрать действие и атакует.`, GAME_CONFIG.LOG_TYPE_INFO);
serverGameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
}
// if (!actionValid && aiDecision.actionType !== 'pass') {
// this.addToLog(`${attackerState.name} не смог выполнить выбранное действие и пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
// }
if (!actionValid) this.addToLog(`${attackerState.name} не смог выполнить выбранное действие и пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; }
this.switchTurn(); this.switchTurn(); // Переключаем ход после действия AI
} }
checkGameOver() { checkGameOver() {
if (!this.gameState || this.gameState.isGameOver) return this.gameState ? this.gameState.isGameOver : true; if (!this.gameState || this.gameState.isGameOver) return this.gameState ? this.gameState.isGameOver : true; // Если игра уже закончена, или нет gameState
const playerState = this.gameState.player; const playerState = this.gameState.player;
const opponentState = this.gameState.opponent; const opponentState = this.gameState.opponent;
if (!playerState || !opponentState) return false;
if (!playerState || !opponentState || opponentState.name === 'Ожидание игрока...') {
// Если одного из игроков нет (например, PvP игра ожидает второго), игра не может закончиться по HP
return false;
}
const playerDead = playerState.currentHp <= 0; const playerDead = playerState.currentHp <= 0;
const opponentDead = opponentState.currentHp <= 0; const opponentDead = opponentState.currentHp <= 0;
@ -548,31 +562,78 @@ class GameInstance {
this.gameState.isGameOver = true; this.gameState.isGameOver = true;
const winnerRole = opponentDead ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const winnerRole = opponentDead ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
const loserRole = opponentDead ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const loserRole = opponentDead ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const winnerState = this.gameState[winnerRole]; const winnerState = this.gameState[winnerRole];
const loserState = this.gameState[loserRole]; const loserState = this.gameState[loserRole];
const winnerName = winnerState?.name || (winnerRole === GAME_CONFIG.PLAYER_ID ? "Игрок" : "Противник"); const winnerName = winnerState?.name || (winnerRole === GAME_CONFIG.PLAYER_ID ? "Игрок" : "Противник");
const loserName = loserState?.name || (loserRole === GAME_CONFIG.PLAYER_ID ? "Игрок" : "Противник"); const loserName = loserState?.name || (loserRole === GAME_CONFIG.PLAYER_ID ? "Игрок" : "Противник");
this.addToLog(`ПОБЕДА! ${winnerName} одолел(а) ${loserName}!`, GAME_CONFIG.LOG_TYPE_SYSTEM); this.addToLog(`🏁 ПОБЕДА! ${winnerName} одолел(а) ${loserName}! 🏁`, GAME_CONFIG.LOG_TYPE_SYSTEM);
// Дополнительные сообщения о конце игры
if (winnerState?.characterKey === 'elena') { if (winnerState?.characterKey === 'elena') {
const taunt = serverGameLogic.getElenaTaunt('opponentNearDefeatCheck', {}, GAME_CONFIG, gameData, this.gameState); const tauntContext = loserState?.characterKey === 'balard' ? 'opponentNearDefeatBalard' : 'opponentNearDefeatAlmagest';
const taunt = serverGameLogic.getElenaTaunt(tauntContext, {}, GAME_CONFIG, gameData, this.gameState);
if (taunt && taunt !== "(Молчание)") this.addToLog(`${winnerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO); if (taunt && taunt !== "(Молчание)") this.addToLog(`${winnerState.name}: "${taunt}"`, GAME_CONFIG.LOG_TYPE_INFO);
if (loserState?.characterKey === 'balard') this.addToLog(`Елена исполнила свой тяжкий долг. ${loserName} развоплощен...`, GAME_CONFIG.LOG_TYPE_SYSTEM); if (loserState?.characterKey === 'balard') this.addToLog(`Елена исполнила свой тяжкий долг. ${loserName} развоплощен...`, GAME_CONFIG.LOG_TYPE_SYSTEM);
else if (loserState?.characterKey === 'almagest') this.addToLog(`Елена одержала победу над темной волшебницей ${loserName}!`, GAME_CONFIG.LOG_TYPE_SYSTEM); else if (loserState?.characterKey === 'almagest') this.addToLog(`Елена одержала победу над темной волшебницей ${loserName}!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
} }
this.io.to(this.id).emit('gameOver', { winnerId: winnerRole, reason: `${loserName} побежден(а)`, finalGameState: this.gameState, log: this.consumeLogBuffer() });
this.io.to(this.id).emit('gameOver', {
winnerId: winnerRole,
reason: `${loserName} побежден(а)`,
finalGameState: this.gameState,
log: this.consumeLogBuffer()
});
return true; return true;
} }
return false; return false;
} }
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) { if (!message) return; this.logBuffer.push({ message, type, timestamp: Date.now() }); } addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) {
consumeLogBuffer() { const logs = [...this.logBuffer]; this.logBuffer = []; return logs; } if (!message) return;
broadcastGameStateUpdate() { if (!this.gameState) return; this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() }); } this.logBuffer.push({ message, type, timestamp: Date.now() });
broadcastLogUpdate() { if (this.logBuffer.length > 0) this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() });} }
_getCharacterData(key) { if(!key) return null; switch (key) { case 'elena': return { baseStats: gameData.playerBaseStats, abilities: gameData.playerAbilities }; case 'balard': return { baseStats: gameData.opponentBaseStats, abilities: gameData.opponentAbilities }; case 'almagest': return { baseStats: gameData.almagestBaseStats, abilities: gameData.almagestAbilities }; default: console.error(`_getCharacterData: Unknown character key "${key}"`); return null; }}
_getCharacterBaseData(key) { if(!key) return null; switch (key) { case 'elena': return gameData.playerBaseStats; case 'balard': return gameData.opponentBaseStats; case 'almagest': return gameData.almagestBaseStats; default: console.error(`_getCharacterBaseData: Unknown character key "${key}"`); return null; }} consumeLogBuffer() {
_getCharacterAbilities(key) { if(!key) return null; switch (key) { case 'elena': return gameData.playerAbilities; case 'balard': return gameData.opponentAbilities; case 'almagest': return gameData.almagestAbilities; default: console.error(`_getCharacterAbilities: Unknown character key "${key}"`); return null; }} const logs = [...this.logBuffer];
this.logBuffer = [];
return logs;
}
broadcastGameStateUpdate() {
if (!this.gameState) return;
this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() });
}
broadcastLogUpdate() { // Если нужно отправить только лог без полного gameState
if (this.logBuffer.length > 0) {
this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() });
}
}
// Вспомогательные функции для получения данных персонажа
_getCharacterData(key) {
if (!key) return null;
switch (key) {
case 'elena': return { baseStats: gameData.playerBaseStats, abilities: gameData.playerAbilities };
case 'balard': return { baseStats: gameData.opponentBaseStats, abilities: gameData.opponentAbilities };
case 'almagest': return { baseStats: gameData.almagestBaseStats, abilities: gameData.almagestAbilities };
default: console.error(`_getCharacterData: Unknown character key "${key}"`); return null;
}
}
_getCharacterBaseData(key) {
if (!key) return null;
const charData = this._getCharacterData(key);
return charData ? charData.baseStats : null;
}
_getCharacterAbilities(key) {
if (!key) return null;
const charData = this._getCharacterData(key);
return charData ? charData.abilities : null;
}
} }
module.exports = GameInstance; module.exports = GameInstance;

View File

@ -1,23 +1,18 @@
// /server_modules/gameManager.js // /server_modules/gameManager.js
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const GameInstance = require('./gameInstance'); const GameInstance = require('./gameInstance');
const gameData = require('./data'); const gameData = require('./data'); // Нужен для getAvailablePvPGamesListForClient
const GAME_CONFIG = require('./config'); // Нужен для GAME_CONFIG.PLAYER_ID и других констант
class GameManager { class GameManager {
constructor(io) { constructor(io) {
this.io = io; this.io = io;
this.games = {}; // { gameId: GameInstance } this.games = {}; // { gameId: GameInstance }
this.socketToGame = {}; // { socket.id: gameId } this.socketToGame = {}; // { socket.id: gameId }
this.pendingPvPGames = []; // [gameId] this.pendingPvPGames = []; // [gameId] - ID игр, ожидающих второго игрока в PvP
this.userToPendingGame = {}; // { userId: gameId } или { socketId: gameId } this.userToPendingGame = {}; // { userId: gameId } или { socketId: gameId } - для отслеживания созданных ожидающих игр
} }
/**
* Удаляет предыдущие ожидающие PvP игры, созданные этим же пользователем/сокетом.
* @param {string} currentSocketId - ID текущего сокета игрока.
* @param {number|string} [identifier] - userId игрока или socketId.
* @param {string} [excludeGameId] - ID игры, которую НЕ нужно удалять.
*/
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) { _removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
const keyToUse = identifier || currentSocketId; const keyToUse = identifier || currentSocketId;
const oldPendingGameId = this.userToPendingGame[keyToUse]; const oldPendingGameId = this.userToPendingGame[keyToUse];
@ -26,8 +21,6 @@ class GameManager {
const gameToRemove = this.games[oldPendingGameId]; const gameToRemove = this.games[oldPendingGameId];
if (gameToRemove && gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) { if (gameToRemove && gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) {
const playersInOldGame = Object.values(gameToRemove.players); const playersInOldGame = Object.values(gameToRemove.players);
// Убеждаемся, что единственный игрок в старой игре - это действительно тот, кто сейчас создает/присоединяется
// Либо по ID сокета, либо по userId, если он был владельцем
const isOwnerBySocket = playersInOldGame.length === 1 && playersInOldGame[0].socket.id === currentSocketId; const isOwnerBySocket = playersInOldGame.length === 1 && playersInOldGame[0].socket.id === currentSocketId;
const isOwnerByUserId = identifier && gameToRemove.ownerUserId === identifier; const isOwnerByUserId = identifier && gameToRemove.ownerUserId === identifier;
@ -38,43 +31,41 @@ class GameManager {
const pendingIndex = this.pendingPvPGames.indexOf(oldPendingGameId); const pendingIndex = this.pendingPvPGames.indexOf(oldPendingGameId);
if (pendingIndex > -1) this.pendingPvPGames.splice(pendingIndex, 1); if (pendingIndex > -1) this.pendingPvPGames.splice(pendingIndex, 1);
// Удаляем привязки для старого сокета, если он там был
if (playersInOldGame.length === 1 && this.socketToGame[playersInOldGame[0].socket.id] === oldPendingGameId) { if (playersInOldGame.length === 1 && this.socketToGame[playersInOldGame[0].socket.id] === oldPendingGameId) {
delete this.socketToGame[playersInOldGame[0].socket.id]; delete this.socketToGame[playersInOldGame[0].socket.id];
} }
delete this.userToPendingGame[keyToUse]; // Удаляем по ключу, который использовали для поиска delete this.userToPendingGame[keyToUse];
this.broadcastAvailablePvPGames(); this.broadcastAvailablePvPGames();
} }
} else if (oldPendingGameId === excludeGameId) { } else if (oldPendingGameId === excludeGameId) {
// Это та же игра, ничего не делаем // Это та же игра, к которой игрок присоединяется, ничего не делаем
} else { } else {
// Запись в userToPendingGame устарела или не соответствует условиям, чистим
delete this.userToPendingGame[keyToUse]; delete this.userToPendingGame[keyToUse];
} }
} }
} }
createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', userId = null) { createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', userId = null) {
const identifier = userId || socket.id; const identifier = userId || socket.id;
this._removePreviousPendingGames(socket.id, identifier); // Удаляем старые перед созданием новой this._removePreviousPendingGames(socket.id, identifier);
const gameId = uuidv4(); const gameId = uuidv4();
const game = new GameInstance(gameId, this.io, mode); const game = new GameInstance(gameId, this.io, mode);
if (userId) game.ownerUserId = userId; // Устанавливаем владельца игры if (userId) game.ownerUserId = userId;
this.games[gameId] = game; this.games[gameId] = game;
// В AI режиме игрок всегда Елена, в PvP - тот, кого выбрали
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena'; const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
if (game.addPlayer(socket, charKeyForInstance)) { // addPlayer теперь сам установит userId в game.ownerUserId если это первый игрок if (game.addPlayer(socket, charKeyForInstance)) {
this.socketToGame[socket.id] = gameId; // Устанавливаем привязку после успешного добавления this.socketToGame[socket.id] = gameId;
console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${socket.id} (userId: ${userId}, выбран: ${charKeyForInstance})`); console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${socket.userData?.username || socket.id} (userId: ${userId}, выбран: ${charKeyForInstance})`);
const assignedPlayerId = game.players[socket.id]?.id; const assignedPlayerId = game.players[socket.id]?.id;
if (!assignedPlayerId) { if (!assignedPlayerId) {
delete this.games[gameId]; if(this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id]; delete this.games[gameId]; if(this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id];
socket.emit('gameError', { message: 'Ошибка сервера при создании игры (ID игрока).' }); return; socket.emit('gameError', { message: 'Ошибка сервера при создании игры (не удалось назначить ID игрока).' }); return;
} }
socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId }); socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });
@ -84,10 +75,9 @@ class GameManager {
this.broadcastAvailablePvPGames(); this.broadcastAvailablePvPGames();
} }
} else { } else {
delete this.games[gameId]; // game.addPlayer вернул false, чистим delete this.games[gameId];
// socketToGame не должен был быть установлен, если addPlayer вернул false
if (this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id]; if (this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id];
socket.emit('gameError', { message: 'Не удалось создать игру или добавить игрока.' }); // Сообщение об ошибке отправляется из game.addPlayer
} }
} }
@ -98,22 +88,21 @@ class GameManager {
if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; } if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'Эта игра не является PvP игрой.' }); return; } if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'Эта игра не является PvP игрой.' }); return; }
if (game.playerCount >= 2) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; } if (game.playerCount >= 2) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; }
if (game.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре (попытка двойного присоединения).' }); return;} // Доп. проверка if (game.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре.' }); return;}
// Удаляем предыдущие ожидающие игры этого пользователя
this._removePreviousPendingGames(socket.id, identifier, gameId); this._removePreviousPendingGames(socket.id, identifier, gameId);
// addPlayer в GameInstance сам определит персонажа для второго игрока на основе первого
if (game.addPlayer(socket)) { if (game.addPlayer(socket)) {
this.socketToGame[socket.id] = gameId; this.socketToGame[socket.id] = gameId;
// console.log(`[GameManager] Игрок ${socket.id} (userId: ${userId}) присоединился к PvP игре ${gameId}`); console.log(`[GameManager] Игрок ${socket.userData?.username || socket.id} (userId: ${userId}) присоединился к PvP игре ${gameId}`);
const gameIndex = this.pendingPvPGames.indexOf(gameId); const gameIndex = this.pendingPvPGames.indexOf(gameId);
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1); if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
// Очищаем запись о создателе из userToPendingGame, так как игра началась
if (game.ownerUserId && this.userToPendingGame[game.ownerUserId] === gameId) { if (game.ownerUserId && this.userToPendingGame[game.ownerUserId] === gameId) {
delete this.userToPendingGame[game.ownerUserId]; delete this.userToPendingGame[game.ownerUserId];
} else { // Если ownerUserId не был или не совпал, пробуем по socketId первого игрока } else {
const firstPlayerSocketId = Object.keys(game.players).find(sId => game.players[sId].id === GAME_CONFIG.PLAYER_ID && game.players[sId].socket.id !== socket.id); const firstPlayerSocketId = Object.keys(game.players).find(sId => game.players[sId].id === GAME_CONFIG.PLAYER_ID && game.players[sId].socket.id !== socket.id);
if (firstPlayerSocketId && this.userToPendingGame[firstPlayerSocketId] === gameId) { if (firstPlayerSocketId && this.userToPendingGame[firstPlayerSocketId] === gameId) {
delete this.userToPendingGame[firstPlayerSocketId]; delete this.userToPendingGame[firstPlayerSocketId];
@ -121,28 +110,32 @@ class GameManager {
} }
this.broadcastAvailablePvPGames(); this.broadcastAvailablePvPGames();
} else { } else {
socket.emit('gameError', { message: 'Не удалось присоединиться к игре (внутренняя ошибка).' }); // Сообщение об ошибке отправляется из game.addPlayer
} }
} }
findAndJoinRandomPvPGame(socket, chosenCharacterKey = 'elena', userId = null) { findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', userId = null) {
const identifier = userId || socket.id; const identifier = userId || socket.id;
this._removePreviousPendingGames(socket.id, identifier); // Удаляем старые перед поиском/созданием this._removePreviousPendingGames(socket.id, identifier);
let gameIdToJoin = null; let gameIdToJoin = null;
const preferredOpponentKey = chosenCharacterKey === 'elena' ? 'almagest' : 'elena'; // Персонаж, которого мы бы хотели видеть у оппонента (зеркальный нашему выбору)
const preferredOpponentKey = chosenCharacterKeyForCreation === 'elena' ? 'almagest' : 'elena';
// Сначала ищем игру, где первый игрок выбрал "зеркального" персонажа
for (const id of this.pendingPvPGames) { for (const id of this.pendingPvPGames) {
const pendingGame = this.games[id]; const pendingGame = this.games[id];
if (pendingGame && pendingGame.playerCount === 1 && pendingGame.mode === 'pvp') { if (pendingGame && pendingGame.playerCount === 1 && pendingGame.mode === 'pvp') {
const firstPlayerInfo = Object.values(pendingGame.players)[0]; const firstPlayerInfo = Object.values(pendingGame.players)[0];
const isMyOwnGame = (userId && pendingGame.ownerUserId === userId) || (firstPlayerInfo.socket.id === socket.id); const isMyOwnGame = (userId && pendingGame.ownerUserId === userId) || (firstPlayerInfo.socket.id === socket.id);
if (isMyOwnGame) continue; if (isMyOwnGame) continue;
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) { if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) {
gameIdToJoin = id; break; gameIdToJoin = id; break;
} }
} }
} }
// Если не нашли с предпочтительным оппонентом, ищем любую свободную (не нашу)
if (!gameIdToJoin && this.pendingPvPGames.length > 0) { if (!gameIdToJoin && this.pendingPvPGames.length > 0) {
for (const id of this.pendingPvPGames) { for (const id of this.pendingPvPGames) {
const pendingGame = this.games[id]; const pendingGame = this.games[id];
@ -156,11 +149,16 @@ class GameManager {
} }
if (gameIdToJoin) { if (gameIdToJoin) {
// Присоединяемся к найденной игре. GameInstance.addPlayer сам назначит нужного персонажа второму игроку.
this.joinGame(socket, gameIdToJoin, userId); this.joinGame(socket, gameIdToJoin, userId);
} else { } else {
this.createGame(socket, 'pvp', chosenCharacterKey, userId); // Если свободных игр нет, создаем новую с выбранным персонажем
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, userId);
// Клиент получит 'gameCreated', а 'noPendingGamesFound' используется для информационного сообщения
socket.emit('noPendingGamesFound', { socket.emit('noPendingGamesFound', {
message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.', message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.',
gameId: this.userToPendingGame[identifier], // ID только что созданной игры
yourPlayerId: GAME_CONFIG.PLAYER_ID // При создании всегда PLAYER_ID
}); });
} }
} }
@ -172,17 +170,7 @@ class GameManager {
game.processPlayerAction(socketId, actionData); game.processPlayerAction(socketId, actionData);
} else { } else {
const playerSocket = this.io.sockets.sockets.get(socketId); const playerSocket = this.io.sockets.sockets.get(socketId);
if (playerSocket) playerSocket.emit('gameError', { message: 'Ошибка: игровая сессия потеряна.' }); if (playerSocket) playerSocket.emit('gameError', { message: 'Ошибка: игровая сессия потеряна для этого действия.' });
}
}
requestRestart(socketId, gameId) {
const game = this.games[gameId];
if (game && game.players[socketId]) {
game.handleVoteRestart(socketId);
} else {
const playerSocket = this.io.sockets.sockets.get(socketId);
if (playerSocket) playerSocket.emit('gameError', { message: 'Не удалось перезапустить: сессия не найдена.' });
} }
} }
@ -192,7 +180,9 @@ class GameManager {
if (gameId && this.games[gameId]) { if (gameId && this.games[gameId]) {
const game = this.games[gameId]; const game = this.games[gameId];
console.log(`[GameManager] Игрок ${socketId} (userId: ${userId}) отключился от игры ${gameId}.`); const playerInfo = game.players[socketId];
const username = playerInfo?.socket?.userData?.username || socketId;
console.log(`[GameManager] Игрок ${username} (socket: ${socketId}, userId: ${userId}) отключился от игры ${gameId}.`);
game.removePlayer(socketId); game.removePlayer(socketId);
if (game.playerCount === 0) { if (game.playerCount === 0) {
@ -200,7 +190,6 @@ class GameManager {
delete this.games[gameId]; delete this.games[gameId];
const gameIndexPending = this.pendingPvPGames.indexOf(gameId); const gameIndexPending = this.pendingPvPGames.indexOf(gameId);
if (gameIndexPending > -1) this.pendingPvPGames.splice(gameIndexPending, 1); if (gameIndexPending > -1) this.pendingPvPGames.splice(gameIndexPending, 1);
// Удаляем из userToPendingGame, если игра была там по любому ключу
for (const key in this.userToPendingGame) { for (const key in this.userToPendingGame) {
if (this.userToPendingGame[key] === gameId) delete this.userToPendingGame[key]; if (this.userToPendingGame[key] === gameId) delete this.userToPendingGame[key];
} }
@ -209,26 +198,32 @@ class GameManager {
if (!this.pendingPvPGames.includes(gameId)) { if (!this.pendingPvPGames.includes(gameId)) {
this.pendingPvPGames.push(gameId); this.pendingPvPGames.push(gameId);
} }
// Обновляем ownerUserId и userToPendingGame для оставшегося игрока
const remainingPlayerSocketId = Object.keys(game.players)[0]; const remainingPlayerSocketId = Object.keys(game.players)[0];
const remainingPlayerSocket = game.players[remainingPlayerSocketId]?.socket; const remainingPlayerSocket = game.players[remainingPlayerSocketId]?.socket;
const remainingUserId = remainingPlayerSocket?.userData?.userId; const remainingUserId = remainingPlayerSocket?.userData?.userId;
const newIdentifier = remainingUserId || remainingPlayerSocketId; const newIdentifier = remainingUserId || remainingPlayerSocketId;
game.ownerUserId = remainingUserId; // Устанавливаем нового владельца (может быть null) game.ownerUserId = remainingUserId;
this.userToPendingGame[newIdentifier] = gameId; // Связываем нового владельца this.userToPendingGame[newIdentifier] = gameId;
// Удаляем старую привязку отключившегося, если она была и не совпадает с новой
if (identifier !== newIdentifier && this.userToPendingGame[identifier] === gameId) { if (identifier !== newIdentifier && this.userToPendingGame[identifier] === gameId) {
delete this.userToPendingGame[identifier]; delete this.userToPendingGame[identifier];
} }
console.log(`[GameManager] Игра ${gameId} возвращена в список ожидания PvP. Новый владелец: ${newIdentifier}`); console.log(`[GameManager] Игра ${gameId} возвращена в список ожидания PvP. Новый владелец: ${newIdentifier}`);
this.broadcastAvailablePvPGames(); this.broadcastAvailablePvPGames();
} }
} else { // Если игрок не был в активной игре, но мог иметь ожидающую } else {
this._removePreviousPendingGames(socketId, identifier); const pendingGameIdToRemove = this.userToPendingGame[identifier];
if (pendingGameIdToRemove && this.games[pendingGameIdToRemove] && this.games[pendingGameIdToRemove].playerCount === 1) {
console.log(`[GameManager] Игрок ${socketId} (identifier: ${identifier}) отключился, удаляем его ожидающую игру ${pendingGameIdToRemove}`);
delete this.games[pendingGameIdToRemove];
const idx = this.pendingPvPGames.indexOf(pendingGameIdToRemove);
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
delete this.userToPendingGame[identifier];
this.broadcastAvailablePvPGames();
} }
delete this.socketToGame[socketId]; // Всегда удаляем эту связь }
delete this.socketToGame[socketId];
} }
getAvailablePvPGamesListForClient() { getAvailablePvPGamesListForClient() {
@ -236,16 +231,50 @@ class GameManager {
.map(gameId => { .map(gameId => {
const game = this.games[gameId]; const game = this.games[gameId];
if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) { if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
let firstPlayerName = 'Игрок'; let firstPlayerUsername = 'Игрок';
let firstPlayerCharacterName = '';
if (game.players && Object.keys(game.players).length > 0) { if (game.players && Object.keys(game.players).length > 0) {
const firstPlayerSocketId = Object.keys(game.players)[0]; const firstPlayerSocketId = Object.keys(game.players)[0];
const firstPlayerInfo = game.players[firstPlayerSocketId]; const firstPlayerInfo = game.players[firstPlayerSocketId];
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey) {
const charData = gameData[firstPlayerInfo.chosenCharacterKey + 'BaseStats']; if (firstPlayerInfo) {
if (charData) firstPlayerName = charData.name; if (firstPlayerInfo.socket?.userData?.username) {
firstPlayerUsername = firstPlayerInfo.socket.userData.username;
}
const charKey = firstPlayerInfo.chosenCharacterKey;
if (charKey) {
let charBaseStats;
if (charKey === 'elena') {
charBaseStats = gameData.playerBaseStats;
} else if (charKey === 'almagest') {
charBaseStats = gameData.almagestBaseStats;
}
// Баларда не должно быть в pending PvP как создателя
if (charBaseStats && charBaseStats.name) {
firstPlayerCharacterName = charBaseStats.name;
} else {
console.warn(`[GameManager] getAvailablePvPGamesList: Не удалось найти имя для charKey '${charKey}' в gameData.`);
firstPlayerCharacterName = charKey; // В крайнем случае ключ
}
} else {
console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo.chosenCharacterKey отсутствует для игры ${gameId}`);
} }
} }
return { id: gameId, status: `Ожидает 1 игрока (Создал: ${firstPlayerName})` }; }
let statusString = `Ожидает 1 игрока (Создал: ${firstPlayerUsername}`;
if (firstPlayerCharacterName) {
statusString += ` за ${firstPlayerCharacterName}`;
}
statusString += `)`;
return {
id: gameId,
status: statusString
};
} }
return null; return null;
}) })
@ -256,17 +285,24 @@ class GameManager {
this.io.emit('availablePvPGamesList', this.getAvailablePvPGamesListForClient()); this.io.emit('availablePvPGamesList', this.getAvailablePvPGamesListForClient());
} }
getActiveGamesList() { getActiveGamesList() { // Для отладки на сервере
return Object.values(this.games).map(game => { return Object.values(this.games).map(game => {
let playerSlotChar = game.gameState?.player?.name || (game.playerCharacterKey ? gameData[game.playerCharacterKey + 'BaseStats']?.name : 'N/A'); let playerSlotChar = game.gameState?.player?.name || (game.playerCharacterKey ? gameData[game.playerCharacterKey === 'elena' ? 'playerBaseStats' : (game.playerCharacterKey === 'almagest' ? 'almagestBaseStats' : null)]?.name : 'N/A');
let opponentSlotChar = game.gameState?.opponent?.name || (game.opponentCharacterKey ? gameData[game.opponentCharacterKey + 'BaseStats']?.name : 'N/A'); let opponentSlotChar = game.gameState?.opponent?.name || (game.opponentCharacterKey ? gameData[game.opponentCharacterKey === 'elena' ? 'playerBaseStats' : (game.opponentCharacterKey === 'almagest' ? 'almagestBaseStats' : (game.opponentCharacterKey === 'balard' ? 'opponentBaseStats' : null))]?.name : 'N/A');
if (game.mode === 'pvp' && game.playerCount === 1 && !game.opponentCharacterKey) opponentSlotChar = 'Ожидание...';
if (game.mode === 'pvp' && game.playerCount === 1 && !game.opponentCharacterKey && game.gameState && !game.gameState.isGameOver) {
opponentSlotChar = 'Ожидание...';
}
return { return {
id: game.id, mode: game.mode, playerCount: game.playerCount, id: game.id.substring(0,8),
mode: game.mode,
playerCount: game.playerCount,
isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A', isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A',
playerSlot: playerSlotChar, opponentSlot: opponentSlotChar, playerSlot: playerSlotChar,
ownerUserId: game.ownerUserId || 'N/A' opponentSlot: opponentSlotChar,
ownerUserId: game.ownerUserId || 'N/A',
pending: this.pendingPvPGames.includes(game.id)
}; };
}); });
} }