User prompt
haz que el mini boss salga en el enemigo 36
User prompt
haz que las mejoras no le hagan damage al jugador
User prompt
haz que despues de matar 35 enemigos paraesca un mini boss βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
Implement **spell charging** - hold tap longer for more powerful attacks βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
Implement enemy death animation βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
ahora reduce a la mitad el fondodelacueva
User prompt
dobla el tamano del fondodelacueva
User prompt
add spell trail βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
haz que la pantalla se agite bastante cuando el jugador recibe damage βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
aplica todas la mejoras que mencionaste βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1, @upit/storage.v1
User prompt
puedes implementar todas las opciones del 1 βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
haz que tiemble mas βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
haz que la pantalla tiemble cuando recibes damage βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
haz efectos de impactos cuando golpeas a los enemigos βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
haz que el jugador empieze con 1000 monedas y la tienda se active cada 10 enemigos
User prompt
reduce la hitbox de los orbes
User prompt
haz que el orbe sea el doble de lento
User prompt
haz que los orbes esten el doble de lejos del jugador
User prompt
haz que el orbe no aumente velocidad con las mejoras
User prompt
haz que la lv2 del orbe no sea tan rapido βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
haz que el jugador empieze con 1000 monedas y la tienda salga cada 10 enemigos
User prompt
haz que cada vez que se compra una mejora salga una version mejorada despues en la tienda
User prompt
haz un nuevo menu de mejora al llegar al enemigo 70
User prompt
haz que la musica no se pause con el menu de emjora
User prompt
pon el orbe mas arriba βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
/****
* 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 = 1;
self.maxHealth = 1;
self.speed = 7;
// Note: health, maxHealth and speed will be overridden by difficulty system
self.lastX = 0;
self.frozen = false;
self.frozenTimer = 0;
self.update = function () {
// Skip all movement and collision if enemy is dying
if (self.isDying) {
return;
}
// 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
});
}
// Handle frozen state
if (self.frozen) {
self.frozenTimer--;
if (self.frozenTimer <= 0) {
self.frozen = false;
}
return; // Skip movement when frozen
}
// 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);
// Create impact effect
var impactEffect = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 0.5,
scaleY: 0.5
}));
impactEffect.tint = 0xFFFFFF;
impactEffect.alpha = 0.9;
// Animate impact effect
tween(impactEffect, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
impactEffect.destroy();
}
});
// Return to walking animation after brief attacking animation
tween({}, {}, {
duration: 300,
onFinish: function onFinish() {
if (self.animationState === 'attacking') {
self.animationState = 'walking';
}
}
});
// Check if enemy should die based on health
if (self.health <= 0) {
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();
// Start death animation sequence
self.isDying = true;
// Animate enemy death with fade out and scale down
tween(self, {
alpha: 0,
scaleX: 0.3,
scaleY: 0.3,
rotation: Math.PI * 0.5
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
// After death animation completes, execute all death logic
// 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 - improved chance and healing
if (wizard.lifeDrainLevel && wizard.lifeDrainLevel > 0) {
var drainChance = Math.random();
var chanceThreshold = upgradeLevels.lifeDrain > 1 ? 0.10 : 0.05; // 10% vs 5%
var healAmount = upgradeLevels.lifeDrain > 1 ? 20 : 10; // 20 vs 10 HP
if (drainChance < chanceThreshold) {
wizard.health = Math.min(wizard.health + healAmount, 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 EnergyBeam = Container.expand(function () {
var self = Container.call(this);
// Create beam visual using projectile glow
var beamGraphics = self.attachAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.0,
scaleY: 1.0
});
beamGraphics.tint = 0x00ffff; // Cyan color
beamGraphics.alpha = 0.9;
self.speed = 60;
self.direction = {
x: 0,
y: 0
};
self.targetEnemy = null;
self.lastIntersecting = false;
self.update = function () {
// Move toward target
self.x += self.direction.x * self.speed;
self.y += self.direction.y * self.speed;
// Rotate beam to point toward movement direction
var angle = Math.atan2(self.direction.y, self.direction.x);
beamGraphics.rotation = angle;
// Remove if off screen
if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) {
self.destroy();
return;
}
// Check collision with target enemy
if (self.targetEnemy && self.targetEnemy.parent) {
var currentIntersecting = self.intersects(self.targetEnemy);
if (!self.lastIntersecting && currentIntersecting) {
// Hit the target enemy
// Calculate damage based on spell power upgrade
var damage = 100;
if (wizard && wizard.spellPowerLevel && wizard.spellPowerLevel > 0) {
// Check if target is an ogre and we're before enemy 35
if (self.targetEnemy && self.targetEnemy.ogreFrames && enemyKillCounter < 35) {
// Always kill ogres in one hit until enemy 35
damage = self.targetEnemy.health + 1; // Ensure one-hit kill
} else {
damage = 200; // Double damage when spell power is active
}
}
self.targetEnemy.takeDamage(damage);
self.destroy();
return;
}
self.lastIntersecting = currentIntersecting;
} else {
// Target destroyed, remove beam
self.destroy();
}
};
return self;
});
var EnergyOrb = Container.expand(function () {
var self = Container.call(this);
// Create energy sphere visual
var sphereGraphics = self.attachAsset('energySphere', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
// Add glowing effect
sphereGraphics.alpha = 0.8;
self.attackTimer = 0;
self.attackInterval = 180; // 3 seconds at 60fps
self.orbitalAngle = 0;
self.orbitalRadius = 120;
self.update = function () {
// Pause energy orb when upgrade menu is visible
if (upgradeMenu && upgradeMenu.visible) {
return;
}
// Keep sphere at wizard's position (stationary relative to wizard)
if (wizard) {
self.x = wizard.x + 140; // Position further to the right side of wizard
self.y = wizard.y - 20; // Position slightly lower relative to wizard
}
// Pulsing glow effect
var pulse = 1 + Math.sin(LK.ticks * 0.2) * 0.3;
sphereGraphics.scaleX = 1.5 * pulse;
sphereGraphics.scaleY = 1.5 * pulse;
// Attack timer - keep original interval regardless of upgrades
self.attackTimer++;
if (self.attackTimer >= 180) {
// Fixed at 3 seconds
self.attackTimer = 0;
self.attackClosestEnemy();
}
};
self.attackClosestEnemy = function () {
var closestEnemy = null;
var closestDistance = Infinity;
// Check all enemy types for closest one, prioritizing mini bosses
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// Attack closest enemy if found
if (closestEnemy) {
// Create energy beam projectile
var energyBeam = game.addChild(new EnergyBeam());
energyBeam.x = self.x;
energyBeam.y = self.y;
energyBeam.targetEnemy = closestEnemy;
// Calculate direction to target
var dx = closestEnemy.x - self.x;
var dy = closestEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
energyBeam.direction.x = dx / distance;
energyBeam.direction.y = dy / distance;
}
// Flash effect on sphere when attacking
tween(sphereGraphics, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 1.0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(sphereGraphics, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0.8
}, {
duration: 200,
easing: tween.easeIn
});
}
});
LK.getSound('spellCast').play();
}
};
return self;
});
var FireBall = Container.expand(function () {
var self = Container.call(this);
// Create fire ball visual using projectile glow with fire colors
var fireBallGraphics = self.attachAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
fireBallGraphics.tint = 0xFF4500; // Orange-red fire color
fireBallGraphics.alpha = 0.9;
self.speed = 40;
self.direction = {
x: 0,
y: 0
};
self.lastIntersecting = {};
self.update = function () {
// Move fire ball
self.x += self.direction.x * self.speed;
self.y += self.direction.y * self.speed;
// Add fire flickering effect
var flicker = 1 + Math.sin(LK.ticks * 0.4) * 0.3;
fireBallGraphics.scaleX = 1.5 * flicker;
fireBallGraphics.scaleY = 1.5 * flicker;
// Rotate fire ball
fireBallGraphics.rotation += 0.2;
// Remove if off screen
if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) {
self.destroy();
return;
}
// Check collision with all enemy types
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
if (!self.lastIntersecting[i]) {
self.lastIntersecting[i] = false;
}
var currentIntersecting = self.intersects(enemy);
if (!self.lastIntersecting[i] && currentIntersecting) {
// Hit enemy - deal fire damage
var damage = 150; // High damage for fire ball
enemy.takeDamage(damage);
// Create fire explosion effect
LK.effects.flashObject(enemy, 0xFF4500, 400);
// Create additional fire visual effect
var fireEffect = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x,
y: enemy.y,
scaleX: 3,
scaleY: 3
}));
fireEffect.tint = 0xFF6600;
fireEffect.alpha = 0.8;
// Animate fire effect
tween(fireEffect, {
scaleX: 6,
scaleY: 6,
alpha: 0
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
fireEffect.destroy();
}
});
self.destroy();
return;
}
self.lastIntersecting[i] = currentIntersecting;
}
};
return self;
});
var GameMenu = Container.expand(function () {
var self = Container.call(this);
// Menu background image instead of cave background
var menuBg = self.attachAsset('menuBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 4.0,
scaleY: 4.0
});
menuBg.alpha = 1.0;
// Title text
var titleText = new Text2('WIZARD DEFENDER', {
size: 150,
fill: 0xFFD700,
font: "monospace"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 800;
self.addChild(titleText);
// Instructions text
var instructionsText = new Text2('TAP PATHS TO CAST SPELLS\nDEFEND YOUR CASTLE!', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
instructionsText.anchor.set(0.5, 0.5);
instructionsText.x = 2048 / 2;
instructionsText.y = 1200;
self.addChild(instructionsText);
// Start button
var startButton = self.attachAsset('wizard1', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 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 = 3;
self.maxHealth = 3;
self.speed = 7;
// Note: health, maxHealth and speed will be overridden by difficulty system
self.lastX = 0;
self.frozen = false;
self.frozenTimer = 0;
self.update = function () {
// Skip all movement and collision if knight is dying
if (self.isDying) {
return;
}
// 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
});
}
// Handle frozen state
if (self.frozen) {
self.frozenTimer--;
if (self.frozenTimer <= 0) {
self.frozen = false;
}
return; // Skip movement when frozen
}
// 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;
// Change animation state to attacking when hit
self.animationState = 'attacking';
// Flash red when hit
LK.effects.flashObject(self, 0xFF0000, 200);
// Create impact effect
var impactEffect = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 0.8,
scaleY: 0.8
}));
impactEffect.tint = 0xFFD700;
impactEffect.alpha = 0.9;
// Animate impact effect with sparks
tween(impactEffect, {
scaleX: 3.0,
scaleY: 3.0,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
impactEffect.destroy();
}
});
// Return to walking animation after brief attacking animation
tween({}, {}, {
duration: 500,
onFinish: function onFinish() {
if (self.animationState === 'attacking') {
self.animationState = 'walking';
}
}
});
// Check if knight should die based on health
if (self.health <= 0) {
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();
// Start death animation sequence
self.isDying = true;
// Animate knight death with dramatic fall effect
tween(self, {
alpha: 0,
scaleX: 0.2,
scaleY: 0.2,
rotation: Math.PI * 0.8,
y: self.y + 50
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
// After death animation completes, execute all death logic
// 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 MiniBoss = Container.expand(function () {
var self = Container.call(this);
// Animation system for mini boss
self.currentFrame = 1;
self.animationTimer = 0;
self.animationSpeed = 10; // Faster animation for intimidating effect
self.animationState = 'walking'; // walking, attacking, dying, idle
// Create all knight graphics frames for mini boss (reusing knight assets but larger)
self.bossFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset('knight' + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 4.0,
// Much larger than normal knights
scaleY: 4.0
});
frameGraphics.visible = i === 1; // Only show first frame initially
frameGraphics.tint = 0x8B0000; // Dark red tint to distinguish from normal knights
self.bossFrames.push(frameGraphics);
}
// Create invisible hitbox with larger size
var hitbox = self.attachAsset('knight1', {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 8,
scaleY: 8
});
hitbox.alpha = 0; // Make hitbox invisible
self.health = 1000; // Very high health for mini boss
self.maxHealth = 1000;
self.speed = 4; // Slower than normal enemies but still threatening
self.lastX = 0;
self.frozen = false;
self.frozenTimer = 0;
self.isDying = false;
// Special mini boss abilities
self.specialAttackTimer = 0;
self.specialAttackCooldown = 300; // 5 seconds between special attacks
self.update = function () {
// Skip all movement and collision if mini boss is dying
if (self.isDying) {
return;
}
// Animation system - cycle through boss frames based on state
self.animationTimer++;
var frameSpeed = self.animationSpeed;
if (self.animationState === 'attacking') {
frameSpeed = 5; // Very fast animation for attacking
} else if (self.animationState === 'dying') {
frameSpeed = 15; // Slower animation for dying
}
if (self.animationTimer >= frameSpeed) {
self.animationTimer = 0;
// Hide current frame
self.bossFrames[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++;
}
}
// Show new frame
self.bossFrames[self.currentFrame - 1].visible = true;
}
// Handle frozen state
if (self.frozen) {
self.frozenTimer--;
if (self.frozenTimer <= 0) {
self.frozen = false;
}
return; // Skip movement when frozen
}
// Always move directly toward the wizard
if (wizard) {
// Calculate direction from mini boss 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.bossFrames.length; frameIdx++) {
self.bossFrames[frameIdx].scaleX = -4.0; // Flip to face left
}
} else {
// Wizard is to the right - face right
for (var frameIdx = 0; frameIdx < self.bossFrames.length; frameIdx++) {
self.bossFrames[frameIdx].scaleX = 4.0; // Normal orientation to face right
}
}
}
// Special attack timer
self.specialAttackTimer++;
if (self.specialAttackTimer >= self.specialAttackCooldown) {
self.specialAttackTimer = 0;
self.performSpecialAttack();
}
};
self.performSpecialAttack = function () {
// Change to attacking animation
self.animationState = 'attacking';
// Create shockwave effect
LK.effects.flashScreen(0x8B0000, 400);
// Create multiple projectiles in all directions
for (var angle = 0; angle < Math.PI * 2; angle += Math.PI / 4) {
var projectile = game.addChild(new Projectile());
projectile.x = self.x;
projectile.y = self.y;
projectile.direction.x = Math.cos(angle);
projectile.direction.y = Math.sin(angle);
projectile.speed = 30; // Slower than normal projectiles
projectiles.push(projectile);
}
// Return to walking animation after attack
tween({}, {}, {
duration: 800,
onFinish: function onFinish() {
if (self.animationState === 'attacking') {
self.animationState = 'walking';
}
}
});
LK.getSound('spellCast').play();
};
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, 300);
// Create large impact effect
var impactEffect = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 2.0,
scaleY: 2.0
}));
impactEffect.tint = 0xFF4500;
impactEffect.alpha = 1.0;
// Animate impact effect with massive explosion
tween(impactEffect, {
scaleX: 8.0,
scaleY: 8.0,
alpha: 0
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
impactEffect.destroy();
}
});
// Return to walking animation after brief attacking animation
tween({}, {}, {
duration: 600,
onFinish: function onFinish() {
if (self.animationState === 'attacking') {
self.animationState = 'walking';
}
}
});
// Check if mini boss should die based on health
if (self.health <= 0) {
self.die();
}
};
self.down = function (x, y, obj) {
// Set this mini boss 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 mini boss when tapped
if (wizard && projectiles.length < 10) {
var projectile = game.addChild(new Projectile());
projectile.x = wizard.x;
projectile.y = wizard.y;
// Calculate direction from wizard to mini boss
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 mini boss dies
LK.getSound('painSound').play();
// Start death animation sequence
self.isDying = true;
// Dramatic screen shake for mini boss death
var shakeIntensity = 50;
var originalX = game.x;
var originalY = game.y;
tween(game, {
x: originalX + shakeIntensity,
y: originalY + shakeIntensity * 0.5
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(game, {
x: originalX,
y: originalY
}, {
duration: 100,
easing: tween.easeIn
});
}
});
// Animate mini boss death with epic collapse effect
tween(self, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1,
rotation: Math.PI * 2,
y: self.y + 200
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
// After death animation completes, execute all death logic
// Drop multiple coins for mini boss
for (var coinIdx = 0; coinIdx < 5; coinIdx++) {
var coin = game.addChild(new Coin());
coin.x = self.x + (Math.random() - 0.5) * 200;
coin.y = self.y - 50 - coinIdx * 20;
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: 1500 + coinIdx * 200,
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 (counts as 5 kills)
enemyKillCounter += 5;
killCountText.setText('Puntuacion: ' + enemyKillCounter);
// Add massive experience to wizard
wizard.gainExperience(100);
// Clear selected enemy if this was the target
if (selectedEnemy === self) {
selectedEnemy = null;
}
// Remove from miniBosses array
for (var i = miniBosses.length - 1; i >= 0; i--) {
if (miniBosses[i] === self) {
miniBosses.splice(i, 1);
break;
}
}
self.destroy();
LK.setScore(LK.getScore() + 100);
}
});
};
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 = 2;
self.maxHealth = 2;
self.speed = 7;
// Note: health, maxHealth and speed will be overridden by difficulty system
self.lastX = 0;
self.frozen = false;
self.frozenTimer = 0;
self.update = function () {
// Skip all movement and collision if ogre is dying
if (self.isDying) {
return;
}
// 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
});
}
// Handle frozen state
if (self.frozen) {
self.frozenTimer--;
if (self.frozenTimer <= 0) {
self.frozen = false;
}
return; // Skip movement when frozen
}
// 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;
// Change animation state to attacking when hit
self.animationState = 'attacking';
// Flash red when hit
LK.effects.flashObject(self, 0xFF0000, 200);
// Create impact effect
var impactEffect = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 1.0,
scaleY: 1.0
}));
impactEffect.tint = 0xFF4500;
impactEffect.alpha = 1.0;
// Animate impact effect with explosion
tween(impactEffect, {
scaleX: 4.0,
scaleY: 4.0,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
impactEffect.destroy();
}
});
// Return to walking animation after brief attacking animation
tween({}, {}, {
duration: 400,
onFinish: function onFinish() {
if (self.animationState === 'attacking') {
self.animationState = 'walking';
}
}
});
// Check if ogre should die based on health
if (self.health <= 0) {
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();
// Start death animation sequence
self.isDying = true;
// Animate ogre death with heavy collapse effect
tween(self, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1,
rotation: Math.PI * 1.2,
y: self.y + 80
}, {
duration: 1200,
easing: tween.easeOut,
onFinish: function onFinish() {
// After death animation completes, execute all death logic
// 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 Orb = Container.expand(function () {
var self = Container.call(this);
// Create orb visual using energy sphere
var orbGraphics = self.attachAsset('energySphere', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.4,
scaleY: 0.4
});
orbGraphics.tint = 0xFFD700; // Golden color for orbs
orbGraphics.alpha = 0.9;
self.orbitalAngle = 0; // Starting angle for this orb
self.orbitalRadius = 880; // Distance from wizard - doubled again for even more separation
self.rotationSpeed = 0.025; // How fast orbs rotate - halved for slower movement
self.update = function () {
// Pause orb when upgrade menu is visible
if (upgradeMenu && upgradeMenu.visible) {
return;
}
// Rotate around wizard
if (wizard) {
self.orbitalAngle += self.rotationSpeed;
self.x = wizard.x + Math.cos(self.orbitalAngle) * self.orbitalRadius;
self.y = wizard.y + Math.sin(self.orbitalAngle) * self.orbitalRadius - 240; // Position orb much higher up
}
// Add pulsing effect
var pulse = 1 + Math.sin(LK.ticks * 0.3) * 0.2;
orbGraphics.scaleX = 0.4 * pulse;
orbGraphics.scaleY = 0.4 * pulse;
// Check collision with enemies
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
if (self.intersects(enemy)) {
// Deal damage to enemy on contact
enemy.takeDamage(50);
// Visual effect for orb hit
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Create orb impact effect
var orbImpact = game.addChild(LK.getAsset('energySphere', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x,
y: enemy.y,
scaleX: 0.3,
scaleY: 0.3
}));
orbImpact.tint = 0xFFD700;
orbImpact.alpha = 0.8;
// Animate orb impact
tween(orbImpact, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 250,
easing: tween.easeOut,
onFinish: function onFinish() {
orbImpact.destroy();
}
});
}
}
};
return self;
});
var Projectile = Container.expand(function () {
var self = Container.call(this);
// Create glowing light effect behind projectile
var lightGlow = self.attachAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.0,
scaleY: 2.0
});
lightGlow.alpha = 0.15;
lightGlow.tint = 0x44aaff; // Blue-white glow
// Animate the glow effect
tween(lightGlow, {
scaleX: 3.5,
scaleY: 3.5,
alpha: 0.05
}, {
duration: 800,
easing: tween.easeInOut
});
tween(lightGlow, {
rotation: Math.PI * 4
}, {
duration: 1000,
easing: tween.linear
});
var projectileGraphics = self.attachAsset('projectile', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3.0,
scaleY: 3.0
});
// Create spell trail array
self.trailSegments = [];
self.maxTrailSegments = 8;
self.trailTimer = 0;
self.speed = 50;
self.direction = {
x: 0,
y: 0
};
self.lastIntersecting = {};
self.targetEnemy = null; // The specific enemy this projectile should damage
self.update = function () {
// 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
// Create spell trail segments
self.trailTimer++;
if (self.trailTimer >= 3) {
// Create trail segment every 3 frames
self.trailTimer = 0;
// Create new trail segment at current position
var trailSegment = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
scaleX: 0.6,
scaleY: 0.6
}));
trailSegment.tint = 0x44aaff; // Blue tint for trail
trailSegment.alpha = 0.8;
// Animate trail segment to fade out and shrink
tween(trailSegment, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
trailSegment.destroy();
}
});
// Add trail segment to array
self.trailSegments.push(trailSegment);
// Remove old trail segments if too many
if (self.trailSegments.length > self.maxTrailSegments) {
var oldSegment = self.trailSegments.shift();
if (oldSegment && oldSegment.parent) {
oldSegment.destroy();
}
}
}
// 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
// Calculate damage based on spell power upgrade
var damage = 100;
if (wizard && wizard.spellPowerLevel && wizard.spellPowerLevel > 0) {
// Check if target is an ogre and we're before enemy 35
if (self.targetEnemy && self.targetEnemy.ogreFrames && enemyKillCounter < 35) {
// Always kill ogres in one hit until enemy 35
damage = self.targetEnemy.health + 1; // Ensure one-hit kill
} else {
damage = 200; // Double damage when spell power is active
}
}
self.targetEnemy.takeDamage(damage);
self.removeFromGame();
return;
}
self.lastIntersecting.target = currentTargetIntersecting;
}
};
self.removeFromGame = function () {
// Clean up trail segments
for (var i = 0; i < self.trailSegments.length; i++) {
var segment = self.trailSegments[i];
if (segment && segment.parent) {
segment.destroy();
}
}
self.trailSegments = [];
// Remove from projectiles array
for (var i = projectiles.length - 1; i >= 0; i--) {
if (projectiles[i] === self) {
projectiles.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
var UpgradeMenu = Container.expand(function () {
var self = Container.call(this);
// Set high z-index to ensure menu appears above all game elements
self.zIndex = 1000;
// Cooldown system for upgrade menu
self.cooldownDuration = 120; // 2 seconds at 60fps
self.cooldownTimer = 0;
// Background removed - no overlay needed
// Title text
var titleText = new Text2('UPGRADES', {
size: 120,
fill: 0xFFD700,
font: "monospace"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 1050;
self.addChild(titleText);
// Initialize upgrade cost
self.upgradeCost = 5;
// Store references to upgrade text elements for updates
self.upgradeTextElements = [];
// Upgrade options - expanded to 10 upgrades
var upgradeOptions = [{
name: 'SHIELD',
description: 'Block 1 attack'
}, {
name: 'HEALTH BOOST',
description: 'More health'
}, {
name: 'LIFE DRAIN',
description: '5% chance +10 HP'
}, {
name: 'ENERGY SPHERE',
description: 'Auto-attack every 3s'
}, {
name: 'FORCE PUSH',
description: 'Push enemies back'
}, {
name: 'SPELL POWER',
description: 'Double damage'
}, {
name: 'REGENERATION',
description: 'Heal 5 HP/sec'
}, {
name: 'FIRE BALL',
description: 'Launch fire ball every 3s'
}, {
name: 'FREEZE PULSE',
description: 'Freeze all enemies'
}, {
name: 'ORBS',
description: 'Orbs rotate around player'
}];
// Create upgrade buttons in 2x5 grid layout
for (var i = 0; i < upgradeOptions.length; i++) {
var upgrade = upgradeOptions[i];
// Calculate position in 2-row, 5-column grid
var col = i % 5; // Column (0-4)
var row = Math.floor(i / 5); // Row (0-1)
var xPos = 2048 / 2 - 800 + col * 400; // Space 5 upgrades across screen width
var yPos = 1200 + row * 300; // Two rows with vertical spacing
// Upgrade button background
var upgradeBtn = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: xPos,
y: yPos,
scaleX: 5,
scaleY: 3.5
});
upgradeBtn.upgradeIndex = i;
upgradeBtn.tint = 0x8B008B;
// Upgrade text
var upgradeText = new Text2(upgrade.name + '\n' + upgrade.description + '\nCost: ' + self.upgradeCost + ' coins', {
size: 35,
fill: 0xFFFFFF,
font: "monospace"
});
upgradeText.anchor.set(0.5, 0.5);
upgradeText.x = xPos;
upgradeText.y = yPos;
upgradeText.upgradeIndex = i; // Store upgrade index for reference
upgradeText.upgradeName = upgrade.name;
upgradeText.upgradeDescription = upgrade.description;
self.addChild(upgradeText);
self.upgradeTextElements.push(upgradeText); // Store reference for updates
}
// Close button
var closeBtn = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1900,
scaleX: 2,
scaleY: 2
});
closeBtn.tint = 0xFF0000;
var closeText = new Text2('CLOSE', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
closeText.anchor.set(0.5, 0.5);
closeText.x = 2048 / 2;
closeText.y = 1900;
self.addChild(closeText);
// Method to update upgrade text with new costs and show improved versions
self.updateUpgradeCosts = function () {
var upgradeOptions = [{
name: 'SHIELD',
key: 'shield',
baseDesc: 'Block 1 attack',
improvedDesc: 'Block 2 attacks + regen'
}, {
name: 'HEALTH BOOST',
key: 'healthBoost',
baseDesc: 'More health',
improvedDesc: 'Much more health'
}, {
name: 'LIFE DRAIN',
key: 'lifeDrain',
baseDesc: '5% chance +10 HP',
improvedDesc: '10% chance +20 HP'
}, {
name: 'ENERGY SPHERE',
key: 'energySphere',
baseDesc: 'Auto-attack every 3s',
improvedDesc: 'Auto-attack every 2s'
}, {
name: 'FORCE PUSH',
key: 'forcePush',
baseDesc: 'Push enemies back',
improvedDesc: 'Stronger push + damage'
}, {
name: 'SPELL POWER',
key: 'spellPower',
baseDesc: 'Double damage',
improvedDesc: 'Triple damage'
}, {
name: 'REGENERATION',
key: 'regeneration',
baseDesc: 'Heal 5 HP/sec',
improvedDesc: 'Heal 10 HP/sec'
}, {
name: 'FIRE BALL',
key: 'fireBall',
baseDesc: 'Launch fire ball every 3s',
improvedDesc: 'Launch fire ball every 2s'
}, {
name: 'FREEZE PULSE',
key: 'freezePulse',
baseDesc: 'Freeze all enemies',
improvedDesc: 'Freeze longer + damage'
}, {
name: 'ORBS',
key: 'orbs',
baseDesc: 'Orbs rotate around player',
improvedDesc: 'More orbs + faster rotation'
}];
for (var i = 0; i < self.upgradeTextElements.length; i++) {
var textElement = self.upgradeTextElements[i];
var upgrade = upgradeOptions[i];
var currentLevel = upgradeLevels[upgrade.key];
// Show improved version if already purchased
var description = currentLevel > 0 ? upgrade.improvedDesc : upgrade.baseDesc;
var levelText = currentLevel > 0 ? ' (LV' + (currentLevel + 1) + ')' : '';
textElement.setText(upgrade.name + levelText + '\n' + description + '\nCost: ' + self.upgradeCost + ' coins');
textElement.upgradeName = upgrade.name;
textElement.upgradeDescription = description;
}
};
// 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 grid position (2x5 layout)
var upgradeIndex = -1;
// Check which grid cell was clicked
for (var i = 0; i < 10; i++) {
var col = i % 5; // Column (0-4)
var row = Math.floor(i / 5); // Row (0-1)
var cellX = 2048 / 2 - 800 + col * 400; // Calculate cell center X
var cellY = 1200 + row * 300; // Calculate cell center Y
var cellWidth = 300; // Touch area width
var cellHeight = 200; // Touch area height
// Check if click is within this cell
if (x >= cellX - cellWidth / 2 && x <= cellX + cellWidth / 2 && y >= cellY - cellHeight / 2 && y <= cellY + cellHeight / 2) {
upgradeIndex = i;
break;
}
}
// Check if upgrade button was clicked
if (upgradeIndex !== -1) {
// Close menu immediately when any upgrade option is selected
self.closeMenu();
var currentCost = self.upgradeCost || 5; // Use dynamic cost or default to 5
if (coinCounter >= currentCost) {
coinCounter -= currentCost;
coinText.setText('Coins: ' + coinCounter);
// Apply upgrade based on index
switch (upgradeIndex) {
case 0:
// Shield upgrade
upgradeLevels.shield++;
if (upgradeLevels.shield === 1) {
// First level: Basic shield
wizard.shieldActive = true;
wizard.maxShieldHits = 1;
wizard.currentShieldHits = 0;
} else {
// Improved level: Double shield + regeneration
wizard.shieldActive = true;
wizard.maxShieldHits = 2;
wizard.currentShieldHits = 0;
wizard.shieldRegen = true; // Enable shield regeneration
}
// Visual feedback for shield activation
LK.effects.flashObject(wizard, 0x00BFFF, 500);
// Add shield activation animation effect
tween(wizard.shieldGraphics, {
scaleX: 5,
scaleY: 5,
alpha: 1.0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(wizard.shieldGraphics, {
scaleX: 3,
scaleY: 3,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeIn
});
}
});
break;
case 1:
// Health Boost upgrade
upgradeLevels.healthBoost++;
var healthIncrease = upgradeLevels.healthBoost === 1 ? 30 : 50;
wizard.maxHealth += healthIncrease;
wizard.health = Math.min(wizard.health + healthIncrease, wizard.maxHealth);
updateHealthBar();
break;
case 2:
// Life Drain upgrade
upgradeLevels.lifeDrain++;
if (!wizard.lifeDrainLevel) {
wizard.lifeDrainLevel = 0;
}
wizard.lifeDrainLevel = upgradeLevels.lifeDrain;
break;
case 3:
// Energy Sphere upgrade
upgradeLevels.energySphere++;
if (upgradeLevels.energySphere === 1) {
// First level: Create energy sphere
if (!wizard.energySphere) {
wizard.energySphere = game.addChild(new EnergyOrb());
}
}
// Visual feedback for energy sphere activation
LK.effects.flashObject(wizard, 0x00FFFF, 500);
break;
case 4:
// Force Push upgrade
upgradeLevels.forcePush++;
if (!wizard.forcePushLevel) {
wizard.forcePushLevel = 0;
wizard.forcePushTimer = 0;
}
wizard.forcePushLevel = upgradeLevels.forcePush;
// Visual feedback for force push
LK.effects.flashObject(wizard, 0xFFFF00, 500);
break;
case 5:
// Spell Power upgrade
upgradeLevels.spellPower++;
if (!wizard.spellPowerLevel) {
wizard.spellPowerLevel = 0;
}
wizard.spellPowerLevel = upgradeLevels.spellPower;
// Visual feedback for spell power
LK.effects.flashObject(wizard, 0xFF4500, 500);
break;
case 6:
// Regeneration upgrade
upgradeLevels.regeneration++;
if (!wizard.regenerationLevel) {
wizard.regenerationLevel = 0;
}
wizard.regenerationLevel = upgradeLevels.regeneration;
// Visual feedback for regeneration
LK.effects.flashObject(wizard, 0x90EE90, 500);
break;
case 7:
// Fire Ball upgrade
upgradeLevels.fireBall++;
if (!wizard.fireBallLevel) {
wizard.fireBallLevel = 0;
wizard.fireBallTimer = 0;
}
wizard.fireBallLevel = upgradeLevels.fireBall;
// Visual feedback for fire ball
LK.effects.flashObject(wizard, 0xFF4500, 500);
break;
case 8:
// Freeze Pulse upgrade
upgradeLevels.freezePulse++;
if (!wizard.freezePulseLevel) {
wizard.freezePulseLevel = 0;
wizard.freezePulseTimer = 0;
}
wizard.freezePulseLevel = upgradeLevels.freezePulse;
// Visual feedback for freeze pulse
LK.effects.flashObject(wizard, 0x87CEEB, 500);
break;
case 9:
// Orbs upgrade
upgradeLevels.orbs++;
if (!wizard.orbLevel) {
wizard.orbLevel = 0;
wizard.orbs = []; // Array to store orb instances
}
wizard.orbLevel++;
// Create new orb
var newOrb = game.addChild(new Orb());
// Set starting angle based on number of orbs for even distribution
newOrb.orbitalAngle = wizard.orbs.length * (Math.PI * 2) / (wizard.orbLevel + 1);
wizard.orbs.push(newOrb);
// Redistribute existing orbs evenly
for (var orbIdx = 0; orbIdx < wizard.orbs.length; orbIdx++) {
wizard.orbs[orbIdx].orbitalAngle = orbIdx * (Math.PI * 2) / wizard.orbs.length;
}
// Visual feedback for orb upgrade
LK.effects.flashObject(wizard, 0xFFD700, 500);
break;
}
// Visual feedback - flash the upgrade area green
LK.effects.flashScreen(0x00FF00, 300);
} else {
// Not enough coins - flash red
LK.effects.flashScreen(0xFF0000, 300);
}
} else if (y >= 1750 && y <= 2050) {
// Close button area clicked
self.closeMenu();
}
};
self.closeMenu = function () {
self.visible = false;
gameStarted = true;
// Resume appropriate music based on game progress
if (enemyKillCounter >= 25) {
LK.playMusic('mysticalAmbient', {
volume: 0.6,
fade: {
start: 0,
end: 0.6,
duration: 1000
}
});
} else if (enemyKillCounter >= 10) {
LK.playMusic('epicBattle', {
volume: 0.8,
fade: {
start: 0,
end: 0.8,
duration: 1000
}
});
} else {
LK.playMusic('medievalTheme', {
volume: 0.7,
fade: {
start: 0,
end: 0.7,
duration: 1000
}
});
}
};
return self;
});
var Wizard = Container.expand(function () {
var self = Container.call(this);
// Animation system for wizard
self.currentFrame = 1;
self.animationTimer = 0;
self.animationSpeed = 18; // Change frame every 18 ticks (300ms at 60fps)
// Create all wizard graphics frames and store them
self.wizardFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset('wizard' + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 2.5,
scaleY: 2.5
});
frameGraphics.visible = i === 1; // Only show first frame initially
self.wizardFrames.push(frameGraphics);
}
// Create shield visual effect
self.shieldGraphics = self.attachAsset('shield', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 3
});
self.shieldGraphics.alpha = 0.7;
self.shieldGraphics.visible = false;
self.attackCooldown = 0;
self.level = 1;
self.experience = 0;
self.health = 100;
self.maxHealth = 100;
self.spellPowerLevel = 0; // Track spell power upgrades
self.shieldActive = false; // Track shield status
self.orbLevel = 0; // Track orb upgrades
self.orbs = []; // Array to store orb instances
self.update = function () {
// Pause all upgrades when upgrade menu is visible
if (upgradeMenu && upgradeMenu.visible) {
return;
}
if (self.attackCooldown > 0) {
self.attackCooldown--;
}
// Update shield visibility based on shield status
self.shieldGraphics.visible = self.shieldActive;
if (self.shieldActive) {
// Animate shield with pulsing effect
var pulse = 1 + Math.sin(LK.ticks * 0.15) * 0.2;
self.shieldGraphics.scaleX = 3 * pulse;
self.shieldGraphics.scaleY = 3 * pulse;
// Slowly rotate shield
self.shieldGraphics.rotation += 0.03;
// Add glowing effect
self.shieldGraphics.alpha = 0.6 + Math.sin(LK.ticks * 0.1) * 0.2;
}
// Force Push upgrade - push all enemies backward every 4 seconds
if (self.forcePushLevel && self.forcePushLevel > 0) {
if (!self.forcePushTimer) {
self.forcePushTimer = 0;
}
self.forcePushTimer++;
if (self.forcePushTimer >= 240) {
// 4 seconds at 60fps
self.forcePushTimer = 0;
self.activateForcePush();
}
}
// Freeze Pulse upgrade - freeze all enemies every 4 seconds
if (self.freezePulseLevel && self.freezePulseLevel > 0) {
if (!self.freezePulseTimer) {
self.freezePulseTimer = 0;
}
self.freezePulseTimer++;
if (self.freezePulseTimer >= 240) {
// 4 seconds at 60fps
self.freezePulseTimer = 0;
self.activateFreezePulse();
}
}
// Fire Ball upgrade - launch fire ball with improved timing
if (self.fireBallLevel && self.fireBallLevel > 0) {
if (!self.fireBallTimer) {
self.fireBallTimer = 0;
}
self.fireBallTimer++;
var fireBallInterval = upgradeLevels.fireBall > 1 ? 120 : 180; // 2s vs 3s
if (self.fireBallTimer >= fireBallInterval) {
self.fireBallTimer = 0;
self.launchFireBall();
}
}
// Regeneration upgrade - heal over time with improved rate
if (self.regenerationLevel && self.regenerationLevel > 0) {
if (!self.regenTimer) {
self.regenTimer = 0;
}
self.regenTimer++;
if (self.regenTimer >= 60) {
// Every second
self.regenTimer = 0;
var healAmount = upgradeLevels.regeneration > 1 ? 10 : 5; // Improved healing
self.health = Math.min(self.health + healAmount, self.maxHealth);
updateHealthBar();
// Visual feedback for regeneration
LK.effects.flashObject(self, 0x90EE90, 200);
}
}
// Animation system - cycle through wizard frames
self.animationTimer++;
if (self.animationTimer >= self.animationSpeed) {
self.animationTimer = 0;
// Hide current frame
self.wizardFrames[self.currentFrame - 1].visible = false;
// Move to next frame
self.currentFrame++;
if (self.currentFrame > 4) {
self.currentFrame = 1;
}
// Show new frame
self.wizardFrames[self.currentFrame - 1].visible = true;
}
};
self.attack = function (direction) {
if (self.attackCooldown <= 0) {
// Default direction if none specified
if (direction === undefined) {
direction = 0; // Default to center path
}
// Get attack angle based on path direction
var attackAngle = pathAngles[direction];
var attackDistance = 100;
// Calculate spell position based on attack direction
var spellX = self.x + Math.cos(attackAngle) * attackDistance;
var spellY = self.y + Math.sin(attackAngle) * attackDistance;
// Create spell effect
var spell = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: spellX,
y: spellY,
scaleX: 0.5,
scaleY: 0.5
}));
// Animate spell with magical effects
tween(spell, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
spell.destroy();
}
});
// Add rotation animation to spell
tween(spell, {
rotation: Math.PI * 2
}, {
duration: 500,
easing: tween.linear
});
self.attackCooldown = 30; // 0.5 seconds at 60fps
LK.getSound('spellCast').play();
// Base damage without spell power upgrades
var totalDamage = 1;
// Attack enemies in the specified direction/path
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.pathIndex === direction) {
// Only hit enemies on exact same path - no distance validation
enemy.takeDamage(totalDamage);
}
}
// Attack ogres in the specified direction/path
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
if (ogre.pathIndex === direction) {
// Only hit ogres on exact same path - no distance validation
ogre.takeDamage(totalDamage);
}
}
// Attack knights in the specified direction/path
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
if (knight.pathIndex === direction) {
// Only hit knights on exact same path - no distance validation
knight.takeDamage(totalDamage);
}
}
// Attack mini bosses in the specified direction/path
for (var i = miniBosses.length - 1; i >= 0; i--) {
var miniBoss = miniBosses[i];
if (miniBoss.pathIndex === direction) {
// Only hit mini bosses on exact same path - no distance validation
miniBoss.takeDamage(totalDamage);
}
}
return true;
}
return false;
};
self.gainExperience = function (amount) {
self.experience += amount;
var expNeeded = self.level * 100;
if (self.experience >= expNeeded) {
self.levelUp();
}
};
self.levelUp = function () {
self.level++;
self.experience = 0;
// Visual level up effect
LK.effects.flashObject(self, 0xFFD700, 500);
};
self.takeDamage = function (damage) {
// Check if shield is active
if (self.shieldActive) {
// Initialize shield properties if not set
if (self.maxShieldHits === undefined) {
self.maxShieldHits = 1;
self.currentShieldHits = 0;
}
// Increment shield hits
self.currentShieldHits++;
// Visual feedback for shield use
LK.effects.flashObject(self, 0x00BFFF, 300);
// Check if shield is depleted
if (self.currentShieldHits >= self.maxShieldHits) {
self.shieldActive = false;
// Start shield regeneration timer
var regenTime = self.shieldRegen ? 5000 : 10000; // Faster regen if improved
tween({}, {}, {
duration: regenTime,
onFinish: function onFinish() {
// Regenerate shield
self.shieldActive = true;
self.currentShieldHits = 0;
// Visual feedback for shield regeneration
LK.effects.flashObject(self, 0x00BFFF, 500);
// Add shield regeneration animation
tween(self.shieldGraphics, {
scaleX: 5,
scaleY: 5,
alpha: 1.0
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.shieldGraphics, {
scaleX: 3,
scaleY: 3,
alpha: 0.7
}, {
duration: 400,
easing: tween.easeIn
});
}
});
}
});
}
// No damage taken, shield absorbed it
return;
}
self.health -= damage;
if (self.health <= 0) {
self.health = 0;
// Game over when health reaches 0
LK.effects.flashScreen(0xFF0000, 1000);
LK.showGameOver();
}
// Update health bar
updateHealthBar();
// Add significant screen shake when taking damage
var shakeIntensity = 30; // Strong shake for taking damage
var originalX = game.x;
var originalY = game.y;
// Create aggressive screen shake sequence
tween(game, {
x: originalX + shakeIntensity,
y: originalY + shakeIntensity * 0.5
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(game, {
x: originalX - shakeIntensity * 0.8,
y: originalY - shakeIntensity * 0.3
}, {
duration: 70,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(game, {
x: originalX + shakeIntensity * 0.6,
y: originalY + shakeIntensity * 0.4
}, {
duration: 60,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(game, {
x: originalX - shakeIntensity * 0.3,
y: originalY - shakeIntensity * 0.2
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(game, {
x: originalX,
y: originalY
}, {
duration: 40,
easing: tween.easeIn
});
}
});
}
});
}
});
}
});
// Flash red when hit
LK.effects.flashObject(self, 0xFF0000, 200);
};
self.activateForcePush = function () {
// Visual effect for force push activation
LK.effects.flashScreen(0x8A2BE2, 300); // Purple flash
LK.effects.flashObject(self, 0x8A2BE2, 500); // Purple flash on wizard
// Push back all enemies with improved effects
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
// Calculate direction from wizard to enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Improved force push: stronger push and damage
var pushDistance = upgradeLevels.forcePush > 1 ? 300 : 200; // Stronger push
var pushX = dx / distance * pushDistance;
var pushY = dy / distance * pushDistance;
// Calculate new position
var newX = enemy.x + pushX;
var newY = enemy.y + pushY;
// Ensure enemies don't go off screen
newX = Math.max(50, Math.min(1998, newX));
newY = Math.max(-100, Math.min(2732, newY));
// Animate the push effect
tween(enemy, {
x: newX,
y: newY
}, {
duration: 300,
easing: tween.easeOut
});
// Improved force push: deal damage
if (upgradeLevels.forcePush > 1) {
enemy.takeDamage(50);
}
// Visual effect on each enemy
LK.effects.flashObject(enemy, 0x8A2BE2, 200);
}
}
};
self.activateFreezePulse = function () {
// Visual effect for freeze pulse activation
LK.effects.flashScreen(0x87CEEB, 500); // Light blue flash
LK.effects.flashObject(self, 0x87CEEB, 700); // Light blue flash on wizard
// Freeze all enemies with improved effects
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
// Improved freeze: longer duration and damage
var freezeDuration = upgradeLevels.freezePulse > 1 ? 120 : 60; // 2s vs 1s
enemy.frozen = true;
enemy.frozenTimer = freezeDuration;
// Improved freeze: deal damage
if (upgradeLevels.freezePulse > 1) {
enemy.takeDamage(30);
}
// Visual freeze effect - tint enemy light blue
tween(enemy, {
tint: 0x87CEEB
}, {
duration: 100,
easing: tween.easeOut
});
// Remove freeze tint after frozen state ends
var visualDuration = upgradeLevels.freezePulse > 1 ? 2000 : 1000;
tween({}, {}, {
duration: visualDuration,
onFinish: function onFinish() {
if (enemy && enemy.parent) {
// Remove freeze tint after frozen effect ends
tween(enemy, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeIn
});
}
}
});
}
};
self.launchFireBall = function () {
// Visual effect for fire ball launch
LK.effects.flashScreen(0xFF4500, 300); // Orange flash
LK.effects.flashObject(self, 0xFF4500, 500); // Orange flash on wizard
// Create fire ball projectile at wizard position
var fireBall = game.addChild(new FireBall());
fireBall.x = self.x;
fireBall.y = self.y;
// Find closest enemy to target
var closestEnemy = null;
var closestDistance = Infinity;
var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// Set fire ball direction toward closest enemy or default direction
if (closestEnemy) {
var dx = closestEnemy.x - self.x;
var dy = closestEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
fireBall.direction.x = dx / distance;
fireBall.direction.y = dy / distance;
}
} else {
// Default direction upward if no enemies
fireBall.direction.x = 0;
fireBall.direction.y = -1;
}
LK.getSound('spellCast').play();
};
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 thirdUpgradeMenuShown = false;
var fourthUpgradeMenuShown = false;
var selectedEnemy = null; // Track currently selected enemy for projectile targeting
var energySphere = null; // Track energy sphere instance
// Upgrade level tracking system
var upgradeLevels = {
shield: 0,
healthBoost: 0,
lifeDrain: 0,
energySphere: 0,
forcePush: 0,
spellPower: 0,
regeneration: 0,
fireBall: 0,
freezePulse: 0,
orbs: 0
};
// Game arrays to track objects
var enemies = [];
var ogres = [];
var knights = [];
var miniBosses = [];
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;
// Increase upgrade costs by 5 coins when menu is shown
if (!upgradeMenu.upgradeCost) {
upgradeMenu.upgradeCost = 5; // Initial cost
}
upgradeMenu.upgradeCost += 5; // Increase by 5 coins each time
// Update all upgrade text to show new costs
upgradeMenu.updateUpgradeCosts();
upgradeMenu.visible = true;
upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown
gameStarted = false; // Pause game while menu is open
// Keep music playing during upgrade menu
// Clear all enemies on screen when upgrade menu is shown
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemies.splice(i, 1);
enemy.destroy();
}
// Clear all ogres on screen when upgrade menu is shown
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
ogres.splice(i, 1);
ogre.destroy();
}
// Clear all knights on screen when upgrade menu is shown
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
knights.splice(i, 1);
knight.destroy();
}
// Clear all mini bosses on screen when upgrade menu is shown
for (var i = miniBosses.length - 1; i >= 0; i--) {
var miniBoss = miniBosses[i];
miniBosses.splice(i, 1);
miniBoss.destroy();
}
}
// Show upgrade menu again after 35 enemies are killed
if (enemyKillCounter >= 35 && upgradeMenuShown && !secondUpgradeMenuShown) {
secondUpgradeMenuShown = true;
// Increase upgrade costs by 5 coins when menu is shown
if (!upgradeMenu.upgradeCost) {
upgradeMenu.upgradeCost = 5; // Initial cost
}
upgradeMenu.upgradeCost += 5; // Increase by 5 coins each time
// Update all upgrade text to show new costs
upgradeMenu.updateUpgradeCosts();
upgradeMenu.visible = true;
upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown
gameStarted = false; // Pause game while menu is open
// Keep music playing during upgrade menu
// Clear all enemies on screen when upgrade menu is shown
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemies.splice(i, 1);
enemy.destroy();
}
// Clear all ogres on screen when upgrade menu is shown
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
ogres.splice(i, 1);
ogre.destroy();
}
// Clear all knights on screen when upgrade menu is shown
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
knights.splice(i, 1);
knight.destroy();
}
}
// Show third upgrade menu after 50 enemies are killed
if (enemyKillCounter >= 50 && secondUpgradeMenuShown && !thirdUpgradeMenuShown) {
thirdUpgradeMenuShown = true;
// Increase upgrade costs by 5 coins when menu is shown
if (!upgradeMenu.upgradeCost) {
upgradeMenu.upgradeCost = 5; // Initial cost
}
upgradeMenu.upgradeCost += 5; // Increase by 5 coins each time
// Update all upgrade text to show new costs
upgradeMenu.updateUpgradeCosts();
upgradeMenu.visible = true;
upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown
gameStarted = false; // Pause game while menu is open
// Keep music playing during upgrade menu
// Clear all enemies on screen when upgrade menu is shown
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemies.splice(i, 1);
enemy.destroy();
}
// Clear all ogres on screen when upgrade menu is shown
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
ogres.splice(i, 1);
ogre.destroy();
}
// Clear all knights on screen when upgrade menu is shown
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
knights.splice(i, 1);
knight.destroy();
}
}
// Show fourth upgrade menu after 70 enemies are killed
if (enemyKillCounter >= 70 && thirdUpgradeMenuShown && !fourthUpgradeMenuShown) {
fourthUpgradeMenuShown = true;
// Increase upgrade costs by 5 coins when menu is shown
if (!upgradeMenu.upgradeCost) {
upgradeMenu.upgradeCost = 5; // Initial cost
}
upgradeMenu.upgradeCost += 5; // Increase by 5 coins each time
// Update all upgrade text to show new costs
upgradeMenu.updateUpgradeCosts();
upgradeMenu.visible = true;
upgradeMenu.cooldownTimer = upgradeMenu.cooldownDuration; // Start 2-second cooldown
gameStarted = false; // Pause game while menu is open
// Keep music playing during upgrade menu
// Clear all enemies on screen when upgrade menu is shown
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemies.splice(i, 1);
enemy.destroy();
}
// Clear all ogres on screen when upgrade menu is shown
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
ogres.splice(i, 1);
ogre.destroy();
}
// Clear all knights on screen when upgrade menu is shown
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
knights.splice(i, 1);
knight.destroy();
}
}
// Reset consecutive spawns for paths that have cooled down (runs every frame)
for (var pathIdx = 0; pathIdx < 5; pathIdx++) {
// Check if enough time has passed since last spawn (cooldown expired)
if (pathLastSpawnTime[pathIdx] !== -1 && LK.ticks - pathLastSpawnTime[pathIdx] > pathCooldownDuration) {
// Reset consecutive spawns for this path due to cooldown
pathConsecutiveSpawns[pathIdx] = 0;
}
}
// 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 = 100;
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 = 200; // 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 mini boss after 35 enemies killed (only once)
if (enemyKillCounter === 35) {
var miniBoss = game.addChild(new MiniBoss());
// Spawn mini boss at center top edge for dramatic entrance
miniBoss.x = 2048 / 2;
miniBoss.y = -200;
miniBoss.lastX = miniBoss.x;
// Store path information (center path)
miniBoss.pathIndex = 0;
miniBoss.pathAngle = pathAngles[0];
miniBosses.push(miniBoss);
// Visual and audio feedback for mini boss spawn
LK.effects.flashScreen(0x8B0000, 1000); // Dark red flash
LK.getSound('painSound').play(); // Ominous sound
// Screen shake for mini boss entrance
var shakeIntensity = 40;
var originalX = game.x;
var originalY = game.y;
tween(game, {
x: originalX + shakeIntensity,
y: originalY + shakeIntensity * 0.5
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(game, {
x: originalX - shakeIntensity * 0.7,
y: originalY - shakeIntensity * 0.3
}, {
duration: 120,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(game, {
x: originalX,
y: originalY
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
});
}
// 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 = 300; // 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;
}
// Check mini boss-wizard collisions and cleanup off-screen mini bosses
for (var i = miniBosses.length - 1; i >= 0; i--) {
var miniBoss = miniBosses[i];
// Remove mini bosses that went off-screen at bottom
if (miniBoss.y > 2732 + 100) {
miniBosses.splice(i, 1);
miniBoss.destroy();
continue;
}
// Initialize lastIntersecting if not set
if (miniBoss.lastIntersecting === undefined) {
miniBoss.lastIntersecting = false;
}
// Check for collision transition
var currentMiniBossIntersecting = miniBoss.intersects(wizard);
if (!miniBoss.lastIntersecting && currentMiniBossIntersecting) {
// Damage wizard when mini boss touches for the first time
wizard.takeDamage(80); // Mini bosses deal massive damage
// Remove mini boss after dealing damage
miniBosses.splice(i, 1);
miniBoss.destroy();
continue;
}
// Update last intersecting state
miniBoss.lastIntersecting = currentMiniBossIntersecting;
}
// 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();
}
}
}); ===================================================================
--- original.js
+++ change.js
@@ -420,10 +420,10 @@
};
self.attackClosestEnemy = function () {
var closestEnemy = null;
var closestDistance = Infinity;
- // Check all enemy types for closest one
- var allEnemies = enemies.concat(ogres).concat(knights);
+ // Check all enemy types for closest one, prioritizing mini bosses
+ var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
@@ -504,9 +504,9 @@
self.destroy();
return;
}
// Check collision with all enemy types
- var allEnemies = enemies.concat(ogres).concat(knights);
+ var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
if (!self.lastIntersecting[i]) {
self.lastIntersecting[i] = false;
@@ -922,8 +922,312 @@
});
};
return self;
});
+var MiniBoss = Container.expand(function () {
+ var self = Container.call(this);
+ // Animation system for mini boss
+ self.currentFrame = 1;
+ self.animationTimer = 0;
+ self.animationSpeed = 10; // Faster animation for intimidating effect
+ self.animationState = 'walking'; // walking, attacking, dying, idle
+ // Create all knight graphics frames for mini boss (reusing knight assets but larger)
+ self.bossFrames = [];
+ for (var i = 1; i <= 4; i++) {
+ var frameGraphics = self.attachAsset('knight' + i, {
+ anchorX: 0.5,
+ anchorY: 1.0,
+ scaleX: 4.0,
+ // Much larger than normal knights
+ scaleY: 4.0
+ });
+ frameGraphics.visible = i === 1; // Only show first frame initially
+ frameGraphics.tint = 0x8B0000; // Dark red tint to distinguish from normal knights
+ self.bossFrames.push(frameGraphics);
+ }
+ // Create invisible hitbox with larger size
+ var hitbox = self.attachAsset('knight1', {
+ anchorX: 0.5,
+ anchorY: 1.0,
+ scaleX: 8,
+ scaleY: 8
+ });
+ hitbox.alpha = 0; // Make hitbox invisible
+ self.health = 1000; // Very high health for mini boss
+ self.maxHealth = 1000;
+ self.speed = 4; // Slower than normal enemies but still threatening
+ self.lastX = 0;
+ self.frozen = false;
+ self.frozenTimer = 0;
+ self.isDying = false;
+ // Special mini boss abilities
+ self.specialAttackTimer = 0;
+ self.specialAttackCooldown = 300; // 5 seconds between special attacks
+ self.update = function () {
+ // Skip all movement and collision if mini boss is dying
+ if (self.isDying) {
+ return;
+ }
+ // Animation system - cycle through boss frames based on state
+ self.animationTimer++;
+ var frameSpeed = self.animationSpeed;
+ if (self.animationState === 'attacking') {
+ frameSpeed = 5; // Very fast animation for attacking
+ } else if (self.animationState === 'dying') {
+ frameSpeed = 15; // Slower animation for dying
+ }
+ if (self.animationTimer >= frameSpeed) {
+ self.animationTimer = 0;
+ // Hide current frame
+ self.bossFrames[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++;
+ }
+ }
+ // Show new frame
+ self.bossFrames[self.currentFrame - 1].visible = true;
+ }
+ // Handle frozen state
+ if (self.frozen) {
+ self.frozenTimer--;
+ if (self.frozenTimer <= 0) {
+ self.frozen = false;
+ }
+ return; // Skip movement when frozen
+ }
+ // Always move directly toward the wizard
+ if (wizard) {
+ // Calculate direction from mini boss 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.bossFrames.length; frameIdx++) {
+ self.bossFrames[frameIdx].scaleX = -4.0; // Flip to face left
+ }
+ } else {
+ // Wizard is to the right - face right
+ for (var frameIdx = 0; frameIdx < self.bossFrames.length; frameIdx++) {
+ self.bossFrames[frameIdx].scaleX = 4.0; // Normal orientation to face right
+ }
+ }
+ }
+ // Special attack timer
+ self.specialAttackTimer++;
+ if (self.specialAttackTimer >= self.specialAttackCooldown) {
+ self.specialAttackTimer = 0;
+ self.performSpecialAttack();
+ }
+ };
+ self.performSpecialAttack = function () {
+ // Change to attacking animation
+ self.animationState = 'attacking';
+ // Create shockwave effect
+ LK.effects.flashScreen(0x8B0000, 400);
+ // Create multiple projectiles in all directions
+ for (var angle = 0; angle < Math.PI * 2; angle += Math.PI / 4) {
+ var projectile = game.addChild(new Projectile());
+ projectile.x = self.x;
+ projectile.y = self.y;
+ projectile.direction.x = Math.cos(angle);
+ projectile.direction.y = Math.sin(angle);
+ projectile.speed = 30; // Slower than normal projectiles
+ projectiles.push(projectile);
+ }
+ // Return to walking animation after attack
+ tween({}, {}, {
+ duration: 800,
+ onFinish: function onFinish() {
+ if (self.animationState === 'attacking') {
+ self.animationState = 'walking';
+ }
+ }
+ });
+ LK.getSound('spellCast').play();
+ };
+ 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, 300);
+ // Create large impact effect
+ var impactEffect = game.addChild(LK.getAsset('projectileGlow', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ x: self.x,
+ y: self.y,
+ scaleX: 2.0,
+ scaleY: 2.0
+ }));
+ impactEffect.tint = 0xFF4500;
+ impactEffect.alpha = 1.0;
+ // Animate impact effect with massive explosion
+ tween(impactEffect, {
+ scaleX: 8.0,
+ scaleY: 8.0,
+ alpha: 0
+ }, {
+ duration: 600,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ impactEffect.destroy();
+ }
+ });
+ // Return to walking animation after brief attacking animation
+ tween({}, {}, {
+ duration: 600,
+ onFinish: function onFinish() {
+ if (self.animationState === 'attacking') {
+ self.animationState = 'walking';
+ }
+ }
+ });
+ // Check if mini boss should die based on health
+ if (self.health <= 0) {
+ self.die();
+ }
+ };
+ self.down = function (x, y, obj) {
+ // Set this mini boss 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 mini boss when tapped
+ if (wizard && projectiles.length < 10) {
+ var projectile = game.addChild(new Projectile());
+ projectile.x = wizard.x;
+ projectile.y = wizard.y;
+ // Calculate direction from wizard to mini boss
+ 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 mini boss dies
+ LK.getSound('painSound').play();
+ // Start death animation sequence
+ self.isDying = true;
+ // Dramatic screen shake for mini boss death
+ var shakeIntensity = 50;
+ var originalX = game.x;
+ var originalY = game.y;
+ tween(game, {
+ x: originalX + shakeIntensity,
+ y: originalY + shakeIntensity * 0.5
+ }, {
+ duration: 100,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ tween(game, {
+ x: originalX,
+ y: originalY
+ }, {
+ duration: 100,
+ easing: tween.easeIn
+ });
+ }
+ });
+ // Animate mini boss death with epic collapse effect
+ tween(self, {
+ alpha: 0,
+ scaleX: 0.1,
+ scaleY: 0.1,
+ rotation: Math.PI * 2,
+ y: self.y + 200
+ }, {
+ duration: 2000,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ // After death animation completes, execute all death logic
+ // Drop multiple coins for mini boss
+ for (var coinIdx = 0; coinIdx < 5; coinIdx++) {
+ var coin = game.addChild(new Coin());
+ coin.x = self.x + (Math.random() - 0.5) * 200;
+ coin.y = self.y - 50 - coinIdx * 20;
+ 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: 1500 + coinIdx * 200,
+ 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 (counts as 5 kills)
+ enemyKillCounter += 5;
+ killCountText.setText('Puntuacion: ' + enemyKillCounter);
+ // Add massive experience to wizard
+ wizard.gainExperience(100);
+ // Clear selected enemy if this was the target
+ if (selectedEnemy === self) {
+ selectedEnemy = null;
+ }
+ // Remove from miniBosses array
+ for (var i = miniBosses.length - 1; i >= 0; i--) {
+ if (miniBosses[i] === self) {
+ miniBosses.splice(i, 1);
+ break;
+ }
+ }
+ self.destroy();
+ LK.setScore(LK.getScore() + 100);
+ }
+ });
+ };
+ return self;
+});
var Ogre = Container.expand(function () {
var self = Container.call(this);
// Animation system for ogre
self.currentFrame = 1;
@@ -1229,9 +1533,9 @@
var pulse = 1 + Math.sin(LK.ticks * 0.3) * 0.2;
orbGraphics.scaleX = 0.4 * pulse;
orbGraphics.scaleY = 0.4 * pulse;
// Check collision with enemies
- var allEnemies = enemies.concat(ogres).concat(knights);
+ var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
if (self.intersects(enemy)) {
// Deal damage to enemy on contact
@@ -2005,8 +2309,16 @@
// Only hit knights on exact same path - no distance validation
knight.takeDamage(totalDamage);
}
}
+ // Attack mini bosses in the specified direction/path
+ for (var i = miniBosses.length - 1; i >= 0; i--) {
+ var miniBoss = miniBosses[i];
+ if (miniBoss.pathIndex === direction) {
+ // Only hit mini bosses on exact same path - no distance validation
+ miniBoss.takeDamage(totalDamage);
+ }
+ }
return true;
}
return false;
};
@@ -2137,9 +2449,9 @@
// Visual effect for force push activation
LK.effects.flashScreen(0x8A2BE2, 300); // Purple flash
LK.effects.flashObject(self, 0x8A2BE2, 500); // Purple flash on wizard
// Push back all enemies with improved effects
- var allEnemies = enemies.concat(ogres).concat(knights);
+ var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
// Calculate direction from wizard to enemy
var dx = enemy.x - self.x;
@@ -2177,9 +2489,9 @@
// Visual effect for freeze pulse activation
LK.effects.flashScreen(0x87CEEB, 500); // Light blue flash
LK.effects.flashObject(self, 0x87CEEB, 700); // Light blue flash on wizard
// Freeze all enemies with improved effects
- var allEnemies = enemies.concat(ogres).concat(knights);
+ var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
// Improved freeze: longer duration and damage
var freezeDuration = upgradeLevels.freezePulse > 1 ? 120 : 60; // 2s vs 1s
@@ -2224,9 +2536,9 @@
fireBall.y = self.y;
// Find closest enemy to target
var closestEnemy = null;
var closestDistance = Infinity;
- var allEnemies = enemies.concat(ogres).concat(knights);
+ var allEnemies = enemies.concat(ogres).concat(knights).concat(miniBosses);
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
@@ -2251,92 +2563,8 @@
fireBall.direction.y = -1;
}
LK.getSound('spellCast').play();
};
- self.chargedAttack = function (direction, chargeLevel) {
- 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 enhanced spell effect based on charge level
- var spellScale = 0.5 + chargeLevel * 1.5; // Scale from 0.5 to 2.0
- var spell = game.addChild(LK.getAsset('spell', {
- anchorX: 0.5,
- anchorY: 0.5,
- x: spellX,
- y: spellY,
- scaleX: spellScale,
- scaleY: spellScale
- }));
- // Tint spell based on charge level
- if (chargeLevel < 0.3) {
- spell.tint = 0xff69b4; // Pink for low charge
- } else if (chargeLevel < 0.7) {
- spell.tint = 0x44aaff; // Blue for medium charge
- } else {
- spell.tint = 0xffd700; // Gold for high charge
- }
- // Animate spell with magical effects
- var animationDuration = 300 + chargeLevel * 400; // Longer animation for higher charge
- tween(spell, {
- scaleX: spellScale * 3,
- scaleY: spellScale * 3,
- alpha: 0
- }, {
- duration: animationDuration,
- easing: tween.easeOut,
- onFinish: function onFinish() {
- spell.destroy();
- }
- });
- // Add rotation animation to spell
- tween(spell, {
- rotation: Math.PI * 2 * (1 + chargeLevel)
- }, {
- duration: animationDuration,
- easing: tween.linear
- });
- self.attackCooldown = Math.max(15, 30 - chargeLevel * 15); // Shorter cooldown for charged attacks
- LK.getSound('spellCast').play();
- // Calculate damage based on charge level
- var baseDamage = 1;
- var chargeDamageMultiplier = 1 + chargeLevel * 4; // 1x to 5x damage multiplier
- var totalDamage = Math.floor(baseDamage * chargeDamageMultiplier);
- // 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;
- };
return self;
});
/****
@@ -2358,14 +2586,8 @@
var thirdUpgradeMenuShown = false;
var fourthUpgradeMenuShown = false;
var selectedEnemy = null; // Track currently selected enemy for projectile targeting
var energySphere = null; // Track energy sphere instance
-// Spell charging system variables
-var spellCharging = false;
-var chargeStartTime = 0;
-var chargingPath = -1;
-var chargeVisualEffect = null;
-var maxChargeTime = 180; // 3 seconds at 60fps for maximum charge
// Upgrade level tracking system
var upgradeLevels = {
shield: 0,
healthBoost: 0,
@@ -2381,8 +2603,9 @@
// Game arrays to track objects
var enemies = [];
var ogres = [];
var knights = [];
+var miniBosses = [];
var coins = [];
var projectiles = [];
// Create and show game menu
gameMenu = game.addChild(new GameMenu());
@@ -2525,23 +2748,12 @@
pathNumber.pathIndex = p;
game.addChild(pathNumber);
// Add touch handler for directional attacks
path.down = function (x, y, obj) {
- // Start spell charging
- spellCharging = true;
- chargeStartTime = LK.ticks;
- chargingPath = obj.pathIndex;
- // Create charge visual effect at wizard position
- chargeVisualEffect = game.addChild(LK.getAsset('projectileGlow', {
- anchorX: 0.5,
- anchorY: 0.5,
- x: wizard.x,
- y: wizard.y,
- scaleX: 1.0,
- scaleY: 1.0
- }));
- chargeVisualEffect.tint = 0x44aaff;
- chargeVisualEffect.alpha = 0.3;
+ // 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
@@ -2665,27 +2877,8 @@
}
}
// No default attack - player must tap a path to attack
};
-// Game input handling for spell release
-game.up = function (x, y, obj) {
- if (spellCharging && chargingPath !== -1) {
- // Calculate charge duration
- var chargeDuration = LK.ticks - chargeStartTime;
- var chargeLevel = Math.min(chargeDuration / maxChargeTime, 1.0); // 0.0 to 1.0
- // Clean up charge visual effect
- if (chargeVisualEffect && chargeVisualEffect.parent) {
- chargeVisualEffect.destroy();
- chargeVisualEffect = null;
- }
- // Execute charged attack
- wizard.chargedAttack(chargingPath, chargeLevel);
- // Reset charging state
- spellCharging = false;
- chargingPath = -1;
- chargeStartTime = 0;
- }
-};
// Main game update loop
game.update = function () {
// Sort children by z-index to ensure proper rendering order
game.children.sort(function (a, b) {
@@ -2748,8 +2941,14 @@
var knight = knights[i];
knights.splice(i, 1);
knight.destroy();
}
+ // Clear all mini bosses on screen when upgrade menu is shown
+ for (var i = miniBosses.length - 1; i >= 0; i--) {
+ var miniBoss = miniBosses[i];
+ miniBosses.splice(i, 1);
+ miniBoss.destroy();
+ }
}
// Show upgrade menu again after 35 enemies are killed
if (enemyKillCounter >= 35 && upgradeMenuShown && !secondUpgradeMenuShown) {
secondUpgradeMenuShown = true;
@@ -2976,8 +3175,52 @@
ogre.y = Math.max(-200, Math.min(2732 + 100, ogre.y));
ogre.lastX = ogre.x;
ogres.push(ogre);
}
+ // Spawn mini boss after 35 enemies killed (only once)
+ if (enemyKillCounter === 35) {
+ var miniBoss = game.addChild(new MiniBoss());
+ // Spawn mini boss at center top edge for dramatic entrance
+ miniBoss.x = 2048 / 2;
+ miniBoss.y = -200;
+ miniBoss.lastX = miniBoss.x;
+ // Store path information (center path)
+ miniBoss.pathIndex = 0;
+ miniBoss.pathAngle = pathAngles[0];
+ miniBosses.push(miniBoss);
+ // Visual and audio feedback for mini boss spawn
+ LK.effects.flashScreen(0x8B0000, 1000); // Dark red flash
+ LK.getSound('painSound').play(); // Ominous sound
+ // Screen shake for mini boss entrance
+ var shakeIntensity = 40;
+ var originalX = game.x;
+ var originalY = game.y;
+ tween(game, {
+ x: originalX + shakeIntensity,
+ y: originalY + shakeIntensity * 0.5
+ }, {
+ duration: 150,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ tween(game, {
+ x: originalX - shakeIntensity * 0.7,
+ y: originalY - shakeIntensity * 0.3
+ }, {
+ duration: 120,
+ easing: tween.easeInOut,
+ onFinish: function onFinish() {
+ tween(game, {
+ x: originalX,
+ y: originalY
+ }, {
+ duration: 100,
+ easing: tween.easeIn
+ });
+ }
+ });
+ }
+ });
+ }
// 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());
@@ -3096,33 +3339,33 @@
}
// Update last intersecting state
knight.lastIntersecting = currentKnightIntersecting;
}
- // Update spell charging visual effects
- if (spellCharging && chargeVisualEffect) {
- var chargeDuration = LK.ticks - chargeStartTime;
- var chargeLevel = Math.min(chargeDuration / maxChargeTime, 1.0);
- // Scale charge effect based on charge level
- var chargeScale = 1.0 + chargeLevel * 3.0; // Scale from 1.0 to 4.0
- chargeVisualEffect.scaleX = chargeScale;
- chargeVisualEffect.scaleY = chargeScale;
- // Change color based on charge level
- if (chargeLevel < 0.3) {
- chargeVisualEffect.tint = 0xff69b4; // Pink for low charge
- chargeVisualEffect.alpha = 0.3 + chargeLevel * 0.3;
- } else if (chargeLevel < 0.7) {
- chargeVisualEffect.tint = 0x44aaff; // Blue for medium charge
- chargeVisualEffect.alpha = 0.4 + chargeLevel * 0.4;
- } else {
- chargeVisualEffect.tint = 0xffd700; // Gold for high charge
- chargeVisualEffect.alpha = 0.6 + chargeLevel * 0.4;
+ // Check mini boss-wizard collisions and cleanup off-screen mini bosses
+ for (var i = miniBosses.length - 1; i >= 0; i--) {
+ var miniBoss = miniBosses[i];
+ // Remove mini bosses that went off-screen at bottom
+ if (miniBoss.y > 2732 + 100) {
+ miniBosses.splice(i, 1);
+ miniBoss.destroy();
+ continue;
}
- // Add pulsing effect when fully charged
- if (chargeLevel >= 1.0) {
- var pulse = 1 + Math.sin(LK.ticks * 0.3) * 0.3;
- chargeVisualEffect.scaleX = chargeScale * pulse;
- chargeVisualEffect.scaleY = chargeScale * pulse;
+ // Initialize lastIntersecting if not set
+ if (miniBoss.lastIntersecting === undefined) {
+ miniBoss.lastIntersecting = false;
}
+ // Check for collision transition
+ var currentMiniBossIntersecting = miniBoss.intersects(wizard);
+ if (!miniBoss.lastIntersecting && currentMiniBossIntersecting) {
+ // Damage wizard when mini boss touches for the first time
+ wizard.takeDamage(80); // Mini bosses deal massive damage
+ // Remove mini boss after dealing damage
+ miniBosses.splice(i, 1);
+ miniBoss.destroy();
+ continue;
+ }
+ // Update last intersecting state
+ miniBoss.lastIntersecting = currentMiniBossIntersecting;
}
// Make tap text pulse
var pulse = 1 + Math.sin(LK.ticks * 0.1) * 0.2;
tapText.scale.set(pulse, pulse);