Аеро Хокей
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();
};