User prompt
remove the old route when new created
User prompt
When the knife button is pressed, draw a route that shows the shortest distance between the enemy and the player, and display the effect where that route meets the player's asset. the knife meets the enemy in the middle of asset. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
add a knife throw button. enemies take damage when knife hits them. knives are limited to 5 for each level ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
When the knife button is pressed, draw a route that shows the shortest distance between the enemy and the player, and display the effect where that route meets the player's asset. remove the same thing with attack button attack button only creates punch effect not any route effect ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
add a knife throw button and do this when new button is pressed. also enemies take damage when knife hits them. knives are limited to 5 for each level ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
When the attack button is pressed, draw a route that shows the shortest distance between the enemy and the player, and display the effect where that route meets the player's asset. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
move the movement keys away from each other and prevent them from intersecting with each other
User prompt
Show a warning with an arrow on the side where the enemy is. As you move forward, the warning will change from transparent to visible and when the enemy becomes visible on the screen, the warning will disappear. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
add collision to player and mobs. they collide and They don't interfere with each other.
User prompt
make all buttons bigger and add an asset for each of them
User prompt
It should move when the movement keys are hold, not when clicked.
User prompt
add buttons for all directions to move the player, add a attack button, increase attacks range
User prompt
game is not wave based. game is level based with each level contains different type and number of enemies. you move the player and the playable map is not limited with the visble screen camera follows the char as the char moves. enemies roam randomly and starts to attack the pkayer once they sees it
Code edit (1 edits merged)
Please save this source code
User prompt
Pixel Hero Adventure
Initial prompt
a pixel game like dan the man
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var ArcherProjectile = Container.expand(function () { var self = Container.call(this); var projectileGraphics = self.attachAsset('bossProjectile', { anchorX: 0.5, anchorY: 0.5 }); projectileGraphics.tint = 0x8844ff; // Purple arrows projectileGraphics.scaleX = 0.8; projectileGraphics.scaleY = 0.8; self.velocityX = 0; self.velocityY = 0; self.lifetime = 240; // 4 seconds self.update = function () { self.x += self.velocityX; self.y += self.velocityY; self.lifetime--; // Set rotation to face movement direction if (self.velocityX !== 0 || self.velocityY !== 0) { var angle = Math.atan2(self.velocityY, self.velocityX); projectileGraphics.rotation = angle; } // Check collision with hero var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); if (distanceToHero < 60) { hero.takeDamage(); self.destroy(); // Remove from projectiles array for (var i = bossProjectiles.length - 1; i >= 0; i--) { if (bossProjectiles[i] === self) { bossProjectiles.splice(i, 1); break; } } return; } // Remove if lifetime expired or off screen if (self.lifetime <= 0 || self.x < -100 || self.x > currentLevelData.width + 100 || self.y < -100 || self.y > 3000) { self.destroy(); // Remove from projectiles array for (var i = bossProjectiles.length - 1; i >= 0; i--) { if (bossProjectiles[i] === self) { bossProjectiles.splice(i, 1); break; } } } }; return self; }); // Base Boss class with common functionality var BaseBoss = Container.expand(function () { var self = Container.call(this); self.health = 50; self.maxHealth = 50; self.speed = 3; self.lastX = 0; self.lastY = 0; self.state = 'IDLE'; // AI states: IDLE, PURSUING, ATTACKING, STUNNED, ENRAGED self.stateTimer = 0; self.attackCooldown = 0; self.isInvulnerable = false; self.bossType = 'base'; self.phase = 1; self.predictedHeroX = 0; self.predictedHeroY = 0; // Predictive targeting system self.updatePrediction = function () { var heroVelocityX = hero.x - hero.lastX; var heroVelocityY = hero.y - hero.lastY; var predictionFrames = 30; // Predict 0.5 seconds ahead self.predictedHeroX = hero.x + heroVelocityX * predictionFrames; self.predictedHeroY = hero.y + heroVelocityY * predictionFrames; }; // State machine update self.updateState = function () { var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); self.stateTimer++; switch (self.state) { case 'IDLE': if (distanceToHero < 600) { self.setState('PURSUING'); } break; case 'PURSUING': if (distanceToHero < 200) { self.setState('ATTACKING'); } else if (distanceToHero > 800) { self.setState('IDLE'); } break; case 'ATTACKING': if (distanceToHero > 300) { self.setState('PURSUING'); } break; case 'STUNNED': if (self.stateTimer > 120) { // 2 seconds self.setState('ENRAGED'); } break; case 'ENRAGED': if (self.health > self.maxHealth * 0.5) { self.setState('PURSUING'); } break; } }; self.setState = function (newState) { self.state = newState; self.stateTimer = 0; self.onStateChange(newState); }; // Override for BossType1 specific state changes self.onStateChange = function (newState) { var graphics = self.getChildAt(0); if (newState === 'ENRAGED') { graphics.tint = 0xff4444; self.speed *= 2.0; // More aggressive speed boost // Add particle effect for enrage for (var i = 0; i < 8; i++) { var particle = game.addChild(getFromPool('bloodParticle')); particle.reset(self.x + (Math.random() - 0.5) * 100, self.y - 150 + (Math.random() - 0.5) * 100); var particleGraphics = particle.getChildAt(0); particleGraphics.tint = 0xff4444; bloodParticles.push(particle); } } else if (newState === 'STUNNED') { graphics.tint = 0x888888; self.speed *= 0.3; // More pronounced slowdown // Vulnerability window - takes extra damage when stunned self.isInvulnerable = false; } else if (newState === 'ATTACKING') { graphics.tint = 0xffaa00; // Orange tint when attacking } else { graphics.tint = 0xffffff; } }; self.takeDamage = function (fromKnife) { if (self.isInvulnerable) return; var damage = 1; var isCritical = Math.random() < 0.2; // 20% critical chance on bosses var isVulnerable = self.state === 'STUNNED'; if (isVulnerable) { damage *= 2; // Double damage when stunned } if (isCritical) { damage += 1; // Extra damage for critical } self.health -= damage; // Create damage number var damageNumber = game.addChild(getFromPool('damageNumber')); damageNumber.reset(self.x + (Math.random() - 0.5) * 60, self.y - 150, damage, isCritical || isVulnerable); damageNumbers.push(damageNumber); // Screen flash for critical hits on bosses if (isCritical) { createScreenFlash(0xFFD700, 400, 0.2); // Gold flash for critical } LK.effects.flashObject(self, isCritical ? 0xFFD700 : 0xffffff, isCritical ? 600 : 200); // Create enhanced blood particles with variety for (var p = 0; p < 10; p++) { var bloodParticle = game.addChild(getFromPool('bloodParticle')); var particleType = Math.random() < 0.3 ? 'large' : Math.random() < 0.5 ? 'small' : 'normal'; bloodParticle.reset(self.x + (Math.random() - 0.5) * 60, self.y - 100 + (Math.random() - 0.5) * 60, particleType); bloodParticles.push(bloodParticle); } // Add spray particles for dramatic effect for (var p = 0; p < 8; p++) { var sprayParticle = game.addChild(getFromPool('bloodParticle')); sprayParticle.reset(self.x + (Math.random() - 0.5) * 40, self.y - 120 + (Math.random() - 0.5) * 40, 'spray'); bloodParticles.push(sprayParticle); } // State transitions based on damage if (self.health <= self.maxHealth * 0.25 && self.state !== 'ENRAGED') { self.setState('ENRAGED'); } else if (fromKnife && Math.random() < 0.3) { self.setState('STUNNED'); } if (self.health <= 0) { self.die(); } }; self.die = function () { LK.effects.flashScreen(0xffffff, 2000); // Determine coin reward based on boss type var coinReward = 50; // Default boss reward if (self.bossType === 'melee') { coinReward = 50; } else if (self.bossType === 'summoner') { coinReward = 75; } else if (self.bossType === 'environmental') { coinReward = 100; } // Automatically give coins to upgrade tree upgradeTree.addCoins(coinReward); for (var i = 0; i < 10; i++) { var coin = game.addChild(new Coin()); coin.x = self.x + (Math.random() - 0.5) * 200; coin.y = self.y + (Math.random() - 0.5) * 200; coins.push(coin); } LK.setScore(LK.getScore() + 1000); hero.addCombo(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } self.destroy(); updateScoreDisplay(); updateEnemiesLeftDisplay(); }; return self; }); // BossType3: Environmental Manipulator var BossType3 = BaseBoss.expand(function () { var self = BaseBoss.call(this); var bossGraphics = self.attachAsset('boss', { anchorX: 0.5, anchorY: 1.0 }); bossGraphics.tint = 0x00ff88; // Green for environmental self.bossType = 'environmental'; self.hazardCooldown = 0; self.wallCooldown = 0; self.earthquakeCooldown = 0; self.activeHazards = []; self.isChanneling = false; self.update = function () { self.updatePrediction(); self.updateState(); var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); // Phase transitions if (self.health > self.maxHealth * 0.66) { self.phase = 1; } else if (self.health > self.maxHealth * 0.33) { self.phase = 2; } else { self.phase = 3; } // Environmental boss positioning based on state if (self.state === 'PURSUING' || self.state === 'ATTACKING' || self.state === 'ENRAGED') { // Move to strategic positions based on predicted hero movement var strategicX = self.state === 'ENRAGED' ? currentLevelData.width / 2 + (self.predictedHeroX > currentLevelData.width / 2 ? -300 : 300) : currentLevelData.width / 2; var strategicY = 2000; var dx = strategicX - self.x; var dy = strategicY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 50) { self.lastX = self.x; self.lastY = self.y; var moveSpeed = self.state === 'ENRAGED' ? self.speed : self.speed * 0.5; self.x += dx / distance * moveSpeed; self.y += dy / distance * moveSpeed; } } // Enhanced environmental attacks with state-based timing if (self.state === 'ATTACKING' && self.hazardCooldown <= 0) { self.showHazardWarning(); } if (self.phase >= 2 && self.state === 'ATTACKING' && self.wallCooldown <= 0) { self.showWallWarning(); } if (self.phase === 3 && self.state === 'ENRAGED' && self.earthquakeCooldown <= 0) { self.showEarthquakeWarning(); } // Cooldown updates if (self.hazardCooldown > 0) self.hazardCooldown--; if (self.wallCooldown > 0) self.wallCooldown--; if (self.earthquakeCooldown > 0) self.earthquakeCooldown--; }; self.createHazard = function () { self.isChanneling = true; tween(bossGraphics, { tint: 0xff4400 }, { duration: 300 }); // Create fire hazard near predicted hero position var hazard = game.addChild(LK.getAsset('hitEffect', { anchorX: 0.5, anchorY: 0.5, alpha: 0 })); hazard.x = self.predictedHeroX + (Math.random() - 0.5) * 100; hazard.y = self.predictedHeroY + (Math.random() - 0.5) * 100; // Warning phase tween(hazard, { alpha: 0.5, scaleX: 2, scaleY: 2 }, { duration: 1000 }); // Damage phase LK.setTimeout(function () { tween(hazard, { alpha: 1, scaleX: 3, scaleY: 3, tint: 0xff0000 }, { duration: 500, onFinish: function onFinish() { // Check if hero is in hazard area var dx = hero.x - hazard.x; var dy = hero.y - hazard.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 100) { hero.takeDamage(); } // Remove hazard hazard.destroy(); for (var i = self.activeHazards.length - 1; i >= 0; i--) { if (self.activeHazards[i] === hazard) { self.activeHazards.splice(i, 1); break; } } } }); }, 1000); self.activeHazards.push(hazard); tween(bossGraphics, { tint: 0x00ff88 }, { duration: 300 }); self.isChanneling = false; self.hazardCooldown = self.phase === 3 ? 120 : 180; }; self.createWalls = function () { // Create temporary walls that block movement for (var i = 0; i < 3; i++) { var wall = game.addChild(LK.getAsset('backgroundTile', { anchorX: 0.5, anchorY: 0.5, alpha: 0, tint: 0x666666 })); wall.x = 300 + i * 400; wall.y = 1800 + Math.random() * 400; tween(wall, { alpha: 0.8, scaleY: 3 }, { duration: 500 }); // Remove walls after duration LK.setTimeout(function () { tween(wall, { alpha: 0, scaleY: 0.1 }, { duration: 500, onFinish: function onFinish() { wall.destroy(); } }); }, 5000); } self.wallCooldown = 420; // 7 seconds }; self.showHazardWarning = function () { // Create multiple warning areas using predictive targeting var warningAreas = [{ x: self.predictedHeroX, y: self.predictedHeroY }, { x: hero.x, y: hero.y }, // Current position as backup { x: self.predictedHeroX + (Math.random() - 0.5) * 200, y: self.predictedHeroY + (Math.random() - 0.5) * 200 }]; for (var i = 0; i < warningAreas.length; i++) { var warning = game.addChild(LK.getAsset('hitEffect', { anchorX: 0.5, anchorY: 0.5, alpha: 0, tint: 0xff4400 })); warning.x = warningAreas[i].x; warning.y = warningAreas[i].y; tween(warning, { alpha: 0.6, scaleX: 2.5, scaleY: 2.5 }, { duration: 1500, onFinish: function onFinish() { warning.destroy(); } }); } // Boss visual warning tween(bossGraphics, { tint: 0xff4400, scaleX: 1.2, scaleY: 1.2 }, { duration: 500, onFinish: function onFinish() { self.createHazard(); tween(bossGraphics, { tint: 0x00ff88, scaleX: 1, scaleY: 1 }, { duration: 300 }); } }); }; self.showWallWarning = function () { // Show where walls will appear var wallPositions = [{ x: 300, y: 1800 }, { x: 700, y: 2000 }, { x: 1100, y: 1900 }]; for (var i = 0; i < wallPositions.length; i++) { var warning = game.addChild(LK.getAsset('backgroundTile', { anchorX: 0.5, anchorY: 0.5, alpha: 0, tint: 0xffaa00, scaleY: 0.1 })); warning.x = wallPositions[i].x; warning.y = wallPositions[i].y; tween(warning, { alpha: 0.7, scaleY: 2 }, { duration: 1000, onFinish: function onFinish() { warning.destroy(); } }); } LK.setTimeout(function () { self.createWalls(); }, 1000); }; self.showEarthquakeWarning = function () { // Screen warning for earthquake LK.effects.flashScreen(0x8b4513, 1500); // Boss charges up tween(bossGraphics, { tint: 0x8b4513, scaleX: 1.5, scaleY: 1.5 }, { duration: 1500, onFinish: function onFinish() { self.earthquake(); } }); }; self.earthquake = function () { self.isChanneling = true; // Create vulnerability window - boss can't move during earthquake self.speed = 0; LK.effects.flashScreen(0x8b4513, 2000); // Intense screen shake for earthquake triggerScreenShake(25, 1500, tween.easeInOut); // Screen shake effect simulation through rapid position changes var originalX = bossGraphics.x; var originalY = bossGraphics.y; for (var i = 0; i < 20; i++) { LK.setTimeout(function () { bossGraphics.x = originalX + (Math.random() - 0.5) * 10; bossGraphics.y = originalY + (Math.random() - 0.5) * 10; }, i * 50); } LK.setTimeout(function () { bossGraphics.x = originalX; bossGraphics.y = originalY; self.isChanneling = false; // Restore speed and brief stun self.speed = 3; self.setState('STUNNED'); // Reset visual tween(bossGraphics, { tint: 0x00ff88, scaleX: 1, scaleY: 1 }, { duration: 500 }); }, 1000); // Damage hero if on ground LK.setTimeout(function () { if (hero.y > 2200) { // Near ground level hero.takeDamage(); } }, 1000); self.earthquakeCooldown = 600; // 10 seconds }; return self; }); // Legacy Boss class for backward compatibility (delegates to BossType1) // BossType2: Summoner Boss var BossType2 = BaseBoss.expand(function () { var self = BaseBoss.call(this); var bossGraphics = self.attachAsset('boss', { anchorX: 0.5, anchorY: 1.0 }); bossGraphics.tint = 0x9b59b6; // Purple for summoner self.bossType = 'summoner'; self.summonCooldown = 0; self.minionCount = 0; self.maxMinions = 4; self.teleportCooldown = 0; self.shieldActive = false; self.shieldHealth = 0; self.update = function () { self.updatePrediction(); self.updateState(); var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); // Phase transitions affect minion count if (self.health > self.maxHealth * 0.66) { self.phase = 1; self.maxMinions = 2; } else if (self.health > self.maxHealth * 0.33) { self.phase = 2; self.maxMinions = 3; } else { self.phase = 3; self.maxMinions = 4; } // Enhanced movement based on state with predictive positioning if (self.state === 'PURSUING' || self.state === 'ATTACKING' || self.state === 'ENRAGED') { var optimalDistance = self.state === 'ENRAGED' ? 300 : 450; if (distanceToHero < optimalDistance) { // Move away from hero, but anticipate their movement var escapeX = self.x - self.predictedHeroX; var escapeY = self.y - self.predictedHeroY; var distance = Math.sqrt(escapeX * escapeX + escapeY * escapeY); if (distance > 0) { self.lastX = self.x; self.lastY = self.y; var moveSpeed = self.state === 'ENRAGED' ? self.speed * 1.5 : self.speed; self.x += escapeX / distance * moveSpeed; self.y += escapeY / distance * moveSpeed; bossGraphics.scaleX = escapeX > 0 ? -1 : 1; // Face away from hero } } } // Enhanced summoning with warnings if (self.state === 'ATTACKING' && self.summonCooldown <= 0 && self.minionCount < self.maxMinions) { self.showSummonWarning(); } // Predictive teleport when threatened if (self.phase >= 2 && self.teleportCooldown <= 0 && distanceToHero < 250) { self.showTeleportWarning(); } // Shield ability in phase 3 if (self.phase === 3 && !self.shieldActive && self.health <= self.maxHealth * 0.25) { self.activateShield(); } // Cooldown updates if (self.summonCooldown > 0) self.summonCooldown--; if (self.teleportCooldown > 0) self.teleportCooldown--; }; self.summonMinion = function () { // Create a specialized minion near the boss var minion = game.addChild(new Minion()); minion.x = self.x + (Math.random() - 0.5) * 200; minion.y = self.y + (Math.random() - 0.5) * 100; minion.summoner = self; // Link minion to summoner // Keep within bounds minion.x = Math.max(100, Math.min(minion.x, currentLevelData.width - 100)); minion.y = Math.max(1600, Math.min(minion.y, 2400)); // Visual summoning effect tween(bossGraphics, { tint: 0xff00ff }, { duration: 200 }); tween(bossGraphics, { tint: 0x9b59b6 }, { duration: 200 }); LK.effects.flashObject(minion, 0x9b59b6, 500); enemies.push(minion); self.minionCount++; self.summonCooldown = self.phase === 3 ? 180 : 240; // Faster summoning in phase 3 updateEnemiesLeftDisplay(); }; self.teleport = function () { // Teleport to a safe distance from hero var teleportDistance = 500; var angle = Math.random() * Math.PI * 2; var newX = hero.x + Math.cos(angle) * teleportDistance; var newY = hero.y + Math.sin(angle) * teleportDistance; // Keep within bounds newX = Math.max(200, Math.min(newX, currentLevelData.width - 200)); newY = Math.max(1700, Math.min(newY, 2300)); // Visual teleport effect tween(bossGraphics, { alpha: 0, scaleX: 0.1, scaleY: 0.1 }, { duration: 300, onFinish: function onFinish() { self.x = newX; self.y = newY; tween(bossGraphics, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 300 }); LK.effects.flashObject(self, 0x9b59b6, 500); } }); self.teleportCooldown = 300; // 5 seconds }; self.activateShield = function () { self.shieldActive = true; self.shieldHealth = 10; self.isInvulnerable = true; // Visual shield effect tween(bossGraphics, { tint: 0x00ffff }, { duration: 200 }); LK.effects.flashObject(self, 0x00ffff, 1000); // Shield lasts until destroyed or timeout LK.setTimeout(function () { self.deactivateShield(); }, 10000); // 10 seconds max }; self.showSummonWarning = function () { // Create warning circles at summon locations var summonLocations = [{ x: self.x + 150, y: self.y }, { x: self.x - 150, y: self.y }, { x: self.x, y: self.y + 100 }]; for (var i = 0; i < summonLocations.length; i++) { var warning = game.addChild(LK.getAsset('hitEffect', { anchorX: 0.5, anchorY: 0.5, alpha: 0, tint: 0x9b59b6 })); warning.x = summonLocations[i].x; warning.y = summonLocations[i].y; tween(warning, { alpha: 0.7, scaleX: 1.5, scaleY: 1.5 }, { duration: 1000, onFinish: function onFinish() { warning.destroy(); } }); } // Delay actual summon LK.setTimeout(function () { if (self.minionCount < self.maxMinions) { self.summonMinion(); } }, 1000); }; self.showTeleportWarning = function () { // Flash before teleporting tween(bossGraphics, { alpha: 0.3, tint: 0xff00ff }, { duration: 300, onFinish: function onFinish() { self.teleport(); } }); }; self.deactivateShield = function () { self.shieldActive = false; self.isInvulnerable = false; // Vulnerability window after shield breaks self.setState('STUNNED'); tween(bossGraphics, { tint: 0x9b59b6 }, { duration: 500 }); }; // Override takeDamage to handle shield var originalTakeDamage = self.takeDamage; self.takeDamage = function (fromKnife) { if (self.shieldActive) { self.shieldHealth--; LK.effects.flashObject(self, 0x00ffff, 100); if (self.shieldHealth <= 0) { self.deactivateShield(); } return; } // Reduce minion count when boss takes damage if (self.minionCount > 0) { self.minionCount--; } originalTakeDamage.call(self, fromKnife); }; return self; }); // BossType1: Melee/Projectile Boss (original boss enhanced) var BossType1 = BaseBoss.expand(function () { var self = BaseBoss.call(this); var bossGraphics = self.attachAsset('boss', { anchorX: 0.5, anchorY: 1.0 }); self.bossType = 'melee'; self.projectileAttackCooldown = 0; self.chargeAttackCooldown = 0; self.isCharging = false; self.chargingTarget = null; self.spinAttackCooldown = 0; self.isSpinning = false; self.update = function () { self.updatePrediction(); self.updateState(); var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); // Phase transitions if (self.health > self.maxHealth * 0.66) { self.phase = 1; } else if (self.health > self.maxHealth * 0.33) { self.phase = 2; } else { self.phase = 3; } // Movement based on state if (self.state === 'PURSUING' || self.state === 'ENRAGED') { var targetX = self.state === 'ENRAGED' ? self.predictedHeroX : hero.x; var targetY = self.state === 'ENRAGED' ? self.predictedHeroY : hero.y; var dx = targetX - self.x; var dy = targetY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { self.lastX = self.x; self.lastY = self.y; var moveSpeed = self.speed; // Enraged state moves faster toward predicted position if (self.state === 'ENRAGED') { moveSpeed *= 1.8; } self.x += dx / distance * moveSpeed; self.y += dy / distance * moveSpeed; bossGraphics.scaleX = dx > 0 ? 1 : -1; } } // Attacks based on state with visual warnings if (self.state === 'ATTACKING' && self.attackCooldown <= 0) { if (distanceToHero < 150) { self.showMeleeWarning(); } else if (self.phase >= 2 && self.projectileAttackCooldown <= 0) { self.showProjectileWarning(); } } // Special attacks with warnings if (self.phase >= 2 && self.chargeAttackCooldown <= 0 && distanceToHero > 300 && !self.isCharging) { self.showChargeWarning(); } if (self.phase === 3 && self.spinAttackCooldown <= 0 && distanceToHero < 250 && !self.isSpinning) { self.showSpinWarning(); } // Cooldown updates if (self.attackCooldown > 0) self.attackCooldown--; if (self.projectileAttackCooldown > 0) self.projectileAttackCooldown--; if (self.chargeAttackCooldown > 0) self.chargeAttackCooldown--; if (self.spinAttackCooldown > 0) self.spinAttackCooldown--; }; self.meleeAttack = function () { hero.takeDamage(); self.attackCooldown = 90; LK.effects.flashObject(self, 0xffffff, 200); // Screen shake on boss melee attack triggerScreenShake(15, 300, tween.easeOut); }; self.fireProjectile = function () { tween(bossGraphics, { tint: 0x8800ff }, { duration: 200 }); tween(bossGraphics, { tint: 0xffffff }, { duration: 200 }); // Screen shake on boss projectile attack triggerScreenShake(8, 200, tween.easeOut); var projectile = game.addChild(new BossProjectile()); projectile.x = self.x; projectile.y = self.y - 200; var dx = self.predictedHeroX - self.x; var dy = self.predictedHeroY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { var speed = self.phase === 3 ? 12 : 8; projectile.velocityX = dx / distance * speed; projectile.velocityY = dy / distance * speed; } bossProjectiles.push(projectile); self.projectileAttackCooldown = self.phase === 3 ? 60 : 120; }; self.chargeAttack = function () { self.isCharging = true; self.chargingTarget = { x: self.predictedHeroX, y: self.predictedHeroY }; tween(bossGraphics, { tint: 0xff4444, scaleX: 1.2, scaleY: 1.2 }, { duration: 800, easing: tween.easeIn }); LK.setTimeout(function () { if (!self.chargingTarget) return; LK.effects.flashObject(self, 0xffffff, 300); var dx = self.chargingTarget.x - self.x; var dy = self.chargingTarget.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { var chargeDistance = 400; var targetX = self.x + dx / distance * chargeDistance; var targetY = self.y + dy / distance * chargeDistance; targetX = Math.max(100, Math.min(targetX, currentLevelData.width - 100)); targetY = Math.max(1600, Math.min(targetY, 2400)); tween(self, { x: targetX, y: targetY }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(bossGraphics, { tint: 0xffffff, scaleX: 1, scaleY: 1 }, { duration: 500 }); self.isCharging = false; self.chargingTarget = null; } }); LK.setTimeout(function () { var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); if (distanceToHero < 150) { hero.takeDamage(); // Screen shake on successful charge attack hit triggerScreenShake(20, 400, tween.easeOut); } }, 150); } }, 800); self.chargeAttackCooldown = self.phase === 3 ? 240 : 360; }; self.showMeleeWarning = function () { // Flash red warning for 0.5 seconds before attacking tween(bossGraphics, { tint: 0xff0000, scaleX: bossGraphics.scaleX * 1.3, scaleY: 1.3 }, { duration: 500, onFinish: function onFinish() { self.meleeAttack(); tween(bossGraphics, { tint: 0xffffff, scaleX: bossGraphics.scaleX > 0 ? 1 : -1, scaleY: 1 }, { duration: 200 }); } }); }; self.showProjectileWarning = function () { // Create warning indicator at predicted position var warningIndicator = game.addChild(LK.getAsset('hitEffect', { anchorX: 0.5, anchorY: 0.5, alpha: 0, tint: 0xff4444 })); warningIndicator.x = self.predictedHeroX; warningIndicator.y = self.predictedHeroY; tween(warningIndicator, { alpha: 0.8, scaleX: 2, scaleY: 2 }, { duration: 800, onFinish: function onFinish() { self.fireProjectile(); warningIndicator.destroy(); } }); }; self.showChargeWarning = function () { // Show charge direction warning var warningLine = game.addChild(LK.getAsset('slashEffect', { anchorX: 0, anchorY: 0.5, alpha: 0, tint: 0xff4444 })); var dx = self.predictedHeroX - self.x; var dy = self.predictedHeroY - self.y; var angle = Math.atan2(dy, dx); warningLine.x = self.x; warningLine.y = self.y - 100; warningLine.rotation = angle; warningLine.scaleX = 8; warningLine.scaleY = 2; tween(warningLine, { alpha: 0.7 }, { duration: 600, onFinish: function onFinish() { self.chargeAttack(); warningLine.destroy(); } }); }; self.showSpinWarning = function () { // Create expanding red circle warning var warningCircle = game.addChild(LK.getAsset('hitEffect', { anchorX: 0.5, anchorY: 0.5, alpha: 0, tint: 0xff0000 })); warningCircle.x = self.x; warningCircle.y = self.y - 100; tween(warningCircle, { alpha: 0.6, scaleX: 4, scaleY: 4 }, { duration: 1000, onFinish: function onFinish() { self.spinAttack(); warningCircle.destroy(); } }); }; self.spinAttack = function () { self.isSpinning = true; // Create vulnerability window - boss takes double damage during spin self.isInvulnerable = false; var originalTakeDamage = self.takeDamage; self.takeDamage = function (fromKnife) { // Double damage during spin attack originalTakeDamage.call(self, fromKnife); if (self.health > 0) { originalTakeDamage.call(self, fromKnife); } }; tween(bossGraphics, { rotation: Math.PI * 4 }, { duration: 1500, easing: tween.easeInOut, onFinish: function onFinish() { bossGraphics.rotation = 0; self.isSpinning = false; // Restore normal damage after spin self.takeDamage = originalTakeDamage; // Brief stunned state after spin self.setState('STUNNED'); } }); tween(bossGraphics, { scaleX: 1.5, scaleY: 1.5, tint: 0xff8800 }, { duration: 750, easing: tween.easeOut }); tween(bossGraphics, { scaleX: 1, scaleY: 1, tint: 0xffffff }, { duration: 750, easing: tween.easeIn }); for (var i = 0; i < 5; i++) { LK.setTimeout(function () { var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); if (distanceToHero < 200) { hero.takeDamage(); LK.effects.flashObject(hero, 0xff0000, 200); // Screen shake on spin attack hit triggerScreenShake(12, 250, tween.easeOut); } }, i * 300); } self.spinAttackCooldown = 420; }; return self; }); var BloodParticle = Container.expand(function () { var self = Container.call(this); var particleGraphics = self.attachAsset('bloodParticle', { anchorX: 0.5, anchorY: 0.5 }); self.velocityX = (Math.random() - 0.5) * 8; self.velocityY = -Math.random() * 6 - 2; self.gravity = 0.3; self.lifetime = 60; // 1 second at 60fps self.particleType = 'normal'; // normal, large, small, spray self.reset = function (startX, startY, type) { self.x = startX; self.y = startY; self.particleType = type || 'normal'; // Different sizes, speeds, and lifetimes based on type if (self.particleType === 'large') { self.velocityX = (Math.random() - 0.5) * 6; self.velocityY = -Math.random() * 4 - 1; self.lifetime = 90; // Larger particles last longer particleGraphics.scaleX = 1.5; particleGraphics.scaleY = 1.5; particleGraphics.tint = 0x8b0000; // Dark red } else if (self.particleType === 'small') { self.velocityX = (Math.random() - 0.5) * 12; // Faster small particles self.velocityY = -Math.random() * 8 - 3; self.lifetime = 30; // Short-lived particleGraphics.scaleX = 0.5; particleGraphics.scaleY = 0.5; particleGraphics.tint = 0xff4444; // Bright red } else if (self.particleType === 'spray') { self.velocityX = (Math.random() - 0.5) * 16; // Very fast spray self.velocityY = -Math.random() * 10 - 1; self.lifetime = 45; particleGraphics.scaleX = 0.7; particleGraphics.scaleY = 0.7; particleGraphics.tint = 0xcc2222; // Medium red } else { // Normal particles self.velocityX = (Math.random() - 0.5) * 8; self.velocityY = -Math.random() * 6 - 2; self.lifetime = 60; particleGraphics.scaleX = 1; particleGraphics.scaleY = 1; particleGraphics.tint = 0x8b0000; } particleGraphics.alpha = 1; self.visible = true; }; self.update = function () { self.x += self.velocityX; self.y += self.velocityY; self.velocityY += self.gravity; // Different fade patterns based on type var maxLifetime = 60; if (self.particleType === 'large') maxLifetime = 90;else if (self.particleType === 'small') maxLifetime = 30;else if (self.particleType === 'spray') maxLifetime = 45; // Fade out over time self.lifetime--; particleGraphics.alpha = self.lifetime / maxLifetime; if (self.lifetime <= 0) { // Remove from bloodParticles array for (var i = bloodParticles.length - 1; i >= 0; i--) { if (bloodParticles[i] === self) { bloodParticles.splice(i, 1); break; } } returnToPool(self, 'bloodParticle'); } }; return self; }); var BossProjectile = Container.expand(function () { var self = Container.call(this); var projectileGraphics = self.attachAsset('bossProjectile', { anchorX: 0.5, anchorY: 0.5 }); self.velocityX = 0; self.velocityY = 0; self.lifetime = 300; // 5 seconds self.update = function () { self.x += self.velocityX; self.y += self.velocityY; self.lifetime--; // Set rotation to face movement direction if (self.velocityX !== 0 || self.velocityY !== 0) { var angle = Math.atan2(self.velocityY, self.velocityX); projectileGraphics.rotation = angle; } // Check collision with hero var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); if (distanceToHero < 80) { hero.takeDamage(); self.destroy(); // Remove from bossProjectiles array for (var i = bossProjectiles.length - 1; i >= 0; i--) { if (bossProjectiles[i] === self) { bossProjectiles.splice(i, 1); break; } } return; } // Remove if lifetime expired or off screen if (self.lifetime <= 0 || self.x < -100 || self.x > currentLevelData.width + 100 || self.y < -100 || self.y > 3000) { self.destroy(); // Remove from bossProjectiles array for (var i = bossProjectiles.length - 1; i >= 0; i--) { if (bossProjectiles[i] === self) { bossProjectiles.splice(i, 1); break; } } } }; return self; }); var Coin = Container.expand(function () { var self = Container.call(this); var coinGraphics = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5 }); self.collectTimer = 0; self.update = function () { // Auto-collect after short delay self.collectTimer++; if (self.collectTimer > 30) { // 0.5 seconds self.collect(); } // Spin animation coinGraphics.rotation += 0.1; }; self.collect = function () { LK.getSound('coin').play(); // Remove from coins array for (var i = coins.length - 1; i >= 0; i--) { if (coins[i] === self) { coins.splice(i, 1); break; } } self.destroy(); }; return self; }); var DamageNumber = Container.expand(function () { var self = Container.call(this); var damageText = new Text2('', { size: 60, fill: 0xFFFFFF }); damageText.anchor.set(0.5, 0.5); self.addChild(damageText); self.lifetime = 120; // 2 seconds at 60fps self.velocityY = -3; // Float upward self.fadeSpeed = 1 / 120; // Fade over lifetime self.reset = function (startX, startY, damage, isCritical) { self.x = startX; self.y = startY; self.lifetime = 120; self.velocityY = isCritical ? -4 : -3; // Faster rise for critical hits damageText.setText(damage.toString()); damageText.alpha = 1; self.visible = true; // Different styling for critical hits if (isCritical) { damageText.fill = 0xFFD700; // Gold for critical damageText.size = 80; // Larger text tween(self, { scaleX: 1.3, scaleY: 1.3 }, { duration: 200, easing: tween.easeOut }); } else { damageText.fill = 0xFFFFFF; // White for normal damageText.size = 60; self.scaleX = 1; self.scaleY = 1; } }; self.update = function () { self.y += self.velocityY; self.velocityY *= 0.98; // Slow down over time self.lifetime--; damageText.alpha = self.lifetime / 120; // Fade out if (self.lifetime <= 0) { // Remove from damageNumbers array for (var i = damageNumbers.length - 1; i >= 0; i--) { if (damageNumbers[i] === self) { damageNumbers.splice(i, 1); break; } } returnToPool(self, 'damageNumber'); } }; return self; }); var DustCloud = Container.expand(function () { var self = Container.call(this); var dustGraphics = self.attachAsset('bloodParticle', { anchorX: 0.5, anchorY: 0.5 }); dustGraphics.tint = 0x8b7355; // Brown dust color dustGraphics.scaleX = 0.8; dustGraphics.scaleY = 0.8; self.velocityX = 0; self.velocityY = 0; self.lifetime = 40; // Medium lifetime self.expandRate = 0.02; // How fast dust expands self.reset = function (startX, startY, direction) { self.x = startX; self.y = startY; // Dust moves opposite to movement direction var baseSpeed = 1 + Math.random() * 2; if (direction === 'left') { self.velocityX = baseSpeed; } else if (direction === 'right') { self.velocityX = -baseSpeed; } else if (direction === 'up') { self.velocityY = baseSpeed * 0.5; } else if (direction === 'down') { self.velocityY = -baseSpeed * 0.5; } else { // Random direction for general dust var angle = Math.random() * Math.PI * 2; self.velocityX = Math.cos(angle) * baseSpeed; self.velocityY = Math.sin(angle) * baseSpeed; } self.velocityY -= Math.random() * 1; // Slight upward drift dustGraphics.alpha = 0.6; dustGraphics.scaleX = 0.4 + Math.random() * 0.4; dustGraphics.scaleY = 0.4 + Math.random() * 0.4; self.visible = true; }; self.update = function () { self.x += self.velocityX; self.y += self.velocityY; // Dust slows down and expands self.velocityX *= 0.98; self.velocityY *= 0.98; dustGraphics.scaleX += self.expandRate; dustGraphics.scaleY += self.expandRate; // Fade out over time self.lifetime--; dustGraphics.alpha = self.lifetime / 40 * 0.6; if (self.lifetime <= 0) { // Remove from dustClouds array for (var i = dustClouds.length - 1; i >= 0; i--) { if (dustClouds[i] === self) { dustClouds.splice(i, 1); break; } } returnToPool(self, 'dustCloud'); } }; return self; }); var Enemy = Container.expand(function (enemyType) { var self = Container.call(this); self.enemyType = enemyType || 'basic'; // Choose asset based on enemy type var assetName = 'enemy'; if (self.enemyType === 'basic') { assetName = 'enemyBasic'; } else if (self.enemyType === 'strong') { assetName = 'enemyStrong'; } else if (self.enemyType === 'fast') { assetName = 'enemyFast'; } else if (self.enemyType === 'tank') { assetName = 'enemyTank'; } else if (self.enemyType === 'hunter') { assetName = 'enemyHunter'; } else if (self.enemyType === 'assassin') { assetName = 'enemyAssassin'; } var enemyGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 1.0 }); self.health = 2; self.speed = 1; self.attackCooldown = 0; self.fromLeft = true; self.lastX = 0; self.lastY = 0; self.alerted = false; self.alertedByKnife = false; self.update = function () { var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); // Initialize group AI properties if not already set if (!self.hasOwnProperty('isAssignedFlanker')) self.isAssignedFlanker = false; if (!self.hasOwnProperty('flankingTarget')) self.flankingTarget = null; if (!self.hasOwnProperty('packGroup')) self.packGroup = null; if (!self.hasOwnProperty('leaderBuff')) self.leaderBuff = null; if (!self.hasOwnProperty('communicationTimer')) self.communicationTimer = 0; // Update communication timer if (self.communicationTimer > 0) self.communicationTimer--; // Process group communications var communications = groupAI.getCommunications(self, 120, 400); for (var c = 0; c < communications.length; c++) { var comm = communications[c]; if (comm.type === 'spotted_hero' && !self.alerted) { self.alerted = true; groupAI.broadcastAlert(self, 'spotted_hero', 1); } else if (comm.type === 'under_attack' && comm.priority >= 2) { self.alerted = true; // Move to assist ally under attack if (!self.isAssignedFlanker && Math.random() < 0.3) { self.assistTarget = { x: comm.x, y: comm.y }; } } else if (comm.type === 'flanking_position' && !self.isAssignedFlanker && self.enemyType !== 'tank') { groupAI.assignFlankingPosition(self); } } // Different sight ranges for different enemy types var sightRange = 500; if (self.enemyType === 'hunter') { sightRange = 800; // Hunters have better sight } else if (self.enemyType === 'assassin') { sightRange = 600; // Assassins have good sight } else if (self.enemyType === 'tank') { sightRange = 400; // Tanks have poor sight } else if (self.enemyType === 'leader') { sightRange = 700; // Leaders have good coordination sight } var canSeeHero = distanceToHero < sightRange; // Broadcast hero sighting if (canSeeHero && !self.alerted && self.communicationTimer <= 0) { groupAI.broadcastAlert(self, 'spotted_hero', 1); self.alerted = true; self.communicationTimer = 60; // 1 second cooldown } if (canSeeHero || self.alerted) { // Determine movement target based on role var targetX = hero.x; var targetY = hero.y; var moveSpeed = self.speed; // Apply leader buffs if (self.leaderBuff) { moveSpeed *= self.leaderBuff.speedMultiplier; } // Flanking behavior if (self.isAssignedFlanker && self.flankingTarget) { var flankDistance = Math.sqrt(Math.pow(self.x - self.flankingTarget.x, 2) + Math.pow(self.y - self.flankingTarget.y, 2)); if (flankDistance > 50) { // Move to flanking position targetX = self.flankingTarget.x; targetY = self.flankingTarget.y; } else { // Reached flanking position, now attack hero targetX = hero.x; targetY = hero.y; moveSpeed *= 1.2; // Speed boost when in flanking position } } // Pack hunting behavior for fast enemies if (self.enemyType === 'fast' || self.enemyType === 'hunter') { var nearbyPackMembers = spatialGrid.getNearbyObjects(self.x, self.y, 200); var packMates = 0; for (var p = 0; p < nearbyPackMembers.length; p++) { if (nearbyPackMembers[p] !== self && (nearbyPackMembers[p].enemyType === 'fast' || nearbyPackMembers[p].enemyType === 'hunter')) { packMates++; } } if (packMates >= 1) { // Pack hunting behavior - try to herd hero toward stronger enemies var nearbyStrongEnemies = spatialGrid.getNearbyObjects(hero.x, hero.y, 600); var strongestEnemy = null; for (var s = 0; s < nearbyStrongEnemies.length; s++) { if (nearbyStrongEnemies[s].enemyType === 'tank' || nearbyStrongEnemies[s].enemyType === 'strong' || nearbyStrongEnemies[s].enemyType === 'leader') { strongestEnemy = nearbyStrongEnemies[s]; break; } } if (strongestEnemy) { // Position to herd hero toward strongest enemy var herdX = hero.x + (strongestEnemy.x - hero.x) * 0.3; var herdY = hero.y + (strongestEnemy.y - hero.y) * 0.3; targetX = herdX; targetY = herdY; moveSpeed *= 1.4; // Faster when pack hunting } } } // Assist behavior if (self.assistTarget && !self.isAssignedFlanker) { var assistDistance = Math.sqrt(Math.pow(self.x - self.assistTarget.x, 2) + Math.pow(self.y - self.assistTarget.y, 2)); if (assistDistance > 100) { targetX = self.assistTarget.x; targetY = self.assistTarget.y; moveSpeed *= 1.1; // Slight speed boost when assisting } else { self.assistTarget = null; // Clear assist target when reached } } // Calculate movement toward target var dx = targetX - self.x; var dy = targetY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { // Special movement for assassin type - teleport ability if (self.enemyType === 'assassin' && distanceToHero > 300 && distanceToHero < 600 && Math.random() < 0.02) { // Teleport closer to hero var teleportDistance = 150; var teleportX = hero.x + (Math.random() - 0.5) * teleportDistance; var teleportY = hero.y + (Math.random() - 0.5) * teleportDistance; // Keep within bounds teleportX = Math.max(100, Math.min(teleportX, currentLevelData.width - 100)); teleportY = Math.max(1600, Math.min(teleportY, 2400)); self.x = teleportX; self.y = teleportY; // Flash effect for teleport LK.effects.flashObject(self, 0x9b59b6, 300); var newX = self.x; var newY = self.y; } else { // Hunter type - faster when far from hero if (self.enemyType === 'hunter' && distanceToHero > 400) { moveSpeed *= 1.5; // 50% speed boost when hunting from distance } var newX = self.x + dx / distance * moveSpeed; var newY = self.y + dy / distance * moveSpeed; } // Check collision with other enemies before moving using spatial partitioning var wouldCollide = false; var nearbyEnemies = spatialGrid.getNearbyObjects(newX, newY, 100); for (var i = 0; i < nearbyEnemies.length; i++) { var otherEnemy = nearbyEnemies[i]; if (otherEnemy === self) continue; // Skip self var edx = newX - otherEnemy.x; var edy = newY - otherEnemy.y; var edistance = Math.sqrt(edx * edx + edy * edy); if (edistance < 70) { // Collision threshold between enemies wouldCollide = true; break; } } // Only move if no collision would occur if (!wouldCollide) { self.lastX = self.x; self.lastY = self.y; self.x = newX; self.y = newY; } // Face direction of movement enemyGraphics.scaleX = dx > 0 ? 1 : -1; } // Attack hero if close enough var attackRange = 100; if (self.leaderBuff) { attackRange *= 1.1; // Slightly increased attack range with leader buff } if (distanceToHero < attackRange && self.attackCooldown <= 0) { var damage = 1; if (self.leaderBuff && self.leaderBuff.damageMultiplier) { damage = Math.floor(damage * self.leaderBuff.damageMultiplier); } hero.takeDamage(); self.attackCooldown = 120; // 2 seconds at 60fps } // Glow effects based on state if (self.alerted && !self.glowOutline) { createGlowOutline(self, 0xff8844, 0.5); // Orange glow for alerted } else if (!self.alerted && self.glowOutline) { removeGlowOutline(self); } if (self.health <= 1 && (!self.glowOutline || self.glowOutline.tint !== 0xff0000)) { removeGlowOutline(self); createGlowOutline(self, 0xff0000, 0.8); // Red glow for low health } else if (self.isAssignedFlanker && (!self.glowOutline || self.glowOutline.tint !== 0x8844ff)) { removeGlowOutline(self); createGlowOutline(self, 0x8844ff, 0.4); // Blue glow for flankers } } else if (!self.alerted) { // Only roam if not alerted - alerted enemies keep chasing even when they can't see hero if (!self.roamDirection || Math.random() < 0.01) { self.roamDirection = { x: (Math.random() - 0.5) * 2, y: (Math.random() - 0.5) * 2 }; } var newX = self.x + self.roamDirection.x * self.speed * 0.5; var newY = self.y + self.roamDirection.y * self.speed * 0.5; // Check collision with other enemies before roaming var wouldCollide = false; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy === self) continue; // Skip self var edx = newX - otherEnemy.x; var edy = newY - otherEnemy.y; var edistance = Math.sqrt(edx * edx + edy * edy); if (edistance < 70) { // Collision threshold between enemies wouldCollide = true; break; } } // Keep within level bounds and check collisions if (!wouldCollide && newX > 100 && newX < currentLevelData.width - 100) { self.lastX = self.x; self.x = newX; enemyGraphics.scaleX = self.roamDirection.x > 0 ? 1 : -1; } if (!wouldCollide && newY > 1600 && newY < 2400) { self.lastY = self.y; self.y = newY; } } else { // Alerted enemies that can't see hero still try to chase in last known direction var dx = hero.x - self.x; var dy = hero.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { var newX = self.x + dx / distance * self.speed; var newY = self.y + dy / distance * self.speed; // Check collision with other enemies before moving var wouldCollide = false; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy === self) continue; // Skip self var edx = newX - otherEnemy.x; var edy = newY - otherEnemy.y; var edistance = Math.sqrt(edx * edx + edy * edy); if (edistance < 70) { // Collision threshold between enemies wouldCollide = true; break; } } // Only move if no collision would occur if (!wouldCollide) { self.lastX = self.x; self.lastY = self.y; self.x = newX; self.y = newY; } // Face direction of movement enemyGraphics.scaleX = dx > 0 ? 1 : -1; } } if (self.attackCooldown > 0) { self.attackCooldown--; } }; self.takeDamage = function (fromKnife) { var damage = 1; var isCritical = Math.random() < 0.15; // 15% critical chance if (isCritical) { damage = 2; self.health -= 2; } else { self.health--; } // Create damage number var damageNumber = game.addChild(getFromPool('damageNumber')); damageNumber.reset(self.x + (Math.random() - 0.5) * 40, self.y - 100, damage, isCritical); damageNumbers.push(damageNumber); LK.effects.flashObject(self, isCritical ? 0xFFD700 : 0xffffff, isCritical ? 400 : 200); // Add glow effect based on enemy state if (self.alerted && !self.glowOutline) { createGlowOutline(self, 0xff4444, 0.6); // Red glow for alerted } else if (self.health <= 1 && !self.glowOutline) { createGlowOutline(self, 0xffaa00, 0.8); // Orange glow for low health } // Create knockback effect var knockbackForce = 40; var knockbackDirection = fromKnife ? 1 : hero.x < self.x ? 1 : -1; var targetX = self.x + knockbackDirection * knockbackForce; var targetY = self.y - 20; // Slight upward knockback // Apply knockback with bounds checking targetX = Math.max(100, Math.min(targetX, currentLevelData.width - 100)); targetY = Math.max(1600, Math.min(targetY, 2400)); tween(self, { x: targetX, y: targetY }, { duration: 200, easing: tween.easeOut }); // Create enhanced blood particles with variety for (var p = 0; p < 8; p++) { var bloodParticle = game.addChild(getFromPool('bloodParticle')); var particleType = Math.random() < 0.2 ? 'large' : Math.random() < 0.6 ? 'small' : 'normal'; bloodParticle.reset(self.x + (Math.random() - 0.5) * 40, self.y - 60 + (Math.random() - 0.5) * 40, particleType); bloodParticles.push(bloodParticle); } // Add impact sparks when hit by knife if (fromKnife) { for (var s = 0; s < 6; s++) { var spark = game.addChild(getFromPool('impactSpark')); var sparkType = Math.random() < 0.3 ? 'bright' : Math.random() < 0.5 ? 'orange' : 'normal'; spark.reset(self.x + (Math.random() - 0.5) * 30, self.y - 70 + (Math.random() - 0.5) * 30, sparkType); impactSparks.push(spark); } self.alerted = true; self.alertedByKnife = true; } if (self.health <= 0) { self.die(); } }; self.die = function () { // Screen shake on enemy death (smaller intensity) triggerScreenShake(6, 150, tween.easeOut); // Determine coin reward based on enemy difficulty var coinReward = 1; // Default for basic enemies if (self.enemyType === 'basic') { coinReward = 1; } else if (self.enemyType === 'fast') { coinReward = 2; } else if (self.enemyType === 'scout') { coinReward = 3; } else if (self.enemyType === 'strong') { coinReward = 3; } else if (self.enemyType === 'archer') { coinReward = 4; } else if (self.enemyType === 'berserker') { coinReward = 4; } else if (self.enemyType === 'hunter') { coinReward = 5; } else if (self.enemyType === 'shield') { coinReward = 6; } else if (self.enemyType === 'assassin') { coinReward = 6; } else if (self.enemyType === 'tank') { coinReward = 8; } else if (self.enemyType === 'leader') { coinReward = 10; } // Automatically give coins to upgrade tree upgradeTree.addCoins(coinReward); // Drop visual coin var coin = game.addChild(new Coin()); coin.x = self.x; coin.y = self.y; coins.push(coin); // 10% chance to drop health potion if (Math.random() < 0.1) { var healthPotion = game.addChild(new HealthPotion()); healthPotion.x = self.x + (Math.random() - 0.5) * 80; // Slight random offset healthPotion.y = self.y + (Math.random() - 0.5) * 80; healthPotions.push(healthPotion); } // Add score and combo var baseScore = 10; var comboMultiplier = Math.floor(hero.comboCount / 5) + 1; var finalScore = baseScore * comboMultiplier; LK.setScore(LK.getScore() + finalScore); hero.addCombo(); // Remove from enemies array for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } self.destroy(); updateScoreDisplay(); updateEnemiesLeftDisplay(); }; return self; }); var Shield = Enemy.expand(function () { var self = Enemy.call(this, 'shield'); var shieldGraphics = self.attachAsset('enemyTank', { anchorX: 0.5, anchorY: 1.0 }); shieldGraphics.tint = 0x4488ff; // Blue tint for shields self.enemyType = 'shield'; self.health = 6; self.speed = 1.0; self.shieldActive = true; self.protectionRadius = 200; self.blockCooldown = 0; // Override update to include shield behavior var originalUpdate = self.update; self.update = function () { originalUpdate.call(self); // Shield positioning: try to stay between hero and vulnerable allies var vulnerableAllies = []; var nearbyAllies = spatialGrid.getNearbyObjects(self.x, self.y, self.protectionRadius * 1.5); for (var i = 0; i < nearbyAllies.length; i++) { var ally = nearbyAllies[i]; if (ally !== self && ally.enemyType && (ally.enemyType === 'archer' || ally.enemyType === 'scout' || ally.health <= 2)) { vulnerableAllies.push(ally); } } // If there are vulnerable allies, position between them and hero if (vulnerableAllies.length > 0) { var avgAllyX = 0; var avgAllyY = 0; for (var i = 0; i < vulnerableAllies.length; i++) { avgAllyX += vulnerableAllies[i].x; avgAllyY += vulnerableAllies[i].y; } avgAllyX /= vulnerableAllies.length; avgAllyY /= vulnerableAllies.length; // Position between average ally position and hero var protectX = avgAllyX + (hero.x - avgAllyX) * 0.3; var protectY = avgAllyY + (hero.y - avgAllyY) * 0.3; var dx = protectX - self.x; var dy = protectY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 50) { // Move toward protection position var newX = self.x + dx / distance * self.speed * 0.8; var newY = self.y + dy / distance * self.speed * 0.8; // Check collision before moving var wouldCollide = false; var nearbyEnemies = spatialGrid.getNearbyObjects(newX, newY, 100); for (var j = 0; j < nearbyEnemies.length; j++) { var otherEnemy = nearbyEnemies[j]; if (otherEnemy === self) continue; var edx = newX - otherEnemy.x; var edy = newY - otherEnemy.y; var edistance = Math.sqrt(edx * edx + edy * edy); if (edistance < 80) { wouldCollide = true; break; } } if (!wouldCollide) { // Keep within bounds if (newX > 100 && newX < currentLevelData.width - 100) { self.lastX = self.x; self.x = newX; } if (newY > 1600 && newY < 2400) { self.lastY = self.y; self.y = newY; } shieldGraphics.scaleX = dx > 0 ? 1 : -1; } } } if (self.blockCooldown > 0) { self.blockCooldown--; } }; self.blockProjectile = function (projectile) { if (!self.shieldActive || self.blockCooldown > 0) return false; // Check if projectile is within blocking range var dx = projectile.x - self.x; var dy = projectile.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 120) { // Block the projectile projectile.destroy(); // Remove from projectiles array for (var i = bossProjectiles.length - 1; i >= 0; i--) { if (bossProjectiles[i] === projectile) { bossProjectiles.splice(i, 1); break; } } // Visual block effect tween(shieldGraphics, { tint: 0x88aaff, scaleX: shieldGraphics.scaleX * 1.3, scaleY: 1.3 }, { duration: 200, onFinish: function onFinish() { tween(shieldGraphics, { tint: 0x4488ff, scaleX: shieldGraphics.scaleX > 0 ? 1 : -1, scaleY: 1 }, { duration: 200 }); } }); // Create block sparks for (var s = 0; s < 8; s++) { var spark = game.addChild(getFromPool('impactSpark')); spark.reset(self.x + (Math.random() - 0.5) * 60, self.y - 60 + (Math.random() - 0.5) * 60, 'bright'); impactSparks.push(spark); } self.blockCooldown = 60; // 1 second cooldown return true; } return false; }; // Override takeDamage to handle shield protection var originalTakeDamage = self.takeDamage; self.takeDamage = function (fromKnife) { // Shield enemies take reduced damage from the front var damage = 1; if (fromKnife) { // Check if knife is coming from the front var dx = hero.x - self.x; var shieldFacing = shieldGraphics.scaleX > 0 ? 1 : -1; if (dx > 0 && shieldFacing > 0 || dx < 0 && shieldFacing < 0) { // Knife blocked by shield, take reduced damage damage = 0; // Visual block effect LK.effects.flashObject(self, 0x88aaff, 300); // Create block sparks for (var s = 0; s < 6; s++) { var spark = game.addChild(getFromPool('impactSpark')); spark.reset(self.x + (Math.random() - 0.5) * 50, self.y - 70 + (Math.random() - 0.5) * 50, 'bright'); impactSparks.push(spark); } return; // No damage taken } } if (damage > 0) { originalTakeDamage.call(self, fromKnife); } }; return self; }); var Scout = Enemy.expand(function () { var self = Enemy.call(this, 'scout'); var scoutGraphics = self.attachAsset('enemyFast', { anchorX: 0.5, anchorY: 1.0 }); scoutGraphics.tint = 0x88ff88; // Light green for scouts scoutGraphics.scaleX = 0.9; scoutGraphics.scaleY = 0.9; self.enemyType = 'scout'; self.health = 1; self.speed = 5; // Very fast self.sightRange = 900; // Excellent sight self.alertCooldown = 0; self.fleeingFromCombat = false; self.lastAlertTime = 0; // Override update to include scout-specific behaviors var originalUpdate = self.update; self.update = function () { var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); // Scout-specific sight and alerting if (distanceToHero < self.sightRange && !self.alerted) { self.alerted = true; // Scouts immediately alert all nearby enemies groupAI.broadcastAlert(self, 'spotted_hero', 3); // High priority alert self.lastAlertTime = LK.ticks; // Create alert visual effect tween(scoutGraphics, { tint: 0xffff44, // Yellow alert color scaleX: 1.2, scaleY: 1.2 }, { duration: 500, onFinish: function onFinish() { tween(scoutGraphics, { tint: 0x88ff88, scaleX: 0.9, scaleY: 0.9 }, { duration: 300 }); } }); } // Flee from combat when hero gets too close if (distanceToHero < 250) { self.fleeingFromCombat = true; } else if (distanceToHero > 500) { self.fleeingFromCombat = false; } if (self.fleeingFromCombat) { // Move away from hero at maximum speed var dx = self.x - hero.x; var dy = self.y - hero.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { var fleeSpeed = self.speed * 1.5; // Extra fast when fleeing var newX = self.x + dx / distance * fleeSpeed; var newY = self.y + dy / distance * fleeSpeed; // Check collision with other enemies before moving var wouldCollide = false; var nearbyEnemies = spatialGrid.getNearbyObjects(newX, newY, 80); for (var i = 0; i < nearbyEnemies.length; i++) { var otherEnemy = nearbyEnemies[i]; if (otherEnemy === self) continue; var edx = newX - otherEnemy.x; var edy = newY - otherEnemy.y; var edistance = Math.sqrt(edx * edx + edy * edy); if (edistance < 60) { wouldCollide = true; break; } } if (!wouldCollide) { // Keep within level bounds if (newX > 100 && newX < currentLevelData.width - 100) { self.lastX = self.x; self.x = newX; } if (newY > 1600 && newY < 2400) { self.lastY = self.y; self.y = newY; } scoutGraphics.scaleX = dx > 0 ? 0.9 : -0.9; // Face away from hero } } } else { // Use original enemy update when not fleeing originalUpdate.call(self); } // Periodic re-alerting to maintain enemy awareness if (self.alerted && LK.ticks - self.lastAlertTime > 300) { // Every 5 seconds groupAI.broadcastAlert(self, 'spotted_hero', 2); self.lastAlertTime = LK.ticks; } }; // Override takeDamage to trigger emergency broadcast var originalTakeDamage = self.takeDamage; self.takeDamage = function (fromKnife) { // Emergency alert when scout is attacked groupAI.broadcastAlert(self, 'under_attack', 3); originalTakeDamage.call(self, fromKnife); }; return self; }); var Leader = Enemy.expand(function () { var self = Enemy.call(this, 'leader'); var leaderGraphics = self.attachAsset('enemyStrong', { anchorX: 0.5, anchorY: 1.0 }); leaderGraphics.tint = 0xFFD700; // Golden tint for leaders leaderGraphics.scaleX = 1.2; leaderGraphics.scaleY = 1.2; self.enemyType = 'leader'; self.health = 6; self.speed = 1.5; self.buffRadius = 250; self.commandRadius = 400; self.buffCooldown = 0; self.isCommanding = false; self.commandedAllies = []; // Override update to include leadership behaviors var originalUpdate = self.update; self.update = function () { // Call original enemy update first originalUpdate.call(self); // Leadership AI behaviors self.manageAllies(); self.coordinateAttacks(); self.provideBattlefieldIntelligence(); // Update buff cooldown if (self.buffCooldown > 0) self.buffCooldown--; }; self.manageAllies = function () { var nearbyAllies = spatialGrid.getNearbyObjects(self.x, self.y, self.commandRadius); self.commandedAllies = []; for (var i = 0; i < nearbyAllies.length; i++) { var ally = nearbyAllies[i]; if (ally !== self && ally.enemyType !== 'boss' && ally.enemyType !== 'leader') { self.commandedAllies.push(ally); // Provide speed buff to nearby allies if (Math.sqrt(Math.pow(self.x - ally.x, 2) + Math.pow(self.y - ally.y, 2)) <= self.buffRadius) { if (!ally.leaderBuff) { ally.leaderBuff = { speedMultiplier: 1.3, damageMultiplier: 1.2, source: self }; // Visual indication of buff createGlowOutline(ally, 0xFFD700, 0.3); } } } } }; self.coordinateAttacks = function () { if (self.commandedAllies.length >= 2 && self.buffCooldown <= 0) { // Order flanking maneuvers groupAI.broadcastAlert(self, 'flanking_position', 3); for (var i = 0; i < Math.min(3, self.commandedAllies.length); i++) { var ally = self.commandedAllies[i]; if (!ally.isAssignedFlanker) { groupAI.assignFlankingPosition(ally); } } self.buffCooldown = 300; // 5 seconds self.isCommanding = true; // Visual command effect tween(leaderGraphics, { tint: 0xFFAA00, scaleX: 1.4, scaleY: 1.4 }, { duration: 500, onFinish: function onFinish() { tween(leaderGraphics, { tint: 0xFFD700, scaleX: 1.2, scaleY: 1.2 }, { duration: 300 }); self.isCommanding = false; } }); } }; self.provideBattlefieldIntelligence = function () { // Share hero position with distant allies if (Math.random() < 0.05) { // 5% chance per frame groupAI.broadcastAlert(self, 'spotted_hero', 2); } }; // Override takeDamage to rally allies when leader is threatened var originalTakeDamage = self.takeDamage; self.takeDamage = function (fromKnife) { originalTakeDamage.call(self, fromKnife); // Rally cry when damaged groupAI.broadcastAlert(self, 'under_attack', 3); // Boost nearby allies when leader is in danger for (var i = 0; i < self.commandedAllies.length; i++) { var ally = self.commandedAllies[i]; ally.alerted = true; if (ally.leaderBuff) { ally.leaderBuff.speedMultiplier = 1.5; // Increased speed when leader threatened } } }; // Override die to remove buffs from allies var originalDie = self.die; self.die = function () { // Remove leader buffs from all allies for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.leaderBuff && enemy.leaderBuff.source === self) { enemy.leaderBuff = null; removeGlowOutline(enemy); } } originalDie.call(self); }; return self; }); var Berserker = Enemy.expand(function () { var self = Enemy.call(this, 'berserker'); var berserkerGraphics = self.attachAsset('enemyStrong', { anchorX: 0.5, anchorY: 1.0 }); berserkerGraphics.tint = 0xff6666; // Red tint for berserkers self.enemyType = 'berserker'; self.health = 4; self.maxHealth = 4; self.speed = 1.5; self.baseSpeed = 1.5; self.isBerserk = false; self.berserkThreshold = 1; // Goes berserk at 1 health // Override update to include berserker rage var originalUpdate = self.update; self.update = function () { // Check for berserk activation if (!self.isBerserk && self.health <= self.berserkThreshold) { self.activateBerserk(); } originalUpdate.call(self); }; self.activateBerserk = function () { self.isBerserk = true; self.speed = self.baseSpeed * 2.5; // Much faster when berserk berserkerGraphics.tint = 0xff0000; // Bright red when berserk berserkerGraphics.scaleX = 1.3; berserkerGraphics.scaleY = 1.3; self.attackCooldown = Math.floor(self.attackCooldown * 0.5); // Faster attacks // Visual berserk effect tween(berserkerGraphics, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(berserkerGraphics, { scaleX: 1.3, scaleY: 1.3 }, { duration: 200 }); } }); // Create rage particles for (var i = 0; i < 12; i++) { var particle = game.addChild(getFromPool('bloodParticle')); particle.reset(self.x + (Math.random() - 0.5) * 80, self.y - 80 + (Math.random() - 0.5) * 80, 'spray'); var particleGraphics = particle.getChildAt(0); particleGraphics.tint = 0xff4444; // Red rage particles bloodParticles.push(particle); } // Broadcast rage to alert other enemies groupAI.broadcastAlert(self, 'under_attack', 2); }; // Override takeDamage to handle berserk damage bonus var originalTakeDamage = self.takeDamage; self.takeDamage = function (fromKnife) { var damage = 1; // Berserk berserkers deal damage back to attackers if (self.isBerserk && !fromKnife) { // Only counter-attack melee attacks, not knife throws var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); if (distanceToHero < 150) { hero.takeDamage(); // Counter-attack LK.effects.flashObject(self, 0xff0000, 300); } } originalTakeDamage.call(self, fromKnife); }; return self; }); var Archer = Enemy.expand(function () { var self = Enemy.call(this, 'archer'); var archerGraphics = self.attachAsset('enemyHunter', { anchorX: 0.5, anchorY: 1.0 }); archerGraphics.tint = 0x8844ff; // Purple tint for archers self.enemyType = 'archer'; self.health = 2; self.speed = 1.8; self.shootCooldown = 0; self.optimalRange = 400; // Preferred distance from hero self.maxRange = 600; // Maximum shooting range // Override update to include archer behavior var originalUpdate = self.update; self.update = function () { var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); // Archer positioning: maintain optimal distance if (distanceToHero < self.optimalRange - 50) { // Too close, move away var dx = self.x - hero.x; var dy = self.y - hero.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { var newX = self.x + dx / distance * self.speed; var newY = self.y + dy / distance * self.speed; // Check collision before moving var wouldCollide = false; var nearbyEnemies = spatialGrid.getNearbyObjects(newX, newY, 80); for (var i = 0; i < nearbyEnemies.length; i++) { var otherEnemy = nearbyEnemies[i]; if (otherEnemy === self) continue; var edx = newX - otherEnemy.x; var edy = newY - otherEnemy.y; var edistance = Math.sqrt(edx * edx + edy * edy); if (edistance < 70) { wouldCollide = true; break; } } if (!wouldCollide) { // Keep within bounds if (newX > 100 && newX < currentLevelData.width - 100) { self.lastX = self.x; self.x = newX; } if (newY > 1600 && newY < 2400) { self.lastY = self.y; self.y = newY; } archerGraphics.scaleX = dx > 0 ? 1 : -1; // Face away from hero } } } else if (distanceToHero > self.optimalRange + 50) { // Too far, move closer (use original enemy behavior) originalUpdate.call(self); } // Shoot at hero if in range and cooldown is ready if (distanceToHero <= self.maxRange && self.shootCooldown <= 0 && self.alerted) { self.shootArrow(); } if (self.shootCooldown > 0) { self.shootCooldown--; } // Update attack cooldown from original if (self.attackCooldown > 0) { self.attackCooldown--; } }; self.shootArrow = function () { // Create projectile var arrow = game.addChild(new ArcherProjectile()); arrow.x = self.x; arrow.y = self.y - 100; // Calculate trajectory to hero var dx = hero.x - self.x; var dy = hero.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { var speed = 8; arrow.velocityX = dx / distance * speed; arrow.velocityY = dy / distance * speed; } bossProjectiles.push(arrow); // Reuse boss projectiles array self.shootCooldown = 180; // 3 seconds // Visual shooting effect tween(archerGraphics, { tint: 0xff4488, scaleX: archerGraphics.scaleX * 1.2, scaleY: 1.2 }, { duration: 200, onFinish: function onFinish() { tween(archerGraphics, { tint: 0x8844ff, scaleX: archerGraphics.scaleX > 0 ? 1 : -1, scaleY: 1 }, { duration: 200 }); } }); }; return self; }); var EnemyWarning = Container.expand(function () { var self = Container.call(this); self.targetEnemy = null; self.direction = 'left'; // 'left', 'right', 'up', 'down' self.warningGraphics = null; self.lastAlpha = 0; self.isVisible = false; self.setDirection = function (direction) { if (self.warningGraphics) { self.warningGraphics.destroy(); } var assetName = 'warningArrow' + direction.charAt(0).toUpperCase() + direction.slice(1); self.warningGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5, alpha: 0 }); self.direction = direction; }; self.update = function () { if (!self.targetEnemy || !self.warningGraphics) return; // Check if enemy is still alive var enemyExists = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i] === self.targetEnemy) { enemyExists = true; break; } } if (!enemyExists) { self.hide(); return; } // Calculate distance from hero to enemy var distanceToHero = Math.sqrt(Math.pow(self.targetEnemy.x - hero.x, 2) + Math.pow(self.targetEnemy.y - hero.y, 2)); // Check if enemy is visible on screen var enemyScreenX = self.targetEnemy.x - camera.x; var enemyScreenY = self.targetEnemy.y - camera.y; var isOnScreen = enemyScreenX >= -100 && enemyScreenX <= 2148 && enemyScreenY >= -100 && enemyScreenY <= 2832; if (isOnScreen) { self.hide(); return; } // Calculate warning opacity based on distance (closer = more visible) var maxDistance = 800; var minDistance = 300; var targetAlpha = 0; if (distanceToHero <= maxDistance) { var normalizedDistance = Math.max(0, Math.min(1, (maxDistance - distanceToHero) / (maxDistance - minDistance))); targetAlpha = normalizedDistance * 0.8; } // Smooth alpha transition if (Math.abs(targetAlpha - self.lastAlpha) > 0.01) { tween.stop(self.warningGraphics, { alpha: true }); tween(self.warningGraphics, { alpha: targetAlpha }, { duration: 200 }); self.lastAlpha = targetAlpha; } // Position warning at screen edge var screenCenterX = 1024; var screenCenterY = 1366; var dx = self.targetEnemy.x - hero.x; var dy = self.targetEnemy.y - hero.y; if (Math.abs(dx) > Math.abs(dy)) { // Horizontal warning if (dx > 0) { self.setDirection('right'); self.x = screenCenterX + 900; self.y = screenCenterY + Math.max(-600, Math.min(600, dy * 0.5)); } else { self.setDirection('left'); self.x = screenCenterX - 900; self.y = screenCenterY + Math.max(-600, Math.min(600, dy * 0.5)); } } else { // Vertical warning if (dy > 0) { self.setDirection('down'); self.x = screenCenterX + Math.max(-800, Math.min(800, dx * 0.5)); self.y = screenCenterY + 1200; } else { self.setDirection('up'); self.x = screenCenterX + Math.max(-800, Math.min(800, dx * 0.5)); self.y = screenCenterY - 1200; } } }; self.hide = function () { if (self.warningGraphics && self.warningGraphics.alpha > 0) { tween.stop(self.warningGraphics, { alpha: true }); tween(self.warningGraphics, { alpha: 0 }, { duration: 300 }); self.lastAlpha = 0; } }; return self; }); var HealthPotion = Container.expand(function () { var self = Container.call(this); var potionGraphics = self.attachAsset('healthPotion', { anchorX: 0.5, anchorY: 0.5 }); self.collectTimer = 0; self.update = function () { // Auto-collect after short delay self.collectTimer++; if (self.collectTimer > 30) { // 0.5 seconds self.collect(); } // Gentle floating animation potionGraphics.y = Math.sin(LK.ticks * 0.1) * 5; potionGraphics.rotation += 0.05; }; self.collect = function () { LK.getSound('powerup').play(); hero.heal(); // Remove from healthPotions array for (var i = healthPotions.length - 1; i >= 0; i--) { if (healthPotions[i] === self) { healthPotions.splice(i, 1); break; } } self.destroy(); }; return self; }); var Hero = Container.expand(function () { var self = Container.call(this); var heroGraphics = self.attachAsset('hero', { anchorX: 0.5, anchorY: 1.0 }); self.maxHealth = 5; self.health = self.maxHealth; self.isAttacking = false; self.invulnerable = false; self.damageBoost = false; self.comboCount = 0; self.slashCooldown = 0; self.isSlashing = false; self.slashGraphics = null; self.lastX = 0; self.lastY = 0; self.isWalking = false; self.walkAnimationActive = false; self.walkAnimationFrame = 0; self.walkAnimationTimer = 0; self.walkAnimationSpeed = 10; // frames between texture changes self.attack = function (targetX) { if (self.isAttacking || self.slashCooldown > 0) return; self.isAttacking = true; self.isSlashing = true; // Apply attack speed upgrade var baseSlashCooldown = 24; // 0.4 seconds at 60fps self.slashCooldown = Math.floor(baseSlashCooldown / (self.attackSpeedMultiplier || 1)); // Face direction of attack if (targetX < self.x) { heroGraphics.scaleX = -1; } else { heroGraphics.scaleX = 1; } // Replace hero graphic with slash animation self.removeChild(heroGraphics); self.slashGraphics = self.attachAsset('heroSlash', { anchorX: 0.5, anchorY: 1.0 }); // Match facing direction self.slashGraphics.scaleX = heroGraphics.scaleX; // Create weapon trail effect var trail = game.addChild(getFromPool('weaponTrail')); var trailType = self.damageBoost ? 'power' : 'normal'; trail.reset(self.x + heroGraphics.scaleX * 100, self.y - 80, heroGraphics.scaleX > 0 ? -0.3 : 0.3, 2, trailType); weaponTrails.push(trail); // Attack animation tween(self.slashGraphics, { scaleY: 1.2 }, { duration: 100 }); tween(self.slashGraphics, { scaleY: 1.0 }, { duration: 100, onFinish: function onFinish() { // Return to normal hero graphic self.removeChild(self.slashGraphics); heroGraphics = self.attachAsset('hero', { anchorX: 0.5, anchorY: 1.0 }); // Maintain facing direction heroGraphics.scaleX = self.slashGraphics.scaleX; self.isAttacking = false; self.isSlashing = false; self.slashGraphics = null; } }); // Create sword slash effect var effect = game.addChild(LK.getAsset('slashEffect', { anchorX: 0.5, anchorY: 0.5, alpha: 0.9 })); effect.x = self.x + heroGraphics.scaleX * 120; effect.y = self.y - 80; // Set slash rotation and scale based on direction effect.rotation = heroGraphics.scaleX > 0 ? -0.3 : 0.3; // Diagonal slash effect.scaleX = heroGraphics.scaleX > 0 ? 1 : -1; // Mirror for left attacks tween(effect, { scaleX: effect.scaleX * 4, scaleY: 4, alpha: 0 }, { duration: 150, onFinish: function onFinish() { effect.destroy(); } }); LK.getSound('slash').play(); }; self.takeDamage = function () { if (self.invulnerable) return; // Apply damage reduction var damageReduction = self.damageReduction || 0; if (Math.random() < damageReduction) { // Damage reduced! Show visual effect LK.effects.flashObject(self, 0x3498db, 300); return; } self.health--; self.comboCount = 0; // Flash red when hit LK.effects.flashObject(self, 0xff0000, 500); // Screen shake when player is hit triggerScreenShake(18, 400, tween.easeOut); // Temporary invulnerability with upgrade bonus self.invulnerable = true; var invulnerabilityDuration = 1000 + (self.invulnerabilityBonus || 0); LK.setTimeout(function () { self.invulnerable = false; }, invulnerabilityDuration); LK.getSound('hit').play(); updateHealthDisplay(); if (self.health <= 0) { LK.showGameOver(); } }; self.heal = function () { if (self.health < self.maxHealth) { self.health++; updateHealthDisplay(); } }; self.addCombo = function () { self.comboCount++; }; self.startWalkAnimation = function () { if (self.walkAnimationActive) return; self.walkAnimationActive = true; self.walkAnimationFrame = 0; self.walkAnimationTimer = 0; }; self.stopWalkAnimation = function () { self.isWalking = false; self.walkAnimationActive = false; // Store current scale direction before removing graphics var currentScaleX = heroGraphics.scaleX; // Reset to default hero texture self.removeChild(heroGraphics); heroGraphics = self.attachAsset('hero', { anchorX: 0.5, anchorY: 1.0 }); // Maintain the current scale direction if (currentScaleX < 0) { heroGraphics.scaleX = -1; } }; self.move = function (direction) { var speed = self.speed || 8; var newX = self.x; var newY = self.y; var didMove = false; if (direction === 'left' && self.x > 100) { newX = self.x - speed; heroGraphics.scaleX = -1; didMove = true; } else if (direction === 'right' && self.x < currentLevelData.width - 100) { newX = self.x + speed; heroGraphics.scaleX = 1; didMove = true; } else if (direction === 'up' && self.y > 1600) { newY = self.y - speed; didMove = true; } else if (direction === 'down' && self.y < 2400) { newY = self.y + speed; didMove = true; } // Check collision with enemies before moving using spatial partitioning var wouldCollide = false; var nearbyEnemies = spatialGrid.getNearbyObjects(newX, newY, 150); for (var i = 0; i < nearbyEnemies.length; i++) { var enemy = nearbyEnemies[i]; var dx = newX - enemy.x; var dy = newY - enemy.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 120) { // Increased collision threshold to prevent overlap wouldCollide = true; break; } } // Only move if no collision would occur if (!wouldCollide && didMove) { self.lastX = self.x; self.lastY = self.y; self.x = newX; self.y = newY; // Create dust clouds when moving (occasional) if (Math.random() < 0.3) { var dustCloud = game.addChild(getFromPool('dustCloud')); dustCloud.reset(self.x + (Math.random() - 0.5) * 30, self.y - 10 + (Math.random() - 0.5) * 20, direction); dustClouds.push(dustCloud); } // Start walking animation if (!self.isWalking) { self.isWalking = true; self.startWalkAnimation(); } // Update walking animation texture cycling if (self.walkAnimationActive) { self.walkAnimationTimer++; if (self.walkAnimationTimer >= self.walkAnimationSpeed) { self.walkAnimationTimer = 0; self.walkAnimationFrame = (self.walkAnimationFrame + 1) % 4; // Cycle through textures: hero, heroWalk1, heroWalk2, heroWalk3 var textureNames = ['hero', 'heroWalk1', 'heroWalk2', 'heroWalk3']; // Store current scale direction before removing graphics var currentScaleX = heroGraphics.scaleX; // Remove current graphics and add new one with correct texture self.removeChild(heroGraphics); heroGraphics = self.attachAsset(textureNames[self.walkAnimationFrame], { anchorX: 0.5, anchorY: 1.0 }); // Maintain the current scale direction if (currentScaleX < 0) { heroGraphics.scaleX = -1; } } } } // Update camera target camera.targetX = self.x - 1024; camera.targetY = self.y - 1366; // Clamp camera to level bounds camera.targetX = Math.max(0, Math.min(camera.targetX, currentLevelData.width - 2048)); camera.targetY = Math.max(0, Math.min(camera.targetY, currentLevelData.height - 2732)); }; self.update = function () { if (self.slashCooldown > 0) { self.slashCooldown--; } }; return self; }); var ImpactSpark = Container.expand(function () { var self = Container.call(this); var sparkGraphics = self.attachAsset('bloodParticle', { anchorX: 0.5, anchorY: 0.5 }); sparkGraphics.tint = 0xffff44; // Yellow-white sparks sparkGraphics.scaleX = 0.3; sparkGraphics.scaleY = 0.3; self.velocityX = 0; self.velocityY = 0; self.lifetime = 20; // Short-lived sparks self.sparkType = 'normal'; // normal, bright, orange self.reset = function (startX, startY, type) { self.x = startX; self.y = startY; self.sparkType = type || 'normal'; // Random direction and speed for sparks var angle = Math.random() * Math.PI * 2; var speed = 3 + Math.random() * 5; self.velocityX = Math.cos(angle) * speed; self.velocityY = Math.sin(angle) * speed; if (self.sparkType === 'bright') { sparkGraphics.tint = 0xffffff; // White sparks sparkGraphics.scaleX = 0.4; sparkGraphics.scaleY = 0.4; self.lifetime = 30; } else if (self.sparkType === 'orange') { sparkGraphics.tint = 0xff8844; // Orange sparks sparkGraphics.scaleX = 0.2; sparkGraphics.scaleY = 0.2; self.lifetime = 15; } else { sparkGraphics.tint = 0xffff44; // Yellow sparks sparkGraphics.scaleX = 0.3; sparkGraphics.scaleY = 0.3; self.lifetime = 20; } sparkGraphics.alpha = 1; self.visible = true; }; self.update = function () { self.x += self.velocityX; self.y += self.velocityY; // Sparks slow down over time self.velocityX *= 0.95; self.velocityY *= 0.95; // Fade out quickly self.lifetime--; sparkGraphics.alpha = self.lifetime / 20; if (self.lifetime <= 0) { // Remove from impactSparks array for (var i = impactSparks.length - 1; i >= 0; i--) { if (impactSparks[i] === self) { impactSparks.splice(i, 1); break; } } returnToPool(self, 'impactSpark'); } }; return self; }); var Knife = Container.expand(function () { var self = Container.call(this); var knifeGraphics = self.attachAsset('knife', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 15; self.direction = 1; // 1 for right, -1 for left self.routePoints = []; // Points to follow along the route self.currentRouteIndex = 0; // Current target point index self.velocityX = 0; // Velocity for precise targeting self.velocityY = 0; // Velocity for precise targeting self.lastX = 0; self.lastY = 0; self.setRoute = function (routePoints) { self.routePoints = routePoints; self.currentRouteIndex = 0; }; self.update = function () { self.lastX = self.x; self.lastY = self.y; // Follow route if available if (self.routePoints.length > 0 && self.currentRouteIndex < self.routePoints.length) { var targetPoint = self.routePoints[self.currentRouteIndex]; var dx = targetPoint.x - self.x; var dy = targetPoint.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 20) { // Close enough to current target, move to next point self.currentRouteIndex++; } else { // Move toward current target point if (distance > 0) { self.x += dx / distance * self.speed; self.y += dy / distance * self.speed; // Calculate rotation angle to face target direction var angle = Math.atan2(dy, dx); knifeGraphics.rotation = angle; } } } else if (self.velocityX !== 0 || self.velocityY !== 0) { // Use velocity if set, otherwise use direction-based movement self.x += self.velocityX; self.y += self.velocityY; // Calculate rotation for velocity-based movement var angle = Math.atan2(self.velocityY, self.velocityX); knifeGraphics.rotation = angle; } else { self.x += self.speed * self.direction; self.y -= 2; // Slight upward arc // Calculate rotation for direction-based movement var angle = Math.atan2(-2, self.speed * self.direction); knifeGraphics.rotation = angle; } // Check if knife went off screen if (self.x < -100 || self.x > currentLevelData.width + 100 || self.y < -100 || self.y > 2900) { self.destroy(); // Remove from knives array for (var i = knives.length - 1; i >= 0; i--) { if (knives[i] === self) { knives.splice(i, 1); break; } } } // Check collision with enemies (hit enemy center) var penetration = upgradeTree ? upgradeTree.getKnifePenetration() : 0; var enemiesHit = 0; var maxEnemiesHit = 1 + penetration; for (var i = 0; i < enemies.length && enemiesHit < maxEnemiesHit; i++) { var enemy = enemies[i]; var dx = self.x - enemy.x; var dy = self.y - (enemy.y - 70); // Target enemy center var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 50) { // Hit enemy center - deal damage with upgrade multiplier var damage = Math.floor(hero.damageMultiplier || 1); for (var d = 0; d < damage; d++) { enemy.takeDamage(true); } // Light screen shake on knife impact triggerScreenShake(4, 100, tween.easeOut); enemiesHit++; // If no penetration or hit max enemies, destroy knife if (penetration === 0 || enemiesHit >= maxEnemiesHit) { self.destroy(); // Remove from knives array for (var j = knives.length - 1; j >= 0; j--) { if (knives[j] === self) { knives.splice(j, 1); break; } } return; // Exit update immediately to prevent further movement or collision checks } } } }; return self; }); var MagicalEffect = Container.expand(function () { var self = Container.call(this); var effectGraphics = self.attachAsset('bloodParticle', { anchorX: 0.5, anchorY: 0.5 }); self.velocityX = 0; self.velocityY = 0; self.lifetime = 60; self.effectType = 'sparkle'; // sparkle, healing, power, shield self.rotationSpeed = 0; self.reset = function (startX, startY, type) { self.x = startX; self.y = startY; self.effectType = type || 'sparkle'; if (self.effectType === 'sparkle') { effectGraphics.tint = 0xffff88; // Golden sparkle effectGraphics.scaleX = 0.6; effectGraphics.scaleY = 0.6; var angle = Math.random() * Math.PI * 2; var speed = 2 + Math.random() * 3; self.velocityX = Math.cos(angle) * speed; self.velocityY = Math.sin(angle) * speed - 2; // Float upward self.rotationSpeed = (Math.random() - 0.5) * 0.2; self.lifetime = 80; } else if (self.effectType === 'healing') { effectGraphics.tint = 0x44ff44; // Green healing effectGraphics.scaleX = 0.8; effectGraphics.scaleY = 0.8; self.velocityX = (Math.random() - 0.5) * 2; self.velocityY = -3 - Math.random() * 2; // Rise upward self.rotationSpeed = 0.1; self.lifetime = 60; } else if (self.effectType === 'power') { effectGraphics.tint = 0xff4488; // Purple power effectGraphics.scaleX = 1.0; effectGraphics.scaleY = 1.0; self.velocityX = (Math.random() - 0.5) * 4; self.velocityY = -1 - Math.random() * 3; self.rotationSpeed = 0.15; self.lifetime = 70; } else if (self.effectType === 'shield') { effectGraphics.tint = 0x44ddff; // Blue shield effectGraphics.scaleX = 0.5; effectGraphics.scaleY = 0.5; var angle = Math.random() * Math.PI * 2; var radius = 50 + Math.random() * 30; self.velocityX = Math.cos(angle) * 2; self.velocityY = Math.sin(angle) * 2; self.rotationSpeed = 0.08; self.lifetime = 90; } effectGraphics.alpha = 1; self.visible = true; }; self.update = function () { self.x += self.velocityX; self.y += self.velocityY; effectGraphics.rotation += self.rotationSpeed; // Different behaviors per effect type if (self.effectType === 'sparkle') { self.velocityY -= 0.05; // Continue floating up } else if (self.effectType === 'healing') { self.velocityX *= 0.99; // Slow down horizontally } else if (self.effectType === 'power') { // Pulsing effect var pulseScale = 1.0 + Math.sin(LK.ticks * 0.3) * 0.2; effectGraphics.scaleX = pulseScale; effectGraphics.scaleY = pulseScale; } // Fade out over time self.lifetime--; var maxLifetime = 60; if (self.effectType === 'sparkle') maxLifetime = 80;else if (self.effectType === 'power') maxLifetime = 70;else if (self.effectType === 'shield') maxLifetime = 90; effectGraphics.alpha = self.lifetime / maxLifetime; if (self.lifetime <= 0) { // Remove from magicalEffects array for (var i = magicalEffects.length - 1; i >= 0; i--) { if (magicalEffects[i] === self) { magicalEffects.splice(i, 1); break; } } returnToPool(self, 'magicalEffect'); } }; return self; }); var Minion = Container.expand(function () { var self = Container.call(this); var minionGraphics = self.attachAsset('enemyFast', { anchorX: 0.5, anchorY: 1.0 }); minionGraphics.tint = 0x9b59b6; // Purple tint for minions minionGraphics.scaleX = 0.8; // Smaller than normal enemies minionGraphics.scaleY = 0.8; self.health = 1; self.speed = 3.5; self.attackCooldown = 0; self.lastX = 0; self.lastY = 0; self.summoner = null; // Reference to summoning boss self.lifetime = 1800; // 30 seconds lifetime self.update = function () { self.lifetime--; if (self.lifetime <= 0) { self.die(); return; } var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2)); // Aggressive AI - always chase hero var dx = hero.x - self.x; var dy = hero.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { // Check collision with other enemies before moving var wouldCollide = false; var newX = self.x + dx / distance * self.speed; var newY = self.y + dy / distance * self.speed; var nearbyEnemies = spatialGrid.getNearbyObjects(newX, newY, 100); for (var i = 0; i < nearbyEnemies.length; i++) { var otherEnemy = nearbyEnemies[i]; if (otherEnemy === self) continue; var edx = newX - otherEnemy.x; var edy = newY - otherEnemy.y; var edistance = Math.sqrt(edx * edx + edy * edy); if (edistance < 60) { // Smaller collision radius for minions wouldCollide = true; break; } } if (!wouldCollide) { self.lastX = self.x; self.lastY = self.y; self.x = newX; self.y = newY; } minionGraphics.scaleX = dx > 0 ? 0.8 : -0.8; } // Attack hero if close enough if (distanceToHero < 80 && self.attackCooldown <= 0) { hero.takeDamage(); self.attackCooldown = 90; // 1.5 seconds } if (self.attackCooldown > 0) { self.attackCooldown--; } }; self.takeDamage = function (fromKnife) { self.health--; LK.effects.flashObject(self, 0xffffff, 200); // Create smaller blood particles for (var p = 0; p < 8; p++) { var bloodParticle = game.addChild(getFromPool('bloodParticle')); bloodParticle.reset(self.x + (Math.random() - 0.5) * 30, self.y - 40 + (Math.random() - 0.5) * 30); bloodParticles.push(bloodParticle); } if (self.health <= 0) { self.die(); } }; self.die = function () { // Minions give 2 coins upgradeTree.addCoins(2); // Drop small coin var coin = game.addChild(new Coin()); coin.x = self.x; coin.y = self.y; var coinGraphics = coin.getChildAt(0); coinGraphics.scaleX = 0.7; // Smaller coin coinGraphics.scaleY = 0.7; coins.push(coin); // Reduce summoner's minion count if (self.summoner && self.summoner.minionCount) { self.summoner.minionCount--; } // Small score bonus LK.setScore(LK.getScore() + 5); hero.addCombo(); // Remove from enemies array for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } self.destroy(); updateScoreDisplay(); updateEnemiesLeftDisplay(); }; return self; }); var PowerUp = Container.expand(function () { var self = Container.call(this); var powerupGraphics = self.attachAsset('powerup', { anchorX: 0.5, anchorY: 0.5 }); self.type = 'health'; // 'health', 'invulnerable', 'damage' self.lifetime = 600; // 10 seconds self.update = function () { self.lifetime--; if (self.lifetime <= 0) { self.expire(); } // Check collision with hero if (self.intersects(hero)) { self.collect(); } // Pulse animation var scale = 1 + Math.sin(LK.ticks * 0.2) * 0.2; powerupGraphics.scaleX = scale; powerupGraphics.scaleY = scale; }; self.collect = function () { LK.getSound('powerup').play(); // Create magical effects based on power-up type var effectType = 'sparkle'; if (self.type === 'health') { hero.heal(); effectType = 'healing'; } else if (self.type === 'invulnerable') { hero.invulnerable = true; effectType = 'shield'; LK.setTimeout(function () { hero.invulnerable = false; }, 5000); } else if (self.type === 'damage') { hero.damageBoost = true; effectType = 'power'; LK.setTimeout(function () { hero.damageBoost = false; }, 5000); } // Create magical particle effects for (var e = 0; e < 12; e++) { var magicalEffect = game.addChild(getFromPool('magicalEffect')); magicalEffect.reset(self.x + (Math.random() - 0.5) * 60, self.y + (Math.random() - 0.5) * 60, effectType); magicalEffects.push(magicalEffect); } // Remove from powerups array for (var i = powerups.length - 1; i >= 0; i--) { if (powerups[i] === self) { powerups.splice(i, 1); break; } } self.destroy(); }; self.expire = function () { // Remove from powerups array for (var i = powerups.length - 1; i >= 0; i--) { if (powerups[i] === self) { powerups.splice(i, 1); break; } } self.destroy(); }; return self; }); var RouteEffect = Container.expand(function () { var self = Container.call(this); self.routePoints = []; self.routePositions = []; // Store just the x,y coordinates for knife to follow self.targetEnemy = null; self.createRoute = function (enemy) { self.targetEnemy = enemy; self.clearRoute(); // Calculate direct path from hero center to enemy center var heroStartX = hero.x; var heroStartY = hero.y - 80; // Middle of hero asset var enemyMiddleX = enemy.x; var enemyMiddleY = enemy.y - 70; // Middle of enemy asset (enemy height is 140, so middle is -70 from bottom) var dx = enemyMiddleX - heroStartX; var dy = enemyMiddleY - heroStartY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { // Create route points along the path var numPoints = Math.floor(distance / 50); // Point every 50 pixels for (var i = 0; i <= numPoints; i++) { var t = i / numPoints; var x = heroStartX + dx * t; var y = heroStartY + dy * t; var routePoint = self.addChild(LK.getAsset('routeLine', { anchorX: 0.5, anchorY: 0.5, alpha: 0 })); routePoint.x = x; routePoint.y = y; self.routePoints.push(routePoint); // Store position for knife to follow self.routePositions.push({ x: x, y: y }); // Animate route points appearing with delay tween(routePoint, { alpha: 0.8, scaleX: 2, scaleY: 2 }, { duration: 100 + i * 20 }); } // Add impact effect at enemy position var impactEffect = self.addChild(LK.getAsset('routeEffect', { anchorX: 0.5, anchorY: 0.5, alpha: 0, scaleX: 0.5, scaleY: 0.5 })); impactEffect.x = enemyMiddleX; impactEffect.y = enemyMiddleY; self.routePoints.push(impactEffect); // Animate impact effect tween(impactEffect, { alpha: 1, scaleX: 3, scaleY: 3 }, { duration: 300, easing: tween.easeOut }); // Remove route after 0.5 seconds LK.setTimeout(function () { self.destroy(); // Remove from routeEffects array for (var i = routeEffects.length - 1; i >= 0; i--) { if (routeEffects[i] === self) { routeEffects.splice(i, 1); break; } } }, 500); } }; self.clearRoute = function () { for (var i = self.routePoints.length - 1; i >= 0; i--) { self.routePoints[i].destroy(); } self.routePoints = []; }; self.fadeOut = function () { for (var i = 0; i < self.routePoints.length; i++) { tween(self.routePoints[i], { alpha: 0, scaleX: 0.1, scaleY: 0.1 }, { duration: 200 }); } LK.setTimeout(function () { self.destroy(); }, 300); }; return self; }); var ScreenShake = Container.expand(function () { var self = Container.call(this); self.intensity = 0; self.duration = 0; self.remainingTime = 0; self.offsetX = 0; self.offsetY = 0; self.isActive = false; self.decayEasing = tween.easeOut; self.shake = function (intensity, duration, easing) { self.intensity = intensity || 10; self.duration = duration || 500; self.remainingTime = self.duration; self.isActive = true; self.decayEasing = easing || tween.easeOut; }; self.update = function () { if (!self.isActive) { self.offsetX = 0; self.offsetY = 0; return; } self.remainingTime -= 16.67; // Approximately 1 frame at 60fps if (self.remainingTime <= 0) { self.isActive = false; self.offsetX = 0; self.offsetY = 0; return; } // Calculate decay factor using easing function var progress = 1 - self.remainingTime / self.duration; var decayFactor = 1 - self.decayEasing(progress); // Generate random shake offset var currentIntensity = self.intensity * decayFactor; self.offsetX = (Math.random() - 0.5) * 2 * currentIntensity; self.offsetY = (Math.random() - 0.5) * 2 * currentIntensity; }; self.getOffsetX = function () { return self.offsetX; }; self.getOffsetY = function () { return self.offsetY; }; return self; }); var UpgradeMenu = Container.expand(function () { var self = Container.call(this); self.isVisible = false; self.upgradeTree = null; self.upgradeButtons = []; self.categoryHeaders = []; self.background = null; self.show = function (upgradeTree) { self.upgradeTree = upgradeTree; self.isVisible = true; self.x = 0; self.y = 0; self.createInterface(); // Animate menu appearance self.alpha = 0; self.scaleX = 0.8; self.scaleY = 0.8; tween(self, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.easeOut }); }; self.hide = function () { tween(self, { alpha: 0, scaleX: 0.8, scaleY: 0.8 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { self.isVisible = false; self.clearInterface(); } }); }; self.createInterface = function () { // Create background centered on screen self.background = self.addChild(LK.getAsset('backgroundTile', { anchorX: 0.5, anchorY: 0.5, tint: 0x2c3e50, alpha: 0.95, scaleX: 10, scaleY: 12 })); self.background.x = 0; self.background.y = 0; // Title var title = new Text2('UPGRADE TREE', { size: 100, fill: '#FFD700' }); title.anchor.set(0.5, 0.5); title.x = 0; title.y = -1000; self.addChild(title); // Coins display var coinsText = new Text2('Coins: ' + self.upgradeTree.coins, { size: 60, fill: '#F1C40F' }); coinsText.anchor.set(0.5, 0.5); coinsText.x = 0; coinsText.y = -900; self.addChild(coinsText); self.coinsText = coinsText; // Combat category self.createCategory('COMBAT', -600, [{ type: 'damage', name: 'Damage Boost', desc: '+20% damage per level' }, { type: 'attackSpeed', name: 'Attack Speed', desc: '+25% attack speed per level' }, { type: 'knifePenetration', name: 'Knife Penetration', desc: 'Knives hit multiple enemies' }]); // Defense category self.createCategory('DEFENSE', -100, [{ type: 'maxHealth', name: 'Max Health', desc: '+1 max health per level' }, { type: 'damageReduction', name: 'Damage Reduction', desc: '+15% damage reduction per level' }, { type: 'invulnerability', name: 'Invulnerability', desc: '+1 second invulnerability per level' }]); // Utility category self.createCategory('UTILITY', 400, [{ type: 'moveSpeed', name: 'Move Speed', desc: '+25% movement speed per level' }, { type: 'startingKnives', name: 'Starting Knives', desc: '+2 starting knives per level' }, { type: 'coinMagnetism', name: 'Coin Magnetism', desc: 'Increased coin collection range' }]); // Close button var closeButton = self.addChild(LK.getAsset('backgroundTile', { anchorX: 0.5, anchorY: 0.5, tint: 0xe74c3c, scaleX: 2, scaleY: 1 })); closeButton.x = 0; closeButton.y = 1000; var closeText = new Text2('CLOSE', { size: 60, fill: '#FFFFFF' }); closeText.anchor.set(0.5, 0.5); closeText.x = 0; closeText.y = 1000; self.addChild(closeText); closeButton.down = function () { self.hide(); }; }; self.createCategory = function (categoryName, startY, upgrades) { // Category header var header = new Text2(categoryName, { size: 80, fill: '#3498db' }); header.anchor.set(0.5, 0.5); header.x = 0; header.y = startY; self.addChild(header); // Upgrade buttons for (var i = 0; i < upgrades.length; i++) { var upgrade = upgrades[i]; var buttonY = startY + 100 + i * 120; self.createUpgradeButton(upgrade, buttonY); } }; self.createUpgradeButton = function (upgrade, y) { var currentLevel = self.upgradeTree.upgrades[upgrade.type]; var maxLevel = self.upgradeTree.upgradeCosts[upgrade.type].length; var canUpgrade = self.upgradeTree.canUpgrade(upgrade.type); var cost = currentLevel < maxLevel ? self.upgradeTree.upgradeCosts[upgrade.type][currentLevel] : 0; // Button background var buttonColor = currentLevel >= maxLevel ? 0x27ae60 : canUpgrade ? 0x3498db : 0x7f8c8d; var button = self.addChild(LK.getAsset('backgroundTile', { anchorX: 0.5, anchorY: 0.5, tint: buttonColor, scaleX: 8, scaleY: 0.8 })); button.x = 0; button.y = y; // Upgrade name var nameText = new Text2(upgrade.name, { size: 50, fill: '#FFFFFF' }); nameText.anchor.set(0.5, 0.5); nameText.x = -424; nameText.y = y - 15; self.addChild(nameText); // Level display var levelText = new Text2('Level: ' + currentLevel + '/' + maxLevel, { size: 40, fill: '#BDC3C7' }); levelText.anchor.set(0.5, 0.5); levelText.x = -424; levelText.y = y + 15; self.addChild(levelText); // Cost display if (currentLevel < maxLevel) { var costText = new Text2('Cost: ' + cost, { size: 40, fill: canUpgrade ? '#F1C40F' : '#E74C3C' }); costText.anchor.set(0.5, 0.5); costText.x = 376; costText.y = y; self.addChild(costText); } else { var maxText = new Text2('MAX', { size: 40, fill: '#27AE60' }); maxText.anchor.set(0.5, 0.5); maxText.x = 376; maxText.y = y; self.addChild(maxText); } // Purchase functionality if (canUpgrade) { button.down = function () { if (self.upgradeTree.purchaseUpgrade(upgrade.type)) { // Refresh interface self.clearInterface(); self.createInterface(); // Success effect tween(button, { tint: 0x27ae60, scaleX: 9, scaleY: 0.9 }, { duration: 200, onFinish: function onFinish() { tween(button, { tint: buttonColor, scaleX: 8, scaleY: 0.8 }, { duration: 200 }); } }); } }; } }; self.clearInterface = function () { // Remove all children while (self.children.length > 0) { self.children[0].destroy(); } self.upgradeButtons = []; self.categoryHeaders = []; self.background = null; }; return self; }); var UpgradeTree = Container.expand(function () { var self = Container.call(this); // Initialize upgrade data from storage self.upgrades = storage.upgrades || { // Combat upgrades damage: 0, // 0-5: +20% damage per level attackSpeed: 0, // 0-3: +25% attack speed per level knifePenetration: 0, // 0-3: knives can hit multiple enemies // Defense upgrades maxHealth: 0, // 0-5: +1 max health per level damageReduction: 0, // 0-3: +15% damage reduction per level invulnerability: 0, // 0-2: +1 second invulnerability per level // Utility upgrades moveSpeed: 0, // 0-3: +25% movement speed per level startingKnives: 0, // 0-4: +2 starting knives per level coinMagnetism: 0 // 0-3: increased coin collection range per level }; // Upgrade costs (coins required) self.upgradeCosts = { damage: [100, 200, 400, 800, 1600], attackSpeed: [150, 300, 600], knifePenetration: [200, 500, 1000], maxHealth: [80, 160, 320, 640, 1280], damageReduction: [120, 240, 480], invulnerability: [300, 600], moveSpeed: [100, 200, 400], startingKnives: [150, 300, 600, 1200], coinMagnetism: [100, 250, 500] }; self.coins = storage.coins || 0; self.canUpgrade = function (upgradeType) { var currentLevel = self.upgrades[upgradeType]; var maxLevel = self.upgradeCosts[upgradeType].length; if (currentLevel >= maxLevel) return false; return self.coins >= self.upgradeCosts[upgradeType][currentLevel]; }; self.purchaseUpgrade = function (upgradeType) { if (!self.canUpgrade(upgradeType)) return false; var currentLevel = self.upgrades[upgradeType]; var cost = self.upgradeCosts[upgradeType][currentLevel]; self.coins -= cost; self.upgrades[upgradeType]++; // Save to storage storage.upgrades = self.upgrades; storage.coins = self.coins; return true; }; self.addCoins = function (amount) { self.coins += amount; storage.coins = self.coins; }; // Get upgrade bonuses self.getDamageMultiplier = function () { return 1 + self.upgrades.damage * 0.2; }; self.getAttackSpeedMultiplier = function () { return 1 + self.upgrades.attackSpeed * 0.25; }; self.getKnifePenetration = function () { return self.upgrades.knifePenetration; }; self.getMaxHealthBonus = function () { return self.upgrades.maxHealth; }; self.getDamageReduction = function () { return self.upgrades.damageReduction * 0.15; }; self.getInvulnerabilityBonus = function () { return self.upgrades.invulnerability * 1000; // milliseconds }; self.getMoveSpeedMultiplier = function () { return 1 + self.upgrades.moveSpeed * 0.25; }; self.getStartingKnivesBonus = function () { return self.upgrades.startingKnives * 2; }; self.getCoinMagnetismRange = function () { return 100 + self.upgrades.coinMagnetism * 50; }; return self; }); var WeaponTrail = Container.expand(function () { var self = Container.call(this); var trailGraphics = self.attachAsset('slashEffect', { anchorX: 0.5, anchorY: 0.5, alpha: 0.6, tint: 0xFFFFFF }); self.lifetime = 30; // Short trail life self.maxLifetime = 30; self.reset = function (startX, startY, rotation, scale, trailType) { self.x = startX; self.y = startY; self.lifetime = self.maxLifetime; trailGraphics.rotation = rotation; trailGraphics.scaleX = scale; trailGraphics.scaleY = scale * 0.5; // Flatter trail trailGraphics.alpha = 0.6; self.visible = true; // Different trail effects based to type if (trailType === 'critical') { trailGraphics.tint = 0xFFD700; // Golden trail for critical hits trailGraphics.scaleY = scale * 0.8; // Thicker trail } else if (trailType === 'power') { trailGraphics.tint = 0xFF4488; // Purple trail for powered attacks } else { trailGraphics.tint = 0xFFFFFF; // White trail for normal } }; self.update = function () { self.lifetime--; // Fade and shrink over time var lifeRatio = self.lifetime / self.maxLifetime; trailGraphics.alpha = 0.6 * lifeRatio; trailGraphics.scaleX *= 0.98; trailGraphics.scaleY *= 0.96; if (self.lifetime <= 0) { // Remove from weaponTrails array for (var i = weaponTrails.length - 1; i >= 0; i--) { if (weaponTrails[i] === self) { weaponTrails.splice(i, 1); break; } } returnToPool(self, 'weaponTrail'); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x2c3e50, title: 'Dungeon Crawler' }); /**** * Game Code ****/ // Legacy Boss class for backward compatibility (delegates to BossType1) // Game variables var Boss = BossType1; // Upgrade system var upgradeTree = new UpgradeTree(); var upgradeMenu = new UpgradeMenu(); var showUpgradesButton = null; // Visual effect functions function createGlowOutline(target, color, intensity) { var outline = target.addChild(LK.getAsset('slashEffect', { anchorX: 0.5, anchorY: 0.5, alpha: 0, tint: color, scaleX: 1.2, scaleY: 1.5 })); outline.x = 0; outline.y = -70; // Center on enemy target.glowOutline = outline; tween(outline, { alpha: intensity * 0.4, scaleX: 1.4, scaleY: 1.8 }, { duration: 300, easing: tween.easeInOut }); // Pulse effect function pulseGlow() { if (outline.parent) { tween(outline, { alpha: intensity * 0.2 }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { if (outline.parent) { tween(outline, { alpha: intensity * 0.4 }, { duration: 500, easing: tween.easeInOut, onFinish: pulseGlow }); } } }); } } pulseGlow(); } function removeGlowOutline(target) { if (target.glowOutline) { tween(target.glowOutline, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { if (target.glowOutline) { target.glowOutline.destroy(); target.glowOutline = null; } } }); } } function createScreenFlash(color, duration, intensity) { var flashOverlay = LK.gui.center.addChild(LK.getAsset('backgroundTile', { anchorX: 0.5, anchorY: 0.5, alpha: 0, tint: color, scaleX: 20, scaleY: 20 })); flashOverlay.x = 0; flashOverlay.y = 0; tween(flashOverlay, { alpha: intensity || 0.3 }, { duration: duration * 0.2, easing: tween.easeIn, onFinish: function onFinish() { tween(flashOverlay, { alpha: 0 }, { duration: duration * 0.8, easing: tween.easeOut, onFinish: function onFinish() { flashOverlay.destroy(); } }); } }); } var screenShake = new ScreenShake(); // Helper function to trigger screen shake with different intensities function triggerScreenShake(intensity, duration, easing) { screenShake.shake(intensity, duration, easing); } var hero; var enemies = []; var coins = []; var powerups = []; var enemyWarnings = []; var knives = []; var knivesRemaining = 5; var routeEffects = []; var bloodParticles = []; var bossProjectiles = []; var impactSparks = []; var dustClouds = []; var magicalEffects = []; var currentDungeon = 1; var dungeonComplete = false; var hearts = []; var healthPotions = []; var damageNumbers = []; var weaponTrails = []; // Object pooling system var objectPools = { bloodParticle: [], bossProjectile: [], coin: [], healthPotion: [], impactSpark: [], dustCloud: [], magicalEffect: [], damageNumber: [], weaponTrail: [] }; // Spatial partitioning system var spatialGrid = { cellSize: 200, grid: {}, clear: function clear() { this.grid = {}; }, getCellKey: function getCellKey(x, y) { var cellX = Math.floor(x / this.cellSize); var cellY = Math.floor(y / this.cellSize); return cellX + ',' + cellY; }, addObject: function addObject(obj, x, y) { var key = this.getCellKey(x, y); if (!this.grid[key]) { this.grid[key] = []; } this.grid[key].push(obj); }, getNearbyObjects: function getNearbyObjects(x, y, radius) { var nearby = []; var cellRadius = Math.ceil(radius / this.cellSize); var centerCellX = Math.floor(x / this.cellSize); var centerCellY = Math.floor(y / this.cellSize); for (var dx = -cellRadius; dx <= cellRadius; dx++) { for (var dy = -cellRadius; dy <= cellRadius; dy++) { var key = centerCellX + dx + ',' + (centerCellY + dy); if (this.grid[key]) { nearby = nearby.concat(this.grid[key]); } } } return nearby; } }; // Group AI communication system var groupAI = { communications: [], // Store recent communications alertRadius: 400, // Radius for alert propagation flankingCoordination: [], // Store flanking coordination data leaderBuffs: [], // Store active leader buffs packHunters: [], // Track pack hunting groups // Communicate alert state between enemies broadcastAlert: function broadcastAlert(enemy, alertType, priority) { var communication = { source: enemy, type: alertType, // 'spotted_hero', 'under_attack', 'flanking_position', 'retreat' priority: priority || 1, timestamp: LK.ticks, x: enemy.x, y: enemy.y }; this.communications.push(communication); // Clean old communications (older than 3 seconds) for (var i = this.communications.length - 1; i >= 0; i--) { if (LK.ticks - this.communications[i].timestamp > 180) { this.communications.splice(i, 1); } } }, // Get recent communications within range getCommunications: function getCommunications(enemy, maxAge, maxDistance) { var relevant = []; maxAge = maxAge || 120; // 2 seconds default maxDistance = maxDistance || this.alertRadius; for (var i = 0; i < this.communications.length; i++) { var comm = this.communications[i]; if (LK.ticks - comm.timestamp <= maxAge && comm.source !== enemy) { var distance = Math.sqrt(Math.pow(enemy.x - comm.x, 2) + Math.pow(enemy.y - comm.y, 2)); if (distance <= maxDistance) { relevant.push(comm); } } } return relevant; }, // Calculate optimal flanking positions calculateFlankingPositions: function calculateFlankingPositions(heroX, heroY) { var positions = []; var angles = [Math.PI * 0.25, Math.PI * 0.75, Math.PI * 1.25, Math.PI * 1.75]; // 45, 135, 225, 315 degrees var distance = 300; for (var i = 0; i < angles.length; i++) { var x = heroX + Math.cos(angles[i]) * distance; var y = heroY + Math.sin(angles[i]) * distance; // Keep within bounds x = Math.max(100, Math.min(x, currentLevelData.width - 100)); y = Math.max(1600, Math.min(y, 2400)); positions.push({ x: x, y: y, angle: angles[i], occupied: false }); } return positions; }, // Assign flanking position to enemy assignFlankingPosition: function assignFlankingPosition(enemy) { var positions = this.calculateFlankingPositions(hero.x, hero.y); var assigned = null; // Find closest unoccupied position var minDistance = Infinity; for (var i = 0; i < positions.length; i++) { if (!positions[i].occupied) { var distance = Math.sqrt(Math.pow(enemy.x - positions[i].x, 2) + Math.pow(enemy.y - positions[i].y, 2)); if (distance < minDistance) { minDistance = distance; assigned = positions[i]; } } } if (assigned) { assigned.occupied = true; enemy.flankingTarget = { x: assigned.x, y: assigned.y }; enemy.isAssignedFlanker = true; } } }; function getFromPool(type) { if (objectPools[type] && objectPools[type].length > 0) { var obj = objectPools[type].pop(); obj.visible = true; return obj; } // Create new object if pool is empty if (type === 'bloodParticle') { return new BloodParticle(); } else if (type === 'bossProjectile') { return new BossProjectile(); } else if (type === 'coin') { return new Coin(); } else if (type === 'healthPotion') { return new HealthPotion(); } else if (type === 'impactSpark') { return new ImpactSpark(); } else if (type === 'dustCloud') { return new DustCloud(); } else if (type === 'magicalEffect') { return new MagicalEffect(); } else if (type === 'damageNumber') { return new DamageNumber(); } else if (type === 'weaponTrail') { return new WeaponTrail(); } return null; } function returnToPool(obj, type) { if (!objectPools[type]) { objectPools[type] = []; } obj.visible = false; obj.x = -1000; // Move off screen obj.y = -1000; objectPools[type].push(obj); } // Movement state tracking var movementState = { left: false, right: false, up: false, down: false }; // Ensure movement state is properly initialized function resetMovementState() { movementState.left = false; movementState.right = false; movementState.up = false; movementState.down = false; } // Initialize clean movement state resetMovementState(); // Camera system var camera = { x: 0, y: 0, targetX: 0, targetY: 0, smoothing: 0.1 }; // Dungeon configuration var levels = [{ enemies: [{ type: 'basic', count: 3 }, { type: 'scout', count: 1 }], width: 4096, height: 2732 }, { enemies: [{ type: 'basic', count: 4 }, { type: 'strong', count: 2 }, { type: 'scout', count: 1 }, { type: 'archer', count: 1 }], width: 5120, height: 2732 }, { enemies: [{ type: 'basic', count: 5 }, { type: 'strong', count: 2 }, { type: 'fast', count: 2 }, { type: 'scout', count: 2 }, { type: 'archer', count: 1 }, { type: 'berserker', count: 1 }], width: 6144, height: 2732 }, { enemies: [{ type: 'basic', count: 4 }, { type: 'strong', count: 3 }, { type: 'fast', count: 2 }, { type: 'tank', count: 1 }, { type: 'scout', count: 2 }, { type: 'archer', count: 2 }, { type: 'berserker', count: 1 }, { type: 'shield', count: 1 }], width: 7168, height: 2732 }, { enemies: [{ type: 'basic', count: 6 }, { type: 'strong', count: 3 }, { type: 'fast', count: 3 }, { type: 'tank', count: 2 }, { type: 'hunter', count: 2 }, { type: 'scout', count: 3 }, { type: 'archer', count: 2 }, { type: 'berserker', count: 2 }, { type: 'shield', count: 1 }], width: 8192, height: 2732 }, { enemies: [{ type: 'boss1', count: 1 }], width: 9216, height: 2732 }, { enemies: [{ type: 'basic', count: 4 }, { type: 'strong', count: 2 }, { type: 'fast', count: 3 }, { type: 'tank', count: 2 }, { type: 'hunter', count: 2 }, { type: 'assassin', count: 2 }, { type: 'leader', count: 1 }, { type: 'scout', count: 2 }, { type: 'archer', count: 3 }, { type: 'berserker', count: 2 }, { type: 'shield', count: 2 }, { type: 'boss2', count: 1 }], width: 10240, height: 2732 }, { enemies: [{ type: 'basic', count: 3 }, { type: 'strong', count: 2 }, { type: 'fast', count: 3 }, { type: 'tank', count: 2 }, { type: 'hunter', count: 3 }, { type: 'assassin', count: 3 }, { type: 'leader', count: 2 }, { type: 'scout', count: 3 }, { type: 'archer', count: 4 }, { type: 'berserker', count: 3 }, { type: 'shield', count: 2 }, { type: 'boss3', count: 1 }, { type: 'boss1', count: 1 }], width: 12288, height: 2732 }]; var currentLevelData = levels[0]; // UI Elements var scoreText = new Text2('Score: 0', { size: 80, fill: 0xFFFFFF }); scoreText.anchor.set(0, 0); scoreText.x = 150; scoreText.y = 50; LK.gui.topLeft.addChild(scoreText); var levelText = new Text2('Dungeon: 1', { size: 80, fill: 0xFFFFFF }); levelText.anchor.set(0.5, 0); LK.gui.top.addChild(levelText); levelText.y = 50; var comboText = new Text2('Combo: 0x', { size: 60, fill: 0xFFFF00 }); comboText.anchor.set(1, 0); LK.gui.topRight.addChild(comboText); comboText.x = -50; comboText.y = 120; var knivesText = new Text2('Knives: 5', { size: 60, fill: 0x8e44ad }); knivesText.anchor.set(1, 0); LK.gui.topRight.addChild(knivesText); knivesText.x = -50; knivesText.y = 190; // Add upgrades button showUpgradesButton = LK.getAsset('backgroundTile', { anchorX: 0.5, anchorY: 0.5, tint: 0x9b59b6, scaleX: 2, scaleY: 0.8 }); showUpgradesButton.x = -150; showUpgradesButton.y = 350; LK.gui.topRight.addChild(showUpgradesButton); var upgradesText = new Text2('UPGRADES', { size: 40, fill: 0xFFFFFF }); upgradesText.anchor.set(0.5, 0.5); upgradesText.x = -150; upgradesText.y = 350; LK.gui.topRight.addChild(upgradesText); // Add coins display var coinsDisplay = new Text2('Coins: ' + upgradeTree.coins, { size: 50, fill: 0xF1C40F }); coinsDisplay.anchor.set(1, 0); coinsDisplay.x = -50; coinsDisplay.y = 400; LK.gui.topRight.addChild(coinsDisplay); showUpgradesButton.down = function () { if (!upgradeMenu.isVisible) { upgradeMenu.show(upgradeTree); LK.gui.center.addChild(upgradeMenu); } }; var enemiesLeftText = new Text2('Enemies: 0', { size: 60, fill: 0xff4444 }); enemiesLeftText.anchor.set(1, 0); LK.gui.topRight.addChild(enemiesLeftText); enemiesLeftText.x = -50; enemiesLeftText.y = 260; // Create movement and attack buttons var leftButton = LK.getAsset('leftButton', { anchorX: 0.5, anchorY: 0.5, alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }); leftButton.x = 80; leftButton.y = -300; LK.gui.bottomLeft.addChild(leftButton); var rightButton = LK.getAsset('rightButton', { anchorX: 0.5, anchorY: 0.5, alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }); rightButton.x = 480; rightButton.y = -300; LK.gui.bottomLeft.addChild(rightButton); var upButton = LK.getAsset('upButton', { anchorX: 0.5, anchorY: 0.5, alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }); upButton.x = 280; upButton.y = -480; LK.gui.bottomLeft.addChild(upButton); var downButton = LK.getAsset('downButton', { anchorX: 0.5, anchorY: 0.5, alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }); downButton.x = 280; downButton.y = -120; LK.gui.bottomLeft.addChild(downButton); var attackButton = LK.getAsset('attackButton', { anchorX: 0.5, anchorY: 0.5, alpha: 0.9 }); attackButton.x = -150; attackButton.y = -200; LK.gui.bottomRight.addChild(attackButton); var knifeButton = LK.getAsset('knifeButton', { anchorX: 0.5, anchorY: 0.5, alpha: 0.9 }); knifeButton.x = -350; knifeButton.y = -200; LK.gui.bottomRight.addChild(knifeButton); // Create background grid var backgroundTiles = []; function createBackgroundGrid() { // Clear existing background tiles for (var i = backgroundTiles.length - 1; i >= 0; i--) { backgroundTiles[i].destroy(); } backgroundTiles = []; var tileSize = 200; var tilesX = Math.ceil(currentLevelData.width / tileSize) + 2; var tilesY = Math.ceil(currentLevelData.height / tileSize) + 2; for (var x = 0; x < tilesX; x++) { for (var y = 0; y < tilesY; y++) { var tile = game.addChild(LK.getAsset('backgroundTile', { anchorX: 0, anchorY: 0, alpha: 0.3 })); tile.x = x * tileSize; tile.y = y * tileSize; // Add subtle pattern variation if ((x + y) % 2 === 0) { tile.alpha = 0.2; } backgroundTiles.push(tile); } } } // Add background var background = game.addChild(LK.getAsset('background', { anchorX: 0, anchorY: 0, x: 0, y: 0, alpha: 1 })); // Create hero hero = game.addChild(new Hero()); hero.x = 1024; // Center of screen hero.y = 2300; // Near bottom // Apply upgrade bonuses to hero hero.maxHealth = 5 + upgradeTree.getMaxHealthBonus(); hero.health = hero.maxHealth; hero.baseSpeed = 8; hero.speed = hero.baseSpeed * upgradeTree.getMoveSpeedMultiplier(); hero.damageMultiplier = upgradeTree.getDamageMultiplier(); hero.attackSpeedMultiplier = upgradeTree.getAttackSpeedMultiplier(); hero.damageReduction = upgradeTree.getDamageReduction(); hero.invulnerabilityBonus = upgradeTree.getInvulnerabilityBonus(); // Create health display function updateHealthDisplay() { // Remove existing hearts for (var i = hearts.length - 1; i >= 0; i--) { hearts[i].destroy(); } hearts = []; // Create new hearts for (var i = 0; i < hero.health; i++) { var heart = LK.getAsset('heart', { anchorX: 0.5, anchorY: 0.5 }); heart.x = 200 + i * 80; heart.y = 200; LK.gui.topLeft.addChild(heart); hearts.push(heart); } } function updateScoreDisplay() { scoreText.setText('Score: ' + LK.getScore()); var comboMultiplier = Math.floor(hero.comboCount / 5) + 1; comboText.setText('Combo: ' + comboMultiplier + 'x'); if (coinsDisplay) { coinsDisplay.setText('Coins: ' + upgradeTree.coins); } } function updateKnivesDisplay() { knivesText.setText('Knives: ' + knivesRemaining); // Update button alpha based on availability knifeButton.alpha = knivesRemaining > 0 ? 0.9 : 0.3; } function updateEnemiesLeftDisplay() { enemiesLeftText.setText('Enemies: ' + enemies.length); } function initializeLevel() { // Show boss fight alert for final dungeon if (currentDungeon === levels.length) { LK.setTimeout(function () { alert('FINAL BOSS APPROACHING! Prepare for the ultimate challenge!'); }, 500); } // Clear existing enemies for (var i = enemies.length - 1; i >= 0; i--) { enemies[i].destroy(); } enemies = []; // Clear existing warnings for (var i = enemyWarnings.length - 1; i >= 0; i--) { enemyWarnings[i].destroy(); } enemyWarnings = []; // Clear existing knives for (var i = knives.length - 1; i >= 0; i--) { knives[i].destroy(); } knives = []; // Clear existing route effects for (var i = routeEffects.length - 1; i >= 0; i--) { routeEffects[i].destroy(); } routeEffects = []; // Clear existing blood particles for (var i = bloodParticles.length - 1; i >= 0; i--) { bloodParticles[i].destroy(); } bloodParticles = []; // Clear existing impact sparks for (var i = impactSparks.length - 1; i >= 0; i--) { impactSparks[i].destroy(); } impactSparks = []; // Clear existing dust clouds for (var i = dustClouds.length - 1; i >= 0; i--) { dustClouds[i].destroy(); } dustClouds = []; // Clear existing magical effects for (var i = magicalEffects.length - 1; i >= 0; i--) { magicalEffects[i].destroy(); } magicalEffects = []; // Clear existing boss projectiles for (var i = bossProjectiles.length - 1; i >= 0; i--) { bossProjectiles[i].destroy(); } bossProjectiles = []; // Clear existing health potions for (var i = healthPotions.length - 1; i >= 0; i--) { healthPotions[i].destroy(); } healthPotions = []; // Clear existing damage numbers for (var i = damageNumbers.length - 1; i >= 0; i--) { damageNumbers[i].destroy(); } damageNumbers = []; // Clear existing weapon trails for (var i = weaponTrails.length - 1; i >= 0; i--) { weaponTrails[i].destroy(); } weaponTrails = []; // Clear group AI data groupAI.communications = []; groupAI.flankingCoordination = []; groupAI.leaderBuffs = []; groupAI.packHunters = []; // Reset knife count with upgrade bonus knivesRemaining = 5 + upgradeTree.getStartingKnivesBonus(); currentLevelData = levels[currentDungeon - 1] || levels[levels.length - 1]; dungeonComplete = false; // Spawn enemies for this level for (var j = 0; j < currentLevelData.enemies.length; j++) { var enemyGroup = currentLevelData.enemies[j]; for (var k = 0; k < enemyGroup.count; k++) { spawnEnemy(enemyGroup.type); } } levelText.setText('Dungeon: ' + currentDungeon); updateKnivesDisplay(); updateEnemiesLeftDisplay(); // Create background grid for this level createBackgroundGrid(); } function spawnEnemy(type) { var enemy; if (type === 'boss') { // Randomly choose boss type based on dungeon var bossTypes = [BossType1, BossType2, BossType3]; var BossClass = bossTypes[Math.floor(Math.random() * bossTypes.length)]; enemy = game.addChild(new BossClass()); // Boss spawns at center of level enemy.x = currentLevelData.width / 2; enemy.y = 2200; } else if (type === 'boss1') { enemy = game.addChild(new BossType1()); enemy.x = currentLevelData.width / 2; enemy.y = 2200; } else if (type === 'boss2') { enemy = game.addChild(new BossType2()); enemy.x = currentLevelData.width / 2; enemy.y = 2200; } else if (type === 'boss3') { enemy = game.addChild(new BossType3()); enemy.x = currentLevelData.width / 2; enemy.y = 2200; } else if (type === 'leader') { enemy = game.addChild(new Leader()); // Leaders spawn in strategic positions enemy.x = currentLevelData.width / 2 + (Math.random() - 0.5) * 200; enemy.y = 1900 + Math.random() * 200; } else if (type === 'scout') { enemy = game.addChild(new Scout()); // Scouts spawn at level edges for early warning enemy.x = Math.random() < 0.5 ? 200 + Math.random() * 300 : currentLevelData.width - 500 + Math.random() * 300; enemy.y = 1700 + Math.random() * 600; } else if (type === 'berserker') { enemy = game.addChild(new Berserker()); // Berserkers spawn closer to center for aggressive positioning enemy.x = currentLevelData.width / 2 + (Math.random() - 0.5) * 600; enemy.y = 1800 + Math.random() * 400; } else if (type === 'archer') { enemy = game.addChild(new Archer()); // Archers spawn at elevated positions (back areas) enemy.x = 300 + Math.random() * (currentLevelData.width - 600); enemy.y = 1600 + Math.random() * 200; // Higher ground } else if (type === 'shield') { enemy = game.addChild(new Shield()); // Shields spawn in protective positions enemy.x = currentLevelData.width / 2 + (Math.random() - 0.5) * 400; enemy.y = 1850 + Math.random() * 300; } else { enemy = game.addChild(new Enemy(type)); // Find a spawn position that's not too close to the hero var minDistanceFromPlayer = 500; // Minimum distance from player var attempts = 0; var maxAttempts = 20; var enemyX, enemyY; do { // Random spawn position within level bounds enemyX = 200 + Math.random() * (currentLevelData.width - 400); enemyY = 1700 + Math.random() * 600; // Calculate distance from hero var dx = enemyX - hero.x; var dy = enemyY - hero.y; var distanceFromPlayer = Math.sqrt(dx * dx + dy * dy); attempts++; // If far enough from player or we've tried too many times, use this position if (distanceFromPlayer >= minDistanceFromPlayer || attempts >= maxAttempts) { enemy.x = enemyX; enemy.y = enemyY; break; } } while (attempts < maxAttempts); // Set enemy properties based on type if (type === 'basic') { enemy.health = 2; enemy.speed = 2; } else if (type === 'strong') { enemy.health = 4; enemy.speed = 0.8; } else if (type === 'fast') { enemy.health = 1; enemy.speed = 4; } else if (type === 'tank') { enemy.health = 8; enemy.speed = 0.5; } else if (type === 'hunter') { enemy.health = 3; enemy.speed = 2.5; } else if (type === 'assassin') { enemy.health = 2; enemy.speed = 3; } } enemies.push(enemy); } function spawnPowerUp() { var powerup = game.addChild(new PowerUp()); powerup.x = 500 + Math.random() * 1048; // Random x position powerup.y = 1800 + Math.random() * 400; // Above ground level // Random powerup type var types = ['health', 'invulnerable', 'damage']; powerup.type = types[Math.floor(Math.random() * types.length)]; // Color by type var powerupGraphics = powerup.getChildAt(0); if (powerup.type === 'health') { powerupGraphics.tint = 0x00ff00; // Green } else if (powerup.type === 'invulnerable') { powerupGraphics.tint = 0x0088ff; // Blue } else if (powerup.type === 'damage') { powerupGraphics.tint = 0xff8800; // Orange } powerups.push(powerup); } function findNearestEnemy(x, y) { var nearest = null; var shortestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var distance = Math.sqrt(Math.pow(enemy.x - x, 2) + Math.pow(enemy.y - y, 2)); if (distance < shortestDistance) { shortestDistance = distance; nearest = enemy; } } return nearest; } function isVisible(obj, buffer) { buffer = buffer || 100; var objScreenX = obj.x - camera.x; var objScreenY = obj.y - camera.y; return objScreenX >= -buffer && objScreenX <= 2048 + buffer && objScreenY >= -buffer && objScreenY <= 2732 + buffer; } function updateEnemyWarnings() { // Remove warnings for dead enemies for (var i = enemyWarnings.length - 1; i >= 0; i--) { var warning = enemyWarnings[i]; var enemyExists = false; for (var j = 0; j < enemies.length; j++) { if (enemies[j] === warning.targetEnemy) { enemyExists = true; break; } } if (!enemyExists) { warning.destroy(); enemyWarnings.splice(i, 1); } } // Create warnings for new enemies for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var hasWarning = false; for (var j = 0; j < enemyWarnings.length; j++) { if (enemyWarnings[j].targetEnemy === enemy) { hasWarning = true; break; } } if (!hasWarning) { var warning = LK.gui.center.addChild(new EnemyWarning()); warning.targetEnemy = enemy; warning.setDirection('left'); enemyWarnings.push(warning); } } } // Initialize UI updateHealthDisplay(); updateScoreDisplay(); updateKnivesDisplay(); updateEnemiesLeftDisplay(); // Initialize first level initializeLevel(); // Create initial background grid createBackgroundGrid(); // Set initial camera position camera.targetX = hero.x - 1024; camera.targetY = hero.y - 1366; camera.x = camera.targetX; camera.y = camera.targetY; // Button event handlers leftButton.down = function (x, y, obj) { movementState.left = true; }; leftButton.up = function (x, y, obj) { movementState.left = false; resetMovementState(); // Reset all movement when any button is released }; // Global function to check which button is under a given position function getButtonUnderPosition(screenX, screenY) { // Convert GUI coordinates and check bounds for each button (accounting for 1.5x scale) var scaledButtonWidth = 200 * 1.5; var scaledButtonHeight = 200 * 1.5; var leftBounds = { x: leftButton.x - scaledButtonWidth / 2, y: leftButton.y - scaledButtonHeight / 2, width: scaledButtonWidth, height: scaledButtonHeight }; var rightBounds = { x: rightButton.x - scaledButtonWidth / 2, y: rightButton.y - scaledButtonHeight / 2, width: scaledButtonWidth, height: scaledButtonHeight }; var upBounds = { x: upButton.x - scaledButtonWidth / 2, y: upButton.y - scaledButtonHeight / 2, width: scaledButtonWidth, height: scaledButtonHeight }; var downBounds = { x: downButton.x - scaledButtonWidth / 2, y: downButton.y - scaledButtonHeight / 2, width: scaledButtonWidth, height: scaledButtonHeight }; // Adjust screen coordinates relative to bottomLeft GUI var relativeX = screenX; var relativeY = screenY - (2732 - 500); // Approximate bottomLeft offset if (relativeX >= leftBounds.x && relativeX <= leftBounds.x + leftBounds.width && relativeY >= leftBounds.y && relativeY <= leftBounds.y + leftBounds.height) { return 'left'; } if (relativeX >= rightBounds.x && relativeX <= rightBounds.x + rightBounds.width && relativeY >= rightBounds.y && relativeY <= rightBounds.y + rightBounds.height) { return 'right'; } if (relativeX >= upBounds.x && relativeX <= upBounds.x + upBounds.width && relativeY >= upBounds.y && relativeY <= upBounds.y + upBounds.height) { return 'up'; } if (relativeX >= downBounds.x && relativeX <= downBounds.x + downBounds.width && relativeY >= downBounds.y && relativeY <= downBounds.y + downBounds.height) { return 'down'; } return null; } // Global movement handling function function handleMovementInput(direction) { // Reset all movement states first movementState.left = false; movementState.right = false; movementState.up = false; movementState.down = false; // Set the active direction if (direction === 'left') { movementState.left = true; } else if (direction === 'right') { movementState.right = true; } else if (direction === 'up') { movementState.up = true; } else if (direction === 'down') { movementState.down = true; } } leftButton.move = function (x, y, obj) { var currentButton = getButtonUnderPosition(x, y); if (currentButton) { handleMovementInput(currentButton); } else { handleMovementInput('left'); } }; rightButton.down = function (x, y, obj) { movementState.right = true; }; rightButton.up = function (x, y, obj) { movementState.right = false; resetMovementState(); // Reset all movement when any button is released }; rightButton.move = function (x, y, obj) { var currentButton = getButtonUnderPosition(x, y); if (currentButton) { handleMovementInput(currentButton); } else { handleMovementInput('right'); } }; upButton.down = function (x, y, obj) { movementState.up = true; }; upButton.up = function (x, y, obj) { movementState.up = false; resetMovementState(); // Reset all movement when any button is released }; upButton.move = function (x, y, obj) { var currentButton = getButtonUnderPosition(x, y); if (currentButton) { handleMovementInput(currentButton); } else { handleMovementInput('up'); } }; downButton.down = function (x, y, obj) { movementState.down = true; }; downButton.up = function (x, y, obj) { movementState.down = false; resetMovementState(); // Reset all movement when any button is released }; downButton.move = function (x, y, obj) { var currentButton = getButtonUnderPosition(x, y); if (currentButton) { handleMovementInput(currentButton); } else { handleMovementInput('down'); } }; attackButton.down = function (x, y, obj) { if (!hero.isAttacking) { var nearestEnemy = findNearestEnemy(hero.x, hero.y); if (nearestEnemy) { hero.attack(nearestEnemy.x); // Check if attack hits with increased range var distanceToEnemy = Math.sqrt(Math.pow(hero.x - nearestEnemy.x, 2) + Math.pow(hero.y - nearestEnemy.y, 2)); if (distanceToEnemy < 350) { // Increased from 250 to 350 var damage = hero.damageBoost ? 2 : 1; for (var i = 0; i < damage; i++) { nearestEnemy.takeDamage(); } } } else { // Attack in hero's facing direction hero.attack(hero.x + (hero.getChildAt(0).scaleX > 0 ? 100 : -100)); } } }; knifeButton.down = function (x, y, obj) { if (knivesRemaining > 0) { // Find nearest enemy for targeting var nearestEnemy = findNearestEnemy(hero.x, hero.y); if (nearestEnemy) { // Clear existing route effects before creating new one for (var i = routeEffects.length - 1; i >= 0; i--) { routeEffects[i].destroy(); routeEffects.splice(i, 1); } // Create route visualization var routeEffect = game.addChild(new RouteEffect()); routeEffect.createRoute(nearestEnemy); routeEffects.push(routeEffect); // Throw knife to follow the route var knife = game.addChild(new Knife()); knife.x = hero.x; knife.y = hero.y - 80; // Set the route for the knife to follow knife.setRoute(routeEffect.routePositions); // Rotation will be handled automatically in knife update based on target direction knives.push(knife); knivesRemaining--; updateKnivesDisplay(); LK.getSound('knifeThrow').play(); } else { // No enemy found, throw in hero facing direction var knife = game.addChild(new Knife()); knife.x = hero.x; knife.y = hero.y - 80; var heroGraphics = hero.getChildAt(0); knife.direction = heroGraphics.scaleX > 0 ? 1 : -1; // Rotation will be handled automatically in knife update based on movement direction knives.push(knife); knivesRemaining--; updateKnivesDisplay(); LK.getSound('knifeThrow').play(); } } }; // Game input (fallback for screen taps outside buttons) game.down = function (x, y, obj) { // Convert screen coordinates to world coordinates var worldX = x + camera.x; var worldY = y + camera.y; // Check if tap is for movement or attack var distanceToHero = Math.sqrt(Math.pow(worldX - hero.x, 2) + Math.pow(worldY - hero.y, 2)); if (distanceToHero > 200) { // Movement - move toward tap position var dx = worldX - hero.x; var dy = worldY - hero.y; if (Math.abs(dx) > Math.abs(dy)) { hero.move(dx > 0 ? 'right' : 'left'); } else { hero.move(dy > 0 ? 'down' : 'up'); } } else { // Attack if (!hero.isAttacking) { var nearestEnemy = findNearestEnemy(worldX, worldY); if (nearestEnemy) { hero.attack(nearestEnemy.x); // Check if attack hits with increased range var distanceToEnemy = Math.sqrt(Math.pow(hero.x - nearestEnemy.x, 2) + Math.pow(hero.y - nearestEnemy.y, 2)); if (distanceToEnemy < 350) { // Increased from 250 to 350 var damage = hero.damageBoost ? 2 : 1; for (var i = 0; i < damage; i++) { nearestEnemy.takeDamage(); } } } else { // Attack in direction of tap hero.attack(worldX); } } } }; // Main game loop game.update = function () { // Track if hero is moving this frame var wasMoving = hero.isWalking; var isMovingThisFrame = false; // Handle continuous movement based on button states - only move if button is actively pressed if (movementState.left === true) { hero.move('left'); isMovingThisFrame = true; } if (movementState.right === true) { hero.move('right'); isMovingThisFrame = true; } if (movementState.up === true) { hero.move('up'); isMovingThisFrame = true; } if (movementState.down === true) { hero.move('down'); isMovingThisFrame = true; } // Stop walking animation if no movement this frame if (wasMoving && !isMovingThisFrame) { hero.stopWalkAnimation(); } // Update screen shake screenShake.update(); // Update camera position smoothly camera.x += (camera.targetX - camera.x) * camera.smoothing; camera.y += (camera.targetY - camera.y) * camera.smoothing; // Apply camera position to game with screen shake offset game.x = -camera.x + screenShake.getOffsetX(); game.y = -camera.y + screenShake.getOffsetY(); // Check for hero-enemy collisions and apply push-back using spatial partitioning var nearbyEnemies = spatialGrid.getNearbyObjects(hero.x, hero.y, 150); for (var i = 0; i < nearbyEnemies.length; i++) { var enemy = nearbyEnemies[i]; var dx = hero.x - enemy.x; var dy = hero.y - enemy.y; var distance = Math.sqrt(dx * dx + dy * dy); // If too close, push hero away from enemy if (distance < 100 && distance > 0) { var pushForce = (100 - distance) * 0.3; var pushX = dx / distance * pushForce; var pushY = dy / distance * pushForce; // Apply push with bounds checking var newHeroX = hero.x + pushX; var newHeroY = hero.y + pushY; // Keep hero within level bounds if (newHeroX > 100 && newHeroX < currentLevelData.width - 100) { hero.x = newHeroX; } if (newHeroY > 1600 && newHeroY < 2400) { hero.y = newHeroY; } // Update camera target when hero is pushed camera.targetX = hero.x - 1024; camera.targetY = hero.y - 1366; camera.targetX = Math.max(0, Math.min(camera.targetX, currentLevelData.width - 2048)); camera.targetY = Math.max(0, Math.min(camera.targetY, currentLevelData.height - 2732)); } } // Clear and rebuild spatial grid spatialGrid.clear(); for (var i = 0; i < enemies.length; i++) { spatialGrid.addObject(enemies[i], enemies[i].x, enemies[i].y); } // Update all game objects with culling for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; if (isVisible(enemy, 300)) { // Only update visible enemies + buffer enemy.update(); } } for (var i = coins.length - 1; i >= 0; i--) { // Safety check to ensure coin exists at this index if (coins[i] && isVisible(coins[i], 100)) { // Check for coin magnetism var coin = coins[i]; var distanceToHero = Math.sqrt(Math.pow(coin.x - hero.x, 2) + Math.pow(coin.y - hero.y, 2)); var magnetRange = upgradeTree.getCoinMagnetismRange(); if (distanceToHero < magnetRange) { // Move coin toward hero var dx = hero.x - coin.x; var dy = hero.y - coin.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { var magnetSpeed = 8; coin.x += dx / distance * magnetSpeed; coin.y += dy / distance * magnetSpeed; } // Collect if very close if (distanceToHero < 50) { upgradeTree.addCoins(10); coinsDisplay.setText('Coins: ' + upgradeTree.coins); coin.collect(); } } // Additional safety check before calling update if (coins[i]) { coins[i].update(); } } } for (var i = powerups.length - 1; i >= 0; i--) { if (isVisible(powerups[i], 100)) { powerups[i].update(); } } for (var i = healthPotions.length - 1; i >= 0; i--) { if (isVisible(healthPotions[i], 100)) { healthPotions[i].update(); } } for (var i = knives.length - 1; i >= 0; i--) { knives[i].update(); // Always update knives as they move fast } for (var i = bloodParticles.length - 1; i >= 0; i--) { if (isVisible(bloodParticles[i], 50)) { bloodParticles[i].update(); } } for (var i = impactSparks.length - 1; i >= 0; i--) { if (isVisible(impactSparks[i], 50)) { impactSparks[i].update(); } } for (var i = dustClouds.length - 1; i >= 0; i--) { if (isVisible(dustClouds[i], 100)) { dustClouds[i].update(); } } for (var i = magicalEffects.length - 1; i >= 0; i--) { if (isVisible(magicalEffects[i], 100)) { magicalEffects[i].update(); } } for (var i = bossProjectiles.length - 1; i >= 0; i--) { var projectile = bossProjectiles[i]; // Check if any shield enemy can block this projectile var blocked = false; for (var j = 0; j < enemies.length; j++) { var enemy = enemies[j]; if (enemy.enemyType === 'shield' && enemy.blockProjectile) { if (enemy.blockProjectile(projectile)) { blocked = true; break; } } } // Only update projectile if it wasn't blocked if (!blocked) { projectile.update(); // Always update projectiles as they move fast } } // Update coins display to reflect automatic coin collection updateScoreDisplay(); for (var i = damageNumbers.length - 1; i >= 0; i--) { if (isVisible(damageNumbers[i], 100)) { damageNumbers[i].update(); } } for (var i = weaponTrails.length - 1; i >= 0; i--) { weaponTrails[i].update(); // Always update trails for smooth effect } // Clean up destroyed route effects for (var i = routeEffects.length - 1; i >= 0; i--) { var routeEffect = routeEffects[i]; if (!routeEffect.parent) { routeEffects.splice(i, 1); } } // Update enemy warnings updateEnemyWarnings(); for (var i = 0; i < enemyWarnings.length; i++) { enemyWarnings[i].update(); } // Special check for dungeon 7 boss - kill boss when all soldiers are dead if (currentDungeon === 7 && !dungeonComplete && enemies.length > 0) { var bossCount = 0; var summonerBoss = null; var otherEnemyCount = 0; // Count bosses and other enemies for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.bossType === 'summoner') { bossCount++; summonerBoss = enemy; // Reduce boss health specifically for dungeon 7 if (summonerBoss.health > 25) { summonerBoss.health = 25; summonerBoss.maxHealth = 25; } } else { otherEnemyCount++; } } // If only summoner boss remains (all soldiers dead), kill the boss if (bossCount === 1 && otherEnemyCount === 0 && summonerBoss) { summonerBoss.die(); } } // Check for dungeon completion if (!dungeonComplete && enemies.length === 0) { dungeonComplete = true; currentDungeon++; if (currentDungeon <= levels.length) { // Start next dungeon after delay LK.setTimeout(function () { initializeLevel(); }, 2000); } else { // All dungeons completed LK.showYouWin(); } } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var ArcherProjectile = Container.expand(function () {
var self = Container.call(this);
var projectileGraphics = self.attachAsset('bossProjectile', {
anchorX: 0.5,
anchorY: 0.5
});
projectileGraphics.tint = 0x8844ff; // Purple arrows
projectileGraphics.scaleX = 0.8;
projectileGraphics.scaleY = 0.8;
self.velocityX = 0;
self.velocityY = 0;
self.lifetime = 240; // 4 seconds
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
self.lifetime--;
// Set rotation to face movement direction
if (self.velocityX !== 0 || self.velocityY !== 0) {
var angle = Math.atan2(self.velocityY, self.velocityX);
projectileGraphics.rotation = angle;
}
// Check collision with hero
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
if (distanceToHero < 60) {
hero.takeDamage();
self.destroy();
// Remove from projectiles array
for (var i = bossProjectiles.length - 1; i >= 0; i--) {
if (bossProjectiles[i] === self) {
bossProjectiles.splice(i, 1);
break;
}
}
return;
}
// Remove if lifetime expired or off screen
if (self.lifetime <= 0 || self.x < -100 || self.x > currentLevelData.width + 100 || self.y < -100 || self.y > 3000) {
self.destroy();
// Remove from projectiles array
for (var i = bossProjectiles.length - 1; i >= 0; i--) {
if (bossProjectiles[i] === self) {
bossProjectiles.splice(i, 1);
break;
}
}
}
};
return self;
});
// Base Boss class with common functionality
var BaseBoss = Container.expand(function () {
var self = Container.call(this);
self.health = 50;
self.maxHealth = 50;
self.speed = 3;
self.lastX = 0;
self.lastY = 0;
self.state = 'IDLE'; // AI states: IDLE, PURSUING, ATTACKING, STUNNED, ENRAGED
self.stateTimer = 0;
self.attackCooldown = 0;
self.isInvulnerable = false;
self.bossType = 'base';
self.phase = 1;
self.predictedHeroX = 0;
self.predictedHeroY = 0;
// Predictive targeting system
self.updatePrediction = function () {
var heroVelocityX = hero.x - hero.lastX;
var heroVelocityY = hero.y - hero.lastY;
var predictionFrames = 30; // Predict 0.5 seconds ahead
self.predictedHeroX = hero.x + heroVelocityX * predictionFrames;
self.predictedHeroY = hero.y + heroVelocityY * predictionFrames;
};
// State machine update
self.updateState = function () {
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
self.stateTimer++;
switch (self.state) {
case 'IDLE':
if (distanceToHero < 600) {
self.setState('PURSUING');
}
break;
case 'PURSUING':
if (distanceToHero < 200) {
self.setState('ATTACKING');
} else if (distanceToHero > 800) {
self.setState('IDLE');
}
break;
case 'ATTACKING':
if (distanceToHero > 300) {
self.setState('PURSUING');
}
break;
case 'STUNNED':
if (self.stateTimer > 120) {
// 2 seconds
self.setState('ENRAGED');
}
break;
case 'ENRAGED':
if (self.health > self.maxHealth * 0.5) {
self.setState('PURSUING');
}
break;
}
};
self.setState = function (newState) {
self.state = newState;
self.stateTimer = 0;
self.onStateChange(newState);
};
// Override for BossType1 specific state changes
self.onStateChange = function (newState) {
var graphics = self.getChildAt(0);
if (newState === 'ENRAGED') {
graphics.tint = 0xff4444;
self.speed *= 2.0; // More aggressive speed boost
// Add particle effect for enrage
for (var i = 0; i < 8; i++) {
var particle = game.addChild(getFromPool('bloodParticle'));
particle.reset(self.x + (Math.random() - 0.5) * 100, self.y - 150 + (Math.random() - 0.5) * 100);
var particleGraphics = particle.getChildAt(0);
particleGraphics.tint = 0xff4444;
bloodParticles.push(particle);
}
} else if (newState === 'STUNNED') {
graphics.tint = 0x888888;
self.speed *= 0.3; // More pronounced slowdown
// Vulnerability window - takes extra damage when stunned
self.isInvulnerable = false;
} else if (newState === 'ATTACKING') {
graphics.tint = 0xffaa00; // Orange tint when attacking
} else {
graphics.tint = 0xffffff;
}
};
self.takeDamage = function (fromKnife) {
if (self.isInvulnerable) return;
var damage = 1;
var isCritical = Math.random() < 0.2; // 20% critical chance on bosses
var isVulnerable = self.state === 'STUNNED';
if (isVulnerable) {
damage *= 2; // Double damage when stunned
}
if (isCritical) {
damage += 1; // Extra damage for critical
}
self.health -= damage;
// Create damage number
var damageNumber = game.addChild(getFromPool('damageNumber'));
damageNumber.reset(self.x + (Math.random() - 0.5) * 60, self.y - 150, damage, isCritical || isVulnerable);
damageNumbers.push(damageNumber);
// Screen flash for critical hits on bosses
if (isCritical) {
createScreenFlash(0xFFD700, 400, 0.2); // Gold flash for critical
}
LK.effects.flashObject(self, isCritical ? 0xFFD700 : 0xffffff, isCritical ? 600 : 200);
// Create enhanced blood particles with variety
for (var p = 0; p < 10; p++) {
var bloodParticle = game.addChild(getFromPool('bloodParticle'));
var particleType = Math.random() < 0.3 ? 'large' : Math.random() < 0.5 ? 'small' : 'normal';
bloodParticle.reset(self.x + (Math.random() - 0.5) * 60, self.y - 100 + (Math.random() - 0.5) * 60, particleType);
bloodParticles.push(bloodParticle);
}
// Add spray particles for dramatic effect
for (var p = 0; p < 8; p++) {
var sprayParticle = game.addChild(getFromPool('bloodParticle'));
sprayParticle.reset(self.x + (Math.random() - 0.5) * 40, self.y - 120 + (Math.random() - 0.5) * 40, 'spray');
bloodParticles.push(sprayParticle);
}
// State transitions based on damage
if (self.health <= self.maxHealth * 0.25 && self.state !== 'ENRAGED') {
self.setState('ENRAGED');
} else if (fromKnife && Math.random() < 0.3) {
self.setState('STUNNED');
}
if (self.health <= 0) {
self.die();
}
};
self.die = function () {
LK.effects.flashScreen(0xffffff, 2000);
// Determine coin reward based on boss type
var coinReward = 50; // Default boss reward
if (self.bossType === 'melee') {
coinReward = 50;
} else if (self.bossType === 'summoner') {
coinReward = 75;
} else if (self.bossType === 'environmental') {
coinReward = 100;
}
// Automatically give coins to upgrade tree
upgradeTree.addCoins(coinReward);
for (var i = 0; i < 10; i++) {
var coin = game.addChild(new Coin());
coin.x = self.x + (Math.random() - 0.5) * 200;
coin.y = self.y + (Math.random() - 0.5) * 200;
coins.push(coin);
}
LK.setScore(LK.getScore() + 1000);
hero.addCombo();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
self.destroy();
updateScoreDisplay();
updateEnemiesLeftDisplay();
};
return self;
});
// BossType3: Environmental Manipulator
var BossType3 = BaseBoss.expand(function () {
var self = BaseBoss.call(this);
var bossGraphics = self.attachAsset('boss', {
anchorX: 0.5,
anchorY: 1.0
});
bossGraphics.tint = 0x00ff88; // Green for environmental
self.bossType = 'environmental';
self.hazardCooldown = 0;
self.wallCooldown = 0;
self.earthquakeCooldown = 0;
self.activeHazards = [];
self.isChanneling = false;
self.update = function () {
self.updatePrediction();
self.updateState();
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
// Phase transitions
if (self.health > self.maxHealth * 0.66) {
self.phase = 1;
} else if (self.health > self.maxHealth * 0.33) {
self.phase = 2;
} else {
self.phase = 3;
}
// Environmental boss positioning based on state
if (self.state === 'PURSUING' || self.state === 'ATTACKING' || self.state === 'ENRAGED') {
// Move to strategic positions based on predicted hero movement
var strategicX = self.state === 'ENRAGED' ? currentLevelData.width / 2 + (self.predictedHeroX > currentLevelData.width / 2 ? -300 : 300) : currentLevelData.width / 2;
var strategicY = 2000;
var dx = strategicX - self.x;
var dy = strategicY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 50) {
self.lastX = self.x;
self.lastY = self.y;
var moveSpeed = self.state === 'ENRAGED' ? self.speed : self.speed * 0.5;
self.x += dx / distance * moveSpeed;
self.y += dy / distance * moveSpeed;
}
}
// Enhanced environmental attacks with state-based timing
if (self.state === 'ATTACKING' && self.hazardCooldown <= 0) {
self.showHazardWarning();
}
if (self.phase >= 2 && self.state === 'ATTACKING' && self.wallCooldown <= 0) {
self.showWallWarning();
}
if (self.phase === 3 && self.state === 'ENRAGED' && self.earthquakeCooldown <= 0) {
self.showEarthquakeWarning();
}
// Cooldown updates
if (self.hazardCooldown > 0) self.hazardCooldown--;
if (self.wallCooldown > 0) self.wallCooldown--;
if (self.earthquakeCooldown > 0) self.earthquakeCooldown--;
};
self.createHazard = function () {
self.isChanneling = true;
tween(bossGraphics, {
tint: 0xff4400
}, {
duration: 300
});
// Create fire hazard near predicted hero position
var hazard = game.addChild(LK.getAsset('hitEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
}));
hazard.x = self.predictedHeroX + (Math.random() - 0.5) * 100;
hazard.y = self.predictedHeroY + (Math.random() - 0.5) * 100;
// Warning phase
tween(hazard, {
alpha: 0.5,
scaleX: 2,
scaleY: 2
}, {
duration: 1000
});
// Damage phase
LK.setTimeout(function () {
tween(hazard, {
alpha: 1,
scaleX: 3,
scaleY: 3,
tint: 0xff0000
}, {
duration: 500,
onFinish: function onFinish() {
// Check if hero is in hazard area
var dx = hero.x - hazard.x;
var dy = hero.y - hazard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
hero.takeDamage();
}
// Remove hazard
hazard.destroy();
for (var i = self.activeHazards.length - 1; i >= 0; i--) {
if (self.activeHazards[i] === hazard) {
self.activeHazards.splice(i, 1);
break;
}
}
}
});
}, 1000);
self.activeHazards.push(hazard);
tween(bossGraphics, {
tint: 0x00ff88
}, {
duration: 300
});
self.isChanneling = false;
self.hazardCooldown = self.phase === 3 ? 120 : 180;
};
self.createWalls = function () {
// Create temporary walls that block movement
for (var i = 0; i < 3; i++) {
var wall = game.addChild(LK.getAsset('backgroundTile', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
tint: 0x666666
}));
wall.x = 300 + i * 400;
wall.y = 1800 + Math.random() * 400;
tween(wall, {
alpha: 0.8,
scaleY: 3
}, {
duration: 500
});
// Remove walls after duration
LK.setTimeout(function () {
tween(wall, {
alpha: 0,
scaleY: 0.1
}, {
duration: 500,
onFinish: function onFinish() {
wall.destroy();
}
});
}, 5000);
}
self.wallCooldown = 420; // 7 seconds
};
self.showHazardWarning = function () {
// Create multiple warning areas using predictive targeting
var warningAreas = [{
x: self.predictedHeroX,
y: self.predictedHeroY
}, {
x: hero.x,
y: hero.y
},
// Current position as backup
{
x: self.predictedHeroX + (Math.random() - 0.5) * 200,
y: self.predictedHeroY + (Math.random() - 0.5) * 200
}];
for (var i = 0; i < warningAreas.length; i++) {
var warning = game.addChild(LK.getAsset('hitEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
tint: 0xff4400
}));
warning.x = warningAreas[i].x;
warning.y = warningAreas[i].y;
tween(warning, {
alpha: 0.6,
scaleX: 2.5,
scaleY: 2.5
}, {
duration: 1500,
onFinish: function onFinish() {
warning.destroy();
}
});
}
// Boss visual warning
tween(bossGraphics, {
tint: 0xff4400,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
onFinish: function onFinish() {
self.createHazard();
tween(bossGraphics, {
tint: 0x00ff88,
scaleX: 1,
scaleY: 1
}, {
duration: 300
});
}
});
};
self.showWallWarning = function () {
// Show where walls will appear
var wallPositions = [{
x: 300,
y: 1800
}, {
x: 700,
y: 2000
}, {
x: 1100,
y: 1900
}];
for (var i = 0; i < wallPositions.length; i++) {
var warning = game.addChild(LK.getAsset('backgroundTile', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
tint: 0xffaa00,
scaleY: 0.1
}));
warning.x = wallPositions[i].x;
warning.y = wallPositions[i].y;
tween(warning, {
alpha: 0.7,
scaleY: 2
}, {
duration: 1000,
onFinish: function onFinish() {
warning.destroy();
}
});
}
LK.setTimeout(function () {
self.createWalls();
}, 1000);
};
self.showEarthquakeWarning = function () {
// Screen warning for earthquake
LK.effects.flashScreen(0x8b4513, 1500);
// Boss charges up
tween(bossGraphics, {
tint: 0x8b4513,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1500,
onFinish: function onFinish() {
self.earthquake();
}
});
};
self.earthquake = function () {
self.isChanneling = true;
// Create vulnerability window - boss can't move during earthquake
self.speed = 0;
LK.effects.flashScreen(0x8b4513, 2000);
// Intense screen shake for earthquake
triggerScreenShake(25, 1500, tween.easeInOut);
// Screen shake effect simulation through rapid position changes
var originalX = bossGraphics.x;
var originalY = bossGraphics.y;
for (var i = 0; i < 20; i++) {
LK.setTimeout(function () {
bossGraphics.x = originalX + (Math.random() - 0.5) * 10;
bossGraphics.y = originalY + (Math.random() - 0.5) * 10;
}, i * 50);
}
LK.setTimeout(function () {
bossGraphics.x = originalX;
bossGraphics.y = originalY;
self.isChanneling = false;
// Restore speed and brief stun
self.speed = 3;
self.setState('STUNNED');
// Reset visual
tween(bossGraphics, {
tint: 0x00ff88,
scaleX: 1,
scaleY: 1
}, {
duration: 500
});
}, 1000);
// Damage hero if on ground
LK.setTimeout(function () {
if (hero.y > 2200) {
// Near ground level
hero.takeDamage();
}
}, 1000);
self.earthquakeCooldown = 600; // 10 seconds
};
return self;
});
// Legacy Boss class for backward compatibility (delegates to BossType1)
// BossType2: Summoner Boss
var BossType2 = BaseBoss.expand(function () {
var self = BaseBoss.call(this);
var bossGraphics = self.attachAsset('boss', {
anchorX: 0.5,
anchorY: 1.0
});
bossGraphics.tint = 0x9b59b6; // Purple for summoner
self.bossType = 'summoner';
self.summonCooldown = 0;
self.minionCount = 0;
self.maxMinions = 4;
self.teleportCooldown = 0;
self.shieldActive = false;
self.shieldHealth = 0;
self.update = function () {
self.updatePrediction();
self.updateState();
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
// Phase transitions affect minion count
if (self.health > self.maxHealth * 0.66) {
self.phase = 1;
self.maxMinions = 2;
} else if (self.health > self.maxHealth * 0.33) {
self.phase = 2;
self.maxMinions = 3;
} else {
self.phase = 3;
self.maxMinions = 4;
}
// Enhanced movement based on state with predictive positioning
if (self.state === 'PURSUING' || self.state === 'ATTACKING' || self.state === 'ENRAGED') {
var optimalDistance = self.state === 'ENRAGED' ? 300 : 450;
if (distanceToHero < optimalDistance) {
// Move away from hero, but anticipate their movement
var escapeX = self.x - self.predictedHeroX;
var escapeY = self.y - self.predictedHeroY;
var distance = Math.sqrt(escapeX * escapeX + escapeY * escapeY);
if (distance > 0) {
self.lastX = self.x;
self.lastY = self.y;
var moveSpeed = self.state === 'ENRAGED' ? self.speed * 1.5 : self.speed;
self.x += escapeX / distance * moveSpeed;
self.y += escapeY / distance * moveSpeed;
bossGraphics.scaleX = escapeX > 0 ? -1 : 1; // Face away from hero
}
}
}
// Enhanced summoning with warnings
if (self.state === 'ATTACKING' && self.summonCooldown <= 0 && self.minionCount < self.maxMinions) {
self.showSummonWarning();
}
// Predictive teleport when threatened
if (self.phase >= 2 && self.teleportCooldown <= 0 && distanceToHero < 250) {
self.showTeleportWarning();
}
// Shield ability in phase 3
if (self.phase === 3 && !self.shieldActive && self.health <= self.maxHealth * 0.25) {
self.activateShield();
}
// Cooldown updates
if (self.summonCooldown > 0) self.summonCooldown--;
if (self.teleportCooldown > 0) self.teleportCooldown--;
};
self.summonMinion = function () {
// Create a specialized minion near the boss
var minion = game.addChild(new Minion());
minion.x = self.x + (Math.random() - 0.5) * 200;
minion.y = self.y + (Math.random() - 0.5) * 100;
minion.summoner = self; // Link minion to summoner
// Keep within bounds
minion.x = Math.max(100, Math.min(minion.x, currentLevelData.width - 100));
minion.y = Math.max(1600, Math.min(minion.y, 2400));
// Visual summoning effect
tween(bossGraphics, {
tint: 0xff00ff
}, {
duration: 200
});
tween(bossGraphics, {
tint: 0x9b59b6
}, {
duration: 200
});
LK.effects.flashObject(minion, 0x9b59b6, 500);
enemies.push(minion);
self.minionCount++;
self.summonCooldown = self.phase === 3 ? 180 : 240; // Faster summoning in phase 3
updateEnemiesLeftDisplay();
};
self.teleport = function () {
// Teleport to a safe distance from hero
var teleportDistance = 500;
var angle = Math.random() * Math.PI * 2;
var newX = hero.x + Math.cos(angle) * teleportDistance;
var newY = hero.y + Math.sin(angle) * teleportDistance;
// Keep within bounds
newX = Math.max(200, Math.min(newX, currentLevelData.width - 200));
newY = Math.max(1700, Math.min(newY, 2300));
// Visual teleport effect
tween(bossGraphics, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 300,
onFinish: function onFinish() {
self.x = newX;
self.y = newY;
tween(bossGraphics, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 300
});
LK.effects.flashObject(self, 0x9b59b6, 500);
}
});
self.teleportCooldown = 300; // 5 seconds
};
self.activateShield = function () {
self.shieldActive = true;
self.shieldHealth = 10;
self.isInvulnerable = true;
// Visual shield effect
tween(bossGraphics, {
tint: 0x00ffff
}, {
duration: 200
});
LK.effects.flashObject(self, 0x00ffff, 1000);
// Shield lasts until destroyed or timeout
LK.setTimeout(function () {
self.deactivateShield();
}, 10000); // 10 seconds max
};
self.showSummonWarning = function () {
// Create warning circles at summon locations
var summonLocations = [{
x: self.x + 150,
y: self.y
}, {
x: self.x - 150,
y: self.y
}, {
x: self.x,
y: self.y + 100
}];
for (var i = 0; i < summonLocations.length; i++) {
var warning = game.addChild(LK.getAsset('hitEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
tint: 0x9b59b6
}));
warning.x = summonLocations[i].x;
warning.y = summonLocations[i].y;
tween(warning, {
alpha: 0.7,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1000,
onFinish: function onFinish() {
warning.destroy();
}
});
}
// Delay actual summon
LK.setTimeout(function () {
if (self.minionCount < self.maxMinions) {
self.summonMinion();
}
}, 1000);
};
self.showTeleportWarning = function () {
// Flash before teleporting
tween(bossGraphics, {
alpha: 0.3,
tint: 0xff00ff
}, {
duration: 300,
onFinish: function onFinish() {
self.teleport();
}
});
};
self.deactivateShield = function () {
self.shieldActive = false;
self.isInvulnerable = false;
// Vulnerability window after shield breaks
self.setState('STUNNED');
tween(bossGraphics, {
tint: 0x9b59b6
}, {
duration: 500
});
};
// Override takeDamage to handle shield
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (fromKnife) {
if (self.shieldActive) {
self.shieldHealth--;
LK.effects.flashObject(self, 0x00ffff, 100);
if (self.shieldHealth <= 0) {
self.deactivateShield();
}
return;
}
// Reduce minion count when boss takes damage
if (self.minionCount > 0) {
self.minionCount--;
}
originalTakeDamage.call(self, fromKnife);
};
return self;
});
// BossType1: Melee/Projectile Boss (original boss enhanced)
var BossType1 = BaseBoss.expand(function () {
var self = BaseBoss.call(this);
var bossGraphics = self.attachAsset('boss', {
anchorX: 0.5,
anchorY: 1.0
});
self.bossType = 'melee';
self.projectileAttackCooldown = 0;
self.chargeAttackCooldown = 0;
self.isCharging = false;
self.chargingTarget = null;
self.spinAttackCooldown = 0;
self.isSpinning = false;
self.update = function () {
self.updatePrediction();
self.updateState();
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
// Phase transitions
if (self.health > self.maxHealth * 0.66) {
self.phase = 1;
} else if (self.health > self.maxHealth * 0.33) {
self.phase = 2;
} else {
self.phase = 3;
}
// Movement based on state
if (self.state === 'PURSUING' || self.state === 'ENRAGED') {
var targetX = self.state === 'ENRAGED' ? self.predictedHeroX : hero.x;
var targetY = self.state === 'ENRAGED' ? self.predictedHeroY : hero.y;
var dx = targetX - self.x;
var dy = targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
self.lastX = self.x;
self.lastY = self.y;
var moveSpeed = self.speed;
// Enraged state moves faster toward predicted position
if (self.state === 'ENRAGED') {
moveSpeed *= 1.8;
}
self.x += dx / distance * moveSpeed;
self.y += dy / distance * moveSpeed;
bossGraphics.scaleX = dx > 0 ? 1 : -1;
}
}
// Attacks based on state with visual warnings
if (self.state === 'ATTACKING' && self.attackCooldown <= 0) {
if (distanceToHero < 150) {
self.showMeleeWarning();
} else if (self.phase >= 2 && self.projectileAttackCooldown <= 0) {
self.showProjectileWarning();
}
}
// Special attacks with warnings
if (self.phase >= 2 && self.chargeAttackCooldown <= 0 && distanceToHero > 300 && !self.isCharging) {
self.showChargeWarning();
}
if (self.phase === 3 && self.spinAttackCooldown <= 0 && distanceToHero < 250 && !self.isSpinning) {
self.showSpinWarning();
}
// Cooldown updates
if (self.attackCooldown > 0) self.attackCooldown--;
if (self.projectileAttackCooldown > 0) self.projectileAttackCooldown--;
if (self.chargeAttackCooldown > 0) self.chargeAttackCooldown--;
if (self.spinAttackCooldown > 0) self.spinAttackCooldown--;
};
self.meleeAttack = function () {
hero.takeDamage();
self.attackCooldown = 90;
LK.effects.flashObject(self, 0xffffff, 200);
// Screen shake on boss melee attack
triggerScreenShake(15, 300, tween.easeOut);
};
self.fireProjectile = function () {
tween(bossGraphics, {
tint: 0x8800ff
}, {
duration: 200
});
tween(bossGraphics, {
tint: 0xffffff
}, {
duration: 200
});
// Screen shake on boss projectile attack
triggerScreenShake(8, 200, tween.easeOut);
var projectile = game.addChild(new BossProjectile());
projectile.x = self.x;
projectile.y = self.y - 200;
var dx = self.predictedHeroX - self.x;
var dy = self.predictedHeroY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var speed = self.phase === 3 ? 12 : 8;
projectile.velocityX = dx / distance * speed;
projectile.velocityY = dy / distance * speed;
}
bossProjectiles.push(projectile);
self.projectileAttackCooldown = self.phase === 3 ? 60 : 120;
};
self.chargeAttack = function () {
self.isCharging = true;
self.chargingTarget = {
x: self.predictedHeroX,
y: self.predictedHeroY
};
tween(bossGraphics, {
tint: 0xff4444,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 800,
easing: tween.easeIn
});
LK.setTimeout(function () {
if (!self.chargingTarget) return;
LK.effects.flashObject(self, 0xffffff, 300);
var dx = self.chargingTarget.x - self.x;
var dy = self.chargingTarget.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var chargeDistance = 400;
var targetX = self.x + dx / distance * chargeDistance;
var targetY = self.y + dy / distance * chargeDistance;
targetX = Math.max(100, Math.min(targetX, currentLevelData.width - 100));
targetY = Math.max(1600, Math.min(targetY, 2400));
tween(self, {
x: targetX,
y: targetY
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(bossGraphics, {
tint: 0xffffff,
scaleX: 1,
scaleY: 1
}, {
duration: 500
});
self.isCharging = false;
self.chargingTarget = null;
}
});
LK.setTimeout(function () {
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
if (distanceToHero < 150) {
hero.takeDamage();
// Screen shake on successful charge attack hit
triggerScreenShake(20, 400, tween.easeOut);
}
}, 150);
}
}, 800);
self.chargeAttackCooldown = self.phase === 3 ? 240 : 360;
};
self.showMeleeWarning = function () {
// Flash red warning for 0.5 seconds before attacking
tween(bossGraphics, {
tint: 0xff0000,
scaleX: bossGraphics.scaleX * 1.3,
scaleY: 1.3
}, {
duration: 500,
onFinish: function onFinish() {
self.meleeAttack();
tween(bossGraphics, {
tint: 0xffffff,
scaleX: bossGraphics.scaleX > 0 ? 1 : -1,
scaleY: 1
}, {
duration: 200
});
}
});
};
self.showProjectileWarning = function () {
// Create warning indicator at predicted position
var warningIndicator = game.addChild(LK.getAsset('hitEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
tint: 0xff4444
}));
warningIndicator.x = self.predictedHeroX;
warningIndicator.y = self.predictedHeroY;
tween(warningIndicator, {
alpha: 0.8,
scaleX: 2,
scaleY: 2
}, {
duration: 800,
onFinish: function onFinish() {
self.fireProjectile();
warningIndicator.destroy();
}
});
};
self.showChargeWarning = function () {
// Show charge direction warning
var warningLine = game.addChild(LK.getAsset('slashEffect', {
anchorX: 0,
anchorY: 0.5,
alpha: 0,
tint: 0xff4444
}));
var dx = self.predictedHeroX - self.x;
var dy = self.predictedHeroY - self.y;
var angle = Math.atan2(dy, dx);
warningLine.x = self.x;
warningLine.y = self.y - 100;
warningLine.rotation = angle;
warningLine.scaleX = 8;
warningLine.scaleY = 2;
tween(warningLine, {
alpha: 0.7
}, {
duration: 600,
onFinish: function onFinish() {
self.chargeAttack();
warningLine.destroy();
}
});
};
self.showSpinWarning = function () {
// Create expanding red circle warning
var warningCircle = game.addChild(LK.getAsset('hitEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
tint: 0xff0000
}));
warningCircle.x = self.x;
warningCircle.y = self.y - 100;
tween(warningCircle, {
alpha: 0.6,
scaleX: 4,
scaleY: 4
}, {
duration: 1000,
onFinish: function onFinish() {
self.spinAttack();
warningCircle.destroy();
}
});
};
self.spinAttack = function () {
self.isSpinning = true;
// Create vulnerability window - boss takes double damage during spin
self.isInvulnerable = false;
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (fromKnife) {
// Double damage during spin attack
originalTakeDamage.call(self, fromKnife);
if (self.health > 0) {
originalTakeDamage.call(self, fromKnife);
}
};
tween(bossGraphics, {
rotation: Math.PI * 4
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: function onFinish() {
bossGraphics.rotation = 0;
self.isSpinning = false;
// Restore normal damage after spin
self.takeDamage = originalTakeDamage;
// Brief stunned state after spin
self.setState('STUNNED');
}
});
tween(bossGraphics, {
scaleX: 1.5,
scaleY: 1.5,
tint: 0xff8800
}, {
duration: 750,
easing: tween.easeOut
});
tween(bossGraphics, {
scaleX: 1,
scaleY: 1,
tint: 0xffffff
}, {
duration: 750,
easing: tween.easeIn
});
for (var i = 0; i < 5; i++) {
LK.setTimeout(function () {
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
if (distanceToHero < 200) {
hero.takeDamage();
LK.effects.flashObject(hero, 0xff0000, 200);
// Screen shake on spin attack hit
triggerScreenShake(12, 250, tween.easeOut);
}
}, i * 300);
}
self.spinAttackCooldown = 420;
};
return self;
});
var BloodParticle = Container.expand(function () {
var self = Container.call(this);
var particleGraphics = self.attachAsset('bloodParticle', {
anchorX: 0.5,
anchorY: 0.5
});
self.velocityX = (Math.random() - 0.5) * 8;
self.velocityY = -Math.random() * 6 - 2;
self.gravity = 0.3;
self.lifetime = 60; // 1 second at 60fps
self.particleType = 'normal'; // normal, large, small, spray
self.reset = function (startX, startY, type) {
self.x = startX;
self.y = startY;
self.particleType = type || 'normal';
// Different sizes, speeds, and lifetimes based on type
if (self.particleType === 'large') {
self.velocityX = (Math.random() - 0.5) * 6;
self.velocityY = -Math.random() * 4 - 1;
self.lifetime = 90; // Larger particles last longer
particleGraphics.scaleX = 1.5;
particleGraphics.scaleY = 1.5;
particleGraphics.tint = 0x8b0000; // Dark red
} else if (self.particleType === 'small') {
self.velocityX = (Math.random() - 0.5) * 12; // Faster small particles
self.velocityY = -Math.random() * 8 - 3;
self.lifetime = 30; // Short-lived
particleGraphics.scaleX = 0.5;
particleGraphics.scaleY = 0.5;
particleGraphics.tint = 0xff4444; // Bright red
} else if (self.particleType === 'spray') {
self.velocityX = (Math.random() - 0.5) * 16; // Very fast spray
self.velocityY = -Math.random() * 10 - 1;
self.lifetime = 45;
particleGraphics.scaleX = 0.7;
particleGraphics.scaleY = 0.7;
particleGraphics.tint = 0xcc2222; // Medium red
} else {
// Normal particles
self.velocityX = (Math.random() - 0.5) * 8;
self.velocityY = -Math.random() * 6 - 2;
self.lifetime = 60;
particleGraphics.scaleX = 1;
particleGraphics.scaleY = 1;
particleGraphics.tint = 0x8b0000;
}
particleGraphics.alpha = 1;
self.visible = true;
};
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
self.velocityY += self.gravity;
// Different fade patterns based on type
var maxLifetime = 60;
if (self.particleType === 'large') maxLifetime = 90;else if (self.particleType === 'small') maxLifetime = 30;else if (self.particleType === 'spray') maxLifetime = 45;
// Fade out over time
self.lifetime--;
particleGraphics.alpha = self.lifetime / maxLifetime;
if (self.lifetime <= 0) {
// Remove from bloodParticles array
for (var i = bloodParticles.length - 1; i >= 0; i--) {
if (bloodParticles[i] === self) {
bloodParticles.splice(i, 1);
break;
}
}
returnToPool(self, 'bloodParticle');
}
};
return self;
});
var BossProjectile = Container.expand(function () {
var self = Container.call(this);
var projectileGraphics = self.attachAsset('bossProjectile', {
anchorX: 0.5,
anchorY: 0.5
});
self.velocityX = 0;
self.velocityY = 0;
self.lifetime = 300; // 5 seconds
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
self.lifetime--;
// Set rotation to face movement direction
if (self.velocityX !== 0 || self.velocityY !== 0) {
var angle = Math.atan2(self.velocityY, self.velocityX);
projectileGraphics.rotation = angle;
}
// Check collision with hero
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
if (distanceToHero < 80) {
hero.takeDamage();
self.destroy();
// Remove from bossProjectiles array
for (var i = bossProjectiles.length - 1; i >= 0; i--) {
if (bossProjectiles[i] === self) {
bossProjectiles.splice(i, 1);
break;
}
}
return;
}
// Remove if lifetime expired or off screen
if (self.lifetime <= 0 || self.x < -100 || self.x > currentLevelData.width + 100 || self.y < -100 || self.y > 3000) {
self.destroy();
// Remove from bossProjectiles array
for (var i = bossProjectiles.length - 1; i >= 0; i--) {
if (bossProjectiles[i] === self) {
bossProjectiles.splice(i, 1);
break;
}
}
}
};
return self;
});
var Coin = Container.expand(function () {
var self = Container.call(this);
var coinGraphics = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5
});
self.collectTimer = 0;
self.update = function () {
// Auto-collect after short delay
self.collectTimer++;
if (self.collectTimer > 30) {
// 0.5 seconds
self.collect();
}
// Spin animation
coinGraphics.rotation += 0.1;
};
self.collect = function () {
LK.getSound('coin').play();
// Remove from coins array
for (var i = coins.length - 1; i >= 0; i--) {
if (coins[i] === self) {
coins.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
var DamageNumber = Container.expand(function () {
var self = Container.call(this);
var damageText = new Text2('', {
size: 60,
fill: 0xFFFFFF
});
damageText.anchor.set(0.5, 0.5);
self.addChild(damageText);
self.lifetime = 120; // 2 seconds at 60fps
self.velocityY = -3; // Float upward
self.fadeSpeed = 1 / 120; // Fade over lifetime
self.reset = function (startX, startY, damage, isCritical) {
self.x = startX;
self.y = startY;
self.lifetime = 120;
self.velocityY = isCritical ? -4 : -3; // Faster rise for critical hits
damageText.setText(damage.toString());
damageText.alpha = 1;
self.visible = true;
// Different styling for critical hits
if (isCritical) {
damageText.fill = 0xFFD700; // Gold for critical
damageText.size = 80; // Larger text
tween(self, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut
});
} else {
damageText.fill = 0xFFFFFF; // White for normal
damageText.size = 60;
self.scaleX = 1;
self.scaleY = 1;
}
};
self.update = function () {
self.y += self.velocityY;
self.velocityY *= 0.98; // Slow down over time
self.lifetime--;
damageText.alpha = self.lifetime / 120; // Fade out
if (self.lifetime <= 0) {
// Remove from damageNumbers array
for (var i = damageNumbers.length - 1; i >= 0; i--) {
if (damageNumbers[i] === self) {
damageNumbers.splice(i, 1);
break;
}
}
returnToPool(self, 'damageNumber');
}
};
return self;
});
var DustCloud = Container.expand(function () {
var self = Container.call(this);
var dustGraphics = self.attachAsset('bloodParticle', {
anchorX: 0.5,
anchorY: 0.5
});
dustGraphics.tint = 0x8b7355; // Brown dust color
dustGraphics.scaleX = 0.8;
dustGraphics.scaleY = 0.8;
self.velocityX = 0;
self.velocityY = 0;
self.lifetime = 40; // Medium lifetime
self.expandRate = 0.02; // How fast dust expands
self.reset = function (startX, startY, direction) {
self.x = startX;
self.y = startY;
// Dust moves opposite to movement direction
var baseSpeed = 1 + Math.random() * 2;
if (direction === 'left') {
self.velocityX = baseSpeed;
} else if (direction === 'right') {
self.velocityX = -baseSpeed;
} else if (direction === 'up') {
self.velocityY = baseSpeed * 0.5;
} else if (direction === 'down') {
self.velocityY = -baseSpeed * 0.5;
} else {
// Random direction for general dust
var angle = Math.random() * Math.PI * 2;
self.velocityX = Math.cos(angle) * baseSpeed;
self.velocityY = Math.sin(angle) * baseSpeed;
}
self.velocityY -= Math.random() * 1; // Slight upward drift
dustGraphics.alpha = 0.6;
dustGraphics.scaleX = 0.4 + Math.random() * 0.4;
dustGraphics.scaleY = 0.4 + Math.random() * 0.4;
self.visible = true;
};
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
// Dust slows down and expands
self.velocityX *= 0.98;
self.velocityY *= 0.98;
dustGraphics.scaleX += self.expandRate;
dustGraphics.scaleY += self.expandRate;
// Fade out over time
self.lifetime--;
dustGraphics.alpha = self.lifetime / 40 * 0.6;
if (self.lifetime <= 0) {
// Remove from dustClouds array
for (var i = dustClouds.length - 1; i >= 0; i--) {
if (dustClouds[i] === self) {
dustClouds.splice(i, 1);
break;
}
}
returnToPool(self, 'dustCloud');
}
};
return self;
});
var Enemy = Container.expand(function (enemyType) {
var self = Container.call(this);
self.enemyType = enemyType || 'basic';
// Choose asset based on enemy type
var assetName = 'enemy';
if (self.enemyType === 'basic') {
assetName = 'enemyBasic';
} else if (self.enemyType === 'strong') {
assetName = 'enemyStrong';
} else if (self.enemyType === 'fast') {
assetName = 'enemyFast';
} else if (self.enemyType === 'tank') {
assetName = 'enemyTank';
} else if (self.enemyType === 'hunter') {
assetName = 'enemyHunter';
} else if (self.enemyType === 'assassin') {
assetName = 'enemyAssassin';
}
var enemyGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 1.0
});
self.health = 2;
self.speed = 1;
self.attackCooldown = 0;
self.fromLeft = true;
self.lastX = 0;
self.lastY = 0;
self.alerted = false;
self.alertedByKnife = false;
self.update = function () {
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
// Initialize group AI properties if not already set
if (!self.hasOwnProperty('isAssignedFlanker')) self.isAssignedFlanker = false;
if (!self.hasOwnProperty('flankingTarget')) self.flankingTarget = null;
if (!self.hasOwnProperty('packGroup')) self.packGroup = null;
if (!self.hasOwnProperty('leaderBuff')) self.leaderBuff = null;
if (!self.hasOwnProperty('communicationTimer')) self.communicationTimer = 0;
// Update communication timer
if (self.communicationTimer > 0) self.communicationTimer--;
// Process group communications
var communications = groupAI.getCommunications(self, 120, 400);
for (var c = 0; c < communications.length; c++) {
var comm = communications[c];
if (comm.type === 'spotted_hero' && !self.alerted) {
self.alerted = true;
groupAI.broadcastAlert(self, 'spotted_hero', 1);
} else if (comm.type === 'under_attack' && comm.priority >= 2) {
self.alerted = true;
// Move to assist ally under attack
if (!self.isAssignedFlanker && Math.random() < 0.3) {
self.assistTarget = {
x: comm.x,
y: comm.y
};
}
} else if (comm.type === 'flanking_position' && !self.isAssignedFlanker && self.enemyType !== 'tank') {
groupAI.assignFlankingPosition(self);
}
}
// Different sight ranges for different enemy types
var sightRange = 500;
if (self.enemyType === 'hunter') {
sightRange = 800; // Hunters have better sight
} else if (self.enemyType === 'assassin') {
sightRange = 600; // Assassins have good sight
} else if (self.enemyType === 'tank') {
sightRange = 400; // Tanks have poor sight
} else if (self.enemyType === 'leader') {
sightRange = 700; // Leaders have good coordination sight
}
var canSeeHero = distanceToHero < sightRange;
// Broadcast hero sighting
if (canSeeHero && !self.alerted && self.communicationTimer <= 0) {
groupAI.broadcastAlert(self, 'spotted_hero', 1);
self.alerted = true;
self.communicationTimer = 60; // 1 second cooldown
}
if (canSeeHero || self.alerted) {
// Determine movement target based on role
var targetX = hero.x;
var targetY = hero.y;
var moveSpeed = self.speed;
// Apply leader buffs
if (self.leaderBuff) {
moveSpeed *= self.leaderBuff.speedMultiplier;
}
// Flanking behavior
if (self.isAssignedFlanker && self.flankingTarget) {
var flankDistance = Math.sqrt(Math.pow(self.x - self.flankingTarget.x, 2) + Math.pow(self.y - self.flankingTarget.y, 2));
if (flankDistance > 50) {
// Move to flanking position
targetX = self.flankingTarget.x;
targetY = self.flankingTarget.y;
} else {
// Reached flanking position, now attack hero
targetX = hero.x;
targetY = hero.y;
moveSpeed *= 1.2; // Speed boost when in flanking position
}
}
// Pack hunting behavior for fast enemies
if (self.enemyType === 'fast' || self.enemyType === 'hunter') {
var nearbyPackMembers = spatialGrid.getNearbyObjects(self.x, self.y, 200);
var packMates = 0;
for (var p = 0; p < nearbyPackMembers.length; p++) {
if (nearbyPackMembers[p] !== self && (nearbyPackMembers[p].enemyType === 'fast' || nearbyPackMembers[p].enemyType === 'hunter')) {
packMates++;
}
}
if (packMates >= 1) {
// Pack hunting behavior - try to herd hero toward stronger enemies
var nearbyStrongEnemies = spatialGrid.getNearbyObjects(hero.x, hero.y, 600);
var strongestEnemy = null;
for (var s = 0; s < nearbyStrongEnemies.length; s++) {
if (nearbyStrongEnemies[s].enemyType === 'tank' || nearbyStrongEnemies[s].enemyType === 'strong' || nearbyStrongEnemies[s].enemyType === 'leader') {
strongestEnemy = nearbyStrongEnemies[s];
break;
}
}
if (strongestEnemy) {
// Position to herd hero toward strongest enemy
var herdX = hero.x + (strongestEnemy.x - hero.x) * 0.3;
var herdY = hero.y + (strongestEnemy.y - hero.y) * 0.3;
targetX = herdX;
targetY = herdY;
moveSpeed *= 1.4; // Faster when pack hunting
}
}
}
// Assist behavior
if (self.assistTarget && !self.isAssignedFlanker) {
var assistDistance = Math.sqrt(Math.pow(self.x - self.assistTarget.x, 2) + Math.pow(self.y - self.assistTarget.y, 2));
if (assistDistance > 100) {
targetX = self.assistTarget.x;
targetY = self.assistTarget.y;
moveSpeed *= 1.1; // Slight speed boost when assisting
} else {
self.assistTarget = null; // Clear assist target when reached
}
}
// Calculate movement toward target
var dx = targetX - self.x;
var dy = targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Special movement for assassin type - teleport ability
if (self.enemyType === 'assassin' && distanceToHero > 300 && distanceToHero < 600 && Math.random() < 0.02) {
// Teleport closer to hero
var teleportDistance = 150;
var teleportX = hero.x + (Math.random() - 0.5) * teleportDistance;
var teleportY = hero.y + (Math.random() - 0.5) * teleportDistance;
// Keep within bounds
teleportX = Math.max(100, Math.min(teleportX, currentLevelData.width - 100));
teleportY = Math.max(1600, Math.min(teleportY, 2400));
self.x = teleportX;
self.y = teleportY;
// Flash effect for teleport
LK.effects.flashObject(self, 0x9b59b6, 300);
var newX = self.x;
var newY = self.y;
} else {
// Hunter type - faster when far from hero
if (self.enemyType === 'hunter' && distanceToHero > 400) {
moveSpeed *= 1.5; // 50% speed boost when hunting from distance
}
var newX = self.x + dx / distance * moveSpeed;
var newY = self.y + dy / distance * moveSpeed;
}
// Check collision with other enemies before moving using spatial partitioning
var wouldCollide = false;
var nearbyEnemies = spatialGrid.getNearbyObjects(newX, newY, 100);
for (var i = 0; i < nearbyEnemies.length; i++) {
var otherEnemy = nearbyEnemies[i];
if (otherEnemy === self) continue; // Skip self
var edx = newX - otherEnemy.x;
var edy = newY - otherEnemy.y;
var edistance = Math.sqrt(edx * edx + edy * edy);
if (edistance < 70) {
// Collision threshold between enemies
wouldCollide = true;
break;
}
}
// Only move if no collision would occur
if (!wouldCollide) {
self.lastX = self.x;
self.lastY = self.y;
self.x = newX;
self.y = newY;
}
// Face direction of movement
enemyGraphics.scaleX = dx > 0 ? 1 : -1;
}
// Attack hero if close enough
var attackRange = 100;
if (self.leaderBuff) {
attackRange *= 1.1; // Slightly increased attack range with leader buff
}
if (distanceToHero < attackRange && self.attackCooldown <= 0) {
var damage = 1;
if (self.leaderBuff && self.leaderBuff.damageMultiplier) {
damage = Math.floor(damage * self.leaderBuff.damageMultiplier);
}
hero.takeDamage();
self.attackCooldown = 120; // 2 seconds at 60fps
}
// Glow effects based on state
if (self.alerted && !self.glowOutline) {
createGlowOutline(self, 0xff8844, 0.5); // Orange glow for alerted
} else if (!self.alerted && self.glowOutline) {
removeGlowOutline(self);
}
if (self.health <= 1 && (!self.glowOutline || self.glowOutline.tint !== 0xff0000)) {
removeGlowOutline(self);
createGlowOutline(self, 0xff0000, 0.8); // Red glow for low health
} else if (self.isAssignedFlanker && (!self.glowOutline || self.glowOutline.tint !== 0x8844ff)) {
removeGlowOutline(self);
createGlowOutline(self, 0x8844ff, 0.4); // Blue glow for flankers
}
} else if (!self.alerted) {
// Only roam if not alerted - alerted enemies keep chasing even when they can't see hero
if (!self.roamDirection || Math.random() < 0.01) {
self.roamDirection = {
x: (Math.random() - 0.5) * 2,
y: (Math.random() - 0.5) * 2
};
}
var newX = self.x + self.roamDirection.x * self.speed * 0.5;
var newY = self.y + self.roamDirection.y * self.speed * 0.5;
// Check collision with other enemies before roaming
var wouldCollide = false;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy === self) continue; // Skip self
var edx = newX - otherEnemy.x;
var edy = newY - otherEnemy.y;
var edistance = Math.sqrt(edx * edx + edy * edy);
if (edistance < 70) {
// Collision threshold between enemies
wouldCollide = true;
break;
}
}
// Keep within level bounds and check collisions
if (!wouldCollide && newX > 100 && newX < currentLevelData.width - 100) {
self.lastX = self.x;
self.x = newX;
enemyGraphics.scaleX = self.roamDirection.x > 0 ? 1 : -1;
}
if (!wouldCollide && newY > 1600 && newY < 2400) {
self.lastY = self.y;
self.y = newY;
}
} else {
// Alerted enemies that can't see hero still try to chase in last known direction
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var newX = self.x + dx / distance * self.speed;
var newY = self.y + dy / distance * self.speed;
// Check collision with other enemies before moving
var wouldCollide = false;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy === self) continue; // Skip self
var edx = newX - otherEnemy.x;
var edy = newY - otherEnemy.y;
var edistance = Math.sqrt(edx * edx + edy * edy);
if (edistance < 70) {
// Collision threshold between enemies
wouldCollide = true;
break;
}
}
// Only move if no collision would occur
if (!wouldCollide) {
self.lastX = self.x;
self.lastY = self.y;
self.x = newX;
self.y = newY;
}
// Face direction of movement
enemyGraphics.scaleX = dx > 0 ? 1 : -1;
}
}
if (self.attackCooldown > 0) {
self.attackCooldown--;
}
};
self.takeDamage = function (fromKnife) {
var damage = 1;
var isCritical = Math.random() < 0.15; // 15% critical chance
if (isCritical) {
damage = 2;
self.health -= 2;
} else {
self.health--;
}
// Create damage number
var damageNumber = game.addChild(getFromPool('damageNumber'));
damageNumber.reset(self.x + (Math.random() - 0.5) * 40, self.y - 100, damage, isCritical);
damageNumbers.push(damageNumber);
LK.effects.flashObject(self, isCritical ? 0xFFD700 : 0xffffff, isCritical ? 400 : 200);
// Add glow effect based on enemy state
if (self.alerted && !self.glowOutline) {
createGlowOutline(self, 0xff4444, 0.6); // Red glow for alerted
} else if (self.health <= 1 && !self.glowOutline) {
createGlowOutline(self, 0xffaa00, 0.8); // Orange glow for low health
}
// Create knockback effect
var knockbackForce = 40;
var knockbackDirection = fromKnife ? 1 : hero.x < self.x ? 1 : -1;
var targetX = self.x + knockbackDirection * knockbackForce;
var targetY = self.y - 20; // Slight upward knockback
// Apply knockback with bounds checking
targetX = Math.max(100, Math.min(targetX, currentLevelData.width - 100));
targetY = Math.max(1600, Math.min(targetY, 2400));
tween(self, {
x: targetX,
y: targetY
}, {
duration: 200,
easing: tween.easeOut
});
// Create enhanced blood particles with variety
for (var p = 0; p < 8; p++) {
var bloodParticle = game.addChild(getFromPool('bloodParticle'));
var particleType = Math.random() < 0.2 ? 'large' : Math.random() < 0.6 ? 'small' : 'normal';
bloodParticle.reset(self.x + (Math.random() - 0.5) * 40, self.y - 60 + (Math.random() - 0.5) * 40, particleType);
bloodParticles.push(bloodParticle);
}
// Add impact sparks when hit by knife
if (fromKnife) {
for (var s = 0; s < 6; s++) {
var spark = game.addChild(getFromPool('impactSpark'));
var sparkType = Math.random() < 0.3 ? 'bright' : Math.random() < 0.5 ? 'orange' : 'normal';
spark.reset(self.x + (Math.random() - 0.5) * 30, self.y - 70 + (Math.random() - 0.5) * 30, sparkType);
impactSparks.push(spark);
}
self.alerted = true;
self.alertedByKnife = true;
}
if (self.health <= 0) {
self.die();
}
};
self.die = function () {
// Screen shake on enemy death (smaller intensity)
triggerScreenShake(6, 150, tween.easeOut);
// Determine coin reward based on enemy difficulty
var coinReward = 1; // Default for basic enemies
if (self.enemyType === 'basic') {
coinReward = 1;
} else if (self.enemyType === 'fast') {
coinReward = 2;
} else if (self.enemyType === 'scout') {
coinReward = 3;
} else if (self.enemyType === 'strong') {
coinReward = 3;
} else if (self.enemyType === 'archer') {
coinReward = 4;
} else if (self.enemyType === 'berserker') {
coinReward = 4;
} else if (self.enemyType === 'hunter') {
coinReward = 5;
} else if (self.enemyType === 'shield') {
coinReward = 6;
} else if (self.enemyType === 'assassin') {
coinReward = 6;
} else if (self.enemyType === 'tank') {
coinReward = 8;
} else if (self.enemyType === 'leader') {
coinReward = 10;
}
// Automatically give coins to upgrade tree
upgradeTree.addCoins(coinReward);
// Drop visual coin
var coin = game.addChild(new Coin());
coin.x = self.x;
coin.y = self.y;
coins.push(coin);
// 10% chance to drop health potion
if (Math.random() < 0.1) {
var healthPotion = game.addChild(new HealthPotion());
healthPotion.x = self.x + (Math.random() - 0.5) * 80; // Slight random offset
healthPotion.y = self.y + (Math.random() - 0.5) * 80;
healthPotions.push(healthPotion);
}
// Add score and combo
var baseScore = 10;
var comboMultiplier = Math.floor(hero.comboCount / 5) + 1;
var finalScore = baseScore * comboMultiplier;
LK.setScore(LK.getScore() + finalScore);
hero.addCombo();
// Remove from enemies array
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
self.destroy();
updateScoreDisplay();
updateEnemiesLeftDisplay();
};
return self;
});
var Shield = Enemy.expand(function () {
var self = Enemy.call(this, 'shield');
var shieldGraphics = self.attachAsset('enemyTank', {
anchorX: 0.5,
anchorY: 1.0
});
shieldGraphics.tint = 0x4488ff; // Blue tint for shields
self.enemyType = 'shield';
self.health = 6;
self.speed = 1.0;
self.shieldActive = true;
self.protectionRadius = 200;
self.blockCooldown = 0;
// Override update to include shield behavior
var originalUpdate = self.update;
self.update = function () {
originalUpdate.call(self);
// Shield positioning: try to stay between hero and vulnerable allies
var vulnerableAllies = [];
var nearbyAllies = spatialGrid.getNearbyObjects(self.x, self.y, self.protectionRadius * 1.5);
for (var i = 0; i < nearbyAllies.length; i++) {
var ally = nearbyAllies[i];
if (ally !== self && ally.enemyType && (ally.enemyType === 'archer' || ally.enemyType === 'scout' || ally.health <= 2)) {
vulnerableAllies.push(ally);
}
}
// If there are vulnerable allies, position between them and hero
if (vulnerableAllies.length > 0) {
var avgAllyX = 0;
var avgAllyY = 0;
for (var i = 0; i < vulnerableAllies.length; i++) {
avgAllyX += vulnerableAllies[i].x;
avgAllyY += vulnerableAllies[i].y;
}
avgAllyX /= vulnerableAllies.length;
avgAllyY /= vulnerableAllies.length;
// Position between average ally position and hero
var protectX = avgAllyX + (hero.x - avgAllyX) * 0.3;
var protectY = avgAllyY + (hero.y - avgAllyY) * 0.3;
var dx = protectX - self.x;
var dy = protectY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 50) {
// Move toward protection position
var newX = self.x + dx / distance * self.speed * 0.8;
var newY = self.y + dy / distance * self.speed * 0.8;
// Check collision before moving
var wouldCollide = false;
var nearbyEnemies = spatialGrid.getNearbyObjects(newX, newY, 100);
for (var j = 0; j < nearbyEnemies.length; j++) {
var otherEnemy = nearbyEnemies[j];
if (otherEnemy === self) continue;
var edx = newX - otherEnemy.x;
var edy = newY - otherEnemy.y;
var edistance = Math.sqrt(edx * edx + edy * edy);
if (edistance < 80) {
wouldCollide = true;
break;
}
}
if (!wouldCollide) {
// Keep within bounds
if (newX > 100 && newX < currentLevelData.width - 100) {
self.lastX = self.x;
self.x = newX;
}
if (newY > 1600 && newY < 2400) {
self.lastY = self.y;
self.y = newY;
}
shieldGraphics.scaleX = dx > 0 ? 1 : -1;
}
}
}
if (self.blockCooldown > 0) {
self.blockCooldown--;
}
};
self.blockProjectile = function (projectile) {
if (!self.shieldActive || self.blockCooldown > 0) return false;
// Check if projectile is within blocking range
var dx = projectile.x - self.x;
var dy = projectile.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 120) {
// Block the projectile
projectile.destroy();
// Remove from projectiles array
for (var i = bossProjectiles.length - 1; i >= 0; i--) {
if (bossProjectiles[i] === projectile) {
bossProjectiles.splice(i, 1);
break;
}
}
// Visual block effect
tween(shieldGraphics, {
tint: 0x88aaff,
scaleX: shieldGraphics.scaleX * 1.3,
scaleY: 1.3
}, {
duration: 200,
onFinish: function onFinish() {
tween(shieldGraphics, {
tint: 0x4488ff,
scaleX: shieldGraphics.scaleX > 0 ? 1 : -1,
scaleY: 1
}, {
duration: 200
});
}
});
// Create block sparks
for (var s = 0; s < 8; s++) {
var spark = game.addChild(getFromPool('impactSpark'));
spark.reset(self.x + (Math.random() - 0.5) * 60, self.y - 60 + (Math.random() - 0.5) * 60, 'bright');
impactSparks.push(spark);
}
self.blockCooldown = 60; // 1 second cooldown
return true;
}
return false;
};
// Override takeDamage to handle shield protection
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (fromKnife) {
// Shield enemies take reduced damage from the front
var damage = 1;
if (fromKnife) {
// Check if knife is coming from the front
var dx = hero.x - self.x;
var shieldFacing = shieldGraphics.scaleX > 0 ? 1 : -1;
if (dx > 0 && shieldFacing > 0 || dx < 0 && shieldFacing < 0) {
// Knife blocked by shield, take reduced damage
damage = 0;
// Visual block effect
LK.effects.flashObject(self, 0x88aaff, 300);
// Create block sparks
for (var s = 0; s < 6; s++) {
var spark = game.addChild(getFromPool('impactSpark'));
spark.reset(self.x + (Math.random() - 0.5) * 50, self.y - 70 + (Math.random() - 0.5) * 50, 'bright');
impactSparks.push(spark);
}
return; // No damage taken
}
}
if (damage > 0) {
originalTakeDamage.call(self, fromKnife);
}
};
return self;
});
var Scout = Enemy.expand(function () {
var self = Enemy.call(this, 'scout');
var scoutGraphics = self.attachAsset('enemyFast', {
anchorX: 0.5,
anchorY: 1.0
});
scoutGraphics.tint = 0x88ff88; // Light green for scouts
scoutGraphics.scaleX = 0.9;
scoutGraphics.scaleY = 0.9;
self.enemyType = 'scout';
self.health = 1;
self.speed = 5; // Very fast
self.sightRange = 900; // Excellent sight
self.alertCooldown = 0;
self.fleeingFromCombat = false;
self.lastAlertTime = 0;
// Override update to include scout-specific behaviors
var originalUpdate = self.update;
self.update = function () {
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
// Scout-specific sight and alerting
if (distanceToHero < self.sightRange && !self.alerted) {
self.alerted = true;
// Scouts immediately alert all nearby enemies
groupAI.broadcastAlert(self, 'spotted_hero', 3); // High priority alert
self.lastAlertTime = LK.ticks;
// Create alert visual effect
tween(scoutGraphics, {
tint: 0xffff44,
// Yellow alert color
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
onFinish: function onFinish() {
tween(scoutGraphics, {
tint: 0x88ff88,
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 300
});
}
});
}
// Flee from combat when hero gets too close
if (distanceToHero < 250) {
self.fleeingFromCombat = true;
} else if (distanceToHero > 500) {
self.fleeingFromCombat = false;
}
if (self.fleeingFromCombat) {
// Move away from hero at maximum speed
var dx = self.x - hero.x;
var dy = self.y - hero.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var fleeSpeed = self.speed * 1.5; // Extra fast when fleeing
var newX = self.x + dx / distance * fleeSpeed;
var newY = self.y + dy / distance * fleeSpeed;
// Check collision with other enemies before moving
var wouldCollide = false;
var nearbyEnemies = spatialGrid.getNearbyObjects(newX, newY, 80);
for (var i = 0; i < nearbyEnemies.length; i++) {
var otherEnemy = nearbyEnemies[i];
if (otherEnemy === self) continue;
var edx = newX - otherEnemy.x;
var edy = newY - otherEnemy.y;
var edistance = Math.sqrt(edx * edx + edy * edy);
if (edistance < 60) {
wouldCollide = true;
break;
}
}
if (!wouldCollide) {
// Keep within level bounds
if (newX > 100 && newX < currentLevelData.width - 100) {
self.lastX = self.x;
self.x = newX;
}
if (newY > 1600 && newY < 2400) {
self.lastY = self.y;
self.y = newY;
}
scoutGraphics.scaleX = dx > 0 ? 0.9 : -0.9; // Face away from hero
}
}
} else {
// Use original enemy update when not fleeing
originalUpdate.call(self);
}
// Periodic re-alerting to maintain enemy awareness
if (self.alerted && LK.ticks - self.lastAlertTime > 300) {
// Every 5 seconds
groupAI.broadcastAlert(self, 'spotted_hero', 2);
self.lastAlertTime = LK.ticks;
}
};
// Override takeDamage to trigger emergency broadcast
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (fromKnife) {
// Emergency alert when scout is attacked
groupAI.broadcastAlert(self, 'under_attack', 3);
originalTakeDamage.call(self, fromKnife);
};
return self;
});
var Leader = Enemy.expand(function () {
var self = Enemy.call(this, 'leader');
var leaderGraphics = self.attachAsset('enemyStrong', {
anchorX: 0.5,
anchorY: 1.0
});
leaderGraphics.tint = 0xFFD700; // Golden tint for leaders
leaderGraphics.scaleX = 1.2;
leaderGraphics.scaleY = 1.2;
self.enemyType = 'leader';
self.health = 6;
self.speed = 1.5;
self.buffRadius = 250;
self.commandRadius = 400;
self.buffCooldown = 0;
self.isCommanding = false;
self.commandedAllies = [];
// Override update to include leadership behaviors
var originalUpdate = self.update;
self.update = function () {
// Call original enemy update first
originalUpdate.call(self);
// Leadership AI behaviors
self.manageAllies();
self.coordinateAttacks();
self.provideBattlefieldIntelligence();
// Update buff cooldown
if (self.buffCooldown > 0) self.buffCooldown--;
};
self.manageAllies = function () {
var nearbyAllies = spatialGrid.getNearbyObjects(self.x, self.y, self.commandRadius);
self.commandedAllies = [];
for (var i = 0; i < nearbyAllies.length; i++) {
var ally = nearbyAllies[i];
if (ally !== self && ally.enemyType !== 'boss' && ally.enemyType !== 'leader') {
self.commandedAllies.push(ally);
// Provide speed buff to nearby allies
if (Math.sqrt(Math.pow(self.x - ally.x, 2) + Math.pow(self.y - ally.y, 2)) <= self.buffRadius) {
if (!ally.leaderBuff) {
ally.leaderBuff = {
speedMultiplier: 1.3,
damageMultiplier: 1.2,
source: self
};
// Visual indication of buff
createGlowOutline(ally, 0xFFD700, 0.3);
}
}
}
}
};
self.coordinateAttacks = function () {
if (self.commandedAllies.length >= 2 && self.buffCooldown <= 0) {
// Order flanking maneuvers
groupAI.broadcastAlert(self, 'flanking_position', 3);
for (var i = 0; i < Math.min(3, self.commandedAllies.length); i++) {
var ally = self.commandedAllies[i];
if (!ally.isAssignedFlanker) {
groupAI.assignFlankingPosition(ally);
}
}
self.buffCooldown = 300; // 5 seconds
self.isCommanding = true;
// Visual command effect
tween(leaderGraphics, {
tint: 0xFFAA00,
scaleX: 1.4,
scaleY: 1.4
}, {
duration: 500,
onFinish: function onFinish() {
tween(leaderGraphics, {
tint: 0xFFD700,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 300
});
self.isCommanding = false;
}
});
}
};
self.provideBattlefieldIntelligence = function () {
// Share hero position with distant allies
if (Math.random() < 0.05) {
// 5% chance per frame
groupAI.broadcastAlert(self, 'spotted_hero', 2);
}
};
// Override takeDamage to rally allies when leader is threatened
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (fromKnife) {
originalTakeDamage.call(self, fromKnife);
// Rally cry when damaged
groupAI.broadcastAlert(self, 'under_attack', 3);
// Boost nearby allies when leader is in danger
for (var i = 0; i < self.commandedAllies.length; i++) {
var ally = self.commandedAllies[i];
ally.alerted = true;
if (ally.leaderBuff) {
ally.leaderBuff.speedMultiplier = 1.5; // Increased speed when leader threatened
}
}
};
// Override die to remove buffs from allies
var originalDie = self.die;
self.die = function () {
// Remove leader buffs from all allies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.leaderBuff && enemy.leaderBuff.source === self) {
enemy.leaderBuff = null;
removeGlowOutline(enemy);
}
}
originalDie.call(self);
};
return self;
});
var Berserker = Enemy.expand(function () {
var self = Enemy.call(this, 'berserker');
var berserkerGraphics = self.attachAsset('enemyStrong', {
anchorX: 0.5,
anchorY: 1.0
});
berserkerGraphics.tint = 0xff6666; // Red tint for berserkers
self.enemyType = 'berserker';
self.health = 4;
self.maxHealth = 4;
self.speed = 1.5;
self.baseSpeed = 1.5;
self.isBerserk = false;
self.berserkThreshold = 1; // Goes berserk at 1 health
// Override update to include berserker rage
var originalUpdate = self.update;
self.update = function () {
// Check for berserk activation
if (!self.isBerserk && self.health <= self.berserkThreshold) {
self.activateBerserk();
}
originalUpdate.call(self);
};
self.activateBerserk = function () {
self.isBerserk = true;
self.speed = self.baseSpeed * 2.5; // Much faster when berserk
berserkerGraphics.tint = 0xff0000; // Bright red when berserk
berserkerGraphics.scaleX = 1.3;
berserkerGraphics.scaleY = 1.3;
self.attackCooldown = Math.floor(self.attackCooldown * 0.5); // Faster attacks
// Visual berserk effect
tween(berserkerGraphics, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(berserkerGraphics, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200
});
}
});
// Create rage particles
for (var i = 0; i < 12; i++) {
var particle = game.addChild(getFromPool('bloodParticle'));
particle.reset(self.x + (Math.random() - 0.5) * 80, self.y - 80 + (Math.random() - 0.5) * 80, 'spray');
var particleGraphics = particle.getChildAt(0);
particleGraphics.tint = 0xff4444; // Red rage particles
bloodParticles.push(particle);
}
// Broadcast rage to alert other enemies
groupAI.broadcastAlert(self, 'under_attack', 2);
};
// Override takeDamage to handle berserk damage bonus
var originalTakeDamage = self.takeDamage;
self.takeDamage = function (fromKnife) {
var damage = 1;
// Berserk berserkers deal damage back to attackers
if (self.isBerserk && !fromKnife) {
// Only counter-attack melee attacks, not knife throws
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
if (distanceToHero < 150) {
hero.takeDamage(); // Counter-attack
LK.effects.flashObject(self, 0xff0000, 300);
}
}
originalTakeDamage.call(self, fromKnife);
};
return self;
});
var Archer = Enemy.expand(function () {
var self = Enemy.call(this, 'archer');
var archerGraphics = self.attachAsset('enemyHunter', {
anchorX: 0.5,
anchorY: 1.0
});
archerGraphics.tint = 0x8844ff; // Purple tint for archers
self.enemyType = 'archer';
self.health = 2;
self.speed = 1.8;
self.shootCooldown = 0;
self.optimalRange = 400; // Preferred distance from hero
self.maxRange = 600; // Maximum shooting range
// Override update to include archer behavior
var originalUpdate = self.update;
self.update = function () {
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
// Archer positioning: maintain optimal distance
if (distanceToHero < self.optimalRange - 50) {
// Too close, move away
var dx = self.x - hero.x;
var dy = self.y - hero.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var newX = self.x + dx / distance * self.speed;
var newY = self.y + dy / distance * self.speed;
// Check collision before moving
var wouldCollide = false;
var nearbyEnemies = spatialGrid.getNearbyObjects(newX, newY, 80);
for (var i = 0; i < nearbyEnemies.length; i++) {
var otherEnemy = nearbyEnemies[i];
if (otherEnemy === self) continue;
var edx = newX - otherEnemy.x;
var edy = newY - otherEnemy.y;
var edistance = Math.sqrt(edx * edx + edy * edy);
if (edistance < 70) {
wouldCollide = true;
break;
}
}
if (!wouldCollide) {
// Keep within bounds
if (newX > 100 && newX < currentLevelData.width - 100) {
self.lastX = self.x;
self.x = newX;
}
if (newY > 1600 && newY < 2400) {
self.lastY = self.y;
self.y = newY;
}
archerGraphics.scaleX = dx > 0 ? 1 : -1; // Face away from hero
}
}
} else if (distanceToHero > self.optimalRange + 50) {
// Too far, move closer (use original enemy behavior)
originalUpdate.call(self);
}
// Shoot at hero if in range and cooldown is ready
if (distanceToHero <= self.maxRange && self.shootCooldown <= 0 && self.alerted) {
self.shootArrow();
}
if (self.shootCooldown > 0) {
self.shootCooldown--;
}
// Update attack cooldown from original
if (self.attackCooldown > 0) {
self.attackCooldown--;
}
};
self.shootArrow = function () {
// Create projectile
var arrow = game.addChild(new ArcherProjectile());
arrow.x = self.x;
arrow.y = self.y - 100;
// Calculate trajectory to hero
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var speed = 8;
arrow.velocityX = dx / distance * speed;
arrow.velocityY = dy / distance * speed;
}
bossProjectiles.push(arrow); // Reuse boss projectiles array
self.shootCooldown = 180; // 3 seconds
// Visual shooting effect
tween(archerGraphics, {
tint: 0xff4488,
scaleX: archerGraphics.scaleX * 1.2,
scaleY: 1.2
}, {
duration: 200,
onFinish: function onFinish() {
tween(archerGraphics, {
tint: 0x8844ff,
scaleX: archerGraphics.scaleX > 0 ? 1 : -1,
scaleY: 1
}, {
duration: 200
});
}
});
};
return self;
});
var EnemyWarning = Container.expand(function () {
var self = Container.call(this);
self.targetEnemy = null;
self.direction = 'left'; // 'left', 'right', 'up', 'down'
self.warningGraphics = null;
self.lastAlpha = 0;
self.isVisible = false;
self.setDirection = function (direction) {
if (self.warningGraphics) {
self.warningGraphics.destroy();
}
var assetName = 'warningArrow' + direction.charAt(0).toUpperCase() + direction.slice(1);
self.warningGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
self.direction = direction;
};
self.update = function () {
if (!self.targetEnemy || !self.warningGraphics) return;
// Check if enemy is still alive
var enemyExists = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i] === self.targetEnemy) {
enemyExists = true;
break;
}
}
if (!enemyExists) {
self.hide();
return;
}
// Calculate distance from hero to enemy
var distanceToHero = Math.sqrt(Math.pow(self.targetEnemy.x - hero.x, 2) + Math.pow(self.targetEnemy.y - hero.y, 2));
// Check if enemy is visible on screen
var enemyScreenX = self.targetEnemy.x - camera.x;
var enemyScreenY = self.targetEnemy.y - camera.y;
var isOnScreen = enemyScreenX >= -100 && enemyScreenX <= 2148 && enemyScreenY >= -100 && enemyScreenY <= 2832;
if (isOnScreen) {
self.hide();
return;
}
// Calculate warning opacity based on distance (closer = more visible)
var maxDistance = 800;
var minDistance = 300;
var targetAlpha = 0;
if (distanceToHero <= maxDistance) {
var normalizedDistance = Math.max(0, Math.min(1, (maxDistance - distanceToHero) / (maxDistance - minDistance)));
targetAlpha = normalizedDistance * 0.8;
}
// Smooth alpha transition
if (Math.abs(targetAlpha - self.lastAlpha) > 0.01) {
tween.stop(self.warningGraphics, {
alpha: true
});
tween(self.warningGraphics, {
alpha: targetAlpha
}, {
duration: 200
});
self.lastAlpha = targetAlpha;
}
// Position warning at screen edge
var screenCenterX = 1024;
var screenCenterY = 1366;
var dx = self.targetEnemy.x - hero.x;
var dy = self.targetEnemy.y - hero.y;
if (Math.abs(dx) > Math.abs(dy)) {
// Horizontal warning
if (dx > 0) {
self.setDirection('right');
self.x = screenCenterX + 900;
self.y = screenCenterY + Math.max(-600, Math.min(600, dy * 0.5));
} else {
self.setDirection('left');
self.x = screenCenterX - 900;
self.y = screenCenterY + Math.max(-600, Math.min(600, dy * 0.5));
}
} else {
// Vertical warning
if (dy > 0) {
self.setDirection('down');
self.x = screenCenterX + Math.max(-800, Math.min(800, dx * 0.5));
self.y = screenCenterY + 1200;
} else {
self.setDirection('up');
self.x = screenCenterX + Math.max(-800, Math.min(800, dx * 0.5));
self.y = screenCenterY - 1200;
}
}
};
self.hide = function () {
if (self.warningGraphics && self.warningGraphics.alpha > 0) {
tween.stop(self.warningGraphics, {
alpha: true
});
tween(self.warningGraphics, {
alpha: 0
}, {
duration: 300
});
self.lastAlpha = 0;
}
};
return self;
});
var HealthPotion = Container.expand(function () {
var self = Container.call(this);
var potionGraphics = self.attachAsset('healthPotion', {
anchorX: 0.5,
anchorY: 0.5
});
self.collectTimer = 0;
self.update = function () {
// Auto-collect after short delay
self.collectTimer++;
if (self.collectTimer > 30) {
// 0.5 seconds
self.collect();
}
// Gentle floating animation
potionGraphics.y = Math.sin(LK.ticks * 0.1) * 5;
potionGraphics.rotation += 0.05;
};
self.collect = function () {
LK.getSound('powerup').play();
hero.heal();
// Remove from healthPotions array
for (var i = healthPotions.length - 1; i >= 0; i--) {
if (healthPotions[i] === self) {
healthPotions.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
var Hero = Container.expand(function () {
var self = Container.call(this);
var heroGraphics = self.attachAsset('hero', {
anchorX: 0.5,
anchorY: 1.0
});
self.maxHealth = 5;
self.health = self.maxHealth;
self.isAttacking = false;
self.invulnerable = false;
self.damageBoost = false;
self.comboCount = 0;
self.slashCooldown = 0;
self.isSlashing = false;
self.slashGraphics = null;
self.lastX = 0;
self.lastY = 0;
self.isWalking = false;
self.walkAnimationActive = false;
self.walkAnimationFrame = 0;
self.walkAnimationTimer = 0;
self.walkAnimationSpeed = 10; // frames between texture changes
self.attack = function (targetX) {
if (self.isAttacking || self.slashCooldown > 0) return;
self.isAttacking = true;
self.isSlashing = true;
// Apply attack speed upgrade
var baseSlashCooldown = 24; // 0.4 seconds at 60fps
self.slashCooldown = Math.floor(baseSlashCooldown / (self.attackSpeedMultiplier || 1));
// Face direction of attack
if (targetX < self.x) {
heroGraphics.scaleX = -1;
} else {
heroGraphics.scaleX = 1;
}
// Replace hero graphic with slash animation
self.removeChild(heroGraphics);
self.slashGraphics = self.attachAsset('heroSlash', {
anchorX: 0.5,
anchorY: 1.0
});
// Match facing direction
self.slashGraphics.scaleX = heroGraphics.scaleX;
// Create weapon trail effect
var trail = game.addChild(getFromPool('weaponTrail'));
var trailType = self.damageBoost ? 'power' : 'normal';
trail.reset(self.x + heroGraphics.scaleX * 100, self.y - 80, heroGraphics.scaleX > 0 ? -0.3 : 0.3, 2, trailType);
weaponTrails.push(trail);
// Attack animation
tween(self.slashGraphics, {
scaleY: 1.2
}, {
duration: 100
});
tween(self.slashGraphics, {
scaleY: 1.0
}, {
duration: 100,
onFinish: function onFinish() {
// Return to normal hero graphic
self.removeChild(self.slashGraphics);
heroGraphics = self.attachAsset('hero', {
anchorX: 0.5,
anchorY: 1.0
});
// Maintain facing direction
heroGraphics.scaleX = self.slashGraphics.scaleX;
self.isAttacking = false;
self.isSlashing = false;
self.slashGraphics = null;
}
});
// Create sword slash effect
var effect = game.addChild(LK.getAsset('slashEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.9
}));
effect.x = self.x + heroGraphics.scaleX * 120;
effect.y = self.y - 80;
// Set slash rotation and scale based on direction
effect.rotation = heroGraphics.scaleX > 0 ? -0.3 : 0.3; // Diagonal slash
effect.scaleX = heroGraphics.scaleX > 0 ? 1 : -1; // Mirror for left attacks
tween(effect, {
scaleX: effect.scaleX * 4,
scaleY: 4,
alpha: 0
}, {
duration: 150,
onFinish: function onFinish() {
effect.destroy();
}
});
LK.getSound('slash').play();
};
self.takeDamage = function () {
if (self.invulnerable) return;
// Apply damage reduction
var damageReduction = self.damageReduction || 0;
if (Math.random() < damageReduction) {
// Damage reduced! Show visual effect
LK.effects.flashObject(self, 0x3498db, 300);
return;
}
self.health--;
self.comboCount = 0;
// Flash red when hit
LK.effects.flashObject(self, 0xff0000, 500);
// Screen shake when player is hit
triggerScreenShake(18, 400, tween.easeOut);
// Temporary invulnerability with upgrade bonus
self.invulnerable = true;
var invulnerabilityDuration = 1000 + (self.invulnerabilityBonus || 0);
LK.setTimeout(function () {
self.invulnerable = false;
}, invulnerabilityDuration);
LK.getSound('hit').play();
updateHealthDisplay();
if (self.health <= 0) {
LK.showGameOver();
}
};
self.heal = function () {
if (self.health < self.maxHealth) {
self.health++;
updateHealthDisplay();
}
};
self.addCombo = function () {
self.comboCount++;
};
self.startWalkAnimation = function () {
if (self.walkAnimationActive) return;
self.walkAnimationActive = true;
self.walkAnimationFrame = 0;
self.walkAnimationTimer = 0;
};
self.stopWalkAnimation = function () {
self.isWalking = false;
self.walkAnimationActive = false;
// Store current scale direction before removing graphics
var currentScaleX = heroGraphics.scaleX;
// Reset to default hero texture
self.removeChild(heroGraphics);
heroGraphics = self.attachAsset('hero', {
anchorX: 0.5,
anchorY: 1.0
});
// Maintain the current scale direction
if (currentScaleX < 0) {
heroGraphics.scaleX = -1;
}
};
self.move = function (direction) {
var speed = self.speed || 8;
var newX = self.x;
var newY = self.y;
var didMove = false;
if (direction === 'left' && self.x > 100) {
newX = self.x - speed;
heroGraphics.scaleX = -1;
didMove = true;
} else if (direction === 'right' && self.x < currentLevelData.width - 100) {
newX = self.x + speed;
heroGraphics.scaleX = 1;
didMove = true;
} else if (direction === 'up' && self.y > 1600) {
newY = self.y - speed;
didMove = true;
} else if (direction === 'down' && self.y < 2400) {
newY = self.y + speed;
didMove = true;
}
// Check collision with enemies before moving using spatial partitioning
var wouldCollide = false;
var nearbyEnemies = spatialGrid.getNearbyObjects(newX, newY, 150);
for (var i = 0; i < nearbyEnemies.length; i++) {
var enemy = nearbyEnemies[i];
var dx = newX - enemy.x;
var dy = newY - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 120) {
// Increased collision threshold to prevent overlap
wouldCollide = true;
break;
}
}
// Only move if no collision would occur
if (!wouldCollide && didMove) {
self.lastX = self.x;
self.lastY = self.y;
self.x = newX;
self.y = newY;
// Create dust clouds when moving (occasional)
if (Math.random() < 0.3) {
var dustCloud = game.addChild(getFromPool('dustCloud'));
dustCloud.reset(self.x + (Math.random() - 0.5) * 30, self.y - 10 + (Math.random() - 0.5) * 20, direction);
dustClouds.push(dustCloud);
}
// Start walking animation
if (!self.isWalking) {
self.isWalking = true;
self.startWalkAnimation();
}
// Update walking animation texture cycling
if (self.walkAnimationActive) {
self.walkAnimationTimer++;
if (self.walkAnimationTimer >= self.walkAnimationSpeed) {
self.walkAnimationTimer = 0;
self.walkAnimationFrame = (self.walkAnimationFrame + 1) % 4;
// Cycle through textures: hero, heroWalk1, heroWalk2, heroWalk3
var textureNames = ['hero', 'heroWalk1', 'heroWalk2', 'heroWalk3'];
// Store current scale direction before removing graphics
var currentScaleX = heroGraphics.scaleX;
// Remove current graphics and add new one with correct texture
self.removeChild(heroGraphics);
heroGraphics = self.attachAsset(textureNames[self.walkAnimationFrame], {
anchorX: 0.5,
anchorY: 1.0
});
// Maintain the current scale direction
if (currentScaleX < 0) {
heroGraphics.scaleX = -1;
}
}
}
}
// Update camera target
camera.targetX = self.x - 1024;
camera.targetY = self.y - 1366;
// Clamp camera to level bounds
camera.targetX = Math.max(0, Math.min(camera.targetX, currentLevelData.width - 2048));
camera.targetY = Math.max(0, Math.min(camera.targetY, currentLevelData.height - 2732));
};
self.update = function () {
if (self.slashCooldown > 0) {
self.slashCooldown--;
}
};
return self;
});
var ImpactSpark = Container.expand(function () {
var self = Container.call(this);
var sparkGraphics = self.attachAsset('bloodParticle', {
anchorX: 0.5,
anchorY: 0.5
});
sparkGraphics.tint = 0xffff44; // Yellow-white sparks
sparkGraphics.scaleX = 0.3;
sparkGraphics.scaleY = 0.3;
self.velocityX = 0;
self.velocityY = 0;
self.lifetime = 20; // Short-lived sparks
self.sparkType = 'normal'; // normal, bright, orange
self.reset = function (startX, startY, type) {
self.x = startX;
self.y = startY;
self.sparkType = type || 'normal';
// Random direction and speed for sparks
var angle = Math.random() * Math.PI * 2;
var speed = 3 + Math.random() * 5;
self.velocityX = Math.cos(angle) * speed;
self.velocityY = Math.sin(angle) * speed;
if (self.sparkType === 'bright') {
sparkGraphics.tint = 0xffffff; // White sparks
sparkGraphics.scaleX = 0.4;
sparkGraphics.scaleY = 0.4;
self.lifetime = 30;
} else if (self.sparkType === 'orange') {
sparkGraphics.tint = 0xff8844; // Orange sparks
sparkGraphics.scaleX = 0.2;
sparkGraphics.scaleY = 0.2;
self.lifetime = 15;
} else {
sparkGraphics.tint = 0xffff44; // Yellow sparks
sparkGraphics.scaleX = 0.3;
sparkGraphics.scaleY = 0.3;
self.lifetime = 20;
}
sparkGraphics.alpha = 1;
self.visible = true;
};
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
// Sparks slow down over time
self.velocityX *= 0.95;
self.velocityY *= 0.95;
// Fade out quickly
self.lifetime--;
sparkGraphics.alpha = self.lifetime / 20;
if (self.lifetime <= 0) {
// Remove from impactSparks array
for (var i = impactSparks.length - 1; i >= 0; i--) {
if (impactSparks[i] === self) {
impactSparks.splice(i, 1);
break;
}
}
returnToPool(self, 'impactSpark');
}
};
return self;
});
var Knife = Container.expand(function () {
var self = Container.call(this);
var knifeGraphics = self.attachAsset('knife', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 15;
self.direction = 1; // 1 for right, -1 for left
self.routePoints = []; // Points to follow along the route
self.currentRouteIndex = 0; // Current target point index
self.velocityX = 0; // Velocity for precise targeting
self.velocityY = 0; // Velocity for precise targeting
self.lastX = 0;
self.lastY = 0;
self.setRoute = function (routePoints) {
self.routePoints = routePoints;
self.currentRouteIndex = 0;
};
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
// Follow route if available
if (self.routePoints.length > 0 && self.currentRouteIndex < self.routePoints.length) {
var targetPoint = self.routePoints[self.currentRouteIndex];
var dx = targetPoint.x - self.x;
var dy = targetPoint.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 20) {
// Close enough to current target, move to next point
self.currentRouteIndex++;
} else {
// Move toward current target point
if (distance > 0) {
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
// Calculate rotation angle to face target direction
var angle = Math.atan2(dy, dx);
knifeGraphics.rotation = angle;
}
}
} else if (self.velocityX !== 0 || self.velocityY !== 0) {
// Use velocity if set, otherwise use direction-based movement
self.x += self.velocityX;
self.y += self.velocityY;
// Calculate rotation for velocity-based movement
var angle = Math.atan2(self.velocityY, self.velocityX);
knifeGraphics.rotation = angle;
} else {
self.x += self.speed * self.direction;
self.y -= 2; // Slight upward arc
// Calculate rotation for direction-based movement
var angle = Math.atan2(-2, self.speed * self.direction);
knifeGraphics.rotation = angle;
}
// Check if knife went off screen
if (self.x < -100 || self.x > currentLevelData.width + 100 || self.y < -100 || self.y > 2900) {
self.destroy();
// Remove from knives array
for (var i = knives.length - 1; i >= 0; i--) {
if (knives[i] === self) {
knives.splice(i, 1);
break;
}
}
}
// Check collision with enemies (hit enemy center)
var penetration = upgradeTree ? upgradeTree.getKnifePenetration() : 0;
var enemiesHit = 0;
var maxEnemiesHit = 1 + penetration;
for (var i = 0; i < enemies.length && enemiesHit < maxEnemiesHit; i++) {
var enemy = enemies[i];
var dx = self.x - enemy.x;
var dy = self.y - (enemy.y - 70); // Target enemy center
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 50) {
// Hit enemy center - deal damage with upgrade multiplier
var damage = Math.floor(hero.damageMultiplier || 1);
for (var d = 0; d < damage; d++) {
enemy.takeDamage(true);
}
// Light screen shake on knife impact
triggerScreenShake(4, 100, tween.easeOut);
enemiesHit++;
// If no penetration or hit max enemies, destroy knife
if (penetration === 0 || enemiesHit >= maxEnemiesHit) {
self.destroy();
// Remove from knives array
for (var j = knives.length - 1; j >= 0; j--) {
if (knives[j] === self) {
knives.splice(j, 1);
break;
}
}
return; // Exit update immediately to prevent further movement or collision checks
}
}
}
};
return self;
});
var MagicalEffect = Container.expand(function () {
var self = Container.call(this);
var effectGraphics = self.attachAsset('bloodParticle', {
anchorX: 0.5,
anchorY: 0.5
});
self.velocityX = 0;
self.velocityY = 0;
self.lifetime = 60;
self.effectType = 'sparkle'; // sparkle, healing, power, shield
self.rotationSpeed = 0;
self.reset = function (startX, startY, type) {
self.x = startX;
self.y = startY;
self.effectType = type || 'sparkle';
if (self.effectType === 'sparkle') {
effectGraphics.tint = 0xffff88; // Golden sparkle
effectGraphics.scaleX = 0.6;
effectGraphics.scaleY = 0.6;
var angle = Math.random() * Math.PI * 2;
var speed = 2 + Math.random() * 3;
self.velocityX = Math.cos(angle) * speed;
self.velocityY = Math.sin(angle) * speed - 2; // Float upward
self.rotationSpeed = (Math.random() - 0.5) * 0.2;
self.lifetime = 80;
} else if (self.effectType === 'healing') {
effectGraphics.tint = 0x44ff44; // Green healing
effectGraphics.scaleX = 0.8;
effectGraphics.scaleY = 0.8;
self.velocityX = (Math.random() - 0.5) * 2;
self.velocityY = -3 - Math.random() * 2; // Rise upward
self.rotationSpeed = 0.1;
self.lifetime = 60;
} else if (self.effectType === 'power') {
effectGraphics.tint = 0xff4488; // Purple power
effectGraphics.scaleX = 1.0;
effectGraphics.scaleY = 1.0;
self.velocityX = (Math.random() - 0.5) * 4;
self.velocityY = -1 - Math.random() * 3;
self.rotationSpeed = 0.15;
self.lifetime = 70;
} else if (self.effectType === 'shield') {
effectGraphics.tint = 0x44ddff; // Blue shield
effectGraphics.scaleX = 0.5;
effectGraphics.scaleY = 0.5;
var angle = Math.random() * Math.PI * 2;
var radius = 50 + Math.random() * 30;
self.velocityX = Math.cos(angle) * 2;
self.velocityY = Math.sin(angle) * 2;
self.rotationSpeed = 0.08;
self.lifetime = 90;
}
effectGraphics.alpha = 1;
self.visible = true;
};
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
effectGraphics.rotation += self.rotationSpeed;
// Different behaviors per effect type
if (self.effectType === 'sparkle') {
self.velocityY -= 0.05; // Continue floating up
} else if (self.effectType === 'healing') {
self.velocityX *= 0.99; // Slow down horizontally
} else if (self.effectType === 'power') {
// Pulsing effect
var pulseScale = 1.0 + Math.sin(LK.ticks * 0.3) * 0.2;
effectGraphics.scaleX = pulseScale;
effectGraphics.scaleY = pulseScale;
}
// Fade out over time
self.lifetime--;
var maxLifetime = 60;
if (self.effectType === 'sparkle') maxLifetime = 80;else if (self.effectType === 'power') maxLifetime = 70;else if (self.effectType === 'shield') maxLifetime = 90;
effectGraphics.alpha = self.lifetime / maxLifetime;
if (self.lifetime <= 0) {
// Remove from magicalEffects array
for (var i = magicalEffects.length - 1; i >= 0; i--) {
if (magicalEffects[i] === self) {
magicalEffects.splice(i, 1);
break;
}
}
returnToPool(self, 'magicalEffect');
}
};
return self;
});
var Minion = Container.expand(function () {
var self = Container.call(this);
var minionGraphics = self.attachAsset('enemyFast', {
anchorX: 0.5,
anchorY: 1.0
});
minionGraphics.tint = 0x9b59b6; // Purple tint for minions
minionGraphics.scaleX = 0.8; // Smaller than normal enemies
minionGraphics.scaleY = 0.8;
self.health = 1;
self.speed = 3.5;
self.attackCooldown = 0;
self.lastX = 0;
self.lastY = 0;
self.summoner = null; // Reference to summoning boss
self.lifetime = 1800; // 30 seconds lifetime
self.update = function () {
self.lifetime--;
if (self.lifetime <= 0) {
self.die();
return;
}
var distanceToHero = Math.sqrt(Math.pow(self.x - hero.x, 2) + Math.pow(self.y - hero.y, 2));
// Aggressive AI - always chase hero
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Check collision with other enemies before moving
var wouldCollide = false;
var newX = self.x + dx / distance * self.speed;
var newY = self.y + dy / distance * self.speed;
var nearbyEnemies = spatialGrid.getNearbyObjects(newX, newY, 100);
for (var i = 0; i < nearbyEnemies.length; i++) {
var otherEnemy = nearbyEnemies[i];
if (otherEnemy === self) continue;
var edx = newX - otherEnemy.x;
var edy = newY - otherEnemy.y;
var edistance = Math.sqrt(edx * edx + edy * edy);
if (edistance < 60) {
// Smaller collision radius for minions
wouldCollide = true;
break;
}
}
if (!wouldCollide) {
self.lastX = self.x;
self.lastY = self.y;
self.x = newX;
self.y = newY;
}
minionGraphics.scaleX = dx > 0 ? 0.8 : -0.8;
}
// Attack hero if close enough
if (distanceToHero < 80 && self.attackCooldown <= 0) {
hero.takeDamage();
self.attackCooldown = 90; // 1.5 seconds
}
if (self.attackCooldown > 0) {
self.attackCooldown--;
}
};
self.takeDamage = function (fromKnife) {
self.health--;
LK.effects.flashObject(self, 0xffffff, 200);
// Create smaller blood particles
for (var p = 0; p < 8; p++) {
var bloodParticle = game.addChild(getFromPool('bloodParticle'));
bloodParticle.reset(self.x + (Math.random() - 0.5) * 30, self.y - 40 + (Math.random() - 0.5) * 30);
bloodParticles.push(bloodParticle);
}
if (self.health <= 0) {
self.die();
}
};
self.die = function () {
// Minions give 2 coins
upgradeTree.addCoins(2);
// Drop small coin
var coin = game.addChild(new Coin());
coin.x = self.x;
coin.y = self.y;
var coinGraphics = coin.getChildAt(0);
coinGraphics.scaleX = 0.7; // Smaller coin
coinGraphics.scaleY = 0.7;
coins.push(coin);
// Reduce summoner's minion count
if (self.summoner && self.summoner.minionCount) {
self.summoner.minionCount--;
}
// Small score bonus
LK.setScore(LK.getScore() + 5);
hero.addCombo();
// Remove from enemies array
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
self.destroy();
updateScoreDisplay();
updateEnemiesLeftDisplay();
};
return self;
});
var PowerUp = Container.expand(function () {
var self = Container.call(this);
var powerupGraphics = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5
});
self.type = 'health'; // 'health', 'invulnerable', 'damage'
self.lifetime = 600; // 10 seconds
self.update = function () {
self.lifetime--;
if (self.lifetime <= 0) {
self.expire();
}
// Check collision with hero
if (self.intersects(hero)) {
self.collect();
}
// Pulse animation
var scale = 1 + Math.sin(LK.ticks * 0.2) * 0.2;
powerupGraphics.scaleX = scale;
powerupGraphics.scaleY = scale;
};
self.collect = function () {
LK.getSound('powerup').play();
// Create magical effects based on power-up type
var effectType = 'sparkle';
if (self.type === 'health') {
hero.heal();
effectType = 'healing';
} else if (self.type === 'invulnerable') {
hero.invulnerable = true;
effectType = 'shield';
LK.setTimeout(function () {
hero.invulnerable = false;
}, 5000);
} else if (self.type === 'damage') {
hero.damageBoost = true;
effectType = 'power';
LK.setTimeout(function () {
hero.damageBoost = false;
}, 5000);
}
// Create magical particle effects
for (var e = 0; e < 12; e++) {
var magicalEffect = game.addChild(getFromPool('magicalEffect'));
magicalEffect.reset(self.x + (Math.random() - 0.5) * 60, self.y + (Math.random() - 0.5) * 60, effectType);
magicalEffects.push(magicalEffect);
}
// Remove from powerups array
for (var i = powerups.length - 1; i >= 0; i--) {
if (powerups[i] === self) {
powerups.splice(i, 1);
break;
}
}
self.destroy();
};
self.expire = function () {
// Remove from powerups array
for (var i = powerups.length - 1; i >= 0; i--) {
if (powerups[i] === self) {
powerups.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
var RouteEffect = Container.expand(function () {
var self = Container.call(this);
self.routePoints = [];
self.routePositions = []; // Store just the x,y coordinates for knife to follow
self.targetEnemy = null;
self.createRoute = function (enemy) {
self.targetEnemy = enemy;
self.clearRoute();
// Calculate direct path from hero center to enemy center
var heroStartX = hero.x;
var heroStartY = hero.y - 80; // Middle of hero asset
var enemyMiddleX = enemy.x;
var enemyMiddleY = enemy.y - 70; // Middle of enemy asset (enemy height is 140, so middle is -70 from bottom)
var dx = enemyMiddleX - heroStartX;
var dy = enemyMiddleY - heroStartY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Create route points along the path
var numPoints = Math.floor(distance / 50); // Point every 50 pixels
for (var i = 0; i <= numPoints; i++) {
var t = i / numPoints;
var x = heroStartX + dx * t;
var y = heroStartY + dy * t;
var routePoint = self.addChild(LK.getAsset('routeLine', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
}));
routePoint.x = x;
routePoint.y = y;
self.routePoints.push(routePoint);
// Store position for knife to follow
self.routePositions.push({
x: x,
y: y
});
// Animate route points appearing with delay
tween(routePoint, {
alpha: 0.8,
scaleX: 2,
scaleY: 2
}, {
duration: 100 + i * 20
});
}
// Add impact effect at enemy position
var impactEffect = self.addChild(LK.getAsset('routeEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}));
impactEffect.x = enemyMiddleX;
impactEffect.y = enemyMiddleY;
self.routePoints.push(impactEffect);
// Animate impact effect
tween(impactEffect, {
alpha: 1,
scaleX: 3,
scaleY: 3
}, {
duration: 300,
easing: tween.easeOut
});
// Remove route after 0.5 seconds
LK.setTimeout(function () {
self.destroy();
// Remove from routeEffects array
for (var i = routeEffects.length - 1; i >= 0; i--) {
if (routeEffects[i] === self) {
routeEffects.splice(i, 1);
break;
}
}
}, 500);
}
};
self.clearRoute = function () {
for (var i = self.routePoints.length - 1; i >= 0; i--) {
self.routePoints[i].destroy();
}
self.routePoints = [];
};
self.fadeOut = function () {
for (var i = 0; i < self.routePoints.length; i++) {
tween(self.routePoints[i], {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 200
});
}
LK.setTimeout(function () {
self.destroy();
}, 300);
};
return self;
});
var ScreenShake = Container.expand(function () {
var self = Container.call(this);
self.intensity = 0;
self.duration = 0;
self.remainingTime = 0;
self.offsetX = 0;
self.offsetY = 0;
self.isActive = false;
self.decayEasing = tween.easeOut;
self.shake = function (intensity, duration, easing) {
self.intensity = intensity || 10;
self.duration = duration || 500;
self.remainingTime = self.duration;
self.isActive = true;
self.decayEasing = easing || tween.easeOut;
};
self.update = function () {
if (!self.isActive) {
self.offsetX = 0;
self.offsetY = 0;
return;
}
self.remainingTime -= 16.67; // Approximately 1 frame at 60fps
if (self.remainingTime <= 0) {
self.isActive = false;
self.offsetX = 0;
self.offsetY = 0;
return;
}
// Calculate decay factor using easing function
var progress = 1 - self.remainingTime / self.duration;
var decayFactor = 1 - self.decayEasing(progress);
// Generate random shake offset
var currentIntensity = self.intensity * decayFactor;
self.offsetX = (Math.random() - 0.5) * 2 * currentIntensity;
self.offsetY = (Math.random() - 0.5) * 2 * currentIntensity;
};
self.getOffsetX = function () {
return self.offsetX;
};
self.getOffsetY = function () {
return self.offsetY;
};
return self;
});
var UpgradeMenu = Container.expand(function () {
var self = Container.call(this);
self.isVisible = false;
self.upgradeTree = null;
self.upgradeButtons = [];
self.categoryHeaders = [];
self.background = null;
self.show = function (upgradeTree) {
self.upgradeTree = upgradeTree;
self.isVisible = true;
self.x = 0;
self.y = 0;
self.createInterface();
// Animate menu appearance
self.alpha = 0;
self.scaleX = 0.8;
self.scaleY = 0.8;
tween(self, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
};
self.hide = function () {
tween(self, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.isVisible = false;
self.clearInterface();
}
});
};
self.createInterface = function () {
// Create background centered on screen
self.background = self.addChild(LK.getAsset('backgroundTile', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x2c3e50,
alpha: 0.95,
scaleX: 10,
scaleY: 12
}));
self.background.x = 0;
self.background.y = 0;
// Title
var title = new Text2('UPGRADE TREE', {
size: 100,
fill: '#FFD700'
});
title.anchor.set(0.5, 0.5);
title.x = 0;
title.y = -1000;
self.addChild(title);
// Coins display
var coinsText = new Text2('Coins: ' + self.upgradeTree.coins, {
size: 60,
fill: '#F1C40F'
});
coinsText.anchor.set(0.5, 0.5);
coinsText.x = 0;
coinsText.y = -900;
self.addChild(coinsText);
self.coinsText = coinsText;
// Combat category
self.createCategory('COMBAT', -600, [{
type: 'damage',
name: 'Damage Boost',
desc: '+20% damage per level'
}, {
type: 'attackSpeed',
name: 'Attack Speed',
desc: '+25% attack speed per level'
}, {
type: 'knifePenetration',
name: 'Knife Penetration',
desc: 'Knives hit multiple enemies'
}]);
// Defense category
self.createCategory('DEFENSE', -100, [{
type: 'maxHealth',
name: 'Max Health',
desc: '+1 max health per level'
}, {
type: 'damageReduction',
name: 'Damage Reduction',
desc: '+15% damage reduction per level'
}, {
type: 'invulnerability',
name: 'Invulnerability',
desc: '+1 second invulnerability per level'
}]);
// Utility category
self.createCategory('UTILITY', 400, [{
type: 'moveSpeed',
name: 'Move Speed',
desc: '+25% movement speed per level'
}, {
type: 'startingKnives',
name: 'Starting Knives',
desc: '+2 starting knives per level'
}, {
type: 'coinMagnetism',
name: 'Coin Magnetism',
desc: 'Increased coin collection range'
}]);
// Close button
var closeButton = self.addChild(LK.getAsset('backgroundTile', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xe74c3c,
scaleX: 2,
scaleY: 1
}));
closeButton.x = 0;
closeButton.y = 1000;
var closeText = new Text2('CLOSE', {
size: 60,
fill: '#FFFFFF'
});
closeText.anchor.set(0.5, 0.5);
closeText.x = 0;
closeText.y = 1000;
self.addChild(closeText);
closeButton.down = function () {
self.hide();
};
};
self.createCategory = function (categoryName, startY, upgrades) {
// Category header
var header = new Text2(categoryName, {
size: 80,
fill: '#3498db'
});
header.anchor.set(0.5, 0.5);
header.x = 0;
header.y = startY;
self.addChild(header);
// Upgrade buttons
for (var i = 0; i < upgrades.length; i++) {
var upgrade = upgrades[i];
var buttonY = startY + 100 + i * 120;
self.createUpgradeButton(upgrade, buttonY);
}
};
self.createUpgradeButton = function (upgrade, y) {
var currentLevel = self.upgradeTree.upgrades[upgrade.type];
var maxLevel = self.upgradeTree.upgradeCosts[upgrade.type].length;
var canUpgrade = self.upgradeTree.canUpgrade(upgrade.type);
var cost = currentLevel < maxLevel ? self.upgradeTree.upgradeCosts[upgrade.type][currentLevel] : 0;
// Button background
var buttonColor = currentLevel >= maxLevel ? 0x27ae60 : canUpgrade ? 0x3498db : 0x7f8c8d;
var button = self.addChild(LK.getAsset('backgroundTile', {
anchorX: 0.5,
anchorY: 0.5,
tint: buttonColor,
scaleX: 8,
scaleY: 0.8
}));
button.x = 0;
button.y = y;
// Upgrade name
var nameText = new Text2(upgrade.name, {
size: 50,
fill: '#FFFFFF'
});
nameText.anchor.set(0.5, 0.5);
nameText.x = -424;
nameText.y = y - 15;
self.addChild(nameText);
// Level display
var levelText = new Text2('Level: ' + currentLevel + '/' + maxLevel, {
size: 40,
fill: '#BDC3C7'
});
levelText.anchor.set(0.5, 0.5);
levelText.x = -424;
levelText.y = y + 15;
self.addChild(levelText);
// Cost display
if (currentLevel < maxLevel) {
var costText = new Text2('Cost: ' + cost, {
size: 40,
fill: canUpgrade ? '#F1C40F' : '#E74C3C'
});
costText.anchor.set(0.5, 0.5);
costText.x = 376;
costText.y = y;
self.addChild(costText);
} else {
var maxText = new Text2('MAX', {
size: 40,
fill: '#27AE60'
});
maxText.anchor.set(0.5, 0.5);
maxText.x = 376;
maxText.y = y;
self.addChild(maxText);
}
// Purchase functionality
if (canUpgrade) {
button.down = function () {
if (self.upgradeTree.purchaseUpgrade(upgrade.type)) {
// Refresh interface
self.clearInterface();
self.createInterface();
// Success effect
tween(button, {
tint: 0x27ae60,
scaleX: 9,
scaleY: 0.9
}, {
duration: 200,
onFinish: function onFinish() {
tween(button, {
tint: buttonColor,
scaleX: 8,
scaleY: 0.8
}, {
duration: 200
});
}
});
}
};
}
};
self.clearInterface = function () {
// Remove all children
while (self.children.length > 0) {
self.children[0].destroy();
}
self.upgradeButtons = [];
self.categoryHeaders = [];
self.background = null;
};
return self;
});
var UpgradeTree = Container.expand(function () {
var self = Container.call(this);
// Initialize upgrade data from storage
self.upgrades = storage.upgrades || {
// Combat upgrades
damage: 0,
// 0-5: +20% damage per level
attackSpeed: 0,
// 0-3: +25% attack speed per level
knifePenetration: 0,
// 0-3: knives can hit multiple enemies
// Defense upgrades
maxHealth: 0,
// 0-5: +1 max health per level
damageReduction: 0,
// 0-3: +15% damage reduction per level
invulnerability: 0,
// 0-2: +1 second invulnerability per level
// Utility upgrades
moveSpeed: 0,
// 0-3: +25% movement speed per level
startingKnives: 0,
// 0-4: +2 starting knives per level
coinMagnetism: 0 // 0-3: increased coin collection range per level
};
// Upgrade costs (coins required)
self.upgradeCosts = {
damage: [100, 200, 400, 800, 1600],
attackSpeed: [150, 300, 600],
knifePenetration: [200, 500, 1000],
maxHealth: [80, 160, 320, 640, 1280],
damageReduction: [120, 240, 480],
invulnerability: [300, 600],
moveSpeed: [100, 200, 400],
startingKnives: [150, 300, 600, 1200],
coinMagnetism: [100, 250, 500]
};
self.coins = storage.coins || 0;
self.canUpgrade = function (upgradeType) {
var currentLevel = self.upgrades[upgradeType];
var maxLevel = self.upgradeCosts[upgradeType].length;
if (currentLevel >= maxLevel) return false;
return self.coins >= self.upgradeCosts[upgradeType][currentLevel];
};
self.purchaseUpgrade = function (upgradeType) {
if (!self.canUpgrade(upgradeType)) return false;
var currentLevel = self.upgrades[upgradeType];
var cost = self.upgradeCosts[upgradeType][currentLevel];
self.coins -= cost;
self.upgrades[upgradeType]++;
// Save to storage
storage.upgrades = self.upgrades;
storage.coins = self.coins;
return true;
};
self.addCoins = function (amount) {
self.coins += amount;
storage.coins = self.coins;
};
// Get upgrade bonuses
self.getDamageMultiplier = function () {
return 1 + self.upgrades.damage * 0.2;
};
self.getAttackSpeedMultiplier = function () {
return 1 + self.upgrades.attackSpeed * 0.25;
};
self.getKnifePenetration = function () {
return self.upgrades.knifePenetration;
};
self.getMaxHealthBonus = function () {
return self.upgrades.maxHealth;
};
self.getDamageReduction = function () {
return self.upgrades.damageReduction * 0.15;
};
self.getInvulnerabilityBonus = function () {
return self.upgrades.invulnerability * 1000; // milliseconds
};
self.getMoveSpeedMultiplier = function () {
return 1 + self.upgrades.moveSpeed * 0.25;
};
self.getStartingKnivesBonus = function () {
return self.upgrades.startingKnives * 2;
};
self.getCoinMagnetismRange = function () {
return 100 + self.upgrades.coinMagnetism * 50;
};
return self;
});
var WeaponTrail = Container.expand(function () {
var self = Container.call(this);
var trailGraphics = self.attachAsset('slashEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.6,
tint: 0xFFFFFF
});
self.lifetime = 30; // Short trail life
self.maxLifetime = 30;
self.reset = function (startX, startY, rotation, scale, trailType) {
self.x = startX;
self.y = startY;
self.lifetime = self.maxLifetime;
trailGraphics.rotation = rotation;
trailGraphics.scaleX = scale;
trailGraphics.scaleY = scale * 0.5; // Flatter trail
trailGraphics.alpha = 0.6;
self.visible = true;
// Different trail effects based to type
if (trailType === 'critical') {
trailGraphics.tint = 0xFFD700; // Golden trail for critical hits
trailGraphics.scaleY = scale * 0.8; // Thicker trail
} else if (trailType === 'power') {
trailGraphics.tint = 0xFF4488; // Purple trail for powered attacks
} else {
trailGraphics.tint = 0xFFFFFF; // White trail for normal
}
};
self.update = function () {
self.lifetime--;
// Fade and shrink over time
var lifeRatio = self.lifetime / self.maxLifetime;
trailGraphics.alpha = 0.6 * lifeRatio;
trailGraphics.scaleX *= 0.98;
trailGraphics.scaleY *= 0.96;
if (self.lifetime <= 0) {
// Remove from weaponTrails array
for (var i = weaponTrails.length - 1; i >= 0; i--) {
if (weaponTrails[i] === self) {
weaponTrails.splice(i, 1);
break;
}
}
returnToPool(self, 'weaponTrail');
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2c3e50,
title: 'Dungeon Crawler'
});
/****
* Game Code
****/
// Legacy Boss class for backward compatibility (delegates to BossType1)
// Game variables
var Boss = BossType1;
// Upgrade system
var upgradeTree = new UpgradeTree();
var upgradeMenu = new UpgradeMenu();
var showUpgradesButton = null;
// Visual effect functions
function createGlowOutline(target, color, intensity) {
var outline = target.addChild(LK.getAsset('slashEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
tint: color,
scaleX: 1.2,
scaleY: 1.5
}));
outline.x = 0;
outline.y = -70; // Center on enemy
target.glowOutline = outline;
tween(outline, {
alpha: intensity * 0.4,
scaleX: 1.4,
scaleY: 1.8
}, {
duration: 300,
easing: tween.easeInOut
});
// Pulse effect
function pulseGlow() {
if (outline.parent) {
tween(outline, {
alpha: intensity * 0.2
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (outline.parent) {
tween(outline, {
alpha: intensity * 0.4
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: pulseGlow
});
}
}
});
}
}
pulseGlow();
}
function removeGlowOutline(target) {
if (target.glowOutline) {
tween(target.glowOutline, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
if (target.glowOutline) {
target.glowOutline.destroy();
target.glowOutline = null;
}
}
});
}
}
function createScreenFlash(color, duration, intensity) {
var flashOverlay = LK.gui.center.addChild(LK.getAsset('backgroundTile', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
tint: color,
scaleX: 20,
scaleY: 20
}));
flashOverlay.x = 0;
flashOverlay.y = 0;
tween(flashOverlay, {
alpha: intensity || 0.3
}, {
duration: duration * 0.2,
easing: tween.easeIn,
onFinish: function onFinish() {
tween(flashOverlay, {
alpha: 0
}, {
duration: duration * 0.8,
easing: tween.easeOut,
onFinish: function onFinish() {
flashOverlay.destroy();
}
});
}
});
}
var screenShake = new ScreenShake();
// Helper function to trigger screen shake with different intensities
function triggerScreenShake(intensity, duration, easing) {
screenShake.shake(intensity, duration, easing);
}
var hero;
var enemies = [];
var coins = [];
var powerups = [];
var enemyWarnings = [];
var knives = [];
var knivesRemaining = 5;
var routeEffects = [];
var bloodParticles = [];
var bossProjectiles = [];
var impactSparks = [];
var dustClouds = [];
var magicalEffects = [];
var currentDungeon = 1;
var dungeonComplete = false;
var hearts = [];
var healthPotions = [];
var damageNumbers = [];
var weaponTrails = [];
// Object pooling system
var objectPools = {
bloodParticle: [],
bossProjectile: [],
coin: [],
healthPotion: [],
impactSpark: [],
dustCloud: [],
magicalEffect: [],
damageNumber: [],
weaponTrail: []
};
// Spatial partitioning system
var spatialGrid = {
cellSize: 200,
grid: {},
clear: function clear() {
this.grid = {};
},
getCellKey: function getCellKey(x, y) {
var cellX = Math.floor(x / this.cellSize);
var cellY = Math.floor(y / this.cellSize);
return cellX + ',' + cellY;
},
addObject: function addObject(obj, x, y) {
var key = this.getCellKey(x, y);
if (!this.grid[key]) {
this.grid[key] = [];
}
this.grid[key].push(obj);
},
getNearbyObjects: function getNearbyObjects(x, y, radius) {
var nearby = [];
var cellRadius = Math.ceil(radius / this.cellSize);
var centerCellX = Math.floor(x / this.cellSize);
var centerCellY = Math.floor(y / this.cellSize);
for (var dx = -cellRadius; dx <= cellRadius; dx++) {
for (var dy = -cellRadius; dy <= cellRadius; dy++) {
var key = centerCellX + dx + ',' + (centerCellY + dy);
if (this.grid[key]) {
nearby = nearby.concat(this.grid[key]);
}
}
}
return nearby;
}
};
// Group AI communication system
var groupAI = {
communications: [],
// Store recent communications
alertRadius: 400,
// Radius for alert propagation
flankingCoordination: [],
// Store flanking coordination data
leaderBuffs: [],
// Store active leader buffs
packHunters: [],
// Track pack hunting groups
// Communicate alert state between enemies
broadcastAlert: function broadcastAlert(enemy, alertType, priority) {
var communication = {
source: enemy,
type: alertType,
// 'spotted_hero', 'under_attack', 'flanking_position', 'retreat'
priority: priority || 1,
timestamp: LK.ticks,
x: enemy.x,
y: enemy.y
};
this.communications.push(communication);
// Clean old communications (older than 3 seconds)
for (var i = this.communications.length - 1; i >= 0; i--) {
if (LK.ticks - this.communications[i].timestamp > 180) {
this.communications.splice(i, 1);
}
}
},
// Get recent communications within range
getCommunications: function getCommunications(enemy, maxAge, maxDistance) {
var relevant = [];
maxAge = maxAge || 120; // 2 seconds default
maxDistance = maxDistance || this.alertRadius;
for (var i = 0; i < this.communications.length; i++) {
var comm = this.communications[i];
if (LK.ticks - comm.timestamp <= maxAge && comm.source !== enemy) {
var distance = Math.sqrt(Math.pow(enemy.x - comm.x, 2) + Math.pow(enemy.y - comm.y, 2));
if (distance <= maxDistance) {
relevant.push(comm);
}
}
}
return relevant;
},
// Calculate optimal flanking positions
calculateFlankingPositions: function calculateFlankingPositions(heroX, heroY) {
var positions = [];
var angles = [Math.PI * 0.25, Math.PI * 0.75, Math.PI * 1.25, Math.PI * 1.75]; // 45, 135, 225, 315 degrees
var distance = 300;
for (var i = 0; i < angles.length; i++) {
var x = heroX + Math.cos(angles[i]) * distance;
var y = heroY + Math.sin(angles[i]) * distance;
// Keep within bounds
x = Math.max(100, Math.min(x, currentLevelData.width - 100));
y = Math.max(1600, Math.min(y, 2400));
positions.push({
x: x,
y: y,
angle: angles[i],
occupied: false
});
}
return positions;
},
// Assign flanking position to enemy
assignFlankingPosition: function assignFlankingPosition(enemy) {
var positions = this.calculateFlankingPositions(hero.x, hero.y);
var assigned = null;
// Find closest unoccupied position
var minDistance = Infinity;
for (var i = 0; i < positions.length; i++) {
if (!positions[i].occupied) {
var distance = Math.sqrt(Math.pow(enemy.x - positions[i].x, 2) + Math.pow(enemy.y - positions[i].y, 2));
if (distance < minDistance) {
minDistance = distance;
assigned = positions[i];
}
}
}
if (assigned) {
assigned.occupied = true;
enemy.flankingTarget = {
x: assigned.x,
y: assigned.y
};
enemy.isAssignedFlanker = true;
}
}
};
function getFromPool(type) {
if (objectPools[type] && objectPools[type].length > 0) {
var obj = objectPools[type].pop();
obj.visible = true;
return obj;
}
// Create new object if pool is empty
if (type === 'bloodParticle') {
return new BloodParticle();
} else if (type === 'bossProjectile') {
return new BossProjectile();
} else if (type === 'coin') {
return new Coin();
} else if (type === 'healthPotion') {
return new HealthPotion();
} else if (type === 'impactSpark') {
return new ImpactSpark();
} else if (type === 'dustCloud') {
return new DustCloud();
} else if (type === 'magicalEffect') {
return new MagicalEffect();
} else if (type === 'damageNumber') {
return new DamageNumber();
} else if (type === 'weaponTrail') {
return new WeaponTrail();
}
return null;
}
function returnToPool(obj, type) {
if (!objectPools[type]) {
objectPools[type] = [];
}
obj.visible = false;
obj.x = -1000; // Move off screen
obj.y = -1000;
objectPools[type].push(obj);
}
// Movement state tracking
var movementState = {
left: false,
right: false,
up: false,
down: false
};
// Ensure movement state is properly initialized
function resetMovementState() {
movementState.left = false;
movementState.right = false;
movementState.up = false;
movementState.down = false;
}
// Initialize clean movement state
resetMovementState();
// Camera system
var camera = {
x: 0,
y: 0,
targetX: 0,
targetY: 0,
smoothing: 0.1
};
// Dungeon configuration
var levels = [{
enemies: [{
type: 'basic',
count: 3
}, {
type: 'scout',
count: 1
}],
width: 4096,
height: 2732
}, {
enemies: [{
type: 'basic',
count: 4
}, {
type: 'strong',
count: 2
}, {
type: 'scout',
count: 1
}, {
type: 'archer',
count: 1
}],
width: 5120,
height: 2732
}, {
enemies: [{
type: 'basic',
count: 5
}, {
type: 'strong',
count: 2
}, {
type: 'fast',
count: 2
}, {
type: 'scout',
count: 2
}, {
type: 'archer',
count: 1
}, {
type: 'berserker',
count: 1
}],
width: 6144,
height: 2732
}, {
enemies: [{
type: 'basic',
count: 4
}, {
type: 'strong',
count: 3
}, {
type: 'fast',
count: 2
}, {
type: 'tank',
count: 1
}, {
type: 'scout',
count: 2
}, {
type: 'archer',
count: 2
}, {
type: 'berserker',
count: 1
}, {
type: 'shield',
count: 1
}],
width: 7168,
height: 2732
}, {
enemies: [{
type: 'basic',
count: 6
}, {
type: 'strong',
count: 3
}, {
type: 'fast',
count: 3
}, {
type: 'tank',
count: 2
}, {
type: 'hunter',
count: 2
}, {
type: 'scout',
count: 3
}, {
type: 'archer',
count: 2
}, {
type: 'berserker',
count: 2
}, {
type: 'shield',
count: 1
}],
width: 8192,
height: 2732
}, {
enemies: [{
type: 'boss1',
count: 1
}],
width: 9216,
height: 2732
}, {
enemies: [{
type: 'basic',
count: 4
}, {
type: 'strong',
count: 2
}, {
type: 'fast',
count: 3
}, {
type: 'tank',
count: 2
}, {
type: 'hunter',
count: 2
}, {
type: 'assassin',
count: 2
}, {
type: 'leader',
count: 1
}, {
type: 'scout',
count: 2
}, {
type: 'archer',
count: 3
}, {
type: 'berserker',
count: 2
}, {
type: 'shield',
count: 2
}, {
type: 'boss2',
count: 1
}],
width: 10240,
height: 2732
}, {
enemies: [{
type: 'basic',
count: 3
}, {
type: 'strong',
count: 2
}, {
type: 'fast',
count: 3
}, {
type: 'tank',
count: 2
}, {
type: 'hunter',
count: 3
}, {
type: 'assassin',
count: 3
}, {
type: 'leader',
count: 2
}, {
type: 'scout',
count: 3
}, {
type: 'archer',
count: 4
}, {
type: 'berserker',
count: 3
}, {
type: 'shield',
count: 2
}, {
type: 'boss3',
count: 1
}, {
type: 'boss1',
count: 1
}],
width: 12288,
height: 2732
}];
var currentLevelData = levels[0];
// UI Elements
var scoreText = new Text2('Score: 0', {
size: 80,
fill: 0xFFFFFF
});
scoreText.anchor.set(0, 0);
scoreText.x = 150;
scoreText.y = 50;
LK.gui.topLeft.addChild(scoreText);
var levelText = new Text2('Dungeon: 1', {
size: 80,
fill: 0xFFFFFF
});
levelText.anchor.set(0.5, 0);
LK.gui.top.addChild(levelText);
levelText.y = 50;
var comboText = new Text2('Combo: 0x', {
size: 60,
fill: 0xFFFF00
});
comboText.anchor.set(1, 0);
LK.gui.topRight.addChild(comboText);
comboText.x = -50;
comboText.y = 120;
var knivesText = new Text2('Knives: 5', {
size: 60,
fill: 0x8e44ad
});
knivesText.anchor.set(1, 0);
LK.gui.topRight.addChild(knivesText);
knivesText.x = -50;
knivesText.y = 190;
// Add upgrades button
showUpgradesButton = LK.getAsset('backgroundTile', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x9b59b6,
scaleX: 2,
scaleY: 0.8
});
showUpgradesButton.x = -150;
showUpgradesButton.y = 350;
LK.gui.topRight.addChild(showUpgradesButton);
var upgradesText = new Text2('UPGRADES', {
size: 40,
fill: 0xFFFFFF
});
upgradesText.anchor.set(0.5, 0.5);
upgradesText.x = -150;
upgradesText.y = 350;
LK.gui.topRight.addChild(upgradesText);
// Add coins display
var coinsDisplay = new Text2('Coins: ' + upgradeTree.coins, {
size: 50,
fill: 0xF1C40F
});
coinsDisplay.anchor.set(1, 0);
coinsDisplay.x = -50;
coinsDisplay.y = 400;
LK.gui.topRight.addChild(coinsDisplay);
showUpgradesButton.down = function () {
if (!upgradeMenu.isVisible) {
upgradeMenu.show(upgradeTree);
LK.gui.center.addChild(upgradeMenu);
}
};
var enemiesLeftText = new Text2('Enemies: 0', {
size: 60,
fill: 0xff4444
});
enemiesLeftText.anchor.set(1, 0);
LK.gui.topRight.addChild(enemiesLeftText);
enemiesLeftText.x = -50;
enemiesLeftText.y = 260;
// Create movement and attack buttons
var leftButton = LK.getAsset('leftButton', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
});
leftButton.x = 80;
leftButton.y = -300;
LK.gui.bottomLeft.addChild(leftButton);
var rightButton = LK.getAsset('rightButton', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
});
rightButton.x = 480;
rightButton.y = -300;
LK.gui.bottomLeft.addChild(rightButton);
var upButton = LK.getAsset('upButton', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
});
upButton.x = 280;
upButton.y = -480;
LK.gui.bottomLeft.addChild(upButton);
var downButton = LK.getAsset('downButton', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
});
downButton.x = 280;
downButton.y = -120;
LK.gui.bottomLeft.addChild(downButton);
var attackButton = LK.getAsset('attackButton', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.9
});
attackButton.x = -150;
attackButton.y = -200;
LK.gui.bottomRight.addChild(attackButton);
var knifeButton = LK.getAsset('knifeButton', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.9
});
knifeButton.x = -350;
knifeButton.y = -200;
LK.gui.bottomRight.addChild(knifeButton);
// Create background grid
var backgroundTiles = [];
function createBackgroundGrid() {
// Clear existing background tiles
for (var i = backgroundTiles.length - 1; i >= 0; i--) {
backgroundTiles[i].destroy();
}
backgroundTiles = [];
var tileSize = 200;
var tilesX = Math.ceil(currentLevelData.width / tileSize) + 2;
var tilesY = Math.ceil(currentLevelData.height / tileSize) + 2;
for (var x = 0; x < tilesX; x++) {
for (var y = 0; y < tilesY; y++) {
var tile = game.addChild(LK.getAsset('backgroundTile', {
anchorX: 0,
anchorY: 0,
alpha: 0.3
}));
tile.x = x * tileSize;
tile.y = y * tileSize;
// Add subtle pattern variation
if ((x + y) % 2 === 0) {
tile.alpha = 0.2;
}
backgroundTiles.push(tile);
}
}
}
// Add background
var background = game.addChild(LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
alpha: 1
}));
// Create hero
hero = game.addChild(new Hero());
hero.x = 1024; // Center of screen
hero.y = 2300; // Near bottom
// Apply upgrade bonuses to hero
hero.maxHealth = 5 + upgradeTree.getMaxHealthBonus();
hero.health = hero.maxHealth;
hero.baseSpeed = 8;
hero.speed = hero.baseSpeed * upgradeTree.getMoveSpeedMultiplier();
hero.damageMultiplier = upgradeTree.getDamageMultiplier();
hero.attackSpeedMultiplier = upgradeTree.getAttackSpeedMultiplier();
hero.damageReduction = upgradeTree.getDamageReduction();
hero.invulnerabilityBonus = upgradeTree.getInvulnerabilityBonus();
// Create health display
function updateHealthDisplay() {
// Remove existing hearts
for (var i = hearts.length - 1; i >= 0; i--) {
hearts[i].destroy();
}
hearts = [];
// Create new hearts
for (var i = 0; i < hero.health; i++) {
var heart = LK.getAsset('heart', {
anchorX: 0.5,
anchorY: 0.5
});
heart.x = 200 + i * 80;
heart.y = 200;
LK.gui.topLeft.addChild(heart);
hearts.push(heart);
}
}
function updateScoreDisplay() {
scoreText.setText('Score: ' + LK.getScore());
var comboMultiplier = Math.floor(hero.comboCount / 5) + 1;
comboText.setText('Combo: ' + comboMultiplier + 'x');
if (coinsDisplay) {
coinsDisplay.setText('Coins: ' + upgradeTree.coins);
}
}
function updateKnivesDisplay() {
knivesText.setText('Knives: ' + knivesRemaining);
// Update button alpha based on availability
knifeButton.alpha = knivesRemaining > 0 ? 0.9 : 0.3;
}
function updateEnemiesLeftDisplay() {
enemiesLeftText.setText('Enemies: ' + enemies.length);
}
function initializeLevel() {
// Show boss fight alert for final dungeon
if (currentDungeon === levels.length) {
LK.setTimeout(function () {
alert('FINAL BOSS APPROACHING! Prepare for the ultimate challenge!');
}, 500);
}
// Clear existing enemies
for (var i = enemies.length - 1; i >= 0; i--) {
enemies[i].destroy();
}
enemies = [];
// Clear existing warnings
for (var i = enemyWarnings.length - 1; i >= 0; i--) {
enemyWarnings[i].destroy();
}
enemyWarnings = [];
// Clear existing knives
for (var i = knives.length - 1; i >= 0; i--) {
knives[i].destroy();
}
knives = [];
// Clear existing route effects
for (var i = routeEffects.length - 1; i >= 0; i--) {
routeEffects[i].destroy();
}
routeEffects = [];
// Clear existing blood particles
for (var i = bloodParticles.length - 1; i >= 0; i--) {
bloodParticles[i].destroy();
}
bloodParticles = [];
// Clear existing impact sparks
for (var i = impactSparks.length - 1; i >= 0; i--) {
impactSparks[i].destroy();
}
impactSparks = [];
// Clear existing dust clouds
for (var i = dustClouds.length - 1; i >= 0; i--) {
dustClouds[i].destroy();
}
dustClouds = [];
// Clear existing magical effects
for (var i = magicalEffects.length - 1; i >= 0; i--) {
magicalEffects[i].destroy();
}
magicalEffects = [];
// Clear existing boss projectiles
for (var i = bossProjectiles.length - 1; i >= 0; i--) {
bossProjectiles[i].destroy();
}
bossProjectiles = [];
// Clear existing health potions
for (var i = healthPotions.length - 1; i >= 0; i--) {
healthPotions[i].destroy();
}
healthPotions = [];
// Clear existing damage numbers
for (var i = damageNumbers.length - 1; i >= 0; i--) {
damageNumbers[i].destroy();
}
damageNumbers = [];
// Clear existing weapon trails
for (var i = weaponTrails.length - 1; i >= 0; i--) {
weaponTrails[i].destroy();
}
weaponTrails = [];
// Clear group AI data
groupAI.communications = [];
groupAI.flankingCoordination = [];
groupAI.leaderBuffs = [];
groupAI.packHunters = [];
// Reset knife count with upgrade bonus
knivesRemaining = 5 + upgradeTree.getStartingKnivesBonus();
currentLevelData = levels[currentDungeon - 1] || levels[levels.length - 1];
dungeonComplete = false;
// Spawn enemies for this level
for (var j = 0; j < currentLevelData.enemies.length; j++) {
var enemyGroup = currentLevelData.enemies[j];
for (var k = 0; k < enemyGroup.count; k++) {
spawnEnemy(enemyGroup.type);
}
}
levelText.setText('Dungeon: ' + currentDungeon);
updateKnivesDisplay();
updateEnemiesLeftDisplay();
// Create background grid for this level
createBackgroundGrid();
}
function spawnEnemy(type) {
var enemy;
if (type === 'boss') {
// Randomly choose boss type based on dungeon
var bossTypes = [BossType1, BossType2, BossType3];
var BossClass = bossTypes[Math.floor(Math.random() * bossTypes.length)];
enemy = game.addChild(new BossClass());
// Boss spawns at center of level
enemy.x = currentLevelData.width / 2;
enemy.y = 2200;
} else if (type === 'boss1') {
enemy = game.addChild(new BossType1());
enemy.x = currentLevelData.width / 2;
enemy.y = 2200;
} else if (type === 'boss2') {
enemy = game.addChild(new BossType2());
enemy.x = currentLevelData.width / 2;
enemy.y = 2200;
} else if (type === 'boss3') {
enemy = game.addChild(new BossType3());
enemy.x = currentLevelData.width / 2;
enemy.y = 2200;
} else if (type === 'leader') {
enemy = game.addChild(new Leader());
// Leaders spawn in strategic positions
enemy.x = currentLevelData.width / 2 + (Math.random() - 0.5) * 200;
enemy.y = 1900 + Math.random() * 200;
} else if (type === 'scout') {
enemy = game.addChild(new Scout());
// Scouts spawn at level edges for early warning
enemy.x = Math.random() < 0.5 ? 200 + Math.random() * 300 : currentLevelData.width - 500 + Math.random() * 300;
enemy.y = 1700 + Math.random() * 600;
} else if (type === 'berserker') {
enemy = game.addChild(new Berserker());
// Berserkers spawn closer to center for aggressive positioning
enemy.x = currentLevelData.width / 2 + (Math.random() - 0.5) * 600;
enemy.y = 1800 + Math.random() * 400;
} else if (type === 'archer') {
enemy = game.addChild(new Archer());
// Archers spawn at elevated positions (back areas)
enemy.x = 300 + Math.random() * (currentLevelData.width - 600);
enemy.y = 1600 + Math.random() * 200; // Higher ground
} else if (type === 'shield') {
enemy = game.addChild(new Shield());
// Shields spawn in protective positions
enemy.x = currentLevelData.width / 2 + (Math.random() - 0.5) * 400;
enemy.y = 1850 + Math.random() * 300;
} else {
enemy = game.addChild(new Enemy(type));
// Find a spawn position that's not too close to the hero
var minDistanceFromPlayer = 500; // Minimum distance from player
var attempts = 0;
var maxAttempts = 20;
var enemyX, enemyY;
do {
// Random spawn position within level bounds
enemyX = 200 + Math.random() * (currentLevelData.width - 400);
enemyY = 1700 + Math.random() * 600;
// Calculate distance from hero
var dx = enemyX - hero.x;
var dy = enemyY - hero.y;
var distanceFromPlayer = Math.sqrt(dx * dx + dy * dy);
attempts++;
// If far enough from player or we've tried too many times, use this position
if (distanceFromPlayer >= minDistanceFromPlayer || attempts >= maxAttempts) {
enemy.x = enemyX;
enemy.y = enemyY;
break;
}
} while (attempts < maxAttempts);
// Set enemy properties based on type
if (type === 'basic') {
enemy.health = 2;
enemy.speed = 2;
} else if (type === 'strong') {
enemy.health = 4;
enemy.speed = 0.8;
} else if (type === 'fast') {
enemy.health = 1;
enemy.speed = 4;
} else if (type === 'tank') {
enemy.health = 8;
enemy.speed = 0.5;
} else if (type === 'hunter') {
enemy.health = 3;
enemy.speed = 2.5;
} else if (type === 'assassin') {
enemy.health = 2;
enemy.speed = 3;
}
}
enemies.push(enemy);
}
function spawnPowerUp() {
var powerup = game.addChild(new PowerUp());
powerup.x = 500 + Math.random() * 1048; // Random x position
powerup.y = 1800 + Math.random() * 400; // Above ground level
// Random powerup type
var types = ['health', 'invulnerable', 'damage'];
powerup.type = types[Math.floor(Math.random() * types.length)];
// Color by type
var powerupGraphics = powerup.getChildAt(0);
if (powerup.type === 'health') {
powerupGraphics.tint = 0x00ff00; // Green
} else if (powerup.type === 'invulnerable') {
powerupGraphics.tint = 0x0088ff; // Blue
} else if (powerup.type === 'damage') {
powerupGraphics.tint = 0xff8800; // Orange
}
powerups.push(powerup);
}
function findNearestEnemy(x, y) {
var nearest = null;
var shortestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var distance = Math.sqrt(Math.pow(enemy.x - x, 2) + Math.pow(enemy.y - y, 2));
if (distance < shortestDistance) {
shortestDistance = distance;
nearest = enemy;
}
}
return nearest;
}
function isVisible(obj, buffer) {
buffer = buffer || 100;
var objScreenX = obj.x - camera.x;
var objScreenY = obj.y - camera.y;
return objScreenX >= -buffer && objScreenX <= 2048 + buffer && objScreenY >= -buffer && objScreenY <= 2732 + buffer;
}
function updateEnemyWarnings() {
// Remove warnings for dead enemies
for (var i = enemyWarnings.length - 1; i >= 0; i--) {
var warning = enemyWarnings[i];
var enemyExists = false;
for (var j = 0; j < enemies.length; j++) {
if (enemies[j] === warning.targetEnemy) {
enemyExists = true;
break;
}
}
if (!enemyExists) {
warning.destroy();
enemyWarnings.splice(i, 1);
}
}
// Create warnings for new enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var hasWarning = false;
for (var j = 0; j < enemyWarnings.length; j++) {
if (enemyWarnings[j].targetEnemy === enemy) {
hasWarning = true;
break;
}
}
if (!hasWarning) {
var warning = LK.gui.center.addChild(new EnemyWarning());
warning.targetEnemy = enemy;
warning.setDirection('left');
enemyWarnings.push(warning);
}
}
}
// Initialize UI
updateHealthDisplay();
updateScoreDisplay();
updateKnivesDisplay();
updateEnemiesLeftDisplay();
// Initialize first level
initializeLevel();
// Create initial background grid
createBackgroundGrid();
// Set initial camera position
camera.targetX = hero.x - 1024;
camera.targetY = hero.y - 1366;
camera.x = camera.targetX;
camera.y = camera.targetY;
// Button event handlers
leftButton.down = function (x, y, obj) {
movementState.left = true;
};
leftButton.up = function (x, y, obj) {
movementState.left = false;
resetMovementState(); // Reset all movement when any button is released
};
// Global function to check which button is under a given position
function getButtonUnderPosition(screenX, screenY) {
// Convert GUI coordinates and check bounds for each button (accounting for 1.5x scale)
var scaledButtonWidth = 200 * 1.5;
var scaledButtonHeight = 200 * 1.5;
var leftBounds = {
x: leftButton.x - scaledButtonWidth / 2,
y: leftButton.y - scaledButtonHeight / 2,
width: scaledButtonWidth,
height: scaledButtonHeight
};
var rightBounds = {
x: rightButton.x - scaledButtonWidth / 2,
y: rightButton.y - scaledButtonHeight / 2,
width: scaledButtonWidth,
height: scaledButtonHeight
};
var upBounds = {
x: upButton.x - scaledButtonWidth / 2,
y: upButton.y - scaledButtonHeight / 2,
width: scaledButtonWidth,
height: scaledButtonHeight
};
var downBounds = {
x: downButton.x - scaledButtonWidth / 2,
y: downButton.y - scaledButtonHeight / 2,
width: scaledButtonWidth,
height: scaledButtonHeight
};
// Adjust screen coordinates relative to bottomLeft GUI
var relativeX = screenX;
var relativeY = screenY - (2732 - 500); // Approximate bottomLeft offset
if (relativeX >= leftBounds.x && relativeX <= leftBounds.x + leftBounds.width && relativeY >= leftBounds.y && relativeY <= leftBounds.y + leftBounds.height) {
return 'left';
}
if (relativeX >= rightBounds.x && relativeX <= rightBounds.x + rightBounds.width && relativeY >= rightBounds.y && relativeY <= rightBounds.y + rightBounds.height) {
return 'right';
}
if (relativeX >= upBounds.x && relativeX <= upBounds.x + upBounds.width && relativeY >= upBounds.y && relativeY <= upBounds.y + upBounds.height) {
return 'up';
}
if (relativeX >= downBounds.x && relativeX <= downBounds.x + downBounds.width && relativeY >= downBounds.y && relativeY <= downBounds.y + downBounds.height) {
return 'down';
}
return null;
}
// Global movement handling function
function handleMovementInput(direction) {
// Reset all movement states first
movementState.left = false;
movementState.right = false;
movementState.up = false;
movementState.down = false;
// Set the active direction
if (direction === 'left') {
movementState.left = true;
} else if (direction === 'right') {
movementState.right = true;
} else if (direction === 'up') {
movementState.up = true;
} else if (direction === 'down') {
movementState.down = true;
}
}
leftButton.move = function (x, y, obj) {
var currentButton = getButtonUnderPosition(x, y);
if (currentButton) {
handleMovementInput(currentButton);
} else {
handleMovementInput('left');
}
};
rightButton.down = function (x, y, obj) {
movementState.right = true;
};
rightButton.up = function (x, y, obj) {
movementState.right = false;
resetMovementState(); // Reset all movement when any button is released
};
rightButton.move = function (x, y, obj) {
var currentButton = getButtonUnderPosition(x, y);
if (currentButton) {
handleMovementInput(currentButton);
} else {
handleMovementInput('right');
}
};
upButton.down = function (x, y, obj) {
movementState.up = true;
};
upButton.up = function (x, y, obj) {
movementState.up = false;
resetMovementState(); // Reset all movement when any button is released
};
upButton.move = function (x, y, obj) {
var currentButton = getButtonUnderPosition(x, y);
if (currentButton) {
handleMovementInput(currentButton);
} else {
handleMovementInput('up');
}
};
downButton.down = function (x, y, obj) {
movementState.down = true;
};
downButton.up = function (x, y, obj) {
movementState.down = false;
resetMovementState(); // Reset all movement when any button is released
};
downButton.move = function (x, y, obj) {
var currentButton = getButtonUnderPosition(x, y);
if (currentButton) {
handleMovementInput(currentButton);
} else {
handleMovementInput('down');
}
};
attackButton.down = function (x, y, obj) {
if (!hero.isAttacking) {
var nearestEnemy = findNearestEnemy(hero.x, hero.y);
if (nearestEnemy) {
hero.attack(nearestEnemy.x);
// Check if attack hits with increased range
var distanceToEnemy = Math.sqrt(Math.pow(hero.x - nearestEnemy.x, 2) + Math.pow(hero.y - nearestEnemy.y, 2));
if (distanceToEnemy < 350) {
// Increased from 250 to 350
var damage = hero.damageBoost ? 2 : 1;
for (var i = 0; i < damage; i++) {
nearestEnemy.takeDamage();
}
}
} else {
// Attack in hero's facing direction
hero.attack(hero.x + (hero.getChildAt(0).scaleX > 0 ? 100 : -100));
}
}
};
knifeButton.down = function (x, y, obj) {
if (knivesRemaining > 0) {
// Find nearest enemy for targeting
var nearestEnemy = findNearestEnemy(hero.x, hero.y);
if (nearestEnemy) {
// Clear existing route effects before creating new one
for (var i = routeEffects.length - 1; i >= 0; i--) {
routeEffects[i].destroy();
routeEffects.splice(i, 1);
}
// Create route visualization
var routeEffect = game.addChild(new RouteEffect());
routeEffect.createRoute(nearestEnemy);
routeEffects.push(routeEffect);
// Throw knife to follow the route
var knife = game.addChild(new Knife());
knife.x = hero.x;
knife.y = hero.y - 80;
// Set the route for the knife to follow
knife.setRoute(routeEffect.routePositions);
// Rotation will be handled automatically in knife update based on target direction
knives.push(knife);
knivesRemaining--;
updateKnivesDisplay();
LK.getSound('knifeThrow').play();
} else {
// No enemy found, throw in hero facing direction
var knife = game.addChild(new Knife());
knife.x = hero.x;
knife.y = hero.y - 80;
var heroGraphics = hero.getChildAt(0);
knife.direction = heroGraphics.scaleX > 0 ? 1 : -1;
// Rotation will be handled automatically in knife update based on movement direction
knives.push(knife);
knivesRemaining--;
updateKnivesDisplay();
LK.getSound('knifeThrow').play();
}
}
};
// Game input (fallback for screen taps outside buttons)
game.down = function (x, y, obj) {
// Convert screen coordinates to world coordinates
var worldX = x + camera.x;
var worldY = y + camera.y;
// Check if tap is for movement or attack
var distanceToHero = Math.sqrt(Math.pow(worldX - hero.x, 2) + Math.pow(worldY - hero.y, 2));
if (distanceToHero > 200) {
// Movement - move toward tap position
var dx = worldX - hero.x;
var dy = worldY - hero.y;
if (Math.abs(dx) > Math.abs(dy)) {
hero.move(dx > 0 ? 'right' : 'left');
} else {
hero.move(dy > 0 ? 'down' : 'up');
}
} else {
// Attack
if (!hero.isAttacking) {
var nearestEnemy = findNearestEnemy(worldX, worldY);
if (nearestEnemy) {
hero.attack(nearestEnemy.x);
// Check if attack hits with increased range
var distanceToEnemy = Math.sqrt(Math.pow(hero.x - nearestEnemy.x, 2) + Math.pow(hero.y - nearestEnemy.y, 2));
if (distanceToEnemy < 350) {
// Increased from 250 to 350
var damage = hero.damageBoost ? 2 : 1;
for (var i = 0; i < damage; i++) {
nearestEnemy.takeDamage();
}
}
} else {
// Attack in direction of tap
hero.attack(worldX);
}
}
}
};
// Main game loop
game.update = function () {
// Track if hero is moving this frame
var wasMoving = hero.isWalking;
var isMovingThisFrame = false;
// Handle continuous movement based on button states - only move if button is actively pressed
if (movementState.left === true) {
hero.move('left');
isMovingThisFrame = true;
}
if (movementState.right === true) {
hero.move('right');
isMovingThisFrame = true;
}
if (movementState.up === true) {
hero.move('up');
isMovingThisFrame = true;
}
if (movementState.down === true) {
hero.move('down');
isMovingThisFrame = true;
}
// Stop walking animation if no movement this frame
if (wasMoving && !isMovingThisFrame) {
hero.stopWalkAnimation();
}
// Update screen shake
screenShake.update();
// Update camera position smoothly
camera.x += (camera.targetX - camera.x) * camera.smoothing;
camera.y += (camera.targetY - camera.y) * camera.smoothing;
// Apply camera position to game with screen shake offset
game.x = -camera.x + screenShake.getOffsetX();
game.y = -camera.y + screenShake.getOffsetY();
// Check for hero-enemy collisions and apply push-back using spatial partitioning
var nearbyEnemies = spatialGrid.getNearbyObjects(hero.x, hero.y, 150);
for (var i = 0; i < nearbyEnemies.length; i++) {
var enemy = nearbyEnemies[i];
var dx = hero.x - enemy.x;
var dy = hero.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// If too close, push hero away from enemy
if (distance < 100 && distance > 0) {
var pushForce = (100 - distance) * 0.3;
var pushX = dx / distance * pushForce;
var pushY = dy / distance * pushForce;
// Apply push with bounds checking
var newHeroX = hero.x + pushX;
var newHeroY = hero.y + pushY;
// Keep hero within level bounds
if (newHeroX > 100 && newHeroX < currentLevelData.width - 100) {
hero.x = newHeroX;
}
if (newHeroY > 1600 && newHeroY < 2400) {
hero.y = newHeroY;
}
// Update camera target when hero is pushed
camera.targetX = hero.x - 1024;
camera.targetY = hero.y - 1366;
camera.targetX = Math.max(0, Math.min(camera.targetX, currentLevelData.width - 2048));
camera.targetY = Math.max(0, Math.min(camera.targetY, currentLevelData.height - 2732));
}
}
// Clear and rebuild spatial grid
spatialGrid.clear();
for (var i = 0; i < enemies.length; i++) {
spatialGrid.addObject(enemies[i], enemies[i].x, enemies[i].y);
}
// Update all game objects with culling
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (isVisible(enemy, 300)) {
// Only update visible enemies + buffer
enemy.update();
}
}
for (var i = coins.length - 1; i >= 0; i--) {
// Safety check to ensure coin exists at this index
if (coins[i] && isVisible(coins[i], 100)) {
// Check for coin magnetism
var coin = coins[i];
var distanceToHero = Math.sqrt(Math.pow(coin.x - hero.x, 2) + Math.pow(coin.y - hero.y, 2));
var magnetRange = upgradeTree.getCoinMagnetismRange();
if (distanceToHero < magnetRange) {
// Move coin toward hero
var dx = hero.x - coin.x;
var dy = hero.y - coin.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var magnetSpeed = 8;
coin.x += dx / distance * magnetSpeed;
coin.y += dy / distance * magnetSpeed;
}
// Collect if very close
if (distanceToHero < 50) {
upgradeTree.addCoins(10);
coinsDisplay.setText('Coins: ' + upgradeTree.coins);
coin.collect();
}
}
// Additional safety check before calling update
if (coins[i]) {
coins[i].update();
}
}
}
for (var i = powerups.length - 1; i >= 0; i--) {
if (isVisible(powerups[i], 100)) {
powerups[i].update();
}
}
for (var i = healthPotions.length - 1; i >= 0; i--) {
if (isVisible(healthPotions[i], 100)) {
healthPotions[i].update();
}
}
for (var i = knives.length - 1; i >= 0; i--) {
knives[i].update(); // Always update knives as they move fast
}
for (var i = bloodParticles.length - 1; i >= 0; i--) {
if (isVisible(bloodParticles[i], 50)) {
bloodParticles[i].update();
}
}
for (var i = impactSparks.length - 1; i >= 0; i--) {
if (isVisible(impactSparks[i], 50)) {
impactSparks[i].update();
}
}
for (var i = dustClouds.length - 1; i >= 0; i--) {
if (isVisible(dustClouds[i], 100)) {
dustClouds[i].update();
}
}
for (var i = magicalEffects.length - 1; i >= 0; i--) {
if (isVisible(magicalEffects[i], 100)) {
magicalEffects[i].update();
}
}
for (var i = bossProjectiles.length - 1; i >= 0; i--) {
var projectile = bossProjectiles[i];
// Check if any shield enemy can block this projectile
var blocked = false;
for (var j = 0; j < enemies.length; j++) {
var enemy = enemies[j];
if (enemy.enemyType === 'shield' && enemy.blockProjectile) {
if (enemy.blockProjectile(projectile)) {
blocked = true;
break;
}
}
}
// Only update projectile if it wasn't blocked
if (!blocked) {
projectile.update(); // Always update projectiles as they move fast
}
}
// Update coins display to reflect automatic coin collection
updateScoreDisplay();
for (var i = damageNumbers.length - 1; i >= 0; i--) {
if (isVisible(damageNumbers[i], 100)) {
damageNumbers[i].update();
}
}
for (var i = weaponTrails.length - 1; i >= 0; i--) {
weaponTrails[i].update(); // Always update trails for smooth effect
}
// Clean up destroyed route effects
for (var i = routeEffects.length - 1; i >= 0; i--) {
var routeEffect = routeEffects[i];
if (!routeEffect.parent) {
routeEffects.splice(i, 1);
}
}
// Update enemy warnings
updateEnemyWarnings();
for (var i = 0; i < enemyWarnings.length; i++) {
enemyWarnings[i].update();
}
// Special check for dungeon 7 boss - kill boss when all soldiers are dead
if (currentDungeon === 7 && !dungeonComplete && enemies.length > 0) {
var bossCount = 0;
var summonerBoss = null;
var otherEnemyCount = 0;
// Count bosses and other enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.bossType === 'summoner') {
bossCount++;
summonerBoss = enemy;
// Reduce boss health specifically for dungeon 7
if (summonerBoss.health > 25) {
summonerBoss.health = 25;
summonerBoss.maxHealth = 25;
}
} else {
otherEnemyCount++;
}
}
// If only summoner boss remains (all soldiers dead), kill the boss
if (bossCount === 1 && otherEnemyCount === 0 && summonerBoss) {
summonerBoss.die();
}
}
// Check for dungeon completion
if (!dungeonComplete && enemies.length === 0) {
dungeonComplete = true;
currentDungeon++;
if (currentDungeon <= levels.length) {
// Start next dungeon after delay
LK.setTimeout(function () {
initializeLevel();
}, 2000);
} else {
// All dungeons completed
LK.showYouWin();
}
}
};