/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Audience class var Audience = Container.expand(function () { var self = Container.call(this); // Use a random spectator image from 10 assets var audienceAssetIds = ['audience_0x4dd0e1', 'audience_0x90caf9', 'audience_0x9575cd', 'audience_0xa5d6a7', 'audience_0xc5e1a5', 'audience_0xf48fb1', 'audience_0xff7043', 'audience_0xff8a65', 'audience_0xffb300', 'audience_0xffe082']; var assetId = audienceAssetIds[Math.floor(Math.random() * audienceAssetIds.length)]; var audienceSprite = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); return self; }); // Ball class var Ball = Container.expand(function () { var self = Container.call(this); var ballSprite = self.attachAsset('ball', { anchorX: 0.5, anchorY: 0.5 }); // Ball physics self.vx = 0; self.vy = 0; self.friction = 1; // Friction for instant stopping self.update = function () { // Move ball // Store lastX/lastY for physics and event triggers if (typeof self.lastX === "undefined") self.lastX = self.x; if (typeof self.lastY === "undefined") self.lastY = self.y; self.lastX = self.x; self.lastY = self.y; self.x += self.vx; self.y += self.vy; // Friction self.vx *= self.friction; self.vy *= self.friction; // Clamp very small velocities to zero if (Math.abs(self.vx) < 0.1) self.vx = 0; if (Math.abs(self.vy) < 0.1) self.vy = 0; // Bounce off field boundaries // Left/right if (self.x - ballSprite.width / 2 < fieldLeft) { self.x = fieldLeft + ballSprite.width / 2; self.vx = -self.vx * 0.7; } if (self.x + ballSprite.width / 2 > fieldRight) { self.x = fieldRight - ballSprite.width / 2; self.vx = -self.vx * 0.7; } // Top/bottom (except inside goal area) if (self.y - ballSprite.height / 2 < fieldTop) { self.y = fieldTop + ballSprite.height / 2; self.vy = -self.vy * 0.7; } if (self.y + ballSprite.height / 2 > fieldBottom) { self.y = fieldBottom - ballSprite.height / 2; self.vy = -self.vy * 0.7; } }; return self; }); // Player class var Player = Container.expand(function () { var self = Container.call(this); // Set in init self.isAI = false; self.playerNum = 1; // 1 or 2 // Attach asset var assetId = self.playerNum === 1 ? 'player1' : 'player2'; var playerSprite = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // For drag self.isDragging = false; // For AI self.targetX = 0; self.targetY = 0; // For collision self.radius = playerSprite.width / 2; // For touch events (only for player 1) self.down = function (x, y, obj) { if (!self.isAI) { self.isDragging = true; } }; self.up = function (x, y, obj) { if (!self.isAI) { self.isDragging = false; } }; self.update = function () { // AI movement if (self.isAI) { // Find nearest ball var nearestBall = null; var minDist = Infinity; for (var i = 0; i < balls.length; i++) { var bx = balls[i].x; var by = balls[i].y; var d = Math.sqrt((self.x - bx) * (self.x - bx) + (self.y - by) * (self.y - by)); if (d < minDist) { minDist = d; nearestBall = balls[i]; } } // If no balls, just stay at home var target = { x: self.homeX, y: self.homeY }; if (nearestBall) { target.x = nearestBall.x; target.y = nearestBall.y; // Only chase if ball is on own half var ownHalf = self.playerNum === 2 ? nearestBall.y > fieldCenterY : nearestBall.y < fieldCenterY; if (!ownHalf) { // Stay at home position target.x = self.homeX; target.y = self.homeY; } } // Move towards target var dx = target.x - self.x; var dy = target.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); var speed = 18; if (dist > 10) { self.x += dx / dist * speed; self.y += dy / dist * speed; } } // Clamp inside field if (self.x - self.radius < fieldLeft) self.x = fieldLeft + self.radius; if (self.x + self.radius > fieldRight) self.x = fieldRight - self.radius; if (self.y - self.radius < fieldTop) self.y = fieldTop + self.radius; if (self.y + self.radius > fieldBottom) self.y = fieldBottom - self.radius; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x1b5e20 }); /**** * Game Code ****/ // 10 unique spectator assets (colored ellipses) // Thrown object by spectators (tomato) // Field dimensions // Football field (green rectangle) // Ball (white circle) // Player 1 (blue circle) // Player 2 (red circle) // Goal area (yellow rectangle) // Sound for goal var fieldWidth = 1800; var fieldHeight = 2600; var fieldLeft = (2048 - fieldWidth) / 2; var fieldTop = (2732 - fieldHeight) / 2; var fieldRight = fieldLeft + fieldWidth; var fieldBottom = fieldTop + fieldHeight; var fieldCenterX = 2048 / 2; var fieldCenterY = 2732 / 2; // Add field var field = LK.getAsset('field', { anchorX: 0, anchorY: 0, x: fieldLeft, y: fieldTop }); game.addChild(field); // Add goals // Use actual goal asset size for width/height var goalSprite = LK.getAsset('goal', { anchorX: 0.5, anchorY: 0.5 }); var goalWidth = goalSprite.width; var goalHeight = goalSprite.height; // Place top goal (player 2's goal) var goal1 = LK.getAsset('goal', { anchorX: 0.5, anchorY: 0, x: fieldCenterX, y: fieldTop - goalHeight / 2 + 10 // Place so bottom edge is just inside field }); // Place bottom goal (player 1's goal) var goal2 = LK.getAsset('goal', { anchorX: 0.5, anchorY: 1, x: fieldCenterX, y: fieldBottom + goalHeight / 2 - 10 // Place so top edge is just inside field }); game.addChild(goal1); game.addChild(goal2); // Add audience members to the field var audienceLeft = []; var audienceRight = []; // Place audience members at random Y positions within the field, never the same each play function getRandomAudienceY(existingYs) { // Avoid overlap by keeping at least 120px apart var minY = fieldTop + 120; var maxY = fieldBottom - 120; var tries = 0; while (tries < 30) { var y = Math.floor(Math.random() * (maxY - minY)) + minY; var tooClose = false; for (var i = 0; i < existingYs.length; i++) { if (Math.abs(existingYs[i] - y) < 120) { tooClose = true; break; } } if (!tooClose) return y; tries++; } // fallback: just return a random y return Math.floor(Math.random() * (maxY - minY)) + minY; } var leftYs = []; var rightYs = []; for (var i = 0; i < 3; i++) { var audL = new Audience(); var yL = getRandomAudienceY(leftYs); audL.x = fieldLeft - 80; audL.y = yL; leftYs.push(yL); audienceLeft.push(audL); game.addChild(audL); var audR = new Audience(); var yR = getRandomAudienceY(rightYs); audR.x = fieldRight + 80; audR.y = yR; rightYs.push(yR); audienceRight.push(audR); game.addChild(audR); } // Add balls array and spawn function var balls = []; function spawnBall() { if (balls.length >= 10) return; var newBall = new Ball(); // Place ball at center, but match its sprite's anchor and size var ballSprite = LK.getAsset('ball', { anchorX: 0.5, anchorY: 0.5 }); newBall.x = fieldCenterX; newBall.y = fieldCenterY; // Set velocity to zero newBall.vx = 0; newBall.vy = 0; // Initialize lastX/lastY for correct physics newBall.lastX = newBall.x; newBall.lastY = newBall.y; balls.push(newBall); game.addChild(newBall); } // Initial ball spawnBall(); // Ball spawn timer var ballSpawnTimer = LK.setInterval(function () { spawnBall(); }, 7000); // Add players var player2 = new Player(); player2.playerNum = 2; player2.isAI = true; player2.x = fieldCenterX; player2.y = fieldTop + 400; player2.homeX = player2.x; player2.homeY = player2.y; game.addChild(player2); var player1 = new Player(); player1.playerNum = 1; player1.x = fieldCenterX; player1.y = fieldBottom - 400; player1.homeX = player1.x; player1.homeY = player1.y; game.addChild(player1); // Ball start position function resetBall(kickoffToPlayer) { for (var i = 0; i < balls.length; i++) { balls[i].x = fieldCenterX; balls[i].y = fieldCenterY; balls[i].vx = 0; balls[i].vy = 0; // Give a little nudge towards the player who conceded if (kickoffToPlayer === 1) { balls[i].vy = -18; } else if (kickoffToPlayer === 2) { balls[i].vy = 18; } } } // Score var score1 = 0; var score2 = 0; // Score display var scoreTxt = new Text2('0 : 0', { size: 120, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Timer var gameTime = 60; // seconds var timeLeft = gameTime; var timerTxt = new Text2('01:00', { size: 80, fill: "#fff" }); timerTxt.anchor.set(0.5, 0); LK.gui.top.addChild(timerTxt); timerTxt.y = 130; // Timer interval var timerInterval = LK.setInterval(function () { timeLeft--; if (timeLeft < 0) timeLeft = 0; var min = Math.floor(timeLeft / 60); var sec = timeLeft % 60; var secStr = sec < 10 ? '0' + sec : '' + sec; timerTxt.setText('0' + min + ':' + secStr); if (timeLeft === 0) { // End game if (score1 > score2) { LK.showYouWin(); } else if (score2 > score1) { LK.showGameOver(); } else { // Draw: treat as loss for now LK.showGameOver(); } } }, 1000); // Dragging var dragNode = null; function handleMove(x, y, obj) { if (dragNode && !dragNode.isAI) { // Clamp to field var r = dragNode.radius; var nx = x; var ny = y; if (nx - r < fieldLeft) nx = fieldLeft + r; if (nx + r > fieldRight) nx = fieldRight - r; if (ny - r < fieldTop) ny = fieldTop + r; if (ny + r > fieldBottom) ny = fieldBottom - r; dragNode.x = nx; dragNode.y = ny; } } game.move = handleMove; game.down = function (x, y, obj) { // Only allow drag if touch is inside player1 var dx = x - player1.x; var dy = y - player1.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < player1.radius) { dragNode = player1; player1.isDragging = true; handleMove(x, y, obj); } }; game.up = function (x, y, obj) { if (dragNode) { dragNode.isDragging = false; dragNode = null; } }; // Helper: circle-circle collision function circlesCollide(ax, ay, ar, bx, by, br) { var dx = ax - bx; var dy = ay - by; var dist = Math.sqrt(dx * dx + dy * dy); return dist < ar + br - 2; } // Helper: reflect ball off player (now supports passing ball as argument) function reflectBallFromPlayer(player, b) { if (!b) b = ball; var dx = b.x - player.x; var dy = b.y - player.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist === 0) return; // Place ball just outside player var overlap = player.radius + 45 - dist; if (overlap > 0) { b.x += dx / dist * overlap; b.y += dy / dist * overlap; } // Ball velocity: add a kick var kickPower = 22; var normx = dx / dist; var normy = dy / dist; // If player is moving (drag), add some extra var extraVx = 0, extraVy = 0; if (!player.isAI && player.isDragging && dragNode === player) { // Use move delta (not available, so skip for now) } // Set ball velocity b.vx = normx * kickPower + extraVx; b.vy = normy * kickPower + extraVy; } // Helper: check goal (now supports passing ball and index) function checkGoal(b, idx) { if (!b) b = ball; // Top goal (player 2's goal) if (b.y - 45 < goal1.y + goalHeight // Ball top edge crosses goal bottom edge && b.y + 45 > goal1.y // Ball bottom edge crosses goal top edge && b.x > goal1.x - goalWidth / 2 && b.x < goal1.x + goalWidth / 2) { // Player 1 scores score1++; scoreTxt.setText(score1 + ' : ' + score2); LK.getSound('goal').play(); LK.effects.flashScreen(0x1976d2, 600); // Remove ball and respawn if needed if (typeof idx === "number") { b.destroy(); balls.splice(idx, 1); // Always keep at least 1 ball if (balls.length === 0) spawnBall(); } // Win condition if (score1 >= 5) { LK.showYouWin(); } return; } // Bottom goal (player 1's goal) if (b.y + 45 > goal2.y - goalHeight // Ball bottom edge crosses goal top edge && b.y - 45 < goal2.y // Ball top edge crosses goal bottom edge && b.x > goal2.x - goalWidth / 2 && b.x < goal2.x + goalWidth / 2) { // Player 2 scores score2++; scoreTxt.setText(score1 + ' : ' + score2); LK.getSound('goal').play(); LK.effects.flashScreen(0xd32f2f, 600); if (typeof idx === "number") { b.destroy(); balls.splice(idx, 1); if (balls.length === 0) spawnBall(); } if (score2 >= 5) { LK.showGameOver(); } return; } } // Main update loop game.update = function () { // Update players player1.update(); player2.update(); // Update all balls for (var i = balls.length - 1; i >= 0; i--) { var b = balls[i]; b.update(); // Player-ball collisions if (circlesCollide(player1.x, player1.y, player1.radius, b.x, b.y, 45)) { reflectBallFromPlayer(player1, b); } if (circlesCollide(player2.x, player2.y, player2.radius, b.x, b.y, 45)) { reflectBallFromPlayer(player2, b); } } // Ball-ball collision and response for (var i = 0; i < balls.length; i++) { var a = balls[i]; for (var j = i + 1; j < balls.length; j++) { var b = balls[j]; // Both balls have radius 45 var dx = a.x - b.x; var dy = a.y - b.y; var dist = Math.sqrt(dx * dx + dy * dy); var minDist = 45 + 45 - 2; if (dist < minDist && dist > 0) { // Move balls apart var overlap = minDist - dist; var nx = dx / dist; var ny = dy / dist; a.x += nx * overlap / 2; a.y += ny * overlap / 2; b.x -= nx * overlap / 2; b.y -= ny * overlap / 2; // Exchange velocities (simple elastic collision) // Project velocities onto collision normal var va = a.vx * nx + a.vy * ny; var vb = b.vx * nx + b.vy * ny; // Swap the normal components var vaNew = vb; var vbNew = va; // Tangential components stay the same var vat = -a.vx * ny + a.vy * nx; var vbt = -b.vx * ny + b.vy * nx; // Recompose velocities a.vx = vaNew * nx - vat * ny; a.vy = vaNew * ny + vat * nx; b.vx = vbNew * nx - vbt * ny; b.vy = vbNew * ny + vbt * nx; } } } // Prevent players from overlapping var dx = player1.x - player2.x; var dy = player1.y - player2.y; var dist = Math.sqrt(dx * dx + dy * dy); var minDist = player1.radius + player2.radius - 8; if (dist < minDist && dist > 0) { var overlap = minDist - dist; var nx = dx / dist; var ny = dy / dist; // Push both away from each other player1.x += nx * overlap / 2; player1.y += ny * overlap / 2; player2.x -= nx * overlap / 2; player2.y -= ny * overlap / 2; } // Check for goals for all balls for (var i = balls.length - 1; i >= 0; i--) { checkGoal(balls[i], i); } // --- Stuck detection and reset --- // Consider stuck if all balls and both players have not moved significantly for 2 seconds if (typeof stuckState === "undefined") { stuckState = { lastPositions: [], lastPlayer1: { x: player1.x, y: player1.y }, lastPlayer2: { x: player2.x, y: player2.y }, lastCheck: Date.now(), stuckTime: 0 }; } var allStill = true; for (var i = 0; i < balls.length; i++) { var b = balls[i]; if (!stuckState.lastPositions[i]) stuckState.lastPositions[i] = { x: b.x, y: b.y }; var dx = b.x - stuckState.lastPositions[i].x; var dy = b.y - stuckState.lastPositions[i].y; if (Math.abs(dx) > 2 || Math.abs(dy) > 2) { allStill = false; } } var p1dx = player1.x - stuckState.lastPlayer1.x; var p1dy = player1.y - stuckState.lastPlayer1.y; var p2dx = player2.x - stuckState.lastPlayer2.x; var p2dy = player2.y - stuckState.lastPlayer2.y; if (Math.abs(p1dx) > 2 || Math.abs(p1dy) > 2 || Math.abs(p2dx) > 2 || Math.abs(p2dy) > 2) { allStill = false; } var now = Date.now(); if (allStill) { stuckState.stuckTime += now - stuckState.lastCheck; if (stuckState.stuckTime > 2000) { // Reset all balls to center resetBall(); stuckState.stuckTime = 0; // Also flash screen to indicate reset LK.effects.flashScreen(0xff9800, 500); } } else { stuckState.stuckTime = 0; } // Update last positions and time for (var i = 0; i < balls.length; i++) { if (!stuckState.lastPositions[i]) stuckState.lastPositions[i] = { x: balls[i].x, y: balls[i].y }; stuckState.lastPositions[i].x = balls[i].x; stuckState.lastPositions[i].y = balls[i].y; } stuckState.lastPlayer1.x = player1.x; stuckState.lastPlayer1.y = player1.y; stuckState.lastPlayer2.x = player2.x; stuckState.lastPlayer2.y = player2.y; stuckState.lastCheck = now; }; // Start positions resetBall(1); // Play field music (if any) - not required per guidelines // Clean up timer on game over/win LK.on('gameover', function () { LK.clearInterval(timerInterval); LK.clearInterval(ballSpawnTimer); if (typeof tomatoThrowTimer !== "undefined") LK.clearInterval(tomatoThrowTimer); }); LK.on('youwin', function () { LK.clearInterval(timerInterval); LK.clearInterval(ballSpawnTimer); if (typeof tomatoThrowTimer !== "undefined") LK.clearInterval(tomatoThrowTimer); });
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Audience class
var Audience = Container.expand(function () {
var self = Container.call(this);
// Use a random spectator image from 10 assets
var audienceAssetIds = ['audience_0x4dd0e1', 'audience_0x90caf9', 'audience_0x9575cd', 'audience_0xa5d6a7', 'audience_0xc5e1a5', 'audience_0xf48fb1', 'audience_0xff7043', 'audience_0xff8a65', 'audience_0xffb300', 'audience_0xffe082'];
var assetId = audienceAssetIds[Math.floor(Math.random() * audienceAssetIds.length)];
var audienceSprite = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
// Ball class
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballSprite = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
// Ball physics
self.vx = 0;
self.vy = 0;
self.friction = 1; // Friction for instant stopping
self.update = function () {
// Move ball
// Store lastX/lastY for physics and event triggers
if (typeof self.lastX === "undefined") self.lastX = self.x;
if (typeof self.lastY === "undefined") self.lastY = self.y;
self.lastX = self.x;
self.lastY = self.y;
self.x += self.vx;
self.y += self.vy;
// Friction
self.vx *= self.friction;
self.vy *= self.friction;
// Clamp very small velocities to zero
if (Math.abs(self.vx) < 0.1) self.vx = 0;
if (Math.abs(self.vy) < 0.1) self.vy = 0;
// Bounce off field boundaries
// Left/right
if (self.x - ballSprite.width / 2 < fieldLeft) {
self.x = fieldLeft + ballSprite.width / 2;
self.vx = -self.vx * 0.7;
}
if (self.x + ballSprite.width / 2 > fieldRight) {
self.x = fieldRight - ballSprite.width / 2;
self.vx = -self.vx * 0.7;
}
// Top/bottom (except inside goal area)
if (self.y - ballSprite.height / 2 < fieldTop) {
self.y = fieldTop + ballSprite.height / 2;
self.vy = -self.vy * 0.7;
}
if (self.y + ballSprite.height / 2 > fieldBottom) {
self.y = fieldBottom - ballSprite.height / 2;
self.vy = -self.vy * 0.7;
}
};
return self;
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
// Set in init
self.isAI = false;
self.playerNum = 1; // 1 or 2
// Attach asset
var assetId = self.playerNum === 1 ? 'player1' : 'player2';
var playerSprite = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// For drag
self.isDragging = false;
// For AI
self.targetX = 0;
self.targetY = 0;
// For collision
self.radius = playerSprite.width / 2;
// For touch events (only for player 1)
self.down = function (x, y, obj) {
if (!self.isAI) {
self.isDragging = true;
}
};
self.up = function (x, y, obj) {
if (!self.isAI) {
self.isDragging = false;
}
};
self.update = function () {
// AI movement
if (self.isAI) {
// Find nearest ball
var nearestBall = null;
var minDist = Infinity;
for (var i = 0; i < balls.length; i++) {
var bx = balls[i].x;
var by = balls[i].y;
var d = Math.sqrt((self.x - bx) * (self.x - bx) + (self.y - by) * (self.y - by));
if (d < minDist) {
minDist = d;
nearestBall = balls[i];
}
}
// If no balls, just stay at home
var target = {
x: self.homeX,
y: self.homeY
};
if (nearestBall) {
target.x = nearestBall.x;
target.y = nearestBall.y;
// Only chase if ball is on own half
var ownHalf = self.playerNum === 2 ? nearestBall.y > fieldCenterY : nearestBall.y < fieldCenterY;
if (!ownHalf) {
// Stay at home position
target.x = self.homeX;
target.y = self.homeY;
}
}
// Move towards target
var dx = target.x - self.x;
var dy = target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var speed = 18;
if (dist > 10) {
self.x += dx / dist * speed;
self.y += dy / dist * speed;
}
}
// Clamp inside field
if (self.x - self.radius < fieldLeft) self.x = fieldLeft + self.radius;
if (self.x + self.radius > fieldRight) self.x = fieldRight - self.radius;
if (self.y - self.radius < fieldTop) self.y = fieldTop + self.radius;
if (self.y + self.radius > fieldBottom) self.y = fieldBottom - self.radius;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1b5e20
});
/****
* Game Code
****/
// 10 unique spectator assets (colored ellipses)
// Thrown object by spectators (tomato)
// Field dimensions
// Football field (green rectangle)
// Ball (white circle)
// Player 1 (blue circle)
// Player 2 (red circle)
// Goal area (yellow rectangle)
// Sound for goal
var fieldWidth = 1800;
var fieldHeight = 2600;
var fieldLeft = (2048 - fieldWidth) / 2;
var fieldTop = (2732 - fieldHeight) / 2;
var fieldRight = fieldLeft + fieldWidth;
var fieldBottom = fieldTop + fieldHeight;
var fieldCenterX = 2048 / 2;
var fieldCenterY = 2732 / 2;
// Add field
var field = LK.getAsset('field', {
anchorX: 0,
anchorY: 0,
x: fieldLeft,
y: fieldTop
});
game.addChild(field);
// Add goals
// Use actual goal asset size for width/height
var goalSprite = LK.getAsset('goal', {
anchorX: 0.5,
anchorY: 0.5
});
var goalWidth = goalSprite.width;
var goalHeight = goalSprite.height;
// Place top goal (player 2's goal)
var goal1 = LK.getAsset('goal', {
anchorX: 0.5,
anchorY: 0,
x: fieldCenterX,
y: fieldTop - goalHeight / 2 + 10 // Place so bottom edge is just inside field
});
// Place bottom goal (player 1's goal)
var goal2 = LK.getAsset('goal', {
anchorX: 0.5,
anchorY: 1,
x: fieldCenterX,
y: fieldBottom + goalHeight / 2 - 10 // Place so top edge is just inside field
});
game.addChild(goal1);
game.addChild(goal2);
// Add audience members to the field
var audienceLeft = [];
var audienceRight = [];
// Place audience members at random Y positions within the field, never the same each play
function getRandomAudienceY(existingYs) {
// Avoid overlap by keeping at least 120px apart
var minY = fieldTop + 120;
var maxY = fieldBottom - 120;
var tries = 0;
while (tries < 30) {
var y = Math.floor(Math.random() * (maxY - minY)) + minY;
var tooClose = false;
for (var i = 0; i < existingYs.length; i++) {
if (Math.abs(existingYs[i] - y) < 120) {
tooClose = true;
break;
}
}
if (!tooClose) return y;
tries++;
}
// fallback: just return a random y
return Math.floor(Math.random() * (maxY - minY)) + minY;
}
var leftYs = [];
var rightYs = [];
for (var i = 0; i < 3; i++) {
var audL = new Audience();
var yL = getRandomAudienceY(leftYs);
audL.x = fieldLeft - 80;
audL.y = yL;
leftYs.push(yL);
audienceLeft.push(audL);
game.addChild(audL);
var audR = new Audience();
var yR = getRandomAudienceY(rightYs);
audR.x = fieldRight + 80;
audR.y = yR;
rightYs.push(yR);
audienceRight.push(audR);
game.addChild(audR);
}
// Add balls array and spawn function
var balls = [];
function spawnBall() {
if (balls.length >= 10) return;
var newBall = new Ball();
// Place ball at center, but match its sprite's anchor and size
var ballSprite = LK.getAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
newBall.x = fieldCenterX;
newBall.y = fieldCenterY;
// Set velocity to zero
newBall.vx = 0;
newBall.vy = 0;
// Initialize lastX/lastY for correct physics
newBall.lastX = newBall.x;
newBall.lastY = newBall.y;
balls.push(newBall);
game.addChild(newBall);
}
// Initial ball
spawnBall();
// Ball spawn timer
var ballSpawnTimer = LK.setInterval(function () {
spawnBall();
}, 7000);
// Add players
var player2 = new Player();
player2.playerNum = 2;
player2.isAI = true;
player2.x = fieldCenterX;
player2.y = fieldTop + 400;
player2.homeX = player2.x;
player2.homeY = player2.y;
game.addChild(player2);
var player1 = new Player();
player1.playerNum = 1;
player1.x = fieldCenterX;
player1.y = fieldBottom - 400;
player1.homeX = player1.x;
player1.homeY = player1.y;
game.addChild(player1);
// Ball start position
function resetBall(kickoffToPlayer) {
for (var i = 0; i < balls.length; i++) {
balls[i].x = fieldCenterX;
balls[i].y = fieldCenterY;
balls[i].vx = 0;
balls[i].vy = 0;
// Give a little nudge towards the player who conceded
if (kickoffToPlayer === 1) {
balls[i].vy = -18;
} else if (kickoffToPlayer === 2) {
balls[i].vy = 18;
}
}
}
// Score
var score1 = 0;
var score2 = 0;
// Score display
var scoreTxt = new Text2('0 : 0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Timer
var gameTime = 60; // seconds
var timeLeft = gameTime;
var timerTxt = new Text2('01:00', {
size: 80,
fill: "#fff"
});
timerTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(timerTxt);
timerTxt.y = 130;
// Timer interval
var timerInterval = LK.setInterval(function () {
timeLeft--;
if (timeLeft < 0) timeLeft = 0;
var min = Math.floor(timeLeft / 60);
var sec = timeLeft % 60;
var secStr = sec < 10 ? '0' + sec : '' + sec;
timerTxt.setText('0' + min + ':' + secStr);
if (timeLeft === 0) {
// End game
if (score1 > score2) {
LK.showYouWin();
} else if (score2 > score1) {
LK.showGameOver();
} else {
// Draw: treat as loss for now
LK.showGameOver();
}
}
}, 1000);
// Dragging
var dragNode = null;
function handleMove(x, y, obj) {
if (dragNode && !dragNode.isAI) {
// Clamp to field
var r = dragNode.radius;
var nx = x;
var ny = y;
if (nx - r < fieldLeft) nx = fieldLeft + r;
if (nx + r > fieldRight) nx = fieldRight - r;
if (ny - r < fieldTop) ny = fieldTop + r;
if (ny + r > fieldBottom) ny = fieldBottom - r;
dragNode.x = nx;
dragNode.y = ny;
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
// Only allow drag if touch is inside player1
var dx = x - player1.x;
var dy = y - player1.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < player1.radius) {
dragNode = player1;
player1.isDragging = true;
handleMove(x, y, obj);
}
};
game.up = function (x, y, obj) {
if (dragNode) {
dragNode.isDragging = false;
dragNode = null;
}
};
// Helper: circle-circle collision
function circlesCollide(ax, ay, ar, bx, by, br) {
var dx = ax - bx;
var dy = ay - by;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < ar + br - 2;
}
// Helper: reflect ball off player (now supports passing ball as argument)
function reflectBallFromPlayer(player, b) {
if (!b) b = ball;
var dx = b.x - player.x;
var dy = b.y - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) return;
// Place ball just outside player
var overlap = player.radius + 45 - dist;
if (overlap > 0) {
b.x += dx / dist * overlap;
b.y += dy / dist * overlap;
}
// Ball velocity: add a kick
var kickPower = 22;
var normx = dx / dist;
var normy = dy / dist;
// If player is moving (drag), add some extra
var extraVx = 0,
extraVy = 0;
if (!player.isAI && player.isDragging && dragNode === player) {
// Use move delta (not available, so skip for now)
}
// Set ball velocity
b.vx = normx * kickPower + extraVx;
b.vy = normy * kickPower + extraVy;
}
// Helper: check goal (now supports passing ball and index)
function checkGoal(b, idx) {
if (!b) b = ball;
// Top goal (player 2's goal)
if (b.y - 45 < goal1.y + goalHeight // Ball top edge crosses goal bottom edge
&& b.y + 45 > goal1.y // Ball bottom edge crosses goal top edge
&& b.x > goal1.x - goalWidth / 2 && b.x < goal1.x + goalWidth / 2) {
// Player 1 scores
score1++;
scoreTxt.setText(score1 + ' : ' + score2);
LK.getSound('goal').play();
LK.effects.flashScreen(0x1976d2, 600);
// Remove ball and respawn if needed
if (typeof idx === "number") {
b.destroy();
balls.splice(idx, 1);
// Always keep at least 1 ball
if (balls.length === 0) spawnBall();
}
// Win condition
if (score1 >= 5) {
LK.showYouWin();
}
return;
}
// Bottom goal (player 1's goal)
if (b.y + 45 > goal2.y - goalHeight // Ball bottom edge crosses goal top edge
&& b.y - 45 < goal2.y // Ball top edge crosses goal bottom edge
&& b.x > goal2.x - goalWidth / 2 && b.x < goal2.x + goalWidth / 2) {
// Player 2 scores
score2++;
scoreTxt.setText(score1 + ' : ' + score2);
LK.getSound('goal').play();
LK.effects.flashScreen(0xd32f2f, 600);
if (typeof idx === "number") {
b.destroy();
balls.splice(idx, 1);
if (balls.length === 0) spawnBall();
}
if (score2 >= 5) {
LK.showGameOver();
}
return;
}
}
// Main update loop
game.update = function () {
// Update players
player1.update();
player2.update();
// Update all balls
for (var i = balls.length - 1; i >= 0; i--) {
var b = balls[i];
b.update();
// Player-ball collisions
if (circlesCollide(player1.x, player1.y, player1.radius, b.x, b.y, 45)) {
reflectBallFromPlayer(player1, b);
}
if (circlesCollide(player2.x, player2.y, player2.radius, b.x, b.y, 45)) {
reflectBallFromPlayer(player2, b);
}
}
// Ball-ball collision and response
for (var i = 0; i < balls.length; i++) {
var a = balls[i];
for (var j = i + 1; j < balls.length; j++) {
var b = balls[j];
// Both balls have radius 45
var dx = a.x - b.x;
var dy = a.y - b.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var minDist = 45 + 45 - 2;
if (dist < minDist && dist > 0) {
// Move balls apart
var overlap = minDist - dist;
var nx = dx / dist;
var ny = dy / dist;
a.x += nx * overlap / 2;
a.y += ny * overlap / 2;
b.x -= nx * overlap / 2;
b.y -= ny * overlap / 2;
// Exchange velocities (simple elastic collision)
// Project velocities onto collision normal
var va = a.vx * nx + a.vy * ny;
var vb = b.vx * nx + b.vy * ny;
// Swap the normal components
var vaNew = vb;
var vbNew = va;
// Tangential components stay the same
var vat = -a.vx * ny + a.vy * nx;
var vbt = -b.vx * ny + b.vy * nx;
// Recompose velocities
a.vx = vaNew * nx - vat * ny;
a.vy = vaNew * ny + vat * nx;
b.vx = vbNew * nx - vbt * ny;
b.vy = vbNew * ny + vbt * nx;
}
}
}
// Prevent players from overlapping
var dx = player1.x - player2.x;
var dy = player1.y - player2.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var minDist = player1.radius + player2.radius - 8;
if (dist < minDist && dist > 0) {
var overlap = minDist - dist;
var nx = dx / dist;
var ny = dy / dist;
// Push both away from each other
player1.x += nx * overlap / 2;
player1.y += ny * overlap / 2;
player2.x -= nx * overlap / 2;
player2.y -= ny * overlap / 2;
}
// Check for goals for all balls
for (var i = balls.length - 1; i >= 0; i--) {
checkGoal(balls[i], i);
}
// --- Stuck detection and reset ---
// Consider stuck if all balls and both players have not moved significantly for 2 seconds
if (typeof stuckState === "undefined") {
stuckState = {
lastPositions: [],
lastPlayer1: {
x: player1.x,
y: player1.y
},
lastPlayer2: {
x: player2.x,
y: player2.y
},
lastCheck: Date.now(),
stuckTime: 0
};
}
var allStill = true;
for (var i = 0; i < balls.length; i++) {
var b = balls[i];
if (!stuckState.lastPositions[i]) stuckState.lastPositions[i] = {
x: b.x,
y: b.y
};
var dx = b.x - stuckState.lastPositions[i].x;
var dy = b.y - stuckState.lastPositions[i].y;
if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
allStill = false;
}
}
var p1dx = player1.x - stuckState.lastPlayer1.x;
var p1dy = player1.y - stuckState.lastPlayer1.y;
var p2dx = player2.x - stuckState.lastPlayer2.x;
var p2dy = player2.y - stuckState.lastPlayer2.y;
if (Math.abs(p1dx) > 2 || Math.abs(p1dy) > 2 || Math.abs(p2dx) > 2 || Math.abs(p2dy) > 2) {
allStill = false;
}
var now = Date.now();
if (allStill) {
stuckState.stuckTime += now - stuckState.lastCheck;
if (stuckState.stuckTime > 2000) {
// Reset all balls to center
resetBall();
stuckState.stuckTime = 0;
// Also flash screen to indicate reset
LK.effects.flashScreen(0xff9800, 500);
}
} else {
stuckState.stuckTime = 0;
}
// Update last positions and time
for (var i = 0; i < balls.length; i++) {
if (!stuckState.lastPositions[i]) stuckState.lastPositions[i] = {
x: balls[i].x,
y: balls[i].y
};
stuckState.lastPositions[i].x = balls[i].x;
stuckState.lastPositions[i].y = balls[i].y;
}
stuckState.lastPlayer1.x = player1.x;
stuckState.lastPlayer1.y = player1.y;
stuckState.lastPlayer2.x = player2.x;
stuckState.lastPlayer2.y = player2.y;
stuckState.lastCheck = now;
};
// Start positions
resetBall(1);
// Play field music (if any) - not required per guidelines
// Clean up timer on game over/win
LK.on('gameover', function () {
LK.clearInterval(timerInterval);
LK.clearInterval(ballSpawnTimer);
if (typeof tomatoThrowTimer !== "undefined") LK.clearInterval(tomatoThrowTimer);
});
LK.on('youwin', function () {
LK.clearInterval(timerInterval);
LK.clearInterval(ballSpawnTimer);
if (typeof tomatoThrowTimer !== "undefined") LK.clearInterval(tomatoThrowTimer);
});
full round icy rock. In-Game asset
round ironstone colored. In-Game asset
winter forest top view, trees on the edges empty in the middle. In-Game asset. High contrast
top football goal. In-Game asset. 2d. High contrast. No shadows
tomato. In-Game asset. 2d. High contrast. No shadows
crab. In-Game asset. 2d. High contrast. No shadows
snail. In-Game asset. 2d. High contrast. No shadows
watermelon with face. In-Game asset. 2d. High contrast. No shadows
monkey. In-Game asset. 2d. High contrast. No shadows
tapir. In-Game asset. 2d. High contrast. No shadows
Chinchilla. In-Game asset. 2d. High contrast. No shadows
Marmot. In-Game asset. 2d. High contrast. No shadows
Duck. In-Game asset. 2d. High contrast. No shadows
chicken. In-Game asset. 2d. High contrast. No shadows