/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Ball class var GolfBall = Container.expand(function () { var self = Container.call(this); var ball = self.attachAsset('golfBall', { anchorX: 0.5, anchorY: 0.5 }); self.radius = ball.width / 2; self.vx = 0; self.vy = 0; self.moving = false; // For drag aiming self.aimArrow = null; // For tracking drag self.isAiming = false; self.aimStartX = 0; self.aimStartY = 0; self.aimEndX = 0; self.aimEndY = 0; // Show aim arrow self.showArrow = function (dx, dy) { if (!self.aimArrow) { self.aimArrow = LK.getAsset('golfArrow', { anchorX: 0.5, anchorY: 1 }); self.addChild(self.aimArrow); } var len = Math.sqrt(dx * dx + dy * dy); // Clamp arrow length for visual var maxLen = 300; var arrowLen = Math.min(len, maxLen); self.aimArrow.height = arrowLen; self.aimArrow.rotation = Math.atan2(dy, dx) + Math.PI / 2; self.aimArrow.visible = true; }; self.hideArrow = function () { if (self.aimArrow) { self.aimArrow.visible = false; } }; // Update ball physics self.update = function () { if (self.moving) { // Move ball self.x += self.vx; self.y += self.vy; // Friction var speed = Math.sqrt(self.vx * self.vx + self.vy * self.vy); if (speed > 0) { var friction = 0.98; self.vx *= friction; self.vy *= friction; // Stop if very slow if (Math.sqrt(self.vx * self.vx + self.vy * self.vy) < 1.2) { self.vx = 0; self.vy = 0; self.moving = false; } } } }; return self; }); // Hole class var GolfHole = Container.expand(function () { var self = Container.call(this); var hole = self.attachAsset('golfHole', { anchorX: 0.5, anchorY: 0.5 }); self.radius = hole.width / 2; return self; }); // Wall class var GolfWall = Container.expand(function () { var self = Container.call(this); var wall = self.attachAsset('golfWall', { anchorX: 0.5, anchorY: 0.5 }); self.width = wall.width; self.height = wall.height; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x6abf4b // green grass }); /**** * Game Code ****/ // Menu overlay container // --- MENU SYSTEM --- // Ball: white circle // Hole: black circle // Wall: green rectangle // Arrow: yellow rectangle (for aiming indicator) var menuOverlay = new Container(); // Menu background (semi-transparent) var menuBg = LK.getAsset('golfWall', { anchorX: 0.5, anchorY: 0.5, width: 1200, height: 1200 }); menuBg.alpha = 0.92; menuBg.tint = 0x222222; menuOverlay.addChild(menuBg); menuBg.x = 2048 / 2; menuBg.y = 2732 / 2; // Menu title var menuTitle = new Text2('SELECT MODE', { size: 140, fill: 0xFFFFFF }); menuTitle.anchor.set(0.5, 0.5); menuTitle.x = 2048 / 2; menuTitle.y = 2732 / 2 - 400; menuOverlay.addChild(menuTitle); // Menu options var menuOptions = [{ label: "SANDBOX", desc: "NO TIME LIMIT", timer: null, increment: 0 }, { label: "BEGINNER", desc: "START: 30s, +5s/hole", timer: 30, increment: 5 }, { label: "PRO", desc: "START: 10s, +3s/hole", timer: 10, increment: 3 }, { label: "EXPERT", desc: "START: 3s, +2s/hole", timer: 3, increment: 2 }]; var menuButtons = []; var menuDescLabels = []; var menuButtonYStart = 2732 / 2 - 180; var menuButtonSpacing = 220; for (var i = 0; i < menuOptions.length; i++) { // Button var btn = new Text2(menuOptions[i].label, { size: 120, fill: 0xFFFF00 }); btn.anchor.set(0.5, 0.5); btn.x = 2048 / 2; btn.y = menuButtonYStart + i * menuButtonSpacing; btn.interactive = true; btn.buttonMode = true; menuOverlay.addChild(btn); menuButtons.push(btn); // Description // (Removed description text under menu options) } // Add a back button to the menu overlay var menuBackBtn = new Text2('BACK', { size: 90, fill: 0xFF4444 }); menuBackBtn.anchor.set(0.5, 0.5); menuBackBtn.x = 2048 - 200; menuBackBtn.y = 200; menuBackBtn.interactive = true; menuBackBtn.buttonMode = true; menuBackBtn.down = function (x, y, obj) { // Just hide the menu overlay, do not start a game if (menuOverlay.parent) { menuOverlay.parent.removeChild(menuOverlay); } // Optionally, you could reset to a splash screen or do nothing // For now, just show the menu again (if needed, you can add more logic here) game.addChild(menuOverlay); }; menuOverlay.addChild(menuBackBtn); // Add menu overlay to game game.addChild(menuOverlay); // Hide back button when menu is shown var backBtn = null; menuOverlay.onAdded = function () { if (backBtn) backBtn.visible = false; }; // Hide game elements until menu is dismissed var margin = 120; var courseWidth = 2048 - margin * 2; var courseHeight = 2732 - margin * 2; var walls = []; var topWall, bottomWall, leftWall, rightWall; var hole, ball; var strokes = 0; var score = 0; var scoreLabel, scoreTxt, timerDuration, timerValue, timerInterval, timerTxt; var timeIncrement = 0; var sandboxMode = false; // Helper to show/hide game elements function setGameElementsVisible(visible) { if (topWall) { topWall.visible = visible; } if (bottomWall) { bottomWall.visible = visible; } if (leftWall) { leftWall.visible = visible; } if (rightWall) { rightWall.visible = visible; } if (hole) { hole.visible = visible; } if (ball) { ball.visible = visible; } if (scoreLabel) { scoreLabel.visible = visible; } if (scoreTxt) { scoreTxt.visible = visible; } if (timerTxt) { timerTxt.visible = visible; } if (typeof backBtn !== "undefined" && backBtn !== null) { backBtn.visible = visible; } } // Remove menu and start game with selected mode function startGameWithMode(modeIdx) { // Create back button if not already created if (typeof backBtn === "undefined" || backBtn === null) { backBtn = new Text2('BACK', { size: 90, fill: 0xFF4444 }); backBtn.anchor.set(0.5, 0.5); backBtn.x = 2048 - 200; backBtn.y = 200; backBtn.interactive = true; backBtn.buttonMode = true; backBtn.visible = false; backBtn.down = function (x, y, obj) { // Remove all game elements setGameElementsVisible(false); // Remove walls for (var i = 0; i < walls.length; i++) { if (walls[i] && walls[i].parent) { walls[i].parent.removeChild(walls[i]); } } walls = []; // Remove hole and ball if (hole && hole.parent) { hole.parent.removeChild(hole); } if (ball && ball.parent) { ball.parent.removeChild(ball); } hole = null; ball = null; // Remove score and timer if (scoreLabel && scoreLabel.parent) { scoreLabel.parent.removeChild(scoreLabel); } if (scoreTxt && scoreTxt.parent) { scoreTxt.parent.removeChild(scoreTxt); } if (timerTxt && timerTxt.parent) { timerTxt.parent.removeChild(timerTxt); } // Remove back button if (backBtn && backBtn.parent) { backBtn.parent.removeChild(backBtn); } // Stop timer if running if (timerInterval) { LK.clearInterval(timerInterval); timerInterval = null; } // Show menu overlay again if (!menuOverlay.parent) { game.addChild(menuOverlay); } setGameElementsVisible(false); }; // Always add backBtn after all walls and game elements so it's on top game.addChild(backBtn); if (backBtn.parent) { backBtn.parent.removeChild(backBtn); game.addChild(backBtn); } } backBtn.visible = true; if (backBtn.parent) { backBtn.parent.removeChild(backBtn); game.addChild(backBtn); } // Remove menu overlay if (menuOverlay.parent) { menuOverlay.parent.removeChild(menuOverlay); } // Set timer and increment var opt = menuOptions[modeIdx]; sandboxMode = opt.timer === null; timerDuration = opt.timer === null ? 0 : opt.timer; timeIncrement = opt.increment; // Create walls topWall = new GolfWall(); topWall.width = courseWidth; topWall.height = 600; topWall.x = 2048 / 2; topWall.y = margin - 200; topWall.children[0].width = courseWidth; topWall.children[0].height = 120; walls.push(topWall); game.addChild(topWall); bottomWall = new GolfWall(); bottomWall.width = courseWidth; bottomWall.height = 600; bottomWall.x = 2048 / 2; bottomWall.y = 2732 - margin + 200; bottomWall.children[0].width = courseWidth; bottomWall.children[0].height = 120; walls.push(bottomWall); game.addChild(bottomWall); leftWall = new GolfWall(); leftWall.width = 350; leftWall.height = courseHeight; leftWall.x = margin - 75; leftWall.y = 2732 / 2; leftWall.children[0].width = 120; leftWall.children[0].height = courseHeight; walls.push(leftWall); game.addChild(leftWall); rightWall = new GolfWall(); rightWall.width = 350; rightWall.height = courseHeight; rightWall.x = 2048 - margin + 75; rightWall.y = 2732 / 2; rightWall.children[0].width = 120; rightWall.children[0].height = courseHeight; walls.push(rightWall); game.addChild(rightWall); // Create hole hole = new GolfHole(); hole.x = 2048 - margin - 200; hole.y = 2732 - margin - 200; game.addChild(hole); // Create ball ball = new GolfBall(); ball.x = margin + 200; ball.y = 2732 - margin - 400; game.addChild(ball); // Strokes and score strokes = 0; score = 0; // Score label (number of holes) at the center of the screen scoreLabel = new Text2('Score: ' + score, { size: 100, fill: 0xFFFFFF }); scoreLabel.anchor.set(0.5, 0.5); game.addChild(scoreLabel); scoreLabel.x = 2048 / 2; scoreLabel.y = 2732 / 2 - 1100; // Score text (Strokes) at the top center scoreTxt = new Text2('Strokes: ' + strokes, { size: 100, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); scoreTxt.x = 2048 / 2; scoreTxt.y = 30; // Move score text slightly higher // Timer variables timerValue = timerDuration; timerInterval = null; // Timer text timerTxt = new Text2('Time: ' + (sandboxMode ? '∞' : timerValue), { size: 100, fill: 0xFFFFFF }); timerTxt.anchor.set(0.5, 0.5); game.addChild(timerTxt); timerTxt.x = 2048 / 2; timerTxt.y = 2732 / 2; // Remove the pause button from the GUI overlay if it exists if (LK.gui.pause) { LK.gui.pause.visible = false; if (LK.gui.pause.parent) { LK.gui.pause.parent.removeChild(LK.gui.pause); } } // Start timer if not sandbox if (!sandboxMode) { startTimer(); } else { timerTxt.setText('Time: ∞'); } setGameElementsVisible(true); } // Attach menu button handlers for (var i = 0; i < menuButtons.length; i++) { (function (idx) { menuButtons[idx].down = function (x, y, obj) { setGameElementsVisible(false); startGameWithMode(idx); }; })(i); } // Hide game elements until menu is dismissed setGameElementsVisible(false); // --- END MENU SYSTEM --- // Function to start/reset timer function startTimer() { timerValue = timerDuration; timerTxt.setText('Time: ' + timerValue); if (timerInterval) { LK.clearInterval(timerInterval); } timerInterval = LK.setInterval(function () { timerValue -= 1; if (timerValue < 0) { timerValue = 0; } timerTxt.setText('Time: ' + timerValue); if (timerValue <= 0) { LK.clearInterval(timerInterval); LK.showGameOver(); } }, 1000); } // Drag/aim state var isDragging = false; var dragStartX = 0; var dragStartY = 0; // Helper: clamp ball inside course function clampBallPosition() { // No clamping to invisible walls; allow ball to go to the very edge of the course. } // Helper: ball-wall collision function handleWallCollisions() { for (var i = 0; i < walls.length; i++) { var w = walls[i]; // Axis-aligned rectangle collision var wx = w.x; var wy = w.y; var ww = w.width; var wh = w.height; var bx = ball.x; var by = ball.y; var r = ball.radius; // Rectangle bounds var left = wx - ww / 2; var right = wx + ww / 2; var top = wy - wh / 2; var bottom = wy + wh / 2; // Find closest point on wall to ball var closestX = Math.max(left, Math.min(bx, right)); var closestY = Math.max(top, Math.min(by, bottom)); var dx = bx - closestX; var dy = by - closestY; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < r) { // Ball is colliding with wall // Push ball out var overlap = r - dist + 1; if (dist === 0) { // Prevent NaN dx = 1; dy = 0; dist = 1; } // Move ball out of wall ball.x += dx / dist * overlap; ball.y += dy / dist * overlap; // Calculate the normal vector var nx = dx / dist; var ny = dy / dist; // Calculate dot product of velocity and normal var dot = ball.vx * nx + ball.vy * ny; // Reflect velocity using the normal (like center wall) ball.vx = ball.vx - 2 * dot * nx; ball.vy = ball.vy - 2 * dot * ny; // Dampen speed ball.vx *= 0.7; ball.vy *= 0.7; } } } // Helper: ball-in-hole detection function checkHole() { var dx = ball.x - hole.x; var dy = ball.y - hole.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < hole.radius - ball.radius / 2) { // Ball in hole! Instantly hide ball and the hole ball.moving = false; ball.vx = 0; ball.vy = 0; ball.visible = false; hole.visible = false; return true; } return false; } // Helper: reset ball after win (not needed, handled by LK.showYouWin) // Game move handler (for drag aiming) function handleMove(x, y, obj) { if (isDragging && !ball.moving) { // Clamp drag to max length var dx = x - dragStartX; var dy = y - dragStartY; var maxDrag = 400; var dragLen = Math.sqrt(dx * dx + dy * dy); if (dragLen > maxDrag) { var scale = maxDrag / dragLen; dx *= scale; dy *= scale; } // Show aim arrow ball.showArrow(-dx, -dy); } } // Game down handler (start drag) game.down = function (x, y, obj) { // Only allow aiming if ball exists, is not moving, and touch is near ball if (typeof ball !== "undefined" && ball !== null && !ball.moving) { var dx = x - ball.x; var dy = y - ball.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < ball.radius + 40) { isDragging = true; dragStartX = ball.x; dragStartY = ball.y; ball.aimStartX = x; ball.aimStartY = y; ball.showArrow(0, 0); } } }; // Game move handler game.move = function (x, y, obj) { if (isDragging && !ball.moving) { // Update aim arrow var dx = x - dragStartX; var dy = y - dragStartY; var maxDrag = 400; var dragLen = Math.sqrt(dx * dx + dy * dy); if (dragLen > maxDrag) { var scale = maxDrag / dragLen; dx *= scale; dy *= scale; } ball.showArrow(-dx, -dy); } }; // Game up handler (release drag, shoot) game.up = function (x, y, obj) { if (isDragging && !ball.moving) { var dx = x - dragStartX; var dy = y - dragStartY; // Power proportional to drag length var power = Math.sqrt(dx * dx + dy * dy); var maxPower = 38; var minPower = 10; var shotPower = Math.min(power / 10, maxPower); if (shotPower < minPower / 2) { // Too short, ignore ball.hideArrow(); isDragging = false; return; } // Set velocity (opposite direction of drag) var angle = Math.atan2(dy, dx); ball.vx = -Math.cos(angle) * shotPower; ball.vy = -Math.sin(angle) * shotPower; ball.moving = true; ball.hideArrow(); isDragging = false; // Increment strokes strokes += 1; scoreTxt.setText('Strokes: ' + strokes); } }; // Main game update game.update = function () { // Ball physics if (typeof ball !== "undefined" && ball !== null && typeof ball.update === "function") { ball.update(); } // Clamp ball inside course clampBallPosition(); // Ball-wall collisions handleWallCollisions(); // Ball-in-hole if (typeof hole !== "undefined" && hole !== null && hole.visible && ball && ball.visible) { var dx = ball.x - hole.x; var dy = ball.y - hole.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < hole.radius + ball.radius / 2) { // Ball touched the hole! Instantly hide the hole, but do not dequeue or hide the ball if (hole.parent) { hole.parent.removeChild(hole); } hole.visible = false; // Increment score and update label score += 1; scoreLabel.setText('Score: ' + score); // Add time increment if not sandbox if (!sandboxMode) { timerValue = Math.min(timerValue + timeIncrement, 99); timerTxt.setText('Time: ' + timerValue); } else { timerTxt.setText('Time: ∞'); } // Do not stop the ball when it touches the hole; let it keep moving // Create a new hole at a random position (not too close to the ball or walls) var newHole = new GolfHole(); var safeMargin = margin + 200; var minDistFromBall = 400; var placed = false; for (var tries = 0; tries < 20 && !placed; tries++) { var hx = Math.random() * (2048 - 2 * safeMargin) + safeMargin; var hy = Math.random() * (2732 - 2 * safeMargin) + safeMargin; var bdx = hx - ball.x; var bdy = hy - ball.y; var bdist = Math.sqrt(bdx * bdx + bdy * bdy); if (bdist > minDistFromBall) { newHole.x = hx; newHole.y = hy; placed = true; } } if (!placed) { // fallback: just put it somewhere safe newHole.x = 2048 / 2; newHole.y = 2732 / 2; } game.addChild(newHole); hole = newHole; // No timer reset here, just add time increment above } } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball class
var GolfBall = Container.expand(function () {
var self = Container.call(this);
var ball = self.attachAsset('golfBall', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = ball.width / 2;
self.vx = 0;
self.vy = 0;
self.moving = false;
// For drag aiming
self.aimArrow = null;
// For tracking drag
self.isAiming = false;
self.aimStartX = 0;
self.aimStartY = 0;
self.aimEndX = 0;
self.aimEndY = 0;
// Show aim arrow
self.showArrow = function (dx, dy) {
if (!self.aimArrow) {
self.aimArrow = LK.getAsset('golfArrow', {
anchorX: 0.5,
anchorY: 1
});
self.addChild(self.aimArrow);
}
var len = Math.sqrt(dx * dx + dy * dy);
// Clamp arrow length for visual
var maxLen = 300;
var arrowLen = Math.min(len, maxLen);
self.aimArrow.height = arrowLen;
self.aimArrow.rotation = Math.atan2(dy, dx) + Math.PI / 2;
self.aimArrow.visible = true;
};
self.hideArrow = function () {
if (self.aimArrow) {
self.aimArrow.visible = false;
}
};
// Update ball physics
self.update = function () {
if (self.moving) {
// Move ball
self.x += self.vx;
self.y += self.vy;
// Friction
var speed = Math.sqrt(self.vx * self.vx + self.vy * self.vy);
if (speed > 0) {
var friction = 0.98;
self.vx *= friction;
self.vy *= friction;
// Stop if very slow
if (Math.sqrt(self.vx * self.vx + self.vy * self.vy) < 1.2) {
self.vx = 0;
self.vy = 0;
self.moving = false;
}
}
}
};
return self;
});
// Hole class
var GolfHole = Container.expand(function () {
var self = Container.call(this);
var hole = self.attachAsset('golfHole', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = hole.width / 2;
return self;
});
// Wall class
var GolfWall = Container.expand(function () {
var self = Container.call(this);
var wall = self.attachAsset('golfWall', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = wall.width;
self.height = wall.height;
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x6abf4b // green grass
});
/****
* Game Code
****/
// Menu overlay container
// --- MENU SYSTEM ---
// Ball: white circle
// Hole: black circle
// Wall: green rectangle
// Arrow: yellow rectangle (for aiming indicator)
var menuOverlay = new Container();
// Menu background (semi-transparent)
var menuBg = LK.getAsset('golfWall', {
anchorX: 0.5,
anchorY: 0.5,
width: 1200,
height: 1200
});
menuBg.alpha = 0.92;
menuBg.tint = 0x222222;
menuOverlay.addChild(menuBg);
menuBg.x = 2048 / 2;
menuBg.y = 2732 / 2;
// Menu title
var menuTitle = new Text2('SELECT MODE', {
size: 140,
fill: 0xFFFFFF
});
menuTitle.anchor.set(0.5, 0.5);
menuTitle.x = 2048 / 2;
menuTitle.y = 2732 / 2 - 400;
menuOverlay.addChild(menuTitle);
// Menu options
var menuOptions = [{
label: "SANDBOX",
desc: "NO TIME LIMIT",
timer: null,
increment: 0
}, {
label: "BEGINNER",
desc: "START: 30s, +5s/hole",
timer: 30,
increment: 5
}, {
label: "PRO",
desc: "START: 10s, +3s/hole",
timer: 10,
increment: 3
}, {
label: "EXPERT",
desc: "START: 3s, +2s/hole",
timer: 3,
increment: 2
}];
var menuButtons = [];
var menuDescLabels = [];
var menuButtonYStart = 2732 / 2 - 180;
var menuButtonSpacing = 220;
for (var i = 0; i < menuOptions.length; i++) {
// Button
var btn = new Text2(menuOptions[i].label, {
size: 120,
fill: 0xFFFF00
});
btn.anchor.set(0.5, 0.5);
btn.x = 2048 / 2;
btn.y = menuButtonYStart + i * menuButtonSpacing;
btn.interactive = true;
btn.buttonMode = true;
menuOverlay.addChild(btn);
menuButtons.push(btn);
// Description
// (Removed description text under menu options)
}
// Add a back button to the menu overlay
var menuBackBtn = new Text2('BACK', {
size: 90,
fill: 0xFF4444
});
menuBackBtn.anchor.set(0.5, 0.5);
menuBackBtn.x = 2048 - 200;
menuBackBtn.y = 200;
menuBackBtn.interactive = true;
menuBackBtn.buttonMode = true;
menuBackBtn.down = function (x, y, obj) {
// Just hide the menu overlay, do not start a game
if (menuOverlay.parent) {
menuOverlay.parent.removeChild(menuOverlay);
}
// Optionally, you could reset to a splash screen or do nothing
// For now, just show the menu again (if needed, you can add more logic here)
game.addChild(menuOverlay);
};
menuOverlay.addChild(menuBackBtn);
// Add menu overlay to game
game.addChild(menuOverlay);
// Hide back button when menu is shown
var backBtn = null;
menuOverlay.onAdded = function () {
if (backBtn) backBtn.visible = false;
};
// Hide game elements until menu is dismissed
var margin = 120;
var courseWidth = 2048 - margin * 2;
var courseHeight = 2732 - margin * 2;
var walls = [];
var topWall, bottomWall, leftWall, rightWall;
var hole, ball;
var strokes = 0;
var score = 0;
var scoreLabel, scoreTxt, timerDuration, timerValue, timerInterval, timerTxt;
var timeIncrement = 0;
var sandboxMode = false;
// Helper to show/hide game elements
function setGameElementsVisible(visible) {
if (topWall) {
topWall.visible = visible;
}
if (bottomWall) {
bottomWall.visible = visible;
}
if (leftWall) {
leftWall.visible = visible;
}
if (rightWall) {
rightWall.visible = visible;
}
if (hole) {
hole.visible = visible;
}
if (ball) {
ball.visible = visible;
}
if (scoreLabel) {
scoreLabel.visible = visible;
}
if (scoreTxt) {
scoreTxt.visible = visible;
}
if (timerTxt) {
timerTxt.visible = visible;
}
if (typeof backBtn !== "undefined" && backBtn !== null) {
backBtn.visible = visible;
}
}
// Remove menu and start game with selected mode
function startGameWithMode(modeIdx) {
// Create back button if not already created
if (typeof backBtn === "undefined" || backBtn === null) {
backBtn = new Text2('BACK', {
size: 90,
fill: 0xFF4444
});
backBtn.anchor.set(0.5, 0.5);
backBtn.x = 2048 - 200;
backBtn.y = 200;
backBtn.interactive = true;
backBtn.buttonMode = true;
backBtn.visible = false;
backBtn.down = function (x, y, obj) {
// Remove all game elements
setGameElementsVisible(false);
// Remove walls
for (var i = 0; i < walls.length; i++) {
if (walls[i] && walls[i].parent) {
walls[i].parent.removeChild(walls[i]);
}
}
walls = [];
// Remove hole and ball
if (hole && hole.parent) {
hole.parent.removeChild(hole);
}
if (ball && ball.parent) {
ball.parent.removeChild(ball);
}
hole = null;
ball = null;
// Remove score and timer
if (scoreLabel && scoreLabel.parent) {
scoreLabel.parent.removeChild(scoreLabel);
}
if (scoreTxt && scoreTxt.parent) {
scoreTxt.parent.removeChild(scoreTxt);
}
if (timerTxt && timerTxt.parent) {
timerTxt.parent.removeChild(timerTxt);
}
// Remove back button
if (backBtn && backBtn.parent) {
backBtn.parent.removeChild(backBtn);
}
// Stop timer if running
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
// Show menu overlay again
if (!menuOverlay.parent) {
game.addChild(menuOverlay);
}
setGameElementsVisible(false);
};
// Always add backBtn after all walls and game elements so it's on top
game.addChild(backBtn);
if (backBtn.parent) {
backBtn.parent.removeChild(backBtn);
game.addChild(backBtn);
}
}
backBtn.visible = true;
if (backBtn.parent) {
backBtn.parent.removeChild(backBtn);
game.addChild(backBtn);
}
// Remove menu overlay
if (menuOverlay.parent) {
menuOverlay.parent.removeChild(menuOverlay);
}
// Set timer and increment
var opt = menuOptions[modeIdx];
sandboxMode = opt.timer === null;
timerDuration = opt.timer === null ? 0 : opt.timer;
timeIncrement = opt.increment;
// Create walls
topWall = new GolfWall();
topWall.width = courseWidth;
topWall.height = 600;
topWall.x = 2048 / 2;
topWall.y = margin - 200;
topWall.children[0].width = courseWidth;
topWall.children[0].height = 120;
walls.push(topWall);
game.addChild(topWall);
bottomWall = new GolfWall();
bottomWall.width = courseWidth;
bottomWall.height = 600;
bottomWall.x = 2048 / 2;
bottomWall.y = 2732 - margin + 200;
bottomWall.children[0].width = courseWidth;
bottomWall.children[0].height = 120;
walls.push(bottomWall);
game.addChild(bottomWall);
leftWall = new GolfWall();
leftWall.width = 350;
leftWall.height = courseHeight;
leftWall.x = margin - 75;
leftWall.y = 2732 / 2;
leftWall.children[0].width = 120;
leftWall.children[0].height = courseHeight;
walls.push(leftWall);
game.addChild(leftWall);
rightWall = new GolfWall();
rightWall.width = 350;
rightWall.height = courseHeight;
rightWall.x = 2048 - margin + 75;
rightWall.y = 2732 / 2;
rightWall.children[0].width = 120;
rightWall.children[0].height = courseHeight;
walls.push(rightWall);
game.addChild(rightWall);
// Create hole
hole = new GolfHole();
hole.x = 2048 - margin - 200;
hole.y = 2732 - margin - 200;
game.addChild(hole);
// Create ball
ball = new GolfBall();
ball.x = margin + 200;
ball.y = 2732 - margin - 400;
game.addChild(ball);
// Strokes and score
strokes = 0;
score = 0;
// Score label (number of holes) at the center of the screen
scoreLabel = new Text2('Score: ' + score, {
size: 100,
fill: 0xFFFFFF
});
scoreLabel.anchor.set(0.5, 0.5);
game.addChild(scoreLabel);
scoreLabel.x = 2048 / 2;
scoreLabel.y = 2732 / 2 - 1100;
// Score text (Strokes) at the top center
scoreTxt = new Text2('Strokes: ' + strokes, {
size: 100,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
scoreTxt.x = 2048 / 2;
scoreTxt.y = 30; // Move score text slightly higher
// Timer variables
timerValue = timerDuration;
timerInterval = null;
// Timer text
timerTxt = new Text2('Time: ' + (sandboxMode ? '∞' : timerValue), {
size: 100,
fill: 0xFFFFFF
});
timerTxt.anchor.set(0.5, 0.5);
game.addChild(timerTxt);
timerTxt.x = 2048 / 2;
timerTxt.y = 2732 / 2;
// Remove the pause button from the GUI overlay if it exists
if (LK.gui.pause) {
LK.gui.pause.visible = false;
if (LK.gui.pause.parent) {
LK.gui.pause.parent.removeChild(LK.gui.pause);
}
}
// Start timer if not sandbox
if (!sandboxMode) {
startTimer();
} else {
timerTxt.setText('Time: ∞');
}
setGameElementsVisible(true);
}
// Attach menu button handlers
for (var i = 0; i < menuButtons.length; i++) {
(function (idx) {
menuButtons[idx].down = function (x, y, obj) {
setGameElementsVisible(false);
startGameWithMode(idx);
};
})(i);
}
// Hide game elements until menu is dismissed
setGameElementsVisible(false);
// --- END MENU SYSTEM ---
// Function to start/reset timer
function startTimer() {
timerValue = timerDuration;
timerTxt.setText('Time: ' + timerValue);
if (timerInterval) {
LK.clearInterval(timerInterval);
}
timerInterval = LK.setInterval(function () {
timerValue -= 1;
if (timerValue < 0) {
timerValue = 0;
}
timerTxt.setText('Time: ' + timerValue);
if (timerValue <= 0) {
LK.clearInterval(timerInterval);
LK.showGameOver();
}
}, 1000);
}
// Drag/aim state
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
// Helper: clamp ball inside course
function clampBallPosition() {
// No clamping to invisible walls; allow ball to go to the very edge of the course.
}
// Helper: ball-wall collision
function handleWallCollisions() {
for (var i = 0; i < walls.length; i++) {
var w = walls[i];
// Axis-aligned rectangle collision
var wx = w.x;
var wy = w.y;
var ww = w.width;
var wh = w.height;
var bx = ball.x;
var by = ball.y;
var r = ball.radius;
// Rectangle bounds
var left = wx - ww / 2;
var right = wx + ww / 2;
var top = wy - wh / 2;
var bottom = wy + wh / 2;
// Find closest point on wall to ball
var closestX = Math.max(left, Math.min(bx, right));
var closestY = Math.max(top, Math.min(by, bottom));
var dx = bx - closestX;
var dy = by - closestY;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < r) {
// Ball is colliding with wall
// Push ball out
var overlap = r - dist + 1;
if (dist === 0) {
// Prevent NaN
dx = 1;
dy = 0;
dist = 1;
}
// Move ball out of wall
ball.x += dx / dist * overlap;
ball.y += dy / dist * overlap;
// Calculate the normal vector
var nx = dx / dist;
var ny = dy / dist;
// Calculate dot product of velocity and normal
var dot = ball.vx * nx + ball.vy * ny;
// Reflect velocity using the normal (like center wall)
ball.vx = ball.vx - 2 * dot * nx;
ball.vy = ball.vy - 2 * dot * ny;
// Dampen speed
ball.vx *= 0.7;
ball.vy *= 0.7;
}
}
}
// Helper: ball-in-hole detection
function checkHole() {
var dx = ball.x - hole.x;
var dy = ball.y - hole.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < hole.radius - ball.radius / 2) {
// Ball in hole! Instantly hide ball and the hole
ball.moving = false;
ball.vx = 0;
ball.vy = 0;
ball.visible = false;
hole.visible = false;
return true;
}
return false;
}
// Helper: reset ball after win (not needed, handled by LK.showYouWin)
// Game move handler (for drag aiming)
function handleMove(x, y, obj) {
if (isDragging && !ball.moving) {
// Clamp drag to max length
var dx = x - dragStartX;
var dy = y - dragStartY;
var maxDrag = 400;
var dragLen = Math.sqrt(dx * dx + dy * dy);
if (dragLen > maxDrag) {
var scale = maxDrag / dragLen;
dx *= scale;
dy *= scale;
}
// Show aim arrow
ball.showArrow(-dx, -dy);
}
}
// Game down handler (start drag)
game.down = function (x, y, obj) {
// Only allow aiming if ball exists, is not moving, and touch is near ball
if (typeof ball !== "undefined" && ball !== null && !ball.moving) {
var dx = x - ball.x;
var dy = y - ball.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < ball.radius + 40) {
isDragging = true;
dragStartX = ball.x;
dragStartY = ball.y;
ball.aimStartX = x;
ball.aimStartY = y;
ball.showArrow(0, 0);
}
}
};
// Game move handler
game.move = function (x, y, obj) {
if (isDragging && !ball.moving) {
// Update aim arrow
var dx = x - dragStartX;
var dy = y - dragStartY;
var maxDrag = 400;
var dragLen = Math.sqrt(dx * dx + dy * dy);
if (dragLen > maxDrag) {
var scale = maxDrag / dragLen;
dx *= scale;
dy *= scale;
}
ball.showArrow(-dx, -dy);
}
};
// Game up handler (release drag, shoot)
game.up = function (x, y, obj) {
if (isDragging && !ball.moving) {
var dx = x - dragStartX;
var dy = y - dragStartY;
// Power proportional to drag length
var power = Math.sqrt(dx * dx + dy * dy);
var maxPower = 38;
var minPower = 10;
var shotPower = Math.min(power / 10, maxPower);
if (shotPower < minPower / 2) {
// Too short, ignore
ball.hideArrow();
isDragging = false;
return;
}
// Set velocity (opposite direction of drag)
var angle = Math.atan2(dy, dx);
ball.vx = -Math.cos(angle) * shotPower;
ball.vy = -Math.sin(angle) * shotPower;
ball.moving = true;
ball.hideArrow();
isDragging = false;
// Increment strokes
strokes += 1;
scoreTxt.setText('Strokes: ' + strokes);
}
};
// Main game update
game.update = function () {
// Ball physics
if (typeof ball !== "undefined" && ball !== null && typeof ball.update === "function") {
ball.update();
}
// Clamp ball inside course
clampBallPosition();
// Ball-wall collisions
handleWallCollisions();
// Ball-in-hole
if (typeof hole !== "undefined" && hole !== null && hole.visible && ball && ball.visible) {
var dx = ball.x - hole.x;
var dy = ball.y - hole.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < hole.radius + ball.radius / 2) {
// Ball touched the hole! Instantly hide the hole, but do not dequeue or hide the ball
if (hole.parent) {
hole.parent.removeChild(hole);
}
hole.visible = false;
// Increment score and update label
score += 1;
scoreLabel.setText('Score: ' + score);
// Add time increment if not sandbox
if (!sandboxMode) {
timerValue = Math.min(timerValue + timeIncrement, 99);
timerTxt.setText('Time: ' + timerValue);
} else {
timerTxt.setText('Time: ∞');
}
// Do not stop the ball when it touches the hole; let it keep moving
// Create a new hole at a random position (not too close to the ball or walls)
var newHole = new GolfHole();
var safeMargin = margin + 200;
var minDistFromBall = 400;
var placed = false;
for (var tries = 0; tries < 20 && !placed; tries++) {
var hx = Math.random() * (2048 - 2 * safeMargin) + safeMargin;
var hy = Math.random() * (2732 - 2 * safeMargin) + safeMargin;
var bdx = hx - ball.x;
var bdy = hy - ball.y;
var bdist = Math.sqrt(bdx * bdx + bdy * bdy);
if (bdist > minDistFromBall) {
newHole.x = hx;
newHole.y = hy;
placed = true;
}
}
if (!placed) {
// fallback: just put it somewhere safe
newHole.x = 2048 / 2;
newHole.y = 2732 / 2;
}
game.addChild(newHole);
hole = newHole;
// No timer reset here, just add time increment above
}
}
};