import React, { useState, useEffect, useRef, useCallback } from 'react';
const GAME_WIDTH = 800;
const GAME_HEIGHT = 400;
const GROUND_HEIGHT = 80;
const SLIME_RADIUS = 40;
const BALL_RADIUS = 10;
const GOAL_WIDTH = 80;
const GOAL_HEIGHT = 120;
const GRAVITY = 0.6;
const SLIME_SPEED = 5;
const SLIME_JUMP_POWER = -12;
const BALL_DAMPING = 0.99;
const BALL_BOUNCE_DAMPING = 0.8;
const MAX_BALL_SPEED = 13;
const AI_REACTION_DISTANCE = 300;
const AI_PREDICTION_TIME = 30;
const SlimeSoccer = () => {
const canvasRef = useRef(null);
const animationRef = useRef(null);
const keysRef = useRef({});
// Game state
const [gameMode, setGameMode] = useState(null);
const [playerMode, setPlayerMode] = useState(null);
const [timeLeft, setTimeLeft] = useState(0);
const [score, setScore] = useState({ left: 0, right: 0 });
const [gameStarted, setGameStarted] = useState(false);
const [winner, setWinner] = useState(null);
// Game objects state
const gameStateRef = useRef({
leftSlime: {
x: 200,
y: GAME_HEIGHT - GROUND_HEIGHT,
vx: 0,
vy: 0,
isGrabbing: false,
hasBall: false,
goalLineTime: 0
},
rightSlime: {
x: 600,
y: GAME_HEIGHT - GROUND_HEIGHT,
vx: 0,
vy: 0,
isGrabbing: false,
hasBall: false,
goalLineTime: 0
},
ball: {
x: GAME_WIDTH / 2,
y: 150,
vx: 0,
vy: 0,
grabbedBy: null,
grabAngle: 0,
grabAngularVelocity: 0
}
});
// Handle keyboard input
useEffect(() => {
const handleKeyDown = (e) => {
// Don't prevent default for input fields
if (e.target.tagName === 'INPUT') return;
e.preventDefault();
const key = e.key.toLowerCase();
if (key === 'arrowup' || key === 'arrowdown' || key === 'arrowleft' || key === 'arrowright') {
keysRef.current[key] = true;
} else {
keysRef.current[key] = true;
}
};
const handleKeyUp = (e) => {
// Don't prevent default for input fields
if (e.target.tagName === 'INPUT') return;
e.preventDefault();
const key = e.key.toLowerCase();
if (key === 'arrowup' || key === 'arrowdown' || key === 'arrowleft' || key === 'arrowright') {
keysRef.current[key] = false;
} else {
keysRef.current[key] = false;
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, []);
// Timer
useEffect(() => {
if (gameStarted && timeLeft > 0) {
const timer = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) {
setGameStarted(false);
determineWinner();
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}
}, [gameStarted, timeLeft]);
const determineWinner = () => {
if (score.left > score.right) {
setWinner('Cyan Team');
} else if (score.right > score.left) {
setWinner('Red Team');
} else {
setWinner('Draw');
}
};
const resetPositions = () => {
const state = gameStateRef.current;
// Reset slimes to starting positions
state.leftSlime.x = 200;
state.leftSlime.y = GAME_HEIGHT - GROUND_HEIGHT;
state.leftSlime.vx = 0;
state.leftSlime.vy = 0;
state.leftSlime.isGrabbing = false;
state.leftSlime.hasBall = false;
state.leftSlime.goalLineTime = 0;
state.rightSlime.x = 600;
state.rightSlime.y = GAME_HEIGHT - GROUND_HEIGHT;
state.rightSlime.vx = 0;
state.rightSlime.vy = 0;
state.rightSlime.isGrabbing = false;
state.rightSlime.hasBall = false;
state.rightSlime.goalLineTime = 0;
// Reset ball
state.ball.x = GAME_WIDTH / 2;
state.ball.y = 150;
state.ball.vx = 0;
state.ball.vy = 0;
state.ball.grabbedBy = null;
state.ball.grabAngle = 0;
state.ball.grabAngularVelocity = 0;
};
const resetGame = () => {
resetPositions();
setScore({ left: 0, right: 0 });
setWinner(null);
};
const startGame = (mode) => {
const times = {
'1min': 60,
'2min': 120,
'4min': 240,
'8min': 480,
'worldcup': 300
};
resetGame();
setGameMode(mode);
setTimeLeft(times[mode]);
setGameStarted(true);
};
// AI logic for single player mode
const updateAI = useCallback(() => {
if (playerMode !== 'single') return;
const state = gameStateRef.current;
const ai = state.leftSlime; // AI is now left slime
const opponent = state.rightSlime;
const ball = state.ball;
// Enhanced AI parameters
const FIELD_WIDTH = GAME_WIDTH;
const OPPONENT_GOAL_X = FIELD_WIDTH - GOAL_WIDTH / 2;
const AI_GOAL_X = GOAL_WIDTH / 2;
// Add some randomness to AI behavior
const randomFactor = Math.random();
const aggressiveness = 0.7 + randomFactor * 0.3; // 70-100% aggressive
// Predict ball trajectory with more detail
let predictions = [];
let tempX = ball.x;
let tempY = ball.y;
let tempVx = ball.vx;
let tempVy = ball.vy;
for (let t = 0; t < 100; t++) {
tempVy += GRAVITY;
tempVx *= BALL_DAMPING;
tempX += tempVx;
tempY += tempVy;
// Boundary bounces
if (tempX < BALL_RADIUS) {
tempX = BALL_RADIUS;
tempVx = -tempVx * BALL_BOUNCE_DAMPING;
}
if (tempX > FIELD_WIDTH - BALL_RADIUS) {
tempX = FIELD_WIDTH - BALL_RADIUS;
tempVx = -tempVx * BALL_BOUNCE_DAMPING;
}
predictions.push({ x: tempX, y: tempY, vx: tempVx, vy: tempVy, time: t });
if (tempY > GAME_HEIGHT - GROUND_HEIGHT - BALL_RADIUS) {
tempY = GAME_HEIGHT - GROUND_HEIGHT - BALL_RADIUS;
tempVy = -tempVy * BALL_BOUNCE_DAMPING;
break;
}
}
// Analyze game state
const ballDistanceToOpponentGoal = Math.abs(ball.x - OPPONENT_GOAL_X);
const ballDistanceToAIGoal = Math.abs(ball.x - AI_GOAL_X);
const aiDistanceToBall = Math.abs(ai.x - ball.x);
const opponentDistanceToBall = Math.abs(opponent.x - ball.x);
const ballMovingTowardsAIGoal = ball.vx < -1;
const ballMovingTowardsOpponentGoal = ball.vx > 1;
const ballHeight = GAME_HEIGHT - GROUND_HEIGHT - ball.y;
// Prevent repetitive jumping - track if we're stuck
if (!ai.lastBallY) ai.lastBallY = ball.y;
if (!ai.stuckCounter) ai.stuckCounter = 0;
const ballStuck = Math.abs(ball.y - ai.lastBallY) < 5 && Math.abs(ball.vx) < 2;
if (ballStuck) {
ai.stuckCounter++;
} else {
ai.stuckCounter = 0;
}
ai.lastBallY = ball.y;
// Determine optimal position and strategy
let targetX = ai.x;
let shouldJump = false;
let shouldGrab = false;
let moveSpeed = SLIME_SPEED;
// Starting behavior - add variety
if (timeLeft > 58 && gameMode === '1min') {
const startStrategy = randomFactor;
if (startStrategy < 0.3) {
// Defensive start
targetX = 150 + randomFactor * 100;
} else if (startStrategy < 0.7) {
// Midfield start
targetX = FIELD_WIDTH * 0.3 + randomFactor * 100;
} else {
// Aggressive start
targetX = ball.x - 50;
moveSpeed = SLIME_SPEED * aggressiveness;
}
}
// SUPER AGGRESSIVE OFFENSE - Multiple strategies
else if (ballDistanceToOpponentGoal < ballDistanceToAIGoal * 1.5 ||
(ball.x > FIELD_WIDTH * 0.35 && !ballMovingTowardsAIGoal)) {
// Calculate multiple attack angles
const directAttackX = ball.x - 30;
const overheadAttackX = ball.x - 45;
const underAttackX = ball.x - 20;
// Choose attack based on ball height and position
if (ballHeight > 60 && aiDistanceToBall < 150) {
targetX = overheadAttackX; // Set up for overhead kick
} else if (ballHeight < 30 && aiDistanceToBall < 100) {
targetX = underAttackX; // Get under low balls
} else {
targetX = directAttackX + (randomFactor - 0.5) * 20; // Add variety
}
moveSpeed = SLIME_SPEED * 1.2; // Faster when attacking
// Smart offensive decisions
if (aiDistanceToBall < 100) {
// If ball is stuck, hit it harder
if (ai.stuckCounter > 30) {
shouldJump = true;
targetX = ball.x - 40; // Get better angle
}
// Grab low balls for control
else if (ballHeight < 35 && aiDistanceToBall < 60 && !ai.hasBall && ball.vy > -2) {
shouldGrab = true;
}
// Jump for high balls or to create angles
else if ((ballHeight > 30 && ballHeight < 90) ||
(ball.x > FIELD_WIDTH * 0.6 && ballHeight < 120)) {
if (ai.y >= GAME_HEIGHT - GROUND_HEIGHT - 1) {
const timeToReachBall = Math.abs(ai.x - ball.x) / SLIME_SPEED;
const ballHeightWhenReached = ball.y + ball.vy * timeToReachBall + 0.5 * GRAVITY * timeToReachBall * timeToReachBall;
if (ballHeightWhenReached > GAME_HEIGHT - GROUND_HEIGHT - 100 &&
ballHeightWhenReached < GAME_HEIGHT - GROUND_HEIGHT - 20) {
shouldJump = true;
}
}
}
}
// Strategic ball release
if (ai.hasBall) {
const angleToGoal = Math.atan2(0, OPPONENT_GOAL_X - ai.x);
if (Math.abs(angleToGoal) < 0.5 || ai.x > FIELD_WIDTH * 0.7) {
shouldGrab = false; // Release toward goal
}
}
}
// SMART DEFENSE - Predictive and aggressive
else if (ball.x < FIELD_WIDTH * 0.65 || ballMovingTowardsAIGoal) {
// Predict where to intercept
let bestInterceptX = ball.x;
let interceptTime = 0;
for (let pred of predictions) {
if (pred.x < FIELD_WIDTH * 0.4) {
const timeToReach = Math.abs(ai.x - pred.x) / (SLIME_SPEED * 1.2);
if (timeToReach <= pred.time + 5) {
bestInterceptX = pred.x;
interceptTime = pred.time;
break;
}
}
}
targetX = bestInterceptX;
// Emergency defense
if (ball.x < GOAL_WIDTH * 2.5 && ballMovingTowardsAIGoal) {
targetX = Math.max(ball.x - 10, SLIME_RADIUS);
moveSpeed = SLIME_SPEED * 1.3;
// Jump to block shots
if (aiDistanceToBall < 120 && ballHeight < 100) {
shouldJump = true;
}
}
// Clear stuck balls in defense
if (ai.stuckCounter > 20 && ball.x < FIELD_WIDTH * 0.3) {
shouldJump = true;
targetX = ball.x + 30; // Position to clear forward
}
}
// MIDFIELD CONTROL - Dynamic positioning
else {
// Multiple positioning strategies
const strategies = [
{ x: FIELD_WIDTH * 0.35, weight: 0.3 }, // Defensive mid
{ x: FIELD_WIDTH * 0.45, weight: 0.4 }, // Central mid
{ x: ball.x - 60, weight: 0.3 } // Ball-oriented
];
// Weighted random selection
let strategyRoll = randomFactor;
for (let strategy of strategies) {
if (strategyRoll < strategy.weight) {
targetX = strategy.x + (Math.random() - 0.5) * 40;
break;
}
strategyRoll -= strategy.weight;
}
// Intercept high balls in midfield
for (let pred of predictions) {
if (pred.y < GAME_HEIGHT - GROUND_HEIGHT - 50 &&
Math.abs(pred.x - FIELD_WIDTH * 0.4) < 100) {
const timeToReach = Math.abs(ai.x - pred.x) / SLIME_SPEED;
if (timeToReach < pred.time && pred.time < 30) {
targetX = pred.x;
if (pred.time < 20 && ai.y >= GAME_HEIGHT - GROUND_HEIGHT - 1) {
shouldJump = true;
}
break;
}
}
}
}
// Enhanced grab logic
if (shouldGrab && !ai.isGrabbing && ai.y >= GAME_HEIGHT - GROUND_HEIGHT - 1) {
ai.isGrabbing = true;
} else if (!shouldGrab) {
ai.isGrabbing = false;
}
// Smoother movement with acceleration
const difference = targetX - ai.x;
const absDistance = Math.abs(difference);
if (absDistance > 3) {
// Accelerate/decelerate based on distance
const speedMultiplier = Math.min(absDistance / 50, 1.5);
ai.vx = Math.sign(difference) * moveSpeed * speedMultiplier;
} else {
ai.vx = 0;
}
// Execute jump with timing variations
if (shouldJump && ai.vy === 0 && !ai.isGrabbing) {
// Vary jump power slightly for different trajectories
const jumpVariation = 0.9 + randomFactor * 0.2;
ai.vy = SLIME_JUMP_POWER * jumpVariation;
}
// NO BOUNDARIES - AI can go anywhere on the field!
}, [playerMode, timeLeft, gameMode]);
const updatePhysics = useCallback(() => {
const state = gameStateRef.current;
const keys = keysRef.current;
// Update left slime controls (always human player)
if (playerMode === 'multi') {
// Multiplayer: WASD for left player
if (keys['a']) state.leftSlime.vx = -SLIME_SPEED;
else if (keys['d']) state.leftSlime.vx = SLIME_SPEED;
else state.leftSlime.vx = 0;
if (keys['w'] && state.leftSlime.y >= GAME_HEIGHT - GROUND_HEIGHT - 1 && !state.leftSlime.isGrabbing) {
state.leftSlime.vy = SLIME_JUMP_POWER;
}
// Grab control for left player
state.leftSlime.isGrabbing = keys['s'];
// Arrow keys for right player
if (keys['arrowleft']) state.rightSlime.vx = -SLIME_SPEED;
else if (keys['arrowright']) state.rightSlime.vx = SLIME_SPEED;
else state.rightSlime.vx = 0;
if (keys['arrowup'] && state.rightSlime.y >= GAME_HEIGHT - GROUND_HEIGHT - 1 && !state.rightSlime.isGrabbing) {
state.rightSlime.vy = SLIME_JUMP_POWER;
}
// Grab control for right player
state.rightSlime.isGrabbing = keys['arrowdown'];
} else {
// Single player: Arrow keys for human player (right side)
if (keys['arrowleft']) state.rightSlime.vx = -SLIME_SPEED;
else if (keys['arrowright']) state.rightSlime.vx = SLIME_SPEED;
else state.rightSlime.vx = 0;
if (keys['arrowup'] && state.rightSlime.y >= GAME_HEIGHT - GROUND_HEIGHT - 1 && !state.rightSlime.isGrabbing) {
state.rightSlime.vy = SLIME_JUMP_POWER;
}
// Grab control for human player
state.rightSlime.isGrabbing = keys['arrowdown'];
// AI controls left slime
updateAI();
}
// Update slime positions and physics
[state.leftSlime, state.rightSlime].forEach((slime, index) => {
slime.vy += GRAVITY;
slime.x += slime.vx;
slime.y += slime.vy;
// Boundary collision
if (slime.x < SLIME_RADIUS) slime.x = SLIME_RADIUS;
if (slime.x > GAME_WIDTH - SLIME_RADIUS) slime.x = GAME_WIDTH - SLIME_RADIUS;
// Ground collision
if (slime.y > GAME_HEIGHT - GROUND_HEIGHT) {
slime.y = GAME_HEIGHT - GROUND_HEIGHT;
slime.vy = 0;
}
// Check if slime is camping in their OWN goal area
const isLeftSlime = index === 0;
const inOwnGoalArea = (isLeftSlime && slime.x < GOAL_WIDTH) || (!isLeftSlime && slime.x > GAME_WIDTH - GOAL_WIDTH);
if (inOwnGoalArea) {
// Slime is camping in their own goal
slime.goalLineTime += 1/60; // Assuming 60 FPS
// Check if exceeded 1 second
if (slime.goalLineTime >= 1) {
// Award goal to other team (penalty for camping)
if (isLeftSlime) {
setScore(prev => ({ ...prev, right: prev.right + 1 }));
} else {
setScore(prev => ({ ...prev, left: prev.left + 1 }));
}
resetPositions();
}
} else {
// Reset timer if not in own goal area
slime.goalLineTime = 0;
}
});
// Update ball physics
if (state.ball.grabbedBy) {
// Ball is grabbed by a slime
const grabber = state.ball.grabbedBy === 'left' ? state.leftSlime : state.rightSlime;
// Apply rotational physics based on slime movement
const slimeDirection = state.ball.grabbedBy === 'left' ? 1 : -1;
// When slime moves, ball rotates in opposite direction (slower rotation)
state.ball.grabAngularVelocity += -grabber.vx * 0.008 * slimeDirection;
// Apply angular damping
state.ball.grabAngularVelocity *= 0.85;
// Update angle
state.ball.grabAngle += state.ball.grabAngularVelocity;
// Constrain angle based on slime direction
// For left slime: -90° (left) to +90° (right)
// For right slime: 90° (left) to 270° (right)
if (state.ball.grabbedBy === 'left') {
// Left slime: constrain between -π/2 and π/2
if (state.ball.grabAngle < -Math.PI / 2) {
state.ball.grabAngle = -Math.PI / 2;
state.ball.grabAngularVelocity = 0;
} else if (state.ball.grabAngle > Math.PI / 2) {
state.ball.grabAngle = Math.PI / 2;
state.ball.grabAngularVelocity = 0;
}
} else {
// Right slime: keep angle between π/2 and 3π/2
// Normalize angle to 0-2π range first
while (state.ball.grabAngle < 0) state.ball.grabAngle += Math.PI * 2;
while (state.ball.grabAngle > Math.PI * 2) state.ball.grabAngle -= Math.PI * 2;
// Now constrain
if (state.ball.grabAngle < Math.PI / 2 && state.ball.grabAngle >= 0) {
state.ball.grabAngle = Math.PI / 2;
state.ball.grabAngularVelocity = 0;
} else if (state.ball.grabAngle > 3 * Math.PI / 2 ||
(state.ball.grabAngle < Math.PI / 2 && state.ball.grabAngle < 0)) {
state.ball.grabAngle = 3 * Math.PI / 2;
state.ball.grabAngularVelocity = 0;
}
}
// Calculate ball position based on angle
const holdDistance = SLIME_RADIUS + BALL_RADIUS - 5;
state.ball.x = grabber.x + Math.cos(state.ball.grabAngle) * holdDistance;
state.ball.y = grabber.y + Math.sin(state.ball.grabAngle) * holdDistance;
// Ball inherits slime velocity
state.ball.vx = grabber.vx;
state.ball.vy = grabber.vy;
// Check if slime released the grab
if (!grabber.isGrabbing) {
// Release the ball with angular momentum converted to linear
const releaseAngle = state.ball.grabAngle;
const releaseSpeed = Math.abs(state.ball.grabAngularVelocity) * 20;
state.ball.vx = grabber.vx * 1.5 + Math.cos(releaseAngle) * (3 + releaseSpeed);
state.ball.vy = grabber.vy - 2 + Math.sin(releaseAngle) * releaseSpeed * 0.3;
state.ball.grabbedBy = null;
state.ball.grabAngle = 0;
state.ball.grabAngularVelocity = 0;
grabber.hasBall = false;
}
} else {
// Normal ball physics
state.ball.vy += GRAVITY;
state.ball.vx *= BALL_DAMPING;
state.ball.x += state.ball.vx;
state.ball.y += state.ball.vy;
}
// Ball boundary collision
if (state.ball.x < BALL_RADIUS) {
state.ball.x = BALL_RADIUS;
state.ball.vx = -state.ball.vx * BALL_BOUNCE_DAMPING;
}
if (state.ball.x > GAME_WIDTH - BALL_RADIUS) {
state.ball.x = GAME_WIDTH - BALL_RADIUS;
state.ball.vx = -state.ball.vx * BALL_BOUNCE_DAMPING;
}
// Ball ground collision and goal detection
if (state.ball.y > GAME_HEIGHT - GROUND_HEIGHT - BALL_RADIUS) {
state.ball.y = GAME_HEIGHT - GROUND_HEIGHT - BALL_RADIUS;
state.ball.vy = -state.ball.vy * BALL_BOUNCE_DAMPING;
}
// Goal detection - ball hits back wall at any height up to goal height
if (state.ball.x <= BALL_RADIUS && state.ball.y > GAME_HEIGHT - GROUND_HEIGHT - GOAL_HEIGHT) {
// Goal for right team
setScore(prev => ({ ...prev, right: prev.right + 1 }));
resetPositions();
} else if (state.ball.x >= GAME_WIDTH - BALL_RADIUS && state.ball.y > GAME_HEIGHT - GROUND_HEIGHT - GOAL_HEIGHT) {
// Goal for left team
setScore(prev => ({ ...prev, left: prev.left + 1 }));
resetPositions();
}
// Ball ceiling collision
if (state.ball.y < BALL_RADIUS) {
state.ball.y = BALL_RADIUS;
state.ball.vy = -state.ball.vy * BALL_BOUNCE_DAMPING;
}
// Ball-slime collision and grab detection
[state.leftSlime, state.rightSlime].forEach((slime, index) => {
const slimeName = index === 0 ? 'left' : 'right';
const otherSlime = index === 0 ? state.rightSlime : state.leftSlime;
const dx = state.ball.x - slime.x;
const dy = state.ball.y - slime.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < SLIME_RADIUS + BALL_RADIUS) {
// If ball is grabbed by opponent, check if we can knock it out
if (state.ball.grabbedBy && state.ball.grabbedBy !== slimeName) {
// Calculate collision force
const angle = Math.atan2(dy, dx);
const speed = Math.sqrt(slime.vx * slime.vx + slime.vy * slime.vy);
// If slime is moving fast enough, knock the ball out
if (speed > 2 || Math.abs(slime.vy) > 5) {
// Release ball from opponent's grab
state.ball.grabbedBy = null;
state.ball.grabAngle = 0;
state.ball.grabAngularVelocity = 0;
otherSlime.hasBall = false;
// Apply knockback force
state.ball.vx = Math.cos(angle) * 8 + slime.vx;
state.ball.vy = Math.sin(angle) * 8 + slime.vy;
}
}
// Check if slime is trying to grab an ungrabbed ball
else if (slime.isGrabbing && !state.ball.grabbedBy) {
// Grab the ball and set initial angle based on position
state.ball.grabbedBy = slimeName;
state.ball.grabAngle = Math.atan2(dy, dx);
state.ball.grabAngularVelocity = 0;
slime.hasBall = true;
}
// Normal collision if not grabbing
else if (!state.ball.grabbedBy) {
const angle = Math.atan2(dy, dx);
const targetX = slime.x + Math.cos(angle) * (SLIME_RADIUS + BALL_RADIUS);
const targetY = slime.y + Math.sin(angle) * (SLIME_RADIUS + BALL_RADIUS);
// Only collide if ball is above slime center (semicircle collision)
if (state.ball.y < slime.y || Math.abs(angle) < Math.PI * 0.5) {
state.ball.x = targetX;
state.ball.y = targetY;
// Transfer velocity
const speed = Math.sqrt(state.ball.vx * state.ball.vx + state.ball.vy * state.ball.vy);
state.ball.vx = Math.cos(angle) * speed * 1.5 + slime.vx * 0.5;
state.ball.vy = Math.sin(angle) * speed * 1.5 + slime.vy * 0.5;
// Cap ball speed to prevent it from going too fast
const newSpeed = Math.sqrt(state.ball.vx * state.ball.vx + state.ball.vy * state.ball.vy);
if (newSpeed > MAX_BALL_SPEED) {
const scale = MAX_BALL_SPEED / newSpeed;
state.ball.vx *= scale;
state.ball.vy *= scale;
}
}
}
}
});
}, [playerMode, updateAI]);
const draw = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const state = gameStateRef.current;
// Clear canvas
ctx.fillStyle = '#0000FF';
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
// Draw ground
ctx.fillStyle = '#808080';
ctx.fillRect(0, GAME_HEIGHT - GROUND_HEIGHT, GAME_WIDTH, GROUND_HEIGHT);
// Draw goals with new design
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 3;
// Left goal
ctx.beginPath();
// Horizontal line on ground
ctx.moveTo(0, GAME_HEIGHT - GROUND_HEIGHT);
ctx.lineTo(GOAL_WIDTH, GAME_HEIGHT - GROUND_HEIGHT);
// Vertical line at halfway point
ctx.moveTo(GOAL_WIDTH / 2, GAME_HEIGHT - GROUND_HEIGHT);
ctx.lineTo(GOAL_WIDTH / 2, GAME_HEIGHT - GROUND_HEIGHT - GOAL_HEIGHT);
ctx.stroke();
// Left goal net (between wall and vertical line)
ctx.lineWidth = 1.5; // Updated from 0.6
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
// Vertical net lines
for (let i = 0; i < GOAL_WIDTH / 2; i += 10) {
ctx.beginPath();
ctx.moveTo(i, GAME_HEIGHT - GROUND_HEIGHT - GOAL_HEIGHT);
ctx.lineTo(i, GAME_HEIGHT - GROUND_HEIGHT);
ctx.stroke();
}
// Horizontal net lines
for (let j = GAME_HEIGHT - GROUND_HEIGHT - GOAL_HEIGHT; j <= GAME_HEIGHT - GROUND_HEIGHT; j += 10) {
ctx.beginPath();
ctx.moveTo(0, j);
ctx.lineTo(GOAL_WIDTH / 2, j);
ctx.stroke();
}
// Reset for right goal
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 3;
// Right goal
ctx.beginPath();
// Horizontal line on ground
ctx.moveTo(GAME_WIDTH - GOAL_WIDTH, GAME_HEIGHT - GROUND_HEIGHT);
ctx.lineTo(GAME_WIDTH, GAME_HEIGHT - GROUND_HEIGHT);
// Vertical line at halfway point
ctx.moveTo(GAME_WIDTH - GOAL_WIDTH / 2, GAME_HEIGHT - GROUND_HEIGHT);
ctx.lineTo(GAME_WIDTH - GOAL_WIDTH / 2, GAME_HEIGHT - GROUND_HEIGHT - GOAL_HEIGHT);
ctx.stroke();
// Right goal net (between wall and vertical line)
ctx.lineWidth = 1.5; // Updated from 0.6
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
// Vertical net lines
for (let i = GAME_WIDTH - GOAL_WIDTH / 2; i <= GAME_WIDTH; i += 10) {
ctx.beginPath();
ctx.moveTo(i, GAME_HEIGHT - GROUND_HEIGHT - GOAL_HEIGHT);
ctx.lineTo(i, GAME_HEIGHT - GROUND_HEIGHT);
ctx.stroke();
}
// Horizontal net lines
for (let j = GAME_HEIGHT - GROUND_HEIGHT - GOAL_HEIGHT; j <= GAME_HEIGHT - GROUND_HEIGHT; j += 10) {
ctx.beginPath();
ctx.moveTo(GAME_WIDTH - GOAL_WIDTH / 2, j);
ctx.lineTo(GAME_WIDTH, j);
ctx.stroke();
}
// Draw goal line timers
const drawGoalLineTimer = (slime, goalX, goalWidth) => {
if (slime.goalLineTime > 0) {
const percentage = 1 - (slime.goalLineTime / 1); // 1 second max
const timerWidth = goalWidth * percentage;
ctx.strokeStyle = percentage > 0.3 ? '#FFFF00' : '#FF0000'; // Yellow to red
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(goalX, GAME_HEIGHT - GROUND_HEIGHT + 10);
ctx.lineTo(goalX + timerWidth, GAME_HEIGHT - GROUND_HEIGHT + 10);
ctx.stroke();
// Reset stroke style
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 3;
}
};
// Check and draw timers for slimes camping in their own goals
if (state.leftSlime.x < GOAL_WIDTH) {
drawGoalLineTimer(state.leftSlime, 0, GOAL_WIDTH);
}
if (state.rightSlime.x > GAME_WIDTH - GOAL_WIDTH) {
drawGoalLineTimer(state.rightSlime, GAME_WIDTH - GOAL_WIDTH, GOAL_WIDTH);
}
// Draw slimes (as semicircles)
const drawSlime = (slime, isRightSlime, color, accentColor) => {
ctx.save();
ctx.imageSmoothingEnabled = false;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(slime.x, slime.y, SLIME_RADIUS, Math.PI, 0);
ctx.closePath();
ctx.fill();
// Add accent stripe
ctx.fillStyle = accentColor;
ctx.beginPath();
ctx.arc(slime.x, slime.y, SLIME_RADIUS - 5, Math.PI + 0.3, Math.PI + 0.7);
ctx.arc(slime.x, slime.y, SLIME_RADIUS - 15, Math.PI + 0.7, Math.PI + 0.3, true);
ctx.closePath();
ctx.fill();
// Add grab indicator if grabbing
if (slime.isGrabbing) {
// Remove visual indicator - no yellow outline
}
ctx.restore();
// Draw eye
ctx.fillStyle = '#FFFFFF';
ctx.beginPath();
// Adjust eye position based on which slime
const eyeXOffset = isRightSlime ? -SLIME_RADIUS * 0.3 : SLIME_RADIUS * 0.3;
ctx.arc(slime.x + eyeXOffset, slime.y - SLIME_RADIUS * 0.3, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000000';
ctx.beginPath();
const pupilXOffset = isRightSlime ? -SLIME_RADIUS * 0.35 : SLIME_RADIUS * 0.35;
ctx.arc(slime.x + pupilXOffset, slime.y - SLIME_RADIUS * 0.3, 2, 0, Math.PI * 2);
ctx.fill();
};
drawSlime(state.leftSlime, false, '#00CED1', '#008B8B');
drawSlime(state.rightSlime, true, '#DC143C', '#8B0000');
// Draw ball
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(state.ball.x, state.ball.y, BALL_RADIUS, 0, Math.PI * 2);
ctx.fill();
}, []);
const gameLoop = useCallback(() => {
if (gameStarted) {
updatePhysics();
draw();
animationRef.current = requestAnimationFrame(gameLoop);
}
}, [gameStarted, updatePhysics, draw]);
useEffect(() => {
if (gameStarted) {
animationRef.current = requestAnimationFrame(gameLoop);
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [gameStarted, gameLoop]);
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
return (
{!gameStarted && !gameMode && !playerMode && (
Claude Soccer Slime
Written by Quin Pendragon (originally)
Adapted by none other than Claude
)}
{playerMode && !gameStarted && !gameMode && (
Select Game Duration
Cyan TeamvsRed Team
{playerMode === 'multi' ? (
<>
Left Team: W (jump), A/D (move), S (grab)
Right Team: ↑ (jump), ←/→ (move), ↓ (grab)
>
) : (
<>
Use Arrow Keys: ↑ (jump), ←/→ (move), ↓ (grab)
Hold ↓ to grab the ball when it's near!
>
)}
)}
{(gameStarted || winner) && (
Cyan Team: {score.left}{formatTime(timeLeft)}{score.right} : Red Team
{winner && (
{winner === 'Draw' ? "It's a Draw!" : `${winner} Wins!`}
)}
)}
);
};
export default SlimeSoccer;
Content Management
Pellentesque in ipsum id orci porta dapibus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Data Management
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sed finibus nisi, sed dictum eros. Donec ultricies lobortis eros nec auctor nisl semper.
CRM Management
Ac feugiat ante. Donec ultricies lobortis eros, nec auctor nisl semper ultricies. Aliquam sodales nulla dolor. Fermentum nulla non justo aliquet.
Take A Quick Tour
Donec rutrum congue leo eget malesuada. Vestibulum ac diam sit amet
10x
Analyze Financial Data
Build a Stronger Relationship with Your Customers
Managing Your Business Doesn’t Have to Be Hard
Business Management Software
Live Demo
Mobile Ready
Donec rutrum congue leo eget malesuada. Quisque velit nisi, pretium ut lacinia in, elementum id enim. Vestibulum ac diam sit amet quam vehicula elementum sed sit amet dui. Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Donec rutrum congue leo eget malesuada. Curabitur aliquet quam id dui posuere blandit.
Quisque velit nisi, pretium ut lacinia in, elementum id enim. Praesent sapien massa, convallis.
Analyze Customer Data
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sed finibus nisi, sed dictum eros. Quisque aliquet velit sit amet sem interdum faucibus. In feugiat aliquet mollis. Etiam tincidunt ligula ut hendrerit semper. Quisque luctus lectus non turpis bibendum posuere. Morbi tortor nibh, fringilla sed pretium sit amet, pharetra non ex. Fusce vel egestas nisl.
Analyze Financial Data
Curabitur fermentum nulla non justo aliquet, quis vehicula quam consequat. Duis ut hendrerit tellus, elementum lacinia elit. Maecenas at consectetur ex, vitae consequat augue. Vivamus eget dolor vel quam condimentum sodales. In bibendum odio urna, sit amet fermentum purus venenatis amet.
Social
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sed finibus nisi, sed dictum eros.
Secure
Quisque aliquet velit sit amet sem interdum faucibus. In feugiat aliquet mollis etiam tincidunt ligula.
Connected
Luctus lectus non quisque turpis bibendum posuere. Morbi tortor nibh, fringilla sed pretium sit amet.
Analyze Customer Data
Vivamus suscipit tortor eget felis porttitor volutpat. Nulla quis lorem ut libero malesuada feugiat. Curabitur aliquet quam id dui posuere blandit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus suscipit tortor eget felis porttitor volutpat. Cras ultricies ligula sed magna dictum porta. Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Donec
★★★★★
« Vivamus magna justo, lacinia eget consectetur sed, convallis at tellus. Lorem ipsum dolor sit amet, consectetur. »
★★★★★
« Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sed finibus nisi, sed dictum eros. »
★★★★★
« Quisque aliquet velit sit amet sem interdum faucibus. In feugiat aliquet mollis etiam tincidunt ligula. »
★★★★★
« Luctus lectus non quisque turpis bibendum posuere. Morbi tortor nibh, fringilla sed pretium sit amet. Aliquam sodales nulla dolor sed vulputate sapien. »
Get on Track
Join 28k+ users & teams
Vivamus suscipit tortor eget felis porttitor volutpat. Nulla quis lorem ut libero malesuada feugiat. Curabitur aliquet quam id dui