Code edit (5 edits merged)
Please save this source code
User prompt
Please fix the bug: 'ReferenceError: facekit is not defined' in or related to this line: 'var currentMouthOpen = facekit.mouthOpen; // Get current state from pufferfish' Line Number: 3065 ↪💡 Consider importing and using the following plugins: @upit/facekit.v1
Code edit (2 edits merged)
Please save this source code
User prompt
update with: // Update mouth state and duration var currentMouthOpen = facekit.mouthOpen; // Get current state from pufferfish if (!game.lastMouthState) { game.mouthOpenDuration = 0; } if (currentMouthOpen) { game.mouthOpenDuration++; } else { game.mouthOpenDuration = 0; }
Code edit (2 edits merged)
Please save this source code
User prompt
Update as needed with: function showTutorialPopup(stage) { // Set current stage game.tutorial.stage = stage; // Remove existing tutorial popup if any if (game.tutorialContainer) { game.tutorialContainer.destroy(); } // Create new popup container game.tutorialContainer = new Container(); game.tutorialContainer.creationTime = LK.ticks; // Add this line to track when it was created game.addChild(game.tutorialContainer); // Rest of the function remains the same // ... }
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
User prompt
Reduce base time between fish spawns by a little.
User prompt
Increase base time between jellyfish spawns.
Code edit (1 edits merged)
Please save this source code
User prompt
Change treasure alpha to 0.8
Code edit (1 edits merged)
Please save this source code
User prompt
Don’t change the color of the popeffect to match the bubble color.
Code edit (1 edits merged)
Please save this source code
User prompt
Update tentacle retreat with: var bubble = spawnBubble( self.x + (Math.random() * 80 - 40), self.y + (Math.random() * 80 - 40), 50 + Math.random() * 50, -retreatVector.x * (Math.random() * 2 + 3), // Reduced from (Math.random() * 3 + 5) false );
Code edit (1 edits merged)
Please save this source code
User prompt
Update tentacle class with: var perpRotation = rotation + Math.PI/2; // Changed from - to + to go right var offsetAmount = tentacleFullWidth * 0.04; // Reduced to 4%
User prompt
Update tentacle retreat with: if (bubble) { // Corrected vertical velocity (negative Y is upward on screen) bubble.verticalVelocity = retreatVector.y * (Math.random() * 4 + 6) - (Math.random() * 3 + 2); }
User prompt
Update with: if (self.flailTimer <= 0) { self.state = 'retreating'; // Calculate retreat direction vector var retreatVector = { x: 0, y: 0 }; // Set direction based on which side tentacle is retreating to switch (self.side) { case 'left': retreatVector.x = -1; break; case 'right': retreatVector.x = 1; break; case 'top': retreatVector.y = -1; break; case 'bottom': retreatVector.y = 1; break; } // Spawn bubble cluster on retreat with enhanced velocity for (var i = 0; i < 8; i++) { LK.setTimeout(function () { var bubble = spawnBubble( self.x + (Math.random() * 80 - 40), self.y + (Math.random() * 80 - 40), 50 + Math.random() * 50, -retreatVector.x * (Math.random() * 3 + 5), // Stronger horizontal velocity opposite to retreat false ); if (bubble) { // Higher vertical velocity opposite to retreat direction bubble.verticalVelocity = -retreatVector.y * (Math.random() * 4 + 6) - (Math.random() * 3 + 2); } }, i * 3); } }
User prompt
Increase the outwards volocity of the tentacle bubbles.
User prompt
Update with: game.activeBubbles.forEach(function (bubble) { if (bubble.visible) { // Use the sprite's current rotation directly - it already includes base rotation and flailing var rotation = sprite.rotation; // Calculate the tentacle's actual dimensions var tentacleFullHeight = sprite.height * sprite.scaleY; // 500 * 2 = 1000 var tentacleFullWidth = sprite.width * sprite.scaleX; // 400 * 2 = 800 // Calculate the anchor offset (since anchor is at 0.5, 0.9) var anchorOffsetY = tentacleFullHeight * 0.9; // 900 pixels from top // The base of the tentacle is at the anchor point (self.x, self.y) var baseX = self.x; var baseY = self.y; // Calculate the effective length for collision detection (80% of full height) var effectiveLength = tentacleFullHeight * 0.8; // 800 pixels // Calculate the tip position - this needs to account for the anchor point AND rotation // The (0,0) of sprite is at (self.x, self.y), and we need to go in the rotated -y direction var tipX = baseX + Math.sin(rotation) * effectiveLength; var tipY = baseY - Math.cos(rotation) * effectiveLength; // For accurate visual debugging, show the calculated tip position // (This can be removed in production) game.createPopEffect(tipX, tipY, 20, 0x00FF00); // Use distance to line segment for collision var bubbleDistToLine = pointToLineDistance(bubble.x, bubble.y, baseX, baseY, tipX, tipY); // Width varies along the tentacle - wider in the middle (about 30% of tentacle width) var distanceToBase = Math.sqrt(Math.pow(bubble.x - baseX, 2) + Math.pow(bubble.y - baseY, 2)); var distanceToTip = Math.sqrt(Math.pow(bubble.x - tipX, 2) + Math.pow(bubble.y - tipY, 2)); var totalLength = Math.sqrt(Math.pow(tipX - baseX, 2) + Math.pow(tipY - baseY, 2)); // Calculate how far along the tentacle the nearest point is (0 = base, 1 = tip) var positionAlongTentacle; if (totalLength > 0) { var dot = ((bubble.x - baseX) * (tipX - baseX) + (bubble.y - baseY) * (tipY - baseY)); positionAlongTentacle = Math.max(0, Math.min(1, dot / (totalLength * totalLength))); } else { positionAlongTentacle = 0; } // Width profile - widest in middle (30% of width), narrow at ends (10% of width) var widthProfile = 0.1 + 0.2 * (1 - Math.abs(positionAlongTentacle * 2 - 1)); var collisionWidth = tentacleFullWidth * widthProfile; // If the bubble is close enough to the tentacle line, pop it if (bubbleDistToLine <= bubble.size/2 + collisionWidth/2) { game.createPopEffect(bubble.x, bubble.y, bubble.size, bubble.colorTint); game.addBP(bubble.getBP(), bubble.x, bubble.y, false); bubble.deactivate(); } } });
User prompt
Update with: game.activeBubbles.forEach(function (bubble) { if (bubble.visible) { // Use the actual rotation of the sprite without additional adjustments var angle = sprite.rotation; var tentacleLength = sprite.height * sprite.scaleY * 0.8; var tentacleWidth = sprite.width * sprite.scaleX * 0.35; // Calculate tip position using the anchor point offset // The 0.9 anchor means we need to use 90% of the length from the base position var anchorOffsetY = sprite.height * sprite.scaleY * 0.9; var baseX = self.x - Math.sin(angle) * anchorOffsetY; var baseY = self.y - Math.cos(angle) * anchorOffsetY; // Calculate tip from the actual base position var tipX = baseX + Math.sin(angle) * tentacleLength; var tipY = baseY + Math.cos(angle) * tentacleLength; // Use rectangular hit detection along entire tentacle var bubbleDistToLine = pointToLineDistance(bubble.x, bubble.y, baseX, baseY, tipX, tipY); // Wider at middle, narrower at ends var distanceToBase = Math.sqrt(Math.pow(bubble.x - baseX, 2) + Math.pow(bubble.y - baseY, 2)); var distanceToTip = Math.sqrt(Math.pow(bubble.x - tipX, 2) + Math.pow(bubble.y - tipY, 2)); var tentacleFullLength = Math.sqrt(Math.pow(tipX - baseX, 2) + Math.pow(tipY - baseY, 2)); var distanceAlongTentacle = distanceToBase / tentacleFullLength; // Parabolic width distribution - wider in middle, narrower at ends var widthFactor = tentacleWidth * (1 - Math.pow(2 * distanceAlongTentacle - 1, 2)); if (bubbleDistToLine <= bubble.size/2 + widthFactor) { game.createPopEffect(bubble.x, bubble.y, bubble.size, bubble.colorTint); game.addBP(bubble.getBP(), bubble.x, bubble.y, false); bubble.deactivate(); } } });
User prompt
Update with: // Pop bubbles in actual swept area game.activeBubbles.forEach(function (bubble) { if (bubble.visible) { // Get tentacle dimensions based on scale var tentacleLength = sprite.height * sprite.scaleY * 0.75; // Use 75% of length for better accuracy var tentacleWidth = sprite.width * sprite.scaleX * 0.4; // Use 40% of width for better accuracy // Calculate tentacle tip position var angle = sprite.rotation; var tipX = self.x + Math.cos(angle) * tentacleLength; var tipY = self.y + Math.sin(angle) * tentacleLength; // Calculate perpendicular vector for width var perpAngle = angle + Math.PI/2; // Use rectangular hit detection along tentacle length var bubbleDistToLine = pointToLineDistance(bubble.x, bubble.y, self.x, self.y, tipX, tipY); // Add width factor based on distance along tentacle var distanceAlongTentacle = ((bubble.x - self.x) * (tipX - self.x) + (bubble.y - self.y) * (tipY - self.y)) / ((tipX - self.x) * (tipX - self.x) + (tipY - self.y) * (tipY - self.y)); // Wider near the base, narrower at the tip var widthFactor = tentacleWidth * (1 - Math.min(1, Math.max(0, distanceAlongTentacle)) * 0.5); if (bubbleDistToLine <= bubble.size/2 + widthFactor) { game.createPopEffect(bubble.x, bubble.y, bubble.size, bubble.colorTint); game.addBP(bubble.getBP(), bubble.x, bubble.y, false); bubble.deactivate(); } } });
User prompt
Update with: case 'flailing': // ... existing flailing code ... game.activeBubbles.forEach(function(bubble) { if(bubble.visible) { var tentacleLength = sprite.height * sprite.scaleY; var angle = sprite.rotation; // Adjust angle based on entry side switch(self.side) { case 'left': angle -= Math.PI/4; break; // Rotate 45° left case 'right': angle += Math.PI/4; break; // Rotate 45° right case 'top': angle += Math.PI/4; break; // Rotate 45° right case 'bottom': angle -= Math.PI/4; break; // Rotate 45° left } var tipX = self.x + Math.cos(angle) * tentacleLength; var tipY = self.y + Math.sin(angle) * tentacleLength; // Debug visualization game.createPopEffect(tipX, tipY, 20, 0xFF0000); var bubbleDistToLine = pointToLineDistance( bubble.x, bubble.y, self.x, self.y, tipX, tipY ); if(bubbleDistToLine <= bubble.size/2 + sprite.width * sprite.scaleX * 0.3) { game.createPopEffect(bubble.x, bubble.y, bubble.size, bubble.colorTint); game.addBP(bubble.getBP(), bubble.x, bubble.y, false); bubble.deactivate(); } } });
/**** 
* Plugins
****/ 
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.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.isTwin = false;
	self.starSprite = null;
	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 twin properties
		self.isTwin = false;
		self.twinPair = null; // Add this line to reset twin pair reference
		if (self.starSprite) {
			self.starSprite.visible = false;
		}
		// 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();
		// Clear ALL blast-related flags on activation
		self.isBubbleBlast = false;
		self.isBlastFragment = false;
		self.blastImmuneFrames = 0;
		self.parentBlastBubble = null;
		// Add some debug logs
	};
	self.setTwin = function () {
		self.isTwin = true;
		// Create star sprite if it doesn't exist
		if (!self.starSprite) {
			self.starSprite = LK.getAsset('star', {
				anchorX: 0.5,
				anchorY: 0.5,
				alpha: 0.9
			});
			self.addChild(self.starSprite);
		}
		// Make sure it's visible
		self.starSprite.visible = true;
		// Scale star based on bubble size
		var starScale = self.size / 200; // Adjust divisor to change star size relative to bubble
		self.starSprite.scaleX = starScale;
		self.starSprite.scaleY = starScale;
		// Add a small rotation animation
		self.starSprite.rotationSpeed = Math.random() * 0.02 + 0.01;
	};
	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;
		if (self.starSprite) {
			self.starSprite.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;
	// In the Bubble class, modify the down method:
	self.down = function (e) {
		var currentTime = Date.now();
		if (currentTime - self.lastPopTime < 100) {
			return true;
		}
		self.lastPopTime = currentTime;
		// Calculate points before any effects
		var points = self.getBP();
		// Create pop effect with the bubble's color
		game.createPopEffect(self.x, self.y, self.size, self.colorTint);
		// Award points
		game.addBP(points, self.x, self.y, false);
		// Handle bubble blast for player-blown bubbles
		if (self.isBubbleBlast && self.size > 60 && !self.justSplit) {
			var blastLevel = UPGRADE_CONFIG.player.bubbleBlast.currentLevel;
			var splitCount = 2 + UPGRADE_CONFIG.machine.bubbleDurability.currentLevel;
			var newSize = Math.max(self.MIN_SPLIT_SIZE, self.size * 0.6);
			// Create split bubbles in a circular pattern
			for (var i = 0; i < splitCount; i++) {
				var angle = i / splitCount * Math.PI * 2;
				var velocityMultiplier = 1 + blastLevel * 0.4; // Increase velocity with level
				var split = spawnBubble(self.x, self.y, newSize, Math.cos(angle) * velocityMultiplier, false, self);
				if (split) {
					// Mark these as blast fragments - can pop other bubbles but not each other
					split.isBlastFragment = true;
					// Increase their lifetime to allow longer travel
					split.maxLifetime *= 1.5 + blastLevel * 0.3;
					// Only immune to other fragments for a brief period
					split.blastImmuneFrames = 20; // Already 10, keep it the same
					// Override velocity parameters for more dynamic movement
					var speedMultiplier = 1.5 + blastLevel * 0.5;
					var baseSpeed = 3 + blastLevel * 0.8;
					// Adjust velocity based on direction
					if (Math.sin(angle) < 0) {
						// For upward trajectories
						split.verticalVelocity = Math.sin(angle) * baseSpeed * speedMultiplier * 0.4; // Reduce speed
					} else if (Math.sin(angle) > 0) {
						// For downward trajectories
						split.verticalVelocity = Math.sin(angle) * baseSpeed * speedMultiplier * 0.2; // Also reduce speed
					} else {
						// Horizontal trajectories
						split.verticalVelocity = 0;
					}
					split.driftX = Math.cos(angle) * baseSpeed * speedMultiplier;
				}
			}
		}
		// Normal bubble splitting for non-blast bubbles
		else 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;
				// For blast fragments, don't pass the parent reference to avoid inheriting properties
				var parentRef = self.isBlastFragment ? null : self;
				var split = spawnBubble(self.x, self.y, newSize, Math.cos(angle) * 0.5, false, parentRef);
				if (split) {
					// Explicitly override blast fragment properties for children of blast fragments
					if (self.isBlastFragment) {
						split.isBlastFragment = false;
						split.isBubbleBlast = false;
						split.blastImmuneFrames = 30; // Much longer immunity for these splits
						// Ensure they have different velocities to spread out
						split.verticalVelocity = -4 - Math.random() * 3;
						split.driftX = Math.cos(angle) * (3 + Math.random() * 2);
					} else {
						// Normal immunity for regular splits
						split.blastImmuneFrames = 10;
					}
				}
			}
		}
		// Handle twin bubbles (keep existing code)
		if (self.twinPair && !self.twinPair.popped) {
			self.twinPair.popped = true;
			// Pop the other twin
			var otherTwin = self.twinPair.bubble1 === self ? self.twinPair.bubble2 : self.twinPair.bubble1;
			if (otherTwin && otherTwin.visible) {
				// Award points and create effect for second pop
				game.createPopEffect(otherTwin.x, otherTwin.y, otherTwin.size, otherTwin.colorTint);
				game.addBP(otherTwin.getBP(), otherTwin.x, otherTwin.y, false);
				otherTwin.deactivate();
			}
		}
		// Play sound
		var bubbleSounds = ['bubble1', 'bubble2', 'bubble3', 'bubble4'];
		var randomSound = bubbleSounds[Math.floor(Math.random() * bubbleSounds.length)];
		LK.getSound(randomSound).play();
		// Check if this is the currently growing bubble
		if (game && game.growingBubble === self) {
			game.growingBubble = null;
			game.mouthOpenDuration = 0;
			game.blowCooldown = game.BLOW_COOLDOWN_TIME;
		}
		self.deactivate();
		// Tutorial handling code
		if (game && game.tutorial && game.tutorial.stage === 2) {
			game.tutorial.poppedBubble = true;
			LK.setTimeout(function () {
				showTutorialPopup(3);
			}, 30);
		}
		return true;
	};
	self.getBP = function () {
		var baseValue = Math.max(1, Math.floor(Math.pow(self.size, 1.4) * 0.018)); // Lowered by 10% 
		// 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 () {
		// Keep the existing bubble blast collision detection for player-blown bubbles
		if (self.isBubbleBlast && game.growingBubble !== self) {
			game.activeBubbles.forEach(function (targetBubble) {
				if (targetBubble !== self && targetBubble !== game.growingBubble && !targetBubble.isBubbleBlast && !targetBubble.isBlastFragment && !targetBubble.isTwin && targetBubble.visible) {
					var dx = targetBubble.x - self.x;
					var dy = targetBubble.y - self.y;
					var distance = Math.sqrt(dx * dx + dy * dy);
					if (distance <= self.size / 2 + targetBubble.size / 2) {
						// Create pop effect
						game.createPopEffect(targetBubble.x, targetBubble.y, targetBubble.size, targetBubble.colorTint);
						// Award points with blast bonus
						var blastLevel = UPGRADE_CONFIG.player.bubbleBlast.currentLevel;
						var points = Math.floor(targetBubble.getBP() * (0.7 + blastLevel * 0.1));
						game.addBP(points, targetBubble.x, targetBubble.y, false);
						// Play pop sound
						var bubbleSounds = ['bubble1', 'bubble2', 'bubble3', 'bubble4'];
						var randomSound = bubbleSounds[Math.floor(Math.random() * bubbleSounds.length)];
						LK.getSound(randomSound).play();
						targetBubble.deactivate();
					}
				}
			});
		}
		// Handle blast fragment collisions with other bubbles
		else if (self.isBlastFragment) {
			game.activeBubbles.forEach(function (targetBubble) {
				if (targetBubble !== self && targetBubble.visible && !targetBubble.isBubbleBlast && !targetBubble.isBlastFragment && targetBubble !== game.growingBubble && targetBubble.blastImmuneFrames <= 0) {
					// Add this check 
					var dx = targetBubble.x - self.x;
					var dy = targetBubble.y - self.y;
					var distance = Math.sqrt(dx * dx + dy * dy);
					if (distance <= self.size / 2 + targetBubble.size / 2) {
						// Create pop effect 
						game.createPopEffect(targetBubble.x, targetBubble.y, targetBubble.size, targetBubble.colorTint);
						// Award points 
						var points = Math.floor(targetBubble.getBP() * 0.5); // Fragments give less points 
						game.addBP(points, targetBubble.x, targetBubble.y, false);
						// Play pop sound 
						var bubbleSounds = ['bubble1', 'bubble2', 'bubble3', 'bubble4'];
						var randomSound = bubbleSounds[Math.floor(Math.random() * bubbleSounds.length)];
						LK.getSound(randomSound).play();
						targetBubble.deactivate();
					}
				}
			});
		}
		if (self.blastImmuneFrames > 0) {
			self.blastImmuneFrames--;
		}
		if (UPGRADE_CONFIG.colors.prismaticBubbles.currentLevel > 0) {
			self.colorPhase += 0.02;
			// Only calculate new color every 3 frames
			if (LK.ticks % 3 === 0 || !self.prismaticColor) {
				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;
				self.prismaticColor = (Math.floor(r) << 16) + (Math.floor(g) << 8) + Math.floor(b);
				var colorValue = Math.sin(self.colorPhase) * 0.5 + 0.5;
				self.colorMultiplier = 0.8 + colorValue * 1.2;
			}
			sprite.tint = self.prismaticColor;
			self.colorTint = self.prismaticColor;
		}
		// Update star rotation if this is a twin bubble
		if (self.isTwin && self.starSprite && self.starSprite.visible) {
			self.starSprite.rotation += self.starSprite.rotationSpeed;
			// Scale star based on bubble size
			var starScale = self.size / 200;
			self.starSprite.scaleX = starScale;
			self.starSprite.scaleY = starScale;
		}
		// 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) {
			// Use the deactivate method for consistent cleanup
			self.deactivate();
			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;
	};
	// In the Bubble class, find the autoPop method
	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);
			// Add pop effect for auto-pops
			game.createPopEffect(self.x, self.y, self.size, self.colorTint);
			game.addBP(points, self.x, self.y, true);
			self.autoPopDisplayed = true;
			// Only play sound if cooldown is 0
			if (game.autoPopSoundCooldown <= 0) {
				// Play a softer pop sound for auto-pops
				var bubbleSounds = ['bubble1', 'bubble2', 'bubble3', 'bubble4'];
				var randomSound = bubbleSounds[Math.floor(Math.random() * bubbleSounds.length)];
				var sound = LK.getSound(randomSound);
				sound.volume = 0.3; // Lower volume for auto-pops
				sound.play();
				// Set cooldown
				game.autoPopSoundCooldown = game.AUTO_POP_SOUND_COOLDOWN;
			}
		}
		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 + 70) {
					// Add pop effect for fish pops
					game.createPopEffect(bubble.x, bubble.y, bubble.size, bubble.colorTint);
					var points = bubble.getBP();
					game.addBP(points, bubble.x, bubble.y, false); // false = manual pop points
					// Play a random bubble pop sound
					var bubbleSounds = ['bubble1', 'bubble2', 'bubble3', 'bubble4'];
					var randomSound = bubbleSounds[Math.floor(Math.random() * bubbleSounds.length)];
					LK.getSound(randomSound).play();
					bubble.deactivate();
				}
			}
		});
		// Remove when off screen
		if (self.fromLeft && self.x > game.width + 100 || !self.fromLeft && self.x < -100) {
			self.destroy();
		}
	};
	return self;
});
var Jellyfish = Container.expand(function () {
	var self = Container.call(this);
	// Create jellyfish sprite
	var sprite = self.attachAsset('jellyfish', {
		anchorX: 0.5,
		anchorY: 0.3,
		// Set anchor higher to make tentacles flow better
		x: 0,
		y: 0
	});
	// Initialize position and movement
	self.x = Math.random() * (game.width - 200) + 100;
	self.y = -100; // Start above screen
	self.speed = 2.5; // Slower base speed
	self.targetX = self.x;
	self.touched = false;
	self.immuneTimer = 0; // Timer for bubble popping immunity
	// Motion variables
	self.pulsePhase = Math.random() * Math.PI * 2; // Random starting phase
	self.pulseFrequency = 0.03 + Math.random() * 0.01; // How fast it pulses
	self.pulseAmplitude = 0.15; // How much it pulses
	self.driftPhase = Math.random() * Math.PI * 2;
	self.driftFrequency = 0.01 + Math.random() * 0.005;
	self.driftAmount = 30 + Math.random() * 20;
	// Start pulsing animation immediately
	self.startPulsingAnimation = function () {
		// Cancel any existing animation
		if (self.pulseAnimation) {
			self.pulseAnimation.stop();
		}
		// Create the pulsing animation
		var originalScaleX = 1;
		var originalScaleY = 1;
		// Function to update the pulse animation
		function updatePulse() {
			if (!self || !sprite) {
				return;
			} // Safety check
			var pulseValue = Math.sin(self.pulsePhase);
			// Scale effect - contract and expand
			sprite.scaleX = originalScaleX * (1 - pulseValue * self.pulseAmplitude);
			sprite.scaleY = originalScaleY * (1 + pulseValue * self.pulseAmplitude);
			// Progress the phase
			self.pulsePhase += self.pulseFrequency;
			// Adjust vertical position slightly with pulse
			self.y += pulseValue > 0 ? self.speed * 1.2 : self.speed * 0.8;
			// Update horizontal drift
			var drift = Math.sin(self.driftPhase) * self.driftAmount / 60;
			self.targetX += drift;
			// Constrain to screen bounds
			self.targetX = Math.max(50, Math.min(game.width - 50, self.targetX));
			// Smooth movement toward target X
			self.x += (self.targetX - self.x) * 0.05;
			// Progress drift phase
			self.driftPhase += self.driftFrequency;
			// Continue animation if not destroyed
			if (self && !self.destroyed) {
				LK.setTimeout(updatePulse, 1);
			}
		}
		// Start the update loop
		updatePulse();
	};
	self.startPulsingAnimation();
	self.update = function () {
		// Decrement immunity timer if active
		if (self.immuneTimer > 0) {
			self.immuneTimer--;
		}
		// Only check for bubble collisions if not immune
		if (self.immuneTimer === 0) {
			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 + 70) {
						// Add pop effect for jellyfish pops
						game.createPopEffect(bubble.x, bubble.y, bubble.size, bubble.colorTint);
						var points = bubble.getBP();
						game.addBP(points, bubble.x, bubble.y, false);
						// Play a random bubble pop sound
						var bubbleSounds = ['bubble1', 'bubble2', 'bubble3', 'bubble4'];
						var randomSound = bubbleSounds[Math.floor(Math.random() * bubbleSounds.length)];
						LK.getSound(randomSound).play();
						bubble.deactivate();
					}
				}
			});
		}
		if (self.touched) {
			// When touched, jellyfish floats up rapidly
			self.y -= 15; // Much faster upward movement
			// Check if off screen to destroy
			if (self.y < -100) {
				self.destroy();
			}
			return;
		}
		// Remove when off bottom of screen
		if (self.y > game.height + 100) {
			self.destroy();
		}
	};
	// Handle being touched
	self.down = function () {
		if (self.touched) {
			return true;
		}
		self.touched = true;
		self.immuneTimer = 30; // Immunity for 30 frames (half a second)
		// Play sound
		LK.getSound('jellyfish').play();
		// Spawn bubbles in a jet
		var bubbleCount = 5 + Math.floor(Math.random() * 3);
		for (var i = 0; i < bubbleCount; i++) {
			LK.setTimeout(function () {
				if (!self) {
					return;
				} // Safety check
				var size = 50 + Math.random() * 50;
				var bubble = spawnBubble(self.x + (Math.random() * 40 - 20), self.y + (Math.random() * 40 - 20), size, (Math.random() * 2 - 1) * 2, false);
				if (bubble) {
					bubble.verticalVelocity = -(Math.random() * 6 + 4);
				}
			}, i * 5);
		}
		// Apply a "flee" animation
		tween(sprite, {
			scaleX: sprite.scaleX * 1.3,
			scaleY: sprite.scaleY * 0.7,
			alpha: 0.8
		}, {
			duration: 300,
			easing: tween.easeOutBack
		});
		return true;
	};
	return self;
});
var Tentacle = Container.expand(function () {
	var self = Container.call(this);
	var sprite = LK.getAsset('tentacle', {
		anchorX: 0.5,
		anchorY: 0.9,
		// Move anchor point to base of tentacle
		scaleX: 2,
		scaleY: 2,
		alpha: 0.9
	});
	// Movement states
	self.state = 'entering'; // entering, flailing, retreating
	self.phase = Math.random() * Math.PI * 2;
	self.flailPhase = 0;
	self.side = ['left', 'right', 'top', 'bottom'][Math.floor(Math.random() * 4)];
	// Calculate reach based on level
	var level = UPGRADE_CONFIG.player.tentacleTide.currentLevel;
	self.maxReach = 0.05; // 15% + 5% per level
	// Set initial position based on side
	switch (self.side) {
		case 'left':
			self.x = -sprite.width;
			self.y = game.height * (0.2 + Math.random() * 0.6);
			sprite.rotation = Math.PI * 0.25; // 45 degrees
			break;
		case 'right':
			self.x = game.width + sprite.width;
			self.y = game.height * (0.2 + Math.random() * 0.6);
			sprite.rotation = Math.PI * -0.75;
			break;
		case 'top':
			self.x = game.width * (0.2 + Math.random() * 0.6);
			self.y = -sprite.height;
			sprite.rotation = Math.PI * 0.75;
			break;
		case 'bottom':
			self.x = game.width * (0.2 + Math.random() * 0.6);
			self.y = game.height + sprite.height;
			sprite.rotation = Math.PI * -0.25;
			break;
	}
	self.update = function () {
		self.phase += 0.1;
		switch (self.state) {
			case 'entering':
				var targetX = self.side === 'left' ? game.width * self.maxReach : self.side === 'right' ? game.width * (1 - self.maxReach) : self.x;
				var targetY = self.side === 'top' ? game.height * self.maxReach : self.side === 'bottom' ? game.height * (1 - self.maxReach) : self.y;
				// Smooth entry
				self.x += (targetX - self.x) * 0.1;
				self.y += (targetY - self.y) * 0.1;
				// Transition to flailing when close to target
				if (Math.abs(self.x - targetX) < 5 && Math.abs(self.y - targetY) < 5) {
					self.state = 'flailing';
					self.flailTimer = 120; // 2 seconds of flailing
				}
				break;
			case 'flailing':
				self.flailPhase += 0.2;
				// Adjust flailing phase to account for new anchor point 
				var baseRotation = self.side === 'left' ? Math.PI * 0.25 : self.side === 'right' ? Math.PI * -0.75 : self.side === 'top' ? Math.PI * 0.75 : Math.PI * -0.25;
				// Reduced rotation amount for more natural sweep 
				sprite.rotation = baseRotation + Math.sin(self.flailPhase) * 0.2;
				// Add slight position undulation 
				self.x += Math.sin(self.flailPhase * 0.7) * 2;
				self.y += Math.cos(self.flailPhase * 0.5) * 2;
				// Pop bubbles in actual swept area 
				game.activeBubbles.forEach(function (bubble) {
					if (bubble.visible) {
						// Use the sprite's current rotation directly - it already includes base rotation and flailing
						var rotation = sprite.rotation;
						// Calculate the tentacle's actual dimensions
						var tentacleFullHeight = sprite.height * sprite.scaleY; // 500 * 2 = 1000
						var tentacleFullWidth = sprite.width * sprite.scaleX; // 400 * 2 = 800
						// Calculate the anchor offset (since anchor is at 0.5, 0.9)
						var anchorOffsetY = tentacleFullHeight * 0.9; // 900 pixels from top
						// The base of the tentacle is at the anchor point (self.x, self.y)
						var baseX = self.x;
						var baseY = self.y;
						// Calculate the effective length for collision detection (80% of full height)
						var effectiveLength = tentacleFullHeight * 0.95; // 800 pixels
						// Calculate the tip position - this needs to account for the anchor point AND rotation
						// The (0,0) of sprite is at (self.x, self.y), and we need to go in the rotated -y direction
						var tipX = baseX + Math.sin(rotation) * effectiveLength;
						var tipY = baseY - Math.cos(rotation) * effectiveLength;
						// For accurate visual debugging, show the calculated tip position
						// Use distance to line segment for collision
						var perpRotation = rotation + Math.PI / 2; // Changed from - to + to go right
						var offsetAmount = tentacleFullWidth * 0.04; // Reduced to 4%
						// Apply offset to both base and tip
						baseX += Math.sin(perpRotation) * offsetAmount;
						baseY -= Math.cos(perpRotation) * offsetAmount;
						tipX += Math.sin(perpRotation) * offsetAmount;
						tipY -= Math.cos(perpRotation) * offsetAmount;
						var bubbleDistToLine = pointToLineDistance(bubble.x, bubble.y, baseX, baseY, tipX, tipY);
						// Width varies along the tentacle - wider in the middle (about 30% of tentacle width)
						var distanceToBase = Math.sqrt(Math.pow(bubble.x - baseX, 2) + Math.pow(bubble.y - baseY, 2));
						var distanceToTip = Math.sqrt(Math.pow(bubble.x - tipX, 2) + Math.pow(bubble.y - tipY, 2));
						var totalLength = Math.sqrt(Math.pow(tipX - baseX, 2) + Math.pow(tipY - baseY, 2));
						// Calculate how far along the tentacle the nearest point is (0 = base, 1 = tip)
						var positionAlongTentacle;
						if (totalLength > 0) {
							var dot = (bubble.x - baseX) * (tipX - baseX) + (bubble.y - baseY) * (tipY - baseY);
							positionAlongTentacle = Math.max(0, Math.min(1, dot / (totalLength * totalLength)));
						} else {
							positionAlongTentacle = 0;
						}
						// Width profile - widest in middle (30% of width), narrow at ends (10% of width)
						var widthProfile = 0.1 + 0.2 * (1 - Math.abs(positionAlongTentacle * 2 - 1));
						var collisionWidth = tentacleFullWidth * widthProfile;
						// If the bubble is close enough to the tentacle line, pop it
						if (bubbleDistToLine <= bubble.size / 2 + collisionWidth / 2) {
							game.createPopEffect(bubble.x, bubble.y, bubble.size, bubble.colorTint);
							game.addBP(bubble.getBP(), bubble.x, bubble.y, false);
							bubble.deactivate();
						}
					}
				});
				self.flailTimer--;
				if (self.flailTimer <= 0) {
					self.state = 'retreating';
					// Calculate retreat direction vector
					var retreatVector = {
						x: 0,
						y: 0
					};
					// Set direction based on which side tentacle is retreating to
					switch (self.side) {
						case 'left':
							retreatVector.x = -1;
							break;
						case 'right':
							retreatVector.x = 1;
							break;
						case 'top':
							retreatVector.y = -1;
							break;
						case 'bottom':
							retreatVector.y = 1;
							break;
					}
					// Spawn bubble cluster on retreat with enhanced velocity
					for (var i = 0; i < 8; i++) {
						LK.setTimeout(function () {
							var bubble = spawnBubble(self.x + (Math.random() * 80 - 40), self.y + (Math.random() * 80 - 40), 50 + Math.random() * 50, -retreatVector.x * (Math.random() * 2 + 3),
							// Reduced from (Math.random() * 3 + 5)
							false);
							if (bubble) {
								// Higher vertical velocity opposite to retreat direction
								bubble.verticalVelocity = retreatVector.y * (Math.random() * 4 + 6) - (Math.random() * 3 + 2);
							}
						}, i * 3);
					}
				}
				break;
			case 'retreating':
				// Quick snap back to starting position
				var startX = self.side === 'left' ? -sprite.width : self.side === 'right' ? game.width + sprite.width : self.x;
				var startY = self.side === 'top' ? -sprite.height : self.side === 'bottom' ? game.height + sprite.height : self.y;
				self.x += (startX - self.x) * 0.2;
				self.y += (startY - self.y) * 0.2;
				// Remove when close to start position
				if (Math.abs(self.x - startX) < 5 && Math.abs(self.y - startY) < 5) {
					self.destroy();
				}
				break;
		}
	};
	self.addChild(sprite);
	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 () {
		// Only use face tracking when enabled
		if (game.faceTrackingEnabled) {
			// 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
****/ 
// Import storage plugin for data persistence
function _typeof(o) {
	"@babel/helpers - typeof";
	return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
		return typeof o;
	} : function (o) {
		return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
	}, _typeof(o);
}
function pointToLineDistance(px, py, x1, y1, x2, y2) {
	var A = px - x1;
	var B = py - y1;
	var C = x2 - x1;
	var D = y2 - y1;
	var dot = A * C + B * D;
	var len_sq = C * C + D * D;
	var param = -1;
	if (len_sq !== 0) {
		param = dot / len_sq;
	}
	var xx, yy;
	if (param < 0) {
		xx = x1;
		yy = y1;
	} else if (param > 1) {
		xx = x2;
		yy = y2;
	} else {
		xx = x1 + param * C;
		yy = y1 + param * D;
	}
	var dx = px - xx;
	var dy = py - yy;
	return Math.sqrt(dx * dx + dy * dy);
}
var savedBP = storage.bp || 0;
var savedLastPlayTime = storage.lastPlayTime || Date.now();
var savedTutorialComplete = storage.tutorialComplete || false;
function loadGame() {
	// Load BP
	if (storage.bp !== undefined) {
		game.bp = parseFloat(storage.bp);
		bpText.setText(formatBP(game.bp) + " BP");
	}
	// Load upgrades
	if (storage.simpleUpgrades) {
		var values = storage.simpleUpgrades.split(",");
		if (values.length >= 13) {
			// Clams
			UPGRADE_CONFIG.machines.basicClam.amount = parseInt(values[0]) || 0;
			UPGRADE_CONFIG.machines.advancedClam.amount = parseInt(values[1]) || 0;
			UPGRADE_CONFIG.machines.premiumClam.amount = parseInt(values[2]) || 0;
			// Player upgrades
			UPGRADE_CONFIG.player.lungCapacity.currentLevel = parseInt(values[3]) || 0;
			UPGRADE_CONFIG.player.quickBreath.currentLevel = parseInt(values[4]) || 0;
			UPGRADE_CONFIG.player.autoPop.currentLevel = parseInt(values[5]) || 0;
			UPGRADE_CONFIG.player.bubbleRefinement.currentLevel = parseInt(values[6]) || 0;
			UPGRADE_CONFIG.player.twinBubbles.currentLevel = parseInt(values[7]) || 0;
			// Check if save has jellyfish data (for backward compatibility)
			if (values.length >= 15) {
				UPGRADE_CONFIG.player.jellyfish.currentLevel = parseInt(values[14]) || 0;
			} else {
				// For older save files, initialize to 0
				UPGRADE_CONFIG.player.jellyfish.currentLevel = 0;
			}
			UPGRADE_CONFIG.player.sizeVariance.currentLevel = parseInt(values[8]) || 0;
			// Machine upgrades
			UPGRADE_CONFIG.machine.bubbleDurability.currentLevel = parseInt(values[9]) || 0;
			UPGRADE_CONFIG.machine.autoBubbleSpeed.currentLevel = parseInt(values[10]) || 0;
			UPGRADE_CONFIG.machine.bubbleQuality.currentLevel = parseInt(values[11]) || 0;
			// Treasures
			UPGRADE_CONFIG.decorations.sunkenTreasures.amount = parseInt(values[12]) || 0;
			// Active color setting (if present)
			if (values.length >= 14) {
				UPGRADE_CONFIG.gameSettings.activeColor = values[13] || "auto";
			}
			UPGRADE_CONFIG.player.bubbleBlast.currentLevel = values.length >= 16 ? parseInt(values[15]) || 0 : 0;
			UPGRADE_CONFIG.player.tentacleTide.currentLevel = values.length >= 17 ? parseInt(values[16]) || 0 : 0;
		}
	}
	// Load color unlocks
	if (storage.colorUnlocks) {
		var colorValues = storage.colorUnlocks.split(",");
		var colorKeys = ["blueBubbles", "purpleBubbles", "greenBubbles", "orangeBubbles", "pinkBubbles", "tealBubbles", "goldBubbles", "crimsonBubbles", "silverBubbles", "rainbowBubbles", "prismaticBubbles"];
		colorKeys.forEach(function (key, index) {
			if (index < colorValues.length) {
				UPGRADE_CONFIG.colors[key].currentLevel = parseInt(colorValues[index]) || 0;
			}
		});
	}
	// Load tutorial status
	if (storage.tutorialComplete) {
		game.tutorial.stage = 6; // Set to a stage past the tutorial
	}
	// Update all visuals based on loaded data
	updateClamVisuals();
	updateTreasureDecorations();
	updateAllUpgradeTexts();
	// Apply player upgrade effects
	var lungCapacityLevel = UPGRADE_CONFIG.player.lungCapacity.currentLevel;
	var quickBreathLevel = UPGRADE_CONFIG.player.quickBreath.currentLevel;
	// Update max bubble size from Lung Capacity
	var baseSize = UPGRADE_EFFECTS.lungCapacity.baseValue;
	var increasePercent = UPGRADE_EFFECTS.lungCapacity.incrementPercent;
	var multiplier = 1 + increasePercent / 100 * lungCapacityLevel;
	game.maxBubbleSize = baseSize * multiplier;
	// Update growth rate from Quick Breath
	game.growthRate = UPGRADE_EFFECTS.quickBreath.baseValue * (1 + UPGRADE_EFFECTS.quickBreath.incrementPercent / 100 * quickBreathLevel);
	// Calculate offline progress
	if (storage.lastPlayTime) {
		var currentTime = Date.now();
		var timeDiff = currentTime - storage.lastPlayTime;
		if (timeDiff > 60000) {
			// 1 minute
			calculateOfflineProgress(timeDiff);
		}
	}
}
// Save function - call periodically
function saveGame() {
	// Store basic values
	storage.bp = game.bp;
	// Create color unlock state string
	var colorUnlocks = "";
	var colorKeys = ["blueBubbles", "purpleBubbles", "greenBubbles", "orangeBubbles", "pinkBubbles", "tealBubbles", "goldBubbles", "crimsonBubbles", "silverBubbles", "rainbowBubbles", "prismaticBubbles"];
	colorKeys.forEach(function (key) {
		colorUnlocks += UPGRADE_CONFIG.colors[key].currentLevel + ",";
	});
	// Save active color setting
	var activeColor = UPGRADE_CONFIG.gameSettings.activeColor || "auto";
	// Save treasure amounts
	var treasureAmount = UPGRADE_CONFIG.decorations.sunkenTreasures.amount;
	// Save as comma-separated values - machine amounts and upgrade levels
	storage.simpleUpgrades =
	// Clams
	UPGRADE_CONFIG.machines.basicClam.amount + "," + UPGRADE_CONFIG.machines.advancedClam.amount + "," + UPGRADE_CONFIG.machines.premiumClam.amount + "," +
	// Player upgrades
	UPGRADE_CONFIG.player.lungCapacity.currentLevel + "," + UPGRADE_CONFIG.player.quickBreath.currentLevel + "," + UPGRADE_CONFIG.player.autoPop.currentLevel + "," + UPGRADE_CONFIG.player.bubbleRefinement.currentLevel + "," + UPGRADE_CONFIG.player.twinBubbles.currentLevel + "," + UPGRADE_CONFIG.player.sizeVariance.currentLevel + "," +
	// Machine upgrades
	UPGRADE_CONFIG.machine.bubbleDurability.currentLevel + "," + UPGRADE_CONFIG.machine.autoBubbleSpeed.currentLevel + "," + UPGRADE_CONFIG.machine.bubbleQuality.currentLevel + "," +
	// Treasures
	treasureAmount + "," +
	// Active color setting
	activeColor + "," +
	// Jellyfish
	UPGRADE_CONFIG.player.jellyfish.currentLevel + "," + UPGRADE_CONFIG.player.bubbleBlast.currentLevel + "," + UPGRADE_CONFIG.player.tentacleTide.currentLevel; // Add tentacleTide as new final value
	// Store color unlocks separately
	storage.colorUnlocks = colorUnlocks;
	storage.lastPlayTime = Date.now();
	storage.tutorialComplete = game.tutorial.stage >= 5;
}
// Calculate offline progress
// Calculate offline progress
function calculateOfflineProgress(timeDiff) {
	// Cap at 12 hours as before
	var secondsAway = Math.min(Math.floor(timeDiff / 1000), 43200);
	var totalBP = 0;
	// Calculate for each clam type
	['basicClam', 'advancedClam', 'premiumClam'].forEach(function (clamType) {
		var config = UPGRADE_CONFIG.machines[clamType];
		var clamCount = config.amount;
		if (clamCount > 0) {
			// Same calculation for production rate
			// Correct the production time to match in-game values
			var baseTime = config.production * 2.5; // Apply a multiplier to match the in-game frame conversion
			var speedMultiplier = Math.pow(1 - UPGRADE_EFFECTS.autoBubbleSpeed.decrementPercent / 100, UPGRADE_CONFIG.machine.autoBubbleSpeed.currentLevel);
			var adjustedTime = Math.max(1, baseTime * speedMultiplier);
			var bubblesPerSecond = clamCount / Math.max(adjustedTime, 0.5);
			var bubbleValue = Math.pow(config.bubbleSize, 1.4) * 0.02;
			bubbleValue *= 0.6; // Reduce base bubble value by 40%
			// Apply upgrades as before
			if (UPGRADE_CONFIG.machine.bubbleQuality.currentLevel > 0) {
				bubbleValue *= 1 + 0.4 * UPGRADE_CONFIG.machine.bubbleQuality.currentLevel;
			}
			var refinementLevel = UPGRADE_CONFIG.player.bubbleRefinement.currentLevel;
			if (refinementLevel > 0) {
				bubbleValue *= 1 + 0.25 * refinementLevel;
			}
			var activeColorKey = getActiveColorKey();
			var colorMultiplier = 1.0;
			if (activeColorKey && UPGRADE_CONFIG.colors[activeColorKey]) {
				colorMultiplier = UPGRADE_CONFIG.colors[activeColorKey].multiplier || 1.0;
			}
			bubbleValue *= colorMultiplier;
			// New efficiency calculation - starts lower but improves with upgrades
			var baseEfficiency = 0.015; // Cut in half again from 0.03
			var totalUpgradeLevels = UPGRADE_CONFIG.player.lungCapacity.currentLevel + UPGRADE_CONFIG.player.quickBreath.currentLevel + UPGRADE_CONFIG.player.bubbleRefinement.currentLevel + UPGRADE_CONFIG.machine.bubbleDurability.currentLevel + UPGRADE_CONFIG.machine.autoBubbleSpeed.currentLevel;
			var upgradeBonus = Math.min(0.185, totalUpgradeLevels * 0.003); // Slower upgrade scaling
			var clamTypeBonus = clamType === 'premiumClam' ? 0.08 : clamType === 'advancedClam' ? 0.04 : 0;
			var timeDecay = Math.max(0.4, 1 - secondsAway / 172800); // 48 hours instead of 24
			var timeScale = Math.min(1, secondsAway / 3600); // Scales up over first hour
			var efficiencyFactor = (baseEfficiency + upgradeBonus + clamTypeBonus) * timeDecay * timeScale;
			// Calculate BP from this clam type
			totalBP += bubblesPerSecond * bubbleValue * secondsAway * efficiencyFactor;
		}
	});
	var jellyfishLevel = UPGRADE_CONFIG.player.jellyfish.currentLevel;
	if (jellyfishLevel > 0) {
		// Jellyfish spawn frequency decreases with level
		var jellyfishFrequency = Math.max(60, 960 - jellyfishLevel * 120);
		// Average bubbles per jellyfish (based on in-game behavior)
		var bubblesPerJellyfish = 5 + 1.5;
		// Average jellyfish spawns per second
		var jellyfishPerSecond = 60 / jellyfishFrequency;
		// Average bubble size from jellyfish
		var avgBubbleSize = 75;
		// Base value calculation (matching Bubble.getBP formula)
		var bubbleValue = Math.pow(avgBubbleSize, 1.4) * 0.018;
		// Apply refinement and color multipliers
		var refinementLevel = UPGRADE_CONFIG.player.bubbleRefinement.currentLevel;
		if (refinementLevel > 0) {
			bubbleValue *= 1 + 0.25 * refinementLevel;
		}
		// Color multiplier (using active color)
		var activeColorKey = getActiveColorKey();
		var colorMultiplier = 1.0;
		if (activeColorKey && UPGRADE_CONFIG.colors[activeColorKey]) {
			colorMultiplier = UPGRADE_CONFIG.colors[activeColorKey].multiplier || 1.0;
		}
		bubbleValue *= colorMultiplier;
		// Calculate total jellyfish contribution with efficiency factor
		var efficiency = 0.3 + jellyfishLevel * 0.05; // Higher efficiency for jellyfish
		var jellyfishContribution = jellyfishPerSecond * bubblesPerJellyfish * bubbleValue * secondsAway * efficiency;
		// Add to total BP
		totalBP += jellyfishContribution;
	}
	// Round to integer - no additional penalty needed since it's built into the efficiency formula
	totalBP = Math.floor(totalBP);
	// More progressive cap system
	var minCap = 200;
	var maxCap = 100000; // Increased from 50000
	var totalClams = UPGRADE_CONFIG.machines.basicClam.amount + UPGRADE_CONFIG.machines.advancedClam.amount + UPGRADE_CONFIG.machines.premiumClam.amount;
	var percentCap = Math.min(0.85, 0.2 + UPGRADE_CONFIG.player.bubbleRefinement.currentLevel * 0.05 + UPGRADE_CONFIG.machine.bubbleQuality.currentLevel * 0.03 + totalClams / 12 * 0.1); // Add clam count influence
	var progressiveCap = Math.min(maxCap, Math.max(minCap, game.bp * percentCap));
	totalBP = Math.min(totalBP, progressiveCap);
	// Add the BP and show a message
	if (totalBP > 0) {
		game.bp += totalBP;
		bpText.setText(formatBP(game.bp) + " BP");
		game.showMessage("You earned " + formatBP(totalBP) + " BP while away!");
	}
}
game.faceTrackingEnabled = false;
var UPGRADE_CONFIG = {
	gameSettings: {
		activeColor: "auto" // Default to automatic progression (highest unlocked)
	},
	player: {
		lungCapacity: {
			name: "Lung Capacity",
			baseCost: 200,
			costScale: 3.5,
			maxLevel: 10,
			currentLevel: 0
		},
		quickBreath: {
			name: "Quick Breath",
			baseCost: 200,
			costScale: 3.5,
			maxLevel: 10,
			currentLevel: 0
		},
		autoPop: {
			name: "Fish Friends",
			baseCost: 400,
			costScale: 4,
			maxLevel: 6,
			currentLevel: 0
		},
		bubbleRefinement: {
			name: "Bubble Refinement",
			baseCost: 2500,
			costScale: 3.5,
			maxLevel: 10,
			currentLevel: 0
		},
		twinBubbles: {
			name: "Twin Bubbles",
			baseCost: 4000,
			costScale: 3,
			maxLevel: 8,
			currentLevel: 0
		},
		sizeVariance: {
			name: "Size Variance",
			baseCost: 5000,
			costScale: 3,
			maxLevel: 5,
			currentLevel: 0
		},
		jellyfish: {
			name: "Jellyfish Bloom",
			baseCost: 5000,
			costScale: 4,
			maxLevel: 6,
			currentLevel: 0
		},
		bubbleBlast: {
			name: "Bubble Blast",
			baseCost: 8000,
			costScale: 3.5,
			maxLevel: 5,
			currentLevel: 0
		},
		tentacleTide: {
			name: "Tentacle Tide",
			baseCost: 15000,
			costScale: 4,
			maxLevel: 6,
			currentLevel: 0
		}
	},
	machines: {
		basicClam: {
			name: "Basic Clam",
			baseCost: 300,
			costScale: 3,
			amount: 0,
			maxAmount: 4,
			// Add max amount
			production: 3,
			bubbleSize: 80
		},
		advancedClam: {
			name: "Advanced Clam",
			baseCost: 12000,
			costScale: 3.0,
			amount: 0,
			maxAmount: 4,
			// Add max amount
			production: 2,
			bubbleSize: 100,
			unlockCost: 12000,
			requires: "basicClam" // Add requirement
		},
		premiumClam: {
			name: "Premium Clam",
			baseCost: 80000,
			costScale: 3,
			amount: 0,
			maxAmount: 4,
			// Add max amount
			production: 1,
			bubbleSize: 150,
			unlockCost: 80000,
			requires: "advancedClam" // Add requirement
		}
	},
	machine: {
		bubbleDurability: {
			name: "Bubble Splitting",
			baseCost: 20000,
			costScale: 5,
			maxLevel: 3,
			currentLevel: 0
		},
		autoBubbleSpeed: {
			name: "Clam Speed",
			baseCost: 3000,
			costScale: 3.5,
			maxLevel: 8,
			currentLevel: 0
		},
		bubbleQuality: {
			name: "Bubble Quality",
			baseCost: 5000,
			costScale: 4,
			maxLevel: 8,
			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: 200000,
			costScale: 1.0,
			maxLevel: 1,
			currentLevel: 0,
			requires: "rainbowBubbles",
			multiplier: 1.8
		}
	},
	decorations: {
		sunkenTreasures: {
			name: "Sunken Treasures",
			baseCost: 25000,
			costScale: 2.5,
			amount: 0,
			maxAmount: 3
		}
	}
};
// Initialize upgrade registry for UI elements
game.upgradeRegistry = {};
// At the top level of your game, add a sound cooldown tracker
game.autoPopSoundCooldown = 0;
game.AUTO_POP_SOUND_COOLDOWN = 5; // frames to wait between auto-pop sounds
var background = LK.getAsset('background', {
	anchorX: 0.5,
	anchorY: 0.5,
	x: game.width / 2,
	y: game.height / 2
});
game.addChild(background);
// Add treasure container
game.titleMode = true; // Track if we're in title mode
game.showTitleScreen = function () {
	// Create title container to hold all title elements
	var titleContainer = new Container();
	game.titleContainer = titleContainer;
	game.addChild(titleContainer);
	if (LK.getSound('titlemusic').isPlaying) {
		LK.stopMusic('titlemusic');
	}
	// Try to play music with fade-in effect
	LK.playMusic('titlemusic', {
		fade: {
			start: 0,
			end: 0.5,
			// Slightly lower than in-game volume
			duration: 2000 // Longer fade-in for title screen
		}
	});
	// Add the logo that falls in from top
	var logo = LK.getAsset('titlebubble', {
		anchorX: 0.5,
		anchorY: 0.5,
		x: game.width / 2,
		y: -400,
		// Start above screen
		scaleX: 1,
		scaleY: 1
	});
	titleContainer.addChild(logo);
	// Animate logo falling in
	var savedActiveColor = storage.simpleUpgrades ? storage.simpleUpgrades.split(",")[13] || "auto" : "auto";
	var savedColorUnlocks = storage.colorUnlocks ? storage.colorUnlocks.split(",") : [];
	// Set temporary color settings for title screen
	game.titleColorSettings = {
		activeColor: savedActiveColor,
		colorUnlocks: savedColorUnlocks
	};
	tween(logo, {
		y: game.height / 2 - 300 // Stop at halfway mark
	}, {
		duration: 1500,
		easing: tween.easeOutElastic,
		onFinish: function onFinish() {
			// After logo animation completes, show the start button
			var startButton = LK.getAsset('startbutton', {
				anchorX: 0.5,
				anchorY: 0.5,
				x: game.width / 2,
				y: game.height / 2 + 700,
				scaleX: 0.01,
				// Start tiny
				scaleY: 0.01
			});
			titleContainer.addChild(startButton);
			// Animate button growing
			tween(startButton, {
				scaleX: 1,
				scaleY: 1
			}, {
				duration: 800,
				easing: tween.easeOutBack
			});
			// Add tap/click handler to button
			startButton.down = function () {
				LK.stopMusic(); // Stop title music immediately
				LK.getSound('startbutton').play(); // Play start button sound effect
				LK.effects.flashScreen(0xffffff, 500); // Add screen flash effect
				LK.setTimeout(function () {
					game.startGame();
				}, 500); // Reduce timeout for start game back to 500
				return true;
			};
		}
	});
};
var treasureContainer = new Container();
game.addChild(treasureContainer);
game.setChildIndex(treasureContainer, 1);
// Add clam container
var clamContainer = new Container();
game.addChild(clamContainer);
// Add player mask
var playerMask = new pufferMask();
game.addChild(playerMask);
playerMask.visible = false;
game.showTitleScreen();
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', 'bubbleBlast'], ['player', 'bubbleRefinement'], ['player', 'twinBubbles']],
		right: [['player', 'autoPop'], ['player', 'jellyfish'], ['player', 'sizeVariance'], ['machine', 'bubbleDurability'], ['player', 'tentacleTide']]
	},
	clams: {
		left: [['machines', 'basicClam'], ['machines', 'advancedClam'], ['machines', 'premiumClam']],
		right: [['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: {
		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;
}
// Add this new function outside of other functions
function applyTitleScreenColor(bubble) {
	if (!game.titleColorSettings) {
		return;
	}
	var activeColorKey = game.titleColorSettings.activeColor;
	var colorUnlocks = game.titleColorSettings.colorUnlocks;
	// Check if we have color unlocks
	if (colorUnlocks && colorUnlocks.length > 0) {
		// Find highest unlocked color
		var colorKeys = ["blueBubbles", "purpleBubbles", "greenBubbles", "orangeBubbles", "pinkBubbles", "tealBubbles", "goldBubbles", "crimsonBubbles", "silverBubbles", "rainbowBubbles", "prismaticBubbles"];
		var color = 0xFFFFFF; // Default white
		if (activeColorKey === "auto") {
			// Auto mode - find highest unlocked color
			for (var i = colorKeys.length - 1; i >= 0; i--) {
				if (i < colorUnlocks.length && parseInt(colorUnlocks[i]) > 0) {
					var key = colorKeys[i];
					if (UPGRADE_CONFIG.colors[key]) {
						color = UPGRADE_CONFIG.colors[key].color || 0xFFFFFF;
						break;
					}
				}
			}
		} else if (UPGRADE_CONFIG.colors[activeColorKey]) {
			// Specific color selected
			color = UPGRADE_CONFIG.colors[activeColorKey].color || 0xFFFFFF;
		}
		// Apply the color
		var bubbleSprite = bubble.children[0]; // Get the sprite of the bubble
		if (bubbleSprite) {
			bubbleSprite.tint = color;
		}
	}
}
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 30% bonus
		if (distance <= zone.radius) {
			totalBonus += 0.3; // +30% per overlapping zone
		}
	});
	// Return multiplier (1.0 = no bonus, 1.3 = one zone, 1.6 = 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: 3000,
		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: 3000,
		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);
menuContainer.visible = false;
// 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) {
	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
			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 the 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;
			// 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 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;
// 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);
bpText.visible = false;
// 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) {
	if (game.titleMode) {
		return;
	}
	var currentTime = LK.ticks;
	// Only update combo if it's not an auto-pop
	if (!isAutoPop) {
		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);
	// If menu is open, ensure popup is below menu
	if (menuOpen) {
		game.setChildIndex(pointText, game.getChildIndex(menuContainer) - 1);
	}
	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: 200,
		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') {
				// Check if this clam type is locked
				if (upgrade.requires) {
					var requiredType = UPGRADE_CONFIG.machines[upgrade.requires];
					if (requiredType && requiredType.amount < requiredType.maxAmount) {
						game.showError("Max out " + requiredType.name + " first!");
						return true;
					}
				}
				// Check if this clam type is already maxed
				if (upgrade.amount >= upgrade.maxAmount) {
					updateCostText(category, key, "SOLD OUT", 0x888888);
					return true;
				}
				// Standard purchase logic
				if (game.bp >= cost) {
					upgrade.amount++;
					if (game.tutorial.stage === 4) {
						game.tutorial.boughtClam = true; // We'll keep the same variable name for compatibility
						showTutorialPopup(5); // Show the final tutorial message
					}
					game.bp -= cost;
					LK.getSound('upgrade').play();
					bpText.setText(formatBP(game.bp) + " BP");
					// Update cost display
					if (upgrade.amount >= upgrade.maxAmount) {
						updateCostText(category, key, "SOLD OUT", 0x888888);
					} else {
						updateCostText(category, key, getUpgradeCost(upgrade) + " BP", 0xFFFF00);
					}
					// Update visuals
					updateClamVisuals();
					updateCostTexts('clams');
					saveGame();
					return true;
				} else {
					game.showError("Not enough BP!");
					return true;
				}
			}
			if (upgrade.amount !== undefined) {
				// For clams and decorations with amount
				if (upgrade.amount < (upgrade.maxAmount || 999)) {
					upgrade.amount++;
					if (upgrade.amount >= upgrade.maxAmount) {
						updateCostText(category, key, "SOLD OUT", 0x888888);
					}
					game.bp -= cost;
					LK.getSound('upgrade').play();
					bpText.setText(formatBP(game.bp) + " BP");
					// Check if we're at the max clam limit after this purchase
					var newTotalClams = UPGRADE_CONFIG.machines.basicClam.amount + UPGRADE_CONFIG.machines.advancedClam.amount + UPGRADE_CONFIG.machines.premiumClam.amount;
					// Force update all clam cost displays if we reached the limit
					if (newTotalClams >= 4) {
						updateCostText('machines', 'basicClam', "SOLD OUT", 0x888888);
					}
					updateCostText(category, key, getUpgradeCost(upgrade) + " BP", 0xFFFF00);
					// Update visuals
					if (category === 'machines') {
						updateClamVisuals();
						saveGame();
					} else if (category === 'decorations') {
						if (upgrade.amount < (upgrade.maxAmount || 999)) {
							upgrade.amount++;
							game.bp -= cost;
							LK.getSound('upgrade').play();
							bpText.setText(formatBP(game.bp) + " BP");
							// This is the key fix - use the correct method to update the text
							if (upgrade.amount >= upgrade.maxAmount) {
								updateCostText(category, key, "SOLD OUT", 0x888888);
							} else {
								updateCostText(category, key, getUpgradeCost(upgrade) + " BP", 0xFFFF00);
							}
							if (key === 'bubbleCoral') {
								updateCoralDecorations();
							} else if (key === 'sunkenTreasures') {
								updateTreasureDecorations();
								saveGame();
							}
						}
					}
				}
			} else if (upgrade.currentLevel < upgrade.maxLevel) {
				// For regular upgrades with levels
				upgrade.currentLevel++;
				game.bp -= cost;
				LK.getSound('upgrade').play();
				bpText.setText(formatBP(game.bp) + " BP");
				if (game.tutorial.stage === 4) {
					game.tutorial.boughtClam = true; // We'll keep the same variable name for compatibility
					showTutorialPopup(5); // Show the final tutorial message
				}
				saveGame();
				// 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);
		});
	}
	if (tabName === 'clams') {
		updateClamVisuals();
		updateCostTexts('clams');
	}
	// 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;
			// Special handling for basic clams - always check total count
			if (category === 'machines' && key === 'basicClam') {
				var totalClams = UPGRADE_CONFIG.machines.basicClam.amount + UPGRADE_CONFIG.machines.advancedClam.amount + UPGRADE_CONFIG.machines.premiumClam.amount;
				if (totalClams >= 4) {
					costText.setText("SOLD OUT");
					costText.fill = 0x888888;
					return; // Skip any other updates for basic clams
				}
			}
			// 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 upgrade = UPGRADE_CONFIG[category][key];
				// Check if this clam type is locked by requirements
				if (upgrade.requires) {
					var requiredType = UPGRADE_CONFIG.machines[upgrade.requires];
					if (requiredType && requiredType.amount < requiredType.maxAmount) {
						costText.setText("LOCKED");
						costText.fill = 0x888888;
						return;
					}
				}
				// Check if maxed out
				if (upgrade.amount >= upgrade.maxAmount) {
					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.25;
		var zoneIndicator = LK.getAsset('zoneIndicator', {
			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: 2.2,
			scaleY: 2.2,
			alpha: 0.9
		});
		// Add zone first (so it's behind treasure)
		treasureContainer.addChild(zoneIndicator);
		treasureContainer.addChild(treasure);
	}
}
function showTutorialPopup(stage) {
	// Set current stage
	game.tutorial.stage = stage;
	// Remove existing tutorial popup if any
	if (game.tutorialContainer) {
		game.tutorialContainer.destroy();
	}
	// Create new popup container
	game.tutorialContainer = new Container();
	game.addChild(game.tutorialContainer);
	game.setChildIndex(game.tutorialContainer, game.children.length - 1);
	// Create background
	var bg = LK.getAsset('blower', {
		width: game.width * 0.8,
		height: 500,
		color: 0x000066,
		shape: 'box',
		alpha: 0.8,
		anchorX: 0.5,
		anchorY: 0.5,
		tint: 0x000000 // Use tint to make it black
	});
	// Position above the menu tab
	bg.x = game.width / 2;
	bg.y = game.height - 600;
	game.tutorialContainer.addChild(bg);
	if (menuOpen) {
		// Position above the menu when it's open
		bg.y = game.height - 700; // Move higher up the screen
	}
	// Create tutorial text
	var message = "";
	switch (stage) {
		case 1:
			// Welcome
			message = "Welcome to Bubble Blower Tycoon! Open your mouth to start growing a bubble and close to release it. Go ahead and try!";
			break;
		case 2:
			// After blowing bubble
			message = "Great! Now go ahead and pop it to collect Bubble Points (BP).";
			break;
		case 3:
			// After popping
			message = "That's all there is to it! Now get popping!";
			break;
		case 4:
			// Can afford upgrade
			message = "It looks like you've saved enough BP. Open the 'Upgrades' menu at the bottom and choose any upgrade you can afford!";
			break;
		case 5:
			// After buying upgrade
			message = "Great choice! Continue exploring different upgrades to build your bubble empire. Good luck on becoming the Bubble Blower Tycoon!";
			break;
	}
	var tutorialText = new Text2(message, {
		size: 80,
		fill: 0xFFFFFF,
		stroke: 0x000000,
		strokeThickness: 3,
		font: "Impact",
		wordWrap: true,
		wordWrapWidth: game.width * 0.75
	});
	tutorialText.anchor = {
		x: 0.5,
		y: 0.5
	};
	tutorialText.x = bg.x;
	tutorialText.y = bg.y;
	game.tutorialContainer.addChild(tutorialText);
	// If final message, add a timer to close
	if (stage === 3 || stage === 5 || stage === 7) {
		LK.setTimeout(function () {
			if (game.tutorialContainer) {
				game.tutorialContainer.destroy();
				game.tutorialContainer = null;
			}
		}, 6000);
	}
}
// Function to update coral bubbles
// Function to update coral decorations
// Function to update clam visuals
function updateClamVisuals() {
	// Clear existing clams
	while (clamContainer.children.length) {
		clamContainer.children[0].destroy();
	}
	// Place clams - now much simpler
	game.clamSpawnPoints = [];
	// Process each clam type in order
	['basicClam', 'advancedClam', 'premiumClam'].forEach(function (clamType) {
		var clamCount = UPGRADE_CONFIG.machines[clamType].amount;
		for (var i = 0; i < clamCount; i++) {
			// Position logic for each clam instance
			var isRight = i % 2 === 1;
			var baseX = isRight ? game.width * 0.9 : game.width * 0.1;
			var direction = isRight ? -1 : 1;
			var position = Math.floor(i / 2);
			var spacing = 250;
			var x = baseX + direction * position * spacing;
			var y = game.height - 100;
			// Create clam sprite
			var sprite = LK.getAsset(clamType, {
				anchorX: 0.5,
				anchorY: 1,
				x: x,
				y: y,
				scaleX: isRight ? -0.5 : 0.5,
				scaleY: 0.5
			});
			// Store spawn point for bubble generation
			game.clamSpawnPoints.push({
				x: x + (isRight ? -75 : 75),
				y: y - 50,
				type: clamType,
				isRight: isRight
			});
			clamContainer.addChild(sprite);
		}
	});
}
// 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 autoBubbleLevel = UPGRADE_CONFIG.machine.autoBubbleSpeed.currentLevel;
		var baseEffect = UPGRADE_EFFECTS.autoBubbleSpeed.decrementPercent / 100;
		var effectiveLevel;
		if (autoBubbleLevel <= 3) {
			// First 3 levels at full effect
			effectiveLevel = autoBubbleLevel;
		} else if (autoBubbleLevel <= 6) {
			// Levels 4-6 at 75% effectiveness
			effectiveLevel = 3 + (autoBubbleLevel - 3) * 0.75;
		} else {
			// Levels 7+ at 50% effectiveness
			effectiveLevel = 3 + 3 * 0.75 + (autoBubbleLevel - 6) * 0.5;
		}
		var speedMultiplier = Math.pow(1 - baseEffect, effectiveLevel);
		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 = Math.min(0.5, 0.15 * variance); // Cap at +50% total
					// 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(200).fill(null).map(function () {
	return new Bubble();
});
game.popEffectPool = [];
var POP_EFFECT_COUNT = 30; // Increase slightly to handle more simultaneous pops
game.activeBubbles = [];
game.MAX_BUBBLES = 200; // Active bubble limit
// Add bubbles to game
game.bubblePool.forEach(function (bubble) {
	game.addChild(bubble);
});
for (var i = 0; i < POP_EFFECT_COUNT; i++) {
	var popEffect = LK.getAsset('zoneIndicator', {
		alpha: 0,
		visible: false,
		anchorX: 0.5,
		anchorY: 0.5
	});
	// Pre-set callback to avoid creating closures for each pop
	popEffect.reset = function () {
		this.visible = false;
		this.alpha = 0;
		this.activeTween = null;
	};
	game.addChild(popEffect);
	game.popEffectPool.push(popEffect);
}
// Set base spawn rate
game.baseSpawnRate = 180; // Every 3 seconds
// Adjust existing pop particle pool initialization for more visibility
for (var i = 0; i < game.MAX_POP_PARTICLES; i++) {
	var particle = LK.getAsset('zoneIndicator', {
		width: 15,
		height: 15,
		// Start with larger size
		alpha: 0,
		visible: false,
		anchorX: 0.5,
		anchorY: 0.5
	});
	game.addChild(particle);
	game.popParticlePool.push(particle);
}
// Enhanced pop effect function
game.createPopEffect = function (x, y, size, color) {
	// Find an available effect from the pool
	var popEffect = null;
	// First try to find a completely inactive effect
	for (var i = 0; i < game.popEffectPool.length; i++) {
		if (!game.popEffectPool[i].visible) {
			popEffect = game.popEffectPool[i];
			break;
		}
	}
	// If we couldn't find an inactive one, either use one that's fading out or skip
	if (!popEffect) {
		// Find effect that's almost complete (alpha < 0.3)
		for (var i = 0; i < game.popEffectPool.length; i++) {
			if (game.popEffectPool[i].alpha < 0.3) {
				popEffect = game.popEffectPool[i];
				// Cancel existing tween if any
				if (popEffect.activeTween) {
					popEffect.activeTween.stop();
					popEffect.activeTween = null;
				}
				break;
			}
		}
		// If still no effect available, just skip this effect
		if (!popEffect) {
			return;
		}
	}
	// Position and configure the effect
	popEffect.x = x;
	popEffect.y = y;
	popEffect.width = size;
	popEffect.height = size;
	popEffect.visible = true;
	popEffect.alpha = 0.8;
	popEffect.tint = color || 0xFFFFFF;
	// Create and store tween
	popEffect.activeTween = tween(popEffect, {
		alpha: 0,
		width: size * 1.15,
		height: size * 1.15
	}, {
		duration: 250,
		easing: tween.easeOut,
		onFinish: function onFinish() {
			popEffect.reset();
		}
	});
};
// 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;
	var parentBubble = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null;
	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);
	bubble.justSplit = true; // Mark as split for one frame
	// If this is a split from a bubble blast, make it temporarily immune to the parent
	if (parentBubble && parentBubble.isBubbleBlast) {
		bubble.parentBlastBubble = parentBubble;
		bubble.blastImmuneFrames = 60; // Immune for 5 frames
	}
	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.startGame = function () {
	// After fade-out completes, restart with new fade-in 
	if (LK.getSound('titlemusic').isPlaying) {
		LK.stopMusic('titlemusic');
	}
	LK.playMusic('backgroundmusic', {
		fade: {
			start: 0,
			end: 0.5,
			duration: 2000
		}
	});
	// Remove title screen
	if (game.titleContainer) {
		game.titleContainer.destroy();
		game.titleContainer = null;
	}
	// Exit title mode
	game.titleMode = false;
	// Load saved game data FIRST
	loadGame();
	// Initialize BP display
	bpText.visible = true;
	// Initialize pufferfish for animation (not visible yet)
	playerMask.visible = true;
	playerMask.scaleX = 0.2;
	playerMask.scaleY = 0.2;
	playerMask.alpha = 0.8;
	playerMask.x = game.width / 2;
	playerMask.y = game.height + 100;
	// Start animation sequence
	tween(playerMask, {
		x: game.width / 2,
		y: game.height / 2,
		scaleX: 0.7,
		scaleY: 0.7,
		alpha: 1
	}, {
		duration: 1500,
		easing: tween.easeOutBack,
		onFinish: function onFinish() {
			// Enable face tracking after animation
			game.faceTrackingEnabled = true;
			// Make menu visible
			menuContainer.visible = true;
			// Update clam visuals
			updateClamVisuals();
			updateTreasureDecorations();
		}
	});
	// Only show tutorial if not completed
	if (!storage.tutorialComplete) {
		LK.setTimeout(function () {
			showTutorialPopup(1);
		}, 60);
	}
};
game.tutorial = {
	stage: 0,
	// 0=none, 1=welcome, 2=blow bubble, 3=pop bubble, 4=first clam, 5=open menu, 6=clams tab, 7=buy clam, 8=final
	blownBubble: false,
	poppedBubble: false,
	boughtClam: false,
	tutorialBubble: null,
	minBubbleSize: 60 // Minimum size to consider a proper blown bubble
};
// Game update function
game.update = function () {
	// Update sound cooldowns
	if (game.autoPopSoundCooldown > 0) {
		game.autoPopSoundCooldown--;
	}
	if (!game.titleMode) {
		// Tutorial progression logic
		if (game.tutorial.stage === 1 && game.growingBubble === null && game.tutorial.blownBubble) {
			// Player released a bubble
			showTutorialPopup(2);
		}
		// Track if player has blown a proper bubble
		if (game.tutorial.stage === 1 && game.growingBubble && game.growingBubble.size >= game.tutorial.minBubbleSize) {
			game.tutorial.blownBubble = true;
		}
		// Check if we can afford first clam and haven't bought one yet
		if (game.tutorial.stage === 3 && game.bp >= UPGRADE_CONFIG.machines.basicClam.baseCost && UPGRADE_CONFIG.machines.basicClam.amount === 0) {
			showTutorialPopup(4);
		}
	}
	if (UPGRADE_CONFIG.player.tentacleTide.currentLevel > 0) {
		var spawnTime = Math.max(240, 720 - UPGRADE_CONFIG.player.tentacleTide.currentLevel * 90);
		if (LK.ticks % spawnTime === 0) {
			var tentacle = new Tentacle();
			game.addChild(tentacle);
			if (menuOpen) {
				game.setChildIndex(tentacle, game.getChildIndex(menuContainer) - 1);
			}
		}
	}
	if (game.titleMode) {
		// Just handle bubble spawning and updates during title
		// Random bubble spawning
		if (game.activeBubbles.length < game.MAX_BUBBLES) {
			if (LK.ticks % game.baseSpawnRate == 0) {
				var x = Math.random() * (game.width - 200) + 100;
				var titleBubble = spawnBubble(x, game.height + 100, 100, 0, true);
				// Apply saved color to the bubble if available
				if (titleBubble && game.titleColorSettings) {
					// Add this new code to immediately apply rainbow colors
					var activeColorKey = game.titleColorSettings.activeColor;
					var colorUnlocks = game.titleColorSettings.colorUnlocks;
					var isRainbowActive = activeColorKey === "rainbowBubbles" || activeColorKey === "auto" && colorUnlocks && colorUnlocks.length > 9 && parseInt(colorUnlocks[9]) > 0;
					if (isRainbowActive) {
						var rainbowColors = [0xFF0000, 0xFFA500, 0xFFFF00, 0x00FF00, 0x0000FF, 0xFF00FF];
						var colorChoice = Math.floor(Math.random() * rainbowColors.length);
						var bubbleSprite = titleBubble.children[0];
						if (bubbleSprite) {
							bubbleSprite.tint = rainbowColors[colorChoice];
							titleBubble.hasRainbowColor = true;
						}
					} else {
						applyTitleScreenColor(titleBubble);
					}
				}
			}
		}
		// Update all active bubbles
		game.activeBubbles.forEach(function (bubble) {
			if (bubble.visible && game.titleColorSettings) {
				var activeColorKey = game.titleColorSettings.activeColor;
				var colorUnlocks = game.titleColorSettings.colorUnlocks;
				// Check if rainbow/prismatic is active - either directly or via auto mode
				var isRainbowActive = activeColorKey === "rainbowBubbles" || activeColorKey === "auto" && colorUnlocks && colorUnlocks.length > 9 && parseInt(colorUnlocks[9]) > 0;
				var isPrismaticActive = activeColorKey === "prismaticBubbles" || activeColorKey === "auto" && colorUnlocks && colorUnlocks.length > 10 && parseInt(colorUnlocks[10]) > 0;
				// Apply the appropriate effect
				if (isPrismaticActive) {
					// Initialize or update color phase
					if (!bubble.colorPhase) {
						bubble.colorPhase = Math.random() * Math.PI * 2;
					}
					bubble.colorPhase += 0.02;
					var r = Math.sin(bubble.colorPhase) * 127 + 128;
					var g = Math.sin(bubble.colorPhase + 2) * 127 + 128;
					var b = Math.sin(bubble.colorPhase + 4) * 127 + 128;
					var color = (Math.floor(r) << 16) + (Math.floor(g) << 8) + Math.floor(b);
					var bubbleSprite = bubble.children[0];
					if (bubbleSprite) {
						bubbleSprite.tint = color;
					}
				} else if (isRainbowActive) {
					// For rainbow, assign random color when first created
					var bubbleSprite = bubble.children[0];
					if (bubbleSprite) {
						// Only assign a color if it hasn't been assigned yet
						if (!bubble.hasRainbowColor) {
							var rainbowColors = [0xFF0000, 0xFFA500, 0xFFFF00, 0x00FF00, 0x0000FF, 0xFF00FF];
							var colorChoice = Math.floor(Math.random() * rainbowColors.length);
							bubbleSprite.tint = rainbowColors[colorChoice];
							bubble.hasRainbowColor = true;
						}
						// Otherwise keep the color it already has
					}
				} else {
					// For all other colors, use the static method
					applyTitleScreenColor(bubble);
				}
			}
			if (bubble.update) {
				bubble.update();
			}
		});
		return; // Skip rest of update when in title mode
	}
	// 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) {
			// Fixed vertical offset - keep your preferred value
			var offsetY = playerMask.height * 0.15;
			// Convert rotation to radians
			var angle = playerMask.rotation * (Math.PI / 180);
			// Calculate rotated position - reduce the X rotation effect by applying a dampening factor
			var rotatedX = offsetY * Math.sin(angle) * -0.3; // Reduce horizontal movement by multiplying by 0.3
			var rotatedY = offsetY * Math.cos(angle);
			var spawnX = playerMask.x + rotatedX;
			var spawnY = playerMask.y + rotatedY;
			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) {
			// Fixed vertical offset
			var offsetY = playerMask.height * 0.15;
			// Convert rotation to radians
			var angle = playerMask.rotation * (Math.PI / 180);
			// Calculate rotated position with dampening factor on X
			var rotatedX = offsetY * Math.sin(angle) * -0.3; // Dampen horizontal movement
			var rotatedY = offsetY * Math.cos(angle);
			// Update bubble position
			game.growingBubble.x = playerMask.x + rotatedX;
			game.growingBubble.y = playerMask.y + rotatedY;
			game.growingBubble.size = Math.min(game.growingBubble.size + game.growthRate, game.maxBubbleSize);
			game.growingBubble.verticalVelocity = 0;
			game.growingBubble.driftX = 0;
		}
	} else {
		if (game.growingBubble) {
			LK.getSound('bubbleshoot').play(); // Play bloop sound effect when bubble is released
			// 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;
			// Clear any existing blast properties first
			game.growingBubble.isBubbleBlast = false;
			game.growingBubble.isBlastFragment = false;
			game.growingBubble.blastImmuneFrames = 0;
			// Then apply bubble blast if upgraded
			if (UPGRADE_CONFIG.player.bubbleBlast.currentLevel > 0) {
				game.growingBubble.isBubbleBlast = true;
				var bubbleSprite = game.growingBubble.children[0];
				if (bubbleSprite) {
					bubbleSprite.alpha = 0.85;
					// Preserve/enhance existing color
					if (game.growingBubble.colorTint !== 0xFFFFFF) {
						var r = Math.min(255, (game.growingBubble.colorTint >> 16 & 0xFF) + 40);
						var g = Math.min(255, (game.growingBubble.colorTint >> 8 & 0xFF) + 40);
						var b = Math.min(255, (game.growingBubble.colorTint & 0xFF) + 40);
						bubbleSprite.tint = r << 16 | g << 8 | b;
					} else {
						bubbleSprite.tint = 0xAADDFF;
					}
				}
			}
			var twinLevel = UPGRADE_CONFIG.player.twinBubbles.currentLevel;
			if (twinLevel > 0 && Math.random() < twinLevel * 0.05 + 0.10) {
				// 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) {
					// Set twin flags
					game.growingBubble.setTwin();
					twinBubble.setTwin();
					twinBubble.visible = true;
					// Give temporary immunity
					twinBubble.blastImmuneFrames = 60;
					// Create twin pair object
					var twinPairObject = {
						bubble1: game.growingBubble,
						bubble2: twinBubble,
						popped: false,
						timestamp: LK.ticks
					};
					// Store direct reference in both bubbles
					game.growingBubble.twinPair = twinPairObject;
					twinBubble.twinPair = twinPairObject;
					// Link the bubbles in the game's tracking array
					game.twinBubbles.push(twinPairObject);
				}
			}
			game.growingBubble = null;
			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) {
		if (LK.ticks % Math.max(60, 960 - UPGRADE_CONFIG.player.autoPop.currentLevel * 120) === 0) {
			var fish = new Fish();
			game.addChild(fish);
			// If menu is open, ensure fish is below menu
			if (menuOpen) {
				game.setChildIndex(fish, game.getChildIndex(menuContainer) - 1);
			}
		}
	}
	// Jellyfish spawning
	if (UPGRADE_CONFIG.player.jellyfish.currentLevel > 0) {
		if (LK.ticks % Math.max(60, 960 - UPGRADE_CONFIG.player.jellyfish.currentLevel * 120) === 0) {
			var jellyfish = new Jellyfish();
			game.addChild(jellyfish);
			// If menu is open, ensure jellyfish is below menu
			if (menuOpen) {
				game.setChildIndex(jellyfish, game.getChildIndex(menuContainer) - 1);
			}
		}
	}
	// 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
	// Remove expired twin bubble pairs without recreating the array
	var i = 0;
	while (i < game.twinBubbles.length) {
		var pair = game.twinBubbles[i];
		// Remove pairs where one or both bubbles are invisible or time expired
		if (!pair.bubble1.visible || !pair.bubble2.visible || pair.popped && LK.ticks - pair.timestamp > 60) {
			// Fast removal by swapping with last element and reducing length
			game.twinBubbles[i] = game.twinBubbles[game.twinBubbles.length - 1];
			game.twinBubbles.length--;
		} else {
			i++;
		}
	}
	// Update all active bubbles
	game.activeBubbles.forEach(function (bubble) {
		if (bubble.update) {
			bubble.update();
		}
	});
	if (LK.ticks % 1800 === 0) {
		saveGame();
	}
};
// Handle touch/mouse events for the game
// Replace the game.down function with this improved version
game.down = function (x, y, obj) {
	if (game.titleMode) {
		return false; // Let containers handle their own clicks
	}
	// Check for bubbles first throughout the entire screen
	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 (distance <= bubble.size / 2 + 50 && bubble.down) {
			bubble.down();
			popped = true;
			break;
		}
	}
	// If we popped a bubble, don't check for menu interaction
	if (popped) {
		return true;
	}
	// Now check for menu tab interaction
	var localX = x - menuContainer.x;
	var localY = y - menuContainer.y;
	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) {
		LK.getSound('menuopen').play();
		menuOpen = !menuOpen;
		if (game.tutorial.stage === 4 && menuOpen) {
			// No need to advance to stage 5 anymore since we removed it
			// Just ensure the tutorial is on top
			if (game.tutorialContainer) {
				game.setChildIndex(game.tutorialContainer, game.children.length - 1);
			}
		}
		var targetY = menuOpen ? menuTab.height : game.height;
		if (menuOpen) {
			game.setChildIndex(menuContainer, game.children.length - 1);
			// If we're in tutorial, make sure tutorial popup is above menu
			if (game.tutorialContainer) {
				LK.setTimeout(function () {
					game.setChildIndex(game.tutorialContainer, game.children.length - 1);
				}, 1); // Use tiny delay to run after current frame completes
			}
		}
		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
	}
};
// Initialize decoration visuals
updateClamVisuals();
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(); ===================================================================
--- original.js
+++ change.js
@@ -827,9 +827,9 @@
 						// The base of the tentacle is at the anchor point (self.x, self.y)
 						var baseX = self.x;
 						var baseY = self.y;
 						// Calculate the effective length for collision detection (80% of full height)
-						var effectiveLength = tentacleFullHeight * 0.8; // 800 pixels
+						var effectiveLength = tentacleFullHeight * 0.95; // 800 pixels
 						// Calculate the tip position - this needs to account for the anchor point AND rotation
 						// The (0,0) of sprite is at (self.x, self.y), and we need to go in the rotated -y direction
 						var tipX = baseX + Math.sin(rotation) * effectiveLength;
 						var tipY = baseY - Math.cos(rotation) * effectiveLength;
 A white bubble with a black outline 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
 
 A outstretched straight octopus tentacle. Green with purple suckers. Cartoon.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
 
 
 
 A colorful underwater coral reef background. Cartoon Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 A white bubble with a black outline. Pixel art.. 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