/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var PlayerCharacter = Container.expand(function () {
var self = Container.call(this);
var characterGraphics = self.attachAsset('playerCharacter', {
anchorX: 0.5,
anchorY: 0.5
});
// Set brown transparent color
characterGraphics.alpha = 0.7;
return self;
});
//Storage library which should be used for persistent game data
// var storage = LK.import('@upit/storage.v1');
//Library for using the camera (the background becomes the user's camera video feed) and the microphone. It can access face coordinates for interactive play, as well detect microphone volume / voice interactions
// var facekit = LK.import('@upit/facekit.v1');
//Classes can only be defined here. You cannot create inline classes in the games code.
var TerrainBlock = Container.expand(function (color, speed, noteId) {
var self = Container.call(this);
self.color = color;
self.speed = speed;
self.noteId = noteId;
var blockGraphics = self.attachAsset('terrainBlock' + self.color, {
anchorX: 0.5,
anchorY: 0.5
});
// Create vertical line extending from block
self.line = new Container();
// Get color hex value from colorMap
var lineColor = 0xFFFFFF; // Default white
if (color === 'Red') {
lineColor = 0xFF0000;
} else if (color === 'Orange') {
lineColor = 0xFFA500;
} else if (color === 'Yellow') {
lineColor = 0xFFFF00;
} else if (color === 'Green') {
lineColor = 0x008000;
} else if (color === 'Blue') {
lineColor = 0x0000FF;
} else if (color === 'Indigo') {
lineColor = 0x4B0082;
} else if (color === 'Violet') {
lineColor = 0xEE82EE;
}
// Create thin vertical line shape
var lineGraphics = LK.getAsset('line' + self.color, {
width: 50,
height: 500,
// Shorter line
color: lineColor,
shape: 'box',
anchorX: 0.5,
anchorY: 1
});
self.line.addChild(lineGraphics);
self.addChild(self.line);
self.update = function () {
self.y += self.speed;
// Keep line positioned at block center extending upward
if (self.line) {
self.line.x = 0;
self.line.y = 0;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000 //Init game with black background
});
/****
* Game Code
****/
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
//Only include the plugins you need to create the game.
//We have access to the following plugins. (Note that the variable names used are mandetory for each plugin)
// Initialize assets used in this game. Scale them according to what is needed for the game.
// or via static code analysis based on their usage in the code.
// Assets are automatically created and loaded either dynamically during gameplay
/*
Supported Types:
1. Shape:
- Simple geometric figures with these properties:
* width: (required) pixel width of the shape.
* height: (required) pixel height of the shape.
* color: (required) color of the shape.
* shape: (required) type of shape. Valid options: 'box', 'ellipse'.
2. Image:
- Imported images with these properties:
* width: (required) pixel resolution width.
* height: (required) pixel resolution height.
* id: (required) identifier for the image.
* flipX: (optional) horizontal flip. Valid values: 0 (no flip), 1 (flip).
* flipY: (optional) vertical flip. Valid values: 0 (no flip), 1 (flip).
* orientation: (optional) rotation in multiples of 90 degrees, clockwise. Valid values:
- 0: No rotation.
- 1: Rotate 90 degrees.
- 2: Rotate 180 degrees.
- 3: Rotate 270 degrees.
Note: Width and height remain unchanged upon flipping.
3. Sound:
- Sound effects with these properties:
* id: (required) identifier for the sound.
* volume: (optional) custom volume. Valid values are a float from 0 to 1.
4. Music:
- In contract to sound effects, only one music can be played at a time
- Music is using the same API to initilize just like sound.
- Music loops by default
- Music with these config options:
* id: (required) identifier for the sound.
* volume: (optional) custom volume. Valid values are a float from 0 to 1.
* start: (optional) a float from 0 to 1 used for cropping and indicates the start of the cropping
* end: (optional) a float from 0 to 1 used for cropping and indicates the end of the cropping
*/
//Note game dimensions are 2048x2732
var gameState = 'intro'; // 'intro', 'playing', 'finished', 'finalScore', 'win', or 'scores'
var introContainer = null;
var startButton = null;
var scoreButton = null;
var finalScoreScreenContainer = null; // For final score screen elements
var finalScoreTextElement = null; // For the text on final score screen
var scoreDisplayContainer = null; // For score display screen
var gameBackground = null; // To manage the game's background image
var dragNode = null; // Centralized declaration, used by player controls
var terrainBlocks = []; // Original line
// PlayerCharacter and scoreTxt are already declared globally and initialized to null later
// var playerCharacter = null;
// var scoreTxt = null;
// Forward declaration for the main game initialization logic
function initializeMainGame() {
// Add game background, ensuring any old one is removed
if (gameBackground && typeof gameBackground.destroy === 'function') {
gameBackground.destroy();
}
gameBackground = LK.getAsset('Gamebackground', {
// Use the global gameBackground variable
x: 0,
y: 0,
width: 2048,
height: 2732,
anchorX: 0,
// Optional, default is 0 for x=0, y=0
anchorY: 0 // Optional, default is 0 for x=0, y=0
});
game.addChildAt(gameBackground, 0); // Add at index 0 to ensure it's in the back
LK.playMusic('Gamemusic1', {
loop: false,
onFinish: handleMusicEnd
}); // Play music without looping and set onFinish callback
// Re-initialize scoreTxt if it was part of a previous game state and cleaned up
if (scoreTxt && typeof scoreTxt.destroy === 'function') {
if (scoreTxt.parent && typeof scoreTxt.parent.removeChild === 'function') {
scoreTxt.parent.removeChild(scoreTxt);
}
scoreTxt.destroy();
}
scoreTxt = new Text2('0', {
size: 150,
fill: 0xFFFFFF
});
scoreTxt.setText(LK.getScore());
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
playerCharacter = new PlayerCharacter();
// Position player character near the bottom center
playerCharacter.x = 2048 / 2;
playerCharacter.y = 2732 - playerCharacter.height - 100; // Offset from bottom
game.addChild(playerCharacter);
// Event listeners for player control
// These are assigned when the game starts, so gameState is 'playing'
game.down = function (x, y, obj) {
if (!playerCharacter) {
return;
} // Guard against playerCharacter not being ready
// Check if the touch is on the player character
dragNode = playerCharacter;
};
game.move = function (x, y, obj) {
if (!playerCharacter) {
return;
} // Guard
// Move the player character horizontally based on the cursor's x position,
// ensuring it stays within the horizontal bounds of the screen.
playerCharacter.x = Math.max(playerCharacter.width / 2, Math.min(2048 - playerCharacter.width / 2, x));
};
game.up = function (x, y, obj) {
if (!playerCharacter) {
return;
} // Guard
dragNode = null; // Stop dragging
};
// Define the rhythm pattern (intervals in milliseconds between spawns)
beatIntervalsPattern = [600, 600, 600, 600, 400, 400, 600, 600, 600, 600, 400, 400, 500, 500, 500, 500, 400, 400, 600, 600];
currentBeatInPatternIndex = 0;
timeForNextBlockSpawn = Date.now() + beatIntervalsPattern[0];
// lastSpawnTime = Date.now(); // Initialize block spawning timer to start spawning blocks - Replaced by new system
gameStartTime = Date.now(); // Track when the game started
}
var playerCharacter = null;
var scoreTxt = null;
// var spawnInterval = 1200; // Milliseconds between block spawns - Replaced by beatIntervalsPattern
// var lastSpawnTime = 4; - Replaced by timeForNextBlockSpawn
var beatIntervalsPattern = []; // Defines the ms duration between each block spawn
var currentBeatInPatternIndex = 0; // Tracks the current position in the beatIntervalsPattern
var timeForNextBlockSpawn = 0; // Timestamp for when the next block should spawn
var terrainSpeed = 9;
var gameStartTime = 0; // Track when the game started
var gameDuration = 173000; // 2:53 in milliseconds (173 seconds)
// Mapping colors to hex and sound IDs
var colorMap = {
'Red': '#FF0000',
'Orange': '#FFA500',
'Yellow': '#FFFF00',
'Green': '#008000',
'Blue': '#0000FF',
'Indigo': '#4B0082',
'Violet': '#EE82EE'
};
// Play background music
// Music, score, player, and event handlers are now initialized in initializeMainGame().
var colorNoteMap = {
'Red': 'noteC',
'Orange': 'noteD',
'Yellow': 'noteE',
'Green': 'noteF',
'Blue': 'noteG',
'Indigo': 'noteA',
'Violet': 'noteB'
};
game.setBackgroundColor(0x000000); // Change background color to black
function cleanupGameElements() {
if (playerCharacter && typeof playerCharacter.destroy === 'function') {
playerCharacter.destroy();
playerCharacter = null;
}
for (var i = terrainBlocks.length - 1; i >= 0; i--) {
if (terrainBlocks[i] && typeof terrainBlocks[i].destroy === 'function') {
terrainBlocks[i].destroy();
}
}
terrainBlocks = [];
if (scoreTxt) {
if (scoreTxt.parent && typeof scoreTxt.parent.removeChild === 'function') {
scoreTxt.parent.removeChild(scoreTxt);
}
if (typeof scoreTxt.destroy === 'function') {
scoreTxt.destroy();
}
scoreTxt = null;
}
if (gameBackground && typeof gameBackground.destroy === 'function') {
gameBackground.destroy();
gameBackground = null;
}
// Clear game-specific event listeners
game.down = null;
game.move = null;
game.up = null;
}
function showFinalScoreScreen() {
gameState = 'finalScore'; // Set state before cleanup
cleanupGameElements(); // Clean up active game elements first
finalScoreScreenContainer = new Container();
game.addChild(finalScoreScreenContainer);
// Reuse Introbackground for the final score screen
var finalScoreBackgroundAsset = LK.getAsset('Introbackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
width: 2048,
height: 2732
});
finalScoreScreenContainer.addChild(finalScoreBackgroundAsset);
finalScoreBackgroundAsset.interactive = true; // Make the background tappable
var currentScore = LK.getScore();
// Save the score
saveHighScore(currentScore);
// Add "You Lose" text
var loseText = new Text2('YOU LOSE', {
size: 180,
fill: 0xFF0000,
// Red color
align: 'center'
});
loseText.anchor.set(0.5, 0.5);
loseText.x = 2048 / 2;
loseText.y = 2732 / 2 - 300;
finalScoreScreenContainer.addChild(loseText);
finalScoreTextElement = new Text2('Final Score: ' + currentScore, {
size: 120,
fill: 0xFFFFFF,
align: 'center' // PIXI v4 Text style property
});
finalScoreTextElement.anchor.set(0.5, 0.5);
finalScoreTextElement.x = 2048 / 2;
finalScoreTextElement.y = 2732 / 2 - 50; // Position above center
finalScoreScreenContainer.addChild(finalScoreTextElement);
var tapToContinueText = new Text2('Tap to Restart', {
size: 80,
fill: 0xFFFFFF,
align: 'center' // PIXI v4 Text style property
});
tapToContinueText.anchor.set(0.5, 0.5);
tapToContinueText.x = 2048 / 2;
tapToContinueText.y = 2732 / 2 + 150; // Position below the score
finalScoreScreenContainer.addChild(tapToContinueText);
finalScoreBackgroundAsset.down = function () {
// Attach tap listener to the background
if (gameState === 'finalScore') {
resetToIntro();
}
};
}
function showWinScreen() {
gameState = 'win'; // Set state to win
cleanupGameElements(); // Clean up active game elements first
// Save the score before showing win screen
saveHighScore(LK.getScore());
var winScreenContainer = new Container();
game.addChild(winScreenContainer);
// Create transparent score background
var scoreBackgroundAsset = LK.getAsset('scoreBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
alpha: 0.7 // Transparent background
});
winScreenContainer.addChild(scoreBackgroundAsset);
var winText = new Text2('YOU WIN!', {
size: 180,
fill: 0xFFD700,
// Gold color
align: 'center'
});
winText.anchor.set(0.5, 0.5);
winText.x = 2048 / 2;
winText.y = 2732 / 2 - 200;
winScreenContainer.addChild(winText);
var finalScoreText = new Text2('Final Score: ' + LK.getScore(), {
size: 120,
fill: 0xFFFFFF,
align: 'center'
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 2048 / 2;
finalScoreText.y = 2732 / 2;
winScreenContainer.addChild(finalScoreText);
// Show You Win after a short delay
LK.setTimeout(function () {
LK.showYouWin();
}, 2000);
}
function handleMusicEnd() {
// This function is called when the game music finishes
if (gameState === 'playing') {
// Proceed only if the game was actively playing
gameState = 'finished'; // Set state to finished to stop all game updates
// Stop any remaining game elements from updating
for (var i = terrainBlocks.length - 1; i >= 0; i--) {
if (terrainBlocks[i]) {
terrainBlocks[i].update = function () {}; // Disable update function
}
}
// Small delay to ensure smooth transition
LK.setTimeout(function () {
// Check if player scored more than 2000 for win condition
if (LK.getScore() >= 2000) {
showWinScreen(); // Show custom win screen with score background
} else {
showFinalScoreScreen();
}
}, 500);
}
}
function showScoreDisplay() {
gameState = 'scores';
// Clean up intro screen
if (introContainer && typeof introContainer.destroy === 'function') {
introContainer.destroy();
introContainer = null;
}
startButton = null;
scoreButton = null;
// Create score display container
scoreDisplayContainer = new Container();
game.addChild(scoreDisplayContainer);
// Add score background
var scoreBackgroundAsset = LK.getAsset('scoreBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
alpha: 0.9
});
scoreDisplayContainer.addChild(scoreBackgroundAsset);
scoreBackgroundAsset.interactive = true;
// Add title text
var titleText = new Text2('HIGH SCORES', {
size: 120,
fill: 0xFFD700,
align: 'center'
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 400;
scoreDisplayContainer.addChild(titleText);
// Get stored scores or initialize
var highScores = storage.highScores || [];
// Display scores
var scoreY = 600;
if (highScores.length === 0) {
var noScoresText = new Text2('No scores yet!', {
size: 80,
fill: 0xFFFFFF,
align: 'center'
});
noScoresText.anchor.set(0.5, 0.5);
noScoresText.x = 2048 / 2;
noScoresText.y = scoreY;
scoreDisplayContainer.addChild(noScoresText);
} else {
// Show top 10 scores
var displayCount = Math.min(10, highScores.length);
for (var i = 0; i < displayCount; i++) {
var scoreText = new Text2(i + 1 + '. ' + highScores[i], {
size: 70,
fill: 0xFFFFFF,
align: 'center'
});
scoreText.anchor.set(0.5, 0.5);
scoreText.x = 2048 / 2;
scoreText.y = scoreY + i * 100;
scoreDisplayContainer.addChild(scoreText);
}
}
// Add back button text
var backText = new Text2('Tap to go back', {
size: 60,
fill: 0xFFFFFF,
align: 'center'
});
backText.anchor.set(0.5, 0.5);
backText.x = 2048 / 2;
backText.y = 2732 - 200;
scoreDisplayContainer.addChild(backText);
// Handle tap to go back
scoreBackgroundAsset.down = function () {
if (scoreDisplayContainer && typeof scoreDisplayContainer.destroy === 'function') {
scoreDisplayContainer.destroy();
scoreDisplayContainer = null;
}
setupIntroScreen();
};
}
function saveHighScore(score) {
// Get existing high scores or initialize empty array
var highScores = storage.highScores || [];
// Add new score
highScores.push(score);
// Sort in descending order
highScores.sort(function (a, b) {
return b - a;
});
// Keep only top 10 scores
if (highScores.length > 10) {
highScores = highScores.slice(0, 10);
}
// Save back to storage
storage.highScores = highScores;
}
function resetToIntro() {
if (finalScoreScreenContainer && typeof finalScoreScreenContainer.destroy === 'function') {
finalScoreScreenContainer.destroy();
finalScoreScreenContainer = null;
}
// finalScoreTextElement is a child of finalScoreScreenContainer, so it's destroyed with it.
finalScoreTextElement = null;
LK.setScore(0); // Reset the score for the new game session
setupIntroScreen(); // Transition back to the intro screen
}
function setupIntroScreen() {
gameState = 'intro'; // Set current game state to intro
// Clean up previous intro elements if any (e.g., if returning from final score)
if (introContainer && typeof introContainer.destroy === 'function') {
introContainer.destroy();
}
introContainer = new Container();
game.addChild(introContainer);
var introBackgroundAsset = LK.getAsset('Introbackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
width: 2048,
height: 2732
});
introContainer.addChild(introBackgroundAsset);
// Clean up previous start button if any
if (startButton && typeof startButton.destroy === 'function') {
startButton.destroy();
}
// Clean up previous score button if any
if (scoreButton && typeof scoreButton.destroy === 'function') {
scoreButton.destroy();
}
var startButtonAsset = LK.getAsset('Startbutton', {
anchorX: 0.5,
anchorY: 0.5
});
startButtonAsset.x = 2048 / 2 - 250; // Move left to make room for score button
startButtonAsset.y = 2732 - 100 - startButtonAsset.height / 2; // Position near bottom
introContainer.addChild(startButtonAsset);
startButton = startButtonAsset; // Assign to global variable
// Create and add score button
var scoreButtonAsset = LK.getAsset('Scorebutton', {
anchorX: 0.5,
anchorY: 0.5
});
scoreButtonAsset.x = startButtonAsset.x + startButtonAsset.width / 2 + 100 + scoreButtonAsset.width / 2; // Position to the right of start button with 100px gap
scoreButtonAsset.y = startButtonAsset.y; // Same height as start button
introContainer.addChild(scoreButtonAsset);
scoreButton = scoreButtonAsset; // Assign to global variable
startButton.down = function () {
if (gameState !== 'intro') {
// Ensure action only if in intro state
return;
}
gameState = 'playing'; // Transition to playing state
if (introContainer && typeof introContainer.destroy === 'function') {
introContainer.destroy(); // Clean up intro screen elements
introContainer = null;
}
startButton = null; // Clear button reference
scoreButton = null; // Clear button reference
initializeMainGame(); // Initialize and start the main game
};
scoreButton.down = function () {
if (gameState !== 'intro') {
return;
}
showScoreDisplay();
};
}
// Initial call to set up the intro screen when the game first loads
setupIntroScreen();
// The original scoreTxt, playerCharacter, and game event handlers (down, move, up)
// are removed from here as they are now part of initializeMainGame.
// The old dragNode declaration was also removed as it's now global.
game.update = function () {
if (gameState !== 'playing') {
return; // Don't run game logic if not in 'playing' state
}
var currentTime = Date.now();
// Check if game duration has exceeded 2:53
if (currentTime - gameStartTime >= gameDuration) {
// Stop the game after 2:53
gameState = 'finished';
// Stop music if still playing
LK.stopMusic();
// Stop all blocks from updating
for (var i = terrainBlocks.length - 1; i >= 0; i--) {
if (terrainBlocks[i]) {
terrainBlocks[i].update = function () {}; // Disable update function
}
}
// Show final score after a short delay
LK.setTimeout(function () {
// Check if player scored more than 2000 for win condition
if (LK.getScore() >= 2000) {
showWinScreen(); // Show custom win screen with score background
} else {
showFinalScoreScreen();
}
}, 500);
return; // Exit update function
}
// Spawn new terrain blocks based on music rhythm
if (currentTime >= timeForNextBlockSpawn && gameState === 'playing') {
var colors = Object.keys(colorMap);
var randomColor = colors[Math.floor(Math.random() * colors.length)];
var noteId = colorNoteMap[randomColor];
var newBlock = new TerrainBlock(randomColor, terrainSpeed, noteId);
// Position block randomly at the top within game width
newBlock.x = Math.random() * (2048 - newBlock.width) + newBlock.width / 2;
newBlock.y = -newBlock.height; // Start above the screen
terrainBlocks.push(newBlock);
game.addChild(newBlock);
currentBeatInPatternIndex++;
if (currentBeatInPatternIndex >= beatIntervalsPattern.length) {
currentBeatInPatternIndex = 0; // Loop the pattern
}
timeForNextBlockSpawn = currentTime + beatIntervalsPattern[currentBeatInPatternIndex];
// Gradually increase game speed / difficulty (terrainSpeed only)
terrainSpeed = Math.min(15, terrainSpeed + 0.05);
}
// Update and check collisions for terrain blocks
// Update and check collisions for terrain blocks
for (var i = terrainBlocks.length - 1; i >= 0; i--) {
var block = terrainBlocks[i];
if (block.lastY === undefined) {
block.lastY = block.y;
}
// Check if block is off-screen
if (block.lastY < 2732 + block.height && block.y >= 2732 + block.height) {
// Missed block - just remove it without penalty
block.destroy();
terrainBlocks.splice(i, 1);
continue; // Skip to next block
}
// Check for intersection with player character
var currentIntersecting = block.intersects(playerCharacter);
if (currentIntersecting) {
// Collect any color block
LK.setScore(LK.getScore() + 10);
scoreTxt.setText(LK.getScore());
// Create animation effect based on block color
var animationAsset = null;
if (block.color === 'Red') {
animationAsset = LK.getAsset('Redanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (block.color === 'Orange') {
animationAsset = LK.getAsset('Orangeanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (block.color === 'Yellow') {
animationAsset = LK.getAsset('Yellowanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (block.color === 'Green') {
animationAsset = LK.getAsset('Greenanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (block.color === 'Blue') {
animationAsset = LK.getAsset('Blueanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (block.color === 'Indigo') {
animationAsset = LK.getAsset('Purpleanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (block.color === 'Violet') {
animationAsset = LK.getAsset('Pinkanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
}
if (animationAsset) {
// Position animation at block location
animationAsset.x = block.x;
animationAsset.y = block.y;
game.addChild(animationAsset);
// Animate the animation asset
tween(animationAsset, {
scaleX: 3,
scaleY: 3,
alpha: 0,
rotation: Math.PI * 2
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
animationAsset.destroy();
}
});
}
// Add visual feedback with scale animation on player
tween(playerCharacter, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(playerCharacter, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
// Flash effect on collected block
tween(block, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut
});
// Also fade out the line
if (block.line) {
tween(block.line, {
alpha: 0
}, {
duration: 200,
easing: tween.easeOut
});
}
// Play specific sound based on block color with smooth volume fade
var sound = null;
if (block.color === 'Red') {
sound = LK.getSound('redsound1');
} else if (block.color === 'Orange') {
sound = LK.getSound('orangesound1');
} else if (block.color === 'Yellow') {
sound = LK.getSound('yellowsound1');
} else if (block.color === 'Green') {
sound = LK.getSound('greensound1');
} else if (block.color === 'Blue') {
sound = LK.getSound('bluesound1');
} else if (block.color === 'Indigo') {
sound = LK.getSound('purplesound1');
} else if (block.color === 'Violet') {
sound = LK.getSound('pinksound1');
}
if (sound) {
// Set initial volume to 0 for fade-in effect
sound.volume = 0;
sound.play();
// Tween volume from 0 to 1 over 200ms for smooth fade-in
tween(sound, {
volume: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
block.destroy();
terrainBlocks.splice(i, 1);
}
block.lastY = block.y;
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var PlayerCharacter = Container.expand(function () {
var self = Container.call(this);
var characterGraphics = self.attachAsset('playerCharacter', {
anchorX: 0.5,
anchorY: 0.5
});
// Set brown transparent color
characterGraphics.alpha = 0.7;
return self;
});
//Storage library which should be used for persistent game data
// var storage = LK.import('@upit/storage.v1');
//Library for using the camera (the background becomes the user's camera video feed) and the microphone. It can access face coordinates for interactive play, as well detect microphone volume / voice interactions
// var facekit = LK.import('@upit/facekit.v1');
//Classes can only be defined here. You cannot create inline classes in the games code.
var TerrainBlock = Container.expand(function (color, speed, noteId) {
var self = Container.call(this);
self.color = color;
self.speed = speed;
self.noteId = noteId;
var blockGraphics = self.attachAsset('terrainBlock' + self.color, {
anchorX: 0.5,
anchorY: 0.5
});
// Create vertical line extending from block
self.line = new Container();
// Get color hex value from colorMap
var lineColor = 0xFFFFFF; // Default white
if (color === 'Red') {
lineColor = 0xFF0000;
} else if (color === 'Orange') {
lineColor = 0xFFA500;
} else if (color === 'Yellow') {
lineColor = 0xFFFF00;
} else if (color === 'Green') {
lineColor = 0x008000;
} else if (color === 'Blue') {
lineColor = 0x0000FF;
} else if (color === 'Indigo') {
lineColor = 0x4B0082;
} else if (color === 'Violet') {
lineColor = 0xEE82EE;
}
// Create thin vertical line shape
var lineGraphics = LK.getAsset('line' + self.color, {
width: 50,
height: 500,
// Shorter line
color: lineColor,
shape: 'box',
anchorX: 0.5,
anchorY: 1
});
self.line.addChild(lineGraphics);
self.addChild(self.line);
self.update = function () {
self.y += self.speed;
// Keep line positioned at block center extending upward
if (self.line) {
self.line.x = 0;
self.line.y = 0;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000 //Init game with black background
});
/****
* Game Code
****/
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
//Only include the plugins you need to create the game.
//We have access to the following plugins. (Note that the variable names used are mandetory for each plugin)
// Initialize assets used in this game. Scale them according to what is needed for the game.
// or via static code analysis based on their usage in the code.
// Assets are automatically created and loaded either dynamically during gameplay
/*
Supported Types:
1. Shape:
- Simple geometric figures with these properties:
* width: (required) pixel width of the shape.
* height: (required) pixel height of the shape.
* color: (required) color of the shape.
* shape: (required) type of shape. Valid options: 'box', 'ellipse'.
2. Image:
- Imported images with these properties:
* width: (required) pixel resolution width.
* height: (required) pixel resolution height.
* id: (required) identifier for the image.
* flipX: (optional) horizontal flip. Valid values: 0 (no flip), 1 (flip).
* flipY: (optional) vertical flip. Valid values: 0 (no flip), 1 (flip).
* orientation: (optional) rotation in multiples of 90 degrees, clockwise. Valid values:
- 0: No rotation.
- 1: Rotate 90 degrees.
- 2: Rotate 180 degrees.
- 3: Rotate 270 degrees.
Note: Width and height remain unchanged upon flipping.
3. Sound:
- Sound effects with these properties:
* id: (required) identifier for the sound.
* volume: (optional) custom volume. Valid values are a float from 0 to 1.
4. Music:
- In contract to sound effects, only one music can be played at a time
- Music is using the same API to initilize just like sound.
- Music loops by default
- Music with these config options:
* id: (required) identifier for the sound.
* volume: (optional) custom volume. Valid values are a float from 0 to 1.
* start: (optional) a float from 0 to 1 used for cropping and indicates the start of the cropping
* end: (optional) a float from 0 to 1 used for cropping and indicates the end of the cropping
*/
//Note game dimensions are 2048x2732
var gameState = 'intro'; // 'intro', 'playing', 'finished', 'finalScore', 'win', or 'scores'
var introContainer = null;
var startButton = null;
var scoreButton = null;
var finalScoreScreenContainer = null; // For final score screen elements
var finalScoreTextElement = null; // For the text on final score screen
var scoreDisplayContainer = null; // For score display screen
var gameBackground = null; // To manage the game's background image
var dragNode = null; // Centralized declaration, used by player controls
var terrainBlocks = []; // Original line
// PlayerCharacter and scoreTxt are already declared globally and initialized to null later
// var playerCharacter = null;
// var scoreTxt = null;
// Forward declaration for the main game initialization logic
function initializeMainGame() {
// Add game background, ensuring any old one is removed
if (gameBackground && typeof gameBackground.destroy === 'function') {
gameBackground.destroy();
}
gameBackground = LK.getAsset('Gamebackground', {
// Use the global gameBackground variable
x: 0,
y: 0,
width: 2048,
height: 2732,
anchorX: 0,
// Optional, default is 0 for x=0, y=0
anchorY: 0 // Optional, default is 0 for x=0, y=0
});
game.addChildAt(gameBackground, 0); // Add at index 0 to ensure it's in the back
LK.playMusic('Gamemusic1', {
loop: false,
onFinish: handleMusicEnd
}); // Play music without looping and set onFinish callback
// Re-initialize scoreTxt if it was part of a previous game state and cleaned up
if (scoreTxt && typeof scoreTxt.destroy === 'function') {
if (scoreTxt.parent && typeof scoreTxt.parent.removeChild === 'function') {
scoreTxt.parent.removeChild(scoreTxt);
}
scoreTxt.destroy();
}
scoreTxt = new Text2('0', {
size: 150,
fill: 0xFFFFFF
});
scoreTxt.setText(LK.getScore());
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
playerCharacter = new PlayerCharacter();
// Position player character near the bottom center
playerCharacter.x = 2048 / 2;
playerCharacter.y = 2732 - playerCharacter.height - 100; // Offset from bottom
game.addChild(playerCharacter);
// Event listeners for player control
// These are assigned when the game starts, so gameState is 'playing'
game.down = function (x, y, obj) {
if (!playerCharacter) {
return;
} // Guard against playerCharacter not being ready
// Check if the touch is on the player character
dragNode = playerCharacter;
};
game.move = function (x, y, obj) {
if (!playerCharacter) {
return;
} // Guard
// Move the player character horizontally based on the cursor's x position,
// ensuring it stays within the horizontal bounds of the screen.
playerCharacter.x = Math.max(playerCharacter.width / 2, Math.min(2048 - playerCharacter.width / 2, x));
};
game.up = function (x, y, obj) {
if (!playerCharacter) {
return;
} // Guard
dragNode = null; // Stop dragging
};
// Define the rhythm pattern (intervals in milliseconds between spawns)
beatIntervalsPattern = [600, 600, 600, 600, 400, 400, 600, 600, 600, 600, 400, 400, 500, 500, 500, 500, 400, 400, 600, 600];
currentBeatInPatternIndex = 0;
timeForNextBlockSpawn = Date.now() + beatIntervalsPattern[0];
// lastSpawnTime = Date.now(); // Initialize block spawning timer to start spawning blocks - Replaced by new system
gameStartTime = Date.now(); // Track when the game started
}
var playerCharacter = null;
var scoreTxt = null;
// var spawnInterval = 1200; // Milliseconds between block spawns - Replaced by beatIntervalsPattern
// var lastSpawnTime = 4; - Replaced by timeForNextBlockSpawn
var beatIntervalsPattern = []; // Defines the ms duration between each block spawn
var currentBeatInPatternIndex = 0; // Tracks the current position in the beatIntervalsPattern
var timeForNextBlockSpawn = 0; // Timestamp for when the next block should spawn
var terrainSpeed = 9;
var gameStartTime = 0; // Track when the game started
var gameDuration = 173000; // 2:53 in milliseconds (173 seconds)
// Mapping colors to hex and sound IDs
var colorMap = {
'Red': '#FF0000',
'Orange': '#FFA500',
'Yellow': '#FFFF00',
'Green': '#008000',
'Blue': '#0000FF',
'Indigo': '#4B0082',
'Violet': '#EE82EE'
};
// Play background music
// Music, score, player, and event handlers are now initialized in initializeMainGame().
var colorNoteMap = {
'Red': 'noteC',
'Orange': 'noteD',
'Yellow': 'noteE',
'Green': 'noteF',
'Blue': 'noteG',
'Indigo': 'noteA',
'Violet': 'noteB'
};
game.setBackgroundColor(0x000000); // Change background color to black
function cleanupGameElements() {
if (playerCharacter && typeof playerCharacter.destroy === 'function') {
playerCharacter.destroy();
playerCharacter = null;
}
for (var i = terrainBlocks.length - 1; i >= 0; i--) {
if (terrainBlocks[i] && typeof terrainBlocks[i].destroy === 'function') {
terrainBlocks[i].destroy();
}
}
terrainBlocks = [];
if (scoreTxt) {
if (scoreTxt.parent && typeof scoreTxt.parent.removeChild === 'function') {
scoreTxt.parent.removeChild(scoreTxt);
}
if (typeof scoreTxt.destroy === 'function') {
scoreTxt.destroy();
}
scoreTxt = null;
}
if (gameBackground && typeof gameBackground.destroy === 'function') {
gameBackground.destroy();
gameBackground = null;
}
// Clear game-specific event listeners
game.down = null;
game.move = null;
game.up = null;
}
function showFinalScoreScreen() {
gameState = 'finalScore'; // Set state before cleanup
cleanupGameElements(); // Clean up active game elements first
finalScoreScreenContainer = new Container();
game.addChild(finalScoreScreenContainer);
// Reuse Introbackground for the final score screen
var finalScoreBackgroundAsset = LK.getAsset('Introbackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
width: 2048,
height: 2732
});
finalScoreScreenContainer.addChild(finalScoreBackgroundAsset);
finalScoreBackgroundAsset.interactive = true; // Make the background tappable
var currentScore = LK.getScore();
// Save the score
saveHighScore(currentScore);
// Add "You Lose" text
var loseText = new Text2('YOU LOSE', {
size: 180,
fill: 0xFF0000,
// Red color
align: 'center'
});
loseText.anchor.set(0.5, 0.5);
loseText.x = 2048 / 2;
loseText.y = 2732 / 2 - 300;
finalScoreScreenContainer.addChild(loseText);
finalScoreTextElement = new Text2('Final Score: ' + currentScore, {
size: 120,
fill: 0xFFFFFF,
align: 'center' // PIXI v4 Text style property
});
finalScoreTextElement.anchor.set(0.5, 0.5);
finalScoreTextElement.x = 2048 / 2;
finalScoreTextElement.y = 2732 / 2 - 50; // Position above center
finalScoreScreenContainer.addChild(finalScoreTextElement);
var tapToContinueText = new Text2('Tap to Restart', {
size: 80,
fill: 0xFFFFFF,
align: 'center' // PIXI v4 Text style property
});
tapToContinueText.anchor.set(0.5, 0.5);
tapToContinueText.x = 2048 / 2;
tapToContinueText.y = 2732 / 2 + 150; // Position below the score
finalScoreScreenContainer.addChild(tapToContinueText);
finalScoreBackgroundAsset.down = function () {
// Attach tap listener to the background
if (gameState === 'finalScore') {
resetToIntro();
}
};
}
function showWinScreen() {
gameState = 'win'; // Set state to win
cleanupGameElements(); // Clean up active game elements first
// Save the score before showing win screen
saveHighScore(LK.getScore());
var winScreenContainer = new Container();
game.addChild(winScreenContainer);
// Create transparent score background
var scoreBackgroundAsset = LK.getAsset('scoreBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
alpha: 0.7 // Transparent background
});
winScreenContainer.addChild(scoreBackgroundAsset);
var winText = new Text2('YOU WIN!', {
size: 180,
fill: 0xFFD700,
// Gold color
align: 'center'
});
winText.anchor.set(0.5, 0.5);
winText.x = 2048 / 2;
winText.y = 2732 / 2 - 200;
winScreenContainer.addChild(winText);
var finalScoreText = new Text2('Final Score: ' + LK.getScore(), {
size: 120,
fill: 0xFFFFFF,
align: 'center'
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 2048 / 2;
finalScoreText.y = 2732 / 2;
winScreenContainer.addChild(finalScoreText);
// Show You Win after a short delay
LK.setTimeout(function () {
LK.showYouWin();
}, 2000);
}
function handleMusicEnd() {
// This function is called when the game music finishes
if (gameState === 'playing') {
// Proceed only if the game was actively playing
gameState = 'finished'; // Set state to finished to stop all game updates
// Stop any remaining game elements from updating
for (var i = terrainBlocks.length - 1; i >= 0; i--) {
if (terrainBlocks[i]) {
terrainBlocks[i].update = function () {}; // Disable update function
}
}
// Small delay to ensure smooth transition
LK.setTimeout(function () {
// Check if player scored more than 2000 for win condition
if (LK.getScore() >= 2000) {
showWinScreen(); // Show custom win screen with score background
} else {
showFinalScoreScreen();
}
}, 500);
}
}
function showScoreDisplay() {
gameState = 'scores';
// Clean up intro screen
if (introContainer && typeof introContainer.destroy === 'function') {
introContainer.destroy();
introContainer = null;
}
startButton = null;
scoreButton = null;
// Create score display container
scoreDisplayContainer = new Container();
game.addChild(scoreDisplayContainer);
// Add score background
var scoreBackgroundAsset = LK.getAsset('scoreBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
alpha: 0.9
});
scoreDisplayContainer.addChild(scoreBackgroundAsset);
scoreBackgroundAsset.interactive = true;
// Add title text
var titleText = new Text2('HIGH SCORES', {
size: 120,
fill: 0xFFD700,
align: 'center'
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 400;
scoreDisplayContainer.addChild(titleText);
// Get stored scores or initialize
var highScores = storage.highScores || [];
// Display scores
var scoreY = 600;
if (highScores.length === 0) {
var noScoresText = new Text2('No scores yet!', {
size: 80,
fill: 0xFFFFFF,
align: 'center'
});
noScoresText.anchor.set(0.5, 0.5);
noScoresText.x = 2048 / 2;
noScoresText.y = scoreY;
scoreDisplayContainer.addChild(noScoresText);
} else {
// Show top 10 scores
var displayCount = Math.min(10, highScores.length);
for (var i = 0; i < displayCount; i++) {
var scoreText = new Text2(i + 1 + '. ' + highScores[i], {
size: 70,
fill: 0xFFFFFF,
align: 'center'
});
scoreText.anchor.set(0.5, 0.5);
scoreText.x = 2048 / 2;
scoreText.y = scoreY + i * 100;
scoreDisplayContainer.addChild(scoreText);
}
}
// Add back button text
var backText = new Text2('Tap to go back', {
size: 60,
fill: 0xFFFFFF,
align: 'center'
});
backText.anchor.set(0.5, 0.5);
backText.x = 2048 / 2;
backText.y = 2732 - 200;
scoreDisplayContainer.addChild(backText);
// Handle tap to go back
scoreBackgroundAsset.down = function () {
if (scoreDisplayContainer && typeof scoreDisplayContainer.destroy === 'function') {
scoreDisplayContainer.destroy();
scoreDisplayContainer = null;
}
setupIntroScreen();
};
}
function saveHighScore(score) {
// Get existing high scores or initialize empty array
var highScores = storage.highScores || [];
// Add new score
highScores.push(score);
// Sort in descending order
highScores.sort(function (a, b) {
return b - a;
});
// Keep only top 10 scores
if (highScores.length > 10) {
highScores = highScores.slice(0, 10);
}
// Save back to storage
storage.highScores = highScores;
}
function resetToIntro() {
if (finalScoreScreenContainer && typeof finalScoreScreenContainer.destroy === 'function') {
finalScoreScreenContainer.destroy();
finalScoreScreenContainer = null;
}
// finalScoreTextElement is a child of finalScoreScreenContainer, so it's destroyed with it.
finalScoreTextElement = null;
LK.setScore(0); // Reset the score for the new game session
setupIntroScreen(); // Transition back to the intro screen
}
function setupIntroScreen() {
gameState = 'intro'; // Set current game state to intro
// Clean up previous intro elements if any (e.g., if returning from final score)
if (introContainer && typeof introContainer.destroy === 'function') {
introContainer.destroy();
}
introContainer = new Container();
game.addChild(introContainer);
var introBackgroundAsset = LK.getAsset('Introbackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
width: 2048,
height: 2732
});
introContainer.addChild(introBackgroundAsset);
// Clean up previous start button if any
if (startButton && typeof startButton.destroy === 'function') {
startButton.destroy();
}
// Clean up previous score button if any
if (scoreButton && typeof scoreButton.destroy === 'function') {
scoreButton.destroy();
}
var startButtonAsset = LK.getAsset('Startbutton', {
anchorX: 0.5,
anchorY: 0.5
});
startButtonAsset.x = 2048 / 2 - 250; // Move left to make room for score button
startButtonAsset.y = 2732 - 100 - startButtonAsset.height / 2; // Position near bottom
introContainer.addChild(startButtonAsset);
startButton = startButtonAsset; // Assign to global variable
// Create and add score button
var scoreButtonAsset = LK.getAsset('Scorebutton', {
anchorX: 0.5,
anchorY: 0.5
});
scoreButtonAsset.x = startButtonAsset.x + startButtonAsset.width / 2 + 100 + scoreButtonAsset.width / 2; // Position to the right of start button with 100px gap
scoreButtonAsset.y = startButtonAsset.y; // Same height as start button
introContainer.addChild(scoreButtonAsset);
scoreButton = scoreButtonAsset; // Assign to global variable
startButton.down = function () {
if (gameState !== 'intro') {
// Ensure action only if in intro state
return;
}
gameState = 'playing'; // Transition to playing state
if (introContainer && typeof introContainer.destroy === 'function') {
introContainer.destroy(); // Clean up intro screen elements
introContainer = null;
}
startButton = null; // Clear button reference
scoreButton = null; // Clear button reference
initializeMainGame(); // Initialize and start the main game
};
scoreButton.down = function () {
if (gameState !== 'intro') {
return;
}
showScoreDisplay();
};
}
// Initial call to set up the intro screen when the game first loads
setupIntroScreen();
// The original scoreTxt, playerCharacter, and game event handlers (down, move, up)
// are removed from here as they are now part of initializeMainGame.
// The old dragNode declaration was also removed as it's now global.
game.update = function () {
if (gameState !== 'playing') {
return; // Don't run game logic if not in 'playing' state
}
var currentTime = Date.now();
// Check if game duration has exceeded 2:53
if (currentTime - gameStartTime >= gameDuration) {
// Stop the game after 2:53
gameState = 'finished';
// Stop music if still playing
LK.stopMusic();
// Stop all blocks from updating
for (var i = terrainBlocks.length - 1; i >= 0; i--) {
if (terrainBlocks[i]) {
terrainBlocks[i].update = function () {}; // Disable update function
}
}
// Show final score after a short delay
LK.setTimeout(function () {
// Check if player scored more than 2000 for win condition
if (LK.getScore() >= 2000) {
showWinScreen(); // Show custom win screen with score background
} else {
showFinalScoreScreen();
}
}, 500);
return; // Exit update function
}
// Spawn new terrain blocks based on music rhythm
if (currentTime >= timeForNextBlockSpawn && gameState === 'playing') {
var colors = Object.keys(colorMap);
var randomColor = colors[Math.floor(Math.random() * colors.length)];
var noteId = colorNoteMap[randomColor];
var newBlock = new TerrainBlock(randomColor, terrainSpeed, noteId);
// Position block randomly at the top within game width
newBlock.x = Math.random() * (2048 - newBlock.width) + newBlock.width / 2;
newBlock.y = -newBlock.height; // Start above the screen
terrainBlocks.push(newBlock);
game.addChild(newBlock);
currentBeatInPatternIndex++;
if (currentBeatInPatternIndex >= beatIntervalsPattern.length) {
currentBeatInPatternIndex = 0; // Loop the pattern
}
timeForNextBlockSpawn = currentTime + beatIntervalsPattern[currentBeatInPatternIndex];
// Gradually increase game speed / difficulty (terrainSpeed only)
terrainSpeed = Math.min(15, terrainSpeed + 0.05);
}
// Update and check collisions for terrain blocks
// Update and check collisions for terrain blocks
for (var i = terrainBlocks.length - 1; i >= 0; i--) {
var block = terrainBlocks[i];
if (block.lastY === undefined) {
block.lastY = block.y;
}
// Check if block is off-screen
if (block.lastY < 2732 + block.height && block.y >= 2732 + block.height) {
// Missed block - just remove it without penalty
block.destroy();
terrainBlocks.splice(i, 1);
continue; // Skip to next block
}
// Check for intersection with player character
var currentIntersecting = block.intersects(playerCharacter);
if (currentIntersecting) {
// Collect any color block
LK.setScore(LK.getScore() + 10);
scoreTxt.setText(LK.getScore());
// Create animation effect based on block color
var animationAsset = null;
if (block.color === 'Red') {
animationAsset = LK.getAsset('Redanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (block.color === 'Orange') {
animationAsset = LK.getAsset('Orangeanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (block.color === 'Yellow') {
animationAsset = LK.getAsset('Yellowanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (block.color === 'Green') {
animationAsset = LK.getAsset('Greenanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (block.color === 'Blue') {
animationAsset = LK.getAsset('Blueanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (block.color === 'Indigo') {
animationAsset = LK.getAsset('Purpleanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (block.color === 'Violet') {
animationAsset = LK.getAsset('Pinkanimation1', {
anchorX: 0.5,
anchorY: 0.5
});
}
if (animationAsset) {
// Position animation at block location
animationAsset.x = block.x;
animationAsset.y = block.y;
game.addChild(animationAsset);
// Animate the animation asset
tween(animationAsset, {
scaleX: 3,
scaleY: 3,
alpha: 0,
rotation: Math.PI * 2
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
animationAsset.destroy();
}
});
}
// Add visual feedback with scale animation on player
tween(playerCharacter, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(playerCharacter, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
// Flash effect on collected block
tween(block, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut
});
// Also fade out the line
if (block.line) {
tween(block.line, {
alpha: 0
}, {
duration: 200,
easing: tween.easeOut
});
}
// Play specific sound based on block color with smooth volume fade
var sound = null;
if (block.color === 'Red') {
sound = LK.getSound('redsound1');
} else if (block.color === 'Orange') {
sound = LK.getSound('orangesound1');
} else if (block.color === 'Yellow') {
sound = LK.getSound('yellowsound1');
} else if (block.color === 'Green') {
sound = LK.getSound('greensound1');
} else if (block.color === 'Blue') {
sound = LK.getSound('bluesound1');
} else if (block.color === 'Indigo') {
sound = LK.getSound('purplesound1');
} else if (block.color === 'Violet') {
sound = LK.getSound('pinksound1');
}
if (sound) {
// Set initial volume to 0 for fade-in effect
sound.volume = 0;
sound.play();
// Tween volume from 0 to 1 over 200ms for smooth fade-in
tween(sound, {
volume: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
block.destroy();
terrainBlocks.splice(i, 1);
}
block.lastY = block.y;
}
};