Code edit (13 edits merged)
Please save this source code
Code edit (3 edits merged)
Please save this source code
User prompt
Fix the bezier curve, it should move almost straight towards the gold text. Now it seems like the bezier mid point is placed weird, that point should be slightly random but almost in the direction of the gold text
Code edit (1 edits merged)
Please save this source code
User prompt
The coin bezier curve does not look good, make it a bit random and move almost in a straight line towards the cold text ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (4 edits merged)
Please save this source code
User prompt
The coin bezier curve does not look good, make it a bit random and move almost in a straight line towards the cold text ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (1 edits merged)
Please save this source code
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed, towerType) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; self.type = towerType || 'default'; // Choose bullet asset based on tower type var bulletAsset = 'bullet'; switch (self.type) { case 'sniper': bulletAsset = 'bullet_sniper'; break; case 'splash': bulletAsset = 'bullet_splash'; break; case 'slow': bulletAsset = 'bullet_slow'; break; case 'poison': bulletAsset = 'bullet_poison'; break; } var bulletGraphics = self.attachAsset(bulletAsset, { anchorX: 0.5, anchorY: 0.5, rotation: self.type === 'sniper' ? Math.PI / 4 : 0 // +45 degrees for sniper bullets }); // Create shadow for sniper and default bullets if (self.type === 'sniper' || self.type === 'default') { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Create multiple shadow layers for blur effect var blurLayers = 8; var baseAlpha = self.type === 'default' ? 0.042 : 0.042; // Same alpha as sniper for (var blurIndex = 0; blurIndex < blurLayers; blurIndex++) { var shadowGraphics = self.shadow.attachAsset(bulletAsset, { anchorX: 0.5, anchorY: 0.5, rotation: self.type === 'sniper' ? Math.PI / 4 : 0 // Match the bullet rotation }); // Apply shadow effect with decreasing alpha for blur shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = baseAlpha * (blurLayers - blurIndex) / blurLayers; // Increase offset between layers for more blur spread shadowGraphics.x = blurIndex * 2; shadowGraphics.y = blurIndex * 2; // Make shadow 20% larger shadowGraphics.scaleX = 1.2; shadowGraphics.scaleY = 1.2; } // Position shadow slightly offset self.shadow.x = 15; // Offset right self.shadow.y = 15; // Offset down // Store reference to all shadow graphics for rotation updates self.shadowGraphics = self.shadow.children; } // Apply tower-specific tinting switch (self.type) { case 'splash': bulletGraphics.tint = 0x33CC00; break; case 'slow': bulletGraphics.tint = 0x9900FF; break; case 'poison': bulletGraphics.tint = 0x00FFAA; break; } self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Apply damage to target enemy with immune enemy special handling var actualDamage = self.damage; // Immune enemies take 50% less damage from sniper towers if (self.targetEnemy.isImmune && self.type === 'sniper') { actualDamage = Math.floor(self.damage * 0.5); } self.targetEnemy.health -= actualDamage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply special effects based on bullet type if (self.type === 'splash') { // Create visual splash effect var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); game.addChild(splashEffect); // Splash damage to nearby enemies var splashRadius = CELL_SIZE * 1.5; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) otherEnemy.health -= self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } else if (self.type === 'slow') { // Prevent slow effect on immune enemies if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); // Apply slow effect // Make slow percentage scale with tower level (default 50%, up to 80% at max level) var slowPct = 0.5; if (self.sourceTowerLevel !== undefined) { // Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6 var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8]; var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1)); slowPct = slowLevels[idx]; } if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 1 - slowPct; // Slow by X% self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS } else { self.targetEnemy.slowDuration = 180; // Reset duration } } } else if (self.type === 'poison') { // Create visual poison effect var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison'); game.addChild(poisonEffect); // Apply poison effect - immune enemies are no longer immune to poison self.targetEnemy.poisoned = true; self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS } else if (self.type === 'sniper') { // Create visual critical hit effect for sniper var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper'); game.addChild(sniperEffect); } self.destroy(); } else { var angle = Math.atan2(dy, dx); // Rotate sniper bullets to face movement direction if (self.type === 'sniper') { bulletGraphics.rotation = angle + Math.PI / 4; // Movement angle + base +45 degree offset // Update shadow position and rotation if it exists if (self.shadow && self.shadowGraphics) { self.shadow.x = self.x + 15; // Match bullet x-position + offset self.shadow.y = self.y + 15; // Match bullet y-position + offset // Match shadow rotation with bullet rotation for all blur layers for (var shadowIndex = 0; shadowIndex < self.shadowGraphics.length; shadowIndex++) { self.shadowGraphics[shadowIndex].rotation = angle + Math.PI / 4; } } } else if (self.type === 'default') { // Update shadow position for default bullets if (self.shadow && self.shadowGraphics) { self.shadow.x = self.x + 15; // Match bullet x-position + offset self.shadow.y = self.y + 15; // Match bullet y-position + offset } // Add spinning rotation to default tower bullets if (!self.rotationStarted) { self.rotationStarted = true; tween(bulletGraphics, { rotation: Math.PI * 4 }, { duration: 1000, easing: tween.linear, loop: true }); } } self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); //cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; var numberLabel = new Text2('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); //self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; /*if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); }*/ }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { // Remove existing cell graphics and create new one based on cell type if (cellGraphics && cellGraphics.parent) { self.removeChild(cellGraphics); } // Choose asset based on cell type var assetName; switch (data.type) { case 0: // Transparent floor case 2: // Spawn - use checkerboard pattern var isEvenRow = data.y % 2 === 0; var isEvenCol = data.x % 2 === 0; // Create checkerboard pattern: even+even or odd+odd = primary tile var usePrimaryTile = isEvenRow && isEvenCol || !isEvenRow && !isEvenCol; assetName = usePrimaryTile ? 'floor_tile' : 'floor_tile_alt'; break; case 1: // Wall - check if it has a tower placed on it var hasTower = false; // Check if there's a tower at this grid position for (var towerIndex = 0; towerIndex < towers.length; towerIndex++) { var tower = towers[towerIndex]; if (tower.gridX === data.x && tower.gridY === data.y) { hasTower = true; break; } // For 2x2 towers, also check if this cell is covered by a tower placed nearby if (tower.id !== 'office_prop') { if (tower.gridX === data.x - 1 && tower.gridY === data.y || tower.gridX === data.x && tower.gridY === data.y - 1 || tower.gridX === data.x - 1 && tower.gridY === data.y - 1) { hasTower = true; break; } } } if (!hasTower) { assetName = 'cell'; } else { // Spawn - use checkerboard pattern var isEvenRow = data.y % 2 === 0; var isEvenCol = data.x % 2 === 0; // Create checkerboard pattern: even+even or odd+odd = primary tile var usePrimaryTile = isEvenRow && isEvenCol || !isEvenRow && !isEvenCol; assetName = usePrimaryTile ? 'floor_tile' : 'floor_tile_alt'; break; } break; case 3: // Goal - use checkerboard pattern var isEvenRow = data.y % 2 === 0; var isEvenCol = data.x % 2 === 0; // Create checkerboard pattern: even+even or odd+odd = primary tile var usePrimaryTile = isEvenRow && isEvenCol || !isEvenRow && !isEvenCol; assetName = usePrimaryTile ? 'floor_tile' : 'floor_tile_alt'; break; default: assetName = 'cell'; } // Create new graphics with appropriate asset cellGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); switch (data.type) { case 0: case 2: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.setText("-"); cellGraphics.tint = 0x880000; return; } numberLabel.visible = true; var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0x1cb51c; } else { cellGraphics.tint = 0xffffff; // 0x88 - tint << 8 | tint; } // Special tinting for spawn cells if (data.type === 2) { cellGraphics.tint = 0x00ff00; // Green for spawn } while (debugArrows.length > data.targets.length) { self.removeChild(debugArrows.pop()); } for (var a = 0; a < data.targets.length; a++) { var destination = data.targets[a]; var ox = destination.x - data.x; var oy = destination.y - data.y; var angle = Math.atan2(oy, ox); if (!debugArrows[a]) { debugArrows[a] = LK.getAsset('arrow', { anchorX: -.5, anchorY: 0.5 }); debugArrows[a].alpha = .5; //self.addChildAt(debugArrows[a], 1); } debugArrows[a].rotation = angle; } break; } case 1: { self.removeArrows(); cellGraphics.tint = 0xdddddd; numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.tint = 0xff0000; // Red for goal numberLabel.visible = false; break; } } numberLabel.setText(Math.floor(data.score / 1000) / 10); }; }); var DebugModal = Container.expand(function () { var self = Container.call(this); // Full screen background var background = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); background.width = 2048; background.height = 2732; background.tint = 0x000000; background.alpha = 0.7; // Modal container var modal = new Container(); self.addChild(modal); // Modal background var modalBg = modal.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); modalBg.width = 1600; modalBg.height = 1800; modalBg.tint = 0x333333; modalBg.alpha = 0.95; // Title var titleText = new Text2("DEBUG - Wave Spawner", { size: 80, fill: 0xFFFFFF, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -800; modal.addChild(titleText); // Close button var closeButton = new Container(); modal.addChild(closeButton); var closeBg = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBg.width = 100; closeBg.height = 100; closeBg.tint = 0xAA0000; var closeText = new Text2('X', { size: 60, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = modalBg.width / 2 - 80; closeButton.y = -modalBg.height / 2 + 80; closeButton.down = function () { self.destroy(); }; // Create wave buttons in a grid var buttonsPerRow = 10; var buttonWidth = 120; var buttonHeight = 80; var buttonSpacingX = 140; var buttonSpacingY = 100; for (var wave = 1; wave <= totalWaves; wave++) { var waveButton = new Container(); modal.addChild(waveButton); var buttonBg = waveButton.attachAsset('wave_block', { anchorX: 0.5, anchorY: 0.5 }); buttonBg.width = buttonWidth; buttonBg.height = buttonHeight; // Color based on wave type var waveType = waveIndicator.getWaveType(wave); switch (waveType) { case 'floor_manager': buttonBg.tint = 0x8B4513; break; case 'ceo': buttonBg.tint = 0x4B0082; // Indigo for CEO break; case 'normal': buttonBg.tint = 0xAAAAAA; break; case 'fast': buttonBg.tint = 0x00AAFF; break; case 'immune': buttonBg.tint = 0xAA0000; break; case 'flying': buttonBg.tint = 0xFFFF00; break; case 'swarm': buttonBg.tint = 0xFF00FF; break; default: buttonBg.tint = 0x666666; } // Boss waves get special coloring if (wave % 10 === 0 && wave > 0 && waveType !== 'swarm') { buttonBg.tint = 0xFF0000; // Red for boss } var waveText = new Text2(wave.toString(), { size: 36, fill: 0xFFFFFF, weight: 800 }); waveText.anchor.set(0.5, 0.5); waveButton.addChild(waveText); // Position in grid var row = Math.floor((wave - 1) / buttonsPerRow); var col = (wave - 1) % buttonsPerRow; waveButton.x = -modalBg.width / 2 + 150 + col * buttonSpacingX; waveButton.y = -600 + row * buttonSpacingY; // Create closure to capture wave number (function (waveNum) { waveButton.down = function () { // Spawn the selected wave currentWave = waveNum; waveTimer = 0; waveInProgress = true; waveSpawned = false; var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("DEBUG: Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; self.destroy(); }; })(wave); } return self; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'poison': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'floor_manager': self.maxHealth = 100; // Same health as normal enemy break; case 'ceo': self.maxHealth = 100; // Same health as floor_manager break; case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'normal': default: // Normal enemy uses default values break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm' || currentWave === 51) { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.type !== 'normal') { if (self.type === 'floor_manager') { assetId = 'floor_manager_1'; // Start with first frame } else if (self.type === 'ceo') { assetId = 'ceo_1'; // Start with first frame } else if (self.type === 'swarm') { assetId = 'swarm_1'; // Start with first frame for swarm } else { assetId = 'enemy_' + self.type; } } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Add animation logic for floor manager if (self.type === 'floor_manager') { self.animationFrame = Math.floor(Math.random() * 10) + 1; // Random frame between 1-10 self.animationTimer = Math.random() * 80; // Random timer offset self.animationInterval = 80; // Switch frame every 80ms // Initialize all animation frames at start self.animationFrames = []; for (var frameIndex = 1; frameIndex <= 10; frameIndex++) { var frameAsset = self.attachAsset('floor_manager_' + frameIndex, { anchorX: 0.7, anchorY: 0.5 }); // Scale up boss enemies for each frame if (self.isBoss) { frameAsset.scaleX = 1.8; frameAsset.scaleY = 1.8; } // Hide all frames except the randomized starting frame frameAsset.visible = frameIndex === self.animationFrame; self.animationFrames.push(frameAsset); } // Remove the original enemyGraphics since we now have animation frames if (enemyGraphics && enemyGraphics.parent) { self.removeChild(enemyGraphics); } // Set enemyGraphics to the first frame for compatibility enemyGraphics = self.animationFrames[0]; } // Add animation logic for CEO if (self.type === 'ceo') { self.animationFrame = Math.floor(Math.random() * 11) + 1; // Random frame between 1-11 self.animationTimer = Math.random() * 100; // Random timer offset self.animationInterval = 100; // Switch frame every 80ms // Initialize all animation frames at start self.animationFrames = []; for (var frameIndex = 1; frameIndex <= 11; frameIndex++) { var frameAsset = self.attachAsset('ceo_' + frameIndex, { anchorX: 0.7, anchorY: 0.5 }); // Scale up boss enemies for each frame if (self.isBoss) { frameAsset.scaleX = 1.8; frameAsset.scaleY = 1.8; } // Hide all frames except the randomized starting frame frameAsset.visible = frameIndex === self.animationFrame; self.animationFrames.push(frameAsset); } // Remove the original enemyGraphics since we now have animation frames if (enemyGraphics && enemyGraphics.parent) { self.removeChild(enemyGraphics); } // Set enemyGraphics to the first frame for compatibility enemyGraphics = self.animationFrames[0]; } // Add animation logic for swarm enemy if (self.type === 'swarm') { self.animationFrame = Math.floor(Math.random() * 11) + 1; // Random frame between 1-11 self.animationTimer = Math.random() * 30; // Random timer offset self.animationInterval = 30; // Switch frame every 60ms (faster than floor_manager) // Initialize all animation frames at start self.animationFrames = []; for (var frameIndex = 1; frameIndex <= 11; frameIndex++) { var frameAsset = self.attachAsset('swarm_' + frameIndex, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies for each frame if (self.isBoss) { frameAsset.scaleX = 1.8; frameAsset.scaleY = 1.8; } // Hide all frames except the randomized starting frame frameAsset.visible = frameIndex === self.animationFrame; self.animationFrames.push(frameAsset); } // Remove the original enemyGraphics since we now have animation frames if (enemyGraphics && enemyGraphics.parent) { self.removeChild(enemyGraphics); } // Set enemyGraphics to the first frame for compatibility enemyGraphics = self.animationFrames[0]; } // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Fall back to regular enemy asset if specific type asset not found // Apply tint to differentiate enemy types /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies break; case 'immune': enemyGraphics.tint = 0xAA0000; // Red for immune enemies break; case 'flying': enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies break; case 'swarm': waveType = "Clones"; enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies break; }*/ // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Create multiple shadow layers for blur effect var blurLayers = 8; var baseAlpha = 0.06; for (var blurIndex = 0; blurIndex < blurLayers; blurIndex++) { var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect with decreasing alpha for blur shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = baseAlpha * (blurLayers - blurIndex) / blurLayers; // Increase offset between layers for more blur spread shadowGraphics.x = blurIndex * 5; shadowGraphics.y = blurIndex * 5; // Make shadow 20% larger than before (1.3 * 1.2 = 1.56) shadowGraphics.scaleX = 1.56; shadowGraphics.scaleY = 1.56; // Create simple blur effect without filters by using multiple offset layers // (The blur effect is already created by the multiple shadow layers above) // If this is a boss, scale up the shadow to match if (self.isBoss) { shadowGraphics.scaleX = 1.8 * 1.56; shadowGraphics.scaleY = 1.8 * 1.56; } } // Position shadow slightly offset self.shadow.x = 35; // Offset right self.shadow.y = 35; // Offset down // Store reference to all shadow graphics for rotation updates self.shadowGraphics = self.shadow.children; // Add 4 rotating propellers to flying enemies self.propellers = []; var propellerPositions = [{ x: -50, y: -50 }, // Top-left { x: 50, y: -50 }, // Top-right { x: -50, y: 50 }, // Bottom-left { x: 50, y: 50 } // Bottom-right ]; for (var i = 0; i < 4; i++) { var propeller = self.attachAsset('drone_propeller', { anchorX: 0.5, anchorY: 0.5 }); propeller.x = propellerPositions[i].x; propeller.y = propellerPositions[i].y; // Scale propellers for boss enemies if (self.isBoss) { propeller.scaleX = 1.8; propeller.scaleY = 1.8; propeller.x *= 1.8; propeller.y *= 1.8; } // Create a closure to capture the propeller for each iteration (function (currentPropeller) { // Start continuous rotation for each propeller tween(currentPropeller, { rotation: Math.PI * 2 }, { duration: Math.floor((500 + Math.random() * 200) * 0.8), // Slight variation in speed - 20% faster rotation easing: tween.linear, onFinish: function onFinish() { // Reset rotation and restart currentPropeller.rotation = 0; tween(currentPropeller, { rotation: Math.PI * 2 }, { duration: Math.floor((500 + Math.random() * 200) * 0.8), easing: tween.linear, onFinish: arguments.callee // Recursively call this function to loop }); } }); })(propeller); self.propellers.push(propeller); } } // Add vacuum container for normal enemy (robot vacuum cleaner) if (self.type === 'normal') { // Create vacuum container to hold both vacuum asset and brushes self.vacuumContainer = new Container(); self.addChild(self.vacuumContainer); // Move the enemy graphics to the vacuum container self.removeChild(enemyGraphics); self.vacuumContainer.addChild(enemyGraphics); // Reset enemy graphics position since it's now in vacuum container enemyGraphics.x = 0; enemyGraphics.y = 0; self.brushes = []; // Top left brush - position relative to enemy asset var topLeftBrush = self.vacuumContainer.attachAsset('vacuum_brush', { anchorX: 0.5, anchorY: 0.5 }); // Position relative to vacuum container center (not enemy asset size) topLeftBrush.x = 21; // Position in top left relative to container center topLeftBrush.y = 21; // Top right brush - position relative to enemy asset var topRightBrush = self.vacuumContainer.attachAsset('vacuum_brush', { anchorX: 0.5, anchorY: 0.5 }); topRightBrush.x = 21; // Position in top right relative to container center topRightBrush.y = -21; // Place brushes under the enemy asset by changing their index self.vacuumContainer.addChildAt(topLeftBrush, 0); self.vacuumContainer.addChildAt(topRightBrush, 0); // Scale brushes for boss enemies if (self.isBoss) { topLeftBrush.scaleX = 1.8; topLeftBrush.scaleY = 1.8; // Keep fixed relative positioning for boss enemies topLeftBrush.x = 45; // Scaled position relative to container center topLeftBrush.y = 45; topRightBrush.scaleX = 1.8; topRightBrush.scaleY = 1.8; topRightBrush.x = 45; // Scaled position relative to container center topRightBrush.y = -45; } // Start independent rotation for each brush (function (brush) { // Start continuous rotation for top left brush tween(brush, { rotation: Math.PI * 2 }, { duration: Math.floor(400 + Math.random() * 100), easing: tween.linear, onFinish: function onFinish() { // Reset rotation and restart brush.rotation = 0; tween(brush, { rotation: -Math.PI * 2 }, { duration: Math.floor(400 + Math.random() * 100), easing: tween.linear, onFinish: arguments.callee // Recursively call this function to loop }); } }); })(topLeftBrush); (function (brush) { // Start continuous rotation for top right brush (different speed) tween(brush, { rotation: Math.PI * 2 }, { duration: Math.floor(350 + Math.random() * 120), easing: tween.linear, onFinish: function onFinish() { // Reset rotation and restart brush.rotation = 0; tween(brush, { rotation: Math.PI * 2 }, { duration: Math.floor(350 + Math.random() * 120), easing: tween.linear, onFinish: arguments.callee // Recursively call this function to loop }); } }); })(topRightBrush); self.brushes.push(topLeftBrush); self.brushes.push(topRightBrush); } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5; healthBar.tint = 0x4d7c0f; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { // Handle floor manager animation by switching visibility between pre-initialized frames if (self.type === 'floor_manager') { // Only update animation frame counter, not graphics self.animationTimer += 1000 / 60; // Add milliseconds (assuming 60fps) if (self.animationTimer >= self.animationInterval) { self.animationTimer = 0; // Store current rotation before switching frames var currentRotation = enemyGraphics ? enemyGraphics.rotation : 0; // Hide current frame if (self.animationFrames && self.animationFrames[self.animationFrame - 1]) { self.animationFrames[self.animationFrame - 1].visible = false; } self.animationFrame = self.animationFrame % 10 + 1; // Cycle through frames 1-10 // Show new frame and immediately apply rotation if (self.animationFrames && self.animationFrames[self.animationFrame - 1]) { self.animationFrames[self.animationFrame - 1].visible = true; // Immediately apply the current rotation to the new frame to prevent flickering self.animationFrames[self.animationFrame - 1].rotation = currentRotation; enemyGraphics = self.animationFrames[self.animationFrame - 1]; // Update reference for compatibility } } } // Handle CEO animation by switching visibility between pre-initialized frames if (self.type === 'ceo') { // Only update animation frame counter, not graphics self.animationTimer += 1000 / 60; // Add milliseconds (assuming 60fps) if (self.animationTimer >= self.animationInterval) { self.animationTimer = 0; // Store current rotation before switching frames var currentRotation = enemyGraphics ? enemyGraphics.rotation : 0; // Hide current frame if (self.animationFrames && self.animationFrames[self.animationFrame - 1]) { self.animationFrames[self.animationFrame - 1].visible = false; } self.animationFrame = self.animationFrame % 11 + 1; // Cycle through frames 1-11 // Show new frame and immediately apply rotation if (self.animationFrames && self.animationFrames[self.animationFrame - 1]) { self.animationFrames[self.animationFrame - 1].visible = true; // Immediately apply the current rotation to the new frame to prevent flickering self.animationFrames[self.animationFrame - 1].rotation = currentRotation; enemyGraphics = self.animationFrames[self.animationFrame - 1]; // Update reference for compatibility } } } // Handle swarm animation by switching visibility between pre-initialized frames if (self.type === 'swarm') { // Only update animation frame counter, not graphics self.animationTimer += 1000 / 60; // Add milliseconds (assuming 60fps) if (self.animationTimer >= self.animationInterval) { self.animationTimer = 0; // Store current rotation before switching frames var currentRotation = enemyGraphics ? enemyGraphics.rotation : 0; // Hide current frame if (self.animationFrames && self.animationFrames[self.animationFrame - 1]) { self.animationFrames[self.animationFrame - 1].visible = false; } self.animationFrame = self.animationFrame % 11 + 1; // Cycle through frames 1-11 // Show new frame and immediately apply rotation if (self.animationFrames && self.animationFrames[self.animationFrame - 1]) { self.animationFrames[self.animationFrame - 1].visible = true; // Immediately apply the current rotation to the new frame to prevent flickering self.animationFrames[self.animationFrame - 1].rotation = currentRotation; enemyGraphics = self.animationFrames[self.animationFrame - 1]; // Update reference for compatibility } } } if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed, clear slow effects only self.slowed = false; self.slowEffect = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle slow effect if (self.slowed) { // Visual indication of slowed status if (!self.slowEffect) { self.slowEffect = true; } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Handle poison effect for all enemies (moved outside immune check) if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Set tint based on effect status if (self.poisoned && self.slowed) { // Combine poison (0x00FFAA) and slow (0x9900FF) colors // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212 enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); // For manager, CEO and swarm enemies, apply rotation to all animation frames to prevent flickering if ((self.type === 'floor_manager' || self.type === 'ceo' || self.type === 'swarm') && self.animationFrames) { // Initialize rotation for all frames if not set if (self.animationFrames[0].targetRotation === undefined) { for (var frameIdx = 0; frameIdx < self.animationFrames.length; frameIdx++) { self.animationFrames[frameIdx].targetRotation = angle; self.animationFrames[frameIdx].rotation = angle; } } else if (Math.abs(angle - self.animationFrames[0].targetRotation) > 0.1) { // Stop any existing tweens on all frames for (var frameIdx = 0; frameIdx < self.animationFrames.length; frameIdx++) { tween.stop(self.animationFrames[frameIdx], { rotation: true }); } // Calculate rotation for all frames var currentRotation = self.animationFrames[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Apply tween to all animation frames simultaneously for (var frameIdx = 0; frameIdx < self.animationFrames.length; frameIdx++) { self.animationFrames[frameIdx].targetRotation = angle; tween(self.animationFrames[frameIdx], { rotation: currentRotation + angleDiff }, { duration: 350, easing: tween.easeInOut }); } } } else { // For normal enemies with vacuum container, rotate the container instead of the graphics var rotationTarget = self.type === 'normal' && self.vacuumContainer ? self.vacuumContainer : enemyGraphics; if (rotationTarget) { // Initialize rotation correctly when enemy first gets a target if (rotationTarget.targetRotation === undefined) { rotationTarget.targetRotation = angle; rotationTarget.rotation = angle; } else if (Math.abs(angle - rotationTarget.targetRotation) > 0.1) { // Only update rotation if there's a significant difference to prevent flickering tween.stop(rotationTarget, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = rotationTarget.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } rotationTarget.targetRotation = angle; tween(rotationTarget, { rotation: currentRotation + angleDiff }, { duration: 350, easing: tween.easeInOut }); } } } } } // No need to update brush rotations separately since they're now part of the vacuum container healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [] }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal */ var midX = gridWidth / 2 - 1; for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0; if (i > midX - 3 && i <= midX + 3) { if (j === 0) { cellType = 2; self.spawns.push(cell); } else if (j <= 4) { cellType = 0; } else if (j === gridHeight - 1) { cellType = 3; self.goals.push(cell); } else if (j >= gridHeight - 4) { cellType = 0; } } cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; if (j > 3 && j <= gridHeight - 4) { if (showDebugCells) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } } } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { var before = new Date().getTime(); var toProcess = self.goals.concat([]); maxScore = 0; pathId += 1; for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathId; } function processNode(node, targetValue, targetNode) { if (node && node.type != 1) { if (node.pathId < pathId || targetValue < node.score) { node.targets = [targetNode]; } else if (node.pathId == pathId && targetValue == node.score) { node.targets.push(targetNode); } if (node.pathId < pathId || targetValue < node.score) { node.score = targetValue; if (node.pathId != pathId) { toProcess.push(node); } node.pathId = pathId; if (targetValue > maxScore) { maxScore = targetValue; } } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; var targetScore = node.score + 14142; if (node.up && node.left && node.up.type != 1 && node.left.type != 1) { processNode(node.upLeft, targetScore, node); } if (node.up && node.right && node.up.type != 1 && node.right.type != 1) { processNode(node.upRight, targetScore, node); } if (node.down && node.right && node.down.type != 1 && node.right.type != 1) { processNode(node.downRight, targetScore, node); } if (node.down && node.left && node.down.type != 1 && node.left.type != 1) { processNode(node.downLeft, targetScore, node); } targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId != pathId) { console.warn("Spawn blocked"); return true; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip enemies that haven't entered the viewable area yet if (enemy.currentCellY < 4) { continue; } // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 3) { return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 35; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 35; // Match enemy y-position + offset // Match shadow rotation with enemy rotation for all blur layers if (enemy.children[0] && enemy.shadowGraphics) { for (var shadowIndex = 0; shadowIndex < enemy.shadowGraphics.length; shadowIndex++) { enemy.shadowGraphics[shadowIndex].rotation = enemy.children[0].rotation; } } } // Check if the enemy has reached the entry area (y position is at least 5) var hasReachedEntryArea = enemy.currentCellY >= 4; // If enemy hasn't reached the entry area yet, just move down vertically if (!hasReachedEntryArea) { // Move directly downward enemy.currentCellY += enemy.speed; // Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) var angle = Math.PI / 2; // For normal enemies with vacuum container, rotate the container instead of the first child var rotationTarget = enemy.type === 'normal' && enemy.vacuumContainer ? enemy.vacuumContainer : enemy.children[0]; if (rotationTarget) { // Initialize rotation correctly when enemy first spawns if (rotationTarget.targetRotation === undefined) { rotationTarget.targetRotation = angle; rotationTarget.rotation = angle; } else if (Math.abs(angle - rotationTarget.targetRotation) > 0.1) { // Only update rotation if there's a significant difference to prevent flickering tween.stop(rotationTarget, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = rotationTarget.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it rotationTarget.targetRotation = angle; tween(rotationTarget, { rotation: currentRotation + angleDiff }, { duration: 300, easing: tween.easeInOut }); } } // Update enemy's position enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // If enemy has now reached the entry area, update cell coordinates if (enemy.currentCellY >= 4) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); } return false; } // After reaching entry area, handle flying enemies differently if (enemy.isFlying) { // Flying enemies head straight to the closest goal if (!enemy.flyingTarget) { // Set flying target to the closest goal enemy.flyingTarget = self.goals[0]; // Find closest goal if there are multiple if (self.goals.length > 1) { var closestDist = Infinity; for (var i = 0; i < self.goals.length; i++) { var goal = self.goals[i]; var dx = goal.x - enemy.cellX; var dy = goal.y - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; enemy.flyingTarget = goal; } } } } // Move directly toward the goal var ox = enemy.flyingTarget.x - enemy.currentCellX; var oy = enemy.flyingTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Reached the goal return true; } var angle = Math.atan2(oy, ox); // Rotate enemy graphic to match movement direction // For normal enemies with vacuum container, rotate the container instead of the first child var rotationTarget = enemy.type === 'normal' && enemy.vacuumContainer ? enemy.vacuumContainer : enemy.children[0]; if (rotationTarget) { // Initialize rotation correctly for flying enemies if (rotationTarget.targetRotation === undefined) { rotationTarget.targetRotation = angle; rotationTarget.rotation = angle; } else if (Math.abs(angle - rotationTarget.targetRotation) > 0.1) { // Only update rotation if there's a significant difference to prevent flickering tween.stop(rotationTarget, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = rotationTarget.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it rotationTarget.targetRotation = angle; tween(rotationTarget, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeInOut }); } } // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies if (!enemy.currentTarget) { enemy.currentTarget = cell.targets[0]; } if (enemy.currentTarget) { if (cell.score < enemy.currentTarget.score) { enemy.currentTarget = cell; } var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentTarget = undefined; return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var IntroModal = Container.expand(function () { var self = Container.call(this); // Full screen background var background = self.attachAsset('introBackground', { anchorX: 0.5, anchorY: 0.5 }); // Add a container with a new background asset, placed under the text and over the background var introBgContainer = new Container(); self.addChild(introBgContainer); // Place the new background asset in the container var introContainerBg = introBgContainer.attachAsset('introContainer', { anchorX: 0.5, anchorY: 0.5 }); introContainerBg.alpha = 0.7; introBgContainer.y = 4000; // Start off-screen at the bottom // Start with introBgContainer invisible for fade-in introBgContainer.alpha = 0; // Animate introBgContainer sliding up from y 4000 to 0 and fading in tween(introBgContainer, { y: 0, alpha: 1 }, { duration: 1000, easing: tween.easeOut }); // Title text // Instructions text var instructionText = new Text2("Fortify the cubicles with actual people power—management just ordered your replacements.\n\nFrom stapler snipes to cappuccino splash, weaponize office nonsense into necessary defense.\n\nDefend the budget—or get archived under “Former Staff”.", { size: 80, fill: 0xeeeeee, weight: 350, wordWrap: true, wordWrapWidth: 1400, align: 'left' }); instructionText.anchor.set(0, 0.5); instructionText.y = -150; instructionText.x = -instructionText.width / 2; introBgContainer.addChild(instructionText); // Let's go button var button = new Container(); introBgContainer.addChild(button); var buttonBg = button.attachAsset('button_go', { anchorX: 0.5, anchorY: 0.5 }); button.y = 500; // Add pulse animation to button button.startPulseAnimation = function () { tween(button, { scaleX: 1.1, scaleY: 1.1 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(button, { scaleX: 1.0, scaleY: 1.0 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { // Continue pulsing if button is still visible if (button.parent) { button.startPulseAnimation(); } } }); } }); }; // Start the pulse animation button.startPulseAnimation(); // Hide intro on button click button.down = function () { // Show GUI layer when intro modal closes LK.gui.visible = true; // Show tower instruction after intro modal closes var instruction = new TowerInstruction('tutorial_unlock'); instruction.x = 2048 / 2 - 250; instruction.y = 2732 - 700; // Position in lower portion of screen game.addChild(instruction); // Show tap action pointing to the first locked tower (default tower at index 1) if (sourceTowers && sourceTowers.length > 1) { var defaultTower = sourceTowers[1]; // Index 1 is the default tower var tapX = defaultTower.x - instruction.x - 40; var tapY = defaultTower.y - instruction.y + 100; // Slightly above the tower instruction.showTapAction(tapX, tapY + 50); } self.destroy(); }; return self; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonImage = self.attachAsset('next_wave_button', { anchorX: 0.5, anchorY: 0.5 }); self.enabled = false; self.visible = false; self.update = function () { if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { self.enabled = true; self.visible = true; buttonImage.tint = 0xFFFFFF; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonImage.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + "\n(" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification_bg', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 65, fill: 0xffffff, weight: 800 }); notificationText.anchor.set(0.5, 0.5); // Ensure background is larger than text with 20px margin on all sides notificationGraphics.width = notificationText.width + 40; // 20px margin on each side notificationGraphics.height = notificationText.height + 40; // 20px margin on top and bottom self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 250; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 250 * 2, 1); } else { self.destroy(); } }; return self; }); var StartGameButton = Container.expand(function () { var self = Container.call(this); // Add background image asset var buttonBg = self.attachAsset('start_game_bg', { anchorX: 0.5, anchorY: 0.5 }); // Add pulse animation self.startPulseAnimation = function () { tween(self, { scaleX: 1.1, scaleY: 1.1 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(self, { scaleX: 1.0, scaleY: 1.0 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { // Continue pulsing if button is still visible and game hasn't started if (self.parent && self.visible && waveIndicator && !waveIndicator.gameStarted) { self.startPulseAnimation(); } } }); } }); }; // Don't start pulse animation immediately - wait for first tower placement self.down = function () { if (waveIndicator && !waveIndicator.gameStarted) { // Switch from idle music to game music LK.playMusic('game_music'); waveIndicator.gameStarted = true; gameStarted = true; // Update global game started flag currentWave = 1; // Start directly with wave 1 waveTimer = 0; // Start from 0 instead of nextWaveTime to prevent instant jump waveInProgress = true; // Immediately start wave 1 waveSpawned = false; // Allow wave 1 to spawn buttonBg.tint = 0x00FF00; var waveType = waveIndicator.getWaveTypeName(1); var enemyCount = waveIndicator.getEnemyCount(1); var notification = game.addChild(new Notification("Wave 1 (" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; self.visible = false; } }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level var range; switch (self.id) { case 'sniper': // Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost if (self.level === self.maxLevel) { range = 12 * CELL_SIZE; // Significantly increased range for max level } else { range = (5 + (self.level - 1) * 0.8) * CELL_SIZE; } break; case 'splash': // Splash: base 2.6, +0.26 per level (30% increase from base 2, +0.2 per level) range = (2.6 + (self.level - 1) * 0.26) * CELL_SIZE; break; case 'slow': // Slow: base 3.5, +0.5 per level range = (3.5 + (self.level - 1) * 0.5) * CELL_SIZE; break; case 'poison': // Poison: base 3.2, +0.5 per level range = (3.2 + (self.level - 1) * 0.5) * CELL_SIZE; break; case 'office_prop': // Office prop: base 2, +0.3 per level range = (2 + (self.level - 1) * 0.3) * CELL_SIZE; break; default: // Default: base 3, +0.5 per level range = (3 + (self.level - 1) * 0.5) * CELL_SIZE; break; } // Reduce all tower ranges by 25% return range * 0.75; }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'sniper': self.fireRate = 90; self.damage = 25; self.range = 5 * CELL_SIZE; self.bulletSpeed = 3.1640625; break; case 'splash': self.fireRate = 75; self.damage = 15; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4; break; case 'slow': self.fireRate = 50; self.damage = 8; self.range = 3.5 * CELL_SIZE; self.bulletSpeed = 5; break; case 'poison': self.fireRate = 70; self.damage = 12; self.range = 3.2 * CELL_SIZE; self.bulletSpeed = 5; break; case 'office_prop': self.fireRate = 65; self.damage = 7; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4.5; break; } var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = CELL_SIZE * 2 / (maxDots + 1); var dotSize = CELL_SIZE * 1.5 / 6; for (var i = 0; i < maxDots; i++) { if (self.id === 'office_prop') { continue; } var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x999999; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0x999999; dot.x = -CELL_SIZE + dotSpacing * (i + 1); dot.y = CELL_SIZE * 0.83; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); gunContainer.rotation = 1.5707963267948966; // Start at 90 degrees until first target self.addChild(gunContainer); var readyAsset = 'PewPew'; var loadingAsset = 'PewPew_loading'; switch (self.id) { case 'sniper': readyAsset = 'sniper_tower'; loadingAsset = 'sniper_tower_loading'; break; case 'splash': readyAsset = 'splash_tower'; loadingAsset = 'splash_tower_loading'; break; case 'slow': readyAsset = 'slow_tower'; loadingAsset = 'slow_tower_loading'; break; case 'poison': readyAsset = 'poison_tower'; loadingAsset = 'poison_tower_loading'; break; case 'office_prop': var randomProp = Math.floor(Math.random() * 5) + 1; readyAsset = 'tower_office_prop_' + randomProp; loadingAsset = readyAsset; break; default: readyAsset = 'PewPew'; loadingAsset = 'PewPew_loading'; } var gunGraphicsReady = gunContainer.attachAsset(readyAsset, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.75, scaleY: 0.75 }); var gunGraphicsLoading = gunContainer.attachAsset(loadingAsset, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.75, scaleY: 0.75 }); if (self.id === 'office_prop') { gunGraphicsReady.x -= CELL_SIZE / 2; gunGraphicsReady.y += CELL_SIZE / 2; } gunGraphicsLoading.visible = false; self.gunGraphicsReady = gunGraphicsReady; self.gunGraphicsLoading = gunGraphicsLoading; self.isLoading = false; self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; if (dot && dot.children && dot.children.length > 1) { var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; var centerX = self.gridX + 1; var centerY = self.gridY + 1; var minI = Math.floor(centerX - rangeRadius - 0.5); var maxI = Math.ceil(centerX + rangeRadius + 0.5); var minJ = Math.floor(centerY - rangeRadius - 0.5); var maxJ = Math.ceil(centerY + rangeRadius + 0.5); for (var i = minI; i <= maxI; i++) { for (var j = minJ; j <= maxJ; j++) { var closestX = Math.max(i, Math.min(centerX, i + 1)); var closestY = Math.max(j, Math.min(centerY, j + 1)); var deltaX = closestX - centerX; var deltaY = closestY - centerY; var distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared <= rangeRadius * rangeRadius) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } } grid.renderDebug(); }; self.getTotalValue = function () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.level === self.maxLevel) { // Extra powerful last upgrade for all towers (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { // Office prop towers don't look for targets if (self.id === 'office_prop') { self.targetEnemy = null; return null; } var closestEnemy = null; var closestScore = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is in range if (distance <= self.getRange()) { // Handle flying enemies differently - they can be targeted regardless of path if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < closestScore) { closestScore = distToGoal; closestEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (distance < closestScore) { closestScore = distance; closestEnemy = enemy; } } } else { // For ground enemies, use the original path-based targeting // Get the cell for this enemy var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Use the cell's score (distance to exit) for prioritization // Lower score means closer to exit if (cell.score < closestScore) { closestScore = cell.score; closestEnemy = enemy; } } } } } if (!closestEnemy) { self.targetEnemy = null; } return closestEnemy; }; self.update = function () { self.targetEnemy = self.findTarget(); if (self.targetEnemy) { // Mark that this tower has found its first target if (self.hasHadFirstTarget === undefined) { self.hasHadFirstTarget = true; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); // Point tower towards target enemy with smooth rotation if (gunContainer.targetRotation === undefined) { gunContainer.targetRotation = angle; gunContainer.rotation = angle; } else { if (Math.abs(angle - gunContainer.targetRotation) > 0.05) { tween.stop(gunContainer, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = gunContainer.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } gunContainer.targetRotation = angle; tween(gunContainer, { rotation: currentRotation + angleDiff }, { duration: 200, easing: tween.easeInOut }); } } if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } else if (!self.hasHadFirstTarget) { // Keep rotation at 90 degrees if no target has been found yet var targetAngle = 1.5707963267948966; if (gunContainer.targetRotation === undefined) { gunContainer.targetRotation = targetAngle; gunContainer.rotation = targetAngle; } else { if (Math.abs(targetAngle - gunContainer.targetRotation) > 0.05) { tween.stop(gunContainer, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = gunContainer.rotation; var angleDiff = targetAngle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } gunContainer.targetRotation = targetAngle; tween(gunContainer, { rotation: currentRotation + angleDiff }, { duration: 300, easing: tween.easeInOut }); } } } }; self.down = function (x, y, obj) { // Remove tower instruction when user clicks a tower for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i] instanceof TowerInstruction) { game.children[i].destroy(); } } if (self.id === 'office_prop') { return; } var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } selectedTower = null; grid.renderDebug(); return; } for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = self; var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; game.addChild(rangeIndicator); rangeIndicator.x = self.x; rangeIndicator.y = self.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { // Office prop towers don't fire if (self.id === 'office_prop') { return; } if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; // Special handling for slow tower - create word projectile instead of bullet if (self.id === 'slow') { // Array of random boring life sentences var boringLifeSentences = ['My cat got a new friend!', 'I heard Otto fart', 'Hildas dress does NOT match her hair color!', 'I reorganized my sock drawer today', 'The grocery store was out of my usual bread', 'I watched paint dry for 3 hours', 'My neighbor trimmed their hedge again', 'I found a penny on the sidewalk', 'The mailman was 5 minutes late today', 'I alphabetized my spice rack', 'My phone battery died at 47%', 'I counted 23 birds outside my window', 'The elevator music was slightly off-key', 'I folded laundry for the third time this week', 'My coffee was lukewarm this morning', 'I noticed a new crack in the ceiling', 'The bus driver wore different shoes today', 'I defrosted my freezer on Sunday', 'My pen ran out of ink mid-sentence', 'I watered the same plant twice by mistake']; // Pick a random sentence and split into words var randomSentence = boringLifeSentences[Math.floor(Math.random() * boringLifeSentences.length)]; var words = randomSentence.split(' '); // Add extra spacing between words by joining with multiple spaces var spacedWords = words.join(' '); // 5 spaces between words var finalWords = spacedWords.split(' ').filter(function (w) { return w.length > 0; }); // Split again but keep extra spaces as separate elements // Create individual word projectiles with slight delays and offsets for (var wordIndex = 0; wordIndex < finalWords.length; wordIndex++) { var word = finalWords[wordIndex]; var isFirstWord = wordIndex === 0; // Create slight offset for each word so they don't all overlap var offsetAngle = gunContainer.rotation + (wordIndex - finalWords.length / 2) * 0.2; var offsetDistance = 40 + wordIndex * 10; var wordBulletX = self.x + Math.cos(offsetAngle) * offsetDistance; var wordBulletY = self.y + Math.sin(offsetAngle) * offsetDistance; // Create word projectile with slight delay (function (currentWord, currentIndex, currentIsFirst, currentX, currentY) { LK.setTimeout(function () { var wordProjectile = new WordProjectile(currentX, currentY, self.targetEnemy, self.damage, self.bulletSpeed, self.id, currentWord, currentIsFirst); wordProjectile.sourceTowerLevel = self.level; game.addChild(wordProjectile); bullets.push(wordProjectile); if (currentIsFirst) { // Only the first word counts for targeting tracking self.targetEnemy.bulletsTargetingThis.push(wordProjectile); } }, currentIndex * 100); // 100ms delay between each word })(word, wordIndex, isFirstWord, wordBulletX, wordBulletY); } // Play slow tower firing sound LK.getSound('fire_slow').play(); } else if (self.id === 'splash') { // Otto throws 50 coffee particles in the direction of the target var targetAngle = Math.atan2(self.targetEnemy.y - self.y, self.targetEnemy.x - self.x); // Create 50 coffee particles for (var particleIndex = 0; particleIndex < 50; particleIndex++) { // Create slight spread around the target direction var spreadAngle = targetAngle + (Math.random() - 0.5) * 0.8; // 0.8 radian spread var particleSpeed = self.bulletSpeed + (Math.random() - 0.5) * 2; // Slight speed variation var particleDistance = 20 + Math.random() * 30; // Random distance from tower center var particleX = self.x + Math.cos(spreadAngle) * particleDistance; var particleY = self.y + Math.sin(spreadAngle) * particleDistance; // Create coffee particle var coffeeParticle = new Container(); var particleGraphics = coffeeParticle.attachAsset('bullet_splash', { anchorX: 0.5, anchorY: 0.5 }); particleGraphics.scaleX = 0.3 + Math.random() * 0.4; // Random size between 0.3-0.7 particleGraphics.scaleY = 0.3 + Math.random() * 0.4; particleGraphics.tint = 0x8B4513; // Coffee brown color particleGraphics.rotation = Math.random() * Math.PI * 2; // Random rotation coffeeParticle.x = particleX; coffeeParticle.y = particleY; game.addChild(coffeeParticle); // Animate particle flying toward target with slight randomness var particleEndX = self.targetEnemy.x + (Math.random() - 0.5) * 100; var particleEndY = self.targetEnemy.y + (Math.random() - 0.5) * 100; (function (currentParticle, currentIndex) { tween(currentParticle, { x: particleEndX, y: particleEndY, rotation: particleGraphics.rotation + Math.PI * 4, // Spin while flying alpha: 0.7 }, { duration: 400 + Math.random() * 200, // Slight variation in flight time easing: tween.easeOut, onFinish: function onFinish() { if (currentParticle.parent) { game.removeChild(currentParticle); } } }); })(coffeeParticle, particleIndex); } // Still create the main bullet for damage var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed, self.id); // Make Otto's bullet invisible bullet.alpha = 0; game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // Play splash tower firing sound LK.getSound('fire_splash').play(); } else if (self.id === 'poison') { // Rank Frank spawns 30 particles when firing var particleColors = [0xc59533, 0xb8882d, 0xb7b82d]; // Available colors // Calculate bullet direction for particle movement var bulletDirection = Math.atan2(self.targetEnemy.y - self.y, self.targetEnemy.x - self.x); var bulletVelocityX = Math.cos(bulletDirection) * self.bulletSpeed; var bulletVelocityY = Math.sin(bulletDirection) * self.bulletSpeed; // Calculate particle count based on tower level (20% more per upgrade) var baseParticleCount = 30; var particleCount = Math.floor(baseParticleCount * (1 + (self.level - 1) * 0.2)); for (var particleIndex = 0; particleIndex < particleCount; particleIndex++) { // Create poison particle var poisonParticle = new Container(); var particleGraphics = poisonParticle.attachAsset('bullet_poison', { anchorX: 0.5, anchorY: 0.5 }); // Random color from the specified array var randomColor = particleColors[Math.floor(Math.random() * particleColors.length)]; particleGraphics.tint = randomColor; // Start small and at bullet spawn position particleGraphics.scaleX = 0.3; particleGraphics.scaleY = 0.3; poisonParticle.x = bulletX + (Math.random() - 0.5) * 40; // Small random spread around bullet spawn poisonParticle.y = bulletY + (Math.random() - 0.5) * 40; poisonParticle.alpha = 1; game.addChild(poisonParticle); // Create closure to capture particle reference (function (currentParticle, currentGraphics) { // Calculate particle end position based on bullet velocity and particle lifetime var particleLifetime = 800 + Math.random() * 400; // Duration between 800-1200ms var particleEndX = currentParticle.x + bulletVelocityX * (particleLifetime / 16.67); // Approximate frames at 60fps var particleEndY = currentParticle.y + bulletVelocityY * (particleLifetime / 16.67); // Animate particle to move, grow and fade out tween(currentParticle, { x: particleEndX, y: particleEndY, alpha: 0, scaleX: 1.2 + Math.random() * 0.8, // Grow to random size between 1.2-2.0 scaleY: 1.2 + Math.random() * 0.8 }, { duration: particleLifetime, easing: tween.easeOut, onFinish: function onFinish() { if (currentParticle.parent) { game.removeChild(currentParticle); } } }); })(poisonParticle, particleGraphics); } // Still create the main bullet for damage var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed, self.id); // Make poison bullet invisible bullet.alpha = 0; game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // Play poison tower firing sound LK.getSound('fire_poison').play(); } else { var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed, self.id); // For slow tower, pass level for scaling slow effect if (self.id === 'slow') { bullet.sourceTowerLevel = self.level; } if (self.id === 'sniper') { LK.getSound('fire_sniper').play(); } else { LK.getSound('fire_pewpew').play(); } game.addChild(bullet); // Add bullet shadow to shadow layer for sniper and default bullets if ((self.id === 'sniper' || self.id === 'default') && bullet.shadow) { enemyLayerMiddle.addChild(bullet.shadow); } bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); } // Switch to loading asset immediately after firing self.isLoading = true; self.gunGraphicsReady.visible = false; self.gunGraphicsLoading.visible = true; // Switch back to ready asset after 200ms tween({}, {}, { duration: 500, onFinish: function onFinish() { self.isLoading = false; self.gunGraphicsReady.visible = true; self.gunGraphicsLoading.visible = false; } }); // --- Fire recoil effect for gunContainer --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); // Always use the original resting position for recoil, never accumulate offset if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset to resting position before animating (in case of interrupted tweens) gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Calculate recoil offset (recoil back along the gun's rotation) var recoilDistance = 8; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; // Animate recoil back from the resting position tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original position but keep rotation tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; // Office prop towers take only 1 cell, others take 2x2 var cellsToOccupy = self.id === 'office_prop' ? 1 : 2; for (var i = 0; i < cellsToOccupy; i++) { for (var j = 0; j < cellsToOccupy; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 1; } } } self.refreshCellsInRange(); }; return self; }); var TowerInstruction = Container.expand(function (assetId) { var self = Container.call(this); // Background image var background = self.attachAsset('tutorial_drag', { anchorX: 0.5, anchorY: 0.5 }); if (assetId === 'tutorial_unlock') { background = self.attachAsset('tutorial_unlock', { anchorX: 0.5, anchorY: 0.5 }); } // Add hand indicator self.handIndicator = null; // Function to show tap action at specific position self.showTapAction = function (x, y) { // Remove existing hand if any if (self.handIndicator) { self.removeChild(self.handIndicator); self.handIndicator = null; } // Create hand container self.handIndicator = new Container(); self.addChild(self.handIndicator); // Create hand asset var hand = self.handIndicator.attachAsset('tutorial_hand', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2 }); hand.rotation = Math.PI / 4; // Point down-right // Position hand at tap location self.handIndicator.x = x; self.handIndicator.y = y; // Animate tap motion var _animateTap = function animateTap() { tween(self.handIndicator, { scaleX: 0.8, scaleY: 0.8, y: y + 20 }, { duration: 400, easing: tween.easeInOut, onFinish: function onFinish() { tween(self.handIndicator, { scaleX: 1.0, scaleY: 1.0, y: y }, { duration: 400, easing: tween.easeInOut, onFinish: function onFinish() { if (self.handIndicator && self.handIndicator.parent) { _animateTap(); } } }); } }); }; _animateTap(); }; // Function to show drag action from start to end point self.showDragAction = function (startX, startY, endX, endY) { // Remove existing hand if any if (self.handIndicator) { self.removeChild(self.handIndicator); self.handIndicator = null; } // Create hand container self.handIndicator = new Container(); self.addChild(self.handIndicator); // Create hand asset var hand = self.handIndicator.attachAsset('tutorial_hand', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2 }); // Calculate angle for hand rotation var angle = Math.atan2(endY - startY, endX - startX) + Math.PI / 4; hand.rotation = angle; // Position hand at start location self.handIndicator.x = startX; self.handIndicator.y = startY; // Create drag trail dots var trailDots = []; var dotCount = 5; for (var i = 0; i < dotCount; i++) { var dot = self.handIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); dot.width = 12; dot.height = 12; dot.tint = 0xFFFFFF; dot.alpha = 0.3 + i / dotCount * 0.4; // Fade in along trail // Position dots along the path var t = i / (dotCount - 1); dot.x = (endX - startX) * t; dot.y = (endY - startY) * t; trailDots.push(dot); } // Animate drag motion var _animateDrag = function animateDrag() { // Reset position and hide trail self.handIndicator.x = startX; self.handIndicator.y = startY; for (var i = 0; i < trailDots.length; i++) { trailDots[i].alpha = 0; } // Animate hand moving tween(self.handIndicator, { x: endX, y: endY }, { duration: 1500, easing: tween.easeInOut, onUpdate: function onUpdate(progress) { // Show trail dots progressively for (var i = 0; i < trailDots.length; i++) { var dotProgress = i / (dotCount - 1); if (progress > dotProgress) { var fadeProgress = (progress - dotProgress) * dotCount; trailDots[i].alpha = Math.min(0.3 + i / dotCount * 0.4, fadeProgress); } } }, onFinish: function onFinish() { // Fade out trail tween(self.handIndicator, { alpha: 0.3 }, { duration: 300, onFinish: function onFinish() { self.handIndicator.alpha = 1; if (self.handIndicator && self.handIndicator.parent) { _animateDrag(); } } }); } }); }; _animateDrag(); }; // Start pulse animation immediately upon creation self.startJellyAnimation = function () { tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(self, { scaleX: 1.0, scaleY: 1.0 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { // Continue pulsing if still attached if (self.parent) { self.startJellyAnimation(); } } }); } }); }; // Start the jelly animation self.startJellyAnimation(); return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'default'; self.hasEnoughGold = true; self.draggedTower = null; // Reference to tower being moved var rangeIndicator = new Container(); self.addChild(rangeIndicator); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.alpha = 0.3; var previewGraphics = self.attachAsset('towerpreview', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; // Update preview size when tower type changes self.updatePreviewSize = function () { if (self.towerType === 'office_prop') { previewGraphics.width = CELL_SIZE; previewGraphics.height = CELL_SIZE; } else { previewGraphics.width = CELL_SIZE * 2; previewGraphics.height = CELL_SIZE * 2; } }; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(self.towerType); // Only update appearance if the affordability status has changed if (previousHasEnoughGold !== self.hasEnoughGold) { self.updateAppearance(); } }; self.updateAppearance = function () { // Use Tower class to get the source of truth for range var tempTower = new Tower(self.towerType); var previewRange = tempTower.getRange(); // Clean up tempTower to avoid memory leaks if (tempTower && tempTower.destroy) { tempTower.destroy(); } // Set range indicator using unified range logic rangeGraphics.width = rangeGraphics.height = previewRange * 2; switch (self.towerType) { case 'sniper': previewGraphics.tint = 0xFF5500; break; case 'splash': previewGraphics.tint = 0x33CC00; break; case 'slow': previewGraphics.tint = 0x9900FF; break; case 'poison': previewGraphics.tint = 0x00FFAA; break; case 'office_prop': previewGraphics.tint = 0xFFAA00; break; default: previewGraphics.tint = 0xAAAAAA; } if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } // Update preview size based on tower type self.updatePreviewSize(); }; self.updatePlacementStatus = function () { var validGridPlacement = true; var cellsToCheck = self.towerType === 'office_prop' ? 1 : 2; if (self.gridY <= 4 || self.gridY + (cellsToCheck - 1) >= grid.cells[0].length - 4) { validGridPlacement = false; } else { for (var i = 0; i < cellsToCheck; i++) { for (var j = 0; j < cellsToCheck; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); if (!cell || cell.type !== 0) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } self.blockedByEnemy = false; if (validGridPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.currentCellY < 4) { continue; } // Only check non-flying enemies, flying enemies can pass over towers if (!enemy.isFlying) { var cellsToCheck = self.towerType === 'office_prop' ? 1 : 2; if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + cellsToCheck && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + cellsToCheck) { self.blockedByEnemy = true; break; } if (enemy.currentTarget) { var targetX = enemy.currentTarget.x; var targetY = enemy.currentTarget.y; if (targetX >= self.gridX && targetX < self.gridX + cellsToCheck && targetY >= self.gridY && targetY < self.gridY + cellsToCheck) { self.blockedByEnemy = true; break; } } } } } self.canPlace = validGridPlacement && !self.blockedByEnemy; self.hasEnoughGold = gold >= getTowerCost(self.towerType); self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2; if (self.towerType === 'office_prop') { self.x -= CELL_SIZE / 2; self.y -= CELL_SIZE / 2; } self.checkPlacement(); }; return self; }); var TowerShop = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Check if tower is unlocked self.isUnlocked = function () { return storage.unlockedTowers && storage.unlockedTowers[self.towerType]; }; // Increase size of base for easier touch var baseGraphics = self.attachAsset('tower_background', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.95, scaleY: 1.95 }); // Add tower asset on top of base for all tower types var towerAsset; switch (self.towerType) { case 'sniper': baseGraphics.tint = 0xFF5500; towerAsset = self.attachAsset('sniper_tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.225125, scaleY: 1.225125, rotation: Math.PI / 2 }); break; case 'splash': baseGraphics.tint = 0x33CC00; towerAsset = self.attachAsset('splash_tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.225125, scaleY: 1.225125, rotation: Math.PI / 2 }); break; case 'slow': baseGraphics.tint = 0x9900FF; towerAsset = self.attachAsset('slow_tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.225125, scaleY: 1.225125, rotation: Math.PI / 2 }); break; case 'poison': baseGraphics.tint = 0x00FFAA; towerAsset = self.attachAsset('poison_tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.225125, scaleY: 1.225125, rotation: Math.PI / 2 }); break; case 'office_prop': baseGraphics.tint = 0xFFAA00; var randomProp = Math.floor(Math.random() * 5) + 1; towerAsset = self.attachAsset('tower_office_prop_' + randomProp, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.225125, scaleY: 1.225125, rotation: Math.PI / 2 }); break; default: baseGraphics.tint = 0xAAAAAA; towerAsset = self.attachAsset('PewPew', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.225125, scaleY: 1.225125, rotation: Math.PI / 2 }); } // Rotate tower asset in shop by 90 degrees tween(towerAsset, { rotation: Math.PI / 2 }, { duration: 0 }); // Add lock overlay for locked towers var lockOverlay = null; if (!self.isUnlocked()) { lockOverlay = new Container(); self.addChild(lockOverlay); var lockBg = lockOverlay.attachAsset('lock', { anchorX: 0.5, anchorY: 0.5 }); lockBg.width = 146; // 20% larger than 122 lockBg.height = 146; // 20% larger than 122 lockBg.y = 75; // Position over the tower asset center (moved up by 5px from 80) lockBg.alpha = 0.8; // Set alpha to 0.8 lockOverlay.addChild(lockBg); } var towerCost = getTowerCost(self.towerType); // Get display name for tower type var displayName; switch (self.towerType) { case 'default': displayName = "Linus"; break; case 'sniper': displayName = "Hilda"; break; case 'splash': displayName = "Otto"; break; case 'slow': displayName = "Sunny"; break; case 'poison': displayName = "Rank Frank"; break; case 'office_prop': displayName = "Prop"; break; default: displayName = self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1); } // Add shadow for tower type label var typeLabelShadow = new Text2(displayName, { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(displayName, { size: 50, fill: baseGraphics.tint, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -180; // Position above center of tower (moved up by 10px from -155) typeLabelShadow.y = typeLabel.y + 4; self.addChild(typeLabel); // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; self.addChild(costLabelShadow); // Add container background for price var priceBackground = self.attachAsset('price_container', { anchorX: 0.5, anchorY: 0.5 }); priceBackground.y = 185; // Position 10px further down (was 175) priceBackground.alpha = 0.8; tween(priceBackground, { tint: 0x006400 }, { duration: 0 }); // Tint dark green // Add cost label var costLabel = new Text2(towerCost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 185; // Position 10px further down (was 175) costLabelShadow.y = costLabel.y + 4; self.addChild(costLabel); self.down = function () { if (!self.isUnlocked()) { // Check if user can afford the tower before showing unlock dialog var canAfford = gold >= getTowerCost(self.towerType); if (canAfford) { // Show unlock dialog var unlockDialog = new TowerUnlockDialog(self.towerType); unlockDialog.x = 2048 / 2; unlockDialog.y = 2732 / 2; game.addChild(unlockDialog); } return; } // Allow dragging from shop during waves - no automatic placement }; self.update = function () { // Update lock overlay visibility if (lockOverlay) { lockOverlay.visible = !self.isUnlocked(); } // Only show price labels for unlocked towers costLabel.visible = self.isUnlocked(); costLabelShadow.visible = self.isUnlocked(); priceBackground.visible = self.isUnlocked(); // Check if player can afford this tower and if it's unlocked var canAfford = gold >= getTowerCost(self.towerType); // Update price text color and tower alpha based on affordability if (self.isUnlocked()) { if (canAfford) { costLabel.tint = 0xFFD700; // Gold color when affordable self.alpha = 1.0; // Full opacity when affordable towerAsset.alpha = 1.0; // Full opacity for tower asset } else { costLabel.tint = 0xFF0000; // Red color when not affordable self.alpha = 0.5; // Reduced opacity when not affordable towerAsset.alpha = 0.5; // Reduced opacity for tower asset } } // Add pulsate animation to lock if player can afford and tower is locked if (!self.isUnlocked() && canAfford && lockOverlay) { var lockBg = lockOverlay.children[0]; // Get the lock asset if (lockBg && !lockBg.isPulsating) { lockBg.isPulsating = true; // Create pulsate animation function var _startPulsate = function startPulsate() { tween(lockBg, { scaleX: 0.8, scaleY: 0.8, alpha: 0.7 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(lockBg, { scaleX: 1.0, scaleY: 1.0, alpha: 0.6 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { // Continue pulsating if still affordable and locked if (!self.isUnlocked() && gold >= getTowerCost(self.towerType)) { _startPulsate(); } else { lockBg.isPulsating = false; } } }); } }); }; _startPulsate(); } } else if (lockOverlay && lockOverlay.children[0]) { // Stop pulsating if conditions no longer met var lockBg = lockOverlay.children[0]; if (lockBg.isPulsating) { tween.stop(lockBg, { scaleX: true, scaleY: true, alpha: true }); lockBg.scaleX = 0.8; lockBg.scaleY = 0.8; lockBg.alpha = 0.8; lockBg.isPulsating = false; } } // Use tween to animate to 1.134x size (10% smaller than 1.26x) and move down for all towers tween(self, { scaleX: 1.134, scaleY: 1.134, y: towerY + 50 }, { duration: 300, easing: tween.easeOut }); }; return self; }); var TowerUnlockDialog = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType; // Destroy tutorial_unlock TowerInstruction when unlock modal is shown for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i] instanceof TowerInstruction) { game.children[i].destroy(); } } // Add black background that fills the screen var screenBackground = self.attachAsset('tower_unlock_modal_back', { anchorX: 0.5, anchorY: 0.5 }); screenBackground.width = 2048; screenBackground.height = 2732; screenBackground.tint = 0x000000; screenBackground.alpha = 0.8; // Add click handler to consume clicks on background screenBackground.down = function () { // Consume the click - do nothing but prevent it from propagating }; // Add full overlay background asset var overlayBackground = self.attachAsset('unlock_overlay_bg', { anchorX: 0.5, anchorY: 0.5 }); overlayBackground.height = 2732 * 0.8; overlayBackground.width = overlayBackground.height * (3 / 4); overlayBackground.alpha = 1.0; // Add background pattern or texture // Create a grid pattern background var backgroundPattern = new Container(); self.addChild(backgroundPattern); // Main dialog container var dialog = new Container(); self.addChild(dialog); // Dialog background // Tower title - use same display names as tower shop var displayName; switch (towerType) { case 'default': displayName = "Linus"; break; case 'sniper': displayName = "Hilda"; break; case 'splash': displayName = "Otto"; break; case 'slow': displayName = "Sunny"; break; case 'poison': displayName = "Rank Frank"; break; case 'office_prop': displayName = "Office Prop"; break; default: displayName = towerType.charAt(0).toUpperCase() + towerType.slice(1); } var titleText = new Text2(displayName, { size: 160, fill: 0x000000, weight: 800 }); titleText.anchor.set(0.5, 0.5); titleText.y = -880; dialog.addChild(titleText); // Tower image (larger version) var towerAsset = 'PewPew'; switch (towerType) { case 'sniper': towerAsset = 'sniper_tower'; break; case 'splash': towerAsset = 'splash_tower'; break; case 'slow': towerAsset = 'slow_tower'; break; case 'poison': towerAsset = 'poison_tower'; break; case 'office_prop': var randomProp = Math.floor(Math.random() * 5) + 1; towerAsset = 'tower_office_prop_' + randomProp; break; } var towerImage = dialog.attachAsset(towerAsset, { anchorX: 0.5, anchorY: 0.5, scaleX: 3, scaleY: 3, rotation: Math.PI / 2 }); towerImage.y = -380; // Tower type text - separate and bold var towerTypeLabel = ''; var description = ''; switch (towerType) { case 'default': towerTypeLabel = 'Damage type: Basic'; description = 'Linus from IT treats his staple gun like a precision instrument with patch notes. With clinical aim—He fires ISO-compliant, armor-piercing office metal.\n\nPlease submit a ticket for removal; response time 3–5 business minutes.'; break; case 'sniper': towerTypeLabel = 'Damage type: Long Range'; description = 'Hilda from accounting has mastered the ancient art of paper airplane warfare. With surgical precision, she can hit you from across three cubicles. Her range is legendary, her patience infinite, and her paper cuts devastating. Please allow 3-5 business days for elimination.'; break; case 'splash': towerTypeLabel = 'Damage type: Hot Splash'; description = 'Otto from the break room takes his coffee very seriously - and very hot. One splash of his premium blend can melt through enemy armor and several layers of office carpet. Side effects include: area damage, caffeine addiction, and an inexplicable urge to discuss quarterly reports.'; break; case 'slow': towerTypeLabel = 'Damage type: Crowd Control'; description = 'Sunny from HR has perfected corporate small talk. Her mind-numbing stories about her weekend gardening can slow down even the most determined enemies. "Did I tell you about my petunias?" she asks, as productivity grinds to a halt. Warning: May cause extreme drowsiness.'; break; case 'poison': towerTypeLabel = 'Damage type: Sickening'; description = 'Rank Frank, the overnight janitor, has endured too much corporate nonsense. His legendary bad breath creates toxic clouds that slowly sicken anyone foolish enough to get close. His motto: "I clean up messes... permanently." Warning: Side effects include nausea and career termination.'; break; case 'office_prop': towerTypeLabel = 'Damage type: Basic Defensive'; description = 'Standard office equipment repurposed for defense. These everyday items may look harmless, but in the right hands they become surprisingly effective deterrents. Compact, affordable, and blends perfectly with any office decor. Some assembly required.'; break; default: towerTypeLabel = 'Tower type: Basic'; description = 'Basic defensive tower'; } // Create separate tower type text with bold styling var towerTypeText = new Text2(towerTypeLabel, { size: 50, fill: 0xee0000, weight: 800, // Bold weight wordWrap: false }); towerTypeText.anchor.set(0.5, 0.5); towerTypeText.y = 50; // Position below tower image dialog.addChild(towerTypeText); // Create description text without tower type var descText = new Text2(description, { size: 70, fill: 0x333333, weight: 400, wordWrap: true, wordWrapWidth: 1400, align: 'center' }); descText.anchor.set(0.5, 0.5); descText.y = 420; // Adjusted position to account for separate tower type text dialog.addChild(descText); // Buy button var buyButton = new Container(); dialog.addChild(buyButton); buyButton.y = 900; var buyBg = buyButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buyBg.width = 600; buyBg.height = 160; buyBg.tint = 0x00AA00; var cost = getTowerCost(towerType); var buyText = new Text2('Unlock: ' + cost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800, wordWrap: true, wordWrapWidth: 520 }); buyText.anchor.set(0.5, 0.5); buyButton.addChild(buyText); // Cancel button var cancelButton = new Container(); dialog.addChild(cancelButton); cancelButton.x = overlayBackground.width / 2 - 80; cancelButton.y = -overlayBackground.height / 2 + 80; var cancelBg = cancelButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); cancelBg.width = 100; cancelBg.height = 100; cancelBg.tint = 0xAA0000; var cancelText = new Text2('X', { size: 60, fill: 0xFFFFFF, weight: 800, wordWrap: true, wordWrapWidth: 80 }); cancelText.anchor.set(0.5, 0.5); cancelButton.addChild(cancelText); // Position buy button (centered) buyButton.x = 0; // Button handlers buyButton.down = function () { if (gold >= cost) { setGold(gold - cost); storage.unlockedTowers = storage.unlockedTowers || {}; storage.unlockedTowers[towerType] = true; var notification = game.addChild(new Notification(towerType.charAt(0).toUpperCase() + towerType.slice(1) + " tower unlocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; // Check if this is the first tower unlock (excluding office_prop) var unlockedCount = 0; for (var key in storage.unlockedTowers) { if (storage.unlockedTowers[key] && key !== 'office_prop') { unlockedCount++; } } // Show instruction for first unlock if (unlockedCount === 1 && !hasShownFirstUnlockInstruction) { hasShownFirstUnlockInstruction = true; // Find the tower shop element for this tower type for (var i = 0; i < sourceTowers.length; i++) { if (sourceTowers[i].towerType === towerType) { var instruction = new TowerInstruction('tutorial_drag'); instruction.x = sourceTowers[i].x + 130; instruction.y = sourceTowers[i].y - 350; // Position above the tower game.addChild(instruction); // Show drag action from tower to grid var startX = sourceTowers[i].x - instruction.x + 100; var startY = sourceTowers[i].y - instruction.y + 75; // Start from the tower var endX = 2048 / 2 - instruction.x; // Drag to center of screen var endY = grid.y + CELL_SIZE * 8 - instruction.y; // Drag to middle of grid instruction.showDragAction(startX, startY, endX, endY); break; } } } self.destroy(); } else { var notification = game.addChild(new Notification("Not enough gold to unlock!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; cancelButton.down = function () { self.destroy(); }; // Center dialog on screen dialog.x = 0; dialog.y = 0; // Add wiggle animation if player can afford to unlock var lockBg = null; if (overlayBackground && overlayBackground.parent) { // Find the lock image in the dialog for (var i = 0; i < dialog.children.length; i++) { if (dialog.children[i] && dialog.children[i].children) { for (var j = 0; j < dialog.children[i].children.length; j++) { var child = dialog.children[i].children[j]; if (child && child.texture && child.width === 146 && child.height === 146) { lockBg = child; break; } } } if (lockBg) { break; } } } // Create wiggle animation function self.startWiggle = function () { if (lockBg && lockBg.parent && gold >= cost) { // Stop any existing wiggle tween.stop(lockBg, { rotation: true }); // Reset rotation lockBg.rotation = 0; // Create wiggle animation var wiggleAngle = 0.15; // About 8.5 degrees tween(lockBg, { rotation: wiggleAngle }, { duration: 100, easing: tween.easeInOut, onFinish: function onFinish() { tween(lockBg, { rotation: -wiggleAngle }, { duration: 200, easing: tween.easeInOut, onFinish: function onFinish() { tween(lockBg, { rotation: wiggleAngle }, { duration: 200, easing: tween.easeInOut, onFinish: function onFinish() { tween(lockBg, { rotation: 0 }, { duration: 100, easing: tween.easeInOut, onFinish: function onFinish() { // Repeat the wiggle after a short pause LK.setTimeout(function () { self.startWiggle(); }, 500); } }); } }); } }); } }); } }; // Start wiggle animation immediately if player can afford it if (gold >= cost) { self.startWiggle(); } // Add update method to check affordability self.update = function () { var canAfford = gold >= cost; if (canAfford && !self.wiggling) { self.wiggling = true; self.startWiggle(); } else if (!canAfford && self.wiggling) { self.wiggling = false; tween.stop(lockBg, { rotation: true }); if (lockBg) { lockBg.rotation = 0; } } }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; // Get display name for tower type var displayName; switch (self.tower.id) { case 'default': displayName = "Linus"; break; case 'sniper': displayName = "Hilda"; break; case 'splash': displayName = "Otto"; break; case 'slow': displayName = "Sunny"; break; case 'poison': displayName = "Rank Frank"; break; case 'office_prop': displayName = "Office Prop"; break; default: displayName = self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1); } var towerTypeText = new Text2(displayName, { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' gold'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; var cellsToFree = self.tower.id === 'office_prop' ? 1 : 2; for (var i = 0; i < cellsToFree; i++) { for (var j = 0; j < cellsToFree; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 0; var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; // Add game start marker as the first element var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2 * 0.7 + 10; // 10px higher startBlock.tint = 0x41A0A0; // Use teal/cyan from color scheme for start // Add start text var startTextShadow = new Text2("PREPARE", { size: 56, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("PREPARE", { size: 56, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startText.y = 0; startMarker.addChild(startText); // Position the start marker at the beginning startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); // Now create wave markers, adjusting their positions to account for the start marker for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('wave_block', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2 * 0.7 + 10; // 10px higher // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // New color scheme mapping var colorScheme = { normal: 0xF36C44, // Dark navy blue floor_manager: 0x2E5A7D, // Muted steel blue fast: 0x41A0A0, // Teal/Cyan immune: 0xA8BCA1, // Desaturated sage green flying: 0xFFE6A7, // Pale buttery yellow swarm: 0xF2B589, // Soft peach boss: 0x7B2E17, // Deep burnt red (for boss waves) ceo: 0x4B0082 // Indigo for CEO }; // Ensure all types appear in early waves if (i === 0) { block.tint = colorScheme.normal; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = colorScheme.floor_manager; waveType = "Floor Manager"; enemyType = "floor_manager"; enemyCount = 10; } else if (i === 2) { block.tint = colorScheme.fast; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 3) { block.tint = colorScheme.immune; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = colorScheme.flying; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = colorScheme.swarm; enemyType = "swarm"; enemyCount = 30; } else if (i === 7) { block.tint = colorScheme.floor_manager; waveType = "Floor Manager"; enemyType = "floor_manager"; enemyCount = 10; } else if (i === 50) { // Wave 51 is CEO block.tint = colorScheme.ceo; block.visible = false; waveType = "CEO"; enemyType = "ceo"; enemyCount = 10; // Add FINAL_BOSS_WAVE asset behind wave 51 var finalBossAsset = marker.attachAsset('FINAL_BOSS_WAVE', { anchorX: 0.5, anchorY: 0.5 }); // Position it behind the block marker.addChildAt(finalBossAsset, 0); // Scale to fit behind the wave block finalBossAsset.width = block.width * 1.2; finalBossAsset.height = block.height * 1.5; } else if (isBossWave) { // Boss waves: cycle through all boss types including floor_manager, last boss is always flying var bossTypes = ['floor_manager', 'normal', 'fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 2) { // Second to last boss is flying (since CEO is now the last) enemyType = 'flying'; waveType = "Boss Flying"; block.tint = colorScheme.boss; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'floor_manager': block.tint = colorScheme.boss; waveType = "Boss Manager"; break; case 'normal': block.tint = colorScheme.boss; waveType = "Boss Normal"; break; case 'fast': block.tint = colorScheme.boss; waveType = "Boss Fast"; break; case 'immune': block.tint = colorScheme.boss; waveType = "Boss Immune"; break; case 'flying': block.tint = colorScheme.boss; waveType = "Boss Flying"; break; } } enemyCount = 1; // Make the wave indicator for boss waves stand out // Set boss wave color to the boss color block.tint = colorScheme.boss; } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = colorScheme.fast; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = colorScheme.immune; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = colorScheme.flying; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = colorScheme.swarm; waveType = "Clones"; enemyType = "swarm"; enemyCount = 30; } else { block.tint = colorScheme.normal; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } // --- End new unified wave logic --- // Mark boss waves with a special visual indicator if (isBossWave && enemyType !== 'swarm') { // Add a crown or some indicator to the wave marker for boss waves var bossIndicator = marker.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); bossIndicator.width = 30; bossIndicator.height = 30; bossIndicator.tint = 0xFFD700; // Gold color bossIndicator.y = -block.height / 2 - 15; // Change the wave type text to indicate boss waveType = "BOSS"; } else { // Use custom display names for wave types in the indicator switch (enemyType) { case 'floor_manager': waveType = "Floor manager"; break; case 'normal': waveType = "Vac Attack"; break; case 'flying': waveType = "Office Eye"; break; case 'ceo': waveType = "CEO"; break; case 'fast': //{zl_custom1} waveType = "Mailbots"; break; //{zl_custom2} case 'immune': //{zl_custom3} waveType = "Shredders"; break; //{zl_custom4} case 'swarm': //{zl_custom5} waveType = "Clones"; break; //{zl_custom6} default: waveType = enemyType.charAt(0).toUpperCase() + enemyType.slice(1); break; } } // Store the wave type and enemy count self.waveTypes[i] = enemyType; self.enemyCounts[i] = enemyCount; // Add shadow for wave type - 30% smaller than before, with special handling for Floor manager var shadowTextSize = 56; var shadowUseWordWrap = false; var shadowWordWrapWidth = blockWidth - 20; if (waveType === "Floor manager") { shadowTextSize = 40; // Smaller text for Floor manager shadowUseWordWrap = true; } var waveTypeShadow = new Text2(waveType, { size: shadowTextSize, fill: 0x000000, weight: 800, wordWrap: shadowUseWordWrap, wordWrapWidth: shadowUseWordWrap ? shadowWordWrapWidth : undefined }); waveTypeShadow.anchor.set(0.5, 0.5); waveTypeShadow.x = 4; waveTypeShadow.y = 4; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before, with special handling for Floor manager var textSize = 56; var useWordWrap = false; var wordWrapWidth = blockWidth - 20; if (waveType === "Floor manager") { textSize = 40; // Smaller text for Floor manager useWordWrap = true; } var waveTypeText = new Text2(waveType, { size: textSize, fill: 0xFFFFFF, weight: 800, wordWrap: useWordWrap, wordWrapWidth: useWordWrap ? wordWrapWidth : undefined }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.y = 0; marker.addChild(waveTypeText); // Add shadow for wave number - slightly smaller and centered var waveNumShadow = new Text2('Wave ' + (i + 1).toString(), { size: 32, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(0.5, 0.5); waveNumShadow.x = 4; waveNumShadow.y = 54; marker.addChild(waveNumShadow); // Main wave number text - slightly smaller and centered var waveNum = new Text2('Wave ' + (i + 1).toString(), { size: 32, fill: 0xFFFFFF, weight: 800 }); waveNum.anchor.set(0.5, 0.5); waveNum.x = 0; waveNum.alpha = 0.75; waveNum.y = 50; marker.addChild(waveNum); marker.x = -self.indicatorWidth + (i + 2) * blockWidth; self.addChild(marker); self.waveMarkers.push(marker); } // Get wave type for a specific wave number self.getWaveType = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return "normal"; } // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType // then we should return a different boss type var waveType = self.waveTypes[waveNumber - 1]; return waveType; }; // Get enemy count for a specific wave number self.getEnemyCount = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return 10; } return self.enemyCounts[waveNumber - 1]; }; // Get display name for a wave type self.getWaveTypeName = function (waveNumber) { var type = self.getWaveType(waveNumber); var typeName; // Special case for CEO wave (wave 51) - keep it as "CEO" if (type === 'ceo') { typeName = "CEO"; } else if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') { // Add boss prefix for boss waves (every 10th wave) except CEO typeName = "BOSS"; } else { // Set custom names for specific wave types switch (type) { case 'floor_manager': typeName = "Manager"; break; case 'ceo': typeName = "CEO"; break; case 'normal': typeName = "Vac Attack"; break; case 'flying': typeName = "Office Eye"; break; case 'fast': //{A0_custom1} typeName = "Mailbots"; break; //{A0_custom2} case 'immune': //{A0_custom3} typeName = "Shredders"; break; //{A0_custom4} case 'swarm': //{A0_custom5} typeName = "Clones"; break; //{A0_custom6} default: typeName = type.charAt(0).toUpperCase() + type.slice(1); break; } } return typeName; }; self.positionIndicator = new Container(); var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146 + 10; // Match increased box height leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146 + 10; // Match increased box height rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { // Calculate positioning that accounts for currentWave offset var progress = waveTimer / nextWaveTime; var moveAmount = (progress + currentWave) * blockWidth; // Subtract one block width after game starts to prevent jump if (self.gameStarted) { moveAmount -= blockWidth; } // Use consistent positioning logic regardless of game state for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; if (i === 0) { // Start marker moves with the indicator marker.x = -moveAmount; } else { // Wave markers are positioned relative to start marker marker.x = -moveAmount + i * blockWidth; } } self.positionIndicator.x = 0; for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; var block = marker.children[0]; if (i === 0) { // Start marker fades when game starts if (self.gameStarted) { block.alpha = .5; } } else { // Wave markers fade when completed (adjusted for start marker offset) if (i - 1 < currentWave) { block.alpha = .5; } } } self.handleWaveProgression = function () { if (!self.gameStarted) { return; } if (currentWave < totalWaves) { waveTimer++; if (waveTimer >= nextWaveTime) { waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; // Update global game started flag gameStarted = true; // Always show notification for incoming waves var waveType = self.getWaveTypeName(currentWave); var enemyCount = self.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } } }; self.handleWaveProgression(); }; return self; }); var WordProjectile = Container.expand(function (startX, startY, targetEnemy, damage, speed, towerType, wordText, isFirstWord) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; self.type = towerType || 'slow'; self.isFirstWord = isFirstWord || false; // Track if this is the first word in the sentence // Use provided word text or pick a random sentence and split it var displayWord; if (wordText) { displayWord = wordText; } else { // Array of random boring life sentences var boringLifeSentences = ['My cat got a new friend!', 'I heard Otto fart', 'Hildas dress does NOT match her hair color!', 'I reorganized my sock drawer today', 'The grocery store was out of my usual bread', 'I watched paint dry for 3 hours', 'My neighbor trimmed their hedge again', 'I found a penny on the sidewalk', 'The mailman was 5 minutes late today', 'I alphabetized my spice rack', 'My phone battery died at 47%', 'I counted 23 birds outside my window', 'The elevator music was slightly off-key', 'I folded laundry for the third time this week', 'My coffee was lukewarm this morning', 'I noticed a new crack in the ceiling', 'The bus driver wore different shoes today', 'I defrosted my freezer on Sunday', 'My pen ran out of ink mid-sentence', 'I watered the same plant twice by mistake']; // Pick a random sentence displayWord = boringLifeSentences[Math.floor(Math.random() * boringLifeSentences.length)]; } // Create text instead of bullet graphics var wordTextGraphics = new Text2(displayWord, { size: 28, fill: 0x9900FF, weight: 800 }); wordTextGraphics.anchor.set(0.5, 0.5); self.addChild(wordTextGraphics); // Add a subtle background for better visibility var wordBG = self.attachAsset('bullet_slow', { anchorX: 0.5, anchorY: 0.5 }); wordBG.alpha = 0.3; wordBG.tint = 0x9900FF; // Move text to front self.addChildAt(wordTextGraphics, self.children.length); self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Only apply damage and effects if this is the first word if (self.isFirstWord && !self.targetEnemy.hitBySlowWord) { // Apply damage to target enemy self.targetEnemy.health -= self.damage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply slow effect (same as original bullet logic) if (!self.targetEnemy.isImmune) { // Create visual slow effect var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow'); game.addChild(slowEffect); var slowPct = 0.5; if (self.sourceTowerLevel !== undefined) { var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8]; var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1)); slowPct = slowLevels[idx]; } if (!self.targetEnemy.slowed) { self.targetEnemy.originalSpeed = self.targetEnemy.speed; self.targetEnemy.speed *= 1 - slowPct; self.targetEnemy.slowed = true; self.targetEnemy.slowDuration = 180; } else { self.targetEnemy.slowDuration = 180; } } // Mark this enemy as hit by a slow word self.targetEnemy.hitBySlowWord = true; } self.destroy(); } else { var angle = Math.atan2(dy, dx); // Rotate the word to face movement direction wordTextGraphics.rotation = angle; self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x444444 }); /**** * Game Code ****/ // Use black background color instead of background asset game.setBackgroundColor(0x000000); var DEBUG = false; // Create debug button if DEBUG is enabled var debugButton = null; if (DEBUG) { debugButton = new Container(); var debugBg = debugButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); debugBg.width = 150; debugBg.height = 80; debugBg.tint = 0xFF0000; var debugText = new Text2("DEBUG", { size: 40, fill: 0xFFFFFF, weight: 800 }); debugText.anchor.set(0.5, 0.5); debugButton.addChild(debugText); // Position in lower right corner debugButton.x = 2048 - 100; debugButton.y = 2732 - 100; debugButton.down = function () { // Show debug modal var debugModal = new DebugModal(); debugModal.x = 2048 / 2; debugModal.y = 2732 / 2; game.addChild(debugModal); }; game.addChild(debugButton); } // Grid dimensions constants var GRID_WIDTH = 18; var GRID_HEIGHT = 22; // Reset unlocks on game start and auto-unlock office_prop storage.unlockedTowers = { office_prop: true }; // Track if we've shown the first unlock instruction var hasShownFirstUnlockInstruction = false; // Tower upgrade stats configuration var TOWER_STATS = { "default": { baseCost: 10, baseRange: 3, baseFireRate: 60, baseDamage: 10, baseSpeed: 5, rangePerLevel: 0.5, fireRatePerLevel: 8, damagePerLevel: 5, speedPerLevel: 0.5, maxLevelMultiplier: 2 // For final upgrade boost }, sniper: { baseCost: 30, baseRange: 5, baseFireRate: 90, baseDamage: 25, baseSpeed: 3.375, rangePerLevel: 0.8, fireRatePerLevel: 8, damagePerLevel: 5, speedPerLevel: 0.5, maxLevelMultiplier: 2, maxLevelRangeBonus: 7 // Special range boost for max level sniper }, splash: { baseCost: 40, baseRange: 2, baseFireRate: 75, baseDamage: 15, baseSpeed: 4, rangePerLevel: 0.2, fireRatePerLevel: 8, damagePerLevel: 5, speedPerLevel: 0.5, maxLevelMultiplier: 2 }, slow: { baseCost: 50, baseRange: 3.5, baseFireRate: 50, baseDamage: 8, baseSpeed: 5, rangePerLevel: 0.5, fireRatePerLevel: 8, damagePerLevel: 5, speedPerLevel: 0.5, maxLevelMultiplier: 2 }, poison: { baseCost: 60, baseRange: 3.2, baseFireRate: 70, baseDamage: 12, baseSpeed: 5, rangePerLevel: 0.5, fireRatePerLevel: 8, damagePerLevel: 5, speedPerLevel: 0.5, maxLevelMultiplier: 2 }, office_prop: { baseCost: 5, baseRange: 2, baseFireRate: 65, baseDamage: 7, baseSpeed: 4.5, rangePerLevel: 0.3, fireRatePerLevel: 8, damagePerLevel: 5, speedPerLevel: 0.5, maxLevelMultiplier: 2 } }; var isHidingUpgradeMenu = false; var gameStarted = false; // Track whether the game has started function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } var CELL_SIZE = 2048 / GRID_WIDTH; // Use full width divided by grid width var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var selectedTower = null; var gold = 800; var salary = 3; var score = 0; var currentWave = 0; var totalWaves = 51; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave // Create gold coin image // Create container for UI elements in game layer var gameUIContainer = new Container(); var goldCoin = LK.getAsset('gold_coin', { anchorX: 0.5, anchorY: 0.5 }); // Create salary image var salaryImage = LK.getAsset('salary', { anchorX: 0.5, anchorY: 0.5 }); var goldText = new Text2(gold, { size: 100, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0, 0.5); var livesText = new Text2(salary + " lives left", { size: 50, fill: 0xffffff, weight: 800 }); livesText.anchor.set(0, 0.5); var scoreText = new Text2('Score: ' + score, { size: 30, fill: 0x948a17, weight: 800 }); scoreText.anchor.set(1, 0.5); var topMargin = 30; var centerX = 2048 / 2; var spacing = 400; // Add UI elements to game container instead of GUI gameUIContainer.addChild(salaryImage); gameUIContainer.addChild(livesText); gameUIContainer.addChild(goldCoin); gameUIContainer.addChild(goldText); LK.gui.top.addChild(scoreText); // Position elements with new coordinates for game layer salaryImage.x = centerX - livesText.width / 2; salaryImage.y = 2030; livesText.x = salaryImage.x + salaryImage.width / 2 + 15; livesText.y = salaryImage.y; goldCoin.x = 100; goldCoin.y = 220; goldText.x = goldCoin.x + goldCoin.width / 2 + 20; goldText.y = goldCoin.y; scoreText.x = 700; scoreText.y = 150; function updateUI() { goldText.setText(gold); livesText.setText(salary + " lives left"); scoreText.setText('Score: ' + score); } function setGold(value) { gold = value; updateUI(); } var showDebugCells = true; // Boolean to control adding debug cells var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); var grid = new Grid(GRID_WIDTH, GRID_HEIGHT); grid.x = CELL_SIZE / 2; // Move grid half a cell size to the right grid.y = 290 - CELL_SIZE * 4 + 100 - 50; // Move grid up 150px total grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); game.addChild(gameUIContainer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var isDragging = false; function wouldBlockPath(gridX, gridY, towerType) { var cells = []; var cellsToBlock = towerType === 'office_prop' ? 1 : 2; for (var i = 0; i < cellsToBlock; i++) { for (var j = 0; j < cellsToBlock; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cells.push({ cell: cell, originalType: cell.type }); cell.type = 1; } } } var blocked = grid.pathFind(); for (var i = 0; i < cells.length; i++) { cells[i].cell.type = cells[i].originalType; } grid.pathFind(); grid.renderDebug(); return blocked; } function getTowerCost(towerType) { var cost = 10; switch (towerType) { case 'sniper': cost = 30; break; case 'splash': cost = 40; break; case 'slow': cost = 50; break; case 'poison': cost = 60; break; case 'office_prop': cost = 5; break; } return cost; } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); grid.pathFind(); grid.renderDebug(); // Show and start pulse animation on start game button after first tower is placed if (towers.length === 1 && startGameButton && startGameButton.parent) { startGameButton.visible = true; // Show the button if (!startGameButton.hasPulsed) { startGameButton.hasPulsed = true; startGameButton.startPulseAnimation(); } } return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } // Check for dragging existing towers first var draggedTower = null; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - tower.width / 2; var towerRight = tower.x + tower.width / 2; var towerTop = tower.y - tower.height / 2; var towerBottom = tower.y + tower.height / 2; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { draggedTower = tower; break; } } if (draggedTower) { // Prevent moving existing towers when game is started if (gameStarted) { return; } // Remove tower instruction when starting to drag for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i] instanceof TowerInstruction) { game.children[i].destroy(); } } // Start dragging existing tower towerPreview.visible = true; isDragging = true; towerPreview.towerType = draggedTower.id; towerPreview.draggedTower = draggedTower; // Store reference to tower being moved towerPreview.updateAppearance(); // Apply the same offset as in move handler to ensure consistency when starting drag //towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); return; } // Check for dragging from source towers for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) { // Check if tower is unlocked before allowing drag if (tower.isUnlocked()) { // Remove tower instruction when starting to drag from source towers for (var j = game.children.length - 1; j >= 0; j--) { if (game.children[j] instanceof TowerInstruction) { game.children[j].destroy(); } } towerPreview.visible = true; isDragging = true; towerPreview.towerType = tower.towerType; towerPreview.draggedTower = null; // No existing tower being moved towerPreview.updateAppearance(); // Apply the same offset as in move handler to ensure consistency when starting drag //towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } break; } } }; game.move = function (x, y, obj) { if (isDragging) { towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } }; game.up = function (x, y, obj) { var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - tower.width / 2; var towerRight = tower.x + tower.width / 2; var towerTop = tower.y - tower.height / 2; var towerBottom = tower.y + tower.height / 2; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) { var clickedOnMenu = false; for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; var menuWidth = 2048; var menuHeight = 450; var menuLeft = menu.x - menuWidth / 2; var menuRight = menu.x + menuWidth / 2; var menuTop = menu.y - menuHeight / 2; var menuBottom = menu.y + menuHeight / 2; if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) { clickedOnMenu = true; break; } } if (!clickedOnMenu) { for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; hideUpgradeMenu(menu); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = null; grid.renderDebug(); } } if (isDragging) { isDragging = false; if (towerPreview.draggedTower) { // Moving an existing tower var draggedTower = towerPreview.draggedTower; if (towerPreview.canPlace) { // Clear old position first var oldGridX = draggedTower.gridX; var oldGridY = draggedTower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(oldGridX + i, oldGridY + j); if (cell) { cell.type = 0; var towerIndex = cell.towersInRange.indexOf(draggedTower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY, draggedTower.id)) { // Move tower to new position draggedTower.placeOnGrid(towerPreview.gridX, towerPreview.gridY); // Update range circle for the moved tower var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === draggedTower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { // Update range circle position to match new tower position rangeCircle.x = draggedTower.x; rangeCircle.y = draggedTower.y; // Update range circle size in case tower range changed var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = draggedTower.getRange() * 2; } grid.pathFind(); grid.renderDebug(); } else { // Restore old position if path would be blocked draggedTower.placeOnGrid(oldGridX, oldGridY); var notification = game.addChild(new Notification("Tower would block the path!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else if (towerPreview.blockedByEnemy) { var notification = game.addChild(new Notification("Cannot place: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("Cannot place tower here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else { // Placing a new tower if (towerPreview.canPlace) { if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType)) { placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else { var notification = game.addChild(new Notification("Tower would block the path!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else if (towerPreview.blockedByEnemy) { var notification = game.addChild(new Notification("Cannot build: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (towerPreview.visible) { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } towerPreview.visible = false; towerPreview.draggedTower = null; // Clear reference if (isDragging) { var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } } } }; var waveIndicator = new WaveIndicator(); waveIndicator.x = 2048 / 2; waveIndicator.y = grid.y + CELL_SIZE * 2; // Position wave indicator over the grid, moved up 100px game.addChild(waveIndicator); var startGameButton = new StartGameButton(); startGameButton.x = 2048 / 2; // Position to the left of wave indicator startGameButton.y = 2732 - 200 + 150 - 40; // Same vertical position as wave indicator, moved up 10px startGameButton.visible = false; // Hide initially until first tower is placed game.addChild(startGameButton); var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 / 2; nextWaveButton.y = startGameButton.y; nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); var towerTypes = ['office_prop', 'default', 'sniper', 'splash', 'slow', 'poison']; var sourceTowers = []; var towerSpacing = 308; // Increase space between towers in tower shop by 10px var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 120; // Move tower shop up by 10px for (var i = 0; i < towerTypes.length; i++) { var tower = new TowerShop(towerTypes[i]); tower.x = startX + i * towerSpacing; tower.y = towerY; game.addChild(tower); // Add to game layer instead of towerLayer to render above enemies sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; game.update = function () { if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; // Get wave type and enemy count from the wave indicator var waveType = waveIndicator.getWaveType(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); // Check if this is a boss wave var isBossWave = currentWave % 10 === 0 && currentWave > 0; if (currentWave === 51) { isBossWave = true; } if (isBossWave && waveType !== 'swarm') { // Boss waves have just 1 enemy regardless of what the wave indicator says enemyCount = 1; // Show boss announcement var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy = new Enemy(waveType); // Add enemy to the appropriate layer based on type if (enemy.isFlying) { // Add flying enemy to the top layer enemyLayerTop.addChild(enemy); // If it's a flying enemy, add its shadow to the middle layer if (enemy.shadow) { enemyLayerMiddle.addChild(enemy.shadow); } } else { // Add normal/ground enemies to the bottom layer enemyLayerBottom.addChild(enemy); } // Scale difficulty with wave number but don't apply to boss // as bosses already have their health multiplier // Use exponential scaling for health var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // Increment speed slightly with wave number //enemy.speed = enemy.speed + currentWave * 0.002; // All enemy types now spawn in the middle 6 tiles at the top spacing var gridWidth = GRID_WIDTH; var midPoint = Math.floor(gridWidth / 2); // 12 // Find a column that isn't occupied by another enemy that's not yet in view var availableColumns = []; for (var col = midPoint - 3; col < midPoint + 3; col++) { var columnOccupied = false; // Check if any enemy is already in this column but not yet in view for (var e = 0; e < enemies.length; e++) { if (enemies[e].cellX === col && enemies[e].currentCellY < 4) { columnOccupied = true; break; } } if (!columnOccupied) { availableColumns.push(col); } } // If all columns are occupied, use original random method var spawnX; if (availableColumns.length > 0) { // Choose a random unoccupied column spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)]; } else { // Fallback to random if all columns are occupied spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14 } var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading enemy.cellX = spawnX; enemy.cellY = 5; // Position after entry enemy.currentCellX = spawnX; enemy.currentCellY = spawnY; enemy.waveNumber = currentWave; enemies.push(enemy); } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; // Increase salary when passing a level var isBossLevel = currentWave % 10 === 0 && currentWave > 0; if (currentWave === 51) { isBossLevel = true; } if (isBossLevel) { // Boss level: increase salary by 5 salary += 5; var notification = game.addChild(new Notification("Boss level complete! Salary +$5k")); notification.x = 2048 / 2; notification.y = grid.height - 100; } else { // Regular level: increase salary by 1 salary += 1; } updateUI(); } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Create explosive particle effect for flying enemies if (enemy.isFlying) { var particleCount = enemy.isBoss ? 45 : 30; // Reduced by half from previous 200% increase for (var p = 0; p < particleCount; p++) { var particle = new Container(); var particleGraphics = particle.attachAsset('enemy_flying', { anchorX: 0.5, anchorY: 0.5 }); // Make particles larger and more visible (200% increase) particleGraphics.scaleX = enemy.isBoss ? 1.8 : 1.2; // 200% increase: 0.6->1.8, 0.4->1.2 particleGraphics.scaleY = enemy.isBoss ? 1.8 : 1.2; // 200% increase: 0.6->1.8, 0.4->1.2 particleGraphics.tint = 0xFF4500; // Orange explosion color particle.x = enemy.x; particle.y = enemy.y; particle.alpha = 1; particle.scaleX = 1; particle.scaleY = 1; // Add particle to game instead of enemy layer for better visibility game.addChild(particle); // Random explosion direction and speed (200% increase) var angle = Math.random() * Math.PI * 2; var speed = enemy.isBoss ? 240 + Math.random() * 360 : 150 + Math.random() * 240; // 200% increase: 80+120->240+360, 50+80->150+240 var endX = particle.x + Math.cos(angle) * speed; var endY = particle.y + Math.sin(angle) * speed; // Create closure to capture particle reference (function (currentParticle) { // Animate particle explosion with improved timing (200% increase duration) tween(currentParticle, { x: endX, y: endY, alpha: 0, scaleX: 0.1, scaleY: 0.1, rotation: Math.random() * Math.PI * 18 // 200% increase: 6->18 }, { duration: enemy.isBoss ? 3600 : 3000, // 200% increase: 1200->3600, 1000->3000 easing: tween.easeOut, onFinish: function onFinish() { if (currentParticle.parent) { game.removeChild(currentParticle); } } }); })(particle); } } // Boss enemies give more gold and score var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5); // Create animated coins that fly to the coin counter for (var coinIndex = 0; coinIndex < goldEarned; coinIndex++) { // Create coin sprite var animatedCoin = new Container(); var coinGraphics = animatedCoin.attachAsset('gold_coin', { anchorX: 0.5, anchorY: 0.5 }); // Start position at enemy location with slight random spread animatedCoin.x = enemy.x + (Math.random() - 0.5) * 40; animatedCoin.y = enemy.y + (Math.random() - 0.5) * 40; // Scale coins smaller initially coinGraphics.scaleX = 0.8; coinGraphics.scaleY = 0.8; game.addChild(animatedCoin); // Calculate target position (coin counter position) var targetX = goldCoin.x + goldCoin.width / 2 + 10; // Position near gold text var targetY = goldCoin.y; // No conversion needed since goldCoin is now in game layer var gameTargetX = targetX; var gameTargetY = targetY; // Create bezier curve path using intermediate control points // Calculate direction vector from start to target var dirX = gameTargetX - animatedCoin.x; var dirY = gameTargetY - animatedCoin.y; var distance = Math.sqrt(dirX * dirX + dirY * dirY); // Normalize direction dirX = dirX / distance; dirY = dirY / distance; // Place midpoint at 60% of the way to target, with slight random offset perpendicular to direction var midProgress = 0.6; var perpOffset = (Math.random() - 0.5) * 60; // Random offset of -30 to +30 pixels // Calculate perpendicular vector (rotate direction by 90 degrees) var perpX = -dirY; var perpY = dirX; // Calculate midpoint var midX = animatedCoin.x + dirX * distance * midProgress + perpX * perpOffset; var midY = animatedCoin.y + dirY * distance * midProgress + perpY * perpOffset; // Add slight delay between coins var coinDelay = coinIndex * 80; // Create closure to capture coin reference (function (currentCoin, currentDelay, currentMidX, currentMidY, currentTargetX, currentTargetY) { LK.setTimeout(function () { // First part of bezier curve - to midpoint tween(currentCoin, { x: currentMidX, y: currentMidY, rotation: Math.PI * 2 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { // Second part of bezier curve - to target tween(currentCoin, { x: currentTargetX, y: currentTargetY, scaleX: 1.2, scaleY: 1.2, rotation: Math.PI * 4 }, { duration: 400, easing: tween.easeIn, onFinish: function onFinish() { // Flash and disappear effect tween(currentCoin, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { if (currentCoin.parent) { game.removeChild(currentCoin); } } }); } }); } }); }, currentDelay); })(animatedCoin, coinDelay, midX, midY, gameTargetX, gameTargetY); } var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); continue; } if (grid.updateEnemy(enemy)) { // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); salary = Math.max(0, salary - 1); updateUI(); if (salary <= 0 && !DEBUG) { // Check if user didn't complete the last level if (currentWave < totalWaves) { // Show custom message if not on last level var notification = game.addChild(new Notification("Every is FIRED!")); notification.x = 2048 / 2; notification.y = 2732 / 2; // Wait a moment then show game over LK.setTimeout(function () { LK.showGameOver(); }, 2000); } else { LK.showGameOver(); } } } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } // Clean up bullet shadow if it exists (for sniper and default bullets) if ((bullets[i].type === 'sniper' || bullets[i].type === 'default') && bullets[i].shadow) { enemyLayerMiddle.removeChild(bullets[i].shadow); bullets[i].shadow = null; } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } }; // Hide GUI layer initially LK.gui.visible = false; // Play idle music before game starts LK.playMusic('idle_music'); // Show intro modal if DEBUG is false - placed at the very end to ensure it appears on top if (!DEBUG) { var introModal = new IntroModal(); introModal.x = 2048 / 2; introModal.y = 2732 / 2; game.addChild(introModal); // Show GUI layer when introModal closes introModal.onDestroy = function () { LK.gui.visible = true; }; }
===================================================================
--- original.js
+++ change.js
@@ -1536,18 +1536,18 @@
easing: tween.easeOut
});
// Title text
// Instructions text
- var instructionText = new Text2("Deploy your loyal coworkers against the march of efficiency.\n\nIn this human-vs-robot war, coffee stains and office gossip are your deadliest tools. Keep paychecks flowing—or it’s back to your parents’ Wi-Fi.", {
+ var instructionText = new Text2("Fortify the cubicles with actual people power—management just ordered your replacements.\n\nFrom stapler snipes to cappuccino splash, weaponize office nonsense into necessary defense.\n\nDefend the budget—or get archived under “Former Staff”.", {
size: 80,
- fill: 0xCCCCCC,
+ fill: 0xeeeeee,
weight: 350,
wordWrap: true,
wordWrapWidth: 1400,
align: 'left'
});
instructionText.anchor.set(0, 0.5);
- instructionText.y = -100;
+ instructionText.y = -150;
instructionText.x = -instructionText.width / 2;
introBgContainer.addChild(instructionText);
// Let's go button
var button = new Container();
@@ -3177,13 +3177,13 @@
var description = '';
switch (towerType) {
case 'default':
towerTypeLabel = 'Damage type: Basic';
- description = 'Linus quietly fidgets with his rubber band collection, too shy to make eye contact. Despite his timid nature, his coding precision translates to surprisingly accurate defensive capabilities.';
+ description = 'Linus from IT treats his staple gun like a precision instrument with patch notes. With clinical aim—He fires ISO-compliant, armor-piercing office metal.\n\nPlease submit a ticket for removal; response time 3–5 business minutes.';
break;
case 'sniper':
towerTypeLabel = 'Damage type: Long Range';
- description = 'Heidi from accounting has mastered the ancient art of paper airplane warfare. With surgical precision, she can hit you from across three cubicles. Her range is legendary, her patience infinite, and her paper cuts devastating. Please allow 3-5 business days for elimination.';
+ description = 'Hilda from accounting has mastered the ancient art of paper airplane warfare. With surgical precision, she can hit you from across three cubicles. Her range is legendary, her patience infinite, and her paper cuts devastating. Please allow 3-5 business days for elimination.';
break;
case 'splash':
towerTypeLabel = 'Damage type: Hot Splash';
description = 'Otto from the break room takes his coffee very seriously - and very hot. One splash of his premium blend can melt through enemy armor and several layers of office carpet. Side effects include: area damage, caffeine addiction, and an inexplicable urge to discuss quarterly reports.';
@@ -4436,13 +4436,13 @@
salaryImage.y = 2030;
livesText.x = salaryImage.x + salaryImage.width / 2 + 15;
livesText.y = salaryImage.y;
goldCoin.x = 100;
-goldCoin.y = 200;
+goldCoin.y = 220;
goldText.x = goldCoin.x + goldCoin.width / 2 + 20;
goldText.y = goldCoin.y;
scoreText.x = 700;
-scoreText.y = 200;
+scoreText.y = 150;
function updateUI() {
goldText.setText(gold);
livesText.setText(salary + " lives left");
scoreText.setText('Score: ' + score);
White circle with two eyes, seen from above.. In-Game asset. 2d. High contrast. No shadows
A guy in a leisure suit is 50s holding a large empty coffee cup in his arm facing down, he just threw the coffee at someone. Looks angry as fuck. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. No shadows. top down. View from above.. In-Game asset. 2d. High contrast. No shadows
A padlock. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. No shadows
An office whiteboard aspect ratio 3/4, nothing on it but with it has some marks from previous drawings. Add some marks from previous usage. Should work as a background for a modal dialog. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. No shadows
A lady that just threw a paper plane and is leaned forward due to that. She has a pink dress and orange hair. She looks like she has been working in an office for 40 years. Make her look angry as fuck. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. In-Game asset. 2d. High contrast. No shadows. top down. View from above.
An black office whiteboard, square, nothing on it but with it has some marks from previous drawings. Should work as a background for a modal dialog. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. High resolution. No shadows
Propeller for a drone, viewed straight from above. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. In-Game asset. 2d. High contrast. No shadows. top down.. In-Game asset. 2d. High contrast. No shadows
A paper plane. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. In-Game asset. 2d. High contrast. No shadows. top down.. In-Game asset. 2d. High contrast. No shadows
A robot vacuum brush straight above. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. In-Game asset. 2d. High contrast. No shadows. top down. In-Game asset. 2d. High contrast. No shadows. In-Game asset. 2d. High contrast. No shadows
a white sign, flat design. In-Game asset. 2d. High contrast. No shadows
A women working in an office who loves to talk an talks too much. Dark skin color. Standing up and ready spew the latest gossip to whoever comes near. Full body viewed from the above. Everybody who comes near hear gets bombarded with her talking about what happened the past weekend. Friendly looking. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. No shadows. top down. View from above.
A blurred background in aspect ratio 9/16 showing an office from above. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. No shadows. top down. View from above.. In-Game asset. 2d. High contrast. No shadows
an office building, dark. In-Game asset. 2d. High contrast. No shadows
make it aspect ratio 9/16 and add a bright drop shadow
make it slightly lighter
Make her look 40% more angry and give her more attitude
make the full body show
remove the bad breath cloud
A typical office plant. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. In-Game asset. 2d. High contrast. No shadows. top down.. In-Game asset. 2d. High contrast. No shadows
A rating star. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. In-Game asset. 2d. High contrast. No shadows. top down.. In-Game asset. 2d. High contrast. No shadows
A copy printer. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. In-Game asset. 2d. High contrast. No shadows. top down.. In-Game asset. 2d. High contrast. No shadows
A water cooler. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. In-Game asset. 2d. High contrast. No shadows. top down.. In-Game asset. 2d. High contrast. No shadows
A coffee machine. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. In-Game asset. 2d. High contrast. No shadows. top down.. In-Game asset. 2d. High contrast. No shadows
A green button with text "Let's go!". Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. No shadows. top down. View from above.
A gold coin. Used as an icon for in-game currency. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. No shadows. top down. View from above.
A heart. Used as an icon for in-game lives. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. No shadows. top down. View from above.
make it point straight down, no rotation
A green button with text "I'm ready!". Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. No shadows. top down. View from above.
A purple button with text "Send next wave!". No word wrap. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. No shadows. top down. View from above.
Change text to "Tap to unlock"
An speech bubble with the text "Tap to unlock". Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. No shadows. top down. View from above.
An speech bubble with the text "Drag to place". Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. Tone is premium, friendly, clean with lifestyle or service-oriented appeal. In-Game asset. 2d. High contrast. No shadows. top down. View from above.
A staple projectile from a staple gun. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. In-Game asset. 2d. High contrast. No shadows
An aggressive mobile paper shredder robot. Viewed from a straight angle right in front. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. In-Game asset. 2d. High contrast. No shadows. top down.
A button without text, background color #480070. A border of golden stars. This is a majestic button. 350 px wide, 100px high. Cute soft graphics. soft ambient light. semi-matte to satin surfaces. naturalistic with slight saturation boost colors. In-Game asset. 2d. High contrast. No shadows. top down.