Code edit (12 edits merged)
Please save this source code
User prompt
Erase all Tower Menu code
User prompt
Erase all debug code
User prompt
Please fix the bug: 'Uncaught ReferenceError: worldPos is not defined' in or related to this line: 'var dx = worldPos.x - doge.x;' Line Number: 1287
Code edit (4 edits merged)
Please save this source code
User prompt
Please fix the bug: 'TypeError: enemy.takeDamage is not a function' in or related to this line: 'enemy.takeDamage(self.damage);' Line Number: 69
Code edit (1 edits merged)
Please save this source code
Code edit (20 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of null (reading 'length')' in or related to this line: 'self.graphic = self.attachAsset(null, {}); // Start with no asset, will be set by init' Line Number: 472
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of null (reading 'length')' in or related to this line: 'self.graphic = self.attachAsset(null, {}); // Start with no asset' Line Number: 119
User prompt
Please fix the bug: 'Uncaught TypeError: LK.Rectangle is not a constructor' in or related to this line: 'var background = new LK.Rectangle({' Line Number: 559
Code edit (1 edits merged)
Please save this source code
Code edit (2 edits merged)
Please save this source code
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'fill')' in or related to this line: 'barkButton.style.fill = 0xffcc00; // Restore color' Line Number: 748
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'height')' in or related to this line: 'var SCREEN_HEIGHT = LK.screen.height; // Get screen height /**** NEW ****/' Line Number: 435
User prompt
Please fix the bug: 'LK.getScreenSize is not a function' in or related to this line: 'var screenSize = LK.getScreenSize();' Line Number: 434
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'height')' in or related to this line: 'var SCREEN_HEIGHT = LK.canvas.height; // Get screen height /**** NEW ****/' Line Number: 434
Code edit (1 edits merged)
Please save this source code
User prompt
DOGE vs. Bureaucracy
Initial prompt
Create the foundational code for a 2D top-down Hero Defense game called "DOGE vs. Bureaucracy" using the LK Game Engine. The game features the Doge meme character defending a goal zone from waves of bureaucracy-themed enemies, assisted by office-supply-themed towers placed by the player. **I. Core Game Setup:** 1. Initialize the main game object using `LK.Game`. 2. Create necessary containers: `gameContainer` for gameplay elements (hero, enemies, towers, projectiles), and `uiContainer` for UI elements (text, buttons). Ensure `uiContainer` renders on top. 3. Define a variable `playerCurrency` initialized to a starting value (e.g., 100). 4. Define a `goalZone` object (can be a simple Container with position and size) with a `health` property (e.g., 100). **II. Hero Object (DOGE):** 1. Create a `Hero` class using `Container.expand`. 2. **Assets:** Use a placeholder asset `doge_sprite` (define via `LK.init.image`) for the visual representation. Include basic states like 'idle', 'walk', 'attack'. 3. **Stats:** Add properties for `health` (e.g., 50), `speed` (e.g., 3), and `attackRange` (e.g., 50), `attackDamage` (e.g., 5), `attackCooldown` (e.g., 0.5 seconds). Add a `currentAttackCooldown` timer. 4. **Movement:** * Implement click-to-move. Use `self.down` on the `gameContainer` (or a background layer) to get target coordinates. * In the `Hero.update` method, move the Hero towards the `targetX`, `targetY` at its `speed`. Stop when close to the target. Basic direction vector calculation is sufficient for now (no complex pathfinding needed initially). * Update the visual state to 'walk' when moving, 'idle' when stopped. 5. **Auto-Attack ("Much Bark"):** * In `Hero.update`, decrement `currentAttackCooldown`. * If `currentAttackCooldown <= 0`: * Find the nearest `Enemy` object within `attackRange`. * If an enemy is found: * Apply `attackDamage` to the enemy's health. * Reset `currentAttackCooldown` to `attackCooldown`. * Briefly set the visual state to 'attack'. * (Optional: Create a short-lived visual effect or placeholder `bark_projectile` object). **III. Enemy Object (Paperwork Swarm - Basic):** 1. Create an `Enemy` base class using `Container.expand`. Add basic properties like `health`, `speed`. 2. Create a `PaperworkEnemy` class inheriting from `Enemy` using `Container.expand`. 3. **Assets:** Use a placeholder asset `paper_sprite` (`LK.init.image`). 4. **Stats:** Set low `health` (e.g., 10) and moderate `speed` (e.g., 1.5). Add a property `currencyValue` (e.g., 5). 5. **Movement:** * Define a simple, predefined path using an array of coordinates `pathPoints`. * In `PaperworkEnemy.update`, move the enemy along the `pathPoints` towards the next point in the array at its `speed`. * If the enemy reaches the `goalZone`'s coordinates: decrease `goalZone.health` (e.g., by 10) and destroy the enemy object. 6. **Defeat:** When `health <= 0`, award `currencyValue` to `playerCurrency` and destroy the enemy object. **IV. Tower Object (Stapler Turret - Basic):** 1. Create a `Tower` base class using `Container.expand`. Add properties `cost`, `attackRange`, `attackDamage`, `attackCooldown`. 2. Create a `StaplerTower` class inheriting from `Tower` using `Container.expand`. 3. **Assets:** Use a placeholder asset `stapler_sprite` (`LK.init.image`). 4. **Stats:** Set `cost` (e.g., 50), `attackRange` (e.g., 100), `attackDamage` (e.g., 8), `attackCooldown` (e.g., 1 second). Add `currentAttackCooldown` timer. 5. **Placement:** * Create placeholder `BuildSpot` objects (simple Containers) at predefined locations. * When a `BuildSpot` is clicked (`self.down`): * If `playerCurrency >= StaplerTower.cost`: * Subtract the cost from `playerCurrency`. * Create a new `StaplerTower` instance at the `BuildSpot`'s position. * Make the `BuildSpot` inactive/occupied. 6. **Auto-Attack:** * In `StaplerTower.update`, decrement `currentAttackCooldown`. * If `currentAttackCooldown <= 0`: * Find the nearest `Enemy` within `attackRange`. * If an enemy is found: * Apply `attackDamage` to the enemy's health. * Reset `currentAttackCooldown` to `attackCooldown`. * (Optional: Create placeholder `staple_projectile` object traveling towards the enemy). **V. Basic Game Loop (`game.update`):** 1. Update the Hero instance. 2. Iterate through and update all active Enemy instances. Check for defeat conditions. 3. Iterate through and update all active Tower instances. 4. Implement basic enemy spawning: Use `LK.setInterval` or a timer within `game.update` to spawn a new `PaperworkEnemy` at a starting point every few seconds (e.g., 3 seconds). Add a counter to limit the total number of enemies spawned per wave (e.g., 10 enemies). 5. Check for lose condition: If `goalZone.health <= 0`, stop the game loop or display a "Game Over" message. 6. (Optional: Add win condition check - e.g., after X enemies spawned and defeated). **VI. Basic UI:** 1. Use `Text2` objects within the `uiContainer`. 2. Create text elements to display: * `playerCurrency`. * `goalZone.health`. 3. Update these text elements regularly (e.g., within `game.update` or only when values change). **Instructions for Ava:** * Use placeholder assets (like `doge_sprite`, `paper_sprite`, `stapler_sprite`) defined using `LK.init.image` with basic dimensions. I will replace these with actual art later. * Use simple geometric shapes (`LK.init.shape`) for projectiles or build spots if needed initially. * Focus on setting up the object structures (`Container.expand`), core properties, and the basic `update` logic for movement, attacking, spawning, and health/currency management. * Implement the click handling (`self.down`) for hero movement and tower placement. * Ensure basic collision detection logic (enemy reaching goal zone) and targeting logic (finding nearest enemy) is functional. * Keep the initial implementation simple; advanced features like multiple enemy/tower types, complex pathfinding, hero abilities, upgrades, detailed animations, sound effects, and sophisticated wave logic will be added later.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { highScore: 0 }); /**** * Classes ****/ var AuditorBot = Container.expand(function (spawnX, spawnY, valueToIgnore, customPathData) { var self = Container.call(this); // Animation Properties self.walkFrames = ['enemyAuditorBot', 'enemyAuditorBot_walk_1', 'enemyAuditorBot_walk_2']; self.currentAnimFrame = 0; self.animationSpeed = 8; self.animationCounter = 0; // Path Properties if (customPathData && customPathData.length > 0) { self.myPathPoints = customPathData; } else { self.myPathPoints = pathPoints; } self.currentPathIndex = 0; // Specific stats for AuditorBot self.health = 45; self.speed = 1.0; self.value = 30; self.isFlying = false; // Initial Graphic & Position self.graphic = self.attachAsset(self.walkFrames[0], { anchorX: 0.5, anchorY: 0.5 }); if (self.myPathPoints && self.myPathPoints.length > 0) { self.x = self.myPathPoints[0].x; self.y = self.myPathPoints[0].y; } else if (spawnX !== undefined && spawnY !== undefined) { self.x = spawnX; self.y = spawnY; } self.update = function () { // Path Following Logic if (!self.myPathPoints || self.myPathPoints.length === 0) { return; } if (self.currentPathIndex < self.myPathPoints.length) { var target = self.myPathPoints[self.currentPathIndex]; if (!target) { self.currentPathIndex++; return; } var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed * 1.5) { self.currentPathIndex++; if (self.currentPathIndex >= self.myPathPoints.length) { playerLives--; livesText.setText("Lives: " + playerLives); LK.getSound('enemyReachGoal').play(); if (playerLives <= 0) { if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } LK.showGameOver(); } self.destroy(); return; } } else { dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; } } else { if (self.parent) { self.destroy(); } return; } // Animation Logic if (self.walkFrames && self.walkFrames.length > 1) { self.animationCounter += gameSpeedMultiplier; if (self.animationCounter >= self.animationSpeed) { self.animationCounter = 0; self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length; var newAsset = self.walkFrames[self.currentAnimFrame]; if (self.graphic && self.graphic.parent) { var currentLocalX = self.graphic.x; var currentLocalY = self.graphic.y; self.removeChild(self.graphic); self.graphic = self.attachAsset(newAsset, { anchorX: 0.5, anchorY: 0.5, x: currentLocalX, y: currentLocalY }); } else if (!self.graphic && self.parent) { self.graphic = self.attachAsset(newAsset, { anchorX: 0.5, anchorY: 0.5 }); } } } }; self.takeDamage = function (amount) { self.health -= amount; LK.effects.flashObject(self, 0xff0000, 200); if (self.health <= 0 && self.parent) { var numCoins = Math.ceil(self.value / 5); if (numCoins < 1) { numCoins = 1; } for (var i = 0; i < numCoins; i++) { var coinValue = Math.floor(self.value / numCoins); if (i === 0 && self.value % numCoins !== 0) { coinValue += self.value % numCoins; } var coin = new DogeCoin(self.x, self.y, coinValue); if (coinLayer) { coinLayer.addChild(coin); } else { game.addChild(coin); } } LK.setScore(LK.getScore() + self.value); scoreText.setText("Score: " + LK.getScore()); LK.getSound('enemyDeath').play(); self.destroy(); } }; var originalDestroy = self.destroy; self.destroy = function () { if (typeof originalDestroy === 'function') { originalDestroy.call(self); } else if (self.parent) { self.parent.removeChild(self); } }; return self; }); // ---- END: REPLACE AuditorBot CLASS WITH THIS ---- var BarkWave = Container.expand(function () { var self = Container.call(this); var graphic = self.attachAsset('barkWave', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5 }); self.damage = 2; // Slightly increased damage for manual ability self.duration = 30; // frames self.radius = 100; self.maxRadius = 350; // Slightly larger radius for manual ability self.update = function () { self.radius += 15 * gameSpeedMultiplier; // Expand slightly faster graphic.scaleX = self.radius / 100; graphic.scaleY = self.radius / 100; graphic.alpha -= 0.017 * gameSpeedMultiplier; // Check for enemies in range ONCE per enemy per wave activation // To avoid hitting the same enemy multiple times with one wave if (!self.enemiesHit) { self.enemiesHit = []; } // Initialize if needed for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Only check if enemy hasn't been hit by this wave yet if (self.enemiesHit.indexOf(enemy) === -1) { var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.radius) { enemy.takeDamage(self.damage); self.enemiesHit.push(enemy); // Mark enemy as hit } } } self.duration -= gameSpeedMultiplier; if (self.duration <= 0 || self.radius >= self.maxRadius) { self.destroy(); } }; return self; }); // --- REPLACE THE ENTIRE BuildSpot CLASS DEFINITION WITH THIS --- var BuildSpot = Container.expand(function () { var self = Container.call(this); self.graphic = self.attachAsset('buildSpot', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5 }); self.graphic.interactive = true; // Ensure the graphic is interactive self.graphic.interactive = true; // Ensure the graphic is interactive self.hasTower = false; self.tower = null; // Reference to the tower built on this spot self.selectionIconContainer = null; self.upgradeMenuContainer = null; self.actionIconContainer = null; // Container for upgrade/sell icons self.areIconsVisible = false; self.isUpgradeMenuVisible = false; self.areActionIconsVisible = false; // Flag for upgrade/sell icons self.showSelectionIcons = function () { if (self.hasTower || self.areIconsVisible) { return; } if (currentActiveBuildSpot && currentActiveBuildSpot !== self) { currentActiveBuildSpot.hideSelectionIcons(); if (currentActiveBuildSpot.isUpgradeMenuVisible) { currentActiveBuildSpot.hideUpgradeMenu(); } if (currentActiveBuildSpot.areActionIconsVisible) { currentActiveBuildSpot.hideTowerActionIcons(); } } currentActiveBuildSpot = self; self.selectionIconContainer = new Container(); self.selectionIconContainer.x = self.x; // Position container AT the BuildSpot's x (in 'level' space) self.selectionIconContainer.y = self.y; // Position container AT the BuildSpot's y (in 'level' space) // Add to the SAME parent as the BuildSpot (e.g., 'level'). // This ensures icons scroll with the game world. if (self.parent) { // BuildSpot should have a parent (level) self.parent.addChild(self.selectionIconContainer); } else { game.addChild(self.selectionIconContainer); // Fallback to game, but level is better } for (var i = 0; i < ICON_SELECT_OFFSETS.length; i++) { var offsetData = ICON_SELECT_OFFSETS[i]; var towerTypeKey = offsetData.towerKey; var towerInfo = TOWER_DATA[towerTypeKey]; if (!towerInfo || !towerInfo.levels || !towerInfo.levels[0] || !towerInfo.iconAsset) { // Visual error or skip var err = new Text2("Data? " + towerTypeKey, { size: 10, fill: 0xff0000 }); err.x = offsetData.x; err.y = offsetData.y; self.selectionIconContainer.addChild(err); continue; } var cost = towerInfo.levels[0].cost; // Create the icon graphic directly (no extra container per icon needed) var iconGraphic = self.selectionIconContainer.attachAsset(towerInfo.iconAsset, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); iconGraphic.x = offsetData.x; // Position relative to selectionIconContainer's origin iconGraphic.y = offsetData.y; // (which is the BuildSpot's center) iconGraphic.interactive = true; iconGraphic.towerTypeKey = towerTypeKey; // Store for click // Add cost text AS A CHILD of the iconGraphic (or selectionIconContainer) var costText = new Text2("$" + cost, { size: 20, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2 }); costText.anchor.set(0.5, -0.7); // Position below the icon's center iconGraphic.addChild(costText); // Text is child of icon, moves with it. if (currency < cost) { iconGraphic.alpha = 0.4; iconGraphic.interactive = false; } else { iconGraphic.alpha = 1.0; } iconGraphic.down = function () { var buildCost = TOWER_DATA[this.towerTypeKey].levels[0].cost; if (currency >= buildCost) { self.buildSelectedTower(this.towerTypeKey); // No need to call hideSelectionIcons here, game.down will handle it // because the click was on an interactive element. } else { var tempIconForPos = this; // 'this' is the iconGraphic var tempParentPos = tempIconForPos.parent.localToGlobal({ x: tempIconForPos.x, y: tempIconForPos.y }); var screenY = tempParentPos.y + game.y; spawnFloatingText("Need More $!", tempParentPos.x, screenY - 30, { fill: 0xFF0000 }); } // Always hide icons after a choice or attempted choice on an icon self.hideSelectionIcons(); }; } self.areIconsVisible = true; // LK.getSound('uiOpenMenu').play(); // Sound was moved to BuildSpot.down }; self.hideSelectionIcons = function () { if (self.selectionIconContainer) { self.selectionIconContainer.destroy(); self.selectionIconContainer = null; } self.areIconsVisible = false; if (currentActiveBuildSpot === self) { currentActiveBuildSpot = null; } }; self.hideUpgradeMenu = function () { if (self.upgradeMenuContainer) { self.upgradeMenuContainer.destroy(); self.upgradeMenuContainer = null; } // Destroy range indicators if they exist if (self.currentRangeIndicator) { self.currentRangeIndicator.destroy(); self.currentRangeIndicator = null; } if (self.nextRangeIndicator) { self.nextRangeIndicator.destroy(); self.nextRangeIndicator = null; } self.isUpgradeMenuVisible = false; if (currentActiveBuildSpot === self) { currentActiveBuildSpot = null; } }; self.showTowerActionIcons = function (towerObject) { if (!towerObject || !towerObject.towerType) { return; } // Create container for action icons self.actionIconContainer = new Container(); self.actionIconContainer.x = self.x; self.actionIconContainer.y = self.y; // Add to the same parent as the BuildSpot if (self.parent) { self.parent.addChild(self.actionIconContainer); } else { game.addChild(self.actionIconContainer); } // Get tower data var towerTypeKey = towerObject.towerType; var currentLevel = towerObject.currentLevel; var towerInfo = TOWER_DATA[towerTypeKey]; var currentLevelData = towerInfo.levels[currentLevel]; var nextLevelData = towerInfo.levels[currentLevel + 1]; var isMaxLevel = !nextLevelData; // Create a range indicator to show tower's attack range self.rangeIndicator = createRangeIndicator(self.x, self.y, currentLevelData.range, self.parent); // Position offsets for the icons var upgradeIconOffsetX = -110; var upgradeIconOffsetY = -110; var sellIconOffsetX = 110; var sellIconOffsetY = -110; // Calculate total tower cost for sell value var totalTowerCost = 0; for (var i = 0; i <= currentLevel; i++) { totalTowerCost += towerInfo.levels[i].cost; } var sellValue = Math.floor(totalTowerCost * 0.5); // 50% refund // Upgrade Icon (if upgradable) if (!isMaxLevel) { // Create upgrade icon using the next level's asset var upgradeIconAsset = towerInfo.levels[currentLevel + 1].asset; var upgradeIcon = self.actionIconContainer.attachAsset(upgradeIconAsset, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.0, scaleY: 1.0 }); upgradeIcon.x = upgradeIconOffsetX; upgradeIcon.y = upgradeIconOffsetY; // Add plus indicator var plusIcon = self.actionIconContainer.attachAsset('uiButtonPlus', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); plusIcon.x = upgradeIcon.x + 35; plusIcon.y = upgradeIcon.y - 35; // Add upgrade cost text var upgradeCostText = new Text2("$" + nextLevelData.cost, { size: 20, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2 }); upgradeCostText.anchor.set(0.5, -0.7); upgradeCostText.x = upgradeIconOffsetX; upgradeCostText.y = upgradeIconOffsetY; self.actionIconContainer.addChild(upgradeCostText); // Make upgrade icon interactive upgradeIcon.interactive = currency >= nextLevelData.cost; upgradeIcon.alpha = currency >= nextLevelData.cost ? 1.0 : 0.4; upgradeIcon.down = function () { self.tower.upgrade(); self.hideTowerActionIcons(); // If successfully upgraded and still not at max level, show the action icons again if (self.tower && self.tower.currentLevel < TOWER_DATA[self.tower.towerType].levels.length - 1) { LK.setTimeout(function () { self.showTowerActionIcons(self.tower); }, 100); } }; } else { // Show max level indicator (optional) var maxLevelIcon = self.actionIconContainer.attachAsset(currentLevelData.asset, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.8, alpha: 0.7 }); maxLevelIcon.x = upgradeIconOffsetX; maxLevelIcon.y = upgradeIconOffsetY; var maxText = new Text2("MAX", { size: 18, fill: 0xFFD700, stroke: 0x000000, strokeThickness: 2 }); maxText.anchor.set(0.5, 0.5); maxText.x = upgradeIconOffsetX; maxText.y = upgradeIconOffsetY; self.actionIconContainer.addChild(maxText); } // Sell Icon var sellIcon = self.actionIconContainer.attachAsset('iconSell', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); sellIcon.x = sellIconOffsetX; sellIcon.y = sellIconOffsetY; sellIcon.interactive = true; // Add sell value text var sellText = new Text2("$" + sellValue, { size: 20, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2 }); sellText.anchor.set(0.5, -0.7); sellText.x = sellIconOffsetX; sellText.y = sellIconOffsetY; self.actionIconContainer.addChild(sellText); // Make sell icon interactive sellIcon.down = function () { self.confirmSellTower(towerObject); }; // Mark as visible self.areActionIconsVisible = true; LK.getSound('uiOpenMenu').play(); }; self.hideTowerActionIcons = function () { if (self.actionIconContainer) { self.actionIconContainer.destroy(); self.actionIconContainer = null; } // Destroy range indicator if it exists if (self.rangeIndicator) { self.rangeIndicator.destroy(); self.rangeIndicator = null; } self.areActionIconsVisible = false; if (currentActiveBuildSpot === self) { currentActiveBuildSpot = null; } }; self.confirmSellTower = function (towerObject) { // First, hide action icons self.hideTowerActionIcons(); // Create a confirmation UI var confirmContainer = new Container(); confirmContainer.x = self.x; confirmContainer.y = self.y - 150; if (self.parent) { self.parent.addChild(confirmContainer); } else { game.addChild(confirmContainer); } // Calculate sell value var towerTypeKey = towerObject.towerType; var currentLevel = towerObject.currentLevel; var totalTowerCost = 0; for (var i = 0; i <= currentLevel; i++) { totalTowerCost += TOWER_DATA[towerTypeKey].levels[i].cost; } var sellValue = Math.floor(totalTowerCost * 0.5); // 50% refund // Background var confirmBg = confirmContainer.attachAsset('uiButtonBackground', { anchorX: 0.5, anchorY: 0.5, scaleX: 4.0, scaleY: 2.5, alpha: 0.6 }); // Confirmation text var confirmText = new Text2("Sell for $" + sellValue + "?", { size: 36, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2 }); confirmText.anchor.set(0.5, 0); confirmText.y = -60; confirmContainer.addChild(confirmText); // Yes button var yesButton = new Text2("YES", { size: 40, fill: 0x00FF00, stroke: 0x000000, strokeThickness: 2 }); yesButton.anchor.set(0.5, 0); yesButton.y = 30; yesButton.x = -70; yesButton.interactive = true; yesButton.down = function () { self.sellTower(towerObject, sellValue); confirmContainer.destroy(); }; confirmContainer.addChild(yesButton); // No button var noButton = new Text2("NO", { size: 40, fill: 0xFF0000, stroke: 0x000000, strokeThickness: 2 }); noButton.anchor.set(0.5, 0); noButton.y = 30; noButton.x = 70; noButton.interactive = true; noButton.down = function () { confirmContainer.destroy(); // Show tower action icons again LK.setTimeout(function () { self.showTowerActionIcons(towerObject); }, 100); }; confirmContainer.addChild(noButton); }; self.sellTower = function (towerObject, sellValue) { // Add refund to currency currency += sellValue; currencyText.setText("$: " + currency); // Play sell sound LK.getSound('sellSound').play(); // Spawn floating text spawnFloatingText("SOLD!", self.x, self.y - 50, { fill: 0xFFD700 }); // Clean up tower if (towerObject) { towerObject.destroy(); } // Reset BuildSpot self.tower = null; self.hasTower = false; self.graphic.alpha = 0.5; }; self.updateAffordability = function () { if (!self.areIconsVisible || !self.selectionIconContainer) { return; } var icons = self.selectionIconContainer.children; for (var i = 0; i < icons.length; i++) { var iconGraphic = icons[i]; // Now iconGraphic is the direct child if (iconGraphic && iconGraphic.towerTypeKey && TOWER_DATA[iconGraphic.towerTypeKey]) { var cost = TOWER_DATA[iconGraphic.towerTypeKey].levels[0].cost; if (currency < cost) { iconGraphic.alpha = 0.4; iconGraphic.interactive = false; } else { iconGraphic.alpha = 1.0; iconGraphic.interactive = true; } } } }; self.toggleUpgradeMenu = function () { // If upgrade menu is already visible, hide it if (self.isUpgradeMenuVisible) { self.hideUpgradeMenu(); return; } // Hide any other active menus first if (currentActiveBuildSpot && currentActiveBuildSpot !== self) { if (currentActiveBuildSpot.areIconsVisible) { currentActiveBuildSpot.hideSelectionIcons(); } else if (currentActiveBuildSpot.isUpgradeMenuVisible) { currentActiveBuildSpot.hideUpgradeMenu(); } else if (currentActiveBuildSpot.areActionIconsVisible) { currentActiveBuildSpot.hideTowerActionIcons(); } } // Set this as the active build spot currentActiveBuildSpot = self; // Create and show the upgrade menu self.showUpgradeMenu(self.tower); }; self.showUpgradeMenu = function (towerObject) { if (!towerObject || !towerObject.towerType) { return; } // Create container for upgrade menu self.upgradeMenuContainer = new Container(); self.upgradeMenuContainer.x = self.x; self.upgradeMenuContainer.y = self.y - 150; // Position above the tower // Add to the same parent as the BuildSpot if (self.parent) { self.parent.addChild(self.upgradeMenuContainer); } else { game.addChild(self.upgradeMenuContainer); } // Get tower data var towerTypeKey = towerObject.towerType; var currentLevel = towerObject.currentLevel; var towerInfo = TOWER_DATA[towerTypeKey]; var currentLevelData = towerInfo.levels[currentLevel]; var nextLevelData = towerInfo.levels[currentLevel + 1]; var isMaxLevel = !nextLevelData; // Create range indicator for current level self.currentRangeIndicator = createRangeIndicator(self.x, self.y, currentLevelData.range, self.parent); // If there's a next level, show its range too with a different appearance if (!isMaxLevel) { self.nextRangeIndicator = createRangeIndicator(self.x, self.y, nextLevelData.range, self.parent); // Make the next level range indicator more distinct if (self.nextRangeIndicator.children && self.nextRangeIndicator.children.length >= 2) { self.nextRangeIndicator.children[0].tint = 0x00FF00; // Green tint for the fill self.nextRangeIndicator.children[1].tint = 0x00FF00; // Green tint for the border } } // Create background var menuBg = self.upgradeMenuContainer.attachAsset('uiButtonBackground', { anchorX: 0.5, anchorY: 0.5, scaleX: 3, scaleY: 3 }); // Tower name and level var titleText = new Text2(towerInfo.name + " - Level " + (currentLevel + 1), { size: 24, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2 }); titleText.anchor.set(0.5, 0); titleText.y = -120; self.upgradeMenuContainer.addChild(titleText); // If there's a next level available if (!isMaxLevel) { // Upgrade cost var costText = new Text2("Upgrade Cost: $" + nextLevelData.cost, { size: 20, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2 }); costText.anchor.set(0.5, 0); costText.y = -85; self.upgradeMenuContainer.addChild(costText); // Stat changes var statsY = -55; var statsGap = 25; // Add stat comparisons based on tower type if (towerTypeKey === 'stapler') { // Damage var damageText = new Text2("Damage: " + currentLevelData.damage + " → " + nextLevelData.damage, { size: 18, fill: 0xFFFFFF }); damageText.anchor.set(0.5, 0); damageText.y = statsY; self.upgradeMenuContainer.addChild(damageText); // Range var rangeText = new Text2("Range: " + currentLevelData.range + " → " + nextLevelData.range, { size: 18, fill: 0xFFFFFF }); rangeText.anchor.set(0.5, 0); rangeText.y = statsY + statsGap; self.upgradeMenuContainer.addChild(rangeText); // Fire Rate var fireRateText = new Text2("Fire Rate: " + (60 / currentLevelData.fireRate).toFixed(1) + " → " + (60 / nextLevelData.fireRate).toFixed(1), { size: 18, fill: 0xFFFFFF }); fireRateText.anchor.set(0.5, 0); fireRateText.y = statsY + statsGap * 2; self.upgradeMenuContainer.addChild(fireRateText); } else if (towerTypeKey === 'blocker') { // Slow Factor (convert to percentage for clarity) var slowText = new Text2("Slow: " + (1 - currentLevelData.slowFactor) * 100 + "% → " + (1 - nextLevelData.slowFactor) * 100 + "%", { size: 18, fill: 0xFFFFFF }); slowText.anchor.set(0.5, 0); slowText.y = statsY; self.upgradeMenuContainer.addChild(slowText); // Range var rangeText = new Text2("Range: " + currentLevelData.range + " → " + nextLevelData.range, { size: 18, fill: 0xFFFFFF }); rangeText.anchor.set(0.5, 0); rangeText.y = statsY + statsGap; self.upgradeMenuContainer.addChild(rangeText); } // Upgrade button var upgradeButton = new Text2("UPGRADE", { size: 28, fill: currency >= nextLevelData.cost ? 0x00FF00 : 0xFF0000, stroke: 0x000000, strokeThickness: 2 }); upgradeButton.anchor.set(0.5, 0); upgradeButton.y = 30; upgradeButton.interactive = currency >= nextLevelData.cost; upgradeButton.down = function () { self.tower.upgrade(); }; self.upgradeMenuContainer.addChild(upgradeButton); } else { // Max level text var maxLevelText = new Text2("MAX LEVEL REACHED", { size: 24, fill: 0xFFD700, // Gold color stroke: 0x000000, strokeThickness: 2 }); maxLevelText.anchor.set(0.5, 0); maxLevelText.y = -50; self.upgradeMenuContainer.addChild(maxLevelText); } // Sell button (optional) var sellButton = new Text2("SELL", { size: 24, fill: 0xFF9999, stroke: 0x000000, strokeThickness: 2 }); sellButton.anchor.set(0.5, 0); sellButton.y = 70; sellButton.interactive = true; sellButton.down = function () { // Calculate total value of tower for refund var totalTowerCost = 0; for (var i = 0; i <= currentLevel; i++) { totalTowerCost += TOWER_DATA[towerTypeKey].levels[i].cost; } var sellValue = Math.floor(totalTowerCost * 0.5); // 50% refund // Add refund to currency currency += sellValue; currencyText.setText("$: " + currency); spawnFloatingText("Sold for $" + sellValue, self.x, self.y - 50, { fill: 0xFFD700 }); LK.getSound('sellSound').play(); self.tower.destroy(); self.tower = null; self.hasTower = false; self.graphic.alpha = 0.5; // Reset buildspot opacity self.hideUpgradeMenu(); }; self.upgradeMenuContainer.addChild(sellButton); // Play sound when opening menu LK.getSound('uiOpenMenu').play(); // Mark menu as visible self.isUpgradeMenuVisible = true; }; self.buildSelectedTower = function (towerTypeKey) { if (self.hasTower || !TOWER_DATA[towerTypeKey]) { return; } var towerLevelData = TOWER_DATA[towerTypeKey].levels[0]; if (currency < towerLevelData.cost) { return; } currency -= towerLevelData.cost; currencyText.setText("$: " + currency); var newTower; if (towerTypeKey === 'stapler') { newTower = new StaplerTower(); } else if (towerTypeKey === 'blocker') { newTower = new BureaucracyBlockerTower(); } else if (towerTypeKey === 'laserCat') { newTower = new LaserCatPerch(); } else if (towerTypeKey === 'rickroller') { newTower = new RickrollerTower(); // Play placement sound LK.getSound('sfxRickrollerPlace').play(); } else if (towerTypeKey === 'thisIsFine') { newTower = new ThisIsFinePit(); // Play match strike sound when placing the fire pit LK.getSound('sfxThisIsFinePlaceMatch').play(); // Start gentle fire sound (looping, low volume initially) LK.getSound('sfxThisIsFineFireGentle').play(); } else if (towerTypeKey === 'restruct') { newTower = new RestructuringSpecialist(); } // No need for special newTower.init(0) call - constructor handles initialization // else if (towerTypeKey === 'placeholder') { /* Do nothing or build a dummy */ return; } if (newTower) { // The tower's constructor now calls initializeTowerFromData(self, type, 0) newTower.x = 0; // Place tower at BuildSpot's origin (local to BuildSpot) newTower.y = 0; self.addChild(newTower); // Tower becomes child of BuildSpot self.tower = newTower; self.hasTower = true; self.graphic.alpha = 0.1; // Dim the build spot graphic itself var buildSfx = TOWER_DATA[towerTypeKey].buildSfx || 'uiSelectTower'; // Fallback // Ensure these sounds exist: 'buildStapler', 'buildBlocker' // LK.getSound(buildSfx).play(); // Play specific build sound // Floating text relative to build spot in world space spawnFloatingText(TOWER_DATA[towerTypeKey].name + " BUILT!", self.x, self.y - self.graphic.height / 2, { fill: 0x00FF00 }); // Update affordability for any other spot's menu that might be open (shouldn't be with current logic) if (currentActiveBuildSpot && currentActiveBuildSpot !== self) { currentActiveBuildSpot.updateAffordability(); } } // Icons are hidden by the icon's down handler OR by game.down clicking elsewhere }; self.down = function () { // This is the click on the BuildSpot graphic itself if (self.hasTower) { // If action icons are already visible, clicking again should close them if (self.areActionIconsVisible) { self.hideTowerActionIcons(); } else { // Hide any other open menus if (currentActiveBuildSpot && currentActiveBuildSpot !== self) { if (currentActiveBuildSpot.areIconsVisible) { currentActiveBuildSpot.hideSelectionIcons(); } else if (currentActiveBuildSpot.isUpgradeMenuVisible) { currentActiveBuildSpot.hideUpgradeMenu(); } else if (currentActiveBuildSpot.areActionIconsVisible) { currentActiveBuildSpot.hideTowerActionIcons(); } } // Set this as the active build spot currentActiveBuildSpot = self; // Show tower action icons LK.getSound('uiOpenMenu').play(); self.showTowerActionIcons(self.tower); } return; } // If this spot's icons are already visible, clicking it again should close them. if (self.areIconsVisible) { self.hideSelectionIcons(); } else { // If another spot's icons are visible, hide them first. if (currentActiveBuildSpot && currentActiveBuildSpot !== self) { currentActiveBuildSpot.hideSelectionIcons(); } LK.getSound('uiOpenMenu').play(); // Play sound when opening self.showSelectionIcons(); } }; return self; }); // --- MODIFY BureaucracyBlockerTower --- var BureaucracyBlockerTower = Container.expand(function () { var self = Container.call(this); // --- MODIFIED: No graphic initialization in constructor --- self.enemiesSlowed = []; // init method removed - functionality moved to constructor and initializeTowerFromData self.clearAllSlows = function () { for (var i = 0; i < self.enemiesSlowed.length; i++) { var enemy = self.enemiesSlowed[i]; if (enemy && enemy.parent && enemy.isSlowedByBlocker === self) { enemy.speed = enemy.originalSpeed; enemy.isSlowedByBlocker = null; if (enemy.graphic) { enemy.graphic.tint = 0xFFFFFF; } } } self.enemiesSlowed = []; }; self.update = function () { var buildSpot = self.parent; if (!buildSpot || !buildSpot.parent) { return; } var levelContainer = buildSpot.parent; var towerGraphicGlobalCenter = self.graphic.toGlobal({ x: 0, y: 0 }); var towerCenterInGameSpace = game.toLocal(towerGraphicGlobalCenter); var newlySlowedThisFrame = []; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (!enemy || !enemy.parent) { continue; } // Skip if enemy is already gone var dx = enemy.x - towerCenterInGameSpace.x; var dy = enemy.y - towerCenterInGameSpace.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < self.range * self.range) { // Enemy is in range if (!enemy.isSlowedByBlocker) { // Check a custom flag enemy.originalSpeed = enemy.speed; // Store original speed enemy.speed *= self.slowFactor; enemy.isSlowedByBlocker = self; // Mark who slowed it // Optional: Visual effect on enemy if (enemy.graphic) { enemy.graphic.tint = 0xAAAAFF; } // Light blue tint } newlySlowedThisFrame.push(enemy); } } // Check enemies that were slowed last frame but might be out of range now for (var i = self.enemiesSlowed.length - 1; i >= 0; i--) { var previouslySlowedEnemy = self.enemiesSlowed[i]; if (!previouslySlowedEnemy || !previouslySlowedEnemy.parent || newlySlowedThisFrame.indexOf(previouslySlowedEnemy) === -1) { // Enemy is gone or no longer in range by this tower if (previouslySlowedEnemy && previouslySlowedEnemy.isSlowedByBlocker === self) { // Only unslow if WE slowed it previouslySlowedEnemy.speed = previouslySlowedEnemy.originalSpeed; previouslySlowedEnemy.isSlowedByBlocker = null; if (previouslySlowedEnemy.graphic) { previouslySlowedEnemy.graphic.tint = 0xFFFFFF; } // Reset tint } self.enemiesSlowed.splice(i, 1); } } self.enemiesSlowed = newlySlowedThisFrame; // Update the list of currently slowed enemies }; // When tower is destroyed, make sure to unslow any enemies it was affecting var originalDestroy = self.destroy; self.destroy = function () { for (var i = 0; i < self.enemiesSlowed.length; i++) { var enemy = self.enemiesSlowed[i]; if (enemy && enemy.parent && enemy.isSlowedByBlocker === self) { enemy.speed = enemy.originalSpeed; enemy.isSlowedByBlocker = null; if (enemy.graphic) { enemy.graphic.tint = 0xFFFFFF; } } } self.enemiesSlowed = []; if (originalDestroy) { originalDestroy.call(self); } else if (self.parent) { self.parent.removeChild(self); } // Basic destroy }; // Initialize tower with level 0 stats and graphics initializeTowerFromData(self, 'blocker', 0); // Set initial stats & correct L0 asset return self; }); var DogeAutoProjectile = Container.expand(function () { var self = Container.call(this); self.graphic = self.attachAsset('basicAttackProjectile', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 15; self.target = null; self.damage = 0; // Will be set by DogeHero when creating projectile self.duration = 20; // frames, for self-destruction if target is lost self.update = function () { // Decrement duration self.duration -= gameSpeedMultiplier; if (self.duration <= 0) { self.destroy(); return; } // If target is gone or not in game anymore if (!self.target || !self.target.parent) { self.alpha -= 0.1 * gameSpeedMultiplier; if (self.alpha <= 0) { self.destroy(); } return; } // Move towards target var dx = self.target.x - self.x; var dy = self.target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // If very close to target, destroy if (distance < self.speed * gameSpeedMultiplier || distance < self.target.graphic.width / 2) { // Apply damage when hitting the target if (self.target && self.target.parent) { self.target.takeDamage(self.damage); } self.destroy(); return; } // Move towards target dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; }; return self; }); var DogeCoin = Container.expand(function (spawnX, spawnY, value) { var self = Container.call(this); // Initialize coin properties self.graphic = self.attachAsset('dogeCoin', { anchorX: 0.5, anchorY: 0.5 }); self.x = spawnX; self.y = spawnY; self.value = value || 1; // Default to 1 if no value passed self.isOnGround = false; self.isBeingCollected = false; self.spinSpeed = 0.1; // Radians per frame for spinning self.collectionRadius = 300; // How close Doge needs to be self.lifeTime = 1500; // Frames before it disappears if not collected (25s @ 60fps with gameSpeedMultiplier = 1.0) // Initial scatter animation var scatterAngle = Math.random() * Math.PI * 2; var scatterDistance = 20 + Math.random() * 30; var targetX = self.x + Math.cos(scatterAngle) * scatterDistance; var targetY = self.y + Math.sin(scatterAngle) * scatterDistance; // Start with smaller scale and grow to normal size self.graphic.scale.set(0.5); // Animate the coin to scatter position and grow to normal size tween(self, { x: targetX, y: targetY }, { duration: 300 * gameSpeedMultiplier, easing: tween.quadOut, onFinish: function onFinish() { self.isOnGround = true; } }); tween(self.graphic.scale, { x: 1.0, y: 1.0 }, { duration: 300 * gameSpeedMultiplier, easing: tween.quadOut }); // Update method for coin behavior self.update = function () { self.lifeTime -= gameSpeedMultiplier; // Destroy if lifetime expires and not being collected if (self.lifeTime <= 0 && !self.isBeingCollected) { self.destroy(); return; } // Skip other updates if being collected if (self.isBeingCollected) { return; } // Spin when on ground if (self.isOnGround) { self.graphic.rotation += self.spinSpeed * gameSpeedMultiplier; } // Check if Doge is close enough to collect if (self.isOnGround && doge && doge.parent) { var dx = doge.x - self.x; var dy = doge.y - self.y; if (dx * dx + dy * dy < self.collectionRadius * self.collectionRadius) { self.collect(); } } }; // Collect method when Doge picks up the coin self.collect = function () { if (self.isBeingCollected) { return; } self.isBeingCollected = true; // Update currency and UI currency += self.value; currencyText.setText("$: " + currency); // Show floating text spawnFloatingText("+" + self.value + "$", self.x, self.y - 20, { fill: 0xFFFF00, velocityY: -2 }); // Get target position for animation var targetUiGlobal = currencyText.toGlobal({ x: currencyText.width / 2, y: currencyText.height / 2 }); var targetUiInGame = game.toLocal(targetUiGlobal); // Animate coin flying to the UI tween(self, { x: targetUiInGame.x, y: targetUiInGame.y, alpha: 0.5 }, { duration: 500 * gameSpeedMultiplier, easing: tween.quadIn, onFinish: function onFinish() { self.destroy(); } }); // Scale graphic separately since nested properties need their own tween tween(self.graphic.scale, { x: 0.2, y: 0.2 }, { duration: 500 * gameSpeedMultiplier, easing: tween.quadIn }); }; return self; }); // DogeHero - Now includes auto-attack and manual bark ability logic var DogeHero = Container.expand(function () { var self = Container.call(this); self.graphic = self.attachAsset('dogeHero', { anchorX: 0.5, anchorY: 0.5 }); // Expose graphic if needed for hit check self.width = self.graphic.width; // Store size for hit checks self.height = self.graphic.height; self.speed = 5; self.targetX = self.x; self.targetY = self.y; // Auto Attack Stats self.autoAttackRange = 450; // Increased from 280 self.autoAttackDamage = 1; // Decreased from 2 self.autoAttackCooldownTime = 45; self.currentAutoAttackCooldown = 0; // Manual Bark Ability Stats self.manualBarkCooldownTime = 300; self.currentManualBarkCooldown = 0; self.update = function () { // Movement var dx = self.targetX - self.x; var dy = self.targetY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > self.speed) { dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; } else if (distance > 0) { self.x = self.targetX; self.y = self.targetY; } // Cooldowns if (self.currentAutoAttackCooldown > 0) { self.currentAutoAttackCooldown -= gameSpeedMultiplier; } if (self.currentManualBarkCooldown > 0) { self.currentManualBarkCooldown -= gameSpeedMultiplier; } // Auto Attack Logic if (self.currentAutoAttackCooldown <= 0) { var closestEnemy = null; var minDistanceSq = self.autoAttackRange * self.autoAttackRange; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var ex = enemy.x - self.x; var ey = enemy.y - self.y; var distSq = ex * ex + ey * ey; if (distSq < minDistanceSq) { minDistanceSq = distSq; closestEnemy = enemy; } } if (closestEnemy) { // Play sound and reset cooldown LK.getSound('dogeAutoAttack').play(); self.currentAutoAttackCooldown = self.autoAttackCooldownTime; // Create projectile that will handle damage application var projectile = new DogeAutoProjectile(); projectile.x = self.x; projectile.y = self.y; projectile.target = closestEnemy; projectile.damage = self.autoAttackDamage; // Pass damage value to projectile game.addChild(projectile); } } }; self.setTarget = function (x, y) { // Check if we're trying to target a BuildSpot or any of its menus var isClickingUI = false; // Skip movement if there's any active BuildSpot with open menus if (currentActiveBuildSpot) { isClickingUI = true; } // Check if clicked on any BuildSpot if (level && level.buildSpots && !isClickingUI) { for (var i = 0; i < level.buildSpots.length; i++) { var spot = level.buildSpots[i]; // If we passed the graphic check in game.down // but still reached setTarget, check again var dx = x - spot.x; var dy = y - spot.y; var distSq = dx * dx + dy * dy; var hitRadius = 75; // Half of the 150px BuildSpot width if (distSq <= hitRadius * hitRadius) { isClickingUI = true; break; } } } // Only move if not clicking UI if (!isClickingUI) { self.targetX = x; self.targetY = y; } }; self.manualBark = function () { if (self.currentManualBarkCooldown <= 0) { var wave = new BarkWave(); wave.x = self.x; wave.y = self.y; game.addChild(wave); LK.getSound('dogeBark').play(); self.currentManualBarkCooldown = self.manualBarkCooldownTime; return true; } return false; }; return self; }); // ---- START: REPLACE Enemy CLASS WITH THIS ---- var Enemy = Container.expand(function (spawnX, spawnY, value, customPathData) { var self = Container.call(this); // Added customPathData // Animation Properties (as they were) self.walkFrames = ['enemyPaper', 'enemyPaper_walk_1', 'enemyPaper_walk_2']; self.currentAnimFrame = 0; self.animationSpeed = 15; self.animationCounter = 0; // Path Properties (NEW) if (customPathData && customPathData.length > 0) { self.myPathPoints = customPathData; } else { // Fallback to global pathPoints if no custom path is provided self.myPathPoints = pathPoints; } self.currentPathIndex = 0; // Initialize for its own path // Other Enemy Properties (as they were) self.health = 6; // Your updated health self.speed = 2; self.value = value || 10; // Use passed value or default // Initial Graphic Setup (using first frame of its own path for spawn) // The spawnX and spawnY passed to constructor should ideally be myPathPoints[0].x and .y // If not, self.x and self.y will be set below. self.graphic = self.attachAsset(self.walkFrames[0], { anchorX: 0.5, anchorY: 0.5 }); // Set initial position based on constructor arguments or first path point if (spawnX !== undefined && spawnY !== undefined) { self.x = spawnX; self.y = spawnY; } else if (self.myPathPoints && self.myPathPoints.length > 0) { self.x = self.myPathPoints[0].x; self.y = self.myPathPoints[0].y; } self.update = function () { // --- Path Following Logic (Uses self.myPathPoints) --- if (!self.myPathPoints || self.myPathPoints.length === 0) { // Check its own path return; } if (self.currentPathIndex < self.myPathPoints.length) { // Check its own path var target = self.myPathPoints[self.currentPathIndex]; // Get target from its own path if (!target) { self.currentPathIndex++; return; } var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed * 1.5) { self.currentPathIndex++; if (self.currentPathIndex >= self.myPathPoints.length) { // Reached end of its path playerLives--; livesText.setText("Lives: " + playerLives); LK.getSound('enemyReachGoal').play(); if (playerLives <= 0) { if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } LK.showGameOver(); } self.destroy(); return; } } else { dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; } } else { // Already past the end of its path, should have been destroyed if (self.parent) { self.destroy(); } // Destroy if still somehow around return; } // --- Animation Logic (as it was, using remove/re-attach) --- if (self.walkFrames && self.walkFrames.length > 1) { self.animationCounter += gameSpeedMultiplier; if (self.animationCounter >= self.animationSpeed) { self.animationCounter = 0; self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length; var newAsset = self.walkFrames[self.currentAnimFrame]; if (self.graphic && self.graphic.parent) { var currentLocalX = self.graphic.x; // Should be 0 if graphic is root of self var currentLocalY = self.graphic.y; // Should be 0 self.removeChild(self.graphic); self.graphic = self.attachAsset(newAsset, { anchorX: 0.5, anchorY: 0.5, x: currentLocalX, y: currentLocalY }); } else if (!self.graphic && self.parent) { self.graphic = self.attachAsset(newAsset, { anchorX: 0.5, anchorY: 0.5 }); } } } }; self.takeDamage = function (amount) { self.health -= amount; LK.effects.flashObject(self, 0xff0000, 200); if (self.health <= 0 && self.parent) { var numCoins = Math.ceil(self.value / 5); if (numCoins < 1) { numCoins = 1; } for (var i = 0; i < numCoins; i++) { var coinValue = Math.floor(self.value / numCoins); if (i === 0 && self.value % numCoins !== 0) { coinValue += self.value % numCoins; } // Distribute remainder var coin = new DogeCoin(self.x, self.y, coinValue); if (coinLayer) { // Use global coinLayer coinLayer.addChild(coin); } else { // Fallback, though coinLayer should be defined game.addChild(coin); } } LK.setScore(LK.getScore() + self.value); scoreText.setText("Score: " + LK.getScore()); LK.getSound('enemyDeath').play(); self.destroy(); } }; // Standard destroy method (if not inheriting a more complex one from Container) var originalDestroy = self.destroy; // Capture prototype destroy if it exists self.destroy = function () { if (typeof originalDestroy === 'function') { originalDestroy.call(self); } else if (self.parent) { self.parent.removeChild(self); } }; return self; }); // ---- END: REPLACE Enemy CLASS WITH THIS ---- // GameLevel class remains the same var GameLevel = Container.expand(function () { var self = Container.call(this); // Initialize pathGraphics as an instance property self.pathGraphics = []; // buildSpotGraphics can remain local to createBuildSpots or also be an instance property if needed elsewhere // var buildSpotGraphics = []; // If you want to keep it as an instance property for build spots: self.buildSpotGraphics = []; self.createPath = function (pathData) { // DO NOT clear pathGraphics here, allow accumulation if called multiple times // pathGraphics.forEach(function (tile) { tile.destroy(); }); // REMOVED // pathGraphics = []; // REMOVED if (!pathData || pathData.length === 0) { console.error("createPath called with no pathData"); return; } for (var i = 0; i < pathData.length - 1; i++) { var start = pathData[i]; var end = pathData[i + 1]; if (!start || !end) { continue; } var dx = end.x - start.x; var dy = end.y - start.y; var distance = Math.sqrt(dx * dx + dy * dy); var steps = Math.ceil(distance / 90); for (var j = 0; j < steps; j++) { var ratio = j / steps; var x = start.x + dx * ratio; var y = start.y + dy * ratio; var tile = LK.getAsset('pathTile', { x: x, y: y, alpha: 0.3, anchorX: 0.5, anchorY: 0.5 }); self.addChild(tile); // Add tile to the GameLevel container self.pathGraphics.push(tile); // Store reference in instance array } } }; self.createBuildSpots = function (spotsData) { // Clear previous build spots graphics for THIS INSTANCE of GameLevel self.buildSpotGraphics.forEach(function (spotGraphic) { // Assuming buildSpotGraphics are direct children if (spotGraphic.parent) { // Ensure it has a parent before trying to remove spotGraphic.destroy(); // Destroy the BuildSpot instance } }); self.buildSpotGraphics = []; // Reset the array for graphics // Also clear the logical buildSpots array on the level if it exists // This assumes self.buildSpots is where the actual BuildSpot objects are stored if (self.buildSpots) { self.buildSpots.forEach(function (spotObject) { if (spotObject && typeof spotObject.destroy === 'function') { // spotObject.destroy(); // This was already handled by destroying the graphic if spot is the graphic } }); } self.buildSpots = []; // Reset logical array if (!spotsData || spotsData.length === 0) { console.error("createBuildSpots called with no spotsData"); return; } for (var i = 0; i < spotsData.length; i++) { if (!spotsData[i]) { continue; } var spot = new BuildSpot(); // Create new BuildSpot instance spot.x = spotsData[i].x; spot.y = spotsData[i].y; self.addChild(spot); // Add BuildSpot instance to GameLevel container self.buildSpots.push(spot); // Store logical spot instance self.buildSpotGraphics.push(spot.graphic); // Store reference to its graphic if needed for other reasons // Or just spot, if spot itself is the main thing to track } }; // Add a destroy method to GameLevel to clean up its children self.destroy = function () { // Destroy all children (path tiles, build spots that are children of this level) while (self.children.length > 0) { var child = self.children[0]; if (typeof child.destroy === 'function') { child.destroy(); // Prefer calling child's own destroy method } else if (child.parent) { child.parent.removeChild(child); // Fallback } else { break; // Should not happen if parentage is correct } } self.pathGraphics = []; self.buildSpotGraphics = []; self.buildSpots = []; if (self.parent) { self.parent.removeChild(self); } }; return self; }); // ---- END: REPLACE GameLevel CLASS WITH THIS ---- // Goal class remains the same var Goal = Container.expand(function () { var self = Container.call(this); var graphic = self.attachAsset('goal', { anchorX: 0.5, anchorY: 0.5, alpha: 0.6 }); return self; }); // ---- START: REPLACE InternOnCoffeeRun CLASS WITH THIS ---- var InternOnCoffeeRun = Container.expand(function (spawnX, spawnY, valueToIgnore, customPathData) { var self = Container.call(this); // Animation Properties self.walkFrames = ['enemyIntern', 'enemyIntern_walk_1', 'enemyIntern_walk_2']; self.currentAnimFrame = 0; self.animationSpeed = 5; self.animationCounter = 0; // Path Properties if (customPathData && customPathData.length > 0) { self.myPathPoints = customPathData; } else { self.myPathPoints = pathPoints; } self.currentPathIndex = 0; // Specific stats for Intern self.health = 4; self.speed = 4.0; self.value = 5; self.isFlying = false; // Initial Graphic & Position self.graphic = self.attachAsset(self.walkFrames[0], { anchorX: 0.5, anchorY: 0.5 }); if (self.myPathPoints && self.myPathPoints.length > 0) { self.x = self.myPathPoints[0].x; self.y = self.myPathPoints[0].y; } else if (spawnX !== undefined && spawnY !== undefined) { self.x = spawnX; self.y = spawnY; } self.update = function () { // Path Following Logic if (!self.myPathPoints || self.myPathPoints.length === 0) { return; } if (self.currentPathIndex < self.myPathPoints.length) { var target = self.myPathPoints[self.currentPathIndex]; if (!target) { self.currentPathIndex++; return; } var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed * 1.5) { self.currentPathIndex++; if (self.currentPathIndex >= self.myPathPoints.length) { playerLives--; livesText.setText("Lives: " + playerLives); LK.getSound('enemyReachGoal').play(); if (playerLives <= 0) { if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } LK.showGameOver(); } self.destroy(); return; } } else { dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; } } else { if (self.parent) { self.destroy(); } return; } // Animation Logic if (self.walkFrames && self.walkFrames.length > 1) { self.animationCounter += gameSpeedMultiplier; if (self.animationCounter >= self.animationSpeed) { self.animationCounter = 0; self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length; var newAsset = self.walkFrames[self.currentAnimFrame]; if (self.graphic && self.graphic.parent) { var currentLocalX = self.graphic.x; var currentLocalY = self.graphic.y; self.removeChild(self.graphic); self.graphic = self.attachAsset(newAsset, { anchorX: 0.5, anchorY: 0.5, x: currentLocalX, y: currentLocalY }); } else if (!self.graphic && self.parent) { self.graphic = self.attachAsset(newAsset, { anchorX: 0.5, anchorY: 0.5 }); } } } }; self.takeDamage = function (amount) { self.health -= amount; LK.effects.flashObject(self, 0xff0000, 200); if (self.health <= 0 && self.parent) { var numCoins = Math.ceil(self.value / 5); if (numCoins < 1) { numCoins = 1; } for (var i = 0; i < numCoins; i++) { var coinValue = Math.floor(self.value / numCoins); if (i === 0 && self.value % numCoins !== 0) { coinValue += self.value % numCoins; } var coin = new DogeCoin(self.x, self.y, coinValue); if (coinLayer) { coinLayer.addChild(coin); } else { game.addChild(coin); } } LK.setScore(LK.getScore() + self.value); scoreText.setText("Score: " + LK.getScore()); LK.getSound('enemyDeath').play(); self.destroy(); } }; var originalDestroy = self.destroy; self.destroy = function () { if (typeof originalDestroy === 'function') { originalDestroy.call(self); } else if (self.parent) { self.parent.removeChild(self); } }; return self; }); // ---- END: REPLACE InternOnCoffeeRun CLASS WITH THIS ---- var LaserCatPerch = Container.expand(function () { var self = Container.call(this); // Initialize the tower with level 0 stats and graphics initializeTowerFromData(self, 'laserCat', 0); self.activeBeams = []; // Array to store info about current beams {target, beamGraphic, durationTimer, currentDotDamage} self.cooldownTimer = 0; // Time until it can look for new targets after all beams finish or max targets reached self.update = function () { var buildSpot = self.parent; if (!buildSpot || !buildSpot.parent) { return; } // Get tower position in game space var towerGraphicGlobalCenter = self.graphic.toGlobal({ x: 0, y: 0 }); var towerPosInGameSpace = game.toLocal(towerGraphicGlobalCenter); // Decrement cooldown timer if (self.cooldownTimer > 0) { self.cooldownTimer -= 1000 / 60 * gameSpeedMultiplier; // Convert from ms to frames } // Handle Active Beams for (var i = self.activeBeams.length - 1; i >= 0; i--) { var beamData = self.activeBeams[i]; // Check if target is still valid and in range if (!beamData.target || !beamData.target.parent) { // Target no longer exists beamData.beamGraphic.destroy(); self.activeBeams.splice(i, 1); continue; } // Check if target is out of range var dx = beamData.target.x - towerPosInGameSpace.x; var dy = beamData.target.y - towerPosInGameSpace.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > self.range) { // Target out of range beamData.beamGraphic.destroy(); self.activeBeams.splice(i, 1); continue; } // Decrement beam duration timer beamData.durationTimer -= 1000 / 60 * gameSpeedMultiplier; // Apply DOT damage at intervals beamData.dotTickTimer -= 1000 / 60 * gameSpeedMultiplier; if (beamData.dotTickTimer <= 0) { // Apply damage beamData.target.takeDamage(beamData.currentDotDamage); // Ramp up damage for next tick beamData.currentDotDamage *= self.dotRampUpFactor; // Reset tick timer (damage every 0.5 seconds) beamData.dotTickTimer = 500; } // Update beam visual // Position beam at tower position beamData.beamGraphic.x = towerPosInGameSpace.x; beamData.beamGraphic.y = towerPosInGameSpace.y; // Calculate direction and distance var targetPosInGameSpace = { x: beamData.target.x, y: beamData.target.y }; var dx = targetPosInGameSpace.x - towerPosInGameSpace.x; var dy = targetPosInGameSpace.y - towerPosInGameSpace.y; var distance = Math.sqrt(dx * dx + dy * dy); // Adjust beam rotation and height beamData.beamGraphic.rotation = Math.atan2(dy, dx) - Math.PI / 2; // Adjust rotation beamData.beamGraphic.height = distance; // Stretch beam to reach target // Make sure the beam is in the game container (not a child of the tower) if (!beamData.beamGraphic.parent) { game.addChild(beamData.beamGraphic); } else if (beamData.beamGraphic.parent !== game) { if (beamData.beamGraphic.parent) { beamData.beamGraphic.parent.removeChild(beamData.beamGraphic); } game.addChild(beamData.beamGraphic); } // If beam duration is up, destroy it if (beamData.durationTimer <= 0) { beamData.beamGraphic.destroy(); self.activeBeams.splice(i, 1); } } // Find New Targets if not on cooldown and not at max beams if (self.cooldownTimer <= 0 && self.activeBeams.length < self.numberOfBeams) { // Find potential targets in range that aren't already targeted var potentialTargets = []; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (!enemy || !enemy.parent) { continue; } // Check if enemy is in range var dx = enemy.x - towerPosInGameSpace.x; var dy = enemy.y - towerPosInGameSpace.y; var distanceSq = dx * dx + dy * dy; if (distanceSq <= self.range * self.range) { // Check if enemy is already targeted by an active beam var alreadyTargeted = false; for (var j = 0; j < self.activeBeams.length; j++) { if (self.activeBeams[j].target === enemy) { alreadyTargeted = true; break; } } if (!alreadyTargeted) { potentialTargets.push(enemy); } } } // If targets available, target the strongest (highest health) if (potentialTargets.length > 0) { // Sort by health (highest first) potentialTargets.sort(function (a, b) { return b.health - a.health; }); // Target the strongest enemy var targetEnemy = potentialTargets[0]; // Create beam graphic var beamGfx = LK.getAsset('laserBeam', { anchorX: 0.5, anchorY: 0 // Anchor at top center }); // Position at tower beamGfx.x = towerPosInGameSpace.x; beamGfx.y = towerPosInGameSpace.y; // Calculate direction and distance to target var dx = targetEnemy.x - towerPosInGameSpace.x; var dy = targetEnemy.y - towerPosInGameSpace.y; var distance = Math.sqrt(dx * dx + dy * dy); // Set rotation and height (stretch beam to reach target) beamGfx.rotation = Math.atan2(dy, dx) - Math.PI / 2; beamGfx.height = distance; // Add to game (below enemy layer if possible, or directly to game) game.addChild(beamGfx); // Apply initial damage targetEnemy.takeDamage(self.initialBeamDamage); // Play firing sound // LK.getSound('sfxLaserCatFireInitialPew').play(); // Start beam loop sound // var beamLoopSound = LK.getSound('sfxLaserCatFireBeamLoop').play(); // Add to active beams self.activeBeams.push({ target: targetEnemy, beamGraphic: beamGfx, durationTimer: self.maxBeamDuration, currentDotDamage: self.dotDamagePerSecond, dotTickTimer: 500 // First tick after 0.5 seconds }); // If at max beams, start cooldown if (self.activeBeams.length >= self.numberOfBeams) { self.cooldownTimer = self.cooldownBetweenBeams; } } } // Targeting hum sound logic if (self.activeBeams.length > 0) { // Play targeting hum sound if not already playing // if (!humSoundPlaying) { // humSoundPlaying = true; // LK.getSound('sfxLaserCatTargetHum').play(); // } } else { // Stop hum sound if playing // if (humSoundPlaying) { // humSoundPlaying = false; // LK.getSound('sfxLaserCatTargetHum').stop(); // } } }; // Override destroy method to clean up beams var originalDestroy = self.destroy; self.destroy = function () { // Clean up all active beams for (var i = 0; i < self.activeBeams.length; i++) { if (self.activeBeams[i].beamGraphic) { self.activeBeams[i].beamGraphic.destroy(); } } // Stop any looping sounds // if (humSoundPlaying) { // LK.getSound('sfxLaserCatTargetHum').stop(); // } // Call original destroy if (originalDestroy) { originalDestroy.call(self); } else if (self.parent) { self.parent.removeChild(self); } }; self.shoot = function (target) { // This method is kept for compatibility but not used actively // The beam firing logic is handled in update() }; return self; }); // Range Indicator to visualize tower attack ranges var RangeIndicator = Container.expand(function (centerX, centerY, radius, parentContainer) { var self = Container.call(this); // Create container for the indicator self.x = centerX; self.y = centerY; // Since we can't draw circles directly in LK, use a pre-defined circle asset // and scale it to match our desired radius var rangeCircle = self.attachAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, alpha: 0.05, // Very faint fill tint: 0xFFFFFF // White color }); // The centerCircle asset is 100px in diameter, so scale accordingly var scaleRatio = radius * 2 / 100; rangeCircle.scaleX = scaleRatio; rangeCircle.scaleY = scaleRatio; // Add a slightly larger circle with higher alpha for the border effect var rangeBorder = self.attachAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, alpha: 0.3, // More visible than the fill tint: 0xFFFFFF // White color }); // Make border slightly larger var borderScaleRatio = scaleRatio * 1.02; rangeBorder.scaleX = borderScaleRatio; rangeBorder.scaleY = borderScaleRatio; // Add to parent container if (parentContainer) { parentContainer.addChild(self); } return self; }); // ---- START: REPLACE RedTapeWorm CLASS WITH THIS ---- var RedTapeWorm = Container.expand(function (spawnX, spawnY, valueToIgnore, customPathData) { var self = Container.call(this); // Animation Properties self.walkFrames = ['enemyRedTapeWorm', 'enemyRedTapeWorm_walk_1', 'enemyRedTapeWorm_walk_2']; self.currentAnimFrame = 0; self.animationSpeed = 10; self.animationCounter = 0; // Path Properties if (customPathData && customPathData.length > 0) { self.myPathPoints = customPathData; } else { self.myPathPoints = pathPoints; } self.currentPathIndex = 0; // Specific stats for RedTapeWorm self.health = 30; self.speed = 0.75; self.value = 25; self.isFlying = false; // Initial Graphic & Position self.graphic = self.attachAsset(self.walkFrames[0], { anchorX: 0.5, anchorY: 0.5 }); if (self.myPathPoints && self.myPathPoints.length > 0) { self.x = self.myPathPoints[0].x; self.y = self.myPathPoints[0].y; } else if (spawnX !== undefined && spawnY !== undefined) { self.x = spawnX; self.y = spawnY; } self.update = function () { // Path Following Logic (Uses self.myPathPoints) if (!self.myPathPoints || self.myPathPoints.length === 0) { return; } if (self.currentPathIndex < self.myPathPoints.length) { var target = self.myPathPoints[self.currentPathIndex]; if (!target) { self.currentPathIndex++; return; } var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed * 1.5) { self.currentPathIndex++; if (self.currentPathIndex >= self.myPathPoints.length) { playerLives--; livesText.setText("Lives: " + playerLives); LK.getSound('enemyReachGoal').play(); if (playerLives <= 0) { if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } LK.showGameOver(); } self.destroy(); return; } } else { dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; } } else { if (self.parent) { self.destroy(); } return; } // Animation Logic if (self.walkFrames && self.walkFrames.length > 1) { self.animationCounter += gameSpeedMultiplier; if (self.animationCounter >= self.animationSpeed) { self.animationCounter = 0; self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length; var newAsset = self.walkFrames[self.currentAnimFrame]; if (self.graphic && self.graphic.parent) { var currentLocalX = self.graphic.x; var currentLocalY = self.graphic.y; self.removeChild(self.graphic); self.graphic = self.attachAsset(newAsset, { anchorX: 0.5, anchorY: 0.5, x: currentLocalX, y: currentLocalY }); } else if (!self.graphic && self.parent) { self.graphic = self.attachAsset(newAsset, { anchorX: 0.5, anchorY: 0.5 }); } } } }; self.takeDamage = function (amount) { self.health -= amount; LK.effects.flashObject(self, 0xff0000, 200); if (self.health <= 0 && self.parent) { var numCoins = Math.ceil(self.value / 5); if (numCoins < 1) { numCoins = 1; } for (var i = 0; i < numCoins; i++) { var coinValue = Math.floor(self.value / numCoins); if (i === 0 && self.value % numCoins !== 0) { coinValue += self.value % numCoins; } var coin = new DogeCoin(self.x, self.y, coinValue); if (coinLayer) { coinLayer.addChild(coin); } else { game.addChild(coin); } } LK.setScore(LK.getScore() + self.value); scoreText.setText("Score: " + LK.getScore()); LK.getSound('enemyDeath').play(); self.destroy(); } }; var originalDestroy = self.destroy; self.destroy = function () { // Add any RedTapeWorm-specific cleanup here if needed in future if (typeof originalDestroy === 'function') { originalDestroy.call(self); } else if (self.parent) { self.parent.removeChild(self); } }; return self; }); // ---- END: REPLACE RedTapeWorm CLASS WITH THIS ---- var RestructuringSpecialist = Container.expand(function () { var self = Container.call(this); // Initialize the tower with level 0 stats and graphics initializeTowerFromData(self, 'restruct', 0); // Charge properties self.currentCharge = 0; self.chargeUpTime = 360; // Frames to fully charge self.currentTarget = null; self.isFiring = false; self.chargeVisuals = []; self.chargeSoundInstance = null; self.chargeSfx = 'sfxRestructCharge'; self.update = function () { // If tower is in the firing animation, don't do anything else if (self.isFiring) { return; } // Get tower position in game space var buildSpot = self.parent; if (!buildSpot || !buildSpot.parent) { return; } var towerGraphicGlobalCenter = self.graphic.toGlobal({ x: 0, y: 0 }); var towerPosInGameSpace = game.toLocal(towerGraphicGlobalCenter); // Check if current target exists and is still valid if (self.currentTarget && self.currentTarget.parent) { // Check if target is still in range var targetPosInGameSpace = { x: self.currentTarget.x, y: self.currentTarget.y }; var dx = targetPosInGameSpace.x - towerPosInGameSpace.x; var dy = targetPosInGameSpace.y - towerPosInGameSpace.y; var distanceSq = dx * dx + dy * dy; // If target moved out of range, reset charging if (distanceSq > self.range * self.range) { self.currentTarget = null; self.currentCharge = 0; self.clearChargeVisuals(); self.stopChargeSound(); return; } // Continue charging up self.currentCharge += gameSpeedMultiplier; // Update the charge visuals self.updateChargeVisuals(towerPosInGameSpace); // Update charge sound if (!self.chargeSoundInstance) { self.chargeSoundInstance = LK.getSound(self.chargeSfx || 'sfxRestructCharge').play({ loop: true, volume: 0.3 }); } else { // Optionally increase volume/pitch as charge builds up var chargeProgress = Math.min(1.0, self.currentCharge / self.chargeUpTime); if (self.chargeSoundInstance.volume) { self.chargeSoundInstance.volume = 0.3 + 0.7 * chargeProgress; } } // If fully charged, restructure the enemy if (self.currentCharge >= self.chargeUpTime) { self.fireRestructure(); } } else { // No valid target or target was lost, reset charge self.currentCharge = 0; self.clearChargeVisuals(); self.stopChargeSound(); // Find new target based on priority var newTarget = null; var maxHealth = 0; // Iterate through enemies to find valid targets in range for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (!enemy || !enemy.parent) { continue; } var dx = enemy.x - towerPosInGameSpace.x; var dy = enemy.y - towerPosInGameSpace.y; var distanceSq = dx * dx + dy * dy; if (distanceSq <= self.range * self.range) { // If targeting strongest enemies, track the one with highest health if (self.targetPriority === 'strongest') { if (enemy.health > maxHealth) { maxHealth = enemy.health; newTarget = enemy; } } else { // Default to first enemy in range if no specific priority newTarget = enemy; break; } } } // If a new target was found, set it if (newTarget) { self.currentTarget = newTarget; self.currentCharge = 0; // Start charging from zero } } }; self.updateChargeVisuals = function (towerPos) { // Initialize charge visuals array if needed if (!self.chargeVisuals) { self.chargeVisuals = []; } // Calculate how many visuals to show based on charge progress var numVisuals = Math.floor(self.currentCharge / self.chargeUpTime * 5); // Max 5 visuals // Add new visuals if needed while (self.chargeVisuals.length < numVisuals) { // Randomly choose between two visual types var visualAsset = Math.random() < 0.5 ? 'flowchartSegment1' : 'flowchartSegment2'; var v = self.attachAsset(visualAsset, { anchorX: 0.5, anchorY: 0.5 }); self.chargeVisuals.push(v); } // Remove excess visuals if needed while (self.chargeVisuals.length > numVisuals) { var v = self.chargeVisuals.pop(); v.destroy(); } // Update the position of each visual to orbit the tower for (var i = 0; i < self.chargeVisuals.length; i++) { var v = self.chargeVisuals[i]; // Calculate orbit position based on charge progress and visual index v.x = Math.cos(self.currentCharge * 0.1 + i * Math.PI * 0.4) * 50; v.y = Math.sin(self.currentCharge * 0.1 + i * Math.PI * 0.4) * 50; // Optional: increase scale or alpha as charge progresses var chargeProgress = self.currentCharge / self.chargeUpTime; v.alpha = 0.5 + chargeProgress * 0.5; v.scale.set(0.8 + chargeProgress * 0.4); } }; self.clearChargeVisuals = function () { // Destroy all charge visuals if (self.chargeVisuals) { for (var i = 0; i < self.chargeVisuals.length; i++) { self.chargeVisuals[i].destroy(); } self.chargeVisuals = []; } }; self.stopChargeSound = function () { // Stop the charging sound if it's playing if (self.chargeSoundInstance) { self.chargeSoundInstance.stop(); self.chargeSoundInstance = null; } }; self.fireRestructure = function () { // Exit if no valid target or already firing if (!self.currentTarget || !self.currentTarget.parent || self.isFiring) { return; } self.isFiring = true; self.stopChargeSound(); self.clearChargeVisuals(); // Play launch SFX LK.getSound(self.launchSfx || 'sfxRestructLaunchDoge').play(); // Store reference to target before potentially nulling var enemyToRestructure = self.currentTarget; var targetPosInGameSpace = { x: enemyToRestructure.x, y: enemyToRestructure.y }; // Spawn restructure effect on target var effect = game.attachAsset(self.effectAsset || 'restructureEffect', { anchorX: 0.5, anchorY: 0.5, x: targetPosInGameSpace.x, y: targetPosInGameSpace.y }); // Play impact sound LK.getSound(self.impactSfx || 'sfxRestructImpact').play(); // Apply damage or kill if (enemyToRestructure.health <= self.instantKillThreshold) { // Instant kill if below threshold spawnFloatingText("OPTIMIZED!", enemyToRestructure.x, enemyToRestructure.y, { fill: 0x00FFFF }); enemyToRestructure.destroy(); } else { // Otherwise apply damage enemyToRestructure.takeDamage(self.damage); } // Golden Parachute logic (Level 3+) if (self.currentLevel >= 2 && self.goldenParachuteDamage > 0 && (!enemyToRestructure.parent || enemyToRestructure.health <= 0)) { // If enemy was killed, apply splash damage spawnFloatingText("Severance!", targetPosInGameSpace.x, targetPosInGameSpace.y - 30, { fill: 0xFFD700 }); for (var i = 0; i < enemies.length; i++) { var nearbyEnemy = enemies[i]; if (nearbyEnemy === enemyToRestructure || !nearbyEnemy.parent) { continue; } var dx = nearbyEnemy.x - targetPosInGameSpace.x; var dy = nearbyEnemy.y - targetPosInGameSpace.y; if (dx * dx + dy * dy < self.goldenParachuteRadius * self.goldenParachuteRadius) { nearbyEnemy.takeDamage(self.goldenParachuteDamage); } } } // Reset state and clean up self.currentCharge = 0; self.currentTarget = null; // Clean up effect after animation LK.setTimeout(function () { if (effect && effect.parent) { effect.destroy(); } // Clear firing flag after brief delay LK.setTimeout(function () { self.isFiring = false; }, 200 / gameSpeedMultiplier); }, 500); }; self.findTarget = function () { // This method is preserved for backward compatibility // The targeting logic is now handled in update() }; self.restructureEnemy = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.isCharging = false; self.chargeLevel = 0; return; } // Create visual effect var effect = self.attachAsset('restructureEffect', { anchorX: 0.5, anchorY: 0.5 }); // Destroy effect after animation LK.setTimeout(function () { if (effect && effect.parent) { effect.parent.removeChild(effect); } }, 500); // Give player currency and score for the transformation currency += self.targetEnemy.value; currencyText.setText("$: " + currency); LK.setScore(LK.getScore() + self.targetEnemy.value); scoreText.setText("Score: " + LK.getScore()); // Play effect sound LK.getSound('sfxRestructTransformKill').play(); // Remove the enemy self.targetEnemy.destroy(); // Reset tower state self.isCharging = false; self.chargeLevel = 0; self.targetEnemy = null; }; return self; }); var RickAstleyProjectile = Container.expand(function (startX, startY, endX, endY, towerOwner) { var self = Container.call(this); self.graphic = self.attachAsset(towerOwner.popupAsset, { anchorX: 0.5, anchorY: 0.5, alpha: 0.7, scaleX: 0.5, scaleY: 0.5 }); self.x = startX; self.y = startY; self.targetX = endX; self.targetY = endY; self.speed = towerOwner.projectileSpeed || 7; // Use tower's speed or default to 7 self.towerOwner = towerOwner; self.hasHitTarget = false; self.updateProjectile = function () { if (self.hasHitTarget) { return; } // Calculate direction and distance var dx = self.targetX - self.x; var dy = self.targetY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if reached target if (distance < self.speed * gameSpeedMultiplier) { self.hasHitTarget = true; self.towerOwner.activateEffectAtLocation(self.targetX, self.targetY); self.destroy(); return; } // Move towards target dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; }; return self; }); var RickrollerTower = Container.expand(function () { var self = Container.call(this); // Initialize the tower with level 0 stats and graphics initializeTowerFromData(self, 'rickroller', 0); self.currentCooldown = 0; // Time in ms until it can fire again self.activeProjectiles = []; // To track flying Rick Astleys self.activePopups = []; // To keep track of all active Rick popups for dancing self.update = function () { // Cooldown Management if (self.currentCooldown > 0) { self.currentCooldown -= 1000 / 60 * gameSpeedMultiplier; } else { self.findTargetAndFire(); } // Update Active Projectiles for (var i = self.activeProjectiles.length - 1; i >= 0; i--) { var rickProjectile = self.activeProjectiles[i]; rickProjectile.updateProjectile(); if (rickProjectile.hasHitTarget || !rickProjectile.parent) { self.activeProjectiles.splice(i, 1); } } // Update Rick Astley Dance Animations for (var i = self.activePopups.length - 1; i >= 0; i--) { var p = self.activePopups[i]; if (!p || !p.parent) { // If popup was destroyed elsewhere self.activePopups.splice(i, 1); continue; } if (p.isDancing) { self.updateRickDance(p); // Call method to update dance animation } } }; self.updateRickDance = function (popupGraphic) { var pg = popupGraphic; // shorthand for readability pg.danceTimer += 1000 / 60 * gameSpeedMultiplier; // Increment timer in ms var TILT_DURATION = 300 / gameSpeedMultiplier; // Duration of one tilt/flip segment var FLIP_POINT_IN_CYCLE = 2; // At which phase to trigger a flip if (pg.danceTimer >= TILT_DURATION) { pg.danceTimer = 0; pg.dancePhase = (pg.dancePhase + 1) % 4; // Cycle through 4 phases if (pg.dancePhase === 0) { // Tilt Left pg.rotation = -0.15; // Slight tilt in radians } else if (pg.dancePhase === 1) { // Tilt Right pg.rotation = 0.15; } else if (pg.dancePhase === 2) { // Center rotation pg.rotation = pg.originalRotation; // Flip horizontally on this phase pg.scale.x = -pg.originalScaleX; } else if (pg.dancePhase === 3) { // Center rotation and flip back pg.rotation = pg.originalRotation; pg.scale.x = pg.originalScaleX; } } }; self.findTargetAndFire = function () { var buildSpot = self.parent; if (!buildSpot || !buildSpot.parent) { return; } // Get tower position in game space var towerGraphicGlobalCenter = self.graphic.toGlobal({ x: 0, y: 0 }); var towerPosInGameSpace = game.toLocal(towerGraphicGlobalCenter); // Find target var closestEnemy = null; var minDistanceSq = self.range * self.range; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (!enemy || !enemy.parent) { continue; } var dx = enemy.x - towerPosInGameSpace.x; var dy = enemy.y - towerPosInGameSpace.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < minDistanceSq) { minDistanceSq = distanceSq; closestEnemy = enemy; } } if (closestEnemy) { var targetX = closestEnemy.x; var targetY = closestEnemy.y; // Create Rick Astley projectile var rick = new RickAstleyProjectile(towerPosInGameSpace.x, towerPosInGameSpace.y, targetX, targetY, self); self.activeProjectiles.push(rick); game.addChild(rick); // Start cooldown self.currentCooldown = self.cooldown; } }; self.activateEffectAtLocation = function (effectX, effectY) { // Spawn Rick Astley Popup (Stationary at target) var popup = game.attachAsset(self.popupAsset, { anchorX: 0.5, anchorY: 0.5, x: effectX, y: effectY }); // Add dance properties to the popup popup.isDancing = false; popup.dancePhase = 0; // For cycling through dance moves popup.danceTimer = 0; // Timer for current dance move popup.originalScaleX = popup.scale.x; // Store original scale.x (usually 1) popup.originalRotation = popup.rotation; // Store original rotation (usually 0) // Add to active popups list self.activePopups.push(popup); // Fade in/out animation popup.alpha = 0; tween(popup, // target { alpha: 1.0 }, // props { // options for fade-in duration: 250 / gameSpeedMultiplier, easing: tween.quadOut, onFinish: function onFinish() { // After fade-in completes // Start dancing once fade-in is complete popup.isDancing = true; LK.setTimeout(function () { // Hold duration if (popup && popup.parent) { // Stop dancing before fade-out popup.isDancing = false; // Reset appearance popup.rotation = popup.originalRotation; popup.scale.x = popup.originalScaleX; tween(popup, // target for fade-out { alpha: 0 }, // props { // options for fade-out duration: 250 / gameSpeedMultiplier, easing: tween.quadIn, onFinish: function onFinish() { if (popup.parent) { // Remove from active popups list var index = self.activePopups.indexOf(popup); if (index !== -1) { self.activePopups.splice(index, 1); } popup.destroy(); } } }); } }, (self.popupDuration - 500) / gameSpeedMultiplier); // Time to hold at full alpha } }); // Play Sound var soundToPlay = self.soundEffects[Math.floor(Math.random() * self.soundEffects.length)]; LK.getSound(soundToPlay).play(); // Find and Stun/Damage Enemies in Effect Radius var enemiesAffectedThisTrigger = []; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (!enemy || !enemy.parent) { continue; } var dx = enemy.x - effectX; var dy = enemy.y - effectY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.effectRadius) { if (!enemy.isStunned) { enemy.originalSpeed = enemy.speed; enemy.speed = 0; enemy.isStunned = true; enemy.isStunnedByRickroll = self; // So this tower can unstun enemy.graphic.tint = 0xFFD700; // Gold tint enemiesAffectedThisTrigger.push(enemy); // Start DOT on this enemy self.startDotOnEnemy(enemy); } } } // Unstun Timer LK.setTimeout(function () { for (var i = 0; i < enemiesAffectedThisTrigger.length; i++) { var affectedEnemy = enemiesAffectedThisTrigger[i]; if (affectedEnemy && affectedEnemy.parent && affectedEnemy.isStunnedByRickroll === self) { affectedEnemy.isStunned = false; affectedEnemy.speed = affectedEnemy.originalSpeed; affectedEnemy.isStunnedByRickroll = null; affectedEnemy.graphic.tint = 0xFFFFFF; } } }, self.stunDuration / gameSpeedMultiplier); }; self.startDotOnEnemy = function (enemyInstance) { var dotTicksRemaining = Math.floor(self.stunDuration / self.dotTickRate); function applyTick() { if (!enemyInstance || !enemyInstance.parent || enemyInstance.isStunnedByRickroll !== self || dotTicksRemaining <= 0) { return; } enemyInstance.takeDamage(self.damagePerTick); dotTicksRemaining--; if (dotTicksRemaining > 0) { LK.setTimeout(applyTick, self.dotTickRate / gameSpeedMultiplier); } } applyTick(); // Start the first tick }; return self; }); var SpamEmailUnit = Container.expand(function (spawnX, spawnY, valueToIgnore, customPathData) { var self = Container.call(this); // Animation Properties self.walkFrames = ['enemySpamEmail', 'enemySpamEmail_walk_1', 'enemySpamEmail_walk_2']; self.currentAnimFrame = 0; self.animationSpeed = 6; self.animationCounter = 0; // Path Properties if (customPathData && customPathData.length > 0) { self.myPathPoints = customPathData; } else { self.myPathPoints = pathPoints; } self.currentPathIndex = 0; // For flying, this might be handled differently if not path following // Specific stats for SpamEmailUnit self.health = 2; self.speed = 2.5; self.value = 1; self.isFlying = true; // Key property // Initial Graphic & Position self.graphic = self.attachAsset(self.walkFrames[0], { anchorX: 0.5, anchorY: 0.5 }); if (self.myPathPoints && self.myPathPoints.length > 0) { self.x = self.myPathPoints[0].x; self.y = self.myPathPoints[0].y; } else if (spawnX !== undefined && spawnY !== undefined) { self.x = spawnX; self.y = spawnY; } self.update = function () { // Flying units go directly to the goal (last point in THEIR ASSIGNED path) if (!self.myPathPoints || self.myPathPoints.length === 0) { return; } var target; if (self.isFlying) { target = self.myPathPoints[self.myPathPoints.length - 1]; // Target is the end of its path } else { // Fallback to normal path following if not flying (though this class is isFlying=true) if (self.currentPathIndex >= self.myPathPoints.length) { if (self.parent) { self.destroy(); } return; } target = self.myPathPoints[self.currentPathIndex]; } if (!target) { self.destroy(); return; } var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed * 1.5 * gameSpeedMultiplier) { // Check if reached goal (for flying) or next point if (self.isFlying || self.currentPathIndex >= self.myPathPoints.length - 1) { // Reached final goal playerLives--; livesText.setText("Lives: " + playerLives); LK.getSound('enemyReachGoal').play(); if (playerLives <= 0) { if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } LK.showGameOver(); } self.destroy(); return; } else { // For non-flying, move to next path index self.currentPathIndex++; } } else { dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; } // Animation Logic if (self.walkFrames && self.walkFrames.length > 1) { self.animationCounter += gameSpeedMultiplier; if (self.animationCounter >= self.animationSpeed) { self.animationCounter = 0; self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length; var newAsset = self.walkFrames[self.currentAnimFrame]; if (self.graphic && self.graphic.parent) { var currentLocalX = self.graphic.x; var currentLocalY = self.graphic.y; self.removeChild(self.graphic); self.graphic = self.attachAsset(newAsset, { anchorX: 0.5, anchorY: 0.5, x: currentLocalX, y: currentLocalY }); } else if (!self.graphic && self.parent) { self.graphic = self.attachAsset(newAsset, { anchorX: 0.5, anchorY: 0.5 }); } } } }; self.takeDamage = function (amount) { self.health -= amount; LK.effects.flashObject(self, 0xff0000, 200); if (self.health <= 0 && self.parent) { var numCoins = Math.ceil(self.value / 5); if (numCoins < 1) { numCoins = 1; } for (var i = 0; i < numCoins; i++) { var coinValue = Math.floor(self.value / numCoins); if (i === 0 && self.value % numCoins !== 0) { coinValue += self.value % numCoins; } var coin = new DogeCoin(self.x, self.y, coinValue); if (coinLayer) { coinLayer.addChild(coin); } else { game.addChild(coin); } } LK.setScore(LK.getScore() + self.value); scoreText.setText("Score: " + LK.getScore()); LK.getSound('enemyDeath').play(); self.destroy(); } }; var originalDestroy = self.destroy; self.destroy = function () { if (typeof originalDestroy === 'function') { originalDestroy.call(self); } else if (self.parent) { self.parent.removeChild(self); } }; return self; }); // ---- END: REPLACE SpamEmailUnit CLASS WITH THIS ---- // --- Base Tower Functionality (Conceptual - can be mixed into specific towers) --- // This isn't a formal class, but concepts to apply // Modify StaplerTower // --- MODIFY StaplerTower --- var StaplerTower = Container.expand(function () { var self = Container.call(this); // --- MODIFIED: No graphic initialization in constructor --- self.lastFired = 0; // init method removed - functionality moved to constructor and initializeTowerFromData self.update = function () { self.lastFired += gameSpeedMultiplier; if (self.lastFired >= self.fireRate) { var closestEnemy = null; var minDistanceSq = self.range * self.range; var buildSpot = self.parent; // Tower is child of BuildSpot if (!buildSpot || !buildSpot.parent) { return; } var levelContainer = buildSpot.parent; // BuildSpot is child of Level var towerGraphicGlobalCenter = self.graphic.toGlobal({ x: 0, y: 0 }); var towerCenterInGameSpace = game.toLocal(towerGraphicGlobalCenter); for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - towerCenterInGameSpace.x; var dy = enemy.y - towerCenterInGameSpace.y; var distanceSq = dx * dx + dy * dy; // Squared distance check if (distanceSq < minDistanceSq) { minDistanceSq = distanceSq; closestEnemy = enemy; } } if (closestEnemy) { self.shoot(closestEnemy); self.lastFired = 0; } } }; self.shoot = function (target) { var bullet = new TowerBullet(); var buildSpot = self.parent; if (!buildSpot || !buildSpot.parent) { return; } var levelContainer = buildSpot.parent; var towerGraphicGlobalCenter = self.graphic.toGlobal({ x: 0, y: 0 }); var bulletSpawnPosInGameSpace = game.toLocal(towerGraphicGlobalCenter); bullet.x = bulletSpawnPosInGameSpace.x; bullet.y = bulletSpawnPosInGameSpace.y; bullet.target = target; bullet.damage = self.damage; game.addChild(bullet); bullets.push(bullet); LK.getSound('shoot').play(); }; // Initialize tower with level 0 stats and graphics initializeTowerFromData(self, 'stapler', 0); // Set initial stats & correct L0 asset return self; }); var ThisIsFinePit = Container.expand(function () { var self = Container.call(this); // Initialize buffer tower properties self.nearbyTowers = []; self.currentBuffStacks = 0; self.lastSoundPlayed = null; // For managing dynamic audio self.soundCooldown = 0; // To prevent voice lines spamming // Initialize the tower with level 0 stats and graphics initializeTowerFromData(self, 'thisIsFine', 0); self.update = function () { // A. Count Enemies in Range var buildSpot = self.parent; if (!buildSpot || !buildSpot.parent) { return; } var pitPosInGameSpace; var towerGraphicGlobalCenter = self.graphic.toGlobal({ x: 0, y: 0 }); pitPosInGameSpace = game.toLocal(towerGraphicGlobalCenter); // Count how many enemies are within range var enemyCountInRange = 0; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy && enemy.parent) { var dx = enemy.x - pitPosInGameSpace.x; var dy = enemy.y - pitPosInGameSpace.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < self.range * self.range) { enemyCountInRange++; } } } // Cap at max buff stacks var activeStacks = Math.min(enemyCountInRange, self.maxBuffStacks); // B. Clear Previous Buffs & Find Towers to Buff if (self.currentBuffStacks > 0) { // Revert stats for previously buffed towers for (var i = 0; i < self.nearbyTowers.length; i++) { var towerInRange = self.nearbyTowers[i]; if (towerInRange && towerInRange.parent && towerInRange.hasOwnProperty('originalFireRate') && towerInRange.hasOwnProperty('originalDamage')) { towerInRange.fireRate = towerInRange.originalFireRate; towerInRange.damage = towerInRange.originalDamage; } } // Clear the array of nearby towers self.nearbyTowers = []; } // Find all towers within range if (level && level.buildSpots) { for (var i = 0; i < level.buildSpots.length; i++) { var spot = level.buildSpots[i]; if (spot.hasTower && spot.tower && spot.tower !== self) { // Get tower position in game space var towerPos = spot.tower.graphic.toGlobal({ x: 0, y: 0 }); var otherTowerPosInGameSpace = game.toLocal(towerPos); // Check if tower is in range var dx = otherTowerPosInGameSpace.x - pitPosInGameSpace.x; var dy = otherTowerPosInGameSpace.y - pitPosInGameSpace.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < self.range * self.range) { self.nearbyTowers.push(spot.tower); } } } } // C. Apply New Buffs self.currentBuffStacks = activeStacks; if (activeStacks > 0) { // Calculate buff multipliers var speedMultiplier = 1.0 - self.attackSpeedBuffPerStack * activeStacks; var damageMultiplier = 1.0 + self.damageBuffPerStack * activeStacks; // Apply buffs to nearby towers for (var i = 0; i < self.nearbyTowers.length; i++) { var towerInRange = self.nearbyTowers[i]; // Store original values if not already stored if (!towerInRange.hasOwnProperty('originalFireRate')) { towerInRange.originalFireRate = towerInRange.fireRate; } if (!towerInRange.hasOwnProperty('originalDamage')) { towerInRange.originalDamage = towerInRange.damage; } // Apply buffs towerInRange.fireRate = Math.max(5, Math.floor(towerInRange.originalFireRate * speedMultiplier)); towerInRange.damage = towerInRange.originalDamage * damageMultiplier; } } // D. Dynamic Audio Logic if (self.soundCooldown > 0) { self.soundCooldown -= gameSpeedMultiplier; } if (activeStacks === 0 && self.lastSoundPlayed !== 'gentle') { // Stop other fire sounds if playing // Play sfxThisIsFineFireGentle (looping) self.lastSoundPlayed = 'gentle'; } else if (activeStacks > 0 && activeStacks <= 2 && self.lastSoundPlayed !== 'medium') { // Stop other fire sounds // Play sfxThisIsFineFireMedium (looping) self.lastSoundPlayed = 'medium'; if (self.soundCooldown <= 0) { // Play sfxThisIsFineVoice1 self.soundCooldown = 300; // 5s } } else if (activeStacks > 2 && self.lastSoundPlayed !== 'roar') { // Stop other fire sounds // Play sfxThisIsFineFireRoar (looping) self.lastSoundPlayed = 'roar'; if (self.soundCooldown <= 0) { // Play sfxThisIsFineVoice2 self.soundCooldown = 300; } } }; // Override destroy to clean up buffs when tower is destroyed var originalDestroy = self.destroy; self.destroy = function () { // Revert all buffs before destroying for (var i = 0; i < self.nearbyTowers.length; i++) { var towerInRange = self.nearbyTowers[i]; if (towerInRange && towerInRange.parent && towerInRange.hasOwnProperty('originalFireRate') && towerInRange.hasOwnProperty('originalDamage')) { towerInRange.fireRate = towerInRange.originalFireRate; towerInRange.damage = towerInRange.originalDamage; } } // Stop any looping fire sounds // Call original destroy if (originalDestroy) { originalDestroy.call(self); } else if (self.parent) { self.parent.removeChild(self); } }; return self; }); // TowerBullet class remains the same var TowerBullet = Container.expand(function () { var self = Container.call(this); var graphic = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 10; self.damage = 1; self.target = null; self.update = function () { // Check if target exists and is still in the game if (!self.target || !self.target.parent) { self.destroy(); return; } var dx = self.target.x - self.x; var dy = self.target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Use speed as collision threshold if (distance < self.speed) { self.target.takeDamage(self.damage); self.destroy(); return; } // Normalize and multiply by speed dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x558855 // Darker green background }); /**** * Game Code ****/ // ---- START: ADD THESE LINES FOR LEVEL MANAGEMENT ---- // var towerCenterInGameSpace = game.toLocal(towerGraphicGlobalCenter); // --- Base Tower Functionality (Conceptual - can be mixed into specific towers) --- // Add upgrade method to both tower types via initializeTowerFromData // New Tower - Restructuring Specialist // (Optional) Effect for "restructuring" // (Optional) Sounds // ---- START: ADD THESE Global Variables for Intermission ---- var isIntermissionActive = false; var intermissionTimer = 0; // INTERMISSION_DURATION in frames: 20 seconds * 60fps = 1200 frames var INTERMISSION_DURATION = 1200; var intermissionText = null; // To hold the UI Text2 object for countdown // ---- END: ADD THESE Global Variables for Intermission ---- var MAP_WIDTH = 3000; var MAP_HEIGHT = 4500; var LEVEL_DATA = {}; var currentLevelNumber = 1; var pathPoints = []; // Ensure pathPoints is global and initialized as an empty array // coinLayer and enemyLayer will also be global, but initialized in initializeGame var coinLayer; var enemyLayer; // ---- END: ADD THESE LINES FOR LEVEL MANAGEMENT ---- LEVEL_DATA[1] = { pathPoints: [{ x: MAP_WIDTH * 0.2, y: MAP_HEIGHT * 0.05 }, { x: MAP_WIDTH * 0.2, y: MAP_HEIGHT * 0.2 }, { x: MAP_WIDTH * 0.5, y: MAP_HEIGHT * 0.2 }, { x: MAP_WIDTH * 0.5, y: MAP_HEIGHT * 0.35 }, { x: MAP_WIDTH * 0.7, y: MAP_HEIGHT * 0.35 }, { x: MAP_WIDTH * 0.7, y: MAP_HEIGHT * 0.5 }, { x: MAP_WIDTH * 0.4, y: MAP_HEIGHT * 0.5 }, { x: MAP_WIDTH * 0.3, y: MAP_HEIGHT * 0.55 }, { x: MAP_WIDTH * 0.3, y: MAP_HEIGHT * 0.7 }, { x: MAP_WIDTH * 0.8, y: MAP_HEIGHT * 0.7 }, { x: MAP_WIDTH * 0.6, y: MAP_HEIGHT * 0.85 }, { x: MAP_WIDTH * 0.85, y: MAP_HEIGHT * 0.85 }, { x: MAP_WIDTH * 0.9, y: MAP_HEIGHT * 0.9 }], // Build Spots (Adjust Y values for map height) buildSpots: [{ x: MAP_WIDTH * 0.15, y: MAP_HEIGHT * 0.15 }, { x: MAP_WIDTH * 0.25, y: MAP_HEIGHT * 0.15 }, { x: MAP_WIDTH * 0.22, y: MAP_HEIGHT * 0.25 }, { x: MAP_WIDTH * 0.39, y: MAP_HEIGHT * 0.25 }, { x: MAP_WIDTH * 0.39, y: MAP_HEIGHT * 0.15 }, { x: MAP_WIDTH * 0.75, y: MAP_HEIGHT * 0.42 }, { x: MAP_WIDTH * 0.575, y: MAP_HEIGHT * 0.30 }, { x: MAP_WIDTH * 0.645, y: MAP_HEIGHT * 0.39 }, { x: MAP_WIDTH * 0.645, y: MAP_HEIGHT * 0.456 }, { x: MAP_WIDTH * 0.25, y: MAP_HEIGHT * 0.62 }, { x: MAP_WIDTH * 0.78, y: MAP_HEIGHT * 0.648 }, { x: MAP_WIDTH * 0.37, y: MAP_HEIGHT * 0.58 }, { x: MAP_WIDTH * 0.37, y: MAP_HEIGHT * 0.65 }, { x: MAP_WIDTH * 0.5, y: MAP_HEIGHT * 0.735 }, { x: MAP_WIDTH * 0.74, y: MAP_HEIGHT * 0.81 }, { x: MAP_WIDTH * 0.65, y: MAP_HEIGHT * 0.75 }, { x: MAP_WIDTH * 0.79, y: MAP_HEIGHT * 0.89 }], maxWaves: 10 }; LEVEL_DATA[2] = { // Path A starts top-left area pathPointsA: [{ x: MAP_WIDTH * 0.2, y: MAP_HEIGHT * 0.05 }, // SA_P1 { x: MAP_WIDTH * 0.2, y: MAP_HEIGHT * 0.3 }, // SA_P2 { x: MAP_WIDTH * 0.35, y: MAP_HEIGHT * 0.3 }, // SA_P3 { x: MAP_WIDTH * 0.5, y: MAP_HEIGHT * 0.45 } // CM_P1 (Common Merge Point) ], // Path B starts top-right area pathPointsB: [{ x: MAP_WIDTH * 0.8, y: MAP_HEIGHT * 0.05 }, // SB_P1 { x: MAP_WIDTH * 0.8, y: MAP_HEIGHT * 0.3 }, // SB_P2 { x: MAP_WIDTH * 0.65, y: MAP_HEIGHT * 0.3 }, // SB_P3 { x: MAP_WIDTH * 0.5, y: MAP_HEIGHT * 0.45 } // CM_P1 (Common Merge Point) ], // Merged path starts from CM_P1 // Note: CM_P1 is the last point of PathA/PathB and first point of MergedPath. // The createPath logic will need to handle this (e.g., by appending MergedPath to one of them). // For simplicity now, let's define a full path for spawning logic, and createPath can draw segments. // We'll handle the actual drawing of merged paths in the next step. // For now, let spawnWave use pathPointsA or pathPointsB and then a common merged segment. // Define a 'mainPathForSpawning' that will be one of these, plus merged. // This structure is for game logic; drawing will be separate. // Let's simplify: spawnWave will choose pathA or pathB. // The 'goal' will be the end of a conceptual full merged path. // Merged path segment (starts from where A and B conceptually meet) mergedPathPoints: [{ x: MAP_WIDTH * 0.5, y: MAP_HEIGHT * 0.45 }, // CM_P1 (Start of merged section) { x: MAP_WIDTH * 0.5, y: MAP_HEIGHT * 0.65 }, // M_P2 { x: MAP_WIDTH * 0.25, y: MAP_HEIGHT * 0.75 }, // M_P3 { x: MAP_WIDTH * 0.75, y: MAP_HEIGHT * 0.85 }, // M_P4 { x: MAP_WIDTH * 0.75, y: MAP_HEIGHT * 0.95 } // M_P5 (Goal) ], buildSpots: [{ x: MAP_WIDTH * 0.15, y: MAP_HEIGHT * 0.15 }, { x: MAP_WIDTH * 0.3, y: MAP_HEIGHT * 0.25 }, { x: MAP_WIDTH * 0.4, y: MAP_HEIGHT * 0.35 }, { x: MAP_WIDTH * 0.85, y: MAP_HEIGHT * 0.15 }, { x: MAP_WIDTH * 0.7, y: MAP_HEIGHT * 0.25 }, { x: MAP_WIDTH * 0.6, y: MAP_HEIGHT * 0.35 }, { x: MAP_WIDTH * 0.45, y: MAP_HEIGHT * 0.50 }, { x: MAP_WIDTH * 0.55, y: MAP_HEIGHT * 0.50 }, { x: MAP_WIDTH * 0.4, y: MAP_HEIGHT * 0.60 }, { x: MAP_WIDTH * 0.2, y: MAP_HEIGHT * 0.70 }, { x: MAP_WIDTH * 0.5, y: MAP_HEIGHT * 0.78 }, { x: MAP_WIDTH * 0.8, y: MAP_HEIGHT * 0.90 }, { x: MAP_WIDTH * 0.27, y: MAP_HEIGHT * 0.15 }, { x: MAP_WIDTH * 0.73, y: MAP_HEIGHT * 0.15 }, { x: MAP_WIDTH * 0.5, y: MAP_HEIGHT * 0.35 }], goalPosition: { x: MAP_WIDTH * 0.75, y: MAP_HEIGHT * 0.95 }, // Explicit goal for Level 2 maxWaves: 15 }; function createRangeIndicator(centerX, centerY, radius, parentContainer) { return new RangeIndicator(centerX, centerY, radius, parentContainer); } function addUpgradeToTower(towerInstance) { towerInstance.upgrade = function () { // Check if next level exists if (!this.towerType || !TOWER_DATA[this.towerType]) { return false; } var nextLevel = this.currentLevel + 1; var towerTypeData = TOWER_DATA[this.towerType]; // Check if next level data exists if (!towerTypeData.levels[nextLevel]) { spawnFloatingText("MAX LEVEL!", this.parent.x, this.parent.y - 50, { fill: 0xFFD700 }); return false; } // Get upgrade cost var upgradeCost = towerTypeData.levels[nextLevel].cost; // Check if player can afford upgrade if (currency < upgradeCost) { spawnFloatingText("Need more $!", this.parent.x, this.parent.y - 50, { fill: 0xFF0000 }); return false; } // Pay for upgrade currency -= upgradeCost; currencyText.setText("$: " + currency); // Apply upgrade initializeTowerFromData(this, this.towerType, nextLevel); // Play upgrade sound LK.getSound('uiSelectTower').play(); // Show upgrade message spawnFloatingText("UPGRADED!", this.parent.x, this.parent.y - 50, { fill: 0x00FF00 }); // Hide upgrade menu if (this.parent && typeof this.parent.hideUpgradeMenu === 'function') { this.parent.hideUpgradeMenu(); } return true; }; } // Specific build sounds for each tower type if desired // --- New Sound Effects --- // Bureaucracy Blocker // Even more so // Slightly bigger/cooler // Stapler // --- Tower Level Assets (Placeholders) --- // You'll also need actual tower icons for the buttons eventually // For + symbols // --- UI Assets for Popup --- // Added sound for auto-attack // Game constants // <-- NEW Green Target Marker // --- NEW ASSETS --- // Placeholder path // Placeholder path // Sound for bureaucracy blocker or paperwork // Sound for red tape worm // --- TOWER DEFINITIONS --- // Tower data is defined below // Function to initialize or upgrade tower with data from TOWER_DATA function initializeTowerFromData(towerInstance, towerTypeKey, levelIndex) { if (!towerInstance || !TOWER_DATA[towerTypeKey] || !TOWER_DATA[towerTypeKey].levels || !TOWER_DATA[towerTypeKey].levels[levelIndex]) { console.error("Invalid tower, type, or level data for initialization"); return; } var levelData = TOWER_DATA[towerTypeKey].levels[levelIndex]; // Store tower type and level towerInstance.towerType = towerTypeKey; towerInstance.currentLevel = levelIndex; // Assign all properties from levelData to the tower for (var key in levelData) { if (key !== 'asset' && key !== 'cost' && key !== 'description') { towerInstance[key] = levelData[key]; } } // Graphic handling: if (towerInstance.graphic && towerInstance.graphic.parent) { // If graphic exists and is attached, update it towerInstance.graphic.parent.removeChild(towerInstance.graphic); towerInstance.graphic = towerInstance.attachAsset(levelData.asset, { anchorX: 0.5, anchorY: 0.5 }); } else { // If graphic doesn't exist or is detached, create a new one if (towerInstance.graphic) { towerInstance.graphic.destroy(); // Clean up old if it existed but was detached } towerInstance.graphic = towerInstance.attachAsset(levelData.asset, { anchorX: 0.5, anchorY: 0.5 }); } // Tower-specific re-initialization logic if (towerTypeKey === 'stapler' && towerInstance.hasOwnProperty('lastFired')) { towerInstance.lastFired = 0; // Reset fire cooldown on init/upgrade } if (towerTypeKey === 'blocker' && typeof towerInstance.clearAllSlows === 'function') { towerInstance.clearAllSlows(); // Ensure slows are reset based on new stats } // Add upgrade method if not already present if (!towerInstance.upgrade) { addUpgradeToTower(towerInstance); } } // --- GLOBAL DEFINITION FOR ICON OFFSETS --- // buildSpot graphic width is 150, icon width is ~80-100. Adjust offsets as needed. var ICON_SELECT_OFFSETS = [{ x: -170, y: -110, towerKey: 'stapler' }, { x: 0, y: -200, towerKey: 'blocker' }, { x: 170, y: -110, towerKey: 'laserCat' }, { x: -170, y: 110, towerKey: 'rickroller' }, { x: 0, y: 200, towerKey: 'thisIsFine' }, { x: 170, y: 110, towerKey: 'restruct' }]; var TOWER_DATA = { 'stapler': { name: 'Stapler Turret', iconAsset: 'towerStaplerLvl1', // Use Lvl1 icon for selection button initially buildSfx: 'buildStapler', levels: [{ asset: 'towerStaplerLvl1', cost: 50, damage: 1, range: 400, fireRate: 60, description: "Basic Stapler" }, { asset: 'towerStaplerLvl2', cost: 150, damage: 3, range: 420, fireRate: 55, description: "Improved Firepower" }, { asset: 'towerStaplerLvl3', cost: 300, damage: 4, range: 450, fireRate: 50, description: "Max Staples!" }] }, 'blocker': { name: 'Bureaucracy Blocker', iconAsset: 'towerBlockerLvl1', buildSfx: 'buildBlocker', levels: [{ asset: 'towerBlockerLvl1', cost: 75, slowFactor: 0.5, range: 320, description: "Slows nearby red tape." }, { asset: 'towerBlockerLvl2', cost: 300, slowFactor: 0.4, range: 350, description: "Wider, stronger slow." }, // 0.4 means 60% speed reduction { asset: 'towerBlockerLvl3', cost: 600, slowFactor: 0.3, range: 400, description: "Bureaucratic Gridlock!" }] }, 'laserCat': { name: 'Laser Cat Perch', iconAsset: 'towerLaserCatLvl1', buildSfx: 'buildLaserCat', levels: [{ // Level 1 asset: 'towerLaserCatLvl1', cost: 150, range: 500, initialBeamDamage: 1, // Damage when beam first hits dotDamagePerSecond: 1, // Base DOT dotRampUpFactor: 1.2, // Multiplier for DOT each second it stays on same target (e.g., 2, then 2*1.2, then 2*1.2*1.2) maxBeamDuration: 3000, // Milliseconds (3 seconds) beam can stay on one target cooldownBetweenBeams: 4000, // Milliseconds (4 seconds) after a beam finishes numberOfBeams: 1, // Targets 1 enemy description: "Pew pew! Precision feline firepower. Damage ramps up." }, { // Level 2 asset: 'towerLaserCatLvl1', // Placeholder - use 'towerLaserCatLvl2' when ready cost: 600, range: 550, initialBeamDamage: 5, dotDamagePerSecond: 2, dotRampUpFactor: 1.25, maxBeamDuration: 3500, cooldownBetweenBeams: 3500, numberOfBeams: 2, // Targets 2 enemies description: "Dual-core processing! More lasers, more ouch." }, { // Level 3 asset: 'towerLaserCatLvl1', // Placeholder - use 'towerLaserCatLvl3' when ready cost: 1200, range: 600, initialBeamDamage: 10, dotDamagePerSecond: 5, dotRampUpFactor: 1.3, maxBeamDuration: 4000, cooldownBetweenBeams: 3000, numberOfBeams: 3, // Targets 3 enemies description: "Maximum laser focus! It's a light show of doom." }] }, 'rickroller': { name: 'Rickroller Trap', iconAsset: 'towerRickrollerLvl1', buildSfx: 'sfxRickrollerPlace', // Changed to match your SFX list levels: [{ asset: 'towerRickrollerLvl1', cost: 175, damagePerTick: 1, dotTickRate: 500, // Milliseconds effectRadius: 200, // Radius of stun/damage from impact stunDuration: 2000, // Milliseconds popupDuration: 3000, // Milliseconds popupAsset: 'rickAstleyPopup', soundEffects: ['sfxRickrollTrigger1', 'sfxRickrollTrigger2', 'sfxRickrollTrigger3'], // Array of sound variants cooldown: 10000, // Milliseconds from when the tower fires range: 400, // Range of the TOWER to decide where to shoot the trap projectileSpeed: 7, // Speed of flying Rick Astley projectile description: "Never gonna let them pass! Stuns and confuses." }] }, 'thisIsFine': { name: "'This Is Fine' Fire Pit", iconAsset: 'towerThisIsFineLvl1', buildSfx: 'buildThisIsFine', // Define this sound levels: [{ asset: 'towerThisIsFineLvl1', cost: 125, range: 650, // Aura buff range maxBuffStacks: 5, // How many nearby enemies contribute to max buff attackSpeedBuffPerStack: 0.05, // e.g., 5% faster per stack (0.95 multiplier) damageBuffPerStack: 0.05, // e.g., 5% more damage per stack (1.05 multiplier) description: "Everything's fine. Totally fine. Buffs nearby towers." }] }, 'restruct': { name: 'Restructuring Specialist', iconAsset: 'towerRestructLvl1', buildSfx: 'buildRestruct', levels: [{ asset: 'towerRestructLvl1', cost: 500, damage: 100, instantKillThreshold: 25, range: 450, chargeUpTime: 360, targetPriority: 'strongest', effectAsset: 'restructureEffect', description: "Optimizing threats... permanently." }, { asset: 'towerRestructLvl1', cost: 1000, damage: 150, instantKillThreshold: 35, range: 600, chargeUpTime: 300, targetPriority: 'strongest', effectAsset: 'restructureEffect', description: "Synergistic realignment for enhanced efficiency." }, { asset: 'towerRestructLvl1', cost: 2000, damage: 200, instantKillThreshold: 50, range: 700, chargeUpTime: 250, targetPriority: 'strongest', effectAsset: 'restructureEffect', goldenParachuteDamage: 25, goldenParachuteRadius: 100, description: "Downsizing comes with... severance packages." }] } // Add more tower types here later }; var gameSpeedMultiplier = 1.0; // Default game speed var TOWER_COST = 50; var WAVE_DELAY = 300; // frames between waves var MAX_WAVES = 10; // Access screen dimensions directly from LK var SCREEN_HEIGHT = 2732; // Standard iPad Pro height (portrait mode) var SCREEN_WIDTH = 2048; // Standard iPad Pro width (portrait mode) var PAN_THRESHOLD = SCREEN_HEIGHT * 0.25; // Start panning when Doge is in top/bottom 25% /**** NEW ****/ var MAP_WIDTH = 3000; // Example: roughly double current screen width var MAP_HEIGHT = 4500; // Let's increase this too if we're expanding // Game variables var coinLayer; var enemyLayer; var currency = 100; var playerLives = 5; var currentWave = 0; var waveTimer = 0; var isWaveActive = false; var enemies = []; var bullets = []; var pathPoints = []; var currentActiveBuildSpot = null; var level; var doge; var goal; // --- HELPER FUNCTION FOR FLOATING TEXT --- function isChildOf(possibleParent, target) { if (!target || !target.parent) { return false; } if (target.parent === possibleParent) { return true; } return isChildOf(possibleParent, target.parent); } // Function to update the visual state of speed buttons function updateSpeedButtonVisuals() { if (game.normalSpeedButton && game.fastSpeedButton) { if (gameSpeedMultiplier === 1.0) { game.normalSpeedButton.setText("Regular Work Day", { fill: 0xFFFFFF // Active }); game.fastSpeedButton.setText("Crunch Time!", { fill: 0xDDDDDD // Inactive }); } else { game.normalSpeedButton.setText("Regular Work Day", { fill: 0xDDDDDD // Inactive }); game.fastSpeedButton.setText("Crunch Time!", { fill: 0xFFD700 // Gold/active }); } } } function spawnFloatingText(text, x, y, options) { var defaultOptions = { size: 30, fill: 0xFFFFFF, // White text stroke: 0x000000, strokeThickness: 2, duration: 60, // frames (1 second at 60fps) velocityY: -1.5, // Pixels per frame upwards alphaFadeSpeed: 0.015 }; var settings = Object.assign({}, defaultOptions, options); // Merge user options var floatingText = new Text2(text, { size: settings.size, fill: settings.fill, stroke: settings.stroke, strokeThickness: settings.strokeThickness, anchorX: 0.5, // Center the text anchorY: 0.5 }); floatingText.x = x; floatingText.y = y; floatingText.alpha = 1.0; game.addChild(floatingText); // Add to main game container to scroll with world var framesLived = 0; floatingText.update = function () { floatingText.y += settings.velocityY * gameSpeedMultiplier; floatingText.alpha -= settings.alphaFadeSpeed * gameSpeedMultiplier; framesLived += gameSpeedMultiplier; if (framesLived >= settings.duration || floatingText.alpha <= 0) { floatingText.destroy(); } }; // Add this to a list of updatable text objects if your engine doesn't auto-update children with .update // For LK Engine, if it's a child of `game` and has an `update` method, it should be called. // If not, you'll need a global array like `activeFloatingTexts` and iterate it in `game.update`. // Let's assume LK.Game handles child updates for now. } var scoreText = new Text2("Score: 0", { size: 50, fill: 0xFFFFFF }); // White text scoreText.anchor.set(0.5, 0); LK.gui.top.addChild(scoreText); var waveText = new Text2("Wave: 0/" + MAX_WAVES, { size: 50, fill: 0xFFFFFF }); waveText.anchor.set(1, 0); // Anchor top-right waveText.x = -100; // Position from right edge LK.gui.topRight.addChild(waveText); var currencyText = new Text2("$: " + currency, { size: 50, fill: 0xFFFFFF }); currencyText.anchor.set(0.5, 0); currencyText.y = 60; LK.gui.top.addChild(currencyText); var livesText = new Text2("Lives: " + playerLives, { size: 50, fill: 0xFFFFFF }); livesText.anchor.set(1, 0); // Anchor top-right livesText.x = -100; livesText.y = 60; LK.gui.topRight.addChild(livesText); // Create game speed control buttons var normalSpeedButton = new Text2("Regular Work Day", { size: 60, // Or your preferred size fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 2 }); normalSpeedButton.anchor.set(1, 0); // Anchor to its top-right normalSpeedButton.x = -100; // Position from the right edge of LK.gui.topRight normalSpeedButton.y = 200; // Position from the top edge of LK.gui.topRight (adjust as needed) normalSpeedButton.interactive = true; normalSpeedButton.down = function () { gameSpeedMultiplier = 1.0; updateSpeedButtonVisuals(); LK.getSound('uiSelectTower').play(); }; LK.gui.topRight.addChild(normalSpeedButton); var fastSpeedButton = new Text2("Crunch Time!", { size: 60, // Or your preferred size fill: 0xDDDDDD, stroke: 0x000000, strokeThickness: 2 }); fastSpeedButton.anchor.set(1, 0); // Anchor to its top-right fastSpeedButton.x = -100; // Position from the right edge of LK.gui.topRight fastSpeedButton.y = normalSpeedButton.y + normalSpeedButton.height + 10; // Position below the normal speed button fastSpeedButton.interactive = true; fastSpeedButton.down = function () { gameSpeedMultiplier = 2.0; updateSpeedButtonVisuals(); LK.getSound('uiSelectTower').play(); }; LK.gui.topRight.addChild(fastSpeedButton); // Intermission Text (initially hidden) if (intermissionText && intermissionText.parent) { // If it exists from a previous level, destroy it intermissionText.destroy(); intermissionText = null; } intermissionText = new Text2("", { // Create new size: 70, // Made size larger for better visibility fill: 0xFFD700, // Gold color stroke: 0x000000, strokeThickness: 4, anchorX: 0.5 // Center horizontally }); intermissionText.x = 0; // Centered in LK.gui.top (since anchorX is 0.5) intermissionText.y = SCREEN_HEIGHT / 2 - 100; // Position towards vertical center of screen intermissionText.visible = false; // Start hidden LK.gui.top.addChild(intermissionText); // Add to a main GUI layer // Removed old info text, replaced by Bark button // var infoText = new Text2(...) // Bark Button /**** NEW ****/ var barkButton = new Text2("BARK!", { size: 100, fill: 0xffcc00, // Doge color stroke: 0x000000, strokeThickness: 4 }); barkButton.anchor.set(0.5, 1); // Anchor bottom-center barkButton.y = -50; // Position from bottom edge barkButton.interactive = true; // Make it clickable barkButton.down = function () { if (doge) { var success = doge.manualBark(); if (success) { // Optional: visual feedback on button press barkButton.scale.set(1.1); LK.setTimeout(function () { barkButton.scale.set(1.0); }, 100); } } }; LK.gui.bottom.addChild(barkButton); // ---- START: REPLACE OLD initializeGame WITH THIS NEW VERSION ---- function initializeGame(levelNum) { currentLevelNumber = levelNum; var levelConfig = LEVEL_DATA[levelNum]; if (!levelConfig) { // LK.log is not available, use console.error for feedback if possible, or alert. // For now, we'll just try to fallback or stop. console.error("Error: Level " + levelNum + " data not found!"); if (levelNum !== 1 && LEVEL_DATA[1]) { currentLevelNumber = 1; levelConfig = LEVEL_DATA[1]; if (!levelConfig) { console.error("CRITICAL Error: Level 1 data also not found!"); return; } } else if (!LEVEL_DATA[1]) { console.error("CRITICAL Error: No Level data found at all!"); return; } } // --- Clear Existing Game Objects from Previous Level (if any) --- if (level && typeof level.destroy === 'function' && level.parent) { // Destroy all towers on buildspots if (level.buildSpots && level.buildSpots.length > 0) { for (var i_bs = 0; i_bs < level.buildSpots.length; i_bs++) { var spot_to_clear = level.buildSpots[i_bs]; if (spot_to_clear.tower) { spot_to_clear.tower.destroy(); // Assuming towers have a destroy method spot_to_clear.tower = null; spot_to_clear.hasTower = false; if (spot_to_clear.graphic) { spot_to_clear.graphic.alpha = 0.5; } } // Clear any UI attached to the buildspot if (spot_to_clear.rangeIndicator) { spot_to_clear.hideRangeIndicator(); } // Assuming a method if it exists else if (spot_to_clear.currentRangeIndicator) { spot_to_clear.hideCurrentRangeIndicator(); } // Or this else if (spot_to_clear.rangeIndicator && typeof spot_to_clear.rangeIndicator.destroy === 'function') { spot_to_clear.rangeIndicator.destroy(); } if (spot_to_clear.selectionIconContainer) { spot_to_clear.hideSelectionIcons(); } if (spot_to_clear.actionIconContainer) { spot_to_clear.hideTowerActionIcons(); } if (spot_to_clear.upgradeMenuContainer) { spot_to_clear.hideUpgradeMenu(); } } } level.destroy(); // Assuming GameLevel has a destroy method that cleans its children level = null; } if (doge && typeof doge.destroy === 'function' && doge.parent) { doge.destroy(); } doge = null; if (goal && typeof goal.destroy === 'function' && goal.parent) { goal.destroy(); } goal = null; // Destroy all active enemies for (var i_e = enemies.length - 1; i_e >= 0; i_e--) { if (enemies[i_e] && typeof enemies[i_e].destroy === 'function' && enemies[i_e].parent) { enemies[i_e].destroy(); } } enemies = []; // Destroy all active bullets for (var i_b = bullets.length - 1; i_b >= 0; i_b--) { if (bullets[i_b] && typeof bullets[i_b].destroy === 'function' && bullets[i_b].parent) { bullets[i_b].destroy(); } } bullets = []; // Clear coins from coinLayer if (coinLayer && coinLayer.children) { while (coinLayer.children.length > 0) { if (typeof coinLayer.children[0].destroy === 'function') { coinLayer.children[0].destroy(); } else if (coinLayer.children[0].parent) { // Fallback removal coinLayer.removeChild(coinLayer.children[0]); } else { // Break if child can't be removed (should not happen) break; } } } currentActiveBuildSpot = null; // --- Standard Game Setup --- game.x = 0; game.y = 0; game.scale.set(1); // --- Correct Layering Order (remove if existing, then add in order) --- if (level && level.parent === game) { game.removeChild(level); } // Should have been destroyed already if (coinLayer && coinLayer.parent === game) { game.removeChild(coinLayer); } if (enemyLayer && enemyLayer.parent === game) { game.removeChild(enemyLayer); } level = new GameLevel(); game.addChild(level); // 1. Level (path, build spot graphics) coinLayer = new Container(); game.addChild(coinLayer); // 2. Coins (on top of level) game.coinLayer = coinLayer; // Make accessible via game.coinLayer if needed elsewhere enemyLayer = new Container(); game.addChild(enemyLayer); // 3. Enemies (on top of coins) // ---- START: REPLACE PATH/BUILDSPOT CREATION LOGIC IN initializeGame WITH THIS ---- // --- Load Level Specific Data --- pathPoints.length = 0; // Clear the global pathPoints array first if (currentLevelNumber === 2 && levelConfig.pathPointsA && levelConfig.pathPointsB && levelConfig.mergedPathPoints) { // For Level 2: Draw all three path segments level.createPath(levelConfig.pathPointsA); level.createPath(levelConfig.pathPointsB); level.createPath(levelConfig.mergedPathPoints); // For enemy pathing on Level 2, we need to decide which path they primarily follow // For now, let global pathPoints be Path A + Merged Path. // Enemies spawned on Path B will need special handling later or their own path data. levelConfig.pathPointsA.forEach(function (p) { pathPoints.push(p); }); // Append mergedPathPoints, skipping the first point if it's identical to the last of pathPointsA if (levelConfig.mergedPathPoints.length > 0) { var lastOfA = pathPoints[pathPoints.length - 1]; var firstOfMerged = levelConfig.mergedPathPoints[0]; var startIndexForMerged = lastOfA.x === firstOfMerged.x && lastOfA.y === firstOfMerged.y ? 1 : 0; for (var m_idx = startIndexForMerged; m_idx < levelConfig.mergedPathPoints.length; m_idx++) { pathPoints.push(levelConfig.mergedPathPoints[m_idx]); } } } else if (levelConfig.pathPoints) { // For Level 1 or other single-path levels levelConfig.pathPoints.forEach(function (p) { pathPoints.push(p); }); level.createPath(pathPoints); } else { console.error("No pathPoints defined for level " + currentLevelNumber); } level.createBuildSpots(levelConfig.buildSpots); // This uses the buildSpots from LEVEL_DATA goal = new Goal(); // Re-create goal if (pathPoints.length > 0) { // Use the (potentially modified) global pathPoints for goal var goalPosData = pathPoints[pathPoints.length - 1]; // Check if levelConfig has a specific goal position that overrides the last path point if (levelConfig.goalPosition) { goalPosData = levelConfig.goalPosition; } goal.x = goalPosData.x; goal.y = goalPosData.y; // Add goal as child of 'level' so it's part of the level's display objects // and gets cleaned up when 'level' is destroyed. // It should be added AFTER paths/buildspots so it's visually on top if needed, // or to 'game' if it needs to be above everything in 'level'. // Let's keep it on game for now, to be above path tiles. game.addChild(goal); } // ---- END: REPLACE PATH/BUILDSPOT CREATION LOGIC ---- goal = new Goal(); if (pathPoints.length > 0) { var goalPosData = pathPoints[pathPoints.length - 1]; if (levelConfig.goalPosition) { goalPosData = levelConfig.goalPosition; } goal.x = goalPosData.x; goal.y = goalPosData.y; level.addChild(goal); // Add goal as child of level for better organization } doge = new DogeHero(); doge.x = SCREEN_WIDTH / 2; doge.y = SCREEN_HEIGHT * 0.8; doge.targetX = doge.x; doge.targetY = doge.y; game.addChild(doge); // Doge on top of game layers // --- Reset Game State Variables --- currency = levelConfig.startingCurrency !== undefined ? levelConfig.startingCurrency : 100; playerLives = levelConfig.startingLives !== undefined ? levelConfig.startingLives : 5; currentWave = 0; waveTimer = 0; isWaveActive = false; isIntermissionActive = false; intermissionTimer = 0; // Update UI currencyText.setText("$: " + currency); livesText.setText("Lives: " + playerLives); var maxWavesForLevel = levelConfig && levelConfig.maxWaves !== undefined ? levelConfig.maxWaves : MAX_WAVES; waveText.setText("Wave: " + currentWave + "/" + maxWavesForLevel); scoreText.setText("Score: " + LK.getScore()); barkButton.setText("BARK!"); if (game.normalSpeedButton && game.fastSpeedButton) { updateSpeedButtonVisuals(); } // Consider music here // LK.playMusic('bgmusic'); // If music should restart per level } // ---- END: REPLACE OLD initializeGame WITH THIS NEW VERSION ---- currentActiveBuildSpot = null; function spawnWave() { currentWave++; waveText.setText("Wave: " + currentWave + "/" + MAX_WAVES); var baseEnemyCountPerWave = 8 + currentWave * 3; var enemiesToSpawnThisWave = []; // --- Define enemy composition for this wave --- if (currentLevelNumber === 1) { // LEVEL 1 WAVES if (currentWave <= 2) { // Waves 1-2: Paper & Interns for (var i = 0; i < baseEnemyCountPerWave; i++) { if (Math.random() < 0.6) { enemiesToSpawnThisWave.push(Enemy); } else { enemiesToSpawnThisWave.push(InternOnCoffeeRun); } } } else if (currentWave >= 3 && currentWave <= 9) { // Waves 3-9: Paper, Interns, Spam Emails var actualCountForThisWave = baseEnemyCountPerWave; for (var i = 0; i < actualCountForThisWave; i++) { var r = Math.random(); if (r < 0.4) { enemiesToSpawnThisWave.push(Enemy); } else if (r < 0.7) { enemiesToSpawnThisWave.push(InternOnCoffeeRun); } else { enemiesToSpawnThisWave.push(SpamEmailUnit); } } } else if (currentWave === 10) { // Wave 10: Mix + 3 Auditors enemiesToSpawnThisWave.push(AuditorBot); enemiesToSpawnThisWave.push(AuditorBot); enemiesToSpawnThisWave.push(AuditorBot); var remainingCount = baseEnemyCountPerWave - 3; if (remainingCount < 0) { remainingCount = 0; } for (var i = 0; i < remainingCount; i++) { var r = Math.random(); if (r < 0.3) { enemiesToSpawnThisWave.push(Enemy); } else if (r < 0.6) { enemiesToSpawnThisWave.push(InternOnCoffeeRun); } else { enemiesToSpawnThisWave.push(SpamEmailUnit); } } // Shuffle the array so auditors don't always come first/last enemiesToSpawnThisWave.sort(function () { return Math.random() - 0.5; }); } else { // Fallback for waves > 10 (or unhandled cases) for (var i = 0; i < baseEnemyCountPerWave; i++) { enemiesToSpawnThisWave.push(Enemy); // Default to Paper } } } else if (currentLevelNumber === 2) { // LEVEL 2 WAVES if (currentWave === 1) { // L2, Wave 1 baseEnemyCountPerWave = 10 + currentWave * 2; // Slightly different scaling for L2 for (var i = 0; i < baseEnemyCountPerWave; i++) { if (Math.random() < 0.4) { enemiesToSpawnThisWave.push(Enemy); } else if (Math.random() < 0.7) { enemiesToSpawnThisWave.push(InternOnCoffeeRun); } else { enemiesToSpawnThisWave.push(SpamEmailUnit); } } } else if (currentWave === 2) { // L2, Wave 2 - Introduce RedTapeWorm baseEnemyCountPerWave = 12 + currentWave * 2; for (var i = 0; i < baseEnemyCountPerWave; i++) { var r = Math.random(); if (r < 0.3) { enemiesToSpawnThisWave.push(Enemy); } else if (r < 0.5) { enemiesToSpawnThisWave.push(InternOnCoffeeRun); } else if (r < 0.8) { enemiesToSpawnThisWave.push(SpamEmailUnit); } else { enemiesToSpawnThisWave.push(RedTapeWorm); } // Introduce RedTapeWorm } } else if (currentWave >= 3 && currentWave <= MAX_WAVES) { // L2, Waves 3-10 (example) baseEnemyCountPerWave = 10 + currentWave * 3; // Steeper scaling for (var i = 0; i < baseEnemyCountPerWave; i++) { var r = Math.random(); if (r < 0.2) { enemiesToSpawnThisWave.push(Enemy); } else if (r < 0.4) { enemiesToSpawnThisWave.push(InternOnCoffeeRun); } else if (r < 0.6) { enemiesToSpawnThisWave.push(SpamEmailUnit); } else if (r < 0.85) { enemiesToSpawnThisWave.push(RedTapeWorm); } else { enemiesToSpawnThisWave.push(AuditorBot); } // Auditors appear more often } } // Shuffle the wave for variety enemiesToSpawnThisWave.sort(function () { return Math.random() - 0.5; }); } else { // Fallback for currentLevelNumber other than 1 or 2 for (var i = 0; i < baseEnemyCountPerWave; i++) { enemiesToSpawnThisWave.push(Enemy); // Default to Paper } } var spawnInterval = 60; var spawnIndex = 0; function spawnIndividualEnemy() { if (spawnIndex >= enemiesToSpawnThisWave.length || !pathPoints || pathPoints.length === 0) { isWaveActive = true; return; } var enemyTypeToSpawn = enemiesToSpawnThisWave[spawnIndex]; // Determine the full path for this enemy based on level and spawn position var fullPathForThisEnemy; if (currentLevelNumber === 2 && LEVEL_DATA[2]) { if (spawnIndex % 2 === 0) { // Spawning on Path A fullPathForThisEnemy = LEVEL_DATA[2].pathPointsA.concat(LEVEL_DATA[2].mergedPathPoints.slice(1)); } else { // Spawning on Path B fullPathForThisEnemy = LEVEL_DATA[2].pathPointsB.concat(LEVEL_DATA[2].mergedPathPoints.slice(1)); } } else { // For Level 1 or if Level 2 data is incomplete, use the global pathPoints fullPathForThisEnemy = pathPoints; } // Create the enemy with its custom path data var enemy = new enemyTypeToSpawn(fullPathForThisEnemy[0].x, fullPathForThisEnemy[0].y, undefined, // Value parameter (unused) fullPathForThisEnemy); // Set position (already done in constructor with the parameters above) if (currentLevelNumber === 2 && LEVEL_DATA[2].pathPointsA && LEVEL_DATA[2].pathPointsB) { // Position is already set via the constructor } else { // Ensure position is set correctly for Level 1 (redundant but safe) enemy.x = pathPoints[0].x; enemy.y = pathPoints[0].y; } // Apply wave-based health scaling if you re-add it: // enemy.health += Math.floor(currentWave * 0.5); enemyLayer.addChild(enemy); enemies.push(enemy); spawnIndex++; if (spawnIndex < enemiesToSpawnThisWave.length) { LK.setTimeout(spawnIndividualEnemy, spawnInterval * 16.67 / gameSpeedMultiplier); } else { isWaveActive = true; } } isWaveActive = false; if (enemiesToSpawnThisWave.length > 0) { spawnIndividualEnemy(); } else { isWaveActive = true; // No enemies defined for this wave } } function checkWaveComplete() { if (isWaveActive && enemies.length === 0) { // Double check if we really spawned everything for the wave before proceeding // This basic check assumes spawning finishes before all enemies are killed isWaveActive = false; waveTimer = 0; // Reset timer for next wave delay if (currentWave >= MAX_WAVES) { if (currentLevelNumber === 1) { // Progress to Level 2 currentLevelNumber = 2; initializeGame(currentLevelNumber); return; // prevent showing "You Win" screen yet } else if (currentLevelNumber === 2) { // Player wins after completing Level 2 if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } LK.showYouWin(); } } } } // Event Handlers /**** MODIFIED ****/ // --- NEW APPROACH game.down --- game.down = function (x, y, obj) { var worldX = x; var worldY = y; // --- 1. Active UI Interaction (Menus, Dialogs) --- // Check Build Selection Icons if (currentActiveBuildSpot && currentActiveBuildSpot.areIconsVisible && currentActiveBuildSpot.selectionIconContainer) { var icons = currentActiveBuildSpot.selectionIconContainer.children; for (var i = 0; i < icons.length; i++) { // Check if the click was directly on an icon OR an interactive child of an icon if ((obj === icons[i] || isChildOf(icons[i], obj)) && obj.interactive) { // The icon's own .down() handler will be called by the engine. return; // CRITICAL: Stop further processing in game.down } } } // Check Tower Action Icons (Upgrade/Sell) if (currentActiveBuildSpot && currentActiveBuildSpot.areActionIconsVisible && currentActiveBuildSpot.actionIconContainer) { // Assuming areActionIconsVisible flag var actionIcons = currentActiveBuildSpot.actionIconContainer.children; for (var i = 0; i < actionIcons.length; i++) { // Check if the click was directly on an action icon OR an interactive child of an action icon if ((obj === actionIcons[i] || isChildOf(actionIcons[i], obj)) && obj.interactive) { // The action icon's own .down() handler will be called by the engine. return; // CRITICAL: Stop further processing in game.down } } } // Placeholder for Sell Confirmation Dialog interaction // (Assuming sellConfirmDialog is a global or accessible variable when visible) // if (typeof sellConfirmDialog !== 'undefined' && sellConfirmDialog && sellConfirmDialog.parent && sellConfirmDialog.visible) { // // Check if the click was on any interactive part of the sell confirmation dialog // if (obj === sellConfirmDialog.yesButton || obj === sellConfirmDialog.noButton || (isChildOf(sellConfirmDialog, obj) && obj.interactive) ) { // // The button's .down() handler will be called by the engine. // return; // CRITICAL // } // } // --- 2. Click on Doge (for dragging) --- if (doge && obj === doge.graphic) { dragDoge = true; doge.setTarget(doge.x, doge.y); // Stop current auto-movement return; // CRITICAL } // --- 3. Click on a BuildSpot graphic itself (to toggle its menu) --- if (level && level.buildSpots) { for (var i = 0; i < level.buildSpots.length; i++) { var spot = level.buildSpots[i]; // Check if the click was directly on the BuildSpot's graphic // OR on an interactive child nested within that graphic (if spot.graphic could be a container) if (obj === spot.graphic || isChildOf(spot.graphic, obj) && obj.interactive) { // The BuildSpot's own .down() handler (spot.down()) should be triggered by the engine // to manage its menu visibility. return; // CRITICAL } } } // --- 4. Close an Open Menu if "Empty Space" was clicked --- // This section runs if the click was NOT on an interactive UI icon/button (checked in step 1), // NOT on Doge (checked in step 2), and NOT on a BuildSpot graphic itself (checked in step 3). // Therefore, if a menu is open, this click is on "empty space" relative to that menu's purpose. if (currentActiveBuildSpot) { // A general spot has a menu open var clickedOnAnotherSpotToOpenItsMenu = false; // Flag to prevent closing if the "empty space" click was actually on another buildspot if (level && level.buildSpots) { for (var k = 0; k < level.buildSpots.length; k++) { if (level.buildSpots[k] !== currentActiveBuildSpot && (obj === level.buildSpots[k].graphic || isChildOf(level.buildSpots[k].graphic, obj) && obj.interactive)) { clickedOnAnotherSpotToOpenItsMenu = true; break; } } } if (!clickedOnAnotherSpotToOpenItsMenu) { // Only close if not trying to open another spot's menu if (currentActiveBuildSpot.areIconsVisible) { // Build selection menu is open currentActiveBuildSpot.hideSelectionIcons(); return; // CRITICAL } if (currentActiveBuildSpot.areActionIconsVisible) { // Tower action menu is open currentActiveBuildSpot.hideTowerActionIcons(); return; // CRITICAL } } } // Placeholder for closing Sell Confirmation Dialog on "empty space" click // if (typeof sellConfirmDialog !== 'undefined' && sellConfirmDialog && sellConfirmDialog.parent && sellConfirmDialog.visible) { // // Check if the click was outside the dialog content before closing // if (!isChildOf(sellConfirmDialog, obj) && obj !== sellConfirmDialog) { // Basic check: not on dialog or its children // sellConfirmDialog.destroy(); // or hide() // return; // CRITICAL // } // } // --- 5. Fallback: Move Doge --- // If we've reached this point, no UI element was specifically interacted with to trigger an action, // Doge wasn't clicked for dragging, no BuildSpot was clicked to toggle a menu, // and no menu was open to be closed by an empty space click (or the click was on another spot to open its menu). // This means the click was on the general walkable area. if (doge) { dragDoge = false; // Ensure not dragging if we just clicked empty space doge.setTarget(worldX, worldY); } }; game.move = function (x, y, obj) { // No doge dragging functionality needed }; game.up = function (x, y, obj) { // No doge dragging functionality needed }; // Only one game.up function is needed // Main game loop /**** MODIFIED ****/ game.update = function () { // --- Wave Management --- if (!isWaveActive && currentWave < MAX_WAVES) { // Only increment timer if not won yet waveTimer += gameSpeedMultiplier; if (waveTimer >= WAVE_DELAY) { waveTimer = 0; // Reset timer immediately spawnWave(); } } // --- Update Bullets --- (Remove destroyed ones) for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { bullets.splice(i, 1); } } // --- Update Enemies --- (Remove destroyed ones) for (var i = enemies.length - 1; i >= 0; i--) { if (!enemies[i].parent) { enemies.splice(i, 1); } } // --- Update Doge (handles its own cooldowns and attacks) --- if (doge && doge.parent) { // Check if doge exists doge.update(); // Make sure Doge's update runs } // --- Check Wave Completion --- checkWaveComplete(); // --- Update Bark Button UI --- /**** NEW ****/ if (doge) { // Check if doge exists if (doge.currentManualBarkCooldown > 0) { var secondsLeft = Math.ceil(doge.currentManualBarkCooldown / 60); // Approx seconds barkButton.setText("WAIT (" + secondsLeft + ")"); barkButton.setText("WAIT (" + secondsLeft + ")", { fill: 0x888888 }); // Grey out text } else { barkButton.setText("BARK!"); barkButton.setText("BARK!", { fill: 0xffcc00 }); // Restore color } } // --- Camera Panning Logic --- if (doge) { var CAM_SMOOTH_FACTOR = 0.1; // Define smoothing factor // --- Vertical Panning --- var PAN_THRESHOLD_VERTICAL = SCREEN_HEIGHT * 0.25; // Pan when Doge in top/bottom 25% var dogeScreenY = doge.y + game.y; // Doge's Y position on the actual screen var targetGameY = game.y; if (dogeScreenY < PAN_THRESHOLD_VERTICAL) { targetGameY = -(doge.y - PAN_THRESHOLD_VERTICAL); } else if (dogeScreenY > SCREEN_HEIGHT - PAN_THRESHOLD_VERTICAL) { targetGameY = -(doge.y - (SCREEN_HEIGHT - PAN_THRESHOLD_VERTICAL)); } var maxGameY = 0; var minGameY = -(MAP_HEIGHT - SCREEN_HEIGHT); if (MAP_HEIGHT <= SCREEN_HEIGHT) { // Prevent scrolling if map isn't taller minGameY = 0; } targetGameY = Math.max(minGameY, Math.min(maxGameY, targetGameY)); game.y += (targetGameY - game.y) * CAM_SMOOTH_FACTOR; if (Math.abs(game.y - targetGameY) < 1) { game.y = targetGameY; } // --- Horizontal Panning (NEW) --- var PAN_THRESHOLD_HORIZONTAL = SCREEN_WIDTH * 0.30; // Pan when Doge in left/right 30% var dogeScreenX = doge.x + game.x; // Doge's X position on the actual screen var targetGameX = game.x; if (dogeScreenX < PAN_THRESHOLD_HORIZONTAL) { targetGameX = -(doge.x - PAN_THRESHOLD_HORIZONTAL); } else if (dogeScreenX > SCREEN_WIDTH - PAN_THRESHOLD_HORIZONTAL) { targetGameX = -(doge.x - (SCREEN_WIDTH - PAN_THRESHOLD_HORIZONTAL)); } var maxGameX = 0; var minGameX = -(MAP_WIDTH - SCREEN_WIDTH); if (MAP_WIDTH <= SCREEN_WIDTH) { // Prevent scrolling if map isn't wider minGameX = 0; } targetGameX = Math.max(minGameX, Math.min(maxGameX, targetGameX)); game.x += (targetGameX - game.x) * CAM_SMOOTH_FACTOR; if (Math.abs(game.x - targetGameX) < 1) { game.x = targetGameX; } } // --- End Camera Panning Logic --- // --- Dynamically Update Icon Affordability --- if (currentActiveBuildSpot && currentActiveBuildSpot.areIconsVisible) { currentActiveBuildSpot.updateAffordability(); } }; // Initialize the game LK.setScore(0); // Reset score on start initializeGame(1);
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
var AuditorBot = Container.expand(function (spawnX, spawnY, valueToIgnore, customPathData) {
var self = Container.call(this);
// Animation Properties
self.walkFrames = ['enemyAuditorBot', 'enemyAuditorBot_walk_1', 'enemyAuditorBot_walk_2'];
self.currentAnimFrame = 0;
self.animationSpeed = 8;
self.animationCounter = 0;
// Path Properties
if (customPathData && customPathData.length > 0) {
self.myPathPoints = customPathData;
} else {
self.myPathPoints = pathPoints;
}
self.currentPathIndex = 0;
// Specific stats for AuditorBot
self.health = 45;
self.speed = 1.0;
self.value = 30;
self.isFlying = false;
// Initial Graphic & Position
self.graphic = self.attachAsset(self.walkFrames[0], {
anchorX: 0.5,
anchorY: 0.5
});
if (self.myPathPoints && self.myPathPoints.length > 0) {
self.x = self.myPathPoints[0].x;
self.y = self.myPathPoints[0].y;
} else if (spawnX !== undefined && spawnY !== undefined) {
self.x = spawnX;
self.y = spawnY;
}
self.update = function () {
// Path Following Logic
if (!self.myPathPoints || self.myPathPoints.length === 0) {
return;
}
if (self.currentPathIndex < self.myPathPoints.length) {
var target = self.myPathPoints[self.currentPathIndex];
if (!target) {
self.currentPathIndex++;
return;
}
var dx = target.x - self.x;
var dy = target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed * 1.5) {
self.currentPathIndex++;
if (self.currentPathIndex >= self.myPathPoints.length) {
playerLives--;
livesText.setText("Lives: " + playerLives);
LK.getSound('enemyReachGoal').play();
if (playerLives <= 0) {
if (LK.getScore() > storage.highScore) {
storage.highScore = LK.getScore();
}
LK.showGameOver();
}
self.destroy();
return;
}
} else {
dx = dx / distance * self.speed * gameSpeedMultiplier;
dy = dy / distance * self.speed * gameSpeedMultiplier;
self.x += dx;
self.y += dy;
}
} else {
if (self.parent) {
self.destroy();
}
return;
}
// Animation Logic
if (self.walkFrames && self.walkFrames.length > 1) {
self.animationCounter += gameSpeedMultiplier;
if (self.animationCounter >= self.animationSpeed) {
self.animationCounter = 0;
self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length;
var newAsset = self.walkFrames[self.currentAnimFrame];
if (self.graphic && self.graphic.parent) {
var currentLocalX = self.graphic.x;
var currentLocalY = self.graphic.y;
self.removeChild(self.graphic);
self.graphic = self.attachAsset(newAsset, {
anchorX: 0.5,
anchorY: 0.5,
x: currentLocalX,
y: currentLocalY
});
} else if (!self.graphic && self.parent) {
self.graphic = self.attachAsset(newAsset, {
anchorX: 0.5,
anchorY: 0.5
});
}
}
}
};
self.takeDamage = function (amount) {
self.health -= amount;
LK.effects.flashObject(self, 0xff0000, 200);
if (self.health <= 0 && self.parent) {
var numCoins = Math.ceil(self.value / 5);
if (numCoins < 1) {
numCoins = 1;
}
for (var i = 0; i < numCoins; i++) {
var coinValue = Math.floor(self.value / numCoins);
if (i === 0 && self.value % numCoins !== 0) {
coinValue += self.value % numCoins;
}
var coin = new DogeCoin(self.x, self.y, coinValue);
if (coinLayer) {
coinLayer.addChild(coin);
} else {
game.addChild(coin);
}
}
LK.setScore(LK.getScore() + self.value);
scoreText.setText("Score: " + LK.getScore());
LK.getSound('enemyDeath').play();
self.destroy();
}
};
var originalDestroy = self.destroy;
self.destroy = function () {
if (typeof originalDestroy === 'function') {
originalDestroy.call(self);
} else if (self.parent) {
self.parent.removeChild(self);
}
};
return self;
});
// ---- END: REPLACE AuditorBot CLASS WITH THIS ----
var BarkWave = Container.expand(function () {
var self = Container.call(this);
var graphic = self.attachAsset('barkWave', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5
});
self.damage = 2; // Slightly increased damage for manual ability
self.duration = 30; // frames
self.radius = 100;
self.maxRadius = 350; // Slightly larger radius for manual ability
self.update = function () {
self.radius += 15 * gameSpeedMultiplier; // Expand slightly faster
graphic.scaleX = self.radius / 100;
graphic.scaleY = self.radius / 100;
graphic.alpha -= 0.017 * gameSpeedMultiplier;
// Check for enemies in range ONCE per enemy per wave activation
// To avoid hitting the same enemy multiple times with one wave
if (!self.enemiesHit) {
self.enemiesHit = [];
} // Initialize if needed
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Only check if enemy hasn't been hit by this wave yet
if (self.enemiesHit.indexOf(enemy) === -1) {
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.radius) {
enemy.takeDamage(self.damage);
self.enemiesHit.push(enemy); // Mark enemy as hit
}
}
}
self.duration -= gameSpeedMultiplier;
if (self.duration <= 0 || self.radius >= self.maxRadius) {
self.destroy();
}
};
return self;
});
// --- REPLACE THE ENTIRE BuildSpot CLASS DEFINITION WITH THIS ---
var BuildSpot = Container.expand(function () {
var self = Container.call(this);
self.graphic = self.attachAsset('buildSpot', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5
});
self.graphic.interactive = true; // Ensure the graphic is interactive
self.graphic.interactive = true; // Ensure the graphic is interactive
self.hasTower = false;
self.tower = null; // Reference to the tower built on this spot
self.selectionIconContainer = null;
self.upgradeMenuContainer = null;
self.actionIconContainer = null; // Container for upgrade/sell icons
self.areIconsVisible = false;
self.isUpgradeMenuVisible = false;
self.areActionIconsVisible = false; // Flag for upgrade/sell icons
self.showSelectionIcons = function () {
if (self.hasTower || self.areIconsVisible) {
return;
}
if (currentActiveBuildSpot && currentActiveBuildSpot !== self) {
currentActiveBuildSpot.hideSelectionIcons();
if (currentActiveBuildSpot.isUpgradeMenuVisible) {
currentActiveBuildSpot.hideUpgradeMenu();
}
if (currentActiveBuildSpot.areActionIconsVisible) {
currentActiveBuildSpot.hideTowerActionIcons();
}
}
currentActiveBuildSpot = self;
self.selectionIconContainer = new Container();
self.selectionIconContainer.x = self.x; // Position container AT the BuildSpot's x (in 'level' space)
self.selectionIconContainer.y = self.y; // Position container AT the BuildSpot's y (in 'level' space)
// Add to the SAME parent as the BuildSpot (e.g., 'level').
// This ensures icons scroll with the game world.
if (self.parent) {
// BuildSpot should have a parent (level)
self.parent.addChild(self.selectionIconContainer);
} else {
game.addChild(self.selectionIconContainer); // Fallback to game, but level is better
}
for (var i = 0; i < ICON_SELECT_OFFSETS.length; i++) {
var offsetData = ICON_SELECT_OFFSETS[i];
var towerTypeKey = offsetData.towerKey;
var towerInfo = TOWER_DATA[towerTypeKey];
if (!towerInfo || !towerInfo.levels || !towerInfo.levels[0] || !towerInfo.iconAsset) {
// Visual error or skip
var err = new Text2("Data? " + towerTypeKey, {
size: 10,
fill: 0xff0000
});
err.x = offsetData.x;
err.y = offsetData.y;
self.selectionIconContainer.addChild(err);
continue;
}
var cost = towerInfo.levels[0].cost;
// Create the icon graphic directly (no extra container per icon needed)
var iconGraphic = self.selectionIconContainer.attachAsset(towerInfo.iconAsset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
iconGraphic.x = offsetData.x; // Position relative to selectionIconContainer's origin
iconGraphic.y = offsetData.y; // (which is the BuildSpot's center)
iconGraphic.interactive = true;
iconGraphic.towerTypeKey = towerTypeKey; // Store for click
// Add cost text AS A CHILD of the iconGraphic (or selectionIconContainer)
var costText = new Text2("$" + cost, {
size: 20,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 2
});
costText.anchor.set(0.5, -0.7); // Position below the icon's center
iconGraphic.addChild(costText); // Text is child of icon, moves with it.
if (currency < cost) {
iconGraphic.alpha = 0.4;
iconGraphic.interactive = false;
} else {
iconGraphic.alpha = 1.0;
}
iconGraphic.down = function () {
var buildCost = TOWER_DATA[this.towerTypeKey].levels[0].cost;
if (currency >= buildCost) {
self.buildSelectedTower(this.towerTypeKey);
// No need to call hideSelectionIcons here, game.down will handle it
// because the click was on an interactive element.
} else {
var tempIconForPos = this; // 'this' is the iconGraphic
var tempParentPos = tempIconForPos.parent.localToGlobal({
x: tempIconForPos.x,
y: tempIconForPos.y
});
var screenY = tempParentPos.y + game.y;
spawnFloatingText("Need More $!", tempParentPos.x, screenY - 30, {
fill: 0xFF0000
});
}
// Always hide icons after a choice or attempted choice on an icon
self.hideSelectionIcons();
};
}
self.areIconsVisible = true;
// LK.getSound('uiOpenMenu').play(); // Sound was moved to BuildSpot.down
};
self.hideSelectionIcons = function () {
if (self.selectionIconContainer) {
self.selectionIconContainer.destroy();
self.selectionIconContainer = null;
}
self.areIconsVisible = false;
if (currentActiveBuildSpot === self) {
currentActiveBuildSpot = null;
}
};
self.hideUpgradeMenu = function () {
if (self.upgradeMenuContainer) {
self.upgradeMenuContainer.destroy();
self.upgradeMenuContainer = null;
}
// Destroy range indicators if they exist
if (self.currentRangeIndicator) {
self.currentRangeIndicator.destroy();
self.currentRangeIndicator = null;
}
if (self.nextRangeIndicator) {
self.nextRangeIndicator.destroy();
self.nextRangeIndicator = null;
}
self.isUpgradeMenuVisible = false;
if (currentActiveBuildSpot === self) {
currentActiveBuildSpot = null;
}
};
self.showTowerActionIcons = function (towerObject) {
if (!towerObject || !towerObject.towerType) {
return;
}
// Create container for action icons
self.actionIconContainer = new Container();
self.actionIconContainer.x = self.x;
self.actionIconContainer.y = self.y;
// Add to the same parent as the BuildSpot
if (self.parent) {
self.parent.addChild(self.actionIconContainer);
} else {
game.addChild(self.actionIconContainer);
}
// Get tower data
var towerTypeKey = towerObject.towerType;
var currentLevel = towerObject.currentLevel;
var towerInfo = TOWER_DATA[towerTypeKey];
var currentLevelData = towerInfo.levels[currentLevel];
var nextLevelData = towerInfo.levels[currentLevel + 1];
var isMaxLevel = !nextLevelData;
// Create a range indicator to show tower's attack range
self.rangeIndicator = createRangeIndicator(self.x, self.y, currentLevelData.range, self.parent);
// Position offsets for the icons
var upgradeIconOffsetX = -110;
var upgradeIconOffsetY = -110;
var sellIconOffsetX = 110;
var sellIconOffsetY = -110;
// Calculate total tower cost for sell value
var totalTowerCost = 0;
for (var i = 0; i <= currentLevel; i++) {
totalTowerCost += towerInfo.levels[i].cost;
}
var sellValue = Math.floor(totalTowerCost * 0.5); // 50% refund
// Upgrade Icon (if upgradable)
if (!isMaxLevel) {
// Create upgrade icon using the next level's asset
var upgradeIconAsset = towerInfo.levels[currentLevel + 1].asset;
var upgradeIcon = self.actionIconContainer.attachAsset(upgradeIconAsset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.0,
scaleY: 1.0
});
upgradeIcon.x = upgradeIconOffsetX;
upgradeIcon.y = upgradeIconOffsetY;
// Add plus indicator
var plusIcon = self.actionIconContainer.attachAsset('uiButtonPlus', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
plusIcon.x = upgradeIcon.x + 35;
plusIcon.y = upgradeIcon.y - 35;
// Add upgrade cost text
var upgradeCostText = new Text2("$" + nextLevelData.cost, {
size: 20,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 2
});
upgradeCostText.anchor.set(0.5, -0.7);
upgradeCostText.x = upgradeIconOffsetX;
upgradeCostText.y = upgradeIconOffsetY;
self.actionIconContainer.addChild(upgradeCostText);
// Make upgrade icon interactive
upgradeIcon.interactive = currency >= nextLevelData.cost;
upgradeIcon.alpha = currency >= nextLevelData.cost ? 1.0 : 0.4;
upgradeIcon.down = function () {
self.tower.upgrade();
self.hideTowerActionIcons();
// If successfully upgraded and still not at max level, show the action icons again
if (self.tower && self.tower.currentLevel < TOWER_DATA[self.tower.towerType].levels.length - 1) {
LK.setTimeout(function () {
self.showTowerActionIcons(self.tower);
}, 100);
}
};
} else {
// Show max level indicator (optional)
var maxLevelIcon = self.actionIconContainer.attachAsset(currentLevelData.asset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8,
alpha: 0.7
});
maxLevelIcon.x = upgradeIconOffsetX;
maxLevelIcon.y = upgradeIconOffsetY;
var maxText = new Text2("MAX", {
size: 18,
fill: 0xFFD700,
stroke: 0x000000,
strokeThickness: 2
});
maxText.anchor.set(0.5, 0.5);
maxText.x = upgradeIconOffsetX;
maxText.y = upgradeIconOffsetY;
self.actionIconContainer.addChild(maxText);
}
// Sell Icon
var sellIcon = self.actionIconContainer.attachAsset('iconSell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
sellIcon.x = sellIconOffsetX;
sellIcon.y = sellIconOffsetY;
sellIcon.interactive = true;
// Add sell value text
var sellText = new Text2("$" + sellValue, {
size: 20,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 2
});
sellText.anchor.set(0.5, -0.7);
sellText.x = sellIconOffsetX;
sellText.y = sellIconOffsetY;
self.actionIconContainer.addChild(sellText);
// Make sell icon interactive
sellIcon.down = function () {
self.confirmSellTower(towerObject);
};
// Mark as visible
self.areActionIconsVisible = true;
LK.getSound('uiOpenMenu').play();
};
self.hideTowerActionIcons = function () {
if (self.actionIconContainer) {
self.actionIconContainer.destroy();
self.actionIconContainer = null;
}
// Destroy range indicator if it exists
if (self.rangeIndicator) {
self.rangeIndicator.destroy();
self.rangeIndicator = null;
}
self.areActionIconsVisible = false;
if (currentActiveBuildSpot === self) {
currentActiveBuildSpot = null;
}
};
self.confirmSellTower = function (towerObject) {
// First, hide action icons
self.hideTowerActionIcons();
// Create a confirmation UI
var confirmContainer = new Container();
confirmContainer.x = self.x;
confirmContainer.y = self.y - 150;
if (self.parent) {
self.parent.addChild(confirmContainer);
} else {
game.addChild(confirmContainer);
}
// Calculate sell value
var towerTypeKey = towerObject.towerType;
var currentLevel = towerObject.currentLevel;
var totalTowerCost = 0;
for (var i = 0; i <= currentLevel; i++) {
totalTowerCost += TOWER_DATA[towerTypeKey].levels[i].cost;
}
var sellValue = Math.floor(totalTowerCost * 0.5); // 50% refund
// Background
var confirmBg = confirmContainer.attachAsset('uiButtonBackground', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 4.0,
scaleY: 2.5,
alpha: 0.6
});
// Confirmation text
var confirmText = new Text2("Sell for $" + sellValue + "?", {
size: 36,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 2
});
confirmText.anchor.set(0.5, 0);
confirmText.y = -60;
confirmContainer.addChild(confirmText);
// Yes button
var yesButton = new Text2("YES", {
size: 40,
fill: 0x00FF00,
stroke: 0x000000,
strokeThickness: 2
});
yesButton.anchor.set(0.5, 0);
yesButton.y = 30;
yesButton.x = -70;
yesButton.interactive = true;
yesButton.down = function () {
self.sellTower(towerObject, sellValue);
confirmContainer.destroy();
};
confirmContainer.addChild(yesButton);
// No button
var noButton = new Text2("NO", {
size: 40,
fill: 0xFF0000,
stroke: 0x000000,
strokeThickness: 2
});
noButton.anchor.set(0.5, 0);
noButton.y = 30;
noButton.x = 70;
noButton.interactive = true;
noButton.down = function () {
confirmContainer.destroy();
// Show tower action icons again
LK.setTimeout(function () {
self.showTowerActionIcons(towerObject);
}, 100);
};
confirmContainer.addChild(noButton);
};
self.sellTower = function (towerObject, sellValue) {
// Add refund to currency
currency += sellValue;
currencyText.setText("$: " + currency);
// Play sell sound
LK.getSound('sellSound').play();
// Spawn floating text
spawnFloatingText("SOLD!", self.x, self.y - 50, {
fill: 0xFFD700
});
// Clean up tower
if (towerObject) {
towerObject.destroy();
}
// Reset BuildSpot
self.tower = null;
self.hasTower = false;
self.graphic.alpha = 0.5;
};
self.updateAffordability = function () {
if (!self.areIconsVisible || !self.selectionIconContainer) {
return;
}
var icons = self.selectionIconContainer.children;
for (var i = 0; i < icons.length; i++) {
var iconGraphic = icons[i]; // Now iconGraphic is the direct child
if (iconGraphic && iconGraphic.towerTypeKey && TOWER_DATA[iconGraphic.towerTypeKey]) {
var cost = TOWER_DATA[iconGraphic.towerTypeKey].levels[0].cost;
if (currency < cost) {
iconGraphic.alpha = 0.4;
iconGraphic.interactive = false;
} else {
iconGraphic.alpha = 1.0;
iconGraphic.interactive = true;
}
}
}
};
self.toggleUpgradeMenu = function () {
// If upgrade menu is already visible, hide it
if (self.isUpgradeMenuVisible) {
self.hideUpgradeMenu();
return;
}
// Hide any other active menus first
if (currentActiveBuildSpot && currentActiveBuildSpot !== self) {
if (currentActiveBuildSpot.areIconsVisible) {
currentActiveBuildSpot.hideSelectionIcons();
} else if (currentActiveBuildSpot.isUpgradeMenuVisible) {
currentActiveBuildSpot.hideUpgradeMenu();
} else if (currentActiveBuildSpot.areActionIconsVisible) {
currentActiveBuildSpot.hideTowerActionIcons();
}
}
// Set this as the active build spot
currentActiveBuildSpot = self;
// Create and show the upgrade menu
self.showUpgradeMenu(self.tower);
};
self.showUpgradeMenu = function (towerObject) {
if (!towerObject || !towerObject.towerType) {
return;
}
// Create container for upgrade menu
self.upgradeMenuContainer = new Container();
self.upgradeMenuContainer.x = self.x;
self.upgradeMenuContainer.y = self.y - 150; // Position above the tower
// Add to the same parent as the BuildSpot
if (self.parent) {
self.parent.addChild(self.upgradeMenuContainer);
} else {
game.addChild(self.upgradeMenuContainer);
}
// Get tower data
var towerTypeKey = towerObject.towerType;
var currentLevel = towerObject.currentLevel;
var towerInfo = TOWER_DATA[towerTypeKey];
var currentLevelData = towerInfo.levels[currentLevel];
var nextLevelData = towerInfo.levels[currentLevel + 1];
var isMaxLevel = !nextLevelData;
// Create range indicator for current level
self.currentRangeIndicator = createRangeIndicator(self.x, self.y, currentLevelData.range, self.parent);
// If there's a next level, show its range too with a different appearance
if (!isMaxLevel) {
self.nextRangeIndicator = createRangeIndicator(self.x, self.y, nextLevelData.range, self.parent);
// Make the next level range indicator more distinct
if (self.nextRangeIndicator.children && self.nextRangeIndicator.children.length >= 2) {
self.nextRangeIndicator.children[0].tint = 0x00FF00; // Green tint for the fill
self.nextRangeIndicator.children[1].tint = 0x00FF00; // Green tint for the border
}
}
// Create background
var menuBg = self.upgradeMenuContainer.attachAsset('uiButtonBackground', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 3
});
// Tower name and level
var titleText = new Text2(towerInfo.name + " - Level " + (currentLevel + 1), {
size: 24,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 2
});
titleText.anchor.set(0.5, 0);
titleText.y = -120;
self.upgradeMenuContainer.addChild(titleText);
// If there's a next level available
if (!isMaxLevel) {
// Upgrade cost
var costText = new Text2("Upgrade Cost: $" + nextLevelData.cost, {
size: 20,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 2
});
costText.anchor.set(0.5, 0);
costText.y = -85;
self.upgradeMenuContainer.addChild(costText);
// Stat changes
var statsY = -55;
var statsGap = 25;
// Add stat comparisons based on tower type
if (towerTypeKey === 'stapler') {
// Damage
var damageText = new Text2("Damage: " + currentLevelData.damage + " → " + nextLevelData.damage, {
size: 18,
fill: 0xFFFFFF
});
damageText.anchor.set(0.5, 0);
damageText.y = statsY;
self.upgradeMenuContainer.addChild(damageText);
// Range
var rangeText = new Text2("Range: " + currentLevelData.range + " → " + nextLevelData.range, {
size: 18,
fill: 0xFFFFFF
});
rangeText.anchor.set(0.5, 0);
rangeText.y = statsY + statsGap;
self.upgradeMenuContainer.addChild(rangeText);
// Fire Rate
var fireRateText = new Text2("Fire Rate: " + (60 / currentLevelData.fireRate).toFixed(1) + " → " + (60 / nextLevelData.fireRate).toFixed(1), {
size: 18,
fill: 0xFFFFFF
});
fireRateText.anchor.set(0.5, 0);
fireRateText.y = statsY + statsGap * 2;
self.upgradeMenuContainer.addChild(fireRateText);
} else if (towerTypeKey === 'blocker') {
// Slow Factor (convert to percentage for clarity)
var slowText = new Text2("Slow: " + (1 - currentLevelData.slowFactor) * 100 + "% → " + (1 - nextLevelData.slowFactor) * 100 + "%", {
size: 18,
fill: 0xFFFFFF
});
slowText.anchor.set(0.5, 0);
slowText.y = statsY;
self.upgradeMenuContainer.addChild(slowText);
// Range
var rangeText = new Text2("Range: " + currentLevelData.range + " → " + nextLevelData.range, {
size: 18,
fill: 0xFFFFFF
});
rangeText.anchor.set(0.5, 0);
rangeText.y = statsY + statsGap;
self.upgradeMenuContainer.addChild(rangeText);
}
// Upgrade button
var upgradeButton = new Text2("UPGRADE", {
size: 28,
fill: currency >= nextLevelData.cost ? 0x00FF00 : 0xFF0000,
stroke: 0x000000,
strokeThickness: 2
});
upgradeButton.anchor.set(0.5, 0);
upgradeButton.y = 30;
upgradeButton.interactive = currency >= nextLevelData.cost;
upgradeButton.down = function () {
self.tower.upgrade();
};
self.upgradeMenuContainer.addChild(upgradeButton);
} else {
// Max level text
var maxLevelText = new Text2("MAX LEVEL REACHED", {
size: 24,
fill: 0xFFD700,
// Gold color
stroke: 0x000000,
strokeThickness: 2
});
maxLevelText.anchor.set(0.5, 0);
maxLevelText.y = -50;
self.upgradeMenuContainer.addChild(maxLevelText);
}
// Sell button (optional)
var sellButton = new Text2("SELL", {
size: 24,
fill: 0xFF9999,
stroke: 0x000000,
strokeThickness: 2
});
sellButton.anchor.set(0.5, 0);
sellButton.y = 70;
sellButton.interactive = true;
sellButton.down = function () {
// Calculate total value of tower for refund
var totalTowerCost = 0;
for (var i = 0; i <= currentLevel; i++) {
totalTowerCost += TOWER_DATA[towerTypeKey].levels[i].cost;
}
var sellValue = Math.floor(totalTowerCost * 0.5); // 50% refund
// Add refund to currency
currency += sellValue;
currencyText.setText("$: " + currency);
spawnFloatingText("Sold for $" + sellValue, self.x, self.y - 50, {
fill: 0xFFD700
});
LK.getSound('sellSound').play();
self.tower.destroy();
self.tower = null;
self.hasTower = false;
self.graphic.alpha = 0.5; // Reset buildspot opacity
self.hideUpgradeMenu();
};
self.upgradeMenuContainer.addChild(sellButton);
// Play sound when opening menu
LK.getSound('uiOpenMenu').play();
// Mark menu as visible
self.isUpgradeMenuVisible = true;
};
self.buildSelectedTower = function (towerTypeKey) {
if (self.hasTower || !TOWER_DATA[towerTypeKey]) {
return;
}
var towerLevelData = TOWER_DATA[towerTypeKey].levels[0];
if (currency < towerLevelData.cost) {
return;
}
currency -= towerLevelData.cost;
currencyText.setText("$: " + currency);
var newTower;
if (towerTypeKey === 'stapler') {
newTower = new StaplerTower();
} else if (towerTypeKey === 'blocker') {
newTower = new BureaucracyBlockerTower();
} else if (towerTypeKey === 'laserCat') {
newTower = new LaserCatPerch();
} else if (towerTypeKey === 'rickroller') {
newTower = new RickrollerTower();
// Play placement sound
LK.getSound('sfxRickrollerPlace').play();
} else if (towerTypeKey === 'thisIsFine') {
newTower = new ThisIsFinePit();
// Play match strike sound when placing the fire pit
LK.getSound('sfxThisIsFinePlaceMatch').play();
// Start gentle fire sound (looping, low volume initially)
LK.getSound('sfxThisIsFineFireGentle').play();
} else if (towerTypeKey === 'restruct') {
newTower = new RestructuringSpecialist();
}
// No need for special newTower.init(0) call - constructor handles initialization
// else if (towerTypeKey === 'placeholder') { /* Do nothing or build a dummy */ return; }
if (newTower) {
// The tower's constructor now calls initializeTowerFromData(self, type, 0)
newTower.x = 0; // Place tower at BuildSpot's origin (local to BuildSpot)
newTower.y = 0;
self.addChild(newTower); // Tower becomes child of BuildSpot
self.tower = newTower;
self.hasTower = true;
self.graphic.alpha = 0.1; // Dim the build spot graphic itself
var buildSfx = TOWER_DATA[towerTypeKey].buildSfx || 'uiSelectTower'; // Fallback
// Ensure these sounds exist: 'buildStapler', 'buildBlocker'
// LK.getSound(buildSfx).play(); // Play specific build sound
// Floating text relative to build spot in world space
spawnFloatingText(TOWER_DATA[towerTypeKey].name + " BUILT!", self.x, self.y - self.graphic.height / 2, {
fill: 0x00FF00
});
// Update affordability for any other spot's menu that might be open (shouldn't be with current logic)
if (currentActiveBuildSpot && currentActiveBuildSpot !== self) {
currentActiveBuildSpot.updateAffordability();
}
}
// Icons are hidden by the icon's down handler OR by game.down clicking elsewhere
};
self.down = function () {
// This is the click on the BuildSpot graphic itself
if (self.hasTower) {
// If action icons are already visible, clicking again should close them
if (self.areActionIconsVisible) {
self.hideTowerActionIcons();
} else {
// Hide any other open menus
if (currentActiveBuildSpot && currentActiveBuildSpot !== self) {
if (currentActiveBuildSpot.areIconsVisible) {
currentActiveBuildSpot.hideSelectionIcons();
} else if (currentActiveBuildSpot.isUpgradeMenuVisible) {
currentActiveBuildSpot.hideUpgradeMenu();
} else if (currentActiveBuildSpot.areActionIconsVisible) {
currentActiveBuildSpot.hideTowerActionIcons();
}
}
// Set this as the active build spot
currentActiveBuildSpot = self;
// Show tower action icons
LK.getSound('uiOpenMenu').play();
self.showTowerActionIcons(self.tower);
}
return;
}
// If this spot's icons are already visible, clicking it again should close them.
if (self.areIconsVisible) {
self.hideSelectionIcons();
} else {
// If another spot's icons are visible, hide them first.
if (currentActiveBuildSpot && currentActiveBuildSpot !== self) {
currentActiveBuildSpot.hideSelectionIcons();
}
LK.getSound('uiOpenMenu').play(); // Play sound when opening
self.showSelectionIcons();
}
};
return self;
});
// --- MODIFY BureaucracyBlockerTower ---
var BureaucracyBlockerTower = Container.expand(function () {
var self = Container.call(this);
// --- MODIFIED: No graphic initialization in constructor ---
self.enemiesSlowed = [];
// init method removed - functionality moved to constructor and initializeTowerFromData
self.clearAllSlows = function () {
for (var i = 0; i < self.enemiesSlowed.length; i++) {
var enemy = self.enemiesSlowed[i];
if (enemy && enemy.parent && enemy.isSlowedByBlocker === self) {
enemy.speed = enemy.originalSpeed;
enemy.isSlowedByBlocker = null;
if (enemy.graphic) {
enemy.graphic.tint = 0xFFFFFF;
}
}
}
self.enemiesSlowed = [];
};
self.update = function () {
var buildSpot = self.parent;
if (!buildSpot || !buildSpot.parent) {
return;
}
var levelContainer = buildSpot.parent;
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
var towerCenterInGameSpace = game.toLocal(towerGraphicGlobalCenter);
var newlySlowedThisFrame = [];
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy || !enemy.parent) {
continue;
} // Skip if enemy is already gone
var dx = enemy.x - towerCenterInGameSpace.x;
var dy = enemy.y - towerCenterInGameSpace.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < self.range * self.range) {
// Enemy is in range
if (!enemy.isSlowedByBlocker) {
// Check a custom flag
enemy.originalSpeed = enemy.speed; // Store original speed
enemy.speed *= self.slowFactor;
enemy.isSlowedByBlocker = self; // Mark who slowed it
// Optional: Visual effect on enemy
if (enemy.graphic) {
enemy.graphic.tint = 0xAAAAFF;
} // Light blue tint
}
newlySlowedThisFrame.push(enemy);
}
}
// Check enemies that were slowed last frame but might be out of range now
for (var i = self.enemiesSlowed.length - 1; i >= 0; i--) {
var previouslySlowedEnemy = self.enemiesSlowed[i];
if (!previouslySlowedEnemy || !previouslySlowedEnemy.parent || newlySlowedThisFrame.indexOf(previouslySlowedEnemy) === -1) {
// Enemy is gone or no longer in range by this tower
if (previouslySlowedEnemy && previouslySlowedEnemy.isSlowedByBlocker === self) {
// Only unslow if WE slowed it
previouslySlowedEnemy.speed = previouslySlowedEnemy.originalSpeed;
previouslySlowedEnemy.isSlowedByBlocker = null;
if (previouslySlowedEnemy.graphic) {
previouslySlowedEnemy.graphic.tint = 0xFFFFFF;
} // Reset tint
}
self.enemiesSlowed.splice(i, 1);
}
}
self.enemiesSlowed = newlySlowedThisFrame; // Update the list of currently slowed enemies
};
// When tower is destroyed, make sure to unslow any enemies it was affecting
var originalDestroy = self.destroy;
self.destroy = function () {
for (var i = 0; i < self.enemiesSlowed.length; i++) {
var enemy = self.enemiesSlowed[i];
if (enemy && enemy.parent && enemy.isSlowedByBlocker === self) {
enemy.speed = enemy.originalSpeed;
enemy.isSlowedByBlocker = null;
if (enemy.graphic) {
enemy.graphic.tint = 0xFFFFFF;
}
}
}
self.enemiesSlowed = [];
if (originalDestroy) {
originalDestroy.call(self);
} else if (self.parent) {
self.parent.removeChild(self);
} // Basic destroy
};
// Initialize tower with level 0 stats and graphics
initializeTowerFromData(self, 'blocker', 0); // Set initial stats & correct L0 asset
return self;
});
var DogeAutoProjectile = Container.expand(function () {
var self = Container.call(this);
self.graphic = self.attachAsset('basicAttackProjectile', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 15;
self.target = null;
self.damage = 0; // Will be set by DogeHero when creating projectile
self.duration = 20; // frames, for self-destruction if target is lost
self.update = function () {
// Decrement duration
self.duration -= gameSpeedMultiplier;
if (self.duration <= 0) {
self.destroy();
return;
}
// If target is gone or not in game anymore
if (!self.target || !self.target.parent) {
self.alpha -= 0.1 * gameSpeedMultiplier;
if (self.alpha <= 0) {
self.destroy();
}
return;
}
// Move towards target
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// If very close to target, destroy
if (distance < self.speed * gameSpeedMultiplier || distance < self.target.graphic.width / 2) {
// Apply damage when hitting the target
if (self.target && self.target.parent) {
self.target.takeDamage(self.damage);
}
self.destroy();
return;
}
// Move towards target
dx = dx / distance * self.speed * gameSpeedMultiplier;
dy = dy / distance * self.speed * gameSpeedMultiplier;
self.x += dx;
self.y += dy;
};
return self;
});
var DogeCoin = Container.expand(function (spawnX, spawnY, value) {
var self = Container.call(this);
// Initialize coin properties
self.graphic = self.attachAsset('dogeCoin', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = spawnX;
self.y = spawnY;
self.value = value || 1; // Default to 1 if no value passed
self.isOnGround = false;
self.isBeingCollected = false;
self.spinSpeed = 0.1; // Radians per frame for spinning
self.collectionRadius = 300; // How close Doge needs to be
self.lifeTime = 1500; // Frames before it disappears if not collected (25s @ 60fps with gameSpeedMultiplier = 1.0)
// Initial scatter animation
var scatterAngle = Math.random() * Math.PI * 2;
var scatterDistance = 20 + Math.random() * 30;
var targetX = self.x + Math.cos(scatterAngle) * scatterDistance;
var targetY = self.y + Math.sin(scatterAngle) * scatterDistance;
// Start with smaller scale and grow to normal size
self.graphic.scale.set(0.5);
// Animate the coin to scatter position and grow to normal size
tween(self, {
x: targetX,
y: targetY
}, {
duration: 300 * gameSpeedMultiplier,
easing: tween.quadOut,
onFinish: function onFinish() {
self.isOnGround = true;
}
});
tween(self.graphic.scale, {
x: 1.0,
y: 1.0
}, {
duration: 300 * gameSpeedMultiplier,
easing: tween.quadOut
});
// Update method for coin behavior
self.update = function () {
self.lifeTime -= gameSpeedMultiplier;
// Destroy if lifetime expires and not being collected
if (self.lifeTime <= 0 && !self.isBeingCollected) {
self.destroy();
return;
}
// Skip other updates if being collected
if (self.isBeingCollected) {
return;
}
// Spin when on ground
if (self.isOnGround) {
self.graphic.rotation += self.spinSpeed * gameSpeedMultiplier;
}
// Check if Doge is close enough to collect
if (self.isOnGround && doge && doge.parent) {
var dx = doge.x - self.x;
var dy = doge.y - self.y;
if (dx * dx + dy * dy < self.collectionRadius * self.collectionRadius) {
self.collect();
}
}
};
// Collect method when Doge picks up the coin
self.collect = function () {
if (self.isBeingCollected) {
return;
}
self.isBeingCollected = true;
// Update currency and UI
currency += self.value;
currencyText.setText("$: " + currency);
// Show floating text
spawnFloatingText("+" + self.value + "$", self.x, self.y - 20, {
fill: 0xFFFF00,
velocityY: -2
});
// Get target position for animation
var targetUiGlobal = currencyText.toGlobal({
x: currencyText.width / 2,
y: currencyText.height / 2
});
var targetUiInGame = game.toLocal(targetUiGlobal);
// Animate coin flying to the UI
tween(self, {
x: targetUiInGame.x,
y: targetUiInGame.y,
alpha: 0.5
}, {
duration: 500 * gameSpeedMultiplier,
easing: tween.quadIn,
onFinish: function onFinish() {
self.destroy();
}
});
// Scale graphic separately since nested properties need their own tween
tween(self.graphic.scale, {
x: 0.2,
y: 0.2
}, {
duration: 500 * gameSpeedMultiplier,
easing: tween.quadIn
});
};
return self;
});
// DogeHero - Now includes auto-attack and manual bark ability logic
var DogeHero = Container.expand(function () {
var self = Container.call(this);
self.graphic = self.attachAsset('dogeHero', {
anchorX: 0.5,
anchorY: 0.5
}); // Expose graphic if needed for hit check
self.width = self.graphic.width; // Store size for hit checks
self.height = self.graphic.height;
self.speed = 5;
self.targetX = self.x;
self.targetY = self.y;
// Auto Attack Stats
self.autoAttackRange = 450; // Increased from 280
self.autoAttackDamage = 1; // Decreased from 2
self.autoAttackCooldownTime = 45;
self.currentAutoAttackCooldown = 0;
// Manual Bark Ability Stats
self.manualBarkCooldownTime = 300;
self.currentManualBarkCooldown = 0;
self.update = function () {
// Movement
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > self.speed) {
dx = dx / distance * self.speed * gameSpeedMultiplier;
dy = dy / distance * self.speed * gameSpeedMultiplier;
self.x += dx;
self.y += dy;
} else if (distance > 0) {
self.x = self.targetX;
self.y = self.targetY;
}
// Cooldowns
if (self.currentAutoAttackCooldown > 0) {
self.currentAutoAttackCooldown -= gameSpeedMultiplier;
}
if (self.currentManualBarkCooldown > 0) {
self.currentManualBarkCooldown -= gameSpeedMultiplier;
}
// Auto Attack Logic
if (self.currentAutoAttackCooldown <= 0) {
var closestEnemy = null;
var minDistanceSq = self.autoAttackRange * self.autoAttackRange;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var ex = enemy.x - self.x;
var ey = enemy.y - self.y;
var distSq = ex * ex + ey * ey;
if (distSq < minDistanceSq) {
minDistanceSq = distSq;
closestEnemy = enemy;
}
}
if (closestEnemy) {
// Play sound and reset cooldown
LK.getSound('dogeAutoAttack').play();
self.currentAutoAttackCooldown = self.autoAttackCooldownTime;
// Create projectile that will handle damage application
var projectile = new DogeAutoProjectile();
projectile.x = self.x;
projectile.y = self.y;
projectile.target = closestEnemy;
projectile.damage = self.autoAttackDamage; // Pass damage value to projectile
game.addChild(projectile);
}
}
};
self.setTarget = function (x, y) {
// Check if we're trying to target a BuildSpot or any of its menus
var isClickingUI = false;
// Skip movement if there's any active BuildSpot with open menus
if (currentActiveBuildSpot) {
isClickingUI = true;
}
// Check if clicked on any BuildSpot
if (level && level.buildSpots && !isClickingUI) {
for (var i = 0; i < level.buildSpots.length; i++) {
var spot = level.buildSpots[i];
// If we passed the graphic check in game.down
// but still reached setTarget, check again
var dx = x - spot.x;
var dy = y - spot.y;
var distSq = dx * dx + dy * dy;
var hitRadius = 75; // Half of the 150px BuildSpot width
if (distSq <= hitRadius * hitRadius) {
isClickingUI = true;
break;
}
}
}
// Only move if not clicking UI
if (!isClickingUI) {
self.targetX = x;
self.targetY = y;
}
};
self.manualBark = function () {
if (self.currentManualBarkCooldown <= 0) {
var wave = new BarkWave();
wave.x = self.x;
wave.y = self.y;
game.addChild(wave);
LK.getSound('dogeBark').play();
self.currentManualBarkCooldown = self.manualBarkCooldownTime;
return true;
}
return false;
};
return self;
});
// ---- START: REPLACE Enemy CLASS WITH THIS ----
var Enemy = Container.expand(function (spawnX, spawnY, value, customPathData) {
var self = Container.call(this);
// Added customPathData
// Animation Properties (as they were)
self.walkFrames = ['enemyPaper', 'enemyPaper_walk_1', 'enemyPaper_walk_2'];
self.currentAnimFrame = 0;
self.animationSpeed = 15;
self.animationCounter = 0;
// Path Properties (NEW)
if (customPathData && customPathData.length > 0) {
self.myPathPoints = customPathData;
} else {
// Fallback to global pathPoints if no custom path is provided
self.myPathPoints = pathPoints;
}
self.currentPathIndex = 0; // Initialize for its own path
// Other Enemy Properties (as they were)
self.health = 6; // Your updated health
self.speed = 2;
self.value = value || 10; // Use passed value or default
// Initial Graphic Setup (using first frame of its own path for spawn)
// The spawnX and spawnY passed to constructor should ideally be myPathPoints[0].x and .y
// If not, self.x and self.y will be set below.
self.graphic = self.attachAsset(self.walkFrames[0], {
anchorX: 0.5,
anchorY: 0.5
});
// Set initial position based on constructor arguments or first path point
if (spawnX !== undefined && spawnY !== undefined) {
self.x = spawnX;
self.y = spawnY;
} else if (self.myPathPoints && self.myPathPoints.length > 0) {
self.x = self.myPathPoints[0].x;
self.y = self.myPathPoints[0].y;
}
self.update = function () {
// --- Path Following Logic (Uses self.myPathPoints) ---
if (!self.myPathPoints || self.myPathPoints.length === 0) {
// Check its own path
return;
}
if (self.currentPathIndex < self.myPathPoints.length) {
// Check its own path
var target = self.myPathPoints[self.currentPathIndex]; // Get target from its own path
if (!target) {
self.currentPathIndex++;
return;
}
var dx = target.x - self.x;
var dy = target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed * 1.5) {
self.currentPathIndex++;
if (self.currentPathIndex >= self.myPathPoints.length) {
// Reached end of its path
playerLives--;
livesText.setText("Lives: " + playerLives);
LK.getSound('enemyReachGoal').play();
if (playerLives <= 0) {
if (LK.getScore() > storage.highScore) {
storage.highScore = LK.getScore();
}
LK.showGameOver();
}
self.destroy();
return;
}
} else {
dx = dx / distance * self.speed * gameSpeedMultiplier;
dy = dy / distance * self.speed * gameSpeedMultiplier;
self.x += dx;
self.y += dy;
}
} else {
// Already past the end of its path, should have been destroyed
if (self.parent) {
self.destroy();
} // Destroy if still somehow around
return;
}
// --- Animation Logic (as it was, using remove/re-attach) ---
if (self.walkFrames && self.walkFrames.length > 1) {
self.animationCounter += gameSpeedMultiplier;
if (self.animationCounter >= self.animationSpeed) {
self.animationCounter = 0;
self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length;
var newAsset = self.walkFrames[self.currentAnimFrame];
if (self.graphic && self.graphic.parent) {
var currentLocalX = self.graphic.x; // Should be 0 if graphic is root of self
var currentLocalY = self.graphic.y; // Should be 0
self.removeChild(self.graphic);
self.graphic = self.attachAsset(newAsset, {
anchorX: 0.5,
anchorY: 0.5,
x: currentLocalX,
y: currentLocalY
});
} else if (!self.graphic && self.parent) {
self.graphic = self.attachAsset(newAsset, {
anchorX: 0.5,
anchorY: 0.5
});
}
}
}
};
self.takeDamage = function (amount) {
self.health -= amount;
LK.effects.flashObject(self, 0xff0000, 200);
if (self.health <= 0 && self.parent) {
var numCoins = Math.ceil(self.value / 5);
if (numCoins < 1) {
numCoins = 1;
}
for (var i = 0; i < numCoins; i++) {
var coinValue = Math.floor(self.value / numCoins);
if (i === 0 && self.value % numCoins !== 0) {
coinValue += self.value % numCoins;
} // Distribute remainder
var coin = new DogeCoin(self.x, self.y, coinValue);
if (coinLayer) {
// Use global coinLayer
coinLayer.addChild(coin);
} else {
// Fallback, though coinLayer should be defined
game.addChild(coin);
}
}
LK.setScore(LK.getScore() + self.value);
scoreText.setText("Score: " + LK.getScore());
LK.getSound('enemyDeath').play();
self.destroy();
}
};
// Standard destroy method (if not inheriting a more complex one from Container)
var originalDestroy = self.destroy; // Capture prototype destroy if it exists
self.destroy = function () {
if (typeof originalDestroy === 'function') {
originalDestroy.call(self);
} else if (self.parent) {
self.parent.removeChild(self);
}
};
return self;
});
// ---- END: REPLACE Enemy CLASS WITH THIS ----
// GameLevel class remains the same
var GameLevel = Container.expand(function () {
var self = Container.call(this);
// Initialize pathGraphics as an instance property
self.pathGraphics = [];
// buildSpotGraphics can remain local to createBuildSpots or also be an instance property if needed elsewhere
// var buildSpotGraphics = []; // If you want to keep it as an instance property for build spots:
self.buildSpotGraphics = [];
self.createPath = function (pathData) {
// DO NOT clear pathGraphics here, allow accumulation if called multiple times
// pathGraphics.forEach(function (tile) { tile.destroy(); }); // REMOVED
// pathGraphics = []; // REMOVED
if (!pathData || pathData.length === 0) {
console.error("createPath called with no pathData");
return;
}
for (var i = 0; i < pathData.length - 1; i++) {
var start = pathData[i];
var end = pathData[i + 1];
if (!start || !end) {
continue;
}
var dx = end.x - start.x;
var dy = end.y - start.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var steps = Math.ceil(distance / 90);
for (var j = 0; j < steps; j++) {
var ratio = j / steps;
var x = start.x + dx * ratio;
var y = start.y + dy * ratio;
var tile = LK.getAsset('pathTile', {
x: x,
y: y,
alpha: 0.3,
anchorX: 0.5,
anchorY: 0.5
});
self.addChild(tile); // Add tile to the GameLevel container
self.pathGraphics.push(tile); // Store reference in instance array
}
}
};
self.createBuildSpots = function (spotsData) {
// Clear previous build spots graphics for THIS INSTANCE of GameLevel
self.buildSpotGraphics.forEach(function (spotGraphic) {
// Assuming buildSpotGraphics are direct children
if (spotGraphic.parent) {
// Ensure it has a parent before trying to remove
spotGraphic.destroy(); // Destroy the BuildSpot instance
}
});
self.buildSpotGraphics = []; // Reset the array for graphics
// Also clear the logical buildSpots array on the level if it exists
// This assumes self.buildSpots is where the actual BuildSpot objects are stored
if (self.buildSpots) {
self.buildSpots.forEach(function (spotObject) {
if (spotObject && typeof spotObject.destroy === 'function') {
// spotObject.destroy(); // This was already handled by destroying the graphic if spot is the graphic
}
});
}
self.buildSpots = []; // Reset logical array
if (!spotsData || spotsData.length === 0) {
console.error("createBuildSpots called with no spotsData");
return;
}
for (var i = 0; i < spotsData.length; i++) {
if (!spotsData[i]) {
continue;
}
var spot = new BuildSpot(); // Create new BuildSpot instance
spot.x = spotsData[i].x;
spot.y = spotsData[i].y;
self.addChild(spot); // Add BuildSpot instance to GameLevel container
self.buildSpots.push(spot); // Store logical spot instance
self.buildSpotGraphics.push(spot.graphic); // Store reference to its graphic if needed for other reasons
// Or just spot, if spot itself is the main thing to track
}
};
// Add a destroy method to GameLevel to clean up its children
self.destroy = function () {
// Destroy all children (path tiles, build spots that are children of this level)
while (self.children.length > 0) {
var child = self.children[0];
if (typeof child.destroy === 'function') {
child.destroy(); // Prefer calling child's own destroy method
} else if (child.parent) {
child.parent.removeChild(child); // Fallback
} else {
break; // Should not happen if parentage is correct
}
}
self.pathGraphics = [];
self.buildSpotGraphics = [];
self.buildSpots = [];
if (self.parent) {
self.parent.removeChild(self);
}
};
return self;
});
// ---- END: REPLACE GameLevel CLASS WITH THIS ----
// Goal class remains the same
var Goal = Container.expand(function () {
var self = Container.call(this);
var graphic = self.attachAsset('goal', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.6
});
return self;
});
// ---- START: REPLACE InternOnCoffeeRun CLASS WITH THIS ----
var InternOnCoffeeRun = Container.expand(function (spawnX, spawnY, valueToIgnore, customPathData) {
var self = Container.call(this);
// Animation Properties
self.walkFrames = ['enemyIntern', 'enemyIntern_walk_1', 'enemyIntern_walk_2'];
self.currentAnimFrame = 0;
self.animationSpeed = 5;
self.animationCounter = 0;
// Path Properties
if (customPathData && customPathData.length > 0) {
self.myPathPoints = customPathData;
} else {
self.myPathPoints = pathPoints;
}
self.currentPathIndex = 0;
// Specific stats for Intern
self.health = 4;
self.speed = 4.0;
self.value = 5;
self.isFlying = false;
// Initial Graphic & Position
self.graphic = self.attachAsset(self.walkFrames[0], {
anchorX: 0.5,
anchorY: 0.5
});
if (self.myPathPoints && self.myPathPoints.length > 0) {
self.x = self.myPathPoints[0].x;
self.y = self.myPathPoints[0].y;
} else if (spawnX !== undefined && spawnY !== undefined) {
self.x = spawnX;
self.y = spawnY;
}
self.update = function () {
// Path Following Logic
if (!self.myPathPoints || self.myPathPoints.length === 0) {
return;
}
if (self.currentPathIndex < self.myPathPoints.length) {
var target = self.myPathPoints[self.currentPathIndex];
if (!target) {
self.currentPathIndex++;
return;
}
var dx = target.x - self.x;
var dy = target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed * 1.5) {
self.currentPathIndex++;
if (self.currentPathIndex >= self.myPathPoints.length) {
playerLives--;
livesText.setText("Lives: " + playerLives);
LK.getSound('enemyReachGoal').play();
if (playerLives <= 0) {
if (LK.getScore() > storage.highScore) {
storage.highScore = LK.getScore();
}
LK.showGameOver();
}
self.destroy();
return;
}
} else {
dx = dx / distance * self.speed * gameSpeedMultiplier;
dy = dy / distance * self.speed * gameSpeedMultiplier;
self.x += dx;
self.y += dy;
}
} else {
if (self.parent) {
self.destroy();
}
return;
}
// Animation Logic
if (self.walkFrames && self.walkFrames.length > 1) {
self.animationCounter += gameSpeedMultiplier;
if (self.animationCounter >= self.animationSpeed) {
self.animationCounter = 0;
self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length;
var newAsset = self.walkFrames[self.currentAnimFrame];
if (self.graphic && self.graphic.parent) {
var currentLocalX = self.graphic.x;
var currentLocalY = self.graphic.y;
self.removeChild(self.graphic);
self.graphic = self.attachAsset(newAsset, {
anchorX: 0.5,
anchorY: 0.5,
x: currentLocalX,
y: currentLocalY
});
} else if (!self.graphic && self.parent) {
self.graphic = self.attachAsset(newAsset, {
anchorX: 0.5,
anchorY: 0.5
});
}
}
}
};
self.takeDamage = function (amount) {
self.health -= amount;
LK.effects.flashObject(self, 0xff0000, 200);
if (self.health <= 0 && self.parent) {
var numCoins = Math.ceil(self.value / 5);
if (numCoins < 1) {
numCoins = 1;
}
for (var i = 0; i < numCoins; i++) {
var coinValue = Math.floor(self.value / numCoins);
if (i === 0 && self.value % numCoins !== 0) {
coinValue += self.value % numCoins;
}
var coin = new DogeCoin(self.x, self.y, coinValue);
if (coinLayer) {
coinLayer.addChild(coin);
} else {
game.addChild(coin);
}
}
LK.setScore(LK.getScore() + self.value);
scoreText.setText("Score: " + LK.getScore());
LK.getSound('enemyDeath').play();
self.destroy();
}
};
var originalDestroy = self.destroy;
self.destroy = function () {
if (typeof originalDestroy === 'function') {
originalDestroy.call(self);
} else if (self.parent) {
self.parent.removeChild(self);
}
};
return self;
});
// ---- END: REPLACE InternOnCoffeeRun CLASS WITH THIS ----
var LaserCatPerch = Container.expand(function () {
var self = Container.call(this);
// Initialize the tower with level 0 stats and graphics
initializeTowerFromData(self, 'laserCat', 0);
self.activeBeams = []; // Array to store info about current beams {target, beamGraphic, durationTimer, currentDotDamage}
self.cooldownTimer = 0; // Time until it can look for new targets after all beams finish or max targets reached
self.update = function () {
var buildSpot = self.parent;
if (!buildSpot || !buildSpot.parent) {
return;
}
// Get tower position in game space
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
var towerPosInGameSpace = game.toLocal(towerGraphicGlobalCenter);
// Decrement cooldown timer
if (self.cooldownTimer > 0) {
self.cooldownTimer -= 1000 / 60 * gameSpeedMultiplier; // Convert from ms to frames
}
// Handle Active Beams
for (var i = self.activeBeams.length - 1; i >= 0; i--) {
var beamData = self.activeBeams[i];
// Check if target is still valid and in range
if (!beamData.target || !beamData.target.parent) {
// Target no longer exists
beamData.beamGraphic.destroy();
self.activeBeams.splice(i, 1);
continue;
}
// Check if target is out of range
var dx = beamData.target.x - towerPosInGameSpace.x;
var dy = beamData.target.y - towerPosInGameSpace.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > self.range) {
// Target out of range
beamData.beamGraphic.destroy();
self.activeBeams.splice(i, 1);
continue;
}
// Decrement beam duration timer
beamData.durationTimer -= 1000 / 60 * gameSpeedMultiplier;
// Apply DOT damage at intervals
beamData.dotTickTimer -= 1000 / 60 * gameSpeedMultiplier;
if (beamData.dotTickTimer <= 0) {
// Apply damage
beamData.target.takeDamage(beamData.currentDotDamage);
// Ramp up damage for next tick
beamData.currentDotDamage *= self.dotRampUpFactor;
// Reset tick timer (damage every 0.5 seconds)
beamData.dotTickTimer = 500;
}
// Update beam visual
// Position beam at tower position
beamData.beamGraphic.x = towerPosInGameSpace.x;
beamData.beamGraphic.y = towerPosInGameSpace.y;
// Calculate direction and distance
var targetPosInGameSpace = {
x: beamData.target.x,
y: beamData.target.y
};
var dx = targetPosInGameSpace.x - towerPosInGameSpace.x;
var dy = targetPosInGameSpace.y - towerPosInGameSpace.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Adjust beam rotation and height
beamData.beamGraphic.rotation = Math.atan2(dy, dx) - Math.PI / 2; // Adjust rotation
beamData.beamGraphic.height = distance; // Stretch beam to reach target
// Make sure the beam is in the game container (not a child of the tower)
if (!beamData.beamGraphic.parent) {
game.addChild(beamData.beamGraphic);
} else if (beamData.beamGraphic.parent !== game) {
if (beamData.beamGraphic.parent) {
beamData.beamGraphic.parent.removeChild(beamData.beamGraphic);
}
game.addChild(beamData.beamGraphic);
}
// If beam duration is up, destroy it
if (beamData.durationTimer <= 0) {
beamData.beamGraphic.destroy();
self.activeBeams.splice(i, 1);
}
}
// Find New Targets if not on cooldown and not at max beams
if (self.cooldownTimer <= 0 && self.activeBeams.length < self.numberOfBeams) {
// Find potential targets in range that aren't already targeted
var potentialTargets = [];
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy || !enemy.parent) {
continue;
}
// Check if enemy is in range
var dx = enemy.x - towerPosInGameSpace.x;
var dy = enemy.y - towerPosInGameSpace.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq <= self.range * self.range) {
// Check if enemy is already targeted by an active beam
var alreadyTargeted = false;
for (var j = 0; j < self.activeBeams.length; j++) {
if (self.activeBeams[j].target === enemy) {
alreadyTargeted = true;
break;
}
}
if (!alreadyTargeted) {
potentialTargets.push(enemy);
}
}
}
// If targets available, target the strongest (highest health)
if (potentialTargets.length > 0) {
// Sort by health (highest first)
potentialTargets.sort(function (a, b) {
return b.health - a.health;
});
// Target the strongest enemy
var targetEnemy = potentialTargets[0];
// Create beam graphic
var beamGfx = LK.getAsset('laserBeam', {
anchorX: 0.5,
anchorY: 0 // Anchor at top center
});
// Position at tower
beamGfx.x = towerPosInGameSpace.x;
beamGfx.y = towerPosInGameSpace.y;
// Calculate direction and distance to target
var dx = targetEnemy.x - towerPosInGameSpace.x;
var dy = targetEnemy.y - towerPosInGameSpace.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Set rotation and height (stretch beam to reach target)
beamGfx.rotation = Math.atan2(dy, dx) - Math.PI / 2;
beamGfx.height = distance;
// Add to game (below enemy layer if possible, or directly to game)
game.addChild(beamGfx);
// Apply initial damage
targetEnemy.takeDamage(self.initialBeamDamage);
// Play firing sound
// LK.getSound('sfxLaserCatFireInitialPew').play();
// Start beam loop sound
// var beamLoopSound = LK.getSound('sfxLaserCatFireBeamLoop').play();
// Add to active beams
self.activeBeams.push({
target: targetEnemy,
beamGraphic: beamGfx,
durationTimer: self.maxBeamDuration,
currentDotDamage: self.dotDamagePerSecond,
dotTickTimer: 500 // First tick after 0.5 seconds
});
// If at max beams, start cooldown
if (self.activeBeams.length >= self.numberOfBeams) {
self.cooldownTimer = self.cooldownBetweenBeams;
}
}
}
// Targeting hum sound logic
if (self.activeBeams.length > 0) {
// Play targeting hum sound if not already playing
// if (!humSoundPlaying) {
// humSoundPlaying = true;
// LK.getSound('sfxLaserCatTargetHum').play();
// }
} else {
// Stop hum sound if playing
// if (humSoundPlaying) {
// humSoundPlaying = false;
// LK.getSound('sfxLaserCatTargetHum').stop();
// }
}
};
// Override destroy method to clean up beams
var originalDestroy = self.destroy;
self.destroy = function () {
// Clean up all active beams
for (var i = 0; i < self.activeBeams.length; i++) {
if (self.activeBeams[i].beamGraphic) {
self.activeBeams[i].beamGraphic.destroy();
}
}
// Stop any looping sounds
// if (humSoundPlaying) {
// LK.getSound('sfxLaserCatTargetHum').stop();
// }
// Call original destroy
if (originalDestroy) {
originalDestroy.call(self);
} else if (self.parent) {
self.parent.removeChild(self);
}
};
self.shoot = function (target) {
// This method is kept for compatibility but not used actively
// The beam firing logic is handled in update()
};
return self;
});
// Range Indicator to visualize tower attack ranges
var RangeIndicator = Container.expand(function (centerX, centerY, radius, parentContainer) {
var self = Container.call(this);
// Create container for the indicator
self.x = centerX;
self.y = centerY;
// Since we can't draw circles directly in LK, use a pre-defined circle asset
// and scale it to match our desired radius
var rangeCircle = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.05,
// Very faint fill
tint: 0xFFFFFF // White color
});
// The centerCircle asset is 100px in diameter, so scale accordingly
var scaleRatio = radius * 2 / 100;
rangeCircle.scaleX = scaleRatio;
rangeCircle.scaleY = scaleRatio;
// Add a slightly larger circle with higher alpha for the border effect
var rangeBorder = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3,
// More visible than the fill
tint: 0xFFFFFF // White color
});
// Make border slightly larger
var borderScaleRatio = scaleRatio * 1.02;
rangeBorder.scaleX = borderScaleRatio;
rangeBorder.scaleY = borderScaleRatio;
// Add to parent container
if (parentContainer) {
parentContainer.addChild(self);
}
return self;
});
// ---- START: REPLACE RedTapeWorm CLASS WITH THIS ----
var RedTapeWorm = Container.expand(function (spawnX, spawnY, valueToIgnore, customPathData) {
var self = Container.call(this);
// Animation Properties
self.walkFrames = ['enemyRedTapeWorm', 'enemyRedTapeWorm_walk_1', 'enemyRedTapeWorm_walk_2'];
self.currentAnimFrame = 0;
self.animationSpeed = 10;
self.animationCounter = 0;
// Path Properties
if (customPathData && customPathData.length > 0) {
self.myPathPoints = customPathData;
} else {
self.myPathPoints = pathPoints;
}
self.currentPathIndex = 0;
// Specific stats for RedTapeWorm
self.health = 30;
self.speed = 0.75;
self.value = 25;
self.isFlying = false;
// Initial Graphic & Position
self.graphic = self.attachAsset(self.walkFrames[0], {
anchorX: 0.5,
anchorY: 0.5
});
if (self.myPathPoints && self.myPathPoints.length > 0) {
self.x = self.myPathPoints[0].x;
self.y = self.myPathPoints[0].y;
} else if (spawnX !== undefined && spawnY !== undefined) {
self.x = spawnX;
self.y = spawnY;
}
self.update = function () {
// Path Following Logic (Uses self.myPathPoints)
if (!self.myPathPoints || self.myPathPoints.length === 0) {
return;
}
if (self.currentPathIndex < self.myPathPoints.length) {
var target = self.myPathPoints[self.currentPathIndex];
if (!target) {
self.currentPathIndex++;
return;
}
var dx = target.x - self.x;
var dy = target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed * 1.5) {
self.currentPathIndex++;
if (self.currentPathIndex >= self.myPathPoints.length) {
playerLives--;
livesText.setText("Lives: " + playerLives);
LK.getSound('enemyReachGoal').play();
if (playerLives <= 0) {
if (LK.getScore() > storage.highScore) {
storage.highScore = LK.getScore();
}
LK.showGameOver();
}
self.destroy();
return;
}
} else {
dx = dx / distance * self.speed * gameSpeedMultiplier;
dy = dy / distance * self.speed * gameSpeedMultiplier;
self.x += dx;
self.y += dy;
}
} else {
if (self.parent) {
self.destroy();
}
return;
}
// Animation Logic
if (self.walkFrames && self.walkFrames.length > 1) {
self.animationCounter += gameSpeedMultiplier;
if (self.animationCounter >= self.animationSpeed) {
self.animationCounter = 0;
self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length;
var newAsset = self.walkFrames[self.currentAnimFrame];
if (self.graphic && self.graphic.parent) {
var currentLocalX = self.graphic.x;
var currentLocalY = self.graphic.y;
self.removeChild(self.graphic);
self.graphic = self.attachAsset(newAsset, {
anchorX: 0.5,
anchorY: 0.5,
x: currentLocalX,
y: currentLocalY
});
} else if (!self.graphic && self.parent) {
self.graphic = self.attachAsset(newAsset, {
anchorX: 0.5,
anchorY: 0.5
});
}
}
}
};
self.takeDamage = function (amount) {
self.health -= amount;
LK.effects.flashObject(self, 0xff0000, 200);
if (self.health <= 0 && self.parent) {
var numCoins = Math.ceil(self.value / 5);
if (numCoins < 1) {
numCoins = 1;
}
for (var i = 0; i < numCoins; i++) {
var coinValue = Math.floor(self.value / numCoins);
if (i === 0 && self.value % numCoins !== 0) {
coinValue += self.value % numCoins;
}
var coin = new DogeCoin(self.x, self.y, coinValue);
if (coinLayer) {
coinLayer.addChild(coin);
} else {
game.addChild(coin);
}
}
LK.setScore(LK.getScore() + self.value);
scoreText.setText("Score: " + LK.getScore());
LK.getSound('enemyDeath').play();
self.destroy();
}
};
var originalDestroy = self.destroy;
self.destroy = function () {
// Add any RedTapeWorm-specific cleanup here if needed in future
if (typeof originalDestroy === 'function') {
originalDestroy.call(self);
} else if (self.parent) {
self.parent.removeChild(self);
}
};
return self;
});
// ---- END: REPLACE RedTapeWorm CLASS WITH THIS ----
var RestructuringSpecialist = Container.expand(function () {
var self = Container.call(this);
// Initialize the tower with level 0 stats and graphics
initializeTowerFromData(self, 'restruct', 0);
// Charge properties
self.currentCharge = 0;
self.chargeUpTime = 360; // Frames to fully charge
self.currentTarget = null;
self.isFiring = false;
self.chargeVisuals = [];
self.chargeSoundInstance = null;
self.chargeSfx = 'sfxRestructCharge';
self.update = function () {
// If tower is in the firing animation, don't do anything else
if (self.isFiring) {
return;
}
// Get tower position in game space
var buildSpot = self.parent;
if (!buildSpot || !buildSpot.parent) {
return;
}
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
var towerPosInGameSpace = game.toLocal(towerGraphicGlobalCenter);
// Check if current target exists and is still valid
if (self.currentTarget && self.currentTarget.parent) {
// Check if target is still in range
var targetPosInGameSpace = {
x: self.currentTarget.x,
y: self.currentTarget.y
};
var dx = targetPosInGameSpace.x - towerPosInGameSpace.x;
var dy = targetPosInGameSpace.y - towerPosInGameSpace.y;
var distanceSq = dx * dx + dy * dy;
// If target moved out of range, reset charging
if (distanceSq > self.range * self.range) {
self.currentTarget = null;
self.currentCharge = 0;
self.clearChargeVisuals();
self.stopChargeSound();
return;
}
// Continue charging up
self.currentCharge += gameSpeedMultiplier;
// Update the charge visuals
self.updateChargeVisuals(towerPosInGameSpace);
// Update charge sound
if (!self.chargeSoundInstance) {
self.chargeSoundInstance = LK.getSound(self.chargeSfx || 'sfxRestructCharge').play({
loop: true,
volume: 0.3
});
} else {
// Optionally increase volume/pitch as charge builds up
var chargeProgress = Math.min(1.0, self.currentCharge / self.chargeUpTime);
if (self.chargeSoundInstance.volume) {
self.chargeSoundInstance.volume = 0.3 + 0.7 * chargeProgress;
}
}
// If fully charged, restructure the enemy
if (self.currentCharge >= self.chargeUpTime) {
self.fireRestructure();
}
} else {
// No valid target or target was lost, reset charge
self.currentCharge = 0;
self.clearChargeVisuals();
self.stopChargeSound();
// Find new target based on priority
var newTarget = null;
var maxHealth = 0;
// Iterate through enemies to find valid targets in range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy || !enemy.parent) {
continue;
}
var dx = enemy.x - towerPosInGameSpace.x;
var dy = enemy.y - towerPosInGameSpace.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq <= self.range * self.range) {
// If targeting strongest enemies, track the one with highest health
if (self.targetPriority === 'strongest') {
if (enemy.health > maxHealth) {
maxHealth = enemy.health;
newTarget = enemy;
}
} else {
// Default to first enemy in range if no specific priority
newTarget = enemy;
break;
}
}
}
// If a new target was found, set it
if (newTarget) {
self.currentTarget = newTarget;
self.currentCharge = 0; // Start charging from zero
}
}
};
self.updateChargeVisuals = function (towerPos) {
// Initialize charge visuals array if needed
if (!self.chargeVisuals) {
self.chargeVisuals = [];
}
// Calculate how many visuals to show based on charge progress
var numVisuals = Math.floor(self.currentCharge / self.chargeUpTime * 5); // Max 5 visuals
// Add new visuals if needed
while (self.chargeVisuals.length < numVisuals) {
// Randomly choose between two visual types
var visualAsset = Math.random() < 0.5 ? 'flowchartSegment1' : 'flowchartSegment2';
var v = self.attachAsset(visualAsset, {
anchorX: 0.5,
anchorY: 0.5
});
self.chargeVisuals.push(v);
}
// Remove excess visuals if needed
while (self.chargeVisuals.length > numVisuals) {
var v = self.chargeVisuals.pop();
v.destroy();
}
// Update the position of each visual to orbit the tower
for (var i = 0; i < self.chargeVisuals.length; i++) {
var v = self.chargeVisuals[i];
// Calculate orbit position based on charge progress and visual index
v.x = Math.cos(self.currentCharge * 0.1 + i * Math.PI * 0.4) * 50;
v.y = Math.sin(self.currentCharge * 0.1 + i * Math.PI * 0.4) * 50;
// Optional: increase scale or alpha as charge progresses
var chargeProgress = self.currentCharge / self.chargeUpTime;
v.alpha = 0.5 + chargeProgress * 0.5;
v.scale.set(0.8 + chargeProgress * 0.4);
}
};
self.clearChargeVisuals = function () {
// Destroy all charge visuals
if (self.chargeVisuals) {
for (var i = 0; i < self.chargeVisuals.length; i++) {
self.chargeVisuals[i].destroy();
}
self.chargeVisuals = [];
}
};
self.stopChargeSound = function () {
// Stop the charging sound if it's playing
if (self.chargeSoundInstance) {
self.chargeSoundInstance.stop();
self.chargeSoundInstance = null;
}
};
self.fireRestructure = function () {
// Exit if no valid target or already firing
if (!self.currentTarget || !self.currentTarget.parent || self.isFiring) {
return;
}
self.isFiring = true;
self.stopChargeSound();
self.clearChargeVisuals();
// Play launch SFX
LK.getSound(self.launchSfx || 'sfxRestructLaunchDoge').play();
// Store reference to target before potentially nulling
var enemyToRestructure = self.currentTarget;
var targetPosInGameSpace = {
x: enemyToRestructure.x,
y: enemyToRestructure.y
};
// Spawn restructure effect on target
var effect = game.attachAsset(self.effectAsset || 'restructureEffect', {
anchorX: 0.5,
anchorY: 0.5,
x: targetPosInGameSpace.x,
y: targetPosInGameSpace.y
});
// Play impact sound
LK.getSound(self.impactSfx || 'sfxRestructImpact').play();
// Apply damage or kill
if (enemyToRestructure.health <= self.instantKillThreshold) {
// Instant kill if below threshold
spawnFloatingText("OPTIMIZED!", enemyToRestructure.x, enemyToRestructure.y, {
fill: 0x00FFFF
});
enemyToRestructure.destroy();
} else {
// Otherwise apply damage
enemyToRestructure.takeDamage(self.damage);
}
// Golden Parachute logic (Level 3+)
if (self.currentLevel >= 2 && self.goldenParachuteDamage > 0 && (!enemyToRestructure.parent || enemyToRestructure.health <= 0)) {
// If enemy was killed, apply splash damage
spawnFloatingText("Severance!", targetPosInGameSpace.x, targetPosInGameSpace.y - 30, {
fill: 0xFFD700
});
for (var i = 0; i < enemies.length; i++) {
var nearbyEnemy = enemies[i];
if (nearbyEnemy === enemyToRestructure || !nearbyEnemy.parent) {
continue;
}
var dx = nearbyEnemy.x - targetPosInGameSpace.x;
var dy = nearbyEnemy.y - targetPosInGameSpace.y;
if (dx * dx + dy * dy < self.goldenParachuteRadius * self.goldenParachuteRadius) {
nearbyEnemy.takeDamage(self.goldenParachuteDamage);
}
}
}
// Reset state and clean up
self.currentCharge = 0;
self.currentTarget = null;
// Clean up effect after animation
LK.setTimeout(function () {
if (effect && effect.parent) {
effect.destroy();
}
// Clear firing flag after brief delay
LK.setTimeout(function () {
self.isFiring = false;
}, 200 / gameSpeedMultiplier);
}, 500);
};
self.findTarget = function () {
// This method is preserved for backward compatibility
// The targeting logic is now handled in update()
};
self.restructureEnemy = function () {
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.isCharging = false;
self.chargeLevel = 0;
return;
}
// Create visual effect
var effect = self.attachAsset('restructureEffect', {
anchorX: 0.5,
anchorY: 0.5
});
// Destroy effect after animation
LK.setTimeout(function () {
if (effect && effect.parent) {
effect.parent.removeChild(effect);
}
}, 500);
// Give player currency and score for the transformation
currency += self.targetEnemy.value;
currencyText.setText("$: " + currency);
LK.setScore(LK.getScore() + self.targetEnemy.value);
scoreText.setText("Score: " + LK.getScore());
// Play effect sound
LK.getSound('sfxRestructTransformKill').play();
// Remove the enemy
self.targetEnemy.destroy();
// Reset tower state
self.isCharging = false;
self.chargeLevel = 0;
self.targetEnemy = null;
};
return self;
});
var RickAstleyProjectile = Container.expand(function (startX, startY, endX, endY, towerOwner) {
var self = Container.call(this);
self.graphic = self.attachAsset(towerOwner.popupAsset, {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.7,
scaleX: 0.5,
scaleY: 0.5
});
self.x = startX;
self.y = startY;
self.targetX = endX;
self.targetY = endY;
self.speed = towerOwner.projectileSpeed || 7; // Use tower's speed or default to 7
self.towerOwner = towerOwner;
self.hasHitTarget = false;
self.updateProjectile = function () {
if (self.hasHitTarget) {
return;
}
// Calculate direction and distance
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if reached target
if (distance < self.speed * gameSpeedMultiplier) {
self.hasHitTarget = true;
self.towerOwner.activateEffectAtLocation(self.targetX, self.targetY);
self.destroy();
return;
}
// Move towards target
dx = dx / distance * self.speed * gameSpeedMultiplier;
dy = dy / distance * self.speed * gameSpeedMultiplier;
self.x += dx;
self.y += dy;
};
return self;
});
var RickrollerTower = Container.expand(function () {
var self = Container.call(this);
// Initialize the tower with level 0 stats and graphics
initializeTowerFromData(self, 'rickroller', 0);
self.currentCooldown = 0; // Time in ms until it can fire again
self.activeProjectiles = []; // To track flying Rick Astleys
self.activePopups = []; // To keep track of all active Rick popups for dancing
self.update = function () {
// Cooldown Management
if (self.currentCooldown > 0) {
self.currentCooldown -= 1000 / 60 * gameSpeedMultiplier;
} else {
self.findTargetAndFire();
}
// Update Active Projectiles
for (var i = self.activeProjectiles.length - 1; i >= 0; i--) {
var rickProjectile = self.activeProjectiles[i];
rickProjectile.updateProjectile();
if (rickProjectile.hasHitTarget || !rickProjectile.parent) {
self.activeProjectiles.splice(i, 1);
}
}
// Update Rick Astley Dance Animations
for (var i = self.activePopups.length - 1; i >= 0; i--) {
var p = self.activePopups[i];
if (!p || !p.parent) {
// If popup was destroyed elsewhere
self.activePopups.splice(i, 1);
continue;
}
if (p.isDancing) {
self.updateRickDance(p); // Call method to update dance animation
}
}
};
self.updateRickDance = function (popupGraphic) {
var pg = popupGraphic; // shorthand for readability
pg.danceTimer += 1000 / 60 * gameSpeedMultiplier; // Increment timer in ms
var TILT_DURATION = 300 / gameSpeedMultiplier; // Duration of one tilt/flip segment
var FLIP_POINT_IN_CYCLE = 2; // At which phase to trigger a flip
if (pg.danceTimer >= TILT_DURATION) {
pg.danceTimer = 0;
pg.dancePhase = (pg.dancePhase + 1) % 4; // Cycle through 4 phases
if (pg.dancePhase === 0) {
// Tilt Left
pg.rotation = -0.15; // Slight tilt in radians
} else if (pg.dancePhase === 1) {
// Tilt Right
pg.rotation = 0.15;
} else if (pg.dancePhase === 2) {
// Center rotation
pg.rotation = pg.originalRotation;
// Flip horizontally on this phase
pg.scale.x = -pg.originalScaleX;
} else if (pg.dancePhase === 3) {
// Center rotation and flip back
pg.rotation = pg.originalRotation;
pg.scale.x = pg.originalScaleX;
}
}
};
self.findTargetAndFire = function () {
var buildSpot = self.parent;
if (!buildSpot || !buildSpot.parent) {
return;
}
// Get tower position in game space
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
var towerPosInGameSpace = game.toLocal(towerGraphicGlobalCenter);
// Find target
var closestEnemy = null;
var minDistanceSq = self.range * self.range;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy || !enemy.parent) {
continue;
}
var dx = enemy.x - towerPosInGameSpace.x;
var dy = enemy.y - towerPosInGameSpace.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < minDistanceSq) {
minDistanceSq = distanceSq;
closestEnemy = enemy;
}
}
if (closestEnemy) {
var targetX = closestEnemy.x;
var targetY = closestEnemy.y;
// Create Rick Astley projectile
var rick = new RickAstleyProjectile(towerPosInGameSpace.x, towerPosInGameSpace.y, targetX, targetY, self);
self.activeProjectiles.push(rick);
game.addChild(rick);
// Start cooldown
self.currentCooldown = self.cooldown;
}
};
self.activateEffectAtLocation = function (effectX, effectY) {
// Spawn Rick Astley Popup (Stationary at target)
var popup = game.attachAsset(self.popupAsset, {
anchorX: 0.5,
anchorY: 0.5,
x: effectX,
y: effectY
});
// Add dance properties to the popup
popup.isDancing = false;
popup.dancePhase = 0; // For cycling through dance moves
popup.danceTimer = 0; // Timer for current dance move
popup.originalScaleX = popup.scale.x; // Store original scale.x (usually 1)
popup.originalRotation = popup.rotation; // Store original rotation (usually 0)
// Add to active popups list
self.activePopups.push(popup);
// Fade in/out animation
popup.alpha = 0;
tween(popup,
// target
{
alpha: 1.0
},
// props
{
// options for fade-in
duration: 250 / gameSpeedMultiplier,
easing: tween.quadOut,
onFinish: function onFinish() {
// After fade-in completes
// Start dancing once fade-in is complete
popup.isDancing = true;
LK.setTimeout(function () {
// Hold duration
if (popup && popup.parent) {
// Stop dancing before fade-out
popup.isDancing = false;
// Reset appearance
popup.rotation = popup.originalRotation;
popup.scale.x = popup.originalScaleX;
tween(popup,
// target for fade-out
{
alpha: 0
},
// props
{
// options for fade-out
duration: 250 / gameSpeedMultiplier,
easing: tween.quadIn,
onFinish: function onFinish() {
if (popup.parent) {
// Remove from active popups list
var index = self.activePopups.indexOf(popup);
if (index !== -1) {
self.activePopups.splice(index, 1);
}
popup.destroy();
}
}
});
}
}, (self.popupDuration - 500) / gameSpeedMultiplier); // Time to hold at full alpha
}
});
// Play Sound
var soundToPlay = self.soundEffects[Math.floor(Math.random() * self.soundEffects.length)];
LK.getSound(soundToPlay).play();
// Find and Stun/Damage Enemies in Effect Radius
var enemiesAffectedThisTrigger = [];
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy || !enemy.parent) {
continue;
}
var dx = enemy.x - effectX;
var dy = enemy.y - effectY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.effectRadius) {
if (!enemy.isStunned) {
enemy.originalSpeed = enemy.speed;
enemy.speed = 0;
enemy.isStunned = true;
enemy.isStunnedByRickroll = self; // So this tower can unstun
enemy.graphic.tint = 0xFFD700; // Gold tint
enemiesAffectedThisTrigger.push(enemy);
// Start DOT on this enemy
self.startDotOnEnemy(enemy);
}
}
}
// Unstun Timer
LK.setTimeout(function () {
for (var i = 0; i < enemiesAffectedThisTrigger.length; i++) {
var affectedEnemy = enemiesAffectedThisTrigger[i];
if (affectedEnemy && affectedEnemy.parent && affectedEnemy.isStunnedByRickroll === self) {
affectedEnemy.isStunned = false;
affectedEnemy.speed = affectedEnemy.originalSpeed;
affectedEnemy.isStunnedByRickroll = null;
affectedEnemy.graphic.tint = 0xFFFFFF;
}
}
}, self.stunDuration / gameSpeedMultiplier);
};
self.startDotOnEnemy = function (enemyInstance) {
var dotTicksRemaining = Math.floor(self.stunDuration / self.dotTickRate);
function applyTick() {
if (!enemyInstance || !enemyInstance.parent || enemyInstance.isStunnedByRickroll !== self || dotTicksRemaining <= 0) {
return;
}
enemyInstance.takeDamage(self.damagePerTick);
dotTicksRemaining--;
if (dotTicksRemaining > 0) {
LK.setTimeout(applyTick, self.dotTickRate / gameSpeedMultiplier);
}
}
applyTick(); // Start the first tick
};
return self;
});
var SpamEmailUnit = Container.expand(function (spawnX, spawnY, valueToIgnore, customPathData) {
var self = Container.call(this);
// Animation Properties
self.walkFrames = ['enemySpamEmail', 'enemySpamEmail_walk_1', 'enemySpamEmail_walk_2'];
self.currentAnimFrame = 0;
self.animationSpeed = 6;
self.animationCounter = 0;
// Path Properties
if (customPathData && customPathData.length > 0) {
self.myPathPoints = customPathData;
} else {
self.myPathPoints = pathPoints;
}
self.currentPathIndex = 0; // For flying, this might be handled differently if not path following
// Specific stats for SpamEmailUnit
self.health = 2;
self.speed = 2.5;
self.value = 1;
self.isFlying = true; // Key property
// Initial Graphic & Position
self.graphic = self.attachAsset(self.walkFrames[0], {
anchorX: 0.5,
anchorY: 0.5
});
if (self.myPathPoints && self.myPathPoints.length > 0) {
self.x = self.myPathPoints[0].x;
self.y = self.myPathPoints[0].y;
} else if (spawnX !== undefined && spawnY !== undefined) {
self.x = spawnX;
self.y = spawnY;
}
self.update = function () {
// Flying units go directly to the goal (last point in THEIR ASSIGNED path)
if (!self.myPathPoints || self.myPathPoints.length === 0) {
return;
}
var target;
if (self.isFlying) {
target = self.myPathPoints[self.myPathPoints.length - 1]; // Target is the end of its path
} else {
// Fallback to normal path following if not flying (though this class is isFlying=true)
if (self.currentPathIndex >= self.myPathPoints.length) {
if (self.parent) {
self.destroy();
}
return;
}
target = self.myPathPoints[self.currentPathIndex];
}
if (!target) {
self.destroy();
return;
}
var dx = target.x - self.x;
var dy = target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed * 1.5 * gameSpeedMultiplier) {
// Check if reached goal (for flying) or next point
if (self.isFlying || self.currentPathIndex >= self.myPathPoints.length - 1) {
// Reached final goal
playerLives--;
livesText.setText("Lives: " + playerLives);
LK.getSound('enemyReachGoal').play();
if (playerLives <= 0) {
if (LK.getScore() > storage.highScore) {
storage.highScore = LK.getScore();
}
LK.showGameOver();
}
self.destroy();
return;
} else {
// For non-flying, move to next path index
self.currentPathIndex++;
}
} else {
dx = dx / distance * self.speed * gameSpeedMultiplier;
dy = dy / distance * self.speed * gameSpeedMultiplier;
self.x += dx;
self.y += dy;
}
// Animation Logic
if (self.walkFrames && self.walkFrames.length > 1) {
self.animationCounter += gameSpeedMultiplier;
if (self.animationCounter >= self.animationSpeed) {
self.animationCounter = 0;
self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length;
var newAsset = self.walkFrames[self.currentAnimFrame];
if (self.graphic && self.graphic.parent) {
var currentLocalX = self.graphic.x;
var currentLocalY = self.graphic.y;
self.removeChild(self.graphic);
self.graphic = self.attachAsset(newAsset, {
anchorX: 0.5,
anchorY: 0.5,
x: currentLocalX,
y: currentLocalY
});
} else if (!self.graphic && self.parent) {
self.graphic = self.attachAsset(newAsset, {
anchorX: 0.5,
anchorY: 0.5
});
}
}
}
};
self.takeDamage = function (amount) {
self.health -= amount;
LK.effects.flashObject(self, 0xff0000, 200);
if (self.health <= 0 && self.parent) {
var numCoins = Math.ceil(self.value / 5);
if (numCoins < 1) {
numCoins = 1;
}
for (var i = 0; i < numCoins; i++) {
var coinValue = Math.floor(self.value / numCoins);
if (i === 0 && self.value % numCoins !== 0) {
coinValue += self.value % numCoins;
}
var coin = new DogeCoin(self.x, self.y, coinValue);
if (coinLayer) {
coinLayer.addChild(coin);
} else {
game.addChild(coin);
}
}
LK.setScore(LK.getScore() + self.value);
scoreText.setText("Score: " + LK.getScore());
LK.getSound('enemyDeath').play();
self.destroy();
}
};
var originalDestroy = self.destroy;
self.destroy = function () {
if (typeof originalDestroy === 'function') {
originalDestroy.call(self);
} else if (self.parent) {
self.parent.removeChild(self);
}
};
return self;
});
// ---- END: REPLACE SpamEmailUnit CLASS WITH THIS ----
// --- Base Tower Functionality (Conceptual - can be mixed into specific towers) ---
// This isn't a formal class, but concepts to apply
// Modify StaplerTower
// --- MODIFY StaplerTower ---
var StaplerTower = Container.expand(function () {
var self = Container.call(this);
// --- MODIFIED: No graphic initialization in constructor ---
self.lastFired = 0;
// init method removed - functionality moved to constructor and initializeTowerFromData
self.update = function () {
self.lastFired += gameSpeedMultiplier;
if (self.lastFired >= self.fireRate) {
var closestEnemy = null;
var minDistanceSq = self.range * self.range;
var buildSpot = self.parent; // Tower is child of BuildSpot
if (!buildSpot || !buildSpot.parent) {
return;
}
var levelContainer = buildSpot.parent; // BuildSpot is child of Level
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
var towerCenterInGameSpace = game.toLocal(towerGraphicGlobalCenter);
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - towerCenterInGameSpace.x;
var dy = enemy.y - towerCenterInGameSpace.y;
var distanceSq = dx * dx + dy * dy; // Squared distance check
if (distanceSq < minDistanceSq) {
minDistanceSq = distanceSq;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.shoot(closestEnemy);
self.lastFired = 0;
}
}
};
self.shoot = function (target) {
var bullet = new TowerBullet();
var buildSpot = self.parent;
if (!buildSpot || !buildSpot.parent) {
return;
}
var levelContainer = buildSpot.parent;
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
var bulletSpawnPosInGameSpace = game.toLocal(towerGraphicGlobalCenter);
bullet.x = bulletSpawnPosInGameSpace.x;
bullet.y = bulletSpawnPosInGameSpace.y;
bullet.target = target;
bullet.damage = self.damage;
game.addChild(bullet);
bullets.push(bullet);
LK.getSound('shoot').play();
};
// Initialize tower with level 0 stats and graphics
initializeTowerFromData(self, 'stapler', 0); // Set initial stats & correct L0 asset
return self;
});
var ThisIsFinePit = Container.expand(function () {
var self = Container.call(this);
// Initialize buffer tower properties
self.nearbyTowers = [];
self.currentBuffStacks = 0;
self.lastSoundPlayed = null; // For managing dynamic audio
self.soundCooldown = 0; // To prevent voice lines spamming
// Initialize the tower with level 0 stats and graphics
initializeTowerFromData(self, 'thisIsFine', 0);
self.update = function () {
// A. Count Enemies in Range
var buildSpot = self.parent;
if (!buildSpot || !buildSpot.parent) {
return;
}
var pitPosInGameSpace;
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
pitPosInGameSpace = game.toLocal(towerGraphicGlobalCenter);
// Count how many enemies are within range
var enemyCountInRange = 0;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy && enemy.parent) {
var dx = enemy.x - pitPosInGameSpace.x;
var dy = enemy.y - pitPosInGameSpace.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < self.range * self.range) {
enemyCountInRange++;
}
}
}
// Cap at max buff stacks
var activeStacks = Math.min(enemyCountInRange, self.maxBuffStacks);
// B. Clear Previous Buffs & Find Towers to Buff
if (self.currentBuffStacks > 0) {
// Revert stats for previously buffed towers
for (var i = 0; i < self.nearbyTowers.length; i++) {
var towerInRange = self.nearbyTowers[i];
if (towerInRange && towerInRange.parent && towerInRange.hasOwnProperty('originalFireRate') && towerInRange.hasOwnProperty('originalDamage')) {
towerInRange.fireRate = towerInRange.originalFireRate;
towerInRange.damage = towerInRange.originalDamage;
}
}
// Clear the array of nearby towers
self.nearbyTowers = [];
}
// Find all towers within range
if (level && level.buildSpots) {
for (var i = 0; i < level.buildSpots.length; i++) {
var spot = level.buildSpots[i];
if (spot.hasTower && spot.tower && spot.tower !== self) {
// Get tower position in game space
var towerPos = spot.tower.graphic.toGlobal({
x: 0,
y: 0
});
var otherTowerPosInGameSpace = game.toLocal(towerPos);
// Check if tower is in range
var dx = otherTowerPosInGameSpace.x - pitPosInGameSpace.x;
var dy = otherTowerPosInGameSpace.y - pitPosInGameSpace.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < self.range * self.range) {
self.nearbyTowers.push(spot.tower);
}
}
}
}
// C. Apply New Buffs
self.currentBuffStacks = activeStacks;
if (activeStacks > 0) {
// Calculate buff multipliers
var speedMultiplier = 1.0 - self.attackSpeedBuffPerStack * activeStacks;
var damageMultiplier = 1.0 + self.damageBuffPerStack * activeStacks;
// Apply buffs to nearby towers
for (var i = 0; i < self.nearbyTowers.length; i++) {
var towerInRange = self.nearbyTowers[i];
// Store original values if not already stored
if (!towerInRange.hasOwnProperty('originalFireRate')) {
towerInRange.originalFireRate = towerInRange.fireRate;
}
if (!towerInRange.hasOwnProperty('originalDamage')) {
towerInRange.originalDamage = towerInRange.damage;
}
// Apply buffs
towerInRange.fireRate = Math.max(5, Math.floor(towerInRange.originalFireRate * speedMultiplier));
towerInRange.damage = towerInRange.originalDamage * damageMultiplier;
}
}
// D. Dynamic Audio Logic
if (self.soundCooldown > 0) {
self.soundCooldown -= gameSpeedMultiplier;
}
if (activeStacks === 0 && self.lastSoundPlayed !== 'gentle') {
// Stop other fire sounds if playing
// Play sfxThisIsFineFireGentle (looping)
self.lastSoundPlayed = 'gentle';
} else if (activeStacks > 0 && activeStacks <= 2 && self.lastSoundPlayed !== 'medium') {
// Stop other fire sounds
// Play sfxThisIsFineFireMedium (looping)
self.lastSoundPlayed = 'medium';
if (self.soundCooldown <= 0) {
// Play sfxThisIsFineVoice1
self.soundCooldown = 300; // 5s
}
} else if (activeStacks > 2 && self.lastSoundPlayed !== 'roar') {
// Stop other fire sounds
// Play sfxThisIsFineFireRoar (looping)
self.lastSoundPlayed = 'roar';
if (self.soundCooldown <= 0) {
// Play sfxThisIsFineVoice2
self.soundCooldown = 300;
}
}
};
// Override destroy to clean up buffs when tower is destroyed
var originalDestroy = self.destroy;
self.destroy = function () {
// Revert all buffs before destroying
for (var i = 0; i < self.nearbyTowers.length; i++) {
var towerInRange = self.nearbyTowers[i];
if (towerInRange && towerInRange.parent && towerInRange.hasOwnProperty('originalFireRate') && towerInRange.hasOwnProperty('originalDamage')) {
towerInRange.fireRate = towerInRange.originalFireRate;
towerInRange.damage = towerInRange.originalDamage;
}
}
// Stop any looping fire sounds
// Call original destroy
if (originalDestroy) {
originalDestroy.call(self);
} else if (self.parent) {
self.parent.removeChild(self);
}
};
return self;
});
// TowerBullet class remains the same
var TowerBullet = Container.expand(function () {
var self = Container.call(this);
var graphic = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 10;
self.damage = 1;
self.target = null;
self.update = function () {
// Check if target exists and is still in the game
if (!self.target || !self.target.parent) {
self.destroy();
return;
}
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Use speed as collision threshold
if (distance < self.speed) {
self.target.takeDamage(self.damage);
self.destroy();
return;
}
// Normalize and multiply by speed
dx = dx / distance * self.speed * gameSpeedMultiplier;
dy = dy / distance * self.speed * gameSpeedMultiplier;
self.x += dx;
self.y += dy;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x558855 // Darker green background
});
/****
* Game Code
****/
// ---- START: ADD THESE LINES FOR LEVEL MANAGEMENT ----
// var towerCenterInGameSpace = game.toLocal(towerGraphicGlobalCenter);
// --- Base Tower Functionality (Conceptual - can be mixed into specific towers) ---
// Add upgrade method to both tower types via initializeTowerFromData
// New Tower - Restructuring Specialist
// (Optional) Effect for "restructuring"
// (Optional) Sounds
// ---- START: ADD THESE Global Variables for Intermission ----
var isIntermissionActive = false;
var intermissionTimer = 0;
// INTERMISSION_DURATION in frames: 20 seconds * 60fps = 1200 frames
var INTERMISSION_DURATION = 1200;
var intermissionText = null; // To hold the UI Text2 object for countdown
// ---- END: ADD THESE Global Variables for Intermission ----
var MAP_WIDTH = 3000;
var MAP_HEIGHT = 4500;
var LEVEL_DATA = {};
var currentLevelNumber = 1;
var pathPoints = []; // Ensure pathPoints is global and initialized as an empty array
// coinLayer and enemyLayer will also be global, but initialized in initializeGame
var coinLayer;
var enemyLayer;
// ---- END: ADD THESE LINES FOR LEVEL MANAGEMENT ----
LEVEL_DATA[1] = {
pathPoints: [{
x: MAP_WIDTH * 0.2,
y: MAP_HEIGHT * 0.05
}, {
x: MAP_WIDTH * 0.2,
y: MAP_HEIGHT * 0.2
}, {
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.2
}, {
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.35
}, {
x: MAP_WIDTH * 0.7,
y: MAP_HEIGHT * 0.35
}, {
x: MAP_WIDTH * 0.7,
y: MAP_HEIGHT * 0.5
}, {
x: MAP_WIDTH * 0.4,
y: MAP_HEIGHT * 0.5
}, {
x: MAP_WIDTH * 0.3,
y: MAP_HEIGHT * 0.55
}, {
x: MAP_WIDTH * 0.3,
y: MAP_HEIGHT * 0.7
}, {
x: MAP_WIDTH * 0.8,
y: MAP_HEIGHT * 0.7
}, {
x: MAP_WIDTH * 0.6,
y: MAP_HEIGHT * 0.85
}, {
x: MAP_WIDTH * 0.85,
y: MAP_HEIGHT * 0.85
}, {
x: MAP_WIDTH * 0.9,
y: MAP_HEIGHT * 0.9
}],
// Build Spots (Adjust Y values for map height)
buildSpots: [{
x: MAP_WIDTH * 0.15,
y: MAP_HEIGHT * 0.15
}, {
x: MAP_WIDTH * 0.25,
y: MAP_HEIGHT * 0.15
}, {
x: MAP_WIDTH * 0.22,
y: MAP_HEIGHT * 0.25
}, {
x: MAP_WIDTH * 0.39,
y: MAP_HEIGHT * 0.25
}, {
x: MAP_WIDTH * 0.39,
y: MAP_HEIGHT * 0.15
}, {
x: MAP_WIDTH * 0.75,
y: MAP_HEIGHT * 0.42
}, {
x: MAP_WIDTH * 0.575,
y: MAP_HEIGHT * 0.30
}, {
x: MAP_WIDTH * 0.645,
y: MAP_HEIGHT * 0.39
}, {
x: MAP_WIDTH * 0.645,
y: MAP_HEIGHT * 0.456
}, {
x: MAP_WIDTH * 0.25,
y: MAP_HEIGHT * 0.62
}, {
x: MAP_WIDTH * 0.78,
y: MAP_HEIGHT * 0.648
}, {
x: MAP_WIDTH * 0.37,
y: MAP_HEIGHT * 0.58
}, {
x: MAP_WIDTH * 0.37,
y: MAP_HEIGHT * 0.65
}, {
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.735
}, {
x: MAP_WIDTH * 0.74,
y: MAP_HEIGHT * 0.81
}, {
x: MAP_WIDTH * 0.65,
y: MAP_HEIGHT * 0.75
}, {
x: MAP_WIDTH * 0.79,
y: MAP_HEIGHT * 0.89
}],
maxWaves: 10
};
LEVEL_DATA[2] = {
// Path A starts top-left area
pathPointsA: [{
x: MAP_WIDTH * 0.2,
y: MAP_HEIGHT * 0.05
},
// SA_P1
{
x: MAP_WIDTH * 0.2,
y: MAP_HEIGHT * 0.3
},
// SA_P2
{
x: MAP_WIDTH * 0.35,
y: MAP_HEIGHT * 0.3
},
// SA_P3
{
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.45
} // CM_P1 (Common Merge Point)
],
// Path B starts top-right area
pathPointsB: [{
x: MAP_WIDTH * 0.8,
y: MAP_HEIGHT * 0.05
},
// SB_P1
{
x: MAP_WIDTH * 0.8,
y: MAP_HEIGHT * 0.3
},
// SB_P2
{
x: MAP_WIDTH * 0.65,
y: MAP_HEIGHT * 0.3
},
// SB_P3
{
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.45
} // CM_P1 (Common Merge Point)
],
// Merged path starts from CM_P1
// Note: CM_P1 is the last point of PathA/PathB and first point of MergedPath.
// The createPath logic will need to handle this (e.g., by appending MergedPath to one of them).
// For simplicity now, let's define a full path for spawning logic, and createPath can draw segments.
// We'll handle the actual drawing of merged paths in the next step.
// For now, let spawnWave use pathPointsA or pathPointsB and then a common merged segment.
// Define a 'mainPathForSpawning' that will be one of these, plus merged.
// This structure is for game logic; drawing will be separate.
// Let's simplify: spawnWave will choose pathA or pathB.
// The 'goal' will be the end of a conceptual full merged path.
// Merged path segment (starts from where A and B conceptually meet)
mergedPathPoints: [{
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.45
},
// CM_P1 (Start of merged section)
{
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.65
},
// M_P2
{
x: MAP_WIDTH * 0.25,
y: MAP_HEIGHT * 0.75
},
// M_P3
{
x: MAP_WIDTH * 0.75,
y: MAP_HEIGHT * 0.85
},
// M_P4
{
x: MAP_WIDTH * 0.75,
y: MAP_HEIGHT * 0.95
} // M_P5 (Goal)
],
buildSpots: [{
x: MAP_WIDTH * 0.15,
y: MAP_HEIGHT * 0.15
}, {
x: MAP_WIDTH * 0.3,
y: MAP_HEIGHT * 0.25
}, {
x: MAP_WIDTH * 0.4,
y: MAP_HEIGHT * 0.35
}, {
x: MAP_WIDTH * 0.85,
y: MAP_HEIGHT * 0.15
}, {
x: MAP_WIDTH * 0.7,
y: MAP_HEIGHT * 0.25
}, {
x: MAP_WIDTH * 0.6,
y: MAP_HEIGHT * 0.35
}, {
x: MAP_WIDTH * 0.45,
y: MAP_HEIGHT * 0.50
}, {
x: MAP_WIDTH * 0.55,
y: MAP_HEIGHT * 0.50
}, {
x: MAP_WIDTH * 0.4,
y: MAP_HEIGHT * 0.60
}, {
x: MAP_WIDTH * 0.2,
y: MAP_HEIGHT * 0.70
}, {
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.78
}, {
x: MAP_WIDTH * 0.8,
y: MAP_HEIGHT * 0.90
}, {
x: MAP_WIDTH * 0.27,
y: MAP_HEIGHT * 0.15
}, {
x: MAP_WIDTH * 0.73,
y: MAP_HEIGHT * 0.15
}, {
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.35
}],
goalPosition: {
x: MAP_WIDTH * 0.75,
y: MAP_HEIGHT * 0.95
},
// Explicit goal for Level 2
maxWaves: 15
};
function createRangeIndicator(centerX, centerY, radius, parentContainer) {
return new RangeIndicator(centerX, centerY, radius, parentContainer);
}
function addUpgradeToTower(towerInstance) {
towerInstance.upgrade = function () {
// Check if next level exists
if (!this.towerType || !TOWER_DATA[this.towerType]) {
return false;
}
var nextLevel = this.currentLevel + 1;
var towerTypeData = TOWER_DATA[this.towerType];
// Check if next level data exists
if (!towerTypeData.levels[nextLevel]) {
spawnFloatingText("MAX LEVEL!", this.parent.x, this.parent.y - 50, {
fill: 0xFFD700
});
return false;
}
// Get upgrade cost
var upgradeCost = towerTypeData.levels[nextLevel].cost;
// Check if player can afford upgrade
if (currency < upgradeCost) {
spawnFloatingText("Need more $!", this.parent.x, this.parent.y - 50, {
fill: 0xFF0000
});
return false;
}
// Pay for upgrade
currency -= upgradeCost;
currencyText.setText("$: " + currency);
// Apply upgrade
initializeTowerFromData(this, this.towerType, nextLevel);
// Play upgrade sound
LK.getSound('uiSelectTower').play();
// Show upgrade message
spawnFloatingText("UPGRADED!", this.parent.x, this.parent.y - 50, {
fill: 0x00FF00
});
// Hide upgrade menu
if (this.parent && typeof this.parent.hideUpgradeMenu === 'function') {
this.parent.hideUpgradeMenu();
}
return true;
};
}
// Specific build sounds for each tower type if desired
// --- New Sound Effects ---
// Bureaucracy Blocker
// Even more so
// Slightly bigger/cooler
// Stapler
// --- Tower Level Assets (Placeholders) ---
// You'll also need actual tower icons for the buttons eventually
// For + symbols
// --- UI Assets for Popup ---
// Added sound for auto-attack
// Game constants
// <-- NEW Green Target Marker
// --- NEW ASSETS ---
// Placeholder path
// Placeholder path
// Sound for bureaucracy blocker or paperwork
// Sound for red tape worm
// --- TOWER DEFINITIONS ---
// Tower data is defined below
// Function to initialize or upgrade tower with data from TOWER_DATA
function initializeTowerFromData(towerInstance, towerTypeKey, levelIndex) {
if (!towerInstance || !TOWER_DATA[towerTypeKey] || !TOWER_DATA[towerTypeKey].levels || !TOWER_DATA[towerTypeKey].levels[levelIndex]) {
console.error("Invalid tower, type, or level data for initialization");
return;
}
var levelData = TOWER_DATA[towerTypeKey].levels[levelIndex];
// Store tower type and level
towerInstance.towerType = towerTypeKey;
towerInstance.currentLevel = levelIndex;
// Assign all properties from levelData to the tower
for (var key in levelData) {
if (key !== 'asset' && key !== 'cost' && key !== 'description') {
towerInstance[key] = levelData[key];
}
}
// Graphic handling:
if (towerInstance.graphic && towerInstance.graphic.parent) {
// If graphic exists and is attached, update it
towerInstance.graphic.parent.removeChild(towerInstance.graphic);
towerInstance.graphic = towerInstance.attachAsset(levelData.asset, {
anchorX: 0.5,
anchorY: 0.5
});
} else {
// If graphic doesn't exist or is detached, create a new one
if (towerInstance.graphic) {
towerInstance.graphic.destroy(); // Clean up old if it existed but was detached
}
towerInstance.graphic = towerInstance.attachAsset(levelData.asset, {
anchorX: 0.5,
anchorY: 0.5
});
}
// Tower-specific re-initialization logic
if (towerTypeKey === 'stapler' && towerInstance.hasOwnProperty('lastFired')) {
towerInstance.lastFired = 0; // Reset fire cooldown on init/upgrade
}
if (towerTypeKey === 'blocker' && typeof towerInstance.clearAllSlows === 'function') {
towerInstance.clearAllSlows(); // Ensure slows are reset based on new stats
}
// Add upgrade method if not already present
if (!towerInstance.upgrade) {
addUpgradeToTower(towerInstance);
}
}
// --- GLOBAL DEFINITION FOR ICON OFFSETS ---
// buildSpot graphic width is 150, icon width is ~80-100. Adjust offsets as needed.
var ICON_SELECT_OFFSETS = [{
x: -170,
y: -110,
towerKey: 'stapler'
}, {
x: 0,
y: -200,
towerKey: 'blocker'
}, {
x: 170,
y: -110,
towerKey: 'laserCat'
}, {
x: -170,
y: 110,
towerKey: 'rickroller'
}, {
x: 0,
y: 200,
towerKey: 'thisIsFine'
}, {
x: 170,
y: 110,
towerKey: 'restruct'
}];
var TOWER_DATA = {
'stapler': {
name: 'Stapler Turret',
iconAsset: 'towerStaplerLvl1',
// Use Lvl1 icon for selection button initially
buildSfx: 'buildStapler',
levels: [{
asset: 'towerStaplerLvl1',
cost: 50,
damage: 1,
range: 400,
fireRate: 60,
description: "Basic Stapler"
}, {
asset: 'towerStaplerLvl2',
cost: 150,
damage: 3,
range: 420,
fireRate: 55,
description: "Improved Firepower"
}, {
asset: 'towerStaplerLvl3',
cost: 300,
damage: 4,
range: 450,
fireRate: 50,
description: "Max Staples!"
}]
},
'blocker': {
name: 'Bureaucracy Blocker',
iconAsset: 'towerBlockerLvl1',
buildSfx: 'buildBlocker',
levels: [{
asset: 'towerBlockerLvl1',
cost: 75,
slowFactor: 0.5,
range: 320,
description: "Slows nearby red tape."
}, {
asset: 'towerBlockerLvl2',
cost: 300,
slowFactor: 0.4,
range: 350,
description: "Wider, stronger slow."
},
// 0.4 means 60% speed reduction
{
asset: 'towerBlockerLvl3',
cost: 600,
slowFactor: 0.3,
range: 400,
description: "Bureaucratic Gridlock!"
}]
},
'laserCat': {
name: 'Laser Cat Perch',
iconAsset: 'towerLaserCatLvl1',
buildSfx: 'buildLaserCat',
levels: [{
// Level 1
asset: 'towerLaserCatLvl1',
cost: 150,
range: 500,
initialBeamDamage: 1,
// Damage when beam first hits
dotDamagePerSecond: 1,
// Base DOT
dotRampUpFactor: 1.2,
// Multiplier for DOT each second it stays on same target (e.g., 2, then 2*1.2, then 2*1.2*1.2)
maxBeamDuration: 3000,
// Milliseconds (3 seconds) beam can stay on one target
cooldownBetweenBeams: 4000,
// Milliseconds (4 seconds) after a beam finishes
numberOfBeams: 1,
// Targets 1 enemy
description: "Pew pew! Precision feline firepower. Damage ramps up."
}, {
// Level 2
asset: 'towerLaserCatLvl1',
// Placeholder - use 'towerLaserCatLvl2' when ready
cost: 600,
range: 550,
initialBeamDamage: 5,
dotDamagePerSecond: 2,
dotRampUpFactor: 1.25,
maxBeamDuration: 3500,
cooldownBetweenBeams: 3500,
numberOfBeams: 2,
// Targets 2 enemies
description: "Dual-core processing! More lasers, more ouch."
}, {
// Level 3
asset: 'towerLaserCatLvl1',
// Placeholder - use 'towerLaserCatLvl3' when ready
cost: 1200,
range: 600,
initialBeamDamage: 10,
dotDamagePerSecond: 5,
dotRampUpFactor: 1.3,
maxBeamDuration: 4000,
cooldownBetweenBeams: 3000,
numberOfBeams: 3,
// Targets 3 enemies
description: "Maximum laser focus! It's a light show of doom."
}]
},
'rickroller': {
name: 'Rickroller Trap',
iconAsset: 'towerRickrollerLvl1',
buildSfx: 'sfxRickrollerPlace',
// Changed to match your SFX list
levels: [{
asset: 'towerRickrollerLvl1',
cost: 175,
damagePerTick: 1,
dotTickRate: 500,
// Milliseconds
effectRadius: 200,
// Radius of stun/damage from impact
stunDuration: 2000,
// Milliseconds
popupDuration: 3000,
// Milliseconds
popupAsset: 'rickAstleyPopup',
soundEffects: ['sfxRickrollTrigger1', 'sfxRickrollTrigger2', 'sfxRickrollTrigger3'],
// Array of sound variants
cooldown: 10000,
// Milliseconds from when the tower fires
range: 400,
// Range of the TOWER to decide where to shoot the trap
projectileSpeed: 7,
// Speed of flying Rick Astley projectile
description: "Never gonna let them pass! Stuns and confuses."
}]
},
'thisIsFine': {
name: "'This Is Fine' Fire Pit",
iconAsset: 'towerThisIsFineLvl1',
buildSfx: 'buildThisIsFine',
// Define this sound
levels: [{
asset: 'towerThisIsFineLvl1',
cost: 125,
range: 650,
// Aura buff range
maxBuffStacks: 5,
// How many nearby enemies contribute to max buff
attackSpeedBuffPerStack: 0.05,
// e.g., 5% faster per stack (0.95 multiplier)
damageBuffPerStack: 0.05,
// e.g., 5% more damage per stack (1.05 multiplier)
description: "Everything's fine. Totally fine. Buffs nearby towers."
}]
},
'restruct': {
name: 'Restructuring Specialist',
iconAsset: 'towerRestructLvl1',
buildSfx: 'buildRestruct',
levels: [{
asset: 'towerRestructLvl1',
cost: 500,
damage: 100,
instantKillThreshold: 25,
range: 450,
chargeUpTime: 360,
targetPriority: 'strongest',
effectAsset: 'restructureEffect',
description: "Optimizing threats... permanently."
}, {
asset: 'towerRestructLvl1',
cost: 1000,
damage: 150,
instantKillThreshold: 35,
range: 600,
chargeUpTime: 300,
targetPriority: 'strongest',
effectAsset: 'restructureEffect',
description: "Synergistic realignment for enhanced efficiency."
}, {
asset: 'towerRestructLvl1',
cost: 2000,
damage: 200,
instantKillThreshold: 50,
range: 700,
chargeUpTime: 250,
targetPriority: 'strongest',
effectAsset: 'restructureEffect',
goldenParachuteDamage: 25,
goldenParachuteRadius: 100,
description: "Downsizing comes with... severance packages."
}]
}
// Add more tower types here later
};
var gameSpeedMultiplier = 1.0; // Default game speed
var TOWER_COST = 50;
var WAVE_DELAY = 300; // frames between waves
var MAX_WAVES = 10;
// Access screen dimensions directly from LK
var SCREEN_HEIGHT = 2732; // Standard iPad Pro height (portrait mode)
var SCREEN_WIDTH = 2048; // Standard iPad Pro width (portrait mode)
var PAN_THRESHOLD = SCREEN_HEIGHT * 0.25; // Start panning when Doge is in top/bottom 25% /**** NEW ****/
var MAP_WIDTH = 3000; // Example: roughly double current screen width
var MAP_HEIGHT = 4500; // Let's increase this too if we're expanding
// Game variables
var coinLayer;
var enemyLayer;
var currency = 100;
var playerLives = 5;
var currentWave = 0;
var waveTimer = 0;
var isWaveActive = false;
var enemies = [];
var bullets = [];
var pathPoints = [];
var currentActiveBuildSpot = null;
var level;
var doge;
var goal;
// --- HELPER FUNCTION FOR FLOATING TEXT ---
function isChildOf(possibleParent, target) {
if (!target || !target.parent) {
return false;
}
if (target.parent === possibleParent) {
return true;
}
return isChildOf(possibleParent, target.parent);
}
// Function to update the visual state of speed buttons
function updateSpeedButtonVisuals() {
if (game.normalSpeedButton && game.fastSpeedButton) {
if (gameSpeedMultiplier === 1.0) {
game.normalSpeedButton.setText("Regular Work Day", {
fill: 0xFFFFFF // Active
});
game.fastSpeedButton.setText("Crunch Time!", {
fill: 0xDDDDDD // Inactive
});
} else {
game.normalSpeedButton.setText("Regular Work Day", {
fill: 0xDDDDDD // Inactive
});
game.fastSpeedButton.setText("Crunch Time!", {
fill: 0xFFD700 // Gold/active
});
}
}
}
function spawnFloatingText(text, x, y, options) {
var defaultOptions = {
size: 30,
fill: 0xFFFFFF,
// White text
stroke: 0x000000,
strokeThickness: 2,
duration: 60,
// frames (1 second at 60fps)
velocityY: -1.5,
// Pixels per frame upwards
alphaFadeSpeed: 0.015
};
var settings = Object.assign({}, defaultOptions, options); // Merge user options
var floatingText = new Text2(text, {
size: settings.size,
fill: settings.fill,
stroke: settings.stroke,
strokeThickness: settings.strokeThickness,
anchorX: 0.5,
// Center the text
anchorY: 0.5
});
floatingText.x = x;
floatingText.y = y;
floatingText.alpha = 1.0;
game.addChild(floatingText); // Add to main game container to scroll with world
var framesLived = 0;
floatingText.update = function () {
floatingText.y += settings.velocityY * gameSpeedMultiplier;
floatingText.alpha -= settings.alphaFadeSpeed * gameSpeedMultiplier;
framesLived += gameSpeedMultiplier;
if (framesLived >= settings.duration || floatingText.alpha <= 0) {
floatingText.destroy();
}
};
// Add this to a list of updatable text objects if your engine doesn't auto-update children with .update
// For LK Engine, if it's a child of `game` and has an `update` method, it should be called.
// If not, you'll need a global array like `activeFloatingTexts` and iterate it in `game.update`.
// Let's assume LK.Game handles child updates for now.
}
var scoreText = new Text2("Score: 0", {
size: 50,
fill: 0xFFFFFF
}); // White text
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
var waveText = new Text2("Wave: 0/" + MAX_WAVES, {
size: 50,
fill: 0xFFFFFF
});
waveText.anchor.set(1, 0); // Anchor top-right
waveText.x = -100; // Position from right edge
LK.gui.topRight.addChild(waveText);
var currencyText = new Text2("$: " + currency, {
size: 50,
fill: 0xFFFFFF
});
currencyText.anchor.set(0.5, 0);
currencyText.y = 60;
LK.gui.top.addChild(currencyText);
var livesText = new Text2("Lives: " + playerLives, {
size: 50,
fill: 0xFFFFFF
});
livesText.anchor.set(1, 0); // Anchor top-right
livesText.x = -100;
livesText.y = 60;
LK.gui.topRight.addChild(livesText);
// Create game speed control buttons
var normalSpeedButton = new Text2("Regular Work Day", {
size: 60,
// Or your preferred size
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 2
});
normalSpeedButton.anchor.set(1, 0); // Anchor to its top-right
normalSpeedButton.x = -100; // Position from the right edge of LK.gui.topRight
normalSpeedButton.y = 200; // Position from the top edge of LK.gui.topRight (adjust as needed)
normalSpeedButton.interactive = true;
normalSpeedButton.down = function () {
gameSpeedMultiplier = 1.0;
updateSpeedButtonVisuals();
LK.getSound('uiSelectTower').play();
};
LK.gui.topRight.addChild(normalSpeedButton);
var fastSpeedButton = new Text2("Crunch Time!", {
size: 60,
// Or your preferred size
fill: 0xDDDDDD,
stroke: 0x000000,
strokeThickness: 2
});
fastSpeedButton.anchor.set(1, 0); // Anchor to its top-right
fastSpeedButton.x = -100; // Position from the right edge of LK.gui.topRight
fastSpeedButton.y = normalSpeedButton.y + normalSpeedButton.height + 10; // Position below the normal speed button
fastSpeedButton.interactive = true;
fastSpeedButton.down = function () {
gameSpeedMultiplier = 2.0;
updateSpeedButtonVisuals();
LK.getSound('uiSelectTower').play();
};
LK.gui.topRight.addChild(fastSpeedButton);
// Intermission Text (initially hidden)
if (intermissionText && intermissionText.parent) {
// If it exists from a previous level, destroy it
intermissionText.destroy();
intermissionText = null;
}
intermissionText = new Text2("", {
// Create new
size: 70,
// Made size larger for better visibility
fill: 0xFFD700,
// Gold color
stroke: 0x000000,
strokeThickness: 4,
anchorX: 0.5 // Center horizontally
});
intermissionText.x = 0; // Centered in LK.gui.top (since anchorX is 0.5)
intermissionText.y = SCREEN_HEIGHT / 2 - 100; // Position towards vertical center of screen
intermissionText.visible = false; // Start hidden
LK.gui.top.addChild(intermissionText); // Add to a main GUI layer
// Removed old info text, replaced by Bark button
// var infoText = new Text2(...)
// Bark Button /**** NEW ****/
var barkButton = new Text2("BARK!", {
size: 100,
fill: 0xffcc00,
// Doge color
stroke: 0x000000,
strokeThickness: 4
});
barkButton.anchor.set(0.5, 1); // Anchor bottom-center
barkButton.y = -50; // Position from bottom edge
barkButton.interactive = true; // Make it clickable
barkButton.down = function () {
if (doge) {
var success = doge.manualBark();
if (success) {
// Optional: visual feedback on button press
barkButton.scale.set(1.1);
LK.setTimeout(function () {
barkButton.scale.set(1.0);
}, 100);
}
}
};
LK.gui.bottom.addChild(barkButton);
// ---- START: REPLACE OLD initializeGame WITH THIS NEW VERSION ----
function initializeGame(levelNum) {
currentLevelNumber = levelNum;
var levelConfig = LEVEL_DATA[levelNum];
if (!levelConfig) {
// LK.log is not available, use console.error for feedback if possible, or alert.
// For now, we'll just try to fallback or stop.
console.error("Error: Level " + levelNum + " data not found!");
if (levelNum !== 1 && LEVEL_DATA[1]) {
currentLevelNumber = 1;
levelConfig = LEVEL_DATA[1];
if (!levelConfig) {
console.error("CRITICAL Error: Level 1 data also not found!");
return;
}
} else if (!LEVEL_DATA[1]) {
console.error("CRITICAL Error: No Level data found at all!");
return;
}
}
// --- Clear Existing Game Objects from Previous Level (if any) ---
if (level && typeof level.destroy === 'function' && level.parent) {
// Destroy all towers on buildspots
if (level.buildSpots && level.buildSpots.length > 0) {
for (var i_bs = 0; i_bs < level.buildSpots.length; i_bs++) {
var spot_to_clear = level.buildSpots[i_bs];
if (spot_to_clear.tower) {
spot_to_clear.tower.destroy(); // Assuming towers have a destroy method
spot_to_clear.tower = null;
spot_to_clear.hasTower = false;
if (spot_to_clear.graphic) {
spot_to_clear.graphic.alpha = 0.5;
}
}
// Clear any UI attached to the buildspot
if (spot_to_clear.rangeIndicator) {
spot_to_clear.hideRangeIndicator();
} // Assuming a method if it exists
else if (spot_to_clear.currentRangeIndicator) {
spot_to_clear.hideCurrentRangeIndicator();
} // Or this
else if (spot_to_clear.rangeIndicator && typeof spot_to_clear.rangeIndicator.destroy === 'function') {
spot_to_clear.rangeIndicator.destroy();
}
if (spot_to_clear.selectionIconContainer) {
spot_to_clear.hideSelectionIcons();
}
if (spot_to_clear.actionIconContainer) {
spot_to_clear.hideTowerActionIcons();
}
if (spot_to_clear.upgradeMenuContainer) {
spot_to_clear.hideUpgradeMenu();
}
}
}
level.destroy(); // Assuming GameLevel has a destroy method that cleans its children
level = null;
}
if (doge && typeof doge.destroy === 'function' && doge.parent) {
doge.destroy();
}
doge = null;
if (goal && typeof goal.destroy === 'function' && goal.parent) {
goal.destroy();
}
goal = null;
// Destroy all active enemies
for (var i_e = enemies.length - 1; i_e >= 0; i_e--) {
if (enemies[i_e] && typeof enemies[i_e].destroy === 'function' && enemies[i_e].parent) {
enemies[i_e].destroy();
}
}
enemies = [];
// Destroy all active bullets
for (var i_b = bullets.length - 1; i_b >= 0; i_b--) {
if (bullets[i_b] && typeof bullets[i_b].destroy === 'function' && bullets[i_b].parent) {
bullets[i_b].destroy();
}
}
bullets = [];
// Clear coins from coinLayer
if (coinLayer && coinLayer.children) {
while (coinLayer.children.length > 0) {
if (typeof coinLayer.children[0].destroy === 'function') {
coinLayer.children[0].destroy();
} else if (coinLayer.children[0].parent) {
// Fallback removal
coinLayer.removeChild(coinLayer.children[0]);
} else {
// Break if child can't be removed (should not happen)
break;
}
}
}
currentActiveBuildSpot = null;
// --- Standard Game Setup ---
game.x = 0;
game.y = 0;
game.scale.set(1);
// --- Correct Layering Order (remove if existing, then add in order) ---
if (level && level.parent === game) {
game.removeChild(level);
} // Should have been destroyed already
if (coinLayer && coinLayer.parent === game) {
game.removeChild(coinLayer);
}
if (enemyLayer && enemyLayer.parent === game) {
game.removeChild(enemyLayer);
}
level = new GameLevel();
game.addChild(level); // 1. Level (path, build spot graphics)
coinLayer = new Container();
game.addChild(coinLayer); // 2. Coins (on top of level)
game.coinLayer = coinLayer; // Make accessible via game.coinLayer if needed elsewhere
enemyLayer = new Container();
game.addChild(enemyLayer); // 3. Enemies (on top of coins)
// ---- START: REPLACE PATH/BUILDSPOT CREATION LOGIC IN initializeGame WITH THIS ----
// --- Load Level Specific Data ---
pathPoints.length = 0; // Clear the global pathPoints array first
if (currentLevelNumber === 2 && levelConfig.pathPointsA && levelConfig.pathPointsB && levelConfig.mergedPathPoints) {
// For Level 2: Draw all three path segments
level.createPath(levelConfig.pathPointsA);
level.createPath(levelConfig.pathPointsB);
level.createPath(levelConfig.mergedPathPoints);
// For enemy pathing on Level 2, we need to decide which path they primarily follow
// For now, let global pathPoints be Path A + Merged Path.
// Enemies spawned on Path B will need special handling later or their own path data.
levelConfig.pathPointsA.forEach(function (p) {
pathPoints.push(p);
});
// Append mergedPathPoints, skipping the first point if it's identical to the last of pathPointsA
if (levelConfig.mergedPathPoints.length > 0) {
var lastOfA = pathPoints[pathPoints.length - 1];
var firstOfMerged = levelConfig.mergedPathPoints[0];
var startIndexForMerged = lastOfA.x === firstOfMerged.x && lastOfA.y === firstOfMerged.y ? 1 : 0;
for (var m_idx = startIndexForMerged; m_idx < levelConfig.mergedPathPoints.length; m_idx++) {
pathPoints.push(levelConfig.mergedPathPoints[m_idx]);
}
}
} else if (levelConfig.pathPoints) {
// For Level 1 or other single-path levels
levelConfig.pathPoints.forEach(function (p) {
pathPoints.push(p);
});
level.createPath(pathPoints);
} else {
console.error("No pathPoints defined for level " + currentLevelNumber);
}
level.createBuildSpots(levelConfig.buildSpots); // This uses the buildSpots from LEVEL_DATA
goal = new Goal(); // Re-create goal
if (pathPoints.length > 0) {
// Use the (potentially modified) global pathPoints for goal
var goalPosData = pathPoints[pathPoints.length - 1];
// Check if levelConfig has a specific goal position that overrides the last path point
if (levelConfig.goalPosition) {
goalPosData = levelConfig.goalPosition;
}
goal.x = goalPosData.x;
goal.y = goalPosData.y;
// Add goal as child of 'level' so it's part of the level's display objects
// and gets cleaned up when 'level' is destroyed.
// It should be added AFTER paths/buildspots so it's visually on top if needed,
// or to 'game' if it needs to be above everything in 'level'.
// Let's keep it on game for now, to be above path tiles.
game.addChild(goal);
}
// ---- END: REPLACE PATH/BUILDSPOT CREATION LOGIC ----
goal = new Goal();
if (pathPoints.length > 0) {
var goalPosData = pathPoints[pathPoints.length - 1];
if (levelConfig.goalPosition) {
goalPosData = levelConfig.goalPosition;
}
goal.x = goalPosData.x;
goal.y = goalPosData.y;
level.addChild(goal); // Add goal as child of level for better organization
}
doge = new DogeHero();
doge.x = SCREEN_WIDTH / 2;
doge.y = SCREEN_HEIGHT * 0.8;
doge.targetX = doge.x;
doge.targetY = doge.y;
game.addChild(doge); // Doge on top of game layers
// --- Reset Game State Variables ---
currency = levelConfig.startingCurrency !== undefined ? levelConfig.startingCurrency : 100;
playerLives = levelConfig.startingLives !== undefined ? levelConfig.startingLives : 5;
currentWave = 0;
waveTimer = 0;
isWaveActive = false;
isIntermissionActive = false;
intermissionTimer = 0;
// Update UI
currencyText.setText("$: " + currency);
livesText.setText("Lives: " + playerLives);
var maxWavesForLevel = levelConfig && levelConfig.maxWaves !== undefined ? levelConfig.maxWaves : MAX_WAVES;
waveText.setText("Wave: " + currentWave + "/" + maxWavesForLevel);
scoreText.setText("Score: " + LK.getScore());
barkButton.setText("BARK!");
if (game.normalSpeedButton && game.fastSpeedButton) {
updateSpeedButtonVisuals();
}
// Consider music here
// LK.playMusic('bgmusic'); // If music should restart per level
}
// ---- END: REPLACE OLD initializeGame WITH THIS NEW VERSION ----
currentActiveBuildSpot = null;
function spawnWave() {
currentWave++;
waveText.setText("Wave: " + currentWave + "/" + MAX_WAVES);
var baseEnemyCountPerWave = 8 + currentWave * 3;
var enemiesToSpawnThisWave = [];
// --- Define enemy composition for this wave ---
if (currentLevelNumber === 1) {
// LEVEL 1 WAVES
if (currentWave <= 2) {
// Waves 1-2: Paper & Interns
for (var i = 0; i < baseEnemyCountPerWave; i++) {
if (Math.random() < 0.6) {
enemiesToSpawnThisWave.push(Enemy);
} else {
enemiesToSpawnThisWave.push(InternOnCoffeeRun);
}
}
} else if (currentWave >= 3 && currentWave <= 9) {
// Waves 3-9: Paper, Interns, Spam Emails
var actualCountForThisWave = baseEnemyCountPerWave;
for (var i = 0; i < actualCountForThisWave; i++) {
var r = Math.random();
if (r < 0.4) {
enemiesToSpawnThisWave.push(Enemy);
} else if (r < 0.7) {
enemiesToSpawnThisWave.push(InternOnCoffeeRun);
} else {
enemiesToSpawnThisWave.push(SpamEmailUnit);
}
}
} else if (currentWave === 10) {
// Wave 10: Mix + 3 Auditors
enemiesToSpawnThisWave.push(AuditorBot);
enemiesToSpawnThisWave.push(AuditorBot);
enemiesToSpawnThisWave.push(AuditorBot);
var remainingCount = baseEnemyCountPerWave - 3;
if (remainingCount < 0) {
remainingCount = 0;
}
for (var i = 0; i < remainingCount; i++) {
var r = Math.random();
if (r < 0.3) {
enemiesToSpawnThisWave.push(Enemy);
} else if (r < 0.6) {
enemiesToSpawnThisWave.push(InternOnCoffeeRun);
} else {
enemiesToSpawnThisWave.push(SpamEmailUnit);
}
}
// Shuffle the array so auditors don't always come first/last
enemiesToSpawnThisWave.sort(function () {
return Math.random() - 0.5;
});
} else {
// Fallback for waves > 10 (or unhandled cases)
for (var i = 0; i < baseEnemyCountPerWave; i++) {
enemiesToSpawnThisWave.push(Enemy); // Default to Paper
}
}
} else if (currentLevelNumber === 2) {
// LEVEL 2 WAVES
if (currentWave === 1) {
// L2, Wave 1
baseEnemyCountPerWave = 10 + currentWave * 2; // Slightly different scaling for L2
for (var i = 0; i < baseEnemyCountPerWave; i++) {
if (Math.random() < 0.4) {
enemiesToSpawnThisWave.push(Enemy);
} else if (Math.random() < 0.7) {
enemiesToSpawnThisWave.push(InternOnCoffeeRun);
} else {
enemiesToSpawnThisWave.push(SpamEmailUnit);
}
}
} else if (currentWave === 2) {
// L2, Wave 2 - Introduce RedTapeWorm
baseEnemyCountPerWave = 12 + currentWave * 2;
for (var i = 0; i < baseEnemyCountPerWave; i++) {
var r = Math.random();
if (r < 0.3) {
enemiesToSpawnThisWave.push(Enemy);
} else if (r < 0.5) {
enemiesToSpawnThisWave.push(InternOnCoffeeRun);
} else if (r < 0.8) {
enemiesToSpawnThisWave.push(SpamEmailUnit);
} else {
enemiesToSpawnThisWave.push(RedTapeWorm);
} // Introduce RedTapeWorm
}
} else if (currentWave >= 3 && currentWave <= MAX_WAVES) {
// L2, Waves 3-10 (example)
baseEnemyCountPerWave = 10 + currentWave * 3; // Steeper scaling
for (var i = 0; i < baseEnemyCountPerWave; i++) {
var r = Math.random();
if (r < 0.2) {
enemiesToSpawnThisWave.push(Enemy);
} else if (r < 0.4) {
enemiesToSpawnThisWave.push(InternOnCoffeeRun);
} else if (r < 0.6) {
enemiesToSpawnThisWave.push(SpamEmailUnit);
} else if (r < 0.85) {
enemiesToSpawnThisWave.push(RedTapeWorm);
} else {
enemiesToSpawnThisWave.push(AuditorBot);
} // Auditors appear more often
}
}
// Shuffle the wave for variety
enemiesToSpawnThisWave.sort(function () {
return Math.random() - 0.5;
});
} else {
// Fallback for currentLevelNumber other than 1 or 2
for (var i = 0; i < baseEnemyCountPerWave; i++) {
enemiesToSpawnThisWave.push(Enemy); // Default to Paper
}
}
var spawnInterval = 60;
var spawnIndex = 0;
function spawnIndividualEnemy() {
if (spawnIndex >= enemiesToSpawnThisWave.length || !pathPoints || pathPoints.length === 0) {
isWaveActive = true;
return;
}
var enemyTypeToSpawn = enemiesToSpawnThisWave[spawnIndex];
// Determine the full path for this enemy based on level and spawn position
var fullPathForThisEnemy;
if (currentLevelNumber === 2 && LEVEL_DATA[2]) {
if (spawnIndex % 2 === 0) {
// Spawning on Path A
fullPathForThisEnemy = LEVEL_DATA[2].pathPointsA.concat(LEVEL_DATA[2].mergedPathPoints.slice(1));
} else {
// Spawning on Path B
fullPathForThisEnemy = LEVEL_DATA[2].pathPointsB.concat(LEVEL_DATA[2].mergedPathPoints.slice(1));
}
} else {
// For Level 1 or if Level 2 data is incomplete, use the global pathPoints
fullPathForThisEnemy = pathPoints;
}
// Create the enemy with its custom path data
var enemy = new enemyTypeToSpawn(fullPathForThisEnemy[0].x, fullPathForThisEnemy[0].y, undefined,
// Value parameter (unused)
fullPathForThisEnemy);
// Set position (already done in constructor with the parameters above)
if (currentLevelNumber === 2 && LEVEL_DATA[2].pathPointsA && LEVEL_DATA[2].pathPointsB) {
// Position is already set via the constructor
} else {
// Ensure position is set correctly for Level 1 (redundant but safe)
enemy.x = pathPoints[0].x;
enemy.y = pathPoints[0].y;
}
// Apply wave-based health scaling if you re-add it:
// enemy.health += Math.floor(currentWave * 0.5);
enemyLayer.addChild(enemy);
enemies.push(enemy);
spawnIndex++;
if (spawnIndex < enemiesToSpawnThisWave.length) {
LK.setTimeout(spawnIndividualEnemy, spawnInterval * 16.67 / gameSpeedMultiplier);
} else {
isWaveActive = true;
}
}
isWaveActive = false;
if (enemiesToSpawnThisWave.length > 0) {
spawnIndividualEnemy();
} else {
isWaveActive = true; // No enemies defined for this wave
}
}
function checkWaveComplete() {
if (isWaveActive && enemies.length === 0) {
// Double check if we really spawned everything for the wave before proceeding
// This basic check assumes spawning finishes before all enemies are killed
isWaveActive = false;
waveTimer = 0; // Reset timer for next wave delay
if (currentWave >= MAX_WAVES) {
if (currentLevelNumber === 1) {
// Progress to Level 2
currentLevelNumber = 2;
initializeGame(currentLevelNumber);
return; // prevent showing "You Win" screen yet
} else if (currentLevelNumber === 2) {
// Player wins after completing Level 2
if (LK.getScore() > storage.highScore) {
storage.highScore = LK.getScore();
}
LK.showYouWin();
}
}
}
}
// Event Handlers /**** MODIFIED ****/
// --- NEW APPROACH game.down ---
game.down = function (x, y, obj) {
var worldX = x;
var worldY = y;
// --- 1. Active UI Interaction (Menus, Dialogs) ---
// Check Build Selection Icons
if (currentActiveBuildSpot && currentActiveBuildSpot.areIconsVisible && currentActiveBuildSpot.selectionIconContainer) {
var icons = currentActiveBuildSpot.selectionIconContainer.children;
for (var i = 0; i < icons.length; i++) {
// Check if the click was directly on an icon OR an interactive child of an icon
if ((obj === icons[i] || isChildOf(icons[i], obj)) && obj.interactive) {
// The icon's own .down() handler will be called by the engine.
return; // CRITICAL: Stop further processing in game.down
}
}
}
// Check Tower Action Icons (Upgrade/Sell)
if (currentActiveBuildSpot && currentActiveBuildSpot.areActionIconsVisible && currentActiveBuildSpot.actionIconContainer) {
// Assuming areActionIconsVisible flag
var actionIcons = currentActiveBuildSpot.actionIconContainer.children;
for (var i = 0; i < actionIcons.length; i++) {
// Check if the click was directly on an action icon OR an interactive child of an action icon
if ((obj === actionIcons[i] || isChildOf(actionIcons[i], obj)) && obj.interactive) {
// The action icon's own .down() handler will be called by the engine.
return; // CRITICAL: Stop further processing in game.down
}
}
}
// Placeholder for Sell Confirmation Dialog interaction
// (Assuming sellConfirmDialog is a global or accessible variable when visible)
// if (typeof sellConfirmDialog !== 'undefined' && sellConfirmDialog && sellConfirmDialog.parent && sellConfirmDialog.visible) {
// // Check if the click was on any interactive part of the sell confirmation dialog
// if (obj === sellConfirmDialog.yesButton || obj === sellConfirmDialog.noButton || (isChildOf(sellConfirmDialog, obj) && obj.interactive) ) {
// // The button's .down() handler will be called by the engine.
// return; // CRITICAL
// }
// }
// --- 2. Click on Doge (for dragging) ---
if (doge && obj === doge.graphic) {
dragDoge = true;
doge.setTarget(doge.x, doge.y); // Stop current auto-movement
return; // CRITICAL
}
// --- 3. Click on a BuildSpot graphic itself (to toggle its menu) ---
if (level && level.buildSpots) {
for (var i = 0; i < level.buildSpots.length; i++) {
var spot = level.buildSpots[i];
// Check if the click was directly on the BuildSpot's graphic
// OR on an interactive child nested within that graphic (if spot.graphic could be a container)
if (obj === spot.graphic || isChildOf(spot.graphic, obj) && obj.interactive) {
// The BuildSpot's own .down() handler (spot.down()) should be triggered by the engine
// to manage its menu visibility.
return; // CRITICAL
}
}
}
// --- 4. Close an Open Menu if "Empty Space" was clicked ---
// This section runs if the click was NOT on an interactive UI icon/button (checked in step 1),
// NOT on Doge (checked in step 2), and NOT on a BuildSpot graphic itself (checked in step 3).
// Therefore, if a menu is open, this click is on "empty space" relative to that menu's purpose.
if (currentActiveBuildSpot) {
// A general spot has a menu open
var clickedOnAnotherSpotToOpenItsMenu = false; // Flag to prevent closing if the "empty space" click was actually on another buildspot
if (level && level.buildSpots) {
for (var k = 0; k < level.buildSpots.length; k++) {
if (level.buildSpots[k] !== currentActiveBuildSpot && (obj === level.buildSpots[k].graphic || isChildOf(level.buildSpots[k].graphic, obj) && obj.interactive)) {
clickedOnAnotherSpotToOpenItsMenu = true;
break;
}
}
}
if (!clickedOnAnotherSpotToOpenItsMenu) {
// Only close if not trying to open another spot's menu
if (currentActiveBuildSpot.areIconsVisible) {
// Build selection menu is open
currentActiveBuildSpot.hideSelectionIcons();
return; // CRITICAL
}
if (currentActiveBuildSpot.areActionIconsVisible) {
// Tower action menu is open
currentActiveBuildSpot.hideTowerActionIcons();
return; // CRITICAL
}
}
}
// Placeholder for closing Sell Confirmation Dialog on "empty space" click
// if (typeof sellConfirmDialog !== 'undefined' && sellConfirmDialog && sellConfirmDialog.parent && sellConfirmDialog.visible) {
// // Check if the click was outside the dialog content before closing
// if (!isChildOf(sellConfirmDialog, obj) && obj !== sellConfirmDialog) { // Basic check: not on dialog or its children
// sellConfirmDialog.destroy(); // or hide()
// return; // CRITICAL
// }
// }
// --- 5. Fallback: Move Doge ---
// If we've reached this point, no UI element was specifically interacted with to trigger an action,
// Doge wasn't clicked for dragging, no BuildSpot was clicked to toggle a menu,
// and no menu was open to be closed by an empty space click (or the click was on another spot to open its menu).
// This means the click was on the general walkable area.
if (doge) {
dragDoge = false; // Ensure not dragging if we just clicked empty space
doge.setTarget(worldX, worldY);
}
};
game.move = function (x, y, obj) {
// No doge dragging functionality needed
};
game.up = function (x, y, obj) {
// No doge dragging functionality needed
};
// Only one game.up function is needed
// Main game loop /**** MODIFIED ****/
game.update = function () {
// --- Wave Management ---
if (!isWaveActive && currentWave < MAX_WAVES) {
// Only increment timer if not won yet
waveTimer += gameSpeedMultiplier;
if (waveTimer >= WAVE_DELAY) {
waveTimer = 0; // Reset timer immediately
spawnWave();
}
}
// --- Update Bullets --- (Remove destroyed ones)
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
bullets.splice(i, 1);
}
}
// --- Update Enemies --- (Remove destroyed ones)
for (var i = enemies.length - 1; i >= 0; i--) {
if (!enemies[i].parent) {
enemies.splice(i, 1);
}
}
// --- Update Doge (handles its own cooldowns and attacks) ---
if (doge && doge.parent) {
// Check if doge exists
doge.update(); // Make sure Doge's update runs
}
// --- Check Wave Completion ---
checkWaveComplete();
// --- Update Bark Button UI --- /**** NEW ****/
if (doge) {
// Check if doge exists
if (doge.currentManualBarkCooldown > 0) {
var secondsLeft = Math.ceil(doge.currentManualBarkCooldown / 60); // Approx seconds
barkButton.setText("WAIT (" + secondsLeft + ")");
barkButton.setText("WAIT (" + secondsLeft + ")", {
fill: 0x888888
}); // Grey out text
} else {
barkButton.setText("BARK!");
barkButton.setText("BARK!", {
fill: 0xffcc00
}); // Restore color
}
}
// --- Camera Panning Logic ---
if (doge) {
var CAM_SMOOTH_FACTOR = 0.1; // Define smoothing factor
// --- Vertical Panning ---
var PAN_THRESHOLD_VERTICAL = SCREEN_HEIGHT * 0.25; // Pan when Doge in top/bottom 25%
var dogeScreenY = doge.y + game.y; // Doge's Y position on the actual screen
var targetGameY = game.y;
if (dogeScreenY < PAN_THRESHOLD_VERTICAL) {
targetGameY = -(doge.y - PAN_THRESHOLD_VERTICAL);
} else if (dogeScreenY > SCREEN_HEIGHT - PAN_THRESHOLD_VERTICAL) {
targetGameY = -(doge.y - (SCREEN_HEIGHT - PAN_THRESHOLD_VERTICAL));
}
var maxGameY = 0;
var minGameY = -(MAP_HEIGHT - SCREEN_HEIGHT);
if (MAP_HEIGHT <= SCREEN_HEIGHT) {
// Prevent scrolling if map isn't taller
minGameY = 0;
}
targetGameY = Math.max(minGameY, Math.min(maxGameY, targetGameY));
game.y += (targetGameY - game.y) * CAM_SMOOTH_FACTOR;
if (Math.abs(game.y - targetGameY) < 1) {
game.y = targetGameY;
}
// --- Horizontal Panning (NEW) ---
var PAN_THRESHOLD_HORIZONTAL = SCREEN_WIDTH * 0.30; // Pan when Doge in left/right 30%
var dogeScreenX = doge.x + game.x; // Doge's X position on the actual screen
var targetGameX = game.x;
if (dogeScreenX < PAN_THRESHOLD_HORIZONTAL) {
targetGameX = -(doge.x - PAN_THRESHOLD_HORIZONTAL);
} else if (dogeScreenX > SCREEN_WIDTH - PAN_THRESHOLD_HORIZONTAL) {
targetGameX = -(doge.x - (SCREEN_WIDTH - PAN_THRESHOLD_HORIZONTAL));
}
var maxGameX = 0;
var minGameX = -(MAP_WIDTH - SCREEN_WIDTH);
if (MAP_WIDTH <= SCREEN_WIDTH) {
// Prevent scrolling if map isn't wider
minGameX = 0;
}
targetGameX = Math.max(minGameX, Math.min(maxGameX, targetGameX));
game.x += (targetGameX - game.x) * CAM_SMOOTH_FACTOR;
if (Math.abs(game.x - targetGameX) < 1) {
game.x = targetGameX;
}
}
// --- End Camera Panning Logic ---
// --- Dynamically Update Icon Affordability ---
if (currentActiveBuildSpot && currentActiveBuildSpot.areIconsVisible) {
currentActiveBuildSpot.updateAffordability();
}
};
// Initialize the game
LK.setScore(0); // Reset score on start
initializeGame(1);
Stapler Turret Sprite Sheet: An office stapler mounted on a simple rotating base images show it opening and closing.. In-Game asset. 2d. High contrast. No shadows
Stapler bullet. In-Game asset. 2d. High contrast. No shadows
Remove the background
A stylized golden fire hydrant labeled "Free Speech" OR a glowing server rack labeled "Meme Archive".. In-Game asset. 2d. High contrast. No shadows
Paperclip. In-Game asset. 2d. High contrast. No shadows
A simple, slightly glowing circular outline indicating where towers can be placed.. In-Game asset. 2d. High contrast. No shadows
More cabinet, More Files
black circle. In-Game asset. 2d. High contrast. No shadows
DOGE Enemy Auditor. In-Game asset. 2d. High contrast. No shadows
grow the image and have papers fall from the folders
Squish the image like the cabinet is squeezing in on itself
Red Tape enemy extends as if bouncing while moving
Envelope flying through the air with wings. In-Game asset. 2d. High contrast. No shadows
"Laser Cat Perch": A cat with laser eyes that "targets" and zaps high-priority enemies with precision. (Internet loves cats).. In-Game asset. 2d. High contrast. No shadows
"Rickroller": A RickAstley tower holding a mic. In-Game asset. 2d. High contrast. No shadows
"'This Is Fine' Fire Pit": A tower resembling the "This is Fine" dog meme.. In-Game asset. 2d. High contrast. No shadows
Sell icon with a money symbol. In-Game asset. 2d. High contrast. No shadows
DOGE Coin. In-Game asset. 2d. High contrast. No shadows
Realistic MEME of Rick Astley dancing with mic. In-Game asset. 2d. High contrast. No shadows
Range Circle. In-Game asset. 2d. High contrast. No shadows
Shape: A tall, sleek, perhaps slightly intimidating rectangular or obelisk-like structure. Think modern skyscraper aesthetics scaled down. Material/Color: Polished chrome, brushed aluminum, dark grey, or a very clean white. Minimalist. Details: Maybe a single, subtly glowing slit or a small, focused lens near the top where the "restructuring energy" will eventually be directed from (though the actual effect happens on the target). Very clean lines, sharp edges. A small, almost unnoticeable corporate logo (maybe a stylized "R" or an abstract "efficiency" symbol). No visible moving parts when idle. It's about quiet, decisive power. Meme Angle: Evokes the feeling of an unapproachable, all-powerful corporate entity or a consultant's "black box" solution.. In-Game asset. 2d. High contrast. No shadows
Beam of disintegration. In-Game asset. 2d. High contrast. No shadows
Intern holding a coffee cup running 3 frames. In-Game asset. 2d. High contrast. No shadows