/* global React */
const { useEffect, useRef, useState, useCallback } = React;
// ─────────── INTERACTIVE GOAL: kickable ball with physics ───────────
function GoalPlay() {
const sectionRef = useRef(null);
const stageRef = useRef(null);
const ballRef = useRef(null);
const aimSvgRef = useRef(null);
const cleatRef = useRef(null);
const stateRef = useRef({
// ball physics (in stage-relative px)
x: 0, y: 0, vx: 0, vy: 0, r: 28,
// drag state
dragging: false,
dragStartX: 0, dragStartY: 0,
dragX: 0, dragY: 0,
// mouse (for cleat rotation + click-kick)
mouseStageX: 0, mouseStageY: 0, mouseInStage: false,
lastMouseX: 0, lastMouseY: 0, mouseAngle: 0,
// resize-tracked
stageW: 900, stageH: 460,
goalRect: { x: 0, y: 0, w: 0, h: 0 },
kickoff: { x: 0, y: 0 },
scoring: false,
// touch device
isTouch: false,
});
const [showGoal, setShowGoal] = useState(false);
const [confetti, setConfetti] = useState([]);
const [shake, setShake] = useState(false);
const isTouch = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);
const layout = useCallback(() => {
const stage = stageRef.current;
if (!stage) return;
const { width, height } = stage.getBoundingClientRect();
stateRef.current.stageW = width;
stateRef.current.stageH = height;
const mobile = width < 600;
const goalW = Math.min(width * (mobile ? 0.275 : 0.55), mobile ? 240 : 480);
const goalH = Math.min(height * (mobile ? 0.275 : 0.55), mobile ? 120 : 240);
const goalX = (width - goalW) / 2;
const goalY = height * 0.08;
stateRef.current.goalRect = { x: goalX, y: goalY, w: goalW, h: goalH };
const spawns = [
{ x: width / 2, y: height - 70 }, // centre
{ x: width * 0.12, y: height - 70 }, // left corner kick
{ x: width * 0.88, y: height - 70 }, // right corner kick
];
stateRef.current.spawnPositions = spawns;
stateRef.current.kickoff = spawns[0];
if (stateRef.current.x === 0 && stateRef.current.y === 0) {
stateRef.current.x = stateRef.current.kickoff.x;
stateRef.current.y = stateRef.current.kickoff.y;
}
}, []);
useEffect(() => {
layout();
// re-run a couple times to catch font-load/reflow on wide screens
const t1 = setTimeout(layout, 50);
const t2 = setTimeout(layout, 250);
const t3 = setTimeout(layout, 800);
const onResize = () => layout();
window.addEventListener('resize', onResize);
return () => {
clearTimeout(t1); clearTimeout(t2); clearTimeout(t3);
window.removeEventListener('resize', onResize);
};
}, [layout]);
// Score celebration
const triggerGoal = useCallback(() => {
if (stateRef.current.scoring) return;
stateRef.current.scoring = true;
setShake(true);
setShowGoal(true);
const colors = ['#FEE658', '#DF0024', '#FFEEAB', '#FFFFFF', '#F6D300'];
const pieces = [];
for (let i = 0; i < 70; i++) {
pieces.push({
id: i + Math.random(),
x: 50 + (Math.random() - 0.5) * 16,
y: 38 + (Math.random() - 0.5) * 8,
vx: (Math.random() - 0.5) * 700,
vy: -350 - Math.random() * 450,
rot: Math.random() * 360,
color: colors[i % colors.length],
delay: Math.random() * 0.12,
});
}
setConfetti(pieces);
setTimeout(() => setShake(false), 500);
setTimeout(() => setShowGoal(false), 1400);
setTimeout(() => {
// respawn at a random spawn position
const s = stateRef.current;
const spawns = s.spawnPositions || [s.kickoff];
const spawn = spawns[Math.floor(Math.random() * spawns.length)];
s.x = spawn.x; s.y = spawn.y;
s.vx = 0; s.vy = 0;
s.scoring = false;
setConfetti([]);
}, 2200);
}, []);
// Physics loop
useEffect(() => {
let raf;
let lastT = performance.now();
const tick = (t) => {
const dt = Math.min(0.05, (t - lastT) / 1000);
lastT = t;
const s = stateRef.current;
// friction
const friction = 0.985;
s.vx *= Math.pow(friction, dt * 60);
s.vy *= Math.pow(friction, dt * 60);
// step
if (!s.dragging && !s.scoring) {
s.x += s.vx * dt;
s.y += s.vy * dt;
// wall collisions (inside stage)
const r = s.r;
if (s.x - r < 0) { s.x = r; s.vx = -s.vx * 0.7; }
if (s.x + r > s.stageW) { s.x = s.stageW - r; s.vx = -s.vx * 0.7; }
if (s.y - r < 0) { s.y = r; s.vy = -s.vy * 0.7; }
if (s.y + r > s.stageH) { s.y = s.stageH - r; s.vy = -s.vy * 0.7; }
// stop tiny motion
if (Math.abs(s.vx) < 2) s.vx = 0;
if (Math.abs(s.vy) < 2) s.vy = 0;
// Goal frame collisions — top + sides closed, bottom open
const g = s.goalRect;
const ballAtGoalX = s.x - r < g.x + g.w && s.x + r > g.x;
const ballAtGoalY = s.y - r < g.y + g.h && s.y + r > g.y;
// Crossbar: bounce if approaching from above (vy > 0 = moving down in screen)
if (ballAtGoalX && s.y < g.y && s.y + r > g.y && s.vy > 0) {
s.y = g.y - r;
s.vy *= -0.65;
}
// Left post: bounce if approaching from left (vx > 0 = moving right)
if (ballAtGoalY && s.y > g.y && s.x < g.x && s.x + r > g.x && s.vx > 0) {
s.x = g.x - r;
s.vx *= -0.65;
}
// Right post: bounce if approaching from right (vx < 0 = moving left)
if (ballAtGoalY && s.y > g.y && s.x > g.x + g.w && s.x - r < g.x + g.w && s.vx < 0) {
s.x = g.x + g.w + r;
s.vx *= -0.65;
}
// Score: ball entered through open bottom (vy < 0 = moving upward toward goal)
if (s.x > g.x && s.x < g.x + g.w && s.y > g.y && s.y < g.y + g.h && s.vy < 0) {
triggerGoal();
}
}
// render ball
const ball = ballRef.current;
if (ball) {
const speed = Math.hypot(s.vx, s.vy);
const rot = (t / 4) % 360;
ball.style.transform = `translate(${s.x - s.r}px, ${s.y - s.r}px) rotate(${rot}deg)`;
}
// render cleat cursor (uses viewport/fixed coords)
const cleat = cleatRef.current;
if (cleat && !s.isTouch && s.viewportX != null) {
cleat.style.left = s.viewportX + 'px';
cleat.style.top = s.viewportY + 'px';
cleat.style.transform = `translate(-50%,-50%) rotate(${s.mouseAngle + 90}deg)`;
}
// render aim line while dragging
const svg = aimSvgRef.current;
if (svg) {
if (s.dragging) {
// slingshot: aim opposite of drag direction
const dx = s.dragStartX - s.dragX;
const dy = s.dragStartY - s.dragY;
const len = Math.min(160, Math.hypot(dx, dy));
const ang = Math.atan2(dy, dx);
const ex = s.x + Math.cos(ang) * len;
const ey = s.y + Math.sin(ang) * len;
svg.innerHTML = `
{isTouch
? 'Drag the ball back like a slingshot, then release to shoot. Score on the goal for the celebration.'
: 'Move your cleat into the ball to kick it. The faster you swing, the harder you shoot. Score for confetti.'}
Take Your Shot