User prompt
please create a new asset named Background and add it to the backgroudn container as the background for the game. stretch it accross the entire screen
User prompt
when the hero is about to move to a cell that is on the 5th state which means it's game over, the game over state triggers the moment the hero is ABOUT to jump on that cell, thus it looks like the game ends prematrely. wait for the hero to land on that cell first before ending the game ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Go to the GridService Find the GridService function. Inside it, locate the createAllVisuals function. 2. Modify the Background Loop The very first loop in that function is the problem. It currently draws a background for all 10 rows of the grid. We need to tell it to only draw backgrounds for the beat grid rows (the bottom 4). Replace the first for loop in createAllVisuals with this corrected version: This is the original, incorrect code: Generated javascript // 1. Create the background cells and add them to the midground for (var r = 0; r < this.gameGrid.rows; r++) { // <-- This draws for ALL 10 rows for (var c = 0; c < this.gameGrid.cols; c++) { var bgCell = LK.getAsset('emptycell', { //... }); //... midgroundContainer.addChild(bgCell); } } Use code with caution. JavaScript Change it to this: Generated javascript // 1. Create the background cells ONLY for the BEAT GRID and add them to the midground for (var r = this.gameGrid.playableRows; r < this.gameGrid.rows; r++) { // <-- CORRECTED for (var c = 0; c < this.gameGrid.cols; c++) { var bgCell = LK.getAsset('emptycell', { anchorX: 0.5, anchorY: 0.5, alpha: 1.0 }); bgCell.x = GameConstants.GRID_LEFT + c * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; bgCell.y = GameConstants.GRID_TOP + r * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; midgroundContainer.addChild(bgCell); } } Use code with caution. JavaScript By changing the loop's starting point from r = 0 to r = this.gameGrid.playableRows, you are now only drawing backgrounds behind the beat grid. The playable area will be left empty, allowing the PlayableCell objects to have full control.
User prompt
each cell must have 5 frames or states. The first 4 frames are disaplyed correctly, but at the 5th frame you revert back to frame 1, when instead you should remove the asset completely, there should be no asset remaining in that cell on the 5th state
User prompt
Fix the PlayableCell Class Logic Now, let's fix the health and visual logic to correctly handle the final "hole" state. Find the PlayableCell function. Replace the entire PlayableCell function with this corrected version. I have marked the specific changes with comments. Generated javascript function PlayableCell(row, col) { this.health = 4; this.gfx = null; this.entity = null; this.row = row; this.col = col; this.updateVisual = function () { if (this.gfx) { this.gfx.destroy(); } var assetName = null; if (this.health === 4) assetName = 'emptycell'; else if (this.health === 3) assetName = 'emptycell2'; else if (this.health === 2) assetName = 'emptycell3'; else if (this.health === 1) assetName = 'emptycell4'; // If health is 0, assetName will be null, creating a hole. if (assetName) { this.gfx = LK.getAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); this.gfx.x = GameConstants.GRID_LEFT + this.col * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; this.gfx.y = GameConstants.GRID_TOP + this.row * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; midgroundContainer.addChild(this.gfx); } else { this.gfx = null; // This now correctly handles the hole state. } }; this.takeDamage = function () { // --- FIX 1: Change condition from >= to > --- // This allows health to reach 0. if (this.health > 0) { this.health--; this.updateVisual(); } }; this.isHole = function () { // --- FIX 2: Change condition to <= --- // A cell is a hole if its health is 0 or less. return this.health <= 0; }; // Initialize the first visual this.updateVisual(); }
User prompt
ensure the cells go from emptycell, next to emptycell2, next emptycell3, emptycell4, and only on the 5th frame you remove all cells completely
User prompt
Next, replace your entire moveVertical function with this new version. It does the exact same thing for vertical movement. Generated javascript // In PlayerService... this.moveVertical = function (direction) { // --- Capture the state BEFORE the move --- var oldRow = this.hero.gridRow; var oldCol = this.hero.currentCol; var minRow = 0; var maxRow = this.gameGrid.playableRows - 1; var newRow = this.hero.gridRow !== undefined ? this.hero.gridRow : GameConstants.CHAR_ROW; newRow += direction; if (newRow < minRow) { newRow = maxRow; } else if (newRow > maxRow) { newRow = minRow; } this.hero.gridRow = newRow; var ny = GameConstants.GRID_TOP + newRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; // --- Add the onFinish callback to the tween --- tween(this.hero, { x: this.hero.x, y: ny }, { duration: 180, easing: tween.cubicOut, onFinish: function() { // This code runs ONLY after the hero arrives var previousCell = gameGrid.grid[oldRow][oldCol]; if (previousCell) { previousCell.takeDamage(); } } }); AudioService.playMove(); }; ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Upgrade the PlayerService Move Functions Now, we will add the correct logic to the two functions that actually move the hero: move (for horizontal movement) and moveVertical. Go to the PlayerService function. Replace your entire move function with this new version. It captures the hero's old position and then uses the tween's onFinish callback to damage the cell at that old position after the animation is done. Generated javascript // In PlayerService... this.move = function (direction) { // --- Capture the state BEFORE the move --- var oldRow = this.hero.gridRow; var oldCol = this.hero.currentCol; var newCol = this.hero.currentCol + direction; if (newCol < 0) { newCol = GameConstants.GRID_COLS - 1; } if (newCol >= GameConstants.GRID_COLS) { newCol = 0; } this.hero.currentCol = newCol; var nx = GameConstants.GRID_LEFT + newCol * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; // --- Add the onFinish callback to the tween --- tween(this.hero, { x: nx, y: this.hero.y }, { duration: 180, easing: tween.cubicOut, onFinish: function() { // This code runs ONLY after the hero arrives var previousCell = gameGrid.grid[oldRow][oldCol]; if (previousCell) { previousCell.takeDamage(); } } }); AudioService.playMove(); }; ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Remove the Incorrect Logic from SequencerService First, we must remove the code that is currently causing the problem. Go to the SequencerService function. Find the doStepActions function inside it. Delete the entire block of code that handles damaging the previous cell. It looks like this—delete all of it: Generated javascript // --- DELETE THIS ENTIRE BLOCK --- // Damage the cell the hero just moved FROM // Check if prevHeroRow is a valid number before proceeding if (prevHeroRow !== undefined && prevHeroCol !== undefined) { // Make sure it's a valid cell in the grid if (gameGrid.isPlayableCell(prevHeroRow, prevHeroCol)) { var previousPlayableCell = gameGrid.grid[prevHeroRow][prevHeroCol]; previousPlayableCell.takeDamage(); } } // --- END OF BLOCK TO DELETE ---
User prompt
Remove the Incorrect Logic from SequencerService First, we must remove the code that is currently causing the problem. Go to the SequencerService function. Find the doStepActions function inside it. Delete the entire block of code that handles damaging the previous cell. It looks like this—delete all of it: Generated javascript // --- DELETE THIS ENTIRE BLOCK --- // Damage the cell the hero just moved FROM // Check if prevHeroRow is a valid number before proceeding if (prevHeroRow !== undefined && prevHeroCol !== undefined) { // Make sure it's a valid cell in the grid if (gameGrid.isPlayableCell(prevHeroRow, prevHeroCol)) { var previousPlayableCell = gameGrid.grid[prevHeroRow][prevHeroCol]; previousPlayableCell.takeDamage(); } } // --- END OF BLOCK TO DELETE --- Use code with caution. JavaScript Step 2: Upgrade the PlayerService Move Functions Now, we will add the correct logic to the two functions that actually move the hero: move (for horizontal movement) and moveVertical. Go to the PlayerService function. Replace your entire move function with this new version. It captures the hero's old position and then uses the tween's onFinish callback to damage the cell at that old position after the animation is done. Generated javascript // In PlayerService... this.move = function (direction) { // --- Capture the state BEFORE the move --- var oldRow = this.hero.gridRow; var oldCol = this.hero.currentCol; var newCol = this.hero.currentCol + direction; if (newCol < 0) { newCol = GameConstants.GRID_COLS - 1; } if (newCol >= GameConstants.GRID_COLS) { newCol = 0; } this.hero.currentCol = newCol; var nx = GameConstants.GRID_LEFT + newCol * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; // --- Add the onFinish callback to the tween --- tween(this.hero, { x: nx, y: this.hero.y }, { duration: 180, easing: tween.cubicOut, onFinish: function() { // This code runs ONLY after the hero arrives var previousCell = gameGrid.grid[oldRow][oldCol]; if (previousCell) { previousCell.takeDamage(); } } }); AudioService.playMove(); }; Use code with caution. JavaScript Next, replace your entire moveVertical function with this new version. It does the exact same thing for vertical movement. Generated javascript // In PlayerService... this.moveVertical = function (direction) { // --- Capture the state BEFORE the move --- var oldRow = this.hero.gridRow; var oldCol = this.hero.currentCol; var minRow = 0; var maxRow = this.gameGrid.playableRows - 1; var newRow = this.hero.gridRow !== undefined ? this.hero.gridRow : GameConstants.CHAR_ROW; newRow += direction; if (newRow < minRow) { newRow = maxRow; } else if (newRow > maxRow) { newRow = minRow; } this.hero.gridRow = newRow; var ny = GameConstants.GRID_TOP + newRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; // --- Add the onFinish callback to the tween --- tween(this.hero, { x: this.hero.x, y: ny }, { duration: 180, easing: tween.cubicOut, onFinish: function() { // This code runs ONLY after the hero arrives var previousCell = gameGrid.grid[oldRow][oldCol]; if (previousCell) { previousCell.takeDamage(); } } }); AudioService.playMove(); }; ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Go to the PlayableCell Class Find the function PlayableCell(row, col). Inside it, locate the takeDamage function. It currently looks like this: Generated javascript this.takeDamage = function () { if (this.health > 0) { this.health--; this.updateVisual(); // <-- This is the line we will change } }; Use code with caution. JavaScript 2. Add the Delay We are going to wrap the this.updateVisual() call in a timed delay. Replace the line this.updateVisual(); with this new line: Generated javascript LK.setTimeout(() => { this.updateVisual(); }, 200); Use code with caution. JavaScript Your final takeDamage function should look like this: Generated javascript this.takeDamage = function () { if (this.health > 0) { this.health--; LK.setTimeout(() => { this.updateVisual(); }, 200); // <-- The new, delayed call } };
User prompt
the cell should only break AFTER the hero moves off of it, not as it's standing on it. while the hero is on the cell, it should retain it's current frame, only go to the next AFTER the hero moves off of it
User prompt
Update How PlayableCells Are Created Now that the class needs to know its row and col, we have to provide that information when we create it. Go to the _setupLogicalGrid function. Find the loop where you create the PlayableCell objects. Change the line gameGrid.grid[r][c] = new PlayableCell(); to: Generated javascript gameGrid.grid[r][c] = new PlayableCell(r, c); Use code with caution. JavaScript Step 3: Simplify the _createVisuals Function Because the PlayableCell now completely manages its own background graphic, the _createVisuals function no longer needs to. We can clean it up. Go to the _createVisuals function. Find the loop that iterates through the playable area. Delete the entire if (playableCell.gfx) block. It's no longer needed here. Your loop should now look much simpler, only responsible for placing the entities (hero, coins, etc.): Generated javascript // --- Your new, cleaner loop in _createVisuals --- for (var r = 0; r < gameGrid.playableRows; r++) { for (var c = 0; c < gameGrid.cols; c++) { var playableCell = gameGrid.grid[r][c]; var entity = playableCell.entity; // Draw the entity on top of the cell (coin, hero, etc.) if (entity) { entity.x = GameConstants.GRID_LEFT + c * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; entity.y = GameConstants.GRID_TOP + r * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; // Add entities to the FOREGROUND foregroundContainer.addChild(entity); } } }
User prompt
Upgrade the PlayableCell Class We need to tell each PlayableCell where it lives on the grid. Find your PlayableCell function. Modify its definition so it can accept its row and column when it's created. We will then use this information to position the graphic every time updateVisual is called. Replace your entire PlayableCell function with this upgraded version: Generated javascript function PlayableCell(row, col) { // <-- Added row, col // Health: 4=pristine, 3, 2, 1=damaged, 0=hole this.health = 4; this.gfx = null; // The visual asset for this cell this.entity = null; // What's ON TOP of the cell (coin, key, etc.) this.row = row; // <-- Store its row this.col = col; // <-- Store its col this.updateVisual = function () { if (this.gfx) { this.gfx.destroy(); // Remove the old visual } var assetName = null; if (this.health === 4) assetName = 'emptycell'; else if (this.health === 3) assetName = 'emptycell2'; else if (this.health === 2) assetName = 'emptycell3'; else if (this.health === 1) assetName = 'emptycell4'; if (assetName) { this.gfx = LK.getAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); // --- THIS IS THE FIX --- // Position the new graphic and add it to the scene this.gfx.x = GameConstants.GRID_LEFT + this.col * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; this.gfx.y = GameConstants.GRID_TOP + this.row * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; midgroundContainer.addChild(this.gfx); // <-- Add it to the screen! } else { this.gfx = null; // Ensure gfx is null for holes } }; this.takeDamage = function () { if (this.health > 0) { this.health--; this.updateVisual(); } }; this.isHole = function () { return this.health <= 0; }; // Initialize the first visual this.updateVisual(); }
User prompt
Implement the Game Over Check Finally, let's check if the hero landed on a hole. This logic also goes in doStepActions, right after the "Breaking" logic you just added. Generated javascript // --- ADD THIS BLOCK to doStepActions, after the breaking logic --- // Check if the hero has landed on a hole var currentPlayableCell = gameGrid.grid[hero.gridRow][hero.currentCol]; if (currentPlayableCell.isHole()) { LK.showGameOver(); return; // Stop the function immediately } // (The rest of the function, like processPlayerCollision, comes after this) Use code with caution. JavaScript One Final Tweak: Your collision code now needs to look for the entity inside the cell. Find the GameRulesService and its processPlayerCollision function. Change the first line inside that function from var entity = this.gameGrid.grid[heroRow][heroCol]; to: var entity = this.gameGrid.grid[heroRow][heroCol].entity; Also, when a coin is collected, we need to remove it from the cell. Change this.gameGrid.grid[heroRow][heroCol] = null; to: this.gameGrid.grid[heroRow][heroCol].entity = null;
User prompt
Implement the "Breaking" Logic This is where the magic happens. We'll damage the cell the hero just left. Find the SequencerService and its doStepActions function. In that function, you already have prevHeroRow and prevHeroCol. This is perfect. Right after the line this.playerService.performActions(actions);, add the following code block: Generated javascript // --- ADD THIS BLOCK to doStepActions --- // Damage the cell the hero just moved FROM // Check if prevHeroRow is a valid number before proceeding if (prevHeroRow !== undefined && prevHeroCol !== undefined) { // Make sure it's a valid cell in the grid if (gameGrid.isPlayableCell(prevHeroRow, prevHeroCol)) { var previousPlayableCell = gameGrid.grid[prevHeroRow][prevHeroCol]; previousPlayableCell.takeDamage(); } }
User prompt
Rework the Grid and Visuals Now we need to tell the game to use these new PlayableCell objects. Modify _setupLogicalGrid: Find the function _setupLogicalGrid. Right after the line gameGrid.initBeatGrid();, delete the code that creates the cellPool and places the hero and coins directly into gameGrid.grid. In its place, add this new logic. It first creates a PlayableCell for every spot, then it places the coins and hero inside those cells. Generated javascript // --- New Logic for _setupLogicalGrid --- // 1. Create a PlayableCell for every spot in the playable area for (var r = 0; r < gameGrid.playableRows; r++) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { gameGrid.grid[r][c] = new PlayableCell(); } } // 2. Now place entities ON TOP of these cells var cellPool = []; // We still need this to pick random spots for (var r = 0; r < gameGrid.playableRows; r++) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { cellPool.push({ row: r, col: c }); } } // Shuffle the pool (your shuffle code is good, keep it) for (var i = cellPool.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = cellPool[i]; cellPool[i] = cellPool[j]; cellPool[j] = temp; } // Place Hero playerService.initialize(); var hero = playerService.getHeroInstance(); var heroCellPos = cellPool.shift(); hero.gridRow = heroCellPos.row; hero.currentCol = heroCellPos.col; // IMPORTANT: Don't replace the cell, set the hero as its entity gameGrid.grid[heroCellPos.row][heroCellPos.col].entity = hero; // Place Coins gameState.totalCoins = 0; for (var r = 0; r < gameGrid.playableRows; r++) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { // If the cell doesn't already have an entity (the hero) if (gameGrid.grid[r][c].entity === null) { var coin = new Coin(); gameGrid.grid[r][c].entity = coin; gameState.totalCoins++; } } } // (The rest of your key/princess logic can stay the same) Use code with caution. JavaScript Modify _createVisuals: We need to change this function to draw both the breakable cell and the entity on top of it. Find the _createVisuals function. Replace the loop that creates visuals for playable items with this new version: Generated javascript // --- New Logic for _createVisuals --- // Create the visuals for the playable items for (var r = 0; r < gameGrid.playableRows; r++) { for (var c = 0; c < gameGrid.cols; c++) { var playableCell = gameGrid.grid[r][c]; var entity = playableCell.entity; // 1. Draw the breakable cell itself (its 'gfx') if (playableCell.gfx) { playableCell.gfx.x = GameConstants.GRID_LEFT + c * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; playableCell.gfx.y = GameConstants.GRID_TOP + r * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; // Add cell backgrounds to the MIDGROUND midgroundContainer.addChild(playableCell.gfx); } // 2. Draw the entity on top of the cell (coin, hero, etc.) if (entity) { entity.x = GameConstants.GRID_LEFT + c * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; entity.y = GameConstants.GRID_TOP + r * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; // Add entities to the FOREGROUND foregroundContainer.addChild(entity); } } }
User prompt
Create the PlayableCell Class This is the new heart of your playable area. It will manage its own state. Find a good place near your other class definitions (like Coin or Hero). Add this new class definition. It's a function that will act as a blueprint: Generated javascript /** * PlayableCell * Responsibility: To model a single walkable, breakable cell in the top grid. * It tracks its own health and visual state. */ function PlayableCell() { // Health: 4=pristine, 3, 2, 1=damaged, 0=hole this.health = 4; this.gfx = null; // The visual asset for this cell this.entity = null; // What's ON TOP of the cell (coin, key, etc.) this.updateVisual = function() { if (this.gfx) { this.gfx.destroy(); // Remove the old visual } var assetName = null; if (this.health === 4) assetName = 'emptycell'; else if (this.health === 3) assetName = 'emptycell2'; else if (this.health === 2) assetName = 'emptycell3'; else if (this.health === 1) assetName = 'emptycell4'; // If health is 0, assetName remains null (it's a hole) if (assetName) { this.gfx = LK.getAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); // We'll set the position later when we create the full grid } else { this.gfx = null; // Ensure gfx is null for holes } }; this.takeDamage = function() { if (this.health > 0) { this.health--; this.updateVisual(); } }; this.isHole = function() { return this.health <= 0; }; // Initialize the first visual this.updateVisual(); }
User prompt
please create 3 new assets named emptycell2, emptycell3 and emptycell4
Code edit (1 edits merged)
Please save this source code
User prompt
can you make the highlighter even more obvious, it's too subtle
User prompt
Create a New Function in UIService First, we need to isolate the code that creates the highlighters so we can call it whenever we want. Find the UIService function. Inside it, create a new function. Let's name it createColumnHighlights. Now, find the initialize function within UIService. Locate the entire block of code that creates the column highlights (it starts with a comment like // Create columnHighlights... and includes a for loop). Cut that entire block of code from the initialize function. Paste it inside your new createColumnHighlights function. Also, make sure the first line inside your new function is this.columnHighlights = [];. This ensures the array is cleared before you add the new highlight objects each round. 2. Update the initialize Function Now that you've moved the code, you still need to make sure the highlighters are created for the very first round. Inside the UIService's initialize function (where you just cut the code from), add a single line that calls your new function: this.createColumnHighlights();. 3. Call the New Function Every Round This is the final and most important step. We need to tell the game to recreate the highlighters every time a new round starts. Find the function named _createVisuals. This is the perfect place for this logic, as it's where all the other visual elements for a new round are created. At the end of the _createVisuals function, add a new line: uiService.createColumnHighlights();. That’s it! Now, your game flow will be: A new round starts. _clearSceneAndState wipes the screen, destroying the old highlighters. _createVisuals runs, and at the end, it calls uiService.createColumnHighlights(), which creates a fresh set of highlighters for the new round.
User prompt
I still don't see the beat highlighter. ensure the 4 layers are highligthed as the beats moves. so if the beat is on the first column, that column, or those 4 beats on each layer,should be highlighted
User prompt
the beat highlighter doesn't work anymore. there's 8 total beats, each consisting of 4 layers. the beat should highlight it's position as it keeps traveling to the right, by highligting the current column it's own with a white highlighter
User prompt
the pengu princess appearing is bugged, as it does not appear after all conditions are met. The player has to collect BOTH the key and a specific amount of coins, but the princess does nto appear after those conditions are met
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ /** * Coin * Responsibility: Collectible coin that can be picked up by the hero */ var Coin = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5 }); self.collected = false; // Add wobble animation with random offset self.wobbleOffset = Math.random() * Math.PI * 2; // Random starting phase self.wobbleSpeed = 0.0015 + Math.random() * 0.001; // Slightly varied speed self.wobbleAmount = 0.05 + Math.random() * 0.02; // 5-7 degrees in radians self.wobbleDirection = Math.random() > 0.5 ? 1 : -1; // Random initial direction self.baseRotation = 0; self.update = function () { if (!self.collected) { // Calculate wobble using sine wave var time = Date.now() * self.wobbleSpeed + self.wobbleOffset; self.gfx.rotation = self.baseRotation + Math.sin(time) * self.wobbleAmount * self.wobbleDirection; } }; self.collect = function () { if (!self.collected) { self.collected = true; tween(self, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 300, easing: tween.cubicOut, onComplete: function onComplete() { self.destroy(); } }); } }; return self; }); /** * Hero * Responsibility: To model the player character. It manages its own state and animations, * but no longer triggers its own sound effects. */ var Hero = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('hero', { anchorX: 0.5, anchorY: 0.5 }); self.currentCol = 0; self.gridRow = GameConstants.CHAR_ROW; // Track last direction and up-walk alternation self.lastDirection = "down"; // "left", "right", "up", "down" self.lastEat = false; self.upWalkAlt = false; // toggles for up-walk alternation // Helper to set hero frame based on direction and eating state self.setFrame = function (direction, isEating, isUpWalkAlt) { // Only update if changed var assetId = "hero"; if (direction === "left") { assetId = isEating ? "Hero_Left_Eat" : "Hero_Left_Walk"; } else if (direction === "right") { assetId = isEating ? "Hero_Right_Eat" : "Hero_Right_Walk"; } else if (direction === "up") { assetId = isUpWalkAlt ? "Hero_Back_Walk_2" : "Hero_Back_Walk"; } else if (direction === "down") { assetId = "hero"; } // Only swap if different if (!self.gfx.assetId || self.gfx.assetId !== assetId) { if (self.gfx) { self.gfx.destroy(); } self.gfx = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); self.gfx.assetId = assetId; // Track for future checks } self.lastDirection = direction; self.lastEat = isEating; if (direction === "up") { self.upWalkAlt = isUpWalkAlt; } }; self.moveTo = function (nx, ny, duration) { tween(self, { x: nx, y: ny }, { duration: duration || 180, easing: tween.cubicOut }); }; return self; }); /** * Key * Responsibility: Static visual marker for the key */ var Key = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('key', { anchorX: 0.5, anchorY: 0.5 }); return self; }); /** * Princess * Responsibility: Static visual marker for the princess goal */ var Princess = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('princess', { anchorX: 0.5, anchorY: 0.5 }); return self; }); /**** * Initialize Game ****/ /************************* * INITIALIZE & RUN GAME *************************/ // 1. Create Game Instance var game = new LK.Game({ backgroundColor: 0xffffff }); /**** * Game Code ****/ // Create layered containers for proper z-ordering /** * BeatCell * Responsibility: To model the state of a single cell on the beat grid. It no longer * handles its own graphics updates directly, this is delegated to the GridService. */ function BeatCell(row) { this.active = false; this.row = row; this.gfx = null; // We will create and manage the graphic externally this.visible = true; // Track visibility state this.toggle = function () { this.active = !this.active; }; this.setActive = function (val) { if (this.active !== val) { this.toggle(); } }; } var backgroundContainer = new Container(); var midgroundContainer = new Container(); var foregroundContainer = new Container(); // Add containers to game in correct order game.addChild(backgroundContainer); game.addChild(midgroundContainer); game.addChild(foregroundContainer); /** * GameGrid * Responsibility: Centralizes the entire grid (playable + beat area) into a single class. * Provides accessors for both the playable area and the beat grid, and manages their state. */ /** * GameConstants * Responsibility: To hold all static configuration and magic numbers in one place * for easy tuning and maintenance. */ function AntiCheatService(gridService, gameGrid) { this.gridService = gridService; this.gameGrid = gameGrid; this.disabledCells = []; // Array to store currently disabled cells this.cellsPerLayer = 2; // Number of cells to disable per layer this.initialize = function () { // Disable initial cells for round 1 this.disableRandomCells(); }; this.disableRandomCells = function () { this.disabledCells = []; var disabledByRow = []; // Track disabled columns for each row // Process each beat layer in order: up (0), left (1), right (2), down (3) for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { disabledByRow[r] = []; // Create list of available columns for this row var availableCols = []; for (var c = 0; c < 8; c++) { var canPlace = true; // Check minimum distance of 2 from already disabled cells in same row for (var i = 0; i < disabledByRow[r].length; i++) { if (Math.abs(c - disabledByRow[r][i]) < 3) { // minimum 2 tiles distance = 3 cells apart canPlace = false; break; } } // Check diagonal constraint for rows below the top layer // Only check the immediately previous row to avoid over-constraining if (r > 0 && canPlace) { var prevRow = r - 1; // Only check immediate previous row for (var j = 0; j < disabledByRow[prevRow].length; j++) { var prevCol = disabledByRow[prevRow][j]; // Can't place if diagonally adjacent (same col, col-1, or col+1) if (c >= prevCol - 1 && c <= prevCol + 1) { canPlace = false; break; } } } if (canPlace) { availableCols.push(c); } } // If we don't have enough available columns, fall back to simpler constraints if (availableCols.length < this.cellsPerLayer) { availableCols = []; for (var c = 0; c < 8; c++) { var canPlace = true; // Only check minimum distance within same row for (var i = 0; i < disabledByRow[r].length; i++) { if (Math.abs(c - disabledByRow[r][i]) < 3) { canPlace = false; break; } } if (canPlace) { availableCols.push(c); } } } // Shuffle available columns for (var i = availableCols.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = availableCols[i]; availableCols[i] = availableCols[j]; availableCols[j] = temp; } // Pick first two columns from shuffled available list var cellsToDisable = Math.min(this.cellsPerLayer, availableCols.length); for (var i = 0; i < cellsToDisable; i++) { var col = availableCols[i]; disabledByRow[r].push(col); var cell = this.gameGrid.getBeatCell(r, col); if (cell) { this.disabledCells.push({ row: r, col: col, cell: cell }); } } } }; this.reset = function () { // Disable new random cells with a completely new pattern this.disableRandomCells(); }; // Check if a cell is disabled this.isCellDisabled = function (beatRow, col) { for (var i = 0; i < this.disabledCells.length; i++) { var disabled = this.disabledCells[i]; if (disabled.row === beatRow && disabled.col === col) { return true; } } return false; }; } function GameGrid(game) { this.game = game; this.rows = GameConstants.TOTAL_ROWS; this.cols = GameConstants.GRID_COLS; this.grid = []; // 2D array: [row][col] for all rows (playable + beat) this.beatRows = GameConstants.BEAT_ROWS; this.playableRows = this.rows - this.beatRows; // Initialize grid with nulls for (var r = 0; r < this.rows; r++) { this.grid[r] = []; for (var c = 0; c < this.cols; c++) { this.grid[r][c] = null; } } // Create BeatCell objects for the beat grid (bottom rows) this.initBeatGrid = function () { for (var r = 0; r < this.beatRows; r++) { var gridRow = this.playableRows + r; for (var c = 0; c < this.cols; c++) { var cell = new BeatCell(r); // Create the logical object. this.grid[gridRow][c] = cell; // Store it in the data grid. } } }; // Returns the BeatCell at a given (beatRow, col) in the beat grid this.getBeatCell = function (beatRow, col) { var gridRow = this.playableRows + beatRow; return this.grid[gridRow][col]; }; // Returns true if (row, col) is in the playable area this.isPlayableCell = function (row, col) { return row >= 0 && row < this.playableRows && col >= 0 && col < this.cols; }; // Returns true if (row, col) is in the beat grid this.isBeatCell = function (row, col) { return row >= this.playableRows && row < this.rows && col >= 0 && col < this.cols; }; // Returns the BeatCell 2D array for the beat grid (bottom rows) this.getBeatGrid = function () { var arr = []; for (var r = 0; r < this.beatRows; r++) { arr[r] = []; for (var c = 0; c < this.cols; c++) { arr[r][c] = this.getBeatCell(r, c); } } return arr; }; // Finds a random empty cell in the playable area this.findRandomEmptyPlayableCell = function () { var emptyCells = []; for (var r = 0; r < this.playableRows; r++) { for (var c = 0; c < this.cols; c++) { // Check if the cell is empty (no coin, no key, no hero) if (!this.grid[r][c]) { emptyCells.push({ row: r, col: c }); } } } if (emptyCells.length > 0) { var randomIndex = Math.floor(Math.random() * emptyCells.length); return emptyCells[randomIndex]; } return null; // No empty cells found }; // Returns the playable area as a 2D array (nulls, or game objects if you want to add them) this.getPlayableGrid = function () { var arr = []; for (var r = 0; r < this.playableRows; r++) { arr[r] = []; for (var c = 0; c < this.cols; c++) { arr[r][c] = this.grid[r][c]; } } return arr; }; // Reset the playable grid with coins in all cells (no hero logic) this.resetGridWithCoins = function () { // Clear existing coins and enemies for (var r = 0; r < this.playableRows; r++) { for (var c = 0; c < this.cols; c++) { if (this.grid[r][c]) { this.grid[r][c].destroy(); this.grid[r][c] = null; } } } // Add new coins to all playable cells var coinCount = 0; for (var r = 0; r < this.playableRows; r++) { for (var c = 0; c < this.cols; c++) { var coin = new Coin(); coin.x = GameConstants.GRID_LEFT + c * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; coin.y = GameConstants.GRID_TOP + r * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; midgroundContainer.addChild(coin); this.grid[r][c] = coin; coinCount++; } } return coinCount; }; // Call this to initialize all visuals this.initialize = function () { this.initBeatGrid(); }; } var GameConstants = { GRID_COLS: 8, TOTAL_ROWS: 10, // Reduced from 11 to remove top row BEAT_ROWS: 4, // +1 for new down row CHAR_ROW: 3, // hero now starts at row 3 (was 4) due to removed top row JUMP_ROW: 2, // up movement now at row 2 (was 3) due to removed top row DOWN_ROW: 1, // new down movement row 1 (was 2) due to removed top row GRID_MARGIN_X: 40, get CELL_SIZE() { return Math.floor((2048 - 2 * this.GRID_MARGIN_X) / this.GRID_COLS); }, get GRID_HEIGHT() { return this.TOTAL_ROWS * this.CELL_SIZE; }, get GRID_TOP() { return 2732 - this.GRID_HEIGHT - 10; }, get GRID_LEFT() { return this.GRID_MARGIN_X; }, get HERO_MIN_X() { return this.GRID_LEFT + this.CELL_SIZE / 2; }, get HERO_MAX_X() { return this.GRID_LEFT + (this.GRID_COLS - 1) * this.CELL_SIZE + this.CELL_SIZE / 2; }, get HERO_START_COL() { return Math.floor(this.GRID_COLS / 2); }, get HERO_START_X() { return this.GRID_LEFT + this.HERO_START_COL * this.CELL_SIZE + this.CELL_SIZE / 2; }, get HERO_START_Y() { return this.GRID_TOP + this.CHAR_ROW * this.CELL_SIZE + this.CELL_SIZE / 2; }, STEP_INTERVAL: 400, // ms per step (150bpm) COIN_SPAWN_BEATS: 8, COIN_SPAWN_CHANCE: 0.25 }; var AudioService = { playBeat: function playBeat() { LK.getSound('beat').play(); }, playBeatUp: function playBeatUp() { LK.getSound('Sound_Up').play(); }, playBeatLeft: function playBeatLeft() { LK.getSound('Sound_Left').play(); }, playBeatRight: function playBeatRight() { LK.getSound('Sound_Right').play(); }, playBeatDown: function playBeatDown() { LK.getSound('Sound_Down').play(); }, playMove: function playMove() { LK.getSound('moveSnd').play(); }, playCoinCollect: function playCoinCollect() { LK.getSound('coinCollect').play(); }, playNewLevel: function playNewLevel() { LK.getSound('NewLevel').play(); }, startMusic: function startMusic() { LK.playMusic('bgmusic', { fade: { start: 0, end: 0.3, duration: 1200 } }); } }; /** * UIService * Responsibility: To create and manage all non-game-grid UI elements, such * as the score text and the step indicators. */ function UIService(game) { this.game = game; this.scoreTxt = null; // --- LIVES UI STATE --- this.livesIcons = []; this.livesAnchorX = 0.5; // default center, can be set externally this.livesAnchorY = 0; // default top, can be set externally this.livesIconSpacing = 100; // px between icons this.initialize = function () { // Score Text this.scoreTxt = new Text2('0', { size: 100, fill: "#fff" }); this.scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(this.scoreTxt); // --- LIVES ICONS UI --- this.createLivesIcons(5); // Create columnHighlights for beat grid rows this.columnHighlights = []; for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { var highlight = LK.getAsset('cellWhite', { anchorX: 0.5, anchorY: 0.5, alpha: 0 // invisible by default }); // Position highlight at the leftmost column, correct row in the beat grid var gridRow = GameConstants.TOTAL_ROWS - GameConstants.BEAT_ROWS + r; highlight.x = GameConstants.GRID_LEFT + GameConstants.CELL_SIZE / 2; highlight.y = GameConstants.GRID_TOP + gridRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; highlight.alpha = 0; // invisible by default, semi-transparent when shown foregroundContainer.addChild(highlight); this.columnHighlights.push(highlight); } // Hide all highlights at start for (var i = 0; i < this.columnHighlights.length; i++) { this.columnHighlights[i].alpha = 0; } }; // Create the lives icons and add to gui overlay this.createLivesIcons = function (num) { if (this.livesIcons && this.livesIcons.length) { for (var i = 0; i < this.livesIcons.length; i++) { if (this.livesIcons[i] && typeof this.livesIcons[i].destroy === "function") { this.livesIcons[i].destroy(); } } } this.livesIcons = []; for (var i = 0; i < num; i++) { var icon = LK.getAsset('Live', { anchorX: 0.5, anchorY: 0.5 }); icon.width = 80; icon.height = 78.75; this.livesIcons.push(icon); LK.gui.addChild(icon); } this.updateLivesIconsPosition(); }; // Update the position of the lives icons based on anchor this.updateLivesIconsPosition = function () { var num = this.livesIcons.length; this.livesIconSpacing = 90; // Reduced spacing to move icons closer together var totalWidth = (num - 1) * this.livesIconSpacing; var anchorX = 0.85; // Position to the right side of screen var anchorY = this.livesAnchorY; var guiW = LK.gui.width || 2038; var guiH = LK.gui.height || 2732; var baseX = Math.floor(anchorX * guiW - totalWidth / 2); var baseY = Math.floor(anchorY * guiH + 40); // 40px margin from top by default for (var i = 0; i < this.livesIcons.length; i++) { var icon = this.livesIcons[i]; icon.x = baseX + i * this.livesIconSpacing; icon.y = baseY; icon.visible = true; } }; // Set the anchor for the lives UI (0..1 for x and y) this.setLivesAnchor = function (anchorX, anchorY) { this.livesAnchorX = anchorX; this.livesAnchorY = anchorY; this.updateLivesIconsPosition(); }; // Update the lives UI to show the correct number of icons this.updateLivesUI = function (currentLives) { for (var i = 0; i < this.livesIcons.length; i++) { // Hide icons from left to right as lives are lost this.livesIcons[i].visible = i >= this.livesIcons.length - currentLives; } }; this.updateScore = function (newScore) { this.scoreTxt.setText(newScore); }; // Main Highlighting Logic: shows current beat column position this.updateColumnHighlight = function (currentColumnIndex) { // Hide all highlights first for (var i = 0; i < this.columnHighlights.length; i++) { this.columnHighlights[i].alpha = 0; } // Show highlight for all rows in the current column for (var i = 0; i < GameConstants.BEAT_ROWS; i++) { // Position the highlight at the correct cell var gridRow = GameConstants.TOTAL_ROWS - GameConstants.BEAT_ROWS + i; this.columnHighlights[i].x = GameConstants.GRID_LEFT + currentColumnIndex * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; this.columnHighlights[i].y = GameConstants.GRID_TOP + gridRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; this.columnHighlights[i].alpha = 0.3; // semi-transparent highlight to show current column } }; } /** * GridService * Responsibility: To create, manage the state of, and update the visuals of the beat grid. */ function GridService(game, gameGrid, antiCheatService) { this.game = game; this.gameGrid = gameGrid; // Centralized grid this.antiCheatService = antiCheatService; this.initialize = function () { this.gameGrid.initialize(); }; // Create visual representation for a BeatCell this.createAllVisuals = function () { // 1. Create the background cells and add them to the midground for (var r = 0; r < this.gameGrid.rows; r++) { for (var c = 0; c < this.gameGrid.cols; c++) { var bgCell = LK.getAsset('emptycell', { anchorX: 0.5, anchorY: 0.5, alpha: 1.0 }); bgCell.x = GameConstants.GRID_LEFT + c * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; bgCell.y = GameConstants.GRID_TOP + r * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; midgroundContainer.addChild(bgCell); } } // 2. Create the beat cell graphics based on the logical grid for (var r = 0; r < this.gameGrid.beatRows; r++) { for (var c = 0; c < this.gameGrid.cols; c++) { var cell = this.gameGrid.getBeatCell(r, c); if (cell) { this.createCellVisual(cell, r, c); } } } }; this.createCellVisual = function (cell, beatRow, col) { var assetId = 'cell'; if (beatRow === 0) assetId = 'cellActive';else if (beatRow === 1) assetId = 'cellLeft';else if (beatRow === 2) assetId = 'cellRight';else if (beatRow === 3) assetId = 'cellDown'; var isDisabled = this.antiCheatService.isCellDisabled(beatRow, col); // CHECK STATUS var gfx = LK.getAsset(assetId, { anchorX: 0.5, anchorY: 0.5, alpha: isDisabled ? 0 : 0.25 // SET ALPHA BASED ON STATUS }); // Position it var gridRow = this.gameGrid.playableRows + beatRow; gfx.x = GameConstants.GRID_LEFT + col * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; gfx.y = GameConstants.GRID_TOP + gridRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; // Add the graphic to the correct layer and attach it to the logical cell foregroundContainer.addChild(gfx); cell.gfx = gfx; }; // Returns the BeatCell at a given (x, y) screen coordinate, or null if not in beat grid this.getCellAt = function (x, y) { var pos = this.getGridPositionFromCoords(x, y); // If the click was outside the beat grid, pos will be null. if (!pos) { return null; } // Now that we have a definitive {row, col}, check if it's disabled. if (this.antiCheatService.isCellDisabled(pos.row, pos.col)) { return null; // Return null if disabled. } // If it's valid, get the cell object from the gameGrid. return this.gameGrid.getBeatCell(pos.row, pos.col); }; // Returns an array of booleans representing the active state of each BeatCell in the given column this.getActiveStatesForColumn = function (columnIndex) { var states = []; for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { var gridRow = GameConstants.TOTAL_ROWS - GameConstants.BEAT_ROWS + r; var cell = this.gameGrid.grid[gridRow][columnIndex]; // Check if cell is disabled by anti-cheat var isDisabled = this.antiCheatService && this.antiCheatService.isCellDisabled(r, columnIndex); states.push(cell && cell.active && !isDisabled ? true : false); } return states; }; // Toggle a beat cell, ensuring only one beat per layer per column this.toggleCell = function (cell, mode) { // --- Step 1: Find the column of the cell we're trying to modify. var col = -1; for (var c = 0; c < GameConstants.GRID_COLS; c++) { // We use cell.row, which is the beat row index (0-3) if (this.gameGrid.getBeatCell(cell.row, c) === cell) { col = c; break; } } // --- Step 2: The Unbreakable Guard Clause --- // If we couldn't find the cell's column OR if the AntiCheatService says it's disabled, // do absolutely nothing. if (col === -1 || this.antiCheatService.isCellDisabled(cell.row, col)) { return; } // --- Step 3: Perform the Toggle --- var needsChange = mode === 'add' && !cell.active || mode === 'remove' && cell.active; if (needsChange) { // If we are ADDING a beat, we must first deactivate any other beat in the same column. if (mode === 'add') { for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { // Skip the cell we are currently trying to activate. if (r === cell.row) { continue; } var otherCellInColumn = this.gameGrid.getBeatCell(r, col); // --- THE FINAL FIX IS HERE --- // We MUST check if the "other" cell is disabled before we try to change its state. if (otherCellInColumn && otherCellInColumn.active && !this.antiCheatService.isCellDisabled(r, col)) { otherCellInColumn.setActive(false); this.updateCellVisual(otherCellInColumn, r, col); } } } // Now, toggle the state of the originally clicked cell. cell.toggle(); // Update the visual of the originally clicked cell. this.updateCellVisual(cell, cell.row, col); // Notify the sequencer that a beat was placed. if (cell.active && typeof sequencerService !== "undefined" && !sequencerService.hasPlacedBeat) { sequencerService.hasPlacedBeat = true; } } }; // Add this new method to GridService this.getGridPositionFromCoords = function (x, y) { // Check if the click is within the horizontal bounds of the beat grid if (x < GameConstants.GRID_LEFT || x > GameConstants.GRID_LEFT + GameConstants.GRID_COLS * GameConstants.CELL_SIZE) { return null; } // Check if the click is within the vertical bounds of the beat grid var beatGridTop = GameConstants.GRID_TOP + this.gameGrid.playableRows * GameConstants.CELL_SIZE; if (y < beatGridTop || y > beatGridTop + GameConstants.BEAT_ROWS * GameConstants.CELL_SIZE) { return null; } // If it's within the bounds, calculate the row and column var col = Math.floor((x - GameConstants.GRID_LEFT) / GameConstants.CELL_SIZE); var row = Math.floor((y - beatGridTop) / GameConstants.CELL_SIZE); // Return the logical grid position return { row: row, col: col }; }; this.updateCellVisual = function (cell, beatRow, col) { if (cell.gfx) { // No need to find the row and col, they are passed in directly. var isDisabled = this.antiCheatService.isCellDisabled(beatRow, col); if (isDisabled) { cell.gfx.alpha = 0; // Always force disabled cells to be transparent } else { cell.gfx.alpha = cell.active ? 1.0 : 0.25; // Normal behavior } } }; // Returns the actions for a given step (column) by checking the beat grid this.getActiveActionsForStep = function (step) { // The beat grid is in the bottom rows of the grid var actions = { up: false, left: false, right: false, down: false }; for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { var gridRow = GameConstants.TOTAL_ROWS - GameConstants.BEAT_ROWS + r; var cell = this.gameGrid.grid[gridRow][step]; if (!cell) { continue; } // Check if cell is disabled by anti-cheat var isDisabled = this.antiCheatService && this.antiCheatService.isCellDisabled(r, step); if (r === 0) { actions.up = cell.active && !isDisabled; } else if (r === 1) { actions.left = cell.active && !isDisabled; } else if (r === 2) { actions.right = cell.active && !isDisabled; } else if (r === 3) { actions.down = cell.active && !isDisabled; } } return actions; }; } /** * PlayerService * Responsibility: To create and manage the Hero instance, its state, and its actions. * It translates logical actions ("move left") into animations and sound calls. */ function PlayerService(game, gameGrid) { this.game = game; this.gameGrid = gameGrid; this.hero = null; this.reset = function () { if (this.hero) { this.hero.destroy(); this.hero = null; } }; this.initialize = function () { if (this.hero) { this.hero.destroy(); // Destroy old one if it exists } this.hero = new Hero(); // DO NOT set position here. DO NOT add to game here. // That will be handled by startNextRound. }; this.getHeroInstance = function () { return this.hero; }; this.handleDrag = function (x) { this.hero.x = Math.max(GameConstants.HERO_MIN_X, Math.min(GameConstants.HERO_MAX_X, x)); }; this.performActions = function (actions) { var didAction = false; var horizontalMove = 0; var hero = this.hero; var direction = "down"; var isEating = false; var isUpWalkAlt = hero.upWalkAlt || false; if (actions.left && !actions.right) { horizontalMove = -1; direction = "left"; } if (actions.right && !actions.left) { horizontalMove = 1; direction = "right"; } if (actions.up && !actions.down) { direction = "up"; // Alternate up-walk frame isUpWalkAlt = !hero.upWalkAlt; if (horizontalMove !== 0) { this.move(horizontalMove); } this.moveVertical(-1); // Move up didAction = true; } else if (actions.down && !actions.up) { direction = "down"; hero.upWalkAlt = false; // Reset alternation on down if (horizontalMove !== 0) { this.move(horizontalMove); } this.moveVertical(1); // Move down didAction = true; } else if (horizontalMove !== 0) { // Only horizontal direction = horizontalMove === -1 ? "left" : "right"; hero.upWalkAlt = false; // Reset alternation on horizontal this.move(horizontalMove); didAction = true; } // Set hero frame for movement (not eating) hero.setFrame(direction, false, isUpWalkAlt); if (didAction) { // Pop animation: scale up to 1.18, then back to 1.0 tween.stop(hero, { scaleX: true, scaleY: true }); hero.scaleX = 1.0; hero.scaleY = 1.0; tween(hero, { scaleX: 1.1, scaleY: 1.1 }, { duration: 75, easing: tween.cubicOut, onFinish: function onFinish() { tween(hero, { scaleX: 1.0, scaleY: 1.0 }, { duration: 125, easing: tween.cubicIn }); } }); AudioService.playBeat(); } }; this.move = function (direction) { var newCol = this.hero.currentCol + direction; if (newCol < 0) { newCol = GameConstants.GRID_COLS - 1; } if (newCol >= GameConstants.GRID_COLS) { newCol = 0; } this.hero.currentCol = newCol; var nx = GameConstants.GRID_LEFT + newCol * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; this.hero.moveTo(nx, this.hero.y, 180); AudioService.playMove(); }; // Move up or down by 1 row, with vertical teleport at edges of playable area this.moveVertical = function (direction) { var minRow = 0; var maxRow = this.gameGrid.playableRows - 1; // Only teleport within playable area (rows 0-5 after removing top row) var newRow = this.hero.gridRow !== undefined ? this.hero.gridRow : GameConstants.CHAR_ROW; newRow += direction; // Teleport vertically if out of bounds if (newRow < minRow) { newRow = maxRow; } else if (newRow > maxRow) { newRow = minRow; } this.hero.gridRow = newRow; var ny = GameConstants.GRID_TOP + newRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; this.hero.moveTo(this.hero.x, ny, 180); AudioService.playMove(); // No longer update GameConstants.CHAR_ROW here; hero.gridRow is the source of truth }; } /** * InputService * Responsibility: To handle all raw user input (down, up, move) and delegate * the appropriate actions to other services (GridService, PlayerService). */ function InputService(gridService, playerService, antiCheatService) { this.gridService = gridService; this.playerService = playerService; this.antiCheatService = antiCheatService; this.isPainting = false; this.paintMode = null; this.lastPaintedCell = null; this.handleDown = function (x, y) { // gridService.getCellAt now automatically returns null for disabled cells. var cell = this.gridService.getCellAt(x, y); // If cell is not null, we know for a fact it's a valid, clickable cell. if (cell) { this.isPainting = true; this.paintMode = cell.active ? 'remove' : 'add'; this.lastPaintedCell = cell; this.gridService.toggleCell(cell, this.paintMode); } }; this.handleUp = function () { this.isPainting = false; this.paintMode = null; this.lastPaintedCell = null; }; this.handleMove = function (x, y) { if (this.isPainting) { // gridService.getCellAt already returns null for disabled cells. var cell = this.gridService.getCellAt(x, y); // If we found a valid (non-disabled) cell and it's a new one... if (cell && cell !== this.lastPaintedCell) { this.lastPaintedCell = cell; // The now-fortified toggleCell will handle the rest. this.gridService.toggleCell(cell, this.paintMode); } } }; } /** * SequencerService * Responsibility: To manage the main game loop timing (the "beat"). On each step, * it orchestrates calls to the other services to update the game state. */ function SequencerService(gridService, playerService, uiService, gameState, gameRulesService) { this.gridService = gridService; this.playerService = playerService; this.uiService = uiService; this.gameState = gameState; this.gameRulesService = gameRulesService; this.currentStep = 0; this.lastStepTime = 0; this.playing = true; this.comboMultiplier = 1; this.collectedThisBeat = false; this.prevComboMultiplier = 1; // Previous Fibonacci number this.comboCount = 0; // Track position in Fibonacci sequence this.enemyCellRow = undefined; // Track enemy cell position this.enemyCellCol = undefined; // Track enemy cell position this.levelTransitioning = false; // Add this new flag // --- LIVES SYSTEM STATE --- this.lives = 5; this.collectedInSequence = false; // Tracks if a coin was collected in the current 8-beat loop this.lifeSystemActive = false; // Only start deducting lives after first beat placed and sequence ends this.hasPlacedBeat = false; // Tracks if player has placed a beat at all this.initialize = function () { this.lastStepTime = Date.now(); }; this.update = function () { var now = Date.now(); if (this.playing && now - this.lastStepTime >= GameConstants.STEP_INTERVAL) { this.lastStepTime = now; // 1. UPDATE HIGHLIGHT to show where we are about to play this.uiService.updateColumnHighlight(this.currentStep); // 2. Perform actions for the current step this.doStepActions(); // 3. THEN, advance the step to the next beat this.currentStep = (this.currentStep + 1) % GameConstants.GRID_COLS; } }; this.doStepActions = function () { // Reset collection flag for this beat this.collectedThisBeat = false; // Get actions for the CURRENT step (the one being highlighted) var currentStep = this.currentStep; // --- LIFE SYSTEM LOGIC --- // Check if a full sequence has just ended (i.e., we're about to play beat 0) if (currentStep === 0) { // Delegate life system logic to GameRulesService if (this.gameRulesService.checkLifeSystem()) { return; // Game over - stop the game } } // --- END LIFE SYSTEM LOGIC --- var actions = this.gridService.getActiveActionsForStep(currentStep); // Play beat sounds for the previous step if (actions.up) { AudioService.playBeatUp(); } else if (actions.left) { AudioService.playBeatLeft(); } else if (actions.right) { AudioService.playBeatRight(); } else if (actions.down) { AudioService.playBeatDown(); } else { AudioService.playBeat(); } // Animate active beat cells for the current step for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { var cell = gameGrid.getBeatCell(r, currentStep); if (cell && cell.active && !antiCheatService.isCellDisabled(r, currentStep) && cell.gfx) { // Create bump animation: scale up to 110% then back to 100% tween(cell.gfx, { scaleX: 1.1, scaleY: 1.1 }, { duration: 100, easing: tween.easeOut, onFinish: function (targetGfx) { return function () { tween(targetGfx, { scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeIn }); }; }(cell.gfx) }); } } // Store hero's previous position before performing actions var hero = this.playerService.getHeroInstance(); if (!hero) { return; // Exit early if hero doesn't exist } var prevHeroRow = hero.gridRow; var prevHeroCol = hero.currentCol; // Perform actions for the current step this.playerService.performActions(actions); // Check if hero was on an enemy cell and has now moved away if (this.enemyCellRow !== undefined && this.enemyCellCol !== undefined) { // Check if hero has moved to a different cell if (hero.gridRow !== this.enemyCellRow || hero.currentCol !== this.enemyCellCol) { // Hero has left the enemy cell - trigger game over LK.showGameOver(); return; // Exit early } } // Delegate all rule-based logic to GameRulesService this.gameRulesService.processPlayerCollision(hero); }; } // Initialize GameStateManager var gameState = new GameStateManager(); var transitionScreen = null; var transitionOverlay = null; var transitionAsset = null; var transitionText = null; var storyTexts = ["One day, Pengu heard the most beautiful piano song in the village.", "Pengu dreamed of meeting the Princess, but fear held him back.", "An old penguin showed Pengu that music flows from the heart.", "Pengu found a piano and practiced from dawn till midnight.", "Even through mockery and pain, Pengu never gave up.", "His first performance was a complete fiasco.", "But playing with pure heart, Pengu found his true song.", "His heartfelt music finally won the Princess's love."]; var storyText = null; function showTransitionScreen() { // Pause the sequencer to stop beats and movement sequencerService.playing = false; // Hide score text and lives icons if (uiService.scoreTxt) { uiService.scoreTxt.visible = false; } for (var i = 0; i < uiService.livesIcons.length; i++) { uiService.livesIcons[i].visible = false; } // Create white overlay covering entire screen transitionOverlay = LK.getAsset('cellWhite', { anchorX: 0, anchorY: 0, scaleX: 20, // Scale to cover full screen width scaleY: 30 // Scale to cover full screen height }); transitionOverlay.x = 0; transitionOverlay.y = 0; foregroundContainer.addChild(transitionOverlay); // Determine which story panel to show based on current round var storyAssetName = 'Transition'; if (gameState.currentStoryPanel > 1) { storyAssetName = 'Transition_' + gameState.currentStoryPanel; } // Create transition asset with current story panel transitionAsset = LK.getAsset(storyAssetName, { anchorX: 0.5, anchorY: 0.5 }); transitionAsset.x = 2048 / 2; // Center horizontally transitionAsset.y = 2732 / 2; // Center vertically foregroundContainer.addChild(transitionAsset); // Create "Tap to SKIP" text at top transitionText = new Text2('Tap to SKIP', { size: 80, fill: 0x000000 // Black text }); transitionText.anchor.set(0.5, 0); transitionText.x = 2048 / 2; // Center horizontally transitionText.y = 100; // Near top of screen foregroundContainer.addChild(transitionText); // Create story text at bottom var storyTextContent = storyTexts[gameState.currentStoryPanel - 1]; // currentStoryPanel is 1-indexed storyText = new Text2(storyTextContent, { size: 60, fill: 0x000000 // Black text }); storyText.anchor.set(0.5, 1); // Center horizontally, anchor at bottom storyText.x = 2048 / 2; // Center horizontally storyText.y = 2732 - 100; // Near bottom of screen with margin foregroundContainer.addChild(storyText); } function hideTransitionScreen() { // Destroy all current board objects including hero, princess, coins playerService.reset(); // Resume the sequencer sequencerService.playing = true; // Show score text and lives icons again if (uiService.scoreTxt) { uiService.scoreTxt.visible = true; } for (var i = 0; i < uiService.livesIcons.length; i++) { uiService.livesIcons[i].visible = true; } if (transitionOverlay) { transitionOverlay.destroy(); transitionOverlay = null; } if (transitionAsset) { transitionAsset.destroy(); transitionAsset = null; } if (transitionText) { transitionText.destroy(); transitionText = null; } if (storyText) { storyText.destroy(); storyText = null; } } /** * Resets the game state and starts the next round. */ function startNextRound() { // Now it's a clean, high-level summary of the process _clearSceneAndState(); _setupLogicalGrid(); _createVisuals(); _finalizeRoundSetup(); } function _clearSceneAndState() { // Move all container.removeChildren() and gameState.resetForNewRound() calls here. backgroundContainer.removeChildren(); midgroundContainer.removeChildren(); foregroundContainer.removeChildren(); playerService.reset(); gameState.resetForNewRound(); sequencerService.collectedInSequence = false; sequencerService.lifeSystemActive = false; sequencerService.hasPlacedBeat = false; uiService.updateLivesUI(gameState.lives); // Clear the logical grid data for (var r = 0; r < gameGrid.rows; r++) { for (var c = 0; c < gameGrid.cols; c++) { gameGrid.grid[r][c] = null; } } } function _setupLogicalGrid() { // Move all the logic for creating and placing logical objects // (coins, hero) into the gameGrid.grid array. // Create the logical beat cells gameGrid.initBeatGrid(); antiCheatService.reset(); // Create and shuffle the cell pool for playable area var cellPool = []; for (var r = 0; r < gameGrid.playableRows; r++) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { cellPool.push({ row: r, col: c }); } } // Shuffle the pool for (var i = cellPool.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = cellPool[i]; cellPool[i] = cellPool[j]; cellPool[j] = temp; } // Initialize the logical Hero object playerService.initialize(); var hero = playerService.getHeroInstance(); // Place Hero in logical grid var heroCell = cellPool.shift(); hero.gridRow = heroCell.row; hero.currentCol = heroCell.col; gameGrid.grid[heroCell.row][heroCell.col] = hero; // Create logical coins and store in grid gameState.totalCoins = 0; for (var r = 0; r < gameGrid.playableRows; r++) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { if (gameGrid.grid[r][c] === null) { var coin = new Coin(); gameGrid.grid[r][c] = coin; gameState.totalCoins++; } } } // Store key and princess positions for later spawning var keyCell = cellPool.shift(); var princessCell = cellPool.shift(); gameState.keySpawnRow = keyCell.row; gameState.keySpawnCol = keyCell.col; gameState.princessSpawnRow = princessCell.row; gameState.princessSpawnCol = princessCell.col; } function _createVisuals() { // Create the background visuals (gridService.createAllVisuals()) // Loop through the logical grid and create the visuals for coins, hero, etc. // and add them to the correct containers. gridService.createAllVisuals(); // Create the visuals for the playable items for (var r = 0; r < gameGrid.playableRows; r++) { for (var c = 0; c < gameGrid.cols; c++) { var entity = gameGrid.grid[r][c]; if (entity) { // Set its position entity.x = GameConstants.GRID_LEFT + c * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; entity.y = GameConstants.GRID_TOP + r * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; // Add all interactive items to the FOREGROUND layer foregroundContainer.addChild(entity); } } } } function _finalizeRoundSetup() { // Reset all beat grid cells to inactive and update visuals for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { var cell = gameGrid.getBeatCell(r, c); if (cell) { cell.setActive(false); // The grid service is responsible for its own visuals gridService.updateCellVisual(cell, r, c); } } } AudioService.playNewLevel(); } // --- Patch coin collection logic to update counter and reset on projectile fire --- // We'll patch the doStepActions method in SequencerService below // 1. Instantiate All Services (no change here) var uiService = new UIService(game); var gameGrid = new GameGrid(game); var antiCheatService = new AntiCheatService(null, gameGrid); var gridService = new GridService(game, gameGrid, antiCheatService); var playerService = new PlayerService(game, gameGrid); var gameRulesService = new GameRulesService(gameState, gameGrid, uiService); var inputService = new InputService(gridService, playerService, antiCheatService); var sequencerService = new SequencerService(gridService, playerService, uiService, gameState, gameRulesService); // Pass the gridService reference back to antiCheatService now that it exists antiCheatService.gridService = gridService; // 2. Initialize Core Services uiService.initialize(); sequencerService.initialize(); // 3. Start the First Round (places logical objects and non-beat-grid visuals) gameState.currentRound = 0; startNextRound(); // 4. Connect Input Handlers to the Input Service game.down = function (x, y) { // Check if transition screen is active if (transitionOverlay) { // Advance to next story panel gameState.currentStoryPanel++; // Check if we've reached the end of the story if (gameState.currentStoryPanel > gameState.maxStoryPanels) { // Story is complete - show you win and end game hideTransitionScreen(); LK.showYouWin(); return; } // Dismiss transition screen and advance to next round hideTransitionScreen(); LK.setTimeout(startNextRound, 100); return; } inputService.handleDown(x, y); }; game.up = function (x, y) { inputService.handleUp(x, y); }; game.move = function (x, y) { inputService.handleMove(x, y); }; // 5. Set Main Game Loop to the Sequencer Service game.update = function () { sequencerService.update(); }; // 6. Start background music AudioService.startMusic(); // Debug button for instant level completion var debugButton = LK.getAsset('enemy', { anchorX: 0.5, anchorY: 0 }); debugButton.width = 100; debugButton.height = 92.6; LK.gui.addChild(debugButton); // Position debug button to the left of score text debugButton.x = LK.gui.width * 0.3; // Left side of screen debugButton.y = 40; // Same vertical position as score // Add click handler for debug button debugButton.down = function (x, y, obj) { // Instantly trigger level completion by simulating princess collection if (!gameState.levelTransitioning) { gameState.playerHasKey = true; // Ensure player has key gameState.levelTransitioning = true; // Set transition flag showTransitionScreen(); // Show transition screen } }; // End of file. All round/game start logic is now centralized in startNextRound. /** * GameStateManager * Responsibility: Single source of truth for the game's session state */ function GameStateManager() { this.currentRound = 1; this.totalCoins = 0; this.collectedCoins = 0; this.playerHasKey = false; this.keySpawned = false; this.princessSpawned = false; this.lives = 5; this.levelTransitioning = false; this.keySpawnRow = 0; this.keySpawnCol = 0; this.princessSpawnRow = 0; this.princessSpawnCol = 0; this.currentStoryPanel = 1; this.maxStoryPanels = 8; this.resetForNewRound = function () { this.currentRound++; this.collectedCoins = 0; this.playerHasKey = false; this.keySpawned = false; this.princessSpawned = false; this.levelTransitioning = false; this.lives = 5; }; this.collectCoin = function () { this.collectedCoins++; }; this.shouldSpawnKey = function () { return !this.keySpawned && this.collectedCoins >= Math.floor(this.totalCoins * 0.25); }; this.shouldSpawnPrincess = function () { return !this.princessSpawned && this.collectedCoins >= Math.floor(this.totalCoins * 0.51) && this.playerHasKey; }; this.setKeySpawned = function () { this.keySpawned = true; }; this.setPrincessSpawned = function () { this.princessSpawned = true; }; this.setPlayerHasKey = function (hasKey) { this.playerHasKey = hasKey; }; this.setLevelTransitioning = function (transitioning) { this.levelTransitioning = transitioning; }; this.setKeySpawnPosition = function (row, col) { this.keySpawnRow = row; this.keySpawnCol = col; }; this.setPrincessSpawnPosition = function (row, col) { this.princessSpawnRow = row; this.princessSpawnCol = col; }; this.advanceStoryPanel = function () { this.currentStoryPanel++; return this.currentStoryPanel > this.maxStoryPanels; }; this.setLives = function (lives) { this.lives = lives; }; this.setTotalCoins = function (total) { this.totalCoins = total; }; } /** * GameRulesService * Responsibility: To handle the consequences of player actions including coin collection, * key/princess spawning, and life management logic. */ function GameRulesService(gameState, gameGrid, uiService) { this.gameState = gameState; this.gameGrid = gameGrid; this.uiService = uiService; this.checkLifeSystem = function () { // If player has placed a beat and life system is not yet active, activate it now (but don't deduct a life this sequence) if (sequencerService.hasPlacedBeat && !sequencerService.lifeSystemActive) { sequencerService.lifeSystemActive = true; } else if (sequencerService.lifeSystemActive) { if (!sequencerService.collectedInSequence) { // Player failed to collect a coin in the last 8 beats. Deduct a life. this.gameState.lives--; this.uiService.updateLivesUI(this.gameState.lives); // Update the visual display if (this.gameState.lives <= 0) { LK.showGameOver(); return true; // Indicate game over } } } // Reset the flag for the new sequence. sequencerService.collectedInSequence = false; return false; // Game continues }; this.processPlayerCollision = function (hero) { var heroRow = hero.gridRow; var heroCol = hero.currentCol; var entity = this.gameGrid.grid[heroRow][heroCol]; if (entity instanceof Coin) { // Handle coin collection entity.collect(); this.gameGrid.grid[heroRow][heroCol] = null; // Update collected coins counter this.gameState.collectedCoins++; // Apply combo multiplier to score var points = sequencerService.comboMultiplier; LK.setScore(LK.getScore() + points); this.uiService.updateScore(LK.getScore()); AudioService.playCoinCollect(); // Mark that we collected a coin in this sequence sequencerService.collectedInSequence = true; sequencerService.collectedThisBeat = true; // Increment combo multiplier by 1 for each consecutive coin sequencerService.comboMultiplier++; sequencerService.comboCount++; // Clear any enemy tracking sequencerService.enemyCellRow = undefined; sequencerService.enemyCellCol = undefined; // Set hero frame to Eat variant for this direction var eatDirection = hero.lastDirection || "down"; var eatUpWalkAlt = hero.upWalkAlt || false; if (eatDirection === "down") { // Show hero_2 when eating facing camera, then revert to hero hero.gfx.destroy(); hero.gfx = hero.attachAsset('hero_2', { anchorX: 0.5, anchorY: 0.5 }); hero.gfx.assetId = "hero_2"; LK.setTimeout(function () { hero.setFrame("down", false, false); }, 299); } else { hero.setFrame(eatDirection, true, eatUpWalkAlt); // After 299ms, revert to walk frame (not eating) LK.setTimeout(function () { hero.setFrame(eatDirection, false, eatUpWalkAlt); }, 299); } // Check spawns after coin collection this.checkSpawns(); return true; // Coin was collected } else if (entity instanceof Key) { // Handle key collection this.gameState.playerHasKey = true; AudioService.playNewLevel(); // Play a special sound for key collection entity.destroy(); this.gameGrid.grid[heroRow][heroCol] = null; // Check spawns after key collection in case princess should now appear this.checkSpawns(); return true; // Key was collected } else if (entity instanceof Princess) { // Handle princess collision if (this.gameState.playerHasKey && !this.gameState.levelTransitioning) { this.gameState.levelTransitioning = true; showTransitionScreen(); return true; // Princess interaction successful } // If player doesn't have key, do nothing (can't win yet) } else { // No entity at current position - clear any enemy tracking sequencerService.enemyCellRow = undefined; sequencerService.enemyCellCol = undefined; // No coin collected - reset combo if we had one if (sequencerService.comboMultiplier > 1) { sequencerService.comboMultiplier = 1; sequencerService.comboCount = 0; } } return false; // No significant collision }; this.checkSpawns = function () { // Check if we should spawn the key (25% of coins collected) if (!this.gameState.keySpawned && this.gameState.collectedCoins >= Math.floor(this.gameState.totalCoins * 0.25)) { this.spawnKey(); } // Check if we should spawn the princess (51% of coins collected AND key collected) if (!this.gameState.princessSpawned && this.gameState.collectedCoins >= Math.floor(this.gameState.totalCoins * 0.51) && this.gameState.playerHasKey) { this.spawnPrincess(); } }; this.spawnKey = function () { // Find a random empty cell in the playable area var emptyCell = this.gameGrid.findRandomEmptyPlayableCell(); // Spawn key in the found empty cell if (emptyCell) { var key = new Key(); key.x = GameConstants.GRID_LEFT + emptyCell.col * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; key.y = GameConstants.GRID_TOP + emptyCell.row * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; midgroundContainer.addChild(key); this.gameGrid.grid[emptyCell.row][emptyCell.col] = key; this.gameState.keySpawned = true; } }; this.spawnPrincess = function () { // Find a random empty cell in the playable area var emptyCell = this.gameGrid.findRandomEmptyPlayableCell(); // Spawn princess in the found empty cell if (emptyCell) { var princess = new Princess(); princess.x = GameConstants.GRID_LEFT + emptyCell.col * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; princess.y = GameConstants.GRID_TOP + emptyCell.row * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; midgroundContainer.addChild(princess); this.gameGrid.grid[emptyCell.row][emptyCell.col] = princess; this.gameState.princessSpawned = true; } }; }
===================================================================
--- original.js
+++ change.js
@@ -144,14 +144,14 @@
/****
* Game Code
****/
+// Create layered containers for proper z-ordering
/**
* BeatCell
* Responsibility: To model the state of a single cell on the beat grid. It no longer
* handles its own graphics updates directly, this is delegated to the GridService.
*/
-// Create layered containers for proper z-ordering
function BeatCell(row) {
this.active = false;
this.row = row;
this.gfx = null; // We will create and manage the graphic externally
@@ -974,31 +974,31 @@
this.update = function () {
var now = Date.now();
if (this.playing && now - this.lastStepTime >= GameConstants.STEP_INTERVAL) {
this.lastStepTime = now;
- // 1. UPDATE HIGHLIGHT FIRST for the current step
+ // 1. UPDATE HIGHLIGHT to show where we are about to play
this.uiService.updateColumnHighlight(this.currentStep);
- // 2. THEN, advance the step to the next beat
- this.currentStep = (this.currentStep + 1) % GameConstants.GRID_COLS;
- // 3. FINALLY, perform actions for the new step.
+ // 2. Perform actions for the current step
this.doStepActions();
+ // 3. THEN, advance the step to the next beat
+ this.currentStep = (this.currentStep + 1) % GameConstants.GRID_COLS;
}
};
this.doStepActions = function () {
// Reset collection flag for this beat
this.collectedThisBeat = false;
- // Get actions for the PREVIOUS step (the one the highlight just left)
- var previousStep = (this.currentStep - 1 + GameConstants.GRID_COLS) % GameConstants.GRID_COLS;
+ // Get actions for the CURRENT step (the one being highlighted)
+ var currentStep = this.currentStep;
// --- LIFE SYSTEM LOGIC ---
// Check if a full sequence has just ended (i.e., we're about to play beat 0)
- if (previousStep === GameConstants.GRID_COLS - 1) {
+ if (currentStep === 0) {
// Delegate life system logic to GameRulesService
if (this.gameRulesService.checkLifeSystem()) {
return; // Game over - stop the game
}
}
// --- END LIFE SYSTEM LOGIC ---
- var actions = this.gridService.getActiveActionsForStep(previousStep);
+ var actions = this.gridService.getActiveActionsForStep(currentStep);
// Play beat sounds for the previous step
if (actions.up) {
AudioService.playBeatUp();
} else if (actions.left) {
@@ -1009,12 +1009,12 @@
AudioService.playBeatDown();
} else {
AudioService.playBeat();
}
- // Animate active beat cells for the previous step
+ // Animate active beat cells for the current step
for (var r = 0; r < GameConstants.BEAT_ROWS; r++) {
- var cell = gameGrid.getBeatCell(r, previousStep);
- if (cell && cell.active && !antiCheatService.isCellDisabled(r, previousStep) && cell.gfx) {
+ var cell = gameGrid.getBeatCell(r, currentStep);
+ if (cell && cell.active && !antiCheatService.isCellDisabled(r, currentStep) && cell.gfx) {
// Create bump animation: scale up to 110% then back to 100%
tween(cell.gfx, {
scaleX: 1.1,
scaleY: 1.1
@@ -1041,9 +1041,9 @@
return; // Exit early if hero doesn't exist
}
var prevHeroRow = hero.gridRow;
var prevHeroCol = hero.currentCol;
- // Perform actions for the previous step
+ // Perform actions for the current step
this.playerService.performActions(actions);
// Check if hero was on an enemy cell and has now moved away
if (this.enemyCellRow !== undefined && this.enemyCellCol !== undefined) {
// Check if hero has moved to a different cell
cute 2D illustration of a delicious musical note as a collectible item in a casual mobile game. In-Game asset. 2d. High contrast. No shadows
an angry isometric red square bullfrog character for a casual mobile game, facing the camera directly. In-Game asset. 2d. High contrast. No shadows
a delicious looking isometric symphony shaped golden key drawn as a 2D illustration for a cute mobile game. In-Game asset. 2d. High contrast. No shadows
a delicious looking isometric heart icon drawn as a 2D illustration for a cute mobile game. In-Game asset. 2d. High contrast. No shadows. In-Game asset. 2d. High contrast. No shadows
4-panel comic strip, no text, cute cartoon style, bright colors, black outlines. Characters: Penguin Hero (small, determined) + Penguin Princess (elegant, crown) + Village Penguins Central Theme: Music connects hearts, piano mastery wins love. Hero sees Princess's beautiful piano playing, falls in love Panel 1: Hero living normal life in penguin village Panel 2: Hears beautiful piano music from Princess's ice palace Panel 3: Sees Princess playing gracefully, hearts float around Hero Panel 4: Hero determined but nervous, looking at his flippers. In-Game asset. 2d. High contrast. No shadows
4-panel comic strip, no text, cute cartoon style, bright colors, black outlines. Characters: Penguin Hero (small, determined) + Penguin Princess (elegant, crown) + Village Penguins Central Theme: Music connects hearts, piano mastery wins love. Hero doubts himself, thinks he's not good enough Panel 1: Hero tries to approach Princess's palace but stops Panel 2: Sees other talented penguins playing instruments near Princess Panel 3: Hero looks at his flippers sadly, musical notes are wobbly/off-key Panel 4: Hero walks away discouraged, head down. In-Game asset. 2d. High contrast. No shadows
4-panel comic strip, no text, cute cartoon style, bright colors, black outlines. Characters: Penguin Hero (small, determined) + Penguin Princess (elegant, crown) + Village Penguins Central Theme: Music connects hearts, piano mastery wins love. Story: Old wise penguin teaches Hero about music and courage Panel 1: Hero meets old penguin with small piano on ice floe Panel 2: Old penguin demonstrates simple piano scales, notes are warm/golden Panel 3: Hero tries playing, makes mistakes but old penguin encourages Panel 4: Hero practices with determination, musical notes getting brighter. In-Game asset. 2d. High contrast. No shadows
4-panel comic strip, no text, cute cartoon style, bright colors, black outlines. Characters: Penguin Hero (small, determined) + Penguin Princess (elegant, crown) + Village Penguins Central Theme: Music connects hearts, piano mastery wins love. Story: Hero commits to learning piano seriously Panel 1: Hero finds small piano and sets up practice space Panel 2: Hero practicing at sunrise, warm golden morning light Panel 3: Musical notes gradually improve from wobbly to steady Panel 4: Hero still practicing under moonlight, showing dedication through full day In-Game asset. 2d. High contrast. No shadows
4-panel comic strip, no text, cute cartoon style, bright colors, black outlines. Characters: Penguin Hero (small, determined) + Village Penguins Central Theme: Music connects hearts, piano mastery wins love. Story: Hero faces challenges and obstacles in his musical journey Panel 1: Hero struggles with difficult piano piece, frustrated Panel 2: Other penguins mock his practicing, musical notes look harsh/jagged Panel 3: Hero's flippers are sore, he looks exhausted Panel 4: But Hero persists, practicing by moonlight, determined expression In-Game asset. 2d. High contrast. No shadows
4-panel comic strip, no text, cute cartoon style, bright colors, black outlines. Characters: Penguin Hero (small, determined) + Penguin Princess (elegant, crown) + Village Penguins Central Theme: Music connects hearts, piano mastery wins love. Story: Hero's biggest challenge - public performance disaster Panel 1: Hero attempts to play for Princess at village gathering Panel 2: Makes terrible mistakes, wrong red notes everywhere, crowd looks shocked Panel 3: Princess looks disappointed, Hero devastated and embarrassed Panel 4: Hero runs away, hiding behind ice block, feeling defeated In-Game asset. 2d. High contrast. No shadows
4-panel comic strip, no text, cute cartoon style, bright colors, black outlines. Characters: Penguin Hero (small, determined) + Penguin Princess (elegant, crown) + Village Penguins Central Theme: Music connects hearts, piano mastery wins love. Story: Hero finds his true musical voice and inner strength Panel 1: Hero alone with piano, plays from his heart instead of trying to impress Panel 2: Beautiful, unique music flows from him, notes shimmer with emotion Panel 3: Princess secretly listens from distance, moved by his genuine music Panel 4: Hero realizes music should come from the heart, not ego. In-Game asset. 2d. High contrast. No shadows
4-panel comic strip, no text, cute cartoon style, bright colors, black outlines. Characters: Penguin Hero (small, determined) + Penguin Princess (elegant, crown) + Village Penguins Central Theme: Music connects hearts, piano mastery wins love. Story: Hero's grand performance wins Princess's heart and village's admiration Panel 1: Hero plays grand piano on large ice stage, whole village watching Panel 2: Beautiful music fills the air, all penguins are enchanted, notes sparkle Panel 3: Princess approaches Hero, heart symbols floating between them Panel 4: Hero and Princess together at piano, playing duet, village celebrates with hearts/music notes everywhere. In-Game asset. 2d. High contrast. No shadows
A simple top-down 2D illustration of a calm arctic lake. The scene shows the edge of the water, with a soft, snowy shoreline framing the top and one side of the image. The lake itself is a calm, light blue, with subtle light reflections on the surface to suggest water. The art style is soft and clean, like a children's book illustration.. In-Game asset. 2d. High contrast. No shadows
cold water splash 2d illustration. top-view perspective. In-Game asset. 2d. High contrast. No shadows