User prompt
Ava, we're adding Doge Coins that drop from enemies and need to be collected by Doge. Assume an asset named 'dogeCoin' is defined (e.g., LK.init.image('dogeCoin', ...)). Create a new class DogeCoin: It should be a Container.expand. Constructor (spawnX, spawnY, value): 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 = 75; // How close Doge needs to be self.lifeTime = 600; // Frames before it disappears if not collected (10s @ 60fps) Initial Spawn Animation (Scatter): Generate a random small offset: let scatterAngle = Math.random() * Math.PI * 2; let scatterDistance = 20 + Math.random() * 30; let targetX = self.x + Math.cos(scatterAngle) * scatterDistance; let targetY = self.y + Math.sin(scatterAngle) * scatterDistance; Use tween.create(self).to({ x: targetX, y: targetY }, 300 * gameSpeedMultiplier, tween.easing.quadOut).call(function() { self.isOnGround = true; }).start(); (Adjust duration/easing. 300ms). Make coins appear slightly smaller during scatter and grow to full size: self.graphic.scale.set(0.5); tween.create(self.graphic.scale).to({ x: 1.0, y: 1.0 }, 300 * gameSpeedMultiplier, tween.easing.quadOut).start(); self.update = function() {}: self.lifeTime -= gameSpeedMultiplier; if (self.lifeTime <= 0 && !self.isBeingCollected) { self.destroy(); return; } if (self.isBeingCollected) return; // Stop updates if being collected if (self.isOnGround) { self.graphic.rotation += self.spinSpeed * gameSpeedMultiplier; } // Spin when on ground Check for Doge Collection: if (self.isOnGround && doge && doge.parent) { let dx = doge.x - self.x; let dy = doge.y - self.y; if (dx*dx + dy*dy < self.collectionRadius * self.collectionRadius) { self.collect(); } } self.collect = function() {}: if (self.isBeingCollected) return; self.isBeingCollected = true; currency += self.value; currencyText.setText("$: " + currency); (Optional) LK.getSound('sfxCoinCollect').play(); spawnFloatingText("+" + self.value + "$", self.x, self.y - 20, { fill: 0xFFFF00, velocityY: -2 }); Animation to fly to UI (top of screen): Get target UI position (e.g., where currencyText is, but in game space). let targetUiGlobal = currencyText.toGlobal({x: currencyText.width / 2, y: currencyText.height / 2 }); let targetUiInGame = game.globalToLocal(targetUiGlobal); tween.create(self).to({ x: targetUiInGame.x, y: targetUiInGame.y, alpha: 0.5, 'graphic.scale.x': 0.2, 'graphic.scale.y': 0.2 }, 500 * gameSpeedMultiplier, tween.easing.quadIn).call(function() { self.destroy(); }).start(); (Adjust duration/easing, 500ms). Parenting: Coins should be added to a container that is behind enemies but in front of the ground/path. A new global coinLayer = new Container(); game.addChild(coinLayer); (added before level perhaps) would be good. Then add coins to coinLayer. For now, game.addChild(self) is fine, but their depth might be an issue. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (21 edits merged)
Please save this source code
User prompt
Ava, please revise Doge's auto-attack: damage should be applied by the projectile upon hitting the enemy, not instantly by Doge. Also, increase his attack range. Update DogeHero Class Stats: In the DogeHero class, change self.autoAttackRange from its current value to 350. Modify DogeAutoProjectile Class: Add a property self.damage = 0; in its constructor. This will be set when Doge creates the projectile. In its self.update = function() {}: When the projectile "hits" the target (i.e., distance < travelSpeed or very close, e.g., distance < self.target.graphic.width / 2): Before self.destroy(), add the line: if (self.target && self.target.parent) { self.target.takeDamage(self.damage); } Ensure it also destroys itself if !self.target or !self.target.parent. Modify DogeHero.update() Auto-Attack Logic: Remove the line closestEnemy.takeDamage(self.autoAttackDamage);. Damage will now be handled by the projectile. When creating a new DogeAutoProjectile(): After setting projectile.target = closestEnemy;, add the line: projectile.damage = self.autoAttackDamage; (to pass Doge's damage stat to the projectile instance). The sound LK.getSound('dogeAutoAttack').play(); should still play when Doge launches the projectile. This ensures the visual projectile delivers the damage when it connects with the enemy, and Doge's attack range is further increased."
User prompt
Ava, please modify Doge's auto-attack capabilities and add a visual effect. Update DogeHero Class Stats: In the DogeHero class, change self.autoAttackRange from 180 to 280. Change self.autoAttackDamage from 2 to 1. Create a New Projectile Class DogeAutoProjectile: Define this new class. It should be a Container.expand. In its constructor: It should attach the asset 'basicAttackProjectile' (your existing shape). Initialize self.speed = 15; Initialize self.target = null; Initialize self.duration = 20; (frames, for self-destruction if target is lost). Its self.update = function() {} should: Decrement self.duration by gameSpeedMultiplier. If duration <= 0, self.destroy(). If self.target is null or not in parent: Optionally fade out (self.alpha -= 0.1 * gameSpeedMultiplier; if (self.alpha <= 0) self.destroy();). Or simply self.destroy() if it has no target. Move towards self.target.x, self.target.y at self.speed * gameSpeedMultiplier. If it reaches (or is very close to) the target, self.destroy() (damage is applied instantly by Doge). Modify DogeHero.update() for Auto-Attack: The existing logic for finding closestEnemy is mostly fine. Ensure the distance calculation uses Doge's self.x, self.y (which are already in game space, same as enemies) against enemy.x, enemy.y. When closestEnemy is found and Doge attacks: The line closestEnemy.takeDamage(self.autoAttackDamage); should remain (damage is instant). The LK.getSound('dogeAutoAttack').play(); should remain. self.currentAutoAttackCooldown should be reset. NEW: After the above, create an instance of DogeAutoProjectile. Set projectile.x = self.x; (Doge's current X in game space). Set projectile.y = self.y; (Doge's current Y in game space). Set projectile.target = closestEnemy; Add the projectile to the game container: game.addChild(projectile); This will make Doge attack from further away, do less damage per hit, and have a visual "zap" or projectile moving towards the enemy he attacks.
Code edit (1 edits merged)
Please save this source code
Code edit (6 edits merged)
Please save this source code
User prompt
Please fix the bug: 'buildSpots is not defined' in or related to this line: 'level.createBuildSpots(buildSpots);' Line Number: 2461
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'TypeError: game.globalToLocal is not a function' in or related to this line: 'var towerCenterInGameSpace = game.globalToLocal(towerGraphicGlobalCenter);' Line Number: 1767
User prompt
Ava, thank you for clarifying how toGlobal() and globalToLocal() work. It seems the issue is ensuring all comparisons and placements happen in a consistent coordinate space. Let's use the game container's local space as our reference 'world space' for all tower-enemy interactions and projectile spawning. Please modify the following: For ALL tower update() methods where enemy detection occurs (e.g., StaplerTower, BureaucracyBlockerTower, and eventually LaserCatPerch, RickrollerTrap, RestructuringSpecialist): To determine the tower's center for range checking: First, get the true global/stage coordinates of the tower's graphic center: var towerGraphicGlobalCenter = self.graphic.toGlobal({ x: 0, y: 0 }); Then, convert this global point to be local to the game container: var towerCenterInGameSpace = game.globalToLocal(towerGraphicGlobalCenter); When calculating distance to an enemy (whose enemy.x, enemy.y are already local to game): Use dx = enemy.x - towerCenterInGameSpace.x; Use dy = enemy.y - towerCenterInGameSpace.y; For StaplerTower.shoot() (and any future projectile-spawning tower methods): To determine the bullet's spawn position (bullets are children of game): Get the tower's graphic center in true global/stage coordinates: var towerGraphicGlobalCenter = self.graphic.toGlobal({ x: 0, y: 0 }); Convert this point to be local to the game container: var bulletSpawnPosInGameSpace = game.globalToLocal(towerGraphicGlobalCenter); Set bullet.x = bulletSpawnPosInGameSpace.x; Set bullet.y = bulletSpawnPosInGameSpace.y; This ensures that the tower's logical center for range detection and projectile spawning is correctly calculated relative to the game container, which is the same space where enemy positions are defined and where the visual range indicator effectively appears.
User prompt
Ava, there's a systemic issue with towers. The visual range indicators and actual effect/targeting areas are misaligned, especially when the game scrolls. This affects StaplerTower's targeting/shooting and BureaucracyBlockerTower's slowing area. Enemies are children of the game container, which scrolls. Towers are nested within BuildSpot -> level -> game. Please ensure that for all towers (StaplerTower, BureaucracyBlockerTower, and the new ones when their logic is added): Target/Enemy Detection in update() methods: The tower's effective center position for range calculation must be its position transformed into the game container's coordinate space. Let this be towerPosInGame. It can be found by taking the tower's visual graphic's global origin (tower.graphic.toGlobal({x:0,y:0})) and then converting that point to be local to the game container (e.g., game.globalToLocal(globalPoint) if available, or manually y = globalPoint.y - game.y). Distances to enemies (whose enemy.x, enemy.y are already local to game) should then be calculated using towerPosInGame. dx = enemy.x - towerPosInGame.x; dy = enemy.y - towerPosInGame.y; Projectile Spawning in StaplerTower.shoot() (and similar for LaserCatPerch later): Bullets (and other projectiles) are added as children to the game container. Their initial bullet.x and bullet.y must be set to towerPosInGame.x and towerPosInGame.y (calculated as described in point 1). The goal is to have all position-based calculations (range checks, projectile origins) use coordinates relative to the same consistent 'world' space, which is the coordinate system of the game container. The visual range indicators appear to be correctly positioned relative to this game world already because they are children of level, which is a child of game.
User prompt
Fix it
User prompt
Ava, the bullets from the StaplerTower are not originating from the tower's visual location, especially when the game view is scrolled. This suggests an issue with coordinate spaces when spawning bullets. Does the main game container (the one that has its game.y property changed for scrolling and is the parent of spawned bullets) have a method to convert global (world) coordinates to its own local coordinates? For example, a method like game.globalToLocal(point) where point is {x, y}. If such a method exists, please modify StaplerTower.shoot(): First, get the tower's global position (e.g., var towerGlobalPos = self.parent.toGlobal({ x: 0, y: 0 }); assuming self.parent is the BuildSpot). Then, convert towerGlobalPos to coordinates local to the game container using game.globalToLocal(). Set the bullet.x and bullet.y to these new local coordinates before adding the bullet to the game container. If game.globalToLocal() does not exist, please modify StaplerTower.shoot() to manually calculate the bullet's spawn coordinates relative to the game container. Assuming game.x is always 0 and only game.y changes for scrolling: Get the tower's global position: var towerGlobalPos = self.parent.toGlobal({ x: 0, y: 0 }); Set bullet.x = towerGlobalPos.x; Set bullet.y = towerGlobalPos.y - game.y; Then add the bullet to the game container. The goal is for bullet.x and bullet.y to be the correct coordinates within the game container's own space so that the bullets visually originate from the tower, regardless of the current game.y scroll position."
User prompt
Ava, please add an entry for a new tower type, the 'Restructuring Specialist', to the TOWER_DATA global variable. This will be our 6th tower. // Add this to the existing TOWER_DATA object: 'restructSpecialist': { name: 'Restructuring Specialist', iconAsset: 'towerRestructLvl1', // Use Lvl1 asset as the build icon buildSfx: 'buildRestruct', // Define this sound levels: [ { asset: 'towerRestructLvl1', cost: 200, damage: 100, // High single-target damage instantKillThreshold: 25, // Health below this is an instant kill range: 450, // Decent range chargeUpTime: 240, // Frames (4 seconds at 60fps) for "consultation" targetPriority: 'strongest', // Optional: Could target strongest, weakest, first, etc. effectAsset: 'restructureEffect', // Visual for the "restructuring" description: "Optimizing threats... permanently." }, // Lvl 2: "Synergy Realignment" (faster charge time) { asset: 'towerRestructLvl1', // Placeholder, use Lvl2 asset when ready cost: 300, damage: 150, instantKillThreshold: 35, range: 475, chargeUpTime: 180, // Faster targetPriority: 'strongest', effectAsset: 'restructureEffect', description: "Synergistic realignment for enhanced efficiency." }, // Lvl 3: "Golden Parachute" (deals AoE damage on kill/transform) // OR "Hostile Restructuring" (chance to hit second target) - Let's do Golden Parachute for now { asset: 'towerRestructLvl1', // Placeholder, use Lvl3 asset when ready cost: 450, damage: 200, instantKillThreshold: 50, range: 500, chargeUpTime: 150, targetPriority: 'strongest', effectAsset: 'restructureEffect', goldenParachuteDamage: 25, // AoE damage goldenParachuteRadius: 100, // AoE radius description: "Downsizing comes with... severance packages." } ] } Use code with caution. JavaScript Ensure this is added as a new top-level key within the TOWER_DATA object."
User prompt
add these assets for now: // New Tower - Restructuring Specialist LK.init.image('towerRestructLvl1', {width: 100, height: 130, id:'PLACEHOLDER_RESTRUCT_L1_ID'}); // (Optional) Effect for "restructuring" LK.init.image('restructureEffect', {width: 80, height: 80, id:'PLACEHOLDER_RESTRUCT_EFFECT_ID'}); // (Optional) Sounds LK.init.sound('buildRestruct'); LK.init.sound('sfxRestructCharge'); LK.init.sound('sfxRestructFire'); LK.init.sound('sfxRestructTransformKill');
Code edit (1 edits merged)
Please save this source code
User prompt
Increase Icon Display Size in Build Menu: In the BuildSpot.showSelectionIcons method, when the iconGraphic is created using self.selectionIconContainer.attachAsset(...): Add scaleX: 1.3, Add scaleY: 1.3, to its properties. This will make the displayed icons 30% larger.
Code edit (1 edits merged)
Please save this source code
User prompt
Ava, modify the BuildSpot.buildSelectedTower(towerTypeKey) method. Add else if conditions to handle the construction of the new tower types: // Inside BuildSpot.buildSelectedTower, after existing checks: // ... var newTower; if (towerTypeKey === 'stapler') { newTower = new StaplerTower(); } else if (towerTypeKey === 'blocker') { newTower = new BureaucracyBlockerTower(); } else if (towerTypeKey === 'laserCat') { // ADD THIS newTower = new LaserCatPerch(); } else if (towerTypeKey === 'rickroller') { // ADD THIS newTower = new RickrollerTrap(); } else if (towerTypeKey === 'thisIsFine') { // ADD THIS newTower = new ThisIsFinePit(); } // ... rest of the function remains the same ...
User prompt
Ava, create a new tower class named ThisIsFinePit. It should be a Container.expand. Its constructor should call initializeTowerFromData(self, 'thisIsFine', 0);. Add a basic self.update = function() {}; for now (it will eventually check for nearby enemies and buff towers). Ensure it has upgrade functionality."
User prompt
Ava, create a new tower class named RickrollerTrap. It should be a Container.expand. Its constructor should call initializeTowerFromData(self, 'rickroller', 0);. Add a basic self.update = function() {}; for now (it will eventually check for enemies in range). Ensure it has upgrade functionality."
User prompt
Ava, create a new tower class named LaserCatPerch. It should be a Container.expand. Its constructor should call initializeTowerFromData(self, 'laserCat', 0);. Add a basic self.update = function() {}; for now. Add a basic self.shoot = function(target) {}; for now (it will eventually fire a laser). Ensure it has the addUpgradeToTower(self) functionality like other towers (usually handled by initializeTowerFromData if it calls addUpgradeToTower).
User prompt
Ava, please update the ICON_SELECT_OFFSETS global array to accommodate 6 tower types, including the three new ones: 'laserCat', 'rickroller', and 'thisIsFine'. Arrange them in a hexagonal pattern around the BuildSpot. Replace the current ICON_SELECT_OFFSETS with this: var ICON_SELECT_OFFSETS = [ { x: -120, y: -70, towerKey: 'stapler' }, // Top-Left { x: 0, y: -140, towerKey: 'blocker' }, // Top-Center { x: 120, y: -70, towerKey: 'laserCat' }, // Top-Right { x: -120, y: 70, towerKey: 'rickroller' }, // Bottom-Left { x: 0, y: 140, towerKey: 'thisIsFine' }, // Bottom-Center { x: 120, y: 70, towerKey: 'placeholder'} // Bottom-Right (or another new tower later) ]; Use code with caution. JavaScript This will require the placeholder entry in TOWER_DATA to still exist if you keep it as the 6th icon. If you want to replace placeholder with one of the new towers immediately, adjust the towerKey for the 6th slot."
User prompt
Ava, please add entries for three new tower types to the TOWER_DATA global variable. Use the following structure and placeholder stats for Level 1. We will implement their unique mechanics later. // Add these to the existing TOWER_DATA object: 'laserCat': { name: 'Laser Cat Perch', iconAsset: 'towerLaserCatLvl1', // Use Lvl1 asset as the build icon for now buildSfx: 'buildLaserCat', // Define this sound levels: [ { asset: 'towerLaserCatLvl1', cost: 150, damage: 25, // High single-target damage range: 500, // Long range fireRate: 120, // Slow fire rate (frames) projectileAsset: 'laserBeam', // Custom projectile/effect asset description: "Pew pew! Precision feline firepower." } // We can add Lvl 2 & 3 later ] }, 'rickroller': { name: 'Rickroller Trap', iconAsset: 'towerRickrollerLvl1', buildSfx: 'buildRickroller', // Define this sound levels: [ { asset: 'towerRickrollerLvl1', cost: 100, damagePerTick: 0.5, // Small damage over time while stunned range: 150, // Activation radius for the trap stunDuration: 120, // Frames (2 seconds at 60fps) effectDuration: 180,// How long the Rick Astley popup stays popupAsset: 'rickAstleyPopup', soundEffect: 'sfxRickrollTrigger', // The crucial sound! description: "Never gonna let them pass!" } ] }, 'thisIsFine': { name: "'This Is Fine' Fire Pit", iconAsset: 'towerThisIsFineLvl1', buildSfx: 'buildThisIsFine', // Define this sound levels: [ { asset: 'towerThisIsFineLvl1', cost: 125, range: 250, // 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." } ] } Use code with caution. JavaScript Ensure these are added as new top-level keys within the TOWER_DATA object, alongside 'stapler' and 'blocker'." Also, create assets for all of the towers // New Tower Base Assets (Level Visuals) LK.init.image('towerLaserCatLvl1', {width: 100, height: 120, id:'PLACEHOLDER_LASERCAT_L1_ID'}); LK.init.image('towerRickrollerLvl1', {width: 120, height: 90, id:'PLACEHOLDER_RICKROLLER_L1_ID'}); LK.init.image('towerThisIsFineLvl1', {width: 110, height: 110, id:'PLACEHOLDER_THISISFINE_L1_ID'}); // Assets for their projectiles or effects LK.init.image('laserBeam', {width: 10, height: 300 // For Laser Cat LK.init.image('rickAstleyPopup', {width: 150, height: 150, // For Rickroller // Level 2 & 3 assets for these towers // LK.init.image('towerLaserCatLvl2', ...); // LK.init.image('towerRickrollerLvl2', ...); // LK.init.image('towerThisIsFineLvl2', ...); // LK.init.image('towerLaserCatLvl3', ...); // LK.init.image('towerRickrollerLvl3', ...); // LK.init.image('towerThisIsFineLvl3', ...); // Placeholder Sounds (if you want specific ones) LK.init.sound('sfxLaserCatFire'); LK.init.sound('sfxRickrollTrigger'); // "Never Gonna Give You Up" snippet! LK.init.sound('sfxThisIsFineBuff'); LK.init.sound('buildLaserCat'); LK.init.sound('buildRickroller'); LK.init.sound('buildThisIsFine');
User prompt
Ava, the 'remove and re-attach asset' animation method is now working correctly for the AuditorBot class (and was already working for the Enemy class). Please apply this exact same animation logic pattern to the update methods of the following enemy classes: InternOnCoffeeRun RedTapeWorm SpamEmailUnit For each of these classes, ensure their self.update() method includes the following animation logic block (replacing any existing setTexture() animation logic): // --- 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 newAssetIdToAttach = self.walkFrames[self.currentAnimFrame]; if (self.graphic && self.graphic.parent) { // Preserve local position if self.graphic is a direct child of self var currentLocalX = self.graphic.x; var currentLocalY = self.graphic.y; self.removeChild(self.graphic); // Or self.graphic.destroy(); if that's more appropriate self.graphic = self.attachAsset(newAssetIdToAttach, { anchorX: 0.5, anchorY: 0.5, x: currentLocalX, // Restore local x y: currentLocalY // Restore local y }); } else if (!self.graphic && self.parent) { // If graphic was lost but enemy still exists self.graphic = self.attachAsset(newAssetIdToAttach, { anchorX: 0.5, anchorY: 0.5 }); } } } // --- End Animation Logic --- Use code with caution. JavaScript The constructor part of these classes (where self.walkFrames, self.currentAnimFrame, self.animationSpeed, self.animationCounter are initialized, and self.graphic is first attached using self.walkFrames[0]) should already be set up correctly from our previous instructions. This prompt is specifically for updating their self.update() method's animation block to use the 'remove and re-attach' pattern. Please ensure the local x and y of the graphic are preserved if it's a direct child of self (the enemy container), as shown in the provided code snippet. If self.graphic isn't a direct child, or if self.attachAsset adds it to self by default in a way that x:0, y:0 is appropriate after self.removeChild(self.graphic), adjust accordingly."
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { highScore: 0 }); /**** * Classes ****/ var AuditorBot = Container.expand(function () { var self = Container.call(this); self.walkFrames = ['enemyAuditorBot', 'enemyAuditorBot_walk_1', 'enemyAuditorBot_walk_2']; // Animation frames self.currentAnimFrame = 0; self.animationSpeed = 8; self.animationCounter = 0; self.graphic = self.attachAsset(self.walkFrames[0], { anchorX: 0.5, anchorY: 0.5 }); self.health = 30; // High health self.speed = 1.0; // Slow speed self.value = 30; // High currency reward self.isFlying = false; self.currentPathIndex = 0; self.update = function () { // Check if pathPoints is loaded and valid if (!pathPoints || pathPoints.length === 0) { return; } if (self.currentPathIndex < pathPoints.length) { var target = pathPoints[self.currentPathIndex]; if (!target) { // Safety check self.currentPathIndex++; return; } var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Use a slightly larger threshold for path progression if (distance < self.speed * 1.5) { // Adjusted threshold self.currentPathIndex++; // Check if enemy reached the goal if (self.currentPathIndex >= pathPoints.length) { playerLives--; livesText.setText("Lives: " + playerLives); LK.getSound('enemyReachGoal').play(); if (playerLives <= 0) { // Game over if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } LK.showGameOver(); } self.destroy(); return; } } else { // Move towards the target point dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; } } else { // If somehow past the end of path but not destroyed, remove it console.log("Enemy past end of path, destroying."); self.destroy(); } // --- Animation Logic --- if (self.walkFrames && self.walkFrames.length > 1) { // Only animate if there's more than one frame self.animationCounter += gameSpeedMultiplier; // Respect game speed if (self.animationCounter >= self.animationSpeed) { self.animationCounter = 0; self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length; // Use remove and re-attach method for animation frames var newAsset = self.walkFrames[self.currentAnimFrame]; if (self.graphic && self.graphic.parent) { self.graphic.parent.removeChild(self.graphic); self.graphic = self.attachAsset(newAsset, { anchorX: 0.5, anchorY: 0.5 }); } } } // --- End Animation Logic --- }; self.takeDamage = function (amount) { self.health -= amount; // Flash red when taking damage LK.effects.flashObject(self, 0xff0000, 200); if (self.health <= 0 && self.parent) { // Added check for self.parent before accessing currency currency += self.value; currencyText.setText("$: " + currency); LK.setScore(LK.getScore() + self.value); scoreText.setText("Score: " + LK.getScore()); LK.getSound('enemyDeath').play(); self.destroy(); } }; return self; }); 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 = 3; // 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 RickrollerTrap(); } else if (towerTypeKey === 'thisIsFine') { newTower = new ThisIsFinePit(); } 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; // Get tower's position in game space where enemies exist var towerGamePos = { x: buildSpot.x, y: buildSpot.y }; 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 - towerGamePos.x; var dy = enemy.y - towerGamePos.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; }); // 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 = 180; self.autoAttackDamage = 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) { closestEnemy.takeDamage(self.autoAttackDamage); LK.getSound('dogeAutoAttack').play(); self.currentAutoAttackCooldown = self.autoAttackCooldownTime; } } }; 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; }); // Enemy class remains largely the same var Enemy = Container.expand(function () { var self = Container.call(this); self.walkFrames = ['enemyPaper', 'enemyPaper_walk_1', 'enemyPaper_walk_2']; // Animation frames self.currentAnimFrame = 0; self.animationSpeed = 15; // Increased from 10 to slow animation further self.animationCounter = 0; self.graphic = self.attachAsset(self.walkFrames[0], { anchorX: 0.5, anchorY: 0.5 }); self.health = 3; self.speed = 2; self.value = 10; // Currency earned when killed self.currentPathIndex = 0; self.update = function () { // Check if pathPoints is loaded and valid if (!pathPoints || pathPoints.length === 0) { return; } if (self.currentPathIndex < pathPoints.length) { var target = pathPoints[self.currentPathIndex]; if (!target) { // Safety check self.currentPathIndex++; return; } var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Use a slightly larger threshold for path progression if (distance < self.speed * 1.5) { // Adjusted threshold self.currentPathIndex++; // Check if enemy reached the goal if (self.currentPathIndex >= pathPoints.length) { playerLives--; livesText.setText("Lives: " + playerLives); LK.getSound('enemyReachGoal').play(); if (playerLives <= 0) { // Game over if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } LK.showGameOver(); } self.destroy(); return; } } else { // Move towards the target point dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; } } else { // If somehow past the end of path but not destroyed, remove it console.log("Enemy past end of path, destroying."); self.destroy(); } // --- Animation Logic --- if (self.walkFrames && self.walkFrames.length > 1) { // Only animate if there's more than one frame self.animationCounter += gameSpeedMultiplier; // Respect game speed if (self.animationCounter >= self.animationSpeed) { self.animationCounter = 0; self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length; // Replace the graphic with the new texture by getting a fresh asset var newAsset = self.walkFrames[self.currentAnimFrame]; if (self.graphic && self.graphic.parent) { self.graphic.parent.removeChild(self.graphic); self.graphic = self.attachAsset(newAsset, { anchorX: 0.5, anchorY: 0.5 }); } } } // --- End Animation Logic --- }; self.takeDamage = function (amount) { self.health -= amount; // Flash red when taking damage LK.effects.flashObject(self, 0xff0000, 200); if (self.health <= 0 && self.parent) { // Added check for self.parent before accessing currency currency += self.value; currencyText.setText("$: " + currency); LK.setScore(LK.getScore() + self.value); scoreText.setText("Score: " + LK.getScore()); LK.getSound('enemyDeath').play(); self.destroy(); } }; return self; }); // GameLevel class remains the same var GameLevel = Container.expand(function () { var self = Container.call(this); var pathGraphics = []; // Store path visuals var buildSpotGraphics = []; // Store build spot visuals self.createPath = function (pathData) { // Clear previous path graphics pathGraphics.forEach(function (tile) { tile.destroy(); }); pathGraphics = []; // Assume pathData is the pathPoints array for (var i = 0; i < pathData.length - 1; i++) { var start = pathData[i]; var end = pathData[i + 1]; if (!start || !end) { continue; } // Safety check // Calculate direction and distance var dx = end.x - start.x; var dy = end.y - start.y; var distance = Math.sqrt(dx * dx + dy * dy); // Adjust tile spacing slightly var steps = Math.ceil(distance / 90); // Slightly closer tiles 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, // Make path fainter anchorX: 0.5, anchorY: 0.5 }); self.addChild(tile); pathGraphics.push(tile); // Store reference } } }; self.createBuildSpots = function (spotsData) { // Clear previous build spots buildSpotGraphics.forEach(function (spot) { spot.destroy(); }); buildSpotGraphics = []; self.buildSpots = []; // Clear the logical array too for (var i = 0; i < spotsData.length; i++) { if (!spotsData[i]) { continue; } // Safety check var spot = new BuildSpot(); spot.x = spotsData[i].x; spot.y = spotsData[i].y; self.addChild(spot); self.buildSpots.push(spot); // Store logical spot buildSpotGraphics.push(spot); // Store graphical spot } }; return self; }); // 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; }); var InternOnCoffeeRun = Container.expand(function () { var self = Container.call(this); self.walkFrames = ['enemyIntern', 'enemyIntern_walk_1', 'enemyIntern_walk_2']; // Animation frames self.currentAnimFrame = 0; self.animationSpeed = 5; // Faster animation for fast enemy self.animationCounter = 0; self.graphic = self.attachAsset(self.walkFrames[0], { anchorX: 0.5, anchorY: 0.5 }); self.health = 2; // Low health self.speed = 4.0; // Fast speed self.value = 5; // Low currency reward self.isFlying = false; self.currentPathIndex = 0; self.update = function () { // Check if pathPoints is loaded and valid if (!pathPoints || pathPoints.length === 0) { return; } if (self.currentPathIndex < pathPoints.length) { var target = pathPoints[self.currentPathIndex]; if (!target) { // Safety check self.currentPathIndex++; return; } var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Use a slightly larger threshold for path progression if (distance < self.speed * 1.5) { // Adjusted threshold self.currentPathIndex++; // Check if enemy reached the goal if (self.currentPathIndex >= pathPoints.length) { playerLives--; livesText.setText("Lives: " + playerLives); LK.getSound('enemyReachGoal').play(); if (playerLives <= 0) { // Game over if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } LK.showGameOver(); } self.destroy(); return; } } else { // Move towards the target point dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; } } else { // If somehow past the end of path but not destroyed, remove it console.log("Enemy past end of path, destroying."); self.destroy(); } // --- Animation Logic --- if (self.walkFrames && self.walkFrames.length > 1) { // Only animate if there's more than one frame self.animationCounter += gameSpeedMultiplier; // Respect game speed if (self.animationCounter >= self.animationSpeed) { self.animationCounter = 0; self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length; var newAssetIdToAttach = self.walkFrames[self.currentAnimFrame]; if (self.graphic && self.graphic.parent) { // Preserve local position if self.graphic is a direct child of self var currentLocalX = self.graphic.x; var currentLocalY = self.graphic.y; self.removeChild(self.graphic); // Or self.graphic.destroy(); if that's more appropriate self.graphic = self.attachAsset(newAssetIdToAttach, { anchorX: 0.5, anchorY: 0.5, x: currentLocalX, // Restore local x y: currentLocalY // Restore local y }); } else if (!self.graphic && self.parent) { // If graphic was lost but enemy still exists self.graphic = self.attachAsset(newAssetIdToAttach, { anchorX: 0.5, anchorY: 0.5 }); } } } // --- End Animation Logic --- }; self.takeDamage = function (amount) { self.health -= amount; // Flash red when taking damage LK.effects.flashObject(self, 0xff0000, 200); if (self.health <= 0 && self.parent) { // Added check for self.parent before accessing currency currency += self.value; currencyText.setText("$: " + currency); LK.setScore(LK.getScore() + self.value); scoreText.setText("Score: " + LK.getScore()); LK.getSound('enemyDeath').play(); self.destroy(); } }; return self; }); var LaserCatPerch = Container.expand(function () { var self = Container.call(this); // Initialize the tower with level 0 stats and graphics initializeTowerFromData(self, 'laserCat', 0); self.update = function () { // Basic update function to be expanded later }; self.shoot = function (target) { // Basic shoot function to be expanded later }; 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; }); // --- NEW ENEMY: RedTapeWorm --- var RedTapeWorm = Container.expand(function () { var self = Container.call(this); // Inherit from Enemy - this copies properties and methods if Enemy is set up for it. // If Enemy is not a true prototypal base, we'll redefine common things. // For simplicity, let's assume Enemy provides a good base or we'll set manually. // Call parent constructor if applicable // Override or set specific properties self.walkFrames = ['enemyRedTapeWorm', 'enemyRedTapeWorm_walk_1', 'enemyRedTapeWorm_walk_2']; // Animation frames self.currentAnimFrame = 0; self.animationSpeed = 10; // Slower animation for slow enemy self.animationCounter = 0; self.graphic = self.attachAsset(self.walkFrames[0], { anchorX: 0.5, anchorY: 0.5 }); // Use new asset self.health = 20; // High health self.speed = 0.75; // Very slow speed self.value = 25; // More currency // Optional: Custom sound on spawn or movement // LK.getSound('tapeStretch').play(); // (Could be spammy if played on update) // The base Enemy.update() and Enemy.takeDamage() should work if inherited. // If not, you'd copy and paste that logic here, adjusting as needed. // For now, assume base Enemy update handles pathing and goal reaching. // Custom onDefeat behavior if needed (e.g., spawn smaller tapes - too complex for now) self.update = function () { // Check if pathPoints is loaded and valid if (!pathPoints || pathPoints.length === 0) { return; } if (self.currentPathIndex < pathPoints.length) { var target = pathPoints[self.currentPathIndex]; if (!target) { // Safety check self.currentPathIndex++; return; } var dx = target.x - self.x; var dy = target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Use a slightly larger threshold for path progression if (distance < self.speed * 1.5) { // Adjusted threshold self.currentPathIndex++; // Check if enemy reached the goal if (self.currentPathIndex >= pathPoints.length) { playerLives--; livesText.setText("Lives: " + playerLives); LK.getSound('enemyReachGoal').play(); if (playerLives <= 0) { // Game over if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } LK.showGameOver(); } self.destroy(); return; } } else { // Move towards the target point dx = dx / distance * self.speed * gameSpeedMultiplier; dy = dy / distance * self.speed * gameSpeedMultiplier; self.x += dx; self.y += dy; } } else { // If somehow past the end of path but not destroyed, remove it console.log("Enemy past end of path, destroying."); self.destroy(); } // --- Animation Logic --- if (self.walkFrames && self.walkFrames.length > 1) { // Only animate if there's more than one frame self.animationCounter += gameSpeedMultiplier; // Respect game speed if (self.animationCounter >= self.animationSpeed) { self.animationCounter = 0; self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length; var newAssetIdToAttach = self.walkFrames[self.currentAnimFrame]; if (self.graphic && self.graphic.parent) { // Preserve local position if self.graphic is a direct child of self var currentLocalX = self.graphic.x; var currentLocalY = self.graphic.y; self.removeChild(self.graphic); // Or self.graphic.destroy(); if that's more appropriate self.graphic = self.attachAsset(newAssetIdToAttach, { anchorX: 0.5, anchorY: 0.5, x: currentLocalX, // Restore local x y: currentLocalY // Restore local y }); } else if (!self.graphic && self.parent) { // If graphic was lost but enemy still exists self.graphic = self.attachAsset(newAssetIdToAttach, { anchorX: 0.5, anchorY: 0.5 }); } } } // --- End Animation Logic --- }; self.takeDamage = function (amount) { self.health -= amount; // Flash red when taking damage LK.effects.flashObject(self, 0xff0000, 200); if (self.health <= 0 && self.parent) { // Added check for self.parent before accessing currency currency += self.value; currencyText.setText("$: " + currency); LK.setScore(LK.getScore() + self.value); scoreText.setText("Score: " + LK.getScore()); LK.getSound('enemyDeath').play(); self.destroy(); } }; var originalDestroy = self.destroy; // Keep a reference to base destroy self.destroy = function () { // spawnFloatingText("So Bureaucratic!", self.x, self.y - 50, { fill: 0xFF6666 }); // Example originalDestroy.call(self); // Call the original destroy method }; return self; }); var RestructuringSpecialist = Container.expand(function () { var self = Container.call(this); // Initialize the tower with level 0 stats and graphics initializeTowerFromData(self, 'restruct', 0); self.chargeLevel = 0; self.maxCharge = 100; self.isCharging = false; self.targetEnemy = null; self.chargeRate = 1; self.update = function () { // Check if tower has a target and is charging if (self.isCharging && self.targetEnemy) { // Check if target is still valid if (!self.targetEnemy.parent) { self.isCharging = false; self.targetEnemy = null; self.chargeLevel = 0; return; } // Increase charge self.chargeLevel += self.chargeRate * gameSpeedMultiplier; // If fully charged, restructure the enemy if (self.chargeLevel >= self.maxCharge) { self.restructureEnemy(); } } else { // Find new target self.findTarget(); } }; self.findTarget = function () { if (self.isCharging) return; var buildSpot = self.parent; if (!buildSpot || !buildSpot.parent) return; var levelContainer = buildSpot.parent; // Get tower position in game space where enemies exist var towerPos = { x: buildSpot.x, y: buildSpot.y }; 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 - towerPos.x; var dy = enemy.y - towerPos.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < minDistanceSq) { minDistanceSq = distanceSq; closestEnemy = enemy; } } if (closestEnemy) { self.targetEnemy = closestEnemy; self.isCharging = true; LK.getSound('sfxRestructCharge').play(); } }; 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 RickrollerTrap = Container.expand(function () { var self = Container.call(this); // Initialize the tower with level 0 stats and graphics initializeTowerFromData(self, 'rickroller', 0); self.update = function () { // Basic update function to be expanded later (will check for enemies in range) }; return self; }); var SpamEmailUnit = Container.expand(function () { var self = Container.call(this); self.walkFrames = ['enemySpamEmail', 'enemySpamEmail_walk_1', 'enemySpamEmail_walk_2']; // Animation frames self.currentAnimFrame = 0; self.animationSpeed = 6; self.animationCounter = 0; self.graphic = self.attachAsset(self.walkFrames[0], { anchorX: 0.5, anchorY: 0.5 }); self.health = 1; // Very low health self.speed = 2.5; // Moderately fast self.value = 1; // Very low currency reward self.isFlying = true; // Mark as flying self.update = function () { // Check if pathPoints is loaded and valid if (!pathPoints || pathPoints.length === 0) { return; } // Flying units go directly to the goal (last point in path) var target = pathPoints[pathPoints.length - 1]; 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); // Check if reached the goal if (distance < self.speed * 1.5) { playerLives--; livesText.setText("Lives: " + playerLives); LK.getSound('enemyReachGoal').play(); if (playerLives <= 0) { // Game over if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } LK.showGameOver(); } self.destroy(); return; } else { // Move directly towards the goal 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) { // Only animate if there's more than one frame self.animationCounter += gameSpeedMultiplier; // Respect game speed if (self.animationCounter >= self.animationSpeed) { self.animationCounter = 0; self.currentAnimFrame = (self.currentAnimFrame + 1) % self.walkFrames.length; var newAssetIdToAttach = self.walkFrames[self.currentAnimFrame]; if (self.graphic && self.graphic.parent) { // Preserve local position if self.graphic is a direct child of self var currentLocalX = self.graphic.x; var currentLocalY = self.graphic.y; self.removeChild(self.graphic); // Or self.graphic.destroy(); if that's more appropriate self.graphic = self.attachAsset(newAssetIdToAttach, { anchorX: 0.5, anchorY: 0.5, x: currentLocalX, // Restore local x y: currentLocalY // Restore local y }); } else if (!self.graphic && self.parent) { // If graphic was lost but enemy still exists self.graphic = self.attachAsset(newAssetIdToAttach, { anchorX: 0.5, anchorY: 0.5 }); } } } // --- End Animation Logic --- }; self.takeDamage = function (amount) { self.health -= amount; // Flash red when taking damage LK.effects.flashObject(self, 0xff0000, 200); if (self.health <= 0 && self.parent) { // Added check for self.parent before accessing currency currency += self.value; currencyText.setText("$: " + currency); LK.setScore(LK.getScore() + self.value); scoreText.setText("Score: " + LK.getScore()); LK.getSound('enemyDeath').play(); self.destroy(); } }; return self; }); // --- 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 // Tower position in the same coordinate space as enemies var towerPos = { x: buildSpot.x, y: buildSpot.y }; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - towerPos.x; // Use tower position in game space var dy = enemy.y - towerPos.y; // Use tower position in game space 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 towerGlobalPos = levelContainer.toGlobal(buildSpot.position); bullet.x = towerGlobalPos.x; bullet.y = towerGlobalPos.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 the tower with level 0 stats and graphics initializeTowerFromData(self, 'thisIsFine', 0); self.update = function () { // Basic update function to be expanded later (will check for nearby enemies and buff towers) }; 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 ****/ // This isn't a formal class, but concepts to apply // --- 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 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: 75, damage: 2, range: 420, fireRate: 55, description: "Improved Firepower" }, { asset: 'towerStaplerLvl3', cost: 125, damage: 3, 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: 100, slowFactor: 0.4, range: 350, description: "Wider, stronger slow." }, // 0.4 means 60% speed reduction { asset: 'towerBlockerLvl3', cost: 150, slowFactor: 0.3, range: 400, description: "Bureaucratic Gridlock!" }] }, 'laserCat': { name: 'Laser Cat Perch', iconAsset: 'towerLaserCatLvl1', // Use Lvl1 asset as the build icon for now buildSfx: 'buildLaserCat', // Define this sound levels: [{ asset: 'towerLaserCatLvl1', cost: 150, damage: 25, // High single-target damage range: 500, // Long range fireRate: 120, // Slow fire rate (frames) projectileAsset: 'laserBeam', // Custom projectile/effect asset description: "Pew pew! Precision feline firepower." } // We can add Lvl 2 & 3 later ] }, 'rickroller': { name: 'Rickroller Trap', iconAsset: 'towerRickrollerLvl1', buildSfx: 'buildRickroller', // Define this sound levels: [{ asset: 'towerRickrollerLvl1', cost: 100, damagePerTick: 0.5, // Small damage over time while stunned range: 150, // Activation radius for the trap stunDuration: 120, // Frames (2 seconds at 60fps) effectDuration: 180, // How long the Rick Astley popup stays popupAsset: 'rickAstleyPopup', soundEffect: 'sfxRickrollTrigger', // The crucial sound! description: "Never gonna let them pass!" }] }, 'thisIsFine': { name: "'This Is Fine' Fire Pit", iconAsset: 'towerThisIsFineLvl1', buildSfx: 'buildThisIsFine', // Define this sound levels: [{ asset: 'towerThisIsFineLvl1', cost: 125, range: 250, // 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: 200, damage: 100, instantKillThreshold: 25, range: 450, chargeUpTime: 240, targetPriority: 'strongest', effectAsset: 'restructureEffect', description: "Optimizing threats... permanently." }, { asset: 'towerRestructLvl1', cost: 300, damage: 150, instantKillThreshold: 35, range: 475, chargeUpTime: 180, targetPriority: 'strongest', effectAsset: 'restructureEffect', description: "Synergistic realignment for enhanced efficiency." }, { asset: 'towerRestructLvl1', cost: 450, damage: 200, instantKillThreshold: 50, range: 500, chargeUpTime: 150, 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_HEIGHT = 3000; // Define total map height /**** NEW - ADJUST AS NEEDED ****/ // Game variables 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); // Removed old info text, replaced by Bark button // var infoText = new Text2(...) // Bark Button /**** NEW ****/ var barkButton = new Text2("BARK!", { size: 60, 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); // Initialize game level and path function initializeGame() { game.y = 0; game.scale.set(1); // Reset position and scale level = new GameLevel(); game.addChild(level); // Path Points (Adjust Y values for map height) pathPoints = [{ x: SCREEN_WIDTH * 0.1, y: 100 }, { x: SCREEN_WIDTH * 0.1, y: 500 }, { x: SCREEN_WIDTH * 0.4, y: 500 }, { x: SCREEN_WIDTH * 0.4, y: 1000 }, { x: SCREEN_WIDTH * 0.7, y: 1000 }, { x: SCREEN_WIDTH * 0.7, y: 1500 }, { x: SCREEN_WIDTH * 0.3, y: 1500 }, { x: SCREEN_WIDTH * 0.3, y: 2000 }, { x: SCREEN_WIDTH * 0.8, y: 2000 }, { x: SCREEN_WIDTH * 0.8, y: 2800 }]; level.createPath(pathPoints); // Build Spots (Adjust Y values for map height) var buildSpots = [{ x: SCREEN_WIDTH * 0.25, y: 300 }, { x: SCREEN_WIDTH * 0.25, y: 750 }, { x: SCREEN_WIDTH * 0.55, y: 750 }, { x: SCREEN_WIDTH * 0.55, y: 1250 }, { x: SCREEN_WIDTH * 0.85, y: 1250 }, { x: SCREEN_WIDTH * 0.50, y: 1750 }, { x: SCREEN_WIDTH * 0.15, y: 1750 }, { x: SCREEN_WIDTH * 0.50, y: 2400 }, { x: SCREEN_WIDTH * 0.90, y: 2400 }]; level.createBuildSpots(buildSpots); // Goal goal = new Goal(); if (pathPoints.length > 0) { goal.x = pathPoints[pathPoints.length - 1].x; goal.y = pathPoints[pathPoints.length - 1].y; game.addChild(goal); } // Doge doge = new DogeHero(); doge.x = SCREEN_WIDTH / 2; doge.y = SCREEN_HEIGHT / 2; doge.targetX = doge.x; doge.targetY = doge.y; game.addChild(doge); // Reset Variables currency = 100; playerLives = 5; currentWave = 0; waveTimer = 0; isWaveActive = false; enemies.forEach(function (e) { if (e.parent) { e.destroy(); } }); enemies = []; bullets.forEach(function (b) { if (b.parent) { b.destroy(); } }); bullets = []; // Reset UI currencyText.setText("$: " + currency); livesText.setText("Lives: " + playerLives); waveText.setText("Wave: " + currentWave + "/" + MAX_WAVES); scoreText.setText("Score: " + LK.getScore()); barkButton.setText("BARK!"); // Create game speed control buttons var normalSpeedButton = new Text2("Regular Work Day", { size: 35, fill: 0xFFFFFF, // Active initially stroke: 0x000000, strokeThickness: 2 }); normalSpeedButton.anchor.set(1, 0); normalSpeedButton.x = -200; normalSpeedButton.y = 120; 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: 35, fill: 0xDDDDDD, // Dimmer initially stroke: 0x000000, strokeThickness: 2 }); fastSpeedButton.anchor.set(1, 0); fastSpeedButton.x = -200; fastSpeedButton.y = 170; fastSpeedButton.interactive = true; fastSpeedButton.down = function () { gameSpeedMultiplier = 2.0; updateSpeedButtonVisuals(); LK.getSound('uiSelectTower').play(); }; LK.gui.topRight.addChild(fastSpeedButton); // Store buttons as global variables for accessing elsewhere game.normalSpeedButton = normalSpeedButton; game.fastSpeedButton = fastSpeedButton; // Initialize button visuals updateSpeedButtonVisuals(); LK.playMusic('bgmusic'); } currentActiveBuildSpot = null; function spawnWave() { currentWave++; waveText.setText("Wave: " + currentWave + "/" + MAX_WAVES); var enemyCount = 5 + currentWave * 2; var spawnInterval = 60; // frames between enemy spawns // Define enemy types for this wave var enemyTypesForThisWave = []; if (currentWave === 1) { enemyTypesForThisWave.push(Enemy); // Original paper enemy enemyTypesForThisWave.push(InternOnCoffeeRun); } else if (currentWave === 2) { enemyTypesForThisWave.push(Enemy); enemyTypesForThisWave.push(InternOnCoffeeRun); enemyTypesForThisWave.push(InternOnCoffeeRun); // More interns } else if (currentWave === 3) { enemyTypesForThisWave.push(Enemy); enemyTypesForThisWave.push(AuditorBot); // Introduce Auditor enemyTypesForThisWave.push(InternOnCoffeeRun); } else if (currentWave >= 4 && currentWave % 2 === 0) { // Every other wave from 4 onwards, add spam for (var k = 0; k < 5; k++) { // Spawn a small swarm of spam enemyTypesForThisWave.push(SpamEmailUnit); } enemyTypesForThisWave.push(Enemy); enemyTypesForThisWave.push(AuditorBot); } else { // Default for later waves or odd waves after 3 enemyTypesForThisWave.push(Enemy); enemyTypesForThisWave.push(RedTapeWorm); // Bring back RedTapeWorm enemyTypesForThisWave.push(InternOnCoffeeRun); if (Math.random() < 0.3) { enemyTypesForThisWave.push(AuditorBot); } // Chance of Auditor } function spawnEnemy(count) { if (count <= 0 || !pathPoints || pathPoints.length === 0) { // Added check for pathPoints isWaveActive = true; // Mark wave active even if spawning failed/finished return; } // Select random enemy type from the wave's pool if (enemyTypesForThisWave.length === 0) { // Fallback if no types defined for a wave enemyTypesForThisWave.push(Enemy); } var enemyTypeToSpawn = enemyTypesForThisWave[Math.floor(Math.random() * enemyTypesForThisWave.length)]; var enemy = new enemyTypeToSpawn(); enemy.x = pathPoints[0].x; enemy.y = pathPoints[0].y; // We no longer override health/speed as each enemy has its own defined stats game.addChild(enemy); // Add enemy to the main game container enemies.push(enemy); // Use LK.setTimeout for delays LK.setTimeout(function () { spawnEnemy(count - 1); }, spawnInterval * 16.67 / gameSpeedMultiplier); // Adjust for game speed } isWaveActive = false; // Mark wave as starting (will be set true after first spawn attempt or completion) spawnEnemy(enemyCount); } 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) { // Player wins 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 --- /**** NEW ****/ if (doge) { var dogeScreenY = doge.y + game.y; var targetGameY = game.y; if (dogeScreenY < PAN_THRESHOLD) { targetGameY = -(doge.y - PAN_THRESHOLD); } else if (dogeScreenY > SCREEN_HEIGHT - PAN_THRESHOLD) { targetGameY = -(doge.y - (SCREEN_HEIGHT - PAN_THRESHOLD)); } var minGameY = -(MAP_HEIGHT - SCREEN_HEIGHT); if (isNaN(minGameY)) { minGameY = 0; } var maxGameY = 0; targetGameY = Math.max(minGameY, Math.min(maxGameY, targetGameY)); var camSmoothFactor = 0.1; game.y += (targetGameY - game.y) * camSmoothFactor; if (Math.abs(game.y - targetGameY) < 1) { game.y = targetGameY; } } // --- Dynamically Update Icon Affordability --- if (currentActiveBuildSpot && currentActiveBuildSpot.areIconsVisible) { currentActiveBuildSpot.updateAffordability(); } }; // Initialize the game LK.setScore(0); // Reset score on start initializeGame();
===================================================================
--- original.js
+++ change.js
@@ -845,18 +845,21 @@
if (!buildSpot || !buildSpot.parent) {
return;
}
var levelContainer = buildSpot.parent;
- var towerGlobalPos = levelContainer.toGlobal(buildSpot.position);
- var globalPos = self.parent ? self.parent.toGlobal(self.position) : self.position; // Get global position
+ // Get tower's position in game space where enemies exist
+ var towerGamePos = {
+ x: buildSpot.x,
+ y: buildSpot.y
+ };
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 - globalPos.x;
- var dy = enemy.y - globalPos.y;
+ var dx = enemy.x - towerGamePos.x;
+ var dy = enemy.y - towerGamePos.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < self.range * self.range) {
// Enemy is in range
if (!enemy.isSlowedByBlocker) {
@@ -1502,16 +1505,20 @@
if (self.isCharging) return;
var buildSpot = self.parent;
if (!buildSpot || !buildSpot.parent) return;
var levelContainer = buildSpot.parent;
- var towerGlobalPos = levelContainer.toGlobal(buildSpot.position);
+ // Get tower position in game space where enemies exist
+ var towerPos = {
+ x: buildSpot.x,
+ y: buildSpot.y
+ };
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 - towerGlobalPos.x;
- var dy = enemy.y - towerGlobalPos.y;
+ var dx = enemy.x - towerPos.x;
+ var dy = enemy.y - towerPos.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < minDistanceSq) {
minDistanceSq = distanceSq;
closestEnemy = enemy;
@@ -1679,13 +1686,17 @@
if (!buildSpot || !buildSpot.parent) {
return;
}
var levelContainer = buildSpot.parent; // BuildSpot is child of Level
- var towerGlobalPos = levelContainer.toGlobal(buildSpot.position); // World pos of tower's base
+ // Tower position in the same coordinate space as enemies
+ var towerPos = {
+ x: buildSpot.x,
+ y: buildSpot.y
+ };
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
- var dx = enemy.x - towerGlobalPos.x; // Use global tower X
- var dy = enemy.y - towerGlobalPos.y; // Use global tower Y
+ var dx = enemy.x - towerPos.x; // Use tower position in game space
+ var dy = enemy.y - towerPos.y; // Use tower position in game space
var distanceSq = dx * dx + dy * dy; // Squared distance check
if (distanceSq < minDistanceSq) {
minDistanceSq = distanceSq;
closestEnemy = enemy;
@@ -1704,12 +1715,10 @@
return;
}
var levelContainer = buildSpot.parent;
var towerGlobalPos = levelContainer.toGlobal(buildSpot.position);
- // Convert global position to coordinates local to the game container
- // to account for game.y scrolling
bullet.x = towerGlobalPos.x;
- bullet.y = towerGlobalPos.y - game.y; // Adjust for game container's scroll position
+ bullet.y = towerGlobalPos.y;
bullet.target = target;
bullet.damage = self.damage;
game.addChild(bullet);
bullets.push(bullet);
@@ -1771,14 +1780,14 @@
/****
* Game Code
****/
-// (Optional) Sounds
-// (Optional) Effect for "restructuring"
-// New Tower - Restructuring Specialist
-// Add upgrade method to both tower types via initializeTowerFromData
-// --- Base Tower Functionality (Conceptual - can be mixed into specific towers) ---
// This isn't a formal class, but concepts to apply
+// --- 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
function createRangeIndicator(centerX, centerY, radius, parentContainer) {
return new RangeIndicator(centerX, centerY, radius, parentContainer);
}
function addUpgradeToTower(towerInstance) {
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