Аеро Хокей const canvas = document.getElementById("game"); const ctx = canvas.getContext("2d"); const paddleRadius = 30; const puckRadius = 12; const cornerRadius = 10; const goalWidth = 110; let player = { x: 180, y: 520, prevX: 180, prevY: 520 }; let ai = { x: 180, y: 85, prevX: 180, prevY: 85 }; let puck = { x: 180, y: 320, dx: 3, dy: 3, scale: 1 }; let score = [0, 0]; let catHeadLeanY = 0; // глобально let powerBallRotation = 0; let touchPos = null; // картинки const bgImage = document.getElementById("bg"); const playerImage = document.getElementById("playerPaddle"); const aiImage = document.getElementById("aiPaddle"); const puckImage = document.getElementById("puckImage"); const armSectionImage = document.getElementById("armSection"); const pawImage = document.getElementById("paw"); const catHeadImage = document.getElementById("catHead"); const powerBallImage = document.getElementById("powerBallImage"); const bounds = { left: 50, right: 345, top: 70, bottom: 603 }; let aiSkill = 5; // змінюй від 1 (слабкий AI) до 10 (сильний AI) function isGoalForPlayer() { return puck.y - puckRadius <= bounds.top && puck.x > (canvas.width - goalWidth) / 2 && puck.x < (canvas.width + goalWidth) / 2; } function isGoalForAI() { return puck.y + puckRadius >= bounds.bottom && puck.x > (canvas.width - goalWidth) / 2 && puck.x < (canvas.width + goalWidth) / 2; } function keepWithinBounds(obj, radius, isPlayer = false) { obj.x = Math.max(bounds.left + radius, Math.min(bounds.right - radius, obj.x)); if (isPlayer) { obj.y = Math.max((bounds.top + bounds.bottom) / 2 + radius, Math.min(bounds.bottom - radius, obj.y)); } else { obj.y = Math.max(bounds.top + radius, Math.min((bounds.top + bounds.bottom) / 2 - radius, obj.y)); } } let pulseTime = 0; function drawPowerBall() { if (powerBall) { pulseTime += 0.05; const scale = 1 + 0.1 * Math.sin(pulseTime); ctx.save(); ctx.translate(powerBall.x, powerBall.y); ctx.scale(scale, scale); ctx.drawImage(powerBallImage, -20, -20, 40, 40); ctx.restore(); } } function keepOutsideCorners(obj, radius) { const corners = [ { x: bounds.left + cornerRadius, y: bounds.top + cornerRadius }, { x: bounds.right - cornerRadius, y: bounds.top + cornerRadius }, { x: bounds.left + cornerRadius, y: bounds.bottom - cornerRadius }, { x: bounds.right - cornerRadius, y: bounds.bottom - cornerRadius }, ]; for (const c of corners) { const dx = obj.x - c.x; const dy = obj.y - c.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < radius + puckRadius) { const angle = Math.atan2(dy, dx); obj.x = c.x + Math.cos(angle) * (radius + puckRadius); obj.y = c.y + Math.sin(angle) * (radius + puckRadius); } } } function drawCircleImage(img, x, y, radius, scale = 1) { const r = radius * scale; ctx.drawImage(img, x - r, y - r, r * 2, r * 2); } function lerp(start, end, t) { return start + (end - start) * t; } function drawCatHead() { const centerX = 205; const headY = -50; const headWidth = 150; const headHeight = 132; // Рух голови за шайбою const dx = puck.x - centerX; const maxRotation = 15; const sensitivity = 5; const rotationDeg = -Math.max(-maxRotation, Math.min(maxRotation, dx / sensitivity)); const angleRad = rotationDeg * Math.PI / 180; // Якщо шайба на стороні AI і достатньо далеко вниз const isPuckFarOnAISide = puck.y < canvas.height / 2 && puck.y > bounds.top + 150; const targetLeanY = isPuckFarOnAISide ? 20 : 0; // Плавне зміщення вниз catHeadLeanY = lerp(catHeadLeanY, targetLeanY, 0.1); ctx.save(); ctx.translate(centerX, headY + headHeight / 2 + catHeadLeanY); ctx.rotate(angleRad); ctx.drawImage(catHeadImage, -headWidth / 2, -headHeight / 2, headWidth, headHeight); ctx.restore(); } function drawText(text, x, y) { ctx.fillStyle = "#fff"; ctx.font = "24px Arial"; ctx.fillText(text, x, y); } let aiMood = "defensive"; // defensive | aggressive | erratic let moodTimer = 0; let aiState = 'idle'; let aiTarget = { x: ai.x, y: ai.y }; let aiCooldown = 0; function moveAI() { ai.prevX = ai.x; ai.prevY = ai.y; const puckOnAISide = puck.y < canvas.height / 2; const puckMovingToAI = puck.dy < 0; // Обрахунок параметрів на основі aiSkill const speedMultiplier = 0.1 + aiSkill * 0.05; // швидкість const aimAccuracy = 0.1 + aiSkill * 0.08; // точність удару const retreatSpeed = 0.04 + aiSkill * 0.04; // швидкість повернення в захист const hesitation = 11 - aiSkill; // чим нижчий рівень — більше "тупить" if (aiCooldown > 0) aiCooldown--; if (puckOnAISide) { // Якщо AI має бути обережним const puckNearGoal = puck.y < bounds.top + 50; if (puckNearGoal && puckMovingToAI) { // Захищає ворота const goalX = canvas.width / 2; const goalY = bounds.top + 40; ai.x += (goalX - ai.x) * retreatSpeed; ai.y += (goalY - ai.y) * retreatSpeed; aiState = 'idle'; return; } if (aiState === 'idle') { const dx = puck.x - 200; const dy = puck.y - 20; const dist = Math.sqrt(dx * dx + dy * dy); aiTarget = { x: puck.x - (dx / dist) * 30, y: puck.y - (dy / dist) * 30 }; aiState = 'pullBack'; } if (aiState === 'pullBack') { ai.x += (aiTarget.x - ai.x) * (speedMultiplier * 0.5); ai.y += (aiTarget.y - ai.y) * (speedMultiplier * 0.5); if (Math.abs(ai.x - aiTarget.x) < 2 && Math.abs(ai.y - aiTarget.y) < 2) { aiState = 'strike'; aiCooldown = hesitation; } } else if (aiState === 'strike') { const angle = Math.atan2(puck.y - ai.y, puck.x - ai.x); const offsetX = Math.cos(angle) * (20 + (10 - aiSkill) * 3); const offsetY = Math.sin(angle) * (10 + (10 - aiSkill) * 2); ai.x += (puck.x + offsetX - ai.x) * (speedMultiplier); ai.y += (puck.y + offsetY - ai.y) * (speedMultiplier); if (aiCooldown === 0) { aiState = 'recover'; aiCooldown = hesitation; } } else if (aiState === 'recover') { const centerX = canvas.width / 2; const centerY = bounds.top + (bounds.bottom - bounds.top) / 4; ai.x += (centerX - ai.x) * 0.1; ai.y += (centerY - ai.y) * 0.1; if (aiCooldown === 0) aiState = 'idle'; } } else { const centerX = canvas.width / 2; const centerY = bounds.top + (bounds.bottom - bounds.top) / 4; ai.x += (centerX - ai.x) * 0.08; ai.y += (centerY - ai.y) * 0.08; aiState = 'idle'; } keepWithinBounds(ai, paddleRadius, false); keepOutsideCorners(ai, cornerRadius); } function keepPuckWithinBounds(puck, radius) { const minX = bounds.left + radius; const maxX = bounds.right - radius; const minY = bounds.top + radius; const maxY = bounds.bottom - radius; if (puck.x < minX) { puck.x = minX; puck.dx *= -1; } if (puck.x > maxX) { puck.x = maxX; puck.dx *= -1; } if (puck.y < minY) { puck.y = minY; puck.dy *= -1; } if (puck.y > maxY) { puck.y = maxY; puck.dy *= -1; } } function movePuck() { puck.x += puck.dx; puck.y += puck.dy; // нова перевірка меж keepPuckWithinBounds(puck, puckRadius); // 🟡 Додано логіку перевірки, хто забив let goalScoredBy = null; if (isGoalForPlayer()) { score[0]++; goalScoredBy = 'player'; } else if (isGoalForAI()) { score[1]++; goalScoredBy = 'ai'; } if (goalScoredBy) { if (powerBallOwner === goalScoredBy) { if (goalScoredBy === 'player') powerBallActivePlayer = false; else if (goalScoredBy === 'ai') powerBallActiveAI = false; powerBallOwner = null; } resetPuck(); return; } keepOutsideCorners(puck, cornerRadius); checkCollision(player); checkCollision(ai); if (puck.scale < 1) puck.scale += 0.05; } function checkCollision(paddle) { const dx = puck.x - paddle.x; const dy = puck.y - paddle.y; const dist = Math.sqrt(dx * dx + dy * dy); const minDist = puckRadius + paddleRadius; if (dist < minDist - 1) { const angle = Math.atan2(dy, dx); const overlap = minDist - dist; puck.x += Math.cos(angle) * overlap; puck.y += Math.sin(angle) * overlap; } if (dist < minDist) { if (paddle === player) lastPuckOwner = 'player'; else if (paddle === ai) lastPuckOwner = 'ai'; puck.lastHitter = (paddle === player) ? 'player' : 'ai'; const speedX = paddle.x - paddle.prevX; const speedY = paddle.y - paddle.prevY; const angle = Math.atan2(dy, dx); puck.dx = Math.cos(angle) * 3 + speedX * 0.5; puck.dy = Math.sin(angle) * 3 + speedY * 0.5; puck.scale = 0.8; } } function resetPuck() { if (powerBallOwner === 'player' && isGoalForPlayer()) { powerBallActivePlayer = false; powerBallOwner = null; } if (powerBallOwner === 'ai' && isGoalForAI()) { powerBallActiveAI = false; powerBallOwner = null; } puck = { x: 180, y: 320, dx: 3 * (Math.random() > 0.5 ? 1 : -1), dy: 3 * (Math.random() > 0.5 ? 1 : -1), scale: 1 }; } // 🐾 Малювання руки з секцій function drawCatArmToAI() { const baseX = 170; const baseY = -50; const pawWidth = 165 * 0.3; const pawHeight = 210 * 0.3; const sectionWidth = 122 * 0.3; const sectionHeight = 76 * 0.3; // Вектор від лапки до плеча const dx = baseX - ai.x; const dy = baseY - ai.y; const distance = Math.sqrt(dx * dx + dy * dy); const angle = Math.atan2(dy, dx); // Малюємо лапку (обріз зверху) ctx.save(); ctx.translate(ai.x, ai.y); ctx.rotate(angle + Math.PI / 2); ctx.drawImage(pawImage, -pawWidth / 2, -pawHeight + 20, pawWidth, pawHeight); ctx.restore(); // Довжина руки без лапки const availableLength = distance - pawHeight * 0.8; const numSections = Math.max(0, Math.ceil(availableLength / (sectionHeight * 0.9))); // перекриття 10% // Малюємо секції — з невеликим перекриттям ctx.save(); ctx.translate(ai.x, ai.y); ctx.rotate(angle + Math.PI / 2); for (let i = 0; i < numSections; i++) { const y = -(pawHeight * 0.8) - i * (sectionHeight * 0.9); ctx.drawImage(armSectionImage, -sectionWidth / 2, y, sectionWidth, sectionHeight); } ctx.restore(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(bgImage, 0, 0, canvas.width, canvas.height); // Малюємо Power Ball до руки, але після фону if (powerBall && !powerBall.collected) { // приклад із анімацією обертання: pulseTime += 0.05; powerBallRotation += 0.05; const scale = 1 + 0.1 * Math.sin(pulseTime); ctx.save(); ctx.translate(powerBall.x, powerBall.y); ctx.rotate(powerBallRotation); ctx.scale(scale, scale); ctx.shadowColor = 'gold'; ctx.shadowBlur = 15; ctx.drawImage(powerBallImage, -20, -20, 40, 40); ctx.restore(); } drawCircleImage(playerImage, player.x, player.y, paddleRadius); drawCircleImage(aiImage, ai.x, ai.y, paddleRadius); drawCircleImage(puckImage, puck.x, puck.y, puckRadius, puck.scale); drawCatArmToAI(); drawCatHead(); // ✅ Тепер точно викликається drawPowerBallIconOverPaddle(); drawText(score[0], 20, 320); drawText(score[1], canvas.width - 40, 320); } function gameLoop() { if (touchPos) { player.prevX = player.x; player.prevY = player.y; player.x = touchPos.x; player.y = touchPos.y; keepWithinBounds(player, paddleRadius, true); keepOutsideCorners(player, cornerRadius); } moveAI(); // ⬅️ переміщуємо AI movePuck(); // ⬅️ переміщуємо шайбу draw(); // ⬅️ лише після оновлення всіх координат requestAnimationFrame(gameLoop); } canvas.addEventListener("touchstart", e => { const rect = canvas.getBoundingClientRect(); touchPos = { x: e.touches[0].clientX - rect.left, y: e.touches[0].clientY - rect.top }; }); canvas.addEventListener("touchmove", e => { const rect = canvas.getBoundingClientRect(); touchPos = { x: e.touches[0].clientX - rect.left, y: e.touches[0].clientY - rect.top }; }); canvas.addEventListener("touchend", () => { touchPos = null; }); bgImage.onload = () => { gameLoop(); };