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