/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var facekit = LK.import("@upit/facekit.v1"); /**** * Classes ****/ // Ball class: the fixed shooter at bottom left var Ball = Container.expand(function () { var self = Container.call(this); var ballAsset = self.attachAsset('ball', { anchorX: 0.5, anchorY: 0.5, width: BALL_SIZE, height: BALL_SIZE, color: 0xffffff, shape: 'ellipse' }); return self; }); // Bullet class: fired from ball towards crosshair var Bullet = Container.expand(function () { var self = Container.call(this); var bulletAsset = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, width: BULLET_SIZE, height: BULLET_SIZE, color: 0x00e6e6, shape: 'ellipse' }); self.vx = 0; self.vy = 0; self.update = function () { self.x += self.vx; self.y += self.vy; }; return self; }); // Crosshair class: follows facekit position var Crosshair = Container.expand(function () { var self = Container.call(this); var crossAsset = self.attachAsset('crosshair', { anchorX: 0.5, anchorY: 0.5, width: CROSS_SIZE, height: CROSS_SIZE, color: 0xff0000, shape: 'ellipse' }); return self; }); // MusicNoteEffect class: floating music note symbol when a note is hit var MusicNoteEffect = Container.expand(function () { var self = Container.call(this); // Use a Text2 object for the music note symbol var noteSymbol = new Text2("♪", { size: 120, fill: "#fff" }); noteSymbol.anchor.set(0.5, 0.5); self.addChild(noteSymbol); // Animate: float up and fade out self.start = function (startX, startY) { self.x = startX; self.y = startY; self.alpha = 1; // Tween upward and fade out tween(self, { y: self.y - 220, alpha: 0 }, { duration: 700, easing: tween.cubicOut, onFinish: function onFinish() { self.destroy(); } }); }; return self; }); // NotaggNote class: falling notagg note (special note, ends game if tapped) var NotaggNote = Container.expand(function () { var self = Container.call(this); var notaggAsset = self.attachAsset('Notagg', { anchorX: 0.5, anchorY: 0.5, width: NOTE_WIDTH, height: NOTE_HEIGHT }); self.column = 0; self.speed = noteFallSpeed; self.hit = false; self.isNotagg = true; // flag for special handling self.update = function () { self.y += self.speed; }; return self; }); // Note class: falling note var Note = Container.expand(function () { var self = Container.call(this); // Randomly pick a color for the note var color = NOTE_COLORS[Math.floor(Math.random() * NOTE_COLORS.length)]; var noteAsset = self.attachAsset('note', { anchorX: 0.5, anchorY: 0.5, width: NOTE_WIDTH, // now wider height: NOTE_HEIGHT, color: color, shape: 'box' }); self.column = 0; // which column this note is in self.speed = noteFallSpeed; // will be set on spawn self.hit = false; // has this note been hit self.update = function () { self.y += self.speed; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181a20 }); /**** * Game Code ****/ // Note: All note assets will be ellipses with different colors for variety // --- Constants --- // Ensure Notagg matches note image size var NOTE_COLORS = [0xffe066, 0x66b3ff, 0xff66a3, 0x7cff66, 0xff8c66]; var GAME_W = 2048; var GAME_H = 2732; // --- Dynamic Gradient Background --- // We'll use two large colored rectangles, tweening their tints and alpha for a subtle animated gradient effect. var bgLayer1 = null; var bgLayer2 = null; var bgGradientColors = [0x8635f1, // purple 0x66b3ff, // blue 0xff66a3, // pink 0x7cff66, // green 0xffe066, // yellow 0xb39ddb // lavender ]; var bgColorIdx1 = 0; var bgColorIdx2 = 1; var bgTweenDuration = 8000; // ms, slightly longer for smoother transitions function setupGradientBackground() { // Remove if already present if (bgLayer1) { bgLayer1.destroy(); bgLayer1 = null; } if (bgLayer2) { bgLayer2.destroy(); bgLayer2 = null; } // Layer 1 bgLayer1 = LK.getAsset('softbg2', { anchorX: 0.5, anchorY: 0.5, width: GAME_W * 1.1, height: GAME_H * 1.1, x: GAME_W / 2, y: GAME_H / 2, // Start with first color, slightly more visible tint: bgGradientColors[bgColorIdx1], alpha: 0.32 }); // Layer 2 bgLayer2 = LK.getAsset('softbg2', { anchorX: 0.5, anchorY: 0.5, width: GAME_W * 1.1, height: GAME_H * 1.1, x: GAME_W / 2, y: GAME_H / 2, // Start with second color, slightly more visible tint: bgGradientColors[bgColorIdx2], alpha: 0.22 }); // Add to back of display list game.addChildAt(bgLayer1, 0); game.addChildAt(bgLayer2, 1); // Start the animation loop animateGradientBackground(); } function animateGradientBackground() { // Pick next color indices var nextIdx1 = (bgColorIdx1 + 1) % bgGradientColors.length; var nextIdx2 = (bgColorIdx2 + 1) % bgGradientColors.length; // Animate tints and alpha for both layers, smoothly transitioning to next color tween(bgLayer1, { tint: bgGradientColors[nextIdx1], alpha: 0.32 }, { duration: bgTweenDuration, easing: tween.easeInOut, onFinish: function onFinish() { bgColorIdx1 = nextIdx1; animateGradientBackground(); } }); tween(bgLayer2, { tint: bgGradientColors[nextIdx2], alpha: 0.22 }, { duration: bgTweenDuration, easing: tween.easeInOut, onFinish: function onFinish() { bgColorIdx2 = nextIdx2; } }); } // Call setupGradientBackground once at game start setupGradientBackground(); var BALL_SIZE = 420; var NOTE_WIDTH = 180; // was 120, now wider for less thin look var NOTE_HEIGHT = 180; // reduced from 260 to make notes less long var CROSS_SIZE = 160; var BULLET_SIZE = 60; var NOTE_COLS = 5; var NOTE_MARGIN = 60; var NOTE_AREA_W = GAME_W - 2 * NOTE_MARGIN; var NOTE_COL_W = NOTE_AREA_W / NOTE_COLS; var NOTE_START_Y = -NOTE_HEIGHT; var BALL_X = 180; var BALL_Y = GAME_H - 220; var CROSS_MIN_X = 200; var CROSS_MAX_X = GAME_W - 200; var CROSS_MIN_Y = 200; var CROSS_MAX_Y = GAME_H - 400; var BULLET_SPEED = 48; // px per frame // --- Game State --- var notes = []; var bullets = []; var crosshair = null; var ball = null; var scoreTxt = null; var tipTxt = null; var lastNoteSpawnTick = 0; var noteFallSpeed = 12; // will be set by difficulty var noteSpawnInterval = 60; // ticks between notes, will be set by difficulty var difficulty = null; // 'slow', 'normal', 'fast' var gameStarted = false; var canShoot = true; var lastScore = 0; var musicTracks = [{ id: 'music1', name: 'Parça 1' }, { id: 'music2', name: 'Parça 2' }, { id: 'music3', name: 'Parça 3' }]; var currentMusic = null; // --- Difficulty Presets --- // Slowed down for all modes var DIFFICULTY_PRESETS = { 'slow': { noteFallSpeed: 5, noteSpawnInterval: 120 }, 'normal': { noteFallSpeed: 7, noteSpawnInterval: 90 }, 'fast': { noteFallSpeed: 10, noteSpawnInterval: 60 } }; // --- UI: Difficulty Selection --- var diffBtns = []; function showDifficultyMenu() { // Play menu music if not already playing LK.stopMusic(); LK.playMusic('Menu1', { loop: true }); // Hide facekit video feed if visible if (facekit && typeof facekit.setVisible === "function") { facekit.setVisible(false); } // Add Backr image as background if (!game._diffMenuBackr) { var backr = LK.getAsset('Backr', { anchorX: 0.5, anchorY: 0.5, width: GAME_W, height: GAME_H, x: GAME_W / 2, y: GAME_H / 2, scaleMode: 'nearest' // prevent blurriness by using nearest neighbor scaling }); if (typeof backr.setScaleMode === "function") { backr.setScaleMode('nearest'); } game.addChild(backr); game._diffMenuBackr = backr; } var btnLabels = [{ key: 'slow', label: 'Slow' }, { key: 'normal', label: 'Normal' }, { key: 'fast', label: 'Fast' }]; var btnW = 500, btnH = 180, btnGap = 60; var startY = GAME_H / 2 - (btnLabels.length * btnH + (btnLabels.length - 1) * btnGap) / 2; for (var i = 0; i < btnLabels.length; i++) { var btn = new Container(); var bg = btn.attachAsset('btnbg', { anchorX: 0.5, anchorY: 0.5, width: btnW, height: btnH, color: 0x222a38, shape: 'box' }); var txt = new Text2(btnLabels[i].label, { size: 90, fill: "#fff" }); txt.anchor.set(0.5, 0.5); txt.x = 0; txt.y = 0; btn.addChild(txt); btn.x = GAME_W / 2; btn.y = startY + i * (btnH + btnGap); btn.difficulty = btnLabels[i].key; btn.down = function (x, y, obj) { startGame(this.difficulty); }; game.addChild(btn); diffBtns.push(btn); } // Show tip if (!tipTxt) { tipTxt = new Text2("AIM AT THE NOTES BY\nMOVING YOUR HEAD\nAND FIRE THE GUN BY\nTAPPING THE SCREEN.", { size: 70, fill: 0xFFFFFF }); tipTxt.anchor.set(0.5, 0); } // Place btnbg behind tipTxt, sized to match tipTxt if (!game._tipBg) { // Use tipTxt.width/height after anchor set var bgPadX = 60; var bgPadY = 40; var btnbgW = tipTxt.width + bgPadX; var btnbgH = tipTxt.height + bgPadY; var tipBg = LK.getAsset('btnbg', { anchorX: 0.5, anchorY: 0, width: btnbgW, height: btnbgH, color: 0x000000, shape: 'box' }); tipBg.x = GAME_W / 2; tipBg.y = 180 - bgPadY / 2; game.addChild(tipBg); game._tipBg = tipBg; } tipTxt.x = GAME_W / 2; tipTxt.y = 180; game.addChild(tipTxt); } // Remove difficulty menu function hideDifficultyMenu() { // Restore facekit video feed if (facekit && typeof facekit.setVisible === "function") { facekit.setVisible(true); } // Remove Backr background if present if (game._diffMenuBackr) { game._diffMenuBackr.destroy(); game._diffMenuBackr = null; } for (var i = 0; i < diffBtns.length; i++) { diffBtns[i].destroy(); } diffBtns = []; if (tipTxt) { tipTxt.destroy(); tipTxt = null; if (game._tipBg) { game._tipBg.destroy(); game._tipBg = null; } } } // --- Start Game --- function startGame(diffKey) { difficulty = diffKey; noteFallSpeed = DIFFICULTY_PRESETS[diffKey].noteFallSpeed; noteSpawnInterval = DIFFICULTY_PRESETS[diffKey].noteSpawnInterval; gameStarted = false; // Will be set to true after countdown // Stop menu music if playing LK.stopMusic(); hideDifficultyMenu(); LK.setScore(0); lastScore = 0; // Clean up any existing notes and bullets before starting for (var i = 0; i < notes.length; i++) { if (notes[i]) notes[i].destroy(); } notes = []; for (var i = 0; i < bullets.length; i++) { if (bullets[i]) bullets[i].destroy(); } bullets = []; // Play PH6 only in fast mode, and make it more dominant in randomization var musicChoices; if (difficulty === 'fast') { // PH6 is more dominant: add it multiple times to increase its chance musicChoices = ['Ph6', 'Ph6', 'Ph6', 'Piano1', 'P2', 'P3', 'P4', 'P5']; } else { musicChoices = ['Piano1', 'P2', 'P3', 'P4', 'P5']; } var selectedMusic = musicChoices[Math.floor(Math.random() * musicChoices.length)]; // Stop any currently playing music before starting new one LK.stopMusic(); // Play the selected music with default options (looping, full volume) LK.playMusic(selectedMusic, { loop: true, fade: { start: 0, end: 1, duration: 400 } }); // Ball ball = new Ball(); ball.x = BALL_X; ball.y = BALL_Y; game.addChild(ball); // Crosshair crosshair = new Crosshair(); crosshair.x = BALL_X + 400; crosshair.y = BALL_Y - 400; game.addChild(crosshair); // Score if (!scoreTxt) { scoreTxt = new Text2('0', { size: 120, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); } scoreTxt.setText(0); // Remove tipTxt immediately if present when game starts if (tipTxt) { tipTxt.destroy(); tipTxt = null; } // --- 3 2 1 Go! Countdown --- var countdownColors = ["#ff2d2d", "#ffe066", "#7cff66", "#a259ff"]; var countdownTexts = ["3", "2", "1", "Go!"]; var countdownObjs = []; var countdownIdx = 0; function showCountdownStep(idx) { // Remove previous countdown text if any for (var i = 0; i < countdownObjs.length; i++) { if (countdownObjs[i]) { countdownObjs[i].destroy(); } } countdownObjs = []; if (idx < countdownTexts.length) { var txt = new Text2(countdownTexts[idx], { size: idx === 3 ? 180 : 160, fill: countdownColors[idx] }); txt.anchor.set(0.5, 0.5); txt.x = GAME_W / 2; txt.y = GAME_H / 2; game.addChild(txt); countdownObjs.push(txt); // Animate scale up and fade out txt.scale.set(1, 1); txt.alpha = 1; tween(txt, { scaleX: 1.25, scaleY: 1.25, alpha: 0.0 }, { duration: 600, delay: 400, onFinish: function onFinish() { txt.destroy(); } }); // Next step after 800ms LK.setTimeout(function () { showCountdownStep(idx + 1); }, 800); } else { // Countdown finished, start game gameStarted = true; } } showCountdownStep(0); } // --- End Game --- function endGame(win) { // Clean up for (var i = 0; i < notes.length; i++) { if (notes[i]) notes[i].destroy(); } notes = []; for (var i = 0; i < bullets.length; i++) { if (bullets[i]) bullets[i].destroy(); } bullets = []; if (ball) { ball.destroy(); ball = null; } if (crosshair) { crosshair.destroy(); crosshair = null; } gameStarted = false; LK.stopMusic(); if (win) { // Show win, then play menu music after popup closes LK.showYouWin(); LK.setTimeout(function () { LK.stopMusic(); LK.playMusic('Menu1', { loop: true }); }, 1200); } else { LK.showGameOver(); LK.setTimeout(function () { LK.stopMusic(); LK.playMusic('Menu1', { loop: true }); }, 1200); } } // --- Note Spawning --- function spawnNote() { // 15% chance to spawn NotaggNote, otherwise normal Note var isNotagg = Math.random() < 0.15; var note; if (isNotagg) { note = new NotaggNote(); } else { note = new Note(); } // Pick a random column, now including the far left (index 0) so notes can fall above the gun // allowedCols now includes 0,1,2,3,4 (all columns) var allowedCols = []; for (var i = 0; i < NOTE_COLS; i++) { allowedCols.push(i); } // Increase position variation: allow some random offset within each column var col = allowedCols[Math.floor(Math.random() * allowedCols.length)]; note.column = col; // Add random offset within the column for more variation var colOffset = 0; if (difficulty === 'slow') { colOffset = (Math.random() - 0.5) * (NOTE_COL_W * 0.3); // up to ±15% of col width } else if (difficulty === 'normal') { colOffset = (Math.random() - 0.5) * (NOTE_COL_W * 0.5); // up to ±25% of col width } else if (difficulty === 'fast') { colOffset = (Math.random() - 0.5) * (NOTE_COL_W * 0.7); // up to ±35% of col width } note.x = NOTE_MARGIN + NOTE_COL_W / 2 + col * NOTE_COL_W + colOffset; note.y = NOTE_START_Y; // Increase falling speed slightly per mode if (difficulty === 'slow') { note.speed = noteFallSpeed + 1.2 + Math.random() * 0.8; // 1.2-2.0 px/frame more } else if (difficulty === 'normal') { note.speed = noteFallSpeed + 2.2 + Math.random() * 1.2; // 2.2-3.4 px/frame more } else if (difficulty === 'fast') { note.speed = noteFallSpeed + 3.2 + Math.random() * 1.8; // 3.2-5 px/frame more } else { note.speed = noteFallSpeed; } notes.push(note); game.addChild(note); } // --- Difficulty Increase --- function maybeIncreaseDifficulty() { var score = LK.getScore(); if (score > 0 && score % 10 === 0 && score !== lastScore) { // Increase speed and spawn rate noteFallSpeed += 2; if (noteSpawnInterval > 20) noteSpawnInterval -= 6; lastScore = score; } } // --- Facekit Crosshair Update --- function updateCrosshair() { if (!crosshair) return; // Use facekit.noseTip or facekit.mouthCenter for aiming var fx = facekit.noseTip ? facekit.noseTip.x : facekit.mouthCenter ? facekit.mouthCenter.x : GAME_W / 2; var fy = facekit.noseTip ? facekit.noseTip.y : facekit.mouthCenter ? facekit.mouthCenter.y : GAME_H / 2; // Clamp to play area if (fx < CROSS_MIN_X) fx = CROSS_MIN_X; if (fx > CROSS_MAX_X) fx = CROSS_MAX_X; if (fy < CROSS_MIN_Y) fy = CROSS_MIN_Y; if (fy > CROSS_MAX_Y) fy = CROSS_MAX_Y; // Instantly set crosshair position (no smoothing, no tween) crosshair.x = fx; crosshair.y = fy; } // --- Fire Bullet --- function fireBullet() { if (!canShoot || !gameStarted) return; canShoot = false; // Find if crosshair is over a note var hitNote = null; for (var i = 0; i < notes.length; i++) { var n = notes[i]; if (!n.hit && crosshair && crosshair.intersects(n)) { hitNote = n; break; } } // Fire bullet towards crosshair var b = new Bullet(); b.x = ball.x; b.y = ball.y; // Direction vector var dx = crosshair.x - ball.x; var dy = crosshair.y - ball.y; var len = Math.sqrt(dx * dx + dy * dy); if (len === 0) { dx = 0; dy = -1; len = 1; } b.vx = BULLET_SPEED * dx / len; b.vy = BULLET_SPEED * dy / len; bullets.push(b); game.addChild(b); // If hitNote, mark as hit (so only one bullet per note) if (hitNote) { // If it's a NotaggNote, end game immediately if (hitNote.isNotagg) { endGame(false); return; } hitNote.hit = true; } // Play note sound (simulate: play a sound per note column) // Stop all note sounds before playing a new one to prevent overlap for (var i = 0; i < NOTE_COLS; i++) { var s = LK.getSound('note' + i); if (s && typeof s.stop === "function") s.stop(); } var soundId = 'note' + (hitNote ? hitNote.column : Math.floor(Math.random() * NOTE_COLS)); LK.getSound(soundId).play(); // Allow next shot after a normal delay (normal fire rate) LK.setTimeout(function () { canShoot = true; }, 320); } // --- Main Game Loop --- game.down = function (x, y, obj) { if (gameStarted) { fireBullet(); } }; game.update = function () { if (!gameStarted) return; // --- Optimize: Use local variables, avoid per-frame allocations, and minimize nested loops --- // Update crosshair position from facekit updateCrosshair(); // Spawn notes if (LK.ticks - lastNoteSpawnTick >= noteSpawnInterval) { spawnNote(); lastNoteSpawnTick = LK.ticks; } // --- Update notes --- // Use a single pass to update and remove notes that are offscreen or hit for (var i = notes.length - 1; i >= 0; i--) { var n = notes[i]; n.update(); // If note falls below screen, trigger game over for all except NotaggNote if (n.y > GAME_H + NOTE_HEIGHT / 2) { // If it's a NotaggNote, just remove it, do not end game if (n.isNotagg) { n.visible = false; n.destroy(); notes.splice(i, 1); continue; } // For all other notes, reaching the bottom is game over endGame(false); return; } } // --- Update bullets --- // Use local references to avoid repeated property lookups for (var i = bullets.length - 1; i >= 0; i--) { var b = bullets[i]; b.update(); // Remove bullet if out of bounds if (b.x < -100 || b.x > GAME_W + 100 || b.y < -100 || b.y > GAME_H + 100) { b.destroy(); bullets.splice(i, 1); continue; } // --- Optimize: Only check for collisions with visible, unhit notes --- var bulletHit = false; for (var j = notes.length - 1; j >= 0; j--) { var n = notes[j]; if (n.hit || n.visible === false) continue; // Initialize lastWasIntersecting if not set if (typeof b.lastWasIntersecting === "undefined") b.lastWasIntersecting = false; var isIntersecting = b.intersects(n); // Make hitbox more forgiving: allow hit if bullet is close to note center (within 0.6*NOTE_WIDTH and 0.6*NOTE_HEIGHT) var forgivingHit = false; var dx = Math.abs(b.x - n.x); var dy = Math.abs(b.y - n.y); if (dx < NOTE_WIDTH * 0.6 && dy < NOTE_HEIGHT * 0.6) { forgivingHit = true; } // Only allow hit if note is not already hit and not already destroyed, and bullet just started intersecting or is within forgiving hitbox if (b.lastWasIntersecting === false && isIntersecting || forgivingHit) { // If it's a NotaggNote, end game immediately if (n.isNotagg) { endGame(false); return; } n.hit = true; // Mark as hit immediately to prevent double hits notes.splice(j, 1); // Remove from notes array immediately to prevent further processing LK.setScore(LK.getScore() + 1); scoreTxt.setText(LK.getScore()); // Animate note color to gray/black, then remove if (n.children && n.children.length > 0 && n.children[0]) { var noteAsset = n.children[0]; tween(noteAsset, { tint: 0x222222 }, { duration: 60, onFinish: function (noteAsset, n) { return function () { if (noteAsset && noteAsset.parent) { noteAsset.parent.removeChild(noteAsset); } n.visible = false; n.destroy(); }; }(noteAsset, n) }); } else { n.visible = false; n.destroy(); } // Spawn floating music note effect at note position var effect = new MusicNoteEffect(); effect.start(n.x, n.y); game.addChild(effect); b.destroy(); bullets.splice(i, 1); // Difficulty up maybeIncreaseDifficulty(); // Win condition: 50 points if (LK.getScore() >= 50) { endGame(true); return; } bulletHit = true; break; } b.lastWasIntersecting = isIntersecting; } if (bulletHit) continue; } }; // --- Show Difficulty Menu on Start --- showDifficultyMenu(); // Prevent in-game sounds from playing in the menu by disabling their playback when not in game // Patch LK.getSound to return a dummy object with a no-op play() if not gameStarted var _LK_getSound = LK.getSound; LK.getSound = function (id) { if (!gameStarted) { return { play: function play() {} }; } return _LK_getSound.call(LK, id); }; /**** * Asset Initialization (for static analysis) ****/ // Notes (5 columns, 5 colors) for (var i = 0; i < NOTE_COLS; i++) {} // Ball // Crosshair // Bullet // Button background // Music tracks
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var facekit = LK.import("@upit/facekit.v1");
/****
* Classes
****/
// Ball class: the fixed shooter at bottom left
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballAsset = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5,
width: BALL_SIZE,
height: BALL_SIZE,
color: 0xffffff,
shape: 'ellipse'
});
return self;
});
// Bullet class: fired from ball towards crosshair
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletAsset = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
width: BULLET_SIZE,
height: BULLET_SIZE,
color: 0x00e6e6,
shape: 'ellipse'
});
self.vx = 0;
self.vy = 0;
self.update = function () {
self.x += self.vx;
self.y += self.vy;
};
return self;
});
// Crosshair class: follows facekit position
var Crosshair = Container.expand(function () {
var self = Container.call(this);
var crossAsset = self.attachAsset('crosshair', {
anchorX: 0.5,
anchorY: 0.5,
width: CROSS_SIZE,
height: CROSS_SIZE,
color: 0xff0000,
shape: 'ellipse'
});
return self;
});
// MusicNoteEffect class: floating music note symbol when a note is hit
var MusicNoteEffect = Container.expand(function () {
var self = Container.call(this);
// Use a Text2 object for the music note symbol
var noteSymbol = new Text2("♪", {
size: 120,
fill: "#fff"
});
noteSymbol.anchor.set(0.5, 0.5);
self.addChild(noteSymbol);
// Animate: float up and fade out
self.start = function (startX, startY) {
self.x = startX;
self.y = startY;
self.alpha = 1;
// Tween upward and fade out
tween(self, {
y: self.y - 220,
alpha: 0
}, {
duration: 700,
easing: tween.cubicOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// NotaggNote class: falling notagg note (special note, ends game if tapped)
var NotaggNote = Container.expand(function () {
var self = Container.call(this);
var notaggAsset = self.attachAsset('Notagg', {
anchorX: 0.5,
anchorY: 0.5,
width: NOTE_WIDTH,
height: NOTE_HEIGHT
});
self.column = 0;
self.speed = noteFallSpeed;
self.hit = false;
self.isNotagg = true; // flag for special handling
self.update = function () {
self.y += self.speed;
};
return self;
});
// Note class: falling note
var Note = Container.expand(function () {
var self = Container.call(this);
// Randomly pick a color for the note
var color = NOTE_COLORS[Math.floor(Math.random() * NOTE_COLORS.length)];
var noteAsset = self.attachAsset('note', {
anchorX: 0.5,
anchorY: 0.5,
width: NOTE_WIDTH,
// now wider
height: NOTE_HEIGHT,
color: color,
shape: 'box'
});
self.column = 0; // which column this note is in
self.speed = noteFallSpeed; // will be set on spawn
self.hit = false; // has this note been hit
self.update = function () {
self.y += self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181a20
});
/****
* Game Code
****/
// Note: All note assets will be ellipses with different colors for variety
// --- Constants ---
// Ensure Notagg matches note image size
var NOTE_COLORS = [0xffe066, 0x66b3ff, 0xff66a3, 0x7cff66, 0xff8c66];
var GAME_W = 2048;
var GAME_H = 2732;
// --- Dynamic Gradient Background ---
// We'll use two large colored rectangles, tweening their tints and alpha for a subtle animated gradient effect.
var bgLayer1 = null;
var bgLayer2 = null;
var bgGradientColors = [0x8635f1,
// purple
0x66b3ff,
// blue
0xff66a3,
// pink
0x7cff66,
// green
0xffe066,
// yellow
0xb39ddb // lavender
];
var bgColorIdx1 = 0;
var bgColorIdx2 = 1;
var bgTweenDuration = 8000; // ms, slightly longer for smoother transitions
function setupGradientBackground() {
// Remove if already present
if (bgLayer1) {
bgLayer1.destroy();
bgLayer1 = null;
}
if (bgLayer2) {
bgLayer2.destroy();
bgLayer2 = null;
}
// Layer 1
bgLayer1 = LK.getAsset('softbg2', {
anchorX: 0.5,
anchorY: 0.5,
width: GAME_W * 1.1,
height: GAME_H * 1.1,
x: GAME_W / 2,
y: GAME_H / 2,
// Start with first color, slightly more visible
tint: bgGradientColors[bgColorIdx1],
alpha: 0.32
});
// Layer 2
bgLayer2 = LK.getAsset('softbg2', {
anchorX: 0.5,
anchorY: 0.5,
width: GAME_W * 1.1,
height: GAME_H * 1.1,
x: GAME_W / 2,
y: GAME_H / 2,
// Start with second color, slightly more visible
tint: bgGradientColors[bgColorIdx2],
alpha: 0.22
});
// Add to back of display list
game.addChildAt(bgLayer1, 0);
game.addChildAt(bgLayer2, 1);
// Start the animation loop
animateGradientBackground();
}
function animateGradientBackground() {
// Pick next color indices
var nextIdx1 = (bgColorIdx1 + 1) % bgGradientColors.length;
var nextIdx2 = (bgColorIdx2 + 1) % bgGradientColors.length;
// Animate tints and alpha for both layers, smoothly transitioning to next color
tween(bgLayer1, {
tint: bgGradientColors[nextIdx1],
alpha: 0.32
}, {
duration: bgTweenDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
bgColorIdx1 = nextIdx1;
animateGradientBackground();
}
});
tween(bgLayer2, {
tint: bgGradientColors[nextIdx2],
alpha: 0.22
}, {
duration: bgTweenDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
bgColorIdx2 = nextIdx2;
}
});
}
// Call setupGradientBackground once at game start
setupGradientBackground();
var BALL_SIZE = 420;
var NOTE_WIDTH = 180; // was 120, now wider for less thin look
var NOTE_HEIGHT = 180; // reduced from 260 to make notes less long
var CROSS_SIZE = 160;
var BULLET_SIZE = 60;
var NOTE_COLS = 5;
var NOTE_MARGIN = 60;
var NOTE_AREA_W = GAME_W - 2 * NOTE_MARGIN;
var NOTE_COL_W = NOTE_AREA_W / NOTE_COLS;
var NOTE_START_Y = -NOTE_HEIGHT;
var BALL_X = 180;
var BALL_Y = GAME_H - 220;
var CROSS_MIN_X = 200;
var CROSS_MAX_X = GAME_W - 200;
var CROSS_MIN_Y = 200;
var CROSS_MAX_Y = GAME_H - 400;
var BULLET_SPEED = 48; // px per frame
// --- Game State ---
var notes = [];
var bullets = [];
var crosshair = null;
var ball = null;
var scoreTxt = null;
var tipTxt = null;
var lastNoteSpawnTick = 0;
var noteFallSpeed = 12; // will be set by difficulty
var noteSpawnInterval = 60; // ticks between notes, will be set by difficulty
var difficulty = null; // 'slow', 'normal', 'fast'
var gameStarted = false;
var canShoot = true;
var lastScore = 0;
var musicTracks = [{
id: 'music1',
name: 'Parça 1'
}, {
id: 'music2',
name: 'Parça 2'
}, {
id: 'music3',
name: 'Parça 3'
}];
var currentMusic = null;
// --- Difficulty Presets ---
// Slowed down for all modes
var DIFFICULTY_PRESETS = {
'slow': {
noteFallSpeed: 5,
noteSpawnInterval: 120
},
'normal': {
noteFallSpeed: 7,
noteSpawnInterval: 90
},
'fast': {
noteFallSpeed: 10,
noteSpawnInterval: 60
}
};
// --- UI: Difficulty Selection ---
var diffBtns = [];
function showDifficultyMenu() {
// Play menu music if not already playing
LK.stopMusic();
LK.playMusic('Menu1', {
loop: true
});
// Hide facekit video feed if visible
if (facekit && typeof facekit.setVisible === "function") {
facekit.setVisible(false);
}
// Add Backr image as background
if (!game._diffMenuBackr) {
var backr = LK.getAsset('Backr', {
anchorX: 0.5,
anchorY: 0.5,
width: GAME_W,
height: GAME_H,
x: GAME_W / 2,
y: GAME_H / 2,
scaleMode: 'nearest' // prevent blurriness by using nearest neighbor scaling
});
if (typeof backr.setScaleMode === "function") {
backr.setScaleMode('nearest');
}
game.addChild(backr);
game._diffMenuBackr = backr;
}
var btnLabels = [{
key: 'slow',
label: 'Slow'
}, {
key: 'normal',
label: 'Normal'
}, {
key: 'fast',
label: 'Fast'
}];
var btnW = 500,
btnH = 180,
btnGap = 60;
var startY = GAME_H / 2 - (btnLabels.length * btnH + (btnLabels.length - 1) * btnGap) / 2;
for (var i = 0; i < btnLabels.length; i++) {
var btn = new Container();
var bg = btn.attachAsset('btnbg', {
anchorX: 0.5,
anchorY: 0.5,
width: btnW,
height: btnH,
color: 0x222a38,
shape: 'box'
});
var txt = new Text2(btnLabels[i].label, {
size: 90,
fill: "#fff"
});
txt.anchor.set(0.5, 0.5);
txt.x = 0;
txt.y = 0;
btn.addChild(txt);
btn.x = GAME_W / 2;
btn.y = startY + i * (btnH + btnGap);
btn.difficulty = btnLabels[i].key;
btn.down = function (x, y, obj) {
startGame(this.difficulty);
};
game.addChild(btn);
diffBtns.push(btn);
}
// Show tip
if (!tipTxt) {
tipTxt = new Text2("AIM AT THE NOTES BY\nMOVING YOUR HEAD\nAND FIRE THE GUN BY\nTAPPING THE SCREEN.", {
size: 70,
fill: 0xFFFFFF
});
tipTxt.anchor.set(0.5, 0);
}
// Place btnbg behind tipTxt, sized to match tipTxt
if (!game._tipBg) {
// Use tipTxt.width/height after anchor set
var bgPadX = 60;
var bgPadY = 40;
var btnbgW = tipTxt.width + bgPadX;
var btnbgH = tipTxt.height + bgPadY;
var tipBg = LK.getAsset('btnbg', {
anchorX: 0.5,
anchorY: 0,
width: btnbgW,
height: btnbgH,
color: 0x000000,
shape: 'box'
});
tipBg.x = GAME_W / 2;
tipBg.y = 180 - bgPadY / 2;
game.addChild(tipBg);
game._tipBg = tipBg;
}
tipTxt.x = GAME_W / 2;
tipTxt.y = 180;
game.addChild(tipTxt);
}
// Remove difficulty menu
function hideDifficultyMenu() {
// Restore facekit video feed
if (facekit && typeof facekit.setVisible === "function") {
facekit.setVisible(true);
}
// Remove Backr background if present
if (game._diffMenuBackr) {
game._diffMenuBackr.destroy();
game._diffMenuBackr = null;
}
for (var i = 0; i < diffBtns.length; i++) {
diffBtns[i].destroy();
}
diffBtns = [];
if (tipTxt) {
tipTxt.destroy();
tipTxt = null;
if (game._tipBg) {
game._tipBg.destroy();
game._tipBg = null;
}
}
}
// --- Start Game ---
function startGame(diffKey) {
difficulty = diffKey;
noteFallSpeed = DIFFICULTY_PRESETS[diffKey].noteFallSpeed;
noteSpawnInterval = DIFFICULTY_PRESETS[diffKey].noteSpawnInterval;
gameStarted = false; // Will be set to true after countdown
// Stop menu music if playing
LK.stopMusic();
hideDifficultyMenu();
LK.setScore(0);
lastScore = 0;
// Clean up any existing notes and bullets before starting
for (var i = 0; i < notes.length; i++) {
if (notes[i]) notes[i].destroy();
}
notes = [];
for (var i = 0; i < bullets.length; i++) {
if (bullets[i]) bullets[i].destroy();
}
bullets = [];
// Play PH6 only in fast mode, and make it more dominant in randomization
var musicChoices;
if (difficulty === 'fast') {
// PH6 is more dominant: add it multiple times to increase its chance
musicChoices = ['Ph6', 'Ph6', 'Ph6', 'Piano1', 'P2', 'P3', 'P4', 'P5'];
} else {
musicChoices = ['Piano1', 'P2', 'P3', 'P4', 'P5'];
}
var selectedMusic = musicChoices[Math.floor(Math.random() * musicChoices.length)];
// Stop any currently playing music before starting new one
LK.stopMusic();
// Play the selected music with default options (looping, full volume)
LK.playMusic(selectedMusic, {
loop: true,
fade: {
start: 0,
end: 1,
duration: 400
}
});
// Ball
ball = new Ball();
ball.x = BALL_X;
ball.y = BALL_Y;
game.addChild(ball);
// Crosshair
crosshair = new Crosshair();
crosshair.x = BALL_X + 400;
crosshair.y = BALL_Y - 400;
game.addChild(crosshair);
// Score
if (!scoreTxt) {
scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
}
scoreTxt.setText(0);
// Remove tipTxt immediately if present when game starts
if (tipTxt) {
tipTxt.destroy();
tipTxt = null;
}
// --- 3 2 1 Go! Countdown ---
var countdownColors = ["#ff2d2d", "#ffe066", "#7cff66", "#a259ff"];
var countdownTexts = ["3", "2", "1", "Go!"];
var countdownObjs = [];
var countdownIdx = 0;
function showCountdownStep(idx) {
// Remove previous countdown text if any
for (var i = 0; i < countdownObjs.length; i++) {
if (countdownObjs[i]) {
countdownObjs[i].destroy();
}
}
countdownObjs = [];
if (idx < countdownTexts.length) {
var txt = new Text2(countdownTexts[idx], {
size: idx === 3 ? 180 : 160,
fill: countdownColors[idx]
});
txt.anchor.set(0.5, 0.5);
txt.x = GAME_W / 2;
txt.y = GAME_H / 2;
game.addChild(txt);
countdownObjs.push(txt);
// Animate scale up and fade out
txt.scale.set(1, 1);
txt.alpha = 1;
tween(txt, {
scaleX: 1.25,
scaleY: 1.25,
alpha: 0.0
}, {
duration: 600,
delay: 400,
onFinish: function onFinish() {
txt.destroy();
}
});
// Next step after 800ms
LK.setTimeout(function () {
showCountdownStep(idx + 1);
}, 800);
} else {
// Countdown finished, start game
gameStarted = true;
}
}
showCountdownStep(0);
}
// --- End Game ---
function endGame(win) {
// Clean up
for (var i = 0; i < notes.length; i++) {
if (notes[i]) notes[i].destroy();
}
notes = [];
for (var i = 0; i < bullets.length; i++) {
if (bullets[i]) bullets[i].destroy();
}
bullets = [];
if (ball) {
ball.destroy();
ball = null;
}
if (crosshair) {
crosshair.destroy();
crosshair = null;
}
gameStarted = false;
LK.stopMusic();
if (win) {
// Show win, then play menu music after popup closes
LK.showYouWin();
LK.setTimeout(function () {
LK.stopMusic();
LK.playMusic('Menu1', {
loop: true
});
}, 1200);
} else {
LK.showGameOver();
LK.setTimeout(function () {
LK.stopMusic();
LK.playMusic('Menu1', {
loop: true
});
}, 1200);
}
}
// --- Note Spawning ---
function spawnNote() {
// 15% chance to spawn NotaggNote, otherwise normal Note
var isNotagg = Math.random() < 0.15;
var note;
if (isNotagg) {
note = new NotaggNote();
} else {
note = new Note();
}
// Pick a random column, now including the far left (index 0) so notes can fall above the gun
// allowedCols now includes 0,1,2,3,4 (all columns)
var allowedCols = [];
for (var i = 0; i < NOTE_COLS; i++) {
allowedCols.push(i);
}
// Increase position variation: allow some random offset within each column
var col = allowedCols[Math.floor(Math.random() * allowedCols.length)];
note.column = col;
// Add random offset within the column for more variation
var colOffset = 0;
if (difficulty === 'slow') {
colOffset = (Math.random() - 0.5) * (NOTE_COL_W * 0.3); // up to ±15% of col width
} else if (difficulty === 'normal') {
colOffset = (Math.random() - 0.5) * (NOTE_COL_W * 0.5); // up to ±25% of col width
} else if (difficulty === 'fast') {
colOffset = (Math.random() - 0.5) * (NOTE_COL_W * 0.7); // up to ±35% of col width
}
note.x = NOTE_MARGIN + NOTE_COL_W / 2 + col * NOTE_COL_W + colOffset;
note.y = NOTE_START_Y;
// Increase falling speed slightly per mode
if (difficulty === 'slow') {
note.speed = noteFallSpeed + 1.2 + Math.random() * 0.8; // 1.2-2.0 px/frame more
} else if (difficulty === 'normal') {
note.speed = noteFallSpeed + 2.2 + Math.random() * 1.2; // 2.2-3.4 px/frame more
} else if (difficulty === 'fast') {
note.speed = noteFallSpeed + 3.2 + Math.random() * 1.8; // 3.2-5 px/frame more
} else {
note.speed = noteFallSpeed;
}
notes.push(note);
game.addChild(note);
}
// --- Difficulty Increase ---
function maybeIncreaseDifficulty() {
var score = LK.getScore();
if (score > 0 && score % 10 === 0 && score !== lastScore) {
// Increase speed and spawn rate
noteFallSpeed += 2;
if (noteSpawnInterval > 20) noteSpawnInterval -= 6;
lastScore = score;
}
}
// --- Facekit Crosshair Update ---
function updateCrosshair() {
if (!crosshair) return;
// Use facekit.noseTip or facekit.mouthCenter for aiming
var fx = facekit.noseTip ? facekit.noseTip.x : facekit.mouthCenter ? facekit.mouthCenter.x : GAME_W / 2;
var fy = facekit.noseTip ? facekit.noseTip.y : facekit.mouthCenter ? facekit.mouthCenter.y : GAME_H / 2;
// Clamp to play area
if (fx < CROSS_MIN_X) fx = CROSS_MIN_X;
if (fx > CROSS_MAX_X) fx = CROSS_MAX_X;
if (fy < CROSS_MIN_Y) fy = CROSS_MIN_Y;
if (fy > CROSS_MAX_Y) fy = CROSS_MAX_Y;
// Instantly set crosshair position (no smoothing, no tween)
crosshair.x = fx;
crosshair.y = fy;
}
// --- Fire Bullet ---
function fireBullet() {
if (!canShoot || !gameStarted) return;
canShoot = false;
// Find if crosshair is over a note
var hitNote = null;
for (var i = 0; i < notes.length; i++) {
var n = notes[i];
if (!n.hit && crosshair && crosshair.intersects(n)) {
hitNote = n;
break;
}
}
// Fire bullet towards crosshair
var b = new Bullet();
b.x = ball.x;
b.y = ball.y;
// Direction vector
var dx = crosshair.x - ball.x;
var dy = crosshair.y - ball.y;
var len = Math.sqrt(dx * dx + dy * dy);
if (len === 0) {
dx = 0;
dy = -1;
len = 1;
}
b.vx = BULLET_SPEED * dx / len;
b.vy = BULLET_SPEED * dy / len;
bullets.push(b);
game.addChild(b);
// If hitNote, mark as hit (so only one bullet per note)
if (hitNote) {
// If it's a NotaggNote, end game immediately
if (hitNote.isNotagg) {
endGame(false);
return;
}
hitNote.hit = true;
}
// Play note sound (simulate: play a sound per note column)
// Stop all note sounds before playing a new one to prevent overlap
for (var i = 0; i < NOTE_COLS; i++) {
var s = LK.getSound('note' + i);
if (s && typeof s.stop === "function") s.stop();
}
var soundId = 'note' + (hitNote ? hitNote.column : Math.floor(Math.random() * NOTE_COLS));
LK.getSound(soundId).play();
// Allow next shot after a normal delay (normal fire rate)
LK.setTimeout(function () {
canShoot = true;
}, 320);
}
// --- Main Game Loop ---
game.down = function (x, y, obj) {
if (gameStarted) {
fireBullet();
}
};
game.update = function () {
if (!gameStarted) return;
// --- Optimize: Use local variables, avoid per-frame allocations, and minimize nested loops ---
// Update crosshair position from facekit
updateCrosshair();
// Spawn notes
if (LK.ticks - lastNoteSpawnTick >= noteSpawnInterval) {
spawnNote();
lastNoteSpawnTick = LK.ticks;
}
// --- Update notes ---
// Use a single pass to update and remove notes that are offscreen or hit
for (var i = notes.length - 1; i >= 0; i--) {
var n = notes[i];
n.update();
// If note falls below screen, trigger game over for all except NotaggNote
if (n.y > GAME_H + NOTE_HEIGHT / 2) {
// If it's a NotaggNote, just remove it, do not end game
if (n.isNotagg) {
n.visible = false;
n.destroy();
notes.splice(i, 1);
continue;
}
// For all other notes, reaching the bottom is game over
endGame(false);
return;
}
}
// --- Update bullets ---
// Use local references to avoid repeated property lookups
for (var i = bullets.length - 1; i >= 0; i--) {
var b = bullets[i];
b.update();
// Remove bullet if out of bounds
if (b.x < -100 || b.x > GAME_W + 100 || b.y < -100 || b.y > GAME_H + 100) {
b.destroy();
bullets.splice(i, 1);
continue;
}
// --- Optimize: Only check for collisions with visible, unhit notes ---
var bulletHit = false;
for (var j = notes.length - 1; j >= 0; j--) {
var n = notes[j];
if (n.hit || n.visible === false) continue;
// Initialize lastWasIntersecting if not set
if (typeof b.lastWasIntersecting === "undefined") b.lastWasIntersecting = false;
var isIntersecting = b.intersects(n);
// Make hitbox more forgiving: allow hit if bullet is close to note center (within 0.6*NOTE_WIDTH and 0.6*NOTE_HEIGHT)
var forgivingHit = false;
var dx = Math.abs(b.x - n.x);
var dy = Math.abs(b.y - n.y);
if (dx < NOTE_WIDTH * 0.6 && dy < NOTE_HEIGHT * 0.6) {
forgivingHit = true;
}
// Only allow hit if note is not already hit and not already destroyed, and bullet just started intersecting or is within forgiving hitbox
if (b.lastWasIntersecting === false && isIntersecting || forgivingHit) {
// If it's a NotaggNote, end game immediately
if (n.isNotagg) {
endGame(false);
return;
}
n.hit = true; // Mark as hit immediately to prevent double hits
notes.splice(j, 1); // Remove from notes array immediately to prevent further processing
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
// Animate note color to gray/black, then remove
if (n.children && n.children.length > 0 && n.children[0]) {
var noteAsset = n.children[0];
tween(noteAsset, {
tint: 0x222222
}, {
duration: 60,
onFinish: function (noteAsset, n) {
return function () {
if (noteAsset && noteAsset.parent) {
noteAsset.parent.removeChild(noteAsset);
}
n.visible = false;
n.destroy();
};
}(noteAsset, n)
});
} else {
n.visible = false;
n.destroy();
}
// Spawn floating music note effect at note position
var effect = new MusicNoteEffect();
effect.start(n.x, n.y);
game.addChild(effect);
b.destroy();
bullets.splice(i, 1);
// Difficulty up
maybeIncreaseDifficulty();
// Win condition: 50 points
if (LK.getScore() >= 50) {
endGame(true);
return;
}
bulletHit = true;
break;
}
b.lastWasIntersecting = isIntersecting;
}
if (bulletHit) continue;
}
};
// --- Show Difficulty Menu on Start ---
showDifficultyMenu();
// Prevent in-game sounds from playing in the menu by disabling their playback when not in game
// Patch LK.getSound to return a dummy object with a no-op play() if not gameStarted
var _LK_getSound = LK.getSound;
LK.getSound = function (id) {
if (!gameStarted) {
return {
play: function play() {}
};
}
return _LK_getSound.call(LK, id);
};
/****
* Asset Initialization (for static analysis)
****/
// Notes (5 columns, 5 colors)
for (var i = 0; i < NOTE_COLS; i++) {}
// Ball
// Crosshair
// Bullet
// Button background
// Music tracks