html, body {
margin: 0;
padding: 0;
overflow: hidden;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #333;
}
canvas {
display: block;
}
let puckPos, puckVel;
let puckRadius, puckFriction, puckMinSpeed, puckMaxSpeed;
let paddlePos, prevPaddlePos;
let paddleRadius;
let arenaMargin, canvasWidth, canvasHeight;
let goalRadius;
let cornerRadius, cornerCenters = [];
let playerScore = 0;
let opponentScore = 0;
let passedGoalThreshold = false;
let goalScored = false;
let goalTimer = 0;
let goalDelay;
let playerSideStart = true;
let wallSmacked = false;
let wallCollisionTimer = 0;
let shakeTime;
let sparks = [];
let aiPos, aiPrevPos;
let aiRadius;
const AI_STATE = { DEFEND: 0, ATTACK: 1, POSSESSION: 2 };
let aiState = AI_STATE.DEFEND;
let MAX_AI_SPD, stallFrames = 0, stallDuration;
let aiDefendNoiseOffset = 0; // for smooth randomness
let adaptiveMaxAISpd, adaptiveClampRange, adaptiveGoalKick;
let isMuted = false;
const WINNING_SCORE = 7;
let gameOver = false;
function setup() {
calculateCanvasSize();
createCanvas(canvasWidth, canvasHeight);
// Responsive values based on canvas size
arenaMargin = canvasHeight * 0.05;
puckRadius = canvasWidth * 0.0375;
paddleRadius = canvasWidth * 0.05;
aiRadius = paddleRadius;
cornerRadius = canvasWidth * 0.025;
goalRadius = canvasWidth * 0.2;
puckFriction = 0.985;
puckMinSpeed = canvasWidth * 0.0008;
puckMaxSpeed = canvasWidth * 0.055;
goalDelay = 1000;
shakeTime = 500;
MAX_AI_SPD = canvasHeight * 0.014;
stallDuration = 25;
adaptiveMaxAISpd = MAX_AI_SPD;
adaptiveClampRange = goalRadius;
adaptiveGoalKick = canvasWidth * 0.013;
cornerCenters = [
createVector(arenaMargin, arenaMargin), // Top left
createVector(width - arenaMargin, arenaMargin), // Top right
createVector(arenaMargin, height - arenaMargin), // Bottom left
createVector(width - arenaMargin, height - arenaMargin) // Bottom right
];
initGameObjects();
}
function calculateCanvasSize() {
let minHeight = 320;
canvasHeight = max(minHeight, windowHeight);
canvasWidth = canvasHeight * 0.75; // 3:4 aspect ratio
}
function initGameObjects() {
if (playerSideStart) {
puckPos = createVector(width / 2, height / 2 + height / 8);
} else {
puckPos = createVector(width / 2, height / 2 - height / 8);
}
puckVel = createVector(0, 0);
paddlePos = createVector(mouseX, constrain(mouseY, height / 2, height)); // player is on bottom half
prevPaddlePos = paddlePos.copy();
aiPos = createVector(width / 2, arenaMargin + aiRadius * 2);
aiPrevPos = aiPos.copy();
}
function draw() {
if (gameOver) {
drawGameOverScreen();
// Rematch button
let btnW = canvasWidth * 0.38;
let btnH = canvasWidth * 0.12;
let btnX = width / 2;
let btnY = height - arenaMargin - btnH * 1.2;
if (
mouseX > btnX - btnW / 2 &&
mouseX < btnX + btnW / 2 &&
mouseY > btnY - btnH / 2 &&
mouseY < btnY + btnH / 2
) {
cursor('pointer');
} else {
cursor('default');
}
return;
}
if (goalScored) {
let shake = width / 72;
translate(random(-shake, shake), random(-shake, shake));
}
if (wallSmacked && millis() - wallCollisionTimer <= shakeTime) {
let shake = map(pVelocity, 0, puckMaxSpeed, 0, width / 72);
translate(random(-shake, shake), random(-shake, shake));
}
if (millis() - wallCollisionTimer > shakeTime) {
wallSmacked = false;
}
background(30);
drawArena();
prevPaddlePos.set(paddlePos);
let targetX, targetY;
if (touches.length > 0) {
targetX = touches[0].x;
targetY = touches[0].y;
} else {
targetX = mouseX;
targetY = mouseY;
}
paddlePos.set(
constrain(targetX, arenaMargin + paddleRadius, width - arenaMargin - paddleRadius),
constrain(targetY, height / 2 + paddleRadius, height - arenaMargin - paddleRadius)
);
aiPrevPos.set(aiPos);
updateAI();
updatePuck();
checkWallCollision();
checkCornerCollision();
if (passedGoalThreshold) checkGoal();
checkPaddleCollision(); // human paddle
checkAIPaddleCollision(aiPos, aiPrevPos); // AI paddle
drawPuck();
drawPlayerPaddle();
drawAIPaddle();
for (let i = sparks.length - 1; i >= 0; i--) {
sparks[i].update();
sparks[i].show();
if (sparks[i].isDead()) sparks.splice(i, 1);
}
drawTopAndBackWalls();
drawScoreboard();
// Mute button
let btnSize = canvasWidth * 0.08;
let muteBtnX = width - btnSize * 0.7;
let muteBtnY = btnSize * 0.7;
if (
mouseX > muteBtnX - btnSize / 2 &&
mouseX < muteBtnX + btnSize / 2 &&
mouseY > muteBtnY - btnSize / 2 &&
mouseY < muteBtnY + btnSize / 2
) {
cursor('pointer');
} else {
cursor('default');
}
}
function drawArena() {
noFill();
stroke(100);
strokeWeight(canvasWidth * 0.005);
rect(arenaMargin, arenaMargin, width - arenaMargin * 2, height - arenaMargin * 2, cornerRadius * 2);
drawGoalAreas();
drawCenterLine();
drawCenterFaceoffCircleWithCornerHashmarks();
if (goalScored) {
push();
stroke(random(150, 255));
strokeWeight(width * 0.025);
playerSideStart ? fill(227, 66, 52) : fill(50, 150, 255);
textSize(width * 0.25);
textAlign(CENTER, CENTER);
textStyle(BOLD);
text("GOAL!", width / 2, height / 2)
pop();
}
}
// for puck sliding in the goal effect
function drawTopAndBackWalls() {
fill(30, 30, 30);
rect(0, 0, width, arenaMargin - 2);
rect(0, height - arenaMargin + 2, width, height);
}
function drawScoreboard() {
textAlign(CENTER, TOP);
textSize(width * 0.05);
fill(50, 150, 255);
text(`${playerScore}`, width / 2 - 30, 10);
fill(255);
text(` - `, width / 2, 10);
fill(227, 66, 52);
text(`${opponentScore}`, width / 2 + 30, 10);
// mute button
let btnSize = canvasWidth * 0.08; // 8% of width
let btnX = width - btnSize * 0.7;
let btnY = btnSize * 0.7;
let btnRadius = btnSize * 0.18;
fill(40);
stroke(80);
strokeWeight(canvasWidth * 0.005);
rectMode(CENTER);
rect(btnX, btnY, btnSize, btnSize, btnRadius);
// Emoji Icons
noStroke();
fill(230);
textSize(btnSize * 0.5);
textAlign(CENTER, CENTER);
text(isMuted ? "🔇" : "🔊", btnX, btnY - btnSize * 0.18);
// Clarifying text inside button
textSize(btnSize * 0.23);
fill(220);
textAlign(CENTER, CENTER);
text(isMuted ? "unmute" : "mute", btnX, btnY + btnSize * 0.22);
rectMode(CORNER); // Restore default
}
function drawGoalAreas() {
stroke(180); // light gray
strokeWeight(canvasWidth * 0.0025);
noFill();
const goalWidth = goalRadius * 2;
// Top Goal
arc(width / 2, arenaMargin, goalWidth, goalWidth, 0, PI);
// Bottom Goal
arc(width / 2, height - arenaMargin, goalWidth, goalWidth, PI, TWO_PI);
}
function drawCenterLine() {
stroke(180);
strokeWeight(canvasWidth * 0.0025);
const dashLength = width * 0.015;
const gapLength = dashLength;
const centerY = height / 2;
const startX = arenaMargin;
const endX = width - arenaMargin;
for (let x = startX; x < endX; x += dashLength + gapLength) {
line(x, centerY, x + dashLength, centerY);
}
}
function drawCenterFaceoffCircleWithCornerHashmarks() {
const centerX = width / 2;
const centerY = height / 2;
const baseSize = width;
const circleRadius = baseSize * 0.15;
const dotDiameter = circleRadius * 0.2;
const hashOffset = circleRadius * 0.1;
const hashLength = circleRadius * 0.1;
let lineColor = color(180);
stroke(lineColor);
strokeWeight(canvasWidth * 0.0025);
noFill();
// Circle ring
ellipse(centerX, centerY, circleRadius * 2);
// Center dot
noStroke();
fill(lineColor);
ellipse(centerX, centerY, dotDiameter);
// Corner hashmark L shapes
stroke(lineColor);
strokeWeight(canvasWidth * 0.0025);
const dirs = [
{ dx: -1, dy: -1 }, // top left
{ dx: 1, dy: -1 }, // top right
{ dx: -1, dy: 1 }, // bottom left
{ dx: 1, dy: 1 } // bottom right
];
for (let d of dirs) {
const x = centerX + d.dx * hashOffset;
const y = centerY + d.dy * hashOffset;
// Vertical segment
line(x, y, x, y + d.dy * hashLength);
// Horizontal segment
line(x, y, x + d.dx * hashLength, y);
}
}
function drawPuck() {
fill(255);
noStroke();
circle(puckPos.x, puckPos.y, puckRadius * 2);
}
function drawPlayerPaddle() {
drawPaddleBase(paddlePos, color(50, 150, 255));
}
function drawAIPaddle() {
drawPaddleBase(aiPos, color(227, 66, 52));
}
function drawPaddleBase(pos, col) {
fill(col);
circle(pos.x, pos.y, paddleRadius * 2);
fill(30);
noStroke();
circle(pos.x, pos.y, paddleRadius);
fill(col);
noStroke();
circle(pos.x, pos.y, paddleRadius * 0.75);
}
function updatePuck() {
puckPos.add(puckVel);
puckVel.mult(puckFriction);
if (puckVel.mag() < puckMinSpeed) {
puckVel.set(0, 0);
}
if (puckVel.mag() > puckMaxSpeed) {
puckVel.setMag(puckMaxSpeed);
}
}
function resetPuck() {
if (playerSideStart) {
puckPos.set(width / 2, height / 2 + height / 8);
} else {
puckPos.set(width / 2, height / 2 - height / 8);
}
puckVel.set(0, 0);
}
function checkGoal() {
if (!goalScored) {
puckVel.y *= 5;
if (puckPos.y + puckRadius > height - arenaMargin) {
goalScored = true;
opponentScore++;
playerSideStart = true;
goalTimer = millis();
playGoalSound();
} else if (puckPos.y - puckRadius < arenaMargin) {
goalScored = true;
playerScore++;
playerSideStart = false;
goalTimer = millis();
playGoalSound();
}
}
// Check for game over
if (playerScore >= WINNING_SCORE || opponentScore >= WINNING_SCORE) {
gameOver = true;
return;
}
if (goalScored && millis() - goalTimer > goalDelay) {
resetPuck();
goalScored = false;
passedGoalThreshold = false;
}
}
function isInsideGoalRange() {
const goalCenter = width / 2;
if (puckPos.x >= goalCenter - goalRadius && puckPos.x <= goalCenter + goalRadius) {
return true
}
}
function checkWallCollision() {
// Check Goal threshold
const isInsideGoalXRange = isInsideGoalRange();
if (isInsideGoalXRange || passedGoalThreshold) {
if (puckPos.y > height - arenaMargin || puckPos.y < arenaMargin) {
passedGoalThreshold = true
}
} else {
// Left wall
if (puckPos.x - puckRadius < arenaMargin) {
let normal = createVector(1, 0);
let penetration = arenaMargin + puckRadius - puckPos.x;
puckPos.x += penetration + 0.5;
wallSmacked = true;
wallCollisionTimer = millis();
pVelocity = puckVel.mag();
playWallSound(puckVel.mag());
createSparks(puckPos, normal);
applyWallGlide(normal);
}
// Right wall
if (puckPos.x + puckRadius > width - arenaMargin) {
let normal = createVector(-1, 0);
let penetration = puckPos.x + puckRadius - (width - arenaMargin);
puckPos.x -= penetration + 0.5;
wallSmacked = true;
wallCollisionTimer = millis();
pVelocity = puckVel.mag();
playWallSound(puckVel.mag());
createSparks(puckPos, normal);
applyWallGlide(normal);
}
// Top wall
if (puckPos.y - puckRadius < arenaMargin) {
let normal = createVector(0, 1);
let penetration = arenaMargin + puckRadius - puckPos.y;
puckPos.y += penetration + 0.5;
wallSmacked = true;
wallCollisionTimer = millis();
pVelocity = puckVel.mag();
playWallSound(puckVel.mag());
createSparks(puckPos, normal);
applyWallGlide(normal);
}
// Bottom wall
if (puckPos.y + puckRadius > height - arenaMargin) {
let normal = createVector(0, -1);
let penetration = puckPos.y + puckRadius - (height - arenaMargin);
puckPos.y -= penetration + 0.5;
wallSmacked = true;
wallCollisionTimer = millis();
pVelocity = puckVel.mag();
playWallSound(puckVel.mag());
createSparks(puckPos, normal);
applyWallGlide(normal);
}
}
}
function applyWallGlide(normal) {
let vDotN = puckVel.dot(normal);
if (vDotN < 0) {
// Tangent direction = velocity - normal component
let normalComponent = normal.copy().mult(vDotN);
let tangentComponent = puckVel.copy().sub(normalComponent);
let approachAngle = abs(vDotN) / puckVel.mag(); // 0 = grazing, 1 = head-on
// Blend between gliding and bounce
let glideRatio = constrain(map(approachAngle, 0, 0.5, 1, 0), 0, 1);
let bounceRatio = 1 - glideRatio;
let reflected = tangentComponent.copy().add(normal.copy().mult(-vDotN * bounceRatio));
puckVel.set(reflected);
puckVel.mult(0.98);
}
}
function checkCornerCollision() {
for (let corner of cornerCenters) {
let toPuck = p5.Vector.sub(puckPos, corner);
let dist = toPuck.mag();
let allowedDist = cornerRadius - 2 + puckRadius;
if (dist < allowedDist && dist > 0.01) {
let normal = toPuck.copy().normalize();
let overlap = allowedDist - dist;
// Push puck out of the wall
puckPos.add(normal.copy().mult(overlap + 0.5));
let vDotN = puckVel.dot(normal);
if (vDotN < 0) {
// Project velocity onto tangent
let tangent = createVector(-normal.y, normal.x); // Perpendicular to normal
let tangentSpeed = puckVel.dot(tangent);
// Preserve full tangent speed, discard only the wall-piercing part
puckVel = tangent.copy().mult(tangentSpeed);
// Optional: very small speed loss to simulate real contact
puckVel.mult(0.999);
}
}
}
}
function checkPaddleCollision() {
let toPuck = p5.Vector.sub(puckPos, paddlePos);
let dist = toPuck.mag();
let minDist = puckRadius + paddleRadius;
if (dist < minDist && dist > 0.01) {
let collisionNormal = toPuck.copy().normalize();
let paddleVel = p5.Vector.sub(paddlePos, prevPaddlePos);
let relativeVel = p5.Vector.sub(puckVel, paddleVel);
let speedTowardEachOther = relativeVel.dot(collisionNormal);
// Only apply if actually moving toward each other
if (speedTowardEachOther < 0) {
// Scale impulse based on approach speed
let impulseStrength = constrain(-speedTowardEachOther, 0, 10);
let impulse = collisionNormal.copy().mult(impulseStrength * 1.2);
puckVel.add(impulse);
playHitSound(impulseStrength);
createSparks(puckPos, collisionNormal);
if (puckVel.mag() > puckMaxSpeed) {
puckVel.setMag(puckMaxSpeed);
}
// Resolve overlap, but prevent pushing puck into walls
let overlap = minDist - dist;
let correction = collisionNormal.copy().mult(overlap + 0.5);
let newPos = p5.Vector.add(puckPos, correction);
// Clamp to stay inside arena after correction
newPos.x = constrain(newPos.x, arenaMargin + puckRadius, width - arenaMargin - puckRadius);
newPos.y = constrain(newPos.y, arenaMargin + puckRadius, height - arenaMargin - puckRadius);
puckPos.set(newPos);
}
}
}
function checkAIPaddleCollision(pos, prev) {
let toPuck = p5.Vector.sub(puckPos, pos);
let dist = toPuck.mag();
let minDist = puckRadius + paddleRadius;
if (dist < minDist && dist > 0.01) {
let collisionNormal = toPuck.copy().normalize();
let paddleVel = p5.Vector.sub(pos, prev);
let relativeVel = p5.Vector.sub(puckVel, paddleVel);
let speedToward = relativeVel.dot(collisionNormal);
if (speedToward < 0) {
let impulseStrength = constrain(-speedToward, 0, 20);
let impulse = collisionNormal.copy().mult(impulseStrength * 1.2);
const goalKick = adaptiveGoalKick; // adaptive shot strength
const goalDir = dirToPlayerGoal(puckPos);
impulse.add(goalDir.mult(goalKick));
puckVel.add(impulse);
playHitSound(impulseStrength);
createSparks(puckPos, collisionNormal);
if (puckVel.mag() > puckMaxSpeed) puckVel.setMag(puckMaxSpeed);
let overlap = minDist - dist;
let correction = collisionNormal.copy().mult(overlap + 0.5);
let newPos = p5.Vector.add(puckPos, correction);
newPos.x = constrain(newPos.x, arenaMargin + puckRadius, width - arenaMargin - puckRadius);
newPos.y = constrain(newPos.y, arenaMargin + paddleRadius, height - arenaMargin - paddleRadius);
puckPos.set(newPos);
}
}
}
function dirToPlayerGoal(fromVec) {
const goalCenter = createVector(width / 2, height - arenaMargin);
return p5.Vector.sub(goalCenter, fromVec).normalize(); // unit vector
}
function updateAI() {
// Adaptive Difficulty
let scoreDiff = playerScore - opponentScore;
adaptiveMaxAISpd = map(constrain(scoreDiff, -5, 5), -5, 5, canvasHeight * 0.008, canvasHeight * 0.025); // speed
adaptiveClampRange = map(constrain(scoreDiff, -5, 5), -5, 5, goalRadius * 1.5, goalRadius * 0.5); // defense
adaptiveGoalKick = map(constrain(scoreDiff, -5, 5), -5, 5, canvasWidth * 0.009, canvasWidth * 0.025); // shot
let puckOnAIHalf = puckPos.y < height / 2;
// Responsive stall threshold
if (puckOnAIHalf && puckVel.mag() < puckMinSpeed * 9) stallFrames++; else stallFrames = 0;
if (stallFrames > stallDuration) aiState = AI_STATE.POSSESSION;
else if (puckOnAIHalf && puckVel.y < 0) aiState = AI_STATE.ATTACK;
else aiState = AI_STATE.DEFEND;
// choose target
let target;
switch (aiState) {
case AI_STATE.DEFEND: {
aiDefendNoiseOffset += 0.01;
let noiseVal = noise(aiDefendNoiseOffset);
let maxOffset = canvasWidth * 0.07;
let offset = map(noiseVal, 0, 1, -maxOffset, maxOffset);
let goalCenterX = width / 2;
let defendX = constrain(
puckPos.x + offset,
goalCenterX - adaptiveClampRange - offset,
goalCenterX + adaptiveClampRange + offset
);
target = createVector(
defendX,
arenaMargin + aiRadius + 10
);
break;
}
case AI_STATE.ATTACK:
const lookAhead = 20; // frames
let anticipated = p5.Vector.add(
puckPos,
p5.Vector.mult(puckVel, lookAhead / 60)
);
anticipated.y = constrain(anticipated.y,
arenaMargin + aiRadius,
height / 2 - aiRadius * 1.2);
anticipated.x = constrain(anticipated.x,
arenaMargin + aiRadius,
width - arenaMargin - aiRadius);
target = anticipated;
break;
case AI_STATE.POSSESSION: // retrieve stalled puck
target = puckPos.copy();
if (puckPos.dist(aiPos) < aiRadius * 0.6) {
aiState = AI_STATE.ATTACK;
}
break;
}
// move towards target
let desire = p5.Vector.sub(target, aiPos);
if (desire.mag() > adaptiveMaxAISpd) desire.setMag(adaptiveMaxAISpd);
aiPos.add(desire);
let maxY = (aiState === AI_STATE.POSSESSION) ? height / 2 + aiRadius * 0.4
: height / 2 - aiRadius;
aiPos.y = constrain(aiPos.y, arenaMargin + aiRadius, maxY);
aiPos.x = constrain(aiPos.x, arenaMargin + aiRadius, width - arenaMargin - aiRadius);
}
function playHitSound(force = 1) {
if (isMuted) return;
// Map force to volume
let vol = map(force, 0, 20, 0.1, 0.5);
let noise = new p5.Noise('white');
let env = new p5.Envelope();
env.setADSR(0.001, 0.05, 0, 0.05);
env.setRange(vol, 0);
noise.amp(env);
noise.start();
env.play(noise, 0, 0.03);
noise.stop(0.08);
let osc = new p5.Oscillator('sine');
let env2 = new p5.Envelope();
env2.setADSR(0.005, 0.08, 0, 0.08);
env2.setRange(vol * 0.7, 0);
osc.freq(180 + random(-30, 30));
osc.amp(env2);
osc.start();
env2.play(osc, 0, 0.04);
osc.stop(0.12);
}
function playWallSound(force = 1) {
if (isMuted) return;
let vol = map(force, 0, puckMaxSpeed, 0.1, 0.4);
let noise = new p5.Noise('white');
let env = new p5.Envelope();
env.setADSR(0.001, 0.07, 0, 0.07);
env.setRange(vol, 0);
noise.amp(env);
noise.start();
env.play(noise, 0, 0.04);
noise.stop(0.12);
let osc = new p5.Oscillator('sine');
let env2 = new p5.Envelope();
env2.setADSR(0.002, 0.06, 0, 0.06);
env2.setRange(vol * 0.7, 0);
osc.freq(320 + random(-40, 40));
osc.amp(env2);
osc.start();
env2.play(osc, 0, 0.03);
osc.stop(0.10);
}
function playGoalSound() {
if (isMuted) return;
let osc = new p5.Oscillator('triangle');
let env = new p5.Envelope();
env.setADSR(0.01, 0.2, 0.2, 0.2);
env.setRange(0.4, 0);
osc.freq(220);
osc.amp(env);
osc.start();
env.play(osc, 0, 0.1);
osc.freq(440, 0.1);
osc.stop(0.3);
}
class Spark {
constructor(position, direction) {
this.pos = position.copy();
this.vel = direction.copy().mult(random(2, 5)).rotate(random(-PI / 6, PI / 6));
this.life = 255;
this.size = random(canvasWidth * 0.0035, canvasWidth * 0.008);
this.color = color(255, 200 + random(55), 0); // fiery orange
}
update() {
this.pos.add(this.vel);
this.vel.mult(0.95);
this.life -= 10;
}
isDead() {
return this.life <= 0;
}
show() {
noStroke();
fill(red(this.color), green(this.color), blue(this.color), this.life);
circle(this.pos.x, this.pos.y, this.size);
}
}
function createSparks(position, direction, count = 7) {
for (let i = 0; i < count; i++) {
sparks.push(new Spark(position, direction));
}
}
function windowResized() {
calculateCanvasSize();
resizeCanvas(canvasWidth, canvasHeight);
// Update all responsive values based on new canvas size
arenaMargin = canvasHeight * 0.05;
puckRadius = canvasWidth * 0.0375;
paddleRadius = canvasWidth * 0.05;
aiRadius = paddleRadius;
cornerRadius = canvasWidth * 0.025;
goalRadius = canvasWidth * 0.2;
puckFriction = 0.985;
puckMinSpeed = canvasWidth * 0.0008;
puckMaxSpeed = canvasWidth * 0.055;
goalDelay = 1000;
shakeTime = 500;
MAX_AI_SPD = canvasHeight * 0.014;
stallDuration = 25;
adaptiveMaxAISpd = MAX_AI_SPD;
adaptiveClampRange = goalRadius;
adaptiveGoalKick = canvasWidth * 0.013;
cornerCenters = [
createVector(arenaMargin, arenaMargin),
createVector(width - arenaMargin, arenaMargin),
createVector(arenaMargin, height - arenaMargin),
createVector(width - arenaMargin, height - arenaMargin)
];
// Re-initialize paddle and puck positions to fit new canvas
initGameObjects();
}
function mousePressed() {
if (gameOver) {
let btnW = canvasWidth * 0.38;
let btnH = canvasWidth * 0.12;
let btnX = width / 2;
let btnY = height - arenaMargin - btnH * 1.2;
if (
mouseX > btnX - btnW / 2 &&
mouseX < btnX + btnW / 2 &&
mouseY > btnY - btnH / 2 &&
mouseY < btnY + btnH / 2
) {
// Reset game
playerScore = 0;
opponentScore = 0;
gameOver = false;
goalScored = false;
passedGoalThreshold = false;
resetPuck();
}
return;
}
// Mute button
let btnSize = canvasWidth * 0.08;
let btnX = width - btnSize * 0.7;
let btnY = btnSize * 0.7;
if (
mouseX > btnX - btnSize / 2 &&
mouseX < btnX + btnSize / 2 &&
mouseY > btnY - btnSize / 2 &&
mouseY < btnY + btnSize / 2
) {
isMuted = !isMuted;
}
}
function drawGameOverScreen() {
background(30, 220); // semi-transparent overlay
let winnerY = height * 0.22;
let rematchY = height - arenaMargin - canvasWidth * 0.12 * 1.2;
let scoreY = (winnerY + rematchY) / 2;
let winner, winnerColor;
if (playerScore >= WINNING_SCORE) {
winner = "You Win!";
winnerColor = color(50, 150, 255); // blue
} else {
winner = "Computer Wins!";
winnerColor = color(227, 66, 52); // red
}
textAlign(CENTER, CENTER);
textSize(canvasWidth * 0.12);
fill(winnerColor);
textStyle(BOLD);
text(winner, width / 2, winnerY);
// Final score
textSize(canvasWidth * 0.16);
fill(50, 150, 255);
text(`${playerScore}`, width / 2 - canvasWidth * 0.13, scoreY);
fill(255);
text(` - `, width / 2, scoreY);
fill(227, 66, 52);
text(`${opponentScore}`, width / 2 + canvasWidth * 0.13, scoreY);
// Rematch button
let btnW = canvasWidth * 0.38;
let btnH = canvasWidth * 0.12;
let btnX = width / 2;
let btnY = rematchY;
let btnRadius = btnH * 0.35;
fill(40);
stroke(80);
strokeWeight(canvasWidth * 0.005);
rectMode(CENTER);
rect(btnX, btnY, btnW, btnH, btnRadius);
noStroke();
fill(230);
textSize(btnH * 0.5);
textAlign(CENTER, CENTER);
text("Rematch", btnX, btnY);
rectMode(CORNER); // Restore default
}
This Pen doesn't use any external CSS resources.