/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { highScore: 0 }); /**** * Classes ****/ // 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; }; // Called when note is hit self.onHit = function () { if (self.hit || self.missed) return; self.hit = true; // 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; // Attach a body (ellipse, larger, lower color) var body = self.attachAsset('centerCircle', { anchorX: 0.5, anchorY: 1, width: 100, height: 180, y: 0 }); // 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 ****/ // --- UI Elements --- // 4 note lanes, each with a different color for clarity // Notes are destroyed by tapping them directly before they reach the white target area at the bottom var NUM_LANES = 4; var LANE_WIDTH = 200; 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; // --- 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; // --- 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 targetMissTexts = []; // Miss counters for each target for (var i = 0; i < NUM_LANES; i++) { // Lane background var laneX = LANE_START_X + i * (LANE_WIDTH + LANE_SPACING); var lane = LK.getAsset('lane', { anchorX: 0.5, anchorY: 0, x: laneX, y: 0, width: LANE_WIDTH, height: LANE_HEIGHT }); game.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: laneX, 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 }); game.addChild(target); // Add a second, inner target for a "bullseye" effect var innerTarget = LK.getAsset('target', { anchorX: 0.5, anchorY: 0.5, x: laneX, y: TARGET_Y + 40, //{1W} // Move inner target further back as well width: LANE_WIDTH - 30, height: TARGET_HEIGHT - 20 }); game.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 = laneX; targetOnText.y = TARGET_Y + 40; game.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 = 16; // Increased people per side for more crowd 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); } // Left edge: randomize y, keep x always outside leftmost lane, avoid center, avoid overlap for (var i = 0; i < NUM_PEOPLE_PER_EDGE; i++) { var leftPerson = new People(); var attempts = 0; var px, py; do { py = Y_MIN + Math.random() * (Y_MAX - Y_MIN); // Randomize x across the full left empty area px = FAR_LEFT_X_MIN + Math.random() * (FAR_LEFT_X_MAX - FAR_LEFT_X_MIN); attempts++; } while ((!isFarEnough(px, py, peopleLeft) || !isNotCenter(px)) && attempts < MAX_ATTEMPTS); leftPerson.x = px; leftPerson.y = py; game.addChild(leftPerson); peopleLeft.push(leftPerson); } // Right edge: randomize y, keep x always outside rightmost lane, avoid center, avoid overlap for (var i = 0; i < NUM_PEOPLE_PER_EDGE; i++) { var rightPerson = new People(); var attempts = 0; var px, py; do { py = Y_MIN + Math.random() * (Y_MAX - Y_MIN); // Randomize x across the full right empty area px = FAR_RIGHT_X_MIN + Math.random() * (FAR_RIGHT_X_MAX - FAR_RIGHT_X_MIN); attempts++; } while ((!isFarEnough(px, py, peopleRight) || !isNotCenter(px)) && attempts < MAX_ATTEMPTS); rightPerson.x = px; rightPerson.y = py; game.addChild(rightPerson); peopleRight.push(rightPerson); } // --- 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) { return LANE_START_X + laneIdx * (LANE_WIDTH + LANE_SPACING); } // --- 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); 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; 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 = 2.0; // Much faster 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; // 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); } 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; // 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); note.x = getLaneX(lane); note.y = -NOTE_HEIGHT / 2; note.speed = (TARGET_Y + NOTE_HEIGHT / 2) / NOTE_TRAVEL_TICKS * noteSpeedMultiplier; note.time = noteTime; note.spawned = true; notes.push(note); game.addChild(note); } // Schedule next note nextNoteTick += getRandomInterval(); } // 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 }); } 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); // --- 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; // 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); } 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
****/
// 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;
};
// Called when note is hit
self.onHit = function () {
if (self.hit || self.missed) return;
self.hit = true;
// 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;
// Attach a body (ellipse, larger, lower color)
var body = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 1,
width: 100,
height: 180,
y: 0
});
// 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
****/
// --- UI Elements ---
// 4 note lanes, each with a different color for clarity
// Notes are destroyed by tapping them directly before they reach the white target area at the bottom
var NUM_LANES = 4;
var LANE_WIDTH = 200;
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;
// --- 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;
// --- 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 targetMissTexts = []; // Miss counters for each target
for (var i = 0; i < NUM_LANES; i++) {
// Lane background
var laneX = LANE_START_X + i * (LANE_WIDTH + LANE_SPACING);
var lane = LK.getAsset('lane', {
anchorX: 0.5,
anchorY: 0,
x: laneX,
y: 0,
width: LANE_WIDTH,
height: LANE_HEIGHT
});
game.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: laneX,
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
});
game.addChild(target);
// Add a second, inner target for a "bullseye" effect
var innerTarget = LK.getAsset('target', {
anchorX: 0.5,
anchorY: 0.5,
x: laneX,
y: TARGET_Y + 40,
//{1W} // Move inner target further back as well
width: LANE_WIDTH - 30,
height: TARGET_HEIGHT - 20
});
game.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 = laneX;
targetOnText.y = TARGET_Y + 40;
game.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 = 16; // Increased people per side for more crowd
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);
}
// Left edge: randomize y, keep x always outside leftmost lane, avoid center, avoid overlap
for (var i = 0; i < NUM_PEOPLE_PER_EDGE; i++) {
var leftPerson = new People();
var attempts = 0;
var px, py;
do {
py = Y_MIN + Math.random() * (Y_MAX - Y_MIN);
// Randomize x across the full left empty area
px = FAR_LEFT_X_MIN + Math.random() * (FAR_LEFT_X_MAX - FAR_LEFT_X_MIN);
attempts++;
} while ((!isFarEnough(px, py, peopleLeft) || !isNotCenter(px)) && attempts < MAX_ATTEMPTS);
leftPerson.x = px;
leftPerson.y = py;
game.addChild(leftPerson);
peopleLeft.push(leftPerson);
}
// Right edge: randomize y, keep x always outside rightmost lane, avoid center, avoid overlap
for (var i = 0; i < NUM_PEOPLE_PER_EDGE; i++) {
var rightPerson = new People();
var attempts = 0;
var px, py;
do {
py = Y_MIN + Math.random() * (Y_MAX - Y_MIN);
// Randomize x across the full right empty area
px = FAR_RIGHT_X_MIN + Math.random() * (FAR_RIGHT_X_MAX - FAR_RIGHT_X_MIN);
attempts++;
} while ((!isFarEnough(px, py, peopleRight) || !isNotCenter(px)) && attempts < MAX_ATTEMPTS);
rightPerson.x = px;
rightPerson.y = py;
game.addChild(rightPerson);
peopleRight.push(rightPerson);
}
// --- 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) {
return LANE_START_X + laneIdx * (LANE_WIDTH + LANE_SPACING);
}
// --- 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);
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;
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 = 2.0; // Much faster 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;
// 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);
}
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;
// 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);
note.x = getLaneX(lane);
note.y = -NOTE_HEIGHT / 2;
note.speed = (TARGET_Y + NOTE_HEIGHT / 2) / NOTE_TRAVEL_TICKS * noteSpeedMultiplier;
note.time = noteTime;
note.spawned = true;
notes.push(note);
game.addChild(note);
}
// Schedule next note
nextNoteTick += getRandomInterval();
}
// 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
});
}
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);
// --- 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;
// 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);
}
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();;