User prompt
sürekli şekilde çoklu gelmesin ama oyun ilerledikçe oyun zorlaşsın notalar daha hızlı gelsin ve dağınık şekilde çoklu gelmeye başlasın bu zorluk oldukça yavaş şekilde artsın
User prompt
notalar gittikçe hızlanan ve oyun ilerledikçe çoklu şekilde gelebilme şeklinde düzenle
User prompt
misses kısmını ve puan kısmını ekranın üzerinde daha güzel olacak şekilde düzenle
User prompt
can olayını kaldır komple
User prompt
The character’s visible health on the screen should decrease by 1 each time a monster approaches and explodes. (Each explosion deals 1 damage.)
User prompt
karakterin canı hala azalmıyor
User prompt
canavar karakterin yanında patladığında karakterin üstte gözüken health:5 1 azalsın ve health 4 olsun bu şekilde 5 canavar karaktere ulaşırsa oyun bitsin
User prompt
Please fix the bug: 'ReferenceError: heroHealth is not defined' in or related to this line: 'heroHealthTxt.setText('Health: ' + heroHealth);' Line Number: 1020
User prompt
canavarlar karakter ulaştığında patlasın ve karakterin 1 healthını götürsün
User prompt
hala karakterin canı azalmıyor canavarlar karakterin canını götürmeli bunu düzelt health 0 olduğunda karakter ölecek ve oyun bitecek
User prompt
canavarlar karaktere vurduğunda health 1 azalsın ve her canavar healthı 3 saniye 1 götürsün
User prompt
karakterin canı oyun içerisinde gözüksün ve canavarlar yanına geldiğinde ona saldırıp canını götürsün şuanda hala karakterin canı gitmiyor
User prompt
karakterin azalabilen bir oyun içi canı olsun canavarlar onun yanına geldiğinde ona hasar versin ve canı gitsin canı kalmadığında oyun bitsin karakter ölsün
User prompt
canavarlar karakterin canını götürmeli yani üstündeki kalplerden eksilmeli ama canavarlar karakterin üstünde gözüken kalbini eksiltmiyor
User prompt
canavarlar şuanda karaktere hasar verip canını götürmüyorlar bunu düzlet
User prompt
Oyundaki karakterin 5 canı olsun Canavarlar karakterin yanına geldiğinde dursun ve ona her 3 saniyede 1 can hasar versin Karakterin 5 canının hepsi gittiğinde karakter ölsün ve oyun bitsinm
User prompt
oyundaki can ve heart assetlerini kaldır
User prompt
canavarlar her 3 saniyede 1 karakterin 1 canın götürsün
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'x')' in or related to this line: 'healthBar.x = hero.x - barWidth / 2 + heartSize / 2;' Line Number: 932
User prompt
karaktere oyun ekranında gözüken 5 adet can ekle
User prompt
Oyundaki canavarlar karaktere yaklaştığında onun sahip olduğu 5 canı yavaş yavaş azaltsın karakterin canı kalmadığında oyun bitsin
User prompt
Majestic Mountains haritasında canavarlar öldüğünde monsterdead3 sounds asseti çalışsın
User prompt
düşen notaların boyutunu biracık arttır
User prompt
hala Majestic Mountains haritasında gözüken canavarlar monster assetinden geliyor fakat monster3 asseti kullanılsın bu haritada
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // --- Win Condition (Optional: e.g. score 30) --- /* if (score >= 30) { LK.showYouWin(); } */ // --- End of File --- // Monster: Enemy advancing from the top var Monster = Container.expand(function () { var self = Container.call(this); // Attach monster asset (box/ellipse) var monsterAsset = self.attachAsset('monster', { anchorX: 0.5, anchorY: 0.5, width: self.monsterSize, height: self.monsterSize }); // Health points self.hp = self.maxHp; // Called every tick self.update = function () { // Monsters only move forward if not destroyed if (!self.destroyed) { // Move toward hero using direction vector if set if (typeof self.dirX === "number" && typeof self.dirY === "number") { self.x += self.dirX * self.speed; self.y += self.dirY * self.speed; } else { // fallback: move down self.y += self.speed; } } }; // --- Flying effect: gentle up/down hover using tween --- function startFlyingEffect() { // Reset to default before starting self.yOffset = 0; // Animate up tween(self, { yOffset: -18 }, { duration: 420 + Math.random() * 120, easing: tween.easeInOut, onUpdate: function onUpdate() { // Apply yOffset to monster position self.y += self.yOffset - (self._lastYOffset || 0); self._lastYOffset = self.yOffset; }, onFinish: function onFinish() { // Animate down tween(self, { yOffset: 18 }, { duration: 420 + Math.random() * 120, easing: tween.easeInOut, onUpdate: function onUpdate() { self.y += self.yOffset - (self._lastYOffset || 0); self._lastYOffset = self.yOffset; }, onFinish: function onFinish() { startFlyingEffect(); } }); } }); } startFlyingEffect(); // Take damage self.hit = function () { self.hp -= 1; if (self.hp <= 0) { self.destroyed = true; // Play monster dead sound LK.getSound('monsterdead').play(); // Animate scale up and fade out for death effect tween(self, { alpha: 0, scaleX: 1.7, scaleY: 1.7 }, { duration: 220, onFinish: function onFinish() { self.destroy(); } }); } else { // Flash red tween(monsterAsset, { tint: 0xff4444 }, { duration: 80, onFinish: function onFinish() { tween(monsterAsset, { tint: self.baseColor }, { duration: 120 }); } }); } }; return self; }); // Monster2: Funky Groove special monster var Monster2 = Container.expand(function () { var self = Container.call(this); // Attach monster2 asset var monsterAsset = self.attachAsset('monster2', { anchorX: 0.5, anchorY: 0.5, width: self.monsterSize, height: self.monsterSize }); // Health points self.hp = self.maxHp; // Called every tick self.update = function () { if (!self.destroyed) { if (typeof self.dirX === "number" && typeof self.dirY === "number") { self.x += self.dirX * self.speed; self.y += self.dirY * self.speed; } else { self.y += self.speed; } } }; // --- Flying effect: gentle up/down hover using tween --- function startFlyingEffect() { self.yOffset = 0; tween(self, { yOffset: -18 }, { duration: 420 + Math.random() * 120, easing: tween.easeInOut, onUpdate: function onUpdate() { self.y += self.yOffset - (self._lastYOffset || 0); self._lastYOffset = self.yOffset; }, onFinish: function onFinish() { tween(self, { yOffset: 18 }, { duration: 420 + Math.random() * 120, easing: tween.easeInOut, onUpdate: function onUpdate() { self.y += self.yOffset - (self._lastYOffset || 0); self._lastYOffset = self.yOffset; }, onFinish: function onFinish() { startFlyingEffect(); } }); } }); } startFlyingEffect(); // Take damage self.hit = function () { self.hp -= 1; if (self.hp <= 0) { self.destroyed = true; // Play monsterdead2 sound for Funky Groove monsters LK.getSound('monsterdead2').play(); tween(self, { alpha: 0, scaleX: 1.7, scaleY: 1.7 }, { duration: 220, onFinish: function onFinish() { self.destroy(); } }); } else { // Flash red tween(monsterAsset, { tint: 0xff4444 }, { duration: 80, onFinish: function onFinish() { tween(monsterAsset, { tint: self.baseColor }, { duration: 120 }); } }); } }; return self; }); // Monster3: Majestic Mountains special monster var Monster3 = Container.expand(function () { var self = Container.call(this); // Attach monster3 asset var monsterAsset = self.attachAsset('monster3', { anchorX: 0.5, anchorY: 0.5, width: self.monsterSize, height: self.monsterSize }); // Health points self.hp = self.maxHp; // Called every tick self.update = function () { if (!self.destroyed) { if (typeof self.dirX === "number" && typeof self.dirY === "number") { self.x += self.dirX * self.speed; self.y += self.dirY * self.speed; } else { self.y += self.speed; } } }; // --- Flying effect: gentle up/down hover using tween --- function startFlyingEffect() { self.yOffset = 0; tween(self, { yOffset: -18 }, { duration: 420 + Math.random() * 120, easing: tween.easeInOut, onUpdate: function onUpdate() { self.y += self.yOffset - (self._lastYOffset || 0); self._lastYOffset = self.yOffset; }, onFinish: function onFinish() { tween(self, { yOffset: 18 }, { duration: 420 + Math.random() * 120, easing: tween.easeInOut, onUpdate: function onUpdate() { self.y += self.yOffset - (self._lastYOffset || 0); self._lastYOffset = self.yOffset; }, onFinish: function onFinish() { startFlyingEffect(); } }); } }); } startFlyingEffect(); // Take damage self.hit = function () { self.hp -= 1; if (self.hp <= 0) { self.destroyed = true; // Play monster dead sound (same as Monster) LK.getSound('monsterdead').play(); tween(self, { alpha: 0, scaleX: 1.7, scaleY: 1.7 }, { duration: 220, onFinish: function onFinish() { self.destroy(); } }); } else { // Flash red tween(monsterAsset, { tint: 0xff4444 }, { duration: 80, onFinish: function onFinish() { tween(monsterAsset, { tint: self.baseColor }, { duration: 120 }); } }); } }; return self; }); // Note: Falling note that must be hit in time var Note = Container.expand(function () { var self = Container.call(this); // Pick a random note asset for this note var noteAssetId = 'note' + (1 + Math.floor(Math.random() * 4)); self.noteAssetId = noteAssetId; var noteAsset = self.attachAsset(noteAssetId, { anchorX: 0.5, anchorY: 0.5, width: self.noteSize, height: self.noteSize }); // Index of the key this note targets self.keyIndex = typeof self.keyIndex !== "undefined" ? self.keyIndex : self.index; // Used for hit/miss detection self.active = true; // Called every tick self.update = function () { // Notes always fall straight down toward their assigned key self.y += self.speed; // Find the key this note is assigned to var key = typeof self.keyIndex !== "undefined" && self.keyIndex >= 0 && self.keyIndex < NUM_KEYS ? pianoKeys[self.keyIndex] : null; if (key) { var missY = key.y + KEY_HEIGHT + NOTE_SIZE / 2; // If note crosses the bottom boundary and is still active, mark as miss and destroy if (self.y > missY && self.active) { self.active = false; // Register miss missCount += 1; missTxt.setText('Misses: ' + missCount); LK.effects.flashObject(hero, 0xff4444, 200); checkGameOver(); self.destroy(); } } else { // Defensive: If key is missing, just destroy the note self.destroy(); } }; return self; }); // PianoKey: Represents a single piano key at the bottom var PianoKey = Container.expand(function () { var self = Container.call(this); // Defensive: Set defaults if not set if (typeof self.assetId === "undefined") { self.assetId = 'key_piano'; } if (typeof self.keyWidth === "undefined") { self.keyWidth = KEY_WIDTH; } if (typeof self.keyHeight === "undefined") { self.keyHeight = KEY_HEIGHT; } if (typeof self.baseColor === "undefined") { self.baseColor = 0xffffff; } if (typeof self.index === "undefined") { self.index = 0; } // Attach key asset (always key_piano) var keyAsset = self.attachAsset(self.assetId, { anchorX: 0.5, anchorY: 0, width: self.keyWidth, height: self.keyHeight }); // Store index for reference self.keyIndex = self.index; // Set initial dim state keyAsset.alpha = 0.45; // Visual feedback for press self.flash = function () { // Light up key tween(keyAsset, { alpha: 1 }, { duration: 60, onFinish: function onFinish() { // Return to dim after a short time tween(keyAsset, { alpha: 0.45 }, { duration: 120 }); } }); }; return self; }); // Projectile: Fired by hero toward a monster var Projectile = Container.expand(function () { var self = Container.call(this); // Defensive: Set defaults if not set if (typeof self.size === "undefined") { self.size = PROJECTILE_SIZE; } if (typeof self.speed === "undefined") { self.speed = PROJECTILE_SPEED; } // Pick a random bullet asset for this projectile var bulletAssetIds = ['bullet', 'bullet2', 'bullet3', 'bullet4']; var chosenBulletAsset = bulletAssetIds[Math.floor(Math.random() * bulletAssetIds.length)]; // Attach bullet asset var projAsset = self.attachAsset(chosenBulletAsset, { anchorX: 0.5, anchorY: 0.5, width: self.size, height: self.size }); // Target monster (set externally) self.target = null; // Called every tick self.update = function () { // If no target or target destroyed, fade out and destroy if (!self.target || self.target.destroyed || !self.target.parent) { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 120, onFinish: function onFinish() { self.destroy(); } }); return; } // Move toward target var dx = self.target.x - self.x; var dy = self.target.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < self.speed) { // Arrived at target: hit! if (typeof self.target.hit === "function") { self.target.hit(); } // Impact effect tween(self, { alpha: 0, scaleX: 1.7, scaleY: 1.7 }, { duration: 120, onFinish: function onFinish() { self.destroy(); } }); return; } // Move toward target self.x += dx / dist * self.speed; self.y += dy / dist * self.speed; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181830 }); /**** * Game Code ****/ // Funky Groove top background (separate from splash and other screens) // --- SPLASH SCREEN LOGIC --- // Create splash background asset (full screen, custom color or image) var splashBg = LK.getAsset('entry_bg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 2732 }); game.addChild(splashBg); // Title text var splashTitle = new Text2("Melody Mayhem", { size: 210, fill: 0xFFE600, font: "'Impact','GillSans-Bold','Arial Black',Tahoma,sans-serif", stroke: "#222", strokeThickness: 16, dropShadow: true, dropShadowColor: "#000", dropShadowDistance: 8, dropShadowAngle: Math.PI / 2, dropShadowBlur: 8 }); // Move splash title and buttons to the bottom of the screen splashTitle.anchor.set(0.5, 1); splashTitle.x = 2048 / 2; splashTitle.y = 2732 - 420; game.addChild(splashTitle); // "Choose The Music" button var chooseMusicBtn = new Container(); var chooseMusicBg = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, width: 700, height: 180, x: 0, y: 0 }); chooseMusicBtn.addChild(chooseMusicBg); var chooseMusicTxt = new Text2("Choose The Music", { size: 82, fill: ["#FF5F6D", "#FFC371", "#43E97B", "#38F9D7"], // gradient-like array for color pop font: "'Comic Sans MS','Luckiest Guy','Impact','GillSans-Bold','Arial Black',Tahoma,sans-serif", stroke: "#fff", strokeThickness: 7, dropShadow: true, dropShadowColor: "#222", dropShadowDistance: 3, dropShadowAngle: Math.PI / 2, dropShadowBlur: 7 }); chooseMusicTxt.anchor.set(0.5, 0.5); chooseMusicTxt.x = 0; chooseMusicTxt.y = 0; chooseMusicBtn.addChild(chooseMusicTxt); // "How to Play" button var howToPlayBtn = new Container(); var howToPlayBg = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, width: 700, height: 180, x: 0, y: 0 }); howToPlayBtn.addChild(howToPlayBg); var howToPlayTxt = new Text2("How to Play", { size: 82, fill: ["#43E97B", "#38F9D7", "#FF5F6D", "#FFC371"], // gradient-like array for color pop font: "'Comic Sans MS','Luckiest Guy','Impact','GillSans-Bold','Arial Black',Tahoma,sans-serif", stroke: "#fff", strokeThickness: 7, dropShadow: true, dropShadowColor: "#222", dropShadowDistance: 3, dropShadowAngle: Math.PI / 2, dropShadowBlur: 7 }); howToPlayTxt.anchor.set(0.5, 0.5); howToPlayTxt.x = 0; howToPlayTxt.y = 0; howToPlayBtn.addChild(howToPlayTxt); // Position both buttons side by side, centered horizontally at the bottom var buttonSpacing = 80; // space between buttons var buttonY = 2732 - 180; // bottom margin, same for both var buttonTotalWidth = 700 * 2 + buttonSpacing; var leftBtnX = 2048 / 2 - (700 / 2 + buttonSpacing / 2); var rightBtnX = 2048 / 2 + (700 / 2 + buttonSpacing / 2); chooseMusicBtn.x = leftBtnX; chooseMusicBtn.y = buttonY; howToPlayBtn.x = rightBtnX; howToPlayBtn.y = buttonY; game.addChild(chooseMusicBtn); game.addChild(howToPlayBtn); // Music selection menu (hidden by default) var musicMenu = new Container(); musicMenu.visible = false; musicMenu.x = 2048 / 2; musicMenu.y = 1200; var musicNames = [{ id: "gamemusic", label: "Cyber : 2099", fill: ["#00F0FF", "#FF00C8", "#00FF85"] // Vibrant neon }, { id: "gamemusic2", label: "Welcome to Hell", fill: ["#FF2D00", "#FFB800", "#FF00A8"] // Hot, fiery }, { id: "gamemusic3", label: "Majestic Mountains", fill: ["#00FFB2", "#00B2FF", "#FFFA00"] // Lively, fresh }]; var musicBtns = []; for (var i = 0; i < musicNames.length; i++) { var btn = new Container(); var btnBg = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, width: 820, // wider for long names height: 150, // slightly taller for better fit x: 0, y: 0 }); btn.addChild(btnBg); var btnTxt = new Text2(musicNames[i].label, { size: 66, // smaller font to fit long names fill: musicNames[i].fill, font: "'Comic Sans MS','Luckiest Guy','Impact','GillSans-Bold','Arial Black',Tahoma,sans-serif", stroke: "#fff", strokeThickness: 8, dropShadow: true, dropShadowColor: "#000", dropShadowDistance: 4, dropShadowAngle: Math.PI / 2, dropShadowBlur: 8 }); btnTxt.anchor.set(0.5, 0.5); btnTxt.x = 0; btnTxt.y = 0; btn.addChild(btnTxt); btn.x = 0; btn.y = i * 180; btn.musicId = musicNames[i].id; musicBtns.push(btn); musicMenu.addChild(btn); } // Add a back button to the music menu var backBtn = new Container(); var backBtnBg = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, width: 500, height: 120, x: 0, y: 0 }); backBtn.addChild(backBtnBg); var backBtnTxt = new Text2("Back", { size: 70, fill: ["#FF5F6D", "#43E97B"], font: "'Comic Sans MS','Luckiest Guy','Impact','GillSans-Bold','Arial Black',Tahoma,sans-serif", stroke: "#fff", strokeThickness: 7, dropShadow: true, dropShadowColor: "#222", dropShadowDistance: 3, dropShadowAngle: Math.PI / 2, dropShadowBlur: 7 }); backBtnTxt.anchor.set(0.5, 0.5); backBtnTxt.x = 0; backBtnTxt.y = 0; backBtn.addChild(backBtnTxt); // Position back button below music buttons backBtn.x = 0; backBtn.y = musicNames.length * 180 + 80; musicMenu.addChild(backBtn); game.addChild(musicMenu); // How to Play popup (hidden by default) var howToPlayPopup = new Container(); howToPlayPopup.visible = false; howToPlayPopup.x = 2048 / 2; howToPlayPopup.y = 1200; var popupBg = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, width: 1400, height: 900, x: 0, y: 0 }); howToPlayPopup.addChild(popupBg); var howToPlayText = new Text2("Tap the falling notes or the correct piano key at the right time to shoot monsters!\n\n" + "- Hit notes before they reach the bottom.\n" + "- Each hit fires a projectile at a monster.\n" + "- Don't let monsters reach you or miss too many notes!\n\n" + "Good luck!", { size: 60, fill: "#222", align: "center", wordWrap: true, wordWrapWidth: 1200 }); howToPlayText.anchor.set(0.5, 0.5); howToPlayText.x = 0; howToPlayText.y = -80; howToPlayPopup.addChild(howToPlayText); // Close button for popup var closePopupBtn = new Container(); var closePopupBg = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, width: 400, height: 120, x: 0, y: 0 }); closePopupBtn.addChild(closePopupBg); var closePopupTxt = new Text2("Close", { size: 70, fill: "#222" }); closePopupTxt.anchor.set(0.5, 0.5); closePopupTxt.x = 0; closePopupTxt.y = 0; closePopupBtn.addChild(closePopupTxt); closePopupBtn.x = 0; closePopupBtn.y = 320; howToPlayPopup.addChild(closePopupBtn); game.addChild(howToPlayPopup); // --- Splash screen input handling --- var splashActive = true; game.down = function (x, y, obj) { if (!splashActive) return; // Convert to local coordinates for splash elements var localX = x, localY = y; // Check Choose Music button if (localX >= chooseMusicBtn.x - 350 && localX <= chooseMusicBtn.x + 350 && localY >= chooseMusicBtn.y - 90 && localY <= chooseMusicBtn.y + 90) { // Show music menu musicMenu.visible = true; chooseMusicBtn.visible = false; howToPlayBtn.visible = false; return; } // Check How to Play button if (localX >= howToPlayBtn.x - 350 && localX <= howToPlayBtn.x + 350 && localY >= howToPlayBtn.y - 90 && localY <= howToPlayBtn.y + 90) { // Show how to play popup howToPlayPopup.visible = true; chooseMusicBtn.visible = false; howToPlayBtn.visible = false; return; } // Check music menu buttons if (musicMenu.visible) { // Check music selection buttons for (var i = 0; i < musicBtns.length; i++) { var btn = musicBtns[i]; var btnTop = musicMenu.y + btn.y - 70; var btnBottom = musicMenu.y + btn.y + 70; var btnLeft = musicMenu.x - 300; var btnRight = musicMenu.x + 300; if (localX >= btnLeft && localX <= btnRight && localY >= btnTop && localY <= btnBottom) { // Start game with selected music LK.playMusic(btn.musicId); // Track which music was selected selectedMusicId = btn.musicId; // Hide splash elements splashBg.visible = false; splashTitle.visible = false; chooseMusicBtn.visible = false; howToPlayBtn.visible = false; musicMenu.visible = false; howToPlayPopup.visible = false; splashActive = false; // Show correct background for selected music showMainGameBg(); // Enable main game input game.down = mainGameDownHandler; return; } } // Check back button var backBtnTop = musicMenu.y + musicMenu.children[musicMenu.children.length - 1].y - 60; var backBtnBottom = musicMenu.y + musicMenu.children[musicMenu.children.length - 1].y + 60; var backBtnLeft = musicMenu.x - 250; var backBtnRight = musicMenu.x + 250; if (localX >= backBtnLeft && localX <= backBtnRight && localY >= backBtnTop && localY <= backBtnBottom) { // Hide music menu, show splash buttons again musicMenu.visible = false; chooseMusicBtn.visible = true; howToPlayBtn.visible = true; return; } } // Check close button on how to play popup if (howToPlayPopup.visible) { var closeBtnTop = howToPlayPopup.y + closePopupBtn.y - 60; var closeBtnBottom = howToPlayPopup.y + closePopupBtn.y + 60; var closeBtnLeft = howToPlayPopup.x - 200; var closeBtnRight = howToPlayPopup.x + 200; if (localX >= closeBtnLeft && localX <= closeBtnRight && localY >= closeBtnTop && localY <= closeBtnBottom) { howToPlayPopup.visible = false; chooseMusicBtn.visible = true; howToPlayBtn.visible = true; return; } } }; // Save the original main game input handler var mainGameDownHandler = function mainGameDownHandler(x, y, obj) { // (Original game.down code will be inserted here by the next block) }; // --- Add top background asset for main game (hidden until splash is gone) --- // Default background (Classical Adventure) var topBg = LK.getAsset('top_bg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 1366 // Top half of the screen }); game.addChild(topBg); topBg.visible = false; // Funky Groove background (new asset, must be defined in Assets section) var funkyBg = LK.getAsset('funky_top_bg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 1366 }); game.addChild(funkyBg); funkyBg.visible = false; // Epic Battle background (new asset, must be defined in Assets section) var epicBattleBg = LK.getAsset('epic_battle_bg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 1366 }); game.addChild(epicBattleBg); epicBattleBg.visible = false; // Track which music is selected var selectedMusicId = null; // When splash is dismissed, show main game background var showMainGameBg = function showMainGameBg() { // Show only the correct background if (selectedMusicId === "gamemusic2") { funkyBg.visible = true; topBg.visible = false; epicBattleBg.visible = false; updateHeroPositionForMusic(); } else if (selectedMusicId === "gamemusic3") { epicBattleBg.visible = true; topBg.visible = false; funkyBg.visible = false; updateHeroPositionForMusic(); } else { topBg.visible = true; funkyBg.visible = false; epicBattleBg.visible = false; updateHeroPositionForMusic(); } }; // --- Constants --- // Tween plugin for note/monster animations // Bullet asset for weapon fire // 5 separate heart assets for health (can be different colors for each) var NUM_KEYS = 5; // --- Piano and Layout Constants --- var KEY_WIDTH = Math.floor(2048 / NUM_KEYS); // Each key fills 1/5 of width var KEY_HEIGHT = Math.floor(2732 / 2); // Keys fill bottom half var KEY_GAP = 0; // No gap, keys are flush var KEY_BOTTOM_MARGIN = 0; // No margin, keys reach bottom var NOTE_SIZE = 170; var NOTE_BASE_SPEED = 7; // Start slower var NOTE_MAX_SPEED = 7; // Cap speed var NOTE_SPEED = NOTE_BASE_SPEED; // Will be updated dynamically var NOTE_SPAWN_INTERVAL = 60; // frames var NOTE_SPEEDUP_INTERVAL = 600; // Every 10 seconds at 60fps var NOTE_SPEEDUP_AMOUNT = 1.2; // Increase by this amount each interval var MONSTER_SIZE = 180; var MONSTER_SPEED = 2.5; var MONSTER_HP = 2; var MONSTER_SPAWN_INTERVAL = 180; // frames var PROJECTILE_SIZE = 60; var PROJECTILE_SPEED = 40; var MAX_MISSES = 8; // --- Asset Initialization --- // --- Game State --- var pianoKeys = []; var notes = []; var monsters = []; var projectiles = []; var missCount = 0; var score = 0; var lastNoteSpawn = 0; var lastMonsterSpawn = 0; // --- Health Bar State (global) --- if (typeof playerMaxHealth === "undefined") { var playerMaxHealth = 5; } if (typeof playerHealth === "undefined") { var playerHealth = playerMaxHealth; } var healthBar = null; // --- Layout Calculations --- var pianoWidth = NUM_KEYS * KEY_WIDTH + (NUM_KEYS - 1) * KEY_GAP; var pianoLeft = 0; // Keys start at left edge var pianoTop = 2732 / 2; // Keys start at vertical center // --- No divider line: screen is visually split by content only --- // --- Hero (player) --- // --- Breathing effect for hero and weapon (in sync) --- // Move hero and weapon slightly lower only for Funky Groove map var heroYDefault = 770; var heroYFunky = 920; // Move down by 150px for Funky Groove // Move hero and weapon even higher for Funky Groove (very slightly up) var funkyGrooveYOffset = 18; // move up by 18px more (previously was -80px, now -98px) var hero = game.addChild(LK.getAsset('hero', { anchorX: 0.5, anchorY: 0.5, x: 400, y: heroYDefault })); // Add a weapon to hero's lap (bigger and centered for lap position) var weapon = LK.getAsset('projectile', { anchorX: 0.5, anchorY: 0.85, width: 230, height: 120, x: hero.x, y: hero.y + 90 }); game.addChild(weapon); // Helper to update hero/weapon Y for Funky Groove function updateHeroPositionForMusic() { if (selectedMusicId === "gamemusic2") { hero.y = heroYFunky - 98; // Move up by 98px for Funky Groove (was 80px, now even higher) weapon.y = hero.y + 90; } else if (selectedMusicId === "gamemusic3") { hero.y = heroYDefault - 134; // Move hero up by 4px more for Epic Battle weapon.y = hero.y + 90; } else { hero.y = heroYDefault; weapon.y = hero.y + 90; } } // Call on music select and in game.update to ensure correct position function startHeroBreathing() { // Reset scale to default before starting hero.scaleX = 1; hero.scaleY = 1; weapon.scaleX = 1; weapon.scaleY = 1; // Helper to tween both hero and weapon together function inhale() { tween(hero, { scaleX: 1.07, scaleY: 0.93 }, { duration: 900, easing: tween.easeInOut, onFinish: function onFinish() { tween(hero, { scaleX: 1, scaleY: 1 }, { duration: 900, easing: tween.easeInOut, onFinish: inhale }); } }); tween(weapon, { scaleX: 1.07, scaleY: 0.93 }, { duration: 900, easing: tween.easeInOut, onFinish: function onFinish() { tween(weapon, { scaleX: 1, scaleY: 1 }, { duration: 900, easing: tween.easeInOut }); } }); } inhale(); } startHeroBreathing(); // Keep weapon attached to hero's lap game.updateWeapon = function () { weapon.x = hero.x; weapon.y = hero.y + 90; }; // --- Score Display --- var scoreTxt = new Text2('0', { size: 120, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // --- Misses Display --- var missTxt = new Text2('Misses: 0', { size: 70, fill: 0xFF6666 }); missTxt.anchor.set(0.5, 0); LK.gui.top.addChild(missTxt); missTxt.y = 120; // --- Piano Keys --- for (var i = 0; i < NUM_KEYS; i++) { var key = new PianoKey(); key.index = i; key.keyWidth = KEY_WIDTH; key.keyHeight = KEY_HEIGHT; key.assetId = 'key_piano'; // Use key_piano asset for all keys key.baseColor = 0xffffff; key.x = pianoLeft + i * (KEY_WIDTH + KEY_GAP) + KEY_WIDTH / 2; key.y = 2732 - KEY_HEIGHT; // Place keys at the very bottom of the screen key.width = KEY_WIDTH; key.height = KEY_HEIGHT; game.addChild(key); pianoKeys.push(key); } // --- Input Handling --- mainGameDownHandler = function mainGameDownHandler(x, y, obj) { // Check if a key was pressed var keyPressed = false; for (var i = 0; i < pianoKeys.length; i++) { var key = pianoKeys[i]; // Key bounds var left = key.x - KEY_WIDTH / 2; var right = key.x + KEY_WIDTH / 2; var top = key.y; var bottom = key.y + KEY_HEIGHT; if (x >= left && x <= right && y >= top && y <= bottom) { key.flash(); handleKeyPress(i); keyPressed = true; break; } } // If not on a key, check if a note was tapped if (!keyPressed) { for (var n = 0; n < notes.length; n++) { var note = notes[n]; if (!note.active) { continue; } // Note bounds var noteLeft = note.x - NOTE_SIZE / 2; var noteRight = note.x + NOTE_SIZE / 2; var noteTop = note.y - NOTE_SIZE / 2; var noteBottom = note.y + NOTE_SIZE / 2; if (x >= noteLeft && x <= noteRight && y >= noteTop && y <= noteBottom) { // Only allow firing if note is above the bottom of its assigned key (not missed yet) var key = typeof note.keyIndex !== "undefined" && note.keyIndex >= 0 && note.keyIndex < pianoKeys.length ? pianoKeys[note.keyIndex] : null; if (key) { var missY = key.y + KEY_HEIGHT + NOTE_SIZE / 2; if (note.y < missY) { // Fire projectile at nearest monster, scale speed by timing accuracy var target = findNearestMonster(); if (target) { // Calculate accuracy: closer to center line = more accurate var centerLine = 2732 / 2; var maxDist = pianoTop - centerLine; var distFromCenter = Math.abs(note.y - centerLine); var accuracy = 1 - Math.min(distFromCenter / maxDist, 1); // 1 = perfect, 0 = worst var proj = new Projectile(); proj.size = PROJECTILE_SIZE; proj.x = weapon.x; proj.y = weapon.y; // Scale projectile speed: min 60, max 120 proj.speed = PROJECTILE_SPEED + Math.floor(accuracy * 60); proj.monster = target; proj.target = target; projectiles.push(proj); game.addChild(proj); // Animate hero and weapon for shooting tween(hero, { scaleX: 1.15, scaleY: 0.92 }, { duration: 80, yoyo: true, repeat: 1, onFinish: function onFinish() { // Restart breathing effect after shooting startHeroBreathing(); } }); // Rotate weapon to point toward the enemy var angleToTarget = Math.atan2(target.y - weapon.y, target.x - weapon.x); tween(weapon, { rotation: angleToTarget }, { duration: 80, yoyo: true, repeat: 1 }); } // Animate note note.active = false; tween(note, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 120, onFinish: function onFinish() { note.destroy(); } }); break; } } } } } }; // --- Key Press Logic --- function handleKeyPress(keyIndex) { // Find the first active note for this key that is still in the air (not missed yet) var hit = false; for (var n = 0; n < notes.length; n++) { var note = notes[n]; if (!note.active) { continue; } if (note.keyIndex !== keyIndex) { continue; } var key = pianoKeys[keyIndex]; // Only allow hit if note is inside the key bounds (not just above the key) var keyTop = key.y; var keyBottom = key.y + KEY_HEIGHT; var noteTop = note.y - NOTE_SIZE / 2; var noteBottom = note.y + NOTE_SIZE / 2; // Check if note is at least partially inside the key area var insideKey = !(noteBottom < keyTop || noteTop > keyBottom); // Rhythm mechanic: Only allow hit if note is visible (not missed, not below key) var missY = key.y + KEY_HEIGHT + NOTE_SIZE / 2; if (note.y >= missY || !note.active) { // Note is no longer visible, or already inactive, so pressing now is a miss break; } if (insideKey) { // Play key1 sound when any key is pressed LK.getSound('key1').play(); // Only allow hit if note is still active (not destroyed/missed) if (!note.active) { continue; } // Hit! note.active = false; hit = true; score += 1; scoreTxt.setText(score); // Fire projectile at nearest monster, scale speed by timing accuracy var target = findNearestMonster(); if (target) { // Calculate accuracy: closer to center line = more accurate var centerLine = 2732 / 2; var maxDist = pianoTop - centerLine; var distFromCenter = Math.abs(note.y - centerLine); var accuracy = 1 - Math.min(distFromCenter / maxDist, 1); // 1 = perfect, 0 = worst var proj = new Projectile(); proj.size = PROJECTILE_SIZE; proj.x = weapon.x; proj.y = weapon.y; // Scale projectile speed: min 60, max 120 proj.speed = PROJECTILE_SPEED + Math.floor(accuracy * 60); proj.monster = target; proj.target = target; projectiles.push(proj); game.addChild(proj); // Animate hero and weapon for shooting tween(hero, { scaleX: 1.15, scaleY: 0.92 }, { duration: 80, yoyo: true, repeat: 1, onFinish: function onFinish() { // Restart breathing effect after shooting startHeroBreathing(); } }); // Rotate weapon to point toward the enemy var angleToTarget = Math.atan2(target.y - weapon.y, target.x - weapon.x); tween(weapon, { rotation: angleToTarget }, { duration: 80, yoyo: true, repeat: 1 }); } // Animate note tween(note, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 120, onFinish: function onFinish() { note.destroy(); } }); break; } } if (!hit) { // Missed (pressed wrong key, or note is not visible, or note was already destroyed/missed) missCount += 1; missTxt.setText('Misses: ' + missCount); LK.effects.flashObject(hero, 0xff4444, 200); checkGameOver(); } } // --- Find Nearest Monster --- function findNearestMonster() { var minDist = 99999; var nearest = null; for (var i = 0; i < monsters.length; i++) { var m = monsters[i]; if (m.destroyed) { continue; } // Distance from hero to monster var dx = m.x - hero.x; var dy = m.y - hero.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < minDist) { minDist = dist; nearest = m; } } return nearest; } // --- Game Update Loop --- game.update = function () { // If splash screen is active, hide all main game elements and skip game logic if (typeof splashActive !== "undefined" && splashActive) { // Hide main game elements if (typeof topBg !== "undefined") topBg.visible = false; if (typeof hero !== "undefined") hero.visible = false; if (typeof weapon !== "undefined") weapon.visible = false; if (typeof healthBar !== "undefined" && healthBar) healthBar.visible = false; for (var i = 0; i < pianoKeys.length; i++) { if (pianoKeys[i]) pianoKeys[i].visible = false; } for (var i = 0; i < notes.length; i++) { if (notes[i]) notes[i].visible = false; } for (var i = 0; i < monsters.length; i++) { if (monsters[i]) monsters[i].visible = false; } for (var i = 0; i < projectiles.length; i++) { if (projectiles[i]) projectiles[i].visible = false; } if (typeof scoreTxt !== "undefined") scoreTxt.visible = false; if (typeof missTxt !== "undefined") missTxt.visible = false; return; } else { // Show main game elements when splash is gone if (typeof showMainGameBg === "function") showMainGameBg(); if (typeof hero !== "undefined") hero.visible = true; if (typeof weapon !== "undefined") weapon.visible = true; if (typeof healthBar !== "undefined" && healthBar) healthBar.visible = true; for (var i = 0; i < pianoKeys.length; i++) { if (pianoKeys[i]) pianoKeys[i].visible = true; } for (var i = 0; i < notes.length; i++) { if (notes[i]) notes[i].visible = true; } for (var i = 0; i < monsters.length; i++) { if (monsters[i]) monsters[i].visible = true; } for (var i = 0; i < projectiles.length; i++) { if (projectiles[i]) projectiles[i].visible = true; } if (typeof scoreTxt !== "undefined") scoreTxt.visible = true; if (typeof missTxt !== "undefined") missTxt.visible = true; } // Gradually speed up notes and monsters every NOTE_SPEEDUP_INTERVAL frames, up to max if (LK.ticks % NOTE_SPEEDUP_INTERVAL === 0 && LK.ticks > 0) { NOTE_SPEED = Math.min(NOTE_MAX_SPEED, NOTE_SPEED + NOTE_SPEEDUP_AMOUNT); // Cap monster speed at a separate, reasonable maximum var MONSTER_MAX_SPEED = 8; // Set a reasonable cap for monster speed // Also increase speed of all future monsters (and update current monsters to match) for (var i = 0; i < notes.length; i++) { notes[i].speed = NOTE_SPEED; } for (var j = 0; j < monsters.length; j++) { // Only update monsters that are not in attack range (so they don't "jump" while attacking) if (!monsters[j].destroyed && typeof monsters[j].dirX === "number" && typeof monsters[j].dirY === "number") { monsters[j].speed = Math.min(MONSTER_MAX_SPEED, NOTE_SPEED); } } } // --- DENSITY CONTROL --- // As the game progresses, increase the number of notes spawned at once (density), but cap speed // For example, every 20 seconds, increase density by 1, up to a max var DENSITY_INCREASE_INTERVAL = 1200; // every 20 seconds at 60fps var MAX_NOTE_DENSITY = Math.min(NUM_KEYS, 4); // never more than number of keys if (typeof noteDensity === "undefined") { var noteDensity = 1; } if (LK.ticks % DENSITY_INCREASE_INTERVAL === 0 && LK.ticks > 0) { noteDensity = Math.min(MAX_NOTE_DENSITY, noteDensity + 1); } // Spawn notes and corresponding monsters (enemies) in sync if (LK.ticks - lastNoteSpawn >= NOTE_SPAWN_INTERVAL) { lastNoteSpawn = LK.ticks; // Pick random keys to spawn notes on, up to noteDensity, never duplicate keys in one spawn var availableKeys = []; for (var i = 0; i < NUM_KEYS; i++) { availableKeys.push(i); } // Shuffle availableKeys for (var i = availableKeys.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = availableKeys[i]; availableKeys[i] = availableKeys[j]; availableKeys[j] = temp; } for (var d = 0; d < noteDensity; d++) { if (availableKeys.length === 0) { break; } var keyIdx = availableKeys.pop(); // Create note var note = new Note(); note.index = keyIdx; note.noteSize = NOTE_SIZE; // Assign note to a random key var key = pianoKeys[note.index]; note.keyIndex = note.index; // Start notes at the center line, horizontally aligned with their key note.x = key.x; note.y = 2732 / 2 - NOTE_SIZE / 2; note.speed = NOTE_SPEED; note.active = true; notes.push(note); game.addChild(note); // Create corresponding monster (enemy) for this note var monster; if (selectedMusicId === "gamemusic2") { monster = new Monster2(); } else if (selectedMusicId === "gamemusic3") { monster = new Monster3(); } else { monster = new Monster(); } monster.monsterSize = MONSTER_SIZE; // Spawn at the top right of the upper half of the screen monster.x = 2048 - MONSTER_SIZE / 2 - 40; // right margin, with a little padding monster.y = 220 + Math.random() * (2732 / 2 - 220 - MONSTER_SIZE); // random Y in upper half, not below center // Monster speed matches note speed, but is capped var MONSTER_MAX_SPEED = 8; monster.speed = Math.min(MONSTER_MAX_SPEED, note.speed); monster.maxHp = MONSTER_HP; monster.hp = MONSTER_HP; monster.baseColor = 0x8e44ad; monster.destroyed = false; // Calculate direction vector: from spawn point toward hero var dx = hero.x - monster.x; var dy = hero.y - monster.y; var dist = Math.sqrt(dx * dx + dy * dy); monster.dirX = dx / dist; monster.dirY = dy / dist; // Link monster to note and note to monster for removal note.linkedMonster = monster; monster.linkedNote = note; monsters.push(monster); game.addChild(monster); } } // Update notes for (var i = notes.length - 1; i >= 0; i--) { var note = notes[i]; if (note && typeof note.update === "function") { note.update(); } // Remove destroyed/inactive notes if (!note || !note.active || !note.parent || note.alpha === 0) { if (note && typeof note.destroy === "function" && note.parent) { note.destroy(); } notes.splice(i, 1); } else if (typeof note.keyIndex === "undefined" || note.keyIndex < 0 || note.keyIndex >= pianoKeys.length) { if (note && typeof note.destroy === "function" && note.parent) { note.destroy(); } notes.splice(i, 1); } } // Update projectiles for (var i = projectiles.length - 1; i >= 0; i--) { var p = projectiles[i]; if (p && typeof p.update === "function") { p.update(); } if (!p || !p.parent || p.alpha === 0) { if (p && typeof p.destroy === "function" && p.parent) { p.destroy(); } projectiles.splice(i, 1); } } // Update monsters and clean up destroyed ones for (var i = monsters.length - 1; i >= 0; i--) { var m = monsters[i]; if (m && typeof m.update === "function") { m.update(); } if (!m || m.destroyed && (!m.parent || m.alpha === 0)) { if (m && typeof m.destroy === "function" && m.parent) { m.destroy(); } monsters.splice(i, 1); } } // Defensive: Full cleanup for any orphaned/destroyed objects (extra safety, every 120 frames) if (LK.ticks % 120 === 0) { for (var i = notes.length - 1; i >= 0; i--) { var note = notes[i]; if (!note || !note.parent || note.alpha === 0) { if (note && typeof note.destroy === "function" && note.parent) { note.destroy(); } notes.splice(i, 1); } } for (var i = monsters.length - 1; i >= 0; i--) { var m = monsters[i]; if (!m || !m.parent || m.alpha === 0) { if (m && typeof m.destroy === "function" && m.parent) { m.destroy(); } monsters.splice(i, 1); } } for (var i = projectiles.length - 1; i >= 0; i--) { var p = projectiles[i]; if (!p || !p.parent || p.alpha === 0) { if (p && typeof p.destroy === "function" && p.parent) { p.destroy(); } projectiles.splice(i, 1); } } } // --- Monster attack logic --- // Player health bar and state if (typeof playerMaxHealth === "undefined") { var playerMaxHealth = 5; } if (typeof playerHealth === "undefined") { var playerHealth = playerMaxHealth; } if (typeof healthBar === "undefined") { var healthBar = null; } // Health bar setup (if not already) if (!healthBar) { healthBar = new Container(); healthBar.segments = []; // Heart icon size and spacing (make hearts bigger and move higher) var heartSize = 90; var gap = 24; var barWidth = playerMaxHealth * heartSize + (playerMaxHealth - 1) * gap; for (var h = 0; h < playerMaxHealth; h++) { // Use heart1-heart5 assets for each health segment var heartAssetId = 'heart' + (h + 1); var seg = LK.getAsset(heartAssetId, { anchorX: 0.5, anchorY: 0.5, width: heartSize, height: heartSize, x: h * (heartSize + gap), y: 0 }); healthBar.addChild(seg); healthBar.segments.push(seg); } // Move health bar higher above the hero and center healthBar.x = hero.x - barWidth / 2 + heartSize / 2; healthBar.y = hero.y - 220; game.addChild(healthBar); healthBar.update = function () { // Immediately update segment alpha for lost health for (var s = 0; s < healthBar.segments.length; s++) { var seg = healthBar.segments[s]; var shouldBeVisible = s < playerHealth; if (shouldBeVisible && seg.alpha < 1) { seg.alpha = 1; } else if (!shouldBeVisible && seg.alpha > 0) { seg.alpha = 0; } } }; healthBar.reset = function () { playerHealth = playerMaxHealth; for (var s = 0; s < healthBar.segments.length; s++) { healthBar.segments[s].alpha = 1; } if (typeof healthBar.update === "function") { healthBar.update(); } if (typeof healthBar.update === "function") { healthBar.update(); } }; } // Update health bar position to follow hero if (healthBar && healthBar.segments && healthBar.segments.length > 0) { var heartSize = healthBar.segments[0].width; var gap = healthBar.segments.length > 1 ? healthBar.segments[1].x - healthBar.segments[0].x - heartSize : 0; var barWidth = playerMaxHealth * heartSize + (playerMaxHealth - 1) * gap; healthBar.x = hero.x - barWidth / 2 + heartSize / 2; healthBar.y = hero.y - 220; } if (typeof healthBar.update === "function") { healthBar.update(); } // --- Monster attack logic --- var ATTACK_RANGE = 120; var DAMAGE_INTERVAL = 180; // 3 seconds at 60fps if (typeof gameOverTriggered === "undefined") { var gameOverTriggered = false; } if (typeof playerMaxHealth === "undefined") { var playerMaxHealth = 5; } if (typeof playerHealth === "undefined") { var playerHealth = playerMaxHealth; } for (var i = monsters.length - 1; i >= 0; i--) { var m = monsters[i]; if (m.destroyed) { continue; } // Calculate distance to hero var dx = m.x - hero.x; var dy = m.y - hero.y; var dist = Math.sqrt(dx * dx + dy * dy); // If in attack range, stop moving and attack if (dist <= ATTACK_RANGE) { // Stop monster movement m.dirX = 0; m.dirY = 0; m.speed = 0; // Place monster at edge of attack range (don't overlap hero) var angle = Math.atan2(dy, dx); m.x = hero.x + Math.cos(angle) * ATTACK_RANGE; m.y = hero.y + Math.sin(angle) * ATTACK_RANGE; // Each monster tracks its own attack timer (in frames) if (typeof m.attackTimer === "undefined") { m.attackTimer = 0; } // Track if monster was previously in range if (typeof m.lastInAttackRange === "undefined") { m.lastInAttackRange = false; } // If just entered attack range, reset timer so damage doesn't happen instantly if (!m.lastInAttackRange) { m.attackTimer = 0; } m.lastInAttackRange = true; // --- BITE ANIMATION & DAMAGE --- // Only increment timer and deal damage if game is not over if (!gameOverTriggered && playerHealth > 0) { m.attackTimer++; // Monster bites every DAMAGE_INTERVAL frames (3 seconds) if (m.attackTimer >= DAMAGE_INTERVAL) { m.attackTimer = 0; if (playerHealth > 0) { // Animate monster "bite" (scale up and back quickly) tween(m, { scaleX: 1.18, scaleY: 0.82 }, { duration: 90, yoyo: true, repeat: 1 }); // Remove the highest-numbered visible heart asset var heartToRemove = -1; for (var h = healthBar.segments.length - 1; h >= 0; h--) { if (healthBar.segments[h].alpha > 0) { heartToRemove = h; break; } } if (heartToRemove !== -1) { // Visually remove the heart asset healthBar.segments[heartToRemove].alpha = 0; // Flash hero red LK.effects.flashObject(hero, 0xff0000, 300); // Flash health bar segment (the one just lost) tween(healthBar.segments[heartToRemove], { tint: 0xff4444 }, { duration: 120, onFinish: function onFinish() { tween(this, { tint: 0x2ecc71 }, { duration: 120 }); }.bind(healthBar.segments[heartToRemove]) }); // Decrement playerHealth playerHealth--; if (typeof healthBar.update === "function") { healthBar.update(); } // If all hearts are gone, trigger game over var allHeartsGone = true; for (var h = 0; h < healthBar.segments.length; h++) { if (healthBar.segments[h].alpha > 0) { allHeartsGone = false; break; } } if (allHeartsGone && !gameOverTriggered) { gameOverTriggered = true; if (healthBar && typeof healthBar.update === "function") { healthBar.update(); } LK.effects.flashScreen(0xff0000, 800); LK.showGameOver(); // Reset health bar and health for next game if (healthBar) { healthBar.reset(); } playerHealth = playerMaxHealth; // Clean up all notes, monsters, and projectiles to prevent buildup for (var i = notes.length - 1; i >= 0; i--) { if (notes[i] && typeof notes[i].destroy === "function" && notes[i].parent) { notes[i].destroy(); } notes.splice(i, 1); } for (var i = monsters.length - 1; i >= 0; i--) { if (monsters[i] && typeof monsters[i].destroy === "function" && monsters[i].parent) { monsters[i].destroy(); } monsters.splice(i, 1); } for (var i = projectiles.length - 1; i >= 0; i--) { if (projectiles[i] && typeof projectiles[i].destroy === "function" && projectiles[i].parent) { projectiles[i].destroy(); } projectiles.splice(i, 1); } // Defensive: Reset arrays to empty to ensure no lingering references notes = []; monsters = []; projectiles = []; } } } } } } else { // Not in attack range, monster moves as normal if (typeof m.dirX !== "number" || typeof m.dirY !== "number" || m.speed === 0) { // Recalculate direction and restore speed, but cap it var dx2 = hero.x - m.x; var dy2 = hero.y - m.y; var dist2 = Math.sqrt(dx2 * dx2 + dy2 * dy2); m.dirX = dx2 / dist2; m.dirY = dy2 / dist2; } // Always set monster speed to match note speed (capped) var MONSTER_MAX_SPEED = 8; m.speed = Math.min(MONSTER_MAX_SPEED, NOTE_SPEED); // Reset attack timer and in-range flag if monster leaves range if (typeof m.attackTimer !== "undefined") { m.attackTimer = 0; } m.lastInAttackRange = false; } // Clean up timer if monster destroyed if (m.destroyed && typeof m.attackTimer !== "undefined") { delete m.attackTimer; delete m.lastInAttackRange; } } // Game over if health reaches 0 if (playerHealth <= 0 && !gameOverTriggered) { gameOverTriggered = true; if (healthBar && typeof healthBar.update === "function") { healthBar.update(); } LK.effects.flashScreen(0xff0000, 800); LK.showGameOver(); // Stop all monster attacks and player actions // Reset health bar and health for next game if (healthBar) { healthBar.reset(); } playerHealth = playerMaxHealth; // Clean up all notes, monsters, and projectiles to prevent buildup for (var i = notes.length - 1; i >= 0; i--) { if (notes[i] && typeof notes[i].destroy === "function" && notes[i].parent) { notes[i].destroy(); } notes.splice(i, 1); } for (var i = monsters.length - 1; i >= 0; i--) { if (monsters[i] && typeof monsters[i].destroy === "function" && monsters[i].parent) { monsters[i].destroy(); } monsters.splice(i, 1); } for (var i = projectiles.length - 1; i >= 0; i--) { if (projectiles[i] && typeof projectiles[i].destroy === "function" && projectiles[i].parent) { projectiles[i].destroy(); } projectiles.splice(i, 1); } // Defensive: Reset arrays to empty to ensure no lingering references notes = []; monsters = []; projectiles = []; } // Update weapon position to follow hero if (typeof updateHeroPositionForMusic === "function") { updateHeroPositionForMusic(); } if (typeof game.updateWeapon === "function") { game.updateWeapon(); } }; // --- Game Over Check --- function checkGameOver() { if (missCount >= MAX_MISSES) { LK.effects.flashScreen(0xff0000, 800); LK.showGameOver(); // Reset health bar and health for next game if (healthBar && typeof healthBar.update === "function") { healthBar.update(); } if (healthBar) { healthBar.reset(); } playerHealth = playerMaxHealth; // Clean up all notes, monsters, and projectiles to prevent buildup for (var i = notes.length - 1; i >= 0; i--) { if (notes[i] && typeof notes[i].destroy === "function" && notes[i].parent) { notes[i].destroy(); } notes.splice(i, 1); } for (var i = monsters.length - 1; i >= 0; i--) { if (monsters[i] && typeof monsters[i].destroy === "function" && monsters[i].parent) { monsters[i].destroy(); } monsters.splice(i, 1); } for (var i = projectiles.length - 1; i >= 0; i--) { if (projectiles[i] && typeof projectiles[i].destroy === "function" && projectiles[i].parent) { projectiles[i].destroy(); } projectiles.splice(i, 1); } // Defensive: Reset arrays to empty to ensure no lingering references notes = []; monsters = []; projectiles = []; } }
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// --- Win Condition (Optional: e.g. score 30) ---
/*
if (score >= 30) {
LK.showYouWin();
}
*/
// --- End of File ---
// Monster: Enemy advancing from the top
var Monster = Container.expand(function () {
var self = Container.call(this);
// Attach monster asset (box/ellipse)
var monsterAsset = self.attachAsset('monster', {
anchorX: 0.5,
anchorY: 0.5,
width: self.monsterSize,
height: self.monsterSize
});
// Health points
self.hp = self.maxHp;
// Called every tick
self.update = function () {
// Monsters only move forward if not destroyed
if (!self.destroyed) {
// Move toward hero using direction vector if set
if (typeof self.dirX === "number" && typeof self.dirY === "number") {
self.x += self.dirX * self.speed;
self.y += self.dirY * self.speed;
} else {
// fallback: move down
self.y += self.speed;
}
}
};
// --- Flying effect: gentle up/down hover using tween ---
function startFlyingEffect() {
// Reset to default before starting
self.yOffset = 0;
// Animate up
tween(self, {
yOffset: -18
}, {
duration: 420 + Math.random() * 120,
easing: tween.easeInOut,
onUpdate: function onUpdate() {
// Apply yOffset to monster position
self.y += self.yOffset - (self._lastYOffset || 0);
self._lastYOffset = self.yOffset;
},
onFinish: function onFinish() {
// Animate down
tween(self, {
yOffset: 18
}, {
duration: 420 + Math.random() * 120,
easing: tween.easeInOut,
onUpdate: function onUpdate() {
self.y += self.yOffset - (self._lastYOffset || 0);
self._lastYOffset = self.yOffset;
},
onFinish: function onFinish() {
startFlyingEffect();
}
});
}
});
}
startFlyingEffect();
// Take damage
self.hit = function () {
self.hp -= 1;
if (self.hp <= 0) {
self.destroyed = true;
// Play monster dead sound
LK.getSound('monsterdead').play();
// Animate scale up and fade out for death effect
tween(self, {
alpha: 0,
scaleX: 1.7,
scaleY: 1.7
}, {
duration: 220,
onFinish: function onFinish() {
self.destroy();
}
});
} else {
// Flash red
tween(monsterAsset, {
tint: 0xff4444
}, {
duration: 80,
onFinish: function onFinish() {
tween(monsterAsset, {
tint: self.baseColor
}, {
duration: 120
});
}
});
}
};
return self;
});
// Monster2: Funky Groove special monster
var Monster2 = Container.expand(function () {
var self = Container.call(this);
// Attach monster2 asset
var monsterAsset = self.attachAsset('monster2', {
anchorX: 0.5,
anchorY: 0.5,
width: self.monsterSize,
height: self.monsterSize
});
// Health points
self.hp = self.maxHp;
// Called every tick
self.update = function () {
if (!self.destroyed) {
if (typeof self.dirX === "number" && typeof self.dirY === "number") {
self.x += self.dirX * self.speed;
self.y += self.dirY * self.speed;
} else {
self.y += self.speed;
}
}
};
// --- Flying effect: gentle up/down hover using tween ---
function startFlyingEffect() {
self.yOffset = 0;
tween(self, {
yOffset: -18
}, {
duration: 420 + Math.random() * 120,
easing: tween.easeInOut,
onUpdate: function onUpdate() {
self.y += self.yOffset - (self._lastYOffset || 0);
self._lastYOffset = self.yOffset;
},
onFinish: function onFinish() {
tween(self, {
yOffset: 18
}, {
duration: 420 + Math.random() * 120,
easing: tween.easeInOut,
onUpdate: function onUpdate() {
self.y += self.yOffset - (self._lastYOffset || 0);
self._lastYOffset = self.yOffset;
},
onFinish: function onFinish() {
startFlyingEffect();
}
});
}
});
}
startFlyingEffect();
// Take damage
self.hit = function () {
self.hp -= 1;
if (self.hp <= 0) {
self.destroyed = true;
// Play monsterdead2 sound for Funky Groove monsters
LK.getSound('monsterdead2').play();
tween(self, {
alpha: 0,
scaleX: 1.7,
scaleY: 1.7
}, {
duration: 220,
onFinish: function onFinish() {
self.destroy();
}
});
} else {
// Flash red
tween(monsterAsset, {
tint: 0xff4444
}, {
duration: 80,
onFinish: function onFinish() {
tween(monsterAsset, {
tint: self.baseColor
}, {
duration: 120
});
}
});
}
};
return self;
});
// Monster3: Majestic Mountains special monster
var Monster3 = Container.expand(function () {
var self = Container.call(this);
// Attach monster3 asset
var monsterAsset = self.attachAsset('monster3', {
anchorX: 0.5,
anchorY: 0.5,
width: self.monsterSize,
height: self.monsterSize
});
// Health points
self.hp = self.maxHp;
// Called every tick
self.update = function () {
if (!self.destroyed) {
if (typeof self.dirX === "number" && typeof self.dirY === "number") {
self.x += self.dirX * self.speed;
self.y += self.dirY * self.speed;
} else {
self.y += self.speed;
}
}
};
// --- Flying effect: gentle up/down hover using tween ---
function startFlyingEffect() {
self.yOffset = 0;
tween(self, {
yOffset: -18
}, {
duration: 420 + Math.random() * 120,
easing: tween.easeInOut,
onUpdate: function onUpdate() {
self.y += self.yOffset - (self._lastYOffset || 0);
self._lastYOffset = self.yOffset;
},
onFinish: function onFinish() {
tween(self, {
yOffset: 18
}, {
duration: 420 + Math.random() * 120,
easing: tween.easeInOut,
onUpdate: function onUpdate() {
self.y += self.yOffset - (self._lastYOffset || 0);
self._lastYOffset = self.yOffset;
},
onFinish: function onFinish() {
startFlyingEffect();
}
});
}
});
}
startFlyingEffect();
// Take damage
self.hit = function () {
self.hp -= 1;
if (self.hp <= 0) {
self.destroyed = true;
// Play monster dead sound (same as Monster)
LK.getSound('monsterdead').play();
tween(self, {
alpha: 0,
scaleX: 1.7,
scaleY: 1.7
}, {
duration: 220,
onFinish: function onFinish() {
self.destroy();
}
});
} else {
// Flash red
tween(monsterAsset, {
tint: 0xff4444
}, {
duration: 80,
onFinish: function onFinish() {
tween(monsterAsset, {
tint: self.baseColor
}, {
duration: 120
});
}
});
}
};
return self;
});
// Note: Falling note that must be hit in time
var Note = Container.expand(function () {
var self = Container.call(this);
// Pick a random note asset for this note
var noteAssetId = 'note' + (1 + Math.floor(Math.random() * 4));
self.noteAssetId = noteAssetId;
var noteAsset = self.attachAsset(noteAssetId, {
anchorX: 0.5,
anchorY: 0.5,
width: self.noteSize,
height: self.noteSize
});
// Index of the key this note targets
self.keyIndex = typeof self.keyIndex !== "undefined" ? self.keyIndex : self.index;
// Used for hit/miss detection
self.active = true;
// Called every tick
self.update = function () {
// Notes always fall straight down toward their assigned key
self.y += self.speed;
// Find the key this note is assigned to
var key = typeof self.keyIndex !== "undefined" && self.keyIndex >= 0 && self.keyIndex < NUM_KEYS ? pianoKeys[self.keyIndex] : null;
if (key) {
var missY = key.y + KEY_HEIGHT + NOTE_SIZE / 2;
// If note crosses the bottom boundary and is still active, mark as miss and destroy
if (self.y > missY && self.active) {
self.active = false;
// Register miss
missCount += 1;
missTxt.setText('Misses: ' + missCount);
LK.effects.flashObject(hero, 0xff4444, 200);
checkGameOver();
self.destroy();
}
} else {
// Defensive: If key is missing, just destroy the note
self.destroy();
}
};
return self;
});
// PianoKey: Represents a single piano key at the bottom
var PianoKey = Container.expand(function () {
var self = Container.call(this);
// Defensive: Set defaults if not set
if (typeof self.assetId === "undefined") {
self.assetId = 'key_piano';
}
if (typeof self.keyWidth === "undefined") {
self.keyWidth = KEY_WIDTH;
}
if (typeof self.keyHeight === "undefined") {
self.keyHeight = KEY_HEIGHT;
}
if (typeof self.baseColor === "undefined") {
self.baseColor = 0xffffff;
}
if (typeof self.index === "undefined") {
self.index = 0;
}
// Attach key asset (always key_piano)
var keyAsset = self.attachAsset(self.assetId, {
anchorX: 0.5,
anchorY: 0,
width: self.keyWidth,
height: self.keyHeight
});
// Store index for reference
self.keyIndex = self.index;
// Set initial dim state
keyAsset.alpha = 0.45;
// Visual feedback for press
self.flash = function () {
// Light up key
tween(keyAsset, {
alpha: 1
}, {
duration: 60,
onFinish: function onFinish() {
// Return to dim after a short time
tween(keyAsset, {
alpha: 0.45
}, {
duration: 120
});
}
});
};
return self;
});
// Projectile: Fired by hero toward a monster
var Projectile = Container.expand(function () {
var self = Container.call(this);
// Defensive: Set defaults if not set
if (typeof self.size === "undefined") {
self.size = PROJECTILE_SIZE;
}
if (typeof self.speed === "undefined") {
self.speed = PROJECTILE_SPEED;
}
// Pick a random bullet asset for this projectile
var bulletAssetIds = ['bullet', 'bullet2', 'bullet3', 'bullet4'];
var chosenBulletAsset = bulletAssetIds[Math.floor(Math.random() * bulletAssetIds.length)];
// Attach bullet asset
var projAsset = self.attachAsset(chosenBulletAsset, {
anchorX: 0.5,
anchorY: 0.5,
width: self.size,
height: self.size
});
// Target monster (set externally)
self.target = null;
// Called every tick
self.update = function () {
// If no target or target destroyed, fade out and destroy
if (!self.target || self.target.destroyed || !self.target.parent) {
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 120,
onFinish: function onFinish() {
self.destroy();
}
});
return;
}
// Move toward target
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < self.speed) {
// Arrived at target: hit!
if (typeof self.target.hit === "function") {
self.target.hit();
}
// Impact effect
tween(self, {
alpha: 0,
scaleX: 1.7,
scaleY: 1.7
}, {
duration: 120,
onFinish: function onFinish() {
self.destroy();
}
});
return;
}
// Move toward target
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181830
});
/****
* Game Code
****/
// Funky Groove top background (separate from splash and other screens)
// --- SPLASH SCREEN LOGIC ---
// Create splash background asset (full screen, custom color or image)
var splashBg = LK.getAsset('entry_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChild(splashBg);
// Title text
var splashTitle = new Text2("Melody Mayhem", {
size: 210,
fill: 0xFFE600,
font: "'Impact','GillSans-Bold','Arial Black',Tahoma,sans-serif",
stroke: "#222",
strokeThickness: 16,
dropShadow: true,
dropShadowColor: "#000",
dropShadowDistance: 8,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 8
});
// Move splash title and buttons to the bottom of the screen
splashTitle.anchor.set(0.5, 1);
splashTitle.x = 2048 / 2;
splashTitle.y = 2732 - 420;
game.addChild(splashTitle);
// "Choose The Music" button
var chooseMusicBtn = new Container();
var chooseMusicBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 700,
height: 180,
x: 0,
y: 0
});
chooseMusicBtn.addChild(chooseMusicBg);
var chooseMusicTxt = new Text2("Choose The Music", {
size: 82,
fill: ["#FF5F6D", "#FFC371", "#43E97B", "#38F9D7"],
// gradient-like array for color pop
font: "'Comic Sans MS','Luckiest Guy','Impact','GillSans-Bold','Arial Black',Tahoma,sans-serif",
stroke: "#fff",
strokeThickness: 7,
dropShadow: true,
dropShadowColor: "#222",
dropShadowDistance: 3,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 7
});
chooseMusicTxt.anchor.set(0.5, 0.5);
chooseMusicTxt.x = 0;
chooseMusicTxt.y = 0;
chooseMusicBtn.addChild(chooseMusicTxt);
// "How to Play" button
var howToPlayBtn = new Container();
var howToPlayBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 700,
height: 180,
x: 0,
y: 0
});
howToPlayBtn.addChild(howToPlayBg);
var howToPlayTxt = new Text2("How to Play", {
size: 82,
fill: ["#43E97B", "#38F9D7", "#FF5F6D", "#FFC371"],
// gradient-like array for color pop
font: "'Comic Sans MS','Luckiest Guy','Impact','GillSans-Bold','Arial Black',Tahoma,sans-serif",
stroke: "#fff",
strokeThickness: 7,
dropShadow: true,
dropShadowColor: "#222",
dropShadowDistance: 3,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 7
});
howToPlayTxt.anchor.set(0.5, 0.5);
howToPlayTxt.x = 0;
howToPlayTxt.y = 0;
howToPlayBtn.addChild(howToPlayTxt);
// Position both buttons side by side, centered horizontally at the bottom
var buttonSpacing = 80; // space between buttons
var buttonY = 2732 - 180; // bottom margin, same for both
var buttonTotalWidth = 700 * 2 + buttonSpacing;
var leftBtnX = 2048 / 2 - (700 / 2 + buttonSpacing / 2);
var rightBtnX = 2048 / 2 + (700 / 2 + buttonSpacing / 2);
chooseMusicBtn.x = leftBtnX;
chooseMusicBtn.y = buttonY;
howToPlayBtn.x = rightBtnX;
howToPlayBtn.y = buttonY;
game.addChild(chooseMusicBtn);
game.addChild(howToPlayBtn);
// Music selection menu (hidden by default)
var musicMenu = new Container();
musicMenu.visible = false;
musicMenu.x = 2048 / 2;
musicMenu.y = 1200;
var musicNames = [{
id: "gamemusic",
label: "Cyber : 2099",
fill: ["#00F0FF", "#FF00C8", "#00FF85"] // Vibrant neon
}, {
id: "gamemusic2",
label: "Welcome to Hell",
fill: ["#FF2D00", "#FFB800", "#FF00A8"] // Hot, fiery
}, {
id: "gamemusic3",
label: "Majestic Mountains",
fill: ["#00FFB2", "#00B2FF", "#FFFA00"] // Lively, fresh
}];
var musicBtns = [];
for (var i = 0; i < musicNames.length; i++) {
var btn = new Container();
var btnBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 820,
// wider for long names
height: 150,
// slightly taller for better fit
x: 0,
y: 0
});
btn.addChild(btnBg);
var btnTxt = new Text2(musicNames[i].label, {
size: 66,
// smaller font to fit long names
fill: musicNames[i].fill,
font: "'Comic Sans MS','Luckiest Guy','Impact','GillSans-Bold','Arial Black',Tahoma,sans-serif",
stroke: "#fff",
strokeThickness: 8,
dropShadow: true,
dropShadowColor: "#000",
dropShadowDistance: 4,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 8
});
btnTxt.anchor.set(0.5, 0.5);
btnTxt.x = 0;
btnTxt.y = 0;
btn.addChild(btnTxt);
btn.x = 0;
btn.y = i * 180;
btn.musicId = musicNames[i].id;
musicBtns.push(btn);
musicMenu.addChild(btn);
}
// Add a back button to the music menu
var backBtn = new Container();
var backBtnBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 500,
height: 120,
x: 0,
y: 0
});
backBtn.addChild(backBtnBg);
var backBtnTxt = new Text2("Back", {
size: 70,
fill: ["#FF5F6D", "#43E97B"],
font: "'Comic Sans MS','Luckiest Guy','Impact','GillSans-Bold','Arial Black',Tahoma,sans-serif",
stroke: "#fff",
strokeThickness: 7,
dropShadow: true,
dropShadowColor: "#222",
dropShadowDistance: 3,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 7
});
backBtnTxt.anchor.set(0.5, 0.5);
backBtnTxt.x = 0;
backBtnTxt.y = 0;
backBtn.addChild(backBtnTxt);
// Position back button below music buttons
backBtn.x = 0;
backBtn.y = musicNames.length * 180 + 80;
musicMenu.addChild(backBtn);
game.addChild(musicMenu);
// How to Play popup (hidden by default)
var howToPlayPopup = new Container();
howToPlayPopup.visible = false;
howToPlayPopup.x = 2048 / 2;
howToPlayPopup.y = 1200;
var popupBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 1400,
height: 900,
x: 0,
y: 0
});
howToPlayPopup.addChild(popupBg);
var howToPlayText = new Text2("Tap the falling notes or the correct piano key at the right time to shoot monsters!\n\n" + "- Hit notes before they reach the bottom.\n" + "- Each hit fires a projectile at a monster.\n" + "- Don't let monsters reach you or miss too many notes!\n\n" + "Good luck!", {
size: 60,
fill: "#222",
align: "center",
wordWrap: true,
wordWrapWidth: 1200
});
howToPlayText.anchor.set(0.5, 0.5);
howToPlayText.x = 0;
howToPlayText.y = -80;
howToPlayPopup.addChild(howToPlayText);
// Close button for popup
var closePopupBtn = new Container();
var closePopupBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 400,
height: 120,
x: 0,
y: 0
});
closePopupBtn.addChild(closePopupBg);
var closePopupTxt = new Text2("Close", {
size: 70,
fill: "#222"
});
closePopupTxt.anchor.set(0.5, 0.5);
closePopupTxt.x = 0;
closePopupTxt.y = 0;
closePopupBtn.addChild(closePopupTxt);
closePopupBtn.x = 0;
closePopupBtn.y = 320;
howToPlayPopup.addChild(closePopupBtn);
game.addChild(howToPlayPopup);
// --- Splash screen input handling ---
var splashActive = true;
game.down = function (x, y, obj) {
if (!splashActive) return;
// Convert to local coordinates for splash elements
var localX = x,
localY = y;
// Check Choose Music button
if (localX >= chooseMusicBtn.x - 350 && localX <= chooseMusicBtn.x + 350 && localY >= chooseMusicBtn.y - 90 && localY <= chooseMusicBtn.y + 90) {
// Show music menu
musicMenu.visible = true;
chooseMusicBtn.visible = false;
howToPlayBtn.visible = false;
return;
}
// Check How to Play button
if (localX >= howToPlayBtn.x - 350 && localX <= howToPlayBtn.x + 350 && localY >= howToPlayBtn.y - 90 && localY <= howToPlayBtn.y + 90) {
// Show how to play popup
howToPlayPopup.visible = true;
chooseMusicBtn.visible = false;
howToPlayBtn.visible = false;
return;
}
// Check music menu buttons
if (musicMenu.visible) {
// Check music selection buttons
for (var i = 0; i < musicBtns.length; i++) {
var btn = musicBtns[i];
var btnTop = musicMenu.y + btn.y - 70;
var btnBottom = musicMenu.y + btn.y + 70;
var btnLeft = musicMenu.x - 300;
var btnRight = musicMenu.x + 300;
if (localX >= btnLeft && localX <= btnRight && localY >= btnTop && localY <= btnBottom) {
// Start game with selected music
LK.playMusic(btn.musicId);
// Track which music was selected
selectedMusicId = btn.musicId;
// Hide splash elements
splashBg.visible = false;
splashTitle.visible = false;
chooseMusicBtn.visible = false;
howToPlayBtn.visible = false;
musicMenu.visible = false;
howToPlayPopup.visible = false;
splashActive = false;
// Show correct background for selected music
showMainGameBg();
// Enable main game input
game.down = mainGameDownHandler;
return;
}
}
// Check back button
var backBtnTop = musicMenu.y + musicMenu.children[musicMenu.children.length - 1].y - 60;
var backBtnBottom = musicMenu.y + musicMenu.children[musicMenu.children.length - 1].y + 60;
var backBtnLeft = musicMenu.x - 250;
var backBtnRight = musicMenu.x + 250;
if (localX >= backBtnLeft && localX <= backBtnRight && localY >= backBtnTop && localY <= backBtnBottom) {
// Hide music menu, show splash buttons again
musicMenu.visible = false;
chooseMusicBtn.visible = true;
howToPlayBtn.visible = true;
return;
}
}
// Check close button on how to play popup
if (howToPlayPopup.visible) {
var closeBtnTop = howToPlayPopup.y + closePopupBtn.y - 60;
var closeBtnBottom = howToPlayPopup.y + closePopupBtn.y + 60;
var closeBtnLeft = howToPlayPopup.x - 200;
var closeBtnRight = howToPlayPopup.x + 200;
if (localX >= closeBtnLeft && localX <= closeBtnRight && localY >= closeBtnTop && localY <= closeBtnBottom) {
howToPlayPopup.visible = false;
chooseMusicBtn.visible = true;
howToPlayBtn.visible = true;
return;
}
}
};
// Save the original main game input handler
var mainGameDownHandler = function mainGameDownHandler(x, y, obj) {
// (Original game.down code will be inserted here by the next block)
};
// --- Add top background asset for main game (hidden until splash is gone) ---
// Default background (Classical Adventure)
var topBg = LK.getAsset('top_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 1366 // Top half of the screen
});
game.addChild(topBg);
topBg.visible = false;
// Funky Groove background (new asset, must be defined in Assets section)
var funkyBg = LK.getAsset('funky_top_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 1366
});
game.addChild(funkyBg);
funkyBg.visible = false;
// Epic Battle background (new asset, must be defined in Assets section)
var epicBattleBg = LK.getAsset('epic_battle_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 1366
});
game.addChild(epicBattleBg);
epicBattleBg.visible = false;
// Track which music is selected
var selectedMusicId = null;
// When splash is dismissed, show main game background
var showMainGameBg = function showMainGameBg() {
// Show only the correct background
if (selectedMusicId === "gamemusic2") {
funkyBg.visible = true;
topBg.visible = false;
epicBattleBg.visible = false;
updateHeroPositionForMusic();
} else if (selectedMusicId === "gamemusic3") {
epicBattleBg.visible = true;
topBg.visible = false;
funkyBg.visible = false;
updateHeroPositionForMusic();
} else {
topBg.visible = true;
funkyBg.visible = false;
epicBattleBg.visible = false;
updateHeroPositionForMusic();
}
};
// --- Constants ---
// Tween plugin for note/monster animations
// Bullet asset for weapon fire
// 5 separate heart assets for health (can be different colors for each)
var NUM_KEYS = 5;
// --- Piano and Layout Constants ---
var KEY_WIDTH = Math.floor(2048 / NUM_KEYS); // Each key fills 1/5 of width
var KEY_HEIGHT = Math.floor(2732 / 2); // Keys fill bottom half
var KEY_GAP = 0; // No gap, keys are flush
var KEY_BOTTOM_MARGIN = 0; // No margin, keys reach bottom
var NOTE_SIZE = 170;
var NOTE_BASE_SPEED = 7; // Start slower
var NOTE_MAX_SPEED = 7; // Cap speed
var NOTE_SPEED = NOTE_BASE_SPEED; // Will be updated dynamically
var NOTE_SPAWN_INTERVAL = 60; // frames
var NOTE_SPEEDUP_INTERVAL = 600; // Every 10 seconds at 60fps
var NOTE_SPEEDUP_AMOUNT = 1.2; // Increase by this amount each interval
var MONSTER_SIZE = 180;
var MONSTER_SPEED = 2.5;
var MONSTER_HP = 2;
var MONSTER_SPAWN_INTERVAL = 180; // frames
var PROJECTILE_SIZE = 60;
var PROJECTILE_SPEED = 40;
var MAX_MISSES = 8;
// --- Asset Initialization ---
// --- Game State ---
var pianoKeys = [];
var notes = [];
var monsters = [];
var projectiles = [];
var missCount = 0;
var score = 0;
var lastNoteSpawn = 0;
var lastMonsterSpawn = 0;
// --- Health Bar State (global) ---
if (typeof playerMaxHealth === "undefined") {
var playerMaxHealth = 5;
}
if (typeof playerHealth === "undefined") {
var playerHealth = playerMaxHealth;
}
var healthBar = null;
// --- Layout Calculations ---
var pianoWidth = NUM_KEYS * KEY_WIDTH + (NUM_KEYS - 1) * KEY_GAP;
var pianoLeft = 0; // Keys start at left edge
var pianoTop = 2732 / 2; // Keys start at vertical center
// --- No divider line: screen is visually split by content only ---
// --- Hero (player) ---
// --- Breathing effect for hero and weapon (in sync) ---
// Move hero and weapon slightly lower only for Funky Groove map
var heroYDefault = 770;
var heroYFunky = 920; // Move down by 150px for Funky Groove
// Move hero and weapon even higher for Funky Groove (very slightly up)
var funkyGrooveYOffset = 18; // move up by 18px more (previously was -80px, now -98px)
var hero = game.addChild(LK.getAsset('hero', {
anchorX: 0.5,
anchorY: 0.5,
x: 400,
y: heroYDefault
}));
// Add a weapon to hero's lap (bigger and centered for lap position)
var weapon = LK.getAsset('projectile', {
anchorX: 0.5,
anchorY: 0.85,
width: 230,
height: 120,
x: hero.x,
y: hero.y + 90
});
game.addChild(weapon);
// Helper to update hero/weapon Y for Funky Groove
function updateHeroPositionForMusic() {
if (selectedMusicId === "gamemusic2") {
hero.y = heroYFunky - 98; // Move up by 98px for Funky Groove (was 80px, now even higher)
weapon.y = hero.y + 90;
} else if (selectedMusicId === "gamemusic3") {
hero.y = heroYDefault - 134; // Move hero up by 4px more for Epic Battle
weapon.y = hero.y + 90;
} else {
hero.y = heroYDefault;
weapon.y = hero.y + 90;
}
}
// Call on music select and in game.update to ensure correct position
function startHeroBreathing() {
// Reset scale to default before starting
hero.scaleX = 1;
hero.scaleY = 1;
weapon.scaleX = 1;
weapon.scaleY = 1;
// Helper to tween both hero and weapon together
function inhale() {
tween(hero, {
scaleX: 1.07,
scaleY: 0.93
}, {
duration: 900,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(hero, {
scaleX: 1,
scaleY: 1
}, {
duration: 900,
easing: tween.easeInOut,
onFinish: inhale
});
}
});
tween(weapon, {
scaleX: 1.07,
scaleY: 0.93
}, {
duration: 900,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(weapon, {
scaleX: 1,
scaleY: 1
}, {
duration: 900,
easing: tween.easeInOut
});
}
});
}
inhale();
}
startHeroBreathing();
// Keep weapon attached to hero's lap
game.updateWeapon = function () {
weapon.x = hero.x;
weapon.y = hero.y + 90;
};
// --- Score Display ---
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Misses Display ---
var missTxt = new Text2('Misses: 0', {
size: 70,
fill: 0xFF6666
});
missTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(missTxt);
missTxt.y = 120;
// --- Piano Keys ---
for (var i = 0; i < NUM_KEYS; i++) {
var key = new PianoKey();
key.index = i;
key.keyWidth = KEY_WIDTH;
key.keyHeight = KEY_HEIGHT;
key.assetId = 'key_piano'; // Use key_piano asset for all keys
key.baseColor = 0xffffff;
key.x = pianoLeft + i * (KEY_WIDTH + KEY_GAP) + KEY_WIDTH / 2;
key.y = 2732 - KEY_HEIGHT; // Place keys at the very bottom of the screen
key.width = KEY_WIDTH;
key.height = KEY_HEIGHT;
game.addChild(key);
pianoKeys.push(key);
}
// --- Input Handling ---
mainGameDownHandler = function mainGameDownHandler(x, y, obj) {
// Check if a key was pressed
var keyPressed = false;
for (var i = 0; i < pianoKeys.length; i++) {
var key = pianoKeys[i];
// Key bounds
var left = key.x - KEY_WIDTH / 2;
var right = key.x + KEY_WIDTH / 2;
var top = key.y;
var bottom = key.y + KEY_HEIGHT;
if (x >= left && x <= right && y >= top && y <= bottom) {
key.flash();
handleKeyPress(i);
keyPressed = true;
break;
}
}
// If not on a key, check if a note was tapped
if (!keyPressed) {
for (var n = 0; n < notes.length; n++) {
var note = notes[n];
if (!note.active) {
continue;
}
// Note bounds
var noteLeft = note.x - NOTE_SIZE / 2;
var noteRight = note.x + NOTE_SIZE / 2;
var noteTop = note.y - NOTE_SIZE / 2;
var noteBottom = note.y + NOTE_SIZE / 2;
if (x >= noteLeft && x <= noteRight && y >= noteTop && y <= noteBottom) {
// Only allow firing if note is above the bottom of its assigned key (not missed yet)
var key = typeof note.keyIndex !== "undefined" && note.keyIndex >= 0 && note.keyIndex < pianoKeys.length ? pianoKeys[note.keyIndex] : null;
if (key) {
var missY = key.y + KEY_HEIGHT + NOTE_SIZE / 2;
if (note.y < missY) {
// Fire projectile at nearest monster, scale speed by timing accuracy
var target = findNearestMonster();
if (target) {
// Calculate accuracy: closer to center line = more accurate
var centerLine = 2732 / 2;
var maxDist = pianoTop - centerLine;
var distFromCenter = Math.abs(note.y - centerLine);
var accuracy = 1 - Math.min(distFromCenter / maxDist, 1); // 1 = perfect, 0 = worst
var proj = new Projectile();
proj.size = PROJECTILE_SIZE;
proj.x = weapon.x;
proj.y = weapon.y;
// Scale projectile speed: min 60, max 120
proj.speed = PROJECTILE_SPEED + Math.floor(accuracy * 60);
proj.monster = target;
proj.target = target;
projectiles.push(proj);
game.addChild(proj);
// Animate hero and weapon for shooting
tween(hero, {
scaleX: 1.15,
scaleY: 0.92
}, {
duration: 80,
yoyo: true,
repeat: 1,
onFinish: function onFinish() {
// Restart breathing effect after shooting
startHeroBreathing();
}
});
// Rotate weapon to point toward the enemy
var angleToTarget = Math.atan2(target.y - weapon.y, target.x - weapon.x);
tween(weapon, {
rotation: angleToTarget
}, {
duration: 80,
yoyo: true,
repeat: 1
});
}
// Animate note
note.active = false;
tween(note, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 120,
onFinish: function onFinish() {
note.destroy();
}
});
break;
}
}
}
}
}
};
// --- Key Press Logic ---
function handleKeyPress(keyIndex) {
// Find the first active note for this key that is still in the air (not missed yet)
var hit = false;
for (var n = 0; n < notes.length; n++) {
var note = notes[n];
if (!note.active) {
continue;
}
if (note.keyIndex !== keyIndex) {
continue;
}
var key = pianoKeys[keyIndex];
// Only allow hit if note is inside the key bounds (not just above the key)
var keyTop = key.y;
var keyBottom = key.y + KEY_HEIGHT;
var noteTop = note.y - NOTE_SIZE / 2;
var noteBottom = note.y + NOTE_SIZE / 2;
// Check if note is at least partially inside the key area
var insideKey = !(noteBottom < keyTop || noteTop > keyBottom);
// Rhythm mechanic: Only allow hit if note is visible (not missed, not below key)
var missY = key.y + KEY_HEIGHT + NOTE_SIZE / 2;
if (note.y >= missY || !note.active) {
// Note is no longer visible, or already inactive, so pressing now is a miss
break;
}
if (insideKey) {
// Play key1 sound when any key is pressed
LK.getSound('key1').play();
// Only allow hit if note is still active (not destroyed/missed)
if (!note.active) {
continue;
}
// Hit!
note.active = false;
hit = true;
score += 1;
scoreTxt.setText(score);
// Fire projectile at nearest monster, scale speed by timing accuracy
var target = findNearestMonster();
if (target) {
// Calculate accuracy: closer to center line = more accurate
var centerLine = 2732 / 2;
var maxDist = pianoTop - centerLine;
var distFromCenter = Math.abs(note.y - centerLine);
var accuracy = 1 - Math.min(distFromCenter / maxDist, 1); // 1 = perfect, 0 = worst
var proj = new Projectile();
proj.size = PROJECTILE_SIZE;
proj.x = weapon.x;
proj.y = weapon.y;
// Scale projectile speed: min 60, max 120
proj.speed = PROJECTILE_SPEED + Math.floor(accuracy * 60);
proj.monster = target;
proj.target = target;
projectiles.push(proj);
game.addChild(proj);
// Animate hero and weapon for shooting
tween(hero, {
scaleX: 1.15,
scaleY: 0.92
}, {
duration: 80,
yoyo: true,
repeat: 1,
onFinish: function onFinish() {
// Restart breathing effect after shooting
startHeroBreathing();
}
});
// Rotate weapon to point toward the enemy
var angleToTarget = Math.atan2(target.y - weapon.y, target.x - weapon.x);
tween(weapon, {
rotation: angleToTarget
}, {
duration: 80,
yoyo: true,
repeat: 1
});
}
// Animate note
tween(note, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 120,
onFinish: function onFinish() {
note.destroy();
}
});
break;
}
}
if (!hit) {
// Missed (pressed wrong key, or note is not visible, or note was already destroyed/missed)
missCount += 1;
missTxt.setText('Misses: ' + missCount);
LK.effects.flashObject(hero, 0xff4444, 200);
checkGameOver();
}
}
// --- Find Nearest Monster ---
function findNearestMonster() {
var minDist = 99999;
var nearest = null;
for (var i = 0; i < monsters.length; i++) {
var m = monsters[i];
if (m.destroyed) {
continue;
}
// Distance from hero to monster
var dx = m.x - hero.x;
var dy = m.y - hero.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist) {
minDist = dist;
nearest = m;
}
}
return nearest;
}
// --- Game Update Loop ---
game.update = function () {
// If splash screen is active, hide all main game elements and skip game logic
if (typeof splashActive !== "undefined" && splashActive) {
// Hide main game elements
if (typeof topBg !== "undefined") topBg.visible = false;
if (typeof hero !== "undefined") hero.visible = false;
if (typeof weapon !== "undefined") weapon.visible = false;
if (typeof healthBar !== "undefined" && healthBar) healthBar.visible = false;
for (var i = 0; i < pianoKeys.length; i++) {
if (pianoKeys[i]) pianoKeys[i].visible = false;
}
for (var i = 0; i < notes.length; i++) {
if (notes[i]) notes[i].visible = false;
}
for (var i = 0; i < monsters.length; i++) {
if (monsters[i]) monsters[i].visible = false;
}
for (var i = 0; i < projectiles.length; i++) {
if (projectiles[i]) projectiles[i].visible = false;
}
if (typeof scoreTxt !== "undefined") scoreTxt.visible = false;
if (typeof missTxt !== "undefined") missTxt.visible = false;
return;
} else {
// Show main game elements when splash is gone
if (typeof showMainGameBg === "function") showMainGameBg();
if (typeof hero !== "undefined") hero.visible = true;
if (typeof weapon !== "undefined") weapon.visible = true;
if (typeof healthBar !== "undefined" && healthBar) healthBar.visible = true;
for (var i = 0; i < pianoKeys.length; i++) {
if (pianoKeys[i]) pianoKeys[i].visible = true;
}
for (var i = 0; i < notes.length; i++) {
if (notes[i]) notes[i].visible = true;
}
for (var i = 0; i < monsters.length; i++) {
if (monsters[i]) monsters[i].visible = true;
}
for (var i = 0; i < projectiles.length; i++) {
if (projectiles[i]) projectiles[i].visible = true;
}
if (typeof scoreTxt !== "undefined") scoreTxt.visible = true;
if (typeof missTxt !== "undefined") missTxt.visible = true;
}
// Gradually speed up notes and monsters every NOTE_SPEEDUP_INTERVAL frames, up to max
if (LK.ticks % NOTE_SPEEDUP_INTERVAL === 0 && LK.ticks > 0) {
NOTE_SPEED = Math.min(NOTE_MAX_SPEED, NOTE_SPEED + NOTE_SPEEDUP_AMOUNT);
// Cap monster speed at a separate, reasonable maximum
var MONSTER_MAX_SPEED = 8; // Set a reasonable cap for monster speed
// Also increase speed of all future monsters (and update current monsters to match)
for (var i = 0; i < notes.length; i++) {
notes[i].speed = NOTE_SPEED;
}
for (var j = 0; j < monsters.length; j++) {
// Only update monsters that are not in attack range (so they don't "jump" while attacking)
if (!monsters[j].destroyed && typeof monsters[j].dirX === "number" && typeof monsters[j].dirY === "number") {
monsters[j].speed = Math.min(MONSTER_MAX_SPEED, NOTE_SPEED);
}
}
}
// --- DENSITY CONTROL ---
// As the game progresses, increase the number of notes spawned at once (density), but cap speed
// For example, every 20 seconds, increase density by 1, up to a max
var DENSITY_INCREASE_INTERVAL = 1200; // every 20 seconds at 60fps
var MAX_NOTE_DENSITY = Math.min(NUM_KEYS, 4); // never more than number of keys
if (typeof noteDensity === "undefined") {
var noteDensity = 1;
}
if (LK.ticks % DENSITY_INCREASE_INTERVAL === 0 && LK.ticks > 0) {
noteDensity = Math.min(MAX_NOTE_DENSITY, noteDensity + 1);
}
// Spawn notes and corresponding monsters (enemies) in sync
if (LK.ticks - lastNoteSpawn >= NOTE_SPAWN_INTERVAL) {
lastNoteSpawn = LK.ticks;
// Pick random keys to spawn notes on, up to noteDensity, never duplicate keys in one spawn
var availableKeys = [];
for (var i = 0; i < NUM_KEYS; i++) {
availableKeys.push(i);
}
// Shuffle availableKeys
for (var i = availableKeys.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = availableKeys[i];
availableKeys[i] = availableKeys[j];
availableKeys[j] = temp;
}
for (var d = 0; d < noteDensity; d++) {
if (availableKeys.length === 0) {
break;
}
var keyIdx = availableKeys.pop();
// Create note
var note = new Note();
note.index = keyIdx;
note.noteSize = NOTE_SIZE;
// Assign note to a random key
var key = pianoKeys[note.index];
note.keyIndex = note.index;
// Start notes at the center line, horizontally aligned with their key
note.x = key.x;
note.y = 2732 / 2 - NOTE_SIZE / 2;
note.speed = NOTE_SPEED;
note.active = true;
notes.push(note);
game.addChild(note);
// Create corresponding monster (enemy) for this note
var monster;
if (selectedMusicId === "gamemusic2") {
monster = new Monster2();
} else if (selectedMusicId === "gamemusic3") {
monster = new Monster3();
} else {
monster = new Monster();
}
monster.monsterSize = MONSTER_SIZE;
// Spawn at the top right of the upper half of the screen
monster.x = 2048 - MONSTER_SIZE / 2 - 40; // right margin, with a little padding
monster.y = 220 + Math.random() * (2732 / 2 - 220 - MONSTER_SIZE); // random Y in upper half, not below center
// Monster speed matches note speed, but is capped
var MONSTER_MAX_SPEED = 8;
monster.speed = Math.min(MONSTER_MAX_SPEED, note.speed);
monster.maxHp = MONSTER_HP;
monster.hp = MONSTER_HP;
monster.baseColor = 0x8e44ad;
monster.destroyed = false;
// Calculate direction vector: from spawn point toward hero
var dx = hero.x - monster.x;
var dy = hero.y - monster.y;
var dist = Math.sqrt(dx * dx + dy * dy);
monster.dirX = dx / dist;
monster.dirY = dy / dist;
// Link monster to note and note to monster for removal
note.linkedMonster = monster;
monster.linkedNote = note;
monsters.push(monster);
game.addChild(monster);
}
}
// Update notes
for (var i = notes.length - 1; i >= 0; i--) {
var note = notes[i];
if (note && typeof note.update === "function") {
note.update();
}
// Remove destroyed/inactive notes
if (!note || !note.active || !note.parent || note.alpha === 0) {
if (note && typeof note.destroy === "function" && note.parent) {
note.destroy();
}
notes.splice(i, 1);
} else if (typeof note.keyIndex === "undefined" || note.keyIndex < 0 || note.keyIndex >= pianoKeys.length) {
if (note && typeof note.destroy === "function" && note.parent) {
note.destroy();
}
notes.splice(i, 1);
}
}
// Update projectiles
for (var i = projectiles.length - 1; i >= 0; i--) {
var p = projectiles[i];
if (p && typeof p.update === "function") {
p.update();
}
if (!p || !p.parent || p.alpha === 0) {
if (p && typeof p.destroy === "function" && p.parent) {
p.destroy();
}
projectiles.splice(i, 1);
}
}
// Update monsters and clean up destroyed ones
for (var i = monsters.length - 1; i >= 0; i--) {
var m = monsters[i];
if (m && typeof m.update === "function") {
m.update();
}
if (!m || m.destroyed && (!m.parent || m.alpha === 0)) {
if (m && typeof m.destroy === "function" && m.parent) {
m.destroy();
}
monsters.splice(i, 1);
}
}
// Defensive: Full cleanup for any orphaned/destroyed objects (extra safety, every 120 frames)
if (LK.ticks % 120 === 0) {
for (var i = notes.length - 1; i >= 0; i--) {
var note = notes[i];
if (!note || !note.parent || note.alpha === 0) {
if (note && typeof note.destroy === "function" && note.parent) {
note.destroy();
}
notes.splice(i, 1);
}
}
for (var i = monsters.length - 1; i >= 0; i--) {
var m = monsters[i];
if (!m || !m.parent || m.alpha === 0) {
if (m && typeof m.destroy === "function" && m.parent) {
m.destroy();
}
monsters.splice(i, 1);
}
}
for (var i = projectiles.length - 1; i >= 0; i--) {
var p = projectiles[i];
if (!p || !p.parent || p.alpha === 0) {
if (p && typeof p.destroy === "function" && p.parent) {
p.destroy();
}
projectiles.splice(i, 1);
}
}
}
// --- Monster attack logic ---
// Player health bar and state
if (typeof playerMaxHealth === "undefined") {
var playerMaxHealth = 5;
}
if (typeof playerHealth === "undefined") {
var playerHealth = playerMaxHealth;
}
if (typeof healthBar === "undefined") {
var healthBar = null;
}
// Health bar setup (if not already)
if (!healthBar) {
healthBar = new Container();
healthBar.segments = [];
// Heart icon size and spacing (make hearts bigger and move higher)
var heartSize = 90;
var gap = 24;
var barWidth = playerMaxHealth * heartSize + (playerMaxHealth - 1) * gap;
for (var h = 0; h < playerMaxHealth; h++) {
// Use heart1-heart5 assets for each health segment
var heartAssetId = 'heart' + (h + 1);
var seg = LK.getAsset(heartAssetId, {
anchorX: 0.5,
anchorY: 0.5,
width: heartSize,
height: heartSize,
x: h * (heartSize + gap),
y: 0
});
healthBar.addChild(seg);
healthBar.segments.push(seg);
}
// Move health bar higher above the hero and center
healthBar.x = hero.x - barWidth / 2 + heartSize / 2;
healthBar.y = hero.y - 220;
game.addChild(healthBar);
healthBar.update = function () {
// Immediately update segment alpha for lost health
for (var s = 0; s < healthBar.segments.length; s++) {
var seg = healthBar.segments[s];
var shouldBeVisible = s < playerHealth;
if (shouldBeVisible && seg.alpha < 1) {
seg.alpha = 1;
} else if (!shouldBeVisible && seg.alpha > 0) {
seg.alpha = 0;
}
}
};
healthBar.reset = function () {
playerHealth = playerMaxHealth;
for (var s = 0; s < healthBar.segments.length; s++) {
healthBar.segments[s].alpha = 1;
}
if (typeof healthBar.update === "function") {
healthBar.update();
}
if (typeof healthBar.update === "function") {
healthBar.update();
}
};
}
// Update health bar position to follow hero
if (healthBar && healthBar.segments && healthBar.segments.length > 0) {
var heartSize = healthBar.segments[0].width;
var gap = healthBar.segments.length > 1 ? healthBar.segments[1].x - healthBar.segments[0].x - heartSize : 0;
var barWidth = playerMaxHealth * heartSize + (playerMaxHealth - 1) * gap;
healthBar.x = hero.x - barWidth / 2 + heartSize / 2;
healthBar.y = hero.y - 220;
}
if (typeof healthBar.update === "function") {
healthBar.update();
}
// --- Monster attack logic ---
var ATTACK_RANGE = 120;
var DAMAGE_INTERVAL = 180; // 3 seconds at 60fps
if (typeof gameOverTriggered === "undefined") {
var gameOverTriggered = false;
}
if (typeof playerMaxHealth === "undefined") {
var playerMaxHealth = 5;
}
if (typeof playerHealth === "undefined") {
var playerHealth = playerMaxHealth;
}
for (var i = monsters.length - 1; i >= 0; i--) {
var m = monsters[i];
if (m.destroyed) {
continue;
}
// Calculate distance to hero
var dx = m.x - hero.x;
var dy = m.y - hero.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// If in attack range, stop moving and attack
if (dist <= ATTACK_RANGE) {
// Stop monster movement
m.dirX = 0;
m.dirY = 0;
m.speed = 0;
// Place monster at edge of attack range (don't overlap hero)
var angle = Math.atan2(dy, dx);
m.x = hero.x + Math.cos(angle) * ATTACK_RANGE;
m.y = hero.y + Math.sin(angle) * ATTACK_RANGE;
// Each monster tracks its own attack timer (in frames)
if (typeof m.attackTimer === "undefined") {
m.attackTimer = 0;
}
// Track if monster was previously in range
if (typeof m.lastInAttackRange === "undefined") {
m.lastInAttackRange = false;
}
// If just entered attack range, reset timer so damage doesn't happen instantly
if (!m.lastInAttackRange) {
m.attackTimer = 0;
}
m.lastInAttackRange = true;
// --- BITE ANIMATION & DAMAGE ---
// Only increment timer and deal damage if game is not over
if (!gameOverTriggered && playerHealth > 0) {
m.attackTimer++;
// Monster bites every DAMAGE_INTERVAL frames (3 seconds)
if (m.attackTimer >= DAMAGE_INTERVAL) {
m.attackTimer = 0;
if (playerHealth > 0) {
// Animate monster "bite" (scale up and back quickly)
tween(m, {
scaleX: 1.18,
scaleY: 0.82
}, {
duration: 90,
yoyo: true,
repeat: 1
});
// Remove the highest-numbered visible heart asset
var heartToRemove = -1;
for (var h = healthBar.segments.length - 1; h >= 0; h--) {
if (healthBar.segments[h].alpha > 0) {
heartToRemove = h;
break;
}
}
if (heartToRemove !== -1) {
// Visually remove the heart asset
healthBar.segments[heartToRemove].alpha = 0;
// Flash hero red
LK.effects.flashObject(hero, 0xff0000, 300);
// Flash health bar segment (the one just lost)
tween(healthBar.segments[heartToRemove], {
tint: 0xff4444
}, {
duration: 120,
onFinish: function onFinish() {
tween(this, {
tint: 0x2ecc71
}, {
duration: 120
});
}.bind(healthBar.segments[heartToRemove])
});
// Decrement playerHealth
playerHealth--;
if (typeof healthBar.update === "function") {
healthBar.update();
}
// If all hearts are gone, trigger game over
var allHeartsGone = true;
for (var h = 0; h < healthBar.segments.length; h++) {
if (healthBar.segments[h].alpha > 0) {
allHeartsGone = false;
break;
}
}
if (allHeartsGone && !gameOverTriggered) {
gameOverTriggered = true;
if (healthBar && typeof healthBar.update === "function") {
healthBar.update();
}
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
// Reset health bar and health for next game
if (healthBar) {
healthBar.reset();
}
playerHealth = playerMaxHealth;
// Clean up all notes, monsters, and projectiles to prevent buildup
for (var i = notes.length - 1; i >= 0; i--) {
if (notes[i] && typeof notes[i].destroy === "function" && notes[i].parent) {
notes[i].destroy();
}
notes.splice(i, 1);
}
for (var i = monsters.length - 1; i >= 0; i--) {
if (monsters[i] && typeof monsters[i].destroy === "function" && monsters[i].parent) {
monsters[i].destroy();
}
monsters.splice(i, 1);
}
for (var i = projectiles.length - 1; i >= 0; i--) {
if (projectiles[i] && typeof projectiles[i].destroy === "function" && projectiles[i].parent) {
projectiles[i].destroy();
}
projectiles.splice(i, 1);
}
// Defensive: Reset arrays to empty to ensure no lingering references
notes = [];
monsters = [];
projectiles = [];
}
}
}
}
}
} else {
// Not in attack range, monster moves as normal
if (typeof m.dirX !== "number" || typeof m.dirY !== "number" || m.speed === 0) {
// Recalculate direction and restore speed, but cap it
var dx2 = hero.x - m.x;
var dy2 = hero.y - m.y;
var dist2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
m.dirX = dx2 / dist2;
m.dirY = dy2 / dist2;
}
// Always set monster speed to match note speed (capped)
var MONSTER_MAX_SPEED = 8;
m.speed = Math.min(MONSTER_MAX_SPEED, NOTE_SPEED);
// Reset attack timer and in-range flag if monster leaves range
if (typeof m.attackTimer !== "undefined") {
m.attackTimer = 0;
}
m.lastInAttackRange = false;
}
// Clean up timer if monster destroyed
if (m.destroyed && typeof m.attackTimer !== "undefined") {
delete m.attackTimer;
delete m.lastInAttackRange;
}
}
// Game over if health reaches 0
if (playerHealth <= 0 && !gameOverTriggered) {
gameOverTriggered = true;
if (healthBar && typeof healthBar.update === "function") {
healthBar.update();
}
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
// Stop all monster attacks and player actions
// Reset health bar and health for next game
if (healthBar) {
healthBar.reset();
}
playerHealth = playerMaxHealth;
// Clean up all notes, monsters, and projectiles to prevent buildup
for (var i = notes.length - 1; i >= 0; i--) {
if (notes[i] && typeof notes[i].destroy === "function" && notes[i].parent) {
notes[i].destroy();
}
notes.splice(i, 1);
}
for (var i = monsters.length - 1; i >= 0; i--) {
if (monsters[i] && typeof monsters[i].destroy === "function" && monsters[i].parent) {
monsters[i].destroy();
}
monsters.splice(i, 1);
}
for (var i = projectiles.length - 1; i >= 0; i--) {
if (projectiles[i] && typeof projectiles[i].destroy === "function" && projectiles[i].parent) {
projectiles[i].destroy();
}
projectiles.splice(i, 1);
}
// Defensive: Reset arrays to empty to ensure no lingering references
notes = [];
monsters = [];
projectiles = [];
}
// Update weapon position to follow hero
if (typeof updateHeroPositionForMusic === "function") {
updateHeroPositionForMusic();
}
if (typeof game.updateWeapon === "function") {
game.updateWeapon();
}
};
// --- Game Over Check ---
function checkGameOver() {
if (missCount >= MAX_MISSES) {
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
// Reset health bar and health for next game
if (healthBar && typeof healthBar.update === "function") {
healthBar.update();
}
if (healthBar) {
healthBar.reset();
}
playerHealth = playerMaxHealth;
// Clean up all notes, monsters, and projectiles to prevent buildup
for (var i = notes.length - 1; i >= 0; i--) {
if (notes[i] && typeof notes[i].destroy === "function" && notes[i].parent) {
notes[i].destroy();
}
notes.splice(i, 1);
}
for (var i = monsters.length - 1; i >= 0; i--) {
if (monsters[i] && typeof monsters[i].destroy === "function" && monsters[i].parent) {
monsters[i].destroy();
}
monsters.splice(i, 1);
}
for (var i = projectiles.length - 1; i >= 0; i--) {
if (projectiles[i] && typeof projectiles[i].destroy === "function" && projectiles[i].parent) {
projectiles[i].destroy();
}
projectiles.splice(i, 1);
}
// Defensive: Reset arrays to empty to ensure no lingering references
notes = [];
monsters = [];
projectiles = [];
}
}
shine yellow color music note. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
robotic monster. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
blue shiny music note. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat