/**** * 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; }); // Enemy class with refactored animation management var Enemy = Container.expand(function (type) { var self = Container.call(this); // Enemy properties self.type = type || 'basic'; self.speed = 7; self.isOnGround = true; self.velocityY = 0; self.currentPlatform = null; self.groundY = GAME_HEIGHT / 1.5; self.isHit = false; self.isDying = false; self.deathTimer = 0; self.throwBackSpeed = 15; self.throwBackDistance = 0; // Loot modifier - skeletons get 1.5x self.lootModifier = self.type === 'skeleton' ? 1.6 : 1.0; self.maxThrowBack = 200; self.hitType = 'none'; // Can be 'none', 'attack', or 'slide' // Hitbox properties self.hitboxWidth = 200; self.hitboxHeight = self.type === 'eyeball' ? 90 : 260; // Animation properties self.sprites = []; self.animationCounter = 0; self.animationSpeed = 0.08; // Define all animation arrays based on enemy type if (self.type === 'eyeball') { // Eyeball-specific properties self.isFlying = true; self.flyingHeight = 0; self.verticalSpeed = 2; self.maxVerticalSpeed = 4; self.homingDelay = 80; self.homingTimer = 0; self.flyFrame = 0; // Animation frames self.flyAnimation = ['eyefly1', 'eyefly2', 'eyefly3', 'eyefly4', 'eyefly5', 'eyefly6', 'eyefly7', 'eyefly8']; self.hitAnimation = ['eyedie1', 'eyedie2']; self.dieAnimation = ['eyedie3', 'eyedie4', 'eyedie5']; } else if (self.type === 'goblin') { // Goblin-specific properties self.runFrame = 0; // Animation frames self.runAnimation = ['goblinrun1', 'goblinrun2', 'goblinrun3', 'goblinrun4', 'goblinrun5', 'goblinrun6', 'goblinrun7', 'goblinrun8']; self.hitAnimation = ['goblinhit1']; self.dieAnimation = ['goblindie1', 'goblindie2', 'goblindie3', 'goblindie4']; } else if (self.type === 'skeleton') { // Skeleton-specific properties self.runFrame = 0; self.speed = 6; // Slower than goblin // Throwing properties self.isThrowing = false; self.throwingTimer = 0; self.throwingDuration = 90; self.throwingPauseTime = 15; self.throwingFrame = 0; // Animation frames self.runAnimation = ['skeletonwalk1', 'skeletonwalk2', 'skeletonwalk3', 'skeletonwalk4']; self.hitAnimation = ['skeletonhit1', 'skeletonhit2']; self.dieAnimation = ['skeletondie1', 'skeletondie2', 'skeletondie3', 'skeletondie4']; self.throwingAnimation = ['skeletonthrow1', 'skeletonthrow2', 'skeletonthrow3', 'skeletonthrow4', 'skeletonthrow5', 'skeletonthrow6']; } // Initialize animation sprites for eyeball self.initEyeballSprites = function () { // Add fly animations for (var i = 0; i < self.flyAnimation.length; i++) { var sprite = self.attachAsset(self.flyAnimation[i], { anchorX: 0.5, anchorY: 0.5 }); sprite.alpha = i === 0 ? 1 : 0; self.sprites.push(sprite); } // Add hit animations for (var i = 0; i < self.hitAnimation.length; i++) { var sprite = self.attachAsset(self.hitAnimation[i], { anchorX: 0.5, anchorY: 0.5 }); sprite.alpha = 0; self.sprites.push(sprite); } // Add die animations for (var i = 0; i < self.dieAnimation.length; i++) { var sprite = self.attachAsset(self.dieAnimation[i], { anchorX: 0.5, anchorY: 0.5 }); sprite.alpha = 0; self.sprites.push(sprite); } }; // Initialize animation sprites for goblin self.initGoblinSprites = function () { // Add 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); } // Add hit animations for (var i = 0; i < self.hitAnimation.length; i++) { var sprite = self.attachAsset(self.hitAnimation[i], { anchorX: 0.5, anchorY: 0.5 }); sprite.alpha = 0; self.sprites.push(sprite); } // Add die animations for (var i = 0; i < self.dieAnimation.length; i++) { var sprite = self.attachAsset(self.dieAnimation[i], { anchorX: 0.5, anchorY: 0.5 }); sprite.alpha = 0; self.sprites.push(sprite); } }; // Initialize animation sprites for skeleton self.initSkeletonSprites = function () { // Add walk 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); } // Add hit animations for (var i = 0; i < self.hitAnimation.length; i++) { var sprite = self.attachAsset(self.hitAnimation[i], { anchorX: 0.5, anchorY: 0.5 }); sprite.alpha = 0; self.sprites.push(sprite); } // Add die animations for (var i = 0; i < self.dieAnimation.length; i++) { var sprite = self.attachAsset(self.dieAnimation[i], { anchorX: 0.5, anchorY: 0.5 }); sprite.alpha = 0; self.sprites.push(sprite); } // Add throwing animations for (var i = 0; i < self.throwingAnimation.length; i++) { var sprite = self.attachAsset(self.throwingAnimation[i], { anchorX: 0.5, anchorY: 0.5 }); sprite.alpha = 0; self.sprites.push(sprite); } }; // Hide all animation sprites self.hideAllSprites = function () { for (var i = 0; i < self.sprites.length; i++) { self.sprites[i].alpha = 0; } }; // Update eyeball hit/die animation - fixed to allow falling without platform collision self.updateEyeballDamageState = function () { if (self.isHit) { var hitOffset = self.flyAnimation.length; self.throwBackDistance += Math.abs(self.throwBackSpeed); self.x += self.throwBackSpeed; // Apply vertical velocity without any dampening on the first few frames // to ensure the eyeball gets significant vertical movement if (self.hitTimer > 30) { // Apply full vertical movement without reduction for initial frames self.y += self.velocityY; } else { // After initial frames, gradually reduce the vertical movement self.y += self.velocityY; self.velocityY *= 0.8; // Less aggressive dampening to maintain vertical motion } self.throwBackSpeed *= 0.95; // Show hit animation var hitFrame = Math.floor(self.hitTimer / 100) % 2; self.sprites[hitOffset + hitFrame].alpha = 1; self.hitTimer--; if (self.hitTimer <= 0) { self.isHit = false; if (self.hitType === 'attack') { // Only start dying if it was a regular attack self.isDying = true; self.deathTimer = 180; self.deathFrame = 0; // Add dropLoot here when transitioning to dying state self.dropLoot(); } self.hitType = 'none'; } } else if (self.isDying) { var dieOffset = self.flyAnimation.length + self.hitAnimation.length; // Apply gravity without platform collision - this ensures constant falling self.velocityY += 0.5; self.y += self.velocityY; // Keep moving horizontally during fall self.x -= PLATFORM_SPEED * 0.5; // Progress through death frames if (self.deathTimer > 120) { self.sprites[dieOffset].alpha = 1; // eyedie3 } else if (self.deathTimer > 60) { self.sprites[dieOffset + 1].alpha = 1; // eyedie4 } else { self.sprites[dieOffset + 2].alpha = 1; // eyedie5 } self.deathTimer--; if (self.deathTimer <= 0) { self.alpha -= 0.05; if (self.alpha <= 0) { self.destroy(); } } } }; // Update eyeball normal movement self.updateEyeballNormalState = function () { self.x -= self.speed * gameSpeedMultiplier; // Only start homing after delay if (self.homingTimer >= self.homingDelay) { // Home toward player 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 { // Before homing, maintain height with slight wave motion self.velocityY = Math.sin(self.homingTimer * 0.05) * 2; self.homingTimer++; } self.y += self.velocityY; // Animate self.animationCounter += self.animationSpeed; if (self.animationCounter >= 1) { self.animationCounter = 0; self.flyFrame = (self.flyFrame + 1) % self.flyAnimation.length; } self.sprites[self.flyFrame].alpha = 1; }; // Update goblin hit animation self.updateGoblinHitState = function () { // Handle throw back motion self.x += self.throwBackSpeed; self.throwBackDistance += Math.abs(self.throwBackSpeed); self.throwBackSpeed *= 0.95; // Check if still on platform 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; // Check for another platform 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; foundAnotherPlatform = true; break; } } // Start falling if no other platform found if (!foundAnotherPlatform) { self.isOnGround = false; self.currentPlatform = null; if (self.velocityY === 0) { self.velocityY = 0.1; } } } } // Apply gravity if not on ground if (!self.isOnGround) { self.velocityY += 0.7; self.y += self.velocityY; self.checkPlatformCollision(); } // Show hit animation var hitOffset = self.runAnimation.length; self.sprites[hitOffset].alpha = 1; // Decrease hit timer self.hitTimer--; // Once hit timer expires, check hit type if (self.hitTimer <= 0) { self.isHit = false; if (self.hitType === 'attack') { self.isDying = true; self.deathTimer = 60; self.deathFrame = 0; self.dropLoot(); } self.hitType = 'none'; } }; // Update skeleton hit animation self.updateSkeletonHitState = function () { // Handle throw back motion self.x += self.throwBackSpeed; self.throwBackDistance += Math.abs(self.throwBackSpeed); self.throwBackSpeed *= 0.95; // Check if still on platform 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; // Check for another platform 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; foundAnotherPlatform = true; break; } } // Start falling if no other platform found if (!foundAnotherPlatform) { self.isOnGround = false; self.currentPlatform = null; if (self.velocityY === 0) { self.velocityY = 0.1; } } } } // Apply gravity if not on ground if (!self.isOnGround) { self.velocityY += 0.7; self.y += self.velocityY; self.checkPlatformCollision(); } // Show hit animation var hitOffset = self.runAnimation.length; var hitFrame = Math.floor(self.hitTimer / 18) % 2; // Alternate between hit frames self.sprites[hitOffset + hitFrame].alpha = 1; // Decrease hit timer self.hitTimer--; // Once hit timer expires, check hit type if (self.hitTimer <= 0) { self.isHit = false; if (self.hitType === 'attack') { self.isDying = true; self.deathTimer = 60; self.deathFrame = 0; self.dropLoot(); } self.hitType = 'none'; } }; // Update goblin dying animation self.updateGoblinDyingState = function () { // Continue throw back during death self.x += self.throwBackSpeed; // Check if still on platform 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; // Check for another platform 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; foundAnotherPlatform = true; break; } } // Start falling if no other platform found if (!foundAnotherPlatform) { self.isOnGround = false; self.currentPlatform = null; if (self.velocityY === 0) { self.velocityY = 0.1; } } } } // Apply gravity if not on ground if (!self.isOnGround) { self.velocityY += 0.7; self.y += self.velocityY; self.checkPlatformCollision(); } // After halfway through death animation, match platform speed if (self.deathFrame >= 2) { self.x -= PLATFORM_SPEED; } self.throwBackSpeed *= 0.95; // Handle death animation var dieOffset = self.runAnimation.length + self.hitAnimation.length; // Progress frame every 15 frames if (self.deathTimer % 15 === 0 && self.deathFrame < self.dieAnimation.length - 1) { self.deathFrame++; } self.sprites[dieOffset + self.deathFrame].alpha = 1; // Count down death timer self.deathTimer--; // After timer expires, fade out if (self.deathTimer <= 0) { self.alpha -= 0.1; if (self.alpha <= 0) { self.destroy(); } } }; // Update skeleton dying animation self.updateSkeletonDyingState = function () { // Continue throw back during death self.x += self.throwBackSpeed; // Check if still on platform 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; // Check for another platform 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; foundAnotherPlatform = true; break; } } // Start falling if no other platform found if (!foundAnotherPlatform) { self.isOnGround = false; self.currentPlatform = null; if (self.velocityY === 0) { self.velocityY = 0.1; } } } } // Apply gravity if not on ground if (!self.isOnGround) { self.velocityY += 0.7; self.y += self.velocityY; self.checkPlatformCollision(); } // After halfway through death animation, match platform speed if (self.deathFrame >= 2) { self.x -= PLATFORM_SPEED; } self.throwBackSpeed *= 0.95; // Handle death animation var dieOffset = self.runAnimation.length + self.hitAnimation.length; // Progress frame every 15 frames if (self.deathTimer % 15 === 0 && self.deathFrame < self.dieAnimation.length - 1) { self.deathFrame++; } self.sprites[dieOffset + self.deathFrame].alpha = 1; // Count down death timer self.deathTimer--; // After timer expires, fade out if (self.deathTimer <= 0) { self.alpha -= 0.1; if (self.alpha <= 0) { self.destroy(); } } }; // Update goblin normal movement self.updateGoblinNormalState = function () { // Move left with speed multiplier self.x -= self.speed * gameSpeedMultiplier; // Platform and gravity code if (!self.isOnGround) { self.velocityY += 0.7; self.y += self.velocityY; self.checkPlatformCollision(); } // Ensure goblin stays on platform 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; // Check for another platform 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; foundAnotherPlatform = true; break; } } // Start falling if no other platform found if (!foundAnotherPlatform) { self.isOnGround = false; self.currentPlatform = null; if (self.velocityY === 0) { self.velocityY = 0.1; } } } } // Animate running 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; }; // Update skeleton throwing animation self.updateSkeletonThrowingState = function () { // Always move exactly with platform speed during throwing self.x -= PLATFORM_SPEED * gameSpeedMultiplier; // Offset to find throwing animation sprites var throwOffset = self.runAnimation.length + self.hitAnimation.length + self.dieAnimation.length; // Progress through throwing frames if (self.throwingTimer > self.throwingDuration - self.throwingPauseTime) { // Early frames building up to throw var frameIndex = Math.floor((self.throwingDuration - self.throwingTimer) / ((self.throwingDuration - self.throwingPauseTime) / 5)); frameIndex = Math.min(4, Math.max(0, frameIndex)); self.sprites[throwOffset + frameIndex].alpha = 1; } else if (self.throwingTimer <= Math.floor(self.throwingDuration * 0.8) && self.throwingTimer > Math.floor(self.throwingDuration * 0.8) - 2 && !self.swordThrown) { // Flag to ensure we only throw once during this window self.swordThrown = true; var sword = new SkeletonSword(); sword.x = self.x - 100; sword.y = self.y - 100; game.addChild(sword); skeletonSwords.push(sword); self.sprites[throwOffset + 5].alpha = 1; } else if (self.throwingTimer <= self.throwingPauseTime) { // During pause, keep showing the final frame self.sprites[throwOffset + 5].alpha = 1; } else { // Middle of animation (after throw, before pause) self.sprites[throwOffset + 5].alpha = 1; } // Decrease timer self.throwingTimer--; // End throwing state and restore speed if (self.throwingTimer <= 0) { self.isThrowing = false; self.swordThrown = false; // Reset the flag self.speed = self.originalSpeed || 6; } }; // Update skeleton normal movement self.updateSkeletonNormalState = function () { // Track if this is the first time we're seeing this skeleton if (self.initialThrowMade === undefined) { self.initialThrowMade = false; self.enteredScreenTime = LK.ticks; self.throwDelay = 60; // Reduce delay before first throw } // Check for initial throw if (!self.initialThrowMade && self.x < GAME_WIDTH + 300 && self.isOnGround && !self.isThrowing) { if (LK.ticks - self.enteredScreenTime > self.throwDelay) { self.isThrowing = true; self.swordThrown = false; // Initialize flag here self.throwingTimer = self.throwingDuration; self.throwingFrame = 0; self.originalSpeed = self.speed; self.initialThrowMade = true; } } // Continue with normal movement regardless of throw state // Move left with speed multiplier self.x -= self.speed * gameSpeedMultiplier; // Platform and gravity code if (!self.isOnGround) { self.velocityY += 0.7; self.y += self.velocityY; self.checkPlatformCollision(); } // Ensure skeleton stays on platform 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; // Check for another platform 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; foundAnotherPlatform = true; break; } } // Start falling if no other platform found if (!foundAnotherPlatform) { self.isOnGround = false; self.currentPlatform = null; if (self.velocityY === 0) { self.velocityY = 0.1; } } } } // Animate walking - slower animation speed for skeleton self.animationCounter += self.animationSpeed * 0.8; if (self.animationCounter >= 1) { self.animationCounter = 0; self.runFrame = (self.runFrame + 1) % self.runAnimation.length; } self.sprites[self.runFrame].alpha = 1; }; // 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(); } // Rest of the collision code remains the same // Add this check: if (platform instanceof CrumblePlatform) { platform.trigger(); } // 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(); } // Rest of the collision code remains the same // Add this check here too: if (platform instanceof CrumblePlatform) { platform.trigger(); } // 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'; } 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; } // 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 { LK.getSound('enemyhit').play(); } // Add particle effect var particleOffset = self.type === 'eyeball' ? 175 : 250; particleSystem.emitFromHit(self.x + particleOffset, self.y, player.x, verticalTrajectory, self.type); } }; // Hit handling 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 } // Add particle effect var particleOffset = self.type === 'eyeball' ? 175 : 250; particleSystem.emitFromHit(self.x + particleOffset, self.y, player.x, verticalTrajectory, self.type); } }; self.dropLoot = function () { // Don't drop collectibles during tutorial if (tutorialActive) { return; } // 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); } // 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); } }; self.isDeathAnimationComplete = function () { return self.isDying && self.deathTimer <= 10; // Check if we're near the end of death animation }; // Main update method self.update = function () { // Hide all sprites first self.hideAllSprites(); // Handle different enemy types and states if (self.type === 'eyeball') { if (self.isHit || self.isDying) { self.updateEyeballDamageState(); } else { self.updateEyeballNormalState(); } } else if (self.type === 'skeleton') { // Skeleton logic if (self.isHit) { self.updateSkeletonHitState(); } else if (self.isDying) { self.updateSkeletonDyingState(); } else if (self.isThrowing) { self.updateSkeletonThrowingState(); } else { self.updateSkeletonNormalState(); } } else { // Goblin logic if (self.isHit) { self.updateGoblinHitState(); } else if (self.isDying) { self.updateGoblinDyingState(); } else { self.updateGoblinNormalState(); } } // Destroy if off screen if (self.x < -50 || self.y > GAME_HEIGHT) { self.destroy(); } }; // Initialize based on enemy type AFTER defining the methods if (self.type === 'eyeball') { self.initEyeballSprites(); } else if (self.type === 'goblin') { self.initGoblinSprites(); } else if (self.type === 'skeleton') { self.initSkeletonSprites(); } else { // Basic enemy var enemyGraphics = self.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); } 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 * lootMultiplier) { 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 * lootMultiplier) { 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']; // 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.08; 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 = 90; 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 // 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); } }; // 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]; var leftEdge = platform.x - PLATFORM_HALF_WIDTH + 20; var rightEdge = platform.x + PLATFORM_HALF_WIDTH + 20; // 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 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; } 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) { // 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.7; // 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; } 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.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.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 platforms that are actually broken (but not just crumbling) 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; } // Rest of the original collision detection... 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 + 60) && self.x < platform.x + (PLATFORM_HALF_WIDTH + 60)) { // 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; 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; // Adjust game speed multiplier instead of base speed gameSpeedMultiplier = self.slideSpeedMultiplier; } }; // 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.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 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 self.applyPhysics(); // Handle platform collision and falling self.handlePlatformCollision(); // Handle invulnerability self.updateInvulnerability(); // Update animations based on state 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.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: 600, 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: 2500, maxLevel: 1, icon: 'icon_combat', effect: function effect(level) { console.log("Slide damage upgrade purchased: level " + level); } }, { id: 'better_loot', category: 'Loot', title: 'Fortune Finder', description: 'Increase chances of finding valuable loot', basePrice: 700, maxLevel: 4, icon: 'icon_treasure', effect: function effect(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'); 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; } // 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 self.speed = 6; // Slightly slower than arrows (25) self.rotationSpeed = -0.1; // Counterclockwise rotation // Update method called every frame self.update = function () { // Move from right to left with speed adjusted by game speed multiplier self.x -= self.speed * gameSpeedMultiplier; // 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; }); /**** * 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 * lootMultiplier) { 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 * lootMultiplier) { 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', 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.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; } // Complete tutorial when player has finished sliding and succeeded if (self.hasSlideSucceeded && !player.isSliding) { self.currentState = self.states.COMPLETE; self.setMessage("That's it!\nNow go get some treasure!"); LK.setTimeout(function () { self.complete(); }, 3000); } break; } }; // Input handling 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(); } } 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 (deltaY > 120 && self.currentState === self.states.DROP_DOWN) { self.dropDownInput = true; player.fallThrough(); } } /* // Additional mouse control checks like in game.move if (Math.abs(deltaX) > Math.abs(deltaY)) { if (deltaX > SLIDE_MOVE_THRESHOLD && self.currentState === self.states.SLIDE) { player.slide(); } else if (deltaX < -SLIDE_MOVE_THRESHOLD && self.currentState === self.states.BOW) { player.shoot(); } } // Check for downward swipe with threshold similar to game.move if (deltaY > SLIDE_MOVE_THRESHOLD && 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(); /**** * Game Management ****/ // Base collectible behavior for items that can be collected // Add ShopUpgrade.prototype methods here 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; // Track timestamp storage.lastSaveTime = Date.now(); console.log("Storage values:", { gold: storage.gold, health: storage.healthUpgrade, arrow: storage.arrowUpgrade, levitate: storage.levitateUpgrade, slide: storage.slideUpgrade, loot: storage.lootUpgrade }); }, // 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; // 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; } // 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.lootMultiplier = 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.slideDamage = UPGRADE_REGISTRY.slide_damage > 0; player.lootMultiplier = 1 + UPGRADE_REGISTRY.better_loot * 0.15; // 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; } }; 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.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; // Clear existing upgrades self.upgradeList.removeChildren(); // Filter and show upgrades for this category var categoryUpgrades = self.upgrades.filter(function (upgrade) { return upgrade.category === category; }); categoryUpgrades.forEach(function (upgradeConfig, index) { 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; } // 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 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 }); // Update buy button for the visible upgrade self.updateBuyButton(); }; 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; // Update buy button based on max level first if (isMaxLevel) { self.priceText.setText('MAX'); self.priceCoinIcon.visible = false; // Set text position consistently when showing MAX self.priceText.x = 0; // Center the MAX text // Optionally disable button visual self.buyButtonBg.tint = 0x888888; } else { self.priceText.setText(price); self.priceCoinIcon.visible = true; // Fix coin icon at a consistent position self.priceCoinIcon.x = 160; // Keep fixed position // Right-align price text relative to the coin position var textPadding = 10; // Space between text and coin self.priceText.x = self.priceCoinIcon.x - textPadding - self.priceText.width; // Set button tint based on affordability 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; } // 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 itemBounds = { left: self.x - 25, right: self.x + 25, top: self.y - 25, bottom: self.y + 25 }; 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 }; /**** * 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; } } 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) { self.score = newScore; // Update the registry UPGRADE_REGISTRY.gold = self.score; if (self.scoreText) { self.scoreText.setText(newScore); // Keep right edge at a fixed position - same approach as updateAmmo function 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 enemySpawnInterval = 100; var goblinSpawnCounter = 0; var goblinSpawnInterval = 100; var eyeballSpawnCounter = 0; var eyeballSpawnInterval = 200; var skeletonSpawnCounter = 0; var skeletonSpawnInterval = 350; 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 })); var mg2 = midgroundContainer.addChild(LK.getAsset('midground', { anchorX: 0, anchorY: 1 })); 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 })); var fg2 = foregroundContainer.addChild(LK.getAsset('foreground', { anchorX: 0, anchorY: 1 })); 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 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; } 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); // 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 (!tutorialActive) { var isPlatformOccupied = function isPlatformOccupied(platform) { return enemies.some(function (enemy) { return enemy.currentPlatform === platform && (enemy.type === 'goblin' || enemy.type === 'skeleton'); }); }; // Get available platforms for ground enemies // Create helper function to check if platform is already taken 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); }); // Goblin spawning goblinSpawnCounter++; if (goblinSpawnCounter >= goblinSpawnInterval && unoccupiedPlatforms.length > 0) { var platform = unoccupiedPlatforms[Math.floor(Math.random() * unoccupiedPlatforms.length)]; var enemy = new Enemy('goblin'); enemy.x = platform.x; enemy.y = platform.y - ENEMY_PLATFORM_OFFSET; enemy.currentPlatform = platform; enemies.push(enemy); game.addChild(enemy); goblinSpawnInterval = Math.floor(Math.random() * 150) + 150; goblinSpawnCounter = 0; // Remove used platform from unoccupied list unoccupiedPlatforms = unoccupiedPlatforms.filter(function (p) { return p !== platform; }); } // Skeleton spawning - only try if there are still unoccupied platforms skeletonSpawnCounter++; if (skeletonSpawnCounter >= skeletonSpawnInterval && unoccupiedPlatforms.length > 0) { var platform = unoccupiedPlatforms[Math.floor(Math.random() * unoccupiedPlatforms.length)]; var enemy = new Enemy('skeleton'); enemy.x = platform.x; enemy.y = platform.y - ENEMY_PLATFORM_OFFSET; enemy.currentPlatform = platform; enemies.push(enemy); game.addChild(enemy); skeletonSpawnInterval = Math.floor(Math.random() * 200) + 350; skeletonSpawnCounter = 0; } // Eyeball spawning - updated height range 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; // Spawn anywhere between highest and lowest enemies.push(enemy); game.addChild(enemy); eyeballSpawnInterval = Math.floor(Math.random() * 300) + 250; 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); } } // 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 (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 (!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) { // Right swipe - trigger slide player.slide(); } else if (deltaX < -50) { // Left swipe - trigger bow shot player.shoot(); } } else if (deltaY > 120) { // Downward swipe 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; /*if (Math.abs(deltaX) > Math.abs(y - touchStartY)) { if (deltaX > SLIDE_MOVE_THRESHOLD) { player.slide(); } else if (deltaX < -(SLIDE_MOVE_THRESHOLD + 100)) { player.shoot(); } } // Check for downward swipe var deltaY = y - touchStartY; if (deltaY > SLIDE_MOVE_THRESHOLD) { player.fallThrough(); } if (lastMoveY !== 0) { var deltaY = y - lastMoveY; var currentTime = Date.now(); if (deltaY < -MOVE_THRESHOLD && currentTime - lastJumpTime > JUMP_COOLDOWN) { player.jump(); lastJumpTime = currentTime; } } */ lastMoveY = y; };
/****
* 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;
});
// Enemy class with refactored animation management
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
// Enemy properties
self.type = type || 'basic';
self.speed = 7;
self.isOnGround = true;
self.velocityY = 0;
self.currentPlatform = null;
self.groundY = GAME_HEIGHT / 1.5;
self.isHit = false;
self.isDying = false;
self.deathTimer = 0;
self.throwBackSpeed = 15;
self.throwBackDistance = 0;
// Loot modifier - skeletons get 1.5x
self.lootModifier = self.type === 'skeleton' ? 1.6 : 1.0;
self.maxThrowBack = 200;
self.hitType = 'none'; // Can be 'none', 'attack', or 'slide'
// Hitbox properties
self.hitboxWidth = 200;
self.hitboxHeight = self.type === 'eyeball' ? 90 : 260;
// Animation properties
self.sprites = [];
self.animationCounter = 0;
self.animationSpeed = 0.08;
// Define all animation arrays based on enemy type
if (self.type === 'eyeball') {
// Eyeball-specific properties
self.isFlying = true;
self.flyingHeight = 0;
self.verticalSpeed = 2;
self.maxVerticalSpeed = 4;
self.homingDelay = 80;
self.homingTimer = 0;
self.flyFrame = 0;
// Animation frames
self.flyAnimation = ['eyefly1', 'eyefly2', 'eyefly3', 'eyefly4', 'eyefly5', 'eyefly6', 'eyefly7', 'eyefly8'];
self.hitAnimation = ['eyedie1', 'eyedie2'];
self.dieAnimation = ['eyedie3', 'eyedie4', 'eyedie5'];
} else if (self.type === 'goblin') {
// Goblin-specific properties
self.runFrame = 0;
// Animation frames
self.runAnimation = ['goblinrun1', 'goblinrun2', 'goblinrun3', 'goblinrun4', 'goblinrun5', 'goblinrun6', 'goblinrun7', 'goblinrun8'];
self.hitAnimation = ['goblinhit1'];
self.dieAnimation = ['goblindie1', 'goblindie2', 'goblindie3', 'goblindie4'];
} else if (self.type === 'skeleton') {
// Skeleton-specific properties
self.runFrame = 0;
self.speed = 6; // Slower than goblin
// Throwing properties
self.isThrowing = false;
self.throwingTimer = 0;
self.throwingDuration = 90;
self.throwingPauseTime = 15;
self.throwingFrame = 0;
// Animation frames
self.runAnimation = ['skeletonwalk1', 'skeletonwalk2', 'skeletonwalk3', 'skeletonwalk4'];
self.hitAnimation = ['skeletonhit1', 'skeletonhit2'];
self.dieAnimation = ['skeletondie1', 'skeletondie2', 'skeletondie3', 'skeletondie4'];
self.throwingAnimation = ['skeletonthrow1', 'skeletonthrow2', 'skeletonthrow3', 'skeletonthrow4', 'skeletonthrow5', 'skeletonthrow6'];
}
// Initialize animation sprites for eyeball
self.initEyeballSprites = function () {
// Add fly animations
for (var i = 0; i < self.flyAnimation.length; i++) {
var sprite = self.attachAsset(self.flyAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = i === 0 ? 1 : 0;
self.sprites.push(sprite);
}
// Add hit animations
for (var i = 0; i < self.hitAnimation.length; i++) {
var sprite = self.attachAsset(self.hitAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
// Add die animations
for (var i = 0; i < self.dieAnimation.length; i++) {
var sprite = self.attachAsset(self.dieAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
};
// Initialize animation sprites for goblin
self.initGoblinSprites = function () {
// Add 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);
}
// Add hit animations
for (var i = 0; i < self.hitAnimation.length; i++) {
var sprite = self.attachAsset(self.hitAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
// Add die animations
for (var i = 0; i < self.dieAnimation.length; i++) {
var sprite = self.attachAsset(self.dieAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
};
// Initialize animation sprites for skeleton
self.initSkeletonSprites = function () {
// Add walk 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);
}
// Add hit animations
for (var i = 0; i < self.hitAnimation.length; i++) {
var sprite = self.attachAsset(self.hitAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
// Add die animations
for (var i = 0; i < self.dieAnimation.length; i++) {
var sprite = self.attachAsset(self.dieAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
// Add throwing animations
for (var i = 0; i < self.throwingAnimation.length; i++) {
var sprite = self.attachAsset(self.throwingAnimation[i], {
anchorX: 0.5,
anchorY: 0.5
});
sprite.alpha = 0;
self.sprites.push(sprite);
}
};
// Hide all animation sprites
self.hideAllSprites = function () {
for (var i = 0; i < self.sprites.length; i++) {
self.sprites[i].alpha = 0;
}
};
// Update eyeball hit/die animation - fixed to allow falling without platform collision
self.updateEyeballDamageState = function () {
if (self.isHit) {
var hitOffset = self.flyAnimation.length;
self.throwBackDistance += Math.abs(self.throwBackSpeed);
self.x += self.throwBackSpeed;
// Apply vertical velocity without any dampening on the first few frames
// to ensure the eyeball gets significant vertical movement
if (self.hitTimer > 30) {
// Apply full vertical movement without reduction for initial frames
self.y += self.velocityY;
} else {
// After initial frames, gradually reduce the vertical movement
self.y += self.velocityY;
self.velocityY *= 0.8; // Less aggressive dampening to maintain vertical motion
}
self.throwBackSpeed *= 0.95;
// Show hit animation
var hitFrame = Math.floor(self.hitTimer / 100) % 2;
self.sprites[hitOffset + hitFrame].alpha = 1;
self.hitTimer--;
if (self.hitTimer <= 0) {
self.isHit = false;
if (self.hitType === 'attack') {
// Only start dying if it was a regular attack
self.isDying = true;
self.deathTimer = 180;
self.deathFrame = 0;
// Add dropLoot here when transitioning to dying state
self.dropLoot();
}
self.hitType = 'none';
}
} else if (self.isDying) {
var dieOffset = self.flyAnimation.length + self.hitAnimation.length;
// Apply gravity without platform collision - this ensures constant falling
self.velocityY += 0.5;
self.y += self.velocityY;
// Keep moving horizontally during fall
self.x -= PLATFORM_SPEED * 0.5;
// Progress through death frames
if (self.deathTimer > 120) {
self.sprites[dieOffset].alpha = 1; // eyedie3
} else if (self.deathTimer > 60) {
self.sprites[dieOffset + 1].alpha = 1; // eyedie4
} else {
self.sprites[dieOffset + 2].alpha = 1; // eyedie5
}
self.deathTimer--;
if (self.deathTimer <= 0) {
self.alpha -= 0.05;
if (self.alpha <= 0) {
self.destroy();
}
}
}
};
// Update eyeball normal movement
self.updateEyeballNormalState = function () {
self.x -= self.speed * gameSpeedMultiplier;
// Only start homing after delay
if (self.homingTimer >= self.homingDelay) {
// Home toward player
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 {
// Before homing, maintain height with slight wave motion
self.velocityY = Math.sin(self.homingTimer * 0.05) * 2;
self.homingTimer++;
}
self.y += self.velocityY;
// Animate
self.animationCounter += self.animationSpeed;
if (self.animationCounter >= 1) {
self.animationCounter = 0;
self.flyFrame = (self.flyFrame + 1) % self.flyAnimation.length;
}
self.sprites[self.flyFrame].alpha = 1;
};
// Update goblin hit animation
self.updateGoblinHitState = function () {
// Handle throw back motion
self.x += self.throwBackSpeed;
self.throwBackDistance += Math.abs(self.throwBackSpeed);
self.throwBackSpeed *= 0.95;
// Check if still on platform
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;
// Check for another platform
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;
foundAnotherPlatform = true;
break;
}
}
// Start falling if no other platform found
if (!foundAnotherPlatform) {
self.isOnGround = false;
self.currentPlatform = null;
if (self.velocityY === 0) {
self.velocityY = 0.1;
}
}
}
}
// Apply gravity if not on ground
if (!self.isOnGround) {
self.velocityY += 0.7;
self.y += self.velocityY;
self.checkPlatformCollision();
}
// Show hit animation
var hitOffset = self.runAnimation.length;
self.sprites[hitOffset].alpha = 1;
// Decrease hit timer
self.hitTimer--;
// Once hit timer expires, check hit type
if (self.hitTimer <= 0) {
self.isHit = false;
if (self.hitType === 'attack') {
self.isDying = true;
self.deathTimer = 60;
self.deathFrame = 0;
self.dropLoot();
}
self.hitType = 'none';
}
};
// Update skeleton hit animation
self.updateSkeletonHitState = function () {
// Handle throw back motion
self.x += self.throwBackSpeed;
self.throwBackDistance += Math.abs(self.throwBackSpeed);
self.throwBackSpeed *= 0.95;
// Check if still on platform
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;
// Check for another platform
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;
foundAnotherPlatform = true;
break;
}
}
// Start falling if no other platform found
if (!foundAnotherPlatform) {
self.isOnGround = false;
self.currentPlatform = null;
if (self.velocityY === 0) {
self.velocityY = 0.1;
}
}
}
}
// Apply gravity if not on ground
if (!self.isOnGround) {
self.velocityY += 0.7;
self.y += self.velocityY;
self.checkPlatformCollision();
}
// Show hit animation
var hitOffset = self.runAnimation.length;
var hitFrame = Math.floor(self.hitTimer / 18) % 2; // Alternate between hit frames
self.sprites[hitOffset + hitFrame].alpha = 1;
// Decrease hit timer
self.hitTimer--;
// Once hit timer expires, check hit type
if (self.hitTimer <= 0) {
self.isHit = false;
if (self.hitType === 'attack') {
self.isDying = true;
self.deathTimer = 60;
self.deathFrame = 0;
self.dropLoot();
}
self.hitType = 'none';
}
};
// Update goblin dying animation
self.updateGoblinDyingState = function () {
// Continue throw back during death
self.x += self.throwBackSpeed;
// Check if still on platform
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;
// Check for another platform
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;
foundAnotherPlatform = true;
break;
}
}
// Start falling if no other platform found
if (!foundAnotherPlatform) {
self.isOnGround = false;
self.currentPlatform = null;
if (self.velocityY === 0) {
self.velocityY = 0.1;
}
}
}
}
// Apply gravity if not on ground
if (!self.isOnGround) {
self.velocityY += 0.7;
self.y += self.velocityY;
self.checkPlatformCollision();
}
// After halfway through death animation, match platform speed
if (self.deathFrame >= 2) {
self.x -= PLATFORM_SPEED;
}
self.throwBackSpeed *= 0.95;
// Handle death animation
var dieOffset = self.runAnimation.length + self.hitAnimation.length;
// Progress frame every 15 frames
if (self.deathTimer % 15 === 0 && self.deathFrame < self.dieAnimation.length - 1) {
self.deathFrame++;
}
self.sprites[dieOffset + self.deathFrame].alpha = 1;
// Count down death timer
self.deathTimer--;
// After timer expires, fade out
if (self.deathTimer <= 0) {
self.alpha -= 0.1;
if (self.alpha <= 0) {
self.destroy();
}
}
};
// Update skeleton dying animation
self.updateSkeletonDyingState = function () {
// Continue throw back during death
self.x += self.throwBackSpeed;
// Check if still on platform
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;
// Check for another platform
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;
foundAnotherPlatform = true;
break;
}
}
// Start falling if no other platform found
if (!foundAnotherPlatform) {
self.isOnGround = false;
self.currentPlatform = null;
if (self.velocityY === 0) {
self.velocityY = 0.1;
}
}
}
}
// Apply gravity if not on ground
if (!self.isOnGround) {
self.velocityY += 0.7;
self.y += self.velocityY;
self.checkPlatformCollision();
}
// After halfway through death animation, match platform speed
if (self.deathFrame >= 2) {
self.x -= PLATFORM_SPEED;
}
self.throwBackSpeed *= 0.95;
// Handle death animation
var dieOffset = self.runAnimation.length + self.hitAnimation.length;
// Progress frame every 15 frames
if (self.deathTimer % 15 === 0 && self.deathFrame < self.dieAnimation.length - 1) {
self.deathFrame++;
}
self.sprites[dieOffset + self.deathFrame].alpha = 1;
// Count down death timer
self.deathTimer--;
// After timer expires, fade out
if (self.deathTimer <= 0) {
self.alpha -= 0.1;
if (self.alpha <= 0) {
self.destroy();
}
}
};
// Update goblin normal movement
self.updateGoblinNormalState = function () {
// Move left with speed multiplier
self.x -= self.speed * gameSpeedMultiplier;
// Platform and gravity code
if (!self.isOnGround) {
self.velocityY += 0.7;
self.y += self.velocityY;
self.checkPlatformCollision();
}
// Ensure goblin stays on platform
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;
// Check for another platform
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;
foundAnotherPlatform = true;
break;
}
}
// Start falling if no other platform found
if (!foundAnotherPlatform) {
self.isOnGround = false;
self.currentPlatform = null;
if (self.velocityY === 0) {
self.velocityY = 0.1;
}
}
}
}
// Animate running
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;
};
// Update skeleton throwing animation
self.updateSkeletonThrowingState = function () {
// Always move exactly with platform speed during throwing
self.x -= PLATFORM_SPEED * gameSpeedMultiplier;
// Offset to find throwing animation sprites
var throwOffset = self.runAnimation.length + self.hitAnimation.length + self.dieAnimation.length;
// Progress through throwing frames
if (self.throwingTimer > self.throwingDuration - self.throwingPauseTime) {
// Early frames building up to throw
var frameIndex = Math.floor((self.throwingDuration - self.throwingTimer) / ((self.throwingDuration - self.throwingPauseTime) / 5));
frameIndex = Math.min(4, Math.max(0, frameIndex));
self.sprites[throwOffset + frameIndex].alpha = 1;
} else if (self.throwingTimer <= Math.floor(self.throwingDuration * 0.8) && self.throwingTimer > Math.floor(self.throwingDuration * 0.8) - 2 && !self.swordThrown) {
// Flag to ensure we only throw once during this window
self.swordThrown = true;
var sword = new SkeletonSword();
sword.x = self.x - 100;
sword.y = self.y - 100;
game.addChild(sword);
skeletonSwords.push(sword);
self.sprites[throwOffset + 5].alpha = 1;
} else if (self.throwingTimer <= self.throwingPauseTime) {
// During pause, keep showing the final frame
self.sprites[throwOffset + 5].alpha = 1;
} else {
// Middle of animation (after throw, before pause)
self.sprites[throwOffset + 5].alpha = 1;
}
// Decrease timer
self.throwingTimer--;
// End throwing state and restore speed
if (self.throwingTimer <= 0) {
self.isThrowing = false;
self.swordThrown = false; // Reset the flag
self.speed = self.originalSpeed || 6;
}
};
// Update skeleton normal movement
self.updateSkeletonNormalState = function () {
// Track if this is the first time we're seeing this skeleton
if (self.initialThrowMade === undefined) {
self.initialThrowMade = false;
self.enteredScreenTime = LK.ticks;
self.throwDelay = 60; // Reduce delay before first throw
}
// Check for initial throw
if (!self.initialThrowMade && self.x < GAME_WIDTH + 300 && self.isOnGround && !self.isThrowing) {
if (LK.ticks - self.enteredScreenTime > self.throwDelay) {
self.isThrowing = true;
self.swordThrown = false; // Initialize flag here
self.throwingTimer = self.throwingDuration;
self.throwingFrame = 0;
self.originalSpeed = self.speed;
self.initialThrowMade = true;
}
}
// Continue with normal movement regardless of throw state
// Move left with speed multiplier
self.x -= self.speed * gameSpeedMultiplier;
// Platform and gravity code
if (!self.isOnGround) {
self.velocityY += 0.7;
self.y += self.velocityY;
self.checkPlatformCollision();
}
// Ensure skeleton stays on platform
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;
// Check for another platform
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;
foundAnotherPlatform = true;
break;
}
}
// Start falling if no other platform found
if (!foundAnotherPlatform) {
self.isOnGround = false;
self.currentPlatform = null;
if (self.velocityY === 0) {
self.velocityY = 0.1;
}
}
}
}
// Animate walking - slower animation speed for skeleton
self.animationCounter += self.animationSpeed * 0.8;
if (self.animationCounter >= 1) {
self.animationCounter = 0;
self.runFrame = (self.runFrame + 1) % self.runAnimation.length;
}
self.sprites[self.runFrame].alpha = 1;
};
// 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();
}
// Rest of the collision code remains the same
// Add this check:
if (platform instanceof CrumblePlatform) {
platform.trigger();
}
// 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();
}
// Rest of the collision code remains the same
// Add this check here too:
if (platform instanceof CrumblePlatform) {
platform.trigger();
}
// 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';
}
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;
}
// 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 {
LK.getSound('enemyhit').play();
}
// Add particle effect
var particleOffset = self.type === 'eyeball' ? 175 : 250;
particleSystem.emitFromHit(self.x + particleOffset, self.y, player.x, verticalTrajectory, self.type);
}
};
// Hit handling
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
}
// Add particle effect
var particleOffset = self.type === 'eyeball' ? 175 : 250;
particleSystem.emitFromHit(self.x + particleOffset, self.y, player.x, verticalTrajectory, self.type);
}
};
self.dropLoot = function () {
// Don't drop collectibles during tutorial
if (tutorialActive) {
return;
}
// 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);
}
// 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);
}
};
self.isDeathAnimationComplete = function () {
return self.isDying && self.deathTimer <= 10; // Check if we're near the end of death animation
};
// Main update method
self.update = function () {
// Hide all sprites first
self.hideAllSprites();
// Handle different enemy types and states
if (self.type === 'eyeball') {
if (self.isHit || self.isDying) {
self.updateEyeballDamageState();
} else {
self.updateEyeballNormalState();
}
} else if (self.type === 'skeleton') {
// Skeleton logic
if (self.isHit) {
self.updateSkeletonHitState();
} else if (self.isDying) {
self.updateSkeletonDyingState();
} else if (self.isThrowing) {
self.updateSkeletonThrowingState();
} else {
self.updateSkeletonNormalState();
}
} else {
// Goblin logic
if (self.isHit) {
self.updateGoblinHitState();
} else if (self.isDying) {
self.updateGoblinDyingState();
} else {
self.updateGoblinNormalState();
}
}
// Destroy if off screen
if (self.x < -50 || self.y > GAME_HEIGHT) {
self.destroy();
}
};
// Initialize based on enemy type AFTER defining the methods
if (self.type === 'eyeball') {
self.initEyeballSprites();
} else if (self.type === 'goblin') {
self.initGoblinSprites();
} else if (self.type === 'skeleton') {
self.initSkeletonSprites();
} else {
// Basic enemy
var enemyGraphics = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
}
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 * lootMultiplier) {
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 * lootMultiplier) {
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'];
// 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.08;
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 = 90;
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
// 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);
}
};
// 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];
var leftEdge = platform.x - PLATFORM_HALF_WIDTH + 20;
var rightEdge = platform.x + PLATFORM_HALF_WIDTH + 20;
// 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
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;
}
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) {
// 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.7; // 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;
} 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.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.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 platforms that are actually broken (but not just crumbling)
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;
}
// Rest of the original collision detection...
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 + 60) && self.x < platform.x + (PLATFORM_HALF_WIDTH + 60)) {
// 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;
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;
// Adjust game speed multiplier instead of base speed
gameSpeedMultiplier = self.slideSpeedMultiplier;
}
};
// 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.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 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
self.applyPhysics();
// Handle platform collision and falling
self.handlePlatformCollision();
// Handle invulnerability
self.updateInvulnerability();
// Update animations based on state
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.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: 600,
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: 2500,
maxLevel: 1,
icon: 'icon_combat',
effect: function effect(level) {
console.log("Slide damage upgrade purchased: level " + level);
}
}, {
id: 'better_loot',
category: 'Loot',
title: 'Fortune Finder',
description: 'Increase chances of finding valuable loot',
basePrice: 700,
maxLevel: 4,
icon: 'icon_treasure',
effect: function effect(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');
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;
}
// 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
self.speed = 6; // Slightly slower than arrows (25)
self.rotationSpeed = -0.1; // Counterclockwise rotation
// Update method called every frame
self.update = function () {
// Move from right to left with speed adjusted by game speed multiplier
self.x -= self.speed * gameSpeedMultiplier;
// 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;
});
/****
* 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 * lootMultiplier) {
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 * lootMultiplier) {
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',
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.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;
}
// Complete tutorial when player has finished sliding and succeeded
if (self.hasSlideSucceeded && !player.isSliding) {
self.currentState = self.states.COMPLETE;
self.setMessage("That's it!\nNow go get some treasure!");
LK.setTimeout(function () {
self.complete();
}, 3000);
}
break;
}
};
// Input handling
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();
}
} 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 (deltaY > 120 && self.currentState === self.states.DROP_DOWN) {
self.dropDownInput = true;
player.fallThrough();
}
}
/*
// Additional mouse control checks like in game.move
if (Math.abs(deltaX) > Math.abs(deltaY)) {
if (deltaX > SLIDE_MOVE_THRESHOLD && self.currentState === self.states.SLIDE) {
player.slide();
} else if (deltaX < -SLIDE_MOVE_THRESHOLD && self.currentState === self.states.BOW) {
player.shoot();
}
}
// Check for downward swipe with threshold similar to game.move
if (deltaY > SLIDE_MOVE_THRESHOLD && 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();
/****
* Game Management
****/
// Base collectible behavior for items that can be collected
// Add ShopUpgrade.prototype methods here
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;
// Track timestamp
storage.lastSaveTime = Date.now();
console.log("Storage values:", {
gold: storage.gold,
health: storage.healthUpgrade,
arrow: storage.arrowUpgrade,
levitate: storage.levitateUpgrade,
slide: storage.slideUpgrade,
loot: storage.lootUpgrade
});
},
// 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;
// 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;
}
// 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.lootMultiplier = 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.slideDamage = UPGRADE_REGISTRY.slide_damage > 0;
player.lootMultiplier = 1 + UPGRADE_REGISTRY.better_loot * 0.15;
// 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;
}
};
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.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;
// Clear existing upgrades
self.upgradeList.removeChildren();
// Filter and show upgrades for this category
var categoryUpgrades = self.upgrades.filter(function (upgrade) {
return upgrade.category === category;
});
categoryUpgrades.forEach(function (upgradeConfig, index) {
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;
}
// 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 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
});
// Update buy button for the visible upgrade
self.updateBuyButton();
};
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;
// Update buy button based on max level first
if (isMaxLevel) {
self.priceText.setText('MAX');
self.priceCoinIcon.visible = false;
// Set text position consistently when showing MAX
self.priceText.x = 0; // Center the MAX text
// Optionally disable button visual
self.buyButtonBg.tint = 0x888888;
} else {
self.priceText.setText(price);
self.priceCoinIcon.visible = true;
// Fix coin icon at a consistent position
self.priceCoinIcon.x = 160; // Keep fixed position
// Right-align price text relative to the coin position
var textPadding = 10; // Space between text and coin
self.priceText.x = self.priceCoinIcon.x - textPadding - self.priceText.width;
// Set button tint based on affordability
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;
}
// 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 itemBounds = {
left: self.x - 25,
right: self.x + 25,
top: self.y - 25,
bottom: self.y + 25
};
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
};
/****
* 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;
}
}
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) {
self.score = newScore;
// Update the registry
UPGRADE_REGISTRY.gold = self.score;
if (self.scoreText) {
self.scoreText.setText(newScore);
// Keep right edge at a fixed position - same approach as updateAmmo function
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 enemySpawnInterval = 100;
var goblinSpawnCounter = 0;
var goblinSpawnInterval = 100;
var eyeballSpawnCounter = 0;
var eyeballSpawnInterval = 200;
var skeletonSpawnCounter = 0;
var skeletonSpawnInterval = 350;
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
}));
var mg2 = midgroundContainer.addChild(LK.getAsset('midground', {
anchorX: 0,
anchorY: 1
}));
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
}));
var fg2 = foregroundContainer.addChild(LK.getAsset('foreground', {
anchorX: 0,
anchorY: 1
}));
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 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;
}
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);
// 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 (!tutorialActive) {
var isPlatformOccupied = function isPlatformOccupied(platform) {
return enemies.some(function (enemy) {
return enemy.currentPlatform === platform && (enemy.type === 'goblin' || enemy.type === 'skeleton');
});
}; // Get available platforms for ground enemies
// Create helper function to check if platform is already taken
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);
});
// Goblin spawning
goblinSpawnCounter++;
if (goblinSpawnCounter >= goblinSpawnInterval && unoccupiedPlatforms.length > 0) {
var platform = unoccupiedPlatforms[Math.floor(Math.random() * unoccupiedPlatforms.length)];
var enemy = new Enemy('goblin');
enemy.x = platform.x;
enemy.y = platform.y - ENEMY_PLATFORM_OFFSET;
enemy.currentPlatform = platform;
enemies.push(enemy);
game.addChild(enemy);
goblinSpawnInterval = Math.floor(Math.random() * 150) + 150;
goblinSpawnCounter = 0;
// Remove used platform from unoccupied list
unoccupiedPlatforms = unoccupiedPlatforms.filter(function (p) {
return p !== platform;
});
}
// Skeleton spawning - only try if there are still unoccupied platforms
skeletonSpawnCounter++;
if (skeletonSpawnCounter >= skeletonSpawnInterval && unoccupiedPlatforms.length > 0) {
var platform = unoccupiedPlatforms[Math.floor(Math.random() * unoccupiedPlatforms.length)];
var enemy = new Enemy('skeleton');
enemy.x = platform.x;
enemy.y = platform.y - ENEMY_PLATFORM_OFFSET;
enemy.currentPlatform = platform;
enemies.push(enemy);
game.addChild(enemy);
skeletonSpawnInterval = Math.floor(Math.random() * 200) + 350;
skeletonSpawnCounter = 0;
}
// Eyeball spawning - updated height range
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; // Spawn anywhere between highest and lowest
enemies.push(enemy);
game.addChild(enemy);
eyeballSpawnInterval = Math.floor(Math.random() * 300) + 250;
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);
}
}
// 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 (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 (!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) {
// Right swipe - trigger slide
player.slide();
} else if (deltaX < -50) {
// Left swipe - trigger bow shot
player.shoot();
}
} else if (deltaY > 120) {
// Downward swipe
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;
/*if (Math.abs(deltaX) > Math.abs(y - touchStartY)) {
if (deltaX > SLIDE_MOVE_THRESHOLD) {
player.slide();
} else if (deltaX < -(SLIDE_MOVE_THRESHOLD + 100)) {
player.shoot();
}
}
// Check for downward swipe
var deltaY = y - touchStartY;
if (deltaY > SLIDE_MOVE_THRESHOLD) {
player.fallThrough();
}
if (lastMoveY !== 0) {
var deltaY = y - lastMoveY;
var currentTime = Date.now();
if (deltaY < -MOVE_THRESHOLD && currentTime - lastJumpTime > JUMP_COOLDOWN) {
player.jump();
lastJumpTime = currentTime;
}
}
*/
lastMoveY = y;
};
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
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