/**** * 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();
}
}
};