User prompt
ahora haz que las cartas puedan usarse presionando las mismas
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'createEnemyConfig')' in or related to this line: 'self.typeConfig = GAME_CONFIG.createEnemyConfig(self.enemyType, config || {});' Line Number: 957
User prompt
implementa la 2.3 ahora
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'length')' in or related to this line: 'for (var i = ProjectileFactory.activeProjectiles.length - 1; i >= 0; i--) {' Line Number: 6159
User prompt
Please fix the bug: 'ReferenceError: _typeof4 is not defined' in or related to this line: 'var threshold = _typeof4(gameConfigThreshold) === 'object' ? gameConfigThreshold[difficulty] : gameConfigThreshold;' Line Number: 5786
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'NORMAL')' in or related to this line: 'var base = GAME_CONFIG.enemies.skeleton.spawnInterval[difficulty];' Line Number: 5791
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'scaleX')' in or related to this line: 'manaBarBg = LK.getAsset('manaBarBg', {' Line Number: 5223
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'scaleX')' in or related to this line: 'manaBarBg = LK.getAsset('manaBarBg', {' Line Number: 5223
User prompt
Ahora implementa 2.1
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'skeleton')' in or related to this line: 'var base = GAME_CONFIG.enemies.skeleton.spawnInterval[difficulty];' Line Number: 5837
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'scaleX')' in or related to this line: 'manaBarBg = LK.getAsset('manaBarBg', {' Line Number: 5261
User prompt
implementa el 1.2
User prompt
implementa el 1.1
User prompt
hazme una lista con estos puntos
User prompt
haz que el critico que hizo la ultima resena haga una nueva con el codigo actual
User prompt
haz la 1.4
User prompt
entonces empieza con la 1.3
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'processOrbEnemyCollisions')' in or related to this line: 'self.processOrbEnemyCollisions = function () {' Line Number: 6199
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'processOrbEnemyCollisions')' in or related to this line: 'self.processOrbEnemyCollisions = function () {' Line Number: 6199
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'processOrbEnemyCollisions')' in or related to this line: 'self.processOrbEnemyCollisions = function () {' Line Number: 6199
User prompt
ahora pon la 1.2
User prompt
me gustaria empezar por la 1.1
User prompt
como lo puedo arreglar?
User prompt
1. **Cada proyectil se elimine del array cuando se destruye** 2. **Los proyectiles fuera de pantalla se detecten y eliminen correctamente** 3. **Los proyectiles que pierden su objetivo se eliminen automáticamente** 4. **El sistema de pool y los arrays se mantengan sincronizados**
User prompt
haz que se eliminen los proyectiles cuando golpean objetivos
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Intelligent Animation Manager with frame skipping optimization for performance enhancement
var AnimationManager = Container.expand(function () {
var self = Container.call(this);
// Performance monitoring for intelligent frame skipping
self.performanceMetrics = {
frameTime: 0,
avgFrameTime: 16.67,
// Target 60fps = 16.67ms per frame
frameTimeHistory: [],
maxHistorySize: 60,
// Track last 60 frames
lowPerformanceThreshold: 25,
// Skip frames if above 25ms (40fps)
criticalPerformanceThreshold: 33 // Aggressive skipping above 33ms (30fps)
};
// Frame skipping configuration
self.skipConfig = {
enabled: true,
skipLevel: 0,
// 0=none, 1=moderate, 2=aggressive
lastSkipUpdate: 0,
skipUpdateInterval: 30,
// Update skip level every 30 frames
entitySkipPatterns: {
enemies: {
moderate: 2,
aggressive: 3
},
// Skip every 2nd or 3rd frame
projectiles: {
moderate: 1,
aggressive: 2
},
effects: {
moderate: 3,
aggressive: 4
},
ui: {
moderate: 1,
aggressive: 1
} // UI rarely skips
}
};
// Optimized animation state tracking
self.animatedEntities = [];
self.entityGroups = {
enemies: [],
projectiles: [],
effects: [],
ui: []
};
// Performance analysis and frame skip decision
self.analyzePerformance = function () {
var currentTime = performance ? performance.now() : Date.now();
if (self.performanceMetrics.lastFrameTime) {
var frameTime = currentTime - self.performanceMetrics.lastFrameTime;
self.performanceMetrics.frameTimeHistory.push(frameTime);
// Maintain history size
if (self.performanceMetrics.frameTimeHistory.length > self.performanceMetrics.maxHistorySize) {
self.performanceMetrics.frameTimeHistory.shift();
}
// Calculate rolling average
var sum = 0;
for (var i = 0; i < self.performanceMetrics.frameTimeHistory.length; i++) {
sum += self.performanceMetrics.frameTimeHistory[i];
}
self.performanceMetrics.avgFrameTime = sum / self.performanceMetrics.frameTimeHistory.length;
}
self.performanceMetrics.lastFrameTime = currentTime;
// Update skip level based on performance
if (LK.ticks - self.skipConfig.lastSkipUpdate > self.skipConfig.skipUpdateInterval) {
self.updateSkipLevel();
self.skipConfig.lastSkipUpdate = LK.ticks;
}
};
// Intelligent skip level adjustment
self.updateSkipLevel = function () {
var avgTime = self.performanceMetrics.avgFrameTime;
var newSkipLevel = 0;
if (avgTime > self.performanceMetrics.criticalPerformanceThreshold) {
newSkipLevel = 2; // Aggressive skipping
} else if (avgTime > self.performanceMetrics.lowPerformanceThreshold) {
newSkipLevel = 1; // Moderate skipping
}
// Smooth transitions to avoid jerky animation changes
if (newSkipLevel > self.skipConfig.skipLevel) {
self.skipConfig.skipLevel = newSkipLevel;
} else if (newSkipLevel < self.skipConfig.skipLevel) {
// Gradually reduce skip level to ensure stability
self.skipConfig.skipLevel = Math.max(0, self.skipConfig.skipLevel - 1);
}
};
// Register entity for optimized animation management
self.registerEntity = function (entity, type, animationCallback) {
if (!entity || typeof animationCallback !== 'function') return;
var animationData = {
entity: entity,
type: type || 'effects',
callback: animationCallback,
lastUpdate: LK.ticks,
skipCounter: 0,
priority: self.getAnimationPriority(type)
};
self.animatedEntities.push(animationData);
// Group by type for efficient processing
if (!self.entityGroups[type]) {
self.entityGroups[type] = [];
}
self.entityGroups[type].push(animationData);
return animationData;
};
// Unregister entity from animation management
self.unregisterEntity = function (entity) {
// Remove from main list
for (var i = self.animatedEntities.length - 1; i >= 0; i--) {
if (self.animatedEntities[i].entity === entity) {
self.animatedEntities.splice(i, 1);
break;
}
}
// Remove from type groups
for (var type in self.entityGroups) {
var group = self.entityGroups[type];
for (var i = group.length - 1; i >= 0; i--) {
if (group[i].entity === entity) {
group.splice(i, 1);
break;
}
}
}
};
// Determine animation priority based on type and game state
self.getAnimationPriority = function (type) {
var priorities = {
ui: 1,
// Highest priority - always animate
projectiles: 2,
// High priority - important for gameplay
enemies: 3,
// Medium priority - can skip occasionally
effects: 4 // Lower priority - can skip more frequently
};
return priorities[type] || 4;
};
// Intelligent frame skipping decision
self.shouldSkipAnimation = function (animationData) {
if (!self.skipConfig.enabled || self.skipConfig.skipLevel === 0) {
return false;
}
var type = animationData.type;
var skipPattern = self.skipConfig.entitySkipPatterns[type];
if (!skipPattern) return false;
var skipInterval = self.skipConfig.skipLevel === 1 ? skipPattern.moderate : skipPattern.aggressive;
animationData.skipCounter++;
// Skip based on pattern and priority
if (animationData.skipCounter >= skipInterval) {
animationData.skipCounter = 0;
return true;
}
return false;
};
// Optimized animation update cycle with enhanced tween integration
self.updateAnimations = function () {
// Performance analysis first
self.analyzePerformance();
// Process animations by priority groups
var typeOrder = ['ui', 'projectiles', 'enemies', 'effects'];
for (var t = 0; t < typeOrder.length; t++) {
var type = typeOrder[t];
var group = self.entityGroups[type];
if (!group) continue;
for (var i = group.length - 1; i >= 0; i--) {
var animData = group[i];
// Clean up destroyed entities
if (!animData.entity || !animData.entity.parent) {
self.unregisterEntity(animData.entity);
continue;
}
// Apply intelligent frame skipping
if (self.shouldSkipAnimation(animData)) {
continue; // Skip this frame
}
// Enhanced animation with tween-based smooth transitions
try {
// Apply performance-based tween timing adjustments
if (self.skipConfig.skipLevel > 0) {
// Slow down tweens during performance issues for smoother appearance
var tweenSpeedMultiplier = 1 + self.skipConfig.skipLevel * 0.3;
if (animData.entity._activeTweens) {
for (var tweenIdx = 0; tweenIdx < animData.entity._activeTweens.length; tweenIdx++) {
var activeTween = animData.entity._activeTweens[tweenIdx];
if (activeTween && activeTween.duration) {
activeTween.duration *= tweenSpeedMultiplier;
}
}
}
}
// Execute animation callback with tween enhancement
animData.callback.call(animData.entity);
animData.lastUpdate = LK.ticks;
// Add micro-tweens for smooth property transitions during low performance
if (self.skipConfig.skipLevel > 1 && animData.type === 'enemies') {
self.applyPerformanceTweens(animData.entity);
}
} catch (error) {
// Remove problematic animations to prevent crashes
console.error('Animation error:', error);
self.unregisterEntity(animData.entity);
}
}
}
};
// Enhanced animation wrapper for existing entities
self.optimizeEntityAnimation = function (entity, originalUpdateFn) {
if (!entity || typeof originalUpdateFn !== 'function') return;
// Store original update function
entity._originalUpdate = originalUpdateFn;
// Create optimized update wrapper
entity.update = function () {
// Let AnimationManager handle the timing
// The original update will be called via registerEntity callback
};
// Register with animation manager
var entityType = 'effects'; // Default type
if (entity.enemyType) entityType = 'enemies';else if (entity.type && entity.direction) entityType = 'projectiles';else if (entity.setText) entityType = 'ui';
self.registerEntity(entity, entityType, originalUpdateFn);
};
// Batch animation processing for similar entities
self.processBatchAnimations = function (entities, animationType) {
if (!entities || entities.length === 0) return;
var shouldProcess = true;
// Apply batch-level frame skipping for large groups
if (entities.length > 10 && self.skipConfig.skipLevel > 0) {
var batchSkipChance = self.skipConfig.skipLevel === 2 ? 0.3 : 0.15;
shouldProcess = Math.random() > batchSkipChance;
}
if (!shouldProcess) return;
// Process entities with staggered updates to spread CPU load
var processCount = Math.min(entities.length, self.skipConfig.skipLevel === 2 ? 5 : 8);
for (var i = 0; i < processCount; i++) {
var entity = entities[(LK.ticks + i) % entities.length];
if (entity && entity.update && typeof entity.update === 'function') {
entity.update();
}
}
};
// Apply performance-optimized micro-tweens for smooth animations during frame skipping
self.applyPerformanceTweens = function (entity) {
if (!entity || entity.x === undefined) return;
// Store last position if not exists
if (!entity._lastSmoothPos) {
entity._lastSmoothPos = {
x: entity.x,
y: entity.y,
rotation: entity.rotation || 0
};
return;
}
// Calculate movement delta
var deltaX = entity.x - entity._lastSmoothPos.x;
var deltaY = entity.y - entity._lastSmoothPos.y;
var deltaRot = (entity.rotation || 0) - entity._lastSmoothPos.rotation;
// Apply micro-tween only if significant movement detected
if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2 || Math.abs(deltaRot) > 0.1) {
// Calculate performance-based smoothing factor
var performanceSmoothing = self.calculatePerformanceSmoothingFactor();
var adaptiveDuration = Math.floor(performanceSmoothing.duration);
// Create smooth transition tween with performance-adjusted parameters
tween(entity, {
x: entity.x,
y: entity.y,
rotation: entity.rotation || 0
}, {
duration: adaptiveDuration,
easing: performanceSmoothing.easing
});
}
// Update stored position
entity._lastSmoothPos.x = entity.x;
entity._lastSmoothPos.y = entity.y;
entity._lastSmoothPos.rotation = entity.rotation || 0;
};
// Calculate performance-based smoothing parameters for tween optimization
self.calculatePerformanceSmoothingFactor = function () {
var avgFrameTime = self.performanceMetrics.avgFrameTime;
var skipLevel = self.skipConfig.skipLevel;
// Base smoothing configuration
var smoothingConfig = {
duration: 100,
easing: tween.easeOut
};
// Adjust based on performance metrics
if (avgFrameTime > self.performanceMetrics.criticalPerformanceThreshold) {
// Critical performance: minimal tweening
smoothingConfig.duration = 50;
smoothingConfig.easing = tween.linear;
} else if (avgFrameTime > self.performanceMetrics.lowPerformanceThreshold) {
// Low performance: reduced smoothing
smoothingConfig.duration = 75;
smoothingConfig.easing = tween.easeOut;
} else if (skipLevel === 0) {
// High performance: enhanced smoothing
smoothingConfig.duration = 150;
smoothingConfig.easing = tween.easeInOut;
}
return smoothingConfig;
};
// Get performance statistics for debugging
self.getPerformanceStats = function () {
return {
avgFrameTime: Math.round(self.performanceMetrics.avgFrameTime * 100) / 100,
skipLevel: self.skipConfig.skipLevel,
entityCount: self.animatedEntities.length,
groupCounts: {
enemies: self.entityGroups.enemies ? self.entityGroups.enemies.length : 0,
projectiles: self.entityGroups.projectiles ? self.entityGroups.projectiles.length : 0,
effects: self.entityGroups.effects ? self.entityGroups.effects.length : 0,
ui: self.entityGroups.ui ? self.entityGroups.ui.length : 0
}
};
};
return self;
});
var Coin = Container.expand(function () {
var self = Container.call(this);
var coinGraphics = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5
});
self.bobOffset = Math.random() * Math.PI * 2;
self.initialY = 0;
self.update = function () {
if (self.initialY === 0) {
self.initialY = self.y;
}
// Only do bobbing animation and collection if not animating to coin counter
if (!self.isAnimating) {
// Bobbing animation
self.y = self.initialY + Math.sin(LK.ticks * 0.1 + self.bobOffset) * 10;
// Check collection by wizard
if (wizard && self.intersects(wizard)) {
self.collect();
}
}
};
self.collect = function () {
LK.getSound('coinCollect').play();
LK.setScore(LK.getScore() + 5);
coinCounter++;
coinText.setText('Coins: ' + coinCounter);
// Remove from coins array
for (var i = coins.length - 1; i >= 0; i--) {
if (coins[i] === self) {
coins.splice(i, 1);
break;
}
}
// Return to pool instead of destroying
if (self.pooled && globalObjectPool) {
globalObjectPool.returnObject(self);
} else {
self.destroy();
}
};
return self;
});
// Unified visual effects manager for streamlined and efficient effects system
var EffectManager = Container.expand(function () {
var self = Container.call(this);
// Effect configuration templates for consistent visual effects
self.effectTemplates = {
damage: {
asset: 'projectileGlow',
color: 0xFF0000,
duration: 50
},
impact: {
asset: 'projectileGlow',
color: 0xFFAA00,
duration: 450,
scale: 1.5
},
explosion: {
asset: 'projectileGlow',
color: 0xFF6600,
duration: 600,
scale: 4.0
},
heal: {
asset: 'energySphere',
color: 0x00FF00,
duration: 500,
scale: 2.0
},
shield: {
asset: 'shield',
color: 0x0080FF,
duration: 300,
scale: 3.0
},
lightning: {
asset: 'spell',
color: 0x00FFFF,
duration: 500,
scale: 2.0
}
};
// Unified damage application with streamlined visual feedback
self.applyDamage = function (target, amount, damageType) {
if (!target || target.health === undefined) return false;
target.health -= amount;
target.animationState = 'attacking';
// Create streamlined damage effects using templates
self.createEffect('damage', target, {
amount: amount
});
self.createDamageText(target.x, target.y, amount, target.typeConfig ? target.typeConfig.damageTextColor : 0xFF4444);
// Return to walking state after brief animation
tween({}, {}, {
duration: 300,
onFinish: function onFinish() {
if (target.animationState === 'attacking') target.animationState = 'walking';
}
});
return true;
};
// Streamlined effect creation using pooled objects with enhanced tween animations
self.createEffect = function (effectType, target, options) {
var template = self.effectTemplates[effectType];
if (!template) return;
// Get effect from pool with proper initialization
var effect = globalObjectPool.getObject('effect', {
x: target.x,
y: target.y,
scaleX: 0.3,
scaleY: 0.3,
anchorX: 0.5,
anchorY: 0.5,
alpha: 1.0,
rotation: 0,
visible: true
});
if (!effect) {
// Fallback to direct creation if pool fails
effect = game.addChild(LK.getAsset(template.asset, {
anchorX: 0.5,
anchorY: 0.5,
x: target.x,
y: target.y,
scaleX: 0.3,
scaleY: 0.3
}));
} else {
game.addChild(effect);
}
effect.tint = options.color || template.color;
effect.alpha = 0.8;
var targetScale = options.scale || template.scale || 1.5; // Provide default scale value
// Enhanced tween animation with multiple sequential effects
tween(effect, {
scaleX: targetScale * 0.7,
scaleY: targetScale * 0.7,
alpha: 1.0,
rotation: Math.PI * 0.25
}, {
duration: template.duration * 0.3,
easing: tween.easeOut,
onFinish: function onFinish() {
// Second phase: expand and fade
tween(effect, {
scaleX: targetScale,
scaleY: targetScale,
alpha: 0.6,
rotation: Math.PI * 0.5
}, {
duration: template.duration * 0.4,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Final phase: rapid fade with slight shrink
tween(effect, {
scaleX: targetScale * 1.2,
scaleY: targetScale * 1.2,
alpha: 0,
rotation: Math.PI
}, {
duration: template.duration * 0.3,
easing: tween.easeIn,
onFinish: function onFinish() {
// Return to pool instead of destroying
if (effect.pooled) {
globalObjectPool.returnObject(effect);
} else {
effect.destroy();
}
}
});
}
});
}
});
};
// Efficient damage text with object pooling for optimal memory usage
self.createDamageText = function (x, y, damage, color) {
// Get damage text from pool
var damageText = globalObjectPool.getObject('damageText', {
x: x + (Math.random() - 0.5) * 60,
y: y - 40
});
if (!damageText) {
// Fallback to direct creation if pool fails
damageText = new Text2('-' + damage, {
size: 120,
fill: color || 0xFF4444,
font: "monospace"
});
damageText.x = x + (Math.random() - 0.5) * 60;
damageText.y = y - 40;
} else {
// Configure pooled text - ensure style object exists
if (!damageText.style) {
damageText.style = {
size: 120,
fill: color || 0xFF4444,
font: "monospace"
};
} else {
damageText.style.fill = color || 0xFF4444;
}
}
damageText.setText('-' + damage);
damageText.anchor.set(0.5, 0.5);
game.addChild(damageText);
var startY = damageText.y;
tween(damageText, {
y: startY - 120,
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
// Return to pool instead of destroying
if (damageText.pooled) {
globalObjectPool.returnObject(damageText);
} else {
damageText.destroy();
}
}
});
};
// Streamlined flash effect wrapper
self.createFlashEffect = function (target, color, duration) {
LK.effects.flashObject(target, color, duration);
};
// Unified visual effect dispatcher with enhanced tween-based effects
self.createVisualEffect = function (type, target, config) {
var options = config || {};
// Calculate performance-based effect scaling
var performanceScale = self.calculateEffectPerformanceScale();
switch (type) {
case 'impact':
self.createEnhancedImpactEffect(target, options, performanceScale);
break;
case 'explosion':
self.createEnhancedExplosionEffect(target, options, performanceScale);
break;
case 'heal':
self.createEnhancedHealEffect(target, options, performanceScale);
break;
case 'shield':
self.createEnhancedShieldEffect(target, options, performanceScale);
break;
case 'lightning':
self.createEnhancedLightningEffect(target, options, performanceScale);
break;
default:
// Fallback to original effect creation
self.createEffect(type, target, options);
break;
}
};
// Calculate performance-based effect scaling for optimal visual quality
self.calculateEffectPerformanceScale = function () {
var skipLevel = globalAnimationManager ? globalAnimationManager.skipConfig.skipLevel : 0;
var avgFrameTime = globalAnimationManager ? globalAnimationManager.performanceMetrics.avgFrameTime : 16.67;
return {
particleCount: skipLevel > 1 ? 0.3 : skipLevel > 0 ? 0.6 : 1.0,
duration: skipLevel > 1 ? 0.5 : skipLevel > 0 ? 0.75 : 1.0,
complexity: avgFrameTime > 25 ? 'simple' : avgFrameTime > 20 ? 'medium' : 'complex'
};
};
// Enhanced impact effect with sophisticated tween sequences
self.createEnhancedImpactEffect = function (target, options, performanceScale) {
var template = self.effectTemplates.impact;
var effect = self.getEffectFromPool('impact', target);
if (effect) {
effect.tint = options.color || template.color;
effect.alpha = 0.1;
// Multi-stage tween sequence for dynamic impact
tween(effect, {
scaleX: template.scale * 0.3,
scaleY: template.scale * 0.3,
alpha: 1.0,
rotation: Math.PI * 0.1
}, {
duration: template.duration * 0.2 * performanceScale.duration,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(effect, {
scaleX: template.scale * 1.2,
scaleY: template.scale * 1.2,
alpha: 0.7,
rotation: Math.PI * 0.3
}, {
duration: template.duration * 0.5 * performanceScale.duration,
easing: tween.bounceOut,
onFinish: function onFinish() {
tween(effect, {
scaleX: template.scale * 0.1,
scaleY: template.scale * 0.1,
alpha: 0,
rotation: Math.PI * 0.5
}, {
duration: template.duration * 0.3 * performanceScale.duration,
easing: tween.easeIn,
onFinish: function onFinish() {
self.returnEffectToPool(effect);
}
});
}
});
}
});
}
};
// Enhanced explosion effect with cascading tween animations
self.createEnhancedExplosionEffect = function (target, options, performanceScale) {
var template = self.effectTemplates.explosion;
var particleCount = Math.floor(6 * performanceScale.particleCount);
for (var i = 0; i < particleCount; i++) {
var particle = self.getEffectFromPool('explosion', target);
if (!particle) continue;
var angle = Math.PI * 2 / particleCount * i;
var distance = template.scale * 30;
particle.tint = options.color || template.color;
particle.alpha = 0.8;
particle.scaleX = 0.2;
particle.scaleY = 0.2;
// Staggered explosion particles with individual tween paths
var delay = i * 50 * performanceScale.duration;
tween({}, {}, {
duration: delay,
onFinish: function (particleRef, angleRef, distanceRef) {
return function () {
tween(particleRef, {
x: target.x + Math.cos(angleRef) * distanceRef,
y: target.y + Math.sin(angleRef) * distanceRef,
scaleX: template.scale * 0.8,
scaleY: template.scale * 0.8,
alpha: 1.0
}, {
duration: template.duration * 0.4 * performanceScale.duration,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(particleRef, {
scaleX: template.scale * 1.5,
scaleY: template.scale * 1.5,
alpha: 0,
rotation: Math.PI
}, {
duration: template.duration * 0.6 * performanceScale.duration,
easing: tween.easeIn,
onFinish: function onFinish() {
self.returnEffectToPool(particleRef);
}
});
}
});
};
}(particle, angle, distance)
});
}
};
// Enhanced heal effect with pulsing tween animations
self.createEnhancedHealEffect = function (target, options, performanceScale) {
var template = self.effectTemplates.heal;
var effect = self.getEffectFromPool('heal', target);
if (effect) {
effect.tint = options.color || template.color;
effect.alpha = 0.3;
effect.scaleX = 0.5;
effect.scaleY = 0.5;
// Pulsing heal animation with multiple cycles
var pulseCount = performanceScale.complexity === 'simple' ? 2 : 4;
self.createPulsingTween(effect, template, pulseCount, performanceScale);
}
};
// Enhanced shield effect with orbital tween movement
self.createEnhancedShieldEffect = function (target, options, performanceScale) {
var template = self.effectTemplates.shield;
var orbCount = performanceScale.complexity === 'simple' ? 3 : 6;
for (var i = 0; i < orbCount; i++) {
var orb = self.getEffectFromPool('shield', target);
if (!orb) continue;
orb.tint = options.color || template.color;
orb.alpha = 0.6;
orb.scaleX = 0.3;
orb.scaleY = 0.3;
var angle = Math.PI * 2 / orbCount * i;
var radius = template.scale * 40;
// Orbital shield animation
self.createOrbitalTween(orb, target, angle, radius, template, performanceScale);
}
};
// Enhanced lightning effect with branching tween paths
self.createEnhancedLightningEffect = function (target, options, performanceScale) {
var template = self.effectTemplates.lightning;
var branchCount = performanceScale.complexity === 'simple' ? 2 : 5;
for (var i = 0; i < branchCount; i++) {
var bolt = self.getEffectFromPool('lightning', target);
if (!bolt) continue;
bolt.tint = options.color || template.color;
bolt.alpha = 1.0;
bolt.scaleX = 0.1;
bolt.scaleY = 2.0;
var angle = (Math.random() - 0.5) * Math.PI * 0.5;
bolt.rotation = angle;
// Lightning flash with rapid tween sequence
tween(bolt, {
scaleX: template.scale * 0.5,
alpha: 0.8
}, {
duration: 100 * performanceScale.duration,
easing: tween.easeOut,
onFinish: function (boltRef) {
return function () {
tween(boltRef, {
scaleX: template.scale * 1.2,
scaleY: template.scale * 3.0,
alpha: 0.3
}, {
duration: 200 * performanceScale.duration,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(boltRef, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 150 * performanceScale.duration,
easing: tween.easeIn,
onFinish: function onFinish() {
self.returnEffectToPool(boltRef);
}
});
}
});
};
}(bolt)
});
}
};
// Helper method for pulsing tween animations
self.createPulsingTween = function (effect, template, pulseCount, performanceScale) {
var currentPulse = 0;
function doPulse() {
if (currentPulse >= pulseCount) {
self.returnEffectToPool(effect);
return;
}
tween(effect, {
scaleX: template.scale * 1.2,
scaleY: template.scale * 1.2,
alpha: 0.8
}, {
duration: template.duration * 0.3 * performanceScale.duration,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(effect, {
scaleX: template.scale * 0.7,
scaleY: template.scale * 0.7,
alpha: 0.4
}, {
duration: template.duration * 0.3 * performanceScale.duration,
easing: tween.easeIn,
onFinish: function onFinish() {
currentPulse++;
doPulse();
}
});
}
});
}
doPulse();
};
// Helper method for orbital tween animations
self.createOrbitalTween = function (orb, target, startAngle, radius, template, performanceScale) {
var currentAngle = startAngle;
var duration = template.duration * performanceScale.duration;
function updateOrbitalPosition() {
orb.x = target.x + Math.cos(currentAngle) * radius;
orb.y = target.y + Math.sin(currentAngle) * radius;
currentAngle += 0.1;
if (duration > 0) {
duration -= 16; // Approximate frame time
tween({}, {}, {
duration: 16,
onFinish: updateOrbitalPosition
});
} else {
self.returnEffectToPool(orb);
}
}
updateOrbitalPosition();
};
// Helper method to get effect from pool
self.getEffectFromPool = function (type, target) {
var effect = globalObjectPool ? globalObjectPool.getObject('effect', {
x: target.x,
y: target.y,
scaleX: 0.3,
scaleY: 0.3,
anchorX: 0.5,
anchorY: 0.5
}) : null;
if (!effect) {
effect = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: target.x,
y: target.y,
scaleX: 0.3,
scaleY: 0.3
}));
} else {
game.addChild(effect);
}
return effect;
};
// Helper method to return effect to pool
self.returnEffectToPool = function (effect) {
if (effect.pooled && globalObjectPool) {
globalObjectPool.returnObject(effect);
} else if (effect.parent) {
effect.destroy();
}
};
return self;
});
// Unified Enemy class - handles all enemy types with streamlined configuration
var Enemy = Container.expand(function (type, config) {
var self = Container.call(this);
// Initialize properties
self.enemyType = type || 'skeleton';
self.currentFrame = 1;
self.animationTimer = 0;
self.animationState = 'walking';
self.frozen = false;
self.frozenTimer = 0;
self.isDying = false;
self.lastX = 0;
self.speedTweenStarted = false;
// Get and apply configuration
self.typeConfig = GAME_CONFIG.createEnemyConfig(self.enemyType, config || {});
self.animationSpeed = self.typeConfig.animationSpeed || 15;
self.health = self.typeConfig.health || self.typeConfig.baseHealth;
self.maxHealth = self.health;
self.speed = self.typeConfig.speed || self.typeConfig.baseSpeed;
// Create animation frames
self.animationFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset(self.typeConfig.assetPrefix + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: self.typeConfig.scale,
scaleY: self.typeConfig.scale
});
frameGraphics.visible = i === 1;
if (self.typeConfig.tint) frameGraphics.tint = self.typeConfig.tint;
self.animationFrames.push(frameGraphics);
}
// Create health bar for mini boss
if (self.enemyType === 'miniBoss') {
self.healthBarBg = game.addChild(LK.getAsset('miniBossHealthBarBg', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.ui.miniBossHealthBar.x,
y: GAME_CONFIG.ui.miniBossHealthBar.y,
scaleX: GAME_CONFIG.ui.miniBossHealthBar.bgScaleX,
scaleY: GAME_CONFIG.ui.miniBossHealthBar.bgScaleY
}));
self.healthBarFg = game.addChild(LK.getAsset('miniBossHealthBar', {
anchorX: 0.0,
anchorY: 0.5,
x: GAME_CONFIG.ui.miniBossHealthBar.x - 200,
y: GAME_CONFIG.ui.miniBossHealthBar.y,
scaleX: GAME_CONFIG.ui.miniBossHealthBar.fgScaleX,
scaleY: GAME_CONFIG.ui.miniBossHealthBar.fgScaleY
}));
self.healthText = new Text2('Boss Health: ' + self.health + '/' + self.maxHealth, {
size: GAME_CONFIG.ui.miniBossHealthBar.textSize,
fill: 0xFFFFFF,
font: "monospace"
});
self.healthText.anchor.set(0.5, 0.5);
self.healthText.x = GAME_CONFIG.ui.miniBossHealthBar.x;
self.healthText.y = GAME_CONFIG.ui.miniBossHealthBar.textY;
game.addChild(self.healthText);
}
// Update health bar for mini boss
self.updateHealthBar = function () {
if (self.enemyType !== 'miniBoss' || !self.healthBarFg) return;
var healthPercent = self.health / self.maxHealth;
self.healthBarFg.scaleX = healthPercent;
self.healthText.setText('Boss Health: ' + self.health + '/' + self.maxHealth);
if (healthPercent > 0.6) {
self.healthBarFg.tint = 0xff0000;
} else if (healthPercent > 0.3) {
self.healthBarFg.tint = 0xff4500;
} else {
self.healthBarFg.tint = 0x8B0000;
}
};
// Optimized animation system with intelligent frame skipping
self.updateAnimation = function () {
self.animationTimer++;
var frameSpeed = self.animationSpeed;
// Apply performance-based frame speed adjustments
if (globalAnimationManager && globalAnimationManager.skipConfig.skipLevel > 0) {
var skipMultiplier = 1 + globalAnimationManager.skipConfig.skipLevel * 0.5;
frameSpeed = Math.floor(frameSpeed * skipMultiplier);
}
if (self.animationState === 'attacking') frameSpeed = Math.floor(frameSpeed * 0.5);else if (self.animationState === 'dying') frameSpeed = Math.floor(frameSpeed * 1.3);else if (self.animationState === 'idle') frameSpeed = Math.floor(frameSpeed * 1.7);
if (self.animationTimer >= frameSpeed) {
self.animationTimer = 0;
self.animationFrames[self.currentFrame - 1].visible = false;
if (self.animationState === 'walking') {
self.currentFrame++;
if (self.currentFrame > 4) self.currentFrame = 1;
} else if (self.animationState === 'attacking') {
self.currentFrame++;
if (self.currentFrame > 4) self.currentFrame = 2;
} else if (self.animationState === 'dying') {
if (self.currentFrame < 4) self.currentFrame++;
} else if (self.animationState === 'idle') {
self.currentFrame = self.currentFrame === 1 ? 2 : 1;
}
self.animationFrames[self.currentFrame - 1].visible = true;
}
};
self.update = function () {
if (tutorial && tutorial.isActive || self.isDying) return;
// Animation and speed progression
self.updateAnimation();
if (!self.speedTweenStarted) {
self.speedTweenStarted = true;
tween(self, {
speed: self.speed * 1.5
}, {
duration: 10000,
easing: tween.easeOut
});
}
// Handle frozen state
if (self.frozen) {
self.frozenTimer--;
if (self.frozenTimer <= 0) self.frozen = false;
return;
}
// Enhanced movement toward wizard with advanced tween smoothing
if (wizard) {
var dx = wizard.x - self.x;
var dy = wizard.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var speedMult = self.timeSlowed ? self.timeSlowAmount : 1.0;
var newX = self.x + dx / distance * self.speed * speedMult;
var newY = self.y + dy / distance * self.speed * speedMult;
// Enhanced tween-based movement system with performance adaptation
var movementConfig = self.calculateMovementTweenConfig();
if (movementConfig.useTween) {
// Advanced movement tween with easing based on performance
tween(self, {
x: newX,
y: newY
}, {
duration: movementConfig.duration,
easing: movementConfig.easing
});
} else {
// Direct assignment during critical performance issues
self.x = newX;
self.y = newY;
}
var flipScale = dx < 0 ? -self.typeConfig.scale : self.typeConfig.scale;
// Enhanced scale transitions with performance-aware tweening
self.applyEnhancedFlipAnimation(flipScale);
} else {
// Enhanced vertical movement fallback with adaptive smoothing
var fallbackConfig = self.calculateFallbackMovementConfig();
if (fallbackConfig.useTween) {
tween(self, {
y: self.y + self.speed
}, {
duration: fallbackConfig.duration,
easing: fallbackConfig.easing
});
} else {
self.y += self.speed;
}
}
}
};
self.takeDamage = function (damage) {
globalDamageHandler.applyDamage(self, damage, 'physical');
if (self.enemyType === 'miniBoss') self.updateHealthBar();
if (self.health <= 0) self.die();
};
self.down = function (x, y, obj) {
if (tutorial && tutorial.isActive || self.isDying) return;
if (wizard && wizard.attackCooldown > 0) {
LK.effects.flashObject(self, 0xFF0000, 200);
return;
}
// Vibration feedback
if (typeof LK.vibrate === 'function') {
if (Array.isArray(self.typeConfig.vibration)) {
LK.vibrate(self.typeConfig.vibration);
} else {
LK.vibrate(self.typeConfig.vibration);
}
}
selectedEnemy = self;
LK.effects.flashObject(self, 0xFFFF00, 500);
if (wizard && projectiles.length < 10) {
var projectile = ProjectileFactory.createBasicAttack(wizard, self);
projectile.targetEnemy = self;
projectiles.push(projectile);
LK.getSound('spellCast').play();
wizard.attackCooldown = 30;
}
};
self.die = function () {
var arrayToUpdate = null;
if (self.enemyType === 'skeleton') arrayToUpdate = enemies;else if (self.enemyType === 'ogre') arrayToUpdate = ogres;else if (self.enemyType === 'knight') arrayToUpdate = knights;else if (self.enemyType === 'miniBoss') arrayToUpdate = miniBosses;
if (globalDeathHandler) {
globalDeathHandler.executeEnemyDeath(self, arrayToUpdate);
}
};
// Calculate optimal movement tween configuration based on performance
self.calculateMovementTweenConfig = function () {
var skipLevel = globalAnimationManager ? globalAnimationManager.skipConfig.skipLevel : 0;
var avgFrameTime = globalAnimationManager ? globalAnimationManager.performanceMetrics.avgFrameTime : 16.67;
// Base configuration for high performance
var config = {
useTween: true,
duration: 16,
easing: tween.linear
};
// Adjust based on performance metrics
if (skipLevel === 0 && avgFrameTime < 20) {
// Excellent performance: enhanced smoothing
config.duration = 16;
config.easing = tween.easeOut;
} else if (skipLevel === 1 || avgFrameTime < 25) {
// Good performance: standard smoothing
config.duration = 20;
config.easing = tween.linear;
} else if (skipLevel === 2 || avgFrameTime < 33) {
// Moderate performance: reduced smoothing
config.duration = 32;
config.easing = tween.linear;
} else {
// Critical performance: disable tweening
config.useTween = false;
}
return config;
};
// Calculate fallback movement configuration for vertical movement
self.calculateFallbackMovementConfig = function () {
var skipLevel = globalAnimationManager ? globalAnimationManager.skipConfig.skipLevel : 0;
return {
useTween: skipLevel === 0,
duration: skipLevel === 0 ? 16 : 32,
easing: skipLevel === 0 ? tween.linear : tween.easeOut
};
};
// Apply enhanced flip animation with performance-aware tweening
self.applyEnhancedFlipAnimation = function (targetScale) {
var flipConfig = self.calculateFlipAnimationConfig();
for (var frameIdx = 0; frameIdx < self.animationFrames.length; frameIdx++) {
var frame = self.animationFrames[frameIdx];
var currentScale = frame.scaleX;
if (Math.abs(currentScale - targetScale) > 0.1) {
if (flipConfig.useTween) {
// Enhanced flip animation with bounce effect for high performance
tween(frame, {
scaleX: targetScale * 1.1
}, {
duration: flipConfig.duration * 0.3,
easing: tween.easeOut,
onFinish: function (frameRef, finalScale) {
return function () {
tween(frameRef, {
scaleX: finalScale
}, {
duration: flipConfig.duration * 0.7,
easing: tween.bounceOut
});
};
}(frame, targetScale)
});
} else {
// Direct assignment for low performance
frame.scaleX = targetScale;
}
} else {
frame.scaleX = targetScale;
}
}
};
// Calculate flip animation configuration based on performance
self.calculateFlipAnimationConfig = function () {
var skipLevel = globalAnimationManager ? globalAnimationManager.skipConfig.skipLevel : 0;
var avgFrameTime = globalAnimationManager ? globalAnimationManager.performanceMetrics.avgFrameTime : 16.67;
// Enhanced flip animations for good performance
if (skipLevel === 0 && avgFrameTime < 20) {
return {
useTween: true,
duration: 300,
useEnhancedEffects: true
};
}
// Standard flip animations for moderate performance
if (skipLevel <= 1 && avgFrameTime < 30) {
return {
useTween: true,
duration: 200,
useEnhancedEffects: false
};
}
// Minimal animations for low performance
return {
useTween: false,
duration: 0,
useEnhancedEffects: false
};
};
return self;
});
// Unified EnemyManager for streamlined enemy lifecycle management
var EnemyManager = Container.expand(function () {
var self = Container.call(this);
// Consolidated enemy arrays for efficient management
self.enemyCollections = {
skeleton: [],
ogre: [],
knight: [],
miniBoss: []
};
// Unified enemy configuration templates
self.enemyTemplates = {
skeleton: {
baseHealth: 100,
baseSpeed: 3,
damage: 20,
coinReward: 1
},
ogre: {
baseHealth: 200,
baseSpeed: 2.5,
damage: 30,
coinReward: 1
},
knight: {
baseHealth: 300,
baseSpeed: 2,
damage: 40,
coinReward: 1
},
miniBoss: {
baseHealth: 3000,
baseSpeed: 4,
damage: 75,
coinReward: 5
}
};
// Streamlined enemy creation with unified configuration
self.createEnemy = function (type, difficulty, level, pathOverride) {
var template = self.enemyTemplates[type];
if (!template) return null;
var config = GAME_CONFIG.createEnemyConfig(type, {
health: template.baseHealth,
speed: template.baseSpeed * (1 + level * 0.3)
});
var enemy = new Enemy(type, config);
enemy.pathIndex = pathOverride || self.selectOptimalPath(type);
self.positionEnemy(enemy, type);
self.applyDifficultyModifications(enemy, type, difficulty);
return enemy;
};
// Unified enemy positioning system
self.positionEnemy = function (enemy, type) {
var spawnPos = GAME_CONFIG.paths.spawnPositions[enemy.pathIndex];
if (spawnPos) {
enemy.x = Math.max(50, Math.min(1998, spawnPos.x));
enemy.y = type === 'miniBoss' ? -200 : Math.max(-200, Math.min(2732 + 100, spawnPos.y));
enemy.lastX = enemy.x;
}
};
// Streamlined difficulty modifications
self.applyDifficultyModifications = function (enemy, type, difficulty) {
if (type === 'miniBoss') {
enemy.updateHealthBar();
LK.effects.flashScreen(0x8B0000, 1000);
} else if (type === 'skeleton' && Math.random() < 0.3) {
LK.getSound('enemyGrowl').play();
}
// Elite enemy system for hard difficulty
if (difficulty === 'DIFICIL' && enemyKillCounter >= 20 && Math.random() < 0.15) {
self.createEliteEnemy(enemy, type);
}
};
// Unified elite enemy creation
self.createEliteEnemy = function (enemy, type) {
var eliteMultipliers = {
health: 1.8,
speed: 1.3,
color: 0xFF6600
};
if (type === 'ogre') eliteMultipliers.color = 0xFF0000;
if (type === 'knight') eliteMultipliers.color = 0xFFD700;
if (type === 'miniBoss') eliteMultipliers.color = 0x8B0000;
enemy.health *= eliteMultipliers.health;
enemy.speed *= eliteMultipliers.speed;
enemy.maxHealth = enemy.health;
enemy.isElite = true;
for (var i = 0; i < enemy.animationFrames.length; i++) {
enemy.animationFrames[i].tint = eliteMultipliers.color;
}
};
// Optimized path selection system
self.selectOptimalPath = function (type) {
if (type === 'skeleton' && enemyKillCounter < 5) return 0;
if (type === 'miniBoss') return 0;
var available = [];
for (var i = 0; i < 5; i++) {
if (pathConsecutiveSpawns[i] < 2) available.push(i);
}
if (available.length === 0) {
for (var i = 0; i < 5; i++) pathConsecutiveSpawns[i] = 0;
available = [0, 1, 2, 3, 4];
}
return available[Math.floor(Math.random() * available.length)];
};
// Unified enemy collection management
self.addToCollection = function (enemy, type) {
if (self.enemyCollections[type]) {
self.enemyCollections[type].push(enemy);
}
};
// Streamlined enemy removal system
self.removeFromCollection = function (enemy, type) {
var collection = self.enemyCollections[type];
if (collection) {
for (var i = collection.length - 1; i >= 0; i--) {
if (collection[i] === enemy) {
collection.splice(i, 1);
break;
}
}
}
};
// Get all enemies as unified array
self.getAllEnemies = function () {
var allEnemies = [];
for (var type in self.enemyCollections) {
allEnemies = allEnemies.concat(self.enemyCollections[type]);
}
return allEnemies;
};
return self;
});
var EnergyOrb = Container.expand(function () {
var self = Container.call(this);
// Create energy sphere visual
var sphereGraphics = self.attachAsset('energySphere', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
// Add glowing effect
sphereGraphics.alpha = 0.8;
self.attackTimer = 0;
self.attackInterval = 180; // 3 seconds at 60fps
self.orbitalAngle = 0;
self.orbitalRadius = 120;
self.update = function () {
// Pause energy orb when tutorial is active
if (tutorial && tutorial.isActive) {
return;
}
// Upgrade menu removed - no pause needed
// Keep sphere at wizard's position (stationary relative to wizard)
if (wizard) {
self.x = wizard.x + 140; // Position further to the right side of wizard
self.y = wizard.y - 20; // Position slightly lower relative to wizard
}
// Pulsing glow effect
var pulse = 1 + Math.sin(LK.ticks * 0.2) * 0.3;
sphereGraphics.scaleX = 1.5 * pulse;
sphereGraphics.scaleY = 1.5 * pulse;
// Attack timer - keep original interval regardless of upgrades
self.attackTimer++;
if (self.attackTimer >= 180) {
// Fixed at 3 seconds
self.attackTimer = 0;
self.attackClosestEnemy();
}
};
self.attackClosestEnemy = function () {
var closestEnemy = null;
var closestDistance = Infinity;
// Check all enemy types for closest one
var allEnemies = collisionArrayPool.getAllEnemies();
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// Attack closest enemy if found
if (closestEnemy) {
// Create energy beam projectile using unified factory
var energyBeam = ProjectileFactory.createProjectile('energyBeam', self.x, self.y, closestEnemy.x, closestEnemy.y, {
targetEnemy: closestEnemy
});
// Flash effect on sphere when attacking
tween(sphereGraphics, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 1.0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(sphereGraphics, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0.8
}, {
duration: 200,
easing: tween.easeIn
});
}
});
LK.getSound('spellCast').play();
}
};
return self;
});
var GameMenu = Container.expand(function () {
var self = Container.call(this);
// Menu background image instead of cave background
var menuBg = self.attachAsset('menuBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 25.0,
scaleY: 35.0
});
menuBg.alpha = 1.0;
// Title text
var titleText = new Text2('WIZARD DEFENDER', {
size: 150,
fill: 0xFFD700,
font: "monospace"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 800;
self.addChild(titleText);
// Instructions text
var instructionsText = new Text2('TAP ENEMIES TO ATTACK\nDEFEND YOUR CASTLE!', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
instructionsText.anchor.set(0.5, 0.5);
instructionsText.x = 2048 / 2;
instructionsText.y = 1200;
self.addChild(instructionsText);
// Start button
var startButton = self.attachAsset('wizard1', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1500,
scaleX: 2,
scaleY: 2
});
var startButtonText = new Text2('START GAME', {
size: 100,
fill: 0x000000,
font: "monospace"
});
startButtonText.anchor.set(0.5, 0.5);
startButtonText.x = 2048 / 2;
startButtonText.y = 1600;
self.addChild(startButtonText);
// Configuration button
var configButton = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1800,
scaleX: 4,
scaleY: 2
});
configButton.tint = 0x4169E1;
var configButtonText = new Text2('CONFIGURACION', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
configButtonText.anchor.set(0.5, 0.5);
configButtonText.x = 2048 / 2;
configButtonText.y = 1800;
self.addChild(configButtonText);
// Shop button
var shopButton = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1950,
scaleX: 4,
scaleY: 2
});
shopButton.tint = 0xFF6B35;
var shopButtonText = new Text2('TIENDA', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
shopButtonText.anchor.set(0.5, 0.5);
shopButtonText.x = 2048 / 2;
shopButtonText.y = 1950;
self.addChild(shopButtonText);
// Deck button
var deckButton = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2150,
scaleX: 4,
scaleY: 2
});
deckButton.tint = 0x8A2BE2;
var deckButtonText = new Text2('DECK HECHIZOS', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
deckButtonText.anchor.set(0.5, 0.5);
deckButtonText.x = 2048 / 2;
deckButtonText.y = 2150;
self.addChild(deckButtonText);
// Tutorial button (only show if tutorial was completed before)
if (storage.tutorialCompleted) {
var tutorialButton = self.attachAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2350,
scaleX: 4,
scaleY: 2
});
tutorialButton.tint = 0x2E8B57;
var tutorialButtonText = new Text2('TUTORIAL', {
size: 80,
fill: 0xFFFFFF,
font: "monospace"
});
tutorialButtonText.anchor.set(0.5, 0.5);
tutorialButtonText.x = 2048 / 2;
tutorialButtonText.y = 2350;
self.addChild(tutorialButtonText);
}
// Button interaction
self.down = function (x, y, obj) {
// Handle configuration menu interactions
if (self.configMode) {
// Music volume adjustment
if (y >= 1150 && y <= 1250) {
var currentMusicVolume = storage.musicVolume || 0.7;
var newMusicVolume = Math.min(1.0, currentMusicVolume + 0.1);
if (newMusicVolume > 1.0) newMusicVolume = 0.0;
storage.musicVolume = newMusicVolume;
self.musicVolumeText.setText('VOLUMEN MUSICA: ' + Math.round(newMusicVolume * 100) + '%');
return;
}
// Sound volume adjustment
if (y >= 1350 && y <= 1450) {
var currentSoundVolume = storage.soundVolume || 1.0;
var newSoundVolume = Math.min(1.0, currentSoundVolume + 0.1);
if (newSoundVolume > 1.0) newSoundVolume = 0.0;
storage.soundVolume = newSoundVolume;
self.soundVolumeText.setText('VOLUMEN SONIDO: ' + Math.round(newSoundVolume * 100) + '%');
return;
}
// Difficulty adjustment
if (y >= 1550 && y <= 1650) {
var currentDifficulty = storage.difficulty || 'NORMAL';
var difficulties = ['FACIL', 'NORMAL', 'DIFICIL'];
var currentIndex = difficulties.indexOf(currentDifficulty);
var newIndex = (currentIndex + 1) % difficulties.length;
var newDifficulty = difficulties[newIndex];
storage.difficulty = newDifficulty;
self.difficultyText.setText('DIFICULTAD: ' + newDifficulty);
return;
}
// Back button
if (y >= 1950 && y <= 2050) {
self.hideConfigMenu();
return;
}
// Block all other interactions when config menu is active
return;
}
// Handle deck menu interactions
if (self.deckMode) {
// Check deck card clicks with better hit detection
for (var i = 0; i < self.deckElements.length; i++) {
var element = self.deckElements[i];
if (element.spellId && element.isDeckCard) {
var cardX = element.x;
var cardY = element.y;
if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) {
// Visual feedback before removal
LK.effects.flashObject(element, 0xFF0000, 300);
// Remove from deck
if (self.spellDeck.removeFromDeck(element.spellId)) {
self.refreshDeckDisplay();
LK.effects.flashScreen(0xFF8800, 200);
// Show removal message
var removeText = new Text2('HECHIZO REMOVIDO', {
size: 60,
fill: 0xFF6666,
font: "monospace"
});
removeText.anchor.set(0.5, 0.5);
removeText.x = 2048 / 2;
removeText.y = 2200;
self.addChild(removeText);
// Animate and remove message
tween(removeText, {
alpha: 0,
y: removeText.y - 100
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (removeText.parent) removeText.destroy();
}
});
} else {
LK.effects.flashScreen(0xFF0000, 200);
}
return;
}
}
}
// Check available card clicks with better hit detection
for (var i = 0; i < self.availableElements.length; i++) {
var element = self.availableElements[i];
if (element.spellId && !element.isDeckCard) {
var cardX = element.x;
var cardY = element.y;
if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) {
// Visual feedback before addition
LK.effects.flashObject(element, 0x00FF00, 300);
// Add to deck
if (self.spellDeck.addToDeck(element.spellId)) {
self.refreshDeckDisplay();
LK.effects.flashScreen(0x00FF00, 200);
// Show addition message
var addText = new Text2('HECHIZO AÑADIDO', {
size: 60,
fill: 0x66FF66,
font: "monospace"
});
addText.anchor.set(0.5, 0.5);
addText.x = 2048 / 2;
addText.y = 2200;
self.addChild(addText);
// Animate and remove message
tween(addText, {
alpha: 0,
y: addText.y - 100
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (addText.parent) addText.destroy();
}
});
} else {
LK.effects.flashScreen(0xFF0000, 200);
// Show error message
var errorText = new Text2('DECK LLENO (MAX 5)', {
size: 50,
fill: 0xFF6666,
font: "monospace"
});
errorText.anchor.set(0.5, 0.5);
errorText.x = 2048 / 2;
errorText.y = 2200;
self.addChild(errorText);
// Animate and remove message
tween(errorText, {
alpha: 0,
y: errorText.y - 100
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (errorText.parent) errorText.destroy();
}
});
}
return;
}
}
}
// Deck back button
if (y >= 2350 && y <= 2650) {
self.hideDeck();
}
// Block all other interactions when deck menu is active
return;
}
// Handle shop menu interactions
if (self.shopMode) {
// Shop item purchase buttons
for (var i = 0; i < 3; i++) {
var itemY = 1100 + i * 200;
if (y >= itemY - 50 && y <= itemY + 50 && x >= 2048 / 2 + 200 && x <= 2048 / 2 + 400) {
// Purchase item logic
var shopItems = [{
name: 'POCION SALUD',
cost: 10
}, {
name: 'ESCUDO MAGICO',
cost: 15
}, {
name: 'ESPADA MALDITA',
cost: 20
}];
var item = shopItems[i];
if (coinCounter >= item.cost) {
coinCounter -= item.cost;
// Apply item effect based on type
if (i === 0) {
// Health potion
if (wizard) {
wizard.health = Math.min(wizard.health + 50, wizard.maxHealth);
updateHealthBar();
}
} else if (i === 1) {
// Magic shield
if (wizard) {
wizard.shieldActive = true;
wizard.maxShieldHits = 3;
wizard.currentShieldHits = 0;
}
} else if (i === 2) {
// Cursed sword - temporary damage boost
if (wizard) {
wizard.tempDamageBoost = true;
wizard.tempDamageTimer = 1800; // 30 seconds at 60fps
}
}
LK.effects.flashScreen(0x00FF00, 300);
} else {
LK.effects.flashScreen(0xFF0000, 300);
}
return;
}
}
// Shop back button
if (y >= 1950 && y <= 2050) {
self.hideShop();
return;
}
// Block all other interactions when shop menu is active
return;
}
// Check if configuration button was clicked
if (y >= 1700 && y <= 1900 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) {
// Show configuration menu
self.showConfigMenu();
} else if (y >= 1850 && y <= 2050 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) {
// Show shop menu
self.showShop();
} else if (y >= 2050 && y <= 2250 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) {
// Show deck menu
self.showDeck();
} else if (storage.tutorialCompleted && y >= 2250 && y <= 2450 && x >= 2048 / 2 - 200 && x <= 2048 / 2 + 200) {
// Show tutorial again
if (tutorial) {
self.visible = false;
tutorial.startTutorial();
}
} else if (y >= 1450 && y <= 1650) {
// Start the game by hiding menu
self.startGame();
}
};
self.showConfigMenu = function () {
if (!self.configOverlay) {
self.configOverlay = self.createMenuOverlay(0x000000);
self.configTitle = self.createMenuText('CONFIGURACION', 2048 / 2, 800, 120, 0xFFD700);
self.musicVolumeText = self.createMenuText('VOLUMEN MUSICA: ' + Math.round((storage.musicVolume || 0.7) * 100) + '%', 2048 / 2, 1200, 80, 0xFFFFFF);
self.soundVolumeText = self.createMenuText('VOLUMEN SONIDO: ' + Math.round((storage.soundVolume || 1.0) * 100) + '%', 2048 / 2, 1400, 80, 0xFFFFFF);
self.difficultyText = self.createMenuText('DIFICULTAD: ' + (storage.difficulty || 'NORMAL'), 2048 / 2, 1600, 80, 0xFFFFFF);
self.backButton = self.createMenuButton('coin', 2048 / 2, 2000, 0x00FF00);
self.backText = self.createMenuText('VOLVER', 2048 / 2, 2000, 80, 0xFFFFFF);
}
self.configOverlay.visible = true;
self.configMode = true;
};
self.hideConfigMenu = function () {
if (self.configOverlay) {
self.configOverlay.destroy();
self.configOverlay = null;
}
// Remove all configuration text elements
if (self.musicVolumeText) {
self.musicVolumeText.destroy();
self.musicVolumeText = null;
}
if (self.soundVolumeText) {
self.soundVolumeText.destroy();
self.soundVolumeText = null;
}
if (self.difficultyText) {
self.difficultyText.destroy();
self.difficultyText = null;
}
// Remove back button elements
if (self.backButton) {
self.backButton.destroy();
self.backButton = null;
}
if (self.backText) {
self.backText.destroy();
self.backText = null;
}
// Remove configuration title
if (self.configTitle) {
self.configTitle.destroy();
self.configTitle = null;
}
// Remove all configuration children that were added
for (var i = self.children.length - 1; i >= 0; i--) {
var child = self.children[i];
// Remove config-related elements (title, texts, buttons created in showConfigMenu)
if (child.setText && child.text && (child.text.includes('CONFIGURACION') || child.text.includes('VOLUMEN') || child.text.includes('DIFICULTAD') || child.text.includes('VOLVER'))) {
child.destroy();
}
}
self.configMode = false;
// Reset to show main menu elements
self.visible = true;
};
self.showShop = function () {
if (!self.shopOverlay) {
self.shopOverlay = self.createMenuOverlay(0x000033);
self.shopTitle = self.createMenuText('TIENDA', 2048 / 2, 800, 120, 0xFFD700);
var shopItems = self.getShopItemsData();
self.initializeShopArrays();
self.createShopItems(shopItems);
self.shopBackButton = self.createMenuButton('coin', 2048 / 2, 2000, 0x00FF00);
self.shopBackText = self.createMenuText('VOLVER', 2048 / 2, 2000, 80, 0xFFFFFF);
}
self.shopOverlay.visible = true;
self.shopMode = true;
};
self.hideShop = function () {
if (self.shopOverlay) {
self.shopOverlay.destroy();
self.shopOverlay = null;
}
// Remove shop title
if (self.shopTitle) {
self.shopTitle.destroy();
self.shopTitle = null;
}
// Remove shop back button elements
if (self.shopBackButton) {
self.shopBackButton.destroy();
self.shopBackButton = null;
}
if (self.shopBackText) {
self.shopBackText.destroy();
self.shopBackText = null;
}
// Remove all shop icons
if (self.shopIcons) {
for (var i = 0; i < self.shopIcons.length; i++) {
if (self.shopIcons[i]) {
self.shopIcons[i].destroy();
}
}
self.shopIcons = [];
}
// Remove all shop texts
if (self.shopTexts) {
for (var i = 0; i < self.shopTexts.length; i++) {
if (self.shopTexts[i]) {
self.shopTexts[i].destroy();
}
}
self.shopTexts = [];
}
// Remove all shop buy buttons
if (self.shopBuyButtons) {
for (var i = 0; i < self.shopBuyButtons.length; i++) {
if (self.shopBuyButtons[i]) {
self.shopBuyButtons[i].destroy();
}
}
self.shopBuyButtons = [];
}
// Remove all shop buy texts
if (self.shopBuyTexts) {
for (var i = 0; i < self.shopBuyTexts.length; i++) {
if (self.shopBuyTexts[i]) {
self.shopBuyTexts[i].destroy();
}
}
self.shopBuyTexts = [];
}
// Remove all shop children that were added
for (var i = self.children.length - 1; i >= 0; i--) {
var child = self.children[i];
// Remove shop-related elements
if (child.setText && child.text && (child.text.includes('TIENDA') || child.text.includes('POCION') || child.text.includes('ESCUDO') || child.text.includes('ESPADA') || child.text.includes('COMPRAR'))) {
child.destroy();
}
}
self.shopMode = false;
// Reset to show main menu elements
self.visible = true;
};
self.showDeck = function () {
if (!self.deckOverlay) {
self.deckOverlay = self.createMenuOverlay(0x1a0a2e);
self.deckTitle = self.createMenuText('DECK DE HECHIZOS', 2048 / 2, 600, 100, 0xFFD700);
self.initializeDeckArrays();
if (!self.spellDeck) self.spellDeck = new SpellDeck();
self.refreshDeckDisplay();
self.deckBackButton = self.createMenuButton('coin', 2048 / 2, 2500, 0x00FF00);
self.deckBackText = self.createMenuText('VOLVER', 2048 / 2, 2500, 80, 0xFFFFFF);
}
self.deckOverlay.visible = true;
self.deckMode = true;
self.refreshDeckDisplay();
};
self.initializeDeckArrays = function () {
if (!self.deckElements) self.deckElements = [];
if (!self.availableElements) self.availableElements = [];
};
self.refreshDeckDisplay = function () {
if (!self.spellDeck) return;
// Clear existing deck elements
for (var i = 0; i < self.deckElements.length; i++) {
if (self.deckElements[i] && self.deckElements[i].parent) {
self.deckElements[i].destroy();
}
}
self.deckElements = [];
// Clear existing available elements
for (var i = 0; i < self.availableElements.length; i++) {
if (self.availableElements[i] && self.availableElements[i].parent) {
self.availableElements[i].destroy();
}
}
self.availableElements = [];
// Add helpful instructions at the top
var instructionText = new Text2('TOCA CARTAS PARA AÑADIR/QUITAR DEL DECK', {
size: 50,
fill: 0x00FF00,
font: "monospace"
});
instructionText.anchor.set(0.5, 0.5);
instructionText.x = 2048 / 2;
instructionText.y = 700;
self.addChild(instructionText);
self.deckElements.push(instructionText);
// Display current deck (top section)
var deckLabel = new Text2('MI DECK ACTUAL (' + self.spellDeck.currentDeck.length + '/5):', {
size: 70,
fill: 0xFFD700,
font: "monospace"
});
deckLabel.anchor.set(0.5, 0.5);
deckLabel.x = 2048 / 2;
deckLabel.y = 800;
self.addChild(deckLabel);
self.deckElements.push(deckLabel);
// Add deck instruction
var deckInstruction = new Text2('TOCA PARA QUITAR', {
size: 40,
fill: 0xFF6666,
font: "monospace"
});
deckInstruction.anchor.set(0.5, 0.5);
deckInstruction.x = 2048 / 2;
deckInstruction.y = 850;
self.addChild(deckInstruction);
self.deckElements.push(deckInstruction);
// Display deck cards with better spacing
for (var i = 0; i < 5; i++) {
var cardX = 200 + i * 350;
var cardY = 1050;
if (i < self.spellDeck.currentDeck.length) {
var spell = self.spellDeck.getSpell(self.spellDeck.currentDeck[i]);
if (spell) {
// Card background with glow effect
var cardBg = self.attachAsset('spellCard', {
anchorX: 0.5,
anchorY: 0.5,
x: cardX,
y: cardY,
scaleX: 3.5,
scaleY: 4.5
});
cardBg.tint = self.spellDeck.getRarityColor(spell.rarity);
cardBg.spellId = spell.id;
cardBg.isDeckCard = true;
cardBg.alpha = 0.9;
self.deckElements.push(cardBg);
// Add border glow
var glowBorder = self.attachAsset('spellCardBg', {
anchorX: 0.5,
anchorY: 0.5,
x: cardX,
y: cardY,
scaleX: 4,
scaleY: 5
});
glowBorder.tint = 0x00FF00;
glowBorder.alpha = 0.3;
self.deckElements.push(glowBorder);
// Card name
var cardName = new Text2(spell.name, {
size: 35,
fill: 0xFFFFFF,
font: "monospace"
});
cardName.anchor.set(0.5, 0.5);
cardName.x = cardX;
cardName.y = cardY - 60;
self.addChild(cardName);
self.deckElements.push(cardName);
// Card description
var cardDesc = new Text2(spell.description, {
size: 25,
fill: 0xCCCCCC,
font: "monospace",
wordWrap: true,
wordWrapWidth: 250
});
cardDesc.anchor.set(0.5, 0.5);
cardDesc.x = cardX;
cardDesc.y = cardY + 20;
self.addChild(cardDesc);
self.deckElements.push(cardDesc);
// Card stats
var statsText = '';
if (spell.damage) statsText += 'Daño: ' + spell.damage + '\n';
if (spell.healing) statsText += 'Cura: ' + spell.healing + '\n';
if (spell.manaCost) statsText += 'Mana: ' + spell.manaCost;
if (statsText) {
var cardStats = new Text2(statsText, {
size: 20,
fill: 0xFFD700,
font: "monospace"
});
cardStats.anchor.set(0.5, 0.5);
cardStats.x = cardX;
cardStats.y = cardY + 80;
self.addChild(cardStats);
self.deckElements.push(cardStats);
}
}
} else {
// Empty slot
var emptySlot = self.attachAsset('spellCardBg', {
anchorX: 0.5,
anchorY: 0.5,
x: cardX,
y: cardY,
scaleX: 3.5,
scaleY: 4.5
});
emptySlot.tint = 0x444444;
emptySlot.alpha = 0.5;
self.deckElements.push(emptySlot);
var emptyText = new Text2('VACIO', {
size: 40,
fill: 0x666666,
font: "monospace"
});
emptyText.anchor.set(0.5, 0.5);
emptyText.x = cardX;
emptyText.y = cardY;
self.addChild(emptyText);
self.deckElements.push(emptyText);
}
}
// Display available spells (bottom section)
var availableLabel = new Text2('HECHIZOS DISPONIBLES PARA AÑADIR:', {
size: 60,
fill: 0xFFD700,
font: "monospace"
});
availableLabel.anchor.set(0.5, 0.5);
availableLabel.x = 2048 / 2;
availableLabel.y = 1350;
self.addChild(availableLabel);
self.availableElements.push(availableLabel);
// Add available instruction
var availableInstruction = new Text2('TOCA PARA AÑADIR A TU DECK', {
size: 40,
fill: 0x66FF66,
font: "monospace"
});
availableInstruction.anchor.set(0.5, 0.5);
availableInstruction.x = 2048 / 2;
availableInstruction.y = 1400;
self.addChild(availableInstruction);
self.availableElements.push(availableInstruction);
// Display available spell cards
var availableSpells = [];
for (var i = 0; i < self.spellDeck.availableSpells.length; i++) {
var spell = self.spellDeck.availableSpells[i];
if (spell.unlocked && self.spellDeck.currentDeck.indexOf(spell.id) === -1) {
availableSpells.push(spell);
}
}
if (availableSpells.length === 0) {
var noSpellsText = new Text2('NO HAY HECHIZOS DISPONIBLES\nDESBLOQUEA MAS JUGANDO', {
size: 50,
fill: 0x888888,
font: "monospace"
});
noSpellsText.anchor.set(0.5, 0.5);
noSpellsText.x = 2048 / 2;
noSpellsText.y = 1600;
self.addChild(noSpellsText);
self.availableElements.push(noSpellsText);
}
for (var i = 0; i < availableSpells.length && i < 8; i++) {
var spell = availableSpells[i];
var cardX = 150 + i % 4 * 450;
var cardY = 1550 + Math.floor(i / 4) * 400;
// Card background with hover effect
var cardBg = self.attachAsset('spellCard', {
anchorX: 0.5,
anchorY: 0.5,
x: cardX,
y: cardY,
scaleX: 3.2,
scaleY: 4.2
});
cardBg.tint = self.spellDeck.getRarityColor(spell.rarity);
cardBg.spellId = spell.id;
cardBg.isDeckCard = false;
cardBg.alpha = 0.8;
self.availableElements.push(cardBg);
// Add selection glow
var selectionGlow = self.attachAsset('spellCardBg', {
anchorX: 0.5,
anchorY: 0.5,
x: cardX,
y: cardY,
scaleX: 3.7,
scaleY: 4.7
});
selectionGlow.tint = 0x00FFFF;
selectionGlow.alpha = 0.2;
self.availableElements.push(selectionGlow);
// Card name
var cardName = new Text2(spell.name, {
size: 32,
fill: 0xFFFFFF,
font: "monospace"
});
cardName.anchor.set(0.5, 0.5);
cardName.x = cardX;
cardName.y = cardY - 60;
self.addChild(cardName);
self.availableElements.push(cardName);
// Card description
var cardDesc = new Text2(spell.description, {
size: 22,
fill: 0xCCCCCC,
font: "monospace",
wordWrap: true,
wordWrapWidth: 280
});
cardDesc.anchor.set(0.5, 0.5);
cardDesc.x = cardX;
cardDesc.y = cardY + 10;
self.addChild(cardDesc);
self.availableElements.push(cardDesc);
// Card stats
var statsText = '';
if (spell.damage) statsText += 'Daño: ' + spell.damage + '\n';
if (spell.healing) statsText += 'Cura: ' + spell.healing + '\n';
if (spell.manaCost) statsText += 'Mana: ' + spell.manaCost;
if (statsText) {
var cardStats = new Text2(statsText, {
size: 18,
fill: 0xFFD700,
font: "monospace"
});
cardStats.anchor.set(0.5, 0.5);
cardStats.x = cardX;
cardStats.y = cardY + 70;
self.addChild(cardStats);
self.availableElements.push(cardStats);
}
// Rarity indicator
var rarityText = new Text2(spell.rarity.toUpperCase(), {
size: 20,
fill: self.spellDeck.getRarityColor(spell.rarity),
font: "monospace"
});
rarityText.anchor.set(0.5, 0.5);
rarityText.x = cardX;
rarityText.y = cardY + 100;
self.addChild(rarityText);
self.availableElements.push(rarityText);
}
};
self.hideDeck = function () {
if (self.deckOverlay) {
self.deckOverlay.destroy();
self.deckOverlay = null;
}
// Remove deck title
if (self.deckTitle) {
self.deckTitle.destroy();
self.deckTitle = null;
}
// Remove deck back button elements
if (self.deckBackButton) {
self.deckBackButton.destroy();
self.deckBackButton = null;
}
if (self.deckBackText) {
self.deckBackText.destroy();
self.deckBackText = null;
}
// Clear deck elements
for (var i = 0; i < self.deckElements.length; i++) {
if (self.deckElements[i] && self.deckElements[i].parent) {
self.deckElements[i].destroy();
}
}
self.deckElements = [];
// Clear available elements
for (var i = 0; i < self.availableElements.length; i++) {
if (self.availableElements[i] && self.availableElements[i].parent) {
self.availableElements[i].destroy();
}
}
self.availableElements = [];
// Remove all deck-related children
for (var i = self.children.length - 1; i >= 0; i--) {
var child = self.children[i];
if (child.setText && child.text && (child.text.includes('DECK') || child.text.includes('HECHIZOS') || child.text.includes('ACTUAL') || child.text.includes('DISPONIBLES'))) {
child.destroy();
}
}
self.deckMode = false;
self.visible = true;
};
// Unified menu element factory methods
self.createMenuOverlay = function (tintColor) {
var overlay = self.addChild(LK.getAsset('startMenuBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 1.0,
scaleY: 1.0
}));
overlay.alpha = 1.0;
overlay.tint = tintColor;
return overlay;
};
self.createMenuText = function (text, x, y, size, fill) {
var textElement = new Text2(text, {
size: size,
fill: fill,
font: "monospace"
});
textElement.anchor.set(0.5, 0.5);
textElement.x = x;
textElement.y = y;
self.addChild(textElement);
return textElement;
};
self.createMenuButton = function (asset, x, y, tintColor) {
var button = self.attachAsset(asset, {
anchorX: 0.5,
anchorY: 0.5,
x: x,
y: y,
scaleX: 2,
scaleY: 2
});
button.tint = tintColor;
return button;
};
self.getShopItemsData = function () {
return [{
name: 'POCION SALUD',
description: 'Restaura 50 HP',
cost: 10,
icon: 'energySphere'
}, {
name: 'ESCUDO MAGICO',
description: 'Bloquea 3 ataques',
cost: 15,
icon: 'shield'
}, {
name: 'ESPADA MALDITA',
description: 'Daño x2 por 30s',
cost: 20,
icon: 'spell'
}];
};
self.initializeShopArrays = function () {
if (!self.shopIcons) self.shopIcons = [];
if (!self.shopTexts) self.shopTexts = [];
if (!self.shopBuyButtons) self.shopBuyButtons = [];
if (!self.shopBuyTexts) self.shopBuyTexts = [];
};
self.createShopItems = function (shopItems) {
for (var i = 0; i < shopItems.length; i++) {
var item = shopItems[i];
var yPos = 1100 + i * 200;
var itemIcon = self.attachAsset(item.icon, {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2 - 300,
y: yPos,
scaleX: 2,
scaleY: 2
});
itemIcon.tint = 0xFFD700;
self.shopIcons.push(itemIcon);
var itemText = new Text2(item.name + '\n' + item.description + '\nCosto: ' + item.cost + ' monedas', {
size: 60,
fill: 0xFFFFFF,
font: "monospace"
});
itemText.anchor.set(0, 0.5);
itemText.x = 2048 / 2 - 200;
itemText.y = yPos;
self.addChild(itemText);
self.shopTexts.push(itemText);
var buyButton = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2 + 300,
y: yPos,
scaleX: 2,
scaleY: 2
});
buyButton.tint = 0x00FF00;
buyButton.itemIndex = i;
self.shopBuyButtons.push(buyButton);
var buyText = new Text2('COMPRAR', {
size: 50,
fill: 0xFFFFFF,
font: "monospace"
});
buyText.anchor.set(0.5, 0.5);
buyText.x = 2048 / 2 + 300;
buyText.y = yPos;
self.addChild(buyText);
self.shopBuyTexts.push(buyText);
}
};
self.startGame = function () {
// Check if this is a new player (no tutorial completed)
if (!storage.tutorialCompleted && tutorial) {
// Show tutorial for new players
self.visible = false;
// Small delay to ensure menu is hidden before tutorial starts
tween({}, {}, {
duration: 100,
onFinish: function onFinish() {
tutorial.startTutorial();
}
});
// Tutorial started successfully
return;
}
// Hide menu and start game normally
self.visible = false;
gameStarted = true;
// Show cave background when game starts
if (backgroundMap) {
backgroundMap.visible = true;
}
// Show all game elements
wizard.visible = true;
for (var i = 0; i < paths.length; i++) {
paths[i].visible = true;
}
// Show all stone path segments and make them visible
for (var i = 0; i < game.children.length; i++) {
var child = game.children[i];
if (child.pathIndex !== undefined && child !== paths[child.pathIndex]) {
child.visible = true;
// Check if it's a stone path segment or path number
if (child.alpha !== undefined && child.setText === undefined) {
child.alpha = 0; // Keep stone paths invisible
}
}
}
coinText.visible = true;
killCountText.visible = true;
tapText.visible = true;
healthBarBg.visible = true;
healthBar.visible = true;
healthText.visible = true;
// Show spell UI
if (manaBarBg) manaBarBg.visible = true;
if (manaBar) manaBar.visible = true;
if (manaText) manaText.visible = true;
for (var i = 0; i < spellSlots.length; i++) {
spellSlots[i].visible = true;
}
// Start medieval music with user's volume setting
var musicVolume = storage.musicVolume || 0.7;
LK.playMusic('medievalTheme', {
volume: musicVolume,
fade: {
start: 0,
end: musicVolume,
duration: 2000
}
});
};
return self;
});
// Advanced Object Pool system with intelligent performance analysis and predictive scaling
var ObjectPool = Container.expand(function () {
var self = Container.call(this);
// Advanced performance metrics for intelligent pool management
self.performanceMetrics = {
frameTime: 0,
avgFrameTime: 16.67,
// Target 60fps
frameHistory: [],
maxHistorySize: 300,
// 5 seconds at 60fps
memoryPressure: 0,
// 0-1 scale of memory pressure
gcFrequency: 0,
lastGCTime: 0,
poolEfficiency: {} // Hit/miss ratios per pool
};
// Intelligent pool configurations with dynamic scaling
self.poolConfig = {
// Projectile pools with combat intensity awareness
projectiles: {
basic: {
maxSize: 75,
// Increased for intense combat
minSize: 20,
createFn: function createFn() {
return new Projectile('projectile');
}
},
fireBall: {
maxSize: 45,
// Enhanced for area damage
minSize: 15,
createFn: function createFn() {
return new Projectile('fireBall');
}
},
energyBeam: {
maxSize: 35,
minSize: 10,
createFn: function createFn() {
return new Projectile('energyBeam');
}
}
},
// Particle pools with performance-based scaling
particles: {
explosions: {
maxSize: 150,
// High for intense effects
minSize: 30,
createFn: function createFn() {
return LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5
});
}
},
sparks: {
maxSize: 200,
minSize: 50,
createFn: function createFn() {
return LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5
});
}
},
smoke: {
maxSize: 100,
minSize: 25,
createFn: function createFn() {
return LK.getAsset('energySphere', {
anchorX: 0.5,
anchorY: 0.5
});
}
}
},
// Enemy pools with spawn prediction
enemies: {
skeleton: {
maxSize: 40,
minSize: 15,
createFn: function createFn() {
return new Enemy('skeleton');
}
},
ogre: {
maxSize: 20,
minSize: 8,
createFn: function createFn() {
return new Enemy('ogre');
}
},
knight: {
maxSize: 15,
minSize: 6,
createFn: function createFn() {
return new Enemy('knight');
}
}
},
// UI pools for frequent text updates
ui: {
damageText: {
maxSize: 80,
// High frequency damage numbers
minSize: 20,
createFn: function createFn() {
return new Text2('', {
size: 120,
fill: 0xFF4444,
font: "monospace"
});
}
},
healText: {
maxSize: 40,
minSize: 10,
createFn: function createFn() {
return new Text2('', {
size: 100,
fill: 0x00FF00,
font: "monospace"
});
}
}
},
// World objects with predictive allocation
world: {
coin: {
maxSize: 50,
// Increased for coin shower effects
minSize: 15,
createFn: function createFn() {
return new Coin();
}
}
}
};
// Active pools with advanced tracking
self.activePools = {};
self.poolStats = {
totalRequests: 0,
totalHits: 0,
totalMisses: 0,
avgRequestsPerSecond: 0,
peakUsage: {},
fragmentationLevel: 0
};
// Intelligent pool initialization with performance prediction
self.initializePools = function () {
for (var category in self.poolConfig) {
self.activePools[category] = {};
for (var type in self.poolConfig[category]) {
var config = self.poolConfig[category][type];
self.activePools[category][type] = {
available: [],
inUse: [],
config: config,
// Advanced metrics per pool
metrics: {
hitRate: 0,
missRate: 0,
peakUsage: 0,
avgUsage: 0,
lastUsageSpike: 0,
predictedDemand: config.minSize
},
// Smart pre-warming based on game context
preWarmSize: self.calculatePreWarmSize(category, type, config)
};
// Pre-populate with intelligent sizing
var pool = self.activePools[category][type];
for (var i = 0; i < pool.preWarmSize; i++) {
var obj = config.createFn();
obj.poolType = category + '_' + type;
obj.visible = false;
obj.pooled = true;
obj._poolCategory = category;
obj._poolSubType = type;
pool.available.push(obj);
}
}
}
};
// Calculate intelligent pre-warm size based on game context
self.calculatePreWarmSize = function (category, type, config) {
var gameIntensity = self.estimateGameIntensity();
var baseSize = config.minSize;
// Adjust based on category importance and game state
switch (category) {
case 'projectiles':
return Math.floor(baseSize * (1 + gameIntensity * 0.8));
case 'particles':
return Math.floor(baseSize * (1 + gameIntensity * 1.2));
// Particles scale more
case 'enemies':
return Math.floor(baseSize * (0.8 + gameIntensity * 0.4));
// Conservative
case 'ui':
return Math.floor(baseSize * (1 + gameIntensity * 0.3));
default:
return baseSize;
}
};
// Estimate current game intensity for pool sizing
self.estimateGameIntensity = function () {
if (typeof enemyKillCounter === 'undefined') return 0.3; // Early game default
return Math.min(1.0, enemyKillCounter / 100); // Scale 0-1 over 100 kills
};
// Enhanced object retrieval with predictive scaling
self.getObject = function (category, type, initData) {
var pool = self.activePools[category] && self.activePools[category][type];
if (!pool) {
console.error('Pool not found:', category, type);
return null;
}
self.poolStats.totalRequests++;
var obj;
if (pool.available.length > 0) {
// Hit: Reuse existing object
obj = pool.available.pop();
self.resetObject(obj, category, type);
self.poolStats.totalHits++;
pool.metrics.hitRate++;
} else if (pool.inUse.length < pool.config.maxSize) {
// Miss but can expand: Create new object
obj = pool.config.createFn();
obj.poolType = category + '_' + type;
obj.pooled = true;
obj._poolCategory = category;
obj._poolSubType = type;
self.poolStats.totalMisses++;
pool.metrics.missRate++;
} else {
// Pool exhausted: Intelligent reuse strategy
obj = self.handlePoolExhaustion(pool, category, type);
if (!obj) return null;
}
// Initialize object with provided data
if (initData) {
for (var key in initData) {
obj[key] = initData[key];
}
}
obj.visible = true;
obj.inPool = false;
obj.alpha = 1.0;
obj._acquisitionTime = LK.ticks;
pool.inUse.push(obj);
// Update pool metrics
pool.metrics.peakUsage = Math.max(pool.metrics.peakUsage, pool.inUse.length);
// Trigger predictive scaling if needed
self.triggerPredictiveScaling(pool, category, type);
return obj;
};
// Intelligent pool exhaustion handling
self.handlePoolExhaustion = function (pool, category, type) {
// Strategy 1: Find oldest object for reuse
var oldestObj = null;
var oldestTime = Infinity;
for (var i = 0; i < pool.inUse.length; i++) {
var obj = pool.inUse[i];
var age = LK.ticks - (obj._acquisitionTime || 0);
if (age > oldestTime) {
oldestTime = age;
oldestObj = obj;
}
}
if (oldestObj) {
// Remove from game before reusing
if (oldestObj.parent) {
oldestObj.parent.removeChild(oldestObj);
}
// Remove from inUse array
var index = pool.inUse.indexOf(oldestObj);
if (index !== -1) {
pool.inUse.splice(index, 1);
}
self.resetObject(oldestObj, category, type);
return oldestObj;
}
return null;
};
// Predictive scaling based on usage patterns
self.triggerPredictiveScaling = function (pool, category, type) {
var usageRatio = pool.inUse.length / pool.config.maxSize;
var recentMissRate = pool.metrics.missRate / (pool.metrics.hitRate + pool.metrics.missRate + 1);
// Scale up if high usage and high miss rate
if (usageRatio > 0.8 && recentMissRate > 0.3) {
self.expandPool(pool, category, type);
}
};
// Dynamic pool expansion
self.expandPool = function (pool, category, type) {
var currentMax = pool.config.maxSize;
var expansion = Math.min(currentMax * 0.2, 20); // 20% increase, capped at 20
var newMax = currentMax + Math.floor(expansion);
// Check memory pressure before expanding
if (self.performanceMetrics.memoryPressure < 0.7) {
pool.config.maxSize = newMax;
// Pre-create some objects for the expanded pool
var preCreateCount = Math.min(5, expansion);
for (var i = 0; i < preCreateCount; i++) {
var obj = pool.config.createFn();
obj.poolType = category + '_' + type;
obj.pooled = true;
obj._poolCategory = category;
obj._poolSubType = type;
obj.visible = false;
pool.available.push(obj);
}
}
};
// Enhanced object return with analytics
self.returnObject = function (obj) {
if (!obj || !obj.pooled) return;
var category = obj._poolCategory;
var type = obj._poolSubType;
var pool = self.activePools[category] && self.activePools[category][type];
if (!pool) return;
// Remove from in-use array
var inUseIndex = pool.inUse.indexOf(obj);
if (inUseIndex !== -1) {
pool.inUse.splice(inUseIndex, 1);
}
// Clean up object
obj.visible = false;
obj.inPool = true;
obj._returnTime = LK.ticks;
self.resetObject(obj, category, type);
// Intelligent return strategy based on pool pressure
if (pool.available.length < pool.config.maxSize * 0.5) {
// Pool is under-supplied, keep object
pool.available.push(obj);
} else if (self.performanceMetrics.memoryPressure > 0.6) {
// High memory pressure, destroy excess objects
if (obj.parent) obj.parent.removeChild(obj);
if (obj.destroy) obj.destroy();
} else {
// Normal pressure, keep in pool
pool.available.push(obj);
}
};
// Advanced object reset with category-specific handling
self.resetObject = function (obj, category, type) {
// Universal reset properties
obj.x = 0;
obj.y = 0;
obj.rotation = 0;
obj.alpha = 1;
obj.scaleX = 1;
obj.scaleY = 1;
obj.tint = 0xFFFFFF;
obj.visible = false;
// Category-specific resets
switch (category + '_' + type) {
case 'projectiles_basic':
case 'projectiles_fireBall':
case 'projectiles_energyBeam':
obj.direction = {
x: 0,
y: 0
};
obj.targetEnemy = null;
obj.hitEnemy = false;
obj.damage = 100;
obj.speed = 50;
self.clearTweens(obj);
break;
case 'particles_explosions':
case 'particles_sparks':
case 'particles_smoke':
obj.tint = 0xFFFFFF;
obj.scaleX = 0.3;
obj.scaleY = 0.3;
self.clearTweens(obj);
break;
case 'ui_damageText':
case 'ui_healText':
if (obj.setText) obj.setText('');
if (obj.anchor && obj.anchor.set) obj.anchor.set(0.5, 0.5);
obj.scaleX = 1;
obj.scaleY = 1;
break;
case 'world_coin':
obj.bobOffset = Math.random() * Math.PI * 2;
obj.initialY = 0;
obj.isAnimating = false;
obj.scaleX = 1;
obj.scaleY = 1;
if (obj.coinGraphics) {
obj.coinGraphics.visible = true;
obj.coinGraphics.alpha = 1;
}
break;
case 'enemies_skeleton':
case 'enemies_ogre':
case 'enemies_knight':
obj.health = 100;
obj.isDying = false;
obj.frozen = false;
obj.timeSlowed = false;
self.clearTweens(obj);
break;
}
};
// Clear all tween references
self.clearTweens = function (obj) {
if (obj._activeTweens) {
for (var i = 0; i < obj._activeTweens.length; i++) {
if (obj._activeTweens[i] && obj._activeTweens[i].stop) {
obj._activeTweens[i].stop();
}
}
obj._activeTweens = [];
}
if (obj._tweens) {
for (var i = 0; i < obj._tweens.length; i++) {
if (obj._tweens[i] && obj._tweens[i].stop) {
obj._tweens[i].stop();
}
}
obj._tweens = [];
}
};
// Performance-aware cleanup with memory optimization
self.cleanup = function () {
var memoryFreed = 0;
for (var category in self.activePools) {
for (var type in self.activePools[category]) {
var pool = self.activePools[category][type];
// Smart cleanup: Keep minimum, destroy excess
var keepCount = Math.max(pool.config.minSize, Math.floor(pool.metrics.avgUsage));
// Destroy excess available objects
while (pool.available.length > keepCount) {
var obj = pool.available.pop();
if (obj.parent) obj.parent.removeChild(obj);
if (obj.destroy) obj.destroy();
memoryFreed++;
}
// Force return all in-use objects if doing full cleanup
for (var i = pool.inUse.length - 1; i >= 0; i--) {
self.returnObject(pool.inUse[i]);
}
}
}
console.log('ObjectPool cleanup freed', memoryFreed, 'objects');
};
// Advanced performance monitoring
self.updatePerformanceMetrics = function () {
var currentTime = performance ? performance.now() : Date.now();
if (self.performanceMetrics.lastFrameTime) {
var frameTime = currentTime - self.performanceMetrics.lastFrameTime;
self.performanceMetrics.frameHistory.push(frameTime);
if (self.performanceMetrics.frameHistory.length > self.performanceMetrics.maxHistorySize) {
self.performanceMetrics.frameHistory.shift();
}
// Calculate moving average
var sum = 0;
for (var i = 0; i < self.performanceMetrics.frameHistory.length; i++) {
sum += self.performanceMetrics.frameHistory[i];
}
self.performanceMetrics.avgFrameTime = sum / self.performanceMetrics.frameHistory.length;
}
self.performanceMetrics.lastFrameTime = currentTime;
// Estimate memory pressure based on frame time and pool usage
var frameStress = Math.max(0, (self.performanceMetrics.avgFrameTime - 16.67) / 16.67);
var totalInUse = 0,
totalCapacity = 0;
for (var category in self.activePools) {
for (var type in self.activePools[category]) {
var pool = self.activePools[category][type];
totalInUse += pool.inUse.length;
totalCapacity += pool.config.maxSize;
}
}
var poolStress = totalInUse / totalCapacity;
self.performanceMetrics.memoryPressure = Math.min(1.0, (frameStress + poolStress) / 2);
};
// Advanced analytics and optimization
self.getAdvancedStats = function () {
var stats = {
overall: {
totalRequests: self.poolStats.totalRequests,
hitRate: self.poolStats.totalRequests > 0 ? self.poolStats.totalHits / self.poolStats.totalRequests : 0,
memoryPressure: self.performanceMetrics.memoryPressure,
avgFrameTime: Math.round(self.performanceMetrics.avgFrameTime * 100) / 100
},
categories: {}
};
for (var category in self.activePools) {
stats.categories[category] = {};
for (var type in self.activePools[category]) {
var pool = self.activePools[category][type];
stats.categories[category][type] = {
available: pool.available.length,
inUse: pool.inUse.length,
total: pool.available.length + pool.inUse.length,
maxSize: pool.config.maxSize,
hitRate: pool.metrics.hitRate / (pool.metrics.hitRate + pool.metrics.missRate + 1),
peakUsage: pool.metrics.peakUsage,
efficiency: pool.inUse.length / pool.config.maxSize
};
}
}
return stats;
};
// Automatic optimization routine
self.optimizePools = function () {
for (var category in self.activePools) {
for (var type in self.activePools[category]) {
var pool = self.activePools[category][type];
var efficiency = pool.inUse.length / pool.config.maxSize;
// Shrink over-allocated pools
if (efficiency < 0.3 && pool.config.maxSize > pool.config.minSize) {
var newSize = Math.max(pool.config.minSize, Math.floor(pool.config.maxSize * 0.8));
pool.config.maxSize = newSize;
// Remove excess objects
while (pool.available.length > newSize - pool.inUse.length) {
var obj = pool.available.pop();
if (obj.destroy) obj.destroy();
}
}
}
}
};
// Initialize performance monitoring
self.initializePools();
// Auto-run optimization every 30 seconds
var optimizationTimer = 0;
var originalUpdate = self.update || function () {};
self.update = function () {
originalUpdate.call(self);
self.updatePerformanceMetrics();
optimizationTimer++;
if (optimizationTimer >= 1800) {
// 30 seconds at 60fps
self.optimizePools();
optimizationTimer = 0;
}
};
return self;
});
// All enemy types now use the unified Enemy class with type parameter
var Orb = Container.expand(function () {
var self = Container.call(this);
// Create orb visual using energy sphere
var orbGraphics = self.attachAsset('energySphere', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.4,
scaleY: 0.4
});
orbGraphics.tint = 0xFFD700; // Golden color for orbs
orbGraphics.alpha = 0.9;
self.orbitalAngle = 0; // Starting angle for this orb
self.orbitalRadius = 880; // Distance from wizard - doubled again for even more separation
self.rotationSpeed = 0.025; // How fast orbs rotate - halved for slower movement
// Category 3: Orb-enemy collisions (separate from projectile-enemy and enemy-wizard)
self.processOrbEnemyCollisions = function () {
var allEnemies = collisionArrayPool.getAllEnemies();
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
// Skip collision check with wizard
if (enemy === wizard) {
continue;
}
// Initialize collision tracking for this enemy if not exists
if (!self.lastIntersecting) {
self.lastIntersecting = {};
}
if (self.lastIntersecting[i] === undefined) {
self.lastIntersecting[i] = false;
}
// 1.1 Distance Culling: Quick distance check before expensive collision detection
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var maxOrbRange = 80; // Orb collision range
var currentIntersecting = false;
if (distance <= maxOrbRange) {
// Only perform expensive intersection test if enemy is close enough
currentIntersecting = self.intersects(enemy);
}
if (!self.lastIntersecting[i] && currentIntersecting) {
// Deal damage to enemy on contact transition (first contact only)
enemy.takeDamage(200);
// Visual effect for orb hit
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Create orb impact effect
var orbImpact = game.addChild(LK.getAsset('energySphere', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x,
y: enemy.y,
scaleX: 0.3,
scaleY: 0.3
}));
orbImpact.tint = 0xFFD700;
orbImpact.alpha = 0.8;
// Animate orb impact
tween(orbImpact, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 250,
easing: tween.easeOut,
onFinish: function onFinish() {
orbImpact.destroy();
}
});
}
// Update collision state for this enemy
self.lastIntersecting[i] = currentIntersecting;
}
};
self.update = function () {
// Pause orb when tutorial is active
if (tutorial && tutorial.isActive) {
return;
}
// Upgrade menu removed - no pause needed
// Rotate around wizard
if (wizard) {
self.orbitalAngle += self.rotationSpeed;
self.x = wizard.x + Math.cos(self.orbitalAngle) * self.orbitalRadius;
self.y = wizard.y + Math.sin(self.orbitalAngle) * self.orbitalRadius - 240; // Position orb much higher up
}
// Add pulsing effect
var pulse = 1 + Math.sin(LK.ticks * 0.3) * 0.2;
orbGraphics.scaleX = 0.4 * pulse;
orbGraphics.scaleY = 0.4 * pulse;
// Category 3: Orb-enemy collisions (separate from projectile-enemy and enemy-wizard)
self.processOrbEnemyCollisions();
};
return self;
});
// Create global death handler instance
var Projectile = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'projectile';
self.speed = 50;
self.direction = {
x: 0,
y: 0
};
self.targetEnemy = null;
self.damage = 100;
self.hitEnemy = false;
// Define local projectile configurations since GAME_CONFIG is not available yet
var projectileConfigs = {
projectile: {
assetId: 'projectile',
scale: 1.5,
speed: 50,
damage: 100,
tint: 0x44aaff,
glowTint: 0x44aaff,
hasRotation: true
},
energyBeam: {
assetId: 'projectileGlow',
scale: 1.0,
speed: 60,
damage: 100,
tint: 0x00ffff,
glowTint: 0x00ffff,
hasRotation: true
},
fireBall: {
assetId: 'projectileGlow',
scale: 1.5,
speed: 40,
damage: 150,
tint: 0xFF4500,
glowTint: 0xFF6600,
hasRotation: true,
hasFlicker: true
}
};
// Get configuration from local configs
var config = projectileConfigs[self.type];
if (!config) {
// Fallback to projectile config
config = projectileConfigs.projectile;
}
self.speed = config.speed;
self.damage = config.damage;
// Create graphics based on type
var assetId = config.assetId;
self.graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: config.scale,
scaleY: config.scale
});
self.graphics.tint = config.tint;
// Add glow for basic projectiles
if (self.type === 'projectile') {
var glow = self.attachAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.0,
scaleY: 2.0
});
glow.alpha = 0.3;
glow.tint = config.glowTint;
}
self.update = function () {
if (tutorial && tutorial.isActive) return;
// Move projectile
self.x += self.direction.x * self.speed;
self.y += self.direction.y * self.speed;
// Visual effects
if (config.hasRotation) {
var angle = Math.atan2(self.direction.y, self.direction.x);
self.graphics.rotation = angle + Math.PI / 2;
}
if (config.hasFlicker) {
var flicker = 1 + Math.sin(LK.ticks * 0.4) * 0.3;
self.graphics.scaleX = config.scale * flicker;
self.graphics.scaleY = config.scale * flicker;
}
// Remove if off screen
if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) {
// Remove from projectiles array before destroying
ProjectileFactory.removeProjectile(self);
return;
}
// Handle collisions
self.checkCollisions();
};
self.checkCollisions = function () {
if (self.hitEnemy) return;
// Category 1: Area damage projectiles (fireball)
if (self.type === 'fireBall') {
self.processAreaDamageCollisions();
} else {
// Category 2: Single target projectiles
self.processSingleTargetCollisions();
}
};
// Separate collision processing for area damage projectiles
self.processAreaDamageCollisions = function () {
var allEnemies = collisionArrayPool.getAllEnemies();
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
if (enemy === wizard || enemy.isDying) continue;
// 1.1 Distance Culling: Quick distance check before expensive collision detection
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var maxFireballRange = 120; // Fireball collision range including area effect
// Only perform expensive intersection test if enemy is within range
if (distance <= maxFireballRange) {
if (self.intersects(enemy)) {
enemy.takeDamage(self.damage);
self.createExplosion(enemy);
self.hitEnemy = true;
// Remove from projectiles array before destroying
ProjectileFactory.removeProjectile(self);
return;
}
}
}
};
// Separate collision processing for single target projectiles
self.processSingleTargetCollisions = function () {
if (self.targetEnemy && self.targetEnemy.parent && !self.targetEnemy.isDying) {
// 1.1 Distance Culling: Quick distance check before expensive collision detection
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var maxProjectileRange = 100; // Standard projectile collision range
// Only perform expensive intersection test if target enemy is within range
if (distance <= maxProjectileRange) {
if (self.intersects(self.targetEnemy)) {
self.targetEnemy.takeDamage(self.damage);
self.hitEnemy = true;
// Remove from projectiles array before destroying
ProjectileFactory.removeProjectile(self);
return;
}
}
} else {
// Target is invalid, remove projectile
self.hitEnemy = true;
// Remove from projectiles array before destroying
ProjectileFactory.removeProjectile(self);
return;
}
};
self.createExplosion = function (enemy) {
LK.effects.flashObject(enemy, 0xFF4500, 400);
var explosion = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x,
y: enemy.y,
scaleX: 2,
scaleY: 2
}));
explosion.tint = 0xFF6600;
explosion.alpha = 0.8;
tween(explosion, {
scaleX: 5,
scaleY: 5,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
};
return self;
});
var SpellDeck = Container.expand(function () {
var self = Container.call(this);
// Available spells in the game with enhanced mechanics
self.availableSpells = [{
id: 'fireball',
name: 'FIREBALL',
description: 'Launches a burning projectile',
damage: 150,
cooldown: 3000,
manaCost: 30,
unlocked: true,
rarity: 'common',
element: 'fire',
chainTargets: 0,
areaRadius: 0
}, {
id: 'iceShard',
name: 'ICE SHARD',
description: 'Freezes and damages enemies',
damage: 100,
cooldown: 2000,
manaCost: 20,
unlocked: true,
rarity: 'common',
element: 'ice',
freezeDuration: 2000,
slowAmount: 0.5
}, {
id: 'lightning',
name: 'LIGHTNING',
description: 'Chain lightning between enemies',
damage: 200,
cooldown: 4000,
manaCost: 40,
unlocked: false,
rarity: 'rare',
element: 'lightning',
chainTargets: 3,
chainRange: 200
}, {
id: 'heal',
name: 'HEAL',
description: 'Restores wizard health',
healing: 50,
cooldown: 5000,
manaCost: 25,
unlocked: true,
rarity: 'common',
element: 'light',
healOverTime: 10,
healDuration: 3000
}, {
id: 'shield',
name: 'MAGIC SHIELD',
description: 'Temporary damage immunity',
duration: 3000,
cooldown: 8000,
manaCost: 50,
unlocked: false,
rarity: 'epic',
element: 'arcane',
shieldStrength: 3,
reflectDamage: true
}, {
id: 'meteor',
name: 'METEOR',
description: 'Massive area damage',
damage: 500,
cooldown: 10000,
manaCost: 80,
unlocked: false,
rarity: 'legendary',
element: 'fire',
areaRadius: 300,
stunDuration: 1000
}, {
id: 'teleport',
name: 'TELEPORT',
description: 'Instantly move wizard',
cooldown: 6000,
manaCost: 35,
unlocked: false,
rarity: 'rare',
element: 'arcane',
teleportRange: 400,
invulnDuration: 500
}, {
id: 'timeSlow',
name: 'TIME SLOW',
description: 'Slows all enemies',
duration: 4000,
cooldown: 12000,
manaCost: 60,
unlocked: false,
rarity: 'epic',
element: 'time',
slowAmount: 0.3,
damageBonus: 1.5
}];
// Current deck (max 5 spells)
self.currentDeck = storage.spellDeck || ['fireball', 'iceShard', 'heal'];
// Active spell cooldowns
self.spellCooldowns = {};
// Mana system
self.maxMana = 100;
self.currentMana = self.maxMana;
self.manaRegenRate = 2; // Mana per second
self.manaRegenTimer = 0;
// Combo system
self.lastSpellCast = 0;
self.comboMultiplier = 1.0;
self.comboTimer = 0;
// Spell evolution system
self.spellEvolutions = {};
// Enhanced spell mechanics
self.canCastSpell = function (spellId) {
var spell = self.getSpell(spellId);
if (!spell) return false;
// Check mana
if (self.currentMana < spell.manaCost) return false;
// Check cooldown
if (self.spellCooldowns[spellId] && self.spellCooldowns[spellId] > LK.ticks) return false;
return true;
};
// Cast spell with enhanced mechanics
self.castSpell = function (spellId, targetX, targetY) {
if (!self.canCastSpell(spellId)) return false;
var spell = self.getSpell(spellId);
if (!spell) return false;
// Consume mana
self.currentMana -= spell.manaCost;
// Set cooldown
self.spellCooldowns[spellId] = LK.ticks + spell.cooldown / 1000 * 60; // Convert to ticks
// Update combo system
self.updateCombo();
// Apply spell effects
self.applySpellEffect(spell, targetX, targetY);
// Update spell evolution
self.updateSpellEvolution(spellId);
return true;
};
// Update combo system
self.updateCombo = function () {
var currentTime = LK.ticks;
if (currentTime - self.lastSpellCast < 180) {
// Within 3 seconds
self.comboMultiplier = Math.min(self.comboMultiplier + 0.2, 3.0);
self.comboTimer = 180; // Reset combo timer
} else {
self.comboMultiplier = 1.0;
}
self.lastSpellCast = currentTime;
};
// Apply spell effects using unified factory pattern
self.applySpellEffect = function (spell, targetX, targetY) {
var spellConfig = GAME_CONFIG.spells[spell.id];
if (!spellConfig) {
console.error('Unknown spell configuration:', spell.id);
return;
}
self.executeSpellEffect(spell, spellConfig, targetX, targetY);
};
// Unified spell effect execution method
self.executeSpellEffect = function (spell, config, targetX, targetY) {
var damage = config.baseDamage ? config.baseDamage * self.comboMultiplier : 0;
var healing = config.baseHealing ? config.baseHealing * self.comboMultiplier : 0;
// Play sound effect
if (config.soundEffect) {
LK.getSound(config.soundEffect).play();
}
// Execute based on target type
switch (config.targetType) {
case 'enemy':
self.executeEnemyTargetSpell(spell, config, targetX, targetY, damage);
break;
case 'chain':
self.executeChainSpell(spell, config, targetX, targetY, damage);
break;
case 'self':
self.executeSelfTargetSpell(spell, config, healing);
break;
case 'area':
self.executeAreaSpell(spell, config, targetX, targetY, damage);
break;
case 'position':
self.executePositionSpell(spell, config, targetX, targetY);
break;
case 'all':
self.executeAllTargetSpell(spell, config);
break;
}
};
// Execute enemy-targeted spells (fireball, iceShard)
self.executeEnemyTargetSpell = function (spell, config, targetX, targetY, damage) {
var projectile = ProjectileFactory.createSpellProjectile(spell.id, wizard, targetX, targetY);
projectiles.push(projectile);
// Apply status effects
if (config.statusEffect) {
projectile.statusEffect = config.statusEffect;
}
};
// Streamlined spell execution using unified effect templates
self.executeChainSpell = function (spell, config, targetX, targetY, damage) {
var allEnemies = collisionArrayPool.getAllEnemies();
var hitEnemies = [];
var currentTarget = self.findClosestEnemy(allEnemies, targetX, targetY);
for (var chain = 0; chain < config.chainTargets && currentTarget; chain++) {
currentTarget.takeDamage(damage);
hitEnemies.push(currentTarget);
globalEffectManager.createVisualEffect('lightning', currentTarget, {});
currentTarget = self.findNextChainTarget(allEnemies, hitEnemies, currentTarget, config.chainRange);
}
};
// Streamlined self-target spells using effect manager
self.executeSelfTargetSpell = function (spell, config, healing) {
if (spell.id === 'heal') {
wizard.health = Math.min(wizard.health + healing, wizard.maxHealth);
updateHealthBar();
globalEffectManager.createVisualEffect('heal', wizard, {});
} else if (spell.id === 'shield') {
wizard.shieldActive = true;
wizard.maxShieldHits = config.shieldStrength;
wizard.currentShieldHits = 0;
wizard.shieldReflect = spell.reflectDamage;
globalEffectManager.createVisualEffect('shield', wizard, {});
}
};
// Streamlined area spells with efficient meteor effect
self.executeAreaSpell = function (spell, config, targetX, targetY, damage) {
var meteor = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: targetX,
y: targetY - 500,
scaleX: 3,
scaleY: 3
}));
meteor.tint = 0xFF4500;
tween(meteor, {
y: targetY,
scaleX: 5,
scaleY: 5
}, {
duration: 1000,
easing: tween.easeIn,
onFinish: function onFinish() {
self.executeAreaDamage(targetX, targetY, config.areaRadius, damage, config.stunDuration);
LK.effects.flashScreen(0xFF4500, 500);
meteor.destroy();
}
});
};
// Streamlined teleport with effect manager
self.executePositionSpell = function (spell, config, targetX, targetY) {
wizard.x = targetX;
wizard.y = targetY;
globalEffectManager.createFlashEffect(wizard, 0x8000FF, 300);
wizard.teleportInvuln = true;
tween({}, {}, {
duration: config.invulnDuration,
onFinish: function onFinish() {
wizard.teleportInvuln = false;
}
});
};
// Streamlined time slow using screen flash
self.executeAllTargetSpell = function (spell, config) {
var allEnemies = collisionArrayPool.getAllEnemies();
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
enemy.timeSlowed = true;
enemy.timeSlowAmount = config.slowAmount;
enemy.timeSlowTimer = config.duration / 1000 * 60;
}
LK.effects.flashScreen(0x8000FF, 300);
};
// Helper methods for spell execution
self.findClosestEnemy = function (allEnemies, targetX, targetY) {
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
var dx = enemy.x - targetX;
var dy = enemy.y - targetY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
return closestEnemy;
};
self.findNextChainTarget = function (allEnemies, hitEnemies, currentTarget, chainRange) {
var nextTarget = null;
var nextDistance = Infinity;
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
if (hitEnemies.indexOf(enemy) === -1) {
var dx = enemy.x - currentTarget.x;
var dy = enemy.y - currentTarget.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < chainRange && distance < nextDistance) {
nextDistance = distance;
nextTarget = enemy;
}
}
}
return nextTarget;
};
// Lightning effects now handled by unified effect manager
self.executeAreaDamage = function (centerX, centerY, radius, damage, stunDuration) {
var allEnemies = collisionArrayPool.getAllEnemies();
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
var dx = enemy.x - centerX;
var dy = enemy.y - centerY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= radius) {
enemy.takeDamage(damage);
enemy.frozen = true;
enemy.frozenTimer = stunDuration / 1000 * 60;
}
}
};
// Individual spell implementations - REMOVED (replaced by unified system)
self.castFireball = function (spell, targetX, targetY) {
// DEPRECATED: Use executeSpellEffect instead
self.executeSpellEffect(spell, GAME_CONFIG.spells.fireball, targetX, targetY);
};
// All individual spell casting methods have been replaced by the unified executeSpellEffect system
// This reduces code duplication and centralizes spell logic in GAME_CONFIG.spells
// Update mana regeneration
self.updateMana = function () {
self.manaRegenTimer++;
if (self.manaRegenTimer >= 60) {
// Every second
self.manaRegenTimer = 0;
self.currentMana = Math.min(self.currentMana + self.manaRegenRate, self.maxMana);
}
};
// Update combo timer
self.updateCombo = function () {
if (self.comboTimer > 0) {
self.comboTimer--;
if (self.comboTimer <= 0) {
self.comboMultiplier = 1.0;
}
}
};
// Update spell evolution
self.updateSpellEvolution = function (spellId) {
if (!self.spellEvolutions[spellId]) {
self.spellEvolutions[spellId] = 0;
}
self.spellEvolutions[spellId]++;
// Check for evolution milestones
if (self.spellEvolutions[spellId] % 10 === 0) {
self.evolveSpell(spellId);
}
};
// Evolve spell
self.evolveSpell = function (spellId) {
var spell = self.getSpell(spellId);
if (!spell) return;
// Enhance spell based on usage
switch (spellId) {
case 'fireball':
spell.damage += 25;
spell.areaRadius += 20;
break;
case 'iceShard':
spell.damage += 15;
spell.freezeDuration += 500;
break;
case 'lightning':
spell.chainTargets += 1;
spell.chainRange += 50;
break;
case 'heal':
spell.healing += 10;
break;
}
};
// Get spell evolution level
self.getEvolutionLevel = function (spellId) {
return Math.floor((self.spellEvolutions[spellId] || 0) / 10);
};
// Unlock spell based on achievements
self.unlockSpell = function (spellId) {
for (var i = 0; i < self.availableSpells.length; i++) {
if (self.availableSpells[i].id === spellId) {
self.availableSpells[i].unlocked = true;
storage.unlockedSpells = storage.unlockedSpells || [];
if (storage.unlockedSpells.indexOf(spellId) === -1) {
storage.unlockedSpells.push(spellId);
}
break;
}
}
};
// Load unlocked spells from storage
self.loadUnlockedSpells = function () {
var unlockedSpells = storage.unlockedSpells || [];
for (var i = 0; i < unlockedSpells.length; i++) {
var spellId = unlockedSpells[i];
for (var j = 0; j < self.availableSpells.length; j++) {
if (self.availableSpells[j].id === spellId) {
self.availableSpells[j].unlocked = true;
break;
}
}
}
};
// Add spell to deck
self.addToDeck = function (spellId) {
if (self.currentDeck.length >= 5) return false;
if (self.currentDeck.indexOf(spellId) !== -1) return false;
var spell = self.getSpell(spellId);
if (spell && spell.unlocked) {
self.currentDeck.push(spellId);
storage.spellDeck = self.currentDeck;
return true;
}
return false;
};
// Remove spell from deck
self.removeFromDeck = function (spellId) {
var index = self.currentDeck.indexOf(spellId);
if (index !== -1) {
self.currentDeck.splice(index, 1);
storage.spellDeck = self.currentDeck;
return true;
}
return false;
};
// Get spell data by ID
self.getSpell = function (spellId) {
for (var i = 0; i < self.availableSpells.length; i++) {
if (self.availableSpells[i].id === spellId) {
return self.availableSpells[i];
}
}
return null;
};
// Get rarity color
self.getRarityColor = function (rarity) {
switch (rarity) {
case 'common':
return 0xFFFFFF;
case 'rare':
return 0x0080FF;
case 'epic':
return 0x8000FF;
case 'legendary':
return 0xFF8000;
default:
return 0xFFFFFF;
}
};
// Initialize unlocked spells
self.loadUnlockedSpells();
return self;
});
var Tutorial = Container.expand(function () {
var self = Container.call(this);
// Tutorial state variables
self.currentStep = 0;
self.isActive = false;
self.skipped = false; // Track if tutorial was skipped
self.tutorialSteps = [];
self.highlightElements = [];
self.tutorialTexts = [];
self.arrows = [];
// Tutorial overlay background
var tutorialOverlay = self.attachAsset('startMenuBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 1.0,
scaleY: 1.0
});
tutorialOverlay.alpha = 0.8;
tutorialOverlay.tint = 0x000000;
tutorialOverlay.visible = false; // Initially hidden
tutorialOverlay.zIndex = 1999; // Ensure proper layering
tutorialOverlay.interactive = true; // Always interactive to block clicks
// Define tutorial steps
self.initializeTutorialSteps = function () {
self.tutorialSteps = [{
id: 'welcome',
title: 'BIENVENIDO A WIZARD DEFENDER!',
description: 'Eres un poderoso mago que debe defender su castillo. Toca directamente sobre los enemigos para atacarlos con hechizos.',
duration: 3000,
showSkip: true,
highlightElement: 'screen',
waitForTap: true
}, {
id: 'enemies_approach',
title: 'ENEMIGOS Y MEJORAS',
description: 'Los esqueletos vienen por 5 caminos diferentes. Toca directamente sobre cada enemigo para atacarlo. Gana monedas y desbloquea mejoras cada 12 enemigos.',
duration: 3000,
spawnDemoEnemy: true,
highlightElement: 'coinCounter'
}, {
id: 'tutorial_complete',
title: 'TUTORIAL COMPLETADO!',
description: 'Recuerda: toca directamente sobre los enemigos para atacarlos. Cuida tu salud y sobrevive el mayor tiempo posible. ¡Buena suerte!',
duration: 3000,
startGame: true,
highlightElement: 'healthBar'
}];
};
// Start the tutorial
self.startTutorial = function () {
// Always show tutorial when explicitly called
self.isActive = true;
self.currentStep = 0;
// Initialize tutorial steps first
self.initializeTutorialSteps();
// Make tutorial visible and properly layered
self.visible = true;
self.zIndex = 2000;
// Configure tutorial overlay properly
tutorialOverlay.visible = true;
tutorialOverlay.alpha = 0.8;
tutorialOverlay.tint = 0x000000;
tutorialOverlay.interactive = true; // Block clicks to game below
tutorialOverlay.zIndex = 1999; // Ensure proper layering
// Hide game menu while tutorial is active
if (gameMenu) {
gameMenu.visible = false;
}
// Hide all game elements during tutorial
if (wizard) wizard.visible = false;
if (backgroundMap) backgroundMap.visible = false;
coinText.visible = false;
killCountText.visible = false;
tapText.visible = false;
healthBarBg.visible = false;
healthBar.visible = false;
healthText.visible = false;
for (var i = 0; i < paths.length; i++) {
paths[i].visible = false;
}
// Start first step
self.showStep(0);
return true; // Tutorial started
};
// Show a specific tutorial step
self.showStep = function (stepIndex) {
if (stepIndex >= self.tutorialSteps.length) {
self.completeTutorial();
return;
}
// Clear previous step elements
self.clearStepElements();
var step = self.tutorialSteps[stepIndex];
self.currentStep = stepIndex;
// Ensure tutorial is visible and on top
self.visible = true;
tutorialOverlay.visible = true;
self.zIndex = 2000;
// Create step title with proper sizing
var titleText = new Text2(step.title, {
size: 80,
fill: 0xFFD700,
font: "monospace",
wordWrap: true,
wordWrapWidth: 1600
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 900;
self.addChild(titleText);
self.tutorialTexts.push(titleText);
// Create step description with proper text wrapping
var descText = new Text2(step.description, {
size: 60,
fill: 0xFFFFFF,
font: "monospace",
wordWrap: true,
wordWrapWidth: 1800
});
descText.anchor.set(0.5, 0.5);
descText.x = 2048 / 2;
descText.y = 1300;
self.addChild(descText);
self.tutorialTexts.push(descText);
// Handle special step behaviors
if (step.spawnDemoEnemy) {
self.spawnDemoEnemy();
}
if (step.highlightElement) {
self.highlightElement(step.highlightElement);
}
// Show continue button or wait for specific action
if (step.waitForTap) {
var tapPrompt = new Text2('TOCA LA PANTALLA PARA CONTINUAR', {
size: 50,
fill: 0x00FF00,
font: "monospace",
wordWrap: true,
wordWrapWidth: 1400
});
tapPrompt.anchor.set(0.5, 0.5);
tapPrompt.x = 2048 / 2;
tapPrompt.y = 1800;
self.addChild(tapPrompt);
self.tutorialTexts.push(tapPrompt);
// Add pulsing effect to tap prompt
tween(tapPrompt, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(tapPrompt, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 800,
easing: tween.easeInOut
});
}
});
} else {
// Always show continue prompt - no auto-advance
var continuePrompt = new Text2('TOCA LA PANTALLA PARA CONTINUAR', {
size: 50,
fill: 0x00FF00,
font: "monospace",
wordWrap: true,
wordWrapWidth: 1400
});
continuePrompt.anchor.set(0.5, 0.5);
continuePrompt.x = 2048 / 2;
continuePrompt.y = 1800;
self.addChild(continuePrompt);
self.tutorialTexts.push(continuePrompt);
// Add pulsing effect to continue prompt
tween(continuePrompt, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(continuePrompt, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 800,
easing: tween.easeInOut
});
}
});
}
// Show skip button on first step
if (step.showSkip) {
var skipBtn = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 200,
scaleX: 1.5,
scaleY: 1.5
});
skipBtn.tint = 0xFF4444;
self.highlightElements.push(skipBtn);
var skipText = new Text2('OMITIR', {
size: 50,
fill: 0xFFFFFF,
font: "monospace"
});
skipText.anchor.set(0.5, 0.5);
skipText.x = 2048 - 200;
skipText.y = 200;
self.addChild(skipText);
self.tutorialTexts.push(skipText);
}
};
// Highlight specific game elements
self.highlightElement = function (elementType) {
switch (elementType) {
case 'healthBar':
if (healthBarBg && healthBar) {
// Show health UI temporarily
healthBarBg.visible = true;
healthBar.visible = true;
healthText.visible = true;
// Create highlight glow around health bar
var healthGlow = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: 270,
y: 110,
scaleX: 3,
scaleY: 1.5
}));
healthGlow.tint = 0x00FF00;
healthGlow.alpha = 0.6;
self.highlightElements.push(healthGlow);
// Animate glow
tween(healthGlow, {
alpha: 0.3
}, {
duration: 1000,
easing: tween.easeInOut
});
}
break;
case 'coinCounter':
if (coinText) {
// Show coin UI temporarily
coinText.visible = true;
coinText.setText('Coins: 15'); // Show example coins
// Create highlight glow around coin counter
var coinGlow = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: 270,
y: 150,
scaleX: 3,
scaleY: 1.5
}));
coinGlow.tint = 0xFFD700;
coinGlow.alpha = 0.6;
self.highlightElements.push(coinGlow);
// Animate glow
tween(coinGlow, {
alpha: 0.3
}, {
duration: 1000,
easing: tween.easeInOut
});
}
break;
case 'screen':
// Create large highlight overlay
var screenGlow = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 20,
scaleY: 25
}));
screenGlow.tint = 0x00FFFF;
screenGlow.alpha = 0.2;
self.highlightElements.push(screenGlow);
// Animate screen glow
tween(screenGlow, {
alpha: 0.1
}, {
duration: 1500,
easing: tween.easeInOut
});
// Create arrow pointing to center
self.createArrow(2048 / 2, 2732 / 2 - 400, 2048 / 2, 2732 / 2);
break;
}
};
// Create pointing arrow
self.createArrow = function (fromX, fromY, toX, toY) {
var arrow = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: fromX,
y: fromY,
scaleX: 3,
scaleY: 3
}));
arrow.tint = 0xFFD700;
arrow.alpha = 0.8;
// Calculate arrow direction
var dx = toX - fromX;
var dy = toY - fromY;
var angle = Math.atan2(dy, dx);
arrow.rotation = angle;
// Animate arrow bouncing
tween(arrow, {
x: toX,
y: toY
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(arrow, {
x: fromX,
y: fromY
}, {
duration: 1000,
easing: tween.easeInOut
});
}
});
self.arrows.push(arrow);
};
// Spawn a demo enemy for tutorial
self.spawnDemoEnemy = function () {
// Create a slow-moving demo enemy
var demoEnemy = game.addChild(new Enemy());
demoEnemy.x = 2048 / 2;
demoEnemy.y = -100;
demoEnemy.speed = 1; // Very slow for demo
demoEnemy.health = 1;
demoEnemy.maxHealth = 1;
demoEnemy.pathIndex = 0; // Center path
// Make demo enemy more visible
for (var frameIdx = 0; frameIdx < demoEnemy.animationFrames.length; frameIdx++) {
demoEnemy.animationFrames[frameIdx].tint = 0xFF6600; // Orange tint
}
enemies.push(demoEnemy);
// Create highlight around demo enemy
var enemyGlow = game.addChild(LK.getAsset('projectileGlow', {
anchorX: 0.5,
anchorY: 0.5,
x: demoEnemy.x,
y: demoEnemy.y,
scaleX: 4,
scaleY: 4
}));
enemyGlow.tint = 0xFF0000;
enemyGlow.alpha = 0.5;
// Make glow follow enemy
var _glowFollower = function glowFollower() {
if (demoEnemy && demoEnemy.parent && enemyGlow && enemyGlow.parent) {
enemyGlow.x = demoEnemy.x;
enemyGlow.y = demoEnemy.y;
// Continue following
tween({}, {}, {
duration: 60,
onFinish: _glowFollower
});
} else if (enemyGlow && enemyGlow.parent) {
enemyGlow.destroy();
}
};
_glowFollower();
self.highlightElements.push(enemyGlow);
};
// Clear all tutorial step elements
self.clearStepElements = function () {
// Clear tutorial texts
for (var i = 0; i < self.tutorialTexts.length; i++) {
if (self.tutorialTexts[i] && self.tutorialTexts[i].parent) {
self.tutorialTexts[i].destroy();
}
}
self.tutorialTexts = [];
// Clear highlight elements
for (var i = 0; i < self.highlightElements.length; i++) {
if (self.highlightElements[i] && self.highlightElements[i].parent) {
self.highlightElements[i].destroy();
}
}
self.highlightElements = [];
// Clear arrows
for (var i = 0; i < self.arrows.length; i++) {
if (self.arrows[i] && self.arrows[i].parent) {
self.arrows[i].destroy();
}
}
self.arrows = [];
};
// Advance to next tutorial step
self.nextStep = function () {
self.currentStep++;
if (self.currentStep < self.tutorialSteps.length) {
self.showStep(self.currentStep);
} else {
// Tutorial completed naturally (not skipped)
self.skipped = false;
self.completeTutorial();
}
};
// Complete the tutorial
self.completeTutorial = function () {
// Mark tutorial as completed
storage.tutorialCompleted = true;
// Clear all tutorial elements
self.clearStepElements();
// Hide tutorial completely
tutorialOverlay.visible = false;
tutorialOverlay.interactive = false; // Remove interactivity
self.visible = false;
self.isActive = false;
// Only auto-start game if tutorial was completed naturally (not skipped)
if (!self.skipped) {
// Start game immediately without showing menu
if (gameMenu) {
gameMenu.startGame();
}
} else {
// If skipped, show the menu
if (gameMenu) {
gameMenu.visible = true;
}
}
// Reset skipped flag for next time
self.skipped = false;
};
// Handle tutorial interactions
// Add interaction handler for spell cards in deck view
self.down = function (x, y, obj) {
// Only handle interactions if deck is active or if this is tutorial
if (self.deckMode) {
// Check deck card clicks for spell casting (from current deck)
for (var i = 0; i < self.deckElements.length; i++) {
var element = self.deckElements[i];
if (element.spellId && element.isDeckCard) {
var cardX = element.x;
var cardY = element.y;
if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) {
// Try to cast the spell instead of removing it
if (activeSpellDeck && activeSpellDeck.canCastSpell(element.spellId)) {
var spell = activeSpellDeck.getSpell(element.spellId);
if (spell) {
var targetX = wizard.x;
var targetY = wizard.y - 100;
// Find nearest enemy for targeted spells
if (spell.id === 'fireball' || spell.id === 'iceShard' || spell.id === 'lightning') {
var allEnemies = collisionArrayPool.getAllEnemies();
var nearestEnemy = null;
var nearestDistance = Infinity;
for (var e = 0; e < allEnemies.length; e++) {
var enemy = allEnemies[e];
var dx = enemy.x - wizard.x;
var dy = enemy.y - wizard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestEnemy = enemy;
}
}
if (nearestEnemy) {
targetX = nearestEnemy.x;
targetY = nearestEnemy.y;
}
}
// Cast the spell
activeSpellDeck.castSpell(element.spellId, targetX, targetY);
LK.effects.flashObject(element, 0x00FF00, 200);
// Show cast message
var castText = new Text2('HECHIZO LANZADO!', {
size: 60,
fill: 0x00FF00,
font: "monospace"
});
castText.anchor.set(0.5, 0.5);
castText.x = 2048 / 2;
castText.y = 2200;
self.addChild(castText);
// Animate and remove message
tween(castText, {
alpha: 0,
y: castText.y - 100
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (castText.parent) castText.destroy();
}
});
}
} else {
LK.effects.flashObject(element, 0xFF0000, 200);
// Show "cannot cast" message
var errorText = new Text2('NO SE PUEDE LANZAR', {
size: 50,
fill: 0xFF6666,
font: "monospace"
});
errorText.anchor.set(0.5, 0.5);
errorText.x = 2048 / 2;
errorText.y = 2200;
self.addChild(errorText);
// Animate and remove message
tween(errorText, {
alpha: 0,
y: errorText.y - 100
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (errorText.parent) errorText.destroy();
}
});
}
return;
}
}
}
// Continue with existing deck menu handling for adding/removing cards
// Check deck card clicks with better hit detection for removal
for (var i = 0; i < self.deckElements.length; i++) {
var element = self.deckElements[i];
if (element.spellId && element.isDeckCard) {
var cardX = element.x;
var cardY = element.y;
// Use different area for removal (bottom part of card)
if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY + 100 && y <= cardY + 225) {
// Visual feedback before removal
LK.effects.flashObject(element, 0xFF0000, 300);
// Remove from deck
if (self.spellDeck.removeFromDeck(element.spellId)) {
self.refreshDeckDisplay();
LK.effects.flashScreen(0xFF8800, 200);
// Show removal message
var removeText = new Text2('HECHIZO REMOVIDO', {
size: 60,
fill: 0xFF6666,
font: "monospace"
});
removeText.anchor.set(0.5, 0.5);
removeText.x = 2048 / 2;
removeText.y = 2200;
self.addChild(removeText);
// Animate and remove message
tween(removeText, {
alpha: 0,
y: removeText.y - 100
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (removeText.parent) removeText.destroy();
}
});
} else {
LK.effects.flashScreen(0xFF0000, 200);
}
return;
}
}
}
// Continue with existing available cards handling...
// Check available card clicks with better hit detection
for (var i = 0; i < self.availableElements.length; i++) {
var element = self.availableElements[i];
if (element.spellId && !element.isDeckCard) {
var cardX = element.x;
var cardY = element.y;
if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) {
// Visual feedback before addition
LK.effects.flashObject(element, 0x00FF00, 300);
// Add to deck
if (self.spellDeck.addToDeck(element.spellId)) {
self.refreshDeckDisplay();
LK.effects.flashScreen(0x00FF00, 200);
// Show addition message
var addText = new Text2('HECHIZO AÑADIDO', {
size: 60,
fill: 0x66FF66,
font: "monospace"
});
addText.anchor.set(0.5, 0.5);
addText.x = 2048 / 2;
addText.y = 2200;
self.addChild(addText);
// Animate and remove message
tween(addText, {
alpha: 0,
y: addText.y - 100
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (addText.parent) addText.destroy();
}
});
} else {
LK.effects.flashScreen(0xFF0000, 200);
// Show error message
var errorText = new Text2('DECK LLENO (MAX 5)', {
size: 50,
fill: 0xFF6666,
font: "monospace"
});
errorText.anchor.set(0.5, 0.5);
errorText.x = 2048 / 2;
errorText.y = 2200;
self.addChild(errorText);
// Animate and remove message
tween(errorText, {
alpha: 0,
y: errorText.y - 100
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (errorText.parent) errorText.destroy();
}
});
}
return;
}
}
}
// Deck back button
if (y >= 2350 && y <= 2650) {
self.hideDeck();
}
// Block all other interactions when deck menu is active
return;
}
// Tutorial handling (moved after deck handling)
if (!self.isActive) return;
var step = self.tutorialSteps[self.currentStep];
// Handle skip button (top-right corner)
if (step.showSkip && x >= 2048 - 300 && x <= 2048 - 100 && y >= 100 && y <= 300) {
self.skipped = true; // Mark as skipped
self.completeTutorial();
return;
}
// For any tap anywhere on screen, advance to next step
if (step.id === 'tap_to_attack') {
// Simulate spell casting for demo
if (wizard) {
wizard.attack(0); // Attack center path
}
// Wait a moment then advance
tween({}, {}, {
duration: 1000,
onFinish: function onFinish() {
self.nextStep();
}
});
} else {
// For all other steps, advance immediately on any tap
self.nextStep();
}
};
return self;
});
// Impact effect function now handled by BaseDamageHandler
// Unified death handler for all enemy types using enemy configuration
var UnifiedDeathHandler = Container.expand(function () {
var self = Container.call(this);
// Execute enemy death with consolidated coin and reward logic
self.executeEnemyDeath = function (enemy, enemyArray) {
enemy.animationState = 'dying';
enemy.currentFrame = 3;
LK.getSound('painSound').play();
enemy.isDying = true;
// Use enemy's own type configuration for death properties
var config = enemy.typeConfig;
var deathRotation = config.deathRotation || Math.PI * 0.5;
var numCoins = config.coinReward || 1;
// Special cleanup for mini boss UI elements
if (enemy.enemyType === 'miniBoss') {
self.cleanupMiniBossUI(enemy);
}
// Execute unified death animation
tween(enemy, {
alpha: 0,
scaleX: 0.3,
scaleY: 0.3,
rotation: deathRotation
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
self.handleDeathRewards(enemy, numCoins);
self.updateProgression(enemy);
self.cleanupEnemy(enemy, enemyArray);
}
});
};
// Clean up mini boss UI elements
self.cleanupMiniBossUI = function (enemy) {
if (enemy.healthBarBg && enemy.healthBarBg.parent) {
enemy.healthBarBg.destroy();
}
if (enemy.healthBarFg && enemy.healthBarFg.parent) {
enemy.healthBarFg.destroy();
}
if (enemy.healthText && enemy.healthText.parent) {
enemy.healthText.destroy();
}
};
// Handle coin drops and difficulty-based rewards using pooled coins
self.handleDeathRewards = function (enemy, numCoins) {
var selectedDifficulty = storage.difficulty || 'NORMAL';
for (var coinIdx = 0; coinIdx < numCoins; coinIdx++) {
// Get coin from pool with proper initialization
var coin = globalObjectPool.getObject('coin', {
x: enemy.x + (enemy.enemyType === 'miniBoss' ? (Math.random() - 0.5) * 200 : 0),
y: enemy.y - 50 + (enemy.enemyType === 'miniBoss' ? (Math.random() - 0.5) * 100 : 0),
isAnimating: true,
bobOffset: Math.random() * Math.PI * 2,
initialY: 0,
visible: true,
alpha: 1.0
});
if (!coin) {
// Fallback to direct creation if pool fails
coin = new Coin();
coin.x = enemy.x + (enemy.enemyType === 'miniBoss' ? (Math.random() - 0.5) * 200 : 0);
coin.y = enemy.y - 50 + (enemy.enemyType === 'miniBoss' ? (Math.random() - 0.5) * 100 : 0);
coin.isAnimating = true;
game.addChild(coin);
} else {
game.addChild(coin);
}
coins.push(coin);
var coinTargetX = 120 + coinText.width / 2;
var coinTargetY = 90 + coinText.height / 2;
tween(coin, {
x: coinTargetX,
y: coinTargetY,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 1000 + (enemy.enemyType === 'miniBoss' ? coinIdx * 200 : 0),
easing: tween.easeOut,
onFinish: function onFinish() {
self.processCoinReward(enemy, selectedDifficulty, coin);
}
});
}
};
// Process coin rewards with difficulty modifiers
self.processCoinReward = function (enemy, selectedDifficulty, coin) {
var coinReward = enemy.enemyType === 'miniBoss' ? 10 : 1;
if (selectedDifficulty === 'FACIL') {
coinReward = Math.floor(coinReward * 1.5);
} else if (selectedDifficulty === 'DIFICIL') {
coinReward = Math.max(1, Math.floor(coinReward * 0.75));
}
coinCounter += coinReward;
coinText.setText('Coins: ' + coinCounter);
// Easy difficulty healing bonus
if (selectedDifficulty === 'FACIL' && Math.random() < 0.15) {
wizard.health = Math.min(wizard.health + 5, wizard.maxHealth);
updateHealthBar();
LK.effects.flashObject(wizard, 0x00FF00, 200);
}
// Remove coin from tracking array
for (var i = coins.length - 1; i >= 0; i--) {
if (coins[i] === coin) {
coins.splice(i, 1);
break;
}
}
// Return coin to pool instead of destroying
if (coin.pooled) {
globalObjectPool.returnObject(coin);
} else {
coin.destroy();
}
};
// Update kill counter and experience progression
self.updateProgression = function (enemy) {
var killIncrement = enemy.enemyType === 'miniBoss' ? 10 : 1;
enemyKillCounter += killIncrement;
killCountText.setText('Puntuacion: ' + enemyKillCounter);
wizard.gainExperience(enemy.enemyType === 'miniBoss' ? 250 : 25);
if (selectedEnemy === enemy) {
selectedEnemy = null;
}
};
// Clean up enemy from arrays and game
self.cleanupEnemy = function (enemy, enemyArray) {
// Remove from appropriate array
for (var i = enemyArray.length - 1; i >= 0; i--) {
if (enemyArray[i] === enemy) {
enemyArray.splice(i, 1);
break;
}
}
// Also remove from global enemy manager collections
globalEnemyManager.removeFromCollection(enemy, enemy.enemyType);
// Remove from all legacy arrays to ensure proper cleanup
var allArrays = [enemies, ogres, knights, miniBosses];
for (var arrayIdx = 0; arrayIdx < allArrays.length; arrayIdx++) {
var array = allArrays[arrayIdx];
for (var i = array.length - 1; i >= 0; i--) {
if (array[i] === enemy) {
array.splice(i, 1);
break;
}
}
}
enemy.destroy();
LK.setScore(LK.getScore() + enemy.typeConfig.scoreReward);
};
return self;
});
// UpgradeMenu class removed - using spell deck system instead
// Unified Projectile Factory using consolidated GAME_CONFIG.projectiles
var Wizard = Container.expand(function () {
var self = Container.call(this);
// Animation system for wizard
self.currentFrame = 1;
self.animationTimer = 0;
self.animationSpeed = 18; // Change frame every 18 ticks (300ms at 60fps)
// Create all wizard graphics frames and store them
self.wizardFrames = [];
for (var i = 1; i <= 4; i++) {
var frameGraphics = self.attachAsset('wizard' + i, {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 2.5,
scaleY: 2.5
});
frameGraphics.visible = i === 1; // Only show first frame initially
self.wizardFrames.push(frameGraphics);
}
// Create invisible hitbox with much smaller size for more precise collision
var hitbox = self.attachAsset('wizard1', {
anchorX: 0.3,
anchorY: 1.0,
scaleX: 0.25,
// Much smaller size for very precise collision
scaleY: 0.3 // Much smaller size for very precise collision
});
hitbox.alpha = 0; // Make hitbox invisible
// Position hitbox slightly to the right to reduce left side
hitbox.x = 15; // Offset hitbox to the right
// Create shield visual effect
self.shieldGraphics = self.attachAsset('shield', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 3
});
self.shieldGraphics.alpha = 0.7;
self.shieldGraphics.visible = false;
self.attackCooldown = 0;
self.level = 1;
self.experience = 0;
self.health = 100;
self.maxHealth = 100;
self.shieldActive = false; // Track shield status
// Upgrade system removed - simplified wizard properties
// Override intersects method to use smaller hitbox
self.intersects = function (other) {
return hitbox.intersects(other);
};
self.update = function () {
// Pause wizard when tutorial is active
if (tutorial && tutorial.isActive) {
return;
}
// Upgrade menu removed - no pause needed
if (self.attackCooldown > 0) {
self.attackCooldown--;
}
// Update shield visibility based on shield status
self.shieldGraphics.visible = self.shieldActive;
if (self.shieldActive) {
// Animate shield with pulsing effect
var pulse = 1 + Math.sin(LK.ticks * 0.15) * 0.2;
self.shieldGraphics.scaleX = 3 * pulse;
self.shieldGraphics.scaleY = 3 * pulse;
// Slowly rotate shield
self.shieldGraphics.rotation += 0.03;
// Add glowing effect
self.shieldGraphics.alpha = 0.6 + Math.sin(LK.ticks * 0.1) * 0.2;
}
// Upgrade-based abilities removed - using spell deck system instead
// Optimized animation system with performance awareness
self.animationTimer++;
var adjustedSpeed = self.animationSpeed;
// Apply performance-based animation adjustments
if (globalAnimationManager && globalAnimationManager.skipConfig.skipLevel > 0) {
var skipMultiplier = 1 + globalAnimationManager.skipConfig.skipLevel * 0.3;
adjustedSpeed = Math.floor(adjustedSpeed * skipMultiplier);
}
if (self.animationTimer >= adjustedSpeed) {
self.animationTimer = 0;
// Calculate advanced frame transition configuration
var transitionConfig = self.calculateFrameTransitionConfig(adjustedSpeed);
var currentFrame = self.wizardFrames[self.currentFrame - 1];
var nextFrameIndex = self.currentFrame >= 4 ? 0 : self.currentFrame;
var nextFrame = self.wizardFrames[nextFrameIndex];
// Advanced frame transition system with multiple tween effects
if (transitionConfig.useAdvancedTransition) {
self.performAdvancedFrameTransition(currentFrame, nextFrame, transitionConfig);
} else if (transitionConfig.useBasicTransition) {
self.performBasicFrameTransition(currentFrame, nextFrame, transitionConfig);
} else {
// Instant transition for low performance
currentFrame.visible = false;
nextFrame.visible = true;
}
// Move to next frame (walking state)
self.currentFrame++;
if (self.currentFrame > 4) {
self.currentFrame = 1;
}
}
};
self.attack = function (direction) {
if (self.attackCooldown <= 0) {
// Default direction if none specified
if (direction === undefined) {
direction = 0; // Default to center path
}
// Get attack angle based on path direction
var attackAngle = pathAngles[direction];
var attackDistance = 100;
// Calculate spell position based on attack direction
var spellX = self.x + Math.cos(attackAngle) * attackDistance;
var spellY = self.y + Math.sin(attackAngle) * attackDistance;
// Create spell effect
var spell = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: spellX,
y: spellY,
scaleX: 0.5,
scaleY: 0.5
}));
// Animate spell with magical effects
tween(spell, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
spell.destroy();
}
});
// Add rotation animation to spell
tween(spell, {
rotation: Math.PI * 2
}, {
duration: 500,
easing: tween.linear
});
self.attackCooldown = 30; // 0.5 seconds at 60fps
LK.getSound('spellCast').play();
// Base damage for wizard attack
var totalDamage = 1;
// Attack enemies in the specified direction/path
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.pathIndex === direction) {
// Only hit enemies on exact same path - no distance validation
enemy.takeDamage(totalDamage);
}
}
// Attack ogres in the specified direction/path
for (var i = ogres.length - 1; i >= 0; i--) {
var ogre = ogres[i];
if (ogre.pathIndex === direction) {
// Only hit ogres on exact same path - no distance validation
ogre.takeDamage(totalDamage);
}
}
// Attack knights in the specified direction/path
for (var i = knights.length - 1; i >= 0; i--) {
var knight = knights[i];
if (knight.pathIndex === direction) {
// Only hit knights on exact same path - no distance validation
knight.takeDamage(totalDamage);
}
}
return true;
}
return false;
};
self.gainExperience = function (amount) {
self.experience += amount;
var expNeeded = self.level * 100;
if (self.experience >= expNeeded) {
self.levelUp();
}
};
self.levelUp = function () {
self.level++;
self.experience = 0;
// Visual level up effect
LK.effects.flashObject(self, 0xFFD700, 500);
};
self.takeDamage = function (damage) {
// Check if teleport invulnerability is active
if (self.teleportInvuln) {
globalDamageHandler.createFlashEffect(self, 0x8000FF, 200);
return;
}
// Check if shield is active
if (self.shieldActive) {
// Initialize shield properties if not set
if (self.maxShieldHits === undefined) {
self.maxShieldHits = 1;
self.currentShieldHits = 0;
}
// Increment shield hits
self.currentShieldHits++;
// Visual feedback for shield use
globalDamageHandler.createFlashEffect(self, 0x00BFFF, 300);
// Check if shield is depleted
if (self.currentShieldHits >= self.maxShieldHits) {
self.shieldActive = false;
// Start shield regeneration timer
var regenTime = self.shieldRegen ? 5000 : 10000; // Faster regen if improved
tween({}, {}, {
duration: regenTime,
onFinish: function onFinish() {
// Regenerate shield
self.shieldActive = true;
self.currentShieldHits = 0;
// Visual feedback for shield regeneration
globalDamageHandler.createFlashEffect(self, 0x00BFFF, 500);
// Add shield regeneration animation
tween(self.shieldGraphics, {
scaleX: 5,
scaleY: 5,
alpha: 1.0
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.shieldGraphics, {
scaleX: 3,
scaleY: 3,
alpha: 0.7
}, {
duration: 400,
easing: tween.easeIn
});
}
});
}
});
}
// No damage taken, shield absorbed it
return;
}
// Use unified damage handler for core damage logic
self.health -= damage;
globalDamageHandler.createFlashEffect(self, 0xFF0000, 200);
if (self.health <= 0) {
self.health = 0;
// 10% chance to revive when dying
var reviveChance = Math.random();
if (reviveChance < 0.10) {
// Revival successful!
self.health = Math.floor(self.maxHealth * 0.5); // Revive with 50% health
// Destroy ALL enemies when revival activates (no distance restriction)
var allEnemies = collisionArrayPool.getAllEnemies();
for (var enemyIdx = allEnemies.length - 1; enemyIdx >= 0; enemyIdx--) {
var enemy = allEnemies[enemyIdx];
// Create destruction effect for each enemy
globalDamageHandler.createFlashEffect(enemy, 0xFFD700, 500);
// Create golden explosion particles
globalDamageHandler.createVisualEffect('explosion', enemy, {
explosionColor: 0xFFD700,
explosionScale: 4.0
});
// Kill ALL enemies instantly by calling die() method
enemy.die();
}
// Visual effects for revival
LK.effects.flashScreen(0x00FF00, 1500); // Green flash for revival
globalDamageHandler.createFlashEffect(self, 0xFFD700, 1000); // Golden flash on wizard
// Create healing aura effect
globalDamageHandler.createVisualEffect('explosion', self, {
explosionColor: 0x00FF00,
explosionScale: 8.0
});
// Play spell cast sound for revival
LK.getSound('spellCast').play();
// Update health bar to show revival
updateHealthBar();
} else {
// Game over when health reaches 0 and no revival
LK.effects.flashScreen(0xFF0000, 1000);
LK.showGameOver();
}
}
// Update health bar
updateHealthBar();
// Simplified screen shake for better performance
var shakeIntensity = 8;
var originalX = game.x;
var originalY = game.y;
// Simple single shake effect
tween(game, {
x: originalX + shakeIntensity,
y: originalY + shakeIntensity * 0.5
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(game, {
x: originalX,
y: originalY
}, {
duration: 100,
easing: tween.easeIn
});
}
});
};
self.activateForcePush = function () {
// Visual effect for force push activation
LK.effects.flashScreen(0x8A2BE2, 300); // Purple flash
LK.effects.flashObject(self, 0x8A2BE2, 500); // Purple flash on wizard
// Push back all enemies with improved effects
var allEnemies = collisionArrayPool.getAllEnemies();
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
// Calculate direction from wizard to enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Improved force push: stronger push and damage
var pushDistance = upgradeLevels.forcePush > 1 ? 300 : 200; // Stronger push
var pushX = dx / distance * pushDistance;
var pushY = dy / distance * pushDistance;
// Calculate new position
var newX = enemy.x + pushX;
var newY = enemy.y + pushY;
// Ensure enemies don't go off screen
newX = Math.max(50, Math.min(1998, newX));
newY = Math.max(-100, Math.min(2732, newY));
// Animate the push effect
tween(enemy, {
x: newX,
y: newY
}, {
duration: 300,
easing: tween.easeOut
});
// Improved force push: deal damage
if (upgradeLevels.forcePush > 1) {
enemy.takeDamage(50);
}
// Visual effect on each enemy
LK.effects.flashObject(enemy, 0x8A2BE2, 200);
}
}
};
self.activateFreezePulse = function () {
// Visual effect for freeze pulse activation
LK.effects.flashScreen(0x87CEEB, 500); // Light blue flash
LK.effects.flashObject(self, 0x87CEEB, 700); // Light blue flash on wizard
// Play freeze sound effect
LK.getSound('iceFreeze').play();
// Freeze all enemies with improved effects
var allEnemies = collisionArrayPool.getAllEnemies();
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
// Improved freeze: longer duration and damage
var freezeDuration = upgradeLevels.freezePulse > 1 ? 120 : 60; // 2s vs 1s
enemy.frozen = true;
enemy.frozenTimer = freezeDuration;
// Improved freeze: deal damage
if (upgradeLevels.freezePulse > 1) {
enemy.takeDamage(30);
}
// Visual freeze effect - tint enemy light blue
tween(enemy, {
tint: 0x87CEEB
}, {
duration: 100,
easing: tween.easeOut
});
// Reduced ice crystal particles for better performance
if (i % 2 === 0) {
// Only create effects for every other enemy
for (var iceIdx = 0; iceIdx < 3; iceIdx++) {
var iceCrystal = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: enemy.x + (Math.random() - 0.5) * 60,
y: enemy.y + (Math.random() - 0.5) * 60,
scaleX: 1.0,
scaleY: 1.0
}));
iceCrystal.tint = 0x87CEEB;
iceCrystal.alpha = 0.9;
// Create floating ice effect
tween(iceCrystal, {
y: iceCrystal.y - 30,
scaleX: 0.3,
scaleY: 0.3,
alpha: 0
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
iceCrystal.destroy();
}
});
}
}
// Remove freeze tint after frozen state ends
var visualDuration = upgradeLevels.freezePulse > 1 ? 2000 : 1000;
tween({}, {}, {
duration: visualDuration,
onFinish: function onFinish() {
if (enemy && enemy.parent) {
// Remove freeze tint after frozen effect ends
tween(enemy, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeIn
});
}
}
});
}
};
self.activateThorns = function () {
// Visual effect for thorns activation
LK.effects.flashScreen(0x8B4513, 300); // Brown flash
LK.effects.flashObject(self, 0x8B4513, 500); // Brown flash on wizard
// Find the closest enemy to the wizard
var closestEnemy = null;
var closestDistance = Infinity;
var allEnemies = collisionArrayPool.getAllEnemies();
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// Only create spikes if there is a closest enemy
if (!closestEnemy) {
LK.getSound('spellCast').play();
return;
}
// Create spikes only along the closest enemy's path
var pathIdx = closestEnemy.pathIndex;
var pathAngle = pathAngles[pathIdx];
// Calculate spawn position for this path (same as enemy spawning)
var spawnX, spawnY;
if (pathIdx === 0) {
// Center path - spawn at top edge
spawnX = 2048 / 2;
spawnY = -100;
} else if (pathIdx === 1) {
// Path 2 - spawn at top right edge
spawnX = 2048 + 50;
spawnY = -50;
} else if (pathIdx === 2) {
// Path 3 - spawn at top left edge
spawnX = -50;
spawnY = -50;
} else if (pathIdx === 3) {
// Path 4 - spawn at left edge
spawnX = -100;
spawnY = 2732 / 2 + 400;
} else if (pathIdx === 4) {
// Path 5 - spawn at right edge
spawnX = 2048 + 100;
spawnY = 2732 / 2 + 400;
}
// Calculate wizard position (same as enemy targeting)
var wizardX = self.x;
var wizardY = self.y;
// Calculate path distance and divide into 3 sections with gaps
var pathDistance = Math.sqrt((spawnX - wizardX) * (spawnX - wizardX) + (spawnY - wizardY) * (spawnY - wizardY));
var sectionLength = pathDistance / 5; // Each section is 1/5 of total path
var gapLength = pathDistance / 10; // Gaps are 1/10 of total path
// Define 3 sections along the path with gaps between them
var sections = [{
start: 0.1,
end: 0.3
},
// First section: 10% to 30% along path
{
start: 0.45,
end: 0.65
},
// Second section: 45% to 65% along path
{
start: 0.8,
end: 1.0
} // Third section: 80% to 100% along path
];
// Create spikes in reverse sequential order: last section, then middle, then first
var sectionOrder = [2, 1, 0]; // Create sections starting from farthest outward toward wizard
for (var orderIdx = 0; orderIdx < sectionOrder.length; orderIdx++) {
var sectionIdx = sectionOrder[orderIdx];
var section = sections[sectionIdx];
var sectionStartDistance = pathDistance * section.start;
var sectionEndDistance = pathDistance * section.end;
var spikeSpacing = 150; // Distance between spikes within each section
// Calculate number of spikes in this section
var sectionLength = sectionEndDistance - sectionStartDistance;
var numSpikesInSection = Math.floor(sectionLength / spikeSpacing);
// Calculate delay for sequential appearance
var baseDelay = orderIdx * 300; // 300ms delay between sections
// Create spikes within this section with sequential timing
for (var s = 0; s < numSpikesInSection; s++) {
var spikeDistanceInSection = s * spikeSpacing + spikeSpacing / 2;
var totalSpikeDistance = sectionStartDistance + spikeDistanceInSection;
var progress = totalSpikeDistance / pathDistance;
var spikeX = spawnX + (wizardX - spawnX) * progress;
var spikeY = spawnY + (wizardY - spawnY) * progress;
// Only create spike if position is within game bounds
if (spikeX >= 0 && spikeX <= 2048 && spikeY >= 0 && spikeY <= 2732) {
// Create spike with delay
(function (delayTime, spikeX, spikeY, pathIdx) {
tween({}, {}, {
duration: delayTime,
onFinish: function onFinish() {
var spike = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: spikeX,
y: spikeY,
scaleX: 0.1,
scaleY: 0.1
}));
spike.tint = 0x8B4513; // Brown color for thorns
spike.pathIndex = pathIdx;
// Initialize hit tracking for this spike
spike.hitEnemies = [];
// Set spike to be visible immediately
spike.alpha = 1.0;
spike.scaleX = 1.5;
spike.scaleY = 1.5;
// Animate spike emerging from ground
tween(spike, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 1.0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Spike stays for a moment then disappears
tween(spike, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 800,
easing: tween.easeIn,
onFinish: function onFinish() {
spike.destroy();
}
});
}
});
}
});
})(baseDelay + s * 50, spikeX, spikeY, pathIdx); // 50ms delay between spikes in same section
}
}
}
LK.getSound('spellCast').play();
};
self.launchFireBall = function () {
// Visual effect for fire ball launch
LK.effects.flashScreen(0xFF4500, 300); // Orange flash
LK.effects.flashObject(self, 0xFF4500, 500); // Orange flash on wizard
// Find closest enemy to target
var closestEnemy = null;
var closestDistance = Infinity;
var allEnemies = collisionArrayPool.getAllEnemies();
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// Create fire ball projectile using unified factory
var targetX = closestEnemy ? closestEnemy.x : self.x;
var targetY = closestEnemy ? closestEnemy.y : self.y - 100;
var fireBall = ProjectileFactory.createProjectile('fireBall', self.x, self.y, targetX, targetY);
LK.getSound('spellCast').play();
LK.getSound('fireWhoosh').play();
};
// Calculate frame transition configuration based on performance metrics
self.calculateFrameTransitionConfig = function (adjustedSpeed) {
var skipLevel = globalAnimationManager ? globalAnimationManager.skipConfig.skipLevel : 0;
var avgFrameTime = globalAnimationManager ? globalAnimationManager.performanceMetrics.avgFrameTime : 16.67;
var config = {
useAdvancedTransition: false,
useBasicTransition: false,
duration: adjustedSpeed * 0.3,
easing: tween.easeOut
};
// Determine transition complexity based on performance
if (skipLevel === 0 && avgFrameTime < 18) {
// Excellent performance: advanced transitions with multiple effects
config.useAdvancedTransition = true;
config.duration = adjustedSpeed * 0.4;
config.easing = tween.easeInOut;
} else if (skipLevel <= 1 && avgFrameTime < 25) {
// Good performance: basic smooth transitions
config.useBasicTransition = true;
config.duration = adjustedSpeed * 0.3;
config.easing = tween.easeOut;
}
return config;
};
// Perform advanced frame transition with sophisticated tween effects
self.performAdvancedFrameTransition = function (currentFrame, nextFrame, config) {
// Phase 1: Current frame fade and scale out
tween(currentFrame, {
alpha: 0,
scaleX: currentFrame.scaleX * 0.95,
scaleY: currentFrame.scaleY * 0.95
}, {
duration: config.duration * 0.5,
easing: tween.easeIn,
onFinish: function onFinish() {
currentFrame.visible = false;
currentFrame.alpha = 1;
currentFrame.scaleX = 2.5; // Reset scale
currentFrame.scaleY = 2.5;
}
});
// Phase 2: Next frame scale and fade in
nextFrame.visible = true;
nextFrame.alpha = 0;
nextFrame.scaleX = 2.5 * 1.05; // Start slightly larger
nextFrame.scaleY = 2.5 * 1.05;
tween(nextFrame, {
alpha: 1,
scaleX: 2.5,
scaleY: 2.5
}, {
duration: config.duration * 0.7,
easing: tween.bounceOut,
delay: config.duration * 0.3 // Start after current frame begins fading
});
// Phase 3: Subtle magical sparkle effect for enhanced visual appeal
self.createFrameTransitionSparkle(nextFrame, config);
};
// Perform basic frame transition with smooth fade effect
self.performBasicFrameTransition = function (currentFrame, nextFrame, config) {
// Smooth fade out current frame
tween(currentFrame, {
alpha: 0
}, {
duration: config.duration,
easing: config.easing,
onFinish: function onFinish() {
currentFrame.visible = false;
currentFrame.alpha = 1;
}
});
// Smooth fade in next frame
nextFrame.visible = true;
nextFrame.alpha = 0;
tween(nextFrame, {
alpha: 1
}, {
duration: config.duration,
easing: tween.easeIn
});
};
// Create subtle sparkle effect for frame transitions
self.createFrameTransitionSparkle = function (frame, config) {
var sparkleCount = 3;
for (var i = 0; i < sparkleCount; i++) {
var sparkle = globalObjectPool ? globalObjectPool.getObject('effect', {
x: frame.x + (Math.random() - 0.5) * 40,
y: frame.y + (Math.random() - 0.5) * 40,
scaleX: 0.1,
scaleY: 0.1,
anchorX: 0.5,
anchorY: 0.5
}) : null;
if (!sparkle) {
sparkle = game.addChild(LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5,
x: frame.x + (Math.random() - 0.5) * 40,
y: frame.y + (Math.random() - 0.5) * 40,
scaleX: 0.1,
scaleY: 0.1
}));
} else {
game.addChild(sparkle);
}
sparkle.tint = 0xFFD700;
sparkle.alpha = 0.8;
// Animate sparkle with delayed start
var delay = i * 100;
tween({}, {}, {
duration: delay,
onFinish: function (sparkleRef) {
return function () {
tween(sparkleRef, {
scaleX: 0.5,
scaleY: 0.5,
alpha: 1.0,
rotation: Math.PI * 2
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(sparkleRef, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
if (sparkleRef.pooled && globalObjectPool) {
globalObjectPool.returnObject(sparkleRef);
} else if (sparkleRef.parent) {
sparkleRef.destroy();
}
}
});
}
});
};
}(sparkle)
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000 // Black background for pixel art
});
/****
* Game Code
****/
// Game Constants
/****
* Constants & Configuration
****/
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _defineProperty(e, r, t) {
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: !0,
configurable: !0,
writable: !0
}) : e[r] = t, e;
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == _typeof(i) ? i : i + "";
}
function _toPrimitive(t, r) {
if ("object" != _typeof(t) || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != _typeof(i)) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
var GAME_CONSTANTS = {
SCREEN_WIDTH: 2048,
SCREEN_HEIGHT: 2732,
MAX_ENEMIES: 25,
SEGMENT_SIZE: 80,
PATH_COUNT: 5,
COOLDOWN_DURATION: 300
};
// Simple Enemy Configuration
var ENEMY_CONFIGS = {
skeleton: {
health: 100,
speed: 3,
damage: 20,
spawnInterval: {
FACIL: {
base: 120,
min: 60,
scaling: 5
},
NORMAL: {
base: 90,
min: 40,
scaling: 6
},
DIFICIL: {
base: 60,
min: 20,
scaling: 4
}
},
startThreshold: 0
},
ogre: {
health: 200,
speed: 2.5,
damage: 30,
spawnInterval: {
FACIL: 240,
NORMAL: 180,
DIFICIL: 120
},
startThreshold: 15
},
knight: {
health: 300,
speed: 2,
damage: 40,
spawnInterval: {
FACIL: 400,
NORMAL: 300,
DIFICIL: 200
},
startThreshold: 30
},
miniBoss: {
health: 3000,
speed: 4,
damage: 75,
spawnInterval: {
FACIL: 80,
NORMAL: 60,
DIFICIL: 40
},
startThreshold: 80
}
};
// Simple UI Positions
var UI_POSITIONS = _defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty({
healthBar: {
x: 120,
y: 20
},
healthBarBg: {
x: 120,
y: 20,
scaleX: 2,
scaleY: 1
},
coinText: {
x: 120,
y: 90
},
killText: {
x: 120,
y: 50,
size: 60,
fill: 0xFFFFFF,
font: "monospace"
},
tapText: {
size: 80,
fill: 0x00FF00,
font: "monospace",
yOffset: -400
},
healthText: {
x: 120,
y: 65,
size: 60,
fill: 0xFFFFFF,
font: "monospace"
}
}, "coinText", {
x: 120,
y: 90,
size: 60,
fill: 0xFFD700,
font: "monospace"
}), "manaBar", {
x: -300,
y: 20,
scaleX: 2,
scaleY: 1
}), "manaBarBg", {
x: -300,
y: 20,
scaleX: 2,
scaleY: 1
}), "manaText", {
x: -300,
y: 65,
size: 60,
fill: 0x4169E1,
font: "monospace"
}), "spellSlots", {
count: 5,
startX: -400,
y: -100,
spacing: 160,
scaleX: 2,
scaleY: 2
});
// Path Configuration
var PATH_CONFIG = {
angles: [-Math.PI / 2, -Math.PI / 3, -2 * Math.PI / 3, Math.PI / 6, 5 * Math.PI / 6],
spawnPositions: [{
x: GAME_CONSTANTS.SCREEN_WIDTH / 2,
y: -100
}, {
x: GAME_CONSTANTS.SCREEN_WIDTH + 50,
y: -50
}, {
x: -50,
y: -50
}, {
x: -100,
y: GAME_CONSTANTS.SCREEN_HEIGHT / 2 + 400
}, {
x: GAME_CONSTANTS.SCREEN_WIDTH + 100,
y: GAME_CONSTANTS.SCREEN_HEIGHT / 2 + 400
}]
};
/****
* Unified Game Configuration
****/
var GAME_CONFIG = {
// Single responsibility: Enemy configuration
enemies: ENEMY_CONFIGS,
createEnemyConfig: function createEnemyConfig(type, overrides) {
var base = ENEMY_CONFIGS[type] || ENEMY_CONFIGS.skeleton;
var config = {
assetPrefix: type === 'skeleton' ? 'esqueleto' : type,
scale: type === 'miniBoss' ? 5.0 : 3.0,
baseHealth: base.health,
baseSpeed: base.speed,
damage: base.damage,
coinReward: type === 'miniBoss' ? 5 : 1,
scoreReward: type === 'skeleton' ? 10 : type === 'ogre' ? 15 : type === 'knight' ? 20 : 100
};
if (overrides) {
for (var key in overrides) {
config[key] = overrides[key];
}
}
return config;
},
// Single responsibility: UI layout
ui: UI_POSITIONS,
// Single responsibility: Path system
paths: PATH_CONFIG,
// Single responsibility: Spell configuration
spells: {
fireball: {
baseDamage: 150,
targetType: 'enemy'
},
iceShard: {
baseDamage: 100,
targetType: 'enemy'
},
lightning: {
baseDamage: 200,
targetType: 'chain',
chainTargets: 3
},
heal: {
baseHealing: 50,
targetType: 'self'
},
shield: {
targetType: 'self',
shieldStrength: 3
},
meteor: {
baseDamage: 500,
targetType: 'area'
},
teleport: {
targetType: 'position'
},
timeSlow: {
targetType: 'all',
slowAmount: 0.3
}
}
};
/****
* Global State Management
****/
var gameState = {
// Game flags
gameStarted: false,
// Counters
coinCounter: 0,
enemyKillCounter: 0,
// Enemy arrays
enemies: [],
ogres: [],
knights: [],
miniBosses: [],
coins: [],
projectiles: [],
// Selected objects
selectedEnemy: null,
// Path tracking
pathLastSpawnTime: [-1, -1, -1, -1, -1],
pathConsecutiveSpawns: [0, 0, 0, 0, 0],
lastSpawnedPath: -1
};
/****
* Global Systems
****/
// Create global object pool instance for optimized memory management
var globalObjectPool = new ObjectPool();
// Create global animation manager for intelligent frame skipping
var globalAnimationManager = new AnimationManager();
// Simple Collision Array Pool
var collisionArrayPool = {
allEnemies: [],
clearArray: function clearArray(arrayName) {
if (this[arrayName]) {
this[arrayName].length = 0;
}
return this[arrayName];
},
getAllEnemies: function getAllEnemies() {
var array = this.clearArray('allEnemies');
for (var i = 0; i < gameState.enemies.length; i++) {
array.push(gameState.enemies[i]);
}
for (var i = 0; i < gameState.ogres.length; i++) {
array.push(gameState.ogres[i]);
}
for (var i = 0; i < gameState.knights.length; i++) {
array.push(gameState.knights[i]);
}
for (var i = 0; i < gameState.miniBosses.length; i++) {
array.push(gameState.miniBosses[i]);
}
return array;
}
};
/****
* Simple Projectile Factory
****/
var ProjectileFactory = {
// Initialize activeProjectiles array for tracking
activeProjectiles: [],
// Single responsibility: Create projectiles
createProjectile: function createProjectile(type, startX, startY, targetX, targetY, config) {
var projectile = new Projectile(type);
projectile.x = startX;
projectile.y = startY;
// Apply configuration
if (config) {
for (var key in config) {
projectile[key] = config[key];
}
}
// Calculate direction
if (targetX !== undefined && targetY !== undefined) {
var dx = targetX - startX;
var dy = targetY - startY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
projectile.direction.x = dx / distance;
projectile.direction.y = dy / distance;
}
}
game.addChild(projectile);
gameState.projectiles.push(projectile);
this.activeProjectiles.push(projectile);
return projectile;
},
// Single responsibility: Create basic attacks
createBasicAttack: function createBasicAttack(wizard, enemy) {
return this.createProjectile('projectile', wizard.x, wizard.y, enemy.x, enemy.y, {
targetEnemy: enemy,
damage: 100
});
},
// Single responsibility: Remove projectiles
removeProjectile: function removeProjectile(projectile) {
for (var i = gameState.projectiles.length - 1; i >= 0; i--) {
if (gameState.projectiles[i] === projectile) {
gameState.projectiles.splice(i, 1);
break;
}
}
// Remove from activeProjectiles array
for (var i = this.activeProjectiles.length - 1; i >= 0; i--) {
if (this.activeProjectiles[i] === projectile) {
this.activeProjectiles.splice(i, 1);
break;
}
}
if (projectile.parent) {
projectile.destroy();
}
}
};
var globalEffectManager = new EffectManager();
var globalDamageHandler = globalEffectManager; // Backward compatibility alias
/****
* Game Objects (Legacy compatibility)
****/
// Legacy variable aliases for compatibility
var gameStarted = false;
var selectedEnemy = null;
var enemies = gameState.enemies;
var ogres = gameState.ogres;
var knights = gameState.knights;
var miniBosses = gameState.miniBosses;
var coins = gameState.coins;
var projectiles = gameState.projectiles;
var coinCounter = gameState.coinCounter;
var enemyKillCounter = gameState.enemyKillCounter;
var pathLastSpawnTime = gameState.pathLastSpawnTime;
var pathConsecutiveSpawns = gameState.pathConsecutiveSpawns;
// Single game menu object
var gameMenu;
// Create tutorial system first (initially hidden)
var tutorial = game.addChild(new Tutorial());
tutorial.visible = false;
// Create and show game menu
gameMenu = game.addChild(new GameMenu());
// Create active spell deck system
var activeSpellDeck = new SpellDeck();
var spellSlots = [];
var manaBar, manaBarBg, manaText;
// Create spell UI
function createSpellUI() {
// Mana bar
manaBarBg = LK.getAsset('manaBarBg', {
anchorX: 0,
anchorY: 0,
scaleX: GAME_CONFIG.ui.manaBarBg.scaleX,
scaleY: GAME_CONFIG.ui.manaBarBg.scaleY
});
LK.gui.topRight.addChild(manaBarBg);
manaBarBg.x = GAME_CONFIG.ui.manaBarBg.x;
manaBarBg.y = GAME_CONFIG.ui.manaBarBg.y;
manaBarBg.visible = false;
manaBar = LK.getAsset('manaBar', {
anchorX: 0,
anchorY: 0,
scaleX: GAME_CONFIG.ui.manaBar.scaleX,
scaleY: GAME_CONFIG.ui.manaBar.scaleY
});
LK.gui.topRight.addChild(manaBar);
manaBar.x = GAME_CONFIG.ui.manaBar.x;
manaBar.y = GAME_CONFIG.ui.manaBar.y;
manaBar.visible = false;
manaText = new Text2('Mana: 100/100', {
size: GAME_CONFIG.ui.manaText.size,
fill: GAME_CONFIG.ui.manaText.fill,
font: GAME_CONFIG.ui.manaText.font
});
manaText.anchor.set(0, 0);
LK.gui.topRight.addChild(manaText);
manaText.x = GAME_CONFIG.ui.manaText.x;
manaText.y = GAME_CONFIG.ui.manaText.y;
manaText.visible = false;
// Spell slots
for (var i = 0; i < GAME_CONFIG.ui.spellSlots.count; i++) {
var slot = LK.getAsset('spellSlot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: GAME_CONFIG.ui.spellSlots.scaleX,
scaleY: GAME_CONFIG.ui.spellSlots.scaleY
});
LK.gui.bottom.addChild(slot);
slot.x = GAME_CONFIG.ui.spellSlots.startX + i * GAME_CONFIG.ui.spellSlots.spacing;
slot.y = GAME_CONFIG.ui.spellSlots.y;
slot.visible = false;
slot.slotIndex = i;
// Add spell icon if spell exists
if (i < activeSpellDeck.currentDeck.length) {
var spellId = activeSpellDeck.currentDeck[i];
var spell = activeSpellDeck.getSpell(spellId);
if (spell) {
var spellIcon = LK.getAsset('spell', {
anchorX: 0.5,
anchorY: 0.5
});
slot.addChild(spellIcon);
spellIcon.tint = activeSpellDeck.getRarityColor(spell.rarity);
spellIcon.scaleX = 0.8;
spellIcon.scaleY = 0.8;
slot.spellId = spellId;
slot.spellIcon = spellIcon;
}
}
// Add click handler for spell casting
slot.down = function (x, y, obj) {
if (obj.spellId && activeSpellDeck.canCastSpell(obj.spellId)) {
// Cast spell at wizard position or target
var spell = activeSpellDeck.getSpell(obj.spellId);
if (spell) {
var targetX = wizard.x;
var targetY = wizard.y - 100;
// Find nearest enemy for targeted spells
if (spell.id === 'fireball' || spell.id === 'iceShard' || spell.id === 'lightning') {
var allEnemies = collisionArrayPool.getAllEnemies();
var nearestEnemy = null;
var nearestDistance = Infinity;
for (var e = 0; e < allEnemies.length; e++) {
var enemy = allEnemies[e];
var dx = enemy.x - wizard.x;
var dy = enemy.y - wizard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestEnemy = enemy;
}
}
if (nearestEnemy) {
targetX = nearestEnemy.x;
targetY = nearestEnemy.y;
}
}
activeSpellDeck.castSpell(obj.spellId, targetX, targetY);
LK.effects.flashObject(obj, 0x00FF00, 200);
}
} else {
LK.effects.flashObject(obj, 0xFF0000, 200);
}
};
spellSlots.push(slot);
}
}
// Update mana bar
function updateManaBar() {
if (!manaBar || !manaText) return;
var manaPercent = activeSpellDeck.currentMana / activeSpellDeck.maxMana;
manaBar.scaleX = manaPercent * 2;
manaText.setText('Mana: ' + Math.floor(activeSpellDeck.currentMana) + '/' + activeSpellDeck.maxMana);
}
// Update spell slot cooldowns
function updateSpellSlots() {
for (var i = 0; i < spellSlots.length; i++) {
var slot = spellSlots[i];
if (slot.spellId) {
var spell = activeSpellDeck.getSpell(slot.spellId);
if (spell) {
var cooldownEnd = activeSpellDeck.spellCooldowns[slot.spellId] || 0;
var isOnCooldown = cooldownEnd > LK.ticks;
// Update visual state
if (isOnCooldown) {
slot.alpha = 0.5;
// Show cooldown overlay
if (!slot.cooldownOverlay) {
slot.cooldownOverlay = LK.getAsset('cooldownOverlay', {
anchorX: 0.5,
anchorY: 0.5
});
slot.addChild(slot.cooldownOverlay);
slot.cooldownOverlay.alpha = 0.7;
}
// Update cooldown progress
var remaining = cooldownEnd - LK.ticks;
var total = spell.cooldown / 1000 * 60;
var progress = 1 - remaining / total;
slot.cooldownOverlay.scaleY = 1 - progress;
} else {
slot.alpha = 1.0;
if (slot.cooldownOverlay) {
slot.cooldownOverlay.destroy();
slot.cooldownOverlay = null;
}
}
}
}
}
}
// Initialize spell UI
createSpellUI();
// Initialize spell unlocking system
var lastUnlockCheck = 0;
function checkSpellUnlocks() {
if (!gameMenu.spellDeck) {
gameMenu.spellDeck = new SpellDeck();
}
// Only check unlocks when kill counter changes
if (enemyKillCounter === lastUnlockCheck) return;
lastUnlockCheck = enemyKillCounter;
// Unlock spells based on achievements with messages
if (enemyKillCounter >= 10 && !storage.lightningUnlocked) {
storage.lightningUnlocked = true;
gameMenu.spellDeck.unlockSpell('lightning');
LK.effects.flashScreen(0x00FFFF, 500);
showSpellUnlockMessage('LIGHTNING', 'Cadena de rayos entre enemigos');
}
if (enemyKillCounter >= 25 && !storage.shieldUnlocked) {
storage.shieldUnlocked = true;
gameMenu.spellDeck.unlockSpell('shield');
LK.effects.flashScreen(0x0080FF, 500);
showSpellUnlockMessage('MAGIC SHIELD', 'Inmunidad temporal al daño');
}
if (enemyKillCounter >= 50 && !storage.teleportUnlocked) {
storage.teleportUnlocked = true;
gameMenu.spellDeck.unlockSpell('teleport');
LK.effects.flashScreen(0x8000FF, 500);
showSpellUnlockMessage('TELEPORT', 'Mueve instantáneamente al mago');
}
if (enemyKillCounter >= 75 && !storage.timeSlowUnlocked) {
storage.timeSlowUnlocked = true;
gameMenu.spellDeck.unlockSpell('timeSlow');
LK.effects.flashScreen(0xFF8000, 500);
showSpellUnlockMessage('TIME SLOW', 'Ralentiza todos los enemigos');
}
if (enemyKillCounter >= 100 && !storage.meteorUnlocked) {
storage.meteorUnlocked = true;
gameMenu.spellDeck.unlockSpell('meteor');
LK.effects.flashScreen(0xFF0000, 500);
showSpellUnlockMessage('METEOR', 'Daño masivo en área');
}
}
function showSpellUnlockMessage(spellName, description) {
var unlockText = new Text2('NUEVO HECHIZO DESBLOQUEADO!\n' + spellName + '\n' + description, {
size: 60,
fill: 0xFFD700,
font: "monospace"
});
unlockText.anchor.set(0.5, 0.5);
unlockText.x = 2048 / 2;
unlockText.y = 2732 / 2;
game.addChild(unlockText);
// Animate unlock message
tween(unlockText, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(unlockText, {
alpha: 0,
y: unlockText.y - 200
}, {
duration: 1000,
easing: tween.easeIn,
onFinish: function onFinish() {
if (unlockText.parent) unlockText.destroy();
}
});
}
});
}
// Upgrade system removed - using spell deck system instead
// Create unified path system - all 5 paths at once
var paths = [];
var knightX = 2048 / 2;
var knightY = 2732 - 250;
var pathAngles = [-Math.PI / 2, -Math.PI / 3, -2 * Math.PI / 3, Math.PI / 6, 5 * Math.PI / 6];
var wizardX = knightX;
var wizardY = 2732 - 600;
// Create knight reference for backward compatibility
var knight = wizard;
// Unified path creation function
function createUnifiedPaths() {
var spawnPositions = GAME_CONFIG.paths.spawnPositions;
for (var p = 0; p < GAME_CONFIG.paths.count; p++) {
var angle = GAME_CONFIG.paths.angles[p];
var spawnPos = spawnPositions[p];
var actualPathLength = Math.sqrt((spawnPos.x - wizardX) * (spawnPos.x - wizardX) + (spawnPos.y - wizardY) * (spawnPos.y - wizardY));
// Create stone segments
var segmentSize = GAME_CONFIG.gameplay.segmentSize;
var numSegments = Math.floor(actualPathLength / segmentSize);
for (var s = 0; s < numSegments; s++) {
var segmentDistance = s * segmentSize + segmentSize / 2;
var segmentX = spawnPos.x - Math.cos(angle) * segmentDistance;
var segmentY = spawnPos.y - Math.sin(angle) * segmentDistance;
if (segmentX >= -100 && segmentX <= 2148 && segmentY >= -100 && segmentY <= 2832) {
var stoneSegment = game.addChild(LK.getAsset('stonePath', {
anchorX: 0.5,
anchorY: 0.5,
x: segmentX,
y: segmentY,
scaleX: 2.0,
scaleY: 2.0,
rotation: angle + Math.PI / 2
}));
stoneSegment.alpha = 0;
stoneSegment.visible = false;
stoneSegment.pathIndex = p;
}
}
// Create collision area
var centerX = (spawnPos.x + wizardX) / 2;
var centerY = (spawnPos.y + wizardY) / 2;
var path = game.addChild(LK.getAsset('pathSelector', {
anchorX: 0.5,
anchorY: 0.5,
x: centerX,
y: centerY,
scaleX: 4,
scaleY: actualPathLength / 60,
rotation: angle + Math.PI / 2
}));
path.alpha = 0;
path.visible = false;
path.pathIndex = p;
// Add path number
var pathNumber = new Text2((p + 1).toString(), {
size: 120,
fill: 0xFFD700,
font: "monospace"
});
pathNumber.anchor.set(0.5, 0.5);
pathNumber.x = spawnPos.x;
pathNumber.y = spawnPos.y - 80;
pathNumber.visible = false;
pathNumber.pathIndex = p;
game.addChild(pathNumber);
// Add touch handler
path.down = function (x, y, obj) {
wizard.attack(obj.pathIndex);
};
paths.push(path);
}
}
// Create all paths
createUnifiedPaths();
// Pixel art scaling handled by engine automatically
// Set fondodelacueva as the actual game background
var backgroundMap = game.addChild(LK.getAsset('fondodelacueva', {
anchorX: 0,
anchorY: 0,
scaleX: 19.5,
scaleY: 26.0,
x: 0,
y: 0
}));
// Send background to the back but use a less extreme z-index
backgroundMap.zIndex = -100;
// Hide background initially during menu
backgroundMap.visible = false;
backgroundMap.alpha = 1.0;
// Create wizard early to ensure it's available for other classes
var wizard = game.addChild(new Wizard());
wizard.x = knightX;
wizard.y = 2732 - 600; // Position wizard higher on screen
wizard.visible = false;
// UI Elements
// Removed scoreText and levelText to eliminate stray characters in top right
var coinCounter = 0;
var enemyKillCounter = 0;
var coinText = new Text2('Coins: 0', {
size: GAME_CONFIG.ui.coinText.size,
fill: GAME_CONFIG.ui.coinText.fill,
font: GAME_CONFIG.ui.coinText.font
});
coinText.anchor.set(0, 0);
LK.gui.topLeft.addChild(coinText);
coinText.x = GAME_CONFIG.ui.coinText.x;
coinText.y = GAME_CONFIG.ui.coinText.y;
coinText.visible = false;
var killCountText = new Text2('Puntuacion: 0', {
size: GAME_CONFIG.ui.killText.size,
fill: GAME_CONFIG.ui.killText.fill,
font: GAME_CONFIG.ui.killText.font
});
killCountText.anchor.set(0, 0);
LK.gui.topLeft.addChild(killCountText);
killCountText.x = GAME_CONFIG.ui.killText.x;
killCountText.y = GAME_CONFIG.ui.killText.y;
killCountText.visible = false;
var tapText = new Text2('TAP ENEMIES TO ATTACK!', {
size: GAME_CONFIG.ui.tapText.size,
fill: GAME_CONFIG.ui.tapText.fill,
font: GAME_CONFIG.ui.tapText.font
});
tapText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(tapText);
tapText.y = GAME_CONFIG.ui.tapText.yOffset;
tapText.visible = false;
// Health bar UI
var healthBarBg = LK.getAsset('healthBarBg', {
anchorX: 0,
anchorY: 0,
scaleX: GAME_CONFIG.ui.healthBarBg.scaleX,
scaleY: GAME_CONFIG.ui.healthBarBg.scaleY
});
LK.gui.topLeft.addChild(healthBarBg);
healthBarBg.x = GAME_CONFIG.ui.healthBarBg.x;
healthBarBg.y = GAME_CONFIG.ui.healthBarBg.y;
healthBarBg.visible = false;
var healthBar = LK.getAsset('healthBar', {
anchorX: 0,
anchorY: 0,
scaleX: GAME_CONFIG.ui.healthBar.scaleX,
scaleY: GAME_CONFIG.ui.healthBar.scaleY
});
LK.gui.topLeft.addChild(healthBar);
healthBar.x = GAME_CONFIG.ui.healthBar.x;
healthBar.y = GAME_CONFIG.ui.healthBar.y;
healthBar.visible = false;
var healthText = new Text2('Health: 100/100', {
size: GAME_CONFIG.ui.healthText.size,
fill: GAME_CONFIG.ui.healthText.fill,
font: GAME_CONFIG.ui.healthText.font
});
healthText.anchor.set(0, 0);
LK.gui.topLeft.addChild(healthText);
healthText.x = GAME_CONFIG.ui.healthText.x;
healthText.y = GAME_CONFIG.ui.healthText.y;
healthText.visible = false;
function updateHealthBar() {
var healthPercent = wizard.health / wizard.maxHealth;
healthBar.scaleX = healthPercent;
healthText.setText('Health: ' + wizard.health + '/' + wizard.maxHealth);
// Change color based on health
if (healthPercent > 0.6) {
healthBar.tint = 0x00ff00; // Green
} else if (healthPercent > 0.3) {
healthBar.tint = 0xffff00; // Yellow
} else {
healthBar.tint = 0xff0000; // Red
}
}
// Enemy spawning variables
var enemySpawnTimer = 0;
var lastSpawnedPath = -1; // Track the last spawned path
var consecutiveSpawns = 0; // Track consecutive spawns from same path
// Cooldown system variables
var pathLastSpawnTime = [-1, -1, -1, -1, -1]; // Track last spawn time for each path
var pathConsecutiveSpawns = [0, 0, 0, 0, 0]; // Track consecutive spawns per path
var pathCooldownDuration = 300; // 5 seconds at 60fps
// Unified SpawnManager for streamlined spawn control and enemy lifecycle
var SpawnManager = {
// Consolidated spawn configuration
spawnConfig: {
skeleton: {
interval: 90,
maxCount: 15,
startAt: 0
},
ogre: {
interval: 180,
maxCount: 4,
startAt: 15
},
knight: {
interval: 300,
maxCount: 3,
startAt: 30
},
miniBoss: {
interval: 60,
maxCount: 1,
startAt: 80,
endAt: 85,
chance: 0.02
}
},
// Unified spawn timers
spawnTimers: {
skeleton: 0,
ogre: 0,
knight: 0,
miniBoss: 0
},
// Streamlined spawn validation
canSpawnEnemy: function canSpawnEnemy(type, difficulty, totalEnemies) {
var config = this.spawnConfig[type];
if (!config) return false;
var collection = globalEnemyManager.enemyCollections[type];
var timer = this.spawnTimers[type];
// Get threshold from GAME_CONFIG instead of local config
var gameConfigThreshold = GAME_CONFIG.enemies[type].startThreshold;
var threshold = _typeof(gameConfigThreshold) === 'object' ? gameConfigThreshold[difficulty] : gameConfigThreshold;
// Universal spawn conditions with type-specific rules
var baseConditions = timer >= config.interval && enemyKillCounter >= threshold && collection.length < config.maxCount && totalEnemies < 25;
// Special conditions for specific types
if (type === 'miniBoss') {
return baseConditions && enemyKillCounter <= config.endAt && globalEnemyManager.enemyCollections.miniBoss.length === 0 && Math.random() < config.chance;
}
return baseConditions && globalEnemyManager.enemyCollections.miniBoss.length === 0;
},
// Unified spawn execution
executeSpawn: function executeSpawn(type, difficulty, level) {
var totalEnemies = globalEnemyManager.getAllEnemies().length;
if (!this.canSpawnEnemy(type, difficulty, totalEnemies)) return null;
var enemy = globalEnemyManager.createEnemy(type, difficulty, level);
if (enemy) {
game.addChild(enemy);
globalEnemyManager.addToCollection(enemy, type);
// Legacy array compatibility
if (type === 'skeleton') enemies.push(enemy);else if (type === 'ogre') ogres.push(enemy);else if (type === 'knight') knights.push(enemy);else if (type === 'miniBoss') miniBosses.push(enemy);
this.spawnTimers[type] = 0;
// Update path tracking for skeleton spawns
if (type === 'skeleton') {
pathConsecutiveSpawns[enemy.pathIndex]++;
pathLastSpawnTime[enemy.pathIndex] = LK.ticks;
lastSpawnedPath = enemy.pathIndex;
}
}
return enemy;
},
// Streamlined interval updates
updateSpawnIntervals: function updateSpawnIntervals(difficulty, level) {
var base = GAME_CONFIG.enemies.skeleton.spawnInterval[difficulty];
this.spawnConfig.skeleton.interval = Math.max(base.min, base.base - level * base.scaling);
this.spawnConfig.ogre.interval = GAME_CONFIG.enemies.ogre.spawnInterval[difficulty];
this.spawnConfig.knight.interval = GAME_CONFIG.enemies.knight.spawnInterval[difficulty];
},
// Unified timer updates
updateTimers: function updateTimers() {
for (var type in this.spawnTimers) {
this.spawnTimers[type]++;
}
// Check if all paths are in cooldown and reset if needed
var allPathsInCooldown = true;
for (var pathIdx = 0; pathIdx < 5; pathIdx++) {
if (pathConsecutiveSpawns[pathIdx] < 2) {
allPathsInCooldown = false;
break;
}
}
if (allPathsInCooldown) {
// Reset all path cooldowns to prevent spawn deadlock
for (var pathIdx = 0; pathIdx < 5; pathIdx++) {
pathConsecutiveSpawns[pathIdx] = 0;
pathLastSpawnTime[pathIdx] = -1;
}
}
},
// Streamlined spawn cycle for all enemy types
processSpawnCycle: function processSpawnCycle(difficulty, level) {
this.updateSpawnIntervals(difficulty, level);
this.updateTimers();
// Clean up any null/destroyed enemies from collections first
this.cleanupDestroyedEnemies();
var enemyTypes = ['skeleton', 'ogre', 'knight', 'miniBoss'];
for (var i = 0; i < enemyTypes.length; i++) {
this.executeSpawn(enemyTypes[i], difficulty, level);
}
},
// Add cleanup method for destroyed enemies
cleanupDestroyedEnemies: function cleanupDestroyedEnemies() {
// Clean up legacy arrays
var allArrays = [enemies, ogres, knights, miniBosses];
for (var arrayIdx = 0; arrayIdx < allArrays.length; arrayIdx++) {
var array = allArrays[arrayIdx];
for (var i = array.length - 1; i >= 0; i--) {
if (!array[i] || !array[i].parent || array[i].isDying) {
array.splice(i, 1);
}
}
}
// Clean up manager collections
for (var type in globalEnemyManager.enemyCollections) {
var collection = globalEnemyManager.enemyCollections[type];
for (var i = collection.length - 1; i >= 0; i--) {
if (!collection[i] || !collection[i].parent || collection[i].isDying) {
collection.splice(i, 1);
}
}
}
}
};
// Game input handling
game.down = function (x, y, obj) {
// Tap-to-attack is now handled directly by individual enemies
// No need for path-based attacks since enemies handle their own tap events
};
// Initialize unified management systems for streamlined game architecture
var globalEnemyManager = new EnemyManager();
var globalDeathHandler = new UnifiedDeathHandler();
// Unified death animation function for all enemy types
function createEnemyDeathAnimation(enemy, enemyType, enemyArray) {
globalDeathHandler.executeEnemyDeath(enemy, enemyArray);
}
// Main game update loop
game.update = function () {
// Sort children by z-index to ensure proper rendering order
game.children.sort(function (a, b) {
return (a.zIndex || 0) - (b.zIndex || 0);
});
// Pause game when tutorial is active
if (tutorial && tutorial.isActive) {
return;
}
// Only update game logic if game has started
if (!gameStarted) {
return;
}
// Change music to epic battle theme at 10 enemies killed
if (enemyKillCounter === 10) {
LK.playMusic('epicBattle', {
volume: 0.8,
fade: {
start: 0,
end: 0.8,
duration: 1500
}
});
}
// Change to mystical ambient music at 25 enemies killed
if (enemyKillCounter === 25) {
LK.playMusic('mysticalAmbient', {
volume: 0.6,
fade: {
start: 0,
end: 0.6,
duration: 2000
}
});
}
// Upgrade menus removed - using spell deck system instead
// Reset consecutive spawns for paths that have cooled down (runs every frame)
for (var pathIdx = 0; pathIdx < 5; pathIdx++) {
// Check if enough time has passed since last spawn (cooldown expired)
if (pathLastSpawnTime[pathIdx] !== -1 && LK.ticks - pathLastSpawnTime[pathIdx] > pathCooldownDuration) {
// Reset consecutive spawns for this path due to cooldown
pathConsecutiveSpawns[pathIdx] = 0;
}
}
// Get stored difficulty setting
var selectedDifficulty = storage.difficulty || 'NORMAL';
var difficultyLevel = Math.floor(enemyKillCounter / 10); // Every 10 kills increases difficulty
// Apply unique difficulty modifiers
var currentSpawnRate, enemyHealthMultiplier, enemySpeedMultiplier;
var specialMechanics = {};
if (selectedDifficulty === 'FACIL') {
// EASY: Slower enemies, less health, longer spawn intervals
currentSpawnRate = Math.max(60, 120 - difficultyLevel * 5); // Much slower spawning
enemyHealthMultiplier = 1; // No health scaling over time
enemySpeedMultiplier = 1 + difficultyLevel * 0.20; // 20% speed increase per level
specialMechanics.bonusCoins = true; // 50% more coins
specialMechanics.healingChance = 0.15; // 15% chance to heal 5 HP on enemy kill
} else if (selectedDifficulty === 'NORMAL') {
// NORMAL: Balanced progression
currentSpawnRate = Math.max(40, 90 - difficultyLevel * 6); // Standard spawning
enemyHealthMultiplier = 1; // No health scaling over time
enemySpeedMultiplier = 1 + difficultyLevel * 0.30; // 30% speed increase per level
specialMechanics.standardRewards = true;
} else if (selectedDifficulty === 'DIFICIL') {
// HARD: Faster enemies, more health, shorter spawn intervals, special enemy abilities
currentSpawnRate = Math.max(20, 60 - difficultyLevel * 4); // Much faster spawning
enemyHealthMultiplier = 1; // No health scaling over time
enemySpeedMultiplier = 1 + difficultyLevel * 0.50; // 50% speed increase per level
specialMechanics.eliteEnemies = true; // 20% chance for elite enemies with double stats
specialMechanics.aggressiveAI = true; // Enemies move more directly toward wizard
specialMechanics.reducedCoins = true; // 25% fewer coins
}
// Helper functions now integrated into EnemyFactory
// Unified spawn management system
SpawnManager.processSpawnCycle(selectedDifficulty, difficultyLevel);
// Reset path cooldowns for optimized path management
for (var pathIdx = 0; pathIdx < 5; pathIdx++) {
if (pathLastSpawnTime[pathIdx] !== -1 && LK.ticks - pathLastSpawnTime[pathIdx] > pathCooldownDuration) {
pathConsecutiveSpawns[pathIdx] = 0;
}
}
// Unified CollisionManager for streamlined collision detection and response
var CollisionManager = {
// Consolidated collision configurations
collisionConfig: {
skeleton: {
damage: 20,
removeOnHit: true
},
ogre: {
damage: 30,
removeOnHit: true
},
knight: {
damage: 40,
removeOnHit: true
},
miniBoss: {
damage: 75,
removeOnHit: false
}
},
// Streamlined enemy collision processing with categorized collision types
processEnemyCollisions: function processEnemyCollisions() {
var allEnemies = globalEnemyManager.getAllEnemies();
// Category 1: Off-screen cleanup (non-collision processing)
this.processOffScreenCleanup(allEnemies);
// Category 2: Enemy-wizard collisions
this.processEnemyWizardCollisions(allEnemies);
},
// Separate processing for off-screen enemy cleanup
processOffScreenCleanup: function processOffScreenCleanup(allEnemies) {
for (var i = allEnemies.length - 1; i >= 0; i--) {
var enemy = allEnemies[i];
if (this.isOffScreen(enemy)) {
this.removeEnemyFromGame(enemy);
}
}
},
// Separate processing for enemy-wizard collisions
processEnemyWizardCollisions: function processEnemyWizardCollisions(allEnemies) {
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
if (!enemy.isDying && enemy.parent) {
this.checkWizardCollision(enemy);
}
}
},
// Efficient off-screen detection
isOffScreen: function isOffScreen(enemy) {
return enemy.y > 2732 + 100;
},
// Unified enemy removal system
removeEnemyFromGame: function removeEnemyFromGame(enemy) {
// Remove from manager collections
for (var type in globalEnemyManager.enemyCollections) {
globalEnemyManager.removeFromCollection(enemy, type);
}
// Legacy array cleanup for compatibility
this.removeFromLegacyArrays(enemy);
enemy.destroy();
},
// Legacy array compatibility cleanup
removeFromLegacyArrays: function removeFromLegacyArrays(enemy) {
var arrays = [enemies, ogres, knights, miniBosses];
for (var a = 0; a < arrays.length; a++) {
var array = arrays[a];
for (var i = array.length - 1; i >= 0; i--) {
if (array[i] === enemy) {
array.splice(i, 1);
break;
}
}
}
},
// 1.1 Distance Culling: Enhanced wizard collision detection with optimized distance-based culling
checkWizardCollision: function checkWizardCollision(enemy) {
// Initialize collision tracking
if (enemy.lastIntersecting === undefined) {
enemy.lastIntersecting = false;
}
// 1.1 Distance Culling: Skip expensive intersection test if objects are too far apart
var dx = enemy.x - wizard.x;
var dy = enemy.y - wizard.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var maxCollisionDistance = 150; // Approximate maximum collision distance based on sprite sizes
var currentIntersecting = false;
if (distance <= maxCollisionDistance) {
// Only perform expensive intersection test if objects are close enough
currentIntersecting = wizard.intersects(enemy);
}
// Check collision transition
if (!enemy.lastIntersecting && currentIntersecting && !enemy.isDying) {
var config = this.getEnemyConfig(enemy);
wizard.takeDamage(config.damage);
// Handle enemy removal based on type
if (config.removeOnHit) {
this.removeEnemyFromGame(enemy);
return;
}
}
// Update collision state
enemy.lastIntersecting = currentIntersecting;
},
// Get enemy configuration by type
getEnemyConfig: function getEnemyConfig(enemy) {
return this.collisionConfig[enemy.enemyType] || this.collisionConfig.skeleton;
}
};
// Replace the old collision function call
function checkAllEnemyCollisions() {
CollisionManager.processEnemyCollisions();
}
// Call the unified collision detection function
checkAllEnemyCollisions();
// Check thorns spike collisions with all enemies continuously
var allSpikes = [];
for (var childIdx = 0; childIdx < game.children.length; childIdx++) {
var child = game.children[childIdx];
// Check if this child is a spike (has hitEnemies array and brown tint)
if (child.hitEnemies && child.tint === 0x8B4513) {
allSpikes.push(child);
}
}
for (var spikeIdx = 0; spikeIdx < allSpikes.length; spikeIdx++) {
var spike = allSpikes[spikeIdx];
var allEnemies = collisionArrayPool.getAllEnemies();
for (var enemyIdx = 0; enemyIdx < allEnemies.length; enemyIdx++) {
var enemy = allEnemies[enemyIdx];
// Only hit enemies that haven't been hit by this spike yet and are not dying
if (spike.intersects(enemy) && spike.hitEnemies.indexOf(enemy) === -1 && !enemy.isDying) {
var thornDamage = 100; // Always deal 100 damage
enemy.takeDamage(thornDamage);
LK.effects.flashObject(enemy, 0x8B4513, 300);
// Mark this enemy as hit by this spike
spike.hitEnemies.push(enemy);
}
}
}
// Update optimized animation system
if (globalAnimationManager) {
globalAnimationManager.updateAnimations();
}
// Update spell deck system
if (activeSpellDeck && gameStarted) {
activeSpellDeck.updateMana();
activeSpellDeck.updateCombo();
updateManaBar();
updateSpellSlots();
}
// Optimized time slow effects processing with batch management
var allEnemies = collisionArrayPool.getAllEnemies();
// Use batch processing for large enemy groups to improve performance
if (allEnemies.length > 10 && globalAnimationManager) {
globalAnimationManager.processBatchAnimations(allEnemies, 'enemies');
}
for (var i = 0; i < allEnemies.length; i++) {
var enemy = allEnemies[i];
if (enemy.timeSlowed) {
enemy.timeSlowTimer--;
if (enemy.timeSlowTimer <= 0) {
enemy.timeSlowed = false;
enemy.timeSlowAmount = 1.0;
}
}
}
// Make tap text pulse
var pulse = 1 + Math.sin(LK.ticks * 0.1) * 0.2;
tapText.scale.set(pulse, pulse);
// Check for spell unlocks
checkSpellUnlocks();
// Clean up any orphaned projectiles that may not have been properly removed
for (var i = projectiles.length - 1; i >= 0; i--) {
var projectile = projectiles[i];
if (!projectile || !projectile.parent || projectile.hitEnemy) {
projectiles.splice(i, 1);
}
}
// Ensure ProjectileFactory activeProjectiles array stays synchronized
for (var i = ProjectileFactory.activeProjectiles.length - 1; i >= 0; i--) {
var projectile = ProjectileFactory.activeProjectiles[i];
if (!projectile || !projectile.parent || projectile.hitEnemy) {
ProjectileFactory.activeProjectiles.splice(i, 1);
}
}
};
// Remove tap text after 5 seconds
tween({}, {}, {
duration: 5000,
onFinish: function onFinish() {
if (tapText && tapText.parent) {
tapText.destroy();
}
}
}); ===================================================================
--- original.js
+++ change.js
@@ -4104,9 +4104,212 @@
// Reset skipped flag for next time
self.skipped = false;
};
// Handle tutorial interactions
+ // Add interaction handler for spell cards in deck view
self.down = function (x, y, obj) {
+ // Only handle interactions if deck is active or if this is tutorial
+ if (self.deckMode) {
+ // Check deck card clicks for spell casting (from current deck)
+ for (var i = 0; i < self.deckElements.length; i++) {
+ var element = self.deckElements[i];
+ if (element.spellId && element.isDeckCard) {
+ var cardX = element.x;
+ var cardY = element.y;
+ if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) {
+ // Try to cast the spell instead of removing it
+ if (activeSpellDeck && activeSpellDeck.canCastSpell(element.spellId)) {
+ var spell = activeSpellDeck.getSpell(element.spellId);
+ if (spell) {
+ var targetX = wizard.x;
+ var targetY = wizard.y - 100;
+ // Find nearest enemy for targeted spells
+ if (spell.id === 'fireball' || spell.id === 'iceShard' || spell.id === 'lightning') {
+ var allEnemies = collisionArrayPool.getAllEnemies();
+ var nearestEnemy = null;
+ var nearestDistance = Infinity;
+ for (var e = 0; e < allEnemies.length; e++) {
+ var enemy = allEnemies[e];
+ var dx = enemy.x - wizard.x;
+ var dy = enemy.y - wizard.y;
+ var distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance < nearestDistance) {
+ nearestDistance = distance;
+ nearestEnemy = enemy;
+ }
+ }
+ if (nearestEnemy) {
+ targetX = nearestEnemy.x;
+ targetY = nearestEnemy.y;
+ }
+ }
+ // Cast the spell
+ activeSpellDeck.castSpell(element.spellId, targetX, targetY);
+ LK.effects.flashObject(element, 0x00FF00, 200);
+ // Show cast message
+ var castText = new Text2('HECHIZO LANZADO!', {
+ size: 60,
+ fill: 0x00FF00,
+ font: "monospace"
+ });
+ castText.anchor.set(0.5, 0.5);
+ castText.x = 2048 / 2;
+ castText.y = 2200;
+ self.addChild(castText);
+ // Animate and remove message
+ tween(castText, {
+ alpha: 0,
+ y: castText.y - 100
+ }, {
+ duration: 1500,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ if (castText.parent) castText.destroy();
+ }
+ });
+ }
+ } else {
+ LK.effects.flashObject(element, 0xFF0000, 200);
+ // Show "cannot cast" message
+ var errorText = new Text2('NO SE PUEDE LANZAR', {
+ size: 50,
+ fill: 0xFF6666,
+ font: "monospace"
+ });
+ errorText.anchor.set(0.5, 0.5);
+ errorText.x = 2048 / 2;
+ errorText.y = 2200;
+ self.addChild(errorText);
+ // Animate and remove message
+ tween(errorText, {
+ alpha: 0,
+ y: errorText.y - 100
+ }, {
+ duration: 2000,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ if (errorText.parent) errorText.destroy();
+ }
+ });
+ }
+ return;
+ }
+ }
+ }
+ // Continue with existing deck menu handling for adding/removing cards
+ // Check deck card clicks with better hit detection for removal
+ for (var i = 0; i < self.deckElements.length; i++) {
+ var element = self.deckElements[i];
+ if (element.spellId && element.isDeckCard) {
+ var cardX = element.x;
+ var cardY = element.y;
+ // Use different area for removal (bottom part of card)
+ if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY + 100 && y <= cardY + 225) {
+ // Visual feedback before removal
+ LK.effects.flashObject(element, 0xFF0000, 300);
+ // Remove from deck
+ if (self.spellDeck.removeFromDeck(element.spellId)) {
+ self.refreshDeckDisplay();
+ LK.effects.flashScreen(0xFF8800, 200);
+ // Show removal message
+ var removeText = new Text2('HECHIZO REMOVIDO', {
+ size: 60,
+ fill: 0xFF6666,
+ font: "monospace"
+ });
+ removeText.anchor.set(0.5, 0.5);
+ removeText.x = 2048 / 2;
+ removeText.y = 2200;
+ self.addChild(removeText);
+ // Animate and remove message
+ tween(removeText, {
+ alpha: 0,
+ y: removeText.y - 100
+ }, {
+ duration: 1500,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ if (removeText.parent) removeText.destroy();
+ }
+ });
+ } else {
+ LK.effects.flashScreen(0xFF0000, 200);
+ }
+ return;
+ }
+ }
+ }
+ // Continue with existing available cards handling...
+ // Check available card clicks with better hit detection
+ for (var i = 0; i < self.availableElements.length; i++) {
+ var element = self.availableElements[i];
+ if (element.spellId && !element.isDeckCard) {
+ var cardX = element.x;
+ var cardY = element.y;
+ if (x >= cardX - 175 && x <= cardX + 175 && y >= cardY - 225 && y <= cardY + 225) {
+ // Visual feedback before addition
+ LK.effects.flashObject(element, 0x00FF00, 300);
+ // Add to deck
+ if (self.spellDeck.addToDeck(element.spellId)) {
+ self.refreshDeckDisplay();
+ LK.effects.flashScreen(0x00FF00, 200);
+ // Show addition message
+ var addText = new Text2('HECHIZO AÑADIDO', {
+ size: 60,
+ fill: 0x66FF66,
+ font: "monospace"
+ });
+ addText.anchor.set(0.5, 0.5);
+ addText.x = 2048 / 2;
+ addText.y = 2200;
+ self.addChild(addText);
+ // Animate and remove message
+ tween(addText, {
+ alpha: 0,
+ y: addText.y - 100
+ }, {
+ duration: 1500,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ if (addText.parent) addText.destroy();
+ }
+ });
+ } else {
+ LK.effects.flashScreen(0xFF0000, 200);
+ // Show error message
+ var errorText = new Text2('DECK LLENO (MAX 5)', {
+ size: 50,
+ fill: 0xFF6666,
+ font: "monospace"
+ });
+ errorText.anchor.set(0.5, 0.5);
+ errorText.x = 2048 / 2;
+ errorText.y = 2200;
+ self.addChild(errorText);
+ // Animate and remove message
+ tween(errorText, {
+ alpha: 0,
+ y: errorText.y - 100
+ }, {
+ duration: 2000,
+ easing: tween.easeOut,
+ onFinish: function onFinish() {
+ if (errorText.parent) errorText.destroy();
+ }
+ });
+ }
+ return;
+ }
+ }
+ }
+ // Deck back button
+ if (y >= 2350 && y <= 2650) {
+ self.hideDeck();
+ }
+ // Block all other interactions when deck menu is active
+ return;
+ }
+ // Tutorial handling (moved after deck handling)
if (!self.isActive) return;
var step = self.tutorialSteps[self.currentStep];
// Handle skip button (top-right corner)
if (step.showSkip && x >= 2048 - 300 && x <= 2048 - 100 && y >= 100 && y <= 300) {
@@ -5038,12 +5241,12 @@
/****
* Game Code
****/
+// Game Constants
/****
* Constants & Configuration
****/
-// Game Constants
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;