Павел. Изменение интерфейса.
This commit is contained in:
parent
6d3d4ef6f6
commit
0d6d224a99
99
bc.js
99
bc.js
@ -1,42 +1,50 @@
|
||||
// bc.js (или server.js - ваш основной файл сервера)
|
||||
// КОД ОСТАЕТСЯ БЕЗ ИЗМЕНЕНИЙ ОТНОСИТЕЛЬНО ПРЕДЫДУЩЕЙ ВЕРСИИ,
|
||||
// ГДЕ УЖЕ БЫЛА ДОБАВЛЕНА ПЕРЕДАЧА characterKey В GameManager
|
||||
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const socketIo = require('socket.io');
|
||||
const path = require('path');
|
||||
|
||||
// Серверные модули
|
||||
const GameManager = require('./server_modules/gameManager'); // GameManager будет изменен
|
||||
const authController = require('./server_modules/auth');
|
||||
const GameManager = require('./server_modules/gameManager');
|
||||
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 server = http.createServer(app);
|
||||
const io = socketIo(server);
|
||||
const io = socketIo(server, {
|
||||
cors: {
|
||||
origin: "*", // Разрешить все источники для простоты разработки. В продакшене укажите конкретный домен клиента.
|
||||
methods: ["GET", "POST"]
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3200;
|
||||
|
||||
// Статическое обслуживание файлов из папки '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) => {
|
||||
console.log('[Server] New client connected:', socket.id);
|
||||
console.log(`[Server BC.JS] New client connected: ${socket.id}`);
|
||||
|
||||
// При подключении нового клиента, отправляем ему текущий список доступных PvP игр
|
||||
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
||||
socket.emit('availablePvPGamesList', availableGames);
|
||||
|
||||
// Обработчик запроса на обновление списка PvP игр
|
||||
socket.on('requestPvPGameList', () => {
|
||||
const currentAvailableGames = gameManager.getAvailablePvPGamesListForClient();
|
||||
socket.emit('availablePvPGamesList', currentAvailableGames);
|
||||
});
|
||||
|
||||
// --- Аутентификация ---
|
||||
socket.on('register', async (data) => {
|
||||
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') {
|
||||
socket.emit('registerResponse', { success: false, message: 'Некорректные данные запроса.' });
|
||||
socket.emit('registerResponse', { success: false, message: 'Некорректные данные запроса для регистрации.' });
|
||||
return;
|
||||
}
|
||||
const result = await authController.registerUser(data.username, data.password);
|
||||
@ -46,12 +54,13 @@ io.on('connection', (socket) => {
|
||||
socket.on('login', async (data) => {
|
||||
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') {
|
||||
socket.emit('loginResponse', { success: false, message: 'Некорректные данные запроса.' });
|
||||
socket.emit('loginResponse', { success: false, message: 'Некорректные данные запроса для входа.' });
|
||||
return;
|
||||
}
|
||||
const result = await authController.loginUser(data.username, data.password);
|
||||
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!`);
|
||||
}
|
||||
socket.emit('loginResponse', result);
|
||||
@ -61,21 +70,24 @@ io.on('connection', (socket) => {
|
||||
const username = socket.userData?.username || socket.id;
|
||||
console.log(`[Server BC.JS] Received 'logout' event from ${username}.`);
|
||||
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}.`);
|
||||
}
|
||||
// Можно отправить подтверждение выхода, если нужно
|
||||
// socket.emit('logoutResponse', { success: true, message: 'Вы успешно вышли.' });
|
||||
});
|
||||
|
||||
// --- Управление Играми ---
|
||||
socket.on('createGame', (data) => {
|
||||
if (!socket.userData) {
|
||||
socket.emit('gameError', { message: "Ошибка: Вы не авторизованы для создания игры." });
|
||||
return;
|
||||
}
|
||||
const mode = data?.mode || 'ai';
|
||||
const characterKey = (data?.characterKey === 'almagest') ? 'almagest' : 'elena';
|
||||
const mode = data?.mode || 'ai'; // 'ai' или 'pvp'
|
||||
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}`);
|
||||
// Передаем socket.userData.userId в GameManager, если он нужен для идентификации пользователя между сессиями
|
||||
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}`);
|
||||
if (data && typeof data.gameId === 'string') {
|
||||
// Передаем socket.userData.userId
|
||||
gameManager.joinGame(socket, data.gameId, socket.userData.userId);
|
||||
} else {
|
||||
socket.emit('gameError', { message: 'Ошибка присоединения: неверный формат ID игры.' });
|
||||
@ -100,38 +111,56 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
const characterKey = (data?.characterKey === 'almagest') ? 'almagest' : 'elena';
|
||||
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);
|
||||
});
|
||||
|
||||
// --- Игровые Действия ---
|
||||
socket.on('playerAction', (data) => {
|
||||
if (!socket.userData) return;
|
||||
if (!socket.userData) {
|
||||
// Если пользователь не авторизован, но пытается совершить действие (маловероятно при правильной логике клиента)
|
||||
socket.emit('gameError', { message: "Ошибка: Вы не авторизованы для совершения этого действия." });
|
||||
return;
|
||||
}
|
||||
// GameManager сам проверит, принадлежит ли этот сокет к активной игре
|
||||
gameManager.handlePlayerAction(socket.id, data);
|
||||
});
|
||||
|
||||
socket.on('requestRestart', (data) => {
|
||||
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 игры.' });
|
||||
}
|
||||
});
|
||||
// Обработчик 'requestRestart' удален, так как эта функциональность заменена на "возврат в меню"
|
||||
|
||||
// --- Отключение Клиента ---
|
||||
socket.on('disconnect', (reason) => {
|
||||
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
|
||||
// (например, для удаления его ожидающих игр или корректного завершения активной игры)
|
||||
const userId = socket.userData?.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, () => {
|
||||
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(`===============================`);
|
||||
});
|
@ -9,196 +9,6 @@
|
||||
<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 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>
|
||||
<body>
|
||||
|
||||
@ -233,7 +43,7 @@
|
||||
<div id="game-setup" style="display: none;"> <!-- Секция Настройки Игры (после логина) -->
|
||||
<h2>Настройка Игры</h2>
|
||||
<div>
|
||||
<button id="create-ai-game">Играть против AI (Балард)</button> <!-- Уточнили против кого AI -->
|
||||
<button id="create-ai-game">Играть против AI (Балард)</button>
|
||||
</div>
|
||||
<hr style="margin: 15px 0;">
|
||||
<div>
|
||||
@ -246,7 +56,7 @@
|
||||
<label for="char-elena"><i class="fas fa-hat-wizard"></i> Елена</label>
|
||||
|
||||
<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>
|
||||
<!-- === Конец блока выбора персонажа === -->
|
||||
|
||||
@ -267,7 +77,6 @@
|
||||
|
||||
<div class="game-wrapper" style="display: none;"> <!-- Игровая арена, изначально скрыта -->
|
||||
<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>
|
||||
</header>
|
||||
<main class="battle-arena-container">
|
||||
@ -276,10 +85,8 @@
|
||||
<section id="player-panel" class="fighter-panel">
|
||||
<div class="panel-header">
|
||||
<div class="character-visual">
|
||||
<!-- Аватар будет обновляться из ui.js -->
|
||||
<img src="images/elena_avatar.webp" alt="Аватар игрока 1" class="avatar-image player-avatar">
|
||||
</div>
|
||||
<!-- Имя будет обновляться из ui.js -->
|
||||
<h2 id="player-name" class="fighter-name">
|
||||
<i class="fas fa-hat-wizard icon-player"></i> Елена
|
||||
</h2>
|
||||
@ -294,7 +101,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Тип ресурса (mana/stamina/dark-energy) и иконка будут обновляться из ui.js -->
|
||||
<div class="stat-bar-container resource mana">
|
||||
<div class="bar-icon"><i class="fas fa-flask"></i></div>
|
||||
<div class="bar-wrapper">
|
||||
@ -323,7 +129,6 @@
|
||||
</section>
|
||||
|
||||
<section id="controls-panel" class="controls-panel-new">
|
||||
<!-- Индикатор хода будет обновляться из ui.js -->
|
||||
<h3 id="turn-indicator">Ход: Игрок 1</h3>
|
||||
<div class="controls-layout">
|
||||
<div class="control-group basic-actions">
|
||||
@ -332,7 +137,6 @@
|
||||
</div>
|
||||
<div class="control-group ability-list">
|
||||
<h4><i class="fas fa-book-sparkles"></i> Способности</h4>
|
||||
<!-- Способности будут загружены из client.js -->
|
||||
<div id="abilities-grid" class="abilities-grid">
|
||||
<p class="placeholder-text">Загрузка способностей...</p>
|
||||
</div>
|
||||
@ -346,10 +150,8 @@
|
||||
<section id="opponent-panel" class="fighter-panel">
|
||||
<div class="panel-header">
|
||||
<div class="character-visual">
|
||||
<!-- Аватар будет обновляться из ui.js -->
|
||||
<img src="images/balard_avatar.jpg" alt="Аватар игрока 2" class="avatar-image opponent-avatar">
|
||||
</div>
|
||||
<!-- Имя будет обновляться из ui.js -->
|
||||
<h2 id="opponent-name" class="fighter-name">
|
||||
<i class="fas fa-khanda icon-opponent"></i> Балард
|
||||
</h2>
|
||||
@ -364,7 +166,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Тип ресурса и иконка будут обновляться из ui.js -->
|
||||
<div class="stat-bar-container resource stamina">
|
||||
<div class="bar-icon"><i class="fas fa-fire-alt"></i></div>
|
||||
<div class="bar-wrapper">
|
||||
@ -405,13 +206,16 @@
|
||||
<div id="game-over-screen" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<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> <!-- Конец .game-wrapper -->
|
||||
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
@ -7,14 +7,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// --- Состояние клиента ---
|
||||
let currentGameState = null;
|
||||
let myPlayerId = null; // Технический ID слота ('player' или 'opponent')
|
||||
let myCharacterKey = null; // Ключ моего персонажа ('elena' или 'almagest')
|
||||
let opponentCharacterKey = null; // Ключ персонажа оппонента
|
||||
let myPlayerId = null; // Технический ID слота, который занимает ЭТОТ клиент ('player' или 'opponent')
|
||||
let myCharacterKey = null;
|
||||
let opponentCharacterKey = null;
|
||||
let currentGameId = null;
|
||||
let playerBaseStatsServer = null; // Статы персонажа, которым УПРАВЛЯЕТ этот клиент
|
||||
let opponentBaseStatsServer = null; // Статы персонажа-противника этого клиента
|
||||
let playerAbilitiesServer = null; // Способности персонажа, которым УПРАВЛЯЕТ этот клиент
|
||||
let opponentAbilitiesServer = null; // Способности персонажа-противника
|
||||
let playerBaseStatsServer = null; // Статы персонажа, которым УПРАВЛЯЕТ этот клиент (приходят от сервера как data.playerBaseStats)
|
||||
let opponentBaseStatsServer = null; // Статы персонажа-оппонента этого клиента (приходят от сервера как data.opponentBaseStats)
|
||||
let playerAbilitiesServer = null;
|
||||
let opponentAbilitiesServer = null;
|
||||
let isLoggedIn = false;
|
||||
let loggedInUsername = '';
|
||||
|
||||
@ -42,7 +42,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Игровая Арена
|
||||
const gameWrapper = document.querySelector('.game-wrapper');
|
||||
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');
|
||||
|
||||
console.log('Client.js DOMContentLoaded. Initializing elements...');
|
||||
@ -91,11 +91,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
function hideGameOverModal() {
|
||||
const hiddenClass = (window.GAME_CONFIG && window.GAME_CONFIG.CSS_CLASS_HIDDEN) ? window.GAME_CONFIG.CSS_CLASS_HIDDEN : 'hidden';
|
||||
if (gameOverScreen && !gameOverScreen.classList.contains(hiddenClass)) {
|
||||
console.log('[Client.js DEBUG] Hiding GameOver Modal.');
|
||||
gameOverScreen.classList.add(hiddenClass);
|
||||
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.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 = '';
|
||||
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;
|
||||
showAuthScreen();
|
||||
setGameStatusMessage("Вы вышли из системы.");
|
||||
});
|
||||
@ -227,24 +245,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (restartGameButton) {
|
||||
restartGameButton.addEventListener('click', () => {
|
||||
if (currentGameId && currentGameState && currentGameState.isGameOver && isLoggedIn) {
|
||||
socket.emit('requestRestart', { gameId: currentGameId });
|
||||
setGameStatusMessage("Запрос на рестарт отправлен...");
|
||||
restartGameButton.disabled = true;
|
||||
} else {
|
||||
if (!currentGameId && isLoggedIn) {
|
||||
alert("Ошибка: ID текущей игры не определен. Невозможно запросить рестарт.");
|
||||
showGameSelectionScreen(loggedInUsername);
|
||||
} else if (!isLoggedIn) {
|
||||
if (returnToMenuButton) {
|
||||
returnToMenuButton.addEventListener('click', () => {
|
||||
if (!isLoggedIn) {
|
||||
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() {
|
||||
const abilitiesGrid = document.getElementById('abilities-grid');
|
||||
if (!abilitiesGrid || !window.gameUI || !window.GAME_CONFIG) {
|
||||
@ -254,8 +280,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
abilitiesGrid.innerHTML = '';
|
||||
const config = window.GAME_CONFIG;
|
||||
|
||||
const abilitiesToDisplay = playerAbilitiesServer;
|
||||
const baseStatsForResource = playerBaseStatsServer;
|
||||
const abilitiesToDisplay = playerAbilitiesServer; // Используем данные, сохраненные при gameStarted
|
||||
const baseStatsForResource = playerBaseStatsServer; // Используем данные, сохраненные при gameStarted
|
||||
|
||||
if (!abilitiesToDisplay || abilitiesToDisplay.length === 0 || !baseStatsForResource) {
|
||||
abilitiesGrid.innerHTML = '<p class="placeholder-text">Нет доступных способностей.</p>';
|
||||
@ -271,7 +297,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
let descriptionText = ability.description;
|
||||
if (typeof ability.descriptionFunction === 'function') {
|
||||
const targetStatsForDesc = opponentBaseStatsServer;
|
||||
const targetStatsForDesc = opponentBaseStatsServer; // Используем данные, сохраненные при gameStarted
|
||||
descriptionText = ability.descriptionFunction(config, targetStatsForDesc);
|
||||
}
|
||||
|
||||
@ -336,6 +362,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!isLoggedIn) {
|
||||
showAuthScreen();
|
||||
} else {
|
||||
console.log(`[Client] Reconnected as ${loggedInUsername}. Requesting state or showing game selection.`);
|
||||
showGameSelectionScreen(loggedInUsername);
|
||||
}
|
||||
});
|
||||
@ -343,15 +370,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log('[Client] Disconnected from server:', reason);
|
||||
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();
|
||||
});
|
||||
|
||||
@ -373,7 +391,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
socket.on('gameCreated', (data) => {
|
||||
if (!isLoggedIn) return;
|
||||
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}`);
|
||||
if (data.mode === 'pvp') {
|
||||
if (gameIdInput) gameIdInput.value = currentGameId;
|
||||
@ -386,18 +404,39 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
socket.on('gameStarted', (data) => {
|
||||
if (!isLoggedIn) return;
|
||||
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;
|
||||
myPlayerId = data.yourPlayerId;
|
||||
myPlayerId = data.yourPlayerId; // Сервер присылает ID слота, который занимает ЭТОТ клиент
|
||||
currentGameState = data.initialGameState;
|
||||
|
||||
// Сервер присылает playerBaseStats и opponentBaseStats ОТНОСИТЕЛЬНО этого клиента
|
||||
// То есть, data.playerBaseStats - это статы персонажа, которым управляет этот клиент
|
||||
// data.opponentBaseStats - это статы персонажа-оппонента для этого клиента
|
||||
playerBaseStatsServer = data.playerBaseStats;
|
||||
opponentBaseStatsServer = data.opponentBaseStats;
|
||||
playerAbilitiesServer = data.playerAbilities;
|
||||
opponentAbilitiesServer = data.opponentAbilities;
|
||||
|
||||
myCharacterKey = playerBaseStatsServer?.characterKey;
|
||||
opponentCharacterKey = opponentBaseStatsServer?.characterKey;
|
||||
console.log(`[Client] Game started! My Slot ID: ${myPlayerId}, My Character: ${myCharacterKey}, Opponent Character: ${opponentCharacterKey}`);
|
||||
myCharacterKey = playerBaseStatsServer?.characterKey; // Ключ персонажа этого клиента
|
||||
opponentCharacterKey = opponentBaseStatsServer?.characterKey; // Ключ персонажа оппонента этого клиента
|
||||
|
||||
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) {
|
||||
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' };
|
||||
}
|
||||
|
||||
// Глобальные переменные для ui.js
|
||||
window.gameState = currentGameState;
|
||||
window.gameData = {
|
||||
playerBaseStats: playerBaseStatsServer,
|
||||
opponentBaseStats: opponentBaseStatsServer,
|
||||
playerAbilities: playerAbilitiesServer,
|
||||
opponentAbilities: opponentAbilitiesServer
|
||||
window.gameData = { // Эти данные используются в ui.js для отображения панелей
|
||||
playerBaseStats: playerBaseStatsServer, // Статы "моего" персонажа
|
||||
opponentBaseStats: opponentBaseStatsServer, // Статы "моего оппонента"
|
||||
playerAbilities: playerAbilitiesServer, // Способности "моего" персонажа
|
||||
opponentAbilities: opponentAbilitiesServer // Способности "моего оппонента"
|
||||
};
|
||||
window.myPlayerId = myPlayerId;
|
||||
window.myPlayerId = myPlayerId; // Технический ID слота этого клиента
|
||||
|
||||
showGameScreen();
|
||||
initializeAbilityButtons();
|
||||
@ -423,14 +463,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) {
|
||||
data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type));
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (window.gameUI && typeof gameUI.updateUI === 'function') {
|
||||
console.log('[Client] Calling gameUI.updateUI() after style reset and rAF in gameStarted.');
|
||||
gameUI.updateUI();
|
||||
}
|
||||
});
|
||||
|
||||
hideGameOverModal();
|
||||
if (restartGameButton) {
|
||||
restartGameButton.disabled = true;
|
||||
restartGameButton.dataset.gameIdForRestart = '';
|
||||
if (returnToMenuButton) {
|
||||
returnToMenuButton.disabled = true;
|
||||
}
|
||||
setGameStatusMessage("");
|
||||
});
|
||||
@ -438,7 +481,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
socket.on('gameStateUpdate', (data) => {
|
||||
if (!isLoggedIn || !currentGameId) return;
|
||||
currentGameState = data.gameState;
|
||||
window.gameState = currentGameState;
|
||||
window.gameState = currentGameState; // ui.js использует это для обновления
|
||||
|
||||
if (window.gameUI && typeof gameUI.updateUI === 'function') {
|
||||
gameUI.updateUI();
|
||||
@ -457,23 +500,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
socket.on('gameOver', (data) => {
|
||||
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;
|
||||
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.addToLog === 'function' && data.log) {
|
||||
data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type));
|
||||
}
|
||||
if (window.gameUI && typeof gameUI.showGameOver === 'function') {
|
||||
const playerWon = data.winnerId === myPlayerId;
|
||||
gameUI.showGameOver(playerWon, data.reason);
|
||||
if (restartGameButton) {
|
||||
restartGameButton.disabled = false;
|
||||
restartGameButton.dataset.gameIdForRestart = currentGameId;
|
||||
// opponentCharacterKeyFromClient передается, чтобы ui.js знал, какой персонаж был оппонентом
|
||||
// и мог применить, например, анимацию .dissolving к правильному типу оппонента (Балард/Альмагест)
|
||||
const opponentKeyForModal = window.gameData?.opponentBaseStats?.characterKey;
|
||||
gameUI.showGameOver(playerWon, data.reason, opponentKeyForModal);
|
||||
|
||||
if (returnToMenuButton) {
|
||||
returnToMenuButton.disabled = false;
|
||||
}
|
||||
}
|
||||
setGameStatusMessage("Игра окончена. " + (data.winnerId === myPlayerId ? "Вы победили!" : "Вы проиграли."));
|
||||
setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли."));
|
||||
});
|
||||
|
||||
socket.on('waitingForOpponent', () => {
|
||||
@ -485,37 +540,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!isLoggedIn || !currentGameId) return;
|
||||
const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system';
|
||||
if (window.gameUI && typeof gameUI.addToLog === 'function') {
|
||||
gameUI.addToLog("Противник отключился.", systemLogType);
|
||||
gameUI.addToLog(`Противник (${data.disconnectedCharacterName || 'Игрок'}) отключился.`, systemLogType);
|
||||
}
|
||||
if (currentGameState && !currentGameState.isGameOver) {
|
||||
setGameStatusMessage("Противник отключился. Игра завершена. Вы можете начать новую.", true);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
setGameStatusMessage("Противник отключился. Игра может быть завершена сервером.", true);
|
||||
// Сервер должен прислать 'gameOver', если игра действительно завершается
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('gameError', (data) => {
|
||||
console.error('[Client] Server error:', data.message);
|
||||
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);
|
||||
}
|
||||
setGameStatusMessage(`Ошибка: ${data.message}`, true);
|
||||
@ -532,7 +568,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateAvailableGamesList([]);
|
||||
if (data.gameId) {
|
||||
currentGameId = data.gameId;
|
||||
myPlayerId = data.yourPlayerId;
|
||||
myPlayerId = data.yourPlayerId; // Запоминаем наш технический ID слота
|
||||
if (gameIdInput) gameIdInput.value = currentGameId;
|
||||
console.log(`[Client] New game ${currentGameId} created after no pending games found. My slot: ${myPlayerId}`);
|
||||
}
|
||||
|
292
public/js/ui.js
292
public/js/ui.js
@ -30,7 +30,7 @@
|
||||
controls: {
|
||||
turnIndicator: document.getElementById('turn-indicator'),
|
||||
buttonAttack: document.getElementById('button-attack'),
|
||||
buttonBlock: document.getElementById('button-block'),
|
||||
buttonBlock: document.getElementById('button-block'), // Защита пока не активна
|
||||
abilitiesGrid: document.getElementById('abilities-grid'),
|
||||
},
|
||||
log: {
|
||||
@ -39,7 +39,8 @@
|
||||
gameOver: {
|
||||
screen: document.getElementById('game-over-screen'),
|
||||
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')
|
||||
},
|
||||
gameHeaderTitle: document.querySelector('.game-header h1'),
|
||||
@ -54,106 +55,146 @@
|
||||
if (!logListElement) return;
|
||||
const li = document.createElement('li');
|
||||
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}`;
|
||||
li.className = logTypeClass;
|
||||
logListElement.appendChild(li);
|
||||
// Прокрутка лога вниз
|
||||
requestAnimationFrame(() => { logListElement.scrollTop = logListElement.scrollHeight; });
|
||||
}
|
||||
|
||||
function updateFighterPanelUI(panelRole, fighterState, fighterBaseStats, isControlledByThisClient) {
|
||||
const elements = uiElements[panelRole];
|
||||
const elements = uiElements[panelRole]; // 'player' или 'opponent'
|
||||
const config = window.GAME_CONFIG || {};
|
||||
|
||||
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;
|
||||
}
|
||||
// Если панель была полупрозрачной (из-за отсутствия данных), а теперь данные есть, делаем ее полностью видимой
|
||||
// if (elements.panel && elements.panel.style.opacity !== '1' && fighterState && fighterBaseStats) {
|
||||
// elements.panel.style.opacity = '1';
|
||||
// }
|
||||
|
||||
|
||||
// Обновление имени и иконки персонажа
|
||||
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;
|
||||
|
||||
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)'; }
|
||||
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 += " (Вы)";
|
||||
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;
|
||||
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 currentHp = Math.max(0, fighterState.currentHp);
|
||||
const currentRes = Math.max(0, fighterState.currentResource);
|
||||
|
||||
elements.hpFill.style.width = `${(currentHp / maxHp) * 100}%`;
|
||||
elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`;
|
||||
elements.resourceFill.style.width = `${(currentRes / maxRes) * 100}%`;
|
||||
elements.resourceText.textContent = `${Math.round(currentRes)} / ${fighterBaseStats.maxResource}`;
|
||||
|
||||
const resourceBarContainer = elements[`${panelRole}ResourceBarContainer`];
|
||||
const resourceIconElement = elements[`${panelRole}ResourceTypeIcon`];
|
||||
if (resourceBarContainer && resourceIconElement) {
|
||||
resourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
|
||||
let resourceClass = 'mana'; let iconClass = 'fa-flask';
|
||||
// Обновление типа ресурса и иконки (mana/stamina/dark-energy)
|
||||
const resourceBarContainerToUpdate = (panelRole === 'player') ? uiElements.playerResourceBarContainer : uiElements.opponentResourceBarContainer;
|
||||
const resourceIconElementToUpdate = (panelRole === 'player') ? uiElements.playerResourceTypeIcon : uiElements.opponentResourceTypeIcon;
|
||||
|
||||
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'; }
|
||||
else if (fighterBaseStats.resourceName === 'Темная Энергия') { resourceClass = 'dark-energy'; iconClass = 'fa-skull'; }
|
||||
resourceBarContainer.classList.add(resourceClass);
|
||||
resourceIconElement.className = `fas ${iconClass}`;
|
||||
resourceBarContainerToUpdate.classList.add(resourceClass);
|
||||
resourceIconElementToUpdate.className = `fas ${iconClass}`;
|
||||
}
|
||||
|
||||
// Обновление статуса (Готов/Защищается)
|
||||
const statusText = fighterState.isBlocking ? (config.STATUS_BLOCKING || 'Защищается') : (config.STATUS_READY || 'Готов(а)');
|
||||
elements.status.textContent = statusText;
|
||||
elements.status.classList.toggle(config.CSS_CLASS_BLOCKING || 'blocking', fighterState.isBlocking);
|
||||
|
||||
// Обновление подсветки и рамки панели
|
||||
if (elements.panel) {
|
||||
let glowColorVar = '--panel-glow-opponent'; let borderColorVar = '--accent-opponent';
|
||||
if (fighterBaseStats.characterKey === 'elena') { glowColorVar = '--panel-glow-player'; borderColorVar = '--accent-player'; }
|
||||
else if (fighterBaseStats.characterKey === 'almagest') { glowColorVar = '--panel-glow-opponent'; borderColorVar = 'var(--accent-almagest)'; } // Цвет рамки Альмагест
|
||||
elements.panel.style.borderColor = borderColorVar; // Прямое присвоение, т.к. var() не сработает для accent-almagest если он не в :root
|
||||
elements.panel.style.boxShadow = `0 0 15px var(${glowColorVar}), inset 0 0 10px rgba(0, 0, 0, 0.3)`;
|
||||
let glowColorVar = '--panel-glow-opponent';
|
||||
let borderColorVar = 'var(--accent-opponent)'; // По умолчанию для оппонента
|
||||
if (fighterBaseStats.characterKey === 'elena') { glowColorVar = '--panel-glow-player'; borderColorVar = 'var(--accent-player)'; }
|
||||
else if (fighterBaseStats.characterKey === 'almagest') { glowColorVar = 'var(--panel-glow-opponent)'; borderColorVar = 'var(--accent-almagest)'; } // Для Альмагест используется ее цвет рамки
|
||||
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) {
|
||||
const config = window.GAME_CONFIG || {};
|
||||
if (!effectsArray || effectsArray.length === 0) return 'Нет';
|
||||
|
||||
return effectsArray.map(eff => {
|
||||
let effectClasses = config.CSS_CLASS_EFFECT || 'effect';
|
||||
const title = `${eff.name}${eff.description ? ` - ${eff.description}` : ''} (Осталось: ${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';
|
||||
else if (eff.grantsBlock) effectClasses += ' effect-block';
|
||||
else effectClasses += ' effect-buff';
|
||||
|
||||
if (eff.isFullSilence || eff.id.startsWith('playerSilencedOn_') || (eff.type === config.ACTION_TYPE_DISABLE && !eff.grantsBlock) ) {
|
||||
effectClasses += ' effect-stun'; // Стан/безмолвие
|
||||
} 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>`;
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
function updateEffectsUI(currentGameState) {
|
||||
if (!currentGameState || !uiElements.player.buffsList || !uiElements.opponent.buffsList) return;
|
||||
const mySlotId = window.myPlayerId; // Наш слот ('player' или 'opponent')
|
||||
if (!currentGameState || !window.GAME_CONFIG) { return; }
|
||||
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 myState = currentGameState[mySlotId];
|
||||
if (uiElements.player && myState && myState.activeEffects) {
|
||||
uiElements.player.buffsList.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));
|
||||
const myState = currentGameState[mySlotId]; // Состояние персонажа этого клиента
|
||||
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 || (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 && !(e.type === window.GAME_CONFIG.ACTION_TYPE_HEAL && e.turnsLeft > 0) ));
|
||||
}
|
||||
|
||||
const opponentState = currentGameState[opponentSlotId];
|
||||
if (uiElements.opponent && opponentState && opponentState.activeEffects) {
|
||||
uiElements.opponent.buffsList.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));
|
||||
const opponentState = currentGameState[opponentSlotId]; // Состояние оппонента этого клиента
|
||||
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 || (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 && !(e.type === window.GAME_CONFIG.ACTION_TYPE_HEAL && e.turnsLeft > 0) ));
|
||||
}
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
const currentGameState = window.gameState;
|
||||
const gameDataGlobal = window.gameData;
|
||||
const configGlobal = window.GAME_CONFIG;
|
||||
const myActualPlayerId = window.myPlayerId; // Слот, который занимает ЭТОТ клиент ('player' или 'opponent')
|
||||
const currentGameState = window.gameState; // Глобальное состояние игры
|
||||
const gameDataGlobal = window.gameData; // Глобальные данные (статы, абилки) для этого клиента
|
||||
const configGlobal = window.GAME_CONFIG; // Глобальный конфиг
|
||||
const myActualPlayerId = window.myPlayerId; // Технический ID слота этого клиента
|
||||
|
||||
if (!currentGameState || !gameDataGlobal || !configGlobal || !myActualPlayerId) {
|
||||
console.warn("updateUI: Отсутствуют глобальные gameState, gameData, GAME_CONFIG или myActualPlayerId.");
|
||||
@ -164,35 +205,64 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Определяем ID слота того, кто сейчас ходит
|
||||
// Определяем, чей сейчас ход по ID слота
|
||||
const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID;
|
||||
|
||||
// Обновление панелей бойцов
|
||||
// Определяем 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);
|
||||
} 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);
|
||||
} else {
|
||||
if (uiElements.opponent.panel) uiElements.opponent.panel.style.opacity = '0.5';
|
||||
}
|
||||
|
||||
updateEffectsUI(currentGameState);
|
||||
|
||||
// Обновление заголовка игры (Имя1 vs Имя2)
|
||||
if (uiElements.gameHeaderTitle && gameDataGlobal.playerBaseStats && gameDataGlobal.opponentBaseStats) {
|
||||
const myName = gameDataGlobal.playerBaseStats.name;
|
||||
const opponentName = gameDataGlobal.opponentBaseStats.name;
|
||||
const myName = gameDataGlobal.playerBaseStats.name; // Имя моего персонажа
|
||||
const opponentName = gameDataGlobal.opponentBaseStats.name; // Имя моего оппонента
|
||||
const myKey = gameDataGlobal.playerBaseStats.characterKey;
|
||||
const opponentKey = gameDataGlobal.opponentBaseStats.characterKey;
|
||||
|
||||
let myClass = 'title-player';
|
||||
if (myKey === 'elena') myClass = 'title-enchantress';
|
||||
else if (myKey === 'almagest') myClass = 'title-sorceress';
|
||||
if (myKey === 'elena') myClass = 'title-enchantress'; else if (myKey === 'almagest') myClass = 'title-sorceress';
|
||||
|
||||
let opponentClass = 'title-opponent';
|
||||
if (opponentKey === 'elena') opponentClass = 'title-enchantress';
|
||||
else if (opponentKey === 'almagest') opponentClass = 'title-sorceress';
|
||||
else if (opponentKey === 'balard') opponentClass = 'title-knight';
|
||||
if (opponentKey === 'elena') opponentClass = 'title-enchantress'; 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>`;
|
||||
}
|
||||
|
||||
// Обновление индикатора хода
|
||||
if (uiElements.controls.turnIndicator) {
|
||||
const currentTurnActorState = currentGameState[actorSlotWhoseTurnItIs];
|
||||
// Имя того, чей ход, берем из gameState по ID слота
|
||||
const currentTurnActorState = currentGameState[actorSlotWhoseTurnItIs]; // 'player' или 'opponent'
|
||||
const currentTurnName = currentTurnActorState?.name || 'Неизвестно';
|
||||
uiElements.controls.turnIndicator.textContent = `Ход: ${currentTurnName}`;
|
||||
|
||||
const currentTurnCharacterKey = currentTurnActorState?.characterKey;
|
||||
let turnColor = 'var(--turn-color)';
|
||||
if (currentTurnCharacterKey === 'elena') turnColor = 'var(--accent-player)';
|
||||
@ -201,31 +271,35 @@
|
||||
uiElements.controls.turnIndicator.style.color = turnColor;
|
||||
}
|
||||
|
||||
const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId;
|
||||
// Управление активностью кнопок
|
||||
const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId; // Может ли ЭТОТ клиент ходить
|
||||
const isGameActive = !currentGameState.isGameOver;
|
||||
|
||||
// Кнопка атаки
|
||||
if (uiElements.controls.buttonAttack) {
|
||||
uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive);
|
||||
const myCharKey = gameDataGlobal.playerBaseStats.characterKey;
|
||||
const myState = currentGameState[myActualPlayerId];
|
||||
const myCharKey = gameDataGlobal.playerBaseStats?.characterKey;
|
||||
const myStateForAttackBuff = currentGameState[myActualPlayerId]; // Состояние моего персонажа
|
||||
let attackBuffId = null;
|
||||
if (myCharKey === 'elena') attackBuffId = configGlobal.ABILITY_ID_NATURE_STRENGTH;
|
||||
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);
|
||||
} else {
|
||||
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 actingPlayerResourceName = gameDataGlobal.playerBaseStats.resourceName;
|
||||
// Кнопки способностей
|
||||
const actingPlayerState = currentGameState[myActualPlayerId]; // Состояние моего персонажа
|
||||
const actingPlayerAbilities = gameDataGlobal.playerAbilities; // Способности моего персонажа
|
||||
const actingPlayerResourceName = gameDataGlobal.playerBaseStats?.resourceName;
|
||||
|
||||
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;
|
||||
return;
|
||||
}
|
||||
@ -234,17 +308,21 @@
|
||||
if (!ability) { button.disabled = true; return; }
|
||||
|
||||
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 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 silenceTurnsLeft = isGenerallySilenced ? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0)
|
||||
: (isSpecificallySilenced ? (actingPlayerState.disabledAbilities.find(dis => dis.abilityId === abilityId)?.turnsLeft || 0) : 0);
|
||||
const silenceTurnsLeft = isGenerallySilenced
|
||||
? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0)
|
||||
: (specificSilenceEffect?.turnsLeft || 0);
|
||||
|
||||
let isDisabledByDebuffOnTarget = false;
|
||||
const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId];
|
||||
if ((ability.id === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF) && opponentStateForDebuffCheck) {
|
||||
const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId]; // Состояние моего оппонента
|
||||
if (opponentStateForDebuffCheck && opponentStateForDebuffCheck.activeEffects &&
|
||||
(ability.id === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF)) {
|
||||
const effectIdForDebuff = 'effect_' + ability.id;
|
||||
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);
|
||||
}
|
||||
|
||||
// Обновление title (всплывающей подсказки)
|
||||
let titleText = `${ability.name} (${ability.cost} ${actingPlayerResourceName})`;
|
||||
let descriptionText = ability.description;
|
||||
if (typeof ability.descriptionFunction === 'function') {
|
||||
descriptionText = ability.descriptionFunction(configGlobal, gameDataGlobal.opponentBaseStats);
|
||||
descriptionText = ability.descriptionFunction(configGlobal, gameDataGlobal.opponentBaseStats); // Для описания используем статы оппонента этого клиента
|
||||
}
|
||||
if (descriptionText) titleText += ` - ${descriptionText}`;
|
||||
let abilityBaseCooldown = ability.cooldown;
|
||||
@ -279,45 +358,87 @@
|
||||
if (isOnCooldown) titleText = `${ability.name} - На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[ability.id]} х.`;
|
||||
else if (isSilenced) titleText = `Безмолвие! Осталось: ${silenceTurnsLeft} х.`;
|
||||
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} х.)` : ''}`;
|
||||
} 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} х.)` : ''}`;
|
||||
}
|
||||
button.setAttribute('title', titleText);
|
||||
});
|
||||
}
|
||||
|
||||
function showGameOver(playerWon, reason = "") {
|
||||
function showGameOver(playerWon, reason = "", opponentCharacterKeyFromClient = null) {
|
||||
const config = window.GAME_CONFIG || {};
|
||||
const gameDataGlobal = window.gameData || {};
|
||||
const currentGameState = window.gameState;
|
||||
// Используем gameData, сохраненное в client.js, так как оно отражает перспективу этого клиента
|
||||
const clientSpecificGameData = window.gameData;
|
||||
const currentActualGameState = window.gameState; // Самое актуальное состояние игры
|
||||
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 opponentPanelElement = uiElements.opponent.panel;
|
||||
const myName = gameDataGlobal.playerBaseStats?.name || "Игрок";
|
||||
const opponentName = gameDataGlobal.opponentBaseStats?.name || "Противник";
|
||||
const opponentCharacterKey = gameDataGlobal.opponentBaseStats?.characterKey;
|
||||
// Имена для сообщения берем из clientSpecificGameData, т.к. они уже "перевернуты" для каждого клиента
|
||||
const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок";
|
||||
const opponentNameForResult = clientSpecificGameData?.opponentBaseStats?.name || "Противник";
|
||||
|
||||
if (resultMsgElement) {
|
||||
let winText = `Победа! ${myName} празднует!`;
|
||||
let loseText = `Поражение! ${opponentName} оказался(лась) сильнее!`;
|
||||
if (reason === 'opponent_disconnected') winText = `${opponentName} покинул(а) игру. Победа присуждается вам!`;
|
||||
let winText = `Победа! ${myNameForResult} празднует!`;
|
||||
let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`;
|
||||
if (reason === 'opponent_disconnected') {
|
||||
winText = `${opponentNameForResult} покинул(а) игру. Победа присуждается вам!`;
|
||||
// Если оппонент отключился, а мы проиграли (технически такое возможно, если сервер так решит)
|
||||
// То текст поражения можно оставить стандартным или специфичным.
|
||||
// Пока оставим стандартный, если playerWon = false и reason = opponent_disconnected.
|
||||
}
|
||||
resultMsgElement.textContent = playerWon ? winText : loseText;
|
||||
resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)';
|
||||
}
|
||||
|
||||
const opponentPanelElement = uiElements.opponent.panel;
|
||||
if (opponentPanelElement) {
|
||||
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');
|
||||
} 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(() => {
|
||||
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');
|
||||
requestAnimationFrame(() => {
|
||||
gameOverScreenElement.style.opacity = '0';
|
||||
@ -329,6 +450,9 @@
|
||||
}
|
||||
}, 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);
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@
|
||||
--scrollbar-track: #10121c;
|
||||
--shake-duration: 0.4s;
|
||||
--cast-duration: 0.6s;
|
||||
--dissolve-duration: 6.0s;
|
||||
--dissolve-duration: 6.0s; /* Убедитесь, что это значение используется в JS для синхронизации, если нужно */
|
||||
--log-panel-fixed-height: 280px;
|
||||
}
|
||||
|
||||
@ -59,11 +59,11 @@ body {
|
||||
color: var(--text-light);
|
||||
line-height: 1.5;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
overflow: hidden; /* Предотвращает прокрутку основного body, если контент не помещается */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: center; /* Центрирует auth-game-setup-wrapper */
|
||||
padding: 10px;
|
||||
}
|
||||
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 {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin: 20px auto;
|
||||
margin: 20px auto; /* auto для центрирования, если flex в body изменится */
|
||||
padding: 25px 30px;
|
||||
background: var(--panel-bg);
|
||||
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);
|
||||
color: var(--text-light);
|
||||
text-align: center;
|
||||
/* Добавим overflow-y: auto для случаев, когда контент не помещается */
|
||||
max-height: calc(100vh - 40px); /* Чтобы не вылезал за экран */
|
||||
overflow-y: auto;
|
||||
}
|
||||
.auth-game-setup-wrapper h2,
|
||||
.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;
|
||||
}
|
||||
.auth-game-setup-wrapper h3 { font-size: 1.2em; margin-top: 1.5em; }
|
||||
|
||||
/* Общие стили для кнопок в .auth-game-setup-wrapper и форм аутентификации */
|
||||
.auth-game-setup-wrapper button,
|
||||
#auth-section form button {
|
||||
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;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Стили для инпутов */
|
||||
.auth-game-setup-wrapper input[type="text"],
|
||||
#auth-section input[type="text"],
|
||||
#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);
|
||||
margin: 5px 5px 10px 5px;
|
||||
font-size: 0.9em;
|
||||
width: calc(100% - 22px);
|
||||
width: calc(100% - 22px); /* Учитываем padding и border */
|
||||
max-width: 300px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Стили для списка доступных игр */
|
||||
#available-games-list {
|
||||
margin-top: 20px;
|
||||
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 li {
|
||||
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;
|
||||
justify-content: space-between;
|
||||
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 button { padding: 6px 10px; font-size: 0.8em; margin-left: 10px; }
|
||||
|
||||
/* Контейнер для статусных сообщений */
|
||||
#status-container { min-height: 2.5em; margin-bottom: 15px; }
|
||||
#game-status-message, #auth-message {
|
||||
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.error { color: var(--damage-color); }
|
||||
|
||||
/* Формы аутентификации */
|
||||
#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 { background: linear-gradient(145deg, #8c3a3a, #6b2b2b) !important; } /* !important чтобы перебить общий стиль кнопки */
|
||||
#logout-button:hover { background: linear-gradient(145deg, #a04040, #8c3a3a) !important; }
|
||||
|
||||
/* Стили для выбора персонажа (перенесены из index.html) */
|
||||
/* Стили для выбора персонажа */
|
||||
.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);
|
||||
border: 1px solid rgba(var(--panel-border), 0.5); /* Тоже может быть проблемой с var() в rgba() */
|
||||
/* Исправлено: */
|
||||
/* border: 1px solid #353a52; /* Пример полупрозрачного panel-border */
|
||||
}
|
||||
.character-selection h4 {
|
||||
font-size: 1.1em;
|
||||
@ -222,10 +245,12 @@ i { margin-right: 6px; color: var(--icon-color); width: 1.2em; text-align: cente
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
/* Стилизация для Елены */
|
||||
.character-selection input[type="radio"][value="elena"]:checked + label {
|
||||
background-color: var(--accent-player);
|
||||
box-shadow: 0 0 8px rgba(108, 149, 255, 0.5);
|
||||
}
|
||||
/* Стилизация для Альмагест */
|
||||
.character-selection input[type="radio"][value="almagest"]:checked + label {
|
||||
background-color: var(--accent-almagest); /* Новый цвет для Альмагест */
|
||||
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;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
overflow: hidden; /* Предотвращает выпадение контента */
|
||||
}
|
||||
.player-column, .opponent-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
min-width: 0; /* Для корректной работы flex с overflow */
|
||||
overflow: hidden; /* Если контент внутри колонок может быть больше */
|
||||
}
|
||||
|
||||
/* --- Стили Панелей Персонажей, Управления, Лога --- */
|
||||
@ -288,7 +313,7 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -300,40 +325,40 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
|
||||
gap: 10px;
|
||||
padding-bottom: 10px;
|
||||
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 .icon-player { color: var(--accent-player); } /* Елена */
|
||||
.fighter-name .icon-opponent { color: var(--accent-opponent); } /* Балард */
|
||||
.fighter-name .icon-almagest { color: var(--accent-almagest); } /* Альмагест */
|
||||
.fighter-name .icon-player { color: var(--accent-player); }
|
||||
.fighter-name .icon-opponent { color: var(--accent-opponent); }
|
||||
.fighter-name .icon-almagest { color: var(--accent-almagest); }
|
||||
|
||||
.character-visual { flex-shrink: 0; margin-bottom: 0; }
|
||||
.avatar-image {
|
||||
display: block;
|
||||
max-width: 50px;
|
||||
height: auto;
|
||||
max-width: 50px; /* Фиксируем или делаем адаптивным */
|
||||
height: auto; /* Для сохранения пропорций */
|
||||
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);
|
||||
}
|
||||
.panel-content {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding-right: 5px;
|
||||
overflow-y: auto; /* Позволяет прокручивать контент, если он не помещается */
|
||||
padding-right: 5px; /* Для отступа от скроллбара */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
padding-top: 10px;
|
||||
margin-top: 0;
|
||||
min-height: 0; /* Для корректной работы flex с overflow */
|
||||
padding-top: 10px; /* Отступ от panel-header */
|
||||
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 .bar-icon { flex-shrink: 0; font-size: 1.4em; }
|
||||
/* Цвета иконок ресурсов */
|
||||
.stat-bar-container.health .bar-icon { color: var(--hp-color); }
|
||||
.stat-bar-container.mana .bar-icon { color: var(--mana-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.mana .bar-icon { color: var(--mana-color); }
|
||||
.stat-bar-container.stamina .bar-icon { color: var(--stamina-color); }
|
||||
.stat-bar-container.dark-energy .bar-icon { color: var(--dark-energy-color); }
|
||||
|
||||
.bar-wrapper { flex-grow: 1; }
|
||||
.bar {
|
||||
@ -342,7 +367,7 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
|
||||
position: relative; background-color: var(--bar-bg);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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;
|
||||
font-size: 0.75em; font-weight: bold; color: #fff;
|
||||
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); }
|
||||
@ -362,30 +387,30 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
|
||||
/* Статус и Эффекты */
|
||||
.status-area {
|
||||
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 span { font-weight: bold; }
|
||||
.status-area span.blocking { color: var(--block-color); font-style: italic; }
|
||||
|
||||
.effects-area {
|
||||
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 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;
|
||||
}
|
||||
.effect-category .icon-effects-buff, .effect-category .icon-effects-debuff {
|
||||
font-size: 1em; flex-shrink: 0; margin-top: 0.1em;
|
||||
width: 1.2em; text-align: center;
|
||||
font-size: 1em; flex-shrink: 0; margin-top: 0.1em; /* Подгонка выравнивания */
|
||||
width: 1.2em; text-align: center; /* Для иконок */
|
||||
}
|
||||
.effect-category .icon-effects-buff { color: var(--heal-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 {
|
||||
display: inline-block; margin: 2px 3px 2px 0; padding: 1px 6px;
|
||||
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-debuff { border-color: var(--damage-color); color: var(--damage-color); }
|
||||
.effect-stun { border-color: var(--turn-color); color: var(--turn-color); }
|
||||
.effect-block { border-color: var(--block-color); color: var(--block-color); }
|
||||
.effect-stun { border-color: var(--turn-color); color: var(--turn-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);
|
||||
}
|
||||
.action-button.basic:active:enabled { transform: translateY(0px); box-shadow: 0 1px 2px rgba(0,0,0,0.4); }
|
||||
|
||||
/* Стиль для бафнутой атаки */
|
||||
#button-attack.attack-buffed:enabled {
|
||||
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);
|
||||
background: linear-gradient(145deg, #70c070, #5a9a5a);
|
||||
transform: translateY(-1px);
|
||||
background: linear-gradient(145deg, #70c070, #5a9a5a); /* Зеленый градиент */
|
||||
transform: translateY(-1px); /* Небольшой подъем */
|
||||
}
|
||||
|
||||
.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));
|
||||
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);
|
||||
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 {
|
||||
grid-column: 1 / -1; text-align: center; color: var(--text-muted);
|
||||
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 {
|
||||
font-size: 0.65em; font-weight: normal; color: #aaccce; opacity: 0.8;
|
||||
text-shadow: none; max-height: 2em; overflow: hidden; width: 95%;
|
||||
display: block; margin-top: auto;
|
||||
display: block; margin-top: auto; /* Прижимает описание вниз */
|
||||
}
|
||||
.ability-button:hover:enabled {
|
||||
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);
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
/* Общие стили для неактивных кнопок (базовых и способностей) */
|
||||
.ability-button:disabled, .action-button.basic:disabled {
|
||||
background: var(--button-disabled-bg) !important; border-color: transparent !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;
|
||||
opacity: 0.6; text-shadow: none !important; filter: grayscale(50%) !important;
|
||||
}
|
||||
/* Нехватка ресурса */
|
||||
.ability-button.not-enough-resource:not(:disabled) {
|
||||
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;
|
||||
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 {
|
||||
0%, 100% { border-color: var(--damage-color); }
|
||||
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-silenced .ability-name, .ability-button.is-silenced .ability-desc { opacity: 0.6; }
|
||||
.ability-button.is-on-cooldown .ability-desc, .ability-button.is-silenced .ability-desc { display: none; }
|
||||
.ability-button.is-silenced .ability-name, .ability-button.is-silenced .ability-desc {
|
||||
opacity: 0.6;
|
||||
}
|
||||
/* Скрываем описание для КД и безмолвия, чтобы освободить место для таймера/иконки */
|
||||
.ability-button.is-on-cooldown .ability-desc, .ability-button.is-silenced .ability-desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ability-cooldown-display { /* Также используется для отображения безмолвия */
|
||||
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;
|
||||
}
|
||||
.ability-button.is-on-cooldown .ability-cooldown-display,
|
||||
.ability-button.is-silenced .ability-cooldown-display { display: block !important; }
|
||||
|
||||
.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; */
|
||||
/* ... (стили для иконки замка, если используется) ... */
|
||||
}
|
||||
.ability-button.is-silenced .ability-cooldown-display { display: block !important; } /* !important для переопределения display:none */
|
||||
|
||||
|
||||
/* --- Панель Лога --- */
|
||||
@ -520,22 +551,23 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
|
||||
#log-list {
|
||||
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;
|
||||
color: var(--log-text); padding: 10px; min-height: 0;
|
||||
color: var(--log-text); padding: 10px; min-height: 0; /* Для корректной работы flex-grow и overflow */
|
||||
}
|
||||
#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;
|
||||
}
|
||||
#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-heal { color: var(--heal-color); font-weight: 500; }
|
||||
.log-block { color: var(--block-color); font-style: italic; }
|
||||
.log-info { color: #b0c4de; }
|
||||
.log-info { color: #b0c4de; } /* Светло-голубой для общей информации */
|
||||
.log-turn {
|
||||
font-weight: bold; color: var(--turn-color); margin-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-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);
|
||||
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-content {
|
||||
background: var(--modal-content-bg); padding: 40px 50px; border-radius: 10px;
|
||||
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-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;
|
||||
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);
|
||||
|
||||
/* Стили для кнопки "В меню выбора игры" - ОБЩИЙ КЛАСС */
|
||||
.modal-action-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;
|
||||
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;
|
||||
}
|
||||
#restart-game-button:hover:enabled {
|
||||
background: var(--button-hover-bg); transform: scale(1.05) translateY(-1px);
|
||||
.modal-action-button:hover:enabled {
|
||||
background: var(--button-hover-bg);
|
||||
transform: scale(1.05) translateY(-1px);
|
||||
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); }
|
||||
#restart-game-button:disabled { background: var(--button-disabled-bg); color: var(--button-disabled-text); cursor: not-allowed; opacity: 0.7; }
|
||||
#restart-game-button i { margin-right: 8px; }
|
||||
.modal-action-button:active:enabled {
|
||||
transform: scale(1) translateY(0);
|
||||
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 {
|
||||
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% {
|
||||
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)),
|
||||
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);
|
||||
transform: scale(1.005);
|
||||
transform: scale(1.005); /* Легкое увеличение */
|
||||
}
|
||||
}
|
||||
/* Применение анимации каста к панели игрока (добавляется через 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) - могут быть адаптированы или расширены */
|
||||
#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-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 {
|
||||
@ -604,13 +669,24 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
|
||||
}
|
||||
#opponent-panel.is-shaking {
|
||||
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 {
|
||||
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;
|
||||
pointer-events: none;
|
||||
pointer-events: none; /* Чтобы нельзя было взаимодействовать во время исчезновения */
|
||||
}
|
||||
/* Состояние после завершения анимации dissolving, если класс остается */
|
||||
#opponent-panel.dissolved-state { /* Этот класс можно добавлять по завершению анимации через JS, если нужно */
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(20px);
|
||||
}
|
||||
|
||||
|
||||
@keyframes shake-short {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-3px); }
|
||||
@ -622,20 +698,38 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } /* Новый ц
|
||||
|
||||
/* --- Отзывчивость (Медиа-запросы) --- */
|
||||
@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-header h1 { font-size: 1.5em; }
|
||||
.battle-arena-container { flex-direction: column; 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; }
|
||||
.battle-log-new { height: auto; min-height: 150px; }
|
||||
#log-list { max-height: 200px; }
|
||||
|
||||
.fighter-panel, .controls-panel-new, .battle-log-new {
|
||||
min-height: auto; /* Убираем min-height, пусть контент определяет */
|
||||
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::after { display: none; }
|
||||
.abilities-grid::after { display: none; } /* Убираем псевдоэлемент, т.к. нет скролла */
|
||||
.ability-list, .controls-layout { overflow: visible; }
|
||||
|
||||
.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; }
|
||||
.bar { height: 18px; }
|
||||
.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 .ability-name { margin-bottom: 2px; }
|
||||
.ability-button .ability-desc { font-size: 0.65em; }
|
||||
|
||||
.modal-content { padding: 25px 30px; width: 90%; max-width: 400px; }
|
||||
.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 h2 { font-size: 1.6em; } #game-setup h3 { font-size: 1.1em; }
|
||||
#game-setup button { padding: 8px 12px; font-size: 0.9em; }
|
||||
#game-setup input[type="text"] { width: calc(100% - 90px); max-width: 200px; padding: 8px;}
|
||||
#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) {
|
||||
@ -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; }
|
||||
.ability-button { font-size: 0.7em; padding: 4px; }
|
||||
.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; }
|
||||
|
||||
.modal-content { padding: 20px; }
|
||||
.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; }
|
||||
.bar { height: 16px; } .bar-text { font-size: 0.7em; }
|
||||
.effects-area, .effect { font-size: 0.8em; }
|
||||
|
||||
/* Стили для auth-game-setup на мобильных */
|
||||
.auth-game-setup-wrapper { padding: 15px; }
|
||||
#game-setup { padding: 10px; }
|
||||
#game-setup h2 { font-size: 1.4em; }
|
||||
#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 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;}
|
||||
/* Делаем кнопки и инпуты в game-setup блочными для лучшего отображения на мобильных */
|
||||
#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 li button { font-size: 0.75em; padding: 5px 8px;}
|
||||
|
||||
.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;}
|
||||
}
|
@ -7,22 +7,22 @@ class GameInstance {
|
||||
constructor(gameId, io, mode = 'ai') {
|
||||
this.id = gameId;
|
||||
this.io = io;
|
||||
this.mode = mode;
|
||||
this.mode = mode; // 'ai' или 'pvp'
|
||||
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.gameState = null;
|
||||
this.gameState = null; // Хранит текущее состояние игры (HP, ресурсы, эффекты, чей ход и т.д.)
|
||||
this.aiOpponent = (mode === 'ai');
|
||||
this.logBuffer = [];
|
||||
this.restartVotes = new Set();
|
||||
this.logBuffer = []; // Буфер для сообщений лога боя
|
||||
// this.restartVotes = new Set(); // Удалено, так как рестарт той же сессии убран
|
||||
|
||||
this.playerCharacterKey = null;
|
||||
this.opponentCharacterKey = null;
|
||||
this.ownerUserId = null; // userId создателя игры
|
||||
// Ключи персонажей для текущей игры
|
||||
this.playerCharacterKey = null; // Ключ персонажа в слоте 'player' (Елена или Альмагест)
|
||||
this.opponentCharacterKey = null; // Ключ персонажа в слоте 'opponent' (Балард, Елена или Альмагест)
|
||||
this.ownerUserId = null; // userId создателя игры (важно для PvP ожидающих игр)
|
||||
}
|
||||
|
||||
addPlayer(socket, chosenCharacterKey = 'elena') {
|
||||
// Проверка, не пытается ли игрок присоединиться к игре, в которой он уже есть
|
||||
if (this.players[socket.id]) {
|
||||
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре.' });
|
||||
console.warn(`[Game ${this.id}] Игрок ${socket.id} попытался присоединиться к игре, в которой уже состоит.`);
|
||||
@ -34,29 +34,30 @@ class GameInstance {
|
||||
return false;
|
||||
}
|
||||
|
||||
let assignedPlayerId;
|
||||
let actualCharacterKey;
|
||||
let assignedPlayerId; // 'player' или 'opponent' (технический ID слота)
|
||||
let actualCharacterKey; // 'elena', 'almagest', 'balard'
|
||||
|
||||
if (this.mode === 'ai') {
|
||||
if (this.playerCount > 0) { // В AI игру может войти только один реальный игрок
|
||||
if (this.playerCount > 0) {
|
||||
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
|
||||
return false;
|
||||
}
|
||||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||||
actualCharacterKey = 'elena';
|
||||
actualCharacterKey = 'elena'; // В AI режиме игрок всегда Елена
|
||||
if (socket.userData?.userId) {
|
||||
this.ownerUserId = socket.userData.userId;
|
||||
this.ownerUserId = socket.userData.userId; // Запоминаем создателя
|
||||
}
|
||||
} else { // PvP
|
||||
if (this.playerCount === 0) { // Первый игрок PvP
|
||||
} else { // PvP режим
|
||||
if (this.playerCount === 0) { // Первый игрок в PvP
|
||||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||||
actualCharacterKey = (chosenCharacterKey === 'almagest') ? 'almagest' : 'elena';
|
||||
if (socket.userData?.userId) {
|
||||
this.ownerUserId = socket.userData.userId;
|
||||
this.ownerUserId = socket.userData.userId; // Запоминаем создателя
|
||||
}
|
||||
} else { // Второй игрок PvP
|
||||
} else { // Второй игрок в PvP
|
||||
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';
|
||||
}
|
||||
}
|
||||
@ -64,15 +65,14 @@ class GameInstance {
|
||||
this.players[socket.id] = {
|
||||
id: assignedPlayerId,
|
||||
socket: socket,
|
||||
chosenCharacterKey: actualCharacterKey
|
||||
chosenCharacterKey: actualCharacterKey // Запоминаем ключ выбранного/назначенного персонажа
|
||||
};
|
||||
this.playerSockets[assignedPlayerId] = socket;
|
||||
|
||||
this.playerCount++;
|
||||
socket.join(this.id);
|
||||
socket.join(this.id); // Присоединяем сокет к комнате игры
|
||||
|
||||
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) {
|
||||
socket.emit('waitingForOpponent');
|
||||
@ -80,12 +80,12 @@ class GameInstance {
|
||||
|
||||
// Если игра готова к старту (2 игрока в PvP, или 1 в AI)
|
||||
if ((this.mode === 'ai' && this.playerCount === 1) || (this.mode === 'pvp' && this.playerCount === 2)) {
|
||||
this.initializeGame();
|
||||
this.initializeGame(); // Инициализируем состояние игры
|
||||
if (this.gameState) {
|
||||
this.startGame();
|
||||
this.startGame(); // Запускаем игру
|
||||
} else {
|
||||
// Ошибка инициализации уже должна была быть залогирована и отправлена клиенту
|
||||
console.error(`[Game ${this.id}] Не удалось запустить игру, так как gameState не был инициализирован.`);
|
||||
console.error(`[Game ${this.id}] Не удалось запустить игру: gameState не был инициализирован.`);
|
||||
// Ошибка должна была быть отправлена клиенту из initializeGame
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@ -94,20 +94,23 @@ class GameInstance {
|
||||
removePlayer(socketId) {
|
||||
const playerInfo = this.players[socketId];
|
||||
if (playerInfo) {
|
||||
const playerRole = playerInfo.id;
|
||||
let characterKeyToRemove = playerInfo.chosenCharacterKey;
|
||||
const playerRole = playerInfo.id; // 'player' or 'opponent'
|
||||
let characterKeyOfLeavingPlayer = playerInfo.chosenCharacterKey;
|
||||
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
|
||||
characterKeyToRemove = 'balard';
|
||||
} else if (!characterKeyToRemove && this.gameState) { // Фоллбэк, если ключ не был в playerInfo
|
||||
characterKeyToRemove = (playerRole === GAME_CONFIG.PLAYER_ID)
|
||||
// Для AI оппонента, у него нет записи в this.players, но его ключ 'balard'
|
||||
if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) { // Если уходит игрок из AI игры
|
||||
// AI оппонент не имеет 'chosenCharacterKey' в this.players, так как он не сокет
|
||||
} else if (!characterKeyOfLeavingPlayer && this.gameState) {
|
||||
// Фоллбэк, если ключ не был в playerInfo (маловероятно для реальных игроков)
|
||||
characterKeyOfLeavingPlayer = (playerRole === GAME_CONFIG.PLAYER_ID)
|
||||
? this.gameState.player?.characterKey
|
||||
: this.gameState.opponent?.characterKey;
|
||||
}
|
||||
|
||||
const characterData = this._getCharacterBaseData(characterKeyToRemove);
|
||||
console.log(`[Game ${this.id}] Игрок ${socketId} (userId: ${userIdOfLeavingPlayer || 'N/A'}) (${characterData?.name || 'Неизвестно'}, роль: ${playerRole}, персонаж: ${characterKeyToRemove || 'N/A'}) покинул игру.`);
|
||||
const characterData = this._getCharacterBaseData(characterKeyOfLeavingPlayer);
|
||||
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) {
|
||||
delete this.playerSockets[playerRole];
|
||||
@ -115,17 +118,19 @@ class GameInstance {
|
||||
delete this.players[socketId];
|
||||
this.playerCount--;
|
||||
|
||||
// Если создатель PvP игры вышел, и остался один игрок, обновляем ownerUserId
|
||||
if (this.mode === 'pvp' && this.ownerUserId === userIdOfLeavingPlayer && this.playerCount === 1) {
|
||||
const remainingPlayerSocketId = Object.keys(this.players)[0];
|
||||
const remainingPlayerSocket = this.players[remainingPlayerSocketId]?.socket;
|
||||
this.ownerUserId = remainingPlayerSocket?.userData?.userId || null;
|
||||
console.log(`[Game ${this.id}] Owner left. New potential owner for pending game: ${this.ownerUserId || remainingPlayerSocketId}`);
|
||||
this.ownerUserId = remainingPlayerSocket?.userData?.userId || null; // Новый владелец - userId оставшегося или null
|
||||
console.log(`[Game ${this.id}] Owner left PvP game. New potential owner for pending game: ${this.ownerUserId || remainingPlayerSocketId}`);
|
||||
} else if (this.playerCount === 0) {
|
||||
this.ownerUserId = null;
|
||||
this.ownerUserId = null; // Если игра пуста, нет владельца
|
||||
}
|
||||
|
||||
// Если игра была активна, завершаем ее из-за дисконнекта
|
||||
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;
|
||||
const winnerRole = disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
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', {
|
||||
winnerId: (this.mode === 'pvp' || winnerRole === GAME_CONFIG.OPPONENT_ID) ? winnerRole : GAME_CONFIG.OPPONENT_ID,
|
||||
winnerId: winnerRole,
|
||||
reason: 'opponent_disconnected',
|
||||
finalGameState: this.gameState,
|
||||
log: this.consumeLogBuffer()
|
||||
@ -152,52 +160,59 @@ class GameInstance {
|
||||
console.log(`[Game ${this.id}] Initializing game state for (re)start... Mode: ${this.mode}`);
|
||||
|
||||
if (this.mode === 'ai') {
|
||||
this.playerCharacterKey = 'elena';
|
||||
this.opponentCharacterKey = 'balard';
|
||||
this.playerCharacterKey = 'elena'; // Игрок в AI всегда Елена
|
||||
this.opponentCharacterKey = 'balard'; // AI всегда Балард
|
||||
} else { // pvp
|
||||
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);
|
||||
|
||||
this.playerCharacterKey = playerSocketInfo?.chosenCharacterKey || 'elena'; // Игрок 1 (слот 'player')
|
||||
if (this.playerCount === 2 && opponentSocketInfo) { // Если есть второй игрок (слот 'opponent')
|
||||
this.playerCharacterKey = playerSocketInfo?.chosenCharacterKey || 'elena'; // Фоллбэк, если что-то пошло не так
|
||||
|
||||
if (this.playerCount === 2 && opponentSocketInfo) {
|
||||
this.opponentCharacterKey = opponentSocketInfo.chosenCharacterKey;
|
||||
// Убедимся, что персонажи разные
|
||||
// Дополнительная проверка, чтобы персонажи были разными, если вдруг оба выбрали одного
|
||||
if (this.playerCharacterKey === this.opponentCharacterKey) {
|
||||
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 {
|
||||
}
|
||||
} else if (this.playerCount === 1) { // Только один игрок в PvP, оппонент еще не определен
|
||||
this.opponentCharacterKey = null;
|
||||
} else { // Неожиданная ситуация
|
||||
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: ${this.playerCharacterKey}, Opponent Slot: ${this.opponentCharacterKey || 'N/A'}`);
|
||||
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)'}`);
|
||||
|
||||
const playerBase = this._getCharacterBaseData(this.playerCharacterKey);
|
||||
const playerAbilities = this._getCharacterAbilities(this.playerCharacterKey);
|
||||
let opponentBase = null;
|
||||
let opponentAbilities = null;
|
||||
|
||||
// Загружаем данные оппонента, только если он определен (т.е. PvP игра с 2 игроками или AI игра)
|
||||
if (this.opponentCharacterKey) {
|
||||
opponentBase = this._getCharacterBaseData(this.opponentCharacterKey);
|
||||
opponentAbilities = this._getCharacterAbilities(this.opponentCharacterKey);
|
||||
}
|
||||
|
||||
// Проверяем, готовы ли мы к созданию полного игрового состояния
|
||||
const isReadyForFullGameState = (this.mode === 'ai') || (this.mode === 'pvp' && this.playerCount === 2 && opponentBase && opponentAbilities);
|
||||
|
||||
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}`);
|
||||
this.logBuffer = [];
|
||||
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.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;
|
||||
}
|
||||
|
||||
// Создаем gameState
|
||||
this.gameState = {
|
||||
player: {
|
||||
id: GAME_CONFIG.PLAYER_ID, characterKey: this.playerCharacterKey, name: playerBase.name,
|
||||
@ -206,53 +221,64 @@ class GameInstance {
|
||||
resourceName: playerBase.resourceName, attackPower: playerBase.attackPower,
|
||||
isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {}
|
||||
},
|
||||
opponent: {
|
||||
opponent: { // Данные оппонента, если он есть, иначе плейсхолдеры
|
||||
id: GAME_CONFIG.OPPONENT_ID, characterKey: this.opponentCharacterKey,
|
||||
name: opponentBase?.name || 'Ожидание...',
|
||||
name: opponentBase?.name || 'Ожидание игрока...',
|
||||
currentHp: opponentBase?.maxHp || 1, maxHp: opponentBase?.maxHp || 1,
|
||||
currentResource: opponentBase?.maxResource || 0, maxResource: opponentBase?.maxResource || 0,
|
||||
resourceName: opponentBase?.resourceName || 'Неизвестно', attackPower: opponentBase?.attackPower || 0,
|
||||
isBlocking: false, activeEffects: [],
|
||||
// Специальные кулдауны для Баларда (AI)
|
||||
silenceCooldownTurns: this.opponentCharacterKey === 'balard' ? 0 : undefined,
|
||||
manaDrainCooldownTurns: this.opponentCharacterKey === 'balard' ? 0 : undefined,
|
||||
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 => {
|
||||
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) {
|
||||
opponentAbilities.forEach(ability => {
|
||||
let cd = 0;
|
||||
if (ability.cooldown) cd = ability.cooldown;
|
||||
else if (this.opponentCharacterKey === 'balard') {
|
||||
if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) cd = GAME_CONFIG[ability.internalCooldownFromConfig];
|
||||
else if (typeof ability.internalCooldownValue === 'number') cd = ability.internalCooldownValue;
|
||||
else if (this.opponentCharacterKey === 'balard') { // Специальные внутренние КД для AI Баларда
|
||||
if (ability.internalCooldownFromConfig && GAME_CONFIG[ability.internalCooldownFromConfig]) {
|
||||
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 isFullGameReadyForLog = (this.mode === 'ai' && this.playerCount === 1) || (this.mode === 'pvp' && this.playerCount === 2 && this.opponentCharacterKey);
|
||||
const isRestart = this.logBuffer.length > 0 && isFullGameReadyForLog;
|
||||
this.logBuffer = [];
|
||||
if (isFullGameReadyForLog) {
|
||||
const isRestart = this.logBuffer.length > 0 && isReadyForFullGameState; // Проверяем, был ли лог до этого (признак рестарта)
|
||||
this.logBuffer = []; // Очищаем лог перед новой игрой/рестартом
|
||||
if (isReadyForFullGameState) { // Лог о начале битвы только если игра полностью готова
|
||||
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 || 'Оппонент')}`);
|
||||
}
|
||||
|
||||
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) {
|
||||
console.log(`[Game ${this.id}] startGame: Waiting for opponent in PvP game.`);
|
||||
console.log(`[Game ${this.id}] startGame: PvP игра ожидает второго игрока.`);
|
||||
} else if (!this.gameState) {
|
||||
console.error(`[Game ${this.id}] Game cannot start: gameState is null.`);
|
||||
} 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;
|
||||
}
|
||||
@ -263,84 +289,65 @@ class GameInstance {
|
||||
|
||||
if (!playerCharData || !opponentCharData) {
|
||||
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;
|
||||
}
|
||||
|
||||
Object.values(this.players).forEach(pInfo => {
|
||||
// Отправляем каждому игроку его персональные данные для игры
|
||||
Object.values(this.players).forEach(playerInfo => {
|
||||
let dataForThisClient;
|
||||
if (pInfo.id === GAME_CONFIG.PLAYER_ID) {
|
||||
if (playerInfo.id === GAME_CONFIG.PLAYER_ID) { // Этот клиент играет за слот 'player'
|
||||
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,
|
||||
playerAbilities: playerCharData.abilities, opponentAbilities: opponentCharData.abilities,
|
||||
log: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
|
||||
log: this.consumeLogBuffer(), // Первый игрок получает весь накопленный лог
|
||||
clientConfig: { ...GAME_CONFIG } // Копия конфига для клиента
|
||||
};
|
||||
} else {
|
||||
} else { // Этот клиент играет за слот 'opponent'
|
||||
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,
|
||||
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;
|
||||
this.addToLog(`--- ${firstTurnName} ходит первым! ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
||||
this.broadcastGameStateUpdate();
|
||||
this.addToLog(`--- ${firstTurnName} ходит первым! (Ход ${this.gameState.turnNumber}) ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
||||
this.broadcastGameStateUpdate(); // Отправляем начальное состояние и лог
|
||||
|
||||
// Если ход AI, запускаем его логику
|
||||
if (!this.gameState.isPlayerTurn) {
|
||||
if (this.aiOpponent && this.opponentCharacterKey === 'balard') {
|
||||
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
|
||||
} else {
|
||||
} else { // PvP, ход второго игрока
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
handleVoteRestart(requestingSocketId) {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
// Метод handleVoteRestart удален
|
||||
|
||||
processPlayerAction(requestingSocketId, actionData) {
|
||||
if (!this.gameState || this.gameState.isGameOver) return;
|
||||
const actingPlayerInfo = this.players[requestingSocketId];
|
||||
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) ||
|
||||
(!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 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);
|
||||
|
||||
if (!attackerData || !defenderData) {
|
||||
this.addToLog('Критическая ошибка сервера при обработке действия (данные персонажа)!', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
this.addToLog('Критическая ошибка сервера при обработке действия (не найдены данные персонажа)!', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||
this.broadcastLogUpdate(); return;
|
||||
}
|
||||
const attackerBaseStats = attackerData.baseStats;
|
||||
const defenderBaseStats = defenderData.baseStats;
|
||||
const attackerAbilities = attackerData.abilities;
|
||||
let actionValid = true;
|
||||
let actionValid = true; // Флаг валидности действия
|
||||
|
||||
// Обработка атаки
|
||||
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(
|
||||
attackerState, defenderState, attackerBaseStats, defenderBaseStats,
|
||||
attackerState, defenderState, attackerData.baseStats, defenderData.baseStats,
|
||||
this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData
|
||||
);
|
||||
|
||||
if (attackBuffEffect) {
|
||||
const actualRegen = Math.min(GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN, attackerBaseStats.maxResource - attackerState.currentResource);
|
||||
// Логика для "Силы Природы" и аналогов - бафф применяется после атаки
|
||||
const attackBuffAbilityId = attackerState.characterKey === 'elena' ? GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH
|
||||
: (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) {
|
||||
attackerState.currentResource += actualRegen;
|
||||
this.addToLog(`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от эффекта "${attackBuffEffect.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL);
|
||||
}
|
||||
// Эффект НЕ удаляется здесь для многоразового действия
|
||||
// Не удаляем эффект, если он многоразовый. Если одноразовый - удалить тут.
|
||||
// В текущей реализации Сила Природы имеет duration, поэтому управляется через processEffects.
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка способности
|
||||
} 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 (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 (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_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_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) { 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) {
|
||||
if (defenderState.activeEffects.some(e => e.id === 'effect_' + ability.id)) { this.addToLog(`Эффект "${ability.name}" уже наложен на ${defenderState.name}!`, GAME_CONFIG.LOG_TYPE_INFO); actionValid = false; }
|
||||
// Нельзя кастовать дебафф на цель, если он уже на ней (для определенных дебаффов)
|
||||
const isTargetedDebuff = ability.id === GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_DEBUFF;
|
||||
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) {
|
||||
attackerState.currentResource -= ability.cost;
|
||||
// Установка кулдауна
|
||||
let baseCooldown = 0;
|
||||
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;}
|
||||
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; }
|
||||
}
|
||||
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})`;
|
||||
if (attackerState.characterKey === 'elena') {
|
||||
const taunt = serverGameLogic.getElenaTaunt('playerActionCast', { abilityId: ability.id }, GAME_CONFIG, gameData, this.gameState);
|
||||
if (taunt && taunt !== "(Молчание)") logMessage += `: "${taunt}"`;
|
||||
serverGameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, gameData);
|
||||
}
|
||||
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;
|
||||
this.addToLog(logMessage, logType);
|
||||
} else { actionValid = false; } // Неизвестный тип действия
|
||||
|
||||
const targetForAbility = (ability.type === GAME_CONFIG.ACTION_TYPE_HEAL || ability.type === GAME_CONFIG.ACTION_TYPE_BUFF) ? attackerState : defenderState;
|
||||
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 (!actionValid) { this.broadcastLogUpdate(); return; }
|
||||
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; }
|
||||
setTimeout(() => { this.switchTurn(); }, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||||
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } // Проверяем конец игры после действия
|
||||
setTimeout(() => { this.switchTurn(); }, GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); // Переключаем ход с задержкой
|
||||
}
|
||||
|
||||
switchTurn() {
|
||||
if (!this.gameState || this.gameState.isGameOver) return;
|
||||
|
||||
const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||
const endingTurnActorState = this.gameState[endingTurnActorRole];
|
||||
const endingTurnCharacterData = this._getCharacterData(endingTurnActorState.characterKey);
|
||||
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.updateBlockingStatus(this.gameState.player);
|
||||
serverGameLogic.updateBlockingStatus(this.gameState.player); // Обновляем статус блока для обоих
|
||||
serverGameLogic.updateBlockingStatus(this.gameState.opponent);
|
||||
|
||||
// Уменьшение кулдаунов способностей
|
||||
if (endingTurnActorState.abilityCooldowns) {
|
||||
serverGameLogic.processPlayerAbilityCooldowns(endingTurnActorState.abilityCooldowns, endingTurnCharacterData.abilities, endingTurnActorState.name, this.addToLog.bind(this));
|
||||
}
|
||||
// Специальные КД для Баларда
|
||||
if (endingTurnActorState.characterKey === 'balard') {
|
||||
if (endingTurnActorState.silenceCooldownTurns !== undefined && endingTurnActorState.silenceCooldownTurns > 0) endingTurnActorState.silenceCooldownTurns--;
|
||||
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) {
|
||||
const playerCharAbilities = this._getCharacterAbilities(playerStateInGame.characterKey);
|
||||
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.gameState.isPlayerTurn) this.gameState.turnNumber++;
|
||||
if (this.checkGameOver()) { this.broadcastGameStateUpdate(); return; } // Проверяем конец игры после эффектов
|
||||
|
||||
this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn; // Меняем ход
|
||||
if (this.gameState.isPlayerTurn) this.gameState.turnNumber++; // Новый ход игрока - увеличиваем номер хода
|
||||
|
||||
const currentTurnActorState = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent;
|
||||
this.addToLog(`--- Начинается ход ${this.gameState.turnNumber} для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
||||
this.broadcastGameStateUpdate();
|
||||
|
||||
// Если ход AI, запускаем его логику
|
||||
if (!this.gameState.isPlayerTurn) {
|
||||
if (this.aiOpponent && this.opponentCharacterKey === 'balard') {
|
||||
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
|
||||
} else {
|
||||
} else { // PvP, ход второго игрока
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
processAiTurn() {
|
||||
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;
|
||||
}
|
||||
|
||||
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 attackerData = this._getCharacterData('balard');
|
||||
const defenderData = this._getCharacterData(defenderState.characterKey);
|
||||
if (!attackerData || !defenderData) { this.switchTurn(); return; }
|
||||
const defenderData = this._getCharacterData(defenderState.characterKey); // Обычно 'elena'
|
||||
|
||||
if (!attackerData || !defenderData) { this.addToLog("AI не может действовать: ошибка данных персонажа.", GAME_CONFIG.LOG_TYPE_SYSTEM); this.switchTurn(); return; }
|
||||
let actionValid = true;
|
||||
|
||||
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);
|
||||
} else if (aiDecision.actionType === 'ability' && aiDecision.ability) {
|
||||
const ability = aiDecision.ability;
|
||||
// Проверки валидности (ресурс, КД) для AI
|
||||
if (attackerState.currentResource < ability.cost ||
|
||||
(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_MANA_DRAIN && attackerState.manaDrainCooldownTurns > 0)
|
||||
) {
|
||||
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;
|
||||
// Установка кулдауна для AI
|
||||
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;}
|
||||
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; }
|
||||
if (baseCooldown > 0 && attackerState.abilityCooldowns) attackerState.abilityCooldowns[ability.id] = baseCooldown + 1;
|
||||
|
||||
this.addToLog(`${attackerState.name} применяет "${ability.name}"...`, GAME_CONFIG.LOG_TYPE_EFFECT);
|
||||
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);
|
||||
serverGameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, 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);
|
||||
else this.addToLog(`${attackerState.name} пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
|
||||
} else actionValid = false;
|
||||
else this.addToLog(`${attackerState.name} обдумывает свой следующий ход...`, GAME_CONFIG.LOG_TYPE_INFO);
|
||||
} 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; }
|
||||
this.switchTurn();
|
||||
this.switchTurn(); // Переключаем ход после действия AI
|
||||
}
|
||||
|
||||
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 opponentState = this.gameState.opponent;
|
||||
if (!playerState || !opponentState) return false;
|
||||
|
||||
if (!playerState || !opponentState || opponentState.name === 'Ожидание игрока...') {
|
||||
// Если одного из игроков нет (например, PvP игра ожидает второго), игра не может закончиться по HP
|
||||
return false;
|
||||
}
|
||||
|
||||
const playerDead = playerState.currentHp <= 0;
|
||||
const opponentDead = opponentState.currentHp <= 0;
|
||||
@ -548,31 +562,78 @@ class GameInstance {
|
||||
this.gameState.isGameOver = true;
|
||||
const winnerRole = opponentDead ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||
const loserRole = opponentDead ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||
|
||||
const winnerState = this.gameState[winnerRole];
|
||||
const loserState = this.gameState[loserRole];
|
||||
|
||||
const winnerName = winnerState?.name || (winnerRole === 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') {
|
||||
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 (loserState?.characterKey === 'balard') 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 false;
|
||||
}
|
||||
|
||||
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) { if (!message) return; this.logBuffer.push({ message, type, timestamp: Date.now() }); }
|
||||
consumeLogBuffer() { 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() { 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; }}
|
||||
_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; }}
|
||||
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) {
|
||||
if (!message) return;
|
||||
this.logBuffer.push({ message, type, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
consumeLogBuffer() {
|
||||
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;
|
@ -1,23 +1,18 @@
|
||||
// /server_modules/gameManager.js
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const GameInstance = require('./gameInstance');
|
||||
const gameData = require('./data');
|
||||
const gameData = require('./data'); // Нужен для getAvailablePvPGamesListForClient
|
||||
const GAME_CONFIG = require('./config'); // Нужен для GAME_CONFIG.PLAYER_ID и других констант
|
||||
|
||||
class GameManager {
|
||||
constructor(io) {
|
||||
this.io = io;
|
||||
this.games = {}; // { gameId: GameInstance }
|
||||
this.socketToGame = {}; // { socket.id: gameId }
|
||||
this.pendingPvPGames = []; // [gameId]
|
||||
this.userToPendingGame = {}; // { userId: gameId } или { socketId: gameId }
|
||||
this.pendingPvPGames = []; // [gameId] - ID игр, ожидающих второго игрока в PvP
|
||||
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) {
|
||||
const keyToUse = identifier || currentSocketId;
|
||||
const oldPendingGameId = this.userToPendingGame[keyToUse];
|
||||
@ -26,8 +21,6 @@ class GameManager {
|
||||
const gameToRemove = this.games[oldPendingGameId];
|
||||
if (gameToRemove && gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) {
|
||||
const playersInOldGame = Object.values(gameToRemove.players);
|
||||
// Убеждаемся, что единственный игрок в старой игре - это действительно тот, кто сейчас создает/присоединяется
|
||||
// Либо по ID сокета, либо по userId, если он был владельцем
|
||||
const isOwnerBySocket = playersInOldGame.length === 1 && playersInOldGame[0].socket.id === currentSocketId;
|
||||
const isOwnerByUserId = identifier && gameToRemove.ownerUserId === identifier;
|
||||
|
||||
@ -38,43 +31,41 @@ class GameManager {
|
||||
const pendingIndex = this.pendingPvPGames.indexOf(oldPendingGameId);
|
||||
if (pendingIndex > -1) this.pendingPvPGames.splice(pendingIndex, 1);
|
||||
|
||||
// Удаляем привязки для старого сокета, если он там был
|
||||
if (playersInOldGame.length === 1 && this.socketToGame[playersInOldGame[0].socket.id] === oldPendingGameId) {
|
||||
delete this.socketToGame[playersInOldGame[0].socket.id];
|
||||
}
|
||||
delete this.userToPendingGame[keyToUse]; // Удаляем по ключу, который использовали для поиска
|
||||
delete this.userToPendingGame[keyToUse];
|
||||
|
||||
this.broadcastAvailablePvPGames();
|
||||
}
|
||||
} else if (oldPendingGameId === excludeGameId) {
|
||||
// Это та же игра, ничего не делаем
|
||||
// Это та же игра, к которой игрок присоединяется, ничего не делаем
|
||||
} else {
|
||||
// Запись в userToPendingGame устарела или не соответствует условиям, чистим
|
||||
delete this.userToPendingGame[keyToUse];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', userId = null) {
|
||||
const identifier = userId || socket.id;
|
||||
this._removePreviousPendingGames(socket.id, identifier); // Удаляем старые перед созданием новой
|
||||
this._removePreviousPendingGames(socket.id, identifier);
|
||||
|
||||
const gameId = uuidv4();
|
||||
const game = new GameInstance(gameId, this.io, mode);
|
||||
if (userId) game.ownerUserId = userId; // Устанавливаем владельца игры
|
||||
if (userId) game.ownerUserId = userId;
|
||||
this.games[gameId] = game;
|
||||
|
||||
// В AI режиме игрок всегда Елена, в PvP - тот, кого выбрали
|
||||
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
|
||||
|
||||
if (game.addPlayer(socket, charKeyForInstance)) { // addPlayer теперь сам установит userId в game.ownerUserId если это первый игрок
|
||||
this.socketToGame[socket.id] = gameId; // Устанавливаем привязку после успешного добавления
|
||||
console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${socket.id} (userId: ${userId}, выбран: ${charKeyForInstance})`);
|
||||
if (game.addPlayer(socket, charKeyForInstance)) {
|
||||
this.socketToGame[socket.id] = gameId;
|
||||
console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${socket.userData?.username || socket.id} (userId: ${userId}, выбран: ${charKeyForInstance})`);
|
||||
|
||||
const assignedPlayerId = game.players[socket.id]?.id;
|
||||
if (!assignedPlayerId) {
|
||||
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 });
|
||||
|
||||
@ -84,10 +75,9 @@ class GameManager {
|
||||
this.broadcastAvailablePvPGames();
|
||||
}
|
||||
} else {
|
||||
delete this.games[gameId]; // game.addPlayer вернул false, чистим
|
||||
// socketToGame не должен был быть установлен, если addPlayer вернул false
|
||||
delete this.games[gameId];
|
||||
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.mode !== 'pvp') { 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);
|
||||
|
||||
// addPlayer в GameInstance сам определит персонажа для второго игрока на основе первого
|
||||
if (game.addPlayer(socket)) {
|
||||
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);
|
||||
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
|
||||
|
||||
// Очищаем запись о создателе из userToPendingGame, так как игра началась
|
||||
if (game.ownerUserId && this.userToPendingGame[game.ownerUserId] === gameId) {
|
||||
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);
|
||||
if (firstPlayerSocketId && this.userToPendingGame[firstPlayerSocketId] === gameId) {
|
||||
delete this.userToPendingGame[firstPlayerSocketId];
|
||||
@ -121,28 +110,32 @@ class GameManager {
|
||||
}
|
||||
this.broadcastAvailablePvPGames();
|
||||
} else {
|
||||
socket.emit('gameError', { message: 'Не удалось присоединиться к игре (внутренняя ошибка).' });
|
||||
// Сообщение об ошибке отправляется из game.addPlayer
|
||||
}
|
||||
}
|
||||
|
||||
findAndJoinRandomPvPGame(socket, chosenCharacterKey = 'elena', userId = null) {
|
||||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', userId = null) {
|
||||
const identifier = userId || socket.id;
|
||||
this._removePreviousPendingGames(socket.id, identifier); // Удаляем старые перед поиском/созданием
|
||||
this._removePreviousPendingGames(socket.id, identifier);
|
||||
|
||||
let gameIdToJoin = null;
|
||||
const preferredOpponentKey = chosenCharacterKey === 'elena' ? 'almagest' : 'elena';
|
||||
// Персонаж, которого мы бы хотели видеть у оппонента (зеркальный нашему выбору)
|
||||
const preferredOpponentKey = chosenCharacterKeyForCreation === 'elena' ? 'almagest' : 'elena';
|
||||
|
||||
// Сначала ищем игру, где первый игрок выбрал "зеркального" персонажа
|
||||
for (const id of this.pendingPvPGames) {
|
||||
const pendingGame = this.games[id];
|
||||
if (pendingGame && pendingGame.playerCount === 1 && pendingGame.mode === 'pvp') {
|
||||
const firstPlayerInfo = Object.values(pendingGame.players)[0];
|
||||
const isMyOwnGame = (userId && pendingGame.ownerUserId === userId) || (firstPlayerInfo.socket.id === socket.id);
|
||||
if (isMyOwnGame) continue;
|
||||
|
||||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) {
|
||||
gameIdToJoin = id; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Если не нашли с предпочтительным оппонентом, ищем любую свободную (не нашу)
|
||||
if (!gameIdToJoin && this.pendingPvPGames.length > 0) {
|
||||
for (const id of this.pendingPvPGames) {
|
||||
const pendingGame = this.games[id];
|
||||
@ -156,11 +149,16 @@ class GameManager {
|
||||
}
|
||||
|
||||
if (gameIdToJoin) {
|
||||
// Присоединяемся к найденной игре. GameInstance.addPlayer сам назначит нужного персонажа второму игроку.
|
||||
this.joinGame(socket, gameIdToJoin, userId);
|
||||
} else {
|
||||
this.createGame(socket, 'pvp', chosenCharacterKey, userId);
|
||||
// Если свободных игр нет, создаем новую с выбранным персонажем
|
||||
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, userId);
|
||||
// Клиент получит 'gameCreated', а 'noPendingGamesFound' используется для информационного сообщения
|
||||
socket.emit('noPendingGamesFound', {
|
||||
message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.',
|
||||
gameId: this.userToPendingGame[identifier], // ID только что созданной игры
|
||||
yourPlayerId: GAME_CONFIG.PLAYER_ID // При создании всегда PLAYER_ID
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -172,17 +170,7 @@ class GameManager {
|
||||
game.processPlayerAction(socketId, actionData);
|
||||
} else {
|
||||
const playerSocket = this.io.sockets.sockets.get(socketId);
|
||||
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: 'Не удалось перезапустить: сессия не найдена.' });
|
||||
if (playerSocket) playerSocket.emit('gameError', { message: 'Ошибка: игровая сессия потеряна для этого действия.' });
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,7 +180,9 @@ class GameManager {
|
||||
|
||||
if (gameId && 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);
|
||||
|
||||
if (game.playerCount === 0) {
|
||||
@ -200,7 +190,6 @@ class GameManager {
|
||||
delete this.games[gameId];
|
||||
const gameIndexPending = this.pendingPvPGames.indexOf(gameId);
|
||||
if (gameIndexPending > -1) this.pendingPvPGames.splice(gameIndexPending, 1);
|
||||
// Удаляем из userToPendingGame, если игра была там по любому ключу
|
||||
for (const key in this.userToPendingGame) {
|
||||
if (this.userToPendingGame[key] === gameId) delete this.userToPendingGame[key];
|
||||
}
|
||||
@ -209,26 +198,32 @@ class GameManager {
|
||||
if (!this.pendingPvPGames.includes(gameId)) {
|
||||
this.pendingPvPGames.push(gameId);
|
||||
}
|
||||
// Обновляем ownerUserId и userToPendingGame для оставшегося игрока
|
||||
const remainingPlayerSocketId = Object.keys(game.players)[0];
|
||||
const remainingPlayerSocket = game.players[remainingPlayerSocketId]?.socket;
|
||||
const remainingUserId = remainingPlayerSocket?.userData?.userId;
|
||||
const newIdentifier = remainingUserId || remainingPlayerSocketId;
|
||||
|
||||
game.ownerUserId = remainingUserId; // Устанавливаем нового владельца (может быть null)
|
||||
this.userToPendingGame[newIdentifier] = gameId; // Связываем нового владельца
|
||||
game.ownerUserId = remainingUserId;
|
||||
this.userToPendingGame[newIdentifier] = gameId;
|
||||
|
||||
// Удаляем старую привязку отключившегося, если она была и не совпадает с новой
|
||||
if (identifier !== newIdentifier && this.userToPendingGame[identifier] === gameId) {
|
||||
delete this.userToPendingGame[identifier];
|
||||
}
|
||||
console.log(`[GameManager] Игра ${gameId} возвращена в список ожидания PvP. Новый владелец: ${newIdentifier}`);
|
||||
this.broadcastAvailablePvPGames();
|
||||
}
|
||||
} else { // Если игрок не был в активной игре, но мог иметь ожидающую
|
||||
this._removePreviousPendingGames(socketId, identifier);
|
||||
} else {
|
||||
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() {
|
||||
@ -236,16 +231,50 @@ class GameManager {
|
||||
.map(gameId => {
|
||||
const game = this.games[gameId];
|
||||
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) {
|
||||
const firstPlayerSocketId = Object.keys(game.players)[0];
|
||||
const firstPlayerInfo = game.players[firstPlayerSocketId];
|
||||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey) {
|
||||
const charData = gameData[firstPlayerInfo.chosenCharacterKey + 'BaseStats'];
|
||||
if (charData) firstPlayerName = charData.name;
|
||||
|
||||
if (firstPlayerInfo) {
|
||||
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;
|
||||
})
|
||||
@ -256,17 +285,24 @@ class GameManager {
|
||||
this.io.emit('availablePvPGamesList', this.getAvailablePvPGamesListForClient());
|
||||
}
|
||||
|
||||
getActiveGamesList() {
|
||||
getActiveGamesList() { // Для отладки на сервере
|
||||
return Object.values(this.games).map(game => {
|
||||
let playerSlotChar = game.gameState?.player?.name || (game.playerCharacterKey ? gameData[game.playerCharacterKey + 'BaseStats']?.name : 'N/A');
|
||||
let opponentSlotChar = game.gameState?.opponent?.name || (game.opponentCharacterKey ? gameData[game.opponentCharacterKey + 'BaseStats']?.name : 'N/A');
|
||||
if (game.mode === 'pvp' && game.playerCount === 1 && !game.opponentCharacterKey) opponentSlotChar = 'Ожидание...';
|
||||
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 === 'elena' ? 'playerBaseStats' : (game.opponentCharacterKey === 'almagest' ? 'almagestBaseStats' : (game.opponentCharacterKey === 'balard' ? 'opponentBaseStats' : null))]?.name : 'N/A');
|
||||
|
||||
if (game.mode === 'pvp' && game.playerCount === 1 && !game.opponentCharacterKey && game.gameState && !game.gameState.isGameOver) {
|
||||
opponentSlotChar = 'Ожидание...';
|
||||
}
|
||||
|
||||
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',
|
||||
playerSlot: playerSlotChar, opponentSlot: opponentSlotChar,
|
||||
ownerUserId: game.ownerUserId || 'N/A'
|
||||
playerSlot: playerSlotChar,
|
||||
opponentSlot: opponentSlotChar,
|
||||
ownerUserId: game.ownerUserId || 'N/A',
|
||||
pending: this.pendingPvPGames.includes(game.id)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user