User prompt
Update just this: game.tabIndicators = []; game.tabIndicators.push(activeTabIndicator);
User prompt
Update as needed with: tabButton.down = function () { if (tab !== currentTab) { // Update tab appearance Object.keys(tabButtons).forEach(function (t) { if (tabButtons[t]) { tabButtons[t].alpha = t === tab ? 1.0 : 0.7; } }); // Remove ALL existing indicators while (game.tabIndicators.length > 0) { var oldIndicator = game.tabIndicators.pop(); tabsContainer.removeChild(oldIndicator); oldIndicator.destroy(); } // Create new indicator for current tab var newIndicator = LK.getAsset('blower', { width: tabWidth, height: 10, color: 0xFFFF00, alpha: 1.0 }); // Position at the bottom of this tab newIndicator.x = tabButton.x - tabWidth/2; newIndicator.y = tabHeight - 5; // Add to container and tracking array tabsContainer.addChild(newIndicator); game.tabIndicators.push(newIndicator); // Hide current tab content, show new tab content if (tabContainers[currentTab]) { tabContainers[currentTab].visible = false; } if (tabContainers[tab]) { tabContainers[tab].visible = true; } // Update current tab currentTab = tab; } return true; };
Code edit (1 edits merged)
Please save this source code
User prompt
Update with: // Modify the tab button down function for all tabs menuTabs.forEach(function (tab, index) { var tabButton = tabButtons[tab]; if (tabButton) { tabButton.down = function () { if (tab !== currentTab) { // Update tab appearance Object.keys(tabButtons).forEach(function (t) { if (tabButtons[t]) { tabButtons[t].alpha = t === tab ? 1.0 : 0.7; } }); // Remove ALL indicators (by finding them in tabsContainer) for (var i = tabsContainer.children.length - 1; i >= 0; i--) { var child = tabsContainer.children[i]; // Check if this is an indicator (has 10px height and yellow color) if (child.height === 10 && child.color === 0xFFFF00) { tabsContainer.removeChild(child); child.destroy(); } } // Create new indicator for current tab var newIndicator = LK.getAsset('blower', { width: tabWidth, height: 10, color: 0xFFFF00, alpha: 1.0 }); // Position at the bottom of this tab newIndicator.x = tabButton.x - tabWidth/2; newIndicator.y = tabHeight - 5; // Add to container tabsContainer.addChild(newIndicator); // Hide current tab content, show new tab content if (tabContainers[currentTab]) { tabContainers[currentTab].visible = false; } if (tabContainers[tab]) { tabContainers[tab].visible = true; } // Update current tab currentTab = tab; } return true; }; } }); // Call the down function for the current tab to create the initial indicator if (tabButtons[currentTab]) { tabButtons[currentTab].down(); }
Code edit (1 edits merged)
Please save this source code
User prompt
Update as needed with: // Modify the tab button down function for all tabs menuTabs.forEach(function (tab, index) { var tabButton = tabButtons[tab]; if (tabButton) { tabButton.down = function () { if (tab !== currentTab) { // Update tab appearance Object.keys(tabButtons).forEach(function (t) { if (tabButtons[t]) { tabButtons[t].alpha = t === tab ? 1.0 : 0.7; } }); // Remove ALL indicators (by finding them in tabsContainer) for (var i = tabsContainer.children.length - 1; i >= 0; i--) { var child = tabsContainer.children[i]; // Check if this is an indicator (has 10px height and yellow color) if (child.height === 10 && child.color === 0xFFFF00) { tabsContainer.removeChild(child); child.destroy(); } } // Create new indicator for current tab var newIndicator = LK.getAsset('blower', { width: tabWidth, height: 10, color: 0xFFFF00, alpha: 1.0 }); // Position at the bottom of this tab newIndicator.x = tabButton.x - tabWidth/2; newIndicator.y = tabHeight - 5; // Add to container tabsContainer.addChild(newIndicator); // Hide current tab content, show new tab content if (tabContainers[currentTab]) { tabContainers[currentTab].visible = false; } if (tabContainers[tab]) { tabContainers[tab].visible = true; } // Update current tab currentTab = tab; } return true; }; } }); // Call the down function for the current tab to create the initial indicator if (tabButtons[currentTab]) { tabButtons[currentTab].down(); }
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'tabButtons[currentTab]')' in or related to this line: 'if (tabButtons[currentTab]) {' Line Number: 786
User prompt
Update with: // Modify the tab button down function for all tabs menuTabs.forEach(function (tab, index) { var tabButton = tabButtons[tab]; if (tabButton) { tabButton.down = function () { if (tab !== currentTab) { // Update tab appearance Object.keys(tabButtons).forEach(function (t) { if (tabButtons[t]) { tabButtons[t].alpha = t === tab ? 1.0 : 0.7; } }); // Remove ALL indicators (by finding them in tabsContainer) for (var i = tabsContainer.children.length - 1; i >= 0; i--) { var child = tabsContainer.children[i]; // Check if this is an indicator (has 10px height and yellow color) if (child.height === 10 && child.color === 0xFFFF00) { tabsContainer.removeChild(child); child.destroy(); } } // Create new indicator for current tab var newIndicator = LK.getAsset('blower', { width: tabWidth, height: 10, color: 0xFFFF00, alpha: 1.0 }); // Position at the bottom of this tab newIndicator.x = tabButton.x - tabWidth/2; newIndicator.y = tabHeight - 5; // Add to container tabsContainer.addChild(newIndicator); // Hide current tab content, show new tab content if (tabContainers[currentTab]) { tabContainers[currentTab].visible = false; } if (tabContainers[tab]) { tabContainers[tab].visible = true; } // Update current tab currentTab = tab; } return true; }; } });
User prompt
Update with: // Create initial indicator for current tab (bubbles) var activeTabIndicator = LK.getAsset('blower', { width: tabWidth, height: 10, color: 0xFFFF00, alpha: 1.0 }); // Position at the bottom of current tab activeTabIndicator.x = tabButtons[currentTab].x - tabWidth/2; activeTabIndicator.y = tabHeight - 5; // Add to container tabsContainer.addChild(activeTabIndicator); // Store it in a property for tracking tabsContainer.currentIndicator = activeTabIndicator;
User prompt
Update with: // Update tab button down function to manage the indicator menuTabs.forEach(function (tab) { var tabButton = tabButtons[tab]; if (tabButton) { tabButton.down = function () { if (tab !== currentTab) { // Update tab appearance Object.keys(tabButtons).forEach(function (t) { if (tabButtons[t]) { tabButtons[t].alpha = t === tab ? 1.0 : 0.7; } }); // Remove old indicator if (tabsContainer.currentIndicator) { tabsContainer.removeChild(tabsContainer.currentIndicator); tabsContainer.currentIndicator.destroy(); } // Create new indicator var newIndicator = LK.getAsset('blower', { width: tabWidth, height: 10, color: 0xFFFF00, alpha: 1.0 }); // Position at bottom of this tab newIndicator.x = tabButton.x - tabWidth/2; newIndicator.y = tabHeight - 5; // Add to container and track it tabsContainer.addChild(newIndicator); tabsContainer.currentIndicator = newIndicator; // Switch tabs if (tabContainers[currentTab]) { tabContainers[currentTab].visible = false; } if (tabContainers[tab]) { tabContainers[tab].visible = true; } currentTab = tab; } return true; }; } });
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'tabButtons[currentTab].x')' in or related to this line: 'activeTabIndicator.x = tabButtons[currentTab].x - tabWidth / 2;' Line Number: 1021
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'tabButtons[currentTab].x')' in or related to this line: 'activeTabIndicator.x = tabButtons[currentTab].x - tabWidth / 2;' Line Number: 1021
User prompt
Update as needed with: // ADD THIS AFTER CREATING ALL TABS // Create initial indicator for current tab (bubbles) var activeTabIndicator = LK.getAsset('blower', { width: tabWidth, height: 10, color: 0xFFFF00, alpha: 1.0 }); // Position at the bottom of current tab activeTabIndicator.x = tabButtons[currentTab].x - tabWidth/2; activeTabIndicator.y = tabHeight - 5; // Add to container tabsContainer.addChild(activeTabIndicator); // Store it in a property for tracking tabsContainer.currentIndicator = activeTabIndicator;
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'tabButtons[currentTab].x')' in or related to this line: 'activeTabIndicator.x = tabButtons[currentTab].x - tabWidth / 2;' Line Number: 1027
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'menuPanel.width')' in or related to this line: 'var tabButton = LK.getAsset('upgradetab', {' Line Number: 960
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'tabButtons[currentTab].x')' in or related to this line: 'activeTabIndicator.x = tabButtons[currentTab].x - tabWidth / 2;' Line Number: 1016
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'tabsContainer.addChild')' in or related to this line: 'tabsContainer.addChild(activeTabIndicator);' Line Number: 1021
Code edit (1 edits merged)
Please save this source code
User prompt
Update as needed with: // Update tab button down function to manage the indicator menuTabs.forEach(function (tab) { var tabButton = tabButtons[tab]; if (tabButton) { tabButton.down = function () { if (tab !== currentTab) { // Update tab appearance Object.keys(tabButtons).forEach(function (t) { if (tabButtons[t]) { tabButtons[t].alpha = t === tab ? 1.0 : 0.7; } }); // Remove old indicator if (tabsContainer.currentIndicator) { tabsContainer.removeChild(tabsContainer.currentIndicator); tabsContainer.currentIndicator.destroy(); } // Create new indicator var newIndicator = LK.getAsset('blower', { width: tabWidth, height: 10, color: 0xFFFF00, alpha: 1.0 }); // Position at bottom of this tab newIndicator.x = tabButton.x - tabWidth/2; newIndicator.y = tabHeight - 5; // Add to container and track it tabsContainer.addChild(newIndicator); tabsContainer.currentIndicator = newIndicator; // Switch tabs if (tabContainers[currentTab]) { tabContainers[currentTab].visible = false; } if (tabContainers[tab]) { tabContainers[tab].visible = true; } currentTab = tab; } return true; }; } });
Code edit (1 edits merged)
Please save this source code
User prompt
Update with: var tabColumns = { bubbles: { left: [['player', 'lungCapacity'], ['player', 'quickBreath'], ['player', 'bubbleRefinement'], ['player', 'twinBubbles']], right: [['player', 'autoPop'], ['player', 'sizeVariance'], ['player', 'bubbleBreath'], ['machine', 'bubbleDurability']] // Added bubbleDurability here }, clams: { left: [['machines', 'basicClam'], ['machines', 'advancedClam'], ['machines', 'premiumClam']], right: [['machine', 'autoBubbleSpeed'], ['machine', 'bubbleQuality']] // Removed bubbleDurability from here }, // Other tabs remain unchanged }
Code edit (1 edits merged)
Please save this source code
User prompt
Update with: // In the updateCostTexts function, modify the 'machines' handling section: else if (category === 'machines') { var totalClams = UPGRADE_CONFIG.machines.basicClam.amount + UPGRADE_CONFIG.machines.advancedClam.amount + UPGRADE_CONFIG.machines.premiumClam.amount; // Check if we're at max clams (4) if (totalClams >= 4) { // For basic clams, always show "SOLD OUT" when at max if (key === 'basicClam') { costText.setText("SOLD OUT"); costText.fill = 0x888888; } // For advanced/premium, only show "SOLD OUT" if no lower tier clams to upgrade else if (key === 'advancedClam' && UPGRADE_CONFIG.machines.basicClam.amount <= 0) { costText.setText("SOLD OUT"); costText.fill = 0x888888; } else if (key === 'premiumClam' && UPGRADE_CONFIG.machines.basicClam.amount <= 0 && UPGRADE_CONFIG.machines.advancedClam.amount <= 0) { costText.setText("SOLD OUT"); costText.fill = 0x888888; } else { costText.setText(getUpgradeCost(upgrade) + " BP"); costText.fill = 0xFFFF00; } } else { costText.setText(getUpgradeCost(upgrade) + " BP"); costText.fill = 0xFFFF00; } }
User prompt
Update as needed with: if (category === 'machines') { var totalClams = UPGRADE_CONFIG.machines.basicClam.amount + UPGRADE_CONFIG.machines.advancedClam.amount + UPGRADE_CONFIG.machines.premiumClam.amount; // Check if already at max clams if (totalClams >= 4) { // For basic clams, immediately show "SOLD OUT" when at max if (key === 'basicClam') { updateCostText(category, key, "SOLD OUT", 0x888888); return true; } // Rest of the existing code for advanced/premium clams... } // Rest of your existing code for purchasing clams... }
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var facekit = LK.import("@upit/facekit.v1");
/****
* Classes
****/
// Bubble class to represent each bubble in the game
var Bubble = Container.expand(function () {
var self = Container.call(this);
self.lifetime = 0;
self.hasSplit = false;
self.splitHeight = null;
self.AUTO_POP_SIZE = 40;
self.MIN_SPLIT_SIZE = 30;
self.lastPopTime = 0;
self.visible = false; // Start invisible in pool
var sprite = self.attachAsset('bubble', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
self.colorTint = 0xFFFFFF; // Default white tint
self.colorMultiplier = 1.0; // Default multiplier
self.colorPhase = Math.random() * Math.PI * 2; // Random starting phase
self.size = 100;
self.updateColor = function () {
// Check if color upgrades are active
if (UPGRADE_CONFIG.colors.blueBubbles.currentLevel > 0) {
var color = 0xFFFFFF;
var multiplier = 1.0;
if (UPGRADE_CONFIG.colors.prismaticBubbles.currentLevel > 0) {
// Prismatic - constantly shifting color with variable multiplier
self.colorPhase += 0.02;
var colorValue = Math.sin(self.colorPhase) * 0.5 + 0.5; // 0 to 1
multiplier = 0.8 + colorValue * 1.2; // 0.8x to 2.0x
// Generate rainbow color
var r = Math.sin(self.colorPhase) * 127 + 128;
var g = Math.sin(self.colorPhase + 2) * 127 + 128;
var b = Math.sin(self.colorPhase + 4) * 127 + 128;
color = (Math.floor(r) << 16) + (Math.floor(g) << 8) + Math.floor(b);
} else if (UPGRADE_CONFIG.colors.rainbowBubbles.currentLevel > 0) {
// Rainbow - randomly changes color on activation
var colorChoice = Math.floor(Math.random() * 6);
switch (colorChoice) {
case 0:
color = 0xFF0000;
multiplier = 1.5;
break;
// Red
case 1:
color = 0xFFAA00;
multiplier = 1.4;
break;
// Orange
case 2:
color = 0xFFFF00;
multiplier = 1.3;
break;
// Yellow
case 3:
color = 0x00FF00;
multiplier = 1.2;
break;
// Green
case 4:
color = 0x0000FF;
multiplier = 1.1;
break;
// Blue
case 5:
color = 0xFF00FF;
multiplier = 1.6;
break;
// Purple
}
} else if (UPGRADE_CONFIG.colors.pinkBubbles.currentLevel > 0) {
color = 0xFF80C0; // Pink
multiplier = 1.3;
} else if (UPGRADE_CONFIG.colors.greenBubbles.currentLevel > 0) {
color = 0x00FF80; // Green
multiplier = 1.2;
} else {
// Blue bubbles
color = 0x80C0FF; // Blue
multiplier = 1.1;
}
self.colorTint = color;
self.colorMultiplier = multiplier;
// Apply tint to the sprite
sprite.tint = color; // Try direct assignment
// If that doesn't work, try:
// sprite.color = color;
}
};
self.activate = function (x, y, size, isPlayerBlown) {
// Existing reset code
self.x = x;
self.y = y;
self.size = size;
self.lifetime = 0;
self.hasSplit = false;
self.splitHeight = null;
self.justSplit = false;
self.autoPopDisplayed = false;
self.lastPopTime = 0;
self.verticalVelocity = 0;
self.driftX = (Math.random() * 20 - 10) / 60;
self.floatSpeed = 50 * (120 / size * (0.9 + Math.random() * 0.2)) / 60;
self.initLifetime(); // Always get fresh lifetime
self.visible = true;
// Reset color properties
self.colorTint = 0xFFFFFF; // Reset to default white
self.colorMultiplier = 1.0; // Reset multiplier
self.colorPhase = Math.random() * Math.PI * 2; // Fresh random phase
sprite.tint = 0xFFFFFF; // Reset the sprite tint directly
// Check if color upgrades are active and apply colors
self.applyColorUpgrade();
// Add some debug logs
};
self.applyColorUpgrade = function () {
// Only apply if any color upgrade is active
if (UPGRADE_CONFIG.colors.blueBubbles.currentLevel > 0) {
var color = 0xFFFFFF;
var multiplier = 1.0;
// Get active color setting
var activeColorKey = getActiveColorKey();
// Apply the selected color
if (activeColorKey === "prismaticBubbles") {
// Prismatic - constantly shifting color with variable multiplier
self.colorPhase += 0.02;
var colorValue = Math.sin(self.colorPhase) * 0.5 + 0.5; // 0 to 1
multiplier = 0.8 + colorValue * 1.2; // 0.8x to 2.0x
// Generate rainbow color
var r = Math.sin(self.colorPhase) * 127 + 128;
var g = Math.sin(self.colorPhase + 2) * 127 + 128;
var b = Math.sin(self.colorPhase + 4) * 127 + 128;
color = (Math.floor(r) << 16) + (Math.floor(g) << 8) + Math.floor(b);
} else if (activeColorKey === "rainbowBubbles") {
// Rainbow - randomly changes color
var rainbowColors = [0xFF0000,
// Red
0xFFA500,
// Orange
0xFFFF00,
// Yellow
0x00FF00,
// Green
0x0000FF,
// Blue
0xFF00FF // Purple
];
var multipliers = [1.5, 1.4, 1.3, 1.2, 1.1, 1.6];
var index = Math.floor(Math.random() * rainbowColors.length);
color = rainbowColors[index];
multiplier = multipliers[index];
} else {
// Check all single colors in a more maintainable way
var colorKeys = ["silverBubbles", "crimsonBubbles", "goldBubbles", "tealBubbles", "pinkBubbles", "orangeBubbles", "greenBubbles", "purpleBubbles", "blueBubbles"];
// Find the active color or highest unlocked
for (var i = 0; i < colorKeys.length; i++) {
var key = colorKeys[i];
if (activeColorKey === key || activeColorKey === "auto" && UPGRADE_CONFIG.colors[key].currentLevel > 0) {
color = UPGRADE_CONFIG.colors[key].color;
multiplier = UPGRADE_CONFIG.colors[key].multiplier;
break;
}
}
}
// Apply the color and multiplier
sprite.tint = color;
self.colorTint = color;
self.colorMultiplier = multiplier;
}
};
self.deactivate = function () {
self.visible = false;
var index = game.activeBubbles.indexOf(self);
if (index > -1) {
game.activeBubbles.splice(index, 1);
}
// Don't award points here - let the calling function handle it
};
self.initLifetime = function () {
self.maxLifetime = Math.floor(Math.random() * 960 + 1440);
self.maxLifetime *= Math.min(1, self.size / 100);
};
self.initLifetime();
// Subtle size-based variance plus small random factor
var speedMultiplier = 120 / self.size * (0.9 + Math.random() * 0.2); // Just 10% variance
self.floatSpeed = 50 * speedMultiplier / 60;
self.driftX = (Math.random() * 20 - 10) / 60; // Normal drift variance
self.verticalVelocity = 0;
self.down = function (e) {
var currentTime = Date.now();
if (currentTime - self.lastPopTime < 100) {
return true;
}
self.lastPopTime = currentTime;
var points = self.getBP();
game.addBP(points, self.x, self.y, false);
if (self.size > 60 && !self.justSplit) {
var splitCount = 2 + UPGRADE_CONFIG.machine.bubbleDurability.currentLevel;
var newSize = Math.max(self.MIN_SPLIT_SIZE, self.size * 0.6);
for (var i = 0; i < splitCount; i++) {
var angle = i / splitCount * Math.PI * 2;
// Reduce the direction multiplier to 0.5 to make movement more gentle
spawnBubble(self.x, self.y, newSize, Math.cos(angle) * 0.5, false);
}
}
// Check if this bubble is part of twin pair
var twinPair = game.twinBubbles.find(function (pair) {
return pair.bubble1 === self && !pair.popped || pair.bubble2 === self && !pair.popped;
});
if (twinPair) {
// Mark as popped
twinPair.popped = true;
twinPair.timestamp = LK.ticks;
// Find the other bubble in the pair
var otherBubble = twinPair.bubble1 === self ? twinPair.bubble2 : twinPair.bubble1;
// Add bonus for twin pop
var twinLevel = UPGRADE_CONFIG.player.twinBubbles.currentLevel;
var bonusMultiplier = 0.5 + 0.25 * twinLevel; // 50/75/100% bonus
// Pop the other bubble automatically and add bonus points
if (otherBubble.visible) {
var bonusPoints = Math.floor(otherBubble.getBP() * bonusMultiplier);
game.addBP(bonusPoints, otherBubble.x, otherBubble.y, false);
otherBubble.deactivate();
}
}
self.deactivate();
return true;
};
self.getBP = function () {
var baseValue = Math.max(1, Math.floor(Math.pow(self.size, 1.4) * 0.02 * 100));
// Apply Bubble Refinement upgrade
var refinementLevel = UPGRADE_CONFIG.player.bubbleRefinement.currentLevel;
if (refinementLevel > 0) {
baseValue *= 1 + 0.25 * refinementLevel; // +25% per level
}
if (self.fromClam && UPGRADE_CONFIG.machine.bubbleQuality.currentLevel > 0) {
var qualityLevel = UPGRADE_CONFIG.machine.bubbleQuality.currentLevel;
baseValue *= 1 + 0.4 * qualityLevel; // +40% per level
}
// Apply color multiplier
baseValue = Math.floor(baseValue * self.colorMultiplier);
// Apply treasure zone bonus if applicable
baseValue = Math.floor(baseValue * getTreasureBonusMultiplier(self.x, self.y));
return baseValue;
};
self.update = function () {
if (UPGRADE_CONFIG.colors.prismaticBubbles.currentLevel > 0) {
self.colorPhase += 0.02;
var r = Math.sin(self.colorPhase) * 127 + 128;
var g = Math.sin(self.colorPhase + 2) * 127 + 128;
var b = Math.sin(self.colorPhase + 4) * 127 + 128;
var color = (Math.floor(r) << 16) + (Math.floor(g) << 8) + Math.floor(b);
// Update the color value
sprite.tint = color;
self.colorTint = color;
// Make the multiplier oscillate for prismatic
var colorValue = Math.sin(self.colorPhase) * 0.5 + 0.5;
self.colorMultiplier = 0.8 + colorValue * 1.2; // 0.8x to 2.0x
}
// Only increment lifetime if not being actively blown
if (game.growingBubble !== self) {
self.lifetime++;
if (self.lifetime % 60 === 0) {
self.driftX += (Math.random() - 0.5) * 0.8;
}
}
self.x += self.driftX * 1.2;
if (self.lifetime > self.maxLifetime) {
if (self.size > 60 && !self.hasSplit) {
self.hasSplit = true;
var newSize = Math.max(self.MIN_SPLIT_SIZE, self.size * 0.6);
for (var i = 0; i < 2; i++) {
var split = spawnBubble(self.x, self.y, newSize, i === 0 ? -1 : 1, true);
if (split) {
split.maxLifetime *= 0.7;
}
}
self.deactivate();
return;
}
self.autoPop();
return;
}
if (self.y < -self.size) {
// Just deactivate, no points
self.visible = false;
var index = game.activeBubbles.indexOf(self);
if (index > -1) {
game.activeBubbles.splice(index, 1);
}
return;
}
self.justSplit = false;
if (self.verticalVelocity < self.floatSpeed) {
self.verticalVelocity += 0.08;
}
self.y -= self.verticalVelocity;
if (Math.abs(self.driftX) > (Math.random() * 20 - 10) / 60) {
self.driftX *= 0.98;
}
self.x += self.driftX;
if (self.x < self.size) {
self.x = self.size;
self.driftX = Math.abs(self.driftX);
} else if (self.x > game.width - self.size) {
self.x = game.width - self.size;
self.driftX = -Math.abs(self.driftX);
}
var scale = self.size / sprite.width;
sprite.scaleX = scale;
sprite.scaleY = scale;
};
self.autoPop = function () {
// Only award points if bubble is on screen
if (!self.autoPopDisplayed && self.y > -self.size) {
var points = Math.floor(self.getBP() * 0.5);
game.addBP(points, self.x, self.y, true);
self.autoPopDisplayed = true;
}
self.deactivate();
return;
};
self.updateColor = function () {
// Check if color upgrades are active
if (UPGRADE_CONFIG.colors.blueBubbles.currentLevel > 0) {
var color = 0xFFFFFF;
var multiplier = 1.0;
if (UPGRADE_CONFIG.colors.prismaticBubbles.currentLevel > 0) {
// Prismatic - constantly shifting color with variable multiplier
self.colorPhase += 0.02;
var colorValue = Math.sin(self.colorPhase) * 0.5 + 0.5; // 0 to 1
multiplier = 0.8 + colorValue * 1.2; // 0.8x to 2.0x
// Generate rainbow color
var r = Math.sin(self.colorPhase) * 127 + 128;
var g = Math.sin(self.colorPhase + 2) * 127 + 128;
var b = Math.sin(self.colorPhase + 4) * 127 + 128;
color = (Math.floor(r) << 16) + (Math.floor(g) << 8) + Math.floor(b);
} else if (UPGRADE_CONFIG.colors.rainbowBubbles.currentLevel > 0) {
// Rainbow - randomly changes color on activation
var colorChoice = Math.floor(Math.random() * 6);
switch (colorChoice) {
case 0:
color = 0xFF0000;
multiplier = 1.5;
break;
// Red
case 1:
color = 0xFFAA00;
multiplier = 1.4;
break;
// Orange
case 2:
color = 0xFFFF00;
multiplier = 1.3;
break;
// Yellow
case 3:
color = 0x00FF00;
multiplier = 1.2;
break;
// Green
case 4:
color = 0x0000FF;
multiplier = 1.1;
break;
// Blue
case 5:
color = 0xFF00FF;
multiplier = 1.6;
break;
// Purple
}
} else if (UPGRADE_CONFIG.colors.pinkBubbles.currentLevel > 0) {
color = 0xFF80C0; // Pink
multiplier = 1.3;
} else if (UPGRADE_CONFIG.colors.greenBubbles.currentLevel > 0) {
color = 0x00FF80; // Green
multiplier = 1.2;
} else {
// Blue bubbles
color = 0x80C0FF; // Blue
multiplier = 1.1;
}
self.colorTint = color;
self.colorMultiplier = multiplier;
// Apply tint to the sprite
sprite.tint = self.colorTint;
}
};
return self;
});
var Fish = Container.expand(function () {
var self = Container.call(this);
var fishTypes = ['redfish', 'bluefish', 'yellowfish'];
var fishType = fishTypes[Math.floor(Math.random() * fishTypes.length)];
// Create fish sprite
var sprite = self.attachAsset(fishType, {
anchorX: 0.5,
anchorY: 0.5
});
// Initialize position and movement
self.fromLeft = Math.random() < 0.5;
self.x = self.fromLeft ? -100 : game.width + 100;
self.y = Math.random() * (game.height * 0.7) + game.height * 0.1;
sprite.scaleX = self.fromLeft ? 1 : -1;
self.speed = 8; // Changed from 4
self.update = function () {
self.x += self.fromLeft ? self.speed : -self.speed;
// Add bubble collision check
game.activeBubbles.forEach(function (bubble) {
if (bubble.visible) {
var dx = self.x - bubble.x;
var dy = self.y - bubble.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= bubble.size / 2 + 50) {
// Increased from 30 to 50
// 30px collision radius
var points = bubble.getBP();
game.addBP(points, bubble.x, bubble.y, false); // false = manual pop points
bubble.deactivate();
}
}
});
// Remove when off screen
if (self.fromLeft && self.x > game.width + 100 || !self.fromLeft && self.x < -100) {
self.destroy();
}
};
return self;
});
// Pufferfish mask that follows face
var pufferMask = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('pufferfish', {
anchorX: 0.5,
anchorY: 0.5
});
var targetX = 0;
var targetY = 0;
var smoothingFactor = 0.12;
var prevX = null;
var prevY = null;
var targetRotation = 0;
var rotationSmoothingFactor = 0.1;
var targetTilt = 0;
var tiltSmoothingFactor = 0.11; // Reduced from 0.08 for smoother movement
var tiltScaleFactor = 0.09; // Reduced from 0.15 for less tilt
var scaleHistory = new Array(5).fill(0); // Keep last 5 scale values
var scaleIndex = 0;
var baseScale = 1;
var minScale = 0.1;
var maxScale = 3;
self.update = function () {
// Adjust scale based on face size
if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) {
var eyeDistance = Math.abs(facekit.rightEye.x - facekit.leftEye.x);
var newScale = eyeDistance / 500;
// Update rolling average
scaleHistory[scaleIndex] = newScale;
scaleIndex = (scaleIndex + 1) % scaleHistory.length;
// Calculate average scale
var avgScale = scaleHistory.reduce(function (a, b) {
return a + b;
}, 0) / scaleHistory.length;
// More gentle smoothing
sprite.scaleX = sprite.scaleX * 0.85 + avgScale * 0.15;
sprite.scaleY = sprite.scaleY * 0.85 + avgScale * 0.15;
}
// Follow nose position for main face tracking
if (facekit.noseTip) {
targetX = facekit.noseTip.x;
targetY = facekit.noseTip.y;
// Initialize previous positions if not set
if (prevX === null) {
prevX = targetX;
prevY = targetY;
}
// Weighted average between previous and target position
var newX = prevX * (1 - smoothingFactor) + targetX * smoothingFactor;
var newY = prevY * (1 - smoothingFactor) + targetY * smoothingFactor;
self.x = newX;
self.y = newY;
// Update previous positions
prevX = newX;
prevY = newY;
}
if (facekit.leftEye && facekit.rightEye) {
targetTilt = calculateFaceTilt() * tiltScaleFactor; // Scale down the tilt
// Reduce max rotation to ±15 degrees
targetTilt = Math.max(-15, Math.min(15, targetTilt));
self.rotation += (targetTilt - self.rotation) * tiltSmoothingFactor;
}
};
function calculateFaceTilt() {
if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) {
// Calculate midpoint between eyes
var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2;
var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2;
// Calculate angle between eye midpoint and mouth, negated to fix direction
var dx = facekit.mouthCenter.x - eyeMidX;
var dy = facekit.mouthCenter.y - eyeMidY;
var angle = -(Math.atan2(dx, dy) * (180 / Math.PI));
// Reduced max angle to ±15 degrees and lowered multiplier
return Math.max(-15, Math.min(15, angle * 0.15));
}
return 0; // Default to straight when face points aren't available
}
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87CEEB // Light blue background to represent the sky
});
/****
* Game Code
****/
var UPGRADE_CONFIG = {
gameSettings: {
activeColor: "auto" // Default to automatic progression (highest unlocked)
},
player: {
lungCapacity: {
name: "Lung Capacity",
baseCost: 100,
costScale: 3.0,
maxLevel: 10,
currentLevel: 0
},
quickBreath: {
name: "Quick Breath",
baseCost: 100,
costScale: 3.0,
maxLevel: 10,
currentLevel: 0
},
autoPop: {
name: "Fish Friends",
baseCost: 400,
costScale: 3,
maxLevel: 6,
currentLevel: 0
},
bubbleRefinement: {
name: "Bubble Refinement",
baseCost: 2500,
costScale: 2.5,
maxLevel: 5,
currentLevel: 0
},
twinBubbles: {
name: "Twin Bubbles",
baseCost: 5000,
costScale: 2.3,
maxLevel: 3,
currentLevel: 0
},
sizeVariance: {
name: "Size Variance",
baseCost: 4000,
costScale: 1.8,
maxLevel: 5,
currentLevel: 0
},
bubbleBreath: {
name: "Bubble Breath",
baseCost: 12000,
costScale: 2.2,
maxLevel: 4,
currentLevel: 0
}
},
machines: {
basicClam: {
name: "Basic Clam",
baseCost: 300,
costScale: 2,
amount: 0,
production: 3,
bubbleSize: 80
},
advancedClam: {
name: "Advanced Clam",
baseCost: 3000,
costScale: 3.0,
amount: 0,
production: 2,
bubbleSize: 100,
unlockCost: 3000
},
premiumClam: {
name: "Premium Clam",
baseCost: 20000,
costScale: 3,
amount: 0,
production: 1,
bubbleSize: 150,
unlockCost: 20000
}
},
machine: {
bubbleDurability: {
name: "Bubble Splitting",
baseCost: 500,
costScale: 5,
maxLevel: 3,
currentLevel: 0
},
autoBubbleSpeed: {
name: "Clam Speed",
baseCost: 500,
costScale: 3.5,
maxLevel: 8,
currentLevel: 0
},
bubbleQuality: {
name: "Bubble Quality",
baseCost: 3000,
costScale: 2.2,
maxLevel: 5,
currentLevel: 0
}
},
colors: {
blueBubbles: {
name: "Blue Bubbles",
baseCost: 1000,
costScale: 1.0,
maxLevel: 1,
currentLevel: 0,
multiplier: 1.1,
color: 0x80C0FF
},
purpleBubbles: {
name: "Purple Bubbles",
baseCost: 2500,
costScale: 1.0,
maxLevel: 1,
currentLevel: 0,
requires: "blueBubbles",
multiplier: 1.15,
color: 0x8A2BE2
},
greenBubbles: {
name: "Green Bubbles",
baseCost: 5000,
costScale: 1.0,
maxLevel: 1,
currentLevel: 0,
requires: "purpleBubbles",
multiplier: 1.2,
color: 0x00FF80
},
orangeBubbles: {
name: "Orange Bubbles",
baseCost: 8000,
costScale: 1.0,
maxLevel: 1,
currentLevel: 0,
requires: "greenBubbles",
multiplier: 1.25,
color: 0xFFA500
},
pinkBubbles: {
name: "Pink Bubbles",
baseCost: 12000,
costScale: 1.0,
maxLevel: 1,
currentLevel: 0,
requires: "orangeBubbles",
multiplier: 1.3,
color: 0xFF80C0
},
tealBubbles: {
name: "Teal Bubbles",
baseCost: 18000,
costScale: 1.0,
maxLevel: 1,
currentLevel: 0,
requires: "pinkBubbles",
multiplier: 1.35,
color: 0x00CED1
},
goldBubbles: {
name: "Gold Bubbles",
baseCost: 25000,
costScale: 1.0,
maxLevel: 1,
currentLevel: 0,
requires: "tealBubbles",
multiplier: 1.4,
color: 0xFFD700
},
crimsonBubbles: {
name: "Crimson Bubbles",
baseCost: 35000,
costScale: 1.0,
maxLevel: 1,
currentLevel: 0,
requires: "goldBubbles",
multiplier: 1.45,
color: 0xDC143C
},
silverBubbles: {
name: "Silver Bubbles",
baseCost: 45000,
costScale: 1.0,
maxLevel: 1,
currentLevel: 0,
requires: "crimsonBubbles",
multiplier: 1.5,
color: 0xC0C0C0
},
rainbowBubbles: {
name: "Rainbow Bubbles",
baseCost: 75000,
costScale: 1.0,
maxLevel: 1,
currentLevel: 0,
requires: "silverBubbles",
multiplier: 1.6
},
prismaticBubbles: {
name: "Prismatic Bubbles",
baseCost: 150000,
costScale: 1.0,
maxLevel: 1,
currentLevel: 0,
requires: "rainbowBubbles",
multiplier: 1.8
}
},
decorations: {
bubbleCoral: {
name: "Bubble Coral",
baseCost: 10000,
costScale: 2.0,
amount: 0,
maxAmount: 5
},
sunkenTreasures: {
name: "Sunken Treasures",
baseCost: 25000,
costScale: 2.5,
amount: 0,
maxAmount: 3
}
}
};
// Initialize upgrade registry for UI elements
game.upgradeRegistry = {};
var background = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
x: game.width / 2,
y: game.height / 2
});
game.addChild(background);
// Add coral container
var coralContainer = new Container();
game.addChild(coralContainer);
game.setChildIndex(coralContainer, 1);
// Add treasure container
var treasureContainer = new Container();
game.addChild(treasureContainer);
game.setChildIndex(treasureContainer, 2);
// Add clam container
var clamContainer = new Container();
game.addChild(clamContainer);
// Add player mask
var playerMask = new pufferMask();
game.addChild(playerMask);
var UPGRADE_EFFECTS = {
lungCapacity: {
baseValue: 160,
incrementPercent: 25
},
quickBreath: {
baseValue: 1.6,
incrementPercent: 25
},
autoBubbleSpeed: {
decrementPercent: 10
},
bubbleDurability: {
extraSplits: 1
},
autoPop: {
timeReduction: 0.8
}
};
// Define column assignments for each tab
var tabColumns = {
bubbles: {
left: [['player', 'lungCapacity'], ['player', 'quickBreath'], ['player', 'bubbleRefinement'], ['player', 'twinBubbles']],
right: [['player', 'autoPop'], ['player', 'sizeVariance'], ['player', 'bubbleBreath']]
},
clams: {
left: [['machines', 'basicClam'], ['machines', 'advancedClam'], ['machines', 'premiumClam']],
right: [['machine', 'bubbleDurability'], ['machine', 'autoBubbleSpeed'], ['machine', 'bubbleQuality']]
},
colors: {
left: [['colors', 'blueBubbles'], ['colors', 'purpleBubbles'], ['colors', 'greenBubbles'], ['colors', 'orangeBubbles'], ['colors', 'pinkBubbles']],
right: [['colors', 'tealBubbles'], ['colors', 'goldBubbles'], ['colors', 'crimsonBubbles'], ['colors', 'silverBubbles'], ['colors', 'rainbowBubbles'], ['colors', 'prismaticBubbles']]
},
decorations: {
left: [['decorations', 'bubbleCoral']],
right: [['decorations', 'sunkenTreasures']]
}
};
function getUpgradeCost(upgrade) {
if (upgrade.amount !== undefined) {
// For clams and decorations
return Math.floor(upgrade.baseCost * Math.pow(upgrade.costScale, upgrade.amount));
} else {
// For regular upgrades
return Math.floor(upgrade.baseCost * Math.pow(upgrade.costScale, upgrade.currentLevel));
}
}
function getActiveColorKey() {
if (UPGRADE_CONFIG.gameSettings.activeColor === "auto") {
// Find highest unlocked color in order of value
var colorKeys = ["prismaticBubbles", "rainbowBubbles", "silverBubbles", "crimsonBubbles", "goldBubbles", "tealBubbles", "pinkBubbles", "orangeBubbles", "greenBubbles", "purpleBubbles", "blueBubbles"];
for (var i = 0; i < colorKeys.length; i++) {
if (UPGRADE_CONFIG.colors[colorKeys[i]].currentLevel > 0) {
return colorKeys[i];
}
}
}
return UPGRADE_CONFIG.gameSettings.activeColor;
}
function setActiveColor(colorKey) {
if (colorKey === "auto" || UPGRADE_CONFIG.colors[colorKey].currentLevel > 0) {
UPGRADE_CONFIG.gameSettings.activeColor = colorKey;
// Update visual indicator in UI
updateColorSelectionUI();
} else {
game.showError("Unlock this color first!");
}
}
function getTreasureBonusMultiplier(x, y) {
if (!game.treasureZones || game.treasureZones.length === 0) {
return 1.0; // No bonus if no treasures
}
// Start with no bonus
var totalBonus = 0;
// Check each treasure zone
game.treasureZones.forEach(function (zone) {
var dx = x - zone.x;
var dy = y - zone.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// If bubble is in zone, add 50% bonus
if (distance <= zone.radius) {
totalBonus += 0.5; // +50% per overlapping zone
}
});
// Return multiplier (1.0 = no bonus, 1.5 = one zone, 2.0 = two zones, etc.)
return 1.0 + totalBonus;
}
function formatBP(value) {
var units = ['', 'K', 'M', 'B', 'T'];
var unitIndex = 0;
while (value >= 1000 && unitIndex < units.length - 1) {
value /= 1000;
unitIndex++;
}
return Math.floor(value * 10) / 10 + units[unitIndex];
}
game.showError = function (message) {
var errorText = new Text2(message, {
size: 120,
fill: 0xFF0000,
stroke: 0x000000,
strokeThickness: 5,
font: "Impact"
});
errorText.anchor = {
x: 0.5,
y: 0.5
};
errorText.x = game.width / 2;
errorText.y = game.height / 2;
game.addChild(errorText);
tween(errorText, {
alpha: 0,
y: errorText.y - 50
}, {
duration: 1200,
onFinish: function onFinish() {
errorText.destroy();
}
});
};
game.showMessage = function (message) {
var messageText = new Text2(message, {
size: 80,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 3,
font: "Impact"
});
messageText.anchor = {
x: 0.5,
y: 0.5
};
messageText.x = game.width / 2;
messageText.y = game.height / 2;
game.addChild(messageText);
tween(messageText, {
alpha: 0,
y: messageText.y - 30
}, {
duration: 1000,
onFinish: function onFinish() {
messageText.destroy();
}
});
};
var currentTab = 'bubbles'; // Default tab
var menuTabs = ['bubbles', 'clams', 'colors', 'decorations'];
var tabButtons = {}; // Will hold references to tab buttons
// Create menu container at the right position
var menuContainer = new Container();
menuContainer.x = game.width / 2;
menuContainer.y = game.height; // Position at bottom
var menuPanel = LK.getAsset('upgradetab', {
anchorX: 0.5,
anchorY: 0,
y: -570,
alpha: 0.9,
scaleX: 2048 / 200,
scaleY: game.height * 0.4 / 100.3
});
var menuTab = LK.getAsset('upgradetab', {
anchorX: 0.5,
anchorY: 1,
y: 0,
scaleX: 3,
scaleY: 0.8,
alpha: 0.9
});
// Add panel first (so it's behind tab)
menuContainer.addChild(menuPanel);
menuContainer.addChild(menuTab);
// Menu text
var menuText = new Text2("Upgrades", {
size: 90,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 3,
font: "Impact"
});
menuText.anchor = {
x: 0.5,
y: 0.5
};
menuText.x = 0; // Relative to container
menuText.y = -menuTab.height / 2; // Position relative to container bottom
menuContainer.addChild(menuText);
// Add to game
game.addChild(menuContainer);
// Create text container
var menuTextContainer = new Container();
menuContainer.addChild(menuTextContainer);
// Create tab containers
var tabContainers = {};
menuTabs.forEach(function (tab) {
var contentContainer = new Container();
contentContainer.visible = tab === currentTab;
tabContainers[tab] = contentContainer;
menuTextContainer.addChild(contentContainer);
});
// Create tabs container (only visible when menu is open)
var tabsContainer = new Container();
tabsContainer.x = menuPanel.width * menuPanel.scaleX / 2 - 850; // Center horizontally
tabsContainer.y = 0;
menuContainer.addChild(tabsContainer);
// Define tab dimensions
var tabWidth = menuPanel.width * 0.8 * menuPanel.scaleX / menuTabs.length;
var tabHeight = 120;
// Create tabs
menuTabs.forEach(function (tab, index) {
// Create tab button
var tabButton = LK.getAsset('upgradetab', {
anchorX: 0.5,
anchorY: 0,
x: -menuPanel.width * menuPanel.scaleX / 2 + (index + 0.5) * tabWidth,
y: 0,
scaleX: tabWidth / 200,
scaleY: tabHeight / 299,
alpha: tab === currentTab ? 1.0 : 0.7
});
// Add hit detection
tabButton.down = function () {
if (tab !== currentTab) {
// Update tab appearance
Object.keys(tabButtons).forEach(function (t) {
if (tabButtons[t]) {
tabButtons[t].alpha = t === tab ? 1.0 : 0.7;
}
});
// Remove old indicator from display
if (game.activeTabIndicator) {
tabsContainer.removeChild(game.activeTabIndicator);
}
// Create new indicator for current tab
var newIndicator = LK.getAsset('blower', {
width: tabWidth,
height: 10,
color: 0xFFFF00,
alpha: 1.0
});
// Position at the bottom of this tab
newIndicator.x = tabButton.x - tabWidth / 2;
newIndicator.y = tabHeight - 5;
// Add to container and store reference
tabsContainer.addChild(newIndicator);
game.activeTabIndicator = newIndicator;
// Hide current tab content, show new tab content
if (tabContainers[currentTab]) {
tabContainers[currentTab].visible = false;
}
if (tabContainers[tab]) {
tabContainers[tab].visible = true;
}
// Update current tab
currentTab = tab;
}
return true;
};
// Store reference to the button
tabButtons[tab] = tabButton;
// Add text to tab
var tabText = new Text2(tab.charAt(0).toUpperCase() + tab.slice(1), {
size: 80,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 3,
font: "Impact"
});
tabText.anchor = {
x: 0.5,
y: 0.5
};
tabText.x = tabButton.x;
tabText.y = tabHeight / 2;
// Add to container
tabsContainer.addChild(tabButton);
tabsContainer.addChild(tabText);
// Create active tab indicator
var activeTabIndicator = LK.getAsset('blower', {
width: tabWidth,
height: 10,
color: 0xFFFF00,
alpha: 1.0
});
// Initialize game.tabIndicators as an array and store activeTabIndicator in it
game.tabIndicators = [];
game.tabIndicators.push(activeTabIndicator);
// Position at the bottom of the current tab
activeTabIndicator.x = tabButtons[currentTab].x - tabWidth / 2;
activeTabIndicator.y = tabHeight - 5;
// Add to tab container
tabsContainer.addChild(activeTabIndicator);
});
// Move the entire text container to align with panel
menuTextContainer.y = 0;
menuTextContainer.x = 0;
// Initialize menu state
var menuOpen = false;
var menuTargetY = game.height;
// Create BP display text
var bpText = new Text2("0 BP", {
size: 120,
fill: 0xFFFFFF,
stroke: 0x33caf8,
strokeThickness: 4,
font: "Impact",
fontWeight: "bold"
});
bpText.anchor.set(1, 0);
bpText.x = game.width - 20;
bpText.y = 20;
game.addChild(bpText);
// Initialize BP tracking
game.bp = 0;
game.combo = 0;
game.lastPopTime = 0;
game.COMBO_WINDOW = 60; // 1 second in frames
game.addBP = function (points, x, y, isAutoPop) {
var currentTime = LK.ticks;
// Only update combo if it's not an auto-pop
if (!isAutoPop) {
if (currentTime - game.lastPopTime < game.COMBO_WINDOW) {
game.combo++;
points *= 1 + game.combo * 0.1; // 10% bonus per combo
} else {
game.combo = 0;
}
game.lastPopTime = currentTime;
}
// Ensure points is at least 1
points = Math.max(1, Math.floor(points));
game.bp += points;
bpText.setText(formatBP(game.bp) + " BP");
// Set size and color based on point value
var textSize = 96;
var textColor = 0xFFFF00; // Default yellow
if (points > 100) {
textSize = 120;
textColor = 0xFF0000; // Red for >100
} else if (points > 50) {
textSize = 108;
textColor = 0xFFA500; // Orange for 51-100
}
// Always show point text
var pointText = new Text2("+" + points, {
size: textSize,
fill: textColor,
font: "Impact",
fontWeight: 'bold'
});
pointText.anchorX = 0.5;
pointText.anchorY = 0.5;
pointText.x = x;
pointText.y = y;
game.addChild(pointText);
tween(pointText, {
y: pointText.y - 100,
alpha: 0
}, {
duration: 1200,
onFinish: function onFinish() {
pointText.destroy();
}
});
// Only show combo text if it's a manual pop and we have a combo
if (!isAutoPop && game.combo > 0) {
var comboText = new Text2("x" + (game.combo + 1), {
size: 96,
fill: 0xFFA500,
stroke: 0x000000,
strokeThickness: 4,
fontWeight: 'bold'
});
comboText.anchorX = 0.5;
comboText.anchorY = 0;
comboText.x = game.width / 2;
comboText.y = 20;
game.addChild(comboText);
tween(comboText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
comboText.destroy();
}
});
}
};
// Function to create upgrade text (MODIFIED with Registry)
function createUpgradeText(category, key, index, isLeftColumn, tab) {
var upgrade = UPGRADE_CONFIG[category][key];
if (!upgrade) {
return;
}
var xOffset = isLeftColumn ? -750 : 200;
var yPos = startY + index * upgradeSpacing + 120;
// Create hit container
var hitContainer = new Container();
var hitArea = LK.getAsset('blower', {
width: 600,
height: 140,
color: 0xFFFFFF,
alpha: 0.0
});
hitContainer.addChild(hitArea);
hitContainer.x = xOffset;
hitContainer.y = yPos;
hitArea.x = 0;
hitArea.y = -40;
// Create name text
var nameText = new Text2(upgrade.name, {
size: 96,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 2,
font: "Impact"
});
nameText.x = xOffset;
nameText.y = yPos;
// Create cost text
var cost = getUpgradeCost(upgrade);
var costText = new Text2(cost + " BP", {
size: 96,
fill: 0xFFFF00,
stroke: 0x000000,
strokeThickness: 2,
font: "Impact"
});
costText.x = xOffset;
costText.y = yPos + 100;
// Register the UI elements
if (!game.upgradeRegistry[category]) {
game.upgradeRegistry[category] = {};
}
game.upgradeRegistry[category][key] = {
nameText: nameText,
costText: costText,
hitContainer: hitContainer,
tab: tab
};
// Add click handler
hitContainer.down = function () {
var cost = getUpgradeCost(upgrade);
// Special handling for colors that are already purchased
if (category === 'colors' && upgrade.currentLevel > 0) {
// Toggle this color as active
if (UPGRADE_CONFIG.gameSettings.activeColor === key) {
// If already active, switch to auto
UPGRADE_CONFIG.gameSettings.activeColor = "auto";
game.showMessage("Auto color mode");
} else {
// Otherwise activate this color
UPGRADE_CONFIG.gameSettings.activeColor = key;
game.showMessage(upgrade.name + " activated");
}
// Refresh the tab to update the text displays
refreshUpgradeTab('colors');
return true;
}
// Regular upgrade purchase logic
if (category === 'colors' && upgrade.requires) {
var required = UPGRADE_CONFIG.colors[upgrade.requires];
if (required && required.currentLevel === 0) {
game.showError("Unlock " + required.name + " first!");
return true;
}
}
if (game.bp >= cost) {
if (category === 'machines') {
var totalClams = UPGRADE_CONFIG.machines.basicClam.amount + UPGRADE_CONFIG.machines.advancedClam.amount + UPGRADE_CONFIG.machines.premiumClam.amount;
// Check if already at max clams
if (totalClams >= 4) {
if (key === 'basicClam') {
updateCostText(category, key, "SOLD OUT", 0x888888);
return true; // Add return here
}
// If trying to upgrade (e.g. basic to advanced)
if (key === 'advancedClam') {
if (UPGRADE_CONFIG.machines.basicClam.amount > 0) {
// Replace one basic clam
UPGRADE_CONFIG.machines.basicClam.amount--;
upgrade.amount++;
game.bp -= cost;
// Update BP text
bpText.setText(formatBP(game.bp) + " BP");
// Force a complete refresh of the clams tab
refreshUpgradeTab('clams');
// Update visual representation of clams
updateClamVisuals();
return true;
} else {
game.showError("No basic clams to upgrade!");
return true;
}
} else if (key === 'premiumClam' && (UPGRADE_CONFIG.machines.basicClam.amount > 0 || UPGRADE_CONFIG.machines.advancedClam.amount > 0)) {
// Replace lower tier clam with premium
if (UPGRADE_CONFIG.machines.basicClam.amount > 0) {
UPGRADE_CONFIG.machines.basicClam.amount--;
} else {
UPGRADE_CONFIG.machines.advancedClam.amount--;
}
upgrade.amount++;
game.bp -= cost;
bpText.setText(formatBP(game.bp) + " BP");
updateClamVisuals();
return true;
} else {
game.showError("Maximum of 4 clams reached!");
return true;
}
}
}
if (upgrade.amount !== undefined) {
// For clams and decorations with amount
if (upgrade.amount < (upgrade.maxAmount || 999)) {
upgrade.amount++;
game.bp -= cost;
bpText.setText(formatBP(game.bp) + " BP");
updateCostText(category, key, getUpgradeCost(upgrade) + " BP", 0xFFFF00);
// Update visuals
if (category === 'machines') {
updateClamVisuals();
} else if (category === 'decorations') {
if (key === 'bubbleCoral') {
updateCoralDecorations();
} else if (key === 'sunkenTreasures') {
updateTreasureDecorations();
}
}
}
} else if (upgrade.currentLevel < upgrade.maxLevel) {
// For regular upgrades with levels
upgrade.currentLevel++;
game.bp -= cost;
bpText.setText(formatBP(game.bp) + " BP");
// If this is a color upgrade, update the UI
if (category === 'colors') {
refreshUpgradeTab('colors');
return true;
}
// Update cost text
if (upgrade.currentLevel >= upgrade.maxLevel) {
updateCostText(category, key, "SOLD OUT", 0x888888);
} else {
updateCostText(category, key, getUpgradeCost(upgrade) + " BP", 0xFFFF00);
}
// Handle specific upgrade effects
if (category === 'player') {
if (key === 'lungCapacity') {
var baseSize = UPGRADE_EFFECTS.lungCapacity.baseValue;
var increasePercent = UPGRADE_EFFECTS.lungCapacity.incrementPercent;
var multiplier = 1 + increasePercent / 100 * upgrade.currentLevel;
game.maxBubbleSize = baseSize * multiplier;
} else if (key === 'quickBreath') {
game.growthRate = UPGRADE_EFFECTS.quickBreath.baseValue * (1 + UPGRADE_EFFECTS.quickBreath.incrementPercent / 100 * upgrade.currentLevel);
}
}
}
} else {
game.showError("Not enough BP!");
}
return true;
};
// Add elements to the appropriate tab container
tabContainers[tab].addChild(hitContainer);
tabContainers[tab].addChild(nameText);
tabContainers[tab].addChild(costText);
}
// Helper function to update cost text (NEW)
function updateCostText(category, key, text, color) {
if (game.upgradeRegistry[category] && game.upgradeRegistry[category][key] && game.upgradeRegistry[category][key].costText) {
var costText = game.upgradeRegistry[category][key].costText;
costText.setText(text);
costText.fill = color;
}
}
// Function to refresh upgrade tab (MODIFIED)
function refreshUpgradeTab(tabName) {
// Clear the tab container
while (tabContainers[tabName].children.length > 0) {
tabContainers[tabName].children[0].destroy();
}
// Clear registry entries for this tab
Object.keys(game.upgradeRegistry).forEach(function (category) {
Object.keys(game.upgradeRegistry[category]).forEach(function (key) {
if (game.upgradeRegistry[category][key].tab === tabName) {
delete game.upgradeRegistry[category][key];
}
});
});
// Recreate all upgrades for the tab
if (tabColumns[tabName] && tabColumns[tabName].left) {
tabColumns[tabName].left.forEach(function (upgrade, index) {
createUpgradeText(upgrade[0], upgrade[1], index, true, tabName);
});
}
if (tabColumns[tabName] && tabColumns[tabName].right) {
tabColumns[tabName].right.forEach(function (upgrade, index) {
createUpgradeText(upgrade[0], upgrade[1], index, false, tabName);
});
}
// Update all cost texts for this tab
updateCostTexts(tabName);
}
// Function to update cost texts (NEW)
function updateCostTexts(tabName) {
Object.keys(game.upgradeRegistry).forEach(function (category) {
Object.keys(game.upgradeRegistry[category]).forEach(function (key) {
var registry = game.upgradeRegistry[category][key];
// Only update elements in the current tab
if (registry.tab !== tabName) {
return;
}
var upgrade = UPGRADE_CONFIG[category][key];
var costText = registry.costText;
// Handle color upgrades
if (category === 'colors') {
var activeColorKey = getActiveColorKey();
if (upgrade.currentLevel > 0) {
if (key === activeColorKey || UPGRADE_CONFIG.gameSettings.activeColor === "auto" && key === activeColorKey) {
costText.setText("ACTIVE");
costText.fill = 0x00FF00;
} else if (upgrade.currentLevel >= upgrade.maxLevel) {
costText.setText("SOLD OUT");
costText.fill = 0x888888;
} else {
costText.setText(getUpgradeCost(upgrade) + " BP");
costText.fill = 0xFFFF00;
}
} else if (upgrade.requires) {
var required = UPGRADE_CONFIG.colors[upgrade.requires];
if (required && required.currentLevel === 0) {
costText.setText("LOCKED");
costText.fill = 0x888888;
} else {
costText.setText(getUpgradeCost(upgrade) + " BP");
costText.fill = 0xFFFF00;
}
}
}
// Handle machine upgrades
else if (category === 'machines') {
var totalClams = UPGRADE_CONFIG.machines.basicClam.amount + UPGRADE_CONFIG.machines.advancedClam.amount + UPGRADE_CONFIG.machines.premiumClam.amount;
if (totalClams >= 4 && key === 'basicClam') {
costText.setText("SOLD OUT");
costText.fill = 0x888888;
} else {
costText.setText(getUpgradeCost(upgrade) + " BP");
costText.fill = 0xFFFF00;
}
}
// Handle regular upgrades
else if (upgrade.currentLevel >= upgrade.maxLevel) {
costText.setText("SOLD OUT");
costText.fill = 0x888888;
} else {
costText.setText(getUpgradeCost(upgrade) + " BP");
costText.fill = 0xFFFF00;
}
});
});
}
// New function to update all upgrade texts
function updateAllUpgradeTexts() {
menuTabs.forEach(function (tab) {
updateCostTexts(tab);
});
}
// Replace the old updateColorSelectionUI function with this
function updateColorSelectionUI() {
updateCostTexts('colors');
}
// Variables for upgrade texts
var upgradeTexts = [];
var startY = 150;
var upgradeSpacing = 250;
var columnWidth = 1024;
// Define legacy variables for backward compatibility
var leftColumnUpgrades = [['player', 'lungCapacity'], ['player', 'quickBreath'], ['player', 'autoPop']];
var rightColumnUpgrades = [['machines', 'basicClam'], ['machines', 'advancedClam'], ['machines', 'premiumClam'], ['machine', 'bubbleDurability'], ['machine', 'autoBubbleSpeed']];
// Function to determine which tab an upgrade belongs to (for backward compatibility)
function getTabForUpgrade(category, key) {
if (category === 'player') {
return 'bubbles';
}
if (category === 'machines' || category === 'machine') {
return 'clams';
}
if (category === 'colors') {
return 'colors';
}
if (category === 'decorations') {
return 'decorations';
}
return 'bubbles'; // Default
}
// Function to switch between tabs
function switchTab(newTab) {
// Hide old tab content, show new tab content
tabContainers[currentTab].visible = false;
tabContainers[newTab].visible = true;
// Update tab button appearance
tabButtons[currentTab].alpha = 0.7;
tabButtons[newTab].alpha = 1.0;
currentTab = newTab;
}
// Function to update treasure decorations
function updateTreasureDecorations() {
// Clear existing treasures
while (treasureContainer.children.length) {
treasureContainer.children[0].destroy();
}
// Clear zone tracking
game.treasureZones = [];
var treasureCount = UPGRADE_CONFIG.decorations.sunkenTreasures.amount;
if (treasureCount <= 0) {
return;
}
// Available treasure types
var treasureTypes = ['treasure1', 'treasure2', 'treasure3'];
// Position treasures at specific spots in the bottom half of screen
var positions = [
// Left side
{
x: game.width * 0.25,
y: game.height * 0.65
},
// Right side
{
x: game.width * 0.75,
y: game.height * 0.65
},
// Middle bottom
{
x: game.width * 0.5,
y: game.height * 0.8
}];
// Place treasures at predetermined spots
for (var i = 0; i < treasureCount; i++) {
// Don't exceed available positions
if (i >= positions.length) {
break;
}
// Choose treasure type based on position
var treasureType = treasureTypes[i % treasureTypes.length];
var pos = positions[i];
// Create circular zone indicator
var zoneRadius = game.width * 0.15;
var zoneIndicator = LK.getAsset('blower', {
width: zoneRadius * 2,
height: zoneRadius * 2,
shape: 'circle',
color: 0xFFFFFF,
alpha: 0.15,
anchorX: 0.5,
anchorY: 0.5,
x: pos.x,
y: pos.y
});
// Add zone to tracking for bonus calculation
game.treasureZones.push({
id: 'treasure_' + i,
x: pos.x,
y: pos.y,
radius: zoneRadius
});
// Create treasure sprite on top of zone
var treasure = LK.getAsset(treasureType, {
anchorX: 0.5,
anchorY: 0.5,
x: pos.x,
y: pos.y,
scaleX: 1.8,
scaleY: 1.8,
alpha: 0.9
});
// Add zone first (so it's behind treasure)
treasureContainer.addChild(zoneIndicator);
treasureContainer.addChild(treasure);
}
}
// Function to update coral bubbles
function updateCoralBubbles() {
if (UPGRADE_CONFIG.decorations.bubbleCoral.amount <= 0) {
return;
}
coralContainer.children.forEach(function (coral) {
// 5% chance per second to spawn bubble (0.083% per frame at 60fps)
if (Math.random() < 0.00083 || LK.ticks - coral.lastBubbleTime > 600) {
// Force spawn after 10 seconds
coral.lastBubbleTime = LK.ticks;
// Calculate bubble size based on player's max size
var bubbleSize = game.maxBubbleSize * (0.5 + Math.random() * 0.3);
// Spawn position slightly above coral
var bubbleX = coral.x + (Math.random() * 30 - 15);
var bubbleY = coral.y - coral.height * 0.3;
var bubble = spawnBubble(bubbleX, bubbleY, bubbleSize, 0, false);
if (bubble) {
// Set moderate upward velocity
bubble.verticalVelocity = -(2 + Math.random() * 3);
bubble.driftX = Math.random() * 2 - 1;
}
}
});
}
// Function to update coral decorations
function updateCoralDecorations() {
// Clear existing corals
while (coralContainer.children.length) {
coralContainer.children[0].destroy();
}
// Position corals along bottom of screen
var coralCount = UPGRADE_CONFIG.decorations.bubbleCoral.amount;
if (coralCount <= 0) {
return;
}
// Available coral types
var coralTypes = ['coral1', 'coral2', 'coral3', 'coral4'];
// Space corals evenly along bottom
var spacing = game.width / (coralCount + 1);
for (var i = 0; i < coralCount; i++) {
// Choose random coral type
var coralType = coralTypes[Math.floor(Math.random() * coralTypes.length)];
// Create coral sprite
var coral = LK.getAsset(coralType, {
anchorX: 0.5,
anchorY: 1,
x: spacing * (i + 1),
y: game.height - 20,
// Slightly above bottom
scaleX: 1.5 + Math.random() * 0.5,
scaleY: 1.5 + Math.random() * 0.5
});
// Store last bubble spawn time
coral.lastBubbleTime = 0;
coralContainer.addChild(coral);
}
}
// Function to update clam visuals
function updateClamVisuals() {
// Define positioning variables (these were likely defined elsewhere)
var leftStart = game.width * 0.1;
var rightStart = game.width * 0.9;
var spacing = 250;
var y = game.height - 100;
// Total number of clams to display
while (clamContainer.children.length) {
clamContainer.children[0].destroy();
}
// Total number of clams to display
var totalClams = UPGRADE_CONFIG.machines.basicClam.amount + UPGRADE_CONFIG.machines.advancedClam.amount + UPGRADE_CONFIG.machines.premiumClam.amount;
// Create array to hold exactly the right number of clams
var clamTypes = new Array(totalClams);
// Fill all positions with null initially
for (var i = 0; i < clamTypes.length; i++) {
clamTypes[i] = null;
}
// Fill with premium clams first (highest priority)
var premiumCount = UPGRADE_CONFIG.machines.premiumClam.amount;
for (var i = 0; i < premiumCount; i++) {
if (i < clamTypes.length) {
clamTypes[i] = 'premiumClam';
}
}
// Fill with advanced clams next
var advancedCount = UPGRADE_CONFIG.machines.advancedClam.amount;
for (var i = 0; i < advancedCount; i++) {
if (i + premiumCount < clamTypes.length) {
clamTypes[i + premiumCount] = 'advancedClam';
}
}
// Fill remaining slots with basic clams
var basicCount = UPGRADE_CONFIG.machines.basicClam.amount;
for (var i = 0; i < basicCount; i++) {
if (i + premiumCount + advancedCount < clamTypes.length) {
clamTypes[i + premiumCount + advancedCount] = 'basicClam';
}
}
// Place clams
game.clamSpawnPoints = [];
clamTypes.forEach(function (type, i) {
if (!type) {
return;
} // Skip null entries
var isRight = i % 2 === 1;
var baseX = isRight ? rightStart : leftStart;
var direction = isRight ? -1 : 1;
var position = Math.floor(i / 2);
var x = baseX + direction * position * spacing;
var sprite = LK.getAsset(type, {
anchorX: 0.5,
anchorY: 1,
x: x,
y: y,
scaleX: isRight ? -0.5 : 0.5,
// Flip right-side clams
scaleY: 0.5
});
// Store spawn point for this clam
game.clamSpawnPoints.push({
x: x + (isRight ? -75 : 75),
// 25% from edge
y: y - 50,
// Slightly above clam
type: type,
isRight: isRight
});
clamContainer.addChild(sprite);
});
console.log("After visual update - Clam types: " + clamTypes.join(", "));
}
// Function to update clams (spawn bubbles)
function updateClams() {
if (!game.clamSpawnPoints) {
return;
}
game.clamSpawnPoints.forEach(function (spawnPoint) {
var config = UPGRADE_CONFIG.machines[spawnPoint.type];
// Calculate production time with speed upgrade
var baseTime = config.production * 150; // Convert to frames and further increase base time
var speedMultiplier = Math.pow(1 - UPGRADE_EFFECTS.autoBubbleSpeed.decrementPercent / 100, UPGRADE_CONFIG.machine.autoBubbleSpeed.currentLevel);
var adjustedTime = Math.max(1, Math.floor(baseTime * speedMultiplier));
var qualityLevel = UPGRADE_CONFIG.machine.bubbleQuality.currentLevel;
if (qualityLevel > 0) {
// -10% production rate per level
adjustedTime = Math.floor(adjustedTime * (1 + 0.1 * qualityLevel));
}
if (LK.ticks % adjustedTime === 0) {
// Find first available bubble in pool
var bubble = game.bubblePool.find(function (b) {
return !b.visible;
});
if (bubble && game.activeBubbles.length < game.MAX_BUBBLES) {
bubble.activate(spawnPoint.x, spawnPoint.y, config.bubbleSize, false);
bubble.fromClam = true;
if (UPGRADE_CONFIG.player.sizeVariance.currentLevel > 0) {
var variance = UPGRADE_CONFIG.player.sizeVariance.currentLevel;
var minIncrease = 0.1 * variance; // +10% per level to min size
var maxIncrease = 0.15 * variance; // +15% per level to max size
// Apply size variance
var sizeMultiplier = 1 - minIncrease + Math.random() * (minIncrease + maxIncrease);
bubble.size *= sizeMultiplier;
}
// Set initial velocities for clam bubbles
bubble.verticalVelocity = 0;
bubble.driftX = (spawnPoint.isRight ? -1 : 1) * (Math.random() * 1.5 + 2);
game.activeBubbles.push(bubble);
}
}
});
}
// Initialize twin bubbles array
game.twinBubbles = [];
game.treasureZones = [];
// Initialize game variables for bubbles
game.growingBubble = null;
game.lastMouthState = false; // Track previous mouth state
game.mouthOpenDuration = 0; // Track how long mouth has been open
game.MOUTH_OPEN_THRESHOLD = 10; // Frames required with mouth open to start bubble
game.MIN_SPAWN_SIZE = 25; // Minimum initial bubble size
game.blowCooldown = 0; // Cooldown timer between bubble starts
game.BLOW_COOLDOWN_TIME = 15; // Frames to wait between new bubbles
game.maxBubbleSize = 120; // Increased by 30%
game.growthRate = UPGRADE_EFFECTS.quickBreath.baseValue * (1 + UPGRADE_EFFECTS.quickBreath.incrementPercent / 100 * UPGRADE_CONFIG.player.quickBreath.currentLevel);
// Create bubble pool
game.bubblePool = Array(250).fill(null).map(function () {
return new Bubble();
});
game.activeBubbles = [];
game.MAX_BUBBLES = 200; // Active bubble limit
// Add bubbles to game
game.bubblePool.forEach(function (bubble) {
game.addChild(bubble);
});
// Set base spawn rate
game.baseSpawnRate = 180; // Every 3 seconds
// Function to spawn a bubble
function spawnBubble(x, y, size) {
var direction = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
var isAutoPop = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
if (game.activeBubbles.length >= game.MAX_BUBBLES) {
return null;
}
// Find first available bubble in pool
var bubble = game.bubblePool.find(function (b) {
return !b.visible;
});
if (!bubble) {
return null;
}
bubble.activate(x, y, size);
if (isAutoPop) {
bubble.verticalVelocity = bubble.floatSpeed;
bubble.driftX = direction * (Math.random() * 0.8 + 0.5);
} else {
bubble.verticalVelocity = -(Math.random() * 2 + 4);
bubble.driftX = direction * (Math.random() * 1.5 + 2);
}
game.activeBubbles.push(bubble);
return bubble;
}
// Game update function
game.update = function () {
updateCoralBubbles();
// Update mouth state and duration
if (!game.lastMouthState) {
game.mouthOpenDuration = 0;
}
if (facekit.mouthOpen) {
game.mouthOpenDuration++;
} else {
game.mouthOpenDuration = 0;
}
// Only allow bubble creation if menu is closed and mouth has been open long enough
if (!menuOpen && facekit.mouthOpen && game.mouthOpenDuration >= game.MOUTH_OPEN_THRESHOLD) {
// Only allow new bubbles after cooldown
if (!game.growingBubble && game.blowCooldown <= 0) {
var spawnX = playerMask.x;
var spawnY = playerMask.y + playerMask.height * 0.15;
var sizeVarianceLevel = UPGRADE_CONFIG.player.sizeVariance.currentLevel;
var minSizeMultiplier = 1 + 0.1 * sizeVarianceLevel;
var adjustedMinSize = game.MIN_SPAWN_SIZE * minSizeMultiplier;
game.growingBubble = spawnBubble(spawnX, spawnY, adjustedMinSize, 0, false);
if (game.growingBubble) {
game.blowCooldown = game.BLOW_COOLDOWN_TIME;
}
}
if (game.growingBubble) {
game.growingBubble.x = playerMask.x;
game.growingBubble.y = playerMask.y + playerMask.height * 0.15;
game.growingBubble.size = Math.min(game.growingBubble.size + game.growthRate, game.maxBubbleSize);
game.growingBubble.verticalVelocity = 0;
game.growingBubble.driftX = 0;
}
} else {
if (game.growingBubble) {
// Recalculate float speed and lifetime based on final size
game.growingBubble.floatSpeed = 50 * (120 / game.growingBubble.size * (0.9 + Math.random() * 0.2)) / 60;
game.growingBubble.initLifetime();
// Then apply the release velocity
game.growingBubble.verticalVelocity = -10;
game.growingBubble.driftX = (Math.random() * 2 - 1) * 2.5;
var twinLevel = UPGRADE_CONFIG.player.twinBubbles.currentLevel;
if (twinLevel > 0 && Math.random() < twinLevel * 0.05) {
// 5/10/15% chance based on level
var twinBubble = spawnBubble(game.growingBubble.x + (Math.random() * 40 - 20), game.growingBubble.y + (Math.random() * 40 - 20), game.growingBubble.size * 0.9, game.growingBubble.driftX * 0.8, false);
if (twinBubble) {
// Link the bubbles
game.twinBubbles.push({
bubble1: game.growingBubble,
bubble2: twinBubble,
popped: false,
timestamp: LK.ticks
});
}
}
game.growingBubble = null;
var breathLevel = UPGRADE_CONFIG.player.bubbleBreath.currentLevel;
if (breathLevel > 0) {
// Calculate chances for extra bubbles
var extraBubbles = 0;
var rand = Math.random();
if (rand < breathLevel * 0.2) {
extraBubbles++;
// Check for 2nd extra bubble at level 3-4
if (breathLevel >= 3 && rand < (breathLevel - 2) * 0.2) {
extraBubbles++;
}
}
// Spawn extra bubbles
for (var i = 0; i < extraBubbles; i++) {
var angle = Math.random() * Math.PI * 2;
var distance = 20 + Math.random() * 30;
var extraX = playerMask.x + Math.cos(angle) * distance;
var extraY = playerMask.y + playerMask.height * 0.15 + Math.sin(angle) * distance;
var extraSize = game.growingBubble ? Math.max(game.MIN_SPAWN_SIZE, game.growingBubble.size * (0.6 + Math.random() * 0.2)) : game.MIN_SPAWN_SIZE;
var extraBubble = spawnBubble(extraX, extraY, extraSize, 0, false);
if (extraBubble) {
extraBubble.verticalVelocity = -8 - Math.random() * 4;
extraBubble.driftX = Math.random() * 4 - 2;
}
}
}
game.mouthOpenDuration = 0;
}
}
// Update cooldown timer
if (game.blowCooldown > 0) {
game.blowCooldown--;
}
game.lastMouthState = facekit.mouthOpen;
updateClams();
// Fish spawning (auto-pop upgrade)
if (UPGRADE_CONFIG.player.autoPop.currentLevel > 0) {
// 8 seconds (480 frames) base, reduced by 2 seconds (120 frames) per level
if (LK.ticks % Math.max(60, 660 - UPGRADE_CONFIG.player.autoPop.currentLevel * 120) === 0) {
var fish = new Fish();
game.addChild(fish);
}
}
// Random bubble spawning
if (game.activeBubbles.length < game.MAX_BUBBLES) {
if (LK.ticks % game.baseSpawnRate == 0) {
var x = Math.random() * (game.width - 200) + 100;
spawnBubble(x, game.height + 100, 100, 0, true);
}
}
// Clean up old twin bubble pairs
game.twinBubbles = game.twinBubbles.filter(function (pair) {
// Remove pairs where both bubbles are gone or time expired (60 frames = 1 second)
if (!pair.bubble1.visible || !pair.bubble2.visible || pair.popped && LK.ticks - pair.timestamp > 60) {
return false;
}
return true;
});
// Update all active bubbles
game.activeBubbles.forEach(function (bubble) {
if (bubble.update) {
bubble.update();
}
});
};
// Handle touch/mouse events for the game
game.down = function (x, y, obj) {
var localX = x - menuContainer.x;
var localY = y - menuContainer.y;
// Tab click handling
var tabBounds = {
x: -menuTab.width * menuTab.scaleX / 2,
y: -menuTab.height * menuTab.scaleY,
width: menuTab.width * menuTab.scaleX,
height: menuTab.height * menuTab.scaleY
};
if (localX >= tabBounds.x && localX <= tabBounds.x + tabBounds.width && localY >= tabBounds.y && localY <= tabBounds.y + tabBounds.height) {
menuOpen = !menuOpen;
var targetY = menuOpen ? menuTab.height : game.height;
if (menuOpen) {
game.setChildIndex(menuContainer, game.children.length - 1);
}
tween(menuContainer, {
y: targetY
}, {
duration: 300,
easing: tween.easeOutBack,
onFinish: function onFinish() {
tabsContainer.visible = menuOpen; // Show/hide tabs based on menu state
if (!menuOpen) {
game.setChildIndex(menuContainer, 1);
}
}
});
return true;
}
if (menuOpen) {
return true; // Let containers handle their own clicks
}
// Bubble popping logic
var popped = false;
for (var i = game.activeBubbles.length - 1; i >= 0; i--) {
var bubble = game.activeBubbles[i];
var dx = x - bubble.x;
var dy = y - bubble.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (!popped && distance <= bubble.size / 2 + 30 && bubble.down) {
bubble.down();
popped = true;
break;
}
}
};
// Initialize decoration visuals
updateClamVisuals();
updateCoralDecorations();
updateTreasureDecorations();
// Initialize upgrade texts for all tabs
menuTabs.forEach(function (tab) {
if (tabColumns[tab] && tabColumns[tab].left) {
tabColumns[tab].left.forEach(function (upgrade, index) {
createUpgradeText(upgrade[0], upgrade[1], index, true, tab);
});
}
if (tabColumns[tab] && tabColumns[tab].right) {
tabColumns[tab].right.forEach(function (upgrade, index) {
createUpgradeText(upgrade[0], upgrade[1], index, false, tab);
});
}
});
// Update all upgrade texts to their correct initial state
updateAllUpgradeTexts();
A treasure chest with gold coins. Cartoon.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A golden skull with diamonds for eyes. Cartoon.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A golden necklace with a ruby pendant. Cartoon.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A filled in white circle.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A yellow star. Cartoon.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
a game logo for a game called 'Bubble Blower Tycoon' about a happy purple pufferfish with yellow fins and spines that builds an underwater empire of bubbles. Cartoon. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
an SVG of the word 'Start'. word should be yellow and the font should look like its made out of bubbles. cartoon. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
bubblelow
Sound effect
backgroundmusic
Music
bubblehigh
Sound effect
bubble1
Sound effect
bubble2
Sound effect
bubble3
Sound effect
bubble4
Sound effect
blowing
Sound effect
bubbleshoot
Sound effect
fishtank
Sound effect
menuopen
Sound effect
upgrade
Sound effect
jellyfish
Sound effect
titlemusic
Music
startbutton
Sound effect