User prompt
elimina el sistema de medallas
User prompt
haz una sola funcion de colision para todos los enemigos
User prompt
haz que el el proyectil, fire ball, energybeam esten en una sola clase con comportamientos diferentes
User prompt
haz que el tutorial solo tenga 3 pasos
User prompt
haz que los primeros 5 enemigos salgan del camino del centro
User prompt
haz que la barra de vida sea la que estaba antes
User prompt
haz que la barra de vida sea mas bonta
User prompt
Please fix the bug: 'TypeError: self.createHealthBar is not a function' in or related to this line: 'self.createHealthBar();' Line Number: 196
User prompt
haz que los enemigos se vean mas grandes
User prompt
haz que el proyectil sea mas pequeno
User prompt
haz que los proyectiles siempre tengan la parte de arriba mirando hacia el enemigo
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of undefined (reading 'length')' in or related to this line: 'for (var frameIdx = 0; frameIdx < demoEnemy.skeletonFrames.length; frameIdx++) {' Line Number: 2160
User prompt
consolida el sistema de assets
User prompt
optimiza el lopp principal, haz la logica de spawn menos repetitiva y unificala para todos los enemigos
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of undefined (reading 'thorns')' in or related to this line: 'var effectFunction = UPGRADE_EFFECTS[upgradeKey];' Line Number: 2638
User prompt
puedes refactorizar el upgrademenu para reducir lineas de codigo
User prompt
unifica el createimpacteffect para que el mismo en todos los enemigos ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
arregla el problema con las animaciones de muerte de los enemigos ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Please fix the bug: 'ReferenceError: createEnemyDeathAnimation is not defined' in or related to this line: 'createEnemyDeathAnimation(self, enemyType, arrayToUpdate);' Line Number: 440
User prompt
haz que las animaciones de muertes de los enemigos se unifiquen para reducir codigo ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
ayudame a profundizar la consolidacion de enemigos para optimizar el juego
User prompt
haz que todas las colisiones al mago sea una sola funcion para acortar el codigo
User prompt
haz que los esqueletos se unan al asset de esqueleto
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'health')' in or related to this line: 'enemy.health = Math.floor(baseStats[type].health * mods.healthMod * healthMult);' Line Number: 3811
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'health')' in or related to this line: 'enemy.health = Math.floor(stats.health * mods.healthMod * healthMult);' Line Number: 3811
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // Game arrays to track objects var BaseEnemy = Container.expand(function (config) { var self = Container.call(this); // Default config config = config || {}; self.enemyType = config.type || 'skeleton'; self.assetBase = config.assetBase || 'esqueleto'; self.scale = config.scale || 2.0; self.baseHealth = config.health || 1; self.baseSpeed = config.speed || 3; self.animSpeed = config.animSpeed || 15; self.damage = config.damage || 20; self.experience = config.experience || 25; self.score = config.score || 10; self.vibration = config.vibration || 50; self.impactColor = config.impactColor || 0xFFAA00; self.damageColor = config.damageColor || 0xFF4444; // Animation system self.currentFrame = 1; self.animationTimer = 0; self.animationSpeed = self.animSpeed; self.animationState = 'walking'; // Create frames self.frames = []; for (var i = 1; i <= 4; i++) { var frameGraphics = self.attachAsset(self.assetBase + i, { anchorX: 0.5, anchorY: 1.0, scaleX: self.scale, scaleY: self.scale }); frameGraphics.visible = i === 1; self.frames.push(frameGraphics); } // Hitbox var hitbox = self.attachAsset(self.assetBase + '1', { anchorX: 0.5, anchorY: 1.0, scaleX: 4, scaleY: 4 }); hitbox.alpha = 0; self.health = self.baseHealth; self.maxHealth = self.baseHealth; self.speed = self.baseSpeed; self.lastX = 0; self.frozen = false; self.frozenTimer = 0; self.update = function () { if (tutorial && tutorial.isActive) return; if (self.isDying) return; var speedMultiplier = upgradeMenu && upgradeMenu.visible ? 0.3 : 1.0; // Animation self.animationTimer++; var frameSpeed = self.animationSpeed; if (self.animationState === 'attacking') frameSpeed = 8;else if (self.animationState === 'dying') frameSpeed = 20;else if (self.animationState === 'idle') frameSpeed = 25; if (self.animationTimer >= frameSpeed) { self.animationTimer = 0; self.frames[self.currentFrame - 1].visible = false; if (self.animationState === 'walking') { self.currentFrame++; if (self.currentFrame > 4) self.currentFrame = 1; } else if (self.animationState === 'attacking') { self.currentFrame++; if (self.currentFrame > 4) self.currentFrame = 2; } else if (self.animationState === 'dying') { if (self.currentFrame < 4) self.currentFrame++; } else if (self.animationState === 'idle') { self.currentFrame = self.currentFrame === 1 ? 2 : 1; } self.frames[self.currentFrame - 1].visible = true; } // Speed increase if (!self.speedTweenStarted) { self.speedTweenStarted = true; tween(self, { speed: self.speed * 1.5 }, { duration: 10000, easing: tween.easeOut }); } // Frozen state if (self.frozen) { self.frozenTimer--; if (self.frozenTimer <= 0) self.frozen = false; return; } // Movement if (wizard) { var dx = wizard.x - self.x, dy = wizard.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { self.x += dx / distance * self.speed * speedMultiplier; self.y += dy / distance * self.speed * speedMultiplier; } // Facing var flipScale = dx < 0 ? -self.scale : self.scale; for (var frameIdx = 0; frameIdx < self.frames.length; frameIdx++) { self.frames[frameIdx].scaleX = flipScale; } } else self.y += self.speed * speedMultiplier; }; self.takeDamage = function (damage) { self.health -= damage; self.animationState = 'attacking'; LK.effects.flashObject(self, 0xFF0000, 50); // Unified damage text var damageText = new Text2('-' + damage, { size: 120, fill: self.damageColor, font: "monospace" }); damageText.anchor.set(0.5, 0.5); damageText.x = self.x + (Math.random() - 0.5) * 60; damageText.y = self.y - 40; game.addChild(damageText); tween(damageText, { y: damageText.y - 120, alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { damageText.destroy(); } }); // Unified impact effect createImpactEffect(self.x, self.y, self.impactColor, 1.5); // Return to walking tween({}, {}, { duration: 300, onFinish: function onFinish() { if (self.animationState === 'attacking') self.animationState = 'walking'; } }); if (self.health <= 0) self.die(); }; self.down = function (x, y, obj) { if (typeof LK.vibrate === 'function') LK.vibrate(self.vibration); selectedEnemy = self; LK.effects.flashObject(self, 0xFFFF00, 500); if (wizard && projectiles.length < 10) { var projectile = game.addChild(new Projectile()); projectile.x = wizard.x; projectile.y = wizard.y; var dx = self.x - wizard.x, dy = self.y - wizard.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { projectile.direction.x = dx / distance; projectile.direction.y = dy / distance; } projectile.targetEnemy = self; projectiles.push(projectile); LK.getSound('spellCast').play(); } }; self.die = function () { self.animationState = 'dying'; self.currentFrame = 3; LK.getSound('painSound').play(); self.isDying = true; // Death animation tween(self, { alpha: 0, scaleX: 0.3, scaleY: 0.3, rotation: Math.PI * 0.5 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { // Drop coin dropCoin(self.x, self.y); // Update counters enemyKillCounter++; killCountText.setText('Puntuacion: ' + enemyKillCounter); // Path combo updatePathCombo(self.pathIndex); // Stats if (!storage.totalKills) storage.totalKills = 0; storage.totalKills++; wizard.gainExperience(self.experience); // Life drain if (wizard.lifeDrainLevel > 0 && Math.random() < 0.20) { wizard.health = Math.min(wizard.health + 20, wizard.maxHealth); updateHealthBar(); LK.effects.flashObject(wizard, 0x00FF00, 300); } // Clear selected if (selectedEnemy === self) selectedEnemy = null; // Remove from array removeFromArray(self); self.destroy(); LK.setScore(LK.getScore() + self.score); } }); }; return self; }); // Factory functions for specific enemy types var Coin = Container.expand(function () { var self = Container.call(this); var coinGraphics = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5 }); self.bobOffset = Math.random() * Math.PI * 2; self.initialY = 0; self.update = function () { if (self.initialY === 0) { self.initialY = self.y; } // Only do bobbing animation and collection if not animating to coin counter if (!self.isAnimating) { // Bobbing animation self.y = self.initialY + Math.sin(LK.ticks * 0.1 + self.bobOffset) * 10; // Check collection by knight if (knight && self.intersects(knight)) { self.collect(); } } }; self.collect = function () { LK.getSound('coinCollect').play(); LK.setScore(LK.getScore() + 5); coinCounter++; coinText.setText('Coins: ' + coinCounter); // Update medal statistics if (!storage.totalCoins) storage.totalCoins = 0; storage.totalCoins++; // 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 EnergyBeam = Container.expand(function () { var self = Container.call(this); // Create beam visual using projectile glow var beamGraphics = self.attachAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.0, scaleY: 1.0 }); beamGraphics.tint = 0x00ffff; // Cyan color beamGraphics.alpha = 0.9; self.speed = 60; self.direction = { x: 0, y: 0 }; self.targetEnemy = null; self.lastIntersecting = false; self.update = function () { // Move toward target self.x += self.direction.x * self.speed; self.y += self.direction.y * self.speed; // Rotate beam to point toward movement direction var angle = Math.atan2(self.direction.y, self.direction.x); beamGraphics.rotation = angle; // Remove if off screen if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) { self.destroy(); return; } // Check collision with target enemy (but never with wizard) if (self.targetEnemy && self.targetEnemy.parent && self.targetEnemy !== wizard) { var currentIntersecting = self.intersects(self.targetEnemy); if (!self.lastIntersecting && currentIntersecting) { // Hit the target enemy // Calculate damage based on spell power upgrade var damage = 100; if (wizard && wizard.spellPowerLevel && wizard.spellPowerLevel > 0) { // Check if target is an ogre and we're before enemy 35 if (self.targetEnemy && self.targetEnemy.ogreFrames && enemyKillCounter < 35) { // Always kill ogres in one hit until enemy 35 damage = self.targetEnemy.health + 1; // Ensure one-hit kill } else { damage = 200; // Double damage when spell power is active } } self.targetEnemy.takeDamage(damage); self.destroy(); return; } self.lastIntersecting = currentIntersecting; } else { // Target destroyed, remove beam self.destroy(); } }; return self; }); var EnergyOrb = Container.expand(function () { var self = Container.call(this); // Create energy sphere visual var sphereGraphics = self.attachAsset('energySphere', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); // Add glowing effect sphereGraphics.alpha = 0.8; self.attackTimer = 0; self.attackInterval = 180; // 3 seconds at 60fps self.orbitalAngle = 0; self.orbitalRadius = 120; self.update = function () { // Pause energy orb when tutorial is active if (tutorial && tutorial.isActive) { return; } // Pause energy orb when upgrade menu is visible if (upgradeMenu && upgradeMenu.visible) { return; } // Keep sphere at wizard's position (stationary relative to wizard) if (wizard) { self.x = wizard.x + 140; // Position further to the right side of wizard self.y = wizard.y - 20; // Position slightly lower relative to wizard } // Pulsing glow effect var pulse = 1 + Math.sin(LK.ticks * 0.2) * 0.3; sphereGraphics.scaleX = 1.5 * pulse; sphereGraphics.scaleY = 1.5 * pulse; // Attack timer - keep original interval regardless of upgrades self.attackTimer++; if (self.attackTimer >= 180) { // Fixed at 3 seconds self.attackTimer = 0; self.attackClosestEnemy(); } }; self.attackClosestEnemy = function () { var closestEnemy = null; var closestDistance = Infinity; // Check all enemy types for closest one var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses); for (var i = 0; i < allEnemies.length; i++) { var enemy = allEnemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // Attack closest enemy if found if (closestEnemy) { // Create energy beam projectile var energyBeam = game.addChild(new EnergyBeam()); energyBeam.x = self.x; energyBeam.y = self.y; energyBeam.targetEnemy = closestEnemy; // Calculate direction to target var dx = closestEnemy.x - self.x; var dy = closestEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { energyBeam.direction.x = dx / distance; energyBeam.direction.y = dy / distance; } // Flash effect on sphere when attacking tween(sphereGraphics, { scaleX: 2.5, scaleY: 2.5, alpha: 1.0 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(sphereGraphics, { scaleX: 1.5, scaleY: 1.5, alpha: 0.8 }, { duration: 200, easing: tween.easeIn }); } }); LK.getSound('spellCast').play(); } }; return self; }); var FireBall = Container.expand(function () { var self = Container.call(this); // Create fire ball visual using projectile glow with fire colors var fireBallGraphics = self.attachAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); fireBallGraphics.tint = 0xFF4500; // Orange-red fire color fireBallGraphics.alpha = 0.9; self.speed = 40; self.direction = { x: 0, y: 0 }; self.lastIntersecting = {}; self.update = function () { // Move fire ball self.x += self.direction.x * self.speed; self.y += self.direction.y * self.speed; // Add fire flickering effect var flicker = 1 + Math.sin(LK.ticks * 0.4) * 0.3; fireBallGraphics.scaleX = 1.5 * flicker; fireBallGraphics.scaleY = 1.5 * flicker; // Rotate fire ball fireBallGraphics.rotation += 0.2; // Remove if off screen if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) { self.destroy(); return; } // Check collision with all enemy types (but never with wizard) var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses); for (var i = 0; i < allEnemies.length; i++) { var enemy = allEnemies[i]; // Skip collision check with wizard if (enemy === wizard) { continue; } if (!self.lastIntersecting[i]) { self.lastIntersecting[i] = false; } var currentIntersecting = self.intersects(enemy); if (!self.lastIntersecting[i] && currentIntersecting) { // Hit enemy - deal fire damage var damage = 150; // High damage for fire ball enemy.takeDamage(damage); // Create fire explosion effect LK.effects.flashObject(enemy, 0xFF4500, 400); // Create additional fire visual effect var fireEffect = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: enemy.x, y: enemy.y, scaleX: 3, scaleY: 3 })); fireEffect.tint = 0xFF6600; fireEffect.alpha = 0.8; // Animate fire effect tween(fireEffect, { scaleX: 6, scaleY: 6, alpha: 0 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { fireEffect.destroy(); } }); // Reduced fire trail particles for better performance for (var fireIdx = 0; fireIdx < 6; fireIdx++) { var fireParticle = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: enemy.x, y: enemy.y, scaleX: 0.6, scaleY: 0.6 })); fireParticle.tint = 0xFF4500; fireParticle.alpha = 0.9; // Random fire particle direction var fireAngle = fireIdx / 6 * Math.PI * 2; var fireSpeed = 50; var fireTargetX = enemy.x + Math.cos(fireAngle) * fireSpeed; var fireTargetY = enemy.y + Math.sin(fireAngle) * fireSpeed; // Animate fire particle tween(fireParticle, { x: fireTargetX, y: fireTargetY, scaleX: 0.1, scaleY: 0.1, alpha: 0 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { fireParticle.destroy(); } }); } self.destroy(); return; } self.lastIntersecting[i] = currentIntersecting; } }; return self; }); var GameMenu = Container.expand(function () { var self = Container.call(this); // Menu background image instead of cave background var menuBg = self.attachAsset('menuBackground', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, scaleX: 4.0, scaleY: 4.0 }); menuBg.alpha = 1.0; // Title text var titleText = new Text2('WIZARD DEFENDER', { size: 150, fill: 0xFFD700, font: "monospace" }); titleText.anchor.set(0.5, 0.5); titleText.x = 2048 / 2; titleText.y = 800; self.addChild(titleText); // Instructions text var instructionsText = new Text2('TAP PATHS TO CAST SPELLS\nDEFEND YOUR CASTLE!', { size: 80, fill: 0xFFFFFF, font: "monospace" }); instructionsText.anchor.set(0.5, 0.5); instructionsText.x = 2048 / 2; instructionsText.y = 1200; self.addChild(instructionsText); // Start button var startButton = self.attachAsset('wizard1', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 1500, scaleX: 2, scaleY: 2 }); var startButtonText = new Text2('START GAME', { size: 100, fill: 0x000000, font: "monospace" }); startButtonText.anchor.set(0.5, 0.5); startButtonText.x = 2048 / 2; startButtonText.y = 1600; self.addChild(startButtonText); // Configuration button var configButton = self.attachAsset('pathSelector', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 1800, scaleX: 4, scaleY: 2 }); configButton.tint = 0x4169E1; var configButtonText = new Text2('CONFIGURACION', { size: 80, fill: 0xFFFFFF, font: "monospace" }); configButtonText.anchor.set(0.5, 0.5); configButtonText.x = 2048 / 2; configButtonText.y = 1800; self.addChild(configButtonText); // Shop button var shopButton = self.attachAsset('pathSelector', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2000, scaleX: 4, scaleY: 2 }); shopButton.tint = 0xFF6B35; var shopButtonText = new Text2('TIENDA', { size: 80, fill: 0xFFFFFF, font: "monospace" }); shopButtonText.anchor.set(0.5, 0.5); shopButtonText.x = 2048 / 2; shopButtonText.y = 2000; self.addChild(shopButtonText); // Medals button var medalsButton = self.attachAsset('pathSelector', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2200, scaleX: 4, scaleY: 2 }); medalsButton.tint = 0x9B59B6; var medalsButtonText = new Text2('MEDALLAS', { size: 80, fill: 0xFFFFFF, font: "monospace" }); medalsButtonText.anchor.set(0.5, 0.5); medalsButtonText.x = 2048 / 2; medalsButtonText.y = 2200; self.addChild(medalsButtonText); // Tutorial button (only show if tutorial was completed before) if (storage.tutorialCompleted) { var tutorialButton = self.attachAsset('pathSelector', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2400, scaleX: 4, scaleY: 2 }); tutorialButton.tint = 0x2E8B57; var tutorialButtonText = new Text2('TUTORIAL', { size: 80, fill: 0xFFFFFF, font: "monospace" }); tutorialButtonText.anchor.set(0.5, 0.5); tutorialButtonText.x = 2048 / 2; tutorialButtonText.y = 2400; self.addChild(tutorialButtonText); } // Button interaction self.down = function (x, y, obj) { // Handle configuration menu interactions if (self.configMode) { // Music volume adjustment if (y >= 1150 && y <= 1250) { var currentMusicVolume = storage.musicVolume || 0.7; var newMusicVolume = Math.min(1.0, currentMusicVolume + 0.1); if (newMusicVolume > 1.0) newMusicVolume = 0.0; storage.musicVolume = newMusicVolume; self.musicVolumeText.setText('VOLUMEN MUSICA: ' + Math.round(newMusicVolume * 100) + '%'); return; } // Sound volume adjustment if (y >= 1350 && y <= 1450) { var currentSoundVolume = storage.soundVolume || 1.0; var newSoundVolume = Math.min(1.0, currentSoundVolume + 0.1); if (newSoundVolume > 1.0) newSoundVolume = 0.0; storage.soundVolume = newSoundVolume; self.soundVolumeText.setText('VOLUMEN SONIDO: ' + Math.round(newSoundVolume * 100) + '%'); return; } // Difficulty adjustment if (y >= 1550 && y <= 1650) { var currentDifficulty = storage.difficulty || 'NORMAL'; var difficulties = ['FACIL', 'NORMAL', 'DIFICIL']; var currentIndex = difficulties.indexOf(currentDifficulty); var newIndex = (currentIndex + 1) % difficulties.length; var newDifficulty = difficulties[newIndex]; storage.difficulty = newDifficulty; self.difficultyText.setText('DIFICULTAD: ' + newDifficulty); return; } // Back button if (y >= 1950 && y <= 2050) { self.hideConfigMenu(); return; } // Block all other interactions when config menu is active return; } // Handle shop menu interactions if (self.shopMode) { // Shop item purchase buttons for (var i = 0; i < 3; i++) { var itemY = 1100 + i * 200; if (y >= itemY - 50 && y <= itemY + 50 && x >= 2048 / 2 + 200 && x <= 2048 / 2 + 400) { // Purchase item logic var shopItems = [{ name: 'POCION SALUD', cost: 10 }, { name: 'ESCUDO MAGICO', cost: 15 }, { name: 'ESPADA MALDITA', cost: 20 }]; var item = shopItems[i]; if (coinCounter >= item.cost) { coinCounter -= item.cost; // Apply item effect based on type if (i === 0) { // Health potion if (wizard) { wizard.health = Math.min(wizard.health + 50, wizard.maxHealth); updateHealthBar(); } } else if (i === 1) { // Magic shield if (wizard) { wizard.shieldActive = true; wizard.maxShieldHits = 3; wizard.currentShieldHits = 0; } } else if (i === 2) { // Cursed sword - temporary damage boost if (wizard) { wizard.tempDamageBoost = true; wizard.tempDamageTimer = 1800; // 30 seconds at 60fps } } LK.effects.flashScreen(0x00FF00, 300); } else { LK.effects.flashScreen(0xFF0000, 300); } return; } } // Shop back button if (y >= 1950 && y <= 2050) { self.hideShop(); return; } // Block all other interactions when shop menu is active return; } // Check if configuration button was clicked if (y >= 1700 && y <= 1900 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) { // Show configuration menu self.showConfigMenu(); } else if (y >= 1900 && y <= 2100 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) { // Show shop menu self.showShop(); } else if (y >= 2100 && y <= 2300 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) { // Show medals menu if (prestigeMedals) { prestigeMedals.showMedals(); } } else if (storage.tutorialCompleted && y >= 2300 && y <= 2500 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) { // Show tutorial again if (tutorial) { self.visible = false; tutorial.startTutorial(); } } else if (y >= 1450 && y <= 1650) { // Start the game by hiding menu self.startGame(); } }; self.showConfigMenu = function () { // Create configuration overlay if (!self.configOverlay) { self.configOverlay = self.addChild(LK.getAsset('startMenuBackground', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, scaleX: 1.0, scaleY: 1.0 })); self.configOverlay.alpha = 1.0; self.configOverlay.tint = 0x000000; // Config title var configTitle = new Text2('CONFIGURACION', { size: 120, fill: 0xFFD700, font: "monospace" }); configTitle.anchor.set(0.5, 0.5); configTitle.x = 2048 / 2; configTitle.y = 800; self.addChild(configTitle); self.configTitle = configTitle; // Music volume setting var musicVolumeText = new Text2('VOLUMEN MUSICA: ' + Math.round((storage.musicVolume || 0.7) * 100) + '%', { size: 80, fill: 0xFFFFFF, font: "monospace" }); musicVolumeText.anchor.set(0.5, 0.5); musicVolumeText.x = 2048 / 2; musicVolumeText.y = 1200; self.addChild(musicVolumeText); self.musicVolumeText = musicVolumeText; // Sound volume setting var soundVolumeText = new Text2('VOLUMEN SONIDO: ' + Math.round((storage.soundVolume || 1.0) * 100) + '%', { size: 80, fill: 0xFFFFFF, font: "monospace" }); soundVolumeText.anchor.set(0.5, 0.5); soundVolumeText.x = 2048 / 2; soundVolumeText.y = 1400; self.addChild(soundVolumeText); self.soundVolumeText = soundVolumeText; // Difficulty setting var difficultyText = new Text2('DIFICULTAD: ' + (storage.difficulty || 'NORMAL'), { size: 80, fill: 0xFFFFFF, font: "monospace" }); difficultyText.anchor.set(0.5, 0.5); difficultyText.x = 2048 / 2; difficultyText.y = 1600; self.addChild(difficultyText); self.difficultyText = difficultyText; // Back button var backButton = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2000, scaleX: 2, scaleY: 2 }); backButton.tint = 0x00FF00; self.backButton = backButton; var backText = new Text2('VOLVER', { size: 80, fill: 0xFFFFFF, font: "monospace" }); backText.anchor.set(0.5, 0.5); backText.x = 2048 / 2; backText.y = 2000; self.addChild(backText); self.backText = backText; } self.configOverlay.visible = true; self.configMode = true; }; self.hideConfigMenu = function () { if (self.configOverlay) { self.configOverlay.destroy(); self.configOverlay = null; } // Remove all configuration text elements if (self.musicVolumeText) { self.musicVolumeText.destroy(); self.musicVolumeText = null; } if (self.soundVolumeText) { self.soundVolumeText.destroy(); self.soundVolumeText = null; } if (self.difficultyText) { self.difficultyText.destroy(); self.difficultyText = null; } // Remove back button elements if (self.backButton) { self.backButton.destroy(); self.backButton = null; } if (self.backText) { self.backText.destroy(); self.backText = null; } // Remove configuration title if (self.configTitle) { self.configTitle.destroy(); self.configTitle = null; } // Remove all configuration children that were added for (var i = self.children.length - 1; i >= 0; i--) { var child = self.children[i]; // Remove config-related elements (title, texts, buttons created in showConfigMenu) if (child.setText && child.text && (child.text.includes('CONFIGURACION') || child.text.includes('VOLUMEN') || child.text.includes('DIFICULTAD') || child.text.includes('VOLVER'))) { child.destroy(); } } self.configMode = false; // Reset to show main menu elements self.visible = true; }; self.showShop = function () { // Create shop overlay if (!self.shopOverlay) { self.shopOverlay = self.addChild(LK.getAsset('startMenuBackground', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, scaleX: 1.0, scaleY: 1.0 })); self.shopOverlay.alpha = 1.0; self.shopOverlay.tint = 0x000033; // Shop title var shopTitle = new Text2('TIENDA', { size: 120, fill: 0xFFD700, font: "monospace" }); shopTitle.anchor.set(0.5, 0.5); shopTitle.x = 2048 / 2; shopTitle.y = 800; self.addChild(shopTitle); self.shopTitle = shopTitle; // Shop items var shopItems = [{ name: 'POCION SALUD', description: 'Restaura 50 HP', cost: 10, icon: 'energySphere' }, { name: 'ESCUDO MAGICO', description: 'Bloquea 3 ataques', cost: 15, icon: 'shield' }, { name: 'ESPADA MALDITA', description: 'Daño x2 por 30s', cost: 20, icon: 'spell' }]; // Initialize shop elements arrays if not exists if (!self.shopIcons) self.shopIcons = []; if (!self.shopTexts) self.shopTexts = []; if (!self.shopBuyButtons) self.shopBuyButtons = []; if (!self.shopBuyTexts) self.shopBuyTexts = []; // Display shop items for (var i = 0; i < shopItems.length; i++) { var item = shopItems[i]; var yPos = 1100 + i * 200; // Item icon var itemIcon = self.attachAsset(item.icon, { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2 - 300, y: yPos, scaleX: 2, scaleY: 2 }); itemIcon.tint = 0xFFD700; self.shopIcons.push(itemIcon); // Item text var itemText = new Text2(item.name + '\n' + item.description + '\nCosto: ' + item.cost + ' monedas', { size: 60, fill: 0xFFFFFF, font: "monospace" }); itemText.anchor.set(0, 0.5); itemText.x = 2048 / 2 - 200; itemText.y = yPos; self.addChild(itemText); self.shopTexts.push(itemText); // Buy button var buyButton = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2 + 300, y: yPos, scaleX: 2, scaleY: 2 }); buyButton.tint = 0x00FF00; buyButton.itemIndex = i; self.shopBuyButtons.push(buyButton); var buyText = new Text2('COMPRAR', { size: 50, fill: 0xFFFFFF, font: "monospace" }); buyText.anchor.set(0.5, 0.5); buyText.x = 2048 / 2 + 300; buyText.y = yPos; self.addChild(buyText); self.shopBuyTexts.push(buyText); } // Back button var shopBackButton = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2000, scaleX: 2, scaleY: 2 }); shopBackButton.tint = 0x00FF00; self.shopBackButton = shopBackButton; var shopBackText = new Text2('VOLVER', { size: 80, fill: 0xFFFFFF, font: "monospace" }); shopBackText.anchor.set(0.5, 0.5); shopBackText.x = 2048 / 2; shopBackText.y = 2000; self.addChild(shopBackText); self.shopBackText = shopBackText; } self.shopOverlay.visible = true; self.shopMode = true; }; self.hideShop = function () { if (self.shopOverlay) { self.shopOverlay.destroy(); self.shopOverlay = null; } // Remove shop title if (self.shopTitle) { self.shopTitle.destroy(); self.shopTitle = null; } // Remove shop back button elements if (self.shopBackButton) { self.shopBackButton.destroy(); self.shopBackButton = null; } if (self.shopBackText) { self.shopBackText.destroy(); self.shopBackText = null; } // Remove all shop icons if (self.shopIcons) { for (var i = 0; i < self.shopIcons.length; i++) { if (self.shopIcons[i]) { self.shopIcons[i].destroy(); } } self.shopIcons = []; } // Remove all shop texts if (self.shopTexts) { for (var i = 0; i < self.shopTexts.length; i++) { if (self.shopTexts[i]) { self.shopTexts[i].destroy(); } } self.shopTexts = []; } // Remove all shop buy buttons if (self.shopBuyButtons) { for (var i = 0; i < self.shopBuyButtons.length; i++) { if (self.shopBuyButtons[i]) { self.shopBuyButtons[i].destroy(); } } self.shopBuyButtons = []; } // Remove all shop buy texts if (self.shopBuyTexts) { for (var i = 0; i < self.shopBuyTexts.length; i++) { if (self.shopBuyTexts[i]) { self.shopBuyTexts[i].destroy(); } } self.shopBuyTexts = []; } // Remove all shop children that were added for (var i = self.children.length - 1; i >= 0; i--) { var child = self.children[i]; // Remove shop-related elements if (child.setText && child.text && (child.text.includes('TIENDA') || child.text.includes('POCION') || child.text.includes('ESCUDO') || child.text.includes('ESPADA') || child.text.includes('COMPRAR'))) { child.destroy(); } } self.shopMode = false; // Reset to show main menu elements self.visible = true; }; self.startGame = function () { // Check if this is a new player (no tutorial completed) if (!storage.tutorialCompleted && tutorial) { // Show tutorial for new players self.visible = false; // Small delay to ensure menu is hidden before tutorial starts tween({}, {}, { duration: 100, onFinish: function onFinish() { tutorial.startTutorial(); } }); // Tutorial started successfully return; } // Hide menu and start game normally self.visible = false; gameStarted = true; // Show cave background when game starts if (backgroundMap) { backgroundMap.visible = true; } // Show all game elements wizard.visible = true; for (var i = 0; i < paths.length; i++) { paths[i].visible = true; } // Show all stone path segments and make them visible for (var i = 0; i < game.children.length; i++) { var child = game.children[i]; if (child.pathIndex !== undefined && child !== paths[child.pathIndex]) { child.visible = true; // Check if it's a stone path segment or path number if (child.alpha !== undefined && child.setText === undefined) { child.alpha = 0; // Keep stone paths invisible } } } coinText.visible = true; killCountText.visible = true; tapText.visible = true; healthBarBg.visible = true; healthBar.visible = true; healthText.visible = true; // Start medieval music with user's volume setting var musicVolume = storage.musicVolume || 0.7; LK.playMusic('medievalTheme', { volume: musicVolume, fade: { start: 0, end: musicVolume, duration: 2000 } }); }; return self; }); // Simplified class definitions now handled by BaseEnemy factory functions var Orb = Container.expand(function () { var self = Container.call(this); // Create orb visual using energy sphere var orbGraphics = self.attachAsset('energySphere', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.4, scaleY: 0.4 }); orbGraphics.tint = 0xFFD700; // Golden color for orbs orbGraphics.alpha = 0.9; self.orbitalAngle = 0; // Starting angle for this orb self.orbitalRadius = 880; // Distance from wizard - doubled again for even more separation self.rotationSpeed = 0.025; // How fast orbs rotate - halved for slower movement self.update = function () { // Pause orb when tutorial is active if (tutorial && tutorial.isActive) { return; } // Pause orb when upgrade menu is visible if (upgradeMenu && upgradeMenu.visible) { return; } // Rotate around wizard if (wizard) { self.orbitalAngle += self.rotationSpeed; self.x = wizard.x + Math.cos(self.orbitalAngle) * self.orbitalRadius; self.y = wizard.y + Math.sin(self.orbitalAngle) * self.orbitalRadius - 240; // Position orb much higher up } // Add pulsing effect var pulse = 1 + Math.sin(LK.ticks * 0.3) * 0.2; orbGraphics.scaleX = 0.4 * pulse; orbGraphics.scaleY = 0.4 * pulse; // Check collision with enemies using collision state tracking (but never with wizard) var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses); for (var i = 0; i < allEnemies.length; i++) { var enemy = allEnemies[i]; // Skip collision check with wizard if (enemy === wizard) { continue; } // Initialize collision tracking for this enemy if not exists if (!self.lastIntersecting) { self.lastIntersecting = {}; } if (self.lastIntersecting[i] === undefined) { self.lastIntersecting[i] = false; } var currentIntersecting = self.intersects(enemy); if (!self.lastIntersecting[i] && currentIntersecting) { // Deal damage to enemy on contact transition (first contact only) enemy.takeDamage(50); // Visual effect for orb hit LK.effects.flashObject(self, 0xFFFFFF, 200); // Create orb impact effect var orbImpact = game.addChild(LK.getAsset('energySphere', { anchorX: 0.5, anchorY: 0.5, x: enemy.x, y: enemy.y, scaleX: 0.3, scaleY: 0.3 })); orbImpact.tint = 0xFFD700; orbImpact.alpha = 0.8; // Animate orb impact tween(orbImpact, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 250, easing: tween.easeOut, onFinish: function onFinish() { orbImpact.destroy(); } }); } // Update collision state for this enemy self.lastIntersecting[i] = currentIntersecting; } }; return self; }); var PrestigeMedals = Container.expand(function () { var self = Container.call(this); // Background var medalsBg = self.attachAsset('startMenuBackground', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, scaleX: 1.0, scaleY: 1.0 }); medalsBg.alpha = 1.0; medalsBg.tint = 0x1a1a2e; // Title var titleText = new Text2('MEDALLAS DE PRESTIGIO', { size: 120, fill: 0xFFD700, font: "monospace" }); titleText.anchor.set(0.5, 0.5); titleText.x = 2048 / 2; titleText.y = 600; self.addChild(titleText); // Initialize medals data if not exists if (!storage.medals) { storage.medals = {}; } // Medal definitions var medalDefinitions = [{ id: 'firstKill', name: 'PRIMER ENEMIGO', description: 'Mata tu primer esqueleto', requirement: 'Eliminar 1 enemigo', icon: 'esqueleto1', unlocked: false }, { id: 'tenKills', name: 'CAZADOR', description: 'Mata 10 enemigos', requirement: 'Eliminar 10 enemigos', icon: 'spell', unlocked: false }, { id: 'survivorMedal', name: 'SUPERVIVIENTE', description: 'Mata 25 enemigos', requirement: 'Eliminar 25 enemigos', icon: 'knight1', unlocked: false }, { id: 'masterWizard', name: 'MAESTRO HECHICERO', description: 'Mata 50 enemigos', requirement: 'Eliminar 50 enemigos', icon: 'wizard1', unlocked: false }, { id: 'coinCollector', name: 'COLECCIONISTA', description: 'Consigue 100 monedas', requirement: 'Conseguir 100 monedas', icon: 'coin', unlocked: false }, { id: 'bossSlayer', name: 'MATA JEFES', description: 'Derrota un mini jefe', requirement: 'Eliminar mini jefe', icon: 'ogre1', unlocked: false }, { id: 'legendary', name: 'LEGENDARIO', description: 'Mata 100 enemigos', requirement: 'Eliminar 100 enemigos', icon: 'energySphere', unlocked: false }, { id: 'deathCheater', name: 'BURLANDO LA MUERTE', description: 'Revive cuando mueres', requirement: 'Activar revival', icon: 'shield', unlocked: false }, { id: 'upgradeExpert', name: 'EXPERTO EN MEJORAS', description: 'Compra 5 mejoras', requirement: 'Comprar 5 mejoras', icon: 'pathSelector', unlocked: false }]; // Store references for cleanup self.medalIcons = []; self.medalTexts = []; self.medalBorders = []; // Check and update medal status self.updateMedals = function () { // Get current game stats var totalKills = storage.totalKills || 0; var totalCoins = storage.totalCoins || 0; var totalBossKills = storage.totalBossKills || 0; var totalRevives = storage.totalRevives || 0; var totalUpgrades = storage.totalUpgrades || 0; // Check each medal for (var i = 0; i < medalDefinitions.length; i++) { var medal = medalDefinitions[i]; var isUnlocked = false; switch (medal.id) { case 'firstKill': isUnlocked = totalKills >= 1; break; case 'tenKills': isUnlocked = totalKills >= 10; break; case 'survivorMedal': isUnlocked = totalKills >= 25; break; case 'masterWizard': isUnlocked = totalKills >= 50; break; case 'coinCollector': isUnlocked = totalCoins >= 100; break; case 'bossSlayer': isUnlocked = totalBossKills >= 1; break; case 'legendary': isUnlocked = totalKills >= 100; break; case 'deathCheater': isUnlocked = totalRevives >= 1; break; case 'upgradeExpert': isUnlocked = totalUpgrades >= 5; break; } // Update storage storage.medals[medal.id] = isUnlocked; medal.unlocked = isUnlocked; } }; // Display medals self.displayMedals = function () { var medalRows = 3; var medalCols = 3; var startX = 2048 / 2 - 400; var startY = 1000; var medalSpacing = 300; for (var i = 0; i < medalDefinitions.length; i++) { var medal = medalDefinitions[i]; var row = Math.floor(i / medalCols); var col = i % medalCols; var medalX = startX + col * medalSpacing; var medalY = startY + row * medalSpacing; // Medal background/border var medalBorder = self.attachAsset('pathSelector', { anchorX: 0.5, anchorY: 0.5, x: medalX, y: medalY, scaleX: 3, scaleY: 3 }); medalBorder.tint = medal.unlocked ? 0xFFD700 : 0x555555; medalBorder.alpha = medal.unlocked ? 1.0 : 0.5; self.medalBorders.push(medalBorder); // Medal icon var medalIcon = self.attachAsset(medal.icon, { anchorX: 0.5, anchorY: 0.5, x: medalX, y: medalY, scaleX: 1.5, scaleY: 1.5 }); medalIcon.tint = medal.unlocked ? 0xFFFFFF : 0x333333; medalIcon.alpha = medal.unlocked ? 1.0 : 0.3; self.medalIcons.push(medalIcon); // Medal text var medalText = new Text2(medal.name + '\n' + medal.description, { size: 30, fill: medal.unlocked ? 0xFFFFFF : 0x666666, font: "monospace" }); medalText.anchor.set(0.5, 0); medalText.x = medalX; medalText.y = medalY + 80; self.addChild(medalText); self.medalTexts.push(medalText); // Add shine effect for unlocked medals if (medal.unlocked) { var shine = self.attachAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: medalX, y: medalY, scaleX: 2, scaleY: 2 }); shine.alpha = 0.3; shine.tint = 0xFFD700; // Animate shine tween(shine, { rotation: Math.PI * 2, alpha: 0.1 }, { duration: 2000, easing: tween.linear, loop: true }); } } }; // Back button var backButton = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2400, scaleX: 2, scaleY: 2 }); backButton.tint = 0x00FF00; var backText = new Text2('VOLVER', { size: 80, fill: 0xFFFFFF, font: "monospace" }); backText.anchor.set(0.5, 0.5); backText.x = 2048 / 2; backText.y = 2400; self.addChild(backText); // Handle interactions self.down = function (x, y, obj) { // Check back button if (y >= 2300 && y <= 2500 && x >= 2048 / 2 - 100 && x <= 2048 / 2 + 100) { self.hideMedals(); } }; self.showMedals = function () { self.updateMedals(); self.displayMedals(); self.visible = true; }; self.hideMedals = function () { // Clean up medal elements for (var i = 0; i < self.medalIcons.length; i++) { if (self.medalIcons[i]) { self.medalIcons[i].destroy(); } } for (var i = 0; i < self.medalTexts.length; i++) { if (self.medalTexts[i]) { self.medalTexts[i].destroy(); } } for (var i = 0; i < self.medalBorders.length; i++) { if (self.medalBorders[i]) { self.medalBorders[i].destroy(); } } self.medalIcons = []; self.medalTexts = []; self.medalBorders = []; self.visible = false; }; return self; }); var Projectile = Container.expand(function () { var self = Container.call(this); // Create glowing light effect behind projectile var lightGlow = self.attachAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.0, scaleY: 2.0 }); lightGlow.alpha = 0.15; lightGlow.tint = 0x44aaff; // Blue-white glow // Animate the glow effect tween(lightGlow, { scaleX: 3.5, scaleY: 3.5, alpha: 0.05 }, { duration: 800, easing: tween.easeInOut }); tween(lightGlow, { rotation: Math.PI * 4 }, { duration: 1000, easing: tween.linear }); var projectileGraphics = self.attachAsset('projectile', { anchorX: 0.5, anchorY: 0.5, scaleX: 3.0, scaleY: 3.0 }); // Create spell trail array self.trailSegments = []; self.maxTrailSegments = 8; self.trailTimer = 0; self.speed = 50; self.direction = { x: 0, y: 0 }; self.lastIntersecting = {}; self.targetEnemy = null; // The specific enemy this projectile should damage self.update = function () { // Pause projectile when tutorial is active if (tutorial && tutorial.isActive) { return; } // Move projectile self.x += self.direction.x * self.speed; self.y += self.direction.y * self.speed; // Rotate projectile to point towards movement direction var angle = Math.atan2(self.direction.y, self.direction.x); projectileGraphics.rotation = angle + Math.PI / 2; // Add 90 degrees so top points forward // Simplified visual effect - no trail system for better performance // Remove if off screen if (self.x < -50 || self.x > 2098 || self.y < -50 || self.y > 2782) { self.removeFromGame(); return; } // Only check collision with the target enemy if one is set if (self.targetEnemy && self.targetEnemy.parent) { if (!self.lastIntersecting.target) { self.lastIntersecting.target = false; } var currentTargetIntersecting = self.intersects(self.targetEnemy); if (!self.lastIntersecting.target && currentTargetIntersecting) { // Hit the target enemy // Calculate damage based on spell power upgrade var damage = 100; if (wizard && wizard.spellPowerLevel && wizard.spellPowerLevel > 0) { // Check if target is an ogre and we're before enemy 35 if (self.targetEnemy && self.targetEnemy.ogreFrames && enemyKillCounter < 35) { // Always kill ogres in one hit until enemy 35 damage = self.targetEnemy.health + 1; // Ensure one-hit kill } else { damage = 200; // Double damage when spell power is active } } // Apply combo double damage if active if (nextProjectileDoubleDamage) { damage *= 2; // Double the damage nextProjectileDoubleDamage = false; // Reset the flag after use // Visual feedback for combo damage LK.effects.flashObject(self.targetEnemy, 0xFFD700, 300); // Golden flash on enemy // Create special combo damage indicator var comboDamageText = new Text2('COMBO x2!', { size: 140, fill: 0xFFD700, font: "monospace" }); comboDamageText.anchor.set(0.5, 0.5); comboDamageText.x = self.targetEnemy.x; comboDamageText.y = self.targetEnemy.y - 80; game.addChild(comboDamageText); // Animate combo damage text tween(comboDamageText, { y: comboDamageText.y - 150, scaleX: 1.8, scaleY: 1.8, alpha: 0 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { comboDamageText.destroy(); } }); } self.targetEnemy.takeDamage(damage); self.removeFromGame(); return; } self.lastIntersecting.target = currentTargetIntersecting; } }; self.removeFromGame = function () { // Clean up trail segments for (var i = 0; i < self.trailSegments.length; i++) { var segment = self.trailSegments[i]; if (segment && segment.parent) { segment.destroy(); } } self.trailSegments = []; // Remove from projectiles array for (var i = projectiles.length - 1; i >= 0; i--) { if (projectiles[i] === self) { projectiles.splice(i, 1); break; } } self.destroy(); }; return self; }); var Tutorial = Container.expand(function () { var self = Container.call(this); // Tutorial state variables self.currentStep = 0; self.isActive = false; self.skipped = false; // Track if tutorial was skipped self.tutorialSteps = []; self.highlightElements = []; self.tutorialTexts = []; self.arrows = []; // Tutorial overlay background var tutorialOverlay = self.attachAsset('startMenuBackground', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, scaleX: 1.0, scaleY: 1.0 }); tutorialOverlay.alpha = 0.8; tutorialOverlay.tint = 0x000000; tutorialOverlay.visible = false; // Initially hidden tutorialOverlay.zIndex = 1999; // Ensure proper layering tutorialOverlay.interactive = true; // Always interactive to block clicks // Define tutorial steps self.initializeTutorialSteps = function () { self.tutorialSteps = [{ id: 'welcome', title: 'BIENVENIDO A WIZARD DEFENDER!', description: 'Eres un poderoso mago que debe defender su castillo de hordas de enemigos.', duration: 3000, showSkip: true }, { id: 'enemies_approach', title: 'ENEMIGOS SE ACERCAN!', description: 'Los esqueletos vienen por 5 caminos diferentes hacia ti. Debes detenerlos!', duration: 3000, spawnDemoEnemy: true }, { id: 'tap_to_attack', title: 'TOCA PARA ATACAR', description: 'Toca cualquier parte de la pantalla para lanzar hechizos hacia los enemigos.', highlightElement: 'screen', waitForTap: true }, { id: 'health_system', title: 'CUIDA TU SALUD', description: 'Si los enemigos te alcanzan, perderás vida. No dejes que lleguen a ti!', highlightElement: 'healthBar', duration: 3000 }, { id: 'coins_and_score', title: 'GANA MONEDAS Y PUNTOS', description: 'Elimina enemigos para ganar monedas y aumentar tu puntuación.', highlightElement: 'coinCounter', duration: 3000 }, { id: 'upgrades_intro', title: 'MEJORAS PODEROSAS', description: 'Cada 12 enemigos eliminados podrás elegir mejoras para hacerte más fuerte!', duration: 3000 }, { id: 'survival_goal', title: 'OBJETIVO: SOBREVIVIR', description: 'Sobrevive el mayor tiempo posible y elimina tantos enemigos como puedas!', duration: 3000 }, { id: 'tutorial_complete', title: 'TUTORIAL COMPLETADO!', description: 'Ahora estás listo para la batalla. ¡Buena suerte, valiente mago!', duration: 3000, startGame: true }]; }; // Start the tutorial self.startTutorial = function () { // Always show tutorial when explicitly called self.isActive = true; self.currentStep = 0; // Initialize tutorial steps first self.initializeTutorialSteps(); // Make tutorial visible and properly layered self.visible = true; self.zIndex = 2000; // Configure tutorial overlay properly tutorialOverlay.visible = true; tutorialOverlay.alpha = 0.8; tutorialOverlay.tint = 0x000000; tutorialOverlay.interactive = true; // Block clicks to game below tutorialOverlay.zIndex = 1999; // Ensure proper layering // Hide game menu while tutorial is active if (gameMenu) { gameMenu.visible = false; } // Hide all game elements during tutorial if (wizard) wizard.visible = false; if (backgroundMap) backgroundMap.visible = false; coinText.visible = false; killCountText.visible = false; tapText.visible = false; healthBarBg.visible = false; healthBar.visible = false; healthText.visible = false; for (var i = 0; i < paths.length; i++) { paths[i].visible = false; } // Start first step self.showStep(0); return true; // Tutorial started }; // Show a specific tutorial step self.showStep = function (stepIndex) { if (stepIndex >= self.tutorialSteps.length) { self.completeTutorial(); return; } // Clear previous step elements self.clearStepElements(); var step = self.tutorialSteps[stepIndex]; self.currentStep = stepIndex; // Ensure tutorial is visible and on top self.visible = true; tutorialOverlay.visible = true; self.zIndex = 2000; // Create step title with proper sizing var titleText = new Text2(step.title, { size: 80, fill: 0xFFD700, font: "monospace", wordWrap: true, wordWrapWidth: 1600 }); titleText.anchor.set(0.5, 0.5); titleText.x = 2048 / 2; titleText.y = 900; self.addChild(titleText); self.tutorialTexts.push(titleText); // Create step description with proper text wrapping var descText = new Text2(step.description, { size: 60, fill: 0xFFFFFF, font: "monospace", wordWrap: true, wordWrapWidth: 1800 }); descText.anchor.set(0.5, 0.5); descText.x = 2048 / 2; descText.y = 1300; self.addChild(descText); self.tutorialTexts.push(descText); // Handle special step behaviors if (step.spawnDemoEnemy) { self.spawnDemoEnemy(); } if (step.highlightElement) { self.highlightElement(step.highlightElement); } // Show continue button or wait for specific action if (step.waitForTap) { var tapPrompt = new Text2('TOCA LA PANTALLA PARA CONTINUAR', { size: 50, fill: 0x00FF00, font: "monospace", wordWrap: true, wordWrapWidth: 1400 }); tapPrompt.anchor.set(0.5, 0.5); tapPrompt.x = 2048 / 2; tapPrompt.y = 1800; self.addChild(tapPrompt); self.tutorialTexts.push(tapPrompt); // Add pulsing effect to tap prompt tween(tapPrompt, { scaleX: 1.2, scaleY: 1.2 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(tapPrompt, { scaleX: 1.0, scaleY: 1.0 }, { duration: 800, easing: tween.easeInOut }); } }); } else { // Always show continue prompt - no auto-advance var continuePrompt = new Text2('TOCA LA PANTALLA PARA CONTINUAR', { size: 50, fill: 0x00FF00, font: "monospace", wordWrap: true, wordWrapWidth: 1400 }); continuePrompt.anchor.set(0.5, 0.5); continuePrompt.x = 2048 / 2; continuePrompt.y = 1800; self.addChild(continuePrompt); self.tutorialTexts.push(continuePrompt); // Add pulsing effect to continue prompt tween(continuePrompt, { scaleX: 1.2, scaleY: 1.2 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(continuePrompt, { scaleX: 1.0, scaleY: 1.0 }, { duration: 800, easing: tween.easeInOut }); } }); } // Show skip button on first step if (step.showSkip) { var skipBtn = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5, x: 2048 - 200, y: 200, scaleX: 1.5, scaleY: 1.5 }); skipBtn.tint = 0xFF4444; self.highlightElements.push(skipBtn); var skipText = new Text2('OMITIR', { size: 50, fill: 0xFFFFFF, font: "monospace" }); skipText.anchor.set(0.5, 0.5); skipText.x = 2048 - 200; skipText.y = 200; self.addChild(skipText); self.tutorialTexts.push(skipText); } }; // Highlight specific game elements self.highlightElement = function (elementType) { switch (elementType) { case 'healthBar': if (healthBarBg && healthBar) { // Show health UI temporarily healthBarBg.visible = true; healthBar.visible = true; healthText.visible = true; // Create highlight glow around health bar var healthGlow = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: 270, y: 110, scaleX: 3, scaleY: 1.5 })); healthGlow.tint = 0x00FF00; healthGlow.alpha = 0.6; self.highlightElements.push(healthGlow); // Animate glow tween(healthGlow, { alpha: 0.3 }, { duration: 1000, easing: tween.easeInOut }); } break; case 'coinCounter': if (coinText) { // Show coin UI temporarily coinText.visible = true; coinText.setText('Coins: 15'); // Show example coins // Create highlight glow around coin counter var coinGlow = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: 270, y: 150, scaleX: 3, scaleY: 1.5 })); coinGlow.tint = 0xFFD700; coinGlow.alpha = 0.6; self.highlightElements.push(coinGlow); // Animate glow tween(coinGlow, { alpha: 0.3 }, { duration: 1000, easing: tween.easeInOut }); } break; case 'screen': // Create large highlight overlay var screenGlow = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, scaleX: 20, scaleY: 25 })); screenGlow.tint = 0x00FFFF; screenGlow.alpha = 0.2; self.highlightElements.push(screenGlow); // Animate screen glow tween(screenGlow, { alpha: 0.1 }, { duration: 1500, easing: tween.easeInOut }); // Create arrow pointing to center self.createArrow(2048 / 2, 2732 / 2 - 400, 2048 / 2, 2732 / 2); break; } }; // Create pointing arrow self.createArrow = function (fromX, fromY, toX, toY) { var arrow = game.addChild(LK.getAsset('spell', { anchorX: 0.5, anchorY: 0.5, x: fromX, y: fromY, scaleX: 3, scaleY: 3 })); arrow.tint = 0xFFD700; arrow.alpha = 0.8; // Calculate arrow direction var dx = toX - fromX; var dy = toY - fromY; var angle = Math.atan2(dy, dx); arrow.rotation = angle; // Animate arrow bouncing tween(arrow, { x: toX, y: toY }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(arrow, { x: fromX, y: fromY }, { duration: 1000, easing: tween.easeInOut }); } }); self.arrows.push(arrow); }; // Spawn a demo enemy for tutorial self.spawnDemoEnemy = function () { // Create a slow-moving demo enemy var demoEnemy = game.addChild(new Enemy()); demoEnemy.x = 2048 / 2; demoEnemy.y = -100; demoEnemy.speed = 1; // Very slow for demo demoEnemy.health = 1; demoEnemy.maxHealth = 1; demoEnemy.pathIndex = 0; // Center path // Make demo enemy more visible for (var frameIdx = 0; frameIdx < demoEnemy.skeletonFrames.length; frameIdx++) { demoEnemy.skeletonFrames[frameIdx].tint = 0xFF6600; // Orange tint } enemies.push(demoEnemy); // Create highlight around demo enemy var enemyGlow = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: demoEnemy.x, y: demoEnemy.y, scaleX: 4, scaleY: 4 })); enemyGlow.tint = 0xFF0000; enemyGlow.alpha = 0.5; // Make glow follow enemy var _glowFollower = function glowFollower() { if (demoEnemy && demoEnemy.parent && enemyGlow && enemyGlow.parent) { enemyGlow.x = demoEnemy.x; enemyGlow.y = demoEnemy.y; // Continue following tween({}, {}, { duration: 60, onFinish: _glowFollower }); } else if (enemyGlow && enemyGlow.parent) { enemyGlow.destroy(); } }; _glowFollower(); self.highlightElements.push(enemyGlow); }; // Clear all tutorial step elements self.clearStepElements = function () { // Clear tutorial texts for (var i = 0; i < self.tutorialTexts.length; i++) { if (self.tutorialTexts[i] && self.tutorialTexts[i].parent) { self.tutorialTexts[i].destroy(); } } self.tutorialTexts = []; // Clear highlight elements for (var i = 0; i < self.highlightElements.length; i++) { if (self.highlightElements[i] && self.highlightElements[i].parent) { self.highlightElements[i].destroy(); } } self.highlightElements = []; // Clear arrows for (var i = 0; i < self.arrows.length; i++) { if (self.arrows[i] && self.arrows[i].parent) { self.arrows[i].destroy(); } } self.arrows = []; }; // Advance to next tutorial step self.nextStep = function () { self.currentStep++; if (self.currentStep < self.tutorialSteps.length) { self.showStep(self.currentStep); } else { // Tutorial completed naturally (not skipped) self.skipped = false; self.completeTutorial(); } }; // Complete the tutorial self.completeTutorial = function () { // Mark tutorial as completed storage.tutorialCompleted = true; // Clear all tutorial elements self.clearStepElements(); // Hide tutorial completely tutorialOverlay.visible = false; tutorialOverlay.interactive = false; // Remove interactivity self.visible = false; self.isActive = false; // Only auto-start game if tutorial was completed naturally (not skipped) if (!self.skipped) { // Start game immediately without showing menu if (gameMenu) { gameMenu.startGame(); } } else { // If skipped, show the menu if (gameMenu) { gameMenu.visible = true; } } // Reset skipped flag for next time self.skipped = false; }; // Handle tutorial interactions self.down = function (x, y, obj) { if (!self.isActive) return; var step = self.tutorialSteps[self.currentStep]; // Handle skip button (top-right corner) if (step.showSkip && x >= 2048 - 300 && x <= 2048 - 100 && y >= 100 && y <= 300) { self.skipped = true; // Mark as skipped self.completeTutorial(); return; } // For any tap anywhere on screen, advance to next step if (step.id === 'tap_to_attack') { // Simulate spell casting for demo if (wizard) { wizard.attack(0); // Attack center path } // Wait a moment then advance tween({}, {}, { duration: 1000, onFinish: function onFinish() { self.nextStep(); } }); } else { // For all other steps, advance immediately on any tap self.nextStep(); } }; return self; }); var UpgradeMenu = Container.expand(function () { var self = Container.call(this); // Set high z-index to ensure menu appears above all game elements self.zIndex = 1000; // Cooldown system for upgrade menu self.cooldownDuration = 120; // 2 seconds at 60fps self.cooldownTimer = 0; // Background removed - no overlay needed // Title text var titleText = new Text2('UPGRADES', { size: 120, fill: 0xFFD700, font: "monospace" }); titleText.anchor.set(0.5, 0.5); titleText.x = 2048 / 2; titleText.y = 1050; self.addChild(titleText); // Initialize upgrade cost self.upgradeCost = 5; // Store references to upgrade text elements for updates self.upgradeTextElements = []; // Upgrade options - expanded to 10 upgrades var upgradeOptions = [{ name: 'SHIELD', description: 'Block 1 attack' }, { name: 'HEALTH BOOST', description: 'More health' }, { name: 'LIFE DRAIN', description: '5% chance +10 HP' }, { name: 'ENERGY SPHERE', description: 'Auto-attack every 3s' }, { name: 'FORCE PUSH', description: 'Push enemies back' }, { name: 'SPELL POWER', description: 'Double damage' }, { name: 'THORNS', description: 'Spikes on all paths' }, { name: 'FIRE BALL', description: 'Launch fire ball every 3s' }, { name: 'FREEZE PULSE', description: 'Freeze all enemies' }, { name: 'ORBS', description: 'Orbs rotate around player' }]; // Select 3 alternating upgrades from the available options var selectedUpgrades = []; var availableIndices = []; for (var i = 0; i < upgradeOptions.length; i++) { availableIndices.push(i); } // Track which upgrades were shown last time if (!self.lastShownUpgrades) { self.lastShownUpgrades = []; } // Remove previously shown upgrades from available options if possible if (self.lastShownUpgrades.length > 0 && availableIndices.length > 3) { for (var lastIdx = 0; lastIdx < self.lastShownUpgrades.length; lastIdx++) { var indexToRemove = availableIndices.indexOf(self.lastShownUpgrades[lastIdx]); if (indexToRemove !== -1) { availableIndices.splice(indexToRemove, 1); } } } // If we don't have enough unique options, add back some previously shown ones if (availableIndices.length < 3) { availableIndices = []; for (var i = 0; i < upgradeOptions.length; i++) { availableIndices.push(i); } } // Randomly select 3 unique upgrades from available options for (var selection = 0; selection < 3; selection++) { var randomIndex = Math.floor(Math.random() * availableIndices.length); selectedUpgrades.push(availableIndices[randomIndex]); availableIndices.splice(randomIndex, 1); } // Store current selection for next time self.lastShownUpgrades = selectedUpgrades.slice(); // Copy array self.selectedUpgradeIndices = selectedUpgrades; // Store for reference // Create upgrade buttons in single row layout for 3 upgrades for (var i = 0; i < selectedUpgrades.length; i++) { var upgradeIndex = selectedUpgrades[i]; var upgrade = upgradeOptions[upgradeIndex]; // Calculate position in single row, 3-column grid var xPos = 2048 / 2 - 500 + i * 500; // Space 3 upgrades across screen width with more separation var yPos = 1400; // Single row positioned lower // Upgrade button background var upgradeBtn = self.attachAsset('pathSelector', { anchorX: 0.5, anchorY: 0.5, x: xPos, y: yPos, scaleX: 6, scaleY: 4.5 }); upgradeBtn.upgradeIndex = upgradeIndex; upgradeBtn.displayIndex = i; // Store display index for touch detection upgradeBtn.tint = 0x8B008B; // Upgrade text var upgradeText = new Text2(upgrade.name + '\n' + upgrade.description + '\nCost: ' + self.upgradeCost + ' coins', { size: 40, fill: 0xFFFFFF, font: "monospace" }); upgradeText.anchor.set(0.5, 0.5); upgradeText.x = xPos; upgradeText.y = yPos; upgradeText.upgradeIndex = upgradeIndex; // Store actual upgrade index for reference upgradeText.displayIndex = i; // Store display index for touch detection upgradeText.upgradeName = upgrade.name; upgradeText.upgradeDescription = upgrade.description; self.addChild(upgradeText); self.upgradeTextElements.push(upgradeText); // Store reference for updates } // Close button var closeBtn = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 1900, scaleX: 2, scaleY: 2 }); closeBtn.tint = 0xFF0000; var closeText = new Text2('CLOSE', { size: 80, fill: 0xFFFFFF, font: "monospace" }); closeText.anchor.set(0.5, 0.5); closeText.x = 2048 / 2; closeText.y = 1900; self.addChild(closeText); // Method to refresh upgrade selection for alternating self.refreshUpgradeSelection = function () { var upgradeOptions = [{ name: 'SHIELD', key: 'shield', baseDesc: 'Block 1 attack', improvedDesc: 'Block 2 attacks + regen' }, { name: 'HEALTH BOOST', key: 'healthBoost', baseDesc: 'More health', improvedDesc: 'Much more health' }, { name: 'LIFE DRAIN', key: 'lifeDrain', baseDesc: '20% chance +20 HP', improvedDesc: '20% chance +20 HP' }, { name: 'ENERGY SPHERE', key: 'energySphere', baseDesc: 'Auto-attack every 3s', improvedDesc: 'Auto-attack every 2s' }, { name: 'FORCE PUSH', key: 'forcePush', baseDesc: 'Push enemies back', improvedDesc: 'Stronger push + damage' }, { name: 'SPELL POWER', key: 'spellPower', baseDesc: 'Double damage', improvedDesc: 'Triple damage' }, { name: 'THORNS', key: 'thorns', baseDesc: 'Spikes on all paths', improvedDesc: 'Stronger spikes + faster' }, { name: 'FIRE BALL', key: 'fireBall', baseDesc: 'Launch fire ball every 3s', improvedDesc: 'Launch fire ball every 2s' }, { name: 'FREEZE PULSE', key: 'freezePulse', baseDesc: 'Freeze all enemies', improvedDesc: 'Freeze longer + damage' }, { name: 'ORBS', key: 'orbs', baseDesc: 'Orbs rotate around player', improvedDesc: 'More orbs + faster rotation' }]; // Select new alternating upgrades var selectedUpgrades = []; var availableIndices = []; for (var i = 0; i < upgradeOptions.length; i++) { availableIndices.push(i); } // Remove previously shown upgrades from available options if possible if (self.lastShownUpgrades && self.lastShownUpgrades.length > 0 && availableIndices.length > 3) { for (var lastIdx = 0; lastIdx < self.lastShownUpgrades.length; lastIdx++) { var indexToRemove = availableIndices.indexOf(self.lastShownUpgrades[lastIdx]); if (indexToRemove !== -1) { availableIndices.splice(indexToRemove, 1); } } } // If we don't have enough unique options, add back some previously shown ones if (availableIndices.length < 3) { availableIndices = []; for (var i = 0; i < upgradeOptions.length; i++) { availableIndices.push(i); } } // Select 3 unique upgrades from available options for (var selection = 0; selection < 3; selection++) { var randomIndex = Math.floor(Math.random() * availableIndices.length); selectedUpgrades.push(availableIndices[randomIndex]); availableIndices.splice(randomIndex, 1); } // Store current selection for next time self.lastShownUpgrades = selectedUpgrades.slice(); self.selectedUpgradeIndices = selectedUpgrades; // Update upgrade text elements with new selections for (var i = 0; i < self.upgradeTextElements.length && i < selectedUpgrades.length; i++) { var upgradeIndex = selectedUpgrades[i]; var upgrade = upgradeOptions[upgradeIndex]; var textElement = self.upgradeTextElements[i]; // Update the text element properties textElement.upgradeIndex = upgradeIndex; textElement.displayIndex = i; textElement.upgradeName = upgrade.name; textElement.upgradeDescription = upgrade.baseDesc; } }; // Method to update upgrade text with new costs and show improved versions self.updateUpgradeCosts = function () { // First refresh the upgrade selection to ensure alternating upgrades self.refreshUpgradeSelection(); var upgradeOptions = [{ name: 'SHIELD', key: 'shield', baseDesc: 'Block 1 attack', improvedDesc: 'Block 2 attacks + regen' }, { name: 'HEALTH BOOST', key: 'healthBoost', baseDesc: 'More health', improvedDesc: 'Much more health' }, { name: 'LIFE DRAIN', key: 'lifeDrain', baseDesc: '20% chance +20 HP', improvedDesc: '20% chance +20 HP' }, { name: 'ENERGY SPHERE', key: 'energySphere', baseDesc: 'Auto-attack every 3s', improvedDesc: 'Auto-attack every 2s' }, { name: 'FORCE PUSH', key: 'forcePush', baseDesc: 'Push enemies back', improvedDesc: 'Stronger push + damage' }, { name: 'SPELL POWER', key: 'spellPower', baseDesc: 'Double damage', improvedDesc: 'Triple damage' }, { name: 'THORNS', key: 'thorns', baseDesc: 'Spikes on all paths', improvedDesc: 'Stronger spikes + faster' }, { name: 'FIRE BALL', key: 'fireBall', baseDesc: 'Launch fire ball every 3s', improvedDesc: 'Launch fire ball every 2s' }, { name: 'FREEZE PULSE', key: 'freezePulse', baseDesc: 'Freeze all enemies', improvedDesc: 'Freeze longer + damage' }, { name: 'ORBS', key: 'orbs', baseDesc: 'Orbs rotate around player', improvedDesc: 'More orbs + faster rotation' }]; // Randomly select one upgrade to have modified cost (20% chance) var hasModifiedCost = Math.random() < 0.20; // 20% probability var modifiedCostIndex = hasModifiedCost ? Math.floor(Math.random() * 3) : -1; // Random upgrade among the 3 for (var i = 0; i < self.upgradeTextElements.length; i++) { var textElement = self.upgradeTextElements[i]; var upgradeIndex = textElement.upgradeIndex; // Use stored upgrade index var upgrade = upgradeOptions[upgradeIndex]; var currentLevel = upgradeLevels[upgrade.key]; // Calculate cost for this upgrade var upgradeCost = self.upgradeCost; if (i === modifiedCostIndex) { upgradeCost = Math.floor(self.upgradeCost * 0.95); // 5% decrease, rounded down } // Show improved version if already purchased var description = currentLevel > 0 ? upgrade.improvedDesc : upgrade.baseDesc; var levelText = currentLevel > 0 ? ' (LV' + (currentLevel + 1) + ')' : ''; textElement.setText(upgrade.name + levelText + '\n' + description + '\nCost: ' + upgradeCost + ' coins'); textElement.upgradeName = upgrade.name; textElement.upgradeDescription = description; textElement.actualCost = upgradeCost; // Store the actual cost for purchase logic } }; // Update method for cooldown timer self.update = function () { if (self.cooldownTimer > 0) { self.cooldownTimer--; // Apply visual feedback during cooldown var cooldownAlpha = 0.3 + self.cooldownTimer / self.cooldownDuration * 0.7; self.alpha = cooldownAlpha; } else { self.alpha = 1.0; // Full opacity when cooldown is over } }; // Handle upgrade purchases self.down = function (x, y, obj) { // Check if cooldown is active if (self.cooldownTimer > 0) { // Flash red to indicate cooldown is active LK.effects.flashScreen(0xFF0000, 200); return; // Exit early if cooldown is active } // Determine which upgrade was clicked based on 3-upgrade layout var upgradeIndex = -1; // Check which upgrade cell was clicked (single row, 3 columns) for (var i = 0; i < 3; i++) { var cellX = 2048 / 2 - 500 + i * 500; // Calculate cell center X for 3-upgrade layout with more separation var cellY = 1400; // Single row Y position var cellWidth = 400; // Touch area width - increased to match new spacing var cellHeight = 250; // Touch area height // Check if click is within this cell if (x >= cellX - cellWidth / 2 && x <= cellX + cellWidth / 2 && y >= cellY - cellHeight / 2 && y <= cellY + cellHeight / 2) { upgradeIndex = self.selectedUpgradeIndices[i]; // Get actual upgrade index from selected list break; } } // Check if upgrade button was clicked if (upgradeIndex !== -1) { // Close menu immediately when any upgrade option is selected self.closeMenu(); // Get the actual cost from the text element var actualCost = self.upgradeTextElements[0].actualCost || self.upgradeCost || 5; // Default fallback for (var textIdx = 0; textIdx < self.upgradeTextElements.length; textIdx++) { if (self.upgradeTextElements[textIdx].displayIndex === Math.floor(upgradeIndex / 1)) { actualCost = self.upgradeTextElements[textIdx].actualCost || self.upgradeCost || 5; break; } } // Find the correct text element by matching the upgrade selection for (var cellIdx = 0; cellIdx < 3; cellIdx++) { var cellX = 2048 / 2 - 500 + cellIdx * 500; var cellY = 1400; var cellWidth = 400; var cellHeight = 250; if (x >= cellX - cellWidth / 2 && x <= cellX + cellWidth / 2 && y >= cellY - cellHeight / 2 && y <= cellY + cellHeight / 2) { actualCost = self.upgradeTextElements[cellIdx].actualCost || self.upgradeCost || 5; break; } } if (coinCounter >= actualCost) { coinCounter -= actualCost; coinText.setText('Coins: ' + coinCounter); // Apply upgrade based on index switch (upgradeIndex) { case 0: // Shield upgrade upgradeLevels.shield++; if (upgradeLevels.shield === 1) { // First level: Basic shield wizard.shieldActive = true; wizard.maxShieldHits = 1; wizard.currentShieldHits = 0; } else { // Improved level: Double shield + regeneration wizard.shieldActive = true; wizard.maxShieldHits = 2; wizard.currentShieldHits = 0; wizard.shieldRegen = true; // Enable shield regeneration } // Visual feedback for shield activation LK.effects.flashObject(wizard, 0x00BFFF, 500); // Create shield activation particles for (var shieldIdx = 0; shieldIdx < 10; shieldIdx++) { var shieldParticle = game.addChild(LK.getAsset('shield', { anchorX: 0.5, anchorY: 0.5, x: wizard.x + (Math.random() - 0.5) * 200, y: wizard.y + (Math.random() - 0.5) * 200, scaleX: 0.5, scaleY: 0.5 })); shieldParticle.tint = 0x00BFFF; shieldParticle.alpha = 0.8; // Animate shield particles tween(shieldParticle, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 500 + Math.random() * 300, easing: tween.easeOut, onFinish: function onFinish() { shieldParticle.destroy(); } }); } // Add shield activation animation effect tween(wizard.shieldGraphics, { scaleX: 5, scaleY: 5, alpha: 1.0 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { tween(wizard.shieldGraphics, { scaleX: 3, scaleY: 3, alpha: 0.7 }, { duration: 300, easing: tween.easeIn }); } }); break; case 1: // Health Boost upgrade upgradeLevels.healthBoost++; var healthIncrease = 40; wizard.maxHealth += healthIncrease; wizard.health = Math.min(wizard.health + healthIncrease, wizard.maxHealth); updateHealthBar(); break; case 2: // Life Drain upgrade upgradeLevels.lifeDrain++; if (!wizard.lifeDrainLevel) { wizard.lifeDrainLevel = 0; } wizard.lifeDrainLevel = upgradeLevels.lifeDrain; break; case 3: // Energy Sphere upgrade upgradeLevels.energySphere++; if (upgradeLevels.energySphere === 1) { // First level: Create energy sphere if (!wizard.energySphere) { wizard.energySphere = game.addChild(new EnergyOrb()); } } // Visual feedback for energy sphere activation LK.effects.flashObject(wizard, 0x00FFFF, 500); break; case 4: // Force Push upgrade upgradeLevels.forcePush++; if (!wizard.forcePushLevel) { wizard.forcePushLevel = 0; wizard.forcePushTimer = 0; } wizard.forcePushLevel = upgradeLevels.forcePush; // Visual feedback for force push LK.effects.flashObject(wizard, 0xFFFF00, 500); break; case 5: // Spell Power upgrade upgradeLevels.spellPower++; if (!wizard.spellPowerLevel) { wizard.spellPowerLevel = 0; } wizard.spellPowerLevel = upgradeLevels.spellPower; // Visual feedback for spell power LK.effects.flashObject(wizard, 0xFF4500, 500); break; case 6: // Thorns upgrade upgradeLevels.thorns++; if (!wizard.thornsLevel) { wizard.thornsLevel = 0; wizard.thornsTimer = 0; } wizard.thornsLevel = upgradeLevels.thorns; // Visual feedback for thorns LK.effects.flashObject(wizard, 0x8B4513, 500); break; case 7: // Fire Ball upgrade upgradeLevels.fireBall++; if (!wizard.fireBallLevel) { wizard.fireBallLevel = 0; wizard.fireBallTimer = 0; } wizard.fireBallLevel = upgradeLevels.fireBall; // Visual feedback for fire ball LK.effects.flashObject(wizard, 0xFF4500, 500); break; case 8: // Freeze Pulse upgrade upgradeLevels.freezePulse++; if (!wizard.freezePulseLevel) { wizard.freezePulseLevel = 0; wizard.freezePulseTimer = 0; } wizard.freezePulseLevel = upgradeLevels.freezePulse; // Visual feedback for freeze pulse LK.effects.flashObject(wizard, 0x87CEEB, 500); break; case 9: // Orbs upgrade upgradeLevels.orbs++; if (!wizard.orbLevel) { wizard.orbLevel = 0; wizard.orbs = []; // Array to store orb instances } wizard.orbLevel++; // Create new orb var newOrb = game.addChild(new Orb()); // Set starting angle based on number of orbs for even distribution newOrb.orbitalAngle = wizard.orbs.length * (Math.PI * 2) / (wizard.orbLevel + 1); wizard.orbs.push(newOrb); // Redistribute existing orbs evenly for (var orbIdx = 0; orbIdx < wizard.orbs.length; orbIdx++) { wizard.orbs[orbIdx].orbitalAngle = orbIdx * (Math.PI * 2) / wizard.orbs.length; } // Visual feedback for orb upgrade LK.effects.flashObject(wizard, 0xFFD700, 500); break; } // Visual feedback - flash the upgrade area green LK.effects.flashScreen(0x00FF00, 300); // Update medal statistics if (!storage.totalUpgrades) storage.totalUpgrades = 0; storage.totalUpgrades++; } else { // Not enough coins - flash red LK.effects.flashScreen(0xFF0000, 300); } } else if (y >= 1750 && y <= 2050) { // Close button area clicked self.closeMenu(); } }; self.closeMenu = function () { self.visible = false; gameStarted = true; // Resume appropriate music based on game progress if (enemyKillCounter >= 25) { LK.playMusic('mysticalAmbient', { volume: 0.6, fade: { start: 0, end: 0.6, duration: 1000 } }); } else if (enemyKillCounter >= 10) { LK.playMusic('epicBattle', { volume: 0.8, fade: { start: 0, end: 0.8, duration: 1000 } }); } else { LK.playMusic('medievalTheme', { volume: 0.7, fade: { start: 0, end: 0.7, duration: 1000 } }); } }; return self; }); var Wizard = Container.expand(function () { var self = Container.call(this); // Animation system for wizard self.currentFrame = 1; self.animationTimer = 0; self.animationSpeed = 18; // Change frame every 18 ticks (300ms at 60fps) // Create all wizard graphics frames and store them self.wizardFrames = []; for (var i = 1; i <= 4; i++) { var frameGraphics = self.attachAsset('wizard' + i, { anchorX: 0.5, anchorY: 1.0, scaleX: 2.5, scaleY: 2.5 }); frameGraphics.visible = i === 1; // Only show first frame initially self.wizardFrames.push(frameGraphics); } // Create invisible hitbox with much smaller size for more precise collision var hitbox = self.attachAsset('wizard1', { anchorX: 0.3, anchorY: 1.0, scaleX: 0.25, // Much smaller size for very precise collision scaleY: 0.3 // Much smaller size for very precise collision }); hitbox.alpha = 0; // Make hitbox invisible // Position hitbox slightly to the right to reduce left side hitbox.x = 15; // Offset hitbox to the right // Create shield visual effect self.shieldGraphics = self.attachAsset('shield', { anchorX: 0.5, anchorY: 0.5, scaleX: 3, scaleY: 3 }); self.shieldGraphics.alpha = 0.7; self.shieldGraphics.visible = false; self.attackCooldown = 0; self.level = 1; self.experience = 0; self.health = 100; self.maxHealth = 100; self.spellPowerLevel = 0; // Track spell power upgrades self.shieldActive = false; // Track shield status self.orbLevel = 0; // Track orb upgrades self.orbs = []; // Array to store orb instances // Override intersects method to use smaller hitbox self.intersects = function (other) { return hitbox.intersects(other); }; self.update = function () { // Pause wizard when tutorial is active if (tutorial && tutorial.isActive) { return; } // Pause all upgrades when upgrade menu is visible if (upgradeMenu && upgradeMenu.visible) { return; } if (self.attackCooldown > 0) { self.attackCooldown--; } // Update shield visibility based on shield status self.shieldGraphics.visible = self.shieldActive; if (self.shieldActive) { // Animate shield with pulsing effect var pulse = 1 + Math.sin(LK.ticks * 0.15) * 0.2; self.shieldGraphics.scaleX = 3 * pulse; self.shieldGraphics.scaleY = 3 * pulse; // Slowly rotate shield self.shieldGraphics.rotation += 0.03; // Add glowing effect self.shieldGraphics.alpha = 0.6 + Math.sin(LK.ticks * 0.1) * 0.2; } // Force Push upgrade - push all enemies backward every 4 seconds if (self.forcePushLevel && self.forcePushLevel > 0) { if (!self.forcePushTimer) { self.forcePushTimer = 0; } self.forcePushTimer++; if (self.forcePushTimer >= 240) { // 4 seconds at 60fps self.forcePushTimer = 0; self.activateForcePush(); } } // Freeze Pulse upgrade - freeze all enemies every 4 seconds if (self.freezePulseLevel && self.freezePulseLevel > 0) { if (!self.freezePulseTimer) { self.freezePulseTimer = 0; } self.freezePulseTimer++; if (self.freezePulseTimer >= 240) { // 4 seconds at 60fps self.freezePulseTimer = 0; self.activateFreezePulse(); } } // Fire Ball upgrade - launch fire ball with improved timing if (self.fireBallLevel && self.fireBallLevel > 0) { if (!self.fireBallTimer) { self.fireBallTimer = 0; } self.fireBallTimer++; var fireBallInterval = upgradeLevels.fireBall > 1 ? 120 : 180; // 2s vs 3s if (self.fireBallTimer >= fireBallInterval) { self.fireBallTimer = 0; self.launchFireBall(); } } // Thorns upgrade - spawn spikes along all paths every 3 seconds if (self.thornsLevel && self.thornsLevel > 0) { if (!self.thornsTimer) { self.thornsTimer = 0; } self.thornsTimer++; if (self.thornsTimer >= 180) { // Always 3 seconds (180 ticks at 60fps) self.thornsTimer = 0; self.activateThorns(); } } // Animation system - cycle through wizard frames self.animationTimer++; if (self.animationTimer >= self.animationSpeed) { self.animationTimer = 0; // Hide current frame self.wizardFrames[self.currentFrame - 1].visible = false; // Move to next frame self.currentFrame++; if (self.currentFrame > 4) { self.currentFrame = 1; } // Show new frame self.wizardFrames[self.currentFrame - 1].visible = true; } }; self.attack = function (direction) { if (self.attackCooldown <= 0) { // Default direction if none specified if (direction === undefined) { direction = 0; // Default to center path } // Get attack angle based on path direction var attackAngle = pathAngles[direction]; var attackDistance = 100; // Calculate spell position based on attack direction var spellX = self.x + Math.cos(attackAngle) * attackDistance; var spellY = self.y + Math.sin(attackAngle) * attackDistance; // Create spell effect var spell = game.addChild(LK.getAsset('spell', { anchorX: 0.5, anchorY: 0.5, x: spellX, y: spellY, scaleX: 0.5, scaleY: 0.5 })); // Animate spell with magical effects tween(spell, { scaleX: 2, scaleY: 2, alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { spell.destroy(); } }); // Add rotation animation to spell tween(spell, { rotation: Math.PI * 2 }, { duration: 500, easing: tween.linear }); self.attackCooldown = 30; // 0.5 seconds at 60fps LK.getSound('spellCast').play(); // Base damage without spell power upgrades var totalDamage = 1; // Attack enemies in the specified direction/path for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; if (enemy.pathIndex === direction) { // Only hit enemies on exact same path - no distance validation enemy.takeDamage(totalDamage); } } // Attack ogres in the specified direction/path for (var i = ogres.length - 1; i >= 0; i--) { var ogre = ogres[i]; if (ogre.pathIndex === direction) { // Only hit ogres on exact same path - no distance validation ogre.takeDamage(totalDamage); } } // Attack knights in the specified direction/path for (var i = knights.length - 1; i >= 0; i--) { var knight = knights[i]; if (knight.pathIndex === direction) { // Only hit knights on exact same path - no distance validation knight.takeDamage(totalDamage); } } return true; } return false; }; self.gainExperience = function (amount) { self.experience += amount; var expNeeded = self.level * 100; if (self.experience >= expNeeded) { self.levelUp(); } }; self.levelUp = function () { self.level++; self.experience = 0; // Visual level up effect LK.effects.flashObject(self, 0xFFD700, 500); }; self.takeDamage = function (damage) { // Check if shield is active if (self.shieldActive) { // Initialize shield properties if not set if (self.maxShieldHits === undefined) { self.maxShieldHits = 1; self.currentShieldHits = 0; } // Increment shield hits self.currentShieldHits++; // Visual feedback for shield use LK.effects.flashObject(self, 0x00BFFF, 300); // Check if shield is depleted if (self.currentShieldHits >= self.maxShieldHits) { self.shieldActive = false; // Start shield regeneration timer var regenTime = self.shieldRegen ? 5000 : 10000; // Faster regen if improved tween({}, {}, { duration: regenTime, onFinish: function onFinish() { // Regenerate shield self.shieldActive = true; self.currentShieldHits = 0; // Visual feedback for shield regeneration LK.effects.flashObject(self, 0x00BFFF, 500); // Add shield regeneration animation tween(self.shieldGraphics, { scaleX: 5, scaleY: 5, alpha: 1.0 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { tween(self.shieldGraphics, { scaleX: 3, scaleY: 3, alpha: 0.7 }, { duration: 400, easing: tween.easeIn }); } }); } }); } // No damage taken, shield absorbed it return; } self.health -= damage; if (self.health <= 0) { self.health = 0; // 10% chance to revive when dying var reviveChance = Math.random(); if (reviveChance < 0.10) { // Revival successful! self.health = Math.floor(self.maxHealth * 0.5); // Revive with 50% health // Destroy ALL enemies when revival activates (no distance restriction) var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses); for (var enemyIdx = allEnemies.length - 1; enemyIdx >= 0; enemyIdx--) { var enemy = allEnemies[enemyIdx]; // Create destruction effect for each enemy LK.effects.flashObject(enemy, 0xFFD700, 500); // Create golden explosion particles for (var particleIdx = 0; particleIdx < 8; particleIdx++) { var destructionParticle = game.addChild(LK.getAsset('energySphere', { anchorX: 0.5, anchorY: 0.5, x: enemy.x, y: enemy.y, scaleX: 0.5, scaleY: 0.5 })); destructionParticle.tint = 0xFFD700; destructionParticle.alpha = 0.9; // Random explosion direction var explosionAngle = particleIdx / 8 * Math.PI * 2; var explosionSpeed = 60 + Math.random() * 40; var explosionTargetX = enemy.x + Math.cos(explosionAngle) * explosionSpeed; var explosionTargetY = enemy.y + Math.sin(explosionAngle) * explosionSpeed; // Animate explosion particle tween(destructionParticle, { x: explosionTargetX, y: explosionTargetY, scaleX: 0.1, scaleY: 0.1, alpha: 0 }, { duration: 400 + Math.random() * 200, easing: tween.easeOut, onFinish: function onFinish() { destructionParticle.destroy(); } }); } // Kill ALL enemies instantly by calling die() method enemy.die(); } // Visual effects for revival LK.effects.flashScreen(0x00FF00, 1500); // Green flash for revival LK.effects.flashObject(self, 0xFFD700, 1000); // Golden flash on wizard // Reduced revival particles for better performance for (var reviveIdx = 0; reviveIdx < 8; reviveIdx++) { var reviveParticle = game.addChild(LK.getAsset('energySphere', { anchorX: 0.5, anchorY: 0.5, x: self.x + (Math.random() - 0.5) * 200, y: self.y + (Math.random() - 0.5) * 200, scaleX: 1.0, scaleY: 1.0 })); reviveParticle.tint = 0x00FF00; reviveParticle.alpha = 0.9; // Animate revival particles toward wizard tween(reviveParticle, { x: self.x, y: self.y, scaleX: 0.3, scaleY: 0.3, alpha: 0 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { reviveParticle.destroy(); } }); } // Create healing aura effect var healingAura = game.addChild(LK.getAsset('shield', { anchorX: 0.5, anchorY: 0.5, x: self.x, y: self.y, scaleX: 1, scaleY: 1 })); healingAura.tint = 0x00FF00; healingAura.alpha = 0.8; // Animate healing aura expansion tween(healingAura, { scaleX: 8, scaleY: 8, alpha: 0 }, { duration: 1200, easing: tween.easeOut, onFinish: function onFinish() { healingAura.destroy(); } }); // Play spell cast sound for revival LK.getSound('spellCast').play(); // Update health bar to show revival updateHealthBar(); // Update medal statistics if (!storage.totalRevives) storage.totalRevives = 0; storage.totalRevives++; } else { // Game over when health reaches 0 and no revival LK.effects.flashScreen(0xFF0000, 1000); LK.showGameOver(); } } // Update health bar updateHealthBar(); // Simplified screen shake for better performance var shakeIntensity = 8; var originalX = game.x; var originalY = game.y; // Simple single shake effect tween(game, { x: originalX + shakeIntensity, y: originalY + shakeIntensity * 0.5 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(game, { x: originalX, y: originalY }, { duration: 100, easing: tween.easeIn }); } }); // Flash red when hit LK.effects.flashObject(self, 0xFF0000, 200); }; self.activateForcePush = function () { // Visual effect for force push activation LK.effects.flashScreen(0x8A2BE2, 300); // Purple flash LK.effects.flashObject(self, 0x8A2BE2, 500); // Purple flash on wizard // Push back all enemies with improved effects var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses); for (var i = 0; i < allEnemies.length; i++) { var enemy = allEnemies[i]; // Calculate direction from wizard to enemy var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { // Improved force push: stronger push and damage var pushDistance = upgradeLevels.forcePush > 1 ? 300 : 200; // Stronger push var pushX = dx / distance * pushDistance; var pushY = dy / distance * pushDistance; // Calculate new position var newX = enemy.x + pushX; var newY = enemy.y + pushY; // Ensure enemies don't go off screen newX = Math.max(50, Math.min(1998, newX)); newY = Math.max(-100, Math.min(2732, newY)); // Animate the push effect tween(enemy, { x: newX, y: newY }, { duration: 300, easing: tween.easeOut }); // Improved force push: deal damage if (upgradeLevels.forcePush > 1) { enemy.takeDamage(50); } // Visual effect on each enemy LK.effects.flashObject(enemy, 0x8A2BE2, 200); } } }; self.activateFreezePulse = function () { // Visual effect for freeze pulse activation LK.effects.flashScreen(0x87CEEB, 500); // Light blue flash LK.effects.flashObject(self, 0x87CEEB, 700); // Light blue flash on wizard // Play freeze sound effect LK.getSound('iceFreeze').play(); // Freeze all enemies with improved effects var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses); for (var i = 0; i < allEnemies.length; i++) { var enemy = allEnemies[i]; // Improved freeze: longer duration and damage var freezeDuration = upgradeLevels.freezePulse > 1 ? 120 : 60; // 2s vs 1s enemy.frozen = true; enemy.frozenTimer = freezeDuration; // Improved freeze: deal damage if (upgradeLevels.freezePulse > 1) { enemy.takeDamage(30); } // Visual freeze effect - tint enemy light blue tween(enemy, { tint: 0x87CEEB }, { duration: 100, easing: tween.easeOut }); // Reduced ice crystal particles for better performance if (i % 2 === 0) { // Only create effects for every other enemy for (var iceIdx = 0; iceIdx < 3; iceIdx++) { var iceCrystal = game.addChild(LK.getAsset('spell', { anchorX: 0.5, anchorY: 0.5, x: enemy.x + (Math.random() - 0.5) * 60, y: enemy.y + (Math.random() - 0.5) * 60, scaleX: 1.0, scaleY: 1.0 })); iceCrystal.tint = 0x87CEEB; iceCrystal.alpha = 0.9; // Create floating ice effect tween(iceCrystal, { y: iceCrystal.y - 30, scaleX: 0.3, scaleY: 0.3, alpha: 0 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { iceCrystal.destroy(); } }); } } // Remove freeze tint after frozen state ends var visualDuration = upgradeLevels.freezePulse > 1 ? 2000 : 1000; tween({}, {}, { duration: visualDuration, onFinish: function onFinish() { if (enemy && enemy.parent) { // Remove freeze tint after frozen effect ends tween(enemy, { tint: 0xFFFFFF }, { duration: 200, easing: tween.easeIn }); } } }); } }; self.activateThorns = function () { // Visual effect for thorns activation LK.effects.flashScreen(0x8B4513, 300); // Brown flash LK.effects.flashObject(self, 0x8B4513, 500); // Brown flash on wizard // Find the closest enemy to the wizard var closestEnemy = null; var closestDistance = Infinity; var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses); for (var i = 0; i < allEnemies.length; i++) { var enemy = allEnemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // Only create spikes if there is a closest enemy if (!closestEnemy) { LK.getSound('spellCast').play(); return; } // Create spikes only along the closest enemy's path var pathIdx = closestEnemy.pathIndex; var pathAngle = pathAngles[pathIdx]; // Calculate spawn position for this path (same as enemy spawning) var spawnX, spawnY; if (pathIdx === 0) { // Center path - spawn at top edge spawnX = 2048 / 2; spawnY = -100; } else if (pathIdx === 1) { // Path 2 - spawn at top right edge spawnX = 2048 + 50; spawnY = -50; } else if (pathIdx === 2) { // Path 3 - spawn at top left edge spawnX = -50; spawnY = -50; } else if (pathIdx === 3) { // Path 4 - spawn at left edge spawnX = -100; spawnY = 2732 / 2 + 400; } else if (pathIdx === 4) { // Path 5 - spawn at right edge spawnX = 2048 + 100; spawnY = 2732 / 2 + 400; } // Calculate wizard position (same as enemy targeting) var wizardX = self.x; var wizardY = self.y; // Calculate path distance and divide into 3 sections with gaps var pathDistance = Math.sqrt((spawnX - wizardX) * (spawnX - wizardX) + (spawnY - wizardY) * (spawnY - wizardY)); var sectionLength = pathDistance / 5; // Each section is 1/5 of total path var gapLength = pathDistance / 10; // Gaps are 1/10 of total path // Define 3 sections along the path with gaps between them var sections = [{ start: 0.1, end: 0.3 }, // First section: 10% to 30% along path { start: 0.45, end: 0.65 }, // Second section: 45% to 65% along path { start: 0.8, end: 1.0 } // Third section: 80% to 100% along path ]; // Create spikes in reverse sequential order: last section, then middle, then first var sectionOrder = [2, 1, 0]; // Create sections starting from farthest outward toward wizard for (var orderIdx = 0; orderIdx < sectionOrder.length; orderIdx++) { var sectionIdx = sectionOrder[orderIdx]; var section = sections[sectionIdx]; var sectionStartDistance = pathDistance * section.start; var sectionEndDistance = pathDistance * section.end; var spikeSpacing = 150; // Distance between spikes within each section // Calculate number of spikes in this section var sectionLength = sectionEndDistance - sectionStartDistance; var numSpikesInSection = Math.floor(sectionLength / spikeSpacing); // Calculate delay for sequential appearance var baseDelay = orderIdx * 300; // 300ms delay between sections // Create spikes within this section with sequential timing for (var s = 0; s < numSpikesInSection; s++) { var spikeDistanceInSection = s * spikeSpacing + spikeSpacing / 2; var totalSpikeDistance = sectionStartDistance + spikeDistanceInSection; var progress = totalSpikeDistance / pathDistance; var spikeX = spawnX + (wizardX - spawnX) * progress; var spikeY = spawnY + (wizardY - spawnY) * progress; // Only create spike if position is within game bounds if (spikeX >= 0 && spikeX <= 2048 && spikeY >= 0 && spikeY <= 2732) { // Create spike with delay (function (delayTime, spikeX, spikeY, pathIdx) { tween({}, {}, { duration: delayTime, onFinish: function onFinish() { var spike = game.addChild(LK.getAsset('spell', { anchorX: 0.5, anchorY: 0.5, x: spikeX, y: spikeY, scaleX: 0.1, scaleY: 0.1 })); spike.tint = 0x8B4513; // Brown color for thorns spike.pathIndex = pathIdx; // Initialize hit tracking for this spike spike.hitEnemies = []; // Set spike to be visible immediately spike.alpha = 1.0; spike.scaleX = 1.5; spike.scaleY = 1.5; // Animate spike emerging from ground tween(spike, { scaleX: 2.5, scaleY: 2.5, alpha: 1.0 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { // Spike stays for a moment then disappears tween(spike, { scaleX: 0.1, scaleY: 0.1, alpha: 0 }, { duration: 800, easing: tween.easeIn, onFinish: function onFinish() { spike.destroy(); } }); } }); } }); })(baseDelay + s * 50, spikeX, spikeY, pathIdx); // 50ms delay between spikes in same section } } } LK.getSound('spellCast').play(); }; self.launchFireBall = function () { // Visual effect for fire ball launch LK.effects.flashScreen(0xFF4500, 300); // Orange flash LK.effects.flashObject(self, 0xFF4500, 500); // Orange flash on wizard // Create fire ball projectile at wizard position var fireBall = game.addChild(new FireBall()); fireBall.x = self.x; fireBall.y = self.y; // Find closest enemy to target var closestEnemy = null; var closestDistance = Infinity; var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses); for (var i = 0; i < allEnemies.length; i++) { var enemy = allEnemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // Set fire ball direction toward closest enemy or default direction if (closestEnemy) { var dx = closestEnemy.x - self.x; var dy = closestEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { fireBall.direction.x = dx / distance; fireBall.direction.y = dy / distance; } } else { // Default direction upward if no enemies fireBall.direction.x = 0; fireBall.direction.y = -1; } LK.getSound('spellCast').play(); LK.getSound('fireWhoosh').play(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 // Black background for pixel art }); /**** * Game Code ****/ // Unified helper functions // Game arrays to track objects function createImpactEffect(x, y, color, scale) { var impact = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: x, y: y, scaleX: 0.3, scaleY: 0.3 })); impact.tint = color; impact.alpha = 1.0; tween(impact, { scaleX: scale, scaleY: scale, alpha: 0 }, { duration: 450, easing: tween.easeOut, onFinish: function onFinish() { impact.destroy(); } }); } function dropCoin(x, y) { var coin = game.addChild(new Coin()); coin.x = x; coin.y = y - 50; coin.isAnimating = true; coins.push(coin); var targetX = 120 + coinText.width / 2, targetY = 90 + coinText.height / 2; tween(coin, { x: targetX, y: targetY, scaleX: 0.5, scaleY: 0.5 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { var difficulty = storage.difficulty || 'NORMAL'; var reward = difficulty === 'FACIL' ? Math.floor(1.5) : difficulty === 'DIFICIL' ? Math.max(1, Math.floor(0.75)) : 1; coinCounter += reward; coinText.setText('Coins: ' + coinCounter); if (difficulty === 'FACIL' && Math.random() < 0.15) { wizard.health = Math.min(wizard.health + 5, wizard.maxHealth); updateHealthBar(); LK.effects.flashObject(wizard, 0x00FF00, 200); } removeFromArray(coin, coins); coin.destroy(); } }); } function updatePathCombo(pathIndex) { if (pathIndex !== undefined) { pathKillCounts[pathIndex]++; if (pathKillCounts[pathIndex] >= 3) { pathKillCounts[pathIndex] = 0; nextProjectileDoubleDamage = true; LK.effects.flashScreen(0xFFD700, 500); var comboText = new Text2('combo', { size: 120, fill: 0xFFD700, font: "monospace" }); comboText.anchor.set(0.5, 0.5); comboText.x = 2048 / 2; comboText.y = 2732 / 2 - 200; game.addChild(comboText); tween(comboText, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { comboText.destroy(); } }); } } } function removeFromArray(obj, array) { array = array || (obj.enemyType === 'skeleton' ? enemies : obj.enemyType === 'ogre' ? ogres : obj.enemyType === 'knight' ? knights : miniBosses); for (var i = array.length - 1; i >= 0; i--) { if (array[i] === obj) { array.splice(i, 1); break; } } } function getSpawnPath() { if (enemyKillCounter < 5) return 0; // Center path for first 5 var availablePaths = []; for (var i = 0; i < 5; i++) { if (pathConsecutiveSpawns[i] < 2) availablePaths.push(i); } if (availablePaths.length === 0) { for (var i = 0; i < 5; i++) { pathConsecutiveSpawns[i] = 0; availablePaths.push(i); } } return availablePaths[Math.floor(Math.random() * availablePaths.length)]; } function getSpawnPosition(pathIndex) { var positions = [{ x: 2048 / 2, y: -100 }, // Center { x: 2048 + 50, y: -50 }, // Top right { x: -50, y: -50 }, // Top left { x: -100, y: 2732 / 2 + 400 }, // Left { x: 2048 + 100, y: 2732 / 2 + 400 } // Right ]; return positions[pathIndex]; } function spawnEnemy(type, difficulty, healthMult, speedMult) { var enemy, array; var baseStats = { skeleton: { health: 100, speed: 3, eliteChance: 0.20, eliteThreshold: 20 }, ogre: { health: 200, speed: 2.5, eliteChance: 0.15, eliteThreshold: 30 }, knight: { health: 300, speed: 2, eliteChance: 0.10, eliteThreshold: 40 }, miniboss: { health: 3000, speed: 4, eliteChance: 0, eliteThreshold: 999 } }; var stats = baseStats[type]; // Create enemy using BaseEnemy constructor directly if (type === 'skeleton') { enemy = game.addChild(new BaseEnemy({ type: 'skeleton', assetBase: 'esqueleto', health: 1, speed: 3, score: 10, experience: 25 })); array = enemies; } else if (type === 'ogre') { enemy = game.addChild(new BaseEnemy({ type: 'ogre', assetBase: 'ogre', health: 2, speed: 3, score: 15, experience: 25, vibration: 75, impactColor: 0xFF4500, damageColor: 0xFF6600 })); array = ogres; } else if (type === 'berserkerOgre') { enemy = game.addChild(new BaseEnemy({ type: 'ogre', assetBase: 'berserkerOgre', health: 2, speed: 3, score: 15, experience: 25, vibration: 75, impactColor: 0xFF4500, damageColor: 0xFF6600 })); array = ogres; } else if (type === 'knight') { enemy = game.addChild(new BaseEnemy({ type: 'knight', assetBase: 'knight', health: 3, speed: 3, score: 20, experience: 25, vibration: 100, impactColor: 0xFFD700, damageColor: 0xFFD700 })); array = knights; } else if (type === 'miniboss') { enemy = game.addChild(new BaseEnemy({ type: 'miniboss', assetBase: 'knight', health: 3000, speed: 4, score: 100, experience: 250, scale: 4.0, vibration: 100, impactColor: 0xFF0000, damageColor: 0xFF0000, animSpeed: 12 })); array = miniBosses; } // Apply difficulty scaling var diffMods = { FACIL: { healthMod: 0.8, speedMod: 0.9 }, NORMAL: { healthMod: 1.0, speedMod: 1.0 }, DIFICIL: { healthMod: 1.0, speedMod: 1.1 } }; var mods = diffMods[difficulty] || diffMods.NORMAL; enemy.health = Math.floor(baseStats[type].health * mods.healthMod * healthMult); enemy.speed = baseStats[type].speed * mods.speedMod * speedMult; enemy.maxHealth = enemy.health; // Elite variants for hard mode if (difficulty === 'DIFICIL' && enemyKillCounter >= stats.eliteThreshold && Math.random() < stats.eliteChance) { enemy.health *= type === 'knight' ? 1.67 : 1.5; // Champion knights: 500hp enemy.speed *= type === 'knight' ? 1.5 : 1.3; var tintColor = type === 'skeleton' ? 0xFF6600 : type === 'ogre' ? 0xFF0000 : 0xFFD700; for (var i = 0; i < enemy.frames.length; i++) enemy.frames[i].tint = tintColor; } // Position enemy var pathIndex = type === 'miniboss' ? 0 : getSpawnPath(); var pos = getSpawnPosition(pathIndex); enemy.x = Math.max(50, Math.min(1998, pos.x)); enemy.y = Math.max(-200, Math.min(2732 + 100, pos.y)); enemy.pathIndex = pathIndex; enemy.pathAngle = pathAngles[pathIndex]; enemy.lastX = enemy.x; // Update path tracking pathConsecutiveSpawns[pathIndex]++; pathLastSpawnTime[pathIndex] = LK.ticks; // Special setup for miniboss if (type === 'miniboss' && enemy.updateHealthBar) enemy.updateHealthBar(); array.push(enemy); } var gameStarted = false; var gameMenu; var upgradeMenu; var upgradeMenuShown = false; var secondUpgradeMenuShown = false; var thirdUpgradeMenuShown = false; var fourthUpgradeMenuShown = false; var selectedEnemy = null; // Track currently selected enemy for projectile targeting var energySphere = null; // Track energy sphere instance // Upgrade level tracking system var upgradeLevels = { shield: 0, healthBoost: 0, lifeDrain: 0, energySphere: 0, forcePush: 0, spellPower: 0, thorns: 0, fireBall: 0, freezePulse: 0, orbs: 0 }; // Game arrays to track objects var enemies = []; var ogres = []; var knights = []; var miniBosses = []; var coins = []; var projectiles = []; // Path combo system variables var pathKillCounts = [0, 0, 0, 0, 0]; // Track kills per path (5 paths) var nextProjectileDoubleDamage = false; // Flag for double damage on next projectile // Create tutorial system first (initially hidden) var tutorial = game.addChild(new Tutorial()); tutorial.visible = false; // Create and show game menu gameMenu = game.addChild(new GameMenu()); // Create upgrade menu (initially hidden) upgradeMenu = game.addChild(new UpgradeMenu()); upgradeMenu.visible = false; // Create prestige medals menu (initially hidden) var prestigeMedals = game.addChild(new PrestigeMedals()); prestigeMedals.visible = false; // Create 5 stone paths leading toward the wizard position var paths = []; var knightX = 2048 / 2; var knightY = 2732 - 250; // Create paths: all pointing toward the wizard position var pathAngles = [-Math.PI / 2, // Center (straight down toward wizard) -Math.PI / 3, // Diagonal right toward wizard -2 * Math.PI / 3, // Diagonal left toward wizard Math.PI / 6, // Path 4: opposite to path 2 (diagonal left) 5 * Math.PI / 6 // Path 5: opposite to path 3 (diagonal right) ]; // 5 paths: all directed toward wizard position // Create multiple stone segments for each path to form visible stone roads for (var p = 0; p < 5; p++) { var angle = pathAngles[p]; // Calculate distance from spawn point to wizard position var wizardX = knightX; var wizardY = 2732 - 600; // Wizard position var spawnDistance = p === 0 ? 2000 : p === 3 || p === 4 ? 1200 : 1800; // Distance from wizard to spawn point // Set spawn points to screen edges for all paths var spawnX, spawnY; if (p === 0) { // Center path - spawn at top edge spawnX = 2048 / 2; spawnY = -100; } else if (p === 1) { // Path 2 - spawn at top right edge spawnX = 2048 + 50; spawnY = -50; } else if (p === 2) { // Path 3 - spawn at top left edge spawnX = -50; spawnY = -50; } else if (p === 3) { // Path 4 - spawn at left edge spawnX = -100; spawnY = 2732 / 2 + 400; } else if (p === 4) { // Path 5 - spawn at right edge spawnX = 2048 + 100; spawnY = 2732 / 2 + 400; } // Calculate actual path length from spawn point to wizard var actualPathLength = Math.sqrt((spawnX - wizardX) * (spawnX - wizardX) + (spawnY - wizardY) * (spawnY - wizardY)); var segmentSize = 120; // Size of each stone segment var numSegments = Math.floor(actualPathLength / segmentSize); // Create individual stone segments along the path from spawn point toward wizard for (var s = 0; s < numSegments; s++) { var segmentDistance = s * segmentSize + segmentSize / 2; var segmentX = spawnX - Math.cos(angle) * segmentDistance; var segmentY = spawnY - Math.sin(angle) * segmentDistance; // Only create segments that are within the visible game area if (segmentX >= -100 && segmentX <= 2148 && segmentY >= -100 && segmentY <= 2832) { // Create stone path segment var stoneSegment = game.addChild(LK.getAsset('stonePath', { anchorX: 0.5, anchorY: 0.5, x: segmentX, y: segmentY, scaleX: 2.0, scaleY: 2.0, rotation: angle + Math.PI / 2 // Rotate stone to align with path })); // Make stone paths completely invisible stoneSegment.alpha = 0; // Make completely invisible stoneSegment.visible = false; // Initially hidden, will be shown when game starts stoneSegment.pathIndex = p; } } } // Create invisible path collision areas for touch detection for (var p = 0; p < 5; p++) { var angle = pathAngles[p]; // Calculate distance from spawn point to wizard position var wizardX = knightX; var wizardY = 2732 - 600; // Wizard position var spawnDistance = p === 0 ? 3000 : p === 3 || p === 4 ? 1200 : 1800; // Distance from wizard to spawn point var spawnX = wizardX + Math.cos(angle) * spawnDistance; var spawnY = wizardY + Math.sin(angle) * spawnDistance; // Move all spawn points to screen edges if (p === 0) { // Center path - spawn at top edge spawnX = 2048 / 2; spawnY = -100; } else if (p === 1) { // Path 2 - spawn at top right edge spawnX = 2048 + 50; spawnY = -50; } else if (p === 2) { // Path 3 - spawn at top left edge spawnX = -50; spawnY = -50; } else if (p === 3) { // Path 4 - spawn at left edge spawnX = -100; spawnY = 2732 / 2 + 400; } else if (p === 4) { // Path 5 - spawn at right edge spawnX = 2048 + 100; spawnY = 2732 / 2 + 400; } // Calculate actual path length from spawn point to wizard var actualPathLength = Math.sqrt((spawnX - wizardX) * (spawnX - wizardX) + (spawnY - wizardY) * (spawnY - wizardY)); var centerX = (spawnX + wizardX) / 2; var centerY = (spawnY + wizardY) / 2; // Create invisible collision path var path = game.addChild(LK.getAsset('pathSelector', { anchorX: 0.5, anchorY: 0.5, x: centerX, y: centerY, scaleX: 4, scaleY: actualPathLength / 60, rotation: angle + Math.PI / 2 })); // Make collision path completely invisible path.alpha = 0; path.visible = false; path.pathIndex = p; // Add numbered text label for each path var pathNumber = new Text2((p + 1).toString(), { size: 120, fill: 0xFFD700, font: "monospace" }); pathNumber.anchor.set(0.5, 0.5); // Position number directly at spawn point pathNumber.x = spawnX; pathNumber.y = spawnY - 80; // Position slightly above spawn point pathNumber.visible = false; // Initially hidden, will be shown when game starts pathNumber.pathIndex = p; game.addChild(pathNumber); // Add touch handler for directional attacks path.down = function (x, y, obj) { // Attack in this path's direction wizard.attack(obj.pathIndex); // Visual feedback - no flash for invisible paths // Paths remain invisible when tapped }; paths.push(path); } // Pixel art scaling handled by engine automatically // Set fondodelacueva as the actual game background var backgroundMap = game.addChild(LK.getAsset('fondodelacueva', { anchorX: 0, anchorY: 0, scaleX: 19.5, scaleY: 26.0, x: 0, y: 0 })); // Send background to the back but use a less extreme z-index backgroundMap.zIndex = -100; // Hide background initially during menu backgroundMap.visible = false; backgroundMap.alpha = 1.0; // Create wizard var wizard = game.addChild(new Wizard()); wizard.x = knightX; wizard.y = 2732 - 600; // Position wizard higher on screen wizard.visible = false; // UI Elements // Removed scoreText and levelText to eliminate stray characters in top right var coinCounter = 0; var enemyKillCounter = 0; var coinText = new Text2('Coins: 0', { size: 60, fill: 0xFFD700, font: "monospace" }); coinText.anchor.set(0, 0); LK.gui.topLeft.addChild(coinText); coinText.x = 120; coinText.y = 90; coinText.visible = false; var killCountText = new Text2('Puntuacion: 0', { size: 60, fill: 0xFF6B6B, font: "monospace" }); killCountText.anchor.set(0, 0); LK.gui.topLeft.addChild(killCountText); killCountText.x = 120; killCountText.y = 150; killCountText.visible = false; var tapText = new Text2('TAP TO CAST SPELLS!', { size: 100, fill: 0xFF6B6B, font: "monospace" }); tapText.anchor.set(0.5, 0.5); LK.gui.center.addChild(tapText); tapText.y = -200; tapText.visible = false; // Health bar UI var healthBarBg = LK.getAsset('healthBarBg', { anchorX: 0, anchorY: 0 }); LK.gui.topLeft.addChild(healthBarBg); healthBarBg.x = 120; healthBarBg.y = 20; healthBarBg.visible = false; var healthBar = LK.getAsset('healthBar', { anchorX: 0, anchorY: 0 }); LK.gui.topLeft.addChild(healthBar); healthBar.x = 120; healthBar.y = 20; healthBar.visible = false; var healthText = new Text2('Health: 100/100', { size: 50, fill: 0xFFFFFF, font: "monospace" }); healthText.anchor.set(0, 0); LK.gui.topLeft.addChild(healthText); healthText.x = 120; healthText.y = 50; healthText.visible = false; function updateHealthBar() { var healthPercent = wizard.health / wizard.maxHealth; healthBar.scaleX = healthPercent; healthText.setText('Health: ' + wizard.health + '/' + wizard.maxHealth); // Change color based on health if (healthPercent > 0.6) { healthBar.tint = 0x00ff00; // Green } else if (healthPercent > 0.3) { healthBar.tint = 0xffff00; // Yellow } else { healthBar.tint = 0xff0000; // Red } } // Enemy spawning variables var enemySpawnTimer = 0; var lastSpawnedPath = -1; // Track the last spawned path var consecutiveSpawns = 0; // Track consecutive spawns from same path // Cooldown system variables var pathLastSpawnTime = [-1, -1, -1, -1, -1]; // Track last spawn time for each path var pathConsecutiveSpawns = [0, 0, 0, 0, 0]; // Track consecutive spawns per path var pathCooldownDuration = 300; // 5 seconds at 60fps // Game input handling game.down = function (x, y, obj) { // Check if a path was tapped for directional attack var pathTapped = false; for (var p = 0; p < paths.length; p++) { var path = paths[p]; // Convert tap position to path's local coordinates var localPos = path.toLocal({ x: x, y: y }); // Check if tap is within path bounds if (Math.abs(localPos.x) < path.width / 2 && Math.abs(localPos.y) < path.height / 2) { // Attack in this path's direction wizard.attack(path.pathIndex); pathTapped = true; break; } } // No default attack - player must tap a path to attack }; // Main game update loop game.update = function () { // Sort children by z-index to ensure proper rendering order game.children.sort(function (a, b) { return (a.zIndex || 0) - (b.zIndex || 0); }); // Pause game when tutorial is active if (tutorial && tutorial.isActive) { return; } // Only update game logic if game has started if (!gameStarted) { return; } // Change music to epic battle theme at 10 enemies killed if (enemyKillCounter === 10) { LK.playMusic('epicBattle', { volume: 0.8, fade: { start: 0, end: 0.8, duration: 1500 } }); } // Change to mystical ambient music at 25 enemies killed if (enemyKillCounter === 25) { LK.playMusic('mysticalAmbient', { volume: 0.6, fade: { start: 0, end: 0.6, duration: 2000 } }); } // Show upgrade menu after 12 enemies are killed if (enemyKillCounter >= 12 && !upgradeMenuShown) { upgradeMenuShown = true; // Increase upgrade costs by 5 coins when menu is shown if (!upgradeMenu.upgradeCost) { upgradeMenu.upgradeCost = 5; // Initial cost } upgradeMenu.upgradeCost += 5; // Increase by 5 coins each time // Update all upgrade text to show new costs upgradeMenu.updateUpgradeCosts(); upgradeMenu.visible = true; upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown gameStarted = false; // Pause game while menu is open // Keep music playing during upgrade menu // Clear all enemies on screen when upgrade menu is shown for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; enemies.splice(i, 1); enemy.destroy(); } // Clear all ogres on screen when upgrade menu is shown for (var i = ogres.length - 1; i >= 0; i--) { var ogre = ogres[i]; ogres.splice(i, 1); ogre.destroy(); } // Clear all knights on screen when upgrade menu is shown for (var i = knights.length - 1; i >= 0; i--) { var knight = knights[i]; knights.splice(i, 1); knight.destroy(); } } // Show upgrade menu again after 35 enemies are killed if (enemyKillCounter >= 35 && upgradeMenuShown && !secondUpgradeMenuShown) { secondUpgradeMenuShown = true; // Increase upgrade costs by 5 coins when menu is shown if (!upgradeMenu.upgradeCost) { upgradeMenu.upgradeCost = 5; // Initial cost } upgradeMenu.upgradeCost += 5; // Increase by 5 coins each time // Update all upgrade text to show new costs upgradeMenu.updateUpgradeCosts(); upgradeMenu.visible = true; upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown gameStarted = false; // Pause game while menu is open // Keep music playing during upgrade menu // Clear all enemies on screen when upgrade menu is shown for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; enemies.splice(i, 1); enemy.destroy(); } // Clear all ogres on screen when upgrade menu is shown for (var i = ogres.length - 1; i >= 0; i--) { var ogre = ogres[i]; ogres.splice(i, 1); ogre.destroy(); } // Clear all knights on screen when upgrade menu is shown for (var i = knights.length - 1; i >= 0; i--) { var knight = knights[i]; knights.splice(i, 1); knight.destroy(); } } // Show third upgrade menu after 50 enemies are killed if (enemyKillCounter >= 50 && secondUpgradeMenuShown && !thirdUpgradeMenuShown) { thirdUpgradeMenuShown = true; // Increase upgrade costs by 5 coins when menu is shown if (!upgradeMenu.upgradeCost) { upgradeMenu.upgradeCost = 5; // Initial cost } upgradeMenu.upgradeCost += 5; // Increase by 5 coins each time // Update all upgrade text to show new costs upgradeMenu.updateUpgradeCosts(); upgradeMenu.visible = true; upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown gameStarted = false; // Pause game while menu is open // Keep music playing during upgrade menu // Clear all enemies on screen when upgrade menu is shown for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; enemies.splice(i, 1); enemy.destroy(); } // Clear all ogres on screen when upgrade menu is shown for (var i = ogres.length - 1; i >= 0; i--) { var ogre = ogres[i]; ogres.splice(i, 1); ogre.destroy(); } // Clear all knights on screen when upgrade menu is shown for (var i = knights.length - 1; i >= 0; i--) { var knight = knights[i]; knights.splice(i, 1); knight.destroy(); } } // Show fourth upgrade menu after 70 enemies are killed if (enemyKillCounter >= 70 && thirdUpgradeMenuShown && !fourthUpgradeMenuShown) { fourthUpgradeMenuShown = true; // Increase upgrade costs by 5 coins when menu is shown if (!upgradeMenu.upgradeCost) { upgradeMenu.upgradeCost = 5; // Initial cost } upgradeMenu.upgradeCost += 5; // Increase by 5 coins each time // Update all upgrade text to show new costs upgradeMenu.updateUpgradeCosts(); upgradeMenu.visible = true; upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown gameStarted = false; // Pause game while menu is open // Keep music playing during upgrade menu // Clear all enemies on screen when upgrade menu is shown for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; enemies.splice(i, 1); enemy.destroy(); } // Clear all ogres on screen when upgrade menu is shown for (var i = ogres.length - 1; i >= 0; i--) { var ogre = ogres[i]; ogres.splice(i, 1); ogre.destroy(); } // Clear all knights on screen when upgrade menu is shown for (var i = knights.length - 1; i >= 0; i--) { var knight = knights[i]; knights.splice(i, 1); knight.destroy(); } } // Reset consecutive spawns for paths that have cooled down (runs every frame) for (var pathIdx = 0; pathIdx < 5; pathIdx++) { // Check if enough time has passed since last spawn (cooldown expired) if (pathLastSpawnTime[pathIdx] !== -1 && LK.ticks - pathLastSpawnTime[pathIdx] > pathCooldownDuration) { // Reset consecutive spawns for this path due to cooldown pathConsecutiveSpawns[pathIdx] = 0; } } // Get stored difficulty setting var selectedDifficulty = storage.difficulty || 'NORMAL'; var difficultyLevel = Math.floor(enemyKillCounter / 10); // Every 10 kills increases difficulty // Apply unique difficulty modifiers var currentSpawnRate, enemyHealthMultiplier, enemySpeedMultiplier; var specialMechanics = {}; if (selectedDifficulty === 'FACIL') { // EASY: Slower enemies, less health, longer spawn intervals currentSpawnRate = Math.max(60, 120 - difficultyLevel * 5); // Much slower spawning enemyHealthMultiplier = 1; // No health scaling over time enemySpeedMultiplier = 1 + difficultyLevel * 0.20; // 20% speed increase per level specialMechanics.bonusCoins = true; // 50% more coins specialMechanics.healingChance = 0.15; // 15% chance to heal 5 HP on enemy kill } else if (selectedDifficulty === 'NORMAL') { // NORMAL: Balanced progression currentSpawnRate = Math.max(40, 90 - difficultyLevel * 6); // Standard spawning enemyHealthMultiplier = 1; // No health scaling over time enemySpeedMultiplier = 1 + difficultyLevel * 0.30; // 30% speed increase per level specialMechanics.standardRewards = true; } else if (selectedDifficulty === 'DIFICIL') { // HARD: Faster enemies, more health, shorter spawn intervals, special enemy abilities currentSpawnRate = Math.max(20, 60 - difficultyLevel * 4); // Much faster spawning enemyHealthMultiplier = 1; // No health scaling over time enemySpeedMultiplier = 1 + difficultyLevel * 0.50; // 50% speed increase per level specialMechanics.eliteEnemies = true; // 20% chance for elite enemies with double stats specialMechanics.aggressiveAI = true; // Enemies move more directly toward wizard specialMechanics.reducedCoins = true; // 25% fewer coins } // Unified enemy spawning system enemySpawnTimer++; var totalEnemies = enemies.length + ogres.length + knights.length + miniBosses.length; if (enemySpawnTimer >= currentSpawnRate && miniBosses.length === 0 && totalEnemies < 15) { enemySpawnTimer = 0; spawnEnemy('skeleton', selectedDifficulty, enemyHealthMultiplier, enemySpeedMultiplier); if (Math.random() < 0.3) LK.getSound('enemyGrowl').play(); } // Ogre spawning var ogreInterval = selectedDifficulty === 'FACIL' ? 240 : selectedDifficulty === 'DIFICIL' ? 120 : 180; if (LK.ticks >= 900 && LK.ticks % ogreInterval === 0 && miniBosses.length === 0) { // 30% chance to spawn berserker ogre (red variant) instead of regular ogre if (Math.random() < 0.30) { spawnEnemy('berserkerOgre', selectedDifficulty, enemyHealthMultiplier, enemySpeedMultiplier); } else { spawnEnemy('ogre', selectedDifficulty, enemyHealthMultiplier, enemySpeedMultiplier); } } // Knight spawning var knightInterval = selectedDifficulty === 'FACIL' ? 420 : selectedDifficulty === 'DIFICIL' ? 240 : 300; var knightThreshold = selectedDifficulty === 'FACIL' ? 40 : selectedDifficulty === 'DIFICIL' ? 20 : 30; if (enemyKillCounter >= knightThreshold && LK.ticks % knightInterval === 0 && miniBosses.length === 0) { spawnEnemy('knight', selectedDifficulty, enemyHealthMultiplier, enemySpeedMultiplier); } // Mini boss spawning if (enemyKillCounter >= 80 && enemyKillCounter <= 85 && miniBosses.length === 0 && Math.random() < 0.02) { spawnEnemy('miniboss', selectedDifficulty, enemyHealthMultiplier, enemySpeedMultiplier); LK.effects.flashScreen(0x8B0000, 1000); } // Check enemy-knight collisions and cleanup off-screen enemies for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; // Remove enemies that went off-screen at bottom if (enemy.y > 2732 + 100) { enemies.splice(i, 1); enemy.destroy(); continue; } // Initialize lastIntersecting if not set if (enemy.lastIntersecting === undefined) { enemy.lastIntersecting = false; } // Check for collision transition (only if enemy is not dying) var currentIntersecting = wizard.intersects(enemy); if (!enemy.lastIntersecting && currentIntersecting && !enemy.isDying) { // Damage wizard when enemy touches for the first time wizard.takeDamage(20); // Remove enemy after dealing damage enemies.splice(i, 1); enemy.destroy(); continue; } // Update last intersecting state enemy.lastIntersecting = currentIntersecting; } // Check ogre-wizard collisions and cleanup off-screen ogres for (var i = ogres.length - 1; i >= 0; i--) { var ogre = ogres[i]; // Remove ogres that went off-screen at bottom if (ogre.y > 2732 + 100) { ogres.splice(i, 1); ogre.destroy(); continue; } // Initialize lastIntersecting if not set if (ogre.lastIntersecting === undefined) { ogre.lastIntersecting = false; } // Check for collision transition (only if ogre is not dying) var currentOgreIntersecting = wizard.intersects(ogre); if (!ogre.lastIntersecting && currentOgreIntersecting && !ogre.isDying) { // Damage wizard when ogre touches for the first time wizard.takeDamage(30); // Ogres deal 30 damage // Remove ogre after dealing damage ogres.splice(i, 1); ogre.destroy(); continue; } // Update last intersecting state ogre.lastIntersecting = currentOgreIntersecting; } // Check knight-wizard collisions and cleanup off-screen knights for (var i = knights.length - 1; i >= 0; i--) { var knight = knights[i]; // Remove knights that went off-screen at bottom if (knight.y > 2732 + 100) { knights.splice(i, 1); knight.destroy(); continue; } // Initialize lastIntersecting if not set if (knight.lastIntersecting === undefined) { knight.lastIntersecting = false; } // Check for collision transition (only if knight is not dying) var currentKnightIntersecting = wizard.intersects(knight); if (!knight.lastIntersecting && currentKnightIntersecting && !knight.isDying) { // Damage wizard when knight touches for the first time wizard.takeDamage(40); // Knights deal 40 damage // Remove knight after dealing damage knights.splice(i, 1); knight.destroy(); continue; } // Update last intersecting state knight.lastIntersecting = currentKnightIntersecting; } // Check mini boss-wizard collisions and cleanup off-screen mini bosses for (var i = miniBosses.length - 1; i >= 0; i--) { var miniBoss = miniBosses[i]; // Remove mini bosses that went off-screen at bottom if (miniBoss.y > 2732 + 100) { miniBosses.splice(i, 1); miniBoss.destroy(); continue; } // Initialize lastIntersecting if not set if (miniBoss.lastIntersecting === undefined) { miniBoss.lastIntersecting = false; } // Check for collision transition (only if mini boss is not dying) var currentMiniBossIntersecting = wizard.intersects(miniBoss); if (!miniBoss.lastIntersecting && currentMiniBossIntersecting && !miniBoss.isDying) { // Damage wizard when mini boss touches for the first time wizard.takeDamage(75); // Mini boss deals massive damage // Don't remove mini boss after dealing damage - it stays alive } // Update last intersecting state miniBoss.lastIntersecting = currentMiniBossIntersecting; } // Check thorns spike collisions with all enemies continuously var allSpikes = []; for (var childIdx = 0; childIdx < game.children.length; childIdx++) { var child = game.children[childIdx]; // Check if this child is a spike (has hitEnemies array and brown tint) if (child.hitEnemies && child.tint === 0x8B4513) { allSpikes.push(child); } } for (var spikeIdx = 0; spikeIdx < allSpikes.length; spikeIdx++) { var spike = allSpikes[spikeIdx]; var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses); for (var enemyIdx = 0; enemyIdx < allEnemies.length; enemyIdx++) { var enemy = allEnemies[enemyIdx]; // Only hit enemies that haven't been hit by this spike yet and are not dying if (spike.intersects(enemy) && spike.hitEnemies.indexOf(enemy) === -1 && !enemy.isDying) { var thornDamage = 100; // Always deal 100 damage enemy.takeDamage(thornDamage); LK.effects.flashObject(enemy, 0x8B4513, 300); // Mark this enemy as hit by this spike spike.hitEnemies.push(enemy); } } } // Make tap text pulse var pulse = 1 + Math.sin(LK.ticks * 0.1) * 0.2; tapText.scale.set(pulse, pulse); }; // Remove tap text after 5 seconds tween({}, {}, { duration: 5000, onFinish: function onFinish() { if (tapText && tapText.parent) { tapText.destroy(); } } });
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Game arrays to track objects
var BaseEnemy = Container.expand(function (config) {
var self = Container.call(this);
// Default config
config = config || {};
self.enemyType = config.type || 'skeleton';
self.assetBase = config.assetBase || 'esqueleto';
self.scale = config.scale || 2.0;
self.baseHealth = config.health || 1;
self.baseSpeed = config.speed || 3;
self.animSpeed = config.animSpeed || 15;
self.damage = config.damage || 20;
self.experience = config.experience || 25;
self.score = config.score || 10;
self.vibration = config.vibration || 50;
self.impactColor = config.impactColor || 0xFFAA00;
self.damageColor = config.damageColor || 0xFF4444;
// Animation system
self.currentFrame = 1;
self.animationTimer = 0;
self.animationSpeed = self.animSpeed;
self.animationState = 'walking';
// Create frames
self.frames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset(self.assetBase + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: self.scale,
scaleY: self.scale
});
frameGraphics.visible = i === 1;
self.frames.push(frameGraphics);
}
// Hitbox
var hitbox = self.attachAsset(self.assetBase + '1', {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 4,
scaleY: 4
});
hitbox.alpha = 0;
self.health = self.baseHealth;
self.maxHealth = self.baseHealth;
self.speed = self.baseSpeed;
self.lastX = 0;
self.frozen = false;
self.frozenTimer = 0;
self.update = function () {
if (tutorial && tutorial.isActive) return;
if (self.isDying) return;
var speedMultiplier = upgradeMenu && upgradeMenu.visible ? 0.3 : 1.0;
// Animation
self.animationTimer++;
var frameSpeed = self.animationSpeed;
if (self.animationState === 'attacking') frameSpeed = 8;else if (self.animationState === 'dying') frameSpeed = 20;else if (self.animationState === 'idle') frameSpeed = 25;
if (self.animationTimer >= frameSpeed) {
self.animationTimer = 0;
self.frames[self.currentFrame - 1].visible = false;
if (self.animationState === 'walking') {
self.currentFrame++;
if (self.currentFrame > 4) self.currentFrame = 1;
} else if (self.animationState === 'attacking') {
self.currentFrame++;
if (self.currentFrame > 4) self.currentFrame = 2;
} else if (self.animationState === 'dying') {
if (self.currentFrame < 4) self.currentFrame++;
} else if (self.animationState === 'idle') {
self.currentFrame = self.currentFrame === 1 ? 2 : 1;
}
self.frames[self.currentFrame - 1].visible = true;
}
// Speed increase
if (!self.speedTweenStarted) {
self.speedTweenStarted = true;
tween(self, {
speed: self.speed * 1.5
}, {
duration: 10000,
easing: tween.easeOut
});
}
// Frozen state
if (self.frozen) {
self.frozenTimer--;
if (self.frozenTimer <= 0) self.frozen = false;
return;
}
// Movement
if (wizard) {
var dx = wizard.x - self.x,
dy = wizard.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
self.x += dx / distance * self.speed * speedMultiplier;
self.y += dy / distance * self.speed * speedMultiplier;
}
// Facing
var flipScale = dx < 0 ? -self.scale : self.scale;
for (var frameIdx = 0; frameIdx < self.frames.length; frameIdx++) {
self.frames[frameIdx].scaleX = flipScale;
}
} else self.y += self.speed * speedMultiplier;
};
self.takeDamage = function (damage) {
self.health -= damage;
self.animationState = 'attacking';
LK.effects.flashObject(self, 0xFF0000, 50);
// Unified damage text
var damageText = new Text2('-' + damage, {
size: 120,
fill: self.damageColor,
font: "monospace"
});
damageText.anchor.set(0.5, 0.5);
damageText.x = self.x + (Math.random() - 0.5) * 60;
damageText.y = self.y - 40;
game.addChild(damageText);
tween(damageText, {
y: damageText.y - 120,
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
damageText.destroy();
}
});
// Unified impact effect
createImpactEffect(self.x, self.y, self.impactColor, 1.5);
// Return to walking
tween({}, {}, {
duration: 300,
onFinish: function onFinish() {
if (self.animationState === 'attacking') self.animationState = 'walking';
}
});
if (self.health <= 0) self.die();
};
self.down = function (x, y, obj) {
if (typeof LK.vibrate === 'function') LK.vibrate(self.vibration);
selectedEnemy = self;
LK.effects.flashObject(self, 0xFFFF00, 500);
if (wizard && projectiles.length < 10) {
var projectile = game.addChild(new Projectile());
projectile.x = wizard.x;
projectile.y = wizard.y;
var dx = self.x - wizard.x,
dy = self.y - wizard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
projectile.direction.x = dx / distance;
projectile.direction.y = dy / distance;
}
projectile.targetEnemy = self;
projectiles.push(projectile);
LK.getSound('spellCast').play();
}
};
self.die = function () {
self.animationState = 'dying';
self.currentFrame = 3;
LK.getSound('painSound').play();
self.isDying = true;
// Death animation
tween(self, {
alpha: 0,
scaleX: 0.3,
scaleY: 0.3,
rotation: Math.PI * 0.5
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
// Drop coin
dropCoin(self.x, self.y);
// Update counters
enemyKillCounter++;
killCountText.setText('Puntuacion: ' + enemyKillCounter);
// Path combo
updatePathCombo(self.pathIndex);
// Stats
if (!storage.totalKills) storage.totalKills = 0;
storage.totalKills++;
wizard.gainExperience(self.experience);
// Life drain
if (wizard.lifeDrainLevel > 0 && Math.random() < 0.20) {
wizard.health = Math.min(wizard.health + 20, wizard.maxHealth);
updateHealthBar();
LK.effects.flashObject(wizard, 0x00FF00, 300);
}
// Clear selected
if (selectedEnemy === self) selectedEnemy = null;
// Remove from array
removeFromArray(self);
self.destroy();
LK.setScore(LK.getScore() + self.score);
}
});
};
return self;
});
// Factory functions for specific enemy types
var Coin = Container.expand(function () {
var self = Container.call(this);
var coinGraphics = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5
});
self.bobOffset = Math.random() * Math.PI * 2;
self.initialY = 0;
self.update = function () {
if (self.initialY === 0) {
self.initialY = self.y;
}
// Only do bobbing animation and collection if not animating to coin counter
if (!self.isAnimating) {
// Bobbing animation
self.y = self.initialY + Math.sin(LK.ticks * 0.1 + self.bobOffset) * 10;
// Check collection by knight
if (knight && self.intersects(knight)) {
self.collect();
}
}
};
self.collect = function () {
LK.getSound('coinCollect').play();
LK.setScore(LK.getScore() + 5);
coinCounter++;
coinText.setText('Coins: ' + coinCounter);
// Update medal statistics
if (!storage.totalCoins) storage.totalCoins = 0;
storage.totalCoins++;
// 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 EnergyBeam = Container.expand(function () {
var self = Container.call(this);
// Create beam visual using projectile glow
var beamGraphics = self.attachAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.0,
scaleY: 1.0
});
beamGraphics.tint = 0x00ffff; // Cyan color
beamGraphics.alpha = 0.9;
self.speed = 60;
self.direction = {
x: 0,
y: 0
};
self.targetEnemy = null;
self.lastIntersecting = false;
self.update = function () {
// Move toward target
self.x += self.direction.x * self.speed;
self.y += self.direction.y * self.speed;
// Rotate beam to point toward movement direction
var angle = Math.atan2(self.direction.y, self.direction.x);
beamGraphics.rotation = angle;
// Remove if off screen
if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) {
self.destroy();
return;
}
// Check collision with target enemy (but never with wizard)
if (self.targetEnemy && self.targetEnemy.parent && self.targetEnemy !== wizard) {
var currentIntersecting = self.intersects(self.targetEnemy);
if (!self.lastIntersecting && currentIntersecting) {
// Hit the target enemy
// Calculate damage based on spell power upgrade
var damage = 100;
if (wizard && wizard.spellPowerLevel && wizard.spellPowerLevel > 0) {
// Check if target is an ogre and we're before enemy 35
if (self.targetEnemy && self.targetEnemy.ogreFrames && enemyKillCounter < 35) {
// Always kill ogres in one hit until enemy 35
damage = self.targetEnemy.health + 1; // Ensure one-hit kill
} else {
damage = 200; // Double damage when spell power is active
}
}
self.targetEnemy.takeDamage(damage);
self.destroy();
return;
}
self.lastIntersecting = currentIntersecting;
} else {
// Target destroyed, remove beam
self.destroy();
}
};
return self;
});
var EnergyOrb = Container.expand(function () {
var self = Container.call(this);
// Create energy sphere visual
var sphereGraphics = self.attachAsset('energySphere', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
// Add glowing effect
sphereGraphics.alpha = 0.8;
self.attackTimer = 0;
self.attackInterval = 180; // 3 seconds at 60fps
self.orbitalAngle = 0;
self.orbitalRadius = 120;
self.update = function () {
// Pause energy orb when tutorial is active
if (tutorial && tutorial.isActive) {
return;
}
// Pause energy orb when upgrade menu is visible
if (upgradeMenu && upgradeMenu.visible) {
return;
}
// Keep sphere at wizard's position (stationary relative to wizard)
if (wizard) {
self.x = wizard.x + 140; // Position further to the right side of wizard
self.y = wizard.y - 20; // Position slightly lower relative to wizard
}
// Pulsing glow effect
var pulse = 1 + Math.sin(LK.ticks * 0.2) * 0.3;
sphereGraphics.scaleX = 1.5 * pulse;
sphereGraphics.scaleY = 1.5 * pulse;
// Attack timer - keep original interval regardless of upgrades
self.attackTimer++;
if (self.attackTimer >= 180) {
// Fixed at 3 seconds
self.attackTimer = 0;
self.attackClosestEnemy();
}
};
self.attackClosestEnemy = function () {
var closestEnemy = null;
var closestDistance = Infinity;
// Check all enemy types for closest one
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// Attack closest enemy if found
if (closestEnemy) {
// Create energy beam projectile
var energyBeam = game.addChild(new EnergyBeam());
energyBeam.x = self.x;
energyBeam.y = self.y;
energyBeam.targetEnemy = closestEnemy;
// Calculate direction to target
var dx = closestEnemy.x - self.x;
var dy = closestEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
energyBeam.direction.x = dx / distance;
energyBeam.direction.y = dy / distance;
}
// Flash effect on sphere when attacking
tween(sphereGraphics, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 1.0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(sphereGraphics, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0.8
}, {
duration: 200,
easing: tween.easeIn
});
}
});
LK.getSound('spellCast').play();
}
};
return self;
});
var FireBall = Container.expand(function () {
var self = Container.call(this);
// Create fire ball visual using projectile glow with fire colors
var fireBallGraphics = self.attachAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
fireBallGraphics.tint = 0xFF4500; // Orange-red fire color
fireBallGraphics.alpha = 0.9;
self.speed = 40;
self.direction = {
x: 0,
y: 0
};
self.lastIntersecting = {};
self.update = function () {
// Move fire ball
self.x += self.direction.x * self.speed;
self.y += self.direction.y * self.speed;
// Add fire flickering effect
var flicker = 1 + Math.sin(LK.ticks * 0.4) * 0.3;
fireBallGraphics.scaleX = 1.5 * flicker;
fireBallGraphics.scaleY = 1.5 * flicker;
// Rotate fire ball
fireBallGraphics.rotation += 0.2;
// Remove if off screen
if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) {
self.destroy();
return;
}
// Check collision with all enemy types (but never with wizard)
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
// Skip collision check with wizard
if (enemy === wizard) {
continue;
}
if (!self.lastIntersecting[i]) {
self.lastIntersecting[i] = false;
}
var currentIntersecting = self.intersects(enemy);
if (!self.lastIntersecting[i] && currentIntersecting) {
// Hit enemy - deal fire damage
var damage = 150; // High damage for fire ball
enemy.takeDamage(damage);
// Create fire explosion effect
LK.effects.flashObject(enemy, 0xFF4500, 400);
// Create additional fire visual effect
var fireEffect = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x,
y: enemy.y,
scaleX: 3,
scaleY: 3
}));
fireEffect.tint = 0xFF6600;
fireEffect.alpha = 0.8;
// Animate fire effect
tween(fireEffect, {
scaleX: 6,
scaleY: 6,
alpha: 0
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
fireEffect.destroy();
}
});
// Reduced fire trail particles for better performance
for (var fireIdx = 0; fireIdx < 6; fireIdx++) {
var fireParticle = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x,
y: enemy.y,
scaleX: 0.6,
scaleY: 0.6
}));
fireParticle.tint = 0xFF4500;
fireParticle.alpha = 0.9;
// Random fire particle direction
var fireAngle = fireIdx / 6 * Math.PI * 2;
var fireSpeed = 50;
var fireTargetX = enemy.x + Math.cos(fireAngle) * fireSpeed;
var fireTargetY = enemy.y + Math.sin(fireAngle) * fireSpeed;
// Animate fire particle
tween(fireParticle, {
x: fireTargetX,
y: fireTargetY,
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
fireParticle.destroy();
}
});
}
self.destroy();
return;
}
self.lastIntersecting[i] = currentIntersecting;
}
};
return self;
});
var GameMenu = Container.expand(function () {
var self = Container.call(this);
// Menu background image instead of cave background
var menuBg = self.attachAsset('menuBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 4.0,
scaleY: 4.0
});
menuBg.alpha = 1.0;
// Title text
var titleText = new Text2('WIZARD DEFENDER', {
size: 150,
fill: 0xFFD700,
font: "monospace"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 800;
self.addChild(titleText);
// Instructions text
var instructionsText = new Text2('TAP PATHS TO CAST SPELLS\nDEFEND YOUR CASTLE!', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
instructionsText.anchor.set(0.5, 0.5);
instructionsText.x = 2048 / 2;
instructionsText.y = 1200;
self.addChild(instructionsText);
// Start button
var startButton = self.attachAsset('wizard1', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1500,
scaleX: 2,
scaleY: 2
});
var startButtonText = new Text2('START GAME', {
size: 100,
fill: 0x000000,
font: "monospace"
});
startButtonText.anchor.set(0.5, 0.5);
startButtonText.x = 2048 / 2;
startButtonText.y = 1600;
self.addChild(startButtonText);
// Configuration button
var configButton = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1800,
scaleX: 4,
scaleY: 2
});
configButton.tint = 0x4169E1;
var configButtonText = new Text2('CONFIGURACION', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
configButtonText.anchor.set(0.5, 0.5);
configButtonText.x = 2048 / 2;
configButtonText.y = 1800;
self.addChild(configButtonText);
// Shop button
var shopButton = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2000,
scaleX: 4,
scaleY: 2
});
shopButton.tint = 0xFF6B35;
var shopButtonText = new Text2('TIENDA', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
shopButtonText.anchor.set(0.5, 0.5);
shopButtonText.x = 2048 / 2;
shopButtonText.y = 2000;
self.addChild(shopButtonText);
// Medals button
var medalsButton = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2200,
scaleX: 4,
scaleY: 2
});
medalsButton.tint = 0x9B59B6;
var medalsButtonText = new Text2('MEDALLAS', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
medalsButtonText.anchor.set(0.5, 0.5);
medalsButtonText.x = 2048 / 2;
medalsButtonText.y = 2200;
self.addChild(medalsButtonText);
// Tutorial button (only show if tutorial was completed before)
if (storage.tutorialCompleted) {
var tutorialButton = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2400,
scaleX: 4,
scaleY: 2
});
tutorialButton.tint = 0x2E8B57;
var tutorialButtonText = new Text2('TUTORIAL', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
tutorialButtonText.anchor.set(0.5, 0.5);
tutorialButtonText.x = 2048 / 2;
tutorialButtonText.y = 2400;
self.addChild(tutorialButtonText);
}
// Button interaction
self.down = function (x, y, obj) {
// Handle configuration menu interactions
if (self.configMode) {
// Music volume adjustment
if (y >= 1150 && y <= 1250) {
var currentMusicVolume = storage.musicVolume || 0.7;
var newMusicVolume = Math.min(1.0, currentMusicVolume + 0.1);
if (newMusicVolume > 1.0) newMusicVolume = 0.0;
storage.musicVolume = newMusicVolume;
self.musicVolumeText.setText('VOLUMEN MUSICA: ' + Math.round(newMusicVolume * 100) + '%');
return;
}
// Sound volume adjustment
if (y >= 1350 && y <= 1450) {
var currentSoundVolume = storage.soundVolume || 1.0;
var newSoundVolume = Math.min(1.0, currentSoundVolume + 0.1);
if (newSoundVolume > 1.0) newSoundVolume = 0.0;
storage.soundVolume = newSoundVolume;
self.soundVolumeText.setText('VOLUMEN SONIDO: ' + Math.round(newSoundVolume * 100) + '%');
return;
}
// Difficulty adjustment
if (y >= 1550 && y <= 1650) {
var currentDifficulty = storage.difficulty || 'NORMAL';
var difficulties = ['FACIL', 'NORMAL', 'DIFICIL'];
var currentIndex = difficulties.indexOf(currentDifficulty);
var newIndex = (currentIndex + 1) % difficulties.length;
var newDifficulty = difficulties[newIndex];
storage.difficulty = newDifficulty;
self.difficultyText.setText('DIFICULTAD: ' + newDifficulty);
return;
}
// Back button
if (y >= 1950 && y <= 2050) {
self.hideConfigMenu();
return;
}
// Block all other interactions when config menu is active
return;
}
// Handle shop menu interactions
if (self.shopMode) {
// Shop item purchase buttons
for (var i = 0; i < 3; i++) {
var itemY = 1100 + i * 200;
if (y >= itemY - 50 && y <= itemY + 50 && x >= 2048 / 2 + 200 && x <= 2048 / 2 + 400) {
// Purchase item logic
var shopItems = [{
name: 'POCION SALUD',
cost: 10
}, {
name: 'ESCUDO MAGICO',
cost: 15
}, {
name: 'ESPADA MALDITA',
cost: 20
}];
var item = shopItems[i];
if (coinCounter >= item.cost) {
coinCounter -= item.cost;
// Apply item effect based on type
if (i === 0) {
// Health potion
if (wizard) {
wizard.health = Math.min(wizard.health + 50, wizard.maxHealth);
updateHealthBar();
}
} else if (i === 1) {
// Magic shield
if (wizard) {
wizard.shieldActive = true;
wizard.maxShieldHits = 3;
wizard.currentShieldHits = 0;
}
} else if (i === 2) {
// Cursed sword - temporary damage boost
if (wizard) {
wizard.tempDamageBoost = true;
wizard.tempDamageTimer = 1800; // 30 seconds at 60fps
}
}
LK.effects.flashScreen(0x00FF00, 300);
} else {
LK.effects.flashScreen(0xFF0000, 300);
}
return;
}
}
// Shop back button
if (y >= 1950 && y <= 2050) {
self.hideShop();
return;
}
// Block all other interactions when shop menu is active
return;
}
// Check if configuration button was clicked
if (y >= 1700 && y <= 1900 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) {
// Show configuration menu
self.showConfigMenu();
} else if (y >= 1900 && y <= 2100 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) {
// Show shop menu
self.showShop();
} else if (y >= 2100 && y <= 2300 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) {
// Show medals menu
if (prestigeMedals) {
prestigeMedals.showMedals();
}
} else if (storage.tutorialCompleted && y >= 2300 && y <= 2500 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) {
// Show tutorial again
if (tutorial) {
self.visible = false;
tutorial.startTutorial();
}
} else if (y >= 1450 && y <= 1650) {
// Start the game by hiding menu
self.startGame();
}
};
self.showConfigMenu = function () {
// Create configuration overlay
if (!self.configOverlay) {
self.configOverlay = self.addChild(LK.getAsset('startMenuBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 1.0,
scaleY: 1.0
}));
self.configOverlay.alpha = 1.0;
self.configOverlay.tint = 0x000000;
// Config title
var configTitle = new Text2('CONFIGURACION', {
size: 120,
fill: 0xFFD700,
font: "monospace"
});
configTitle.anchor.set(0.5, 0.5);
configTitle.x = 2048 / 2;
configTitle.y = 800;
self.addChild(configTitle);
self.configTitle = configTitle;
// Music volume setting
var musicVolumeText = new Text2('VOLUMEN MUSICA: ' + Math.round((storage.musicVolume || 0.7) * 100) + '%', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
musicVolumeText.anchor.set(0.5, 0.5);
musicVolumeText.x = 2048 / 2;
musicVolumeText.y = 1200;
self.addChild(musicVolumeText);
self.musicVolumeText = musicVolumeText;
// Sound volume setting
var soundVolumeText = new Text2('VOLUMEN SONIDO: ' + Math.round((storage.soundVolume || 1.0) * 100) + '%', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
soundVolumeText.anchor.set(0.5, 0.5);
soundVolumeText.x = 2048 / 2;
soundVolumeText.y = 1400;
self.addChild(soundVolumeText);
self.soundVolumeText = soundVolumeText;
// Difficulty setting
var difficultyText = new Text2('DIFICULTAD: ' + (storage.difficulty || 'NORMAL'), {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
difficultyText.anchor.set(0.5, 0.5);
difficultyText.x = 2048 / 2;
difficultyText.y = 1600;
self.addChild(difficultyText);
self.difficultyText = difficultyText;
// Back button
var backButton = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2000,
scaleX: 2,
scaleY: 2
});
backButton.tint = 0x00FF00;
self.backButton = backButton;
var backText = new Text2('VOLVER', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
backText.anchor.set(0.5, 0.5);
backText.x = 2048 / 2;
backText.y = 2000;
self.addChild(backText);
self.backText = backText;
}
self.configOverlay.visible = true;
self.configMode = true;
};
self.hideConfigMenu = function () {
if (self.configOverlay) {
self.configOverlay.destroy();
self.configOverlay = null;
}
// Remove all configuration text elements
if (self.musicVolumeText) {
self.musicVolumeText.destroy();
self.musicVolumeText = null;
}
if (self.soundVolumeText) {
self.soundVolumeText.destroy();
self.soundVolumeText = null;
}
if (self.difficultyText) {
self.difficultyText.destroy();
self.difficultyText = null;
}
// Remove back button elements
if (self.backButton) {
self.backButton.destroy();
self.backButton = null;
}
if (self.backText) {
self.backText.destroy();
self.backText = null;
}
// Remove configuration title
if (self.configTitle) {
self.configTitle.destroy();
self.configTitle = null;
}
// Remove all configuration children that were added
for (var i = self.children.length - 1; i >= 0; i--) {
var child = self.children[i];
// Remove config-related elements (title, texts, buttons created in showConfigMenu)
if (child.setText && child.text && (child.text.includes('CONFIGURACION') || child.text.includes('VOLUMEN') || child.text.includes('DIFICULTAD') || child.text.includes('VOLVER'))) {
child.destroy();
}
}
self.configMode = false;
// Reset to show main menu elements
self.visible = true;
};
self.showShop = function () {
// Create shop overlay
if (!self.shopOverlay) {
self.shopOverlay = self.addChild(LK.getAsset('startMenuBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 1.0,
scaleY: 1.0
}));
self.shopOverlay.alpha = 1.0;
self.shopOverlay.tint = 0x000033;
// Shop title
var shopTitle = new Text2('TIENDA', {
size: 120,
fill: 0xFFD700,
font: "monospace"
});
shopTitle.anchor.set(0.5, 0.5);
shopTitle.x = 2048 / 2;
shopTitle.y = 800;
self.addChild(shopTitle);
self.shopTitle = shopTitle;
// Shop items
var shopItems = [{
name: 'POCION SALUD',
description: 'Restaura 50 HP',
cost: 10,
icon: 'energySphere'
}, {
name: 'ESCUDO MAGICO',
description: 'Bloquea 3 ataques',
cost: 15,
icon: 'shield'
}, {
name: 'ESPADA MALDITA',
description: 'Daño x2 por 30s',
cost: 20,
icon: 'spell'
}];
// Initialize shop elements arrays if not exists
if (!self.shopIcons) self.shopIcons = [];
if (!self.shopTexts) self.shopTexts = [];
if (!self.shopBuyButtons) self.shopBuyButtons = [];
if (!self.shopBuyTexts) self.shopBuyTexts = [];
// Display shop items
for (var i = 0; i < shopItems.length; i++) {
var item = shopItems[i];
var yPos = 1100 + i * 200;
// Item icon
var itemIcon = self.attachAsset(item.icon, {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2 - 300,
y: yPos,
scaleX: 2,
scaleY: 2
});
itemIcon.tint = 0xFFD700;
self.shopIcons.push(itemIcon);
// Item text
var itemText = new Text2(item.name + '\n' + item.description + '\nCosto: ' + item.cost + ' monedas', {
size: 60,
fill: 0xFFFFFF,
font: "monospace"
});
itemText.anchor.set(0, 0.5);
itemText.x = 2048 / 2 - 200;
itemText.y = yPos;
self.addChild(itemText);
self.shopTexts.push(itemText);
// Buy button
var buyButton = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2 + 300,
y: yPos,
scaleX: 2,
scaleY: 2
});
buyButton.tint = 0x00FF00;
buyButton.itemIndex = i;
self.shopBuyButtons.push(buyButton);
var buyText = new Text2('COMPRAR', {
size: 50,
fill: 0xFFFFFF,
font: "monospace"
});
buyText.anchor.set(0.5, 0.5);
buyText.x = 2048 / 2 + 300;
buyText.y = yPos;
self.addChild(buyText);
self.shopBuyTexts.push(buyText);
}
// Back button
var shopBackButton = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2000,
scaleX: 2,
scaleY: 2
});
shopBackButton.tint = 0x00FF00;
self.shopBackButton = shopBackButton;
var shopBackText = new Text2('VOLVER', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
shopBackText.anchor.set(0.5, 0.5);
shopBackText.x = 2048 / 2;
shopBackText.y = 2000;
self.addChild(shopBackText);
self.shopBackText = shopBackText;
}
self.shopOverlay.visible = true;
self.shopMode = true;
};
self.hideShop = function () {
if (self.shopOverlay) {
self.shopOverlay.destroy();
self.shopOverlay = null;
}
// Remove shop title
if (self.shopTitle) {
self.shopTitle.destroy();
self.shopTitle = null;
}
// Remove shop back button elements
if (self.shopBackButton) {
self.shopBackButton.destroy();
self.shopBackButton = null;
}
if (self.shopBackText) {
self.shopBackText.destroy();
self.shopBackText = null;
}
// Remove all shop icons
if (self.shopIcons) {
for (var i = 0; i < self.shopIcons.length; i++) {
if (self.shopIcons[i]) {
self.shopIcons[i].destroy();
}
}
self.shopIcons = [];
}
// Remove all shop texts
if (self.shopTexts) {
for (var i = 0; i < self.shopTexts.length; i++) {
if (self.shopTexts[i]) {
self.shopTexts[i].destroy();
}
}
self.shopTexts = [];
}
// Remove all shop buy buttons
if (self.shopBuyButtons) {
for (var i = 0; i < self.shopBuyButtons.length; i++) {
if (self.shopBuyButtons[i]) {
self.shopBuyButtons[i].destroy();
}
}
self.shopBuyButtons = [];
}
// Remove all shop buy texts
if (self.shopBuyTexts) {
for (var i = 0; i < self.shopBuyTexts.length; i++) {
if (self.shopBuyTexts[i]) {
self.shopBuyTexts[i].destroy();
}
}
self.shopBuyTexts = [];
}
// Remove all shop children that were added
for (var i = self.children.length - 1; i >= 0; i--) {
var child = self.children[i];
// Remove shop-related elements
if (child.setText && child.text && (child.text.includes('TIENDA') || child.text.includes('POCION') || child.text.includes('ESCUDO') || child.text.includes('ESPADA') || child.text.includes('COMPRAR'))) {
child.destroy();
}
}
self.shopMode = false;
// Reset to show main menu elements
self.visible = true;
};
self.startGame = function () {
// Check if this is a new player (no tutorial completed)
if (!storage.tutorialCompleted && tutorial) {
// Show tutorial for new players
self.visible = false;
// Small delay to ensure menu is hidden before tutorial starts
tween({}, {}, {
duration: 100,
onFinish: function onFinish() {
tutorial.startTutorial();
}
});
// Tutorial started successfully
return;
}
// Hide menu and start game normally
self.visible = false;
gameStarted = true;
// Show cave background when game starts
if (backgroundMap) {
backgroundMap.visible = true;
}
// Show all game elements
wizard.visible = true;
for (var i = 0; i < paths.length; i++) {
paths[i].visible = true;
}
// Show all stone path segments and make them visible
for (var i = 0; i < game.children.length; i++) {
var child = game.children[i];
if (child.pathIndex !== undefined && child !== paths[child.pathIndex]) {
child.visible = true;
// Check if it's a stone path segment or path number
if (child.alpha !== undefined && child.setText === undefined) {
child.alpha = 0; // Keep stone paths invisible
}
}
}
coinText.visible = true;
killCountText.visible = true;
tapText.visible = true;
healthBarBg.visible = true;
healthBar.visible = true;
healthText.visible = true;
// Start medieval music with user's volume setting
var musicVolume = storage.musicVolume || 0.7;
LK.playMusic('medievalTheme', {
volume: musicVolume,
fade: {
start: 0,
end: musicVolume,
duration: 2000
}
});
};
return self;
});
// Simplified class definitions now handled by BaseEnemy factory functions
var Orb = Container.expand(function () {
var self = Container.call(this);
// Create orb visual using energy sphere
var orbGraphics = self.attachAsset('energySphere', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.4,
scaleY: 0.4
});
orbGraphics.tint = 0xFFD700; // Golden color for orbs
orbGraphics.alpha = 0.9;
self.orbitalAngle = 0; // Starting angle for this orb
self.orbitalRadius = 880; // Distance from wizard - doubled again for even more separation
self.rotationSpeed = 0.025; // How fast orbs rotate - halved for slower movement
self.update = function () {
// Pause orb when tutorial is active
if (tutorial && tutorial.isActive) {
return;
}
// Pause orb when upgrade menu is visible
if (upgradeMenu && upgradeMenu.visible) {
return;
}
// Rotate around wizard
if (wizard) {
self.orbitalAngle += self.rotationSpeed;
self.x = wizard.x + Math.cos(self.orbitalAngle) * self.orbitalRadius;
self.y = wizard.y + Math.sin(self.orbitalAngle) * self.orbitalRadius - 240; // Position orb much higher up
}
// Add pulsing effect
var pulse = 1 + Math.sin(LK.ticks * 0.3) * 0.2;
orbGraphics.scaleX = 0.4 * pulse;
orbGraphics.scaleY = 0.4 * pulse;
// Check collision with enemies using collision state tracking (but never with wizard)
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
// Skip collision check with wizard
if (enemy === wizard) {
continue;
}
// Initialize collision tracking for this enemy if not exists
if (!self.lastIntersecting) {
self.lastIntersecting = {};
}
if (self.lastIntersecting[i] === undefined) {
self.lastIntersecting[i] = false;
}
var currentIntersecting = self.intersects(enemy);
if (!self.lastIntersecting[i] && currentIntersecting) {
// Deal damage to enemy on contact transition (first contact only)
enemy.takeDamage(50);
// Visual effect for orb hit
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Create orb impact effect
var orbImpact = game.addChild(LK.getAsset('energySphere', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x,
y: enemy.y,
scaleX: 0.3,
scaleY: 0.3
}));
orbImpact.tint = 0xFFD700;
orbImpact.alpha = 0.8;
// Animate orb impact
tween(orbImpact, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 250,
easing: tween.easeOut,
onFinish: function onFinish() {
orbImpact.destroy();
}
});
}
// Update collision state for this enemy
self.lastIntersecting[i] = currentIntersecting;
}
};
return self;
});
var PrestigeMedals = Container.expand(function () {
var self = Container.call(this);
// Background
var medalsBg = self.attachAsset('startMenuBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 1.0,
scaleY: 1.0
});
medalsBg.alpha = 1.0;
medalsBg.tint = 0x1a1a2e;
// Title
var titleText = new Text2('MEDALLAS DE PRESTIGIO', {
size: 120,
fill: 0xFFD700,
font: "monospace"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 600;
self.addChild(titleText);
// Initialize medals data if not exists
if (!storage.medals) {
storage.medals = {};
}
// Medal definitions
var medalDefinitions = [{
id: 'firstKill',
name: 'PRIMER ENEMIGO',
description: 'Mata tu primer esqueleto',
requirement: 'Eliminar 1 enemigo',
icon: 'esqueleto1',
unlocked: false
}, {
id: 'tenKills',
name: 'CAZADOR',
description: 'Mata 10 enemigos',
requirement: 'Eliminar 10 enemigos',
icon: 'spell',
unlocked: false
}, {
id: 'survivorMedal',
name: 'SUPERVIVIENTE',
description: 'Mata 25 enemigos',
requirement: 'Eliminar 25 enemigos',
icon: 'knight1',
unlocked: false
}, {
id: 'masterWizard',
name: 'MAESTRO HECHICERO',
description: 'Mata 50 enemigos',
requirement: 'Eliminar 50 enemigos',
icon: 'wizard1',
unlocked: false
}, {
id: 'coinCollector',
name: 'COLECCIONISTA',
description: 'Consigue 100 monedas',
requirement: 'Conseguir 100 monedas',
icon: 'coin',
unlocked: false
}, {
id: 'bossSlayer',
name: 'MATA JEFES',
description: 'Derrota un mini jefe',
requirement: 'Eliminar mini jefe',
icon: 'ogre1',
unlocked: false
}, {
id: 'legendary',
name: 'LEGENDARIO',
description: 'Mata 100 enemigos',
requirement: 'Eliminar 100 enemigos',
icon: 'energySphere',
unlocked: false
}, {
id: 'deathCheater',
name: 'BURLANDO LA MUERTE',
description: 'Revive cuando mueres',
requirement: 'Activar revival',
icon: 'shield',
unlocked: false
}, {
id: 'upgradeExpert',
name: 'EXPERTO EN MEJORAS',
description: 'Compra 5 mejoras',
requirement: 'Comprar 5 mejoras',
icon: 'pathSelector',
unlocked: false
}];
// Store references for cleanup
self.medalIcons = [];
self.medalTexts = [];
self.medalBorders = [];
// Check and update medal status
self.updateMedals = function () {
// Get current game stats
var totalKills = storage.totalKills || 0;
var totalCoins = storage.totalCoins || 0;
var totalBossKills = storage.totalBossKills || 0;
var totalRevives = storage.totalRevives || 0;
var totalUpgrades = storage.totalUpgrades || 0;
// Check each medal
for (var i = 0; i < medalDefinitions.length; i++) {
var medal = medalDefinitions[i];
var isUnlocked = false;
switch (medal.id) {
case 'firstKill':
isUnlocked = totalKills >= 1;
break;
case 'tenKills':
isUnlocked = totalKills >= 10;
break;
case 'survivorMedal':
isUnlocked = totalKills >= 25;
break;
case 'masterWizard':
isUnlocked = totalKills >= 50;
break;
case 'coinCollector':
isUnlocked = totalCoins >= 100;
break;
case 'bossSlayer':
isUnlocked = totalBossKills >= 1;
break;
case 'legendary':
isUnlocked = totalKills >= 100;
break;
case 'deathCheater':
isUnlocked = totalRevives >= 1;
break;
case 'upgradeExpert':
isUnlocked = totalUpgrades >= 5;
break;
}
// Update storage
storage.medals[medal.id] = isUnlocked;
medal.unlocked = isUnlocked;
}
};
// Display medals
self.displayMedals = function () {
var medalRows = 3;
var medalCols = 3;
var startX = 2048 / 2 - 400;
var startY = 1000;
var medalSpacing = 300;
for (var i = 0; i < medalDefinitions.length; i++) {
var medal = medalDefinitions[i];
var row = Math.floor(i / medalCols);
var col = i % medalCols;
var medalX = startX + col * medalSpacing;
var medalY = startY + row * medalSpacing;
// Medal background/border
var medalBorder = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: medalX,
y: medalY,
scaleX: 3,
scaleY: 3
});
medalBorder.tint = medal.unlocked ? 0xFFD700 : 0x555555;
medalBorder.alpha = medal.unlocked ? 1.0 : 0.5;
self.medalBorders.push(medalBorder);
// Medal icon
var medalIcon = self.attachAsset(medal.icon, {
anchorX: 0.5,
anchorY: 0.5,
x: medalX,
y: medalY,
scaleX: 1.5,
scaleY: 1.5
});
medalIcon.tint = medal.unlocked ? 0xFFFFFF : 0x333333;
medalIcon.alpha = medal.unlocked ? 1.0 : 0.3;
self.medalIcons.push(medalIcon);
// Medal text
var medalText = new Text2(medal.name + '\n' + medal.description, {
size: 30,
fill: medal.unlocked ? 0xFFFFFF : 0x666666,
font: "monospace"
});
medalText.anchor.set(0.5, 0);
medalText.x = medalX;
medalText.y = medalY + 80;
self.addChild(medalText);
self.medalTexts.push(medalText);
// Add shine effect for unlocked medals
if (medal.unlocked) {
var shine = self.attachAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: medalX,
y: medalY,
scaleX: 2,
scaleY: 2
});
shine.alpha = 0.3;
shine.tint = 0xFFD700;
// Animate shine
tween(shine, {
rotation: Math.PI * 2,
alpha: 0.1
}, {
duration: 2000,
easing: tween.linear,
loop: true
});
}
}
};
// Back button
var backButton = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2400,
scaleX: 2,
scaleY: 2
});
backButton.tint = 0x00FF00;
var backText = new Text2('VOLVER', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
backText.anchor.set(0.5, 0.5);
backText.x = 2048 / 2;
backText.y = 2400;
self.addChild(backText);
// Handle interactions
self.down = function (x, y, obj) {
// Check back button
if (y >= 2300 && y <= 2500 && x >= 2048 / 2 - 100 && x <= 2048 / 2 + 100) {
self.hideMedals();
}
};
self.showMedals = function () {
self.updateMedals();
self.displayMedals();
self.visible = true;
};
self.hideMedals = function () {
// Clean up medal elements
for (var i = 0; i < self.medalIcons.length; i++) {
if (self.medalIcons[i]) {
self.medalIcons[i].destroy();
}
}
for (var i = 0; i < self.medalTexts.length; i++) {
if (self.medalTexts[i]) {
self.medalTexts[i].destroy();
}
}
for (var i = 0; i < self.medalBorders.length; i++) {
if (self.medalBorders[i]) {
self.medalBorders[i].destroy();
}
}
self.medalIcons = [];
self.medalTexts = [];
self.medalBorders = [];
self.visible = false;
};
return self;
});
var Projectile = Container.expand(function () {
var self = Container.call(this);
// Create glowing light effect behind projectile
var lightGlow = self.attachAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.0,
scaleY: 2.0
});
lightGlow.alpha = 0.15;
lightGlow.tint = 0x44aaff; // Blue-white glow
// Animate the glow effect
tween(lightGlow, {
scaleX: 3.5,
scaleY: 3.5,
alpha: 0.05
}, {
duration: 800,
easing: tween.easeInOut
});
tween(lightGlow, {
rotation: Math.PI * 4
}, {
duration: 1000,
easing: tween.linear
});
var projectileGraphics = self.attachAsset('projectile', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3.0,
scaleY: 3.0
});
// Create spell trail array
self.trailSegments = [];
self.maxTrailSegments = 8;
self.trailTimer = 0;
self.speed = 50;
self.direction = {
x: 0,
y: 0
};
self.lastIntersecting = {};
self.targetEnemy = null; // The specific enemy this projectile should damage
self.update = function () {
// Pause projectile when tutorial is active
if (tutorial && tutorial.isActive) {
return;
}
// Move projectile
self.x += self.direction.x * self.speed;
self.y += self.direction.y * self.speed;
// Rotate projectile to point towards movement direction
var angle = Math.atan2(self.direction.y, self.direction.x);
projectileGraphics.rotation = angle + Math.PI / 2; // Add 90 degrees so top points forward
// Simplified visual effect - no trail system for better performance
// Remove if off screen
if (self.x < -50 || self.x > 2098 || self.y < -50 || self.y > 2782) {
self.removeFromGame();
return;
}
// Only check collision with the target enemy if one is set
if (self.targetEnemy && self.targetEnemy.parent) {
if (!self.lastIntersecting.target) {
self.lastIntersecting.target = false;
}
var currentTargetIntersecting = self.intersects(self.targetEnemy);
if (!self.lastIntersecting.target && currentTargetIntersecting) {
// Hit the target enemy
// Calculate damage based on spell power upgrade
var damage = 100;
if (wizard && wizard.spellPowerLevel && wizard.spellPowerLevel > 0) {
// Check if target is an ogre and we're before enemy 35
if (self.targetEnemy && self.targetEnemy.ogreFrames && enemyKillCounter < 35) {
// Always kill ogres in one hit until enemy 35
damage = self.targetEnemy.health + 1; // Ensure one-hit kill
} else {
damage = 200; // Double damage when spell power is active
}
}
// Apply combo double damage if active
if (nextProjectileDoubleDamage) {
damage *= 2; // Double the damage
nextProjectileDoubleDamage = false; // Reset the flag after use
// Visual feedback for combo damage
LK.effects.flashObject(self.targetEnemy, 0xFFD700, 300); // Golden flash on enemy
// Create special combo damage indicator
var comboDamageText = new Text2('COMBO x2!', {
size: 140,
fill: 0xFFD700,
font: "monospace"
});
comboDamageText.anchor.set(0.5, 0.5);
comboDamageText.x = self.targetEnemy.x;
comboDamageText.y = self.targetEnemy.y - 80;
game.addChild(comboDamageText);
// Animate combo damage text
tween(comboDamageText, {
y: comboDamageText.y - 150,
scaleX: 1.8,
scaleY: 1.8,
alpha: 0
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
comboDamageText.destroy();
}
});
}
self.targetEnemy.takeDamage(damage);
self.removeFromGame();
return;
}
self.lastIntersecting.target = currentTargetIntersecting;
}
};
self.removeFromGame = function () {
// Clean up trail segments
for (var i = 0; i < self.trailSegments.length; i++) {
var segment = self.trailSegments[i];
if (segment && segment.parent) {
segment.destroy();
}
}
self.trailSegments = [];
// Remove from projectiles array
for (var i = projectiles.length - 1; i >= 0; i--) {
if (projectiles[i] === self) {
projectiles.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
var Tutorial = Container.expand(function () {
var self = Container.call(this);
// Tutorial state variables
self.currentStep = 0;
self.isActive = false;
self.skipped = false; // Track if tutorial was skipped
self.tutorialSteps = [];
self.highlightElements = [];
self.tutorialTexts = [];
self.arrows = [];
// Tutorial overlay background
var tutorialOverlay = self.attachAsset('startMenuBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 1.0,
scaleY: 1.0
});
tutorialOverlay.alpha = 0.8;
tutorialOverlay.tint = 0x000000;
tutorialOverlay.visible = false; // Initially hidden
tutorialOverlay.zIndex = 1999; // Ensure proper layering
tutorialOverlay.interactive = true; // Always interactive to block clicks
// Define tutorial steps
self.initializeTutorialSteps = function () {
self.tutorialSteps = [{
id: 'welcome',
title: 'BIENVENIDO A WIZARD DEFENDER!',
description: 'Eres un poderoso mago que debe defender su castillo de hordas de enemigos.',
duration: 3000,
showSkip: true
}, {
id: 'enemies_approach',
title: 'ENEMIGOS SE ACERCAN!',
description: 'Los esqueletos vienen por 5 caminos diferentes hacia ti. Debes detenerlos!',
duration: 3000,
spawnDemoEnemy: true
}, {
id: 'tap_to_attack',
title: 'TOCA PARA ATACAR',
description: 'Toca cualquier parte de la pantalla para lanzar hechizos hacia los enemigos.',
highlightElement: 'screen',
waitForTap: true
}, {
id: 'health_system',
title: 'CUIDA TU SALUD',
description: 'Si los enemigos te alcanzan, perderás vida. No dejes que lleguen a ti!',
highlightElement: 'healthBar',
duration: 3000
}, {
id: 'coins_and_score',
title: 'GANA MONEDAS Y PUNTOS',
description: 'Elimina enemigos para ganar monedas y aumentar tu puntuación.',
highlightElement: 'coinCounter',
duration: 3000
}, {
id: 'upgrades_intro',
title: 'MEJORAS PODEROSAS',
description: 'Cada 12 enemigos eliminados podrás elegir mejoras para hacerte más fuerte!',
duration: 3000
}, {
id: 'survival_goal',
title: 'OBJETIVO: SOBREVIVIR',
description: 'Sobrevive el mayor tiempo posible y elimina tantos enemigos como puedas!',
duration: 3000
}, {
id: 'tutorial_complete',
title: 'TUTORIAL COMPLETADO!',
description: 'Ahora estás listo para la batalla. ¡Buena suerte, valiente mago!',
duration: 3000,
startGame: true
}];
};
// Start the tutorial
self.startTutorial = function () {
// Always show tutorial when explicitly called
self.isActive = true;
self.currentStep = 0;
// Initialize tutorial steps first
self.initializeTutorialSteps();
// Make tutorial visible and properly layered
self.visible = true;
self.zIndex = 2000;
// Configure tutorial overlay properly
tutorialOverlay.visible = true;
tutorialOverlay.alpha = 0.8;
tutorialOverlay.tint = 0x000000;
tutorialOverlay.interactive = true; // Block clicks to game below
tutorialOverlay.zIndex = 1999; // Ensure proper layering
// Hide game menu while tutorial is active
if (gameMenu) {
gameMenu.visible = false;
}
// Hide all game elements during tutorial
if (wizard) wizard.visible = false;
if (backgroundMap) backgroundMap.visible = false;
coinText.visible = false;
killCountText.visible = false;
tapText.visible = false;
healthBarBg.visible = false;
healthBar.visible = false;
healthText.visible = false;
for (var i = 0; i < paths.length; i++) {
paths[i].visible = false;
}
// Start first step
self.showStep(0);
return true; // Tutorial started
};
// Show a specific tutorial step
self.showStep = function (stepIndex) {
if (stepIndex >= self.tutorialSteps.length) {
self.completeTutorial();
return;
}
// Clear previous step elements
self.clearStepElements();
var step = self.tutorialSteps[stepIndex];
self.currentStep = stepIndex;
// Ensure tutorial is visible and on top
self.visible = true;
tutorialOverlay.visible = true;
self.zIndex = 2000;
// Create step title with proper sizing
var titleText = new Text2(step.title, {
size: 80,
fill: 0xFFD700,
font: "monospace",
wordWrap: true,
wordWrapWidth: 1600
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 900;
self.addChild(titleText);
self.tutorialTexts.push(titleText);
// Create step description with proper text wrapping
var descText = new Text2(step.description, {
size: 60,
fill: 0xFFFFFF,
font: "monospace",
wordWrap: true,
wordWrapWidth: 1800
});
descText.anchor.set(0.5, 0.5);
descText.x = 2048 / 2;
descText.y = 1300;
self.addChild(descText);
self.tutorialTexts.push(descText);
// Handle special step behaviors
if (step.spawnDemoEnemy) {
self.spawnDemoEnemy();
}
if (step.highlightElement) {
self.highlightElement(step.highlightElement);
}
// Show continue button or wait for specific action
if (step.waitForTap) {
var tapPrompt = new Text2('TOCA LA PANTALLA PARA CONTINUAR', {
size: 50,
fill: 0x00FF00,
font: "monospace",
wordWrap: true,
wordWrapWidth: 1400
});
tapPrompt.anchor.set(0.5, 0.5);
tapPrompt.x = 2048 / 2;
tapPrompt.y = 1800;
self.addChild(tapPrompt);
self.tutorialTexts.push(tapPrompt);
// Add pulsing effect to tap prompt
tween(tapPrompt, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(tapPrompt, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 800,
easing: tween.easeInOut
});
}
});
} else {
// Always show continue prompt - no auto-advance
var continuePrompt = new Text2('TOCA LA PANTALLA PARA CONTINUAR', {
size: 50,
fill: 0x00FF00,
font: "monospace",
wordWrap: true,
wordWrapWidth: 1400
});
continuePrompt.anchor.set(0.5, 0.5);
continuePrompt.x = 2048 / 2;
continuePrompt.y = 1800;
self.addChild(continuePrompt);
self.tutorialTexts.push(continuePrompt);
// Add pulsing effect to continue prompt
tween(continuePrompt, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(continuePrompt, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 800,
easing: tween.easeInOut
});
}
});
}
// Show skip button on first step
if (step.showSkip) {
var skipBtn = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 200,
scaleX: 1.5,
scaleY: 1.5
});
skipBtn.tint = 0xFF4444;
self.highlightElements.push(skipBtn);
var skipText = new Text2('OMITIR', {
size: 50,
fill: 0xFFFFFF,
font: "monospace"
});
skipText.anchor.set(0.5, 0.5);
skipText.x = 2048 - 200;
skipText.y = 200;
self.addChild(skipText);
self.tutorialTexts.push(skipText);
}
};
// Highlight specific game elements
self.highlightElement = function (elementType) {
switch (elementType) {
case 'healthBar':
if (healthBarBg && healthBar) {
// Show health UI temporarily
healthBarBg.visible = true;
healthBar.visible = true;
healthText.visible = true;
// Create highlight glow around health bar
var healthGlow = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: 270,
y: 110,
scaleX: 3,
scaleY: 1.5
}));
healthGlow.tint = 0x00FF00;
healthGlow.alpha = 0.6;
self.highlightElements.push(healthGlow);
// Animate glow
tween(healthGlow, {
alpha: 0.3
}, {
duration: 1000,
easing: tween.easeInOut
});
}
break;
case 'coinCounter':
if (coinText) {
// Show coin UI temporarily
coinText.visible = true;
coinText.setText('Coins: 15'); // Show example coins
// Create highlight glow around coin counter
var coinGlow = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: 270,
y: 150,
scaleX: 3,
scaleY: 1.5
}));
coinGlow.tint = 0xFFD700;
coinGlow.alpha = 0.6;
self.highlightElements.push(coinGlow);
// Animate glow
tween(coinGlow, {
alpha: 0.3
}, {
duration: 1000,
easing: tween.easeInOut
});
}
break;
case 'screen':
// Create large highlight overlay
var screenGlow = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 20,
scaleY: 25
}));
screenGlow.tint = 0x00FFFF;
screenGlow.alpha = 0.2;
self.highlightElements.push(screenGlow);
// Animate screen glow
tween(screenGlow, {
alpha: 0.1
}, {
duration: 1500,
easing: tween.easeInOut
});
// Create arrow pointing to center
self.createArrow(2048 / 2, 2732 / 2 - 400, 2048 / 2, 2732 / 2);
break;
}
};
// Create pointing arrow
self.createArrow = function (fromX, fromY, toX, toY) {
var arrow = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: fromX,
y: fromY,
scaleX: 3,
scaleY: 3
}));
arrow.tint = 0xFFD700;
arrow.alpha = 0.8;
// Calculate arrow direction
var dx = toX - fromX;
var dy = toY - fromY;
var angle = Math.atan2(dy, dx);
arrow.rotation = angle;
// Animate arrow bouncing
tween(arrow, {
x: toX,
y: toY
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(arrow, {
x: fromX,
y: fromY
}, {
duration: 1000,
easing: tween.easeInOut
});
}
});
self.arrows.push(arrow);
};
// Spawn a demo enemy for tutorial
self.spawnDemoEnemy = function () {
// Create a slow-moving demo enemy
var demoEnemy = game.addChild(new Enemy());
demoEnemy.x = 2048 / 2;
demoEnemy.y = -100;
demoEnemy.speed = 1; // Very slow for demo
demoEnemy.health = 1;
demoEnemy.maxHealth = 1;
demoEnemy.pathIndex = 0; // Center path
// Make demo enemy more visible
for (var frameIdx = 0; frameIdx < demoEnemy.skeletonFrames.length; frameIdx++) {
demoEnemy.skeletonFrames[frameIdx].tint = 0xFF6600; // Orange tint
}
enemies.push(demoEnemy);
// Create highlight around demo enemy
var enemyGlow = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: demoEnemy.x,
y: demoEnemy.y,
scaleX: 4,
scaleY: 4
}));
enemyGlow.tint = 0xFF0000;
enemyGlow.alpha = 0.5;
// Make glow follow enemy
var _glowFollower = function glowFollower() {
if (demoEnemy && demoEnemy.parent && enemyGlow && enemyGlow.parent) {
enemyGlow.x = demoEnemy.x;
enemyGlow.y = demoEnemy.y;
// Continue following
tween({}, {}, {
duration: 60,
onFinish: _glowFollower
});
} else if (enemyGlow && enemyGlow.parent) {
enemyGlow.destroy();
}
};
_glowFollower();
self.highlightElements.push(enemyGlow);
};
// Clear all tutorial step elements
self.clearStepElements = function () {
// Clear tutorial texts
for (var i = 0; i < self.tutorialTexts.length; i++) {
if (self.tutorialTexts[i] && self.tutorialTexts[i].parent) {
self.tutorialTexts[i].destroy();
}
}
self.tutorialTexts = [];
// Clear highlight elements
for (var i = 0; i < self.highlightElements.length; i++) {
if (self.highlightElements[i] && self.highlightElements[i].parent) {
self.highlightElements[i].destroy();
}
}
self.highlightElements = [];
// Clear arrows
for (var i = 0; i < self.arrows.length; i++) {
if (self.arrows[i] && self.arrows[i].parent) {
self.arrows[i].destroy();
}
}
self.arrows = [];
};
// Advance to next tutorial step
self.nextStep = function () {
self.currentStep++;
if (self.currentStep < self.tutorialSteps.length) {
self.showStep(self.currentStep);
} else {
// Tutorial completed naturally (not skipped)
self.skipped = false;
self.completeTutorial();
}
};
// Complete the tutorial
self.completeTutorial = function () {
// Mark tutorial as completed
storage.tutorialCompleted = true;
// Clear all tutorial elements
self.clearStepElements();
// Hide tutorial completely
tutorialOverlay.visible = false;
tutorialOverlay.interactive = false; // Remove interactivity
self.visible = false;
self.isActive = false;
// Only auto-start game if tutorial was completed naturally (not skipped)
if (!self.skipped) {
// Start game immediately without showing menu
if (gameMenu) {
gameMenu.startGame();
}
} else {
// If skipped, show the menu
if (gameMenu) {
gameMenu.visible = true;
}
}
// Reset skipped flag for next time
self.skipped = false;
};
// Handle tutorial interactions
self.down = function (x, y, obj) {
if (!self.isActive) return;
var step = self.tutorialSteps[self.currentStep];
// Handle skip button (top-right corner)
if (step.showSkip && x >= 2048 - 300 && x <= 2048 - 100 && y >= 100 && y <= 300) {
self.skipped = true; // Mark as skipped
self.completeTutorial();
return;
}
// For any tap anywhere on screen, advance to next step
if (step.id === 'tap_to_attack') {
// Simulate spell casting for demo
if (wizard) {
wizard.attack(0); // Attack center path
}
// Wait a moment then advance
tween({}, {}, {
duration: 1000,
onFinish: function onFinish() {
self.nextStep();
}
});
} else {
// For all other steps, advance immediately on any tap
self.nextStep();
}
};
return self;
});
var UpgradeMenu = Container.expand(function () {
var self = Container.call(this);
// Set high z-index to ensure menu appears above all game elements
self.zIndex = 1000;
// Cooldown system for upgrade menu
self.cooldownDuration = 120; // 2 seconds at 60fps
self.cooldownTimer = 0;
// Background removed - no overlay needed
// Title text
var titleText = new Text2('UPGRADES', {
size: 120,
fill: 0xFFD700,
font: "monospace"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 1050;
self.addChild(titleText);
// Initialize upgrade cost
self.upgradeCost = 5;
// Store references to upgrade text elements for updates
self.upgradeTextElements = [];
// Upgrade options - expanded to 10 upgrades
var upgradeOptions = [{
name: 'SHIELD',
description: 'Block 1 attack'
}, {
name: 'HEALTH BOOST',
description: 'More health'
}, {
name: 'LIFE DRAIN',
description: '5% chance +10 HP'
}, {
name: 'ENERGY SPHERE',
description: 'Auto-attack every 3s'
}, {
name: 'FORCE PUSH',
description: 'Push enemies back'
}, {
name: 'SPELL POWER',
description: 'Double damage'
}, {
name: 'THORNS',
description: 'Spikes on all paths'
}, {
name: 'FIRE BALL',
description: 'Launch fire ball every 3s'
}, {
name: 'FREEZE PULSE',
description: 'Freeze all enemies'
}, {
name: 'ORBS',
description: 'Orbs rotate around player'
}];
// Select 3 alternating upgrades from the available options
var selectedUpgrades = [];
var availableIndices = [];
for (var i = 0; i < upgradeOptions.length; i++) {
availableIndices.push(i);
}
// Track which upgrades were shown last time
if (!self.lastShownUpgrades) {
self.lastShownUpgrades = [];
}
// Remove previously shown upgrades from available options if possible
if (self.lastShownUpgrades.length > 0 && availableIndices.length > 3) {
for (var lastIdx = 0; lastIdx < self.lastShownUpgrades.length; lastIdx++) {
var indexToRemove = availableIndices.indexOf(self.lastShownUpgrades[lastIdx]);
if (indexToRemove !== -1) {
availableIndices.splice(indexToRemove, 1);
}
}
}
// If we don't have enough unique options, add back some previously shown ones
if (availableIndices.length < 3) {
availableIndices = [];
for (var i = 0; i < upgradeOptions.length; i++) {
availableIndices.push(i);
}
}
// Randomly select 3 unique upgrades from available options
for (var selection = 0; selection < 3; selection++) {
var randomIndex = Math.floor(Math.random() * availableIndices.length);
selectedUpgrades.push(availableIndices[randomIndex]);
availableIndices.splice(randomIndex, 1);
}
// Store current selection for next time
self.lastShownUpgrades = selectedUpgrades.slice(); // Copy array
self.selectedUpgradeIndices = selectedUpgrades; // Store for reference
// Create upgrade buttons in single row layout for 3 upgrades
for (var i = 0; i < selectedUpgrades.length; i++) {
var upgradeIndex = selectedUpgrades[i];
var upgrade = upgradeOptions[upgradeIndex];
// Calculate position in single row, 3-column grid
var xPos = 2048 / 2 - 500 + i * 500; // Space 3 upgrades across screen width with more separation
var yPos = 1400; // Single row positioned lower
// Upgrade button background
var upgradeBtn = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: xPos,
y: yPos,
scaleX: 6,
scaleY: 4.5
});
upgradeBtn.upgradeIndex = upgradeIndex;
upgradeBtn.displayIndex = i; // Store display index for touch detection
upgradeBtn.tint = 0x8B008B;
// Upgrade text
var upgradeText = new Text2(upgrade.name + '\n' + upgrade.description + '\nCost: ' + self.upgradeCost + ' coins', {
size: 40,
fill: 0xFFFFFF,
font: "monospace"
});
upgradeText.anchor.set(0.5, 0.5);
upgradeText.x = xPos;
upgradeText.y = yPos;
upgradeText.upgradeIndex = upgradeIndex; // Store actual upgrade index for reference
upgradeText.displayIndex = i; // Store display index for touch detection
upgradeText.upgradeName = upgrade.name;
upgradeText.upgradeDescription = upgrade.description;
self.addChild(upgradeText);
self.upgradeTextElements.push(upgradeText); // Store reference for updates
}
// Close button
var closeBtn = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1900,
scaleX: 2,
scaleY: 2
});
closeBtn.tint = 0xFF0000;
var closeText = new Text2('CLOSE', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
closeText.anchor.set(0.5, 0.5);
closeText.x = 2048 / 2;
closeText.y = 1900;
self.addChild(closeText);
// Method to refresh upgrade selection for alternating
self.refreshUpgradeSelection = function () {
var upgradeOptions = [{
name: 'SHIELD',
key: 'shield',
baseDesc: 'Block 1 attack',
improvedDesc: 'Block 2 attacks + regen'
}, {
name: 'HEALTH BOOST',
key: 'healthBoost',
baseDesc: 'More health',
improvedDesc: 'Much more health'
}, {
name: 'LIFE DRAIN',
key: 'lifeDrain',
baseDesc: '20% chance +20 HP',
improvedDesc: '20% chance +20 HP'
}, {
name: 'ENERGY SPHERE',
key: 'energySphere',
baseDesc: 'Auto-attack every 3s',
improvedDesc: 'Auto-attack every 2s'
}, {
name: 'FORCE PUSH',
key: 'forcePush',
baseDesc: 'Push enemies back',
improvedDesc: 'Stronger push + damage'
}, {
name: 'SPELL POWER',
key: 'spellPower',
baseDesc: 'Double damage',
improvedDesc: 'Triple damage'
}, {
name: 'THORNS',
key: 'thorns',
baseDesc: 'Spikes on all paths',
improvedDesc: 'Stronger spikes + faster'
}, {
name: 'FIRE BALL',
key: 'fireBall',
baseDesc: 'Launch fire ball every 3s',
improvedDesc: 'Launch fire ball every 2s'
}, {
name: 'FREEZE PULSE',
key: 'freezePulse',
baseDesc: 'Freeze all enemies',
improvedDesc: 'Freeze longer + damage'
}, {
name: 'ORBS',
key: 'orbs',
baseDesc: 'Orbs rotate around player',
improvedDesc: 'More orbs + faster rotation'
}];
// Select new alternating upgrades
var selectedUpgrades = [];
var availableIndices = [];
for (var i = 0; i < upgradeOptions.length; i++) {
availableIndices.push(i);
}
// Remove previously shown upgrades from available options if possible
if (self.lastShownUpgrades && self.lastShownUpgrades.length > 0 && availableIndices.length > 3) {
for (var lastIdx = 0; lastIdx < self.lastShownUpgrades.length; lastIdx++) {
var indexToRemove = availableIndices.indexOf(self.lastShownUpgrades[lastIdx]);
if (indexToRemove !== -1) {
availableIndices.splice(indexToRemove, 1);
}
}
}
// If we don't have enough unique options, add back some previously shown ones
if (availableIndices.length < 3) {
availableIndices = [];
for (var i = 0; i < upgradeOptions.length; i++) {
availableIndices.push(i);
}
}
// Select 3 unique upgrades from available options
for (var selection = 0; selection < 3; selection++) {
var randomIndex = Math.floor(Math.random() * availableIndices.length);
selectedUpgrades.push(availableIndices[randomIndex]);
availableIndices.splice(randomIndex, 1);
}
// Store current selection for next time
self.lastShownUpgrades = selectedUpgrades.slice();
self.selectedUpgradeIndices = selectedUpgrades;
// Update upgrade text elements with new selections
for (var i = 0; i < self.upgradeTextElements.length && i < selectedUpgrades.length; i++) {
var upgradeIndex = selectedUpgrades[i];
var upgrade = upgradeOptions[upgradeIndex];
var textElement = self.upgradeTextElements[i];
// Update the text element properties
textElement.upgradeIndex = upgradeIndex;
textElement.displayIndex = i;
textElement.upgradeName = upgrade.name;
textElement.upgradeDescription = upgrade.baseDesc;
}
};
// Method to update upgrade text with new costs and show improved versions
self.updateUpgradeCosts = function () {
// First refresh the upgrade selection to ensure alternating upgrades
self.refreshUpgradeSelection();
var upgradeOptions = [{
name: 'SHIELD',
key: 'shield',
baseDesc: 'Block 1 attack',
improvedDesc: 'Block 2 attacks + regen'
}, {
name: 'HEALTH BOOST',
key: 'healthBoost',
baseDesc: 'More health',
improvedDesc: 'Much more health'
}, {
name: 'LIFE DRAIN',
key: 'lifeDrain',
baseDesc: '20% chance +20 HP',
improvedDesc: '20% chance +20 HP'
}, {
name: 'ENERGY SPHERE',
key: 'energySphere',
baseDesc: 'Auto-attack every 3s',
improvedDesc: 'Auto-attack every 2s'
}, {
name: 'FORCE PUSH',
key: 'forcePush',
baseDesc: 'Push enemies back',
improvedDesc: 'Stronger push + damage'
}, {
name: 'SPELL POWER',
key: 'spellPower',
baseDesc: 'Double damage',
improvedDesc: 'Triple damage'
}, {
name: 'THORNS',
key: 'thorns',
baseDesc: 'Spikes on all paths',
improvedDesc: 'Stronger spikes + faster'
}, {
name: 'FIRE BALL',
key: 'fireBall',
baseDesc: 'Launch fire ball every 3s',
improvedDesc: 'Launch fire ball every 2s'
}, {
name: 'FREEZE PULSE',
key: 'freezePulse',
baseDesc: 'Freeze all enemies',
improvedDesc: 'Freeze longer + damage'
}, {
name: 'ORBS',
key: 'orbs',
baseDesc: 'Orbs rotate around player',
improvedDesc: 'More orbs + faster rotation'
}];
// Randomly select one upgrade to have modified cost (20% chance)
var hasModifiedCost = Math.random() < 0.20; // 20% probability
var modifiedCostIndex = hasModifiedCost ? Math.floor(Math.random() * 3) : -1; // Random upgrade among the 3
for (var i = 0; i < self.upgradeTextElements.length; i++) {
var textElement = self.upgradeTextElements[i];
var upgradeIndex = textElement.upgradeIndex; // Use stored upgrade index
var upgrade = upgradeOptions[upgradeIndex];
var currentLevel = upgradeLevels[upgrade.key];
// Calculate cost for this upgrade
var upgradeCost = self.upgradeCost;
if (i === modifiedCostIndex) {
upgradeCost = Math.floor(self.upgradeCost * 0.95); // 5% decrease, rounded down
}
// Show improved version if already purchased
var description = currentLevel > 0 ? upgrade.improvedDesc : upgrade.baseDesc;
var levelText = currentLevel > 0 ? ' (LV' + (currentLevel + 1) + ')' : '';
textElement.setText(upgrade.name + levelText + '\n' + description + '\nCost: ' + upgradeCost + ' coins');
textElement.upgradeName = upgrade.name;
textElement.upgradeDescription = description;
textElement.actualCost = upgradeCost; // Store the actual cost for purchase logic
}
};
// Update method for cooldown timer
self.update = function () {
if (self.cooldownTimer > 0) {
self.cooldownTimer--;
// Apply visual feedback during cooldown
var cooldownAlpha = 0.3 + self.cooldownTimer / self.cooldownDuration * 0.7;
self.alpha = cooldownAlpha;
} else {
self.alpha = 1.0; // Full opacity when cooldown is over
}
};
// Handle upgrade purchases
self.down = function (x, y, obj) {
// Check if cooldown is active
if (self.cooldownTimer > 0) {
// Flash red to indicate cooldown is active
LK.effects.flashScreen(0xFF0000, 200);
return; // Exit early if cooldown is active
}
// Determine which upgrade was clicked based on 3-upgrade layout
var upgradeIndex = -1;
// Check which upgrade cell was clicked (single row, 3 columns)
for (var i = 0; i < 3; i++) {
var cellX = 2048 / 2 - 500 + i * 500; // Calculate cell center X for 3-upgrade layout with more separation
var cellY = 1400; // Single row Y position
var cellWidth = 400; // Touch area width - increased to match new spacing
var cellHeight = 250; // Touch area height
// Check if click is within this cell
if (x >= cellX - cellWidth / 2 && x <= cellX + cellWidth / 2 && y >= cellY - cellHeight / 2 && y <= cellY + cellHeight / 2) {
upgradeIndex = self.selectedUpgradeIndices[i]; // Get actual upgrade index from selected list
break;
}
}
// Check if upgrade button was clicked
if (upgradeIndex !== -1) {
// Close menu immediately when any upgrade option is selected
self.closeMenu();
// Get the actual cost from the text element
var actualCost = self.upgradeTextElements[0].actualCost || self.upgradeCost || 5; // Default fallback
for (var textIdx = 0; textIdx < self.upgradeTextElements.length; textIdx++) {
if (self.upgradeTextElements[textIdx].displayIndex === Math.floor(upgradeIndex / 1)) {
actualCost = self.upgradeTextElements[textIdx].actualCost || self.upgradeCost || 5;
break;
}
}
// Find the correct text element by matching the upgrade selection
for (var cellIdx = 0; cellIdx < 3; cellIdx++) {
var cellX = 2048 / 2 - 500 + cellIdx * 500;
var cellY = 1400;
var cellWidth = 400;
var cellHeight = 250;
if (x >= cellX - cellWidth / 2 && x <= cellX + cellWidth / 2 && y >= cellY - cellHeight / 2 && y <= cellY + cellHeight / 2) {
actualCost = self.upgradeTextElements[cellIdx].actualCost || self.upgradeCost || 5;
break;
}
}
if (coinCounter >= actualCost) {
coinCounter -= actualCost;
coinText.setText('Coins: ' + coinCounter);
// Apply upgrade based on index
switch (upgradeIndex) {
case 0:
// Shield upgrade
upgradeLevels.shield++;
if (upgradeLevels.shield === 1) {
// First level: Basic shield
wizard.shieldActive = true;
wizard.maxShieldHits = 1;
wizard.currentShieldHits = 0;
} else {
// Improved level: Double shield + regeneration
wizard.shieldActive = true;
wizard.maxShieldHits = 2;
wizard.currentShieldHits = 0;
wizard.shieldRegen = true; // Enable shield regeneration
}
// Visual feedback for shield activation
LK.effects.flashObject(wizard, 0x00BFFF, 500);
// Create shield activation particles
for (var shieldIdx = 0; shieldIdx < 10; shieldIdx++) {
var shieldParticle = game.addChild(LK.getAsset('shield', {
anchorX: 0.5,
anchorY: 0.5,
x: wizard.x + (Math.random() - 0.5) * 200,
y: wizard.y + (Math.random() - 0.5) * 200,
scaleX: 0.5,
scaleY: 0.5
}));
shieldParticle.tint = 0x00BFFF;
shieldParticle.alpha = 0.8;
// Animate shield particles
tween(shieldParticle, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 500 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
shieldParticle.destroy();
}
});
}
// Add shield activation animation effect
tween(wizard.shieldGraphics, {
scaleX: 5,
scaleY: 5,
alpha: 1.0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(wizard.shieldGraphics, {
scaleX: 3,
scaleY: 3,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeIn
});
}
});
break;
case 1:
// Health Boost upgrade
upgradeLevels.healthBoost++;
var healthIncrease = 40;
wizard.maxHealth += healthIncrease;
wizard.health = Math.min(wizard.health + healthIncrease, wizard.maxHealth);
updateHealthBar();
break;
case 2:
// Life Drain upgrade
upgradeLevels.lifeDrain++;
if (!wizard.lifeDrainLevel) {
wizard.lifeDrainLevel = 0;
}
wizard.lifeDrainLevel = upgradeLevels.lifeDrain;
break;
case 3:
// Energy Sphere upgrade
upgradeLevels.energySphere++;
if (upgradeLevels.energySphere === 1) {
// First level: Create energy sphere
if (!wizard.energySphere) {
wizard.energySphere = game.addChild(new EnergyOrb());
}
}
// Visual feedback for energy sphere activation
LK.effects.flashObject(wizard, 0x00FFFF, 500);
break;
case 4:
// Force Push upgrade
upgradeLevels.forcePush++;
if (!wizard.forcePushLevel) {
wizard.forcePushLevel = 0;
wizard.forcePushTimer = 0;
}
wizard.forcePushLevel = upgradeLevels.forcePush;
// Visual feedback for force push
LK.effects.flashObject(wizard, 0xFFFF00, 500);
break;
case 5:
// Spell Power upgrade
upgradeLevels.spellPower++;
if (!wizard.spellPowerLevel) {
wizard.spellPowerLevel = 0;
}
wizard.spellPowerLevel = upgradeLevels.spellPower;
// Visual feedback for spell power
LK.effects.flashObject(wizard, 0xFF4500, 500);
break;
case 6:
// Thorns upgrade
upgradeLevels.thorns++;
if (!wizard.thornsLevel) {
wizard.thornsLevel = 0;
wizard.thornsTimer = 0;
}
wizard.thornsLevel = upgradeLevels.thorns;
// Visual feedback for thorns
LK.effects.flashObject(wizard, 0x8B4513, 500);
break;
case 7:
// Fire Ball upgrade
upgradeLevels.fireBall++;
if (!wizard.fireBallLevel) {
wizard.fireBallLevel = 0;
wizard.fireBallTimer = 0;
}
wizard.fireBallLevel = upgradeLevels.fireBall;
// Visual feedback for fire ball
LK.effects.flashObject(wizard, 0xFF4500, 500);
break;
case 8:
// Freeze Pulse upgrade
upgradeLevels.freezePulse++;
if (!wizard.freezePulseLevel) {
wizard.freezePulseLevel = 0;
wizard.freezePulseTimer = 0;
}
wizard.freezePulseLevel = upgradeLevels.freezePulse;
// Visual feedback for freeze pulse
LK.effects.flashObject(wizard, 0x87CEEB, 500);
break;
case 9:
// Orbs upgrade
upgradeLevels.orbs++;
if (!wizard.orbLevel) {
wizard.orbLevel = 0;
wizard.orbs = []; // Array to store orb instances
}
wizard.orbLevel++;
// Create new orb
var newOrb = game.addChild(new Orb());
// Set starting angle based on number of orbs for even distribution
newOrb.orbitalAngle = wizard.orbs.length * (Math.PI * 2) / (wizard.orbLevel + 1);
wizard.orbs.push(newOrb);
// Redistribute existing orbs evenly
for (var orbIdx = 0; orbIdx < wizard.orbs.length; orbIdx++) {
wizard.orbs[orbIdx].orbitalAngle = orbIdx * (Math.PI * 2) / wizard.orbs.length;
}
// Visual feedback for orb upgrade
LK.effects.flashObject(wizard, 0xFFD700, 500);
break;
}
// Visual feedback - flash the upgrade area green
LK.effects.flashScreen(0x00FF00, 300);
// Update medal statistics
if (!storage.totalUpgrades) storage.totalUpgrades = 0;
storage.totalUpgrades++;
} else {
// Not enough coins - flash red
LK.effects.flashScreen(0xFF0000, 300);
}
} else if (y >= 1750 && y <= 2050) {
// Close button area clicked
self.closeMenu();
}
};
self.closeMenu = function () {
self.visible = false;
gameStarted = true;
// Resume appropriate music based on game progress
if (enemyKillCounter >= 25) {
LK.playMusic('mysticalAmbient', {
volume: 0.6,
fade: {
start: 0,
end: 0.6,
duration: 1000
}
});
} else if (enemyKillCounter >= 10) {
LK.playMusic('epicBattle', {
volume: 0.8,
fade: {
start: 0,
end: 0.8,
duration: 1000
}
});
} else {
LK.playMusic('medievalTheme', {
volume: 0.7,
fade: {
start: 0,
end: 0.7,
duration: 1000
}
});
}
};
return self;
});
var Wizard = Container.expand(function () {
var self = Container.call(this);
// Animation system for wizard
self.currentFrame = 1;
self.animationTimer = 0;
self.animationSpeed = 18; // Change frame every 18 ticks (300ms at 60fps)
// Create all wizard graphics frames and store them
self.wizardFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset('wizard' + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 2.5,
scaleY: 2.5
});
frameGraphics.visible = i === 1; // Only show first frame initially
self.wizardFrames.push(frameGraphics);
}
// Create invisible hitbox with much smaller size for more precise collision
var hitbox = self.attachAsset('wizard1', {
anchorX: 0.3,
anchorY: 1.0,
scaleX: 0.25,
// Much smaller size for very precise collision
scaleY: 0.3 // Much smaller size for very precise collision
});
hitbox.alpha = 0; // Make hitbox invisible
// Position hitbox slightly to the right to reduce left side
hitbox.x = 15; // Offset hitbox to the right
// Create shield visual effect
self.shieldGraphics = self.attachAsset('shield', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 3
});
self.shieldGraphics.alpha = 0.7;
self.shieldGraphics.visible = false;
self.attackCooldown = 0;
self.level = 1;
self.experience = 0;
self.health = 100;
self.maxHealth = 100;
self.spellPowerLevel = 0; // Track spell power upgrades
self.shieldActive = false; // Track shield status
self.orbLevel = 0; // Track orb upgrades
self.orbs = []; // Array to store orb instances
// Override intersects method to use smaller hitbox
self.intersects = function (other) {
return hitbox.intersects(other);
};
self.update = function () {
// Pause wizard when tutorial is active
if (tutorial && tutorial.isActive) {
return;
}
// Pause all upgrades when upgrade menu is visible
if (upgradeMenu && upgradeMenu.visible) {
return;
}
if (self.attackCooldown > 0) {
self.attackCooldown--;
}
// Update shield visibility based on shield status
self.shieldGraphics.visible = self.shieldActive;
if (self.shieldActive) {
// Animate shield with pulsing effect
var pulse = 1 + Math.sin(LK.ticks * 0.15) * 0.2;
self.shieldGraphics.scaleX = 3 * pulse;
self.shieldGraphics.scaleY = 3 * pulse;
// Slowly rotate shield
self.shieldGraphics.rotation += 0.03;
// Add glowing effect
self.shieldGraphics.alpha = 0.6 + Math.sin(LK.ticks * 0.1) * 0.2;
}
// Force Push upgrade - push all enemies backward every 4 seconds
if (self.forcePushLevel && self.forcePushLevel > 0) {
if (!self.forcePushTimer) {
self.forcePushTimer = 0;
}
self.forcePushTimer++;
if (self.forcePushTimer >= 240) {
// 4 seconds at 60fps
self.forcePushTimer = 0;
self.activateForcePush();
}
}
// Freeze Pulse upgrade - freeze all enemies every 4 seconds
if (self.freezePulseLevel && self.freezePulseLevel > 0) {
if (!self.freezePulseTimer) {
self.freezePulseTimer = 0;
}
self.freezePulseTimer++;
if (self.freezePulseTimer >= 240) {
// 4 seconds at 60fps
self.freezePulseTimer = 0;
self.activateFreezePulse();
}
}
// Fire Ball upgrade - launch fire ball with improved timing
if (self.fireBallLevel && self.fireBallLevel > 0) {
if (!self.fireBallTimer) {
self.fireBallTimer = 0;
}
self.fireBallTimer++;
var fireBallInterval = upgradeLevels.fireBall > 1 ? 120 : 180; // 2s vs 3s
if (self.fireBallTimer >= fireBallInterval) {
self.fireBallTimer = 0;
self.launchFireBall();
}
}
// Thorns upgrade - spawn spikes along all paths every 3 seconds
if (self.thornsLevel && self.thornsLevel > 0) {
if (!self.thornsTimer) {
self.thornsTimer = 0;
}
self.thornsTimer++;
if (self.thornsTimer >= 180) {
// Always 3 seconds (180 ticks at 60fps)
self.thornsTimer = 0;
self.activateThorns();
}
}
// Animation system - cycle through wizard frames
self.animationTimer++;
if (self.animationTimer >= self.animationSpeed) {
self.animationTimer = 0;
// Hide current frame
self.wizardFrames[self.currentFrame - 1].visible = false;
// Move to next frame
self.currentFrame++;
if (self.currentFrame > 4) {
self.currentFrame = 1;
}
// Show new frame
self.wizardFrames[self.currentFrame - 1].visible = true;
}
};
self.attack = function (direction) {
if (self.attackCooldown <= 0) {
// Default direction if none specified
if (direction === undefined) {
direction = 0; // Default to center path
}
// Get attack angle based on path direction
var attackAngle = pathAngles[direction];
var attackDistance = 100;
// Calculate spell position based on attack direction
var spellX = self.x + Math.cos(attackAngle) * attackDistance;
var spellY = self.y + Math.sin(attackAngle) * attackDistance;
// Create spell effect
var spell = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: spellX,
y: spellY,
scaleX: 0.5,
scaleY: 0.5
}));
// Animate spell with magical effects
tween(spell, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
spell.destroy();
}
});
// Add rotation animation to spell
tween(spell, {
rotation: Math.PI * 2
}, {
duration: 500,
easing: tween.linear
});
self.attackCooldown = 30; // 0.5 seconds at 60fps
LK.getSound('spellCast').play();
// Base damage without spell power upgrades
var totalDamage = 1;
// Attack enemies in the specified direction/path
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.pathIndex === direction) {
// Only hit enemies on exact same path - no distance validation
enemy.takeDamage(totalDamage);
}
}
// Attack ogres in the specified direction/path
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
if (ogre.pathIndex === direction) {
// Only hit ogres on exact same path - no distance validation
ogre.takeDamage(totalDamage);
}
}
// Attack knights in the specified direction/path
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
if (knight.pathIndex === direction) {
// Only hit knights on exact same path - no distance validation
knight.takeDamage(totalDamage);
}
}
return true;
}
return false;
};
self.gainExperience = function (amount) {
self.experience += amount;
var expNeeded = self.level * 100;
if (self.experience >= expNeeded) {
self.levelUp();
}
};
self.levelUp = function () {
self.level++;
self.experience = 0;
// Visual level up effect
LK.effects.flashObject(self, 0xFFD700, 500);
};
self.takeDamage = function (damage) {
// Check if shield is active
if (self.shieldActive) {
// Initialize shield properties if not set
if (self.maxShieldHits === undefined) {
self.maxShieldHits = 1;
self.currentShieldHits = 0;
}
// Increment shield hits
self.currentShieldHits++;
// Visual feedback for shield use
LK.effects.flashObject(self, 0x00BFFF, 300);
// Check if shield is depleted
if (self.currentShieldHits >= self.maxShieldHits) {
self.shieldActive = false;
// Start shield regeneration timer
var regenTime = self.shieldRegen ? 5000 : 10000; // Faster regen if improved
tween({}, {}, {
duration: regenTime,
onFinish: function onFinish() {
// Regenerate shield
self.shieldActive = true;
self.currentShieldHits = 0;
// Visual feedback for shield regeneration
LK.effects.flashObject(self, 0x00BFFF, 500);
// Add shield regeneration animation
tween(self.shieldGraphics, {
scaleX: 5,
scaleY: 5,
alpha: 1.0
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.shieldGraphics, {
scaleX: 3,
scaleY: 3,
alpha: 0.7
}, {
duration: 400,
easing: tween.easeIn
});
}
});
}
});
}
// No damage taken, shield absorbed it
return;
}
self.health -= damage;
if (self.health <= 0) {
self.health = 0;
// 10% chance to revive when dying
var reviveChance = Math.random();
if (reviveChance < 0.10) {
// Revival successful!
self.health = Math.floor(self.maxHealth * 0.5); // Revive with 50% health
// Destroy ALL enemies when revival activates (no distance restriction)
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var enemyIdx = allEnemies.length - 1; enemyIdx >= 0; enemyIdx--) {
var enemy = allEnemies[enemyIdx];
// Create destruction effect for each enemy
LK.effects.flashObject(enemy, 0xFFD700, 500);
// Create golden explosion particles
for (var particleIdx = 0; particleIdx < 8; particleIdx++) {
var destructionParticle = game.addChild(LK.getAsset('energySphere', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x,
y: enemy.y,
scaleX: 0.5,
scaleY: 0.5
}));
destructionParticle.tint = 0xFFD700;
destructionParticle.alpha = 0.9;
// Random explosion direction
var explosionAngle = particleIdx / 8 * Math.PI * 2;
var explosionSpeed = 60 + Math.random() * 40;
var explosionTargetX = enemy.x + Math.cos(explosionAngle) * explosionSpeed;
var explosionTargetY = enemy.y + Math.sin(explosionAngle) * explosionSpeed;
// Animate explosion particle
tween(destructionParticle, {
x: explosionTargetX,
y: explosionTargetY,
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 400 + Math.random() * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
destructionParticle.destroy();
}
});
}
// Kill ALL enemies instantly by calling die() method
enemy.die();
}
// Visual effects for revival
LK.effects.flashScreen(0x00FF00, 1500); // Green flash for revival
LK.effects.flashObject(self, 0xFFD700, 1000); // Golden flash on wizard
// Reduced revival particles for better performance
for (var reviveIdx = 0; reviveIdx < 8; reviveIdx++) {
var reviveParticle = game.addChild(LK.getAsset('energySphere', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x + (Math.random() - 0.5) * 200,
y: self.y + (Math.random() - 0.5) * 200,
scaleX: 1.0,
scaleY: 1.0
}));
reviveParticle.tint = 0x00FF00;
reviveParticle.alpha = 0.9;
// Animate revival particles toward wizard
tween(reviveParticle, {
x: self.x,
y: self.y,
scaleX: 0.3,
scaleY: 0.3,
alpha: 0
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
reviveParticle.destroy();
}
});
}
// Create healing aura effect
var healingAura = game.addChild(LK.getAsset('shield', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 1,
scaleY: 1
}));
healingAura.tint = 0x00FF00;
healingAura.alpha = 0.8;
// Animate healing aura expansion
tween(healingAura, {
scaleX: 8,
scaleY: 8,
alpha: 0
}, {
duration: 1200,
easing: tween.easeOut,
onFinish: function onFinish() {
healingAura.destroy();
}
});
// Play spell cast sound for revival
LK.getSound('spellCast').play();
// Update health bar to show revival
updateHealthBar();
// Update medal statistics
if (!storage.totalRevives) storage.totalRevives = 0;
storage.totalRevives++;
} else {
// Game over when health reaches 0 and no revival
LK.effects.flashScreen(0xFF0000, 1000);
LK.showGameOver();
}
}
// Update health bar
updateHealthBar();
// Simplified screen shake for better performance
var shakeIntensity = 8;
var originalX = game.x;
var originalY = game.y;
// Simple single shake effect
tween(game, {
x: originalX + shakeIntensity,
y: originalY + shakeIntensity * 0.5
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(game, {
x: originalX,
y: originalY
}, {
duration: 100,
easing: tween.easeIn
});
}
});
// Flash red when hit
LK.effects.flashObject(self, 0xFF0000, 200);
};
self.activateForcePush = function () {
// Visual effect for force push activation
LK.effects.flashScreen(0x8A2BE2, 300); // Purple flash
LK.effects.flashObject(self, 0x8A2BE2, 500); // Purple flash on wizard
// Push back all enemies with improved effects
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
// Calculate direction from wizard to enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Improved force push: stronger push and damage
var pushDistance = upgradeLevels.forcePush > 1 ? 300 : 200; // Stronger push
var pushX = dx / distance * pushDistance;
var pushY = dy / distance * pushDistance;
// Calculate new position
var newX = enemy.x + pushX;
var newY = enemy.y + pushY;
// Ensure enemies don't go off screen
newX = Math.max(50, Math.min(1998, newX));
newY = Math.max(-100, Math.min(2732, newY));
// Animate the push effect
tween(enemy, {
x: newX,
y: newY
}, {
duration: 300,
easing: tween.easeOut
});
// Improved force push: deal damage
if (upgradeLevels.forcePush > 1) {
enemy.takeDamage(50);
}
// Visual effect on each enemy
LK.effects.flashObject(enemy, 0x8A2BE2, 200);
}
}
};
self.activateFreezePulse = function () {
// Visual effect for freeze pulse activation
LK.effects.flashScreen(0x87CEEB, 500); // Light blue flash
LK.effects.flashObject(self, 0x87CEEB, 700); // Light blue flash on wizard
// Play freeze sound effect
LK.getSound('iceFreeze').play();
// Freeze all enemies with improved effects
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
// Improved freeze: longer duration and damage
var freezeDuration = upgradeLevels.freezePulse > 1 ? 120 : 60; // 2s vs 1s
enemy.frozen = true;
enemy.frozenTimer = freezeDuration;
// Improved freeze: deal damage
if (upgradeLevels.freezePulse > 1) {
enemy.takeDamage(30);
}
// Visual freeze effect - tint enemy light blue
tween(enemy, {
tint: 0x87CEEB
}, {
duration: 100,
easing: tween.easeOut
});
// Reduced ice crystal particles for better performance
if (i % 2 === 0) {
// Only create effects for every other enemy
for (var iceIdx = 0; iceIdx < 3; iceIdx++) {
var iceCrystal = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x + (Math.random() - 0.5) * 60,
y: enemy.y + (Math.random() - 0.5) * 60,
scaleX: 1.0,
scaleY: 1.0
}));
iceCrystal.tint = 0x87CEEB;
iceCrystal.alpha = 0.9;
// Create floating ice effect
tween(iceCrystal, {
y: iceCrystal.y - 30,
scaleX: 0.3,
scaleY: 0.3,
alpha: 0
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
iceCrystal.destroy();
}
});
}
}
// Remove freeze tint after frozen state ends
var visualDuration = upgradeLevels.freezePulse > 1 ? 2000 : 1000;
tween({}, {}, {
duration: visualDuration,
onFinish: function onFinish() {
if (enemy && enemy.parent) {
// Remove freeze tint after frozen effect ends
tween(enemy, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeIn
});
}
}
});
}
};
self.activateThorns = function () {
// Visual effect for thorns activation
LK.effects.flashScreen(0x8B4513, 300); // Brown flash
LK.effects.flashObject(self, 0x8B4513, 500); // Brown flash on wizard
// Find the closest enemy to the wizard
var closestEnemy = null;
var closestDistance = Infinity;
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// Only create spikes if there is a closest enemy
if (!closestEnemy) {
LK.getSound('spellCast').play();
return;
}
// Create spikes only along the closest enemy's path
var pathIdx = closestEnemy.pathIndex;
var pathAngle = pathAngles[pathIdx];
// Calculate spawn position for this path (same as enemy spawning)
var spawnX, spawnY;
if (pathIdx === 0) {
// Center path - spawn at top edge
spawnX = 2048 / 2;
spawnY = -100;
} else if (pathIdx === 1) {
// Path 2 - spawn at top right edge
spawnX = 2048 + 50;
spawnY = -50;
} else if (pathIdx === 2) {
// Path 3 - spawn at top left edge
spawnX = -50;
spawnY = -50;
} else if (pathIdx === 3) {
// Path 4 - spawn at left edge
spawnX = -100;
spawnY = 2732 / 2 + 400;
} else if (pathIdx === 4) {
// Path 5 - spawn at right edge
spawnX = 2048 + 100;
spawnY = 2732 / 2 + 400;
}
// Calculate wizard position (same as enemy targeting)
var wizardX = self.x;
var wizardY = self.y;
// Calculate path distance and divide into 3 sections with gaps
var pathDistance = Math.sqrt((spawnX - wizardX) * (spawnX - wizardX) + (spawnY - wizardY) * (spawnY - wizardY));
var sectionLength = pathDistance / 5; // Each section is 1/5 of total path
var gapLength = pathDistance / 10; // Gaps are 1/10 of total path
// Define 3 sections along the path with gaps between them
var sections = [{
start: 0.1,
end: 0.3
},
// First section: 10% to 30% along path
{
start: 0.45,
end: 0.65
},
// Second section: 45% to 65% along path
{
start: 0.8,
end: 1.0
} // Third section: 80% to 100% along path
];
// Create spikes in reverse sequential order: last section, then middle, then first
var sectionOrder = [2, 1, 0]; // Create sections starting from farthest outward toward wizard
for (var orderIdx = 0; orderIdx < sectionOrder.length; orderIdx++) {
var sectionIdx = sectionOrder[orderIdx];
var section = sections[sectionIdx];
var sectionStartDistance = pathDistance * section.start;
var sectionEndDistance = pathDistance * section.end;
var spikeSpacing = 150; // Distance between spikes within each section
// Calculate number of spikes in this section
var sectionLength = sectionEndDistance - sectionStartDistance;
var numSpikesInSection = Math.floor(sectionLength / spikeSpacing);
// Calculate delay for sequential appearance
var baseDelay = orderIdx * 300; // 300ms delay between sections
// Create spikes within this section with sequential timing
for (var s = 0; s < numSpikesInSection; s++) {
var spikeDistanceInSection = s * spikeSpacing + spikeSpacing / 2;
var totalSpikeDistance = sectionStartDistance + spikeDistanceInSection;
var progress = totalSpikeDistance / pathDistance;
var spikeX = spawnX + (wizardX - spawnX) * progress;
var spikeY = spawnY + (wizardY - spawnY) * progress;
// Only create spike if position is within game bounds
if (spikeX >= 0 && spikeX <= 2048 && spikeY >= 0 && spikeY <= 2732) {
// Create spike with delay
(function (delayTime, spikeX, spikeY, pathIdx) {
tween({}, {}, {
duration: delayTime,
onFinish: function onFinish() {
var spike = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: spikeX,
y: spikeY,
scaleX: 0.1,
scaleY: 0.1
}));
spike.tint = 0x8B4513; // Brown color for thorns
spike.pathIndex = pathIdx;
// Initialize hit tracking for this spike
spike.hitEnemies = [];
// Set spike to be visible immediately
spike.alpha = 1.0;
spike.scaleX = 1.5;
spike.scaleY = 1.5;
// Animate spike emerging from ground
tween(spike, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 1.0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Spike stays for a moment then disappears
tween(spike, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 800,
easing: tween.easeIn,
onFinish: function onFinish() {
spike.destroy();
}
});
}
});
}
});
})(baseDelay + s * 50, spikeX, spikeY, pathIdx); // 50ms delay between spikes in same section
}
}
}
LK.getSound('spellCast').play();
};
self.launchFireBall = function () {
// Visual effect for fire ball launch
LK.effects.flashScreen(0xFF4500, 300); // Orange flash
LK.effects.flashObject(self, 0xFF4500, 500); // Orange flash on wizard
// Create fire ball projectile at wizard position
var fireBall = game.addChild(new FireBall());
fireBall.x = self.x;
fireBall.y = self.y;
// Find closest enemy to target
var closestEnemy = null;
var closestDistance = Infinity;
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// Set fire ball direction toward closest enemy or default direction
if (closestEnemy) {
var dx = closestEnemy.x - self.x;
var dy = closestEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
fireBall.direction.x = dx / distance;
fireBall.direction.y = dy / distance;
}
} else {
// Default direction upward if no enemies
fireBall.direction.x = 0;
fireBall.direction.y = -1;
}
LK.getSound('spellCast').play();
LK.getSound('fireWhoosh').play();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000 // Black background for pixel art
});
/****
* Game Code
****/
// Unified helper functions
// Game arrays to track objects
function createImpactEffect(x, y, color, scale) {
var impact = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: x,
y: y,
scaleX: 0.3,
scaleY: 0.3
}));
impact.tint = color;
impact.alpha = 1.0;
tween(impact, {
scaleX: scale,
scaleY: scale,
alpha: 0
}, {
duration: 450,
easing: tween.easeOut,
onFinish: function onFinish() {
impact.destroy();
}
});
}
function dropCoin(x, y) {
var coin = game.addChild(new Coin());
coin.x = x;
coin.y = y - 50;
coin.isAnimating = true;
coins.push(coin);
var targetX = 120 + coinText.width / 2,
targetY = 90 + coinText.height / 2;
tween(coin, {
x: targetX,
y: targetY,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
var difficulty = storage.difficulty || 'NORMAL';
var reward = difficulty === 'FACIL' ? Math.floor(1.5) : difficulty === 'DIFICIL' ? Math.max(1, Math.floor(0.75)) : 1;
coinCounter += reward;
coinText.setText('Coins: ' + coinCounter);
if (difficulty === 'FACIL' && Math.random() < 0.15) {
wizard.health = Math.min(wizard.health + 5, wizard.maxHealth);
updateHealthBar();
LK.effects.flashObject(wizard, 0x00FF00, 200);
}
removeFromArray(coin, coins);
coin.destroy();
}
});
}
function updatePathCombo(pathIndex) {
if (pathIndex !== undefined) {
pathKillCounts[pathIndex]++;
if (pathKillCounts[pathIndex] >= 3) {
pathKillCounts[pathIndex] = 0;
nextProjectileDoubleDamage = true;
LK.effects.flashScreen(0xFFD700, 500);
var comboText = new Text2('combo', {
size: 120,
fill: 0xFFD700,
font: "monospace"
});
comboText.anchor.set(0.5, 0.5);
comboText.x = 2048 / 2;
comboText.y = 2732 / 2 - 200;
game.addChild(comboText);
tween(comboText, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
comboText.destroy();
}
});
}
}
}
function removeFromArray(obj, array) {
array = array || (obj.enemyType === 'skeleton' ? enemies : obj.enemyType === 'ogre' ? ogres : obj.enemyType === 'knight' ? knights : miniBosses);
for (var i = array.length - 1; i >= 0; i--) {
if (array[i] === obj) {
array.splice(i, 1);
break;
}
}
}
function getSpawnPath() {
if (enemyKillCounter < 5) return 0; // Center path for first 5
var availablePaths = [];
for (var i = 0; i < 5; i++) {
if (pathConsecutiveSpawns[i] < 2) availablePaths.push(i);
}
if (availablePaths.length === 0) {
for (var i = 0; i < 5; i++) {
pathConsecutiveSpawns[i] = 0;
availablePaths.push(i);
}
}
return availablePaths[Math.floor(Math.random() * availablePaths.length)];
}
function getSpawnPosition(pathIndex) {
var positions = [{
x: 2048 / 2,
y: -100
},
// Center
{
x: 2048 + 50,
y: -50
},
// Top right
{
x: -50,
y: -50
},
// Top left
{
x: -100,
y: 2732 / 2 + 400
},
// Left
{
x: 2048 + 100,
y: 2732 / 2 + 400
} // Right
];
return positions[pathIndex];
}
function spawnEnemy(type, difficulty, healthMult, speedMult) {
var enemy, array;
var baseStats = {
skeleton: {
health: 100,
speed: 3,
eliteChance: 0.20,
eliteThreshold: 20
},
ogre: {
health: 200,
speed: 2.5,
eliteChance: 0.15,
eliteThreshold: 30
},
knight: {
health: 300,
speed: 2,
eliteChance: 0.10,
eliteThreshold: 40
},
miniboss: {
health: 3000,
speed: 4,
eliteChance: 0,
eliteThreshold: 999
}
};
var stats = baseStats[type];
// Create enemy using BaseEnemy constructor directly
if (type === 'skeleton') {
enemy = game.addChild(new BaseEnemy({
type: 'skeleton',
assetBase: 'esqueleto',
health: 1,
speed: 3,
score: 10,
experience: 25
}));
array = enemies;
} else if (type === 'ogre') {
enemy = game.addChild(new BaseEnemy({
type: 'ogre',
assetBase: 'ogre',
health: 2,
speed: 3,
score: 15,
experience: 25,
vibration: 75,
impactColor: 0xFF4500,
damageColor: 0xFF6600
}));
array = ogres;
} else if (type === 'berserkerOgre') {
enemy = game.addChild(new BaseEnemy({
type: 'ogre',
assetBase: 'berserkerOgre',
health: 2,
speed: 3,
score: 15,
experience: 25,
vibration: 75,
impactColor: 0xFF4500,
damageColor: 0xFF6600
}));
array = ogres;
} else if (type === 'knight') {
enemy = game.addChild(new BaseEnemy({
type: 'knight',
assetBase: 'knight',
health: 3,
speed: 3,
score: 20,
experience: 25,
vibration: 100,
impactColor: 0xFFD700,
damageColor: 0xFFD700
}));
array = knights;
} else if (type === 'miniboss') {
enemy = game.addChild(new BaseEnemy({
type: 'miniboss',
assetBase: 'knight',
health: 3000,
speed: 4,
score: 100,
experience: 250,
scale: 4.0,
vibration: 100,
impactColor: 0xFF0000,
damageColor: 0xFF0000,
animSpeed: 12
}));
array = miniBosses;
}
// Apply difficulty scaling
var diffMods = {
FACIL: {
healthMod: 0.8,
speedMod: 0.9
},
NORMAL: {
healthMod: 1.0,
speedMod: 1.0
},
DIFICIL: {
healthMod: 1.0,
speedMod: 1.1
}
};
var mods = diffMods[difficulty] || diffMods.NORMAL;
enemy.health = Math.floor(baseStats[type].health * mods.healthMod * healthMult);
enemy.speed = baseStats[type].speed * mods.speedMod * speedMult;
enemy.maxHealth = enemy.health;
// Elite variants for hard mode
if (difficulty === 'DIFICIL' && enemyKillCounter >= stats.eliteThreshold && Math.random() < stats.eliteChance) {
enemy.health *= type === 'knight' ? 1.67 : 1.5; // Champion knights: 500hp
enemy.speed *= type === 'knight' ? 1.5 : 1.3;
var tintColor = type === 'skeleton' ? 0xFF6600 : type === 'ogre' ? 0xFF0000 : 0xFFD700;
for (var i = 0; i < enemy.frames.length; i++) enemy.frames[i].tint = tintColor;
}
// Position enemy
var pathIndex = type === 'miniboss' ? 0 : getSpawnPath();
var pos = getSpawnPosition(pathIndex);
enemy.x = Math.max(50, Math.min(1998, pos.x));
enemy.y = Math.max(-200, Math.min(2732 + 100, pos.y));
enemy.pathIndex = pathIndex;
enemy.pathAngle = pathAngles[pathIndex];
enemy.lastX = enemy.x;
// Update path tracking
pathConsecutiveSpawns[pathIndex]++;
pathLastSpawnTime[pathIndex] = LK.ticks;
// Special setup for miniboss
if (type === 'miniboss' && enemy.updateHealthBar) enemy.updateHealthBar();
array.push(enemy);
}
var gameStarted = false;
var gameMenu;
var upgradeMenu;
var upgradeMenuShown = false;
var secondUpgradeMenuShown = false;
var thirdUpgradeMenuShown = false;
var fourthUpgradeMenuShown = false;
var selectedEnemy = null; // Track currently selected enemy for projectile targeting
var energySphere = null; // Track energy sphere instance
// Upgrade level tracking system
var upgradeLevels = {
shield: 0,
healthBoost: 0,
lifeDrain: 0,
energySphere: 0,
forcePush: 0,
spellPower: 0,
thorns: 0,
fireBall: 0,
freezePulse: 0,
orbs: 0
};
// Game arrays to track objects
var enemies = [];
var ogres = [];
var knights = [];
var miniBosses = [];
var coins = [];
var projectiles = [];
// Path combo system variables
var pathKillCounts = [0, 0, 0, 0, 0]; // Track kills per path (5 paths)
var nextProjectileDoubleDamage = false; // Flag for double damage on next projectile
// Create tutorial system first (initially hidden)
var tutorial = game.addChild(new Tutorial());
tutorial.visible = false;
// Create and show game menu
gameMenu = game.addChild(new GameMenu());
// Create upgrade menu (initially hidden)
upgradeMenu = game.addChild(new UpgradeMenu());
upgradeMenu.visible = false;
// Create prestige medals menu (initially hidden)
var prestigeMedals = game.addChild(new PrestigeMedals());
prestigeMedals.visible = false;
// Create 5 stone paths leading toward the wizard position
var paths = [];
var knightX = 2048 / 2;
var knightY = 2732 - 250;
// Create paths: all pointing toward the wizard position
var pathAngles = [-Math.PI / 2,
// Center (straight down toward wizard)
-Math.PI / 3,
// Diagonal right toward wizard
-2 * Math.PI / 3,
// Diagonal left toward wizard
Math.PI / 6,
// Path 4: opposite to path 2 (diagonal left)
5 * Math.PI / 6 // Path 5: opposite to path 3 (diagonal right)
]; // 5 paths: all directed toward wizard position
// Create multiple stone segments for each path to form visible stone roads
for (var p = 0; p < 5; p++) {
var angle = pathAngles[p];
// Calculate distance from spawn point to wizard position
var wizardX = knightX;
var wizardY = 2732 - 600; // Wizard position
var spawnDistance = p === 0 ? 2000 : p === 3 || p === 4 ? 1200 : 1800; // Distance from wizard to spawn point
// Set spawn points to screen edges for all paths
var spawnX, spawnY;
if (p === 0) {
// Center path - spawn at top edge
spawnX = 2048 / 2;
spawnY = -100;
} else if (p === 1) {
// Path 2 - spawn at top right edge
spawnX = 2048 + 50;
spawnY = -50;
} else if (p === 2) {
// Path 3 - spawn at top left edge
spawnX = -50;
spawnY = -50;
} else if (p === 3) {
// Path 4 - spawn at left edge
spawnX = -100;
spawnY = 2732 / 2 + 400;
} else if (p === 4) {
// Path 5 - spawn at right edge
spawnX = 2048 + 100;
spawnY = 2732 / 2 + 400;
}
// Calculate actual path length from spawn point to wizard
var actualPathLength = Math.sqrt((spawnX - wizardX) * (spawnX - wizardX) + (spawnY - wizardY) * (spawnY - wizardY));
var segmentSize = 120; // Size of each stone segment
var numSegments = Math.floor(actualPathLength / segmentSize);
// Create individual stone segments along the path from spawn point toward wizard
for (var s = 0; s < numSegments; s++) {
var segmentDistance = s * segmentSize + segmentSize / 2;
var segmentX = spawnX - Math.cos(angle) * segmentDistance;
var segmentY = spawnY - Math.sin(angle) * segmentDistance;
// Only create segments that are within the visible game area
if (segmentX >= -100 && segmentX <= 2148 && segmentY >= -100 && segmentY <= 2832) {
// Create stone path segment
var stoneSegment = game.addChild(LK.getAsset('stonePath', {
anchorX: 0.5,
anchorY: 0.5,
x: segmentX,
y: segmentY,
scaleX: 2.0,
scaleY: 2.0,
rotation: angle + Math.PI / 2 // Rotate stone to align with path
}));
// Make stone paths completely invisible
stoneSegment.alpha = 0; // Make completely invisible
stoneSegment.visible = false; // Initially hidden, will be shown when game starts
stoneSegment.pathIndex = p;
}
}
}
// Create invisible path collision areas for touch detection
for (var p = 0; p < 5; p++) {
var angle = pathAngles[p];
// Calculate distance from spawn point to wizard position
var wizardX = knightX;
var wizardY = 2732 - 600; // Wizard position
var spawnDistance = p === 0 ? 3000 : p === 3 || p === 4 ? 1200 : 1800; // Distance from wizard to spawn point
var spawnX = wizardX + Math.cos(angle) * spawnDistance;
var spawnY = wizardY + Math.sin(angle) * spawnDistance;
// Move all spawn points to screen edges
if (p === 0) {
// Center path - spawn at top edge
spawnX = 2048 / 2;
spawnY = -100;
} else if (p === 1) {
// Path 2 - spawn at top right edge
spawnX = 2048 + 50;
spawnY = -50;
} else if (p === 2) {
// Path 3 - spawn at top left edge
spawnX = -50;
spawnY = -50;
} else if (p === 3) {
// Path 4 - spawn at left edge
spawnX = -100;
spawnY = 2732 / 2 + 400;
} else if (p === 4) {
// Path 5 - spawn at right edge
spawnX = 2048 + 100;
spawnY = 2732 / 2 + 400;
}
// Calculate actual path length from spawn point to wizard
var actualPathLength = Math.sqrt((spawnX - wizardX) * (spawnX - wizardX) + (spawnY - wizardY) * (spawnY - wizardY));
var centerX = (spawnX + wizardX) / 2;
var centerY = (spawnY + wizardY) / 2;
// Create invisible collision path
var path = game.addChild(LK.getAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: centerX,
y: centerY,
scaleX: 4,
scaleY: actualPathLength / 60,
rotation: angle + Math.PI / 2
}));
// Make collision path completely invisible
path.alpha = 0;
path.visible = false;
path.pathIndex = p;
// Add numbered text label for each path
var pathNumber = new Text2((p + 1).toString(), {
size: 120,
fill: 0xFFD700,
font: "monospace"
});
pathNumber.anchor.set(0.5, 0.5);
// Position number directly at spawn point
pathNumber.x = spawnX;
pathNumber.y = spawnY - 80; // Position slightly above spawn point
pathNumber.visible = false; // Initially hidden, will be shown when game starts
pathNumber.pathIndex = p;
game.addChild(pathNumber);
// Add touch handler for directional attacks
path.down = function (x, y, obj) {
// Attack in this path's direction
wizard.attack(obj.pathIndex);
// Visual feedback - no flash for invisible paths
// Paths remain invisible when tapped
};
paths.push(path);
}
// Pixel art scaling handled by engine automatically
// Set fondodelacueva as the actual game background
var backgroundMap = game.addChild(LK.getAsset('fondodelacueva', {
anchorX: 0,
anchorY: 0,
scaleX: 19.5,
scaleY: 26.0,
x: 0,
y: 0
}));
// Send background to the back but use a less extreme z-index
backgroundMap.zIndex = -100;
// Hide background initially during menu
backgroundMap.visible = false;
backgroundMap.alpha = 1.0;
// Create wizard
var wizard = game.addChild(new Wizard());
wizard.x = knightX;
wizard.y = 2732 - 600; // Position wizard higher on screen
wizard.visible = false;
// UI Elements
// Removed scoreText and levelText to eliminate stray characters in top right
var coinCounter = 0;
var enemyKillCounter = 0;
var coinText = new Text2('Coins: 0', {
size: 60,
fill: 0xFFD700,
font: "monospace"
});
coinText.anchor.set(0, 0);
LK.gui.topLeft.addChild(coinText);
coinText.x = 120;
coinText.y = 90;
coinText.visible = false;
var killCountText = new Text2('Puntuacion: 0', {
size: 60,
fill: 0xFF6B6B,
font: "monospace"
});
killCountText.anchor.set(0, 0);
LK.gui.topLeft.addChild(killCountText);
killCountText.x = 120;
killCountText.y = 150;
killCountText.visible = false;
var tapText = new Text2('TAP TO CAST SPELLS!', {
size: 100,
fill: 0xFF6B6B,
font: "monospace"
});
tapText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(tapText);
tapText.y = -200;
tapText.visible = false;
// Health bar UI
var healthBarBg = LK.getAsset('healthBarBg', {
anchorX: 0,
anchorY: 0
});
LK.gui.topLeft.addChild(healthBarBg);
healthBarBg.x = 120;
healthBarBg.y = 20;
healthBarBg.visible = false;
var healthBar = LK.getAsset('healthBar', {
anchorX: 0,
anchorY: 0
});
LK.gui.topLeft.addChild(healthBar);
healthBar.x = 120;
healthBar.y = 20;
healthBar.visible = false;
var healthText = new Text2('Health: 100/100', {
size: 50,
fill: 0xFFFFFF,
font: "monospace"
});
healthText.anchor.set(0, 0);
LK.gui.topLeft.addChild(healthText);
healthText.x = 120;
healthText.y = 50;
healthText.visible = false;
function updateHealthBar() {
var healthPercent = wizard.health / wizard.maxHealth;
healthBar.scaleX = healthPercent;
healthText.setText('Health: ' + wizard.health + '/' + wizard.maxHealth);
// Change color based on health
if (healthPercent > 0.6) {
healthBar.tint = 0x00ff00; // Green
} else if (healthPercent > 0.3) {
healthBar.tint = 0xffff00; // Yellow
} else {
healthBar.tint = 0xff0000; // Red
}
}
// Enemy spawning variables
var enemySpawnTimer = 0;
var lastSpawnedPath = -1; // Track the last spawned path
var consecutiveSpawns = 0; // Track consecutive spawns from same path
// Cooldown system variables
var pathLastSpawnTime = [-1, -1, -1, -1, -1]; // Track last spawn time for each path
var pathConsecutiveSpawns = [0, 0, 0, 0, 0]; // Track consecutive spawns per path
var pathCooldownDuration = 300; // 5 seconds at 60fps
// Game input handling
game.down = function (x, y, obj) {
// Check if a path was tapped for directional attack
var pathTapped = false;
for (var p = 0; p < paths.length; p++) {
var path = paths[p];
// Convert tap position to path's local coordinates
var localPos = path.toLocal({
x: x,
y: y
});
// Check if tap is within path bounds
if (Math.abs(localPos.x) < path.width / 2 && Math.abs(localPos.y) < path.height / 2) {
// Attack in this path's direction
wizard.attack(path.pathIndex);
pathTapped = true;
break;
}
}
// No default attack - player must tap a path to attack
};
// Main game update loop
game.update = function () {
// Sort children by z-index to ensure proper rendering order
game.children.sort(function (a, b) {
return (a.zIndex || 0) - (b.zIndex || 0);
});
// Pause game when tutorial is active
if (tutorial && tutorial.isActive) {
return;
}
// Only update game logic if game has started
if (!gameStarted) {
return;
}
// Change music to epic battle theme at 10 enemies killed
if (enemyKillCounter === 10) {
LK.playMusic('epicBattle', {
volume: 0.8,
fade: {
start: 0,
end: 0.8,
duration: 1500
}
});
}
// Change to mystical ambient music at 25 enemies killed
if (enemyKillCounter === 25) {
LK.playMusic('mysticalAmbient', {
volume: 0.6,
fade: {
start: 0,
end: 0.6,
duration: 2000
}
});
}
// Show upgrade menu after 12 enemies are killed
if (enemyKillCounter >= 12 && !upgradeMenuShown) {
upgradeMenuShown = true;
// Increase upgrade costs by 5 coins when menu is shown
if (!upgradeMenu.upgradeCost) {
upgradeMenu.upgradeCost = 5; // Initial cost
}
upgradeMenu.upgradeCost += 5; // Increase by 5 coins each time
// Update all upgrade text to show new costs
upgradeMenu.updateUpgradeCosts();
upgradeMenu.visible = true;
upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown
gameStarted = false; // Pause game while menu is open
// Keep music playing during upgrade menu
// Clear all enemies on screen when upgrade menu is shown
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemies.splice(i, 1);
enemy.destroy();
}
// Clear all ogres on screen when upgrade menu is shown
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
ogres.splice(i, 1);
ogre.destroy();
}
// Clear all knights on screen when upgrade menu is shown
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
knights.splice(i, 1);
knight.destroy();
}
}
// Show upgrade menu again after 35 enemies are killed
if (enemyKillCounter >= 35 && upgradeMenuShown && !secondUpgradeMenuShown) {
secondUpgradeMenuShown = true;
// Increase upgrade costs by 5 coins when menu is shown
if (!upgradeMenu.upgradeCost) {
upgradeMenu.upgradeCost = 5; // Initial cost
}
upgradeMenu.upgradeCost += 5; // Increase by 5 coins each time
// Update all upgrade text to show new costs
upgradeMenu.updateUpgradeCosts();
upgradeMenu.visible = true;
upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown
gameStarted = false; // Pause game while menu is open
// Keep music playing during upgrade menu
// Clear all enemies on screen when upgrade menu is shown
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemies.splice(i, 1);
enemy.destroy();
}
// Clear all ogres on screen when upgrade menu is shown
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
ogres.splice(i, 1);
ogre.destroy();
}
// Clear all knights on screen when upgrade menu is shown
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
knights.splice(i, 1);
knight.destroy();
}
}
// Show third upgrade menu after 50 enemies are killed
if (enemyKillCounter >= 50 && secondUpgradeMenuShown && !thirdUpgradeMenuShown) {
thirdUpgradeMenuShown = true;
// Increase upgrade costs by 5 coins when menu is shown
if (!upgradeMenu.upgradeCost) {
upgradeMenu.upgradeCost = 5; // Initial cost
}
upgradeMenu.upgradeCost += 5; // Increase by 5 coins each time
// Update all upgrade text to show new costs
upgradeMenu.updateUpgradeCosts();
upgradeMenu.visible = true;
upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown
gameStarted = false; // Pause game while menu is open
// Keep music playing during upgrade menu
// Clear all enemies on screen when upgrade menu is shown
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemies.splice(i, 1);
enemy.destroy();
}
// Clear all ogres on screen when upgrade menu is shown
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
ogres.splice(i, 1);
ogre.destroy();
}
// Clear all knights on screen when upgrade menu is shown
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
knights.splice(i, 1);
knight.destroy();
}
}
// Show fourth upgrade menu after 70 enemies are killed
if (enemyKillCounter >= 70 && thirdUpgradeMenuShown && !fourthUpgradeMenuShown) {
fourthUpgradeMenuShown = true;
// Increase upgrade costs by 5 coins when menu is shown
if (!upgradeMenu.upgradeCost) {
upgradeMenu.upgradeCost = 5; // Initial cost
}
upgradeMenu.upgradeCost += 5; // Increase by 5 coins each time
// Update all upgrade text to show new costs
upgradeMenu.updateUpgradeCosts();
upgradeMenu.visible = true;
upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown
gameStarted = false; // Pause game while menu is open
// Keep music playing during upgrade menu
// Clear all enemies on screen when upgrade menu is shown
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemies.splice(i, 1);
enemy.destroy();
}
// Clear all ogres on screen when upgrade menu is shown
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
ogres.splice(i, 1);
ogre.destroy();
}
// Clear all knights on screen when upgrade menu is shown
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
knights.splice(i, 1);
knight.destroy();
}
}
// Reset consecutive spawns for paths that have cooled down (runs every frame)
for (var pathIdx = 0; pathIdx < 5; pathIdx++) {
// Check if enough time has passed since last spawn (cooldown expired)
if (pathLastSpawnTime[pathIdx] !== -1 && LK.ticks - pathLastSpawnTime[pathIdx] > pathCooldownDuration) {
// Reset consecutive spawns for this path due to cooldown
pathConsecutiveSpawns[pathIdx] = 0;
}
}
// Get stored difficulty setting
var selectedDifficulty = storage.difficulty || 'NORMAL';
var difficultyLevel = Math.floor(enemyKillCounter / 10); // Every 10 kills increases difficulty
// Apply unique difficulty modifiers
var currentSpawnRate, enemyHealthMultiplier, enemySpeedMultiplier;
var specialMechanics = {};
if (selectedDifficulty === 'FACIL') {
// EASY: Slower enemies, less health, longer spawn intervals
currentSpawnRate = Math.max(60, 120 - difficultyLevel * 5); // Much slower spawning
enemyHealthMultiplier = 1; // No health scaling over time
enemySpeedMultiplier = 1 + difficultyLevel * 0.20; // 20% speed increase per level
specialMechanics.bonusCoins = true; // 50% more coins
specialMechanics.healingChance = 0.15; // 15% chance to heal 5 HP on enemy kill
} else if (selectedDifficulty === 'NORMAL') {
// NORMAL: Balanced progression
currentSpawnRate = Math.max(40, 90 - difficultyLevel * 6); // Standard spawning
enemyHealthMultiplier = 1; // No health scaling over time
enemySpeedMultiplier = 1 + difficultyLevel * 0.30; // 30% speed increase per level
specialMechanics.standardRewards = true;
} else if (selectedDifficulty === 'DIFICIL') {
// HARD: Faster enemies, more health, shorter spawn intervals, special enemy abilities
currentSpawnRate = Math.max(20, 60 - difficultyLevel * 4); // Much faster spawning
enemyHealthMultiplier = 1; // No health scaling over time
enemySpeedMultiplier = 1 + difficultyLevel * 0.50; // 50% speed increase per level
specialMechanics.eliteEnemies = true; // 20% chance for elite enemies with double stats
specialMechanics.aggressiveAI = true; // Enemies move more directly toward wizard
specialMechanics.reducedCoins = true; // 25% fewer coins
}
// Unified enemy spawning system
enemySpawnTimer++;
var totalEnemies = enemies.length + ogres.length + knights.length + miniBosses.length;
if (enemySpawnTimer >= currentSpawnRate && miniBosses.length === 0 && totalEnemies < 15) {
enemySpawnTimer = 0;
spawnEnemy('skeleton', selectedDifficulty, enemyHealthMultiplier, enemySpeedMultiplier);
if (Math.random() < 0.3) LK.getSound('enemyGrowl').play();
}
// Ogre spawning
var ogreInterval = selectedDifficulty === 'FACIL' ? 240 : selectedDifficulty === 'DIFICIL' ? 120 : 180;
if (LK.ticks >= 900 && LK.ticks % ogreInterval === 0 && miniBosses.length === 0) {
// 30% chance to spawn berserker ogre (red variant) instead of regular ogre
if (Math.random() < 0.30) {
spawnEnemy('berserkerOgre', selectedDifficulty, enemyHealthMultiplier, enemySpeedMultiplier);
} else {
spawnEnemy('ogre', selectedDifficulty, enemyHealthMultiplier, enemySpeedMultiplier);
}
}
// Knight spawning
var knightInterval = selectedDifficulty === 'FACIL' ? 420 : selectedDifficulty === 'DIFICIL' ? 240 : 300;
var knightThreshold = selectedDifficulty === 'FACIL' ? 40 : selectedDifficulty === 'DIFICIL' ? 20 : 30;
if (enemyKillCounter >= knightThreshold && LK.ticks % knightInterval === 0 && miniBosses.length === 0) {
spawnEnemy('knight', selectedDifficulty, enemyHealthMultiplier, enemySpeedMultiplier);
}
// Mini boss spawning
if (enemyKillCounter >= 80 && enemyKillCounter <= 85 && miniBosses.length === 0 && Math.random() < 0.02) {
spawnEnemy('miniboss', selectedDifficulty, enemyHealthMultiplier, enemySpeedMultiplier);
LK.effects.flashScreen(0x8B0000, 1000);
}
// Check enemy-knight collisions and cleanup off-screen enemies
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
// Remove enemies that went off-screen at bottom
if (enemy.y > 2732 + 100) {
enemies.splice(i, 1);
enemy.destroy();
continue;
}
// Initialize lastIntersecting if not set
if (enemy.lastIntersecting === undefined) {
enemy.lastIntersecting = false;
}
// Check for collision transition (only if enemy is not dying)
var currentIntersecting = wizard.intersects(enemy);
if (!enemy.lastIntersecting && currentIntersecting && !enemy.isDying) {
// Damage wizard when enemy touches for the first time
wizard.takeDamage(20);
// Remove enemy after dealing damage
enemies.splice(i, 1);
enemy.destroy();
continue;
}
// Update last intersecting state
enemy.lastIntersecting = currentIntersecting;
}
// Check ogre-wizard collisions and cleanup off-screen ogres
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
// Remove ogres that went off-screen at bottom
if (ogre.y > 2732 + 100) {
ogres.splice(i, 1);
ogre.destroy();
continue;
}
// Initialize lastIntersecting if not set
if (ogre.lastIntersecting === undefined) {
ogre.lastIntersecting = false;
}
// Check for collision transition (only if ogre is not dying)
var currentOgreIntersecting = wizard.intersects(ogre);
if (!ogre.lastIntersecting && currentOgreIntersecting && !ogre.isDying) {
// Damage wizard when ogre touches for the first time
wizard.takeDamage(30); // Ogres deal 30 damage
// Remove ogre after dealing damage
ogres.splice(i, 1);
ogre.destroy();
continue;
}
// Update last intersecting state
ogre.lastIntersecting = currentOgreIntersecting;
}
// Check knight-wizard collisions and cleanup off-screen knights
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
// Remove knights that went off-screen at bottom
if (knight.y > 2732 + 100) {
knights.splice(i, 1);
knight.destroy();
continue;
}
// Initialize lastIntersecting if not set
if (knight.lastIntersecting === undefined) {
knight.lastIntersecting = false;
}
// Check for collision transition (only if knight is not dying)
var currentKnightIntersecting = wizard.intersects(knight);
if (!knight.lastIntersecting && currentKnightIntersecting && !knight.isDying) {
// Damage wizard when knight touches for the first time
wizard.takeDamage(40); // Knights deal 40 damage
// Remove knight after dealing damage
knights.splice(i, 1);
knight.destroy();
continue;
}
// Update last intersecting state
knight.lastIntersecting = currentKnightIntersecting;
}
// Check mini boss-wizard collisions and cleanup off-screen mini bosses
for (var i = miniBosses.length - 1; i >= 0; i--) {
var miniBoss = miniBosses[i];
// Remove mini bosses that went off-screen at bottom
if (miniBoss.y > 2732 + 100) {
miniBosses.splice(i, 1);
miniBoss.destroy();
continue;
}
// Initialize lastIntersecting if not set
if (miniBoss.lastIntersecting === undefined) {
miniBoss.lastIntersecting = false;
}
// Check for collision transition (only if mini boss is not dying)
var currentMiniBossIntersecting = wizard.intersects(miniBoss);
if (!miniBoss.lastIntersecting && currentMiniBossIntersecting && !miniBoss.isDying) {
// Damage wizard when mini boss touches for the first time
wizard.takeDamage(75); // Mini boss deals massive damage
// Don't remove mini boss after dealing damage - it stays alive
}
// Update last intersecting state
miniBoss.lastIntersecting = currentMiniBossIntersecting;
}
// Check thorns spike collisions with all enemies continuously
var allSpikes = [];
for (var childIdx = 0; childIdx < game.children.length; childIdx++) {
var child = game.children[childIdx];
// Check if this child is a spike (has hitEnemies array and brown tint)
if (child.hitEnemies && child.tint === 0x8B4513) {
allSpikes.push(child);
}
}
for (var spikeIdx = 0; spikeIdx < allSpikes.length; spikeIdx++) {
var spike = allSpikes[spikeIdx];
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var enemyIdx = 0; enemyIdx < allEnemies.length; enemyIdx++) {
var enemy = allEnemies[enemyIdx];
// Only hit enemies that haven't been hit by this spike yet and are not dying
if (spike.intersects(enemy) && spike.hitEnemies.indexOf(enemy) === -1 && !enemy.isDying) {
var thornDamage = 100; // Always deal 100 damage
enemy.takeDamage(thornDamage);
LK.effects.flashObject(enemy, 0x8B4513, 300);
// Mark this enemy as hit by this spike
spike.hitEnemies.push(enemy);
}
}
}
// Make tap text pulse
var pulse = 1 + Math.sin(LK.ticks * 0.1) * 0.2;
tapText.scale.set(pulse, pulse);
};
// Remove tap text after 5 seconds
tween({}, {}, {
duration: 5000,
onFinish: function onFinish() {
if (tapText && tapText.parent) {
tapText.destroy();
}
}
});