User prompt
cuando el menu de mejora se abre haz que se pausen las mejoras
User prompt
cambia la opcion de speed boost por una que cada 4 segundos empuje a todos los enemigos hacia atras a los enemigos ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
cambia la opcion de speed boost por una que cada 5 segundos empuje para atras a los enemigos ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
expande el menu de mejoras para que se pueda poner hasta 10 mejoras
User prompt
crea una mejora que haga un disparo doble
User prompt
separa un poco las mejoras
User prompt
crea una quinta mejora que te permita disparar doble
User prompt
crea una mejora que haga un disparo doble
User prompt
quita el menu naranja que aparece antes del menu principal
User prompt
ahora haz una interfaz de menu de inicio con un color blanco hueso
User prompt
haz que aparesca un tercer menu de mejoras despeues del enemigo 50
User prompt
pon la esfera un poquito mas a la drecha y un poquito mas ababjo
User prompt
haz que la esfera este a un lado del jugador
User prompt
haz que la esfera no se mueva
User prompt
agrega una mejora que ponga una esfera de energia que cada 3s haga un ataque a el enemigo mas cercano ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
arreglalo
User prompt
haz que el coste inicial de las mejoras sea 5
User prompt
aumenta las monedas cuando el menu es mostrado
User prompt
haz que cada vez que se muestre el menu de mejora se aumente 5 monedas en todas las mejoras
User prompt
ahora las mejoras del menu de mejora costaran 5 monedas
User prompt
inicia el costo de las mejoras en 10 y cuando pase a la siguiente fase aumenten 5 monedas
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading '0')' in or related to this line: 'var upgradeText = new Text2(upgrade.name + '\n' + upgrade.description + '\nCost: ' + upgradeCosts[i] + ' coins', {' Line Number: 993
User prompt
ahora crea un sistema que cada vez que entra el menu de mejora aumenta en 5 de oro 2 de las mejoras
User prompt
haz que la mejora de health boost que de 30 puntos de vida
User prompt
haz que los enemigos lleguen al jugador
/**** * 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 knight if (knight && self.intersects(knight)) { self.collect(); } } }; self.collect = function () { LK.getSound('coinCollect').play(); LK.setScore(LK.getScore() + 5); coinCounter++; coinText.setText('Coins: ' + coinCounter); // Remove from coins array for (var i = coins.length - 1; i >= 0; i--) { if (coins[i] === self) { coins.splice(i, 1); break; } } self.destroy(); }; return self; }); // Game arrays to track objects var Enemy = Container.expand(function () { var self = Container.call(this); // Animation system for skeleton self.currentFrame = 1; self.animationTimer = 0; self.animationSpeed = 15; // Change frame every 15 ticks (250ms at 60fps) self.animationState = 'walking'; // walking, attacking, dying, idle // Create all skeleton graphics frames and store them self.skeletonFrames = []; for (var i = 1; i <= 4; i++) { var frameGraphics = self.attachAsset('esqueleto' + i, { anchorX: 0.5, anchorY: 1.0, scaleX: 2.0, scaleY: 2.0 }); frameGraphics.visible = i === 1; // Only show first frame initially self.skeletonFrames.push(frameGraphics); } // Create invisible hitbox with double size var hitbox = self.attachAsset('esqueleto1', { anchorX: 0.5, anchorY: 1.0, scaleX: 4, scaleY: 4 }); hitbox.alpha = 0; // Make hitbox invisible self.health = 100; self.maxHealth = 100; self.speed = 7; // Note: health, maxHealth and speed will be overridden by difficulty system self.lastX = 0; self.update = function () { // Animation system - cycle through skeleton frames based on state self.animationTimer++; var frameSpeed = self.animationSpeed; if (self.animationState === 'attacking') { frameSpeed = 8; // Faster animation for attacking } else if (self.animationState === 'dying') { frameSpeed = 20; // Slower animation for dying } else if (self.animationState === 'idle') { frameSpeed = 25; // Very slow animation for idle } if (self.animationTimer >= frameSpeed) { self.animationTimer = 0; // Hide current frame self.skeletonFrames[self.currentFrame - 1].visible = false; // Move to next frame based on animation state if (self.animationState === 'walking') { self.currentFrame++; if (self.currentFrame > 4) { self.currentFrame = 1; } } else if (self.animationState === 'attacking') { // Cycle through frames 2-4 for attacking self.currentFrame++; if (self.currentFrame > 4) { self.currentFrame = 2; } } else if (self.animationState === 'dying') { // Play frames 3-4 once for dying if (self.currentFrame < 4) { self.currentFrame++; } } else if (self.animationState === 'idle') { // Cycle between frames 1-2 for idle self.currentFrame = self.currentFrame === 1 ? 2 : 1; } // Show new frame self.skeletonFrames[self.currentFrame - 1].visible = true; } // Progressive speed increase - increase speed by 50% over 10 seconds if (!self.speedTweenStarted) { self.speedTweenStarted = true; var targetSpeed = self.speed * 2.5; // Increase speed by 150% tween(self, { speed: targetSpeed }, { duration: 10000, // 10 seconds easing: tween.easeOut }); } // Always move directly toward the wizard to ensure enemies reach the player if (wizard) { // Calculate direction from enemy to wizard var dx = wizard.x - self.x; var dy = wizard.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { // Normalize direction and apply speed self.x += dx / distance * self.speed; self.y += dy / distance * self.speed; } // Face toward the wizard - flip horizontally based on wizard position if (dx < 0) { // Wizard is to the left - flip to face left for (var frameIdx = 0; frameIdx < self.skeletonFrames.length; frameIdx++) { self.skeletonFrames[frameIdx].scaleX = -2.0; // Flip to face left } } else { // Wizard is to the right - face right for (var frameIdx = 0; frameIdx < self.skeletonFrames.length; frameIdx++) { self.skeletonFrames[frameIdx].scaleX = 2.0; // Normal orientation to face right } } } else { // Fallback: move down if wizard doesn't exist self.y += self.speed; } }; self.takeDamage = function (damage) { self.health -= damage; // Change animation state to attacking when hit self.animationState = 'attacking'; // Flash red when hit LK.effects.flashObject(self, 0xFF0000, 200); // Return to walking animation after brief attacking animation tween({}, {}, { duration: 300, onFinish: function onFinish() { if (self.animationState === 'attacking') { self.animationState = 'walking'; } } }); // Enemies die from any projectile hit self.die(); }; self.down = function (x, y, obj) { // Set this enemy as the selected target selectedEnemy = self; // Visual feedback for selection LK.effects.flashObject(self, 0xFFFF00, 500); // Yellow flash to indicate selection // Create projectile from wizard to enemy when enemy is tapped if (wizard && projectiles.length < 10) { // Limit projectiles to prevent spam var projectile = game.addChild(new Projectile()); projectile.x = wizard.x; projectile.y = wizard.y; // Calculate direction from wizard to enemy var dx = self.x - wizard.x; var dy = self.y - wizard.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { projectile.direction.x = dx / distance; projectile.direction.y = dy / distance; } projectile.targetEnemy = self; // Set the target for this projectile projectiles.push(projectile); LK.getSound('spellCast').play(); } }; self.die = function () { // Change animation state to dying self.animationState = 'dying'; self.currentFrame = 3; // Start dying animation from frame 3 // Play pain sound when enemy dies LK.getSound('painSound').play(); // Drop coin var coin = game.addChild(new Coin()); coin.x = self.x; coin.y = self.y - 50; coin.isAnimating = true; coins.push(coin); // Animate coin moving toward coin counter 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, easing: tween.easeOut, onFinish: function onFinish() { // Increment coin counter after animation coinCounter++; coinText.setText('Coins: ' + coinCounter); // Remove coin from array and destroy for (var i = coins.length - 1; i >= 0; i--) { if (coins[i] === coin) { coins.splice(i, 1); break; } } coin.destroy(); } }); // Increment enemy kill counter enemyKillCounter++; killCountText.setText('Puntuacion: ' + enemyKillCounter); // Add experience to wizard wizard.gainExperience(25); // Life drain upgrade - 5% chance to restore 10 health if (wizard.lifeDrainLevel && wizard.lifeDrainLevel > 0) { var drainChance = Math.random(); if (drainChance < 0.05) { // 5% chance wizard.health = Math.min(wizard.health + 10, wizard.maxHealth); updateHealthBar(); // Visual feedback for life drain LK.effects.flashObject(wizard, 0x00FF00, 300); } } // Clear selected enemy if this was the target if (selectedEnemy === self) { selectedEnemy = null; } // Remove from enemies array for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } self.destroy(); LK.setScore(LK.getScore() + 10); }; return self; }); var GameMenu = Container.expand(function () { var self = Container.call(this); // Menu background image instead of cave background var menuBg = self.attachAsset('menuBackground', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, scaleX: 4.0, scaleY: 4.0 }); menuBg.alpha = 1.0; // Title text var titleText = new Text2('WIZARD DEFENDER', { size: 150, fill: 0xFFD700, font: "monospace" }); titleText.anchor.set(0.5, 0.5); titleText.x = 2048 / 2; titleText.y = 800; self.addChild(titleText); // Instructions text var instructionsText = new Text2('TAP PATHS TO CAST SPELLS\nDEFEND YOUR CASTLE!', { size: 80, fill: 0xFFFFFF, font: "monospace" }); instructionsText.anchor.set(0.5, 0.5); instructionsText.x = 2048 / 2; instructionsText.y = 1200; self.addChild(instructionsText); // Start button var startButton = self.attachAsset('wizard1', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 1600, 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 = 1700; self.addChild(startButtonText); // Button interaction self.down = function (x, y, obj) { // Start the game by hiding menu self.startGame(); }; self.startGame = function () { // Hide menu and start game self.visible = false; gameStarted = true; // Show cave background when game starts if (backgroundMap) { backgroundMap.visible = true; } // Show all game elements wizard.visible = true; for (var i = 0; i < paths.length; i++) { paths[i].visible = true; } // Show all stone path segments and make them visible for (var i = 0; i < game.children.length; i++) { var child = game.children[i]; if (child.pathIndex !== undefined && child !== paths[child.pathIndex]) { child.visible = true; // Check if it's a stone path segment or path number if (child.alpha !== undefined && child.setText === undefined) { child.alpha = 0; // Keep stone paths invisible } } } coinText.visible = true; killCountText.visible = true; tapText.visible = true; healthBarBg.visible = true; healthBar.visible = true; healthText.visible = true; // Start medieval music LK.playMusic('medievalTheme', { volume: 0.7, fade: { start: 0, end: 0.7, duration: 2000 } }); }; return self; }); var Knight = Container.expand(function () { var self = Container.call(this); // Animation system for knight self.currentFrame = 1; self.animationTimer = 0; self.animationSpeed = 22; // Change frame every 22 ticks (367ms at 60fps) self.animationState = 'walking'; // walking, attacking, dying, idle // Create all knight graphics frames and store them self.knightFrames = []; for (var i = 1; i <= 4; i++) { var frameGraphics = self.attachAsset('knight' + i, { anchorX: 0.5, anchorY: 1.0, scaleX: 2.0, scaleY: 2.0 }); frameGraphics.visible = i === 1; // Only show first frame initially self.knightFrames.push(frameGraphics); } // Create invisible hitbox with double size var hitbox = self.attachAsset('knight1', { anchorX: 0.5, anchorY: 1.0, scaleX: 4, scaleY: 4 }); hitbox.alpha = 0; // Make hitbox invisible self.health = 100; self.maxHealth = 100; self.speed = 7; self.hitsToKill = 3; // Knights need 3 hits to die self.hitsTaken = 0; // Note: health, maxHealth and speed will be overridden by difficulty system self.lastX = 0; self.update = function () { // Animation system - cycle through knight frames based on state self.animationTimer++; var frameSpeed = self.animationSpeed; if (self.animationState === 'attacking') { frameSpeed = 12; // Faster animation for attacking } else if (self.animationState === 'dying') { frameSpeed = 28; // Slower animation for dying } else if (self.animationState === 'idle') { frameSpeed = 35; // Very slow animation for idle } if (self.animationTimer >= frameSpeed) { self.animationTimer = 0; // Hide current frame self.knightFrames[self.currentFrame - 1].visible = false; // Move to next frame based on animation state if (self.animationState === 'walking') { self.currentFrame++; if (self.currentFrame > 4) { self.currentFrame = 1; } } else if (self.animationState === 'attacking') { // Cycle through frames 2-4 for attacking self.currentFrame++; if (self.currentFrame > 4) { self.currentFrame = 2; } } else if (self.animationState === 'dying') { // Play frames 3-4 once for dying if (self.currentFrame < 4) { self.currentFrame++; } } else if (self.animationState === 'idle') { // Cycle between frames 1-2 for idle self.currentFrame = self.currentFrame === 1 ? 2 : 1; } // Show new frame self.knightFrames[self.currentFrame - 1].visible = true; } // Progressive speed increase - increase speed by 150% over 10 seconds if (!self.speedTweenStarted) { self.speedTweenStarted = true; var targetSpeed = self.speed * 2.5; // Increase speed by 150% tween(self, { speed: targetSpeed }, { duration: 10000, // 10 seconds easing: tween.easeOut }); } // Always move directly toward the wizard to ensure knights reach the player if (wizard) { // Calculate direction from knight to wizard var dx = wizard.x - self.x; var dy = wizard.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { // Normalize direction and apply speed self.x += dx / distance * self.speed; self.y += dy / distance * self.speed; } // Face toward the wizard - flip horizontally based on wizard position if (dx < 0) { // Wizard is to the left - flip to face left for (var frameIdx = 0; frameIdx < self.knightFrames.length; frameIdx++) { self.knightFrames[frameIdx].scaleX = -2.0; // Flip to face left } } else { // Wizard is to the right - face right for (var frameIdx = 0; frameIdx < self.knightFrames.length; frameIdx++) { self.knightFrames[frameIdx].scaleX = 2.0; // Normal orientation to face right } } } else { // Fallback: move down if wizard doesn't exist self.y += self.speed; } }; self.takeDamage = function (damage) { self.health -= damage; self.hitsTaken++; // Change animation state to attacking when hit self.animationState = 'attacking'; // Flash red when hit LK.effects.flashObject(self, 0xFF0000, 200); // Return to walking animation after brief attacking animation tween({}, {}, { duration: 500, onFinish: function onFinish() { if (self.animationState === 'attacking') { self.animationState = 'walking'; } } }); // Knights need 3 hits to die if (self.hitsTaken >= self.hitsToKill) { self.die(); } }; self.down = function (x, y, obj) { // Set this knight as the selected target selectedEnemy = self; // Visual feedback for selection LK.effects.flashObject(self, 0xFFFF00, 500); // Yellow flash to indicate selection // Create projectile from wizard to knight when knight is tapped if (wizard && projectiles.length < 10) { // Limit projectiles to prevent spam var projectile = game.addChild(new Projectile()); projectile.x = wizard.x; projectile.y = wizard.y; // Calculate direction from wizard to knight var dx = self.x - wizard.x; var dy = self.y - wizard.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { projectile.direction.x = dx / distance; projectile.direction.y = dy / distance; } projectile.targetEnemy = self; // Set the target for this projectile projectiles.push(projectile); LK.getSound('spellCast').play(); } }; self.die = function () { // Change animation state to dying self.animationState = 'dying'; self.currentFrame = 3; // Start dying animation from frame 3 // Play pain sound when knight dies LK.getSound('painSound').play(); // Drop coin var coin = game.addChild(new Coin()); coin.x = self.x; coin.y = self.y - 50; coin.isAnimating = true; coins.push(coin); // Animate coin moving toward coin counter 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, easing: tween.easeOut, onFinish: function onFinish() { // Increment coin counter after animation coinCounter++; coinText.setText('Coins: ' + coinCounter); // Remove coin from array and destroy for (var i = coins.length - 1; i >= 0; i--) { if (coins[i] === coin) { coins.splice(i, 1); break; } } coin.destroy(); } }); // Increment enemy kill counter enemyKillCounter++; killCountText.setText('Puntuacion: ' + enemyKillCounter); // Add experience to wizard wizard.gainExperience(25); // Life drain upgrade - 5% chance to restore 10 health if (wizard.lifeDrainLevel && wizard.lifeDrainLevel > 0) { var drainChance = Math.random(); if (drainChance < 0.05) { // 5% chance wizard.health = Math.min(wizard.health + 10, wizard.maxHealth); updateHealthBar(); // Visual feedback for life drain LK.effects.flashObject(wizard, 0x00FF00, 300); } } // Clear selected enemy if this was the target if (selectedEnemy === self) { selectedEnemy = null; } // Remove from knights array for (var i = knights.length - 1; i >= 0; i--) { if (knights[i] === self) { knights.splice(i, 1); break; } } self.destroy(); LK.setScore(LK.getScore() + 20); }; return self; }); var Ogre = Container.expand(function () { var self = Container.call(this); // Animation system for ogre self.currentFrame = 1; self.animationTimer = 0; self.animationSpeed = 20; // Change frame every 20 ticks (333ms at 60fps) self.animationState = 'walking'; // walking, attacking, dying, idle // Create all ogre graphics frames and store them self.ogreFrames = []; for (var i = 1; i <= 4; i++) { var frameGraphics = self.attachAsset('ogre' + i, { anchorX: 0.5, anchorY: 1.0, scaleX: 2.0, scaleY: 2.0 }); frameGraphics.visible = i === 1; // Only show first frame initially self.ogreFrames.push(frameGraphics); } // Create invisible hitbox with double size var hitbox = self.attachAsset('ogre1', { anchorX: 0.5, anchorY: 1.0, scaleX: 4, scaleY: 4 }); hitbox.alpha = 0; // Make hitbox invisible self.health = 100; self.maxHealth = 100; self.speed = 7; self.hitsToKill = 2; self.hitsTaken = 0; // Note: health, maxHealth and speed will be overridden by difficulty system self.lastX = 0; self.update = function () { // Animation system - cycle through ogre frames based on state self.animationTimer++; var frameSpeed = self.animationSpeed; if (self.animationState === 'attacking') { frameSpeed = 10; // Faster animation for attacking } else if (self.animationState === 'dying') { frameSpeed = 25; // Slower animation for dying } else if (self.animationState === 'idle') { frameSpeed = 30; // Very slow animation for idle } if (self.animationTimer >= frameSpeed) { self.animationTimer = 0; // Hide current frame self.ogreFrames[self.currentFrame - 1].visible = false; // Move to next frame based on animation state if (self.animationState === 'walking') { self.currentFrame++; if (self.currentFrame > 4) { self.currentFrame = 1; } } else if (self.animationState === 'attacking') { // Cycle through frames 2-4 for attacking self.currentFrame++; if (self.currentFrame > 4) { self.currentFrame = 2; } } else if (self.animationState === 'dying') { // Play frames 3-4 once for dying if (self.currentFrame < 4) { self.currentFrame++; } } else if (self.animationState === 'idle') { // Cycle between frames 1-2 for idle self.currentFrame = self.currentFrame === 1 ? 2 : 1; } // Show new frame self.ogreFrames[self.currentFrame - 1].visible = true; } // Progressive speed increase - increase speed by 150% over 10 seconds if (!self.speedTweenStarted) { self.speedTweenStarted = true; var targetSpeed = self.speed * 2.5; // Increase speed by 150% tween(self, { speed: targetSpeed }, { duration: 10000, // 10 seconds easing: tween.easeOut }); } // Always move directly toward the wizard to ensure ogres reach the player if (wizard) { // Calculate direction from ogre to wizard var dx = wizard.x - self.x; var dy = wizard.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { // Normalize direction and apply speed self.x += dx / distance * self.speed; self.y += dy / distance * self.speed; } // Face toward the wizard - flip horizontally based on wizard position if (dx < 0) { // Wizard is to the left - flip to face left for (var frameIdx = 0; frameIdx < self.ogreFrames.length; frameIdx++) { self.ogreFrames[frameIdx].scaleX = -2.0; // Flip to face left } } else { // Wizard is to the right - face right for (var frameIdx = 0; frameIdx < self.ogreFrames.length; frameIdx++) { self.ogreFrames[frameIdx].scaleX = 2.0; // Normal orientation to face right } } } else { // Fallback: move down if wizard doesn't exist self.y += self.speed; } }; self.takeDamage = function (damage) { self.health -= damage; self.hitsTaken++; // Change animation state to attacking when hit self.animationState = 'attacking'; // Flash red when hit LK.effects.flashObject(self, 0xFF0000, 200); // Return to walking animation after brief attacking animation tween({}, {}, { duration: 400, onFinish: function onFinish() { if (self.animationState === 'attacking') { self.animationState = 'walking'; } } }); // Ogres need 2 hits to die if (self.hitsTaken >= self.hitsToKill) { self.die(); } }; self.down = function (x, y, obj) { // Set this ogre as the selected target selectedEnemy = self; // Visual feedback for selection LK.effects.flashObject(self, 0xFFFF00, 500); // Yellow flash to indicate selection // Create projectile from wizard to ogre when ogre is tapped if (wizard && projectiles.length < 10) { // Limit projectiles to prevent spam var projectile = game.addChild(new Projectile()); projectile.x = wizard.x; projectile.y = wizard.y; // Calculate direction from wizard to ogre var dx = self.x - wizard.x; var dy = self.y - wizard.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { projectile.direction.x = dx / distance; projectile.direction.y = dy / distance; } projectile.targetEnemy = self; // Set the target for this projectile projectiles.push(projectile); LK.getSound('spellCast').play(); } }; self.die = function () { // Change animation state to dying self.animationState = 'dying'; self.currentFrame = 3; // Start dying animation from frame 3 // Play pain sound when ogre dies LK.getSound('painSound').play(); // Drop coin var coin = game.addChild(new Coin()); coin.x = self.x; coin.y = self.y - 50; coin.isAnimating = true; coins.push(coin); // Animate coin moving toward coin counter 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, easing: tween.easeOut, onFinish: function onFinish() { // Increment coin counter after animation coinCounter++; coinText.setText('Coins: ' + coinCounter); // Remove coin from array and destroy for (var i = coins.length - 1; i >= 0; i--) { if (coins[i] === coin) { coins.splice(i, 1); break; } } coin.destroy(); } }); // Increment enemy kill counter enemyKillCounter++; killCountText.setText('Puntuacion: ' + enemyKillCounter); // Add experience to wizard wizard.gainExperience(25); // Life drain upgrade - 5% chance to restore 10 health if (wizard.lifeDrainLevel && wizard.lifeDrainLevel > 0) { var drainChance = Math.random(); if (drainChance < 0.05) { // 5% chance wizard.health = Math.min(wizard.health + 10, wizard.maxHealth); updateHealthBar(); // Visual feedback for life drain LK.effects.flashObject(wizard, 0x00FF00, 300); } } // Clear selected enemy if this was the target if (selectedEnemy === self) { selectedEnemy = null; } // Remove from ogres array for (var i = ogres.length - 1; i >= 0; i--) { if (ogres[i] === self) { ogres.splice(i, 1); break; } } self.destroy(); LK.setScore(LK.getScore() + 15); }; return self; }); var Projectile = Container.expand(function () { var self = Container.call(this); // Create glowing light effect behind projectile var lightGlow = self.attachAsset('projectileGlow', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.0, scaleY: 2.0 }); lightGlow.alpha = 0.15; lightGlow.tint = 0x44aaff; // Blue-white glow // Animate the glow effect tween(lightGlow, { scaleX: 3.5, scaleY: 3.5, alpha: 0.05 }, { duration: 800, easing: tween.easeInOut }); tween(lightGlow, { rotation: Math.PI * 4 }, { duration: 1000, easing: tween.linear }); var projectileGraphics = self.attachAsset('projectile', { anchorX: 0.5, anchorY: 0.5, scaleX: 3.0, scaleY: 3.0 }); self.speed = 50; self.direction = { x: 0, y: 0 }; self.lastIntersecting = {}; self.targetEnemy = null; // The specific enemy this projectile should damage self.update = function () { // Move projectile self.x += self.direction.x * self.speed; self.y += self.direction.y * self.speed; // Rotate projectile to point towards movement direction var angle = Math.atan2(self.direction.y, self.direction.x); projectileGraphics.rotation = angle + Math.PI / 2; // Add 90 degrees so top points forward // Add pulsing light effect if (lightGlow && lightGlow.parent) { var pulse = 1 + Math.sin(LK.ticks * 0.3) * 0.4; lightGlow.scaleX = 2.0 * pulse; lightGlow.scaleY = 2.0 * pulse; var alphaFlicker = 0.1 + Math.sin(LK.ticks * 0.5) * 0.08; lightGlow.alpha = alphaFlicker; } // Remove if off screen if (self.x < -50 || self.x > 2098 || self.y < -50 || self.y > 2782) { self.removeFromGame(); return; } // Only check collision with the target enemy if one is set if (self.targetEnemy && self.targetEnemy.parent) { if (!self.lastIntersecting.target) { self.lastIntersecting.target = false; } var currentTargetIntersecting = self.intersects(self.targetEnemy); if (!self.lastIntersecting.target && currentTargetIntersecting) { // Hit the target enemy self.targetEnemy.takeDamage(50); self.removeFromGame(); return; } self.lastIntersecting.target = currentTargetIntersecting; } }; self.removeFromGame = function () { // Remove from projectiles array for (var i = projectiles.length - 1; i >= 0; i--) { if (projectiles[i] === self) { projectiles.splice(i, 1); break; } } self.destroy(); }; return self; }); var UpgradeMenu = Container.expand(function () { var self = Container.call(this); // Set high z-index to ensure menu appears above all game elements self.zIndex = 1000; // Cooldown system for upgrade menu self.cooldownDuration = 120; // 2 seconds at 60fps self.cooldownTimer = 0; // Background removed - no overlay needed // Title text var titleText = new Text2('UPGRADES', { size: 120, fill: 0xFFD700, font: "monospace" }); titleText.anchor.set(0.5, 0.5); titleText.x = 2048 / 2; titleText.y = 1050; self.addChild(titleText); // Upgrade options var upgradeOptions = [{ name: 'SHIELD', description: 'Block 1 attack' }, { name: 'HEALTH BOOST', description: 'More health' }, { name: 'LIFE DRAIN', description: '5% chance +10 HP' }]; // Create upgrade buttons for (var i = 0; i < upgradeOptions.length; i++) { var upgrade = upgradeOptions[i]; var xPos = 2048 / 2 - 400 + i * 400; // Center the 3 upgrades horizontally var yPos = 1300; // Same Y position for all // Upgrade button background var upgradeBtn = self.attachAsset('pathSelector', { anchorX: 0.5, anchorY: 0.5, x: xPos, y: yPos, scaleX: 6, scaleY: 4 }); upgradeBtn.upgradeIndex = i; upgradeBtn.tint = 0x8B008B; // Upgrade text var upgradeText = new Text2(upgrade.name + '\n' + upgrade.description + '\nCost: 10 coins', { size: 45, fill: 0xFFFFFF, font: "monospace" }); upgradeText.anchor.set(0.5, 0.5); upgradeText.x = xPos; upgradeText.y = yPos; self.addChild(upgradeText); } // Close button var closeBtn = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 1600, scaleX: 2, scaleY: 2 }); closeBtn.tint = 0xFF0000; var closeText = new Text2('CLOSE', { size: 80, fill: 0xFFFFFF, font: "monospace" }); closeText.anchor.set(0.5, 0.5); closeText.x = 2048 / 2; closeText.y = 1600; self.addChild(closeText); // Update method for cooldown timer self.update = function () { if (self.cooldownTimer > 0) { self.cooldownTimer--; // Apply visual feedback during cooldown var cooldownAlpha = 0.3 + self.cooldownTimer / self.cooldownDuration * 0.7; self.alpha = cooldownAlpha; } else { self.alpha = 1.0; // Full opacity when cooldown is over } }; // Handle upgrade purchases self.down = function (x, y, obj) { // Check if cooldown is active if (self.cooldownTimer > 0) { // Flash red to indicate cooldown is active LK.effects.flashScreen(0xFF0000, 200); return; // Exit early if cooldown is active } // Determine which upgrade was clicked based on X position (horizontal layout) var upgradeIndex = -1; if (x >= 424 && x <= 724 && y >= 1150 && y <= 1450) { upgradeIndex = 0; // Shield } else if (x >= 824 && x <= 1124 && y >= 1150 && y <= 1450) { upgradeIndex = 1; // Health Boost } else if (x >= 1224 && x <= 1524 && y >= 1150 && y <= 1450) { upgradeIndex = 2; // Life Drain } // Check if upgrade button was clicked if (upgradeIndex !== -1) { // Close menu immediately when any upgrade option is selected self.closeMenu(); if (coinCounter >= 10) { coinCounter -= 10; coinText.setText('Coins: ' + coinCounter); // Apply upgrade based on index switch (upgradeIndex) { case 0: // Shield - give one-time protection if (!wizard.shieldActive) { wizard.shieldActive = false; } wizard.shieldActive = true; // Visual feedback for shield activation LK.effects.flashObject(wizard, 0x00BFFF, 500); // Add shield activation animation effect tween(wizard.shieldGraphics, { scaleX: 5, scaleY: 5, alpha: 1.0 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { tween(wizard.shieldGraphics, { scaleX: 3, scaleY: 3, alpha: 0.7 }, { duration: 300, easing: tween.easeIn }); } }); break; case 1: // Health Boost wizard.maxHealth += 25; wizard.health = Math.min(wizard.health + 25, wizard.maxHealth); updateHealthBar(); break; case 2: // Life Drain - enable health restoration on kill if (!wizard.lifeDrainLevel) { wizard.lifeDrainLevel = 0; } wizard.lifeDrainLevel++; break; } // Visual feedback - flash the upgrade area green LK.effects.flashScreen(0x00FF00, 300); } else { // Not enough coins - flash red LK.effects.flashScreen(0xFF0000, 300); } } else if (y >= 1450 && y <= 1750) { // Close button area clicked self.closeMenu(); } }; self.closeMenu = function () { self.visible = false; gameStarted = true; // Resume appropriate music based on game progress if (enemyKillCounter >= 25) { LK.playMusic('mysticalAmbient', { volume: 0.6, fade: { start: 0, end: 0.6, duration: 1000 } }); } else if (enemyKillCounter >= 10) { LK.playMusic('epicBattle', { volume: 0.8, fade: { start: 0, end: 0.8, duration: 1000 } }); } else { LK.playMusic('medievalTheme', { volume: 0.7, fade: { start: 0, end: 0.7, duration: 1000 } }); } }; return self; }); var Wizard = Container.expand(function () { var self = Container.call(this); // Animation system for wizard self.currentFrame = 1; self.animationTimer = 0; self.animationSpeed = 18; // Change frame every 18 ticks (300ms at 60fps) // Create all wizard graphics frames and store them self.wizardFrames = []; for (var i = 1; i <= 4; i++) { var frameGraphics = self.attachAsset('wizard' + i, { anchorX: 0.5, anchorY: 1.0, scaleX: 2.5, scaleY: 2.5 }); frameGraphics.visible = i === 1; // Only show first frame initially self.wizardFrames.push(frameGraphics); } // Create shield visual effect self.shieldGraphics = self.attachAsset('shield', { anchorX: 0.5, anchorY: 0.5, scaleX: 3, scaleY: 3 }); self.shieldGraphics.alpha = 0.7; self.shieldGraphics.visible = false; self.attackCooldown = 0; self.level = 1; self.experience = 0; self.health = 100; self.maxHealth = 100; self.spellPowerLevel = 0; // Track spell power upgrades self.shieldActive = false; // Track shield status self.update = function () { 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; } // Animation system - cycle through wizard frames self.animationTimer++; if (self.animationTimer >= self.animationSpeed) { self.animationTimer = 0; // Hide current frame self.wizardFrames[self.currentFrame - 1].visible = false; // Move to next frame self.currentFrame++; if (self.currentFrame > 4) { self.currentFrame = 1; } // Show new frame self.wizardFrames[self.currentFrame - 1].visible = true; } }; self.attack = function (direction) { if (self.attackCooldown <= 0) { // Default direction if none specified if (direction === undefined) { direction = 0; // Default to center path } // Get attack angle based on path direction var attackAngle = pathAngles[direction]; var attackDistance = 100; // Calculate spell position based on attack direction var spellX = self.x + Math.cos(attackAngle) * attackDistance; var spellY = self.y + Math.sin(attackAngle) * attackDistance; // Create spell effect var spell = game.addChild(LK.getAsset('spell', { anchorX: 0.5, anchorY: 0.5, x: spellX, y: spellY, scaleX: 0.5, scaleY: 0.5 })); // Animate spell with magical effects tween(spell, { scaleX: 2, scaleY: 2, alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { spell.destroy(); } }); // Add rotation animation to spell tween(spell, { rotation: Math.PI * 2 }, { duration: 500, easing: tween.linear }); self.attackCooldown = 30; // 0.5 seconds at 60fps LK.getSound('spellCast').play(); // Base damage without spell power upgrades var totalDamage = 50; // Attack enemies in the specified direction/path for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; if (enemy.pathIndex === direction) { // Only hit enemies on exact same path - no distance validation enemy.takeDamage(totalDamage); } } // Attack ogres in the specified direction/path for (var i = ogres.length - 1; i >= 0; i--) { var ogre = ogres[i]; if (ogre.pathIndex === direction) { // Only hit ogres on exact same path - no distance validation ogre.takeDamage(totalDamage); } } // Attack knights in the specified direction/path for (var i = knights.length - 1; i >= 0; i--) { var knight = knights[i]; if (knight.pathIndex === direction) { // Only hit knights on exact same path - no distance validation knight.takeDamage(totalDamage); } } return true; } return false; }; self.gainExperience = function (amount) { self.experience += amount; var expNeeded = self.level * 100; if (self.experience >= expNeeded) { self.levelUp(); } }; self.levelUp = function () { self.level++; self.experience = 0; // Visual level up effect LK.effects.flashObject(self, 0xFFD700, 500); }; self.takeDamage = function (damage) { // Check if shield is active if (self.shieldActive) { // Shield blocks the damage self.shieldActive = false; // Visual feedback for shield use LK.effects.flashObject(self, 0x00BFFF, 300); // Start shield regeneration timer (10 seconds) tween({}, {}, { duration: 10000, // 10 seconds onFinish: function onFinish() { // Regenerate shield after 10 seconds self.shieldActive = true; // Visual feedback for shield regeneration LK.effects.flashObject(self, 0x00BFFF, 500); // Add shield regeneration animation tween(self.shieldGraphics, { scaleX: 5, scaleY: 5, alpha: 1.0 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { tween(self.shieldGraphics, { scaleX: 3, scaleY: 3, alpha: 0.7 }, { duration: 400, easing: tween.easeIn }); } }); } }); // No damage taken, shield absorbed it return; } self.health -= damage; if (self.health <= 0) { self.health = 0; // Game over when health reaches 0 LK.effects.flashScreen(0xFF0000, 1000); LK.showGameOver(); } // Update health bar updateHealthBar(); // Flash red when hit LK.effects.flashObject(self, 0xFF0000, 200); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 // Black background for pixel art }); /**** * Game Code ****/ // Game state variables var gameStarted = false; var gameMenu; var upgradeMenu; var upgradeMenuShown = false; var secondUpgradeMenuShown = false; var selectedEnemy = null; // Track currently selected enemy for projectile targeting // Game arrays to track objects var enemies = []; var ogres = []; var knights = []; var coins = []; var projectiles = []; // Create and show game menu gameMenu = game.addChild(new GameMenu()); // Create upgrade menu (initially hidden) upgradeMenu = game.addChild(new UpgradeMenu()); upgradeMenu.visible = false; // Create 5 stone paths leading toward the wizard position var paths = []; var knightX = 2048 / 2; var knightY = 2732 - 250; // Create paths: all pointing toward the wizard position var pathAngles = [-Math.PI / 2, // Center (straight down toward wizard) -Math.PI / 3, // Diagonal right toward wizard -2 * Math.PI / 3, // Diagonal left toward wizard Math.PI / 6, // Path 4: opposite to path 2 (diagonal left) 5 * Math.PI / 6 // Path 5: opposite to path 3 (diagonal right) ]; // 5 paths: all directed toward wizard position // Create multiple stone segments for each path to form visible stone roads for (var p = 0; p < 5; p++) { var angle = pathAngles[p]; // Calculate distance from spawn point to wizard position var wizardX = knightX; var wizardY = 2732 - 600; // Wizard position var spawnDistance = p === 0 ? 2000 : p === 3 || p === 4 ? 1200 : 1800; // Distance from wizard to spawn point // Set spawn points to screen edges for all paths var spawnX, spawnY; if (p === 0) { // Center path - spawn at top edge spawnX = 2048 / 2; spawnY = -100; } else if (p === 1) { // Path 2 - spawn at top right edge spawnX = 2048 + 50; spawnY = -50; } else if (p === 2) { // Path 3 - spawn at top left edge spawnX = -50; spawnY = -50; } else if (p === 3) { // Path 4 - spawn at left edge spawnX = -100; spawnY = 2732 / 2 + 400; } else if (p === 4) { // Path 5 - spawn at right edge spawnX = 2048 + 100; spawnY = 2732 / 2 + 400; } // Calculate actual path length from spawn point to wizard var actualPathLength = Math.sqrt((spawnX - wizardX) * (spawnX - wizardX) + (spawnY - wizardY) * (spawnY - wizardY)); var segmentSize = 120; // Size of each stone segment var numSegments = Math.floor(actualPathLength / segmentSize); // Create individual stone segments along the path from spawn point toward wizard for (var s = 0; s < numSegments; s++) { var segmentDistance = s * segmentSize + segmentSize / 2; var segmentX = spawnX - Math.cos(angle) * segmentDistance; var segmentY = spawnY - Math.sin(angle) * segmentDistance; // Only create segments that are within the visible game area if (segmentX >= -100 && segmentX <= 2148 && segmentY >= -100 && segmentY <= 2832) { // Create stone path segment var stoneSegment = game.addChild(LK.getAsset('stonePath', { anchorX: 0.5, anchorY: 0.5, x: segmentX, y: segmentY, scaleX: 2.0, scaleY: 2.0, rotation: angle + Math.PI / 2 // Rotate stone to align with path })); // Make stone paths completely invisible stoneSegment.alpha = 0; // Make completely invisible stoneSegment.visible = false; // Initially hidden, will be shown when game starts stoneSegment.pathIndex = p; } } } // Create invisible path collision areas for touch detection for (var p = 0; p < 5; p++) { var angle = pathAngles[p]; // Calculate distance from spawn point to wizard position var wizardX = knightX; var wizardY = 2732 - 600; // Wizard position var spawnDistance = p === 0 ? 3000 : p === 3 || p === 4 ? 1200 : 1800; // Distance from wizard to spawn point var spawnX = wizardX + Math.cos(angle) * spawnDistance; var spawnY = wizardY + Math.sin(angle) * spawnDistance; // Move all spawn points to screen edges if (p === 0) { // Center path - spawn at top edge spawnX = 2048 / 2; spawnY = -100; } else if (p === 1) { // Path 2 - spawn at top right edge spawnX = 2048 + 50; spawnY = -50; } else if (p === 2) { // Path 3 - spawn at top left edge spawnX = -50; spawnY = -50; } else if (p === 3) { // Path 4 - spawn at left edge spawnX = -100; spawnY = 2732 / 2 + 400; } else if (p === 4) { // Path 5 - spawn at right edge spawnX = 2048 + 100; spawnY = 2732 / 2 + 400; } // Calculate actual path length from spawn point to wizard var actualPathLength = Math.sqrt((spawnX - wizardX) * (spawnX - wizardX) + (spawnY - wizardY) * (spawnY - wizardY)); var centerX = (spawnX + wizardX) / 2; var centerY = (spawnY + wizardY) / 2; // Create invisible collision path var path = game.addChild(LK.getAsset('pathSelector', { anchorX: 0.5, anchorY: 0.5, x: centerX, y: centerY, scaleX: 4, scaleY: actualPathLength / 60, rotation: angle + Math.PI / 2 })); // Make collision path completely invisible path.alpha = 0; path.visible = false; path.pathIndex = p; // Add numbered text label for each path var pathNumber = new Text2((p + 1).toString(), { size: 120, fill: 0xFFD700, font: "monospace" }); pathNumber.anchor.set(0.5, 0.5); // Position number directly at spawn point pathNumber.x = spawnX; pathNumber.y = spawnY - 80; // Position slightly above spawn point pathNumber.visible = false; // Initially hidden, will be shown when game starts pathNumber.pathIndex = p; game.addChild(pathNumber); // Add touch handler for directional attacks path.down = function (x, y, obj) { // Attack in this path's direction wizard.attack(obj.pathIndex); // Visual feedback - no flash for invisible paths // Paths remain invisible when tapped }; paths.push(path); } // Pixel art scaling handled by engine automatically // Set fondodelacueva as the actual game background var backgroundMap = game.addChild(LK.getAsset('fondodelacueva', { anchorX: 0, anchorY: 0, scaleX: 19.5, scaleY: 26.0, x: 0, y: 0 })); // Send background to the back but use a less extreme z-index backgroundMap.zIndex = -100; // Hide background initially during menu backgroundMap.visible = false; backgroundMap.alpha = 1.0; // Create wizard var wizard = game.addChild(new Wizard()); wizard.x = knightX; wizard.y = 2732 - 600; // Position wizard higher on screen wizard.visible = false; // UI Elements // Removed scoreText and levelText to eliminate stray characters in top right var coinCounter = 0; var enemyKillCounter = 0; var coinText = new Text2('Coins: 0', { size: 60, fill: 0xFFD700, font: "monospace" }); coinText.anchor.set(0, 0); LK.gui.topLeft.addChild(coinText); coinText.x = 120; coinText.y = 90; coinText.visible = false; var killCountText = new Text2('Puntuacion: 0', { size: 60, fill: 0xFF6B6B, font: "monospace" }); killCountText.anchor.set(0, 0); LK.gui.topLeft.addChild(killCountText); killCountText.x = 120; killCountText.y = 150; killCountText.visible = false; var tapText = new Text2('TAP TO CAST SPELLS!', { size: 100, fill: 0xFF6B6B, font: "monospace" }); tapText.anchor.set(0.5, 0.5); LK.gui.center.addChild(tapText); tapText.y = -200; tapText.visible = false; // Health bar UI var healthBarBg = LK.getAsset('healthBarBg', { anchorX: 0, anchorY: 0 }); LK.gui.topLeft.addChild(healthBarBg); healthBarBg.x = 120; healthBarBg.y = 20; healthBarBg.visible = false; var healthBar = LK.getAsset('healthBar', { anchorX: 0, anchorY: 0 }); LK.gui.topLeft.addChild(healthBar); healthBar.x = 120; healthBar.y = 20; healthBar.visible = false; var healthText = new Text2('Health: 100/100', { size: 50, fill: 0xFFFFFF, font: "monospace" }); healthText.anchor.set(0, 0); LK.gui.topLeft.addChild(healthText); healthText.x = 120; healthText.y = 50; healthText.visible = false; function updateHealthBar() { var healthPercent = wizard.health / wizard.maxHealth; healthBar.scaleX = healthPercent; healthText.setText('Health: ' + wizard.health + '/' + wizard.maxHealth); // Change color based on health if (healthPercent > 0.6) { healthBar.tint = 0x00ff00; // Green } else if (healthPercent > 0.3) { healthBar.tint = 0xffff00; // Yellow } else { healthBar.tint = 0xff0000; // Red } } // Enemy spawning variables var enemySpawnTimer = 0; var lastSpawnedPath = -1; // Track the last spawned path var consecutiveSpawns = 0; // Track consecutive spawns from same path // Cooldown system variables var pathLastSpawnTime = [-1, -1, -1, -1, -1]; // Track last spawn time for each path var pathConsecutiveSpawns = [0, 0, 0, 0, 0]; // Track consecutive spawns per path var pathCooldownDuration = 300; // 5 seconds at 60fps // Game input handling game.down = function (x, y, obj) { // Check if a path was tapped for directional attack var pathTapped = false; for (var p = 0; p < paths.length; p++) { var path = paths[p]; // Convert tap position to path's local coordinates var localPos = path.toLocal({ x: x, y: y }); // Check if tap is within path bounds if (Math.abs(localPos.x) < path.width / 2 && Math.abs(localPos.y) < path.height / 2) { // Attack in this path's direction wizard.attack(path.pathIndex); pathTapped = true; break; } } // No default attack - player must tap a path to attack }; // Main game update loop game.update = function () { // Sort children by z-index to ensure proper rendering order game.children.sort(function (a, b) { return (a.zIndex || 0) - (b.zIndex || 0); }); // Only update game logic if game has started if (!gameStarted) { return; } // Change music to epic battle theme at 10 enemies killed if (enemyKillCounter === 10) { LK.playMusic('epicBattle', { volume: 0.8, fade: { start: 0, end: 0.8, duration: 1500 } }); } // Change to mystical ambient music at 25 enemies killed if (enemyKillCounter === 25) { LK.playMusic('mysticalAmbient', { volume: 0.6, fade: { start: 0, end: 0.6, duration: 2000 } }); } // Show upgrade menu after 12 enemies are killed if (enemyKillCounter >= 12 && !upgradeMenuShown) { upgradeMenuShown = true; upgradeMenu.visible = true; upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown gameStarted = false; // Pause game while menu is open LK.stopMusic(); // Pause music // Clear all enemies on screen when upgrade menu is shown for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; enemies.splice(i, 1); enemy.destroy(); } // Clear all ogres on screen when upgrade menu is shown for (var i = ogres.length - 1; i >= 0; i--) { var ogre = ogres[i]; ogres.splice(i, 1); ogre.destroy(); } // Clear all knights on screen when upgrade menu is shown for (var i = knights.length - 1; i >= 0; i--) { var knight = knights[i]; knights.splice(i, 1); knight.destroy(); } } // Show upgrade menu again after 35 enemies are killed if (enemyKillCounter >= 35 && upgradeMenuShown && !secondUpgradeMenuShown) { secondUpgradeMenuShown = true; upgradeMenu.visible = true; upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown gameStarted = false; // Pause game while menu is open LK.stopMusic(); // Pause music // Clear all enemies on screen when upgrade menu is shown for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; enemies.splice(i, 1); enemy.destroy(); } // Clear all ogres on screen when upgrade menu is shown for (var i = ogres.length - 1; i >= 0; i--) { var ogre = ogres[i]; ogres.splice(i, 1); ogre.destroy(); } // Clear all knights on screen when upgrade menu is shown for (var i = knights.length - 1; i >= 0; i--) { var knight = knights[i]; knights.splice(i, 1); knight.destroy(); } } // Reset consecutive spawns for paths that have cooled down (runs every frame) for (var pathIdx = 0; pathIdx < 5; pathIdx++) { // Check if enough time has passed since last spawn (cooldown expired) if (pathLastSpawnTime[pathIdx] !== -1 && LK.ticks - pathLastSpawnTime[pathIdx] > pathCooldownDuration) { // Reset consecutive spawns for this path due to cooldown pathConsecutiveSpawns[pathIdx] = 0; } } // Progressive difficulty system based on kills var difficultyLevel = Math.floor(enemyKillCounter / 10); // Every 10 kills increases difficulty var currentSpawnRate = Math.max(30, 90 - difficultyLevel * 8); // Faster spawning over time var enemyHealthMultiplier = 1 + difficultyLevel * 0.3; // 30% more health per difficulty level var enemySpeedMultiplier = 1 + difficultyLevel * 0.35; // 35% faster per difficulty level // Spawn enemies enemySpawnTimer++; if (enemySpawnTimer >= currentSpawnRate) { enemySpawnTimer = 0; var enemy = game.addChild(new Enemy()); // Apply difficulty scaling to enemy stats enemy.health = Math.floor(100 * enemyHealthMultiplier); enemy.maxHealth = enemy.health; enemy.speed = 3 * enemySpeedMultiplier; // Spawn enemy at a random path entrance var randomPathIndex; if (enemyKillCounter < 5) { // First 5 enemies spawn from center path only randomPathIndex = 0; // Center path } else { // After 5 enemies, spawn from any path but limit consecutive spawns with cooldown system // Build available paths list var availablePaths = []; for (var pathIdx = 0; pathIdx < 5; pathIdx++) { // If path has spawned less than 2 consecutive times, it's available if (pathConsecutiveSpawns[pathIdx] < 2) { availablePaths.push(pathIdx); } } // If no paths available, reset ALL paths to ensure balanced distribution if (availablePaths.length === 0) { // Reset all paths' consecutive spawn counters for (var pathIdx = 0; pathIdx < 5; pathIdx++) { pathConsecutiveSpawns[pathIdx] = 0; availablePaths.push(pathIdx); } } randomPathIndex = availablePaths[Math.floor(Math.random() * availablePaths.length)]; } // Update path-specific spawn tracking // Always increment consecutive spawns for the selected path pathConsecutiveSpawns[randomPathIndex]++; // Update path spawn time and global tracking pathLastSpawnTime[randomPathIndex] = LK.ticks; lastSpawnedPath = randomPathIndex; consecutiveSpawns = pathConsecutiveSpawns[randomPathIndex]; var pathAngle = pathAngles[randomPathIndex]; // Store the path information on the enemy enemy.pathIndex = randomPathIndex; enemy.pathAngle = pathAngle; // Calculate spawn position at screen edges if (randomPathIndex === 0) { // Center path - spawn at top edge enemy.x = 2048 / 2; enemy.y = -100; } else if (randomPathIndex === 1) { // Path 2 - spawn at top right edge enemy.x = 2048 + 50; enemy.y = -50; } else if (randomPathIndex === 2) { // Path 3 - spawn at top left edge enemy.x = -50; enemy.y = -50; } else if (randomPathIndex === 3) { // Path 4 - spawn at left edge enemy.x = -100; enemy.y = 2732 / 2 + 400; } else if (randomPathIndex === 4) { // Path 5 - spawn at right edge enemy.x = 2048 + 100; enemy.y = 2732 / 2 + 400; } // Make sure enemies spawn within screen bounds enemy.x = Math.max(50, Math.min(1998, enemy.x)); enemy.y = Math.max(-200, Math.min(2732 + 100, enemy.y)); enemy.lastX = enemy.x; enemies.push(enemy); } // Spawn ogres after 15 seconds (900 ticks at 60fps) if (LK.ticks >= 900 && LK.ticks % 180 === 0) { // Every 3 seconds after 15 seconds var ogre = game.addChild(new Ogre()); // Apply difficulty scaling to ogre stats ogre.health = Math.floor(150 * enemyHealthMultiplier); // Ogres have more base health ogre.maxHealth = ogre.health; ogre.speed = 2.5 * enemySpeedMultiplier; // Ogres are slightly slower // Spawn ogre at a random path entrance var ogrePathIndex = Math.floor(Math.random() * 5); var ogrePathAngle = pathAngles[ogrePathIndex]; // Store the path information on the ogre ogre.pathIndex = ogrePathIndex; ogre.pathAngle = ogrePathAngle; // Calculate spawn position at screen edges if (ogrePathIndex === 0) { // Center path - spawn at top edge ogre.x = 2048 / 2; ogre.y = -100; } else if (ogrePathIndex === 1) { // Path 2 - spawn at top right edge ogre.x = 2048 + 50; ogre.y = -50; } else if (ogrePathIndex === 2) { // Path 3 - spawn at top left edge ogre.x = -50; ogre.y = -50; } else if (ogrePathIndex === 3) { // Path 4 - spawn at left edge ogre.x = -100; ogre.y = 2732 / 2 + 400; } else if (ogrePathIndex === 4) { // Path 5 - spawn at right edge ogre.x = 2048 + 100; ogre.y = 2732 / 2 + 400; } // Make sure ogres spawn within screen bounds ogre.x = Math.max(50, Math.min(1998, ogre.x)); ogre.y = Math.max(-200, Math.min(2732 + 100, ogre.y)); ogre.lastX = ogre.x; ogres.push(ogre); } // Spawn knights after 30 enemies killed if (enemyKillCounter >= 30 && LK.ticks % 300 === 0) { // Every 5 seconds after 30th enemy var knight = game.addChild(new Knight()); // Apply difficulty scaling to knight stats knight.health = Math.floor(200 * enemyHealthMultiplier); // Knights have highest base health knight.maxHealth = knight.health; knight.speed = 2 * enemySpeedMultiplier; // Knights are slowest // Spawn knight at a random path entrance var knightPathIndex = Math.floor(Math.random() * 5); var knightPathAngle = pathAngles[knightPathIndex]; // Store the path information on the knight knight.pathIndex = knightPathIndex; knight.pathAngle = knightPathAngle; // Calculate spawn position at screen edges if (knightPathIndex === 0) { // Center path - spawn at top edge knight.x = 2048 / 2; knight.y = -100; } else if (knightPathIndex === 1) { // Path 2 - spawn at top right edge knight.x = 2048 + 50; knight.y = -50; } else if (knightPathIndex === 2) { // Path 3 - spawn at top left edge knight.x = -50; knight.y = -50; } else if (knightPathIndex === 3) { // Path 4 - spawn at left edge knight.x = -100; knight.y = 2732 / 2 + 400; } else if (knightPathIndex === 4) { // Path 5 - spawn at right edge knight.x = 2048 + 100; knight.y = 2732 / 2 + 400; } // Make sure knights spawn within screen bounds knight.x = Math.max(50, Math.min(1998, knight.x)); knight.y = Math.max(-200, Math.min(2732 + 100, knight.y)); knight.lastX = knight.x; knights.push(knight); } // Check enemy-knight collisions and cleanup off-screen enemies for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; // Remove enemies that went off-screen at bottom if (enemy.y > 2732 + 100) { enemies.splice(i, 1); enemy.destroy(); continue; } // Initialize lastIntersecting if not set if (enemy.lastIntersecting === undefined) { enemy.lastIntersecting = false; } // Check for collision transition var currentIntersecting = enemy.intersects(wizard); if (!enemy.lastIntersecting && currentIntersecting) { // Damage wizard when enemy touches for the first time wizard.takeDamage(20); // Remove enemy after dealing damage enemies.splice(i, 1); enemy.destroy(); continue; } // Update last intersecting state enemy.lastIntersecting = currentIntersecting; } // Check ogre-wizard collisions and cleanup off-screen ogres for (var i = ogres.length - 1; i >= 0; i--) { var ogre = ogres[i]; // Remove ogres that went off-screen at bottom if (ogre.y > 2732 + 100) { ogres.splice(i, 1); ogre.destroy(); continue; } // Initialize lastIntersecting if not set if (ogre.lastIntersecting === undefined) { ogre.lastIntersecting = false; } // Check for collision transition var currentOgreIntersecting = ogre.intersects(wizard); if (!ogre.lastIntersecting && currentOgreIntersecting) { // Damage wizard when ogre touches for the first time wizard.takeDamage(30); // Ogres deal 30 damage // Remove ogre after dealing damage ogres.splice(i, 1); ogre.destroy(); continue; } // Update last intersecting state ogre.lastIntersecting = currentOgreIntersecting; } // Check knight-wizard collisions and cleanup off-screen knights for (var i = knights.length - 1; i >= 0; i--) { var knight = knights[i]; // Remove knights that went off-screen at bottom if (knight.y > 2732 + 100) { knights.splice(i, 1); knight.destroy(); continue; } // Initialize lastIntersecting if not set if (knight.lastIntersecting === undefined) { knight.lastIntersecting = false; } // Check for collision transition var currentKnightIntersecting = knight.intersects(wizard); if (!knight.lastIntersecting && currentKnightIntersecting) { // Damage wizard when knight touches for the first time wizard.takeDamage(40); // Knights deal 40 damage // Remove knight after dealing damage knights.splice(i, 1); knight.destroy(); continue; } // Update last intersecting state knight.lastIntersecting = currentKnightIntersecting; } // Make tap text pulse var pulse = 1 + Math.sin(LK.ticks * 0.1) * 0.2; tapText.scale.set(pulse, pulse); }; // Remove tap text after 5 seconds tween({}, {}, { duration: 5000, onFinish: function onFinish() { if (tapText && tapText.parent) { tapText.destroy(); } } });
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Coin = Container.expand(function () {
var self = Container.call(this);
var coinGraphics = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5
});
self.bobOffset = Math.random() * Math.PI * 2;
self.initialY = 0;
self.update = function () {
if (self.initialY === 0) {
self.initialY = self.y;
}
// Only do bobbing animation and collection if not animating to coin counter
if (!self.isAnimating) {
// Bobbing animation
self.y = self.initialY + Math.sin(LK.ticks * 0.1 + self.bobOffset) * 10;
// Check collection by knight
if (knight && self.intersects(knight)) {
self.collect();
}
}
};
self.collect = function () {
LK.getSound('coinCollect').play();
LK.setScore(LK.getScore() + 5);
coinCounter++;
coinText.setText('Coins: ' + coinCounter);
// Remove from coins array
for (var i = coins.length - 1; i >= 0; i--) {
if (coins[i] === self) {
coins.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
// Game arrays to track objects
var Enemy = Container.expand(function () {
var self = Container.call(this);
// Animation system for skeleton
self.currentFrame = 1;
self.animationTimer = 0;
self.animationSpeed = 15; // Change frame every 15 ticks (250ms at 60fps)
self.animationState = 'walking'; // walking, attacking, dying, idle
// Create all skeleton graphics frames and store them
self.skeletonFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset('esqueleto' + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 2.0,
scaleY: 2.0
});
frameGraphics.visible = i === 1; // Only show first frame initially
self.skeletonFrames.push(frameGraphics);
}
// Create invisible hitbox with double size
var hitbox = self.attachAsset('esqueleto1', {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 4,
scaleY: 4
});
hitbox.alpha = 0; // Make hitbox invisible
self.health = 100;
self.maxHealth = 100;
self.speed = 7;
// Note: health, maxHealth and speed will be overridden by difficulty system
self.lastX = 0;
self.update = function () {
// Animation system - cycle through skeleton frames based on state
self.animationTimer++;
var frameSpeed = self.animationSpeed;
if (self.animationState === 'attacking') {
frameSpeed = 8; // Faster animation for attacking
} else if (self.animationState === 'dying') {
frameSpeed = 20; // Slower animation for dying
} else if (self.animationState === 'idle') {
frameSpeed = 25; // Very slow animation for idle
}
if (self.animationTimer >= frameSpeed) {
self.animationTimer = 0;
// Hide current frame
self.skeletonFrames[self.currentFrame - 1].visible = false;
// Move to next frame based on animation state
if (self.animationState === 'walking') {
self.currentFrame++;
if (self.currentFrame > 4) {
self.currentFrame = 1;
}
} else if (self.animationState === 'attacking') {
// Cycle through frames 2-4 for attacking
self.currentFrame++;
if (self.currentFrame > 4) {
self.currentFrame = 2;
}
} else if (self.animationState === 'dying') {
// Play frames 3-4 once for dying
if (self.currentFrame < 4) {
self.currentFrame++;
}
} else if (self.animationState === 'idle') {
// Cycle between frames 1-2 for idle
self.currentFrame = self.currentFrame === 1 ? 2 : 1;
}
// Show new frame
self.skeletonFrames[self.currentFrame - 1].visible = true;
}
// Progressive speed increase - increase speed by 50% over 10 seconds
if (!self.speedTweenStarted) {
self.speedTweenStarted = true;
var targetSpeed = self.speed * 2.5; // Increase speed by 150%
tween(self, {
speed: targetSpeed
}, {
duration: 10000,
// 10 seconds
easing: tween.easeOut
});
}
// Always move directly toward the wizard to ensure enemies reach the player
if (wizard) {
// Calculate direction from enemy to wizard
var dx = wizard.x - self.x;
var dy = wizard.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Normalize direction and apply speed
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
}
// Face toward the wizard - flip horizontally based on wizard position
if (dx < 0) {
// Wizard is to the left - flip to face left
for (var frameIdx = 0; frameIdx < self.skeletonFrames.length; frameIdx++) {
self.skeletonFrames[frameIdx].scaleX = -2.0; // Flip to face left
}
} else {
// Wizard is to the right - face right
for (var frameIdx = 0; frameIdx < self.skeletonFrames.length; frameIdx++) {
self.skeletonFrames[frameIdx].scaleX = 2.0; // Normal orientation to face right
}
}
} else {
// Fallback: move down if wizard doesn't exist
self.y += self.speed;
}
};
self.takeDamage = function (damage) {
self.health -= damage;
// Change animation state to attacking when hit
self.animationState = 'attacking';
// Flash red when hit
LK.effects.flashObject(self, 0xFF0000, 200);
// Return to walking animation after brief attacking animation
tween({}, {}, {
duration: 300,
onFinish: function onFinish() {
if (self.animationState === 'attacking') {
self.animationState = 'walking';
}
}
});
// Enemies die from any projectile hit
self.die();
};
self.down = function (x, y, obj) {
// Set this enemy as the selected target
selectedEnemy = self;
// Visual feedback for selection
LK.effects.flashObject(self, 0xFFFF00, 500); // Yellow flash to indicate selection
// Create projectile from wizard to enemy when enemy is tapped
if (wizard && projectiles.length < 10) {
// Limit projectiles to prevent spam
var projectile = game.addChild(new Projectile());
projectile.x = wizard.x;
projectile.y = wizard.y;
// Calculate direction from wizard to enemy
var dx = self.x - wizard.x;
var dy = self.y - wizard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
projectile.direction.x = dx / distance;
projectile.direction.y = dy / distance;
}
projectile.targetEnemy = self; // Set the target for this projectile
projectiles.push(projectile);
LK.getSound('spellCast').play();
}
};
self.die = function () {
// Change animation state to dying
self.animationState = 'dying';
self.currentFrame = 3; // Start dying animation from frame 3
// Play pain sound when enemy dies
LK.getSound('painSound').play();
// Drop coin
var coin = game.addChild(new Coin());
coin.x = self.x;
coin.y = self.y - 50;
coin.isAnimating = true;
coins.push(coin);
// Animate coin moving toward coin counter
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,
easing: tween.easeOut,
onFinish: function onFinish() {
// Increment coin counter after animation
coinCounter++;
coinText.setText('Coins: ' + coinCounter);
// Remove coin from array and destroy
for (var i = coins.length - 1; i >= 0; i--) {
if (coins[i] === coin) {
coins.splice(i, 1);
break;
}
}
coin.destroy();
}
});
// Increment enemy kill counter
enemyKillCounter++;
killCountText.setText('Puntuacion: ' + enemyKillCounter);
// Add experience to wizard
wizard.gainExperience(25);
// Life drain upgrade - 5% chance to restore 10 health
if (wizard.lifeDrainLevel && wizard.lifeDrainLevel > 0) {
var drainChance = Math.random();
if (drainChance < 0.05) {
// 5% chance
wizard.health = Math.min(wizard.health + 10, wizard.maxHealth);
updateHealthBar();
// Visual feedback for life drain
LK.effects.flashObject(wizard, 0x00FF00, 300);
}
}
// Clear selected enemy if this was the target
if (selectedEnemy === self) {
selectedEnemy = null;
}
// Remove from enemies array
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
self.destroy();
LK.setScore(LK.getScore() + 10);
};
return self;
});
var GameMenu = Container.expand(function () {
var self = Container.call(this);
// Menu background image instead of cave background
var menuBg = self.attachAsset('menuBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 4.0,
scaleY: 4.0
});
menuBg.alpha = 1.0;
// Title text
var titleText = new Text2('WIZARD DEFENDER', {
size: 150,
fill: 0xFFD700,
font: "monospace"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 800;
self.addChild(titleText);
// Instructions text
var instructionsText = new Text2('TAP PATHS TO CAST SPELLS\nDEFEND YOUR CASTLE!', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
instructionsText.anchor.set(0.5, 0.5);
instructionsText.x = 2048 / 2;
instructionsText.y = 1200;
self.addChild(instructionsText);
// Start button
var startButton = self.attachAsset('wizard1', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1600,
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 = 1700;
self.addChild(startButtonText);
// Button interaction
self.down = function (x, y, obj) {
// Start the game by hiding menu
self.startGame();
};
self.startGame = function () {
// Hide menu and start game
self.visible = false;
gameStarted = true;
// Show cave background when game starts
if (backgroundMap) {
backgroundMap.visible = true;
}
// Show all game elements
wizard.visible = true;
for (var i = 0; i < paths.length; i++) {
paths[i].visible = true;
}
// Show all stone path segments and make them visible
for (var i = 0; i < game.children.length; i++) {
var child = game.children[i];
if (child.pathIndex !== undefined && child !== paths[child.pathIndex]) {
child.visible = true;
// Check if it's a stone path segment or path number
if (child.alpha !== undefined && child.setText === undefined) {
child.alpha = 0; // Keep stone paths invisible
}
}
}
coinText.visible = true;
killCountText.visible = true;
tapText.visible = true;
healthBarBg.visible = true;
healthBar.visible = true;
healthText.visible = true;
// Start medieval music
LK.playMusic('medievalTheme', {
volume: 0.7,
fade: {
start: 0,
end: 0.7,
duration: 2000
}
});
};
return self;
});
var Knight = Container.expand(function () {
var self = Container.call(this);
// Animation system for knight
self.currentFrame = 1;
self.animationTimer = 0;
self.animationSpeed = 22; // Change frame every 22 ticks (367ms at 60fps)
self.animationState = 'walking'; // walking, attacking, dying, idle
// Create all knight graphics frames and store them
self.knightFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset('knight' + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 2.0,
scaleY: 2.0
});
frameGraphics.visible = i === 1; // Only show first frame initially
self.knightFrames.push(frameGraphics);
}
// Create invisible hitbox with double size
var hitbox = self.attachAsset('knight1', {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 4,
scaleY: 4
});
hitbox.alpha = 0; // Make hitbox invisible
self.health = 100;
self.maxHealth = 100;
self.speed = 7;
self.hitsToKill = 3; // Knights need 3 hits to die
self.hitsTaken = 0;
// Note: health, maxHealth and speed will be overridden by difficulty system
self.lastX = 0;
self.update = function () {
// Animation system - cycle through knight frames based on state
self.animationTimer++;
var frameSpeed = self.animationSpeed;
if (self.animationState === 'attacking') {
frameSpeed = 12; // Faster animation for attacking
} else if (self.animationState === 'dying') {
frameSpeed = 28; // Slower animation for dying
} else if (self.animationState === 'idle') {
frameSpeed = 35; // Very slow animation for idle
}
if (self.animationTimer >= frameSpeed) {
self.animationTimer = 0;
// Hide current frame
self.knightFrames[self.currentFrame - 1].visible = false;
// Move to next frame based on animation state
if (self.animationState === 'walking') {
self.currentFrame++;
if (self.currentFrame > 4) {
self.currentFrame = 1;
}
} else if (self.animationState === 'attacking') {
// Cycle through frames 2-4 for attacking
self.currentFrame++;
if (self.currentFrame > 4) {
self.currentFrame = 2;
}
} else if (self.animationState === 'dying') {
// Play frames 3-4 once for dying
if (self.currentFrame < 4) {
self.currentFrame++;
}
} else if (self.animationState === 'idle') {
// Cycle between frames 1-2 for idle
self.currentFrame = self.currentFrame === 1 ? 2 : 1;
}
// Show new frame
self.knightFrames[self.currentFrame - 1].visible = true;
}
// Progressive speed increase - increase speed by 150% over 10 seconds
if (!self.speedTweenStarted) {
self.speedTweenStarted = true;
var targetSpeed = self.speed * 2.5; // Increase speed by 150%
tween(self, {
speed: targetSpeed
}, {
duration: 10000,
// 10 seconds
easing: tween.easeOut
});
}
// Always move directly toward the wizard to ensure knights reach the player
if (wizard) {
// Calculate direction from knight to wizard
var dx = wizard.x - self.x;
var dy = wizard.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Normalize direction and apply speed
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
}
// Face toward the wizard - flip horizontally based on wizard position
if (dx < 0) {
// Wizard is to the left - flip to face left
for (var frameIdx = 0; frameIdx < self.knightFrames.length; frameIdx++) {
self.knightFrames[frameIdx].scaleX = -2.0; // Flip to face left
}
} else {
// Wizard is to the right - face right
for (var frameIdx = 0; frameIdx < self.knightFrames.length; frameIdx++) {
self.knightFrames[frameIdx].scaleX = 2.0; // Normal orientation to face right
}
}
} else {
// Fallback: move down if wizard doesn't exist
self.y += self.speed;
}
};
self.takeDamage = function (damage) {
self.health -= damage;
self.hitsTaken++;
// Change animation state to attacking when hit
self.animationState = 'attacking';
// Flash red when hit
LK.effects.flashObject(self, 0xFF0000, 200);
// Return to walking animation after brief attacking animation
tween({}, {}, {
duration: 500,
onFinish: function onFinish() {
if (self.animationState === 'attacking') {
self.animationState = 'walking';
}
}
});
// Knights need 3 hits to die
if (self.hitsTaken >= self.hitsToKill) {
self.die();
}
};
self.down = function (x, y, obj) {
// Set this knight as the selected target
selectedEnemy = self;
// Visual feedback for selection
LK.effects.flashObject(self, 0xFFFF00, 500); // Yellow flash to indicate selection
// Create projectile from wizard to knight when knight is tapped
if (wizard && projectiles.length < 10) {
// Limit projectiles to prevent spam
var projectile = game.addChild(new Projectile());
projectile.x = wizard.x;
projectile.y = wizard.y;
// Calculate direction from wizard to knight
var dx = self.x - wizard.x;
var dy = self.y - wizard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
projectile.direction.x = dx / distance;
projectile.direction.y = dy / distance;
}
projectile.targetEnemy = self; // Set the target for this projectile
projectiles.push(projectile);
LK.getSound('spellCast').play();
}
};
self.die = function () {
// Change animation state to dying
self.animationState = 'dying';
self.currentFrame = 3; // Start dying animation from frame 3
// Play pain sound when knight dies
LK.getSound('painSound').play();
// Drop coin
var coin = game.addChild(new Coin());
coin.x = self.x;
coin.y = self.y - 50;
coin.isAnimating = true;
coins.push(coin);
// Animate coin moving toward coin counter
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,
easing: tween.easeOut,
onFinish: function onFinish() {
// Increment coin counter after animation
coinCounter++;
coinText.setText('Coins: ' + coinCounter);
// Remove coin from array and destroy
for (var i = coins.length - 1; i >= 0; i--) {
if (coins[i] === coin) {
coins.splice(i, 1);
break;
}
}
coin.destroy();
}
});
// Increment enemy kill counter
enemyKillCounter++;
killCountText.setText('Puntuacion: ' + enemyKillCounter);
// Add experience to wizard
wizard.gainExperience(25);
// Life drain upgrade - 5% chance to restore 10 health
if (wizard.lifeDrainLevel && wizard.lifeDrainLevel > 0) {
var drainChance = Math.random();
if (drainChance < 0.05) {
// 5% chance
wizard.health = Math.min(wizard.health + 10, wizard.maxHealth);
updateHealthBar();
// Visual feedback for life drain
LK.effects.flashObject(wizard, 0x00FF00, 300);
}
}
// Clear selected enemy if this was the target
if (selectedEnemy === self) {
selectedEnemy = null;
}
// Remove from knights array
for (var i = knights.length - 1; i >= 0; i--) {
if (knights[i] === self) {
knights.splice(i, 1);
break;
}
}
self.destroy();
LK.setScore(LK.getScore() + 20);
};
return self;
});
var Ogre = Container.expand(function () {
var self = Container.call(this);
// Animation system for ogre
self.currentFrame = 1;
self.animationTimer = 0;
self.animationSpeed = 20; // Change frame every 20 ticks (333ms at 60fps)
self.animationState = 'walking'; // walking, attacking, dying, idle
// Create all ogre graphics frames and store them
self.ogreFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset('ogre' + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 2.0,
scaleY: 2.0
});
frameGraphics.visible = i === 1; // Only show first frame initially
self.ogreFrames.push(frameGraphics);
}
// Create invisible hitbox with double size
var hitbox = self.attachAsset('ogre1', {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 4,
scaleY: 4
});
hitbox.alpha = 0; // Make hitbox invisible
self.health = 100;
self.maxHealth = 100;
self.speed = 7;
self.hitsToKill = 2;
self.hitsTaken = 0;
// Note: health, maxHealth and speed will be overridden by difficulty system
self.lastX = 0;
self.update = function () {
// Animation system - cycle through ogre frames based on state
self.animationTimer++;
var frameSpeed = self.animationSpeed;
if (self.animationState === 'attacking') {
frameSpeed = 10; // Faster animation for attacking
} else if (self.animationState === 'dying') {
frameSpeed = 25; // Slower animation for dying
} else if (self.animationState === 'idle') {
frameSpeed = 30; // Very slow animation for idle
}
if (self.animationTimer >= frameSpeed) {
self.animationTimer = 0;
// Hide current frame
self.ogreFrames[self.currentFrame - 1].visible = false;
// Move to next frame based on animation state
if (self.animationState === 'walking') {
self.currentFrame++;
if (self.currentFrame > 4) {
self.currentFrame = 1;
}
} else if (self.animationState === 'attacking') {
// Cycle through frames 2-4 for attacking
self.currentFrame++;
if (self.currentFrame > 4) {
self.currentFrame = 2;
}
} else if (self.animationState === 'dying') {
// Play frames 3-4 once for dying
if (self.currentFrame < 4) {
self.currentFrame++;
}
} else if (self.animationState === 'idle') {
// Cycle between frames 1-2 for idle
self.currentFrame = self.currentFrame === 1 ? 2 : 1;
}
// Show new frame
self.ogreFrames[self.currentFrame - 1].visible = true;
}
// Progressive speed increase - increase speed by 150% over 10 seconds
if (!self.speedTweenStarted) {
self.speedTweenStarted = true;
var targetSpeed = self.speed * 2.5; // Increase speed by 150%
tween(self, {
speed: targetSpeed
}, {
duration: 10000,
// 10 seconds
easing: tween.easeOut
});
}
// Always move directly toward the wizard to ensure ogres reach the player
if (wizard) {
// Calculate direction from ogre to wizard
var dx = wizard.x - self.x;
var dy = wizard.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Normalize direction and apply speed
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
}
// Face toward the wizard - flip horizontally based on wizard position
if (dx < 0) {
// Wizard is to the left - flip to face left
for (var frameIdx = 0; frameIdx < self.ogreFrames.length; frameIdx++) {
self.ogreFrames[frameIdx].scaleX = -2.0; // Flip to face left
}
} else {
// Wizard is to the right - face right
for (var frameIdx = 0; frameIdx < self.ogreFrames.length; frameIdx++) {
self.ogreFrames[frameIdx].scaleX = 2.0; // Normal orientation to face right
}
}
} else {
// Fallback: move down if wizard doesn't exist
self.y += self.speed;
}
};
self.takeDamage = function (damage) {
self.health -= damage;
self.hitsTaken++;
// Change animation state to attacking when hit
self.animationState = 'attacking';
// Flash red when hit
LK.effects.flashObject(self, 0xFF0000, 200);
// Return to walking animation after brief attacking animation
tween({}, {}, {
duration: 400,
onFinish: function onFinish() {
if (self.animationState === 'attacking') {
self.animationState = 'walking';
}
}
});
// Ogres need 2 hits to die
if (self.hitsTaken >= self.hitsToKill) {
self.die();
}
};
self.down = function (x, y, obj) {
// Set this ogre as the selected target
selectedEnemy = self;
// Visual feedback for selection
LK.effects.flashObject(self, 0xFFFF00, 500); // Yellow flash to indicate selection
// Create projectile from wizard to ogre when ogre is tapped
if (wizard && projectiles.length < 10) {
// Limit projectiles to prevent spam
var projectile = game.addChild(new Projectile());
projectile.x = wizard.x;
projectile.y = wizard.y;
// Calculate direction from wizard to ogre
var dx = self.x - wizard.x;
var dy = self.y - wizard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
projectile.direction.x = dx / distance;
projectile.direction.y = dy / distance;
}
projectile.targetEnemy = self; // Set the target for this projectile
projectiles.push(projectile);
LK.getSound('spellCast').play();
}
};
self.die = function () {
// Change animation state to dying
self.animationState = 'dying';
self.currentFrame = 3; // Start dying animation from frame 3
// Play pain sound when ogre dies
LK.getSound('painSound').play();
// Drop coin
var coin = game.addChild(new Coin());
coin.x = self.x;
coin.y = self.y - 50;
coin.isAnimating = true;
coins.push(coin);
// Animate coin moving toward coin counter
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,
easing: tween.easeOut,
onFinish: function onFinish() {
// Increment coin counter after animation
coinCounter++;
coinText.setText('Coins: ' + coinCounter);
// Remove coin from array and destroy
for (var i = coins.length - 1; i >= 0; i--) {
if (coins[i] === coin) {
coins.splice(i, 1);
break;
}
}
coin.destroy();
}
});
// Increment enemy kill counter
enemyKillCounter++;
killCountText.setText('Puntuacion: ' + enemyKillCounter);
// Add experience to wizard
wizard.gainExperience(25);
// Life drain upgrade - 5% chance to restore 10 health
if (wizard.lifeDrainLevel && wizard.lifeDrainLevel > 0) {
var drainChance = Math.random();
if (drainChance < 0.05) {
// 5% chance
wizard.health = Math.min(wizard.health + 10, wizard.maxHealth);
updateHealthBar();
// Visual feedback for life drain
LK.effects.flashObject(wizard, 0x00FF00, 300);
}
}
// Clear selected enemy if this was the target
if (selectedEnemy === self) {
selectedEnemy = null;
}
// Remove from ogres array
for (var i = ogres.length - 1; i >= 0; i--) {
if (ogres[i] === self) {
ogres.splice(i, 1);
break;
}
}
self.destroy();
LK.setScore(LK.getScore() + 15);
};
return self;
});
var Projectile = Container.expand(function () {
var self = Container.call(this);
// Create glowing light effect behind projectile
var lightGlow = self.attachAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.0,
scaleY: 2.0
});
lightGlow.alpha = 0.15;
lightGlow.tint = 0x44aaff; // Blue-white glow
// Animate the glow effect
tween(lightGlow, {
scaleX: 3.5,
scaleY: 3.5,
alpha: 0.05
}, {
duration: 800,
easing: tween.easeInOut
});
tween(lightGlow, {
rotation: Math.PI * 4
}, {
duration: 1000,
easing: tween.linear
});
var projectileGraphics = self.attachAsset('projectile', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3.0,
scaleY: 3.0
});
self.speed = 50;
self.direction = {
x: 0,
y: 0
};
self.lastIntersecting = {};
self.targetEnemy = null; // The specific enemy this projectile should damage
self.update = function () {
// Move projectile
self.x += self.direction.x * self.speed;
self.y += self.direction.y * self.speed;
// Rotate projectile to point towards movement direction
var angle = Math.atan2(self.direction.y, self.direction.x);
projectileGraphics.rotation = angle + Math.PI / 2; // Add 90 degrees so top points forward
// Add pulsing light effect
if (lightGlow && lightGlow.parent) {
var pulse = 1 + Math.sin(LK.ticks * 0.3) * 0.4;
lightGlow.scaleX = 2.0 * pulse;
lightGlow.scaleY = 2.0 * pulse;
var alphaFlicker = 0.1 + Math.sin(LK.ticks * 0.5) * 0.08;
lightGlow.alpha = alphaFlicker;
}
// Remove if off screen
if (self.x < -50 || self.x > 2098 || self.y < -50 || self.y > 2782) {
self.removeFromGame();
return;
}
// Only check collision with the target enemy if one is set
if (self.targetEnemy && self.targetEnemy.parent) {
if (!self.lastIntersecting.target) {
self.lastIntersecting.target = false;
}
var currentTargetIntersecting = self.intersects(self.targetEnemy);
if (!self.lastIntersecting.target && currentTargetIntersecting) {
// Hit the target enemy
self.targetEnemy.takeDamage(50);
self.removeFromGame();
return;
}
self.lastIntersecting.target = currentTargetIntersecting;
}
};
self.removeFromGame = function () {
// Remove from projectiles array
for (var i = projectiles.length - 1; i >= 0; i--) {
if (projectiles[i] === self) {
projectiles.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
var UpgradeMenu = Container.expand(function () {
var self = Container.call(this);
// Set high z-index to ensure menu appears above all game elements
self.zIndex = 1000;
// Cooldown system for upgrade menu
self.cooldownDuration = 120; // 2 seconds at 60fps
self.cooldownTimer = 0;
// Background removed - no overlay needed
// Title text
var titleText = new Text2('UPGRADES', {
size: 120,
fill: 0xFFD700,
font: "monospace"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 1050;
self.addChild(titleText);
// Upgrade options
var upgradeOptions = [{
name: 'SHIELD',
description: 'Block 1 attack'
}, {
name: 'HEALTH BOOST',
description: 'More health'
}, {
name: 'LIFE DRAIN',
description: '5% chance +10 HP'
}];
// Create upgrade buttons
for (var i = 0; i < upgradeOptions.length; i++) {
var upgrade = upgradeOptions[i];
var xPos = 2048 / 2 - 400 + i * 400; // Center the 3 upgrades horizontally
var yPos = 1300; // Same Y position for all
// Upgrade button background
var upgradeBtn = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: xPos,
y: yPos,
scaleX: 6,
scaleY: 4
});
upgradeBtn.upgradeIndex = i;
upgradeBtn.tint = 0x8B008B;
// Upgrade text
var upgradeText = new Text2(upgrade.name + '\n' + upgrade.description + '\nCost: 10 coins', {
size: 45,
fill: 0xFFFFFF,
font: "monospace"
});
upgradeText.anchor.set(0.5, 0.5);
upgradeText.x = xPos;
upgradeText.y = yPos;
self.addChild(upgradeText);
}
// Close button
var closeBtn = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1600,
scaleX: 2,
scaleY: 2
});
closeBtn.tint = 0xFF0000;
var closeText = new Text2('CLOSE', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
closeText.anchor.set(0.5, 0.5);
closeText.x = 2048 / 2;
closeText.y = 1600;
self.addChild(closeText);
// Update method for cooldown timer
self.update = function () {
if (self.cooldownTimer > 0) {
self.cooldownTimer--;
// Apply visual feedback during cooldown
var cooldownAlpha = 0.3 + self.cooldownTimer / self.cooldownDuration * 0.7;
self.alpha = cooldownAlpha;
} else {
self.alpha = 1.0; // Full opacity when cooldown is over
}
};
// Handle upgrade purchases
self.down = function (x, y, obj) {
// Check if cooldown is active
if (self.cooldownTimer > 0) {
// Flash red to indicate cooldown is active
LK.effects.flashScreen(0xFF0000, 200);
return; // Exit early if cooldown is active
}
// Determine which upgrade was clicked based on X position (horizontal layout)
var upgradeIndex = -1;
if (x >= 424 && x <= 724 && y >= 1150 && y <= 1450) {
upgradeIndex = 0; // Shield
} else if (x >= 824 && x <= 1124 && y >= 1150 && y <= 1450) {
upgradeIndex = 1; // Health Boost
} else if (x >= 1224 && x <= 1524 && y >= 1150 && y <= 1450) {
upgradeIndex = 2; // Life Drain
}
// Check if upgrade button was clicked
if (upgradeIndex !== -1) {
// Close menu immediately when any upgrade option is selected
self.closeMenu();
if (coinCounter >= 10) {
coinCounter -= 10;
coinText.setText('Coins: ' + coinCounter);
// Apply upgrade based on index
switch (upgradeIndex) {
case 0:
// Shield - give one-time protection
if (!wizard.shieldActive) {
wizard.shieldActive = false;
}
wizard.shieldActive = true;
// Visual feedback for shield activation
LK.effects.flashObject(wizard, 0x00BFFF, 500);
// Add shield activation animation effect
tween(wizard.shieldGraphics, {
scaleX: 5,
scaleY: 5,
alpha: 1.0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(wizard.shieldGraphics, {
scaleX: 3,
scaleY: 3,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeIn
});
}
});
break;
case 1:
// Health Boost
wizard.maxHealth += 25;
wizard.health = Math.min(wizard.health + 25, wizard.maxHealth);
updateHealthBar();
break;
case 2:
// Life Drain - enable health restoration on kill
if (!wizard.lifeDrainLevel) {
wizard.lifeDrainLevel = 0;
}
wizard.lifeDrainLevel++;
break;
}
// Visual feedback - flash the upgrade area green
LK.effects.flashScreen(0x00FF00, 300);
} else {
// Not enough coins - flash red
LK.effects.flashScreen(0xFF0000, 300);
}
} else if (y >= 1450 && y <= 1750) {
// Close button area clicked
self.closeMenu();
}
};
self.closeMenu = function () {
self.visible = false;
gameStarted = true;
// Resume appropriate music based on game progress
if (enemyKillCounter >= 25) {
LK.playMusic('mysticalAmbient', {
volume: 0.6,
fade: {
start: 0,
end: 0.6,
duration: 1000
}
});
} else if (enemyKillCounter >= 10) {
LK.playMusic('epicBattle', {
volume: 0.8,
fade: {
start: 0,
end: 0.8,
duration: 1000
}
});
} else {
LK.playMusic('medievalTheme', {
volume: 0.7,
fade: {
start: 0,
end: 0.7,
duration: 1000
}
});
}
};
return self;
});
var Wizard = Container.expand(function () {
var self = Container.call(this);
// Animation system for wizard
self.currentFrame = 1;
self.animationTimer = 0;
self.animationSpeed = 18; // Change frame every 18 ticks (300ms at 60fps)
// Create all wizard graphics frames and store them
self.wizardFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset('wizard' + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 2.5,
scaleY: 2.5
});
frameGraphics.visible = i === 1; // Only show first frame initially
self.wizardFrames.push(frameGraphics);
}
// Create shield visual effect
self.shieldGraphics = self.attachAsset('shield', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 3
});
self.shieldGraphics.alpha = 0.7;
self.shieldGraphics.visible = false;
self.attackCooldown = 0;
self.level = 1;
self.experience = 0;
self.health = 100;
self.maxHealth = 100;
self.spellPowerLevel = 0; // Track spell power upgrades
self.shieldActive = false; // Track shield status
self.update = function () {
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;
}
// Animation system - cycle through wizard frames
self.animationTimer++;
if (self.animationTimer >= self.animationSpeed) {
self.animationTimer = 0;
// Hide current frame
self.wizardFrames[self.currentFrame - 1].visible = false;
// Move to next frame
self.currentFrame++;
if (self.currentFrame > 4) {
self.currentFrame = 1;
}
// Show new frame
self.wizardFrames[self.currentFrame - 1].visible = true;
}
};
self.attack = function (direction) {
if (self.attackCooldown <= 0) {
// Default direction if none specified
if (direction === undefined) {
direction = 0; // Default to center path
}
// Get attack angle based on path direction
var attackAngle = pathAngles[direction];
var attackDistance = 100;
// Calculate spell position based on attack direction
var spellX = self.x + Math.cos(attackAngle) * attackDistance;
var spellY = self.y + Math.sin(attackAngle) * attackDistance;
// Create spell effect
var spell = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: spellX,
y: spellY,
scaleX: 0.5,
scaleY: 0.5
}));
// Animate spell with magical effects
tween(spell, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
spell.destroy();
}
});
// Add rotation animation to spell
tween(spell, {
rotation: Math.PI * 2
}, {
duration: 500,
easing: tween.linear
});
self.attackCooldown = 30; // 0.5 seconds at 60fps
LK.getSound('spellCast').play();
// Base damage without spell power upgrades
var totalDamage = 50;
// Attack enemies in the specified direction/path
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.pathIndex === direction) {
// Only hit enemies on exact same path - no distance validation
enemy.takeDamage(totalDamage);
}
}
// Attack ogres in the specified direction/path
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
if (ogre.pathIndex === direction) {
// Only hit ogres on exact same path - no distance validation
ogre.takeDamage(totalDamage);
}
}
// Attack knights in the specified direction/path
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
if (knight.pathIndex === direction) {
// Only hit knights on exact same path - no distance validation
knight.takeDamage(totalDamage);
}
}
return true;
}
return false;
};
self.gainExperience = function (amount) {
self.experience += amount;
var expNeeded = self.level * 100;
if (self.experience >= expNeeded) {
self.levelUp();
}
};
self.levelUp = function () {
self.level++;
self.experience = 0;
// Visual level up effect
LK.effects.flashObject(self, 0xFFD700, 500);
};
self.takeDamage = function (damage) {
// Check if shield is active
if (self.shieldActive) {
// Shield blocks the damage
self.shieldActive = false;
// Visual feedback for shield use
LK.effects.flashObject(self, 0x00BFFF, 300);
// Start shield regeneration timer (10 seconds)
tween({}, {}, {
duration: 10000,
// 10 seconds
onFinish: function onFinish() {
// Regenerate shield after 10 seconds
self.shieldActive = true;
// Visual feedback for shield regeneration
LK.effects.flashObject(self, 0x00BFFF, 500);
// Add shield regeneration animation
tween(self.shieldGraphics, {
scaleX: 5,
scaleY: 5,
alpha: 1.0
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.shieldGraphics, {
scaleX: 3,
scaleY: 3,
alpha: 0.7
}, {
duration: 400,
easing: tween.easeIn
});
}
});
}
});
// No damage taken, shield absorbed it
return;
}
self.health -= damage;
if (self.health <= 0) {
self.health = 0;
// Game over when health reaches 0
LK.effects.flashScreen(0xFF0000, 1000);
LK.showGameOver();
}
// Update health bar
updateHealthBar();
// Flash red when hit
LK.effects.flashObject(self, 0xFF0000, 200);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000 // Black background for pixel art
});
/****
* Game Code
****/
// Game state variables
var gameStarted = false;
var gameMenu;
var upgradeMenu;
var upgradeMenuShown = false;
var secondUpgradeMenuShown = false;
var selectedEnemy = null; // Track currently selected enemy for projectile targeting
// Game arrays to track objects
var enemies = [];
var ogres = [];
var knights = [];
var coins = [];
var projectiles = [];
// Create and show game menu
gameMenu = game.addChild(new GameMenu());
// Create upgrade menu (initially hidden)
upgradeMenu = game.addChild(new UpgradeMenu());
upgradeMenu.visible = false;
// Create 5 stone paths leading toward the wizard position
var paths = [];
var knightX = 2048 / 2;
var knightY = 2732 - 250;
// Create paths: all pointing toward the wizard position
var pathAngles = [-Math.PI / 2,
// Center (straight down toward wizard)
-Math.PI / 3,
// Diagonal right toward wizard
-2 * Math.PI / 3,
// Diagonal left toward wizard
Math.PI / 6,
// Path 4: opposite to path 2 (diagonal left)
5 * Math.PI / 6 // Path 5: opposite to path 3 (diagonal right)
]; // 5 paths: all directed toward wizard position
// Create multiple stone segments for each path to form visible stone roads
for (var p = 0; p < 5; p++) {
var angle = pathAngles[p];
// Calculate distance from spawn point to wizard position
var wizardX = knightX;
var wizardY = 2732 - 600; // Wizard position
var spawnDistance = p === 0 ? 2000 : p === 3 || p === 4 ? 1200 : 1800; // Distance from wizard to spawn point
// Set spawn points to screen edges for all paths
var spawnX, spawnY;
if (p === 0) {
// Center path - spawn at top edge
spawnX = 2048 / 2;
spawnY = -100;
} else if (p === 1) {
// Path 2 - spawn at top right edge
spawnX = 2048 + 50;
spawnY = -50;
} else if (p === 2) {
// Path 3 - spawn at top left edge
spawnX = -50;
spawnY = -50;
} else if (p === 3) {
// Path 4 - spawn at left edge
spawnX = -100;
spawnY = 2732 / 2 + 400;
} else if (p === 4) {
// Path 5 - spawn at right edge
spawnX = 2048 + 100;
spawnY = 2732 / 2 + 400;
}
// Calculate actual path length from spawn point to wizard
var actualPathLength = Math.sqrt((spawnX - wizardX) * (spawnX - wizardX) + (spawnY - wizardY) * (spawnY - wizardY));
var segmentSize = 120; // Size of each stone segment
var numSegments = Math.floor(actualPathLength / segmentSize);
// Create individual stone segments along the path from spawn point toward wizard
for (var s = 0; s < numSegments; s++) {
var segmentDistance = s * segmentSize + segmentSize / 2;
var segmentX = spawnX - Math.cos(angle) * segmentDistance;
var segmentY = spawnY - Math.sin(angle) * segmentDistance;
// Only create segments that are within the visible game area
if (segmentX >= -100 && segmentX <= 2148 && segmentY >= -100 && segmentY <= 2832) {
// Create stone path segment
var stoneSegment = game.addChild(LK.getAsset('stonePath', {
anchorX: 0.5,
anchorY: 0.5,
x: segmentX,
y: segmentY,
scaleX: 2.0,
scaleY: 2.0,
rotation: angle + Math.PI / 2 // Rotate stone to align with path
}));
// Make stone paths completely invisible
stoneSegment.alpha = 0; // Make completely invisible
stoneSegment.visible = false; // Initially hidden, will be shown when game starts
stoneSegment.pathIndex = p;
}
}
}
// Create invisible path collision areas for touch detection
for (var p = 0; p < 5; p++) {
var angle = pathAngles[p];
// Calculate distance from spawn point to wizard position
var wizardX = knightX;
var wizardY = 2732 - 600; // Wizard position
var spawnDistance = p === 0 ? 3000 : p === 3 || p === 4 ? 1200 : 1800; // Distance from wizard to spawn point
var spawnX = wizardX + Math.cos(angle) * spawnDistance;
var spawnY = wizardY + Math.sin(angle) * spawnDistance;
// Move all spawn points to screen edges
if (p === 0) {
// Center path - spawn at top edge
spawnX = 2048 / 2;
spawnY = -100;
} else if (p === 1) {
// Path 2 - spawn at top right edge
spawnX = 2048 + 50;
spawnY = -50;
} else if (p === 2) {
// Path 3 - spawn at top left edge
spawnX = -50;
spawnY = -50;
} else if (p === 3) {
// Path 4 - spawn at left edge
spawnX = -100;
spawnY = 2732 / 2 + 400;
} else if (p === 4) {
// Path 5 - spawn at right edge
spawnX = 2048 + 100;
spawnY = 2732 / 2 + 400;
}
// Calculate actual path length from spawn point to wizard
var actualPathLength = Math.sqrt((spawnX - wizardX) * (spawnX - wizardX) + (spawnY - wizardY) * (spawnY - wizardY));
var centerX = (spawnX + wizardX) / 2;
var centerY = (spawnY + wizardY) / 2;
// Create invisible collision path
var path = game.addChild(LK.getAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: centerX,
y: centerY,
scaleX: 4,
scaleY: actualPathLength / 60,
rotation: angle + Math.PI / 2
}));
// Make collision path completely invisible
path.alpha = 0;
path.visible = false;
path.pathIndex = p;
// Add numbered text label for each path
var pathNumber = new Text2((p + 1).toString(), {
size: 120,
fill: 0xFFD700,
font: "monospace"
});
pathNumber.anchor.set(0.5, 0.5);
// Position number directly at spawn point
pathNumber.x = spawnX;
pathNumber.y = spawnY - 80; // Position slightly above spawn point
pathNumber.visible = false; // Initially hidden, will be shown when game starts
pathNumber.pathIndex = p;
game.addChild(pathNumber);
// Add touch handler for directional attacks
path.down = function (x, y, obj) {
// Attack in this path's direction
wizard.attack(obj.pathIndex);
// Visual feedback - no flash for invisible paths
// Paths remain invisible when tapped
};
paths.push(path);
}
// Pixel art scaling handled by engine automatically
// Set fondodelacueva as the actual game background
var backgroundMap = game.addChild(LK.getAsset('fondodelacueva', {
anchorX: 0,
anchorY: 0,
scaleX: 19.5,
scaleY: 26.0,
x: 0,
y: 0
}));
// Send background to the back but use a less extreme z-index
backgroundMap.zIndex = -100;
// Hide background initially during menu
backgroundMap.visible = false;
backgroundMap.alpha = 1.0;
// Create wizard
var wizard = game.addChild(new Wizard());
wizard.x = knightX;
wizard.y = 2732 - 600; // Position wizard higher on screen
wizard.visible = false;
// UI Elements
// Removed scoreText and levelText to eliminate stray characters in top right
var coinCounter = 0;
var enemyKillCounter = 0;
var coinText = new Text2('Coins: 0', {
size: 60,
fill: 0xFFD700,
font: "monospace"
});
coinText.anchor.set(0, 0);
LK.gui.topLeft.addChild(coinText);
coinText.x = 120;
coinText.y = 90;
coinText.visible = false;
var killCountText = new Text2('Puntuacion: 0', {
size: 60,
fill: 0xFF6B6B,
font: "monospace"
});
killCountText.anchor.set(0, 0);
LK.gui.topLeft.addChild(killCountText);
killCountText.x = 120;
killCountText.y = 150;
killCountText.visible = false;
var tapText = new Text2('TAP TO CAST SPELLS!', {
size: 100,
fill: 0xFF6B6B,
font: "monospace"
});
tapText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(tapText);
tapText.y = -200;
tapText.visible = false;
// Health bar UI
var healthBarBg = LK.getAsset('healthBarBg', {
anchorX: 0,
anchorY: 0
});
LK.gui.topLeft.addChild(healthBarBg);
healthBarBg.x = 120;
healthBarBg.y = 20;
healthBarBg.visible = false;
var healthBar = LK.getAsset('healthBar', {
anchorX: 0,
anchorY: 0
});
LK.gui.topLeft.addChild(healthBar);
healthBar.x = 120;
healthBar.y = 20;
healthBar.visible = false;
var healthText = new Text2('Health: 100/100', {
size: 50,
fill: 0xFFFFFF,
font: "monospace"
});
healthText.anchor.set(0, 0);
LK.gui.topLeft.addChild(healthText);
healthText.x = 120;
healthText.y = 50;
healthText.visible = false;
function updateHealthBar() {
var healthPercent = wizard.health / wizard.maxHealth;
healthBar.scaleX = healthPercent;
healthText.setText('Health: ' + wizard.health + '/' + wizard.maxHealth);
// Change color based on health
if (healthPercent > 0.6) {
healthBar.tint = 0x00ff00; // Green
} else if (healthPercent > 0.3) {
healthBar.tint = 0xffff00; // Yellow
} else {
healthBar.tint = 0xff0000; // Red
}
}
// Enemy spawning variables
var enemySpawnTimer = 0;
var lastSpawnedPath = -1; // Track the last spawned path
var consecutiveSpawns = 0; // Track consecutive spawns from same path
// Cooldown system variables
var pathLastSpawnTime = [-1, -1, -1, -1, -1]; // Track last spawn time for each path
var pathConsecutiveSpawns = [0, 0, 0, 0, 0]; // Track consecutive spawns per path
var pathCooldownDuration = 300; // 5 seconds at 60fps
// Game input handling
game.down = function (x, y, obj) {
// Check if a path was tapped for directional attack
var pathTapped = false;
for (var p = 0; p < paths.length; p++) {
var path = paths[p];
// Convert tap position to path's local coordinates
var localPos = path.toLocal({
x: x,
y: y
});
// Check if tap is within path bounds
if (Math.abs(localPos.x) < path.width / 2 && Math.abs(localPos.y) < path.height / 2) {
// Attack in this path's direction
wizard.attack(path.pathIndex);
pathTapped = true;
break;
}
}
// No default attack - player must tap a path to attack
};
// Main game update loop
game.update = function () {
// Sort children by z-index to ensure proper rendering order
game.children.sort(function (a, b) {
return (a.zIndex || 0) - (b.zIndex || 0);
});
// Only update game logic if game has started
if (!gameStarted) {
return;
}
// Change music to epic battle theme at 10 enemies killed
if (enemyKillCounter === 10) {
LK.playMusic('epicBattle', {
volume: 0.8,
fade: {
start: 0,
end: 0.8,
duration: 1500
}
});
}
// Change to mystical ambient music at 25 enemies killed
if (enemyKillCounter === 25) {
LK.playMusic('mysticalAmbient', {
volume: 0.6,
fade: {
start: 0,
end: 0.6,
duration: 2000
}
});
}
// Show upgrade menu after 12 enemies are killed
if (enemyKillCounter >= 12 && !upgradeMenuShown) {
upgradeMenuShown = true;
upgradeMenu.visible = true;
upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown
gameStarted = false; // Pause game while menu is open
LK.stopMusic(); // Pause music
// Clear all enemies on screen when upgrade menu is shown
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemies.splice(i, 1);
enemy.destroy();
}
// Clear all ogres on screen when upgrade menu is shown
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
ogres.splice(i, 1);
ogre.destroy();
}
// Clear all knights on screen when upgrade menu is shown
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
knights.splice(i, 1);
knight.destroy();
}
}
// Show upgrade menu again after 35 enemies are killed
if (enemyKillCounter >= 35 && upgradeMenuShown && !secondUpgradeMenuShown) {
secondUpgradeMenuShown = true;
upgradeMenu.visible = true;
upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown
gameStarted = false; // Pause game while menu is open
LK.stopMusic(); // Pause music
// Clear all enemies on screen when upgrade menu is shown
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemies.splice(i, 1);
enemy.destroy();
}
// Clear all ogres on screen when upgrade menu is shown
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
ogres.splice(i, 1);
ogre.destroy();
}
// Clear all knights on screen when upgrade menu is shown
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
knights.splice(i, 1);
knight.destroy();
}
}
// Reset consecutive spawns for paths that have cooled down (runs every frame)
for (var pathIdx = 0; pathIdx < 5; pathIdx++) {
// Check if enough time has passed since last spawn (cooldown expired)
if (pathLastSpawnTime[pathIdx] !== -1 && LK.ticks - pathLastSpawnTime[pathIdx] > pathCooldownDuration) {
// Reset consecutive spawns for this path due to cooldown
pathConsecutiveSpawns[pathIdx] = 0;
}
}
// Progressive difficulty system based on kills
var difficultyLevel = Math.floor(enemyKillCounter / 10); // Every 10 kills increases difficulty
var currentSpawnRate = Math.max(30, 90 - difficultyLevel * 8); // Faster spawning over time
var enemyHealthMultiplier = 1 + difficultyLevel * 0.3; // 30% more health per difficulty level
var enemySpeedMultiplier = 1 + difficultyLevel * 0.35; // 35% faster per difficulty level
// Spawn enemies
enemySpawnTimer++;
if (enemySpawnTimer >= currentSpawnRate) {
enemySpawnTimer = 0;
var enemy = game.addChild(new Enemy());
// Apply difficulty scaling to enemy stats
enemy.health = Math.floor(100 * enemyHealthMultiplier);
enemy.maxHealth = enemy.health;
enemy.speed = 3 * enemySpeedMultiplier;
// Spawn enemy at a random path entrance
var randomPathIndex;
if (enemyKillCounter < 5) {
// First 5 enemies spawn from center path only
randomPathIndex = 0; // Center path
} else {
// After 5 enemies, spawn from any path but limit consecutive spawns with cooldown system
// Build available paths list
var availablePaths = [];
for (var pathIdx = 0; pathIdx < 5; pathIdx++) {
// If path has spawned less than 2 consecutive times, it's available
if (pathConsecutiveSpawns[pathIdx] < 2) {
availablePaths.push(pathIdx);
}
}
// If no paths available, reset ALL paths to ensure balanced distribution
if (availablePaths.length === 0) {
// Reset all paths' consecutive spawn counters
for (var pathIdx = 0; pathIdx < 5; pathIdx++) {
pathConsecutiveSpawns[pathIdx] = 0;
availablePaths.push(pathIdx);
}
}
randomPathIndex = availablePaths[Math.floor(Math.random() * availablePaths.length)];
}
// Update path-specific spawn tracking
// Always increment consecutive spawns for the selected path
pathConsecutiveSpawns[randomPathIndex]++;
// Update path spawn time and global tracking
pathLastSpawnTime[randomPathIndex] = LK.ticks;
lastSpawnedPath = randomPathIndex;
consecutiveSpawns = pathConsecutiveSpawns[randomPathIndex];
var pathAngle = pathAngles[randomPathIndex];
// Store the path information on the enemy
enemy.pathIndex = randomPathIndex;
enemy.pathAngle = pathAngle;
// Calculate spawn position at screen edges
if (randomPathIndex === 0) {
// Center path - spawn at top edge
enemy.x = 2048 / 2;
enemy.y = -100;
} else if (randomPathIndex === 1) {
// Path 2 - spawn at top right edge
enemy.x = 2048 + 50;
enemy.y = -50;
} else if (randomPathIndex === 2) {
// Path 3 - spawn at top left edge
enemy.x = -50;
enemy.y = -50;
} else if (randomPathIndex === 3) {
// Path 4 - spawn at left edge
enemy.x = -100;
enemy.y = 2732 / 2 + 400;
} else if (randomPathIndex === 4) {
// Path 5 - spawn at right edge
enemy.x = 2048 + 100;
enemy.y = 2732 / 2 + 400;
}
// Make sure enemies spawn within screen bounds
enemy.x = Math.max(50, Math.min(1998, enemy.x));
enemy.y = Math.max(-200, Math.min(2732 + 100, enemy.y));
enemy.lastX = enemy.x;
enemies.push(enemy);
}
// Spawn ogres after 15 seconds (900 ticks at 60fps)
if (LK.ticks >= 900 && LK.ticks % 180 === 0) {
// Every 3 seconds after 15 seconds
var ogre = game.addChild(new Ogre());
// Apply difficulty scaling to ogre stats
ogre.health = Math.floor(150 * enemyHealthMultiplier); // Ogres have more base health
ogre.maxHealth = ogre.health;
ogre.speed = 2.5 * enemySpeedMultiplier; // Ogres are slightly slower
// Spawn ogre at a random path entrance
var ogrePathIndex = Math.floor(Math.random() * 5);
var ogrePathAngle = pathAngles[ogrePathIndex];
// Store the path information on the ogre
ogre.pathIndex = ogrePathIndex;
ogre.pathAngle = ogrePathAngle;
// Calculate spawn position at screen edges
if (ogrePathIndex === 0) {
// Center path - spawn at top edge
ogre.x = 2048 / 2;
ogre.y = -100;
} else if (ogrePathIndex === 1) {
// Path 2 - spawn at top right edge
ogre.x = 2048 + 50;
ogre.y = -50;
} else if (ogrePathIndex === 2) {
// Path 3 - spawn at top left edge
ogre.x = -50;
ogre.y = -50;
} else if (ogrePathIndex === 3) {
// Path 4 - spawn at left edge
ogre.x = -100;
ogre.y = 2732 / 2 + 400;
} else if (ogrePathIndex === 4) {
// Path 5 - spawn at right edge
ogre.x = 2048 + 100;
ogre.y = 2732 / 2 + 400;
}
// Make sure ogres spawn within screen bounds
ogre.x = Math.max(50, Math.min(1998, ogre.x));
ogre.y = Math.max(-200, Math.min(2732 + 100, ogre.y));
ogre.lastX = ogre.x;
ogres.push(ogre);
}
// Spawn knights after 30 enemies killed
if (enemyKillCounter >= 30 && LK.ticks % 300 === 0) {
// Every 5 seconds after 30th enemy
var knight = game.addChild(new Knight());
// Apply difficulty scaling to knight stats
knight.health = Math.floor(200 * enemyHealthMultiplier); // Knights have highest base health
knight.maxHealth = knight.health;
knight.speed = 2 * enemySpeedMultiplier; // Knights are slowest
// Spawn knight at a random path entrance
var knightPathIndex = Math.floor(Math.random() * 5);
var knightPathAngle = pathAngles[knightPathIndex];
// Store the path information on the knight
knight.pathIndex = knightPathIndex;
knight.pathAngle = knightPathAngle;
// Calculate spawn position at screen edges
if (knightPathIndex === 0) {
// Center path - spawn at top edge
knight.x = 2048 / 2;
knight.y = -100;
} else if (knightPathIndex === 1) {
// Path 2 - spawn at top right edge
knight.x = 2048 + 50;
knight.y = -50;
} else if (knightPathIndex === 2) {
// Path 3 - spawn at top left edge
knight.x = -50;
knight.y = -50;
} else if (knightPathIndex === 3) {
// Path 4 - spawn at left edge
knight.x = -100;
knight.y = 2732 / 2 + 400;
} else if (knightPathIndex === 4) {
// Path 5 - spawn at right edge
knight.x = 2048 + 100;
knight.y = 2732 / 2 + 400;
}
// Make sure knights spawn within screen bounds
knight.x = Math.max(50, Math.min(1998, knight.x));
knight.y = Math.max(-200, Math.min(2732 + 100, knight.y));
knight.lastX = knight.x;
knights.push(knight);
}
// Check enemy-knight collisions and cleanup off-screen enemies
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
// Remove enemies that went off-screen at bottom
if (enemy.y > 2732 + 100) {
enemies.splice(i, 1);
enemy.destroy();
continue;
}
// Initialize lastIntersecting if not set
if (enemy.lastIntersecting === undefined) {
enemy.lastIntersecting = false;
}
// Check for collision transition
var currentIntersecting = enemy.intersects(wizard);
if (!enemy.lastIntersecting && currentIntersecting) {
// Damage wizard when enemy touches for the first time
wizard.takeDamage(20);
// Remove enemy after dealing damage
enemies.splice(i, 1);
enemy.destroy();
continue;
}
// Update last intersecting state
enemy.lastIntersecting = currentIntersecting;
}
// Check ogre-wizard collisions and cleanup off-screen ogres
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
// Remove ogres that went off-screen at bottom
if (ogre.y > 2732 + 100) {
ogres.splice(i, 1);
ogre.destroy();
continue;
}
// Initialize lastIntersecting if not set
if (ogre.lastIntersecting === undefined) {
ogre.lastIntersecting = false;
}
// Check for collision transition
var currentOgreIntersecting = ogre.intersects(wizard);
if (!ogre.lastIntersecting && currentOgreIntersecting) {
// Damage wizard when ogre touches for the first time
wizard.takeDamage(30); // Ogres deal 30 damage
// Remove ogre after dealing damage
ogres.splice(i, 1);
ogre.destroy();
continue;
}
// Update last intersecting state
ogre.lastIntersecting = currentOgreIntersecting;
}
// Check knight-wizard collisions and cleanup off-screen knights
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
// Remove knights that went off-screen at bottom
if (knight.y > 2732 + 100) {
knights.splice(i, 1);
knight.destroy();
continue;
}
// Initialize lastIntersecting if not set
if (knight.lastIntersecting === undefined) {
knight.lastIntersecting = false;
}
// Check for collision transition
var currentKnightIntersecting = knight.intersects(wizard);
if (!knight.lastIntersecting && currentKnightIntersecting) {
// Damage wizard when knight touches for the first time
wizard.takeDamage(40); // Knights deal 40 damage
// Remove knight after dealing damage
knights.splice(i, 1);
knight.destroy();
continue;
}
// Update last intersecting state
knight.lastIntersecting = currentKnightIntersecting;
}
// Make tap text pulse
var pulse = 1 + Math.sin(LK.ticks * 0.1) * 0.2;
tapText.scale.set(pulse, pulse);
};
// Remove tap text after 5 seconds
tween({}, {}, {
duration: 5000,
onFinish: function onFinish() {
if (tapText && tapText.parent) {
tapText.destroy();
}
}
});