/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { highScore: 0 }); /**** * Classes ****/ // DrumNote class for notes on drums var DrumNote = Container.expand(function () { var self = Container.call(this); self.spawnTime = 0; self.isLeft = true; self.hit = false; self.lifeTime = 300; // 5 seconds at 60fps var noteGraphics = self.attachAsset('drumNote', { anchorX: 0.5, anchorY: 0.5 }); // Add a timer text above the note self.timerText = new Text2('3.0', { size: 60, fill: 0xFF2222, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); self.timerText.anchor.set(0.5, 1); // center, bottom self.timerText.x = 0; self.timerText.y = -noteGraphics.height / 2 - 10; self.addChild(self.timerText); self.update = function () { // Update timer text if not hit if (!self.hit) { var elapsed = songTicks - self.spawnTime; var remain = Math.max(0, (self.lifeTime - elapsed) / 60); // Show with 1 decimal, clamp to 0 self.timerText.setText(remain.toFixed(1)); } if (!self.hit && songTicks - self.spawnTime > self.lifeTime) { // Note expired - game over LK.showGameOver(); } }; self.onHit = function () { if (self.hit) return; self.hit = true; // Explosion effect var particleCount = 6; var colors = [0xFFD700, 0xFFAA00, 0xFFFF00]; for (var p = 0; p < particleCount; p++) { var particle = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, x: self.x, y: self.y, width: 30 + Math.random() * 40, height: 30 + Math.random() * 40 }); particle.tint = colors[Math.floor(Math.random() * colors.length)]; if (self.parent) self.parent.addChild(particle); var angle = Math.PI * 2 * p / particleCount; var distance = 100 + Math.random() * 100; var targetX = self.x + Math.cos(angle) * distance; var targetY = self.y + Math.sin(angle) * distance; tween(particle, { x: targetX, y: targetY, alpha: 0, scaleX: 0.1, scaleY: 0.1 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { if (particle.parent) particle.destroy(); } }); } tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); } }); }; return self; }); // Note class for falling notes var Note = Container.expand(function () { var self = Container.call(this); // Lane index (0-3) self.lane = 0; self.hit = false; self.missed = false; self.speed = 0; // pixels per tick self.time = 0; // time (in ticks) when note should reach the target self.spawned = false; // Attach correct note asset based on lane self.setLane = function (laneIdx) { self.lane = laneIdx; var assetId = 'note' + (laneIdx + 1); var noteAsset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, width: NOTE_WIDTH, height: NOTE_HEIGHT }); }; // Called every tick self.update = function () { if (!self.spawned) return; self.y += self.speed; // Follow lane container movement if (self.laneContainer) { self.x = self.laneContainer.x; } }; // Called when note is hit self.onHit = function () { if (self.hit || self.missed) return; self.hit = true; // Create explosion effect with particles var particleCount = 8; var colors = [0xFFD700, 0xFF4444, 0x44FF44, 0x4444FF, 0xFF44FF, 0x44FFFF]; for (var p = 0; p < particleCount; p++) { var particle = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, x: self.x, y: self.y, width: 20 + Math.random() * 30, height: 20 + Math.random() * 30 }); particle.tint = colors[Math.floor(Math.random() * colors.length)]; if (self.parent) self.parent.addChild(particle); // Random direction for explosion var angle = Math.PI * 2 * p / particleCount + (Math.random() - 0.5) * 0.5; var distance = 80 + Math.random() * 120; var targetX = self.x + Math.cos(angle) * distance; var targetY = self.y + Math.sin(angle) * distance; // Animate particle explosion tween(particle, { x: targetX, y: targetY, alpha: 0, scaleX: 0.2, scaleY: 0.2 }, { duration: 300 + Math.random() * 200, easing: tween.easeOut, onFinish: function onFinish() { if (particle.parent) particle.destroy(); } }); } // Animate note tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); } }); }; // Called when note is missed self.onMiss = function () { if (self.hit || self.missed) return; self.missed = true; LK.getSound('miss').play({ fade: { start: 1, end: 0, duration: 1000 } }); tween(self, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { self.destroy(); } }); }; return self; }); // People class for animated people at lane edges var People = Container.expand(function () { var self = Container.call(this); self.isJumping = false; self.baseY = 0; self.personType = Math.floor(Math.random() * 5); // Random type 0-4 // Create different visual styles based on person type var body; switch (self.personType) { case 0: // Type 1: Tall blue person body = self.attachAsset('centerCircle', { anchorX: 0.5, anchorY: 1, width: 110, height: 240, y: 0, tint: 0x3366FF }); break; case 1: // Type 2: Wide green person body = self.attachAsset('centerCircle', { anchorX: 0.5, anchorY: 1, width: 150, height: 200, y: 0, tint: 0x33FF66 }); break; case 2: // Type 3: Medium red person body = self.attachAsset('centerCircle', { anchorX: 0.5, anchorY: 1, width: 125, height: 220, y: 0, tint: 0xFF3366 }); break; case 3: // Type 4: Small yellow person body = self.attachAsset('centerCircle', { anchorX: 0.5, anchorY: 1, width: 100, height: 170, y: 0, tint: 0xFFFF33 }); break; case 4: // Type 5: Average purple person body = self.attachAsset('centerCircle', { anchorX: 0.5, anchorY: 1, width: 120, height: 210, y: 0, tint: 0xCC33FF }); break; } // Arms removed: only body remains // Save baseY for jump animation self.baseY = self.y; // Animate jump self.jump = function () { if (self.isJumping) return; self.isJumping = true; var jumpHeight = 120; var jumpDuration = 180; var originalY = self.y; // --- Screen shake effect when people jump --- if (typeof game !== "undefined" && typeof tween !== "undefined") { // Only shake if not already shaking if (!game._isShaking) { game._isShaking = true; var originalGameX = game.x || 0; var originalGameY = game.y || 0; // Determine if all people are jumping at once for stronger shake var allJumping = false; if (typeof peopleLeft !== "undefined" && typeof peopleRight !== "undefined") { var allLeftJumping = true; for (var i = 0; i < peopleLeft.length; i++) { if (!peopleLeft[i].isJumping) { allLeftJumping = false; break; } } var allRightJumping = true; for (var i = 0; i < peopleRight.length; i++) { if (!peopleRight[i].isJumping) { allRightJumping = false; break; } } allJumping = allLeftJumping && allRightJumping; } var shakeAmount = allJumping ? 120 : 40; // much stronger shake if all people are jumping var shakeDuration = allJumping ? 260 : 180; // Shake out tween(game, { x: originalGameX + (Math.random() - 0.5) * shakeAmount, y: originalGameY + (Math.random() - 0.5) * shakeAmount }, { duration: shakeDuration, onFinish: function onFinish() { // Shake back tween(game, { x: originalGameX, y: originalGameY }, { duration: shakeDuration, onFinish: function onFinish() { game._isShaking = false; } }); } }); } } tween(self, { y: originalY - jumpHeight }, { duration: jumpDuration, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { y: originalY }, { duration: jumpDuration, easing: tween.easeIn, onFinish: function onFinish() { self.isJumping = false; } }); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181818 }); /**** * Game Code ****/ // Notes are destroyed by tapping them directly before they reach the white target area at the bottom // 4 note lanes, each with a different color for clarity // --- UI Elements --- var NUM_LANES = 4; var LANE_WIDTH = 230; var LANE_SPACING = 40; var NOTE_WIDTH = 340; // Wider notes var NOTE_HEIGHT = 340; // Taller notes (stretches upward) var TARGET_HEIGHT = 60; // White target area is now just a visual "danger" zone, reduced height for smaller targets var LANE_HEIGHT = 2200; var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; var LANE_TOTAL_WIDTH = NUM_LANES * LANE_WIDTH + (NUM_LANES - 1) * LANE_SPACING; var LANE_START_X = (GAME_WIDTH - LANE_TOTAL_WIDTH) / 2 + LANE_WIDTH / 2; var TARGET_Y = GAME_HEIGHT - 420; // Target zone Y position (moved higher) // --- Game State --- var notes = []; // All active notes var noteIndex = 0; // Index of next note to spawn var songTicks = 0; // Ticks since song start var score = 0; var combo = 0; var maxCombo = 0; var misses = 0; var maxMisses = 10; var songEnded = false; var songStarted = false; var lastTick = 0; // Note speed multiplier for gradual speed up var noteSpeedMultiplier = 1.0; // Lane color flashing state var laneColorFlashActive = false; var laneColorFlashStartTime = 0; var laneColorFlashTriggered = false; // Drum variables var leftDrum = null; var rightDrum = null; var drumNotes = []; var drumsActive = false; var nextDrumNoteTime = 0; // --- UI Elements --- var scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // High Score UI // --- Per-mode high score keys --- function getCurrentModeKey() { if (typeof extremeMode !== "undefined" && extremeMode) return "extreme"; if (typeof hardMode !== "undefined" && hardMode) return "hard"; return "easy"; } function getHighScoreKey() { return "highScore_" + getCurrentModeKey(); } function getHighScore() { var key = getHighScoreKey(); return typeof storage[key] !== "undefined" ? storage[key] : 0; } function setHighScore(val) { var key = getHighScoreKey(); storage[key] = val; } var highScore = getHighScore(); var highScoreTxt = new Text2('High Score: ' + highScore, { size: 60, fill: 0xFFD700 }); // Move high score to top-right, with some margin from the edge highScoreTxt.anchor.set(1, 0); // right aligned, top highScoreTxt.x = LK.gui.width - 40; // 40px margin from right highScoreTxt.y = 30; // 30px from top LK.gui.top.addChild(highScoreTxt); var comboTxt = new Text2('', { size: 70, fill: 0xFFE066 }); comboTxt.anchor.set(0.5, 0); LK.gui.top.addChild(comboTxt); comboTxt.y = 130; // --- Popularity Display --- var popularityTxt = new Text2('Popularity: 0%', { size: 60, fill: 0xFFD700 }); popularityTxt.anchor.set(1, 1); // right aligned, bottom popularityTxt.x = GAME_WIDTH - 40; // 40px margin from right popularityTxt.y = GAME_HEIGHT - 40; // 40px margin from bottom game.addChild(popularityTxt); // --- High Score Display in Bottom Left --- var bottomHighScoreTxt = new Text2('High Score: ' + getHighScore(), { size: 60, fill: 0xFFD700 }); bottomHighScoreTxt.anchor.set(0, 1); // left aligned, bottom bottomHighScoreTxt.x = 40; // 40px margin from left bottomHighScoreTxt.y = GAME_HEIGHT - 40; // 40px margin from bottom game.addChild(bottomHighScoreTxt); // Function to update popularity based on total people count function updatePopularity() { var totalPeople = peopleLeft.length + peopleRight.length; var popularity = Math.min(totalPeople * 10, 100); // Each person is 10%, cap at 100% popularityTxt.setText('Popularity: ' + popularity + '%'); } // --- Mode Labels --- var hardMode = typeof hardMode !== "undefined" ? hardMode : false; var extremeMode = typeof extremeMode !== "undefined" ? extremeMode : false; var hardModeLabel = null; var extremeModeLabel = null; if (extremeMode) { extremeModeLabel = new Text2('EXTREME MODE', { size: 80, fill: 0xFF00FF, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); extremeModeLabel.anchor.set(0.5, 0); extremeModeLabel.x = GAME_WIDTH / 2; extremeModeLabel.y = 40; LK.gui.top.addChild(extremeModeLabel); } else if (hardMode) { hardModeLabel = new Text2('HARD MODE', { size: 80, fill: 0xFF2222, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); hardModeLabel.anchor.set(0.5, 0); hardModeLabel.x = GAME_WIDTH / 2; hardModeLabel.y = 40; LK.gui.top.addChild(hardModeLabel); } // Removed red 'misses' text from the top GUI. Misses are now only shown in white below each target. // --- Add Background --- var background = LK.getAsset('background', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: GAME_WIDTH, height: GAME_HEIGHT }); game.addChild(background); // --- Lanes and Targets --- var lanes = []; var targets = []; var peopleLeft = []; var peopleRight = []; var currentPeoplePerSide = 0; // Track current number of people per side var lastScoreMilestone = 0; // Track last score milestone for adding people var targetMissTexts = []; // Miss counters for each target var laneContainers = []; // Containers to group lane elements together for (var i = 0; i < NUM_LANES; i++) { // Create a container for each lane to group all elements var laneContainer = new Container(); var laneX = LANE_START_X + i * (LANE_WIDTH + LANE_SPACING); laneContainer.x = laneX; laneContainer.baseX = laneX; // Store base position for movement game.addChild(laneContainer); laneContainers.push(laneContainer); // Lane background var lane = LK.getAsset('lane', { anchorX: 0.5, anchorY: 0, x: 0, //{2r} // Relative to container y: 0, width: LANE_WIDTH, height: LANE_HEIGHT }); laneContainer.addChild(lane); lanes.push(lane); // Visually improved target: larger, more distinct, with a colored border effect var target = LK.getAsset('target', { anchorX: 0.5, anchorY: 0.5, x: 0, //{2w} // Relative to container y: TARGET_Y + 40, //{1R} // Move target further back (higher y = lower on screen) width: LANE_WIDTH + 40, // Make target slightly wider for better visuals height: TARGET_HEIGHT + 40 // Make target slightly taller for better visuals }); laneContainer.addChild(target); // Add a second, inner target for a "bullseye" effect var innerTarget = LK.getAsset('target', { anchorX: 0.5, anchorY: 0.5, x: 0, //{2B} // Relative to container y: TARGET_Y + 40, //{1W} // Move inner target further back as well width: LANE_WIDTH - 30, height: TARGET_HEIGHT - 20 }); laneContainer.addChild(innerTarget); // (removed: misses counter above target, now shown on the target itself) // Add a misses counter directly ON the target (replaces label text with misses count) var targetOnText = new Text2('0', { size: 54, fill: 0x3399FF // Blue (not black) }); targetOnText.anchor.set(0.5, 0.5); targetOnText.x = 0; // Relative to container targetOnText.y = TARGET_Y + 40; laneContainer.addChild(targetOnText); // Store this as the main misses counter for this target (for updating) targetMissTexts[i] = targetOnText; // Store both for later effects targets.push({ outer: target, inner: innerTarget }); } // Place people only outside the lanes, never over the road/lane area // --- Randomized, non-overlapping, not-in-center people placement --- peopleLeft = []; peopleRight = []; var NUM_PEOPLE_PER_EDGE = 0; // Start with no people var PERSON_RADIUS = 90; // Half of body width, for spacing var OUTER_X_OFFSET = 120; // How far outside the lane edge to place people // Allow people to go all the way to the screen edge, not just near the lanes var FAR_LEFT_X_MIN = 60; var FAR_LEFT_X_MAX = getLaneX(0) - LANE_WIDTH / 2 - OUTER_X_OFFSET - 10; var FAR_RIGHT_X_MIN = getLaneX(NUM_LANES - 1) + LANE_WIDTH / 2 + OUTER_X_OFFSET + 10; var FAR_RIGHT_X_MAX = GAME_WIDTH - 60; var Y_MIN = 200; // Don't go too high var Y_MAX = TARGET_Y - 60; // Don't go too low var CENTER_EXCLUSION_X = GAME_WIDTH / 2 - 320; // Exclude a wide center band var CENTER_EXCLUSION_WIDTH = 640; // Center band width to avoid var MAX_ATTEMPTS = 40; // Max tries to find a non-overlapping spot function isFarEnough(x, y, arr) { for (var i = 0; i < arr.length; i++) { var dx = x - arr[i].x; var dy = y - arr[i].y; if (Math.sqrt(dx * dx + dy * dy) < PERSON_RADIUS * 2.1) return false; } return true; } function isNotCenter(x) { return !(x > CENTER_EXCLUSION_X && x < CENTER_EXCLUSION_X + CENTER_EXCLUSION_WIDTH); } // Check if position is not in drum area function isNotDrumArea(x, y) { var drumRadius = 220; var leftDrumX = 200, leftDrumY = GAME_HEIGHT / 2; var rightDrumX = GAME_WIDTH - 200, rightDrumY = GAME_HEIGHT / 2; var distLeft = Math.sqrt((x - leftDrumX) * (x - leftDrumX) + (y - leftDrumY) * (y - leftDrumY)); var distRight = Math.sqrt((x - rightDrumX) * (x - rightDrumX) + (y - rightDrumY) * (y - rightDrumY)); return distLeft > drumRadius && distRight > drumRadius; } // Update popularity after recreating people updatePopularity(); // Update initial popularity updatePopularity(); // --- Song Data (Random Infinite Notes) --- // Each note: {lane: 0-3, time: tick when note should reach target} // We'll generate notes on the fly, at random lanes and random intervals var bpm = 60; var ticksPerBeat = 60 * 60 / bpm; // 60fps var NOTE_TRAVEL_TICKS = 180; // 3 seconds at 60fps // Infinite random note generator state var nextNoteTick = 60; // When the next note should appear (in songTicks) function getRandomLane() { return Math.floor(Math.random() * NUM_LANES); } function getRandomInterval() { // In extreme mode, spawn notes extremely frequently (interval is 0.12x to 0.35x of ticksPerBeat) if (typeof extremeMode !== "undefined" && extremeMode) { return Math.floor(ticksPerBeat * (0.12 + Math.random() * 0.23)); } // In hard mode, spawn notes more frequently (interval is 0.3x to 1.0x of ticksPerBeat) if (typeof hardMode !== "undefined" && hardMode) { return Math.floor(ticksPerBeat * (0.3 + Math.random() * 0.7)); } // Random interval between notes: 0.5x to 1.5x of ticksPerBeat return Math.floor(ticksPerBeat * (0.5 + Math.random())); } // --- Helper Functions --- function getLaneX(laneIdx) { if (typeof laneContainers !== "undefined" && laneContainers[laneIdx]) { return laneContainers[laneIdx].x; } return LANE_START_X + laneIdx * (LANE_WIDTH + LANE_SPACING); } // Start continuous random lane movement function startLaneMovement() { for (var i = 0; i < laneContainers.length; i++) { moveLaneRandomly(laneContainers[i], i); } } // Move a single lane to a random position function moveLaneRandomly(laneContainer, index) { if (!laneContainer || songEnded) return; // Random offset from base position (-80 to +80 pixels) var randomOffset = (Math.random() - 0.5) * 160; var targetX = laneContainer.baseX + randomOffset; // Random duration (3-5 seconds) var duration = 3000 + Math.random() * 2000; tween(laneContainer, { x: targetX }, { duration: duration, easing: tween.easeInOut, onFinish: function onFinish() { // Continue moving after reaching target moveLaneRandomly(laneContainer, index); } }); } // --- Game Logic --- // Start song/music function startSong() { if (songStarted) return; songStarted = true; LK.playMusic('song1'); songTicks = 0; noteIndex = 0; score = 0; combo = 0; maxCombo = 0; misses = 0; songEnded = false; scoreTxt.setText('0'); comboTxt.setText(''); highScore = getHighScore(); highScoreTxt.setText('High Score: ' + highScore); if (typeof bottomHighScoreTxt !== "undefined") { bottomHighScoreTxt.setText('High Score: ' + highScore); } notes.length = 0; // Reset per-target misses counters (on the target itself) if (typeof targets !== "undefined" && typeof targetMissTexts !== "undefined") { for (var i = 0; i < targetMissTexts.length; i++) { if (targetMissTexts[i]) { targetMissTexts[i].setText('0'); targetMissTexts[i].setStyle({ fill: 0x3399FF }); } if (targets[i]) targets[i].missCount = 0; } } // Reset nextNoteTick and noteSpeedMultiplier to ensure smooth start nextNoteTick = 60; // Reset lane color flash state laneColorFlashActive = false; laneColorFlashStartTime = 0; laneColorFlashTriggered = false; // Reset drum state drumsActive = true; drumNotes.length = 0; if (leftDrum) { leftDrum.destroy(); leftDrum = null; } if (rightDrum) { rightDrum.destroy(); rightDrum = null; } // Remove peopleLeft from game if (typeof peopleLeft !== "undefined") { for (var i = 0; i < peopleLeft.length; i++) { if (peopleLeft[i] && typeof peopleLeft[i].destroy === "function") { peopleLeft[i].destroy(); } } peopleLeft.length = 0; } // Remove peopleRight from game if (typeof peopleRight !== "undefined") { for (var i = 0; i < peopleRight.length; i++) { if (peopleRight[i] && typeof peopleRight[i].destroy === "function") { peopleRight[i].destroy(); } } peopleRight.length = 0; } // Re-create peopleLeft and peopleRight, but keep them away from drum asset positions peopleLeft = []; peopleRight = []; currentPeoplePerSide = 0; // Reset to 0 people per side lastScoreMilestone = 0; // Reset score milestone var NUM_PEOPLE_PER_EDGE = 0; // Start with no people var PERSON_RADIUS = 90; var OUTER_X_OFFSET = 120; var FAR_LEFT_X_MIN = 60; var FAR_LEFT_X_MAX = getLaneX(0) - LANE_WIDTH / 2 - OUTER_X_OFFSET - 10; var FAR_RIGHT_X_MIN = getLaneX(NUM_LANES - 1) + LANE_WIDTH / 2 + OUTER_X_OFFSET + 10; var FAR_RIGHT_X_MAX = GAME_WIDTH - 60; var Y_MIN = 200; var Y_MAX = TARGET_Y - 60; var CENTER_EXCLUSION_X = GAME_WIDTH / 2 - 320; var CENTER_EXCLUSION_WIDTH = 640; var MAX_ATTEMPTS = 40; function isFarEnough(x, y, arr) { for (var i = 0; i < arr.length; i++) { var dx = x - arr[i].x; var dy = y - arr[i].y; if (Math.sqrt(dx * dx + dy * dy) < PERSON_RADIUS * 2.1) return false; } return true; } function isNotCenter(x) { return !(x > CENTER_EXCLUSION_X && x < CENTER_EXCLUSION_X + CENTER_EXCLUSION_WIDTH); } // No people are created at the start // Create left drum after people to ensure it appears in front leftDrum = LK.getAsset('drumV2', { anchorX: 0.5, anchorY: 0.5, x: 200, y: GAME_HEIGHT / 2 }); game.addChild(leftDrum); // Create right drum after people to ensure it appears in front rightDrum = LK.getAsset('drumV2', { anchorX: 0.5, anchorY: 0.5, x: GAME_WIDTH - 200, y: GAME_HEIGHT / 2 }); game.addChild(rightDrum); // Drums will be created after people to ensure they appear in front // Set initial next drum note time based on mode: easy=10s, hard=5s, extreme=3s if (typeof extremeMode !== "undefined" && extremeMode) { // 3 seconds interval (180 ticks) nextDrumNoteTime = songTicks + 180; } else if (typeof hardMode !== "undefined" && hardMode) { // 5 seconds interval (300 ticks) nextDrumNoteTime = songTicks + 300; } else { // Easy mode or default: 10 seconds interval (600 ticks) nextDrumNoteTime = songTicks + 600; } // Start lane movement animations startLaneMovement(); if (typeof extremeMode !== "undefined" && extremeMode) { noteSpeedMultiplier = 1.5; // Slower than before for extreme mode (spawn rate unchanged) // Remove any previous mode labels if (typeof hardModeLabel !== "undefined" && hardModeLabel && hardModeLabel.parent) { hardModeLabel.destroy(); } if (typeof extremeModeLabel !== "undefined" && extremeModeLabel && extremeModeLabel.parent) { extremeModeLabel.destroy(); } extremeModeLabel = new Text2('EXTREME MODE', { size: 80, fill: 0xFF00FF, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); extremeModeLabel.anchor.set(0.5, 0); extremeModeLabel.x = GAME_WIDTH / 2; extremeModeLabel.y = 40; LK.gui.top.addChild(extremeModeLabel); } else if (typeof hardMode !== "undefined" && hardMode) { noteSpeedMultiplier = 1.7; // Reduced speed for hard mode // Show Hard Mode label in gameplay if (typeof hardModeLabel !== "undefined" && hardModeLabel && hardModeLabel.parent) { hardModeLabel.destroy(); } if (typeof extremeModeLabel !== "undefined" && extremeModeLabel && extremeModeLabel.parent) { extremeModeLabel.destroy(); } hardModeLabel = new Text2('HARD MODE', { size: 80, fill: 0xFF2222, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); hardModeLabel.anchor.set(0.5, 0); hardModeLabel.x = GAME_WIDTH / 2; hardModeLabel.y = 40; LK.gui.top.addChild(hardModeLabel); } else if (typeof easyMode !== "undefined" && easyMode) { noteSpeedMultiplier = 1.0; // Show Easy Mode label in gameplay if (typeof hardModeLabel !== "undefined" && hardModeLabel && hardModeLabel.parent) { hardModeLabel.destroy(); } if (typeof extremeModeLabel !== "undefined" && extremeModeLabel && extremeModeLabel.parent) { extremeModeLabel.destroy(); } if (typeof easyModeLabel !== "undefined" && easyModeLabel && easyModeLabel.parent) { easyModeLabel.destroy(); } easyModeLabel = new Text2('EASY MODE', { size: 80, fill: 0x44FF44, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); easyModeLabel.anchor.set(0.5, 0); easyModeLabel.x = GAME_WIDTH / 2; easyModeLabel.y = 40; LK.gui.top.addChild(easyModeLabel); } else { noteSpeedMultiplier = 1.0; if (typeof hardModeLabel !== "undefined" && hardModeLabel && hardModeLabel.parent) { hardModeLabel.destroy(); } if (typeof extremeModeLabel !== "undefined" && extremeModeLabel && extremeModeLabel.parent) { extremeModeLabel.destroy(); } } } // End song/game function endSong(win) { if (songEnded) return; songEnded = true; LK.stopMusic(); // No win or game over, just stop music and mark as ended } // --- Input Handling --- game.down = function (x, y, obj) { if (menuActive) return; // Prevent gameplay input until menu is dismissed // Only allow input if song is running if (!songStarted || songEnded) return; // Check drum hits first if drums are active if (drumsActive) { // Check left drum if (leftDrum) { var leftDrumLeft = leftDrum.x - 150; var leftDrumRight = leftDrum.x + 150; var leftDrumTop = leftDrum.y - 150; var leftDrumBottom = leftDrum.y + 150; if (x >= leftDrumLeft && x <= leftDrumRight && y >= leftDrumTop && y <= leftDrumBottom) { // Hit left drum - check for drum notes for (var d = drumNotes.length - 1; d >= 0; d--) { var drumNote = drumNotes[d]; if (!drumNote.hit && drumNote.isLeft) { drumNote.onHit(); score += 200; scoreTxt.setText(score + ''); // Update high score if (score > highScore) { highScore = score; setHighScore(highScore); highScoreTxt.setText('High Score: ' + highScore); if (typeof bottomHighScoreTxt !== "undefined") { bottomHighScoreTxt.setText('High Score: ' + highScore); } } LK.getSound('davul').play(); LK.getSound('tap').play(); // Flash drum LK.effects.flashObject(leftDrum, 0xFFD700, 200); // Add drum hit animation tween.stop(leftDrum, { scaleX: true, scaleY: true }); leftDrum.scaleX = 1; leftDrum.scaleY = 1; tween(leftDrum, { scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(leftDrum, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); // Add strong screen shake for drum hit if (typeof game !== "undefined" && typeof tween !== "undefined") { if (!game._isShaking) { game._isShaking = true; var originalGameX = game.x || 0; var originalGameY = game.y || 0; var shakeAmount = 200; // Always use consistent shake amount for drums var shakeDuration = 300; tween(game, { x: originalGameX + (Math.random() - 0.5) * shakeAmount, y: originalGameY + (Math.random() - 0.5) * shakeAmount }, { duration: shakeDuration, onFinish: function onFinish() { tween(game, { x: originalGameX, y: originalGameY }, { duration: shakeDuration, onFinish: function onFinish() { game._isShaking = false; } }); } }); } } return; } } } } // Check right drum if (rightDrum) { var rightDrumLeft = rightDrum.x - 150; var rightDrumRight = rightDrum.x + 150; var rightDrumTop = rightDrum.y - 150; var rightDrumBottom = rightDrum.y + 150; if (x >= rightDrumLeft && x <= rightDrumRight && y >= rightDrumTop && y <= rightDrumBottom) { // Hit right drum - check for drum notes for (var d = drumNotes.length - 1; d >= 0; d--) { var drumNote = drumNotes[d]; if (!drumNote.hit && !drumNote.isLeft) { drumNote.onHit(); score += 200; scoreTxt.setText(score + ''); // Update high score if (score > highScore) { highScore = score; setHighScore(highScore); highScoreTxt.setText('High Score: ' + highScore); if (typeof bottomHighScoreTxt !== "undefined") { bottomHighScoreTxt.setText('High Score: ' + highScore); } } LK.getSound('davul').play(); LK.getSound('tap').play(); // Flash drum LK.effects.flashObject(rightDrum, 0xFFD700, 200); // Add drum hit animation tween.stop(rightDrum, { scaleX: true, scaleY: true }); rightDrum.scaleX = 1; rightDrum.scaleY = 1; tween(rightDrum, { scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(rightDrum, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); // Add strong screen shake for drum hit if (typeof game !== "undefined" && typeof tween !== "undefined") { if (!game._isShaking) { game._isShaking = true; var originalGameX = game.x || 0; var originalGameY = game.y || 0; var shakeAmount = 200; // Always use consistent shake amount for drums var shakeDuration = 300; tween(game, { x: originalGameX + (Math.random() - 0.5) * shakeAmount, y: originalGameY + (Math.random() - 0.5) * shakeAmount }, { duration: shakeDuration, onFinish: function onFinish() { tween(game, { x: originalGameX, y: originalGameY }, { duration: shakeDuration, onFinish: function onFinish() { game._isShaking = false; } }); } }); } } return; } } } } } // Pause people movement for 2 seconds (120 ticks) after a note is hit if (typeof game._peopleMovePausedUntil === "undefined") game._peopleMovePausedUntil = 0; game._peopleMovePausedUntil = songTicks + 120; // Check if tap is on any note (from topmost to bottom) var hit = false; var _loop = function _loop() { note = notes[i]; if (note.hit || note.missed) return 0; // continue // Get note bounds noteLeft = note.x - NOTE_WIDTH / 2; noteRight = note.x + NOTE_WIDTH / 2; noteTop = note.y - NOTE_HEIGHT / 2; noteBottom = note.y + NOTE_HEIGHT / 2; if (x >= noteLeft && x <= noteRight && y >= noteTop && y <= noteBottom) { // Hit! note.onHit(); hit = true; score += 100; combo += 1; if (combo > maxCombo) maxCombo = combo; scoreTxt.setText(score + ''); comboTxt.setText(combo > 1 ? combo + ' Combo!' : ''); // High score logic if (score > highScore) { highScore = score; setHighScore(highScore); highScoreTxt.setText('High Score: ' + highScore); if (typeof bottomHighScoreTxt !== "undefined") { bottomHighScoreTxt.setText('High Score: ' + highScore); } } LK.getSound('tap').play(); // Make a random subset of people jump when any note is hit! // Every 10 combo, make all people jump and apply a stronger shake if (typeof peopleLeft !== "undefined" && typeof peopleRight !== "undefined") { if (combo > 0 && combo % 50 === 0) { // --- MASSIVE SHAKE for every 50 combo --- // All people jump! for (var iAll = 0; iAll < peopleLeft.length; iAll++) { if (peopleLeft[iAll]) peopleLeft[iAll].jump(); } for (var iAll = 0; iAll < peopleRight.length; iAll++) { if (peopleRight[iAll]) peopleRight[iAll].jump(); } // Massive shake: override game.x/y with a huge shake if (typeof game !== "undefined" && typeof tween !== "undefined") { if (!game._isShaking) { game._isShaking = true; var originalGameX = game.x || 0; var originalGameY = game.y || 0; var shakeAmount = 420; // extremely strong shake var shakeDuration = 420; tween(game, { x: originalGameX + (Math.random() - 0.5) * shakeAmount, y: originalGameY + (Math.random() - 0.5) * shakeAmount }, { duration: shakeDuration, onFinish: function onFinish() { tween(game, { x: originalGameX, y: originalGameY }, { duration: shakeDuration, onFinish: function onFinish() { game._isShaking = false; } }); } }); } } } else if (combo > 0 && combo % 10 === 0) { // All people jump! for (var iAll = 0; iAll < peopleLeft.length; iAll++) { if (peopleLeft[iAll]) peopleLeft[iAll].jump(); } for (var iAll = 0; iAll < peopleRight.length; iAll++) { if (peopleRight[iAll]) peopleRight[iAll].jump(); } // Stronger shake: override game.x/y with a bigger shake if (typeof game !== "undefined" && typeof tween !== "undefined") { if (!game._isShaking) { game._isShaking = true; var originalGameX = game.x || 0; var originalGameY = game.y || 0; var shakeAmount = 60; // much stronger shake var shakeDuration = 120; tween(game, { x: originalGameX + (Math.random() - 0.5) * shakeAmount, y: originalGameY + (Math.random() - 0.5) * shakeAmount }, { duration: shakeDuration, onFinish: function onFinish() { tween(game, { x: originalGameX, y: originalGameY }, { duration: shakeDuration, onFinish: function onFinish() { game._isShaking = false; } }); } }); } } } else { // Helper to get unique random indices var getRandomIndices = function getRandomIndices(arrLen, count) { var indices = []; var used = []; while (indices.length < count && indices.length < arrLen) { var idx = Math.floor(Math.random() * arrLen); if (!used[idx]) { indices.push(idx); used[idx] = true; } } return indices; }; // How many people to jump per side? 1-3 random per side leftJumpCount = 1 + Math.floor(Math.random() * 3); rightJumpCount = 1 + Math.floor(Math.random() * 3); leftIndices = getRandomIndices(peopleLeft.length, leftJumpCount); rightIndices = getRandomIndices(peopleRight.length, rightJumpCount); for (j = 0; j < leftIndices.length; j++) { idx = leftIndices[j]; if (peopleLeft[idx]) peopleLeft[idx].jump(); } for (j = 0; j < rightIndices.length; j++) { idx = rightIndices[j]; if (peopleRight[idx]) peopleRight[idx].jump(); } } } // Show floating feedback text based on combo feedbackText = ''; if (combo >= 30) { feedbackText = 'PERFECT!'; } else if (combo >= 15) { feedbackText = 'GREAT!'; } else if (combo >= 5) { feedbackText = 'GOOD!'; } if (feedbackText) { fbTxt = new Text2(feedbackText, { size: 120, fill: 0xFFD700, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); fbTxt.anchor.set(0.5, 0.5); fbTxt.x = GAME_WIDTH / 2; fbTxt.y = GAME_HEIGHT / 2 - 200; fbTxt.alpha = 1; game.addChild(fbTxt); tween(fbTxt, { y: fbTxt.y - 120, alpha: 0 }, { duration: 700, easing: tween.easeOut, onFinish: function onFinish() { fbTxt.destroy(); } }); } return 1; // break } }, note, noteLeft, noteRight, noteTop, noteBottom, leftJumpCount, rightJumpCount, leftIndices, rightIndices, j, idx, j, idx, feedbackText, fbTxt, _ret; for (var i = notes.length - 1; i >= 0; i--) { _ret = _loop(); if (_ret === 0) continue; if (_ret === 1) break; } if (!hit) { // Missed tap (no note hit) combo = 0; comboTxt.setText(''); // No misses for tap misses, no flash } }; // --- Main Game Loop --- game.update = function () { if (!songStarted || songEnded) return; songTicks += 1; // Create drums at game start (handled in startSong) // Spawn drum notes if (drumsActive && songTicks >= nextDrumNoteTime) { var drumNote = new DrumNote(); drumNote.spawnTime = songTicks; // Randomly choose left or right drum drumNote.isLeft = Math.random() < 0.5; if (drumNote.isLeft) { drumNote.x = leftDrum.x; drumNote.y = leftDrum.y; } else { drumNote.x = rightDrum.x; drumNote.y = rightDrum.y; } drumNotes.push(drumNote); game.addChild(drumNote); // Schedule next drum note based on mode: easy=10s, hard=5s, extreme=3s if (typeof extremeMode !== "undefined" && extremeMode) { // 3 seconds interval (180 ticks) nextDrumNoteTime = songTicks + 180; } else if (typeof hardMode !== "undefined" && hardMode) { // 5 seconds interval (300 ticks) nextDrumNoteTime = songTicks + 300; } else { // Easy mode or default: 10 seconds interval (600 ticks) nextDrumNoteTime = songTicks + 600; } } // Update drum notes for (var d = drumNotes.length - 1; d >= 0; d--) { var drumNote = drumNotes[d]; drumNote.update(); if (drumNote.destroyed) { drumNotes.splice(d, 1); } } // Check if all people are jumping and change lane colors if (typeof peopleLeft !== "undefined" && typeof peopleRight !== "undefined") { var allJumping = true; // Check if all left people are jumping for (var i = 0; i < peopleLeft.length; i++) { if (peopleLeft[i] && !peopleLeft[i].isJumping) { allJumping = false; break; } } // Check if all right people are jumping if (allJumping) { for (var i = 0; i < peopleRight.length; i++) { if (peopleRight[i] && !peopleRight[i].isJumping) { allJumping = false; break; } } } // If all people are jumping, change lane colors randomly if (allJumping && peopleLeft.length > 0 && peopleRight.length > 0) { if (!game._lastAllJumping) { // This is the moment when all started jumping var colors = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF, 0xFFA500, 0xFF1493, 0x32CD32, 0x8A2BE2]; for (var l = 0; l < lanes.length; l++) { if (lanes[l]) { var randomColor = colors[Math.floor(Math.random() * colors.length)]; lanes[l].tint = randomColor; } } } game._lastAllJumping = true; } else { game._lastAllJumping = false; } } // Gradually increase noteSpeedMultiplier (very slow ramp, e.g. +0.0002 per tick) // In normal mode, do NOT increase speed faster after 5000 points if (noteSpeedMultiplier < 2.0) { noteSpeedMultiplier += 0.0002; if (noteSpeedMultiplier > 2.0) noteSpeedMultiplier = 2.0; } // Spawn notes as needed (random, infinite) // After 5000 points, do not increase note spawn count; only speed increases var notesToSpawn = 1; // No change to notesToSpawn after 5000 points while (songTicks >= nextNoteTick - NOTE_TRAVEL_TICKS) { for (var spawnIdx = 0; spawnIdx < notesToSpawn; spawnIdx++) { var lane = getRandomLane(); var noteTime = nextNoteTick; var note = new Note(); note.setLane(lane); // Spawn note at current lane container position note.x = laneContainers[lane].x; note.y = -NOTE_HEIGHT / 2; note.speed = (TARGET_Y + NOTE_HEIGHT / 2) / NOTE_TRAVEL_TICKS * noteSpeedMultiplier; note.time = noteTime; note.spawned = true; // Store reference to lane container so note can follow it note.laneContainer = laneContainers[lane]; notes.push(note); game.addChild(note); } // Schedule next note nextNoteTick += getRandomInterval(); } // Check if score reached 5000 for lane color flashing if (score >= 5000 && !laneColorFlashTriggered) { laneColorFlashTriggered = true; laneColorFlashActive = true; laneColorFlashStartTime = songTicks; } // Handle rapid lane color changes if (laneColorFlashActive) { var flashDuration = 600; // 10 seconds at 60fps var elapsed = songTicks - laneColorFlashStartTime; if (elapsed < flashDuration) { // Change colors very rapidly (every 3 frames = 20 times per second) if (songTicks % 3 === 0) { var colors = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF, 0xFFA500, 0xFF1493, 0x32CD32, 0x8A2BE2, 0xFFD700, 0x4B0082, 0x7FFF00, 0xDC143C, 0x00CED1]; for (var l = 0; l < lanes.length; l++) { if (lanes[l]) { var randomColor = colors[Math.floor(Math.random() * colors.length)]; tween(lanes[l], { tint: randomColor }, { duration: 50, easing: tween.linear }); } } } } else { // Reset to original color after 10 seconds laneColorFlashActive = false; for (var l = 0; l < lanes.length; l++) { if (lanes[l]) { tween(lanes[l], { tint: 0x222222 }, { duration: 300, easing: tween.easeOut }); } } } } // Add people every 500 points, up to 5 per side (10 total) if (score >= lastScoreMilestone + 500 && currentPeoplePerSide < 5) { lastScoreMilestone = Math.floor(score / 500) * 500; currentPeoplePerSide++; // Add one person to left side var leftPerson = new People(); var attempts = 0; var px, py; do { py = Y_MIN + Math.random() * (Y_MAX - Y_MIN); px = FAR_LEFT_X_MIN + Math.random() * (FAR_LEFT_X_MAX - FAR_LEFT_X_MIN); attempts++; } while ((!isFarEnough(px, py, peopleLeft) || !isNotCenter(px) || !isNotDrumArea(px, py)) && attempts < MAX_ATTEMPTS); leftPerson.x = px; leftPerson.y = py; game.addChild(leftPerson); peopleLeft.push(leftPerson); // Add one person to right side var rightPerson = new People(); attempts = 0; do { py = Y_MIN + Math.random() * (Y_MAX - Y_MIN); px = FAR_RIGHT_X_MIN + Math.random() * (FAR_RIGHT_X_MAX - FAR_RIGHT_X_MIN); attempts++; } while ((!isFarEnough(px, py, peopleRight) || !isNotCenter(px) || !isNotDrumArea(px, py)) && attempts < MAX_ATTEMPTS); rightPerson.x = px; rightPerson.y = py; game.addChild(rightPerson); peopleRight.push(rightPerson); // Update popularity when new people are added updatePopularity(); } // Update notes for (var i = notes.length - 1; i >= 0; i--) { var note = notes[i]; note.update(); // If note reached target zone and not hit, mark as missed if (!note.hit && !note.missed && note.y >= TARGET_Y + TARGET_HEIGHT / 2) { note.onMiss(); combo = 0; misses += 1; comboTxt.setText(''); // Increment and show per-target misses in white above each target if (!targets[note.lane].missCount) targets[note.lane].missCount = 0; targets[note.lane].missCount += 1; if (targetMissTexts && targetMissTexts[note.lane]) { targetMissTexts[note.lane].setText(targets[note.lane].missCount + ''); targetMissTexts[note.lane].setStyle({ fill: 0x3399FF }); } // Decrease popularity by 2% and remove one person when a note is missed // Remove one person randomly from either left or right side var removedPerson = false; if (Math.random() < 0.5 && peopleLeft.length > 0) { // Remove from left side var personToRemove = peopleLeft.pop(); if (personToRemove && personToRemove.destroy) { personToRemove.destroy(); removedPerson = true; } } else if (peopleRight.length > 0) { // Remove from right side var personToRemove = peopleRight.pop(); if (personToRemove && personToRemove.destroy) { personToRemove.destroy(); removedPerson = true; } } // Update popularity after removing person if (removedPerson) { updatePopularity(); } LK.getSound('miss').play({ fade: { start: 1, end: 0, duration: 1000 } }); LK.effects.flashObject(targets[note.lane].outer, 0xff0000, 200); LK.effects.flashObject(targets[note.lane].inner, 0xff6666, 200); if (misses >= maxMisses) { // No game over, just keep going } // Check if any target's miss count exceeds 10, trigger game over for (var t = 0; t < targets.length; t++) { if (targets[t].missCount && targets[t].missCount > 10) { LK.showGameOver(); return; } } } // Remove destroyed notes if (note.destroyed) { notes.splice(i, 1); } } // --- Move people to new random positions every 2 seconds --- if (typeof game._lastPeopleMoveTick === "undefined") game._lastPeopleMoveTick = 0; if (typeof game._peopleMovePausedUntil === "undefined") game._peopleMovePausedUntil = 0; // Only move people if not in the 2s pause after a note hit if (songTicks - game._lastPeopleMoveTick > 120 && songTicks >= game._peopleMovePausedUntil) { // Helper to move a person to a new random position (non-overlapping, not center) var movePerson = function movePerson(person, arr, isLeft) { var attempts = 0; var px, py; do { py = Y_MIN + Math.random() * (Y_MAX - Y_MIN); if (isLeft) { // Move anywhere in the left empty area px = FAR_LEFT_X_MIN + Math.random() * (FAR_LEFT_X_MAX - FAR_LEFT_X_MIN); } else { // Move anywhere in the right empty area px = FAR_RIGHT_X_MIN + Math.random() * (FAR_RIGHT_X_MAX - FAR_RIGHT_X_MIN); } attempts++; } while ((!isFarEnough(px, py, arr) || !isNotCenter(px)) && attempts < MAX_ATTEMPTS); // Animate to new position (move very slowly, never teleport) tween(person, { x: px, y: py }, { duration: 3200, // much slower movement (3.2 seconds) easing: tween.easeInOut }); person.baseY = py; }; // Move left people // every 2 seconds at 60fps game._lastPeopleMoveTick = songTicks; for (var i = 0; i < peopleLeft.length; i++) { movePerson(peopleLeft[i], peopleLeft, true); } // Move right people for (var i = 0; i < peopleRight.length; i++) { movePerson(peopleRight[i], peopleRight, false); } } // No win condition, keep game running }; // --- Start Menu Overlay --- var startMenuOverlay = new Container(); var overlayBg = LK.getAsset('lane', { anchorX: 0.5, anchorY: 0.5, x: GAME_WIDTH / 2, y: GAME_HEIGHT / 2, width: GAME_WIDTH, height: GAME_HEIGHT, color: 0x000000 }); overlayBg.alpha = 0.85; startMenuOverlay.addChild(overlayBg); var titleText = new Text2('BEAT TAPPER', { size: 120, fill: 0xFFD700, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); titleText.anchor.set(0.5, 0.5); titleText.x = GAME_WIDTH / 2; titleText.y = GAME_HEIGHT / 2 - 400; startMenuOverlay.addChild(titleText); var tapToStartText = new Text2('Tap to Start', { size: 90, fill: 0xFFFFFF }); tapToStartText.anchor.set(0.5, 0.5); tapToStartText.x = GAME_WIDTH / 2; // Move even higher above instructions (was -60, now -120 for more space above instructions) tapToStartText.y = GAME_HEIGHT / 2 - 120; // Create a box behind the text var tapBoxPaddingX = 60; var tapBoxPaddingY = 30; var tapBox = LK.getAsset('lane', { anchorX: 0.5, anchorY: 0.5, x: tapToStartText.x, y: tapToStartText.y, width: tapToStartText.width + tapBoxPaddingX, height: tapToStartText.height + tapBoxPaddingY, color: 0xFFFFFF }); tapBox.alpha = 0.18; startMenuOverlay.addChild(tapBox); startMenuOverlay.addChild(tapToStartText); var instructionsText = new Text2("How to Play:\n\n" + "• Tap the falling notes when they reach the targets.\n" + "• Don't let too many notes pass the targets!\n" + "• Each lane shows your misses. If any lane gets more than 10 misses, it's game over.\n" + "• Try to get the highest combo and score!", { size: 44, fill: 0xFFFFFF, align: "center" }); instructionsText.anchor.set(0.5, 0.5); instructionsText.x = GAME_WIDTH / 2; instructionsText.y = GAME_HEIGHT / 2 + 120; startMenuOverlay.addChild(instructionsText); var highScoreMenuText = new Text2('High Score: ' + getHighScore(), { size: 70, fill: 0xFFD700, align: "center" }); highScoreMenuText.anchor.set(0.5, 0.5); highScoreMenuText.x = GAME_WIDTH / 2; highScoreMenuText.y = GAME_HEIGHT / 2 + 320; startMenuOverlay.addChild(highScoreMenuText); // Add drum instructions at the very bottom of the menu overlay var drumInstructionsText = new Text2("At the start of the game, two drums (left and right) will appear.\n" + "Drum notes will randomly appear above the drums.\n" + "Tap the drum note within 5 seconds to score points!\n" + "If you don't tap the drum note in 5 seconds, you lose.", { size: 40, fill: 0xFFD700, align: "center" }); drumInstructionsText.anchor.set(0.5, 1); drumInstructionsText.x = GAME_WIDTH / 2; drumInstructionsText.y = GAME_HEIGHT - 60; startMenuOverlay.addChild(drumInstructionsText); // --- Easy Mode Button --- var easyModeBtn = new Text2('Easy Mode', { size: 80, fill: 0x44FF44, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); easyModeBtn.anchor.set(0.5, 0.5); easyModeBtn.x = GAME_WIDTH / 2; easyModeBtn.y = GAME_HEIGHT / 2 + 480; easyModeBtn.alpha = 0.92; easyModeBtn._isEasyModeBtn = true; startMenuOverlay.addChild(easyModeBtn); // --- Hard Mode Button --- var hardModeBtn = new Text2('Hard Mode', { size: 80, fill: 0xFF2222, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); hardModeBtn.anchor.set(0.5, 0.5); hardModeBtn.x = GAME_WIDTH / 2; hardModeBtn.y = GAME_HEIGHT / 2 + 620; hardModeBtn.alpha = 0.92; hardModeBtn._isHardModeBtn = true; startMenuOverlay.addChild(hardModeBtn); // --- Extreme Mode Button --- var extremeModeBtn = new Text2('Extreme Mode', { size: 80, fill: 0xFF00FF, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); extremeModeBtn.anchor.set(0.5, 0.5); extremeModeBtn.x = GAME_WIDTH / 2; extremeModeBtn.y = GAME_HEIGHT / 2 + 760; extremeModeBtn.alpha = 0.92; extremeModeBtn._isExtremeModeBtn = true; startMenuOverlay.addChild(extremeModeBtn); game.addChild(startMenuOverlay); var menuActive = true; var hardMode = false; var extremeMode = false; var easyMode = true; // Default to easy mode game.down = function (x, y, obj) { if (menuActive) { // Check if tap is on Easy Mode button var easyBtnLeft = easyModeBtn.x - easyModeBtn.width / 2; var easyBtnRight = easyModeBtn.x + easyModeBtn.width / 2; var easyBtnTop = easyModeBtn.y - easyModeBtn.height / 2; var easyBtnBottom = easyModeBtn.y + easyModeBtn.height / 2; if (x >= easyBtnLeft && x <= easyBtnRight && y >= easyBtnTop && y <= easyBtnBottom) { // Toggle easy mode ON and visually indicate easyMode = true; hardMode = false; extremeMode = false; easyModeBtn.setText('Easy Mode: ON'); easyModeBtn.fill = 0x66FF66; easyModeBtn.alpha = 1; hardModeBtn.setText('Hard Mode'); hardModeBtn.fill = 0xFF2222; hardModeBtn.alpha = 0.92; extremeModeBtn.setText('Extreme Mode'); extremeModeBtn.fill = 0xFF00FF; extremeModeBtn.alpha = 0.92; // Update high score in menu overlay if (typeof highScoreMenuText !== "undefined") { highScoreMenuText.setText('High Score: ' + getHighScore()); } // Optionally, flash or animate tween(easyModeBtn, { scaleX: 1.2, scaleY: 1.2 }, { duration: 120, yoyo: true, repeat: 1, onFinish: function onFinish() { easyModeBtn.scaleX = 1; easyModeBtn.scaleY = 1; } }); return; } // Check if tap is on Hard Mode button var btnLeft = hardModeBtn.x - hardModeBtn.width / 2; var btnRight = hardModeBtn.x + hardModeBtn.width / 2; var btnTop = hardModeBtn.y - hardModeBtn.height / 2; var btnBottom = hardModeBtn.y + hardModeBtn.height / 2; if (x >= btnLeft && x <= btnRight && y >= btnTop && y <= btnBottom) { // Toggle hard mode ON and visually indicate hardMode = true; easyMode = false; extremeMode = false; hardModeBtn.setText('Hard Mode: ON'); hardModeBtn.fill = 0xFF4444; hardModeBtn.alpha = 1; easyModeBtn.setText('Easy Mode'); easyModeBtn.fill = 0x44FF44; easyModeBtn.alpha = 0.92; extremeModeBtn.setText('Extreme Mode'); extremeModeBtn.fill = 0xFF00FF; extremeModeBtn.alpha = 0.92; // Update high score in menu overlay if (typeof highScoreMenuText !== "undefined") { highScoreMenuText.setText('High Score: ' + getHighScore()); } // Optionally, flash or animate tween(hardModeBtn, { scaleX: 1.2, scaleY: 1.2 }, { duration: 120, yoyo: true, repeat: 1, onFinish: function onFinish() { hardModeBtn.scaleX = 1; hardModeBtn.scaleY = 1; } }); return; } // Check if tap is on Extreme Mode button var ebtnLeft = extremeModeBtn.x - extremeModeBtn.width / 2; var ebtnRight = extremeModeBtn.x + extremeModeBtn.width / 2; var ebtnTop = extremeModeBtn.y - extremeModeBtn.height / 2; var ebtnBottom = extremeModeBtn.y + extremeModeBtn.height / 2; if (x >= ebtnLeft && x <= ebtnRight && y >= ebtnTop && y <= ebtnBottom) { // Toggle extreme mode ON and visually indicate extremeMode = true; hardMode = false; easyMode = false; extremeModeBtn.setText('Extreme Mode: ON'); extremeModeBtn.fill = 0xFF66FF; extremeModeBtn.alpha = 1; hardModeBtn.setText('Hard Mode'); hardModeBtn.fill = 0xFF2222; hardModeBtn.alpha = 0.92; easyModeBtn.setText('Easy Mode'); easyModeBtn.fill = 0x44FF44; easyModeBtn.alpha = 0.92; // Update high score in menu overlay if (typeof highScoreMenuText !== "undefined") { highScoreMenuText.setText('High Score: ' + getHighScore()); } // Optionally, flash or animate tween(extremeModeBtn, { scaleX: 1.2, scaleY: 1.2 }, { duration: 120, yoyo: true, repeat: 1, onFinish: function onFinish() { extremeModeBtn.scaleX = 1; extremeModeBtn.scaleY = 1; } }); return; } menuActive = false; startMenuOverlay.destroy(); startSong(); // Restore original input handler for gameplay game.down = function (x, y, obj) { // Only allow input if song is running if (!songStarted || songEnded) return; // Check drum hits first if drums are active if (drumsActive) { // Check left drum if (leftDrum) { var leftDrumLeft = leftDrum.x - 150; var leftDrumRight = leftDrum.x + 150; var leftDrumTop = leftDrum.y - 150; var leftDrumBottom = leftDrum.y + 150; if (x >= leftDrumLeft && x <= leftDrumRight && y >= leftDrumTop && y <= leftDrumBottom) { // Hit left drum - check for drum notes for (var d = drumNotes.length - 1; d >= 0; d--) { var drumNote = drumNotes[d]; if (!drumNote.hit && drumNote.isLeft) { drumNote.onHit(); score += 200; scoreTxt.setText(score + ''); // Update high score if (score > highScore) { highScore = score; setHighScore(highScore); highScoreTxt.setText('High Score: ' + highScore); if (typeof bottomHighScoreTxt !== "undefined") { bottomHighScoreTxt.setText('High Score: ' + highScore); } } LK.getSound('davul').play(); LK.getSound('tap').play(); // Flash drum LK.effects.flashObject(leftDrum, 0xFFD700, 200); // Add drum hit animation tween.stop(leftDrum, { scaleX: true, scaleY: true }); leftDrum.scaleX = 1; leftDrum.scaleY = 1; tween(leftDrum, { scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(leftDrum, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); // Add strong screen shake for drum hit if (typeof game !== "undefined" && typeof tween !== "undefined") { if (!game._isShaking) { game._isShaking = true; var originalGameX = game.x || 0; var originalGameY = game.y || 0; var shakeAmount = 200; // Always use consistent shake amount for drums var shakeDuration = 300; tween(game, { x: originalGameX + (Math.random() - 0.5) * shakeAmount, y: originalGameY + (Math.random() - 0.5) * shakeAmount }, { duration: shakeDuration, onFinish: function onFinish() { tween(game, { x: originalGameX, y: originalGameY }, { duration: shakeDuration, onFinish: function onFinish() { game._isShaking = false; } }); } }); } } return; } } } } // Check right drum if (rightDrum) { var rightDrumLeft = rightDrum.x - 150; var rightDrumRight = rightDrum.x + 150; var rightDrumTop = rightDrum.y - 150; var rightDrumBottom = rightDrum.y + 150; if (x >= rightDrumLeft && x <= rightDrumRight && y >= rightDrumTop && y <= rightDrumBottom) { // Hit right drum - check for drum notes for (var d = drumNotes.length - 1; d >= 0; d--) { var drumNote = drumNotes[d]; if (!drumNote.hit && !drumNote.isLeft) { drumNote.onHit(); score += 200; scoreTxt.setText(score + ''); // Update high score if (score > highScore) { highScore = score; setHighScore(highScore); highScoreTxt.setText('High Score: ' + highScore); if (typeof bottomHighScoreTxt !== "undefined") { bottomHighScoreTxt.setText('High Score: ' + highScore); } } LK.getSound('davul').play(); LK.getSound('tap').play(); // Flash drum LK.effects.flashObject(rightDrum, 0xFFD700, 200); // Add drum hit animation tween.stop(rightDrum, { scaleX: true, scaleY: true }); rightDrum.scaleX = 1; rightDrum.scaleY = 1; tween(rightDrum, { scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(rightDrum, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); // Add strong screen shake for drum hit if (typeof game !== "undefined" && typeof tween !== "undefined") { if (!game._isShaking) { game._isShaking = true; var originalGameX = game.x || 0; var originalGameY = game.y || 0; var shakeAmount = 200; // Always use consistent shake amount for drums var shakeDuration = 300; tween(game, { x: originalGameX + (Math.random() - 0.5) * shakeAmount, y: originalGameY + (Math.random() - 0.5) * shakeAmount }, { duration: shakeDuration, onFinish: function onFinish() { tween(game, { x: originalGameX, y: originalGameY }, { duration: shakeDuration, onFinish: function onFinish() { game._isShaking = false; } }); } }); } } return; } } } } } // Pause people movement for 2 seconds (120 ticks) after a note is hit if (typeof game._peopleMovePausedUntil === "undefined") game._peopleMovePausedUntil = 0; game._peopleMovePausedUntil = songTicks + 120; // Check if tap is on any note (from topmost to bottom) var hit = false; var _loop = function _loop() { note = notes[i]; if (note.hit || note.missed) return 0; // continue // Get note bounds noteLeft = note.x - NOTE_WIDTH / 2; noteRight = note.x + NOTE_WIDTH / 2; noteTop = note.y - NOTE_HEIGHT / 2; noteBottom = note.y + NOTE_HEIGHT / 2; if (x >= noteLeft && x <= noteRight && y >= noteTop && y <= noteBottom) { // Hit! note.onHit(); hit = true; score += 100; combo += 1; if (combo > maxCombo) maxCombo = combo; scoreTxt.setText(score + ''); comboTxt.setText(combo > 1 ? combo + ' Combo!' : ''); // High score logic if (score > highScore) { highScore = score; setHighScore(highScore); highScoreTxt.setText('High Score: ' + highScore); if (typeof bottomHighScoreTxt !== "undefined") { bottomHighScoreTxt.setText('High Score: ' + highScore); } } LK.getSound('tap').play(); // Make a random subset of people jump when any note is hit! // Every 10 combo, make all people jump and apply a stronger shake if (typeof peopleLeft !== "undefined" && typeof peopleRight !== "undefined") { if (combo > 0 && combo % 50 === 0) { // --- MASSIVE SHAKE for every 50 combo --- // All people jump! for (var iAll = 0; iAll < peopleLeft.length; iAll++) { if (peopleLeft[iAll]) peopleLeft[iAll].jump(); } for (var iAll = 0; iAll < peopleRight.length; iAll++) { if (peopleRight[iAll]) peopleRight[iAll].jump(); } // Massive shake: override game.x/y with a huge shake if (typeof game !== "undefined" && typeof tween !== "undefined") { if (!game._isShaking) { game._isShaking = true; var originalGameX = game.x || 0; var originalGameY = game.y || 0; var shakeAmount = 420; // extremely strong shake var shakeDuration = 420; tween(game, { x: originalGameX + (Math.random() - 0.5) * shakeAmount, y: originalGameY + (Math.random() - 0.5) * shakeAmount }, { duration: shakeDuration, onFinish: function onFinish() { tween(game, { x: originalGameX, y: originalGameY }, { duration: shakeDuration, onFinish: function onFinish() { game._isShaking = false; } }); } }); } } } else if (combo > 0 && combo % 10 === 0) { // All people jump! for (var iAll = 0; iAll < peopleLeft.length; iAll++) { if (peopleLeft[iAll]) peopleLeft[iAll].jump(); } for (var iAll = 0; iAll < peopleRight.length; iAll++) { if (peopleRight[iAll]) peopleRight[iAll].jump(); } // Stronger shake: override game.x/y with a bigger shake if (typeof game !== "undefined" && typeof tween !== "undefined") { if (!game._isShaking) { game._isShaking = true; var originalGameX = game.x || 0; var originalGameY = game.y || 0; var shakeAmount = 60; // much stronger shake var shakeDuration = 120; tween(game, { x: originalGameX + (Math.random() - 0.5) * shakeAmount, y: originalGameY + (Math.random() - 0.5) * shakeAmount }, { duration: shakeDuration, onFinish: function onFinish() { tween(game, { x: originalGameX, y: originalGameY }, { duration: shakeDuration, onFinish: function onFinish() { game._isShaking = false; } }); } }); } } } else { // Helper to get unique random indices var getRandomIndices = function getRandomIndices(arrLen, count) { var indices = []; var used = []; while (indices.length < count && indices.length < arrLen) { var idx = Math.floor(Math.random() * arrLen); if (!used[idx]) { indices.push(idx); used[idx] = true; } } return indices; }; // How many people to jump per side? 1-3 random per side leftJumpCount = 1 + Math.floor(Math.random() * 3); rightJumpCount = 1 + Math.floor(Math.random() * 3); leftIndices = getRandomIndices(peopleLeft.length, leftJumpCount); rightIndices = getRandomIndices(peopleRight.length, rightJumpCount); for (j = 0; j < leftIndices.length; j++) { idx = leftIndices[j]; if (peopleLeft[idx]) peopleLeft[idx].jump(); } for (j = 0; j < rightIndices.length; j++) { idx = rightIndices[j]; if (peopleRight[idx]) peopleRight[idx].jump(); } } } // Show floating feedback text based on combo feedbackText = ''; if (combo >= 30) { feedbackText = 'PERFECT!'; } else if (combo >= 15) { feedbackText = 'GREAT!'; } else if (combo >= 5) { feedbackText = 'GOOD!'; } if (feedbackText) { fbTxt = new Text2(feedbackText, { size: 120, fill: 0xFFD700, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); fbTxt.anchor.set(0.5, 0.5); fbTxt.x = GAME_WIDTH / 2; fbTxt.y = GAME_HEIGHT / 2 - 200; fbTxt.alpha = 1; game.addChild(fbTxt); tween(fbTxt, { y: fbTxt.y - 120, alpha: 0 }, { duration: 700, easing: tween.easeOut, onFinish: function onFinish() { fbTxt.destroy(); } }); } return 1; // break } }, note, noteLeft, noteRight, noteTop, noteBottom, leftJumpCount, rightJumpCount, leftIndices, rightIndices, j, idx, j, idx, feedbackText, fbTxt, _ret; for (var i = notes.length - 1; i >= 0; i--) { _ret = _loop(); if (_ret === 0) continue; if (_ret === 1) break; } if (!hit) { // Missed tap (no note hit) combo = 0; comboTxt.setText(''); // No misses for tap misses, no flash } }; } }; // Do not start the song immediately; wait for user tap // startSong();;
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
// DrumNote class for notes on drums
var DrumNote = Container.expand(function () {
var self = Container.call(this);
self.spawnTime = 0;
self.isLeft = true;
self.hit = false;
self.lifeTime = 300; // 5 seconds at 60fps
var noteGraphics = self.attachAsset('drumNote', {
anchorX: 0.5,
anchorY: 0.5
});
// Add a timer text above the note
self.timerText = new Text2('3.0', {
size: 60,
fill: 0xFF2222,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
self.timerText.anchor.set(0.5, 1); // center, bottom
self.timerText.x = 0;
self.timerText.y = -noteGraphics.height / 2 - 10;
self.addChild(self.timerText);
self.update = function () {
// Update timer text if not hit
if (!self.hit) {
var elapsed = songTicks - self.spawnTime;
var remain = Math.max(0, (self.lifeTime - elapsed) / 60);
// Show with 1 decimal, clamp to 0
self.timerText.setText(remain.toFixed(1));
}
if (!self.hit && songTicks - self.spawnTime > self.lifeTime) {
// Note expired - game over
LK.showGameOver();
}
};
self.onHit = function () {
if (self.hit) return;
self.hit = true;
// Explosion effect
var particleCount = 6;
var colors = [0xFFD700, 0xFFAA00, 0xFFFF00];
for (var p = 0; p < particleCount; p++) {
var particle = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
width: 30 + Math.random() * 40,
height: 30 + Math.random() * 40
});
particle.tint = colors[Math.floor(Math.random() * colors.length)];
if (self.parent) self.parent.addChild(particle);
var angle = Math.PI * 2 * p / particleCount;
var distance = 100 + Math.random() * 100;
var targetX = self.x + Math.cos(angle) * distance;
var targetY = self.y + Math.sin(angle) * distance;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
if (particle.parent) particle.destroy();
}
});
}
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// Note class for falling notes
var Note = Container.expand(function () {
var self = Container.call(this);
// Lane index (0-3)
self.lane = 0;
self.hit = false;
self.missed = false;
self.speed = 0; // pixels per tick
self.time = 0; // time (in ticks) when note should reach the target
self.spawned = false;
// Attach correct note asset based on lane
self.setLane = function (laneIdx) {
self.lane = laneIdx;
var assetId = 'note' + (laneIdx + 1);
var noteAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
width: NOTE_WIDTH,
height: NOTE_HEIGHT
});
};
// Called every tick
self.update = function () {
if (!self.spawned) return;
self.y += self.speed;
// Follow lane container movement
if (self.laneContainer) {
self.x = self.laneContainer.x;
}
};
// Called when note is hit
self.onHit = function () {
if (self.hit || self.missed) return;
self.hit = true;
// Create explosion effect with particles
var particleCount = 8;
var colors = [0xFFD700, 0xFF4444, 0x44FF44, 0x4444FF, 0xFF44FF, 0x44FFFF];
for (var p = 0; p < particleCount; p++) {
var particle = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y,
width: 20 + Math.random() * 30,
height: 20 + Math.random() * 30
});
particle.tint = colors[Math.floor(Math.random() * colors.length)];
if (self.parent) self.parent.addChild(particle);
// Random direction for explosion
var angle = Math.PI * 2 * p / particleCount + (Math.random() - 0.5) * 0.5;
var distance = 80 + Math.random() * 120;
var targetX = self.x + Math.cos(angle) * distance;
var targetY = self.y + Math.sin(angle) * distance;
// Animate particle explosion
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.2,
scaleY: 0.2
}, {
duration: 300 + Math.random() * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
if (particle.parent) particle.destroy();
}
});
}
// Animate note
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
// Called when note is missed
self.onMiss = function () {
if (self.hit || self.missed) return;
self.missed = true;
LK.getSound('miss').play({
fade: {
start: 1,
end: 0,
duration: 1000
}
});
tween(self, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// People class for animated people at lane edges
var People = Container.expand(function () {
var self = Container.call(this);
self.isJumping = false;
self.baseY = 0;
self.personType = Math.floor(Math.random() * 5); // Random type 0-4
// Create different visual styles based on person type
var body;
switch (self.personType) {
case 0:
// Type 1: Tall blue person
body = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 1,
width: 110,
height: 240,
y: 0,
tint: 0x3366FF
});
break;
case 1:
// Type 2: Wide green person
body = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 1,
width: 150,
height: 200,
y: 0,
tint: 0x33FF66
});
break;
case 2:
// Type 3: Medium red person
body = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 1,
width: 125,
height: 220,
y: 0,
tint: 0xFF3366
});
break;
case 3:
// Type 4: Small yellow person
body = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 1,
width: 100,
height: 170,
y: 0,
tint: 0xFFFF33
});
break;
case 4:
// Type 5: Average purple person
body = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 1,
width: 120,
height: 210,
y: 0,
tint: 0xCC33FF
});
break;
}
// Arms removed: only body remains
// Save baseY for jump animation
self.baseY = self.y;
// Animate jump
self.jump = function () {
if (self.isJumping) return;
self.isJumping = true;
var jumpHeight = 120;
var jumpDuration = 180;
var originalY = self.y;
// --- Screen shake effect when people jump ---
if (typeof game !== "undefined" && typeof tween !== "undefined") {
// Only shake if not already shaking
if (!game._isShaking) {
game._isShaking = true;
var originalGameX = game.x || 0;
var originalGameY = game.y || 0;
// Determine if all people are jumping at once for stronger shake
var allJumping = false;
if (typeof peopleLeft !== "undefined" && typeof peopleRight !== "undefined") {
var allLeftJumping = true;
for (var i = 0; i < peopleLeft.length; i++) {
if (!peopleLeft[i].isJumping) {
allLeftJumping = false;
break;
}
}
var allRightJumping = true;
for (var i = 0; i < peopleRight.length; i++) {
if (!peopleRight[i].isJumping) {
allRightJumping = false;
break;
}
}
allJumping = allLeftJumping && allRightJumping;
}
var shakeAmount = allJumping ? 120 : 40; // much stronger shake if all people are jumping
var shakeDuration = allJumping ? 260 : 180;
// Shake out
tween(game, {
x: originalGameX + (Math.random() - 0.5) * shakeAmount,
y: originalGameY + (Math.random() - 0.5) * shakeAmount
}, {
duration: shakeDuration,
onFinish: function onFinish() {
// Shake back
tween(game, {
x: originalGameX,
y: originalGameY
}, {
duration: shakeDuration,
onFinish: function onFinish() {
game._isShaking = false;
}
});
}
});
}
}
tween(self, {
y: originalY - jumpHeight
}, {
duration: jumpDuration,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
y: originalY
}, {
duration: jumpDuration,
easing: tween.easeIn,
onFinish: function onFinish() {
self.isJumping = false;
}
});
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Notes are destroyed by tapping them directly before they reach the white target area at the bottom
// 4 note lanes, each with a different color for clarity
// --- UI Elements ---
var NUM_LANES = 4;
var LANE_WIDTH = 230;
var LANE_SPACING = 40;
var NOTE_WIDTH = 340; // Wider notes
var NOTE_HEIGHT = 340; // Taller notes (stretches upward)
var TARGET_HEIGHT = 60; // White target area is now just a visual "danger" zone, reduced height for smaller targets
var LANE_HEIGHT = 2200;
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var LANE_TOTAL_WIDTH = NUM_LANES * LANE_WIDTH + (NUM_LANES - 1) * LANE_SPACING;
var LANE_START_X = (GAME_WIDTH - LANE_TOTAL_WIDTH) / 2 + LANE_WIDTH / 2;
var TARGET_Y = GAME_HEIGHT - 420; // Target zone Y position (moved higher)
// --- Game State ---
var notes = []; // All active notes
var noteIndex = 0; // Index of next note to spawn
var songTicks = 0; // Ticks since song start
var score = 0;
var combo = 0;
var maxCombo = 0;
var misses = 0;
var maxMisses = 10;
var songEnded = false;
var songStarted = false;
var lastTick = 0;
// Note speed multiplier for gradual speed up
var noteSpeedMultiplier = 1.0;
// Lane color flashing state
var laneColorFlashActive = false;
var laneColorFlashStartTime = 0;
var laneColorFlashTriggered = false;
// Drum variables
var leftDrum = null;
var rightDrum = null;
var drumNotes = [];
var drumsActive = false;
var nextDrumNoteTime = 0;
// --- UI Elements ---
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// High Score UI
// --- Per-mode high score keys ---
function getCurrentModeKey() {
if (typeof extremeMode !== "undefined" && extremeMode) return "extreme";
if (typeof hardMode !== "undefined" && hardMode) return "hard";
return "easy";
}
function getHighScoreKey() {
return "highScore_" + getCurrentModeKey();
}
function getHighScore() {
var key = getHighScoreKey();
return typeof storage[key] !== "undefined" ? storage[key] : 0;
}
function setHighScore(val) {
var key = getHighScoreKey();
storage[key] = val;
}
var highScore = getHighScore();
var highScoreTxt = new Text2('High Score: ' + highScore, {
size: 60,
fill: 0xFFD700
});
// Move high score to top-right, with some margin from the edge
highScoreTxt.anchor.set(1, 0); // right aligned, top
highScoreTxt.x = LK.gui.width - 40; // 40px margin from right
highScoreTxt.y = 30; // 30px from top
LK.gui.top.addChild(highScoreTxt);
var comboTxt = new Text2('', {
size: 70,
fill: 0xFFE066
});
comboTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(comboTxt);
comboTxt.y = 130;
// --- Popularity Display ---
var popularityTxt = new Text2('Popularity: 0%', {
size: 60,
fill: 0xFFD700
});
popularityTxt.anchor.set(1, 1); // right aligned, bottom
popularityTxt.x = GAME_WIDTH - 40; // 40px margin from right
popularityTxt.y = GAME_HEIGHT - 40; // 40px margin from bottom
game.addChild(popularityTxt);
// --- High Score Display in Bottom Left ---
var bottomHighScoreTxt = new Text2('High Score: ' + getHighScore(), {
size: 60,
fill: 0xFFD700
});
bottomHighScoreTxt.anchor.set(0, 1); // left aligned, bottom
bottomHighScoreTxt.x = 40; // 40px margin from left
bottomHighScoreTxt.y = GAME_HEIGHT - 40; // 40px margin from bottom
game.addChild(bottomHighScoreTxt);
// Function to update popularity based on total people count
function updatePopularity() {
var totalPeople = peopleLeft.length + peopleRight.length;
var popularity = Math.min(totalPeople * 10, 100); // Each person is 10%, cap at 100%
popularityTxt.setText('Popularity: ' + popularity + '%');
}
// --- Mode Labels ---
var hardMode = typeof hardMode !== "undefined" ? hardMode : false;
var extremeMode = typeof extremeMode !== "undefined" ? extremeMode : false;
var hardModeLabel = null;
var extremeModeLabel = null;
if (extremeMode) {
extremeModeLabel = new Text2('EXTREME MODE', {
size: 80,
fill: 0xFF00FF,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
extremeModeLabel.anchor.set(0.5, 0);
extremeModeLabel.x = GAME_WIDTH / 2;
extremeModeLabel.y = 40;
LK.gui.top.addChild(extremeModeLabel);
} else if (hardMode) {
hardModeLabel = new Text2('HARD MODE', {
size: 80,
fill: 0xFF2222,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
hardModeLabel.anchor.set(0.5, 0);
hardModeLabel.x = GAME_WIDTH / 2;
hardModeLabel.y = 40;
LK.gui.top.addChild(hardModeLabel);
}
// Removed red 'misses' text from the top GUI. Misses are now only shown in white below each target.
// --- Add Background ---
var background = LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: GAME_WIDTH,
height: GAME_HEIGHT
});
game.addChild(background);
// --- Lanes and Targets ---
var lanes = [];
var targets = [];
var peopleLeft = [];
var peopleRight = [];
var currentPeoplePerSide = 0; // Track current number of people per side
var lastScoreMilestone = 0; // Track last score milestone for adding people
var targetMissTexts = []; // Miss counters for each target
var laneContainers = []; // Containers to group lane elements together
for (var i = 0; i < NUM_LANES; i++) {
// Create a container for each lane to group all elements
var laneContainer = new Container();
var laneX = LANE_START_X + i * (LANE_WIDTH + LANE_SPACING);
laneContainer.x = laneX;
laneContainer.baseX = laneX; // Store base position for movement
game.addChild(laneContainer);
laneContainers.push(laneContainer);
// Lane background
var lane = LK.getAsset('lane', {
anchorX: 0.5,
anchorY: 0,
x: 0,
//{2r} // Relative to container
y: 0,
width: LANE_WIDTH,
height: LANE_HEIGHT
});
laneContainer.addChild(lane);
lanes.push(lane);
// Visually improved target: larger, more distinct, with a colored border effect
var target = LK.getAsset('target', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
//{2w} // Relative to container
y: TARGET_Y + 40,
//{1R} // Move target further back (higher y = lower on screen)
width: LANE_WIDTH + 40,
// Make target slightly wider for better visuals
height: TARGET_HEIGHT + 40 // Make target slightly taller for better visuals
});
laneContainer.addChild(target);
// Add a second, inner target for a "bullseye" effect
var innerTarget = LK.getAsset('target', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
//{2B} // Relative to container
y: TARGET_Y + 40,
//{1W} // Move inner target further back as well
width: LANE_WIDTH - 30,
height: TARGET_HEIGHT - 20
});
laneContainer.addChild(innerTarget);
// (removed: misses counter above target, now shown on the target itself)
// Add a misses counter directly ON the target (replaces label text with misses count)
var targetOnText = new Text2('0', {
size: 54,
fill: 0x3399FF // Blue (not black)
});
targetOnText.anchor.set(0.5, 0.5);
targetOnText.x = 0; // Relative to container
targetOnText.y = TARGET_Y + 40;
laneContainer.addChild(targetOnText);
// Store this as the main misses counter for this target (for updating)
targetMissTexts[i] = targetOnText;
// Store both for later effects
targets.push({
outer: target,
inner: innerTarget
});
}
// Place people only outside the lanes, never over the road/lane area
// --- Randomized, non-overlapping, not-in-center people placement ---
peopleLeft = [];
peopleRight = [];
var NUM_PEOPLE_PER_EDGE = 0; // Start with no people
var PERSON_RADIUS = 90; // Half of body width, for spacing
var OUTER_X_OFFSET = 120; // How far outside the lane edge to place people
// Allow people to go all the way to the screen edge, not just near the lanes
var FAR_LEFT_X_MIN = 60;
var FAR_LEFT_X_MAX = getLaneX(0) - LANE_WIDTH / 2 - OUTER_X_OFFSET - 10;
var FAR_RIGHT_X_MIN = getLaneX(NUM_LANES - 1) + LANE_WIDTH / 2 + OUTER_X_OFFSET + 10;
var FAR_RIGHT_X_MAX = GAME_WIDTH - 60;
var Y_MIN = 200; // Don't go too high
var Y_MAX = TARGET_Y - 60; // Don't go too low
var CENTER_EXCLUSION_X = GAME_WIDTH / 2 - 320; // Exclude a wide center band
var CENTER_EXCLUSION_WIDTH = 640; // Center band width to avoid
var MAX_ATTEMPTS = 40; // Max tries to find a non-overlapping spot
function isFarEnough(x, y, arr) {
for (var i = 0; i < arr.length; i++) {
var dx = x - arr[i].x;
var dy = y - arr[i].y;
if (Math.sqrt(dx * dx + dy * dy) < PERSON_RADIUS * 2.1) return false;
}
return true;
}
function isNotCenter(x) {
return !(x > CENTER_EXCLUSION_X && x < CENTER_EXCLUSION_X + CENTER_EXCLUSION_WIDTH);
}
// Check if position is not in drum area
function isNotDrumArea(x, y) {
var drumRadius = 220;
var leftDrumX = 200,
leftDrumY = GAME_HEIGHT / 2;
var rightDrumX = GAME_WIDTH - 200,
rightDrumY = GAME_HEIGHT / 2;
var distLeft = Math.sqrt((x - leftDrumX) * (x - leftDrumX) + (y - leftDrumY) * (y - leftDrumY));
var distRight = Math.sqrt((x - rightDrumX) * (x - rightDrumX) + (y - rightDrumY) * (y - rightDrumY));
return distLeft > drumRadius && distRight > drumRadius;
}
// Update popularity after recreating people
updatePopularity();
// Update initial popularity
updatePopularity();
// --- Song Data (Random Infinite Notes) ---
// Each note: {lane: 0-3, time: tick when note should reach target}
// We'll generate notes on the fly, at random lanes and random intervals
var bpm = 60;
var ticksPerBeat = 60 * 60 / bpm; // 60fps
var NOTE_TRAVEL_TICKS = 180; // 3 seconds at 60fps
// Infinite random note generator state
var nextNoteTick = 60; // When the next note should appear (in songTicks)
function getRandomLane() {
return Math.floor(Math.random() * NUM_LANES);
}
function getRandomInterval() {
// In extreme mode, spawn notes extremely frequently (interval is 0.12x to 0.35x of ticksPerBeat)
if (typeof extremeMode !== "undefined" && extremeMode) {
return Math.floor(ticksPerBeat * (0.12 + Math.random() * 0.23));
}
// In hard mode, spawn notes more frequently (interval is 0.3x to 1.0x of ticksPerBeat)
if (typeof hardMode !== "undefined" && hardMode) {
return Math.floor(ticksPerBeat * (0.3 + Math.random() * 0.7));
}
// Random interval between notes: 0.5x to 1.5x of ticksPerBeat
return Math.floor(ticksPerBeat * (0.5 + Math.random()));
}
// --- Helper Functions ---
function getLaneX(laneIdx) {
if (typeof laneContainers !== "undefined" && laneContainers[laneIdx]) {
return laneContainers[laneIdx].x;
}
return LANE_START_X + laneIdx * (LANE_WIDTH + LANE_SPACING);
}
// Start continuous random lane movement
function startLaneMovement() {
for (var i = 0; i < laneContainers.length; i++) {
moveLaneRandomly(laneContainers[i], i);
}
}
// Move a single lane to a random position
function moveLaneRandomly(laneContainer, index) {
if (!laneContainer || songEnded) return;
// Random offset from base position (-80 to +80 pixels)
var randomOffset = (Math.random() - 0.5) * 160;
var targetX = laneContainer.baseX + randomOffset;
// Random duration (3-5 seconds)
var duration = 3000 + Math.random() * 2000;
tween(laneContainer, {
x: targetX
}, {
duration: duration,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Continue moving after reaching target
moveLaneRandomly(laneContainer, index);
}
});
}
// --- Game Logic ---
// Start song/music
function startSong() {
if (songStarted) return;
songStarted = true;
LK.playMusic('song1');
songTicks = 0;
noteIndex = 0;
score = 0;
combo = 0;
maxCombo = 0;
misses = 0;
songEnded = false;
scoreTxt.setText('0');
comboTxt.setText('');
highScore = getHighScore();
highScoreTxt.setText('High Score: ' + highScore);
if (typeof bottomHighScoreTxt !== "undefined") {
bottomHighScoreTxt.setText('High Score: ' + highScore);
}
notes.length = 0;
// Reset per-target misses counters (on the target itself)
if (typeof targets !== "undefined" && typeof targetMissTexts !== "undefined") {
for (var i = 0; i < targetMissTexts.length; i++) {
if (targetMissTexts[i]) {
targetMissTexts[i].setText('0');
targetMissTexts[i].setStyle({
fill: 0x3399FF
});
}
if (targets[i]) targets[i].missCount = 0;
}
}
// Reset nextNoteTick and noteSpeedMultiplier to ensure smooth start
nextNoteTick = 60;
// Reset lane color flash state
laneColorFlashActive = false;
laneColorFlashStartTime = 0;
laneColorFlashTriggered = false;
// Reset drum state
drumsActive = true;
drumNotes.length = 0;
if (leftDrum) {
leftDrum.destroy();
leftDrum = null;
}
if (rightDrum) {
rightDrum.destroy();
rightDrum = null;
}
// Remove peopleLeft from game
if (typeof peopleLeft !== "undefined") {
for (var i = 0; i < peopleLeft.length; i++) {
if (peopleLeft[i] && typeof peopleLeft[i].destroy === "function") {
peopleLeft[i].destroy();
}
}
peopleLeft.length = 0;
}
// Remove peopleRight from game
if (typeof peopleRight !== "undefined") {
for (var i = 0; i < peopleRight.length; i++) {
if (peopleRight[i] && typeof peopleRight[i].destroy === "function") {
peopleRight[i].destroy();
}
}
peopleRight.length = 0;
}
// Re-create peopleLeft and peopleRight, but keep them away from drum asset positions
peopleLeft = [];
peopleRight = [];
currentPeoplePerSide = 0; // Reset to 0 people per side
lastScoreMilestone = 0; // Reset score milestone
var NUM_PEOPLE_PER_EDGE = 0; // Start with no people
var PERSON_RADIUS = 90;
var OUTER_X_OFFSET = 120;
var FAR_LEFT_X_MIN = 60;
var FAR_LEFT_X_MAX = getLaneX(0) - LANE_WIDTH / 2 - OUTER_X_OFFSET - 10;
var FAR_RIGHT_X_MIN = getLaneX(NUM_LANES - 1) + LANE_WIDTH / 2 + OUTER_X_OFFSET + 10;
var FAR_RIGHT_X_MAX = GAME_WIDTH - 60;
var Y_MIN = 200;
var Y_MAX = TARGET_Y - 60;
var CENTER_EXCLUSION_X = GAME_WIDTH / 2 - 320;
var CENTER_EXCLUSION_WIDTH = 640;
var MAX_ATTEMPTS = 40;
function isFarEnough(x, y, arr) {
for (var i = 0; i < arr.length; i++) {
var dx = x - arr[i].x;
var dy = y - arr[i].y;
if (Math.sqrt(dx * dx + dy * dy) < PERSON_RADIUS * 2.1) return false;
}
return true;
}
function isNotCenter(x) {
return !(x > CENTER_EXCLUSION_X && x < CENTER_EXCLUSION_X + CENTER_EXCLUSION_WIDTH);
}
// No people are created at the start
// Create left drum after people to ensure it appears in front
leftDrum = LK.getAsset('drumV2', {
anchorX: 0.5,
anchorY: 0.5,
x: 200,
y: GAME_HEIGHT / 2
});
game.addChild(leftDrum);
// Create right drum after people to ensure it appears in front
rightDrum = LK.getAsset('drumV2', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_WIDTH - 200,
y: GAME_HEIGHT / 2
});
game.addChild(rightDrum);
// Drums will be created after people to ensure they appear in front
// Set initial next drum note time based on mode: easy=10s, hard=5s, extreme=3s
if (typeof extremeMode !== "undefined" && extremeMode) {
// 3 seconds interval (180 ticks)
nextDrumNoteTime = songTicks + 180;
} else if (typeof hardMode !== "undefined" && hardMode) {
// 5 seconds interval (300 ticks)
nextDrumNoteTime = songTicks + 300;
} else {
// Easy mode or default: 10 seconds interval (600 ticks)
nextDrumNoteTime = songTicks + 600;
}
// Start lane movement animations
startLaneMovement();
if (typeof extremeMode !== "undefined" && extremeMode) {
noteSpeedMultiplier = 1.5; // Slower than before for extreme mode (spawn rate unchanged)
// Remove any previous mode labels
if (typeof hardModeLabel !== "undefined" && hardModeLabel && hardModeLabel.parent) {
hardModeLabel.destroy();
}
if (typeof extremeModeLabel !== "undefined" && extremeModeLabel && extremeModeLabel.parent) {
extremeModeLabel.destroy();
}
extremeModeLabel = new Text2('EXTREME MODE', {
size: 80,
fill: 0xFF00FF,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
extremeModeLabel.anchor.set(0.5, 0);
extremeModeLabel.x = GAME_WIDTH / 2;
extremeModeLabel.y = 40;
LK.gui.top.addChild(extremeModeLabel);
} else if (typeof hardMode !== "undefined" && hardMode) {
noteSpeedMultiplier = 1.7; // Reduced speed for hard mode
// Show Hard Mode label in gameplay
if (typeof hardModeLabel !== "undefined" && hardModeLabel && hardModeLabel.parent) {
hardModeLabel.destroy();
}
if (typeof extremeModeLabel !== "undefined" && extremeModeLabel && extremeModeLabel.parent) {
extremeModeLabel.destroy();
}
hardModeLabel = new Text2('HARD MODE', {
size: 80,
fill: 0xFF2222,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
hardModeLabel.anchor.set(0.5, 0);
hardModeLabel.x = GAME_WIDTH / 2;
hardModeLabel.y = 40;
LK.gui.top.addChild(hardModeLabel);
} else if (typeof easyMode !== "undefined" && easyMode) {
noteSpeedMultiplier = 1.0;
// Show Easy Mode label in gameplay
if (typeof hardModeLabel !== "undefined" && hardModeLabel && hardModeLabel.parent) {
hardModeLabel.destroy();
}
if (typeof extremeModeLabel !== "undefined" && extremeModeLabel && extremeModeLabel.parent) {
extremeModeLabel.destroy();
}
if (typeof easyModeLabel !== "undefined" && easyModeLabel && easyModeLabel.parent) {
easyModeLabel.destroy();
}
easyModeLabel = new Text2('EASY MODE', {
size: 80,
fill: 0x44FF44,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
easyModeLabel.anchor.set(0.5, 0);
easyModeLabel.x = GAME_WIDTH / 2;
easyModeLabel.y = 40;
LK.gui.top.addChild(easyModeLabel);
} else {
noteSpeedMultiplier = 1.0;
if (typeof hardModeLabel !== "undefined" && hardModeLabel && hardModeLabel.parent) {
hardModeLabel.destroy();
}
if (typeof extremeModeLabel !== "undefined" && extremeModeLabel && extremeModeLabel.parent) {
extremeModeLabel.destroy();
}
}
}
// End song/game
function endSong(win) {
if (songEnded) return;
songEnded = true;
LK.stopMusic();
// No win or game over, just stop music and mark as ended
}
// --- Input Handling ---
game.down = function (x, y, obj) {
if (menuActive) return; // Prevent gameplay input until menu is dismissed
// Only allow input if song is running
if (!songStarted || songEnded) return;
// Check drum hits first if drums are active
if (drumsActive) {
// Check left drum
if (leftDrum) {
var leftDrumLeft = leftDrum.x - 150;
var leftDrumRight = leftDrum.x + 150;
var leftDrumTop = leftDrum.y - 150;
var leftDrumBottom = leftDrum.y + 150;
if (x >= leftDrumLeft && x <= leftDrumRight && y >= leftDrumTop && y <= leftDrumBottom) {
// Hit left drum - check for drum notes
for (var d = drumNotes.length - 1; d >= 0; d--) {
var drumNote = drumNotes[d];
if (!drumNote.hit && drumNote.isLeft) {
drumNote.onHit();
score += 200;
scoreTxt.setText(score + '');
// Update high score
if (score > highScore) {
highScore = score;
setHighScore(highScore);
highScoreTxt.setText('High Score: ' + highScore);
if (typeof bottomHighScoreTxt !== "undefined") {
bottomHighScoreTxt.setText('High Score: ' + highScore);
}
}
LK.getSound('davul').play();
LK.getSound('tap').play();
// Flash drum
LK.effects.flashObject(leftDrum, 0xFFD700, 200);
// Add drum hit animation
tween.stop(leftDrum, {
scaleX: true,
scaleY: true
});
leftDrum.scaleX = 1;
leftDrum.scaleY = 1;
tween(leftDrum, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(leftDrum, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
// Add strong screen shake for drum hit
if (typeof game !== "undefined" && typeof tween !== "undefined") {
if (!game._isShaking) {
game._isShaking = true;
var originalGameX = game.x || 0;
var originalGameY = game.y || 0;
var shakeAmount = 200; // Always use consistent shake amount for drums
var shakeDuration = 300;
tween(game, {
x: originalGameX + (Math.random() - 0.5) * shakeAmount,
y: originalGameY + (Math.random() - 0.5) * shakeAmount
}, {
duration: shakeDuration,
onFinish: function onFinish() {
tween(game, {
x: originalGameX,
y: originalGameY
}, {
duration: shakeDuration,
onFinish: function onFinish() {
game._isShaking = false;
}
});
}
});
}
}
return;
}
}
}
}
// Check right drum
if (rightDrum) {
var rightDrumLeft = rightDrum.x - 150;
var rightDrumRight = rightDrum.x + 150;
var rightDrumTop = rightDrum.y - 150;
var rightDrumBottom = rightDrum.y + 150;
if (x >= rightDrumLeft && x <= rightDrumRight && y >= rightDrumTop && y <= rightDrumBottom) {
// Hit right drum - check for drum notes
for (var d = drumNotes.length - 1; d >= 0; d--) {
var drumNote = drumNotes[d];
if (!drumNote.hit && !drumNote.isLeft) {
drumNote.onHit();
score += 200;
scoreTxt.setText(score + '');
// Update high score
if (score > highScore) {
highScore = score;
setHighScore(highScore);
highScoreTxt.setText('High Score: ' + highScore);
if (typeof bottomHighScoreTxt !== "undefined") {
bottomHighScoreTxt.setText('High Score: ' + highScore);
}
}
LK.getSound('davul').play();
LK.getSound('tap').play();
// Flash drum
LK.effects.flashObject(rightDrum, 0xFFD700, 200);
// Add drum hit animation
tween.stop(rightDrum, {
scaleX: true,
scaleY: true
});
rightDrum.scaleX = 1;
rightDrum.scaleY = 1;
tween(rightDrum, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(rightDrum, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
// Add strong screen shake for drum hit
if (typeof game !== "undefined" && typeof tween !== "undefined") {
if (!game._isShaking) {
game._isShaking = true;
var originalGameX = game.x || 0;
var originalGameY = game.y || 0;
var shakeAmount = 200; // Always use consistent shake amount for drums
var shakeDuration = 300;
tween(game, {
x: originalGameX + (Math.random() - 0.5) * shakeAmount,
y: originalGameY + (Math.random() - 0.5) * shakeAmount
}, {
duration: shakeDuration,
onFinish: function onFinish() {
tween(game, {
x: originalGameX,
y: originalGameY
}, {
duration: shakeDuration,
onFinish: function onFinish() {
game._isShaking = false;
}
});
}
});
}
}
return;
}
}
}
}
}
// Pause people movement for 2 seconds (120 ticks) after a note is hit
if (typeof game._peopleMovePausedUntil === "undefined") game._peopleMovePausedUntil = 0;
game._peopleMovePausedUntil = songTicks + 120;
// Check if tap is on any note (from topmost to bottom)
var hit = false;
var _loop = function _loop() {
note = notes[i];
if (note.hit || note.missed) return 0; // continue
// Get note bounds
noteLeft = note.x - NOTE_WIDTH / 2;
noteRight = note.x + NOTE_WIDTH / 2;
noteTop = note.y - NOTE_HEIGHT / 2;
noteBottom = note.y + NOTE_HEIGHT / 2;
if (x >= noteLeft && x <= noteRight && y >= noteTop && y <= noteBottom) {
// Hit!
note.onHit();
hit = true;
score += 100;
combo += 1;
if (combo > maxCombo) maxCombo = combo;
scoreTxt.setText(score + '');
comboTxt.setText(combo > 1 ? combo + ' Combo!' : '');
// High score logic
if (score > highScore) {
highScore = score;
setHighScore(highScore);
highScoreTxt.setText('High Score: ' + highScore);
if (typeof bottomHighScoreTxt !== "undefined") {
bottomHighScoreTxt.setText('High Score: ' + highScore);
}
}
LK.getSound('tap').play();
// Make a random subset of people jump when any note is hit!
// Every 10 combo, make all people jump and apply a stronger shake
if (typeof peopleLeft !== "undefined" && typeof peopleRight !== "undefined") {
if (combo > 0 && combo % 50 === 0) {
// --- MASSIVE SHAKE for every 50 combo ---
// All people jump!
for (var iAll = 0; iAll < peopleLeft.length; iAll++) {
if (peopleLeft[iAll]) peopleLeft[iAll].jump();
}
for (var iAll = 0; iAll < peopleRight.length; iAll++) {
if (peopleRight[iAll]) peopleRight[iAll].jump();
}
// Massive shake: override game.x/y with a huge shake
if (typeof game !== "undefined" && typeof tween !== "undefined") {
if (!game._isShaking) {
game._isShaking = true;
var originalGameX = game.x || 0;
var originalGameY = game.y || 0;
var shakeAmount = 420; // extremely strong shake
var shakeDuration = 420;
tween(game, {
x: originalGameX + (Math.random() - 0.5) * shakeAmount,
y: originalGameY + (Math.random() - 0.5) * shakeAmount
}, {
duration: shakeDuration,
onFinish: function onFinish() {
tween(game, {
x: originalGameX,
y: originalGameY
}, {
duration: shakeDuration,
onFinish: function onFinish() {
game._isShaking = false;
}
});
}
});
}
}
} else if (combo > 0 && combo % 10 === 0) {
// All people jump!
for (var iAll = 0; iAll < peopleLeft.length; iAll++) {
if (peopleLeft[iAll]) peopleLeft[iAll].jump();
}
for (var iAll = 0; iAll < peopleRight.length; iAll++) {
if (peopleRight[iAll]) peopleRight[iAll].jump();
}
// Stronger shake: override game.x/y with a bigger shake
if (typeof game !== "undefined" && typeof tween !== "undefined") {
if (!game._isShaking) {
game._isShaking = true;
var originalGameX = game.x || 0;
var originalGameY = game.y || 0;
var shakeAmount = 60; // much stronger shake
var shakeDuration = 120;
tween(game, {
x: originalGameX + (Math.random() - 0.5) * shakeAmount,
y: originalGameY + (Math.random() - 0.5) * shakeAmount
}, {
duration: shakeDuration,
onFinish: function onFinish() {
tween(game, {
x: originalGameX,
y: originalGameY
}, {
duration: shakeDuration,
onFinish: function onFinish() {
game._isShaking = false;
}
});
}
});
}
}
} else {
// Helper to get unique random indices
var getRandomIndices = function getRandomIndices(arrLen, count) {
var indices = [];
var used = [];
while (indices.length < count && indices.length < arrLen) {
var idx = Math.floor(Math.random() * arrLen);
if (!used[idx]) {
indices.push(idx);
used[idx] = true;
}
}
return indices;
};
// How many people to jump per side? 1-3 random per side
leftJumpCount = 1 + Math.floor(Math.random() * 3);
rightJumpCount = 1 + Math.floor(Math.random() * 3);
leftIndices = getRandomIndices(peopleLeft.length, leftJumpCount);
rightIndices = getRandomIndices(peopleRight.length, rightJumpCount);
for (j = 0; j < leftIndices.length; j++) {
idx = leftIndices[j];
if (peopleLeft[idx]) peopleLeft[idx].jump();
}
for (j = 0; j < rightIndices.length; j++) {
idx = rightIndices[j];
if (peopleRight[idx]) peopleRight[idx].jump();
}
}
}
// Show floating feedback text based on combo
feedbackText = '';
if (combo >= 30) {
feedbackText = 'PERFECT!';
} else if (combo >= 15) {
feedbackText = 'GREAT!';
} else if (combo >= 5) {
feedbackText = 'GOOD!';
}
if (feedbackText) {
fbTxt = new Text2(feedbackText, {
size: 120,
fill: 0xFFD700,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
fbTxt.anchor.set(0.5, 0.5);
fbTxt.x = GAME_WIDTH / 2;
fbTxt.y = GAME_HEIGHT / 2 - 200;
fbTxt.alpha = 1;
game.addChild(fbTxt);
tween(fbTxt, {
y: fbTxt.y - 120,
alpha: 0
}, {
duration: 700,
easing: tween.easeOut,
onFinish: function onFinish() {
fbTxt.destroy();
}
});
}
return 1; // break
}
},
note,
noteLeft,
noteRight,
noteTop,
noteBottom,
leftJumpCount,
rightJumpCount,
leftIndices,
rightIndices,
j,
idx,
j,
idx,
feedbackText,
fbTxt,
_ret;
for (var i = notes.length - 1; i >= 0; i--) {
_ret = _loop();
if (_ret === 0) continue;
if (_ret === 1) break;
}
if (!hit) {
// Missed tap (no note hit)
combo = 0;
comboTxt.setText('');
// No misses for tap misses, no flash
}
};
// --- Main Game Loop ---
game.update = function () {
if (!songStarted || songEnded) return;
songTicks += 1;
// Create drums at game start (handled in startSong)
// Spawn drum notes
if (drumsActive && songTicks >= nextDrumNoteTime) {
var drumNote = new DrumNote();
drumNote.spawnTime = songTicks;
// Randomly choose left or right drum
drumNote.isLeft = Math.random() < 0.5;
if (drumNote.isLeft) {
drumNote.x = leftDrum.x;
drumNote.y = leftDrum.y;
} else {
drumNote.x = rightDrum.x;
drumNote.y = rightDrum.y;
}
drumNotes.push(drumNote);
game.addChild(drumNote);
// Schedule next drum note based on mode: easy=10s, hard=5s, extreme=3s
if (typeof extremeMode !== "undefined" && extremeMode) {
// 3 seconds interval (180 ticks)
nextDrumNoteTime = songTicks + 180;
} else if (typeof hardMode !== "undefined" && hardMode) {
// 5 seconds interval (300 ticks)
nextDrumNoteTime = songTicks + 300;
} else {
// Easy mode or default: 10 seconds interval (600 ticks)
nextDrumNoteTime = songTicks + 600;
}
}
// Update drum notes
for (var d = drumNotes.length - 1; d >= 0; d--) {
var drumNote = drumNotes[d];
drumNote.update();
if (drumNote.destroyed) {
drumNotes.splice(d, 1);
}
}
// Check if all people are jumping and change lane colors
if (typeof peopleLeft !== "undefined" && typeof peopleRight !== "undefined") {
var allJumping = true;
// Check if all left people are jumping
for (var i = 0; i < peopleLeft.length; i++) {
if (peopleLeft[i] && !peopleLeft[i].isJumping) {
allJumping = false;
break;
}
}
// Check if all right people are jumping
if (allJumping) {
for (var i = 0; i < peopleRight.length; i++) {
if (peopleRight[i] && !peopleRight[i].isJumping) {
allJumping = false;
break;
}
}
}
// If all people are jumping, change lane colors randomly
if (allJumping && peopleLeft.length > 0 && peopleRight.length > 0) {
if (!game._lastAllJumping) {
// This is the moment when all started jumping
var colors = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF, 0xFFA500, 0xFF1493, 0x32CD32, 0x8A2BE2];
for (var l = 0; l < lanes.length; l++) {
if (lanes[l]) {
var randomColor = colors[Math.floor(Math.random() * colors.length)];
lanes[l].tint = randomColor;
}
}
}
game._lastAllJumping = true;
} else {
game._lastAllJumping = false;
}
}
// Gradually increase noteSpeedMultiplier (very slow ramp, e.g. +0.0002 per tick)
// In normal mode, do NOT increase speed faster after 5000 points
if (noteSpeedMultiplier < 2.0) {
noteSpeedMultiplier += 0.0002;
if (noteSpeedMultiplier > 2.0) noteSpeedMultiplier = 2.0;
}
// Spawn notes as needed (random, infinite)
// After 5000 points, do not increase note spawn count; only speed increases
var notesToSpawn = 1;
// No change to notesToSpawn after 5000 points
while (songTicks >= nextNoteTick - NOTE_TRAVEL_TICKS) {
for (var spawnIdx = 0; spawnIdx < notesToSpawn; spawnIdx++) {
var lane = getRandomLane();
var noteTime = nextNoteTick;
var note = new Note();
note.setLane(lane);
// Spawn note at current lane container position
note.x = laneContainers[lane].x;
note.y = -NOTE_HEIGHT / 2;
note.speed = (TARGET_Y + NOTE_HEIGHT / 2) / NOTE_TRAVEL_TICKS * noteSpeedMultiplier;
note.time = noteTime;
note.spawned = true;
// Store reference to lane container so note can follow it
note.laneContainer = laneContainers[lane];
notes.push(note);
game.addChild(note);
}
// Schedule next note
nextNoteTick += getRandomInterval();
}
// Check if score reached 5000 for lane color flashing
if (score >= 5000 && !laneColorFlashTriggered) {
laneColorFlashTriggered = true;
laneColorFlashActive = true;
laneColorFlashStartTime = songTicks;
}
// Handle rapid lane color changes
if (laneColorFlashActive) {
var flashDuration = 600; // 10 seconds at 60fps
var elapsed = songTicks - laneColorFlashStartTime;
if (elapsed < flashDuration) {
// Change colors very rapidly (every 3 frames = 20 times per second)
if (songTicks % 3 === 0) {
var colors = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF, 0xFFA500, 0xFF1493, 0x32CD32, 0x8A2BE2, 0xFFD700, 0x4B0082, 0x7FFF00, 0xDC143C, 0x00CED1];
for (var l = 0; l < lanes.length; l++) {
if (lanes[l]) {
var randomColor = colors[Math.floor(Math.random() * colors.length)];
tween(lanes[l], {
tint: randomColor
}, {
duration: 50,
easing: tween.linear
});
}
}
}
} else {
// Reset to original color after 10 seconds
laneColorFlashActive = false;
for (var l = 0; l < lanes.length; l++) {
if (lanes[l]) {
tween(lanes[l], {
tint: 0x222222
}, {
duration: 300,
easing: tween.easeOut
});
}
}
}
}
// Add people every 500 points, up to 5 per side (10 total)
if (score >= lastScoreMilestone + 500 && currentPeoplePerSide < 5) {
lastScoreMilestone = Math.floor(score / 500) * 500;
currentPeoplePerSide++;
// Add one person to left side
var leftPerson = new People();
var attempts = 0;
var px, py;
do {
py = Y_MIN + Math.random() * (Y_MAX - Y_MIN);
px = FAR_LEFT_X_MIN + Math.random() * (FAR_LEFT_X_MAX - FAR_LEFT_X_MIN);
attempts++;
} while ((!isFarEnough(px, py, peopleLeft) || !isNotCenter(px) || !isNotDrumArea(px, py)) && attempts < MAX_ATTEMPTS);
leftPerson.x = px;
leftPerson.y = py;
game.addChild(leftPerson);
peopleLeft.push(leftPerson);
// Add one person to right side
var rightPerson = new People();
attempts = 0;
do {
py = Y_MIN + Math.random() * (Y_MAX - Y_MIN);
px = FAR_RIGHT_X_MIN + Math.random() * (FAR_RIGHT_X_MAX - FAR_RIGHT_X_MIN);
attempts++;
} while ((!isFarEnough(px, py, peopleRight) || !isNotCenter(px) || !isNotDrumArea(px, py)) && attempts < MAX_ATTEMPTS);
rightPerson.x = px;
rightPerson.y = py;
game.addChild(rightPerson);
peopleRight.push(rightPerson);
// Update popularity when new people are added
updatePopularity();
}
// Update notes
for (var i = notes.length - 1; i >= 0; i--) {
var note = notes[i];
note.update();
// If note reached target zone and not hit, mark as missed
if (!note.hit && !note.missed && note.y >= TARGET_Y + TARGET_HEIGHT / 2) {
note.onMiss();
combo = 0;
misses += 1;
comboTxt.setText('');
// Increment and show per-target misses in white above each target
if (!targets[note.lane].missCount) targets[note.lane].missCount = 0;
targets[note.lane].missCount += 1;
if (targetMissTexts && targetMissTexts[note.lane]) {
targetMissTexts[note.lane].setText(targets[note.lane].missCount + '');
targetMissTexts[note.lane].setStyle({
fill: 0x3399FF
});
}
// Decrease popularity by 2% and remove one person when a note is missed
// Remove one person randomly from either left or right side
var removedPerson = false;
if (Math.random() < 0.5 && peopleLeft.length > 0) {
// Remove from left side
var personToRemove = peopleLeft.pop();
if (personToRemove && personToRemove.destroy) {
personToRemove.destroy();
removedPerson = true;
}
} else if (peopleRight.length > 0) {
// Remove from right side
var personToRemove = peopleRight.pop();
if (personToRemove && personToRemove.destroy) {
personToRemove.destroy();
removedPerson = true;
}
}
// Update popularity after removing person
if (removedPerson) {
updatePopularity();
}
LK.getSound('miss').play({
fade: {
start: 1,
end: 0,
duration: 1000
}
});
LK.effects.flashObject(targets[note.lane].outer, 0xff0000, 200);
LK.effects.flashObject(targets[note.lane].inner, 0xff6666, 200);
if (misses >= maxMisses) {
// No game over, just keep going
}
// Check if any target's miss count exceeds 10, trigger game over
for (var t = 0; t < targets.length; t++) {
if (targets[t].missCount && targets[t].missCount > 10) {
LK.showGameOver();
return;
}
}
}
// Remove destroyed notes
if (note.destroyed) {
notes.splice(i, 1);
}
}
// --- Move people to new random positions every 2 seconds ---
if (typeof game._lastPeopleMoveTick === "undefined") game._lastPeopleMoveTick = 0;
if (typeof game._peopleMovePausedUntil === "undefined") game._peopleMovePausedUntil = 0;
// Only move people if not in the 2s pause after a note hit
if (songTicks - game._lastPeopleMoveTick > 120 && songTicks >= game._peopleMovePausedUntil) {
// Helper to move a person to a new random position (non-overlapping, not center)
var movePerson = function movePerson(person, arr, isLeft) {
var attempts = 0;
var px, py;
do {
py = Y_MIN + Math.random() * (Y_MAX - Y_MIN);
if (isLeft) {
// Move anywhere in the left empty area
px = FAR_LEFT_X_MIN + Math.random() * (FAR_LEFT_X_MAX - FAR_LEFT_X_MIN);
} else {
// Move anywhere in the right empty area
px = FAR_RIGHT_X_MIN + Math.random() * (FAR_RIGHT_X_MAX - FAR_RIGHT_X_MIN);
}
attempts++;
} while ((!isFarEnough(px, py, arr) || !isNotCenter(px)) && attempts < MAX_ATTEMPTS);
// Animate to new position (move very slowly, never teleport)
tween(person, {
x: px,
y: py
}, {
duration: 3200,
// much slower movement (3.2 seconds)
easing: tween.easeInOut
});
person.baseY = py;
}; // Move left people
// every 2 seconds at 60fps
game._lastPeopleMoveTick = songTicks;
for (var i = 0; i < peopleLeft.length; i++) {
movePerson(peopleLeft[i], peopleLeft, true);
}
// Move right people
for (var i = 0; i < peopleRight.length; i++) {
movePerson(peopleRight[i], peopleRight, false);
}
}
// No win condition, keep game running
};
// --- Start Menu Overlay ---
var startMenuOverlay = new Container();
var overlayBg = LK.getAsset('lane', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_WIDTH / 2,
y: GAME_HEIGHT / 2,
width: GAME_WIDTH,
height: GAME_HEIGHT,
color: 0x000000
});
overlayBg.alpha = 0.85;
startMenuOverlay.addChild(overlayBg);
var titleText = new Text2('BEAT TAPPER', {
size: 120,
fill: 0xFFD700,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = GAME_WIDTH / 2;
titleText.y = GAME_HEIGHT / 2 - 400;
startMenuOverlay.addChild(titleText);
var tapToStartText = new Text2('Tap to Start', {
size: 90,
fill: 0xFFFFFF
});
tapToStartText.anchor.set(0.5, 0.5);
tapToStartText.x = GAME_WIDTH / 2;
// Move even higher above instructions (was -60, now -120 for more space above instructions)
tapToStartText.y = GAME_HEIGHT / 2 - 120;
// Create a box behind the text
var tapBoxPaddingX = 60;
var tapBoxPaddingY = 30;
var tapBox = LK.getAsset('lane', {
anchorX: 0.5,
anchorY: 0.5,
x: tapToStartText.x,
y: tapToStartText.y,
width: tapToStartText.width + tapBoxPaddingX,
height: tapToStartText.height + tapBoxPaddingY,
color: 0xFFFFFF
});
tapBox.alpha = 0.18;
startMenuOverlay.addChild(tapBox);
startMenuOverlay.addChild(tapToStartText);
var instructionsText = new Text2("How to Play:\n\n" + "• Tap the falling notes when they reach the targets.\n" + "• Don't let too many notes pass the targets!\n" + "• Each lane shows your misses. If any lane gets more than 10 misses, it's game over.\n" + "• Try to get the highest combo and score!", {
size: 44,
fill: 0xFFFFFF,
align: "center"
});
instructionsText.anchor.set(0.5, 0.5);
instructionsText.x = GAME_WIDTH / 2;
instructionsText.y = GAME_HEIGHT / 2 + 120;
startMenuOverlay.addChild(instructionsText);
var highScoreMenuText = new Text2('High Score: ' + getHighScore(), {
size: 70,
fill: 0xFFD700,
align: "center"
});
highScoreMenuText.anchor.set(0.5, 0.5);
highScoreMenuText.x = GAME_WIDTH / 2;
highScoreMenuText.y = GAME_HEIGHT / 2 + 320;
startMenuOverlay.addChild(highScoreMenuText);
// Add drum instructions at the very bottom of the menu overlay
var drumInstructionsText = new Text2("At the start of the game, two drums (left and right) will appear.\n" + "Drum notes will randomly appear above the drums.\n" + "Tap the drum note within 5 seconds to score points!\n" + "If you don't tap the drum note in 5 seconds, you lose.", {
size: 40,
fill: 0xFFD700,
align: "center"
});
drumInstructionsText.anchor.set(0.5, 1);
drumInstructionsText.x = GAME_WIDTH / 2;
drumInstructionsText.y = GAME_HEIGHT - 60;
startMenuOverlay.addChild(drumInstructionsText);
// --- Easy Mode Button ---
var easyModeBtn = new Text2('Easy Mode', {
size: 80,
fill: 0x44FF44,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
easyModeBtn.anchor.set(0.5, 0.5);
easyModeBtn.x = GAME_WIDTH / 2;
easyModeBtn.y = GAME_HEIGHT / 2 + 480;
easyModeBtn.alpha = 0.92;
easyModeBtn._isEasyModeBtn = true;
startMenuOverlay.addChild(easyModeBtn);
// --- Hard Mode Button ---
var hardModeBtn = new Text2('Hard Mode', {
size: 80,
fill: 0xFF2222,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
hardModeBtn.anchor.set(0.5, 0.5);
hardModeBtn.x = GAME_WIDTH / 2;
hardModeBtn.y = GAME_HEIGHT / 2 + 620;
hardModeBtn.alpha = 0.92;
hardModeBtn._isHardModeBtn = true;
startMenuOverlay.addChild(hardModeBtn);
// --- Extreme Mode Button ---
var extremeModeBtn = new Text2('Extreme Mode', {
size: 80,
fill: 0xFF00FF,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
extremeModeBtn.anchor.set(0.5, 0.5);
extremeModeBtn.x = GAME_WIDTH / 2;
extremeModeBtn.y = GAME_HEIGHT / 2 + 760;
extremeModeBtn.alpha = 0.92;
extremeModeBtn._isExtremeModeBtn = true;
startMenuOverlay.addChild(extremeModeBtn);
game.addChild(startMenuOverlay);
var menuActive = true;
var hardMode = false;
var extremeMode = false;
var easyMode = true; // Default to easy mode
game.down = function (x, y, obj) {
if (menuActive) {
// Check if tap is on Easy Mode button
var easyBtnLeft = easyModeBtn.x - easyModeBtn.width / 2;
var easyBtnRight = easyModeBtn.x + easyModeBtn.width / 2;
var easyBtnTop = easyModeBtn.y - easyModeBtn.height / 2;
var easyBtnBottom = easyModeBtn.y + easyModeBtn.height / 2;
if (x >= easyBtnLeft && x <= easyBtnRight && y >= easyBtnTop && y <= easyBtnBottom) {
// Toggle easy mode ON and visually indicate
easyMode = true;
hardMode = false;
extremeMode = false;
easyModeBtn.setText('Easy Mode: ON');
easyModeBtn.fill = 0x66FF66;
easyModeBtn.alpha = 1;
hardModeBtn.setText('Hard Mode');
hardModeBtn.fill = 0xFF2222;
hardModeBtn.alpha = 0.92;
extremeModeBtn.setText('Extreme Mode');
extremeModeBtn.fill = 0xFF00FF;
extremeModeBtn.alpha = 0.92;
// Update high score in menu overlay
if (typeof highScoreMenuText !== "undefined") {
highScoreMenuText.setText('High Score: ' + getHighScore());
}
// Optionally, flash or animate
tween(easyModeBtn, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
yoyo: true,
repeat: 1,
onFinish: function onFinish() {
easyModeBtn.scaleX = 1;
easyModeBtn.scaleY = 1;
}
});
return;
}
// Check if tap is on Hard Mode button
var btnLeft = hardModeBtn.x - hardModeBtn.width / 2;
var btnRight = hardModeBtn.x + hardModeBtn.width / 2;
var btnTop = hardModeBtn.y - hardModeBtn.height / 2;
var btnBottom = hardModeBtn.y + hardModeBtn.height / 2;
if (x >= btnLeft && x <= btnRight && y >= btnTop && y <= btnBottom) {
// Toggle hard mode ON and visually indicate
hardMode = true;
easyMode = false;
extremeMode = false;
hardModeBtn.setText('Hard Mode: ON');
hardModeBtn.fill = 0xFF4444;
hardModeBtn.alpha = 1;
easyModeBtn.setText('Easy Mode');
easyModeBtn.fill = 0x44FF44;
easyModeBtn.alpha = 0.92;
extremeModeBtn.setText('Extreme Mode');
extremeModeBtn.fill = 0xFF00FF;
extremeModeBtn.alpha = 0.92;
// Update high score in menu overlay
if (typeof highScoreMenuText !== "undefined") {
highScoreMenuText.setText('High Score: ' + getHighScore());
}
// Optionally, flash or animate
tween(hardModeBtn, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
yoyo: true,
repeat: 1,
onFinish: function onFinish() {
hardModeBtn.scaleX = 1;
hardModeBtn.scaleY = 1;
}
});
return;
}
// Check if tap is on Extreme Mode button
var ebtnLeft = extremeModeBtn.x - extremeModeBtn.width / 2;
var ebtnRight = extremeModeBtn.x + extremeModeBtn.width / 2;
var ebtnTop = extremeModeBtn.y - extremeModeBtn.height / 2;
var ebtnBottom = extremeModeBtn.y + extremeModeBtn.height / 2;
if (x >= ebtnLeft && x <= ebtnRight && y >= ebtnTop && y <= ebtnBottom) {
// Toggle extreme mode ON and visually indicate
extremeMode = true;
hardMode = false;
easyMode = false;
extremeModeBtn.setText('Extreme Mode: ON');
extremeModeBtn.fill = 0xFF66FF;
extremeModeBtn.alpha = 1;
hardModeBtn.setText('Hard Mode');
hardModeBtn.fill = 0xFF2222;
hardModeBtn.alpha = 0.92;
easyModeBtn.setText('Easy Mode');
easyModeBtn.fill = 0x44FF44;
easyModeBtn.alpha = 0.92;
// Update high score in menu overlay
if (typeof highScoreMenuText !== "undefined") {
highScoreMenuText.setText('High Score: ' + getHighScore());
}
// Optionally, flash or animate
tween(extremeModeBtn, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
yoyo: true,
repeat: 1,
onFinish: function onFinish() {
extremeModeBtn.scaleX = 1;
extremeModeBtn.scaleY = 1;
}
});
return;
}
menuActive = false;
startMenuOverlay.destroy();
startSong();
// Restore original input handler for gameplay
game.down = function (x, y, obj) {
// Only allow input if song is running
if (!songStarted || songEnded) return;
// Check drum hits first if drums are active
if (drumsActive) {
// Check left drum
if (leftDrum) {
var leftDrumLeft = leftDrum.x - 150;
var leftDrumRight = leftDrum.x + 150;
var leftDrumTop = leftDrum.y - 150;
var leftDrumBottom = leftDrum.y + 150;
if (x >= leftDrumLeft && x <= leftDrumRight && y >= leftDrumTop && y <= leftDrumBottom) {
// Hit left drum - check for drum notes
for (var d = drumNotes.length - 1; d >= 0; d--) {
var drumNote = drumNotes[d];
if (!drumNote.hit && drumNote.isLeft) {
drumNote.onHit();
score += 200;
scoreTxt.setText(score + '');
// Update high score
if (score > highScore) {
highScore = score;
setHighScore(highScore);
highScoreTxt.setText('High Score: ' + highScore);
if (typeof bottomHighScoreTxt !== "undefined") {
bottomHighScoreTxt.setText('High Score: ' + highScore);
}
}
LK.getSound('davul').play();
LK.getSound('tap').play();
// Flash drum
LK.effects.flashObject(leftDrum, 0xFFD700, 200);
// Add drum hit animation
tween.stop(leftDrum, {
scaleX: true,
scaleY: true
});
leftDrum.scaleX = 1;
leftDrum.scaleY = 1;
tween(leftDrum, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(leftDrum, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
// Add strong screen shake for drum hit
if (typeof game !== "undefined" && typeof tween !== "undefined") {
if (!game._isShaking) {
game._isShaking = true;
var originalGameX = game.x || 0;
var originalGameY = game.y || 0;
var shakeAmount = 200; // Always use consistent shake amount for drums
var shakeDuration = 300;
tween(game, {
x: originalGameX + (Math.random() - 0.5) * shakeAmount,
y: originalGameY + (Math.random() - 0.5) * shakeAmount
}, {
duration: shakeDuration,
onFinish: function onFinish() {
tween(game, {
x: originalGameX,
y: originalGameY
}, {
duration: shakeDuration,
onFinish: function onFinish() {
game._isShaking = false;
}
});
}
});
}
}
return;
}
}
}
}
// Check right drum
if (rightDrum) {
var rightDrumLeft = rightDrum.x - 150;
var rightDrumRight = rightDrum.x + 150;
var rightDrumTop = rightDrum.y - 150;
var rightDrumBottom = rightDrum.y + 150;
if (x >= rightDrumLeft && x <= rightDrumRight && y >= rightDrumTop && y <= rightDrumBottom) {
// Hit right drum - check for drum notes
for (var d = drumNotes.length - 1; d >= 0; d--) {
var drumNote = drumNotes[d];
if (!drumNote.hit && !drumNote.isLeft) {
drumNote.onHit();
score += 200;
scoreTxt.setText(score + '');
// Update high score
if (score > highScore) {
highScore = score;
setHighScore(highScore);
highScoreTxt.setText('High Score: ' + highScore);
if (typeof bottomHighScoreTxt !== "undefined") {
bottomHighScoreTxt.setText('High Score: ' + highScore);
}
}
LK.getSound('davul').play();
LK.getSound('tap').play();
// Flash drum
LK.effects.flashObject(rightDrum, 0xFFD700, 200);
// Add drum hit animation
tween.stop(rightDrum, {
scaleX: true,
scaleY: true
});
rightDrum.scaleX = 1;
rightDrum.scaleY = 1;
tween(rightDrum, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(rightDrum, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
// Add strong screen shake for drum hit
if (typeof game !== "undefined" && typeof tween !== "undefined") {
if (!game._isShaking) {
game._isShaking = true;
var originalGameX = game.x || 0;
var originalGameY = game.y || 0;
var shakeAmount = 200; // Always use consistent shake amount for drums
var shakeDuration = 300;
tween(game, {
x: originalGameX + (Math.random() - 0.5) * shakeAmount,
y: originalGameY + (Math.random() - 0.5) * shakeAmount
}, {
duration: shakeDuration,
onFinish: function onFinish() {
tween(game, {
x: originalGameX,
y: originalGameY
}, {
duration: shakeDuration,
onFinish: function onFinish() {
game._isShaking = false;
}
});
}
});
}
}
return;
}
}
}
}
}
// Pause people movement for 2 seconds (120 ticks) after a note is hit
if (typeof game._peopleMovePausedUntil === "undefined") game._peopleMovePausedUntil = 0;
game._peopleMovePausedUntil = songTicks + 120;
// Check if tap is on any note (from topmost to bottom)
var hit = false;
var _loop = function _loop() {
note = notes[i];
if (note.hit || note.missed) return 0; // continue
// Get note bounds
noteLeft = note.x - NOTE_WIDTH / 2;
noteRight = note.x + NOTE_WIDTH / 2;
noteTop = note.y - NOTE_HEIGHT / 2;
noteBottom = note.y + NOTE_HEIGHT / 2;
if (x >= noteLeft && x <= noteRight && y >= noteTop && y <= noteBottom) {
// Hit!
note.onHit();
hit = true;
score += 100;
combo += 1;
if (combo > maxCombo) maxCombo = combo;
scoreTxt.setText(score + '');
comboTxt.setText(combo > 1 ? combo + ' Combo!' : '');
// High score logic
if (score > highScore) {
highScore = score;
setHighScore(highScore);
highScoreTxt.setText('High Score: ' + highScore);
if (typeof bottomHighScoreTxt !== "undefined") {
bottomHighScoreTxt.setText('High Score: ' + highScore);
}
}
LK.getSound('tap').play();
// Make a random subset of people jump when any note is hit!
// Every 10 combo, make all people jump and apply a stronger shake
if (typeof peopleLeft !== "undefined" && typeof peopleRight !== "undefined") {
if (combo > 0 && combo % 50 === 0) {
// --- MASSIVE SHAKE for every 50 combo ---
// All people jump!
for (var iAll = 0; iAll < peopleLeft.length; iAll++) {
if (peopleLeft[iAll]) peopleLeft[iAll].jump();
}
for (var iAll = 0; iAll < peopleRight.length; iAll++) {
if (peopleRight[iAll]) peopleRight[iAll].jump();
}
// Massive shake: override game.x/y with a huge shake
if (typeof game !== "undefined" && typeof tween !== "undefined") {
if (!game._isShaking) {
game._isShaking = true;
var originalGameX = game.x || 0;
var originalGameY = game.y || 0;
var shakeAmount = 420; // extremely strong shake
var shakeDuration = 420;
tween(game, {
x: originalGameX + (Math.random() - 0.5) * shakeAmount,
y: originalGameY + (Math.random() - 0.5) * shakeAmount
}, {
duration: shakeDuration,
onFinish: function onFinish() {
tween(game, {
x: originalGameX,
y: originalGameY
}, {
duration: shakeDuration,
onFinish: function onFinish() {
game._isShaking = false;
}
});
}
});
}
}
} else if (combo > 0 && combo % 10 === 0) {
// All people jump!
for (var iAll = 0; iAll < peopleLeft.length; iAll++) {
if (peopleLeft[iAll]) peopleLeft[iAll].jump();
}
for (var iAll = 0; iAll < peopleRight.length; iAll++) {
if (peopleRight[iAll]) peopleRight[iAll].jump();
}
// Stronger shake: override game.x/y with a bigger shake
if (typeof game !== "undefined" && typeof tween !== "undefined") {
if (!game._isShaking) {
game._isShaking = true;
var originalGameX = game.x || 0;
var originalGameY = game.y || 0;
var shakeAmount = 60; // much stronger shake
var shakeDuration = 120;
tween(game, {
x: originalGameX + (Math.random() - 0.5) * shakeAmount,
y: originalGameY + (Math.random() - 0.5) * shakeAmount
}, {
duration: shakeDuration,
onFinish: function onFinish() {
tween(game, {
x: originalGameX,
y: originalGameY
}, {
duration: shakeDuration,
onFinish: function onFinish() {
game._isShaking = false;
}
});
}
});
}
}
} else {
// Helper to get unique random indices
var getRandomIndices = function getRandomIndices(arrLen, count) {
var indices = [];
var used = [];
while (indices.length < count && indices.length < arrLen) {
var idx = Math.floor(Math.random() * arrLen);
if (!used[idx]) {
indices.push(idx);
used[idx] = true;
}
}
return indices;
};
// How many people to jump per side? 1-3 random per side
leftJumpCount = 1 + Math.floor(Math.random() * 3);
rightJumpCount = 1 + Math.floor(Math.random() * 3);
leftIndices = getRandomIndices(peopleLeft.length, leftJumpCount);
rightIndices = getRandomIndices(peopleRight.length, rightJumpCount);
for (j = 0; j < leftIndices.length; j++) {
idx = leftIndices[j];
if (peopleLeft[idx]) peopleLeft[idx].jump();
}
for (j = 0; j < rightIndices.length; j++) {
idx = rightIndices[j];
if (peopleRight[idx]) peopleRight[idx].jump();
}
}
}
// Show floating feedback text based on combo
feedbackText = '';
if (combo >= 30) {
feedbackText = 'PERFECT!';
} else if (combo >= 15) {
feedbackText = 'GREAT!';
} else if (combo >= 5) {
feedbackText = 'GOOD!';
}
if (feedbackText) {
fbTxt = new Text2(feedbackText, {
size: 120,
fill: 0xFFD700,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
fbTxt.anchor.set(0.5, 0.5);
fbTxt.x = GAME_WIDTH / 2;
fbTxt.y = GAME_HEIGHT / 2 - 200;
fbTxt.alpha = 1;
game.addChild(fbTxt);
tween(fbTxt, {
y: fbTxt.y - 120,
alpha: 0
}, {
duration: 700,
easing: tween.easeOut,
onFinish: function onFinish() {
fbTxt.destroy();
}
});
}
return 1; // break
}
},
note,
noteLeft,
noteRight,
noteTop,
noteBottom,
leftJumpCount,
rightJumpCount,
leftIndices,
rightIndices,
j,
idx,
j,
idx,
feedbackText,
fbTxt,
_ret;
for (var i = notes.length - 1; i >= 0; i--) {
_ret = _loop();
if (_ret === 0) continue;
if (_ret === 1) break;
}
if (!hit) {
// Missed tap (no note hit)
combo = 0;
comboTxt.setText('');
// No misses for tap misses, no flash
}
};
}
};
// Do not start the song immediately; wait for user tap
// startSong();;