/**** * 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 monsterdead3 sound for Majestic Mountains monsters LK.getSound('monsterdead3').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 + self.noteSize / 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 ****/ // Create splash background asset (full screen, custom color or image) // --- SPLASH SCREEN LOGIC --- // Funky Groove top background (separate from splash and other screens) 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 = 210; var NOTE_BASE_SPEED = 7; // Start slower var NOTE_MAX_SPEED = 18; // Cap speed (increased for more challenge) 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; // --- Note density (how many notes at once) --- 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; } // --- Asset Initialization --- // --- Game State --- var pianoKeys = []; var notes = []; var monsters = []; var projectiles = []; var missCount = 0; var score = 0; var lastNoteSpawn = 0; var lastMonsterSpawn = 0; // --- 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.noteSize / 2; var noteRight = note.x + note.noteSize / 2; var noteTop = note.y - note.noteSize / 2; var noteBottom = note.y + note.noteSize / 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.noteSize / 2; var noteBottom = note.y + note.noteSize / 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.noteSize / 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 --- // Difficulty variables if (typeof noteDensity === "undefined") noteDensity = 1; if (typeof noteScatter === "undefined") noteScatter = 0; if (typeof noteScatterMax === "undefined") noteScatterMax = 120; // max px scatter if (typeof noteScatterStep === "undefined") noteScatterStep = 12; // how much scatter increases per interval // Gradually increase note density and scatter, but very slowly if (LK.ticks % (DENSITY_INCREASE_INTERVAL * 2) === 0 && LK.ticks > 0) { // Only increase density if not at max, and only after a long time if (noteDensity < MAX_NOTE_DENSITY) { noteDensity += 1; } else if (noteScatter < noteScatterMax) { noteScatter = Math.min(noteScatterMax, noteScatter + noteScatterStep); } } // Spawn notes and corresponding monsters (enemies) in sync if (LK.ticks - lastNoteSpawn >= NOTE_SPAWN_INTERVAL) { lastNoteSpawn = LK.ticks; // Decide how many notes to spawn: mostly 1, but as density increases, sometimes more var spawnCount = 1; if (noteDensity > 1) { // As density increases, increase chance to spawn more than 1 note var chance = Math.random(); if (chance < 0.25 * (noteDensity - 1)) spawnCount = 2; if (noteDensity > 2 && chance < 0.10 * (noteDensity - 1)) spawnCount = 3; if (noteDensity > 3 && chance < 0.04 * (noteDensity - 1)) spawnCount = 4; spawnCount = Math.min(noteDensity, spawnCount); } // Pick random keys to spawn notes on, 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 < spawnCount; 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; // Add scatter as difficulty increases (notes start at slightly different Y) var scatterY = 0; if (noteScatter > 0) { scatterY = Math.floor((Math.random() - 0.5) * noteScatter); } note.y = 2732 / 2 - NOTE_SIZE / 2 + scatterY; 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); } } } // (Hero health and monster attack logic removed) // 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(); // 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 monsterdead3 sound for Majestic Mountains monsters
LK.getSound('monsterdead3').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 + self.noteSize / 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
****/
// Create splash background asset (full screen, custom color or image)
// --- SPLASH SCREEN LOGIC ---
// Funky Groove top background (separate from splash and other screens)
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 = 210;
var NOTE_BASE_SPEED = 7; // Start slower
var NOTE_MAX_SPEED = 18; // Cap speed (increased for more challenge)
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;
// --- Note density (how many notes at once) ---
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;
}
// --- Asset Initialization ---
// --- Game State ---
var pianoKeys = [];
var notes = [];
var monsters = [];
var projectiles = [];
var missCount = 0;
var score = 0;
var lastNoteSpawn = 0;
var lastMonsterSpawn = 0;
// --- 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.noteSize / 2;
var noteRight = note.x + note.noteSize / 2;
var noteTop = note.y - note.noteSize / 2;
var noteBottom = note.y + note.noteSize / 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.noteSize / 2;
var noteBottom = note.y + note.noteSize / 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.noteSize / 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 ---
// Difficulty variables
if (typeof noteDensity === "undefined") noteDensity = 1;
if (typeof noteScatter === "undefined") noteScatter = 0;
if (typeof noteScatterMax === "undefined") noteScatterMax = 120; // max px scatter
if (typeof noteScatterStep === "undefined") noteScatterStep = 12; // how much scatter increases per interval
// Gradually increase note density and scatter, but very slowly
if (LK.ticks % (DENSITY_INCREASE_INTERVAL * 2) === 0 && LK.ticks > 0) {
// Only increase density if not at max, and only after a long time
if (noteDensity < MAX_NOTE_DENSITY) {
noteDensity += 1;
} else if (noteScatter < noteScatterMax) {
noteScatter = Math.min(noteScatterMax, noteScatter + noteScatterStep);
}
}
// Spawn notes and corresponding monsters (enemies) in sync
if (LK.ticks - lastNoteSpawn >= NOTE_SPAWN_INTERVAL) {
lastNoteSpawn = LK.ticks;
// Decide how many notes to spawn: mostly 1, but as density increases, sometimes more
var spawnCount = 1;
if (noteDensity > 1) {
// As density increases, increase chance to spawn more than 1 note
var chance = Math.random();
if (chance < 0.25 * (noteDensity - 1)) spawnCount = 2;
if (noteDensity > 2 && chance < 0.10 * (noteDensity - 1)) spawnCount = 3;
if (noteDensity > 3 && chance < 0.04 * (noteDensity - 1)) spawnCount = 4;
spawnCount = Math.min(noteDensity, spawnCount);
}
// Pick random keys to spawn notes on, 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 < spawnCount; 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;
// Add scatter as difficulty increases (notes start at slightly different Y)
var scatterY = 0;
if (noteScatter > 0) {
scatterY = Math.floor((Math.random() - 0.5) * noteScatter);
}
note.y = 2732 / 2 - NOTE_SIZE / 2 + scatterY;
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);
}
}
}
// (Hero health and monster attack logic removed)
// 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();
// 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