/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // LetterBubble: A bubble with a letter inside, touchable var LetterBubble = Container.expand(function () { var self = Container.call(this); // Properties to be set after creation: // self.letter (string, e.g. "A") // self.isTarget (bool, is this the correct letter to pop?) // We'll assign a color randomly from the available bubble assets // Pick a random bubble color asset for the background var bubbleAssetIds = ['bubbleBlue', 'bubbleGreen', 'bubbleRed', 'bubbleYellow', 'bubblePurple', 'bubbleOrange']; var bubbleIdx = Math.floor(Math.random() * bubbleAssetIds.length); self.bubbleBg = LK.getAsset(bubbleAssetIds[bubbleIdx], { anchorX: 0.5, anchorY: 0.5 }); self.addChild(self.bubbleBg); // Letter color should contrast with bubble color for readability self.letterText = new Text2('A', { size: 120, fill: 0xFFFFFF, font: "GillSans-Bold,Impact,'Arial Black',Tahoma" }); self.letterText.anchor.set(0.5, 0.5); // Add a subtle drop shadow for pop self.letterText.setStyle({ dropShadow: true, dropShadowColor: "#222", dropShadowBlur: 8, dropShadowDistance: 4 }); self.addChild(self.letterText); // Animate in (pop effect) self.scale.set(0.1, 0.1); tween(self.scale, { x: 1, y: 1 }, { duration: 300, easing: tween.elasticOut }); // Touch event self.down = function (x, y, obj) { // Only allow popping if not already popped if (self.popped) { return; } self.popped = true; onBubbleTapped(self); }; // Pop animation self.pop = function (_onFinish) { // Play pop sound LK.getSound('pop').play(); // Animate: scale up, fade out, then destroy tween(self.scale, { x: 1.3, y: 1.3 }, { duration: 120, easing: tween.easeOut }); tween(self, { alpha: 0 }, { duration: 180, delay: 100, onFinish: function onFinish() { if (_onFinish) { _onFinish(); } self.destroy(); } }); }; // Gentle shake for incorrect self.shake = function () { // Animate left-right shake var origX = self.x; tween(self, { x: origX - 20 }, { duration: 60, easing: tween.easeIn, onFinish: function onFinish() { tween(self, { x: origX + 20 }, { duration: 60, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { x: origX }, { duration: 60 }); } }); } }); }; return self; }); // ZigzagLetterBubble: A harder bubble with zigzag movement and faded color var ZigzagLetterBubble = Container.expand(function () { var self = Container.call(this); // Properties to be set after creation: // self.letter (string) // self.isTarget (bool) // Pick a faded color bubble (use alpha) var bubbleAssetIds = ['bubbleBlue', 'bubbleGreen', 'bubbleRed', 'bubbleYellow', 'bubblePurple', 'bubbleOrange']; var bubbleIdx = Math.floor(Math.random() * bubbleAssetIds.length); self.bubbleBg = LK.getAsset(bubbleAssetIds[bubbleIdx], { anchorX: 0.5, anchorY: 0.5 }); self.bubbleBg.alpha = 0.45 + Math.random() * 0.25; // faded look self.addChild(self.bubbleBg); // Letter text, more faded self.letterText = new Text2('A', { size: 120, fill: 0xFFFFFF, font: "GillSans-Bold,Impact,'Arial Black',Tahoma" }); self.letterText.anchor.set(0.5, 0.5); self.letterText.alpha = 0.7; self.letterText.setStyle({ dropShadow: true, dropShadowColor: "#222", dropShadowBlur: 8, dropShadowDistance: 4 }); self.addChild(self.letterText); // Animate in (pop effect) self.scale.set(0.1, 0.1); tween(self.scale, { x: 1, y: 1 }, { duration: 300, easing: tween.elasticOut }); // Zigzag movement parameters self._zigzagPhase = Math.random() * Math.PI * 2; self._zigzagSpeed = 0.015 + Math.random() * 0.01; // radians per tick self._zigzagAmp = 30 + Math.random() * 30; // amplitude in px self._zigzagBaseX = 0; // will be set on placement // Touch event self.down = function (x, y, obj) { if (self.popped) { return; } self.popped = true; onBubbleTapped(self); }; // Pop animation self.pop = function (_onFinish) { LK.getSound('pop').play(); tween(self.scale, { x: 1.3, y: 1.3 }, { duration: 120, easing: tween.easeOut }); tween(self, { alpha: 0 }, { duration: 180, delay: 100, onFinish: function onFinish() { if (_onFinish) { _onFinish(); } self.destroy(); } }); }; // Gentle shake for incorrect self.shake = function () { var origX = self.x; tween(self, { x: origX - 20 }, { duration: 60, easing: tween.easeIn, onFinish: function onFinish() { tween(self, { x: origX + 20 }, { duration: 60, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { x: origX }, { duration: 60 }); } }); } }); }; // Zigzag update self.update = function () { if (typeof self._zigzagBaseX === "number") { self._zigzagPhase += self._zigzagSpeed; self.x = self._zigzagBaseX + Math.sin(self._zigzagPhase) * self._zigzagAmp; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xE3F6FD // Light blue background for kid-friendly look }); /**** * Game Code ****/ // Simple celebratory sound for win // Sounds for correct/incorrect feedback // We'll use 26 different colors for variety, but for MVP, 5-6 colors are enough and can be reused. // Letter bubbles: We'll use colored ellipses for bubbles, and overlay Text2 for letters. // Alphabet array var alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]; // Game state var selectedGameMode = "endless"; // "endless", "timed", or "hard" var currentLetterIdx = 0; // Index in alphabet for the current target letter var bubbles = []; // Array of LetterBubble instances var lettersPerRound = 4; // Start with 4, will increase as player gets correct answers var correctInARow = 0; // Track correct answers in a row for difficulty var roundActive = false; // Is a round currently active? var score = 0; // Number of correct letters popped // Endless mode: after all letters, reshuffle and continue var endlessMode = true; // Timer variables var timerActive = false; var timerValue = 0; // seconds left var timerInterval = null; var TIMER_START = 119; // seconds to start with when timer mode begins var TIMER_ADD = 3; // seconds to add per correct answer // High score per mode var highScores = { endless: typeof storage.letterpop_highscore_endless !== "undefined" ? storage.letterpop_highscore_endless : 0, timed: typeof storage.letterpop_highscore_timed !== "undefined" ? storage.letterpop_highscore_timed : 0, hard: typeof storage.letterpop_highscore_hard !== "undefined" ? storage.letterpop_highscore_hard : 0 }; var highScore = 0; // Will be set per mode // UI elements var promptText = new Text2('', { size: 110, fill: 0x333333, font: "GillSans-Bold,Impact,'Arial Black',Tahoma" }); promptText.anchor.set(0.5, 0); promptText.y = 90; // Move prompt lower from the very top LK.gui.top.addChild(promptText); var scoreText = new Text2('0', { size: 110, fill: 0x4A90E2, font: "GillSans-Bold,Impact,'Arial Black',Tahoma" }); scoreText.anchor.set(0.5, 1); scoreText.y = -120; // Move it higher above the very bottom // Add scoreText to LK.gui.bottom only on game screens, not on the main screen // (High score text removed from home screen) var highScoreText = new Text2('', { size: 80, fill: 0x4A90E2, font: "GillSans-Bold,Impact,'Arial Black',Tahoma" }); highScoreText.anchor.set(0.5, 0); highScoreText.y = 200; // Place below promptText, but above bubbles highScoreText.visible = false; LK.gui.top.addChild(highScoreText); // Timer text (placed just above the score) var timerText = new Text2('', { size: 80, fill: 0xD0021B, font: "GillSans-Bold,Impact,'Arial Black',Tahoma" }); timerText.anchor.set(0.5, 1); // We'll position timerText just above the score timerText.y = scoreText.y - 100; LK.gui.bottom.addChild(timerText); // Timer change effect (shows +2s/-1s) var timerChangeText = new Text2('', { size: 70, fill: 0x43A047, font: "GillSans-Bold,Impact,'Arial Black',Tahoma" }); timerChangeText.anchor.set(0.5, 1); timerChangeText.alpha = 0; timerChangeText.y = timerText.y - 60; LK.gui.bottom.addChild(timerChangeText); // Game control button (pause/play) above the score - DISABLED/REMOVED var gameControlButton = { visible: false }; var gameControlText = { visible: false }; // Home button to return to main menu - positioned to the right var homeButton = LK.getAsset('bubbleGreen', { anchorX: 0.5, anchorY: 1, scaleX: 0.4, scaleY: 0.4 }); homeButton.y = scoreText.y - 1600; homeButton.x = 580; // Position to the right LK.gui.bottom.addChild(homeButton); var homeButtonText = new Text2('Home', { size: 30, fill: 0xFFFFFF, font: "GillSans-Bold,Impact,'Arial Black',Tahoma" }); homeButtonText.anchor.set(0.5, 0.5); homeButtonText.x = homeButton.x; homeButtonText.y = homeButton.y - 60; LK.gui.bottom.addChild(homeButtonText); homeButton.down = function () { // Stop timer if active if (timerInterval) { LK.clearInterval(timerInterval); timerInterval = null; } timerActive = false; // Complete game reset - remove all bubbles for (var i = 0; i < bubbles.length; i++) { bubbles[i].destroy(); } bubbles = []; // Reset all game state variables currentLetterIdx = 0; score = 0; correctInARow = 0; lettersPerRound = 4; roundActive = false; gamePaused = false; timerValue = 0; // Reset UI elements scoreText.setText('0'); promptText.setText(''); feedbackText.alpha = 0; timerText.setText(''); timerChangeText.alpha = 0; // Hide high score text on home screen if (typeof highScoreText !== "undefined") { highScoreText.visible = false; } // Reset game state and show home screen // Hide home button when returning to home homeButton.visible = false; homeButtonText.visible = false; startGame.hasRun = undefined; startGame(); // Move subtitle lower on home screen if it exists if (typeof subtitle !== "undefined" && subtitle && typeof subtitle.y === "number") { subtitle.y = 1000; } }; homeButtonText.down = homeButton.down; // Pause button disabled: no pause logic or event handlers var gamePaused = false; // Helper to show timer change effect function showTimerChangeEffect(text, color) { timerChangeText.setText(text); timerChangeText.setStyle({ fill: color }); timerChangeText.alpha = 0; timerChangeText.y = timerText.y - 80; tween(timerChangeText, { alpha: 1, y: timerChangeText.y - 40 }, { duration: 200, onFinish: function onFinish() { LK.setTimeout(function () { tween(timerChangeText, { alpha: 0 }, { duration: 300 }); }, 400); } }); } // Feedback text (centered, fades in/out) var feedbackText = new Text2('', { size: 130, fill: 0x43A047, font: "GillSans-Bold,Impact,'Arial Black',Tahoma" }); feedbackText.anchor.set(0.5, 0.5); feedbackText.alpha = 0; LK.gui.center.addChild(feedbackText); // Helper: Shuffle array (Fisher-Yates) function shuffleArray(arr) { var a = arr.slice(); for (var i = a.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var t = a[i]; a[i] = a[j]; a[j] = t; } return a; } // Helper: Start a new round function startRound() { // Remove old bubbles for (var i = 0; i < bubbles.length; i++) { bubbles[i].destroy(); } bubbles = []; if (currentLetterIdx >= alphabet.length) { if (endlessMode) { // Reshuffle for endless play, keep score and difficulty currentLetterIdx = 0; // Shuffle alphabet for next endless cycle alphabet = shuffleArray(alphabet); } else { // All letters done! showCelebration(); return; } } roundActive = true; // Set prompt var targetLetter = alphabet[currentLetterIdx]; promptText.setText("Find: " + targetLetter); // Pick distractor letters (not the target) var distractors = []; var pool = []; for (var i = 0; i < alphabet.length; i++) { if (i !== currentLetterIdx) { pool.push(alphabet[i]); } } pool = shuffleArray(pool); for (var i = 0; i < lettersPerRound - 1; i++) { distractors.push(pool[i]); } // Combine and shuffle var roundLetters = distractors.concat([targetLetter]); roundLetters = shuffleArray(roundLetters); // Dynamically arrange bubbles in a centered grid, scaling and spacing to fit up to 20 bubbles var numBubbles = roundLetters.length; var maxBubbles = 20; var minBubbleSize = 110; var maxBubbleSize = 300; var minMargin = 24; var maxMargin = 60; // Calculate grid: try to make it as square as possible var cols = Math.ceil(Math.sqrt(numBubbles)); var rows = Math.ceil(numBubbles / cols); // Compute available width/height (leave some padding) var padX = 80, padY = 200; var availW = 2048 - padX * 2; var availH = 1800 - padY * 2; // keep bubbles in upper 2/3 of screen // Compute max bubble size that fits var bubbleSizeW = Math.floor((availW - (cols - 1) * minMargin) / cols); var bubbleSizeH = Math.floor((availH - (rows - 1) * minMargin) / rows); var bubbleSize = Math.max(minBubbleSize, Math.min(maxBubbleSize, Math.min(bubbleSizeW, bubbleSizeH))); // Compute margin to center bubbles var marginX = Math.max(minMargin, Math.min(maxMargin, Math.floor((availW - cols * bubbleSize) / Math.max(1, cols - 1)))); var marginY = Math.max(minMargin, Math.min(maxMargin, Math.floor((availH - rows * bubbleSize) / Math.max(1, rows - 1)))); // Compute grid start var totalGridW = cols * bubbleSize + (cols - 1) * marginX; var totalGridH = rows * bubbleSize + (rows - 1) * marginY; // Vertically center grid in the middle of the screen, but never closer than 200px to the top // 2732 is the screen height. Center the grid, but keep at least 200px from the top and 200px from the bottom. var minTop = 200; var minBottom = 200; var availableHeight = 2732 - minTop - minBottom; var gridTop = minTop + Math.max(0, (availableHeight - totalGridH) / 2); var startX = (2048 - totalGridW) / 2 + bubbleSize / 2; var startY = gridTop + bubbleSize / 2; // Place bubbles in grid var positions = []; for (var i = 0; i < numBubbles; i++) { var row = Math.floor(i / cols); var col = i % cols; positions.push({ x: startX + col * (bubbleSize + marginX), y: startY + row * (bubbleSize + marginY) }); } // Timer text is now statically positioned above the score in LK.gui.bottom // Create and add bubbles, scale them to fit for (var i = 0; i < roundLetters.length; i++) { var useZigzag = false; // Hard mode: all bubbles are zigzag/faded if (typeof selectedGameMode !== "undefined" && selectedGameMode === "hard") { useZigzag = true; } else { // Timed/Endless: introduce zigzag as difficulty increases if (roundLetters.length >= 7) { var zigzagCount = Math.floor((roundLetters.length - 6) * 0.7); if (i < zigzagCount) { useZigzag = true; } } } var bubble; if (useZigzag) { bubble = new ZigzagLetterBubble(); } else { bubble = new LetterBubble(); } bubble.letter = roundLetters[i]; bubble.letterText.setText(bubble.letter); bubble.isTarget = bubble.letter === targetLetter; // Position bubble.x = positions[i].x; bubble.y = positions[i].y; // For zigzag, set baseX for zigzag movement if (useZigzag) { bubble._zigzagBaseX = bubble.x; } // Scale bubble to fit var scale = bubbleSize / 300; // 300 is the asset's base size bubble.scale.set(scale, scale); // Also scale letter text for readability bubble.letterText.setStyle({ size: Math.floor(120 * scale) }); // Add to game game.addChild(bubble); bubbles.push(bubble); } } // Handle bubble tap function onBubbleTapped(bubble) { if (!roundActive || gamePaused) { return; } if (bubble.isTarget) { // Correct! roundActive = false; score++; scoreText.setText(score); // Update high score if needed (per mode) if (score > highScore) { highScore = score; highScores[selectedGameMode] = score; highScoreText.setText('High Score: ' + highScore); if (selectedGameMode === "endless") { storage.letterpop_highscore_endless = score; } else if (selectedGameMode === "timed") { storage.letterpop_highscore_timed = score; } else if (selectedGameMode === "hard") { storage.letterpop_highscore_hard = score; } } // Start timer mode after 10 points (only in Timed Mode), or always active in Hard Mode if (selectedGameMode === "timed" && !timerActive && score >= 10 || selectedGameMode === "hard" && !timerActive) { timerActive = true; timerValue = TIMER_START; timerText.setText('Time: ' + timerValue); if (timerInterval) { LK.clearInterval(timerInterval); } timerInterval = LK.setInterval(function () { if (!timerActive) { return; } timerValue--; timerText.setText('Time: ' + timerValue); if (timerValue <= 0) { timerValue = 0; timerText.setText('Time: 0'); endGameTimeout(); } }, 1000); } // Add time for correct answer if timer is active if (timerActive) { timerValue += 2; timerText.setText('Time: ' + timerValue); showTimerChangeEffect("+2s", "#43A047"); } // Track correct answers in a row for difficulty increase if (typeof correctInARow === "undefined") { correctInARow = 0; } correctInARow++; // After every two correct answers, increase number of options (up to 20) if (correctInARow % 2 === 0) { if (typeof lettersPerRound === "undefined") { lettersPerRound = 4; } lettersPerRound = Math.min(lettersPerRound + 1, 20); } // Feedback showFeedback("Great!", "#43A047"); LK.getSound('ding').play(); // Pop animation, then next round bubble.pop(function () { // Remove other bubbles with fade out for (var i = 0; i < bubbles.length; i++) { if (bubbles[i] !== bubble) { tween(bubbles[i], { alpha: 0 }, { duration: 200, onFinish: function (bub) { return function () { bub.destroy(); }; }(bubbles[i]) }); } } // Next letter after short delay LK.setTimeout(function () { currentLetterIdx++; startRound(); }, 600); }); } else { // Reset streak on incorrect answer correctInARow = 0; // Incorrect showFeedback("Try again!", "#D0021B"); LK.getSound('oops').play(); bubble.shake(); // Subtract 1s for wrong answer if timer is active (including Hard Mode) if (timerActive) { timerValue = Math.max(0, timerValue - 1); timerText.setText('Time: ' + timerValue); showTimerChangeEffect("-1s", "#D0021B"); if (timerValue <= 0) { timerValue = 0; timerText.setText('Time: 0'); endGameTimeout(); } } // Allow another try (do not end round) } } // Show feedback text in center, fade in/out function showFeedback(msg, color) { feedbackText.setText(msg); // Use setStyle to update fill color safely feedbackText.setStyle({ fill: color }); feedbackText.alpha = 0; tween(feedbackText, { alpha: 1 }, { duration: 120, onFinish: function onFinish() { LK.setTimeout(function () { tween(feedbackText, { alpha: 0 }, { duration: 200 }); }, 500); } }); } // End game due to timer running out function endGameTimeout() { timerActive = false; if (timerInterval) { LK.clearInterval(timerInterval); timerInterval = null; } // Remove any remaining bubbles for (var i = 0; i < bubbles.length; i++) { bubbles[i].destroy(); } bubbles = []; promptText.setText("Time's up!"); feedbackText.setText("Score: " + score); feedbackText.setStyle({ fill: 0xD0021B }); feedbackText.alpha = 0; tween(feedbackText, { alpha: 1 }, { duration: 300 }); // Play oops sound LK.getSound('oops').play(); // Show game over after a short delay (triggers LK's game over popup) LK.setTimeout(function () { LK.showGameOver(); }, 1200); } // Show celebration screen function showCelebration() { // Remove any remaining bubbles for (var i = 0; i < bubbles.length; i++) { bubbles[i].destroy(); } bubbles = []; promptText.setText("All done!"); feedbackText.setText("You did it!"); // Use setStyle to update fill color safely feedbackText.setStyle({ fill: 0xF5A623 }); feedbackText.alpha = 0; tween(feedbackText, { alpha: 1 }, { duration: 300 }); // Play cheer sound LK.getSound('cheer').play(); // Show "You Win" after a short delay (triggers LK's win popup) LK.setTimeout(function () { LK.showYouWin(); }, 1200); } // Start game function startGame() { // Show a visually appealing home screen before the first round if (typeof startGame.hasRun === "undefined") { // 435px // Helper to create a mode button with text that fits nicely var createModeButton = function createModeButton(bubbleId, label, y, textSize, bubbleScale) { var btn = LK.getAsset(bubbleId, { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: y, scaleX: bubbleScale, scaleY: bubbleScale }); homeScreen.addChild(btn); // Use a slightly smaller font size and allow for two lines if needed var text = new Text2(label, { size: textSize, fill: "#fff", font: "GillSans-Bold,Impact,'Arial Black',Tahoma", align: "center", wordWrap: true, wordWrapWidth: Math.floor(modeBtnBubbleSize * 0.85) }); text.anchor.set(0.5, 0.5); text.x = btn.x; text.y = btn.y; homeScreen.addChild(text); return { btn: btn, text: text }; }; // Endless Mode Button var isFarFromOthers = function isFarFromOthers(x, y, minDist) { for (var i = 0; i < placedPositions.length; i++) { var dx = x - placedPositions[i].x; var dy = y - placedPositions[i].y; if (Math.sqrt(dx * dx + dy * dy) < minDist) { return false; } } return true; }; // Only show on first load, not after game over startGame.hasRun = true; // Create a custom home screen container var homeScreen = new Container(); // Big colorful title var title = new Text2("Letter Pop!", { size: 220, fill: ["#4A90E2", "#F5A623", "#7ED321", "#D0021B"], font: "GillSans-Bold,Impact,'Arial Black',Tahoma" }); title.anchor.set(0.5, 0.5); title.x = 2048 / 2; title.y = 700; homeScreen.addChild(title); // Fun subtitle var subtitle = new Text2("Pop the right letter bubbles!", { size: 90, fill: 0x9013FE, font: "GillSans-Bold,Impact,'Arial Black',Tahoma", align: "center", wordWrap: false // Force single line }); subtitle.anchor.set(0.5, 0.5); subtitle.x = 2048 / 2; subtitle.y = 900; homeScreen.addChild(subtitle); // (Sample letters removed for a cleaner home screen) // Game mode buttons - redesigned for better text fit and visual appeal var modeBtnY = 1450; var modeBtnSpacing = 320; // More vertical space for larger bubbles var modeBtnScale = 1.45; // Larger bubbles for more text room var modeBtnBubbleSize = 300 * modeBtnScale; var endlessBtnObj = createModeButton('bubbleBlue', "Endless\nMode", modeBtnY, 78, modeBtnScale); // Timed Mode Button var timedBtnObj = createModeButton('bubbleOrange', "Timed\nMode", modeBtnY + modeBtnSpacing, 78, modeBtnScale); // Hard Mode Button var hardBtnObj = createModeButton('bubbleRed', "Hard\nMode", modeBtnY + 2 * modeBtnSpacing, 78, modeBtnScale); var endlessBtn = endlessBtnObj.btn; var endlessText = endlessBtnObj.text; var timedBtn = timedBtnObj.btn; var timedText = timedBtnObj.text; var hardBtn = hardBtnObj.btn; var hardText = hardBtnObj.text; // Hide home button on home screen homeButton.visible = false; homeButtonText.visible = false; // Remove scoreText from LK.gui.bottom if present (hide on home screen) if (typeof scoreText !== "undefined" && scoreText.parent === LK.gui.bottom) { LK.gui.bottom.removeChild(scoreText); } // Add homeScreen to game game.addChild(homeScreen); // Button interactions endlessBtn.down = function () { game.removeChild(homeScreen); selectedGameMode = "endless"; // Show home button for gameplay homeButton.visible = true; homeButtonText.visible = true; actuallyStartGame(); }; endlessText.down = endlessBtn.down; timedBtn.down = function () { game.removeChild(homeScreen); selectedGameMode = "timed"; // Show home button for gameplay homeButton.visible = true; homeButtonText.visible = true; actuallyStartGame(); }; timedText.down = timedBtn.down; hardBtn.down = function () { game.removeChild(homeScreen); selectedGameMode = "hard"; // Show home button for gameplay homeButton.visible = true; homeButtonText.visible = true; actuallyStartGame(); }; hardText.down = hardBtn.down; // Don't start the game yet! return; } actuallyStartGame(); function actuallyStartGame() { currentLetterIdx = 0; score = 0; scoreText.setText(score); feedbackText.alpha = 0; correctInARow = 0; lettersPerRound = 4; // Add scoreText to LK.gui.bottom if not already present (show on game screens) if (typeof scoreText !== "undefined" && scoreText.parent !== LK.gui.bottom) { LK.gui.bottom.addChild(scoreText); } // Set game mode defaults if (typeof selectedGameMode === "undefined") { selectedGameMode = "endless"; } // Endless Mode: no timer, endless play, normal bubbles // Timed Mode: timer, increasing difficulty, normal/zigzag bubbles // Hard Mode: no timer, all bubbles zigzag/faded, increasing count if (selectedGameMode === "endless") { endlessMode = true; timerActive = false; timerValue = 0; timerText.setText(''); if (timerInterval) { LK.clearInterval(timerInterval); timerInterval = null; } } else if (selectedGameMode === "timed") { endlessMode = false; timerActive = true; timerValue = TIMER_START; timerText.setText('Time: ' + timerValue); if (timerInterval) { LK.clearInterval(timerInterval); timerInterval = null; } timerInterval = LK.setInterval(function () { if (!timerActive) { return; } timerValue--; timerText.setText('Time: ' + timerValue); if (timerValue <= 0) { timerValue = 0; timerText.setText('Time: 0'); endGameTimeout(); } }, 1000); } else if (selectedGameMode === "hard") { endlessMode = false; timerActive = true; timerValue = TIMER_START; timerText.setText('Time: ' + timerValue); if (timerInterval) { LK.clearInterval(timerInterval); timerInterval = null; } timerInterval = LK.setInterval(function () { if (!timerActive) { return; } timerValue--; timerText.setText('Time: ' + timerValue); if (timerValue <= 0) { timerValue = 0; timerText.setText('Time: 0'); endGameTimeout(); } }, 1000); } // Reload high score from storage for the selected mode if (selectedGameMode === "endless") { highScore = typeof storage.letterpop_highscore_endless !== "undefined" ? storage.letterpop_highscore_endless : 0; } else if (selectedGameMode === "timed") { highScore = typeof storage.letterpop_highscore_timed !== "undefined" ? storage.letterpop_highscore_timed : 0; } else if (selectedGameMode === "hard") { highScore = typeof storage.letterpop_highscore_hard !== "undefined" ? storage.letterpop_highscore_hard : 0; } else { highScore = 0; } if (typeof highScoreText !== "undefined") { highScoreText.visible = true; highScoreText.setText('High Score: ' + highScore); } startRound(); } } // Start on load startGame(); // Animate zigzag bubbles (if any) each frame game.update = function () { for (var i = 0; i < bubbles.length; i++) { if (typeof bubbles[i].update === "function") { bubbles[i].update(); } } }; // Touchscreen: No drag/move needed, only tap (down) on bubbles // Make sure no elements are in top-left 100x100 (all UI is top/center/right);
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// LetterBubble: A bubble with a letter inside, touchable
var LetterBubble = Container.expand(function () {
var self = Container.call(this);
// Properties to be set after creation:
// self.letter (string, e.g. "A")
// self.isTarget (bool, is this the correct letter to pop?)
// We'll assign a color randomly from the available bubble assets
// Pick a random bubble color asset for the background
var bubbleAssetIds = ['bubbleBlue', 'bubbleGreen', 'bubbleRed', 'bubbleYellow', 'bubblePurple', 'bubbleOrange'];
var bubbleIdx = Math.floor(Math.random() * bubbleAssetIds.length);
self.bubbleBg = LK.getAsset(bubbleAssetIds[bubbleIdx], {
anchorX: 0.5,
anchorY: 0.5
});
self.addChild(self.bubbleBg);
// Letter color should contrast with bubble color for readability
self.letterText = new Text2('A', {
size: 120,
fill: 0xFFFFFF,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
self.letterText.anchor.set(0.5, 0.5);
// Add a subtle drop shadow for pop
self.letterText.setStyle({
dropShadow: true,
dropShadowColor: "#222",
dropShadowBlur: 8,
dropShadowDistance: 4
});
self.addChild(self.letterText);
// Animate in (pop effect)
self.scale.set(0.1, 0.1);
tween(self.scale, {
x: 1,
y: 1
}, {
duration: 300,
easing: tween.elasticOut
});
// Touch event
self.down = function (x, y, obj) {
// Only allow popping if not already popped
if (self.popped) {
return;
}
self.popped = true;
onBubbleTapped(self);
};
// Pop animation
self.pop = function (_onFinish) {
// Play pop sound
LK.getSound('pop').play();
// Animate: scale up, fade out, then destroy
tween(self.scale, {
x: 1.3,
y: 1.3
}, {
duration: 120,
easing: tween.easeOut
});
tween(self, {
alpha: 0
}, {
duration: 180,
delay: 100,
onFinish: function onFinish() {
if (_onFinish) {
_onFinish();
}
self.destroy();
}
});
};
// Gentle shake for incorrect
self.shake = function () {
// Animate left-right shake
var origX = self.x;
tween(self, {
x: origX - 20
}, {
duration: 60,
easing: tween.easeIn,
onFinish: function onFinish() {
tween(self, {
x: origX + 20
}, {
duration: 60,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
x: origX
}, {
duration: 60
});
}
});
}
});
};
return self;
});
// ZigzagLetterBubble: A harder bubble with zigzag movement and faded color
var ZigzagLetterBubble = Container.expand(function () {
var self = Container.call(this);
// Properties to be set after creation:
// self.letter (string)
// self.isTarget (bool)
// Pick a faded color bubble (use alpha)
var bubbleAssetIds = ['bubbleBlue', 'bubbleGreen', 'bubbleRed', 'bubbleYellow', 'bubblePurple', 'bubbleOrange'];
var bubbleIdx = Math.floor(Math.random() * bubbleAssetIds.length);
self.bubbleBg = LK.getAsset(bubbleAssetIds[bubbleIdx], {
anchorX: 0.5,
anchorY: 0.5
});
self.bubbleBg.alpha = 0.45 + Math.random() * 0.25; // faded look
self.addChild(self.bubbleBg);
// Letter text, more faded
self.letterText = new Text2('A', {
size: 120,
fill: 0xFFFFFF,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
self.letterText.anchor.set(0.5, 0.5);
self.letterText.alpha = 0.7;
self.letterText.setStyle({
dropShadow: true,
dropShadowColor: "#222",
dropShadowBlur: 8,
dropShadowDistance: 4
});
self.addChild(self.letterText);
// Animate in (pop effect)
self.scale.set(0.1, 0.1);
tween(self.scale, {
x: 1,
y: 1
}, {
duration: 300,
easing: tween.elasticOut
});
// Zigzag movement parameters
self._zigzagPhase = Math.random() * Math.PI * 2;
self._zigzagSpeed = 0.015 + Math.random() * 0.01; // radians per tick
self._zigzagAmp = 30 + Math.random() * 30; // amplitude in px
self._zigzagBaseX = 0; // will be set on placement
// Touch event
self.down = function (x, y, obj) {
if (self.popped) {
return;
}
self.popped = true;
onBubbleTapped(self);
};
// Pop animation
self.pop = function (_onFinish) {
LK.getSound('pop').play();
tween(self.scale, {
x: 1.3,
y: 1.3
}, {
duration: 120,
easing: tween.easeOut
});
tween(self, {
alpha: 0
}, {
duration: 180,
delay: 100,
onFinish: function onFinish() {
if (_onFinish) {
_onFinish();
}
self.destroy();
}
});
};
// Gentle shake for incorrect
self.shake = function () {
var origX = self.x;
tween(self, {
x: origX - 20
}, {
duration: 60,
easing: tween.easeIn,
onFinish: function onFinish() {
tween(self, {
x: origX + 20
}, {
duration: 60,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
x: origX
}, {
duration: 60
});
}
});
}
});
};
// Zigzag update
self.update = function () {
if (typeof self._zigzagBaseX === "number") {
self._zigzagPhase += self._zigzagSpeed;
self.x = self._zigzagBaseX + Math.sin(self._zigzagPhase) * self._zigzagAmp;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xE3F6FD // Light blue background for kid-friendly look
});
/****
* Game Code
****/
// Simple celebratory sound for win
// Sounds for correct/incorrect feedback
// We'll use 26 different colors for variety, but for MVP, 5-6 colors are enough and can be reused.
// Letter bubbles: We'll use colored ellipses for bubbles, and overlay Text2 for letters.
// Alphabet array
var alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"];
// Game state
var selectedGameMode = "endless"; // "endless", "timed", or "hard"
var currentLetterIdx = 0; // Index in alphabet for the current target letter
var bubbles = []; // Array of LetterBubble instances
var lettersPerRound = 4; // Start with 4, will increase as player gets correct answers
var correctInARow = 0; // Track correct answers in a row for difficulty
var roundActive = false; // Is a round currently active?
var score = 0; // Number of correct letters popped
// Endless mode: after all letters, reshuffle and continue
var endlessMode = true;
// Timer variables
var timerActive = false;
var timerValue = 0; // seconds left
var timerInterval = null;
var TIMER_START = 119; // seconds to start with when timer mode begins
var TIMER_ADD = 3; // seconds to add per correct answer
// High score per mode
var highScores = {
endless: typeof storage.letterpop_highscore_endless !== "undefined" ? storage.letterpop_highscore_endless : 0,
timed: typeof storage.letterpop_highscore_timed !== "undefined" ? storage.letterpop_highscore_timed : 0,
hard: typeof storage.letterpop_highscore_hard !== "undefined" ? storage.letterpop_highscore_hard : 0
};
var highScore = 0; // Will be set per mode
// UI elements
var promptText = new Text2('', {
size: 110,
fill: 0x333333,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
promptText.anchor.set(0.5, 0);
promptText.y = 90; // Move prompt lower from the very top
LK.gui.top.addChild(promptText);
var scoreText = new Text2('0', {
size: 110,
fill: 0x4A90E2,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
scoreText.anchor.set(0.5, 1);
scoreText.y = -120; // Move it higher above the very bottom
// Add scoreText to LK.gui.bottom only on game screens, not on the main screen
// (High score text removed from home screen)
var highScoreText = new Text2('', {
size: 80,
fill: 0x4A90E2,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
highScoreText.anchor.set(0.5, 0);
highScoreText.y = 200; // Place below promptText, but above bubbles
highScoreText.visible = false;
LK.gui.top.addChild(highScoreText);
// Timer text (placed just above the score)
var timerText = new Text2('', {
size: 80,
fill: 0xD0021B,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
timerText.anchor.set(0.5, 1);
// We'll position timerText just above the score
timerText.y = scoreText.y - 100;
LK.gui.bottom.addChild(timerText);
// Timer change effect (shows +2s/-1s)
var timerChangeText = new Text2('', {
size: 70,
fill: 0x43A047,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
timerChangeText.anchor.set(0.5, 1);
timerChangeText.alpha = 0;
timerChangeText.y = timerText.y - 60;
LK.gui.bottom.addChild(timerChangeText);
// Game control button (pause/play) above the score - DISABLED/REMOVED
var gameControlButton = {
visible: false
};
var gameControlText = {
visible: false
};
// Home button to return to main menu - positioned to the right
var homeButton = LK.getAsset('bubbleGreen', {
anchorX: 0.5,
anchorY: 1,
scaleX: 0.4,
scaleY: 0.4
});
homeButton.y = scoreText.y - 1600;
homeButton.x = 580; // Position to the right
LK.gui.bottom.addChild(homeButton);
var homeButtonText = new Text2('Home', {
size: 30,
fill: 0xFFFFFF,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
homeButtonText.anchor.set(0.5, 0.5);
homeButtonText.x = homeButton.x;
homeButtonText.y = homeButton.y - 60;
LK.gui.bottom.addChild(homeButtonText);
homeButton.down = function () {
// Stop timer if active
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
timerActive = false;
// Complete game reset - remove all bubbles
for (var i = 0; i < bubbles.length; i++) {
bubbles[i].destroy();
}
bubbles = [];
// Reset all game state variables
currentLetterIdx = 0;
score = 0;
correctInARow = 0;
lettersPerRound = 4;
roundActive = false;
gamePaused = false;
timerValue = 0;
// Reset UI elements
scoreText.setText('0');
promptText.setText('');
feedbackText.alpha = 0;
timerText.setText('');
timerChangeText.alpha = 0;
// Hide high score text on home screen
if (typeof highScoreText !== "undefined") {
highScoreText.visible = false;
}
// Reset game state and show home screen
// Hide home button when returning to home
homeButton.visible = false;
homeButtonText.visible = false;
startGame.hasRun = undefined;
startGame();
// Move subtitle lower on home screen if it exists
if (typeof subtitle !== "undefined" && subtitle && typeof subtitle.y === "number") {
subtitle.y = 1000;
}
};
homeButtonText.down = homeButton.down;
// Pause button disabled: no pause logic or event handlers
var gamePaused = false;
// Helper to show timer change effect
function showTimerChangeEffect(text, color) {
timerChangeText.setText(text);
timerChangeText.setStyle({
fill: color
});
timerChangeText.alpha = 0;
timerChangeText.y = timerText.y - 80;
tween(timerChangeText, {
alpha: 1,
y: timerChangeText.y - 40
}, {
duration: 200,
onFinish: function onFinish() {
LK.setTimeout(function () {
tween(timerChangeText, {
alpha: 0
}, {
duration: 300
});
}, 400);
}
});
}
// Feedback text (centered, fades in/out)
var feedbackText = new Text2('', {
size: 130,
fill: 0x43A047,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
feedbackText.anchor.set(0.5, 0.5);
feedbackText.alpha = 0;
LK.gui.center.addChild(feedbackText);
// Helper: Shuffle array (Fisher-Yates)
function shuffleArray(arr) {
var a = arr.slice();
for (var i = a.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = a[i];
a[i] = a[j];
a[j] = t;
}
return a;
}
// Helper: Start a new round
function startRound() {
// Remove old bubbles
for (var i = 0; i < bubbles.length; i++) {
bubbles[i].destroy();
}
bubbles = [];
if (currentLetterIdx >= alphabet.length) {
if (endlessMode) {
// Reshuffle for endless play, keep score and difficulty
currentLetterIdx = 0;
// Shuffle alphabet for next endless cycle
alphabet = shuffleArray(alphabet);
} else {
// All letters done!
showCelebration();
return;
}
}
roundActive = true;
// Set prompt
var targetLetter = alphabet[currentLetterIdx];
promptText.setText("Find: " + targetLetter);
// Pick distractor letters (not the target)
var distractors = [];
var pool = [];
for (var i = 0; i < alphabet.length; i++) {
if (i !== currentLetterIdx) {
pool.push(alphabet[i]);
}
}
pool = shuffleArray(pool);
for (var i = 0; i < lettersPerRound - 1; i++) {
distractors.push(pool[i]);
}
// Combine and shuffle
var roundLetters = distractors.concat([targetLetter]);
roundLetters = shuffleArray(roundLetters);
// Dynamically arrange bubbles in a centered grid, scaling and spacing to fit up to 20 bubbles
var numBubbles = roundLetters.length;
var maxBubbles = 20;
var minBubbleSize = 110;
var maxBubbleSize = 300;
var minMargin = 24;
var maxMargin = 60;
// Calculate grid: try to make it as square as possible
var cols = Math.ceil(Math.sqrt(numBubbles));
var rows = Math.ceil(numBubbles / cols);
// Compute available width/height (leave some padding)
var padX = 80,
padY = 200;
var availW = 2048 - padX * 2;
var availH = 1800 - padY * 2; // keep bubbles in upper 2/3 of screen
// Compute max bubble size that fits
var bubbleSizeW = Math.floor((availW - (cols - 1) * minMargin) / cols);
var bubbleSizeH = Math.floor((availH - (rows - 1) * minMargin) / rows);
var bubbleSize = Math.max(minBubbleSize, Math.min(maxBubbleSize, Math.min(bubbleSizeW, bubbleSizeH)));
// Compute margin to center bubbles
var marginX = Math.max(minMargin, Math.min(maxMargin, Math.floor((availW - cols * bubbleSize) / Math.max(1, cols - 1))));
var marginY = Math.max(minMargin, Math.min(maxMargin, Math.floor((availH - rows * bubbleSize) / Math.max(1, rows - 1))));
// Compute grid start
var totalGridW = cols * bubbleSize + (cols - 1) * marginX;
var totalGridH = rows * bubbleSize + (rows - 1) * marginY;
// Vertically center grid in the middle of the screen, but never closer than 200px to the top
// 2732 is the screen height. Center the grid, but keep at least 200px from the top and 200px from the bottom.
var minTop = 200;
var minBottom = 200;
var availableHeight = 2732 - minTop - minBottom;
var gridTop = minTop + Math.max(0, (availableHeight - totalGridH) / 2);
var startX = (2048 - totalGridW) / 2 + bubbleSize / 2;
var startY = gridTop + bubbleSize / 2;
// Place bubbles in grid
var positions = [];
for (var i = 0; i < numBubbles; i++) {
var row = Math.floor(i / cols);
var col = i % cols;
positions.push({
x: startX + col * (bubbleSize + marginX),
y: startY + row * (bubbleSize + marginY)
});
}
// Timer text is now statically positioned above the score in LK.gui.bottom
// Create and add bubbles, scale them to fit
for (var i = 0; i < roundLetters.length; i++) {
var useZigzag = false;
// Hard mode: all bubbles are zigzag/faded
if (typeof selectedGameMode !== "undefined" && selectedGameMode === "hard") {
useZigzag = true;
} else {
// Timed/Endless: introduce zigzag as difficulty increases
if (roundLetters.length >= 7) {
var zigzagCount = Math.floor((roundLetters.length - 6) * 0.7);
if (i < zigzagCount) {
useZigzag = true;
}
}
}
var bubble;
if (useZigzag) {
bubble = new ZigzagLetterBubble();
} else {
bubble = new LetterBubble();
}
bubble.letter = roundLetters[i];
bubble.letterText.setText(bubble.letter);
bubble.isTarget = bubble.letter === targetLetter;
// Position
bubble.x = positions[i].x;
bubble.y = positions[i].y;
// For zigzag, set baseX for zigzag movement
if (useZigzag) {
bubble._zigzagBaseX = bubble.x;
}
// Scale bubble to fit
var scale = bubbleSize / 300; // 300 is the asset's base size
bubble.scale.set(scale, scale);
// Also scale letter text for readability
bubble.letterText.setStyle({
size: Math.floor(120 * scale)
});
// Add to game
game.addChild(bubble);
bubbles.push(bubble);
}
}
// Handle bubble tap
function onBubbleTapped(bubble) {
if (!roundActive || gamePaused) {
return;
}
if (bubble.isTarget) {
// Correct!
roundActive = false;
score++;
scoreText.setText(score);
// Update high score if needed (per mode)
if (score > highScore) {
highScore = score;
highScores[selectedGameMode] = score;
highScoreText.setText('High Score: ' + highScore);
if (selectedGameMode === "endless") {
storage.letterpop_highscore_endless = score;
} else if (selectedGameMode === "timed") {
storage.letterpop_highscore_timed = score;
} else if (selectedGameMode === "hard") {
storage.letterpop_highscore_hard = score;
}
}
// Start timer mode after 10 points (only in Timed Mode), or always active in Hard Mode
if (selectedGameMode === "timed" && !timerActive && score >= 10 || selectedGameMode === "hard" && !timerActive) {
timerActive = true;
timerValue = TIMER_START;
timerText.setText('Time: ' + timerValue);
if (timerInterval) {
LK.clearInterval(timerInterval);
}
timerInterval = LK.setInterval(function () {
if (!timerActive) {
return;
}
timerValue--;
timerText.setText('Time: ' + timerValue);
if (timerValue <= 0) {
timerValue = 0;
timerText.setText('Time: 0');
endGameTimeout();
}
}, 1000);
}
// Add time for correct answer if timer is active
if (timerActive) {
timerValue += 2;
timerText.setText('Time: ' + timerValue);
showTimerChangeEffect("+2s", "#43A047");
}
// Track correct answers in a row for difficulty increase
if (typeof correctInARow === "undefined") {
correctInARow = 0;
}
correctInARow++;
// After every two correct answers, increase number of options (up to 20)
if (correctInARow % 2 === 0) {
if (typeof lettersPerRound === "undefined") {
lettersPerRound = 4;
}
lettersPerRound = Math.min(lettersPerRound + 1, 20);
}
// Feedback
showFeedback("Great!", "#43A047");
LK.getSound('ding').play();
// Pop animation, then next round
bubble.pop(function () {
// Remove other bubbles with fade out
for (var i = 0; i < bubbles.length; i++) {
if (bubbles[i] !== bubble) {
tween(bubbles[i], {
alpha: 0
}, {
duration: 200,
onFinish: function (bub) {
return function () {
bub.destroy();
};
}(bubbles[i])
});
}
}
// Next letter after short delay
LK.setTimeout(function () {
currentLetterIdx++;
startRound();
}, 600);
});
} else {
// Reset streak on incorrect answer
correctInARow = 0;
// Incorrect
showFeedback("Try again!", "#D0021B");
LK.getSound('oops').play();
bubble.shake();
// Subtract 1s for wrong answer if timer is active (including Hard Mode)
if (timerActive) {
timerValue = Math.max(0, timerValue - 1);
timerText.setText('Time: ' + timerValue);
showTimerChangeEffect("-1s", "#D0021B");
if (timerValue <= 0) {
timerValue = 0;
timerText.setText('Time: 0');
endGameTimeout();
}
}
// Allow another try (do not end round)
}
}
// Show feedback text in center, fade in/out
function showFeedback(msg, color) {
feedbackText.setText(msg);
// Use setStyle to update fill color safely
feedbackText.setStyle({
fill: color
});
feedbackText.alpha = 0;
tween(feedbackText, {
alpha: 1
}, {
duration: 120,
onFinish: function onFinish() {
LK.setTimeout(function () {
tween(feedbackText, {
alpha: 0
}, {
duration: 200
});
}, 500);
}
});
}
// End game due to timer running out
function endGameTimeout() {
timerActive = false;
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
// Remove any remaining bubbles
for (var i = 0; i < bubbles.length; i++) {
bubbles[i].destroy();
}
bubbles = [];
promptText.setText("Time's up!");
feedbackText.setText("Score: " + score);
feedbackText.setStyle({
fill: 0xD0021B
});
feedbackText.alpha = 0;
tween(feedbackText, {
alpha: 1
}, {
duration: 300
});
// Play oops sound
LK.getSound('oops').play();
// Show game over after a short delay (triggers LK's game over popup)
LK.setTimeout(function () {
LK.showGameOver();
}, 1200);
}
// Show celebration screen
function showCelebration() {
// Remove any remaining bubbles
for (var i = 0; i < bubbles.length; i++) {
bubbles[i].destroy();
}
bubbles = [];
promptText.setText("All done!");
feedbackText.setText("You did it!");
// Use setStyle to update fill color safely
feedbackText.setStyle({
fill: 0xF5A623
});
feedbackText.alpha = 0;
tween(feedbackText, {
alpha: 1
}, {
duration: 300
});
// Play cheer sound
LK.getSound('cheer').play();
// Show "You Win" after a short delay (triggers LK's win popup)
LK.setTimeout(function () {
LK.showYouWin();
}, 1200);
}
// Start game
function startGame() {
// Show a visually appealing home screen before the first round
if (typeof startGame.hasRun === "undefined") {
// 435px
// Helper to create a mode button with text that fits nicely
var createModeButton = function createModeButton(bubbleId, label, y, textSize, bubbleScale) {
var btn = LK.getAsset(bubbleId, {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: y,
scaleX: bubbleScale,
scaleY: bubbleScale
});
homeScreen.addChild(btn);
// Use a slightly smaller font size and allow for two lines if needed
var text = new Text2(label, {
size: textSize,
fill: "#fff",
font: "GillSans-Bold,Impact,'Arial Black',Tahoma",
align: "center",
wordWrap: true,
wordWrapWidth: Math.floor(modeBtnBubbleSize * 0.85)
});
text.anchor.set(0.5, 0.5);
text.x = btn.x;
text.y = btn.y;
homeScreen.addChild(text);
return {
btn: btn,
text: text
};
}; // Endless Mode Button
var isFarFromOthers = function isFarFromOthers(x, y, minDist) {
for (var i = 0; i < placedPositions.length; i++) {
var dx = x - placedPositions[i].x;
var dy = y - placedPositions[i].y;
if (Math.sqrt(dx * dx + dy * dy) < minDist) {
return false;
}
}
return true;
};
// Only show on first load, not after game over
startGame.hasRun = true;
// Create a custom home screen container
var homeScreen = new Container();
// Big colorful title
var title = new Text2("Letter Pop!", {
size: 220,
fill: ["#4A90E2", "#F5A623", "#7ED321", "#D0021B"],
font: "GillSans-Bold,Impact,'Arial Black',Tahoma"
});
title.anchor.set(0.5, 0.5);
title.x = 2048 / 2;
title.y = 700;
homeScreen.addChild(title);
// Fun subtitle
var subtitle = new Text2("Pop the right letter bubbles!", {
size: 90,
fill: 0x9013FE,
font: "GillSans-Bold,Impact,'Arial Black',Tahoma",
align: "center",
wordWrap: false // Force single line
});
subtitle.anchor.set(0.5, 0.5);
subtitle.x = 2048 / 2;
subtitle.y = 900;
homeScreen.addChild(subtitle);
// (Sample letters removed for a cleaner home screen)
// Game mode buttons - redesigned for better text fit and visual appeal
var modeBtnY = 1450;
var modeBtnSpacing = 320; // More vertical space for larger bubbles
var modeBtnScale = 1.45; // Larger bubbles for more text room
var modeBtnBubbleSize = 300 * modeBtnScale;
var endlessBtnObj = createModeButton('bubbleBlue', "Endless\nMode", modeBtnY, 78, modeBtnScale);
// Timed Mode Button
var timedBtnObj = createModeButton('bubbleOrange', "Timed\nMode", modeBtnY + modeBtnSpacing, 78, modeBtnScale);
// Hard Mode Button
var hardBtnObj = createModeButton('bubbleRed', "Hard\nMode", modeBtnY + 2 * modeBtnSpacing, 78, modeBtnScale);
var endlessBtn = endlessBtnObj.btn;
var endlessText = endlessBtnObj.text;
var timedBtn = timedBtnObj.btn;
var timedText = timedBtnObj.text;
var hardBtn = hardBtnObj.btn;
var hardText = hardBtnObj.text;
// Hide home button on home screen
homeButton.visible = false;
homeButtonText.visible = false;
// Remove scoreText from LK.gui.bottom if present (hide on home screen)
if (typeof scoreText !== "undefined" && scoreText.parent === LK.gui.bottom) {
LK.gui.bottom.removeChild(scoreText);
}
// Add homeScreen to game
game.addChild(homeScreen);
// Button interactions
endlessBtn.down = function () {
game.removeChild(homeScreen);
selectedGameMode = "endless";
// Show home button for gameplay
homeButton.visible = true;
homeButtonText.visible = true;
actuallyStartGame();
};
endlessText.down = endlessBtn.down;
timedBtn.down = function () {
game.removeChild(homeScreen);
selectedGameMode = "timed";
// Show home button for gameplay
homeButton.visible = true;
homeButtonText.visible = true;
actuallyStartGame();
};
timedText.down = timedBtn.down;
hardBtn.down = function () {
game.removeChild(homeScreen);
selectedGameMode = "hard";
// Show home button for gameplay
homeButton.visible = true;
homeButtonText.visible = true;
actuallyStartGame();
};
hardText.down = hardBtn.down;
// Don't start the game yet!
return;
}
actuallyStartGame();
function actuallyStartGame() {
currentLetterIdx = 0;
score = 0;
scoreText.setText(score);
feedbackText.alpha = 0;
correctInARow = 0;
lettersPerRound = 4;
// Add scoreText to LK.gui.bottom if not already present (show on game screens)
if (typeof scoreText !== "undefined" && scoreText.parent !== LK.gui.bottom) {
LK.gui.bottom.addChild(scoreText);
}
// Set game mode defaults
if (typeof selectedGameMode === "undefined") {
selectedGameMode = "endless";
}
// Endless Mode: no timer, endless play, normal bubbles
// Timed Mode: timer, increasing difficulty, normal/zigzag bubbles
// Hard Mode: no timer, all bubbles zigzag/faded, increasing count
if (selectedGameMode === "endless") {
endlessMode = true;
timerActive = false;
timerValue = 0;
timerText.setText('');
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
} else if (selectedGameMode === "timed") {
endlessMode = false;
timerActive = true;
timerValue = TIMER_START;
timerText.setText('Time: ' + timerValue);
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
timerInterval = LK.setInterval(function () {
if (!timerActive) {
return;
}
timerValue--;
timerText.setText('Time: ' + timerValue);
if (timerValue <= 0) {
timerValue = 0;
timerText.setText('Time: 0');
endGameTimeout();
}
}, 1000);
} else if (selectedGameMode === "hard") {
endlessMode = false;
timerActive = true;
timerValue = TIMER_START;
timerText.setText('Time: ' + timerValue);
if (timerInterval) {
LK.clearInterval(timerInterval);
timerInterval = null;
}
timerInterval = LK.setInterval(function () {
if (!timerActive) {
return;
}
timerValue--;
timerText.setText('Time: ' + timerValue);
if (timerValue <= 0) {
timerValue = 0;
timerText.setText('Time: 0');
endGameTimeout();
}
}, 1000);
}
// Reload high score from storage for the selected mode
if (selectedGameMode === "endless") {
highScore = typeof storage.letterpop_highscore_endless !== "undefined" ? storage.letterpop_highscore_endless : 0;
} else if (selectedGameMode === "timed") {
highScore = typeof storage.letterpop_highscore_timed !== "undefined" ? storage.letterpop_highscore_timed : 0;
} else if (selectedGameMode === "hard") {
highScore = typeof storage.letterpop_highscore_hard !== "undefined" ? storage.letterpop_highscore_hard : 0;
} else {
highScore = 0;
}
if (typeof highScoreText !== "undefined") {
highScoreText.visible = true;
highScoreText.setText('High Score: ' + highScore);
}
startRound();
}
}
// Start on load
startGame();
// Animate zigzag bubbles (if any) each frame
game.update = function () {
for (var i = 0; i < bubbles.length; i++) {
if (typeof bubbles[i].update === "function") {
bubbles[i].update();
}
}
};
// Touchscreen: No drag/move needed, only tap (down) on bubbles
// Make sure no elements are in top-left 100x100 (all UI is top/center/right);