Исправление Гипнотического взгляда.
This commit is contained in:
parent
0e7ef20332
commit
35fe54e345
151
bc.js
151
bc.js
@ -1,7 +1,7 @@
|
|||||||
// bc.js - Главный файл сервера Battle Club
|
// bc.js - Главный файл сервера Battle Club
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const http = require('http'); // Используем HTTP, так как SSL будет на Node.js прокси (server.js)
|
const http = require('http');
|
||||||
const { Server } = require('socket.io');
|
const { Server } = require('socket.io');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
@ -16,13 +16,10 @@ const app = express();
|
|||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
// Настройка Socket.IO
|
// Настройка Socket.IO
|
||||||
|
// cors options могут потребоваться, если клиент и сервер работают на разных портах/доменах
|
||||||
const io = new Server(server, {
|
const io = new Server(server, {
|
||||||
cors: {
|
cors: {
|
||||||
origin: "https://pavel-chagovsky.com:3200", // Указываем точный origin, включая порт, откуда придет запрос К ПРОКСИ
|
origin: "*", // Разрешить подключение с любого домена (для разработки). В продакшене лучше указать конкретный домен клиента.
|
||||||
// Если доступ будет с нескольких доменов или портов, можно использовать массив:
|
|
||||||
// origin: ["https://pavel-chagovsky.com:3200", "https://oleg-okhotnikov.ru:3200"],
|
|
||||||
// Или для разработки можно временно использовать "*", но это менее безопасно:
|
|
||||||
// origin: "*",
|
|
||||||
methods: ["GET", "POST"]
|
methods: ["GET", "POST"]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -34,132 +31,198 @@ app.use(express.static(path.join(__dirname, 'public')));
|
|||||||
const gameManager = new GameManager(io);
|
const gameManager = new GameManager(io);
|
||||||
|
|
||||||
// Хранилище информации о залогиненных пользователях по socket.id
|
// Хранилище информации о залогиненных пользователях по socket.id
|
||||||
|
// В более сложном приложении здесь может быть Redis или другое внешнее хранилище сессий
|
||||||
const loggedInUsers = {}; // { socket.id: { userId: ..., username: ... } }
|
const loggedInUsers = {}; // { socket.id: { userId: ..., username: ... } }
|
||||||
|
|
||||||
// Обработка подключений Socket.IO
|
// Обработка подключений Socket.IO
|
||||||
io.on('connection', (socket) => {
|
io.on('connection', (socket) => {
|
||||||
console.log(`[BC App HTTP] Socket.IO User connected: ${socket.id}`);
|
console.log(`[Socket.IO] Пользователь подключился: ${socket.id}`);
|
||||||
|
|
||||||
socket.userData = null;
|
// Привязываем user data к сокету (пока пустые)
|
||||||
|
socket.userData = null; // { userId: ..., username: ... }
|
||||||
|
|
||||||
|
// При подключении клиента, если он уже залогинен (например, по cookie/token, что здесь не реализовано,
|
||||||
|
// но может быть добавлено), нужно восстановить его user data и проверить, не в игре ли он.
|
||||||
|
// В текущей простой реализации, мы полагаемся на то, что клиент после коннекта сам отправит логин,
|
||||||
|
// если он был залогинен. Но если бы была проверка сессии, логика была бы тут.
|
||||||
|
// Добавляем вызов handleRequestGameState при коннекте, если есть user data (для примера,
|
||||||
|
// но для полной реализации нужны cookies/токены)
|
||||||
|
// if (socket.userData?.userId) { // Эта проверка сработает только после успешного логина в текущей сессии
|
||||||
|
// gameManager.handleRequestGameState(socket, socket.userData.userId); // Передаем объект socket
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// --- Обработчики событий Аутентификации ---
|
||||||
socket.on('register', async (data) => {
|
socket.on('register', async (data) => {
|
||||||
console.log(`[BC App HTTP Socket.IO] Register attempt for username: "${data?.username}" from ${socket.id}`);
|
console.log(`[Socket.IO] Register attempt for username: "${data?.username}" from ${socket.id}`);
|
||||||
const result = await auth.registerUser(data?.username, data?.password);
|
const result = await auth.registerUser(data?.username, data?.password);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(`[BC App HTTP Socket.IO] Registration successful for ${result.username} (${result.userId})`);
|
console.log(`[Socket.IO] Registration successful for ${result.username} (${result.userId})`);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[BC App HTTP Socket.IO] Registration failed for "${data?.username}": ${result.message}`);
|
console.warn(`[Socket.IO] Registration failed for "${data?.username}": ${result.message}`);
|
||||||
}
|
}
|
||||||
socket.emit('registerResponse', result);
|
socket.emit('registerResponse', result);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('login', async (data) => {
|
socket.on('login', async (data) => {
|
||||||
console.log(`[BC App HTTP Socket.IO] Login attempt for username: "${data?.username}" from ${socket.id}`);
|
console.log(`[Socket.IO] Login attempt for username: "${data?.username}" from ${socket.id}`);
|
||||||
const result = await auth.loginUser(data?.username, data?.password);
|
const result = await auth.loginUser(data?.username, data?.password);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(`[BC App HTTP Socket.IO] Login successful for ${result.username} (${result.userId}). Assigning to socket ${socket.id}.`);
|
console.log(`[Socket.IO] Login successful for ${result.username} (${result.userId}). Assigning to socket ${socket.id}.`);
|
||||||
|
// Сохраняем информацию о пользователе в сессии сокета
|
||||||
socket.userData = { userId: result.userId, username: result.username };
|
socket.userData = { userId: result.userId, username: result.username };
|
||||||
loggedInUsers[socket.id] = socket.userData;
|
loggedInUsers[socket.id] = socket.userData;
|
||||||
|
|
||||||
|
// Проверяем, есть ли у пользователя активная игра при логине (если он был отключен)
|
||||||
|
// ИСПРАВЛЕНИЕ: Передаем объект socket
|
||||||
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[BC App HTTP Socket.IO] Login failed for "${data?.username}": ${result.message}`);
|
console.warn(`[Socket.IO] Login failed for "${data?.username}": ${result.message}`);
|
||||||
socket.userData = null;
|
socket.userData = null; // Убеждаемся, что данные пользователя на сокете сброшены
|
||||||
if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id];
|
if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id];
|
||||||
}
|
}
|
||||||
socket.emit('loginResponse', result);
|
socket.emit('loginResponse', result);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('logout', () => {
|
socket.on('logout', () => {
|
||||||
console.log(`[BC App HTTP Socket.IO] Logout for user ${socket.userData?.username || socket.id}`);
|
console.log(`[Socket.IO] Logout for user ${socket.userData?.username || socket.id}`);
|
||||||
|
// Уведомляем gameManager о дисконнекте (для корректного выхода из игры, если в ней был)
|
||||||
|
// Game Manager сам очистит ссылку socketToGame[socket.id] при handleDisconnect
|
||||||
|
// ИСПРАВЛЕНИЕ: Передаем userId или socket.id в handleDisconnect
|
||||||
gameManager.handleDisconnect(socket.id, socket.userData?.userId || socket.id);
|
gameManager.handleDisconnect(socket.id, socket.userData?.userId || socket.id);
|
||||||
|
|
||||||
|
// Очищаем информацию о пользователе на сокете и в хранилище
|
||||||
socket.userData = null;
|
socket.userData = null;
|
||||||
if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id];
|
if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id];
|
||||||
|
|
||||||
|
// Клиент должен сам переключиться на экран аутентификации
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Обработчики событий Управления Играми ---
|
||||||
|
|
||||||
socket.on('createGame', (data) => {
|
socket.on('createGame', (data) => {
|
||||||
const identifier = socket.userData?.userId || socket.id;
|
// Пользователь, даже не залогиненный, может создать AI игру (идентифицируется по socket.id)
|
||||||
|
// Для PvP игры нужна аутентификация (идентификация по userId)
|
||||||
|
const identifier = socket.userData?.userId || socket.id; // Используем userId для залогиненных, socket.id для гостей
|
||||||
const mode = data?.mode || 'ai';
|
const mode = data?.mode || 'ai';
|
||||||
|
|
||||||
if (mode === 'pvp' && !socket.userData) {
|
if (mode === 'pvp' && !socket.userData) {
|
||||||
socket.emit('gameError', { message: 'Необходимо войти в систему для создания PvP игры.' });
|
socket.emit('gameError', { message: 'Необходимо войти в систему для создания PvP игры.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[BC App HTTP Socket.IO] Create Game request from ${socket.userData?.username || socket.id} (Identifier: ${identifier}). Mode: ${mode}, Character: ${data?.characterKey}`);
|
|
||||||
const characterKey = data?.characterKey || 'elena';
|
console.log(`[Socket.IO] Create Game request from ${socket.userData?.username || socket.id} (Identifier: ${identifier}). Mode: ${mode}, Character: ${data?.characterKey}`);
|
||||||
gameManager.createGame(socket, mode, characterKey, identifier);
|
|
||||||
|
const characterKey = data?.characterKey || 'elena'; // По умолчанию Елена
|
||||||
|
gameManager.createGame(socket, mode, characterKey, identifier); // Передаем идентификатор
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('joinGame', (data) => {
|
socket.on('joinGame', (data) => {
|
||||||
if (!socket.userData) {
|
if (!socket.userData) { // Проверяем, залогинен ли пользователь
|
||||||
socket.emit('gameError', { message: 'Необходимо войти в систему для присоединения к игре.' });
|
socket.emit('gameError', { message: 'Необходимо войти в систему для присоединения к игре.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[BC App HTTP Socket.IO] Join Game request from ${socket.userData.username} (${socket.id}). Game ID: ${data?.gameId}`);
|
console.log(`[Socket.IO] Join Game request from ${socket.userData.username} (${socket.id}). Game ID: ${data?.gameId}`);
|
||||||
const gameId = data?.gameId;
|
const gameId = data?.gameId;
|
||||||
const identifier = socket.userData.userId;
|
const identifier = socket.userData.userId; // Присоединиться может только залогиненный
|
||||||
|
|
||||||
if (gameId) {
|
if (gameId) {
|
||||||
gameManager.joinGame(socket, gameId, identifier);
|
gameManager.joinGame(socket, gameId, identifier); // Передаем идентификатор
|
||||||
} else {
|
} else {
|
||||||
socket.emit('gameError', { message: 'Не указан ID игры для присоединения.' });
|
socket.emit('gameError', { message: 'Не указан ID игры для присоединения.' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('findRandomGame', (data) => {
|
socket.on('findRandomGame', (data) => {
|
||||||
if (!socket.userData) {
|
if (!socket.userData) { // Проверяем, залогинен ли пользователь
|
||||||
socket.emit('gameError', { message: 'Необходимо войти в систему для поиска игры.' });
|
socket.emit('gameError', { message: 'Необходимо войти в систему для поиска игры.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[BC App HTTP Socket.IO] Find Random Game request from ${socket.userData.username} (${socket.id}). Preferred Character: ${data?.characterKey}`);
|
console.log(`[Socket.IO] Find Random Game request from ${socket.userData.username} (${socket.id}). Preferred Character: ${data?.characterKey}`);
|
||||||
const characterKey = data?.characterKey || 'elena';
|
const characterKey = data?.characterKey || 'elena'; // Предпочитаемый персонаж для создания, если не найдено
|
||||||
const identifier = socket.userData.userId;
|
const identifier = socket.userData.userId; // Ищет и создает только залогиненный
|
||||||
gameManager.findAndJoinRandomPvPGame(socket, characterKey, identifier);
|
|
||||||
|
gameManager.findAndJoinRandomPvPGame(socket, characterKey, identifier); // Передаем идентификатор
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('requestPvPGameList', () => {
|
socket.on('requestPvPGameList', () => {
|
||||||
console.log(`[BC App HTTP Socket.IO] Request PvP Game List from ${socket.userData?.username || socket.id}`);
|
// Список игр доступен всем, даже не залогиненным, но присоединиться можно только залогиненным
|
||||||
|
// if (!socket.userData) {
|
||||||
|
// socket.emit('gameError', { message: 'Необходимо войти в систему для просмотра игр.' });
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
console.log(`[Socket.IO] Request PvP Game List from ${socket.userData?.username || socket.id}`);
|
||||||
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
||||||
socket.emit('availablePvPGamesList', availableGames);
|
socket.emit('availablePvPGamesList', availableGames);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Обработчик для клиента, запрашивающего состояние игры (например, при переподключении)
|
||||||
socket.on('requestGameState', () => {
|
socket.on('requestGameState', () => {
|
||||||
|
// Запрашивать состояние игры может только залогиненный пользователь, т.к. только у них есть userId для идентификации
|
||||||
if (!socket.userData) {
|
if (!socket.userData) {
|
||||||
console.log(`[BC App HTTP Socket.IO] Request Game State from unauthenticated socket ${socket.id}.`);
|
console.log(`[Socket.IO] Request Game State from unauthenticated socket ${socket.id}.`);
|
||||||
socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' });
|
socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[BC App HTTP Socket.IO] Request Game State from ${socket.userData.username} (${socket.id}).`);
|
console.log(`[Socket.IO] Request Game State from ${socket.userData.username} (${socket.id}).`);
|
||||||
|
// ИСПРАВЛЕНИЕ: Передаем объект socket и identifier (userId)
|
||||||
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- Обработчик события Игрового Действия ---
|
||||||
socket.on('playerAction', (actionData) => {
|
socket.on('playerAction', (actionData) => {
|
||||||
|
// Действие в игре может совершить как залогиненный (PvP), так и не залогиненный (AI) игрок.
|
||||||
|
// Используем userId для залогиненных, socket.id для гостей.
|
||||||
const identifier = socket.userData?.userId || socket.id;
|
const identifier = socket.userData?.userId || socket.id;
|
||||||
gameManager.handlePlayerAction(identifier, actionData);
|
|
||||||
|
// Game Manager сам проверит, находится ли идентификатор в игре и его ли сейчас ход
|
||||||
|
// ИСПРАВЛЕНИЕ: Передаем идентификатор вместо socket.id
|
||||||
|
gameManager.handlePlayerAction(identifier, actionData); // Передаем идентификатор
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- Обработчик отключения сокета ---
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
const identifier = socket.userData?.userId || socket.id;
|
const identifier = socket.userData?.userId || socket.id; // Используем userId для залогиненных, socket.id для гостей
|
||||||
console.log(`[BC App HTTP Socket.IO] User disconnected: ${socket.id} (Причина: ${reason}). Identifier: ${identifier}`);
|
console.log(`[Socket.IO] Пользователь отключился: ${socket.id} (Причина: ${reason}). Identifier: ${identifier}`);
|
||||||
gameManager.handleDisconnect(socket.id, identifier);
|
|
||||||
|
// Уведомляем gameManager о дисконнекте, чтобы он обновил состояние игры.
|
||||||
|
// Передаем идентификатор пользователя.
|
||||||
|
gameManager.handleDisconnect(socket.id, identifier); // Передаем как socketId, так и identifier
|
||||||
|
|
||||||
|
// Удаляем пользователя из списка залогиненных, если был там
|
||||||
if (loggedInUsers[socket.id]) {
|
if (loggedInUsers[socket.id]) {
|
||||||
delete loggedInUsers[socket.id];
|
delete loggedInUsers[socket.id];
|
||||||
}
|
}
|
||||||
|
// Если сокет не был залогинен, его identifier был socket.id.
|
||||||
|
// Связь userIdentifierToGameId будет очищена в gameManager.handleDisconnect, если игра пуста.
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Опционально: отправка списка активных игр на сервере для отладки (по запросу с консоли или админки)
|
||||||
|
// global.getActiveGames = () => gameManager.getActiveGamesList();
|
||||||
|
// console.log("Type getActiveGames() in server console to list games.");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Запуск HTTP сервера
|
// Запуск HTTP сервера
|
||||||
const PORT = process.env.BC_INTERNAL_PORT || 3200; // Внутренний порт для bc.js
|
const PORT = process.env.PORT || 3200; // Использовать порт из переменных окружения или 3000 по умолчанию
|
||||||
const HOSTNAME = '127.0.0.1'; // Слушать ТОЛЬКО на localhost
|
server.listen(PORT, () => {
|
||||||
|
console.log(`Server running on port ${PORT}`);
|
||||||
server.listen(PORT, HOSTNAME, () => { // Явно указываем HOSTNAME
|
|
||||||
console.log(`Battle Club HTTP Application Server running at http://${HOSTNAME}:${PORT}`);
|
|
||||||
console.log(`This server should only be accessed locally by the reverse proxy.`);
|
|
||||||
console.log(`Serving static files from: ${path.join(__dirname, 'public')}`);
|
console.log(`Serving static files from: ${path.join(__dirname, 'public')}`);
|
||||||
|
// console.log("Database connection pool created/checked (from db.js require)."); // db.js уже логирует
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обработка необработанных промис-ошибок
|
// Обработка необработанных промис-ошибок
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
console.error('[BC App HTTP UNHANDLED REJECTION] Unhandled Rejection at:', promise, 'reason:', reason);
|
console.error('[UNHANDLED REJECTION] Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
|
// Логировать ошибку, возможно, завершить процесс в продакшене
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', (err) => {
|
||||||
console.error('[BC App HTTP UNCAUGHT EXCEPTION] Caught exception:', err);
|
console.error('[UNCAUGHT EXCEPTION] Caught exception:', err);
|
||||||
|
// Логировать ошибку, выполнить очистку ресурсов, и завершить процесс
|
||||||
|
// В продакшене здесь может быть более сложная логика, например, graceful shutdown
|
||||||
|
// process.exit(1); // Аварийное завершение процесса - можно раскомментировать в продакшене
|
||||||
});
|
});
|
663
public/js/ui.js
663
public/js/ui.js
@ -14,6 +14,7 @@
|
|||||||
status: document.getElementById('player-status'),
|
status: document.getElementById('player-status'),
|
||||||
effectsContainer: document.getElementById('player-effects'),
|
effectsContainer: document.getElementById('player-effects'),
|
||||||
buffsList: document.getElementById('player-effects')?.querySelector('.player-buffs'),
|
buffsList: document.getElementById('player-effects')?.querySelector('.player-buffs'),
|
||||||
|
// ИСПРАВЛЕНО: Селектор для списка дебаффов игрока
|
||||||
debuffsList: document.getElementById('player-effects')?.querySelector('.player-debuffs')
|
debuffsList: document.getElementById('player-effects')?.querySelector('.player-debuffs')
|
||||||
},
|
},
|
||||||
opponent: { // Панель для персонажа-противника ЭТОГО клиента
|
opponent: { // Панель для персонажа-противника ЭТОГО клиента
|
||||||
@ -24,7 +25,9 @@
|
|||||||
resourceFill: document.getElementById('opponent-resource-fill'), resourceText: document.getElementById('opponent-resource-text'),
|
resourceFill: document.getElementById('opponent-resource-fill'), resourceText: document.getElementById('opponent-resource-text'),
|
||||||
status: document.getElementById('opponent-status'),
|
status: document.getElementById('opponent-status'),
|
||||||
effectsContainer: document.getElementById('opponent-effects'),
|
effectsContainer: document.getElementById('opponent-effects'),
|
||||||
|
// ИСПРАВЛЕНО: Селектор для списка баффов оппонента
|
||||||
buffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-buffs'),
|
buffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-buffs'),
|
||||||
|
// ИСПРАВЛЕНО: Селектор для списка дебаффов оппонента
|
||||||
debuffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-debuffs')
|
debuffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-debuffs')
|
||||||
},
|
},
|
||||||
controls: {
|
controls: {
|
||||||
@ -39,8 +42,7 @@
|
|||||||
gameOver: {
|
gameOver: {
|
||||||
screen: document.getElementById('game-over-screen'),
|
screen: document.getElementById('game-over-screen'),
|
||||||
message: document.getElementById('result-message'),
|
message: document.getElementById('result-message'),
|
||||||
// restartButton: document.getElementById('restart-game-button'), // Старый ID, заменен
|
returnToMenuButton: document.getElementById('return-to-menu-button'),
|
||||||
returnToMenuButton: document.getElementById('return-to-menu-button'), // Новый ID
|
|
||||||
modalContent: document.getElementById('game-over-screen')?.querySelector('.modal-content')
|
modalContent: document.getElementById('game-over-screen')?.querySelector('.modal-content')
|
||||||
},
|
},
|
||||||
gameHeaderTitle: document.querySelector('.game-header h1'),
|
gameHeaderTitle: document.querySelector('.game-header h1'),
|
||||||
@ -55,7 +57,7 @@
|
|||||||
if (!logListElement) return;
|
if (!logListElement) return;
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.textContent = message;
|
li.textContent = message;
|
||||||
const config = window.GAME_CONFIG || {}; // Получаем конфиг из глобальной области
|
const config = window.GAME_CONFIG || {};
|
||||||
// Формируем класс для лога на основе типа (используем константы из конфига или фоллбэк)
|
// Формируем класс для лога на основе типа (используем константы из конфига или фоллбэк)
|
||||||
const logTypeClass = config[`LOG_TYPE_${type.toUpperCase()}`] ? `log-${config[`LOG_TYPE_${type.toUpperCase()}`]}` : `log-${type}`;
|
const logTypeClass = config[`LOG_TYPE_${type.toUpperCase()}`] ? `log-${config[`LOG_TYPE_${type.toUpperCase()}`]}` : `log-${type}`;
|
||||||
li.className = logTypeClass;
|
li.className = logTypeClass;
|
||||||
@ -70,63 +72,69 @@
|
|||||||
|
|
||||||
// Базовая проверка наличия необходимых элементов и данных
|
// Базовая проверка наличия необходимых элементов и данных
|
||||||
if (!elements || !elements.hpFill || !elements.hpText || !elements.resourceFill || !elements.resourceText || !elements.status || !fighterState || !fighterBaseStats) {
|
if (!elements || !elements.hpFill || !elements.hpText || !elements.resourceFill || !elements.resourceText || !elements.status || !fighterState || !fighterBaseStats) {
|
||||||
// console.warn(`updateFighterPanelUI: Отсутствуют элементы UI, состояние бойца или базовые статы для панели ${panelRole}.`);
|
|
||||||
// Если панель должна быть видима, но нет данных, можно ее скрыть или показать плейсхолдер
|
// Если панель должна быть видима, но нет данных, можно ее скрыть или показать плейсхолдер
|
||||||
if (elements && elements.panel && elements.panel.style.display !== 'none') {
|
if (elements && elements.panel && elements.panel.style.display !== 'none') {
|
||||||
// console.warn(`updateFighterPanelUI: Нет данных для видимой панели ${panelRole}.`);
|
// console.warn(`updateFighterPanelUI: Нет данных для видимой панели ${panelRole}.`);
|
||||||
// elements.panel.style.opacity = '0.5'; // Пример: сделать полупрозрачной, если нет данных
|
// elements.panel.style.opacity = '0.5'; // Пример: сделать полупрозрачной, если нет данных
|
||||||
}
|
}
|
||||||
|
// ВАЖНО: Очистить содержимое панели, если данных нет.
|
||||||
|
if (elements) {
|
||||||
|
if(elements.name) elements.name.innerHTML = (panelRole === 'player') ? '<i class="fas fa-question icon-player"></i> Ожидание данных...' : '<i class="fas fa-question icon-opponent"></i> Ожидание игрока...';
|
||||||
|
if(elements.hpText) elements.hpText.textContent = 'N/A';
|
||||||
|
if(elements.resourceText) elements.resourceText.textContent = 'N/A';
|
||||||
|
if(elements.status) elements.status.textContent = 'Неизвестно';
|
||||||
|
if(elements.buffsList) elements.buffsList.innerHTML = 'Нет';
|
||||||
|
if(elements.debuffsList) elements.debuffsList.innerHTML = 'Нет';
|
||||||
|
if(elements.avatar) elements.avatar.src = 'images/default_avatar.png';
|
||||||
|
if(panelRole === 'player' && uiElements.playerResourceTypeIcon) uiElements.playerResourceTypeIcon.className = 'fas fa-question';
|
||||||
|
if(panelRole === 'opponent' && uiElements.opponentResourceTypeIcon) uiElements.opponentResourceTypeIcon.className = 'fas fa-question';
|
||||||
|
if(panelRole === 'player' && uiElements.playerResourceBarContainer) uiElements.playerResourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
|
||||||
|
if(panelRole === 'opponent' && uiElements.opponentResourceBarContainer) uiElements.opponentResourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
|
||||||
|
if(elements.panel) elements.panel.style.opacity = '0.5'; // Затемняем
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Если панель была полупрозрачной (из-за отсутствия данных), а теперь данные есть, делаем ее полностью видимой
|
if (elements.panel) elements.panel.style.opacity = '1'; // Делаем видимой, если данные есть
|
||||||
// if (elements.panel && elements.panel.style.opacity !== '1' && fighterState && fighterBaseStats) {
|
|
||||||
// elements.panel.style.opacity = '1';
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// Обновление имени и иконки персонажа
|
// Обновление имени и иконки персонажа
|
||||||
if (elements.name) {
|
if (elements.name) {
|
||||||
let iconClass = 'fa-question'; // Иконка по умолчанию
|
let iconClass = 'fa-question'; // Иконка по умолчанию
|
||||||
// let accentColor = 'var(--text-muted)'; // Цвет по умолчанию - теперь берется из CSS через классы иконок
|
|
||||||
const characterKey = fighterBaseStats.characterKey;
|
const characterKey = fighterBaseStats.characterKey;
|
||||||
|
|
||||||
// Определяем класс иконки в зависимости от персонажа
|
// Определяем класс иконки в зависимости от персонажа
|
||||||
if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-player'; } // icon-player имеет цвет через CSS
|
if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-player'; }
|
||||||
else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; } // icon-almagest имеет цвет через CSS
|
else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; }
|
||||||
else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-opponent'; } // icon-opponent имеет цвет через CSS
|
else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-opponent'; }
|
||||||
else { /* console.warn(`updateFighterPanelUI: Неизвестный characterKey "${characterKey}" для иконки имени.`); */ }
|
else { /* console.warn(`updateFighterPanelUI: Неизвестный characterKey "${characterKey}" для иконки имени.`); */ }
|
||||||
|
|
||||||
// Обновляем innerHTML имени, включая иконку и текст. Добавляем "(Вы)" для управляемого персонажа.
|
|
||||||
let nameHtml = `<i class="fas ${iconClass}"></i> ${fighterBaseStats.name || 'Неизвестно'}`;
|
let nameHtml = `<i class="fas ${iconClass}"></i> ${fighterBaseStats.name || 'Неизвестно'}`;
|
||||||
if (isControlledByThisClient) nameHtml += " (Вы)";
|
if (isControlledByThisClient) nameHtml += " (Вы)";
|
||||||
elements.name.innerHTML = nameHtml;
|
elements.name.innerHTML = nameHtml;
|
||||||
// Цвет имени теперь задается CSS через классы icon-player/opponent/almagest, примененные к самой иконке
|
|
||||||
// elements.name.style.color = accentColor; // Эту строку можно удалить, если цвет задан через CSS
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление аватара
|
// Обновление аватара
|
||||||
if (elements.avatar && fighterBaseStats.avatarPath) {
|
if (elements.avatar && fighterBaseStats.avatarPath) {
|
||||||
elements.avatar.src = fighterBaseStats.avatarPath;
|
elements.avatar.src = fighterBaseStats.avatarPath;
|
||||||
// Обновляем рамку аватара в зависимости от персонажа
|
// Обновляем рамку аватара в зависимости от персонажа
|
||||||
elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard'); // Убираем старые классы
|
elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard');
|
||||||
elements.avatar.classList.add(`avatar-${fighterBaseStats.characterKey}`); // Добавляем класс для текущего персонажа
|
elements.avatar.classList.add(`avatar-${fighterBaseStats.characterKey}`);
|
||||||
} else if (elements.avatar) {
|
} else if (elements.avatar) {
|
||||||
elements.avatar.src = 'images/default_avatar.png'; // Запасной аватар
|
elements.avatar.src = 'images/default_avatar.png';
|
||||||
elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard'); // Убираем старые классы
|
elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Обновление полос здоровья и ресурса
|
// Обновление полос здоровья и ресурса
|
||||||
const maxHp = Math.max(1, fighterBaseStats.maxHp); // Избегаем деления на ноль
|
const maxHp = Math.max(1, fighterBaseStats.maxHp);
|
||||||
const maxRes = Math.max(1, fighterBaseStats.maxResource);
|
const maxRes = Math.max(1, fighterBaseStats.maxResource);
|
||||||
const currentHp = Math.max(0, fighterState.currentHp);
|
const currentHp = Math.max(0, fighterState.currentHp);
|
||||||
const currentRes = Math.max(0, fighterState.currentResource);
|
const currentRes = Math.max(0, fighterState.currentResource);
|
||||||
|
|
||||||
elements.hpFill.style.width = `${(currentHp / maxHp) * 100}%`;
|
elements.hpFill.style.width = `${(currentHp / maxHp) * 100}%`;
|
||||||
elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`;
|
elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`; // Здоровье округляем
|
||||||
// ИСПРАВЛЕНО: Убрано округление для отображения текущего ресурса
|
|
||||||
elements.resourceFill.style.width = `${(currentRes / maxRes) * 100}%`;
|
elements.resourceFill.style.width = `${(currentRes / maxRes) * 100}%`;
|
||||||
elements.resourceText.textContent = `${currentRes} / ${fighterBaseStats.maxResource}`; // <-- ИСПРАВЛЕНО
|
elements.resourceText.textContent = `${currentRes} / ${fighterBaseStats.maxResource}`; // Ресурс не округляем
|
||||||
|
|
||||||
|
|
||||||
// Обновление типа ресурса и иконки (mana/stamina/dark-energy)
|
// Обновление типа ресурса и иконки (mana/stamina/dark-energy)
|
||||||
@ -134,124 +142,158 @@
|
|||||||
const resourceIconElementToUpdate = (panelRole === 'player') ? uiElements.playerResourceTypeIcon : uiElements.opponentResourceTypeIcon;
|
const resourceIconElementToUpdate = (panelRole === 'player') ? uiElements.playerResourceTypeIcon : uiElements.opponentResourceTypeIcon;
|
||||||
|
|
||||||
if (resourceBarContainerToUpdate && resourceIconElementToUpdate) {
|
if (resourceBarContainerToUpdate && resourceIconElementToUpdate) {
|
||||||
resourceBarContainerToUpdate.classList.remove('mana', 'stamina', 'dark-energy'); // Сначала удаляем все классы ресурсов
|
resourceBarContainerToUpdate.classList.remove('mana', 'stamina', 'dark-energy');
|
||||||
let resourceClass = 'mana'; let iconClass = 'fa-flask'; // Значения по умолчанию (для Маны)
|
let resourceClass = 'mana'; let iconClass = 'fa-flask';
|
||||||
if (fighterBaseStats.resourceName === 'Ярость') { resourceClass = 'stamina'; iconClass = 'fa-fire-alt'; }
|
if (fighterBaseStats.resourceName === 'Ярость') { resourceClass = 'stamina'; iconClass = 'fa-fire-alt'; }
|
||||||
else if (fighterBaseStats.resourceName === 'Темная Энергия') { resourceClass = 'dark-energy'; iconClass = 'fa-skull'; } // Или другую иконку для темной энергии
|
else if (fighterBaseStats.resourceName === 'Темная Энергия') { resourceClass = 'dark-energy'; iconClass = 'fa-skull'; } // или fa-wand-magic-sparkles, fa-star-half-alt и т.д.
|
||||||
|
else { console.warn(`updateFighterPanelUI: Unknown resource name "${fighterBaseStats.resourceName}" for icon/color.`); iconClass = 'fa-question-circle'; }
|
||||||
resourceBarContainerToUpdate.classList.add(resourceClass);
|
resourceBarContainerToUpdate.classList.add(resourceClass);
|
||||||
resourceIconElementToUpdate.className = `fas ${iconClass}`; // Обновляем класс иконки
|
resourceIconElementToUpdate.className = `fas ${iconClass}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление статуса (Готов/Защищается)
|
// Обновление статуса (Готов/Защищается)
|
||||||
const statusText = fighterState.isBlocking ? (config.STATUS_BLOCKING || 'Защищается') : (config.STATUS_READY || 'Готов(а)');
|
const statusText = fighterState.isBlocking ? (config.STATUS_BLOCKING || 'Защищается') : (config.STATUS_READY || 'Готов(а)');
|
||||||
elements.status.textContent = statusText;
|
elements.status.textContent = statusText;
|
||||||
elements.status.classList.toggle(config.CSS_CLASS_BLOCKING || 'blocking', fighterState.isBlocking); // Применяем класс для стилизации статуса "Защищается"
|
elements.status.classList.toggle(config.CSS_CLASS_BLOCKING || 'blocking', fighterState.isBlocking);
|
||||||
|
|
||||||
|
|
||||||
// Обновление подсветки и рамки панели (в зависимости от персонажа)
|
// Обновление подсветки и рамки панели (в зависимости от персонажа)
|
||||||
if (elements.panel) {
|
if (elements.panel) {
|
||||||
let borderColorVar = 'var(--panel-border)'; // Цвет по умолчанию
|
let borderColorVar = 'var(--panel-border)';
|
||||||
// Снимаем все старые классы для рамки
|
|
||||||
elements.panel.classList.remove('panel-elena', 'panel-almagest', 'panel-balard');
|
elements.panel.classList.remove('panel-elena', 'panel-almagest', 'panel-balard');
|
||||||
|
|
||||||
// Применяем класс для рамки в зависимости от персонажа
|
if (fighterBaseStats.characterKey === 'elena') { elements.panel.classList.add('panel-elena'); borderColorVar = 'var(--accent-player)'; }
|
||||||
if (fighterBaseStats.characterKey === 'elena') { elements.panel.classList.add('panel-elena'); borderColorVar = 'var(--accent-player)'; } // Цвет рамки через CSS переменную
|
else if (fighterBaseStats.characterKey === 'almagest') { elements.panel.classList.add('panel-almagest'); borderColorVar = 'var(--accent-almagest)'; }
|
||||||
else if (fighterBaseStats.characterKey === 'almagest') { elements.panel.classList.add('panel-almagest'); borderColorVar = 'var(--accent-almagest)'; } // Цвет рамки через CSS переменную
|
else if (fighterBaseStats.characterKey === 'balard') { elements.panel.classList.add('panel-balard'); borderColorVar = 'var(--accent-opponent)'; }
|
||||||
else if (fighterBaseStats.characterKey === 'balard') { elements.panel.classList.add('panel-balard'); borderColorVar = 'var(--accent-opponent)'; } // Цвет рамки через CSS переменную
|
else { console.warn(`updateFighterPanelUI: Unknown character key "${fighterBaseStats.characterKey}" for panel border color.`); }
|
||||||
|
|
||||||
|
|
||||||
// Обновляем тень (свечение). Цвет свечения тоже может быть переменной.
|
|
||||||
let glowColorVar = 'rgba(0, 0, 0, 0.4)'; // Базовая тень
|
let glowColorVar = 'rgba(0, 0, 0, 0.4)'; // Базовая тень
|
||||||
if (fighterBaseStats.characterKey === 'elena') glowColorVar = 'var(--panel-glow-player)';
|
if (fighterBaseStats.characterKey === 'elena') glowColorVar = 'var(--panel-glow-player)';
|
||||||
else if (fighterBaseStats.characterKey === 'almagest' || fighterBaseStats.characterKey === 'balard') glowColorVar = 'var(--panel-glow-opponent)'; // Используем одну тень для всех оппонентов (Балард/Альмагест)
|
// В твоем CSS --panel-glow-opponent используется для обоих Баларда и Альмагест
|
||||||
|
else if (fighterBaseStats.characterKey === 'almagest' || fighterBaseStats.characterKey === 'balard') glowColorVar = 'var(--panel-glow-opponent)';
|
||||||
|
|
||||||
// Устанавливаем рамку и тень
|
|
||||||
elements.panel.style.borderColor = borderColorVar;
|
elements.panel.style.borderColor = borderColorVar;
|
||||||
// Используем переменную для свечения. Базовая тень inset оставлена как есть.
|
|
||||||
elements.panel.style.boxShadow = `0 0 15px ${glowColorVar}, inset 0 0 10px rgba(0, 0, 0, 0.3)`;
|
elements.panel.style.boxShadow = `0 0 15px ${glowColorVar}, inset 0 0 10px rgba(0, 0, 0, 0.3)`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерирует HTML для списка эффектов.
|
||||||
|
* @param {Array<object>} effectsArray - Массив объектов эффектов, УЖЕ отфильтрованных и отсортированных.
|
||||||
|
* @returns {string} HTML-строка для отображения списка эффектов.
|
||||||
|
*/
|
||||||
function generateEffectsHTML(effectsArray) {
|
function generateEffectsHTML(effectsArray) {
|
||||||
const config = window.GAME_CONFIG || {};
|
const config = window.GAME_CONFIG || {};
|
||||||
if (!effectsArray || effectsArray.length === 0) return 'Нет';
|
if (!effectsArray || effectsArray.length === 0) return 'Нет';
|
||||||
|
|
||||||
// Сортируем эффекты: сначала положительные (buff, block, heal), затем отрицательные (debuff, disable)
|
// ВАЖНО: Сортировка теперь выполняется ВНЕ этой функции (в updateEffectsUI)
|
||||||
// иконка для стана/безмолвия, иконка для ослабления, иконка для усиления
|
|
||||||
const sortedEffects = [...effectsArray].sort((a, b) => {
|
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.isFullSilence || eff.id.startsWith('playerSilencedOn_') || eff.type === config.ACTION_TYPE_DISABLE) {
|
||||||
|
// Эффекты полного безмолвия, заглушения абилок или типа DISABLE
|
||||||
|
effectClasses += ' effect-stun'; // Класс для стана/безмолвия (красный/желтый)
|
||||||
|
} else if (eff.grantsBlock) { // Эффекты, дающие блок
|
||||||
|
effectClasses += ' effect-block'; // Класс для эффектов блока (синий)
|
||||||
|
} else if (eff.type === config.ACTION_TYPE_DEBUFF) { // Явные дебаффы (например, сжигание ресурса)
|
||||||
|
effectClasses += ' effect-debuff'; // Класс для ослаблений (красноватый)
|
||||||
|
} else if (eff.type === config.ACTION_TYPE_BUFF) { // Явные баффы (например, усиление атаки)
|
||||||
|
effectClasses += ' effect-buff'; // Класс для усилений (зеленый)
|
||||||
|
} else if (eff.type === config.ACTION_TYPE_HEAL) { // Эффекты лечения (HoT)
|
||||||
|
effectClasses += ' effect-buff'; // HoT стилизуем как бафф (зеленый)
|
||||||
|
}
|
||||||
|
// Если есть другие типы (DoT, Drain и т.п.), которые не входят в эти категории,
|
||||||
|
// их нужно добавить или стилизовать как info.
|
||||||
|
// DoT можно стилизовать как effect-debuff or effect-damage, Drain as effect-debuff.
|
||||||
|
// Например: else if (eff.type === config.ACTION_TYPE_DAMAGE) { effectClasses += ' effect-debuff'; } // DoT как дебафф
|
||||||
|
// else if (eff.type === config.ACTION_TYPE_DRAIN) { effectClasses += ' effect-debuff'; } // Drain как дебафф
|
||||||
|
else {
|
||||||
|
//console.warn(`generateEffectsHTML: Эффект ID "${eff.id}" с типом "${eff.type}" не имеет специфичного класса стилизации.`);
|
||||||
|
effectClasses += ' effect-info'; // Класс по умолчанию или информационный (серый/синий)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<span class="${effectClasses}" title="${title}">${displayText}</span>`;
|
||||||
|
}).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEffectsUI(currentGameState) {
|
||||||
|
if (!currentGameState || !window.GAME_CONFIG) { return; }
|
||||||
|
const mySlotId = window.myPlayerId;
|
||||||
|
const config = window.GAME_CONFIG;
|
||||||
|
if (!mySlotId) { return; }
|
||||||
|
|
||||||
|
const opponentSlotId = mySlotId === config.PLAYER_ID ? config.OPPONENT_ID : config.PLAYER_ID;
|
||||||
|
|
||||||
|
const myState = currentGameState[mySlotId];
|
||||||
|
const opponentState = currentGameState[opponentSlotId];
|
||||||
|
|
||||||
|
// --- Логика сортировки эффектов (для использования как для баффов, так и для дебаффов) ---
|
||||||
|
// Сортируем эффекты по типу: сначала позитивные, потом негативные, потом контроля
|
||||||
const typeOrder = {
|
const typeOrder = {
|
||||||
[config.ACTION_TYPE_BUFF]: 1,
|
[config.ACTION_TYPE_BUFF]: 1,
|
||||||
grantsBlock: 2,
|
grantsBlock: 2,
|
||||||
[config.ACTION_TYPE_HEAL]: 3, // HoT эффекты
|
[config.ACTION_TYPE_HEAL]: 3, // HoT эффекты
|
||||||
[config.ACTION_TYPE_DEBUFF]: 4, // DoT, ресурсные дебаффы
|
[config.ACTION_TYPE_DEBUFF]: 4, // DoT, ресурсные дебаффы
|
||||||
[config.ACTION_TYPE_DISABLE]: 5 // Silence, Stun
|
[config.ACTION_TYPE_DISABLE]: 5 // Silence, Stun
|
||||||
|
// Добавьте другие типы, если нужно сортировать
|
||||||
};
|
};
|
||||||
|
const sortEffects = (a, b) => {
|
||||||
// Определяем порядок для эффекта A
|
// Определяем порядок для эффекта A
|
||||||
let orderA = typeOrder[a.type];
|
let orderA = typeOrder[a.type] || 99;
|
||||||
if (a.grantsBlock) orderA = typeOrder.grantsBlock;
|
if (a.grantsBlock) orderA = typeOrder.grantsBlock;
|
||||||
|
// isFullSilence и playerSilencedOn_X - это эффекты типа DISABLE, но их можно поставить выше в приоритете дебаффов
|
||||||
if (a.isFullSilence || a.id.startsWith('playerSilencedOn_')) orderA = typeOrder[config.ACTION_TYPE_DISABLE];
|
if (a.isFullSilence || a.id.startsWith('playerSilencedOn_')) orderA = typeOrder[config.ACTION_TYPE_DISABLE];
|
||||||
|
// Добавьте сюда другие специфичные проверки, если нужно изменить стандартный порядок по типу
|
||||||
|
|
||||||
// Определяем порядок для эффекта B
|
// Определяем порядок для эффекта B
|
||||||
let orderB = typeOrder[b.type];
|
let orderB = typeOrder[b.type] || 99;
|
||||||
if (b.grantsBlock) orderB = typeOrder.grantsBlock;
|
if (b.grantsBlock) orderB = typeOrder.grantsBlock;
|
||||||
if (b.isFullSilence || b.id.startsWith('playerSilencedOn_')) orderB = typeOrder[config.ACTION_TYPE_DISABLE];
|
if (b.isFullSilence || b.id.startsWith('playerSilencedOn_')) orderB = typeOrder[config.ACTION_TYPE_DISABLE];
|
||||||
|
|
||||||
return (orderA || 99) - (orderB || 99); // Сортируем по порядку, неизвестные типы в конец
|
return (orderA || 99) - (orderB || 99); // Сортируем по порядку
|
||||||
|
};
|
||||||
|
// --- Конец логики сортировки ---
|
||||||
|
|
||||||
|
|
||||||
|
// --- Обработка эффектов Игрока (My Player) ---
|
||||||
|
if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList && myState && myState.activeEffects) {
|
||||||
|
const myBuffs = [];
|
||||||
|
const myDebuffs = [];
|
||||||
|
|
||||||
|
// ИСПРАВЛЕНО: Проходим по массиву activeEffects один раз и пушим в нужный список
|
||||||
|
myState.activeEffects.forEach(e => {
|
||||||
|
// Определяем, является ли эффект баффом
|
||||||
|
const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL; // HoT как бафф
|
||||||
|
|
||||||
|
// Определяем, является ли эффект дебаффом
|
||||||
|
// Учитываем типы DEBUFF, DISABLE, а также специфические флаги/ID для полного безмолвия и заглушения конкретных абилок
|
||||||
|
const isDebuff = e.type === config.ACTION_TYPE_DEBUFF || e.type === config.ACTION_TYPE_DISABLE || e.isFullSilence || e.id.startsWith('playerSilencedOn_');
|
||||||
|
|
||||||
|
// Добавляем эффект в соответствующий список (каждый эффект должен попасть только в один)
|
||||||
|
if (isBuff) {
|
||||||
|
myBuffs.push(e);
|
||||||
|
} else if (isDebuff) {
|
||||||
|
myDebuffs.push(e);
|
||||||
|
} else {
|
||||||
|
// Если эффект не попал ни в одну категорию (например, новый тип?)
|
||||||
|
//console.warn(`updateEffectsUI: Эффект ID "${e.id}" с типом "${e.type}" не отнесен ни к баффам, ни к дебаффам для Игрока.`);
|
||||||
|
myDebuffs.push(e); // Добавим в дебаффы по умолчанию
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Сортируем списки баффов и дебаффов перед генерацией HTML
|
||||||
return sortedEffects.map(eff => {
|
myBuffs.sort(sortEffects);
|
||||||
let effectClasses = config.CSS_CLASS_EFFECT || 'effect'; // Базовый класс для всех эффектов
|
myDebuffs.sort(sortEffects);
|
||||||
// Формируем заголовок тултипа
|
|
||||||
const title = `${eff.name}${eff.description ? ` - ${eff.description}` : ''} (Осталось: ${eff.turnsLeft} х.)`;
|
|
||||||
// Текст, отображаемый на самой плашке эффекта
|
|
||||||
const displayText = `${eff.name} (${eff.turnsLeft} х.)`;
|
|
||||||
|
|
||||||
// Добавляем специфичные классы для стилизации по типу эффекта
|
|
||||||
if (eff.isFullSilence || eff.id.startsWith('playerSilencedOn_') || (eff.type === config.ACTION_TYPE_DISABLE)) { // Эффекты полного безмолвия или специфичного заглушения
|
|
||||||
effectClasses += ' effect-stun'; // Класс для стана/безмолвия
|
|
||||||
} else if (eff.grantsBlock) { // Эффекты, дающие блок
|
|
||||||
effectClasses += ' effect-block'; // Класс для эффектов блока
|
|
||||||
} else if (eff.type === config.ACTION_TYPE_DEBUFF) { // Явные дебаффы (например, сжигание ресурса, DoT)
|
|
||||||
effectClasses += ' effect-debuff'; // Класс для ослаблений
|
|
||||||
} else if (eff.type === config.ACTION_TYPE_BUFF || eff.type === config.ACTION_TYPE_HEAL) { // Явные баффы или эффекты HoT
|
|
||||||
effectClasses += ' effect-buff'; // Класс для усилений
|
|
||||||
} else {
|
|
||||||
// console.warn(`generateEffectsHTML: Неизвестный тип эффекта для стилизации: ${eff.type} (ID: ${eff.id})`);
|
|
||||||
effectClasses += ' effect-info'; // Класс по умолчанию или информационный
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<span class="${effectClasses}" title="${title}">${displayText}</span>`;
|
|
||||||
}).join(' '); // Объединяем все HTML-строки эффектов в одну
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateEffectsUI(currentGameState) {
|
|
||||||
if (!currentGameState || !window.GAME_CONFIG) { return; }
|
|
||||||
const mySlotId = window.myPlayerId; // Технический ID слота этого клиента
|
|
||||||
const config = window.GAME_CONFIG;
|
|
||||||
if (!mySlotId) { return; }
|
|
||||||
|
|
||||||
const opponentSlotId = mySlotId === config.PLAYER_ID ? config.OPPONENT_ID : config.PLAYER_ID;
|
|
||||||
|
|
||||||
const myState = currentGameState[mySlotId]; // Состояние персонажа этого клиента
|
|
||||||
if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList && myState && myState.activeEffects) {
|
|
||||||
// Разделяем эффекты на баффы и дебаффы для отображения
|
|
||||||
// Критерии разделения могут быть специфичны для игры:
|
|
||||||
// Баффы: тип BUFF, эффекты дающие блок (grantsBlock), эффекты лечения (HoT)
|
|
||||||
// Дебаффы: тип DEBUFF, тип DISABLE (кроме grantsBlock), эффекты урона (DoT)
|
|
||||||
// Включаем isFullSilence и playerSilencedOn_X в "дебаффы" для отображения (можно сделать отдельную категорию)
|
|
||||||
const myBuffs = myState.activeEffects.filter(e =>
|
|
||||||
e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || (e.type === config.ACTION_TYPE_HEAL) // HoT как бафф
|
|
||||||
);
|
|
||||||
const myDebuffs = myState.activeEffects.filter(e =>
|
|
||||||
e.type === config.ACTION_TYPE_DEBUFF ||
|
|
||||||
e.type === config.ACTION_TYPE_DISABLE // Disable как дебафф
|
|
||||||
// || (e.type === config.ACTION_TYPE_DAMAGE) // DoT как дебафф, если есть
|
|
||||||
);
|
|
||||||
// Специально добавляем полные безмолвия и заглушения абилок в дебаффы, даже если их тип не DEBUFF/DISABLE
|
|
||||||
myDebuffs.push(...myState.activeEffects.filter(e => e.isFullSilence || e.id.startsWith('playerSilencedOn_') && !myDebuffs.some(d => d.id === e.id))); // Избегаем дублирования
|
|
||||||
|
|
||||||
uiElements.player.buffsList.innerHTML = generateEffectsHTML(myBuffs);
|
uiElements.player.buffsList.innerHTML = generateEffectsHTML(myBuffs);
|
||||||
uiElements.player.debuffsList.innerHTML = generateEffectsHTML(myDebuffs);
|
uiElements.player.debuffsList.innerHTML = generateEffectsHTML(myDebuffs);
|
||||||
|
|
||||||
} else if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList) {
|
} else if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList) {
|
||||||
// Если нет активных эффектов или состояния, очищаем списки
|
// Если нет активных эффектов или состояния, очищаем списки
|
||||||
uiElements.player.buffsList.innerHTML = 'Нет';
|
uiElements.player.buffsList.innerHTML = 'Нет';
|
||||||
@ -259,21 +301,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const opponentState = currentGameState[opponentSlotId]; // Состояние оппонента этого клиента
|
// --- Обработка эффектов Оппонента (Opponent Player) ---
|
||||||
|
// Логика аналогична игроку, но условия дебаффов могут немного отличаться
|
||||||
|
// (например, префикс ID заглушения абилок)
|
||||||
if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList && opponentState && opponentState.activeEffects) {
|
if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList && opponentState && opponentState.activeEffects) {
|
||||||
const opponentBuffs = opponentState.activeEffects.filter(e =>
|
const opponentBuffs = [];
|
||||||
e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || (e.type === config.ACTION_TYPE_HEAL) // HoT как бафф
|
const opponentDebuffs = [];
|
||||||
);
|
|
||||||
const opponentDebuffs = opponentState.activeEffects.filter(e =>
|
// ИСПРАВЛЕНО: Проходим по массиву activeEffects оппонента один раз и пушим в нужный список
|
||||||
e.type === config.ACTION_TYPE_DEBUFF ||
|
opponentState.activeEffects.forEach(e => {
|
||||||
e.type === config.ACTION_TYPE_DISABLE // Disable как дебафф
|
const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL; // HoT как бафф
|
||||||
// || (e.type === config.ACTION_TYPE_DAMAGE) // DoT как дебафф, если есть
|
|
||||||
);
|
// Определяем, является ли эффект дебаффом для ОППОНЕНТА
|
||||||
// Специально добавляем полные безмолвия и заглушения абилок в дебаффы оппонента, даже если их тип не DEBUFF/DISABLE
|
// Учитываем типы DEBUFF, DISABLE, isFullSilence.
|
||||||
opponentDebuffs.push(...opponentState.activeEffects.filter(e => e.isFullSilence || e.id.startsWith('playerSilencedOn_') && !opponentDebuffs.some(d => d.id === e.id))); // Избегаем дублирования
|
// id.startsWith('playerSilencedOn_') специфично для игрока,
|
||||||
|
// id.startsWith('effect_') используется для дебаффов, наложенных на цель (например, Seal of Weakness)
|
||||||
|
const isDebuff = e.type === config.ACTION_TYPE_DEBUFF || e.type === config.ACTION_TYPE_DISABLE || e.isFullSilence || e.id.startsWith('effect_');
|
||||||
|
// Если у оппонента есть свои специфичные эффекты заглушения с другим префиксом, его тоже нужно добавить сюда.
|
||||||
|
|
||||||
|
if (isBuff) {
|
||||||
|
opponentBuffs.push(e);
|
||||||
|
} else if (isDebuff) {
|
||||||
|
opponentDebuffs.push(e);
|
||||||
|
} else {
|
||||||
|
//console.warn(`updateEffectsUI: Эффект ID "${e.id}" с типом "${e.type}" не отнесен ни к баффам, ни к дебаффам для Оппонента.`);
|
||||||
|
opponentDebuffs.push(e); // Добавим в дебаффы по умолчанию
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Сортируем списки баффов и дебаффов оппонента
|
||||||
|
opponentBuffs.sort(sortEffects);
|
||||||
|
opponentDebuffs.sort(sortEffects);
|
||||||
|
|
||||||
uiElements.opponent.buffsList.innerHTML = generateEffectsHTML(opponentBuffs);
|
uiElements.opponent.buffsList.innerHTML = generateEffectsHTML(opponentBuffs);
|
||||||
uiElements.opponent.debuffsList.innerHTML = generateEffectsHTML(opponentDebuffs);
|
uiElements.opponent.debuffsList.innerHTML = generateEffectsHTML(opponentDebuffs);
|
||||||
|
|
||||||
} else if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList) {
|
} else if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList) {
|
||||||
// Если нет активных эффектов или состояния оппонента, очищаем списки
|
// Если нет активных эффектов или состояния оппонента, очищаем списки
|
||||||
uiElements.opponent.buffsList.innerHTML = 'Нет';
|
uiElements.opponent.buffsList.innerHTML = 'Нет';
|
||||||
@ -288,7 +350,17 @@
|
|||||||
const myActualPlayerId = window.myPlayerId; // Технический ID слота этого клиента
|
const myActualPlayerId = window.myPlayerId; // Технический ID слота этого клиента
|
||||||
|
|
||||||
if (!currentGameState || !gameDataGlobal || !configGlobal || !myActualPlayerId) {
|
if (!currentGameState || !gameDataGlobal || !configGlobal || !myActualPlayerId) {
|
||||||
console.warn("updateUI: Отсутствуют глобальные gameState, gameData, GAME_CONFIG или myActualPlayerId.");
|
// console.warn("updateUI: Отсутствуют глобальные gameState, gameData, GAME_CONFIG или myActualPlayerId.");
|
||||||
|
// Сбрасываем UI панелей, если данные отсутствуют
|
||||||
|
updateFighterPanelUI('player', null, null, true);
|
||||||
|
updateFighterPanelUI('opponent', null, null, false);
|
||||||
|
// Скрываем/очищаем остальные элементы UI игры
|
||||||
|
if(uiElements.gameHeaderTitle) uiElements.gameHeaderTitle.innerHTML = `<span>Ожидание данных...</span>`;
|
||||||
|
if(uiElements.controls.turnIndicator) uiElements.controls.turnIndicator.textContent = "Ожидание данных...";
|
||||||
|
if(uiElements.controls.buttonAttack) uiElements.controls.buttonAttack.disabled = true;
|
||||||
|
if(uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true;
|
||||||
|
if(uiElements.controls.abilitiesGrid) uiElements.controls.abilitiesGrid.innerHTML = '<p class="placeholder-text">Загрузка способностей...</p>';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) {
|
if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) {
|
||||||
@ -298,54 +370,61 @@
|
|||||||
|
|
||||||
// Определяем, чей сейчас ход по ID слота
|
// Определяем, чей сейчас ход по ID слота
|
||||||
const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID;
|
const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID;
|
||||||
// Определяем ID слота оппонента для этого клиента ( необходимо для определения, чьи панели обновлять как "мои" и "противника")
|
// Определяем ID слота оппонента для этого клиента
|
||||||
const opponentActualSlotId = myActualPlayerId === configGlobal.PLAYER_ID ? configGlobal.OPPONENT_ID : configGlobal.PLAYER_ID;
|
const opponentActualSlotId = myActualPlayerId === configGlobal.PLAYER_ID ? configGlobal.OPPONENT_ID : configGlobal.PLAYER_ID;
|
||||||
|
|
||||||
// Обновление панели "моего" персонажа ( которым управляет этот клиент)
|
// Обновление панели "моего" персонажа
|
||||||
const myStateInGameState = currentGameState[myActualPlayerId];
|
const myStateInGameState = currentGameState[myActualPlayerId];
|
||||||
const myBaseStatsForUI = gameDataGlobal.playerBaseStats; // playerBaseStats в gameData - это всегда статы персонажа этого клиента
|
const myBaseStatsForUI = gameDataGlobal.playerBaseStats; // playerBaseStats в gameData - это всегда статы персонажа этого клиента
|
||||||
if (myStateInGameState && myBaseStatsForUI) {
|
if (myStateInGameState && myBaseStatsForUI) {
|
||||||
if (uiElements.player.panel) uiElements.player.panel.style.opacity = '1'; // Делаем панель полностью видимой, если есть данные
|
|
||||||
updateFighterPanelUI('player', myStateInGameState, myBaseStatsForUI, true);
|
updateFighterPanelUI('player', myStateInGameState, myBaseStatsForUI, true);
|
||||||
} else {
|
} else {
|
||||||
if (uiElements.player.panel) uiElements.player.panel.style.opacity = '0.5'; // Затемняем, если нет данных
|
updateFighterPanelUI('player', null, null, true); // Нет данных, показываем состояние ожидания
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление панели "моего оппонента" ( персонажа в слоте противника для этого клиента)
|
// Обновление панели "моего оппонента"
|
||||||
const opponentStateInGameState = currentGameState[opponentActualSlotId];
|
const opponentStateInGameState = currentGameState[opponentActualSlotId];
|
||||||
const opponentBaseStatsForUI = gameDataGlobal.opponentBaseStats; // opponentBaseStats в gameData - это всегда статы оппонента этого клиента
|
const opponentBaseStatsForUI = gameDataGlobal.opponentBaseStats; // opponentBaseStats в gameData - это всегда статы оппонента этого клиента
|
||||||
|
|
||||||
|
// Если игра окончена и игрок победил, возможно, панель оппонента уже анимирована на исчезновение.
|
||||||
|
// Не сбрасываем ее opacity/transform здесь, если она в состоянии dissolving.
|
||||||
|
const isOpponentPanelDissolving = uiElements.opponent.panel?.classList.contains('dissolving');
|
||||||
|
|
||||||
if (opponentStateInGameState && opponentBaseStatsForUI) {
|
if (opponentStateInGameState && opponentBaseStatsForUI) {
|
||||||
// Если игра не окончена, а панель оппонента "тает" или не полностью видна, восстанавливаем это
|
// Если игра не окончена, а панель оппонента "тает" или не полностью видна, восстанавливаем это
|
||||||
if (uiElements.opponent.panel && (uiElements.opponent.panel.style.opacity !== '1' || uiElements.opponent.panel.classList.contains('dissolving')) && currentGameState.isGameOver === false ) {
|
// Но не если она активно в анимации растворения (dissolving)
|
||||||
console.log("[UI UPDATE DEBUG] Opponent panel not fully visible/dissolving but game not over. Restoring opacity/transform.");
|
if (uiElements.opponent.panel && (uiElements.opponent.panel.style.opacity !== '1' || (uiElements.opponent.panel.classList.contains('dissolving') && currentGameState.isGameOver === false) )) {
|
||||||
|
// console.log("[UI UPDATE DEBUG] Opponent panel not fully visible/dissolving but game not over. Restoring opacity/transform.");
|
||||||
const panel = uiElements.opponent.panel;
|
const panel = uiElements.opponent.panel;
|
||||||
|
if (panel.classList.contains('dissolving')) {
|
||||||
panel.classList.remove('dissolving');
|
panel.classList.remove('dissolving');
|
||||||
// Force reflow before applying instant style change
|
panel.style.transition = 'none'; // Отключаем переход временно
|
||||||
panel.offsetHeight; // Trigger reflow
|
panel.offsetHeight; // Trigger reflow
|
||||||
panel.style.opacity = '1';
|
panel.style.opacity = '1';
|
||||||
panel.style.transform = 'scale(1) translateY(0)';
|
panel.style.transform = 'scale(1) translateY(0)';
|
||||||
} else if (uiElements.opponent.panel) {
|
panel.style.transition = ''; // Восстанавливаем переход
|
||||||
uiElements.opponent.panel.style.opacity = '1'; // Убеждаемся, что видна, если есть данные
|
} else {
|
||||||
|
panel.style.opacity = '1';
|
||||||
|
panel.style.transform = 'scale(1) translateY(0)'; // В случае если просто opacity < 1
|
||||||
|
}
|
||||||
|
} else if (uiElements.opponent.panel && !isOpponentPanelDissolving) {
|
||||||
|
uiElements.opponent.panel.style.opacity = '1'; // Убеждаемся, что видна, если есть данные и не растворяется
|
||||||
}
|
}
|
||||||
updateFighterPanelUI('opponent', opponentStateInGameState, opponentBaseStatsForUI, false);
|
updateFighterPanelUI('opponent', opponentStateInGameState, opponentBaseStatsForUI, false);
|
||||||
} else {
|
} else {
|
||||||
// Нет данных оппонента ( например, PvP игра ожидает игрока). Затемняем панель.
|
// Нет данных оппонента ( например, PvP игра ожидает игрока). Затемняем панель и очищаем.
|
||||||
if (uiElements.opponent.panel) {
|
// Но не сбрасываем opacity/transform, если она активно в анимации растворения
|
||||||
uiElements.opponent.panel.style.opacity = '0.5';
|
if (!isOpponentPanelDissolving) {
|
||||||
// Очищаем панель, если данных нет
|
updateFighterPanelUI('opponent', null, null, false); // Нет данных, показываем состояние ожидания/пустоты
|
||||||
if(uiElements.opponent.name) uiElements.opponent.name.innerHTML = '<i class="fas fa-question icon-opponent"></i> Ожидание игрока...';
|
} else {
|
||||||
if(uiElements.opponent.hpText) uiElements.opponent.hpText.textContent = 'N/A';
|
// Если панель растворяется, не обновляем ее содержимое и оставляем текущие стили opacity/transform
|
||||||
if(uiElements.opponent.resourceText) uiElements.opponent.resourceText.textContent = 'N/A';
|
console.log("[UI UPDATE DEBUG] Opponent panel is dissolving, skipping content update.");
|
||||||
if(uiElements.opponent.status) uiElements.opponent.status.textContent = 'Не готов';
|
|
||||||
if(uiElements.opponent.buffsList) uiElements.opponent.buffsList.innerHTML = 'Нет';
|
|
||||||
if(uiElements.opponent.debuffsList) uiElements.opponent.debuffsList.innerHTML = 'Нет';
|
|
||||||
if(uiElements.opponent.avatar) uiElements.opponent.avatar.src = 'images/default_avatar.png';
|
|
||||||
if(uiElements.opponentResourceTypeIcon) uiElements.opponentResourceTypeIcon.className = 'fas fa-question';
|
|
||||||
if(uiElements.opponentResourceBarContainer) uiElements.opponentResourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Обновление эффектов
|
||||||
updateEffectsUI(currentGameState);
|
updateEffectsUI(currentGameState);
|
||||||
|
|
||||||
// Обновление заголовка игры ( Имя1 vs Имя2)
|
// Обновление заголовка игры ( Имя1 vs Имя2)
|
||||||
@ -355,41 +434,76 @@
|
|||||||
const myKey = gameDataGlobal.playerBaseStats.characterKey;
|
const myKey = gameDataGlobal.playerBaseStats.characterKey;
|
||||||
const opponentKey = gameDataGlobal.opponentBaseStats.characterKey;
|
const opponentKey = gameDataGlobal.opponentBaseStats.characterKey;
|
||||||
|
|
||||||
let myClass = 'title-player'; // Базовый класс
|
let myClass = 'title-player';
|
||||||
if (myKey === 'elena') myClass = 'title-enchantress';
|
if (myKey === 'elena') myClass = 'title-enchantress';
|
||||||
else if (myKey === 'almagest') myClass = 'title-sorceress';
|
else if (myKey === 'almagest') myClass = 'title-sorceress';
|
||||||
|
else if (myKey === 'balard') myClass = 'title-knight'; // Вдруг AI Балард в PvP
|
||||||
|
|
||||||
let opponentClass = 'title-opponent'; // Базовый класс
|
let opponentClass = 'title-opponent';
|
||||||
if (opponentKey === 'elena') opponentClass = 'title-enchantress';
|
if (opponentKey === 'elena') opponentClass = 'title-enchantress';
|
||||||
else if (opponentKey === 'almagest') opponentClass = 'title-sorceress';
|
else if (opponentKey === 'almagest') opponentClass = 'title-sorceress';
|
||||||
else if (opponentKey === 'balard') opponentClass = 'title-knight';
|
else if (opponentKey === 'balard') opponentClass = 'title-knight';
|
||||||
|
|
||||||
// Используем имена персонажей, которые видит этот клиент
|
|
||||||
uiElements.gameHeaderTitle.innerHTML = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="${opponentClass}">${opponentName}</span>`;
|
uiElements.gameHeaderTitle.innerHTML = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="${opponentClass}">${opponentName}</span>`;
|
||||||
|
} else if (uiElements.gameHeaderTitle) {
|
||||||
|
// Обновление заголовка в режиме ожидания
|
||||||
|
const myName = gameDataGlobal.playerBaseStats?.name || 'Игрок 1';
|
||||||
|
const myKey = gameDataGlobal.playerBaseStats?.characterKey;
|
||||||
|
let myClass = 'title-player';
|
||||||
|
if (myKey === 'elena') myClass = 'title-enchantress';
|
||||||
|
else if (myKey === 'almagest') myClass = 'title-sorceress';
|
||||||
|
|
||||||
|
uiElements.gameHeaderTitle.innerHTML = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="title-opponent">Ожидание игрока...</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Управление активностью кнопок
|
|
||||||
// Этот клиент может действовать только если его технический слот ('player' или 'opponent')
|
// Управление активностью кнопок и индикатор хода
|
||||||
// соответствует слоту, чей сейчас ход ( actorSlotWhoseTurnItIs).
|
|
||||||
const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId;
|
const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId;
|
||||||
const isGameActive = !currentGameState.isGameOver;
|
const isGameActive = !currentGameState.isGameOver;
|
||||||
|
const myCharacterState = currentGameState[myActualPlayerId];
|
||||||
|
|
||||||
|
// Обновление индикатора хода
|
||||||
|
if (uiElements.controls.turnIndicator) {
|
||||||
|
if (isGameActive) {
|
||||||
|
const currentTurnActor = currentGameState.isPlayerTurn ? currentGameState.player : currentGameState.opponent;
|
||||||
|
uiElements.controls.turnIndicator.textContent = `Ход ${currentGameState.turnNumber}: ${currentTurnActor?.name || 'Неизвестно'}`;
|
||||||
|
// Управляем цветом индикатора хода
|
||||||
|
if (currentTurnActor?.id === myActualPlayerId) {
|
||||||
|
uiElements.controls.turnIndicator.style.color = 'var(--turn-color)'; // Свой ход - желтый
|
||||||
|
} else {
|
||||||
|
uiElements.controls.turnIndicator.style.color = 'var(--text-muted)'; // Ход противника - приглушенный
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uiElements.controls.turnIndicator.textContent = "Игра окончена"; // Или можно скрыть его
|
||||||
|
uiElements.controls.turnIndicator.style.color = 'var(--text-muted)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Кнопка атаки
|
// Кнопка атаки
|
||||||
if (uiElements.controls.buttonAttack) {
|
if (uiElements.controls.buttonAttack) {
|
||||||
// Кнопка атаки активна, если это ход этого клиента и игра активна
|
// Кнопка атаки активна, если это ход этого клиента и игра активна (полное безмолвие не блокирует базовую атаку)
|
||||||
|
// ИСПРАВЛЕНО: Убрана проверка !isFullySilenced из условия disabled для базовой атаки
|
||||||
uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive);
|
uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive);
|
||||||
|
|
||||||
// Управление классом для подсветки бафнутой атаки
|
// Управление классом для подсветки бафнутой атаки
|
||||||
const myCharKey = gameDataGlobal.playerBaseStats?.characterKey;
|
const myCharKey = gameDataGlobal.playerBaseStats?.characterKey;
|
||||||
const myStateForAttackBuff = currentGameState[myActualPlayerId]; // Состояние моего персонажа
|
|
||||||
let attackBuffId = null;
|
let attackBuffId = null;
|
||||||
if (myCharKey === 'elena') attackBuffId = configGlobal.ABILITY_ID_NATURE_STRENGTH;
|
if (myCharKey === 'elena') attackBuffId = configGlobal.ABILITY_ID_NATURE_STRENGTH;
|
||||||
else if (myCharKey === 'almagest') attackBuffId = configGlobal.ABILITY_ID_ALMAGEST_BUFF_ATTACK;
|
else if (myCharKey === 'almagest') attackBuffId = configGlobal.ABILITY_ID_ALMAGEST_BUFF_ATTACK;
|
||||||
|
|
||||||
if (attackBuffId && myStateForAttackBuff && myStateForAttackBuff.activeEffects) {
|
if (attackBuffId && myCharacterState && myCharacterState.activeEffects) {
|
||||||
// Проверяем, есть ли активный бафф атаки И он не только что наложен в этом ходу
|
// Проверяем, есть ли активный "отложенный" бафф (isDelayed=true) на атакующем,
|
||||||
const isAttackBuffReady = myStateForAttackBuff.activeEffects.some(eff => eff.id === attackBuffId && !eff.justCast && eff.turnsLeft > 0);
|
// который готов сработать на следующую атаку.
|
||||||
|
const isAttackBuffReady = myCharacterState.activeEffects.some(
|
||||||
|
eff => (eff.id === attackBuffId || eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK)
|
||||||
|
&& eff.isDelayed // Явно проверяем, что это отложенный бафф
|
||||||
|
&& eff.turnsLeft > 0 // Эффект должен еще действовать
|
||||||
|
&& !eff.justCast // Не должен быть наложен в этом ходу, чтобы сработать НА ЭТОМ ходу
|
||||||
|
);
|
||||||
|
|
||||||
// Подсветка активна, если бафф готов И это ход этого клиента И игра активна
|
// Подсветка активна, если бафф готов И это ход этого клиента И игра активна
|
||||||
|
// Подсветка не зависит от безмолвия, т.к. атака возможна и под безмолвием.
|
||||||
uiElements.controls.buttonAttack.classList.toggle(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed', isAttackBuffReady && canThisClientAct && isGameActive);
|
uiElements.controls.buttonAttack.classList.toggle(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed', isAttackBuffReady && canThisClientAct && isGameActive);
|
||||||
} else {
|
} else {
|
||||||
uiElements.controls.buttonAttack.classList.remove(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed');
|
uiElements.controls.buttonAttack.classList.remove(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed');
|
||||||
@ -398,81 +512,70 @@
|
|||||||
if (uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true; // Пока не используется
|
if (uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true; // Пока не используется
|
||||||
|
|
||||||
// Кнопки способностей
|
// Кнопки способностей
|
||||||
const actingPlayerState = currentGameState[myActualPlayerId]; // Состояние моего персонажа
|
const actingPlayerState = myCharacterState; // Состояние моего персонажа
|
||||||
const actingPlayerAbilities = gameDataGlobal.playerAbilities; // Способности моего персонажа (с точки зрения клиента)
|
const actingPlayerAbilities = gameDataGlobal.playerAbilities; // Способности моего персонажа (с точки зрения клиента)
|
||||||
const actingPlayerResourceName = gameDataGlobal.playerBaseStats?.resourceName; // Имя ресурса моего персонажа
|
const actingPlayerResourceName = gameDataGlobal.playerBaseStats?.resourceName; // Имя ресурса моего персонажа
|
||||||
|
const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId]; // Состояние оппонента этого клиента
|
||||||
|
|
||||||
uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => {
|
uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => {
|
||||||
if (!(button instanceof HTMLButtonElement) || !actingPlayerState || !actingPlayerAbilities || !actingPlayerResourceName || !isGameActive) {
|
// Получаем актуальное состояние способности из actingPlayerState (которое пришло с сервера)
|
||||||
// Если нет необходимых данных или игра неактивна, дизейблим все кнопки способностей
|
const abilityId = button.dataset.abilityId;
|
||||||
|
const abilityDataFromGameData = actingPlayerAbilities?.find(ab => ab.id === abilityId);
|
||||||
|
|
||||||
|
// Если игра неактивна, нет данных о бойце, способностях или ресурсе, дизейблим кнопку.
|
||||||
|
if (!(button instanceof HTMLButtonElement) || !isGameActive || !canThisClientAct || !actingPlayerState || !actingPlayerAbilities || !actingPlayerResourceName || !abilityDataFromGameData) {
|
||||||
if(button instanceof HTMLButtonElement) button.disabled = true;
|
if(button instanceof HTMLButtonElement) button.disabled = true;
|
||||||
button.classList.remove(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced', configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
|
button.classList.remove(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced', configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
|
||||||
const cooldownDisplay = button.querySelector('.ability-cooldown-display');
|
const cooldownDisplay = button.querySelector('.ability-cooldown-display');
|
||||||
if (cooldownDisplay) cooldownDisplay.style.display = 'none';
|
if (cooldownDisplay) cooldownDisplay.style.display = 'none';
|
||||||
return;
|
return; // Пропускаем дальнейшую логику обновления кнопки, если она должна быть disabled по базовым условиям
|
||||||
}
|
|
||||||
const abilityId = button.dataset.abilityId;
|
|
||||||
const ability = actingPlayerAbilities.find(ab => ab.id === abilityId);
|
|
||||||
if (!ability) {
|
|
||||||
button.disabled = true; // Если способность не найдена в данных (ошибка)
|
|
||||||
button.classList.remove(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced', configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
|
|
||||||
const cooldownDisplay = button.querySelector('.ability-cooldown-display');
|
|
||||||
if (cooldownDisplay) cooldownDisplay.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем условия доступности способности
|
// Проверяем условия доступности способности из актуального состояния игры (actingPlayerState)
|
||||||
const hasEnoughResource = actingPlayerState.currentResource >= ability.cost;
|
const hasEnoughResource = actingPlayerState.currentResource >= abilityDataFromGameData.cost;
|
||||||
// Бафф уже активен (для баффов, которые не стакаются)
|
const isOnCooldown = (actingPlayerState.abilityCooldowns?.[abilityId] || 0) > 0; // Проверяем КД по 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 isGenerallySilenced = actingPlayerState.activeEffects?.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
|
||||||
// Под специфическим заглушением этой способности
|
// Под специфическим заглушением этой способности (ищем в disabledAbilities актуального состояния)
|
||||||
const specificSilenceEffect = actingPlayerState.disabledAbilities?.find(dis => dis.abilityId === abilityId && dis.turnsLeft > 0);
|
const isAbilitySpecificallySilenced = actingPlayerState.disabledAbilities?.some(dis => dis.abilityId === abilityId && dis.turnsLeft > 0);
|
||||||
const isSpecificallySilenced = !!specificSilenceEffect;
|
const isSilenced = isGenerallySilenced || isAbilitySpecificallySilenced; // Считается заглушенным, если под полным или специфическим безмолвием
|
||||||
const isSilenced = isGenerallySilenced || isSpecificallySilenced; // Считается заглушенным, если под полным или специфическим безмолвием
|
|
||||||
// Определяем длительность безмолвия для отображения (берем из специфического, если есть, иначе из полного)
|
// Определяем длительность безмолвия для отображения (берем из специфического, если есть, иначе из полного)
|
||||||
const silenceTurnsLeft = isSpecificallySilenced
|
const silenceTurnsLeft = isAbilitySpecificallySilenced
|
||||||
? (specificSilenceEffect?.turnsLeft || 0)
|
? (actingPlayerState.disabledAbilities?.find(dis => dis.abilityId === abilityId)?.turnsLeft || 0)
|
||||||
: (isGenerallySilenced ? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0) : 0);
|
: (isGenerallySilenced ? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0) : 0);
|
||||||
|
|
||||||
|
|
||||||
|
// Нельзя кастовать бафф, если он уже активен (для баффов, которые не стакаются)
|
||||||
|
const isBuffAlreadyActive = abilityDataFromGameData.type === configGlobal.ACTION_TYPE_BUFF && actingPlayerState.activeEffects?.some(eff => eff.id === abilityId);
|
||||||
|
|
||||||
// Нельзя кастовать дебафф на цель, если он уже на ней (для определенных дебаффов)
|
// Нельзя кастовать дебафф на цель, если он уже на ней (для определенных дебаффов)
|
||||||
// Нужна проверка состояния ОППОНЕНТА этого клиента (т.е. цели)
|
const isTargetedDebuffAbility = abilityId === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || abilityId === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF;
|
||||||
const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId];
|
const effectIdForDebuff = 'effect_' + abilityId; // Ищем эффект с префиксом effect_ на цели (оппоненте)
|
||||||
let isDebuffAlreadyOnTarget = false;
|
const isDebuffAlreadyOnTarget = isTargetedDebuffAbility && opponentStateForDebuffCheck && opponentStateForDebuffCheck.activeEffects?.some(e => e.id === effectIdForDebuff);
|
||||||
const isTargetedDebuffAbility = ability.id === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF;
|
|
||||||
if (isTargetedDebuffAbility && opponentStateForDebuffCheck && opponentStateForDebuffCheck.activeEffects) {
|
|
||||||
const effectIdForDebuff = 'effect_' + ability.id; // Ищем эффект с префиксом effect_ на цели
|
|
||||||
isDebuffAlreadyOnTarget = opponentStateForDebuffCheck.activeEffects.some(e => e.id === effectIdForDebuff);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Кнопка активна, если:
|
// Кнопка способности активна, если:
|
||||||
// - Это ход этого клиента
|
// - Это ход этого клиента (проверено выше: canThisClientAct)
|
||||||
// - Игра активна
|
// - Игра активна (проверено выше: isGameActive)
|
||||||
// - Достаточно ресурса
|
// - Достаточно ресурса
|
||||||
// - Бафф не активен (если это бафф)
|
// - Бафф не активен (если это бафф)
|
||||||
// - Не на кулдауне
|
// - Не на кулдауне
|
||||||
// - Не под безмолвием (полным или специфическим)
|
// - Не под безмолвием (полным или специфическим) <--- ЭТО УСЛОВИЕ ОСТАЕТСЯ ДЛЯ СПОСОБНОСТЕЙ
|
||||||
// - Дебафф не активен на цели (если это такой дебафф)
|
// - Дебафф не активен на цели (если это такой дебафф)
|
||||||
button.disabled = !(canThisClientAct && isGameActive) ||
|
button.disabled = !hasEnoughResource ||
|
||||||
!hasEnoughResource ||
|
|
||||||
isBuffAlreadyActive ||
|
isBuffAlreadyActive ||
|
||||||
isSilenced ||
|
isSilenced || // Способности БЛОКИРУЮТСЯ полным безмолвием
|
||||||
isOnCooldown ||
|
isOnCooldown ||
|
||||||
isDebuffAlreadyOnTarget;
|
isDebuffAlreadyOnTarget;
|
||||||
|
|
||||||
|
|
||||||
// Управление классами для стилизации кнопки
|
// Управление классами для стилизации кнопки (применяются независимо от окончательного disabled состояния)
|
||||||
button.classList.remove(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced', configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
|
button.classList.remove(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced', configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
|
||||||
const cooldownDisplay = button.querySelector('.ability-cooldown-display');
|
const cooldownDisplay = button.querySelector('.ability-cooldown-display');
|
||||||
|
|
||||||
if (isOnCooldown) {
|
if (isOnCooldown) {
|
||||||
button.classList.add(configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
|
button.classList.add(configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
|
||||||
if (cooldownDisplay) { cooldownDisplay.textContent = `КД: ${actingPlayerState.abilityCooldowns[ability.id]}`; cooldownDisplay.style.display = 'block'; }
|
if (cooldownDisplay) { cooldownDisplay.textContent = `КД: ${actingPlayerState.abilityCooldowns[abilityId]}`; cooldownDisplay.style.display = 'block'; }
|
||||||
} else if (isSilenced) {
|
} else if (isSilenced) {
|
||||||
button.classList.add(configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced');
|
button.classList.add(configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced');
|
||||||
if (cooldownDisplay) {
|
if (cooldownDisplay) {
|
||||||
@ -482,49 +585,64 @@
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (cooldownDisplay) cooldownDisplay.style.display = 'none'; // Скрываем, если нет ни КД, ни безмолвия
|
if (cooldownDisplay) cooldownDisplay.style.display = 'none'; // Скрываем, если нет ни КД, ни безмолвия
|
||||||
// Добавляем классы, если действие возможно, но есть ограничения (недостаточно ресурса, бафф активен, дебафф на цели)
|
|
||||||
// Эти классы используются для визуальной обратной связи, когда кнопка *не* задизейблена по КД или безмолвию.
|
// Добавляем классы для визуальной обратной связи, ЕСЛИ кнопка НЕ задизейблена по КД или Безмолвию
|
||||||
// Если кнопка disabled из-за !hasEnoughResource, классы not-enough-resource и buff-is-active все равно могут быть добавлены.
|
// (т.е. эти классы показывают *другие* причины, по которым кнопка может быть disabled)
|
||||||
button.classList.toggle(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', !hasEnoughResource && !isBuffAlreadyActive && !isDebuffAlreadyOnTarget);
|
if (!isOnCooldown && !isSilenced) {
|
||||||
button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', isBuffAlreadyActive && !isDebuffAlreadyOnTarget);
|
button.classList.toggle(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', !hasEnoughResource);
|
||||||
// Если дебафф уже на цели, но кнопка не задизейблена по другим причинам, можно добавить отдельный класс
|
button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', isBuffAlreadyActive);
|
||||||
// button.classList.toggle('debuff-on-target', isDebuffAlreadyOnTarget && !button.disabled);
|
// Если дебафф уже на цели, но кнопка не задизейблена по другим причинам, можно добавить отдельный класс для стилизации
|
||||||
|
// button.classList.toggle('debuff-on-target', isDebuffAlreadyOnTarget);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление title (всплывающей подсказки) - показываем полную информацию
|
// Обновление title (всплывающей подсказки) - показываем полную информацию
|
||||||
let titleText = `${ability.name} (${ability.cost} ${actingPlayerResourceName})`;
|
// Используем abilityDataFromGameData для базовой информации
|
||||||
let descriptionTextFull = ability.description; // Используем описание, пришедшее с сервера
|
let titleText = `${abilityDataFromGameData.name} (${abilityDataFromGameData.cost} ${actingPlayerResourceName})`;
|
||||||
|
let descriptionTextFull = abilityDataFromGameData.description; // Используем описание, пришедшее с сервера
|
||||||
|
|
||||||
|
// Если есть функция описания, используем ее
|
||||||
|
if (typeof abilityDataFromGameData.descriptionFunction === 'function') {
|
||||||
|
// Передаем конфиг и статы оппонента (цели) для генерации описания
|
||||||
|
const opponentBaseStatsForDesc = gameDataGlobal.opponentBaseStats; // Статы оппонента этого клиента
|
||||||
|
descriptionTextFull = abilityDataFromGameData.descriptionFunction(configGlobal, opponentBaseStatsForDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (descriptionTextFull) titleText += ` - ${descriptionTextFull}`;
|
if (descriptionTextFull) titleText += ` - ${descriptionTextFull}`;
|
||||||
|
|
||||||
let abilityBaseCooldown = ability.cooldown; // Исходный КД из данных
|
// Добавляем информацию об исходном КД из данных способности
|
||||||
// Учитываем внутренние КД Баларда, если это способность Баларда (хотя игроку AI способности не отображаются, но для полноты)
|
let abilityBaseCooldown = abilityDataFromGameData.cooldown;
|
||||||
// if (actingPlayerState.characterKey === 'balard') {
|
|
||||||
// if (ability.id === configGlobal.ABILITY_ID_BALARD_SILENCE) abilityBaseCooldown = configGlobal.BALARD_SILENCE_INTERNAL_COOLDOWN;
|
|
||||||
// else if (ability.id === configGlobal.ABILITY_ID_BALARD_MANA_DRAIN && typeof ability.internalCooldownValue === 'number') abilityBaseCooldown = ability.internalCooldownValue;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (typeof abilityBaseCooldown === 'number' && abilityBaseCooldown > 0) {
|
if (typeof abilityBaseCooldown === 'number' && abilityBaseCooldown > 0) {
|
||||||
titleText += ` (КД: ${abilityBaseCooldown} х.)`;
|
titleText += ` (Исходный КД: ${abilityBaseCooldown} х.)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем информацию о текущем состоянии (КД, безмолвие, активный бафф/debuff) в тултип, если применимо
|
// Добавляем информацию о текущем состоянии (КД, безмолвие, активный бафф/debuff) в тултип, если применимо
|
||||||
if (isOnCooldown) {
|
if (isOnCooldown) {
|
||||||
titleText += ` | На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[ability.id]} х.`;
|
titleText += ` | На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[abilityId]} х.`;
|
||||||
}
|
}
|
||||||
if (isSilenced) {
|
if (isSilenced) {
|
||||||
titleText += ` | Под безмолвием! Осталось: ${silenceTurnsLeft} х.`;
|
titleText += ` | Под безмолвием! Осталось: ${silenceTurnsLeft} х.`;
|
||||||
}
|
}
|
||||||
if (isBuffAlreadyActive) {
|
if (isBuffAlreadyActive) {
|
||||||
const activeEffect = actingPlayerState.activeEffects?.find(eff => eff.id === abilityId);
|
const activeEffect = actingPlayerState.activeEffects?.find(eff => eff.id === abilityId); // Ищем активный эффект по ID способности
|
||||||
titleText += ` | Эффект уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}.`;
|
// Если бафф имеет свойство 'justCast' и наложен в этом ходу, он не "готов" сработать на ЭТОМ ходу.
|
||||||
|
// Это может быть важно для тултипа, если нужно отличать "только что наложен" от "готов к следующему действию".
|
||||||
|
// Для "Силы Природы" (isDelayed=true) состояние "активен" означает "готов сработать на следующую атаку".
|
||||||
|
const isDelayedBuffReady = isBuffAlreadyActive && activeEffect && activeEffect.isDelayed && !activeEffect.justCast && activeEffect.turnsLeft > 0;
|
||||||
|
|
||||||
|
if (isDelayedBuffReady) {
|
||||||
|
titleText += ` | Эффект активен и сработает при следующей базовой атаке (${activeEffect.turnsLeft} х.)`;
|
||||||
|
} else if (isBuffAlreadyActive) {
|
||||||
|
titleText += ` | Эффект уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}. Нельзя применить повторно.`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (isDebuffAlreadyOnTarget && opponentStateForDebuffCheck) {
|
if (isDebuffAlreadyOnTarget && opponentStateForDebuffCheck) {
|
||||||
const activeDebuff = opponentStateForDebuffCheck.activeEffects?.find(e => e.id === 'effect_' + abilityId);
|
const activeDebuff = opponentStateForDebuffCheck.activeEffects?.find(e => e.id === 'effect_' + abilityId);
|
||||||
titleText += ` | Эффект уже наложен на ${opponentBaseStatsForUI?.name || 'противника'}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}.`;
|
titleText += ` | Эффект уже наложен на ${gameDataGlobal.opponentBaseStats?.name || 'противника'}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}.`;
|
||||||
}
|
}
|
||||||
if (!hasEnoughResource) {
|
if (!hasEnoughResource) {
|
||||||
titleText += ` | Недостаточно ${actingPlayerResourceName} (${actingPlayerState.currentResource}/${ability.cost})`;
|
titleText += ` | Недостаточно ${actingPlayerResourceName} (${actingPlayerState.currentResource}/${abilityDataFromGameData.cost})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -539,19 +657,18 @@
|
|||||||
* @param {string|null} opponentCharacterKeyFromClient - Ключ персонажа оппонента с т.з. клиента.
|
* @param {string|null} opponentCharacterKeyFromClient - Ключ персонажа оппонента с т.з. клиента.
|
||||||
* @param {object} [data=null] - Полный объект данных из события gameOver (включает disconnectedCharacterName и т.д.)
|
* @param {object} [data=null] - Полный объект данных из события gameOver (включает disconnectedCharacterName и т.д.)
|
||||||
*/
|
*/
|
||||||
function showGameOver(playerWon, reason = "", opponentCharacterKeyFromClient = null, data = null) { // ИСПРАВЛЕНО: Добавлен аргумент data
|
function showGameOver(playerWon, reason = "", opponentCharacterKeyFromClient = null, data = null) {
|
||||||
const config = window.GAME_CONFIG || {};
|
const config = window.GAME_CONFIG || {};
|
||||||
// Используем gameData, сохраненное в client.js, так как оно отражает перспективу этого клиента
|
const clientSpecificGameData = window.gameData; // Используем gameData, сохраненное в client.js
|
||||||
const clientSpecificGameData = window.gameData;
|
|
||||||
const currentActualGameState = window.gameState; // Самое актуальное состояние игры
|
const currentActualGameState = window.gameState; // Самое актуальное состояние игры
|
||||||
const gameOverScreenElement = uiElements.gameOver.screen;
|
const gameOverScreenElement = uiElements.gameOver.screen;
|
||||||
|
|
||||||
console.log(`[UI.JS DEBUG] showGameOver CALLED. PlayerWon: ${playerWon}, Reason: ${reason}`);
|
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] captured currentActualGameState?.isGameOver at call time: ${currentActualGameState?.isGameOver}`); // Log state at call time
|
||||||
console.log(`[UI.JS DEBUG] Opponent Character Key (from client via param): ${opponentCharacterKeyFromClient}`);
|
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] My Character Name (from window.gameData): ${clientSpecificGameData?.playerBaseStats?.name}`);
|
||||||
console.log(`[UI.JS DEBUG] Opponent Character Name (from window.gameData): ${clientSpecificGameData?.opponentBaseStats?.name}`);
|
console.log(`[UI.JS DEBUG] Opponent Character Name (from window.gameData): ${clientSpecificGameData?.opponentBaseStats?.name}`);
|
||||||
console.log(`[UI.JS DEBUG] Full game over data received:`, data); // Добавьте этот лог
|
console.log(`[UI.JS DEBUG] Full game over data received:`, data);
|
||||||
|
|
||||||
|
|
||||||
if (!gameOverScreenElement) {
|
if (!gameOverScreenElement) {
|
||||||
@ -560,7 +677,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resultMsgElement = uiElements.gameOver.message;
|
const resultMsgElement = uiElements.gameOver.message;
|
||||||
// Имена для сообщения берем из clientSpecificGameData, т.к. они уже "перевернуты" для каждого клиента
|
|
||||||
const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок";
|
const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок";
|
||||||
const opponentNameForResult = clientSpecificGameData?.opponentBaseStats?.name || "Противник";
|
const opponentNameForResult = clientSpecificGameData?.opponentBaseStats?.name || "Противник";
|
||||||
|
|
||||||
@ -568,8 +684,8 @@
|
|||||||
let winText = `Победа! ${myNameForResult} празднует!`;
|
let winText = `Победа! ${myNameForResult} празднует!`;
|
||||||
let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`;
|
let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`;
|
||||||
if (reason === 'opponent_disconnected') {
|
if (reason === 'opponent_disconnected') {
|
||||||
// Определяем, кто отключился, по данным из события gameOver
|
|
||||||
let disconnectedName = "Противник";
|
let disconnectedName = "Противник";
|
||||||
|
// Если в данных gameOver есть имя отключившегося персонажа, используем его
|
||||||
if (data && data.disconnectedCharacterName) {
|
if (data && data.disconnectedCharacterName) {
|
||||||
disconnectedName = data.disconnectedCharacterName;
|
disconnectedName = data.disconnectedCharacterName;
|
||||||
} else {
|
} else {
|
||||||
@ -578,10 +694,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
winText = `${disconnectedName} покинул(а) игру. Победа присуждается вам!`;
|
winText = `${disconnectedName} покинул(а) игру. Победа присуждается вам!`;
|
||||||
// Если оппонент отключился, а мы проиграли (технически такое возможно, если сервер так решит)
|
// В PvP, если оппонент отключился, а текущий игрок проиграл (что странно, но возможно),
|
||||||
// То текст поражения можно оставить стандартным или специфичным.
|
// сообщение о поражении может быть стандартным или специфичным.
|
||||||
// Пока оставим стандартный, если playerWon = false и reason = opponent_disconnected.
|
// В AI режиме, если игрок отключился, нет формального победителя AI.
|
||||||
|
// Пусть будет стандартное поражение, если playerWon === false
|
||||||
|
if (!playerWon) {
|
||||||
|
// Возможно, специфичный текст для дисконнекта, когда ты проиграл?
|
||||||
|
// loseText = `Игра завершена из-за отключения ${disconnectedName}. Вы проиграли.`
|
||||||
}
|
}
|
||||||
|
} else if (reason === 'hp_zero') {
|
||||||
|
// Стандартное завершение по HP - тексты определены выше
|
||||||
|
}
|
||||||
|
// Добавьте обработку других причин завершения, если они будут
|
||||||
|
else {
|
||||||
|
// Неизвестная причина завершения
|
||||||
|
winText = `Игра окончена. Победа! (${reason})`;
|
||||||
|
loseText = `Игра окончена. Поражение. (${reason})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
resultMsgElement.textContent = playerWon ? winText : loseText;
|
resultMsgElement.textContent = playerWon ? winText : loseText;
|
||||||
resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)';
|
resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)';
|
||||||
}
|
}
|
||||||
@ -589,74 +720,104 @@
|
|||||||
const opponentPanelElement = uiElements.opponent.panel;
|
const opponentPanelElement = uiElements.opponent.panel;
|
||||||
if (opponentPanelElement) {
|
if (opponentPanelElement) {
|
||||||
// Сначала убеждаемся, что анимация растворения снята, если она была активна от предыдущей попытки
|
// Сначала убеждаемся, что анимация растворения снята, если она была активна от предыдущей попытки
|
||||||
// console.log(`[UI.JS DEBUG] Opponent panel classList before potential dissolve: ${opponentPanelElement.className}`);
|
// и не должна применяться сейчас.
|
||||||
opponentPanelElement.classList.remove('dissolving');
|
opponentPanelElement.classList.remove('dissolving');
|
||||||
opponentPanelElement.offsetHeight; // Trigger reflow to reset state instantly
|
opponentPanelElement.style.transition = 'none'; // Временно отключаем transition
|
||||||
|
opponentPanelElement.offsetHeight; // Trigger reflow to apply style instantly
|
||||||
|
|
||||||
// Используем opponentCharacterKeyFromClient, так как это ключ реального персонажа оппонента с т.з. клиента
|
// Используем characterKey проигравшего (переданный из GameInstance),
|
||||||
const keyForDissolveEffect = opponentCharacterKeyFromClient;
|
// так как анимация растворения должна быть специфична для проигравшего персонажа,
|
||||||
|
// который может быть Балардом или Альмагест.
|
||||||
|
const loserCharacterKeyForDissolve = data?.loserCharacterKey;
|
||||||
|
|
||||||
// Применяем анимацию растворения только если игра окончена, игрок выиграл, и это не дисконнект,
|
// Применяем анимацию растворения только если игра окончена, игрок победил,
|
||||||
// и противник был Балардом или Альмагест (у которых есть эта анимация).
|
// и проигравший был Балардом или Альмагест (у которых есть эта анимация).
|
||||||
if (currentActualGameState && currentActualGameState.isGameOver === true && playerWon && reason !== 'opponent_disconnected') {
|
// Исключаем случай дисконнекта, если анимация должна быть только при "убийстве" по HP.
|
||||||
if (keyForDissolveEffect === 'balard' || keyForDissolveEffect === 'almagest') {
|
// В текущем CSS анимация растворения не зависит от причины, но зависит от класса 'dissolving'.
|
||||||
console.log(`[UI.JS DEBUG] ADDING .dissolving to opponent panel. Conditions met.`);
|
// Добавляем класс, если игра окончена, игрок победил, и проигравший персонаж - Балард или Альмагест.
|
||||||
|
// Если игра окончена И игрок проиграл И оппонент был Балардом/Альмагест, но игрок проиграл, анимация растворения НЕ применяется к панели оппонента.
|
||||||
|
// Поэтому условие playerWon && ... корректно.
|
||||||
|
if (currentActualGameState && currentActualGameState.isGameOver === true && playerWon) {
|
||||||
|
// Проверяем, является ли проигравший (т.е. оппонент этого клиента) Балардом или Альмагест
|
||||||
|
if (loserCharacterKeyForDissolve === 'balard' || loserCharacterKeyForDissolve === 'almagest') {
|
||||||
|
console.log(`[UI.JS DEBUG] ADDING .dissolving to opponent panel.`);
|
||||||
opponentPanelElement.classList.add('dissolving');
|
opponentPanelElement.classList.add('dissolving');
|
||||||
// Убеждаемся, что панель станет полностью прозрачной и сместится после анимации
|
// Убеждаемся, что панель станет полностью прозрачной и сместится после анимации.
|
||||||
opponentPanelElement.style.opacity = '0';
|
// Конечные стили (opacity: 0, transform) могут быть заданы в CSS для класса .dissolving,
|
||||||
// opponentPanelElement.style.transform = 'scale(0.9) translateY(20px)'; // Трансформация уже в CSS анимации
|
// но их можно также установить здесь после добавления класса для гарантии.
|
||||||
|
opponentPanelElement.style.opacity = '0'; // Конечный стиль для transition
|
||||||
|
// opponentPanelElement.style.transform = 'scale(0.9) translateY(20px)'; // Конечный стиль для transition, если нужен
|
||||||
} else {
|
} else {
|
||||||
console.log(`[UI.JS DEBUG] NOT adding .dissolving (opponent key mismatch for dissolve effect: ${keyForDissolveEffect} or reason: ${reason}).`);
|
console.log(`[UI.JS DEBUG] NOT adding .dissolving (loser key mismatch: ${loserCharacterKeyForDissolve}).`);
|
||||||
// Если анимация не применяется, убеждаемся, что панель видна
|
// Если анимация не применяется, убеждаемся, что панель полностью видна
|
||||||
opponentPanelElement.style.opacity = '1';
|
opponentPanelElement.style.opacity = '1';
|
||||||
opponentPanelElement.style.transform = 'scale(1) translateY(0)';
|
opponentPanelElement.style.transform = 'scale(1) translateY(0)';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`[UI.JS DEBUG] NOT adding .dissolving. Conditions NOT met: isGameOver=${currentActualGameState?.isGameOver}, playerWon=${playerWon}, reason=${reason}.`);
|
console.log(`[UI.JS DEBUG] NOT adding .dissolving. Conditions NOT met: isGameOver=${currentActualGameState?.isGameOver}, playerWon=${playerWon}.`);
|
||||||
// Если игра не окончена или игрок проиграл/оппонент отключился, убеждаемся, что панель видна
|
// Если игра не окончена или игрок проиграл, убеждаемся, что панель полностью видна
|
||||||
opponentPanelElement.style.opacity = '1';
|
opponentPanelElement.style.opacity = '1';
|
||||||
opponentPanelElement.style.transform = 'scale(1) translateY(0)';
|
opponentPanelElement.style.transform = 'scale(1) translateY(0)';
|
||||||
}
|
}
|
||||||
// console.log(`[UI.JS DEBUG] Opponent panel classList FINAL in showGameOver (before timeout): ${opponentPanelElement.className}`);
|
opponentPanelElement.style.transition = ''; // Восстанавливаем transition после установки начальных/конечных стилей
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Показываем модальное окно конца игры с небольшой задержкой
|
// Показываем модальное окно конца игры с небольшой задержкой
|
||||||
// ИСПРАВЛЕНО: Передаем аргументы в колбэк, чтобы не полагаться на глобальный gameState
|
// Передаем аргументы в колбэк, чтобы не полагаться на глобальный gameState в момент срабатывания setTimeout
|
||||||
setTimeout((finalState, won, gameOverReason, opponentKeyForModalParam, eventData) => { // Названия аргументов, чтобы избежать путаницы
|
setTimeout((finalStateInTimeout, wonInTimeout, reasonInTimeout, keyInTimeout, dataInTimeout) => { // Use distinct names in timeout
|
||||||
console.log("[UI.JS DEBUG] Timeout callback fired.");
|
console.log("[UI.JS DEBUG] Timeout callback fired for showGameOver.");
|
||||||
console.log("[UI.JS DEBUG] State in timeout:", finalState);
|
console.log("[UI.JS DEBUG] State object received in timeout:", finalStateInTimeout); // Check the whole object
|
||||||
console.log("[UI.JS DEBUG] isGameOver in state:", finalState?.isGameOver);
|
console.log("[UI.JS DEBUG] isGameOver in state (TIMEOUT):", finalStateInTimeout?.isGameOver); // Check property
|
||||||
|
console.log("[UI.JS DEBUG] playerWon flag (TIMEOUT):", wonInTimeout); // Check playerWon flag passed
|
||||||
|
|
||||||
// Перепроверяем состояние перед показом на случай быстрых обновлений
|
|
||||||
if (finalState && finalState.isGameOver === true) { // Используем переданный state
|
// Проверяем условия для показа модального окна: элемент существует И состояние игры помечено как оконченное
|
||||||
console.log(`[UI.JS DEBUG] Condition (finalState && finalState.isGameOver === true) IS TRUE. Attempting to show modal.`);
|
// ИСПРАВЛЕНО: Убрана проверка gameOverScreenElement.offsetParent !== null
|
||||||
|
if (gameOverScreenElement && finalStateInTimeout && finalStateInTimeout.isGameOver === true) {
|
||||||
|
console.log(`[UI.JS DEBUG] Modal SHOW condition met: gameOverScreenElement exists, finalState exists, isGameOver is true.`);
|
||||||
// Убеждаемся, что modal не имеет display: none перед запуском transition opacity
|
// Убеждаемся, что modal не имеет display: none перед запуском transition opacity
|
||||||
// display: none полностью убирает элемент из потока и не позволяет анимировать opacity
|
|
||||||
// Переводим display в flex, если он был hidden (display: none !important в CSS)
|
|
||||||
if (gameOverScreenElement.classList.contains(config.CSS_CLASS_HIDDEN || 'hidden')) {
|
if (gameOverScreenElement.classList.contains(config.CSS_CLASS_HIDDEN || 'hidden')) {
|
||||||
gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden');
|
gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden');
|
||||||
}
|
}
|
||||||
// Убеждаемся, что opacity 0 для начала анимации
|
// Применяем display: flex (или другой нужный) только один раз, если нужно
|
||||||
gameOverScreenElement.style.opacity = '0';
|
if(window.getComputedStyle(gameOverScreenElement).display === 'none') {
|
||||||
// Убеждаемся, что display корректен
|
|
||||||
gameOverScreenElement.style.display = 'flex'; // Или какой там display в CSS для .modal
|
gameOverScreenElement.style.display = 'flex'; // Или какой там display в CSS для .modal
|
||||||
|
}
|
||||||
|
gameOverScreenElement.style.opacity = '0'; // Start from hidden opacity
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// Применяем opacity 1 после display flex для анимации
|
console.log("[UI.JS DEBUG] RequestAnimationFrame callback fired, animating modal.");
|
||||||
|
// Animate to visible
|
||||||
gameOverScreenElement.style.opacity = '1';
|
gameOverScreenElement.style.opacity = '1';
|
||||||
// Запускаем анимацию контента модального окна (scale/translate)
|
|
||||||
if (uiElements.gameOver.modalContent) {
|
if (uiElements.gameOver.modalContent) {
|
||||||
|
uiElements.gameOver.modalContent.style.transition = 'transform 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.2), opacity 0.4s ease-out'; // Убеждаемся, что transition включен
|
||||||
uiElements.gameOver.modalContent.style.transform = 'scale(1) translateY(0)';
|
uiElements.gameOver.modalContent.style.transform = 'scale(1) translateY(0)';
|
||||||
uiElements.gameOver.modalContent.style.opacity = '1';
|
uiElements.gameOver.modalContent.style.opacity = '1';
|
||||||
|
// uiElements.gameOver.modalContent.style.transition = ''; // Можно и так, если не отключали ранее
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(`[UI.JS DEBUG] Condition (finalState && finalState.isGameOver === true) IS FALSE. Modal will NOT be shown.`);
|
console.log(`[UI.JS DEBUG] Modal SHOW condition NOT met.`);
|
||||||
// Убеждаемся, что модалка скрыта, если условия больше не выполняются
|
console.log(`[UI.JS DEBUG] Details: gameOverScreenElement=${!!gameOverScreenElement}, finalState=${!!finalStateInTimeout}, finalState?.isGameOver=${finalStateInTimeout?.isGameOver}. Hiding modal.`); // More details
|
||||||
|
// Убеждаемся, что модалка скрыта, если условия не выполняются
|
||||||
|
if (gameOverScreenElement) {
|
||||||
|
// Ensure transition is off when hiding instantly
|
||||||
|
gameOverScreenElement.style.transition = 'none';
|
||||||
|
if (uiElements.gameOver.modalContent) uiElements.gameOver.modalContent.style.transition = 'none';
|
||||||
|
|
||||||
gameOverScreenElement.classList.add(config.CSS_CLASS_HIDDEN || 'hidden');
|
gameOverScreenElement.classList.add(config.CSS_CLASS_HIDDEN || 'hidden');
|
||||||
gameOverScreenElement.style.opacity = '0'; // Убеждаемся, что opacity сброшен
|
gameOverScreenElement.style.opacity = '0';
|
||||||
|
if (uiElements.gameOver.modalContent) {
|
||||||
|
uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)';
|
||||||
|
uiElements.gameOver.modalContent.style.opacity = '0';
|
||||||
}
|
}
|
||||||
}, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState, playerWon, reason, opponentCharacterKeyFromClient, data); // ИСПРАВЛЕНО: Передаем аргументы
|
// Trigger reflow to ensure transition is off before hiding
|
||||||
|
gameOverScreenElement.offsetHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState, playerWon, reason, opponentCharacterKeyFromClient, data); // Pass captured state and other values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user