User prompt
update as needed with: // Upgrade Menu Configuration var UPGRADE_CONFIG = { player: { lungCapacity: { name: "Lung Capacity", baseCost: 50, costScale: 2, maxLevel: 10, currentLevel: 0 }, quickBreath: { name: "Quick Breath", baseCost: 75, costScale: 2, maxLevel: 10, currentLevel: 0 } }, machine: { bubbleDurability: { name: "Bubble Durability", baseCost: 200, costScale: 3, maxLevel: 5, currentLevel: 0 }, machineSpeed: { name: "Machine Speed", baseCost: 150, costScale: 2.5, maxLevel: 10, currentLevel: 0 }, autoPop: { name: "Auto-Pop", baseCost: 500, costScale: 2, maxLevel: 5, currentLevel: 0 } } }; // Create upgrade menu elements var menuTab = LK.getAsset('upgradetab', { anchorX: 0.5, anchorY: 1, x: game.width / 2, y: game.height, alpha: 0.9 }); var menuText = new Text2("Upgrades", { size: 48, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2, font: "Impact" }); menuText.anchorX = 0.5; menuText.anchorY = 0.5; menuText.x = menuTab.width / 2; menuText.y = menuTab.height / 2; menuTab.addChild(menuText); var menuPanel = LK.getAsset('upgradetab', { anchorX: 0.5, anchorY: 1, x: game.width / 2, y: game.height, alpha: 0.5, scaleX: game.width / 67, // Adjust to game width scaleY: game.height * 0.25 / 100.3 // 25% of game height }); // Add upgrade text elements var upgradeTexts = []; var startY = 50; var columnWidth = game.width / 2; Object.entries(UPGRADE_CONFIG).forEach(([category, upgrades], categoryIndex) => { Object.entries(upgrades).forEach(([key, upgrade], index) => { var x = categoryIndex * columnWidth + 20; var y = startY + index * 60; var text = new Text2(upgrade.name + " - " + upgrade.baseCost + " BP", { size: 36, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2, font: "Impact" }); text.x = x; text.y = y; text.upgrade = key; upgradeTexts.push(text); menuPanel.addChild(text); }); }); // Menu state and animation var menuOpen = false; var menuTargetY = game.height; game.addChild(menuPanel); game.addChild(menuTab); // Extend the existing down handler var originalDown = game.down; game.down = function(x, y, obj) { // Check if clicked on menu tab var tabBounds = { x: menuTab.x - menuTab.width / 2, y: menuTab.y - menuTab.height, width: menuTab.width, height: menuTab.height }; if (x >= tabBounds.x && x <= tabBounds.x + tabBounds.width && y >= tabBounds.y && y <= tabBounds.y + tabBounds.height) { menuOpen = !menuOpen; var targetY = menuOpen ? game.height - menuPanel.height : game.height; tween(menuPanel, { y: targetY }, { duration: 300 }); tween(menuTab, { y: targetY }, { duration: 300 }); return true; } // Close menu if clicking outside when open if (menuOpen) { var menuBounds = { x: menuPanel.x - menuPanel.width / 2, y: menuPanel.y - menuPanel.height, width: menuPanel.width, height: menuPanel.height }; if (!(x >= menuBounds.x && x <= menuBounds.x + menuBounds.width && y >= menuBounds.y && y <= menuBounds.y + menuBounds.height)) { menuOpen = false; tween(menuPanel, { y: game.height }, { duration: 300 }); tween(menuTab, { y: game.height }, { duration: 300 }); return true; } } // Call original down handler for bubble popping return originalDown(x, y, obj); };
User prompt
initialize the background before the menu
User prompt
update with: // Upgrade Menu Configuration var UPGRADE_CONFIG = { player: { lungCapacity: { name: "Lung Capacity", baseCost: 50, costScale: 2, maxLevel: 10, currentLevel: 0 }, quickBreath: { name: "Quick Breath", baseCost: 75, costScale: 2, maxLevel: 10, currentLevel: 0 } }, machine: { bubbleDurability: { name: "Bubble Durability", baseCost: 200, costScale: 3, maxLevel: 5, currentLevel: 0 }, machineSpeed: { name: "Machine Speed", baseCost: 150, costScale: 2.5, maxLevel: 10, currentLevel: 0 }, autoPop: { name: "Auto-Pop", baseCost: 500, costScale: 2, maxLevel: 5, currentLevel: 0 } } }; // Create upgrade menu elements var menuTab = LK.getAsset('upgradetab', { anchorX: 0.5, anchorY: 1, x: game.width / 2, y: game.height, alpha: 0.9 }); var menuText = new Text2("Upgrades", { size: 48, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2, font: "Impact" }); menuText.anchorX = 0.5; menuText.anchorY = 0.5; menuText.x = menuTab.width / 2; menuText.y = menuTab.height / 2; menuTab.addChild(menuText); var menuPanel = LK.getAsset('upgradetab', { anchorX: 0.5, anchorY: 1, x: game.width / 2, y: game.height, alpha: 0.5, scaleX: game.width / 67, // Adjust to game width scaleY: game.height * 0.25 / 100.3 // 25% of game height }); // Add upgrade text elements var upgradeTexts = []; var startY = 50; var columnWidth = game.width / 2; Object.entries(UPGRADE_CONFIG).forEach(([category, upgrades], categoryIndex) => { Object.entries(upgrades).forEach(([key, upgrade], index) => { var x = categoryIndex * columnWidth + 20; var y = startY + index * 60; var text = new Text2(upgrade.name + " - " + upgrade.baseCost + " BP", { size: 36, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2, font: "Impact" }); text.x = x; text.y = y; text.upgrade = key; upgradeTexts.push(text); menuPanel.addChild(text); }); }); // Menu state and animation var menuOpen = false; var menuTargetY = game.height; game.addChild(menuPanel); game.addChild(menuTab);
User prompt
add: // Upgrade Menu Configuration var UPGRADE_CONFIG = { player: { lungCapacity: { name: "Lung Capacity", baseCost: 50, costScale: 2, maxLevel: 10, currentLevel: 0 }, quickBreath: { name: "Quick Breath", baseCost: 75, costScale: 2, maxLevel: 10, currentLevel: 0 } }, machine: { bubbleDurability: { name: "Bubble Durability", baseCost: 200, costScale: 3, maxLevel: 5, currentLevel: 0 }, machineSpeed: { name: "Machine Speed", baseCost: 150, costScale: 2.5, maxLevel: 10, currentLevel: 0 }, autoPop: { name: "Auto-Pop", baseCost: 500, costScale: 2, maxLevel: 5, currentLevel: 0 } } }; // Create upgrade menu elements var menuTab = LK.getAsset('upgradetab', { anchorX: 0.5, anchorY: 1, x: game.width / 2, y: game.height, alpha: 0.9 }); var menuText = new Text2("Upgrades", { size: 48, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2, font: "Impact" }); menuText.anchorX = 0.5; menuText.anchorY = 0.5; menuText.x = menuTab.width / 2; menuText.y = menuTab.height / 2; menuTab.addChild(menuText); var menuPanel = LK.getAsset('upgradetab', { anchorX: 0.5, anchorY: 1, x: game.width / 2, y: game.height, alpha: 0.5, scaleX: game.width / 67, // Adjust to game width scaleY: game.height * 0.25 / 100.3 // 25% of game height }); // Add upgrade text elements var upgradeTexts = []; var startY = 50; var columnWidth = game.width / 2; Object.entries(UPGRADE_CONFIG).forEach(([category, upgrades], categoryIndex) => { Object.entries(upgrades).forEach(([key, upgrade], index) => { var x = categoryIndex * columnWidth + 20; var y = startY + index * 60; var text = new Text2(upgrade.name + " - " + upgrade.baseCost + " BP", { size: 36, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2, font: "Impact" }); text.x = x; text.y = y; text.upgrade = key; upgradeTexts.push(text); menuPanel.addChild(text); }); }); // Menu state and animation var menuOpen = false; var menuTargetY = game.height; game.addChild(menuPanel); game.addChild(menuTab); // Extend the existing down handler var originalDown = game.down; game.down = function(x, y, obj) { // Check if clicked on menu tab var tabBounds = { x: menuTab.x - menuTab.width / 2, y: menuTab.y - menuTab.height, width: menuTab.width, height: menuTab.height }; if (x >= tabBounds.x && x <= tabBounds.x + tabBounds.width && y >= tabBounds.y && y <= tabBounds.y + tabBounds.height) { menuOpen = !menuOpen; var targetY = menuOpen ? game.height - menuPanel.height : game.height; tween(menuPanel, { y: targetY }, { duration: 300 }); tween(menuTab, { y: targetY }, { duration: 300 }); return true; } // Close menu if clicking outside when open if (menuOpen) { var menuBounds = { x: menuPanel.x - menuPanel.width / 2, y: menuPanel.y - menuPanel.height, width: menuPanel.width, height: menuPanel.height }; if (!(x >= menuBounds.x && x <= menuBounds.x + menuBounds.width && y >= menuBounds.y && y <= menuBounds.y + menuBounds.height)) { menuOpen = false; tween(menuPanel, { y: game.height }, { duration: 300 }); tween(menuTab, { y: game.height }, { duration: 300 }); return true; } } // Call original down handler for bubble popping return originalDown(x, y, obj); };
Code edit (1 edits merged)
Please save this source code
User prompt
update as needed with: // Add near other game initialization variables game.mouthTracking = { lastY: 0, centerY: 0, blowThreshold: 5, cooldown: 0, COOLDOWN_FRAMES: 10 }; // Replace the mouth detection section in game.update if (facekit.mouthCenter) { // Track mouth center movement game.mouthTracking.lastY = game.mouthTracking.centerY; game.mouthTracking.centerY = facekit.mouthCenter.y; // Calculate mouth movement (positive = mouth closing) var mouthMovement = game.mouthTracking.centerY - game.mouthTracking.lastY; // Detect quick closing motion (blowing gesture) if (mouthMovement > game.mouthTracking.blowThreshold && game.mouthTracking.cooldown === 0) { // Create bubble var bubble = new Bubble(); bubble.x = facekit.mouthCenter.x; bubble.y = facekit.mouthCenter.y; bubble.size = 50 + Math.min(50, Math.abs(mouthMovement * 2)); bubble.verticalVelocity = -Math.max(5, Math.abs(mouthMovement)); bubble.driftX = (Math.random() * 2 - 1) * 2; game.addChild(bubble); game.bubbles.push(bubble); // Set cooldown game.mouthTracking.cooldown = game.mouthTracking.COOLDOWN_FRAMES; } // Update cooldown if (game.mouthTracking.cooldown > 0) { game.mouthTracking.cooldown--; } } βͺπ‘ Consider importing and using the following plugins: @upit/facekit.v1
User prompt
update as needed with: // Add near other game initialization variables game.mouthState = { isOpen: false, openWidth: 0, currentWidth: 0, blowCooldown: 0, COOLDOWN_FRAMES: 15 // Prevent rapid-fire blowing }; // Replace the mouth detection section in game.update if (facekit.mouthCenter && facekit.mouthLeft && facekit.mouthRight) { var currentWidth = Math.abs(facekit.mouthLeft.x - facekit.mouthRight.x); var currentHeight = Math.abs(facekit.mouthTop.y - facekit.mouthBottom.y); var mouthRatio = currentHeight / currentWidth; // Track mouth states if (mouthRatio > 0.7) { // Mouth is clearly open game.mouthState.isOpen = true; game.mouthState.openWidth = currentWidth; } else if (game.mouthState.isOpen && mouthRatio < 0.3 && game.mouthState.blowCooldown === 0) { // Transition from open to nearly closed - this is a "blow" game.mouthState.isOpen = false; game.mouthState.blowCooldown = game.mouthState.COOLDOWN_FRAMES; // Create and launch bubble var bubble = new Bubble(); bubble.x = playerMask.x; bubble.y = playerMask.y + playerMask.height * 0.15; bubble.size = Math.min(100, game.mouthState.openWidth); bubble.verticalVelocity = -8; bubble.driftX = (Math.random() * 2 - 1) * 2; game.addChild(bubble); game.bubbles.push(bubble); } // Update cooldown if (game.mouthState.blowCooldown > 0) { game.mouthState.blowCooldown--; } } βͺπ‘ Consider importing and using the following plugins: @upit/facekit.v1
User prompt
update with: // Add near other game initialization variables game.mouthState = { isOpen: false, openWidth: 0, currentWidth: 0, blowCooldown: 0, COOLDOWN_FRAMES: 15 // Prevent rapid-fire blowing }; // Replace the mouth detection section in game.update if (facekit.mouthCenter && facekit.mouthLeft && facekit.mouthRight) { var currentWidth = Math.abs(facekit.mouthLeft.x - facekit.mouthRight.x); var currentHeight = Math.abs(facekit.mouthTop.y - facekit.mouthBottom.y); var mouthRatio = currentHeight / currentWidth; // Track mouth states if (mouthRatio > 0.7) { // Mouth is clearly open game.mouthState.isOpen = true; game.mouthState.openWidth = currentWidth; } else if (game.mouthState.isOpen && mouthRatio < 0.3 && game.mouthState.blowCooldown === 0) { // Transition from open to nearly closed - this is a "blow" game.mouthState.isOpen = false; game.mouthState.blowCooldown = game.mouthState.COOLDOWN_FRAMES; // Create and launch bubble var bubble = new Bubble(); bubble.x = playerMask.x; bubble.y = playerMask.y + playerMask.height * 0.15; bubble.size = Math.min(100, game.mouthState.openWidth); bubble.verticalVelocity = -8; bubble.driftX = (Math.random() * 2 - 1) * 2; game.addChild(bubble); game.bubbles.push(bubble); } // Update cooldown if (game.mouthState.blowCooldown > 0) { game.mouthState.blowCooldown--; } } βͺπ‘ Consider importing and using the following plugins: @upit/facekit.v1
User prompt
update with: // Replace the if(facekit.mouthOpen) section in game.update if (facekit.mouthCenter && facekit.mouthLeft && facekit.mouthRight) { // Calculate current mouth width var currentWidth = Math.abs(facekit.mouthLeft.x - facekit.mouthRight.x); // Establish baseline during calibration if (game.calibrationFrames < game.CALIBRATION_PERIOD) { if (!game.baselineWidth) game.baselineWidth = currentWidth; game.baselineWidth = game.baselineWidth * 0.95 + currentWidth * 0.05; game.calibrationFrames++; return; } // Check for blowing gesture (pursed lips) var widthRatio = currentWidth / game.baselineWidth; var isBlowing = widthRatio < game.BLOW_THRESHOLD; if (isBlowing) { // Existing bubble growing logic var spawnX = playerMask.x; var spawnY = playerMask.y + playerMask.height * 0.15; if (!game.growingBubble) { game.growingBubble = new Bubble(); game.growingBubble.size = 25; game.addChild(game.growingBubble); game.bubbles.push(game.growingBubble); } if (game.growingBubble) { game.growingBubble.x = spawnX; game.growingBubble.y = spawnY; game.growingBubble.size = Math.min( game.growingBubble.size + game.growthRate, game.maxBubbleSize ); game.growingBubble.verticalVelocity = 0; game.growingBubble.driftX = 0; } } else if (game.growingBubble) { // Release bubble when stopped blowing game.growingBubble.verticalVelocity = -12; game.growingBubble.driftX = (Math.random() * 2 - 1) * 2.5; game.growingBubble = null; } game.lastMouthWidth = currentWidth; }
User prompt
update with: game.lastMouthWidth = 0; game.baselineWidth = null; game.BLOW_THRESHOLD = 0.6; // Mouth must be 60% narrower than baseline to count as blowing game.calibrationFrames = 0; game.CALIBRATION_PERIOD = 60; // 1 second to establish baseline
User prompt
set bubble alpha to 0.9
Code edit (2 edits merged)
Please save this source code
User prompt
use impact font for bp pop ups
User prompt
use impact font and make bp display bold
Code edit (1 edits merged)
Please save this source code
User prompt
increase size of bp display
User prompt
set background asset as background, one layer behind puffermask
User prompt
update bubble with: var points = self.getBP(); game.addBP(points, self.x, self.y, false); // Pass position and flag for manual pop
Code edit (1 edits merged)
Please save this source code
User prompt
update with: game.addBP = function(points, x, y, isAutoPop) { var currentTime = LK.ticks; // Only update combo if it's not an auto-pop if (!isAutoPop) { if (currentTime - game.lastPopTime < game.COMBO_WINDOW) { game.combo++; points *= 1 + game.combo * 0.1; // 10% bonus per combo } else { game.combo = 0; } game.lastPopTime = currentTime; } game.bp += Math.floor(points); bpText.setText(formatBP(game.bp) + " BP"); // Always show point text regardless of auto or manual pop var pointText = new Text2("+" + Math.floor(points), { size: 80, fill: 0xFFFF00, stroke: 0x000000, strokeThickness: 4, fontWeight: 'bold' }); pointText.anchorX = 0.5; pointText.anchorY = 0.5; pointText.x = x; pointText.y = y; game.addChild(pointText); tween(pointText, { y: pointText.y - 100, alpha: 0 }, { duration: 1000, 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(); } }); } }; βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
update with: game.addBP = function(points, x, y, isAutoPop) { var currentTime = LK.ticks; // Only update combo if it's not an auto-pop if (!isAutoPop) { if (currentTime - game.lastPopTime < game.COMBO_WINDOW) { game.combo++; points *= 1 + game.combo * 0.1; // 10% bonus per combo } else { game.combo = 0; } game.lastPopTime = currentTime; } game.bp += Math.floor(points); bpText.setText(formatBP(game.bp) + " BP"); // Only show combo 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(); } }); } }; βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
update with: self.autoPop = function() { if (!self.autoPopDisplayed) { var points = Math.floor(self.getBP() * 0.5); // Half points for auto-pop game.addBP(points, self.x, self.y, true); // Added true flag for autoPop self.autoPopDisplayed = true; } self.destroy(); return; };
Code edit (1 edits merged)
Please save this source code
User prompt
update as needed with: if (game.combo > 0) { var comboText = new Text2("x" + (game.combo + 1), { size: 96, // Much bigger fill: 0xFFA500, stroke: 0x000000, strokeThickness: 4, fontWeight: 'bold' }); comboText.anchorX = 0.5; comboText.anchorY = 0; comboText.x = game.width / 2; // Center of screen comboText.y = 20; // Near top game.addChild(comboText); tween(comboText, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { comboText.destroy(); } }); } βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
update with: // Inside game.addBP function, after the points text animation: if (game.combo > 0) { var comboText = new Text2("x" + (game.combo + 1), { size: 48, // Increased from 24 for better visibility fill: 0xFFA500, stroke: 0x000000, strokeThickness: 4, fontWeight: 'bold' }); comboText.anchorX = 1; comboText.anchorY = 0; comboText.x = bpText.x - 10; // Position it to the left of BP counter comboText.y = bpText.y + 50; // Position it below BP counter game.addChild(comboText); tween(comboText, { alpha: 0, y: comboText.y - 50 // Float upward as it fades }, { duration: 500, onFinish: function onFinish() { comboText.destroy(); } }); } βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
/**** 
* Plugins
****/ 
var tween = LK.import("@upit/tween.v1");
var facekit = LK.import("@upit/facekit.v1");
/**** 
* Classes
****/ 
// Bubble class to represent each bubble in the game
var Bubble = Container.expand(function () {
	var self = Container.call(this);
	self.lifetime = 0;
	self.hasSplit = false;
	self.splitHeight = null;
	self.AUTO_POP_SIZE = 40;
	self.MIN_SPLIT_SIZE = 30;
	self.lastPopTime = 0; // Add timestamp tracking
	var sprite = self.attachAsset('bubble', {
		anchorX: 0.5,
		anchorY: 0.5,
		alpha: 0.9
	});
	self.size = 100;
	self.initLifetime = function () {
		self.maxLifetime = Math.floor(Math.random() * 960 + 1440);
		self.maxLifetime *= Math.min(1, self.size / 100);
	};
	self.initLifetime();
	// Subtle size-based variance plus small random factor
	var speedMultiplier = 120 / self.size * (0.9 + Math.random() * 0.2); // Just 10% variance
	self.floatSpeed = 50 * speedMultiplier / 60;
	self.driftX = (Math.random() * 20 - 10) / 60; // Normal drift variance
	self.verticalVelocity = 0;
	self.down = function (e) {
		// Add cooldown check (100ms)
		var currentTime = Date.now();
		if (currentTime - self.lastPopTime < 100) {
			return true; // Ignore clicks too close together
		}
		self.lastPopTime = currentTime;
		var index = game.bubbles.indexOf(self);
		if (index > -1) {
			game.bubbles.splice(index, 1);
		}
		var points = self.getBP();
		game.addBP(points, self.x, self.y, false); // Pass position and flag for manual pop
		// Only split if manually popped and large enough
		if (self.size > 60 && !self.justSplit) {
			var newSize = Math.max(self.MIN_SPLIT_SIZE, self.size * 0.6);
			for (var i = 0; i < 2; i++) {
				spawnSplitBubble(self.x, self.y, newSize, i === 0 ? -1 : 1);
			}
		}
		self.destroy();
		return true; // Stop event propagation
	};
	self.getBP = function () {
		return Math.floor(Math.pow(self.size, 2) * 0.1);
	};
	self.update = function () {
		self.lifetime++;
		// Add subtle drift variation
		if (self.lifetime % 60 === 0) {
			// Every second
			self.driftX += (Math.random() - 0.5) * 0.3; // Add small random drift
		}
		// Increase overall horizontal movement
		self.x += self.driftX * 1.2; // 20% more horizontal movement
		// Auto-pop or split when lifetime exceeded
		if (self.lifetime > self.maxLifetime) {
			// Just check size and not already split
			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 = spawnSplitBubble(self.x, self.y, newSize, i === 0 ? -1 : 1, true);
					split.maxLifetime *= 0.7; // Shorter lifetime for split bubbles
				}
				self.destroy();
				return;
			}
			// If too small to split, just pop
			self.autoPop();
			return;
		}
		// Clear off-screen bubbles
		if (self.y < -self.size) {
			self.destroy();
			return;
		}
		self.justSplit = false; // Clear the flag after first update
		// More gradual vertical speed transition
		if (self.verticalVelocity < self.floatSpeed) {
			self.verticalVelocity += 0.08; // More gentle transition
		}
		self.y -= self.verticalVelocity;
		// Gradually reduce horizontal speed after split
		if (Math.abs(self.driftX) > (Math.random() * 20 - 10) / 60) {
			self.driftX *= 0.98; // Slowly return to normal drift speed
		}
		self.x += self.driftX;
		// Bounce off edges
		if (self.x < self.size) {
			self.x = self.size;
			self.driftX = Math.abs(self.driftX);
		} else if (self.x > game.width - self.size) {
			self.x = game.width - self.size;
			self.driftX = -Math.abs(self.driftX);
		}
		var scale = self.size / sprite.width;
		sprite.scaleX = scale;
		sprite.scaleY = scale;
	};
	self.autoPop = function () {
		if (!self.autoPopDisplayed) {
			var points = Math.floor(self.getBP() * 0.5); // Half points for auto-pop
			game.addBP(points, self.x, self.y, true); // Added true flag for autoPop
			self.autoPopDisplayed = true;
		}
		self.destroy();
		return;
	};
	return self;
});
// Pufferfish mask that follows face
var pufferMask = Container.expand(function () {
	var self = Container.call(this);
	var sprite = self.attachAsset('pufferfish', {
		anchorX: 0.5,
		anchorY: 0.5
	});
	var targetX = 0;
	var targetY = 0;
	var smoothingFactor = 0.12;
	var prevX = null;
	var prevY = null;
	var targetRotation = 0;
	var rotationSmoothingFactor = 0.1;
	var targetTilt = 0;
	var tiltSmoothingFactor = 0.11; // Reduced from 0.08 for smoother movement
	var tiltScaleFactor = 0.09; // Reduced from 0.15 for less tilt
	var scaleHistory = new Array(5).fill(0); // Keep last 5 scale values
	var scaleIndex = 0;
	var baseScale = 1;
	var minScale = 0.1;
	var maxScale = 3;
	self.update = function () {
		// Adjust scale based on face size
		if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) {
			var eyeDistance = Math.abs(facekit.rightEye.x - facekit.leftEye.x);
			var newScale = eyeDistance / 500;
			// Update rolling average
			scaleHistory[scaleIndex] = newScale;
			scaleIndex = (scaleIndex + 1) % scaleHistory.length;
			// Calculate average scale
			var avgScale = scaleHistory.reduce(function (a, b) {
				return a + b;
			}, 0) / scaleHistory.length;
			// More gentle smoothing
			sprite.scaleX = sprite.scaleX * 0.85 + avgScale * 0.15;
			sprite.scaleY = sprite.scaleY * 0.85 + avgScale * 0.15;
		}
		// Follow nose position for main face tracking
		if (facekit.noseTip) {
			targetX = facekit.noseTip.x;
			targetY = facekit.noseTip.y;
			// Initialize previous positions if not set
			if (prevX === null) {
				prevX = targetX;
				prevY = targetY;
			}
			// Weighted average between previous and target position
			var newX = prevX * (1 - smoothingFactor) + targetX * smoothingFactor;
			var newY = prevY * (1 - smoothingFactor) + targetY * smoothingFactor;
			self.x = newX;
			self.y = newY;
			// Update previous positions
			prevX = newX;
			prevY = newY;
		}
		if (facekit.leftEye && facekit.rightEye) {
			targetTilt = calculateFaceTilt() * tiltScaleFactor; // Scale down the tilt
			// Reduce max rotation to Β±15 degrees
			targetTilt = Math.max(-15, Math.min(15, targetTilt));
			self.rotation += (targetTilt - self.rotation) * tiltSmoothingFactor;
		}
	};
	function calculateFaceTilt() {
		if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) {
			// Calculate midpoint between eyes
			var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2;
			var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2;
			// Calculate angle between eye midpoint and mouth, negated to fix direction
			var dx = facekit.mouthCenter.x - eyeMidX;
			var dy = facekit.mouthCenter.y - eyeMidY;
			var angle = -(Math.atan2(dx, dy) * (180 / Math.PI));
			// Reduced max angle to Β±15 degrees and lowered multiplier
			return Math.max(-15, Math.min(15, angle * 0.15));
		}
		return 0; // Default to straight when face points aren't available
	}
	return self;
});
/**** 
* Initialize Game
****/ 
var game = new LK.Game({
	backgroundColor: 0x87CEEB // Light blue background to represent the sky
});
/**** 
* Game Code
****/ 
function _slicedToArray(r, e) {
	return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
}
function _nonIterableRest() {
	throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _unsupportedIterableToArray(r, a) {
	if (r) {
		if ("string" == typeof r) {
			return _arrayLikeToArray(r, a);
		}
		var t = {}.toString.call(r).slice(8, -1);
		return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
	}
}
function _arrayLikeToArray(r, a) {
	(null == a || a > r.length) && (a = r.length);
	for (var e = 0, n = Array(a); e < a; e++) {
		n[e] = r[e];
	}
	return n;
}
function _iterableToArrayLimit(r, l) {
	var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
	if (null != t) {
		var e,
			n,
			i,
			u,
			a = [],
			f = !0,
			o = !1;
		try {
			if (i = (t = t.call(r)).next, 0 === l) {
				if (Object(t) !== t) {
					return;
				}
				f = !1;
			} else {
				for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0) {
					;
				}
			}
		} catch (r) {
			o = !0, n = r;
		} finally {
			try {
				if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) {
					return;
				}
			} finally {
				if (o) {
					throw n;
				}
			}
		}
		return a;
	}
}
function _arrayWithHoles(r) {
	if (Array.isArray(r)) {
		return r;
	}
}
var UPGRADE_CONFIG = {
	player: {
		lungCapacity: {
			name: "Lung Capacity",
			baseCost: 50,
			costScale: 2,
			maxLevel: 10,
			currentLevel: 0
		},
		quickBreath: {
			name: "Quick Breath",
			baseCost: 75,
			costScale: 2,
			maxLevel: 10,
			currentLevel: 0
		}
	},
	machine: {
		bubbleDurability: {
			name: "Bubble Durability",
			baseCost: 200,
			costScale: 3,
			maxLevel: 5,
			currentLevel: 0
		},
		machineSpeed: {
			name: "Machine Speed",
			baseCost: 150,
			costScale: 2.5,
			maxLevel: 10,
			currentLevel: 0
		},
		autoPop: {
			name: "Auto-Pop",
			baseCost: 500,
			costScale: 2,
			maxLevel: 5,
			currentLevel: 0
		}
	}
};
// Create upgrade menu elements
var menuTab = LK.getAsset('upgradetab', {
	anchorX: 0.5,
	anchorY: 1,
	x: game.width / 2,
	y: game.height,
	alpha: 0.9
});
var menuText = new Text2("Upgrades", {
	size: 48,
	fill: 0xFFFFFF,
	stroke: 0x000000,
	strokeThickness: 2,
	font: "Impact"
});
menuText.anchorX = 0.5;
menuText.anchorY = 0.5;
menuText.x = menuTab.width / 2;
menuText.y = menuTab.height / 2;
menuTab.addChild(menuText);
var menuPanel = LK.getAsset('upgradetab', {
	anchorX: 0.5,
	anchorY: 1,
	x: game.width / 2,
	y: game.height,
	alpha: 0.5,
	scaleX: game.width / 67,
	// Adjust to game width
	scaleY: game.height * 0.25 / 100.3 // 25% of game height
});
// Add upgrade text elements
var upgradeTexts = [];
var startY = 50;
var columnWidth = game.width / 2;
Object.entries(UPGRADE_CONFIG).forEach(function (_ref, categoryIndex) {
	var _ref2 = _slicedToArray(_ref, 2),
		category = _ref2[0],
		upgrades = _ref2[1];
	Object.entries(upgrades).forEach(function (_ref3, index) {
		var _ref4 = _slicedToArray(_ref3, 2),
			key = _ref4[0],
			upgrade = _ref4[1];
		var x = categoryIndex * columnWidth + 20;
		var y = startY + index * 60;
		var text = new Text2(upgrade.name + " - " + upgrade.baseCost + " BP", {
			size: 36,
			fill: 0xFFFFFF,
			stroke: 0x000000,
			strokeThickness: 2,
			font: "Impact"
		});
		text.x = x;
		text.y = y;
		text.upgrade = key;
		upgradeTexts.push(text);
		menuPanel.addChild(text);
	});
});
// Menu state and animation
var menuOpen = false;
var menuTargetY = game.height;
var background = LK.getAsset('background', {
	anchorX: 0.5,
	anchorY: 0.5,
	x: game.width / 2,
	y: game.height / 2
});
game.addChild(background);
game.addChild(menuPanel);
game.addChild(menuTab);
var playerMask = new pufferMask();
game.addChild(playerMask);
// Initialize game variables 
game.growingBubble = null;
game.maxBubbleSize = 160; // Increased by 30% 
game.growthRate = 1.6; // Slightly increased to match new max size 
game.MAX_BUBBLES = 125;
game.baseSpawnRate = 180; // Every 3 seconds
function spawnSplitBubble(parentX, parentY, size, direction) {
	var isAutoPop = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
	var bubble = new Bubble();
	bubble.x = parentX;
	bubble.y = parentY;
	bubble.size = size;
	bubble.initLifetime(); // Recalculate after size is set
	bubble.justSplit = true;
	var speedMultiplier = 120 / size * (0.9 + Math.random() * 0.2);
	if (isAutoPop) {
		// Pure upward/sideways motion for natural splits
		bubble.verticalVelocity = bubble.floatSpeed; // Start at float speed
		bubble.driftX = direction * (Math.random() * 0.8 + 0.5);
	} else {
		// Manual pop physics
		bubble.verticalVelocity = -(Math.random() * 2 + 4);
		bubble.driftX = direction * (Math.random() * 1.5 + 2);
	}
	game.addChild(bubble);
	game.bubbles.push(bubble);
	return bubble;
}
game.bubbles = []; // Track bubble array
// Initialize game variables
//<Assets used in the game will automatically appear here>
game.bp = 0; // Track total BP 
game.combo = 0;
game.lastPopTime = 0;
game.COMBO_WINDOW = 60; // 1 second in frames
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];
}
// Create BP display text (add near game initialization)
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);
game.addBP = function (points, x, y, isAutoPop) {
	var currentTime = LK.ticks;
	// Only update combo if it's not an auto-pop
	if (!isAutoPop) {
		if (currentTime - game.lastPopTime < game.COMBO_WINDOW) {
			game.combo++;
			points *= 1 + game.combo * 0.1; // 10% bonus per combo
		} else {
			game.combo = 0;
		}
		game.lastPopTime = currentTime;
	}
	game.bp += Math.floor(points);
	bpText.setText(formatBP(game.bp) + " BP");
	// Always show point text regardless of auto or manual pop
	var pointText = new Text2("+" + Math.floor(points), {
		size: 96,
		fill: 0xFFFF00,
		font: "Impact",
		fontWeight: 'bold'
	});
	pointText.anchorX = 0.5;
	pointText.anchorY = 0.5;
	pointText.x = x;
	pointText.y = y;
	game.addChild(pointText);
	tween(pointText, {
		y: pointText.y - 100,
		alpha: 0
	}, {
		duration: 1200,
		onFinish: function onFinish() {
			pointText.destroy();
		}
	});
	// Only show combo text if it's a manual pop and we have a combo
	if (!isAutoPop && game.combo > 0) {
		var comboText = new Text2("x" + (game.combo + 1), {
			size: 96,
			fill: 0xFFA500,
			stroke: 0x000000,
			strokeThickness: 4,
			fontWeight: 'bold'
		});
		comboText.anchorX = 0.5;
		comboText.anchorY = 0;
		comboText.x = game.width / 2;
		comboText.y = 20;
		game.addChild(comboText);
		tween(comboText, {
			alpha: 0
		}, {
			duration: 500,
			onFinish: function onFinish() {
				comboText.destroy();
			}
		});
	}
};
game.update = function () {
	// Add logic to grow and release bubbles based on facekit mouth state
	if (facekit.mouthOpen) {
		// Calculate spawn position relative to pufferfish mask 
		var spawnX = playerMask.x; // Center of mask 
		var spawnY = playerMask.y + playerMask.height * 0.15; // 40% from bottom 
		// Start or continue growing bubble 
		if (!game.growingBubble) {
			game.growingBubble = new Bubble();
			game.growingBubble.size = 25; // Keep minimum starting size 
			game.addChild(game.growingBubble);
			game.bubbles.push(game.growingBubble);
		}
		// Update growing bubble position and size 
		if (game.growingBubble) {
			game.growingBubble.x = spawnX;
			game.growingBubble.y = spawnY;
			game.growingBubble.size = Math.min(game.growingBubble.size + game.growthRate, game.maxBubbleSize);
			game.growingBubble.verticalVelocity = 0;
			game.growingBubble.driftX = 0;
		}
	} else if (game.growingBubble) {
		// Stronger initial downward velocity when released 
		game.growingBubble.verticalVelocity = -12; // Increased from -5 
		game.growingBubble.driftX = (Math.random() * 2 - 1) * 2.5; // Slightly increased drift 
		game.growingBubble = null;
	}
	// Update all children (bubbles) 
	// Only spawn if under max bubbles
	if (game.bubbles.length < game.MAX_BUBBLES) {
		if (LK.ticks % game.baseSpawnRate == 0) {
			var x = Math.random() * (game.width - 200) + 100;
			spawnSplitBubble(x, game.height + 100, 100, 0);
		}
	}
	// Clean up destroyed bubbles from array
	game.bubbles = game.bubbles.filter(function (bubble) {
		return !bubble.destroyed;
	});
	// Update all children (bubbles)
	for (var i = game.bubbles.length - 1; i >= 0; i--) {
		var bubble = game.bubbles[i];
		if (bubble.update) {
			bubble.update();
		}
	}
};
// Handle touch/mouse events for the game
game.down = function (x, y, obj) {
	// Check if clicked on menu tab
	var tabBounds = {
		x: menuTab.x - menuTab.width / 2,
		y: menuTab.y - menuTab.height,
		width: menuTab.width,
		height: menuTab.height
	};
	if (x >= tabBounds.x && x <= tabBounds.x + tabBounds.width && y >= tabBounds.y && y <= tabBounds.y + tabBounds.height) {
		menuOpen = !menuOpen;
		var targetY = menuOpen ? game.height - menuPanel.height : game.height;
		tween(menuPanel, {
			y: targetY
		}, {
			duration: 300
		});
		tween(menuTab, {
			y: targetY
		}, {
			duration: 300
		});
		return true;
	}
	// Close menu if clicking outside when open
	if (menuOpen) {
		var menuBounds = {
			x: menuPanel.x - menuPanel.width / 2,
			y: menuPanel.y - menuPanel.height,
			width: menuPanel.width,
			height: menuPanel.height
		};
		if (!(x >= menuBounds.x && x <= menuBounds.x + menuBounds.width && y >= menuBounds.y && y <= menuBounds.y + menuBounds.height)) {
			menuOpen = false;
			tween(menuPanel, {
				y: game.height
			}, {
				duration: 300
			});
			tween(menuTab, {
				y: game.height
			}, {
				duration: 300
			});
			return true;
		}
	}
	var popped = false; // Track if we've popped any bubble
	for (var i = game.bubbles.length - 1; i >= 0; i--) {
		var bubble = game.bubbles[i];
		// Calculate distance between click and bubble center
		var dx = x - bubble.x;
		var dy = y - bubble.y;
		var distance = Math.sqrt(dx * dx + dy * dy);
		// Only pop if we haven't popped anything this click
		if (!popped && distance <= bubble.size / 2 + 10 && bubble.down) {
			bubble.down();
			popped = true;
			break; // Exit loop after first pop
		}
	}
};
; ===================================================================
--- original.js
+++ change.js
@@ -554,8 +554,53 @@
 	}
 };
 // Handle touch/mouse events for the game
 game.down = function (x, y, obj) {
+	// Check if clicked on menu tab
+	var tabBounds = {
+		x: menuTab.x - menuTab.width / 2,
+		y: menuTab.y - menuTab.height,
+		width: menuTab.width,
+		height: menuTab.height
+	};
+	if (x >= tabBounds.x && x <= tabBounds.x + tabBounds.width && y >= tabBounds.y && y <= tabBounds.y + tabBounds.height) {
+		menuOpen = !menuOpen;
+		var targetY = menuOpen ? game.height - menuPanel.height : game.height;
+		tween(menuPanel, {
+			y: targetY
+		}, {
+			duration: 300
+		});
+		tween(menuTab, {
+			y: targetY
+		}, {
+			duration: 300
+		});
+		return true;
+	}
+	// Close menu if clicking outside when open
+	if (menuOpen) {
+		var menuBounds = {
+			x: menuPanel.x - menuPanel.width / 2,
+			y: menuPanel.y - menuPanel.height,
+			width: menuPanel.width,
+			height: menuPanel.height
+		};
+		if (!(x >= menuBounds.x && x <= menuBounds.x + menuBounds.width && y >= menuBounds.y && y <= menuBounds.y + menuBounds.height)) {
+			menuOpen = false;
+			tween(menuPanel, {
+				y: game.height
+			}, {
+				duration: 300
+			});
+			tween(menuTab, {
+				y: game.height
+			}, {
+				duration: 300
+			});
+			return true;
+		}
+	}
 	var popped = false; // Track if we've popped any bubble
 	for (var i = game.bubbles.length - 1; i >= 0; i--) {
 		var bubble = game.bubbles[i];
 		// Calculate distance between click and bubble center
:quality(85)/https://cdn.frvr.ai/67ba4b7d38fe57e0d4bf9475.png%3F3) 
 A white bubble with a black outline Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
:quality(85)/https://cdn.frvr.ai/67bfab127754338fc503f264.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67c22f097033e124bf42ebe7.png%3F3) 
 A filled in white circle.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
:quality(85)/https://cdn.frvr.ai/67c240d2f2b58146864d0ea7.png%3F3) 
 A yellow star. Cartoon.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
:quality(85)/https://cdn.frvr.ai/67c33edae3ef98e836e2092f.png%3F3) 
 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
:quality(85)/https://cdn.frvr.ai/67c34097e3ef98e836e2095a.png%3F3) 
 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
:quality(85)/https://cdn.frvr.ai/67c52c6b3591963c82f8bf83.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67cb3ceb2dbb4136c61192f7.png%3F3) 
 A outstretched straight octopus tentacle. Green with purple suckers. Cartoon.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
:quality(85)/https://cdn.frvr.ai/67cb782f9a1a3019c1ad978e.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67cb789e9a1a3019c1ad9792.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67cb790e9a1a3019c1ad9796.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e08245e50564a967e16f72.png%3F3) 
 A colorful underwater coral reef background. Cartoon Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
:quality(85)/https://cdn.frvr.ai/67e0b732e50564a967e16fe5.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0b774e50564a967e16fe9.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0b7b4e50564a967e16fed.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0b83be50564a967e16ff5.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0b8b7e50564a967e17001.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0b8fee7971efb59b948a3.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0b906e50564a967e17005.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0b94ae50564a967e17009.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0b987e50564a967e1700d.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0b9b1e50564a967e17011.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0b9e5e50564a967e17015.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0ba09e50564a967e17019.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0ba34e50564a967e1701d.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0ba60e50564a967e17021.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0ba8de50564a967e17025.png%3F3) 
 :quality(85)/https://cdn.frvr.ai/67e0bbe2e50564a967e17062.png%3F3) 
 A white bubble with a black outline. Pixel art.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
:quality(85)/https://cdn.frvr.ai/67e0bc03e50564a967e17067.png%3F3) 
 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