/**** * Classes ****/ var Barrier = Container.expand(function () { var self = Container.call(this); var barrierGraphics = self.attachAsset('barrier', { anchorX: .5, anchorY: .5 }); }); var BonusUX = Container.expand(function () { var self = Container.call(this); // No ironbubble class or dummy logic is present, so nothing to remove here. All ironbubble logic will use Bubble class. var barHeight = 50; var bonusLabel = self.addChild(new Text2('Streak Bonus', { size: 90, fill: 0xF4F5FF, font: "Impact" })); var bonusAmountLabel = self.addChild(new Text2('1x', { size: 170, fill: 0xF4F5FF, font: "Impact" })); bonusLabel.anchor.set(1, 1); bonusLabel.y = barHeight / 2; var rightMargin = -10; bonusLabel.x = rightMargin; bonusAmountLabel.anchor.set(.5, .5); bonusAmountLabel.x = 100; var bonusBarWidth = bonusLabel.width; var bonusBarStart = self.attachAsset('bonusend', { y: 30, x: -bonusBarWidth + rightMargin }); var bonuseBarEnd = self.attachAsset('bonusend', { y: 30, x: -bonusBarWidth + rightMargin }); var bonusBarMiddle = self.attachAsset('bonusbarmiddle', { y: 30, x: -bonusBarWidth + rightMargin + barHeight / 2, width: 0 }); self.x = game.width - 270; self.y = game.height - 145; var bonusBarStepSize = (bonusBarWidth - barHeight) / 5; var targetWidth = 0; var currentWidth = 0; var jumpToAtEnd = 0; self.bonusAmount = 1; self.streakCount = 0; var maxLevel = 40; self.setStreakCount = function (level) { self.streakCount = Math.min(level, maxLevel); var newBonus = Math.floor(self.streakCount / 5) + 1; if (newBonus != self.bonusAmount) { for (var a = 0; a < scoreMultipliers.length; a++) { scoreMultipliers[a].setMultiplier(newBonus); } } self.bonusAmount = newBonus; bonusAmountLabel.setText(self.bonusAmount + 'x'); var newbarpos = level >= maxLevel ? 5 : level % 5; targetWidth = newbarpos * bonusBarStepSize; jumpToAtEnd = targetWidth; if (newbarpos == 0 && level > 0) { targetWidth = 5 * bonusBarStepSize; jumpToAtEnd = 0; } }; self.update = function () { var delta = targetWidth - currentWidth; if (delta < 1) { targetWidth = currentWidth = jumpToAtEnd; } else { currentWidth += delta / 8; } bonuseBarEnd.x = -bonusBarWidth + currentWidth + rightMargin; bonusBarMiddle.width = currentWidth; }; // bonuseBarEnd.x = -bonusLabel.width; }); var Bubble = Container.expand(function (max_types, isFireBall, type) { var self = Container.call(this); self.isFireBall = isFireBall; var state = 0; self.isAttached = true; self.isFreeBubble = false; var speedX = 0; var speedY = 0; self.targetX = 0; self.targetY = 0; self.setPos = function (x, y) { self.x = self.targetX = x; self.y = self.targetY = y; }; if (type !== undefined) { this.type = type; } else { max_types = max_types || 3; if (max_types > 4) { self.type = Math.floor(Math.random() * (.8 + Math.random() * .2) * max_types); } else { self.type = Math.floor(Math.random() * max_types); } } if (isFireBall) { var bubbleGraphics = self.attachAsset('fireball', { anchorX: 0.5, anchorY: 0.5 }); bubbleGraphics.width = 150; bubbleGraphics.height = 150; } else if (ironBubblesEnabled && self.type === getMaxTypes()) { // Use ironbubble asset for ironbubble type var bubbleGraphics = self.attachAsset('ironbubble', { anchorX: 0.5, anchorY: 0.5 }); } else { var bubbleGraphics = self.attachAsset('bubble' + self.type, { anchorX: 0.5, anchorY: 0.5 }); } /*if (!isFireBall && self.type > 1) { bubbleGraphics.tint = bubbleColors[self.type]; }*/ self.detach = function () { freeBubbleLayer.addChild(self); LK.getSound('detachCircle').play(); self.y += grid.y; self.isAttached = false; speedX = Math.random() * 40 - 20; speedY = -Math.random() * 30; self.down = undefined; }; var spawnMod = 0; self.update = function () { if (self.isFreeBubble) { if (isFireBall) { if (++spawnMod % 2 == 0 && self.parent) { // Spawn fire particles every 5 ticks var angle = Math.random() * Math.PI * 2; var fireParticle = self.parent.addChild(new FireParticle(angle)); fireParticle.x = self.x + Math.cos(angle) * self.width / 4; fireParticle.y = self.y + Math.sin(angle) * self.width / 4; } } return; } if (self.isAttached) { if (self.x != self.targetX) { self.x += (self.targetX - self.x) / 10; } if (self.y != self.targetY) { self.y += (self.targetY - self.y) / 10; } } else { self.x += speedX; self.y += speedY; speedY += 1.5; if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) { speedX = -speedX; LK.getSound('circleBounce').play(); } // Check for collision with barriers for (var i = 0; i < barriers.length; i++) { var barrier = barriers[i]; var dx = self.x - barrier.x; var dy = self.y - barrier.y; var distance = Math.sqrt(dx * dx + dy * dy); var minDist = bubbleSize / 2 + barrier.width / 2; if (distance < minDist) { // Calculate the angle of the collision var angle = Math.atan2(dy, dx); // Calculate the new speed based on the angle of collision, treating the barrier as a static billiard ball var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY); speedX = Math.cos(angle) * newSpeed * .7; speedY = Math.sin(angle) * newSpeed * .7; // Move the bubble back to the point where it just touches the barrier var overlap = minDist - distance; self.x += overlap * Math.cos(angle); self.y += overlap * Math.sin(angle); LK.getSound('circleBounce').play(); } } // Remove unattached bubbles that fall below 2732 - 500 if (self.y > 2732 - 400) { self.destroy(); scoreMultipliers[Math.floor(self.x / (2048 / 5))].applyBubble(self); LK.getSound('scoreCollected').play(); } } }; }); var BubbleRemoveParticle = Container.expand(function () { var self = Container.call(this); var particle = self.attachAsset('removebubbleeffect', { anchorX: 0.5, anchorY: 0.5 }); particle.blendMode = 1; self.scale.set(.33, .33); var cscale = .5; self.update = function () { cscale += .02; self.scale.set(cscale, cscale); self.alpha = 1 - (cscale - .5) * 1.5; if (self.alpha < 0) { self.destroy(); } }; }); var FireBallPowerupOverlay = Container.expand(function () { var self = Container.call(this); var bubbleGraphics = self.attachAsset('fireball', { anchorX: 0.5, anchorY: 0.5 }); self.y = game.height - 140; self.x = 200; var countBG = self.attachAsset('countbg', { anchorX: 0.5, anchorY: 0.5 }); countBG.x = 90; countBG.y = 50; self.fireballsLeft = 1; var label = self.addChild(new Text2(self.fireballsLeft, { size: 70, fill: 0xFFFFFF, font: "Impact", stroke: 0x000000, strokeThickness: 13 })); label.anchor.set(.5, .5); label.x = 90; label.y = 50; self.down = function () { if (self.fireballsLeft > 0 && !launcher.isFireBall()) { self.fireballsLeft--; label.setText(self.fireballsLeft); launcher.triggerFireBall(); if (self.fireballsLeft == 0) { self.alpha = .5; } } }; }); var FireParticle = Container.expand(function (angle) { var self = Container.call(this); var particleGraphics = self.attachAsset('fireparticle', { anchorX: 0.5, anchorY: 0.5 }); particleGraphics.blendMode = 1; var speedX = Math.cos(angle) * 1; var speedY = Math.sin(angle) * 1; var rotationSpeed = Math.random() * 0.1 - 0.05; self.update = function () { self.x += speedX * self.alpha; self.y += speedY * self.alpha; particleGraphics.rotation += rotationSpeed; self.alpha -= 0.01; if (self.alpha <= 0) { self.destroy(); } }; }); var Grid = Container.expand(function () { var self = Container.call(this); var rows = []; self.container = self.addChild(new Container()); var rowCount = 0; function insertRow() { var row = []; var rowWidth = rowCount % 2 == 0 ? 13 : 12; for (var a = 0; a < rowWidth; a++) { var bubble; // Randomly assign ironbubble type to some bubbles if enabled // Reduce ironbubble probability: now appears at 1/(3*colorBallCount+2) if (ironBubblesEnabled && Math.random() < 1 / (3 * colorBallCount + 2)) { // Use Bubble class, but force type to a special value (e.g., max_types) to indicate ironbubble bubble = new Bubble(getMaxTypes(), false, getMaxTypes()); // Replace the asset with ironbubble image if (bubble.children && bubble.children.length > 0) { var old = bubble.children[0]; bubble.removeChild(old); var newAsset = bubble.attachAsset('ironbubble', { anchorX: 0.5, anchorY: 0.5 }); } } else { bubble = new Bubble(getMaxTypes()); } bubble.setPos((2048 - bubbleSize * rowWidth) / 2 + bubbleSize * a + bubbleSize / 2, -rowCount * (1.7320508076 * bubbleSize) / 2); self.container.addChild(bubble); row.push(bubble); /*bubble.down = function () { var bubbles = self.getConnectedBubbles(this); self.removeBubbles(bubbles); var disconnected = self.getDetachedBubbles(); self.removeBubbles(disconnected); };*/ } rows.push(row); rowCount++; } //Method that removes an array of bubbles from the rows array. self.removeBubbles = function (bubbles) { for (var i = 0; i < bubbles.length; i++) { var bubble = bubbles[i]; if (bubble) { var bubbleIndex = this.findBubbleIndex(bubble); if (bubbleIndex) { rows[bubbleIndex.row][bubbleIndex.col] = null; bubble.detach(); } } } }; self.getConnectedBubbles = function (bubble, ignoreType) { var connectedBubbles = []; var queue = [bubble]; var visited = []; while (queue.length > 0) { var currentBubble = queue.shift(); if (visited.indexOf(currentBubble) === -1) { visited.push(currentBubble); connectedBubbles.push(currentBubble); var neighbors = self.getNeighbors(currentBubble); for (var i = 0; i < neighbors.length; i++) { var neighbor = neighbors[i]; if (neighbor && (neighbor.type === bubble.type || ignoreType)) { queue.push(neighbor); } } } } return connectedBubbles; }; //Get a list of bubbles that are not connected to the top row, or to a chain of bubbles connected to the top row. self.getDetachedBubbles = function () { var detachedBubbles = []; var connectedToTop = []; // Mark all bubbles connected to the bottom row var lastRowIndex = rows.length - 1; for (var i = 0; i < rows[lastRowIndex].length; i++) { if (rows[lastRowIndex][i] !== null) { var bottomConnected = self.getConnectedBubbles(rows[lastRowIndex][i], true); connectedToTop = connectedToTop.concat(bottomConnected); } } // Mark all bubbles as visited or not var visited = connectedToTop.filter(function (bubble) { return bubble != null; }); // Find all bubbles that are not visited and not connected to the top for (var row = 0; row < rows.length - 1; row++) { for (var col = 0; col < rows[row].length; col++) { var bubble = rows[row][col]; if (bubble !== null && visited.indexOf(bubble) == -1) { detachedBubbles.push(bubble); } } } return detachedBubbles; }; self.getNeighbors = function (bubble) { var neighbors = []; var bubbleIndex = this.findBubbleIndex(bubble); if (!bubbleIndex) { return []; } var directions = [[-1, 0], [1, 0], // left and right [0, -1], [0, 1], // above and below [-1, -1], [1, -1] // diagonals for even rows ]; if (bubbleIndex && rows[bubbleIndex.row] && rows[bubbleIndex.row].length == 12) { // Adjust diagonals for odd rows directions[4] = [-1, 1]; directions[5] = [1, 1]; } for (var i = 0; i < directions.length; i++) { var dir = directions[i]; if (bubbleIndex && rows[bubbleIndex.row]) { var newRow = bubbleIndex.row + dir[0]; } var newCol = bubbleIndex.col + dir[1]; if (newRow >= 0 && newRow < rows.length && newCol >= 0 && newCol < rows[newRow].length) { neighbors.push(rows[newRow][newCol]); } } return neighbors; }; self.findBubbleIndex = function (bubble) { for (var row = 0; row < rows.length; row++) { var col = rows[row].indexOf(bubble); if (col !== -1) { return { row: row, col: col }; } } return null; }; self.printRowsToConsole = function () { var gridString = ''; for (var i = rows.length - 1; i >= 0; i--) { var rowString = ': ' + (rows[i].length == 13 ? '' : ' '); for (var j = 0; j < rows[i].length; j++) { var bubble = rows[i][j]; rowString += bubble ? '[' + bubble.type + ']' : '[_]'; } gridString += rowString + '\n'; } console.log(gridString); }; // Method to calculate path of movement based on angle and starting point //TODO: MAKE THIS MUCH FASTER! self.bubbleIntersectsGrid = function (nextX, nextY) { outer: for (var row = 0; row < rows.length; row++) { for (var col = 0; col < rows[row].length; col++) { var bubble = rows[row][col]; if (bubble) { var dist = nextY - bubble.y - self.y; //Quick exit if we are nowhere near the row if (dist > 145 || dist < -145) { continue outer; } var dx = nextX - bubble.x - self.x; var dy = nextY - bubble.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < (bubbleSize - 70) / 2 + bubbleSize / 2) { return bubble; } } } } return false; }; self.calculatePath = function (startPoint, angle) { var path = []; var currentPoint = { x: startPoint.x, y: startPoint.y }; var radians = angle; var stepSize = 4; var hitBubble = false; while (currentPoint.y > 0 && !hitBubble) { // Calculate next point var nextX = currentPoint.x + stepSize * Math.cos(radians); var nextY = currentPoint.y + stepSize * Math.sin(radians); // Check for wall collisions if (nextX < 150 / 2 || nextX > 2048 - 150 / 2) { radians = Math.PI - radians; // Reflect angle nextX = currentPoint.x + stepSize * Math.cos(radians); // Recalculate nextX after reflection } hitBubble = self.bubbleIntersectsGrid(nextX, nextY); // Add point to path and update currentPoint path.push({ x: nextX, y: nextY }); currentPoint.x = nextX; currentPoint.y = nextY; } if (hitBubble) { //Only increase avilable bubble type when we have actually pointed as such a bubble if (hitBubble.type + 1 > maxSelectableBubble) { maxSelectableBubble = hitBubble.type + 1; } ; } return path; }; var bubblesInFlight = []; self.fireBubble = function (bubble, angle) { self.addChild(bubble); bubble.x = launcher.x; bubble.y += launcher.y - self.y; bubblesInFlight.push({ bubble: bubble, angle: angle }); }; self.calculateWarningScoreList = function () { var warningScores = []; for (var i = 0; i < 13; i++) { warningScores.push(0); // Initialize all scores to 0 } // Calculate the distance from the bottom for each bubble and increment the warning score based on proximity for (var row = 0; row < rows.length; row++) { for (var col = 0; col < rows[row].length; col++) { var bubble = rows[row][col]; if (bubble) { var distanceFromBottom = 2732 - (bubble.y + self.y); if (distanceFromBottom < 2000) { // If a bubble is within 500px from the bottom var columnIndex = Math.floor(bubble.x / (2048 / 13)); warningScores[columnIndex] += (2000 - distanceFromBottom) / 2000; // Increment the warning score for the column } } } } return warningScores; }; self.update = function () { outer: for (var a = 0; a < bubblesInFlight.length; a++) { var current = bubblesInFlight[a]; var bubble = current.bubble; var nextX = bubble.x; var nextY = bubble.y + gridSpeed; var prevX = bubble.x; var prevY = bubble.y; for (var rep = 0; rep < 25; rep++) { prevX = nextX; prevY = nextY; nextX += Math.cos(current.angle) * 4; nextY += Math.sin(current.angle) * 4; if (nextX < 150 / 2 || nextX > 2048 - 150 / 2) { current.angle = Math.PI - current.angle; // Reflect angle nextX = Math.min(Math.max(nextX, 150 / 2), 2048 - 150 / 2); LK.getSound('circleBounce').play(); } var intersectedBubble = self.bubbleIntersectsGrid(nextX + self.x, nextY + self.y); if (intersectedBubble) { gameIsStarted = true; if (bubble.isFireBall) { self.removeBubbles([intersectedBubble]); var disconnected = self.getDetachedBubbles(); self.removeBubbles(disconnected); } else { var intersectedBubblePos = self.findBubbleIndex(intersectedBubble); var colOffset = rows[intersectedBubblePos.row].length == 13 ? 0 : 1; var offsetPositions = [{ x: intersectedBubble.targetX - bubbleSize / 2, y: intersectedBubble.targetY - 1.7320508076 * bubbleSize / 2, ro: intersectedBubblePos.row + 1, co: intersectedBubblePos.col - 1 + colOffset }, { x: intersectedBubble.targetX + bubbleSize / 2, y: intersectedBubble.targetY - 1.7320508076 * bubbleSize / 2, ro: intersectedBubblePos.row + 1, co: intersectedBubblePos.col + colOffset }, { x: intersectedBubble.targetX + bubbleSize, y: intersectedBubble.targetY, ro: intersectedBubblePos.row, co: intersectedBubblePos.col + 1 }, { x: intersectedBubble.targetX + bubbleSize / 2, y: intersectedBubble.targetY + 1.7320508076 * bubbleSize / 2, ro: intersectedBubblePos.row - 1, co: intersectedBubblePos.col + colOffset }, { x: intersectedBubble.targetX - bubbleSize / 2, y: intersectedBubble.targetY + 1.7320508076 * bubbleSize / 2, ro: intersectedBubblePos.row - 1, co: intersectedBubblePos.col - 1 + colOffset }, { x: intersectedBubble.targetX - bubbleSize, y: intersectedBubble.targetY, ro: intersectedBubblePos.row, co: intersectedBubblePos.col - 1 }]; var closestPosition = 0; var closestDistance = Math.sqrt(Math.pow(offsetPositions[0].x - bubble.x, 2) + Math.pow(offsetPositions[0].y - bubble.y, 2)); for (var i = 1; i < offsetPositions.length; i++) { var currentPosition = offsetPositions[i]; var currentDistance = Math.sqrt(Math.pow(currentPosition.x - bubble.x, 2) + Math.pow(currentPosition.y - bubble.y, 2)); if (currentDistance < closestDistance) { var row = rows[currentPosition.ro]; if (currentPosition.co < 0) { continue; } if (row) { if (row[currentPosition.co]) { continue; } if (currentPosition.co >= row.length) { continue; } } else { var newRowLength = rows[intersectedBubblePos.row].length == 13 ? 12 : 13; if (currentPosition.co >= newRowLength) { continue; } } closestDistance = currentDistance; closestPosition = i; } } // Attach bubble to the closest position var currentMatch = offsetPositions[closestPosition]; bubble.x = prevX; bubble.y = prevY; bubble.targetX = currentMatch.x; bubble.targetY = currentMatch.y; bubble.isFreeBubble = false; var row = rows[offsetPositions[closestPosition].ro]; if (!row) { if (rows[intersectedBubblePos.row].length == 13) { row = [null, null, null, null, null, null, null, null, null, null, null, null]; } else { row = [null, null, null, null, null, null, null, null, null, null, null, null, null]; } rows.unshift(row); } row[offsetPositions[closestPosition].co] = bubble; bubblesInFlight.splice(a--, 1); refreshHintLine(); var bubbles = self.getConnectedBubbles(bubble); if (bubbles.length > 2) { self.removeBubbles(bubbles); var disconnected = self.getDetachedBubbles(); self.removeBubbles(disconnected); bonusUX.setStreakCount(bonusUX.streakCount + 1); } else { bonusUX.setStreakCount(0); LK.getSound('attachCircle').play(); } //Add a grid movement effect when you don't do a match var neighbors = self.getNeighbors(bubble); var touched = []; var neighbors2 = []; for (var i = 0; i < neighbors.length; i++) { var neighbor = neighbors[i]; if (neighbor) { touched.push(neighbor); neighbors2 = neighbors2.concat(self.getNeighbors(neighbor)); var ox = neighbor.x - bubble.x; var oy = neighbor.y - bubble.y; var angle = Math.atan2(oy, ox); neighbor.x += Math.cos(angle) * 20; neighbor.y += Math.sin(angle) * 20; } } //One more layer for (var i = 0; i < neighbors2.length; i++) { var neighbor = neighbors2[i]; if (neighbor && touched.indexOf(neighbor) == -1) { touched.push(neighbor); var ox = neighbor.x - bubble.x; var oy = neighbor.y - bubble.y; var angle = Math.atan2(oy, ox); neighbor.x += Math.cos(angle) * 10; neighbor.y += Math.sin(angle) * 10; } } //self.printRowsToConsole(); continue outer; } } } bubble.x = nextX; bubble.y = nextY; if (bubble.y + self.y < -1000) { //Destory bubbles that somehow manages to escape at the top bubblesInFlight.splice(a--, 1); bubble.destroy(); } } if (gameIsStarted) { self.y += gridSpeed; } var zeroRow = rows[rows.length - 1]; if (zeroRow) { for (var a = 0; a < zeroRow.length; a++) { var bubble = zeroRow[a]; if (bubble) { if (bubble.y + self.y > 0) { insertRow(); } break; } } } else { insertRow(); } for (var row = rows.length - 1; row >= 0; row--) { if (rows[row].every(function (bubble) { return !bubble; })) { rows.splice(row, 1); } } var lastRow = rows[0]; /*if(LK.ticks % 10 == 0){ self.printRowsToConsole() }*/ if (lastRow) { for (var a = 0; a < zeroRow.length; a++) { var bubble = lastRow[a]; if (bubble) { if (bubble.y + self.y > 2200) { LK.effects.flashScreen(0xff0000, 3000); if (!self._gameOverTriggered) { self._gameOverTriggered = true; var jingle = LK.getSound('gameOverJingle'); jingle.stop(); // Ensure not overlapping jingle.play(); // Play game over only after jingle finishes (duration = end - start) // The jingle is defined with start:0, end:1 (full length), so we need to know the actual duration in ms. // If not available, default to 2000ms (safe fallback for most short jingles) var duration = jingle.duration && typeof jingle.duration === "number" ? jingle.duration * 1000 : 2000; LK.setTimeout(function () { LK.showGameOver(); }, duration); } return; // Prevent further processing after game over } if (gameIsStarted) { var targetSpeed = Math.pow(Math.pow((2200 - (bubble.y + self.y)) / 2200, 2), 2) * 4 + 0.5; if (bubble.y + self.y > 2000) { targetSpeed = .2; } gridSpeed += (targetSpeed - gridSpeed) / 20; if (LK.ticks % 10 == 0) { //console.log(gridSpeed) } } break; } } } }; for (var a = 0; a < 8; a++) { insertRow(); } }); var HintBubble = Container.expand(function () { var self = Container.call(this); var bubble = self.attachAsset('hintbubble', { anchorX: 0.5, anchorY: 0.5 }); self.setTint = function (tint) { bubble.tint = tint; }; self.getTint = function (tint) { return bubble.tint; }; }); var Launcher = Container.expand(function () { var self = Container.call(this); var bubble = self.addChild(new Bubble(getMaxTypes(), false)); bubble.isFreeBubble = true; var previewBubble; var lastTypes = [undefined, bubble.type]; function createPreviewBubble() { var nextType; do { nextType = Math.floor(Math.random() * maxSelectableBubble); // Prevent ironbubble from being selected as preview bubble if ironBubblesEnabled } while (nextType == lastTypes[0] && nextType == lastTypes[1] || ironBubblesEnabled && nextType === getMaxTypes()); lastTypes.shift(); lastTypes.push(nextType); previewBubble = self.addChildAt(new Bubble(maxSelectableBubble, false, nextType), 0); previewBubble.scale.set(.7, .7); previewBubble.x = -90; previewBubble.y = 20; previewBubble.isFreeBubble = true; // Add down event to allow switching with current bubble previewBubble.interactive = true; previewBubble.buttonMode = true; previewBubble.down = function (x, y, obj) { // Swap types between previewBubble and bubble if (bubble && previewBubble) { // Only allow swap if both are not fireballs if (!bubble.isFireBall && !previewBubble.isFireBall) { // Prevent swapping to ironbubble if (ironBubblesEnabled && (bubble.type === getMaxTypes() || previewBubble.type === getMaxTypes())) { return; } var tmpType = bubble.type; bubble.type = previewBubble.type; previewBubble.type = tmpType; // Update graphics for both bubbles if (bubble.children && bubble.children.length > 0) { var old = bubble.children[0]; bubble.removeChild(old); var newAsset = bubble.attachAsset('bubble' + bubble.type, { anchorX: 0.5, anchorY: 0.5 }); } if (previewBubble.children && previewBubble.children.length > 0) { var oldPrev = previewBubble.children[0]; previewBubble.removeChild(oldPrev); var newPrevAsset = previewBubble.attachAsset('bubble' + previewBubble.type, { anchorX: 0.5, anchorY: 0.5 }); newPrevAsset.width = previewBubble.width; newPrevAsset.height = previewBubble.height; } refreshHintLine && refreshHintLine(); } } }; } createPreviewBubble(); self.fire = function () { // Prevent firing if bubble is ironbubble (type === getMaxTypes() when ironBubblesEnabled) if (ironBubblesEnabled && bubble.type === getMaxTypes()) { // Optionally, you could play a sound or flash to indicate not allowed return; } bulletsFired++; LK.getSound('fireBubble').play(); // Play sound when the ball is fired grid.fireBubble(bubble, self.angle); bubble = previewBubble; previewBubble.x = previewBubble.y = 0; previewBubble.scale.set(1, 1); createPreviewBubble(); }; self.angle = -Math.PI / 2; self.getBubble = function () { return bubble; }; self.isFireBall = function () { return bubble.isFireBall; }; self.triggerFireBall = function () { bubble.destroy(); bubble = self.addChild(new Bubble(getMaxTypes(), true)); bubble.isFreeBubble = true; }; }); var ScoreIndicatorLabel = Container.expand(function (score, type) { var self = Container.call(this); // Determine fill color: grey for ironbubble, yellow for bubble3, purple for bubble4, else normal color var fillColor; if (ironBubblesEnabled && type === getMaxTypes()) { fillColor = "#888888"; } else if (type === 3) { fillColor = "#ffff00"; } else if (type === 4) { fillColor = "#cb2bff"; } else if (type === 5) { fillColor = "#28f2f0"; } else if (type === 6) { fillColor = "#ff9c00"; } else if (type === 7) { fillColor = "#001f4d"; } else { fillColor = "#" + (bubbleColors[type] !== undefined ? bubbleColors[type].toString(16).padStart(6, '0') : "ffffff"); } // Ensure score is always a string for Text2 var label = new Text2(score !== undefined ? score.toString() : "0", { size: 100, fill: fillColor, font: "Impact" }); label.anchor.set(0.5, 0); self.addChild(label); self.update = function () { self.y -= 7; self.alpha -= .05; if (self.alpha <= 0) { self.destroy(); increaseScore(score); } }; }); var ScoreMultipliers = Container.expand(function (baseValue) { var self = Container.call(this); // Create a score label text string for ScoreMultipliers var scoreMultiplierLabel = new Text2(baseValue, { size: 100, fill: 0x3954FF, font: "Impact" }); scoreMultiplierLabel.anchor.set(0.5, 0); self.addChild(scoreMultiplierLabel); var currentMultiplier = 1; self.applyBubble = function (bubble) { var scoreIndicator = game.addChild(new ScoreIndicatorLabel(baseValue * currentMultiplier, bubble.type)); scoreIndicator.x = self.x; scoreIndicator.y = self.y; var particle = particlesLayer.addChild(new BubbleRemoveParticle()); particle.x = bubble.x; particle.y = self.y + 150; }; self.setMultiplier = function (multiplier) { currentMultiplier = multiplier; scoreMultiplierLabel.setText(baseValue * currentMultiplier); }; }); var WarningLine = Container.expand(function () { var self = Container.call(this); var warning = self.attachAsset('warningstripe', { anchorX: .5, anchorY: .5 }); var warningOffset = Math.random() * 100; var speed = Math.random() * 1 + 1; self.update = function () { warningOffset += speed; warning.alpha = (Math.cos(warningOffset / 50) + 1) / 2 * 0.3 + .7; }; warning.blendMode = 1; warning.rotation = .79; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x0c0d25 }); /**** * Game Code ****/ var gridSpeed = .5; var colorBallCount = 3; // Default, will be set by menu // Iron bubbles option flag and score multiplier var ironBubblesEnabled = false; var ironScoreMultiplier = 1.0; // Show menu to select number of color balls using LK.gui var menuOverlay = new Container(); var menuBG = LK.getAsset('uxoverlay', { anchorX: 0, anchorY: 0 }); menuBG.width = 1200; menuBG.height = 1200; menuOverlay.addChild(menuBG); // Center the menu overlay vertically and horizontally menuBG.x = -600; menuBG.y = -600; var menuTitle = new Text2("Bubble Shooter", { size: 120, fill: 0xffffff, font: "Impact" }); menuTitle.anchor.set(0.5, 0); menuTitle.x = menuBG.x / 2 + 300; menuTitle.y = menuBG.y + 50; menuOverlay.addChild(menuTitle); var menuDesc = new Text2("How many different color?", { size: 80, fill: 0xffffff, font: "Impact" }); menuDesc.anchor.set(0.5, 0); menuDesc.x = menuBG.x / 2 + 300; menuDesc.y = menuBG.y + 200; menuOverlay.addChild(menuDesc); var colorOptions = [{ label: "3 Colors", value: 3 }, { label: "4 Colors", value: 4 }, { label: "5 Colors", value: 5 }, { label: "6 Colors", value: 6 }, { label: "7 Colors", value: 7 }, { label: "8 Colors", value: 8 }]; var menuButtons = []; // Vertically center the menu buttons below the menu description var buttonSpacing = 125; var totalButtonHeight = colorOptions.length * buttonSpacing; var buttonStartY = menuBG.y + 350; function onMenuSelect(option) { colorBallCount = option.value; maxSelectableBubble = colorBallCount; menuActive = false; if (menuOverlay.parent) { menuOverlay.parent.removeChild(menuOverlay); } // Show iron bubble selection menu showIronBubbleMenu(); } function showIronBubbleMenu() { menuActive = true; var ironMenuOverlay = new Container(); var ironMenuBG = LK.getAsset('uxoverlay2', { anchorX: 0, anchorY: 0 }); ironMenuBG.width = 900; ironMenuBG.height = 700; ironMenuOverlay.addChild(ironMenuBG); ironMenuBG.x = -450; ironMenuBG.y = -350; var ironMenuTitle = new Text2("Iron Bubbles?", { size: 100, fill: 0xffffff, font: "Impact" }); ironMenuTitle.anchor.set(0.5, 0); ironMenuTitle.x = ironMenuBG.x / 2 + 225; ironMenuTitle.y = ironMenuBG.y + 60; ironMenuOverlay.addChild(ironMenuTitle); var ironMenuDesc = new Text2("Do you want to include iron bubbles?", { size: 60, fill: 0xffffff, font: "Impact" }); ironMenuDesc.anchor.set(0.5, 0); ironMenuDesc.x = ironMenuBG.x / 2 + 225; ironMenuDesc.y = ironMenuBG.y + 200; ironMenuOverlay.addChild(ironMenuDesc); var yesBtn = new Text2("Yes", { size: 90, fill: 0x3954FF, font: "Impact", stroke: 0xffffff, strokeThickness: 8 }); yesBtn.anchor.set(0.5, 0); yesBtn.x = ironMenuBG.x / 2 + 225; yesBtn.y = ironMenuBG.y + 350; yesBtn.interactive = true; yesBtn.buttonMode = true; yesBtn.down = function (x, y, obj) { ironBubblesEnabled = true; if (ironMenuOverlay.parent) { ironMenuOverlay.parent.removeChild(ironMenuOverlay); } menuActive = false; // Reset scoreMultipliers and baseScores based on new colorBallCount setupScoreMultipliers(); }; ironMenuOverlay.addChild(yesBtn); var noBtn = new Text2("No", { size: 90, fill: 0x3954FF, font: "Impact", stroke: 0xffffff, strokeThickness: 8 }); noBtn.anchor.set(0.5, 0); noBtn.x = ironMenuBG.x / 2 + 225; noBtn.y = ironMenuBG.y + 500; noBtn.interactive = true; noBtn.buttonMode = true; noBtn.down = function (x, y, obj) { ironBubblesEnabled = false; if (ironMenuOverlay.parent) { ironMenuOverlay.parent.removeChild(ironMenuOverlay); } menuActive = false; // Reset scoreMultipliers and baseScores based on new colorBallCount setupScoreMultipliers(); }; ironMenuOverlay.addChild(noBtn); LK.gui.center.addChild(ironMenuOverlay); } // Helper to reset scoreMultipliers and baseScores after menu function setupScoreMultipliers() { for (var i = 0; i < scoreMultipliers.length; i++) { if (scoreMultipliers[i].parent) { scoreMultipliers[i].parent.removeChild(scoreMultipliers[i]); } } scoreMultipliers = []; // Set ironScoreMultiplier based on ironBubblesEnabled ironScoreMultiplier = ironBubblesEnabled ? 1.3 : 1.0; if (colorBallCount <= 3) { baseScores = [100, 250, 500, 250, 100]; } else if (colorBallCount == 4) { baseScores = [120, 300, 600, 300, 120]; } else if (colorBallCount == 5) { baseScores = [150, 375, 750, 375, 150]; } else if (colorBallCount == 6) { baseScores = [200, 500, 1000, 500, 200]; } else if (colorBallCount == 7) { baseScores = [250, 625, 1250, 625, 250]; } else if (colorBallCount == 8) { baseScores = [300, 750, 1500, 750, 300]; } else { baseScores = [100, 250, 500, 250, 100]; } for (var a = 0; a < 5; a++) { var sm = new ScoreMultipliers(Math.round(baseScores[a] * ironScoreMultiplier)); sm.x = 2048 / 5 * a + 2048 / 10; sm.y = 2300; scoreMultipliers.push(sm); game.addChild(sm); } } for (var i = 0; i < colorOptions.length; i++) { (function (idx) { var opt = colorOptions[idx]; var btn = new Text2(opt.label, { size: 90, fill: 0xffffff, font: "Impact", stroke: 0x3954FF, strokeThickness: 8 }); btn.anchor.set(0.5, 0); btn.x = menuBG.x / 2 + 300; btn.y = buttonStartY + idx * buttonSpacing; btn.interactive = true; btn.buttonMode = true; btn.down = function (x, y, obj) { onMenuSelect(opt); }; menuOverlay.addChild(btn); menuButtons.push(btn); })(i); } LK.gui.center.addChild(menuOverlay); function increaseScore(amount) { var currentScore = LK.getScore(); var newScore = currentScore + amount; LK.setScore(newScore); // Update the game score using LK method scoreLabel.setText(newScore.toString()); // Update the score label with the new score } //Game size 2048x2732 /* Todo: [X] Make sure we GC nodes that drop of screen [ ] Make preview line fade out at the end */ var bulletsFired = 0; //3*30+1 var bubbleSize = 150; var gameIsStarted = false; var bubbleColors = [0xff2853, 0x44d31f, 0x5252ff, 0xcb2bff, 0x28f2f0, 0xffc411]; var barriers = []; var maxSelectableBubble = colorBallCount; var warningLines = []; for (var a = 0; a < 13; a++) { var wl = game.addChild(new WarningLine()); wl.x = 2048 / 13 * (a + .5); wl.y = 2200; wl.alpha = 0; warningLines.push(wl); wl.scale.set(16, 40); } var warningOverlay = game.attachAsset('dangeroverlay', {}); warningOverlay.y = 2280; var uxoverlay = game.attachAsset('uxoverlay', {}); uxoverlay.y = 2440; var uxoverlay2 = game.attachAsset('uxoverlay2', {}); uxoverlay2.y = 2460; for (var a = 0; a < 4; a++) { for (var b = 0; b < 3; b++) { var barrier = game.addChild(new Barrier()); barrier.y = 2732 - 450 + b * 70; barrier.x = 2048 / 5 * a + 2048 / 5; barriers.push(barrier); } var barrierBlock = game.attachAsset('barrierblock', {}); barrierBlock.x = 2048 / 5 * a + 2048 / 5; barrierBlock.y = 2732 - 450; barrierBlock.anchor.x = .5; } // Create a score label var scoreLabel = new Text2('0', { size: 120, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 15, font: "Impact" }); scoreLabel.anchor.set(0.5, 0); LK.gui.top.addChild(scoreLabel); var scoreMultipliers = []; // Score multiplier logic based on colorBallCount var baseScores; if (colorBallCount <= 3) { baseScores = [100, 250, 500, 250, 100]; } else if (colorBallCount == 4) { baseScores = [120, 300, 600, 300, 120]; } else if (colorBallCount == 5) { baseScores = [150, 375,, 750, 375, 150]; } else if (colorBallCount == 6) { baseScores = [200, 500, 1000, 500, 200]; } else if (colorBallCount == 7) { baseScores = [250, 625, 1250, 625, 250]; } else if (colorBallCount == 8) { baseScores = [350, 875, 1750, 875, 350]; } else { baseScores = [100, 250, 500, 250, 100]; } for (var a = 0; a < 5; a++) { var sm = new ScoreMultipliers(Math.round(baseScores[a] * ironScoreMultiplier)); sm.x = 2048 / 5 * a + 2048 / 10; sm.y = 2300; scoreMultipliers.push(sm); game.addChild(sm); } var bonusUX = game.addChild(new BonusUX()); var fireBallPowerupOverlay = game.addChild(new FireBallPowerupOverlay()); var particlesLayer = game.addChild(new Container()); var grid = game.addChild(new Grid()); grid.y = 1000; var freeBubbleLayer = game.addChild(new Container()); var hintBubblePlayer = game.addChild(new Container()); var launcher = game.addChild(new Launcher()); launcher.x = game.width / 2; launcher.y = game.height - 138; var hintBubbleCache = []; var hintBubbles = []; var isValid = false; var path = []; var bubbleAlpha = 1; var hintTargetX = game.width / 2; var hintTargetY = 0; // Prevent game interaction before menu selection var menuActive = true; game.move = function (x, y, obj) { if (menuActive) { return; } hintTargetX = x; hintTargetY = y; refreshHintLine(); // } }; game.down = function (x, y, obj) { if (menuActive) { return; } game.move(x, y, obj); }; function getMaxTypes() { return colorBallCount; } function refreshHintLine() { var ox = hintTargetX - launcher.x; var oy = hintTargetY - launcher.y; var angle = Math.atan2(oy, ox); launcher.angle = angle; isValid = angle < -.2 && angle > -Math.PI + .2; if (isValid) { path = grid.calculatePath(launcher, angle); //This allows updated faster than 60fps, making everyting feel better. } renderHintBubbels(); } var hintOffset = 0; var distanceBetweenHintbubbles = 100; function renderHintBubbels() { if (isValid) { hintOffset = hintOffset % distanceBetweenHintbubbles; var distanceSinceLastDot = -hintOffset + 100; var hintBubbleOffset = 0; var lastPoint = path[0]; var bubble = launcher.getBubble(); var tint; if (bubble.isFireBall) { tint = 0xff9c00; } else if (bubble.type === 3) { tint = 0xffff00; // yellow } else if (bubble.type === 4) { tint = 0xcb2bff; // purple } else if (bubble.type === 5) { tint = 0x28f2f0; // cyan } else if (bubble.type === 6) { tint = 0xff9c00; // orange } else if (bubble.type === 7) { tint = 0x001f4d; // navy blue } else { tint = bubbleColors[bubble.type]; } var updateTint = true; for (var a = 1; a < path.length; a++) { var p2 = path[a]; var ox = p2.x - lastPoint.x; var oy = p2.y - lastPoint.y; var dist = Math.sqrt(ox * ox + oy * oy); distanceSinceLastDot += dist; if (distanceSinceLastDot >= distanceBetweenHintbubbles) { var amountOver = distanceSinceLastDot - distanceBetweenHintbubbles; var angle = Math.atan2(oy, ox); var currentBubble = hintBubbles[hintBubbleOffset]; if (!currentBubble) { currentBubble = hintBubbles[hintBubbleOffset] = new HintBubble(); hintBubblePlayer.addChild(currentBubble); } currentBubble.alpha = bubbleAlpha; currentBubble.visible = true; var currentTint = currentBubble.getTint(); if (hintBubbleOffset == 0) { currentBubble.setTint(tint); } else if (updateTint && currentTint != tint || currentTint == 0xffffff) { currentBubble.setTint(tint); updateTint = false; } currentBubble.x = lastPoint.x - Math.cos(angle) * amountOver; currentBubble.y = lastPoint.y - Math.sin(angle) * amountOver; hintBubbleOffset++; distanceSinceLastDot = 0; lastPoint = currentBubble; } else { lastPoint = p2; } } for (var a = hintBubbleOffset; a < hintBubbles.length; a++) { hintBubbles[a].visible = false; } } else { for (var a = 0; a < hintBubbles.length; a++) { hintBubbles[a].alpha = bubbleAlpha; } } } game.update = function () { if (menuActive) { return; } hintOffset += 5; if (isValid) { bubbleAlpha = Math.min(bubbleAlpha + .05, 1); } else { bubbleAlpha = Math.max(bubbleAlpha - .05, 0); } refreshHintLine(); var alphaList = grid.calculateWarningScoreList(); for (var a = 0; a < warningLines.length; a++) { var value = alphaList[a] / 3; warningLines[a].alpha += (Math.min(value, .6) - warningLines[a].alpha) / 100; warningLines[a].scale.y += (value * 60 - warningLines[a].scale.y) / 100; } }; game.up = function () { if (menuActive) { return; } if (isValid) { launcher.fire(); } }; ; ; ; ; ; ; ; ; ; ;
/****
* Classes
****/
var Barrier = Container.expand(function () {
var self = Container.call(this);
var barrierGraphics = self.attachAsset('barrier', {
anchorX: .5,
anchorY: .5
});
});
var BonusUX = Container.expand(function () {
var self = Container.call(this);
// No ironbubble class or dummy logic is present, so nothing to remove here. All ironbubble logic will use Bubble class.
var barHeight = 50;
var bonusLabel = self.addChild(new Text2('Streak Bonus', {
size: 90,
fill: 0xF4F5FF,
font: "Impact"
}));
var bonusAmountLabel = self.addChild(new Text2('1x', {
size: 170,
fill: 0xF4F5FF,
font: "Impact"
}));
bonusLabel.anchor.set(1, 1);
bonusLabel.y = barHeight / 2;
var rightMargin = -10;
bonusLabel.x = rightMargin;
bonusAmountLabel.anchor.set(.5, .5);
bonusAmountLabel.x = 100;
var bonusBarWidth = bonusLabel.width;
var bonusBarStart = self.attachAsset('bonusend', {
y: 30,
x: -bonusBarWidth + rightMargin
});
var bonuseBarEnd = self.attachAsset('bonusend', {
y: 30,
x: -bonusBarWidth + rightMargin
});
var bonusBarMiddle = self.attachAsset('bonusbarmiddle', {
y: 30,
x: -bonusBarWidth + rightMargin + barHeight / 2,
width: 0
});
self.x = game.width - 270;
self.y = game.height - 145;
var bonusBarStepSize = (bonusBarWidth - barHeight) / 5;
var targetWidth = 0;
var currentWidth = 0;
var jumpToAtEnd = 0;
self.bonusAmount = 1;
self.streakCount = 0;
var maxLevel = 40;
self.setStreakCount = function (level) {
self.streakCount = Math.min(level, maxLevel);
var newBonus = Math.floor(self.streakCount / 5) + 1;
if (newBonus != self.bonusAmount) {
for (var a = 0; a < scoreMultipliers.length; a++) {
scoreMultipliers[a].setMultiplier(newBonus);
}
}
self.bonusAmount = newBonus;
bonusAmountLabel.setText(self.bonusAmount + 'x');
var newbarpos = level >= maxLevel ? 5 : level % 5;
targetWidth = newbarpos * bonusBarStepSize;
jumpToAtEnd = targetWidth;
if (newbarpos == 0 && level > 0) {
targetWidth = 5 * bonusBarStepSize;
jumpToAtEnd = 0;
}
};
self.update = function () {
var delta = targetWidth - currentWidth;
if (delta < 1) {
targetWidth = currentWidth = jumpToAtEnd;
} else {
currentWidth += delta / 8;
}
bonuseBarEnd.x = -bonusBarWidth + currentWidth + rightMargin;
bonusBarMiddle.width = currentWidth;
};
// bonuseBarEnd.x = -bonusLabel.width;
});
var Bubble = Container.expand(function (max_types, isFireBall, type) {
var self = Container.call(this);
self.isFireBall = isFireBall;
var state = 0;
self.isAttached = true;
self.isFreeBubble = false;
var speedX = 0;
var speedY = 0;
self.targetX = 0;
self.targetY = 0;
self.setPos = function (x, y) {
self.x = self.targetX = x;
self.y = self.targetY = y;
};
if (type !== undefined) {
this.type = type;
} else {
max_types = max_types || 3;
if (max_types > 4) {
self.type = Math.floor(Math.random() * (.8 + Math.random() * .2) * max_types);
} else {
self.type = Math.floor(Math.random() * max_types);
}
}
if (isFireBall) {
var bubbleGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
bubbleGraphics.width = 150;
bubbleGraphics.height = 150;
} else if (ironBubblesEnabled && self.type === getMaxTypes()) {
// Use ironbubble asset for ironbubble type
var bubbleGraphics = self.attachAsset('ironbubble', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
var bubbleGraphics = self.attachAsset('bubble' + self.type, {
anchorX: 0.5,
anchorY: 0.5
});
}
/*if (!isFireBall && self.type > 1) {
bubbleGraphics.tint = bubbleColors[self.type];
}*/
self.detach = function () {
freeBubbleLayer.addChild(self);
LK.getSound('detachCircle').play();
self.y += grid.y;
self.isAttached = false;
speedX = Math.random() * 40 - 20;
speedY = -Math.random() * 30;
self.down = undefined;
};
var spawnMod = 0;
self.update = function () {
if (self.isFreeBubble) {
if (isFireBall) {
if (++spawnMod % 2 == 0 && self.parent) {
// Spawn fire particles every 5 ticks
var angle = Math.random() * Math.PI * 2;
var fireParticle = self.parent.addChild(new FireParticle(angle));
fireParticle.x = self.x + Math.cos(angle) * self.width / 4;
fireParticle.y = self.y + Math.sin(angle) * self.width / 4;
}
}
return;
}
if (self.isAttached) {
if (self.x != self.targetX) {
self.x += (self.targetX - self.x) / 10;
}
if (self.y != self.targetY) {
self.y += (self.targetY - self.y) / 10;
}
} else {
self.x += speedX;
self.y += speedY;
speedY += 1.5;
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX;
LK.getSound('circleBounce').play();
}
// Check for collision with barriers
for (var i = 0; i < barriers.length; i++) {
var barrier = barriers[i];
var dx = self.x - barrier.x;
var dy = self.y - barrier.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var minDist = bubbleSize / 2 + barrier.width / 2;
if (distance < minDist) {
// Calculate the angle of the collision
var angle = Math.atan2(dy, dx);
// Calculate the new speed based on the angle of collision, treating the barrier as a static billiard ball
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
speedX = Math.cos(angle) * newSpeed * .7;
speedY = Math.sin(angle) * newSpeed * .7;
// Move the bubble back to the point where it just touches the barrier
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// Remove unattached bubbles that fall below 2732 - 500
if (self.y > 2732 - 400) {
self.destroy();
scoreMultipliers[Math.floor(self.x / (2048 / 5))].applyBubble(self);
LK.getSound('scoreCollected').play();
}
}
};
});
var BubbleRemoveParticle = Container.expand(function () {
var self = Container.call(this);
var particle = self.attachAsset('removebubbleeffect', {
anchorX: 0.5,
anchorY: 0.5
});
particle.blendMode = 1;
self.scale.set(.33, .33);
var cscale = .5;
self.update = function () {
cscale += .02;
self.scale.set(cscale, cscale);
self.alpha = 1 - (cscale - .5) * 1.5;
if (self.alpha < 0) {
self.destroy();
}
};
});
var FireBallPowerupOverlay = Container.expand(function () {
var self = Container.call(this);
var bubbleGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
self.y = game.height - 140;
self.x = 200;
var countBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
countBG.x = 90;
countBG.y = 50;
self.fireballsLeft = 1;
var label = self.addChild(new Text2(self.fireballsLeft, {
size: 70,
fill: 0xFFFFFF,
font: "Impact",
stroke: 0x000000,
strokeThickness: 13
}));
label.anchor.set(.5, .5);
label.x = 90;
label.y = 50;
self.down = function () {
if (self.fireballsLeft > 0 && !launcher.isFireBall()) {
self.fireballsLeft--;
label.setText(self.fireballsLeft);
launcher.triggerFireBall();
if (self.fireballsLeft == 0) {
self.alpha = .5;
}
}
};
});
var FireParticle = Container.expand(function (angle) {
var self = Container.call(this);
var particleGraphics = self.attachAsset('fireparticle', {
anchorX: 0.5,
anchorY: 0.5
});
particleGraphics.blendMode = 1;
var speedX = Math.cos(angle) * 1;
var speedY = Math.sin(angle) * 1;
var rotationSpeed = Math.random() * 0.1 - 0.05;
self.update = function () {
self.x += speedX * self.alpha;
self.y += speedY * self.alpha;
particleGraphics.rotation += rotationSpeed;
self.alpha -= 0.01;
if (self.alpha <= 0) {
self.destroy();
}
};
});
var Grid = Container.expand(function () {
var self = Container.call(this);
var rows = [];
self.container = self.addChild(new Container());
var rowCount = 0;
function insertRow() {
var row = [];
var rowWidth = rowCount % 2 == 0 ? 13 : 12;
for (var a = 0; a < rowWidth; a++) {
var bubble;
// Randomly assign ironbubble type to some bubbles if enabled
// Reduce ironbubble probability: now appears at 1/(3*colorBallCount+2)
if (ironBubblesEnabled && Math.random() < 1 / (3 * colorBallCount + 2)) {
// Use Bubble class, but force type to a special value (e.g., max_types) to indicate ironbubble
bubble = new Bubble(getMaxTypes(), false, getMaxTypes());
// Replace the asset with ironbubble image
if (bubble.children && bubble.children.length > 0) {
var old = bubble.children[0];
bubble.removeChild(old);
var newAsset = bubble.attachAsset('ironbubble', {
anchorX: 0.5,
anchorY: 0.5
});
}
} else {
bubble = new Bubble(getMaxTypes());
}
bubble.setPos((2048 - bubbleSize * rowWidth) / 2 + bubbleSize * a + bubbleSize / 2, -rowCount * (1.7320508076 * bubbleSize) / 2);
self.container.addChild(bubble);
row.push(bubble);
/*bubble.down = function () {
var bubbles = self.getConnectedBubbles(this);
self.removeBubbles(bubbles);
var disconnected = self.getDetachedBubbles();
self.removeBubbles(disconnected);
};*/
}
rows.push(row);
rowCount++;
}
//Method that removes an array of bubbles from the rows array.
self.removeBubbles = function (bubbles) {
for (var i = 0; i < bubbles.length; i++) {
var bubble = bubbles[i];
if (bubble) {
var bubbleIndex = this.findBubbleIndex(bubble);
if (bubbleIndex) {
rows[bubbleIndex.row][bubbleIndex.col] = null;
bubble.detach();
}
}
}
};
self.getConnectedBubbles = function (bubble, ignoreType) {
var connectedBubbles = [];
var queue = [bubble];
var visited = [];
while (queue.length > 0) {
var currentBubble = queue.shift();
if (visited.indexOf(currentBubble) === -1) {
visited.push(currentBubble);
connectedBubbles.push(currentBubble);
var neighbors = self.getNeighbors(currentBubble);
for (var i = 0; i < neighbors.length; i++) {
var neighbor = neighbors[i];
if (neighbor && (neighbor.type === bubble.type || ignoreType)) {
queue.push(neighbor);
}
}
}
}
return connectedBubbles;
};
//Get a list of bubbles that are not connected to the top row, or to a chain of bubbles connected to the top row.
self.getDetachedBubbles = function () {
var detachedBubbles = [];
var connectedToTop = [];
// Mark all bubbles connected to the bottom row
var lastRowIndex = rows.length - 1;
for (var i = 0; i < rows[lastRowIndex].length; i++) {
if (rows[lastRowIndex][i] !== null) {
var bottomConnected = self.getConnectedBubbles(rows[lastRowIndex][i], true);
connectedToTop = connectedToTop.concat(bottomConnected);
}
}
// Mark all bubbles as visited or not
var visited = connectedToTop.filter(function (bubble) {
return bubble != null;
});
// Find all bubbles that are not visited and not connected to the top
for (var row = 0; row < rows.length - 1; row++) {
for (var col = 0; col < rows[row].length; col++) {
var bubble = rows[row][col];
if (bubble !== null && visited.indexOf(bubble) == -1) {
detachedBubbles.push(bubble);
}
}
}
return detachedBubbles;
};
self.getNeighbors = function (bubble) {
var neighbors = [];
var bubbleIndex = this.findBubbleIndex(bubble);
if (!bubbleIndex) {
return [];
}
var directions = [[-1, 0], [1, 0],
// left and right
[0, -1], [0, 1],
// above and below
[-1, -1], [1, -1] // diagonals for even rows
];
if (bubbleIndex && rows[bubbleIndex.row] && rows[bubbleIndex.row].length == 12) {
// Adjust diagonals for odd rows
directions[4] = [-1, 1];
directions[5] = [1, 1];
}
for (var i = 0; i < directions.length; i++) {
var dir = directions[i];
if (bubbleIndex && rows[bubbleIndex.row]) {
var newRow = bubbleIndex.row + dir[0];
}
var newCol = bubbleIndex.col + dir[1];
if (newRow >= 0 && newRow < rows.length && newCol >= 0 && newCol < rows[newRow].length) {
neighbors.push(rows[newRow][newCol]);
}
}
return neighbors;
};
self.findBubbleIndex = function (bubble) {
for (var row = 0; row < rows.length; row++) {
var col = rows[row].indexOf(bubble);
if (col !== -1) {
return {
row: row,
col: col
};
}
}
return null;
};
self.printRowsToConsole = function () {
var gridString = '';
for (var i = rows.length - 1; i >= 0; i--) {
var rowString = ': ' + (rows[i].length == 13 ? '' : ' ');
for (var j = 0; j < rows[i].length; j++) {
var bubble = rows[i][j];
rowString += bubble ? '[' + bubble.type + ']' : '[_]';
}
gridString += rowString + '\n';
}
console.log(gridString);
};
// Method to calculate path of movement based on angle and starting point
//TODO: MAKE THIS MUCH FASTER!
self.bubbleIntersectsGrid = function (nextX, nextY) {
outer: for (var row = 0; row < rows.length; row++) {
for (var col = 0; col < rows[row].length; col++) {
var bubble = rows[row][col];
if (bubble) {
var dist = nextY - bubble.y - self.y;
//Quick exit if we are nowhere near the row
if (dist > 145 || dist < -145) {
continue outer;
}
var dx = nextX - bubble.x - self.x;
var dy = nextY - bubble.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < (bubbleSize - 70) / 2 + bubbleSize / 2) {
return bubble;
}
}
}
}
return false;
};
self.calculatePath = function (startPoint, angle) {
var path = [];
var currentPoint = {
x: startPoint.x,
y: startPoint.y
};
var radians = angle;
var stepSize = 4;
var hitBubble = false;
while (currentPoint.y > 0 && !hitBubble) {
// Calculate next point
var nextX = currentPoint.x + stepSize * Math.cos(radians);
var nextY = currentPoint.y + stepSize * Math.sin(radians);
// Check for wall collisions
if (nextX < 150 / 2 || nextX > 2048 - 150 / 2) {
radians = Math.PI - radians; // Reflect angle
nextX = currentPoint.x + stepSize * Math.cos(radians); // Recalculate nextX after reflection
}
hitBubble = self.bubbleIntersectsGrid(nextX, nextY);
// Add point to path and update currentPoint
path.push({
x: nextX,
y: nextY
});
currentPoint.x = nextX;
currentPoint.y = nextY;
}
if (hitBubble) {
//Only increase avilable bubble type when we have actually pointed as such a bubble
if (hitBubble.type + 1 > maxSelectableBubble) {
maxSelectableBubble = hitBubble.type + 1;
}
;
}
return path;
};
var bubblesInFlight = [];
self.fireBubble = function (bubble, angle) {
self.addChild(bubble);
bubble.x = launcher.x;
bubble.y += launcher.y - self.y;
bubblesInFlight.push({
bubble: bubble,
angle: angle
});
};
self.calculateWarningScoreList = function () {
var warningScores = [];
for (var i = 0; i < 13; i++) {
warningScores.push(0); // Initialize all scores to 0
}
// Calculate the distance from the bottom for each bubble and increment the warning score based on proximity
for (var row = 0; row < rows.length; row++) {
for (var col = 0; col < rows[row].length; col++) {
var bubble = rows[row][col];
if (bubble) {
var distanceFromBottom = 2732 - (bubble.y + self.y);
if (distanceFromBottom < 2000) {
// If a bubble is within 500px from the bottom
var columnIndex = Math.floor(bubble.x / (2048 / 13));
warningScores[columnIndex] += (2000 - distanceFromBottom) / 2000; // Increment the warning score for the column
}
}
}
}
return warningScores;
};
self.update = function () {
outer: for (var a = 0; a < bubblesInFlight.length; a++) {
var current = bubblesInFlight[a];
var bubble = current.bubble;
var nextX = bubble.x;
var nextY = bubble.y + gridSpeed;
var prevX = bubble.x;
var prevY = bubble.y;
for (var rep = 0; rep < 25; rep++) {
prevX = nextX;
prevY = nextY;
nextX += Math.cos(current.angle) * 4;
nextY += Math.sin(current.angle) * 4;
if (nextX < 150 / 2 || nextX > 2048 - 150 / 2) {
current.angle = Math.PI - current.angle; // Reflect angle
nextX = Math.min(Math.max(nextX, 150 / 2), 2048 - 150 / 2);
LK.getSound('circleBounce').play();
}
var intersectedBubble = self.bubbleIntersectsGrid(nextX + self.x, nextY + self.y);
if (intersectedBubble) {
gameIsStarted = true;
if (bubble.isFireBall) {
self.removeBubbles([intersectedBubble]);
var disconnected = self.getDetachedBubbles();
self.removeBubbles(disconnected);
} else {
var intersectedBubblePos = self.findBubbleIndex(intersectedBubble);
var colOffset = rows[intersectedBubblePos.row].length == 13 ? 0 : 1;
var offsetPositions = [{
x: intersectedBubble.targetX - bubbleSize / 2,
y: intersectedBubble.targetY - 1.7320508076 * bubbleSize / 2,
ro: intersectedBubblePos.row + 1,
co: intersectedBubblePos.col - 1 + colOffset
}, {
x: intersectedBubble.targetX + bubbleSize / 2,
y: intersectedBubble.targetY - 1.7320508076 * bubbleSize / 2,
ro: intersectedBubblePos.row + 1,
co: intersectedBubblePos.col + colOffset
}, {
x: intersectedBubble.targetX + bubbleSize,
y: intersectedBubble.targetY,
ro: intersectedBubblePos.row,
co: intersectedBubblePos.col + 1
}, {
x: intersectedBubble.targetX + bubbleSize / 2,
y: intersectedBubble.targetY + 1.7320508076 * bubbleSize / 2,
ro: intersectedBubblePos.row - 1,
co: intersectedBubblePos.col + colOffset
}, {
x: intersectedBubble.targetX - bubbleSize / 2,
y: intersectedBubble.targetY + 1.7320508076 * bubbleSize / 2,
ro: intersectedBubblePos.row - 1,
co: intersectedBubblePos.col - 1 + colOffset
}, {
x: intersectedBubble.targetX - bubbleSize,
y: intersectedBubble.targetY,
ro: intersectedBubblePos.row,
co: intersectedBubblePos.col - 1
}];
var closestPosition = 0;
var closestDistance = Math.sqrt(Math.pow(offsetPositions[0].x - bubble.x, 2) + Math.pow(offsetPositions[0].y - bubble.y, 2));
for (var i = 1; i < offsetPositions.length; i++) {
var currentPosition = offsetPositions[i];
var currentDistance = Math.sqrt(Math.pow(currentPosition.x - bubble.x, 2) + Math.pow(currentPosition.y - bubble.y, 2));
if (currentDistance < closestDistance) {
var row = rows[currentPosition.ro];
if (currentPosition.co < 0) {
continue;
}
if (row) {
if (row[currentPosition.co]) {
continue;
}
if (currentPosition.co >= row.length) {
continue;
}
} else {
var newRowLength = rows[intersectedBubblePos.row].length == 13 ? 12 : 13;
if (currentPosition.co >= newRowLength) {
continue;
}
}
closestDistance = currentDistance;
closestPosition = i;
}
}
// Attach bubble to the closest position
var currentMatch = offsetPositions[closestPosition];
bubble.x = prevX;
bubble.y = prevY;
bubble.targetX = currentMatch.x;
bubble.targetY = currentMatch.y;
bubble.isFreeBubble = false;
var row = rows[offsetPositions[closestPosition].ro];
if (!row) {
if (rows[intersectedBubblePos.row].length == 13) {
row = [null, null, null, null, null, null, null, null, null, null, null, null];
} else {
row = [null, null, null, null, null, null, null, null, null, null, null, null, null];
}
rows.unshift(row);
}
row[offsetPositions[closestPosition].co] = bubble;
bubblesInFlight.splice(a--, 1);
refreshHintLine();
var bubbles = self.getConnectedBubbles(bubble);
if (bubbles.length > 2) {
self.removeBubbles(bubbles);
var disconnected = self.getDetachedBubbles();
self.removeBubbles(disconnected);
bonusUX.setStreakCount(bonusUX.streakCount + 1);
} else {
bonusUX.setStreakCount(0);
LK.getSound('attachCircle').play();
}
//Add a grid movement effect when you don't do a match
var neighbors = self.getNeighbors(bubble);
var touched = [];
var neighbors2 = [];
for (var i = 0; i < neighbors.length; i++) {
var neighbor = neighbors[i];
if (neighbor) {
touched.push(neighbor);
neighbors2 = neighbors2.concat(self.getNeighbors(neighbor));
var ox = neighbor.x - bubble.x;
var oy = neighbor.y - bubble.y;
var angle = Math.atan2(oy, ox);
neighbor.x += Math.cos(angle) * 20;
neighbor.y += Math.sin(angle) * 20;
}
}
//One more layer
for (var i = 0; i < neighbors2.length; i++) {
var neighbor = neighbors2[i];
if (neighbor && touched.indexOf(neighbor) == -1) {
touched.push(neighbor);
var ox = neighbor.x - bubble.x;
var oy = neighbor.y - bubble.y;
var angle = Math.atan2(oy, ox);
neighbor.x += Math.cos(angle) * 10;
neighbor.y += Math.sin(angle) * 10;
}
}
//self.printRowsToConsole();
continue outer;
}
}
}
bubble.x = nextX;
bubble.y = nextY;
if (bubble.y + self.y < -1000) {
//Destory bubbles that somehow manages to escape at the top
bubblesInFlight.splice(a--, 1);
bubble.destroy();
}
}
if (gameIsStarted) {
self.y += gridSpeed;
}
var zeroRow = rows[rows.length - 1];
if (zeroRow) {
for (var a = 0; a < zeroRow.length; a++) {
var bubble = zeroRow[a];
if (bubble) {
if (bubble.y + self.y > 0) {
insertRow();
}
break;
}
}
} else {
insertRow();
}
for (var row = rows.length - 1; row >= 0; row--) {
if (rows[row].every(function (bubble) {
return !bubble;
})) {
rows.splice(row, 1);
}
}
var lastRow = rows[0];
/*if(LK.ticks % 10 == 0){
self.printRowsToConsole()
}*/
if (lastRow) {
for (var a = 0; a < zeroRow.length; a++) {
var bubble = lastRow[a];
if (bubble) {
if (bubble.y + self.y > 2200) {
LK.effects.flashScreen(0xff0000, 3000);
if (!self._gameOverTriggered) {
self._gameOverTriggered = true;
var jingle = LK.getSound('gameOverJingle');
jingle.stop(); // Ensure not overlapping
jingle.play();
// Play game over only after jingle finishes (duration = end - start)
// The jingle is defined with start:0, end:1 (full length), so we need to know the actual duration in ms.
// If not available, default to 2000ms (safe fallback for most short jingles)
var duration = jingle.duration && typeof jingle.duration === "number" ? jingle.duration * 1000 : 2000;
LK.setTimeout(function () {
LK.showGameOver();
}, duration);
}
return; // Prevent further processing after game over
}
if (gameIsStarted) {
var targetSpeed = Math.pow(Math.pow((2200 - (bubble.y + self.y)) / 2200, 2), 2) * 4 + 0.5;
if (bubble.y + self.y > 2000) {
targetSpeed = .2;
}
gridSpeed += (targetSpeed - gridSpeed) / 20;
if (LK.ticks % 10 == 0) {
//console.log(gridSpeed)
}
}
break;
}
}
}
};
for (var a = 0; a < 8; a++) {
insertRow();
}
});
var HintBubble = Container.expand(function () {
var self = Container.call(this);
var bubble = self.attachAsset('hintbubble', {
anchorX: 0.5,
anchorY: 0.5
});
self.setTint = function (tint) {
bubble.tint = tint;
};
self.getTint = function (tint) {
return bubble.tint;
};
});
var Launcher = Container.expand(function () {
var self = Container.call(this);
var bubble = self.addChild(new Bubble(getMaxTypes(), false));
bubble.isFreeBubble = true;
var previewBubble;
var lastTypes = [undefined, bubble.type];
function createPreviewBubble() {
var nextType;
do {
nextType = Math.floor(Math.random() * maxSelectableBubble);
// Prevent ironbubble from being selected as preview bubble if ironBubblesEnabled
} while (nextType == lastTypes[0] && nextType == lastTypes[1] || ironBubblesEnabled && nextType === getMaxTypes());
lastTypes.shift();
lastTypes.push(nextType);
previewBubble = self.addChildAt(new Bubble(maxSelectableBubble, false, nextType), 0);
previewBubble.scale.set(.7, .7);
previewBubble.x = -90;
previewBubble.y = 20;
previewBubble.isFreeBubble = true;
// Add down event to allow switching with current bubble
previewBubble.interactive = true;
previewBubble.buttonMode = true;
previewBubble.down = function (x, y, obj) {
// Swap types between previewBubble and bubble
if (bubble && previewBubble) {
// Only allow swap if both are not fireballs
if (!bubble.isFireBall && !previewBubble.isFireBall) {
// Prevent swapping to ironbubble
if (ironBubblesEnabled && (bubble.type === getMaxTypes() || previewBubble.type === getMaxTypes())) {
return;
}
var tmpType = bubble.type;
bubble.type = previewBubble.type;
previewBubble.type = tmpType;
// Update graphics for both bubbles
if (bubble.children && bubble.children.length > 0) {
var old = bubble.children[0];
bubble.removeChild(old);
var newAsset = bubble.attachAsset('bubble' + bubble.type, {
anchorX: 0.5,
anchorY: 0.5
});
}
if (previewBubble.children && previewBubble.children.length > 0) {
var oldPrev = previewBubble.children[0];
previewBubble.removeChild(oldPrev);
var newPrevAsset = previewBubble.attachAsset('bubble' + previewBubble.type, {
anchorX: 0.5,
anchorY: 0.5
});
newPrevAsset.width = previewBubble.width;
newPrevAsset.height = previewBubble.height;
}
refreshHintLine && refreshHintLine();
}
}
};
}
createPreviewBubble();
self.fire = function () {
// Prevent firing if bubble is ironbubble (type === getMaxTypes() when ironBubblesEnabled)
if (ironBubblesEnabled && bubble.type === getMaxTypes()) {
// Optionally, you could play a sound or flash to indicate not allowed
return;
}
bulletsFired++;
LK.getSound('fireBubble').play(); // Play sound when the ball is fired
grid.fireBubble(bubble, self.angle);
bubble = previewBubble;
previewBubble.x = previewBubble.y = 0;
previewBubble.scale.set(1, 1);
createPreviewBubble();
};
self.angle = -Math.PI / 2;
self.getBubble = function () {
return bubble;
};
self.isFireBall = function () {
return bubble.isFireBall;
};
self.triggerFireBall = function () {
bubble.destroy();
bubble = self.addChild(new Bubble(getMaxTypes(), true));
bubble.isFreeBubble = true;
};
});
var ScoreIndicatorLabel = Container.expand(function (score, type) {
var self = Container.call(this);
// Determine fill color: grey for ironbubble, yellow for bubble3, purple for bubble4, else normal color
var fillColor;
if (ironBubblesEnabled && type === getMaxTypes()) {
fillColor = "#888888";
} else if (type === 3) {
fillColor = "#ffff00";
} else if (type === 4) {
fillColor = "#cb2bff";
} else if (type === 5) {
fillColor = "#28f2f0";
} else if (type === 6) {
fillColor = "#ff9c00";
} else if (type === 7) {
fillColor = "#001f4d";
} else {
fillColor = "#" + (bubbleColors[type] !== undefined ? bubbleColors[type].toString(16).padStart(6, '0') : "ffffff");
}
// Ensure score is always a string for Text2
var label = new Text2(score !== undefined ? score.toString() : "0", {
size: 100,
fill: fillColor,
font: "Impact"
});
label.anchor.set(0.5, 0);
self.addChild(label);
self.update = function () {
self.y -= 7;
self.alpha -= .05;
if (self.alpha <= 0) {
self.destroy();
increaseScore(score);
}
};
});
var ScoreMultipliers = Container.expand(function (baseValue) {
var self = Container.call(this);
// Create a score label text string for ScoreMultipliers
var scoreMultiplierLabel = new Text2(baseValue, {
size: 100,
fill: 0x3954FF,
font: "Impact"
});
scoreMultiplierLabel.anchor.set(0.5, 0);
self.addChild(scoreMultiplierLabel);
var currentMultiplier = 1;
self.applyBubble = function (bubble) {
var scoreIndicator = game.addChild(new ScoreIndicatorLabel(baseValue * currentMultiplier, bubble.type));
scoreIndicator.x = self.x;
scoreIndicator.y = self.y;
var particle = particlesLayer.addChild(new BubbleRemoveParticle());
particle.x = bubble.x;
particle.y = self.y + 150;
};
self.setMultiplier = function (multiplier) {
currentMultiplier = multiplier;
scoreMultiplierLabel.setText(baseValue * currentMultiplier);
};
});
var WarningLine = Container.expand(function () {
var self = Container.call(this);
var warning = self.attachAsset('warningstripe', {
anchorX: .5,
anchorY: .5
});
var warningOffset = Math.random() * 100;
var speed = Math.random() * 1 + 1;
self.update = function () {
warningOffset += speed;
warning.alpha = (Math.cos(warningOffset / 50) + 1) / 2 * 0.3 + .7;
};
warning.blendMode = 1;
warning.rotation = .79;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x0c0d25
});
/****
* Game Code
****/
var gridSpeed = .5;
var colorBallCount = 3; // Default, will be set by menu
// Iron bubbles option flag and score multiplier
var ironBubblesEnabled = false;
var ironScoreMultiplier = 1.0;
// Show menu to select number of color balls using LK.gui
var menuOverlay = new Container();
var menuBG = LK.getAsset('uxoverlay', {
anchorX: 0,
anchorY: 0
});
menuBG.width = 1200;
menuBG.height = 1200;
menuOverlay.addChild(menuBG);
// Center the menu overlay vertically and horizontally
menuBG.x = -600;
menuBG.y = -600;
var menuTitle = new Text2("Bubble Shooter", {
size: 120,
fill: 0xffffff,
font: "Impact"
});
menuTitle.anchor.set(0.5, 0);
menuTitle.x = menuBG.x / 2 + 300;
menuTitle.y = menuBG.y + 50;
menuOverlay.addChild(menuTitle);
var menuDesc = new Text2("How many different color?", {
size: 80,
fill: 0xffffff,
font: "Impact"
});
menuDesc.anchor.set(0.5, 0);
menuDesc.x = menuBG.x / 2 + 300;
menuDesc.y = menuBG.y + 200;
menuOverlay.addChild(menuDesc);
var colorOptions = [{
label: "3 Colors",
value: 3
}, {
label: "4 Colors",
value: 4
}, {
label: "5 Colors",
value: 5
}, {
label: "6 Colors",
value: 6
}, {
label: "7 Colors",
value: 7
}, {
label: "8 Colors",
value: 8
}];
var menuButtons = [];
// Vertically center the menu buttons below the menu description
var buttonSpacing = 125;
var totalButtonHeight = colorOptions.length * buttonSpacing;
var buttonStartY = menuBG.y + 350;
function onMenuSelect(option) {
colorBallCount = option.value;
maxSelectableBubble = colorBallCount;
menuActive = false;
if (menuOverlay.parent) {
menuOverlay.parent.removeChild(menuOverlay);
}
// Show iron bubble selection menu
showIronBubbleMenu();
}
function showIronBubbleMenu() {
menuActive = true;
var ironMenuOverlay = new Container();
var ironMenuBG = LK.getAsset('uxoverlay2', {
anchorX: 0,
anchorY: 0
});
ironMenuBG.width = 900;
ironMenuBG.height = 700;
ironMenuOverlay.addChild(ironMenuBG);
ironMenuBG.x = -450;
ironMenuBG.y = -350;
var ironMenuTitle = new Text2("Iron Bubbles?", {
size: 100,
fill: 0xffffff,
font: "Impact"
});
ironMenuTitle.anchor.set(0.5, 0);
ironMenuTitle.x = ironMenuBG.x / 2 + 225;
ironMenuTitle.y = ironMenuBG.y + 60;
ironMenuOverlay.addChild(ironMenuTitle);
var ironMenuDesc = new Text2("Do you want to include iron bubbles?", {
size: 60,
fill: 0xffffff,
font: "Impact"
});
ironMenuDesc.anchor.set(0.5, 0);
ironMenuDesc.x = ironMenuBG.x / 2 + 225;
ironMenuDesc.y = ironMenuBG.y + 200;
ironMenuOverlay.addChild(ironMenuDesc);
var yesBtn = new Text2("Yes", {
size: 90,
fill: 0x3954FF,
font: "Impact",
stroke: 0xffffff,
strokeThickness: 8
});
yesBtn.anchor.set(0.5, 0);
yesBtn.x = ironMenuBG.x / 2 + 225;
yesBtn.y = ironMenuBG.y + 350;
yesBtn.interactive = true;
yesBtn.buttonMode = true;
yesBtn.down = function (x, y, obj) {
ironBubblesEnabled = true;
if (ironMenuOverlay.parent) {
ironMenuOverlay.parent.removeChild(ironMenuOverlay);
}
menuActive = false;
// Reset scoreMultipliers and baseScores based on new colorBallCount
setupScoreMultipliers();
};
ironMenuOverlay.addChild(yesBtn);
var noBtn = new Text2("No", {
size: 90,
fill: 0x3954FF,
font: "Impact",
stroke: 0xffffff,
strokeThickness: 8
});
noBtn.anchor.set(0.5, 0);
noBtn.x = ironMenuBG.x / 2 + 225;
noBtn.y = ironMenuBG.y + 500;
noBtn.interactive = true;
noBtn.buttonMode = true;
noBtn.down = function (x, y, obj) {
ironBubblesEnabled = false;
if (ironMenuOverlay.parent) {
ironMenuOverlay.parent.removeChild(ironMenuOverlay);
}
menuActive = false;
// Reset scoreMultipliers and baseScores based on new colorBallCount
setupScoreMultipliers();
};
ironMenuOverlay.addChild(noBtn);
LK.gui.center.addChild(ironMenuOverlay);
}
// Helper to reset scoreMultipliers and baseScores after menu
function setupScoreMultipliers() {
for (var i = 0; i < scoreMultipliers.length; i++) {
if (scoreMultipliers[i].parent) {
scoreMultipliers[i].parent.removeChild(scoreMultipliers[i]);
}
}
scoreMultipliers = [];
// Set ironScoreMultiplier based on ironBubblesEnabled
ironScoreMultiplier = ironBubblesEnabled ? 1.3 : 1.0;
if (colorBallCount <= 3) {
baseScores = [100, 250, 500, 250, 100];
} else if (colorBallCount == 4) {
baseScores = [120, 300, 600, 300, 120];
} else if (colorBallCount == 5) {
baseScores = [150, 375, 750, 375, 150];
} else if (colorBallCount == 6) {
baseScores = [200, 500, 1000, 500, 200];
} else if (colorBallCount == 7) {
baseScores = [250, 625, 1250, 625, 250];
} else if (colorBallCount == 8) {
baseScores = [300, 750, 1500, 750, 300];
} else {
baseScores = [100, 250, 500, 250, 100];
}
for (var a = 0; a < 5; a++) {
var sm = new ScoreMultipliers(Math.round(baseScores[a] * ironScoreMultiplier));
sm.x = 2048 / 5 * a + 2048 / 10;
sm.y = 2300;
scoreMultipliers.push(sm);
game.addChild(sm);
}
}
for (var i = 0; i < colorOptions.length; i++) {
(function (idx) {
var opt = colorOptions[idx];
var btn = new Text2(opt.label, {
size: 90,
fill: 0xffffff,
font: "Impact",
stroke: 0x3954FF,
strokeThickness: 8
});
btn.anchor.set(0.5, 0);
btn.x = menuBG.x / 2 + 300;
btn.y = buttonStartY + idx * buttonSpacing;
btn.interactive = true;
btn.buttonMode = true;
btn.down = function (x, y, obj) {
onMenuSelect(opt);
};
menuOverlay.addChild(btn);
menuButtons.push(btn);
})(i);
}
LK.gui.center.addChild(menuOverlay);
function increaseScore(amount) {
var currentScore = LK.getScore();
var newScore = currentScore + amount;
LK.setScore(newScore); // Update the game score using LK method
scoreLabel.setText(newScore.toString()); // Update the score label with the new score
}
//Game size 2048x2732
/*
Todo:
[X] Make sure we GC nodes that drop of screen
[ ] Make preview line fade out at the end
*/
var bulletsFired = 0; //3*30+1
var bubbleSize = 150;
var gameIsStarted = false;
var bubbleColors = [0xff2853, 0x44d31f, 0x5252ff, 0xcb2bff, 0x28f2f0, 0xffc411];
var barriers = [];
var maxSelectableBubble = colorBallCount;
var warningLines = [];
for (var a = 0; a < 13; a++) {
var wl = game.addChild(new WarningLine());
wl.x = 2048 / 13 * (a + .5);
wl.y = 2200;
wl.alpha = 0;
warningLines.push(wl);
wl.scale.set(16, 40);
}
var warningOverlay = game.attachAsset('dangeroverlay', {});
warningOverlay.y = 2280;
var uxoverlay = game.attachAsset('uxoverlay', {});
uxoverlay.y = 2440;
var uxoverlay2 = game.attachAsset('uxoverlay2', {});
uxoverlay2.y = 2460;
for (var a = 0; a < 4; a++) {
for (var b = 0; b < 3; b++) {
var barrier = game.addChild(new Barrier());
barrier.y = 2732 - 450 + b * 70;
barrier.x = 2048 / 5 * a + 2048 / 5;
barriers.push(barrier);
}
var barrierBlock = game.attachAsset('barrierblock', {});
barrierBlock.x = 2048 / 5 * a + 2048 / 5;
barrierBlock.y = 2732 - 450;
barrierBlock.anchor.x = .5;
}
// Create a score label
var scoreLabel = new Text2('0', {
size: 120,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 15,
font: "Impact"
});
scoreLabel.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreLabel);
var scoreMultipliers = [];
// Score multiplier logic based on colorBallCount
var baseScores;
if (colorBallCount <= 3) {
baseScores = [100, 250, 500, 250, 100];
} else if (colorBallCount == 4) {
baseScores = [120, 300, 600, 300, 120];
} else if (colorBallCount == 5) {
baseScores = [150, 375,, 750, 375, 150];
} else if (colorBallCount == 6) {
baseScores = [200, 500, 1000, 500, 200];
} else if (colorBallCount == 7) {
baseScores = [250, 625, 1250, 625, 250];
} else if (colorBallCount == 8) {
baseScores = [350, 875, 1750, 875, 350];
} else {
baseScores = [100, 250, 500, 250, 100];
}
for (var a = 0; a < 5; a++) {
var sm = new ScoreMultipliers(Math.round(baseScores[a] * ironScoreMultiplier));
sm.x = 2048 / 5 * a + 2048 / 10;
sm.y = 2300;
scoreMultipliers.push(sm);
game.addChild(sm);
}
var bonusUX = game.addChild(new BonusUX());
var fireBallPowerupOverlay = game.addChild(new FireBallPowerupOverlay());
var particlesLayer = game.addChild(new Container());
var grid = game.addChild(new Grid());
grid.y = 1000;
var freeBubbleLayer = game.addChild(new Container());
var hintBubblePlayer = game.addChild(new Container());
var launcher = game.addChild(new Launcher());
launcher.x = game.width / 2;
launcher.y = game.height - 138;
var hintBubbleCache = [];
var hintBubbles = [];
var isValid = false;
var path = [];
var bubbleAlpha = 1;
var hintTargetX = game.width / 2;
var hintTargetY = 0;
// Prevent game interaction before menu selection
var menuActive = true;
game.move = function (x, y, obj) {
if (menuActive) {
return;
}
hintTargetX = x;
hintTargetY = y;
refreshHintLine();
// }
};
game.down = function (x, y, obj) {
if (menuActive) {
return;
}
game.move(x, y, obj);
};
function getMaxTypes() {
return colorBallCount;
}
function refreshHintLine() {
var ox = hintTargetX - launcher.x;
var oy = hintTargetY - launcher.y;
var angle = Math.atan2(oy, ox);
launcher.angle = angle;
isValid = angle < -.2 && angle > -Math.PI + .2;
if (isValid) {
path = grid.calculatePath(launcher, angle);
//This allows updated faster than 60fps, making everyting feel better.
}
renderHintBubbels();
}
var hintOffset = 0;
var distanceBetweenHintbubbles = 100;
function renderHintBubbels() {
if (isValid) {
hintOffset = hintOffset % distanceBetweenHintbubbles;
var distanceSinceLastDot = -hintOffset + 100;
var hintBubbleOffset = 0;
var lastPoint = path[0];
var bubble = launcher.getBubble();
var tint;
if (bubble.isFireBall) {
tint = 0xff9c00;
} else if (bubble.type === 3) {
tint = 0xffff00; // yellow
} else if (bubble.type === 4) {
tint = 0xcb2bff; // purple
} else if (bubble.type === 5) {
tint = 0x28f2f0; // cyan
} else if (bubble.type === 6) {
tint = 0xff9c00; // orange
} else if (bubble.type === 7) {
tint = 0x001f4d; // navy blue
} else {
tint = bubbleColors[bubble.type];
}
var updateTint = true;
for (var a = 1; a < path.length; a++) {
var p2 = path[a];
var ox = p2.x - lastPoint.x;
var oy = p2.y - lastPoint.y;
var dist = Math.sqrt(ox * ox + oy * oy);
distanceSinceLastDot += dist;
if (distanceSinceLastDot >= distanceBetweenHintbubbles) {
var amountOver = distanceSinceLastDot - distanceBetweenHintbubbles;
var angle = Math.atan2(oy, ox);
var currentBubble = hintBubbles[hintBubbleOffset];
if (!currentBubble) {
currentBubble = hintBubbles[hintBubbleOffset] = new HintBubble();
hintBubblePlayer.addChild(currentBubble);
}
currentBubble.alpha = bubbleAlpha;
currentBubble.visible = true;
var currentTint = currentBubble.getTint();
if (hintBubbleOffset == 0) {
currentBubble.setTint(tint);
} else if (updateTint && currentTint != tint || currentTint == 0xffffff) {
currentBubble.setTint(tint);
updateTint = false;
}
currentBubble.x = lastPoint.x - Math.cos(angle) * amountOver;
currentBubble.y = lastPoint.y - Math.sin(angle) * amountOver;
hintBubbleOffset++;
distanceSinceLastDot = 0;
lastPoint = currentBubble;
} else {
lastPoint = p2;
}
}
for (var a = hintBubbleOffset; a < hintBubbles.length; a++) {
hintBubbles[a].visible = false;
}
} else {
for (var a = 0; a < hintBubbles.length; a++) {
hintBubbles[a].alpha = bubbleAlpha;
}
}
}
game.update = function () {
if (menuActive) {
return;
}
hintOffset += 5;
if (isValid) {
bubbleAlpha = Math.min(bubbleAlpha + .05, 1);
} else {
bubbleAlpha = Math.max(bubbleAlpha - .05, 0);
}
refreshHintLine();
var alphaList = grid.calculateWarningScoreList();
for (var a = 0; a < warningLines.length; a++) {
var value = alphaList[a] / 3;
warningLines[a].alpha += (Math.min(value, .6) - warningLines[a].alpha) / 100;
warningLines[a].scale.y += (value * 60 - warningLines[a].scale.y) / 100;
}
};
game.up = function () {
if (menuActive) {
return;
}
if (isValid) {
launcher.fire();
}
};
;
;
;
;
;
;
;
;
;
;
Circular white gradient circle on black background. Gradient from white on the center to black on the outer edge all around.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
Soft straight Long red paint on black background. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
Fire ball. Bubble shooter game. Thin black outline.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
green notification bubble. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.