/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
swapsRemaining: 3,
currentLevel: 1,
totalBubblesPopped: 0,
coins: 0,
shopUpgrades: {
extraSwaps: 0,
powerBalls: 0,
timeBonus: 0
}
});
/****
* Classes
****/
// Logo animation moved to StartScreenOverlay class
var BallCounterOverlay = Container.expand(function () {
var self = Container.call(this);
// Position in bottom left area with margin for mobile
self.y = game.height - 280;
self.x = 200;
// Background circle - larger for mobile
var countBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
countBG.width = 180;
countBG.height = 180;
countBG.x = 0;
countBG.y = 0;
// Dropshadow for label
var labelShadow = self.addChild(new Text2(ballsRemaining, {
size: 80,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
labelShadow.anchor.set(.5, .5);
labelShadow.x = 3;
labelShadow.y = 3;
// Main label
var label = self.addChild(new Text2(ballsRemaining, {
size: 80,
fill: 0xFFFFFF,
font: "Impact"
}));
label.anchor.set(.5, .5);
label.x = 0;
label.y = 0;
// Ball icon using bubble asset
var ballIcon = self.attachAsset('bubble0', {
anchorX: 0.5,
anchorY: 0.5
});
ballIcon.x = -90;
ballIcon.y = 0;
ballIcon.width = 90;
ballIcon.height = 90;
ballIcon.alpha = 0.7;
self.updateCount = function () {
label.setText(ballsRemaining);
labelShadow.setText(ballsRemaining);
// Change color based on remaining balls
if (ballsRemaining <= 5) {
label.fill = 0xff0000; // Red when low
countBG.tint = 0xff4444;
} else if (ballsRemaining <= 10) {
label.fill = 0xffff00; // Yellow when medium
countBG.tint = 0xffff44;
} else {
label.fill = 0xFFFFFF; // White when plenty
countBG.tint = 0xffffff;
}
// Fade out when no balls remaining
self.alpha = ballsRemaining > 0 ? 1 : 0.5;
};
// Initialize display
self.updateCount();
return self;
});
var Barrier = Container.expand(function () {
var self = Container.call(this);
var barrierGraphics = self.attachAsset('barrier', {
anchorX: .5,
anchorY: .5
});
});
var BombBall = Container.expand(function (max_types, type) {
var self = Container.call(this);
self.isFireBall = true;
self.isBombBall = true;
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);
}
}
var bubbleGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
bubbleGraphics.width = 150;
bubbleGraphics.height = 150;
bubbleGraphics.tint = 0x333333; // Dark gray/black tint for bomb
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 (++spawnMod % 3 == 0 && self.parent) {
// Spawn bomb particles every 3 ticks
var angle = Math.random() * Math.PI * 2;
var bombParticle = self.parent.addChild(new BombParticle(angle));
bombParticle.x = self.x + Math.cos(angle) * self.width / 4;
bombParticle.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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
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 / 6))].applyBubble(self);
LK.getSound('scoreCollected').play();
}
}
};
return self;
});
var BombParticle = Container.expand(function (angle) {
var self = Container.call(this);
var particleGraphics = self.attachAsset('fireparticle', {
anchorX: 0.5,
anchorY: 0.5
});
particleGraphics.blendMode = 1;
particleGraphics.tint = 0x333333; // Dark gray/black tint for bomb particles
var speedX = Math.cos(angle) * 1.2;
var speedY = Math.sin(angle) * 1.2;
var rotationSpeed = Math.random() * 0.08 - 0.04;
self.update = function () {
self.x += speedX * self.alpha;
self.y += speedY * self.alpha;
particleGraphics.rotation += rotationSpeed;
self.alpha -= 0.012;
if (self.alpha <= 0) {
self.destroy();
}
};
return self;
});
var BonusUX = Container.expand(function () {
var self = Container.call(this);
//Insert label here
var barHeight = 50;
// Dropshadow for bonusLabel
var bonusLabelShadow = self.addChild(new Text2('Streak Bonus', {
size: 90,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
bonusLabelShadow.anchor.set(1, 1);
bonusLabelShadow.x = -10 + 4;
bonusLabelShadow.y = barHeight / 2 + 4;
// Main bonusLabel
var bonusLabel = self.addChild(new Text2('Streak Bonus', {
size: 90,
fill: 0xF4F5FF,
font: "Impact"
}));
bonusLabel.anchor.set(1, 1);
bonusLabel.y = barHeight / 2;
var rightMargin = -10;
bonusLabel.x = rightMargin;
// Dropshadow for bonusAmountLabel
var bonusAmountLabelShadow = self.addChild(new Text2('1x', {
size: 170,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
bonusAmountLabelShadow.anchor.set(.5, .5);
bonusAmountLabelShadow.x = 100 + 4;
bonusAmountLabelShadow.y = 4;
// Main bonusAmountLabel
var bonusAmountLabel = self.addChild(new Text2('1x', {
size: 170,
fill: 0xF4F5FF,
font: "Impact"
}));
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 = 24;
self.setStreakCount = function (level) {
self.streakCount = Math.min(level, maxLevel);
var newBonus = Math.floor(self.streakCount / 8) + 1;
if (newBonus != self.bonusAmount) {
LK.getSound('comboBonus').play();
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 {
var bubbleGraphics = self.attachAsset('bubble' + self.type, {
anchorX: 0.5,
anchorY: 0.5
});
}
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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
// 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 / 6))].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 ColorChangerBubble = Container.expand(function () {
var self = Container.call(this);
// Use a rainbow-tinted bubble to represent color changer
var bubbleGraphics = self.attachAsset('bubble0', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it visually distinct with rainbow effect
bubbleGraphics.tint = 0xffffff;
self.type = -4; // Special type for color changer bubble
self.isColorChangerBubble = true;
self.isAttached = true;
self.isFreeBubble = false;
var speedX = 0;
var speedY = 0;
self.targetX = 0;
self.targetY = 0;
var colorCycleTime = 0;
self.setPos = function (x, y) {
self.x = self.targetX = x;
self.y = self.targetY = y;
};
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;
};
self.update = function () {
if (self.isFreeBubble) {
return;
}
if (self.isAttached) {
// Rainbow color cycling effect
colorCycleTime += 0.1;
var red = Math.sin(colorCycleTime) * 127 + 128;
var green = Math.sin(colorCycleTime + 2) * 127 + 128;
var blue = Math.sin(colorCycleTime + 4) * 127 + 128;
bubbleGraphics.tint = Math.floor(red) << 16 | Math.floor(green) << 8 | Math.floor(blue);
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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
var restitution = 0.6;
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// When color changer reaches bottom, change all bubbles to same color
if (self.y > 2732 - 400) {
self.activateColorChanger();
self.destroy();
}
}
};
self.activateColorChanger = function () {
// Pick a random color for all bubbles to change to
var newColor = Math.floor(Math.random() * getMaxTypes());
var bubblesChanged = 0;
// Change all grid bubbles to the new color
for (var row = 0; row < grid.container.children.length; row++) {
var bubble = grid.container.children[row];
if (bubble && bubble.isAttached && bubble.type >= 0 && bubble.type < 6) {
bubble.type = newColor;
bubblesChanged++;
}
}
if (bubblesChanged > 0) {
LK.getSound('powerupSwoosh').play();
LK.getSound('bubbleSwap').play();
// Visual effect
LK.effects.flashScreen(0xffffff, 500);
// Show effect text
var effectText = game.addChild(new Text2('COLOR CHANGE!', {
size: 120,
fill: 0xffffff,
font: "Impact"
}));
effectText.anchor.set(0.5, 0.5);
effectText.x = game.width / 2;
effectText.y = game.height / 2;
effectText.alpha = 1;
tween(effectText, {
y: effectText.y - 200,
alpha: 0
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
effectText.destroy();
}
});
}
};
return self;
});
var DoubleCoinsBubble = Container.expand(function () {
var self = Container.call(this);
// Use a gold-tinted bubble to represent double coins
var bubbleGraphics = self.attachAsset('bubble5', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it visually distinct with gold tint
bubbleGraphics.tint = 0xffd700;
self.type = -6; // Special type for double coins bubble
self.isDoubleCoinsBubble = true;
self.isAttached = true;
self.isFreeBubble = false;
var speedX = 0;
var speedY = 0;
self.targetX = 0;
self.targetY = 0;
var glowPulse = 0;
self.setPos = function (x, y) {
self.x = self.targetX = x;
self.y = self.targetY = y;
};
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;
};
self.update = function () {
if (self.isFreeBubble) {
return;
}
if (self.isAttached) {
// Gold pulsing effect
glowPulse += 0.15;
var glowIntensity = Math.sin(glowPulse) * 0.3 + 0.7;
bubbleGraphics.alpha = glowIntensity;
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 += 0.8;
speedX *= 0.995;
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75;
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
var restitution = 0.6;
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// When double coins bubble reaches bottom, activate double coins
if (self.y > 2732 - 400) {
self.activateDoubleCoins();
self.destroy();
}
}
};
self.activateDoubleCoins = function () {
// Enable double coins for 15 seconds
doubleCoinsActive = true;
LK.getSound('powerupSwoosh').play();
LK.getSound('scoreCollected').play();
// Visual effect
LK.effects.flashScreen(0xffd700, 500);
// Show effect text
var effectText = game.addChild(new Text2('DOUBLE COINS!', {
size: 120,
fill: 0xffd700,
font: "Impact"
}));
effectText.anchor.set(0.5, 0.5);
effectText.x = game.width / 2;
effectText.y = game.height / 2;
effectText.alpha = 1;
tween(effectText, {
y: effectText.y - 200,
alpha: 0
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
effectText.destroy();
}
});
// Disable double coins after 15 seconds
LK.setTimeout(function () {
doubleCoinsActive = false;
}, 15000);
};
return self;
});
var ExplosiveBubble = Container.expand(function () {
var self = Container.call(this);
// Use a red-tinted bubble to represent explosive
var bubbleGraphics = self.attachAsset('bubble0', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it visually distinct with red tint
bubbleGraphics.tint = 0xff4444;
self.type = -3; // Special type for explosive bubble
self.isExplosiveBubble = true;
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;
};
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;
};
self.explode = function () {
// Create explosion effect
var explosion = game.addChild(new BubbleRemoveParticle());
explosion.x = self.x;
explosion.y = self.y;
explosion.scale.set(3, 3);
// Find all bubbles within explosion radius
var explosionRadius = 200;
var bubblesInRadius = [];
// Check all grid bubbles
for (var row = 0; row < grid.container.children.length; row++) {
var bubble = grid.container.children[row];
if (bubble && bubble.isAttached) {
var dx = bubble.x - (self.x - grid.x);
var dy = bubble.y - (self.y - grid.y);
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= explosionRadius) {
bubblesInRadius.push(bubble);
}
}
}
// Remove bubbles in explosion radius
if (bubblesInRadius.length > 0) {
grid.removeBubbles(bubblesInRadius);
var disconnected = grid.getDetachedBubbles();
grid.removeBubbles(disconnected);
// Track mission progress for exploded bubbles
if (missionActive) {
for (var i = 0; i < bubblesInRadius.length; i++) {
var bubble = bubblesInRadius[i];
if (bubble.type >= 0 && bubble.type < 3) {
missionCollected[bubble.type]++;
}
}
for (var i = 0; i < disconnected.length; i++) {
var bubble = disconnected[i];
if (bubble.type >= 0 && bubble.type < 3) {
missionCollected[bubble.type]++;
}
}
}
}
LK.getSound('explosiveBubble').play();
LK.getSound('fireBubble').play();
LK.getSound('powerupThump').play();
};
self.update = function () {
if (self.isFreeBubble) {
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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// When explosive bubble reaches bottom, explode
if (self.y > 2732 - 400) {
self.explode();
self.destroy();
}
}
};
return self;
});
var FireBallPowerupOverlay = Container.expand(function () {
var self = Container.call(this);
var bubbleGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
// Larger size for mobile touch targets
bubbleGraphics.width = 180;
bubbleGraphics.height = 180;
self.y = game.height - 180;
self.x = 250;
var countBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
// Larger background for better touch target
countBG.width = 160;
countBG.height = 160;
countBG.x = 110;
countBG.y = 60;
self.fireballsLeft = 3; // Start with 3 fireballs
// Dropshadow for label
var labelShadow = self.addChild(new Text2(self.fireballsLeft, {
size: 70,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
labelShadow.anchor.set(.5, .5);
labelShadow.x = 90 + 3;
labelShadow.y = 50 + 3;
// Main label
var label = self.addChild(new Text2(self.fireballsLeft, {
size: 70,
fill: 0xFFFFFF,
font: "Impact"
}));
label.anchor.set(.5, .5);
label.x = 90;
label.y = 50;
self.alpha = 0.5; // Start with greyed out overlay
self.increaseFireballCount = function () {
self.fireballsLeft++;
self.updateDisplay();
tween(self.scale, {
x: 1.3,
y: 1.3
}, {
duration: 120,
easing: tween.cubicOut,
onFinish: function onFinish() {
tween(self.scale, {
x: 1,
y: 1
}, {
duration: 220,
easing: tween.bounceOut
});
}
});
// Spawn powerup particles
for (var i = 0; i < 12; i++) {
var angle = Math.random() * Math.PI * 2;
var speed = 8 + Math.random() * 6;
var particle = game.addChild(new PowerupParticle(angle, speed));
particle.x = self.x + 90; // center of indicator
particle.y = self.y + 50;
}
};
self.updateDisplay = function () {
label.setText(self.fireballsLeft);
labelShadow.setText(self.fireballsLeft);
self.alpha = self.fireballsLeft > 0 ? 1 : 0.5;
};
self.down = function () {
if (self.fireballsLeft > 0 && !launcher.isFireBall()) {
self.fireballsLeft--;
label.setText(self.fireballsLeft);
labelShadow.setText(self.fireballsLeft);
// Cycle through fireball types: normal -> ice -> laser -> bomb -> normal
var fireballType = (4 - self.fireballsLeft) % 4;
launcher.triggerFireBall(fireballType);
if (self.fireballsLeft == 0) {
self.alpha = .5;
}
}
};
// State for wiggle animation
self.isWiggling = false;
// Update method to handle wiggle animation
self.update = function () {
// Check if bubbles are getting close to bottom and we have powerups
if (self.fireballsLeft > 0 && !self.isWiggling) {
// Get warning scores from grid
var warningScores = grid.calculateWarningScoreList();
var maxWarning = 0;
for (var i = 0; i < warningScores.length; i++) {
if (warningScores[i] > maxWarning) {
maxWarning = warningScores[i];
}
}
// If any column has high warning score (bubbles close to bottom), trigger wiggle
if (maxWarning > 1.5) {
// Threshold for "getting close"
self.isWiggling = true;
// First wiggle to the right
tween(self, {
rotation: 0.15
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
// Then wiggle to the left
tween(self, {
rotation: -0.15
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Then wiggle to the right again
tween(self, {
rotation: 0.15
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Return to normal position
tween(self, {
rotation: 0
}, {
duration: 100,
easing: tween.easeIn,
onFinish: function onFinish() {
// Reset wiggle state after a cooldown
LK.setTimeout(function () {
self.isWiggling = false;
}, 2000); // 2 second cooldown before next wiggle
}
});
}
});
}
});
}
});
}
}
};
});
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;
// Determine if this row should have a powerup
var shouldSpawnPowerup = false;
var shouldSpawnTimeBubble = false;
var shouldSpawnExplosiveBubble = false;
var powerupCol = -1;
var POWERUP_ROW_INTERVAL = 20; // Every 20th row has a powerup (was 10)
var TIME_BUBBLE_INTERVAL = 15; // Every 15th row might have a time bubble
var EXPLOSIVE_BUBBLE_INTERVAL = 25; // Every 25th row might have an explosive bubble
// Move first powerup spawn to row 6, and then every POWERUP_ROW_INTERVAL after that
if (rowCount === 6) {
shouldSpawnPowerup = true;
powerupCol = Math.floor(Math.random() * rowWidth);
} else if (rowCount - 6 > 0 && (rowCount - 6) % POWERUP_ROW_INTERVAL === 0) {
shouldSpawnPowerup = true;
powerupCol = Math.floor(Math.random() * rowWidth);
}
// Check for time bubble spawn (level 3+)
if (currentGameLevel >= 3 && rowCount > 10 && rowCount % TIME_BUBBLE_INTERVAL === 0 && Math.random() < 0.3) {
shouldSpawnTimeBubble = true;
powerupCol = Math.floor(Math.random() * rowWidth);
}
// Check for explosive bubble spawn (level 5+)
if (currentGameLevel >= 5 && rowCount > 15 && rowCount % EXPLOSIVE_BUBBLE_INTERVAL === 0 && Math.random() < 0.2) {
shouldSpawnExplosiveBubble = true;
powerupCol = Math.floor(Math.random() * rowWidth);
}
// Check for other power-up spawns
var shouldSpawnColorChanger = false;
var shouldSpawnSlowTime = false;
var shouldSpawnDoubleCoins = false;
// Color Changer (level 4+, every 30 rows, 15% chance)
if (currentGameLevel >= 4 && rowCount > 12 && rowCount % 30 === 0 && Math.random() < 0.15) {
shouldSpawnColorChanger = true;
powerupCol = Math.floor(Math.random() * rowWidth);
}
// Slow Time (level 6+, every 35 rows, 12% chance)
if (currentGameLevel >= 6 && rowCount > 18 && rowCount % 35 === 0 && Math.random() < 0.12) {
shouldSpawnSlowTime = true;
powerupCol = Math.floor(Math.random() * rowWidth);
}
// Double Coins (level 3+, every 25 rows, 20% chance)
if (currentGameLevel >= 3 && rowCount > 8 && rowCount % 25 === 0 && Math.random() < 0.20) {
shouldSpawnDoubleCoins = true;
powerupCol = Math.floor(Math.random() * rowWidth);
}
for (var a = 0; a < rowWidth; a++) {
var bubble;
if (shouldSpawnPowerup && a === powerupCol) {
bubble = new PowerupBubble();
} else if (shouldSpawnTimeBubble && a === powerupCol) {
bubble = new TimeBubble();
} else if (shouldSpawnExplosiveBubble && a === powerupCol) {
bubble = new ExplosiveBubble();
} else if (shouldSpawnColorChanger && a === powerupCol) {
bubble = new ColorChangerBubble();
} else if (shouldSpawnSlowTime && a === powerupCol) {
bubble = new SlowTimeBubble();
} else if (shouldSpawnDoubleCoins && a === powerupCol) {
bubble = new DoubleCoinsBubble();
} 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) {
if (neighbor.isPowerup || neighbor.isTimeBubble || neighbor.isExplosiveBubble || neighbor.isColorChangerBubble || neighbor.isSlowTimeBubble || neighbor.isDoubleCoinsBubble) {
// Special bubbles connect with everything
queue.push(neighbor);
} else if (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 >= 0 && 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, 130 / 2), 2048 - 130 / 2);
LK.getSound('circleBounce').play();
}
var intersectedBubble = self.bubbleIntersectsGrid(nextX + self.x, nextY + self.y);
if (intersectedBubble) {
gameIsStarted = true;
if (bubble.isFireBall) {
if (bubble.isIceBall) {
// Ice ball freezes bubbles in a 3x3 area for 3 seconds
var freezeRadius = 300;
var frozenBubbles = [];
for (var row = 0; row < rows.length; row++) {
for (var col = 0; col < rows[row].length; col++) {
var gridBubble = rows[row][col];
if (gridBubble && gridBubble.isAttached) {
var dx = gridBubble.x - (bubble.x - self.x);
var dy = gridBubble.y - (bubble.y - self.y);
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= freezeRadius) {
frozenBubbles.push(gridBubble);
}
}
}
}
// Freeze effect - slow down grid movement temporarily
var originalSpeed = gridSpeed;
gridSpeed *= 0.1; // Slow down to 10% speed
LK.setTimeout(function () {
gridSpeed = originalSpeed;
}, 3000);
// Visual freeze effect
for (var i = 0; i < frozenBubbles.length; i++) {
var frozenBubble = frozenBubbles[i];
frozenBubble.alpha = 0.5;
frozenBubble.tint = 0x88ccff;
LK.setTimeout(function () {
frozenBubble.alpha = 1;
frozenBubble.tint = 0xffffff;
}, 3000);
}
self.removeBubbles([intersectedBubble]);
} else if (bubble.isLaserBall) {
// Laser ball creates a vertical line destruction
var laserColumn = Math.floor((bubble.x - self.x) / bubbleSize);
var destroyedBubbles = [];
for (var row = 0; row < rows.length; row++) {
for (var col = 0; col < rows[row].length; col++) {
var gridBubble = rows[row][col];
if (gridBubble && Math.abs(col - laserColumn) <= 1) {
destroyedBubbles.push(gridBubble);
}
}
}
self.removeBubbles(destroyedBubbles);
} else if (bubble.isBombBall) {
// Bomb ball explodes in a large radius destroying many bubbles
var bombRadius = 250;
var explodedBubbles = [];
for (var row = 0; row < rows.length; row++) {
for (var col = 0; col < rows[row].length; col++) {
var gridBubble = rows[row][col];
if (gridBubble && gridBubble.isAttached) {
var dx = gridBubble.x - (bubble.x - self.x);
var dy = gridBubble.y - (bubble.y - self.y);
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= bombRadius) {
explodedBubbles.push(gridBubble);
}
}
}
}
// Create explosion effect
var explosion = game.addChild(new BubbleRemoveParticle());
explosion.x = bubble.x;
explosion.y = bubble.y;
explosion.scale.set(4, 4);
explosion.alpha = 0.9;
LK.getSound('explosiveBubble').play();
LK.getSound('fireBubble').play();
self.removeBubbles(explodedBubbles);
} else {
// Regular fireball
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);
LK.getSound('bubblePop').play(); // Sound for popping bubbles
LK.getSound('powerupThump').play(); // Additional sound for popping bubbles
var disconnected = self.getDetachedBubbles();
self.removeBubbles(disconnected);
// Handle special bubble effects before removal
for (var specialBubbleIndex = 0; specialBubbleIndex < bubbles.length; specialBubbleIndex++) {
var specialBubble = bubbles[specialBubbleIndex];
if (specialBubble.isTimeBubble) {
// Add time when time bubble is removed
if (missionActive) {
missionTimeLeft += 10 * 60; // 10 seconds in ticks
}
} else if (specialBubble.isExplosiveBubble) {
// Trigger explosion effect
specialBubble.explode();
} else if (specialBubble.isColorChangerBubble) {
// Activate color changer effect
specialBubble.activateColorChanger();
} else if (specialBubble.isSlowTimeBubble) {
// Activate slow time effect
specialBubble.activateSlowTime();
} else if (specialBubble.isDoubleCoinsBubble) {
// Activate double coins effect
specialBubble.activateDoubleCoins();
}
}
// Track mission progress for removed bubbles
if (missionActive) {
for (var missionBubbleIndex = 0; missionBubbleIndex < bubbles.length; missionBubbleIndex++) {
var missionBubble = bubbles[missionBubbleIndex];
if (missionBubble.type >= 0 && missionBubble.type < 3) {
missionCollected[missionBubble.type]++;
}
}
for (var disconnectedBubbleIndex = 0; disconnectedBubbleIndex < disconnected.length; disconnectedBubbleIndex++) {
var disconnectedBubble = disconnected[disconnectedBubbleIndex];
if (disconnectedBubble.type >= 0 && disconnectedBubble.type < 3) {
missionCollected[disconnectedBubble.type]++;
}
}
// Check if mission is complete
if (checkMissionComplete()) {
missionActive = false;
LK.effects.flashScreen(0x00ff00, 1000); // Green flash for success
LK.getSound('missionComplete').play();
LK.getSound('powerupThump').play();
LK.getSound('scoreCollected').play(); // Additional sound for mission completion
// Calculate coins earned for completing level
var coinsEarned = 50 + currentGameLevel * 25;
if (doubleCoinsActive) {
coinsEarned *= 2; // Double coins when power-up is active
}
storage.coins += coinsEarned;
// Show coins earned display
var coinsEarnedText = game.addChild(new Text2('+' + coinsEarned + ' Coins!', {
size: 120,
fill: 0xFFD700,
font: "Impact"
}));
coinsEarnedText.anchor.set(0.5, 0.5);
coinsEarnedText.x = game.width / 2;
coinsEarnedText.y = game.height / 2 + 100;
coinsEarnedText.alpha = 0;
// Animate coins earned text
tween(coinsEarnedText, {
alpha: 1,
y: game.height / 2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
// Hold display for 1.5 seconds then fade out
LK.setTimeout(function () {
tween(coinsEarnedText, {
alpha: 0,
y: game.height / 2 - 100
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
coinsEarnedText.destroy();
}
});
}, 1500);
}
});
// Advance to next level
currentGameLevel++;
// Refresh ball counter for new level
refreshBallCounter();
if (currentGameLevel > maxGameLevel) {
// Player completed all levels - show victory
LK.showYouWin();
} else {
// Start next level mission after a delay
LK.setTimeout(function () {
initializeMission();
}, 2000);
}
// Advance to next level
currentGameLevel++;
// Refresh ball counter for new level
refreshBallCounter();
LK.getSound('powerupSwoosh').play();
if (currentGameLevel > maxGameLevel) {
// Player completed all levels - show victory
LK.showYouWin();
} else {
// Start next level mission after a delay
LK.setTimeout(function () {
initializeMission();
}, 2000);
}
}
}
bonusUX.setStreakCount(bonusUX.streakCount + 1);
// Track bubbles popped and restore swaps
storage.totalBubblesPopped += bubbles.length + disconnected.length;
// Restore 1 swap for successful pop, up to maximum of 3
if (storage.swapsRemaining < 3) {
storage.swapsRemaining++;
}
// Level progression: every 50 bubbles popped = next level
var newLevel = Math.floor(storage.totalBubblesPopped / 50) + 1;
if (newLevel > storage.currentLevel) {
storage.currentLevel = newLevel;
levelDisplayOverlay.updateLevel();
// Refresh ball counter for new level
refreshBallCounter();
LK.getSound('levelUp').play();
LK.getSound('powerupSwoosh').play();
// Update grid speed for new level
gridSpeed = .5 + (storage.currentLevel - 1) * 0.1;
// Update max selectable bubble types
maxSelectableBubble = getMaxTypes();
}
} 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 * 0.5;
}
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);
LK.getSound('gameOverJingle').play();
LK.showGameOver();
}
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 IceBall = Container.expand(function (max_types, type) {
var self = Container.call(this);
self.isFireBall = true;
self.isIceBall = true;
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);
}
}
var bubbleGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
bubbleGraphics.width = 150;
bubbleGraphics.height = 150;
bubbleGraphics.tint = 0x88ccff; // Ice blue tint
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 (++spawnMod % 3 == 0 && self.parent) {
// Spawn ice particles every 3 ticks
var angle = Math.random() * Math.PI * 2;
var iceParticle = self.parent.addChild(new IceParticle(angle));
iceParticle.x = self.x + Math.cos(angle) * self.width / 4;
iceParticle.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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
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 / 6))].applyBubble(self);
LK.getSound('scoreCollected').play();
}
}
};
return self;
});
var IceParticle = Container.expand(function (angle) {
var self = Container.call(this);
var particleGraphics = self.attachAsset('fireparticle', {
anchorX: 0.5,
anchorY: 0.5
});
particleGraphics.blendMode = 1;
particleGraphics.tint = 0x88ccff; // Ice blue tint
var speedX = Math.cos(angle) * 1.5;
var speedY = Math.sin(angle) * 1.5;
var rotationSpeed = Math.random() * 0.05 - 0.025;
self.update = function () {
self.x += speedX * self.alpha;
self.y += speedY * self.alpha;
particleGraphics.rotation += rotationSpeed;
self.alpha -= 0.008;
if (self.alpha <= 0) {
self.destroy();
}
};
return self;
});
var LaserBall = Container.expand(function (max_types, type) {
var self = Container.call(this);
self.isFireBall = true;
self.isLaserBall = true;
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);
}
}
var bubbleGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
bubbleGraphics.width = 150;
bubbleGraphics.height = 150;
bubbleGraphics.tint = 0xff00ff; // Magenta tint
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 (++spawnMod % 2 == 0 && self.parent) {
// Spawn laser particles every 2 ticks
var angle = Math.random() * Math.PI * 2;
var laserParticle = self.parent.addChild(new LaserParticle(angle));
laserParticle.x = self.x + Math.cos(angle) * self.width / 4;
laserParticle.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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
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 / 6))].applyBubble(self);
LK.getSound('scoreCollected').play();
}
}
};
return self;
});
var LaserParticle = Container.expand(function (angle) {
var self = Container.call(this);
var particleGraphics = self.attachAsset('fireparticle', {
anchorX: 0.5,
anchorY: 0.5
});
particleGraphics.blendMode = 1;
particleGraphics.tint = 0xff00ff; // Magenta tint
var speedX = Math.cos(angle) * 2;
var speedY = Math.sin(angle) * 2;
var rotationSpeed = Math.random() * 0.15 - 0.075;
self.update = function () {
self.x += speedX * self.alpha;
self.y += speedY * self.alpha;
particleGraphics.rotation += rotationSpeed;
self.alpha -= 0.015;
if (self.alpha <= 0) {
self.destroy();
}
};
return self;
});
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);
} while (nextType == lastTypes[lastTypes.length - 1]); // Prevent consecutive same types
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;
}
createPreviewBubble();
self.fire = function () {
// Check if balls remaining
if (ballsRemaining <= 0) {
return; // Can't fire if no balls left
}
bulletsFired++;
ballsRemaining--;
ballCounterOverlay.updateCount();
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();
// Check for game over if no balls left
if (ballsRemaining <= 0) {
LK.setTimeout(function () {
// Check if mission is still active and not completed
if (missionActive && !checkMissionComplete()) {
LK.effects.flashScreen(0xff0000, 3000);
LK.getSound('timeRunOut').play();
LK.getSound('gameOverJingle').play();
LK.showGameOver();
}
}, 1000); // Small delay to allow last ball to take effect
}
};
self.angle = -Math.PI / 2;
self.getBubble = function () {
return bubble;
};
self.isFireBall = function () {
return bubble.isFireBall;
};
self.triggerFireBall = function (type) {
bubble.destroy();
type = type || 0;
if (type === 1) {
bubble = self.addChild(new IceBall(getMaxTypes()));
} else if (type === 2) {
bubble = self.addChild(new LaserBall(getMaxTypes()));
} else if (type === 3) {
bubble = self.addChild(new BombBall(getMaxTypes()));
} else {
bubble = self.addChild(new Bubble(getMaxTypes(), true));
}
bubble.isFreeBubble = true;
};
self.swapBubbles = function () {
if (storage.swapsRemaining > 0) {
storage.swapsRemaining--;
// Swap current bubble with preview bubble
var tempType = bubble.type;
var tempIsFireBall = bubble.isFireBall;
// Store preview bubble properties
var previewType = previewBubble.type;
var previewIsFireBall = previewBubble.isFireBall;
// Destroy current bubbles
bubble.destroy();
previewBubble.destroy();
// Create new current bubble with preview properties
bubble = self.addChild(new Bubble(getMaxTypes(), previewIsFireBall, previewType));
bubble.isFreeBubble = true;
// Create new preview bubble with old current properties
previewBubble = self.addChildAt(new Bubble(getMaxTypes(), tempIsFireBall, tempType), 0);
previewBubble.scale.set(.7, .7);
previewBubble.x = -90;
previewBubble.y = 20;
previewBubble.isFreeBubble = true;
refreshHintLine();
LK.getSound('bubbleSwap').play();
LK.getSound('powerupSwoosh').play();
return true;
}
return false;
};
self.down = function () {
// Handle bubble swap when clicking on launcher
self.swapBubbles();
};
});
var LevelDisplayOverlay = Container.expand(function () {
var self = Container.call(this);
// Position in top left area with more margin for mobile
self.y = 160;
self.x = 200;
// Background circle - larger for mobile
var levelBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
levelBG.width = 180;
levelBG.height = 180;
levelBG.x = 0;
levelBG.y = 0;
// Dropshadow for label
var labelShadow = self.addChild(new Text2(storage.currentLevel, {
size: 80,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
labelShadow.anchor.set(.5, .5);
labelShadow.x = 3;
labelShadow.y = 3;
// Main label
var label = self.addChild(new Text2(storage.currentLevel, {
size: 80,
fill: 0xFFFFFF,
font: "Impact"
}));
label.anchor.set(.5, .5);
label.x = 0;
label.y = 0;
// Level text
var levelText = self.addChild(new Text2('SEVIYE', {
size: 36,
fill: 0xFFFFFF,
font: "Impact"
}));
levelText.anchor.set(.5, .5);
levelText.x = 0;
levelText.y = -60;
levelText.alpha = 0.8;
self.updateLevel = function () {
label.setText(storage.currentLevel);
labelShadow.setText(storage.currentLevel);
};
// Initialize display
self.updateLevel();
return self;
});
var MissionOverlay = Container.expand(function () {
var self = Container.call(this);
// Position in top right corner in GUI space - adjusted for mobile
self.x = 0;
self.y = 0;
// Mission background - larger for mobile readability
var missionBG = self.attachAsset('countbg', {
anchorX: 1,
anchorY: 0
});
missionBG.width = 420;
missionBG.height = 200;
missionBG.alpha = 0.9;
missionBG.x = -20;
missionBG.y = 120;
// Mission title - larger for mobile
var titleText = self.addChild(new Text2('GÖREV', {
size: 42,
fill: 0xFFFFFF,
font: "Impact"
}));
titleText.anchor.set(0.5, 0.5);
titleText.x = -210;
titleText.y = 150;
// Level indicator
var levelText = self.addChild(new Text2('LV 1', {
size: 28,
fill: 0xFFFF00,
font: "Impact"
}));
levelText.anchor.set(0.5, 0.5);
levelText.x = -210;
levelText.y = 175;
// Mission targets display - properly spaced
var targetTexts = [];
var targetColors = [0xff2853, 0x44d31f, 0x5252ff]; // Red, Green, Blue
for (var i = 0; i < 3; i++) {
var targetText = self.addChild(new Text2('0/0', {
size: 28,
fill: targetColors[i],
font: "Impact"
}));
targetText.anchor.set(0.5, 0.5);
targetText.x = -280 + i * 80;
targetText.y = 200;
targetTexts.push(targetText);
}
// Timer display - properly positioned
var timerText = self.addChild(new Text2('60s', {
size: 36,
fill: 0xFFFFFF,
font: "Impact"
}));
timerText.anchor.set(0.5, 0.5);
timerText.x = -210;
timerText.y = 235;
self.updateMission = function (targets, collected, timeLeft) {
for (var i = 0; i < 3; i++) {
targetTexts[i].setText(collected[i] + '/' + targets[i]);
}
timerText.setText(Math.ceil(timeLeft / 60) + 's');
levelText.setText('LV ' + currentGameLevel);
// Flash timer red when low
if (timeLeft < 600) {
// 10 seconds
timerText.fill = timeLeft % 20 < 10 ? 0xff0000 : 0xFFFFFF;
}
};
return self;
});
var PowerupBubble = Container.expand(function () {
var self = Container.call(this);
var bubbleGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
bubbleGraphics.width = 150;
bubbleGraphics.height = 150;
self.type = -1; // Special type for powerup
self.isPowerup = true;
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;
};
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) {
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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// When powerup reaches the bottom of the screen, trigger powerup earned animation
if (self.y > 2732 - 400) {
// Play sound
LK.getSound('scoreCollected').play();
// Create and start powerup earned animation
var animation = game.addChild(new PowerupEarnedAnimation(self.x, self.y));
animation.start();
// Destroy the original bubble
self.destroy();
}
}
};
});
var PowerupEarnedAnimation = Container.expand(function (startX, startY) {
var self = Container.call(this);
// Create a 2x size powerup graphic
var powerupGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
// Create dropshadow for earnedText (not a child of self, but a global overlay)
var earnedTextShadow = new Text2('POWERUP EARNED!', {
size: 150,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
});
earnedTextShadow.anchor.set(0.5, 0.5);
earnedTextShadow.y = game.height / 2 - 250 + 20 + 100 + 8;
earnedTextShadow.x = game.width / 2 + 8;
earnedTextShadow.alpha = 0;
// Create main earnedText (no stroke)
var earnedText = new Text2('POWERUP EARNED!', {
size: 150,
fill: 0xFFFFFF,
font: "Impact"
});
earnedText.anchor.set(0.5, 0.5);
powerupGraphics.width = 150;
powerupGraphics.height = 150;
earnedText.y = game.height / 2 - 250 + 20 + 100;
earnedText.x = game.width / 2;
earnedText.alpha = 0;
// Set initial position
self.x = startX;
self.y = startY;
// Animation phases
var phase = 0;
self.start = function () {
// Add text and dropshadow to overlay (so it doesn't move with self)
LK.getSound('powerupSwoosh').play();
if (!earnedTextShadow.parent) {
game.addChild(earnedTextShadow);
}
if (!earnedText.parent) {
game.addChild(earnedText);
}
// Phase 1: Move to center of screen
tween(powerupGraphics.scale, {
x: 2,
y: 2
}, {
duration: 300,
easing: tween.easeIn
});
tween(self, {
x: game.width / 2,
y: game.height / 2
}, {
duration: 300,
easing: tween.easeIn
});
// Show text and dropshadow
tween(earnedTextShadow, {
alpha: 0.35,
y: game.height / 2 - 250 + 8
}, {
duration: 300,
delay: 100,
easing: tween.easeOut
});
tween(earnedText, {
alpha: 1,
y: game.height / 2 - 250
}, {
duration: 300,
delay: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
// Wait a moment before moving to powerup counter
LK.setTimeout(function () {
LK.getSound('powerupSwoosh').play();
// Get target position (powerup counter)
var targetX = fireBallPowerupOverlay.x;
var targetY = fireBallPowerupOverlay.y;
// Phase 2: Move to powerup counter
tween(self, {
x: targetX,
y: targetY
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
LK.getSound('powerupThump').play();
// Increment powerup counter
fireBallPowerupOverlay.increaseFireballCount();
// Destroy animation
if (earnedTextShadow.parent) {
earnedTextShadow.parent.removeChild(earnedTextShadow);
}
if (earnedText.parent) {
earnedText.parent.removeChild(earnedText);
}
self.destroy();
}
});
// Fade out text and dropshadow as we move
tween(earnedTextShadow, {
alpha: 0,
y: game.height / 2 - 250 - 20 + 8
}, {
duration: 300,
easing: tween.easeOut
});
tween(earnedText, {
alpha: 0,
y: game.height / 2 - 250 - 20
}, {
duration: 300,
easing: tween.easeOut
});
// Scale down as we move to counter
tween(powerupGraphics, {
width: 210,
height: 210
}, {
duration: 300,
easing: tween.easeIn
});
tween(powerupGraphics.scale, {
x: 1,
y: 1
}, {
duration: 300,
easing: tween.easeIn
});
}, 1000);
}
});
};
return self;
});
// Make active when we have fireballs
var PowerupParticle = Container.expand(function (angle, speed) {
var self = Container.call(this);
var particle = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
particle.width = 60;
particle.height = 60;
self.scale.set(0.7 + Math.random() * 0.5, 0.7 + Math.random() * 0.5);
self.alpha = 1;
var vx = Math.cos(angle) * speed;
var vy = Math.sin(angle) * speed;
var gravity = 0.5 + Math.random() * 0.3;
var life = 24 + Math.floor(Math.random() * 10);
var tick = 0;
self.update = function () {
self.x += vx;
self.y += vy;
vy += gravity;
self.alpha -= 0.04 + Math.random() * 0.01;
tick++;
if (tick > life || self.alpha <= 0) {
self.destroy();
}
};
return self;
});
var ScoreIndicatorLabel = Container.expand(function (score, type) {
var self = Container.call(this);
var label = new Text2(score, {
size: 100,
fill: "#" + bubbleColors[type].toString(16).padStart(6, '0'),
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);
// Dropshadow for scoreMultiplierLabel
var scoreMultiplierLabelShadow = new Text2(baseValue, {
size: 100,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
});
scoreMultiplierLabelShadow.anchor.set(0.5, 0);
self.addChild(scoreMultiplierLabelShadow);
// 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 scoreValue = baseValue * currentMultiplier;
// Check if bubble color matches the pool color for bonus scoring
var poolIndex = Math.floor(self.x / (2048 / 6)); // Determine which pool this is (0-5)
var isColorMatch = false;
// Check if bubble type matches pool color (red=0, green=1, blue=2, purple=3, cyan=4, pink=5)
if (bubble.type >= 0 && bubble.type < 6 && bubble.type === poolIndex) {
isColorMatch = true;
scoreValue *= 3; // Triple score for matching color
// Play bonus sound
LK.getSound('comboBonus').play();
// Create special color match particle effect
var bonusParticle = particlesLayer.addChild(new BubbleRemoveParticle());
bonusParticle.x = bubble.x;
bonusParticle.y = self.y + 100;
bonusParticle.scale.set(2, 2);
bonusParticle.alpha = 0.8;
}
var scoreIndicator = game.addChild(new ScoreIndicatorLabel(scoreValue, bubble.type));
scoreIndicator.x = self.x;
scoreIndicator.y = self.y;
// Make score indicator bigger for color matches
if (isColorMatch) {
scoreIndicator.scale.set(1.5, 1.5);
}
// Award coins based on score
var coinsEarned = Math.floor(scoreValue / 50);
if (doubleCoinsActive) {
coinsEarned *= 2; // Double coins when power-up is active
}
if (coinsEarned > 0) {
storage.coins += coinsEarned;
}
var particle = particlesLayer.addChild(new BubbleRemoveParticle());
particle.x = bubble.x;
particle.y = self.y + 150;
// Track mission progress
if (missionActive && bubble.type >= 0 && bubble.type < 3) {
missionCollected[bubble.type]++;
// Check if mission is complete
if (checkMissionComplete()) {
missionActive = false;
// Mission completed - continue playing
LK.effects.flashScreen(0x00ff00, 1000); // Green flash for success
}
}
};
self.setMultiplier = function (multiplier) {
currentMultiplier = multiplier;
scoreMultiplierLabel.setText(baseValue * currentMultiplier);
scoreMultiplierLabelShadow.setText(baseValue * currentMultiplier);
};
});
var ShopOverlay = Container.expand(function () {
var self = Container.call(this);
// Full screen background
var background = self.attachAsset('uxoverlay', {
anchorX: 0,
anchorY: 0
});
background.width = 2048;
background.height = 2732;
background.alpha = 0.95;
// Shop title
var titleShadow = self.addChild(new Text2('SHOP', {
size: 150,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
titleShadow.anchor.set(0.5, 0.5);
titleShadow.x = game.width / 2 + 4;
titleShadow.y = 300 + 4;
var titleText = self.addChild(new Text2('SHOP', {
size: 150,
fill: 0xFFFFFF,
font: "Impact"
}));
titleText.anchor.set(0.5, 0.5);
titleText.x = game.width / 2;
titleText.y = 300;
// Coins display
var coinsText = self.addChild(new Text2('Coins: ' + storage.coins, {
size: 80,
fill: 0xFFD700,
font: "Impact"
}));
coinsText.anchor.set(0.5, 0.5);
coinsText.x = game.width / 2;
coinsText.y = 400;
// Shop items
var shopItems = [{
name: 'Extra Swaps',
description: 'Start with +1 swap',
cost: 100,
maxLevel: 3,
upgrade: 'extraSwaps'
}, {
name: 'Power Balls',
description: 'Start with +1 fireball',
cost: 200,
maxLevel: 3,
upgrade: 'powerBalls'
}, {
name: 'Time Bonus',
description: '+10s mission time',
cost: 150,
maxLevel: 5,
upgrade: 'timeBonus'
}];
var itemElements = [];
for (var i = 0; i < shopItems.length; i++) {
var item = shopItems[i];
var yPos = 550 + i * 220;
var currentLevel = storage.shopUpgrades[item.upgrade];
var isMaxed = currentLevel >= item.maxLevel;
var actualCost = item.cost + currentLevel * Math.floor(item.cost * 0.5);
var canAfford = storage.coins >= actualCost;
// Item background
var itemBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
itemBG.x = game.width / 2;
itemBG.y = yPos;
itemBG.width = 800;
itemBG.height = 180;
itemBG.alpha = 0.7;
// Item name
var itemName = self.addChild(new Text2(item.name, {
size: 60,
fill: 0xFFFFFF,
font: "Impact"
}));
itemName.anchor.set(0.5, 0.5);
itemName.x = game.width / 2;
itemName.y = yPos - 50;
// Item description
var itemDesc = self.addChild(new Text2(item.description, {
size: 40,
fill: 0xCCCCCC,
font: "Impact"
}));
itemDesc.anchor.set(0.5, 0.5);
itemDesc.x = game.width / 2;
itemDesc.y = yPos - 15;
// Level indicator
var levelText = self.addChild(new Text2('Level: ' + currentLevel + '/' + item.maxLevel, {
size: 35,
fill: 0xFFFF00,
font: "Impact"
}));
levelText.anchor.set(0.5, 0.5);
levelText.x = game.width / 2 - 200;
levelText.y = yPos + 25;
// Cost display underneath
var costDisplayText = self.addChild(new Text2(isMaxed ? 'MAXED OUT' : actualCost + ' coins', {
size: 50,
fill: isMaxed ? 0x888888 : 0xFFD700,
font: "Impact"
}));
costDisplayText.anchor.set(0.5, 0.5);
costDisplayText.x = game.width / 2;
costDisplayText.y = yPos + 55;
// Buy button - larger for mobile touch
var buyButtonBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
buyButtonBG.x = game.width / 2 + 200;
buyButtonBG.y = yPos + 25;
buyButtonBG.width = 200; // Larger touch target
buyButtonBG.height = 80; // Larger touch target
buyButtonBG.alpha = isMaxed ? 0.3 : canAfford ? 0.8 : 0.5;
var buttonText = isMaxed ? 'MAXED' : canAfford ? 'BUY' : 'LOCKED';
var buttonColor = isMaxed ? 0x666666 : canAfford ? 0x00FF00 : 0xFF0000;
var buyText = self.addChild(new Text2(buttonText, {
size: 40,
fill: buttonColor,
font: "Impact"
}));
buyText.anchor.set(0.5, 0.5);
buyText.x = game.width / 2 + 200;
buyText.y = yPos + 25;
itemElements.push({
bg: itemBG,
buyBG: buyButtonBG,
item: item,
buyText: buyText,
levelText: levelText,
costText: costDisplayText,
canAfford: canAfford,
isMaxed: isMaxed,
actualCost: actualCost
});
}
// Back button
var backButtonBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
backButtonBG.x = game.width / 2;
backButtonBG.y = game.height - 300;
backButtonBG.width = 300;
backButtonBG.height = 100;
var backButtonText = self.addChild(new Text2('BACK', {
size: 60,
fill: 0xFFFFFF,
font: "Impact"
}));
backButtonText.anchor.set(0.5, 0.5);
backButtonText.x = game.width / 2;
backButtonText.y = game.height - 300;
// Click handler
self.down = function (x, y, obj) {
// Check back button
var backLeft = backButtonBG.x - backButtonBG.width / 2;
var backRight = backButtonBG.x + backButtonBG.width / 2;
var backTop = backButtonBG.y - backButtonBG.height / 2;
var backBottom = backButtonBG.y + backButtonBG.height / 2;
if (x >= backLeft && x <= backRight && y >= backTop && y <= backBottom) {
self.closeShop();
return;
}
// Check shop items - specifically the buy button area
for (var i = 0; i < itemElements.length; i++) {
var element = itemElements[i];
var buyLeft = element.buyBG.x - element.buyBG.width / 2;
var buyRight = element.buyBG.x + element.buyBG.width / 2;
var buyTop = element.buyBG.y - element.buyBG.height / 2;
var buyBottom = element.buyBG.y + element.buyBG.height / 2;
if (x >= buyLeft && x <= buyRight && y >= buyTop && y <= buyBottom) {
self.buyItem(element.item, i);
break;
}
}
};
self.buyItem = function (item, index) {
var currentLevel = storage.shopUpgrades[item.upgrade];
if (currentLevel >= item.maxLevel) return;
var actualCost = item.cost + currentLevel * Math.floor(item.cost * 0.5);
if (storage.coins < actualCost) return;
storage.coins -= actualCost;
storage.shopUpgrades[item.upgrade]++;
LK.getSound('buttonPress').play();
LK.getSound('scoreCollected').play();
// Update display
self.updateDisplay();
};
self.updateDisplay = function () {
coinsText.setText('Coins: ' + storage.coins);
for (var i = 0; i < itemElements.length; i++) {
var element = itemElements[i];
var item = element.item;
var currentLevel = storage.shopUpgrades[item.upgrade];
var isMaxed = currentLevel >= item.maxLevel;
var actualCost = item.cost + currentLevel * Math.floor(item.cost * 0.5);
var canAfford = storage.coins >= actualCost;
element.levelText.setText('Level: ' + currentLevel + '/' + item.maxLevel);
element.costText.setText(isMaxed ? 'MAXED OUT' : actualCost + ' coins');
element.costText.fill = isMaxed ? 0x888888 : 0xFFD700;
var buttonText = isMaxed ? 'MAXED' : canAfford ? 'BUY' : 'LOCKED';
var buttonColor = isMaxed ? 0x666666 : canAfford ? 0x00FF00 : 0xFF0000;
element.buyText.setText(buttonText);
element.buyText.fill = buttonColor;
element.buyBG.alpha = isMaxed ? 0.3 : canAfford ? 0.8 : 0.5;
element.canAfford = canAfford;
element.isMaxed = isMaxed;
element.actualCost = actualCost;
}
};
self.closeShop = function () {
LK.getSound('buttonPress').play();
tween(self, {
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
var SlowTimeBubble = Container.expand(function () {
var self = Container.call(this);
// Use a blue-tinted bubble to represent slow time
var bubbleGraphics = self.attachAsset('bubble2', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it visually distinct with blue tint
bubbleGraphics.tint = 0x4444ff;
self.type = -5; // Special type for slow time bubble
self.isSlowTimeBubble = true;
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;
};
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;
};
self.update = function () {
if (self.isFreeBubble) {
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 += 0.8;
speedX *= 0.995;
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75;
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
var restitution = 0.6;
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// When slow time bubble reaches bottom, slow down grid movement
if (self.y > 2732 - 400) {
self.activateSlowTime();
self.destroy();
}
}
};
self.activateSlowTime = function () {
// Slow down grid movement by 70% for 10 seconds
var originalSpeed = gridSpeed;
gridSpeed *= 0.3; // Slow to 30% speed
LK.getSound('powerupSwoosh').play();
LK.getSound('timeBubbleCollect').play();
// Visual effect
LK.effects.flashScreen(0x4444ff, 500);
// Show effect text
var effectText = game.addChild(new Text2('SLOW TIME!', {
size: 120,
fill: 0x4444ff,
font: "Impact"
}));
effectText.anchor.set(0.5, 0.5);
effectText.x = game.width / 2;
effectText.y = game.height / 2;
effectText.alpha = 1;
tween(effectText, {
y: effectText.y - 200,
alpha: 0
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
effectText.destroy();
}
});
// Restore normal speed after 10 seconds
LK.setTimeout(function () {
gridSpeed = originalSpeed;
}, 10000);
};
return self;
});
var StartScreenOverlay = Container.expand(function () {
var self = Container.call(this);
// Full screen background
var background = self.attachAsset('uxoverlay', {
anchorX: 0,
anchorY: 0
});
background.width = 2048;
background.height = 2732;
background.alpha = 0.95;
// Game logo - visual brand element
var gameLogo = self.attachAsset('gamelogo', {
anchorX: 0.5,
anchorY: 0.5
});
gameLogo.x = game.width / 2;
gameLogo.y = game.height / 2 - 350;
gameLogo.width = 500;
gameLogo.height = 120;
// Store reference for animation
self.gameLogo = gameLogo;
// Add update method for logo animation
self.update = function () {
if (self.gameLogo) {
var logoScale = 1 + Math.sin(LK.ticks * 0.05) * 0.1;
self.gameLogo.scale.set(logoScale, logoScale);
var logoAlpha = 0.8 + Math.sin(LK.ticks * 0.03) * 0.2;
self.gameLogo.alpha = logoAlpha;
}
};
// Game title
var titleShadow = self.addChild(new Text2('BUBBLE CLASH', {
size: 200,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
titleShadow.anchor.set(0.5, 0.5);
titleShadow.x = game.width / 2 + 6;
titleShadow.y = game.height / 2 - 200 + 6;
var titleText = self.addChild(new Text2('BUBBLE CLASH', {
size: 200,
fill: 0xFFFFFF,
font: "Impact"
}));
titleText.anchor.set(0.5, 0.5);
titleText.x = game.width / 2;
titleText.y = game.height / 2 - 200;
// Play button background - larger for mobile
var playButtonBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
playButtonBG.x = game.width / 2;
playButtonBG.y = game.height / 2 + 50;
playButtonBG.width = 500; // Larger touch area
playButtonBG.height = 150; // Larger touch area
playButtonBG.alpha = 0.1; // Slightly visible for better mobile UX
// Play button text shadow
var playButtonShadow = self.addChild(new Text2('PLAY', {
size: 80,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
playButtonShadow.anchor.set(0.5, 0.5);
playButtonShadow.x = game.width / 2 + 4;
playButtonShadow.y = game.height / 2 + 50 + 4;
// Play button text
var playButtonText = self.addChild(new Text2('PLAY', {
size: 80,
fill: 0xFFFFFF,
font: "Impact"
}));
playButtonText.anchor.set(0.5, 0.5);
playButtonText.x = game.width / 2;
playButtonText.y = game.height / 2 + 50;
// Shop button background
var shopButtonBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
shopButtonBG.x = game.width / 2;
shopButtonBG.y = game.height / 2 + 200;
shopButtonBG.width = 300;
shopButtonBG.height = 100;
shopButtonBG.alpha = 0; // Make shop button transparent like play button
// Shop button text shadow
var shopButtonShadow = self.addChild(new Text2('SHOP', {
size: 60,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
shopButtonShadow.anchor.set(0.5, 0.5);
shopButtonShadow.x = game.width / 2 + 3;
shopButtonShadow.y = game.height / 2 + 200 + 3;
// Shop button text
var shopButtonText = self.addChild(new Text2('SHOP', {
size: 60,
fill: 0xFFD700,
font: "Impact"
}));
shopButtonText.anchor.set(0.5, 0.5);
shopButtonText.x = game.width / 2;
shopButtonText.y = game.height / 2 + 200;
// Instructions text
var instructionText = self.addChild(new Text2('Tap to aim and shoot bubbles!', {
size: 60,
fill: 0xFFFFFF,
font: "Impact"
}));
instructionText.anchor.set(0.5, 0.5);
instructionText.x = game.width / 2;
instructionText.y = game.height / 2 + 300;
instructionText.alpha = 0.8;
// Mission info text
var missionInfoText = self.addChild(new Text2('Complete color missions before time runs out!', {
size: 45,
fill: 0xFFFF00,
font: "Impact"
}));
missionInfoText.anchor.set(0.5, 0.5);
missionInfoText.x = game.width / 2;
missionInfoText.y = game.height / 2 + 370;
missionInfoText.alpha = 0.9;
// Coins display
var coinsDisplay = self.addChild(new Text2('Coins: ' + storage.coins, {
size: 60,
fill: 0xFFD700,
font: "Impact"
}));
coinsDisplay.anchor.set(0.5, 0.5);
coinsDisplay.x = game.width / 2;
coinsDisplay.y = game.height / 2 + 480;
coinsDisplay.alpha = 1.0;
// Play button click handler
self.down = function (x, y, obj) {
// Check if click is within play button bounds
var buttonLeft = playButtonBG.x - playButtonBG.width / 2;
var buttonRight = playButtonBG.x + playButtonBG.width / 2;
var buttonTop = playButtonBG.y - playButtonBG.height / 2;
var buttonBottom = playButtonBG.y + playButtonBG.height / 2;
if (x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom) {
// Start the game
self.startGame();
return;
}
// Check if click is within shop button bounds
var shopLeft = shopButtonBG.x - shopButtonBG.width / 2;
var shopRight = shopButtonBG.x + shopButtonBG.width / 2;
var shopTop = shopButtonBG.y - shopButtonBG.height / 2;
var shopBottom = shopButtonBG.y + shopButtonBG.height / 2;
if (x >= shopLeft && x <= shopRight && y >= shopTop && y <= shopBottom) {
// Open shop
self.openShop();
}
};
self.openShop = function () {
LK.getSound('buttonPress').play();
var shop = game.addChild(new ShopOverlay());
};
self.startGame = function () {
LK.getSound('buttonPress').play();
LK.getSound('powerupThump').play();
// Hide start screen with fade out animation
tween(self, {
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
gameStarted = true;
// Apply shop upgrades
applyShopUpgrades();
}
});
};
return self;
});
var SwapsCounterOverlay = Container.expand(function () {
var self = Container.call(this);
// Position in top right area with more margin for mobile
self.y = 160;
self.x = game.width - 280;
// Background circle - larger for mobile
var countBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
countBG.width = 180;
countBG.height = 180;
countBG.x = 0;
countBG.y = 0;
// Dropshadow for label
var labelShadow = self.addChild(new Text2(storage.swapsRemaining, {
size: 80,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
labelShadow.anchor.set(.5, .5);
labelShadow.x = 3;
labelShadow.y = 3;
// Main label
var label = self.addChild(new Text2(storage.swapsRemaining, {
size: 80,
fill: 0xFFFFFF,
font: "Impact"
}));
label.anchor.set(.5, .5);
label.x = 0;
label.y = 0;
// Swap text
var swapText = self.addChild(new Text2('DEĞIŞ', {
size: 32,
fill: 0xFFFFFF,
font: "Impact"
}));
swapText.anchor.set(.5, .5);
swapText.x = 0;
swapText.y = -60;
swapText.alpha = 0.8;
// Swap icon (using bubble asset as placeholder)
var swapIcon = self.attachAsset('bubble0', {
anchorX: 0.5,
anchorY: 0.5
});
swapIcon.x = -90;
swapIcon.y = 0;
swapIcon.width = 90;
swapIcon.height = 90;
swapIcon.alpha = 0.7;
self.updateCount = function () {
label.setText(storage.swapsRemaining);
labelShadow.setText(storage.swapsRemaining);
// Fade out when no swaps remaining
self.alpha = storage.swapsRemaining > 0 ? 1 : 0.5;
};
// Initialize display
self.updateCount();
return self;
});
var TimeBubble = Container.expand(function () {
var self = Container.call(this);
// Use a green-tinted bubble to represent time bonus
var bubbleGraphics = self.attachAsset('bubble1', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it visually distinct with green tint and glow effect
bubbleGraphics.tint = 0x00ff00;
self.type = -2; // Special type for time bubble
self.isTimeBubble = true;
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;
};
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;
};
// Pulsing animation for visual appeal
var pulseScale = 1.0;
var pulseDirection = 1;
self.update = function () {
if (self.isFreeBubble) {
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;
}
// Pulsing animation
pulseScale += pulseDirection * 0.01;
if (pulseScale > 1.1) {
pulseDirection = -1;
} else if (pulseScale < 0.9) {
pulseDirection = 1;
}
self.scale.set(pulseScale, pulseScale);
} else {
self.x += speedX;
self.y += speedY;
speedY += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// When time bubble reaches bottom, add time to mission
if (self.y > 2732 - 400) {
LK.getSound('timeBubbleCollect').play();
LK.getSound('scoreCollected').play();
LK.getSound('powerupSwoosh').play();
// Add 15 seconds to mission time
if (missionActive) {
missionTimeLeft += 15 * 60; // 15 seconds in ticks
// Show time bonus animation
var bonusText = game.addChild(new Text2('+15s', {
size: 120,
fill: 0x00ff00,
font: "Impact"
}));
bonusText.anchor.set(0.5, 0.5);
bonusText.x = self.x;
bonusText.y = self.y - 100;
bonusText.alpha = 1;
tween(bonusText, {
y: bonusText.y - 200,
alpha: 0
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
bonusText.destroy();
}
});
}
self.destroy();
}
}
};
return self;
});
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 + (storage.currentLevel - 1) * 0.1; // Increase speed by 0.1 per level
var gameStarted = false; // Flag to track if game has started
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
scoreLabelShadow.setText(newScore.toString()); // Update the shadow as well
}
//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 ballsRemaining = 30; // Start with 30 balls per level
var maxBallsPerLevel = 30; // Base number of balls per level
var bubbleSize = 150;
var gameIsStarted = false;
var bubbleColors = [0xff2853, 0x44d31f, 0x5252ff, 0xcb2bff, 0x28f2f0, 0xff69b4];
var barriers = [];
var maxSelectableBubble = getMaxTypes();
var warningLines = [];
// Mission system variables
var missionTargets = [0, 0, 0]; // Targets for red, green, blue bubbles
var missionCollected = [0, 0, 0]; // Collected count for each color
var missionTimeLeft = 3600; // 60 seconds * 60 ticks per second
var missionActive = false;
var missionOverlay;
var currentGameLevel = 1; // Current level (1-10)
var maxGameLevel = 10;
var doubleCoinsActive = false; // Track if double coins power-up is active
// Level-based mission configurations
var levelMissions = [{
level: 1,
targets: [8, 6, 4],
time: 45
},
// Level 1: 45 seconds
{
level: 2,
targets: [10, 8, 6],
time: 50
},
// Level 2: 50 seconds
{
level: 3,
targets: [12, 10, 8],
time: 55
},
// Level 3: 55 seconds
{
level: 4,
targets: [15, 12, 10],
time: 60
},
// Level 4: 60 seconds
{
level: 5,
targets: [18, 15, 12],
time: 65
},
// Level 5: 65 seconds
{
level: 6,
targets: [20, 18, 15],
time: 70
},
// Level 6: 70 seconds
{
level: 7,
targets: [25, 20, 18],
time: 75
},
// Level 7: 75 seconds
{
level: 8,
targets: [30, 25, 20],
time: 80
},
// Level 8: 80 seconds
{
level: 9,
targets: [35, 30, 25],
time: 85
},
// Level 9: 85 seconds
{
level: 10,
targets: [40, 35, 30],
time: 90
} // Level 10: 90 seconds
];
// Initialize mission based on current level
function initializeMission() {
var levelConfig = levelMissions[currentGameLevel - 1];
if (levelConfig) {
for (var i = 0; i < 3; i++) {
missionTargets[i] = levelConfig.targets[i];
missionCollected[i] = 0;
}
// Apply time bonus from shop upgrades
var bonusTime = storage.shopUpgrades.timeBonus * 10; // 10 seconds per upgrade level
missionTimeLeft = (levelConfig.time + bonusTime) * 60; // Convert to ticks
missionActive = true;
}
}
// Check if mission is completed
function checkMissionComplete() {
for (var i = 0; i < 3; i++) {
if (missionCollected[i] < missionTargets[i]) {
return false;
}
}
return true;
}
function applyShopUpgrades() {
// Apply extra swaps upgrade
storage.swapsRemaining += storage.shopUpgrades.extraSwaps;
if (swapsCounterOverlay) {
swapsCounterOverlay.updateCount();
}
// Apply power balls upgrade
if (storage.shopUpgrades.powerBalls > 0) {
fireBallPowerupOverlay.fireballsLeft += storage.shopUpgrades.powerBalls;
fireBallPowerupOverlay.updateDisplay();
}
// Initialize ball counter for current level
refreshBallCounter();
// Apply time bonus upgrade (will be used in initializeMission)
}
// Handle mission failure
function handleMissionFailure() {
missionActive = false;
LK.effects.flashScreen(0xff0000, 3000);
LK.getSound('timeRunOut').play();
LK.getSound('gameOverJingle').play();
LK.getSound('powerupSwoosh').play(); // Additional sound for mission failure
LK.showGameOver();
}
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 < 5; a++) {
for (var b = 0; b < 3; b++) {
var barrier = game.addChild(new Barrier());
barrier.y = 2732 - 450 + b * 70;
barrier.x = 2048 / 6 * a + 2048 / 6;
barriers.push(barrier);
}
var barrierBlock = game.attachAsset('barrierblock', {});
barrierBlock.x = 2048 / 6 * a + 2048 / 6;
barrierBlock.y = 2732 - 450;
barrierBlock.anchor.x = .5;
}
// Create a score label dropshadow (offset, semi-transparent)
var scoreLabelShadow = new Text2('0', {
size: 120,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
});
scoreLabelShadow.anchor.set(0.5, 0);
scoreLabelShadow.x = 4;
scoreLabelShadow.y = 6;
LK.gui.top.addChild(scoreLabelShadow);
// Create a score label (main)
var scoreLabel = new Text2('0', {
size: 120,
fill: 0xFFFFFF,
font: "Impact"
});
scoreLabel.anchor.set(0.5, 0);
scoreLabel.x = 0;
scoreLabel.y = 0;
LK.gui.top.addChild(scoreLabel);
var scoreMultipliers = [];
var baseScores = [10, 20, 50, 25, 15, 30];
var scoreIcons = ['bubble0', 'bubble1', 'bubble2', 'bubble3', 'bubble4', 'bubble5']; // Different visual icons for each multiplier
for (var a = 0; a < 6; a++) {
var sm = new ScoreMultipliers(baseScores[a]);
sm.x = 2048 / 6 * a + 2048 / 12;
sm.y = 2300;
// Add icon above the score
var scoreIcon = sm.attachAsset(scoreIcons[a], {
anchorX: 0.5,
anchorY: 0.5
});
scoreIcon.x = 0;
scoreIcon.y = -80;
scoreIcon.width = 80;
scoreIcon.height = 80;
scoreIcon.alpha = 0.8;
scoreMultipliers.push(sm);
game.addChild(sm);
}
var bonusUX = game.addChild(new BonusUX());
var fireBallPowerupOverlay = game.addChild(new FireBallPowerupOverlay());
var swapsCounterOverlay = game.addChild(new SwapsCounterOverlay());
var levelDisplayOverlay = game.addChild(new LevelDisplayOverlay());
var ballCounterOverlay = game.addChild(new BallCounterOverlay());
missionOverlay = LK.gui.topRight.addChild(new MissionOverlay());
// Initialize the mission
initializeMission();
// Add start screen overlay
var startScreenOverlay = game.addChild(new StartScreenOverlay());
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 - 180; // More space from bottom for mobile
// Scale up launcher for better mobile visibility
launcher.scale.set(1.2, 1.2);
var hintBubbleCache = [];
var hintBubbles = [];
var isValid = false;
var path = [];
var bubbleAlpha = 1;
var hintTargetX = game.width / 2;
var hintTargetY = 0;
// Mobile-optimized touch controls with larger touch zones
game.move = function (x, y, obj) {
if (!gameStarted) return; // Don't process moves before game starts
hintTargetX = x;
hintTargetY = y;
refreshHintLine();
};
// Separate down handler for better mobile touch response
game.down = function (x, y, obj) {
if (!gameStarted) return;
// Call move to update hint line immediately
game.move(x, y, obj);
};
function getMaxTypes() {
// Level-based difficulty: more colors as level increases
var level = storage.currentLevel;
// Ensure minimum of 3 colors, with progressive increase
var maxTypes = Math.max(3, Math.min(6, Math.floor(level / 3) + 3));
// Additional progression based on bubbles popped
var bubblesPopped = storage.totalBubblesPopped || 0;
var bonusTypes = Math.floor(bubblesPopped / 100);
maxTypes = Math.min(6, maxTypes + bonusTypes);
return maxTypes;
}
function calculateBallsPerLevel(level) {
// Base balls per level starting from 30, capped at 110
var baseBalls = 30 + (level - 1) * 1;
return Math.min(baseBalls, 110);
}
function refreshBallCounter() {
ballsRemaining = calculateBallsPerLevel(storage.currentLevel);
ballCounterOverlay.updateCount();
}
function refreshHintLine() {
if (!gameStarted) return; // Don't update hint line before game starts
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 = 120; // Increase spacing for mobile performance
function renderHintBubbels() {
if (isValid) {
hintOffset = hintOffset % distanceBetweenHintbubbles;
var distanceSinceLastDot = -hintOffset + 120;
var hintBubbleOffset = 0;
var lastPoint = path[0];
var bubble = launcher.getBubble();
var tint = bubble.isFireBall ? bubble.isBombBall ? 0x333333 : 0xff9c00 : 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 () {
hintOffset += 5;
if (isValid) {
bubbleAlpha = Math.min(bubbleAlpha + .05, 1);
} else {
bubbleAlpha = Math.max(bubbleAlpha - .05, 0);
}
refreshHintLine();
// Update swaps counter display
swapsCounterOverlay.updateCount();
var alphaList = grid.calculateWarningScoreList();
var hasHighWarning = false;
for (var a = 0; a < warningLines.length; a++) {
var value = alphaList[a] / 3;
if (value > 0.4) hasHighWarning = true;
warningLines[a].alpha += (Math.min(value, .6) - warningLines[a].alpha) / 100;
warningLines[a].scale.y += (value * 60 - warningLines[a].scale.y) / 100;
}
// Play warning sound when bubbles get dangerously close
if (hasHighWarning && LK.ticks % 180 === 0) {
LK.getSound('warningAlert').play();
}
// Update mission system - only if game has started
if (missionActive && gameStarted) {
missionTimeLeft--;
// Check for mission failure (time ran out)
if (missionTimeLeft <= 0) {
handleMissionFailure();
}
// Update mission display
missionOverlay.updateMission(missionTargets, missionCollected, missionTimeLeft);
} else if (missionActive) {
// Update mission display without decrementing time
missionOverlay.updateMission(missionTargets, missionCollected, missionTimeLeft);
}
};
game.up = function () {
if (!gameStarted) return; // Don't allow firing before game starts
if (ballsRemaining <= 0) return; // Don't allow firing if no balls left
if (isValid) {
launcher.fire();
// Start mission on first shot if not already active
if (!missionActive) {
initializeMission();
}
}
};
;
;
;
;
;
;
;
;
;
; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
swapsRemaining: 3,
currentLevel: 1,
totalBubblesPopped: 0,
coins: 0,
shopUpgrades: {
extraSwaps: 0,
powerBalls: 0,
timeBonus: 0
}
});
/****
* Classes
****/
// Logo animation moved to StartScreenOverlay class
var BallCounterOverlay = Container.expand(function () {
var self = Container.call(this);
// Position in bottom left area with margin for mobile
self.y = game.height - 280;
self.x = 200;
// Background circle - larger for mobile
var countBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
countBG.width = 180;
countBG.height = 180;
countBG.x = 0;
countBG.y = 0;
// Dropshadow for label
var labelShadow = self.addChild(new Text2(ballsRemaining, {
size: 80,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
labelShadow.anchor.set(.5, .5);
labelShadow.x = 3;
labelShadow.y = 3;
// Main label
var label = self.addChild(new Text2(ballsRemaining, {
size: 80,
fill: 0xFFFFFF,
font: "Impact"
}));
label.anchor.set(.5, .5);
label.x = 0;
label.y = 0;
// Ball icon using bubble asset
var ballIcon = self.attachAsset('bubble0', {
anchorX: 0.5,
anchorY: 0.5
});
ballIcon.x = -90;
ballIcon.y = 0;
ballIcon.width = 90;
ballIcon.height = 90;
ballIcon.alpha = 0.7;
self.updateCount = function () {
label.setText(ballsRemaining);
labelShadow.setText(ballsRemaining);
// Change color based on remaining balls
if (ballsRemaining <= 5) {
label.fill = 0xff0000; // Red when low
countBG.tint = 0xff4444;
} else if (ballsRemaining <= 10) {
label.fill = 0xffff00; // Yellow when medium
countBG.tint = 0xffff44;
} else {
label.fill = 0xFFFFFF; // White when plenty
countBG.tint = 0xffffff;
}
// Fade out when no balls remaining
self.alpha = ballsRemaining > 0 ? 1 : 0.5;
};
// Initialize display
self.updateCount();
return self;
});
var Barrier = Container.expand(function () {
var self = Container.call(this);
var barrierGraphics = self.attachAsset('barrier', {
anchorX: .5,
anchorY: .5
});
});
var BombBall = Container.expand(function (max_types, type) {
var self = Container.call(this);
self.isFireBall = true;
self.isBombBall = true;
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);
}
}
var bubbleGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
bubbleGraphics.width = 150;
bubbleGraphics.height = 150;
bubbleGraphics.tint = 0x333333; // Dark gray/black tint for bomb
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 (++spawnMod % 3 == 0 && self.parent) {
// Spawn bomb particles every 3 ticks
var angle = Math.random() * Math.PI * 2;
var bombParticle = self.parent.addChild(new BombParticle(angle));
bombParticle.x = self.x + Math.cos(angle) * self.width / 4;
bombParticle.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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
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 / 6))].applyBubble(self);
LK.getSound('scoreCollected').play();
}
}
};
return self;
});
var BombParticle = Container.expand(function (angle) {
var self = Container.call(this);
var particleGraphics = self.attachAsset('fireparticle', {
anchorX: 0.5,
anchorY: 0.5
});
particleGraphics.blendMode = 1;
particleGraphics.tint = 0x333333; // Dark gray/black tint for bomb particles
var speedX = Math.cos(angle) * 1.2;
var speedY = Math.sin(angle) * 1.2;
var rotationSpeed = Math.random() * 0.08 - 0.04;
self.update = function () {
self.x += speedX * self.alpha;
self.y += speedY * self.alpha;
particleGraphics.rotation += rotationSpeed;
self.alpha -= 0.012;
if (self.alpha <= 0) {
self.destroy();
}
};
return self;
});
var BonusUX = Container.expand(function () {
var self = Container.call(this);
//Insert label here
var barHeight = 50;
// Dropshadow for bonusLabel
var bonusLabelShadow = self.addChild(new Text2('Streak Bonus', {
size: 90,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
bonusLabelShadow.anchor.set(1, 1);
bonusLabelShadow.x = -10 + 4;
bonusLabelShadow.y = barHeight / 2 + 4;
// Main bonusLabel
var bonusLabel = self.addChild(new Text2('Streak Bonus', {
size: 90,
fill: 0xF4F5FF,
font: "Impact"
}));
bonusLabel.anchor.set(1, 1);
bonusLabel.y = barHeight / 2;
var rightMargin = -10;
bonusLabel.x = rightMargin;
// Dropshadow for bonusAmountLabel
var bonusAmountLabelShadow = self.addChild(new Text2('1x', {
size: 170,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
bonusAmountLabelShadow.anchor.set(.5, .5);
bonusAmountLabelShadow.x = 100 + 4;
bonusAmountLabelShadow.y = 4;
// Main bonusAmountLabel
var bonusAmountLabel = self.addChild(new Text2('1x', {
size: 170,
fill: 0xF4F5FF,
font: "Impact"
}));
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 = 24;
self.setStreakCount = function (level) {
self.streakCount = Math.min(level, maxLevel);
var newBonus = Math.floor(self.streakCount / 8) + 1;
if (newBonus != self.bonusAmount) {
LK.getSound('comboBonus').play();
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 {
var bubbleGraphics = self.attachAsset('bubble' + self.type, {
anchorX: 0.5,
anchorY: 0.5
});
}
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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
// 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 / 6))].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 ColorChangerBubble = Container.expand(function () {
var self = Container.call(this);
// Use a rainbow-tinted bubble to represent color changer
var bubbleGraphics = self.attachAsset('bubble0', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it visually distinct with rainbow effect
bubbleGraphics.tint = 0xffffff;
self.type = -4; // Special type for color changer bubble
self.isColorChangerBubble = true;
self.isAttached = true;
self.isFreeBubble = false;
var speedX = 0;
var speedY = 0;
self.targetX = 0;
self.targetY = 0;
var colorCycleTime = 0;
self.setPos = function (x, y) {
self.x = self.targetX = x;
self.y = self.targetY = y;
};
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;
};
self.update = function () {
if (self.isFreeBubble) {
return;
}
if (self.isAttached) {
// Rainbow color cycling effect
colorCycleTime += 0.1;
var red = Math.sin(colorCycleTime) * 127 + 128;
var green = Math.sin(colorCycleTime + 2) * 127 + 128;
var blue = Math.sin(colorCycleTime + 4) * 127 + 128;
bubbleGraphics.tint = Math.floor(red) << 16 | Math.floor(green) << 8 | Math.floor(blue);
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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
var restitution = 0.6;
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// When color changer reaches bottom, change all bubbles to same color
if (self.y > 2732 - 400) {
self.activateColorChanger();
self.destroy();
}
}
};
self.activateColorChanger = function () {
// Pick a random color for all bubbles to change to
var newColor = Math.floor(Math.random() * getMaxTypes());
var bubblesChanged = 0;
// Change all grid bubbles to the new color
for (var row = 0; row < grid.container.children.length; row++) {
var bubble = grid.container.children[row];
if (bubble && bubble.isAttached && bubble.type >= 0 && bubble.type < 6) {
bubble.type = newColor;
bubblesChanged++;
}
}
if (bubblesChanged > 0) {
LK.getSound('powerupSwoosh').play();
LK.getSound('bubbleSwap').play();
// Visual effect
LK.effects.flashScreen(0xffffff, 500);
// Show effect text
var effectText = game.addChild(new Text2('COLOR CHANGE!', {
size: 120,
fill: 0xffffff,
font: "Impact"
}));
effectText.anchor.set(0.5, 0.5);
effectText.x = game.width / 2;
effectText.y = game.height / 2;
effectText.alpha = 1;
tween(effectText, {
y: effectText.y - 200,
alpha: 0
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
effectText.destroy();
}
});
}
};
return self;
});
var DoubleCoinsBubble = Container.expand(function () {
var self = Container.call(this);
// Use a gold-tinted bubble to represent double coins
var bubbleGraphics = self.attachAsset('bubble5', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it visually distinct with gold tint
bubbleGraphics.tint = 0xffd700;
self.type = -6; // Special type for double coins bubble
self.isDoubleCoinsBubble = true;
self.isAttached = true;
self.isFreeBubble = false;
var speedX = 0;
var speedY = 0;
self.targetX = 0;
self.targetY = 0;
var glowPulse = 0;
self.setPos = function (x, y) {
self.x = self.targetX = x;
self.y = self.targetY = y;
};
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;
};
self.update = function () {
if (self.isFreeBubble) {
return;
}
if (self.isAttached) {
// Gold pulsing effect
glowPulse += 0.15;
var glowIntensity = Math.sin(glowPulse) * 0.3 + 0.7;
bubbleGraphics.alpha = glowIntensity;
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 += 0.8;
speedX *= 0.995;
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75;
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
var restitution = 0.6;
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// When double coins bubble reaches bottom, activate double coins
if (self.y > 2732 - 400) {
self.activateDoubleCoins();
self.destroy();
}
}
};
self.activateDoubleCoins = function () {
// Enable double coins for 15 seconds
doubleCoinsActive = true;
LK.getSound('powerupSwoosh').play();
LK.getSound('scoreCollected').play();
// Visual effect
LK.effects.flashScreen(0xffd700, 500);
// Show effect text
var effectText = game.addChild(new Text2('DOUBLE COINS!', {
size: 120,
fill: 0xffd700,
font: "Impact"
}));
effectText.anchor.set(0.5, 0.5);
effectText.x = game.width / 2;
effectText.y = game.height / 2;
effectText.alpha = 1;
tween(effectText, {
y: effectText.y - 200,
alpha: 0
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
effectText.destroy();
}
});
// Disable double coins after 15 seconds
LK.setTimeout(function () {
doubleCoinsActive = false;
}, 15000);
};
return self;
});
var ExplosiveBubble = Container.expand(function () {
var self = Container.call(this);
// Use a red-tinted bubble to represent explosive
var bubbleGraphics = self.attachAsset('bubble0', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it visually distinct with red tint
bubbleGraphics.tint = 0xff4444;
self.type = -3; // Special type for explosive bubble
self.isExplosiveBubble = true;
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;
};
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;
};
self.explode = function () {
// Create explosion effect
var explosion = game.addChild(new BubbleRemoveParticle());
explosion.x = self.x;
explosion.y = self.y;
explosion.scale.set(3, 3);
// Find all bubbles within explosion radius
var explosionRadius = 200;
var bubblesInRadius = [];
// Check all grid bubbles
for (var row = 0; row < grid.container.children.length; row++) {
var bubble = grid.container.children[row];
if (bubble && bubble.isAttached) {
var dx = bubble.x - (self.x - grid.x);
var dy = bubble.y - (self.y - grid.y);
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= explosionRadius) {
bubblesInRadius.push(bubble);
}
}
}
// Remove bubbles in explosion radius
if (bubblesInRadius.length > 0) {
grid.removeBubbles(bubblesInRadius);
var disconnected = grid.getDetachedBubbles();
grid.removeBubbles(disconnected);
// Track mission progress for exploded bubbles
if (missionActive) {
for (var i = 0; i < bubblesInRadius.length; i++) {
var bubble = bubblesInRadius[i];
if (bubble.type >= 0 && bubble.type < 3) {
missionCollected[bubble.type]++;
}
}
for (var i = 0; i < disconnected.length; i++) {
var bubble = disconnected[i];
if (bubble.type >= 0 && bubble.type < 3) {
missionCollected[bubble.type]++;
}
}
}
}
LK.getSound('explosiveBubble').play();
LK.getSound('fireBubble').play();
LK.getSound('powerupThump').play();
};
self.update = function () {
if (self.isFreeBubble) {
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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// When explosive bubble reaches bottom, explode
if (self.y > 2732 - 400) {
self.explode();
self.destroy();
}
}
};
return self;
});
var FireBallPowerupOverlay = Container.expand(function () {
var self = Container.call(this);
var bubbleGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
// Larger size for mobile touch targets
bubbleGraphics.width = 180;
bubbleGraphics.height = 180;
self.y = game.height - 180;
self.x = 250;
var countBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
// Larger background for better touch target
countBG.width = 160;
countBG.height = 160;
countBG.x = 110;
countBG.y = 60;
self.fireballsLeft = 3; // Start with 3 fireballs
// Dropshadow for label
var labelShadow = self.addChild(new Text2(self.fireballsLeft, {
size: 70,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
labelShadow.anchor.set(.5, .5);
labelShadow.x = 90 + 3;
labelShadow.y = 50 + 3;
// Main label
var label = self.addChild(new Text2(self.fireballsLeft, {
size: 70,
fill: 0xFFFFFF,
font: "Impact"
}));
label.anchor.set(.5, .5);
label.x = 90;
label.y = 50;
self.alpha = 0.5; // Start with greyed out overlay
self.increaseFireballCount = function () {
self.fireballsLeft++;
self.updateDisplay();
tween(self.scale, {
x: 1.3,
y: 1.3
}, {
duration: 120,
easing: tween.cubicOut,
onFinish: function onFinish() {
tween(self.scale, {
x: 1,
y: 1
}, {
duration: 220,
easing: tween.bounceOut
});
}
});
// Spawn powerup particles
for (var i = 0; i < 12; i++) {
var angle = Math.random() * Math.PI * 2;
var speed = 8 + Math.random() * 6;
var particle = game.addChild(new PowerupParticle(angle, speed));
particle.x = self.x + 90; // center of indicator
particle.y = self.y + 50;
}
};
self.updateDisplay = function () {
label.setText(self.fireballsLeft);
labelShadow.setText(self.fireballsLeft);
self.alpha = self.fireballsLeft > 0 ? 1 : 0.5;
};
self.down = function () {
if (self.fireballsLeft > 0 && !launcher.isFireBall()) {
self.fireballsLeft--;
label.setText(self.fireballsLeft);
labelShadow.setText(self.fireballsLeft);
// Cycle through fireball types: normal -> ice -> laser -> bomb -> normal
var fireballType = (4 - self.fireballsLeft) % 4;
launcher.triggerFireBall(fireballType);
if (self.fireballsLeft == 0) {
self.alpha = .5;
}
}
};
// State for wiggle animation
self.isWiggling = false;
// Update method to handle wiggle animation
self.update = function () {
// Check if bubbles are getting close to bottom and we have powerups
if (self.fireballsLeft > 0 && !self.isWiggling) {
// Get warning scores from grid
var warningScores = grid.calculateWarningScoreList();
var maxWarning = 0;
for (var i = 0; i < warningScores.length; i++) {
if (warningScores[i] > maxWarning) {
maxWarning = warningScores[i];
}
}
// If any column has high warning score (bubbles close to bottom), trigger wiggle
if (maxWarning > 1.5) {
// Threshold for "getting close"
self.isWiggling = true;
// First wiggle to the right
tween(self, {
rotation: 0.15
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
// Then wiggle to the left
tween(self, {
rotation: -0.15
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Then wiggle to the right again
tween(self, {
rotation: 0.15
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Return to normal position
tween(self, {
rotation: 0
}, {
duration: 100,
easing: tween.easeIn,
onFinish: function onFinish() {
// Reset wiggle state after a cooldown
LK.setTimeout(function () {
self.isWiggling = false;
}, 2000); // 2 second cooldown before next wiggle
}
});
}
});
}
});
}
});
}
}
};
});
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;
// Determine if this row should have a powerup
var shouldSpawnPowerup = false;
var shouldSpawnTimeBubble = false;
var shouldSpawnExplosiveBubble = false;
var powerupCol = -1;
var POWERUP_ROW_INTERVAL = 20; // Every 20th row has a powerup (was 10)
var TIME_BUBBLE_INTERVAL = 15; // Every 15th row might have a time bubble
var EXPLOSIVE_BUBBLE_INTERVAL = 25; // Every 25th row might have an explosive bubble
// Move first powerup spawn to row 6, and then every POWERUP_ROW_INTERVAL after that
if (rowCount === 6) {
shouldSpawnPowerup = true;
powerupCol = Math.floor(Math.random() * rowWidth);
} else if (rowCount - 6 > 0 && (rowCount - 6) % POWERUP_ROW_INTERVAL === 0) {
shouldSpawnPowerup = true;
powerupCol = Math.floor(Math.random() * rowWidth);
}
// Check for time bubble spawn (level 3+)
if (currentGameLevel >= 3 && rowCount > 10 && rowCount % TIME_BUBBLE_INTERVAL === 0 && Math.random() < 0.3) {
shouldSpawnTimeBubble = true;
powerupCol = Math.floor(Math.random() * rowWidth);
}
// Check for explosive bubble spawn (level 5+)
if (currentGameLevel >= 5 && rowCount > 15 && rowCount % EXPLOSIVE_BUBBLE_INTERVAL === 0 && Math.random() < 0.2) {
shouldSpawnExplosiveBubble = true;
powerupCol = Math.floor(Math.random() * rowWidth);
}
// Check for other power-up spawns
var shouldSpawnColorChanger = false;
var shouldSpawnSlowTime = false;
var shouldSpawnDoubleCoins = false;
// Color Changer (level 4+, every 30 rows, 15% chance)
if (currentGameLevel >= 4 && rowCount > 12 && rowCount % 30 === 0 && Math.random() < 0.15) {
shouldSpawnColorChanger = true;
powerupCol = Math.floor(Math.random() * rowWidth);
}
// Slow Time (level 6+, every 35 rows, 12% chance)
if (currentGameLevel >= 6 && rowCount > 18 && rowCount % 35 === 0 && Math.random() < 0.12) {
shouldSpawnSlowTime = true;
powerupCol = Math.floor(Math.random() * rowWidth);
}
// Double Coins (level 3+, every 25 rows, 20% chance)
if (currentGameLevel >= 3 && rowCount > 8 && rowCount % 25 === 0 && Math.random() < 0.20) {
shouldSpawnDoubleCoins = true;
powerupCol = Math.floor(Math.random() * rowWidth);
}
for (var a = 0; a < rowWidth; a++) {
var bubble;
if (shouldSpawnPowerup && a === powerupCol) {
bubble = new PowerupBubble();
} else if (shouldSpawnTimeBubble && a === powerupCol) {
bubble = new TimeBubble();
} else if (shouldSpawnExplosiveBubble && a === powerupCol) {
bubble = new ExplosiveBubble();
} else if (shouldSpawnColorChanger && a === powerupCol) {
bubble = new ColorChangerBubble();
} else if (shouldSpawnSlowTime && a === powerupCol) {
bubble = new SlowTimeBubble();
} else if (shouldSpawnDoubleCoins && a === powerupCol) {
bubble = new DoubleCoinsBubble();
} 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) {
if (neighbor.isPowerup || neighbor.isTimeBubble || neighbor.isExplosiveBubble || neighbor.isColorChangerBubble || neighbor.isSlowTimeBubble || neighbor.isDoubleCoinsBubble) {
// Special bubbles connect with everything
queue.push(neighbor);
} else if (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 >= 0 && 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, 130 / 2), 2048 - 130 / 2);
LK.getSound('circleBounce').play();
}
var intersectedBubble = self.bubbleIntersectsGrid(nextX + self.x, nextY + self.y);
if (intersectedBubble) {
gameIsStarted = true;
if (bubble.isFireBall) {
if (bubble.isIceBall) {
// Ice ball freezes bubbles in a 3x3 area for 3 seconds
var freezeRadius = 300;
var frozenBubbles = [];
for (var row = 0; row < rows.length; row++) {
for (var col = 0; col < rows[row].length; col++) {
var gridBubble = rows[row][col];
if (gridBubble && gridBubble.isAttached) {
var dx = gridBubble.x - (bubble.x - self.x);
var dy = gridBubble.y - (bubble.y - self.y);
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= freezeRadius) {
frozenBubbles.push(gridBubble);
}
}
}
}
// Freeze effect - slow down grid movement temporarily
var originalSpeed = gridSpeed;
gridSpeed *= 0.1; // Slow down to 10% speed
LK.setTimeout(function () {
gridSpeed = originalSpeed;
}, 3000);
// Visual freeze effect
for (var i = 0; i < frozenBubbles.length; i++) {
var frozenBubble = frozenBubbles[i];
frozenBubble.alpha = 0.5;
frozenBubble.tint = 0x88ccff;
LK.setTimeout(function () {
frozenBubble.alpha = 1;
frozenBubble.tint = 0xffffff;
}, 3000);
}
self.removeBubbles([intersectedBubble]);
} else if (bubble.isLaserBall) {
// Laser ball creates a vertical line destruction
var laserColumn = Math.floor((bubble.x - self.x) / bubbleSize);
var destroyedBubbles = [];
for (var row = 0; row < rows.length; row++) {
for (var col = 0; col < rows[row].length; col++) {
var gridBubble = rows[row][col];
if (gridBubble && Math.abs(col - laserColumn) <= 1) {
destroyedBubbles.push(gridBubble);
}
}
}
self.removeBubbles(destroyedBubbles);
} else if (bubble.isBombBall) {
// Bomb ball explodes in a large radius destroying many bubbles
var bombRadius = 250;
var explodedBubbles = [];
for (var row = 0; row < rows.length; row++) {
for (var col = 0; col < rows[row].length; col++) {
var gridBubble = rows[row][col];
if (gridBubble && gridBubble.isAttached) {
var dx = gridBubble.x - (bubble.x - self.x);
var dy = gridBubble.y - (bubble.y - self.y);
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= bombRadius) {
explodedBubbles.push(gridBubble);
}
}
}
}
// Create explosion effect
var explosion = game.addChild(new BubbleRemoveParticle());
explosion.x = bubble.x;
explosion.y = bubble.y;
explosion.scale.set(4, 4);
explosion.alpha = 0.9;
LK.getSound('explosiveBubble').play();
LK.getSound('fireBubble').play();
self.removeBubbles(explodedBubbles);
} else {
// Regular fireball
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);
LK.getSound('bubblePop').play(); // Sound for popping bubbles
LK.getSound('powerupThump').play(); // Additional sound for popping bubbles
var disconnected = self.getDetachedBubbles();
self.removeBubbles(disconnected);
// Handle special bubble effects before removal
for (var specialBubbleIndex = 0; specialBubbleIndex < bubbles.length; specialBubbleIndex++) {
var specialBubble = bubbles[specialBubbleIndex];
if (specialBubble.isTimeBubble) {
// Add time when time bubble is removed
if (missionActive) {
missionTimeLeft += 10 * 60; // 10 seconds in ticks
}
} else if (specialBubble.isExplosiveBubble) {
// Trigger explosion effect
specialBubble.explode();
} else if (specialBubble.isColorChangerBubble) {
// Activate color changer effect
specialBubble.activateColorChanger();
} else if (specialBubble.isSlowTimeBubble) {
// Activate slow time effect
specialBubble.activateSlowTime();
} else if (specialBubble.isDoubleCoinsBubble) {
// Activate double coins effect
specialBubble.activateDoubleCoins();
}
}
// Track mission progress for removed bubbles
if (missionActive) {
for (var missionBubbleIndex = 0; missionBubbleIndex < bubbles.length; missionBubbleIndex++) {
var missionBubble = bubbles[missionBubbleIndex];
if (missionBubble.type >= 0 && missionBubble.type < 3) {
missionCollected[missionBubble.type]++;
}
}
for (var disconnectedBubbleIndex = 0; disconnectedBubbleIndex < disconnected.length; disconnectedBubbleIndex++) {
var disconnectedBubble = disconnected[disconnectedBubbleIndex];
if (disconnectedBubble.type >= 0 && disconnectedBubble.type < 3) {
missionCollected[disconnectedBubble.type]++;
}
}
// Check if mission is complete
if (checkMissionComplete()) {
missionActive = false;
LK.effects.flashScreen(0x00ff00, 1000); // Green flash for success
LK.getSound('missionComplete').play();
LK.getSound('powerupThump').play();
LK.getSound('scoreCollected').play(); // Additional sound for mission completion
// Calculate coins earned for completing level
var coinsEarned = 50 + currentGameLevel * 25;
if (doubleCoinsActive) {
coinsEarned *= 2; // Double coins when power-up is active
}
storage.coins += coinsEarned;
// Show coins earned display
var coinsEarnedText = game.addChild(new Text2('+' + coinsEarned + ' Coins!', {
size: 120,
fill: 0xFFD700,
font: "Impact"
}));
coinsEarnedText.anchor.set(0.5, 0.5);
coinsEarnedText.x = game.width / 2;
coinsEarnedText.y = game.height / 2 + 100;
coinsEarnedText.alpha = 0;
// Animate coins earned text
tween(coinsEarnedText, {
alpha: 1,
y: game.height / 2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
// Hold display for 1.5 seconds then fade out
LK.setTimeout(function () {
tween(coinsEarnedText, {
alpha: 0,
y: game.height / 2 - 100
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
coinsEarnedText.destroy();
}
});
}, 1500);
}
});
// Advance to next level
currentGameLevel++;
// Refresh ball counter for new level
refreshBallCounter();
if (currentGameLevel > maxGameLevel) {
// Player completed all levels - show victory
LK.showYouWin();
} else {
// Start next level mission after a delay
LK.setTimeout(function () {
initializeMission();
}, 2000);
}
// Advance to next level
currentGameLevel++;
// Refresh ball counter for new level
refreshBallCounter();
LK.getSound('powerupSwoosh').play();
if (currentGameLevel > maxGameLevel) {
// Player completed all levels - show victory
LK.showYouWin();
} else {
// Start next level mission after a delay
LK.setTimeout(function () {
initializeMission();
}, 2000);
}
}
}
bonusUX.setStreakCount(bonusUX.streakCount + 1);
// Track bubbles popped and restore swaps
storage.totalBubblesPopped += bubbles.length + disconnected.length;
// Restore 1 swap for successful pop, up to maximum of 3
if (storage.swapsRemaining < 3) {
storage.swapsRemaining++;
}
// Level progression: every 50 bubbles popped = next level
var newLevel = Math.floor(storage.totalBubblesPopped / 50) + 1;
if (newLevel > storage.currentLevel) {
storage.currentLevel = newLevel;
levelDisplayOverlay.updateLevel();
// Refresh ball counter for new level
refreshBallCounter();
LK.getSound('levelUp').play();
LK.getSound('powerupSwoosh').play();
// Update grid speed for new level
gridSpeed = .5 + (storage.currentLevel - 1) * 0.1;
// Update max selectable bubble types
maxSelectableBubble = getMaxTypes();
}
} 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 * 0.5;
}
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);
LK.getSound('gameOverJingle').play();
LK.showGameOver();
}
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 IceBall = Container.expand(function (max_types, type) {
var self = Container.call(this);
self.isFireBall = true;
self.isIceBall = true;
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);
}
}
var bubbleGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
bubbleGraphics.width = 150;
bubbleGraphics.height = 150;
bubbleGraphics.tint = 0x88ccff; // Ice blue tint
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 (++spawnMod % 3 == 0 && self.parent) {
// Spawn ice particles every 3 ticks
var angle = Math.random() * Math.PI * 2;
var iceParticle = self.parent.addChild(new IceParticle(angle));
iceParticle.x = self.x + Math.cos(angle) * self.width / 4;
iceParticle.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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
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 / 6))].applyBubble(self);
LK.getSound('scoreCollected').play();
}
}
};
return self;
});
var IceParticle = Container.expand(function (angle) {
var self = Container.call(this);
var particleGraphics = self.attachAsset('fireparticle', {
anchorX: 0.5,
anchorY: 0.5
});
particleGraphics.blendMode = 1;
particleGraphics.tint = 0x88ccff; // Ice blue tint
var speedX = Math.cos(angle) * 1.5;
var speedY = Math.sin(angle) * 1.5;
var rotationSpeed = Math.random() * 0.05 - 0.025;
self.update = function () {
self.x += speedX * self.alpha;
self.y += speedY * self.alpha;
particleGraphics.rotation += rotationSpeed;
self.alpha -= 0.008;
if (self.alpha <= 0) {
self.destroy();
}
};
return self;
});
var LaserBall = Container.expand(function (max_types, type) {
var self = Container.call(this);
self.isFireBall = true;
self.isLaserBall = true;
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);
}
}
var bubbleGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
bubbleGraphics.width = 150;
bubbleGraphics.height = 150;
bubbleGraphics.tint = 0xff00ff; // Magenta tint
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 (++spawnMod % 2 == 0 && self.parent) {
// Spawn laser particles every 2 ticks
var angle = Math.random() * Math.PI * 2;
var laserParticle = self.parent.addChild(new LaserParticle(angle));
laserParticle.x = self.x + Math.cos(angle) * self.width / 4;
laserParticle.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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
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 / 6))].applyBubble(self);
LK.getSound('scoreCollected').play();
}
}
};
return self;
});
var LaserParticle = Container.expand(function (angle) {
var self = Container.call(this);
var particleGraphics = self.attachAsset('fireparticle', {
anchorX: 0.5,
anchorY: 0.5
});
particleGraphics.blendMode = 1;
particleGraphics.tint = 0xff00ff; // Magenta tint
var speedX = Math.cos(angle) * 2;
var speedY = Math.sin(angle) * 2;
var rotationSpeed = Math.random() * 0.15 - 0.075;
self.update = function () {
self.x += speedX * self.alpha;
self.y += speedY * self.alpha;
particleGraphics.rotation += rotationSpeed;
self.alpha -= 0.015;
if (self.alpha <= 0) {
self.destroy();
}
};
return self;
});
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);
} while (nextType == lastTypes[lastTypes.length - 1]); // Prevent consecutive same types
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;
}
createPreviewBubble();
self.fire = function () {
// Check if balls remaining
if (ballsRemaining <= 0) {
return; // Can't fire if no balls left
}
bulletsFired++;
ballsRemaining--;
ballCounterOverlay.updateCount();
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();
// Check for game over if no balls left
if (ballsRemaining <= 0) {
LK.setTimeout(function () {
// Check if mission is still active and not completed
if (missionActive && !checkMissionComplete()) {
LK.effects.flashScreen(0xff0000, 3000);
LK.getSound('timeRunOut').play();
LK.getSound('gameOverJingle').play();
LK.showGameOver();
}
}, 1000); // Small delay to allow last ball to take effect
}
};
self.angle = -Math.PI / 2;
self.getBubble = function () {
return bubble;
};
self.isFireBall = function () {
return bubble.isFireBall;
};
self.triggerFireBall = function (type) {
bubble.destroy();
type = type || 0;
if (type === 1) {
bubble = self.addChild(new IceBall(getMaxTypes()));
} else if (type === 2) {
bubble = self.addChild(new LaserBall(getMaxTypes()));
} else if (type === 3) {
bubble = self.addChild(new BombBall(getMaxTypes()));
} else {
bubble = self.addChild(new Bubble(getMaxTypes(), true));
}
bubble.isFreeBubble = true;
};
self.swapBubbles = function () {
if (storage.swapsRemaining > 0) {
storage.swapsRemaining--;
// Swap current bubble with preview bubble
var tempType = bubble.type;
var tempIsFireBall = bubble.isFireBall;
// Store preview bubble properties
var previewType = previewBubble.type;
var previewIsFireBall = previewBubble.isFireBall;
// Destroy current bubbles
bubble.destroy();
previewBubble.destroy();
// Create new current bubble with preview properties
bubble = self.addChild(new Bubble(getMaxTypes(), previewIsFireBall, previewType));
bubble.isFreeBubble = true;
// Create new preview bubble with old current properties
previewBubble = self.addChildAt(new Bubble(getMaxTypes(), tempIsFireBall, tempType), 0);
previewBubble.scale.set(.7, .7);
previewBubble.x = -90;
previewBubble.y = 20;
previewBubble.isFreeBubble = true;
refreshHintLine();
LK.getSound('bubbleSwap').play();
LK.getSound('powerupSwoosh').play();
return true;
}
return false;
};
self.down = function () {
// Handle bubble swap when clicking on launcher
self.swapBubbles();
};
});
var LevelDisplayOverlay = Container.expand(function () {
var self = Container.call(this);
// Position in top left area with more margin for mobile
self.y = 160;
self.x = 200;
// Background circle - larger for mobile
var levelBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
levelBG.width = 180;
levelBG.height = 180;
levelBG.x = 0;
levelBG.y = 0;
// Dropshadow for label
var labelShadow = self.addChild(new Text2(storage.currentLevel, {
size: 80,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
labelShadow.anchor.set(.5, .5);
labelShadow.x = 3;
labelShadow.y = 3;
// Main label
var label = self.addChild(new Text2(storage.currentLevel, {
size: 80,
fill: 0xFFFFFF,
font: "Impact"
}));
label.anchor.set(.5, .5);
label.x = 0;
label.y = 0;
// Level text
var levelText = self.addChild(new Text2('SEVIYE', {
size: 36,
fill: 0xFFFFFF,
font: "Impact"
}));
levelText.anchor.set(.5, .5);
levelText.x = 0;
levelText.y = -60;
levelText.alpha = 0.8;
self.updateLevel = function () {
label.setText(storage.currentLevel);
labelShadow.setText(storage.currentLevel);
};
// Initialize display
self.updateLevel();
return self;
});
var MissionOverlay = Container.expand(function () {
var self = Container.call(this);
// Position in top right corner in GUI space - adjusted for mobile
self.x = 0;
self.y = 0;
// Mission background - larger for mobile readability
var missionBG = self.attachAsset('countbg', {
anchorX: 1,
anchorY: 0
});
missionBG.width = 420;
missionBG.height = 200;
missionBG.alpha = 0.9;
missionBG.x = -20;
missionBG.y = 120;
// Mission title - larger for mobile
var titleText = self.addChild(new Text2('GÖREV', {
size: 42,
fill: 0xFFFFFF,
font: "Impact"
}));
titleText.anchor.set(0.5, 0.5);
titleText.x = -210;
titleText.y = 150;
// Level indicator
var levelText = self.addChild(new Text2('LV 1', {
size: 28,
fill: 0xFFFF00,
font: "Impact"
}));
levelText.anchor.set(0.5, 0.5);
levelText.x = -210;
levelText.y = 175;
// Mission targets display - properly spaced
var targetTexts = [];
var targetColors = [0xff2853, 0x44d31f, 0x5252ff]; // Red, Green, Blue
for (var i = 0; i < 3; i++) {
var targetText = self.addChild(new Text2('0/0', {
size: 28,
fill: targetColors[i],
font: "Impact"
}));
targetText.anchor.set(0.5, 0.5);
targetText.x = -280 + i * 80;
targetText.y = 200;
targetTexts.push(targetText);
}
// Timer display - properly positioned
var timerText = self.addChild(new Text2('60s', {
size: 36,
fill: 0xFFFFFF,
font: "Impact"
}));
timerText.anchor.set(0.5, 0.5);
timerText.x = -210;
timerText.y = 235;
self.updateMission = function (targets, collected, timeLeft) {
for (var i = 0; i < 3; i++) {
targetTexts[i].setText(collected[i] + '/' + targets[i]);
}
timerText.setText(Math.ceil(timeLeft / 60) + 's');
levelText.setText('LV ' + currentGameLevel);
// Flash timer red when low
if (timeLeft < 600) {
// 10 seconds
timerText.fill = timeLeft % 20 < 10 ? 0xff0000 : 0xFFFFFF;
}
};
return self;
});
var PowerupBubble = Container.expand(function () {
var self = Container.call(this);
var bubbleGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
bubbleGraphics.width = 150;
bubbleGraphics.height = 150;
self.type = -1; // Special type for powerup
self.isPowerup = true;
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;
};
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) {
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 += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// When powerup reaches the bottom of the screen, trigger powerup earned animation
if (self.y > 2732 - 400) {
// Play sound
LK.getSound('scoreCollected').play();
// Create and start powerup earned animation
var animation = game.addChild(new PowerupEarnedAnimation(self.x, self.y));
animation.start();
// Destroy the original bubble
self.destroy();
}
}
};
});
var PowerupEarnedAnimation = Container.expand(function (startX, startY) {
var self = Container.call(this);
// Create a 2x size powerup graphic
var powerupGraphics = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
// Create dropshadow for earnedText (not a child of self, but a global overlay)
var earnedTextShadow = new Text2('POWERUP EARNED!', {
size: 150,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
});
earnedTextShadow.anchor.set(0.5, 0.5);
earnedTextShadow.y = game.height / 2 - 250 + 20 + 100 + 8;
earnedTextShadow.x = game.width / 2 + 8;
earnedTextShadow.alpha = 0;
// Create main earnedText (no stroke)
var earnedText = new Text2('POWERUP EARNED!', {
size: 150,
fill: 0xFFFFFF,
font: "Impact"
});
earnedText.anchor.set(0.5, 0.5);
powerupGraphics.width = 150;
powerupGraphics.height = 150;
earnedText.y = game.height / 2 - 250 + 20 + 100;
earnedText.x = game.width / 2;
earnedText.alpha = 0;
// Set initial position
self.x = startX;
self.y = startY;
// Animation phases
var phase = 0;
self.start = function () {
// Add text and dropshadow to overlay (so it doesn't move with self)
LK.getSound('powerupSwoosh').play();
if (!earnedTextShadow.parent) {
game.addChild(earnedTextShadow);
}
if (!earnedText.parent) {
game.addChild(earnedText);
}
// Phase 1: Move to center of screen
tween(powerupGraphics.scale, {
x: 2,
y: 2
}, {
duration: 300,
easing: tween.easeIn
});
tween(self, {
x: game.width / 2,
y: game.height / 2
}, {
duration: 300,
easing: tween.easeIn
});
// Show text and dropshadow
tween(earnedTextShadow, {
alpha: 0.35,
y: game.height / 2 - 250 + 8
}, {
duration: 300,
delay: 100,
easing: tween.easeOut
});
tween(earnedText, {
alpha: 1,
y: game.height / 2 - 250
}, {
duration: 300,
delay: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
// Wait a moment before moving to powerup counter
LK.setTimeout(function () {
LK.getSound('powerupSwoosh').play();
// Get target position (powerup counter)
var targetX = fireBallPowerupOverlay.x;
var targetY = fireBallPowerupOverlay.y;
// Phase 2: Move to powerup counter
tween(self, {
x: targetX,
y: targetY
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
LK.getSound('powerupThump').play();
// Increment powerup counter
fireBallPowerupOverlay.increaseFireballCount();
// Destroy animation
if (earnedTextShadow.parent) {
earnedTextShadow.parent.removeChild(earnedTextShadow);
}
if (earnedText.parent) {
earnedText.parent.removeChild(earnedText);
}
self.destroy();
}
});
// Fade out text and dropshadow as we move
tween(earnedTextShadow, {
alpha: 0,
y: game.height / 2 - 250 - 20 + 8
}, {
duration: 300,
easing: tween.easeOut
});
tween(earnedText, {
alpha: 0,
y: game.height / 2 - 250 - 20
}, {
duration: 300,
easing: tween.easeOut
});
// Scale down as we move to counter
tween(powerupGraphics, {
width: 210,
height: 210
}, {
duration: 300,
easing: tween.easeIn
});
tween(powerupGraphics.scale, {
x: 1,
y: 1
}, {
duration: 300,
easing: tween.easeIn
});
}, 1000);
}
});
};
return self;
});
// Make active when we have fireballs
var PowerupParticle = Container.expand(function (angle, speed) {
var self = Container.call(this);
var particle = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
particle.width = 60;
particle.height = 60;
self.scale.set(0.7 + Math.random() * 0.5, 0.7 + Math.random() * 0.5);
self.alpha = 1;
var vx = Math.cos(angle) * speed;
var vy = Math.sin(angle) * speed;
var gravity = 0.5 + Math.random() * 0.3;
var life = 24 + Math.floor(Math.random() * 10);
var tick = 0;
self.update = function () {
self.x += vx;
self.y += vy;
vy += gravity;
self.alpha -= 0.04 + Math.random() * 0.01;
tick++;
if (tick > life || self.alpha <= 0) {
self.destroy();
}
};
return self;
});
var ScoreIndicatorLabel = Container.expand(function (score, type) {
var self = Container.call(this);
var label = new Text2(score, {
size: 100,
fill: "#" + bubbleColors[type].toString(16).padStart(6, '0'),
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);
// Dropshadow for scoreMultiplierLabel
var scoreMultiplierLabelShadow = new Text2(baseValue, {
size: 100,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
});
scoreMultiplierLabelShadow.anchor.set(0.5, 0);
self.addChild(scoreMultiplierLabelShadow);
// 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 scoreValue = baseValue * currentMultiplier;
// Check if bubble color matches the pool color for bonus scoring
var poolIndex = Math.floor(self.x / (2048 / 6)); // Determine which pool this is (0-5)
var isColorMatch = false;
// Check if bubble type matches pool color (red=0, green=1, blue=2, purple=3, cyan=4, pink=5)
if (bubble.type >= 0 && bubble.type < 6 && bubble.type === poolIndex) {
isColorMatch = true;
scoreValue *= 3; // Triple score for matching color
// Play bonus sound
LK.getSound('comboBonus').play();
// Create special color match particle effect
var bonusParticle = particlesLayer.addChild(new BubbleRemoveParticle());
bonusParticle.x = bubble.x;
bonusParticle.y = self.y + 100;
bonusParticle.scale.set(2, 2);
bonusParticle.alpha = 0.8;
}
var scoreIndicator = game.addChild(new ScoreIndicatorLabel(scoreValue, bubble.type));
scoreIndicator.x = self.x;
scoreIndicator.y = self.y;
// Make score indicator bigger for color matches
if (isColorMatch) {
scoreIndicator.scale.set(1.5, 1.5);
}
// Award coins based on score
var coinsEarned = Math.floor(scoreValue / 50);
if (doubleCoinsActive) {
coinsEarned *= 2; // Double coins when power-up is active
}
if (coinsEarned > 0) {
storage.coins += coinsEarned;
}
var particle = particlesLayer.addChild(new BubbleRemoveParticle());
particle.x = bubble.x;
particle.y = self.y + 150;
// Track mission progress
if (missionActive && bubble.type >= 0 && bubble.type < 3) {
missionCollected[bubble.type]++;
// Check if mission is complete
if (checkMissionComplete()) {
missionActive = false;
// Mission completed - continue playing
LK.effects.flashScreen(0x00ff00, 1000); // Green flash for success
}
}
};
self.setMultiplier = function (multiplier) {
currentMultiplier = multiplier;
scoreMultiplierLabel.setText(baseValue * currentMultiplier);
scoreMultiplierLabelShadow.setText(baseValue * currentMultiplier);
};
});
var ShopOverlay = Container.expand(function () {
var self = Container.call(this);
// Full screen background
var background = self.attachAsset('uxoverlay', {
anchorX: 0,
anchorY: 0
});
background.width = 2048;
background.height = 2732;
background.alpha = 0.95;
// Shop title
var titleShadow = self.addChild(new Text2('SHOP', {
size: 150,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
titleShadow.anchor.set(0.5, 0.5);
titleShadow.x = game.width / 2 + 4;
titleShadow.y = 300 + 4;
var titleText = self.addChild(new Text2('SHOP', {
size: 150,
fill: 0xFFFFFF,
font: "Impact"
}));
titleText.anchor.set(0.5, 0.5);
titleText.x = game.width / 2;
titleText.y = 300;
// Coins display
var coinsText = self.addChild(new Text2('Coins: ' + storage.coins, {
size: 80,
fill: 0xFFD700,
font: "Impact"
}));
coinsText.anchor.set(0.5, 0.5);
coinsText.x = game.width / 2;
coinsText.y = 400;
// Shop items
var shopItems = [{
name: 'Extra Swaps',
description: 'Start with +1 swap',
cost: 100,
maxLevel: 3,
upgrade: 'extraSwaps'
}, {
name: 'Power Balls',
description: 'Start with +1 fireball',
cost: 200,
maxLevel: 3,
upgrade: 'powerBalls'
}, {
name: 'Time Bonus',
description: '+10s mission time',
cost: 150,
maxLevel: 5,
upgrade: 'timeBonus'
}];
var itemElements = [];
for (var i = 0; i < shopItems.length; i++) {
var item = shopItems[i];
var yPos = 550 + i * 220;
var currentLevel = storage.shopUpgrades[item.upgrade];
var isMaxed = currentLevel >= item.maxLevel;
var actualCost = item.cost + currentLevel * Math.floor(item.cost * 0.5);
var canAfford = storage.coins >= actualCost;
// Item background
var itemBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
itemBG.x = game.width / 2;
itemBG.y = yPos;
itemBG.width = 800;
itemBG.height = 180;
itemBG.alpha = 0.7;
// Item name
var itemName = self.addChild(new Text2(item.name, {
size: 60,
fill: 0xFFFFFF,
font: "Impact"
}));
itemName.anchor.set(0.5, 0.5);
itemName.x = game.width / 2;
itemName.y = yPos - 50;
// Item description
var itemDesc = self.addChild(new Text2(item.description, {
size: 40,
fill: 0xCCCCCC,
font: "Impact"
}));
itemDesc.anchor.set(0.5, 0.5);
itemDesc.x = game.width / 2;
itemDesc.y = yPos - 15;
// Level indicator
var levelText = self.addChild(new Text2('Level: ' + currentLevel + '/' + item.maxLevel, {
size: 35,
fill: 0xFFFF00,
font: "Impact"
}));
levelText.anchor.set(0.5, 0.5);
levelText.x = game.width / 2 - 200;
levelText.y = yPos + 25;
// Cost display underneath
var costDisplayText = self.addChild(new Text2(isMaxed ? 'MAXED OUT' : actualCost + ' coins', {
size: 50,
fill: isMaxed ? 0x888888 : 0xFFD700,
font: "Impact"
}));
costDisplayText.anchor.set(0.5, 0.5);
costDisplayText.x = game.width / 2;
costDisplayText.y = yPos + 55;
// Buy button - larger for mobile touch
var buyButtonBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
buyButtonBG.x = game.width / 2 + 200;
buyButtonBG.y = yPos + 25;
buyButtonBG.width = 200; // Larger touch target
buyButtonBG.height = 80; // Larger touch target
buyButtonBG.alpha = isMaxed ? 0.3 : canAfford ? 0.8 : 0.5;
var buttonText = isMaxed ? 'MAXED' : canAfford ? 'BUY' : 'LOCKED';
var buttonColor = isMaxed ? 0x666666 : canAfford ? 0x00FF00 : 0xFF0000;
var buyText = self.addChild(new Text2(buttonText, {
size: 40,
fill: buttonColor,
font: "Impact"
}));
buyText.anchor.set(0.5, 0.5);
buyText.x = game.width / 2 + 200;
buyText.y = yPos + 25;
itemElements.push({
bg: itemBG,
buyBG: buyButtonBG,
item: item,
buyText: buyText,
levelText: levelText,
costText: costDisplayText,
canAfford: canAfford,
isMaxed: isMaxed,
actualCost: actualCost
});
}
// Back button
var backButtonBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
backButtonBG.x = game.width / 2;
backButtonBG.y = game.height - 300;
backButtonBG.width = 300;
backButtonBG.height = 100;
var backButtonText = self.addChild(new Text2('BACK', {
size: 60,
fill: 0xFFFFFF,
font: "Impact"
}));
backButtonText.anchor.set(0.5, 0.5);
backButtonText.x = game.width / 2;
backButtonText.y = game.height - 300;
// Click handler
self.down = function (x, y, obj) {
// Check back button
var backLeft = backButtonBG.x - backButtonBG.width / 2;
var backRight = backButtonBG.x + backButtonBG.width / 2;
var backTop = backButtonBG.y - backButtonBG.height / 2;
var backBottom = backButtonBG.y + backButtonBG.height / 2;
if (x >= backLeft && x <= backRight && y >= backTop && y <= backBottom) {
self.closeShop();
return;
}
// Check shop items - specifically the buy button area
for (var i = 0; i < itemElements.length; i++) {
var element = itemElements[i];
var buyLeft = element.buyBG.x - element.buyBG.width / 2;
var buyRight = element.buyBG.x + element.buyBG.width / 2;
var buyTop = element.buyBG.y - element.buyBG.height / 2;
var buyBottom = element.buyBG.y + element.buyBG.height / 2;
if (x >= buyLeft && x <= buyRight && y >= buyTop && y <= buyBottom) {
self.buyItem(element.item, i);
break;
}
}
};
self.buyItem = function (item, index) {
var currentLevel = storage.shopUpgrades[item.upgrade];
if (currentLevel >= item.maxLevel) return;
var actualCost = item.cost + currentLevel * Math.floor(item.cost * 0.5);
if (storage.coins < actualCost) return;
storage.coins -= actualCost;
storage.shopUpgrades[item.upgrade]++;
LK.getSound('buttonPress').play();
LK.getSound('scoreCollected').play();
// Update display
self.updateDisplay();
};
self.updateDisplay = function () {
coinsText.setText('Coins: ' + storage.coins);
for (var i = 0; i < itemElements.length; i++) {
var element = itemElements[i];
var item = element.item;
var currentLevel = storage.shopUpgrades[item.upgrade];
var isMaxed = currentLevel >= item.maxLevel;
var actualCost = item.cost + currentLevel * Math.floor(item.cost * 0.5);
var canAfford = storage.coins >= actualCost;
element.levelText.setText('Level: ' + currentLevel + '/' + item.maxLevel);
element.costText.setText(isMaxed ? 'MAXED OUT' : actualCost + ' coins');
element.costText.fill = isMaxed ? 0x888888 : 0xFFD700;
var buttonText = isMaxed ? 'MAXED' : canAfford ? 'BUY' : 'LOCKED';
var buttonColor = isMaxed ? 0x666666 : canAfford ? 0x00FF00 : 0xFF0000;
element.buyText.setText(buttonText);
element.buyText.fill = buttonColor;
element.buyBG.alpha = isMaxed ? 0.3 : canAfford ? 0.8 : 0.5;
element.canAfford = canAfford;
element.isMaxed = isMaxed;
element.actualCost = actualCost;
}
};
self.closeShop = function () {
LK.getSound('buttonPress').play();
tween(self, {
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
var SlowTimeBubble = Container.expand(function () {
var self = Container.call(this);
// Use a blue-tinted bubble to represent slow time
var bubbleGraphics = self.attachAsset('bubble2', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it visually distinct with blue tint
bubbleGraphics.tint = 0x4444ff;
self.type = -5; // Special type for slow time bubble
self.isSlowTimeBubble = true;
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;
};
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;
};
self.update = function () {
if (self.isFreeBubble) {
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 += 0.8;
speedX *= 0.995;
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75;
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
var restitution = 0.6;
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// When slow time bubble reaches bottom, slow down grid movement
if (self.y > 2732 - 400) {
self.activateSlowTime();
self.destroy();
}
}
};
self.activateSlowTime = function () {
// Slow down grid movement by 70% for 10 seconds
var originalSpeed = gridSpeed;
gridSpeed *= 0.3; // Slow to 30% speed
LK.getSound('powerupSwoosh').play();
LK.getSound('timeBubbleCollect').play();
// Visual effect
LK.effects.flashScreen(0x4444ff, 500);
// Show effect text
var effectText = game.addChild(new Text2('SLOW TIME!', {
size: 120,
fill: 0x4444ff,
font: "Impact"
}));
effectText.anchor.set(0.5, 0.5);
effectText.x = game.width / 2;
effectText.y = game.height / 2;
effectText.alpha = 1;
tween(effectText, {
y: effectText.y - 200,
alpha: 0
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
effectText.destroy();
}
});
// Restore normal speed after 10 seconds
LK.setTimeout(function () {
gridSpeed = originalSpeed;
}, 10000);
};
return self;
});
var StartScreenOverlay = Container.expand(function () {
var self = Container.call(this);
// Full screen background
var background = self.attachAsset('uxoverlay', {
anchorX: 0,
anchorY: 0
});
background.width = 2048;
background.height = 2732;
background.alpha = 0.95;
// Game logo - visual brand element
var gameLogo = self.attachAsset('gamelogo', {
anchorX: 0.5,
anchorY: 0.5
});
gameLogo.x = game.width / 2;
gameLogo.y = game.height / 2 - 350;
gameLogo.width = 500;
gameLogo.height = 120;
// Store reference for animation
self.gameLogo = gameLogo;
// Add update method for logo animation
self.update = function () {
if (self.gameLogo) {
var logoScale = 1 + Math.sin(LK.ticks * 0.05) * 0.1;
self.gameLogo.scale.set(logoScale, logoScale);
var logoAlpha = 0.8 + Math.sin(LK.ticks * 0.03) * 0.2;
self.gameLogo.alpha = logoAlpha;
}
};
// Game title
var titleShadow = self.addChild(new Text2('BUBBLE CLASH', {
size: 200,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
titleShadow.anchor.set(0.5, 0.5);
titleShadow.x = game.width / 2 + 6;
titleShadow.y = game.height / 2 - 200 + 6;
var titleText = self.addChild(new Text2('BUBBLE CLASH', {
size: 200,
fill: 0xFFFFFF,
font: "Impact"
}));
titleText.anchor.set(0.5, 0.5);
titleText.x = game.width / 2;
titleText.y = game.height / 2 - 200;
// Play button background - larger for mobile
var playButtonBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
playButtonBG.x = game.width / 2;
playButtonBG.y = game.height / 2 + 50;
playButtonBG.width = 500; // Larger touch area
playButtonBG.height = 150; // Larger touch area
playButtonBG.alpha = 0.1; // Slightly visible for better mobile UX
// Play button text shadow
var playButtonShadow = self.addChild(new Text2('PLAY', {
size: 80,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
playButtonShadow.anchor.set(0.5, 0.5);
playButtonShadow.x = game.width / 2 + 4;
playButtonShadow.y = game.height / 2 + 50 + 4;
// Play button text
var playButtonText = self.addChild(new Text2('PLAY', {
size: 80,
fill: 0xFFFFFF,
font: "Impact"
}));
playButtonText.anchor.set(0.5, 0.5);
playButtonText.x = game.width / 2;
playButtonText.y = game.height / 2 + 50;
// Shop button background
var shopButtonBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
shopButtonBG.x = game.width / 2;
shopButtonBG.y = game.height / 2 + 200;
shopButtonBG.width = 300;
shopButtonBG.height = 100;
shopButtonBG.alpha = 0; // Make shop button transparent like play button
// Shop button text shadow
var shopButtonShadow = self.addChild(new Text2('SHOP', {
size: 60,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
shopButtonShadow.anchor.set(0.5, 0.5);
shopButtonShadow.x = game.width / 2 + 3;
shopButtonShadow.y = game.height / 2 + 200 + 3;
// Shop button text
var shopButtonText = self.addChild(new Text2('SHOP', {
size: 60,
fill: 0xFFD700,
font: "Impact"
}));
shopButtonText.anchor.set(0.5, 0.5);
shopButtonText.x = game.width / 2;
shopButtonText.y = game.height / 2 + 200;
// Instructions text
var instructionText = self.addChild(new Text2('Tap to aim and shoot bubbles!', {
size: 60,
fill: 0xFFFFFF,
font: "Impact"
}));
instructionText.anchor.set(0.5, 0.5);
instructionText.x = game.width / 2;
instructionText.y = game.height / 2 + 300;
instructionText.alpha = 0.8;
// Mission info text
var missionInfoText = self.addChild(new Text2('Complete color missions before time runs out!', {
size: 45,
fill: 0xFFFF00,
font: "Impact"
}));
missionInfoText.anchor.set(0.5, 0.5);
missionInfoText.x = game.width / 2;
missionInfoText.y = game.height / 2 + 370;
missionInfoText.alpha = 0.9;
// Coins display
var coinsDisplay = self.addChild(new Text2('Coins: ' + storage.coins, {
size: 60,
fill: 0xFFD700,
font: "Impact"
}));
coinsDisplay.anchor.set(0.5, 0.5);
coinsDisplay.x = game.width / 2;
coinsDisplay.y = game.height / 2 + 480;
coinsDisplay.alpha = 1.0;
// Play button click handler
self.down = function (x, y, obj) {
// Check if click is within play button bounds
var buttonLeft = playButtonBG.x - playButtonBG.width / 2;
var buttonRight = playButtonBG.x + playButtonBG.width / 2;
var buttonTop = playButtonBG.y - playButtonBG.height / 2;
var buttonBottom = playButtonBG.y + playButtonBG.height / 2;
if (x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom) {
// Start the game
self.startGame();
return;
}
// Check if click is within shop button bounds
var shopLeft = shopButtonBG.x - shopButtonBG.width / 2;
var shopRight = shopButtonBG.x + shopButtonBG.width / 2;
var shopTop = shopButtonBG.y - shopButtonBG.height / 2;
var shopBottom = shopButtonBG.y + shopButtonBG.height / 2;
if (x >= shopLeft && x <= shopRight && y >= shopTop && y <= shopBottom) {
// Open shop
self.openShop();
}
};
self.openShop = function () {
LK.getSound('buttonPress').play();
var shop = game.addChild(new ShopOverlay());
};
self.startGame = function () {
LK.getSound('buttonPress').play();
LK.getSound('powerupThump').play();
// Hide start screen with fade out animation
tween(self, {
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
gameStarted = true;
// Apply shop upgrades
applyShopUpgrades();
}
});
};
return self;
});
var SwapsCounterOverlay = Container.expand(function () {
var self = Container.call(this);
// Position in top right area with more margin for mobile
self.y = 160;
self.x = game.width - 280;
// Background circle - larger for mobile
var countBG = self.attachAsset('countbg', {
anchorX: 0.5,
anchorY: 0.5
});
countBG.width = 180;
countBG.height = 180;
countBG.x = 0;
countBG.y = 0;
// Dropshadow for label
var labelShadow = self.addChild(new Text2(storage.swapsRemaining, {
size: 80,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
}));
labelShadow.anchor.set(.5, .5);
labelShadow.x = 3;
labelShadow.y = 3;
// Main label
var label = self.addChild(new Text2(storage.swapsRemaining, {
size: 80,
fill: 0xFFFFFF,
font: "Impact"
}));
label.anchor.set(.5, .5);
label.x = 0;
label.y = 0;
// Swap text
var swapText = self.addChild(new Text2('DEĞIŞ', {
size: 32,
fill: 0xFFFFFF,
font: "Impact"
}));
swapText.anchor.set(.5, .5);
swapText.x = 0;
swapText.y = -60;
swapText.alpha = 0.8;
// Swap icon (using bubble asset as placeholder)
var swapIcon = self.attachAsset('bubble0', {
anchorX: 0.5,
anchorY: 0.5
});
swapIcon.x = -90;
swapIcon.y = 0;
swapIcon.width = 90;
swapIcon.height = 90;
swapIcon.alpha = 0.7;
self.updateCount = function () {
label.setText(storage.swapsRemaining);
labelShadow.setText(storage.swapsRemaining);
// Fade out when no swaps remaining
self.alpha = storage.swapsRemaining > 0 ? 1 : 0.5;
};
// Initialize display
self.updateCount();
return self;
});
var TimeBubble = Container.expand(function () {
var self = Container.call(this);
// Use a green-tinted bubble to represent time bonus
var bubbleGraphics = self.attachAsset('bubble1', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it visually distinct with green tint and glow effect
bubbleGraphics.tint = 0x00ff00;
self.type = -2; // Special type for time bubble
self.isTimeBubble = true;
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;
};
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;
};
// Pulsing animation for visual appeal
var pulseScale = 1.0;
var pulseDirection = 1;
self.update = function () {
if (self.isFreeBubble) {
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;
}
// Pulsing animation
pulseScale += pulseDirection * 0.01;
if (pulseScale > 1.1) {
pulseDirection = -1;
} else if (pulseScale < 0.9) {
pulseDirection = 1;
}
self.scale.set(pulseScale, pulseScale);
} else {
self.x += speedX;
self.y += speedY;
speedY += 0.8; // More realistic gravity
speedX *= 0.995; // Add air resistance
if (self.x < bubbleSize / 2 && speedX < 0 || self.x > game.width - bubbleSize / 2 && speedX > 0) {
speedX = -speedX * 0.75; // Energy loss on wall bounce
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) {
var angle = Math.atan2(dy, dx);
var newSpeed = Math.sqrt(speedX * speedX + speedY * speedY);
// More realistic collision response with proper momentum transfer
var restitution = 0.6; // Coefficient of restitution
speedX = Math.cos(angle) * newSpeed * restitution;
speedY = Math.sin(angle) * newSpeed * restitution;
var overlap = minDist - distance;
self.x += overlap * Math.cos(angle);
self.y += overlap * Math.sin(angle);
LK.getSound('circleBounce').play();
}
}
// When time bubble reaches bottom, add time to mission
if (self.y > 2732 - 400) {
LK.getSound('timeBubbleCollect').play();
LK.getSound('scoreCollected').play();
LK.getSound('powerupSwoosh').play();
// Add 15 seconds to mission time
if (missionActive) {
missionTimeLeft += 15 * 60; // 15 seconds in ticks
// Show time bonus animation
var bonusText = game.addChild(new Text2('+15s', {
size: 120,
fill: 0x00ff00,
font: "Impact"
}));
bonusText.anchor.set(0.5, 0.5);
bonusText.x = self.x;
bonusText.y = self.y - 100;
bonusText.alpha = 1;
tween(bonusText, {
y: bonusText.y - 200,
alpha: 0
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
bonusText.destroy();
}
});
}
self.destroy();
}
}
};
return self;
});
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 + (storage.currentLevel - 1) * 0.1; // Increase speed by 0.1 per level
var gameStarted = false; // Flag to track if game has started
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
scoreLabelShadow.setText(newScore.toString()); // Update the shadow as well
}
//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 ballsRemaining = 30; // Start with 30 balls per level
var maxBallsPerLevel = 30; // Base number of balls per level
var bubbleSize = 150;
var gameIsStarted = false;
var bubbleColors = [0xff2853, 0x44d31f, 0x5252ff, 0xcb2bff, 0x28f2f0, 0xff69b4];
var barriers = [];
var maxSelectableBubble = getMaxTypes();
var warningLines = [];
// Mission system variables
var missionTargets = [0, 0, 0]; // Targets for red, green, blue bubbles
var missionCollected = [0, 0, 0]; // Collected count for each color
var missionTimeLeft = 3600; // 60 seconds * 60 ticks per second
var missionActive = false;
var missionOverlay;
var currentGameLevel = 1; // Current level (1-10)
var maxGameLevel = 10;
var doubleCoinsActive = false; // Track if double coins power-up is active
// Level-based mission configurations
var levelMissions = [{
level: 1,
targets: [8, 6, 4],
time: 45
},
// Level 1: 45 seconds
{
level: 2,
targets: [10, 8, 6],
time: 50
},
// Level 2: 50 seconds
{
level: 3,
targets: [12, 10, 8],
time: 55
},
// Level 3: 55 seconds
{
level: 4,
targets: [15, 12, 10],
time: 60
},
// Level 4: 60 seconds
{
level: 5,
targets: [18, 15, 12],
time: 65
},
// Level 5: 65 seconds
{
level: 6,
targets: [20, 18, 15],
time: 70
},
// Level 6: 70 seconds
{
level: 7,
targets: [25, 20, 18],
time: 75
},
// Level 7: 75 seconds
{
level: 8,
targets: [30, 25, 20],
time: 80
},
// Level 8: 80 seconds
{
level: 9,
targets: [35, 30, 25],
time: 85
},
// Level 9: 85 seconds
{
level: 10,
targets: [40, 35, 30],
time: 90
} // Level 10: 90 seconds
];
// Initialize mission based on current level
function initializeMission() {
var levelConfig = levelMissions[currentGameLevel - 1];
if (levelConfig) {
for (var i = 0; i < 3; i++) {
missionTargets[i] = levelConfig.targets[i];
missionCollected[i] = 0;
}
// Apply time bonus from shop upgrades
var bonusTime = storage.shopUpgrades.timeBonus * 10; // 10 seconds per upgrade level
missionTimeLeft = (levelConfig.time + bonusTime) * 60; // Convert to ticks
missionActive = true;
}
}
// Check if mission is completed
function checkMissionComplete() {
for (var i = 0; i < 3; i++) {
if (missionCollected[i] < missionTargets[i]) {
return false;
}
}
return true;
}
function applyShopUpgrades() {
// Apply extra swaps upgrade
storage.swapsRemaining += storage.shopUpgrades.extraSwaps;
if (swapsCounterOverlay) {
swapsCounterOverlay.updateCount();
}
// Apply power balls upgrade
if (storage.shopUpgrades.powerBalls > 0) {
fireBallPowerupOverlay.fireballsLeft += storage.shopUpgrades.powerBalls;
fireBallPowerupOverlay.updateDisplay();
}
// Initialize ball counter for current level
refreshBallCounter();
// Apply time bonus upgrade (will be used in initializeMission)
}
// Handle mission failure
function handleMissionFailure() {
missionActive = false;
LK.effects.flashScreen(0xff0000, 3000);
LK.getSound('timeRunOut').play();
LK.getSound('gameOverJingle').play();
LK.getSound('powerupSwoosh').play(); // Additional sound for mission failure
LK.showGameOver();
}
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 < 5; a++) {
for (var b = 0; b < 3; b++) {
var barrier = game.addChild(new Barrier());
barrier.y = 2732 - 450 + b * 70;
barrier.x = 2048 / 6 * a + 2048 / 6;
barriers.push(barrier);
}
var barrierBlock = game.attachAsset('barrierblock', {});
barrierBlock.x = 2048 / 6 * a + 2048 / 6;
barrierBlock.y = 2732 - 450;
barrierBlock.anchor.x = .5;
}
// Create a score label dropshadow (offset, semi-transparent)
var scoreLabelShadow = new Text2('0', {
size: 120,
fill: 0x000000,
alpha: 0.35,
font: "Impact"
});
scoreLabelShadow.anchor.set(0.5, 0);
scoreLabelShadow.x = 4;
scoreLabelShadow.y = 6;
LK.gui.top.addChild(scoreLabelShadow);
// Create a score label (main)
var scoreLabel = new Text2('0', {
size: 120,
fill: 0xFFFFFF,
font: "Impact"
});
scoreLabel.anchor.set(0.5, 0);
scoreLabel.x = 0;
scoreLabel.y = 0;
LK.gui.top.addChild(scoreLabel);
var scoreMultipliers = [];
var baseScores = [10, 20, 50, 25, 15, 30];
var scoreIcons = ['bubble0', 'bubble1', 'bubble2', 'bubble3', 'bubble4', 'bubble5']; // Different visual icons for each multiplier
for (var a = 0; a < 6; a++) {
var sm = new ScoreMultipliers(baseScores[a]);
sm.x = 2048 / 6 * a + 2048 / 12;
sm.y = 2300;
// Add icon above the score
var scoreIcon = sm.attachAsset(scoreIcons[a], {
anchorX: 0.5,
anchorY: 0.5
});
scoreIcon.x = 0;
scoreIcon.y = -80;
scoreIcon.width = 80;
scoreIcon.height = 80;
scoreIcon.alpha = 0.8;
scoreMultipliers.push(sm);
game.addChild(sm);
}
var bonusUX = game.addChild(new BonusUX());
var fireBallPowerupOverlay = game.addChild(new FireBallPowerupOverlay());
var swapsCounterOverlay = game.addChild(new SwapsCounterOverlay());
var levelDisplayOverlay = game.addChild(new LevelDisplayOverlay());
var ballCounterOverlay = game.addChild(new BallCounterOverlay());
missionOverlay = LK.gui.topRight.addChild(new MissionOverlay());
// Initialize the mission
initializeMission();
// Add start screen overlay
var startScreenOverlay = game.addChild(new StartScreenOverlay());
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 - 180; // More space from bottom for mobile
// Scale up launcher for better mobile visibility
launcher.scale.set(1.2, 1.2);
var hintBubbleCache = [];
var hintBubbles = [];
var isValid = false;
var path = [];
var bubbleAlpha = 1;
var hintTargetX = game.width / 2;
var hintTargetY = 0;
// Mobile-optimized touch controls with larger touch zones
game.move = function (x, y, obj) {
if (!gameStarted) return; // Don't process moves before game starts
hintTargetX = x;
hintTargetY = y;
refreshHintLine();
};
// Separate down handler for better mobile touch response
game.down = function (x, y, obj) {
if (!gameStarted) return;
// Call move to update hint line immediately
game.move(x, y, obj);
};
function getMaxTypes() {
// Level-based difficulty: more colors as level increases
var level = storage.currentLevel;
// Ensure minimum of 3 colors, with progressive increase
var maxTypes = Math.max(3, Math.min(6, Math.floor(level / 3) + 3));
// Additional progression based on bubbles popped
var bubblesPopped = storage.totalBubblesPopped || 0;
var bonusTypes = Math.floor(bubblesPopped / 100);
maxTypes = Math.min(6, maxTypes + bonusTypes);
return maxTypes;
}
function calculateBallsPerLevel(level) {
// Base balls per level starting from 30, capped at 110
var baseBalls = 30 + (level - 1) * 1;
return Math.min(baseBalls, 110);
}
function refreshBallCounter() {
ballsRemaining = calculateBallsPerLevel(storage.currentLevel);
ballCounterOverlay.updateCount();
}
function refreshHintLine() {
if (!gameStarted) return; // Don't update hint line before game starts
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 = 120; // Increase spacing for mobile performance
function renderHintBubbels() {
if (isValid) {
hintOffset = hintOffset % distanceBetweenHintbubbles;
var distanceSinceLastDot = -hintOffset + 120;
var hintBubbleOffset = 0;
var lastPoint = path[0];
var bubble = launcher.getBubble();
var tint = bubble.isFireBall ? bubble.isBombBall ? 0x333333 : 0xff9c00 : 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 () {
hintOffset += 5;
if (isValid) {
bubbleAlpha = Math.min(bubbleAlpha + .05, 1);
} else {
bubbleAlpha = Math.max(bubbleAlpha - .05, 0);
}
refreshHintLine();
// Update swaps counter display
swapsCounterOverlay.updateCount();
var alphaList = grid.calculateWarningScoreList();
var hasHighWarning = false;
for (var a = 0; a < warningLines.length; a++) {
var value = alphaList[a] / 3;
if (value > 0.4) hasHighWarning = true;
warningLines[a].alpha += (Math.min(value, .6) - warningLines[a].alpha) / 100;
warningLines[a].scale.y += (value * 60 - warningLines[a].scale.y) / 100;
}
// Play warning sound when bubbles get dangerously close
if (hasHighWarning && LK.ticks % 180 === 0) {
LK.getSound('warningAlert').play();
}
// Update mission system - only if game has started
if (missionActive && gameStarted) {
missionTimeLeft--;
// Check for mission failure (time ran out)
if (missionTimeLeft <= 0) {
handleMissionFailure();
}
// Update mission display
missionOverlay.updateMission(missionTargets, missionCollected, missionTimeLeft);
} else if (missionActive) {
// Update mission display without decrementing time
missionOverlay.updateMission(missionTargets, missionCollected, missionTimeLeft);
}
};
game.up = function () {
if (!gameStarted) return; // Don't allow firing before game starts
if (ballsRemaining <= 0) return; // Don't allow firing if no balls left
if (isValid) {
launcher.fire();
// Start mission on first shot if not already active
if (!missionActive) {
initializeMission();
}
}
};
;
;
;
;
;
;
;
;
;
;