/****
* 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