User prompt
Wait until skeleton is fully on screen before throwing.
User prompt
Skeleton should wait a little longer after getting on screen to throw. And remove the random chance for a second throw.
User prompt
Skeleton sword is affected by speedmultiplet
User prompt
Skeletons should throw their sword soon after getting on screen then more of a random chance after the initial throw.
User prompt
Have the skeleton throw their sword event earlier during the animation.
User prompt
The skeleton should throw their sword sooner during the animation.
User prompt
During skeleton throwing animation, their x speed should match the platforms.
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'skeletonSwords[i].destroyed')' in or related to this line: 'if (skeletonSwords[i].destroyed) {' Line Number: 3474
User prompt
update as needed with: self.updateSkeletonThrowingState = function () { var throwOffset = self.runAnimation.length + self.hitAnimation.length + self.dieAnimation.length; if (self.throwingTimer === Math.floor(self.throwingDuration / 2)) { // At mid-point, throw the sword var sword = new SkeletonSword(); sword.x = self.x - 50; sword.y = self.y - 50; game.addChild(sword); skeletonSwords.push(sword); // Play a sound effect if you have one // LK.getSound('swordthrow').play(); } // Decrease timer self.throwingTimer--; // End throwing state and restore speed if (self.throwingTimer <= 0) { self.isThrowing = false; self.speed = self.originalSpeed || 5; // Restore original speed } };
User prompt
update with: self.updateSkeletonNormalState = function () { // Random chance to start throwing - only try if not already throwing if (!self.isThrowing && Math.random() < 0.005 && self.isOnGround) { self.isThrowing = true; self.throwingTimer = self.throwingDuration; self.throwingFrame = 0; self.originalSpeed = self.speed; self.speed = 2; // Slow down more during throw return; } // Move left with speed multiplier self.x -= self.speed * gameSpeedMultiplier;
Code edit (1 edits merged)
Please save this source code
User prompt
update with: self.updateSkeletonThrowingState = function () { var throwOffset = self.runAnimation.length + self.hitAnimation.length + self.dieAnimation.length; if (self.throwingTimer === Math.floor(self.throwingDuration / 2)) { // At mid-point, throw the sword var sword = new SkeletonSword(); sword.x = self.x - 50; sword.y = self.y - 50; game.addChild(sword); // Add sword to global array skeletonSwords.push(sword); // Add this line } };
Code edit (1 edits merged)
Please save this source code
User prompt
Create an animation for the skeleton that uses skeletonthrow1-6. During this phase, the skeleton will slow down to 5, perform the animation, pause for split second and then resume walking at normal speed. The skeleton will throw a skeleton sword during this animation phase. Only complete all the code in the enemy class for now, do not add anything to main game updates. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Create a skeletonsword class that travels at a slightly slower speed than arrows, travels right to left and rotates counterclockwise as it travels. Destroy it when it leaves the screen. Just create this for now, nothing else.
Code edit (1 edits merged)
Please save this source code
User prompt
Do not tint the particles for the skeleton enemy type when it gets hit.
Code edit (1 edits merged)
Please save this source code
User prompt
In player.applyPhysics, instead of calling the gameover immediately, let the player continue to fall all the way offscreen. Then set gamespeedmultiplier to 0, stop music, play gameover sound effect and call the gameover in a 3 second timeout.
User prompt
When the player falls down the pit for the game over, take the player completely offscreen, stop the music, play the game over sound effect and put the gameover call in a 3 second timeout ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
In the tutorial checkInput also add additional control checks as outlined in game.move for mouse users.
Code edit (1 edits merged)
Please save this source code
/**** * Plugins ****/ var tween = LK.import("@upit/tween.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; }); // 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; 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 = 5.5; // Slower than goblin // Animation frames self.runAnimation = ['skeletonwalk1', 'skeletonwalk2', 'skeletonwalk3', 'skeletonwalk4']; self.hitAnimation = ['skeletonhit1', 'skeletonhit2']; self.dieAnimation = ['skeletondie1', 'skeletondie2', 'skeletondie3', 'skeletondie4']; } // 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); } }; // 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; // 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') { // Only start death animation if it was a regular attack self.isDying = true; self.deathTimer = 60; self.deathFrame = 0; // Add dropLoot here when transitioning to dying state 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; // 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') { // Only start death animation if it was a regular attack self.isDying = true; self.deathTimer = 60; self.deathFrame = 0; // Add dropLoot here when transitioning to dying state self.dropLoot(); } self.hitType = 'none'; } }; // Update goblin dying animation self.updateGoblinDyingState = function () { // Continue throw back during death self.x += self.throwBackSpeed; // 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; // 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 normal movement self.updateSkeletonNormalState = 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 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; // Check if we're on a platform (exact position check) if (self.x >= leftEdge && self.x <= rightEdge) { if (Math.abs(self.y - (platform.y - ENEMY_PLATFORM_OFFSET)) < 5) { onAnyPlatform = true; self.currentPlatform = platform; self.y = platform.y - ENEMY_PLATFORM_OFFSET; self.isOnGround = true; self.velocityY = 0; return true; } // Check for passing through platform during fall if (self.velocityY > 0 && self.y < platform.y - ENEMY_PLATFORM_OFFSET && self.y + self.velocityY >= platform.y - ENEMY_PLATFORM_OFFSET) { 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; self.hitType = 'slide'; // Specify this is a slide hit 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') { 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 full vertical trajectory when hit with no reduction self.velocityY = verticalTrajectory; // Set the velocity for the eyeball } // Add particle effect var particleOffset = self.type === 'eyeball' ? 175 : 250; particleSystem.emitFromHit(self.x + particleOffset, self.y, player.x, verticalTrajectory); } }; // 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.dropLoot = function () { // Don't drop collectibles during tutorial if (tutorialActive) { return; } // Determine number of coins to drop (1-5) var coinCount = Math.floor(GameUtils.randomRange(1, 6)); // Small chance for jewels (2% each) var dropJewel = Math.random(); if (dropJewel < 0.02) { // 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 < 0.04) { // 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 < 0.06) { // 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 { 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; 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) { // Spawn health potion with low chance if (Math.random() < 0.05) { 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 (10%) if (Math.random() < 0.25) { 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 var coinCount = Math.floor(GameUtils.randomRange(1, 9)); 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) { 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; particle.tint = self.redTints[Math.floor(Math.random() * self.redTints.length)]; // 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; }); // 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.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; 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) { 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 () { // Apply gravity and velocity to position if (!self.isOnGround || self.isJumping) { // Apply velocity and gravity self.y += self.velocityY; self.velocityY += 0.7; // Check for landing on platforms var hitPlatform = self.checkPlatformCollision(); // REMOVE THE GROUND CHECK - No invisible ground at the bottom! // We only want to land on actual platforms } // 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 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 () { for (var i = 0; i < platforms.length; i++) { var platform = platforms[i]; // Increase vertical tolerance from 15 to 20 pixels // Add larger horizontal buffer (30 pixels) on each side for more forgiving landings 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)) { // Snap to platform more precisely 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 () { 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); // Add screen shake effect applyScreenShake(); if (self.currentHealth <= 0) { // 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 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 self.text = new Text2('+' + amount, { size: 80, fill: 0xFFFFFF, anchorX: 0.5, anchorY: 0.5 }); 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; }); /**** * 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) { // Spawn health potion with medium chance if (Math.random() < 0.25) { 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 (25%) if (Math.random() < 0.40) { var arrowCount = Math.floor(GameUtils.randomRange(1, 4)); 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 var totalItems = Math.floor(GameUtils.randomRange(3, 9)); for (var i = 0; i < totalItems; i++) { // Random chance for different gems var rand = Math.random(); var item; if (rand < 0.05) { // 5% diamond item = new Coin('diamond'); } else if (rand < 0.15) { // 10% emerald item = new Coin('emerald'); } else if (rand < 0.30) { // 15% ruby item = new Coin('ruby'); } else { // 70% gold coin 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', 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 Text2('', { size: 100, fill: 0xFFFFFF, anchorX: 0.5, anchorY: 0.5, align: 'center', width: GAME_WIDTH }); self.messageContainer.addChild(self.messageText); 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 () { // 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") { self.complete(); } }; self.backButton.down = self.backButtonDown; // Clear any existing enemies/arrows enemies = []; arrows = []; self.spawnTutorialEnemy(); }; // Message handling self.setMessage = function (text) { self.messageText.setText(text); // Calculate width of text and offset by half to center it var textWidth = self.messageText.width; self.messageText.x = -textWidth / 2; 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.currentState = self.states.SLIDE; self.setMessage("Swipe right to slide"); // Reset jump tracking self.firstJumpPerformed = false; self.secondJumpPerformed = false; } break; case self.states.SLIDE: if (player.isSliding) { self.currentState = self.states.COMPLETE; self.setMessage("Tutorial Complete!"); LK.setTimeout(function () { self.complete(); }, 2000); } break; } }; // Input handling self.checkInput = function (startX, startY, endX, endY) { if (!gameStarted || player.isDying) { return; } 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(); } return; } // 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 if (self.currentState === self.states.JUMP) { self.hasJumped = true; player.jump(); } else if (self.currentState === self.states.DOUBLE_JUMP) { self.hasDoubleJumped = true; player.jump(); } } 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; } } } } // 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.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 ****/ // Base collectible behavior for items that can be collected /**** * Game Management ****/ 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; /**** * Utilities ****/ 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 and UI self.score = 0; self.scoreText = new Text2('0', { size: 160, fill: 0xFFFFFF, anchorX: 1, anchorY: 0.5 }); self.coinIcon = LK.getAsset('coin', { anchorX: 0, anchorY: 0.5, scaleX: 2, scaleY: 2 }); // New ammo display self.ammoText = new Text2('0x', { size: 160, fill: 0xFFFFFF, anchorX: 1, anchorY: 0.5 }); self.ammoIcon = LK.getAsset('arrow', { anchorX: 0, anchorY: 0.5, scaleX: 1.4, scaleY: 1.4, 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 self.scoreText.x = -100; self.scoreText.y = -100; self.coinIcon.x = 0; self.coinIcon.y = 0; self.ammoText.x = -150; self.ammoText.y = 60; // Place below coin display self.ammoIcon.x = 55; self.ammoIcon.y = 220; // Place below coin display // Add ammo update method self.updateAmmo = function (amount) { self.ammoText.setText(amount + 'x'); // Adjust position based on digit count if (amount >= 10) { self.ammoText.x = -200; } else { self.ammoText.x = -150; } }; // Getter and setter for score self.getScore = function () { return self.score; }; self.setScore = function (newScore) { self.score = newScore; self.scoreText.setText(newScore); // Adjust position based on digit count if (newScore >= 10 && newScore < 100) { self.scoreText.x = -200; } else if (newScore >= 100 && newScore < 1000) { self.scoreText.x = -300; } else if (newScore >= 1000) { self.scoreText.x = -400; } else { self.scoreText.x = -100; } }; // Add score and display popup self.addScore = function (amount, x, y) { self.setScore(self.score + amount); 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); } }; 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 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(); 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); } // 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 / 2; tutorialButton.y = GAME_HEIGHT / 1.15; tween(tutorialButton, { 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(); } // 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 player = game.addChild(new Player()); player.x = GAME_WIDTH / 4.5; player.y = GAME_HEIGHT / 1.5 - 100; game.addChild(heartContainer); 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 scoreManager = new ScoreManager(); 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: 1000 } }); } // 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; if (currentPlatformPattern === 'C') { // Single platform for straight sections, using stored height var 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 = new Platform(); var 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; }); // 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; }); 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) + 100; 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) + 200; eyeballSpawnCounter = 0; } } // Update enemies and check collisions for (var j = enemies.length - 1; j >= 0; j--) { enemies[j].update(); // Check if enemy has been destroyed if (enemies[j].destroyed) { enemies.splice(j, 1); continue; } // Skip if enemy is far behind player if (enemies[j].x < player.x - 100) { continue; } var playerBounds = player.getCollisionBounds(); var enemyBounds = enemies[j].getBounds(); var attackBounds = player.getAttackBounds(); var slideAttackBounds = player.getSlideAttackBounds(); // Check for attack collision first if (attackBounds && !enemies[j].isHit && !enemies[j].isDying) { if (enemies[j].x > player.x && GameUtils.checkCollision(attackBounds, enemyBounds)) { enemies[j].hit(); if (enemies[j].type === 'eyeball') { LK.getSound('eyeballhit').play(); } else if (enemies[j].type === 'skeleton') { LK.getSound('skeletonhit').play(); } else { LK.getSound('enemyhit').play(); } continue; } } // In the updateEnemies() function, inside the enemy update loop for (var i = arrows.length - 1; i >= 0; i--) { var arrow = arrows[i]; if (GameUtils.checkCollision(arrow.getBounds(), enemyBounds)) { // Only hit the enemy and destroy arrow if enemy is not already hit or dying if (!enemies[j].isHit && !enemies[j].isDying) { enemies[j].hit(); if (enemies[j].type === 'eyeball') { LK.getSound('eyeballhit').play(); } else if (enemies[j].type === 'skeleton') { LK.getSound('skeletonhit').play; } else { LK.getSound('enemyhit').play(); } // Only destroy the arrow if it actually damages an enemy arrow.destroy(); arrows.splice(i, 1); break; } // Arrows pass through already hit/dying enemies } } // 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(); } } } } // 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) { 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); } } 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) { 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) { startGame(); } else if (x >= tutorialButtonBounds.left && x <= tutorialButtonBounds.right && y >= tutorialButtonBounds.top && y <= tutorialButtonBounds.bottom) { startTutorial(); } return; } // Prevent input if player is dying or air dying if (player && (player.isDying || player.dyingInAir)) { return; } 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 > 70) { // Right swipe - trigger slide player.slide(); } else if (deltaX < -70) { // 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) { 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; };
===================================================================
--- original.js
+++ change.js
@@ -174,8 +174,16 @@
// 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 = 5.5; // Slower than goblin
+ // Animation frames
+ self.runAnimation = ['skeletonwalk1', 'skeletonwalk2', 'skeletonwalk3', 'skeletonwalk4'];
+ self.hitAnimation = ['skeletonhit1', 'skeletonhit2'];
+ self.dieAnimation = ['skeletondie1', 'skeletondie2', 'skeletondie3', 'skeletondie4'];
}
// Initialize animation sprites for eyeball
self.initEyeballSprites = function () {
// Add fly animations
@@ -235,8 +243,38 @@
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);
+ }
+ };
// Hide all animation sprites
self.hideAllSprites = function () {
for (var i = 0; i < self.sprites.length; i++) {
self.sprites[i].alpha = 0;
@@ -346,8 +384,34 @@
}
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;
+ // 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') {
+ // Only start death animation if it was a regular attack
+ self.isDying = true;
+ self.deathTimer = 60;
+ self.deathFrame = 0;
+ // Add dropLoot here when transitioning to dying state
+ self.dropLoot();
+ }
+ self.hitType = 'none';
+ }
+ };
// Update goblin dying animation
self.updateGoblinDyingState = function () {
// Continue throw back during death
self.x += self.throwBackSpeed;
@@ -372,8 +436,34 @@
self.destroy();
}
}
};
+ // Update skeleton dying animation
+ self.updateSkeletonDyingState = function () {
+ // Continue throw back during death
+ self.x += self.throwBackSpeed;
+ // 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;
@@ -417,8 +507,53 @@
self.runFrame = (self.runFrame + 1) % self.runAnimation.length;
}
self.sprites[self.runFrame].alpha = 1;
};
+ // Update skeleton normal movement
+ self.updateSkeletonNormalState = 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 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++) {
@@ -584,8 +719,17 @@
self.updateEyeballDamageState();
} else {
self.updateEyeballNormalState();
}
+ } else if (self.type === 'skeleton') {
+ // Skeleton logic
+ if (self.isHit) {
+ self.updateSkeletonHitState();
+ } else if (self.isDying) {
+ self.updateSkeletonDyingState();
+ } else {
+ self.updateSkeletonNormalState();
+ }
} else {
// Goblin logic
if (self.isHit) {
self.updateGoblinHitState();
@@ -604,8 +748,10 @@
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,
@@ -2191,12 +2337,12 @@
/****
* Game Code
****/
+// Base collectible behavior for items that can be collected
/****
* Game Management
****/
-// Base collectible behavior for items that can be collected
var CollectibleBehavior = {
initPhysics: function initPhysics(self) {
self.velocityX = 0;
self.velocityY = 0;
@@ -2503,10 +2649,12 @@
var enemies = [];
var enemySpawnInterval = 100;
var goblinSpawnCounter = 0;
var goblinSpawnInterval = 100;
-var eyeballSpawnCounter = 150;
-var eyeballSpawnInterval = 500;
+var eyeballSpawnCounter = 0;
+var eyeballSpawnInterval = 200;
+var skeletonSpawnCounter = 0;
+var skeletonSpawnInterval = 350;
var particleSystem;
var heartContainer = new HeartContainer();
var player;
// Background elements
@@ -2960,28 +3108,51 @@
}
// 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) {
- var availablePlatforms = platforms.filter(function (p) {
- return p.x > GAME_WIDTH - 100 && p.x < GAME_WIDTH + 300;
+ 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) + 100;
+ goblinSpawnCounter = 0;
+ // Remove used platform from unoccupied list
+ unoccupiedPlatforms = unoccupiedPlatforms.filter(function (p) {
+ return p !== platform;
});
- if (availablePlatforms.length > 0) {
- var enemy = new Enemy('goblin');
- var platform = availablePlatforms[Math.floor(Math.random() * availablePlatforms.length)];
- 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) + 100;
- goblinSpawnCounter = 0;
- } else {
- goblinSpawnCounter = Math.max(0, goblinSpawnCounter - 20);
- }
}
+ // 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');
@@ -2990,9 +3161,9 @@
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) + 150;
+ eyeballSpawnInterval = Math.floor(Math.random() * 300) + 200;
eyeballSpawnCounter = 0;
}
}
// Update enemies and check collisions
@@ -3016,8 +3187,10 @@
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;
@@ -3031,8 +3204,10 @@
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
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
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