Játék

import React, { useState, useEffect, useRef } from ‘react’;

// — JÁTÉK BEÁLLÍTÁSOK —
const GAME_TIME = 180; // 3 perc másodpercben
const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 500;

// Pálya határok
const PITCH = {
top: 20,
bottom: 480,
left: 40,
right: 760
};

// Kapu határok
const GOAL = {
top: 190,
bottom: 310,
left_back: 10,
right_back: 790
};

export default function App() {
const canvasRef = useRef(null);

// React állapotok a UI-hoz
const [score, setScore] = useState({ red: 0, white: 0 });
const [timeLeft, setTimeLeft] = useState(GAME_TIME);
const [gameState, setGameState] = useState(‘MENU’); // MENU, PLAYING, EVENT, GAMEOVER
const [message, setMessage] = useState(null);

// Játék motor belső állapota
const engineRef = useRef({
ball: { x: 400, y: 250, vx: 0, vy: 0, r: 12 },
players: [],
keys: { up: false, down: false, left: false, right: false, kick: false },
pointer: { x: 400, y: 250, active: false, isUsing: false },
lastTouch: null,
status: ‘STOPPED’
});

const requestRef = useRef();
const lastTimeRef = useRef();

// — BILLENTYŰZET KEZELŐ —
useEffect(() => {
const handleKeyDown = (e) => {
const keys = engineRef.current.keys;
if ([‘ArrowUp’, ‘w’, ‘W’].includes(e.key)) keys.up = true;
if ([‘ArrowDown’, ‘s’, ‘S’].includes(e.key)) keys.down = true;
if ([‘ArrowLeft’, ‘a’, ‘A’].includes(e.key)) keys.left = true;
if ([‘ArrowRight’, ‘d’, ‘D’].includes(e.key)) keys.right = true;
if (e.key === ‘ ‘) {
keys.kick = true;
e.preventDefault();
}
engineRef.current.pointer.isUsing = false;
};

const handleKeyUp = (e) => {
const keys = engineRef.current.keys;
if ([‘ArrowUp’, ‘w’, ‘W’].includes(e.key)) keys.up = false;
if ([‘ArrowDown’, ‘s’, ‘S’].includes(e.key)) keys.down = false;
if ([‘ArrowLeft’, ‘a’, ‘A’].includes(e.key)) keys.left = false;
if ([‘ArrowRight’, ‘d’, ‘D’].includes(e.key)) keys.right = false;
if (e.key === ‘ ‘) keys.kick = false;
};

window.addEventListener(‘keydown’, handleKeyDown);
window.addEventListener(‘keyup’, handleKeyUp);
return () => {
window.removeEventListener(‘keydown’, handleKeyDown);
window.removeEventListener(‘keyup’, handleKeyUp);
};
}, []);

// — JÁTÉK INICIALIZÁLÁSA —
const initGame = () => {
engineRef.current.ball = { x: 400, y: 250, vx: 0, vy: 0, r: 12 };

const players = [];
// Piros csapat
for (let i = 0; i < 5; i++) {
players.push({
id: `red_${i}`,
team: 'red',
isUser: i === 0,
x: 150 + Math.random() * 50,
y: 100 + i * 70,
vx: 0, vy: 0, r: 15,
homeX: 200, homeY: 100 + i * 70
});
}
// Fehér csapat
for (let i = 0; i {
const engine = engineRef.current;
engine.ball = { x: 400, y: 250, vx: 0, vy: 0, r: 12 };
engine.players.forEach(p => {
p.vx = 0; p.vy = 0;
p.x = p.homeX + (Math.random() * 40 – 20);
p.y = p.homeY + (Math.random() * 40 – 20);
});
};

const setCornerPositions = (side, team) => {
const engine = engineRef.current;
engine.ball.vx = 0; engine.ball.vy = 0;

const cornerY = engine.ball.y {
p.vx = 0; p.vy = 0;
if (p.team === team && !throwerSelected) {
p.x = cornerX + (side === ‘left’ ? -10 : 10);
p.y = cornerY + (engine.ball.y {
const engine = engineRef.current;
engine.ball.vx = 0; engine.ball.vy = 0;

let bx = details.x;
let by = details.y;

if (bx = PITCH.right – 10) bx = PITCH.right – 15;
if (by = PITCH.bottom – 10) by = PITCH.bottom – 15;

engine.ball.x = bx;
engine.ball.y = by;

let throwerSelected = false;

engine.players.forEach(p => {
p.vx = 0; p.vy = 0;
if (p.team === details.team && !throwerSelected) {
p.x = bx;
p.y = by;
throwerSelected = true;
} else {
const dist = Math.hypot(p.x – bx, p.y – by);
if (dist {
engineRef.current.status = ‘EVENT’;
let text = ”; let subtext = ”; let color = ”;

switch (type) {
case ‘GOAL’:
text = ‘GÓÓÓL!’;
color = details.team === ‘red’ ? ‘text-red-500’ : ‘text-gray-100’;
subtext = details.team === ‘red’ ? ‘A Piros csapat betalált!’ : ‘A Fehér csapat betalált!’;
setScore(s => ({ …s, [details.team]: s[details.team] + 1 }));
setTimeout(() => { resetPositions(); resumeGame(); }, 3000);
break;
case ‘CORNER’:
text = ‘SZÖGLET!’;
color = details.team === ‘red’ ? ‘text-red-400’ : ‘text-gray-100’;
subtext = `${details.team === ‘red’ ? ‘Piros’ : ‘Fehér’} szöglet jön.`;
setTimeout(() => { setCornerPositions(details.side, details.team); resumeGame(); }, 2500);
break;
case ‘THROW_IN’:
text = ‘BEDOBÁS!’;
color = details.team === ‘red’ ? ‘text-red-400’ : ‘text-gray-100’;
subtext = `${details.team === ‘red’ ? ‘Piros’ : ‘Fehér’} csapat jön.`;
setTimeout(() => { setThrowInPositions(details); resumeGame(); }, 2000);
break;
case ‘RED_CARD’:
text = ‘PIROS LAP!’;
color = ‘text-red-600’;
subtext = `Egy ${details.team === ‘red’ ? ‘piros’ : ‘fehér’} játékos kiállítva!`;
const teamPlayers = engineRef.current.players.filter(p => p.team === details.team && !p.isUser);
if (teamPlayers.length > 0) {
const removedId = teamPlayers[0].id;
engineRef.current.players = engineRef.current.players.filter(p => p.id !== removedId);
}
setTimeout(() => { resumeGame(); }, 3000);
break;
default:
resumeGame();
}
setMessage({ text, subtext, color });
};

const resumeGame = () => {
setMessage(null);
engineRef.current.status = ‘PLAYING’;
};

// — JÁTÉK MOTOR (FIZIKA) —
const updatePhysics = () => {
const engine = engineRef.current;
if (engine.status !== ‘PLAYING’) return;

const { ball, players, keys, pointer } = engine;

// — 1. KIVÁLASZTJUK AZ IRÁNYÍTOTT JÁTÉKOST (Labdához legközelebbi Piros) —
let closestRed = null;
let minRedDist = Infinity;
players.forEach(p => {
if (p.team === ‘red’) {
const d = Math.hypot(ball.x – p.x, ball.y – p.y);
if (d {
if (p.team === ‘red’) p.isUser = (p === closestRed);
});

// — 2. LABDA SÚRLÓDÁS ÉS MOZGÁS —
ball.vx *= 0.96;
ball.vy *= 0.96;
ball.x += ball.vx;
ball.y += ball.vy;

// — 3. JÁTÉKOSOK MOZGÁSA —
let closestWhite = null;
let minWhiteDist = Infinity;
players.forEach(p => {
if (p.team === ‘white’) {
const d = Math.hypot(ball.x – p.x, ball.y – p.y);
if (d < minWhiteDist) { minWhiteDist = d; closestWhite = p; }
}
});

for (let i = 0; i 10) { ax = dx / dist; ay = dy / dist; }
}

if (ax !== 0 || ay !== 0) {
const len = Math.hypot(ax, ay) || 1;
const speedObj = isSprint ? 2.5 : 1.5;
p.vx += (ax / len) * speedObj;
p.vy += (ay / len) * speedObj;
}
} else {
if (p === closestWhite) {
let targetX = ball.x + 15;
let targetY = ball.y;

if (ball.y PITCH.bottom – 30) targetY = ball.y + 15;

let dx = targetX – p.x;
let dy = targetY – p.y;
let dist = Math.hypot(dx, dy) || 1;

// FEHÉREK LE LASSÍTVA: 0.75 helyett csak 0.3-mal gyorsulnak (kocogás)
p.vx += (dx / dist) * 0.3;
p.vy += (dy / dist) * 0.3;
} else {
let dx = p.homeX – p.x;
let dy = p.homeY – p.y;
let dist = Math.hypot(dx, dy) || 1;
if (dist > 50) {
// Visszatérés is lassabb
p.vx += (dx / dist) * 0.15;
p.vy += (dy / dist) * 0.15;
}
}
}

const speed = Math.hypot(p.vx, p.vy);
// FEHÉREK MAX SEBESSÉGE CSÖKKENTVE (1.8), PIROSAKÉ MARAD GYORS (3.5), USER A LEGGYORSABB (4-6)
const maxSpeed = p.isUser ? (keys.kick || pointer.active ? 6 : 4) : (p.team === ‘red’ ? 3.5 : 1.8);
if (speed > maxSpeed) {
p.vx = (p.vx / speed) * maxSpeed;
p.vy = (p.vy / speed) * maxSpeed;
}

p.x += p.vx;
p.y += p.vy;
}

// — 4. JÁTÉKOS-JÁTÉKOS ÜTKÖZÉS —
for (let i = 0; i < players.length; i++) {
for (let j = i + 1; j < players.length; j++) {
const p1 = players[i];
const p2 = players[j];
let dx = p2.x – p1.x;
let dy = p2.y – p1.y;
let dist = Math.hypot(dx, dy);

if (dist < p1.r + p2.r) {
if (dist === 0) { dx = Math.random() – 0.5; dy = Math.random() – 0.5; dist = Math.hypot(dx, dy); }

const overlap = (p1.r + p2.r) – dist;
const pushX = (dx / dist) * (overlap / 2);
const pushY = (dy / dist) * (overlap / 2);

p1.x -= pushX; p1.y -= pushY;
p2.x += pushX; p2.y += pushY;

p1.vx *= 0.8; p1.vy *= 0.8;
p2.vx *= 0.8; p2.vy *= 0.8;

if (p1.team !== p2.team && engine.status === 'PLAYING') {
if (Math.random() 0.5 ? p1.team : p2.team });
}
}
}
}
}

// — 5. LABDA-JÁTÉKOS ÜTKÖZÉS —
for (let i = 0; i < players.length; i++) {
const p = players[i];
let bdx = ball.x – p.x;
let bdy = ball.y – p.y;
let bDist = Math.hypot(bdx, bdy);

if (bDist < p.r + ball.r) {
if (bDist === 0) { bdx = 1; bdy = 0; bDist = 1; }
engine.lastTouch = p.team;

const overlap = (p.r + ball.r) – bDist;
const force = p.isUser && (keys.kick || pointer.active) ? 2.5 : 1.2;

const nx = bdx / bDist;
const ny = bdy / bDist;
ball.x += nx * overlap;
ball.y += ny * overlap;

if (!p.isUser) {
let targetX = p.team === 'white' ? PITCH.left : PITCH.right;
let targetY = 250;

if (ball.y PITCH.bottom – 60) targetY = 150;

let aimX = targetX – ball.x;
let aimY = targetY – ball.y;
let aimDist = Math.hypot(aimX, aimY) || 1;

let finalNx = (nx * 0.5) + ((aimX / aimDist) * 0.5);
let finalNy = (ny * 0.5) + ((aimY / aimDist) * 0.5);
let finalLen = Math.hypot(finalNx, finalNy) || 1;

// FEHÉREK LÖVŐEREJE GYENGÍTVE: 1.5 helyett csak 0.6 (épphogy elgurítják)
// Ha piros AI ér bele, ő rúghat nagyobbat (1.2)
const aiKickPower = p.team === ‘white’ ? 0.6 : 1.2;
ball.vx += (finalNx / finalLen) * force * aiKickPower;
ball.vy += (finalNy / finalLen) * force * aiKickPower;
} else {
ball.vx += nx * force;
ball.vy += ny * force;
}
}
}

// — 6. HATÁROK BIZTOSÍTÁSA —
players.forEach(p => {
p.x = Math.max(PITCH.left + p.r, Math.min(PITCH.right – p.r, p.x));
p.y = Math.max(PITCH.top + p.r, Math.min(PITCH.bottom – p.r, p.y));
});

// — 7. GÓL ÉS PÁLYA ELHAGYÁS (LABDA – JAVÍTVA!) —
let eventTriggered = false;

// Bal gólvonal / alapvonal
if (ball.x – ball.r GOAL.top && ball.y < GOAL.bottom) {
// GÓL! Amint áthalad a vonalon
if (ball.x PITCH.right) {
if (ball.y > GOAL.top && ball.y PITCH.right && !eventTriggered) {
triggerEvent(‘GOAL’, { team: ‘red’ });
eventTriggered = true;
}
} else {
// Mellé ment az alapvonalon
ball.x = PITCH.right – ball.r; ball.vx *= -0.8;
if (engine.lastTouch === ‘white’) {
triggerEvent(‘CORNER’, { side: ‘right’, team: ‘red’ });
} else {
triggerEvent(‘THROW_IN’, { x: PITCH.right, y: ball.y, team: ‘white’ });
}
eventTriggered = true;
}
}

// Oldalvonalak
if (!eventTriggered && ball.y – ball.r PITCH.bottom) {
ball.y = PITCH.bottom – ball.r; ball.vy *= -0.8;
const tTeam = engine.lastTouch === ‘red’ ? ‘white’ : ‘red’;
triggerEvent(‘THROW_IN’, { x: ball.x, y: PITCH.bottom, team: tTeam });
eventTriggered = true;
}
};

// — RAJZOLÁS —
const draw = (ctx) => {
ctx.fillStyle = ‘#22c55e’;
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

ctx.strokeStyle = ‘rgba(255, 255, 255, 0.8)’;
ctx.lineWidth = 4;

ctx.strokeRect(PITCH.left, PITCH.top, PITCH.right – PITCH.left, PITCH.bottom – PITCH.top);
ctx.beginPath(); ctx.moveTo(CANVAS_WIDTH / 2, PITCH.top); ctx.lineTo(CANVAS_WIDTH / 2, PITCH.bottom); ctx.stroke();
ctx.beginPath(); ctx.arc(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2, 60, 0, Math.PI * 2); ctx.stroke();
ctx.strokeRect(PITCH.left, 130, 120, 240);
ctx.strokeRect(PITCH.right – 120, 130, 120, 240);

ctx.fillStyle = ‘#e5e7eb’;
ctx.fillRect(GOAL.left_back, GOAL.top, PITCH.left – GOAL.left_back, GOAL.bottom – GOAL.top);
ctx.strokeRect(GOAL.left_back, GOAL.top, PITCH.left – GOAL.left_back, GOAL.bottom – GOAL.top);
ctx.fillRect(PITCH.right, GOAL.top, GOAL.right_back – PITCH.right, GOAL.bottom – GOAL.top);
ctx.strokeRect(PITCH.right, GOAL.top, GOAL.right_back – PITCH.right, GOAL.bottom – GOAL.top);

const engine = engineRef.current;

engine.players.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);

if (p.team === ‘red’) {
ctx.fillStyle = ‘#ef4444’; ctx.strokeStyle = ‘#7f1d1d’;
} else {
ctx.fillStyle = ‘#f8fafc’; ctx.strokeStyle = ‘#94a3b8’;
}

ctx.fill(); ctx.lineWidth = 2; ctx.stroke();

if (p.isUser) {
ctx.strokeStyle = ‘#fbbf24’;
ctx.lineWidth = 4;
ctx.beginPath(); ctx.arc(p.x, p.y, p.r + 4, 0, Math.PI * 2); ctx.stroke();
}
});

ctx.beginPath();
ctx.arc(engine.ball.x, engine.ball.y, engine.ball.r, 0, Math.PI * 2);
ctx.fillStyle = ‘white’; ctx.fill();
ctx.lineWidth = 2; ctx.strokeStyle = ‘black’; ctx.stroke();

ctx.beginPath();
ctx.arc(engine.ball.x, engine.ball.y, engine.ball.r / 2, 0, Math.PI * 2);
ctx.fillStyle = ‘black’; ctx.fill();
};

// — CIKLUS ÉS IDŐZÍTŐ —
useEffect(() => {
const loop = (time) => {
if (gameState === ‘PLAYING’) updatePhysics();
const canvas = canvasRef.current;
if (canvas) draw(canvas.getContext(‘2d’));
requestRef.current = requestAnimationFrame(loop);
};
requestRef.current = requestAnimationFrame(loop);
return () => cancelAnimationFrame(requestRef.current);
}, [gameState]);

useEffect(() => {
let timer;
if (gameState === ‘PLAYING’) {
timer = setInterval(() => {
setTimeLeft(prev => {
if (prev clearInterval(timer);
}, [gameState]);

// — EGÉR/UJJ KEZELÉS —
const handlePointerMove = (e) => {
if (gameState !== ‘PLAYING’) return;
const canvas = canvasRef.current;
if (!canvas) return;
engineRef.current.pointer.isUsing = true;

const rect = canvas.getBoundingClientRect();
let clientX = e.clientX || (e.touches && e.touches[0].clientX);
let clientY = e.clientY || (e.touches && e.touches[0].clientY);

engineRef.current.pointer.x = (clientX – rect.left) * (canvas.width / rect.width);
engineRef.current.pointer.y = (clientY – rect.top) * (canvas.height / rect.height);
};
const handlePointerDown = (e) => { engineRef.current.pointer.active = true; handlePointerMove(e); };
const handlePointerUp = () => { engineRef.current.pointer.active = false; };

const formatTime = (seconds) => `${Math.floor(seconds / 60)}:${(seconds % 60).toString().padStart(2, ‘0’)}`;

return (

Piros
{score.red}
Idő
<span className={`text-4xl font-mono font-bold ${timeLeft
{formatTime(timeLeft)}
Fehér
{score.white}

{gameState === ‘MENU’ && (

GYEREKFOCI

Hogyan játssz?

🎮 Irányítás: Nyilak vagy W, A, S, D

Szuper rúgás / Sprint: Szóköz (Space)

(Telefonon és tableten továbbra is húzhatod az ujjad a pályán!)

)}

{message && gameState === ‘PLAYING’ && engineRef.current.status === ‘EVENT’ && (

{message.text}

{message.subtext}

)}

{gameState === ‘GAMEOVER’ && (

VÉGE A MECCSNEK!

Végeredmény: {score.red} – {score.white}

{score.red > score.white &&

A PIROS CSAPAT GYŐZÖTT! 🎉

}
{score.white > score.red &&

A FEHÉREK NYERTEK!

}
{score.red === score.white &&

DÖNTETLEN!

}

)}

);
}