diff --git a/public/game.js b/public/game.js index 5241497..4b939d1 100644 --- a/public/game.js +++ b/public/game.js @@ -411,32 +411,139 @@ const attackerWrap = document.querySelector(`[data-minion-id="${lastLog.attackerMinionId}"]`); const targetWrap = document.querySelector(`[data-minion-id="${lastLog.targetMinionId}"]`); if (!attackerWrap || !targetWrap) return false; - attackerWrap.classList.add('combat-lunge'); + + // Вычисляем позиции для точного перемещения карты + const attackerRect = attackerWrap.getBoundingClientRect(); + const targetRect = targetWrap.getBoundingClientRect(); + + // Вычисляем смещение + const dx = targetRect.left + targetRect.width / 2 - (attackerRect.left + attackerRect.width / 2); + const dy = targetRect.top + targetRect.height / 2 - (attackerRect.top + attackerRect.height / 2); + + // Сохраняем начальную позицию + const startX = attackerRect.left; + const startY = attackerRect.top; + const startWidth = attackerRect.width; + const startHeight = attackerRect.height; + + // Устанавливаем fixed positioning для наложения + attackerWrap.style.position = 'fixed'; + attackerWrap.style.left = startX + 'px'; + attackerWrap.style.top = startY + 'px'; + attackerWrap.style.width = startWidth + 'px'; + attackerWrap.style.height = startHeight + 'px'; + attackerWrap.style.zIndex = '1000'; + attackerWrap.style.transition = 'none'; + + // Добавляем класс для анимации + attackerWrap.classList.add('combat-lunge-overlay'); targetWrap.classList.add('combat-hit'); + + // Устанавливаем CSS переменные для перемещения + attackerWrap.style.setProperty('--attack-dx', dx + 'px'); + attackerWrap.style.setProperty('--attack-dy', dy + 'px'); + if (lastLog.damage) showDamageEffect(targetWrap, lastLog.damage, false); if (typeof window.Sounds !== 'undefined') window.Sounds.attack(); - if (lastLog.attackerDied) { - attackerWrap.classList.add('combat-death'); - showDamageEffect(attackerWrap, '💀', false); - } - if (lastLog.targetDied) { - targetWrap.classList.add('combat-death'); - showDamageEffect(targetWrap, '💀', false); - } + + // Анимация удара + setTimeout(() => { + targetWrap.classList.add('combat-hit-impact'); + }, 200); + + // Возврат карты и очистка + setTimeout(() => { + // Если атакующий не умер, возвращаем карту на место + if (!lastLog.attackerDied) { + attackerWrap.classList.remove('combat-lunge-overlay'); + attackerWrap.style.position = ''; + attackerWrap.style.left = ''; + attackerWrap.style.top = ''; + attackerWrap.style.width = ''; + attackerWrap.style.height = ''; + attackerWrap.style.zIndex = ''; + attackerWrap.style.setProperty('--attack-dx', ''); + attackerWrap.style.setProperty('--attack-dy', ''); + attackerWrap.style.transition = ''; + } else { + // Если атакующий умер, сразу применяем анимацию смерти + attackerWrap.classList.remove('combat-lunge-overlay'); + attackerWrap.style.position = ''; + attackerWrap.style.left = ''; + attackerWrap.style.top = ''; + attackerWrap.style.width = ''; + attackerWrap.style.height = ''; + attackerWrap.style.zIndex = ''; + attackerWrap.style.setProperty('--attack-dx', ''); + attackerWrap.style.setProperty('--attack-dy', ''); + attackerWrap.style.transition = ''; + attackerWrap.classList.add('combat-death'); + showDamageEffect(attackerWrap, '💀', false); + } + + if (lastLog.targetDied) { + targetWrap.classList.add('combat-death'); + showDamageEffect(targetWrap, '💀', false); + } + + targetWrap.classList.remove('combat-hit-impact'); + }, 500); + combatTimeout = setTimeout(function () { combatTimeout = null; renderBoards(state); bindGameEvents(state); }, 600); return true; - } else if (lastLog.type === 'attackHero' && lastLog.toPlayer !== undefined) { + } else if (lastLog.type === 'attackHero' && lastLog.toPlayer !== undefined && lastLog.attackerMinionId) { + const attackerWrap = document.querySelector(`[data-minion-id="${lastLog.attackerMinionId}"]`); const targetPlayer = state.players[lastLog.toPlayer]; - if (targetPlayer) { + if (attackerWrap && targetPlayer) { const heroEl = document.querySelector(`[data-player-index="${lastLog.toPlayer}"].hero-target, .opponent-block[data-opponent-index="${lastLog.toPlayer}"] .drop-target-hero`); if (heroEl) { - heroEl.classList.add('hero-damage-flash'); + // Вычисляем позиции для атаки по герою + const attackerRect = attackerWrap.getBoundingClientRect(); + const heroRect = heroEl.getBoundingClientRect(); + + const dx = heroRect.left + heroRect.width / 2 - (attackerRect.left + attackerRect.width / 2); + const dy = heroRect.top + heroRect.height / 2 - (attackerRect.top + attackerRect.height / 2); + + const startX = attackerRect.left; + const startY = attackerRect.top; + const startWidth = attackerRect.width; + const startHeight = attackerRect.height; + + // Устанавливаем fixed positioning + attackerWrap.style.position = 'fixed'; + attackerWrap.style.left = startX + 'px'; + attackerWrap.style.top = startY + 'px'; + attackerWrap.style.width = startWidth + 'px'; + attackerWrap.style.height = startHeight + 'px'; + attackerWrap.style.zIndex = '1000'; + attackerWrap.style.transition = 'none'; + + attackerWrap.classList.add('combat-lunge-overlay'); + attackerWrap.style.setProperty('--attack-dx', dx + 'px'); + attackerWrap.style.setProperty('--attack-dy', dy + 'px'); + + heroEl.classList.add('hero-damage-flash', 'combat-hit-impact'); if (lastLog.damage) showDamageEffect(heroEl, lastLog.damage, true); - setTimeout(() => heroEl.classList.remove('hero-damage-flash'), 500); + if (typeof window.Sounds !== 'undefined') window.Sounds.attack(); + + setTimeout(() => { + attackerWrap.classList.remove('combat-lunge-overlay'); + attackerWrap.style.position = ''; + attackerWrap.style.left = ''; + attackerWrap.style.top = ''; + attackerWrap.style.width = ''; + attackerWrap.style.height = ''; + attackerWrap.style.zIndex = ''; + attackerWrap.style.setProperty('--attack-dx', ''); + attackerWrap.style.setProperty('--attack-dy', ''); + attackerWrap.style.transition = ''; + + heroEl.classList.remove('hero-damage-flash', 'combat-hit-impact'); + }, 500); } } return true; diff --git a/public/styles.css b/public/styles.css index 6771271..bf2749b 100644 --- a/public/styles.css +++ b/public/styles.css @@ -842,6 +842,48 @@ html, body { 35% { transform: translate(var(--lunge-dx, 30px), var(--lunge-dy, -20px)) scale(1.15); filter: brightness(1.4); } 100% { transform: translate(0, 0) scale(1); filter: brightness(1); } } + +/* Новая анимация: карта накладывается на цель */ +.card-wrap.combat-lunge-overlay { + animation: combatLungeOverlay 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards; + pointer-events: none; +} + +.card-wrap.combat-lunge-overlay .card { + box-shadow: 0 0 40px rgba(255, 200, 0, 0.6), 0 0 80px rgba(255, 100, 0, 0.4); +} + +@keyframes combatLungeOverlay { + 0% { + transform: translate(0, 0) scale(1) rotate(0deg); + filter: brightness(1); + opacity: 1; + } + 25% { + transform: translate(calc(var(--attack-dx, 0) * 0.6), calc(var(--attack-dy, 0) * 0.6)) scale(1.15) rotate(-3deg); + filter: brightness(1.4) drop-shadow(0 0 25px rgba(255, 200, 0, 0.7)); + } + 45% { + transform: translate(var(--attack-dx, 0), var(--attack-dy, 0)) scale(1.3) rotate(0deg); + filter: brightness(2) drop-shadow(0 0 40px rgba(255, 100, 0, 1)); + z-index: 1001; + } + 50% { + transform: translate(var(--attack-dx, 0), var(--attack-dy, 0)) scale(1.28) rotate(1deg); + filter: brightness(1.9) drop-shadow(0 0 35px rgba(255, 150, 0, 0.95)); + } + 55% { + transform: translate(var(--attack-dx, 0), var(--attack-dy, 0)) scale(1.25) rotate(-1deg); + filter: brightness(1.7) drop-shadow(0 0 30px rgba(255, 120, 0, 0.9)); + } + 100% { + transform: translate(0, 0) scale(1) rotate(0deg); + filter: brightness(1); + z-index: auto; + opacity: 1; + } +} + .card-wrap.combat-hit .card { animation: combatHit 0.45s ease-out forwards; } @@ -851,6 +893,66 @@ html, body { 40% { transform: scale(0.95); filter: brightness(1.2); } 100% { transform: scale(1); filter: brightness(1); } } + +/* Эффект удара - карта трясётся и подсвечивается */ +.card-wrap.combat-hit-impact { + animation: combatHitImpact 0.35s ease-out forwards; +} + +.card-wrap.combat-hit-impact .card { + box-shadow: 0 0 30px rgba(255, 50, 50, 0.8), 0 0 60px rgba(255, 100, 100, 0.5); +} + +@keyframes combatHitImpact { + 0% { + transform: translate(0, 0) scale(1) rotate(0deg); + filter: brightness(1); + } + 8% { + transform: translate(-4px, -3px) scale(1.08) rotate(-3deg); + filter: brightness(2.8) saturate(1.3); + } + 16% { + transform: translate(4px, 3px) scale(1.08) rotate(3deg); + filter: brightness(3) saturate(1.4); + } + 24% { + transform: translate(-3px, 2px) scale(1.05) rotate(-2deg); + filter: brightness(2.5) saturate(1.2); + } + 32% { + transform: translate(3px, -2px) scale(1.05) rotate(2deg); + filter: brightness(2.7) saturate(1.3); + } + 40% { + transform: translate(-2px, 1px) scale(1.03) rotate(-1deg); + filter: brightness(2.2) saturate(1.1); + } + 48% { + transform: translate(2px, -1px) scale(1.03) rotate(1deg); + filter: brightness(2.4) saturate(1.2); + } + 56% { + transform: translate(-1px, 0) scale(1.02) rotate(-0.5deg); + filter: brightness(2) saturate(1.05); + } + 64% { + transform: translate(1px, 0) scale(1.02) rotate(0.5deg); + filter: brightness(1.8) saturate(1.1); + } + 72% { + transform: translate(0, 0) scale(1.01) rotate(0deg); + filter: brightness(1.5) saturate(1.05); + } + 80% { + transform: translate(0, 0) scale(1.005) rotate(0deg); + filter: brightness(1.2) saturate(1.02); + } + 100% { + transform: translate(0, 0) scale(1) rotate(0deg); + filter: brightness(1) saturate(1); + } +} .card-wrap.combat-death .card { animation: combatDeath 0.4s ease-in forwards; }