User prompt
Weaken the homing on tracking arrow an little and the arrow should not try to home in on enemies that are past the players X position.
Code edit (2 edits merged)
Please save this source code
User prompt
Add a homing effect to arrows when you have the tracking arrow upgrade. Homing should start week and grow stronger the further it gets from the player.
User prompt
Create an upgrade in the ranged category called ‘Tracking Arrow’. It costs 1500 and adds some homing capabilities to arrows. For now just add the upgrade to the shop and add all proper player saving and loading. We will add the effect after.
User prompt
Analyze and refactor enemy class for redundancy and efficiency without losing ANY functionality.
User prompt
Analyze and refactor Enemy class without losing ANY functionality.
User prompt
Analyze and refactor code to remove redundancy and promote efficiency, readability and performance without losing ANY functionality.
User prompt
Analyze the code and refactor for efficiency, readability and performance without losing ANY functionality. Take your time.
User prompt
Update with: shopButton.down = function () { // Pause the game var previousGameSpeed = gameSpeedMultiplier; // IMPORTANT FIX: Force cancel any active movement states if (player) { // Cancel slide if active if (player.isSliding) { player.isSliding = false; player.slideTimer = 0; player.hitboxHeight = player.normalHitboxHeight; } // Cancel air dash if active if (player.isAirDashing) { player.isAirDashing = false; player.airDashTimer = 0; // Clean up shadows player.shadowImages.forEach(function(shadow) { if (shadow.parent) { shadow.destroy(); } }); player.shadowImages = []; // Reset rotation player.rotation = 0; } } // Now set game speed to 0 gameSpeedMultiplier = 0; // Open shop openGameplayShop(); // Function to return to game function resumeGame() { // Close shop if (game.shopManager && game.shopManager.parent) { game.shopManager.destroy(); game.shopManager = null; } // Restore game speed, but ensure it's not the enhanced speed from slide/dash gameSpeedMultiplier = 1.0; // IMPORTANT FIX: Always restore to 1.0, not previousGameSpeed // Remove back button using our reference if (gameplayBackButton && gameplayBackButton.parent) { gameplayBackButton.destroy(); } } // Add custom back button for gameplay shop gameplayBackButton = game.addChild(LK.getAsset('backbutton', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.75, scaleY: 0.75, x: GAME_WIDTH / 2, y: GAME_HEIGHT - 200 })); gameplayBackButton.down = function () { LK.getSound('menuselect').play(); resumeGame(); }; };
Code edit (4 edits merged)
Please save this source code
User prompt
Play the mushroom bounce sound effect if the mushroom gets hit by slide without the combat slide upgrade.
User prompt
Play the mushroomdie sound effect when the mushroom gets hit by slide and the player has the combat slide upgrade.
User prompt
Play the mushroomdie sound effect when the mushroom gets hit.
User prompt
Play the mushroomdie sound effect when the mushroom starts to die
User prompt
Play the mushroom hiss sound effect when the mushroom attack animation starts.
User prompt
Play mushroomburst sound effect when spores are released
Code edit (3 edits merged)
Please save this source code
User prompt
Update as needed with: function updateEnemies() { if (game.shopManager && game.shopManager.parent) { return; } if (!tutorialActive) { var isPlatformOccupied = function(platform) { return enemies.some(function(enemy) { return enemy.currentPlatform === platform && (enemy.type === 'goblin' || enemy.type === 'skeleton' || enemy.type === 'mushroom'); }); }; // Get available platforms for ground enemies var availablePlatforms = platforms.filter(function(p) { return p.x > GAME_WIDTH - 100 && p.x < GAME_WIDTH + 300; }); // Filter out platforms that already have ground enemies var unoccupiedPlatforms = availablePlatforms.filter(function(p) { return !isPlatformOccupied(p); }); // Unified enemy spawn counter for all ground enemies enemySpawnCounter++; // If we have available platforms and it's time to spawn if (enemySpawnCounter >= enemySpawnInterval && unoccupiedPlatforms.length > 0) { // Decide how many enemies to spawn (1-3 based on available platforms) var spawnCount = Math.min( Math.floor(Math.random() * 3) + 1, unoccupiedPlatforms.length ); for (var i = 0; i < spawnCount; i++) { // Choose a random platform from unoccupied ones var platformIndex = Math.floor(Math.random() * unoccupiedPlatforms.length); var platform = unoccupiedPlatforms[platformIndex]; // Choose enemy type randomly with weighted probabilities var enemyRandom = Math.random(); var enemyType; if (enemyRandom < 0.45) { enemyType = 'goblin'; // 45% chance for goblin } else if (enemyRandom < 0.75) { enemyType = 'mushroom'; // 30% chance for mushroom } else { enemyType = 'skeleton'; // 25% chance for skeleton } // Create the enemy var enemy = new Enemy(enemyType); enemy.x = platform.x; enemy.y = platform.y - ENEMY_PLATFORM_OFFSET; enemy.currentPlatform = platform; enemies.push(enemy); game.addChild(enemy); // Remove the used platform from available platforms unoccupiedPlatforms.splice(platformIndex, 1); } // Reset the counter with a random interval enemySpawnInterval = Math.floor(Math.random() * 100) + 150; enemySpawnCounter = 0; } // Eyeball spawning - kept separate since they don't use platforms eyeballSpawnCounter++; if (eyeballSpawnCounter >= eyeballSpawnInterval) { var enemy = new Enemy('eyeball'); var heightRange = LOWEST_PLATFORM_HEIGHT - HIGHEST_PLATFORM_HEIGHT; var randomHeight = Math.random() * heightRange; enemy.x = GAME_WIDTH + 100; enemy.y = HIGHEST_PLATFORM_HEIGHT + randomHeight; enemies.push(enemy); game.addChild(enemy); // Random interval between 200-400 ticks eyeballSpawnInterval = Math.floor(Math.random() * 200) + 200; eyeballSpawnCounter = 0; } } // The rest of your enemy update code remains the same // [existing code for updating enemies and checking collisions] }
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
User prompt
Update with: self.releaseSpores = function () { // Create 5 spores in pattern described var directions = [{ x: 1, y: 0 }, // Right { x: -1, y: 0 }, // Left { x: 0, y: -1 }, // Up { x: 0.7, y: -0.7 }, // Up-right diagonal { x: -0.7, y: -0.7 } // Up-left diagonal ]; // Calculate an offset position IN FRONT of the mushroom // This compensates for the platform movement and animation delay var offsetX = self.x + 150; // Add a significant forward offset var offsetY = self.y - 50; // Slightly above as before for (var i = 0; i < directions.length; i++) { var spore = new Spore(); // Position spores at the offset position spore.x = offsetX; spore.y = offsetY; // Don't add PLATFORM_SPEED to the velocityX, as that's causing over-compensation spore.velocityX = directions[i].x * spore.speed; spore.velocityY = directions[i].y * spore.speed; // Add to game game.addChild(spore); // Add to a global array for tracking if needed if (!window.spores) { window.spores = []; } window.spores.push(spore); } };
User prompt
Update with: self.releaseSpores = function () { // Create 5 spores in pattern described var directions = [{ x: 1, y: 0 }, // Right { x: -1, y: 0 }, // Left { x: 0, y: -1 }, // Up { x: 0.7, y: -0.7 }, // Up-right diagonal { x: -0.7, y: -0.7 } // Up-left diagonal ]; for (var i = 0; i < directions.length; i++) { var spore = new Spore(); // Set the initial position EXACTLY at the mushroom's current position spore.x = self.x; spore.y = self.y - 50; // Spawn slightly above the mushroom // Set velocity based on direction, plus add the platform speed to x velocity // This compensates for the platform movement that's applied to all entities spore.velocityX = directions[i].x * spore.speed + PLATFORM_SPEED * gameSpeedMultiplier; spore.velocityY = directions[i].y * spore.speed; // Add to game game.addChild(spore); // Add to a global array for tracking if needed if (!window.spores) { window.spores = []; } window.spores.push(spore); } };
User prompt
Update as needed with: self.update = function () { // Hide all sprites first self.hideAllSprites(); // Special behavior for first few frames - stay with parent if (self.attachedFrames > 0) { self.attachedFrames--; // Update position to stay with parent mushroom if (self.parent && !self.parent.destroyed) { self.x = self.parent.x; self.y = self.parent.y - 50; } // On last attached frame, set velocity based on direction if (self.attachedFrames === 0) { self.velocityX = self.direction.x * self.speed; self.velocityY = self.direction.y * self.speed; // Remove parent reference self.parent = null; } } else { // Normal movement after detaching self.x += self.velocityX; self.y += self.velocityY; // Apply world movement self.x -= PLATFORM_SPEED * gameSpeedMultiplier; }
User prompt
Update with: self.releaseSpores = function () { // Create 5 spores in pattern described var directions = [ { x: 1, y: 0 }, // Right { x: -1, y: 0 }, // Left { x: 0, y: -1 }, // Up { x: 0.7, y: -0.7 }, // Up-right diagonal { x: -0.7, y: -0.7 } // Up-left diagonal ]; for (var i = 0; i < directions.length; i++) { var spore = new Spore(); // Initialize properties spore.parent = self; // Reference to parent mushroom spore.attachedFrames = 2; // How many frames to stay attached spore.direction = directions[i]; // Store the direction spore.speed = 5; // Standard speed // Initial position exactly at mushroom spore.x = self.x; spore.y = self.y - 50; // Add to game game.addChild(spore); // Add to a global array for tracking if (!window.spores) { window.spores = []; } window.spores.push(spore); } };
User prompt
Update with: self.update = function () { // Hide all sprites first self.hideAllSprites(); // Move spore with its velocity (already includes platform speed offset) self.x += self.velocityX; self.y += self.velocityY; // Update animation self.animationCounter += self.animationSpeed; if (self.animationCounter >= 1) { self.animationCounter = 0; self.frame = (self.frame + 1) % self.sprites.length; } self.sprites[self.frame].alpha = 1; // Decrease lifespan self.lifespan--; // Start fading out near end of life if (self.lifespan < 30) { self.alpha = self.lifespan / 30; } // Destroy if off screen or lifespan ended if (self.x < -100 || self.x > GAME_WIDTH + 100 || self.y < -100 || self.y > GAME_HEIGHT + 100 || self.lifespan <= 0) { self.destroy(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Arrow = Container.expand(function () {
var self = Container.call(this);
self.sprite = self.attachAsset('arrow', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 25;
self.damage = 1;
self.update = function () {
self.x += self.speed;
// Destroy if off screen
if (self.x > GAME_WIDTH + 100) {
self.destroy();
}
};
self.getBounds = function () {
return {
left: self.x - 40,
right: self.x + 40,
top: self.y - 10,
bottom: self.y + 10
};
};
self.updateAmmo = function (amount) {
self.ammoText.setText('x' + amount);
// Adjust position based on digit count
if (amount >= 10) {
self.ammoText.x = -150;
} else {
self.ammoText.x = -100;
}
};
return self;
});
var ArrowPickup = Container.expand(function () {
var self = Container.call(this);
self.sprite = self.attachAsset('arrow', {
anchorX: 0.2,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2
});
// Initialize physics properties
CollectibleBehavior.initPhysics(self);
self.collect = function () {
if (!self.collected) {
self.collected = true;
player.ammoCount += 1;
scoreManager.updateAmmo(player.ammoCount);
LK.getSound('arrowpickup').play();
// Create arrow popup
var popup = LK.getAsset('arrow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
popup.x = self.x - 50;
popup.y = self.y - 200;
popup.velocityY = -3;
popup.lifespan = 45;
popup.update = function () {
this.y += this.velocityY;
this.lifespan--;
if (this.lifespan < 15) {
this.alpha -= 0.07;
}
if (this.alpha <= 0 || this.lifespan <= 0) {
this.destroy();
}
};
game.addChild(popup);
self.destroy();
}
};
self.update = function () {
CollectibleBehavior.standardUpdate(self);
};
return self;
});
// Refactored classes - keeping original Container.expand pattern
var Coin = Container.expand(function (type) {
var self = Container.call(this);
// Set type and create sprite
self.type = type || 'coin';
self.sprite = self.attachAsset(self.type, {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xFFFFFF
});
// Initialize physics properties
CollectibleBehavior.initPhysics(self);
// Get value based on type
self.getValue = function () {
switch (self.type) {
case 'diamond':
return 10;
case 'emerald':
return 5;
case 'ruby':
return 3;
default:
return 1;
}
};
// Collection functionality
self.collect = function () {
if (!self.collected) {
self.collected = true;
var value = self.getValue();
scoreManager.addScore(value, self.x, self.y);
LK.getSound('coincollect').play();
self.destroy();
}
};
// Standard update method using shared behavior
self.update = function () {
CollectibleBehavior.standardUpdate(self);
};
self.checkPlatformCollision = function () {
return GameUtils.checkPlatformCollision(self, 80, true) != null;
};
return self;
});
// CrumblePiece class
var CrumblePiece = Container.expand(function (pieceNum) {
var self = Container.call(this);
self.sprite = self.attachAsset('crumblepiece' + pieceNum, {
anchorX: 0.5,
anchorY: 0.5
});
self.velocityX = 0;
self.velocityY = 0;
self.rotationSpeed = 0;
self.update = function () {
// Apply physics
self.velocityY += 0.5; // gravity
self.x += self.velocityX;
self.y += self.velocityY;
self.rotation += self.rotationSpeed;
// Destroy if off screen
if (self.y > GAME_HEIGHT || self.x < -100) {
self.destroy();
}
};
return self;
});
var CustomText = Container.expand(function (text, options) {
var self = Container.call(this);
// Basic settings
self.size = options.size || 70;
self.scaleSize = self.size / 70;
self.letterSpacing = (options.letterSpacing || 10) * self.scaleSize;
self.fill = options.fill || 0xFFFFFF;
self.wordWrap = options.wordWrap || false;
self.wordWrapWidth = options.wordWrapWidth || 800;
self.lineHeight = (options.lineHeight || 1.2) * self.size;
self.letters = [];
self.letterPool = {}; // Pool of letter sprites by character
self.text = '';
self.width = 0;
self.height = 0; // Track height for multi-line text
// Get asset name for character - keep original mapping logic
self.getAssetForChar = function (_char) {
if (_char.match(/[a-z]/i)) {
return 'letter_' + _char.toLowerCase();
} else if (_char.match(/[0-9]/)) {
return 'number_' + _char;
} else {
switch (_char) {
case '.':
return 'period_mark';
case ',':
return 'comma_mark';
case '!':
return 'exclamation_mark';
case '?':
return 'question_mark';
case '+':
return 'addition_mark';
case '*':
return 'multiplication_mark';
case ' ':
return null;
default:
return null;
}
}
};
// Get or create sprite from pool
self.getOrCreateSprite = function (_char2) {
// Initialize pool for this character if needed
if (!self.letterPool[_char2]) {
self.letterPool[_char2] = [];
}
// Try to find an inactive sprite
var sprite = self.letterPool[_char2].find(function (s) {
return !s.active;
});
// Create new sprite if none available
if (!sprite) {
var assetName = self.getAssetForChar(_char2);
if (assetName) {
sprite = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: self.scaleSize,
scaleY: self.scaleSize
});
if (self.fill) {
sprite.tint = self.fill;
}
sprite.active = false;
sprite.alpha = 0;
// Store character type for position adjustments
sprite.charType = _char2;
self.letterPool[_char2].push(sprite);
}
}
return sprite;
};
// Calculate the width a text would take
self.calculateTextWidth = function (text) {
var width = 0;
for (var i = 0; i < text.length; i++) {
var _char4 = text[i];
if (_char4 === ' ') {
width += 40 * self.scaleSize;
} else {
width += 50 * self.scaleSize + self.letterSpacing;
}
}
return width;
};
// Break text into lines for word wrap
self.breakTextIntoLines = function (text) {
// First handle explicit line breaks
if (text.includes('\n')) {
var explicitLines = text.split('\n');
var result = [];
// Process each explicit line for word wrapping
for (var j = 0; j < explicitLines.length; j++) {
if (self.wordWrap) {
var wrappedLines = this.breakTextIntoLines(explicitLines[j]);
result = result.concat(wrappedLines);
} else {
result.push(explicitLines[j]);
}
}
return result;
}
if (!self.wordWrap) {
return [text];
}
var words = text.split(' ');
var lines = [];
var currentLine = '';
var currentWidth = 0;
var spaceWidth = 40 * self.scaleSize;
for (var i = 0; i < words.length; i++) {
var word = words[i];
var wordWidth = self.calculateTextWidth(word);
// Check if adding this word would exceed the line width
if (currentWidth + wordWidth > self.wordWrapWidth && currentLine !== '') {
lines.push(currentLine.trim());
currentLine = word + ' ';
currentWidth = wordWidth + spaceWidth;
} else {
currentLine += word + ' ';
currentWidth += wordWidth + spaceWidth;
}
}
// Add the last line if there's anything left
if (currentLine.trim() !== '') {
lines.push(currentLine.trim());
}
return lines;
};
// Set text content with sprite pooling
self.setText = function (newText) {
// Always convert to string
newText = String(newText);
// Deactivate all current letters
self.letters.forEach(function (letter) {
letter.active = false;
letter.alpha = 0;
});
self.letters = [];
// Break text into lines if word wrap is enabled
var lines = self.breakTextIntoLines(newText);
var maxLineWidth = 0;
// Position tracking
var currentX = 0;
var currentY = 0;
var lineCount = 0;
// Process each line
for (var lineIndex = 0; lineIndex < lines.length; lineIndex++) {
var line = lines[lineIndex];
currentX = 0; // Reset X position for each line
// Create/reuse sprites for each character in this line
for (var i = 0; i < line.length; i++) {
var _char3 = line[i];
// Handle spaces
if (_char3 === ' ') {
currentX += 40 * self.scaleSize;
continue;
}
// Get or create sprite
var sprite = self.getOrCreateSprite(_char3);
if (sprite) {
sprite.active = true;
sprite.alpha = 1;
sprite.x = currentX;
sprite.y = currentY;
// Add vertical position adjustment for punctuation
if (_char3 === '.' || _char3 === ',') {
sprite.y += 20 * self.scaleSize; // Move periods and commas down
} else if (_char3 === '*') {
sprite.y += 10 * self.scaleSize; // Move multiplication mark down slightly
}
self.letters.push(sprite);
currentX += 50 * self.scaleSize + self.letterSpacing;
}
}
// Track the maximum line width
if (currentX > maxLineWidth) {
maxLineWidth = currentX;
}
// Move to next line
if (lineIndex < lines.length - 1) {
currentY += self.lineHeight;
lineCount++;
}
}
// Update width and height properties
self.width = maxLineWidth;
self.height = (lineCount + 1) * self.lineHeight;
if (self.letters.length === 0) {
self.width = 0;
self.height = 0;
}
// Handle anchor alignment
if (options.anchorX === 1) {
// Right-aligned text
for (var i = 0; i < self.letters.length; i++) {
self.letters[i].x -= self.width;
}
} else if (options.anchorX === 0.5) {
// Center-aligned each line
var currentLine = 0;
var lineStart = 0;
var lineEnd = 0;
var lineWidth = 0;
for (var i = 0; i < self.letters.length; i++) {
// Check if we're in a new line
var letterY = self.letters[i].y;
if (i === 0 || letterY !== self.letters[lineStart].y) {
// Process previous line if this isn't the first letter
if (i > 0) {
lineEnd = i - 1;
lineWidth = self.letters[lineEnd].x + 50 * self.scaleSize / 2;
// Center this line
for (var j = lineStart; j <= lineEnd; j++) {
self.letters[j].x -= lineWidth / 2;
}
}
// Start tracking new line
lineStart = i;
}
}
// Process the last line
if (lineStart < self.letters.length) {
lineEnd = self.letters.length - 1;
lineWidth = self.letters[lineEnd].x + 50 * self.scaleSize / 2;
// Center this line
for (var j = lineStart; j <= lineEnd; j++) {
self.letters[j].x -= lineWidth / 2;
}
}
}
// Store current text
self.text = newText;
};
// Cleanup method
self.destroy = function () {
// Properly destroy all pooled sprites
Object.values(self.letterPool).forEach(function (pool) {
pool.forEach(function (sprite) {
if (sprite.destroy) {
sprite.destroy();
}
});
});
self.letterPool = {};
self.letters = [];
Container.prototype.destroy.call(this);
};
// Initialize with provided text
self.setText(text);
return self;
});
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'basic';
// Common properties
self.speed = 7;
self.isOnGround = true;
self.velocityY = 0;
self.currentPlatform = null;
self.isHit = false;
self.isDying = false;
self.isFadingOut = false;
self.fadeOutTimer = 0;
// self.deathTimer = 0; // Replaced by animation system + fadeOutTimer
self.throwBackSpeed = 15;
self.throwBackDistance = 0;
self.hitType = 'none'; // 'none', 'attack', 'slide'
self.hitboxWidth = 200;
self.hitboxHeight = 260;
self.lootModifier = 1.0;
self.originalSpeed = self.speed; // To store speed before state changes like attack/throw
// Animation system
self.sprites = []; // Array of all animation sprites for this enemy
self.animations = {}; // Stores arrays of sprite names: {run: [...], hit: [...], die: [...]}
self.animationOffsets = {}; // Stores starting index in self.sprites for each animation type
self.currentAnimation = {
type: null,
frames: [],
frameIndex: 0,
spriteOffset: 0,
counter: 0,
speed: 0.11,
// Default animation speed
loop: true
};
self.animationSpeed = 0.11; // Base animation speed
// Type-specific properties and animation definitions
if (self.type === 'mushroom') {
self.speed = 6;
self.animations.run = ['mushroomrun1', 'mushroomrun2', 'mushroomrun3', 'mushroomrun4', 'mushroomrun5', 'mushroomrun6', 'mushroomrun7', 'mushroomrun8'];
self.animations.hit = ['mushroomhit1', 'mushroomhit2'];
self.animations.die = ['mushroomdie1', 'mushroomdie2', 'mushroomdie3', 'mushroomdie4'];
self.animations.attack = ['mushroomattack1', 'mushroomattack2', 'mushroomattack3', 'mushroomattack4', 'mushroomattack5'];
self.isAttacking = false;
self.attackTimer = 0; // Still used for attack duration/cooldown logic, not frame animation
self.attackDuration = 100;
self.attackCooldown = 180;
self.attackCooldownTimer = 0;
self.sporesReleased = false;
} else if (self.type === 'eyeball') {
self.hitboxHeight = 90;
self.animations.fly = ['eyefly1', 'eyefly2', 'eyefly3', 'eyefly4', 'eyefly5', 'eyefly6', 'eyefly7', 'eyefly8'];
self.animations.hit = ['eyedie1', 'eyedie2']; // Eyeball hit animation uses first 2 die frames
self.animations.die = ['eyedie3', 'eyedie4', 'eyedie5']; // Eyeball die animation uses last 3 die frames
self.isFlying = true;
self.flyingHeight = 0; // Not directly used, dynamic
self.verticalSpeed = 2; // Initial vertical speed component for wave motion
self.maxVerticalSpeed = 4;
self.homingDelay = 80;
self.homingTimer = 0;
} else if (self.type === 'goblin') {
self.animations.run = ['goblinrun1', 'goblinrun2', 'goblinrun3', 'goblinrun4', 'goblinrun5', 'goblinrun6', 'goblinrun7', 'goblinrun8'];
self.animations.hit = ['goblinhit1'];
self.animations.die = ['goblindie1', 'goblindie2', 'goblindie3', 'goblindie4'];
} else if (self.type === 'skeleton') {
self.speed = 6;
self.lootModifier = 1.6;
self.animations.run = ['skeletonwalk1', 'skeletonwalk2', 'skeletonwalk3', 'skeletonwalk4'];
self.animations.hit = ['skeletonhit1', 'skeletonhit2'];
self.animations.die = ['skeletondie1', 'skeletondie2', 'skeletondie3', 'skeletondie4'];
self.animations["throw"] = ['skeletonthrow1', 'skeletonthrow2', 'skeletonthrow3', 'skeletonthrow4', 'skeletonthrow5', 'skeletonthrow6'];
self.isThrowing = false;
self.throwingTimer = 0; // Used for throw sequence timing, not frame animation
self.throwingDuration = 90;
self.throwingPauseTime = 15;
self.swordThrown = false; // Flag for skeleton sword throw
self.initialThrowMade = undefined; // For skeleton's first throw logic
self.enteredScreenTime = 0;
self.throwDelay = 60;
} else {
// Basic enemy: uses a single 'enemy' asset if no animations defined.
// This case should ideally not occur if types are well-defined.
// If it does, it will likely error out on animation calls.
// Consider adding a default 'idle' animation if 'basic' is a valid type.
var enemyGraphics = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.sprites.push(enemyGraphics); // Add to sprites array to avoid errors in hideAllSprites
}
self.originalSpeed = self.speed;
// --- Animation System Methods ---
self.hideAllSprites = function () {
for (var i = 0; i < self.sprites.length; i++) {
if (self.sprites[i]) self.sprites[i].alpha = 0;
}
};
self.initializeSprites = function () {
self.sprites = [];
self.animationOffsets = {};
var currentGlobalSpriteIndex = 0;
var isFirstAnimationCategory = true;
var animationCategoriesOrder = ['run', 'fly', 'hit', 'die', 'attack', 'throw'];
for (var i = 0; i < animationCategoriesOrder.length; i++) {
var animCategoryName = animationCategoriesOrder[i];
if (self.animations[animCategoryName] && self.animations[animCategoryName].length > 0) {
self.animationOffsets[animCategoryName] = currentGlobalSpriteIndex;
var animFrames = self.animations[animCategoryName];
for (var j = 0; j < animFrames.length; j++) {
var sprite = self.attachAsset(animFrames[j], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = isFirstAnimationCategory && j === 0 ? 1 : 0;
self.sprites.push(sprite);
currentGlobalSpriteIndex++;
}
if (animFrames.length > 0) {
isFirstAnimationCategory = false;
}
}
}
};
self.setAnimation = function (animationType, loop, speed, forceRestart) {
if (!self.animations[animationType] || self.animations[animationType].length === 0) {
// console.error("Enemy: Animation not found or empty:", animationType, "for type", self.type);
return;
}
if (!forceRestart && self.currentAnimation.type === animationType) return;
self.currentAnimation.type = animationType;
self.currentAnimation.frames = self.animations[animationType];
self.currentAnimation.frameIndex = 0;
self.currentAnimation.counter = 0;
self.currentAnimation.loop = loop !== undefined ? loop : true;
self.currentAnimation.speed = speed || (self.type === 'skeleton' && animationType === 'run' ? self.animationSpeed * 0.8 : self.animationSpeed);
if (self.animationOffsets[animationType] !== undefined) {
self.currentAnimation.spriteOffset = self.animationOffsets[animationType];
} else {
// console.error("Enemy: Offset not found for animation:", animationType, "for type", self.type);
self.currentAnimation.type = null;
return;
}
self.hideAllSprites();
var firstSpriteOfNewAnim = self.sprites[self.currentAnimation.spriteOffset];
if (firstSpriteOfNewAnim) {
firstSpriteOfNewAnim.alpha = 1;
} else {
// console.error("Enemy: First sprite for animation " + animationType + " not found at offset " + self.currentAnimation.spriteOffset);
}
};
self.updateAnimation = function () {
var anim = self.currentAnimation;
if (!anim.type || !anim.frames || anim.frames.length === 0) {
return;
}
anim.counter += anim.speed * gameSpeedMultiplier; // Animation speed affected by game speed
var oldFrameSpriteIndex = anim.spriteOffset + anim.frameIndex;
if (anim.counter >= 1) {
anim.counter = 0;
anim.frameIndex++;
if (anim.frameIndex >= anim.frames.length) {
if (anim.loop) {
anim.frameIndex = 0;
} else {
anim.frameIndex = anim.frames.length - 1;
if (self.onAnimationEnd) {
self.onAnimationEnd(anim.type);
}
// For non-looping, current frame will be shown, but no further updates to frameIndex
}
}
}
// Hide all sprites then show the current one
// This is inefficient if done every frame. Only do on frame change.
// However, to ensure correctness if something else made a sprite visible:
self.hideAllSprites();
var currentFrameSpriteIndex = anim.spriteOffset + anim.frameIndex;
if (self.sprites[currentFrameSpriteIndex]) {
self.sprites[currentFrameSpriteIndex].alpha = 1;
} else {
// console.error("Enemy: Sprite for current frame not found: " + anim.type + ", frame " + anim.frameIndex);
}
};
self.onAnimationEnd = function (endedAnimationType) {
if (self.isHit && endedAnimationType === 'hit') {
self.isHit = false;
if (self.hitType === 'attack') {
self.isDying = true;
self.dropLoot();
self.setAnimation('die', false, self.animationSpeed, true);
} else {
// Slide or non-lethal hit
var normalAnimName = self.type === 'eyeball' ? 'fly' : 'run';
var normalSpeed = self.type === 'skeleton' && normalAnimName === 'run' ? self.animationSpeed * 0.8 : self.animationSpeed;
self.setAnimation(normalAnimName, true, normalSpeed, true);
}
self.hitType = 'none';
} else if (self.isDying && endedAnimationType === 'die') {
self.isFadingOut = true;
self.fadeOutTimer = 30; // Frames for fade out
} else if (self.type === 'skeleton' && self.isThrowing && endedAnimationType === 'throw') {
self.isThrowing = false;
self.swordThrown = false;
self.speed = self.originalSpeed;
self.setAnimation('run', true, self.animationSpeed * 0.8, true);
} else if (self.type === 'mushroom' && self.isAttacking && endedAnimationType === 'attack') {
self.isAttacking = false;
self.speed = self.originalSpeed;
self.attackCooldownTimer = self.attackCooldown;
self.setAnimation('run', true, self.animationSpeed, true);
}
};
// --- Core Logic Methods ---
self.getBounds = function () {
return {
left: self.x - self.hitboxWidth / 2,
right: self.x + self.hitboxWidth / 2,
top: self.y - self.hitboxHeight / 2,
bottom: self.y + self.hitboxHeight / 2
};
};
self.checkPlatformCollision = function () {
var onAnyPlatform = false;
for (var i = 0; i < platforms.length; i++) {
var platform = platforms[i];
var leftEdge = platform.x - PLATFORM_HALF_WIDTH;
var rightEdge = platform.x + PLATFORM_HALF_WIDTH;
if (self.x >= leftEdge && self.x <= rightEdge) {
// Landed on top
if (self.velocityY >= 0 && self.y < platform.y - ENEMY_PLATFORM_OFFSET + 5 && self.y + self.velocityY >= platform.y - ENEMY_PLATFORM_OFFSET - 5) {
if (platform instanceof CrumblePlatform) platform.trigger();
onAnyPlatform = true;
self.currentPlatform = platform;
self.y = platform.y - ENEMY_PLATFORM_OFFSET;
self.isOnGround = true;
self.velocityY = 0;
return true;
}
// Already on platform and y matches (for cases where velocityY might be 0 but still on platform)
if (Math.abs(self.y - (platform.y - ENEMY_PLATFORM_OFFSET)) < 5) {
if (platform instanceof CrumblePlatform) platform.trigger();
onAnyPlatform = true;
self.currentPlatform = platform;
self.y = platform.y - ENEMY_PLATFORM_OFFSET;
self.isOnGround = true;
self.velocityY = 0;
return true;
}
}
}
if (!onAnyPlatform) {
self.isOnGround = false;
// self.currentPlatform = null; // Don't nullify if falling off current, handle in movement
}
return false;
};
self.dropLoot = function () {
if (tutorialActive) return;
var lootMultiplier = player.lootMultiplier || 1.0;
var coinCount = Math.floor(GameUtils.randomRange(1, 6) * self.lootModifier * lootMultiplier);
var dropJewel = Math.random();
var jewelChance = 0.02 * self.lootModifier * lootMultiplier;
if (dropJewel < jewelChance) {
var diamond = new Coin('diamond');
diamond.x = self.x;
diamond.y = self.y;
diamond.velocityX = GameUtils.randomRange(-2, 2);
diamond.velocityY = -GameUtils.randomRange(6, 10);
game.addChild(diamond);
coins.push(diamond);
} else if (dropJewel < jewelChance * 2) {
var emerald = new Coin('emerald');
emerald.x = self.x;
emerald.y = self.y;
emerald.velocityX = GameUtils.randomRange(-2, 2);
emerald.velocityY = -GameUtils.randomRange(6, 10);
game.addChild(emerald);
coins.push(emerald);
} else if (dropJewel < jewelChance * 3) {
var ruby = new Coin('ruby');
ruby.x = self.x;
ruby.y = self.y;
ruby.velocityX = GameUtils.randomRange(-2, 2);
ruby.velocityY = -GameUtils.randomRange(6, 10);
game.addChild(ruby);
coins.push(ruby);
}
for (var i = 0; i < coinCount; i++) {
var coin = new Coin();
coin.x = self.x;
coin.y = self.y;
coin.velocityX = GameUtils.randomRange(-2, 2);
coin.velocityY = -GameUtils.randomRange(6, 10);
game.addChild(coin);
coins.push(coin);
}
};
// --- State Update Handlers ---
self.updateGroundMovement = function () {
self.x -= self.speed * gameSpeedMultiplier;
if (!self.isOnGround) {
var gravity = self.type === 'skeleton' || self.type === 'goblin' ? 0.7 : 0.75; // mushroom has slightly higher gravity
self.velocityY += gravity;
self.y += self.velocityY;
self.checkPlatformCollision();
}
if (self.currentPlatform) {
var stillOnPlatform = self.x >= self.currentPlatform.x - PLATFORM_HALF_WIDTH && self.x <= self.currentPlatform.x + PLATFORM_HALF_WIDTH;
if (!stillOnPlatform) {
var foundAnotherPlatform = false;
for (var i = 0; i < platforms.length; i++) {
var otherPlatform = platforms[i];
if (otherPlatform === self.currentPlatform) continue;
if (self.x >= otherPlatform.x - PLATFORM_HALF_WIDTH && self.x <= otherPlatform.x + PLATFORM_HALF_WIDTH && Math.abs(self.y - (otherPlatform.y - ENEMY_PLATFORM_OFFSET)) < 5) {
self.currentPlatform = otherPlatform;
self.y = otherPlatform.y - ENEMY_PLATFORM_OFFSET; // Snap to new platform
self.isOnGround = true; // Ensure is on ground
self.velocityY = 0;
foundAnotherPlatform = true;
break;
}
}
if (!foundAnotherPlatform) {
self.isOnGround = false;
self.currentPlatform = null;
if (self.velocityY === 0) self.velocityY = 0.1; // Start falling
}
}
}
};
self.updateEyeballMovement = function () {
self.x -= self.speed * gameSpeedMultiplier;
if (self.homingTimer >= self.homingDelay) {
var deltaY = player.y - self.y;
self.velocityY += deltaY > 0 ? 0.2 : -0.2;
self.velocityY = Math.max(-self.maxVerticalSpeed, Math.min(self.maxVerticalSpeed, self.velocityY));
} else {
self.velocityY = Math.sin(self.homingTimer * 0.05) * self.verticalSpeed;
self.homingTimer++;
}
self.y += self.velocityY;
self.setAnimation('fly', true, self.animationSpeed);
};
self.updateSkeletonThrowing = function () {
self.x -= PLATFORM_SPEED * gameSpeedMultiplier; // Move with platform
self.setAnimation('throw', false, self.animationSpeed * 0.8); // Ensure throw animation is playing
var currentFrame = self.currentAnimation.frameIndex; // 0-indexed
var totalFrames = self.animations["throw"].length;
// Throw sword at a specific frame (e.g., frame 4 of 6, which is index 3)
// Original logic: (timer <= duration * 0.8) && (timer > duration * 0.8 - 2)
// New logic: Check frame index. If throw animation has 6 frames, sword thrown at frame 4 (index 3)
if (currentFrame === 3 && !self.swordThrown) {
// Assuming 6 frames, index 3 is 4th frame.
self.swordThrown = true;
var sword = new SkeletonSword();
sword.x = self.x - 100;
sword.y = self.y - 100;
sword.constantVelocity = 8;
game.addChild(sword);
skeletonSwords.push(sword);
}
// onAnimationEnd for 'throw' will handle transition back to run
};
self.checkMushroomAttack = function () {
// Renamed from updateMushroomAttack to indicate it's a check
if (self.type !== 'mushroom' || self.isHit || self.isDying || self.isAttacking || self.attackCooldownTimer > 0) {
return false;
}
var distanceX = Math.abs(player.x - self.x);
var distanceY = Math.abs(player.y - self.y);
if (distanceX < 800 && distanceY < 700 && self.x > player.x) {
self.platformOffset = self.currentPlatform ? self.x - self.currentPlatform.x : 0;
self.isAttacking = true;
self.attackTimer = self.attackDuration; // Used for overall duration, not frame animation
self.sporesReleased = false;
self.originalSpeed = self.speed;
self.speed = 0; // Stop moving during attack
self.setAnimation('attack', false, self.animationSpeed, true);
LK.getSound('mushroomhiss').play(); // Play sound at start of attack
return true;
}
return false;
};
self.performMushroomAttackLogic = function () {
// New function for attack execution
if (self.currentPlatform) {
self.x = self.currentPlatform.x + (self.platformOffset || 0);
} else {
self.x -= PLATFORM_SPEED * gameSpeedMultiplier; // Should not happen if attack starts on platform
}
// Release spores at frame 3 (index 2) of attack animation
if (self.currentAnimation.type === 'attack' && self.currentAnimation.frameIndex === 2 && !self.sporesReleased) {
self.releaseSpores();
self.sporesReleased = true;
}
// onAnimationEnd for 'attack' will handle transition back to run
};
self.releaseSpores = function () {
var directions = [{
x: 1,
y: 0
}, {
x: -1,
y: 0
}, {
x: 0,
y: -1
}, {
x: 0.7,
y: -0.7
}, {
x: -0.7,
y: -0.7
}];
var offsetX = self.x;
var offsetY = self.y - 50;
LK.getSound('mushroomburst').play();
for (var i = 0; i < directions.length; i++) {
var spore = new Spore();
spore.x = offsetX;
spore.y = offsetY;
spore.velocityX = directions[i].x * spore.initialSpeed; // Use initialSpeed
spore.velocityY = directions[i].y * spore.initialSpeed;
game.addChild(spore);
if (!window.spores) window.spores = [];
window.spores.push(spore);
}
};
self.updateHitStateInternal = function () {
self.x += self.throwBackSpeed;
self.throwBackDistance += Math.abs(self.throwBackSpeed); // Not strictly used but kept for consistency
self.throwBackSpeed *= 0.95;
if (self.type === 'eyeball') {
self.y += self.velocityY; // Vertical trajectory applied in hit()
self.velocityY *= 0.9; // Dampen vertical for eyeball
} else {
// Ground enemies
if (self.currentPlatform) {
var stillOnPlatform = self.x >= self.currentPlatform.x - PLATFORM_HALF_WIDTH && self.x <= self.currentPlatform.x + PLATFORM_HALF_WIDTH;
if (!stillOnPlatform) {
/* ... logic to find new platform or fall ... */
var foundAnotherPlatform = false;
for (var i = 0; i < platforms.length; i++) {
var otherPlatform = platforms[i];
if (otherPlatform === self.currentPlatform) continue;
if (self.x >= otherPlatform.x - PLATFORM_HALF_WIDTH && self.x <= otherPlatform.x + PLATFORM_HALF_WIDTH && Math.abs(self.y - (otherPlatform.y - ENEMY_PLATFORM_OFFSET)) < 5) {
self.currentPlatform = otherPlatform;
self.y = otherPlatform.y - ENEMY_PLATFORM_OFFSET;
self.isOnGround = true;
self.velocityY = 0;
foundAnotherPlatform = true;
break;
}
}
if (!foundAnotherPlatform) {
self.isOnGround = false;
self.currentPlatform = null;
if (self.velocityY === 0) self.velocityY = 0.1;
}
}
}
if (!self.isOnGround) {
var gravity = self.type === 'skeleton' ? 0.7 : 0.75;
self.velocityY += gravity;
self.y += self.velocityY;
self.checkPlatformCollision();
}
}
// Animation transition handled by onAnimationEnd('hit')
};
self.updateDyingStateInternal = function () {
if (self.type === 'eyeball') {
self.velocityY += 0.5; // Gravity for eyeball
self.y += self.velocityY;
self.x -= PLATFORM_SPEED * 0.5 * gameSpeedMultiplier; // Eyeball drift
} else {
// Ground enemies
self.x += self.throwBackSpeed;
self.throwBackSpeed *= 0.95;
if (self.currentPlatform) {
/* ... platform boundary check ... */
var stillOnPlatform = self.x >= self.currentPlatform.x - PLATFORM_HALF_WIDTH && self.x <= self.currentPlatform.x + PLATFORM_HALF_WIDTH;
if (!stillOnPlatform) {
var foundAnotherPlatform = false;
for (var i = 0; i < platforms.length; i++) {
var otherPlatform = platforms[i];
if (otherPlatform === self.currentPlatform) continue;
if (self.x >= otherPlatform.x - PLATFORM_HALF_WIDTH && self.x <= otherPlatform.x + PLATFORM_HALF_WIDTH && Math.abs(self.y - (otherPlatform.y - ENEMY_PLATFORM_OFFSET)) < 5) {
self.currentPlatform = otherPlatform;
self.y = otherPlatform.y - ENEMY_PLATFORM_OFFSET;
self.isOnGround = true;
self.velocityY = 0;
foundAnotherPlatform = true;
break;
}
}
if (!foundAnotherPlatform) {
self.isOnGround = false;
self.currentPlatform = null;
if (self.velocityY === 0) self.velocityY = 0.1;
}
}
}
if (!self.isOnGround) {
var gravity = self.type === 'skeleton' ? 0.7 : 0.7;
self.velocityY += gravity;
self.y += self.velocityY;
self.checkPlatformCollision();
}
// Match platform speed after half of die animation
var dieAnimFrames = self.animations.die ? self.animations.die.length : 0;
if (dieAnimFrames > 0 && self.currentAnimation.frameIndex >= Math.floor(dieAnimFrames / 2)) {
self.x -= PLATFORM_SPEED * gameSpeedMultiplier;
}
}
// Fade out handled by onAnimationEnd('die') -> isFadingOut flag
if (self.isFadingOut) {
self.alpha -= 1 / self.fadeOutTimer;
if (self.alpha <= 0) {
self.destroy();
}
}
};
// --- Hit Handlers ---
self.hit = function () {
if (self.isHit || self.isDying) return;
self.isHit = true;
self.hitType = 'attack';
self.throwBackSpeed = 25;
self.throwBackDistance = 0; // Reset
var verticalTrajectory = 0;
if (self.type === 'eyeball') {
var yDifference = self.y - player.y;
var heightThreshold = 100;
if (yDifference < -heightThreshold) verticalTrajectory = -20;else if (yDifference > heightThreshold) verticalTrajectory = 14;else verticalTrajectory = -2;
self.velocityY = verticalTrajectory;
} else {
self.velocityY = 0;
}
var soundToPlay = 'enemyhit';
if (self.type === 'eyeball') soundToPlay = 'eyeballhit';else if (self.type === 'skeleton') soundToPlay = 'skeletonhit';else if (self.type === 'mushroom') soundToPlay = 'mushroomdie';
LK.getSound(soundToPlay).play();
particleSystem.emitFromHit(self.x + (self.type === 'eyeball' ? 175 : 250), self.y, player.x, verticalTrajectory, self.type);
self.setAnimation('hit', false, self.animationSpeed * 1.5, true); // Play hit animation faster
};
self.slideHit = function () {
if (self.isHit || self.isDying) return;
self.isHit = true;
self.hitType = player.slideDamage && player.slideDamageTimer <= 0 ? 'attack' : 'slide';
if (self.hitType === 'attack' && player.slideDamageTimer <= 0) player.slideDamageTimer = player.slideDamageCooldown;
self.throwBackSpeed = 25;
self.throwBackDistance = 0;
var verticalTrajectory = 0;
if (self.type === 'eyeball') {
var yDifference = self.y - player.y;
var heightThreshold = 100;
if (yDifference < -heightThreshold) verticalTrajectory = -20;else if (yDifference > heightThreshold) verticalTrajectory = 14;else verticalTrajectory = -2;
self.velocityY = verticalTrajectory;
}
var soundToPlay = 'enemyhit';
if (self.type === 'eyeball') soundToPlay = 'eyeballhit';else if (self.type === 'skeleton') soundToPlay = 'skeletonhit';else if (self.type === 'mushroom') {
soundToPlay = player.slideDamage && self.hitType === 'attack' ? 'mushroomdie' : 'mushroombounce';
}
LK.getSound(soundToPlay).play();
particleSystem.emitFromHit(self.x + (self.type === 'eyeball' ? 175 : 250), self.y, player.x, verticalTrajectory, self.type);
self.setAnimation('hit', false, self.animationSpeed * 1.5, true);
};
// --- Main Update Method ---
self.update = function () {
if (self.isDying) {
self.updateDyingStateInternal();
} else if (self.isHit) {
self.updateHitStateInternal();
} else {
// Normal state
if (self.type === 'eyeball') {
self.updateEyeballMovement();
} else if (self.type === 'skeleton') {
if (self.isThrowing) {
self.updateSkeletonThrowing();
} else {
self.updateGroundMovement();
// Skeleton specific pre-movement logic for initial throw
if (self.initialThrowMade === undefined) {
self.initialThrowMade = false;
self.enteredScreenTime = LK.ticks;
}
if (!self.isThrowing && !self.initialThrowMade && self.x < GAME_WIDTH + 300 && self.isOnGround) {
if (LK.ticks - self.enteredScreenTime > self.throwDelay) {
self.isThrowing = true;
self.swordThrown = false;
self.throwingTimer = self.throwingDuration;
self.originalSpeed = self.speed;
self.speed = 0; // Stop during throw setup
self.initialThrowMade = true;
self.setAnimation('throw', false, self.animationSpeed * 0.8, true); // Switch to throw animation
}
}
if (!self.isThrowing) self.setAnimation('run', true, self.animationSpeed * 0.8);
}
} else if (self.type === 'mushroom') {
if (self.checkMushroomAttack()) {
// checkMushroomAttack now sets isAttacking and animation
} else {
if (self.attackCooldownTimer > 0) self.attackCooldownTimer--;
self.updateGroundMovement();
self.setAnimation('run', true);
}
if (self.isAttacking) {
//If attack was initiated by checkMushroomAttack
self.performMushroomAttackLogic(); // Execute attack logic
}
} else {
// Goblin or other ground enemies
self.updateGroundMovement();
self.setAnimation('run', true);
}
}
self.updateAnimation();
if (self.x < -GAME_WIDTH / 2 - 100 || self.y > GAME_HEIGHT + 200) {
self.destroy();
}
};
// --- Initialization ---
self.initializeSprites();
var initialAnim = self.type === 'eyeball' ? 'fly' : 'run';
var initialSpeed = self.animationSpeed;
if (self.type === 'skeleton' && initialAnim === 'run') initialSpeed *= 0.8;
// Ensure animations for the type exist before setting
if (self.animations[initialAnim] && self.animations[initialAnim].length > 0) {
self.setAnimation(initialAnim, true, initialSpeed);
} else if (Object.keys(self.animations).length > 0) {
// Fallback to the first defined animation if 'run' or 'fly' isn't available
var firstKey = Object.keys(self.animations)[0];
self.setAnimation(firstKey, true, self.animationSpeed);
} else if (self.type === 'basic' && self.sprites.length > 0) {
// For 'basic' enemy with just one sprite, ensure it's visible
self.sprites[0].alpha = 1;
}
return self;
});
var HealthPotion = Container.expand(function () {
var self = Container.call(this);
// Create sprite
self.sprite = self.attachAsset('healthpotion', {
anchorX: 0.5,
anchorY: 0.8
});
// Initialize physics properties
CollectibleBehavior.initPhysics(self);
// Collection functionality
self.collect = function () {
if (!self.collected && player.currentHealth < player.maxHealth) {
self.collected = true;
player.currentHealth++;
player.heartContainer.updateHealth(player.currentHealth);
player.heartContainer.alpha = 1;
player.heartVisibilityTimer = player.heartVisibilityDuration;
// Create heart popup
var popup = LK.getAsset('heart', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xFF0000
});
popup.scaleX = 1.6; // Adjust scale to match original size
popup.scaleY = 1.6;
popup.x = self.x - 50;
popup.y = self.y - 200;
popup.velocityY = -3;
popup.lifespan = 45;
popup.update = function () {
this.y += this.velocityY;
this.lifespan--;
if (this.lifespan < 15) {
this.alpha -= 0.07;
}
if (this.alpha <= 0 || this.lifespan <= 0) {
this.destroy();
}
};
game.addChild(popup);
LK.getSound('potion').play();
self.destroy();
}
};
// Standard update using shared behavior
self.update = function () {
CollectibleBehavior.standardUpdate(self);
};
self.checkPlatformCollision = function () {
return GameUtils.checkPlatformCollision(self, 80, true) != null;
};
return self;
});
var HeartContainer = Container.expand(function () {
var self = Container.call(this);
self.maxHealth = 3;
self.currentHealth = 3;
self.hearts = [];
// Calculate total width of heart display
var heartSpacing = 80;
var totalWidth = (self.maxHealth - 1) * heartSpacing;
// Initialize hearts with centered positioning
for (var i = 0; i < self.maxHealth; i++) {
var heart = self.attachAsset('heart', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xCC0000
});
heart.x = i * heartSpacing - totalWidth / 2;
heart.y = 0;
self.hearts.push(heart);
}
// Start invisible
self.alpha = 0;
// NEW METHOD: Update max health and add/remove heart sprites as needed
self.updateMaxHealth = function (newMaxHealth) {
var heartSpacing = 80;
// Add new hearts if needed
if (newMaxHealth > self.maxHealth) {
for (var i = self.maxHealth; i < newMaxHealth; i++) {
var heart = self.attachAsset('heart', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xCC0000
});
self.hearts.push(heart);
}
}
// Remove hearts if needed (unlikely, but for completeness)
else if (newMaxHealth < self.maxHealth) {
for (var i = self.maxHealth - 1; i >= newMaxHealth; i--) {
if (self.hearts[i]) {
self.hearts[i].destroy();
self.hearts.pop();
}
}
}
// Update max health value
self.maxHealth = newMaxHealth;
// Recalculate total width and reposition all hearts
totalWidth = (self.maxHealth - 1) * heartSpacing;
for (var i = 0; i < self.hearts.length; i++) {
self.hearts[i].x = i * heartSpacing - totalWidth / 2;
}
};
self.updateHealth = function (newHealth) {
self.currentHealth = newHealth;
// Update heart display
for (var i = 0; i < self.maxHealth; i++) {
if (i < newHealth) {
// Full heart
self.hearts[i].tint = 0xFF0000;
self.hearts[i].alpha = 1;
} else {
// Empty heart
self.hearts[i].tint = 0x000000;
self.hearts[i].alpha = 0.5;
}
}
};
return self;
});
var Jar = Container.expand(function () {
var self = Container.call(this);
// Attach jar sprite
self.sprite = self.attachAsset('jar', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xC0C0C0
});
// Initialize as breakable
BreakableBehavior.initBreakable(self);
// Break functionality
self["break"] = function () {
BreakableBehavior.standardBreak(self, JarPiece, 4, function (jar) {
// Get loot multiplier from player
var lootMultiplier = player.lootMultiplier || 1.0;
// Spawn health potion with low chance - improved by loot multiplier
if (Math.random() < 0.05 * (player.potionDropChance || 1.0)) {
var potion = new HealthPotion();
potion.x = jar.x;
potion.y = jar.y;
potion.velocityX = GameUtils.randomRange(2, 6); // Reduced horizontal velocity
potion.velocityY = -GameUtils.randomRange(8, 14); // Reduced vertical velocity
game.addChild(potion);
coins.push(potion);
}
// Add chance for arrow - improved by loot multiplier
if (Math.random() < 0.25 * (player.arrowDropChance || 1.0)) {
var arrow = new ArrowPickup();
arrow.x = jar.x;
arrow.y = jar.y;
arrow.velocityX = GameUtils.randomRange(2, 6);
arrow.velocityY = -GameUtils.randomRange(8, 14);
// Set rotation to 90 degrees counterclockwise
arrow.sprite.rotation = -Math.PI / 2;
arrow.platformOffset = 200;
game.addChild(arrow);
coins.push(arrow);
}
// Spawn coins - quantity affected by loot multiplier
var coinCount = Math.floor(GameUtils.randomRange(1, 9) * lootMultiplier);
for (var i = 0; i < coinCount; i++) {
var coin = new Coin();
coin.x = jar.x;
coin.y = jar.y;
coin.velocityX = GameUtils.randomRange(2, 6); // Reduced horizontal velocity
coin.velocityY = -GameUtils.randomRange(8, 14); // Reduced vertical velocity
game.addChild(coin);
coins.push(coin);
}
LK.getSound('jarbreak').play();
});
};
return self;
});
var JarPiece = Container.expand(function (pieceNum) {
var self = Container.call(this);
// Attach piece sprite
self.sprite = self.attachAsset('jarpiece' + pieceNum, {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xC0C0C0
});
// Initialize as piece
PieceBehavior.initPiece(self);
// Update method
self.update = function () {
PieceBehavior.standardUpdate(self);
};
self.checkPlatformCollision = function () {
return GameUtils.checkPlatformCollision(self, 80, true) != null;
};
return self;
});
var ParticlePool = Container.expand(function (maxParticles) {
var self = Container.call(this);
self.particles = [];
self.activeParticles = [];
self.redTints = [0xff0000, 0xff3333, 0xcc0000];
for (var i = 0; i < maxParticles; i++) {
var particle = self.attachAsset('pixel', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.2,
scaleY: 0.2
});
particle.alpha = 0;
particle.velocityX = 0;
particle.velocityY = 0;
particle.lifespan = 0;
particle.fadeSpeed = 0;
self.particles.push(particle);
}
self.emitFromHit = function (x, y, playerX, verticalTrajectory, enemyType) {
var directionX = x - playerX;
var directionSign = Math.sign(directionX);
for (var i = 0; i < 20; i++) {
if (self.particles.length === 0) {
break;
}
var particle = self.particles.pop();
self.activeParticles.push(particle);
particle.x = x;
particle.y = y;
particle.alpha = 1;
// Only apply red tint if not skeleton enemy type
if (enemyType !== 'skeleton') {
particle.tint = self.redTints[Math.floor(Math.random() * self.redTints.length)];
} else {
particle.tint = 0xFFFFFF; // White particles for skeleton
}
// Set scale
var particleSize = Math.random() * 0.2 + 0.2;
particle.scaleX = particleSize;
particle.scaleY = particleSize;
var angle = Math.random() * Math.PI / 2 - Math.PI / 4;
var speed = Math.random() * 5 + 10;
particle.velocityX = Math.cos(angle) * speed * directionSign;
// Use provided vertical trajectory if available, otherwise use default calculation
if (verticalTrajectory !== undefined) {
// Add some randomness while maintaining the general direction
var randomFactor = Math.random() * 0.5 + 0.75; // 0.75 to 1.25 range
particle.velocityY = verticalTrajectory * randomFactor;
} else {
particle.velocityY = Math.sin(angle) * speed;
}
particle.lifespan = 100;
particle.fadeSpeed = 1 / 60;
}
};
self.update = function () {
for (var i = self.activeParticles.length - 1; i >= 0; i--) {
var particle = self.activeParticles[i];
particle.x += particle.velocityX;
particle.y += particle.velocityY;
particle.alpha -= particle.fadeSpeed;
particle.lifespan--;
if (particle.lifespan <= 0 || particle.alpha <= 0) {
particle.alpha = 0;
self.activeParticles.splice(i, 1);
self.particles.push(particle);
}
}
};
return self;
});
var Platform = Container.expand(function () {
var self = Container.call(this);
var platformGraphics = self.attachAsset('platform', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = PLATFORM_SPEED;
self.passed = false;
self.update = function () {
self.x -= PLATFORM_SPEED * gameSpeedMultiplier;
if (self.x < -500) {
self.destroy();
}
};
return self;
});
var CrumblePlatform = Platform.expand(function () {
var self = Platform.call(this);
// Override the sprite
self.sprite = self.attachAsset('crumbleplatform', {
anchorX: 0.5,
anchorY: 0.5
});
self.originalY = 0;
// Crumbling state
self.isCrumbling = false;
self.crumbleDelay = 60; // frames before breaking
self.crumbleTimer = 0;
self.triggered = false;
self.broken = false; // Add this flag to prevent multiple breaks
// Override update method
self.update = function () {
// Normal platform movement
self.x -= PLATFORM_SPEED * gameSpeedMultiplier;
// Handle crumbling state
if (self.isCrumbling && !self.broken) {
// Quick shake effect
var shakeIntensity = Math.min(6, 2 + (self.crumbleDelay - self.crumbleTimer) / 5);
self.sprite.y = Math.random() * shakeIntensity * 2 - shakeIntensity;
self.crumbleTimer--;
// Only break when the timer is actually finished
if (self.crumbleTimer <= 0) {
self["break"]();
}
}
// Check if off screen
if (self.x < -500) {
self.destroy();
}
};
self.trigger = function () {
if (!self.triggered && !self.broken) {
LK.getSound('platformcrumble').play();
self.triggered = true;
self.isCrumbling = true;
self.crumbleTimer = self.crumbleDelay;
self.sprite.originalY = self.sprite.y;
}
};
self["break"] = function () {
if (self.broken) {
return;
}
self.broken = true;
LK.getSound('rocksfall').play();
// CRITICAL FIX: Remove from platforms array BEFORE updating entities
var index = platforms.indexOf(self);
if (index !== -1) {
platforms.splice(index, 1);
}
// Force all entities on this platform to fall
if (player && player.currentPlatform === self) {
player.isOnGround = false;
player.currentPlatform = null;
player.velocityY = 3; // Stronger initial velocity
}
// Check all enemies too
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].currentPlatform === self) {
enemies[i].isOnGround = false;
enemies[i].currentPlatform = null;
enemies[i].velocityY = 1;
}
}
// Spawn 10 pieces distributed across the platform width
for (var i = 0; i < 10; i++) {
var pieceNum = Math.floor(Math.random() * 3) + 1;
var piece = new CrumblePiece(pieceNum);
// Distribute pieces across the platform width
var distributionOffset = i / 9 * PLATFORM_WIDTH - PLATFORM_HALF_WIDTH;
piece.x = self.x + distributionOffset + (Math.random() * 80 - 40); // Add small random offset
piece.y = self.y + (Math.random() * 30 - 15); // Small vertical variation
// Vary the falling speeds and behaviors
piece.velocityX = -PLATFORM_SPEED + (Math.random() * 2 - 1); // Slight x variation
piece.velocityY = Math.random() * 3; // Some pieces fall faster than others
piece.rotationSpeed = (Math.random() - 0.5) * 0.25; // More varied rotation
game.addChild(piece);
}
self.destroy();
};
return self;
});
// Player class with refactored animation management
var Player = Container.expand(function () {
var self = Container.call(this);
// Animation properties
self.runAnimation = ['playerrun1', 'playerrun2', 'playerrun3', 'playerrun4', 'playerrun5', 'playerrun6'];
self.jumpAnimation = ['playerjump1', 'playerjump2', 'playerjump3'];
self.attackAnimation = ['playerattack1', 'playerattack2', 'playerattack3', 'playerattack4', 'playerattack5'];
self.airAttackAnimation = ['playerairattack1', 'playerairattack2', 'playerairattack3', 'playerairattack4'];
self.slideAnimation = ['playerslide1', 'playerslide2'];
self.standUpAnimation = ['playerstand1', 'playerstand2', 'playerstand3'];
self.deathAnimation = ['playerdie1', 'playerdie2', 'playerdie3', 'playerdie4', 'playerdie5', 'playerdie6', 'playerdie7'];
self.bowAnimation = ['playerbow4', 'playerbow5', 'playerbow6', 'playerbow7', 'playerbow8', 'playerbow9'];
self.airDashAnimation = ['playerairdash'];
self.groundSlamAnimation = ['playergroundslam1', 'playergroundslam2', 'playergroundslam3', 'playergroundslam4', 'playergroundslam5', 'playergroundslam6'];
// Bow properties
self.isShooting = false;
self.bowFrame = 0;
self.bowCooldown = 20; // Frames of cooldown
self.bowCooldownTimer = 0;
self.ammoCount = 5;
// Animation states
self.isAttacking = false;
self.attackFrame = 0;
self.runFrame = 0;
self.animationSpeed = 0.092;
self.attackAnimationSpeed = 0.15;
self.animationCounter = 0;
self.sprites = [];
// Death animation properties
self.isDying = false;
self.deathFrame = 0;
self.deathTimer = 0;
self.deathDuration = 100;
self.deathAnimationSpeed = 0.1;
// Physics properties
self.groundY = GAME_HEIGHT * 0.9;
self.hitboxWidth = 150;
self.hitboxHeight = 300;
self.attackHitboxWidth = 200;
self.attackHitboxHeight = 400;
self.attackHitboxOffset = 50;
// Platform collision properties
self.isOnGround = true;
self.currentPlatform = null;
// Health properties
self.heartContainer = heartContainer;
self.maxHealth = 3;
self.currentHealth = 3;
self.isInvulnerable = false;
self.invulnerabilityDuration = 90;
self.invulnerabilityTimer = 0;
self.heartVisibilityTimer = 0;
self.heartVisibilityDuration = 120;
// Movement properties
self.speed = 5;
self.jumpHeight = 40;
self.isJumping = false;
self.velocityY = 0;
self.jumpState = "none";
self.jumpStartTime = 0;
self.isSliding = false;
self.slideTimer = 0;
self.slideDuration = 100;
self.standUpDuration = 30;
self.slideCooldown = 30;
self.lastSlideTime = 0;
self.slideSpeedMultiplier = 1.75;
self.slideDamage = false; // Will be set based on upgrade
self.slideDamageCooldown = 15; // Frames between slide damage hits
self.slideDamageTimer = 0; // Current cooldown timer
self.normalHitboxHeight = self.hitboxHeight; // Store original height
self.slideHitboxHeight = 200; // Make it taller
self.slideHitboxYOffset = 150; // Move it much lower
self.isAirDashing = false;
self.canAirDash = true; // Reset when landing
self.airDashTimer = 0;
self.airDashDuration = 100; // Half of slide duration since it's faster
self.airDashSpeedMultiplier = 2.3; // Slightly faster than slide
self.shadowImages = [];
// Add ground slam properties
self.isGroundSlamming = false;
self.groundSlamFrame = 0;
self.groundSlamPhase = "start"; // "start", "air", "impact"
self.canGroundSlam = true; // Can only ground slam once per jump
self.groundSlamAnimationSpeed = 0.15;
self.groundSlamImpactTimer = 0;
// Initialize animation sprites
self.initAnimations = function () {
// Run animations
for (var i = 0; i < self.runAnimation.length; i++) {
var sprite = self.attachAsset(self.runAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = i === 0 ? 1 : 0;
self.sprites.push(sprite);
}
// Jump animations
for (var i = 0; i < self.jumpAnimation.length; i++) {
var sprite = self.attachAsset(self.jumpAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
// Attack animations
for (var i = 0; i < self.attackAnimation.length; i++) {
var sprite = self.attachAsset(self.attackAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
// Air attack animations
for (var i = 0; i < self.airAttackAnimation.length; i++) {
var sprite = self.attachAsset(self.airAttackAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
// Add slide animations
for (var i = 0; i < self.slideAnimation.length; i++) {
var sprite = self.attachAsset(self.slideAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
// Add stand up animations
for (var i = 0; i < self.standUpAnimation.length; i++) {
var sprite = self.attachAsset(self.standUpAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
// Add death animations
for (var i = 0; i < self.deathAnimation.length; i++) {
var sprite = self.attachAsset(self.deathAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
// Add bow animations
for (var i = 0; i < self.bowAnimation.length; i++) {
var sprite = self.attachAsset(self.bowAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
// Add air dash animation
for (var i = 0; i < self.airDashAnimation.length; i++) {
var sprite = self.attachAsset(self.airDashAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
for (var i = 0; i < self.groundSlamAnimation.length; i++) {
var sprite = self.attachAsset(self.groundSlamAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
};
// Call initialization
self.initAnimations();
// Get collision bounds
self.getBounds = function () {
// For collecting items, always use the full-size hitbox
return {
left: self.x - self.hitboxWidth / 2,
right: self.x + self.hitboxWidth / 2,
top: self.y - self.normalHitboxHeight / 2,
bottom: self.y + self.normalHitboxHeight / 2
};
};
// Get attack hitbox
self.getAttackBounds = function () {
if (!self.isAttacking || self.isSliding) {
// Add sliding check here
return null;
}
return {
left: self.x + (self.attackHitboxOffset - self.attackHitboxWidth / 2),
right: self.x + (self.attackHitboxOffset + self.attackHitboxWidth / 2),
top: self.y - self.attackHitboxHeight / 2,
bottom: self.y + self.attackHitboxHeight / 2
};
};
self.getSlideAttackBounds = function () {
if (!self.isSliding) {
return null;
}
return {
left: self.x + self.hitboxWidth / 2,
// Extend hitbox forward during slide
right: self.x + self.hitboxWidth / 2 + 100,
// Adjust width as needed
top: self.y - self.slideHitboxHeight / 2 + self.slideHitboxYOffset,
bottom: self.y + self.slideHitboxHeight / 2 + self.slideHitboxYOffset
};
};
self.getCollisionBounds = function () {
if (self.isSliding) {
return {
left: self.x - self.hitboxWidth / 2,
right: self.x + self.hitboxWidth / 2,
top: self.y - self.slideHitboxHeight / 2 + self.slideHitboxYOffset,
bottom: self.y + self.slideHitboxHeight / 2 + self.slideHitboxYOffset
};
}
return self.getBounds();
};
// Update heart container
self.updateHeartContainer = function () {
self.heartContainer.x = self.x + 80;
self.heartContainer.y = self.y - 200;
// Handle heart visibility
if (self.heartVisibilityTimer > 0) {
self.heartVisibilityTimer--;
if (self.heartVisibilityTimer <= 0) {
self.heartContainer.alpha = 0;
}
}
};
// Update invulnerability state
self.updateInvulnerability = function () {
if (self.isInvulnerable) {
self.invulnerabilityTimer--;
// Flash only the player, not the hearts
self.alpha = self.invulnerabilityTimer % 10 < 5 ? 0.5 : 1;
if (self.invulnerabilityTimer <= 0) {
self.isInvulnerable = false;
self.alpha = 1;
}
}
};
// Handle platform collision
self.handlePlatformCollision = function () {
// Check if on any platform
var onAnyPlatform = false;
for (var i = 0; i < platforms.length; i++) {
var platform = platforms[i];
// Use EXACT platform bounds for edge detection - no added buffer
var leftEdge = platform.x - PLATFORM_HALF_WIDTH;
var rightEdge = platform.x + PLATFORM_HALF_WIDTH;
// Check if player is within horizontal bounds of platform
if (self.x >= leftEdge && self.x <= rightEdge) {
// If we're at platform height and not jumping
if (Math.abs(self.y - (platform.y - PLAYER_PLATFORM_OFFSET)) < 5 && !self.isJumping) {
onAnyPlatform = true;
// Add this check for crumble platforms
if (platform instanceof CrumblePlatform) {
platform.trigger();
}
self.currentPlatform = platform;
self.y = platform.y - PLAYER_PLATFORM_OFFSET;
self.isOnGround = true;
self.velocityY = 0;
break;
}
}
}
// Handle falling state
if (!onAnyPlatform && !self.isJumping) {
self.isOnGround = false;
self.currentPlatform = null;
// Apply gravity
if (self.velocityY === 0) {
self.velocityY = 0.1;
}
}
// If on a platform, check if still above it - use EXACT edge detection
if (self.currentPlatform) {
// Add check for destroyed platform
if (self.currentPlatform.destroyed || self.currentPlatform.broken) {
self.isOnGround = false;
self.currentPlatform = null;
if (self.velocityY === 0) {
self.velocityY = 0.1;
}
return;
}
// Use EXACT platform bounds - no buffer for edge detection when moving
var stillOnPlatform = self.x >= self.currentPlatform.x - PLATFORM_HALF_WIDTH && self.x <= self.currentPlatform.x + PLATFORM_HALF_WIDTH;
if (!stillOnPlatform) {
var foundAnotherPlatform = false;
for (var i = 0; i < platforms.length; i++) {
var otherPlatform = platforms[i];
if (otherPlatform === self.currentPlatform) {
continue;
}
// Use EXACT platform bounds for other platforms too
if (self.x >= otherPlatform.x - PLATFORM_HALF_WIDTH && self.x <= otherPlatform.x + PLATFORM_HALF_WIDTH) {
// Found another platform
self.currentPlatform = otherPlatform;
foundAnotherPlatform = true;
break;
}
}
// Fall if no other platform found
if (!foundAnotherPlatform) {
self.isOnGround = false;
self.currentPlatform = null;
self.velocityY = 0.1;
}
}
}
};
// Apply physics (separated from animation) - fixed to check collisions for ALL falling cases
self.applyPhysics = function () {
// First, check if current platform reference is valid
if (self.currentPlatform && (self.currentPlatform.destroyed || self.currentPlatform instanceof CrumblePlatform && self.currentPlatform.broken)) {
// Force falling state if platform reference is to a broken/destroyed platform
self.isOnGround = false;
self.currentPlatform = null;
if (self.velocityY < 1) {
self.velocityY = 1; // Ensure downward movement begins
}
}
// Then apply normal physics
if (!self.isOnGround || self.isJumping) {
// Apply velocity and gravity
self.y += self.velocityY;
self.velocityY += 0.89; // Increase gravity effect
// Check for landing on platforms
var hitPlatform = self.checkPlatformCollision();
}
// Check for falling off screen - but instead of immediate game over,
// let player continue to fall, then trigger game over after 3 seconds
if (self.y > GAME_HEIGHT && !self.isDying && !self.dyingInAir) {
// Start death animation instead of immediately showing game over
SaveManager.save();
self.isDying = true;
self.deathTimer = 0;
self.deathFrame = 0;
// Set game speed multiplier to 0 when player dies
gameSpeedMultiplier = 0;
// Stop background music and play gameover sound
LK.stopMusic();
LK.getSound('gameover').play();
// Flash screen
LK.effects.flashScreen(0xff0000, 1000);
// Schedule game over call after 3 seconds
LK.setTimeout(function () {
LK.showGameOver();
}, 3000);
}
};
// Update attack animation
self.updateAttackAnimation = function () {
var attackOffset = self.runAnimation.length + self.jumpAnimation.length;
self.animationCounter += self.attackAnimationSpeed;
if (self.animationCounter >= 1) {
self.animationCounter = 0;
self.attackFrame++;
if (self.attackFrame >= self.attackAnimation.length) {
self.isAttacking = false;
self.attackFrame = 0;
}
}
self.sprites[attackOffset + self.attackFrame].alpha = 1;
};
// Update air attack animation
self.updateAirAttackAnimation = function () {
var airAttackOffset = self.runAnimation.length + self.jumpAnimation.length + self.attackAnimation.length;
self.animationCounter += self.attackAnimationSpeed;
if (self.animationCounter >= 1) {
self.animationCounter = 0;
self.attackFrame++;
if (self.attackFrame >= self.airAttackAnimation.length) {
self.isAttacking = false;
self.attackFrame = 0;
}
}
self.sprites[airAttackOffset + self.attackFrame].alpha = 1;
};
// Update jump animation (only animation, not physics)
self.updateJumpAnimation = function () {
var jumpOffset = self.runAnimation.length;
var currentTime = Date.now();
// Show appropriate jump frame
if (currentTime - self.jumpStartTime < 100) {
self.sprites[jumpOffset + 0].alpha = 1;
} else if (self.velocityY < 0) {
self.sprites[jumpOffset + 1].alpha = 1;
} else if (self.velocityY > 0) {
self.sprites[jumpOffset + 2].alpha = 1;
}
};
// Update slide animation
self.updateSlideAnimation = function () {
var slideOffset = self.runAnimation.length + self.jumpAnimation.length + self.attackAnimation.length + self.airAttackAnimation.length;
var standUpOffset = slideOffset + self.slideAnimation.length;
self.slideTimer--;
if (self.slideTimer > self.standUpDuration) {
// Main slide animation (alternate between first two frames)
self.animationCounter += self.animationSpeed;
if (self.animationCounter >= 1) {
self.animationCounter = 0;
}
var slideFrame = Math.floor(self.animationCounter * 2);
self.sprites[slideOffset + slideFrame].alpha = 1;
// Create dust particles behind player during slide (every 5 frames)
if (LK.ticks % 5 === 0) {
// Create 2-3 particles per emission
var particleCount = 2 + Math.floor(Math.random() * 2);
for (var i = 0; i < particleCount; i++) {
var particle = game.addChild(LK.getAsset('pixel', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.1 + Math.random() * 0.2,
scaleY: 0.1 + Math.random() * 0.2,
alpha: 0.7 + Math.random() * 0.3
}));
// Position particles behind player near ground level
particle.x = self.x + 120 - Math.random() * 50;
particle.y = self.y + 145 + Math.random() * 40;
// Set velocities with slight variation
particle.velocityX = -Math.random() * 3 - 1;
particle.velocityY = -Math.random() * 2;
// Add slight random rotation
particle.rotationSpeed = (Math.random() - 0.5) * 0.1;
particle.lifespan = 20 + Math.floor(Math.random() * 15);
// Add update method to particles
particle.update = function () {
this.x += this.velocityX;
this.y += this.velocityY;
this.rotation += this.rotationSpeed;
this.lifespan--;
if (this.lifespan < 10) {
this.alpha -= 0.1;
}
if (this.lifespan <= 0) {
this.destroy();
}
};
}
}
} else if (self.slideTimer > 0) {
// Stand up animation
var standUpFrame = Math.floor((self.standUpDuration - self.slideTimer) / (self.standUpDuration / self.standUpAnimation.length));
self.sprites[standUpOffset + standUpFrame].alpha = 1;
} else {
// End slide
if (self.slideTimer <= 0) {
self.isSliding = false;
self.hitboxHeight = self.normalHitboxHeight;
self.lastSlideTime = Date.now();
// Reset speed multiplier instead of base speed
gameSpeedMultiplier = 1.0;
}
}
};
self.updateAirDashAnimation = function () {
// Calculate offset to find air dash animation sprite
var airDashOffset = self.runAnimation.length + self.jumpAnimation.length + self.attackAnimation.length + self.airAttackAnimation.length + self.slideAnimation.length + self.standUpAnimation.length + self.deathAnimation.length + self.bowAnimation.length;
// Show air dash sprite
self.sprites[airDashOffset].alpha = 1;
// Add quick clockwise rotation during air dash
if (self.airDashTimer === self.airDashDuration) {
// Reset rotation at start of dash
self.rotation = 0;
// Play air dash sound effect
LK.getSound('airdash').play();
// Start rotation tween - make it happen faster and then hold value
tween(self, {
rotation: Math.PI / 8 // Rotate about 30 degrees clockwise
}, {
duration: 100,
// Much faster rotation (300ms instead of ~1.5s)
easing: tween.easeOut
// No onFinish needed since we'll hold this rotation for the dash duration
});
}
// Keep vertical velocity at 0 during dash
self.velocityY = 0;
// Update shadow positions with slight motion effect
self.shadowImages.forEach(function (shadow, index) {
// Add slight vertical variation to shadows for more dynamic feel
var verticalOffset = Math.sin(self.airDashTimer * 0.1 + index * 0.5) * 10;
// Adjust spacing and position for a more dramatic trail effect
shadow.x = self.x - (80 * (index + 1) + (self.airDashDuration - self.airDashTimer) * 0.5);
shadow.y = self.y + verticalOffset;
shadow.alpha = 0.3 - index * 0.05; // More gradual fade out for longer trails
// Add slight rotation to shadows
shadow.rotation = self.rotation * (0.6 - index * 0.1); // Less rotation for farther shadows
});
self.airDashTimer--;
if (self.airDashTimer <= 0) {
self.isAirDashing = false;
gameSpeedMultiplier = 1.0;
// Reset rotation back to normal when dash ends
tween(self, {
rotation: 0
}, {
duration: 300,
easing: tween.easeOut
});
// Clean up shadows
self.shadowImages.forEach(function (shadow) {
if (shadow.parent) {
shadow.destroy();
}
});
self.shadowImages = [];
}
};
self.updateBowAnimation = function () {
var bowOffset = self.runAnimation.length + self.jumpAnimation.length + self.attackAnimation.length + self.airAttackAnimation.length + self.slideAnimation.length + self.standUpAnimation.length + self.deathAnimation.length;
self.animationCounter += self.attackAnimationSpeed;
if (self.animationCounter >= 1) {
self.animationCounter = 0;
self.bowFrame++;
// Spawn arrow at frame 5
if (self.bowFrame === 5) {
// If in tutorial, always spawn arrow
if (tutorialActive) {
var arrow = new Arrow();
arrow.x = self.x + 100;
arrow.y = self.y + 20;
game.addChild(arrow);
arrows.push(arrow);
LK.getSound('arrowfire').play();
} else if (self.ammoCount > 0) {
var arrow = new Arrow();
arrow.x = self.x + 100;
arrow.y = self.y + 20;
game.addChild(arrow);
arrows.push(arrow);
LK.getSound('arrowfire').play();
} else {
// Play bow fire sound effect when no ammo
LK.getSound('bowfiring').play();
}
}
if (self.bowFrame >= self.bowAnimation.length) {
self.isShooting = false;
self.bowFrame = 0;
self.bowCooldownTimer = self.bowCooldown;
}
}
self.sprites[bowOffset + self.bowFrame].alpha = 1;
};
// Update death animation
self.updateDeathAnimation = function () {
// Calculate offset to find the death animation sprites
var deathOffset = self.runAnimation.length + self.jumpAnimation.length + self.attackAnimation.length + self.airAttackAnimation.length + self.slideAnimation.length + self.standUpAnimation.length;
// Progress timer
self.deathTimer++;
// Calculate frame based on timer
// We want the animation to play through all frames over the death duration
var frameProgress = self.deathTimer / self.deathDuration * self.deathAnimation.length;
var newFrame = Math.min(Math.floor(frameProgress), self.deathAnimation.length - 1);
// Update frame if it's changed
if (newFrame != self.deathFrame) {
self.deathFrame = newFrame;
}
// Show current frame
self.sprites[deathOffset + self.deathFrame].alpha = 1;
// Death animation is complete, but game over is handled by the timeout
if (self.deathTimer >= self.deathDuration) {
// Game over is now triggered by the timeout in takeDamage
}
};
// Update run animation
self.updateRunAnimation = function () {
self.animationCounter += self.animationSpeed;
if (self.animationCounter >= 1) {
self.animationCounter = 0;
self.runFrame = (self.runFrame + 1) % self.runAnimation.length;
}
self.sprites[self.runFrame].alpha = 1;
};
self.updateGroundSlamAnimation = function () {
// Calculate offset to find ground slam animation sprites
var groundSlamOffset = self.runAnimation.length + self.jumpAnimation.length + self.attackAnimation.length + self.airAttackAnimation.length + self.slideAnimation.length + self.standUpAnimation.length + self.deathAnimation.length + self.bowAnimation.length + self.airDashAnimation.length;
// Create hitbox for the slam (used both in air and on impact)
var slamHitbox = {
left: self.x - 200,
right: self.x + 200,
top: self.y,
bottom: self.y + 200
};
// Update animation based on phase
if (self.groundSlamPhase === "start") {
// Show first frame briefly
self.sprites[groundSlamOffset].alpha = 1;
self.animationCounter += self.attackAnimationSpeed;
if (self.animationCounter >= 1) {
self.groundSlamPhase = "air";
self.animationCounter = 0;
}
} else if (self.groundSlamPhase === "air") {
// Alternate between frames 2 and 3 while falling
self.animationCounter += self.attackAnimationSpeed;
if (self.animationCounter >= 1) {
self.animationCounter = 0;
self.groundSlamFrame = (self.groundSlamFrame + 1) % 2;
}
self.sprites[groundSlamOffset + 1 + self.groundSlamFrame].alpha = 1;
// Check for enemies and breakables during descent
self.checkGroundSlamHits(slamHitbox);
// Check if we've hit the ground
if (self.isOnGround) {
self.groundSlamPhase = "impact";
self.groundSlamFrame = 0;
self.groundSlamImpactTimer = 30;
self.animationCounter = 0;
// Apply screen shake effect on impact
applyScreenShake();
LK.getSound('groundimpact').play();
// Do one final more powerful hit check on impact
self.applyGroundSlamDamage();
}
} else if (self.groundSlamPhase === "impact") {
// Cycle through frames 4-6 for impact
self.animationCounter += self.attackAnimationSpeed * 1.5;
if (self.animationCounter >= 1) {
self.animationCounter = 0;
self.groundSlamFrame++;
if (self.groundSlamFrame >= 3) {
self.isGroundSlamming = false;
return;
}
}
self.sprites[groundSlamOffset + 3 + self.groundSlamFrame].alpha = 1;
// Count down timer
self.groundSlamImpactTimer--;
if (self.groundSlamImpactTimer <= 0) {
self.isGroundSlamming = false;
}
}
};
self.applyGroundSlamDamage = function () {
// Create larger hitbox for ground slam
var slamHitbox = {
left: self.x - 300,
right: self.x + 300,
top: self.y,
bottom: self.y + 200
};
// Add particle spray effect when landing
self.createGroundSlamParticles();
// Check all enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy.isHit && !enemy.isDying) {
var enemyBounds = enemy.getBounds();
// If enemy is on ground and in range
if (GameUtils.checkCollision(slamHitbox, enemyBounds) && (enemy.isOnGround || enemy.y > self.y)) {
enemy.hit();
// Play appropriate sound
if (enemy.type === 'eyeball') {
LK.getSound('eyeballhit').play();
} else if (enemy.type === 'skeleton') {
LK.getSound('skeletonhit').play();
} else {
LK.getSound('enemyhit').play();
}
// Create more particles for impact
particleSystem.emitFromHit(enemy.x + 250, enemy.y, self.x, 5, enemy.type);
}
}
}
};
// Add a new method to create ground slam particles
self.createGroundSlamParticles = function () {
// Create a large number of particles for dramatic effect
var particleCount = 25 + Math.floor(Math.random() * 10);
for (var i = 0; i < particleCount; i++) {
var particle = game.addChild(LK.getAsset('pixel', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.1 + Math.random() * 0.3,
scaleY: 0.1 + Math.random() * 0.3,
alpha: 0.7 + Math.random() * 0.3,
// Keep particles untinted (white)
tint: 0xFFFFFF
}));
// Position particles at the point of impact
particle.x = self.x + Math.random() * 300 - 150; // Spread horizontally
particle.y = self.y + 145; // Just above ground level
// Set velocities with upward trajectory
var angle = Math.PI / 2 + (Math.random() - 0.5) * 0.8; // Mostly upward with spread
var speed = 3 + Math.random() * 9; // Varying speeds
particle.velocityX = Math.cos(angle) * speed * (Math.random() > 0.5 ? 1 : -1);
particle.velocityY = -Math.sin(angle) * speed; // Negative for upward
// Add slight random rotation
particle.rotationSpeed = (Math.random() - 0.5) * 0.2;
// Longer lifespan for more dramatic effect
particle.lifespan = 25 + Math.floor(Math.random() * 20);
// Add update method to particles
particle.update = function () {
this.x += this.velocityX;
this.y += this.velocityY;
this.velocityY += 0.3; // Apply gravity
this.rotation += this.rotationSpeed;
this.lifespan--;
// Start fading out near end of life
if (this.lifespan < 10) {
this.alpha -= 0.1;
}
if (this.lifespan <= 0 || this.alpha <= 0) {
this.destroy();
}
};
}
};
self.checkGroundSlamHits = function (slamHitbox) {
// Check enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy.isHit && !enemy.isDying) {
var enemyBounds = enemy.getBounds();
if (GameUtils.checkCollision(slamHitbox, enemyBounds)) {
enemy.hit();
// Play appropriate sound
if (enemy.type === 'eyeball') {
LK.getSound('eyeballhit').play();
} else if (enemy.type === 'skeleton') {
LK.getSound('skeletonhit').play();
} else {
LK.getSound('enemyhit').play();
}
// Create particles
particleSystem.emitFromHit(enemy.x + 250, enemy.y, self.x, 5, enemy.type);
}
}
}
// Check for breakable objects (jars and chests)
for (var i = collectibles.length - 1; i >= 0; i--) {
var collectible = collectibles[i];
// Only check jars and chests
if (collectible instanceof Jar || collectible instanceof TreasureChest) {
var itemBounds = {
left: collectible.x - 50,
right: collectible.x + 50,
top: collectible.y - 75,
bottom: collectible.y + 75
};
if (GameUtils.checkCollision(slamHitbox, itemBounds)) {
collectible["break"]();
collectibles.splice(i, 1);
}
}
}
};
// Modified applyLandingSquash function
self.applyLandingSquash = function () {
// More extreme initial squash values for better visibility
var initialScaleX = 1.2;
var initialScaleY = 0.8;
var targetScaleX = 1.0;
var targetScaleY = 1.0;
var duration = 370; // ms - slightly longer duration
// Apply squash to the entire player container instead of individual sprites
self.scale.set(initialScaleX, initialScaleY);
// Clear any existing scale tweens
tween.stop(self.scale, {
x: true,
y: true
});
// Tween back to normal scale with a slight delay
LK.setTimeout(function () {
tween(self.scale, {
x: targetScaleX,
y: targetScaleY
}, {
duration: duration,
easing: tween.elasticOut // Keep the bouncier easing
});
}, 90); // Slightly longer delay
};
self.checkPlatformBelow = function () {
if (!self.currentPlatform) {
return null;
}
// Get current platform height
var currentHeight = self.currentPlatform.y;
// Find the closest platform below current one
var platformBelow = null;
var minDistance = Infinity;
for (var i = 0; i < platforms.length; i++) {
var platform = platforms[i];
// Skip if it's not below us or if it's our current platform
if (platform === self.currentPlatform || platform.y <= currentHeight) {
continue;
}
// Check if we're within platform width bounds
if (Math.abs(platform.x - self.x) < PLATFORM_HALF_WIDTH) {
var distance = platform.y - currentHeight;
if (distance < minDistance) {
minDistance = distance;
platformBelow = platform;
}
}
}
return platformBelow;
};
// Check platform collision - fix to properly detect platforms when falling
self.checkPlatformCollision = function () {
// Update timer first
if (self.brokenPlatformTimer > 0) {
self.brokenPlatformTimer--;
}
for (var i = 0; i < platforms.length; i++) {
var platform = platforms[i];
// Skip platforms that are broken or destroyed
if (platform.broken || platform.destroyed) {
continue;
}
// Skip this specific platform if it's the one that just broke
if (self.brokenPlatformTimer > 0 && platform === self.lastBrokenPlatform) {
continue;
}
// Keep the extra buffer for landing on platforms for a smoother experience
// but ensure it's consistent with other checks
if (self.velocityY > 0 && self.y < platform.y - PLAYER_PLATFORM_OFFSET && self.y + self.velocityY >= platform.y - PLAYER_PLATFORM_OFFSET - 20 && self.x >= platform.x - PLATFORM_HALF_WIDTH && self.x <= platform.x + PLATFORM_HALF_WIDTH) {
// Add this check for crumble platforms
if (platform instanceof CrumblePlatform) {
platform.trigger();
}
self.y = platform.y - PLAYER_PLATFORM_OFFSET;
self.velocityY = 0;
self.isJumping = false;
self.isOnGround = true;
self.currentPlatform = platform;
self.applyLandingSquash();
if (self.isOnGround) {
self.canAirDash = true;
self.canGroundSlam = true;
// Clear any remaining shadow images
self.shadowImages.forEach(function (shadow) {
if (shadow.parent) {
shadow.destroy();
}
});
self.shadowImages = [];
}
return true;
}
}
return false;
};
// Hide all sprites
self.hideAllSprites = function () {
for (var i = 0; i < self.sprites.length; i++) {
self.sprites[i].alpha = 0;
}
};
self.checkCollectibles = function () {
// Always check for collectibles regardless of state
for (var i = 0; i < coins.length; i++) {
var coin = coins[i];
var itemBounds = {
left: coin.x - 25,
right: coin.x + 25,
top: coin.y - 25,
bottom: coin.y + 25
};
if (GameUtils.checkCollision(self.getBounds(), itemBounds)) {
coin.collect();
}
}
};
// Jump method
self.jump = function () {
// Add a timestamp check specifically for tutorial
if (!self.lastJumpTime) {
self.lastJumpTime = 0;
}
var currentTime = Date.now();
if (tutorialActive && currentTime - self.lastJumpTime < 300) {
return; // Block rapid jumps in tutorial
}
self.lastJumpTime = currentTime;
if (self.isSliding || self.isShooting && self.bowFrame < 2) {
// Only prevent jumps at start of bow animation
return;
}
if (self.isOnGround) {
self.isJumping = true;
self.isOnGround = false;
self.velocityY = -self.jumpHeight;
self.jumpState = "start";
self.jumpStartTime = Date.now();
LK.getSound('playerjump').play();
self.currentPlatform = null;
if (tutorialManager && tutorialManager.currentState === tutorialManager.states.DOUBLE_JUMP) {
tutorialManager.firstJumpPerformed = true;
}
} else if (self.isJumping && self.velocityY < 10) {
// Small double-jump to reach higher platforms
self.velocityY = -self.jumpHeight * 0.7;
self.jumpStartTime = Date.now();
if (tutorialManager && tutorialManager.currentState === tutorialManager.states.DOUBLE_JUMP) {
tutorialManager.secondJumpPerformed = true;
}
}
};
// Add slide method
self.slide = function () {
var currentTime = Date.now();
if (!self.isSliding && self.isOnGround && !self.isJumping && !self.isAttacking && currentTime - self.lastSlideTime > self.slideCooldown) {
self.isSliding = true;
self.slideTimer = self.slideDuration;
self.hitboxHeight = self.slideHitboxHeight;
self.animationCounter = 0;
// Play slide sound effect
LK.getSound('slide').play();
// Adjust game speed multiplier instead of base speed
gameSpeedMultiplier = self.slideSpeedMultiplier;
}
};
self.airDash = function () {
if (!self.canAirDash || self.isOnGround || self.isSliding || self.isAttacking || self.isShooting || self.isAirDashing) {
return;
}
self.isAirDashing = true;
self.airDashTimer = self.airDashDuration;
self.canAirDash = false;
self.velocityY = 0;
// Create shadow images - increased from 2 to 4 for more dramatic effect
for (var i = 0; i < 4; i++) {
var shadow = game.addChild(LK.getAsset('playerairdash', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3 - i * 0.05,
// Adjusted alpha reduction to spread across more shadows
x: self.x - 80 * (i + 1),
// Adjusted spacing to create a tighter trail
y: self.y
}));
self.shadowImages.push(shadow);
}
gameSpeedMultiplier = self.airDashSpeedMultiplier;
};
// Attack method
self.attack = function () {
if (!self.isAttacking && !self.isSliding) {
// Add sliding check here
self.isAttacking = true;
self.attackFrame = 0;
self.animationCounter = 0;
LK.getSound('swordslash').play();
}
};
self.groundSlam = function () {
if (!self.hasGroundSlam) {
return;
}
// Only allow if in air, not already slamming, and can slam
if (!self.isOnGround && !self.isGroundSlamming && self.canGroundSlam && !self.isSliding && !self.isAttacking && !self.isAirDashing && !self.isShooting) {
self.isGroundSlamming = true;
self.canGroundSlam = false; // Use up the slam for this jump
self.groundSlamFrame = 0;
self.groundSlamPhase = "start";
self.animationCounter = 0;
// Boost downward velocity
self.velocityY = 15; // Strong downward force
// Play sound effect (can use sword slash for now)
LK.getSound('groundsmashfalling').play();
}
};
self.fallThrough = function () {
if (self.isOnGround && !self.isJumping && !self.isSliding && !self.isAttacking) {
var platformBelow = self.checkPlatformBelow();
if (platformBelow) {
// Disable platform collision temporarily
self.isOnGround = false;
self.currentPlatform = null;
self.velocityY = 5; // Start with a small downward velocity
// Re-enable platform collision after a short delay
LK.setTimeout(function () {
self.checkPlatformCollision = self.originalCheckPlatformCollision;
}, 250); // Adjust timing as needed
// Store the original collision check and temporarily disable it
self.originalCheckPlatformCollision = self.checkPlatformCollision;
self.checkPlatformCollision = function () {
return null;
};
}
}
};
self.shoot = function () {
// If in tutorial, only allow shooting during bow phase
if (tutorialActive && tutorialManager.currentState !== tutorialManager.states.BOW) {
return;
}
if (!self.isShooting && !self.isSliding && self.bowCooldownTimer <= 0) {
if (self.isOnGround || !self.isOnGround) {
// Allow shooting in air or ground
self.isShooting = true;
self.bowFrame = 0;
self.animationCounter = 0;
// If in tutorial, don't consume ammo
if (!tutorialActive) {
if (self.ammoCount <= 0) {
// Play bow fire sound effect when no ammo
LK.getSound('bowfiring').play();
return;
}
self.ammoCount--;
scoreManager.updateAmmo(self.ammoCount);
}
// If in air, pause vertical movement
if (!self.isOnGround) {
self.velocityY = 0;
}
}
}
};
// Take damage method
self.takeDamage = function () {
if (!self.isInvulnerable && !self.isDying) {
LK.getSound('playerouch').play();
self.currentHealth--;
self.heartContainer.updateHealth(self.currentHealth);
// Show hearts and set visibility timer
self.heartContainer.alpha = 1;
self.heartVisibilityTimer = self.heartVisibilityDuration;
// Visual feedback
self.isInvulnerable = true;
self.invulnerabilityTimer = self.invulnerabilityDuration;
// Flash player red
self.tint = 0xFF0000;
tween(self, {
tint: 0xFFFFFF
}, {
duration: 500,
easing: tween.easeOut
});
// Add red screen flash
LK.effects.flashScreen(0xff0000, 300);
// Existing damage code...
if (self.currentHealth <= 0) {
// Save before game over
SaveManager.save();
// If in the air, find the nearest platform to fall to
if (!self.isOnGround) {
// Start the dying sequence but let them fall first
self.dyingInAir = true;
// Find the closest platform below the player
var closestPlatform = null;
var closestDistance = Infinity;
for (var i = 0; i < platforms.length; i++) {
var platform = platforms[i];
// Check if platform is below player and within horizontal range
if (platform.y > self.y && Math.abs(platform.x - self.x) < PLATFORM_HALF_WIDTH) {
var distance = platform.y - self.y;
if (distance < closestDistance) {
closestDistance = distance;
closestPlatform = platform;
}
}
}
// If we found a platform, let player fall to it naturally
// Otherwise proceed with normal death sequence
if (!closestPlatform) {
startDeathSequence();
}
// Player will continue falling until they hit a platform or go off screen
// The death sequence will be triggered in the update method
} else {
// Start death sequence immediately if already on ground
startDeathSequence();
}
}
}
function startDeathSequence() {
// Start death animation
self.isDying = true;
self.deathTimer = 0;
self.deathFrame = 0;
// Set game speed multiplier to 0 when player dies
gameSpeedMultiplier = 0;
// Stop background music and play gameover sound
LK.stopMusic();
LK.getSound('gameover').play();
// Flash screen
LK.effects.flashScreen(0xff0000, 1000);
// Apply one final screen shake for dramatic effect
applyScreenShake();
// Schedule game over call after 2 seconds
LK.setTimeout(function () {
LK.showGameOver();
}, 2000);
}
};
// Main update method
self.update = function () {
// Update heart container
self.updateHeartContainer();
// Hide all sprites
self.hideAllSprites();
if (self.slideDamageTimer > 0) {
self.slideDamageTimer--;
}
if (self.isAirDashing) {
// Update air dash animation
self.updateAirDashAnimation();
}
// If player is dying but still in the air, apply physics to let them fall
if (self.dyingInAir) {
// Apply physics for falling
self.applyPhysics();
// Check if they've landed on a platform
if (self.isOnGround) {
// They've landed, now start the death sequence
self.dyingInAir = false;
self.isDying = true;
self.deathTimer = 0;
self.deathFrame = 0;
gameSpeedMultiplier = 0;
LK.stopMusic();
LK.getSound('gameover').play();
LK.effects.flashScreen(0xff0000, 1000);
applyScreenShake();
LK.setTimeout(function () {
LK.showGameOver();
}, 2000);
}
// Show falling animation
var jumpOffset = self.runAnimation.length;
self.sprites[jumpOffset + 2].alpha = 1; // Show falling frame
return;
}
// If player is dying normally, only update death animation
if (self.isDying) {
self.updateDeathAnimation();
return;
}
// Update bow cooldown
if (self.bowCooldownTimer > 0) {
self.bowCooldownTimer--;
}
// Apply physics FIRST - this now happens regardless of animation state
if (!self.isAirDashing) {
self.applyPhysics();
}
// Handle platform collision and falling
self.handlePlatformCollision();
// Handle invulnerability
self.updateInvulnerability();
// Update animations based on state
if (self.isGroundSlamming) {
self.updateGroundSlamAnimation();
} else if (self.isAirDashing) {
self.updateAirDashAnimation();
} else if (self.isShooting) {
self.updateBowAnimation();
} else if (self.isSliding) {
self.updateSlideAnimation();
self.checkCollectibles(); // Explicitly check during slide
} else if (self.isAttacking) {
if (self.isJumping || !self.isOnGround && !self.isSliding) {
self.updateAirAttackAnimation();
} else {
self.updateAttackAnimation();
}
} else if (self.isJumping || !self.isOnGround) {
self.updateJumpAnimation();
} else if (self.isOnGround) {
self.updateRunAnimation();
}
// Check for falling off screen
if (self.y > GAME_HEIGHT && !self.isDying) {
// Start death animation instead of immediately showing game over
self.isDying = true;
self.deathTimer = 0;
self.deathFrame = 0;
LK.effects.flashScreen(0xff0000, 1000);
}
};
return self;
});
var ScorePopup = Container.expand(function (x, y, amount) {
var self = Container.call(this);
// Create the text using CustomText instead of Text2
self.text = new CustomText('+' + amount, {
size: 70,
fill: 0xFFFFFF
});
self.addChild(self.text);
self.x = x;
self.y = y;
self.velocityY = -3;
self.lifespan = 45;
self.update = function () {
self.y += self.velocityY;
self.lifespan--;
if (self.lifespan < 15) {
self.alpha -= 0.07;
}
if (self.alpha <= 0 || self.lifespan <= 0) {
self.destroy();
}
};
return self;
});
var ShopManager = Container.expand(function () {
var self = Container.call(this);
self.categories = ['Health', 'Combat', 'Ranged', 'Loot', 'Spells'];
self.currentCategory = 'Health';
self.currentUpgradeIndex = 0; // Track current upgrade in category
self.categoryUpgrades = []; // Store current category's upgrades
self.selectedUpgrade = null;
// Define upgrades with proper effects - no storage modification
self.upgrades = [{
id: 'health_increase',
category: 'Health',
title: 'Heart Container',
description: 'Increase maximum health by one heart',
basePrice: 300,
maxLevel: 2,
icon: 'health_increase_icon',
// Effect only for immediate visual feedback in shop
effect: function effect(level) {
console.log("Health upgrade purchased: level " + level);
// Could update preview if needed
}
}, {
id: 'arrow_capacity',
category: 'Ranged',
title: 'Quiver Expansion',
description: 'Start with more arrows',
basePrice: 100,
maxLevel: 5,
icon: 'arrow_capacity_icon',
effect: function effect(level) {
console.log("Arrow capacity upgrade purchased: level " + level);
}
}, {
id: 'levitate',
category: 'Spells',
title: 'Levitate',
description: 'Coming soon.',
basePrice: 99999,
maxLevel: 0,
icon: 'levitate_icon',
effect: function effect(level) {
console.log("Levitate upgrade purchased: level " + level);
}
}, {
id: 'slide_damage',
category: 'Combat',
title: 'Crushing Slide',
description: 'Sliding now damages enemies',
basePrice: 1000,
maxLevel: 1,
icon: 'icon_combat',
effect: function effect(level) {
console.log("Slide damage upgrade purchased: level " + level);
}
}, {
id: 'ground_slam',
category: 'Combat',
title: 'Ground Slam',
description: 'Smash downward by swiping down from the air to damage enemies below',
basePrice: 1000,
maxLevel: 1,
icon: 'icon_combat',
effect: function effect(level) {
console.log("Ground slam upgrade purchased: level " + level);
}
}, {
id: 'better_loot',
category: 'Loot',
title: 'Fortune Finder',
description: 'Increase chances of finding valuable loot',
basePrice: 400,
maxLevel: 4,
icon: 'icon_treasure',
effect: function effect(level) {}
}, {
id: 'potion_chance',
category: 'Health',
title: 'Potion Finder',
description: 'Increases the chance of health potions dropping from breakable objects',
basePrice: 300,
// Cheaper than health container (600)
maxLevel: 3,
icon: 'healthpotion',
effect: function effect(level) {}
}, {
id: 'arrow_finder',
category: 'Ranged',
title: 'Arrow Finder',
description: 'Increases the chance of arrows dropping from breakable objects',
basePrice: 300,
maxLevel: 3,
icon: 'arrow_capacity_icon',
effect: function effect(level) {}
}, {
id: 'loot_magnet',
category: 'Loot',
title: 'Loot Magnet',
description: 'Increases collection range for coins and items',
basePrice: 300,
maxLevel: 3,
icon: 'icon_treasure',
effect: function effect(level) {
console.log("Loot magnet upgrade purchased: level " + level);
}
}];
// Create tabs
self.createTabs();
// Create upgrade list
self.upgradeList = new Container();
self.addChild(self.upgradeList);
// Create buy button
self.createBuyButton();
self.initPlayerAnimation();
// Initial setup
self.showCategory('Health');
self.createArrows();
return self;
});
var ShopUpgrade = Container.expand(function (config) {
var self = Container.call(this);
// Keep all existing properties
self.id = config.id;
self.category = config.category;
self.title = config.title;
self.description = config.description;
self.basePrice = config.basePrice;
self.level = 0;
self.maxLevel = config.maxLevel || 1;
self.icon = config.icon;
self.effect = config.effect;
// Create a container for the window contents
self.windowContainer = new Container();
self.addChild(self.windowContainer);
// Add background to the container
self.background = self.windowContainer.attachAsset('shop_window', {
anchorX: 0.5,
anchorY: 0.5
});
self.iconSprite = self.windowContainer.attachAsset(self.id + '_icon', {
anchorX: 0.5,
anchorY: 0.5
});
self.iconSprite.x = -330;
self.iconSprite.y = -500;
self.titleText = new CustomText(self.title, {
size: 50,
fill: 0xE6D5AC,
letterSpacing: 5
});
self.windowContainer.addChild(self.titleText);
self.titleText.x = -200;
self.titleText.y = -500;
self.descriptionText = new CustomText(self.description + '.', {
size: 45,
fill: 0xA89F8A,
letterSpacing: 5,
wordWrapWidth: 800,
wordWrap: true
});
self.windowContainer.addChild(self.descriptionText);
self.descriptionText.x = -370;
self.descriptionText.y = -350;
self.levelText = new CustomText('LEVEL ' + self.level + (self.level >= self.maxLevel ? ' MAX' : ''), {
size: 60,
fill: 0xE6D5AC,
letterSpacing: 5
});
self.windowContainer.addChild(self.levelText);
self.levelText.x = -370;
self.levelText.y = 530;
// Methods with proper separation of concerns
self.getCurrentPrice = function () {
return self.basePrice * Math.pow(2, self.level);
};
self.canPurchase = function (gold) {
return self.level < self.maxLevel && gold >= self.getCurrentPrice();
};
self.purchase = function () {
if (self.level < self.maxLevel) {
this.level++;
// UPDATE THE REGISTRY
switch (this.id) {
case 'health_increase':
UPGRADE_REGISTRY.health_increase = this.level;
break;
case 'arrow_capacity':
UPGRADE_REGISTRY.arrow_capacity = this.level;
break;
case 'levitate':
UPGRADE_REGISTRY.levitate = this.level;
break;
case 'slide_damage':
UPGRADE_REGISTRY.slide_damage = this.level;
break;
case 'better_loot':
UPGRADE_REGISTRY.better_loot = this.level;
break;
case 'potion_chance':
UPGRADE_REGISTRY.potion_chance = this.level;
break;
case 'arrow_finder':
UPGRADE_REGISTRY.arrow_finder = this.level;
break;
case 'ground_slam':
UPGRADE_REGISTRY.ground_slam = this.level;
break;
case 'loot_magnet':
UPGRADE_REGISTRY.loot_magnet = this.level;
break;
}
// Update UI text
this.levelText.setText('LEVEL ' + this.level + (this.level >= this.maxLevel ? ' MAX' : ''));
// Apply the effect for immediate feedback
this.effect(this.level);
return true;
}
return false;
};
});
// Add ShopUpgrade.prototype methods here
var SkeletonSword = Container.expand(function () {
var self = Container.call(this);
// Attach the sword sprite
self.sprite = self.attachAsset('skeletonsword', {
anchorX: 0.5,
anchorY: 0.5
});
// Set properties with default values
self.speed = 6;
self.rotationSpeed = -0.1;
self.constantVelocity = 8; // Can be overridden when created
// Update method called every frame
self.update = function () {
// Use constantVelocity if set, otherwise fall back to speed * gameSpeedMultiplier
var velocity = self.constantVelocity || self.speed * gameSpeedMultiplier;
// Move the sword
self.x -= velocity;
// Rotate counterclockwise
self.sprite.rotation += self.rotationSpeed;
// Destroy if off screen
if (self.x < -100) {
self.destroy();
}
};
// Get bounds for collision detection
self.getBounds = function () {
return {
left: self.x - 40,
right: self.x + 40,
top: self.y - 40,
bottom: self.y + 40
};
};
return self;
});
var Spore = Container.expand(function () {
var self = Container.call(this);
// Animation properties
self.frame = 0;
self.animationCounter = 0;
self.animationSpeed = 0.035;
self.sprites = [];
// Physics properties
self.velocityX = 0;
self.velocityY = 0;
self.lifespan = 180; // Increased lifespan
self.initialSpeed = 4.0; // Slower initial speed
self.speed = self.initialSpeed;
self.damage = 1;
self.isExploding = false;
self.hasHitPlayer = false;
self.isActive = true; // New flag to track if this spore is still active for collision
// Initialize animation sprites
for (var i = 1; i <= 8; i++) {
var sprite = self.attachAsset('spore' + i, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
sprite.alpha = i === 1 ? 1 : 0;
self.sprites.push(sprite);
}
// Get collision bounds - only return bounds if the spore is active
self.getBounds = function () {
if (!self.isActive) {
// Return a dummy bounds that can't possibly collide with anything
return {
left: -9999,
right: -9999,
top: -9999,
bottom: -9999
};
}
return {
left: self.x - 40,
right: self.x + 40,
top: self.y - 40,
bottom: self.y + 40
};
};
// Override the destroy method to ensure proper cleanup
var originalDestroy = self.destroy;
self.destroy = function () {
self.isActive = false; // Mark as inactive first
// Remove from global spores array if it exists
if (window.spores) {
var index = window.spores.indexOf(self);
if (index !== -1) {
window.spores.splice(index, 1);
}
}
// Call the original destroy method
originalDestroy.call(self);
};
// Hide all sprites
self.hideAllSprites = function () {
for (var i = 0; i < self.sprites.length; i++) {
self.sprites[i].alpha = 0;
}
};
// Start explosion animation
self.explode = function () {
if (!self.isExploding) {
self.isExploding = true;
self.frame = 4; // Start at explosion frame
self.lifespan = Math.min(self.lifespan, 30); // Short explosion time
// Stop movement
self.velocityX *= 0.3;
self.velocityY *= 0.3;
// Deactivate the spore for collision once explosion starts
self.isActive = false;
}
};
// Update method
self.update = function () {
// Hide all sprites first
self.hideAllSprites();
// Move spore
self.x += self.velocityX;
self.y += self.velocityY;
// Apply platform movement to match world movement
self.x -= PLATFORM_SPEED * gameSpeedMultiplier;
// Update animation
self.animationCounter += self.animationSpeed;
if (self.animationCounter >= 1) {
self.animationCounter = 0;
self.frame = (self.frame + 1) % self.sprites.length;
}
self.sprites[self.frame].alpha = 1;
// Decrease lifespan
self.lifespan--;
// Start fading out near end of life
if (self.lifespan < 30) {
self.alpha = self.lifespan / 30;
}
// Destroy if off screen or lifespan ended
if (self.x < -100 || self.x > GAME_WIDTH + 100 || self.y < -100 || self.y > GAME_HEIGHT + 100 || self.lifespan <= 0) {
self.destroy();
}
};
return self;
});
/****
* Constants
****/
var Torch = Container.expand(function () {
var self = Container.call(this);
// Create base torch sprite
self.base = self.attachAsset('torch', {
anchorX: 0.5,
anchorY: 1
});
// Create flame sprite
self.flame = self.attachAsset('torchflame', {
anchorX: 0.5,
anchorY: 1,
y: -180
});
// Create aura sprite
self.aura = self.attachAsset('torchaura', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3,
y: -250
});
// Animation properties
self.flameTime = Math.random() * Math.PI * 2;
self.auraTime = Math.random() * Math.PI * 2;
self.flameSpeed = 0.05;
self.auraSpeed = 0.03;
// Update animation
self.update = function () {
// Animate flame scale
self.flameTime += self.flameSpeed;
var flameScale = 1 + Math.sin(self.flameTime) * 0.2;
self.flame.scaleY = flameScale;
// Random flip chance for flame
if (Math.random() < 0.02) {
self.flame.scaleX *= -1;
}
// Animate aura alpha
self.auraTime += self.auraSpeed;
var auraAlpha = 0.3 + Math.sin(self.auraTime) * 0.15;
self.aura.alpha = auraAlpha;
};
return self;
});
var TreasureChest = Container.expand(function () {
var self = Container.call(this);
// Attach chest sprite
self.sprite = self.attachAsset('treasurechest', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xC0C0C0
});
// Initialize as breakable
BreakableBehavior.initBreakable(self);
// Break functionality
self["break"] = function () {
BreakableBehavior.standardBreak(self, TreasureChestPiece, 4, function (chest) {
// Get loot multiplier from player
var lootMultiplier = player.lootMultiplier || 1.0;
// Spawn health potion with medium chance - improved by loot multiplier
if (Math.random() < 0.25 * (player.potionDropChance || 1.0)) {
var potion = new HealthPotion();
potion.x = chest.x;
potion.y = chest.y;
potion.velocityX = GameUtils.randomRange(1, 6); // Reduced horizontal velocity
potion.velocityY = -GameUtils.randomRange(8, 16); // Reduced vertical velocity
game.addChild(potion);
coins.push(potion);
}
// Higher chance for arrows - improved by loot multiplier
if (Math.random() < 0.40 * (player.arrowDropChance || 1.0)) {
var arrowCount = Math.floor(GameUtils.randomRange(1, 4) * lootMultiplier);
for (var i = 0; i < arrowCount; i++) {
var arrow = new ArrowPickup();
arrow.x = chest.x;
arrow.y = chest.y;
arrow.velocityX = GameUtils.randomRange(1, 6);
arrow.velocityY = -GameUtils.randomRange(8, 16);
// Set rotation to 90 degrees counterclockwise
arrow.sprite.rotation = -Math.PI / 2;
arrow.platformOffset = 40;
game.addChild(arrow);
coins.push(arrow);
}
}
// Spawn valuable items - quantity affected by loot multiplier
var totalItems = Math.floor(GameUtils.randomRange(3, 9) * lootMultiplier);
for (var i = 0; i < totalItems; i++) {
// Random chance for different gems - probabilities improved by loot multiplier
var rand = Math.random();
var item;
// Increase chances for better gems based on multiplier (cap at reasonable values)
var diamondChance = Math.min(0.05 * lootMultiplier, 0.15);
var emeraldChance = Math.min(0.15 * lootMultiplier, 0.25);
var rubyChance = Math.min(0.30 * lootMultiplier, 0.40);
if (rand < diamondChance) {
item = new Coin('diamond');
} else if (rand < emeraldChance) {
item = new Coin('emerald');
} else if (rand < rubyChance) {
item = new Coin('ruby');
} else {
item = new Coin('coin');
}
item.x = chest.x;
item.y = chest.y;
item.velocityX = GameUtils.randomRange(1, 6); // Reduced horizontal velocity
item.velocityY = -GameUtils.randomRange(8, 16); // Reduced vertical velocity
game.addChild(item);
coins.push(item);
}
LK.getSound('woodbreak').play();
});
};
return self;
});
var TreasureChestPiece = Container.expand(function (pieceNum) {
var self = Container.call(this);
// Attach piece sprite
self.sprite = self.attachAsset('treasurechestpiece' + pieceNum, {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xC0C0C0
});
// Initialize as piece
PieceBehavior.initPiece(self);
// Update method
self.update = function () {
PieceBehavior.standardUpdate(self);
};
self.checkPlatformCollision = function () {
return GameUtils.checkPlatformCollision(self, 80, true) != null;
};
return self;
});
var TutorialManager = Container.expand(function () {
var self = Container.call(this);
// Tutorial states
self.states = {
ATTACK: 'attack',
BOW: 'bow',
JUMP: 'jump',
DROP_DOWN: 'drop_down',
DOUBLE_JUMP: 'double_jump',
TRANSITIONING_TO_SLIDE: 'transitioning_to_slide',
// New state
SLIDE: 'slide',
AIR_DASH: 'air_dash',
COMPLETE: 'complete'
};
// Current state and tracking
self.currentState = self.states.ATTACK;
self.enemyKilledByAttack = false;
self.enemyKilledByBow = false;
self.dropDownInput = false;
self.hasJumped = false;
self.hasDoubleJumped = false;
self.hasAirDashSucceeded = false;
self.firstJumpPerformed = false;
self.secondJumpPerformed = false;
// Message display
self.messageContainer = new Container();
self.messageText = new CustomText('', {
size: 60,
fill: 0xFFFFFF,
letterSpacing: 5,
wordWrap: true,
wordWrapWidth: 1200,
anchorX: 0.5
});
self.messageContainer.addChild(self.messageText);
self.messageContainer.width = 1200;
self.messageContainer.x = GAME_WIDTH / 2;
self.messageContainer.y = GAME_HEIGHT / 3.2;
self.messageText.x = 0; // Center the text in the container
self.messageText.y = 0; // Center the text in the container
// Initialize tutorial
self.init = function () {
self.slideSword = null; // Track the sword for slide phase
self.platformRemovalTimer = 0;
self.removingPlatforms = false;
self.hasSlideSucceeded = false;
// Create invulnerable player
player = game.addChild(new Player());
player.x = GAME_WIDTH / 4.5;
player.y = GAME_HEIGHT / 1.5 - 100;
player.isInvulnerable = true;
// Reset state tracking
self.enemyKilledByAttack = false;
self.enemyKilledByBow = false;
self.dropDownInput = false;
// Set up initial platforms
self.setupInitialPlatforms();
// Add message container
game.addChild(self.messageContainer);
self.setMessage("Tap anywhere to attack \n Kill the goblin!");
// Add backbutton centered under the main platform
self.backButton = self.messageContainer.attachAsset('backbutton', {
anchorX: 0.5,
anchorY: 0.5
});
self.backButton.x = -700;
self.backButton.y = -500; // 400px below message, visually under platform
// Add touch handler for backbutton
self.backButtonDown = function (x, y, obj) {
// Only allow backbutton if tutorial is active
if (tutorialActive && typeof self.complete === "function") {
// Add visual feedback animation
tween(self.backButton, {
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.backButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.complete();
}
});
}
});
// Play menu select sound
LK.getSound('menuselect').play();
}
};
self.backButton.down = self.backButtonDown;
// Clear any existing enemies/arrows
enemies = [];
arrows = [];
self.spawnTutorialEnemy();
};
// Message handling
self.setMessage = function (text) {
// First, remove the existing text
if (self.messageText.parent) {
self.messageContainer.removeChild(self.messageText);
}
// Create a new text object each time (like the shop does)
self.messageText = new CustomText(text, {
size: 60,
fill: 0xFFFFFF,
letterSpacing: 5,
wordWrap: true,
wordWrapWidth: 1200,
anchorX: 0.5
});
// Add to container with fixed positioning
self.messageContainer.addChild(self.messageText);
self.messageText.x = 0;
self.messageText.y = 0;
};
// Platform setup and management
self.setupInitialPlatforms = function () {
for (var i = 0; i < 5; i++) {
var platform = new Platform();
platform.x = i * (PLATFORM_WIDTH - PLATFORM_OVERLAP);
platform.y = lowPlatformHeight;
platforms.push(platform);
game.addChild(platform);
}
};
self.setupJumpPlatforms = function () {
// Clear any high platforms
for (var i = platforms.length - 1; i >= 0; i--) {
if (platforms[i].y < lowPlatformHeight) {
platforms[i].destroy();
platforms.splice(i, 1);
}
}
// Create initial high platform
var platform = new Platform();
platform.x = GAME_WIDTH;
platform.y = highPlatformHeight;
platforms.push(platform);
game.addChild(platform);
};
self.setupDoubleJumpPlatforms = function () {
// Clear any existing high platforms
for (var i = platforms.length - 1; i >= 0; i--) {
if (platforms[i].y < lowPlatformHeight) {
platforms[i].destroy();
platforms.splice(i, 1);
}
}
// Create initial highest platform
var platform = new Platform();
platform.x = GAME_WIDTH;
platform.y = HIGHEST_PLATFORM_HEIGHT;
platforms.push(platform);
game.addChild(platform);
};
self.updateTutorialPlatforms = function () {
// Always maintain ground level platforms
var lastGroundPlatform = null;
for (var i = platforms.length - 1; i >= 0; i--) {
if (platforms[i].y === lowPlatformHeight) {
lastGroundPlatform = platforms[i];
break;
}
}
if (lastGroundPlatform && lastGroundPlatform.x < GAME_WIDTH + 500) {
var platform = new Platform();
platform.x = lastGroundPlatform.x + (PLATFORM_WIDTH - PLATFORM_OVERLAP);
platform.y = lowPlatformHeight;
platforms.push(platform);
game.addChild(platform);
}
// During JUMP and DROP_DOWN phases, maintain mid height platforms
if (self.currentState === self.states.JUMP || self.currentState === self.states.DROP_DOWN) {
var lastMidPlatform = null;
for (var i = platforms.length - 1; i >= 0; i--) {
if (platforms[i].y === highPlatformHeight) {
lastMidPlatform = platforms[i];
break;
}
}
if (lastMidPlatform && lastMidPlatform.x < GAME_WIDTH + 500) {
var platform = new Platform();
platform.x = lastMidPlatform.x + (PLATFORM_WIDTH - PLATFORM_OVERLAP);
platform.y = highPlatformHeight;
platforms.push(platform);
game.addChild(platform);
}
}
// During DOUBLE_JUMP phase, maintain highest platforms
else if (self.currentState === self.states.DOUBLE_JUMP) {
var lastHighPlatform = null;
for (var i = platforms.length - 1; i >= 0; i--) {
if (platforms[i].y === HIGHEST_PLATFORM_HEIGHT) {
lastHighPlatform = platforms[i];
break;
}
}
if (!lastHighPlatform || lastHighPlatform.x < GAME_WIDTH + 500) {
var platform = new Platform();
platform.x = lastHighPlatform ? lastHighPlatform.x + (PLATFORM_WIDTH - PLATFORM_OVERLAP) : GAME_WIDTH + 100;
platform.y = HIGHEST_PLATFORM_HEIGHT;
platforms.push(platform);
game.addChild(platform);
}
}
// Update existing platforms
for (var i = platforms.length - 1; i >= 0; i--) {
platforms[i].update();
if (platforms[i].destroyed) {
platforms.splice(i, 1);
}
}
};
// Enemy spawning and handling
self.spawnTutorialEnemy = function () {
if (self.currentState === self.states.ATTACK || self.currentState === self.states.BOW) {
// Clear existing enemies
for (var i = enemies.length - 1; i >= 0; i--) {
enemies[i].destroy();
}
enemies = [];
var enemy = new Enemy('goblin');
enemy.x = GAME_WIDTH + 200; // Start offscreen to the right
enemy.y = lowPlatformHeight - ENEMY_PLATFORM_OFFSET;
enemy.isOnGround = true;
enemy.currentPlatform = platforms[0];
enemies.push(enemy);
game.addChild(enemy);
}
};
// State progression check
self.checkStateProgress = function () {
switch (self.currentState) {
case self.states.ATTACK:
// Reset if enemy killed by bow
if (self.enemyKilledByBow) {
self.enemyKilledByBow = false;
self.spawnTutorialEnemy();
return;
}
// Progress if enemy killed by attack and death animation is nearly complete
if (self.enemyKilledByAttack && (enemies.length === 0 || enemies[0] && enemies[0].isDeathAnimationComplete())) {
self.enemyKilledByAttack = false;
self.currentState = self.states.BOW;
self.setMessage("Swipe left to fire your bow");
self.spawnTutorialEnemy();
}
break;
case self.states.BOW:
// Reset if enemy killed by attack
if (self.enemyKilledByAttack) {
self.enemyKilledByAttack = false;
self.spawnTutorialEnemy();
return;
}
// Progress if enemy killed by bow and death animation is nearly complete
if (self.enemyKilledByBow && (enemies.length === 0 || enemies[0] && enemies[0].isDeathAnimationComplete())) {
self.enemyKilledByBow = false;
self.currentState = self.states.JUMP;
self.setMessage("Swipe up to jump");
self.setupJumpPlatforms();
}
break;
case self.states.JUMP:
if (self.hasJumped && player.isOnGround && player.currentPlatform && player.currentPlatform.y === highPlatformHeight) {
self.currentState = self.states.DROP_DOWN;
self.setMessage("Swipe down to drop back down");
self.hasJumped = false; // Reset for next use
}
break;
case self.states.DROP_DOWN:
if (self.dropDownInput && player.isOnGround && player.currentPlatform && player.currentPlatform.y === lowPlatformHeight) {
self.currentState = self.states.DOUBLE_JUMP;
self.setMessage("Swipe up twice for double jump");
self.setupDoubleJumpPlatforms();
self.dropDownInput = false;
}
break;
case self.states.DOUBLE_JUMP:
if (self.firstJumpPerformed && self.secondJumpPerformed && player.isOnGround && player.currentPlatform && player.currentPlatform.y === HIGHEST_PLATFORM_HEIGHT && !self.removingPlatforms) {
self.currentState = self.states.TRANSITIONING_TO_SLIDE;
self.platformRemovalTimer = 30;
self.removingPlatforms = true;
}
break;
case self.states.TRANSITIONING_TO_SLIDE:
if (self.platformRemovalTimer > 0) {
self.platformRemovalTimer--;
if (self.platformRemovalTimer === 0) {
// Remove all high platforms after delay
for (var i = platforms.length - 1; i >= 0; i--) {
if (platforms[i].y < lowPlatformHeight) {
platforms[i].destroy();
platforms.splice(i, 1);
}
}
}
}
// Check for transition to slide state after platforms are removed
if (self.platformRemovalTimer === 0 && player.isOnGround && player.currentPlatform && player.currentPlatform.y === lowPlatformHeight) {
self.currentState = self.states.SLIDE;
self.setMessage("Swipe right to slide\nDodge the sword");
self.removingPlatforms = false;
// Reset jump tracking
self.firstJumpPerformed = false;
self.secondJumpPerformed = false;
}
break;
case self.states.SLIDE:
// Only spawn sword if in slide state and player is on ground
if (!self.slideSword && player.isOnGround && player.currentPlatform && player.currentPlatform.y === lowPlatformHeight) {
self.slideSword = new SkeletonSword();
self.slideSword.x = GAME_WIDTH + 100;
self.slideSword.y = player.y - 120;
game.addChild(self.slideSword);
}
// Track successful slide with a new flag
if (!self.hasSlideSucceeded && self.slideSword && self.slideSword.x < player.x && player.isSliding) {
self.hasSlideSucceeded = true;
}
if (self.hasSlideSucceeded && !player.isSliding) {
self.currentState = self.states.AIR_DASH;
self.setMessage("Swipe right while in the air to air dash!");
self.hasSlideSucceeded = false;
self.hasAirDashSucceeded = false;
// Reset player state for air dashing
player.canAirDash = true;
}
break;
case self.states.AIR_DASH:
// Simply check if the player has air dashed
if (!self.hasAirDashSucceeded && player.isAirDashing) {
self.hasAirDashSucceeded = true;
// As soon as they air dash, consider the tutorial complete
LK.setTimeout(function () {
self.currentState = self.states.COMPLETE;
self.setMessage("That's it!\nNow go get some treasure!");
LK.setTimeout(function () {
self.complete();
}, 3000);
}, 1000); // Wait 1 second after air dash before showing completion message
}
break;
}
};
// Input handling
// Inside TutorialManager's checkInput method, where it checks different states
self.checkInput = function (startX, startY, endX, endY) {
if (!gameStarted || player.isDying) {
return;
}
// Initialize the jump tracking property if it doesn't exist
if (!self.inputProcessed) {
self.inputProcessed = false;
}
var deltaY = endY - startY;
var deltaX = endX - startX;
// Ignore very small movements
if (Math.abs(deltaY) < VERTICAL_DEADZONE && Math.abs(deltaX) < VERTICAL_DEADZONE) {
if (self.currentState === self.states.ATTACK) {
player.attack();
}
self.inputProcessed = true;
return;
}
// Don't process the same input multiple times
if (self.inputProcessed) {
return;
}
self.inputProcessed = true;
// Track last jump time to prevent multiple jumps from a single swipe
if (!self.lastJumpTime) {
self.lastJumpTime = 0;
}
var currentTime = Date.now();
// Check directional swipes
if (Math.abs(deltaX) > Math.abs(deltaY)) {
if (deltaX < -70 && self.currentState === self.states.BOW) {
player.shoot();
} else if (deltaX > 70 && self.currentState === self.states.SLIDE) {
player.slide();
}
// ADD THIS AIR DASH HANDLING RIGHT HERE
else if (deltaX > 70 && self.currentState === self.states.AIR_DASH && !player.isOnGround) {
player.airDash();
}
} else {
if (deltaY < -120) {
// Upward swipe with cooldown check
if (currentTime - self.lastJumpTime > JUMP_COOLDOWN) {
if (self.currentState === self.states.JUMP) {
self.hasJumped = true;
player.jump();
self.lastJumpTime = currentTime;
} else if (self.currentState === self.states.DOUBLE_JUMP) {
self.hasDoubleJumped = true;
player.jump();
self.lastJumpTime = currentTime;
} else if (self.currentState === self.states.AIR_DASH) {
// Allow jumping during air dash tutorial too
player.jump();
self.lastJumpTime = currentTime;
}
}
} else if (deltaY > 120 && self.currentState === self.states.DROP_DOWN) {
self.dropDownInput = true;
player.fallThrough();
}
}
};
// Main update loop
self.update = function () {
// Update platforms
self.updateTutorialPlatforms();
// Update arrows and check collisions
for (var i = arrows.length - 1; i >= 0; i--) {
arrows[i].update();
if (arrows[i].destroyed) {
arrows.splice(i, 1);
continue;
}
// Check arrow collisions with enemies
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (GameUtils.checkCollision(arrows[i].getBounds(), enemy.getBounds())) {
if (!enemy.isHit && !enemy.isDying) {
enemy.hit();
LK.getSound('enemyhit').play();
arrows[i].destroy();
arrows.splice(i, 1);
if (self.currentState === self.states.BOW) {
self.enemyKilledByBow = true;
}
break;
}
}
}
}
// --- SLIDE SKELETON SWORD LOGIC BEGIN ---
if (self.currentState === self.states.SLIDE) {
// Only spawn one sword at a time
if (!self.slideSword && player.isOnGround && !player.isJumping) {
self.slideSword = new SkeletonSword();
self.slideSword.x = GAME_WIDTH + 100;
self.slideSword.y = player.y - 120;
game.addChild(self.slideSword);
} else if (self.slideSword) {
// Update sword
self.slideSword.update();
// Check collision with player (must slide under to avoid)
var swordBounds = self.slideSword.getBounds();
var playerBounds = player.getCollisionBounds();
// Only count as a hit if player is NOT sliding
if (GameUtils.checkCollision(swordBounds, playerBounds) && !player.isSliding && !player.isDying && !player.isInvulnerable) {
// Flash screen red when hit
LK.effects.flashScreen(0xff0000, 300);
// Remove sword so it can respawn
self.slideSword.destroy();
self.slideSword = null;
}
// If sword leaves screen, reset it
if (self.slideSword && (self.slideSword.x < -100 || self.slideSword.destroyed)) {
if (self.slideSword.parent) {
self.slideSword.destroy();
}
self.slideSword = null;
}
}
} else {
// Clean up sword if not in slide state
if (self.slideSword && self.slideSword.parent) {
self.slideSword.destroy();
}
self.slideSword = null;
}
// --- SLIDE SKELETON SWORD LOGIC END ---
// Update enemies and check collisions
for (var i = enemies.length - 1; i >= 0; i--) {
enemies[i].update();
// Check for attack collisions
var playerBounds = player.getCollisionBounds();
var enemyBounds = enemies[i].getBounds();
var attackBounds = player.getAttackBounds();
if (attackBounds && !enemies[i].isHit && !enemies[i].isDying) {
if (enemies[i].x > player.x && GameUtils.checkCollision(attackBounds, enemyBounds)) {
enemies[i].hit();
LK.getSound('enemyhit').play();
if (self.currentState === self.states.ATTACK) {
self.enemyKilledByAttack = true;
} else if (self.currentState === self.states.BOW) {
self.enemyKilledByAttack = true;
}
continue;
}
}
// Remove destroyed enemies
if (enemies[i].destroyed || enemies[i].x < -50) {
enemies.splice(i, 1);
// Only respawn if the enemy wasn't killed correctly
if (!self.enemyKilledByAttack && !self.enemyKilledByBow) {
self.spawnTutorialEnemy();
}
}
}
// Update particle effects
if (particleSystem) {
particleSystem.update();
}
// Check state progression
self.checkStateProgress();
};
// Cleanup and completion
self.complete = function () {
if (self.slideSword && self.slideSword.parent) {
self.slideSword.destroy();
self.slideSword = null;
}
if (self.backButton) {
self.backButton.destroy();
self.backButton = null;
}
self.messageContainer.destroy();
for (var i = platforms.length - 1; i >= 0; i--) {
platforms[i].destroy();
}
platforms = [];
for (var i = enemies.length - 1; i >= 0; i--) {
enemies[i].destroy();
}
enemies = [];
for (var i = arrows.length - 1; i >= 0; i--) {
arrows[i].destroy();
}
arrows = [];
if (player) {
player.destroy();
player = null;
}
createTitleScreen();
gameStarted = false;
tutorialActive = false;
};
return self;
});
/****
* Initialize Game
****/
/****
* Game Variables
****/
// Containers
/****
* Game Initialization
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
//resetAllStorage();
// Add ShopUpgrade.prototype methods here
// Base collectible behavior for items that can be collected
/****
* Game Management
****/
initializeStorage();
ShopUpgrade.prototype.setSelected = function (isSelected) {
var self = this;
// Find the background asset within windowContainer children
var background = null;
for (var i = 0; i < self.windowContainer.children.length; i++) {
if (self.windowContainer.children[i].asset && (self.windowContainer.children[i].asset.id === 'shop_upgrade_bg' || self.windowContainer.children[i].asset.id === 'shop_upgrade_bg_selected')) {
background = self.windowContainer.children[i];
break;
}
}
if (background) {
background.asset = isSelected ? 'shop_upgrade_bg_selected' : 'shop_upgrade_bg';
}
};
var SaveManager = {
// Save all game data
save: function save() {
// Update gold in registry from score manager
UPGRADE_REGISTRY.gold = Number(scoreManager.getScore()) || 0;
// Save gold from registry to storage
storage.gold = UPGRADE_REGISTRY.gold;
// Save all upgrade levels directly from registry
storage.healthUpgrade = UPGRADE_REGISTRY.health_increase;
storage.arrowUpgrade = UPGRADE_REGISTRY.arrow_capacity;
storage.levitateUpgrade = UPGRADE_REGISTRY.levitate;
storage.slideUpgrade = UPGRADE_REGISTRY.slide_damage;
storage.lootUpgrade = UPGRADE_REGISTRY.better_loot;
storage.potionChanceUpgrade = UPGRADE_REGISTRY.potion_chance;
storage.arrowFinderUpgrade = UPGRADE_REGISTRY.arrow_finder;
storage.groundSlamUpgrade = UPGRADE_REGISTRY.ground_slam;
storage.lootMagnetUpgrade = UPGRADE_REGISTRY.loot_magnet;
// Track timestamp
storage.lastSaveTime = Date.now();
},
// Load saved game data
load: function load() {
// Initialize registry from storage with safe number conversions
UPGRADE_REGISTRY.health_increase = parseInt(storage.healthUpgrade) || 0;
UPGRADE_REGISTRY.arrow_capacity = parseInt(storage.arrowUpgrade) || 0;
UPGRADE_REGISTRY.levitate = parseInt(storage.levitateUpgrade) || 0;
UPGRADE_REGISTRY.slide_damage = parseInt(storage.slideUpgrade) || 0;
UPGRADE_REGISTRY.better_loot = parseInt(storage.lootUpgrade) || 0;
UPGRADE_REGISTRY.potion_chance = parseInt(storage.potionChanceUpgrade) || 0;
UPGRADE_REGISTRY.arrow_finder = parseInt(storage.arrowFinderUpgrade) || 0;
UPGRADE_REGISTRY.ground_slam = parseInt(storage.groundSlamUpgrade) || 0;
UPGRADE_REGISTRY.loot_magnet = parseInt(storage.lootMagnetUpgrade) || 0;
// Load gold with explicit number conversion
UPGRADE_REGISTRY.gold = parseInt(storage.gold) || 0;
// Update score display
if (scoreManager) {
scoreManager.setScore(UPGRADE_REGISTRY.gold);
}
// If shop exists, update the visual state of upgrades
this.updateShopDisplay();
// Apply the loaded upgrade effects to the player
this.applyUpgradeEffects();
},
// Update shop display based on registry values
updateShopDisplay: function updateShopDisplay() {
if (game.shopManager && game.shopManager.upgrades) {
game.shopManager.upgrades.forEach(function (upgrade) {
// Get the appropriate level from registry
var registryLevel = 0;
switch (upgrade.id) {
case 'health_increase':
registryLevel = UPGRADE_REGISTRY.health_increase;
break;
case 'arrow_capacity':
registryLevel = UPGRADE_REGISTRY.arrow_capacity;
break;
case 'levitate':
registryLevel = UPGRADE_REGISTRY.levitate;
break;
case 'slide_damage':
registryLevel = UPGRADE_REGISTRY.slide_damage;
break;
case 'better_loot':
registryLevel = UPGRADE_REGISTRY.better_loot;
break;
case 'potion_chance':
registryLevel = UPGRADE_REGISTRY.potion_chance;
break;
case 'arrow_finder':
registryLevel = UPGRADE_REGISTRY.arrow_finder;
break;
case 'ground_slam':
registryLevel = UPGRADE_REGISTRY.ground_slam;
break;
case 'loot_magnet':
registryLevel = UPGRADE_REGISTRY.loot_magnet;
break;
}
// Update internal level directly from registry
upgrade.level = registryLevel;
// Update UI text
if (upgrade.levelText) {
upgrade.levelText.setText('LEVEL ' + upgrade.level + (upgrade.level >= upgrade.maxLevel ? ' MAX' : ''));
}
console.log("Updated " + upgrade.id + " display to level " + upgrade.level);
});
}
},
// Apply upgrade effects to player
applyUpgradeEffects: function applyUpgradeEffects(player) {
if (!player && window.player) {
player = window.player;
}
if (!player) {
return;
}
// Reset to base values
player.maxHealth = 3;
player.ammoCount = 5;
player.hasLevitate = false;
player.slideDamage = false;
player.hasGroundSlam = false;
player.arrowDropChance = 1.0;
player.lootMultiplier = 1.0;
player.potionDropChance = 1.0;
player.lootMagnetRange = 1.0;
// Apply all upgrades from registry
player.maxHealth += UPGRADE_REGISTRY.health_increase;
player.currentHealth = player.maxHealth;
player.ammoCount += UPGRADE_REGISTRY.arrow_capacity * 2;
player.hasLevitate = UPGRADE_REGISTRY.levitate > 0;
player.hasGroundSlam = UPGRADE_REGISTRY.ground_slam > 0;
player.slideDamage = UPGRADE_REGISTRY.slide_damage > 0;
player.lootMultiplier = 1 + UPGRADE_REGISTRY.better_loot * 0.15;
player.potionDropChance = 1 + UPGRADE_REGISTRY.potion_chance * 0.5;
player.arrowDropChance = 1 + UPGRADE_REGISTRY.arrow_finder * 0.5;
player.lootMagnetRange = 1 + UPGRADE_REGISTRY.loot_magnet * 0.5;
// Update UI elements
if (player.heartContainer) {
player.heartContainer.updateMaxHealth(player.maxHealth);
player.heartContainer.updateHealth(player.currentHealth);
}
if (scoreManager) {
scoreManager.updateAmmo(player.ammoCount);
}
},
// Clear all saved data
clear: function clear() {
// Reset storage
storage.gold = 0;
storage.healthUpgrade = 0;
storage.arrowUpgrade = 0;
storage.levitateUpgrade = 0;
storage.slideUpgrade = 0;
storage.lootUpgrade = 0;
// Reset registry
UPGRADE_REGISTRY.gold = 0;
UPGRADE_REGISTRY.health_increase = 0;
UPGRADE_REGISTRY.arrow_capacity = 0;
UPGRADE_REGISTRY.levitate = 0;
UPGRADE_REGISTRY.slide_damage = 0;
UPGRADE_REGISTRY.better_loot = 0;
UPGRADE_REGISTRY.potion_chance = 0;
UPGRADE_REGISTRY.arrow_finder = 0;
}
};
ShopManager.prototype.initPlayerAnimation = function () {
var self = this;
self.playerSprites = [];
self.idleFrame = 0;
self.animationCounter = 0;
self.animationSpeed = 0.037;
// Create sprites for idle animation
for (var i = 1; i <= 4; i++) {
// Create and attach first
var sprite = self.attachAsset('playeridle' + i, {
anchorX: 0.5,
anchorY: 0.5,
x: -550,
y: 400
});
// Set initial visibility
sprite.alpha = i === 1 ? 1 : 0;
// Push to array after attaching
self.playerSprites.push(sprite);
}
};
ShopManager.prototype.hideAllSprites = function () {
var self = this;
for (var i = 0; i < self.playerSprites.length; i++) {
self.playerSprites[i].alpha = 0;
}
};
ShopManager.prototype.updatePlayerAnimation = function () {
var self = this;
// Hide all sprites first
self.hideAllSprites();
// Update animation
self.animationCounter += self.animationSpeed;
if (self.animationCounter >= 1) {
self.animationCounter = 0;
self.idleFrame = (self.idleFrame + 1) % self.playerSprites.length;
}
// Show current frame
self.playerSprites[self.idleFrame].alpha = 1;
};
ShopManager.prototype.updateArrowVisibility = function () {
var self = this;
// Check if arrows exist before updating their visibility
if (self.leftArrow) {
// Only show left arrow if we're not at the first upgrade
self.leftArrow.alpha = self.currentUpgradeIndex > 0 ? 1 : 0;
}
if (self.rightArrow) {
// Only show right arrow if we're not at the last upgrade
self.rightArrow.alpha = self.currentUpgradeIndex < self.categoryUpgrades.length - 1 ? 1 : 0;
}
};
ShopManager.prototype.createTabs = function () {
var self = this;
// Add shop background first
self.shopBackground = self.attachAsset('shop_background', {
anchorX: 0.5,
anchorY: 0.5
});
// Add shop banner
self.shopBanner = self.attachAsset('shop_banner', {
anchorX: 0.5,
anchorY: 0.5,
y: -1000
});
// Create tabs container AFTER background
self.tabs = new Container();
self.tabs.x = 0;
self.tabs.y = -880;
self.addChild(self.tabs); // This adds it on top of previously added elements
// Add windows
self.playerWindow = self.attachAsset('shop_player', {
anchorX: 0.5,
anchorY: 0.5,
x: -490,
y: 60
});
// Gold display
self.goldWindow = new Container(); // Create container instead of asset
// Add dark window background first
var goldWindowBg = self.goldWindow.attachAsset('shop_dark_window', {
anchorX: 0.5,
anchorY: 0.5
});
self.goldWindow.x = -490;
self.goldWindow.y = 850;
self.addChild(self.goldWindow);
self.createGoldDisplay();
// Create category tabs
var spacing = 335;
var startX = -(self.categories.length - 1) * spacing / 2;
self.categories.forEach(function (category, index) {
var tab = new Container();
// Create both tab backgrounds
var normalBg = tab.attachAsset('shop_tab', {
anchorX: 0.5,
anchorY: 0.5
});
var selectedBg = tab.attachAsset('shop_tab_selected', {
anchorX: 0.5,
anchorY: 0.5
});
selectedBg.alpha = 0; // Initially hide the selected state
var text = new CustomText(category, {
size: 48,
fill: 0xE6D5AC,
letterSpacing: 5
});
tab.addChild(text);
// Position text at center of tab
text.x = -text.width / 2 + 10;
text.y = 0;
tab.x = startX + spacing * index;
tab.y = 180;
tab.down = function () {
// Play sound when tab is clicked
LK.getSound('menuselect').play();
self.showCategory(category);
};
self.tabs.addChild(tab);
});
};
ShopManager.prototype.createBuyButton = function () {
var self = this;
self.buyButton = new Container();
self.buyButtonBg = self.buyButton.attachAsset('shop_button', {
anchorX: 0.5,
anchorY: 0.5
});
self.priceContainer = new Container();
self.priceText = new CustomText('', {
size: 60,
fill: 0xFFFFFF,
letterSpacing: 5
});
self.priceCoinIcon = LK.getAsset('coin', {
anchorX: 0,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
// Position coin at fixed location
self.priceCoinIcon.x = 160;
self.priceCoinIcon.y = 40;
// Initially position text with some default value
self.priceText.x = self.priceCoinIcon.x - 10;
self.priceText.y = 40;
self.priceContainer.addChild(self.priceText);
self.priceContainer.addChild(self.priceCoinIcon);
self.priceContainer.y = -300;
self.buyButton.addChild(self.priceContainer);
self.buyButton.x = 525;
self.buyButton.y = 850;
self.buyButton.down = function () {
self.purchaseSelectedUpgrade();
};
self.addChild(self.buyButton);
};
ShopManager.prototype.showCategory = function (category) {
var self = this;
self.currentCategory = category;
// Filter upgrades for this category
self.categoryUpgrades = self.upgrades.filter(function (upgrade) {
return upgrade.category === category;
});
// Reset to first upgrade in category
self.currentUpgradeIndex = 0;
// Show first upgrade
self.showUpgradeAtIndex(0);
// Update tab visuals
self.tabs.children.forEach(function (tab) {
var isSelected = tab.children[2].text === category;
tab.children[0].alpha = isSelected ? 0 : 1; // normal bg
tab.children[1].alpha = isSelected ? 1 : 0; // selected bg
});
};
ShopManager.prototype.showUpgradeAtIndex = function (index) {
var self = this;
// Update current index
self.currentUpgradeIndex = index;
// Clear existing upgrades
self.upgradeList.removeChildren();
// Show upgrade at current index
if (self.categoryUpgrades.length > 0) {
var upgradeConfig = self.categoryUpgrades[self.currentUpgradeIndex];
var upgrade = new ShopUpgrade(upgradeConfig);
// Initialize level from registry based on ID
switch (upgrade.id) {
case 'health_increase':
upgrade.level = UPGRADE_REGISTRY.health_increase;
break;
case 'arrow_capacity':
upgrade.level = UPGRADE_REGISTRY.arrow_capacity;
break;
case 'levitate':
upgrade.level = UPGRADE_REGISTRY.levitate;
break;
case 'slide_damage':
upgrade.level = UPGRADE_REGISTRY.slide_damage;
break;
case 'better_loot':
upgrade.level = UPGRADE_REGISTRY.better_loot;
break;
case 'potion_chance':
upgrade.level = UPGRADE_REGISTRY.potion_chance || 0;
break;
case 'arrow_finder':
upgrade.level = UPGRADE_REGISTRY.arrow_finder || 0;
break;
case 'ground_slam':
upgrade.level = UPGRADE_REGISTRY.ground_slam;
break;
case 'loot_magnet':
upgrade.level = UPGRADE_REGISTRY.loot_magnet;
break;
}
// Update level text to match registry
upgrade.levelText.setText('LEVEL ' + upgrade.level + (upgrade.level >= upgrade.maxLevel ? ' MAX' : ''));
upgrade.x = 355;
upgrade.y = 60;
self.upgradeList.addChild(upgrade);
// Make sure we're selecting the upgrade
self.selectedUpgrade = upgrade;
}
// Update arrow visibility
self.updateArrowVisibility();
// Update buy button
self.updateBuyButton();
};
ShopManager.prototype.createArrows = function () {
var self = this;
// Create left arrow
self.leftArrow = new Container();
self.leftArrow.attachAsset('shop_arrow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: -1,
// Flip horizontally
alpha: 1
});
self.leftArrow.x = 0;
self.leftArrow.y = 150; // Center with the upgrade display
self.addChild(self.leftArrow);
// Create right arrow
self.rightArrow = new Container();
self.rightArrow.attachAsset('shop_arrow', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 1
});
self.rightArrow.x = 710;
self.rightArrow.y = 150; // Center with the upgrade display
self.addChild(self.rightArrow);
// Set up interactions
self.leftArrow.down = function () {
if (self.currentUpgradeIndex > 0) {
LK.getSound('menuselect').play();
self.showUpgradeAtIndex(self.currentUpgradeIndex - 1);
}
};
self.rightArrow.down = function () {
if (self.currentUpgradeIndex < self.categoryUpgrades.length - 1) {
LK.getSound('menuselect').play();
self.showUpgradeAtIndex(self.currentUpgradeIndex + 1);
}
};
// Set initial visibility
if (self.leftArrow && self.rightArrow) {
self.updateArrowVisibility();
}
};
ShopManager.prototype.selectUpgrade = function (upgrade) {
var self = this;
// Deselect previous upgrade
if (self.selectedUpgrade) {
self.selectedUpgrade.setSelected(false);
}
self.selectedUpgrade = upgrade;
upgrade.setSelected(true);
// Update buy button and price
self.updateBuyButton();
};
ShopManager.prototype.updateBuyButton = function () {
var self = this;
if (!self.selectedUpgrade) {
return;
}
var price = self.selectedUpgrade.getCurrentPrice();
var canAfford = scoreManager.getScore() >= price;
var isMaxLevel = self.selectedUpgrade.level >= self.selectedUpgrade.maxLevel;
// Store current price length to detect changes
var oldLength = self.priceText.text.length;
var newLength = (isMaxLevel ? 'MAX' : price.toString()).length;
// Store current vertical position
var currentY = self.priceText.y;
// Update buy button based on max level first
if (isMaxLevel) {
self.priceText.setText('MAX');
self.priceCoinIcon.visible = false;
self.priceText.x = 0; // Center the MAX text
self.buyButtonBg.tint = 0x888888;
} else {
// If changing from 4 to 3 digits, completely recreate the text
if (oldLength > 3 && newLength === 3) {
// Remove the old text
if (self.priceText.parent) {
self.priceContainer.removeChild(self.priceText);
}
// Create new text with same properties
self.priceText = new CustomText(price.toString(), {
size: 60,
fill: 0xFFFFFF,
letterSpacing: 5
});
// Restore the vertical position
self.priceText.y = currentY;
self.priceContainer.addChild(self.priceText);
} else {
// Just update the text for other cases
self.priceText.setText(price.toString());
}
self.priceCoinIcon.visible = true;
self.priceCoinIcon.x = 160; // Fixed position
var textPadding = 10;
self.priceText.x = self.priceCoinIcon.x - textPadding - self.priceText.width;
self.buyButtonBg.tint = canAfford ? 0xFFFFFF : 0x888888;
}
// Update total gold display
self.updateTotalGold();
};
ShopManager.prototype.purchaseSelectedUpgrade = function () {
var self = this;
if (!self.selectedUpgrade) {
return;
}
var price = self.selectedUpgrade.getCurrentPrice();
if (scoreManager.getScore() >= price && self.selectedUpgrade.level < self.selectedUpgrade.maxLevel) {
// Add visual feedback animation for buy button
tween(self.buyButton, {
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.buyButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
// Deduct price using the global scoreManager
scoreManager.addScore(-price);
// Call purchase() method
var result = self.selectedUpgrade.purchase();
// Play purchase sound
LK.getSound('shopbuy').play();
// Update button UI
self.updateBuyButton();
// Save the changes
SaveManager.save();
} else {
// Play "can't buy" sound when player doesn't have enough gold or upgrade is maxed
LK.getSound('cantbuy').play();
}
};
ShopManager.prototype.createGoldDisplay = function () {
var self = this;
// Create a container for the gold display
self.goldDisplayContainer = new Container();
self.totalGoldText = new CustomText('0', {
size: 90,
fill: 0xE6D5AC,
letterSpacing: 5
});
self.totalGoldCoinIcon = LK.getAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
self.goldDisplayContainer.addChild(self.totalGoldText);
self.goldDisplayContainer.addChild(self.totalGoldCoinIcon);
self.goldDisplayContainer.x = 90; // Center of gold window
self.goldDisplayContainer.y = -50;
// Position text and coin
self.totalGoldText.x = -50;
self.totalGoldText.y = 65;
self.totalGoldCoinIcon.x = 70;
self.totalGoldCoinIcon.y = 70;
// Add to the window
self.goldWindow.addChild(self.goldDisplayContainer);
// Initial update
self.updateTotalGold();
};
ShopManager.prototype.updateTotalGold = function () {
var self = this;
var totalGold = scoreManager.getScore();
self.totalGoldText.setText(totalGold.toString());
// Keep coin at fixed position
self.totalGoldCoinIcon.x = 165; // Fixed position
// Right-align text relative to coin
// Adjust position based on digit count but keep right edge aligned
var textPadding = 50; // Space between text and coin
self.totalGoldText.x = self.totalGoldCoinIcon.x - textPadding - self.totalGoldText.width;
};
ShopManager.prototype.update = function () {
this.updatePlayerAnimation();
this.updateBuyButton();
};
var CollectibleBehavior = {
initPhysics: function initPhysics(self) {
self.velocityX = 0;
self.velocityY = 0;
self.collected = false;
self.bounceCount = 0;
self.maxBounces = 2;
},
standardUpdate: function standardUpdate(self) {
if (self.collected) {
return;
}
// Apply physics
self.velocityY += 0.5; // gravity
self.x += self.velocityX; // Original throw physics
self.y += self.velocityY;
self.x -= PLATFORM_SPEED * (gameSpeedMultiplier - 1); // Only apply the extra speed from sliding
// Check for platform collision with bounce
if (self.velocityY > 0) {
GameUtils.checkPlatformCollision(self, 80, true);
}
// Check if off screen
if (self.x < -50 || self.y > GAME_HEIGHT) {
self.destroy();
return;
}
// Only apply magnet effect if it's not a health potion OR if player is not at full health
if (!(self instanceof HealthPotion && player.currentHealth >= player.maxHealth)) {
if (player.lootMagnetRange > 1) {
var dx = player.x - self.x;
var dy = player.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var maxAttractionRange = 300 * player.lootMagnetRange;
if (distance < maxAttractionRange) {
// Calculate attraction strength based on distance
var strength = (1 - distance / maxAttractionRange) * 3;
self.velocityX += dx / distance * strength;
self.velocityY += dy / distance * strength;
}
}
}
// Player collection detection - use slide hitbox height if sliding
var playerBounds = {
left: player.x - player.hitboxWidth / 2,
right: player.x + player.hitboxWidth / 2,
top: player.y - player.hitboxHeight / 2,
bottom: player.y + player.hitboxHeight / 2
};
var magnetRange = player.lootMagnetRange || 1;
var itemBounds = {
left: self.x - 25 * magnetRange,
right: self.x + 25 * magnetRange,
top: self.y - 25 * magnetRange,
bottom: self.y + 25 * magnetRange
};
if (GameUtils.checkCollision(playerBounds, itemBounds)) {
self.collect();
}
}
};
// Base behavior for breakable objects
var BreakableBehavior = {
initBreakable: function initBreakable(self) {
self.isBreaking = false;
self.currentPlatform = null;
},
standardBreak: function standardBreak(self, pieceClass, pieceCount, itemSpawnCallback) {
if (self.isBreaking) {
return;
}
self.isBreaking = true;
// Spawn pieces
for (var i = 1; i <= pieceCount; i++) {
var piece = new pieceClass(i);
piece.x = self.x;
piece.y = self.y;
piece.velocityX = GameUtils.randomRange(-6, 6);
piece.velocityY = -GameUtils.randomRange(6, 12);
piece.rotationSpeed = GameUtils.randomRange(-0.1, 0.1);
game.addChild(piece);
}
// Call the custom item spawn callback
if (itemSpawnCallback) {
itemSpawnCallback(self);
}
self.destroy();
}
};
// Base behavior for pieces of broken objects
var PieceBehavior = {
initPiece: function initPiece(self) {
self.velocityX = 0;
self.velocityY = 0;
self.rotationSpeed = 0;
self.fadeSpeed = 0.02;
self.bounceCount = 0;
self.maxBounces = 2;
},
standardUpdate: function standardUpdate(self) {
// Apply physics
self.velocityY += 0.5; // gravity
self.x += self.velocityX;
self.y += self.velocityY;
self.rotation += self.rotationSpeed;
// Check for platform collision
var platformCollision = GameUtils.checkPlatformCollision(self, 80, self.bounceCount < self.maxBounces);
if (platformCollision && self.bounceCount >= self.maxBounces) {
// Start fading after max bounces
self.velocityY = 0;
self.velocityX = -PLATFORM_SPEED;
self.alpha -= self.fadeSpeed;
if (self.alpha <= 0) {
self.destroy();
}
}
// Destroy if off screen
if (self.x < -50 || self.y > GAME_HEIGHT) {
self.destroy();
}
}
};
/****
* Constants
****/
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var PLATFORM_WIDTH = 1000;
var PLATFORM_HALF_WIDTH = PLATFORM_WIDTH / 2;
var PLATFORM_OVERLAP = 50;
var PLATFORM_SPEED = 5;
var GAP_START_TIME = 90000; // 1.5 minutes in milliseconds
var ENEMY_PLATFORM_OFFSET = 225;
var PLAYER_PLATFORM_OFFSET = 250;
var MIN_PLATFORMS_IN_SEQUENCE = 2;
var MAX_PLATFORMS_IN_SEQUENCE = 5;
var JUMP_COOLDOWN = 200;
var MOVE_THRESHOLD = 90;
var SLIDE_MOVE_THRESHOLD = 300;
var VERTICAL_DEADZONE = 30;
var UPGRADE_REGISTRY = {
gold: 0,
health_increase: 0,
arrow_capacity: 0,
levitate: 0,
slide_damage: 0,
better_loot: 0,
potion_chance: 0,
arrow_finder: 0,
ground_slam: 0,
loot_magnet: 0
};
/****
* Utilities
****/
function initializeStorage() {
if (storage.gold === undefined) {
storage.gold = 0;
}
if (storage.healthUpgrade === undefined) {
storage.healthUpgrade = 0;
}
if (storage.arrowUpgrade === undefined) {
storage.arrowUpgrade = 0;
}
if (storage.levitateUpgrade === undefined) {
storage.levitateUpgrade = 0;
}
if (storage.slideUpgrade === undefined) {
storage.slideUpgrade = 0;
}
if (storage.lootUpgrade === undefined) {
storage.lootUpgrade = 0;
}
if (storage.potionChanceUpgrade === undefined) {
storage.potionChanceUpgrade = 0;
}
if (storage.arrowFinderUpgrade === undefined) {
storage.arrowFinderUpgrade = 0;
}
if (storage.groundSlamUpgrade === undefined) {
storage.groundSlamUpgrade = 0;
}
if (storage.lootMagnetUpgrade === undefined) {
storage.lootMagnetUpgrade = 0;
}
}
var GameUtils = {
// Check for collision between two rectangular bounds
checkCollision: function checkCollision(bounds1, bounds2) {
return bounds1.left < bounds2.right && bounds1.right > bounds2.left && bounds1.top < bounds2.bottom && bounds1.bottom > bounds2.top;
},
// Check platform collision with standard offset
checkPlatformCollision: function checkPlatformCollision(obj, offsetY, bounceOnCollision) {
for (var i = 0; i < platforms.length; i++) {
var platform = platforms[i];
if (Math.abs(obj.y - (platform.y - offsetY)) < 10 && obj.x > platform.x - PLATFORM_HALF_WIDTH && obj.x < platform.x + PLATFORM_HALF_WIDTH) {
obj.y = platform.y - offsetY;
if (bounceOnCollision && obj.bounceCount < obj.maxBounces) {
LK.getSound('coinbounce').play();
var impactSpeed = Math.abs(obj.velocityY);
obj.velocityY = -(impactSpeed * 0.5);
obj.velocityX *= 0.8;
obj.bounceCount++;
return true;
} else if (bounceOnCollision) {
obj.velocityY = 0;
obj.velocityX = -PLATFORM_SPEED;
}
return platform;
}
}
return null;
},
// Get random value within range
randomRange: function randomRange(min, max) {
return Math.random() * (max - min) + min;
},
// Check if position is clear for spawning
canSpawnAtPosition: function canSpawnAtPosition(x) {
var safeDistance = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 200;
for (var i = 0; i < collectibles.length; i++) {
if (Math.abs(collectibles[i].x - x) < safeDistance) {
return false;
}
}
return true;
}
};
var ScoreManager = function ScoreManager() {
var self = {};
// Initialize score first
self.score = 0;
// Define methods before using them
self.getScore = function () {
return self.score;
};
self.setScore = function (newScore) {
var oldScore = self.score;
self.score = newScore;
// Update the registry
UPGRADE_REGISTRY.gold = self.score;
// Check if digit count has changed
var oldLength = oldScore.toString().length;
var newLength = newScore.toString().length;
if (oldLength !== newLength && self.scoreText) {
// Store current position
var currentX = self.scoreText.x;
var currentY = self.scoreText.y;
// Remove old text
if (self.scoreText.parent) {
self.container.removeChild(self.scoreText);
}
// Create new text
self.scoreText = new CustomText(newScore.toString(), {
size: 100,
fill: 0xFFFFFF,
letterSpacing: 5
});
// Restore position properties
self.scoreText.y = currentY;
self.container.addChild(self.scoreText);
} else if (self.scoreText) {
// Just update text content
self.scoreText.setText(newScore.toString());
}
// Keep right edge at a fixed position
if (self.scoreText) {
var rightEdgePosition = -10; // Fixed right edge position
self.scoreText.x = rightEdgePosition - self.scoreText.width;
}
};
self.addScore = function (amount, x, y) {
self.setScore(self.score + amount);
// Update the registry
UPGRADE_REGISTRY.gold = self.score;
SaveManager.save();
LK.setScore(self.score);
// Create score popup if position is provided
if (x !== undefined && y !== undefined) {
var popup = new ScorePopup(x, y - 30, amount);
game.addChild(popup);
}
};
// Create UI elements
self.scoreText = new CustomText('0', {
size: 100,
fill: 0xFFFFFF,
letterSpacing: 5
});
// Initialize with default position
self.scoreText.x = -100;
// AFTER UI elements exist AND methods are defined, load saved score
if (storage.gold) {
self.score = Number(storage.gold);
self.setScore(self.score); // Now this is safe because scoreText exists and setScore is defined
}
self.coinIcon = LK.getAsset('coin', {
anchorX: 0,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
// New ammo display
self.ammoText = new CustomText('0*', {
size: 90,
fill: 0xFFFFFF,
letterSpacing: 5
});
self.ammoIcon = LK.getAsset('arrow', {
anchorX: 0,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3,
rotation: -Math.PI / 2 // Rotate 90 degrees counterclockwise
});
// Initialize container
self.container = new Container();
self.container.addChild(self.scoreText);
self.container.addChild(self.coinIcon);
self.container.addChild(self.ammoText);
self.container.addChild(self.ammoIcon);
// Position container
self.container.x = GAME_WIDTH - 100;
self.container.y = 100;
// Position elements
// Position elements
self.scoreText.x = -200; // Less negative value
self.scoreText.y = 0; // Center vertically
self.coinIcon.x = 0;
self.coinIcon.y = 0;
self.ammoText.x = 30;
self.ammoText.y = 170; // Place below coin display
self.ammoIcon.x = 55;
self.ammoIcon.y = 240; // Place below coin display
// Add ammo update method
self.updateAmmo = function (amount) {
self.ammoText.setText(amount + '*');
// Instead of hardcoded adjustments based on digit count,
// calculate the width of the text and properly align it
// Keep the right edge at a fixed position
var rightEdgePosition = 30; // Fixed right edge position
self.ammoText.x = rightEdgePosition - self.ammoText.width;
};
return self;
};
/****
* Game Variables
****/
// Containers
var backgroundContainer = game.addChild(new Container());
var midgroundContainer = game.addChild(new Container());
var foregroundContainer = game.addChild(new Container());
var scoreManager;
var scoreContainer = game.addChild(new Container());
// Game state
var gameStarted = false;
var titleScreen;
var playButton;
var gameStartTime;
var tutorialActive = false;
var tutorialManager;
var tutorialButton;
var playButtonFlashInterval;
// Platform management
var platforms = [];
var gameSpeedMultiplier = 1.0;
var platformSpawnCounter = 0;
var platformsUntilNextChange = 0;
var lowPlatformHeight = GAME_HEIGHT / 1.5 + PLAYER_PLATFORM_OFFSET - 100;
var highPlatformHeight = lowPlatformHeight - 600;
var HIGHEST_PLATFORM_HEIGHT = highPlatformHeight - 600;
var LOWEST_PLATFORM_HEIGHT = lowPlatformHeight + 600;
var currentPlatformHeight = lowPlatformHeight;
var lastPlatformHeight = lowPlatformHeight;
var lastPlatformX = 0;
var currentPlatformPattern = 'A'; // 'A' for 1&3, 'B' for 2&4
var currentStraightHeight;
// Touch controls
var touchStartX = 0;
var touchStartY = 0;
var touchEndX = 0;
var touchEndY = 0;
var lastMoveY = 0;
var lastJumpTime = 0;
// Game objects
var jars = [];
var coins = [];
var collectibles = [];
var arrows = [];
var skeletonSwords = [];
var jarSpawnCounter = 0;
var jarSpawnInterval = 10;
var chestSpawnCounter = 0;
var chestSpawnInterval = 75;
var enemies = [];
var enemySpawnCounter = 0;
var enemySpawnInterval = 100;
var eyeballSpawnCounter = 0;
var eyeballSpawnInterval = 200;
var particleSystem;
var heartContainer = new HeartContainer();
scoreManager = new ScoreManager(); // Create it early
var player;
// Background elements
var bg1 = backgroundContainer.addChild(LK.getAsset('background', {
anchorX: 0,
anchorY: 1
}));
var bg2 = backgroundContainer.addChild(LK.getAsset('background', {
anchorX: 0,
anchorY: 1
}));
bg1.y = GAME_HEIGHT;
bg2.y = GAME_HEIGHT;
bg2.x = GAME_WIDTH;
// Midground elements
var mg1 = midgroundContainer.addChild(LK.getAsset('midground', {
anchorX: 0,
anchorY: 1,
tint: 0x777777
}));
var mg2 = midgroundContainer.addChild(LK.getAsset('midground', {
anchorX: 0,
anchorY: 1,
tint: 0x777777
}));
mg1.y = GAME_HEIGHT;
mg2.y = GAME_HEIGHT;
mg2.x = GAME_WIDTH;
// Foreground elements
var fg1 = foregroundContainer.addChild(LK.getAsset('foreground', {
anchorX: 0,
anchorY: 1,
tint: 0x777777
}));
var fg2 = foregroundContainer.addChild(LK.getAsset('foreground', {
anchorX: 0,
anchorY: 1,
tint: 0x777777
}));
fg1.y = GAME_HEIGHT * 1.25;
fg2.y = GAME_HEIGHT * 1.25;
fg1.x = 0;
fg2.x = GAME_WIDTH;
/****
* Game Functions
****/
// Screen shake effect function
function applyScreenShake() {
var shakeIntensity = 30;
var shakeDuration = 400;
var shakeCount = 8;
var originalX = game.x;
var originalY = game.y;
// Stop any existing shake
tween.stop(game, {
x: true,
y: true
});
// Reset position to ensure we start from 0,0
game.x = 0;
game.y = 0;
// Recursive function to create multiple shakes
function createShake(count) {
if (count <= 0) {
// Final tween to settle back to original position
tween(game, {
x: originalX,
y: originalY
}, {
duration: shakeDuration / 4,
easing: tween.easeOut
});
return;
}
// Calculate decreasing intensity
var currentIntensity = shakeIntensity * (count / shakeCount);
// Random offset direction
var offsetX = (Math.random() - 0.5) * 2 * currentIntensity;
var offsetY = (Math.random() - 0.5) * 2 * currentIntensity;
// Apply shake movement
tween(game, {
x: offsetX,
y: offsetY
}, {
duration: shakeDuration / shakeCount,
easing: tween.linear,
onFinish: function onFinish() {
createShake(count - 1);
}
});
}
// Start the shake sequence
createShake(shakeCount);
}
function createGameplayShopButton() {
// Create shop button in top corner
var shopButton = game.addChild(LK.getAsset('shopbutton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.7,
scaleY: 0.7
}));
shopButton.x = 250;
shopButton.y = 310; // Position below score display
shopButton.down = function () {
// Pause the game
var previousGameSpeed = gameSpeedMultiplier;
// IMPORTANT FIX: Force cancel any active movement states
if (player) {
// Cancel slide if active
if (player.isSliding) {
player.isSliding = false;
player.slideTimer = 0;
player.hitboxHeight = player.normalHitboxHeight;
}
// Cancel air dash if active
if (player.isAirDashing) {
player.isAirDashing = false;
player.airDashTimer = 0;
// Clean up shadows
player.shadowImages.forEach(function (shadow) {
if (shadow.parent) {
shadow.destroy();
}
});
player.shadowImages = [];
// Reset rotation
player.rotation = 0;
}
}
// Now set game speed to 0
gameSpeedMultiplier = 0;
// Open shop
openGameplayShop();
// Function to return to game
function resumeGame() {
// Close shop
if (game.shopManager && game.shopManager.parent) {
game.shopManager.destroy();
game.shopManager = null;
}
// Restore game speed, but ensure it's not the enhanced speed from slide/dash
gameSpeedMultiplier = 1.0; // IMPORTANT FIX: Always restore to 1.0, not previousGameSpeed
// Remove back button using our reference
if (gameplayBackButton && gameplayBackButton.parent) {
gameplayBackButton.destroy();
}
}
// Add custom back button for gameplay shop
gameplayBackButton = game.addChild(LK.getAsset('backbutton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.75,
scaleY: 0.75,
x: GAME_WIDTH / 2,
y: GAME_HEIGHT - 200
}));
gameplayBackButton.down = function () {
LK.getSound('menuselect').play();
resumeGame();
};
};
return shopButton;
}
// Modified openShop function for gameplay
function openGameplayShop() {
SaveManager.load();
// Create shop if it doesn't exist
if (!game.shopManager) {
game.shopManager = new ShopManager();
game.shopManager.x = GAME_WIDTH / 2;
game.shopManager.y = GAME_HEIGHT / 2;
}
// Show shop and force an update
game.addChild(game.shopManager);
game.shopManager.updateTotalGold();
// Override the purchase function to apply upgrades immediately
var originalPurchase = game.shopManager.purchaseSelectedUpgrade;
game.shopManager.purchaseSelectedUpgrade = function () {
originalPurchase.call(game.shopManager);
SaveManager.applyUpgradeEffects(player);
};
}
function applyPlayerUpgradeEffects() {
if (!player) {
return;
}
// Apply health upgrade if saved
if (storage.playerMaxHealth) {
player.maxHealth = parseInt(storage.playerMaxHealth);
player.currentHealth = player.maxHealth;
player.heartContainer.updateHealth(player.currentHealth);
}
// Apply arrow capacity upgrade if saved
if (storage.playerArrowCapacity) {
player.ammoCount = parseInt(storage.playerArrowCapacity);
scoreManager.updateAmmo(player.ammoCount);
}
// Apply levitate upgrade if saved
if (storage.playerHasLevitate) {
player.hasLevitate = true;
}
// Apply any other player effect upgrades here
}
// Create the title screen
function createTitleScreen() {
titleScreen = new Container();
game.addChild(titleScreen);
// Add title image with fade-in
var titleImage = titleScreen.attachAsset('title', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
titleImage.x = GAME_WIDTH / 2;
titleImage.y = GAME_HEIGHT / 2.7;
// Fade in title
tween(titleImage, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeIn
});
// Add play button
playButton = titleScreen.attachAsset('playbutton', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
playButton.x = GAME_WIDTH / 2;
playButton.y = GAME_HEIGHT / 1.4;
tween(playButton, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeIn
});
// Add tutorial button
tutorialButton = titleScreen.attachAsset('tutorialbutton', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
tutorialButton.x = GAME_WIDTH / 3;
tutorialButton.y = GAME_HEIGHT / 1.15;
tween(tutorialButton, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeIn
});
var shopButton = titleScreen.attachAsset('shopbutton', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
shopButton.x = GAME_WIDTH / 1.4;
shopButton.y = GAME_HEIGHT / 1.15; // Position between play and tutorial buttons
shopButton.down = function () {
// Add visual feedback animation
tween(shopButton, {
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(shopButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
openShop();
}
});
}
});
// Play menu select sound
LK.getSound('menuselect').play();
};
tween(shopButton, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeIn
});
// Add flashing animation for play button only
function flashPlayButton() {
tween(playButton, {
alpha: 0
}, {
duration: 250,
easing: tween.linear,
onFinish: function onFinish() {
tween(playButton, {
alpha: 1
}, {
duration: 250,
easing: tween.linear
});
}
});
}
if (playButtonFlashInterval) {
LK.clearInterval(playButtonFlashInterval);
}
// Flash every 2 seconds
playButtonFlashInterval = LK.setInterval(flashPlayButton, 2000);
// Initialize torch decorations
initializeTorches();
}
function resetAllStorage() {
// Log the current state
// Delete all stored values
storage.gold = 0;
storage.healthUpgrade = 0;
storage.arrowUpgrade = 0;
storage.levitateUpgrade = 0;
storage.slideUpgrade = 0;
storage.lootUpgrade = 0;
storage.groundSlamUpgrade = 0;
storage.potionChanceUpgrade = 0;
storage.arrowFinderUpgrade = 0;
}
function openShop() {
// Hide title screen
titleScreen.visible = false;
SaveManager.load();
// Create shop if it doesn't exist
if (!game.shopManager) {
game.shopManager = new ShopManager();
game.shopManager.x = GAME_WIDTH / 2;
game.shopManager.y = GAME_HEIGHT / 2;
}
// Show shop and force an update
game.addChild(game.shopManager);
game.shopManager.updateTotalGold();
// Add back button
var backButton = game.shopManager.attachAsset('backbutton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.75,
scaleY: 0.75,
x: 0,
y: 1100
});
backButton.down = function () {
// Add visual feedback animation
tween(backButton, {
scaleX: 0.65,
scaleY: 0.65
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(backButton, {
scaleX: 0.75,
scaleY: 0.75
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
closeShop();
}
});
}
});
// Play menu select sound
LK.getSound('menuselect').play();
};
}
function closeShop() {
// Save the current state before closing
SaveManager.save();
if (game.shopManager && game.shopManager.parent) {
game.shopManager.destroy(); // Destroy the manager and all its children
game.shopManager = null; // Clear the reference
}
// Ensure title screen is visible after shop closes
if (titleScreen && !titleScreen.parent) {
// If title screen was destroyed or removed, recreate it
createTitleScreen();
} else if (titleScreen) {
titleScreen.visible = true;
}
}
// Place torches in the scene
function initializeTorches() {
// Check if torches already exist
if (midgroundContainer.children.some(function (child) {
return child instanceof Torch;
})) {
return;
}
// Create torches for the two background sections
var torch1 = new Torch();
torch1.x = 25;
torch1.y = GAME_HEIGHT * 0.7;
midgroundContainer.addChild(torch1);
var torch2 = new Torch();
torch2.x = GAME_WIDTH + 25;
torch2.y = GAME_HEIGHT * 0.7;
midgroundContainer.addChild(torch2);
}
function startTutorial() {
// Clear title screen
titleScreen.destroy();
// Initialize game state
gameStarted = true;
tutorialActive = true;
// Create and initialize tutorial manager
tutorialManager = new TutorialManager();
tutorialManager.init();
// Initialize particle system if needed
if (!particleSystem) {
particleSystem = new ParticlePool(100);
game.addChild(particleSystem);
}
}
// Initialize game elements
function initializeGame() {
// Create player
SaveManager.load();
player = game.addChild(new Player());
player.x = GAME_WIDTH / 4.5;
player.y = GAME_HEIGHT / 1.5 - 100;
game.addChild(heartContainer);
SaveManager.applyUpgradeEffects(player);
scoreManager.updateAmmo(player.ammoCount);
// Create initial platforms at the low level
for (var i = 0; i < 5; i++) {
var platform = new Platform();
if (i === 0) {
// First platform centered on player
platform.x = player.x;
} else {
// Position with slight overlap
platform.x = lastPlatformX + PLATFORM_WIDTH - PLATFORM_OVERLAP;
}
platform.y = lowPlatformHeight;
platforms.push(platform);
game.addChild(platform);
lastPlatformX = platform.x;
}
lastPlatformHeight = lowPlatformHeight;
currentPlatformPattern = 'A';
platformsUntilNextChange = 0; // This will trigger new sequence immediately after initial platforms
player.isOnGround = true;
player.currentPlatform = platforms[0];
}
// Start the game
function startGame() {
gameStarted = true;
gameStartTime = Date.now();
titleScreen.destroy();
// Initialize score manager
game.addChild(scoreManager.container);
initializeGame();
// Initialize particle system
particleSystem = new ParticlePool(100);
game.addChild(particleSystem);
// Add shop button to gameplay
createGameplayShopButton();
// Show health
player.heartContainer.alpha = 1;
player.heartVisibilityTimer = 120;
// Play background music
LK.playMusic('backgroundmusic1', {
fade: {
start: 0,
end: 0.7,
duration: 3500
}
});
}
// Update background layers
function updateBackgrounds() {
// Background layer (slowest)
bg1.x -= PLATFORM_SPEED * 0.3 * gameSpeedMultiplier;
bg2.x -= PLATFORM_SPEED * 0.3 * gameSpeedMultiplier;
if (bg1.x <= -GAME_WIDTH) {
bg1.x = bg2.x + GAME_WIDTH;
}
if (bg2.x <= -GAME_WIDTH) {
bg2.x = bg1.x + GAME_WIDTH;
}
// Midground layer
mg1.x -= PLATFORM_SPEED * 0.6 * gameSpeedMultiplier;
mg2.x -= PLATFORM_SPEED * 0.6 * gameSpeedMultiplier;
if (mg1.x <= -GAME_WIDTH) {
mg1.x = mg2.x + GAME_WIDTH;
}
if (mg2.x <= -GAME_WIDTH) {
mg2.x = mg1.x + GAME_WIDTH;
}
// Foreground layer (fastest)
fg1.x -= PLATFORM_SPEED * gameSpeedMultiplier;
fg2.x -= PLATFORM_SPEED * gameSpeedMultiplier;
if (fg1.x <= -GAME_WIDTH) {
fg1.x = fg2.x + GAME_WIDTH;
}
if (fg2.x <= -GAME_WIDTH) {
fg2.x = fg1.x + GAME_WIDTH;
}
// Update torches
for (var i = 0; i < midgroundContainer.children.length; i++) {
var child = midgroundContainer.children[i];
if (child instanceof Torch) {
child.update();
child.x -= PLATFORM_SPEED * 0.6 * gameSpeedMultiplier;
if (child.x <= -GAME_WIDTH) {
child.x = child.x + GAME_WIDTH * 2;
}
}
}
}
// Update and spawn platforms
function updatePlatforms() {
var lastPlatform = platforms[platforms.length - 1];
if (lastPlatform && lastPlatform.x < GAME_WIDTH + 500) {
// Special case for first platform after initial sequence
if (!currentPlatformPattern) {
currentPlatformPattern = 'A';
platformsUntilNextChange = Math.floor(Math.random() * (MAX_PLATFORMS_IN_SEQUENCE - MIN_PLATFORMS_IN_SEQUENCE + 1)) + MIN_PLATFORMS_IN_SEQUENCE;
}
if (platformsUntilNextChange <= 0) {
// Add chance to switch to pattern C (straight section)
var patternChance = Math.random();
if (patternChance < 0.1) {
currentPlatformPattern = 'C';
platformsUntilNextChange = Math.floor(Math.random() * 4) + 6;
currentStraightHeight = Math.random() < 0.5 ? highPlatformHeight : lowPlatformHeight;
} else {
currentPlatformPattern = currentPlatformPattern === 'A' ? 'B' : 'A';
platformsUntilNextChange = Math.floor(Math.random() * (MAX_PLATFORMS_IN_SEQUENCE - MIN_PLATFORMS_IN_SEQUENCE + 1)) + MIN_PLATFORMS_IN_SEQUENCE;
}
}
// Calculate gap probability
var timeSinceStart = Date.now() - gameStartTime;
var baseGapChance = 0.025;
var additionalChance = Math.min(0.10, timeSinceStart / 600000);
var totalGapChance = baseGapChance + additionalChance;
// Calculate trap platform chance based on time
var baseTrapChance = 0.01; // 1% base chance
var additionalChance = Math.min(0.15, timeSinceStart / 300000); // Up to 15% extra after 5 minutes
var totalTrapChance = baseTrapChance + additionalChance;
if (currentPlatformPattern === 'C') {
// Single platform for straight sections, using stored height
var platform;
if (Math.random() < totalTrapChance) {
platform = new CrumblePlatform();
} else {
platform = new Platform();
}
if (Math.random() < totalGapChance) {
platform.x = lastPlatform.x + (PLATFORM_WIDTH + 400);
} else {
platform.x = lastPlatform.x + (PLATFORM_WIDTH - PLATFORM_OVERLAP);
}
platform.y = currentStraightHeight;
platforms.push(platform);
game.addChild(platform);
} else {
// Regular dual platform patterns with possible double gaps
var platformUpper, platformLower;
// Create upper platform
if (Math.random() < totalTrapChance) {
platformUpper = new CrumblePlatform();
} else {
platformUpper = new Platform();
}
// Create lower platform
if (Math.random() < totalTrapChance) {
platformLower = new CrumblePlatform();
} else {
platformLower = new Platform();
}
// Determine gap type with random chance
var gapChance = Math.random();
var hasDoubleGap = gapChance < totalGapChance * 0.3; // 30% of gap chance is double gap
var hasSingleGap = !hasDoubleGap && gapChance < totalGapChance; // Remaining gap chance is single gap
// If single gap, randomly choose which platform gets it
var upperGap = hasDoubleGap || hasSingleGap && Math.random() < 0.5;
var lowerGap = hasDoubleGap || hasSingleGap && !upperGap;
// Position upper platform
if (upperGap) {
platformUpper.x = lastPlatform.x + (PLATFORM_WIDTH + 400);
} else {
platformUpper.x = lastPlatform.x + (PLATFORM_WIDTH - PLATFORM_OVERLAP);
}
// Position lower platform
if (lowerGap) {
platformLower.x = lastPlatform.x + (PLATFORM_WIDTH + 400);
} else {
platformLower.x = lastPlatform.x + (PLATFORM_WIDTH - PLATFORM_OVERLAP);
}
if (currentPlatformPattern === 'A') {
platformUpper.y = HIGHEST_PLATFORM_HEIGHT;
platformLower.y = lowPlatformHeight;
} else {
platformUpper.y = highPlatformHeight;
platformLower.y = LOWEST_PLATFORM_HEIGHT;
}
platforms.push(platformUpper);
platforms.push(platformLower);
game.addChild(platformUpper);
game.addChild(platformLower);
}
platformsUntilNextChange--;
}
// Update platforms
for (var i = platforms.length - 1; i >= 0; i--) {
platforms[i].update();
if (platforms[i].destroyed) {
platforms.splice(i, 1);
}
}
}
// Update and spawn collectibles
function updateCollectibles() {
// Jar spawning
jarSpawnCounter++;
if (jarSpawnCounter >= jarSpawnInterval) {
var availablePlatforms = platforms.filter(function (p) {
return p.x > GAME_WIDTH && p.x < GAME_WIDTH + 300 && !(p instanceof CrumblePlatform);
});
// Try to spawn on multiple platforms
availablePlatforms.forEach(function (platform) {
if (Math.random() < 0.25 && GameUtils.canSpawnAtPosition(platform.x)) {
// Adjusted probability
var jar = new Jar();
jar.x = platform.x;
jar.y = platform.y - 130;
jar.currentPlatform = platform;
collectibles.push(jar);
game.addChild(jar);
}
});
jarSpawnCounter = 0;
}
// Treasure chest spawning
chestSpawnCounter++;
if (chestSpawnCounter >= chestSpawnInterval) {
var availablePlatforms = platforms.filter(function (p) {
return p.x > GAME_WIDTH && p.x < GAME_WIDTH + 300 && !(p instanceof CrumblePlatform);
});
availablePlatforms.forEach(function (platform) {
if (Math.random() < 0.12 && GameUtils.canSpawnAtPosition(platform.x)) {
// Adjusted probability
var chest = new TreasureChest();
chest.x = platform.x;
chest.y = platform.y - 130;
chest.currentPlatform = platform;
collectibles.push(chest);
game.addChild(chest);
}
});
chestSpawnCounter = 0;
}
// Update existing collectibles
for (var i = collectibles.length - 1; i >= 0; i--) {
var collectible = collectibles[i];
if (collectible.currentPlatform) {
collectible.x = collectible.currentPlatform.x;
}
var attackBounds = player.getAttackBounds();
var slideAttackBounds = player.getSlideAttackBounds();
var itemBounds = {
left: collectible.x - 50,
right: collectible.x + 50,
top: collectible.y - 75,
bottom: collectible.y + 75
};
if (attackBounds && GameUtils.checkCollision(attackBounds, itemBounds) || slideAttackBounds && GameUtils.checkCollision(slideAttackBounds, itemBounds)) {
collectible["break"]();
collectibles.splice(i, 1);
continue;
}
if (collectible.x < -50) {
collectible.destroy();
collectibles.splice(i, 1);
}
}
// Update coins and other collectibles
for (var i = coins.length - 1; i >= 0; i--) {
coins[i].update();
if (coins[i].destroyed) {
coins.splice(i, 1);
}
}
// Update score popups
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
if (child instanceof ScorePopup) {
child.update();
}
}
}
// Update and spawn enemies
function updateEnemies() {
if (game.shopManager && game.shopManager.parent) {
return;
}
if (!tutorialActive) {
var isPlatformOccupied = function isPlatformOccupied(platform) {
return enemies.some(function (enemy) {
return enemy.currentPlatform === platform && (enemy.type === 'goblin' || enemy.type === 'skeleton' || enemy.type === 'mushroom');
});
};
// Get available platforms for ground enemies
var availablePlatforms = platforms.filter(function (p) {
return p.x > GAME_WIDTH - 100 && p.x < GAME_WIDTH + 300;
});
// Filter out platforms that already have ground enemies
var unoccupiedPlatforms = availablePlatforms.filter(function (p) {
return !isPlatformOccupied(p);
});
// Unified enemy spawn counter for all ground enemies
enemySpawnCounter++;
// If we have available platforms and it's time to spawn
if (enemySpawnCounter >= enemySpawnInterval && unoccupiedPlatforms.length > 0) {
// Decide how many enemies to spawn (1-3 based on available platforms)
var spawnCount = Math.min(Math.floor(Math.random() * 3) + 1, unoccupiedPlatforms.length);
for (var i = 0; i < spawnCount; i++) {
// Choose a random platform from unoccupied ones
var platformIndex = Math.floor(Math.random() * unoccupiedPlatforms.length);
var platform = unoccupiedPlatforms[platformIndex];
// Choose enemy type randomly with weighted probabilities
var enemyRandom = Math.random();
var enemyType;
if (enemyRandom < 0.45) {
enemyType = 'goblin'; // 45% chance for goblin
} else if (enemyRandom < 0.75) {
enemyType = 'mushroom'; // 30% chance for mushroom
} else {
enemyType = 'skeleton'; // 25% chance for skeleton
}
// Create the enemy
var enemy = new Enemy(enemyType);
enemy.x = platform.x;
enemy.y = platform.y - ENEMY_PLATFORM_OFFSET;
enemy.currentPlatform = platform;
enemies.push(enemy);
game.addChild(enemy);
// Remove the used platform from available platforms
unoccupiedPlatforms.splice(platformIndex, 1);
}
// Reset the counter with a random interval
enemySpawnInterval = Math.floor(Math.random() * 100) + 150;
enemySpawnCounter = 0;
}
// Eyeball spawning - kept separate since they don't use platforms
eyeballSpawnCounter++;
if (eyeballSpawnCounter >= eyeballSpawnInterval) {
var enemy = new Enemy('eyeball');
var heightRange = LOWEST_PLATFORM_HEIGHT - HIGHEST_PLATFORM_HEIGHT;
var randomHeight = Math.random() * heightRange;
enemy.x = GAME_WIDTH + 100;
enemy.y = HIGHEST_PLATFORM_HEIGHT + randomHeight;
enemies.push(enemy);
game.addChild(enemy);
// Random interval between 200-400 ticks
eyeballSpawnInterval = Math.floor(Math.random() * 200) + 200;
eyeballSpawnCounter = 0;
}
}
// Update enemies and check collisions
for (var j = enemies.length - 1; j >= 0; j--) {
enemies[j].update();
// Check if enemy has been destroyed
if (enemies[j].destroyed) {
enemies.splice(j, 1);
continue;
}
// Skip if enemy is far behind player
if (enemies[j].x < player.x - 100) {
continue;
}
var playerBounds = player.getCollisionBounds();
var enemyBounds = enemies[j].getBounds();
var attackBounds = player.getAttackBounds();
var slideAttackBounds = player.getSlideAttackBounds();
// Check for attack collision first
if (attackBounds && !enemies[j].isHit && !enemies[j].isDying) {
if (enemies[j].x > player.x && GameUtils.checkCollision(attackBounds, enemyBounds)) {
enemies[j].hit();
if (enemies[j].type === 'eyeball') {
LK.getSound('eyeballhit').play();
} else if (enemies[j].type === 'skeleton') {
LK.getSound('skeletonhit').play();
} else {
LK.getSound('enemyhit').play();
}
continue;
}
}
// In the updateEnemies() function, inside the enemy update loop
for (var i = arrows.length - 1; i >= 0; i--) {
var arrow = arrows[i];
if (GameUtils.checkCollision(arrow.getBounds(), enemyBounds)) {
// Only hit the enemy and destroy arrow if enemy is not already hit or dying
if (!enemies[j].isHit && !enemies[j].isDying) {
enemies[j].hit();
if (enemies[j].type === 'eyeball') {
LK.getSound('eyeballhit').play();
} else if (enemies[j].type === 'skeleton') {
LK.getSound('skeletonhit').play();
} else {
LK.getSound('enemyhit').play();
}
// Only destroy the arrow if it actually damages an enemy
arrow.destroy();
arrows.splice(i, 1);
break;
}
// Arrows pass through already hit/dying enemies
}
}
for (var i = skeletonSwords.length - 1; i >= 0; i--) {
if (!skeletonSwords[i]) {
skeletonSwords.splice(i, 1);
continue;
}
skeletonSwords[i].update();
// Check collision with player
if (GameUtils.checkCollision(skeletonSwords[i].getBounds(), playerBounds)) {
if (!player.isInvulnerable && !player.isDying) {
player.takeDamage();
skeletonSwords[i].destroy();
skeletonSwords.splice(i, 1);
}
}
// Remove destroyed swords - safely check if the sword exists first and if it has destroyed property
if (skeletonSwords[i] && skeletonSwords[i].destroyed === true) {
skeletonSwords.splice(i, 1);
}
}
// In the updateEnemies function where spore collisions are checked
if (window.spores) {
for (var i = window.spores.length - 1; i >= 0; i--) {
var spore = window.spores[i];
// Skip if spore is invalid or already marked for removal
if (!spore || !spore.parent || spore.destroyed) {
window.spores.splice(i, 1);
continue;
}
// Update spore
spore.update();
// Only check collisions if the spore is active and player can take damage
if (spore.isActive && !player.isInvulnerable && !player.isDying) {
if (GameUtils.checkCollision(spore.getBounds(), player.getCollisionBounds())) {
player.takeDamage();
spore.hasHitPlayer = true;
spore.explode();
}
}
// Safety check - if spore is destroyed but still in array, remove it
if (spore.destroyed) {
window.spores.splice(i, 1);
}
}
}
// Check for slide collision
if (slideAttackBounds && !enemies[j].isHit && !enemies[j].isDying) {
if (enemies[j].x > player.x && GameUtils.checkCollision(slideAttackBounds, enemyBounds)) {
enemies[j].slideHit();
continue;
}
}
if ((slideAttackBounds || player.isAirDashing && playerBounds) && !enemies[j].isHit && !enemies[j].isDying) {
if (enemies[j].x > player.x && GameUtils.checkCollision(player.isAirDashing ? playerBounds : slideAttackBounds, enemyBounds)) {
enemies[j].slideHit();
continue;
}
}
if (GameUtils.checkCollision(playerBounds, enemyBounds)) {
if (!enemies[j].isHit && !enemies[j].isDying) {
player.takeDamage();
}
}
}
}
if (!game.saveManager) {
game.saveManager = SaveManager;
SaveManager.load();
}
// Create the initial title screen
createTitleScreen();
/****
* Game Loop & Input Handlers
****/
// Main game update loop
// Modify the main game update function
game.update = function () {
// Always update backgrounds
updateBackgrounds();
if (!gameStarted) {
// Add shop update here if shop is open
if (game.shopManager) {
game.shopManager.update();
}
return;
}
// Update player and particles
player.update();
if (particleSystem) {
particleSystem.update();
}
// Update arrows
for (var i = arrows.length - 1; i >= 0; i--) {
arrows[i].update();
if (arrows[i].destroyed) {
arrows.splice(i, 1);
}
}
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
if (child instanceof CrumblePiece) {
child.update();
}
}
if (tutorialActive) {
tutorialManager.update();
} else {
// Normal game updates
updatePlatforms();
updateCollectibles();
updateEnemies();
}
};
// Handle touch/click events
game.down = function (x, y, obj) {
touchStartX = x;
touchStartY = y;
};
game.up = function (x, y, obj) {
if (game.shopManager && game.shopManager.parent) {
return;
}
if (!gameStarted) {
// Don't process button clicks if shop is open
if (game.shopManager && game.shopManager.parent) {
return;
}
var playButtonBounds = {
left: playButton.x - playButton.width / 2,
right: playButton.x + playButton.width / 2,
top: playButton.y - playButton.height / 2,
bottom: playButton.y + playButton.height / 2
};
var tutorialButtonBounds = {
left: tutorialButton.x - tutorialButton.width / 2,
right: tutorialButton.x + tutorialButton.width / 2,
top: tutorialButton.y - tutorialButton.height / 2,
bottom: tutorialButton.y + tutorialButton.height / 2
};
if (x >= playButtonBounds.left && x <= playButtonBounds.right && y >= playButtonBounds.top && y <= playButtonBounds.bottom) {
// Check if the button has already been pressed
if (game.playButtonPressed) {
return;
}
game.playButtonPressed = true;
// Stop the flashing animation
if (playButtonFlashInterval) {
LK.clearInterval(playButtonFlashInterval);
playButtonFlashInterval = null;
// Ensure button is fully visible before animation
playButton.alpha = 1;
}
// Play start game sound effect
LK.getSound('startgame').play();
// Add scale animation
tween(playButton, {
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(playButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 250,
easing: tween.easeOut,
onFinish: function onFinish() {
// Start game after a 1 second delay
LK.setTimeout(function () {
startGame();
}, 900);
}
});
}
});
} else if (x >= tutorialButtonBounds.left && x <= tutorialButtonBounds.right && y >= tutorialButtonBounds.top && y <= tutorialButtonBounds.bottom) {
// Add visual feedback animation
tween(tutorialButton, {
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(tutorialButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
startTutorial();
}
});
}
});
// Play menu select sound
LK.getSound('menuselect').play();
return;
}
return;
}
// Prevent input if player is dying or air dying
if (player && (player.isDying || player.dyingInAir)) {
return;
}
if (tutorialActive && tutorialManager) {
tutorialManager.inputProcessed = false; // Reset the flag on touch end
}
touchEndX = x;
touchEndY = y;
if (tutorialActive) {
tutorialManager.checkInput(touchStartX, touchStartY, touchEndX, touchEndY);
}
var deltaY = touchEndY - touchStartY;
var deltaX = touchEndX - touchStartX;
// Ignore very small movements
if (Math.abs(deltaY) < VERTICAL_DEADZONE && Math.abs(deltaX) < VERTICAL_DEADZONE) {
player.attack(); // Just treat as tap/attack
return;
}
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Horizontal swipe
if (deltaX > 50) {
if (player.isOnGround) {
player.slide();
} else {
player.airDash();
}
} else if (deltaX < -50) {
// Left swipe - trigger bow shot
player.shoot();
}
} else if (deltaY > 120) {
// Downward swipe
if (!player.isOnGround) {
player.groundSlam(); // Call ground slam FIRST when in air
} else {
player.fallThrough();
}
} else if (deltaY < -120) {
// Upward swipe - existing jump code
var currentTime = Date.now();
if (currentTime - lastJumpTime > JUMP_COOLDOWN) {
player.jump();
lastJumpTime = currentTime;
}
} else {
// Tap - existing attack code
player.attack();
}
};
game.move = function (x, y, obj) {
if (!gameStarted) {
return;
}
var deltaX = x - touchStartX;
lastMoveY = y;
}; ===================================================================
--- original.js
+++ change.js
@@ -407,241 +407,324 @@
// Initialize with provided text
self.setText(text);
return self;
});
-// Configuration object for different enemy types
-// Enemy class with refactored animation management
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
- self.config = ENEMY_CONFIGS[type] || ENEMY_CONFIGS.goblin; // Default to goblin if type is invalid
- // Common properties from config
- self.type = self.config.type;
- self.speed = self.config.speed;
- self.lootModifier = self.config.lootModifier;
- self.hitboxWidth = self.config.hitboxWidth;
- self.hitboxHeight = self.config.hitboxHeight;
- self.animationSpeed = self.config.animationSpeed;
- // Common state properties
+ self.type = type || 'basic';
+ // Common properties
+ self.speed = 7;
self.isOnGround = true;
self.velocityY = 0;
self.currentPlatform = null;
self.isHit = false;
self.isDying = false;
- self.hitTimer = 0; // Controls duration of hit state
- self.deathTimer = 0; // Controls progression of death animation/state
- self.throwBackSpeed = 0; // Initialized when hit
+ self.isFadingOut = false;
+ self.fadeOutTimer = 0;
+ // self.deathTimer = 0; // Replaced by animation system + fadeOutTimer
+ self.throwBackSpeed = 15;
self.throwBackDistance = 0;
- self.maxThrowBack = 200;
- self.hitType = 'none'; // 'attack', 'slide'
- self.isFadingOut = false; // For death sequence
- self.fadeOutTicks = 0;
- // Animation system properties
- self.sprites = []; // Flat list of all sprite objects for this enemy
- self.animationFrameMap = {}; // e.g., { run: [sprite1, sprite2], hit: [sprite3] }
- self.currentAnimationName = null;
- self.currentFrameIndex = 0;
- self.animationTickCounter = 0; // Used instead of global self.animationCounter
- // Type-specific properties initialization
- if (self.type === 'eyeball') {
- self.isFlying = self.config.isFlying;
- self.flyingHeight = GAME_HEIGHT / 2 + (Math.random() * 400 - 200); // Centered around middle, wider range
- self.verticalSpeed = self.config.verticalSpeed; // Current vertical speed for bobbing/homing
- self.maxVerticalSpeed = self.config.maxVerticalSpeed; // Max homing speed
- self.homingDelay = self.config.homingDelay;
- self.homingTimer = 0;
- }
- if (self.type === 'skeleton') {
- self.isThrowing = false;
- self.throwingTimer = 0; // Countdown for throw animation
- self.throwingDuration = self.config.throwingDuration;
- self.throwingPauseTime = self.config.throwingPauseTime;
- // self.throwingFrame is managed by playAnimation's currentFrameIndex
- self.swordThrown = false; // Flag to ensure sword is thrown only once per animation cycle
- self.initialThrowMade = undefined; // For first throw logic
- self.originalSpeed = self.speed; // To restore after throwing
- }
+ self.hitType = 'none'; // 'none', 'attack', 'slide'
+ self.hitboxWidth = 200;
+ self.hitboxHeight = 260;
+ self.lootModifier = 1.0;
+ self.originalSpeed = self.speed; // To store speed before state changes like attack/throw
+ // Animation system
+ self.sprites = []; // Array of all animation sprites for this enemy
+ self.animations = {}; // Stores arrays of sprite names: {run: [...], hit: [...], die: [...]}
+ self.animationOffsets = {}; // Stores starting index in self.sprites for each animation type
+ self.currentAnimation = {
+ type: null,
+ frames: [],
+ frameIndex: 0,
+ spriteOffset: 0,
+ counter: 0,
+ speed: 0.11,
+ // Default animation speed
+ loop: true
+ };
+ self.animationSpeed = 0.11; // Base animation speed
+ // Type-specific properties and animation definitions
if (self.type === 'mushroom') {
+ self.speed = 6;
+ self.animations.run = ['mushroomrun1', 'mushroomrun2', 'mushroomrun3', 'mushroomrun4', 'mushroomrun5', 'mushroomrun6', 'mushroomrun7', 'mushroomrun8'];
+ self.animations.hit = ['mushroomhit1', 'mushroomhit2'];
+ self.animations.die = ['mushroomdie1', 'mushroomdie2', 'mushroomdie3', 'mushroomdie4'];
+ self.animations.attack = ['mushroomattack1', 'mushroomattack2', 'mushroomattack3', 'mushroomattack4', 'mushroomattack5'];
self.isAttacking = false;
- self.attackTimer = 0; // Countdown for attack animation
- self.attackDuration = self.config.attackDuration;
- self.attackCooldown = self.config.attackCooldown;
- self.attackCooldownTimer = 0; // Countdown for cooldown between attacks
+ self.attackTimer = 0; // Still used for attack duration/cooldown logic, not frame animation
+ self.attackDuration = 100;
+ self.attackCooldown = 180;
+ self.attackCooldownTimer = 0;
self.sporesReleased = false;
- self.originalSpeed = self.speed; // To restore after attacking
- self.platformOffset = 0; // For maintaining position during attack
+ } else if (self.type === 'eyeball') {
+ self.hitboxHeight = 90;
+ self.animations.fly = ['eyefly1', 'eyefly2', 'eyefly3', 'eyefly4', 'eyefly5', 'eyefly6', 'eyefly7', 'eyefly8'];
+ self.animations.hit = ['eyedie1', 'eyedie2']; // Eyeball hit animation uses first 2 die frames
+ self.animations.die = ['eyedie3', 'eyedie4', 'eyedie5']; // Eyeball die animation uses last 3 die frames
+ self.isFlying = true;
+ self.flyingHeight = 0; // Not directly used, dynamic
+ self.verticalSpeed = 2; // Initial vertical speed component for wave motion
+ self.maxVerticalSpeed = 4;
+ self.homingDelay = 80;
+ self.homingTimer = 0;
+ } else if (self.type === 'goblin') {
+ self.animations.run = ['goblinrun1', 'goblinrun2', 'goblinrun3', 'goblinrun4', 'goblinrun5', 'goblinrun6', 'goblinrun7', 'goblinrun8'];
+ self.animations.hit = ['goblinhit1'];
+ self.animations.die = ['goblindie1', 'goblindie2', 'goblindie3', 'goblindie4'];
+ } else if (self.type === 'skeleton') {
+ self.speed = 6;
+ self.lootModifier = 1.6;
+ self.animations.run = ['skeletonwalk1', 'skeletonwalk2', 'skeletonwalk3', 'skeletonwalk4'];
+ self.animations.hit = ['skeletonhit1', 'skeletonhit2'];
+ self.animations.die = ['skeletondie1', 'skeletondie2', 'skeletondie3', 'skeletondie4'];
+ self.animations["throw"] = ['skeletonthrow1', 'skeletonthrow2', 'skeletonthrow3', 'skeletonthrow4', 'skeletonthrow5', 'skeletonthrow6'];
+ self.isThrowing = false;
+ self.throwingTimer = 0; // Used for throw sequence timing, not frame animation
+ self.throwingDuration = 90;
+ self.throwingPauseTime = 15;
+ self.swordThrown = false; // Flag for skeleton sword throw
+ self.initialThrowMade = undefined; // For skeleton's first throw logic
+ self.enteredScreenTime = 0;
+ self.throwDelay = 60;
+ } else {
+ // Basic enemy: uses a single 'enemy' asset if no animations defined.
+ // This case should ideally not occur if types are well-defined.
+ // If it does, it will likely error out on animation calls.
+ // Consider adding a default 'idle' animation if 'basic' is a valid type.
+ var enemyGraphics = self.attachAsset('enemy', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ self.sprites.push(enemyGraphics); // Add to sprites array to avoid errors in hideAllSprites
}
- self.initSprites = function () {
- for (var animKey in self.config.animations) {
- var frameNames = self.config.animations[animKey];
- self.animationFrameMap[animKey] = [];
- for (var i = 0; i < frameNames.length; i++) {
- var sprite = self.attachAsset(frameNames[i], {
- anchorX: 0.5,
- anchorY: 0.5,
- alpha: 0 // Initially hide all
- });
- self.sprites.push(sprite); // Keep all sprites in one flat list
- self.animationFrameMap[animKey].push(sprite); // Store for easy access
+ self.originalSpeed = self.speed;
+ // --- Animation System Methods ---
+ self.hideAllSprites = function () {
+ for (var i = 0; i < self.sprites.length; i++) {
+ if (self.sprites[i]) self.sprites[i].alpha = 0;
+ }
+ };
+ self.initializeSprites = function () {
+ self.sprites = [];
+ self.animationOffsets = {};
+ var currentGlobalSpriteIndex = 0;
+ var isFirstAnimationCategory = true;
+ var animationCategoriesOrder = ['run', 'fly', 'hit', 'die', 'attack', 'throw'];
+ for (var i = 0; i < animationCategoriesOrder.length; i++) {
+ var animCategoryName = animationCategoriesOrder[i];
+ if (self.animations[animCategoryName] && self.animations[animCategoryName].length > 0) {
+ self.animationOffsets[animCategoryName] = currentGlobalSpriteIndex;
+ var animFrames = self.animations[animCategoryName];
+ for (var j = 0; j < animFrames.length; j++) {
+ var sprite = self.attachAsset(animFrames[j], {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ sprite.alpha = isFirstAnimationCategory && j === 0 ? 1 : 0;
+ self.sprites.push(sprite);
+ currentGlobalSpriteIndex++;
+ }
+ if (animFrames.length > 0) {
+ isFirstAnimationCategory = false;
+ }
}
}
};
- // Plays an animation, typically looping. Frame speed can be adjusted.
- self.playAnimation = function (animName, frameSpeedMultiplier) {
- frameSpeedMultiplier = frameSpeedMultiplier || 1;
- var animFrames = self.animationFrameMap[animName];
- if (!animFrames || animFrames.length === 0) return;
- if (self.currentAnimationName !== animName) {
- self.currentAnimationName = animName;
- self.currentFrameIndex = 0;
- self.animationTickCounter = 0;
+ self.setAnimation = function (animationType, loop, speed, forceRestart) {
+ if (!self.animations[animationType] || self.animations[animationType].length === 0) {
+ // console.error("Enemy: Animation not found or empty:", animationType, "for type", self.type);
+ return;
}
- // Hide all other sprites
- for (var i = 0; i < self.sprites.length; i++) {
- self.sprites[i].alpha = 0;
+ if (!forceRestart && self.currentAnimation.type === animationType) return;
+ self.currentAnimation.type = animationType;
+ self.currentAnimation.frames = self.animations[animationType];
+ self.currentAnimation.frameIndex = 0;
+ self.currentAnimation.counter = 0;
+ self.currentAnimation.loop = loop !== undefined ? loop : true;
+ self.currentAnimation.speed = speed || (self.type === 'skeleton' && animationType === 'run' ? self.animationSpeed * 0.8 : self.animationSpeed);
+ if (self.animationOffsets[animationType] !== undefined) {
+ self.currentAnimation.spriteOffset = self.animationOffsets[animationType];
+ } else {
+ // console.error("Enemy: Offset not found for animation:", animationType, "for type", self.type);
+ self.currentAnimation.type = null;
+ return;
}
- // Show current frame
- animFrames[self.currentFrameIndex].alpha = 1;
- // Advance frame
- self.animationTickCounter += self.animationSpeed * frameSpeedMultiplier * gameSpeedMultiplier;
- if (self.animationTickCounter >= 1) {
- self.animationTickCounter = 0;
- self.currentFrameIndex = (self.currentFrameIndex + 1) % animFrames.length;
+ self.hideAllSprites();
+ var firstSpriteOfNewAnim = self.sprites[self.currentAnimation.spriteOffset];
+ if (firstSpriteOfNewAnim) {
+ firstSpriteOfNewAnim.alpha = 1;
+ } else {
+ // console.error("Enemy: First sprite for animation " + animationType + " not found at offset " + self.currentAnimation.spriteOffset);
}
};
- // Plays an animation once. Returns true if the animation completed this tick.
- // Manages its own timer for frame progression.
- self.playAnimationOnce = function (animName, timerProperty, totalDuration, onCompleteCallback) {
- var animFrames = self.animationFrameMap[animName];
- if (!animFrames || animFrames.length === 0) {
- if (onCompleteCallback) onCompleteCallback();
- return true;
+ self.updateAnimation = function () {
+ var anim = self.currentAnimation;
+ if (!anim.type || !anim.frames || anim.frames.length === 0) {
+ return;
}
- if (self.currentAnimationName !== animName) {
- self.currentAnimationName = animName;
- // self[timerProperty] should be set before calling this for the first time
- // e.g., self.hitTimer = self.config.hitTimerDuration
+ anim.counter += anim.speed * gameSpeedMultiplier; // Animation speed affected by game speed
+ var oldFrameSpriteIndex = anim.spriteOffset + anim.frameIndex;
+ if (anim.counter >= 1) {
+ anim.counter = 0;
+ anim.frameIndex++;
+ if (anim.frameIndex >= anim.frames.length) {
+ if (anim.loop) {
+ anim.frameIndex = 0;
+ } else {
+ anim.frameIndex = anim.frames.length - 1;
+ if (self.onAnimationEnd) {
+ self.onAnimationEnd(anim.type);
+ }
+ // For non-looping, current frame will be shown, but no further updates to frameIndex
+ }
+ }
}
- // Calculate current frame based on timer progress
- var progress = 1 - self[timerProperty] / totalDuration;
- self.currentFrameIndex = Math.floor(progress * animFrames.length);
- self.currentFrameIndex = Math.min(self.currentFrameIndex, animFrames.length - 1); // Cap at last frame
- self.currentFrameIndex = Math.max(0, self.currentFrameIndex); // Ensure it's not negative
- for (var i = 0; i < self.sprites.length; i++) {
- self.sprites[i].alpha = 0;
+ // Hide all sprites then show the current one
+ // This is inefficient if done every frame. Only do on frame change.
+ // However, to ensure correctness if something else made a sprite visible:
+ self.hideAllSprites();
+ var currentFrameSpriteIndex = anim.spriteOffset + anim.frameIndex;
+ if (self.sprites[currentFrameSpriteIndex]) {
+ self.sprites[currentFrameSpriteIndex].alpha = 1;
+ } else {
+ // console.error("Enemy: Sprite for current frame not found: " + anim.type + ", frame " + anim.frameIndex);
}
- animFrames[self.currentFrameIndex].alpha = 1;
- // Timer is decremented by the calling state function
- if (self[timerProperty] <= 0) {
- if (onCompleteCallback) onCompleteCallback();
- return true; // Animation considered complete when timer runs out
- }
- return false; // Animation ongoing
};
- // Call initSprites after all properties are set up
- self.initSprites();
- // Set initial animation based on type
- if (self.type === 'eyeball') {
- self.playAnimation('fly');
- } else {
- self.playAnimation('run');
- }
- // Old init functions (initEyeballSprites, initGoblinSprites, etc.) are now replaced by self.initSprites()
- // Old self.hideAllSprites() is implicitly handled by playAnimation/playAnimationOnce,
- // and a general hide all at the start of the main update.
- // We'll add a new hideAllSprites for the main update loop's beginning.
- self.hideAllSprites = function () {
- for (var i = 0; i < self.sprites.length; i++) {
- if (self.sprites[i] && self.sprites[i].parent) {
- // Check if sprite still exists
- self.sprites[i].alpha = 0;
+ self.onAnimationEnd = function (endedAnimationType) {
+ if (self.isHit && endedAnimationType === 'hit') {
+ self.isHit = false;
+ if (self.hitType === 'attack') {
+ self.isDying = true;
+ self.dropLoot();
+ self.setAnimation('die', false, self.animationSpeed, true);
+ } else {
+ // Slide or non-lethal hit
+ var normalAnimName = self.type === 'eyeball' ? 'fly' : 'run';
+ var normalSpeed = self.type === 'skeleton' && normalAnimName === 'run' ? self.animationSpeed * 0.8 : self.animationSpeed;
+ self.setAnimation(normalAnimName, true, normalSpeed, true);
}
+ self.hitType = 'none';
+ } else if (self.isDying && endedAnimationType === 'die') {
+ self.isFadingOut = true;
+ self.fadeOutTimer = 30; // Frames for fade out
+ } else if (self.type === 'skeleton' && self.isThrowing && endedAnimationType === 'throw') {
+ self.isThrowing = false;
+ self.swordThrown = false;
+ self.speed = self.originalSpeed;
+ self.setAnimation('run', true, self.animationSpeed * 0.8, true);
+ } else if (self.type === 'mushroom' && self.isAttacking && endedAnimationType === 'attack') {
+ self.isAttacking = false;
+ self.speed = self.originalSpeed;
+ self.attackCooldownTimer = self.attackCooldown;
+ self.setAnimation('run', true, self.animationSpeed, true);
}
};
- self.updateEyeballDamageState = function () {
- if (self.isHit) {
- self.throwBackDistance += Math.abs(self.throwBackSpeed);
- self.x += self.throwBackSpeed;
- if (self.hitTimer > self.config.hitTimerDuration - 70) {
- // First 30 frames of 100
- self.y += self.velocityY;
- } else {
- self.y += self.velocityY;
- self.velocityY *= 0.8;
- }
- self.throwBackSpeed *= 0.95;
- self.playAnimationOnce('hit', 'hitTimer', self.config.hitTimerDuration);
- self.hitTimer--;
- if (self.hitTimer <= 0) {
- self.isHit = false;
- if (self.hitType === 'attack') {
- self.isDying = true;
- self.deathTimer = self.config.deathTimerDuration; // Use config
- self.currentAnimationName = null; // Reset for die animation
- self.dropLoot();
+ // --- Core Logic Methods ---
+ self.getBounds = function () {
+ return {
+ left: self.x - self.hitboxWidth / 2,
+ right: self.x + self.hitboxWidth / 2,
+ top: self.y - self.hitboxHeight / 2,
+ bottom: self.y + self.hitboxHeight / 2
+ };
+ };
+ self.checkPlatformCollision = function () {
+ var onAnyPlatform = false;
+ for (var i = 0; i < platforms.length; i++) {
+ var platform = platforms[i];
+ var leftEdge = platform.x - PLATFORM_HALF_WIDTH;
+ var rightEdge = platform.x + PLATFORM_HALF_WIDTH;
+ if (self.x >= leftEdge && self.x <= rightEdge) {
+ // Landed on top
+ if (self.velocityY >= 0 && self.y < platform.y - ENEMY_PLATFORM_OFFSET + 5 && self.y + self.velocityY >= platform.y - ENEMY_PLATFORM_OFFSET - 5) {
+ if (platform instanceof CrumblePlatform) platform.trigger();
+ onAnyPlatform = true;
+ self.currentPlatform = platform;
+ self.y = platform.y - ENEMY_PLATFORM_OFFSET;
+ self.isOnGround = true;
+ self.velocityY = 0;
+ return true;
}
- self.hitType = 'none';
- }
- } else if (self.isDying) {
- self.velocityY += self.config.gravity;
- self.y += self.velocityY;
- self.x -= PLATFORM_SPEED * 0.5 * gameSpeedMultiplier; // Slower horizontal drift when dying
- // Eyeball death animation is specific: 3 frames over 180 ticks
- var dieAnimFrames = self.animationFrameMap.die;
- if (dieAnimFrames && dieAnimFrames.length === 3) {
- for (var i = 0; i < self.sprites.length; i++) {
- self.sprites[i].alpha = 0;
- } // Hide all first
- if (self.deathTimer > self.config.deathTimerDuration * (2 / 3)) {
- // First 1/3 of time
- dieAnimFrames[0].alpha = 1;
- } else if (self.deathTimer > self.config.deathTimerDuration * (1 / 3)) {
- // Middle 1/3
- dieAnimFrames[1].alpha = 1;
- } else {
- // Last 1/3
- dieAnimFrames[2].alpha = 1;
+ // Already on platform and y matches (for cases where velocityY might be 0 but still on platform)
+ if (Math.abs(self.y - (platform.y - ENEMY_PLATFORM_OFFSET)) < 5) {
+ if (platform instanceof CrumblePlatform) platform.trigger();
+ onAnyPlatform = true;
+ self.currentPlatform = platform;
+ self.y = platform.y - ENEMY_PLATFORM_OFFSET;
+ self.isOnGround = true;
+ self.velocityY = 0;
+ return true;
}
}
- self.deathTimer--;
- if (self.deathTimer <= 0) {
- if (!self.isFadingOut) {
- self.isFadingOut = true;
- self.fadeOutTicks = 30; // Standard fade duration
- }
- }
- if (self.isFadingOut) {
- self.fadeOutTicks--;
- self.alpha = Math.max(0, self.fadeOutTicks / 30);
- if (self.alpha <= 0) self.destroy();
- }
}
+ if (!onAnyPlatform) {
+ self.isOnGround = false;
+ // self.currentPlatform = null; // Don't nullify if falling off current, handle in movement
+ }
+ return false;
};
- self.updateEyeballNormalState = function () {
- self.x -= self.speed * gameSpeedMultiplier;
- if (self.homingTimer >= self.homingDelay) {
- var deltaY = player.y - self.y;
- self.velocityY += deltaY > 0 ? 0.2 : -0.2;
- self.velocityY = Math.max(-self.maxVerticalSpeed, Math.min(self.maxVerticalSpeed, self.velocityY));
- } else {
- self.velocityY = Math.sin(self.homingTimer * 0.05) * 2; // Bobbing motion
- self.homingTimer++;
+ self.dropLoot = function () {
+ if (tutorialActive) return;
+ var lootMultiplier = player.lootMultiplier || 1.0;
+ var coinCount = Math.floor(GameUtils.randomRange(1, 6) * self.lootModifier * lootMultiplier);
+ var dropJewel = Math.random();
+ var jewelChance = 0.02 * self.lootModifier * lootMultiplier;
+ if (dropJewel < jewelChance) {
+ var diamond = new Coin('diamond');
+ diamond.x = self.x;
+ diamond.y = self.y;
+ diamond.velocityX = GameUtils.randomRange(-2, 2);
+ diamond.velocityY = -GameUtils.randomRange(6, 10);
+ game.addChild(diamond);
+ coins.push(diamond);
+ } else if (dropJewel < jewelChance * 2) {
+ var emerald = new Coin('emerald');
+ emerald.x = self.x;
+ emerald.y = self.y;
+ emerald.velocityX = GameUtils.randomRange(-2, 2);
+ emerald.velocityY = -GameUtils.randomRange(6, 10);
+ game.addChild(emerald);
+ coins.push(emerald);
+ } else if (dropJewel < jewelChance * 3) {
+ var ruby = new Coin('ruby');
+ ruby.x = self.x;
+ ruby.y = self.y;
+ ruby.velocityX = GameUtils.randomRange(-2, 2);
+ ruby.velocityY = -GameUtils.randomRange(6, 10);
+ game.addChild(ruby);
+ coins.push(ruby);
}
- self.y += self.velocityY;
- self.playAnimation('fly');
+ for (var i = 0; i < coinCount; i++) {
+ var coin = new Coin();
+ coin.x = self.x;
+ coin.y = self.y;
+ coin.velocityX = GameUtils.randomRange(-2, 2);
+ coin.velocityY = -GameUtils.randomRange(6, 10);
+ game.addChild(coin);
+ coins.push(coin);
+ }
};
- self.handlePlatformAndGravityDuringHitOrDie = function () {
+ // --- State Update Handlers ---
+ self.updateGroundMovement = function () {
+ self.x -= self.speed * gameSpeedMultiplier;
+ if (!self.isOnGround) {
+ var gravity = self.type === 'skeleton' || self.type === 'goblin' ? 0.7 : 0.75; // mushroom has slightly higher gravity
+ self.velocityY += gravity;
+ self.y += self.velocityY;
+ self.checkPlatformCollision();
+ }
if (self.currentPlatform) {
var stillOnPlatform = self.x >= self.currentPlatform.x - PLATFORM_HALF_WIDTH && self.x <= self.currentPlatform.x + PLATFORM_HALF_WIDTH;
if (!stillOnPlatform) {
var foundAnotherPlatform = false;
for (var i = 0; i < platforms.length; i++) {
var otherPlatform = platforms[i];
if (otherPlatform === self.currentPlatform) continue;
- if (self.x >= otherPlatform.x - PLATFORM_HALF_WIDTH && self.x <= otherPlatform.x + PLATFORM_HALF_WIDTH && Math.abs(self.y - (otherPlatform.y - (self.config.platformOffsetY || ENEMY_PLATFORM_OFFSET))) < 10) {
- // Increased tolerance
+ if (self.x >= otherPlatform.x - PLATFORM_HALF_WIDTH && self.x <= otherPlatform.x + PLATFORM_HALF_WIDTH && Math.abs(self.y - (otherPlatform.y - ENEMY_PLATFORM_OFFSET)) < 5) {
self.currentPlatform = otherPlatform;
- self.y = otherPlatform.y - (self.config.platformOffsetY || ENEMY_PLATFORM_OFFSET);
- self.isOnGround = true;
+ self.y = otherPlatform.y - ENEMY_PLATFORM_OFFSET; // Snap to new platform
+ self.isOnGround = true; // Ensure is on ground
self.velocityY = 0;
foundAnotherPlatform = true;
break;
}
@@ -652,523 +735,318 @@
if (self.velocityY === 0) self.velocityY = 0.1; // Start falling
}
}
}
- if (!self.isOnGround) {
- self.velocityY += self.config.gravity;
- self.y += self.velocityY;
- self.checkPlatformCollision(); // This will set isOnGround if collision occurs
- }
};
- self.updateGroundEnemyHitState = function () {
- self.x += self.throwBackSpeed;
- self.throwBackDistance += Math.abs(self.throwBackSpeed);
- self.throwBackSpeed *= 0.95;
- self.handlePlatformAndGravityDuringHitOrDie();
- self.playAnimationOnce('hit', 'hitTimer', self.config.hitTimerDuration);
- self.hitTimer--; // Manually decrement timer
- if (self.hitTimer <= 0) {
- self.isHit = false;
- if (self.hitType === 'attack') {
- self.isDying = true;
- self.deathTimer = self.config.deathTimerDuration;
- self.currentAnimationName = null; // Reset for die animation
- self.dropLoot();
- }
- self.hitType = 'none';
- }
- };
- self.updateGroundEnemyDyingState = function () {
- self.x += self.throwBackSpeed * 0.5; // Slower throwback
- self.throwBackSpeed *= 0.95;
- self.handlePlatformAndGravityDuringHitOrDie();
- // Match platform speed after a certain point in animation
- var dieAnimFrames = self.animationFrameMap.die;
- if (dieAnimFrames && self.currentFrameIndex >= dieAnimFrames.length / 2) {
- if (self.isOnGround && self.currentPlatform) {
- // Only if on a platform
- self.x = self.currentPlatform.x + (self.x - self.currentPlatform.x) - PLATFORM_SPEED * gameSpeedMultiplier;
- } else {
- self.x -= PLATFORM_SPEED * gameSpeedMultiplier * 0.5; // Slower drift if falling
- }
- }
- self.playAnimationOnce('die', 'deathTimer', self.config.deathTimerDuration);
- self.deathTimer--; // Manually decrement
- if (self.deathTimer <= 0) {
- if (!self.isFadingOut) {
- self.isFadingOut = true;
- self.fadeOutTicks = 30; // Fade over 30 frames
- }
- }
- if (self.isFadingOut) {
- self.fadeOutTicks--;
- self.alpha = Math.max(0, self.fadeOutTicks / 30);
- if (self.alpha <= 0) self.destroy();
- }
- };
- // --- Goblin Specific ---
- self.updateGoblinHitState = function () {
- self.updateGroundEnemyHitState();
- };
- self.updateGoblinDyingState = function () {
- self.updateGroundEnemyDyingState();
- };
- self.updateGoblinNormalState = function () {
+ self.updateEyeballMovement = function () {
self.x -= self.speed * gameSpeedMultiplier;
- if (!self.isOnGround) {
- // Apply gravity if in air
- self.velocityY += self.config.gravity;
- self.y += self.velocityY;
- self.checkPlatformCollision();
- } else if (self.currentPlatform) {
- // Check if fell off current platform
- var stillOnPlatform = self.x >= self.currentPlatform.x - PLATFORM_HALF_WIDTH && self.x <= self.currentPlatform.x + PLATFORM_HALF_WIDTH;
- if (!stillOnPlatform) {
- // Fell off
- self.isOnGround = false;
- self.currentPlatform = null;
- if (self.velocityY === 0) self.velocityY = 0.1; // Start falling
- }
- }
- self.playAnimation('run');
- };
- // --- Skeleton Specific ---
- self.updateSkeletonHitState = function () {
- self.updateGroundEnemyHitState();
- };
- self.updateSkeletonDyingState = function () {
- self.updateGroundEnemyDyingState();
- };
- self.updateSkeletonNormalState = function () {
- if (self.initialThrowMade === undefined) {
- self.initialThrowMade = false;
- self.enteredScreenTime = LK.ticks;
- self.throwDelay = 60;
- }
- if (!self.initialThrowMade && self.x < GAME_WIDTH + 300 && self.isOnGround && !self.isThrowing) {
- if (LK.ticks - self.enteredScreenTime > self.throwDelay) {
- self.isThrowing = true;
- self.throwingTimer = self.throwingDuration;
- self.swordThrown = false;
- // self.originalSpeed = self.speed; // Already set in constructor
- self.initialThrowMade = true;
- self.currentAnimationName = null; // Force switch to throw animation
- return; // Transition to throwing state in next update tick
- }
- }
- self.x -= self.speed * gameSpeedMultiplier;
- if (!self.isOnGround) {
- self.velocityY += self.config.gravity;
- self.y += self.velocityY;
- self.checkPlatformCollision();
- } else if (self.currentPlatform) {
- var stillOnPlatform = self.x >= self.currentPlatform.x - PLATFORM_HALF_WIDTH && self.x <= self.currentPlatform.x + PLATFORM_HALF_WIDTH;
- if (!stillOnPlatform) {
- self.isOnGround = false;
- self.currentPlatform = null;
- if (self.velocityY === 0) self.velocityY = 0.1;
- }
- }
- self.playAnimation('run');
- };
- self.updateSkeletonThrowingState = function () {
- if (self.currentPlatform) {
- // Ensure skeleton stays on platform while throwing
- self.x = self.currentPlatform.x + (self.x - self.currentPlatform.x) - PLATFORM_SPEED * gameSpeedMultiplier;
+ if (self.homingTimer >= self.homingDelay) {
+ var deltaY = player.y - self.y;
+ self.velocityY += deltaY > 0 ? 0.2 : -0.2;
+ self.velocityY = Math.max(-self.maxVerticalSpeed, Math.min(self.maxVerticalSpeed, self.velocityY));
} else {
- // If somehow not on a platform, just move with global speed
- self.x -= PLATFORM_SPEED * gameSpeedMultiplier;
+ self.velocityY = Math.sin(self.homingTimer * 0.05) * self.verticalSpeed;
+ self.homingTimer++;
}
- var animDone = self.playAnimationOnce('throw', 'throwingTimer', self.throwingDuration, function () {
- self.isThrowing = false;
- self.speed = self.originalSpeed;
- self.currentAnimationName = null; // Go back to run
- });
- self.throwingTimer--;
- // Throw sword logic (based on original timer logic, map to animation progress)
- // Example: throw at a specific frame or time point
- var throwProgress = 1 - self.throwingTimer / self.throwingDuration;
- if (throwProgress >= 0.2 && throwProgress < 0.25 && !self.swordThrown) {
- // Corresponds to old (timer <= duration*0.8)
+ self.y += self.velocityY;
+ self.setAnimation('fly', true, self.animationSpeed);
+ };
+ self.updateSkeletonThrowing = function () {
+ self.x -= PLATFORM_SPEED * gameSpeedMultiplier; // Move with platform
+ self.setAnimation('throw', false, self.animationSpeed * 0.8); // Ensure throw animation is playing
+ var currentFrame = self.currentAnimation.frameIndex; // 0-indexed
+ var totalFrames = self.animations["throw"].length;
+ // Throw sword at a specific frame (e.g., frame 4 of 6, which is index 3)
+ // Original logic: (timer <= duration * 0.8) && (timer > duration * 0.8 - 2)
+ // New logic: Check frame index. If throw animation has 6 frames, sword thrown at frame 4 (index 3)
+ if (currentFrame === 3 && !self.swordThrown) {
+ // Assuming 6 frames, index 3 is 4th frame.
self.swordThrown = true;
var sword = new SkeletonSword();
sword.x = self.x - 100;
sword.y = self.y - 100;
sword.constantVelocity = 8;
game.addChild(sword);
skeletonSwords.push(sword);
}
+ // onAnimationEnd for 'throw' will handle transition back to run
};
- // --- Mushroom Specific ---
- self.updateMushroomHitState = function () {
- self.updateGroundEnemyHitState();
- };
- self.updateMushroomDyingState = function () {
- self.updateGroundEnemyDyingState();
- };
- self.updateMushroomNormalState = function () {
- if (self.checkMushroomAttack()) {
- // This sets isAttacking = true
- self.currentAnimationName = null; // Force switch to attack animation
- return;
- }
- if (self.attackCooldownTimer > 0) {
- self.attackCooldownTimer--;
- }
- self.x -= self.speed * gameSpeedMultiplier;
- if (!self.isOnGround) {
- self.velocityY += self.config.gravity;
- self.y += self.velocityY;
- self.checkPlatformCollision();
- } else if (self.currentPlatform) {
- var stillOnPlatform = self.x >= self.currentPlatform.x - PLATFORM_HALF_WIDTH && self.x <= self.currentPlatform.x + PLATFORM_HALF_WIDTH;
- if (!stillOnPlatform) {
- self.isOnGround = false;
- self.currentPlatform = null;
- if (self.velocityY === 0) self.velocityY = 0.1;
- }
- }
- self.playAnimation('run');
- };
self.checkMushroomAttack = function () {
- // Largely same logic
- if (self.isHit || self.isDying || self.isAttacking || self.attackCooldownTimer > 0) {
+ // Renamed from updateMushroomAttack to indicate it's a check
+ if (self.type !== 'mushroom' || self.isHit || self.isDying || self.isAttacking || self.attackCooldownTimer > 0) {
return false;
}
var distanceX = Math.abs(player.x - self.x);
var distanceY = Math.abs(player.y - self.y);
if (distanceX < 800 && distanceY < 700 && self.x > player.x) {
self.platformOffset = self.currentPlatform ? self.x - self.currentPlatform.x : 0;
self.isAttacking = true;
- self.attackTimer = self.attackDuration;
+ self.attackTimer = self.attackDuration; // Used for overall duration, not frame animation
self.sporesReleased = false;
- // self.originalSpeed = self.speed; // Already set in constructor
+ self.originalSpeed = self.speed;
+ self.speed = 0; // Stop moving during attack
+ self.setAnimation('attack', false, self.animationSpeed, true);
+ LK.getSound('mushroomhiss').play(); // Play sound at start of attack
return true;
}
return false;
};
- self.updateMushroomAttackState = function () {
- var animDone = self.playAnimationOnce('attack', 'attackTimer', self.attackDuration, function () {
- self.isAttacking = false;
- self.speed = self.originalSpeed;
- self.attackCooldownTimer = self.attackCooldown;
- self.currentAnimationName = null; // Go back to run
- });
- self.attackTimer--;
- // Spore release logic (based on animation progress)
- var attackProgress = 1 - self.attackTimer / self.attackDuration;
- if (attackProgress >= 0.5 && attackProgress < 0.6 && !self.sporesReleased) {
- // Approx frame 3 of 5
- self.releaseSpores();
- self.sporesReleased = true;
- }
- // Play hiss sound when attack animation starts (e.g. first frame)
- if (self.attackTimer === self.attackDuration - 1 && self.currentFrameIndex === 0) {
- LK.getSound('mushroomhiss').play();
- }
+ self.performMushroomAttackLogic = function () {
+ // New function for attack execution
if (self.currentPlatform) {
- self.x = self.currentPlatform.x + self.platformOffset;
+ self.x = self.currentPlatform.x + (self.platformOffset || 0);
} else {
- self.x -= PLATFORM_SPEED * gameSpeedMultiplier;
+ self.x -= PLATFORM_SPEED * gameSpeedMultiplier; // Should not happen if attack starts on platform
}
+ // Release spores at frame 3 (index 2) of attack animation
+ if (self.currentAnimation.type === 'attack' && self.currentAnimation.frameIndex === 2 && !self.sporesReleased) {
+ self.releaseSpores();
+ self.sporesReleased = true;
+ }
+ // onAnimationEnd for 'attack' will handle transition back to run
};
- // Function to release spores in the specified pattern
self.releaseSpores = function () {
- // Create 5 spores in pattern described
var directions = [{
x: 1,
y: 0
- },
- // Right
- {
+ }, {
x: -1,
y: 0
- },
- // Left
- {
+ }, {
x: 0,
y: -1
- },
- // Up
- {
+ }, {
x: 0.7,
y: -0.7
- },
- // Up-right diagonal
- {
+ }, {
x: -0.7,
y: -0.7
- } // Up-left diagonal
- ];
- // Calculate an offset position IN FRONT of the mushroom
- // This compensates for the platform movement and animation delay
- var offsetX = self.x; // Add a significant forward offset
- var offsetY = self.y - 50; // Slightly above as before
- // Play mushroom burst sound effect
+ }];
+ var offsetX = self.x;
+ var offsetY = self.y - 50;
LK.getSound('mushroomburst').play();
for (var i = 0; i < directions.length; i++) {
var spore = new Spore();
- // Position spores at the offset position
spore.x = offsetX;
spore.y = offsetY;
- // Don't add PLATFORM_SPEED to the velocityX, as that's causing over-compensation
- spore.velocityX = directions[i].x * spore.speed;
- spore.velocityY = directions[i].y * spore.speed;
- // Add to game
+ spore.velocityX = directions[i].x * spore.initialSpeed; // Use initialSpeed
+ spore.velocityY = directions[i].y * spore.initialSpeed;
game.addChild(spore);
- // Add to a global array for tracking if needed
- if (!window.spores) {
- window.spores = [];
- }
+ if (!window.spores) window.spores = [];
window.spores.push(spore);
}
};
- // Check platform collision
- self.checkPlatformCollision = function () {
- var onAnyPlatform = false;
- for (var i = 0; i < platforms.length; i++) {
- var platform = platforms[i];
- var leftEdge = platform.x - PLATFORM_HALF_WIDTH;
- var rightEdge = platform.x + PLATFORM_HALF_WIDTH;
- if (self.x >= leftEdge && self.x <= rightEdge) {
- if (Math.abs(self.y - (platform.y - ENEMY_PLATFORM_OFFSET)) < 5) {
- // Add this check:
- if (platform instanceof CrumblePlatform) {
- platform.trigger();
+ self.updateHitStateInternal = function () {
+ self.x += self.throwBackSpeed;
+ self.throwBackDistance += Math.abs(self.throwBackSpeed); // Not strictly used but kept for consistency
+ self.throwBackSpeed *= 0.95;
+ if (self.type === 'eyeball') {
+ self.y += self.velocityY; // Vertical trajectory applied in hit()
+ self.velocityY *= 0.9; // Dampen vertical for eyeball
+ } else {
+ // Ground enemies
+ if (self.currentPlatform) {
+ var stillOnPlatform = self.x >= self.currentPlatform.x - PLATFORM_HALF_WIDTH && self.x <= self.currentPlatform.x + PLATFORM_HALF_WIDTH;
+ if (!stillOnPlatform) {
+ /* ... logic to find new platform or fall ... */
+ var foundAnotherPlatform = false;
+ for (var i = 0; i < platforms.length; i++) {
+ var otherPlatform = platforms[i];
+ if (otherPlatform === self.currentPlatform) continue;
+ if (self.x >= otherPlatform.x - PLATFORM_HALF_WIDTH && self.x <= otherPlatform.x + PLATFORM_HALF_WIDTH && Math.abs(self.y - (otherPlatform.y - ENEMY_PLATFORM_OFFSET)) < 5) {
+ self.currentPlatform = otherPlatform;
+ self.y = otherPlatform.y - ENEMY_PLATFORM_OFFSET;
+ self.isOnGround = true;
+ self.velocityY = 0;
+ foundAnotherPlatform = true;
+ break;
+ }
}
- // Rest of the collision code remains the same
- // Add this check:
- if (platform instanceof CrumblePlatform) {
- platform.trigger();
+ if (!foundAnotherPlatform) {
+ self.isOnGround = false;
+ self.currentPlatform = null;
+ if (self.velocityY === 0) self.velocityY = 0.1;
}
- // Rest of the collision code remains the same
- onAnyPlatform = true;
- self.currentPlatform = platform;
- self.y = platform.y - ENEMY_PLATFORM_OFFSET;
- self.isOnGround = true;
- self.velocityY = 0;
- return true;
}
- if (self.velocityY > 0 && self.y < platform.y - ENEMY_PLATFORM_OFFSET && self.y + self.velocityY >= platform.y - ENEMY_PLATFORM_OFFSET) {
- // Add this check here too:
- if (platform instanceof CrumblePlatform) {
- platform.trigger();
+ }
+ if (!self.isOnGround) {
+ var gravity = self.type === 'skeleton' ? 0.7 : 0.75;
+ self.velocityY += gravity;
+ self.y += self.velocityY;
+ self.checkPlatformCollision();
+ }
+ }
+ // Animation transition handled by onAnimationEnd('hit')
+ };
+ self.updateDyingStateInternal = function () {
+ if (self.type === 'eyeball') {
+ self.velocityY += 0.5; // Gravity for eyeball
+ self.y += self.velocityY;
+ self.x -= PLATFORM_SPEED * 0.5 * gameSpeedMultiplier; // Eyeball drift
+ } else {
+ // Ground enemies
+ self.x += self.throwBackSpeed;
+ self.throwBackSpeed *= 0.95;
+ if (self.currentPlatform) {
+ /* ... platform boundary check ... */
+ var stillOnPlatform = self.x >= self.currentPlatform.x - PLATFORM_HALF_WIDTH && self.x <= self.currentPlatform.x + PLATFORM_HALF_WIDTH;
+ if (!stillOnPlatform) {
+ var foundAnotherPlatform = false;
+ for (var i = 0; i < platforms.length; i++) {
+ var otherPlatform = platforms[i];
+ if (otherPlatform === self.currentPlatform) continue;
+ if (self.x >= otherPlatform.x - PLATFORM_HALF_WIDTH && self.x <= otherPlatform.x + PLATFORM_HALF_WIDTH && Math.abs(self.y - (otherPlatform.y - ENEMY_PLATFORM_OFFSET)) < 5) {
+ self.currentPlatform = otherPlatform;
+ self.y = otherPlatform.y - ENEMY_PLATFORM_OFFSET;
+ self.isOnGround = true;
+ self.velocityY = 0;
+ foundAnotherPlatform = true;
+ break;
+ }
}
- // Rest of the collision code remains the same
- // Add this check here too:
- if (platform instanceof CrumblePlatform) {
- platform.trigger();
+ if (!foundAnotherPlatform) {
+ self.isOnGround = false;
+ self.currentPlatform = null;
+ if (self.velocityY === 0) self.velocityY = 0.1;
}
- // Rest of the collision code remains the same
- self.y = platform.y - ENEMY_PLATFORM_OFFSET;
- self.velocityY = 0;
- self.isOnGround = true;
- self.currentPlatform = platform;
- return true;
}
}
- }
- if (!onAnyPlatform) {
- self.isOnGround = false;
- self.currentPlatform = null;
- }
- return false;
- };
- // Get collision bounds
- self.getBounds = function () {
- return {
- left: self.x - self.hitboxWidth / 2,
- right: self.x + self.hitboxWidth / 2,
- top: self.y - self.hitboxHeight / 2,
- bottom: self.y + self.hitboxHeight / 2
- };
- };
- self.slideHit = function () {
- if (!self.isHit && !self.isDying) {
- self.isHit = true;
- // Check if player has slide damage upgrade
- if (player.slideDamage && player.slideDamageTimer <= 0) {
- // If upgraded, treat as regular attack
- self.hitType = 'attack';
- player.slideDamageTimer = player.slideDamageCooldown;
- } else {
- // Otherwise just knockback
- self.hitType = 'slide';
+ if (!self.isOnGround) {
+ var gravity = self.type === 'skeleton' ? 0.7 : 0.7;
+ self.velocityY += gravity;
+ self.y += self.velocityY;
+ self.checkPlatformCollision();
}
- self.throwBackSpeed = 25;
- self.throwBackDistance = 0;
- self.hitTimer = 30;
- // Add vertical trajectory for eyeball hits based on Y position comparison with player
- var verticalTrajectory;
- if (self.type === 'eyeball') {
- // (existing eyeball trajectory calculation)
- var yDifference = self.y - player.y;
- var heightThreshold = 100; // Adjust this value to control the size of the middle zone
- if (yDifference < -heightThreshold) {
- // Eyeball is well above player, push upward strongly
- verticalTrajectory = -20;
- } else if (yDifference > heightThreshold) {
- // Eyeball is well below player, push downward strongly
- verticalTrajectory = 14;
- } else {
- // Eyeball is in middle zone, small upward push
- verticalTrajectory = -2;
- }
- self.velocityY = verticalTrajectory;
+ // Match platform speed after half of die animation
+ var dieAnimFrames = self.animations.die ? self.animations.die.length : 0;
+ if (dieAnimFrames > 0 && self.currentAnimation.frameIndex >= Math.floor(dieAnimFrames / 2)) {
+ self.x -= PLATFORM_SPEED * gameSpeedMultiplier;
}
- // Play the appropriate hit sound based on enemy type
- if (self.type === 'eyeball') {
- LK.getSound('eyeballhit').play();
- } else if (self.type === 'skeleton') {
- LK.getSound('skeletonhit').play();
- } else if (self.type === 'mushroom' && player.slideDamage) {
- LK.getSound('mushroomdie').play();
- } else if (self.type === 'mushroom') {
- LK.getSound('mushroombounce').play();
- } else {
- LK.getSound('enemyhit').play();
+ }
+ // Fade out handled by onAnimationEnd('die') -> isFadingOut flag
+ if (self.isFadingOut) {
+ self.alpha -= 1 / self.fadeOutTimer;
+ if (self.alpha <= 0) {
+ self.destroy();
}
- // Add particle effect
- var particleOffset = self.type === 'eyeball' ? 175 : 250;
- particleSystem.emitFromHit(self.x + particleOffset, self.y, player.x, verticalTrajectory, self.type);
}
};
- // Hit handling
+ // --- Hit Handlers ---
self.hit = function () {
- if (!self.isHit && !self.isDying) {
- self.isHit = true;
- self.hitType = 'attack'; // Specify this is a regular attack hit
- self.throwBackSpeed = 25;
- self.throwBackDistance = 0;
- self.hitTimer = 35;
- // Calculate vertical trajectory for eyeball hits based on Y position comparison with player
- var verticalTrajectory;
- if (self.type === 'eyeball') {
- var yDifference = self.y - player.y;
- var heightThreshold = 100; // Adjust this value to control the size of the middle zone
- if (yDifference < -heightThreshold) {
- // Eyeball is well above player, push upward strongly
- verticalTrajectory = -20;
- } else if (yDifference > heightThreshold) {
- // Eyeball is well below player, push downward strongly
- verticalTrajectory = 14;
- } else {
- // Eyeball is in middle zone, small upward push
- verticalTrajectory = -2;
- }
- // Immediately apply the vertical trajectory with stronger values
- self.velocityY = verticalTrajectory; // Set the velocity for the eyeball
- } else {
- self.velocityY = 0; // Reset vertical velocity for new hit for non-eyeball enemies
- }
- // Play appropriate hit sound based on enemy type
- if (self.type === 'mushroom') {
- LK.getSound('mushroomdie').play();
- }
- // Add particle effect
- var particleOffset = self.type === 'eyeball' ? 175 : 250;
- particleSystem.emitFromHit(self.x + particleOffset, self.y, player.x, verticalTrajectory, self.type);
+ if (self.isHit || self.isDying) return;
+ self.isHit = true;
+ self.hitType = 'attack';
+ self.throwBackSpeed = 25;
+ self.throwBackDistance = 0; // Reset
+ var verticalTrajectory = 0;
+ if (self.type === 'eyeball') {
+ var yDifference = self.y - player.y;
+ var heightThreshold = 100;
+ if (yDifference < -heightThreshold) verticalTrajectory = -20;else if (yDifference > heightThreshold) verticalTrajectory = 14;else verticalTrajectory = -2;
+ self.velocityY = verticalTrajectory;
+ } else {
+ self.velocityY = 0;
}
+ var soundToPlay = 'enemyhit';
+ if (self.type === 'eyeball') soundToPlay = 'eyeballhit';else if (self.type === 'skeleton') soundToPlay = 'skeletonhit';else if (self.type === 'mushroom') soundToPlay = 'mushroomdie';
+ LK.getSound(soundToPlay).play();
+ particleSystem.emitFromHit(self.x + (self.type === 'eyeball' ? 175 : 250), self.y, player.x, verticalTrajectory, self.type);
+ self.setAnimation('hit', false, self.animationSpeed * 1.5, true); // Play hit animation faster
};
- self.dropLoot = function () {
- // Don't drop collectibles during tutorial
- if (tutorialActive) {
- return;
+ self.slideHit = function () {
+ if (self.isHit || self.isDying) return;
+ self.isHit = true;
+ self.hitType = player.slideDamage && player.slideDamageTimer <= 0 ? 'attack' : 'slide';
+ if (self.hitType === 'attack' && player.slideDamageTimer <= 0) player.slideDamageTimer = player.slideDamageCooldown;
+ self.throwBackSpeed = 25;
+ self.throwBackDistance = 0;
+ var verticalTrajectory = 0;
+ if (self.type === 'eyeball') {
+ var yDifference = self.y - player.y;
+ var heightThreshold = 100;
+ if (yDifference < -heightThreshold) verticalTrajectory = -20;else if (yDifference > heightThreshold) verticalTrajectory = 14;else verticalTrajectory = -2;
+ self.velocityY = verticalTrajectory;
}
- // Apply loot multiplier from Fortune Finder upgrade
- var lootMultiplier = player.lootMultiplier || 1.0;
- // Determine number of coins to drop (1-5) adjusted by loot modifier and player's upgrade
- var coinCount = Math.floor(GameUtils.randomRange(1, 6) * self.lootModifier * lootMultiplier);
- // Small chance for jewels - increased by loot modifier AND player upgrade
- var dropJewel = Math.random();
- var jewelChance = 0.02 * self.lootModifier * lootMultiplier; // Base 2% chance multiplied by modifiers
- if (dropJewel < jewelChance) {
- // Diamond (rarest)
- var diamond = new Coin('diamond');
- diamond.x = self.x;
- diamond.y = self.y;
- diamond.velocityX = GameUtils.randomRange(-2, 2); // Reduced horizontal velocity
- diamond.velocityY = -GameUtils.randomRange(6, 10); // Reduced vertical velocity
- game.addChild(diamond);
- coins.push(diamond);
- } else if (dropJewel < jewelChance * 2) {
- // Emerald
- var emerald = new Coin('emerald');
- emerald.x = self.x;
- emerald.y = self.y;
- emerald.velocityX = GameUtils.randomRange(-2, 2); // Reduced horizontal velocity
- emerald.velocityY = -GameUtils.randomRange(6, 10); // Reduced vertical velocity
- game.addChild(emerald);
- coins.push(emerald);
- } else if (dropJewel < jewelChance * 3) {
- // Ruby
- var ruby = new Coin('ruby');
- ruby.x = self.x;
- ruby.y = self.y;
- ruby.velocityX = GameUtils.randomRange(-2, 2); // Reduced horizontal velocity
- ruby.velocityY = -GameUtils.randomRange(6, 10); // Reduced vertical velocity
- game.addChild(ruby);
- coins.push(ruby);
+ var soundToPlay = 'enemyhit';
+ if (self.type === 'eyeball') soundToPlay = 'eyeballhit';else if (self.type === 'skeleton') soundToPlay = 'skeletonhit';else if (self.type === 'mushroom') {
+ soundToPlay = player.slideDamage && self.hitType === 'attack' ? 'mushroomdie' : 'mushroombounce';
}
- // Drop regular coins
- for (var i = 0; i < coinCount; i++) {
- var coin = new Coin();
- coin.x = self.x;
- coin.y = self.y;
- coin.velocityX = GameUtils.randomRange(-2, 2); // Reduced horizontal velocity
- coin.velocityY = -GameUtils.randomRange(6, 10); // Reduced vertical velocity
- game.addChild(coin);
- coins.push(coin);
- }
+ LK.getSound(soundToPlay).play();
+ particleSystem.emitFromHit(self.x + (self.type === 'eyeball' ? 175 : 250), self.y, player.x, verticalTrajectory, self.type);
+ self.setAnimation('hit', false, self.animationSpeed * 1.5, true);
};
- self.isDeathAnimationComplete = function () {
- return self.isDying && self.deathTimer <= 10; // Check if we're near the end of death animation
- };
- // Main update method
+ // --- Main Update Method ---
self.update = function () {
- self.hideAllSprites(); // Ensure clean slate for current frame's animation
- var stateUpdateMethodName;
if (self.isDying) {
- stateUpdateMethodName = self.config.stateUpdateMethods.dying;
+ self.updateDyingStateInternal();
} else if (self.isHit) {
- stateUpdateMethodName = self.config.stateUpdateMethods.hit;
- } else if (self.type === 'skeleton' && self.isThrowing) {
- stateUpdateMethodName = self.config.stateUpdateMethods.throwing;
- } else if (self.type === 'mushroom' && self.isAttacking) {
- stateUpdateMethodName = self.config.stateUpdateMethods.attacking;
+ self.updateHitStateInternal();
} else {
- stateUpdateMethodName = self.config.stateUpdateMethods.normal;
- }
- if (stateUpdateMethodName && typeof self[stateUpdateMethodName] === 'function') {
- self[stateUpdateMethodName]();
- } else {
- // Fallback or error for undefined state method
- console.log("Error: Missing state update method for " + self.type + " in current state.");
- if (self.config.stateUpdateMethods.normal && typeof self[self.config.stateUpdateMethods.normal] === 'function') {
- self[self.config.stateUpdateMethods.normal](); // Attempt normal update
+ // Normal state
+ if (self.type === 'eyeball') {
+ self.updateEyeballMovement();
+ } else if (self.type === 'skeleton') {
+ if (self.isThrowing) {
+ self.updateSkeletonThrowing();
+ } else {
+ self.updateGroundMovement();
+ // Skeleton specific pre-movement logic for initial throw
+ if (self.initialThrowMade === undefined) {
+ self.initialThrowMade = false;
+ self.enteredScreenTime = LK.ticks;
+ }
+ if (!self.isThrowing && !self.initialThrowMade && self.x < GAME_WIDTH + 300 && self.isOnGround) {
+ if (LK.ticks - self.enteredScreenTime > self.throwDelay) {
+ self.isThrowing = true;
+ self.swordThrown = false;
+ self.throwingTimer = self.throwingDuration;
+ self.originalSpeed = self.speed;
+ self.speed = 0; // Stop during throw setup
+ self.initialThrowMade = true;
+ self.setAnimation('throw', false, self.animationSpeed * 0.8, true); // Switch to throw animation
+ }
+ }
+ if (!self.isThrowing) self.setAnimation('run', true, self.animationSpeed * 0.8);
+ }
+ } else if (self.type === 'mushroom') {
+ if (self.checkMushroomAttack()) {
+ // checkMushroomAttack now sets isAttacking and animation
+ } else {
+ if (self.attackCooldownTimer > 0) self.attackCooldownTimer--;
+ self.updateGroundMovement();
+ self.setAnimation('run', true);
+ }
+ if (self.isAttacking) {
+ //If attack was initiated by checkMushroomAttack
+ self.performMushroomAttackLogic(); // Execute attack logic
+ }
+ } else {
+ // Goblin or other ground enemies
+ self.updateGroundMovement();
+ self.setAnimation('run', true);
}
}
- // Common physics application (gravity for non-flying ground enemies not in special states that manage Y)
- // Eyeballs manage their Y in their normal/damage states.
- // Ground enemies in hit/dying states use handlePlatformAndGravityDuringHitOrDie.
- // Normal state ground enemies also manage their gravity/platform interaction.
- // This block is more of a safety net for falling if not handled by specific states.
- if (!self.isDying && !self.isOnGround && !self.config.isFlying && !self.isHit) {
- // Ensure gravity is only applied if not handled by a more specific state that controls Y.
- // Most normal states for ground enemies already call checkPlatformCollision after applying gravity.
- // This might be redundant if all normal states correctly handle their Y movement.
- }
- // Destroy if off screen (increased buffer)
- if (self.x < -200 || self.x > GAME_WIDTH + 200 || self.y < -200 || self.y > GAME_HEIGHT + 200) {
+ self.updateAnimation();
+ if (self.x < -GAME_WIDTH / 2 - 100 || self.y > GAME_HEIGHT + 200) {
self.destroy();
}
};
- // The old individual init...Sprites() calls are removed.
- // self.initSprites() is called earlier in the constructor.
- // The initial animation (e.g., 'run' or 'fly') is also set in the constructor.
- // The 'basic' enemy type with 'enemy' asset is not part of ENEMY_CONFIGS;
- // if a truly basic enemy is needed, it should be added to ENEMY_CONFIGS or handled as a default.
- // For now, an invalid type defaults to 'goblin' config.
+ // --- Initialization ---
+ self.initializeSprites();
+ var initialAnim = self.type === 'eyeball' ? 'fly' : 'run';
+ var initialSpeed = self.animationSpeed;
+ if (self.type === 'skeleton' && initialAnim === 'run') initialSpeed *= 0.8;
+ // Ensure animations for the type exist before setting
+ if (self.animations[initialAnim] && self.animations[initialAnim].length > 0) {
+ self.setAnimation(initialAnim, true, initialSpeed);
+ } else if (Object.keys(self.animations).length > 0) {
+ // Fallback to the first defined animation if 'run' or 'fly' isn't available
+ var firstKey = Object.keys(self.animations)[0];
+ self.setAnimation(firstKey, true, self.animationSpeed);
+ } else if (self.type === 'basic' && self.sprites.length > 0) {
+ // For 'basic' enemy with just one sprite, ensure it's visible
+ self.sprites[0].alpha = 1;
+ }
return self;
});
var HealthPotion = Container.expand(function () {
var self = Container.call(this);
@@ -3821,131 +3699,14 @@
/****
* Game Code
****/
-// Configuration object for different enemy types
//resetAllStorage();
// Add ShopUpgrade.prototype methods here
// Base collectible behavior for items that can be collected
/****
* Game Management
****/
-var ENEMY_CONFIGS = {
- goblin: {
- type: 'goblin',
- speed: 7,
- hitboxWidth: 200,
- hitboxHeight: 260,
- animations: {
- run: ['goblinrun1', 'goblinrun2', 'goblinrun3', 'goblinrun4', 'goblinrun5', 'goblinrun6', 'goblinrun7', 'goblinrun8'],
- hit: ['goblinhit1'],
- die: ['goblindie1', 'goblindie2', 'goblindie3', 'goblindie4']
- },
- lootModifier: 1.0,
- animationSpeed: 0.11,
- platformOffsetY: ENEMY_PLATFORM_OFFSET,
- gravity: 0.7,
- hitSound: 'enemyhit',
- hitTimerDuration: 35,
- deathTimerDuration: 60,
- stateUpdateMethods: {
- normal: 'updateGoblinNormalState',
- hit: 'updateGoblinHitState',
- dying: 'updateGoblinDyingState'
- }
- },
- eyeball: {
- type: 'eyeball',
- speed: 7,
- hitboxWidth: 200,
- hitboxHeight: 90,
- animations: {
- fly: ['eyefly1', 'eyefly2', 'eyefly3', 'eyefly4', 'eyefly5', 'eyefly6', 'eyefly7', 'eyefly8'],
- hit: ['eyedie1', 'eyedie2'],
- // Hit animation frames
- die: ['eyedie3', 'eyedie4', 'eyedie5'] // Die animation frames
- },
- lootModifier: 1.0,
- isFlying: true,
- verticalSpeed: 2,
- maxVerticalSpeed: 4,
- homingDelay: 80,
- animationSpeed: 0.11,
- gravity: 0.5,
- // Used when dying
- hitSound: 'eyeballhit',
- hitTimerDuration: 100,
- // Eyeball has a longer hit animation sequence
- deathTimerDuration: 180,
- // Eyeball has a longer death animation sequence
- stateUpdateMethods: {
- normal: 'updateEyeballNormalState',
- hit: 'updateEyeballDamageState',
- // Combined hit/dying logic for eyeball
- dying: 'updateEyeballDamageState'
- }
- },
- skeleton: {
- type: 'skeleton',
- speed: 6,
- hitboxWidth: 200,
- hitboxHeight: 260,
- animations: {
- run: ['skeletonwalk1', 'skeletonwalk2', 'skeletonwalk3', 'skeletonwalk4'],
- hit: ['skeletonhit1', 'skeletonhit2'],
- die: ['skeletondie1', 'skeletondie2', 'skeletondie3', 'skeletondie4'],
- "throw": ['skeletonthrow1', 'skeletonthrow2', 'skeletonthrow3', 'skeletonthrow4', 'skeletonthrow5', 'skeletonthrow6']
- },
- lootModifier: 1.6,
- animationSpeed: 0.11 * 0.8,
- // Slower animation
- platformOffsetY: ENEMY_PLATFORM_OFFSET,
- throwingDuration: 90,
- throwingPauseTime: 15,
- gravity: 0.7,
- hitSound: 'skeletonhit',
- hitTimerDuration: 35,
- deathTimerDuration: 60,
- stateUpdateMethods: {
- normal: 'updateSkeletonNormalState',
- // Has throwing logic
- hit: 'updateSkeletonHitState',
- dying: 'updateSkeletonDyingState',
- throwing: 'updateSkeletonThrowingState' // Specific state for throwing
- }
- },
- mushroom: {
- type: 'mushroom',
- speed: 6,
- hitboxWidth: 200,
- hitboxHeight: 260,
- animations: {
- run: ['mushroomrun1', 'mushroomrun2', 'mushroomrun3', 'mushroomrun4', 'mushroomrun5', 'mushroomrun6', 'mushroomrun7', 'mushroomrun8'],
- hit: ['mushroomhit1', 'mushroomhit2'],
- die: ['mushroomdie1', 'mushroomdie2', 'mushroomdie3', 'mushroomdie4'],
- attack: ['mushroomattack1', 'mushroomattack2', 'mushroomattack3', 'mushroomattack4', 'mushroomattack5']
- },
- lootModifier: 1.0,
- animationSpeed: 0.11,
- platformOffsetY: ENEMY_PLATFORM_OFFSET,
- attackDuration: 100,
- // Frames for attack animation cycle
- attackCooldown: 180,
- // Cooldown before next attack
- gravity: 0.7,
- hitSound: 'mushroomdie',
- // Sound when hit by player attack
- hitTimerDuration: 35,
- deathTimerDuration: 60,
- stateUpdateMethods: {
- normal: 'updateMushroomNormalState',
- // Has attack logic
- hit: 'updateMushroomHitState',
- dying: 'updateMushroomDyingState',
- attacking: 'updateMushroomAttackState' // Specific state for attacking
- }
- }
-};
initializeStorage();
ShopUpgrade.prototype.setSelected = function (isSelected) {
var self = this;
// Find the background asset within windowContainer children
2D Single Monster. In-Game asset. 2d. Blank background. High contrast. No shadows..
A gold coin. 8 bit pixel art. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
Dark and moody dungeon background. Infinite repeatable texture. 8 bit pixel art.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A ruby. Pixel art.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A wooden arrow with white feathers and a steel arrow head. Horizontal. Pixel art. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A thin crude sword, no pommel. 8 bit pixel art.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
An icon of white wings. Pixel art.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
a magnet icon. 8 bit pixel art. In-Game asset. 2d. High contrast. No shadows
An icon of a glowing wooden arrow from a bow trailing pink particles. Pixel art.. In-Game asset. 2d. High contrast. No shadows
backgroundmusic1
Music
playerjump
Sound effect
swordslash
Sound effect
jarbreak
Sound effect
enemyhit
Sound effect
eyeballhit
Sound effect
coincollect
Sound effect
woodbreak
Sound effect
coinbounce
Sound effect
potion
Sound effect
playerouch
Sound effect
bowfiring
Sound effect
arrowfire
Sound effect
arrowpickup
Sound effect
gameover
Sound effect
skeletonhit
Sound effect
gameover2
Sound effect
shopbuy
Sound effect
menuselect
Sound effect
cantbuy
Sound effect
startgame
Sound effect
platformcrumble
Sound effect
rocksfall
Sound effect
airdash
Sound effect
groundimpact
Sound effect
groundsmashfalling
Sound effect
slide
Sound effect
mushroomburst
Sound effect
mushroomhiss
Sound effect
mushroomdie
Sound effect
mushroombounce
Sound effect