User prompt
Ava, the Laser Cat's beam visual still only appears for Auditor Bots. The tower damages other enemies, but no beam is shown. We need to make the beam visual update more robust and find out why it's failing for non-Auditor targets. Please modify the LaserCatPerch.update() method, specifically in the 'Handle Active Beams' section where the beamData.beamGraphic visual is updated: Ensure towerPosInGameSpace is calculated correctly at the start of this beam update loop (it might be already, but re-confirm): var towerPosInGameSpace = game.globalToLocal(self.graphic.toGlobal({x:0, y:0})); Ensure targetPosInGameSpace uses the most robust method for all enemies: var targetGraphicGlobalCenter = beamData.target.graphic.toGlobal({x:0, y:0}); var targetPosInGameSpace = game.globalToLocal(targetGraphicGlobalCenter); (This ensures we get the target's visual center in game space, regardless of its direct parent, assuming all enemies have a .graphic property.) Add Safety Checks and Debug Information (for Ava to report back on): Before setting beamData.beamGraphic.rotation and height, add these checks: // Inside the 'Handle Active Beams' loop, for each beamData: // ... (after calculating towerPosInGameSpace and targetPosInGameSpace for beamData.target) let dx = targetPosInGameSpace.x - towerPosInGameSpace.x; let dy = targetPosInGameSpace.y - towerPosInGameSpace.y; let distance = Math.sqrt(dx*dx + dy*dy);// --- DEBUG START --- if (!beamData.beamGraphic || !beamData.beamGraphic.parent) { // If Ava can 'log' or report, this would be useful: // LK.log("LaserCat: Beam graphic missing or detached for target: ", beamData.target); continue; // Skip updating this beam if graphic is bad } if (isNaN(distance) || !isFinite(distance) || distance <= 0) { // LK.log("LaserCat: Invalid distance (" + distance + ") for target: ", beamData.target, ". Hiding beam.");beamData.beamGraphic.visible = false; // Hide the beam if distance is problematic continue; // Skip updating this beam } else { beamData.beamGraphic.visible = true; } // --- DEBUG END --- beamData.beamGraphic.rotation = Math.atan2(dy, dx) - (Math.PI / 2); // Assuming beam asset points 'up' beamData.beamGraphic.height = distance; beamData.beamGraphic.x = towerPosInGameSpace.x; beamData.beamGraphic.y = towerPosInGameSpace.y;
User prompt
Okay, ensure that
User prompt
Ava, the LaserCatPerch tower's damage is currently too high. Please adjust its Level 1 stats in TOWER_DATA. In the TOWER_DATA entry for 'laserCat', for levels[0] (Level 1): Change initialBeamDamage from 10 to 5. Change dotDamagePerSecond from 5 to 2. Keep dotRampUpFactor: 1.2 for now, or reduce slightly to 1.15 if the ramp-up is too aggressive. Let's keep it at 1.2 for now and see. The stats for Level 2 and Level 3 can remain as they are for now, as we are primarily balancing Level 1.
User prompt
Ava, let's implement the mechanics for the LaserCatPerch tower. It fires sustained laser beams that deal increasing damage over time. In LaserCatPerch constructor (it already calls initializeTowerFromData): self.activeBeams = []; // Array to store info about current beams {target, beamGraphic, durationTimer, currentDotDamage} self.cooldownTimer = 0; // Time until it can look for new targets after all beams finish or max targets reached In LaserCatPerch.update() method: Decrement self.cooldownTimer by (1000/60) * gameSpeedMultiplier (since cooldown is in ms). Handle Active Beams: Iterate through self.activeBeams (from last to first if removing). For each beamData: If beamData.target is null, no longer exists (!beamData.target.parent), or is out of self.range: Destroy beamData.beamGraphic. Remove beamData from self.activeBeams. Continue to next beam. Decrement beamData.durationTimer by (1000/60) * gameSpeedMultiplier. Apply DOT: Every second (or more frequently, e.g., every 0.5 seconds): If it's time for a DOT tick (e.g., use a dotTickTimer in beamData): beamData.target.takeDamage(beamData.currentDotDamage); beamData.currentDotDamage *= self.dotRampUpFactor; // Ramp up damage Update Beam Visual: Get towerPosInGameSpace and targetPosInGameSpace. Position beamData.beamGraphic's start at towerPosInGameSpace. Make beamData.beamGraphic point towards targetPosInGameSpace. This involves setting its rotation and height (or scaleY if it's a fixed height asset) to span the distance. let dx = targetPosInGameSpace.x - towerPosInGameSpace.x; let dy = targetPosInGameSpace.y - towerPosInGameSpace.y; let distance = Math.sqrt(dx*dx + dy*dy); beamData.beamGraphic.rotation = Math.atan2(dy, dx) - Math.PI/2; // Adjust if asset points up/down beamData.beamGraphic.height = distance; (If laserBeam asset is 1px wide and designed to be scaled in height) beamData.beamGraphic.x = towerPosInGameSpace.x; beamData.beamGraphic.y = towerPosInGameSpace.y; If beamData.durationTimer <= 0: Destroy beamData.beamGraphic. Remove beamData from self.activeBeams. Find New Targets: If self.cooldownTimer <= 0 AND self.activeBeams.length < self.numberOfBeams: Find potential targets: Iterate enemies in enemyLayer that are within self.range and are not already targeted by one of self.activeBeams. Prioritize (e.g., strongest, closest - let's use strongest for now: highest health). If a suitable new targetEnemy is found: Create beamGraphic: let beamGfx = new LK.Graphics(); (Or use self.attachAsset('laserBeam') if it's an image asset that can be stretched/rotated). If using LK.Graphics: beamGfx.lineStyle(5, 0xFF0000, 1); beamGfx.moveTo(0,0); beamGfx.lineTo(0,1); (a 1px line to be scaled and rotated) Position it (will be updated above): beamGfx.x = towerPosInGameSpace.x; beamGfx.y = towerPosInGameSpace.y; Add to a layer below enemies but above coins/level (e.g., game.addChild(beamGfx) for now, or a dedicated beamLayer). Apply self.initialBeamDamage to targetEnemy. Play sfxLaserCatFireInitialPew. Start sfxLaserCatFireBeamLoop (looping, associate with beamGraphic to stop later). Add to self.activeBeams: { target: targetEnemy, beamGraphic: beamGfx, durationTimer: self.maxBeamDuration, currentDotDamage: self.dotDamagePerSecond, dotTickTimer: 1000 /* ms for first tick */ } If self.activeBeams.length >= self.numberOfBeams, set self.cooldownTimer = self.cooldownBetweenBeams;. Targeting Hum Sound: If self.activeBeams.length > 0 and the targeting hum sound isn't playing, start sfxLaserCatTargetHum (looping). If self.activeBeams.length === 0 and the hum is playing, stop it. In LaserCatPerch.destroy() (or its override): Iterate self.activeBeams, destroy all beamGraphics. Stop any looping sounds (hum, beam loops). Placement Sound: In BuildSpot.buildSelectedTower, when 'laserCat' is built, play sfxLaserCatPlace. Notes on Laser Visual (laserBeam asset): If laserBeam is a LK.init.shape defined as a thin box, you can set its height to the distance and rotation. If it's an image, it might need to be a tiled texture or a very long thin image that you position and rotate, possibly masking parts if it overshoots. Using LK.Graphics to draw a line dynamically might be more flexible if the engine supports it well for this. Let's assume for now the laserBeam shape asset can have its height dynamically set.
User prompt
Ava, please update the TOWER_DATA for the 'laserCat' tower to include new properties for its Damage Over Time (DOT) mechanics and multi-target upgrades. Replace the existing 'laserCat' entry with this: 'laserCat': { name: 'Laser Cat Perch', iconAsset: 'towerLaserCatLvl1', buildSfx: 'buildLaserCat', levels: [ { // Level 1 asset: 'towerLaserCatLvl1', cost: 150, range: 500, initialBeamDamage: 10, // Damage when beam first hits dotDamagePerSecond: 5, // Base DOT dotRampUpFactor: 1.2, // Multiplier for DOT each second it stays on same target (e.g., 5, then 5*1.2, then 5*1.2*1.2) maxBeamDuration: 3000, // Milliseconds (3 seconds) beam can stay on one target cooldownBetweenBeams: 4000, // Milliseconds (4 seconds) after a beam finishes numberOfBeams: 1, // Targets 1 enemy description: "Pew pew! Precision feline firepower. Damage ramps up." }, { // Level 2 asset: 'towerLaserCatLvl1', // Placeholder - use 'towerLaserCatLvl2' when ready cost: 250, range: 550, initialBeamDamage: 15, dotDamagePerSecond: 7, dotRampUpFactor: 1.25, maxBeamDuration: 3500, cooldownBetweenBeams: 3500, numberOfBeams: 2, // Targets 2 enemies description: "Dual-core processing! More lasers, more ouch." }, { // Level 3 asset: 'towerLaserCatLvl1', // Placeholder - use 'towerLaserCatLvl3' when ready cost: 400, range: 600, initialBeamDamage: 20, dotDamagePerSecond: 10, dotRampUpFactor: 1.3, maxBeamDuration: 4000, cooldownBetweenBeams: 3000, numberOfBeams: 3, // Targets 3 enemies description: "Maximum laser focus! It's a light show of doom." } ] }
Code edit (1 edits merged)
Please save this source code
User prompt
Ava, let's implement the mechanics for the 'ThisIsFinePit' tower. In ThisIsFinePit constructor: Initialize self.nearbyTowers = []; Initialize self.currentBuffStacks = 0; Initialize self.lastSoundPlayed = null; (for managing dynamic audio) Initialize self.soundCooldown = 0; (e.g., 120 frames to prevent voice lines spamming) It already calls initializeTowerFromData(self, 'thisIsFine', 0);. In ThisIsFinePit.update() method: A. Count Enemies in Range: Get the Fire Pit's world position (pitPosInGameSpace using the self.graphic.toGlobal() and game.globalToLocal() method). Iterate through all enemies in the enemyLayer. Count how many enemies are within self.range of pitPosInGameSpace. Cap this count at self.maxBuffStacks. Store this as let activeStacks = Math.min(enemyCountInRange, self.maxBuffStacks);. B. Clear Previous Buffs & Find Towers to Buff: If self.currentBuffStacks > 0 (meaning it was buffing last frame): Iterate through self.nearbyTowers. For each towerInRange: If towerInRange still exists and has originalFireRate and originalDamage properties: Revert its stats: towerInRange.fireRate = towerInRange.originalFireRate; towerInRange.damage = towerInRange.originalDamage; Clear self.nearbyTowers = [];. Iterate through all BuildSpot instances in level.buildSpots. For each spot: If spot.hasTower && spot.tower && spot.tower !== self (not itself): Get spot.tower's position in game space (otherTowerPosInGameSpace). If spot.tower is within self.range of pitPosInGameSpace, add spot.tower to self.nearbyTowers. C. Apply New Buffs: Update self.currentBuffStacks = activeStacks;. If activeStacks > 0: Calculate buff multipliers: let speedMultiplier = 1.0 - (self.attackSpeedBuffPerStack * activeStacks); let damageMultiplier = 1.0 + (self.damageBuffPerStack * activeStacks); Iterate through self.nearbyTowers. For each towerInRange: If it doesn't have originalFireRate stored, store it: towerInRange.originalFireRate = towerInRange.fireRate; If it doesn't have originalDamage stored, store it: towerInRange.originalDamage = towerInRange.damage; Apply buffs: towerInRange.fireRate = Math.max(5, Math.floor(towerInRange.originalFireRate * speedMultiplier)); (Ensure fireRate doesn't go too low, e.g., min 5 frames) towerInRange.damage = towerInRange.originalDamage * damageMultiplier; (May need rounding or toFixed if damage must be integer) D. Dynamic Audio Logic: if (self.soundCooldown > 0) self.soundCooldown -= gameSpeedMultiplier; If activeStacks === 0 and self.lastSoundPlayed !== 'gentle': Stop other fire sounds if playing. Play sfxThisIsFineFireGentle (looping). self.lastSoundPlayed = 'gentle'; Else if activeStacks > 0 && activeStacks <= 2 and self.lastSoundPlayed !== 'medium': Stop other fire sounds. Play sfxThisIsFineFireMedium (looping). self.lastSoundPlayed = 'medium'; If self.soundCooldown <= 0: Play sfxThisIsFineVoice1, self.soundCooldown = 300; (5s) Else if activeStacks > 2 and self.lastSoundPlayed !== 'roar': Stop other fire sounds. Play sfxThisIsFineFireRoar (looping). self.lastSoundPlayed = 'roar'; If self.soundCooldown <= 0: Play sfxThisIsFineVoice2, self.soundCooldown = 300; In ThisIsFinePit.destroy() (or its override): Before calling original destroy, iterate through self.nearbyTowers and revert their stats to originalFireRate and originalDamage if those exist. Stop any looping fire sounds associated with this tower instance. Placement Sound: In BuildSpot.buildSelectedTower, when 'thisIsFine' is built, after adding the tower, play sfxThisIsFinePlaceMatch once, then start sfxThisIsFineFireGentle (looping, low volume initially). Store a reference to the looping sound on the tower instance if needed to stop/change it."
Code edit (2 edits merged)
Please save this source code
User prompt
Ava, please make two adjustments to the DogeCoin class to improve the coin collection gameplay: Increase Doge's Pickup Range for Coins: In the DogeCoin constructor, change the line: self.collectionRadius = 75; To: self.collectionRadius = 120; Increase Coin Lifespan Before Disappearing: In the DogeCoin constructor, change the line: self.lifeTime = 600; To: self.lifeTime = 1500; (This is 25 seconds at 60fps with gameSpeedMultiplier = 1.0) These changes will make Doge collect coins from a slightly larger area around him and give the player more time to collect the coins before they disappear.
User prompt
Ava, thank you! Your latest explanation of the @upit/tween.v1 API using tween.to(targetObject).to({props}, duration, easing).call().start() seems to be the correct one. Please refactor the tween animations in the DogeCoin class to use this new syntax: In the DogeCoin constructor, for the initial scatter animation: Replace the old tween.create(self).to(...) line for the coin's position with: tween.to(self) .to( { x: targetX, y: targetY }, 300 * gameSpeedMultiplier, tween.easing.quadOut ) .call(function() { self.isOnGround = true; }) .start(); Use code with caution. JavaScript Replace the old tween.create(self.graphic.scale).to(...) line for the coin's scale with: tween.to(self.graphic.scale) .to( { x: 1.0, y: 1.0 }, 300 * gameSpeedMultiplier, tween.easing.quadOut ) .start(); // No .call() needed for this one Use code with caution. JavaScript In the DogeCoin class's self.collect = function() {} method, for the fly-to-UI animation: Replace the old tween.create(self).to(...) line with: tween.to(self) .to( { x: targetUiInGame.x, y: targetUiInGame.y, alpha: 0.5, // How to tween nested properties like 'graphic.scale.x'? // If direct nesting doesn't work, we might need separate tweens // or the tween library might have a specific syntax for it. // For now, let's try direct property path if Ava understands it, // otherwise we'll need to tween self.graphic.scale separately. // Let's assume for now we just tween self.alpha and position, // and scale self.graphic directly or in a separate tween. // So, for 'graphic.scale.x' and 'graphic.scale.y', we'll simplify for this pass. // We will handle graphic scale separately if needed. }, 500 * gameSpeedMultiplier, tween.easing.quadIn // Using quadIn for flying towards something ) .call(function() { self.destroy(); }) .start(); // If you want to scale down the graphic as it flies, and direct nesting like // 'graphic.scale.x': 0.2 in the properties object above doesn't work, // you might need a separate tween for the scale: tween.to(self.graphic.scale) .to( { x: 0.2, y: 0.2 }, 500 * gameSpeedMultiplier, // Match duration tween.easing.quadIn ) .start(); Use code with caution. JavaScript Note on Animating Nested Properties like graphic.scale.x: I've added a comment in the prompt for self.collect. Some tween engines allow direct paths like 'graphic.scale.x', others require you to tween the self.graphic.scale object itself for its x and y properties. I've provided the code to tween self.graphic.scale separately as a robust alternative if direct path tweening in the main object's properties doesn't work. Ava should pick the correct approach for @upit/tween.v1 or implement both if needed." ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (2 edits merged)
Please save this source code
User prompt
Ava, we are consistently getting the error TypeError: tween.create is not a function when trying to use the @upit/tween.v1 plugin, which is imported as var tween = LK.import('@upit/tween.v1');?
User prompt
Ava, we are consistently getting the error TypeError: tween.create is not a function when trying to use the @upit/tween.v1 plugin, which is imported as var tween = LK.import('@upit/tween.v1');. Please do not assume that it has a tween.create() method. Instead, can you list the actual top-level methods and properties available directly on the tween object that is returned by LK.import('@upit/tween.v1')? For example, if the library was structured differently, it might be something like: tween.new(...) tween.animate(...) new tween.Animation(...).to(...) Or perhaps the main tweening functions are under a different sub-object, like tween.manager.create(...) or LK.Tweening.add(...). I need to know the exact function name or an example of the correct entry point to start creating a tween animation using the @upit/tween.v1 plugin, based on what is actually available on the tween object after it's imported."
User prompt
Please fix the bug: 'TypeError: tween.create is not a function' in or related to this line: 'tween.create(self).to({' Line Number: 1083
User prompt
Ava, we have a persistent TypeError: tween.create is not a function at line 1060 in the DogeCoin class, even though you confirm tween is an object and tween.create().to().start() is the correct syntax for the @upit/tween.v1 plugin. To isolate this, please perform the following modification temporarily for debugging: At the very beginning of the DogeCoin constructor, before any other code in the constructor, add these lines: // ---- START DEBUG BLOCK ---- if (tween && typeof tween.create === 'function') { // Attempt a very simple tween on a dummy object var dummyObject = { x: 0 }; try { var testTween = tween.create(dummyObject) .to({ x: 100 }, 100, tween.easing.quadOut) .start(); // If this line is reached without error, 'tween.create' exists and is callable. // We are not logging, so we just observe if an error still occurs before this. } catch (e) { // This catch block would ideally log the error, but since we can't, // the goal is to see if the original error still happens *before* this test, // or if this test itself throws a *different* error or the same one. } } else { // This block would execute if 'tween' is not an object or 'tween.create' is not a function // right at the start of the DogeCoin constructor. // This would indicate a serious loading/initialization order issue with the plugin. } // ---- END DEBUG BLOCK ---- // ... (rest of DogeCoin constructor, including the original lines that cause the error) ... Use code with caution. JavaScript By placing this debug block at the absolute start of the constructor, we test tween.create in the earliest possible moment within the DogeCoin's lifecycle.
User prompt
Please fix the bug: 'TypeError: tween.create is not a function' in or related to this line: 'tween.create(self).to({' Line Number: 1060
User prompt
Ava, based on the correct syntax you provided for @upit/tween.v1, please find all instances where tween.create(...).to(...) is used (specifically in the DogeCoin class constructor for the scatter animation and in its self.collect method for the fly-to-UI animation) and update them to use the correct API call for creating and starting a tween.
User prompt
Please fix the bug: 'TypeError: tween.create is not a function' in or related to this line: 'tween.create(self).to({' Line Number: 1060
User prompt
Please fix the bug: 'TypeError: tween.create is not a function' in or related to this line: 'tween.create(self).to({' Line Number: 1060
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'TypeError: tween.create is not a function' in or related to this line: 'tween.create(self).to({' Line Number: 1060
User prompt
Ava, when an enemy's health reaches zero or less and it's destroyed, it should drop Doge Coins. Modify the takeDamage method in ALL enemy classes (Enemy, RedTapeWorm, InternOnCoffeeRun, AuditorBot, SpamEmailUnit): Inside the if (self.health <= 0 && self.parent) block, before self.destroy(): Determine number of coins to drop (can be based on self.value or fixed). let numCoins = Math.ceil(self.value / 5); // Example: 1 coin per 5 value, min 1 if (numCoins < 1) numCoins = 1; Loop numCoins times: let coin = new DogeCoin(self.x, self.y, Math.floor(self.value / numCoins)); // Distribute value game.addChild(coin); // Or add to a dedicated 'coinLayer' if it exists (Optional) LK.getSound('sfxCoinDrop').play(); The existing logic for adding to currency, score, and LK.getSound('enemyDeath').play() should remain, but the direct addition to currency here will be replaced by Doge collecting the coins. For now, let's keep the direct currency gain AND coin drop for testing. We can remove direct currency gain later.
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."
/****
* 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
// Determine number of coins to drop based on enemy value
var numCoins = Math.ceil(self.value / 5); // 1 coin per 5 value, min 1
if (numCoins < 1) {
numCoins = 1;
}
// Spawn multiple coins
for (var i = 0; i < numCoins; i++) {
var coin = new DogeCoin(self.x, self.y, Math.floor(self.value / numCoins));
if (game.coinLayer) {
game.coinLayer.addChild(coin);
} else {
coinLayer.addChild(coin);
}
}
// Score still goes up immediately
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();
// Play match strike sound when placing the fire pit
LK.getSound('sfxThisIsFinePlaceMatch').play();
// Start gentle fire sound (looping, low volume initially)
LK.getSound('sfxThisIsFineFireGentle').play();
} else if (towerTypeKey === 'restruct') {
newTower = new RestructuringSpecialist();
}
// No need for special newTower.init(0) call - constructor handles initialization
// else if (towerTypeKey === 'placeholder') { /* Do nothing or build a dummy */ return; }
if (newTower) {
// The tower's constructor now calls initializeTowerFromData(self, type, 0)
newTower.x = 0; // Place tower at BuildSpot's origin (local to BuildSpot)
newTower.y = 0;
self.addChild(newTower); // Tower becomes child of BuildSpot
self.tower = newTower;
self.hasTower = true;
self.graphic.alpha = 0.1; // Dim the build spot graphic itself
var buildSfx = TOWER_DATA[towerTypeKey].buildSfx || 'uiSelectTower'; // Fallback
// Ensure these sounds exist: 'buildStapler', 'buildBlocker'
// LK.getSound(buildSfx).play(); // Play specific build sound
// Floating text relative to build spot in world space
spawnFloatingText(TOWER_DATA[towerTypeKey].name + " BUILT!", self.x, self.y - self.graphic.height / 2, {
fill: 0x00FF00
});
// Update affordability for any other spot's menu that might be open (shouldn't be with current logic)
if (currentActiveBuildSpot && currentActiveBuildSpot !== self) {
currentActiveBuildSpot.updateAffordability();
}
}
// Icons are hidden by the icon's down handler OR by game.down clicking elsewhere
};
self.down = function () {
// This is the click on the BuildSpot graphic itself
if (self.hasTower) {
// If action icons are already visible, clicking again should close them
if (self.areActionIconsVisible) {
self.hideTowerActionIcons();
} else {
// Hide any other open menus
if (currentActiveBuildSpot && currentActiveBuildSpot !== self) {
if (currentActiveBuildSpot.areIconsVisible) {
currentActiveBuildSpot.hideSelectionIcons();
} else if (currentActiveBuildSpot.isUpgradeMenuVisible) {
currentActiveBuildSpot.hideUpgradeMenu();
} else if (currentActiveBuildSpot.areActionIconsVisible) {
currentActiveBuildSpot.hideTowerActionIcons();
}
}
// Set this as the active build spot
currentActiveBuildSpot = self;
// Show tower action icons
LK.getSound('uiOpenMenu').play();
self.showTowerActionIcons(self.tower);
}
return;
}
// If this spot's icons are already visible, clicking it again should close them.
if (self.areIconsVisible) {
self.hideSelectionIcons();
} else {
// If another spot's icons are visible, hide them first.
if (currentActiveBuildSpot && currentActiveBuildSpot !== self) {
currentActiveBuildSpot.hideSelectionIcons();
}
LK.getSound('uiOpenMenu').play(); // Play sound when opening
self.showSelectionIcons();
}
};
return self;
});
// --- MODIFY BureaucracyBlockerTower ---
var BureaucracyBlockerTower = Container.expand(function () {
var self = Container.call(this);
// --- MODIFIED: No graphic initialization in constructor ---
self.enemiesSlowed = [];
// init method removed - functionality moved to constructor and initializeTowerFromData
self.clearAllSlows = function () {
for (var i = 0; i < self.enemiesSlowed.length; i++) {
var enemy = self.enemiesSlowed[i];
if (enemy && enemy.parent && enemy.isSlowedByBlocker === self) {
enemy.speed = enemy.originalSpeed;
enemy.isSlowedByBlocker = null;
if (enemy.graphic) {
enemy.graphic.tint = 0xFFFFFF;
}
}
}
self.enemiesSlowed = [];
};
self.update = function () {
var buildSpot = self.parent;
if (!buildSpot || !buildSpot.parent) {
return;
}
var levelContainer = buildSpot.parent;
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
var towerCenterInGameSpace = game.toLocal(towerGraphicGlobalCenter);
var newlySlowedThisFrame = [];
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy || !enemy.parent) {
continue;
} // Skip if enemy is already gone
var dx = enemy.x - towerCenterInGameSpace.x;
var dy = enemy.y - towerCenterInGameSpace.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < self.range * self.range) {
// Enemy is in range
if (!enemy.isSlowedByBlocker) {
// Check a custom flag
enemy.originalSpeed = enemy.speed; // Store original speed
enemy.speed *= self.slowFactor;
enemy.isSlowedByBlocker = self; // Mark who slowed it
// Optional: Visual effect on enemy
if (enemy.graphic) {
enemy.graphic.tint = 0xAAAAFF;
} // Light blue tint
}
newlySlowedThisFrame.push(enemy);
}
}
// Check enemies that were slowed last frame but might be out of range now
for (var i = self.enemiesSlowed.length - 1; i >= 0; i--) {
var previouslySlowedEnemy = self.enemiesSlowed[i];
if (!previouslySlowedEnemy || !previouslySlowedEnemy.parent || newlySlowedThisFrame.indexOf(previouslySlowedEnemy) === -1) {
// Enemy is gone or no longer in range by this tower
if (previouslySlowedEnemy && previouslySlowedEnemy.isSlowedByBlocker === self) {
// Only unslow if WE slowed it
previouslySlowedEnemy.speed = previouslySlowedEnemy.originalSpeed;
previouslySlowedEnemy.isSlowedByBlocker = null;
if (previouslySlowedEnemy.graphic) {
previouslySlowedEnemy.graphic.tint = 0xFFFFFF;
} // Reset tint
}
self.enemiesSlowed.splice(i, 1);
}
}
self.enemiesSlowed = newlySlowedThisFrame; // Update the list of currently slowed enemies
};
// When tower is destroyed, make sure to unslow any enemies it was affecting
var originalDestroy = self.destroy;
self.destroy = function () {
for (var i = 0; i < self.enemiesSlowed.length; i++) {
var enemy = self.enemiesSlowed[i];
if (enemy && enemy.parent && enemy.isSlowedByBlocker === self) {
enemy.speed = enemy.originalSpeed;
enemy.isSlowedByBlocker = null;
if (enemy.graphic) {
enemy.graphic.tint = 0xFFFFFF;
}
}
}
self.enemiesSlowed = [];
if (originalDestroy) {
originalDestroy.call(self);
} else if (self.parent) {
self.parent.removeChild(self);
} // Basic destroy
};
// Initialize tower with level 0 stats and graphics
initializeTowerFromData(self, 'blocker', 0); // Set initial stats & correct L0 asset
return self;
});
var DogeAutoProjectile = Container.expand(function () {
var self = Container.call(this);
self.graphic = self.attachAsset('basicAttackProjectile', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 15;
self.target = null;
self.damage = 0; // Will be set by DogeHero when creating projectile
self.duration = 20; // frames, for self-destruction if target is lost
self.update = function () {
// Decrement duration
self.duration -= gameSpeedMultiplier;
if (self.duration <= 0) {
self.destroy();
return;
}
// If target is gone or not in game anymore
if (!self.target || !self.target.parent) {
self.alpha -= 0.1 * gameSpeedMultiplier;
if (self.alpha <= 0) {
self.destroy();
}
return;
}
// Move towards target
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// If very close to target, destroy
if (distance < self.speed * gameSpeedMultiplier || distance < self.target.graphic.width / 2) {
// Apply damage when hitting the target
if (self.target && self.target.parent) {
self.target.takeDamage(self.damage);
}
self.destroy();
return;
}
// Move towards target
dx = dx / distance * self.speed * gameSpeedMultiplier;
dy = dy / distance * self.speed * gameSpeedMultiplier;
self.x += dx;
self.y += dy;
};
return self;
});
var DogeCoin = Container.expand(function (spawnX, spawnY, value) {
var self = Container.call(this);
// Initialize coin properties
self.graphic = self.attachAsset('dogeCoin', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = spawnX;
self.y = spawnY;
self.value = value || 1; // Default to 1 if no value passed
self.isOnGround = false;
self.isBeingCollected = false;
self.spinSpeed = 0.1; // Radians per frame for spinning
self.collectionRadius = 300; // How close Doge needs to be
self.lifeTime = 1500; // Frames before it disappears if not collected (25s @ 60fps with gameSpeedMultiplier = 1.0)
// Initial scatter animation
var scatterAngle = Math.random() * Math.PI * 2;
var scatterDistance = 20 + Math.random() * 30;
var targetX = self.x + Math.cos(scatterAngle) * scatterDistance;
var targetY = self.y + Math.sin(scatterAngle) * scatterDistance;
// Start with smaller scale and grow to normal size
self.graphic.scale.set(0.5);
// Animate the coin to scatter position and grow to normal size
tween(self, {
x: targetX,
y: targetY
}, {
duration: 300 * gameSpeedMultiplier,
easing: tween.quadOut,
onFinish: function onFinish() {
self.isOnGround = true;
}
});
tween(self.graphic.scale, {
x: 1.0,
y: 1.0
}, {
duration: 300 * gameSpeedMultiplier,
easing: tween.quadOut
});
// Update method for coin behavior
self.update = function () {
self.lifeTime -= gameSpeedMultiplier;
// Destroy if lifetime expires and not being collected
if (self.lifeTime <= 0 && !self.isBeingCollected) {
self.destroy();
return;
}
// Skip other updates if being collected
if (self.isBeingCollected) {
return;
}
// Spin when on ground
if (self.isOnGround) {
self.graphic.rotation += self.spinSpeed * gameSpeedMultiplier;
}
// Check if Doge is close enough to collect
if (self.isOnGround && doge && doge.parent) {
var dx = doge.x - self.x;
var dy = doge.y - self.y;
if (dx * dx + dy * dy < self.collectionRadius * self.collectionRadius) {
self.collect();
}
}
};
// Collect method when Doge picks up the coin
self.collect = function () {
if (self.isBeingCollected) {
return;
}
self.isBeingCollected = true;
// Update currency and UI
currency += self.value;
currencyText.setText("$: " + currency);
// Show floating text
spawnFloatingText("+" + self.value + "$", self.x, self.y - 20, {
fill: 0xFFFF00,
velocityY: -2
});
// Get target position for animation
var targetUiGlobal = currencyText.toGlobal({
x: currencyText.width / 2,
y: currencyText.height / 2
});
var targetUiInGame = game.toLocal(targetUiGlobal);
// Animate coin flying to the UI
tween(self, {
x: targetUiInGame.x,
y: targetUiInGame.y,
alpha: 0.5
}, {
duration: 500 * gameSpeedMultiplier,
easing: tween.quadIn,
onFinish: function onFinish() {
self.destroy();
}
});
// Scale graphic separately since nested properties need their own tween
tween(self.graphic.scale, {
x: 0.2,
y: 0.2
}, {
duration: 500 * gameSpeedMultiplier,
easing: tween.quadIn
});
};
return self;
});
// DogeHero - Now includes auto-attack and manual bark ability logic
var DogeHero = Container.expand(function () {
var self = Container.call(this);
self.graphic = self.attachAsset('dogeHero', {
anchorX: 0.5,
anchorY: 0.5
}); // Expose graphic if needed for hit check
self.width = self.graphic.width; // Store size for hit checks
self.height = self.graphic.height;
self.speed = 5;
self.targetX = self.x;
self.targetY = self.y;
// Auto Attack Stats
self.autoAttackRange = 450; // Increased from 280
self.autoAttackDamage = 1; // Decreased from 2
self.autoAttackCooldownTime = 45;
self.currentAutoAttackCooldown = 0;
// Manual Bark Ability Stats
self.manualBarkCooldownTime = 300;
self.currentManualBarkCooldown = 0;
self.update = function () {
// Movement
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > self.speed) {
dx = dx / distance * self.speed * gameSpeedMultiplier;
dy = dy / distance * self.speed * gameSpeedMultiplier;
self.x += dx;
self.y += dy;
} else if (distance > 0) {
self.x = self.targetX;
self.y = self.targetY;
}
// Cooldowns
if (self.currentAutoAttackCooldown > 0) {
self.currentAutoAttackCooldown -= gameSpeedMultiplier;
}
if (self.currentManualBarkCooldown > 0) {
self.currentManualBarkCooldown -= gameSpeedMultiplier;
}
// Auto Attack Logic
if (self.currentAutoAttackCooldown <= 0) {
var closestEnemy = null;
var minDistanceSq = self.autoAttackRange * self.autoAttackRange;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var ex = enemy.x - self.x;
var ey = enemy.y - self.y;
var distSq = ex * ex + ey * ey;
if (distSq < minDistanceSq) {
minDistanceSq = distSq;
closestEnemy = enemy;
}
}
if (closestEnemy) {
// Play sound and reset cooldown
LK.getSound('dogeAutoAttack').play();
self.currentAutoAttackCooldown = self.autoAttackCooldownTime;
// Create projectile that will handle damage application
var projectile = new DogeAutoProjectile();
projectile.x = self.x;
projectile.y = self.y;
projectile.target = closestEnemy;
projectile.damage = self.autoAttackDamage; // Pass damage value to projectile
game.addChild(projectile);
}
}
};
self.setTarget = function (x, y) {
// Check if we're trying to target a BuildSpot or any of its menus
var isClickingUI = false;
// Skip movement if there's any active BuildSpot with open menus
if (currentActiveBuildSpot) {
isClickingUI = true;
}
// Check if clicked on any BuildSpot
if (level && level.buildSpots && !isClickingUI) {
for (var i = 0; i < level.buildSpots.length; i++) {
var spot = level.buildSpots[i];
// If we passed the graphic check in game.down
// but still reached setTarget, check again
var dx = x - spot.x;
var dy = y - spot.y;
var distSq = dx * dx + dy * dy;
var hitRadius = 75; // Half of the 150px BuildSpot width
if (distSq <= hitRadius * hitRadius) {
isClickingUI = true;
break;
}
}
}
// Only move if not clicking UI
if (!isClickingUI) {
self.targetX = x;
self.targetY = y;
}
};
self.manualBark = function () {
if (self.currentManualBarkCooldown <= 0) {
var wave = new BarkWave();
wave.x = self.x;
wave.y = self.y;
game.addChild(wave);
LK.getSound('dogeBark').play();
self.currentManualBarkCooldown = self.manualBarkCooldownTime;
return true;
}
return false;
};
return self;
});
// 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
// Determine number of coins to drop based on enemy value
var numCoins = Math.ceil(self.value / 5); // 1 coin per 5 value, min 1
if (numCoins < 1) {
numCoins = 1;
}
// Spawn multiple coins
for (var i = 0; i < numCoins; i++) {
var coin = new DogeCoin(self.x, self.y, Math.floor(self.value / numCoins));
if (game.coinLayer) {
game.coinLayer.addChild(coin);
} else {
coinLayer.addChild(coin);
}
}
// Score still goes up immediately
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
// Determine number of coins to drop based on enemy value
var numCoins = Math.ceil(self.value / 5); // 1 coin per 5 value, min 1
if (numCoins < 1) {
numCoins = 1;
}
// Spawn multiple coins
for (var i = 0; i < numCoins; i++) {
var coin = new DogeCoin(self.x, self.y, Math.floor(self.value / numCoins));
if (game.coinLayer) {
game.coinLayer.addChild(coin);
} else {
coinLayer.addChild(coin);
}
}
// Score still goes up immediately
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.activeBeams = []; // Array to store info about current beams {target, beamGraphic, durationTimer, currentDotDamage}
self.cooldownTimer = 0; // Time until it can look for new targets after all beams finish or max targets reached
self.update = function () {
var buildSpot = self.parent;
if (!buildSpot || !buildSpot.parent) return;
// Get tower position in game space
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
var towerPosInGameSpace = game.toLocal(towerGraphicGlobalCenter);
// Decrement cooldown timer
if (self.cooldownTimer > 0) {
self.cooldownTimer -= 1000 / 60 * gameSpeedMultiplier; // Convert from ms to frames
}
// Handle Active Beams
for (var i = self.activeBeams.length - 1; i >= 0; i--) {
var beamData = self.activeBeams[i];
// Check if target is still valid and in range
if (!beamData.target || !beamData.target.parent) {
// Target no longer exists
beamData.beamGraphic.destroy();
self.activeBeams.splice(i, 1);
continue;
}
// Check if target is out of range
var dx = beamData.target.x - towerPosInGameSpace.x;
var dy = beamData.target.y - towerPosInGameSpace.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > self.range) {
// Target out of range
beamData.beamGraphic.destroy();
self.activeBeams.splice(i, 1);
continue;
}
// Decrement beam duration timer
beamData.durationTimer -= 1000 / 60 * gameSpeedMultiplier;
// Apply DOT damage at intervals
beamData.dotTickTimer -= 1000 / 60 * gameSpeedMultiplier;
if (beamData.dotTickTimer <= 0) {
// Apply damage
beamData.target.takeDamage(beamData.currentDotDamage);
// Ramp up damage for next tick
beamData.currentDotDamage *= self.dotRampUpFactor;
// Reset tick timer (damage every 0.5 seconds)
beamData.dotTickTimer = 500;
}
// Update beam visual
// Position beam at tower position
beamData.beamGraphic.x = towerPosInGameSpace.x;
beamData.beamGraphic.y = towerPosInGameSpace.y;
// Calculate direction and distance
var targetGraphicGlobalCenter = beamData.target.graphic.toGlobal({
x: 0,
y: 0
});
var targetPosInGameSpace = game.toLocal(targetGraphicGlobalCenter);
var dx = targetPosInGameSpace.x - towerPosInGameSpace.x;
var dy = targetPosInGameSpace.y - towerPosInGameSpace.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// --- DEBUG START ---
if (!beamData.beamGraphic || !beamData.beamGraphic.parent) {
continue; // Skip updating this beam if graphic is bad
}
if (isNaN(distance) || !isFinite(distance) || distance <= 0) {
beamData.beamGraphic.visible = false; // Hide the beam if distance is problematic
continue; // Skip updating this beam
} else {
beamData.beamGraphic.visible = true;
}
// --- DEBUG END ---
// Adjust beam rotation and height
beamData.beamGraphic.rotation = Math.atan2(dy, dx) - Math.PI / 2; // Adjust rotation
beamData.beamGraphic.height = distance; // Stretch beam to reach target
// If beam duration is up, destroy it
if (beamData.durationTimer <= 0) {
beamData.beamGraphic.destroy();
self.activeBeams.splice(i, 1);
}
}
// Find New Targets if not on cooldown and not at max beams
if (self.cooldownTimer <= 0 && self.activeBeams.length < self.numberOfBeams) {
// Find potential targets in range that aren't already targeted
var potentialTargets = [];
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy || !enemy.parent) continue;
// Check if enemy is in range
var dx = enemy.x - towerPosInGameSpace.x;
var dy = enemy.y - towerPosInGameSpace.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq <= self.range * self.range) {
// Check if enemy is already targeted by an active beam
var alreadyTargeted = false;
for (var j = 0; j < self.activeBeams.length; j++) {
if (self.activeBeams[j].target === enemy) {
alreadyTargeted = true;
break;
}
}
if (!alreadyTargeted) {
potentialTargets.push(enemy);
}
}
}
// If targets available, target the strongest (highest health)
if (potentialTargets.length > 0) {
// Sort by health (highest first)
potentialTargets.sort(function (a, b) {
return b.health - a.health;
});
// Target the strongest enemy
var targetEnemy = potentialTargets[0];
// Create beam graphic
var beamGfx = LK.getAsset('laserBeam', {
anchorX: 0.5,
anchorY: 0 // Anchor at top center
});
// Position at tower
beamGfx.x = towerPosInGameSpace.x;
beamGfx.y = towerPosInGameSpace.y;
// Add to game (below enemy layer if possible, or directly to game)
game.addChild(beamGfx);
// Apply initial damage
targetEnemy.takeDamage(self.initialBeamDamage);
// Play firing sound
// LK.getSound('sfxLaserCatFireInitialPew').play();
// Start beam loop sound
// var beamLoopSound = LK.getSound('sfxLaserCatFireBeamLoop').play();
// Add to active beams
self.activeBeams.push({
target: targetEnemy,
beamGraphic: beamGfx,
durationTimer: self.maxBeamDuration,
currentDotDamage: self.dotDamagePerSecond,
dotTickTimer: 500 // First tick after 0.5 seconds
});
// If at max beams, start cooldown
if (self.activeBeams.length >= self.numberOfBeams) {
self.cooldownTimer = self.cooldownBetweenBeams;
}
}
}
// Targeting hum sound logic
if (self.activeBeams.length > 0) {
// Play targeting hum sound if not already playing
// if (!humSoundPlaying) {
// humSoundPlaying = true;
// LK.getSound('sfxLaserCatTargetHum').play();
// }
} else {
// Stop hum sound if playing
// if (humSoundPlaying) {
// humSoundPlaying = false;
// LK.getSound('sfxLaserCatTargetHum').stop();
// }
}
};
// Override destroy method to clean up beams
var originalDestroy = self.destroy;
self.destroy = function () {
// Clean up all active beams
for (var i = 0; i < self.activeBeams.length; i++) {
if (self.activeBeams[i].beamGraphic) {
self.activeBeams[i].beamGraphic.destroy();
}
}
// Stop any looping sounds
// if (humSoundPlaying) {
// LK.getSound('sfxLaserCatTargetHum').stop();
// }
// Call original destroy
if (originalDestroy) {
originalDestroy.call(self);
} else if (self.parent) {
self.parent.removeChild(self);
}
};
self.shoot = function (target) {
// This method is kept for compatibility but not used actively
// The beam firing logic is handled in update()
};
return self;
});
// Range Indicator to visualize tower attack ranges
var RangeIndicator = Container.expand(function (centerX, centerY, radius, parentContainer) {
var self = Container.call(this);
// Create container for the indicator
self.x = centerX;
self.y = centerY;
// Since we can't draw circles directly in LK, use a pre-defined circle asset
// and scale it to match our desired radius
var rangeCircle = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.05,
// Very faint fill
tint: 0xFFFFFF // White color
});
// The centerCircle asset is 100px in diameter, so scale accordingly
var scaleRatio = radius * 2 / 100;
rangeCircle.scaleX = scaleRatio;
rangeCircle.scaleY = scaleRatio;
// Add a slightly larger circle with higher alpha for the border effect
var rangeBorder = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3,
// More visible than the fill
tint: 0xFFFFFF // White color
});
// Make border slightly larger
var borderScaleRatio = scaleRatio * 1.02;
rangeBorder.scaleX = borderScaleRatio;
rangeBorder.scaleY = borderScaleRatio;
// Add to parent container
if (parentContainer) {
parentContainer.addChild(self);
}
return self;
});
// --- 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
// Determine number of coins to drop based on enemy value
var numCoins = Math.ceil(self.value / 5); // 1 coin per 5 value, min 1
if (numCoins < 1) {
numCoins = 1;
}
// Spawn multiple coins
for (var i = 0; i < numCoins; i++) {
var coin = new DogeCoin(self.x, self.y, Math.floor(self.value / numCoins));
if (game.coinLayer) {
game.coinLayer.addChild(coin);
} else {
coinLayer.addChild(coin);
}
}
// Score still goes up immediately
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;
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
var towerCenterInGameSpace = game.toLocal(towerGraphicGlobalCenter);
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 - towerCenterInGameSpace.x;
var dy = enemy.y - towerCenterInGameSpace.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 () {
// For future implementation:
// var buildSpot = self.parent;
// if (!buildSpot || !buildSpot.parent) return;
// var towerGraphicGlobalCenter = self.graphic.toGlobal({ x: 0, y: 0 });
// var towerCenterInGameSpace = game.globalToLocal(towerGraphicGlobalCenter);
// Then use towerCenterInGameSpace.x/y for distance calculations
};
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
// Determine number of coins to drop based on enemy value
var numCoins = Math.ceil(self.value / 5); // 1 coin per 5 value, min 1
if (numCoins < 1) {
numCoins = 1;
}
// Spawn multiple coins
for (var i = 0; i < numCoins; i++) {
var coin = new DogeCoin(self.x, self.y, Math.floor(self.value / numCoins));
if (game.coinLayer) {
game.coinLayer.addChild(coin);
} else {
coinLayer.addChild(coin);
}
}
// Score still goes up immediately
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
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
var towerCenterInGameSpace = game.toLocal(towerGraphicGlobalCenter);
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - towerCenterInGameSpace.x;
var dy = enemy.y - towerCenterInGameSpace.y;
var distanceSq = dx * dx + dy * dy; // Squared distance check
if (distanceSq < minDistanceSq) {
minDistanceSq = distanceSq;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.shoot(closestEnemy);
self.lastFired = 0;
}
}
};
self.shoot = function (target) {
var bullet = new TowerBullet();
var buildSpot = self.parent;
if (!buildSpot || !buildSpot.parent) {
return;
}
var levelContainer = buildSpot.parent;
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
var bulletSpawnPosInGameSpace = game.toLocal(towerGraphicGlobalCenter);
bullet.x = bulletSpawnPosInGameSpace.x;
bullet.y = bulletSpawnPosInGameSpace.y;
bullet.target = target;
bullet.damage = self.damage;
game.addChild(bullet);
bullets.push(bullet);
LK.getSound('shoot').play();
};
// Initialize tower with level 0 stats and graphics
initializeTowerFromData(self, 'stapler', 0); // Set initial stats & correct L0 asset
return self;
});
var ThisIsFinePit = Container.expand(function () {
var self = Container.call(this);
// Initialize buffer tower properties
self.nearbyTowers = [];
self.currentBuffStacks = 0;
self.lastSoundPlayed = null; // For managing dynamic audio
self.soundCooldown = 0; // To prevent voice lines spamming
// Initialize the tower with level 0 stats and graphics
initializeTowerFromData(self, 'thisIsFine', 0);
self.update = function () {
// A. Count Enemies in Range
var buildSpot = self.parent;
if (!buildSpot || !buildSpot.parent) {
return;
}
var pitPosInGameSpace;
var towerGraphicGlobalCenter = self.graphic.toGlobal({
x: 0,
y: 0
});
pitPosInGameSpace = game.toLocal(towerGraphicGlobalCenter);
// Count how many enemies are within range
var enemyCountInRange = 0;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy && enemy.parent) {
var dx = enemy.x - pitPosInGameSpace.x;
var dy = enemy.y - pitPosInGameSpace.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < self.range * self.range) {
enemyCountInRange++;
}
}
}
// Cap at max buff stacks
var activeStacks = Math.min(enemyCountInRange, self.maxBuffStacks);
// B. Clear Previous Buffs & Find Towers to Buff
if (self.currentBuffStacks > 0) {
// Revert stats for previously buffed towers
for (var i = 0; i < self.nearbyTowers.length; i++) {
var towerInRange = self.nearbyTowers[i];
if (towerInRange && towerInRange.parent && towerInRange.hasOwnProperty('originalFireRate') && towerInRange.hasOwnProperty('originalDamage')) {
towerInRange.fireRate = towerInRange.originalFireRate;
towerInRange.damage = towerInRange.originalDamage;
}
}
// Clear the array of nearby towers
self.nearbyTowers = [];
}
// Find all towers within range
if (level && level.buildSpots) {
for (var i = 0; i < level.buildSpots.length; i++) {
var spot = level.buildSpots[i];
if (spot.hasTower && spot.tower && spot.tower !== self) {
// Get tower position in game space
var towerPos = spot.tower.graphic.toGlobal({
x: 0,
y: 0
});
var otherTowerPosInGameSpace = game.toLocal(towerPos);
// Check if tower is in range
var dx = otherTowerPosInGameSpace.x - pitPosInGameSpace.x;
var dy = otherTowerPosInGameSpace.y - pitPosInGameSpace.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < self.range * self.range) {
self.nearbyTowers.push(spot.tower);
}
}
}
}
// C. Apply New Buffs
self.currentBuffStacks = activeStacks;
if (activeStacks > 0) {
// Calculate buff multipliers
var speedMultiplier = 1.0 - self.attackSpeedBuffPerStack * activeStacks;
var damageMultiplier = 1.0 + self.damageBuffPerStack * activeStacks;
// Apply buffs to nearby towers
for (var i = 0; i < self.nearbyTowers.length; i++) {
var towerInRange = self.nearbyTowers[i];
// Store original values if not already stored
if (!towerInRange.hasOwnProperty('originalFireRate')) {
towerInRange.originalFireRate = towerInRange.fireRate;
}
if (!towerInRange.hasOwnProperty('originalDamage')) {
towerInRange.originalDamage = towerInRange.damage;
}
// Apply buffs
towerInRange.fireRate = Math.max(5, Math.floor(towerInRange.originalFireRate * speedMultiplier));
towerInRange.damage = towerInRange.originalDamage * damageMultiplier;
}
}
// D. Dynamic Audio Logic
if (self.soundCooldown > 0) {
self.soundCooldown -= gameSpeedMultiplier;
}
if (activeStacks === 0 && self.lastSoundPlayed !== 'gentle') {
// Stop other fire sounds if playing
// Play sfxThisIsFineFireGentle (looping)
self.lastSoundPlayed = 'gentle';
} else if (activeStacks > 0 && activeStacks <= 2 && self.lastSoundPlayed !== 'medium') {
// Stop other fire sounds
// Play sfxThisIsFineFireMedium (looping)
self.lastSoundPlayed = 'medium';
if (self.soundCooldown <= 0) {
// Play sfxThisIsFineVoice1
self.soundCooldown = 300; // 5s
}
} else if (activeStacks > 2 && self.lastSoundPlayed !== 'roar') {
// Stop other fire sounds
// Play sfxThisIsFineFireRoar (looping)
self.lastSoundPlayed = 'roar';
if (self.soundCooldown <= 0) {
// Play sfxThisIsFineVoice2
self.soundCooldown = 300;
}
}
};
// Override destroy to clean up buffs when tower is destroyed
var originalDestroy = self.destroy;
self.destroy = function () {
// Revert all buffs before destroying
for (var i = 0; i < self.nearbyTowers.length; i++) {
var towerInRange = self.nearbyTowers[i];
if (towerInRange && towerInRange.parent && towerInRange.hasOwnProperty('originalFireRate') && towerInRange.hasOwnProperty('originalDamage')) {
towerInRange.fireRate = towerInRange.originalFireRate;
towerInRange.damage = towerInRange.originalDamage;
}
}
// Stop any looping fire sounds
// Call original destroy
if (originalDestroy) {
originalDestroy.call(self);
} else if (self.parent) {
self.parent.removeChild(self);
}
};
return self;
});
// TowerBullet class remains the same
var TowerBullet = Container.expand(function () {
var self = Container.call(this);
var graphic = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 10;
self.damage = 1;
self.target = null;
self.update = function () {
// Check if target exists and is still in the game
if (!self.target || !self.target.parent) {
self.destroy();
return;
}
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Use speed as collision threshold
if (distance < self.speed) {
self.target.takeDamage(self.damage);
self.destroy();
return;
}
// Normalize and multiply by speed
dx = dx / distance * self.speed * gameSpeedMultiplier;
dy = dy / distance * self.speed * gameSpeedMultiplier;
self.x += dx;
self.y += dy;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x558855 // Darker green background
});
/****
* Game Code
****/
// var towerCenterInGameSpace = game.toLocal(towerGraphicGlobalCenter);
// --- Base Tower Functionality (Conceptual - can be mixed into specific towers) ---
// Add upgrade method to both tower types via initializeTowerFromData
// New Tower - Restructuring Specialist
// (Optional) Effect for "restructuring"
// (Optional) Sounds
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',
buildSfx: 'buildLaserCat',
levels: [{
// Level 1
asset: 'towerLaserCatLvl1',
cost: 150,
range: 500,
initialBeamDamage: 5,
// Damage when beam first hits
dotDamagePerSecond: 2,
// Base DOT
dotRampUpFactor: 1.2,
// Multiplier for DOT each second it stays on same target (e.g., 2, then 2*1.2, then 2*1.2*1.2)
maxBeamDuration: 3000,
// Milliseconds (3 seconds) beam can stay on one target
cooldownBetweenBeams: 4000,
// Milliseconds (4 seconds) after a beam finishes
numberOfBeams: 1,
// Targets 1 enemy
description: "Pew pew! Precision feline firepower. Damage ramps up."
}, {
// Level 2
asset: 'towerLaserCatLvl1',
// Placeholder - use 'towerLaserCatLvl2' when ready
cost: 250,
range: 550,
initialBeamDamage: 15,
dotDamagePerSecond: 7,
dotRampUpFactor: 1.25,
maxBeamDuration: 3500,
cooldownBetweenBeams: 3500,
numberOfBeams: 2,
// Targets 2 enemies
description: "Dual-core processing! More lasers, more ouch."
}, {
// Level 3
asset: 'towerLaserCatLvl1',
// Placeholder - use 'towerLaserCatLvl3' when ready
cost: 400,
range: 600,
initialBeamDamage: 20,
dotDamagePerSecond: 10,
dotRampUpFactor: 1.3,
maxBeamDuration: 4000,
cooldownBetweenBeams: 3000,
numberOfBeams: 3,
// Targets 3 enemies
description: "Maximum laser focus! It's a light show of doom."
}]
},
'rickroller': {
name: 'Rickroller Trap',
iconAsset: 'towerRickrollerLvl1',
buildSfx: '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: 650,
// Aura buff range
maxBuffStacks: 5,
// How many nearby enemies contribute to max buff
attackSpeedBuffPerStack: 0.05,
// e.g., 5% faster per stack (0.95 multiplier)
damageBuffPerStack: 0.05,
// e.g., 5% more damage per stack (1.05 multiplier)
description: "Everything's fine. Totally fine. Buffs nearby towers."
}]
},
'restruct': {
name: 'Restructuring Specialist',
iconAsset: 'towerRestructLvl1',
buildSfx: 'buildRestruct',
levels: [{
asset: 'towerRestructLvl1',
cost: 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_WIDTH = 3000; // Example: roughly double current screen width
var MAP_HEIGHT = 4500; // Let's increase this too if we're expanding
// Game variables
var coinLayer;
var enemyLayer;
var currency = 100;
var playerLives = 5;
var currentWave = 0;
var waveTimer = 0;
var isWaveActive = false;
var enemies = [];
var bullets = [];
var pathPoints = [];
var currentActiveBuildSpot = null;
var level;
var doge;
var goal;
// --- HELPER FUNCTION FOR FLOATING TEXT ---
function isChildOf(possibleParent, target) {
if (!target || !target.parent) {
return false;
}
if (target.parent === possibleParent) {
return true;
}
return isChildOf(possibleParent, target.parent);
}
// Function to update the visual state of speed buttons
function updateSpeedButtonVisuals() {
if (game.normalSpeedButton && game.fastSpeedButton) {
if (gameSpeedMultiplier === 1.0) {
game.normalSpeedButton.setText("Regular Work Day", {
fill: 0xFFFFFF // Active
});
game.fastSpeedButton.setText("Crunch Time!", {
fill: 0xDDDDDD // Inactive
});
} else {
game.normalSpeedButton.setText("Regular Work Day", {
fill: 0xDDDDDD // Inactive
});
game.fastSpeedButton.setText("Crunch Time!", {
fill: 0xFFD700 // Gold/active
});
}
}
}
function spawnFloatingText(text, x, y, options) {
var defaultOptions = {
size: 30,
fill: 0xFFFFFF,
// White text
stroke: 0x000000,
strokeThickness: 2,
duration: 60,
// frames (1 second at 60fps)
velocityY: -1.5,
// Pixels per frame upwards
alphaFadeSpeed: 0.015
};
var settings = Object.assign({}, defaultOptions, options); // Merge user options
var floatingText = new Text2(text, {
size: settings.size,
fill: settings.fill,
stroke: settings.stroke,
strokeThickness: settings.strokeThickness,
anchorX: 0.5,
// Center the text
anchorY: 0.5
});
floatingText.x = x;
floatingText.y = y;
floatingText.alpha = 1.0;
game.addChild(floatingText); // Add to main game container to scroll with world
var framesLived = 0;
floatingText.update = function () {
floatingText.y += settings.velocityY * gameSpeedMultiplier;
floatingText.alpha -= settings.alphaFadeSpeed * gameSpeedMultiplier;
framesLived += gameSpeedMultiplier;
if (framesLived >= settings.duration || floatingText.alpha <= 0) {
floatingText.destroy();
}
};
// Add this to a list of updatable text objects if your engine doesn't auto-update children with .update
// For LK Engine, if it's a child of `game` and has an `update` method, it should be called.
// If not, you'll need a global array like `activeFloatingTexts` and iterate it in `game.update`.
// Let's assume LK.Game handles child updates for now.
}
var scoreText = new Text2("Score: 0", {
size: 50,
fill: 0xFFFFFF
}); // White text
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
var waveText = new Text2("Wave: 0/" + MAX_WAVES, {
size: 50,
fill: 0xFFFFFF
});
waveText.anchor.set(1, 0); // Anchor top-right
waveText.x = -100; // Position from right edge
LK.gui.topRight.addChild(waveText);
var currencyText = new Text2("$: " + currency, {
size: 50,
fill: 0xFFFFFF
});
currencyText.anchor.set(0.5, 0);
currencyText.y = 60;
LK.gui.top.addChild(currencyText);
var livesText = new Text2("Lives: " + playerLives, {
size: 50,
fill: 0xFFFFFF
});
livesText.anchor.set(1, 0); // Anchor top-right
livesText.x = -100;
livesText.y = 60;
LK.gui.topRight.addChild(livesText);
// 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
coinLayer = new Container();
game.addChild(coinLayer); // coinLayer will be drawn ON TOP of 'level'
enemyLayer = new Container();
game.addChild(enemyLayer); // enemyLayer will be drawn ON TOP of 'coinLayer'
level = new GameLevel();
game.addChild(level);
// Create a layer for coins to be placed behind enemies but in front of path
var coinLayer = new Container();
game.addChild(coinLayer);
game.coinLayer = coinLayer; // Store reference for easy access
// Path Points (Adjust Y values for map height)
pathPoints = [{
x: MAP_WIDTH * 0.2,
y: MAP_HEIGHT * 0.05
}, {
x: MAP_WIDTH * 0.2,
y: MAP_HEIGHT * 0.2
}, {
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.2
}, {
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.35
}, {
x: MAP_WIDTH * 0.7,
y: MAP_HEIGHT * 0.35
}, {
x: MAP_WIDTH * 0.7,
y: MAP_HEIGHT * 0.5
}, {
x: MAP_WIDTH * 0.4,
y: MAP_HEIGHT * 0.5
}, {
x: MAP_WIDTH * 0.3,
y: MAP_HEIGHT * 0.55
}, {
x: MAP_WIDTH * 0.3,
y: MAP_HEIGHT * 0.7
}, {
x: MAP_WIDTH * 0.8,
y: MAP_HEIGHT * 0.7
}, {
x: MAP_WIDTH * 0.6,
y: MAP_HEIGHT * 0.85
}, {
x: MAP_WIDTH * 0.85,
y: MAP_HEIGHT * 0.85
}, {
x: MAP_WIDTH * 0.9,
y: MAP_HEIGHT * 0.9
}];
level.createPath(pathPoints);
// Build Spots (Adjust Y values for map height)
var buildSpots = [{
x: MAP_WIDTH * 0.15,
y: MAP_HEIGHT * 0.15
}, {
x: MAP_WIDTH * 0.35,
y: MAP_HEIGHT * 0.15
}, {
x: MAP_WIDTH * 0.22,
y: MAP_HEIGHT * 0.25
}, {
x: MAP_WIDTH * 0.45,
y: MAP_HEIGHT * 0.42
}, {
x: MAP_WIDTH * 0.75,
y: MAP_HEIGHT * 0.42
}, {
x: MAP_WIDTH * 0.6,
y: MAP_HEIGHT * 0.45
}, {
x: MAP_WIDTH * 0.25,
y: MAP_HEIGHT * 0.625
}, {
x: MAP_WIDTH * 0.78,
y: MAP_HEIGHT * 0.645
}, {
x: MAP_WIDTH * 0.55,
y: MAP_HEIGHT * 0.6
}, {
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.78
}, {
x: MAP_WIDTH * 0.79,
y: MAP_HEIGHT * 0.78
}, {
x: MAP_WIDTH * 0.65,
y: MAP_HEIGHT * 0.75
}, {
x: MAP_WIDTH * 0.79,
y: MAP_HEIGHT * 0.91
}];
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
enemyLayer.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 ---
if (doge) {
var CAM_SMOOTH_FACTOR = 0.1; // Define smoothing factor
// --- Vertical Panning ---
var PAN_THRESHOLD_VERTICAL = SCREEN_HEIGHT * 0.25; // Pan when Doge in top/bottom 25%
var dogeScreenY = doge.y + game.y; // Doge's Y position on the actual screen
var targetGameY = game.y;
if (dogeScreenY < PAN_THRESHOLD_VERTICAL) {
targetGameY = -(doge.y - PAN_THRESHOLD_VERTICAL);
} else if (dogeScreenY > SCREEN_HEIGHT - PAN_THRESHOLD_VERTICAL) {
targetGameY = -(doge.y - (SCREEN_HEIGHT - PAN_THRESHOLD_VERTICAL));
}
var maxGameY = 0;
var minGameY = -(MAP_HEIGHT - SCREEN_HEIGHT);
if (MAP_HEIGHT <= SCREEN_HEIGHT) {
// Prevent scrolling if map isn't taller
minGameY = 0;
}
targetGameY = Math.max(minGameY, Math.min(maxGameY, targetGameY));
game.y += (targetGameY - game.y) * CAM_SMOOTH_FACTOR;
if (Math.abs(game.y - targetGameY) < 1) {
game.y = targetGameY;
}
// --- Horizontal Panning (NEW) ---
var PAN_THRESHOLD_HORIZONTAL = SCREEN_WIDTH * 0.30; // Pan when Doge in left/right 30%
var dogeScreenX = doge.x + game.x; // Doge's X position on the actual screen
var targetGameX = game.x;
if (dogeScreenX < PAN_THRESHOLD_HORIZONTAL) {
targetGameX = -(doge.x - PAN_THRESHOLD_HORIZONTAL);
} else if (dogeScreenX > SCREEN_WIDTH - PAN_THRESHOLD_HORIZONTAL) {
targetGameX = -(doge.x - (SCREEN_WIDTH - PAN_THRESHOLD_HORIZONTAL));
}
var maxGameX = 0;
var minGameX = -(MAP_WIDTH - SCREEN_WIDTH);
if (MAP_WIDTH <= SCREEN_WIDTH) {
// Prevent scrolling if map isn't wider
minGameX = 0;
}
targetGameX = Math.max(minGameX, Math.min(maxGameX, targetGameX));
game.x += (targetGameX - game.x) * CAM_SMOOTH_FACTOR;
if (Math.abs(game.x - targetGameX) < 1) {
game.x = targetGameX;
}
}
// --- End Camera Panning Logic ---
// --- Dynamically Update Icon Affordability ---
if (currentActiveBuildSpot && currentActiveBuildSpot.areIconsVisible) {
currentActiveBuildSpot.updateAffordability();
}
};
// Initialize the game
LK.setScore(0); // Reset score on start
initializeGame(); ===================================================================
--- original.js
+++ change.js
@@ -1558,28 +1558,27 @@
// Position beam at tower position
beamData.beamGraphic.x = towerPosInGameSpace.x;
beamData.beamGraphic.y = towerPosInGameSpace.y;
// Calculate direction and distance
- // Ensure consistent coordinate space for all enemy types
- var targetPosInGameSpace;
- // Check if target is an AuditorBot (or any enemy that might have different coordinate handling)
- if (beamData.target instanceof AuditorBot) {
- // Get the enemy position in game space explicitly
- var targetGlobalPos = beamData.target.graphic.toGlobal({
- x: 0,
- y: 0
- });
- targetPosInGameSpace = game.toLocal(targetGlobalPos);
- } else {
- // Use direct coordinates for other enemies
- targetPosInGameSpace = {
- x: beamData.target.x,
- y: beamData.target.y
- };
- }
+ var targetGraphicGlobalCenter = beamData.target.graphic.toGlobal({
+ x: 0,
+ y: 0
+ });
+ var targetPosInGameSpace = game.toLocal(targetGraphicGlobalCenter);
var dx = targetPosInGameSpace.x - towerPosInGameSpace.x;
var dy = targetPosInGameSpace.y - towerPosInGameSpace.y;
var distance = Math.sqrt(dx * dx + dy * dy);
+ // --- DEBUG START ---
+ if (!beamData.beamGraphic || !beamData.beamGraphic.parent) {
+ continue; // Skip updating this beam if graphic is bad
+ }
+ if (isNaN(distance) || !isFinite(distance) || distance <= 0) {
+ beamData.beamGraphic.visible = false; // Hide the beam if distance is problematic
+ continue; // Skip updating this beam
+ } else {
+ beamData.beamGraphic.visible = true;
+ }
+ // --- DEBUG END ---
// Adjust beam rotation and height
beamData.beamGraphic.rotation = Math.atan2(dy, dx) - Math.PI / 2; // Adjust rotation
beamData.beamGraphic.height = distance; // Stretch beam to reach target
// If beam duration is up, destroy it
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