(
- src: string | Buffer
-): T;
-
-export interface DotenvConfigOptions {
- /**
- * Default: `path.resolve(process.cwd(), '.env')`
- *
- * Specify a custom path if your file containing environment variables is located elsewhere.
- * Can also be an array of strings, specifying multiple paths.
- *
- * example: `require('dotenv').config({ path: '/custom/path/to/.env' })`
- * example: `require('dotenv').config({ path: ['/path/to/first.env', '/path/to/second.env'] })`
- */
- path?: string | string[] | URL;
-
- /**
- * Default: `utf8`
- *
- * Specify the encoding of your file containing environment variables.
- *
- * example: `require('dotenv').config({ encoding: 'latin1' })`
- */
- encoding?: string;
-
- /**
- * Default: `false`
- *
- * Turn on logging to help debug why certain keys or values are not being set as you expect.
- *
- * example: `require('dotenv').config({ debug: process.env.DEBUG })`
- */
- debug?: boolean;
-
- /**
- * Default: `false`
- *
- * Override any environment variables that have already been set on your machine with values from your .env file.
- *
- * example: `require('dotenv').config({ override: true })`
- */
- override?: boolean;
-
- /**
- * Default: `process.env`
- *
- * Specify an object to write your secrets to. Defaults to process.env environment variables.
- *
- * example: `const processEnv = {}; require('dotenv').config({ processEnv: processEnv })`
- */
- processEnv?: DotenvPopulateInput;
-
- /**
- * Default: `undefined`
- *
- * Pass the DOTENV_KEY directly to config options. Defaults to looking for process.env.DOTENV_KEY environment variable. Note this only applies to decrypting .env.vault files. If passed as null or undefined, or not passed at all, dotenv falls back to its traditional job of parsing a .env file.
- *
- * example: `require('dotenv').config({ DOTENV_KEY: 'dotenv://:key_1234…@dotenvx.com/vault/.env.vault?environment=production' })`
- */
- DOTENV_KEY?: string;
-}
-
-export interface DotenvConfigOutput {
- error?: Error;
- parsed?: DotenvParseOutput;
-}
-
-export interface DotenvPopulateOptions {
- /**
- * Default: `false`
- *
- * Turn on logging to help debug why certain keys or values are not being set as you expect.
- *
- * example: `require('dotenv').config({ debug: process.env.DEBUG })`
- */
- debug?: boolean;
-
- /**
- * Default: `false`
- *
- * Override any environment variables that have already been set on your machine with values from your .env file.
- *
- * example: `require('dotenv').config({ override: true })`
- */
- override?: boolean;
-}
-
-export interface DotenvPopulateInput {
- [name: string]: string;
-}
-
-/**
- * Loads `.env` file contents into process.env by default. If `DOTENV_KEY` is present, it smartly attempts to load encrypted `.env.vault` file contents into process.env.
- *
- * See https://dotenvx.com/docs
- *
- * @param options - additional options. example: `{ path: './custom/path', encoding: 'latin1', debug: true, override: false }`
- * @returns an object with a `parsed` key if successful or `error` key if an error occurred. example: { parsed: { KEY: 'value' } }
- *
- */
-export function config(options?: DotenvConfigOptions): DotenvConfigOutput;
-
-/**
- * Loads `.env` file contents into process.env.
- *
- * See https://dotenvx.com/docs
- *
- * @param options - additional options. example: `{ path: './custom/path', encoding: 'latin1', debug: true, override: false }`
- * @returns an object with a `parsed` key if successful or `error` key if an error occurred. example: { parsed: { KEY: 'value' } }
- *
- */
-export function configDotenv(options?: DotenvConfigOptions): DotenvConfigOutput;
-
-/**
- * Loads `source` json contents into `target` like process.env.
- *
- * See https://dotenvx.com/docs
- *
- * @param processEnv - the target JSON object. in most cases use process.env but you can also pass your own JSON object
- * @param parsed - the source JSON object
- * @param options - additional options. example: `{ debug: true, override: false }`
- * @returns {void}
- *
- */
-export function populate(processEnv: DotenvPopulateInput, parsed: DotenvPopulateInput, options?: DotenvConfigOptions): void;
-
-/**
- * Decrypt ciphertext
- *
- * See https://dotenvx.com/docs
- *
- * @param encrypted - the encrypted ciphertext string
- * @param keyStr - the decryption key string
- * @returns {string}
- *
- */
-export function decrypt(encrypted: string, keyStr: string): string;
diff --git a/node_modules/dotenv/lib/main.js b/node_modules/dotenv/lib/main.js
deleted file mode 100644
index 8788c8e..0000000
--- a/node_modules/dotenv/lib/main.js
+++ /dev/null
@@ -1,360 +0,0 @@
-const fs = require('fs')
-const path = require('path')
-const os = require('os')
-const crypto = require('crypto')
-const packageJson = require('../package.json')
-
-const version = packageJson.version
-
-const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
-
-// Parse src into an Object
-function parse (src) {
- const obj = {}
-
- // Convert buffer to string
- let lines = src.toString()
-
- // Convert line breaks to same format
- lines = lines.replace(/\r\n?/mg, '\n')
-
- let match
- while ((match = LINE.exec(lines)) != null) {
- const key = match[1]
-
- // Default undefined or null to empty string
- let value = (match[2] || '')
-
- // Remove whitespace
- value = value.trim()
-
- // Check if double quoted
- const maybeQuote = value[0]
-
- // Remove surrounding quotes
- value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
-
- // Expand newlines if double quoted
- if (maybeQuote === '"') {
- value = value.replace(/\\n/g, '\n')
- value = value.replace(/\\r/g, '\r')
- }
-
- // Add to object
- obj[key] = value
- }
-
- return obj
-}
-
-function _parseVault (options) {
- const vaultPath = _vaultPath(options)
-
- // Parse .env.vault
- const result = DotenvModule.configDotenv({ path: vaultPath })
- if (!result.parsed) {
- const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`)
- err.code = 'MISSING_DATA'
- throw err
- }
-
- // handle scenario for comma separated keys - for use with key rotation
- // example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod"
- const keys = _dotenvKey(options).split(',')
- const length = keys.length
-
- let decrypted
- for (let i = 0; i < length; i++) {
- try {
- // Get full key
- const key = keys[i].trim()
-
- // Get instructions for decrypt
- const attrs = _instructions(result, key)
-
- // Decrypt
- decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key)
-
- break
- } catch (error) {
- // last key
- if (i + 1 >= length) {
- throw error
- }
- // try next key
- }
- }
-
- // Parse decrypted .env string
- return DotenvModule.parse(decrypted)
-}
-
-function _warn (message) {
- console.log(`[dotenv@${version}][WARN] ${message}`)
-}
-
-function _debug (message) {
- console.log(`[dotenv@${version}][DEBUG] ${message}`)
-}
-
-function _dotenvKey (options) {
- // prioritize developer directly setting options.DOTENV_KEY
- if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
- return options.DOTENV_KEY
- }
-
- // secondary infra already contains a DOTENV_KEY environment variable
- if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
- return process.env.DOTENV_KEY
- }
-
- // fallback to empty string
- return ''
-}
-
-function _instructions (result, dotenvKey) {
- // Parse DOTENV_KEY. Format is a URI
- let uri
- try {
- uri = new URL(dotenvKey)
- } catch (error) {
- if (error.code === 'ERR_INVALID_URL') {
- const err = new Error('INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development')
- err.code = 'INVALID_DOTENV_KEY'
- throw err
- }
-
- throw error
- }
-
- // Get decrypt key
- const key = uri.password
- if (!key) {
- const err = new Error('INVALID_DOTENV_KEY: Missing key part')
- err.code = 'INVALID_DOTENV_KEY'
- throw err
- }
-
- // Get environment
- const environment = uri.searchParams.get('environment')
- if (!environment) {
- const err = new Error('INVALID_DOTENV_KEY: Missing environment part')
- err.code = 'INVALID_DOTENV_KEY'
- throw err
- }
-
- // Get ciphertext payload
- const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
- const ciphertext = result.parsed[environmentKey] // DOTENV_VAULT_PRODUCTION
- if (!ciphertext) {
- const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`)
- err.code = 'NOT_FOUND_DOTENV_ENVIRONMENT'
- throw err
- }
-
- return { ciphertext, key }
-}
-
-function _vaultPath (options) {
- let possibleVaultPath = null
-
- if (options && options.path && options.path.length > 0) {
- if (Array.isArray(options.path)) {
- for (const filepath of options.path) {
- if (fs.existsSync(filepath)) {
- possibleVaultPath = filepath.endsWith('.vault') ? filepath : `${filepath}.vault`
- }
- }
- } else {
- possibleVaultPath = options.path.endsWith('.vault') ? options.path : `${options.path}.vault`
- }
- } else {
- possibleVaultPath = path.resolve(process.cwd(), '.env.vault')
- }
-
- if (fs.existsSync(possibleVaultPath)) {
- return possibleVaultPath
- }
-
- return null
-}
-
-function _resolveHome (envPath) {
- return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
-}
-
-function _configVault (options) {
- const debug = Boolean(options && options.debug)
- if (debug) {
- _debug('Loading env from encrypted .env.vault')
- }
-
- const parsed = DotenvModule._parseVault(options)
-
- let processEnv = process.env
- if (options && options.processEnv != null) {
- processEnv = options.processEnv
- }
-
- DotenvModule.populate(processEnv, parsed, options)
-
- return { parsed }
-}
-
-function configDotenv (options) {
- const dotenvPath = path.resolve(process.cwd(), '.env')
- let encoding = 'utf8'
- const debug = Boolean(options && options.debug)
-
- if (options && options.encoding) {
- encoding = options.encoding
- } else {
- if (debug) {
- _debug('No encoding is specified. UTF-8 is used by default')
- }
- }
-
- let optionPaths = [dotenvPath] // default, look for .env
- if (options && options.path) {
- if (!Array.isArray(options.path)) {
- optionPaths = [_resolveHome(options.path)]
- } else {
- optionPaths = [] // reset default
- for (const filepath of options.path) {
- optionPaths.push(_resolveHome(filepath))
- }
- }
- }
-
- // Build the parsed data in a temporary object (because we need to return it). Once we have the final
- // parsed data, we will combine it with process.env (or options.processEnv if provided).
- let lastError
- const parsedAll = {}
- for (const path of optionPaths) {
- try {
- // Specifying an encoding returns a string instead of a buffer
- const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding }))
-
- DotenvModule.populate(parsedAll, parsed, options)
- } catch (e) {
- if (debug) {
- _debug(`Failed to load ${path} ${e.message}`)
- }
- lastError = e
- }
- }
-
- let processEnv = process.env
- if (options && options.processEnv != null) {
- processEnv = options.processEnv
- }
-
- DotenvModule.populate(processEnv, parsedAll, options)
-
- if (lastError) {
- return { parsed: parsedAll, error: lastError }
- } else {
- return { parsed: parsedAll }
- }
-}
-
-// Populates process.env from .env file
-function config (options) {
- // fallback to original dotenv if DOTENV_KEY is not set
- if (_dotenvKey(options).length === 0) {
- return DotenvModule.configDotenv(options)
- }
-
- const vaultPath = _vaultPath(options)
-
- // dotenvKey exists but .env.vault file does not exist
- if (!vaultPath) {
- _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`)
-
- return DotenvModule.configDotenv(options)
- }
-
- return DotenvModule._configVault(options)
-}
-
-function decrypt (encrypted, keyStr) {
- const key = Buffer.from(keyStr.slice(-64), 'hex')
- let ciphertext = Buffer.from(encrypted, 'base64')
-
- const nonce = ciphertext.subarray(0, 12)
- const authTag = ciphertext.subarray(-16)
- ciphertext = ciphertext.subarray(12, -16)
-
- try {
- const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce)
- aesgcm.setAuthTag(authTag)
- return `${aesgcm.update(ciphertext)}${aesgcm.final()}`
- } catch (error) {
- const isRange = error instanceof RangeError
- const invalidKeyLength = error.message === 'Invalid key length'
- const decryptionFailed = error.message === 'Unsupported state or unable to authenticate data'
-
- if (isRange || invalidKeyLength) {
- const err = new Error('INVALID_DOTENV_KEY: It must be 64 characters long (or more)')
- err.code = 'INVALID_DOTENV_KEY'
- throw err
- } else if (decryptionFailed) {
- const err = new Error('DECRYPTION_FAILED: Please check your DOTENV_KEY')
- err.code = 'DECRYPTION_FAILED'
- throw err
- } else {
- throw error
- }
- }
-}
-
-// Populate process.env with parsed values
-function populate (processEnv, parsed, options = {}) {
- const debug = Boolean(options && options.debug)
- const override = Boolean(options && options.override)
-
- if (typeof parsed !== 'object') {
- const err = new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate')
- err.code = 'OBJECT_REQUIRED'
- throw err
- }
-
- // Set process.env
- for (const key of Object.keys(parsed)) {
- if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
- if (override === true) {
- processEnv[key] = parsed[key]
- }
-
- if (debug) {
- if (override === true) {
- _debug(`"${key}" is already defined and WAS overwritten`)
- } else {
- _debug(`"${key}" is already defined and was NOT overwritten`)
- }
- }
- } else {
- processEnv[key] = parsed[key]
- }
- }
-}
-
-const DotenvModule = {
- configDotenv,
- _configVault,
- _parseVault,
- config,
- decrypt,
- parse,
- populate
-}
-
-module.exports.configDotenv = DotenvModule.configDotenv
-module.exports._configVault = DotenvModule._configVault
-module.exports._parseVault = DotenvModule._parseVault
-module.exports.config = DotenvModule.config
-module.exports.decrypt = DotenvModule.decrypt
-module.exports.parse = DotenvModule.parse
-module.exports.populate = DotenvModule.populate
-
-module.exports = DotenvModule
diff --git a/node_modules/dotenv/package.json b/node_modules/dotenv/package.json
deleted file mode 100644
index 157c1e3..0000000
--- a/node_modules/dotenv/package.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
- "name": "dotenv",
- "version": "16.5.0",
- "description": "Loads environment variables from .env file",
- "main": "lib/main.js",
- "types": "lib/main.d.ts",
- "exports": {
- ".": {
- "types": "./lib/main.d.ts",
- "require": "./lib/main.js",
- "default": "./lib/main.js"
- },
- "./config": "./config.js",
- "./config.js": "./config.js",
- "./lib/env-options": "./lib/env-options.js",
- "./lib/env-options.js": "./lib/env-options.js",
- "./lib/cli-options": "./lib/cli-options.js",
- "./lib/cli-options.js": "./lib/cli-options.js",
- "./package.json": "./package.json"
- },
- "scripts": {
- "dts-check": "tsc --project tests/types/tsconfig.json",
- "lint": "standard",
- "pretest": "npm run lint && npm run dts-check",
- "test": "tap run --allow-empty-coverage --disable-coverage --timeout=60000",
- "test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=lcov",
- "prerelease": "npm test",
- "release": "standard-version"
- },
- "repository": {
- "type": "git",
- "url": "git://github.com/motdotla/dotenv.git"
- },
- "homepage": "https://github.com/motdotla/dotenv#readme",
- "funding": "https://dotenvx.com",
- "keywords": [
- "dotenv",
- "env",
- ".env",
- "environment",
- "variables",
- "config",
- "settings"
- ],
- "readmeFilename": "README.md",
- "license": "BSD-2-Clause",
- "devDependencies": {
- "@types/node": "^18.11.3",
- "decache": "^4.6.2",
- "sinon": "^14.0.1",
- "standard": "^17.0.0",
- "standard-version": "^9.5.0",
- "tap": "^19.2.0",
- "typescript": "^4.8.4"
- },
- "engines": {
- "node": ">=12"
- },
- "browser": {
- "fs": false
- }
-}
diff --git a/node_modules/uuid/dist/esm/bin/uuid b/node_modules/uuid/dist/esm/bin/uuid
old mode 100644
new mode 100755
diff --git a/package-lock.json b/package-lock.json
index b0e68f7..5ddf018 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,11 @@
{
- "name": "battle_club_git",
+ "name": "bc",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"bcryptjs": "^3.0.2",
- "dotenv": "^16.5.0",
"express": "^5.1.0",
"mysql2": "^3.14.1",
"socket.io": "^4.8.1",
@@ -222,17 +221,6 @@
"node": ">= 0.8"
}
},
- "node_modules/dotenv": {
- "version": "16.5.0",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
- "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://dotenvx.com"
- }
- },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
diff --git a/package.json b/package.json
index 1e86957..492a907 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,6 @@
{
"dependencies": {
"bcryptjs": "^3.0.2",
- "dotenv": "^16.5.0",
"express": "^5.1.0",
"mysql2": "^3.14.1",
"socket.io": "^4.8.1",
diff --git a/public/index.html b/public/index.html
index 6ab467d..eea41b0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,239 +1,236 @@
-
-
-
-
-
- Битва: Елена vs Балард (Сетевая Версия)
-
-
-
-
-
-
-
-
-
-
-
-
-
Ожидание подключения к серверу...
-
-
-
-
Вход / Регистрация
-
-
-
-
-
-
-
-
-
Настройка Игры
-
- Играть против AI (Балард)
-
-
-
-
PvP (Игрок против Игрока)
-
-
-
Выберите персонажа для PvP:
-
- Елена
-
-
- Альмагест
-
-
-
Создать PvP Игру
-
Найти случайную PvP Игру
-
-
-
Присоединиться к PvP по ID
-
-
-
Доступные PvP игры:
-
Загрузка списка...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Игрок
-
-
- Противник
-
-
-
-
-
-
-
-
-
-
-
-
-
- Статус: Готов(а)
-
-
-
-
- Усиления:
- Нет
-
-
-
- Ослабления:
- Нет
-
-
-
-
-
-
- Ход: Игрок 1
-
- Время на ход: --
-
-
-
- Атака
- Защита
-
-
-
Способности
-
-
Загрузка способностей...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Статус: Готов(а)
-
-
-
-
- Усиления:
- Нет
-
-
-
- Ослабления:
- Нет
-
-
-
-
-
-
- Лог Боя
-
- Ожидание подключения к серверу...
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ Битва: Елена vs Балард (Сетевая Версия)
+
+
+
+
+
+
+
+
+
+
+
+
Ожидание подключения к серверу...
+
+
+
+
Вход / Регистрация
+
+
+
+
+
+
+
+
+
Настройка Игры
+
+ Играть против AI (Балард)
+
+
+
+
PvP (Игрок против Игрока)
+
+
+
+
Выберите персонажа для PvP:
+
+ Елена
+
+
+ Альмагест
+
+
+
+
Создать PvP Игру
+
Найти случайную PvP Игру
+
+
+
Присоединиться к PvP по ID
+
+
+
Доступные PvP игры:
+
Загрузка списка...
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Игрок
+
+
+ Противник
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Статус: Готов(а)
+
+
+
+
+ Усиления:
+ Нет
+
+
+
+ Ослабления:
+ Нет
+
+
+
+
+
+
+ Ход: Игрок 1
+
+ Время на ход: --
+
+
+
+ Атака
+ Защита
+
+
+
Способности
+
+
Загрузка способностей...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Статус: Готов(а)
+
+
+
+
+ Усиления:
+ Нет
+
+
+
+ Ослабления:
+ Нет
+
+
+
+
+
+
+ Лог Боя
+
+ Ожидание подключения к серверу...
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/js/client.js b/public/js/client.js
index 2b8afef..9f85014 100644
--- a/public/js/client.js
+++ b/public/js/client.js
@@ -1,583 +1,583 @@
-// /public/js/client.js
-
-document.addEventListener('DOMContentLoaded', () => {
- const socket = io({
- // Опции Socket.IO, если нужны
- // transports: ['websocket'], // Можно попробовать для отладки, если есть проблемы с polling
- });
-
- // --- Состояние клиента ---
- let currentGameState = null;
- let myPlayerId = null; // Технический ID слота в игре ('player' или 'opponent')
- let myUserId = null; // ID залогиненного пользователя (из БД)
- let myCharacterKey = null;
- let opponentCharacterKey = null;
- let currentGameId = null;
- let playerBaseStatsServer = null;
- let opponentBaseStatsServer = null;
- let playerAbilitiesServer = null;
- let opponentAbilitiesServer = null;
- let isLoggedIn = false;
- let loggedInUsername = '';
- let isInGame = false;
-
- // --- DOM Элементы ---
- const authSection = document.getElementById('auth-section');
- const registerForm = document.getElementById('register-form');
- const loginForm = document.getElementById('login-form');
- const authMessage = document.getElementById('auth-message');
- const statusContainer = document.getElementById('status-container');
- const userInfoDiv = document.getElementById('user-info');
- const loggedInUsernameSpan = document.getElementById('logged-in-username');
- const logoutButton = document.getElementById('logout-button');
-
- const gameSetupDiv = document.getElementById('game-setup');
- const createAIGameButton = document.getElementById('create-ai-game');
- const createPvPGameButton = document.getElementById('create-pvp-game');
- const joinPvPGameButton = document.getElementById('join-pvp-game'); // Убедитесь, что ID в HTML 'join-pvp-game'
- const findRandomPvPGameButton = document.getElementById('find-random-pvp-game');
- const gameIdInput = document.getElementById('game-id-input');
- const availableGamesDiv = document.getElementById('available-games-list');
- const gameStatusMessage = document.getElementById('game-status-message');
- const pvpCharacterRadios = document.querySelectorAll('input[name="pvp-character"]');
-
- const gameWrapper = document.querySelector('.game-wrapper');
- const attackButton = document.getElementById('button-attack');
- const returnToMenuButton = document.getElementById('return-to-menu-button');
- const gameOverScreen = document.getElementById('game-over-screen');
- const abilitiesGrid = document.getElementById('abilities-grid');
-
- const turnTimerSpan = document.getElementById('turn-timer');
- const turnTimerContainer = document.getElementById('turn-timer-container');
-
- // --- Функции управления UI ---
- function showAuthScreen() {
- authSection.style.display = 'block';
- userInfoDiv.style.display = 'none';
- gameSetupDiv.style.display = 'none';
- gameWrapper.style.display = 'none';
- hideGameOverModal();
- setAuthMessage("Ожидание подключения к серверу...");
- statusContainer.style.display = 'block';
- isInGame = false;
- disableGameControls();
- resetGameVariables();
- if (turnTimerContainer) turnTimerContainer.style.display = 'none';
- if (turnTimerSpan) turnTimerSpan.textContent = '--';
- }
-
- function showGameSelectionScreen(username) {
- authSection.style.display = 'none';
- userInfoDiv.style.display = 'block';
- loggedInUsernameSpan.textContent = username;
- gameSetupDiv.style.display = 'block';
- gameWrapper.style.display = 'none';
- hideGameOverModal();
- setGameStatusMessage("Выберите режим игры или присоединитесь к существующей.");
- statusContainer.style.display = 'block';
- socket.emit('requestPvPGameList');
- updateAvailableGamesList([]); // Очищаем перед запросом
- if (gameIdInput) gameIdInput.value = '';
- const elenaRadio = document.getElementById('char-elena');
- if (elenaRadio) elenaRadio.checked = true;
- isInGame = false;
- disableGameControls();
- resetGameVariables(); // Сбрасываем игровые переменные при выходе в меню
- if (turnTimerContainer) turnTimerContainer.style.display = 'none';
- if (turnTimerSpan) turnTimerSpan.textContent = '--';
- enableSetupButtons(); // Включаем кнопки на экране выбора игры
- }
-
- function showGameScreen() {
- hideGameOverModal();
- authSection.style.display = 'none';
- userInfoDiv.style.display = 'block'; // Оставляем инфо о пользователе
- gameSetupDiv.style.display = 'none';
- gameWrapper.style.display = 'flex';
- setGameStatusMessage(""); // Очищаем статус, т.к. есть индикатор хода
- statusContainer.style.display = 'none'; // Скрываем общий статус контейнер
- isInGame = true;
- disableGameControls(); // Кнопки включатся, когда будет ход игрока
- if (turnTimerContainer) turnTimerContainer.style.display = 'block'; // Показываем таймер
- if (turnTimerSpan) turnTimerSpan.textContent = '--'; // Начальное значение
- }
-
- function resetGameVariables() {
- currentGameId = null; currentGameState = null; myPlayerId = null;
- myCharacterKey = null; opponentCharacterKey = null;
- playerBaseStatsServer = null; opponentBaseStatsServer = null;
- playerAbilitiesServer = null; opponentAbilitiesServer = null;
- window.gameState = null; window.gameData = null; window.myPlayerId = null;
- }
-
- function hideGameOverModal() {
- const hiddenClass = window.GAME_CONFIG?.CSS_CLASS_HIDDEN || 'hidden';
- if (gameOverScreen && !gameOverScreen.classList.contains(hiddenClass)) {
- gameOverScreen.classList.add(hiddenClass);
- if (window.gameUI?.uiElements?.gameOver?.modalContent) {
- window.gameUI.uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)';
- window.gameUI.uiElements.gameOver.modalContent.style.opacity = '0';
- }
- const opponentPanel = window.gameUI?.uiElements?.opponent?.panel;
- if (opponentPanel?.classList.contains('dissolving')) {
- opponentPanel.classList.remove('dissolving');
- opponentPanel.style.opacity = '1'; opponentPanel.style.transform = 'scale(1) translateY(0)';
- }
- }
- }
-
- function setAuthMessage(message, isError = false) {
- if (authMessage) {
- authMessage.textContent = message;
- authMessage.className = isError ? 'error' : 'success';
- authMessage.style.display = message ? 'block' : 'none';
- }
- if (message && gameStatusMessage) gameStatusMessage.style.display = 'none';
- }
-
- function setGameStatusMessage(message, isError = false) {
- if (gameStatusMessage) {
- gameStatusMessage.textContent = message;
- gameStatusMessage.style.display = message ? 'block' : 'none';
- gameStatusMessage.style.color = isError ? 'var(--damage-color, red)' : 'var(--turn-color, yellow)';
- if (statusContainer) statusContainer.style.display = message ? 'block' : 'none';
- }
- if (message && authMessage) authMessage.style.display = 'none';
- }
-
- function getSelectedCharacterKey() {
- let selectedKey = 'elena';
- if (pvpCharacterRadios) {
- pvpCharacterRadios.forEach(radio => { if (radio.checked) selectedKey = radio.value; });
- }
- return selectedKey;
- }
-
- function enableGameControls(enableAttack = true, enableAbilities = true) {
- if (attackButton) attackButton.disabled = !enableAttack;
- if (abilitiesGrid) {
- const cls = window.GAME_CONFIG?.CSS_CLASS_ABILITY_BUTTON || 'ability-button';
- abilitiesGrid.querySelectorAll(`.${cls}`).forEach(b => { b.disabled = !enableAbilities; });
- }
- if (window.gameUI?.uiElements?.controls?.buttonBlock) window.gameUI.uiElements.controls.buttonBlock.disabled = true;
- }
- function disableGameControls() { enableGameControls(false, false); }
-
- function disableSetupButtons() {
- if(createAIGameButton) createAIGameButton.disabled = true;
- if(createPvPGameButton) createPvPGameButton.disabled = true;
- if(joinPvPGameButton) joinPvPGameButton.disabled = true;
- if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = true;
- if(availableGamesDiv) availableGamesDiv.querySelectorAll('button').forEach(btn => btn.disabled = true);
- }
- function enableSetupButtons() {
- if(createAIGameButton) createAIGameButton.disabled = false;
- if(createPvPGameButton) createPvPGameButton.disabled = false;
- if(joinPvPGameButton) joinPvPGameButton.disabled = false;
- if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = false;
- // Кнопки в списке игр включаются в updateAvailableGamesList
- }
-
- // --- Инициализация обработчиков событий ---
- if (registerForm) registerForm.addEventListener('submit', (e) => {
- e.preventDefault();
- const u = document.getElementById('register-username').value;
- const p = document.getElementById('register-password').value;
- registerForm.querySelector('button').disabled = true;
- if(loginForm) loginForm.querySelector('button').disabled = true;
- socket.emit('register', { username: u, password: p });
- });
- if (loginForm) loginForm.addEventListener('submit', (e) => {
- e.preventDefault();
- const u = document.getElementById('login-username').value;
- const p = document.getElementById('login-password').value;
- if(registerForm) registerForm.querySelector('button').disabled = true;
- loginForm.querySelector('button').disabled = true;
- socket.emit('login', { username: u, password: p });
- });
- if (logoutButton) logoutButton.addEventListener('click', () => {
- logoutButton.disabled = true; socket.emit('logout');
- isLoggedIn = false; loggedInUsername = ''; myUserId = null;
- resetGameVariables(); isInGame = false; disableGameControls();
- showAuthScreen(); setGameStatusMessage("Вы вышли из системы.");
- logoutButton.disabled = false;
- });
- if (createAIGameButton) createAIGameButton.addEventListener('click', () => {
- if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; }
- disableSetupButtons();
- socket.emit('createGame', { mode: 'ai', characterKey: 'elena' }); // AI всегда за Елену
- setGameStatusMessage("Создание игры против AI...");
- });
- if (createPvPGameButton) createPvPGameButton.addEventListener('click', () => {
- if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; }
- disableSetupButtons();
- socket.emit('createGame', { mode: 'pvp', characterKey: getSelectedCharacterKey() });
- setGameStatusMessage("Создание PvP игры...");
- });
- if (joinPvPGameButton) joinPvPGameButton.addEventListener('click', () => { // Убедитесь, что ID кнопки 'join-pvp-game'
- if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; }
- const gameId = gameIdInput.value.trim();
- if (gameId) {
- disableSetupButtons();
- socket.emit('joinGame', { gameId: gameId });
- setGameStatusMessage(`Присоединение к игре ${gameId}...`);
- } else setGameStatusMessage("Введите ID игры.", true);
- });
- if (findRandomPvPGameButton) findRandomPvPGameButton.addEventListener('click', () => {
- if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; }
- disableSetupButtons();
- socket.emit('findRandomGame', { characterKey: getSelectedCharacterKey() });
- setGameStatusMessage("Поиск случайной PvP игры...");
- });
- if (attackButton) attackButton.addEventListener('click', () => {
- if (isLoggedIn && isInGame && currentGameId && currentGameState && !currentGameState.isGameOver) {
- socket.emit('playerAction', { actionType: 'attack' });
- } else { /* обработка ошибки/некорректного состояния */ }
- });
- function handleAbilityButtonClick(event) {
- const abilityId = event.currentTarget.dataset.abilityId;
- if (isLoggedIn && isInGame && currentGameId && abilityId && currentGameState && !currentGameState.isGameOver) {
- socket.emit('playerAction', { actionType: 'ability', abilityId: abilityId });
- } else { /* обработка ошибки/некорректного состояния */ }
- }
- if (returnToMenuButton) returnToMenuButton.addEventListener('click', () => {
- if (!isLoggedIn) { showAuthScreen(); return; }
- returnToMenuButton.disabled = true;
- resetGameVariables(); isInGame = false; disableGameControls(); hideGameOverModal();
- showGameSelectionScreen(loggedInUsername); // Возвращаемся на экран выбора
- // Кнопка включится при следующем показе модалки
- });
-
- function initializeAbilityButtons() {
- // ... (код без изменений, как был)
- if (!abilitiesGrid || !window.gameUI || !window.GAME_CONFIG) {
- if (abilitiesGrid) abilitiesGrid.innerHTML = 'Ошибка загрузки способностей.
';
- return;
- }
- abilitiesGrid.innerHTML = '';
- const config = window.GAME_CONFIG;
- const abilitiesToDisplay = playerAbilitiesServer;
- const baseStatsForResource = playerBaseStatsServer;
-
- if (!abilitiesToDisplay || abilitiesToDisplay.length === 0 || !baseStatsForResource) {
- abilitiesGrid.innerHTML = 'Нет доступных способностей.
';
- return;
- }
- const resourceName = baseStatsForResource.resourceName || "Ресурс";
- const abilityButtonClass = config.CSS_CLASS_ABILITY_BUTTON || 'ability-button';
-
- abilitiesToDisplay.forEach(ability => {
- const button = document.createElement('button');
- button.id = `ability-btn-${ability.id}`;
- button.classList.add(abilityButtonClass);
- button.dataset.abilityId = ability.id;
- let cooldown = ability.cooldown;
- let cooldownText = (typeof cooldown === 'number' && cooldown > 0) ? ` (КД: ${cooldown} х.)` : "";
- let title = `${ability.name} (${ability.cost} ${resourceName})${cooldownText} - ${ability.description || 'Нет описания'}`;
- button.setAttribute('title', title);
- const nameSpan = document.createElement('span'); nameSpan.classList.add('ability-name'); nameSpan.textContent = ability.name; button.appendChild(nameSpan);
- const descSpan = document.createElement('span'); descSpan.classList.add('ability-desc'); descSpan.textContent = `(${ability.cost} ${resourceName})`; button.appendChild(descSpan);
- const cdDisplay = document.createElement('span'); cdDisplay.classList.add('ability-cooldown-display'); cdDisplay.style.display = 'none'; button.appendChild(cdDisplay);
- button.addEventListener('click', handleAbilityButtonClick);
- abilitiesGrid.appendChild(button);
- });
- const placeholder = abilitiesGrid.querySelector('.placeholder-text');
- if (placeholder) placeholder.remove();
- }
-
- function updateAvailableGamesList(games) {
- if (!availableGamesDiv) return;
- availableGamesDiv.innerHTML = 'Доступные PvP игры: ';
- if (games && games.length > 0) {
- const ul = document.createElement('ul');
- games.forEach(game => {
- if (game && game.id) {
- const li = document.createElement('li');
- li.textContent = `ID: ${game.id.substring(0, 8)}... - ${game.status || 'Ожидает игрока'}`;
- const joinBtn = document.createElement('button');
- joinBtn.textContent = 'Присоединиться';
- joinBtn.dataset.gameId = game.id;
-
- // === ИЗМЕНЕНИЕ: Деактивация кнопки "Присоединиться" для своих игр ===
- if (isLoggedIn && myUserId && game.ownerIdentifier === myUserId) {
- joinBtn.disabled = true;
- joinBtn.title = "Вы не можете присоединиться к своей же ожидающей игре.";
- } else {
- joinBtn.disabled = false;
- }
- // === КОНЕЦ ИЗМЕНЕНИЯ ===
-
- joinBtn.addEventListener('click', (e) => {
- if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; }
- if (e.target.disabled) return; // Не обрабатывать клик по отключенной кнопке
- disableSetupButtons();
- socket.emit('joinGame', { gameId: e.target.dataset.gameId });
- });
- li.appendChild(joinBtn);
- ul.appendChild(li);
- }
- });
- availableGamesDiv.appendChild(ul);
- } else {
- availableGamesDiv.innerHTML += 'Нет доступных игр. Создайте свою!
';
- }
- enableSetupButtons(); // Включаем основные кнопки создания/поиска
- }
-
-
- // --- Обработчики событий Socket.IO ---
- socket.on('connect', () => {
- console.log('[Client] Socket connected:', socket.id);
- if (isLoggedIn && myUserId) { // Проверяем и isLoggedIn и myUserId
- socket.emit('requestGameState'); // Запрашиваем состояние, если были залогинены
- } else {
- showAuthScreen(); // Иначе показываем экран логина
- }
- });
-
- socket.on('registerResponse', (data) => {
- setAuthMessage(data.message, !data.success);
- if (data.success && registerForm) registerForm.reset();
- if(registerForm) registerForm.querySelector('button').disabled = false;
- if(loginForm) loginForm.querySelector('button').disabled = false;
- });
-
- socket.on('loginResponse', (data) => {
- setAuthMessage(data.message, !data.success);
- if (data.success) {
- isLoggedIn = true;
- loggedInUsername = data.username;
- myUserId = data.userId; // === ИЗМЕНЕНИЕ: Сохраняем ID пользователя ===
- setAuthMessage("");
- showGameSelectionScreen(data.username);
- } else {
- isLoggedIn = false; loggedInUsername = ''; myUserId = null;
- if(registerForm) registerForm.querySelector('button').disabled = false;
- if(loginForm) loginForm.querySelector('button').disabled = false;
- }
- });
-
- socket.on('gameNotFound', (data) => {
- console.log('[Client] Game not found/ended:', data?.message);
- resetGameVariables(); isInGame = false; disableGameControls(); hideGameOverModal();
- if (turnTimerContainer) turnTimerContainer.style.display = 'none';
- if (turnTimerSpan) turnTimerSpan.textContent = '--';
-
- if (isLoggedIn) {
- showGameSelectionScreen(loggedInUsername);
- setGameStatusMessage(data?.message || "Активная игровая сессия не найдена.");
- } else {
- showAuthScreen();
- setAuthMessage(data?.message || "Пожалуйста, войдите.");
- }
- });
-
- socket.on('disconnect', (reason) => {
- console.log('[Client] Disconnected:', reason);
- setGameStatusMessage(`Отключено: ${reason}. Обновите страницу.`, true);
- disableGameControls();
- if (turnTimerSpan) turnTimerSpan.textContent = 'Откл.';
- // Не сбрасываем isLoggedIn, чтобы при переподключении можно было восстановить сессию
- });
-
- socket.on('gameCreated', (data) => { // Сервер присылает это после успешного createGame
- console.log('[Client] Game created by this client:', data);
- currentGameId = data.gameId;
- myPlayerId = data.yourPlayerId; // Сервер должен прислать роль создателя
- // Остальные данные (gameState, baseStats) придут с gameStarted или gameState (если это PvP ожидание)
- // Если это PvP и игра ожидает, сервер может прислать waitingForOpponent
- });
-
-
- socket.on('gameStarted', (data) => {
- if (!isLoggedIn) return;
- console.log('[Client] Game started:', data);
- // ... (остальной код gameStarted без изменений, как был)
- if (window.gameUI?.uiElements?.opponent?.panel) {
- const opponentPanel = window.gameUI.uiElements.opponent.panel;
- if (opponentPanel.classList.contains('dissolving')) {
- opponentPanel.classList.remove('dissolving');
- opponentPanel.style.opacity = '1'; opponentPanel.style.transform = 'scale(1) translateY(0)';
- }
- }
- currentGameId = data.gameId; myPlayerId = data.yourPlayerId; currentGameState = data.initialGameState;
- playerBaseStatsServer = data.playerBaseStats; opponentBaseStatsServer = data.opponentBaseStats;
- playerAbilitiesServer = data.playerAbilities; opponentAbilitiesServer = data.opponentAbilities;
- myCharacterKey = playerBaseStatsServer?.characterKey; opponentCharacterKey = opponentBaseStatsServer?.characterKey;
-
- if (data.clientConfig) window.GAME_CONFIG = { ...data.clientConfig };
- else if (!window.GAME_CONFIG) {
- window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' };
- }
- window.gameState = currentGameState;
- window.gameData = { playerBaseStats: playerBaseStatsServer, opponentBaseStats: opponentBaseStatsServer, playerAbilities: playerAbilitiesServer, opponentAbilities: opponentAbilitiesServer };
- window.myPlayerId = myPlayerId;
-
- showGameScreen(); initializeAbilityButtons();
- if (window.gameUI?.uiElements?.log?.list) window.gameUI.uiElements.log.list.innerHTML = '';
- if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) {
- data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type));
- }
- requestAnimationFrame(() => {
- if (window.gameUI && typeof window.gameUI.updateUI === 'function') {
- window.gameUI.updateUI();
- }
- });
- hideGameOverModal(); setGameStatusMessage("");
- });
-
- // Используется для восстановления состояния уже идущей игры
- socket.on('gameState', (data) => {
- if (!isLoggedIn) return;
- console.log('[Client] Received full gameState (e.g. on reconnect):', data);
- // Это событие теперь может дублировать 'gameStarted' для переподключения.
- // Убедимся, что логика похожа на gameStarted.
- currentGameId = data.gameId;
- myPlayerId = data.yourPlayerId;
- currentGameState = data.gameState; // Используем gameState вместо initialGameState
- playerBaseStatsServer = data.playerBaseStats;
- opponentBaseStatsServer = data.opponentBaseStats;
- playerAbilitiesServer = data.playerAbilities;
- opponentAbilitiesServer = data.opponentAbilities;
- myCharacterKey = playerBaseStatsServer?.characterKey;
- opponentCharacterKey = opponentBaseStatsServer?.characterKey;
-
- if (data.clientConfig) window.GAME_CONFIG = { ...data.clientConfig };
- else if (!window.GAME_CONFIG) {
- window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' };
- }
- window.gameState = currentGameState;
- window.gameData = { playerBaseStats: playerBaseStatsServer, opponentBaseStats: opponentBaseStatsServer, playerAbilities: playerAbilitiesServer, opponentAbilities: opponentAbilitiesServer };
- window.myPlayerId = myPlayerId;
-
- if (!isInGame) showGameScreen(); // Показываем экран игры, если еще не там
- initializeAbilityButtons(); // Переинициализируем кнопки
-
- // Лог при 'gameState' может быть уже накопленным, добавляем его
- if (window.gameUI?.uiElements?.log?.list && data.log) { // Очищаем лог перед добавлением нового при полном обновлении
- window.gameUI.uiElements.log.list.innerHTML = '';
- }
- if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) {
- data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type));
- }
-
- requestAnimationFrame(() => {
- if (window.gameUI && typeof window.gameUI.updateUI === 'function') {
- window.gameUI.updateUI();
- }
- });
- hideGameOverModal();
- // Таймер будет обновлен следующим событием 'turnTimerUpdate'
- });
-
-
- socket.on('gameStateUpdate', (data) => {
- if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) return;
- currentGameState = data.gameState; window.gameState = currentGameState;
- if (window.gameUI?.updateUI) window.gameUI.updateUI();
- if (window.gameUI?.addToLog && data.log) {
- data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
- }
- });
-
- socket.on('logUpdate', (data) => {
- if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) return;
- if (window.gameUI?.addToLog && data.log) {
- data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
- }
- });
-
- socket.on('gameOver', (data) => {
- // ... (код без изменений, как был)
- if (!isLoggedIn || !currentGameId || !window.GAME_CONFIG) {
- if (!currentGameId && isLoggedIn) socket.emit('requestGameState');
- else if (!isLoggedIn) showAuthScreen();
- return;
- }
- const playerWon = data.winnerId === myPlayerId;
- currentGameState = data.finalGameState; window.gameState = currentGameState;
- if (window.gameUI?.updateUI) window.gameUI.updateUI();
- if (window.gameUI?.addToLog && data.log) {
- data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
- }
- if (window.gameUI?.showGameOver) {
- const oppKey = window.gameData?.opponentBaseStats?.characterKey;
- window.gameUI.showGameOver(playerWon, data.reason, oppKey, data);
- }
- if (returnToMenuButton) returnToMenuButton.disabled = false;
- setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли."));
- if (window.gameUI?.updateTurnTimerDisplay) { // Обновляем UI таймера
- window.gameUI.updateTurnTimerDisplay(null, false, currentGameState?.gameMode); // Передаем null, чтобы показать "Конец" или скрыть
- }
- });
-
- socket.on('waitingForOpponent', () => {
- if (!isLoggedIn) return;
- setGameStatusMessage("Ожидание присоединения оппонента...");
- disableGameControls(); // Боевые кнопки неактивны
- disableSetupButtons(); // Кнопки создания/присоединения тоже, пока ждем
- if (createPvPGameButton) createPvPGameButton.disabled = false; // Оставляем активной "Создать PvP" для отмены
- if (window.gameUI?.updateTurnTimerDisplay) {
- window.gameUI.updateTurnTimerDisplay(null, false, 'pvp'); // Таймер неактивен
- }
- });
-
- socket.on('opponentDisconnected', (data) => {
- if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) return;
- const name = data.disconnectedCharacterName || 'Противник';
- if (window.gameUI?.addToLog) window.gameUI.addToLog(`🔌 Противник (${name}) отключился.`, 'system');
- if (currentGameState && !currentGameState.isGameOver) {
- setGameStatusMessage(`Противник (${name}) отключился. Ожидание...`, true);
- disableGameControls();
- }
- });
-
- socket.on('gameError', (data) => {
- console.error('[Client] Server error:', data.message);
- if (isLoggedIn && isInGame && currentGameState && !currentGameState.isGameOver && window.gameUI?.addToLog) {
- window.gameUI.addToLog(`❌ Ошибка игры: ${data.message}`, 'system');
- disableGameControls(); setGameStatusMessage(`Ошибка: ${data.message}.`, true);
- } else {
- setGameStatusMessage(`❌ Ошибка: ${data.message}`, true);
- if (isLoggedIn) enableSetupButtons(); // Если на экране выбора игры, включаем кнопки
- else { // Если на экране логина
- if(registerForm) registerForm.querySelector('button').disabled = false;
- if(loginForm) loginForm.querySelector('button').disabled = false;
- }
- }
- });
-
- socket.on('availablePvPGamesList', (games) => {
- if (!isLoggedIn) return;
- updateAvailableGamesList(games);
- });
-
- socket.on('noPendingGamesFound', (data) => { // Вызывается, когда создается новая игра после поиска
- if (!isLoggedIn) return;
- setGameStatusMessage(data.message || "Свободных игр не найдено. Создана новая для вас.");
- updateAvailableGamesList([]); // Очищаем список
- // currentGameId и myPlayerId должны были прийти с gameCreated
- isInGame = false; // Еще не в активной фазе боя
- disableGameControls();
- disableSetupButtons(); // Мы в ожидающей игре
- if (window.gameUI?.updateTurnTimerDisplay) {
- window.gameUI.updateTurnTimerDisplay(null, false, 'pvp');
- }
- });
-
- socket.on('turnTimerUpdate', (data) => {
- if (!isInGame || !currentGameState || currentGameState.isGameOver) {
- if (window.gameUI?.updateTurnTimerDisplay && !currentGameState?.isGameOver) { // Только если не game over
- window.gameUI.updateTurnTimerDisplay(null, false, currentGameState?.gameMode);
- }
- return;
- }
- if (window.gameUI && typeof window.gameUI.updateTurnTimerDisplay === 'function') {
- // Определяем, является ли текущий ход ходом этого клиента
- const isMyActualTurn = myPlayerId && currentGameState.isPlayerTurn === (myPlayerId === GAME_CONFIG.PLAYER_ID);
- window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyActualTurn, currentGameState.gameMode);
- }
- });
-
- showAuthScreen(); // Начальный экран
+// /public/js/client.js
+
+document.addEventListener('DOMContentLoaded', () => {
+ const socket = io({
+ // Опции Socket.IO, если нужны
+ // transports: ['websocket'], // Можно попробовать для отладки, если есть проблемы с polling
+ });
+
+ // --- Состояние клиента ---
+ let currentGameState = null;
+ let myPlayerId = null; // Технический ID слота в игре ('player' или 'opponent')
+ let myUserId = null; // ID залогиненного пользователя (из БД)
+ let myCharacterKey = null;
+ let opponentCharacterKey = null;
+ let currentGameId = null;
+ let playerBaseStatsServer = null;
+ let opponentBaseStatsServer = null;
+ let playerAbilitiesServer = null;
+ let opponentAbilitiesServer = null;
+ let isLoggedIn = false;
+ let loggedInUsername = '';
+ let isInGame = false;
+
+ // --- DOM Элементы ---
+ const authSection = document.getElementById('auth-section');
+ const registerForm = document.getElementById('register-form');
+ const loginForm = document.getElementById('login-form');
+ const authMessage = document.getElementById('auth-message');
+ const statusContainer = document.getElementById('status-container');
+ const userInfoDiv = document.getElementById('user-info');
+ const loggedInUsernameSpan = document.getElementById('logged-in-username');
+ const logoutButton = document.getElementById('logout-button');
+
+ const gameSetupDiv = document.getElementById('game-setup');
+ const createAIGameButton = document.getElementById('create-ai-game');
+ const createPvPGameButton = document.getElementById('create-pvp-game');
+ const joinPvPGameButton = document.getElementById('join-pvp-game'); // Убедитесь, что ID в HTML 'join-pvp-game'
+ const findRandomPvPGameButton = document.getElementById('find-random-pvp-game');
+ const gameIdInput = document.getElementById('game-id-input');
+ const availableGamesDiv = document.getElementById('available-games-list');
+ const gameStatusMessage = document.getElementById('game-status-message');
+ const pvpCharacterRadios = document.querySelectorAll('input[name="pvp-character"]');
+
+ const gameWrapper = document.querySelector('.game-wrapper');
+ const attackButton = document.getElementById('button-attack');
+ const returnToMenuButton = document.getElementById('return-to-menu-button');
+ const gameOverScreen = document.getElementById('game-over-screen');
+ const abilitiesGrid = document.getElementById('abilities-grid');
+
+ const turnTimerSpan = document.getElementById('turn-timer');
+ const turnTimerContainer = document.getElementById('turn-timer-container');
+
+ // --- Функции управления UI ---
+ function showAuthScreen() {
+ authSection.style.display = 'block';
+ userInfoDiv.style.display = 'none';
+ gameSetupDiv.style.display = 'none';
+ gameWrapper.style.display = 'none';
+ hideGameOverModal();
+ setAuthMessage("Ожидание подключения к серверу...");
+ statusContainer.style.display = 'block';
+ isInGame = false;
+ disableGameControls();
+ resetGameVariables();
+ if (turnTimerContainer) turnTimerContainer.style.display = 'none';
+ if (turnTimerSpan) turnTimerSpan.textContent = '--';
+ }
+
+ function showGameSelectionScreen(username) {
+ authSection.style.display = 'none';
+ userInfoDiv.style.display = 'block';
+ loggedInUsernameSpan.textContent = username;
+ gameSetupDiv.style.display = 'block';
+ gameWrapper.style.display = 'none';
+ hideGameOverModal();
+ setGameStatusMessage("Выберите режим игры или присоединитесь к существующей.");
+ statusContainer.style.display = 'block';
+ socket.emit('requestPvPGameList');
+ updateAvailableGamesList([]); // Очищаем перед запросом
+ if (gameIdInput) gameIdInput.value = '';
+ const elenaRadio = document.getElementById('char-elena');
+ if (elenaRadio) elenaRadio.checked = true;
+ isInGame = false;
+ disableGameControls();
+ resetGameVariables(); // Сбрасываем игровые переменные при выходе в меню
+ if (turnTimerContainer) turnTimerContainer.style.display = 'none';
+ if (turnTimerSpan) turnTimerSpan.textContent = '--';
+ enableSetupButtons(); // Включаем кнопки на экране выбора игры
+ }
+
+ function showGameScreen() {
+ hideGameOverModal();
+ authSection.style.display = 'none';
+ userInfoDiv.style.display = 'block'; // Оставляем инфо о пользователе
+ gameSetupDiv.style.display = 'none';
+ gameWrapper.style.display = 'flex';
+ setGameStatusMessage(""); // Очищаем статус, т.к. есть индикатор хода
+ statusContainer.style.display = 'none'; // Скрываем общий статус контейнер
+ isInGame = true;
+ disableGameControls(); // Кнопки включатся, когда будет ход игрока
+ if (turnTimerContainer) turnTimerContainer.style.display = 'block'; // Показываем таймер
+ if (turnTimerSpan) turnTimerSpan.textContent = '--'; // Начальное значение
+ }
+
+ function resetGameVariables() {
+ currentGameId = null; currentGameState = null; myPlayerId = null;
+ myCharacterKey = null; opponentCharacterKey = null;
+ playerBaseStatsServer = null; opponentBaseStatsServer = null;
+ playerAbilitiesServer = null; opponentAbilitiesServer = null;
+ window.gameState = null; window.gameData = null; window.myPlayerId = null;
+ }
+
+ function hideGameOverModal() {
+ const hiddenClass = window.GAME_CONFIG?.CSS_CLASS_HIDDEN || 'hidden';
+ if (gameOverScreen && !gameOverScreen.classList.contains(hiddenClass)) {
+ gameOverScreen.classList.add(hiddenClass);
+ if (window.gameUI?.uiElements?.gameOver?.modalContent) {
+ window.gameUI.uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)';
+ window.gameUI.uiElements.gameOver.modalContent.style.opacity = '0';
+ }
+ const opponentPanel = window.gameUI?.uiElements?.opponent?.panel;
+ if (opponentPanel?.classList.contains('dissolving')) {
+ opponentPanel.classList.remove('dissolving');
+ opponentPanel.style.opacity = '1'; opponentPanel.style.transform = 'scale(1) translateY(0)';
+ }
+ }
+ }
+
+ function setAuthMessage(message, isError = false) {
+ if (authMessage) {
+ authMessage.textContent = message;
+ authMessage.className = isError ? 'error' : 'success';
+ authMessage.style.display = message ? 'block' : 'none';
+ }
+ if (message && gameStatusMessage) gameStatusMessage.style.display = 'none';
+ }
+
+ function setGameStatusMessage(message, isError = false) {
+ if (gameStatusMessage) {
+ gameStatusMessage.textContent = message;
+ gameStatusMessage.style.display = message ? 'block' : 'none';
+ gameStatusMessage.style.color = isError ? 'var(--damage-color, red)' : 'var(--turn-color, yellow)';
+ if (statusContainer) statusContainer.style.display = message ? 'block' : 'none';
+ }
+ if (message && authMessage) authMessage.style.display = 'none';
+ }
+
+ function getSelectedCharacterKey() {
+ let selectedKey = 'elena';
+ if (pvpCharacterRadios) {
+ pvpCharacterRadios.forEach(radio => { if (radio.checked) selectedKey = radio.value; });
+ }
+ return selectedKey;
+ }
+
+ function enableGameControls(enableAttack = true, enableAbilities = true) {
+ if (attackButton) attackButton.disabled = !enableAttack;
+ if (abilitiesGrid) {
+ const cls = window.GAME_CONFIG?.CSS_CLASS_ABILITY_BUTTON || 'ability-button';
+ abilitiesGrid.querySelectorAll(`.${cls}`).forEach(b => { b.disabled = !enableAbilities; });
+ }
+ if (window.gameUI?.uiElements?.controls?.buttonBlock) window.gameUI.uiElements.controls.buttonBlock.disabled = true;
+ }
+ function disableGameControls() { enableGameControls(false, false); }
+
+ function disableSetupButtons() {
+ if(createAIGameButton) createAIGameButton.disabled = true;
+ if(createPvPGameButton) createPvPGameButton.disabled = true;
+ if(joinPvPGameButton) joinPvPGameButton.disabled = true;
+ if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = true;
+ if(availableGamesDiv) availableGamesDiv.querySelectorAll('button').forEach(btn => btn.disabled = true);
+ }
+ function enableSetupButtons() {
+ if(createAIGameButton) createAIGameButton.disabled = false;
+ if(createPvPGameButton) createPvPGameButton.disabled = false;
+ if(joinPvPGameButton) joinPvPGameButton.disabled = false;
+ if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = false;
+ // Кнопки в списке игр включаются в updateAvailableGamesList
+ }
+
+ // --- Инициализация обработчиков событий ---
+ if (registerForm) registerForm.addEventListener('submit', (e) => {
+ e.preventDefault();
+ const u = document.getElementById('register-username').value;
+ const p = document.getElementById('register-password').value;
+ registerForm.querySelector('button').disabled = true;
+ if(loginForm) loginForm.querySelector('button').disabled = true;
+ socket.emit('register', { username: u, password: p });
+ });
+ if (loginForm) loginForm.addEventListener('submit', (e) => {
+ e.preventDefault();
+ const u = document.getElementById('login-username').value;
+ const p = document.getElementById('login-password').value;
+ if(registerForm) registerForm.querySelector('button').disabled = true;
+ loginForm.querySelector('button').disabled = true;
+ socket.emit('login', { username: u, password: p });
+ });
+ if (logoutButton) logoutButton.addEventListener('click', () => {
+ logoutButton.disabled = true; socket.emit('logout');
+ isLoggedIn = false; loggedInUsername = ''; myUserId = null;
+ resetGameVariables(); isInGame = false; disableGameControls();
+ showAuthScreen(); setGameStatusMessage("Вы вышли из системы.");
+ logoutButton.disabled = false;
+ });
+ if (createAIGameButton) createAIGameButton.addEventListener('click', () => {
+ if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; }
+ disableSetupButtons();
+ socket.emit('createGame', { mode: 'ai', characterKey: 'elena' }); // AI всегда за Елену
+ setGameStatusMessage("Создание игры против AI...");
+ });
+ if (createPvPGameButton) createPvPGameButton.addEventListener('click', () => {
+ if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; }
+ disableSetupButtons();
+ socket.emit('createGame', { mode: 'pvp', characterKey: getSelectedCharacterKey() });
+ setGameStatusMessage("Создание PvP игры...");
+ });
+ if (joinPvPGameButton) joinPvPGameButton.addEventListener('click', () => { // Убедитесь, что ID кнопки 'join-pvp-game'
+ if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; }
+ const gameId = gameIdInput.value.trim();
+ if (gameId) {
+ disableSetupButtons();
+ socket.emit('joinGame', { gameId: gameId });
+ setGameStatusMessage(`Присоединение к игре ${gameId}...`);
+ } else setGameStatusMessage("Введите ID игры.", true);
+ });
+ if (findRandomPvPGameButton) findRandomPvPGameButton.addEventListener('click', () => {
+ if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; }
+ disableSetupButtons();
+ socket.emit('findRandomGame', { characterKey: getSelectedCharacterKey() });
+ setGameStatusMessage("Поиск случайной PvP игры...");
+ });
+ if (attackButton) attackButton.addEventListener('click', () => {
+ if (isLoggedIn && isInGame && currentGameId && currentGameState && !currentGameState.isGameOver) {
+ socket.emit('playerAction', { actionType: 'attack' });
+ } else { /* обработка ошибки/некорректного состояния */ }
+ });
+ function handleAbilityButtonClick(event) {
+ const abilityId = event.currentTarget.dataset.abilityId;
+ if (isLoggedIn && isInGame && currentGameId && abilityId && currentGameState && !currentGameState.isGameOver) {
+ socket.emit('playerAction', { actionType: 'ability', abilityId: abilityId });
+ } else { /* обработка ошибки/некорректного состояния */ }
+ }
+ if (returnToMenuButton) returnToMenuButton.addEventListener('click', () => {
+ if (!isLoggedIn) { showAuthScreen(); return; }
+ returnToMenuButton.disabled = true;
+ resetGameVariables(); isInGame = false; disableGameControls(); hideGameOverModal();
+ showGameSelectionScreen(loggedInUsername); // Возвращаемся на экран выбора
+ // Кнопка включится при следующем показе модалки
+ });
+
+ function initializeAbilityButtons() {
+ // ... (код без изменений, как был)
+ if (!abilitiesGrid || !window.gameUI || !window.GAME_CONFIG) {
+ if (abilitiesGrid) abilitiesGrid.innerHTML = 'Ошибка загрузки способностей.
';
+ return;
+ }
+ abilitiesGrid.innerHTML = '';
+ const config = window.GAME_CONFIG;
+ const abilitiesToDisplay = playerAbilitiesServer;
+ const baseStatsForResource = playerBaseStatsServer;
+
+ if (!abilitiesToDisplay || abilitiesToDisplay.length === 0 || !baseStatsForResource) {
+ abilitiesGrid.innerHTML = 'Нет доступных способностей.
';
+ return;
+ }
+ const resourceName = baseStatsForResource.resourceName || "Ресурс";
+ const abilityButtonClass = config.CSS_CLASS_ABILITY_BUTTON || 'ability-button';
+
+ abilitiesToDisplay.forEach(ability => {
+ const button = document.createElement('button');
+ button.id = `ability-btn-${ability.id}`;
+ button.classList.add(abilityButtonClass);
+ button.dataset.abilityId = ability.id;
+ let cooldown = ability.cooldown;
+ let cooldownText = (typeof cooldown === 'number' && cooldown > 0) ? ` (КД: ${cooldown} х.)` : "";
+ let title = `${ability.name} (${ability.cost} ${resourceName})${cooldownText} - ${ability.description || 'Нет описания'}`;
+ button.setAttribute('title', title);
+ const nameSpan = document.createElement('span'); nameSpan.classList.add('ability-name'); nameSpan.textContent = ability.name; button.appendChild(nameSpan);
+ const descSpan = document.createElement('span'); descSpan.classList.add('ability-desc'); descSpan.textContent = `(${ability.cost} ${resourceName})`; button.appendChild(descSpan);
+ const cdDisplay = document.createElement('span'); cdDisplay.classList.add('ability-cooldown-display'); cdDisplay.style.display = 'none'; button.appendChild(cdDisplay);
+ button.addEventListener('click', handleAbilityButtonClick);
+ abilitiesGrid.appendChild(button);
+ });
+ const placeholder = abilitiesGrid.querySelector('.placeholder-text');
+ if (placeholder) placeholder.remove();
+ }
+
+ function updateAvailableGamesList(games) {
+ if (!availableGamesDiv) return;
+ availableGamesDiv.innerHTML = 'Доступные PvP игры: ';
+ if (games && games.length > 0) {
+ const ul = document.createElement('ul');
+ games.forEach(game => {
+ if (game && game.id) {
+ const li = document.createElement('li');
+ li.textContent = `ID: ${game.id.substring(0, 8)}... - ${game.status || 'Ожидает игрока'}`;
+ const joinBtn = document.createElement('button');
+ joinBtn.textContent = 'Присоединиться';
+ joinBtn.dataset.gameId = game.id;
+
+ // === ИЗМЕНЕНИЕ: Деактивация кнопки "Присоединиться" для своих игр ===
+ if (isLoggedIn && myUserId && game.ownerIdentifier === myUserId) {
+ joinBtn.disabled = true;
+ joinBtn.title = "Вы не можете присоединиться к своей же ожидающей игре.";
+ } else {
+ joinBtn.disabled = false;
+ }
+ // === КОНЕЦ ИЗМЕНЕНИЯ ===
+
+ joinBtn.addEventListener('click', (e) => {
+ if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; }
+ if (e.target.disabled) return; // Не обрабатывать клик по отключенной кнопке
+ disableSetupButtons();
+ socket.emit('joinGame', { gameId: e.target.dataset.gameId });
+ });
+ li.appendChild(joinBtn);
+ ul.appendChild(li);
+ }
+ });
+ availableGamesDiv.appendChild(ul);
+ } else {
+ availableGamesDiv.innerHTML += 'Нет доступных игр. Создайте свою!
';
+ }
+ enableSetupButtons(); // Включаем основные кнопки создания/поиска
+ }
+
+
+ // --- Обработчики событий Socket.IO ---
+ socket.on('connect', () => {
+ console.log('[Client] Socket connected:', socket.id);
+ if (isLoggedIn && myUserId) { // Проверяем и isLoggedIn и myUserId
+ socket.emit('requestGameState'); // Запрашиваем состояние, если были залогинены
+ } else {
+ showAuthScreen(); // Иначе показываем экран логина
+ }
+ });
+
+ socket.on('registerResponse', (data) => {
+ setAuthMessage(data.message, !data.success);
+ if (data.success && registerForm) registerForm.reset();
+ if(registerForm) registerForm.querySelector('button').disabled = false;
+ if(loginForm) loginForm.querySelector('button').disabled = false;
+ });
+
+ socket.on('loginResponse', (data) => {
+ setAuthMessage(data.message, !data.success);
+ if (data.success) {
+ isLoggedIn = true;
+ loggedInUsername = data.username;
+ myUserId = data.userId; // === ИЗМЕНЕНИЕ: Сохраняем ID пользователя ===
+ setAuthMessage("");
+ showGameSelectionScreen(data.username);
+ } else {
+ isLoggedIn = false; loggedInUsername = ''; myUserId = null;
+ if(registerForm) registerForm.querySelector('button').disabled = false;
+ if(loginForm) loginForm.querySelector('button').disabled = false;
+ }
+ });
+
+ socket.on('gameNotFound', (data) => {
+ console.log('[Client] Game not found/ended:', data?.message);
+ resetGameVariables(); isInGame = false; disableGameControls(); hideGameOverModal();
+ if (turnTimerContainer) turnTimerContainer.style.display = 'none';
+ if (turnTimerSpan) turnTimerSpan.textContent = '--';
+
+ if (isLoggedIn) {
+ showGameSelectionScreen(loggedInUsername);
+ setGameStatusMessage(data?.message || "Активная игровая сессия не найдена.");
+ } else {
+ showAuthScreen();
+ setAuthMessage(data?.message || "Пожалуйста, войдите.");
+ }
+ });
+
+ socket.on('disconnect', (reason) => {
+ console.log('[Client] Disconnected:', reason);
+ setGameStatusMessage(`Отключено: ${reason}. Обновите страницу.`, true);
+ disableGameControls();
+ if (turnTimerSpan) turnTimerSpan.textContent = 'Откл.';
+ // Не сбрасываем isLoggedIn, чтобы при переподключении можно было восстановить сессию
+ });
+
+ socket.on('gameCreated', (data) => { // Сервер присылает это после успешного createGame
+ console.log('[Client] Game created by this client:', data);
+ currentGameId = data.gameId;
+ myPlayerId = data.yourPlayerId; // Сервер должен прислать роль создателя
+ // Остальные данные (gameState, baseStats) придут с gameStarted или gameState (если это PvP ожидание)
+ // Если это PvP и игра ожидает, сервер может прислать waitingForOpponent
+ });
+
+
+ socket.on('gameStarted', (data) => {
+ if (!isLoggedIn) return;
+ console.log('[Client] Game started:', data);
+ // ... (остальной код gameStarted без изменений, как был)
+ if (window.gameUI?.uiElements?.opponent?.panel) {
+ const opponentPanel = window.gameUI.uiElements.opponent.panel;
+ if (opponentPanel.classList.contains('dissolving')) {
+ opponentPanel.classList.remove('dissolving');
+ opponentPanel.style.opacity = '1'; opponentPanel.style.transform = 'scale(1) translateY(0)';
+ }
+ }
+ currentGameId = data.gameId; myPlayerId = data.yourPlayerId; currentGameState = data.initialGameState;
+ playerBaseStatsServer = data.playerBaseStats; opponentBaseStatsServer = data.opponentBaseStats;
+ playerAbilitiesServer = data.playerAbilities; opponentAbilitiesServer = data.opponentAbilities;
+ myCharacterKey = playerBaseStatsServer?.characterKey; opponentCharacterKey = opponentBaseStatsServer?.characterKey;
+
+ if (data.clientConfig) window.GAME_CONFIG = { ...data.clientConfig };
+ else if (!window.GAME_CONFIG) {
+ window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' };
+ }
+ window.gameState = currentGameState;
+ window.gameData = { playerBaseStats: playerBaseStatsServer, opponentBaseStats: opponentBaseStatsServer, playerAbilities: playerAbilitiesServer, opponentAbilities: opponentAbilitiesServer };
+ window.myPlayerId = myPlayerId;
+
+ showGameScreen(); initializeAbilityButtons();
+ if (window.gameUI?.uiElements?.log?.list) window.gameUI.uiElements.log.list.innerHTML = '';
+ if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) {
+ data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type));
+ }
+ requestAnimationFrame(() => {
+ if (window.gameUI && typeof window.gameUI.updateUI === 'function') {
+ window.gameUI.updateUI();
+ }
+ });
+ hideGameOverModal(); setGameStatusMessage("");
+ });
+
+ // Используется для восстановления состояния уже идущей игры
+ socket.on('gameState', (data) => {
+ if (!isLoggedIn) return;
+ console.log('[Client] Received full gameState (e.g. on reconnect):', data);
+ // Это событие теперь может дублировать 'gameStarted' для переподключения.
+ // Убедимся, что логика похожа на gameStarted.
+ currentGameId = data.gameId;
+ myPlayerId = data.yourPlayerId;
+ currentGameState = data.gameState; // Используем gameState вместо initialGameState
+ playerBaseStatsServer = data.playerBaseStats;
+ opponentBaseStatsServer = data.opponentBaseStats;
+ playerAbilitiesServer = data.playerAbilities;
+ opponentAbilitiesServer = data.opponentAbilities;
+ myCharacterKey = playerBaseStatsServer?.characterKey;
+ opponentCharacterKey = opponentBaseStatsServer?.characterKey;
+
+ if (data.clientConfig) window.GAME_CONFIG = { ...data.clientConfig };
+ else if (!window.GAME_CONFIG) {
+ window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' };
+ }
+ window.gameState = currentGameState;
+ window.gameData = { playerBaseStats: playerBaseStatsServer, opponentBaseStats: opponentBaseStatsServer, playerAbilities: playerAbilitiesServer, opponentAbilities: opponentAbilitiesServer };
+ window.myPlayerId = myPlayerId;
+
+ if (!isInGame) showGameScreen(); // Показываем экран игры, если еще не там
+ initializeAbilityButtons(); // Переинициализируем кнопки
+
+ // Лог при 'gameState' может быть уже накопленным, добавляем его
+ if (window.gameUI?.uiElements?.log?.list && data.log) { // Очищаем лог перед добавлением нового при полном обновлении
+ window.gameUI.uiElements.log.list.innerHTML = '';
+ }
+ if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) {
+ data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type));
+ }
+
+ requestAnimationFrame(() => {
+ if (window.gameUI && typeof window.gameUI.updateUI === 'function') {
+ window.gameUI.updateUI();
+ }
+ });
+ hideGameOverModal();
+ // Таймер будет обновлен следующим событием 'turnTimerUpdate'
+ });
+
+
+ socket.on('gameStateUpdate', (data) => {
+ if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) return;
+ currentGameState = data.gameState; window.gameState = currentGameState;
+ if (window.gameUI?.updateUI) window.gameUI.updateUI();
+ if (window.gameUI?.addToLog && data.log) {
+ data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
+ }
+ });
+
+ socket.on('logUpdate', (data) => {
+ if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) return;
+ if (window.gameUI?.addToLog && data.log) {
+ data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
+ }
+ });
+
+ socket.on('gameOver', (data) => {
+ // ... (код без изменений, как был)
+ if (!isLoggedIn || !currentGameId || !window.GAME_CONFIG) {
+ if (!currentGameId && isLoggedIn) socket.emit('requestGameState');
+ else if (!isLoggedIn) showAuthScreen();
+ return;
+ }
+ const playerWon = data.winnerId === myPlayerId;
+ currentGameState = data.finalGameState; window.gameState = currentGameState;
+ if (window.gameUI?.updateUI) window.gameUI.updateUI();
+ if (window.gameUI?.addToLog && data.log) {
+ data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
+ }
+ if (window.gameUI?.showGameOver) {
+ const oppKey = window.gameData?.opponentBaseStats?.characterKey;
+ window.gameUI.showGameOver(playerWon, data.reason, oppKey, data);
+ }
+ if (returnToMenuButton) returnToMenuButton.disabled = false;
+ setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли."));
+ if (window.gameUI?.updateTurnTimerDisplay) { // Обновляем UI таймера
+ window.gameUI.updateTurnTimerDisplay(null, false, currentGameState?.gameMode); // Передаем null, чтобы показать "Конец" или скрыть
+ }
+ });
+
+ socket.on('waitingForOpponent', () => {
+ if (!isLoggedIn) return;
+ setGameStatusMessage("Ожидание присоединения оппонента...");
+ disableGameControls(); // Боевые кнопки неактивны
+ disableSetupButtons(); // Кнопки создания/присоединения тоже, пока ждем
+ if (createPvPGameButton) createPvPGameButton.disabled = false; // Оставляем активной "Создать PvP" для отмены
+ if (window.gameUI?.updateTurnTimerDisplay) {
+ window.gameUI.updateTurnTimerDisplay(null, false, 'pvp'); // Таймер неактивен
+ }
+ });
+
+ socket.on('opponentDisconnected', (data) => {
+ if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) return;
+ const name = data.disconnectedCharacterName || 'Противник';
+ if (window.gameUI?.addToLog) window.gameUI.addToLog(`🔌 Противник (${name}) отключился.`, 'system');
+ if (currentGameState && !currentGameState.isGameOver) {
+ setGameStatusMessage(`Противник (${name}) отключился. Ожидание...`, true);
+ disableGameControls();
+ }
+ });
+
+ socket.on('gameError', (data) => {
+ console.error('[Client] Server error:', data.message);
+ if (isLoggedIn && isInGame && currentGameState && !currentGameState.isGameOver && window.gameUI?.addToLog) {
+ window.gameUI.addToLog(`❌ Ошибка игры: ${data.message}`, 'system');
+ disableGameControls(); setGameStatusMessage(`Ошибка: ${data.message}.`, true);
+ } else {
+ setGameStatusMessage(`❌ Ошибка: ${data.message}`, true);
+ if (isLoggedIn) enableSetupButtons(); // Если на экране выбора игры, включаем кнопки
+ else { // Если на экране логина
+ if(registerForm) registerForm.querySelector('button').disabled = false;
+ if(loginForm) loginForm.querySelector('button').disabled = false;
+ }
+ }
+ });
+
+ socket.on('availablePvPGamesList', (games) => {
+ if (!isLoggedIn) return;
+ updateAvailableGamesList(games);
+ });
+
+ socket.on('noPendingGamesFound', (data) => { // Вызывается, когда создается новая игра после поиска
+ if (!isLoggedIn) return;
+ setGameStatusMessage(data.message || "Свободных игр не найдено. Создана новая для вас.");
+ updateAvailableGamesList([]); // Очищаем список
+ // currentGameId и myPlayerId должны были прийти с gameCreated
+ isInGame = false; // Еще не в активной фазе боя
+ disableGameControls();
+ disableSetupButtons(); // Мы в ожидающей игре
+ if (window.gameUI?.updateTurnTimerDisplay) {
+ window.gameUI.updateTurnTimerDisplay(null, false, 'pvp');
+ }
+ });
+
+ socket.on('turnTimerUpdate', (data) => {
+ if (!isInGame || !currentGameState || currentGameState.isGameOver) {
+ if (window.gameUI?.updateTurnTimerDisplay && !currentGameState?.isGameOver) { // Только если не game over
+ window.gameUI.updateTurnTimerDisplay(null, false, currentGameState?.gameMode);
+ }
+ return;
+ }
+ if (window.gameUI && typeof window.gameUI.updateTurnTimerDisplay === 'function') {
+ // Определяем, является ли текущий ход ходом этого клиента
+ const isMyActualTurn = myPlayerId && currentGameState.isPlayerTurn === (myPlayerId === GAME_CONFIG.PLAYER_ID);
+ window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyActualTurn, currentGameState.gameMode);
+ }
+ });
+
+ showAuthScreen(); // Начальный экран
});
\ No newline at end of file
diff --git a/public/js/ui.js b/public/js/ui.js
index 3e69e9a..e7e7922 100644
--- a/public/js/ui.js
+++ b/public/js/ui.js
@@ -1,534 +1,534 @@
-// /public/js/ui.js
-// Этот файл отвечает за обновление DOM на основе состояния игры,
-// полученного от client.js (который, в свою очередь, получает его от сервера).
-
-(function() {
- // --- DOM Элементы ---
- const uiElements = {
- player: {
- panel: document.getElementById('player-panel'),
- name: document.getElementById('player-name'),
- avatar: document.getElementById('player-panel')?.querySelector('.player-avatar'),
- hpFill: document.getElementById('player-hp-fill'), hpText: document.getElementById('player-hp-text'),
- resourceFill: document.getElementById('player-resource-fill'), resourceText: document.getElementById('player-resource-text'),
- status: document.getElementById('player-status'),
- effectsContainer: document.getElementById('player-effects'),
- buffsList: document.getElementById('player-effects')?.querySelector('.player-buffs'),
- debuffsList: document.getElementById('player-effects')?.querySelector('.player-debuffs')
- },
- opponent: {
- panel: document.getElementById('opponent-panel'),
- name: document.getElementById('opponent-name'),
- avatar: document.getElementById('opponent-panel')?.querySelector('.opponent-avatar'),
- hpFill: document.getElementById('opponent-hp-fill'), hpText: document.getElementById('opponent-hp-text'),
- resourceFill: document.getElementById('opponent-resource-fill'), resourceText: document.getElementById('opponent-resource-text'),
- status: document.getElementById('opponent-status'),
- effectsContainer: document.getElementById('opponent-effects'),
- buffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-buffs'),
- debuffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-debuffs')
- },
- controls: {
- turnIndicator: document.getElementById('turn-indicator'),
- buttonAttack: document.getElementById('button-attack'),
- buttonBlock: document.getElementById('button-block'),
- abilitiesGrid: document.getElementById('abilities-grid'),
- turnTimerContainer: document.getElementById('turn-timer-container'),
- turnTimerSpan: document.getElementById('turn-timer')
- },
- log: {
- list: document.getElementById('log-list'),
- },
- gameOver: {
- screen: document.getElementById('game-over-screen'),
- message: document.getElementById('result-message'),
- returnToMenuButton: document.getElementById('return-to-menu-button'),
- modalContent: document.getElementById('game-over-screen')?.querySelector('.modal-content')
- },
- gameHeaderTitle: document.querySelector('.game-header h1'),
- playerResourceTypeIcon: document.getElementById('player-resource-bar')?.closest('.stat-bar-container')?.querySelector('.bar-icon i'),
- opponentResourceTypeIcon: document.getElementById('opponent-resource-bar')?.closest('.stat-bar-container')?.querySelector('.bar-icon i'),
- playerResourceBarContainer: document.getElementById('player-resource-bar')?.closest('.stat-bar-container'),
- opponentResourceBarContainer: document.getElementById('opponent-resource-bar')?.closest('.stat-bar-container'),
-
- // === НОВЫЕ ЭЛЕМЕНТЫ для переключателя панелей ===
- panelSwitcher: {
- controlsContainer: document.querySelector('.panel-switcher-controls'),
- showPlayerBtn: document.getElementById('show-player-panel-btn'),
- showOpponentBtn: document.getElementById('show-opponent-panel-btn')
- },
- battleArenaContainer: document.querySelector('.battle-arena-container')
- // === КОНЕЦ НОВЫХ ЭЛЕМЕНТОВ ===
- };
-
- function addToLog(message, type = 'info') {
- const logListElement = uiElements.log.list;
- if (!logListElement) return;
- const li = document.createElement('li');
- li.textContent = message;
- const config = window.GAME_CONFIG || {};
- const logTypeClass = config[`LOG_TYPE_${type.toUpperCase()}`] ? `log-${config[`LOG_TYPE_${type.toUpperCase()}`]}` : `log-${type}`;
- li.className = logTypeClass;
- logListElement.appendChild(li);
- requestAnimationFrame(() => { logListElement.scrollTop = logListElement.scrollHeight; });
- }
-
- function updateFighterPanelUI(panelRole, fighterState, fighterBaseStats, isControlledByThisClient) {
- const elements = uiElements[panelRole];
- const config = window.GAME_CONFIG || {};
-
- if (!elements || !elements.hpFill || !elements.hpText || !elements.resourceFill || !elements.resourceText || !elements.status || !fighterState || !fighterBaseStats) {
- if (elements) {
- if(elements.name) elements.name.innerHTML = (panelRole === 'player') ? ' Ожидание данных...' : ' Ожидание игрока...';
- 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;
- }
- if (elements.panel) elements.panel.style.opacity = '1';
-
- if (elements.name) {
- let iconClass = 'fa-question';
- const characterKey = fighterBaseStats.characterKey;
- if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-elena'; }
- else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; }
- else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-balard'; }
- let nameHtml = ` ${fighterBaseStats.name || 'Неизвестно'}`;
- if (isControlledByThisClient) nameHtml += " (Вы)";
- elements.name.innerHTML = nameHtml;
- }
-
- if (elements.avatar && fighterBaseStats.avatarPath) {
- elements.avatar.src = fighterBaseStats.avatarPath;
- elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard');
- elements.avatar.classList.add(`avatar-${fighterBaseStats.characterKey}`);
- } else if (elements.avatar) {
- elements.avatar.src = 'images/default_avatar.png';
- elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard');
- }
-
- const maxHp = Math.max(1, fighterBaseStats.maxHp);
- const maxRes = Math.max(1, fighterBaseStats.maxResource);
- const currentHp = Math.max(0, fighterState.currentHp);
- const currentRes = Math.max(0, fighterState.currentResource);
- elements.hpFill.style.width = `${(currentHp / maxHp) * 100}%`;
- elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`;
- elements.resourceFill.style.width = `${(currentRes / maxRes) * 100}%`;
- elements.resourceText.textContent = `${currentRes} / ${fighterBaseStats.maxResource}`;
-
- const resourceBarContainerToUpdate = (panelRole === 'player') ? uiElements.playerResourceBarContainer : uiElements.opponentResourceBarContainer;
- const resourceIconElementToUpdate = (panelRole === 'player') ? uiElements.playerResourceTypeIcon : uiElements.opponentResourceTypeIcon;
- if (resourceBarContainerToUpdate && resourceIconElementToUpdate) {
- resourceBarContainerToUpdate.classList.remove('mana', 'stamina', 'dark-energy');
- let resourceClass = 'mana'; let iconClass = 'fa-flask';
- if (fighterBaseStats.resourceName === 'Ярость') { resourceClass = 'stamina'; iconClass = 'fa-fire-alt'; }
- else if (fighterBaseStats.resourceName === 'Темная Энергия') { resourceClass = 'dark-energy'; iconClass = 'fa-skull'; }
- resourceBarContainerToUpdate.classList.add(resourceClass);
- resourceIconElementToUpdate.className = `fas ${iconClass}`;
- }
-
- const statusText = fighterState.isBlocking ? (config.STATUS_BLOCKING || 'Защищается') : (config.STATUS_READY || 'Готов(а)');
- elements.status.textContent = statusText;
- elements.status.classList.toggle(config.CSS_CLASS_BLOCKING || 'blocking', fighterState.isBlocking);
-
- if (elements.panel) {
- let borderColorVar = 'var(--panel-border)';
- elements.panel.classList.remove('panel-elena', 'panel-almagest', 'panel-balard');
- if (fighterBaseStats.characterKey === 'elena') { elements.panel.classList.add('panel-elena'); borderColorVar = 'var(--accent-player)'; }
- else if (fighterBaseStats.characterKey === 'almagest') { elements.panel.classList.add('panel-almagest'); borderColorVar = 'var(--accent-almagest)'; }
- else if (fighterBaseStats.characterKey === 'balard') { elements.panel.classList.add('panel-balard'); borderColorVar = 'var(--accent-opponent)'; }
- let glowColorVar = 'rgba(0, 0, 0, 0.4)';
- if (fighterBaseStats.characterKey === 'elena') glowColorVar = 'var(--panel-glow-player)';
- else if (fighterBaseStats.characterKey === 'almagest') glowColorVar = 'var(--panel-glow-almagest)';
- else if (fighterBaseStats.characterKey === 'balard') glowColorVar = 'var(--panel-glow-opponent)';
- elements.panel.style.borderColor = borderColorVar;
- elements.panel.style.boxShadow = `0 0 15px ${glowColorVar}, inset 0 0 10px rgba(0, 0, 0, 0.3)`;
- }
- }
-
- function generateEffectsHTML(effectsArray) {
- const config = window.GAME_CONFIG || {};
- if (!effectsArray || effectsArray.length === 0) return 'Нет';
- return effectsArray.map(eff => {
- let effectClasses = config.CSS_CLASS_EFFECT || 'effect';
- const title = `${eff.name}${eff.description ? ` - ${eff.description}` : ''} (Осталось: ${eff.turnsLeft} х.)`;
- const displayText = `${eff.name} (${eff.turnsLeft} х.)`;
- if (eff.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) effectClasses += ' effect-debuff';
- else if (eff.type === config.ACTION_TYPE_BUFF || eff.type === config.ACTION_TYPE_HEAL) effectClasses += ' effect-buff';
- else effectClasses += ' effect-info';
- return `${displayText} `;
- }).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 = { [config.ACTION_TYPE_BUFF]: 1, grantsBlock: 2, [config.ACTION_TYPE_HEAL]: 3, [config.ACTION_TYPE_DEBUFF]: 4, [config.ACTION_TYPE_DISABLE]: 5 };
- const sortEffects = (a, b) => {
- let orderA = typeOrder[a.type] || 99; if (a.grantsBlock) orderA = typeOrder.grantsBlock; if (a.isFullSilence || a.id.startsWith('playerSilencedOn_')) orderA = typeOrder[config.ACTION_TYPE_DISABLE];
- let orderB = typeOrder[b.type] || 99; if (b.grantsBlock) orderB = typeOrder.grantsBlock; if (b.isFullSilence || b.id.startsWith('playerSilencedOn_')) orderB = typeOrder[config.ACTION_TYPE_DISABLE];
- return (orderA || 99) - (orderB || 99);
- };
-
- if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList && myState && myState.activeEffects) {
- const myBuffs = []; const myDebuffs = [];
- myState.activeEffects.forEach(e => {
- const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL;
- 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 myDebuffs.push(e);
- });
- myBuffs.sort(sortEffects); myDebuffs.sort(sortEffects);
- uiElements.player.buffsList.innerHTML = generateEffectsHTML(myBuffs);
- uiElements.player.debuffsList.innerHTML = generateEffectsHTML(myDebuffs);
- } else if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList) {
- uiElements.player.buffsList.innerHTML = 'Нет'; uiElements.player.debuffsList.innerHTML = 'Нет';
- }
-
- if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList && opponentState && opponentState.activeEffects) {
- const opponentBuffs = []; const opponentDebuffs = [];
- opponentState.activeEffects.forEach(e => {
- const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL;
- 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 opponentDebuffs.push(e);
- });
- opponentBuffs.sort(sortEffects); opponentDebuffs.sort(sortEffects);
- uiElements.opponent.buffsList.innerHTML = generateEffectsHTML(opponentBuffs);
- uiElements.opponent.debuffsList.innerHTML = generateEffectsHTML(opponentDebuffs);
- } else if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList) {
- uiElements.opponent.buffsList.innerHTML = 'Нет'; uiElements.opponent.debuffsList.innerHTML = 'Нет';
- }
- }
-
- function updateTurnTimerDisplay(remainingTimeMs, isCurrentPlayerActualTurn, gameMode) {
- const timerSpan = uiElements.controls.turnTimerSpan;
- const timerContainer = uiElements.controls.turnTimerContainer;
-
- if (!timerSpan || !timerContainer) return;
-
- if (window.gameState && window.gameState.isGameOver) {
- timerContainer.style.display = 'block';
- timerSpan.textContent = 'Конец';
- timerSpan.classList.remove('low-time');
- return;
- }
-
- if (remainingTimeMs === null || remainingTimeMs === undefined) {
- timerContainer.style.display = 'block';
- timerSpan.classList.remove('low-time');
- if (gameMode === 'ai' && !isCurrentPlayerActualTurn) {
- timerSpan.textContent = 'Ход ИИ';
- } else if (gameMode === 'pvp' && !isCurrentPlayerActualTurn) {
- timerSpan.textContent = 'Ход оппонента';
- } else {
- timerSpan.textContent = '--';
- }
- } else {
- timerContainer.style.display = 'block';
- const seconds = Math.ceil(remainingTimeMs / 1000);
- timerSpan.textContent = `0:${seconds < 10 ? '0' : ''}${seconds}`;
-
- if (seconds <= 10 && isCurrentPlayerActualTurn) {
- timerSpan.classList.add('low-time');
- } else {
- timerSpan.classList.remove('low-time');
- }
- }
- }
-
-
- function updateUI() {
- const currentGameState = window.gameState;
- const gameDataGlobal = window.gameData;
- const configGlobal = window.GAME_CONFIG;
- const myActualPlayerId = window.myPlayerId;
-
- if (!currentGameState || !gameDataGlobal || !configGlobal || !myActualPlayerId) {
- updateFighterPanelUI('player', null, null, true);
- updateFighterPanelUI('opponent', null, null, false);
- if(uiElements.gameHeaderTitle) uiElements.gameHeaderTitle.innerHTML = `Ожидание данных... `;
- 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 = 'Загрузка способностей...
';
- if (uiElements.controls.turnTimerContainer) uiElements.controls.turnTimerContainer.style.display = 'none';
- if (uiElements.controls.turnTimerSpan) {
- uiElements.controls.turnTimerSpan.textContent = '--';
- uiElements.controls.turnTimerSpan.classList.remove('low-time');
- }
- return;
- }
- if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) {
- console.warn("updateUI: Некоторые базовые uiElements не найдены.");
- return;
- }
-
- const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID;
- const opponentActualSlotId = myActualPlayerId === configGlobal.PLAYER_ID ? configGlobal.OPPONENT_ID : configGlobal.PLAYER_ID;
- const myStateInGameState = currentGameState[myActualPlayerId];
- const myBaseStatsForUI = gameDataGlobal.playerBaseStats;
- if (myStateInGameState && myBaseStatsForUI) updateFighterPanelUI('player', myStateInGameState, myBaseStatsForUI, true);
- else updateFighterPanelUI('player', null, null, true);
-
- const opponentStateInGameState = currentGameState[opponentActualSlotId];
- const opponentBaseStatsForUI = gameDataGlobal.opponentBaseStats;
- const isOpponentPanelDissolving = uiElements.opponent.panel?.classList.contains('dissolving');
- if (opponentStateInGameState && opponentBaseStatsForUI) {
- if (uiElements.opponent.panel && (uiElements.opponent.panel.style.opacity !== '1' || (uiElements.opponent.panel.classList.contains('dissolving') && currentGameState.isGameOver === false) )) {
- const panel = uiElements.opponent.panel;
- if (panel.classList.contains('dissolving')) {
- panel.classList.remove('dissolving'); panel.style.transition = 'none'; panel.offsetHeight;
- panel.style.opacity = '1'; panel.style.transform = 'scale(1) translateY(0)'; panel.style.transition = '';
- } else { panel.style.opacity = '1'; panel.style.transform = 'scale(1) translateY(0)'; }
- } else if (uiElements.opponent.panel && !isOpponentPanelDissolving) {
- uiElements.opponent.panel.style.opacity = '1';
- }
- updateFighterPanelUI('opponent', opponentStateInGameState, opponentBaseStatsForUI, false);
- } else {
- if (!isOpponentPanelDissolving) updateFighterPanelUI('opponent', null, null, false);
- else console.log("[UI UPDATE DEBUG] Opponent panel is dissolving, skipping content update.");
- }
-
- updateEffectsUI(currentGameState);
-
- if (uiElements.gameHeaderTitle && gameDataGlobal.playerBaseStats && gameDataGlobal.opponentBaseStats) {
- const myName = gameDataGlobal.playerBaseStats.name; const opponentName = gameDataGlobal.opponentBaseStats.name;
- const myKey = gameDataGlobal.playerBaseStats.characterKey; const opponentKey = gameDataGlobal.opponentBaseStats.characterKey;
- let myClass = 'title-player'; let opponentClass = 'title-opponent';
- if (myKey === 'elena') myClass = 'title-enchantress'; else if (myKey === 'almagest') myClass = 'title-sorceress'; else if (myKey === 'balard') myClass = 'title-knight';
- if (opponentKey === 'elena') opponentClass = 'title-enchantress'; else if (opponentKey === 'almagest') opponentClass = 'title-sorceress'; else if (opponentKey === 'balard') opponentClass = 'title-knight';
- uiElements.gameHeaderTitle.innerHTML = `${myName} ${opponentName} `;
- } 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 = `${myName} Ожидание игрока... `;
- }
-
- const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId;
- 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 || 'Неизвестно'}`;
- uiElements.controls.turnIndicator.style.color = (currentTurnActor?.id === myActualPlayerId) ? 'var(--turn-color)' : 'var(--text-muted)';
- } else {
- uiElements.controls.turnIndicator.textContent = "Игра окончена";
- uiElements.controls.turnIndicator.style.color = 'var(--text-muted)';
- }
- }
-
- if (uiElements.controls.buttonAttack) {
- uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive);
- const myCharKey = gameDataGlobal.playerBaseStats?.characterKey;
- let attackBuffId = null;
- if (myCharKey === 'elena') attackBuffId = configGlobal.ABILITY_ID_NATURE_STRENGTH;
- else if (myCharKey === 'almagest') attackBuffId = configGlobal.ABILITY_ID_ALMAGEST_BUFF_ATTACK;
- if (attackBuffId && myCharacterState && myCharacterState.activeEffects) {
- 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);
- } else { uiElements.controls.buttonAttack.classList.remove(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed'); }
- }
- if (uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true;
-
- const actingPlayerState = myCharacterState;
- const actingPlayerAbilities = gameDataGlobal.playerAbilities;
- const actingPlayerResourceName = gameDataGlobal.playerBaseStats?.resourceName;
- const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId];
-
- uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => {
- 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;
- 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;
- }
- const hasEnoughResource = actingPlayerState.currentResource >= abilityDataFromGameData.cost;
- const isOnCooldown = (actingPlayerState.abilityCooldowns?.[abilityId] || 0) > 0;
- const isGenerallySilenced = actingPlayerState.activeEffects?.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
- const isAbilitySpecificallySilenced = actingPlayerState.disabledAbilities?.some(dis => dis.abilityId === abilityId && dis.turnsLeft > 0);
- const isSilenced = isGenerallySilenced || isAbilitySpecificallySilenced;
- const silenceTurnsLeft = isAbilitySpecificallySilenced ? (actingPlayerState.disabledAbilities?.find(dis => dis.abilityId === abilityId)?.turnsLeft || 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 effectIdForDebuff = 'effect_' + abilityId;
- const isDebuffAlreadyOnTarget = isTargetedDebuffAbility && opponentStateForDebuffCheck && opponentStateForDebuffCheck.activeEffects?.some(e => e.id === effectIdForDebuff);
- button.disabled = !hasEnoughResource || isBuffAlreadyActive || isSilenced || isOnCooldown || isDebuffAlreadyOnTarget;
- 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 (isOnCooldown) {
- button.classList.add(configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
- if (cooldownDisplay) { cooldownDisplay.textContent = `КД: ${actingPlayerState.abilityCooldowns[abilityId]}`; cooldownDisplay.style.display = 'block'; }
- } else if (isSilenced) {
- button.classList.add(configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced');
- if (cooldownDisplay) { const icon = isGenerallySilenced ? '🔕' : '🔇'; cooldownDisplay.textContent = `${icon} ${silenceTurnsLeft}`; cooldownDisplay.style.display = 'block'; }
- } else {
- if (cooldownDisplay) cooldownDisplay.style.display = 'none';
- if (!isOnCooldown && !isSilenced) {
- 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);
- }
- }
- 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}`;
- let abilityBaseCooldown = abilityDataFromGameData.cooldown;
- if (typeof abilityBaseCooldown === 'number' && abilityBaseCooldown > 0) titleText += ` (Исходный КД: ${abilityBaseCooldown} х.)`;
- if (isOnCooldown) titleText += ` | На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[abilityId]} х.`;
- if (isSilenced) titleText += ` | Под безмолвием! Осталось: ${silenceTurnsLeft} х.`;
- if (isBuffAlreadyActive) {
- const activeEffect = actingPlayerState.activeEffects?.find(eff => eff.id === abilityId);
- 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) {
- const activeDebuff = opponentStateForDebuffCheck.activeEffects?.find(e => e.id === 'effect_' + abilityId);
- titleText += ` | Эффект уже наложен на ${gameDataGlobal.opponentBaseStats?.name || 'противника'}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}.`;
- }
- if (!hasEnoughResource) titleText += ` | Недостаточно ${actingPlayerResourceName} (${actingPlayerState.currentResource}/${abilityDataFromGameData.cost})`;
- button.setAttribute('title', titleText);
- });
- }
-
- function showGameOver(playerWon, reason = "", opponentCharacterKeyFromClient = null, data = null) {
- const config = window.GAME_CONFIG || {};
- const clientSpecificGameData = window.gameData;
- const currentActualGameState = window.gameState;
- const gameOverScreenElement = uiElements.gameOver.screen;
-
- if (!gameOverScreenElement) { return; }
-
- const resultMsgElement = uiElements.gameOver.message;
- const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок";
- const opponentNameForResult = clientSpecificGameData?.opponentBaseStats?.name || "Противник";
-
- if (resultMsgElement) {
- let winText = `Победа! ${myNameForResult} празднует!`;
- let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`;
- if (reason === 'opponent_disconnected') {
- let disconnectedName = data?.disconnectedCharacterName || opponentNameForResult;
- winText = `${disconnectedName} покинул(а) игру. Победа присуждается вам!`;
- } else if (reason === 'turn_timeout') {
- if (!playerWon) {
- loseText = `Время на ход истекло! Поражение. ${opponentNameForResult} побеждает!`;
- } else {
- winText = `Время на ход у ${opponentNameForResult} истекло! Победа!`;
- }
- }
- resultMsgElement.textContent = playerWon ? winText : loseText;
- resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)';
- }
-
- const opponentPanelElement = uiElements.opponent.panel;
- if (opponentPanelElement) {
- opponentPanelElement.classList.remove('dissolving');
- opponentPanelElement.style.transition = 'none'; opponentPanelElement.offsetHeight;
- const loserCharacterKeyForDissolve = data?.loserCharacterKey;
- if (currentActualGameState && currentActualGameState.isGameOver === true && playerWon) {
- if (loserCharacterKeyForDissolve === 'balard' || loserCharacterKeyForDissolve === 'almagest') {
- opponentPanelElement.classList.add('dissolving');
- opponentPanelElement.style.opacity = '0';
- } else {
- opponentPanelElement.style.opacity = '1'; opponentPanelElement.style.transform = 'scale(1) translateY(0)';
- }
- } else {
- opponentPanelElement.style.opacity = '1'; opponentPanelElement.style.transform = 'scale(1) translateY(0)';
- }
- opponentPanelElement.style.transition = '';
- }
-
- setTimeout((finalStateInTimeout) => {
- if (gameOverScreenElement && finalStateInTimeout && finalStateInTimeout.isGameOver === true) {
- if (gameOverScreenElement.classList.contains(config.CSS_CLASS_HIDDEN || 'hidden')) {
- gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden');
- }
- if(window.getComputedStyle(gameOverScreenElement).display === 'none') gameOverScreenElement.style.display = 'flex';
- gameOverScreenElement.style.opacity = '0';
- requestAnimationFrame(() => {
- gameOverScreenElement.style.opacity = '1';
- 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';
- uiElements.gameOver.modalContent.style.transform = 'scale(1) translateY(0)';
- uiElements.gameOver.modalContent.style.opacity = '1';
- }
- });
- } else {
- if (gameOverScreenElement) {
- gameOverScreenElement.style.transition = 'none';
- if (uiElements.gameOver.modalContent) uiElements.gameOver.modalContent.style.transition = 'none';
- gameOverScreenElement.classList.add(config.CSS_CLASS_HIDDEN || 'hidden');
- gameOverScreenElement.style.opacity = '0';
- if (uiElements.gameOver.modalContent) {
- uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)';
- uiElements.gameOver.modalContent.style.opacity = '0';
- }
- gameOverScreenElement.offsetHeight;
- }
- }
- }, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState);
- }
-
- // === НОВАЯ ФУНКЦИЯ для настройки переключателя панелей ===
- function setupPanelSwitcher() {
- const { showPlayerBtn, showOpponentBtn } = uiElements.panelSwitcher;
- const battleArena = uiElements.battleArenaContainer;
-
- if (showPlayerBtn && showOpponentBtn && battleArena) {
- showPlayerBtn.addEventListener('click', () => {
- battleArena.classList.remove('show-opponent-panel');
- showPlayerBtn.classList.add('active');
- showOpponentBtn.classList.remove('active');
- });
-
- showOpponentBtn.addEventListener('click', () => {
- battleArena.classList.add('show-opponent-panel');
- showOpponentBtn.classList.add('active');
- showPlayerBtn.classList.remove('active');
- });
-
- // По умолчанию при загрузке (если кнопки видимы) панель игрока активна
- // CSS уже должен это обеспечивать, но для надежности можно убедиться
- if (window.getComputedStyle(uiElements.panelSwitcher.controlsContainer).display !== 'none') {
- battleArena.classList.remove('show-opponent-panel');
- showPlayerBtn.classList.add('active');
- showOpponentBtn.classList.remove('active');
- }
- }
- }
- // === КОНЕЦ НОВОЙ ФУНКЦИИ ===
-
- window.gameUI = {
- uiElements,
- addToLog,
- updateUI,
- showGameOver,
- updateTurnTimerDisplay
- };
-
- // Настраиваем переключатель панелей при загрузке скрипта
- setupPanelSwitcher();
-
+// /public/js/ui.js
+// Этот файл отвечает за обновление DOM на основе состояния игры,
+// полученного от client.js (который, в свою очередь, получает его от сервера).
+
+(function() {
+ // --- DOM Элементы ---
+ const uiElements = {
+ player: {
+ panel: document.getElementById('player-panel'),
+ name: document.getElementById('player-name'),
+ avatar: document.getElementById('player-panel')?.querySelector('.player-avatar'),
+ hpFill: document.getElementById('player-hp-fill'), hpText: document.getElementById('player-hp-text'),
+ resourceFill: document.getElementById('player-resource-fill'), resourceText: document.getElementById('player-resource-text'),
+ status: document.getElementById('player-status'),
+ effectsContainer: document.getElementById('player-effects'),
+ buffsList: document.getElementById('player-effects')?.querySelector('.player-buffs'),
+ debuffsList: document.getElementById('player-effects')?.querySelector('.player-debuffs')
+ },
+ opponent: {
+ panel: document.getElementById('opponent-panel'),
+ name: document.getElementById('opponent-name'),
+ avatar: document.getElementById('opponent-panel')?.querySelector('.opponent-avatar'),
+ hpFill: document.getElementById('opponent-hp-fill'), hpText: document.getElementById('opponent-hp-text'),
+ resourceFill: document.getElementById('opponent-resource-fill'), resourceText: document.getElementById('opponent-resource-text'),
+ status: document.getElementById('opponent-status'),
+ effectsContainer: document.getElementById('opponent-effects'),
+ buffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-buffs'),
+ debuffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-debuffs')
+ },
+ controls: {
+ turnIndicator: document.getElementById('turn-indicator'),
+ buttonAttack: document.getElementById('button-attack'),
+ buttonBlock: document.getElementById('button-block'),
+ abilitiesGrid: document.getElementById('abilities-grid'),
+ turnTimerContainer: document.getElementById('turn-timer-container'),
+ turnTimerSpan: document.getElementById('turn-timer')
+ },
+ log: {
+ list: document.getElementById('log-list'),
+ },
+ gameOver: {
+ screen: document.getElementById('game-over-screen'),
+ message: document.getElementById('result-message'),
+ returnToMenuButton: document.getElementById('return-to-menu-button'),
+ modalContent: document.getElementById('game-over-screen')?.querySelector('.modal-content')
+ },
+ gameHeaderTitle: document.querySelector('.game-header h1'),
+ playerResourceTypeIcon: document.getElementById('player-resource-bar')?.closest('.stat-bar-container')?.querySelector('.bar-icon i'),
+ opponentResourceTypeIcon: document.getElementById('opponent-resource-bar')?.closest('.stat-bar-container')?.querySelector('.bar-icon i'),
+ playerResourceBarContainer: document.getElementById('player-resource-bar')?.closest('.stat-bar-container'),
+ opponentResourceBarContainer: document.getElementById('opponent-resource-bar')?.closest('.stat-bar-container'),
+
+ // === НОВЫЕ ЭЛЕМЕНТЫ для переключателя панелей ===
+ panelSwitcher: {
+ controlsContainer: document.querySelector('.panel-switcher-controls'),
+ showPlayerBtn: document.getElementById('show-player-panel-btn'),
+ showOpponentBtn: document.getElementById('show-opponent-panel-btn')
+ },
+ battleArenaContainer: document.querySelector('.battle-arena-container')
+ // === КОНЕЦ НОВЫХ ЭЛЕМЕНТОВ ===
+ };
+
+ function addToLog(message, type = 'info') {
+ const logListElement = uiElements.log.list;
+ if (!logListElement) return;
+ const li = document.createElement('li');
+ li.textContent = message;
+ const config = window.GAME_CONFIG || {};
+ const logTypeClass = config[`LOG_TYPE_${type.toUpperCase()}`] ? `log-${config[`LOG_TYPE_${type.toUpperCase()}`]}` : `log-${type}`;
+ li.className = logTypeClass;
+ logListElement.appendChild(li);
+ requestAnimationFrame(() => { logListElement.scrollTop = logListElement.scrollHeight; });
+ }
+
+ function updateFighterPanelUI(panelRole, fighterState, fighterBaseStats, isControlledByThisClient) {
+ const elements = uiElements[panelRole];
+ const config = window.GAME_CONFIG || {};
+
+ if (!elements || !elements.hpFill || !elements.hpText || !elements.resourceFill || !elements.resourceText || !elements.status || !fighterState || !fighterBaseStats) {
+ if (elements) {
+ if(elements.name) elements.name.innerHTML = (panelRole === 'player') ? ' Ожидание данных...' : ' Ожидание игрока...';
+ 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;
+ }
+ if (elements.panel) elements.panel.style.opacity = '1';
+
+ if (elements.name) {
+ let iconClass = 'fa-question';
+ const characterKey = fighterBaseStats.characterKey;
+ if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-elena'; }
+ else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; }
+ else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-balard'; }
+ let nameHtml = ` ${fighterBaseStats.name || 'Неизвестно'}`;
+ if (isControlledByThisClient) nameHtml += " (Вы)";
+ elements.name.innerHTML = nameHtml;
+ }
+
+ if (elements.avatar && fighterBaseStats.avatarPath) {
+ elements.avatar.src = fighterBaseStats.avatarPath;
+ elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard');
+ elements.avatar.classList.add(`avatar-${fighterBaseStats.characterKey}`);
+ } else if (elements.avatar) {
+ elements.avatar.src = 'images/default_avatar.png';
+ elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard');
+ }
+
+ const maxHp = Math.max(1, fighterBaseStats.maxHp);
+ const maxRes = Math.max(1, fighterBaseStats.maxResource);
+ const currentHp = Math.max(0, fighterState.currentHp);
+ const currentRes = Math.max(0, fighterState.currentResource);
+ elements.hpFill.style.width = `${(currentHp / maxHp) * 100}%`;
+ elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`;
+ elements.resourceFill.style.width = `${(currentRes / maxRes) * 100}%`;
+ elements.resourceText.textContent = `${currentRes} / ${fighterBaseStats.maxResource}`;
+
+ const resourceBarContainerToUpdate = (panelRole === 'player') ? uiElements.playerResourceBarContainer : uiElements.opponentResourceBarContainer;
+ const resourceIconElementToUpdate = (panelRole === 'player') ? uiElements.playerResourceTypeIcon : uiElements.opponentResourceTypeIcon;
+ if (resourceBarContainerToUpdate && resourceIconElementToUpdate) {
+ resourceBarContainerToUpdate.classList.remove('mana', 'stamina', 'dark-energy');
+ let resourceClass = 'mana'; let iconClass = 'fa-flask';
+ if (fighterBaseStats.resourceName === 'Ярость') { resourceClass = 'stamina'; iconClass = 'fa-fire-alt'; }
+ else if (fighterBaseStats.resourceName === 'Темная Энергия') { resourceClass = 'dark-energy'; iconClass = 'fa-skull'; }
+ resourceBarContainerToUpdate.classList.add(resourceClass);
+ resourceIconElementToUpdate.className = `fas ${iconClass}`;
+ }
+
+ const statusText = fighterState.isBlocking ? (config.STATUS_BLOCKING || 'Защищается') : (config.STATUS_READY || 'Готов(а)');
+ elements.status.textContent = statusText;
+ elements.status.classList.toggle(config.CSS_CLASS_BLOCKING || 'blocking', fighterState.isBlocking);
+
+ if (elements.panel) {
+ let borderColorVar = 'var(--panel-border)';
+ elements.panel.classList.remove('panel-elena', 'panel-almagest', 'panel-balard');
+ if (fighterBaseStats.characterKey === 'elena') { elements.panel.classList.add('panel-elena'); borderColorVar = 'var(--accent-player)'; }
+ else if (fighterBaseStats.characterKey === 'almagest') { elements.panel.classList.add('panel-almagest'); borderColorVar = 'var(--accent-almagest)'; }
+ else if (fighterBaseStats.characterKey === 'balard') { elements.panel.classList.add('panel-balard'); borderColorVar = 'var(--accent-opponent)'; }
+ let glowColorVar = 'rgba(0, 0, 0, 0.4)';
+ if (fighterBaseStats.characterKey === 'elena') glowColorVar = 'var(--panel-glow-player)';
+ else if (fighterBaseStats.characterKey === 'almagest') glowColorVar = 'var(--panel-glow-almagest)';
+ else if (fighterBaseStats.characterKey === 'balard') glowColorVar = 'var(--panel-glow-opponent)';
+ elements.panel.style.borderColor = borderColorVar;
+ elements.panel.style.boxShadow = `0 0 15px ${glowColorVar}, inset 0 0 10px rgba(0, 0, 0, 0.3)`;
+ }
+ }
+
+ function generateEffectsHTML(effectsArray) {
+ const config = window.GAME_CONFIG || {};
+ if (!effectsArray || effectsArray.length === 0) return 'Нет';
+ return effectsArray.map(eff => {
+ let effectClasses = config.CSS_CLASS_EFFECT || 'effect';
+ const title = `${eff.name}${eff.description ? ` - ${eff.description}` : ''} (Осталось: ${eff.turnsLeft} х.)`;
+ const displayText = `${eff.name} (${eff.turnsLeft} х.)`;
+ if (eff.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) effectClasses += ' effect-debuff';
+ else if (eff.type === config.ACTION_TYPE_BUFF || eff.type === config.ACTION_TYPE_HEAL) effectClasses += ' effect-buff';
+ else effectClasses += ' effect-info';
+ return `${displayText} `;
+ }).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 = { [config.ACTION_TYPE_BUFF]: 1, grantsBlock: 2, [config.ACTION_TYPE_HEAL]: 3, [config.ACTION_TYPE_DEBUFF]: 4, [config.ACTION_TYPE_DISABLE]: 5 };
+ const sortEffects = (a, b) => {
+ let orderA = typeOrder[a.type] || 99; if (a.grantsBlock) orderA = typeOrder.grantsBlock; if (a.isFullSilence || a.id.startsWith('playerSilencedOn_')) orderA = typeOrder[config.ACTION_TYPE_DISABLE];
+ let orderB = typeOrder[b.type] || 99; if (b.grantsBlock) orderB = typeOrder.grantsBlock; if (b.isFullSilence || b.id.startsWith('playerSilencedOn_')) orderB = typeOrder[config.ACTION_TYPE_DISABLE];
+ return (orderA || 99) - (orderB || 99);
+ };
+
+ if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList && myState && myState.activeEffects) {
+ const myBuffs = []; const myDebuffs = [];
+ myState.activeEffects.forEach(e => {
+ const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL;
+ 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 myDebuffs.push(e);
+ });
+ myBuffs.sort(sortEffects); myDebuffs.sort(sortEffects);
+ uiElements.player.buffsList.innerHTML = generateEffectsHTML(myBuffs);
+ uiElements.player.debuffsList.innerHTML = generateEffectsHTML(myDebuffs);
+ } else if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList) {
+ uiElements.player.buffsList.innerHTML = 'Нет'; uiElements.player.debuffsList.innerHTML = 'Нет';
+ }
+
+ if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList && opponentState && opponentState.activeEffects) {
+ const opponentBuffs = []; const opponentDebuffs = [];
+ opponentState.activeEffects.forEach(e => {
+ const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL;
+ 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 opponentDebuffs.push(e);
+ });
+ opponentBuffs.sort(sortEffects); opponentDebuffs.sort(sortEffects);
+ uiElements.opponent.buffsList.innerHTML = generateEffectsHTML(opponentBuffs);
+ uiElements.opponent.debuffsList.innerHTML = generateEffectsHTML(opponentDebuffs);
+ } else if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList) {
+ uiElements.opponent.buffsList.innerHTML = 'Нет'; uiElements.opponent.debuffsList.innerHTML = 'Нет';
+ }
+ }
+
+ function updateTurnTimerDisplay(remainingTimeMs, isCurrentPlayerActualTurn, gameMode) {
+ const timerSpan = uiElements.controls.turnTimerSpan;
+ const timerContainer = uiElements.controls.turnTimerContainer;
+
+ if (!timerSpan || !timerContainer) return;
+
+ if (window.gameState && window.gameState.isGameOver) {
+ timerContainer.style.display = 'block';
+ timerSpan.textContent = 'Конец';
+ timerSpan.classList.remove('low-time');
+ return;
+ }
+
+ if (remainingTimeMs === null || remainingTimeMs === undefined) {
+ timerContainer.style.display = 'block';
+ timerSpan.classList.remove('low-time');
+ if (gameMode === 'ai' && !isCurrentPlayerActualTurn) {
+ timerSpan.textContent = 'Ход ИИ';
+ } else if (gameMode === 'pvp' && !isCurrentPlayerActualTurn) {
+ timerSpan.textContent = 'Ход оппонента';
+ } else {
+ timerSpan.textContent = '--';
+ }
+ } else {
+ timerContainer.style.display = 'block';
+ const seconds = Math.ceil(remainingTimeMs / 1000);
+ timerSpan.textContent = `0:${seconds < 10 ? '0' : ''}${seconds}`;
+
+ if (seconds <= 10 && isCurrentPlayerActualTurn) {
+ timerSpan.classList.add('low-time');
+ } else {
+ timerSpan.classList.remove('low-time');
+ }
+ }
+ }
+
+
+ function updateUI() {
+ const currentGameState = window.gameState;
+ const gameDataGlobal = window.gameData;
+ const configGlobal = window.GAME_CONFIG;
+ const myActualPlayerId = window.myPlayerId;
+
+ if (!currentGameState || !gameDataGlobal || !configGlobal || !myActualPlayerId) {
+ updateFighterPanelUI('player', null, null, true);
+ updateFighterPanelUI('opponent', null, null, false);
+ if(uiElements.gameHeaderTitle) uiElements.gameHeaderTitle.innerHTML = `Ожидание данных... `;
+ 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 = 'Загрузка способностей...
';
+ if (uiElements.controls.turnTimerContainer) uiElements.controls.turnTimerContainer.style.display = 'none';
+ if (uiElements.controls.turnTimerSpan) {
+ uiElements.controls.turnTimerSpan.textContent = '--';
+ uiElements.controls.turnTimerSpan.classList.remove('low-time');
+ }
+ return;
+ }
+ if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) {
+ console.warn("updateUI: Некоторые базовые uiElements не найдены.");
+ return;
+ }
+
+ const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID;
+ const opponentActualSlotId = myActualPlayerId === configGlobal.PLAYER_ID ? configGlobal.OPPONENT_ID : configGlobal.PLAYER_ID;
+ const myStateInGameState = currentGameState[myActualPlayerId];
+ const myBaseStatsForUI = gameDataGlobal.playerBaseStats;
+ if (myStateInGameState && myBaseStatsForUI) updateFighterPanelUI('player', myStateInGameState, myBaseStatsForUI, true);
+ else updateFighterPanelUI('player', null, null, true);
+
+ const opponentStateInGameState = currentGameState[opponentActualSlotId];
+ const opponentBaseStatsForUI = gameDataGlobal.opponentBaseStats;
+ const isOpponentPanelDissolving = uiElements.opponent.panel?.classList.contains('dissolving');
+ if (opponentStateInGameState && opponentBaseStatsForUI) {
+ if (uiElements.opponent.panel && (uiElements.opponent.panel.style.opacity !== '1' || (uiElements.opponent.panel.classList.contains('dissolving') && currentGameState.isGameOver === false) )) {
+ const panel = uiElements.opponent.panel;
+ if (panel.classList.contains('dissolving')) {
+ panel.classList.remove('dissolving'); panel.style.transition = 'none'; panel.offsetHeight;
+ panel.style.opacity = '1'; panel.style.transform = 'scale(1) translateY(0)'; panel.style.transition = '';
+ } else { panel.style.opacity = '1'; panel.style.transform = 'scale(1) translateY(0)'; }
+ } else if (uiElements.opponent.panel && !isOpponentPanelDissolving) {
+ uiElements.opponent.panel.style.opacity = '1';
+ }
+ updateFighterPanelUI('opponent', opponentStateInGameState, opponentBaseStatsForUI, false);
+ } else {
+ if (!isOpponentPanelDissolving) updateFighterPanelUI('opponent', null, null, false);
+ else console.log("[UI UPDATE DEBUG] Opponent panel is dissolving, skipping content update.");
+ }
+
+ updateEffectsUI(currentGameState);
+
+ if (uiElements.gameHeaderTitle && gameDataGlobal.playerBaseStats && gameDataGlobal.opponentBaseStats) {
+ const myName = gameDataGlobal.playerBaseStats.name; const opponentName = gameDataGlobal.opponentBaseStats.name;
+ const myKey = gameDataGlobal.playerBaseStats.characterKey; const opponentKey = gameDataGlobal.opponentBaseStats.characterKey;
+ let myClass = 'title-player'; let opponentClass = 'title-opponent';
+ if (myKey === 'elena') myClass = 'title-enchantress'; else if (myKey === 'almagest') myClass = 'title-sorceress'; else if (myKey === 'balard') myClass = 'title-knight';
+ if (opponentKey === 'elena') opponentClass = 'title-enchantress'; else if (opponentKey === 'almagest') opponentClass = 'title-sorceress'; else if (opponentKey === 'balard') opponentClass = 'title-knight';
+ uiElements.gameHeaderTitle.innerHTML = `${myName} ${opponentName} `;
+ } 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 = `${myName} Ожидание игрока... `;
+ }
+
+ const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId;
+ 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 || 'Неизвестно'}`;
+ uiElements.controls.turnIndicator.style.color = (currentTurnActor?.id === myActualPlayerId) ? 'var(--turn-color)' : 'var(--text-muted)';
+ } else {
+ uiElements.controls.turnIndicator.textContent = "Игра окончена";
+ uiElements.controls.turnIndicator.style.color = 'var(--text-muted)';
+ }
+ }
+
+ if (uiElements.controls.buttonAttack) {
+ uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive);
+ const myCharKey = gameDataGlobal.playerBaseStats?.characterKey;
+ let attackBuffId = null;
+ if (myCharKey === 'elena') attackBuffId = configGlobal.ABILITY_ID_NATURE_STRENGTH;
+ else if (myCharKey === 'almagest') attackBuffId = configGlobal.ABILITY_ID_ALMAGEST_BUFF_ATTACK;
+ if (attackBuffId && myCharacterState && myCharacterState.activeEffects) {
+ 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);
+ } else { uiElements.controls.buttonAttack.classList.remove(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed'); }
+ }
+ if (uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true;
+
+ const actingPlayerState = myCharacterState;
+ const actingPlayerAbilities = gameDataGlobal.playerAbilities;
+ const actingPlayerResourceName = gameDataGlobal.playerBaseStats?.resourceName;
+ const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId];
+
+ uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => {
+ 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;
+ 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;
+ }
+ const hasEnoughResource = actingPlayerState.currentResource >= abilityDataFromGameData.cost;
+ const isOnCooldown = (actingPlayerState.abilityCooldowns?.[abilityId] || 0) > 0;
+ const isGenerallySilenced = actingPlayerState.activeEffects?.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
+ const isAbilitySpecificallySilenced = actingPlayerState.disabledAbilities?.some(dis => dis.abilityId === abilityId && dis.turnsLeft > 0);
+ const isSilenced = isGenerallySilenced || isAbilitySpecificallySilenced;
+ const silenceTurnsLeft = isAbilitySpecificallySilenced ? (actingPlayerState.disabledAbilities?.find(dis => dis.abilityId === abilityId)?.turnsLeft || 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 effectIdForDebuff = 'effect_' + abilityId;
+ const isDebuffAlreadyOnTarget = isTargetedDebuffAbility && opponentStateForDebuffCheck && opponentStateForDebuffCheck.activeEffects?.some(e => e.id === effectIdForDebuff);
+ button.disabled = !hasEnoughResource || isBuffAlreadyActive || isSilenced || isOnCooldown || isDebuffAlreadyOnTarget;
+ 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 (isOnCooldown) {
+ button.classList.add(configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
+ if (cooldownDisplay) { cooldownDisplay.textContent = `КД: ${actingPlayerState.abilityCooldowns[abilityId]}`; cooldownDisplay.style.display = 'block'; }
+ } else if (isSilenced) {
+ button.classList.add(configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced');
+ if (cooldownDisplay) { const icon = isGenerallySilenced ? '🔕' : '🔇'; cooldownDisplay.textContent = `${icon} ${silenceTurnsLeft}`; cooldownDisplay.style.display = 'block'; }
+ } else {
+ if (cooldownDisplay) cooldownDisplay.style.display = 'none';
+ if (!isOnCooldown && !isSilenced) {
+ 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);
+ }
+ }
+ 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}`;
+ let abilityBaseCooldown = abilityDataFromGameData.cooldown;
+ if (typeof abilityBaseCooldown === 'number' && abilityBaseCooldown > 0) titleText += ` (Исходный КД: ${abilityBaseCooldown} х.)`;
+ if (isOnCooldown) titleText += ` | На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[abilityId]} х.`;
+ if (isSilenced) titleText += ` | Под безмолвием! Осталось: ${silenceTurnsLeft} х.`;
+ if (isBuffAlreadyActive) {
+ const activeEffect = actingPlayerState.activeEffects?.find(eff => eff.id === abilityId);
+ 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) {
+ const activeDebuff = opponentStateForDebuffCheck.activeEffects?.find(e => e.id === 'effect_' + abilityId);
+ titleText += ` | Эффект уже наложен на ${gameDataGlobal.opponentBaseStats?.name || 'противника'}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}.`;
+ }
+ if (!hasEnoughResource) titleText += ` | Недостаточно ${actingPlayerResourceName} (${actingPlayerState.currentResource}/${abilityDataFromGameData.cost})`;
+ button.setAttribute('title', titleText);
+ });
+ }
+
+ function showGameOver(playerWon, reason = "", opponentCharacterKeyFromClient = null, data = null) {
+ const config = window.GAME_CONFIG || {};
+ const clientSpecificGameData = window.gameData;
+ const currentActualGameState = window.gameState;
+ const gameOverScreenElement = uiElements.gameOver.screen;
+
+ if (!gameOverScreenElement) { return; }
+
+ const resultMsgElement = uiElements.gameOver.message;
+ const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок";
+ const opponentNameForResult = clientSpecificGameData?.opponentBaseStats?.name || "Противник";
+
+ if (resultMsgElement) {
+ let winText = `Победа! ${myNameForResult} празднует!`;
+ let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`;
+ if (reason === 'opponent_disconnected') {
+ let disconnectedName = data?.disconnectedCharacterName || opponentNameForResult;
+ winText = `${disconnectedName} покинул(а) игру. Победа присуждается вам!`;
+ } else if (reason === 'turn_timeout') {
+ if (!playerWon) {
+ loseText = `Время на ход истекло! Поражение. ${opponentNameForResult} побеждает!`;
+ } else {
+ winText = `Время на ход у ${opponentNameForResult} истекло! Победа!`;
+ }
+ }
+ resultMsgElement.textContent = playerWon ? winText : loseText;
+ resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)';
+ }
+
+ const opponentPanelElement = uiElements.opponent.panel;
+ if (opponentPanelElement) {
+ opponentPanelElement.classList.remove('dissolving');
+ opponentPanelElement.style.transition = 'none'; opponentPanelElement.offsetHeight;
+ const loserCharacterKeyForDissolve = data?.loserCharacterKey;
+ if (currentActualGameState && currentActualGameState.isGameOver === true && playerWon) {
+ if (loserCharacterKeyForDissolve === 'balard' || loserCharacterKeyForDissolve === 'almagest') {
+ opponentPanelElement.classList.add('dissolving');
+ opponentPanelElement.style.opacity = '0';
+ } else {
+ opponentPanelElement.style.opacity = '1'; opponentPanelElement.style.transform = 'scale(1) translateY(0)';
+ }
+ } else {
+ opponentPanelElement.style.opacity = '1'; opponentPanelElement.style.transform = 'scale(1) translateY(0)';
+ }
+ opponentPanelElement.style.transition = '';
+ }
+
+ setTimeout((finalStateInTimeout) => {
+ if (gameOverScreenElement && finalStateInTimeout && finalStateInTimeout.isGameOver === true) {
+ if (gameOverScreenElement.classList.contains(config.CSS_CLASS_HIDDEN || 'hidden')) {
+ gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden');
+ }
+ if(window.getComputedStyle(gameOverScreenElement).display === 'none') gameOverScreenElement.style.display = 'flex';
+ gameOverScreenElement.style.opacity = '0';
+ requestAnimationFrame(() => {
+ gameOverScreenElement.style.opacity = '1';
+ 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';
+ uiElements.gameOver.modalContent.style.transform = 'scale(1) translateY(0)';
+ uiElements.gameOver.modalContent.style.opacity = '1';
+ }
+ });
+ } else {
+ if (gameOverScreenElement) {
+ gameOverScreenElement.style.transition = 'none';
+ if (uiElements.gameOver.modalContent) uiElements.gameOver.modalContent.style.transition = 'none';
+ gameOverScreenElement.classList.add(config.CSS_CLASS_HIDDEN || 'hidden');
+ gameOverScreenElement.style.opacity = '0';
+ if (uiElements.gameOver.modalContent) {
+ uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)';
+ uiElements.gameOver.modalContent.style.opacity = '0';
+ }
+ gameOverScreenElement.offsetHeight;
+ }
+ }
+ }, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState);
+ }
+
+ // === НОВАЯ ФУНКЦИЯ для настройки переключателя панелей ===
+ function setupPanelSwitcher() {
+ const { showPlayerBtn, showOpponentBtn } = uiElements.panelSwitcher;
+ const battleArena = uiElements.battleArenaContainer;
+
+ if (showPlayerBtn && showOpponentBtn && battleArena) {
+ showPlayerBtn.addEventListener('click', () => {
+ battleArena.classList.remove('show-opponent-panel');
+ showPlayerBtn.classList.add('active');
+ showOpponentBtn.classList.remove('active');
+ });
+
+ showOpponentBtn.addEventListener('click', () => {
+ battleArena.classList.add('show-opponent-panel');
+ showOpponentBtn.classList.add('active');
+ showPlayerBtn.classList.remove('active');
+ });
+
+ // По умолчанию при загрузке (если кнопки видимы) панель игрока активна
+ // CSS уже должен это обеспечивать, но для надежности можно убедиться
+ if (window.getComputedStyle(uiElements.panelSwitcher.controlsContainer).display !== 'none') {
+ battleArena.classList.remove('show-opponent-panel');
+ showPlayerBtn.classList.add('active');
+ showOpponentBtn.classList.remove('active');
+ }
+ }
+ }
+ // === КОНЕЦ НОВОЙ ФУНКЦИИ ===
+
+ window.gameUI = {
+ uiElements,
+ addToLog,
+ updateUI,
+ showGameOver,
+ updateTurnTimerDisplay
+ };
+
+ // Настраиваем переключатель панелей при загрузке скрипта
+ setupPanelSwitcher();
+
})();
\ No newline at end of file
diff --git a/public/style_alt.css b/public/style_alt.css
index ba69ff7..1c5a757 100644
--- a/public/style_alt.css
+++ b/public/style_alt.css
@@ -1,1399 +1,704 @@
-/* === style_alt.css (Изменения для user-info и game-header) === */
-@import url('https://fonts.googleapis.com/css2?family=MedievalSharp&family=Roboto:wght@300;400;700&display=swap');
-@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css');
-
-:root {
- /* --- Переменные цветов и шрифтов (из локальной версии) --- */
- --font-main: 'Roboto', sans-serif;
- --font-fancy: 'MedievalSharp', cursive;
-
- --bg-gradient-dark: linear-gradient(160deg, #1f243a, #10121c);
- --panel-bg: rgba(16, 18, 28, 0.8);
- --panel-border: #4a5072;
-
- --panel-glow-player: rgba(80, 150, 255, 0.3);
- --panel-glow-opponent: rgba(255, 80, 80, 0.3);
- --panel-glow-almagest: rgba(199, 108, 255, 0.3);
-
-
- --text-light: #e8effc;
- --text-muted: #9badce;
- --text-heading: #ffffff;
-
- --accent-player: #6c95ff;
- --accent-opponent: #ff6c6c;
- --accent-almagest: #c76cff;
-
- --hp-color: #de4b4b;
- --mana-color: #58a8d0;
- --stamina-color: #ffb347;
- --dark-energy-color: #ab47bc;
- --bar-bg: #252a44;
-
- --button-bg: linear-gradient(145deg, #556190, #3f4a70);
- --button-hover-bg: linear-gradient(145deg, #6a79b0, #556190);
- --button-text: var(--text-light);
-
- --button-ability-bg: linear-gradient(145deg, #305a5e, #1f4043);
- --button-ability-hover-bg: linear-gradient(145deg, #407a7e, #305a5e);
- --button-ability-border: #4db0b5;
-
- --button-disabled-bg: #333950;
- --button-disabled-text: #6b7491;
-
- --log-bg: rgba(10, 12, 20, 0.85);
- --log-border: var(--panel-border);
- --log-text: var(--text-muted);
-
- --icon-color: var(--text-muted);
- --damage-color: #ff8080;
- --heal-color: #90ee90;
- --block-color: #add8e6;
- --effect-color: #d8bfd8;
- --turn-color: #ffd700;
- --system-color: #7fffd4;
-
- --modal-bg: rgba(16, 18, 28, 0.97);
- --modal-content-bg: #2a2f45;
-
- --scrollbar-thumb: #4a5072;
- --scrollbar-track: #10121c;
-
- --shake-duration: 0.4s;
- --cast-duration: 0.6s;
- --dissolve-duration: 6.0s;
-
- --log-panel-fixed-height: 280px;
-
- --timer-text-color: var(--turn-color);
- --timer-icon-color: #b0c4de;
- --timer-low-time-color: var(--damage-color);
-
- /* === Переменные для переключателя панелей (мобильный вид) - ИЗ СЕРВЕРНОЙ ВЕРСИИ === */
- --panel-switcher-bg: rgba(10, 12, 20, 0.9);
- --panel-switcher-border: var(--panel-border);
- --panel-switcher-button-bg: var(--button-bg);
- --panel-switcher-button-text: var(--button-text);
- --panel-switcher-button-active-bg: var(--accent-player);
- --panel-switcher-button-active-text: #fff;
-}
-
-/* --- Базовые Стили и Сброс --- */
-* {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
-}
-
-html {
- height: 100%;
-}
-
-body {
- font-family: var(--font-main);
- background: var(--bg-gradient-dark) fixed;
- color: var(--text-light);
- line-height: 1.5;
- height: 100vh;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 10px;
-}
-
-h1, h2, h3, h4 {
- font-family: var(--font-fancy);
- color: var(--text-heading);
- margin-bottom: 0.75em;
- font-weight: normal;
-}
-
-button {
- font-family: var(--font-main);
-}
-
-i {
- margin-right: 6px;
- color: var(--icon-color);
- width: 1.2em;
- text-align: center;
-}
-
-* {
- scrollbar-width: thin;
- scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
-}
-
-*::-webkit-scrollbar {
- width: 8px;
-}
-
-*::-webkit-scrollbar-track {
- background: var(--scrollbar-track);
- border-radius: 4px;
-}
-
-*::-webkit-scrollbar-thumb {
- background-color: var(--scrollbar-thumb);
- border-radius: 4px;
- border: 2px solid var(--scrollbar-track);
-}
-
-
-/* === Стили для Экранов Аутентификации и Настройки Игры (из локальной версии) === */
-.auth-game-setup-wrapper {
- width: 100%;
- max-width: 700px;
- margin: 20px auto;
- background: var(--panel-bg);
- border: 1px solid var(--panel-border);
- border-radius: 10px;
- box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5);
- color: var(--text-light);
- text-align: center;
- max-height: calc(100vh - 40px);
- overflow-y: hidden; /* Сохраняем из локальной */
- position: relative; /* <<< Добавлено для позиционирования #user-info (из локальной) */
-}
-
-.auth-game-setup-wrapper h2,
-.auth-game-setup-wrapper h3 {
- font-family: var(--font-fancy);
- color: var(--text-heading);
- margin-bottom: 1em;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- padding-bottom: 0.5em;
-}
-
-.auth-game-setup-wrapper h3 {
- font-size: 1.2em;
- margin-top: 1.5em;
-}
-
-.auth-game-setup-wrapper button,
-#auth-section form button {
- font-family: var(--font-main);
- background: var(--button-bg);
- color: var(--button-text);
- border: 1px solid rgba(0, 0, 0, 0.3);
- border-radius: 6px;
- padding: 10px 18px;
- margin: 8px 5px;
- cursor: pointer;
- transition: all 0.15s ease;
- font-weight: bold;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
- outline: none;
-}
-
-.auth-game-setup-wrapper button:hover:enabled,
-#auth-section form button:hover:enabled {
- background: var(--button-hover-bg);
- transform: translateY(-2px) scale(1.02);
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
-}
-
-.auth-game-setup-wrapper button:active:enabled,
-#auth-section form button:active:enabled {
- transform: translateY(0px) scale(1);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
-}
-
-.auth-game-setup-wrapper button:disabled,
-#auth-section form button:disabled {
- background: var(--button-disabled-bg) !important;
- color: var(--button-disabled-text) !important;
- border-color: transparent !important;
- cursor: not-allowed !important;
- opacity: 0.7;
- transform: none !important;
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4) !important;
- filter: grayscale(50%);
-}
-
-.auth-game-setup-wrapper input[type="text"],
-#auth-section input[type="text"],
-#auth-section input[type="password"] {
- padding: 10px;
- border-radius: 5px;
- border: 1px solid var(--panel-border);
- background-color: var(--bar-bg);
- color: var(--text-light);
- margin: 5px 5px 10px 5px;
- font-size: 0.9em;
- width: calc(100% - 22px);
- max-width: 300px;
- box-sizing: border-box;
- outline: none;
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
-}
-
-.auth-game-setup-wrapper input[type="text"]:focus,
-#auth-section input[type="text"]:focus,
-#auth-section input[type="password"]:focus {
- border-color: var(--accent-player);
- box-shadow: 0 0 8px rgba(108, 149, 255, 0.4);
-}
-
-#available-games-list {
- margin-top: 20px;
- text-align: left;
- max-height: 250px;
- height: 100px; /* Сохраняем из локальной */
- overflow-y: scroll;
- padding: 10px 15px;
- background-color: rgba(0, 0, 0, 0.25);
- border: 1px solid var(--log-border);
- border-radius: 6px;
-}
-
-#available-games-list h3 {
- margin-top: 0;
- margin-bottom: 10px;
- padding-bottom: 5px;
- border-bottom: 1px dashed rgba(255, 255, 255, 0.1);
-}
-
-#available-games-list ul {
- list-style: none;
- padding: 0;
- margin: 0;
-}
-
-#available-games-list li {
- padding: 10px;
- border-bottom: 1px solid rgba(74, 80, 114, 0.5);
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 0.9em;
-}
-
-#available-games-list li:last-child {
- border-bottom: none;
-}
-
-#available-games-list li button {
- padding: 6px 10px;
- font-size: 0.8em;
- margin-left: 10px;
- flex-shrink: 0;
-}
-
-#status-container {
- height: 40px; /* Сохраняем из локальной */
-}
-
-#auth-message,
-#game-status-message {
- font-weight: bold;
- font-size: 1.1em;
- padding: 5px;
- background-color: rgba(0, 0, 0, 0.1);
- border-radius: 4px;
- display: block;
- margin-bottom: 5px;
- text-align: center;
-}
-
-#auth-message.success {
- color: var(--heal-color);
-}
-
-#auth-message.error {
- color: var(--damage-color);
-}
-
-#game-status-message {
- color: var(--turn-color);
-}
-
-
-#auth-section form {
- margin-bottom: 20px;
-}
-
-/* === ИЗМЕНЕНИЕ: Стили для #user-info (из локальной версии) === */
-#user-info {
- position: absolute;
- top: 10px; /* Отступ сверху */
- right: 15px; /* Отступ справа */
- line-height: 1.5;
- text-align: right; /* Выравнивание текста и кнопки вправо */
- z-index: 10; /* Чтобы был поверх другого контента в .auth-game-setup-wrapper */
-}
-
-#user-info p {
- margin: 0 10px 0 0; /* Уменьшен нижний отступ */
- font-size: 0.9em; /* Уменьшен шрифт приветствия */
- color: var(--text-muted);
- line-height: 2.5;
-}
-
-#user-info p #logged-in-username {
- font-weight: bold;
- color: var(--text-light);
-}
-
-#user-info div {
- display: flex;
- flex-direction: row;
-}
-
-#logout-button {
- background: linear-gradient(145deg, #6e3c3c, #502626) !important; /* Более темный красный */
- color: #f0d0d0 !important; /* Светло-розовый текст */
- padding: 6px 12px !important; /* Уменьшены паддинги */
- font-size: 0.8em !important; /* Уменьшен шрифт */
- margin: 0 !important; /* Убираем внешние отступы */
- letter-spacing: 0.2px;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
- border: 1px solid #422020 !important; /* Темная рамка */
-}
-
-#logout-button:hover:enabled {
- background: linear-gradient(145deg, #834545, #6e3c3c) !important;
- transform: translateY(-1px) !important;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4) !important;
-}
-#logout-button i {
- margin-right: 4px; /* Уменьшен отступ иконки */
-}
-/* === КОНЕЦ ИЗМЕНЕНИЯ === */
-
-
-.character-selection {
- margin-top: 15px;
- margin-bottom: 15px;
- padding: 15px;
- background-color: rgba(0, 0, 0, 0.2);
- border-radius: 6px;
- border: 1px solid rgba(74, 80, 114, 0.5);
-}
-
-.character-selection h4 {
- font-size: 1.1em;
- color: var(--text-muted);
- margin-bottom: 10px;
- border: none;
- padding: 0;
- text-align: center;
-}
-
-.character-selection label {
- display: inline-block;
- margin: 0 15px;
- cursor: pointer;
- font-size: 1.05em;
- padding: 5px 10px;
- border-radius: 4px;
- transition: background-color 0.2s ease, color 0.2s ease;
- user-select: none;
-}
-
-.character-selection input[type="radio"] {
- display: none;
-}
-
-.character-selection input[type="radio"]:checked + label {
- color: #fff;
- font-weight: bold;
-}
-
-.character-selection input[type="radio"][value="elena"]:checked + label {
- background-color: var(--accent-player);
- box-shadow: 0 0 8px rgba(108, 149, 255, 0.5);
-}
-
-.character-selection input[type="radio"][value="almagest"]:checked + label {
- background-color: var(--accent-almagest);
- box-shadow: 0 0 8px rgba(199, 108, 255, 0.5);
-}
-
-.character-selection label:hover {
- background-color: rgba(255, 255, 255, 0.1);
-}
-
-.character-selection label i {
- margin-right: 8px;
- vertical-align: middle;
-}
-
-label[for="char-elena"] i {
- color: var(--accent-player);
-}
-
-label[for="char-almagest"] i {
- color: var(--accent-almagest);
-}
-
-/* --- Основная Структура Игры (.game-wrapper) --- */
-.game-wrapper {
- width: 100%;
- height: 100%;
- max-width: 1400px;
- margin: 0 auto;
- padding: 10px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- overflow: hidden;
-}
-
-/* === ИЗМЕНЕНИЕ: .game-header удален, стили для него больше не нужны (из локальной версии) === */
-
-/* Глобальные стили для кнопок переключения панелей - ИЗ СЕРВЕРНОЙ ВЕРСИИ */
-.panel-switcher-controls {
- display: none; /* Скрыт по умолчанию для десктопа */
- flex-shrink: 0;
- padding: 8px 5px;
- background: var(--panel-switcher-bg);
- border-bottom: 1px solid var(--panel-switcher-border);
- gap: 10px;
-}
-.panel-switch-button {
- flex: 1;
- padding: 8px 10px;
- font-size: 0.9em;
- font-weight: bold;
- text-transform: uppercase;
- background: var(--panel-switcher-button-bg);
- color: var(--panel-switcher-button-text);
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 5px;
- cursor: pointer;
- transition: background-color 0.2s, color 0.2s, transform 0.1s;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-.panel-switch-button i { margin-right: 8px; }
-.panel-switch-button:hover { filter: brightness(1.1); }
-.panel-switch-button.active {
- background: var(--panel-switcher-button-active-bg);
- color: var(--panel-switcher-button-active-text);
- box-shadow: 0 0 8px rgba(255,255,255,0.3);
-}
-
-.battle-arena-container {
- flex-grow: 1;
- display: flex;
- gap: 10px;
- overflow: hidden;
- /* === ИЗМЕНЕНИЕ: Добавляем верхний отступ, если .game-header был убран, а .game-wrapper виден (из локальной версии) === */
- /* margin-top: 10px; /* или padding-top: 10px; на .game-wrapper, если нужно */
- /* === Изменения из серверной для работы переключения панелей === */
- position: relative;
- min-height: 0;
-}
-
-.player-column,
-.opponent-column {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 10px;
- min-width: 0;
- overflow: hidden;
-}
-
-/* Остальные стили панелей, кнопок, лога и т.д. из локальной версии */
-.fighter-panel,
-.controls-panel-new,
-.battle-log-new {
- background: var(--panel-bg);
- border: 1px solid var(--panel-border);
- border-radius: 8px;
- box-shadow: 0 0 15px rgba(0, 0, 0, 0.4), inset 0 0 10px rgba(0, 0, 0, 0.3);
- padding: 15px;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- transition: box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease-out, transform 0.3s ease-out;
-}
-
-.fighter-panel.panel-elena {
- border-color: var(--accent-player);
-}
-.fighter-panel.panel-almagest {
- border-color: var(--accent-almagest);
-}
-.fighter-panel.panel-balard {
- border-color: var(--accent-opponent);
-}
-
-
-.panel-header {
- flex-shrink: 0;
- display: flex;
- align-items: center;
- gap: 10px;
- padding-bottom: 10px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- margin-bottom: 0;
-}
-
-.fighter-name {
- font-size: 1.6em;
- margin: 0;
- flex-grow: 1;
- text-align: left;
-}
-
-.fighter-name .icon-elena { color: var(--accent-player); }
-.fighter-name .icon-almagest { color: var(--accent-almagest); }
-.fighter-name .icon-balard { color: var(--accent-opponent); }
-
-
-.character-visual {
- flex-shrink: 0;
- margin-bottom: 0;
-}
-
-.avatar-image {
- display: block;
- max-width: 50px;
- height: auto;
- border-radius: 50%;
- border: 2px solid var(--panel-border);
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
-}
-
-.avatar-image.avatar-elena { border-color: var(--accent-player); }
-.avatar-image.avatar-almagest { border-color: var(--accent-almagest); }
-.avatar-image.avatar-balard { border-color: var(--accent-opponent); }
-
-
-.panel-content {
- flex-grow: 1;
- overflow-y: auto;
- padding-right: 5px;
- display: flex;
- flex-direction: column;
- gap: 10px; /* Добавлено из серверной версии для консистентности */
- min-height: 0;
- padding-top: 10px;
- margin-top: 0;
-}
-
-.stat-bar-container {
- display: flex;
- align-items: center;
- gap: 10px;
- flex-shrink: 0;
-}
-
-.stat-bar-container .bar-icon {
- flex-shrink: 0;
- font-size: 1.4em;
-}
-.stat-bar-container.health .bar-icon { color: var(--hp-color); }
-.stat-bar-container.mana .bar-icon { color: var(--mana-color); }
-.stat-bar-container.stamina .bar-icon { color: var(--stamina-color); }
-.stat-bar-container.dark-energy .bar-icon { color: var(--dark-energy-color); }
-
-
-.bar-wrapper {
- flex-grow: 1;
-}
-
-.bar {
- border-radius: 4px;
- height: 20px;
- border: 1px solid rgba(0, 0, 0, 0.5);
- overflow: hidden;
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
- position: relative;
- background-color: var(--bar-bg);
-}
-
-.bar-fill {
- display: block;
- height: 100%;
- border-radius: 3px;
- position: relative;
- z-index: 2;
- transition: width 0.4s ease-out;
-}
-
-.bar-text {
- position: absolute;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- z-index: 3;
- display: flex;
- justify-content: center;
- align-items: center;
- font-size: 0.75em;
- font-weight: bold;
- color: #fff;
- text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.9);
- padding: 0 5px;
- white-space: nowrap;
- pointer-events: none;
-}
-.health .bar-fill { background-color: var(--hp-color); }
-.mana .bar-fill { background-color: var(--mana-color); }
-.stamina .bar-fill { background-color: var(--stamina-color); }
-.dark-energy .bar-fill { background-color: var(--dark-energy-color); }
-
-
-.status-area {
- font-size: 0.9em;
- display: flex;
- align-items: baseline;
- gap: 5px;
- flex-shrink: 0;
- min-height: 1.5em;
-}
-
-.status-area .icon-status {
- font-size: 1em;
- flex-shrink: 0;
- margin-top: 0.1em;
-}
-
-.status-area strong {
- color: var(--text-muted);
- font-weight: normal;
- flex-shrink: 0;
- margin-right: 3px;
-}
-
-.status-area span {
- font-weight: bold;
-}
-
-.status-area span.blocking {
- color: var(--block-color);
- font-style: italic;
-}
-
-.effects-area {
- font-size: 0.9em;
- display: flex;
- flex-direction: column;
- gap: 8px; /* Добавлено из серверной версии для консистентности */
- flex-shrink: 0;
- min-height: 3em;
-}
-
-.effect-category {
- display: flex;
- align-items: baseline;
- gap: 5px;
-}
-
-.effect-category strong {
- color: var(--text-muted);
- font-weight: normal;
- font-family: var(--font-main);
- font-size: 0.9em;
- flex-shrink: 0;
- margin-right: 3px;
-}
-
-.effect-category .icon-effects-buff,
-.effect-category .icon-effects-debuff {
- font-size: 1em;
- flex-shrink: 0;
- margin-top: 0.1em;
- width: 1.2em;
- text-align: center;
-}
-
-.effect-category .icon-effects-buff { color: var(--heal-color); }
-.effect-category .icon-effects-debuff { color: var(--damage-color); }
-
-.effect-list {
- display: inline;
- line-height: 1.4;
- min-width: 0;
- font-weight: bold;
-}
-
-.effect {
- display: inline-block;
- margin: 2px 3px 2px 0;
- padding: 1px 6px;
- font-size: 0.8em;
- border-radius: 10px;
- border: 1px solid;
- cursor: default;
- font-weight: 600;
- background-color: rgba(0, 0, 0, 0.2);
- white-space: nowrap;
- vertical-align: baseline;
-}
-.effect-buff { border-color: var(--heal-color); color: var(--heal-color); }
-.effect-debuff { border-color: var(--damage-color); color: var(--damage-color); }
-.effect-stun { border-color: var(--turn-color); color: var(--turn-color); }
-.effect-block { border-color: var(--block-color); color: var(--block-color); }
-.effect-info { border-color: var(--text-muted); color: var(--text-muted); }
-
-.controls-panel-new {
- flex-grow: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
-}
-
-#turn-indicator {
- flex-shrink: 0;
- text-align: center;
- font-size: 1.4em;
- margin-bottom: 10px;
- padding-bottom: 8px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- transition: color 0.3s ease;
-}
-
-.turn-timer-display {
- flex-shrink: 0;
- text-align: center;
- font-size: 0.9em;
- color: var(--timer-text-color);
- margin-top: -5px;
- margin-bottom: 10px;
- padding: 5px;
- background-color: rgba(0,0,0,0.15);
- border-radius: 4px;
- border-top: 1px solid rgba(255,255,255,0.05);
-}
-
-.turn-timer-display i {
- color: var(--timer-icon-color);
- margin-right: 8px;
-}
-
-#turn-timer {
- font-weight: bold;
- font-size: 1.1em;
- min-width: 35px;
- display: inline-block;
- text-align: left;
-}
-
-#turn-timer.low-time {
- color: var(--timer-low-time-color);
- animation: pulse-timer-warning 1s infinite ease-in-out;
-}
-
-.controls-layout {
- flex-grow: 1;
- display: flex;
- flex-direction: column;
- gap: 10px;
- overflow: hidden;
- min-height: 0;
-}
-
-.control-group {
- flex-shrink: 0;
-}
-
-.control-group h4 {
- font-size: 0.9em;
- color: var(--text-muted);
- margin-bottom: 5px;
- padding-bottom: 5px;
- border-bottom: 1px dashed var(--panel-border);
- text-transform: uppercase;
- letter-spacing: 1px;
-}
-
-.basic-actions {
- display: flex;
- gap: 10px;
-}
-
-.action-button.basic {
- flex: 1;
- padding: 8px 5px;
- font-size: 0.85em;
- font-weight: bold;
- background: var(--button-bg);
- color: var(--button-text);
- border: 1px solid rgba(0, 0, 0, 0.3);
- border-radius: 5px;
- cursor: pointer;
- transition: all 0.15s ease;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
- outline: none;
-}
-
-.action-button.basic:hover:enabled {
- background: var(--button-hover-bg);
- transform: translateY(-1px);
- box-shadow: 0 3px 6px rgba(0, 0, 0, 0.5);
-}
-
-.action-button.basic:active:enabled {
- transform: translateY(0px);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
-}
-
-#button-attack.attack-buffed:enabled {
- border: 2px solid var(--heal-color);
- box-shadow: 0 0 10px 2px rgba(144, 238, 144, 0.6), 0 3px 6px rgba(0, 0, 0, 0.5);
- background: linear-gradient(145deg, #70c070, #5a9a5a);
- transform: translateY(-1px);
-}
-
-
-.ability-list {
- flex-grow: 1;
- display: flex;
- flex-direction: column;
- min-height: 0;
- overflow: hidden;
-}
-
-.ability-list h4 {
- flex-shrink: 0;
-}
-
-.abilities-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(75px, 1fr));
- gap: 8px;
- padding: 8px;
- padding-bottom: 12px;
- background-color: rgba(0, 0, 0, 0.2);
- border-radius: 4px;
- overflow-y: auto;
- border: 1px solid rgba(0, 0, 0, 0.3);
- flex-grow: 1;
- position: relative;
-}
-
-.abilities-grid::after {
- content: '';
- display: block;
- height: 10px;
- width: 100%;
-}
-
-.abilities-grid .placeholder-text {
- grid-column: 1 / -1;
- text-align: center;
- color: var(--text-muted);
- align-self: center;
- font-size: 0.9em;
- padding: 15px 0;
-}
-
-.ability-button {
- aspect-ratio: 1 / 1;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- padding: 5px;
- border-radius: 6px;
- background: var(--button-ability-bg);
- border: 1px solid var(--button-ability-border);
- color: #fff;
- text-align: center;
- line-height: 1.15;
- cursor: pointer;
- transition: all 0.2s ease-out;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
- text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.6);
- position: relative;
- overflow: hidden;
- outline: none;
-}
-
-.ability-button .ability-name {
- font-size: 0.75em;
- font-weight: bold;
- margin-bottom: 2px;
- display: block;
- width: 95%;
-}
-
-.ability-button .ability-desc {
- font-size: 0.65em;
- font-weight: normal;
- color: #aaccce;
- opacity: 0.8;
- text-shadow: none;
- max-height: 2em;
- overflow: hidden;
- width: 95%;
- display: block;
- margin-top: auto;
-}
-
-.ability-button:hover:enabled {
- transform: scale(1.03) translateY(-1px);
- background: var(--button-ability-hover-bg);
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5), 0 0 8px rgba(77, 176, 181, 0.4);
- border-color: #77d9dd;
-}
-
-.ability-button:active:enabled {
- transform: scale(1) translateY(0);
- box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.3);
- filter: brightness(0.9);
-}
-
-.ability-button:disabled,
-.action-button.basic:disabled {
- background: var(--button-disabled-bg) !important;
- border-color: transparent !important;
- color: var(--button-disabled-text) !important;
- cursor: not-allowed !important;
- transform: none !important;
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4) !important;
- opacity: 0.7;
- text-shadow: none !important;
- filter: grayscale(50%);
-}
-
-.ability-button.not-enough-resource {
- border: 2px dashed var(--damage-color);
- animation: pulse-red-border 1s infinite ease-in-out;
-}
-.ability-button.not-enough-resource:disabled {
- border-color: var(--damage-color);
- box-shadow: inset 0 0 8px rgba(255, 80, 80, 0.2), 0 3px 6px rgba(0, 0, 0, 0.2), inset 0 1px 3px rgba(0, 0, 0, 0.4);
-}
-
-.ability-button.buff-is-active {
- border: 2px solid var(--heal-color);
- box-shadow: 0 0 8px rgba(144, 238, 144, 0.5);
-}
-
-.ability-button.buff-is-active:disabled {
- border-color: var(--heal-color);
-}
-
-.ability-button.is-on-cooldown,
-.ability-button.is-silenced {
- filter: grayscale(70%) brightness(0.8);
-}
-
-.ability-button.is-on-cooldown:disabled,
-.ability-button.is-silenced:disabled {
- filter: grayscale(70%) brightness(0.7);
-}
-
-.ability-button.is-on-cooldown .ability-name,
-.ability-button.is-silenced .ability-name,
-.ability-button.is-on-cooldown .ability-desc,
-.ability-button.is-silenced .ability-desc {
- opacity: 0.6;
-}
-
-.ability-button.is-on-cooldown .ability-desc,
-.ability-button.is-silenced .ability-desc {
- display: none;
-}
-
-.ability-cooldown-display {
- position: absolute;
- bottom: 5px;
- left: 0;
- width: 100%;
- text-align: center;
- font-size: 0.75em;
- font-weight: bold;
- color: var(--turn-color);
- text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
- pointer-events: none;
- display: none;
- line-height: 1;
-}
-
-.ability-button.is-on-cooldown .ability-cooldown-display,
-.ability-button.is-silenced .ability-cooldown-display {
- display: block !important;
-}
-
-
-.battle-log-new {
- height: var(--log-panel-fixed-height);
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-
-.battle-log-new h3 {
- flex-shrink: 0;
- font-size: 1.4em;
- margin-bottom: 10px;
- text-align: center;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- padding-bottom: 8px;
-}
-
-#log-list {
- list-style: none;
- flex-grow: 1;
- overflow-y: auto;
- background-color: var(--log-bg);
- border: 1px solid var(--log-border);
- font-size: 0.85em;
- border-radius: 6px;
- color: var(--log-text);
- padding: 10px;
- min-height: 0;
- word-wrap: break-word;
-}
-
-#log-list li {
- padding: 4px 8px;
- border-bottom: 1px solid rgba(74, 80, 114, 0.5);
- line-height: 1.35;
- transition: background-color 0.3s;
-}
-
-#log-list li:last-child {
- border-bottom: none;
-}
-
-#log-list li:hover {
- background-color: rgba(255, 255, 255, 0.03);
-}
-.log-damage { color: var(--damage-color); font-weight: 500; }
-.log-heal { color: var(--heal-color); font-weight: 500; }
-.log-block { color: var(--block-color); font-style: italic; }
-.log-info { color: #b0c4de; }
-.log-turn {
- font-weight: bold;
- color: var(--turn-color);
- margin-top: 6px;
- border-top: 1px solid rgba(255, 215, 0, 0.3);
- padding-top: 6px;
- font-size: 1.05em;
- display: block;
-}
-.log-system {
- font-weight: bold;
- color: var(--system-color);
- font-style: italic;
- opacity: 0.8;
-}
-.log-effect {
- font-style: italic;
- color: var(--effect-color);
-}
-
-.modal {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: var(--modal-bg);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 1000;
- backdrop-filter: blur(4px) brightness(0.7);
- opacity: 0;
- pointer-events: none;
- transition: opacity 0.4s ease-out;
-}
-
-.modal.hidden {
- display: none !important;
-}
-
-.modal:not(.hidden) {
- opacity: 1;
- pointer-events: auto;
-}
-
-.modal-content {
- background: var(--modal-content-bg);
- padding: 40px 50px;
- border-radius: 10px;
- text-align: center;
- border: 1px solid var(--panel-border);
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
- color: var(--text-light);
- transform: scale(0.8) translateY(30px);
- opacity: 0;
- transition: transform 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.2), opacity 0.4s ease-out;
-}
-
-.modal:not(.hidden) .modal-content {
- transform: scale(1) translateY(0);
- opacity: 1;
-}
-
-.modal-content h2#result-message {
- margin-bottom: 25px;
- font-family: var(--font-fancy);
- font-size: 2.5em;
- line-height: 1.2;
-}
-
-.modal-action-button {
- padding: 12px 30px;
- font-size: 1.1em;
- cursor: pointer;
- background: var(--button-bg);
- color: var(--button-text);
- border: 1px solid rgba(0, 0, 0, 0.3);
- border-radius: 6px;
- margin-top: 20px;
- font-weight: bold;
- text-transform: uppercase;
- letter-spacing: 1px;
- transition: all 0.2s ease;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
- outline: none;
-}
-
-.modal-action-button:hover:enabled {
- background: var(--button-hover-bg);
- transform: scale(1.05) translateY(-1px);
- box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
-}
-
-.modal-action-button:active:enabled {
- transform: scale(1) translateY(0);
- box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4);
-}
-
-.modal-action-button:disabled {
- background: var(--button-disabled-bg);
- color: var(--button-disabled-text);
- cursor: not-allowed;
- opacity: 0.7;
-}
-
-.modal-action-button i {
- margin-right: 8px;
-}
-
-@keyframes pulse-red-border {
- 0%, 100% { border-color: var(--damage-color); }
- 50% { border-color: #ffb3b3; }
-}
-
-@keyframes pulse-timer-warning {
- 0%, 100% { color: var(--timer-low-time-color); transform: scale(1); }
- 50% { color: #ff6347; transform: scale(1.05); }
-}
-
-@keyframes flash-effect {
- 0%, 100% {
- box-shadow: var(--initial-box-shadow, 0 0 15px rgba(0, 0, 0, 0.4), inset 0 0 10px rgba(0, 0, 0, 0.3));
- border-color: var(--initial-border-color, var(--panel-border));
- transform: scale(1);
- }
- 50% {
- box-shadow: 0 0 25px 10px var(--flash-color-outer, rgba(255, 255, 255, 0.7)),
- inset 0 0 15px var(--flash-color-inner, rgba(255, 255, 255, 0.4)),
- 0 0 15px rgba(0, 0, 0, 0.4);
- border-color: var(--flash-border-color, #ffffff);
- transform: scale(1.005);
- }
-}
-
-[class*="is-casting-"] {
- animation: flash-effect var(--cast-duration) ease-out;
-}
-
-#player-panel.is-casting-heal, #opponent-panel.is-casting-heal {
- --flash-color-outer: rgba(144, 238, 144, 0.7); --flash-color-inner: rgba(144, 238, 144, 0.4);
- --flash-border-color: var(--heal-color);
-}
-#player-panel.is-casting-fireball, #opponent-panel.is-casting-fireball {
- --flash-color-outer: rgba(255, 100, 100, 0.7); --flash-color-inner: rgba(255, 100, 100, 0.4);
- --flash-border-color: var(--damage-color);
-}
-#player-panel.is-casting-shadowBolt, #opponent-panel.is-casting-shadowBolt {
- --flash-color-outer: rgba(138, 43, 226, 0.6); --flash-color-inner: rgba(138, 43, 226, 0.3);
- --flash-border-color: var(--dark-energy-color);
-}
-
-@keyframes shake-opponent {
- 0%, 100% { transform: translateX(0); }
- 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px) rotate(-0.5deg); }
- 20%, 40%, 60%, 80% { transform: translateX(4px) rotate(0.5deg); }
-}
-
-#opponent-panel.is-shaking {
- animation: shake-opponent var(--shake-duration) cubic-bezier(.36, .07, .19, .97) both;
- transform: translate3d(0, 0, 0);
- backface-visibility: hidden;
- perspective: 1000px;
-}
-
-#opponent-panel.dissolving {
- opacity: 0;
- transform: scale(0.9) translateY(20px);
- transition: opacity var(--dissolve-duration) ease-in, transform var(--dissolve-duration) ease-in;
- pointer-events: none;
-}
-
-@keyframes shake-short {
- 0%, 100% { transform: translateX(0); }
- 25% { transform: translateX(-3px); }
- 50% { transform: translateX(3px); }
- 75% { transform: translateX(-3px); }
-}
-
-.shake-short {
- animation: shake-short 0.3s ease-in-out;
-}
-
-/* --- Отзывчивость (Медиа-запросы) --- */
-@media (max-width: 900px) {
- body {
- height: auto; min-height: 100vh; /* Из серверной, чтобы обеспечить высоту */
- overflow-y: auto;
- padding: 5px 0; font-size: 15px;
- justify-content: flex-start;
- }
- .auth-game-setup-wrapper {
- max-height: none;
- padding-top: 60px; /* Отступ для #user-info из локальной */
- }
- /* === ИЗМЕНЕНИЕ: Адаптация #user-info (из локальной версии) === */
- #user-info { top: 5px; right: 10px; }
- #user-info p { font-size: 0.85em; }
- #logout-button { padding: 5px 10px !important; font-size: 0.75em !important; }
- /* === КОНЕЦ ИЗМЕНЕНИЯ === */
-
- .game-wrapper { padding: 5px; gap: 5px; height: auto; min-height: calc(100vh - 10px); width: 100%; } /* min-height и width из серверной */
- /* === ИЗМЕНЕНИЕ: game-header удален (из локальной версии) === */
-
- /* Показываем кнопки переключения на мобильных - ИЗ СЕРВЕРНОЙ ВЕРСИИ */
- .panel-switcher-controls {
- display: flex;
- }
-
- .battle-arena-container {
- /* flex-direction: column; height: auto; overflow: visible; - из локальной версии заменяется логикой ниже */
- gap: 0; /* Убираем отступ между колонками, т.к. они будут накладываться - ИЗ СЕРВЕРНОЙ ВЕРСИИ */
- /* position: relative; overflow: hidden; flex-grow: 1; min-height: 350px; - Эти стили уже есть глобально, но тут подтверждаем */
- }
-
- /* Стили для колонок при переключении - ИЗ СЕРВЕРНОЙ ВЕРСИИ */
- .player-column,
- .opponent-column {
- /* width: 100%; height: auto; overflow: visible; - из локальной версии заменяется логикой ниже */
- position: absolute; /* Для наложения */
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- overflow-y: auto; /* Прокрутка содержимого колонки */
- transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
- padding: 5px; /* Добавлено для отступов внутри колонок на мобильных */
- gap: 8px; /* Добавлено для отступов между панелями внутри колонок */
- }
-
- .player-column { transform: translateX(0); opacity: 1; z-index: 10; pointer-events: auto; }
- .opponent-column { transform: translateX(100%); opacity: 0; z-index: 5; pointer-events: none; }
-
- .battle-arena-container.show-opponent-panel .player-column { transform: translateX(-100%); opacity: 0; z-index: 5; pointer-events: none; }
- .battle-arena-container.show-opponent-panel .opponent-column { transform: translateX(0); opacity: 1; z-index: 10; pointer-events: auto; }
-
-
- .fighter-panel, .controls-panel-new, .battle-log-new {
- min-height: auto; /* Высота по контенту */
- height: auto;
- padding: 10px;
- flex-grow: 0; /* Локальное */
- flex-shrink: 1; /* Локальное */
- }
- .fighter-panel { flex-shrink: 0; } /* Из серверной для panel-switcher */
- .fighter-panel .panel-content { flex-grow: 1; min-height: 0; } /* Из серверной для panel-switcher */
-
- .controls-panel-new { min-height: 200px; flex-shrink: 0; } /* flex-shrink из серверной */
- .battle-log-new { height: auto; min-height: 150px; flex-shrink: 0; } /* flex-shrink из серверной */
-
- #log-list { max-height: 200px; }
- .abilities-grid { max-height: none; overflow-y: visible; padding-bottom: 8px; }
- .abilities-grid::after { display: none; }
- .ability-list, .controls-layout { overflow: visible; } /* Локальное */
- .fighter-name { font-size: 1.3em; }
- .panel-content { margin-top: 10px; /* Локальное, но теперь panel-content изменен для серверного panel-switcher, возможно, не нужно */ }
- .stat-bar-container .bar-icon { font-size: 1.2em; }
- .bar { height: 18px; }
- .effects-area, .effect { font-size: 0.85em; }
- #turn-indicator { font-size: 1.2em; margin-bottom: 8px; }
- .turn-timer-display { font-size: 0.85em; margin-bottom: 8px; padding: 4px; }
- #turn-timer { font-size: 1em; }
- .action-button.basic { font-size: 0.8em; padding: 8px 4px; }
- .abilities-grid { grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 8px; padding: 8px; }
- .ability-button { font-size: 0.75em; padding: 5px; }
- .ability-button .ability-name { margin-bottom: 2px; }
- .ability-button .ability-desc { font-size: 0.65em; }
- .modal-content { padding: 25px 30px; width: 90%; max-width: 400px; }
- .modal-content h2#result-message { font-size: 1.8em; }
- .modal-action-button { font-size: 1em; padding: 10px 20px; }
- #game-setup { max-width: 95%; padding: 15px; }
- #game-setup h2 { font-size: 1.6em; }
- #game-setup h3 { font-size: 1.1em; }
- #game-setup button { padding: 8px 12px; font-size: 0.9em; }
- #game-setup input[type="text"] { width: calc(100% - 90px); max-width: 200px; padding: 8px; }
- #available-games-list { max-height: 180px; }
- .character-selection label { margin: 0 10px; font-size: 1em; }
-}
-
-@media (max-width: 480px) {
- body { font-size: 14px; }
- /* === ИЗМЕНЕНИЕ: Адаптация #user-info для мобильных (из локальной версии) === */
- .auth-game-setup-wrapper { padding-top: 50px; /* Еще немного места сверху */ }
- #user-info {
- top: 5px;
- right: 5px;
- display: flex; /* В одну строку */
- flex-direction: row;
- align-items: center;
- gap: 8px;
- }
- #user-info p { margin-bottom: 0; font-size: 0.8em; }
- #logout-button { padding: 4px 8px !important; font-size: 0.7em !important; }
- #logout-button i { margin-right: 3px; }
- /* === КОНЕЦ ИЗМЕНЕНИЯ === */
-
- /* Стили для panel-switcher на очень маленьких экранах - ИЗ СЕРВЕРНОЙ ВЕРСИИ */
- .panel-switch-button .button-text { display: none; } /* Скрываем текст, оставляем иконки */
- .panel-switch-button i { margin-right: 0; font-size: 1.2em; }
- .panel-switch-button { padding: 6px 8px; }
-
- /* Локальные изменения */
- .fighter-name { font-size: 1.2em; }
- .avatar-image { max-width: 40px; } /* Из серверной, но не противоречит */
- .abilities-grid { grid-template-columns: repeat(auto-fit, minmax(65px, 1fr)); gap: 5px; padding: 5px; padding-bottom: 10px; }
- .ability-button { font-size: 0.7em; padding: 4px; }
- .ability-button .ability-name { margin-bottom: 1px; }
- .ability-button .ability-desc { display: none; }
- #log-list { font-size: 0.8em; max-height: 150px; }
- .modal-content { padding: 20px; }
- .modal-content h2#result-message { font-size: 1.6em; }
- .modal-action-button { font-size: 0.9em; padding: 8px 16px; }
- .auth-game-setup-wrapper { padding-left: 15px; padding-right: 15px; }
- #game-setup { padding: 10px; }
- #game-setup h2 { font-size: 1.4em; }
- #game-setup button { padding: 7px 10px; font-size: 0.85em; margin: 5px; }
- #game-setup input[type="text"],
- #game-setup button { display: block; width: 100%; margin-left: 0; margin-right: 0; }
- #game-setup input[type="text"] { max-width: none; margin-bottom: 10px; }
- #game-setup div>input[type="text"]+button { margin-top: 5px; }
- #available-games-list { max-height: 120px; }
- #available-games-list li button { font-size: 0.75em; padding: 5px 8px; }
- .character-selection { padding: 10px; }
- .character-selection label { margin: 0 5px 5px 5px; font-size: 0.9em; display: block; }
- .character-selection label i { margin-right: 5px; }
- #turn-indicator { font-size: 1.1em; }
- .turn-timer-display { font-size: 0.8em; margin-top: -3px; margin-bottom: 6px; }
- #turn-timer { font-size: 0.95em; }
+/* === style_alt.css === */
+/* Подключение внешних шрифтов и иконок */
+@import url('https://fonts.googleapis.com/css2?family=MedievalSharp&family=Roboto:wght@300;400;700&display=swap');
+@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css');
+
+:root {
+ /* --- Переменные цветов и шрифтов --- */
+ --font-main: 'Roboto', sans-serif;
+ --font-fancy: 'MedievalSharp', cursive;
+
+ --bg-gradient-dark: linear-gradient(160deg, #1f243a, #10121c); /* Темный фон */
+ --panel-bg: rgba(16, 18, 28, 0.8); /* Фон панелей с прозрачностью */
+ --panel-border: #4a5072; /* Цвет рамки панелей */
+
+ /* Цвета свечения панелей (используются в JS анимации каста и в базовой тени) */
+ --panel-glow-player: rgba(80, 150, 255, 0.3); /* Для Елены (синее) */
+ --panel-glow-opponent: rgba(255, 80, 80, 0.3); /* Для Баларда (красное) */
+ --panel-glow-almagest: rgba(199, 108, 255, 0.3); /* Для Альмагест (фиолетовое) */
+
+
+ --text-light: #e8effc; /* Светлый основной текст */
+ --text-muted: #9badce; /* Приглушенный текст (подзаголовки, иконки по умолчанию) */
+ --text-heading: #ffffff; /* Белый текст заголовков */
+
+ /* Акцентные цвета персонажей (для имен, иконок, рамок) */
+ --accent-player: #6c95ff; /* Елена (светло-синий) */
+ --accent-opponent: #ff6c6c; /* Балард (красный) */
+ --accent-almagest: #c76cff; /* Альмагест (фиолетовый) */
+
+ /* Цвета полос характеристик */
+ --hp-color: #de4b4b; /* Здоровье (красный) */
+ --mana-color: #58a8d0; /* Мана Елены (голубой) */
+ --stamina-color: #ffb347; /* Ярость Баларда (оранжевый) */
+ --dark-energy-color: #ab47bc; /* Темная Энергия Альмагест (пурпурный) */
+ --bar-bg: #252a44; /* Фон полос */
+
+ /* Цвета кнопок */
+ --button-bg: linear-gradient(145deg, #556190, #3f4a70); /* Градиент фона кнопок */
+ --button-hover-bg: linear-gradient(145deg, #6a79b0, #556190); /* Градиент при наведении */
+ --button-text: var(--text-light); /* Цвет текста кнопок */
+
+ --button-ability-bg: linear-gradient(145deg, #305a5e, #1f4043); /* Фон кнопок способностей (темно-зеленый) */
+ --button-ability-hover-bg: linear-gradient(145deg, #407a7e, #305a5e); /* При наведении */
+ --button-ability-border: #4db0b5; /* Цвет рамки кнопок способностей (бирюзовый) */
+
+ --button-disabled-bg: #333950; /* Фон отключенных кнопок (темно-серый) */
+ --button-disabled-text: #6b7491; /* Цвет текста отключенных кнопок (светло-серый) */
+
+ /* Цвета лога боя */
+ --log-bg: rgba(10, 12, 20, 0.85); /* Фон лога */
+ --log-border: var(--panel-border); /* Рамка лога */
+ --log-text: var(--text-muted); /* Цвет текста лога */
+
+ /* Цвета сообщений в логе и элементов UI */
+ --icon-color: var(--text-muted); /* Цвет иконок по умолчанию */
+ --damage-color: #ff8080; /* Цвет урона (красноватый) */
+ --heal-color: #90ee90; /* Цвет лечения (светло-зеленый) */
+ --block-color: #add8e6; /* Цвет блока (светло-синий) */
+ --effect-color: #d8bfd8; /* Цвет эффектов (светло-фиолетовый) */
+ --turn-color: #ffd700; /* Цвет индикатора хода (золотой) */
+ --system-color: #7fffd4; /* Цвет системных сообщений (аквамариновый) */
+
+ /* Стили модального окна */
+ --modal-bg: rgba(16, 18, 28, 0.97); /* Фон модального оверлея */
+ --modal-content-bg: #2a2f45; /* Фон контента модального окна */
+
+ /* Стили скроллбара */
+ --scrollbar-thumb: #4a5072; /* Цвет ползунка скроллбара */
+ --scrollbar-track: #10121c; /* Цвет трека скроллбара */
+
+ /* Длительности анимаций (для JS) */
+ --shake-duration: 0.4s; /* Длительность анимации тряски */
+ --cast-duration: 0.6s; /* Длительность анимации каста */
+ --dissolve-duration: 6.0s; /* Длительность анимации растворения */
+
+ /* Фиксированная высота лог-панели (для десктопа) */
+ --log-panel-fixed-height: 280px;
+
+ /* === Переменные для таймера === */
+ --timer-text-color: var(--turn-color); /* Цвет текста таймера (золотой) */
+ --timer-icon-color: #b0c4de; /* Цвет иконки таймера (светло-голубой) */
+ --timer-low-time-color: var(--damage-color); /* Цвет текста, когда времени мало (красный) */
+
+ /* === Переменные для переключателя панелей (мобильный вид) === */
+ --panel-switcher-bg: rgba(10, 12, 20, 0.9);
+ --panel-switcher-border: var(--panel-border);
+ --panel-switcher-button-bg: var(--button-bg);
+ --panel-switcher-button-text: var(--button-text);
+ --panel-switcher-button-active-bg: var(--accent-player);
+ --panel-switcher-button-active-text: #fff;
+}
+
+/* --- Базовые Стили и Сброс --- */
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ height: 100%;
+}
+
+body {
+ font-family: var(--font-main);
+ background: var(--bg-gradient-dark) fixed;
+ color: var(--text-light);
+ line-height: 1.5;
+ height: 100vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 10px;
+}
+
+h1, h2, h3, h4 {
+ font-family: var(--font-fancy);
+ color: var(--text-heading);
+ margin-bottom: 0.75em;
+ font-weight: normal;
+}
+
+button {
+ font-family: var(--font-main);
+}
+
+i {
+ margin-right: 6px;
+ color: var(--icon-color);
+ width: 1.2em;
+ text-align: center;
+}
+
+/* Стили скроллбара */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
+}
+*::-webkit-scrollbar { width: 8px; }
+*::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 4px; }
+*::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 4px; border: 2px solid var(--scrollbar-track); }
+
+/* === Стили для Экранов Аутентификации и Настройки Игры === */
+.auth-game-setup-wrapper {
+ width: 100%;
+ max-width: 700px;
+ margin: 20px auto;
+ padding: 25px 30px;
+ background: var(--panel-bg);
+ border: 1px solid var(--panel-border);
+ border-radius: 10px;
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5);
+ color: var(--text-light);
+ text-align: center;
+ max-height: calc(100vh - 40px);
+ overflow-y: auto;
+}
+/* ... (остальные стили для .auth-game-setup-wrapper и его дочерних элементов остаются без изменений) ... */
+.auth-game-setup-wrapper h2,
+.auth-game-setup-wrapper h3 {
+ font-family: var(--font-fancy);
+ color: var(--text-heading);
+ margin-bottom: 1em;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ padding-bottom: 0.5em;
+}
+.auth-game-setup-wrapper h3 { font-size: 1.2em; margin-top: 1.5em; }
+.auth-game-setup-wrapper button,
+#auth-section form button {
+ font-family: var(--font-main); background: var(--button-bg); color: var(--button-text);
+ border: 1px solid rgba(0, 0, 0, 0.3); border-radius: 6px; padding: 10px 18px; margin: 8px 5px;
+ cursor: pointer; transition: all 0.15s ease; font-weight: bold; text-transform: uppercase;
+ letter-spacing: 0.5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); outline: none;
+}
+.auth-game-setup-wrapper button:hover:enabled,
+#auth-section form button:hover:enabled {
+ background: var(--button-hover-bg); transform: translateY(-2px) scale(1.02); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
+}
+.auth-game-setup-wrapper button:active:enabled,
+#auth-section form button:active:enabled {
+ transform: translateY(0px) scale(1); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+.auth-game-setup-wrapper button:disabled,
+#auth-section form button:disabled {
+ background: var(--button-disabled-bg) !important; color: var(--button-disabled-text) !important;
+ border-color: transparent !important; cursor: not-allowed !important; opacity: 0.7;
+ transform: none !important; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4) !important; filter: grayscale(50%);
+}
+.auth-game-setup-wrapper input[type="text"],
+#auth-section input[type="text"],
+#auth-section input[type="password"] {
+ padding: 10px; border-radius: 5px; border: 1px solid var(--panel-border); background-color: var(--bar-bg);
+ color: var(--text-light); margin: 5px 5px 10px 5px; font-size: 0.9em; width: calc(100% - 22px);
+ max-width: 300px; box-sizing: border-box; outline: none; transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+.auth-game-setup-wrapper input[type="text"]:focus,
+#auth-section input[type="text"]:focus,
+#auth-section input[type="password"]:focus {
+ border-color: var(--accent-player); box-shadow: 0 0 8px rgba(108, 149, 255, 0.4);
+}
+#available-games-list {
+ margin-top: 20px; text-align: left; max-height: 250px; overflow-y: auto; padding: 10px 15px;
+ background-color: rgba(0, 0, 0, 0.25); border: 1px solid var(--log-border); border-radius: 6px;
+}
+#available-games-list h3 { margin-top: 0; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px dashed rgba(255, 255, 255, 0.1); }
+#available-games-list ul { list-style: none; padding: 0; margin: 0; }
+#available-games-list li { padding: 10px; border-bottom: 1px solid rgba(74, 80, 114, 0.5); display: flex; justify-content: space-between; align-items: center; font-size: 0.9em; }
+#available-games-list li:last-child { border-bottom: none; }
+#available-games-list li button { padding: 6px 10px; font-size: 0.8em; margin-left: 10px; flex-shrink: 0; }
+#status-container { min-height: 2.5em; margin-bottom: 15px; }
+#auth-message, #game-status-message {
+ font-weight: bold; font-size: 1.1em; padding: 5px; background-color: rgba(0, 0, 0, 0.1);
+ border-radius: 4px; display: block; margin-bottom: 5px; text-align: center;
+}
+#auth-message.success { color: var(--heal-color); }
+#auth-message.error { color: var(--damage-color); }
+#game-status-message { color: var(--turn-color); }
+#auth-section form { margin-bottom: 20px; }
+#user-info { padding: 10px; background-color: rgba(255, 255, 255, 0.05); border-radius: 5px; margin-bottom: 20px; }
+#user-info p { margin: 0 0 10px 0; font-size: 1.1em; }
+#logout-button { background: linear-gradient(145deg, #8c3a3a, #6b2b2b) !important; }
+#logout-button:hover { background: linear-gradient(145deg, #a04040, #8c3a3a) !important; }
+.character-selection {
+ margin-top: 15px; margin-bottom: 15px; padding: 15px; background-color: rgba(0, 0, 0, 0.2);
+ border-radius: 6px; border: 1px solid rgba(74, 80, 114, 0.5);
+}
+.character-selection h4 { font-size: 1.1em; color: var(--text-muted); margin-bottom: 10px; border: none; padding: 0; text-align: center; }
+.character-selection label {
+ display: inline-block; margin: 0 15px; cursor: pointer; font-size: 1.05em; padding: 5px 10px;
+ border-radius: 4px; transition: background-color 0.2s ease, color 0.2s ease; user-select: none;
+}
+.character-selection input[type="radio"] { display: none; }
+.character-selection input[type="radio"]:checked + label { color: #fff; font-weight: bold; }
+.character-selection input[type="radio"][value="elena"]:checked + label { background-color: var(--accent-player); box-shadow: 0 0 8px rgba(108, 149, 255, 0.5); }
+.character-selection input[type="radio"][value="almagest"]:checked + label { background-color: var(--accent-almagest); box-shadow: 0 0 8px rgba(199, 108, 255, 0.5); }
+.character-selection label:hover { background-color: rgba(255, 255, 255, 0.1); }
+.character-selection label i { margin-right: 8px; vertical-align: middle; }
+label[for="char-elena"] i { color: var(--accent-player); }
+label[for="char-almagest"] i { color: var(--accent-almagest); }
+
+
+/* --- Основная Структура Игры (.game-wrapper) --- */
+.game-wrapper {
+ width: 100%;
+ height: 100%;
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ overflow: hidden;
+}
+.game-header {
+ flex-shrink: 0;
+ text-align: center;
+ padding: 5px 0 10px 0;
+ border-bottom: 1px solid var(--panel-border);
+}
+.game-header h1 {
+ font-size: 2em;
+ margin: 0;
+ text-shadow: 0 0 8px rgba(255, 255, 255, 0.5);
+}
+.title-enchantress { color: var(--accent-player); }
+.title-knight { color: var(--accent-opponent); }
+.title-sorceress { color: var(--accent-almagest); }
+.separator i { color: var(--text-light); font-size: 0.8em; margin: 0 15px; }
+
+/* Глобальные стили для кнопок переключения панелей (скрыты по умолчанию) */
+.panel-switcher-controls {
+ display: none; /* Скрыт по умолчанию для десктопа */
+ flex-shrink: 0;
+ padding: 8px 5px;
+ background: var(--panel-switcher-bg);
+ border-bottom: 1px solid var(--panel-switcher-border);
+ gap: 10px;
+}
+.panel-switch-button {
+ flex: 1;
+ padding: 8px 10px;
+ font-size: 0.9em;
+ font-weight: bold;
+ text-transform: uppercase;
+ background: var(--panel-switcher-button-bg);
+ color: var(--panel-switcher-button-text);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 5px;
+ cursor: pointer;
+ transition: background-color 0.2s, color 0.2s, transform 0.1s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.panel-switch-button i { margin-right: 8px; }
+.panel-switch-button:hover { filter: brightness(1.1); }
+.panel-switch-button.active {
+ background: var(--panel-switcher-button-active-bg);
+ color: var(--panel-switcher-button-active-text);
+ box-shadow: 0 0 8px rgba(255,255,255,0.3);
+}
+
+.battle-arena-container {
+ flex-grow: 1;
+ display: flex; /* На десктопе панели рядом */
+ gap: 10px; /* Отступ между панелями на десктопе */
+ overflow: hidden; /* Обрезает контент, если он выходит за пределы */
+ position: relative; /* Для мобильного вида, где панели будут накладываться */
+ min-height: 0; /* Для корректной работы flex-grow */
+}
+
+.player-column,
+.opponent-column {
+ flex: 1; /* Равное распределение ширины на десктопе */
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ min-width: 0;
+ overflow: hidden; /* Обрезает внутренний контент, если он слишком большой */
+}
+
+/* ... (все остальные стили панелей, кнопок, лога и т.д. до медиа-запросов остаются как были) ... */
+.fighter-panel, .controls-panel-new, .battle-log-new {
+ background: var(--panel-bg); border: 1px solid var(--panel-border); border-radius: 8px;
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.4), inset 0 0 10px rgba(0, 0, 0, 0.3);
+ padding: 15px; display: flex; flex-direction: column; overflow: hidden;
+ transition: box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease-out, transform 0.3s ease-out;
+}
+.fighter-panel.panel-elena { border-color: var(--accent-player); }
+.fighter-panel.panel-almagest { border-color: var(--accent-almagest); }
+.fighter-panel.panel-balard { border-color: var(--accent-opponent); }
+.panel-header {
+ flex-shrink: 0; display: flex; align-items: center; gap: 10px;
+ padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); margin-bottom: 0;
+}
+.fighter-name { font-size: 1.6em; margin: 0; flex-grow: 1; text-align: left; }
+.fighter-name .icon-elena { color: var(--accent-player); }
+.fighter-name .icon-almagest { color: var(--accent-almagest); }
+.fighter-name .icon-balard { color: var(--accent-opponent); }
+.character-visual { flex-shrink: 0; margin-bottom: 0; }
+.avatar-image {
+ display: block; max-width: 50px; height: auto; border-radius: 50%;
+ border: 2px solid var(--panel-border); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+}
+.avatar-image.avatar-elena { border-color: var(--accent-player); }
+.avatar-image.avatar-almagest { border-color: var(--accent-almagest); }
+.avatar-image.avatar-balard { border-color: var(--accent-opponent); }
+.panel-content {
+ flex-grow: 1; overflow-y: auto; padding-right: 5px; display: flex;
+ flex-direction: column; gap: 10px; min-height: 0; padding-top: 10px; margin-top: 0;
+}
+.stat-bar-container { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
+.stat-bar-container .bar-icon { flex-shrink: 0; font-size: 1.4em; }
+.stat-bar-container.health .bar-icon { color: var(--hp-color); }
+.stat-bar-container.mana .bar-icon { color: var(--mana-color); }
+.stat-bar-container.stamina .bar-icon { color: var(--stamina-color); }
+.stat-bar-container.dark-energy .bar-icon { color: var(--dark-energy-color); }
+.bar-wrapper { flex-grow: 1; }
+.bar {
+ border-radius: 4px; height: 20px; border: 1px solid rgba(0, 0, 0, 0.5); overflow: hidden;
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5); position: relative; background-color: var(--bar-bg);
+}
+.bar-fill {
+ display: block; height: 100%; border-radius: 3px; position: relative;
+ z-index: 2; transition: width 0.4s ease-out;
+}
+.bar-text {
+ position: absolute; left: 0; top: 0; width: 100%; height: 100%; z-index: 3;
+ display: flex; justify-content: center; align-items: center; font-size: 0.75em; font-weight: bold;
+ color: #fff; text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.9); padding: 0 5px; white-space: nowrap; pointer-events: none;
+}
+.health .bar-fill { background-color: var(--hp-color); }
+.mana .bar-fill { background-color: var(--mana-color); }
+.stamina .bar-fill { background-color: var(--stamina-color); }
+.dark-energy .bar-fill { background-color: var(--dark-energy-color); }
+.status-area { font-size: 0.9em; display: flex; align-items: baseline; gap: 5px; flex-shrink: 0; min-height: 1.5em; }
+.status-area .icon-status { font-size: 1em; flex-shrink: 0; margin-top: 0.1em; }
+.status-area strong { color: var(--text-muted); font-weight: normal; flex-shrink: 0; margin-right: 3px; }
+.status-area span { font-weight: bold; }
+.status-area span.blocking { color: var(--block-color); font-style: italic; }
+.effects-area { font-size: 0.9em; display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; min-height: 3em; }
+.effect-category { display: flex; align-items: baseline; gap: 5px; }
+.effect-category strong {
+ color: var(--text-muted); font-weight: normal; font-family: var(--font-main);
+ font-size: 0.9em; flex-shrink: 0; margin-right: 3px;
+}
+.effect-category .icon-effects-buff, .effect-category .icon-effects-debuff {
+ font-size: 1em; flex-shrink: 0; margin-top: 0.1em; width: 1.2em; text-align: center;
+}
+.effect-category .icon-effects-buff { color: var(--heal-color); }
+.effect-category .icon-effects-debuff { color: var(--damage-color); }
+.effect-list { display: inline; line-height: 1.4; min-width: 0; font-weight: bold; }
+.effect {
+ display: inline-block; margin: 2px 3px 2px 0; padding: 1px 6px; font-size: 0.8em;
+ border-radius: 10px; border: 1px solid; cursor: default; font-weight: 600;
+ background-color: rgba(0, 0, 0, 0.2); white-space: nowrap; vertical-align: baseline;
+}
+.effect-buff { border-color: var(--heal-color); color: var(--heal-color); }
+.effect-debuff { border-color: var(--damage-color); color: var(--damage-color); }
+.effect-stun { border-color: var(--turn-color); color: var(--turn-color); }
+.effect-block { border-color: var(--block-color); color: var(--block-color); }
+.effect-info { border-color: var(--text-muted); color: var(--text-muted); }
+.controls-panel-new { flex-grow: 1; min-height: 0; display: flex; flex-direction: column; }
+#turn-indicator {
+ flex-shrink: 0; text-align: center; font-size: 1.4em; margin-bottom: 10px;
+ padding-bottom: 8px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); transition: color 0.3s ease;
+}
+.turn-timer-display {
+ flex-shrink: 0; text-align: center; font-size: 0.9em; color: var(--timer-text-color);
+ margin-top: -5px; margin-bottom: 10px; padding: 5px; background-color: rgba(0,0,0,0.15);
+ border-radius: 4px; border-top: 1px solid rgba(255,255,255,0.05);
+}
+.turn-timer-display i { color: var(--timer-icon-color); margin-right: 8px; }
+#turn-timer { font-weight: bold; font-size: 1.1em; min-width: 35px; display: inline-block; text-align: left; }
+#turn-timer.low-time { color: var(--timer-low-time-color); animation: pulse-timer-warning 1s infinite ease-in-out; }
+.controls-layout { flex-grow: 1; display: flex; flex-direction: column; gap: 10px; overflow: hidden; min-height: 0; }
+.control-group { flex-shrink: 0; }
+.control-group h4 {
+ font-size: 0.9em; color: var(--text-muted); margin-bottom: 5px; padding-bottom: 5px;
+ border-bottom: 1px dashed var(--panel-border); text-transform: uppercase; letter-spacing: 1px;
+}
+.basic-actions { display: flex; gap: 10px; }
+.action-button.basic {
+ flex: 1; padding: 8px 5px; font-size: 0.85em; font-weight: bold; background: var(--button-bg);
+ color: var(--button-text); border: 1px solid rgba(0, 0, 0, 0.3); border-radius: 5px;
+ cursor: pointer; transition: all 0.15s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); outline: none;
+}
+.action-button.basic:hover:enabled { background: var(--button-hover-bg); transform: translateY(-1px); box-shadow: 0 3px 6px rgba(0, 0, 0, 0.5); }
+.action-button.basic:active:enabled { transform: translateY(0px); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); }
+#button-attack.attack-buffed:enabled {
+ border: 2px solid var(--heal-color);
+ box-shadow: 0 0 10px 2px rgba(144, 238, 144, 0.6), 0 3px 6px rgba(0, 0, 0, 0.5);
+ background: linear-gradient(145deg, #70c070, #5a9a5a); transform: translateY(-1px);
+}
+.ability-list { flex-grow: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
+.ability-list h4 { flex-shrink: 0; }
+.abilities-grid {
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(75px, 1fr)); gap: 8px; padding: 8px;
+ background-color: rgba(0, 0, 0, 0.2); border-radius: 4px; overflow-y: auto;
+ border: 1px solid rgba(0, 0, 0, 0.3); flex-grow: 1; position: relative;
+}
+.abilities-grid::after { content: ''; display: block; height: 10px; width: 100%; }
+.abilities-grid .placeholder-text {
+ grid-column: 1 / -1; text-align: center; color: var(--text-muted);
+ align-self: center; font-size: 0.9em; padding: 15px 0;
+}
+.ability-button {
+ aspect-ratio: 1 / 1; display: flex; flex-direction: column; justify-content: center; align-items: center;
+ padding: 5px; border-radius: 6px; background: var(--button-ability-bg); border: 1px solid var(--button-ability-border);
+ color: #fff; text-align: center; line-height: 1.15; cursor: pointer; transition: all 0.2s ease-out;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.6);
+ position: relative; overflow: hidden; outline: none;
+}
+.ability-button .ability-name { font-size: 0.75em; font-weight: bold; margin-bottom: 2px; display: block; width: 95%; }
+.ability-button .ability-desc {
+ font-size: 0.65em; font-weight: normal; color: #aaccce; opacity: 0.8; text-shadow: none;
+ max-height: 2em; overflow: hidden; width: 95%; display: block; margin-top: auto;
+}
+.ability-button:hover:enabled {
+ transform: scale(1.03) translateY(-1px); background: var(--button-ability-hover-bg);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5), 0 0 8px rgba(77, 176, 181, 0.4); border-color: #77d9dd;
+}
+.ability-button:active:enabled {
+ transform: scale(1) translateY(0);
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.3); filter: brightness(0.9);
+}
+.ability-button:disabled, .action-button.basic:disabled {
+ background: var(--button-disabled-bg) !important; border-color: transparent !important;
+ color: var(--button-disabled-text) !important; cursor: not-allowed !important; transform: none !important;
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4) !important; opacity: 0.7; text-shadow: none !important; filter: grayscale(50%);
+}
+.ability-button.not-enough-resource { border: 2px dashed var(--damage-color); animation: pulse-red-border 1s infinite ease-in-out; }
+.ability-button.not-enough-resource:disabled {
+ border-color: var(--damage-color);
+ box-shadow: inset 0 0 8px rgba(255, 80, 80, 0.2), 0 3px 6px rgba(0, 0, 0, 0.2), inset 0 1px 3px rgba(0, 0, 0, 0.4);
+}
+.ability-button.buff-is-active { border: 2px solid var(--heal-color); box-shadow: 0 0 8px rgba(144, 238, 144, 0.5); }
+.ability-button.buff-is-active:disabled { border-color: var(--heal-color); }
+.ability-button.is-on-cooldown, .ability-button.is-silenced { filter: grayscale(70%) brightness(0.8); }
+.ability-button.is-on-cooldown:disabled, .ability-button.is-silenced:disabled { filter: grayscale(70%) brightness(0.7); }
+.ability-button.is-on-cooldown .ability-name, .ability-button.is-silenced .ability-name,
+.ability-button.is-on-cooldown .ability-desc, .ability-button.is-silenced .ability-desc { opacity: 0.6; }
+.ability-button.is-on-cooldown .ability-desc, .ability-button.is-silenced .ability-desc { display: none; }
+.ability-cooldown-display {
+ position: absolute; bottom: 5px; left: 0; width: 100%; text-align: center; font-size: 0.75em;
+ font-weight: bold; color: var(--turn-color); text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
+ pointer-events: none; display: none; line-height: 1;
+}
+.ability-button.is-on-cooldown .ability-cooldown-display,
+.ability-button.is-silenced .ability-cooldown-display { display: block !important; }
+.battle-log-new {
+ height: var(--log-panel-fixed-height); flex-shrink: 0;
+ display: flex; flex-direction: column; overflow: hidden;
+}
+.battle-log-new h3 {
+ flex-shrink: 0; font-size: 1.4em; margin-bottom: 10px; text-align: center;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: 8px;
+}
+#log-list {
+ list-style: none; flex-grow: 1; overflow-y: auto; background-color: var(--log-bg);
+ border: 1px solid var(--log-border); font-size: 0.85em; border-radius: 6px;
+ color: var(--log-text); padding: 10px; min-height: 0; word-wrap: break-word;
+}
+#log-list li {
+ padding: 4px 8px; border-bottom: 1px solid rgba(74, 80, 114, 0.5);
+ line-height: 1.35; transition: background-color 0.3s;
+}
+#log-list li:last-child { border-bottom: none; }
+#log-list li:hover { background-color: rgba(255, 255, 255, 0.03); }
+.log-damage { color: var(--damage-color); font-weight: 500; }
+.log-heal { color: var(--heal-color); font-weight: 500; }
+.log-block { color: var(--block-color); font-style: italic; }
+.log-info { color: #b0c4de; }
+.log-turn {
+ font-weight: bold; color: var(--turn-color); margin-top: 6px;
+ border-top: 1px solid rgba(255, 215, 0, 0.3); padding-top: 6px; font-size: 1.05em; display: block;
+}
+.log-system { font-weight: bold; color: var(--system-color); font-style: italic; opacity: 0.8; }
+.log-effect { font-style: italic; color: var(--effect-color); }
+.modal {
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--modal-bg);
+ display: flex; justify-content: center; align-items: center; z-index: 1000;
+ backdrop-filter: blur(4px) brightness(0.7); opacity: 0; pointer-events: none; transition: opacity 0.4s ease-out;
+}
+.modal.hidden { display: none !important; }
+.modal:not(.hidden) { opacity: 1; pointer-events: auto; }
+.modal-content {
+ background: var(--modal-content-bg); padding: 40px 50px; border-radius: 10px; text-align: center;
+ border: 1px solid var(--panel-border); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6); color: var(--text-light);
+ transform: scale(0.8) translateY(30px); opacity: 0;
+ transition: transform 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.2), opacity 0.4s ease-out;
+}
+.modal:not(.hidden) .modal-content { transform: scale(1) translateY(0); opacity: 1; }
+.modal-content h2#result-message { margin-bottom: 25px; font-family: var(--font-fancy); font-size: 2.5em; line-height: 1.2; }
+.modal-action-button {
+ padding: 12px 30px; font-size: 1.1em; cursor: pointer; background: var(--button-bg); color: var(--button-text);
+ border: 1px solid rgba(0, 0, 0, 0.3); border-radius: 6px; margin-top: 20px; font-weight: bold;
+ text-transform: uppercase; letter-spacing: 1px; transition: all 0.2s ease; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4); outline: none;
+}
+.modal-action-button:hover:enabled {
+ background: var(--button-hover-bg); transform: scale(1.05) translateY(-1px); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
+}
+.modal-action-button:active:enabled { transform: scale(1) translateY(0); box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4); }
+.modal-action-button:disabled { background: var(--button-disabled-bg); color: var(--button-disabled-text); cursor: not-allowed; opacity: 0.7; }
+.modal-action-button i { margin-right: 8px; }
+
+/* Анимации */
+@keyframes pulse-red-border { 0%, 100% { border-color: var(--damage-color); } 50% { border-color: #ffb3b3; } }
+@keyframes pulse-timer-warning { 0%, 100% { color: var(--timer-low-time-color); transform: scale(1); } 50% { color: #ff6347; transform: scale(1.05); } }
+@keyframes flash-effect {
+ 0%, 100% {
+ box-shadow: var(--initial-box-shadow, 0 0 15px rgba(0, 0, 0, 0.4), inset 0 0 10px rgba(0, 0, 0, 0.3));
+ border-color: var(--initial-border-color, var(--panel-border)); transform: scale(1);
+ }
+ 50% {
+ box-shadow: 0 0 25px 10px var(--flash-color-outer, rgba(255, 255, 255, 0.7)),
+ inset 0 0 15px var(--flash-color-inner, rgba(255, 255, 255, 0.4)),
+ 0 0 15px rgba(0, 0, 0, 0.4);
+ border-color: var(--flash-border-color, #ffffff); transform: scale(1.005);
+ }
+}
+[class*="is-casting-"] { animation: flash-effect var(--cast-duration) ease-out; }
+#player-panel.is-casting-heal, #opponent-panel.is-casting-heal { --flash-color-outer: rgba(144, 238, 144, 0.7); --flash-color-inner: rgba(144, 238, 144, 0.4); --flash-border-color: var(--heal-color); }
+#player-panel.is-casting-fireball, #opponent-panel.is-casting-fireball { --flash-color-outer: rgba(255, 100, 100, 0.7); --flash-color-inner: rgba(255, 100, 100, 0.4); --flash-border-color: var(--damage-color); }
+#player-panel.is-casting-shadowBolt, #opponent-panel.is-casting-shadowBolt { --flash-color-outer: rgba(138, 43, 226, 0.6); --flash-color-inner: rgba(138, 43, 226, 0.3); --flash-border-color: var(--dark-energy-color); }
+@keyframes shake-opponent {
+ 0%, 100% { transform: translateX(0); }
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px) rotate(-0.5deg); }
+ 20%, 40%, 60%, 80% { transform: translateX(4px) rotate(0.5deg); }
+}
+#opponent-panel.is-shaking {
+ animation: shake-opponent var(--shake-duration) cubic-bezier(.36, .07, .19, .97) both;
+ transform: translate3d(0, 0, 0); backface-visibility: hidden; perspective: 1000px;
+}
+#opponent-panel.dissolving {
+ opacity: 0; transform: scale(0.9) translateY(20px);
+ transition: opacity var(--dissolve-duration) ease-in, transform var(--dissolve-duration) ease-in; pointer-events: none;
+}
+@keyframes shake-short { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-3px); } 50% { transform: translateX(3px); } 75% { transform: translateX(-3px); } }
+.shake-short { animation: shake-short 0.3s ease-in-out; }
+
+
+/* --- Отзывчивость (Медиа-запросы) --- */
+@media (max-width: 900px) {
+ body { height: auto; min-height: 100vh; overflow-y: auto; padding: 5px 0; font-size: 15px; justify-content: flex-start; }
+ .auth-game-setup-wrapper { max-height: none; }
+ .game-wrapper { padding: 5px; gap: 5px; height: auto; min-height: calc(100vh - 10px); width: 100%; }
+ .game-header h1 { font-size: 1.5em; }
+
+ /* Показываем кнопки переключения на мобильных */
+ .panel-switcher-controls {
+ display: flex;
+ }
+
+ .battle-arena-container {
+ gap: 0; /* Убираем отступ между колонками, т.к. они будут накладываться */
+ /* position: relative; overflow: hidden; flex-grow: 1; min-height: 350px; - Эти стили уже есть глобально, но тут подтверждаем */
+ }
+
+ .player-column,
+ .opponent-column {
+ position: absolute; /* Для наложения */
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow-y: auto; /* Прокрутка содержимого колонки */
+ transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
+ padding: 5px;
+ gap: 8px;
+ /* display: flex; flex-direction: column; - Эти стили уже есть глобально, но тут подтверждаем */
+ }
+
+ .player-column { transform: translateX(0); opacity: 1; z-index: 10; pointer-events: auto; }
+ .opponent-column { transform: translateX(100%); opacity: 0; z-index: 5; pointer-events: none; }
+
+ .battle-arena-container.show-opponent-panel .player-column { transform: translateX(-100%); opacity: 0; z-index: 5; pointer-events: none; }
+ .battle-arena-container.show-opponent-panel .opponent-column { transform: translateX(0); opacity: 1; z-index: 10; pointer-events: auto; }
+
+ .fighter-panel, .controls-panel-new, .battle-log-new {
+ min-height: auto; /* Высота по контенту */
+ height: auto;
+ padding: 10px;
+ }
+
+ .fighter-panel {
+ flex-shrink: 0; /* Не сжимается легко */
+ }
+ .fighter-panel .panel-content {
+ flex-grow: 1; /* Старается занять доступное место в .fighter-panel */
+ min-height: 0; /* Для корректной работы flex */
+ }
+
+ .controls-panel-new { /* В колонке игрока */
+ flex-shrink: 0;
+ min-height: 200px; /* Чтобы кнопки были видны */
+ }
+ .battle-log-new { /* В колонке противника */
+ flex-shrink: 0;
+ min-height: 150px; /* Чтобы лог был виден */
+ }
+
+ #log-list { max-height: 200px; }
+ .abilities-grid { max-height: none; overflow-y: visible; padding-bottom: 8px; } /* Скролл у родителя */
+ .abilities-grid::after { display: none; }
+
+ .fighter-name { font-size: 1.3em; }
+ .stat-bar-container .bar-icon { font-size: 1.2em; }
+ .bar { height: 18px; }
+ .effects-area, .effect { font-size: 0.85em; }
+ #turn-indicator { font-size: 1.2em; margin-bottom: 8px; }
+ .turn-timer-display { font-size: 0.85em; margin-bottom: 8px; padding: 4px; }
+ #turn-timer { font-size: 1em; }
+ .action-button.basic { font-size: 0.8em; padding: 8px 4px; }
+ .ability-button { font-size: 0.75em; padding: 5px; }
+ .ability-button .ability-name { margin-bottom: 2px; }
+ .ability-button .ability-desc { font-size: 0.65em; }
+
+ .modal-content { padding: 25px 30px; width: 90%; max-width: 400px; }
+ .modal-content h2#result-message { font-size: 1.8em; }
+ .modal-action-button { font-size: 1em; padding: 10px 20px; }
+ #game-setup { max-width: 95%; padding: 15px; }
+ #game-setup h2 { font-size: 1.6em; }
+ #game-setup h3 { font-size: 1.1em; }
+ #game-setup button { padding: 8px 12px; font-size: 0.9em; }
+ #game-setup input[type="text"] { width: calc(100% - 90px); max-width: 200px; padding: 8px; }
+ #available-games-list { max-height: 180px; }
+ .character-selection label { margin: 0 10px; font-size: 1em; }
+}
+
+@media (max-width: 480px) {
+ body { font-size: 14px; }
+ .game-header h1 { font-size: 1.3em; }
+ .panel-switch-button .button-text { display: none; } /* Скрываем текст, оставляем иконки */
+ .panel-switch-button i { margin-right: 0; font-size: 1.2em; }
+ .panel-switch-button { padding: 6px 8px; }
+ .fighter-name { font-size: 1.2em; }
+ .avatar-image { max-width: 40px; }
+ .abilities-grid { grid-template-columns: repeat(auto-fit, minmax(65px, 1fr)); gap: 5px; padding: 5px; }
+ .ability-button { font-size: 0.7em; padding: 4px; }
+ .ability-button .ability-name { margin-bottom: 1px; }
+ .ability-button .ability-desc { display: none; } /* Скрываем описание способности */
+ #log-list { font-size: 0.8em; max-height: 150px; }
+ .modal-content { padding: 20px; }
+ .modal-content h2#result-message { font-size: 1.6em; }
+ .modal-action-button { font-size: 0.9em; padding: 8px 16px; }
+ .auth-game-setup-wrapper { padding: 15px; }
+ #game-setup { padding: 10px; }
+ #game-setup h2 { font-size: 1.4em; }
+ #game-setup button { padding: 7px 10px; font-size: 0.85em; margin: 5px; }
+ #game-setup input[type="text"], #game-setup button { display: block; width: 100%; margin-left: 0; margin-right: 0; }
+ #game-setup input[type="text"] { max-width: none; margin-bottom: 10px; }
+ #game-setup div>input[type="text"]+button { margin-top: 5px; }
+ #available-games-list { max-height: 120px; }
+ #available-games-list li button { font-size: 0.75em; padding: 5px 8px; }
+ .character-selection { padding: 10px; }
+ .character-selection label { margin: 0 5px 5px 5px; font-size: 0.9em; display: block; }
+ .character-selection label i { margin-right: 5px; }
+ #turn-indicator { font-size: 1.1em; }
+ .turn-timer-display { font-size: 0.8em; margin-top: -3px; margin-bottom: 6px; }
+ #turn-timer { font-size: 0.95em; }
}
\ No newline at end of file
diff --git a/server/bc.js b/server/bc.js
index 888329b..2e2976e 100644
--- a/server/bc.js
+++ b/server/bc.js
@@ -1,13 +1,5 @@
// /server/bc.js - Главный файл сервера Battle Club
-// Загружаем переменные окружения В САМОМ НАЧАЛЕ, до всех других импортов,
-// которые могут зависеть от process.env
-// Убедитесь, что путь к .env правильный относительно места запуска приложения.
-// Если bc.js запускается из папки server/, а .env в корне, то путь должен быть '../.env'
-// Но обычно dotenv ищет .env в process.cwd()
-require('dotenv').config({ path: require('node:path').resolve(process.cwd(), '.env') });
-
-
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
@@ -16,8 +8,10 @@ const path = require('path');
// Импорт серверных модулей из их новых местоположений
const authService = require('./auth/authService'); // Сервис аутентификации
const GameManager = require('./game/GameManager'); // Менеджер игр
-const db = require('./core/db'); // Модуль базы данных (важно, чтобы он тоже использовал dotenv)
+const db = require('./core/db'); // Модуль базы данных (для инициализации)
const GAME_CONFIG = require('./core/config'); // Глобальный конфиг игры
+// data.js (теперь data/index.js) и gameLogic.js (теперь game/logic/index.js)
+// импортируются внутри GameManager и GameInstance или их компонентов.
const app = express();
const server = http.createServer(app);
@@ -25,25 +19,33 @@ const server = http.createServer(app);
// Настройка Socket.IO
const io = new Server(server, {
cors: {
- // origin: process.env.CORS_ORIGIN || "https://pavel-chagovsky.com:3200", // Пример, если нужно CORS из .env
- // methods: ["GET", "POST"]
+ origin: "https://pavel-chagovsky.com:3200", // Для разработки. В продакшене укажите домен клиента.
+ methods: ["GET", "POST"]
},
- // pingInterval: 10000,
- // pingTimeout: 5000,
+ // Можно настроить pingInterval и pingTimeout для более быстрого обнаружения дисконнектов
+ // pingInterval: 10000, // 10 секунд
+ // pingTimeout: 5000, // 5 секунд (клиент должен ответить в течение этого времени)
});
// Раздача статических файлов из папки 'public'
+// __dirname будет указывать на папку server/, поэтому нужно подняться на уровень выше
app.use(express.static(path.join(__dirname, '..', 'public')));
// Создаем экземпляр GameManager
const gameManager = new GameManager(io);
-const loggedInUsers = {};
+// Хранилище информации о залогиненных пользователях по socket.id
+// (Временное решение, в продакшене лучше использовать Redis или БД для сессий)
+const loggedInUsers = {}; // { socket.id: { userId: ..., username: ... } }
+// Обработка подключений Socket.IO
io.on('connection', (socket) => {
console.log(`[Socket.IO] Пользователь подключился: ${socket.id}`);
- socket.userData = null;
+ // Привязываем user data к сокету (пока пустые, заполняются при логине)
+ socket.userData = null; // { userId: ..., username: ... }
+
+ // --- Обработчики событий Аутентификации ---
socket.on('register', async (data) => {
console.log(`[Socket.IO] Register attempt for username: "${data?.username}" from ${socket.id}`);
const result = await authService.registerUser(data?.username, data?.password);
@@ -58,10 +60,12 @@ io.on('connection', (socket) => {
socket.on('login', async (data) => {
console.log(`[Socket.IO] Login attempt for username: "${data?.username}" from ${socket.id}`);
const result = await authService.loginUser(data?.username, data?.password);
- if (result.success && result.userId && result.username) {
+ if (result.success && result.userId && result.username) { // Убедимся, что userId и username есть
console.log(`[Socket.IO] Login successful for ${result.username} (${result.userId}). Assigning to socket ${socket.id}.`);
socket.userData = { userId: result.userId, username: result.username };
- loggedInUsers[socket.id] = socket.userData;
+ loggedInUsers[socket.id] = socket.userData; // Сохраняем для быстрого доступа, если нужно
+
+ // После успешного логина, просим GameManager проверить, не был ли этот пользователь в игре
if (gameManager && typeof gameManager.handleRequestGameState === 'function') {
gameManager.handleRequestGameState(socket, result.userId);
}
@@ -70,26 +74,37 @@ io.on('connection', (socket) => {
socket.userData = null;
if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id];
}
- socket.emit('loginResponse', result);
+ socket.emit('loginResponse', result); // Отправляем результат клиенту
});
socket.on('logout', () => {
const username = socket.userData?.username || 'UnknownUser';
const userId = socket.userData?.userId;
console.log(`[Socket.IO] Logout request from user ${username} (ID: ${userId}, Socket: ${socket.id})`);
+
if (gameManager && typeof gameManager.handleDisconnect === 'function' && userId) {
+ // Уведомляем GameManager о "дисконнекте" этого пользователя из его игры, если он там был.
+ // handleDisconnect использует identifier (userId в данном случае) для поиска игры.
+ // Передаем socket.id на случай, если игра была AI и identifier был socket.id (хотя при logout должен быть userId).
gameManager.handleDisconnect(socket.id, userId);
}
+
if (loggedInUsers[socket.id]) {
delete loggedInUsers[socket.id];
}
socket.userData = null;
+ // Клиент сам обработает UI после logout (например, покажет экран логина)
+ // Можно отправить подтверждение, но обычно не требуется: socket.emit('logoutResponse', { success: true });
console.log(`[Socket.IO] User ${username} (Socket: ${socket.id}) logged out.`);
});
+ // --- Обработчики событий Управления Играми ---
+ // Все эти события делегируются в GameManager
+
socket.on('createGame', (data) => {
- const identifier = socket.userData?.userId || socket.id;
+ const identifier = socket.userData?.userId || socket.id; // userId для залогиненных, socket.id для гостей (AI игра)
const mode = data?.mode || 'ai';
+
if (mode === 'pvp' && !socket.userData) {
socket.emit('gameError', { message: 'Необходимо войти в систему для создания PvP игры.' });
return;
@@ -117,63 +132,59 @@ io.on('connection', (socket) => {
});
socket.on('requestPvPGameList', () => {
+ // console.log(`[Socket.IO] Request PvP Game List from ${socket.userData?.username || socket.id}`);
const availableGames = gameManager.getAvailablePvPGamesListForClient();
socket.emit('availablePvPGamesList', availableGames);
});
socket.on('requestGameState', () => {
if (!socket.userData?.userId) {
+ // console.log(`[Socket.IO] Request Game State from unauthenticated socket ${socket.id}.`);
socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' });
return;
}
+ // console.log(`[Socket.IO] Request Game State from ${socket.userData.username} (ID: ${socket.userData.userId}).`);
gameManager.handleRequestGameState(socket, socket.userData.userId);
});
+ // --- Обработчик события Игрового Действия ---
socket.on('playerAction', (actionData) => {
- const identifier = socket.userData?.userId || socket.id;
+ const identifier = socket.userData?.userId || socket.id; // Идентификатор для GameManager
+ // console.log(`[Socket.IO] Player Action from ${identifier} (socket ${socket.id}):`, actionData);
gameManager.handlePlayerAction(identifier, actionData);
});
+ // --- Обработчик отключения сокета ---
socket.on('disconnect', (reason) => {
const identifier = socket.userData?.userId || socket.id;
console.log(`[Socket.IO] Пользователь отключился: ${socket.id} (Причина: ${reason}). Identifier: ${identifier}`);
- gameManager.handleDisconnect(socket.id, identifier);
+
+ gameManager.handleDisconnect(socket.id, identifier); // Передаем и socketId, и identifier
+
if (loggedInUsers[socket.id]) {
delete loggedInUsers[socket.id];
}
+ // socket.userData очистится автоматически при уничтожении объекта socket
});
});
-// Запуск HTTP сервера
-// Используем переменные окружения или значения по умолчанию
-const PORT = parseInt(process.env.BC_APP_PORT || '3200', 10);
-const HOSTNAME = process.env.BC_APP_HOSTNAME || '127.0.0.1';
+const PORT = process.env.BC_INTERNAL_PORT || 3200; // Внутренний порт для bc.js
+const HOSTNAME = '127.0.0.1'; // Слушать ТОЛЬКО на localhost
-// Проверка, что порт является числом
-if (isNaN(PORT)) {
- console.error(`[Server FATAL] Некорректное значение для BC_APP_PORT: "${process.env.BC_APP_PORT}". Ожидается число.`);
- process.exit(1);
-}
-
-server.listen(PORT, HOSTNAME, () => {
+server.listen(PORT, HOSTNAME, () => { // Явно указываем HOSTNAME
console.log(`Battle Club HTTP Application Server running at http://${HOSTNAME}:${PORT}`);
- if (HOSTNAME === '127.0.0.1') {
- console.log(`Server is listening on localhost only. This is suitable if a reverse proxy handles external traffic.`);
- } else if (HOSTNAME === '0.0.0.0') {
- console.log(`Server is listening on all available network interfaces.`);
- } else {
- console.log(`Server is listening on a specific interface: ${HOSTNAME}.`);
- }
- console.log(`Environment (NODE_ENV): ${process.env.NODE_ENV || 'not set (defaults to development behavior)'}`);
- console.log(`Serving static files from: ${path.join(__dirname, '..', 'public')}`);
+ console.log(`This server should only be accessed locally by the reverse proxy.`);
+ console.log(`Serving static files from: ${path.join(__dirname, 'public')}`);
});
-
+// Обработка необработанных промис-ошибок
process.on('unhandledRejection', (reason, promise) => {
console.error('[Server FATAL] Unhandled Rejection at:', promise, 'reason:', reason);
+ // В продакшене здесь может быть более сложная логика или перезапуск процесса
// process.exit(1);
});
process.on('uncaughtException', (err) => {
console.error('[Server FATAL] Uncaught Exception:', err);
- process.exit(1);
+ // Критическая ошибка, обычно требует перезапуска приложения
+ process.exit(1); // Аварийное завершение процесса
});
\ No newline at end of file
diff --git a/server/core/db.js b/server/core/db.js
index eb9e4b6..151545b 100644
--- a/server/core/db.js
+++ b/server/core/db.js
@@ -1,53 +1,44 @@
// /server/core/db.js
-require('dotenv').config({ path: require('node:path').resolve(process.cwd(), '.env') }); // Загружаем переменные из .env в process.env
const mysql = require('mysql2'); // Используем mysql2 для поддержки промисов и улучшенной производительности
// Конфигурация подключения к вашей базе данных MySQL
-// Значения теперь берутся из переменных окружения (файла .env)
-const dbConfig = {
- host: process.env.DB_HOST || 'localhost',
- user: process.env.DB_USER, // Обязательно должно быть задано в .env
- password: process.env.DB_PASSWORD, // Обязательно должно быть задано в .env
- database: process.env.DB_NAME, // Обязательно должно быть задано в .env
- port: parseInt(process.env.DB_PORT || '3306', 10), // Порт по умолчанию 3306, если не указан
- waitForConnections: process.env.DB_WAIT_FOR_CONNECTIONS ? (process.env.DB_WAIT_FOR_CONNECTIONS === 'true') : true,
- connectionLimit: parseInt(process.env.DB_CONNECTION_LIMIT || '10', 10),
- queueLimit: parseInt(process.env.DB_QUEUE_LIMIT || '0', 10)
+// ЗАМЕНИТЕ значения на ваши реальные данные!
+const dbConfig = { //Данные для сервера user phpmyadmin password Innamorato8Art
+ host: 'localhost', // или IP-адрес вашего MySQL сервера
+ user: 'phpmyadmin', // Имя пользователя MySQL (например, 'root' для локальной разработки)
+ password: 'Innamorato8Art', // Пароль пользователя MySQL
+ database: 'your_game_db', // Имя вашей базы данных (например, 'your_game_db')
+ port: 3306, // Стандартный порт MySQL, измените если у вас другой
+ waitForConnections: true, // Ожидать доступного соединения, если все заняты
+ connectionLimit: 10, // Максимальное количество соединений в пуле
+ queueLimit: 0 // Максимальное количество запросов в очереди (0 = безлимитно)
};
-// Проверка, что все обязательные переменные окружения для БД заданы
-if (!dbConfig.user || !dbConfig.password || !dbConfig.database || !dbConfig.host) {
- console.error('[DB FATAL] Не все обязательные переменные окружения для БД заданы!');
- console.error('Убедитесь, что у вас есть файл .env в корне проекта и он содержит как минимум:');
- console.error('DB_HOST, DB_USER, DB_PASSWORD, DB_NAME');
- console.error('Текущие загруженные (некоторые могут быть undefined):');
- console.error(` DB_HOST: ${process.env.DB_HOST}`);
- console.error(` DB_USER: ${process.env.DB_USER}`);
- console.error(` DB_PASSWORD: ${process.env.DB_PASSWORD ? '****** (задано)' : 'undefined'}`); // Не выводим пароль в лог
- console.error(` DB_NAME: ${process.env.DB_NAME}`);
- console.error(` DB_PORT: ${process.env.DB_PORT}`);
- process.exit(1); // Завершаем приложение, так как без БД оно не сможет работать корректно.
-}
-
-// Создаем пул соединений.
+// Создаем пул соединений. Пул более эффективен для веб-приложений,
+// чем создание нового соединения для каждого запроса.
let pool;
try {
pool = mysql.createPool(dbConfig);
- console.log('[DB] Пул соединений MySQL успешно создан с конфигурацией из переменных окружения.');
+ console.log('[DB] Пул соединений MySQL успешно создан.');
} catch (error) {
- console.error('[DB FATAL] Не удалось создать пул соединений MySQL. Проверьте конфигурацию и переменные окружения. Ошибка:', error);
+ console.error('[DB FATAL] Не удалось создать пул соединений MySQL. Проверьте конфигурацию `dbConfig`. Ошибка:', error);
+ // Если пул не создался, дальнейшая работа с БД невозможна.
+ // Завершаем приложение, так как без БД оно не сможет работать корректно.
process.exit(1);
}
// Обертка для выполнения запросов с использованием промисов из пула
+// Мы экспортируем именно эту обертку.
const promisePool = pool.promise();
// Проверка соединения (опционально, но полезно для отладки при запуске)
-if (promisePool) {
+// Делаем это после экспорта, чтобы модуль мог быть загружен даже если проверка упадет,
+// хотя в данном случае мы завершаем процесс, если пул не создался.
+if (promisePool) { // Проверяем, что promisePool был успешно создан
promisePool.getConnection()
.then(connection => {
- console.log(`[DB] Успешно подключено к базе данных MySQL (${dbConfig.database}) на ${dbConfig.host}:${dbConfig.port} и получено соединение из пула.`);
- connection.release();
+ console.log('[DB] Успешно подключено к базе данных MySQL и получено соединение из пула.');
+ connection.release(); // Важно!!! Возвращаем соединение в пул
console.log('[DB] Соединение возвращено в пул.');
})
.catch(err => {
@@ -64,20 +55,25 @@ if (promisePool) {
} else if (err.code === 'ECONNREFUSED') {
console.error(`[DB] Соединение с БД было отклонено. Убедитесь, что сервер MySQL запущен и доступен по адресу ${dbConfig.host}:${dbConfig.port}.`);
} else if (err.code === 'ER_ACCESS_DENIED_ERROR') {
- console.error(`[DB] Доступ к БД запрещен для пользователя '${dbConfig.user}'. Проверьте имя пользователя и пароль в вашем файле .env.`);
+ console.error(`[DB] Доступ к БД запрещен для пользователя '${dbConfig.user}'. Проверьте имя пользователя и пароль в server/core/db.js.`);
} else if (err.code === 'ER_BAD_DB_ERROR') {
- console.error(`[DB] База данных "${dbConfig.database}" не найдена. Убедитесь, что она создана на сервере MySQL и указана верно в .env (DB_NAME).`);
+ console.error(`[DB] База данных "${dbConfig.database}" не найдена. Убедитесь, что она создана на сервере MySQL.`);
} else {
console.error(`[DB] Неизвестная ошибка подключения к MySQL. Код: ${err.code}`);
}
- // process.exit(1); // Раскомментируйте, если хотите падать при ошибке подключения
+ // В продакшене здесь может быть логика переподключения или более изящного завершения работы.
+ // Для разработки важно видеть эти ошибки.
+ // Можно раскомментировать process.exit(1), если хотите, чтобы приложение падало при ошибке подключения к БД.
+ // process.exit(1);
});
} else {
- console.error('[DB FATAL] promisePool не был создан. Это не должно было случиться.');
- process.exit(1);
+ // Эта ветка не должна выполниться, если pool.promise() не выбросил ошибку выше.
+ // Но на всякий случай оставляем лог.
+ console.error('[DB FATAL] promisePool не был создан. Проверьте создание `pool`.');
+ process.exit(1); // Завершаем, так как это критическая ошибка
}
-// Экспортируем пул с промисами
+// Экспортируем пул с промисами, чтобы его можно было использовать в других модулях (например, в authService.js)
module.exports = promisePool;
/*
diff --git a/style_alt.css b/style_alt.css
new file mode 100644
index 0000000..531a3b7
--- /dev/null
+++ b/style_alt.css
@@ -0,0 +1,1309 @@
+/* === style_alt.css (Изменения для user-info и game-header) === */
+@import url('https://fonts.googleapis.com/css2?family=MedievalSharp&family=Roboto:wght@300;400;700&display=swap');
+@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css');
+
+:root {
+ /* --- Переменные цветов и шрифтов --- */
+ --font-main: 'Roboto', sans-serif;
+ --font-fancy: 'MedievalSharp', cursive;
+
+ --bg-gradient-dark: linear-gradient(160deg, #1f243a, #10121c);
+ --panel-bg: rgba(16, 18, 28, 0.8);
+ --panel-border: #4a5072;
+
+ --panel-glow-player: rgba(80, 150, 255, 0.3);
+ --panel-glow-opponent: rgba(255, 80, 80, 0.3);
+ --panel-glow-almagest: rgba(199, 108, 255, 0.3);
+
+
+ --text-light: #e8effc;
+ --text-muted: #9badce;
+ --text-heading: #ffffff;
+
+ --accent-player: #6c95ff;
+ --accent-opponent: #ff6c6c;
+ --accent-almagest: #c76cff;
+
+ --hp-color: #de4b4b;
+ --mana-color: #58a8d0;
+ --stamina-color: #ffb347;
+ --dark-energy-color: #ab47bc;
+ --bar-bg: #252a44;
+
+ --button-bg: linear-gradient(145deg, #556190, #3f4a70);
+ --button-hover-bg: linear-gradient(145deg, #6a79b0, #556190);
+ --button-text: var(--text-light);
+
+ --button-ability-bg: linear-gradient(145deg, #305a5e, #1f4043);
+ --button-ability-hover-bg: linear-gradient(145deg, #407a7e, #305a5e);
+ --button-ability-border: #4db0b5;
+
+ --button-disabled-bg: #333950;
+ --button-disabled-text: #6b7491;
+
+ --log-bg: rgba(10, 12, 20, 0.85);
+ --log-border: var(--panel-border);
+ --log-text: var(--text-muted);
+
+ --icon-color: var(--text-muted);
+ --damage-color: #ff8080;
+ --heal-color: #90ee90;
+ --block-color: #add8e6;
+ --effect-color: #d8bfd8;
+ --turn-color: #ffd700;
+ --system-color: #7fffd4;
+
+ --modal-bg: rgba(16, 18, 28, 0.97);
+ --modal-content-bg: #2a2f45;
+
+ --scrollbar-thumb: #4a5072;
+ --scrollbar-track: #10121c;
+
+ --shake-duration: 0.4s;
+ --cast-duration: 0.6s;
+ --dissolve-duration: 6.0s;
+
+ --log-panel-fixed-height: 280px;
+
+ --timer-text-color: var(--turn-color);
+ --timer-icon-color: #b0c4de;
+ --timer-low-time-color: var(--damage-color);
+}
+
+/* --- Базовые Стили и Сброс --- */
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ height: 100%;
+}
+
+body {
+ font-family: var(--font-main);
+ background: var(--bg-gradient-dark) fixed;
+ color: var(--text-light);
+ line-height: 1.5;
+ height: 100vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 10px;
+}
+
+h1, h2, h3, h4 {
+ font-family: var(--font-fancy);
+ color: var(--text-heading);
+ margin-bottom: 0.75em;
+ font-weight: normal;
+}
+
+button {
+ font-family: var(--font-main);
+}
+
+i {
+ margin-right: 6px;
+ color: var(--icon-color);
+ width: 1.2em;
+ text-align: center;
+}
+
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
+}
+
+*::-webkit-scrollbar {
+ width: 8px;
+}
+
+*::-webkit-scrollbar-track {
+ background: var(--scrollbar-track);
+ border-radius: 4px;
+}
+
+*::-webkit-scrollbar-thumb {
+ background-color: var(--scrollbar-thumb);
+ border-radius: 4px;
+ border: 2px solid var(--scrollbar-track);
+}
+
+
+/* === Стили для Экранов Аутентификации и Настройки Игры === */
+.auth-game-setup-wrapper {
+ width: 100%;
+ max-width: 700px;
+ margin: 20px auto;
+ background: var(--panel-bg);
+ border: 1px solid var(--panel-border);
+ border-radius: 10px;
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5);
+ color: var(--text-light);
+ text-align: center;
+ max-height: calc(100vh - 40px);
+ overflow-y: hidden;
+ position: relative; /* <<< Добавлено для позиционирования #user-info */
+}
+
+.auth-game-setup-wrapper h2,
+.auth-game-setup-wrapper h3 {
+ font-family: var(--font-fancy);
+ color: var(--text-heading);
+ margin-bottom: 1em;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ padding-bottom: 0.5em;
+}
+
+.auth-game-setup-wrapper h3 {
+ font-size: 1.2em;
+ margin-top: 1.5em;
+}
+
+.auth-game-setup-wrapper button,
+#auth-section form button {
+ font-family: var(--font-main);
+ background: var(--button-bg);
+ color: var(--button-text);
+ border: 1px solid rgba(0, 0, 0, 0.3);
+ border-radius: 6px;
+ padding: 10px 18px;
+ margin: 8px 5px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ font-weight: bold;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+ outline: none;
+}
+
+.auth-game-setup-wrapper button:hover:enabled,
+#auth-section form button:hover:enabled {
+ background: var(--button-hover-bg);
+ transform: translateY(-2px) scale(1.02);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
+}
+
+.auth-game-setup-wrapper button:active:enabled,
+#auth-section form button:active:enabled {
+ transform: translateY(0px) scale(1);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+.auth-game-setup-wrapper button:disabled,
+#auth-section form button:disabled {
+ background: var(--button-disabled-bg) !important;
+ color: var(--button-disabled-text) !important;
+ border-color: transparent !important;
+ cursor: not-allowed !important;
+ opacity: 0.7;
+ transform: none !important;
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4) !important;
+ filter: grayscale(50%);
+}
+
+.auth-game-setup-wrapper input[type="text"],
+#auth-section input[type="text"],
+#auth-section input[type="password"] {
+ padding: 10px;
+ border-radius: 5px;
+ border: 1px solid var(--panel-border);
+ background-color: var(--bar-bg);
+ color: var(--text-light);
+ margin: 5px 5px 10px 5px;
+ font-size: 0.9em;
+ width: calc(100% - 22px);
+ max-width: 300px;
+ box-sizing: border-box;
+ outline: none;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.auth-game-setup-wrapper input[type="text"]:focus,
+#auth-section input[type="text"]:focus,
+#auth-section input[type="password"]:focus {
+ border-color: var(--accent-player);
+ box-shadow: 0 0 8px rgba(108, 149, 255, 0.4);
+}
+
+#available-games-list {
+ margin-top: 20px;
+ text-align: left;
+ max-height: 250px;
+ height: 100px;
+ overflow-y: scroll;
+ padding: 10px 15px;
+ background-color: rgba(0, 0, 0, 0.25);
+ border: 1px solid var(--log-border);
+ border-radius: 6px;
+}
+
+#available-games-list h3 {
+ margin-top: 0;
+ margin-bottom: 10px;
+ padding-bottom: 5px;
+ border-bottom: 1px dashed rgba(255, 255, 255, 0.1);
+}
+
+#available-games-list ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+#available-games-list li {
+ padding: 10px;
+ border-bottom: 1px solid rgba(74, 80, 114, 0.5);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 0.9em;
+}
+
+#available-games-list li:last-child {
+ border-bottom: none;
+}
+
+#available-games-list li button {
+ padding: 6px 10px;
+ font-size: 0.8em;
+ margin-left: 10px;
+ flex-shrink: 0;
+}
+
+#status-container {
+ height: 40px;
+}
+
+#auth-message,
+#game-status-message {
+ font-weight: bold;
+ font-size: 1.1em;
+ padding: 5px;
+ background-color: rgba(0, 0, 0, 0.1);
+ border-radius: 4px;
+ display: block;
+ margin-bottom: 5px;
+ text-align: center;
+}
+
+#auth-message.success {
+ color: var(--heal-color);
+}
+
+#auth-message.error {
+ color: var(--damage-color);
+}
+
+#game-status-message {
+ color: var(--turn-color);
+}
+
+
+#auth-section form {
+ margin-bottom: 20px;
+}
+
+/* === ИЗМЕНЕНИЕ: Стили для #user-info === */
+#user-info {
+ position: absolute;
+ top: 10px; /* Отступ сверху */
+ right: 15px; /* Отступ справа */
+ line-height: 1.5;
+ text-align: right; /* Выравнивание текста и кнопки вправо */
+ z-index: 10; /* Чтобы был поверх другого контента в .auth-game-setup-wrapper */
+}
+
+#user-info p {
+ margin: 0 10px 0 0; /* Уменьшен нижний отступ */
+ font-size: 0.9em; /* Уменьшен шрифт приветствия */
+ color: var(--text-muted);
+ line-height: 2.5;
+}
+
+#user-info p #logged-in-username {
+ font-weight: bold;
+ color: var(--text-light);
+}
+
+#user-info div {
+ display: flex;
+ flex-direction: row;
+}
+
+#logout-button {
+ background: linear-gradient(145deg, #6e3c3c, #502626) !important; /* Более темный красный */
+ color: #f0d0d0 !important; /* Светло-розовый текст */
+ padding: 6px 12px !important; /* Уменьшены паддинги */
+ font-size: 0.8em !important; /* Уменьшен шрифт */
+ margin: 0 !important; /* Убираем внешние отступы */
+ letter-spacing: 0.2px;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
+ border: 1px solid #422020 !important; /* Темная рамка */
+}
+
+#logout-button:hover:enabled {
+ background: linear-gradient(145deg, #834545, #6e3c3c) !important;
+ transform: translateY(-1px) !important;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4) !important;
+}
+#logout-button i {
+ margin-right: 4px; /* Уменьшен отступ иконки */
+}
+/* === КОНЕЦ ИЗМЕНЕНИЯ === */
+
+
+.character-selection {
+ margin-top: 15px;
+ margin-bottom: 15px;
+ padding: 15px;
+ background-color: rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ border: 1px solid rgba(74, 80, 114, 0.5);
+}
+
+.character-selection h4 {
+ font-size: 1.1em;
+ color: var(--text-muted);
+ margin-bottom: 10px;
+ border: none;
+ padding: 0;
+ text-align: center;
+}
+
+.character-selection label {
+ display: inline-block;
+ margin: 0 15px;
+ cursor: pointer;
+ font-size: 1.05em;
+ padding: 5px 10px;
+ border-radius: 4px;
+ transition: background-color 0.2s ease, color 0.2s ease;
+ user-select: none;
+}
+
+.character-selection input[type="radio"] {
+ display: none;
+}
+
+.character-selection input[type="radio"]:checked + label {
+ color: #fff;
+ font-weight: bold;
+}
+
+.character-selection input[type="radio"][value="elena"]:checked + label {
+ background-color: var(--accent-player);
+ box-shadow: 0 0 8px rgba(108, 149, 255, 0.5);
+}
+
+.character-selection input[type="radio"][value="almagest"]:checked + label {
+ background-color: var(--accent-almagest);
+ box-shadow: 0 0 8px rgba(199, 108, 255, 0.5);
+}
+
+.character-selection label:hover {
+ background-color: rgba(255, 255, 255, 0.1);
+}
+
+.character-selection label i {
+ margin-right: 8px;
+ vertical-align: middle;
+}
+
+label[for="char-elena"] i {
+ color: var(--accent-player);
+}
+
+label[for="char-almagest"] i {
+ color: var(--accent-almagest);
+}
+
+
+/* --- Основная Структура Игры (.game-wrapper) --- */
+.game-wrapper {
+ width: 100%;
+ height: 100%;
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ overflow: hidden;
+}
+
+/* === ИЗМЕНЕНИЕ: .game-header удален, стили для него больше не нужны === */
+
+
+.battle-arena-container {
+ flex-grow: 1;
+ display: flex;
+ gap: 10px;
+ overflow: hidden;
+ /* === ИЗМЕНЕНИЕ: Добавляем верхний отступ, если .game-header был убран, а .game-wrapper виден === */
+ /* margin-top: 10px; /* или padding-top: 10px; на .game-wrapper, если нужно */
+}
+
+.player-column,
+.opponent-column {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.fighter-panel,
+.controls-panel-new,
+.battle-log-new {
+ background: var(--panel-bg);
+ border: 1px solid var(--panel-border);
+ border-radius: 8px;
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.4), inset 0 0 10px rgba(0, 0, 0, 0.3);
+ padding: 15px;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ transition: box-shadow 0.3s ease, border-color 0.3s ease, opacity 0.3s ease-out, transform 0.3s ease-out;
+}
+
+.fighter-panel.panel-elena {
+ border-color: var(--accent-player);
+}
+.fighter-panel.panel-almagest {
+ border-color: var(--accent-almagest);
+}
+.fighter-panel.panel-balard {
+ border-color: var(--accent-opponent);
+}
+
+
+.panel-header {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ margin-bottom: 0;
+}
+
+.fighter-name {
+ font-size: 1.6em;
+ margin: 0;
+ flex-grow: 1;
+ text-align: left;
+}
+
+.fighter-name .icon-elena { color: var(--accent-player); }
+.fighter-name .icon-almagest { color: var(--accent-almagest); }
+.fighter-name .icon-balard { color: var(--accent-opponent); }
+
+
+.character-visual {
+ flex-shrink: 0;
+ margin-bottom: 0;
+}
+
+.avatar-image {
+ display: block;
+ max-width: 50px;
+ height: auto;
+ border-radius: 50%;
+ border: 2px solid var(--panel-border);
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+}
+
+.avatar-image.avatar-elena { border-color: var(--accent-player); }
+.avatar-image.avatar-almagest { border-color: var(--accent-almagest); }
+.avatar-image.avatar-balard { border-color: var(--accent-opponent); }
+
+
+.panel-content {
+ flex-grow: 1;
+ overflow-y: auto;
+ padding-right: 5px;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ padding-top: 10px;
+ margin-top: 0;
+}
+
+.stat-bar-container {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-shrink: 0;
+}
+
+.stat-bar-container .bar-icon {
+ flex-shrink: 0;
+ font-size: 1.4em;
+}
+
+.stat-bar-container.health .bar-icon { color: var(--hp-color); }
+.stat-bar-container.mana .bar-icon { color: var(--mana-color); }
+.stat-bar-container.stamina .bar-icon { color: var(--stamina-color); }
+.stat-bar-container.dark-energy .bar-icon { color: var(--dark-energy-color); }
+
+
+.bar-wrapper {
+ flex-grow: 1;
+}
+
+.bar {
+ border-radius: 4px;
+ height: 20px;
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ overflow: hidden;
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
+ position: relative;
+ background-color: var(--bar-bg);
+}
+
+.bar-fill {
+ display: block;
+ height: 100%;
+ border-radius: 3px;
+ position: relative;
+ z-index: 2;
+ transition: width 0.4s ease-out;
+}
+
+.bar-text {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 3;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 0.75em;
+ font-weight: bold;
+ color: #fff;
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.9);
+ padding: 0 5px;
+ white-space: nowrap;
+ pointer-events: none;
+}
+
+.health .bar-fill { background-color: var(--hp-color); }
+.mana .bar-fill { background-color: var(--mana-color); }
+.stamina .bar-fill { background-color: var(--stamina-color); }
+.dark-energy .bar-fill { background-color: var(--dark-energy-color); }
+
+
+.status-area {
+ font-size: 0.9em;
+ display: flex;
+ align-items: baseline;
+ gap: 5px;
+ flex-shrink: 0;
+ min-height: 1.5em;
+}
+
+.status-area .icon-status {
+ font-size: 1em;
+ flex-shrink: 0;
+ margin-top: 0.1em;
+}
+
+.status-area strong {
+ color: var(--text-muted);
+ font-weight: normal;
+ flex-shrink: 0;
+ margin-right: 3px;
+}
+
+.status-area span {
+ font-weight: bold;
+}
+
+.status-area span.blocking {
+ color: var(--block-color);
+ font-style: italic;
+}
+
+.effects-area {
+ font-size: 0.9em;
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+ min-height: 3em;
+}
+
+.effect-category {
+ display: flex;
+ align-items: baseline;
+ gap: 5px;
+}
+
+.effect-category strong {
+ color: var(--text-muted);
+ font-weight: normal;
+ font-family: var(--font-main);
+ font-size: 0.9em;
+ flex-shrink: 0;
+ margin-right: 3px;
+}
+
+.effect-category .icon-effects-buff,
+.effect-category .icon-effects-debuff {
+ font-size: 1em;
+ flex-shrink: 0;
+ margin-top: 0.1em;
+ width: 1.2em;
+ text-align: center;
+}
+
+.effect-category .icon-effects-buff { color: var(--heal-color); }
+.effect-category .icon-effects-debuff { color: var(--damage-color); }
+
+.effect-list {
+ display: inline;
+ line-height: 1.4;
+ min-width: 0;
+ font-weight: bold;
+}
+
+.effect {
+ display: inline-block;
+ margin: 2px 3px 2px 0;
+ padding: 1px 6px;
+ font-size: 0.8em;
+ border-radius: 10px;
+ border: 1px solid;
+ cursor: default;
+ font-weight: 600;
+ background-color: rgba(0, 0, 0, 0.2);
+ white-space: nowrap;
+ vertical-align: baseline;
+}
+
+.effect-buff { border-color: var(--heal-color); color: var(--heal-color); }
+.effect-debuff { border-color: var(--damage-color); color: var(--damage-color); }
+.effect-stun { border-color: var(--turn-color); color: var(--turn-color); }
+.effect-block { border-color: var(--block-color); color: var(--block-color); }
+.effect-info { border-color: var(--text-muted); color: var(--text-muted); }
+
+.controls-panel-new {
+ flex-grow: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+#turn-indicator {
+ flex-shrink: 0;
+ text-align: center;
+ font-size: 1.4em;
+ margin-bottom: 10px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ transition: color 0.3s ease;
+}
+
+.turn-timer-display {
+ flex-shrink: 0;
+ text-align: center;
+ font-size: 0.9em;
+ color: var(--timer-text-color);
+ margin-top: -5px;
+ margin-bottom: 10px;
+ padding: 5px;
+ background-color: rgba(0,0,0,0.15);
+ border-radius: 4px;
+ border-top: 1px solid rgba(255,255,255,0.05);
+}
+
+.turn-timer-display i {
+ color: var(--timer-icon-color);
+ margin-right: 8px;
+}
+
+#turn-timer {
+ font-weight: bold;
+ font-size: 1.1em;
+ min-width: 35px;
+ display: inline-block;
+ text-align: left;
+}
+
+#turn-timer.low-time {
+ color: var(--timer-low-time-color);
+ animation: pulse-timer-warning 1s infinite ease-in-out;
+}
+
+.controls-layout {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ overflow: hidden;
+ min-height: 0;
+}
+
+.control-group {
+ flex-shrink: 0;
+}
+
+.control-group h4 {
+ font-size: 0.9em;
+ color: var(--text-muted);
+ margin-bottom: 5px;
+ padding-bottom: 5px;
+ border-bottom: 1px dashed var(--panel-border);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.basic-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.action-button.basic {
+ flex: 1;
+ padding: 8px 5px;
+ font-size: 0.85em;
+ font-weight: bold;
+ background: var(--button-bg);
+ color: var(--button-text);
+ border: 1px solid rgba(0, 0, 0, 0.3);
+ border-radius: 5px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
+ outline: none;
+}
+
+.action-button.basic:hover:enabled {
+ background: var(--button-hover-bg);
+ transform: translateY(-1px);
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.5);
+}
+
+.action-button.basic:active:enabled {
+ transform: translateY(0px);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
+}
+
+#button-attack.attack-buffed:enabled {
+ border: 2px solid var(--heal-color);
+ box-shadow: 0 0 10px 2px rgba(144, 238, 144, 0.6), 0 3px 6px rgba(0, 0, 0, 0.5);
+ background: linear-gradient(145deg, #70c070, #5a9a5a);
+ transform: translateY(-1px);
+}
+
+
+.ability-list {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.ability-list h4 {
+ flex-shrink: 0;
+}
+
+.abilities-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(75px, 1fr));
+ gap: 8px;
+ padding: 8px;
+ padding-bottom: 12px;
+ background-color: rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+ overflow-y: auto;
+ border: 1px solid rgba(0, 0, 0, 0.3);
+ flex-grow: 1;
+ position: relative;
+}
+
+.abilities-grid::after {
+ content: '';
+ display: block;
+ height: 10px;
+ width: 100%;
+}
+
+.abilities-grid .placeholder-text {
+ grid-column: 1 / -1;
+ text-align: center;
+ color: var(--text-muted);
+ align-self: center;
+ font-size: 0.9em;
+ padding: 15px 0;
+}
+
+.ability-button {
+ aspect-ratio: 1 / 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ padding: 5px;
+ border-radius: 6px;
+ background: var(--button-ability-bg);
+ border: 1px solid var(--button-ability-border);
+ color: #fff;
+ text-align: center;
+ line-height: 1.15;
+ cursor: pointer;
+ transition: all 0.2s ease-out;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.6);
+ position: relative;
+ overflow: hidden;
+ outline: none;
+}
+
+.ability-button .ability-name {
+ font-size: 0.75em;
+ font-weight: bold;
+ margin-bottom: 2px;
+ display: block;
+ width: 95%;
+}
+
+.ability-button .ability-desc {
+ font-size: 0.65em;
+ font-weight: normal;
+ color: #aaccce;
+ opacity: 0.8;
+ text-shadow: none;
+ max-height: 2em;
+ overflow: hidden;
+ width: 95%;
+ display: block;
+ margin-top: auto;
+}
+
+.ability-button:hover:enabled {
+ transform: scale(1.03) translateY(-1px);
+ background: var(--button-ability-hover-bg);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5), 0 0 8px rgba(77, 176, 181, 0.4);
+ border-color: #77d9dd;
+}
+
+.ability-button:active:enabled {
+ transform: scale(1) translateY(0);
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.3);
+ filter: brightness(0.9);
+}
+
+.ability-button:disabled,
+.action-button.basic:disabled {
+ background: var(--button-disabled-bg) !important;
+ border-color: transparent !important;
+ color: var(--button-disabled-text) !important;
+ cursor: not-allowed !important;
+ transform: none !important;
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4) !important;
+ opacity: 0.7;
+ text-shadow: none !important;
+ filter: grayscale(50%);
+}
+
+.ability-button.not-enough-resource {
+ border: 2px dashed var(--damage-color);
+ animation: pulse-red-border 1s infinite ease-in-out;
+}
+
+.ability-button.not-enough-resource:disabled {
+ border-color: var(--damage-color);
+ box-shadow: inset 0 0 8px rgba(255, 80, 80, 0.2), 0 3px 6px rgba(0, 0, 0, 0.2), inset 0 1px 3px rgba(0, 0, 0, 0.4);
+}
+
+.ability-button.buff-is-active {
+ border: 2px solid var(--heal-color);
+ box-shadow: 0 0 8px rgba(144, 238, 144, 0.5);
+}
+
+.ability-button.buff-is-active:disabled {
+ border-color: var(--heal-color);
+}
+
+.ability-button.is-on-cooldown,
+.ability-button.is-silenced {
+ filter: grayscale(70%) brightness(0.8);
+}
+
+.ability-button.is-on-cooldown:disabled,
+.ability-button.is-silenced:disabled {
+ filter: grayscale(70%) brightness(0.7);
+}
+
+.ability-button.is-on-cooldown .ability-name,
+.ability-button.is-silenced .ability-name,
+.ability-button.is-on-cooldown .ability-desc,
+.ability-button.is-silenced .ability-desc {
+ opacity: 0.6;
+}
+
+.ability-button.is-on-cooldown .ability-desc,
+.ability-button.is-silenced .ability-desc {
+ display: none;
+}
+
+.ability-cooldown-display {
+ position: absolute;
+ bottom: 5px;
+ left: 0;
+ width: 100%;
+ text-align: center;
+ font-size: 0.75em;
+ font-weight: bold;
+ color: var(--turn-color);
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
+ pointer-events: none;
+ display: none;
+ line-height: 1;
+}
+
+.ability-button.is-on-cooldown .ability-cooldown-display,
+.ability-button.is-silenced .ability-cooldown-display {
+ display: block !important;
+}
+
+
+.battle-log-new {
+ height: var(--log-panel-fixed-height);
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.battle-log-new h3 {
+ flex-shrink: 0;
+ font-size: 1.4em;
+ margin-bottom: 10px;
+ text-align: center;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ padding-bottom: 8px;
+}
+
+#log-list {
+ list-style: none;
+ flex-grow: 1;
+ overflow-y: auto;
+ background-color: var(--log-bg);
+ border: 1px solid var(--log-border);
+ font-size: 0.85em;
+ border-radius: 6px;
+ color: var(--log-text);
+ padding: 10px;
+ min-height: 0;
+ word-wrap: break-word;
+}
+
+#log-list li {
+ padding: 4px 8px;
+ border-bottom: 1px solid rgba(74, 80, 114, 0.5);
+ line-height: 1.35;
+ transition: background-color 0.3s;
+}
+
+#log-list li:last-child {
+ border-bottom: none;
+}
+
+#log-list li:hover {
+ background-color: rgba(255, 255, 255, 0.03);
+}
+
+.log-damage { color: var(--damage-color); font-weight: 500; }
+.log-heal { color: var(--heal-color); font-weight: 500; }
+.log-block { color: var(--block-color); font-style: italic; }
+.log-info { color: #b0c4de; }
+.log-turn {
+ font-weight: bold;
+ color: var(--turn-color);
+ margin-top: 6px;
+ border-top: 1px solid rgba(255, 215, 0, 0.3);
+ padding-top: 6px;
+ font-size: 1.05em;
+ display: block;
+}
+.log-system {
+ font-weight: bold;
+ color: var(--system-color);
+ font-style: italic;
+ opacity: 0.8;
+}
+.log-effect {
+ font-style: italic;
+ color: var(--effect-color);
+}
+
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: var(--modal-bg);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+ backdrop-filter: blur(4px) brightness(0.7);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.4s ease-out;
+}
+
+.modal.hidden {
+ display: none !important;
+}
+
+.modal:not(.hidden) {
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.modal-content {
+ background: var(--modal-content-bg);
+ padding: 40px 50px;
+ border-radius: 10px;
+ text-align: center;
+ border: 1px solid var(--panel-border);
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
+ color: var(--text-light);
+ transform: scale(0.8) translateY(30px);
+ opacity: 0;
+ transition: transform 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.2), opacity 0.4s ease-out;
+}
+
+.modal:not(.hidden) .modal-content {
+ transform: scale(1) translateY(0);
+ opacity: 1;
+}
+
+.modal-content h2#result-message {
+ margin-bottom: 25px;
+ font-family: var(--font-fancy);
+ font-size: 2.5em;
+ line-height: 1.2;
+}
+
+.modal-action-button {
+ padding: 12px 30px;
+ font-size: 1.1em;
+ cursor: pointer;
+ background: var(--button-bg);
+ color: var(--button-text);
+ border: 1px solid rgba(0, 0, 0, 0.3);
+ border-radius: 6px;
+ margin-top: 20px;
+ font-weight: bold;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ transition: all 0.2s ease;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
+ outline: none;
+}
+
+.modal-action-button:hover:enabled {
+ background: var(--button-hover-bg);
+ transform: scale(1.05) translateY(-1px);
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
+}
+
+.modal-action-button:active:enabled {
+ transform: scale(1) translateY(0);
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4);
+}
+
+.modal-action-button:disabled {
+ background: var(--button-disabled-bg);
+ color: var(--button-disabled-text);
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+.modal-action-button i {
+ margin-right: 8px;
+}
+
+@keyframes pulse-red-border {
+ 0%, 100% { border-color: var(--damage-color); }
+ 50% { border-color: #ffb3b3; }
+}
+
+@keyframes pulse-timer-warning {
+ 0%, 100% { color: var(--timer-low-time-color); transform: scale(1); }
+ 50% { color: #ff6347; transform: scale(1.05); }
+}
+
+@keyframes flash-effect {
+ 0%, 100% {
+ box-shadow: var(--initial-box-shadow, 0 0 15px rgba(0, 0, 0, 0.4), inset 0 0 10px rgba(0, 0, 0, 0.3));
+ border-color: var(--initial-border-color, var(--panel-border));
+ transform: scale(1);
+ }
+ 50% {
+ box-shadow: 0 0 25px 10px var(--flash-color-outer, rgba(255, 255, 255, 0.7)),
+ inset 0 0 15px var(--flash-color-inner, rgba(255, 255, 255, 0.4)),
+ 0 0 15px rgba(0, 0, 0, 0.4);
+ border-color: var(--flash-border-color, #ffffff);
+ transform: scale(1.005);
+ }
+}
+
+[class*="is-casting-"] {
+ animation: flash-effect var(--cast-duration) ease-out;
+}
+
+#player-panel.is-casting-heal, #opponent-panel.is-casting-heal {
+ --flash-color-outer: rgba(144, 238, 144, 0.7); --flash-color-inner: rgba(144, 238, 144, 0.4);
+ --flash-border-color: var(--heal-color);
+}
+#player-panel.is-casting-fireball, #opponent-panel.is-casting-fireball {
+ --flash-color-outer: rgba(255, 100, 100, 0.7); --flash-color-inner: rgba(255, 100, 100, 0.4);
+ --flash-border-color: var(--damage-color);
+}
+#player-panel.is-casting-shadowBolt, #opponent-panel.is-casting-shadowBolt {
+ --flash-color-outer: rgba(138, 43, 226, 0.6); --flash-color-inner: rgba(138, 43, 226, 0.3);
+ --flash-border-color: var(--dark-energy-color);
+}
+
+@keyframes shake-opponent {
+ 0%, 100% { transform: translateX(0); }
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px) rotate(-0.5deg); }
+ 20%, 40%, 60%, 80% { transform: translateX(4px) rotate(0.5deg); }
+}
+
+#opponent-panel.is-shaking {
+ animation: shake-opponent var(--shake-duration) cubic-bezier(.36, .07, .19, .97) both;
+ transform: translate3d(0, 0, 0);
+ backface-visibility: hidden;
+ perspective: 1000px;
+}
+
+#opponent-panel.dissolving {
+ opacity: 0;
+ transform: scale(0.9) translateY(20px);
+ transition: opacity var(--dissolve-duration) ease-in, transform var(--dissolve-duration) ease-in;
+ pointer-events: none;
+}
+
+@keyframes shake-short {
+ 0%, 100% { transform: translateX(0); }
+ 25% { transform: translateX(-3px); }
+ 50% { transform: translateX(3px); }
+ 75% { transform: translateX(-3px); }
+}
+
+.shake-short {
+ animation: shake-short 0.3s ease-in-out;
+}
+
+@media (max-width: 900px) {
+ body {
+ height: auto; overflow-y: auto;
+ padding: 5px 0; font-size: 15px;
+ justify-content: flex-start;
+ }
+ .auth-game-setup-wrapper { max-height: none; padding-top: 60px; /* Отступ для #user-info */ }
+ /* === ИЗМЕНЕНИЕ: Адаптация #user-info === */
+ #user-info { top: 5px; right: 10px; }
+ #user-info p { font-size: 0.85em; }
+ #logout-button { padding: 5px 10px !important; font-size: 0.75em !important; }
+ /* === КОНЕЦ ИЗМЕНЕНИЯ === */
+
+ .game-wrapper { padding: 5px; gap: 5px; height: auto; }
+ /* === ИЗМЕНЕНИЕ: game-header удален === */
+ .battle-arena-container { flex-direction: column; height: auto; overflow: visible; /* margin-top: 0; /* Если ранее добавляли */ }
+ .player-column, .opponent-column { width: 100%; height: auto; overflow: visible; }
+ .fighter-panel, .controls-panel-new, .battle-log-new {
+ min-height: auto; height: auto; padding: 10px;
+ flex-grow: 0; flex-shrink: 1;
+ }
+ .controls-panel-new { min-height: 200px; }
+ .battle-log-new { height: auto; min-height: 150px; }
+ #log-list { max-height: 200px; }
+ .abilities-grid { max-height: none; overflow-y: visible; padding-bottom: 8px; }
+ .abilities-grid::after { display: none; }
+ .ability-list, .controls-layout { overflow: visible; }
+ .fighter-name { font-size: 1.3em; }
+ .panel-content { margin-top: 10px; }
+ .stat-bar-container .bar-icon { font-size: 1.2em; }
+ .bar { height: 18px; }
+ .effects-area, .effect { font-size: 0.85em; }
+ #turn-indicator { font-size: 1.2em; margin-bottom: 8px; }
+ .turn-timer-display { font-size: 0.85em; margin-bottom: 8px; padding: 4px; }
+ #turn-timer { font-size: 1em; }
+ .action-button.basic { font-size: 0.8em; padding: 8px 4px; }
+ .abilities-grid { grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 8px; padding: 8px; }
+ .ability-button { font-size: 0.75em; padding: 5px; }
+ .ability-button .ability-name { margin-bottom: 2px; }
+ .ability-button .ability-desc { font-size: 0.65em; }
+ .modal-content { padding: 25px 30px; width: 90%; max-width: 400px; }
+ .modal-content h2#result-message { font-size: 1.8em; }
+ .modal-action-button { font-size: 1em; padding: 10px 20px; }
+ #game-setup { max-width: 95%; padding: 15px; }
+ #game-setup h2 { font-size: 1.6em; }
+ #game-setup h3 { font-size: 1.1em; }
+ #game-setup button { padding: 8px 12px; font-size: 0.9em; }
+ #game-setup input[type="text"] { width: calc(100% - 90px); max-width: 200px; padding: 8px; }
+ #available-games-list { max-height: 180px; }
+ .character-selection label { margin: 0 10px; font-size: 1em; }
+}
+
+@media (max-width: 480px) {
+ body { font-size: 14px; }
+ /* === ИЗМЕНЕНИЕ: Адаптация #user-info для мобильных === */
+ .auth-game-setup-wrapper { padding-top: 50px; /* Еще немного места сверху */ }
+ #user-info {
+ top: 5px;
+ right: 5px;
+ display: flex; /* В одну строку */
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+ }
+ #user-info p { margin-bottom: 0; font-size: 0.8em; }
+ #logout-button { padding: 4px 8px !important; font-size: 0.7em !important; }
+ #logout-button i { margin-right: 3px; }
+ /* === КОНЕЦ ИЗМЕНЕНИЯ === */
+
+ /* === ИЗМЕНЕНИЕ: game-header удален === */
+ .fighter-name { font-size: 1.2em; }
+ .abilities-grid { grid-template-columns: repeat(auto-fit, minmax(65px, 1fr)); gap: 5px; padding: 5px; padding-bottom: 10px; }
+ .ability-button { font-size: 0.7em; padding: 4px; }
+ .ability-button .ability-name { margin-bottom: 1px; }
+ .ability-button .ability-desc { display: none; }
+ #log-list { font-size: 0.8em; max-height: 150px; }
+ .modal-content { padding: 20px; }
+ .modal-content h2#result-message { font-size: 1.6em; }
+ .modal-action-button { font-size: 0.9em; padding: 8px 16px; }
+ .auth-game-setup-wrapper { padding-left: 15px; padding-right: 15px; }
+ #game-setup { padding: 10px; }
+ #game-setup h2 { font-size: 1.4em; }
+ #game-setup button { padding: 7px 10px; font-size: 0.85em; margin: 5px; }
+ #game-setup input[type="text"],
+ #game-setup button { display: block; width: 100%; margin-left: 0; margin-right: 0; }
+ #game-setup input[type="text"] { max-width: none; margin-bottom: 10px; }
+ #game-setup div>input[type="text"]+button { margin-top: 5px; }
+ #available-games-list { max-height: 120px; }
+ #available-games-list li button { font-size: 0.75em; padding: 5px 8px; }
+ .character-selection { padding: 10px; }
+ .character-selection label { margin: 0 5px 5px 5px; font-size: 0.9em; display: block; }
+ .character-selection label i { margin-right: 5px; }
+ #turn-indicator { font-size: 1.1em; }
+ .turn-timer-display { font-size: 0.8em; margin-top: -3px; margin-bottom: 6px; }
+ #turn-timer { font-size: 0.95em; }
+}
\ No newline at end of file