/**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x1976d2 // Blue field background }); /**** * Game Code ****/ ; // --- Timer --- var timerSeconds = 0; var timerRunning = true; var timerTxt = new Text2('0:00', { size: 90, fill: 0xffffff, font: "Impact, Arial Black, Tahoma" }); timerTxt.anchor.set(0.5, 1); timerTxt.x = LK.gui.width / 2; // Place timer at top center LK.gui.top.addChild(timerTxt); timerTxt.y = 0; ; // Track dragging state for both players by touchId var draggingPlayers = {}; // { touchId: player } // Convert global (event) coordinates to local game coordinates function toGameCoords(x, y) { // LK events provide coordinates in game space already return { x: x, y: y }; } // Handle touch/mouse down: start dragging if on player game.down = function (x, y, obj) { // Use obj.event.identifier for multi-touch, fallback to 0 for mouse var touchId = obj && obj.event && typeof obj.event.identifier !== "undefined" ? obj.event.identifier : 0; // Check if the down event is on the blue player var dx = x - player.x; var dy = y - player.y; var r = player.width / 2; if (dx * dx + dy * dy <= r * r) { draggingPlayers[touchId] = player; return; } // Check if the down event is on the red player (opponent) // (Red is AI controlled, so do not allow dragging) // var dx2 = x - opponent.x; // var dy2 = y - opponent.y; // var r2 = opponent.width / 2; // if (dx2 * dx2 + dy2 * dy2 <= r2 * r2) { // draggingPlayers[touchId] = opponent; // return; // } }; // Handle touch/mouse move: move player if dragging game.move = function (x, y, obj) { // Use obj.event.identifier for multi-touch, fallback to 0 for mouse var touchId = obj && obj.event && typeof obj.event.identifier !== "undefined" ? obj.event.identifier : 0; var draggingPlayer = draggingPlayers[touchId]; if (draggingPlayer) { if (draggingPlayer === player) { // Blue player (top side) var minX = field.x - field.width / 2 + player.width / 2; var maxX = field.x + field.width / 2 - player.width / 2; var minY = field.y - field.height / 2 + player.height / 2; var maxY = field.y; // Center line is the max for top player draggingPlayer.x = Math.max(minX, Math.min(maxX, x)); draggingPlayer.y = Math.max(minY, Math.min(maxY - player.height / 2, y)); } else if (draggingPlayer === opponent) { // Red player (bottom side) - AI controlled, do not allow dragging } } }; // Handle touch/mouse up: stop dragging game.up = function (x, y, obj) { // Use obj.event.identifier for multi-touch, fallback to 0 for mouse var touchId = obj && obj.event && typeof obj.event.identifier !== "undefined" ? obj.event.identifier : 0; delete draggingPlayers[touchId]; }; ; ; // Track last intersection state between player and ball var lastPlayerBallIntersecting = false; // Update loop to check for player touching ball game.update = function () { // --- Timer update --- if (timerRunning) { if (typeof game._lastTimerTick === "undefined") { game._lastTimerTick = Date.now(); } var now = Date.now(); if (now - game._lastTimerTick >= 1000) { timerSeconds += 1; game._lastTimerTick += 1000; // Format as M:SS var min = Math.floor(timerSeconds / 60); var sec = timerSeconds % 60; timerTxt.setText(min + ":" + (sec < 10 ? "0" : "") + sec); } } // --- AI logic: Move red (opponent) player toward the ball, sometimes randomly --- // Only move if timer is running (not after win/game over) if (timerRunning) { // AI target: try to stay on the same side as the ball (bottom half of field) // Only move if ball is on or below center line var aiTargetY = Math.max(field.y, ball.y); // Clamp AI movement to bottom half of field var minX = field.x - field.width / 2 + opponent.width / 2; var maxX = field.x + field.width / 2 - opponent.width / 2; var minY = field.y; var maxY = field.y + field.height / 2 - opponent.height / 2; // --- Random movement logic --- if (typeof opponent.aiRandomCooldown === "undefined") opponent.aiRandomCooldown = 0; if (typeof opponent.aiRandomTargetX === "undefined") opponent.aiRandomTargetX = opponent.x; if (typeof opponent.aiRandomTargetY === "undefined") opponent.aiRandomTargetY = opponent.y; if (typeof opponent.aiRandomActive === "undefined") opponent.aiRandomActive = false; // Occasionally (about every 1.5-3s), pick a random target in the AI's allowed area if (opponent.aiRandomCooldown <= 0 && Math.random() < 0.012) { opponent.aiRandomActive = true; // Pick a random point in the lower half of the field opponent.aiRandomTargetX = minX + Math.random() * (maxX - minX); opponent.aiRandomTargetY = minY + Math.random() * (maxY - minY); // Random movement lasts 30-60 frames opponent.aiRandomCooldown = 30 + Math.floor(Math.random() * 30); } // If random movement is active, move toward the random target if (opponent.aiRandomActive && opponent.aiRandomCooldown > 0) { var targetX = opponent.aiRandomTargetX; var targetY = opponent.aiRandomTargetY; opponent.aiRandomCooldown--; // If close to the random target, stop random movement var distToRandom = Math.sqrt((targetX - opponent.x) * (targetX - opponent.x) + (targetY - opponent.y) * (targetY - opponent.y)); if (distToRandom < 40 || opponent.aiRandomCooldown <= 0) { opponent.aiRandomActive = false; opponent.aiRandomCooldown = 60 + Math.floor(Math.random() * 60); // Wait before next random } } else { // Normal AI: follow the ball var targetX = Math.max(minX, Math.min(maxX, ball.x)); var targetY = Math.max(minY, Math.min(maxY, aiTargetY)); if (opponent.aiRandomCooldown > 0) { opponent.aiRandomCooldown--; } } // Move opponent toward target position with a max speed var aiSpeed = 32; // px per frame (tune for difficulty) var dx = targetX - opponent.x; var dy = targetY - opponent.y; var dist = Math.sqrt(dx * dx + dy * dy) || 1; if (dist > aiSpeed) { opponent.x += dx / dist * aiSpeed; opponent.y += dy / dist * aiSpeed; } else { opponent.x = targetX; opponent.y = targetY; } } // Check if player is touching the ball (collision/intersection) var isIntersecting = player.intersects(ball); // Detect the exact moment player starts touching the ball if (!lastPlayerBallIntersecting && isIntersecting) { // Calculate direction from player to ball var dx = ball.x - player.x; var dy = ball.y - player.y; var dist = Math.sqrt(dx * dx + dy * dy) || 1; // Give the ball a velocity away from the player (EVEN FASTER than before) ball.vx = dx / dist * 70; ball.vy = dy / dist * 70; LK.effects.flashObject(ball, 0x1976d2, 200); } // AI: Red player can also kick the ball if touching, from any side if (opponent.intersects(ball)) { // Check if ball is in a corner var minBallX = field.x - field.width / 2 + ball.width / 2; var maxBallX = field.x + field.width / 2 - ball.width / 2; var minBallY = field.y - field.height / 2 + ball.height / 2; var maxBallY = field.y + field.height / 2 - ball.height / 2; var atLeft = ball.x <= minBallX + 1; var atRight = ball.x >= maxBallX - 1; var atTop = ball.y <= minBallY + 1; var atBottom = ball.y >= maxBallY - 1; // --- Ball stuck detection --- if (typeof ball.stuckFrames === "undefined") ball.stuckFrames = 0; if (typeof ball.lastStuckX === "undefined") ball.lastStuckX = ball.x; if (typeof ball.lastStuckY === "undefined") ball.lastStuckY = ball.y; // If the ball hasn't moved much in 10 frames, consider it stuck var stuckThreshold = 6; if (Math.abs(ball.x - ball.lastStuckX) < stuckThreshold && Math.abs(ball.y - ball.lastStuckY) < stuckThreshold) { ball.stuckFrames++; } else { ball.stuckFrames = 0; ball.lastStuckX = ball.x; ball.lastStuckY = ball.y; } // If the ball is stuck for too long, red gives up and the game finishes if (ball.stuckFrames > 90) { // 90 frames ~1.5 seconds at 60fps timerRunning = false; LK.effects.flashScreen(0xff0000, 600); LK.showGameOver(); return; } var dxr, dyr; if ((atLeft || atRight) && (atTop || atBottom)) { // Ball is in a corner, AI tries to hit it toward the center of the field dxr = field.x - ball.x; dyr = field.y - ball.y; } else if (ball.stuckFrames > 12) { // Ball is stuck: AI tries to hit the right side of the ball // Always aim to the right (positive X direction) dxr = 1; dyr = 0; // Add a little bias toward the field center to avoid infinite right wall hits dxr += (field.x - ball.x) * 0.005; dyr += (field.y - ball.y) * 0.01; } else { // Normal: hit from AI to ball direction dxr = ball.x - opponent.x; dyr = ball.y - opponent.y; } var distr = Math.sqrt(dxr * dxr + dyr * dyr) || 1; ball.vx = dxr / distr * 50; ball.vy = dyr / distr * 50; LK.effects.flashObject(ball, 0xd32f2f, 200); } // Ball activation: Ball is stopped at start, but after first touch, it never stops and goes faster if (typeof ball.activated === "undefined") ball.activated = false; if (!ball.activated && (player.intersects(ball) || opponent.intersects(ball))) { ball.activated = true; // Give a small nudge in a random direction if ball is perfectly still if (ball.vx === 0 && ball.vy === 0) { var angle = Math.random() * Math.PI * 2; ball.vx = Math.cos(angle) * 18; ball.vy = Math.sin(angle) * 18; } } // Move the ball if it has velocity if (typeof ball.vx === "number" && typeof ball.vy === "number") { // --- Ball bounce off goal posts (horizontal) --- // Track last intersection for each post if (typeof ball.lastLeftGoalPostLeftIntersect === "undefined") ball.lastLeftGoalPostLeftIntersect = false; if (typeof ball.lastLeftGoalPostRightIntersect === "undefined") ball.lastLeftGoalPostRightIntersect = false; if (typeof ball.lastRightGoalPostLeftIntersect === "undefined") ball.lastRightGoalPostLeftIntersect = false; if (typeof ball.lastRightGoalPostRightIntersect === "undefined") ball.lastRightGoalPostRightIntersect = false; // Left goal post left var leftLeftNow = ball.intersects(leftGoalPostLeft); if (!ball.lastLeftGoalPostLeftIntersect && leftLeftNow) { // Bounce: reflect velocity, add a little randomness var dx = ball.x - (leftGoalPostLeft.x - leftGoalPostLeft.width / 2); var dy = ball.y - leftGoalPostLeft.y; var dist = Math.sqrt(dx * dx + dy * dy) || 1; ball.vx = dx / dist * Math.max(Math.abs(ball.vx), 30); ball.vy = dy / dist * Math.max(Math.abs(ball.vy), 30); LK.effects.flashObject(ball, 0xffff00, 120); } ball.lastLeftGoalPostLeftIntersect = leftLeftNow; // Left goal post right var leftRightNow = ball.intersects(leftGoalPostRight); if (!ball.lastLeftGoalPostRightIntersect && leftRightNow) { var dx = ball.x - (leftGoalPostRight.x + leftGoalPostRight.width / 2); var dy = ball.y - leftGoalPostRight.y; var dist = Math.sqrt(dx * dx + dy * dy) || 1; ball.vx = dx / dist * Math.max(Math.abs(ball.vx), 30); ball.vy = dy / dist * Math.max(Math.abs(ball.vy), 30); LK.effects.flashObject(ball, 0xffff00, 120); } ball.lastLeftGoalPostRightIntersect = leftRightNow; // Right goal post left var rightLeftNow = ball.intersects(rightGoalPostLeft); if (!ball.lastRightGoalPostLeftIntersect && rightLeftNow) { var dx = ball.x - (rightGoalPostLeft.x - rightGoalPostLeft.width / 2); var dy = ball.y - rightGoalPostLeft.y; var dist = Math.sqrt(dx * dx + dy * dy) || 1; ball.vx = dx / dist * Math.max(Math.abs(ball.vx), 30); ball.vy = dy / dist * Math.max(Math.abs(ball.vy), 30); LK.effects.flashObject(ball, 0xffff00, 120); } ball.lastRightGoalPostLeftIntersect = rightLeftNow; // Right goal post right var rightRightNow = ball.intersects(rightGoalPostRight); if (!ball.lastRightGoalPostRightIntersect && rightRightNow) { var dx = ball.x - (rightGoalPostRight.x + rightGoalPostRight.width / 2); var dy = ball.y - rightGoalPostRight.y; var dist = Math.sqrt(dx * dx + dy * dy) || 1; ball.vx = dx / dist * Math.max(Math.abs(ball.vx), 30); ball.vy = dy / dist * Math.max(Math.abs(ball.vy), 30); LK.effects.flashObject(ball, 0xffff00, 120); } ball.lastRightGoalPostRightIntersect = rightRightNow; // Track lastX for side detection if (typeof ball.lastX === "undefined") ball.lastX = ball.x; // Detect crossing from left to right side (player to opponent) var centerX = field.x; if (ball.lastX <= centerX && ball.x > centerX) { // Ball just crossed to right side, speed up! ball.vx *= 1.25; ball.vy *= 1.25; } // Detect crossing from right to left side (opponent to player) if (ball.lastX >= centerX && ball.x < centerX) { // Ball just crossed to left side, speed up! ball.vx *= 1.25; ball.vy *= 1.25; } if (ball.activated) { ball.x += ball.vx; ball.y += ball.vy; } // Friction ball.vx *= 0.96; ball.vy *= 0.96; // Clamp to field bounds and bounce off corners (horizontal) var minBallX = field.x - field.width / 2 + ball.width / 2; var maxBallX = field.x + field.width / 2 - ball.width / 2; var minBallY = field.y - field.height / 2 + ball.height / 2; var maxBallY = field.y + field.height / 2 - ball.height / 2; // Detect if ball is in a corner (within 1px of both X and Y bounds) var atLeft = ball.x <= minBallX + 1; var atRight = ball.x >= maxBallX - 1; var atTop = ball.y <= minBallY + 1; var atBottom = ball.y >= maxBallY - 1; // Bounce off corners: if at a corner, reflect both vx and vy if (atLeft && atTop || atLeft && atBottom || atRight && atTop || atRight && atBottom) { // Snap to corner ball.x = atLeft ? minBallX : maxBallX; ball.y = atTop ? minBallY : maxBallY; ball.vx *= -0.5; ball.vy *= -0.5; } else { // Bounce off left/right walls if (ball.x < minBallX) { ball.x = minBallX; ball.vx *= -0.5; } if (ball.x > maxBallX) { ball.x = maxBallX; ball.vx *= -0.5; } // Bounce off top/bottom walls if (ball.y < minBallY) { ball.y = minBallY; ball.vy *= -0.5; } if (ball.y > maxBallY) { ball.y = maxBallY; ball.vy *= -0.5; } } // --- Goal detection (horizontal) --- // If ball intersects leftGoal or rightGoal, score! // Only trigger on the exact frame the ball enters the goal (use lastBallInLeftGoal/lastBallInRightGoal) if (typeof ball.lastInLeftGoal === "undefined") ball.lastInLeftGoal = false; if (typeof ball.lastInRightGoal === "undefined") ball.lastInRightGoal = false; var inLeftGoal = ball.intersects(leftGoal); var inRightGoal = ball.intersects(rightGoal); // Score for blue (player) if ball enters bottom goal if (!ball.lastInRightGoal && inRightGoal) { timerRunning = false; LK.effects.flashScreen(0x00ff00, 600); LK.showYouWin(); return; } // Score for red (opponent) if ball enters top goal if (!ball.lastInLeftGoal && inLeftGoal) { timerRunning = false; LK.effects.flashScreen(0xff0000, 600); LK.showGameOver(); return; } ball.lastInLeftGoal = inLeftGoal; ball.lastInRightGoal = inRightGoal; // Prevent the ball from ever stopping: enforce a minimum velocity if it's moving var minBallSpeed = ball.activated ? 8 : 1.2; if (typeof ball.vx === "number" && typeof ball.vy === "number") { var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); if (speed < minBallSpeed) { if (speed === 0) { // If completely stopped, nudge in a random direction var angle = Math.random() * Math.PI * 2; ball.vx = Math.cos(angle) * minBallSpeed; ball.vy = Math.sin(angle) * minBallSpeed; } else { // If moving but too slow, scale up to minimum speed ball.vx = ball.vx / (speed || 1) * minBallSpeed; ball.vy = ball.vy / (speed || 1) * minBallSpeed; } } } // Never allow the ball to stop: if both vx and vy are 0, nudge in a random direction if (ball.activated && typeof ball.vx === "number" && typeof ball.vy === "number" && ball.vx === 0 && ball.vy === 0) { var angle = Math.random() * Math.PI * 2; ball.vx = Math.cos(angle) * minBallSpeed; ball.vy = Math.sin(angle) * minBallSpeed; } // Update lastX for next frame ball.lastX = ball.x; // --- Goal detection --- // If ball intersects leftGoal or rightGoal, score! // Only trigger on the exact frame the ball enters the goal (use lastBallInLeftGoal/lastBallInRightGoal) if (typeof ball.lastInLeftGoal === "undefined") ball.lastInLeftGoal = false; if (typeof ball.lastInRightGoal === "undefined") ball.lastInRightGoal = false; var inLeftGoal = ball.intersects(leftGoal); var inRightGoal = ball.intersects(rightGoal); // Score for blue (player) if ball enters right goal if (!ball.lastInRightGoal && inRightGoal) { timerRunning = false; LK.effects.flashScreen(0x00ff00, 600); LK.showYouWin(); return; } // Score for red (opponent) if ball enters left goal if (!ball.lastInLeftGoal && inLeftGoal) { timerRunning = false; LK.effects.flashScreen(0xff0000, 600); LK.showGameOver(); return; } ball.lastInLeftGoal = inLeftGoal; ball.lastInRightGoal = inRightGoal; } // Update last intersection state lastPlayerBallIntersecting = isIntersecting; }; // Futsal ball (white) // Center circle (white, ellipse) // Center line (white, box) // Main field (futsal green) // Goal area (penalty box, white) // Goal (yellow, box) // Player (blue, ellipse) // --- VERTICAL FIELD (portrait) --- // Field: centered, tall, normal width var field = LK.getAsset('field', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 }); game.addChild(field); // Center line: horizontal (for portrait/vertical field) var centerLine = LK.getAsset('centerLine', { anchorX: 0.5, anchorY: 0.5, width: 1900, // match field width height: 10, // thin horizontal line x: 2048 / 2, y: 2732 / 2 }); game.addChild(centerLine); // Center circle var centerCircle = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 }); game.addChild(centerCircle); // Draw top goal (top in portrait) var topGoal = LK.getAsset('goal', { anchorX: 0.5, anchorY: 0.5, width: 400, // wide height: 40, // short x: 2048 / 2, // centered y: (2732 - 1200) / 2 + 20 // flush with top field edge }); game.addChild(topGoal); // Define leftGoal for goal detection (top goal is leftGoal in vertical/portrait) var leftGoal = topGoal; // Top goal posts (left and right) - horizontal orientation var goalPostWidth = 70; var goalPostHeight = 12; var goalPostOffsetY = topGoal.height / 2 - goalPostHeight / 2; var topGoalPostLeft = LK.getAsset('goal', { anchorX: 1.0, anchorY: 0.5, width: goalPostWidth, height: goalPostHeight, x: topGoal.x - topGoal.width / 2, // near the left of the goal y: topGoal.y - goalPostOffsetY }); game.addChild(topGoalPostLeft); var topGoalPostRight = LK.getAsset('goal', { anchorX: 0.0, anchorY: 0.5, width: goalPostWidth, height: goalPostHeight, x: topGoal.x + topGoal.width / 2, // near the right of the goal y: topGoal.y - goalPostOffsetY }); game.addChild(topGoalPostRight); // Define leftGoalPostLeft and leftGoalPostRight for collision logic // For vertical field, leftGoalPostLeft is the left post, leftGoalPostRight is the right post var leftGoalPostLeft = topGoalPostLeft; var leftGoalPostRight = topGoalPostRight; // Draw bottom goal (bottom in portrait) var bottomGoal = LK.getAsset('goal', { anchorX: 0.5, anchorY: 0.5, width: 400, height: 40, x: 2048 / 2, // centered y: (2732 + 1200) / 2 - 20 // flush with bottom field edge }); game.addChild(bottomGoal); // Define rightGoal for goal detection (bottom goal is rightGoal in vertical/portrait) var rightGoal = bottomGoal; // Bottom goal posts (left and right) - horizontal orientation var bottomGoalPostLeft = LK.getAsset('goal', { anchorX: 1.0, anchorY: 0.5, width: goalPostWidth, height: goalPostHeight, x: bottomGoal.x - bottomGoal.width / 2, // near the left of the goal y: bottomGoal.y + goalPostOffsetY }); game.addChild(bottomGoalPostLeft); var bottomGoalPostRight = LK.getAsset('goal', { anchorX: 0.0, anchorY: 0.5, width: goalPostWidth, height: goalPostHeight, x: bottomGoal.x + bottomGoal.width / 2, // near the right of the goal y: bottomGoal.y + goalPostOffsetY }); game.addChild(bottomGoalPostRight); // Define rightGoalPostLeft and rightGoalPostRight for collision logic // For vertical field, rightGoalPostLeft is the left post, rightGoalPostRight is the right post var rightGoalPostLeft = bottomGoalPostLeft; var rightGoalPostRight = bottomGoalPostRight; // Draw ball at center var ball = LK.getAsset('ball', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 }); ball.vx = 0; ball.vy = 0; game.addChild(ball); // Draw player at top side (blue) var player = LK.getAsset('playerBlue', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, // center y: 2732 / 2 - 350 // above center }); game.addChild(player); // Draw opponent at bottom side (red) var opponent = LK.getAsset('playerRed', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, // center y: 2732 / 2 + 350 // below center }); game.addChild(opponent); ;
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1976d2 // Blue field background
});
/****
* Game Code
****/
;
// --- Timer ---
var timerSeconds = 0;
var timerRunning = true;
var timerTxt = new Text2('0:00', {
size: 90,
fill: 0xffffff,
font: "Impact, Arial Black, Tahoma"
});
timerTxt.anchor.set(0.5, 1);
timerTxt.x = LK.gui.width / 2;
// Place timer at top center
LK.gui.top.addChild(timerTxt);
timerTxt.y = 0;
;
// Track dragging state for both players by touchId
var draggingPlayers = {}; // { touchId: player }
// Convert global (event) coordinates to local game coordinates
function toGameCoords(x, y) {
// LK events provide coordinates in game space already
return {
x: x,
y: y
};
}
// Handle touch/mouse down: start dragging if on player
game.down = function (x, y, obj) {
// Use obj.event.identifier for multi-touch, fallback to 0 for mouse
var touchId = obj && obj.event && typeof obj.event.identifier !== "undefined" ? obj.event.identifier : 0;
// Check if the down event is on the blue player
var dx = x - player.x;
var dy = y - player.y;
var r = player.width / 2;
if (dx * dx + dy * dy <= r * r) {
draggingPlayers[touchId] = player;
return;
}
// Check if the down event is on the red player (opponent)
// (Red is AI controlled, so do not allow dragging)
// var dx2 = x - opponent.x;
// var dy2 = y - opponent.y;
// var r2 = opponent.width / 2;
// if (dx2 * dx2 + dy2 * dy2 <= r2 * r2) {
// draggingPlayers[touchId] = opponent;
// return;
// }
};
// Handle touch/mouse move: move player if dragging
game.move = function (x, y, obj) {
// Use obj.event.identifier for multi-touch, fallback to 0 for mouse
var touchId = obj && obj.event && typeof obj.event.identifier !== "undefined" ? obj.event.identifier : 0;
var draggingPlayer = draggingPlayers[touchId];
if (draggingPlayer) {
if (draggingPlayer === player) {
// Blue player (top side)
var minX = field.x - field.width / 2 + player.width / 2;
var maxX = field.x + field.width / 2 - player.width / 2;
var minY = field.y - field.height / 2 + player.height / 2;
var maxY = field.y; // Center line is the max for top player
draggingPlayer.x = Math.max(minX, Math.min(maxX, x));
draggingPlayer.y = Math.max(minY, Math.min(maxY - player.height / 2, y));
} else if (draggingPlayer === opponent) {
// Red player (bottom side) - AI controlled, do not allow dragging
}
}
};
// Handle touch/mouse up: stop dragging
game.up = function (x, y, obj) {
// Use obj.event.identifier for multi-touch, fallback to 0 for mouse
var touchId = obj && obj.event && typeof obj.event.identifier !== "undefined" ? obj.event.identifier : 0;
delete draggingPlayers[touchId];
};
;
;
// Track last intersection state between player and ball
var lastPlayerBallIntersecting = false;
// Update loop to check for player touching ball
game.update = function () {
// --- Timer update ---
if (timerRunning) {
if (typeof game._lastTimerTick === "undefined") {
game._lastTimerTick = Date.now();
}
var now = Date.now();
if (now - game._lastTimerTick >= 1000) {
timerSeconds += 1;
game._lastTimerTick += 1000;
// Format as M:SS
var min = Math.floor(timerSeconds / 60);
var sec = timerSeconds % 60;
timerTxt.setText(min + ":" + (sec < 10 ? "0" : "") + sec);
}
}
// --- AI logic: Move red (opponent) player toward the ball, sometimes randomly ---
// Only move if timer is running (not after win/game over)
if (timerRunning) {
// AI target: try to stay on the same side as the ball (bottom half of field)
// Only move if ball is on or below center line
var aiTargetY = Math.max(field.y, ball.y);
// Clamp AI movement to bottom half of field
var minX = field.x - field.width / 2 + opponent.width / 2;
var maxX = field.x + field.width / 2 - opponent.width / 2;
var minY = field.y;
var maxY = field.y + field.height / 2 - opponent.height / 2;
// --- Random movement logic ---
if (typeof opponent.aiRandomCooldown === "undefined") opponent.aiRandomCooldown = 0;
if (typeof opponent.aiRandomTargetX === "undefined") opponent.aiRandomTargetX = opponent.x;
if (typeof opponent.aiRandomTargetY === "undefined") opponent.aiRandomTargetY = opponent.y;
if (typeof opponent.aiRandomActive === "undefined") opponent.aiRandomActive = false;
// Occasionally (about every 1.5-3s), pick a random target in the AI's allowed area
if (opponent.aiRandomCooldown <= 0 && Math.random() < 0.012) {
opponent.aiRandomActive = true;
// Pick a random point in the lower half of the field
opponent.aiRandomTargetX = minX + Math.random() * (maxX - minX);
opponent.aiRandomTargetY = minY + Math.random() * (maxY - minY);
// Random movement lasts 30-60 frames
opponent.aiRandomCooldown = 30 + Math.floor(Math.random() * 30);
}
// If random movement is active, move toward the random target
if (opponent.aiRandomActive && opponent.aiRandomCooldown > 0) {
var targetX = opponent.aiRandomTargetX;
var targetY = opponent.aiRandomTargetY;
opponent.aiRandomCooldown--;
// If close to the random target, stop random movement
var distToRandom = Math.sqrt((targetX - opponent.x) * (targetX - opponent.x) + (targetY - opponent.y) * (targetY - opponent.y));
if (distToRandom < 40 || opponent.aiRandomCooldown <= 0) {
opponent.aiRandomActive = false;
opponent.aiRandomCooldown = 60 + Math.floor(Math.random() * 60); // Wait before next random
}
} else {
// Normal AI: follow the ball
var targetX = Math.max(minX, Math.min(maxX, ball.x));
var targetY = Math.max(minY, Math.min(maxY, aiTargetY));
if (opponent.aiRandomCooldown > 0) {
opponent.aiRandomCooldown--;
}
}
// Move opponent toward target position with a max speed
var aiSpeed = 32; // px per frame (tune for difficulty)
var dx = targetX - opponent.x;
var dy = targetY - opponent.y;
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
if (dist > aiSpeed) {
opponent.x += dx / dist * aiSpeed;
opponent.y += dy / dist * aiSpeed;
} else {
opponent.x = targetX;
opponent.y = targetY;
}
}
// Check if player is touching the ball (collision/intersection)
var isIntersecting = player.intersects(ball);
// Detect the exact moment player starts touching the ball
if (!lastPlayerBallIntersecting && isIntersecting) {
// Calculate direction from player to ball
var dx = ball.x - player.x;
var dy = ball.y - player.y;
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
// Give the ball a velocity away from the player (EVEN FASTER than before)
ball.vx = dx / dist * 70;
ball.vy = dy / dist * 70;
LK.effects.flashObject(ball, 0x1976d2, 200);
}
// AI: Red player can also kick the ball if touching, from any side
if (opponent.intersects(ball)) {
// Check if ball is in a corner
var minBallX = field.x - field.width / 2 + ball.width / 2;
var maxBallX = field.x + field.width / 2 - ball.width / 2;
var minBallY = field.y - field.height / 2 + ball.height / 2;
var maxBallY = field.y + field.height / 2 - ball.height / 2;
var atLeft = ball.x <= minBallX + 1;
var atRight = ball.x >= maxBallX - 1;
var atTop = ball.y <= minBallY + 1;
var atBottom = ball.y >= maxBallY - 1;
// --- Ball stuck detection ---
if (typeof ball.stuckFrames === "undefined") ball.stuckFrames = 0;
if (typeof ball.lastStuckX === "undefined") ball.lastStuckX = ball.x;
if (typeof ball.lastStuckY === "undefined") ball.lastStuckY = ball.y;
// If the ball hasn't moved much in 10 frames, consider it stuck
var stuckThreshold = 6;
if (Math.abs(ball.x - ball.lastStuckX) < stuckThreshold && Math.abs(ball.y - ball.lastStuckY) < stuckThreshold) {
ball.stuckFrames++;
} else {
ball.stuckFrames = 0;
ball.lastStuckX = ball.x;
ball.lastStuckY = ball.y;
}
// If the ball is stuck for too long, red gives up and the game finishes
if (ball.stuckFrames > 90) {
// 90 frames ~1.5 seconds at 60fps
timerRunning = false;
LK.effects.flashScreen(0xff0000, 600);
LK.showGameOver();
return;
}
var dxr, dyr;
if ((atLeft || atRight) && (atTop || atBottom)) {
// Ball is in a corner, AI tries to hit it toward the center of the field
dxr = field.x - ball.x;
dyr = field.y - ball.y;
} else if (ball.stuckFrames > 12) {
// Ball is stuck: AI tries to hit the right side of the ball
// Always aim to the right (positive X direction)
dxr = 1;
dyr = 0;
// Add a little bias toward the field center to avoid infinite right wall hits
dxr += (field.x - ball.x) * 0.005;
dyr += (field.y - ball.y) * 0.01;
} else {
// Normal: hit from AI to ball direction
dxr = ball.x - opponent.x;
dyr = ball.y - opponent.y;
}
var distr = Math.sqrt(dxr * dxr + dyr * dyr) || 1;
ball.vx = dxr / distr * 50;
ball.vy = dyr / distr * 50;
LK.effects.flashObject(ball, 0xd32f2f, 200);
}
// Ball activation: Ball is stopped at start, but after first touch, it never stops and goes faster
if (typeof ball.activated === "undefined") ball.activated = false;
if (!ball.activated && (player.intersects(ball) || opponent.intersects(ball))) {
ball.activated = true;
// Give a small nudge in a random direction if ball is perfectly still
if (ball.vx === 0 && ball.vy === 0) {
var angle = Math.random() * Math.PI * 2;
ball.vx = Math.cos(angle) * 18;
ball.vy = Math.sin(angle) * 18;
}
}
// Move the ball if it has velocity
if (typeof ball.vx === "number" && typeof ball.vy === "number") {
// --- Ball bounce off goal posts (horizontal) ---
// Track last intersection for each post
if (typeof ball.lastLeftGoalPostLeftIntersect === "undefined") ball.lastLeftGoalPostLeftIntersect = false;
if (typeof ball.lastLeftGoalPostRightIntersect === "undefined") ball.lastLeftGoalPostRightIntersect = false;
if (typeof ball.lastRightGoalPostLeftIntersect === "undefined") ball.lastRightGoalPostLeftIntersect = false;
if (typeof ball.lastRightGoalPostRightIntersect === "undefined") ball.lastRightGoalPostRightIntersect = false;
// Left goal post left
var leftLeftNow = ball.intersects(leftGoalPostLeft);
if (!ball.lastLeftGoalPostLeftIntersect && leftLeftNow) {
// Bounce: reflect velocity, add a little randomness
var dx = ball.x - (leftGoalPostLeft.x - leftGoalPostLeft.width / 2);
var dy = ball.y - leftGoalPostLeft.y;
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
ball.vx = dx / dist * Math.max(Math.abs(ball.vx), 30);
ball.vy = dy / dist * Math.max(Math.abs(ball.vy), 30);
LK.effects.flashObject(ball, 0xffff00, 120);
}
ball.lastLeftGoalPostLeftIntersect = leftLeftNow;
// Left goal post right
var leftRightNow = ball.intersects(leftGoalPostRight);
if (!ball.lastLeftGoalPostRightIntersect && leftRightNow) {
var dx = ball.x - (leftGoalPostRight.x + leftGoalPostRight.width / 2);
var dy = ball.y - leftGoalPostRight.y;
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
ball.vx = dx / dist * Math.max(Math.abs(ball.vx), 30);
ball.vy = dy / dist * Math.max(Math.abs(ball.vy), 30);
LK.effects.flashObject(ball, 0xffff00, 120);
}
ball.lastLeftGoalPostRightIntersect = leftRightNow;
// Right goal post left
var rightLeftNow = ball.intersects(rightGoalPostLeft);
if (!ball.lastRightGoalPostLeftIntersect && rightLeftNow) {
var dx = ball.x - (rightGoalPostLeft.x - rightGoalPostLeft.width / 2);
var dy = ball.y - rightGoalPostLeft.y;
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
ball.vx = dx / dist * Math.max(Math.abs(ball.vx), 30);
ball.vy = dy / dist * Math.max(Math.abs(ball.vy), 30);
LK.effects.flashObject(ball, 0xffff00, 120);
}
ball.lastRightGoalPostLeftIntersect = rightLeftNow;
// Right goal post right
var rightRightNow = ball.intersects(rightGoalPostRight);
if (!ball.lastRightGoalPostRightIntersect && rightRightNow) {
var dx = ball.x - (rightGoalPostRight.x + rightGoalPostRight.width / 2);
var dy = ball.y - rightGoalPostRight.y;
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
ball.vx = dx / dist * Math.max(Math.abs(ball.vx), 30);
ball.vy = dy / dist * Math.max(Math.abs(ball.vy), 30);
LK.effects.flashObject(ball, 0xffff00, 120);
}
ball.lastRightGoalPostRightIntersect = rightRightNow;
// Track lastX for side detection
if (typeof ball.lastX === "undefined") ball.lastX = ball.x;
// Detect crossing from left to right side (player to opponent)
var centerX = field.x;
if (ball.lastX <= centerX && ball.x > centerX) {
// Ball just crossed to right side, speed up!
ball.vx *= 1.25;
ball.vy *= 1.25;
}
// Detect crossing from right to left side (opponent to player)
if (ball.lastX >= centerX && ball.x < centerX) {
// Ball just crossed to left side, speed up!
ball.vx *= 1.25;
ball.vy *= 1.25;
}
if (ball.activated) {
ball.x += ball.vx;
ball.y += ball.vy;
}
// Friction
ball.vx *= 0.96;
ball.vy *= 0.96;
// Clamp to field bounds and bounce off corners (horizontal)
var minBallX = field.x - field.width / 2 + ball.width / 2;
var maxBallX = field.x + field.width / 2 - ball.width / 2;
var minBallY = field.y - field.height / 2 + ball.height / 2;
var maxBallY = field.y + field.height / 2 - ball.height / 2;
// Detect if ball is in a corner (within 1px of both X and Y bounds)
var atLeft = ball.x <= minBallX + 1;
var atRight = ball.x >= maxBallX - 1;
var atTop = ball.y <= minBallY + 1;
var atBottom = ball.y >= maxBallY - 1;
// Bounce off corners: if at a corner, reflect both vx and vy
if (atLeft && atTop || atLeft && atBottom || atRight && atTop || atRight && atBottom) {
// Snap to corner
ball.x = atLeft ? minBallX : maxBallX;
ball.y = atTop ? minBallY : maxBallY;
ball.vx *= -0.5;
ball.vy *= -0.5;
} else {
// Bounce off left/right walls
if (ball.x < minBallX) {
ball.x = minBallX;
ball.vx *= -0.5;
}
if (ball.x > maxBallX) {
ball.x = maxBallX;
ball.vx *= -0.5;
}
// Bounce off top/bottom walls
if (ball.y < minBallY) {
ball.y = minBallY;
ball.vy *= -0.5;
}
if (ball.y > maxBallY) {
ball.y = maxBallY;
ball.vy *= -0.5;
}
}
// --- Goal detection (horizontal) ---
// If ball intersects leftGoal or rightGoal, score!
// Only trigger on the exact frame the ball enters the goal (use lastBallInLeftGoal/lastBallInRightGoal)
if (typeof ball.lastInLeftGoal === "undefined") ball.lastInLeftGoal = false;
if (typeof ball.lastInRightGoal === "undefined") ball.lastInRightGoal = false;
var inLeftGoal = ball.intersects(leftGoal);
var inRightGoal = ball.intersects(rightGoal);
// Score for blue (player) if ball enters bottom goal
if (!ball.lastInRightGoal && inRightGoal) {
timerRunning = false;
LK.effects.flashScreen(0x00ff00, 600);
LK.showYouWin();
return;
}
// Score for red (opponent) if ball enters top goal
if (!ball.lastInLeftGoal && inLeftGoal) {
timerRunning = false;
LK.effects.flashScreen(0xff0000, 600);
LK.showGameOver();
return;
}
ball.lastInLeftGoal = inLeftGoal;
ball.lastInRightGoal = inRightGoal;
// Prevent the ball from ever stopping: enforce a minimum velocity if it's moving
var minBallSpeed = ball.activated ? 8 : 1.2;
if (typeof ball.vx === "number" && typeof ball.vy === "number") {
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
if (speed < minBallSpeed) {
if (speed === 0) {
// If completely stopped, nudge in a random direction
var angle = Math.random() * Math.PI * 2;
ball.vx = Math.cos(angle) * minBallSpeed;
ball.vy = Math.sin(angle) * minBallSpeed;
} else {
// If moving but too slow, scale up to minimum speed
ball.vx = ball.vx / (speed || 1) * minBallSpeed;
ball.vy = ball.vy / (speed || 1) * minBallSpeed;
}
}
}
// Never allow the ball to stop: if both vx and vy are 0, nudge in a random direction
if (ball.activated && typeof ball.vx === "number" && typeof ball.vy === "number" && ball.vx === 0 && ball.vy === 0) {
var angle = Math.random() * Math.PI * 2;
ball.vx = Math.cos(angle) * minBallSpeed;
ball.vy = Math.sin(angle) * minBallSpeed;
}
// Update lastX for next frame
ball.lastX = ball.x;
// --- Goal detection ---
// If ball intersects leftGoal or rightGoal, score!
// Only trigger on the exact frame the ball enters the goal (use lastBallInLeftGoal/lastBallInRightGoal)
if (typeof ball.lastInLeftGoal === "undefined") ball.lastInLeftGoal = false;
if (typeof ball.lastInRightGoal === "undefined") ball.lastInRightGoal = false;
var inLeftGoal = ball.intersects(leftGoal);
var inRightGoal = ball.intersects(rightGoal);
// Score for blue (player) if ball enters right goal
if (!ball.lastInRightGoal && inRightGoal) {
timerRunning = false;
LK.effects.flashScreen(0x00ff00, 600);
LK.showYouWin();
return;
}
// Score for red (opponent) if ball enters left goal
if (!ball.lastInLeftGoal && inLeftGoal) {
timerRunning = false;
LK.effects.flashScreen(0xff0000, 600);
LK.showGameOver();
return;
}
ball.lastInLeftGoal = inLeftGoal;
ball.lastInRightGoal = inRightGoal;
}
// Update last intersection state
lastPlayerBallIntersecting = isIntersecting;
};
// Futsal ball (white)
// Center circle (white, ellipse)
// Center line (white, box)
// Main field (futsal green)
// Goal area (penalty box, white)
// Goal (yellow, box)
// Player (blue, ellipse)
// --- VERTICAL FIELD (portrait) ---
// Field: centered, tall, normal width
var field = LK.getAsset('field', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(field);
// Center line: horizontal (for portrait/vertical field)
var centerLine = LK.getAsset('centerLine', {
anchorX: 0.5,
anchorY: 0.5,
width: 1900,
// match field width
height: 10,
// thin horizontal line
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(centerLine);
// Center circle
var centerCircle = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(centerCircle);
// Draw top goal (top in portrait)
var topGoal = LK.getAsset('goal', {
anchorX: 0.5,
anchorY: 0.5,
width: 400,
// wide
height: 40,
// short
x: 2048 / 2,
// centered
y: (2732 - 1200) / 2 + 20
// flush with top field edge
});
game.addChild(topGoal);
// Define leftGoal for goal detection (top goal is leftGoal in vertical/portrait)
var leftGoal = topGoal;
// Top goal posts (left and right) - horizontal orientation
var goalPostWidth = 70;
var goalPostHeight = 12;
var goalPostOffsetY = topGoal.height / 2 - goalPostHeight / 2;
var topGoalPostLeft = LK.getAsset('goal', {
anchorX: 1.0,
anchorY: 0.5,
width: goalPostWidth,
height: goalPostHeight,
x: topGoal.x - topGoal.width / 2,
// near the left of the goal
y: topGoal.y - goalPostOffsetY
});
game.addChild(topGoalPostLeft);
var topGoalPostRight = LK.getAsset('goal', {
anchorX: 0.0,
anchorY: 0.5,
width: goalPostWidth,
height: goalPostHeight,
x: topGoal.x + topGoal.width / 2,
// near the right of the goal
y: topGoal.y - goalPostOffsetY
});
game.addChild(topGoalPostRight);
// Define leftGoalPostLeft and leftGoalPostRight for collision logic
// For vertical field, leftGoalPostLeft is the left post, leftGoalPostRight is the right post
var leftGoalPostLeft = topGoalPostLeft;
var leftGoalPostRight = topGoalPostRight;
// Draw bottom goal (bottom in portrait)
var bottomGoal = LK.getAsset('goal', {
anchorX: 0.5,
anchorY: 0.5,
width: 400,
height: 40,
x: 2048 / 2,
// centered
y: (2732 + 1200) / 2 - 20
// flush with bottom field edge
});
game.addChild(bottomGoal);
// Define rightGoal for goal detection (bottom goal is rightGoal in vertical/portrait)
var rightGoal = bottomGoal;
// Bottom goal posts (left and right) - horizontal orientation
var bottomGoalPostLeft = LK.getAsset('goal', {
anchorX: 1.0,
anchorY: 0.5,
width: goalPostWidth,
height: goalPostHeight,
x: bottomGoal.x - bottomGoal.width / 2,
// near the left of the goal
y: bottomGoal.y + goalPostOffsetY
});
game.addChild(bottomGoalPostLeft);
var bottomGoalPostRight = LK.getAsset('goal', {
anchorX: 0.0,
anchorY: 0.5,
width: goalPostWidth,
height: goalPostHeight,
x: bottomGoal.x + bottomGoal.width / 2,
// near the right of the goal
y: bottomGoal.y + goalPostOffsetY
});
game.addChild(bottomGoalPostRight);
// Define rightGoalPostLeft and rightGoalPostRight for collision logic
// For vertical field, rightGoalPostLeft is the left post, rightGoalPostRight is the right post
var rightGoalPostLeft = bottomGoalPostLeft;
var rightGoalPostRight = bottomGoalPostRight;
// Draw ball at center
var ball = LK.getAsset('ball', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2
});
ball.vx = 0;
ball.vy = 0;
game.addChild(ball);
// Draw player at top side (blue)
var player = LK.getAsset('playerBlue', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
// center
y: 2732 / 2 - 350
// above center
});
game.addChild(player);
// Draw opponent at bottom side (red)
var opponent = LK.getAsset('playerRed', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
// center
y: 2732 / 2 + 350
// below center
});
game.addChild(opponent);
;