User prompt
Please fix the bug: 'Uncaught ReferenceError: cardToggleButton is not defined' in or related to this line: 'cardToggleButton.visible = true;' Line Number: 1576
User prompt
puedes decirme por que esta fallando el paso 1?
User prompt
no se implemento el paso 1 de manera efectiva por que paso esto?
User prompt
soluciona el error que esta presentando cuando se inicia el juego
User prompt
sigue mostrando error podrias decirme por que sigue presentando el error?
User prompt
y como se puede solucionar este problema?
User prompt
haz el paso 1
User prompt
procede con la opcion C
User prompt
ahora haz la fase 2
User prompt
procede ahora con la fase 1
User prompt
ahora implementa el 1C
User prompt
Please fix the bug: 'createSpellUI is not defined' in or related to this line: 'createSpellUI();' Line Number: 4851
User prompt
Please fix the bug: 'createSpellUI is not defined' in or related to this line: 'createSpellUI();' Line Number: 4851
User prompt
procede con la implementacion del 1B
User prompt
ahora implementa el 1A.6
User prompt
si procede con la estrategia
User prompt
haz el paso 1A
User prompt
separa por pasos para poder mejorar el sistema de cartas de una manera mas facil y haz una lista para hacerla una por una
User prompt
dame pasos para arreglar el sistema de cartas
User prompt
porque todas la crtas siguen lanzando la fireball puedes decirme como arreglarlo?
User prompt
todas las cartas tiran la fireball pero como podemos arreglar esto?
User prompt
podrias decirme como se puede arreglar este problema?
User prompt
hazlo
User prompt
haz que cada hechizo tenga una ligica diferente heal cure al jugador 50 puntos y ligntning haga damage a 3 enemigos en cadena
User prompt
aplicala
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ 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 wizard if (wizard && self.intersects(wizard)) { self.collect(); } } }; self.collect = function () { LK.getSound('coinCollect').play(); LK.setScore(LK.getScore() + 5); coinCounter++; coinText.setText('Coins: ' + coinCounter); // Remove from coins array for (var i = coins.length - 1; i >= 0; i--) { if (coins[i] === self) { coins.splice(i, 1); break; } } // Destroy coin directly without object pool self.destroy(); }; return self; }); // Unified EnemyManager for streamlined enemy lifecycle management var EnemyManager = Container.expand(function () { var self = Container.call(this); // Use global arrays directly - no separate collections needed // Unified enemy configuration templates self.enemyTemplates = { skeleton: { baseHealth: 100, baseSpeed: 3, damage: 20, coinReward: 1 }, ogre: { baseHealth: 200, baseSpeed: 2.5, damage: 30, coinReward: 1 }, knight: { baseHealth: 300, baseSpeed: 2, damage: 40, coinReward: 1 }, miniBoss: { baseHealth: 3000, baseSpeed: 4, damage: 75, coinReward: 5 } }; // Streamlined enemy creation with specific enemy classes self.createEnemy = function (type, difficulty, level, pathOverride) { var template = self.enemyTemplates[type]; if (!template) return null; // Create specific enemy class based on type var enemy = null; if (type === 'skeleton') { enemy = new Skeleton(); } else if (type === 'ogre') { enemy = new Ogre(); } else if (type === 'knight') { enemy = new Knight(); } else if (type === 'miniBoss') { enemy = new MiniBoss(); } else { return null; } // Apply difficulty scaling enemy.speed *= 1 + level * 0.3; enemy.pathIndex = pathOverride || self.selectOptimalPath(type); self.positionEnemy(enemy, type); self.applyDifficultyModifications(enemy, type, difficulty); return enemy; }; // Unified enemy positioning system self.positionEnemy = function (enemy, type) { // Use direct spawn position values var spawnPositions = [{ x: 2048 / 2, y: -100 }, { x: 2048 + 50, y: -50 }, { x: -50, y: -50 }, { x: -100, y: 2732 / 2 + 400 }, { x: 2048 + 100, y: 2732 / 2 + 400 }]; var spawnPos = spawnPositions[enemy.pathIndex]; if (spawnPos) { enemy.x = Math.max(50, Math.min(1998, spawnPos.x)); enemy.y = type === 'miniBoss' ? -200 : Math.max(-200, Math.min(2732 + 100, spawnPos.y)); enemy.lastX = enemy.x; } }; // Streamlined difficulty modifications self.applyDifficultyModifications = function (enemy, type, difficulty) { if (type === 'miniBoss') { enemy.updateHealthBar(); LK.effects.flashScreen(0x8B0000, 1000); } else if (type === 'skeleton' && Math.random() < 0.3) { LK.getSound('enemyGrowl').play(); } // Elite enemy system for hard difficulty if (difficulty === 'DIFICIL' && enemyKillCounter >= 20 && Math.random() < 0.15) { self.createEliteEnemy(enemy, type); } }; // Unified elite enemy creation self.createEliteEnemy = function (enemy, type) { var eliteMultipliers = { health: 1.8, speed: 1.3, color: 0xFF6600 }; if (type === 'ogre') eliteMultipliers.color = 0xFF0000; if (type === 'knight') eliteMultipliers.color = 0xFFD700; if (type === 'miniBoss') eliteMultipliers.color = 0x8B0000; enemy.health *= eliteMultipliers.health; enemy.speed *= eliteMultipliers.speed; enemy.maxHealth = enemy.health; enemy.isElite = true; for (var i = 0; i < enemy.animationFrames.length; i++) { enemy.animationFrames[i].tint = eliteMultipliers.color; } }; // Optimized path selection system self.selectOptimalPath = function (type) { if (type === 'skeleton' && enemyKillCounter < 5) return 0; if (type === 'miniBoss') return 0; var available = []; for (var i = 0; i < 5; i++) { if (pathConsecutiveSpawns[i] < 2) available.push(i); } if (available.length === 0) { for (var i = 0; i < 5; i++) pathConsecutiveSpawns[i] = 0; available = [0, 1, 2, 3, 4]; } return available[Math.floor(Math.random() * available.length)]; }; // Unified enemy collection management self.addToCollection = function (enemy, type) { if (type === 'skeleton') enemies.push(enemy);else if (type === 'ogre') ogres.push(enemy);else if (type === 'knight') knights.push(enemy);else if (type === 'miniBoss') miniBosses.push(enemy); }; // Streamlined enemy removal system self.removeFromCollection = function (enemy, type) { var collection = []; if (type === 'skeleton') collection = enemies;else if (type === 'ogre') collection = ogres;else if (type === 'knight') collection = knights;else if (type === 'miniBoss') collection = miniBosses; if (collection.length > 0) { for (var i = collection.length - 1; i >= 0; i--) { if (collection[i] === enemy) { collection.splice(i, 1); break; } } } }; // Get all enemies as unified array self.getAllEnemies = function () { var allEnemies = []; allEnemies = allEnemies.concat(enemies); allEnemies = allEnemies.concat(ogres); allEnemies = allEnemies.concat(knights); allEnemies = allEnemies.concat(miniBosses); return allEnemies; }; 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; } // Upgrade menu removed - no pause needed // 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 = collisionArrayPool.getAllEnemies(); 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 using unified factory var energyBeam = ProjectileFactory.createProjectile('energyBeam', self.x, self.y, closestEnemy.x, closestEnemy.y, { targetEnemy: closestEnemy }); // 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 GameMenu = Container.expand(function () { var self = Container.call(this); // Simple spell deck - load from storage or use defaults var currentDeck = storage.spellDeck || ['fireball', 'heal', 'lightning']; storage.spellDeck = currentDeck.slice(); // 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: 25.0, scaleY: 35.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 ENEMIES TO ATTACK\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: 1950, 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 = 1950; self.addChild(shopButtonText); // Deck button var deckButton = self.attachAsset('pathSelector', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2150, scaleX: 4, scaleY: 2 }); deckButton.tint = 0x8A2BE2; var deckButtonText = new Text2('DECK HECHIZOS', { size: 80, fill: 0xFFFFFF, font: "monospace" }); deckButtonText.anchor.set(0.5, 0.5); deckButtonText.x = 2048 / 2; deckButtonText.y = 2150; self.addChild(deckButtonText); // 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: 2350, 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 = 2350; 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 deck menu interactions if (self.deckMode) { // Check deck card clicks for spell casting (top area) or removal (bottom area) for (var i = 0; i < self.deckElements.length; i++) { var element = self.deckElements[i]; if (element.spellId && element.isDeckCard) { var cardX = element.x; var cardY = element.y; if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) { // Top area of card (above center) - cast spell if (y <= cardY) { // Try to cast the spell if (activeSpellDeck && activeSpellDeck.canCastSpell(element.spellId)) { var spell = activeSpellDeck.getSpell(element.spellId); if (spell) { var targetX = wizard.x; var targetY = wizard.y - 100; // Find nearest enemy for targeted spells if (spell.id === 'fireball' || spell.id === 'iceShard' || spell.id === 'lightning') { var allEnemies = collisionArrayPool.getAllEnemies(); var nearestEnemy = null; var nearestDistance = Infinity; for (var e = 0; e < allEnemies.length; e++) { var enemy = allEnemies[e]; var dx = enemy.x - wizard.x; var dy = enemy.y - wizard.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < nearestDistance) { nearestDistance = distance; nearestEnemy = enemy; } } if (nearestEnemy) { targetX = nearestEnemy.x; targetY = nearestEnemy.y; } } // Cast the spell activeSpellDeck.castSpell(element.spellId, targetX, targetY); LK.effects.flashObject(element, 0x00FF00, 200); // Show cast message var castText = new Text2('HECHIZO LANZADO!', { size: 60, fill: 0x00FF00, font: "monospace" }); castText.anchor.set(0.5, 0.5); castText.x = 2048 / 2; castText.y = 2200; self.addChild(castText); // Animate and remove message tween(castText, { alpha: 0, y: castText.y - 100 }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { if (castText.parent) castText.destroy(); } }); } } else { LK.effects.flashObject(element, 0xFF0000, 200); // Show "cannot cast" message var errorText = new Text2('NO SE PUEDE LANZAR', { size: 50, fill: 0xFF6666, font: "monospace" }); errorText.anchor.set(0.5, 0.5); errorText.x = 2048 / 2; errorText.y = 2200; self.addChild(errorText); // Animate and remove message tween(errorText, { alpha: 0, y: errorText.y - 100 }, { duration: 2000, easing: tween.easeOut, onFinish: function onFinish() { if (errorText.parent) errorText.destroy(); } }); } } else { // Bottom area of card (below center) - remove from deck // Visual feedback before removal LK.effects.flashObject(element, 0xFF0000, 300); // Remove from deck if (self.spellDeck.removeFromDeck(element.spellId)) { self.refreshDeckDisplay(); LK.effects.flashScreen(0xFF8800, 200); // Show removal message var removeText = new Text2('HECHIZO REMOVIDO', { size: 60, fill: 0xFF6666, font: "monospace" }); removeText.anchor.set(0.5, 0.5); removeText.x = 2048 / 2; removeText.y = 2200; self.addChild(removeText); // Animate and remove message tween(removeText, { alpha: 0, y: removeText.y - 100 }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { if (removeText.parent) removeText.destroy(); } }); } else { LK.effects.flashScreen(0xFF0000, 200); } } return; } } } // Check available card clicks with better hit detection for (var i = 0; i < self.availableElements.length; i++) { var element = self.availableElements[i]; if (element.spellId && !element.isDeckCard) { var cardX = element.x; var cardY = element.y; if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) { // Visual feedback before addition LK.effects.flashObject(element, 0x00FF00, 300); // Add to deck if (self.spellDeck.addToDeck(element.spellId)) { self.refreshDeckDisplay(); LK.effects.flashScreen(0x00FF00, 200); // Show addition message var addText = new Text2('HECHIZO AÑADIDO', { size: 60, fill: 0x66FF66, font: "monospace" }); addText.anchor.set(0.5, 0.5); addText.x = 2048 / 2; addText.y = 2200; self.addChild(addText); // Animate and remove message tween(addText, { alpha: 0, y: addText.y - 100 }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { if (addText.parent) addText.destroy(); } }); } else { LK.effects.flashScreen(0xFF0000, 200); // Show error message var errorText = new Text2('DECK LLENO (MAX 5)', { size: 50, fill: 0xFF6666, font: "monospace" }); errorText.anchor.set(0.5, 0.5); errorText.x = 2048 / 2; errorText.y = 2200; self.addChild(errorText); // Animate and remove message tween(errorText, { alpha: 0, y: errorText.y - 100 }, { duration: 2000, easing: tween.easeOut, onFinish: function onFinish() { if (errorText.parent) errorText.destroy(); } }); } return; } } } // Deck back button if (y >= 2350 && y <= 2650) { self.hideDeck(); } // Block all other interactions when deck 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 >= 1850 && y <= 2050 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) { // Show shop menu self.showShop(); } else if (y >= 2050 && y <= 2250 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) { // Show deck menu self.showDeck(); } else if (storage.tutorialCompleted && y >= 2250 && y <= 2450 && 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 () { if (!self.configOverlay) { self.configOverlay = self.createMenuOverlay(0x000000); self.configTitle = self.createMenuText('CONFIGURACION', 2048 / 2, 800, 120, 0xFFD700); self.musicVolumeText = self.createMenuText('VOLUMEN MUSICA: ' + Math.round((storage.musicVolume || 0.7) * 100) + '%', 2048 / 2, 1200, 80, 0xFFFFFF); self.soundVolumeText = self.createMenuText('VOLUMEN SONIDO: ' + Math.round((storage.soundVolume || 1.0) * 100) + '%', 2048 / 2, 1400, 80, 0xFFFFFF); self.difficultyText = self.createMenuText('DIFICULTAD: ' + (storage.difficulty || 'NORMAL'), 2048 / 2, 1600, 80, 0xFFFFFF); self.backButton = self.createMenuButton('coin', 2048 / 2, 2000, 0x00FF00); self.backText = self.createMenuText('VOLVER', 2048 / 2, 2000, 80, 0xFFFFFF); } 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 () { if (!self.shopOverlay) { self.shopOverlay = self.createMenuOverlay(0x000033); self.shopTitle = self.createMenuText('TIENDA', 2048 / 2, 800, 120, 0xFFD700); var shopItems = self.getShopItemsData(); self.initializeShopArrays(); self.createShopItems(shopItems); self.shopBackButton = self.createMenuButton('coin', 2048 / 2, 2000, 0x00FF00); self.shopBackText = self.createMenuText('VOLVER', 2048 / 2, 2000, 80, 0xFFFFFF); } 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.showDeck = function () { if (!self.deckOverlay) { // Ensure spellDeck exists before creating overlay if (!self.spellDeck) self.spellDeck = new SpellDeck(); self.deckOverlay = self.createMenuOverlay(0x1a0a2e); self.deckTitle = self.createMenuText('DECK DE HECHIZOS', 2048 / 2, 600, 100, 0xFFD700); self.initializeDeckArrays(); self.refreshDeckDisplay(); self.deckBackButton = self.createMenuButton('coin', 2048 / 2, 2500, 0x00FF00); self.deckBackText = self.createMenuText('VOLVER', 2048 / 2, 2500, 80, 0xFFFFFF); } self.deckOverlay.visible = true; self.deckMode = true; self.refreshDeckDisplay(); }; self.initializeDeckArrays = function () { if (!self.deckElements) self.deckElements = []; if (!self.availableElements) self.availableElements = []; }; self.refreshDeckDisplay = function () { if (!self.spellDeck) { self.spellDeck = new SpellDeck(); } // Clear existing deck elements for (var i = 0; i < self.deckElements.length; i++) { if (self.deckElements[i] && self.deckElements[i].parent) { self.deckElements[i].destroy(); } } self.deckElements = []; // Clear existing available elements for (var i = 0; i < self.availableElements.length; i++) { if (self.availableElements[i] && self.availableElements[i].parent) { self.availableElements[i].destroy(); } } self.availableElements = []; // Add helpful instructions at the top var instructionText = new Text2('TOCA CARTAS PARA AÑADIR/QUITAR DEL DECK', { size: 50, fill: 0x00FF00, font: "monospace" }); instructionText.anchor.set(0.5, 0.5); instructionText.x = 2048 / 2; instructionText.y = 700; self.addChild(instructionText); self.deckElements.push(instructionText); // Display current deck (top section) var deckLabel = new Text2('MI DECK ACTUAL (' + self.spellDeck.currentDeck.length + '/5):', { size: 70, fill: 0xFFD700, font: "monospace" }); deckLabel.anchor.set(0.5, 0.5); deckLabel.x = 2048 / 2; deckLabel.y = 800; self.addChild(deckLabel); self.deckElements.push(deckLabel); // Add deck instruction var deckInstruction = new Text2('TOCA PARA QUITAR', { size: 40, fill: 0xFF6666, font: "monospace" }); deckInstruction.anchor.set(0.5, 0.5); deckInstruction.x = 2048 / 2; deckInstruction.y = 850; self.addChild(deckInstruction); self.deckElements.push(deckInstruction); // Display deck cards with better spacing for (var i = 0; i < 5; i++) { var cardX = 200 + i * 350; var cardY = 1050; if (i < self.spellDeck.currentDeck.length) { var spell = self.spellDeck.getSpell(self.spellDeck.currentDeck[i]); if (spell) { // Card background with glow effect var cardBg = self.attachAsset('spellCard', { anchorX: 0.5, anchorY: 0.5, x: cardX, y: cardY, scaleX: 3.5, scaleY: 4.5 }); cardBg.tint = self.spellDeck.getRarityColor(spell.rarity); cardBg.spellId = spell.id; cardBg.isDeckCard = true; cardBg.alpha = 0.9; self.deckElements.push(cardBg); // Add border glow var glowBorder = self.attachAsset('spellCardBg', { anchorX: 0.5, anchorY: 0.5, x: cardX, y: cardY, scaleX: 4, scaleY: 5 }); glowBorder.tint = 0x00FF00; glowBorder.alpha = 0.3; self.deckElements.push(glowBorder); // Card name var cardName = new Text2(spell.name, { size: 35, fill: 0xFFFFFF, font: "monospace" }); cardName.anchor.set(0.5, 0.5); cardName.x = cardX; cardName.y = cardY - 60; self.addChild(cardName); self.deckElements.push(cardName); // Card description var description = spell.description || 'Hechizo magico'; var cardDesc = new Text2(description, { size: 25, fill: 0xCCCCCC, font: "monospace", wordWrap: true, wordWrapWidth: 250 }); cardDesc.anchor.set(0.5, 0.5); cardDesc.x = cardX; cardDesc.y = cardY + 20; self.addChild(cardDesc); self.deckElements.push(cardDesc); // Card stats var statsText = ''; if (spell.damage) statsText += 'Daño: ' + spell.damage + '\n'; if (spell.healing) statsText += 'Cura: ' + spell.healing + '\n'; if (spell.manaCost) statsText += 'Mana: ' + spell.manaCost; if (statsText) { var cardStats = new Text2(statsText, { size: 20, fill: 0xFFD700, font: "monospace" }); cardStats.anchor.set(0.5, 0.5); cardStats.x = cardX; cardStats.y = cardY + 80; self.addChild(cardStats); self.deckElements.push(cardStats); } } } else { // Empty slot var emptySlot = self.attachAsset('spellCardBg', { anchorX: 0.5, anchorY: 0.5, x: cardX, y: cardY, scaleX: 3.5, scaleY: 4.5 }); emptySlot.tint = 0x444444; emptySlot.alpha = 0.5; self.deckElements.push(emptySlot); var emptyText = new Text2('VACIO', { size: 40, fill: 0x666666, font: "monospace" }); emptyText.anchor.set(0.5, 0.5); emptyText.x = cardX; emptyText.y = cardY; self.addChild(emptyText); self.deckElements.push(emptyText); } } // Display available spells (bottom section) var availableLabel = new Text2('HECHIZOS DISPONIBLES PARA AÑADIR:', { size: 60, fill: 0xFFD700, font: "monospace" }); availableLabel.anchor.set(0.5, 0.5); availableLabel.x = 2048 / 2; availableLabel.y = 1350; self.addChild(availableLabel); self.availableElements.push(availableLabel); // Add available instruction var availableInstruction = new Text2('TOCA PARA AÑADIR A TU DECK', { size: 40, fill: 0x66FF66, font: "monospace" }); availableInstruction.anchor.set(0.5, 0.5); availableInstruction.x = 2048 / 2; availableInstruction.y = 1400; self.addChild(availableInstruction); self.availableElements.push(availableInstruction); // Display available spells var availableSpells = []; for (var i = 0; i < self.spellDeck.availableSpells.length; i++) { var spell = self.spellDeck.availableSpells[i]; if (self.spellDeck.currentDeck.indexOf(spell.id) === -1) { availableSpells.push(spell); } } if (availableSpells.length === 0) { var noSpellsText = new Text2('NO HAY HECHIZOS DISPONIBLES\nDESBLOQUEA MAS JUGANDO', { size: 50, fill: 0x888888, font: "monospace" }); noSpellsText.anchor.set(0.5, 0.5); noSpellsText.x = 2048 / 2; noSpellsText.y = 1600; self.addChild(noSpellsText); self.availableElements.push(noSpellsText); } for (var i = 0; i < availableSpells.length && i < 8; i++) { var spell = availableSpells[i]; var cardX = 150 + i % 4 * 450; var cardY = 1550 + Math.floor(i / 4) * 400; // Card background with hover effect var cardBg = self.attachAsset('spellCard', { anchorX: 0.5, anchorY: 0.5, x: cardX, y: cardY, scaleX: 3.2, scaleY: 4.2 }); cardBg.tint = self.spellDeck.getRarityColor(spell.rarity); cardBg.spellId = spell.id; cardBg.isDeckCard = false; cardBg.alpha = 0.8; self.availableElements.push(cardBg); // Add selection glow var selectionGlow = self.attachAsset('spellCardBg', { anchorX: 0.5, anchorY: 0.5, x: cardX, y: cardY, scaleX: 3.7, scaleY: 4.7 }); selectionGlow.tint = 0x00FFFF; selectionGlow.alpha = 0.2; self.availableElements.push(selectionGlow); // Card name var cardName = new Text2(spell.name, { size: 32, fill: 0xFFFFFF, font: "monospace" }); cardName.anchor.set(0.5, 0.5); cardName.x = cardX; cardName.y = cardY - 60; self.addChild(cardName); self.availableElements.push(cardName); // Card description var cardDesc = new Text2(spell.description, { size: 22, fill: 0xCCCCCC, font: "monospace", wordWrap: true, wordWrapWidth: 280 }); cardDesc.anchor.set(0.5, 0.5); cardDesc.x = cardX; cardDesc.y = cardY + 10; self.addChild(cardDesc); self.availableElements.push(cardDesc); // Card stats var statsText = ''; if (spell.damage) statsText += 'Daño: ' + spell.damage + '\n'; if (spell.healing) statsText += 'Cura: ' + spell.healing + '\n'; if (spell.manaCost) statsText += 'Mana: ' + spell.manaCost; if (statsText) { var cardStats = new Text2(statsText, { size: 18, fill: 0xFFD700, font: "monospace" }); cardStats.anchor.set(0.5, 0.5); cardStats.x = cardX; cardStats.y = cardY + 70; self.addChild(cardStats); self.availableElements.push(cardStats); } // Rarity indicator var rarityText = new Text2(spell.rarity.toUpperCase(), { size: 20, fill: self.spellDeck.getRarityColor(spell.rarity), font: "monospace" }); rarityText.anchor.set(0.5, 0.5); rarityText.x = cardX; rarityText.y = cardY + 100; self.addChild(rarityText); self.availableElements.push(rarityText); } }; self.hideDeck = function () { if (self.deckOverlay) { self.deckOverlay.destroy(); self.deckOverlay = null; } // Remove deck title if (self.deckTitle) { self.deckTitle.destroy(); self.deckTitle = null; } // Remove deck back button elements if (self.deckBackButton) { self.deckBackButton.destroy(); self.deckBackButton = null; } if (self.deckBackText) { self.deckBackText.destroy(); self.deckBackText = null; } // Clear deck elements for (var i = 0; i < self.deckElements.length; i++) { if (self.deckElements[i] && self.deckElements[i].parent) { self.deckElements[i].destroy(); } } self.deckElements = []; // Clear available elements for (var i = 0; i < self.availableElements.length; i++) { if (self.availableElements[i] && self.availableElements[i].parent) { self.availableElements[i].destroy(); } } self.availableElements = []; // Remove all deck-related children for (var i = self.children.length - 1; i >= 0; i--) { var child = self.children[i]; if (child.setText && child.text && (child.text.includes('DECK') || child.text.includes('HECHIZOS') || child.text.includes('ACTUAL') || child.text.includes('DISPONIBLES'))) { child.destroy(); } } self.deckMode = false; self.visible = true; }; // Unified UI factory for all menu elements // Menu overlay creation function self.createMenuOverlay = function (color) { var overlay = self.attachAsset('startMenuBackground', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, scaleX: 25.0, scaleY: 35.0 }); overlay.alpha = 0.9; overlay.tint = color || 0x000000; overlay.interactive = true; overlay.zIndex = 1000; return overlay; }; // Menu text creation function self.createMenuText = function (text, x, y, size, color) { var textElement = new Text2(text, { size: size, fill: color, font: "monospace" }); textElement.anchor.set(0.5, 0.5); textElement.x = x; textElement.y = y; self.addChild(textElement); return textElement; }; // Menu button creation function self.createMenuButton = function (asset, x, y, color) { var button = self.attachAsset(asset, { anchorX: 0.5, anchorY: 0.5, x: x, y: y, scaleX: 2, scaleY: 2 }); button.tint = color; return button; }; self.createUIElement = function (type, config) { if (type === 'overlay') { var overlay = self.addChild(LK.getAsset('startMenuBackground', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1366, scaleX: 1.0, scaleY: 1.0 })); overlay.alpha = 1.0; overlay.tint = config.tint || 0x000000; return overlay; } else if (type === 'text') { var text = new Text2(config.text, { size: config.size, fill: config.fill, font: "monospace" }); text.anchor.set(0.5, 0.5); text.x = config.x; text.y = config.y; self.addChild(text); return text; } else if (type === 'button') { var button = self.attachAsset(config.asset, { anchorX: 0.5, anchorY: 0.5, x: config.x, y: config.y, scaleX: 2, scaleY: 2 }); button.tint = config.tint; return button; } }; self.getShopItemsData = function () { return [{ 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' }]; }; self.initializeShopArrays = function () { if (!self.shopIcons) self.shopIcons = []; if (!self.shopTexts) self.shopTexts = []; if (!self.shopBuyButtons) self.shopBuyButtons = []; if (!self.shopBuyTexts) self.shopBuyTexts = []; }; self.createShopItems = function (shopItems) { for (var i = 0; i < shopItems.length; i++) { var item = shopItems[i]; var yPos = 1100 + i * 200; 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); 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); 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); } }; 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; } // UNIFIED DECK SYNCHRONIZATION: Fix all deck system inconsistencies // Create unified deck reference that all systems will use var unifiedDeck = []; // Priority 1: Use spellDeck from menu if it exists and has content if (self.spellDeck && self.spellDeck.currentDeck && self.spellDeck.currentDeck.length > 0) { unifiedDeck = self.spellDeck.currentDeck.slice(); } // Priority 2: Use storage deck if available else if (storage.spellDeck && storage.spellDeck.length > 0) { unifiedDeck = storage.spellDeck.slice(); } // Priority 3: Use global currentDeck if available else if (currentDeck && currentDeck.length > 0) { unifiedDeck = currentDeck.slice(); } // Priority 4: Use activeSpellDeck if available else if (activeSpellDeck && activeSpellDeck.currentDeck && activeSpellDeck.currentDeck.length > 0) { unifiedDeck = activeSpellDeck.currentDeck.slice(); } // Priority 5: Default deck as last resort else { unifiedDeck = ['fireball', 'heal', 'lightning']; } // SYNCHRONIZE ALL DECK SYSTEMS with unified deck currentDeck = unifiedDeck.slice(); activeSpellDeck.currentDeck = unifiedDeck.slice(); storage.spellDeck = unifiedDeck.slice(); if (self.spellDeck) { self.spellDeck.currentDeck = unifiedDeck.slice(); } // Debug: Log deck synchronization with enhanced details console.log('=== DECK SYNCHRONIZATION ==='); console.log('Unified deck:', unifiedDeck); console.log('ActiveSpellDeck sync:', activeSpellDeck.currentDeck); console.log('Storage sync:', storage.spellDeck); console.log('CurrentDeck sync:', currentDeck); console.log('Available spells IDs:', availableSpells.map(function (s) { return s.id; })); console.log('Spell validation:'); for (var d = 0; d < unifiedDeck.length; d++) { var spellId = unifiedDeck[d]; var spell = _getSpell(spellId); console.log(' - ' + spellId + ':', spell ? spell.name : 'NOT FOUND'); } // 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; // Show spell UI if (manaBarBg) manaBarBg.visible = true; if (manaBar) manaBar.visible = true; if (manaText) manaText.visible = true; // SPELL SLOT INITIALIZATION AND ASSIGNMENT for (var i = 0; i < spellSlots.length; i++) { var slot = spellSlots[i]; // FORCE PROPER POSITIONING AND VISIBILITY slot.x = 400 + i * 400; // Spread across bottom of screen slot.y = 2400; // Position fully visible within game area slot.visible = true; slot.alpha = 1.0; // Ensure fully visible slot.slotIndex = i; // Ensure slot index is set // CRITICAL: Force interactivity AFTER positioning slot.interactive = true; slot.buttonMode = true; // Enable button mode for better touch handling // Clear existing children first to avoid duplication while (slot.children.length > 0) { slot.children[0].destroy(); } // CRITICAL: FORCE SPELL ID ASSIGNMENT FROM UNIFIED DECK // GARANTIZAR que cada slot recibe un spell ID específico SIEMPRE var spellToAssign = null; if (i < unifiedDeck.length && unifiedDeck[i]) { spellToAssign = unifiedDeck[i]; console.log('UNIFIED DECK ASSIGNED: Slot', i, 'spellId:', spellToAssign); } else { // FORCE DEFAULT SPELL ASSIGNMENT FOR EMPTY SLOTS var defaultSpells = ['fireball', 'heal', 'lightning']; if (i < defaultSpells.length) { spellToAssign = defaultSpells[i]; console.log('DEFAULT ASSIGNED: Slot', i, 'spellId:', spellToAssign); } } // CRITICAL: NEVER ALLOW NULL SPELL IDS - ALWAYS ASSIGN SOMETHING if (!spellToAssign) { var fallbackSpells = ['fireball', 'heal', 'lightning']; spellToAssign = fallbackSpells[i % fallbackSpells.length]; console.log('FALLBACK ASSIGNED: Slot', i, 'spellId:', spellToAssign); } // FORCE ASSIGNMENT REGARDLESS OF VALIDATION slot.spellId = spellToAssign; console.log('FINAL ASSIGNMENT: Slot', i, 'spellId:', slot.spellId); // VALIDATE SPELL EXISTS for visual setup only var spell = _getSpell(spellToAssign); if (spell) { // SUCCESSFUL SPELL ASSIGNMENT slot.tint = 0xFFFFFF; console.log('SUCCESS: Validated spell for slot', i, ':', spellToAssign, spell.name); // Create spell icon var spellIcon = LK.getAsset('spell', { anchorX: 0.5, anchorY: 0.5 }); spellIcon.tint = _getRarityColor(spell.rarity); slot.addChild(spellIcon); // Add spell name text var spellNameText = new Text2(spell.name, { size: 30, fill: 0xFFFFFF, font: "monospace" }); spellNameText.anchor.set(0.5, 0.5); spellNameText.y = 40; slot.addChild(spellNameText); } else { // SPELL NOT FOUND - FORCE FIREBALL AS LAST RESORT console.log('ERROR: Spell not found for ID:', spellToAssign, 'forcing fireball for slot', i); slot.spellId = 'fireball'; console.log('EMERGENCY FALLBACK: Slot', i, 'forced to fireball'); slot.alpha = 0.8; slot.tint = 0xFFAA00; // Orange tint for fallback spells } // FORCE BRING TO FRONT FOR PROPER EVENT HANDLING game.addChild(slot); // Re-add to ensure it's on top // ASSIGN PROPER EVENT HANDLER AFTER FULL INITIALIZATION slot.down = function (x, y, obj) { console.log('=== SPELL SLOT CLICKED (FORCE MODE) ==='); console.log('Slot index:', obj.slotIndex); console.log('Spell ID:', obj.spellId); // IMMEDIATE SUCCESS FEEDBACK - NO VALIDATIONS LK.effects.flashObject(obj, 0x00FF00, 500); LK.effects.flashScreen(0x00FF00, 200); // Get spell ID (use fireball as default) var effectiveSpellId = obj.spellId || 'fireball'; // EXECUTE SPELL EFFECTS DIRECTLY - NO CHECKS if (effectiveSpellId === 'fireball') { // Find closest enemy for fireball var closestEnemy = null; var closestDistance = Infinity; if (wizard && collisionArrayPool && collisionArrayPool.getAllEnemies) { var allEnemies = collisionArrayPool.getAllEnemies(); for (var e = 0; e < allEnemies.length; e++) { var enemy = allEnemies[e]; if (enemy.isDying) continue; var dx = enemy.x - wizard.x; var dy = enemy.y - wizard.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } } // Create fireball projectile toward closest enemy if (closestEnemy) { var fireball = ProjectileFactory.createProjectile('fireBall', wizard.x, wizard.y, closestEnemy.x, closestEnemy.y, { targetEnemy: closestEnemy, damage: 150 }); LK.getSound('fireWhoosh').play(); } // Always create visual effect even if no enemies var fireEffect = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: closestEnemy ? closestEnemy.x : wizard.x, y: closestEnemy ? closestEnemy.y : wizard.y - 100, scaleX: 2, scaleY: 2 })); fireEffect.tint = 0xFF4500; tween(fireEffect, { scaleX: 4, scaleY: 4, alpha: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { if (fireEffect.parent) fireEffect.destroy(); } }); } else if (effectiveSpellId === 'heal') { // Heal wizard immediately if (wizard) { var healAmount = 50; var oldHealth = wizard.health; wizard.health = Math.min(wizard.health + healAmount, wizard.maxHealth); var actualHealing = wizard.health - oldHealth; if (typeof updateHealthBar === 'function') updateHealthBar(); LK.effects.flashObject(wizard, 0x00FF00, 800); // Create healing visual effect var healEffect = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: wizard.x, y: wizard.y, scaleX: 3, scaleY: 3 })); healEffect.tint = 0x00FF00; tween(healEffect, { scaleX: 6, scaleY: 6, alpha: 0 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { if (healEffect.parent) healEffect.destroy(); } }); } } else if (effectiveSpellId === 'lightning') { // Lightning chain attack - instantaneous effect LK.effects.flashScreen(0x00FFFF, 300); LK.getSound('iceFreeze').play(); // Find up to 3 closest enemies for lightning chain if (wizard && collisionArrayPool && collisionArrayPool.getAllEnemies) { var allEnemies = collisionArrayPool.getAllEnemies(); var livingEnemies = []; for (var e = 0; e < allEnemies.length; e++) { if (!allEnemies[e].isDying) { livingEnemies.push(allEnemies[e]); } } var targetsHit = Math.min(3, livingEnemies.length); if (targetsHit > 0) { // Create instantaneous chain lightning effects var lightningTargets = livingEnemies.slice(0, targetsHit); // Deal damage to all targets immediately for (var i = 0; i < lightningTargets.length; i++) { var enemy = lightningTargets[i]; enemy.takeDamage(200); LK.effects.flashObject(enemy, 0x00FFFF, 400); // Create lightning impact effect on each enemy var lightningImpact = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: enemy.x, y: enemy.y, scaleX: 2.5, scaleY: 2.5 })); lightningImpact.tint = 0x00FFFF; lightningImpact.alpha = 0.9; tween(lightningImpact, { scaleX: 5, scaleY: 5, alpha: 0, rotation: Math.PI * 2 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { if (lightningImpact.parent) lightningImpact.destroy(); } }); } // Create chain lightning visual effects between wizard and enemies var previousPoint = { x: wizard.x, y: wizard.y }; for (var i = 0; i < lightningTargets.length; i++) { var currentEnemy = lightningTargets[i]; // Create lightning bolt between previous point and current enemy var dx = currentEnemy.x - previousPoint.x; var dy = currentEnemy.y - previousPoint.y; var distance = Math.sqrt(dx * dx + dy * dy); var lightningBolt = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: (previousPoint.x + currentEnemy.x) / 2, y: (previousPoint.y + currentEnemy.y) / 2, scaleX: 1.5, scaleY: distance / 50 })); lightningBolt.tint = 0x00FFFF; lightningBolt.alpha = 0.8; lightningBolt.rotation = Math.atan2(dy, dx) + Math.PI / 2; // Animate lightning bolt tween(lightningBolt, { alpha: 0, scaleX: 0.5 }, { duration: 400 + i * 100, easing: tween.easeOut, onFinish: function onFinish() { if (lightningBolt.parent) lightningBolt.destroy(); } }); // Update previous point for next chain segment previousPoint = { x: currentEnemy.x, y: currentEnemy.y }; } } } } // Always show success message var castMessage = new Text2('HECHIZO LANZADO!', { size: 50, fill: 0x00FF00, font: "monospace" }); castMessage.anchor.set(0.5, 0.5); castMessage.x = wizard ? wizard.x : 1024; castMessage.y = wizard ? wizard.y - 150 : 1000; game.addChild(castMessage); tween(castMessage, { y: castMessage.y - 100, alpha: 0 }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { if (castMessage.parent) castMessage.destroy(); } }); // Always play spell cast sound LK.getSound('spellCast').play(); console.log('FORCE MODE spell cast completed successfully!'); }; } // PASO 8: Inicializar sistema de maná unificado initializeManaSystem(); // Verificar que la inicialización fue exitosa console.log('=== POST-INITIALIZATION VERIFICATION ==='); console.log('currentMana:', currentMana); console.log('activeSpellDeck.currentMana:', activeSpellDeck ? activeSpellDeck.currentMana : 'undefined'); console.log('maxMana:', maxMana); // IMMEDIATE MANA BAR UPDATE WITH FORCED VALUES updateManaBar(); // DEBUG: Verify mana values after initialization console.log('=== MANA FORCE INITIALIZATION ==='); console.log('currentMana:', currentMana); console.log('activeSpellDeck.currentMana:', activeSpellDeck.currentMana); console.log('maxMana:', maxMana); console.log('Mana bar updated successfully'); // FORCE SPELL SLOT UPDATE with proper synchronization updateSpellSlots(); // ADDITIONAL MANA VERIFICATION - ENSURE MANA STAYS AT 100 tween({}, {}, { duration: 100, // Short delay to ensure all systems are initialized onFinish: function onFinish() { // FORCE MANA TO 100 AGAIN after all initialization currentMana = 100; activeSpellDeck.currentMana = 100; updateManaBar(); // Final debug verification console.log('=== FINAL MANA VERIFICATION ==='); console.log('Final currentMana:', currentMana); console.log('Final activeSpellDeck.currentMana:', activeSpellDeck.currentMana); console.log('Mana system successfully initialized to 100'); } }); // FINAL VALIDATION: Verify all spell slots have proper spell IDs console.log('=== FINAL SPELL SLOT VALIDATION ==='); for (var i = 0; i < spellSlots.length; i++) { var slot = spellSlots[i]; console.log('Slot', i, 'spellId:', slot.spellId, 'visible:', slot.visible, 'interactive:', slot.interactive); if (slot.spellId) { var spell = _getSpell(slot.spellId); console.log(' - Spell found:', !!spell, spell ? spell.name : 'null'); } } // Initialize spell slots with default spells if deck is empty if (activeSpellDeck.currentDeck.length === 0) { // Add default spells to deck activeSpellDeck.currentDeck = ['fireball', 'heal', 'lightning']; storage.spellDeck = activeSpellDeck.currentDeck.slice(); // Update spell slots with default spells for (var i = 0; i < Math.min(3, activeSpellDeck.currentDeck.length); i++) { var slot = spellSlots[i]; var spellId = activeSpellDeck.currentDeck[i]; var spell = activeSpellDeck.getSpell(spellId); if (spell) { slot.spellId = spellId; slot.alpha = 1.0; slot.tint = 0xFFFFFF; // Create spell icon if (slot.children.length === 0) { var spellIcon = LK.getAsset('spell', { anchorX: 0.5, anchorY: 0.5 }); slot.addChild(spellIcon); } // Update spell icon if (slot.children.length > 0) { var spellIcon = slot.children[0]; spellIcon.tint = activeSpellDeck.getRarityColor(spell.rarity); } } } } // 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; }); // Knight class - armored enemy with high health var Knight = Container.expand(function () { var self = Container.call(this); self.enemyType = 'knight'; self.currentFrame = 1; self.animationTimer = 0; self.animationState = 'walking'; self.frozen = false; self.frozenTimer = 0; self.isDying = false; self.lastX = 0; self.speedTweenStarted = false; self.health = 300; self.speed = 2; self.damage = 40; self.maxHealth = self.health; self.animationSpeed = 20; // Create animation frames self.animationFrames = []; for (var i = 1; i <= 4; i++) { var frameGraphics = self.attachAsset('knight' + i, { anchorX: 0.5, anchorY: 1.0, scaleX: 3.0, scaleY: 3.0 }); frameGraphics.visible = i === 1; self.animationFrames.push(frameGraphics); } // Optimized animation system self.updateAnimation = function () { self.animationTimer++; var frameSpeed = self.animationSpeed; if (self.animationState === 'attacking') frameSpeed = Math.floor(frameSpeed * 0.5);else if (self.animationState === 'dying') frameSpeed = Math.floor(frameSpeed * 1.3);else if (self.animationState === 'idle') frameSpeed = Math.floor(frameSpeed * 1.7); if (self.animationTimer >= frameSpeed) { self.animationTimer = 0; self.animationFrames[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.animationFrames[self.currentFrame - 1].visible = true; } }; self.update = function () { if (tutorial && tutorial.isActive || self.isDying) return; // Animation and speed progression self.updateAnimation(); if (!self.speedTweenStarted) { self.speedTweenStarted = true; tween(self, { speed: self.speed * 1.5 }, { duration: 10000, easing: tween.easeOut }); } // Handle frozen state if (self.frozen) { self.frozenTimer--; if (self.frozenTimer <= 0) self.frozen = false; return; } // Enhanced movement toward wizard if (wizard) { var dx = wizard.x - self.x; var dy = wizard.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { var speedMult = self.timeSlowed ? self.timeSlowAmount : 1.0; self.x += dx / distance * self.speed * speedMult; self.y += dy / distance * self.speed * speedMult; var flipScale = dx < 0 ? -3.0 : 3.0; for (var frameIdx = 0; frameIdx < self.animationFrames.length; frameIdx++) { self.animationFrames[frameIdx].scaleX = flipScale; } } else { self.y += self.speed; } } }; self.takeDamage = function (damage) { self.health -= damage; self.animationState = 'attacking'; // Create damage text var damageText = new Text2('-' + damage, { size: 120, fill: 0xFF4444, 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); var startY = damageText.y; tween(damageText, { y: startY - 120, alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { damageText.destroy(); } }); // Flash effect LK.effects.flashObject(self, 0xFF0000, 300); // Return to walking state after brief animation 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 (tutorial && tutorial.isActive || self.isDying) return; if (wizard && wizard.attackCooldown > 0) { LK.effects.flashObject(self, 0xFF0000, 200); return; } // Vibration feedback if (typeof LK.vibrate === 'function') { LK.vibrate([150]); } selectedEnemy = self; LK.effects.flashObject(self, 0xFFFF00, 500); if (wizard && projectiles.length < 10) { var projectile = ProjectileFactory.createBasicAttack(wizard, self); projectile.targetEnemy = self; projectiles.push(projectile); LK.getSound('spellCast').play(); wizard.attackCooldown = 30; } }; self.die = function () { if (globalDeathHandler) { globalDeathHandler.executeEnemyDeath(self, knights); } }; return self; }); // MiniBoss class - powerful boss enemy with health bar var MiniBoss = Container.expand(function () { var self = Container.call(this); self.enemyType = 'miniBoss'; self.currentFrame = 1; self.animationTimer = 0; self.animationState = 'walking'; self.frozen = false; self.frozenTimer = 0; self.isDying = false; self.lastX = 0; self.speedTweenStarted = false; self.health = 3000; self.speed = 4; self.damage = 75; self.maxHealth = self.health; self.animationSpeed = 12; // Create animation frames self.animationFrames = []; for (var i = 1; i <= 4; i++) { var frameGraphics = self.attachAsset('ogre' + i, { anchorX: 0.5, anchorY: 1.0, scaleX: 5.0, scaleY: 5.0 }); frameGraphics.visible = i === 1; frameGraphics.tint = 0x8B0000; self.animationFrames.push(frameGraphics); } // Create health bar for mini boss self.healthBarBg = game.addChild(LK.getAsset('miniBossHealthBarBg', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 300, scaleX: 8, scaleY: 2 })); self.healthBarFg = game.addChild(LK.getAsset('miniBossHealthBar', { anchorX: 0.0, anchorY: 0.5, x: 2048 / 2 - 200, y: 300, scaleX: 8, scaleY: 2 })); self.healthText = new Text2('Boss Health: ' + self.health + '/' + self.maxHealth, { size: 80, fill: 0xFFFFFF, font: "monospace" }); self.healthText.anchor.set(0.5, 0.5); self.healthText.x = 2048 / 2; self.healthText.y = 250; game.addChild(self.healthText); // Update health bar for mini boss self.updateHealthBar = function () { if (!self.healthBarFg) return; var healthPercent = self.health / self.maxHealth; self.healthBarFg.scaleX = healthPercent * 8; self.healthText.setText('Boss Health: ' + self.health + '/' + self.maxHealth); if (healthPercent > 0.6) { self.healthBarFg.tint = 0xff0000; } else if (healthPercent > 0.3) { self.healthBarFg.tint = 0xff4500; } else { self.healthBarFg.tint = 0x8B0000; } }; // Optimized animation system self.updateAnimation = function () { self.animationTimer++; var frameSpeed = self.animationSpeed; if (self.animationState === 'attacking') frameSpeed = Math.floor(frameSpeed * 0.5);else if (self.animationState === 'dying') frameSpeed = Math.floor(frameSpeed * 1.3);else if (self.animationState === 'idle') frameSpeed = Math.floor(frameSpeed * 1.7); if (self.animationTimer >= frameSpeed) { self.animationTimer = 0; self.animationFrames[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.animationFrames[self.currentFrame - 1].visible = true; } }; self.update = function () { if (tutorial && tutorial.isActive || self.isDying) return; // Animation and speed progression self.updateAnimation(); if (!self.speedTweenStarted) { self.speedTweenStarted = true; tween(self, { speed: self.speed * 1.5 }, { duration: 10000, easing: tween.easeOut }); } // Handle frozen state if (self.frozen) { self.frozenTimer--; if (self.frozenTimer <= 0) self.frozen = false; return; } // Enhanced movement toward wizard if (wizard) { var dx = wizard.x - self.x; var dy = wizard.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { var speedMult = self.timeSlowed ? self.timeSlowAmount : 1.0; self.x += dx / distance * self.speed * speedMult; self.y += dy / distance * self.speed * speedMult; var flipScale = dx < 0 ? -5.0 : 5.0; for (var frameIdx = 0; frameIdx < self.animationFrames.length; frameIdx++) { self.animationFrames[frameIdx].scaleX = flipScale; } } else { self.y += self.speed; } } }; self.takeDamage = function (damage) { self.health -= damage; self.animationState = 'attacking'; // Create damage text var damageText = new Text2('-' + damage, { size: 120, fill: 0xFF4444, 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); var startY = damageText.y; tween(damageText, { y: startY - 120, alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { damageText.destroy(); } }); // Flash effect LK.effects.flashObject(self, 0xFF0000, 300); // Return to walking state after brief animation tween({}, {}, { duration: 300, onFinish: function onFinish() { if (self.animationState === 'attacking') self.animationState = 'walking'; } }); self.updateHealthBar(); if (self.health <= 0) self.die(); }; self.down = function (x, y, obj) { if (tutorial && tutorial.isActive || self.isDying) return; if (wizard && wizard.attackCooldown > 0) { LK.effects.flashObject(self, 0xFF0000, 200); return; } // Vibration feedback if (typeof LK.vibrate === 'function') { LK.vibrate([300, 100, 300]); } selectedEnemy = self; LK.effects.flashObject(self, 0xFFFF00, 500); if (wizard && projectiles.length < 10) { var projectile = ProjectileFactory.createBasicAttack(wizard, self); projectile.targetEnemy = self; projectiles.push(projectile); LK.getSound('spellCast').play(); wizard.attackCooldown = 30; } }; self.die = function () { if (globalDeathHandler) { globalDeathHandler.executeEnemyDeath(self, miniBosses); } }; return self; }); // Ogre class - stronger enemy with more health var Ogre = Container.expand(function () { var self = Container.call(this); self.enemyType = 'ogre'; self.currentFrame = 1; self.animationTimer = 0; self.animationState = 'walking'; self.frozen = false; self.frozenTimer = 0; self.isDying = false; self.lastX = 0; self.speedTweenStarted = false; self.health = 200; self.speed = 2.5; self.damage = 30; self.maxHealth = self.health; self.animationSpeed = 18; // Create animation frames self.animationFrames = []; for (var i = 1; i <= 4; i++) { var frameGraphics = self.attachAsset('ogre' + i, { anchorX: 0.5, anchorY: 1.0, scaleX: 3.5, scaleY: 3.5 }); frameGraphics.visible = i === 1; self.animationFrames.push(frameGraphics); } // Optimized animation system self.updateAnimation = function () { self.animationTimer++; var frameSpeed = self.animationSpeed; if (self.animationState === 'attacking') frameSpeed = Math.floor(frameSpeed * 0.5);else if (self.animationState === 'dying') frameSpeed = Math.floor(frameSpeed * 1.3);else if (self.animationState === 'idle') frameSpeed = Math.floor(frameSpeed * 1.7); if (self.animationTimer >= frameSpeed) { self.animationTimer = 0; self.animationFrames[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.animationFrames[self.currentFrame - 1].visible = true; } }; self.update = function () { if (tutorial && tutorial.isActive || self.isDying) return; // Animation and speed progression self.updateAnimation(); if (!self.speedTweenStarted) { self.speedTweenStarted = true; tween(self, { speed: self.speed * 1.5 }, { duration: 10000, easing: tween.easeOut }); } // Handle frozen state if (self.frozen) { self.frozenTimer--; if (self.frozenTimer <= 0) self.frozen = false; return; } // Enhanced movement toward wizard if (wizard) { var dx = wizard.x - self.x; var dy = wizard.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { var speedMult = self.timeSlowed ? self.timeSlowAmount : 1.0; self.x += dx / distance * self.speed * speedMult; self.y += dy / distance * self.speed * speedMult; var flipScale = dx < 0 ? -3.5 : 3.5; for (var frameIdx = 0; frameIdx < self.animationFrames.length; frameIdx++) { self.animationFrames[frameIdx].scaleX = flipScale; } } else { self.y += self.speed; } } }; self.takeDamage = function (damage) { self.health -= damage; self.animationState = 'attacking'; // Create damage text var damageText = new Text2('-' + damage, { size: 120, fill: 0xFF4444, 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); var startY = damageText.y; tween(damageText, { y: startY - 120, alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { damageText.destroy(); } }); // Flash effect LK.effects.flashObject(self, 0xFF0000, 300); // Return to walking state after brief animation 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 (tutorial && tutorial.isActive || self.isDying) return; if (wizard && wizard.attackCooldown > 0) { LK.effects.flashObject(self, 0xFF0000, 200); return; } // Vibration feedback if (typeof LK.vibrate === 'function') { LK.vibrate([200]); } selectedEnemy = self; LK.effects.flashObject(self, 0xFFFF00, 500); if (wizard && projectiles.length < 10) { var projectile = ProjectileFactory.createBasicAttack(wizard, self); projectile.targetEnemy = self; projectiles.push(projectile); LK.getSound('spellCast').play(); wizard.attackCooldown = 30; } }; self.die = function () { if (globalDeathHandler) { globalDeathHandler.executeEnemyDeath(self, ogres); } }; return self; }); // Simple object creation - no pooling needed for this game scope // All enemy types now use the unified Enemy class with type parameter // Ogre, Knight, and MiniBoss classes removed - now handled by Enemy(type) constructor 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 // Category 3: Orb-enemy collisions (separate from projectile-enemy and enemy-wizard) self.processOrbEnemyCollisions = function () { var allEnemies = collisionArrayPool.getAllEnemies(); 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; } // 1.1 Distance Culling: Quick distance check before expensive collision detection var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); var maxOrbRange = 80; // Orb collision range var currentIntersecting = false; if (distance <= maxOrbRange) { // Only perform expensive intersection test if enemy is close enough currentIntersecting = self.intersects(enemy); } if (!self.lastIntersecting[i] && currentIntersecting) { // Deal damage to enemy on contact transition (first contact only) enemy.takeDamage(200); // 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; } }; self.update = function () { // Pause orb when tutorial is active if (tutorial && tutorial.isActive) { return; } // Upgrade menu removed - no pause needed // 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; // Category 3: Orb-enemy collisions (separate from projectile-enemy and enemy-wizard) self.processOrbEnemyCollisions(); }; return self; }); // Create global death handler instance var Projectile = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'projectile'; self.speed = 50; self.direction = { x: 0, y: 0 }; self.targetEnemy = null; self.damage = 100; self.hitEnemy = false; // Define local projectile configurations since GAME_CONFIG is not available yet var projectileConfigs = { projectile: { assetId: 'projectile', scale: 1.5, speed: 50, damage: 100, tint: 0x44aaff, glowTint: 0x44aaff, hasRotation: true }, energyBeam: { assetId: 'projectileGlow', scale: 1.0, speed: 60, damage: 100, tint: 0x00ffff, glowTint: 0x00ffff, hasRotation: true }, fireBall: { assetId: 'projectileGlow', scale: 1.5, speed: 40, damage: 150, tint: 0xFF4500, glowTint: 0xFF6600, hasRotation: true, hasFlicker: true } }; // Get configuration from local configs var config = projectileConfigs[self.type]; if (!config) { // Fallback to projectile config config = projectileConfigs.projectile; } self.speed = config.speed; self.damage = config.damage; // Create graphics based on type var assetId = config.assetId; self.graphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: config.scale, scaleY: config.scale }); self.graphics.tint = config.tint; // Add glow for basic projectiles if (self.type === 'projectile') { var glow = self.attachAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.0, scaleY: 2.0 }); glow.alpha = 0.3; glow.tint = config.glowTint; } self.update = function () { if (tutorial && tutorial.isActive) return; // Move projectile self.x += self.direction.x * self.speed; self.y += self.direction.y * self.speed; // Visual effects if (config.hasRotation) { var angle = Math.atan2(self.direction.y, self.direction.x); self.graphics.rotation = angle + Math.PI / 2; } if (config.hasFlicker) { var flicker = 1 + Math.sin(LK.ticks * 0.4) * 0.3; self.graphics.scaleX = config.scale * flicker; self.graphics.scaleY = config.scale * flicker; } // Remove if off screen if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) { // Remove from projectiles array before destroying ProjectileFactory.removeProjectile(self); return; } // Handle collisions self.checkCollisions(); }; self.checkCollisions = function () { if (self.hitEnemy) return; // Category 1: Area damage projectiles (fireball) if (self.type === 'fireBall') { self.processAreaDamageCollisions(); } else { // Category 2: Single target projectiles self.processSingleTargetCollisions(); } }; // Separate collision processing for area damage projectiles self.processAreaDamageCollisions = function () { var allEnemies = collisionArrayPool.getAllEnemies(); for (var i = 0; i < allEnemies.length; i++) { var enemy = allEnemies[i]; if (enemy === wizard || enemy.isDying) continue; // 1.1 Distance Culling: Quick distance check before expensive collision detection var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); var maxFireballRange = 120; // Fireball collision range including area effect // Only perform expensive intersection test if enemy is within range if (distance <= maxFireballRange) { if (self.intersects(enemy)) { enemy.takeDamage(self.damage); self.createExplosion(enemy); self.hitEnemy = true; // Remove from projectiles array before destroying ProjectileFactory.removeProjectile(self); return; } } } }; // Separate collision processing for single target projectiles self.processSingleTargetCollisions = function () { if (self.targetEnemy && self.targetEnemy.parent && !self.targetEnemy.isDying) { // 1.1 Distance Culling: Quick distance check before expensive collision detection var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); var maxProjectileRange = 100; // Standard projectile collision range // Only perform expensive intersection test if target enemy is within range if (distance <= maxProjectileRange) { if (self.intersects(self.targetEnemy)) { self.targetEnemy.takeDamage(self.damage); self.hitEnemy = true; // Remove from projectiles array before destroying ProjectileFactory.removeProjectile(self); return; } } } else { // Target is invalid, remove projectile self.hitEnemy = true; // Remove from projectiles array before destroying ProjectileFactory.removeProjectile(self); return; } }; self.createExplosion = function (enemy) { LK.effects.flashObject(enemy, 0xFF4500, 400); var explosion = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: enemy.x, y: enemy.y, scaleX: 2, scaleY: 2 })); explosion.tint = 0xFF6600; explosion.alpha = 0.8; tween(explosion, { scaleX: 5, scaleY: 5, alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { explosion.destroy(); } }); }; return self; }); // Simple effects helper for basic visual feedback // Skeleton class - basic enemy with simple properties var Skeleton = Container.expand(function () { var self = Container.call(this); self.enemyType = 'skeleton'; self.currentFrame = 1; self.animationTimer = 0; self.animationState = 'walking'; self.frozen = false; self.frozenTimer = 0; self.isDying = false; self.lastX = 0; self.speedTweenStarted = false; self.health = 100; self.speed = 3; self.damage = 20; self.maxHealth = self.health; self.animationSpeed = 15; // Create animation frames self.animationFrames = []; for (var i = 1; i <= 4; i++) { var frameGraphics = self.attachAsset('esqueleto' + i, { anchorX: 0.5, anchorY: 1.0, scaleX: 3.0, scaleY: 3.0 }); frameGraphics.visible = i === 1; self.animationFrames.push(frameGraphics); } // Optimized animation system self.updateAnimation = function () { self.animationTimer++; var frameSpeed = self.animationSpeed; if (self.animationState === 'attacking') frameSpeed = Math.floor(frameSpeed * 0.5);else if (self.animationState === 'dying') frameSpeed = Math.floor(frameSpeed * 1.3);else if (self.animationState === 'idle') frameSpeed = Math.floor(frameSpeed * 1.7); if (self.animationTimer >= frameSpeed) { self.animationTimer = 0; self.animationFrames[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.animationFrames[self.currentFrame - 1].visible = true; } }; self.update = function () { if (tutorial && tutorial.isActive || self.isDying) return; // Animation and speed progression self.updateAnimation(); if (!self.speedTweenStarted) { self.speedTweenStarted = true; tween(self, { speed: self.speed * 1.5 }, { duration: 10000, easing: tween.easeOut }); } // Handle frozen state if (self.frozen) { self.frozenTimer--; if (self.frozenTimer <= 0) self.frozen = false; return; } // Enhanced movement toward wizard if (wizard) { var dx = wizard.x - self.x; var dy = wizard.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { var speedMult = self.timeSlowed ? self.timeSlowAmount : 1.0; self.x += dx / distance * self.speed * speedMult; self.y += dy / distance * self.speed * speedMult; var flipScale = dx < 0 ? -3.0 : 3.0; for (var frameIdx = 0; frameIdx < self.animationFrames.length; frameIdx++) { self.animationFrames[frameIdx].scaleX = flipScale; } } else { self.y += self.speed; } } }; self.takeDamage = function (damage) { self.health -= damage; self.animationState = 'attacking'; // Create damage text var damageText = new Text2('-' + damage, { size: 120, fill: 0xFF4444, 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); var startY = damageText.y; tween(damageText, { y: startY - 120, alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { damageText.destroy(); } }); // Flash effect LK.effects.flashObject(self, 0xFF0000, 300); // Return to walking state after brief animation 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 (tutorial && tutorial.isActive || self.isDying) return; if (wizard && wizard.attackCooldown > 0) { LK.effects.flashObject(self, 0xFF0000, 200); return; } // Vibration feedback if (typeof LK.vibrate === 'function') { LK.vibrate([100]); } selectedEnemy = self; LK.effects.flashObject(self, 0xFFFF00, 500); if (wizard && projectiles.length < 10) { var projectile = ProjectileFactory.createBasicAttack(wizard, self); projectile.targetEnemy = self; projectiles.push(projectile); LK.getSound('spellCast').play(); wizard.attackCooldown = 30; } }; self.die = function () { if (globalDeathHandler) { globalDeathHandler.executeEnemyDeath(self, enemies); } }; return self; }); // Simple SpellDeck class to replace the complex system var SpellDeck = Container.expand(function () { var self = Container.call(this); // Use the existing simple spell system self.currentDeck = storage.spellDeck || ['fireball', 'heal', 'lightning']; self.availableSpells = availableSpells; // Reference to global availableSpells // Simple methods to match the interface expected by GameMenu self.addToDeck = function (spellId) { if (self.currentDeck.length >= 5) return false; if (self.currentDeck.indexOf(spellId) !== -1) return false; self.currentDeck.push(spellId); storage.spellDeck = self.currentDeck.slice(); return true; }; self.removeFromDeck = function (spellId) { var index = self.currentDeck.indexOf(spellId); if (index === -1) return false; self.currentDeck.splice(index, 1); storage.spellDeck = self.currentDeck.slice(); return true; }; self.getSpell = function (spellId) { return _getSpell(spellId); }; self.getRarityColor = function (rarity) { return _getRarityColor(rarity); }; self.unlockSpell = function (spellId) { // Simple unlock system - spells are always available return true; }; return self; }); // Simple spell system - no complex classes needed 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. Toca directamente sobre los enemigos para atacarlos con hechizos.', duration: 3000, showSkip: true, highlightElement: 'screen', waitForTap: true }, { id: 'enemies_approach', title: 'ENEMIGOS Y MEJORAS', description: 'Los esqueletos vienen por 5 caminos diferentes. Toca directamente sobre cada enemigo para atacarlo. Gana monedas y desbloquea mejoras cada 12 enemigos.', duration: 3000, spawnDemoEnemy: true, highlightElement: 'coinCounter' }, { id: 'tutorial_complete', title: 'TUTORIAL COMPLETADO!', description: 'Recuerda: toca directamente sobre los enemigos para atacarlos. Cuida tu salud y sobrevive el mayor tiempo posible. ¡Buena suerte!', duration: 3000, startGame: true, highlightElement: 'healthBar' }]; }; // 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 Skeleton()); 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.animationFrames.length; frameIdx++) { demoEnemy.animationFrames[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 // Add interaction handler for spell cards in deck view self.down = function (x, y, obj) { // Only handle interactions if deck is active or if this is tutorial if (self.deckMode) { // Check deck card clicks for spell casting (from current deck) for (var i = 0; i < self.deckElements.length; i++) { var element = self.deckElements[i]; if (element.spellId && element.isDeckCard) { var cardX = element.x; var cardY = element.y; if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) { // Cast the spell instead of removing it if (activeSpellDeck && activeSpellDeck.canCastSpell(element.spellId)) { var spell = activeSpellDeck.getSpell(element.spellId); if (spell) { var targetX = wizard.x; var targetY = wizard.y - 100; // Find nearest enemy for targeted spells if (spell.id === 'fireball' || spell.id === 'iceShard' || spell.id === 'lightning') { var allEnemies = collisionArrayPool.getAllEnemies(); var nearestEnemy = null; var nearestDistance = Infinity; for (var e = 0; e < allEnemies.length; e++) { var enemy = allEnemies[e]; var dx = enemy.x - wizard.x; var dy = enemy.y - wizard.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < nearestDistance) { nearestDistance = distance; nearestEnemy = enemy; } } if (nearestEnemy) { targetX = nearestEnemy.x; targetY = nearestEnemy.y; } } // Cast the spell activeSpellDeck.castSpell(element.spellId, targetX, targetY); LK.effects.flashObject(element, 0x00FF00, 200); // Show cast message var castText = new Text2('HECHIZO LANZADO!', { size: 60, fill: 0x00FF00, font: "monospace" }); castText.anchor.set(0.5, 0.5); castText.x = 2048 / 2; castText.y = 2200; self.addChild(castText); // Animate and remove message tween(castText, { alpha: 0, y: castText.y - 100 }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { if (castText.parent) castText.destroy(); } }); } } else { LK.effects.flashObject(element, 0xFF0000, 200); // Show "cannot cast" message var errorText = new Text2('NO SE PUEDE LANZAR', { size: 50, fill: 0xFF6666, font: "monospace" }); errorText.anchor.set(0.5, 0.5); errorText.x = 2048 / 2; errorText.y = 2200; self.addChild(errorText); // Animate and remove message tween(errorText, { alpha: 0, y: errorText.y - 100 }, { duration: 2000, easing: tween.easeOut, onFinish: function onFinish() { if (errorText.parent) errorText.destroy(); } }); } return; } } } // Continue with existing available cards handling... // Check available card clicks with better hit detection for (var i = 0; i < self.availableElements.length; i++) { var element = self.availableElements[i]; if (element.spellId && !element.isDeckCard) { var cardX = element.x; var cardY = element.y; if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) { // Visual feedback before addition LK.effects.flashObject(element, 0x00FF00, 300); // Add to deck if (self.spellDeck.addToDeck(element.spellId)) { self.refreshDeckDisplay(); LK.effects.flashScreen(0x00FF00, 200); // Show addition message var addText = new Text2('HECHIZO AÑADIDO', { size: 60, fill: 0x66FF66, font: "monospace" }); addText.anchor.set(0.5, 0.5); addText.x = 2048 / 2; addText.y = 2200; self.addChild(addText); // Animate and remove message tween(addText, { alpha: 0, y: addText.y - 100 }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { if (addText.parent) addText.destroy(); } }); } else { LK.effects.flashScreen(0xFF0000, 200); // Show error message var errorText = new Text2('DECK LLENO (MAX 5)', { size: 50, fill: 0xFF6666, font: "monospace" }); errorText.anchor.set(0.5, 0.5); errorText.x = 2048 / 2; errorText.y = 2200; self.addChild(errorText); // Animate and remove message tween(errorText, { alpha: 0, y: errorText.y - 100 }, { duration: 2000, easing: tween.easeOut, onFinish: function onFinish() { if (errorText.parent) errorText.destroy(); } }); } return; } } } // Deck back button if (y >= 2350 && y <= 2650) { self.hideDeck(); } // Block all other interactions when deck menu is active return; } // Tutorial handling (moved after deck handling) 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; }); // Impact effect function now handled by BaseDamageHandler // Unified death handler for all enemy types using enemy configuration var UnifiedDeathHandler = Container.expand(function () { var self = Container.call(this); // Execute enemy death with consolidated coin and reward logic self.executeEnemyDeath = function (enemy, enemyArray) { enemy.animationState = 'dying'; enemy.currentFrame = 3; LK.getSound('painSound').play(); enemy.isDying = true; // Use direct enemy configuration values based on type var deathRotation = Math.PI * 0.5; var numCoins = 1; // Set specific values based on enemy type if (enemy.enemyType === 'miniBoss') { deathRotation = Math.PI * 0.8; numCoins = 5; } else if (enemy.enemyType === 'ogre') { deathRotation = Math.PI * 0.6; numCoins = 1; } else if (enemy.enemyType === 'knight') { deathRotation = Math.PI * 0.7; numCoins = 1; } // Special cleanup for mini boss UI elements if (enemy.enemyType === 'miniBoss') { self.cleanupMiniBossUI(enemy); } // Execute unified death animation tween(enemy, { alpha: 0, scaleX: 0.3, scaleY: 0.3, rotation: deathRotation }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { self.handleDeathRewards(enemy, numCoins); self.updateProgression(enemy); self.cleanupEnemy(enemy, enemyArray); } }); }; // Clean up mini boss UI elements self.cleanupMiniBossUI = function (enemy) { if (enemy.healthBarBg && enemy.healthBarBg.parent) { enemy.healthBarBg.destroy(); } if (enemy.healthBarFg && enemy.healthBarFg.parent) { enemy.healthBarFg.destroy(); } if (enemy.healthText && enemy.healthText.parent) { enemy.healthText.destroy(); } }; // Handle coin drops and difficulty-based rewards using pooled coins self.handleDeathRewards = function (enemy, numCoins) { var selectedDifficulty = storage.difficulty || 'NORMAL'; for (var coinIdx = 0; coinIdx < numCoins; coinIdx++) { // Create coin directly without object pool var coin = new Coin(); coin.x = enemy.x + (enemy.enemyType === 'miniBoss' ? (Math.random() - 0.5) * 200 : 0); coin.y = enemy.y - 50 + (enemy.enemyType === 'miniBoss' ? (Math.random() - 0.5) * 100 : 0); coin.isAnimating = true; coin.bobOffset = Math.random() * Math.PI * 2; coin.initialY = 0; coin.visible = true; coin.alpha = 1.0; game.addChild(coin); coins.push(coin); var coinTargetX = 120 + coinText.width / 2; var coinTargetY = 90 + coinText.height / 2; tween(coin, { x: coinTargetX, y: coinTargetY, scaleX: 0.5, scaleY: 0.5 }, { duration: 1000 + (enemy.enemyType === 'miniBoss' ? coinIdx * 200 : 0), easing: tween.easeOut, onFinish: function onFinish() { self.processCoinReward(enemy, selectedDifficulty, coin); } }); } }; // Process coin rewards with difficulty modifiers self.processCoinReward = function (enemy, selectedDifficulty, coin) { var coinReward = enemy.enemyType === 'miniBoss' ? 10 : 1; if (selectedDifficulty === 'FACIL') { coinReward = Math.floor(coinReward * 1.5); } else if (selectedDifficulty === 'DIFICIL') { coinReward = Math.max(1, Math.floor(coinReward * 0.75)); } coinCounter += coinReward; coinText.setText('Coins: ' + coinCounter); // Easy difficulty healing bonus if (selectedDifficulty === 'FACIL' && Math.random() < 0.15) { wizard.health = Math.min(wizard.health + 5, wizard.maxHealth); updateHealthBar(); LK.effects.flashObject(wizard, 0x00FF00, 200); } // Remove coin from tracking array for (var i = coins.length - 1; i >= 0; i--) { if (coins[i] === coin) { coins.splice(i, 1); break; } } // Destroy coin directly without object pool coin.destroy(); }; // Update kill counter and experience progression self.updateProgression = function (enemy) { var killIncrement = enemy.enemyType === 'miniBoss' ? 10 : 1; enemyKillCounter += killIncrement; killCountText.setText('Puntuacion: ' + enemyKillCounter); wizard.gainExperience(enemy.enemyType === 'miniBoss' ? 250 : 25); if (selectedEnemy === enemy) { selectedEnemy = null; } }; // Clean up enemy from arrays and game self.cleanupEnemy = function (enemy, enemyArray) { // Remove from appropriate array for (var i = enemyArray.length - 1; i >= 0; i--) { if (enemyArray[i] === enemy) { enemyArray.splice(i, 1); break; } } // Also remove from global enemy manager collections globalEnemyManager.removeFromCollection(enemy, enemy.enemyType); // Remove from all legacy arrays to ensure proper cleanup var allArrays = [enemies, ogres, knights, miniBosses]; for (var arrayIdx = 0; arrayIdx < allArrays.length; arrayIdx++) { var array = allArrays[arrayIdx]; for (var i = array.length - 1; i >= 0; i--) { if (array[i] === enemy) { array.splice(i, 1); break; } } } enemy.destroy(); // Set score reward based on enemy type var scoreReward = 10; if (enemy.enemyType === 'miniBoss') { scoreReward = 100; } else if (enemy.enemyType === 'ogre') { scoreReward = 20; } else if (enemy.enemyType === 'knight') { scoreReward = 30; } LK.setScore(LK.getScore() + scoreReward); }; return self; }); // UpgradeMenu class removed - using spell deck system instead // Unified Projectile Factory using consolidated GAME_CONFIG.projectiles 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.shieldActive = false; // Track shield status // Upgrade system removed - simplified wizard properties // 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; } // Upgrade menu removed - no pause needed 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; } // Upgrade-based abilities removed - using spell deck system instead // Optimized animation system with performance awareness self.animationTimer++; var adjustedSpeed = self.animationSpeed; // Simple animation speed adjustment without complex performance management // Skip complex animation manager - use direct speed control if (self.animationTimer >= adjustedSpeed) { self.animationTimer = 0; // Calculate advanced frame transition configuration var transitionConfig = self.calculateFrameTransitionConfig(adjustedSpeed); var currentFrame = self.wizardFrames[self.currentFrame - 1]; var nextFrameIndex = self.currentFrame >= 4 ? 0 : self.currentFrame; var nextFrame = self.wizardFrames[nextFrameIndex]; // Advanced frame transition system with multiple tween effects if (transitionConfig.useAdvancedTransition) { self.performAdvancedFrameTransition(currentFrame, nextFrame, transitionConfig); } else if (transitionConfig.useBasicTransition) { self.performBasicFrameTransition(currentFrame, nextFrame, transitionConfig); } else { // Instant transition for low performance currentFrame.visible = false; nextFrame.visible = true; } // Move to next frame (walking state) self.currentFrame++; if (self.currentFrame > 4) { self.currentFrame = 1; } } }; 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 for wizard attack 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 teleport invulnerability is active if (self.teleportInvuln) { GameManager.createFlashEffect(self, 0x8000FF, 200); return; } // 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 GameManager.createFlashEffect(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 GameManager.createFlashEffect(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; } // Use unified damage handler for core damage logic self.health -= damage; GameManager.createFlashEffect(self, 0xFF0000, 200); 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 = collisionArrayPool.getAllEnemies(); for (var enemyIdx = allEnemies.length - 1; enemyIdx >= 0; enemyIdx--) { var enemy = allEnemies[enemyIdx]; // Create destruction effect for each enemy GameManager.createFlashEffect(enemy, 0xFFD700, 500); // Create golden explosion particles GameManager.createVisualEffect('explosion', enemy, { explosionColor: 0xFFD700, explosionScale: 4.0 }); // Kill ALL enemies instantly by calling die() method enemy.die(); } // Visual effects for revival LK.effects.flashScreen(0x00FF00, 1500); // Green flash for revival GameManager.createFlashEffect(self, 0xFFD700, 1000); // Golden flash on wizard // Create healing aura effect GameManager.createVisualEffect('explosion', self, { explosionColor: 0x00FF00, explosionScale: 8.0 }); // Play spell cast sound for revival LK.getSound('spellCast').play(); // Update health bar to show revival updateHealthBar(); } 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 }); } }); }; 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 = collisionArrayPool.getAllEnemies(); 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 = collisionArrayPool.getAllEnemies(); 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 = collisionArrayPool.getAllEnemies(); 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 // Find closest enemy to target var closestEnemy = null; var closestDistance = Infinity; var allEnemies = collisionArrayPool.getAllEnemies(); 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; } } // Create fire ball projectile using unified factory var targetX = closestEnemy ? closestEnemy.x : self.x; var targetY = closestEnemy ? closestEnemy.y : self.y - 100; var fireBall = ProjectileFactory.createProjectile('fireBall', self.x, self.y, targetX, targetY); LK.getSound('spellCast').play(); LK.getSound('fireWhoosh').play(); }; // Simple frame transition configuration without complex performance management self.calculateFrameTransitionConfig = function (adjustedSpeed) { var config = { useAdvancedTransition: false, useBasicTransition: true, duration: adjustedSpeed * 0.3, easing: tween.easeOut }; return config; }; // Perform advanced frame transition with sophisticated tween effects self.performAdvancedFrameTransition = function (currentFrame, nextFrame, config) { // Phase 1: Current frame fade and scale out tween(currentFrame, { alpha: 0, scaleX: currentFrame.scaleX * 0.95, scaleY: currentFrame.scaleY * 0.95 }, { duration: config.duration * 0.5, easing: tween.easeIn, onFinish: function onFinish() { currentFrame.visible = false; currentFrame.alpha = 1; currentFrame.scaleX = 2.5; // Reset scale currentFrame.scaleY = 2.5; } }); // Phase 2: Next frame scale and fade in nextFrame.visible = true; nextFrame.alpha = 0; nextFrame.scaleX = 2.5 * 1.05; // Start slightly larger nextFrame.scaleY = 2.5 * 1.05; tween(nextFrame, { alpha: 1, scaleX: 2.5, scaleY: 2.5 }, { duration: config.duration * 0.7, easing: tween.bounceOut, delay: config.duration * 0.3 // Start after current frame begins fading }); // Phase 3: Subtle magical sparkle effect for enhanced visual appeal self.createFrameTransitionSparkle(nextFrame, config); }; // Perform basic frame transition with smooth fade effect self.performBasicFrameTransition = function (currentFrame, nextFrame, config) { // Smooth fade out current frame tween(currentFrame, { alpha: 0 }, { duration: config.duration, easing: config.easing, onFinish: function onFinish() { currentFrame.visible = false; currentFrame.alpha = 1; } }); // Smooth fade in next frame nextFrame.visible = true; nextFrame.alpha = 0; tween(nextFrame, { alpha: 1 }, { duration: config.duration, easing: tween.easeIn }); }; // Create subtle sparkle effect for frame transitions self.createFrameTransitionSparkle = function (frame, config) { var sparkleCount = 3; for (var i = 0; i < sparkleCount; i++) { var sparkle = game.addChild(LK.getAsset('spell', { anchorX: 0.5, anchorY: 0.5, x: frame.x + (Math.random() - 0.5) * 40, y: frame.y + (Math.random() - 0.5) * 40, scaleX: 0.1, scaleY: 0.1 })); sparkle.tint = 0xFFD700; sparkle.alpha = 0.8; // Animate sparkle with delayed start var delay = i * 100; tween({}, {}, { duration: delay, onFinish: function (sparkleRef) { return function () { tween(sparkleRef, { scaleX: 0.5, scaleY: 0.5, alpha: 1.0, rotation: Math.PI * 2 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { tween(sparkleRef, { scaleX: 0.1, scaleY: 0.1, alpha: 0 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { if (sparkleRef.parent) { sparkleRef.destroy(); } } }); } }); }; }(sparkle) }); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 // Black background for pixel art }); /**** * Game Code ****/ // Simple spell system - no complex classes needed /**** * Constants & Configuration ****/ // Unified Game Manager for all simple operations function _typeof2(o) { "@babel/helpers - typeof"; return _typeof2 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof2(o); } var availableSpells = [{ id: 'fireball', name: 'FIREBALL', damage: 150, manaCost: 30, rarity: 'common', description: 'Lanza bola de fuego explosiva' }, { id: 'heal', name: 'HEAL', healing: 50, manaCost: 25, rarity: 'common', description: 'Restaura puntos de salud' }, { id: 'lightning', name: 'LIGHTNING', damage: 200, manaCost: 40, rarity: 'rare', description: 'Cadena de rayos entre enemigos' }]; // PASO 1: Función centralizada de inicialización de maná function initializeManaSystem() { console.log('=== MANA SYSTEM INITIALIZATION ==='); // CRÍTICO: Asegurar que maxMana se inicializa primero maxMana = 100; // CRÍTICO: Forzar currentMana a 100 y prevenir corrupción currentMana = 100; // CRÍTICO: Inicializar activeSpellDeck si no existe if (!activeSpellDeck) { activeSpellDeck = { currentMana: 100, maxMana: 100 }; } // CRÍTICO: Sincronizar activeSpellDeck con valores seguros if (activeSpellDeck) { activeSpellDeck.currentMana = 100; activeSpellDeck.maxMana = 100; } // CRÍTICO: Actualizar UI inmediatamente para mostrar valores correctos if (typeof updateManaBar === 'function') { updateManaBar(); } // CRÍTICO: Verificar que la inicialización fue exitosa console.log('=== MANA INITIALIZATION COMPLETE ==='); console.log('maxMana:', maxMana); console.log('currentMana:', currentMana); console.log('activeSpellDeck.currentMana:', activeSpellDeck ? activeSpellDeck.currentMana : 'undefined'); console.log('activeSpellDeck.maxMana:', activeSpellDeck ? activeSpellDeck.maxMana : 'undefined'); // CRÍTICO: Forzar una actualización final para asegurar sincronización if (typeof updateManaBar === 'function') { updateManaBar(); } } // PASO 2: Función de validación de maná mejorada function validateManaSystem() { var manaWasCorrupted = false; console.log('=== VALIDATING MANA SYSTEM ==='); console.log('currentMana (before):', currentMana, _typeof2(currentMana)); console.log('activeSpellDeck.currentMana (before):', activeSpellDeck ? activeSpellDeck.currentMana : 'undefined'); // CRÍTICO: Validar currentMana con detección de corrupción if (typeof currentMana !== 'number' || isNaN(currentMana) || currentMana < 0 || currentMana > 100) { console.log('⚠️ currentMana corrupted, resetting to 100'); currentMana = 100; manaWasCorrupted = true; } // CRÍTICO: Validar activeSpellDeck existe y tiene propiedades válidas if (!activeSpellDeck) { console.log('⚠️ activeSpellDeck missing, creating with full mana'); activeSpellDeck = { currentMana: 100, maxMana: 100 }; manaWasCorrupted = true; } // CRÍTICO: Validar activeSpellDeck.currentMana if (typeof activeSpellDeck.currentMana !== 'number' || isNaN(activeSpellDeck.currentMana) || activeSpellDeck.currentMana < 0 || activeSpellDeck.currentMana > 100) { console.log('⚠️ activeSpellDeck.currentMana corrupted, resetting to 100'); activeSpellDeck.currentMana = 100; activeSpellDeck.maxMana = 100; manaWasCorrupted = true; } // CRÍTICO: Sincronizar si hay diferencias entre las dos variables if (Math.abs(currentMana - activeSpellDeck.currentMana) > 1) { console.log('⚠️ Mana desync detected - currentMana:', currentMana, 'activeSpellDeck.currentMana:', activeSpellDeck.currentMana); // Usar el valor más alto pero válido var validMana = Math.max(currentMana, activeSpellDeck.currentMana); if (validMana < 0 || validMana > 100 || typeof validMana !== 'number' || isNaN(validMana)) { validMana = 100; } currentMana = validMana; activeSpellDeck.currentMana = validMana; manaWasCorrupted = true; console.log('✅ Mana synced to:', validMana); } // CRÍTICO: Actualizar UI si hubo corrupción if (manaWasCorrupted) { console.log('🔧 Mana corruption detected and fixed'); if (typeof updateManaBar === 'function') { updateManaBar(); } console.log('✅ Mana system repaired - currentMana:', currentMana, 'activeSpellDeck.currentMana:', activeSpellDeck.currentMana); } else { console.log('✅ Mana system healthy - no corruption detected'); } console.log('=== MANA VALIDATION COMPLETE ==='); return !manaWasCorrupted; } // Simple spell casting functions function _canCastSpell(spellId) { // Simplified validation - spells are always free to cast if (!spellId) { console.log('_canCastSpell: No spell ID provided'); return false; } var spell = _getSpell(spellId); if (!spell) { console.log('_canCastSpell: Spell not found for ID:', spellId); return false; } // All validation removed - spells are completely free console.log('_canCastSpell: Spell can be cast (no restrictions):', spellId); return true; } function _castSpell(spellId, targetX, targetY) { // Simplified spell casting - no mana validation if (!_canCastSpell(spellId)) { console.log('_castSpell: Cannot cast spell', spellId); return false; } var spell = _getSpell(spellId); if (!spell) { console.log('_castSpell: Spell not found during casting:', spellId); return false; } // Mana deduction removed - spells are now free console.log('=== CASTING SPELL (FREE) ==='); console.log('Spell:', spell.name); // No mana consumption spellCooldowns[spellId] = LK.ticks + 180; // 3 second cooldown console.log('_castSpell: Successfully cast', spell.name, '(no mana cost)'); // Spell effects if (spell.id === 'fireball') { // Find the closest enemy for fireball targeting var allEnemies = collisionArrayPool.getAllEnemies(); var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < allEnemies.length; i++) { var enemy = allEnemies[i]; // Skip dying enemies if (enemy.isDying) continue; var dx = enemy.x - wizard.x; var dy = enemy.y - wizard.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } if (closestEnemy) { // Create fireball projectile using unified factory var fireball = ProjectileFactory.createProjectile('fireBall', wizard.x, wizard.y, closestEnemy.x, closestEnemy.y, { targetEnemy: closestEnemy, damage: spell.damage }); LK.getSound('fireWhoosh').play(); // Show spell description showSpellDescription('FIREBALL', 'Daño: ' + spell.damage + ' al enemigo más cercano', 0xFF4500); } else { // No enemies found, show message showSpellDescription('FIREBALL', 'No hay enemigos cerca', 0xFF4500); } } else if (spell.id === 'heal') { var healthBefore = wizard.health; wizard.health = Math.min(wizard.health + spell.healing, wizard.maxHealth); var actualHealing = wizard.health - healthBefore; updateHealthBar(); // Enhanced healing effects LK.effects.flashScreen(0x00FF00, 200); LK.effects.flashObject(wizard, 0x00FF00, 800); // Create multiple healing aura effects for better visibility for (var h = 0; h < 3; h++) { var healingAura = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: wizard.x + (Math.random() - 0.5) * 100, y: wizard.y + (Math.random() - 0.5) * 100, scaleX: 2 + h, scaleY: 2 + h })); healingAura.tint = 0x00FF00; healingAura.alpha = 0.7; // Animate healing aura with delay tween(healingAura, { scaleX: 6 + h * 2, scaleY: 6 + h * 2, alpha: 0 }, { duration: 1200 + h * 200, delay: h * 100, easing: tween.easeOut, onFinish: function onFinish() { if (healingAura.parent) healingAura.destroy(); } }); } // Create floating heal text var healText = new Text2('+' + actualHealing + ' HP', { size: 100, fill: 0x00FF00, font: "monospace" }); healText.anchor.set(0.5, 0.5); healText.x = wizard.x; healText.y = wizard.y - 100; game.addChild(healText); // Animate floating heal text tween(healText, { y: healText.y - 200, alpha: 0, scaleX: 1.8, scaleY: 1.8 }, { duration: 2000, easing: tween.easeOut, onFinish: function onFinish() { if (healText.parent) healText.destroy(); } }); // Show spell description if (actualHealing > 0) { showSpellDescription('HEAL', 'Curado: ' + actualHealing + ' HP', 0x00FF00); } else { showSpellDescription('HEAL', 'Salud ya está completa', 0x00FF00); } } else if (spell.id === 'lightning') { // Lightning chain attack - instantaneous effect LK.effects.flashScreen(0x00FFFF, 300); LK.getSound('iceFreeze').play(); var allEnemies = collisionArrayPool.getAllEnemies(); // Filter out dying enemies and sort by distance from wizard var livingEnemies = []; for (var e = 0; e < allEnemies.length; e++) { var enemy = allEnemies[e]; if (!enemy.isDying && enemy.health > 0 && enemy.parent) { var dx = enemy.x - wizard.x; var dy = enemy.y - wizard.y; enemy.distanceFromWizard = Math.sqrt(dx * dx + dy * dy); livingEnemies.push(enemy); } } // Sort enemies by distance for logical chaining livingEnemies.sort(function (a, b) { return a.distanceFromWizard - b.distanceFromWizard; }); var targetsHit = Math.min(3, livingEnemies.length); console.log('Lightning targeting:', targetsHit, 'enemies out of', livingEnemies.length, 'total living enemies'); if (targetsHit > 0) { // SOLUCION FACIL: Aplicar todo el daño primero antes de que cualquier enemigo muera // Paso 1: Aplicar daño a todos los objetivos SIMULTANEAMENTE for (var i = 0; i < targetsHit; i++) { var enemy = livingEnemies[i]; console.log('Lightning hitting enemy', i + 1, 'with health:', enemy.health, 'damage:', spell.damage); // Aplicar daño directo a la salud sin ejecutar die() todavía if (enemy && enemy.parent && !enemy.isDying && typeof enemy.takeDamage === 'function') { enemy.health -= spell.damage; // Aplicar daño directo console.log('Enemy health after lightning:', enemy.health); } } // Paso 2: DESPUES procesar las muertes for (var i = 0; i < targetsHit; i++) { var enemy = livingEnemies[i]; if (enemy && enemy.parent && !enemy.isDying) { // Crear efectos visuales LK.effects.flashObject(enemy, 0x00FFFF, 400); // Verificar si el enemigo debe morir después de aplicar todo el daño if (enemy.health <= 0) { enemy.die(); // Ahora sí ejecutar die() después de aplicar todo el daño } // Create lightning impact effect on each enemy var lightningImpact = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: enemy.x, y: enemy.y, scaleX: 2.5, scaleY: 2.5 })); lightningImpact.tint = 0x00FFFF; lightningImpact.alpha = 0.9; // Animate lightning impact with delay based on chain position tween(lightningImpact, { scaleX: 5, scaleY: 5, alpha: 0, rotation: Math.PI * 2 }, { duration: 600, delay: i * 100, // Sequential appearance easing: tween.easeOut, onFinish: function onFinish() { if (lightningImpact.parent) lightningImpact.destroy(); } }); } } // Create chain lightning visual effects between targets var chainPoints = [{ x: wizard.x, y: wizard.y }]; for (var i = 0; i < targetsHit; i++) { chainPoints.push({ x: livingEnemies[i].x, y: livingEnemies[i].y }); } // Create lightning bolts between each chain point for (var i = 0; i < chainPoints.length - 1; i++) { var startPoint = chainPoints[i]; var endPoint = chainPoints[i + 1]; var dx = endPoint.x - startPoint.x; var dy = endPoint.y - startPoint.y; var distance = Math.sqrt(dx * dx + dy * dy); var lightningBolt = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: (startPoint.x + endPoint.x) / 2, y: (startPoint.y + endPoint.y) / 2, scaleX: 1.5, scaleY: distance / 50 })); lightningBolt.tint = 0x00FFFF; lightningBolt.alpha = 0.8; lightningBolt.rotation = Math.atan2(dy, dx) + Math.PI / 2; // Animate lightning bolt with sequential timing tween(lightningBolt, { alpha: 0, scaleX: 0.5 }, { duration: 500, delay: i * 100, // Sequential chain effect easing: tween.easeOut, onFinish: function onFinish() { if (lightningBolt.parent) lightningBolt.destroy(); } }); } // Show spell description showSpellDescription('LIGHTNING', 'Cadena de ' + targetsHit + ' rayos - Daño: ' + spell.damage, 0x00FFFF); } else { // No enemies to hit showSpellDescription('LIGHTNING', 'No hay enemigos para la cadena', 0x00FFFF); } } LK.getSound('spellCast').play(); return true; } function _getSpell(spellId) { for (var i = 0; i < availableSpells.length; i++) { if (availableSpells[i].id === spellId) { return availableSpells[i]; } } return null; } function _getRarityColor(rarity) { return rarity === 'rare' ? 0x0080FF : 0xFFFFFF; } var GameManager = { updateEntity: function updateEntity(entity) { if (entity && entity.update && typeof entity.update === 'function') { entity.update(); } }, createDamageText: function createDamageText(x, y, damage) { var damageText = new Text2('-' + damage, { size: 120, fill: 0xFF4444, font: "monospace" }); damageText.anchor.set(0.5, 0.5); damageText.x = x; damageText.y = y - 40; game.addChild(damageText); tween(damageText, { y: y - 120, alpha: 0 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { damageText.destroy(); } }); }, createFlashEffect: function createFlashEffect(target, color, duration) { LK.effects.flashObject(target, color, duration); }, createObject: function createObject(type, config) { if (type === 'coin') return new Coin(); if (type === 'enemy') return new Enemy(config); if (type === 'projectile') return new Projectile(config); return null; }, createVisualEffect: function createVisualEffect(type, target, config) { // Simple visual effect creation if (type === 'explosion') { var explosion = game.addChild(LK.getAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, x: target.x, y: target.y, scaleX: config.explosionScale || 2, scaleY: config.explosionScale || 2 })); explosion.tint = config.explosionColor || 0xFFFFFF; explosion.alpha = 0.8; tween(explosion, { scaleX: (config.explosionScale || 2) * 1.5, scaleY: (config.explosionScale || 2) * 1.5, alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { explosion.destroy(); } }); } } }; function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } // Usar valores directos en lugar de configuraciones // Usar valores directos en lugar de configuraciones complejas // Usar valores directos en lugar de configuraciones complejas // Usar valores directos en lugar de configuraciones complejas // Usar valores directos en lugar de configuraciones complejas /**** * Global State Management ****/ // Simplified global state - no gameState object needed var gameStarted = false; var selectedEnemy = null; var coinCounter = 0; var enemyKillCounter = 0; var pathLastSpawnTime = [-1, -1, -1, -1, -1]; var pathConsecutiveSpawns = [0, 0, 0, 0, 0]; var lastSpawnedPath = -1; /**** * Global Systems ****/ // Unified game management system var gameManager = GameManager; // Simple Collision Array Pool var collisionArrayPool = { allEnemies: [], clearArray: function clearArray(arrayName) { if (this[arrayName]) { this[arrayName].length = 0; } return this[arrayName]; }, getAllEnemies: function getAllEnemies() { var array = this.clearArray('allEnemies'); for (var i = 0; i < enemies.length; i++) { array.push(enemies[i]); } for (var i = 0; i < ogres.length; i++) { array.push(ogres[i]); } for (var i = 0; i < knights.length; i++) { array.push(knights[i]); } for (var i = 0; i < miniBosses.length; i++) { array.push(miniBosses[i]); } return array; } }; /**** * Unified Projectile Manager ****/ var ProjectileFactory = { // Simplified projectile creation - all types use same Projectile class createProjectile: function createProjectile(type, startX, startY, targetX, targetY, config) { var projectile = new Projectile(type); projectile.x = startX; projectile.y = startY; if (config) { for (var key in config) projectile[key] = config[key]; } if (targetX !== undefined && targetY !== undefined) { var dx = targetX - startX; var dy = targetY - startY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { projectile.direction.x = dx / distance; projectile.direction.y = dy / distance; } } game.addChild(projectile); projectiles.push(projectile); return projectile; }, createBasicAttack: function createBasicAttack(wizard, enemy) { return this.createProjectile('projectile', wizard.x, wizard.y, enemy.x, enemy.y, { targetEnemy: enemy, damage: 100 }); }, createSpellProjectile: function createSpellProjectile(spellType, wizard, targetX, targetY) { return this.createProjectile(spellType, wizard.x, wizard.y, targetX, targetY, { damage: 150 }); }, removeProjectile: function removeProjectile(projectile) { for (var i = projectiles.length - 1; i >= 0; i--) { if (projectiles[i] === projectile) { projectiles.splice(i, 1); break; } } if (projectile.parent) projectile.destroy(); } }; // Removed globalDamageHandler and globalEffectManager aliases - use GameManager directly /**** * Game Objects (Legacy compatibility) ****/ // Direct global arrays - no gameState needed var enemies = []; var ogres = []; var knights = []; var miniBosses = []; var coins = []; var projectiles = []; // Single game menu object var gameMenu; // 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()); // Simple spell system variables var currentDeck = storage.spellDeck || ['fireball', 'heal', 'lightning']; var maxMana = 100; var currentMana = 100; // Force to 100 to prevent corruption var manaRegenTimer = 0; var spellCooldowns = {}; storage.spellDeck = currentDeck.slice(); var spellSlots = []; var manaBar, manaBarBg, manaText; // Initialize activeSpellDeck with simple spell system var activeSpellDeck = { currentDeck: currentDeck.slice(), currentMana: maxMana, maxMana: maxMana, availableSpells: availableSpells, getSpell: function getSpell(spellId) { return _getSpell(spellId); }, canCastSpell: function canCastSpell(spellId) { return _canCastSpell(spellId); }, castSpell: function castSpell(spellId, targetX, targetY) { return _castSpell(spellId, targetX, targetY); }, getRarityColor: function getRarityColor(rarity) { return _getRarityColor(rarity); } }; // Create spell UI function createSpellUI() { // Mana bar manaBarBg = LK.getAsset('manaBarBg', { anchorX: 0, anchorY: 0, scaleX: 2, scaleY: 1 }); LK.gui.topRight.addChild(manaBarBg); manaBarBg.x = -300; manaBarBg.y = 20; manaBarBg.visible = false; manaBar = LK.getAsset('manaBar', { anchorX: 0, anchorY: 0, scaleX: 2, scaleY: 1 }); LK.gui.topRight.addChild(manaBar); manaBar.x = -300; manaBar.y = 20; manaBar.visible = false; manaText = new Text2('Mana: 100/100', { size: 60, fill: 0x4169E1, font: "monospace" }); manaText.anchor.set(0, 0); LK.gui.topRight.addChild(manaText); manaText.x = -300; manaText.y = 65; manaText.visible = false; // Create initial spell slots (they will be properly configured in startGame) for (var i = 0; i < 3; i++) { var slot = LK.getAsset('spellSlot', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2 }); game.addChild(slot); // Basic initialization (full setup happens in startGame) slot.x = 400 + i * 400; slot.y = 2400; slot.visible = false; // Initially hidden slot.slotIndex = i; slot.spellId = null; slot.interactive = false; // Will be enabled in startGame // Simple click handler that will be overridden in startGame slot.down = function (x, y, obj) { console.log('Spell slot clicked before proper initialization:', obj.slotIndex); }; spellSlots.push(slot); } } // Update mana bar function updateManaBar() { if (!manaBar || !manaText) return; // Always show full mana - spells are free var manaPercent = 1.0; // Always 100% manaBar.scaleX = manaPercent * 2; manaText.setText('Mana: 100/100'); } // Simplified spell slot cooldown updates function updateSpellSlots() { // Ensure we have activeSpellDeck reference if (!activeSpellDeck) return; // Use activeSpellDeck.currentDeck for sync var currentActiveDeck = activeSpellDeck.currentDeck || []; // Refresh spell slots to match current deck for (var i = 0; i < spellSlots.length; i++) { var slot = spellSlots[i]; // Update slot content based on current active deck if (i < currentActiveDeck.length) { var spellId = currentActiveDeck[i]; var spell = _getSpell(spellId); if (spell) { slot.spellId = spellId; slot.visible = true; slot.interactive = true; // Ensure slot is interactive // Update cooldown visual - no mana checks var cooldownEnd = spellCooldowns[spellId] || 0; var isOnCooldown = cooldownEnd > LK.ticks; // Mana shortage check removed - spells are always castable if (isOnCooldown) { slot.alpha = 0.5; slot.tint = 0x888888; } else { slot.alpha = 1.0; slot.tint = 0xFFFFFF; // Normal appearance } // Create or update spell icon if (slot.children.length === 0) { // Create new spell icon var spellIcon = LK.getAsset('spell', { anchorX: 0.5, anchorY: 0.5 }); slot.addChild(spellIcon); // Create spell name text var spellNameText = new Text2(spell.name, { size: 30, fill: 0xFFFFFF, font: "monospace" }); spellNameText.anchor.set(0.5, 0.5); spellNameText.y = 40; slot.addChild(spellNameText); } // Update existing spell icon if (slot.children.length > 0) { var spellIcon = slot.children[0]; spellIcon.tint = _getRarityColor(spell.rarity); } // Update spell name text if it exists if (slot.children.length > 1) { var spellNameText = slot.children[1]; // Ensure it's a Text2 object before calling setText if (spellNameText && typeof spellNameText.setText === 'function') { spellNameText.setText(spell.name); spellNameText.tint = _getRarityColor(spell.rarity); } } } else { slot.spellId = null; slot.visible = true; slot.alpha = 0.3; slot.tint = 0x444444; } } else { // Empty slot - keep visible but dimmed slot.spellId = null; slot.visible = true; slot.alpha = 0.3; slot.tint = 0x444444; } } } // Initialize spell UI createSpellUI(); // Initialize spell unlocking system var lastUnlockCheck = 0; function checkSpellUnlocks() { if (!gameMenu.spellDeck) { gameMenu.spellDeck = new SpellDeck(); } // Only check unlocks when kill counter changes if (enemyKillCounter === lastUnlockCheck) return; lastUnlockCheck = enemyKillCounter; // Unlock spells based on achievements with messages if (enemyKillCounter >= 10 && !storage.lightningUnlocked) { storage.lightningUnlocked = true; gameMenu.spellDeck.unlockSpell('lightning'); LK.effects.flashScreen(0x00FFFF, 500); showSpellUnlockMessage('LIGHTNING', 'Cadena de rayos entre enemigos'); } if (enemyKillCounter >= 25 && !storage.shieldUnlocked) { storage.shieldUnlocked = true; gameMenu.spellDeck.unlockSpell('shield'); LK.effects.flashScreen(0x0080FF, 500); showSpellUnlockMessage('MAGIC SHIELD', 'Inmunidad temporal al daño'); } if (enemyKillCounter >= 50 && !storage.teleportUnlocked) { storage.teleportUnlocked = true; gameMenu.spellDeck.unlockSpell('teleport'); LK.effects.flashScreen(0x8000FF, 500); showSpellUnlockMessage('TELEPORT', 'Mueve instantáneamente al mago'); } if (enemyKillCounter >= 75 && !storage.timeSlowUnlocked) { storage.timeSlowUnlocked = true; gameMenu.spellDeck.unlockSpell('timeSlow'); LK.effects.flashScreen(0xFF8000, 500); showSpellUnlockMessage('TIME SLOW', 'Ralentiza todos los enemigos'); } if (enemyKillCounter >= 100 && !storage.meteorUnlocked) { storage.meteorUnlocked = true; gameMenu.spellDeck.unlockSpell('meteor'); LK.effects.flashScreen(0xFF0000, 500); showSpellUnlockMessage('METEOR', 'Daño masivo en área'); } } function showSpellUnlockMessage(spellName, description) { var unlockText = new Text2('NUEVO HECHIZO DESBLOQUEADO!\n' + spellName + '\n' + description, { size: 60, fill: 0xFFD700, font: "monospace" }); unlockText.anchor.set(0.5, 0.5); unlockText.x = 2048 / 2; unlockText.y = 2732 / 2; game.addChild(unlockText); // Animate unlock message tween(unlockText, { scaleX: 1.2, scaleY: 1.2, alpha: 0.8 }, { duration: 2000, easing: tween.easeOut, onFinish: function onFinish() { tween(unlockText, { alpha: 0, y: unlockText.y - 200 }, { duration: 1000, easing: tween.easeIn, onFinish: function onFinish() { if (unlockText.parent) unlockText.destroy(); } }); } }); } // Function to show spell description when cast function showSpellDescription(spellName, description, color) { var descText = new Text2(spellName + '\n' + description, { size: 50, fill: color, font: "monospace" }); descText.anchor.set(0.5, 0.5); descText.x = wizard.x; descText.y = wizard.y - 200; game.addChild(descText); // Animate description tween(descText, { y: descText.y - 80, alpha: 0 }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { if (descText.parent) descText.destroy(); } }); } // Upgrade system removed - using spell deck system instead // Create unified path system - all 5 paths at once var paths = []; var knightX = 2048 / 2; var knightY = 2732 - 250; var pathAngles = [-Math.PI / 2, -Math.PI / 3, -2 * Math.PI / 3, Math.PI / 6, 5 * Math.PI / 6]; var wizardX = knightX; var wizardY = 2732 - 600; // Create knight reference for backward compatibility var knight = wizard; // Unified path creation function function createUnifiedPaths() { var spawnPositions = [{ x: 2048 / 2, y: -100 }, { x: 2048 + 50, y: -50 }, { x: -50, y: -50 }, { x: -100, y: 2732 / 2 + 400 }, { x: 2048 + 100, y: 2732 / 2 + 400 }]; var pathAngles = [-Math.PI / 2, -Math.PI / 3, -2 * Math.PI / 3, Math.PI / 6, 5 * Math.PI / 6]; for (var p = 0; p < 5; p++) { var angle = pathAngles[p]; var spawnPos = spawnPositions[p]; var actualPathLength = Math.sqrt((spawnPos.x - wizardX) * (spawnPos.x - wizardX) + (spawnPos.y - wizardY) * (spawnPos.y - wizardY)); // Create stone segments var segmentSize = 80; var numSegments = Math.floor(actualPathLength / segmentSize); for (var s = 0; s < numSegments; s++) { var segmentDistance = s * segmentSize + segmentSize / 2; var segmentX = spawnPos.x - Math.cos(angle) * segmentDistance; var segmentY = spawnPos.y - Math.sin(angle) * segmentDistance; if (segmentX >= -100 && segmentX <= 2148 && segmentY >= -100 && segmentY <= 2832) { 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 })); stoneSegment.alpha = 0; stoneSegment.visible = false; stoneSegment.pathIndex = p; } } // Create collision area var centerX = (spawnPos.x + wizardX) / 2; var centerY = (spawnPos.y + wizardY) / 2; 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 })); path.alpha = 0; path.visible = false; path.pathIndex = p; // Add path number var pathNumber = new Text2((p + 1).toString(), { size: 120, fill: 0xFFD700, font: "monospace" }); pathNumber.anchor.set(0.5, 0.5); pathNumber.x = spawnPos.x; pathNumber.y = spawnPos.y - 80; pathNumber.visible = false; pathNumber.pathIndex = p; game.addChild(pathNumber); // Add touch handler path.down = function (x, y, obj) { wizard.attack(obj.pathIndex); }; paths.push(path); } } // Create all paths createUnifiedPaths(); // 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 early to ensure it's available for other classes 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: 0xFFFFFF, font: "monospace" }); killCountText.anchor.set(0, 0); LK.gui.topLeft.addChild(killCountText); killCountText.x = 120; killCountText.y = 50; killCountText.visible = false; var tapText = new Text2('TAP ENEMIES TO ATTACK!', { size: 80, fill: 0x00FF00, font: "monospace" }); tapText.anchor.set(0.5, 0.5); LK.gui.center.addChild(tapText); tapText.y = -400; tapText.visible = false; // Health bar UI var healthBarBg = LK.getAsset('healthBarBg', { anchorX: 0, anchorY: 0, scaleX: 2, scaleY: 1 }); LK.gui.topLeft.addChild(healthBarBg); healthBarBg.x = 120; healthBarBg.y = 20; healthBarBg.visible = false; var healthBar = LK.getAsset('healthBar', { anchorX: 0, anchorY: 0, scaleX: 2, scaleY: 1 }); LK.gui.topLeft.addChild(healthBar); healthBar.x = 120; healthBar.y = 20; healthBar.visible = false; var healthText = new Text2('Health: 100/100', { size: 60, fill: 0xFFFFFF, font: "monospace" }); healthText.anchor.set(0, 0); LK.gui.topLeft.addChild(healthText); healthText.x = 120; healthText.y = 65; 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 // Unified SpawnManager for streamlined spawn control and enemy lifecycle var SpawnManager = { // Consolidated spawn configuration spawnConfig: { skeleton: { interval: 90, maxCount: 15, startAt: 0 }, ogre: { interval: 180, maxCount: 4, startAt: 15 }, knight: { interval: 300, maxCount: 3, startAt: 30 }, miniBoss: { interval: 60, maxCount: 1, startAt: 80, endAt: 85, chance: 0.02 } }, // Unified spawn timers spawnTimers: { skeleton: 0, ogre: 0, knight: 0, miniBoss: 0 }, // Streamlined spawn validation canSpawnEnemy: function canSpawnEnemy(type, difficulty, totalEnemies) { var config = this.spawnConfig[type]; if (!config) return false; var collection = []; if (type === 'skeleton') collection = enemies;else if (type === 'ogre') collection = ogres;else if (type === 'knight') collection = knights;else if (type === 'miniBoss') collection = miniBosses; var timer = this.spawnTimers[type]; // Usar valores directos para thresholds var threshold = 0; if (type === 'ogre') threshold = 15;else if (type === 'knight') threshold = 30;else if (type === 'miniBoss') threshold = 80; // Universal spawn conditions with type-specific rules var baseConditions = timer >= config.interval && enemyKillCounter >= threshold && collection.length < config.maxCount && totalEnemies < 25; // Special conditions for specific types if (type === 'miniBoss') { return baseConditions && enemyKillCounter <= config.endAt && miniBosses.length === 0 && Math.random() < config.chance; } return baseConditions && miniBosses.length === 0; }, // Unified spawn execution with specific enemy classes executeSpawn: function executeSpawn(type, difficulty, level) { var totalEnemies = globalEnemyManager.getAllEnemies().length; if (!this.canSpawnEnemy(type, difficulty, totalEnemies)) return null; var enemy = globalEnemyManager.createEnemy(type, difficulty, level); if (enemy) { game.addChild(enemy); // Add directly to global arrays if (type === 'skeleton') enemies.push(enemy);else if (type === 'ogre') ogres.push(enemy);else if (type === 'knight') knights.push(enemy);else if (type === 'miniBoss') miniBosses.push(enemy); this.spawnTimers[type] = 0; // Update path tracking for skeleton spawns if (type === 'skeleton') { pathConsecutiveSpawns[enemy.pathIndex]++; pathLastSpawnTime[enemy.pathIndex] = LK.ticks; lastSpawnedPath = enemy.pathIndex; } } return enemy; }, // Streamlined interval updates updateSpawnIntervals: function updateSpawnIntervals(difficulty, level) { // Usar valores directos para skeleton if (difficulty === 'FACIL') { this.spawnConfig.skeleton.interval = Math.max(60, 120 - level * 5); this.spawnConfig.ogre.interval = 240; this.spawnConfig.knight.interval = 400; } else if (difficulty === 'NORMAL') { this.spawnConfig.skeleton.interval = Math.max(40, 90 - level * 6); this.spawnConfig.ogre.interval = 180; this.spawnConfig.knight.interval = 300; } else { // DIFICIL this.spawnConfig.skeleton.interval = Math.max(20, 60 - level * 4); this.spawnConfig.ogre.interval = 120; this.spawnConfig.knight.interval = 200; } }, // Unified timer updates updateTimers: function updateTimers() { for (var type in this.spawnTimers) { this.spawnTimers[type]++; } // Check if all paths are in cooldown and reset if needed var allPathsInCooldown = true; for (var pathIdx = 0; pathIdx < 5; pathIdx++) { if (pathConsecutiveSpawns[pathIdx] < 2) { allPathsInCooldown = false; break; } } if (allPathsInCooldown) { // Reset all path cooldowns to prevent spawn deadlock for (var pathIdx = 0; pathIdx < 5; pathIdx++) { pathConsecutiveSpawns[pathIdx] = 0; pathLastSpawnTime[pathIdx] = -1; } } }, // Streamlined spawn cycle for all enemy types processSpawnCycle: function processSpawnCycle(difficulty, level) { this.updateSpawnIntervals(difficulty, level); this.updateTimers(); // Clean up any null/destroyed enemies from collections first this.cleanupDestroyedEnemies(); var enemyTypes = ['skeleton', 'ogre', 'knight', 'miniBoss']; for (var i = 0; i < enemyTypes.length; i++) { this.executeSpawn(enemyTypes[i], difficulty, level); } }, // Add cleanup method for destroyed enemies cleanupDestroyedEnemies: function cleanupDestroyedEnemies() { // Clean up global arrays directly var allArrays = [enemies, ogres, knights, miniBosses]; for (var arrayIdx = 0; arrayIdx < allArrays.length; arrayIdx++) { var array = allArrays[arrayIdx]; for (var i = array.length - 1; i >= 0; i--) { if (!array[i] || !array[i].parent || array[i].isDying) { array.splice(i, 1); } } } } }; // Game input handling with spell casting support game.down = function (x, y, obj) { // Removed automatic fireball casting - spells are now manual only through spell slots // Tap-to-attack is now handled directly by individual enemies // No need for path-based attacks since enemies handle their own tap events }; // Initialize unified management systems for streamlined game architecture var globalEnemyManager = new EnemyManager(); var globalDeathHandler = new UnifiedDeathHandler(); // Unified death animation function for all enemy types function createEnemyDeathAnimation(enemy, enemyType, enemyArray) { globalDeathHandler.executeEnemyDeath(enemy, enemyArray); } // 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 } }); } // Upgrade menus removed - using spell deck system instead // 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 } // Helper functions now integrated into EnemyFactory // Unified spawn management system SpawnManager.processSpawnCycle(selectedDifficulty, difficultyLevel); // Reset path cooldowns for optimized path management for (var pathIdx = 0; pathIdx < 5; pathIdx++) { if (pathLastSpawnTime[pathIdx] !== -1 && LK.ticks - pathLastSpawnTime[pathIdx] > pathCooldownDuration) { pathConsecutiveSpawns[pathIdx] = 0; } } // Unified CollisionManager for streamlined collision detection and response var CollisionManager = { // Consolidated collision configurations collisionConfig: { skeleton: { damage: 20, removeOnHit: true }, ogre: { damage: 30, removeOnHit: true }, knight: { damage: 40, removeOnHit: true }, miniBoss: { damage: 75, removeOnHit: false } }, // Streamlined enemy collision processing with categorized collision types processEnemyCollisions: function processEnemyCollisions() { var allEnemies = globalEnemyManager.getAllEnemies(); // Category 1: Off-screen cleanup (non-collision processing) this.processOffScreenCleanup(allEnemies); // Category 2: Enemy-wizard collisions this.processEnemyWizardCollisions(allEnemies); }, // Separate processing for off-screen enemy cleanup processOffScreenCleanup: function processOffScreenCleanup(allEnemies) { for (var i = allEnemies.length - 1; i >= 0; i--) { var enemy = allEnemies[i]; if (this.isOffScreen(enemy)) { this.removeEnemyFromGame(enemy); } } }, // Separate processing for enemy-wizard collisions processEnemyWizardCollisions: function processEnemyWizardCollisions(allEnemies) { for (var i = 0; i < allEnemies.length; i++) { var enemy = allEnemies[i]; if (!enemy.isDying && enemy.parent) { this.checkWizardCollision(enemy); } } }, // Efficient off-screen detection isOffScreen: function isOffScreen(enemy) { return enemy.y > 2732 + 100; }, // Unified enemy removal system removeEnemyFromGame: function removeEnemyFromGame(enemy) { // Remove from global arrays directly this.removeFromLegacyArrays(enemy); enemy.destroy(); }, // Legacy array compatibility cleanup removeFromLegacyArrays: function removeFromLegacyArrays(enemy) { var arrays = [enemies, ogres, knights, miniBosses]; for (var a = 0; a < arrays.length; a++) { var array = arrays[a]; for (var i = array.length - 1; i >= 0; i--) { if (array[i] === enemy) { array.splice(i, 1); break; } } } }, // 1.1 Distance Culling: Enhanced wizard collision detection with optimized distance-based culling checkWizardCollision: function checkWizardCollision(enemy) { // Initialize collision tracking if (enemy.lastIntersecting === undefined) { enemy.lastIntersecting = false; } // 1.1 Distance Culling: Skip expensive intersection test if objects are too far apart var dx = enemy.x - wizard.x; var dy = enemy.y - wizard.y; var distance = Math.sqrt(dx * dx + dy * dy); var maxCollisionDistance = 150; // Approximate maximum collision distance based on sprite sizes var currentIntersecting = false; if (distance <= maxCollisionDistance) { // Only perform expensive intersection test if objects are close enough currentIntersecting = wizard.intersects(enemy); } // Check collision transition if (!enemy.lastIntersecting && currentIntersecting && !enemy.isDying) { var config = this.getEnemyConfig(enemy); wizard.takeDamage(config.damage); // Handle enemy removal based on type if (config.removeOnHit) { this.removeEnemyFromGame(enemy); return; } } // Update collision state enemy.lastIntersecting = currentIntersecting; }, // Get enemy configuration by type getEnemyConfig: function getEnemyConfig(enemy) { return this.collisionConfig[enemy.enemyType] || this.collisionConfig.skeleton; } }; // Replace the old collision function call function checkAllEnemyCollisions() { CollisionManager.processEnemyCollisions(); } // Call the unified collision detection function checkAllEnemyCollisions(); // 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 = collisionArrayPool.getAllEnemies(); 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); } } } // Update spell system if (gameStarted) { // Force mana to always be 100 - spells are completely free currentMana = 100; if (activeSpellDeck) { activeSpellDeck.currentMana = 100; } updateManaBar(); updateSpellSlots(); } // Simple time slow effects processing var allEnemies = collisionArrayPool.getAllEnemies(); for (var i = 0; i < allEnemies.length; i++) { var enemy = allEnemies[i]; if (enemy.timeSlowed) { enemy.timeSlowTimer--; if (enemy.timeSlowTimer <= 0) { enemy.timeSlowed = false; enemy.timeSlowAmount = 1.0; } } } // Make tap text pulse var pulse = 1 + Math.sin(LK.ticks * 0.1) * 0.2; tapText.scale.set(pulse, pulse); // Check for spell unlocks checkSpellUnlocks(); // Clean up any orphaned projectiles that may not have been properly removed for (var i = projectiles.length - 1; i >= 0; i--) { var projectile = projectiles[i]; if (!projectile || !projectile.parent || projectile.hitEnemy) { projectiles.splice(i, 1); } } // ProjectileFactory uses the global projectiles array, no separate activeProjectiles array needed }; // 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
****/
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 wizard
if (wizard && self.intersects(wizard)) {
self.collect();
}
}
};
self.collect = function () {
LK.getSound('coinCollect').play();
LK.setScore(LK.getScore() + 5);
coinCounter++;
coinText.setText('Coins: ' + coinCounter);
// Remove from coins array
for (var i = coins.length - 1; i >= 0; i--) {
if (coins[i] === self) {
coins.splice(i, 1);
break;
}
}
// Destroy coin directly without object pool
self.destroy();
};
return self;
});
// Unified EnemyManager for streamlined enemy lifecycle management
var EnemyManager = Container.expand(function () {
var self = Container.call(this);
// Use global arrays directly - no separate collections needed
// Unified enemy configuration templates
self.enemyTemplates = {
skeleton: {
baseHealth: 100,
baseSpeed: 3,
damage: 20,
coinReward: 1
},
ogre: {
baseHealth: 200,
baseSpeed: 2.5,
damage: 30,
coinReward: 1
},
knight: {
baseHealth: 300,
baseSpeed: 2,
damage: 40,
coinReward: 1
},
miniBoss: {
baseHealth: 3000,
baseSpeed: 4,
damage: 75,
coinReward: 5
}
};
// Streamlined enemy creation with specific enemy classes
self.createEnemy = function (type, difficulty, level, pathOverride) {
var template = self.enemyTemplates[type];
if (!template) return null;
// Create specific enemy class based on type
var enemy = null;
if (type === 'skeleton') {
enemy = new Skeleton();
} else if (type === 'ogre') {
enemy = new Ogre();
} else if (type === 'knight') {
enemy = new Knight();
} else if (type === 'miniBoss') {
enemy = new MiniBoss();
} else {
return null;
}
// Apply difficulty scaling
enemy.speed *= 1 + level * 0.3;
enemy.pathIndex = pathOverride || self.selectOptimalPath(type);
self.positionEnemy(enemy, type);
self.applyDifficultyModifications(enemy, type, difficulty);
return enemy;
};
// Unified enemy positioning system
self.positionEnemy = function (enemy, type) {
// Use direct spawn position values
var spawnPositions = [{
x: 2048 / 2,
y: -100
}, {
x: 2048 + 50,
y: -50
}, {
x: -50,
y: -50
}, {
x: -100,
y: 2732 / 2 + 400
}, {
x: 2048 + 100,
y: 2732 / 2 + 400
}];
var spawnPos = spawnPositions[enemy.pathIndex];
if (spawnPos) {
enemy.x = Math.max(50, Math.min(1998, spawnPos.x));
enemy.y = type === 'miniBoss' ? -200 : Math.max(-200, Math.min(2732 + 100, spawnPos.y));
enemy.lastX = enemy.x;
}
};
// Streamlined difficulty modifications
self.applyDifficultyModifications = function (enemy, type, difficulty) {
if (type === 'miniBoss') {
enemy.updateHealthBar();
LK.effects.flashScreen(0x8B0000, 1000);
} else if (type === 'skeleton' && Math.random() < 0.3) {
LK.getSound('enemyGrowl').play();
}
// Elite enemy system for hard difficulty
if (difficulty === 'DIFICIL' && enemyKillCounter >= 20 && Math.random() < 0.15) {
self.createEliteEnemy(enemy, type);
}
};
// Unified elite enemy creation
self.createEliteEnemy = function (enemy, type) {
var eliteMultipliers = {
health: 1.8,
speed: 1.3,
color: 0xFF6600
};
if (type === 'ogre') eliteMultipliers.color = 0xFF0000;
if (type === 'knight') eliteMultipliers.color = 0xFFD700;
if (type === 'miniBoss') eliteMultipliers.color = 0x8B0000;
enemy.health *= eliteMultipliers.health;
enemy.speed *= eliteMultipliers.speed;
enemy.maxHealth = enemy.health;
enemy.isElite = true;
for (var i = 0; i < enemy.animationFrames.length; i++) {
enemy.animationFrames[i].tint = eliteMultipliers.color;
}
};
// Optimized path selection system
self.selectOptimalPath = function (type) {
if (type === 'skeleton' && enemyKillCounter < 5) return 0;
if (type === 'miniBoss') return 0;
var available = [];
for (var i = 0; i < 5; i++) {
if (pathConsecutiveSpawns[i] < 2) available.push(i);
}
if (available.length === 0) {
for (var i = 0; i < 5; i++) pathConsecutiveSpawns[i] = 0;
available = [0, 1, 2, 3, 4];
}
return available[Math.floor(Math.random() * available.length)];
};
// Unified enemy collection management
self.addToCollection = function (enemy, type) {
if (type === 'skeleton') enemies.push(enemy);else if (type === 'ogre') ogres.push(enemy);else if (type === 'knight') knights.push(enemy);else if (type === 'miniBoss') miniBosses.push(enemy);
};
// Streamlined enemy removal system
self.removeFromCollection = function (enemy, type) {
var collection = [];
if (type === 'skeleton') collection = enemies;else if (type === 'ogre') collection = ogres;else if (type === 'knight') collection = knights;else if (type === 'miniBoss') collection = miniBosses;
if (collection.length > 0) {
for (var i = collection.length - 1; i >= 0; i--) {
if (collection[i] === enemy) {
collection.splice(i, 1);
break;
}
}
}
};
// Get all enemies as unified array
self.getAllEnemies = function () {
var allEnemies = [];
allEnemies = allEnemies.concat(enemies);
allEnemies = allEnemies.concat(ogres);
allEnemies = allEnemies.concat(knights);
allEnemies = allEnemies.concat(miniBosses);
return allEnemies;
};
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;
}
// Upgrade menu removed - no pause needed
// 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 = collisionArrayPool.getAllEnemies();
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 using unified factory
var energyBeam = ProjectileFactory.createProjectile('energyBeam', self.x, self.y, closestEnemy.x, closestEnemy.y, {
targetEnemy: closestEnemy
});
// 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 GameMenu = Container.expand(function () {
var self = Container.call(this);
// Simple spell deck - load from storage or use defaults
var currentDeck = storage.spellDeck || ['fireball', 'heal', 'lightning'];
storage.spellDeck = currentDeck.slice();
// 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: 25.0,
scaleY: 35.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 ENEMIES TO ATTACK\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: 1950,
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 = 1950;
self.addChild(shopButtonText);
// Deck button
var deckButton = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2150,
scaleX: 4,
scaleY: 2
});
deckButton.tint = 0x8A2BE2;
var deckButtonText = new Text2('DECK HECHIZOS', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
deckButtonText.anchor.set(0.5, 0.5);
deckButtonText.x = 2048 / 2;
deckButtonText.y = 2150;
self.addChild(deckButtonText);
// 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: 2350,
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 = 2350;
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 deck menu interactions
if (self.deckMode) {
// Check deck card clicks for spell casting (top area) or removal (bottom area)
for (var i = 0; i < self.deckElements.length; i++) {
var element = self.deckElements[i];
if (element.spellId && element.isDeckCard) {
var cardX = element.x;
var cardY = element.y;
if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) {
// Top area of card (above center) - cast spell
if (y <= cardY) {
// Try to cast the spell
if (activeSpellDeck && activeSpellDeck.canCastSpell(element.spellId)) {
var spell = activeSpellDeck.getSpell(element.spellId);
if (spell) {
var targetX = wizard.x;
var targetY = wizard.y - 100;
// Find nearest enemy for targeted spells
if (spell.id === 'fireball' || spell.id === 'iceShard' || spell.id === 'lightning') {
var allEnemies = collisionArrayPool.getAllEnemies();
var nearestEnemy = null;
var nearestDistance = Infinity;
for (var e = 0; e < allEnemies.length; e++) {
var enemy = allEnemies[e];
var dx = enemy.x - wizard.x;
var dy = enemy.y - wizard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestEnemy = enemy;
}
}
if (nearestEnemy) {
targetX = nearestEnemy.x;
targetY = nearestEnemy.y;
}
}
// Cast the spell
activeSpellDeck.castSpell(element.spellId, targetX, targetY);
LK.effects.flashObject(element, 0x00FF00, 200);
// Show cast message
var castText = new Text2('HECHIZO LANZADO!', {
size: 60,
fill: 0x00FF00,
font: "monospace"
});
castText.anchor.set(0.5, 0.5);
castText.x = 2048 / 2;
castText.y = 2200;
self.addChild(castText);
// Animate and remove message
tween(castText, {
alpha: 0,
y: castText.y - 100
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (castText.parent) castText.destroy();
}
});
}
} else {
LK.effects.flashObject(element, 0xFF0000, 200);
// Show "cannot cast" message
var errorText = new Text2('NO SE PUEDE LANZAR', {
size: 50,
fill: 0xFF6666,
font: "monospace"
});
errorText.anchor.set(0.5, 0.5);
errorText.x = 2048 / 2;
errorText.y = 2200;
self.addChild(errorText);
// Animate and remove message
tween(errorText, {
alpha: 0,
y: errorText.y - 100
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (errorText.parent) errorText.destroy();
}
});
}
} else {
// Bottom area of card (below center) - remove from deck
// Visual feedback before removal
LK.effects.flashObject(element, 0xFF0000, 300);
// Remove from deck
if (self.spellDeck.removeFromDeck(element.spellId)) {
self.refreshDeckDisplay();
LK.effects.flashScreen(0xFF8800, 200);
// Show removal message
var removeText = new Text2('HECHIZO REMOVIDO', {
size: 60,
fill: 0xFF6666,
font: "monospace"
});
removeText.anchor.set(0.5, 0.5);
removeText.x = 2048 / 2;
removeText.y = 2200;
self.addChild(removeText);
// Animate and remove message
tween(removeText, {
alpha: 0,
y: removeText.y - 100
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (removeText.parent) removeText.destroy();
}
});
} else {
LK.effects.flashScreen(0xFF0000, 200);
}
}
return;
}
}
}
// Check available card clicks with better hit detection
for (var i = 0; i < self.availableElements.length; i++) {
var element = self.availableElements[i];
if (element.spellId && !element.isDeckCard) {
var cardX = element.x;
var cardY = element.y;
if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) {
// Visual feedback before addition
LK.effects.flashObject(element, 0x00FF00, 300);
// Add to deck
if (self.spellDeck.addToDeck(element.spellId)) {
self.refreshDeckDisplay();
LK.effects.flashScreen(0x00FF00, 200);
// Show addition message
var addText = new Text2('HECHIZO AÑADIDO', {
size: 60,
fill: 0x66FF66,
font: "monospace"
});
addText.anchor.set(0.5, 0.5);
addText.x = 2048 / 2;
addText.y = 2200;
self.addChild(addText);
// Animate and remove message
tween(addText, {
alpha: 0,
y: addText.y - 100
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (addText.parent) addText.destroy();
}
});
} else {
LK.effects.flashScreen(0xFF0000, 200);
// Show error message
var errorText = new Text2('DECK LLENO (MAX 5)', {
size: 50,
fill: 0xFF6666,
font: "monospace"
});
errorText.anchor.set(0.5, 0.5);
errorText.x = 2048 / 2;
errorText.y = 2200;
self.addChild(errorText);
// Animate and remove message
tween(errorText, {
alpha: 0,
y: errorText.y - 100
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (errorText.parent) errorText.destroy();
}
});
}
return;
}
}
}
// Deck back button
if (y >= 2350 && y <= 2650) {
self.hideDeck();
}
// Block all other interactions when deck 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 >= 1850 && y <= 2050 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) {
// Show shop menu
self.showShop();
} else if (y >= 2050 && y <= 2250 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) {
// Show deck menu
self.showDeck();
} else if (storage.tutorialCompleted && y >= 2250 && y <= 2450 && 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 () {
if (!self.configOverlay) {
self.configOverlay = self.createMenuOverlay(0x000000);
self.configTitle = self.createMenuText('CONFIGURACION', 2048 / 2, 800, 120, 0xFFD700);
self.musicVolumeText = self.createMenuText('VOLUMEN MUSICA: ' + Math.round((storage.musicVolume || 0.7) * 100) + '%', 2048 / 2, 1200, 80, 0xFFFFFF);
self.soundVolumeText = self.createMenuText('VOLUMEN SONIDO: ' + Math.round((storage.soundVolume || 1.0) * 100) + '%', 2048 / 2, 1400, 80, 0xFFFFFF);
self.difficultyText = self.createMenuText('DIFICULTAD: ' + (storage.difficulty || 'NORMAL'), 2048 / 2, 1600, 80, 0xFFFFFF);
self.backButton = self.createMenuButton('coin', 2048 / 2, 2000, 0x00FF00);
self.backText = self.createMenuText('VOLVER', 2048 / 2, 2000, 80, 0xFFFFFF);
}
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 () {
if (!self.shopOverlay) {
self.shopOverlay = self.createMenuOverlay(0x000033);
self.shopTitle = self.createMenuText('TIENDA', 2048 / 2, 800, 120, 0xFFD700);
var shopItems = self.getShopItemsData();
self.initializeShopArrays();
self.createShopItems(shopItems);
self.shopBackButton = self.createMenuButton('coin', 2048 / 2, 2000, 0x00FF00);
self.shopBackText = self.createMenuText('VOLVER', 2048 / 2, 2000, 80, 0xFFFFFF);
}
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.showDeck = function () {
if (!self.deckOverlay) {
// Ensure spellDeck exists before creating overlay
if (!self.spellDeck) self.spellDeck = new SpellDeck();
self.deckOverlay = self.createMenuOverlay(0x1a0a2e);
self.deckTitle = self.createMenuText('DECK DE HECHIZOS', 2048 / 2, 600, 100, 0xFFD700);
self.initializeDeckArrays();
self.refreshDeckDisplay();
self.deckBackButton = self.createMenuButton('coin', 2048 / 2, 2500, 0x00FF00);
self.deckBackText = self.createMenuText('VOLVER', 2048 / 2, 2500, 80, 0xFFFFFF);
}
self.deckOverlay.visible = true;
self.deckMode = true;
self.refreshDeckDisplay();
};
self.initializeDeckArrays = function () {
if (!self.deckElements) self.deckElements = [];
if (!self.availableElements) self.availableElements = [];
};
self.refreshDeckDisplay = function () {
if (!self.spellDeck) {
self.spellDeck = new SpellDeck();
}
// Clear existing deck elements
for (var i = 0; i < self.deckElements.length; i++) {
if (self.deckElements[i] && self.deckElements[i].parent) {
self.deckElements[i].destroy();
}
}
self.deckElements = [];
// Clear existing available elements
for (var i = 0; i < self.availableElements.length; i++) {
if (self.availableElements[i] && self.availableElements[i].parent) {
self.availableElements[i].destroy();
}
}
self.availableElements = [];
// Add helpful instructions at the top
var instructionText = new Text2('TOCA CARTAS PARA AÑADIR/QUITAR DEL DECK', {
size: 50,
fill: 0x00FF00,
font: "monospace"
});
instructionText.anchor.set(0.5, 0.5);
instructionText.x = 2048 / 2;
instructionText.y = 700;
self.addChild(instructionText);
self.deckElements.push(instructionText);
// Display current deck (top section)
var deckLabel = new Text2('MI DECK ACTUAL (' + self.spellDeck.currentDeck.length + '/5):', {
size: 70,
fill: 0xFFD700,
font: "monospace"
});
deckLabel.anchor.set(0.5, 0.5);
deckLabel.x = 2048 / 2;
deckLabel.y = 800;
self.addChild(deckLabel);
self.deckElements.push(deckLabel);
// Add deck instruction
var deckInstruction = new Text2('TOCA PARA QUITAR', {
size: 40,
fill: 0xFF6666,
font: "monospace"
});
deckInstruction.anchor.set(0.5, 0.5);
deckInstruction.x = 2048 / 2;
deckInstruction.y = 850;
self.addChild(deckInstruction);
self.deckElements.push(deckInstruction);
// Display deck cards with better spacing
for (var i = 0; i < 5; i++) {
var cardX = 200 + i * 350;
var cardY = 1050;
if (i < self.spellDeck.currentDeck.length) {
var spell = self.spellDeck.getSpell(self.spellDeck.currentDeck[i]);
if (spell) {
// Card background with glow effect
var cardBg = self.attachAsset('spellCard', {
anchorX: 0.5,
anchorY: 0.5,
x: cardX,
y: cardY,
scaleX: 3.5,
scaleY: 4.5
});
cardBg.tint = self.spellDeck.getRarityColor(spell.rarity);
cardBg.spellId = spell.id;
cardBg.isDeckCard = true;
cardBg.alpha = 0.9;
self.deckElements.push(cardBg);
// Add border glow
var glowBorder = self.attachAsset('spellCardBg', {
anchorX: 0.5,
anchorY: 0.5,
x: cardX,
y: cardY,
scaleX: 4,
scaleY: 5
});
glowBorder.tint = 0x00FF00;
glowBorder.alpha = 0.3;
self.deckElements.push(glowBorder);
// Card name
var cardName = new Text2(spell.name, {
size: 35,
fill: 0xFFFFFF,
font: "monospace"
});
cardName.anchor.set(0.5, 0.5);
cardName.x = cardX;
cardName.y = cardY - 60;
self.addChild(cardName);
self.deckElements.push(cardName);
// Card description
var description = spell.description || 'Hechizo magico';
var cardDesc = new Text2(description, {
size: 25,
fill: 0xCCCCCC,
font: "monospace",
wordWrap: true,
wordWrapWidth: 250
});
cardDesc.anchor.set(0.5, 0.5);
cardDesc.x = cardX;
cardDesc.y = cardY + 20;
self.addChild(cardDesc);
self.deckElements.push(cardDesc);
// Card stats
var statsText = '';
if (spell.damage) statsText += 'Daño: ' + spell.damage + '\n';
if (spell.healing) statsText += 'Cura: ' + spell.healing + '\n';
if (spell.manaCost) statsText += 'Mana: ' + spell.manaCost;
if (statsText) {
var cardStats = new Text2(statsText, {
size: 20,
fill: 0xFFD700,
font: "monospace"
});
cardStats.anchor.set(0.5, 0.5);
cardStats.x = cardX;
cardStats.y = cardY + 80;
self.addChild(cardStats);
self.deckElements.push(cardStats);
}
}
} else {
// Empty slot
var emptySlot = self.attachAsset('spellCardBg', {
anchorX: 0.5,
anchorY: 0.5,
x: cardX,
y: cardY,
scaleX: 3.5,
scaleY: 4.5
});
emptySlot.tint = 0x444444;
emptySlot.alpha = 0.5;
self.deckElements.push(emptySlot);
var emptyText = new Text2('VACIO', {
size: 40,
fill: 0x666666,
font: "monospace"
});
emptyText.anchor.set(0.5, 0.5);
emptyText.x = cardX;
emptyText.y = cardY;
self.addChild(emptyText);
self.deckElements.push(emptyText);
}
}
// Display available spells (bottom section)
var availableLabel = new Text2('HECHIZOS DISPONIBLES PARA AÑADIR:', {
size: 60,
fill: 0xFFD700,
font: "monospace"
});
availableLabel.anchor.set(0.5, 0.5);
availableLabel.x = 2048 / 2;
availableLabel.y = 1350;
self.addChild(availableLabel);
self.availableElements.push(availableLabel);
// Add available instruction
var availableInstruction = new Text2('TOCA PARA AÑADIR A TU DECK', {
size: 40,
fill: 0x66FF66,
font: "monospace"
});
availableInstruction.anchor.set(0.5, 0.5);
availableInstruction.x = 2048 / 2;
availableInstruction.y = 1400;
self.addChild(availableInstruction);
self.availableElements.push(availableInstruction);
// Display available spells
var availableSpells = [];
for (var i = 0; i < self.spellDeck.availableSpells.length; i++) {
var spell = self.spellDeck.availableSpells[i];
if (self.spellDeck.currentDeck.indexOf(spell.id) === -1) {
availableSpells.push(spell);
}
}
if (availableSpells.length === 0) {
var noSpellsText = new Text2('NO HAY HECHIZOS DISPONIBLES\nDESBLOQUEA MAS JUGANDO', {
size: 50,
fill: 0x888888,
font: "monospace"
});
noSpellsText.anchor.set(0.5, 0.5);
noSpellsText.x = 2048 / 2;
noSpellsText.y = 1600;
self.addChild(noSpellsText);
self.availableElements.push(noSpellsText);
}
for (var i = 0; i < availableSpells.length && i < 8; i++) {
var spell = availableSpells[i];
var cardX = 150 + i % 4 * 450;
var cardY = 1550 + Math.floor(i / 4) * 400;
// Card background with hover effect
var cardBg = self.attachAsset('spellCard', {
anchorX: 0.5,
anchorY: 0.5,
x: cardX,
y: cardY,
scaleX: 3.2,
scaleY: 4.2
});
cardBg.tint = self.spellDeck.getRarityColor(spell.rarity);
cardBg.spellId = spell.id;
cardBg.isDeckCard = false;
cardBg.alpha = 0.8;
self.availableElements.push(cardBg);
// Add selection glow
var selectionGlow = self.attachAsset('spellCardBg', {
anchorX: 0.5,
anchorY: 0.5,
x: cardX,
y: cardY,
scaleX: 3.7,
scaleY: 4.7
});
selectionGlow.tint = 0x00FFFF;
selectionGlow.alpha = 0.2;
self.availableElements.push(selectionGlow);
// Card name
var cardName = new Text2(spell.name, {
size: 32,
fill: 0xFFFFFF,
font: "monospace"
});
cardName.anchor.set(0.5, 0.5);
cardName.x = cardX;
cardName.y = cardY - 60;
self.addChild(cardName);
self.availableElements.push(cardName);
// Card description
var cardDesc = new Text2(spell.description, {
size: 22,
fill: 0xCCCCCC,
font: "monospace",
wordWrap: true,
wordWrapWidth: 280
});
cardDesc.anchor.set(0.5, 0.5);
cardDesc.x = cardX;
cardDesc.y = cardY + 10;
self.addChild(cardDesc);
self.availableElements.push(cardDesc);
// Card stats
var statsText = '';
if (spell.damage) statsText += 'Daño: ' + spell.damage + '\n';
if (spell.healing) statsText += 'Cura: ' + spell.healing + '\n';
if (spell.manaCost) statsText += 'Mana: ' + spell.manaCost;
if (statsText) {
var cardStats = new Text2(statsText, {
size: 18,
fill: 0xFFD700,
font: "monospace"
});
cardStats.anchor.set(0.5, 0.5);
cardStats.x = cardX;
cardStats.y = cardY + 70;
self.addChild(cardStats);
self.availableElements.push(cardStats);
}
// Rarity indicator
var rarityText = new Text2(spell.rarity.toUpperCase(), {
size: 20,
fill: self.spellDeck.getRarityColor(spell.rarity),
font: "monospace"
});
rarityText.anchor.set(0.5, 0.5);
rarityText.x = cardX;
rarityText.y = cardY + 100;
self.addChild(rarityText);
self.availableElements.push(rarityText);
}
};
self.hideDeck = function () {
if (self.deckOverlay) {
self.deckOverlay.destroy();
self.deckOverlay = null;
}
// Remove deck title
if (self.deckTitle) {
self.deckTitle.destroy();
self.deckTitle = null;
}
// Remove deck back button elements
if (self.deckBackButton) {
self.deckBackButton.destroy();
self.deckBackButton = null;
}
if (self.deckBackText) {
self.deckBackText.destroy();
self.deckBackText = null;
}
// Clear deck elements
for (var i = 0; i < self.deckElements.length; i++) {
if (self.deckElements[i] && self.deckElements[i].parent) {
self.deckElements[i].destroy();
}
}
self.deckElements = [];
// Clear available elements
for (var i = 0; i < self.availableElements.length; i++) {
if (self.availableElements[i] && self.availableElements[i].parent) {
self.availableElements[i].destroy();
}
}
self.availableElements = [];
// Remove all deck-related children
for (var i = self.children.length - 1; i >= 0; i--) {
var child = self.children[i];
if (child.setText && child.text && (child.text.includes('DECK') || child.text.includes('HECHIZOS') || child.text.includes('ACTUAL') || child.text.includes('DISPONIBLES'))) {
child.destroy();
}
}
self.deckMode = false;
self.visible = true;
};
// Unified UI factory for all menu elements
// Menu overlay creation function
self.createMenuOverlay = function (color) {
var overlay = self.attachAsset('startMenuBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 25.0,
scaleY: 35.0
});
overlay.alpha = 0.9;
overlay.tint = color || 0x000000;
overlay.interactive = true;
overlay.zIndex = 1000;
return overlay;
};
// Menu text creation function
self.createMenuText = function (text, x, y, size, color) {
var textElement = new Text2(text, {
size: size,
fill: color,
font: "monospace"
});
textElement.anchor.set(0.5, 0.5);
textElement.x = x;
textElement.y = y;
self.addChild(textElement);
return textElement;
};
// Menu button creation function
self.createMenuButton = function (asset, x, y, color) {
var button = self.attachAsset(asset, {
anchorX: 0.5,
anchorY: 0.5,
x: x,
y: y,
scaleX: 2,
scaleY: 2
});
button.tint = color;
return button;
};
self.createUIElement = function (type, config) {
if (type === 'overlay') {
var overlay = self.addChild(LK.getAsset('startMenuBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366,
scaleX: 1.0,
scaleY: 1.0
}));
overlay.alpha = 1.0;
overlay.tint = config.tint || 0x000000;
return overlay;
} else if (type === 'text') {
var text = new Text2(config.text, {
size: config.size,
fill: config.fill,
font: "monospace"
});
text.anchor.set(0.5, 0.5);
text.x = config.x;
text.y = config.y;
self.addChild(text);
return text;
} else if (type === 'button') {
var button = self.attachAsset(config.asset, {
anchorX: 0.5,
anchorY: 0.5,
x: config.x,
y: config.y,
scaleX: 2,
scaleY: 2
});
button.tint = config.tint;
return button;
}
};
self.getShopItemsData = function () {
return [{
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'
}];
};
self.initializeShopArrays = function () {
if (!self.shopIcons) self.shopIcons = [];
if (!self.shopTexts) self.shopTexts = [];
if (!self.shopBuyButtons) self.shopBuyButtons = [];
if (!self.shopBuyTexts) self.shopBuyTexts = [];
};
self.createShopItems = function (shopItems) {
for (var i = 0; i < shopItems.length; i++) {
var item = shopItems[i];
var yPos = 1100 + i * 200;
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);
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);
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);
}
};
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;
}
// UNIFIED DECK SYNCHRONIZATION: Fix all deck system inconsistencies
// Create unified deck reference that all systems will use
var unifiedDeck = [];
// Priority 1: Use spellDeck from menu if it exists and has content
if (self.spellDeck && self.spellDeck.currentDeck && self.spellDeck.currentDeck.length > 0) {
unifiedDeck = self.spellDeck.currentDeck.slice();
}
// Priority 2: Use storage deck if available
else if (storage.spellDeck && storage.spellDeck.length > 0) {
unifiedDeck = storage.spellDeck.slice();
}
// Priority 3: Use global currentDeck if available
else if (currentDeck && currentDeck.length > 0) {
unifiedDeck = currentDeck.slice();
}
// Priority 4: Use activeSpellDeck if available
else if (activeSpellDeck && activeSpellDeck.currentDeck && activeSpellDeck.currentDeck.length > 0) {
unifiedDeck = activeSpellDeck.currentDeck.slice();
}
// Priority 5: Default deck as last resort
else {
unifiedDeck = ['fireball', 'heal', 'lightning'];
}
// SYNCHRONIZE ALL DECK SYSTEMS with unified deck
currentDeck = unifiedDeck.slice();
activeSpellDeck.currentDeck = unifiedDeck.slice();
storage.spellDeck = unifiedDeck.slice();
if (self.spellDeck) {
self.spellDeck.currentDeck = unifiedDeck.slice();
}
// Debug: Log deck synchronization with enhanced details
console.log('=== DECK SYNCHRONIZATION ===');
console.log('Unified deck:', unifiedDeck);
console.log('ActiveSpellDeck sync:', activeSpellDeck.currentDeck);
console.log('Storage sync:', storage.spellDeck);
console.log('CurrentDeck sync:', currentDeck);
console.log('Available spells IDs:', availableSpells.map(function (s) {
return s.id;
}));
console.log('Spell validation:');
for (var d = 0; d < unifiedDeck.length; d++) {
var spellId = unifiedDeck[d];
var spell = _getSpell(spellId);
console.log(' - ' + spellId + ':', spell ? spell.name : 'NOT FOUND');
}
// 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;
// Show spell UI
if (manaBarBg) manaBarBg.visible = true;
if (manaBar) manaBar.visible = true;
if (manaText) manaText.visible = true;
// SPELL SLOT INITIALIZATION AND ASSIGNMENT
for (var i = 0; i < spellSlots.length; i++) {
var slot = spellSlots[i];
// FORCE PROPER POSITIONING AND VISIBILITY
slot.x = 400 + i * 400; // Spread across bottom of screen
slot.y = 2400; // Position fully visible within game area
slot.visible = true;
slot.alpha = 1.0; // Ensure fully visible
slot.slotIndex = i; // Ensure slot index is set
// CRITICAL: Force interactivity AFTER positioning
slot.interactive = true;
slot.buttonMode = true; // Enable button mode for better touch handling
// Clear existing children first to avoid duplication
while (slot.children.length > 0) {
slot.children[0].destroy();
}
// CRITICAL: FORCE SPELL ID ASSIGNMENT FROM UNIFIED DECK
// GARANTIZAR que cada slot recibe un spell ID específico SIEMPRE
var spellToAssign = null;
if (i < unifiedDeck.length && unifiedDeck[i]) {
spellToAssign = unifiedDeck[i];
console.log('UNIFIED DECK ASSIGNED: Slot', i, 'spellId:', spellToAssign);
} else {
// FORCE DEFAULT SPELL ASSIGNMENT FOR EMPTY SLOTS
var defaultSpells = ['fireball', 'heal', 'lightning'];
if (i < defaultSpells.length) {
spellToAssign = defaultSpells[i];
console.log('DEFAULT ASSIGNED: Slot', i, 'spellId:', spellToAssign);
}
}
// CRITICAL: NEVER ALLOW NULL SPELL IDS - ALWAYS ASSIGN SOMETHING
if (!spellToAssign) {
var fallbackSpells = ['fireball', 'heal', 'lightning'];
spellToAssign = fallbackSpells[i % fallbackSpells.length];
console.log('FALLBACK ASSIGNED: Slot', i, 'spellId:', spellToAssign);
}
// FORCE ASSIGNMENT REGARDLESS OF VALIDATION
slot.spellId = spellToAssign;
console.log('FINAL ASSIGNMENT: Slot', i, 'spellId:', slot.spellId);
// VALIDATE SPELL EXISTS for visual setup only
var spell = _getSpell(spellToAssign);
if (spell) {
// SUCCESSFUL SPELL ASSIGNMENT
slot.tint = 0xFFFFFF;
console.log('SUCCESS: Validated spell for slot', i, ':', spellToAssign, spell.name);
// Create spell icon
var spellIcon = LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5
});
spellIcon.tint = _getRarityColor(spell.rarity);
slot.addChild(spellIcon);
// Add spell name text
var spellNameText = new Text2(spell.name, {
size: 30,
fill: 0xFFFFFF,
font: "monospace"
});
spellNameText.anchor.set(0.5, 0.5);
spellNameText.y = 40;
slot.addChild(spellNameText);
} else {
// SPELL NOT FOUND - FORCE FIREBALL AS LAST RESORT
console.log('ERROR: Spell not found for ID:', spellToAssign, 'forcing fireball for slot', i);
slot.spellId = 'fireball';
console.log('EMERGENCY FALLBACK: Slot', i, 'forced to fireball');
slot.alpha = 0.8;
slot.tint = 0xFFAA00; // Orange tint for fallback spells
}
// FORCE BRING TO FRONT FOR PROPER EVENT HANDLING
game.addChild(slot); // Re-add to ensure it's on top
// ASSIGN PROPER EVENT HANDLER AFTER FULL INITIALIZATION
slot.down = function (x, y, obj) {
console.log('=== SPELL SLOT CLICKED (FORCE MODE) ===');
console.log('Slot index:', obj.slotIndex);
console.log('Spell ID:', obj.spellId);
// IMMEDIATE SUCCESS FEEDBACK - NO VALIDATIONS
LK.effects.flashObject(obj, 0x00FF00, 500);
LK.effects.flashScreen(0x00FF00, 200);
// Get spell ID (use fireball as default)
var effectiveSpellId = obj.spellId || 'fireball';
// EXECUTE SPELL EFFECTS DIRECTLY - NO CHECKS
if (effectiveSpellId === 'fireball') {
// Find closest enemy for fireball
var closestEnemy = null;
var closestDistance = Infinity;
if (wizard && collisionArrayPool && collisionArrayPool.getAllEnemies) {
var allEnemies = collisionArrayPool.getAllEnemies();
for (var e = 0; e < allEnemies.length; e++) {
var enemy = allEnemies[e];
if (enemy.isDying) continue;
var dx = enemy.x - wizard.x;
var dy = enemy.y - wizard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
}
// Create fireball projectile toward closest enemy
if (closestEnemy) {
var fireball = ProjectileFactory.createProjectile('fireBall', wizard.x, wizard.y, closestEnemy.x, closestEnemy.y, {
targetEnemy: closestEnemy,
damage: 150
});
LK.getSound('fireWhoosh').play();
}
// Always create visual effect even if no enemies
var fireEffect = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: closestEnemy ? closestEnemy.x : wizard.x,
y: closestEnemy ? closestEnemy.y : wizard.y - 100,
scaleX: 2,
scaleY: 2
}));
fireEffect.tint = 0xFF4500;
tween(fireEffect, {
scaleX: 4,
scaleY: 4,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
if (fireEffect.parent) fireEffect.destroy();
}
});
} else if (effectiveSpellId === 'heal') {
// Heal wizard immediately
if (wizard) {
var healAmount = 50;
var oldHealth = wizard.health;
wizard.health = Math.min(wizard.health + healAmount, wizard.maxHealth);
var actualHealing = wizard.health - oldHealth;
if (typeof updateHealthBar === 'function') updateHealthBar();
LK.effects.flashObject(wizard, 0x00FF00, 800);
// Create healing visual effect
var healEffect = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: wizard.x,
y: wizard.y,
scaleX: 3,
scaleY: 3
}));
healEffect.tint = 0x00FF00;
tween(healEffect, {
scaleX: 6,
scaleY: 6,
alpha: 0
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (healEffect.parent) healEffect.destroy();
}
});
}
} else if (effectiveSpellId === 'lightning') {
// Lightning chain attack - instantaneous effect
LK.effects.flashScreen(0x00FFFF, 300);
LK.getSound('iceFreeze').play();
// Find up to 3 closest enemies for lightning chain
if (wizard && collisionArrayPool && collisionArrayPool.getAllEnemies) {
var allEnemies = collisionArrayPool.getAllEnemies();
var livingEnemies = [];
for (var e = 0; e < allEnemies.length; e++) {
if (!allEnemies[e].isDying) {
livingEnemies.push(allEnemies[e]);
}
}
var targetsHit = Math.min(3, livingEnemies.length);
if (targetsHit > 0) {
// Create instantaneous chain lightning effects
var lightningTargets = livingEnemies.slice(0, targetsHit);
// Deal damage to all targets immediately
for (var i = 0; i < lightningTargets.length; i++) {
var enemy = lightningTargets[i];
enemy.takeDamage(200);
LK.effects.flashObject(enemy, 0x00FFFF, 400);
// Create lightning impact effect on each enemy
var lightningImpact = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x,
y: enemy.y,
scaleX: 2.5,
scaleY: 2.5
}));
lightningImpact.tint = 0x00FFFF;
lightningImpact.alpha = 0.9;
tween(lightningImpact, {
scaleX: 5,
scaleY: 5,
alpha: 0,
rotation: Math.PI * 2
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
if (lightningImpact.parent) lightningImpact.destroy();
}
});
}
// Create chain lightning visual effects between wizard and enemies
var previousPoint = {
x: wizard.x,
y: wizard.y
};
for (var i = 0; i < lightningTargets.length; i++) {
var currentEnemy = lightningTargets[i];
// Create lightning bolt between previous point and current enemy
var dx = currentEnemy.x - previousPoint.x;
var dy = currentEnemy.y - previousPoint.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var lightningBolt = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: (previousPoint.x + currentEnemy.x) / 2,
y: (previousPoint.y + currentEnemy.y) / 2,
scaleX: 1.5,
scaleY: distance / 50
}));
lightningBolt.tint = 0x00FFFF;
lightningBolt.alpha = 0.8;
lightningBolt.rotation = Math.atan2(dy, dx) + Math.PI / 2;
// Animate lightning bolt
tween(lightningBolt, {
alpha: 0,
scaleX: 0.5
}, {
duration: 400 + i * 100,
easing: tween.easeOut,
onFinish: function onFinish() {
if (lightningBolt.parent) lightningBolt.destroy();
}
});
// Update previous point for next chain segment
previousPoint = {
x: currentEnemy.x,
y: currentEnemy.y
};
}
}
}
}
// Always show success message
var castMessage = new Text2('HECHIZO LANZADO!', {
size: 50,
fill: 0x00FF00,
font: "monospace"
});
castMessage.anchor.set(0.5, 0.5);
castMessage.x = wizard ? wizard.x : 1024;
castMessage.y = wizard ? wizard.y - 150 : 1000;
game.addChild(castMessage);
tween(castMessage, {
y: castMessage.y - 100,
alpha: 0
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (castMessage.parent) castMessage.destroy();
}
});
// Always play spell cast sound
LK.getSound('spellCast').play();
console.log('FORCE MODE spell cast completed successfully!');
};
}
// PASO 8: Inicializar sistema de maná unificado
initializeManaSystem();
// Verificar que la inicialización fue exitosa
console.log('=== POST-INITIALIZATION VERIFICATION ===');
console.log('currentMana:', currentMana);
console.log('activeSpellDeck.currentMana:', activeSpellDeck ? activeSpellDeck.currentMana : 'undefined');
console.log('maxMana:', maxMana);
// IMMEDIATE MANA BAR UPDATE WITH FORCED VALUES
updateManaBar();
// DEBUG: Verify mana values after initialization
console.log('=== MANA FORCE INITIALIZATION ===');
console.log('currentMana:', currentMana);
console.log('activeSpellDeck.currentMana:', activeSpellDeck.currentMana);
console.log('maxMana:', maxMana);
console.log('Mana bar updated successfully');
// FORCE SPELL SLOT UPDATE with proper synchronization
updateSpellSlots();
// ADDITIONAL MANA VERIFICATION - ENSURE MANA STAYS AT 100
tween({}, {}, {
duration: 100,
// Short delay to ensure all systems are initialized
onFinish: function onFinish() {
// FORCE MANA TO 100 AGAIN after all initialization
currentMana = 100;
activeSpellDeck.currentMana = 100;
updateManaBar();
// Final debug verification
console.log('=== FINAL MANA VERIFICATION ===');
console.log('Final currentMana:', currentMana);
console.log('Final activeSpellDeck.currentMana:', activeSpellDeck.currentMana);
console.log('Mana system successfully initialized to 100');
}
});
// FINAL VALIDATION: Verify all spell slots have proper spell IDs
console.log('=== FINAL SPELL SLOT VALIDATION ===');
for (var i = 0; i < spellSlots.length; i++) {
var slot = spellSlots[i];
console.log('Slot', i, 'spellId:', slot.spellId, 'visible:', slot.visible, 'interactive:', slot.interactive);
if (slot.spellId) {
var spell = _getSpell(slot.spellId);
console.log(' - Spell found:', !!spell, spell ? spell.name : 'null');
}
}
// Initialize spell slots with default spells if deck is empty
if (activeSpellDeck.currentDeck.length === 0) {
// Add default spells to deck
activeSpellDeck.currentDeck = ['fireball', 'heal', 'lightning'];
storage.spellDeck = activeSpellDeck.currentDeck.slice();
// Update spell slots with default spells
for (var i = 0; i < Math.min(3, activeSpellDeck.currentDeck.length); i++) {
var slot = spellSlots[i];
var spellId = activeSpellDeck.currentDeck[i];
var spell = activeSpellDeck.getSpell(spellId);
if (spell) {
slot.spellId = spellId;
slot.alpha = 1.0;
slot.tint = 0xFFFFFF;
// Create spell icon
if (slot.children.length === 0) {
var spellIcon = LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5
});
slot.addChild(spellIcon);
}
// Update spell icon
if (slot.children.length > 0) {
var spellIcon = slot.children[0];
spellIcon.tint = activeSpellDeck.getRarityColor(spell.rarity);
}
}
}
}
// 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;
});
// Knight class - armored enemy with high health
var Knight = Container.expand(function () {
var self = Container.call(this);
self.enemyType = 'knight';
self.currentFrame = 1;
self.animationTimer = 0;
self.animationState = 'walking';
self.frozen = false;
self.frozenTimer = 0;
self.isDying = false;
self.lastX = 0;
self.speedTweenStarted = false;
self.health = 300;
self.speed = 2;
self.damage = 40;
self.maxHealth = self.health;
self.animationSpeed = 20;
// Create animation frames
self.animationFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset('knight' + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 3.0,
scaleY: 3.0
});
frameGraphics.visible = i === 1;
self.animationFrames.push(frameGraphics);
}
// Optimized animation system
self.updateAnimation = function () {
self.animationTimer++;
var frameSpeed = self.animationSpeed;
if (self.animationState === 'attacking') frameSpeed = Math.floor(frameSpeed * 0.5);else if (self.animationState === 'dying') frameSpeed = Math.floor(frameSpeed * 1.3);else if (self.animationState === 'idle') frameSpeed = Math.floor(frameSpeed * 1.7);
if (self.animationTimer >= frameSpeed) {
self.animationTimer = 0;
self.animationFrames[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.animationFrames[self.currentFrame - 1].visible = true;
}
};
self.update = function () {
if (tutorial && tutorial.isActive || self.isDying) return;
// Animation and speed progression
self.updateAnimation();
if (!self.speedTweenStarted) {
self.speedTweenStarted = true;
tween(self, {
speed: self.speed * 1.5
}, {
duration: 10000,
easing: tween.easeOut
});
}
// Handle frozen state
if (self.frozen) {
self.frozenTimer--;
if (self.frozenTimer <= 0) self.frozen = false;
return;
}
// Enhanced movement toward wizard
if (wizard) {
var dx = wizard.x - self.x;
var dy = wizard.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var speedMult = self.timeSlowed ? self.timeSlowAmount : 1.0;
self.x += dx / distance * self.speed * speedMult;
self.y += dy / distance * self.speed * speedMult;
var flipScale = dx < 0 ? -3.0 : 3.0;
for (var frameIdx = 0; frameIdx < self.animationFrames.length; frameIdx++) {
self.animationFrames[frameIdx].scaleX = flipScale;
}
} else {
self.y += self.speed;
}
}
};
self.takeDamage = function (damage) {
self.health -= damage;
self.animationState = 'attacking';
// Create damage text
var damageText = new Text2('-' + damage, {
size: 120,
fill: 0xFF4444,
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);
var startY = damageText.y;
tween(damageText, {
y: startY - 120,
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
damageText.destroy();
}
});
// Flash effect
LK.effects.flashObject(self, 0xFF0000, 300);
// Return to walking state after brief animation
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 (tutorial && tutorial.isActive || self.isDying) return;
if (wizard && wizard.attackCooldown > 0) {
LK.effects.flashObject(self, 0xFF0000, 200);
return;
}
// Vibration feedback
if (typeof LK.vibrate === 'function') {
LK.vibrate([150]);
}
selectedEnemy = self;
LK.effects.flashObject(self, 0xFFFF00, 500);
if (wizard && projectiles.length < 10) {
var projectile = ProjectileFactory.createBasicAttack(wizard, self);
projectile.targetEnemy = self;
projectiles.push(projectile);
LK.getSound('spellCast').play();
wizard.attackCooldown = 30;
}
};
self.die = function () {
if (globalDeathHandler) {
globalDeathHandler.executeEnemyDeath(self, knights);
}
};
return self;
});
// MiniBoss class - powerful boss enemy with health bar
var MiniBoss = Container.expand(function () {
var self = Container.call(this);
self.enemyType = 'miniBoss';
self.currentFrame = 1;
self.animationTimer = 0;
self.animationState = 'walking';
self.frozen = false;
self.frozenTimer = 0;
self.isDying = false;
self.lastX = 0;
self.speedTweenStarted = false;
self.health = 3000;
self.speed = 4;
self.damage = 75;
self.maxHealth = self.health;
self.animationSpeed = 12;
// Create animation frames
self.animationFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset('ogre' + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 5.0,
scaleY: 5.0
});
frameGraphics.visible = i === 1;
frameGraphics.tint = 0x8B0000;
self.animationFrames.push(frameGraphics);
}
// Create health bar for mini boss
self.healthBarBg = game.addChild(LK.getAsset('miniBossHealthBarBg', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 300,
scaleX: 8,
scaleY: 2
}));
self.healthBarFg = game.addChild(LK.getAsset('miniBossHealthBar', {
anchorX: 0.0,
anchorY: 0.5,
x: 2048 / 2 - 200,
y: 300,
scaleX: 8,
scaleY: 2
}));
self.healthText = new Text2('Boss Health: ' + self.health + '/' + self.maxHealth, {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
self.healthText.anchor.set(0.5, 0.5);
self.healthText.x = 2048 / 2;
self.healthText.y = 250;
game.addChild(self.healthText);
// Update health bar for mini boss
self.updateHealthBar = function () {
if (!self.healthBarFg) return;
var healthPercent = self.health / self.maxHealth;
self.healthBarFg.scaleX = healthPercent * 8;
self.healthText.setText('Boss Health: ' + self.health + '/' + self.maxHealth);
if (healthPercent > 0.6) {
self.healthBarFg.tint = 0xff0000;
} else if (healthPercent > 0.3) {
self.healthBarFg.tint = 0xff4500;
} else {
self.healthBarFg.tint = 0x8B0000;
}
};
// Optimized animation system
self.updateAnimation = function () {
self.animationTimer++;
var frameSpeed = self.animationSpeed;
if (self.animationState === 'attacking') frameSpeed = Math.floor(frameSpeed * 0.5);else if (self.animationState === 'dying') frameSpeed = Math.floor(frameSpeed * 1.3);else if (self.animationState === 'idle') frameSpeed = Math.floor(frameSpeed * 1.7);
if (self.animationTimer >= frameSpeed) {
self.animationTimer = 0;
self.animationFrames[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.animationFrames[self.currentFrame - 1].visible = true;
}
};
self.update = function () {
if (tutorial && tutorial.isActive || self.isDying) return;
// Animation and speed progression
self.updateAnimation();
if (!self.speedTweenStarted) {
self.speedTweenStarted = true;
tween(self, {
speed: self.speed * 1.5
}, {
duration: 10000,
easing: tween.easeOut
});
}
// Handle frozen state
if (self.frozen) {
self.frozenTimer--;
if (self.frozenTimer <= 0) self.frozen = false;
return;
}
// Enhanced movement toward wizard
if (wizard) {
var dx = wizard.x - self.x;
var dy = wizard.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var speedMult = self.timeSlowed ? self.timeSlowAmount : 1.0;
self.x += dx / distance * self.speed * speedMult;
self.y += dy / distance * self.speed * speedMult;
var flipScale = dx < 0 ? -5.0 : 5.0;
for (var frameIdx = 0; frameIdx < self.animationFrames.length; frameIdx++) {
self.animationFrames[frameIdx].scaleX = flipScale;
}
} else {
self.y += self.speed;
}
}
};
self.takeDamage = function (damage) {
self.health -= damage;
self.animationState = 'attacking';
// Create damage text
var damageText = new Text2('-' + damage, {
size: 120,
fill: 0xFF4444,
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);
var startY = damageText.y;
tween(damageText, {
y: startY - 120,
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
damageText.destroy();
}
});
// Flash effect
LK.effects.flashObject(self, 0xFF0000, 300);
// Return to walking state after brief animation
tween({}, {}, {
duration: 300,
onFinish: function onFinish() {
if (self.animationState === 'attacking') self.animationState = 'walking';
}
});
self.updateHealthBar();
if (self.health <= 0) self.die();
};
self.down = function (x, y, obj) {
if (tutorial && tutorial.isActive || self.isDying) return;
if (wizard && wizard.attackCooldown > 0) {
LK.effects.flashObject(self, 0xFF0000, 200);
return;
}
// Vibration feedback
if (typeof LK.vibrate === 'function') {
LK.vibrate([300, 100, 300]);
}
selectedEnemy = self;
LK.effects.flashObject(self, 0xFFFF00, 500);
if (wizard && projectiles.length < 10) {
var projectile = ProjectileFactory.createBasicAttack(wizard, self);
projectile.targetEnemy = self;
projectiles.push(projectile);
LK.getSound('spellCast').play();
wizard.attackCooldown = 30;
}
};
self.die = function () {
if (globalDeathHandler) {
globalDeathHandler.executeEnemyDeath(self, miniBosses);
}
};
return self;
});
// Ogre class - stronger enemy with more health
var Ogre = Container.expand(function () {
var self = Container.call(this);
self.enemyType = 'ogre';
self.currentFrame = 1;
self.animationTimer = 0;
self.animationState = 'walking';
self.frozen = false;
self.frozenTimer = 0;
self.isDying = false;
self.lastX = 0;
self.speedTweenStarted = false;
self.health = 200;
self.speed = 2.5;
self.damage = 30;
self.maxHealth = self.health;
self.animationSpeed = 18;
// Create animation frames
self.animationFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset('ogre' + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 3.5,
scaleY: 3.5
});
frameGraphics.visible = i === 1;
self.animationFrames.push(frameGraphics);
}
// Optimized animation system
self.updateAnimation = function () {
self.animationTimer++;
var frameSpeed = self.animationSpeed;
if (self.animationState === 'attacking') frameSpeed = Math.floor(frameSpeed * 0.5);else if (self.animationState === 'dying') frameSpeed = Math.floor(frameSpeed * 1.3);else if (self.animationState === 'idle') frameSpeed = Math.floor(frameSpeed * 1.7);
if (self.animationTimer >= frameSpeed) {
self.animationTimer = 0;
self.animationFrames[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.animationFrames[self.currentFrame - 1].visible = true;
}
};
self.update = function () {
if (tutorial && tutorial.isActive || self.isDying) return;
// Animation and speed progression
self.updateAnimation();
if (!self.speedTweenStarted) {
self.speedTweenStarted = true;
tween(self, {
speed: self.speed * 1.5
}, {
duration: 10000,
easing: tween.easeOut
});
}
// Handle frozen state
if (self.frozen) {
self.frozenTimer--;
if (self.frozenTimer <= 0) self.frozen = false;
return;
}
// Enhanced movement toward wizard
if (wizard) {
var dx = wizard.x - self.x;
var dy = wizard.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var speedMult = self.timeSlowed ? self.timeSlowAmount : 1.0;
self.x += dx / distance * self.speed * speedMult;
self.y += dy / distance * self.speed * speedMult;
var flipScale = dx < 0 ? -3.5 : 3.5;
for (var frameIdx = 0; frameIdx < self.animationFrames.length; frameIdx++) {
self.animationFrames[frameIdx].scaleX = flipScale;
}
} else {
self.y += self.speed;
}
}
};
self.takeDamage = function (damage) {
self.health -= damage;
self.animationState = 'attacking';
// Create damage text
var damageText = new Text2('-' + damage, {
size: 120,
fill: 0xFF4444,
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);
var startY = damageText.y;
tween(damageText, {
y: startY - 120,
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
damageText.destroy();
}
});
// Flash effect
LK.effects.flashObject(self, 0xFF0000, 300);
// Return to walking state after brief animation
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 (tutorial && tutorial.isActive || self.isDying) return;
if (wizard && wizard.attackCooldown > 0) {
LK.effects.flashObject(self, 0xFF0000, 200);
return;
}
// Vibration feedback
if (typeof LK.vibrate === 'function') {
LK.vibrate([200]);
}
selectedEnemy = self;
LK.effects.flashObject(self, 0xFFFF00, 500);
if (wizard && projectiles.length < 10) {
var projectile = ProjectileFactory.createBasicAttack(wizard, self);
projectile.targetEnemy = self;
projectiles.push(projectile);
LK.getSound('spellCast').play();
wizard.attackCooldown = 30;
}
};
self.die = function () {
if (globalDeathHandler) {
globalDeathHandler.executeEnemyDeath(self, ogres);
}
};
return self;
});
// Simple object creation - no pooling needed for this game scope
// All enemy types now use the unified Enemy class with type parameter
// Ogre, Knight, and MiniBoss classes removed - now handled by Enemy(type) constructor
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
// Category 3: Orb-enemy collisions (separate from projectile-enemy and enemy-wizard)
self.processOrbEnemyCollisions = function () {
var allEnemies = collisionArrayPool.getAllEnemies();
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;
}
// 1.1 Distance Culling: Quick distance check before expensive collision detection
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var maxOrbRange = 80; // Orb collision range
var currentIntersecting = false;
if (distance <= maxOrbRange) {
// Only perform expensive intersection test if enemy is close enough
currentIntersecting = self.intersects(enemy);
}
if (!self.lastIntersecting[i] && currentIntersecting) {
// Deal damage to enemy on contact transition (first contact only)
enemy.takeDamage(200);
// 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;
}
};
self.update = function () {
// Pause orb when tutorial is active
if (tutorial && tutorial.isActive) {
return;
}
// Upgrade menu removed - no pause needed
// 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;
// Category 3: Orb-enemy collisions (separate from projectile-enemy and enemy-wizard)
self.processOrbEnemyCollisions();
};
return self;
});
// Create global death handler instance
var Projectile = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'projectile';
self.speed = 50;
self.direction = {
x: 0,
y: 0
};
self.targetEnemy = null;
self.damage = 100;
self.hitEnemy = false;
// Define local projectile configurations since GAME_CONFIG is not available yet
var projectileConfigs = {
projectile: {
assetId: 'projectile',
scale: 1.5,
speed: 50,
damage: 100,
tint: 0x44aaff,
glowTint: 0x44aaff,
hasRotation: true
},
energyBeam: {
assetId: 'projectileGlow',
scale: 1.0,
speed: 60,
damage: 100,
tint: 0x00ffff,
glowTint: 0x00ffff,
hasRotation: true
},
fireBall: {
assetId: 'projectileGlow',
scale: 1.5,
speed: 40,
damage: 150,
tint: 0xFF4500,
glowTint: 0xFF6600,
hasRotation: true,
hasFlicker: true
}
};
// Get configuration from local configs
var config = projectileConfigs[self.type];
if (!config) {
// Fallback to projectile config
config = projectileConfigs.projectile;
}
self.speed = config.speed;
self.damage = config.damage;
// Create graphics based on type
var assetId = config.assetId;
self.graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: config.scale,
scaleY: config.scale
});
self.graphics.tint = config.tint;
// Add glow for basic projectiles
if (self.type === 'projectile') {
var glow = self.attachAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.0,
scaleY: 2.0
});
glow.alpha = 0.3;
glow.tint = config.glowTint;
}
self.update = function () {
if (tutorial && tutorial.isActive) return;
// Move projectile
self.x += self.direction.x * self.speed;
self.y += self.direction.y * self.speed;
// Visual effects
if (config.hasRotation) {
var angle = Math.atan2(self.direction.y, self.direction.x);
self.graphics.rotation = angle + Math.PI / 2;
}
if (config.hasFlicker) {
var flicker = 1 + Math.sin(LK.ticks * 0.4) * 0.3;
self.graphics.scaleX = config.scale * flicker;
self.graphics.scaleY = config.scale * flicker;
}
// Remove if off screen
if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) {
// Remove from projectiles array before destroying
ProjectileFactory.removeProjectile(self);
return;
}
// Handle collisions
self.checkCollisions();
};
self.checkCollisions = function () {
if (self.hitEnemy) return;
// Category 1: Area damage projectiles (fireball)
if (self.type === 'fireBall') {
self.processAreaDamageCollisions();
} else {
// Category 2: Single target projectiles
self.processSingleTargetCollisions();
}
};
// Separate collision processing for area damage projectiles
self.processAreaDamageCollisions = function () {
var allEnemies = collisionArrayPool.getAllEnemies();
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
if (enemy === wizard || enemy.isDying) continue;
// 1.1 Distance Culling: Quick distance check before expensive collision detection
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var maxFireballRange = 120; // Fireball collision range including area effect
// Only perform expensive intersection test if enemy is within range
if (distance <= maxFireballRange) {
if (self.intersects(enemy)) {
enemy.takeDamage(self.damage);
self.createExplosion(enemy);
self.hitEnemy = true;
// Remove from projectiles array before destroying
ProjectileFactory.removeProjectile(self);
return;
}
}
}
};
// Separate collision processing for single target projectiles
self.processSingleTargetCollisions = function () {
if (self.targetEnemy && self.targetEnemy.parent && !self.targetEnemy.isDying) {
// 1.1 Distance Culling: Quick distance check before expensive collision detection
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var maxProjectileRange = 100; // Standard projectile collision range
// Only perform expensive intersection test if target enemy is within range
if (distance <= maxProjectileRange) {
if (self.intersects(self.targetEnemy)) {
self.targetEnemy.takeDamage(self.damage);
self.hitEnemy = true;
// Remove from projectiles array before destroying
ProjectileFactory.removeProjectile(self);
return;
}
}
} else {
// Target is invalid, remove projectile
self.hitEnemy = true;
// Remove from projectiles array before destroying
ProjectileFactory.removeProjectile(self);
return;
}
};
self.createExplosion = function (enemy) {
LK.effects.flashObject(enemy, 0xFF4500, 400);
var explosion = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x,
y: enemy.y,
scaleX: 2,
scaleY: 2
}));
explosion.tint = 0xFF6600;
explosion.alpha = 0.8;
tween(explosion, {
scaleX: 5,
scaleY: 5,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
};
return self;
});
// Simple effects helper for basic visual feedback
// Skeleton class - basic enemy with simple properties
var Skeleton = Container.expand(function () {
var self = Container.call(this);
self.enemyType = 'skeleton';
self.currentFrame = 1;
self.animationTimer = 0;
self.animationState = 'walking';
self.frozen = false;
self.frozenTimer = 0;
self.isDying = false;
self.lastX = 0;
self.speedTweenStarted = false;
self.health = 100;
self.speed = 3;
self.damage = 20;
self.maxHealth = self.health;
self.animationSpeed = 15;
// Create animation frames
self.animationFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset('esqueleto' + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 3.0,
scaleY: 3.0
});
frameGraphics.visible = i === 1;
self.animationFrames.push(frameGraphics);
}
// Optimized animation system
self.updateAnimation = function () {
self.animationTimer++;
var frameSpeed = self.animationSpeed;
if (self.animationState === 'attacking') frameSpeed = Math.floor(frameSpeed * 0.5);else if (self.animationState === 'dying') frameSpeed = Math.floor(frameSpeed * 1.3);else if (self.animationState === 'idle') frameSpeed = Math.floor(frameSpeed * 1.7);
if (self.animationTimer >= frameSpeed) {
self.animationTimer = 0;
self.animationFrames[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.animationFrames[self.currentFrame - 1].visible = true;
}
};
self.update = function () {
if (tutorial && tutorial.isActive || self.isDying) return;
// Animation and speed progression
self.updateAnimation();
if (!self.speedTweenStarted) {
self.speedTweenStarted = true;
tween(self, {
speed: self.speed * 1.5
}, {
duration: 10000,
easing: tween.easeOut
});
}
// Handle frozen state
if (self.frozen) {
self.frozenTimer--;
if (self.frozenTimer <= 0) self.frozen = false;
return;
}
// Enhanced movement toward wizard
if (wizard) {
var dx = wizard.x - self.x;
var dy = wizard.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var speedMult = self.timeSlowed ? self.timeSlowAmount : 1.0;
self.x += dx / distance * self.speed * speedMult;
self.y += dy / distance * self.speed * speedMult;
var flipScale = dx < 0 ? -3.0 : 3.0;
for (var frameIdx = 0; frameIdx < self.animationFrames.length; frameIdx++) {
self.animationFrames[frameIdx].scaleX = flipScale;
}
} else {
self.y += self.speed;
}
}
};
self.takeDamage = function (damage) {
self.health -= damage;
self.animationState = 'attacking';
// Create damage text
var damageText = new Text2('-' + damage, {
size: 120,
fill: 0xFF4444,
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);
var startY = damageText.y;
tween(damageText, {
y: startY - 120,
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
damageText.destroy();
}
});
// Flash effect
LK.effects.flashObject(self, 0xFF0000, 300);
// Return to walking state after brief animation
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 (tutorial && tutorial.isActive || self.isDying) return;
if (wizard && wizard.attackCooldown > 0) {
LK.effects.flashObject(self, 0xFF0000, 200);
return;
}
// Vibration feedback
if (typeof LK.vibrate === 'function') {
LK.vibrate([100]);
}
selectedEnemy = self;
LK.effects.flashObject(self, 0xFFFF00, 500);
if (wizard && projectiles.length < 10) {
var projectile = ProjectileFactory.createBasicAttack(wizard, self);
projectile.targetEnemy = self;
projectiles.push(projectile);
LK.getSound('spellCast').play();
wizard.attackCooldown = 30;
}
};
self.die = function () {
if (globalDeathHandler) {
globalDeathHandler.executeEnemyDeath(self, enemies);
}
};
return self;
});
// Simple SpellDeck class to replace the complex system
var SpellDeck = Container.expand(function () {
var self = Container.call(this);
// Use the existing simple spell system
self.currentDeck = storage.spellDeck || ['fireball', 'heal', 'lightning'];
self.availableSpells = availableSpells; // Reference to global availableSpells
// Simple methods to match the interface expected by GameMenu
self.addToDeck = function (spellId) {
if (self.currentDeck.length >= 5) return false;
if (self.currentDeck.indexOf(spellId) !== -1) return false;
self.currentDeck.push(spellId);
storage.spellDeck = self.currentDeck.slice();
return true;
};
self.removeFromDeck = function (spellId) {
var index = self.currentDeck.indexOf(spellId);
if (index === -1) return false;
self.currentDeck.splice(index, 1);
storage.spellDeck = self.currentDeck.slice();
return true;
};
self.getSpell = function (spellId) {
return _getSpell(spellId);
};
self.getRarityColor = function (rarity) {
return _getRarityColor(rarity);
};
self.unlockSpell = function (spellId) {
// Simple unlock system - spells are always available
return true;
};
return self;
});
// Simple spell system - no complex classes needed
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. Toca directamente sobre los enemigos para atacarlos con hechizos.',
duration: 3000,
showSkip: true,
highlightElement: 'screen',
waitForTap: true
}, {
id: 'enemies_approach',
title: 'ENEMIGOS Y MEJORAS',
description: 'Los esqueletos vienen por 5 caminos diferentes. Toca directamente sobre cada enemigo para atacarlo. Gana monedas y desbloquea mejoras cada 12 enemigos.',
duration: 3000,
spawnDemoEnemy: true,
highlightElement: 'coinCounter'
}, {
id: 'tutorial_complete',
title: 'TUTORIAL COMPLETADO!',
description: 'Recuerda: toca directamente sobre los enemigos para atacarlos. Cuida tu salud y sobrevive el mayor tiempo posible. ¡Buena suerte!',
duration: 3000,
startGame: true,
highlightElement: 'healthBar'
}];
};
// 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 Skeleton());
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.animationFrames.length; frameIdx++) {
demoEnemy.animationFrames[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
// Add interaction handler for spell cards in deck view
self.down = function (x, y, obj) {
// Only handle interactions if deck is active or if this is tutorial
if (self.deckMode) {
// Check deck card clicks for spell casting (from current deck)
for (var i = 0; i < self.deckElements.length; i++) {
var element = self.deckElements[i];
if (element.spellId && element.isDeckCard) {
var cardX = element.x;
var cardY = element.y;
if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) {
// Cast the spell instead of removing it
if (activeSpellDeck && activeSpellDeck.canCastSpell(element.spellId)) {
var spell = activeSpellDeck.getSpell(element.spellId);
if (spell) {
var targetX = wizard.x;
var targetY = wizard.y - 100;
// Find nearest enemy for targeted spells
if (spell.id === 'fireball' || spell.id === 'iceShard' || spell.id === 'lightning') {
var allEnemies = collisionArrayPool.getAllEnemies();
var nearestEnemy = null;
var nearestDistance = Infinity;
for (var e = 0; e < allEnemies.length; e++) {
var enemy = allEnemies[e];
var dx = enemy.x - wizard.x;
var dy = enemy.y - wizard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestEnemy = enemy;
}
}
if (nearestEnemy) {
targetX = nearestEnemy.x;
targetY = nearestEnemy.y;
}
}
// Cast the spell
activeSpellDeck.castSpell(element.spellId, targetX, targetY);
LK.effects.flashObject(element, 0x00FF00, 200);
// Show cast message
var castText = new Text2('HECHIZO LANZADO!', {
size: 60,
fill: 0x00FF00,
font: "monospace"
});
castText.anchor.set(0.5, 0.5);
castText.x = 2048 / 2;
castText.y = 2200;
self.addChild(castText);
// Animate and remove message
tween(castText, {
alpha: 0,
y: castText.y - 100
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (castText.parent) castText.destroy();
}
});
}
} else {
LK.effects.flashObject(element, 0xFF0000, 200);
// Show "cannot cast" message
var errorText = new Text2('NO SE PUEDE LANZAR', {
size: 50,
fill: 0xFF6666,
font: "monospace"
});
errorText.anchor.set(0.5, 0.5);
errorText.x = 2048 / 2;
errorText.y = 2200;
self.addChild(errorText);
// Animate and remove message
tween(errorText, {
alpha: 0,
y: errorText.y - 100
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (errorText.parent) errorText.destroy();
}
});
}
return;
}
}
}
// Continue with existing available cards handling...
// Check available card clicks with better hit detection
for (var i = 0; i < self.availableElements.length; i++) {
var element = self.availableElements[i];
if (element.spellId && !element.isDeckCard) {
var cardX = element.x;
var cardY = element.y;
if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) {
// Visual feedback before addition
LK.effects.flashObject(element, 0x00FF00, 300);
// Add to deck
if (self.spellDeck.addToDeck(element.spellId)) {
self.refreshDeckDisplay();
LK.effects.flashScreen(0x00FF00, 200);
// Show addition message
var addText = new Text2('HECHIZO AÑADIDO', {
size: 60,
fill: 0x66FF66,
font: "monospace"
});
addText.anchor.set(0.5, 0.5);
addText.x = 2048 / 2;
addText.y = 2200;
self.addChild(addText);
// Animate and remove message
tween(addText, {
alpha: 0,
y: addText.y - 100
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (addText.parent) addText.destroy();
}
});
} else {
LK.effects.flashScreen(0xFF0000, 200);
// Show error message
var errorText = new Text2('DECK LLENO (MAX 5)', {
size: 50,
fill: 0xFF6666,
font: "monospace"
});
errorText.anchor.set(0.5, 0.5);
errorText.x = 2048 / 2;
errorText.y = 2200;
self.addChild(errorText);
// Animate and remove message
tween(errorText, {
alpha: 0,
y: errorText.y - 100
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (errorText.parent) errorText.destroy();
}
});
}
return;
}
}
}
// Deck back button
if (y >= 2350 && y <= 2650) {
self.hideDeck();
}
// Block all other interactions when deck menu is active
return;
}
// Tutorial handling (moved after deck handling)
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;
});
// Impact effect function now handled by BaseDamageHandler
// Unified death handler for all enemy types using enemy configuration
var UnifiedDeathHandler = Container.expand(function () {
var self = Container.call(this);
// Execute enemy death with consolidated coin and reward logic
self.executeEnemyDeath = function (enemy, enemyArray) {
enemy.animationState = 'dying';
enemy.currentFrame = 3;
LK.getSound('painSound').play();
enemy.isDying = true;
// Use direct enemy configuration values based on type
var deathRotation = Math.PI * 0.5;
var numCoins = 1;
// Set specific values based on enemy type
if (enemy.enemyType === 'miniBoss') {
deathRotation = Math.PI * 0.8;
numCoins = 5;
} else if (enemy.enemyType === 'ogre') {
deathRotation = Math.PI * 0.6;
numCoins = 1;
} else if (enemy.enemyType === 'knight') {
deathRotation = Math.PI * 0.7;
numCoins = 1;
}
// Special cleanup for mini boss UI elements
if (enemy.enemyType === 'miniBoss') {
self.cleanupMiniBossUI(enemy);
}
// Execute unified death animation
tween(enemy, {
alpha: 0,
scaleX: 0.3,
scaleY: 0.3,
rotation: deathRotation
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
self.handleDeathRewards(enemy, numCoins);
self.updateProgression(enemy);
self.cleanupEnemy(enemy, enemyArray);
}
});
};
// Clean up mini boss UI elements
self.cleanupMiniBossUI = function (enemy) {
if (enemy.healthBarBg && enemy.healthBarBg.parent) {
enemy.healthBarBg.destroy();
}
if (enemy.healthBarFg && enemy.healthBarFg.parent) {
enemy.healthBarFg.destroy();
}
if (enemy.healthText && enemy.healthText.parent) {
enemy.healthText.destroy();
}
};
// Handle coin drops and difficulty-based rewards using pooled coins
self.handleDeathRewards = function (enemy, numCoins) {
var selectedDifficulty = storage.difficulty || 'NORMAL';
for (var coinIdx = 0; coinIdx < numCoins; coinIdx++) {
// Create coin directly without object pool
var coin = new Coin();
coin.x = enemy.x + (enemy.enemyType === 'miniBoss' ? (Math.random() - 0.5) * 200 : 0);
coin.y = enemy.y - 50 + (enemy.enemyType === 'miniBoss' ? (Math.random() - 0.5) * 100 : 0);
coin.isAnimating = true;
coin.bobOffset = Math.random() * Math.PI * 2;
coin.initialY = 0;
coin.visible = true;
coin.alpha = 1.0;
game.addChild(coin);
coins.push(coin);
var coinTargetX = 120 + coinText.width / 2;
var coinTargetY = 90 + coinText.height / 2;
tween(coin, {
x: coinTargetX,
y: coinTargetY,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 1000 + (enemy.enemyType === 'miniBoss' ? coinIdx * 200 : 0),
easing: tween.easeOut,
onFinish: function onFinish() {
self.processCoinReward(enemy, selectedDifficulty, coin);
}
});
}
};
// Process coin rewards with difficulty modifiers
self.processCoinReward = function (enemy, selectedDifficulty, coin) {
var coinReward = enemy.enemyType === 'miniBoss' ? 10 : 1;
if (selectedDifficulty === 'FACIL') {
coinReward = Math.floor(coinReward * 1.5);
} else if (selectedDifficulty === 'DIFICIL') {
coinReward = Math.max(1, Math.floor(coinReward * 0.75));
}
coinCounter += coinReward;
coinText.setText('Coins: ' + coinCounter);
// Easy difficulty healing bonus
if (selectedDifficulty === 'FACIL' && Math.random() < 0.15) {
wizard.health = Math.min(wizard.health + 5, wizard.maxHealth);
updateHealthBar();
LK.effects.flashObject(wizard, 0x00FF00, 200);
}
// Remove coin from tracking array
for (var i = coins.length - 1; i >= 0; i--) {
if (coins[i] === coin) {
coins.splice(i, 1);
break;
}
}
// Destroy coin directly without object pool
coin.destroy();
};
// Update kill counter and experience progression
self.updateProgression = function (enemy) {
var killIncrement = enemy.enemyType === 'miniBoss' ? 10 : 1;
enemyKillCounter += killIncrement;
killCountText.setText('Puntuacion: ' + enemyKillCounter);
wizard.gainExperience(enemy.enemyType === 'miniBoss' ? 250 : 25);
if (selectedEnemy === enemy) {
selectedEnemy = null;
}
};
// Clean up enemy from arrays and game
self.cleanupEnemy = function (enemy, enemyArray) {
// Remove from appropriate array
for (var i = enemyArray.length - 1; i >= 0; i--) {
if (enemyArray[i] === enemy) {
enemyArray.splice(i, 1);
break;
}
}
// Also remove from global enemy manager collections
globalEnemyManager.removeFromCollection(enemy, enemy.enemyType);
// Remove from all legacy arrays to ensure proper cleanup
var allArrays = [enemies, ogres, knights, miniBosses];
for (var arrayIdx = 0; arrayIdx < allArrays.length; arrayIdx++) {
var array = allArrays[arrayIdx];
for (var i = array.length - 1; i >= 0; i--) {
if (array[i] === enemy) {
array.splice(i, 1);
break;
}
}
}
enemy.destroy();
// Set score reward based on enemy type
var scoreReward = 10;
if (enemy.enemyType === 'miniBoss') {
scoreReward = 100;
} else if (enemy.enemyType === 'ogre') {
scoreReward = 20;
} else if (enemy.enemyType === 'knight') {
scoreReward = 30;
}
LK.setScore(LK.getScore() + scoreReward);
};
return self;
});
// UpgradeMenu class removed - using spell deck system instead
// Unified Projectile Factory using consolidated GAME_CONFIG.projectiles
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.shieldActive = false; // Track shield status
// Upgrade system removed - simplified wizard properties
// 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;
}
// Upgrade menu removed - no pause needed
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;
}
// Upgrade-based abilities removed - using spell deck system instead
// Optimized animation system with performance awareness
self.animationTimer++;
var adjustedSpeed = self.animationSpeed;
// Simple animation speed adjustment without complex performance management
// Skip complex animation manager - use direct speed control
if (self.animationTimer >= adjustedSpeed) {
self.animationTimer = 0;
// Calculate advanced frame transition configuration
var transitionConfig = self.calculateFrameTransitionConfig(adjustedSpeed);
var currentFrame = self.wizardFrames[self.currentFrame - 1];
var nextFrameIndex = self.currentFrame >= 4 ? 0 : self.currentFrame;
var nextFrame = self.wizardFrames[nextFrameIndex];
// Advanced frame transition system with multiple tween effects
if (transitionConfig.useAdvancedTransition) {
self.performAdvancedFrameTransition(currentFrame, nextFrame, transitionConfig);
} else if (transitionConfig.useBasicTransition) {
self.performBasicFrameTransition(currentFrame, nextFrame, transitionConfig);
} else {
// Instant transition for low performance
currentFrame.visible = false;
nextFrame.visible = true;
}
// Move to next frame (walking state)
self.currentFrame++;
if (self.currentFrame > 4) {
self.currentFrame = 1;
}
}
};
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 for wizard attack
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 teleport invulnerability is active
if (self.teleportInvuln) {
GameManager.createFlashEffect(self, 0x8000FF, 200);
return;
}
// 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
GameManager.createFlashEffect(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
GameManager.createFlashEffect(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;
}
// Use unified damage handler for core damage logic
self.health -= damage;
GameManager.createFlashEffect(self, 0xFF0000, 200);
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 = collisionArrayPool.getAllEnemies();
for (var enemyIdx = allEnemies.length - 1; enemyIdx >= 0; enemyIdx--) {
var enemy = allEnemies[enemyIdx];
// Create destruction effect for each enemy
GameManager.createFlashEffect(enemy, 0xFFD700, 500);
// Create golden explosion particles
GameManager.createVisualEffect('explosion', enemy, {
explosionColor: 0xFFD700,
explosionScale: 4.0
});
// Kill ALL enemies instantly by calling die() method
enemy.die();
}
// Visual effects for revival
LK.effects.flashScreen(0x00FF00, 1500); // Green flash for revival
GameManager.createFlashEffect(self, 0xFFD700, 1000); // Golden flash on wizard
// Create healing aura effect
GameManager.createVisualEffect('explosion', self, {
explosionColor: 0x00FF00,
explosionScale: 8.0
});
// Play spell cast sound for revival
LK.getSound('spellCast').play();
// Update health bar to show revival
updateHealthBar();
} 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
});
}
});
};
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 = collisionArrayPool.getAllEnemies();
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 = collisionArrayPool.getAllEnemies();
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 = collisionArrayPool.getAllEnemies();
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
// Find closest enemy to target
var closestEnemy = null;
var closestDistance = Infinity;
var allEnemies = collisionArrayPool.getAllEnemies();
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;
}
}
// Create fire ball projectile using unified factory
var targetX = closestEnemy ? closestEnemy.x : self.x;
var targetY = closestEnemy ? closestEnemy.y : self.y - 100;
var fireBall = ProjectileFactory.createProjectile('fireBall', self.x, self.y, targetX, targetY);
LK.getSound('spellCast').play();
LK.getSound('fireWhoosh').play();
};
// Simple frame transition configuration without complex performance management
self.calculateFrameTransitionConfig = function (adjustedSpeed) {
var config = {
useAdvancedTransition: false,
useBasicTransition: true,
duration: adjustedSpeed * 0.3,
easing: tween.easeOut
};
return config;
};
// Perform advanced frame transition with sophisticated tween effects
self.performAdvancedFrameTransition = function (currentFrame, nextFrame, config) {
// Phase 1: Current frame fade and scale out
tween(currentFrame, {
alpha: 0,
scaleX: currentFrame.scaleX * 0.95,
scaleY: currentFrame.scaleY * 0.95
}, {
duration: config.duration * 0.5,
easing: tween.easeIn,
onFinish: function onFinish() {
currentFrame.visible = false;
currentFrame.alpha = 1;
currentFrame.scaleX = 2.5; // Reset scale
currentFrame.scaleY = 2.5;
}
});
// Phase 2: Next frame scale and fade in
nextFrame.visible = true;
nextFrame.alpha = 0;
nextFrame.scaleX = 2.5 * 1.05; // Start slightly larger
nextFrame.scaleY = 2.5 * 1.05;
tween(nextFrame, {
alpha: 1,
scaleX: 2.5,
scaleY: 2.5
}, {
duration: config.duration * 0.7,
easing: tween.bounceOut,
delay: config.duration * 0.3 // Start after current frame begins fading
});
// Phase 3: Subtle magical sparkle effect for enhanced visual appeal
self.createFrameTransitionSparkle(nextFrame, config);
};
// Perform basic frame transition with smooth fade effect
self.performBasicFrameTransition = function (currentFrame, nextFrame, config) {
// Smooth fade out current frame
tween(currentFrame, {
alpha: 0
}, {
duration: config.duration,
easing: config.easing,
onFinish: function onFinish() {
currentFrame.visible = false;
currentFrame.alpha = 1;
}
});
// Smooth fade in next frame
nextFrame.visible = true;
nextFrame.alpha = 0;
tween(nextFrame, {
alpha: 1
}, {
duration: config.duration,
easing: tween.easeIn
});
};
// Create subtle sparkle effect for frame transitions
self.createFrameTransitionSparkle = function (frame, config) {
var sparkleCount = 3;
for (var i = 0; i < sparkleCount; i++) {
var sparkle = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: frame.x + (Math.random() - 0.5) * 40,
y: frame.y + (Math.random() - 0.5) * 40,
scaleX: 0.1,
scaleY: 0.1
}));
sparkle.tint = 0xFFD700;
sparkle.alpha = 0.8;
// Animate sparkle with delayed start
var delay = i * 100;
tween({}, {}, {
duration: delay,
onFinish: function (sparkleRef) {
return function () {
tween(sparkleRef, {
scaleX: 0.5,
scaleY: 0.5,
alpha: 1.0,
rotation: Math.PI * 2
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(sparkleRef, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
if (sparkleRef.parent) {
sparkleRef.destroy();
}
}
});
}
});
};
}(sparkle)
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000 // Black background for pixel art
});
/****
* Game Code
****/
// Simple spell system - no complex classes needed
/****
* Constants & Configuration
****/
// Unified Game Manager for all simple operations
function _typeof2(o) {
"@babel/helpers - typeof";
return _typeof2 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof2(o);
}
var availableSpells = [{
id: 'fireball',
name: 'FIREBALL',
damage: 150,
manaCost: 30,
rarity: 'common',
description: 'Lanza bola de fuego explosiva'
}, {
id: 'heal',
name: 'HEAL',
healing: 50,
manaCost: 25,
rarity: 'common',
description: 'Restaura puntos de salud'
}, {
id: 'lightning',
name: 'LIGHTNING',
damage: 200,
manaCost: 40,
rarity: 'rare',
description: 'Cadena de rayos entre enemigos'
}];
// PASO 1: Función centralizada de inicialización de maná
function initializeManaSystem() {
console.log('=== MANA SYSTEM INITIALIZATION ===');
// CRÍTICO: Asegurar que maxMana se inicializa primero
maxMana = 100;
// CRÍTICO: Forzar currentMana a 100 y prevenir corrupción
currentMana = 100;
// CRÍTICO: Inicializar activeSpellDeck si no existe
if (!activeSpellDeck) {
activeSpellDeck = {
currentMana: 100,
maxMana: 100
};
}
// CRÍTICO: Sincronizar activeSpellDeck con valores seguros
if (activeSpellDeck) {
activeSpellDeck.currentMana = 100;
activeSpellDeck.maxMana = 100;
}
// CRÍTICO: Actualizar UI inmediatamente para mostrar valores correctos
if (typeof updateManaBar === 'function') {
updateManaBar();
}
// CRÍTICO: Verificar que la inicialización fue exitosa
console.log('=== MANA INITIALIZATION COMPLETE ===');
console.log('maxMana:', maxMana);
console.log('currentMana:', currentMana);
console.log('activeSpellDeck.currentMana:', activeSpellDeck ? activeSpellDeck.currentMana : 'undefined');
console.log('activeSpellDeck.maxMana:', activeSpellDeck ? activeSpellDeck.maxMana : 'undefined');
// CRÍTICO: Forzar una actualización final para asegurar sincronización
if (typeof updateManaBar === 'function') {
updateManaBar();
}
}
// PASO 2: Función de validación de maná mejorada
function validateManaSystem() {
var manaWasCorrupted = false;
console.log('=== VALIDATING MANA SYSTEM ===');
console.log('currentMana (before):', currentMana, _typeof2(currentMana));
console.log('activeSpellDeck.currentMana (before):', activeSpellDeck ? activeSpellDeck.currentMana : 'undefined');
// CRÍTICO: Validar currentMana con detección de corrupción
if (typeof currentMana !== 'number' || isNaN(currentMana) || currentMana < 0 || currentMana > 100) {
console.log('⚠️ currentMana corrupted, resetting to 100');
currentMana = 100;
manaWasCorrupted = true;
}
// CRÍTICO: Validar activeSpellDeck existe y tiene propiedades válidas
if (!activeSpellDeck) {
console.log('⚠️ activeSpellDeck missing, creating with full mana');
activeSpellDeck = {
currentMana: 100,
maxMana: 100
};
manaWasCorrupted = true;
}
// CRÍTICO: Validar activeSpellDeck.currentMana
if (typeof activeSpellDeck.currentMana !== 'number' || isNaN(activeSpellDeck.currentMana) || activeSpellDeck.currentMana < 0 || activeSpellDeck.currentMana > 100) {
console.log('⚠️ activeSpellDeck.currentMana corrupted, resetting to 100');
activeSpellDeck.currentMana = 100;
activeSpellDeck.maxMana = 100;
manaWasCorrupted = true;
}
// CRÍTICO: Sincronizar si hay diferencias entre las dos variables
if (Math.abs(currentMana - activeSpellDeck.currentMana) > 1) {
console.log('⚠️ Mana desync detected - currentMana:', currentMana, 'activeSpellDeck.currentMana:', activeSpellDeck.currentMana);
// Usar el valor más alto pero válido
var validMana = Math.max(currentMana, activeSpellDeck.currentMana);
if (validMana < 0 || validMana > 100 || typeof validMana !== 'number' || isNaN(validMana)) {
validMana = 100;
}
currentMana = validMana;
activeSpellDeck.currentMana = validMana;
manaWasCorrupted = true;
console.log('✅ Mana synced to:', validMana);
}
// CRÍTICO: Actualizar UI si hubo corrupción
if (manaWasCorrupted) {
console.log('🔧 Mana corruption detected and fixed');
if (typeof updateManaBar === 'function') {
updateManaBar();
}
console.log('✅ Mana system repaired - currentMana:', currentMana, 'activeSpellDeck.currentMana:', activeSpellDeck.currentMana);
} else {
console.log('✅ Mana system healthy - no corruption detected');
}
console.log('=== MANA VALIDATION COMPLETE ===');
return !manaWasCorrupted;
}
// Simple spell casting functions
function _canCastSpell(spellId) {
// Simplified validation - spells are always free to cast
if (!spellId) {
console.log('_canCastSpell: No spell ID provided');
return false;
}
var spell = _getSpell(spellId);
if (!spell) {
console.log('_canCastSpell: Spell not found for ID:', spellId);
return false;
}
// All validation removed - spells are completely free
console.log('_canCastSpell: Spell can be cast (no restrictions):', spellId);
return true;
}
function _castSpell(spellId, targetX, targetY) {
// Simplified spell casting - no mana validation
if (!_canCastSpell(spellId)) {
console.log('_castSpell: Cannot cast spell', spellId);
return false;
}
var spell = _getSpell(spellId);
if (!spell) {
console.log('_castSpell: Spell not found during casting:', spellId);
return false;
}
// Mana deduction removed - spells are now free
console.log('=== CASTING SPELL (FREE) ===');
console.log('Spell:', spell.name);
// No mana consumption
spellCooldowns[spellId] = LK.ticks + 180; // 3 second cooldown
console.log('_castSpell: Successfully cast', spell.name, '(no mana cost)');
// Spell effects
if (spell.id === 'fireball') {
// Find the closest enemy for fireball targeting
var allEnemies = collisionArrayPool.getAllEnemies();
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
// Skip dying enemies
if (enemy.isDying) continue;
var dx = enemy.x - wizard.x;
var dy = enemy.y - wizard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
if (closestEnemy) {
// Create fireball projectile using unified factory
var fireball = ProjectileFactory.createProjectile('fireBall', wizard.x, wizard.y, closestEnemy.x, closestEnemy.y, {
targetEnemy: closestEnemy,
damage: spell.damage
});
LK.getSound('fireWhoosh').play();
// Show spell description
showSpellDescription('FIREBALL', 'Daño: ' + spell.damage + ' al enemigo más cercano', 0xFF4500);
} else {
// No enemies found, show message
showSpellDescription('FIREBALL', 'No hay enemigos cerca', 0xFF4500);
}
} else if (spell.id === 'heal') {
var healthBefore = wizard.health;
wizard.health = Math.min(wizard.health + spell.healing, wizard.maxHealth);
var actualHealing = wizard.health - healthBefore;
updateHealthBar();
// Enhanced healing effects
LK.effects.flashScreen(0x00FF00, 200);
LK.effects.flashObject(wizard, 0x00FF00, 800);
// Create multiple healing aura effects for better visibility
for (var h = 0; h < 3; h++) {
var healingAura = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: wizard.x + (Math.random() - 0.5) * 100,
y: wizard.y + (Math.random() - 0.5) * 100,
scaleX: 2 + h,
scaleY: 2 + h
}));
healingAura.tint = 0x00FF00;
healingAura.alpha = 0.7;
// Animate healing aura with delay
tween(healingAura, {
scaleX: 6 + h * 2,
scaleY: 6 + h * 2,
alpha: 0
}, {
duration: 1200 + h * 200,
delay: h * 100,
easing: tween.easeOut,
onFinish: function onFinish() {
if (healingAura.parent) healingAura.destroy();
}
});
}
// Create floating heal text
var healText = new Text2('+' + actualHealing + ' HP', {
size: 100,
fill: 0x00FF00,
font: "monospace"
});
healText.anchor.set(0.5, 0.5);
healText.x = wizard.x;
healText.y = wizard.y - 100;
game.addChild(healText);
// Animate floating heal text
tween(healText, {
y: healText.y - 200,
alpha: 0,
scaleX: 1.8,
scaleY: 1.8
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (healText.parent) healText.destroy();
}
});
// Show spell description
if (actualHealing > 0) {
showSpellDescription('HEAL', 'Curado: ' + actualHealing + ' HP', 0x00FF00);
} else {
showSpellDescription('HEAL', 'Salud ya está completa', 0x00FF00);
}
} else if (spell.id === 'lightning') {
// Lightning chain attack - instantaneous effect
LK.effects.flashScreen(0x00FFFF, 300);
LK.getSound('iceFreeze').play();
var allEnemies = collisionArrayPool.getAllEnemies();
// Filter out dying enemies and sort by distance from wizard
var livingEnemies = [];
for (var e = 0; e < allEnemies.length; e++) {
var enemy = allEnemies[e];
if (!enemy.isDying && enemy.health > 0 && enemy.parent) {
var dx = enemy.x - wizard.x;
var dy = enemy.y - wizard.y;
enemy.distanceFromWizard = Math.sqrt(dx * dx + dy * dy);
livingEnemies.push(enemy);
}
}
// Sort enemies by distance for logical chaining
livingEnemies.sort(function (a, b) {
return a.distanceFromWizard - b.distanceFromWizard;
});
var targetsHit = Math.min(3, livingEnemies.length);
console.log('Lightning targeting:', targetsHit, 'enemies out of', livingEnemies.length, 'total living enemies');
if (targetsHit > 0) {
// SOLUCION FACIL: Aplicar todo el daño primero antes de que cualquier enemigo muera
// Paso 1: Aplicar daño a todos los objetivos SIMULTANEAMENTE
for (var i = 0; i < targetsHit; i++) {
var enemy = livingEnemies[i];
console.log('Lightning hitting enemy', i + 1, 'with health:', enemy.health, 'damage:', spell.damage);
// Aplicar daño directo a la salud sin ejecutar die() todavía
if (enemy && enemy.parent && !enemy.isDying && typeof enemy.takeDamage === 'function') {
enemy.health -= spell.damage; // Aplicar daño directo
console.log('Enemy health after lightning:', enemy.health);
}
}
// Paso 2: DESPUES procesar las muertes
for (var i = 0; i < targetsHit; i++) {
var enemy = livingEnemies[i];
if (enemy && enemy.parent && !enemy.isDying) {
// Crear efectos visuales
LK.effects.flashObject(enemy, 0x00FFFF, 400);
// Verificar si el enemigo debe morir después de aplicar todo el daño
if (enemy.health <= 0) {
enemy.die(); // Ahora sí ejecutar die() después de aplicar todo el daño
}
// Create lightning impact effect on each enemy
var lightningImpact = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x,
y: enemy.y,
scaleX: 2.5,
scaleY: 2.5
}));
lightningImpact.tint = 0x00FFFF;
lightningImpact.alpha = 0.9;
// Animate lightning impact with delay based on chain position
tween(lightningImpact, {
scaleX: 5,
scaleY: 5,
alpha: 0,
rotation: Math.PI * 2
}, {
duration: 600,
delay: i * 100,
// Sequential appearance
easing: tween.easeOut,
onFinish: function onFinish() {
if (lightningImpact.parent) lightningImpact.destroy();
}
});
}
}
// Create chain lightning visual effects between targets
var chainPoints = [{
x: wizard.x,
y: wizard.y
}];
for (var i = 0; i < targetsHit; i++) {
chainPoints.push({
x: livingEnemies[i].x,
y: livingEnemies[i].y
});
}
// Create lightning bolts between each chain point
for (var i = 0; i < chainPoints.length - 1; i++) {
var startPoint = chainPoints[i];
var endPoint = chainPoints[i + 1];
var dx = endPoint.x - startPoint.x;
var dy = endPoint.y - startPoint.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var lightningBolt = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: (startPoint.x + endPoint.x) / 2,
y: (startPoint.y + endPoint.y) / 2,
scaleX: 1.5,
scaleY: distance / 50
}));
lightningBolt.tint = 0x00FFFF;
lightningBolt.alpha = 0.8;
lightningBolt.rotation = Math.atan2(dy, dx) + Math.PI / 2;
// Animate lightning bolt with sequential timing
tween(lightningBolt, {
alpha: 0,
scaleX: 0.5
}, {
duration: 500,
delay: i * 100,
// Sequential chain effect
easing: tween.easeOut,
onFinish: function onFinish() {
if (lightningBolt.parent) lightningBolt.destroy();
}
});
}
// Show spell description
showSpellDescription('LIGHTNING', 'Cadena de ' + targetsHit + ' rayos - Daño: ' + spell.damage, 0x00FFFF);
} else {
// No enemies to hit
showSpellDescription('LIGHTNING', 'No hay enemigos para la cadena', 0x00FFFF);
}
}
LK.getSound('spellCast').play();
return true;
}
function _getSpell(spellId) {
for (var i = 0; i < availableSpells.length; i++) {
if (availableSpells[i].id === spellId) {
return availableSpells[i];
}
}
return null;
}
function _getRarityColor(rarity) {
return rarity === 'rare' ? 0x0080FF : 0xFFFFFF;
}
var GameManager = {
updateEntity: function updateEntity(entity) {
if (entity && entity.update && typeof entity.update === 'function') {
entity.update();
}
},
createDamageText: function createDamageText(x, y, damage) {
var damageText = new Text2('-' + damage, {
size: 120,
fill: 0xFF4444,
font: "monospace"
});
damageText.anchor.set(0.5, 0.5);
damageText.x = x;
damageText.y = y - 40;
game.addChild(damageText);
tween(damageText, {
y: y - 120,
alpha: 0
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
damageText.destroy();
}
});
},
createFlashEffect: function createFlashEffect(target, color, duration) {
LK.effects.flashObject(target, color, duration);
},
createObject: function createObject(type, config) {
if (type === 'coin') return new Coin();
if (type === 'enemy') return new Enemy(config);
if (type === 'projectile') return new Projectile(config);
return null;
},
createVisualEffect: function createVisualEffect(type, target, config) {
// Simple visual effect creation
if (type === 'explosion') {
var explosion = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: target.x,
y: target.y,
scaleX: config.explosionScale || 2,
scaleY: config.explosionScale || 2
}));
explosion.tint = config.explosionColor || 0xFFFFFF;
explosion.alpha = 0.8;
tween(explosion, {
scaleX: (config.explosionScale || 2) * 1.5,
scaleY: (config.explosionScale || 2) * 1.5,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
}
}
};
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _defineProperty(e, r, t) {
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: !0,
configurable: !0,
writable: !0
}) : e[r] = t, e;
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == _typeof(i) ? i : i + "";
}
function _toPrimitive(t, r) {
if ("object" != _typeof(t) || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != _typeof(i)) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
// Usar valores directos en lugar de configuraciones
// Usar valores directos en lugar de configuraciones complejas
// Usar valores directos en lugar de configuraciones complejas
// Usar valores directos en lugar de configuraciones complejas
// Usar valores directos en lugar de configuraciones complejas
/****
* Global State Management
****/
// Simplified global state - no gameState object needed
var gameStarted = false;
var selectedEnemy = null;
var coinCounter = 0;
var enemyKillCounter = 0;
var pathLastSpawnTime = [-1, -1, -1, -1, -1];
var pathConsecutiveSpawns = [0, 0, 0, 0, 0];
var lastSpawnedPath = -1;
/****
* Global Systems
****/
// Unified game management system
var gameManager = GameManager;
// Simple Collision Array Pool
var collisionArrayPool = {
allEnemies: [],
clearArray: function clearArray(arrayName) {
if (this[arrayName]) {
this[arrayName].length = 0;
}
return this[arrayName];
},
getAllEnemies: function getAllEnemies() {
var array = this.clearArray('allEnemies');
for (var i = 0; i < enemies.length; i++) {
array.push(enemies[i]);
}
for (var i = 0; i < ogres.length; i++) {
array.push(ogres[i]);
}
for (var i = 0; i < knights.length; i++) {
array.push(knights[i]);
}
for (var i = 0; i < miniBosses.length; i++) {
array.push(miniBosses[i]);
}
return array;
}
};
/****
* Unified Projectile Manager
****/
var ProjectileFactory = {
// Simplified projectile creation - all types use same Projectile class
createProjectile: function createProjectile(type, startX, startY, targetX, targetY, config) {
var projectile = new Projectile(type);
projectile.x = startX;
projectile.y = startY;
if (config) {
for (var key in config) projectile[key] = config[key];
}
if (targetX !== undefined && targetY !== undefined) {
var dx = targetX - startX;
var dy = targetY - startY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
projectile.direction.x = dx / distance;
projectile.direction.y = dy / distance;
}
}
game.addChild(projectile);
projectiles.push(projectile);
return projectile;
},
createBasicAttack: function createBasicAttack(wizard, enemy) {
return this.createProjectile('projectile', wizard.x, wizard.y, enemy.x, enemy.y, {
targetEnemy: enemy,
damage: 100
});
},
createSpellProjectile: function createSpellProjectile(spellType, wizard, targetX, targetY) {
return this.createProjectile(spellType, wizard.x, wizard.y, targetX, targetY, {
damage: 150
});
},
removeProjectile: function removeProjectile(projectile) {
for (var i = projectiles.length - 1; i >= 0; i--) {
if (projectiles[i] === projectile) {
projectiles.splice(i, 1);
break;
}
}
if (projectile.parent) projectile.destroy();
}
};
// Removed globalDamageHandler and globalEffectManager aliases - use GameManager directly
/****
* Game Objects (Legacy compatibility)
****/
// Direct global arrays - no gameState needed
var enemies = [];
var ogres = [];
var knights = [];
var miniBosses = [];
var coins = [];
var projectiles = [];
// Single game menu object
var gameMenu;
// 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());
// Simple spell system variables
var currentDeck = storage.spellDeck || ['fireball', 'heal', 'lightning'];
var maxMana = 100;
var currentMana = 100; // Force to 100 to prevent corruption
var manaRegenTimer = 0;
var spellCooldowns = {};
storage.spellDeck = currentDeck.slice();
var spellSlots = [];
var manaBar, manaBarBg, manaText;
// Initialize activeSpellDeck with simple spell system
var activeSpellDeck = {
currentDeck: currentDeck.slice(),
currentMana: maxMana,
maxMana: maxMana,
availableSpells: availableSpells,
getSpell: function getSpell(spellId) {
return _getSpell(spellId);
},
canCastSpell: function canCastSpell(spellId) {
return _canCastSpell(spellId);
},
castSpell: function castSpell(spellId, targetX, targetY) {
return _castSpell(spellId, targetX, targetY);
},
getRarityColor: function getRarityColor(rarity) {
return _getRarityColor(rarity);
}
};
// Create spell UI
function createSpellUI() {
// Mana bar
manaBarBg = LK.getAsset('manaBarBg', {
anchorX: 0,
anchorY: 0,
scaleX: 2,
scaleY: 1
});
LK.gui.topRight.addChild(manaBarBg);
manaBarBg.x = -300;
manaBarBg.y = 20;
manaBarBg.visible = false;
manaBar = LK.getAsset('manaBar', {
anchorX: 0,
anchorY: 0,
scaleX: 2,
scaleY: 1
});
LK.gui.topRight.addChild(manaBar);
manaBar.x = -300;
manaBar.y = 20;
manaBar.visible = false;
manaText = new Text2('Mana: 100/100', {
size: 60,
fill: 0x4169E1,
font: "monospace"
});
manaText.anchor.set(0, 0);
LK.gui.topRight.addChild(manaText);
manaText.x = -300;
manaText.y = 65;
manaText.visible = false;
// Create initial spell slots (they will be properly configured in startGame)
for (var i = 0; i < 3; i++) {
var slot = LK.getAsset('spellSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
game.addChild(slot);
// Basic initialization (full setup happens in startGame)
slot.x = 400 + i * 400;
slot.y = 2400;
slot.visible = false; // Initially hidden
slot.slotIndex = i;
slot.spellId = null;
slot.interactive = false; // Will be enabled in startGame
// Simple click handler that will be overridden in startGame
slot.down = function (x, y, obj) {
console.log('Spell slot clicked before proper initialization:', obj.slotIndex);
};
spellSlots.push(slot);
}
}
// Update mana bar
function updateManaBar() {
if (!manaBar || !manaText) return;
// Always show full mana - spells are free
var manaPercent = 1.0; // Always 100%
manaBar.scaleX = manaPercent * 2;
manaText.setText('Mana: 100/100');
}
// Simplified spell slot cooldown updates
function updateSpellSlots() {
// Ensure we have activeSpellDeck reference
if (!activeSpellDeck) return;
// Use activeSpellDeck.currentDeck for sync
var currentActiveDeck = activeSpellDeck.currentDeck || [];
// Refresh spell slots to match current deck
for (var i = 0; i < spellSlots.length; i++) {
var slot = spellSlots[i];
// Update slot content based on current active deck
if (i < currentActiveDeck.length) {
var spellId = currentActiveDeck[i];
var spell = _getSpell(spellId);
if (spell) {
slot.spellId = spellId;
slot.visible = true;
slot.interactive = true; // Ensure slot is interactive
// Update cooldown visual - no mana checks
var cooldownEnd = spellCooldowns[spellId] || 0;
var isOnCooldown = cooldownEnd > LK.ticks;
// Mana shortage check removed - spells are always castable
if (isOnCooldown) {
slot.alpha = 0.5;
slot.tint = 0x888888;
} else {
slot.alpha = 1.0;
slot.tint = 0xFFFFFF; // Normal appearance
}
// Create or update spell icon
if (slot.children.length === 0) {
// Create new spell icon
var spellIcon = LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5
});
slot.addChild(spellIcon);
// Create spell name text
var spellNameText = new Text2(spell.name, {
size: 30,
fill: 0xFFFFFF,
font: "monospace"
});
spellNameText.anchor.set(0.5, 0.5);
spellNameText.y = 40;
slot.addChild(spellNameText);
}
// Update existing spell icon
if (slot.children.length > 0) {
var spellIcon = slot.children[0];
spellIcon.tint = _getRarityColor(spell.rarity);
}
// Update spell name text if it exists
if (slot.children.length > 1) {
var spellNameText = slot.children[1];
// Ensure it's a Text2 object before calling setText
if (spellNameText && typeof spellNameText.setText === 'function') {
spellNameText.setText(spell.name);
spellNameText.tint = _getRarityColor(spell.rarity);
}
}
} else {
slot.spellId = null;
slot.visible = true;
slot.alpha = 0.3;
slot.tint = 0x444444;
}
} else {
// Empty slot - keep visible but dimmed
slot.spellId = null;
slot.visible = true;
slot.alpha = 0.3;
slot.tint = 0x444444;
}
}
}
// Initialize spell UI
createSpellUI();
// Initialize spell unlocking system
var lastUnlockCheck = 0;
function checkSpellUnlocks() {
if (!gameMenu.spellDeck) {
gameMenu.spellDeck = new SpellDeck();
}
// Only check unlocks when kill counter changes
if (enemyKillCounter === lastUnlockCheck) return;
lastUnlockCheck = enemyKillCounter;
// Unlock spells based on achievements with messages
if (enemyKillCounter >= 10 && !storage.lightningUnlocked) {
storage.lightningUnlocked = true;
gameMenu.spellDeck.unlockSpell('lightning');
LK.effects.flashScreen(0x00FFFF, 500);
showSpellUnlockMessage('LIGHTNING', 'Cadena de rayos entre enemigos');
}
if (enemyKillCounter >= 25 && !storage.shieldUnlocked) {
storage.shieldUnlocked = true;
gameMenu.spellDeck.unlockSpell('shield');
LK.effects.flashScreen(0x0080FF, 500);
showSpellUnlockMessage('MAGIC SHIELD', 'Inmunidad temporal al daño');
}
if (enemyKillCounter >= 50 && !storage.teleportUnlocked) {
storage.teleportUnlocked = true;
gameMenu.spellDeck.unlockSpell('teleport');
LK.effects.flashScreen(0x8000FF, 500);
showSpellUnlockMessage('TELEPORT', 'Mueve instantáneamente al mago');
}
if (enemyKillCounter >= 75 && !storage.timeSlowUnlocked) {
storage.timeSlowUnlocked = true;
gameMenu.spellDeck.unlockSpell('timeSlow');
LK.effects.flashScreen(0xFF8000, 500);
showSpellUnlockMessage('TIME SLOW', 'Ralentiza todos los enemigos');
}
if (enemyKillCounter >= 100 && !storage.meteorUnlocked) {
storage.meteorUnlocked = true;
gameMenu.spellDeck.unlockSpell('meteor');
LK.effects.flashScreen(0xFF0000, 500);
showSpellUnlockMessage('METEOR', 'Daño masivo en área');
}
}
function showSpellUnlockMessage(spellName, description) {
var unlockText = new Text2('NUEVO HECHIZO DESBLOQUEADO!\n' + spellName + '\n' + description, {
size: 60,
fill: 0xFFD700,
font: "monospace"
});
unlockText.anchor.set(0.5, 0.5);
unlockText.x = 2048 / 2;
unlockText.y = 2732 / 2;
game.addChild(unlockText);
// Animate unlock message
tween(unlockText, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(unlockText, {
alpha: 0,
y: unlockText.y - 200
}, {
duration: 1000,
easing: tween.easeIn,
onFinish: function onFinish() {
if (unlockText.parent) unlockText.destroy();
}
});
}
});
}
// Function to show spell description when cast
function showSpellDescription(spellName, description, color) {
var descText = new Text2(spellName + '\n' + description, {
size: 50,
fill: color,
font: "monospace"
});
descText.anchor.set(0.5, 0.5);
descText.x = wizard.x;
descText.y = wizard.y - 200;
game.addChild(descText);
// Animate description
tween(descText, {
y: descText.y - 80,
alpha: 0
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (descText.parent) descText.destroy();
}
});
}
// Upgrade system removed - using spell deck system instead
// Create unified path system - all 5 paths at once
var paths = [];
var knightX = 2048 / 2;
var knightY = 2732 - 250;
var pathAngles = [-Math.PI / 2, -Math.PI / 3, -2 * Math.PI / 3, Math.PI / 6, 5 * Math.PI / 6];
var wizardX = knightX;
var wizardY = 2732 - 600;
// Create knight reference for backward compatibility
var knight = wizard;
// Unified path creation function
function createUnifiedPaths() {
var spawnPositions = [{
x: 2048 / 2,
y: -100
}, {
x: 2048 + 50,
y: -50
}, {
x: -50,
y: -50
}, {
x: -100,
y: 2732 / 2 + 400
}, {
x: 2048 + 100,
y: 2732 / 2 + 400
}];
var pathAngles = [-Math.PI / 2, -Math.PI / 3, -2 * Math.PI / 3, Math.PI / 6, 5 * Math.PI / 6];
for (var p = 0; p < 5; p++) {
var angle = pathAngles[p];
var spawnPos = spawnPositions[p];
var actualPathLength = Math.sqrt((spawnPos.x - wizardX) * (spawnPos.x - wizardX) + (spawnPos.y - wizardY) * (spawnPos.y - wizardY));
// Create stone segments
var segmentSize = 80;
var numSegments = Math.floor(actualPathLength / segmentSize);
for (var s = 0; s < numSegments; s++) {
var segmentDistance = s * segmentSize + segmentSize / 2;
var segmentX = spawnPos.x - Math.cos(angle) * segmentDistance;
var segmentY = spawnPos.y - Math.sin(angle) * segmentDistance;
if (segmentX >= -100 && segmentX <= 2148 && segmentY >= -100 && segmentY <= 2832) {
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
}));
stoneSegment.alpha = 0;
stoneSegment.visible = false;
stoneSegment.pathIndex = p;
}
}
// Create collision area
var centerX = (spawnPos.x + wizardX) / 2;
var centerY = (spawnPos.y + wizardY) / 2;
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
}));
path.alpha = 0;
path.visible = false;
path.pathIndex = p;
// Add path number
var pathNumber = new Text2((p + 1).toString(), {
size: 120,
fill: 0xFFD700,
font: "monospace"
});
pathNumber.anchor.set(0.5, 0.5);
pathNumber.x = spawnPos.x;
pathNumber.y = spawnPos.y - 80;
pathNumber.visible = false;
pathNumber.pathIndex = p;
game.addChild(pathNumber);
// Add touch handler
path.down = function (x, y, obj) {
wizard.attack(obj.pathIndex);
};
paths.push(path);
}
}
// Create all paths
createUnifiedPaths();
// 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 early to ensure it's available for other classes
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: 0xFFFFFF,
font: "monospace"
});
killCountText.anchor.set(0, 0);
LK.gui.topLeft.addChild(killCountText);
killCountText.x = 120;
killCountText.y = 50;
killCountText.visible = false;
var tapText = new Text2('TAP ENEMIES TO ATTACK!', {
size: 80,
fill: 0x00FF00,
font: "monospace"
});
tapText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(tapText);
tapText.y = -400;
tapText.visible = false;
// Health bar UI
var healthBarBg = LK.getAsset('healthBarBg', {
anchorX: 0,
anchorY: 0,
scaleX: 2,
scaleY: 1
});
LK.gui.topLeft.addChild(healthBarBg);
healthBarBg.x = 120;
healthBarBg.y = 20;
healthBarBg.visible = false;
var healthBar = LK.getAsset('healthBar', {
anchorX: 0,
anchorY: 0,
scaleX: 2,
scaleY: 1
});
LK.gui.topLeft.addChild(healthBar);
healthBar.x = 120;
healthBar.y = 20;
healthBar.visible = false;
var healthText = new Text2('Health: 100/100', {
size: 60,
fill: 0xFFFFFF,
font: "monospace"
});
healthText.anchor.set(0, 0);
LK.gui.topLeft.addChild(healthText);
healthText.x = 120;
healthText.y = 65;
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
// Unified SpawnManager for streamlined spawn control and enemy lifecycle
var SpawnManager = {
// Consolidated spawn configuration
spawnConfig: {
skeleton: {
interval: 90,
maxCount: 15,
startAt: 0
},
ogre: {
interval: 180,
maxCount: 4,
startAt: 15
},
knight: {
interval: 300,
maxCount: 3,
startAt: 30
},
miniBoss: {
interval: 60,
maxCount: 1,
startAt: 80,
endAt: 85,
chance: 0.02
}
},
// Unified spawn timers
spawnTimers: {
skeleton: 0,
ogre: 0,
knight: 0,
miniBoss: 0
},
// Streamlined spawn validation
canSpawnEnemy: function canSpawnEnemy(type, difficulty, totalEnemies) {
var config = this.spawnConfig[type];
if (!config) return false;
var collection = [];
if (type === 'skeleton') collection = enemies;else if (type === 'ogre') collection = ogres;else if (type === 'knight') collection = knights;else if (type === 'miniBoss') collection = miniBosses;
var timer = this.spawnTimers[type];
// Usar valores directos para thresholds
var threshold = 0;
if (type === 'ogre') threshold = 15;else if (type === 'knight') threshold = 30;else if (type === 'miniBoss') threshold = 80;
// Universal spawn conditions with type-specific rules
var baseConditions = timer >= config.interval && enemyKillCounter >= threshold && collection.length < config.maxCount && totalEnemies < 25;
// Special conditions for specific types
if (type === 'miniBoss') {
return baseConditions && enemyKillCounter <= config.endAt && miniBosses.length === 0 && Math.random() < config.chance;
}
return baseConditions && miniBosses.length === 0;
},
// Unified spawn execution with specific enemy classes
executeSpawn: function executeSpawn(type, difficulty, level) {
var totalEnemies = globalEnemyManager.getAllEnemies().length;
if (!this.canSpawnEnemy(type, difficulty, totalEnemies)) return null;
var enemy = globalEnemyManager.createEnemy(type, difficulty, level);
if (enemy) {
game.addChild(enemy);
// Add directly to global arrays
if (type === 'skeleton') enemies.push(enemy);else if (type === 'ogre') ogres.push(enemy);else if (type === 'knight') knights.push(enemy);else if (type === 'miniBoss') miniBosses.push(enemy);
this.spawnTimers[type] = 0;
// Update path tracking for skeleton spawns
if (type === 'skeleton') {
pathConsecutiveSpawns[enemy.pathIndex]++;
pathLastSpawnTime[enemy.pathIndex] = LK.ticks;
lastSpawnedPath = enemy.pathIndex;
}
}
return enemy;
},
// Streamlined interval updates
updateSpawnIntervals: function updateSpawnIntervals(difficulty, level) {
// Usar valores directos para skeleton
if (difficulty === 'FACIL') {
this.spawnConfig.skeleton.interval = Math.max(60, 120 - level * 5);
this.spawnConfig.ogre.interval = 240;
this.spawnConfig.knight.interval = 400;
} else if (difficulty === 'NORMAL') {
this.spawnConfig.skeleton.interval = Math.max(40, 90 - level * 6);
this.spawnConfig.ogre.interval = 180;
this.spawnConfig.knight.interval = 300;
} else {
// DIFICIL
this.spawnConfig.skeleton.interval = Math.max(20, 60 - level * 4);
this.spawnConfig.ogre.interval = 120;
this.spawnConfig.knight.interval = 200;
}
},
// Unified timer updates
updateTimers: function updateTimers() {
for (var type in this.spawnTimers) {
this.spawnTimers[type]++;
}
// Check if all paths are in cooldown and reset if needed
var allPathsInCooldown = true;
for (var pathIdx = 0; pathIdx < 5; pathIdx++) {
if (pathConsecutiveSpawns[pathIdx] < 2) {
allPathsInCooldown = false;
break;
}
}
if (allPathsInCooldown) {
// Reset all path cooldowns to prevent spawn deadlock
for (var pathIdx = 0; pathIdx < 5; pathIdx++) {
pathConsecutiveSpawns[pathIdx] = 0;
pathLastSpawnTime[pathIdx] = -1;
}
}
},
// Streamlined spawn cycle for all enemy types
processSpawnCycle: function processSpawnCycle(difficulty, level) {
this.updateSpawnIntervals(difficulty, level);
this.updateTimers();
// Clean up any null/destroyed enemies from collections first
this.cleanupDestroyedEnemies();
var enemyTypes = ['skeleton', 'ogre', 'knight', 'miniBoss'];
for (var i = 0; i < enemyTypes.length; i++) {
this.executeSpawn(enemyTypes[i], difficulty, level);
}
},
// Add cleanup method for destroyed enemies
cleanupDestroyedEnemies: function cleanupDestroyedEnemies() {
// Clean up global arrays directly
var allArrays = [enemies, ogres, knights, miniBosses];
for (var arrayIdx = 0; arrayIdx < allArrays.length; arrayIdx++) {
var array = allArrays[arrayIdx];
for (var i = array.length - 1; i >= 0; i--) {
if (!array[i] || !array[i].parent || array[i].isDying) {
array.splice(i, 1);
}
}
}
}
};
// Game input handling with spell casting support
game.down = function (x, y, obj) {
// Removed automatic fireball casting - spells are now manual only through spell slots
// Tap-to-attack is now handled directly by individual enemies
// No need for path-based attacks since enemies handle their own tap events
};
// Initialize unified management systems for streamlined game architecture
var globalEnemyManager = new EnemyManager();
var globalDeathHandler = new UnifiedDeathHandler();
// Unified death animation function for all enemy types
function createEnemyDeathAnimation(enemy, enemyType, enemyArray) {
globalDeathHandler.executeEnemyDeath(enemy, enemyArray);
}
// 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
}
});
}
// Upgrade menus removed - using spell deck system instead
// 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
}
// Helper functions now integrated into EnemyFactory
// Unified spawn management system
SpawnManager.processSpawnCycle(selectedDifficulty, difficultyLevel);
// Reset path cooldowns for optimized path management
for (var pathIdx = 0; pathIdx < 5; pathIdx++) {
if (pathLastSpawnTime[pathIdx] !== -1 && LK.ticks - pathLastSpawnTime[pathIdx] > pathCooldownDuration) {
pathConsecutiveSpawns[pathIdx] = 0;
}
}
// Unified CollisionManager for streamlined collision detection and response
var CollisionManager = {
// Consolidated collision configurations
collisionConfig: {
skeleton: {
damage: 20,
removeOnHit: true
},
ogre: {
damage: 30,
removeOnHit: true
},
knight: {
damage: 40,
removeOnHit: true
},
miniBoss: {
damage: 75,
removeOnHit: false
}
},
// Streamlined enemy collision processing with categorized collision types
processEnemyCollisions: function processEnemyCollisions() {
var allEnemies = globalEnemyManager.getAllEnemies();
// Category 1: Off-screen cleanup (non-collision processing)
this.processOffScreenCleanup(allEnemies);
// Category 2: Enemy-wizard collisions
this.processEnemyWizardCollisions(allEnemies);
},
// Separate processing for off-screen enemy cleanup
processOffScreenCleanup: function processOffScreenCleanup(allEnemies) {
for (var i = allEnemies.length - 1; i >= 0; i--) {
var enemy = allEnemies[i];
if (this.isOffScreen(enemy)) {
this.removeEnemyFromGame(enemy);
}
}
},
// Separate processing for enemy-wizard collisions
processEnemyWizardCollisions: function processEnemyWizardCollisions(allEnemies) {
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
if (!enemy.isDying && enemy.parent) {
this.checkWizardCollision(enemy);
}
}
},
// Efficient off-screen detection
isOffScreen: function isOffScreen(enemy) {
return enemy.y > 2732 + 100;
},
// Unified enemy removal system
removeEnemyFromGame: function removeEnemyFromGame(enemy) {
// Remove from global arrays directly
this.removeFromLegacyArrays(enemy);
enemy.destroy();
},
// Legacy array compatibility cleanup
removeFromLegacyArrays: function removeFromLegacyArrays(enemy) {
var arrays = [enemies, ogres, knights, miniBosses];
for (var a = 0; a < arrays.length; a++) {
var array = arrays[a];
for (var i = array.length - 1; i >= 0; i--) {
if (array[i] === enemy) {
array.splice(i, 1);
break;
}
}
}
},
// 1.1 Distance Culling: Enhanced wizard collision detection with optimized distance-based culling
checkWizardCollision: function checkWizardCollision(enemy) {
// Initialize collision tracking
if (enemy.lastIntersecting === undefined) {
enemy.lastIntersecting = false;
}
// 1.1 Distance Culling: Skip expensive intersection test if objects are too far apart
var dx = enemy.x - wizard.x;
var dy = enemy.y - wizard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var maxCollisionDistance = 150; // Approximate maximum collision distance based on sprite sizes
var currentIntersecting = false;
if (distance <= maxCollisionDistance) {
// Only perform expensive intersection test if objects are close enough
currentIntersecting = wizard.intersects(enemy);
}
// Check collision transition
if (!enemy.lastIntersecting && currentIntersecting && !enemy.isDying) {
var config = this.getEnemyConfig(enemy);
wizard.takeDamage(config.damage);
// Handle enemy removal based on type
if (config.removeOnHit) {
this.removeEnemyFromGame(enemy);
return;
}
}
// Update collision state
enemy.lastIntersecting = currentIntersecting;
},
// Get enemy configuration by type
getEnemyConfig: function getEnemyConfig(enemy) {
return this.collisionConfig[enemy.enemyType] || this.collisionConfig.skeleton;
}
};
// Replace the old collision function call
function checkAllEnemyCollisions() {
CollisionManager.processEnemyCollisions();
}
// Call the unified collision detection function
checkAllEnemyCollisions();
// 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 = collisionArrayPool.getAllEnemies();
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);
}
}
}
// Update spell system
if (gameStarted) {
// Force mana to always be 100 - spells are completely free
currentMana = 100;
if (activeSpellDeck) {
activeSpellDeck.currentMana = 100;
}
updateManaBar();
updateSpellSlots();
}
// Simple time slow effects processing
var allEnemies = collisionArrayPool.getAllEnemies();
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
if (enemy.timeSlowed) {
enemy.timeSlowTimer--;
if (enemy.timeSlowTimer <= 0) {
enemy.timeSlowed = false;
enemy.timeSlowAmount = 1.0;
}
}
}
// Make tap text pulse
var pulse = 1 + Math.sin(LK.ticks * 0.1) * 0.2;
tapText.scale.set(pulse, pulse);
// Check for spell unlocks
checkSpellUnlocks();
// Clean up any orphaned projectiles that may not have been properly removed
for (var i = projectiles.length - 1; i >= 0; i--) {
var projectile = projectiles[i];
if (!projectile || !projectile.parent || projectile.hitEnemy) {
projectiles.splice(i, 1);
}
}
// ProjectileFactory uses the global projectiles array, no separate activeProjectiles array needed
};
// Remove tap text after 5 seconds
tween({}, {}, {
duration: 5000,
onFinish: function onFinish() {
if (tapText && tapText.parent) {
tapText.destroy();
}
}
});