/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); var facekit = LK.import("@upit/facekit.v1"); /**** * Classes ****/ var Boss = Container.expand(function () { var self = Container.call(this); // States var ENTERING = 'entering'; var WALKING = 'walking'; var ARROW_ATTACK = 'arrow_attack'; var CHARGING_LEFT = 'charging_left'; var CHARGING_RIGHT = 'charging_right'; var FIST_LEFT = 'fist_left'; var FIST_RIGHT = 'fist_right'; var RETRACTING_LEFT = 'retracting_left'; var RETRACTING_RIGHT = 'retracting_right'; var DYING = 'dying'; // All boss sprites var normalBody = self.attachAsset('boss1', { anchorX: 0.5, anchorY: 0, alpha: 1 }); var leftBody = self.attachAsset('boss1left', { anchorX: 0.5, anchorY: 0, alpha: 0 }); var rightBody = self.attachAsset('boss1right', { anchorX: 0.5, anchorY: 0, alpha: 0 }); var leftNoFist = self.attachAsset('boss1leftnofist', { anchorX: 0.5, anchorY: 0, alpha: 0 }); var rightNoFist = self.attachAsset('boss1rightnofist', { anchorX: 0.5, anchorY: 0, alpha: 0 }); // Properties self.health = 100; self.state = ENTERING; self.hasCompletedFirstAttack = false; self.stateDuration = 0; self.bobSpeed = 0.1; self.bobOffset = 0; self.bobBaseY = 2732 / 1.35; // Add base Y position self.activeFist = null; self.attackTimer = 0; self.attackDelay = 120; self.hasPlayedDeathSound = false; // Initial position self.x = 2048 / 2; self.y = 2732; function setBodyState(state) { normalBody.alpha = 0; leftBody.alpha = 0; rightBody.alpha = 0; leftNoFist.alpha = 0; rightNoFist.alpha = 0; switch (state) { case 'normal': normalBody.alpha = 1; break; case 'left': leftBody.alpha = 1; break; case 'right': rightBody.alpha = 1; break; case 'leftNoFist': leftNoFist.alpha = 1; break; case 'rightNoFist': rightNoFist.alpha = 1; break; } } function flashDamage() { [normalBody, leftBody, rightBody, leftNoFist, rightNoFist].forEach(function (sprite) { if (sprite.alpha > 0) { sprite.tint = 0xFF0000; LK.setTimeout(function () { sprite.tint = 0xFFFFFF; }, 5); } }); } self.update = function () { self.stateDuration++; self.bobOffset = Math.sin(LK.ticks * self.bobSpeed) * 15; switch (self.state) { case ENTERING: setBodyState('normal'); self.y -= 2; if (self.y <= 2732 / 1.35) { self.state = WALKING; self.attackTimer = self.attackDelay; } break; case WALKING: setBodyState('normal'); self.bobOffset = Math.sin(LK.ticks * self.bobSpeed) * 15; self.y = self.bobBaseY + self.bobOffset; self.attackTimer--; if (self.attackTimer <= 0) { if (!self.hasCompletedFirstAttack) { self.state = ARROW_ATTACK; self.hasCompletedFirstAttack = true; } else { var rand = Math.random(); if (rand < 0.6) { self.state = ARROW_ATTACK; } else { self.state = Math.random() < 0.5 ? CHARGING_LEFT : CHARGING_RIGHT; } } self.stateDuration = 0; } break; case ARROW_ATTACK: // Keep bobbing motion during arrow attack self.bobOffset = Math.sin(LK.ticks * self.bobSpeed) * 15; self.y = self.bobBaseY + self.bobOffset; if (self.stateDuration === 1) { LK.getSound('arrow').play(); // Play arrow sound effect var centerY = self.y + 200; enemyArrowPool.spawn(self.x, centerY).activate(self.x, centerY, dragon.x, dragon.y); enemyArrowPool.spawn(self.x - 100, centerY).activate(self.x - 100, centerY, dragon.x - 300, dragon.y); enemyArrowPool.spawn(self.x + 100, centerY).activate(self.x + 100, centerY, dragon.x + 300, dragon.y); } else if (self.stateDuration > 60) { self.state = WALKING; self.attackTimer = self.attackDelay; self.stateDuration = 0; } break; case CHARGING_LEFT: case CHARGING_RIGHT: var isLeft = self.state === CHARGING_LEFT; setBodyState(isLeft ? 'left' : 'right'); if (self.stateDuration < 70) { var targetX = self.x + (isLeft ? -200 : 200); var targetY = self.y + 300; if (Math.random() < 0.3) { var angle = Math.random() * Math.PI * 2; var radius = Math.random() * 200 + 100; var px = targetX + Math.cos(angle) * radius; var py = targetY + Math.sin(angle) * radius; var particle = particlePool.spawn(px, py); if (particle) { var dx = targetX - px; var dy = targetY - py; var dist = Math.sqrt(dx * dx + dy * dy); particle.vx = dx / dist * 10; particle.vy = dy / dist * 10; } } } else { if (self.activeFist) { bossFistPool.recycle(self.activeFist); self.activeFist = null; } self.state = isLeft ? FIST_LEFT : FIST_RIGHT; self.stateDuration = 0; setBodyState(isLeft ? 'leftNoFist' : 'rightNoFist'); self.activeFist = bossFistPool.spawn(self.x + (isLeft ? -200 : 200), self.y + 300, isLeft); if (self.activeFist) { self.activeFist.extend(dragon.x, dragon.y); LK.getSound('bossPunch').play(); } } break; case FIST_LEFT: case FIST_RIGHT: if (!self.activeFist || !self.activeFist.visible) { // If fist is somehow missing, recover setBodyState('normal'); self.state = WALKING; self.attackTimer = self.attackDelay; self.stateDuration = 0; break; } if (self.activeFist && !self.activeFist.extending) { self.state = self.state === FIST_LEFT ? RETRACTING_LEFT : RETRACTING_RIGHT; self.stateDuration = 0; if (self.activeFist) { self.activeFist.retract(); } } break; case RETRACTING_LEFT: case RETRACTING_RIGHT: if (!self.activeFist || !self.activeFist.visible) { setBodyState('normal'); self.state = WALKING; self.attackTimer = self.attackDelay; self.stateDuration = 0; if (self.activeFist) { bossFistPool.recycle(self.activeFist); // Add this line } self.activeFist = null; // Add this line to clear the reference } break; case DYING: self.y += 1; self.x += Math.sin(self.stateDuration * 0.2) * 5; // More frequent and spread out particles if (self.stateDuration % 10 === 0) { for (var i = 0; i < 5; i++) { defeatParticlePool.spawn(self.x + (Math.random() * 800 - 400), self.y + Math.random() * 800, 0xff0000); } } // Random explosions every second if (self.stateDuration % 60 === 0) { var explosionX = self.x + (Math.random() * 600 - 300); var explosionY = self.y + (Math.random() * 600 - 300); var explosion = LK.getAsset('explosion', { anchorX: 0.5, anchorY: 0.5, x: explosionX, y: explosionY, scaleX: 0.5, //{39} // Start bigger scaleY: 0.5, alpha: 1 }); game.addChild(explosion); LK.getSound('explosion').play(); tween(explosion, { scaleX: 4, //{3g} // End much bigger scaleY: 4, alpha: 0 }, { duration: 500, //{3k} // Faster animation easing: tween.easeOut, onFinish: function onFinish() { game.removeChild(explosion); } }); } if (self.y > 2732 + 200) { //{3r} // Reduced height check game.removeChild(self); bossHealthBar.visible = false; if (!self.hasPlayedDeathSound) { LK.getSound('dragonScream').play(); self.hasPlayedDeathSound = true; } LK.setTimeout(function () { LK.showYouWin(); }, 3000); } break; } self.lastBobOffset = self.bobOffset; }; self.damage = function (amount) { self.health -= amount; bossHealthBar.updateHealth(self.health / 100); flashDamage(); if (self.health <= 0 && self.state !== DYING) { self.state = DYING; self.stateDuration = 0; bossHealthBar.hide(); // Add boss defeat score score += 5000; scorePopupPool.spawn(self.x, self.y, 5000); scoreTxt.setText(score); updateScoreMeter(score); LK.setScore(score); tween(game, { x: game.x + 20 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { game.x = 0; } }); } }; return self; }); var BossFist = Container.expand(function () { var self = Container.call(this); // Create both fist sprites and manage visibility var leftFist = self.attachAsset('boss1leftfist', { anchorX: 0.5, anchorY: 0.5, visible: false, rotation: -Math.PI / 12 // 15 degrees counterclockwise }); var rightFist = self.attachAsset('boss1rightfist', { anchorX: 0.5, anchorY: 0.5, visible: false, rotation: Math.PI / 12 // 15 degrees clockwise }); self.activate = function (x, y, isLeft) { self.x = x; self.y = y; self.startX = x; self.startY = y; self.isLeft = isLeft; self.visible = true; self.extending = false; self.retracting = false; // Show correct fist, hide other leftFist.visible = isLeft; rightFist.visible = !isLeft; }; self.extend = function (targetX, targetY) { self.targetX = targetX; self.targetY = targetY; self.extending = true; self.retracting = false; }; self.update = function () { if (!self.visible) { return; } if (self.extending || self.retracting) { var dx = self.targetX - self.x; var dy = self.targetY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > self.speed) { self.x += dx / dist * self.speed; self.y += dy / dist * self.speed; // Update chain segments var segmentSpacing = 40; var numSegments = Math.floor(dist / segmentSpacing); // Clean up old segments while (self.chainSegments.length > 0) { chainSegmentPool.recycle(self.chainSegments.pop()); } // Create new segments for (var i = 1; i <= numSegments; i++) { var t = i / numSegments; var segX = self.startX + (self.x - self.startX) * t; var segY = self.startY + (self.y - self.startY) * t; var segment = chainSegmentPool.spawn(segX, segY); if (segment) { var angle = Math.atan2(dy, dx); segment.rotation = angle; self.chainSegments.push(segment); } } } else { if (self.extending) { self.extending = false; // Pause briefly at extension LK.setTimeout(function () { self.retract(); }, 30); } else if (self.retracting) { self.deactivate(); } } } }; self.retract = function () { self.targetX = self.startX; self.targetY = self.startY; self.extending = false; self.retracting = true; }; self.deactivate = function () { while (self.chainSegments.length > 0) { chainSegmentPool.recycle(self.chainSegments.pop()); } self.visible = false; self.extending = false; self.retracting = false; leftFist.visible = false; rightFist.visible = false; }; // Initialize properties self.chainSegments = []; self.speed = 15; return self; }); // In the BossHealthBar class, add position setting // Modify the BossHealthBar class var BossHealthBar = Container.expand(function () { var self = Container.call(this); var emptyMeter = self.attachAsset('meter', { anchorX: 0.5, anchorY: 1.0, tint: 0x333333, // Dark gray for empty scaleX: 0 // Start with zero width }); var healthMeter = self.attachAsset('meter', { anchorX: 0.5, anchorY: 1.0, tint: 0xff0000, // Red for health scaleX: 0 // Start with zero width }); // Position settings self.x = 2048 / 2; // Center horizontally self.y = 2732 - 100; // Position from the top self.show = function () { self.visible = true; // Reset both meters to zero width healthMeter.scaleX = 0; if (self.currentTween) { self.currentTween.stop(); } // First animate the empty meter self.currentTween = tween(emptyMeter, { scaleX: 1 }, { duration: 800, easing: tween.easeOutBack, onFinish: function onFinish() { // Then animate the health meter self.currentTween = tween(healthMeter, { scaleX: 1 }, { duration: 400, easing: tween.easeOut }); } }); }; self.hide = function () { if (self.currentTween) { self.currentTween.stop(); } // First shrink the health meter self.currentTween = tween(healthMeter, { scaleX: 0 }, { duration: 400, easing: tween.easeIn, onFinish: function onFinish() { // Then shrink the empty meter self.currentTween = tween(emptyMeter, { scaleX: 0 }, { duration: 800, easing: tween.easeIn, onFinish: function onFinish() { self.visible = false; } }); } }); }; self.updateHealth = function (percent) { // Only animate the health meter, not the empty meter if (self.healthTween) { self.healthTween.stop(); } self.healthTween = tween(healthMeter, { scaleX: percent }, { duration: 300, easing: tween.easeOut }); }; return self; }); var BossText = Container.expand(function () { var self = Container.call(this); var text = self.attachAsset('bosstext', { anchorX: 0.5, anchorY: 0.5, alpha: 0 }); self.x = 2048 / 2; // Center horizontally self.y = 2732 / 2; // Center vertically self.flashCount = 0; self.maxFlashes = 3; self.flash = function () { self.flashCount = 0; self.doFlash(); }; self.doFlash = function () { if (self.flashCount >= self.maxFlashes) { text.alpha = 0; return; } if (self.currentTween) { self.currentTween.stop(); } LK.getSound('alert').play(); // Fade in self.currentTween = tween(text, { alpha: 1, scaleX: 1.2, scaleY: 1.2 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { // Hold LK.setTimeout(function () { // Fade out self.currentTween = tween(text, { alpha: 0, scaleX: 1, scaleY: 1 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { self.flashCount++; // Wait before next flash LK.setTimeout(function () { self.doFlash(); }, 20); } }); }, 40); } }); }; return self; }); var ChainSegment = Container.expand(function () { var self = Container.call(this); var link = self.attachAsset('fistlink', { anchorX: 0.5, anchorY: 0.5 }); self.activate = function (x, y, rotation) { self.x = x; self.y = y; self.rotation = rotation; self.visible = true; }; self.deactivate = function () { self.visible = false; }; return self; }); var Cloud = Container.expand(function () { var self = Container.call(this); var cloud = self.attachAsset('cloudShape', { anchorX: 0.5, anchorY: 0.5 }); self.activate = function (x, y, scale, alpha) { self.x = x; self.y = y; self.visible = true; self.speedY = 1 + Math.random() * 2; self.speedX = (Math.random() - 0.5) * 1.5; self.targetY = self.horizonY; cloud.scaleX = scale; cloud.scaleY = scale * 0.8; cloud.alpha = alpha || 0.8; }; self.deactivate = function () { self.visible = false; }; return self; }); var DefeatParticle = Container.expand(function () { var self = Container.call(this); var particle = self.attachAsset('particle', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.15, scaleY: 0.15 }); self.activate = function (x, y, tint) { self.x = x + (Math.random() * 40 - 20); // Wider initial spread self.y = y + (Math.random() * 40 - 20); // Wider initial spread self.visible = true; self.age = 0; self.lifespan = 225; // Tripled lifespan for longer visual impact // More dramatic explosion velocities var angle = Math.PI * 2 * Math.random(); var speed = 15 + Math.random() * 25; // Adjusted speed for more variance self.vx = Math.cos(angle) * speed; self.vy = Math.sin(angle) * speed; self.rotation = Math.random() * Math.PI * 2; particle.alpha = 1; // Larger, more varied particle sizes var scaleVariance = 0.3 + Math.random() * 0.6; // Increased variance for scale particle.scaleX = scaleVariance; particle.scaleY = scaleVariance; // Apply tint if provided if (tint !== undefined) { particle.tint = tint; } else { particle.tint = 0xffffff; // Default white } }; self.update = function () { // Check bounds first if (self.y < -50 || self.y > 2732 || self.x < -50 || self.x > 2048) { defeatParticlePool.recycle(self); return; } self.x += self.vx; self.y += self.vy; self.age++; // Keep strong deceleration self.vx *= 0.87; self.vy *= 0.87; var lifePercent = self.age / self.lifespan; // Fade out curve particle.alpha = Math.max(0, 1 - lifePercent * 1.7); // More dramatic shrink effect as particles fade var scale = (0.5 - 0.3 * lifePercent) * (1 - lifePercent * 0.5); particle.scaleX = scale; particle.scaleY = scale; if (lifePercent >= 1) { defeatParticlePool.recycle(self); } }; self.deactivate = function () { self.visible = false; // Reset tint when deactivated particle.tint = 0xffffff; }; return self; }); var DragonHead = Container.expand(function () { var self = Container.call(this); // Dragon sprites var body = self.attachAsset('dragonBody', { anchorX: 0.5, anchorY: 0.7, scaleX: 2, scaleY: 2 }); var head = self.attachAsset('dragonHead', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2 }); var headOpen = self.attachAsset('dragonHeadOpen', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2, alpha: 0 }); var lookLeft = self.attachAsset('dragonLook', { anchorX: 0.5, anchorY: 0.5, scaleX: -2, // Mirror for left direction scaleY: 2, alpha: 0 }); var lookRight = self.attachAsset('dragonLook', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2, alpha: 0 }); var lookLeftOpen = self.attachAsset('dragonLookOpen', { anchorX: 0.5, anchorY: 0.5, scaleX: -2, // Mirror for left direction scaleY: 2, alpha: 0 }); var lookRightOpen = self.attachAsset('dragonLookOpen', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2, alpha: 0 }); self.stateBuffer = []; self.bufferSize = 10; // Adjust 3-5 frames for balance between stability and responsiveness self.currentState = 'forward'; // Add smoothing properties self.mouthOpenness = 0; // Track mouth state from 0 (closed) to 1 (open) self.mouthSmoothingSpeed = 0.2; // Adjust this value to change transition speed self.currentScale = 2; // Starting scale var horizontalSpeed = 0; self.invulnerableTime = 0; self.invulnerableDuration = 30; // Position tracking variables var targetX = 2048 / 2; var targetY = 2732 * 0.2; var smoothingFactor = 0.12; var prevX = null; var prevY = null; // Rotation variables var targetTilt = 0; var tiltSmoothingFactor = 0.11; var tiltScaleFactor = 0.09; // Scale tracking var scaleHistory = new Array(5).fill(2); // Start with default scale of 2 var scaleIndex = 0; self.update = function () { // Add in self.update function if (self.invulnerableTime > 0) { self.invulnerableTime--; } // Position tracking if (facekit.noseTip) { targetX = facekit.noseTip.x; targetY = facekit.noseTip.y - 800; // Initialize previous positions if not set if (prevX === null) { prevX = targetX; prevY = targetY; } // Weighted average between previous and target position var newX = prevX * (1 - smoothingFactor) + targetX * smoothingFactor; var newY = prevY * (1 - smoothingFactor) + targetY * smoothingFactor; self.x = newX; if (newY > dragonShadow.y - 100) { // -100 for visual offset so dragon stays above shadow newY = dragonShadow.y - 100; } self.y = newY; if (prevX !== null) { horizontalSpeed = Math.abs(self.x - prevX); } self.currentShakeSpeed = horizontalSpeed; // Update previous positions prevX = newX; prevY = newY; } // Rotation tracking - CORRECTED VERSION if (facekit.leftEye && facekit.rightEye) { targetTilt = calculateFaceTilt() * tiltScaleFactor; // Limit rotation to ±15 degrees - DON'T convert to radians here targetTilt = Math.max(-15, Math.min(15, targetTilt)); self.rotation += (targetTilt - self.rotation) * tiltSmoothingFactor; } if (facekit.leftEye && facekit.rightEye && facekit.noseTip) { // Calculate face direction var faceCenter = (facekit.leftEye.x + facekit.rightEye.x) / 2; var noseDiff = facekit.noseTip.x - faceCenter; var eyeYDiff = facekit.rightEye.y - facekit.leftEye.y; var newState = 'forward'; if (noseDiff < -120 && Math.abs(eyeYDiff) < 30 || eyeYDiff > 50) { newState = 'left'; } else if (noseDiff > 120 && Math.abs(eyeYDiff) < 30 || eyeYDiff < -50) { newState = 'right'; } // Add to buffer self.stateBuffer.push(newState); if (self.stateBuffer.length > self.bufferSize) { self.stateBuffer.shift(); } // Only change state if buffer is unanimous if (self.stateBuffer.length === self.bufferSize) { var unanimous = self.stateBuffer.every(function (state) { return state === self.stateBuffer[0]; }); if (unanimous) { self.currentState = self.stateBuffer[0]; } } // Reset visibilities based on current state lookLeft.alpha = 0; lookRight.alpha = 0; lookLeftOpen.alpha = 0; lookRightOpen.alpha = 0; head.alpha = 0; headOpen.alpha = 0; if (self.currentState === 'forward') { if (facekit.mouthOpen) { headOpen.alpha = 1; } else { head.alpha = 1; } } else if (self.currentState === 'left') { if (facekit.mouthOpen) { lookLeftOpen.alpha = 1; } else { lookLeft.alpha = 1; } } else if (self.currentState === 'right') { if (facekit.mouthOpen) { lookRightOpen.alpha = 1; } else { lookRight.alpha = 1; } } } // Scale adjustment based on face size if (facekit.leftEye && facekit.rightEye) { var eyeDistance = Math.abs(facekit.rightEye.x - facekit.leftEye.x); var newScale = eyeDistance / 250; // Adjusted divisor for dragon head // Update rolling average scaleHistory[scaleIndex] = newScale; scaleIndex = (scaleIndex + 1) % scaleHistory.length; // Calculate average scale var avgScale = scaleHistory.reduce(function (a, b) { return a + b; }, 0) / scaleHistory.length; // Apply with gentle smoothing (limited range) var targetScale = Math.max(1.5, Math.min(2.5, avgScale)); head.scaleX = head.scaleX * 0.85 + targetScale * 0.15; self.currentScale = head.scaleX; // Track the current scale head.scaleY = head.scaleY * 0.85 + targetScale * 0.15; headOpen.scaleX = headOpen.scaleX * 0.85 + targetScale * 0.15; headOpen.scaleY = headOpen.scaleY * 0.85 + targetScale * 0.15; body.scaleX = body.scaleX * 0.85 + targetScale * 0.15; body.scaleY = body.scaleY * 0.85 + targetScale * 0.15; lookLeft.scaleX = lookLeft.scaleX * 0.85 - targetScale * 0.15; lookLeft.scaleY = lookLeft.scaleY * 0.85 + targetScale * 0.15; lookRight.scaleX = lookRight.scaleX * 0.85 + targetScale * 0.15; lookRight.scaleY = lookRight.scaleY * 0.85 + targetScale * 0.15; lookLeftOpen.scaleX = lookLeftOpen.scaleX * 0.85 - targetScale * 0.15; lookLeftOpen.scaleY = lookLeftOpen.scaleY * 0.85 + targetScale * 0.15; lookRightOpen.scaleX = lookRightOpen.scaleX * 0.85 + targetScale * 0.15; lookRightOpen.scaleY = lookRightOpen.scaleY * 0.85 + targetScale * 0.15; } }; function calculateFaceTilt() { if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) { // Calculate midpoint between eyes var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2; var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2; // Calculate angle between eye midpoint and mouth, negated to fix direction var dx = facekit.mouthCenter.x - eyeMidX; var dy = facekit.mouthCenter.y - eyeMidY; var angle = -(Math.atan2(dx, dy) * (180 / Math.PI)); // Reduced angle impact return Math.max(-15, Math.min(15, angle * 0.15)); } return 0; // Default to straight when face points aren't available } self.flashDamage = function (damageAmount) { // Show game over after effect if (self.invulnerableTime > 0) { return; } // Set invulnerability self.invulnerableTime = self.invulnerableDuration; // Flash effect var dragonSprites = [body]; if (head && head.alpha > 0) { dragonSprites.push(head); } if (headOpen && headOpen.alpha > 0) { dragonSprites.push(headOpen); } if (lookLeft && lookLeft.alpha > 0) { dragonSprites.push(lookLeft); } if (lookRight && lookRight.alpha > 0) { dragonSprites.push(lookRight); } if (lookLeftOpen && lookLeftOpen.alpha > 0) { dragonSprites.push(lookLeftOpen); } if (lookRightOpen && lookRightOpen.alpha > 0) { dragonSprites.push(lookRightOpen); } dragonSprites.forEach(function (sprite) { sprite.tint = 0xFF0000; LK.setTimeout(function () { sprite.tint = 0xFFFFFF; }, 10); }); // Apply damage and check for death dragon.health -= damageAmount; dragonHealthBar.updateHealth(dragon.health / 100); if (dragon.health <= 0 && gameActive) { LK.getSound('dragonScream').play(); gameActive = false; // Explode dragon for (var i = 0; i < 60; i++) { defeatParticlePool.spawn(dragon.x, dragon.y, 0xff0000); } // Hide dragon dragon.visible = false; // Show game over after effect LK.setTimeout(function () { LK.showGameOver(); }, 2000); } }; return self; }); var DragonHealthBar = Container.expand(function () { var self = Container.call(this); var emptyMeter = self.attachAsset('meter', { anchorX: 0.5, anchorY: 1.0, tint: 0x333333, scaleX: 1 }); var healthMeter = self.attachAsset('meter', { anchorX: 0.5, anchorY: 1.0, tint: 0x00ff00, scaleX: 1 }); // Position settings self.x = 2048 / 2; self.y = 220; // 100px from top + some padding self.updateHealth = function (percent) { if (self.healthTween) { self.healthTween.stop(); } self.healthTween = tween(healthMeter, { scaleX: percent }, { duration: 300, easing: tween.easeOut }); }; return self; }); var DragonShadow = Container.expand(function () { var self = Container.call(this); var shadow = self.attachAsset('dragonShadow', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5, scaleX: 1, scaleY: 0.3 }); self.x = 2048 / 2; // Match dragon's initial x self.y = 2732 * 0.6; // Positioned below dragon var positionSmoothing = 0.12; var scaleSmoothing = 0.08; self.update = function (dragonX, dragonY, dragonScale) { if (dragonX !== undefined && dragonY !== undefined && dragonScale !== undefined) { // X position follow self.x += (dragonX - self.x) * positionSmoothing; // Y position based on scale (Z-depth) var baseY = 2732 * 0.6; var scaleOffset = (dragonScale - 1) * 300; var targetY = baseY + scaleOffset; self.y += (targetY - self.y) * positionSmoothing; // Scale calculation based on dragon's Y position var heightRatio = dragonY / 2732; var scaleMultiplier = 0.2 + heightRatio * 1.5; // Add Y-position based size adjustment (0-20% increase based on Y position) var yPositionScale = 1 + self.y / 2732 * 0.2; var targetScaleX = scaleMultiplier * yPositionScale; var targetScaleY = scaleMultiplier * 0.3 * yPositionScale; var shadow = self.getChildAt(0); // Get the shadow sprite shadow.scaleX += (targetScaleX - shadow.scaleX) * scaleSmoothing; shadow.scaleY += (targetScaleY - shadow.scaleY) * scaleSmoothing; shadow.alpha = 0.3 + heightRatio * 0.2; } }; return self; }); var DustParticle = Container.expand(function () { var self = Container.call(this); var dust = self.attachAsset('dust', { anchorX: 0.5, anchorY: 0.5 }); self.activate = function (x, y) { self.x = x + (Math.random() * 20 - 10); self.y = y; self.visible = true; self.age = 0; self.lifespan = 60; // Fixed lifespan for consistency dust.alpha = 0.9; // Vertical velocity - move upward self.vy = -1.5; // Faster upward movement self.vx = (Math.random() - 0.5) * 5; // Slightly more horizontal movement // Add rotation for visual interest self.rotation = Math.random() * Math.PI * 2; }; self.update = function () { // Update position self.x += self.vx; self.y += self.vy; self.age++; // Calculate life percentage var lifePercent = self.age / self.lifespan; // Fade out dust.alpha = 0.8 * (1 - lifePercent); // Grow slightly as it rises var scale = 1 + lifePercent * 0.2; // Linear growth from 0.2 to 0.3 dust.scaleX = scale; dust.scaleY = scale; // Recycle when lifetime is over if (lifePercent >= 1) { dustParticlePool.recycle(self); } }; self.deactivate = function () { self.visible = false; }; return self; }); var Enemy = Container.expand(function () { var self = Container.call(this); // States var RUNNING = 'running'; var LEAPING = 'leaping'; var ATTACHED = 'attached'; var FALLING = 'falling'; // Running enemy sprites var runSprite = self.attachAsset('enemy1', { anchorX: 0.5, anchorY: 0.5, alpha: 1 }); var runSpriteMirrored = self.attachAsset('enemy1', { anchorX: 0.5, anchorY: 0.5, scaleX: -1, alpha: 0 }); // Properties self.health = 1; self.rotationSpeed = 0; self.animTick = 0; self.animRate = 30; self.pulseRate = self.animRate * 2; self.state = RUNNING; self.leapSpeed = 9; self.targetX = 0; self.targetY = 0; self.hasExploded = false; // Add this flag self.activate = function (x, y) { self.visible = true; self.x = x; self.y = y; self.state = RUNNING; self.rotation = 0; // Reset rotation self.health = 1; self.rotationSpeed = 0; self.fallSpeed = 0; self.acceleration = 0; self.leapDuration = 0; self.shadow = enemyShadowPool.spawn(x, y, self.scaleX); self.isPulsedOut = false; self.hasExploded = false; // Reset explosion flag // If there was a tween running, stop it if (self.currentTween) { self.currentTween.stop(); self.currentTween = null; } // Reset animation state self.animTick = 0; runSprite.alpha = 1; runSpriteMirrored.alpha = 0; // Set initial scale based on distance from horizon var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY); var scale = 0.5 + progress; self.scaleX = scale; self.scaleY = scale; }; function checkLeapDistance() { // Get distance on Y-axis only var dy = Math.abs(self.y - dragonShadow.y); // Get relative scales as depth indicators var enemyDepth = self.scaleX; var shadowDepth = dragonShadow.scaleX; // Leap when on same Y-plane regardless of X position var depthDiff = Math.abs(enemyDepth - shadowDepth); var yThreshold = 10; // Close on the Y-axis // If the enemy is on screen and at the right Y position if (dy < yThreshold && depthDiff < 0.3 && self.x > 0 && self.x < 2048) { startLeap(); return true; } return false; } function startLeap() { self.state = LEAPING; LK.getSound('orcShout').play(); // Store where the dragon was when leap started self.leapTargetX = dragon.x; self.leapTargetY = dragon.y; var dx = self.leapTargetX - self.x; var dy = self.leapTargetY - self.y; self.initialLeapDistance = Math.sqrt(dx * dx + dy * dy); self.initialScale = self.scaleX; self.isPulsedOut = false; runSprite.alpha = 1; runSpriteMirrored.alpha = 0; } self.update = function () { if (!self.visible) { return; } switch (self.state) { case RUNNING: // Existing running code var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY); var speed = (flyingBackground.scrollSpeed * 0.05 + flyingBackground.scrollSpeed * (progress * 2.5)) * 0.8; self.y -= speed; // Update scale based on distance var scale = 0.5 + progress; self.scaleX = scale; self.scaleY = scale; // Running animation self.animTick++; if (self.animTick >= self.animRate) { self.animTick = 0; runSprite.alpha = runSprite.alpha === 0 ? 1 : 0; runSpriteMirrored.alpha = runSpriteMirrored.alpha === 0 ? 1 : 0; } // In the Enemy class RUNNING state section, replace the dust spawning code with: if (LK.ticks % 10 === 0 && self.y < 2732) { // More frequent spawning (every 10 ticks) var footY = self.y + 50 * self.scaleY; dustParticlePool.spawn(self.x, footY); } // Check for leap checkLeapDistance(); break; case LEAPING: // Calculate current distance to dragon (not original leap target) var dragonDx = dragon.x - self.x; var dragonDy = dragon.y - self.y; var dragonDistance = Math.sqrt(dragonDx * dragonDx + dragonDy * dragonDy); // If we're very close to dragon, attach if (dragonDistance < 50) { self.state = ATTACHED; break; } // Continue homing leap motion if (dragonDistance > 10) { var dirX = dragonDx / dragonDistance; var dirY = dragonDy / dragonDistance; var speedMultiplier = Math.min(1.5, dragonDistance / 100); self.x += dirX * self.leapSpeed * speedMultiplier; self.y += dirY * self.leapSpeed * speedMultiplier; self.scaleX = self.initialScale * 0.75; self.scaleY = self.initialScale * 0.75; } // Add safety timeout to prevent infinite leaps self.leapDuration = (self.leapDuration || 0) + 1; if (self.leapDuration > 100) { self.state = FALLING; self.rotationSpeed = (Math.random() * 0.1 + 0.05) * (Math.random() < 0.5 ? 1 : -1); self.fallSpeed = 2; self.acceleration = 0.2; } break; case ATTACHED: // Stick to dragon position self.x = dragon.x; self.y = dragon.y; // Check for shake-off if (dragon.currentShakeSpeed > 30) { // Threshold for shake-off self.state = FALLING; self.rotationSpeed = (Math.random() * 0.1 + 0.05) * (Math.random() < 0.5 ? 1 : -1); self.fallSpeed = 2; self.acceleration = 0.2; break; } // Pulsing animation self.animTick++; if (self.animTick >= self.pulseRate) { self.animTick = 0; // Pulse between 0.7 and 0.8 scale tween(self, { scaleX: self.initialScale * (self.isPulsedOut ? 0.7 : 0.8), scaleY: self.initialScale * (self.isPulsedOut ? 0.7 : 0.8) }, { duration: 200, easing: tween.easeOut }); self.isPulsedOut = !self.isPulsedOut; dragon.flashDamage(2); } break; case FALLING: self.rotation += self.rotationSpeed; self.fallSpeed += self.acceleration; self.y += self.fallSpeed; // Check for shadow collision with explosion flag if (self.y >= dragonShadow.y && !self.hasExploded) { self.hasExploded = true; // Create MUCH bigger explosion with red tint for (var i = 0; i < 20; i++) { defeatParticlePool.spawn(self.x, self.y, 0xff0000); // Red tint (0xff0000) } score += 50; scorePopupPool.spawn(self.x, self.y, 50); scoreTxt.setText(score); updateScoreMeter(score); LK.setScore(score); LK.getSound('enemyDefeat').play(); trySpawnHealthPowerup(self.x, self.y); enemyPool.recycle(self); } break; } if (self.shadow) { self.shadow.update(self); } }; self.hit = function () { self.health--; if (self.health <= 0) { if (self.state !== FALLING) { self.state = FALLING; self.rotationSpeed = (Math.random() * 0.1 + 0.05) * (Math.random() < 0.5 ? 1 : -1); self.fallSpeed = 2; self.acceleration = 0.2; return true; } } return false; }; self.deactivate = function () { self.visible = false; runSprite.alpha = 1; runSpriteMirrored.alpha = 0; if (self.shadow) { enemyShadowPool.recycle(self.shadow); self.shadow = null; } }; return self; }); var EnemyArrow = Container.expand(function () { var self = Container.call(this); var arrow = self.attachAsset('enemyArrow', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.5, scaleY: 0.5 }); self.speed = 8; self.update = function () { if (!self.active) { return; } self.x += self.vx; self.y += self.vy; // Point arrow in direction of travel self.rotation = Math.atan2(self.vy, self.vx) + Math.PI / 2; if (self.y < 0 || self.y > 2732 || self.x < 0 || self.x > 2048) { enemyArrowPool.recycle(self); } }; self.activate = function (x, y, targetX, targetY) { self.x = x; self.y = y; self.visible = true; self.active = true; // Calculate direction to target var dx = targetX - x; var dy = targetY - y; var dist = Math.sqrt(dx * dx + dy * dy); self.vx = dx / dist * self.speed; self.vy = dy / dist * self.speed; }; self.deactivate = function () { self.visible = false; self.active = false; }; return self; }); var EnemyShadow = Container.expand(function () { var self = Container.call(this); var shadow = self.attachAsset('enemyShadow', { anchorX: 0.5, anchorY: 0.5, alpha: 0.3, scaleX: 1, scaleY: 0.3 }); // In the EnemyShadow class, update method (around line 1018) self.update = function (enemy) { if (!enemy || !enemy.visible || enemy.hasExploded) { self.visible = false; return; } self.visible = true; // Always follow enemy's X position self.x = enemy.x; // For running enemies, shadow is slightly below them if (enemy.state === 'running') { self.y = enemy.y + 200; // Shadow slightly below enemy } // For leaping enemies, shadow moves to dragon shadow position else if (enemy.state === 'leaping') { self.y = dragonShadow.y + 50; // Position just below dragon shadow } // For attached enemies, follow dragon shadow else if (enemy.state === 'attached') { self.y = dragonShadow.y + 50; // Keep following dragon shadow } // For falling enemies, shadow holds last position else if (enemy.state === 'falling') { // Y position stays the same } // Special case for EnemyTwo type else if (enemy.type === 2) { self.y = enemy.y + 600; // Position higher for enemy type 2 } // Scale shadow based on enemy's scale shadow.scaleX = enemy.scaleX * 1.2; shadow.scaleY = enemy.scaleX * 0.3; }; self.activate = function (x, y, enemyScale) { self.x = x; self.y = y + 200; // Initially position shadow slightly below enemy self.visible = true; shadow.scaleX = (enemyScale || 1) * 1.2; shadow.scaleY = (enemyScale || 1) * 0.3; }; self.deactivate = function () { self.visible = false; }; return self; }); var EnemyThree = Container.expand(function () { var self = Container.call(this); // States var GROUNDED = 'grounded'; var FLYING = 'flying'; // Main sprite var sprite = self.attachAsset('enemy3', { anchorX: 0.5, anchorY: 0.5 }); // Jet flames var leftFlame = new JetFlame(); var rightFlame = new JetFlame(); self.addChild(leftFlame); self.addChild(rightFlame); // Properties self.health = 1; self.state = GROUNDED; self.hasExploded = false; self.activate = function (x, y) { self.x = x; self.y = y; self.stateDuration = 0; self.visible = true; self.state = GROUNDED; self.health = 1; self.hasExploded = false; // Add this line to create the shadow self.shadow = enemyShadowPool.spawn(x, y, self.scaleX); // Position flames at percentage-based positions var flameY = sprite.height * 0.45; var flameX = sprite.width * 0.15; leftFlame.x = -flameX; leftFlame.y = flameY; rightFlame.x = flameX; rightFlame.y = flameY; // Initial scale based on distance from horizon var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY); var scale = 0.5 + progress; self.scaleX = scale; self.scaleY = scale; }; self.startFlying = function () { self.state = FLYING; // Calculate rough trajectory toward dragon var dx = dragon.x - self.x; var dy = dragon.y - self.y; var angle = Math.atan2(dy, dx); // Add intentional inaccuracy (they're bad at this) angle += (Math.random() - 0.5) * 0.5; // ±0.25 radians of error // Set velocities var speed = 8; self.vx = Math.cos(angle) * speed; self.vy = Math.sin(angle) * speed; // Start jet flames leftFlame.startPulse(); rightFlame.startPulse(); }; self.update = function () { if (!self.visible) { return; } if (self.state === FLYING) { self.stateDuration++; } switch (self.state) { case GROUNDED: // Move with background scroll var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY); var speed = (flyingBackground.scrollSpeed * 0.05 + flyingBackground.scrollSpeed * (progress * 2.5)) * 0.8; self.y -= speed; // Scale with distance var scale = 0.5 + progress; self.scaleX = scale; self.scaleY = scale; // Check for ignition trigger if (self.y <= dragonShadow.y + 50) { self.startFlying(); } break; case FLYING: if (self.stateDuration === 1) { LK.getSound('jetPack').play(); // Play jetPack sound effect } // Update position self.x += self.vx; self.y += self.vy; // Create jet particles if (Math.random() < 0.3) { var particle = particlePool.spawn(self.x + leftFlame.x, self.y + leftFlame.y); if (particle) { particle.vy = Math.abs(particle.vy); // Force downward } particle = particlePool.spawn(self.x + rightFlame.x, self.y + rightFlame.y); if (particle) { particle.vy = Math.abs(particle.vy); // Force downward } } // Check bounds if (self.y < -100 || self.y > 2732 || self.x < -100 || self.x > 2148) { enemyThreePool.recycle(self); } break; } if (self.shadow) { self.shadow.update(self); } // In EnemyThree class, modify the part of the update method that handles the shadow: // Update shadow position if (self.shadow) { if (self.state === GROUNDED) { self.shadow.y = self.y + 200; // Shadow slightly below enemy self.shadow.x = self.x; } else if (self.state === FLYING) { self.shadow.y = dragonShadow.y + 50; self.shadow.x = self.x; // Don't update Y position to let it stay where it was } // Scale shadow based on enemy's scale var shadowSprite = self.shadow.getChildAt(0); shadowSprite.scaleX = self.scaleX * 1.2; shadowSprite.scaleY = self.scaleX * 0.3; // Update shadow alpha based on height var heightRatio = self.y / 2732; shadowSprite.alpha = 0.3 + heightRatio * 0.2; } }; self.hit = function () { self.health--; if (self.health <= 0 && !self.hasExploded) { self.hasExploded = true; // Create explosion for (var i = 0; i < 20; i++) { defeatParticlePool.spawn(self.x, self.y, 0xff0000); } score += 25; scorePopupPool.spawn(self.x, self.y, 25); scoreTxt.setText(score.toString()); updateScoreMeter(score); LK.setScore(score); LK.getSound('enemyDefeat').play(); trySpawnHealthPowerup(self.x, self.y); enemyThreePool.recycle(self); return true; } return false; }; self.deactivate = function () { self.visible = false; if (self.shadow) { enemyShadowPool.recycle(self.shadow); self.shadow = null; } leftFlame.deactivate(); rightFlame.deactivate(); }; return self; }); var EnemyTwo = Container.expand(function () { var self = Container.call(this); var sprite = self.attachAsset('enemy2', { anchorX: 0.5, anchorY: 0.5 }); // Properties self.health = 1; self.type = 2; // For scoring self.state = 'approaching'; // approaching, holding self.holdDistance = 800; self.bobOffset = 0; self.bobSpeed = 0.05; self.lastShotTime = 0; self.shotInterval = 2000; // 2 seconds self.activate = function (x, y) { self.visible = true; self.x = x; self.y = y; self.state = 'approaching'; self.health = 1; self.rotation = 0; self.hasExploded = false; // Make sure this flag is set // Ensure shadow is created and stored if (!self.shadow) { self.shadow = enemyShadowPool.spawn(x, y, self.scaleX); } // Moving direction self.moveRight = x < 1024; // If spawned on left side, move right // Initial scale based on distance from horizon var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY); self.scaleX = 0.5 + progress; self.scaleY = 0.5 + progress; }; self.update = function () { if (!self.visible) { return; } switch (self.state) { case 'approaching': // Bobbing motion self.bobOffset = Math.sin(LK.ticks * self.bobSpeed) * 50; // Move towards dragon var dx = dragon.x - self.x; var dy = dragon.y - self.y + self.bobOffset; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > self.holdDistance) { self.x += dx * 0.01; // Reduced from 0.02 self.y += dy * 0.01; // Reduced from 0.02 // Modified scale calculation to maintain larger size var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY); self.scaleX = 0.7 + progress * 0.5; // Modified from 0.5 + progress self.scaleY = 0.7 + progress * 0.5; // Modified from 0.5 + progress } else { self.state = 'holding'; } break; case 'holding': // Maintain position relative to dragon var dx = dragon.x - self.x; var targetX = self.x + dx * 0.01; // Keep on screen targetX = Math.max(100, Math.min(1948, targetX)); self.x = targetX; // Stay below dragon with offset var targetY = dragon.y + 800; // Stay 200 pixels below dragon var currentY = self.y - self.bobOffset; // Remove bobbing to get true position self.y = currentY + (targetY - currentY) * 0.1; // Smooth movement to target // Apply bobbing to final position self.y += self.bobOffset; // Shoot at intervals var now = Date.now(); if (now - self.lastShotTime >= self.shotInterval) { self.shoot(); self.lastShotTime = now; } break; case 'falling': self.rotation += self.rotationSpeed; self.fallSpeed += self.acceleration; self.y += self.fallSpeed; // Check for ground collision if (self.shadow && self.y >= self.shadow.y && !self.hasExploded) { self.hasExploded = true; for (var i = 0; i < 20; i++) { defeatParticlePool.spawn(self.x, self.y, 0xff0000); } score += 75; scorePopupPool.spawn(self.x, self.y, 75); scoreTxt.setText(score); updateScoreMeter(score); LK.setScore(score); LK.getSound('enemyDefeat').play(); trySpawnHealthPowerup(self.x, self.y); enemyTwoPool.recycle(self); } break; } if (self.shadow) { self.shadow.update(self); } }; self.shoot = function () { var arrow = enemyArrowPool.spawn(self.x, self.y); if (arrow) { arrow.activate(self.x, self.y, dragon.x, dragon.y); LK.getSound('arrow').play(); // Play arrow sound effect } }; self.hit = function () { self.health--; if (self.health <= 0 && self.state !== 'falling') { // Prevent double-triggers if (Math.random() < 0.5) { // Mid-air explosion for (var i = 0; i < 20; i++) { defeatParticlePool.spawn(self.x, self.y, 0xff0000); } score += 75; scorePopupPool.spawn(self.x, self.y, 75); scoreTxt.setText(score); updateScoreMeter(score); LK.setScore(score); LK.getSound('enemyDefeat').play(); trySpawnHealthPowerup(self.x, self.y); enemyTwoPool.recycle(self); } else { // Fall and explode self.state = 'falling'; self.rotationSpeed = (Math.random() * 0.1 + 0.05) * (Math.random() < 0.5 ? 1 : -1); self.fallSpeed = 2; self.acceleration = 0.2; } return true; } return false; }; self.deactivate = function () { self.visible = false; if (self.shadow) { enemyShadowPool.recycle(self.shadow); self.shadow = null; } }; return self; }); var FieldElement = Container.expand(function () { var self = Container.call(this); var element = self.attachAsset('fieldElement', { anchorX: 0.5, anchorY: 0.5 }); self.activate = function (x, y, scale, alpha) { self.x = x; self.y = y; self.visible = true; self.speedY = 2 + Math.random() * 3; self.initialX = x; element.scaleX = scale; element.scaleY = scale * 0.8; element.alpha = alpha || 0.9; }; self.deactivate = function () { self.visible = false; }; return self; }); var FireParticle = Container.expand(function () { var self = Container.call(this); var particle = self.attachAsset('fireParticle', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { self.x += self.vx; self.y += self.vy; self.age++; // Update alpha var lifePercent = self.age / self.lifespan; particle.alpha = 1 - lifePercent; // Recycle if lifetime is over OR particle is off screen if (lifePercent >= 1 || self.y < 0 || self.y > 2732 || // Screen height self.x < 0 || self.x > 2048) { // Screen width particlePool.recycle(self); } }; self.activate = function (x, y) { self.x = x; self.y = y; self.visible = true; self.vx = Math.random() * 8 - 4; // Doubled velocity range self.vy = -(Math.random() * 4 + 2); // Make particles move upward self.lifespan = 45 + Math.random() * 30; // Increased lifespan self.age = 0; particle.alpha = 1; // Add scaling particle.scaleX = 0.5 + Math.random() * 0.5; particle.scaleY = 0.5 + Math.random() * 0.5; }; self.deactivate = function () { self.visible = false; }; return self; }); // In the Fireball class, update the properties and update method var Fireball = Container.expand(function () { var self = Container.call(this); var fireballGraphic = self.attachAsset('fireball', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 10; self.vx = 0; // Add these new properties self.weaveDirection = 1; // Will be set to 1 or -1 on activate self.weaveAmplitude = 50; // Adjust this to control width of weave self.weaveFrequency = 0.1; // Adjust this to control speed of weave self.initialY = 0; // Track starting Y position self.update = function () { if (!self.active) { return; } // Update base position along trajectory self.y += self.speed; self.x += self.vx; // Add weaving motion var distanceTraveled = self.y - self.initialY; var weaveOffset = Math.sin(distanceTraveled * self.weaveFrequency) * self.weaveAmplitude * self.weaveDirection; self.x += weaveOffset; // Calculate rotation based only on base trajectory, ignoring weave var angle = Math.atan2(self.speed, self.vx) - Math.PI / 2; self.rotation = angle; // Create fire trail particles if (Math.random() < 0.3) { particlePool.spawn(self.x + (Math.random() * 40 - 20), self.y + 20); } // In Fireball class update method, replace the recycle check with: // In Fireball class update method, replace the recycle check with: var groundImpactY = 2732 * 0.9; // Make impact decision only when first crossing the threshold if (self.y > groundImpactY && self.lastY <= groundImpactY && !self.hasImpacted && self.x > 100 && self.x < 1948) { if (Math.random() < 0.5) { self.hasImpacted = true; flamePool.spawn(self.x, groundImpactY); scorchPool.spawn(self.x, groundImpactY); // Spawn more particles for (var i = 0; i < 15; i++) { // Increased from 8 to 15 var particle = particlePool.spawn(self.x + (Math.random() * 40 - 20), groundImpactY); if (particle) { particle.vy = -(Math.random() * 6 + 3); // More upward velocity particle.vx = (Math.random() - 0.5) * 8; // More spread } } fireballPool.recycle(self); return; } } // Track last Y position self.lastY = self.y; // Only recycle if off screen if (self.y > 2732 || self.x < -100 || self.x > 2148) { fireballPool.recycle(self); } }; self.activate = function (x, y) { self.x = x; self.y = y; self.initialY = y; self.lastY = y; self.visible = true; self.active = true; self.hasImpacted = false; self.vx = 0; self.rotation = 0; fireballGraphic.alpha = 1; }; self.deactivate = function () { self.visible = false; self.active = false; // Clear any references that might cause memory leaks self.currentTween = null; }; return self; }); var FlameEffect = Container.expand(function () { var self = Container.call(this); var flame = self.attachAsset('flame', { anchorX: 0.5, anchorY: 1.0, scaleX: 1, scaleY: 0.5 }); self.activate = function (x, y) { self.x = x; self.y = y; self.visible = true; self.age = 0; flame.scaleX = 0.75; flame.scaleY = 0.5; var initialHeight = flame.height * flame.scaleY; var targetScale = 2.5; if (self.currentTween) { self.currentTween.stop(); } self.currentTween = tween(flame, { scaleY: targetScale, y: -(initialHeight * (targetScale - 0.5)) / 2, // Offset upward to compensate for scaling alpha: 1 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { flamePool.recycle(self); } }); }; self.deactivate = function () { self.visible = false; if (self.currentTween) { self.currentTween.stop(); self.currentTween = null; } flame.y = 0; // Reset position }; return self; }); var FlyingBackground = Container.expand(function () { var self = Container.call(this); // Create bottom container self.bottomContainer = new Container(); self.addChild(self.bottomContainer); // Store the horizon line self.horizonY = 2732 / 2; self.totalHeight = 2732 - self.horizonY; self.fieldStripes = []; // Create field stripes (keep this the same as original) var nbStripes = 30; var lastY = self.horizonY + 10; var stripe = self.bottomContainer.attachAsset('fieldStripes', { anchorX: 0.5, anchorY: 0, x: 2048 / 2, y: lastY }); var progress = Math.max(0, lastY - self.horizonY) / self.totalHeight; stripe.height = 1 + 400 * progress; self.fieldStripes.push({ sprite: stripe, progress: progress }); for (var i = 1; i < nbStripes; i++) { var spacing = 400 * (lastY - self.horizonY) / self.totalHeight; var y = lastY + spacing; var stripe = self.bottomContainer.attachAsset('fieldStripes', { anchorX: 0.5, anchorY: 0, x: 2048 / 2, y: y }); var progress = Math.max(0, y - self.horizonY) / self.totalHeight; stripe.height = 1 + 400 * progress; self.fieldStripes.push({ sprite: stripe, progress: progress }); lastY = y; } // Create top container self.topContainer = new Container(); self.addChild(self.topContainer); // Create sky background in top container var sky = self.topContainer.attachAsset('skyBackground', { anchorX: 0.5, anchorY: 1.0, x: 2048 / 2, y: self.horizonY + 10 }); // Field scroll speed self.scrollSpeed = 4; // Create object pools self.cloudPool = new ObjectPool(Cloud, 15); self.fieldElementPool = new ObjectPool(FieldElement, 20); // Add pools to containers self.topContainer.addChild(self.cloudPool); self.bottomContainer.addChild(self.fieldElementPool); // Initialize clouds for (var i = 0; i < 8; i++) { // Place clouds above the screen var startY = -600 - Math.random() * 600; var distanceFromHorizon = Math.abs(startY - self.horizonY); var startScale = 0.5 + distanceFromHorizon / 800; var cloud = self.cloudPool.spawn(Math.random() * 2048, startY, startScale, 0.8); if (cloud) { cloud.horizonY = self.horizonY; } } // Initialize field elements for (var j = 0; j < 12; j++) { var fieldStartY = self.horizonY + Math.random() * (2732 - self.horizonY); var fieldDistanceFromHorizon = Math.abs(fieldStartY - self.horizonY); var fieldStartScale = 0.5 + fieldDistanceFromHorizon / 1000; var fieldElement = self.fieldElementPool.spawn(Math.random() * 2048, fieldStartY, fieldStartScale, 0.9); if (fieldElement) { fieldElement.horizonY = self.horizonY; } } self.update = function () { // Update field stripes (keep original code) for (var i = 0; i < self.fieldStripes.length; i++) { var stripe = self.fieldStripes[i]; stripe.progress = Math.max(0, stripe.sprite.y - self.horizonY) / self.totalHeight; var tempSpeed = self.scrollSpeed * 0.05 + self.scrollSpeed * (stripe.progress * 3); stripe.sprite.y -= tempSpeed; } // Sort stripes by Y position self.fieldStripes.sort(function (a, b) { return a.sprite.y - b.sprite.y; }); // Reset stripes that moved above horizon for (var i = 0; i < self.fieldStripes.length; i++) { if (self.fieldStripes[i].sprite.y < self.horizonY) { var lowestStripe = self.fieldStripes[self.fieldStripes.length - 1]; self.fieldStripes[i].sprite.y = lowestStripe.sprite.y + 400 * (lowestStripe.sprite.y - self.horizonY) / self.totalHeight; self.fieldStripes[i].progress = Math.max(0, self.fieldStripes[i].sprite.y - self.horizonY) / self.totalHeight; } } // Re-sort after repositioning self.fieldStripes.sort(function (a, b) { return a.sprite.y - b.sprite.y; }); // Adjust stripe heights for (var i = 0; i < self.fieldStripes.length - 1; i++) { var currentStripe = self.fieldStripes[i]; var nextStripe = self.fieldStripes[i + 1]; currentStripe.sprite.height = nextStripe.sprite.y - currentStripe.sprite.y; } // Handle last stripe var lastStripe = self.fieldStripes[self.fieldStripes.length - 1]; lastStripe.sprite.height = Math.max(1 + 400 * lastStripe.progress, self.horizonY + self.totalHeight - lastStripe.sprite.y + 100); // Update clouds and field elements updateCloudPool(); updateFieldElementPool(); }; function updateCloudPool() { var activeObjects = self.cloudPool.getActiveObjects(); for (var i = 0; i < activeObjects.length; i++) { var cloud = activeObjects[i]; // Move cloud toward horizon cloud.y += cloud.speedY; cloud.x += cloud.speedX; // Get the cloud's sprite var sprite = cloud.getChildAt(0); // Calculate scale based on distance from horizon var distanceFactor = Math.max(0.05, (cloud.y - self.horizonY) / -500); sprite.scaleX = distanceFactor; sprite.scaleY = distanceFactor * 0.8; // Adjust alpha for fade effect near horizon sprite.alpha = Math.min(0.9, distanceFactor); // Reset cloud if it reaches horizon or gets too small if (cloud.y >= self.horizonY - 10 || sprite.scaleX < 0.1) { // Recycle and spawn new cloud above screen self.cloudPool.recycle(cloud); var newCloud = self.cloudPool.spawn(Math.random() * 2048, // Random x position -600 - Math.random() * 400, // Above screen 0.5 + 600 / 800, // Scale based on distance 0.8); if (newCloud) { newCloud.horizonY = self.horizonY; } } } } function updateFieldElementPool() { var activeObjects = self.fieldElementPool.getActiveObjects(); for (var j = 0; j < activeObjects.length; j++) { var element = activeObjects[j]; // Calculate progress based on distance from horizon var progress = Math.max(0, element.y - self.horizonY) / self.totalHeight; // Calculate speed using same logic as stripes var tempSpeed = self.scrollSpeed * 0.05 + self.scrollSpeed * (progress * 3); // Move element toward horizon element.y -= tempSpeed; // Get the element's sprite var sprite = element.getChildAt(0); // Calculate scale based on distance from horizon var fieldDistanceFactor = Math.max(0.05, (element.y - self.horizonY) / 300); sprite.scaleX = fieldDistanceFactor * 1.2; sprite.scaleY = fieldDistanceFactor * 1.2; // Adjust alpha for fade effect //sprite.alpha = Math.min(0.9, fieldDistanceFactor); // Reset element if it reaches horizon if (element.y <= self.horizonY + 10 || sprite.scaleX < 0.1) { // Recycle and spawn new element self.fieldElementPool.recycle(element); // Calculate position for new element var section = Math.floor(Math.random() * 8); var sectionWidth = 2048 / 8; var newX = section * sectionWidth + Math.random() * sectionWidth; var newY = 2732 + Math.random() * 600; var newScale = (newY - self.horizonY) / 300; var newElement = self.fieldElementPool.spawn(newX, newY, newScale, 0.9); if (newElement) { newElement.horizonY = self.horizonY; newElement.initialX = newX; } } element.zIndex = element.y; } // After updating all elements, sort them by z-index activeObjects.sort(function (a, b) { return a.y - b.y; // Sort by y position (smaller y values first) }); // Remove and re-add in the sorted order for (var k = 0; k < activeObjects.length; k++) { var element = activeObjects[k]; self.bottomContainer.removeChild(element); self.bottomContainer.addChild(element); } } return self; }); // Modify the HealthPopup class var HealthPopup = Container.expand(function () { var self = Container.call(this); var text = new Text2('+10', { size: 150, fill: 0x00FF00, // Green stroke: 0x000000, strokeThickness: 3 }); text.anchor.set(0.5, 0.5); self.addChild(text); // Initially hide the popup self.visible = false; self.show = function (x, y) { self.x = x; self.y = y; self.visible = true; text.scaleX = 0.1; text.scaleY = 0.1; text.y = 0; // Reset position before animation if (self.currentTween) { self.currentTween.stop(); } self.currentTween = tween(text, { scaleX: 1.5, scaleY: 1.5, y: -50 }, { duration: 300, easing: tween.easeOutBack, onFinish: function onFinish() { tween(text, { alpha: 0, y: -100 }, { duration: 400, easing: tween.easeIn, onFinish: function onFinish() { self.visible = false; text.alpha = 1; text.y = 0; } }); } }); }; return self; }); var HealthPowerup = Container.expand(function () { var self = Container.call(this); var heart = self.attachAsset('heart', { anchorX: 0.5, anchorY: 0.5 }); self.activate = function (x, y) { self.x = x; self.y = y; self.visible = true; self.age = 0; heart.scaleX = heart.scaleY = 1; }; self.update = function () { self.age++; // Wave pattern movement self.x += Math.sin(self.age * 0.05) * 2; self.y -= 3; // Float upward // Pulse scale var pulse = 1 + Math.sin(self.age * 0.1) * 0.2; heart.scaleX = heart.scaleY = pulse; // Remove if off screen if (self.y < -50) { healthPowerupPool.recycle(self); } }; self.deactivate = function () { self.visible = false; }; return self; }); var JetFlame = Container.expand(function () { var self = Container.call(this); var flame = self.attachAsset('flame', { anchorX: 0.5, anchorY: 0, // Keep bottom anchor for natural flame shape scaleX: 1, scaleY: 0.5, rotation: Math.PI // 180 degrees to point downward }); self.startPulse = function () { flame.scaleX = 0.5; flame.scaleY = 0.3; flame.alpha = 0.8; self.pulse(); }; self.pulse = function () { if (self.currentTween) { self.currentTween.stop(); } self.currentTween = tween(flame, { scaleX: 0.75, scaleY: 0.6, alpha: 1 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { self.currentTween = tween(flame, { scaleX: 0.5, scaleY: 0.3, alpha: 0.8 }, { duration: 300, easing: tween.easeOut, onFinish: self.pulse }); } }); }; self.deactivate = function () { if (self.currentTween) { self.currentTween.stop(); self.currentTween = null; } flame.scaleY = 0.5; }; return self; }); var ObjectPool = Container.expand(function (ObjectClass, size) { var self = Container.call(this); var objects = []; var activeCount = 0; for (var i = 0; i < size; i++) { var obj = new ObjectClass(); if (typeof obj.update !== 'function') { obj.update = function () {}; // Provide a default update method if not defined } obj.visible = false; obj._poolIndex = i; objects.push(obj); self.addChild(obj); } self.spawn = function (x, y, params) { if (activeCount >= objects.length) { return null; } var obj = objects[activeCount]; activeCount++; // Increment AFTER getting object obj.visible = true; obj.activate(x, y, params); return obj; }; self.recycle = function (obj) { if (!obj || typeof obj._poolIndex !== 'number' || obj._poolIndex >= activeCount) { return; } activeCount--; // Decrement count // Swap with last active object if needed if (obj._poolIndex < activeCount) { var temp = objects[activeCount]; objects[activeCount] = obj; objects[obj._poolIndex] = temp; // Update indices temp._poolIndex = obj._poolIndex; obj._poolIndex = activeCount; } obj.deactivate(); obj.visible = false; }; self.recycleAll = function () { while (activeCount > 0) { self.recycle(objects[0]); } }; self.update = function () { for (var i = 0; i < activeCount; i++) { objects[i].update(); } }; self.getActiveObjects = function () { return objects.slice(0, activeCount); }; return self; }); var ProgressMeter = Container.expand(function () { var self = Container.call(this); // Create meter background (now horizontal) var meter = self.attachAsset('meter', { anchorX: 0, anchorY: 0.5, scaleX: 1.2, // Adjust width to preference scaleY: 0.6, // Make thinner tint: 0x666666 }); // Progress icons var dragonIcon = self.attachAsset('dragonHead', { anchorX: 0.5, anchorY: 0.7, scaleX: 0.9, scaleY: 0.9 }); var bossIcon = self.attachAsset('skull', { anchorX: 0.5, anchorY: 0.7 }); // Position at top center // Position elements relative to meter center var totalWidth = meter.width * 1.2 + 120; // Include icon spaces self.x = 2048 / 2 - totalWidth / 2; // Center the whole assembly self.y = 80; // Position icons relative to meter edges dragonIcon.x = 0; // Left icon at start dragonIcon.y = meter.height / 2; bossIcon.x = totalWidth; // Right icon at end bossIcon.y = meter.height / 2; // Center meter between icons meter.x = 60; // Space for left icon self.updateProgress = function (progress) { dragonIcon.x = progress * (totalWidth - 60); // Move between start and end positions }; return self; }); var ScorchMark = Container.expand(function () { var self = Container.call(this); var mark = self.attachAsset('scorchMark', { anchorX: 0.5, anchorY: 0.5, alpha: 0.8 }); self.activate = function (x, y) { self.x = x; self.y = y; self.visible = true; self.speedY = flyingBackground.scrollSpeed; // Initial scale based on distance from horizon var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY); mark.scaleX = 0.5 + progress; mark.scaleY = (0.5 + progress) * 0.8; }; self.update = function () { var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY); // Drastically reduce base speed to match field elements var tempSpeed = 0.2 + progress * 3; self.y -= tempSpeed; mark.scaleX = 0.5 + progress; mark.scaleY = (0.5 + progress) * 0.8; mark.alpha = Math.min(0.8, progress); if (self.y <= flyingBackground.horizonY) { scorchPool.recycle(self); } }; self.deactivate = function () { self.visible = false; }; return self; }); var ScorePopup = Container.expand(function () { var self = Container.call(this); // Create text with white color instead of red var scoreTxt = new Text2('', { size: 120, fill: 0xFFFFFF, // White color stroke: 0x000000, strokeThickness: 3 }); scoreTxt.anchor.set(0.5, 0.5); self.addChild(scoreTxt); self.activate = function (x, y, amount, color) { self.x = x; self.y = y; self.visible = true; scoreTxt.setText('+' + amount); // Special case for boss defeat score if (amount === 5000) { scoreTxt.size = 300; // Much larger text color = 0xFFD700; // Gold color } else { scoreTxt.size = 120; // Normal size } // Apply tint if provided if (color !== undefined) { scoreTxt.tint = color; } else { scoreTxt.tint = 0xFF0000; // Default red tint } scoreTxt.scaleX = 0.1; scoreTxt.scaleY = 0.1; if (self.currentTween) { self.currentTween.stop(); } // Special animation for boss defeat if (amount === 5000) { self.currentTween = tween(scoreTxt, { scaleX: 2.5, scaleY: 2.5, y: -150 // Move higher for boss defeat }, { duration: 600, // Longer duration easing: tween.easeOutBack, onFinish: function onFinish() { tween(scoreTxt, { alpha: 0, y: -250 // Float up higher }, { duration: 800, // Longer fade easing: tween.easeIn, onFinish: function onFinish() { scorePopupPool.recycle(self); } }); } }); } else { // Original animation for regular scores self.currentTween = tween(scoreTxt, { scaleX: 1.5, scaleY: 1.5, y: -50 }, { duration: 300, easing: tween.easeOutBack, onFinish: function onFinish() { tween(scoreTxt, { alpha: 0, y: -100 }, { duration: 400, easing: tween.easeIn, onFinish: function onFinish() { scorePopupPool.recycle(self); } }); } }); } }; self.deactivate = function () { self.visible = false; scoreTxt.alpha = 1; scoreTxt.y = 0; scoreTxt.tint = 0xFFFFFF; // Reset to white (no tint) if (self.currentTween) { self.currentTween.stop(); } }; return self; }); var TitleScreen = Container.expand(function () { var self = Container.call(this); var fadeOverlay = self.attachAsset('particle', { anchorX: 0, anchorY: 0, scaleX: 2048, scaleY: 2732, tint: 0x000000, alpha: 1 }); var titleImage = self.attachAsset('titleimage', { anchorX: 0.5, anchorY: 0.5, alpha: 0 }); var startButton = self.attachAsset('startbutton', { anchorX: 0.5, anchorY: 0.5, scaleX: 0, scaleY: 0, alpha: 0 }); titleImage.x = 2048 / 2; titleImage.y = 2732 / 3; startButton.x = 2048 / 2; startButton.y = titleImage.y + 1100; self.start = function () { // Fade in title tween(fadeOverlay, { alpha: 0 }, { duration: 1500, easing: tween.easeOut }); tween(titleImage, { alpha: 1, scaleX: 1, // Add scale pulse start scaleY: 1 // Add scale pulse start }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { // Add pulsing animation function pulseTitle() { tween(titleImage, { tint: 0xFFFF00, scaleX: 1.1, scaleY: 1.1 }, { duration: 1100, easing: tween.easeInOut, onFinish: function onFinish() { tween(titleImage, { tint: 0xFFA500, scaleX: 1, scaleY: 1 }, { duration: 1100, easing: tween.easeInOut, onFinish: pulseTitle }); } }); } pulseTitle(); // Grow button tween(startButton, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 800, easing: tween.easeOutBack, onFinish: function onFinish() { // Add pulsing animation function pulse() { tween(startButton, { tint: 0xFFFF00 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { tween(startButton, { tint: 0xFFA500 }, { duration: 1000, easing: tween.easeInOut, onFinish: pulse }); } }); } pulse(); } }); } }); }; startButton.down = function () { self.startGame(); return true; }; self.startGame = function () { // Prevent multiple clicks LK.getSound('dragonScream').play(); startButton.down = null; // Fade out title elements tween(titleImage, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { // Enable gameplay gameActive = true; // Show UI dragonHealthBar.visible = true; scoreTxt.visible = true; scoreMeterBg.visible = true; progressMeter.visible = true; // Enable dragon and shadow dragon.visible = true; dragonShadow.visible = true; // Remove title screen game.removeChild(self); } }); tween(startButton, { alpha: 0 }, { duration: 500 }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ function processCollisions() { var fireballs = fireballPool.getActiveObjects(); var enemies = enemyPool.getActiveObjects(); var enemiesTwo = enemyTwoPool.getActiveObjects(); // Process enemy arrows first var arrows = enemyArrowPool.getActiveObjects(); // Add after arrow collision checks in processCollisions() var enemiesThree = enemyThreePool.getActiveObjects(); for (var i = enemiesThree.length - 1; i >= 0; i--) { var enemy = enemiesThree[i]; var dx = enemy.x - dragon.x; var dy = enemy.y - dragon.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 210 * enemy.scaleX) { // Adjust collision radius based on enemy scale // Create explosion effect for (var j = 0; j < 20; j++) { defeatParticlePool.spawn(enemy.x, enemy.y, 0xff0000); } enemyThreePool.recycle(enemy); dragon.flashDamage(6); } } // Add inside processCollisions() function, after the existing collision checks: // Check boss fist collisions with dragon if (boss && boss.activeFist) { var fist = boss.activeFist; var dx = fist.x - dragon.x; var dy = fist.y - dragon.y; var distance = Math.sqrt(dx * dx + dy * dy); // Use dragon's current scale for collision radius var collisionRadius = 150 * dragon.currentScale; if (distance < collisionRadius) { dragon.flashDamage(12); // Should the fist retract after hitting? Let me know if you want this behavior } } // Replace the existing fireball-boss collision check with this: if (boss && boss.state !== 'dying') { var fireballs = fireballPool.getActiveObjects(); for (var i = fireballs.length - 1; i >= 0; i--) { var fireball = fireballs[i]; var dx = fireball.x - boss.x; var dy = fireball.y - boss.y; // Use elliptical hitbox: wider horizontally, same vertically var horizontalRange = 300; // Wider horizontal hitbox (was effectively 100) var verticalRange = 100; // Keep the same vertical hitbox // Elliptical distance formula var ellipticalDist = Math.sqrt(dx * dx / (horizontalRange * horizontalRange) + dy * dy / (verticalRange * verticalRange)); if (ellipticalDist < 1) { boss.damage(1); fireballPool.recycle(fireball); } } } // Replace the existing arrow collision check with: for (var i = arrows.length - 1; i >= 0; i--) { var arrow = arrows[i]; var dx = arrow.x - dragon.x; var dy = arrow.y - dragon.y; // Only check collisions if arrow isn't below dragon if (arrow.y <= dragon.y + 50) { // Allow slight vertical tolerance // Wider horizontal check, original vertical bounds var horizontalDistance = Math.abs(dx); var verticalDistance = Math.abs(dy); // Check if within wing range (wider horizontally) if (horizontalDistance < 300 && verticalDistance < 75) { enemyArrowPool.recycle(arrow); // Flash dragon red (keep existing code) dragon.flashDamage(7); } } } var fireballs = fireballPool.getActiveObjects(); var arrows = enemyArrowPool.getActiveObjects(); for (var i = fireballs.length - 1; i >= 0; i--) { var fireball = fireballs[i]; for (var j = arrows.length - 1; j >= 0; j--) { var arrow = arrows[j]; var dx = fireball.x - arrow.x; var dy = fireball.y - arrow.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 60) { // Small collision radius since these are projectiles // Create small explosion for (var k = 0; k < 8; k++) { // Fewer particles than full defeat var particle = defeatParticlePool.spawn(arrow.x + (Math.random() * 20 - 10), // Smaller spread arrow.y + (Math.random() * 20 - 10), 0xff0000 // Red tint ); if (particle) { // Smaller scale for particles var sprite = particle.getChildAt(0); sprite.scaleX = 0.2; sprite.scaleY = 0.2; } } // Remove both projectiles fireballPool.recycle(fireball); enemyArrowPool.recycle(arrow); // Play explosion sound with lower volume LK.getSound('enemyDefeat').play({ volume: 0.5 }); break; // Break inner loop since fireball is gone } } } // Process fireballs against both enemy types for (var i = fireballs.length - 1; i >= 0; i--) { var fireball = fireballs[i]; // Check against type 1 enemies for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Check if enemy is fully on screen (accounting for scaled size) var enemyHeight = 403 * enemy.scaleY; // Original height * scale var enemyWidth = 200 * enemy.scaleX; // Original width * scale if (enemy.y > enemyHeight / 2 && enemy.y < 2732 - enemyHeight / 2 && enemy.x > enemyWidth / 2 && enemy.x < 2048 - enemyWidth / 2) { var dx = fireball.x - enemy.x; var dy = fireball.y - enemy.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 120 * enemy.scaleX) { enemy.hit(); fireballPool.recycle(fireball); break; } } } // Check against type 2 enemies for (var j = enemiesTwo.length - 1; j >= 0; j--) { var enemy = enemiesTwo[j]; var enemyHeight = 412.5 * enemy.scaleY; var enemyWidth = 550 * enemy.scaleX; if (enemy.y > enemyHeight / 2 && enemy.y < 2732 - enemyHeight / 2 && enemy.x > enemyWidth / 2 && enemy.x < 2048 - enemyWidth / 2) { var dx = fireball.x - enemy.x; var dy = fireball.y - enemy.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 170 * enemy.scaleX) { enemy.hit(); fireballPool.recycle(fireball); break; } } } // Check against type 3 enemies for (var j = enemiesThree.length - 1; j >= 0; j--) { var enemy = enemiesThree[j]; var enemyHeight = 253 * enemy.scaleY; var enemyWidth = 300 * enemy.scaleX; if (enemy.y > enemyHeight / 2 && enemy.y < 2732 - enemyHeight / 2 && enemy.x > enemyWidth / 2 && enemy.x < 2048 - enemyWidth / 2) { var dx = fireball.x - enemy.x; var dy = fireball.y - enemy.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 120 * enemy.scaleX) { enemy.hit(); fireballPool.recycle(fireball); break; } } } } var powerups = healthPowerupPool.getActiveObjects(); for (var i = powerups.length - 1; i >= 0; i--) { var powerup = powerups[i]; var dx = powerup.x - dragon.x; var dy = powerup.y - dragon.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 240) { dragon.health = Math.min(100, dragon.health + 10); dragonHealthBar.updateHealth(dragon.health / 100); LK.getSound('health').play(); healthPowerupPool.recycle(powerup); healthPopup.show(powerup.x, powerup.y); } } } function trySpawnHealthPowerup(x, y) { if (Math.random() < 0.10) { // 10% chance // 100% chance // 10% chance // 7% chance // 10% chance // 15% chance var powerup = healthPowerupPool.spawn(x, y); if (powerup) { scorePopupPool.spawn(x, y - 50, "HEALTH", 0x00ff00); } } } // Set dark blue background for sky effect game.setBackgroundColor(0x2c3e50); // Game state variables var score = 0; var fireballPool = new ObjectPool(Fireball, 100); var particlePool = new ObjectPool(FireParticle, 400); var enemyPool = new ObjectPool(Enemy, 25); var defeatParticlePool = new ObjectPool(DefeatParticle, 200); var dustParticlePool = new ObjectPool(DustParticle, 100); var enemyShadowPool = new ObjectPool(EnemyShadow, 50); var enemyArrowPool = new ObjectPool(EnemyArrow, 50); var enemyTwoPool = new ObjectPool(EnemyTwo, 25); var enemyThreePool = new ObjectPool(EnemyThree, 25); var flamePool = new ObjectPool(FlameEffect, 20); var scorchPool = new ObjectPool(ScorchMark, 30); var chainSegmentPool = new ObjectPool(ChainSegment, 30); var scorePopupPool = new ObjectPool(ScorePopup, 20); var healthPowerupPool = new ObjectPool(HealthPowerup, 10); var bossFistPool = new ObjectPool(BossFist, 2); // Left and right fists var boss = null; // Will be created when needed var bossHealthBar = new BossHealthBar(); bossHealthBar.visible = false; bossHealthBar.updateHealth(1.0); var LEVEL_DURATION = 180 * 60; // 3 minutes at 60fps var levelTimer = 0; var progressMeter = new ProgressMeter(); var isFiring = false; var lastFireTime = 0; var fireRate = 200; // ms between fireballs var enemy1StartTime = 30 * 60; // 30 seconds var enemy2StartTime = 15 * 60; // 15 seconds var enemy3StartTime = 5 * 60; // 5 seconds var enemy1SpawnRate = 300; // ~3 seconds var enemy2SpawnRate = 240; // ~4 seconds var enemy3SpawnRate = 180; // ~5 seconds var ENEMY1_MIN_RATE = 150; // Half of 300 var ENEMY2_MIN_RATE = 120; // Half of 240 var ENEMY3_MIN_RATE = 100; // Half of 180 var difficultyScaling = 0; var flyingBackground = new FlyingBackground(); game.addChild(flyingBackground); // Create dragon head (player character) var dragon = game.addChild(new DragonHead()); dragon.x = 2048 / 2; dragon.y = 2732 * 0.2; // Place dragon at top fifth of screen var dragonShadow = new DragonShadow(); game.addChild(dragonShadow); game.addChild(enemyShadowPool); game.addChild(fireballPool); game.addChild(particlePool); game.addChild(enemyPool); game.addChild(enemyTwoPool); game.addChild(enemyThreePool); game.addChild(chainSegmentPool); game.addChild(bossFistPool); game.addChild(scorePopupPool); game.addChild(defeatParticlePool); game.addChild(dustParticlePool); game.addChild(enemyArrowPool); game.addChild(scorchPool); game.addChild(flamePool); game.addChild(healthPowerupPool); var healthPopup = new HealthPopup(); game.addChild(healthPopup); dragon.health = 100; var dragonHealthBar = new DragonHealthBar(); game.addChild(dragonHealthBar); game.addChild(progressMeter); var bossText = new BossText(); game.addChild(bossText); game.cleanupPools = function () { fireballPool.recycleAll(); particlePool.recycleAll(); enemyPool.recycleAll(); enemyTwoPool.recycleAll(); defeatParticlePool.recycleAll(); dustParticlePool.recycleAll(); enemyShadowPool.recycleAll(); enemyArrowPool.recycleAll(); scorchPool.recycleAll(); flamePool.recycleAll(); enemyThreePool.recycleAll(); }; var scoreMeterBg = game.attachAsset('particle', { anchorX: 0, anchorY: 0.5, tint: 0x333333, alpha: 0.5, scaleY: 1 }); // Score display var scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 3 }); // Position meter and score scoreMeterBg.x = 2048 / 2; // Center X scoreMeterBg.anchorX = 0.5; // Center anchor for background scoreMeterBg.y = 300; scoreTxt.anchor.set(0.5, 0.5); // Center anchor for text scoreTxt.x = 2048 / 2; // Center X scoreTxt.y = 300; game.addChild(scoreMeterBg); game.addChild(scoreTxt); function updateScoreMeter(score) { var minWidth = 200; var scoreStr = score.toString(); var padding = 40; // Total padding (20 each side) // Adjust width per digit based on digit count var widthPerDigit; if (scoreStr.length === 1) { widthPerDigit = 70; } else if (scoreStr.length === 2) { widthPerDigit = 60; } else { widthPerDigit = 70; } var targetWidth = Math.max(minWidth, scoreStr.length * widthPerDigit + padding); scoreMeterBg.scaleX = targetWidth / scoreMeterBg.width; // Text stays centered automatically now } // Create a function to add fire particles function createFireParticle(x, y) { particlePool.spawn(x, y); } // Function to spawn a fireball // Update the spawnFireball function function spawnFireball() { if (!gameActive) { return; } var now = Date.now(); if (now - lastFireTime < fireRate) { return; } lastFireTime = now; // Initialize horizontal velocity components var angleOffset = 0; // Get direction from dragon head facing - match thresholds with DragonHead class if (facekit.leftEye && facekit.rightEye && facekit.noseTip) { var faceCenter = (facekit.leftEye.x + facekit.rightEye.x) / 2; var noseDiff = facekit.noseTip.x - faceCenter; var eyeYDiff = facekit.rightEye.y - facekit.leftEye.y; // Keep the original thresholds but require more pronounced movement // to trigger the sideways shots (higher threshold values) if (noseDiff < -120 && Math.abs(eyeYDiff) < 30 || eyeYDiff > 50) { // Facing left - fireballs go left (maintain strong angle) angleOffset = -8; // Original value for strong effect } else if (noseDiff > 120 && Math.abs(eyeYDiff) < 30 || eyeYDiff < -50) { // Facing right - fireballs go right (maintain strong angle) angleOffset = 8; // Original value for strong effect } } // Keep the original tilt impact for consistent feel if (dragon.rotation) { angleOffset += dragon.rotation * 5; // Original value } // Use the new pool's spawn method var fireball = fireballPool.spawn(dragon.x, dragon.y + 50); if (fireball) { fireball.vx = angleOffset; // Add this line to alternate weave direction fireball.weaveDirection = lastFireTime % (fireRate * 2) === 0 ? 1 : -1; } // Play sound LK.getSound('firebreathSound').play(); } dragonHealthBar.visible = false; scoreTxt.visible = false; scoreMeterBg.visible = false; progressMeter.visible = false; dragon.visible = false; dragonShadow.visible = false; gameActive = false; var titleScreen = new TitleScreen(); game.addChild(titleScreen); titleScreen.start(); // Game update logic game.update = function () { if (!gameActive) { return; } // Check if mouth is open for fire breathing if (facekit && facekit.mouthOpen) { isFiring = true; spawnFireball(); } else { isFiring = false; } if (boss) { boss.update(); } levelTimer++; // Only update progress if boss hasn't spawned yet if (!boss) { progressMeter.updateProgress(levelTimer / LEVEL_DURATION); } // Enemy spawning logic // Enemy spawning logic if (!boss || boss.state !== 'dying') { // Add a spawn rate multiplier that's 2x when boss is present var spawnRateMultiplier = boss ? 2 : 1; if (levelTimer >= enemy3StartTime) { // Scale from initial 180 to minimum 90 over level duration var scaled3Rate = Math.max(ENEMY3_MIN_RATE, enemy3SpawnRate - (enemy3SpawnRate - ENEMY3_MIN_RATE) * (levelTimer / LEVEL_DURATION)); // Apply the multiplier to the spawn rate if (LK.ticks % Math.floor(scaled3Rate * spawnRateMultiplier) === 0) { var enemy = enemyThreePool.spawn(Math.random() * (2048 - 200) + 100, 2732 + 100); } } if (levelTimer >= enemy2StartTime) { var scaled2Rate = Math.max(ENEMY2_MIN_RATE, enemy2SpawnRate - (enemy2SpawnRate - ENEMY2_MIN_RATE) * (levelTimer / LEVEL_DURATION)); // Apply the multiplier to the spawn rate if (LK.ticks % Math.floor(scaled2Rate * spawnRateMultiplier) === 0) { var spawnX = Math.random() < 0.5 ? -50 : 2098; var enemy = enemyTwoPool.spawn(spawnX, 2732 - 500); } } if (levelTimer >= enemy1StartTime) { var scaled1Rate = Math.max(ENEMY1_MIN_RATE, enemy1SpawnRate - (enemy1SpawnRate - ENEMY1_MIN_RATE) * (levelTimer / LEVEL_DURATION)); // Apply the multiplier to the spawn rate if (LK.ticks % Math.floor(scaled1Rate * spawnRateMultiplier) === 0) { var enemy = enemyPool.spawn(Math.random() * (2048 - 200) + 100, 2732 + 100); } } } // Boss spawn check if (levelTimer >= LEVEL_DURATION && !boss) { bossText.flash(); boss = new Boss(); game.addChild(boss); game.addChild(bossHealthBar); bossHealthBar.visible = true; bossHealthBar.show(); } dragonShadow.update(dragon.x, dragon.y, dragon.currentScale); // Update all pools fireballPool.update(); enemyPool.update(); particlePool.update(); defeatParticlePool.update(); dustParticlePool.update(); bossFistPool.update(); flamePool.update(); scorchPool.update(); enemyThreePool.update(); enemyTwoPool.update(); scorePopupPool.update(); healthPowerupPool.update(); // Process collisions processCollisions(); }; // Start background music LK.playMusic('gameMusic', { fade: { start: 0, end: 0.7, duration: 1000 } });
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
var facekit = LK.import("@upit/facekit.v1");
/****
* Classes
****/
var Boss = Container.expand(function () {
var self = Container.call(this);
// States
var ENTERING = 'entering';
var WALKING = 'walking';
var ARROW_ATTACK = 'arrow_attack';
var CHARGING_LEFT = 'charging_left';
var CHARGING_RIGHT = 'charging_right';
var FIST_LEFT = 'fist_left';
var FIST_RIGHT = 'fist_right';
var RETRACTING_LEFT = 'retracting_left';
var RETRACTING_RIGHT = 'retracting_right';
var DYING = 'dying';
// All boss sprites
var normalBody = self.attachAsset('boss1', {
anchorX: 0.5,
anchorY: 0,
alpha: 1
});
var leftBody = self.attachAsset('boss1left', {
anchorX: 0.5,
anchorY: 0,
alpha: 0
});
var rightBody = self.attachAsset('boss1right', {
anchorX: 0.5,
anchorY: 0,
alpha: 0
});
var leftNoFist = self.attachAsset('boss1leftnofist', {
anchorX: 0.5,
anchorY: 0,
alpha: 0
});
var rightNoFist = self.attachAsset('boss1rightnofist', {
anchorX: 0.5,
anchorY: 0,
alpha: 0
});
// Properties
self.health = 100;
self.state = ENTERING;
self.hasCompletedFirstAttack = false;
self.stateDuration = 0;
self.bobSpeed = 0.1;
self.bobOffset = 0;
self.bobBaseY = 2732 / 1.35; // Add base Y position
self.activeFist = null;
self.attackTimer = 0;
self.attackDelay = 120;
self.hasPlayedDeathSound = false;
// Initial position
self.x = 2048 / 2;
self.y = 2732;
function setBodyState(state) {
normalBody.alpha = 0;
leftBody.alpha = 0;
rightBody.alpha = 0;
leftNoFist.alpha = 0;
rightNoFist.alpha = 0;
switch (state) {
case 'normal':
normalBody.alpha = 1;
break;
case 'left':
leftBody.alpha = 1;
break;
case 'right':
rightBody.alpha = 1;
break;
case 'leftNoFist':
leftNoFist.alpha = 1;
break;
case 'rightNoFist':
rightNoFist.alpha = 1;
break;
}
}
function flashDamage() {
[normalBody, leftBody, rightBody, leftNoFist, rightNoFist].forEach(function (sprite) {
if (sprite.alpha > 0) {
sprite.tint = 0xFF0000;
LK.setTimeout(function () {
sprite.tint = 0xFFFFFF;
}, 5);
}
});
}
self.update = function () {
self.stateDuration++;
self.bobOffset = Math.sin(LK.ticks * self.bobSpeed) * 15;
switch (self.state) {
case ENTERING:
setBodyState('normal');
self.y -= 2;
if (self.y <= 2732 / 1.35) {
self.state = WALKING;
self.attackTimer = self.attackDelay;
}
break;
case WALKING:
setBodyState('normal');
self.bobOffset = Math.sin(LK.ticks * self.bobSpeed) * 15;
self.y = self.bobBaseY + self.bobOffset;
self.attackTimer--;
if (self.attackTimer <= 0) {
if (!self.hasCompletedFirstAttack) {
self.state = ARROW_ATTACK;
self.hasCompletedFirstAttack = true;
} else {
var rand = Math.random();
if (rand < 0.6) {
self.state = ARROW_ATTACK;
} else {
self.state = Math.random() < 0.5 ? CHARGING_LEFT : CHARGING_RIGHT;
}
}
self.stateDuration = 0;
}
break;
case ARROW_ATTACK:
// Keep bobbing motion during arrow attack
self.bobOffset = Math.sin(LK.ticks * self.bobSpeed) * 15;
self.y = self.bobBaseY + self.bobOffset;
if (self.stateDuration === 1) {
LK.getSound('arrow').play(); // Play arrow sound effect
var centerY = self.y + 200;
enemyArrowPool.spawn(self.x, centerY).activate(self.x, centerY, dragon.x, dragon.y);
enemyArrowPool.spawn(self.x - 100, centerY).activate(self.x - 100, centerY, dragon.x - 300, dragon.y);
enemyArrowPool.spawn(self.x + 100, centerY).activate(self.x + 100, centerY, dragon.x + 300, dragon.y);
} else if (self.stateDuration > 60) {
self.state = WALKING;
self.attackTimer = self.attackDelay;
self.stateDuration = 0;
}
break;
case CHARGING_LEFT:
case CHARGING_RIGHT:
var isLeft = self.state === CHARGING_LEFT;
setBodyState(isLeft ? 'left' : 'right');
if (self.stateDuration < 70) {
var targetX = self.x + (isLeft ? -200 : 200);
var targetY = self.y + 300;
if (Math.random() < 0.3) {
var angle = Math.random() * Math.PI * 2;
var radius = Math.random() * 200 + 100;
var px = targetX + Math.cos(angle) * radius;
var py = targetY + Math.sin(angle) * radius;
var particle = particlePool.spawn(px, py);
if (particle) {
var dx = targetX - px;
var dy = targetY - py;
var dist = Math.sqrt(dx * dx + dy * dy);
particle.vx = dx / dist * 10;
particle.vy = dy / dist * 10;
}
}
} else {
if (self.activeFist) {
bossFistPool.recycle(self.activeFist);
self.activeFist = null;
}
self.state = isLeft ? FIST_LEFT : FIST_RIGHT;
self.stateDuration = 0;
setBodyState(isLeft ? 'leftNoFist' : 'rightNoFist');
self.activeFist = bossFistPool.spawn(self.x + (isLeft ? -200 : 200), self.y + 300, isLeft);
if (self.activeFist) {
self.activeFist.extend(dragon.x, dragon.y);
LK.getSound('bossPunch').play();
}
}
break;
case FIST_LEFT:
case FIST_RIGHT:
if (!self.activeFist || !self.activeFist.visible) {
// If fist is somehow missing, recover
setBodyState('normal');
self.state = WALKING;
self.attackTimer = self.attackDelay;
self.stateDuration = 0;
break;
}
if (self.activeFist && !self.activeFist.extending) {
self.state = self.state === FIST_LEFT ? RETRACTING_LEFT : RETRACTING_RIGHT;
self.stateDuration = 0;
if (self.activeFist) {
self.activeFist.retract();
}
}
break;
case RETRACTING_LEFT:
case RETRACTING_RIGHT:
if (!self.activeFist || !self.activeFist.visible) {
setBodyState('normal');
self.state = WALKING;
self.attackTimer = self.attackDelay;
self.stateDuration = 0;
if (self.activeFist) {
bossFistPool.recycle(self.activeFist); // Add this line
}
self.activeFist = null; // Add this line to clear the reference
}
break;
case DYING:
self.y += 1;
self.x += Math.sin(self.stateDuration * 0.2) * 5;
// More frequent and spread out particles
if (self.stateDuration % 10 === 0) {
for (var i = 0; i < 5; i++) {
defeatParticlePool.spawn(self.x + (Math.random() * 800 - 400), self.y + Math.random() * 800, 0xff0000);
}
}
// Random explosions every second
if (self.stateDuration % 60 === 0) {
var explosionX = self.x + (Math.random() * 600 - 300);
var explosionY = self.y + (Math.random() * 600 - 300);
var explosion = LK.getAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
y: explosionY,
scaleX: 0.5,
//{39} // Start bigger
scaleY: 0.5,
alpha: 1
});
game.addChild(explosion);
LK.getSound('explosion').play();
tween(explosion, {
scaleX: 4,
//{3g} // End much bigger
scaleY: 4,
alpha: 0
}, {
duration: 500,
//{3k} // Faster animation
easing: tween.easeOut,
onFinish: function onFinish() {
game.removeChild(explosion);
}
});
}
if (self.y > 2732 + 200) {
//{3r} // Reduced height check
game.removeChild(self);
bossHealthBar.visible = false;
if (!self.hasPlayedDeathSound) {
LK.getSound('dragonScream').play();
self.hasPlayedDeathSound = true;
}
LK.setTimeout(function () {
LK.showYouWin();
}, 3000);
}
break;
}
self.lastBobOffset = self.bobOffset;
};
self.damage = function (amount) {
self.health -= amount;
bossHealthBar.updateHealth(self.health / 100);
flashDamage();
if (self.health <= 0 && self.state !== DYING) {
self.state = DYING;
self.stateDuration = 0;
bossHealthBar.hide();
// Add boss defeat score
score += 5000;
scorePopupPool.spawn(self.x, self.y, 5000);
scoreTxt.setText(score);
updateScoreMeter(score);
LK.setScore(score);
tween(game, {
x: game.x + 20
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
game.x = 0;
}
});
}
};
return self;
});
var BossFist = Container.expand(function () {
var self = Container.call(this);
// Create both fist sprites and manage visibility
var leftFist = self.attachAsset('boss1leftfist', {
anchorX: 0.5,
anchorY: 0.5,
visible: false,
rotation: -Math.PI / 12 // 15 degrees counterclockwise
});
var rightFist = self.attachAsset('boss1rightfist', {
anchorX: 0.5,
anchorY: 0.5,
visible: false,
rotation: Math.PI / 12 // 15 degrees clockwise
});
self.activate = function (x, y, isLeft) {
self.x = x;
self.y = y;
self.startX = x;
self.startY = y;
self.isLeft = isLeft;
self.visible = true;
self.extending = false;
self.retracting = false;
// Show correct fist, hide other
leftFist.visible = isLeft;
rightFist.visible = !isLeft;
};
self.extend = function (targetX, targetY) {
self.targetX = targetX;
self.targetY = targetY;
self.extending = true;
self.retracting = false;
};
self.update = function () {
if (!self.visible) {
return;
}
if (self.extending || self.retracting) {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > self.speed) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
// Update chain segments
var segmentSpacing = 40;
var numSegments = Math.floor(dist / segmentSpacing);
// Clean up old segments
while (self.chainSegments.length > 0) {
chainSegmentPool.recycle(self.chainSegments.pop());
}
// Create new segments
for (var i = 1; i <= numSegments; i++) {
var t = i / numSegments;
var segX = self.startX + (self.x - self.startX) * t;
var segY = self.startY + (self.y - self.startY) * t;
var segment = chainSegmentPool.spawn(segX, segY);
if (segment) {
var angle = Math.atan2(dy, dx);
segment.rotation = angle;
self.chainSegments.push(segment);
}
}
} else {
if (self.extending) {
self.extending = false;
// Pause briefly at extension
LK.setTimeout(function () {
self.retract();
}, 30);
} else if (self.retracting) {
self.deactivate();
}
}
}
};
self.retract = function () {
self.targetX = self.startX;
self.targetY = self.startY;
self.extending = false;
self.retracting = true;
};
self.deactivate = function () {
while (self.chainSegments.length > 0) {
chainSegmentPool.recycle(self.chainSegments.pop());
}
self.visible = false;
self.extending = false;
self.retracting = false;
leftFist.visible = false;
rightFist.visible = false;
};
// Initialize properties
self.chainSegments = [];
self.speed = 15;
return self;
});
// In the BossHealthBar class, add position setting
// Modify the BossHealthBar class
var BossHealthBar = Container.expand(function () {
var self = Container.call(this);
var emptyMeter = self.attachAsset('meter', {
anchorX: 0.5,
anchorY: 1.0,
tint: 0x333333,
// Dark gray for empty
scaleX: 0 // Start with zero width
});
var healthMeter = self.attachAsset('meter', {
anchorX: 0.5,
anchorY: 1.0,
tint: 0xff0000,
// Red for health
scaleX: 0 // Start with zero width
});
// Position settings
self.x = 2048 / 2; // Center horizontally
self.y = 2732 - 100; // Position from the top
self.show = function () {
self.visible = true;
// Reset both meters to zero width
healthMeter.scaleX = 0;
if (self.currentTween) {
self.currentTween.stop();
}
// First animate the empty meter
self.currentTween = tween(emptyMeter, {
scaleX: 1
}, {
duration: 800,
easing: tween.easeOutBack,
onFinish: function onFinish() {
// Then animate the health meter
self.currentTween = tween(healthMeter, {
scaleX: 1
}, {
duration: 400,
easing: tween.easeOut
});
}
});
};
self.hide = function () {
if (self.currentTween) {
self.currentTween.stop();
}
// First shrink the health meter
self.currentTween = tween(healthMeter, {
scaleX: 0
}, {
duration: 400,
easing: tween.easeIn,
onFinish: function onFinish() {
// Then shrink the empty meter
self.currentTween = tween(emptyMeter, {
scaleX: 0
}, {
duration: 800,
easing: tween.easeIn,
onFinish: function onFinish() {
self.visible = false;
}
});
}
});
};
self.updateHealth = function (percent) {
// Only animate the health meter, not the empty meter
if (self.healthTween) {
self.healthTween.stop();
}
self.healthTween = tween(healthMeter, {
scaleX: percent
}, {
duration: 300,
easing: tween.easeOut
});
};
return self;
});
var BossText = Container.expand(function () {
var self = Container.call(this);
var text = self.attachAsset('bosstext', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
self.x = 2048 / 2; // Center horizontally
self.y = 2732 / 2; // Center vertically
self.flashCount = 0;
self.maxFlashes = 3;
self.flash = function () {
self.flashCount = 0;
self.doFlash();
};
self.doFlash = function () {
if (self.flashCount >= self.maxFlashes) {
text.alpha = 0;
return;
}
if (self.currentTween) {
self.currentTween.stop();
}
LK.getSound('alert').play();
// Fade in
self.currentTween = tween(text, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
// Hold
LK.setTimeout(function () {
// Fade out
self.currentTween = tween(text, {
alpha: 0,
scaleX: 1,
scaleY: 1
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
self.flashCount++;
// Wait before next flash
LK.setTimeout(function () {
self.doFlash();
}, 20);
}
});
}, 40);
}
});
};
return self;
});
var ChainSegment = Container.expand(function () {
var self = Container.call(this);
var link = self.attachAsset('fistlink', {
anchorX: 0.5,
anchorY: 0.5
});
self.activate = function (x, y, rotation) {
self.x = x;
self.y = y;
self.rotation = rotation;
self.visible = true;
};
self.deactivate = function () {
self.visible = false;
};
return self;
});
var Cloud = Container.expand(function () {
var self = Container.call(this);
var cloud = self.attachAsset('cloudShape', {
anchorX: 0.5,
anchorY: 0.5
});
self.activate = function (x, y, scale, alpha) {
self.x = x;
self.y = y;
self.visible = true;
self.speedY = 1 + Math.random() * 2;
self.speedX = (Math.random() - 0.5) * 1.5;
self.targetY = self.horizonY;
cloud.scaleX = scale;
cloud.scaleY = scale * 0.8;
cloud.alpha = alpha || 0.8;
};
self.deactivate = function () {
self.visible = false;
};
return self;
});
var DefeatParticle = Container.expand(function () {
var self = Container.call(this);
var particle = self.attachAsset('particle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.15,
scaleY: 0.15
});
self.activate = function (x, y, tint) {
self.x = x + (Math.random() * 40 - 20); // Wider initial spread
self.y = y + (Math.random() * 40 - 20); // Wider initial spread
self.visible = true;
self.age = 0;
self.lifespan = 225; // Tripled lifespan for longer visual impact
// More dramatic explosion velocities
var angle = Math.PI * 2 * Math.random();
var speed = 15 + Math.random() * 25; // Adjusted speed for more variance
self.vx = Math.cos(angle) * speed;
self.vy = Math.sin(angle) * speed;
self.rotation = Math.random() * Math.PI * 2;
particle.alpha = 1;
// Larger, more varied particle sizes
var scaleVariance = 0.3 + Math.random() * 0.6; // Increased variance for scale
particle.scaleX = scaleVariance;
particle.scaleY = scaleVariance;
// Apply tint if provided
if (tint !== undefined) {
particle.tint = tint;
} else {
particle.tint = 0xffffff; // Default white
}
};
self.update = function () {
// Check bounds first
if (self.y < -50 || self.y > 2732 || self.x < -50 || self.x > 2048) {
defeatParticlePool.recycle(self);
return;
}
self.x += self.vx;
self.y += self.vy;
self.age++;
// Keep strong deceleration
self.vx *= 0.87;
self.vy *= 0.87;
var lifePercent = self.age / self.lifespan;
// Fade out curve
particle.alpha = Math.max(0, 1 - lifePercent * 1.7);
// More dramatic shrink effect as particles fade
var scale = (0.5 - 0.3 * lifePercent) * (1 - lifePercent * 0.5);
particle.scaleX = scale;
particle.scaleY = scale;
if (lifePercent >= 1) {
defeatParticlePool.recycle(self);
}
};
self.deactivate = function () {
self.visible = false;
// Reset tint when deactivated
particle.tint = 0xffffff;
};
return self;
});
var DragonHead = Container.expand(function () {
var self = Container.call(this);
// Dragon sprites
var body = self.attachAsset('dragonBody', {
anchorX: 0.5,
anchorY: 0.7,
scaleX: 2,
scaleY: 2
});
var head = self.attachAsset('dragonHead', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
var headOpen = self.attachAsset('dragonHeadOpen', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2,
alpha: 0
});
var lookLeft = self.attachAsset('dragonLook', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: -2,
// Mirror for left direction
scaleY: 2,
alpha: 0
});
var lookRight = self.attachAsset('dragonLook', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2,
alpha: 0
});
var lookLeftOpen = self.attachAsset('dragonLookOpen', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: -2,
// Mirror for left direction
scaleY: 2,
alpha: 0
});
var lookRightOpen = self.attachAsset('dragonLookOpen', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2,
alpha: 0
});
self.stateBuffer = [];
self.bufferSize = 10; // Adjust 3-5 frames for balance between stability and responsiveness
self.currentState = 'forward';
// Add smoothing properties
self.mouthOpenness = 0; // Track mouth state from 0 (closed) to 1 (open)
self.mouthSmoothingSpeed = 0.2; // Adjust this value to change transition speed
self.currentScale = 2; // Starting scale
var horizontalSpeed = 0;
self.invulnerableTime = 0;
self.invulnerableDuration = 30;
// Position tracking variables
var targetX = 2048 / 2;
var targetY = 2732 * 0.2;
var smoothingFactor = 0.12;
var prevX = null;
var prevY = null;
// Rotation variables
var targetTilt = 0;
var tiltSmoothingFactor = 0.11;
var tiltScaleFactor = 0.09;
// Scale tracking
var scaleHistory = new Array(5).fill(2); // Start with default scale of 2
var scaleIndex = 0;
self.update = function () {
// Add in self.update function
if (self.invulnerableTime > 0) {
self.invulnerableTime--;
}
// Position tracking
if (facekit.noseTip) {
targetX = facekit.noseTip.x;
targetY = facekit.noseTip.y - 800;
// Initialize previous positions if not set
if (prevX === null) {
prevX = targetX;
prevY = targetY;
}
// Weighted average between previous and target position
var newX = prevX * (1 - smoothingFactor) + targetX * smoothingFactor;
var newY = prevY * (1 - smoothingFactor) + targetY * smoothingFactor;
self.x = newX;
if (newY > dragonShadow.y - 100) {
// -100 for visual offset so dragon stays above shadow
newY = dragonShadow.y - 100;
}
self.y = newY;
if (prevX !== null) {
horizontalSpeed = Math.abs(self.x - prevX);
}
self.currentShakeSpeed = horizontalSpeed;
// Update previous positions
prevX = newX;
prevY = newY;
}
// Rotation tracking - CORRECTED VERSION
if (facekit.leftEye && facekit.rightEye) {
targetTilt = calculateFaceTilt() * tiltScaleFactor;
// Limit rotation to ±15 degrees - DON'T convert to radians here
targetTilt = Math.max(-15, Math.min(15, targetTilt));
self.rotation += (targetTilt - self.rotation) * tiltSmoothingFactor;
}
if (facekit.leftEye && facekit.rightEye && facekit.noseTip) {
// Calculate face direction
var faceCenter = (facekit.leftEye.x + facekit.rightEye.x) / 2;
var noseDiff = facekit.noseTip.x - faceCenter;
var eyeYDiff = facekit.rightEye.y - facekit.leftEye.y;
var newState = 'forward';
if (noseDiff < -120 && Math.abs(eyeYDiff) < 30 || eyeYDiff > 50) {
newState = 'left';
} else if (noseDiff > 120 && Math.abs(eyeYDiff) < 30 || eyeYDiff < -50) {
newState = 'right';
}
// Add to buffer
self.stateBuffer.push(newState);
if (self.stateBuffer.length > self.bufferSize) {
self.stateBuffer.shift();
}
// Only change state if buffer is unanimous
if (self.stateBuffer.length === self.bufferSize) {
var unanimous = self.stateBuffer.every(function (state) {
return state === self.stateBuffer[0];
});
if (unanimous) {
self.currentState = self.stateBuffer[0];
}
}
// Reset visibilities based on current state
lookLeft.alpha = 0;
lookRight.alpha = 0;
lookLeftOpen.alpha = 0;
lookRightOpen.alpha = 0;
head.alpha = 0;
headOpen.alpha = 0;
if (self.currentState === 'forward') {
if (facekit.mouthOpen) {
headOpen.alpha = 1;
} else {
head.alpha = 1;
}
} else if (self.currentState === 'left') {
if (facekit.mouthOpen) {
lookLeftOpen.alpha = 1;
} else {
lookLeft.alpha = 1;
}
} else if (self.currentState === 'right') {
if (facekit.mouthOpen) {
lookRightOpen.alpha = 1;
} else {
lookRight.alpha = 1;
}
}
}
// Scale adjustment based on face size
if (facekit.leftEye && facekit.rightEye) {
var eyeDistance = Math.abs(facekit.rightEye.x - facekit.leftEye.x);
var newScale = eyeDistance / 250; // Adjusted divisor for dragon head
// Update rolling average
scaleHistory[scaleIndex] = newScale;
scaleIndex = (scaleIndex + 1) % scaleHistory.length;
// Calculate average scale
var avgScale = scaleHistory.reduce(function (a, b) {
return a + b;
}, 0) / scaleHistory.length;
// Apply with gentle smoothing (limited range)
var targetScale = Math.max(1.5, Math.min(2.5, avgScale));
head.scaleX = head.scaleX * 0.85 + targetScale * 0.15;
self.currentScale = head.scaleX; // Track the current scale
head.scaleY = head.scaleY * 0.85 + targetScale * 0.15;
headOpen.scaleX = headOpen.scaleX * 0.85 + targetScale * 0.15;
headOpen.scaleY = headOpen.scaleY * 0.85 + targetScale * 0.15;
body.scaleX = body.scaleX * 0.85 + targetScale * 0.15;
body.scaleY = body.scaleY * 0.85 + targetScale * 0.15;
lookLeft.scaleX = lookLeft.scaleX * 0.85 - targetScale * 0.15;
lookLeft.scaleY = lookLeft.scaleY * 0.85 + targetScale * 0.15;
lookRight.scaleX = lookRight.scaleX * 0.85 + targetScale * 0.15;
lookRight.scaleY = lookRight.scaleY * 0.85 + targetScale * 0.15;
lookLeftOpen.scaleX = lookLeftOpen.scaleX * 0.85 - targetScale * 0.15;
lookLeftOpen.scaleY = lookLeftOpen.scaleY * 0.85 + targetScale * 0.15;
lookRightOpen.scaleX = lookRightOpen.scaleX * 0.85 + targetScale * 0.15;
lookRightOpen.scaleY = lookRightOpen.scaleY * 0.85 + targetScale * 0.15;
}
};
function calculateFaceTilt() {
if (facekit.leftEye && facekit.rightEye && facekit.mouthCenter) {
// Calculate midpoint between eyes
var eyeMidX = (facekit.leftEye.x + facekit.rightEye.x) / 2;
var eyeMidY = (facekit.leftEye.y + facekit.rightEye.y) / 2;
// Calculate angle between eye midpoint and mouth, negated to fix direction
var dx = facekit.mouthCenter.x - eyeMidX;
var dy = facekit.mouthCenter.y - eyeMidY;
var angle = -(Math.atan2(dx, dy) * (180 / Math.PI));
// Reduced angle impact
return Math.max(-15, Math.min(15, angle * 0.15));
}
return 0; // Default to straight when face points aren't available
}
self.flashDamage = function (damageAmount) {
// Show game over after effect
if (self.invulnerableTime > 0) {
return;
}
// Set invulnerability
self.invulnerableTime = self.invulnerableDuration;
// Flash effect
var dragonSprites = [body];
if (head && head.alpha > 0) {
dragonSprites.push(head);
}
if (headOpen && headOpen.alpha > 0) {
dragonSprites.push(headOpen);
}
if (lookLeft && lookLeft.alpha > 0) {
dragonSprites.push(lookLeft);
}
if (lookRight && lookRight.alpha > 0) {
dragonSprites.push(lookRight);
}
if (lookLeftOpen && lookLeftOpen.alpha > 0) {
dragonSprites.push(lookLeftOpen);
}
if (lookRightOpen && lookRightOpen.alpha > 0) {
dragonSprites.push(lookRightOpen);
}
dragonSprites.forEach(function (sprite) {
sprite.tint = 0xFF0000;
LK.setTimeout(function () {
sprite.tint = 0xFFFFFF;
}, 10);
});
// Apply damage and check for death
dragon.health -= damageAmount;
dragonHealthBar.updateHealth(dragon.health / 100);
if (dragon.health <= 0 && gameActive) {
LK.getSound('dragonScream').play();
gameActive = false;
// Explode dragon
for (var i = 0; i < 60; i++) {
defeatParticlePool.spawn(dragon.x, dragon.y, 0xff0000);
}
// Hide dragon
dragon.visible = false;
// Show game over after effect
LK.setTimeout(function () {
LK.showGameOver();
}, 2000);
}
};
return self;
});
var DragonHealthBar = Container.expand(function () {
var self = Container.call(this);
var emptyMeter = self.attachAsset('meter', {
anchorX: 0.5,
anchorY: 1.0,
tint: 0x333333,
scaleX: 1
});
var healthMeter = self.attachAsset('meter', {
anchorX: 0.5,
anchorY: 1.0,
tint: 0x00ff00,
scaleX: 1
});
// Position settings
self.x = 2048 / 2;
self.y = 220; // 100px from top + some padding
self.updateHealth = function (percent) {
if (self.healthTween) {
self.healthTween.stop();
}
self.healthTween = tween(healthMeter, {
scaleX: percent
}, {
duration: 300,
easing: tween.easeOut
});
};
return self;
});
var DragonShadow = Container.expand(function () {
var self = Container.call(this);
var shadow = self.attachAsset('dragonShadow', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
scaleX: 1,
scaleY: 0.3
});
self.x = 2048 / 2; // Match dragon's initial x
self.y = 2732 * 0.6; // Positioned below dragon
var positionSmoothing = 0.12;
var scaleSmoothing = 0.08;
self.update = function (dragonX, dragonY, dragonScale) {
if (dragonX !== undefined && dragonY !== undefined && dragonScale !== undefined) {
// X position follow
self.x += (dragonX - self.x) * positionSmoothing;
// Y position based on scale (Z-depth)
var baseY = 2732 * 0.6;
var scaleOffset = (dragonScale - 1) * 300;
var targetY = baseY + scaleOffset;
self.y += (targetY - self.y) * positionSmoothing;
// Scale calculation based on dragon's Y position
var heightRatio = dragonY / 2732;
var scaleMultiplier = 0.2 + heightRatio * 1.5;
// Add Y-position based size adjustment (0-20% increase based on Y position)
var yPositionScale = 1 + self.y / 2732 * 0.2;
var targetScaleX = scaleMultiplier * yPositionScale;
var targetScaleY = scaleMultiplier * 0.3 * yPositionScale;
var shadow = self.getChildAt(0); // Get the shadow sprite
shadow.scaleX += (targetScaleX - shadow.scaleX) * scaleSmoothing;
shadow.scaleY += (targetScaleY - shadow.scaleY) * scaleSmoothing;
shadow.alpha = 0.3 + heightRatio * 0.2;
}
};
return self;
});
var DustParticle = Container.expand(function () {
var self = Container.call(this);
var dust = self.attachAsset('dust', {
anchorX: 0.5,
anchorY: 0.5
});
self.activate = function (x, y) {
self.x = x + (Math.random() * 20 - 10);
self.y = y;
self.visible = true;
self.age = 0;
self.lifespan = 60; // Fixed lifespan for consistency
dust.alpha = 0.9;
// Vertical velocity - move upward
self.vy = -1.5; // Faster upward movement
self.vx = (Math.random() - 0.5) * 5; // Slightly more horizontal movement
// Add rotation for visual interest
self.rotation = Math.random() * Math.PI * 2;
};
self.update = function () {
// Update position
self.x += self.vx;
self.y += self.vy;
self.age++;
// Calculate life percentage
var lifePercent = self.age / self.lifespan;
// Fade out
dust.alpha = 0.8 * (1 - lifePercent);
// Grow slightly as it rises
var scale = 1 + lifePercent * 0.2; // Linear growth from 0.2 to 0.3
dust.scaleX = scale;
dust.scaleY = scale;
// Recycle when lifetime is over
if (lifePercent >= 1) {
dustParticlePool.recycle(self);
}
};
self.deactivate = function () {
self.visible = false;
};
return self;
});
var Enemy = Container.expand(function () {
var self = Container.call(this);
// States
var RUNNING = 'running';
var LEAPING = 'leaping';
var ATTACHED = 'attached';
var FALLING = 'falling';
// Running enemy sprites
var runSprite = self.attachAsset('enemy1', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 1
});
var runSpriteMirrored = self.attachAsset('enemy1', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: -1,
alpha: 0
});
// Properties
self.health = 1;
self.rotationSpeed = 0;
self.animTick = 0;
self.animRate = 30;
self.pulseRate = self.animRate * 2;
self.state = RUNNING;
self.leapSpeed = 9;
self.targetX = 0;
self.targetY = 0;
self.hasExploded = false; // Add this flag
self.activate = function (x, y) {
self.visible = true;
self.x = x;
self.y = y;
self.state = RUNNING;
self.rotation = 0; // Reset rotation
self.health = 1;
self.rotationSpeed = 0;
self.fallSpeed = 0;
self.acceleration = 0;
self.leapDuration = 0;
self.shadow = enemyShadowPool.spawn(x, y, self.scaleX);
self.isPulsedOut = false;
self.hasExploded = false; // Reset explosion flag
// If there was a tween running, stop it
if (self.currentTween) {
self.currentTween.stop();
self.currentTween = null;
}
// Reset animation state
self.animTick = 0;
runSprite.alpha = 1;
runSpriteMirrored.alpha = 0;
// Set initial scale based on distance from horizon
var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY);
var scale = 0.5 + progress;
self.scaleX = scale;
self.scaleY = scale;
};
function checkLeapDistance() {
// Get distance on Y-axis only
var dy = Math.abs(self.y - dragonShadow.y);
// Get relative scales as depth indicators
var enemyDepth = self.scaleX;
var shadowDepth = dragonShadow.scaleX;
// Leap when on same Y-plane regardless of X position
var depthDiff = Math.abs(enemyDepth - shadowDepth);
var yThreshold = 10; // Close on the Y-axis
// If the enemy is on screen and at the right Y position
if (dy < yThreshold && depthDiff < 0.3 && self.x > 0 && self.x < 2048) {
startLeap();
return true;
}
return false;
}
function startLeap() {
self.state = LEAPING;
LK.getSound('orcShout').play();
// Store where the dragon was when leap started
self.leapTargetX = dragon.x;
self.leapTargetY = dragon.y;
var dx = self.leapTargetX - self.x;
var dy = self.leapTargetY - self.y;
self.initialLeapDistance = Math.sqrt(dx * dx + dy * dy);
self.initialScale = self.scaleX;
self.isPulsedOut = false;
runSprite.alpha = 1;
runSpriteMirrored.alpha = 0;
}
self.update = function () {
if (!self.visible) {
return;
}
switch (self.state) {
case RUNNING:
// Existing running code
var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY);
var speed = (flyingBackground.scrollSpeed * 0.05 + flyingBackground.scrollSpeed * (progress * 2.5)) * 0.8;
self.y -= speed;
// Update scale based on distance
var scale = 0.5 + progress;
self.scaleX = scale;
self.scaleY = scale;
// Running animation
self.animTick++;
if (self.animTick >= self.animRate) {
self.animTick = 0;
runSprite.alpha = runSprite.alpha === 0 ? 1 : 0;
runSpriteMirrored.alpha = runSpriteMirrored.alpha === 0 ? 1 : 0;
}
// In the Enemy class RUNNING state section, replace the dust spawning code with:
if (LK.ticks % 10 === 0 && self.y < 2732) {
// More frequent spawning (every 10 ticks)
var footY = self.y + 50 * self.scaleY;
dustParticlePool.spawn(self.x, footY);
}
// Check for leap
checkLeapDistance();
break;
case LEAPING:
// Calculate current distance to dragon (not original leap target)
var dragonDx = dragon.x - self.x;
var dragonDy = dragon.y - self.y;
var dragonDistance = Math.sqrt(dragonDx * dragonDx + dragonDy * dragonDy);
// If we're very close to dragon, attach
if (dragonDistance < 50) {
self.state = ATTACHED;
break;
}
// Continue homing leap motion
if (dragonDistance > 10) {
var dirX = dragonDx / dragonDistance;
var dirY = dragonDy / dragonDistance;
var speedMultiplier = Math.min(1.5, dragonDistance / 100);
self.x += dirX * self.leapSpeed * speedMultiplier;
self.y += dirY * self.leapSpeed * speedMultiplier;
self.scaleX = self.initialScale * 0.75;
self.scaleY = self.initialScale * 0.75;
}
// Add safety timeout to prevent infinite leaps
self.leapDuration = (self.leapDuration || 0) + 1;
if (self.leapDuration > 100) {
self.state = FALLING;
self.rotationSpeed = (Math.random() * 0.1 + 0.05) * (Math.random() < 0.5 ? 1 : -1);
self.fallSpeed = 2;
self.acceleration = 0.2;
}
break;
case ATTACHED:
// Stick to dragon position
self.x = dragon.x;
self.y = dragon.y;
// Check for shake-off
if (dragon.currentShakeSpeed > 30) {
// Threshold for shake-off
self.state = FALLING;
self.rotationSpeed = (Math.random() * 0.1 + 0.05) * (Math.random() < 0.5 ? 1 : -1);
self.fallSpeed = 2;
self.acceleration = 0.2;
break;
}
// Pulsing animation
self.animTick++;
if (self.animTick >= self.pulseRate) {
self.animTick = 0;
// Pulse between 0.7 and 0.8 scale
tween(self, {
scaleX: self.initialScale * (self.isPulsedOut ? 0.7 : 0.8),
scaleY: self.initialScale * (self.isPulsedOut ? 0.7 : 0.8)
}, {
duration: 200,
easing: tween.easeOut
});
self.isPulsedOut = !self.isPulsedOut;
dragon.flashDamage(2);
}
break;
case FALLING:
self.rotation += self.rotationSpeed;
self.fallSpeed += self.acceleration;
self.y += self.fallSpeed;
// Check for shadow collision with explosion flag
if (self.y >= dragonShadow.y && !self.hasExploded) {
self.hasExploded = true;
// Create MUCH bigger explosion with red tint
for (var i = 0; i < 20; i++) {
defeatParticlePool.spawn(self.x, self.y, 0xff0000); // Red tint (0xff0000)
}
score += 50;
scorePopupPool.spawn(self.x, self.y, 50);
scoreTxt.setText(score);
updateScoreMeter(score);
LK.setScore(score);
LK.getSound('enemyDefeat').play();
trySpawnHealthPowerup(self.x, self.y);
enemyPool.recycle(self);
}
break;
}
if (self.shadow) {
self.shadow.update(self);
}
};
self.hit = function () {
self.health--;
if (self.health <= 0) {
if (self.state !== FALLING) {
self.state = FALLING;
self.rotationSpeed = (Math.random() * 0.1 + 0.05) * (Math.random() < 0.5 ? 1 : -1);
self.fallSpeed = 2;
self.acceleration = 0.2;
return true;
}
}
return false;
};
self.deactivate = function () {
self.visible = false;
runSprite.alpha = 1;
runSpriteMirrored.alpha = 0;
if (self.shadow) {
enemyShadowPool.recycle(self.shadow);
self.shadow = null;
}
};
return self;
});
var EnemyArrow = Container.expand(function () {
var self = Container.call(this);
var arrow = self.attachAsset('enemyArrow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5
});
self.speed = 8;
self.update = function () {
if (!self.active) {
return;
}
self.x += self.vx;
self.y += self.vy;
// Point arrow in direction of travel
self.rotation = Math.atan2(self.vy, self.vx) + Math.PI / 2;
if (self.y < 0 || self.y > 2732 || self.x < 0 || self.x > 2048) {
enemyArrowPool.recycle(self);
}
};
self.activate = function (x, y, targetX, targetY) {
self.x = x;
self.y = y;
self.visible = true;
self.active = true;
// Calculate direction to target
var dx = targetX - x;
var dy = targetY - y;
var dist = Math.sqrt(dx * dx + dy * dy);
self.vx = dx / dist * self.speed;
self.vy = dy / dist * self.speed;
};
self.deactivate = function () {
self.visible = false;
self.active = false;
};
return self;
});
var EnemyShadow = Container.expand(function () {
var self = Container.call(this);
var shadow = self.attachAsset('enemyShadow', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3,
scaleX: 1,
scaleY: 0.3
});
// In the EnemyShadow class, update method (around line 1018)
self.update = function (enemy) {
if (!enemy || !enemy.visible || enemy.hasExploded) {
self.visible = false;
return;
}
self.visible = true;
// Always follow enemy's X position
self.x = enemy.x;
// For running enemies, shadow is slightly below them
if (enemy.state === 'running') {
self.y = enemy.y + 200; // Shadow slightly below enemy
}
// For leaping enemies, shadow moves to dragon shadow position
else if (enemy.state === 'leaping') {
self.y = dragonShadow.y + 50; // Position just below dragon shadow
}
// For attached enemies, follow dragon shadow
else if (enemy.state === 'attached') {
self.y = dragonShadow.y + 50; // Keep following dragon shadow
}
// For falling enemies, shadow holds last position
else if (enemy.state === 'falling') {
// Y position stays the same
}
// Special case for EnemyTwo type
else if (enemy.type === 2) {
self.y = enemy.y + 600; // Position higher for enemy type 2
}
// Scale shadow based on enemy's scale
shadow.scaleX = enemy.scaleX * 1.2;
shadow.scaleY = enemy.scaleX * 0.3;
};
self.activate = function (x, y, enemyScale) {
self.x = x;
self.y = y + 200; // Initially position shadow slightly below enemy
self.visible = true;
shadow.scaleX = (enemyScale || 1) * 1.2;
shadow.scaleY = (enemyScale || 1) * 0.3;
};
self.deactivate = function () {
self.visible = false;
};
return self;
});
var EnemyThree = Container.expand(function () {
var self = Container.call(this);
// States
var GROUNDED = 'grounded';
var FLYING = 'flying';
// Main sprite
var sprite = self.attachAsset('enemy3', {
anchorX: 0.5,
anchorY: 0.5
});
// Jet flames
var leftFlame = new JetFlame();
var rightFlame = new JetFlame();
self.addChild(leftFlame);
self.addChild(rightFlame);
// Properties
self.health = 1;
self.state = GROUNDED;
self.hasExploded = false;
self.activate = function (x, y) {
self.x = x;
self.y = y;
self.stateDuration = 0;
self.visible = true;
self.state = GROUNDED;
self.health = 1;
self.hasExploded = false;
// Add this line to create the shadow
self.shadow = enemyShadowPool.spawn(x, y, self.scaleX);
// Position flames at percentage-based positions
var flameY = sprite.height * 0.45;
var flameX = sprite.width * 0.15;
leftFlame.x = -flameX;
leftFlame.y = flameY;
rightFlame.x = flameX;
rightFlame.y = flameY;
// Initial scale based on distance from horizon
var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY);
var scale = 0.5 + progress;
self.scaleX = scale;
self.scaleY = scale;
};
self.startFlying = function () {
self.state = FLYING;
// Calculate rough trajectory toward dragon
var dx = dragon.x - self.x;
var dy = dragon.y - self.y;
var angle = Math.atan2(dy, dx);
// Add intentional inaccuracy (they're bad at this)
angle += (Math.random() - 0.5) * 0.5; // ±0.25 radians of error
// Set velocities
var speed = 8;
self.vx = Math.cos(angle) * speed;
self.vy = Math.sin(angle) * speed;
// Start jet flames
leftFlame.startPulse();
rightFlame.startPulse();
};
self.update = function () {
if (!self.visible) {
return;
}
if (self.state === FLYING) {
self.stateDuration++;
}
switch (self.state) {
case GROUNDED:
// Move with background scroll
var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY);
var speed = (flyingBackground.scrollSpeed * 0.05 + flyingBackground.scrollSpeed * (progress * 2.5)) * 0.8;
self.y -= speed;
// Scale with distance
var scale = 0.5 + progress;
self.scaleX = scale;
self.scaleY = scale;
// Check for ignition trigger
if (self.y <= dragonShadow.y + 50) {
self.startFlying();
}
break;
case FLYING:
if (self.stateDuration === 1) {
LK.getSound('jetPack').play(); // Play jetPack sound effect
}
// Update position
self.x += self.vx;
self.y += self.vy;
// Create jet particles
if (Math.random() < 0.3) {
var particle = particlePool.spawn(self.x + leftFlame.x, self.y + leftFlame.y);
if (particle) {
particle.vy = Math.abs(particle.vy); // Force downward
}
particle = particlePool.spawn(self.x + rightFlame.x, self.y + rightFlame.y);
if (particle) {
particle.vy = Math.abs(particle.vy); // Force downward
}
}
// Check bounds
if (self.y < -100 || self.y > 2732 || self.x < -100 || self.x > 2148) {
enemyThreePool.recycle(self);
}
break;
}
if (self.shadow) {
self.shadow.update(self);
}
// In EnemyThree class, modify the part of the update method that handles the shadow:
// Update shadow position
if (self.shadow) {
if (self.state === GROUNDED) {
self.shadow.y = self.y + 200; // Shadow slightly below enemy
self.shadow.x = self.x;
} else if (self.state === FLYING) {
self.shadow.y = dragonShadow.y + 50;
self.shadow.x = self.x;
// Don't update Y position to let it stay where it was
}
// Scale shadow based on enemy's scale
var shadowSprite = self.shadow.getChildAt(0);
shadowSprite.scaleX = self.scaleX * 1.2;
shadowSprite.scaleY = self.scaleX * 0.3;
// Update shadow alpha based on height
var heightRatio = self.y / 2732;
shadowSprite.alpha = 0.3 + heightRatio * 0.2;
}
};
self.hit = function () {
self.health--;
if (self.health <= 0 && !self.hasExploded) {
self.hasExploded = true;
// Create explosion
for (var i = 0; i < 20; i++) {
defeatParticlePool.spawn(self.x, self.y, 0xff0000);
}
score += 25;
scorePopupPool.spawn(self.x, self.y, 25);
scoreTxt.setText(score.toString());
updateScoreMeter(score);
LK.setScore(score);
LK.getSound('enemyDefeat').play();
trySpawnHealthPowerup(self.x, self.y);
enemyThreePool.recycle(self);
return true;
}
return false;
};
self.deactivate = function () {
self.visible = false;
if (self.shadow) {
enemyShadowPool.recycle(self.shadow);
self.shadow = null;
}
leftFlame.deactivate();
rightFlame.deactivate();
};
return self;
});
var EnemyTwo = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('enemy2', {
anchorX: 0.5,
anchorY: 0.5
});
// Properties
self.health = 1;
self.type = 2; // For scoring
self.state = 'approaching'; // approaching, holding
self.holdDistance = 800;
self.bobOffset = 0;
self.bobSpeed = 0.05;
self.lastShotTime = 0;
self.shotInterval = 2000; // 2 seconds
self.activate = function (x, y) {
self.visible = true;
self.x = x;
self.y = y;
self.state = 'approaching';
self.health = 1;
self.rotation = 0;
self.hasExploded = false; // Make sure this flag is set
// Ensure shadow is created and stored
if (!self.shadow) {
self.shadow = enemyShadowPool.spawn(x, y, self.scaleX);
}
// Moving direction
self.moveRight = x < 1024; // If spawned on left side, move right
// Initial scale based on distance from horizon
var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY);
self.scaleX = 0.5 + progress;
self.scaleY = 0.5 + progress;
};
self.update = function () {
if (!self.visible) {
return;
}
switch (self.state) {
case 'approaching':
// Bobbing motion
self.bobOffset = Math.sin(LK.ticks * self.bobSpeed) * 50;
// Move towards dragon
var dx = dragon.x - self.x;
var dy = dragon.y - self.y + self.bobOffset;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > self.holdDistance) {
self.x += dx * 0.01; // Reduced from 0.02
self.y += dy * 0.01; // Reduced from 0.02
// Modified scale calculation to maintain larger size
var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY);
self.scaleX = 0.7 + progress * 0.5; // Modified from 0.5 + progress
self.scaleY = 0.7 + progress * 0.5; // Modified from 0.5 + progress
} else {
self.state = 'holding';
}
break;
case 'holding':
// Maintain position relative to dragon
var dx = dragon.x - self.x;
var targetX = self.x + dx * 0.01;
// Keep on screen
targetX = Math.max(100, Math.min(1948, targetX));
self.x = targetX;
// Stay below dragon with offset
var targetY = dragon.y + 800; // Stay 200 pixels below dragon
var currentY = self.y - self.bobOffset; // Remove bobbing to get true position
self.y = currentY + (targetY - currentY) * 0.1; // Smooth movement to target
// Apply bobbing to final position
self.y += self.bobOffset;
// Shoot at intervals
var now = Date.now();
if (now - self.lastShotTime >= self.shotInterval) {
self.shoot();
self.lastShotTime = now;
}
break;
case 'falling':
self.rotation += self.rotationSpeed;
self.fallSpeed += self.acceleration;
self.y += self.fallSpeed;
// Check for ground collision
if (self.shadow && self.y >= self.shadow.y && !self.hasExploded) {
self.hasExploded = true;
for (var i = 0; i < 20; i++) {
defeatParticlePool.spawn(self.x, self.y, 0xff0000);
}
score += 75;
scorePopupPool.spawn(self.x, self.y, 75);
scoreTxt.setText(score);
updateScoreMeter(score);
LK.setScore(score);
LK.getSound('enemyDefeat').play();
trySpawnHealthPowerup(self.x, self.y);
enemyTwoPool.recycle(self);
}
break;
}
if (self.shadow) {
self.shadow.update(self);
}
};
self.shoot = function () {
var arrow = enemyArrowPool.spawn(self.x, self.y);
if (arrow) {
arrow.activate(self.x, self.y, dragon.x, dragon.y);
LK.getSound('arrow').play(); // Play arrow sound effect
}
};
self.hit = function () {
self.health--;
if (self.health <= 0 && self.state !== 'falling') {
// Prevent double-triggers
if (Math.random() < 0.5) {
// Mid-air explosion
for (var i = 0; i < 20; i++) {
defeatParticlePool.spawn(self.x, self.y, 0xff0000);
}
score += 75;
scorePopupPool.spawn(self.x, self.y, 75);
scoreTxt.setText(score);
updateScoreMeter(score);
LK.setScore(score);
LK.getSound('enemyDefeat').play();
trySpawnHealthPowerup(self.x, self.y);
enemyTwoPool.recycle(self);
} else {
// Fall and explode
self.state = 'falling';
self.rotationSpeed = (Math.random() * 0.1 + 0.05) * (Math.random() < 0.5 ? 1 : -1);
self.fallSpeed = 2;
self.acceleration = 0.2;
}
return true;
}
return false;
};
self.deactivate = function () {
self.visible = false;
if (self.shadow) {
enemyShadowPool.recycle(self.shadow);
self.shadow = null;
}
};
return self;
});
var FieldElement = Container.expand(function () {
var self = Container.call(this);
var element = self.attachAsset('fieldElement', {
anchorX: 0.5,
anchorY: 0.5
});
self.activate = function (x, y, scale, alpha) {
self.x = x;
self.y = y;
self.visible = true;
self.speedY = 2 + Math.random() * 3;
self.initialX = x;
element.scaleX = scale;
element.scaleY = scale * 0.8;
element.alpha = alpha || 0.9;
};
self.deactivate = function () {
self.visible = false;
};
return self;
});
var FireParticle = Container.expand(function () {
var self = Container.call(this);
var particle = self.attachAsset('fireParticle', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
self.x += self.vx;
self.y += self.vy;
self.age++;
// Update alpha
var lifePercent = self.age / self.lifespan;
particle.alpha = 1 - lifePercent;
// Recycle if lifetime is over OR particle is off screen
if (lifePercent >= 1 || self.y < 0 || self.y > 2732 ||
// Screen height
self.x < 0 || self.x > 2048) {
// Screen width
particlePool.recycle(self);
}
};
self.activate = function (x, y) {
self.x = x;
self.y = y;
self.visible = true;
self.vx = Math.random() * 8 - 4; // Doubled velocity range
self.vy = -(Math.random() * 4 + 2); // Make particles move upward
self.lifespan = 45 + Math.random() * 30; // Increased lifespan
self.age = 0;
particle.alpha = 1;
// Add scaling
particle.scaleX = 0.5 + Math.random() * 0.5;
particle.scaleY = 0.5 + Math.random() * 0.5;
};
self.deactivate = function () {
self.visible = false;
};
return self;
});
// In the Fireball class, update the properties and update method
var Fireball = Container.expand(function () {
var self = Container.call(this);
var fireballGraphic = self.attachAsset('fireball', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 10;
self.vx = 0;
// Add these new properties
self.weaveDirection = 1; // Will be set to 1 or -1 on activate
self.weaveAmplitude = 50; // Adjust this to control width of weave
self.weaveFrequency = 0.1; // Adjust this to control speed of weave
self.initialY = 0; // Track starting Y position
self.update = function () {
if (!self.active) {
return;
}
// Update base position along trajectory
self.y += self.speed;
self.x += self.vx;
// Add weaving motion
var distanceTraveled = self.y - self.initialY;
var weaveOffset = Math.sin(distanceTraveled * self.weaveFrequency) * self.weaveAmplitude * self.weaveDirection;
self.x += weaveOffset;
// Calculate rotation based only on base trajectory, ignoring weave
var angle = Math.atan2(self.speed, self.vx) - Math.PI / 2;
self.rotation = angle;
// Create fire trail particles
if (Math.random() < 0.3) {
particlePool.spawn(self.x + (Math.random() * 40 - 20), self.y + 20);
}
// In Fireball class update method, replace the recycle check with:
// In Fireball class update method, replace the recycle check with:
var groundImpactY = 2732 * 0.9;
// Make impact decision only when first crossing the threshold
if (self.y > groundImpactY && self.lastY <= groundImpactY && !self.hasImpacted && self.x > 100 && self.x < 1948) {
if (Math.random() < 0.5) {
self.hasImpacted = true;
flamePool.spawn(self.x, groundImpactY);
scorchPool.spawn(self.x, groundImpactY);
// Spawn more particles
for (var i = 0; i < 15; i++) {
// Increased from 8 to 15
var particle = particlePool.spawn(self.x + (Math.random() * 40 - 20), groundImpactY);
if (particle) {
particle.vy = -(Math.random() * 6 + 3); // More upward velocity
particle.vx = (Math.random() - 0.5) * 8; // More spread
}
}
fireballPool.recycle(self);
return;
}
}
// Track last Y position
self.lastY = self.y;
// Only recycle if off screen
if (self.y > 2732 || self.x < -100 || self.x > 2148) {
fireballPool.recycle(self);
}
};
self.activate = function (x, y) {
self.x = x;
self.y = y;
self.initialY = y;
self.lastY = y;
self.visible = true;
self.active = true;
self.hasImpacted = false;
self.vx = 0;
self.rotation = 0;
fireballGraphic.alpha = 1;
};
self.deactivate = function () {
self.visible = false;
self.active = false;
// Clear any references that might cause memory leaks
self.currentTween = null;
};
return self;
});
var FlameEffect = Container.expand(function () {
var self = Container.call(this);
var flame = self.attachAsset('flame', {
anchorX: 0.5,
anchorY: 1.0,
scaleX: 1,
scaleY: 0.5
});
self.activate = function (x, y) {
self.x = x;
self.y = y;
self.visible = true;
self.age = 0;
flame.scaleX = 0.75;
flame.scaleY = 0.5;
var initialHeight = flame.height * flame.scaleY;
var targetScale = 2.5;
if (self.currentTween) {
self.currentTween.stop();
}
self.currentTween = tween(flame, {
scaleY: targetScale,
y: -(initialHeight * (targetScale - 0.5)) / 2,
// Offset upward to compensate for scaling
alpha: 1
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
flamePool.recycle(self);
}
});
};
self.deactivate = function () {
self.visible = false;
if (self.currentTween) {
self.currentTween.stop();
self.currentTween = null;
}
flame.y = 0; // Reset position
};
return self;
});
var FlyingBackground = Container.expand(function () {
var self = Container.call(this);
// Create bottom container
self.bottomContainer = new Container();
self.addChild(self.bottomContainer);
// Store the horizon line
self.horizonY = 2732 / 2;
self.totalHeight = 2732 - self.horizonY;
self.fieldStripes = [];
// Create field stripes (keep this the same as original)
var nbStripes = 30;
var lastY = self.horizonY + 10;
var stripe = self.bottomContainer.attachAsset('fieldStripes', {
anchorX: 0.5,
anchorY: 0,
x: 2048 / 2,
y: lastY
});
var progress = Math.max(0, lastY - self.horizonY) / self.totalHeight;
stripe.height = 1 + 400 * progress;
self.fieldStripes.push({
sprite: stripe,
progress: progress
});
for (var i = 1; i < nbStripes; i++) {
var spacing = 400 * (lastY - self.horizonY) / self.totalHeight;
var y = lastY + spacing;
var stripe = self.bottomContainer.attachAsset('fieldStripes', {
anchorX: 0.5,
anchorY: 0,
x: 2048 / 2,
y: y
});
var progress = Math.max(0, y - self.horizonY) / self.totalHeight;
stripe.height = 1 + 400 * progress;
self.fieldStripes.push({
sprite: stripe,
progress: progress
});
lastY = y;
}
// Create top container
self.topContainer = new Container();
self.addChild(self.topContainer);
// Create sky background in top container
var sky = self.topContainer.attachAsset('skyBackground', {
anchorX: 0.5,
anchorY: 1.0,
x: 2048 / 2,
y: self.horizonY + 10
});
// Field scroll speed
self.scrollSpeed = 4;
// Create object pools
self.cloudPool = new ObjectPool(Cloud, 15);
self.fieldElementPool = new ObjectPool(FieldElement, 20);
// Add pools to containers
self.topContainer.addChild(self.cloudPool);
self.bottomContainer.addChild(self.fieldElementPool);
// Initialize clouds
for (var i = 0; i < 8; i++) {
// Place clouds above the screen
var startY = -600 - Math.random() * 600;
var distanceFromHorizon = Math.abs(startY - self.horizonY);
var startScale = 0.5 + distanceFromHorizon / 800;
var cloud = self.cloudPool.spawn(Math.random() * 2048, startY, startScale, 0.8);
if (cloud) {
cloud.horizonY = self.horizonY;
}
}
// Initialize field elements
for (var j = 0; j < 12; j++) {
var fieldStartY = self.horizonY + Math.random() * (2732 - self.horizonY);
var fieldDistanceFromHorizon = Math.abs(fieldStartY - self.horizonY);
var fieldStartScale = 0.5 + fieldDistanceFromHorizon / 1000;
var fieldElement = self.fieldElementPool.spawn(Math.random() * 2048, fieldStartY, fieldStartScale, 0.9);
if (fieldElement) {
fieldElement.horizonY = self.horizonY;
}
}
self.update = function () {
// Update field stripes (keep original code)
for (var i = 0; i < self.fieldStripes.length; i++) {
var stripe = self.fieldStripes[i];
stripe.progress = Math.max(0, stripe.sprite.y - self.horizonY) / self.totalHeight;
var tempSpeed = self.scrollSpeed * 0.05 + self.scrollSpeed * (stripe.progress * 3);
stripe.sprite.y -= tempSpeed;
}
// Sort stripes by Y position
self.fieldStripes.sort(function (a, b) {
return a.sprite.y - b.sprite.y;
});
// Reset stripes that moved above horizon
for (var i = 0; i < self.fieldStripes.length; i++) {
if (self.fieldStripes[i].sprite.y < self.horizonY) {
var lowestStripe = self.fieldStripes[self.fieldStripes.length - 1];
self.fieldStripes[i].sprite.y = lowestStripe.sprite.y + 400 * (lowestStripe.sprite.y - self.horizonY) / self.totalHeight;
self.fieldStripes[i].progress = Math.max(0, self.fieldStripes[i].sprite.y - self.horizonY) / self.totalHeight;
}
}
// Re-sort after repositioning
self.fieldStripes.sort(function (a, b) {
return a.sprite.y - b.sprite.y;
});
// Adjust stripe heights
for (var i = 0; i < self.fieldStripes.length - 1; i++) {
var currentStripe = self.fieldStripes[i];
var nextStripe = self.fieldStripes[i + 1];
currentStripe.sprite.height = nextStripe.sprite.y - currentStripe.sprite.y;
}
// Handle last stripe
var lastStripe = self.fieldStripes[self.fieldStripes.length - 1];
lastStripe.sprite.height = Math.max(1 + 400 * lastStripe.progress, self.horizonY + self.totalHeight - lastStripe.sprite.y + 100);
// Update clouds and field elements
updateCloudPool();
updateFieldElementPool();
};
function updateCloudPool() {
var activeObjects = self.cloudPool.getActiveObjects();
for (var i = 0; i < activeObjects.length; i++) {
var cloud = activeObjects[i];
// Move cloud toward horizon
cloud.y += cloud.speedY;
cloud.x += cloud.speedX;
// Get the cloud's sprite
var sprite = cloud.getChildAt(0);
// Calculate scale based on distance from horizon
var distanceFactor = Math.max(0.05, (cloud.y - self.horizonY) / -500);
sprite.scaleX = distanceFactor;
sprite.scaleY = distanceFactor * 0.8;
// Adjust alpha for fade effect near horizon
sprite.alpha = Math.min(0.9, distanceFactor);
// Reset cloud if it reaches horizon or gets too small
if (cloud.y >= self.horizonY - 10 || sprite.scaleX < 0.1) {
// Recycle and spawn new cloud above screen
self.cloudPool.recycle(cloud);
var newCloud = self.cloudPool.spawn(Math.random() * 2048,
// Random x position
-600 - Math.random() * 400,
// Above screen
0.5 + 600 / 800,
// Scale based on distance
0.8);
if (newCloud) {
newCloud.horizonY = self.horizonY;
}
}
}
}
function updateFieldElementPool() {
var activeObjects = self.fieldElementPool.getActiveObjects();
for (var j = 0; j < activeObjects.length; j++) {
var element = activeObjects[j];
// Calculate progress based on distance from horizon
var progress = Math.max(0, element.y - self.horizonY) / self.totalHeight;
// Calculate speed using same logic as stripes
var tempSpeed = self.scrollSpeed * 0.05 + self.scrollSpeed * (progress * 3);
// Move element toward horizon
element.y -= tempSpeed;
// Get the element's sprite
var sprite = element.getChildAt(0);
// Calculate scale based on distance from horizon
var fieldDistanceFactor = Math.max(0.05, (element.y - self.horizonY) / 300);
sprite.scaleX = fieldDistanceFactor * 1.2;
sprite.scaleY = fieldDistanceFactor * 1.2;
// Adjust alpha for fade effect
//sprite.alpha = Math.min(0.9, fieldDistanceFactor);
// Reset element if it reaches horizon
if (element.y <= self.horizonY + 10 || sprite.scaleX < 0.1) {
// Recycle and spawn new element
self.fieldElementPool.recycle(element);
// Calculate position for new element
var section = Math.floor(Math.random() * 8);
var sectionWidth = 2048 / 8;
var newX = section * sectionWidth + Math.random() * sectionWidth;
var newY = 2732 + Math.random() * 600;
var newScale = (newY - self.horizonY) / 300;
var newElement = self.fieldElementPool.spawn(newX, newY, newScale, 0.9);
if (newElement) {
newElement.horizonY = self.horizonY;
newElement.initialX = newX;
}
}
element.zIndex = element.y;
}
// After updating all elements, sort them by z-index
activeObjects.sort(function (a, b) {
return a.y - b.y; // Sort by y position (smaller y values first)
});
// Remove and re-add in the sorted order
for (var k = 0; k < activeObjects.length; k++) {
var element = activeObjects[k];
self.bottomContainer.removeChild(element);
self.bottomContainer.addChild(element);
}
}
return self;
});
// Modify the HealthPopup class
var HealthPopup = Container.expand(function () {
var self = Container.call(this);
var text = new Text2('+10', {
size: 150,
fill: 0x00FF00,
// Green
stroke: 0x000000,
strokeThickness: 3
});
text.anchor.set(0.5, 0.5);
self.addChild(text);
// Initially hide the popup
self.visible = false;
self.show = function (x, y) {
self.x = x;
self.y = y;
self.visible = true;
text.scaleX = 0.1;
text.scaleY = 0.1;
text.y = 0; // Reset position before animation
if (self.currentTween) {
self.currentTween.stop();
}
self.currentTween = tween(text, {
scaleX: 1.5,
scaleY: 1.5,
y: -50
}, {
duration: 300,
easing: tween.easeOutBack,
onFinish: function onFinish() {
tween(text, {
alpha: 0,
y: -100
}, {
duration: 400,
easing: tween.easeIn,
onFinish: function onFinish() {
self.visible = false;
text.alpha = 1;
text.y = 0;
}
});
}
});
};
return self;
});
var HealthPowerup = Container.expand(function () {
var self = Container.call(this);
var heart = self.attachAsset('heart', {
anchorX: 0.5,
anchorY: 0.5
});
self.activate = function (x, y) {
self.x = x;
self.y = y;
self.visible = true;
self.age = 0;
heart.scaleX = heart.scaleY = 1;
};
self.update = function () {
self.age++;
// Wave pattern movement
self.x += Math.sin(self.age * 0.05) * 2;
self.y -= 3; // Float upward
// Pulse scale
var pulse = 1 + Math.sin(self.age * 0.1) * 0.2;
heart.scaleX = heart.scaleY = pulse;
// Remove if off screen
if (self.y < -50) {
healthPowerupPool.recycle(self);
}
};
self.deactivate = function () {
self.visible = false;
};
return self;
});
var JetFlame = Container.expand(function () {
var self = Container.call(this);
var flame = self.attachAsset('flame', {
anchorX: 0.5,
anchorY: 0,
// Keep bottom anchor for natural flame shape
scaleX: 1,
scaleY: 0.5,
rotation: Math.PI // 180 degrees to point downward
});
self.startPulse = function () {
flame.scaleX = 0.5;
flame.scaleY = 0.3;
flame.alpha = 0.8;
self.pulse();
};
self.pulse = function () {
if (self.currentTween) {
self.currentTween.stop();
}
self.currentTween = tween(flame, {
scaleX: 0.75,
scaleY: 0.6,
alpha: 1
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
self.currentTween = tween(flame, {
scaleX: 0.5,
scaleY: 0.3,
alpha: 0.8
}, {
duration: 300,
easing: tween.easeOut,
onFinish: self.pulse
});
}
});
};
self.deactivate = function () {
if (self.currentTween) {
self.currentTween.stop();
self.currentTween = null;
}
flame.scaleY = 0.5;
};
return self;
});
var ObjectPool = Container.expand(function (ObjectClass, size) {
var self = Container.call(this);
var objects = [];
var activeCount = 0;
for (var i = 0; i < size; i++) {
var obj = new ObjectClass();
if (typeof obj.update !== 'function') {
obj.update = function () {}; // Provide a default update method if not defined
}
obj.visible = false;
obj._poolIndex = i;
objects.push(obj);
self.addChild(obj);
}
self.spawn = function (x, y, params) {
if (activeCount >= objects.length) {
return null;
}
var obj = objects[activeCount];
activeCount++; // Increment AFTER getting object
obj.visible = true;
obj.activate(x, y, params);
return obj;
};
self.recycle = function (obj) {
if (!obj || typeof obj._poolIndex !== 'number' || obj._poolIndex >= activeCount) {
return;
}
activeCount--; // Decrement count
// Swap with last active object if needed
if (obj._poolIndex < activeCount) {
var temp = objects[activeCount];
objects[activeCount] = obj;
objects[obj._poolIndex] = temp;
// Update indices
temp._poolIndex = obj._poolIndex;
obj._poolIndex = activeCount;
}
obj.deactivate();
obj.visible = false;
};
self.recycleAll = function () {
while (activeCount > 0) {
self.recycle(objects[0]);
}
};
self.update = function () {
for (var i = 0; i < activeCount; i++) {
objects[i].update();
}
};
self.getActiveObjects = function () {
return objects.slice(0, activeCount);
};
return self;
});
var ProgressMeter = Container.expand(function () {
var self = Container.call(this);
// Create meter background (now horizontal)
var meter = self.attachAsset('meter', {
anchorX: 0,
anchorY: 0.5,
scaleX: 1.2,
// Adjust width to preference
scaleY: 0.6,
// Make thinner
tint: 0x666666
});
// Progress icons
var dragonIcon = self.attachAsset('dragonHead', {
anchorX: 0.5,
anchorY: 0.7,
scaleX: 0.9,
scaleY: 0.9
});
var bossIcon = self.attachAsset('skull', {
anchorX: 0.5,
anchorY: 0.7
});
// Position at top center
// Position elements relative to meter center
var totalWidth = meter.width * 1.2 + 120; // Include icon spaces
self.x = 2048 / 2 - totalWidth / 2; // Center the whole assembly
self.y = 80;
// Position icons relative to meter edges
dragonIcon.x = 0; // Left icon at start
dragonIcon.y = meter.height / 2;
bossIcon.x = totalWidth; // Right icon at end
bossIcon.y = meter.height / 2;
// Center meter between icons
meter.x = 60; // Space for left icon
self.updateProgress = function (progress) {
dragonIcon.x = progress * (totalWidth - 60); // Move between start and end positions
};
return self;
});
var ScorchMark = Container.expand(function () {
var self = Container.call(this);
var mark = self.attachAsset('scorchMark', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
self.activate = function (x, y) {
self.x = x;
self.y = y;
self.visible = true;
self.speedY = flyingBackground.scrollSpeed;
// Initial scale based on distance from horizon
var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY);
mark.scaleX = 0.5 + progress;
mark.scaleY = (0.5 + progress) * 0.8;
};
self.update = function () {
var progress = Math.max(0, self.y - flyingBackground.horizonY) / (2732 - flyingBackground.horizonY);
// Drastically reduce base speed to match field elements
var tempSpeed = 0.2 + progress * 3;
self.y -= tempSpeed;
mark.scaleX = 0.5 + progress;
mark.scaleY = (0.5 + progress) * 0.8;
mark.alpha = Math.min(0.8, progress);
if (self.y <= flyingBackground.horizonY) {
scorchPool.recycle(self);
}
};
self.deactivate = function () {
self.visible = false;
};
return self;
});
var ScorePopup = Container.expand(function () {
var self = Container.call(this);
// Create text with white color instead of red
var scoreTxt = new Text2('', {
size: 120,
fill: 0xFFFFFF,
// White color
stroke: 0x000000,
strokeThickness: 3
});
scoreTxt.anchor.set(0.5, 0.5);
self.addChild(scoreTxt);
self.activate = function (x, y, amount, color) {
self.x = x;
self.y = y;
self.visible = true;
scoreTxt.setText('+' + amount);
// Special case for boss defeat score
if (amount === 5000) {
scoreTxt.size = 300; // Much larger text
color = 0xFFD700; // Gold color
} else {
scoreTxt.size = 120; // Normal size
}
// Apply tint if provided
if (color !== undefined) {
scoreTxt.tint = color;
} else {
scoreTxt.tint = 0xFF0000; // Default red tint
}
scoreTxt.scaleX = 0.1;
scoreTxt.scaleY = 0.1;
if (self.currentTween) {
self.currentTween.stop();
}
// Special animation for boss defeat
if (amount === 5000) {
self.currentTween = tween(scoreTxt, {
scaleX: 2.5,
scaleY: 2.5,
y: -150 // Move higher for boss defeat
}, {
duration: 600,
// Longer duration
easing: tween.easeOutBack,
onFinish: function onFinish() {
tween(scoreTxt, {
alpha: 0,
y: -250 // Float up higher
}, {
duration: 800,
// Longer fade
easing: tween.easeIn,
onFinish: function onFinish() {
scorePopupPool.recycle(self);
}
});
}
});
} else {
// Original animation for regular scores
self.currentTween = tween(scoreTxt, {
scaleX: 1.5,
scaleY: 1.5,
y: -50
}, {
duration: 300,
easing: tween.easeOutBack,
onFinish: function onFinish() {
tween(scoreTxt, {
alpha: 0,
y: -100
}, {
duration: 400,
easing: tween.easeIn,
onFinish: function onFinish() {
scorePopupPool.recycle(self);
}
});
}
});
}
};
self.deactivate = function () {
self.visible = false;
scoreTxt.alpha = 1;
scoreTxt.y = 0;
scoreTxt.tint = 0xFFFFFF; // Reset to white (no tint)
if (self.currentTween) {
self.currentTween.stop();
}
};
return self;
});
var TitleScreen = Container.expand(function () {
var self = Container.call(this);
var fadeOverlay = self.attachAsset('particle', {
anchorX: 0,
anchorY: 0,
scaleX: 2048,
scaleY: 2732,
tint: 0x000000,
alpha: 1
});
var titleImage = self.attachAsset('titleimage', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
var startButton = self.attachAsset('startbutton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0,
scaleY: 0,
alpha: 0
});
titleImage.x = 2048 / 2;
titleImage.y = 2732 / 3;
startButton.x = 2048 / 2;
startButton.y = titleImage.y + 1100;
self.start = function () {
// Fade in title
tween(fadeOverlay, {
alpha: 0
}, {
duration: 1500,
easing: tween.easeOut
});
tween(titleImage, {
alpha: 1,
scaleX: 1,
// Add scale pulse start
scaleY: 1 // Add scale pulse start
}, {
duration: 1500,
easing: tween.easeOut,
onFinish: function onFinish() {
// Add pulsing animation
function pulseTitle() {
tween(titleImage, {
tint: 0xFFFF00,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 1100,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(titleImage, {
tint: 0xFFA500,
scaleX: 1,
scaleY: 1
}, {
duration: 1100,
easing: tween.easeInOut,
onFinish: pulseTitle
});
}
});
}
pulseTitle();
// Grow button
tween(startButton, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 800,
easing: tween.easeOutBack,
onFinish: function onFinish() {
// Add pulsing animation
function pulse() {
tween(startButton, {
tint: 0xFFFF00
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(startButton, {
tint: 0xFFA500
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: pulse
});
}
});
}
pulse();
}
});
}
});
};
startButton.down = function () {
self.startGame();
return true;
};
self.startGame = function () {
// Prevent multiple clicks
LK.getSound('dragonScream').play();
startButton.down = null;
// Fade out title elements
tween(titleImage, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
// Enable gameplay
gameActive = true;
// Show UI
dragonHealthBar.visible = true;
scoreTxt.visible = true;
scoreMeterBg.visible = true;
progressMeter.visible = true;
// Enable dragon and shadow
dragon.visible = true;
dragonShadow.visible = true;
// Remove title screen
game.removeChild(self);
}
});
tween(startButton, {
alpha: 0
}, {
duration: 500
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
function processCollisions() {
var fireballs = fireballPool.getActiveObjects();
var enemies = enemyPool.getActiveObjects();
var enemiesTwo = enemyTwoPool.getActiveObjects();
// Process enemy arrows first
var arrows = enemyArrowPool.getActiveObjects();
// Add after arrow collision checks in processCollisions()
var enemiesThree = enemyThreePool.getActiveObjects();
for (var i = enemiesThree.length - 1; i >= 0; i--) {
var enemy = enemiesThree[i];
var dx = enemy.x - dragon.x;
var dy = enemy.y - dragon.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 210 * enemy.scaleX) {
// Adjust collision radius based on enemy scale
// Create explosion effect
for (var j = 0; j < 20; j++) {
defeatParticlePool.spawn(enemy.x, enemy.y, 0xff0000);
}
enemyThreePool.recycle(enemy);
dragon.flashDamage(6);
}
}
// Add inside processCollisions() function, after the existing collision checks:
// Check boss fist collisions with dragon
if (boss && boss.activeFist) {
var fist = boss.activeFist;
var dx = fist.x - dragon.x;
var dy = fist.y - dragon.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Use dragon's current scale for collision radius
var collisionRadius = 150 * dragon.currentScale;
if (distance < collisionRadius) {
dragon.flashDamage(12);
// Should the fist retract after hitting? Let me know if you want this behavior
}
}
// Replace the existing fireball-boss collision check with this:
if (boss && boss.state !== 'dying') {
var fireballs = fireballPool.getActiveObjects();
for (var i = fireballs.length - 1; i >= 0; i--) {
var fireball = fireballs[i];
var dx = fireball.x - boss.x;
var dy = fireball.y - boss.y;
// Use elliptical hitbox: wider horizontally, same vertically
var horizontalRange = 300; // Wider horizontal hitbox (was effectively 100)
var verticalRange = 100; // Keep the same vertical hitbox
// Elliptical distance formula
var ellipticalDist = Math.sqrt(dx * dx / (horizontalRange * horizontalRange) + dy * dy / (verticalRange * verticalRange));
if (ellipticalDist < 1) {
boss.damage(1);
fireballPool.recycle(fireball);
}
}
}
// Replace the existing arrow collision check with:
for (var i = arrows.length - 1; i >= 0; i--) {
var arrow = arrows[i];
var dx = arrow.x - dragon.x;
var dy = arrow.y - dragon.y;
// Only check collisions if arrow isn't below dragon
if (arrow.y <= dragon.y + 50) {
// Allow slight vertical tolerance
// Wider horizontal check, original vertical bounds
var horizontalDistance = Math.abs(dx);
var verticalDistance = Math.abs(dy);
// Check if within wing range (wider horizontally)
if (horizontalDistance < 300 && verticalDistance < 75) {
enemyArrowPool.recycle(arrow);
// Flash dragon red (keep existing code)
dragon.flashDamage(7);
}
}
}
var fireballs = fireballPool.getActiveObjects();
var arrows = enemyArrowPool.getActiveObjects();
for (var i = fireballs.length - 1; i >= 0; i--) {
var fireball = fireballs[i];
for (var j = arrows.length - 1; j >= 0; j--) {
var arrow = arrows[j];
var dx = fireball.x - arrow.x;
var dy = fireball.y - arrow.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 60) {
// Small collision radius since these are projectiles
// Create small explosion
for (var k = 0; k < 8; k++) {
// Fewer particles than full defeat
var particle = defeatParticlePool.spawn(arrow.x + (Math.random() * 20 - 10),
// Smaller spread
arrow.y + (Math.random() * 20 - 10), 0xff0000 // Red tint
);
if (particle) {
// Smaller scale for particles
var sprite = particle.getChildAt(0);
sprite.scaleX = 0.2;
sprite.scaleY = 0.2;
}
}
// Remove both projectiles
fireballPool.recycle(fireball);
enemyArrowPool.recycle(arrow);
// Play explosion sound with lower volume
LK.getSound('enemyDefeat').play({
volume: 0.5
});
break; // Break inner loop since fireball is gone
}
}
}
// Process fireballs against both enemy types
for (var i = fireballs.length - 1; i >= 0; i--) {
var fireball = fireballs[i];
// Check against type 1 enemies
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Check if enemy is fully on screen (accounting for scaled size)
var enemyHeight = 403 * enemy.scaleY; // Original height * scale
var enemyWidth = 200 * enemy.scaleX; // Original width * scale
if (enemy.y > enemyHeight / 2 && enemy.y < 2732 - enemyHeight / 2 && enemy.x > enemyWidth / 2 && enemy.x < 2048 - enemyWidth / 2) {
var dx = fireball.x - enemy.x;
var dy = fireball.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 120 * enemy.scaleX) {
enemy.hit();
fireballPool.recycle(fireball);
break;
}
}
}
// Check against type 2 enemies
for (var j = enemiesTwo.length - 1; j >= 0; j--) {
var enemy = enemiesTwo[j];
var enemyHeight = 412.5 * enemy.scaleY;
var enemyWidth = 550 * enemy.scaleX;
if (enemy.y > enemyHeight / 2 && enemy.y < 2732 - enemyHeight / 2 && enemy.x > enemyWidth / 2 && enemy.x < 2048 - enemyWidth / 2) {
var dx = fireball.x - enemy.x;
var dy = fireball.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 170 * enemy.scaleX) {
enemy.hit();
fireballPool.recycle(fireball);
break;
}
}
}
// Check against type 3 enemies
for (var j = enemiesThree.length - 1; j >= 0; j--) {
var enemy = enemiesThree[j];
var enemyHeight = 253 * enemy.scaleY;
var enemyWidth = 300 * enemy.scaleX;
if (enemy.y > enemyHeight / 2 && enemy.y < 2732 - enemyHeight / 2 && enemy.x > enemyWidth / 2 && enemy.x < 2048 - enemyWidth / 2) {
var dx = fireball.x - enemy.x;
var dy = fireball.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 120 * enemy.scaleX) {
enemy.hit();
fireballPool.recycle(fireball);
break;
}
}
}
}
var powerups = healthPowerupPool.getActiveObjects();
for (var i = powerups.length - 1; i >= 0; i--) {
var powerup = powerups[i];
var dx = powerup.x - dragon.x;
var dy = powerup.y - dragon.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 240) {
dragon.health = Math.min(100, dragon.health + 10);
dragonHealthBar.updateHealth(dragon.health / 100);
LK.getSound('health').play();
healthPowerupPool.recycle(powerup);
healthPopup.show(powerup.x, powerup.y);
}
}
}
function trySpawnHealthPowerup(x, y) {
if (Math.random() < 0.10) {
// 10% chance
// 100% chance
// 10% chance
// 7% chance
// 10% chance
// 15% chance
var powerup = healthPowerupPool.spawn(x, y);
if (powerup) {
scorePopupPool.spawn(x, y - 50, "HEALTH", 0x00ff00);
}
}
}
// Set dark blue background for sky effect
game.setBackgroundColor(0x2c3e50);
// Game state variables
var score = 0;
var fireballPool = new ObjectPool(Fireball, 100);
var particlePool = new ObjectPool(FireParticle, 400);
var enemyPool = new ObjectPool(Enemy, 25);
var defeatParticlePool = new ObjectPool(DefeatParticle, 200);
var dustParticlePool = new ObjectPool(DustParticle, 100);
var enemyShadowPool = new ObjectPool(EnemyShadow, 50);
var enemyArrowPool = new ObjectPool(EnemyArrow, 50);
var enemyTwoPool = new ObjectPool(EnemyTwo, 25);
var enemyThreePool = new ObjectPool(EnemyThree, 25);
var flamePool = new ObjectPool(FlameEffect, 20);
var scorchPool = new ObjectPool(ScorchMark, 30);
var chainSegmentPool = new ObjectPool(ChainSegment, 30);
var scorePopupPool = new ObjectPool(ScorePopup, 20);
var healthPowerupPool = new ObjectPool(HealthPowerup, 10);
var bossFistPool = new ObjectPool(BossFist, 2); // Left and right fists
var boss = null; // Will be created when needed
var bossHealthBar = new BossHealthBar();
bossHealthBar.visible = false;
bossHealthBar.updateHealth(1.0);
var LEVEL_DURATION = 180 * 60; // 3 minutes at 60fps
var levelTimer = 0;
var progressMeter = new ProgressMeter();
var isFiring = false;
var lastFireTime = 0;
var fireRate = 200; // ms between fireballs
var enemy1StartTime = 30 * 60; // 30 seconds
var enemy2StartTime = 15 * 60; // 15 seconds
var enemy3StartTime = 5 * 60; // 5 seconds
var enemy1SpawnRate = 300; // ~3 seconds
var enemy2SpawnRate = 240; // ~4 seconds
var enemy3SpawnRate = 180; // ~5 seconds
var ENEMY1_MIN_RATE = 150; // Half of 300
var ENEMY2_MIN_RATE = 120; // Half of 240
var ENEMY3_MIN_RATE = 100; // Half of 180
var difficultyScaling = 0;
var flyingBackground = new FlyingBackground();
game.addChild(flyingBackground);
// Create dragon head (player character)
var dragon = game.addChild(new DragonHead());
dragon.x = 2048 / 2;
dragon.y = 2732 * 0.2; // Place dragon at top fifth of screen
var dragonShadow = new DragonShadow();
game.addChild(dragonShadow);
game.addChild(enemyShadowPool);
game.addChild(fireballPool);
game.addChild(particlePool);
game.addChild(enemyPool);
game.addChild(enemyTwoPool);
game.addChild(enemyThreePool);
game.addChild(chainSegmentPool);
game.addChild(bossFistPool);
game.addChild(scorePopupPool);
game.addChild(defeatParticlePool);
game.addChild(dustParticlePool);
game.addChild(enemyArrowPool);
game.addChild(scorchPool);
game.addChild(flamePool);
game.addChild(healthPowerupPool);
var healthPopup = new HealthPopup();
game.addChild(healthPopup);
dragon.health = 100;
var dragonHealthBar = new DragonHealthBar();
game.addChild(dragonHealthBar);
game.addChild(progressMeter);
var bossText = new BossText();
game.addChild(bossText);
game.cleanupPools = function () {
fireballPool.recycleAll();
particlePool.recycleAll();
enemyPool.recycleAll();
enemyTwoPool.recycleAll();
defeatParticlePool.recycleAll();
dustParticlePool.recycleAll();
enemyShadowPool.recycleAll();
enemyArrowPool.recycleAll();
scorchPool.recycleAll();
flamePool.recycleAll();
enemyThreePool.recycleAll();
};
var scoreMeterBg = game.attachAsset('particle', {
anchorX: 0,
anchorY: 0.5,
tint: 0x333333,
alpha: 0.5,
scaleY: 1
});
// Score display
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 3
});
// Position meter and score
scoreMeterBg.x = 2048 / 2; // Center X
scoreMeterBg.anchorX = 0.5; // Center anchor for background
scoreMeterBg.y = 300;
scoreTxt.anchor.set(0.5, 0.5); // Center anchor for text
scoreTxt.x = 2048 / 2; // Center X
scoreTxt.y = 300;
game.addChild(scoreMeterBg);
game.addChild(scoreTxt);
function updateScoreMeter(score) {
var minWidth = 200;
var scoreStr = score.toString();
var padding = 40; // Total padding (20 each side)
// Adjust width per digit based on digit count
var widthPerDigit;
if (scoreStr.length === 1) {
widthPerDigit = 70;
} else if (scoreStr.length === 2) {
widthPerDigit = 60;
} else {
widthPerDigit = 70;
}
var targetWidth = Math.max(minWidth, scoreStr.length * widthPerDigit + padding);
scoreMeterBg.scaleX = targetWidth / scoreMeterBg.width;
// Text stays centered automatically now
}
// Create a function to add fire particles
function createFireParticle(x, y) {
particlePool.spawn(x, y);
}
// Function to spawn a fireball
// Update the spawnFireball function
function spawnFireball() {
if (!gameActive) {
return;
}
var now = Date.now();
if (now - lastFireTime < fireRate) {
return;
}
lastFireTime = now;
// Initialize horizontal velocity components
var angleOffset = 0;
// Get direction from dragon head facing - match thresholds with DragonHead class
if (facekit.leftEye && facekit.rightEye && facekit.noseTip) {
var faceCenter = (facekit.leftEye.x + facekit.rightEye.x) / 2;
var noseDiff = facekit.noseTip.x - faceCenter;
var eyeYDiff = facekit.rightEye.y - facekit.leftEye.y;
// Keep the original thresholds but require more pronounced movement
// to trigger the sideways shots (higher threshold values)
if (noseDiff < -120 && Math.abs(eyeYDiff) < 30 || eyeYDiff > 50) {
// Facing left - fireballs go left (maintain strong angle)
angleOffset = -8; // Original value for strong effect
} else if (noseDiff > 120 && Math.abs(eyeYDiff) < 30 || eyeYDiff < -50) {
// Facing right - fireballs go right (maintain strong angle)
angleOffset = 8; // Original value for strong effect
}
}
// Keep the original tilt impact for consistent feel
if (dragon.rotation) {
angleOffset += dragon.rotation * 5; // Original value
}
// Use the new pool's spawn method
var fireball = fireballPool.spawn(dragon.x, dragon.y + 50);
if (fireball) {
fireball.vx = angleOffset;
// Add this line to alternate weave direction
fireball.weaveDirection = lastFireTime % (fireRate * 2) === 0 ? 1 : -1;
}
// Play sound
LK.getSound('firebreathSound').play();
}
dragonHealthBar.visible = false;
scoreTxt.visible = false;
scoreMeterBg.visible = false;
progressMeter.visible = false;
dragon.visible = false;
dragonShadow.visible = false;
gameActive = false;
var titleScreen = new TitleScreen();
game.addChild(titleScreen);
titleScreen.start();
// Game update logic
game.update = function () {
if (!gameActive) {
return;
}
// Check if mouth is open for fire breathing
if (facekit && facekit.mouthOpen) {
isFiring = true;
spawnFireball();
} else {
isFiring = false;
}
if (boss) {
boss.update();
}
levelTimer++;
// Only update progress if boss hasn't spawned yet
if (!boss) {
progressMeter.updateProgress(levelTimer / LEVEL_DURATION);
}
// Enemy spawning logic
// Enemy spawning logic
if (!boss || boss.state !== 'dying') {
// Add a spawn rate multiplier that's 2x when boss is present
var spawnRateMultiplier = boss ? 2 : 1;
if (levelTimer >= enemy3StartTime) {
// Scale from initial 180 to minimum 90 over level duration
var scaled3Rate = Math.max(ENEMY3_MIN_RATE, enemy3SpawnRate - (enemy3SpawnRate - ENEMY3_MIN_RATE) * (levelTimer / LEVEL_DURATION));
// Apply the multiplier to the spawn rate
if (LK.ticks % Math.floor(scaled3Rate * spawnRateMultiplier) === 0) {
var enemy = enemyThreePool.spawn(Math.random() * (2048 - 200) + 100, 2732 + 100);
}
}
if (levelTimer >= enemy2StartTime) {
var scaled2Rate = Math.max(ENEMY2_MIN_RATE, enemy2SpawnRate - (enemy2SpawnRate - ENEMY2_MIN_RATE) * (levelTimer / LEVEL_DURATION));
// Apply the multiplier to the spawn rate
if (LK.ticks % Math.floor(scaled2Rate * spawnRateMultiplier) === 0) {
var spawnX = Math.random() < 0.5 ? -50 : 2098;
var enemy = enemyTwoPool.spawn(spawnX, 2732 - 500);
}
}
if (levelTimer >= enemy1StartTime) {
var scaled1Rate = Math.max(ENEMY1_MIN_RATE, enemy1SpawnRate - (enemy1SpawnRate - ENEMY1_MIN_RATE) * (levelTimer / LEVEL_DURATION));
// Apply the multiplier to the spawn rate
if (LK.ticks % Math.floor(scaled1Rate * spawnRateMultiplier) === 0) {
var enemy = enemyPool.spawn(Math.random() * (2048 - 200) + 100, 2732 + 100);
}
}
}
// Boss spawn check
if (levelTimer >= LEVEL_DURATION && !boss) {
bossText.flash();
boss = new Boss();
game.addChild(boss);
game.addChild(bossHealthBar);
bossHealthBar.visible = true;
bossHealthBar.show();
}
dragonShadow.update(dragon.x, dragon.y, dragon.currentScale);
// Update all pools
fireballPool.update();
enemyPool.update();
particlePool.update();
defeatParticlePool.update();
dustParticlePool.update();
bossFistPool.update();
flamePool.update();
scorchPool.update();
enemyThreePool.update();
enemyTwoPool.update();
scorePopupPool.update();
healthPowerupPool.update();
// Process collisions
processCollisions();
};
// Start background music
LK.playMusic('gameMusic', {
fade: {
start: 0,
end: 0.7,
duration: 1000
}
});
A clear blue sky background with no clouds.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
a small bush. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
a wooden arrow with red feathers and a metal arrow head. Completely vertical orientation. Cartoon. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A small vertical flame. Cartoon. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
a black scorch mark on the ground left by a meteor impact. cartoon. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A bright spark. Cartoon. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
a red heart. cartoon. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
An SVG of the word **BOSS** in sharp red font.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
An SVG of the word “Start” written in fire. Cartoon.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows