bc/public/js/ui.js
2025-05-13 04:14:01 +00:00

336 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /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'),
},
log: {
list: document.getElementById('log-list'),
},
gameOver: {
screen: document.getElementById('game-over-screen'),
message: document.getElementById('result-message'),
restartButton: document.getElementById('restart-game-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'),
};
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) {
console.warn(`updateFighterPanelUI: Отсутствуют элементы/состояние/статы для панели ${panelRole}.`);
return;
}
if (elements.name) {
let iconClass = 'fa-question'; let accentColor = 'var(--text-muted)';
const characterKey = fighterBaseStats.characterKey;
if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-player'; accentColor = 'var(--accent-player)'; }
else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; accentColor = 'var(--accent-almagest)'; } // Используем новый цвет
else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-opponent'; accentColor = 'var(--accent-opponent)'; }
let nameHtml = `<i class="fas ${iconClass}"></i> ${fighterBaseStats.name}`;
if (isControlledByThisClient) nameHtml += " (Вы)";
elements.name.innerHTML = nameHtml; elements.name.style.color = accentColor;
}
if (elements.avatar && fighterBaseStats.avatarPath) elements.avatar.src = fighterBaseStats.avatarPath;
else if (elements.avatar) elements.avatar.src = 'images/default_avatar.png';
const maxHp = Math.max(1, fighterBaseStats.maxHp);
const maxRes = Math.max(1, fighterBaseStats.maxResource);
const currentHp = Math.max(0, fighterState.currentHp);
const currentRes = Math.max(0, fighterState.currentResource);
elements.hpFill.style.width = `${(currentHp / maxHp) * 100}%`;
elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`;
elements.resourceFill.style.width = `${(currentRes / maxRes) * 100}%`;
elements.resourceText.textContent = `${Math.round(currentRes)} / ${fighterBaseStats.maxResource}`;
const resourceBarContainer = elements[`${panelRole}ResourceBarContainer`];
const resourceIconElement = elements[`${panelRole}ResourceTypeIcon`];
if (resourceBarContainer && resourceIconElement) {
resourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
let resourceClass = 'mana'; let iconClass = 'fa-flask';
if (fighterBaseStats.resourceName === 'Ярость') { resourceClass = 'stamina'; iconClass = 'fa-fire-alt'; }
else if (fighterBaseStats.resourceName === 'Темная Энергия') { resourceClass = 'dark-energy'; iconClass = 'fa-skull'; }
resourceBarContainer.classList.add(resourceClass);
resourceIconElement.className = `fas ${iconClass}`;
}
const statusText = fighterState.isBlocking ? (config.STATUS_BLOCKING || 'Защищается') : (config.STATUS_READY || 'Готов(а)');
elements.status.textContent = statusText;
elements.status.classList.toggle(config.CSS_CLASS_BLOCKING || 'blocking', fighterState.isBlocking);
if (elements.panel) {
let glowColorVar = '--panel-glow-opponent'; let borderColorVar = '--accent-opponent';
if (fighterBaseStats.characterKey === 'elena') { glowColorVar = '--panel-glow-player'; borderColorVar = '--accent-player'; }
else if (fighterBaseStats.characterKey === 'almagest') { glowColorVar = '--panel-glow-opponent'; borderColorVar = 'var(--accent-almagest)'; } // Цвет рамки Альмагест
elements.panel.style.borderColor = borderColorVar; // Прямое присвоение, т.к. var() не сработает для accent-almagest если он не в :root
elements.panel.style.boxShadow = `0 0 15px var(${glowColorVar}), inset 0 0 10px rgba(0, 0, 0, 0.3)`;
}
}
function generateEffectsHTML(effectsArray) {
const config = window.GAME_CONFIG || {};
if (!effectsArray || effectsArray.length === 0) return 'Нет';
return effectsArray.map(eff => {
let effectClasses = config.CSS_CLASS_EFFECT || 'effect';
const title = `${eff.name}${eff.description ? ` - ${eff.description}` : ''} (Осталось: ${eff.turnsLeft} х.)`;
const displayText = `${eff.name} (${eff.turnsLeft} х.)`;
if (eff.type === config.ACTION_TYPE_DISABLE || eff.isFullSilence || eff.id.startsWith('playerSilencedOn_')) effectClasses += ' effect-stun';
else if (eff.type === config.ACTION_TYPE_DEBUFF || (eff.power && eff.power < 0) || eff.id.startsWith('effect_')) effectClasses += ' effect-debuff';
else if (eff.grantsBlock) effectClasses += ' effect-block';
else effectClasses += ' effect-buff';
return `<span class="${effectClasses}" title="${title}">${displayText}</span>`;
}).join(' ');
}
function updateEffectsUI(currentGameState) {
if (!currentGameState || !uiElements.player.buffsList || !uiElements.opponent.buffsList) return;
const mySlotId = window.myPlayerId; // Наш слот ('player' или 'opponent')
const opponentSlotId = mySlotId === window.GAME_CONFIG.PLAYER_ID ? window.GAME_CONFIG.OPPONENT_ID : window.GAME_CONFIG.PLAYER_ID;
const myState = currentGameState[mySlotId];
if (uiElements.player && myState && myState.activeEffects) {
uiElements.player.buffsList.innerHTML = generateEffectsHTML(myState.activeEffects.filter(e => e.type === window.GAME_CONFIG.ACTION_TYPE_BUFF || e.grantsBlock));
uiElements.player.debuffsList.innerHTML = generateEffectsHTML(myState.activeEffects.filter(e => e.type !== window.GAME_CONFIG.ACTION_TYPE_BUFF && !e.grantsBlock));
}
const opponentState = currentGameState[opponentSlotId];
if (uiElements.opponent && opponentState && opponentState.activeEffects) {
uiElements.opponent.buffsList.innerHTML = generateEffectsHTML(opponentState.activeEffects.filter(e => e.type === window.GAME_CONFIG.ACTION_TYPE_BUFF || e.grantsBlock));
uiElements.opponent.debuffsList.innerHTML = generateEffectsHTML(opponentState.activeEffects.filter(e => e.type !== window.GAME_CONFIG.ACTION_TYPE_BUFF && !e.grantsBlock));
}
}
function updateUI() {
const currentGameState = window.gameState;
const gameDataGlobal = window.gameData;
const configGlobal = window.GAME_CONFIG;
const myActualPlayerId = window.myPlayerId; // Слот, который занимает ЭТОТ клиент ('player' или 'opponent')
if (!currentGameState || !gameDataGlobal || !configGlobal || !myActualPlayerId) {
console.warn("updateUI: Отсутствуют глобальные gameState, gameData, GAME_CONFIG или myActualPlayerId.");
return;
}
if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) {
console.warn("updateUI: Некоторые базовые uiElements не найдены.");
return;
}
// Определяем ID слота того, кто сейчас ходит
const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID;
// Обновление панелей бойцов
const opponentActualSlotId = myActualPlayerId === configGlobal.PLAYER_ID ? configGlobal.OPPONENT_ID : configGlobal.PLAYER_ID;
updateFighterPanelUI('player', currentGameState[myActualPlayerId], gameDataGlobal.playerBaseStats, true);
updateFighterPanelUI('opponent', currentGameState[opponentActualSlotId], gameDataGlobal.opponentBaseStats, false);
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';
if (myKey === 'elena') myClass = 'title-enchantress';
else if (myKey === 'almagest') myClass = 'title-sorceress';
let opponentClass = 'title-opponent';
if (opponentKey === 'elena') opponentClass = 'title-enchantress';
else if (opponentKey === 'almagest') opponentClass = 'title-sorceress';
else if (opponentKey === 'balard') opponentClass = 'title-knight';
uiElements.gameHeaderTitle.innerHTML = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="${opponentClass}">${opponentName}</span>`;
}
if (uiElements.controls.turnIndicator) {
const currentTurnActorState = currentGameState[actorSlotWhoseTurnItIs];
const currentTurnName = currentTurnActorState?.name || 'Неизвестно';
uiElements.controls.turnIndicator.textContent = `Ход: ${currentTurnName}`;
const currentTurnCharacterKey = currentTurnActorState?.characterKey;
let turnColor = 'var(--turn-color)';
if (currentTurnCharacterKey === 'elena') turnColor = 'var(--accent-player)';
else if (currentTurnCharacterKey === 'almagest') turnColor = 'var(--accent-almagest)';
else if (currentTurnCharacterKey === 'balard') turnColor = 'var(--accent-opponent)';
uiElements.controls.turnIndicator.style.color = turnColor;
}
const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId;
const isGameActive = !currentGameState.isGameOver;
if (uiElements.controls.buttonAttack) {
uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive);
const myCharKey = gameDataGlobal.playerBaseStats.characterKey;
const myState = currentGameState[myActualPlayerId];
let attackBuffId = null;
if (myCharKey === 'elena') attackBuffId = configGlobal.ABILITY_ID_NATURE_STRENGTH;
else if (myCharKey === 'almagest') attackBuffId = configGlobal.ABILITY_ID_ALMAGEST_BUFF_ATTACK;
if (attackBuffId && myState) {
const isAttackBuffReady = myState.activeEffects.some(eff => eff.id === attackBuffId && !eff.justCast);
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 = currentGameState[myActualPlayerId];
const actingPlayerAbilities = gameDataGlobal.playerAbilities;
const actingPlayerResourceName = gameDataGlobal.playerBaseStats.resourceName;
uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => {
if (!(button instanceof HTMLButtonElement) || !actingPlayerState || !actingPlayerAbilities) {
if(button instanceof HTMLButtonElement) button.disabled = true;
return;
}
const abilityId = button.dataset.abilityId;
const ability = actingPlayerAbilities.find(ab => ab.id === abilityId);
if (!ability) { button.disabled = true; return; }
const hasEnoughResource = actingPlayerState.currentResource >= ability.cost;
const isBuffAlreadyActive = ability.type === configGlobal.ACTION_TYPE_BUFF && actingPlayerState.activeEffects.some(eff => eff.id === ability.id);
const isOnCooldown = (actingPlayerState.abilityCooldowns?.[ability.id] || 0) > 0;
const isGenerallySilenced = actingPlayerState.activeEffects.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
const isSpecificallySilenced = actingPlayerState.disabledAbilities?.some(dis => dis.abilityId === abilityId && dis.turnsLeft > 0);
const isSilenced = isGenerallySilenced || isSpecificallySilenced;
const silenceTurnsLeft = isGenerallySilenced ? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0)
: (isSpecificallySilenced ? (actingPlayerState.disabledAbilities.find(dis => dis.abilityId === abilityId)?.turnsLeft || 0) : 0);
let isDisabledByDebuffOnTarget = false;
const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId];
if ((ability.id === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF) && opponentStateForDebuffCheck) {
const effectIdForDebuff = 'effect_' + ability.id;
isDisabledByDebuffOnTarget = opponentStateForDebuffCheck.activeEffects.some(e => e.id === effectIdForDebuff);
}
button.disabled = !(canThisClientAct && isGameActive) || !hasEnoughResource || isBuffAlreadyActive || isSilenced || isOnCooldown || isDisabledByDebuffOnTarget;
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[ability.id]}`; cooldownDisplay.style.display = 'block'; }
} else if (isSilenced) {
button.classList.add(configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced');
if (cooldownDisplay) { cooldownDisplay.textContent = `Безм: ${silenceTurnsLeft}`; cooldownDisplay.style.display = 'block'; }
} else {
if (cooldownDisplay) cooldownDisplay.style.display = 'none';
button.classList.toggle(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', !hasEnoughResource && !isBuffAlreadyActive && !isDisabledByDebuffOnTarget);
button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', isBuffAlreadyActive && !isDisabledByDebuffOnTarget);
}
let titleText = `${ability.name} (${ability.cost} ${actingPlayerResourceName})`;
let descriptionText = ability.description;
if (typeof ability.descriptionFunction === 'function') {
descriptionText = ability.descriptionFunction(configGlobal, gameDataGlobal.opponentBaseStats);
}
if (descriptionText) titleText += ` - ${descriptionText}`;
let abilityBaseCooldown = ability.cooldown;
if (ability.internalCooldownFromConfig && configGlobal[ability.internalCooldownFromConfig]) abilityBaseCooldown = configGlobal[ability.internalCooldownFromConfig];
else if (ability.internalCooldownValue) abilityBaseCooldown = ability.internalCooldownValue;
if (abilityBaseCooldown) titleText += ` (КД: ${abilityBaseCooldown} х.)`;
if (isOnCooldown) titleText = `${ability.name} - На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[ability.id]} х.`;
else if (isSilenced) titleText = `Безмолвие! Осталось: ${silenceTurnsLeft} х.`;
else if (isBuffAlreadyActive) {
const activeEffect = actingPlayerState.activeEffects.find(eff => eff.id === abilityId);
titleText = `Эффект "${ability.name}" уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}`;
} else if (isDisabledByDebuffOnTarget && opponentStateForDebuffCheck) {
const activeDebuff = opponentStateForDebuffCheck.activeEffects.find(e => e.id === 'effect_' + ability.id);
titleText = `Эффект "${ability.name}" уже наложен на ${opponentStateForDebuffCheck.name}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}`;
}
button.setAttribute('title', titleText);
});
}
function showGameOver(playerWon, reason = "") {
const config = window.GAME_CONFIG || {};
const gameDataGlobal = window.gameData || {};
const currentGameState = window.gameState;
const gameOverScreenElement = uiElements.gameOver.screen;
if (!gameOverScreenElement || !currentGameState) return;
const resultMsgElement = uiElements.gameOver.message;
const opponentPanelElement = uiElements.opponent.panel;
const myName = gameDataGlobal.playerBaseStats?.name || "Игрок";
const opponentName = gameDataGlobal.opponentBaseStats?.name || "Противник";
const opponentCharacterKey = gameDataGlobal.opponentBaseStats?.characterKey;
if (resultMsgElement) {
let winText = `Победа! ${myName} празднует!`;
let loseText = `Поражение! ${opponentName} оказался(лась) сильнее!`;
if (reason === 'opponent_disconnected') winText = `${opponentName} покинул(а) игру. Победа присуждается вам!`;
resultMsgElement.textContent = playerWon ? winText : loseText;
resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)';
}
if (opponentPanelElement) {
opponentPanelElement.classList.remove('dissolving');
if (playerWon && reason !== 'opponent_disconnected' && (opponentCharacterKey === 'balard' || opponentCharacterKey === 'almagest')) {
opponentPanelElement.classList.add('dissolving');
}
}
setTimeout(() => {
gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden');
requestAnimationFrame(() => {
gameOverScreenElement.style.opacity = '0';
setTimeout(() => {
gameOverScreenElement.style.opacity = '1';
if (uiElements.gameOver.modalContent) {
uiElements.gameOver.modalContent.style.transform = 'scale(1) translateY(0)';
uiElements.gameOver.modalContent.style.opacity = '1';
}
}, config.MODAL_TRANSITION_DELAY || 10);
});
}, config.DELAY_BEFORE_VICTORY_MODAL || 1500);
}
window.gameUI = { uiElements, addToLog, updateUI, showGameOver };
})();