/****
* 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