User prompt
Please fix the bug: 'Cannot set properties of null (setting 'x')' in or related to this line: 'return size;' Line Number: 587
User prompt
Please fix the bug: 'Cannot set properties of null (setting 'x')' in or related to this line: 'return size;' Line Number: 584
User prompt
Please fix the bug: 'isFinite is not a function' in or related to this line: 'if (!isFinite(size) || size <= 0) {' Line Number: 581
User prompt
Please fix the bug: 'Cannot set properties of null (setting 'x')' in or related to this line: 'return Math.floor((2048 - 2 * this.GRID_MARGIN_X) / this.GRID_COLS);' Line Number: 579
User prompt
Please fix the bug: 'Cannot set properties of null (setting 'x')' in or related to this line: 'return Math.floor((2048 - 2 * this.GRID_MARGIN_X) / this.GRID_COLS);' Line Number: 579
User prompt
Please fix the bug: 'Cannot set properties of null (setting 'x')' in or related to this line: 'return Math.floor((2048 - 2 * this.GRID_MARGIN_X) / this.GRID_COLS);' Line Number: 575
User prompt
Navigate to the Main Initialization Block: Go to the very bottom of your code, where you instantiate all the services and call their initialize methods. Remove the Old Setup Logic: You need to delete or comment out the lines that are currently responsible for setting up the first round in a unique way. Specifically, remove these two lines: The line that says totalCoins = gameGrid.initBackground(); The line that calls playerService.initialize(); Replace with a Single, Clean Call: In place of the lines you just removed, add a single call to your global startNextRound() function. What will this change do? Instead of a fragmented setup, your game will now start by calling the one function that knows how to do everything: reset counters, repopulate the grid, place the hero, place enemies, and set the final, correct totalCoins value. Your playerService.initialize function will now only be responsible for creating the hero object, not for changing game state like totalCoins. The first round and all subsequent rounds will be generated by the exact same code, eliminating this entire class of bugs.
User prompt
Step 1: Decouple the Grid Reset Logic Your resetGridWithCoins function is currently trying to do two jobs at once: reset the grid AND handle the hero's starting position. We need to simplify it. Navigate to your GameGrid class definition. Find the resetGridWithCoins function. Modify this function so that it no longer accepts any arguments (remove heroRow and heroCol from its definition). Change its logic: Instead of leaving one cell empty, its only job now should be to fill every single playable cell with a coin. It should not have any if condition to check for the hero's position. Make sure this simplified function still correctly returns the total number of coins it created (which will now be the total number of playable cells). You have essentially created a function that gives you a perfectly fresh, full board of coins. Step 2: Fix the Order of Operations in startNextRound Now that you have a simple way to get a full grid, you can fix the startNextRound function to follow a clear, logical sequence. Navigate to your global startNextRound function. Rewrite its logic to follow these exact steps in this exact order: a. First, call your newly modified resetGridWithCoins function (with no arguments). Store the number it returns in your totalCoins variable. At this point, the board is full of coins. b. After the grid is full, choose a new randomRow and randomCol for the hero. c. Set the hero's visual position (hero.x and hero.y) based on these new random coordinates. d. Set the hero's logical position (hero.gridRow and hero.currentCol) to these same random coordinates. e. Now that the hero is logically and visually in its new spot, you need to clean up the coin that is underneath it. Get the coin object from the gameGrid.grid at the hero's new randomRow and randomCol. f. Check if a coin actually exists there. If it does, call its destroy() method and set that spot in the gameGrid.grid array to null. g. Finally, since you just removed one coin, decrement the totalCoins variable by one. By following this sequence, you ensure there is no moment of confusion. You create a full grid, then you place the hero, and then you clean up the spot under the hero. The hero's visual and logical states will be set at the same time from the same source of truth (randomRow and randomCol), completely fixing the desynchronization bug.
User prompt
Instruction 1: Fixing the Player's Coin Collection Navigate to the Right Place: Go to your SequencerService and find the doStepActions function inside it. Locate the Logic Block: Inside doStepActions, find the section that checks what entity the hero is standing on. Specifically, find the part that begins else if (entity.collect). This is the block that runs whenever the player successfully lands on a coin. Identify the Missing Step: You have a comment in this section that says // Do NOT call checkRoundCompletion here!. This comment is the source of the problem. While it's true you don't want to call it before creating the projectile, you absolutely need to call it to register the player's collection. The Fix: At the very end of that else if (entity.collect) block, after you've handled the projectile creation, the scoring, the animations, and the combo counter, you must add a call to your checkRoundCompletion() function. This will ensure that when the player collects a coin, the game's central round-checking logic is triggered, just like it is for the projectile. Instruction 2: Fixing the Start of a New Round The Hidden Problem: When a new round begins, you correctly move the hero to a new random spot. However, you are not removing the coin that is now underneath the hero. This means there is always one "uncollectable" coin on the board, making the round impossible to finish. Navigate to the Right Place: Find your global function named startNextRound(). Locate the Logic Block: Inside this function, you correctly get the new totalCoins from resetGridWithCoins and then you set the hero's new x, y, currentCol, and gridRow. The Fix: Immediately after you finish setting all of the hero's new position properties, you need to add new logic to fix this state. The steps are: Find the object on the game grid at the hero's new randomRow and randomCol. If that object is a coin, destroy it. Set that position on the game grid to null. Crucially, you must then decrease the totalCoins variable by one. This will make the start of round 2 (and beyond) behave exactly like the start of round 1, where the hero's starting spot is empty and the total coin count is accurate.
User prompt
the game doesn't correctly calculate the detroyed coins on that level as it sometimes moves to the next round before all the coins have been eliminated, as if the projectile is counting twice or something. ensure ALL coins must be destroyed first before advancing to the next level
User prompt
you sort of fixed it but not completely, while after the first move the character is indeed coretly going to the correct location, when reloading the round, it doesn't get reset. so it takes an actual move for it to correctly get reset. ensure it gets reset at the same time as the coins are
User prompt
after restarting the new level, the hero doesn't seem positioned correctly, i see it moving on a side of the board but coins are collected in a different place, as if he's positioned in a different spot than the intended position
User prompt
Step 4: Final Touch - Initialize totalCoins Correctly The GameGrid class already creates the initial coins. We need to make sure the global totalCoins variable is set correctly at the very beginning of the game. In your GameGrid class, modify the initBackground function to return the count. // In the GameGrid class this.initBackground = function () { var initialCoinCount = 0; // Add a local counter for (var r = 0; r < this.rows; r++) { for (var c = 0; c < this.cols; c++) { // ... existing background cell creation ... if (r < this.playableRows) { var coin = new Coin(); coin.x = bgCell.x; coin.y = bgCell.y; this.game.addChild(coin); this.grid[r][c] = coin; initialCoinCount++; // Increment local counter } } } return initialCoinCount; // Return the count }; // ... and update the initialization logic at the bottom of your code ... var gridService = new GridService(game, gameGrid); // ... // 3. Initialize Services in correct order totalCoins = gameGrid.initBackground(); // <-- CHANGE THIS LINE gameGrid.initBeatGrid(); // <-- SEPARATE THIS CALL // Then remove 'this.initBackground()' from inside gameGrid.initialize() to avoid running it twice. // OR, make gameGrid.initialize() return the count. Let's do the first option for clarity. // Inside GameGrid class: this.initialize = function () { // this.initBackground(); // Remove this line this.initBeatGrid(); };
User prompt
Step 3: Update the Game Logic to Use the New System Now, we need to modify SequencerService.doStepActions to use our new functions. When creating a projectile, pass it the checkRoundCompletion function. When the player collects a coin, call checkRoundCompletion instead of manually incrementing the counter and checking the total. Here is the modified section of SequencerService.doStepActions: // ... inside SequencerService.doStepActions ... // Check for coin collection or enemy collision at current position var heroRow = hero.gridRow; var heroCol = hero.currentCol; var entity = gameGrid.grid[heroRow][heroCol]; if (entity) { // Check if it's an enemy if (!entity.collect) { // ... existing enemy logic ... } else if (entity.collect) { // It's a coin - clear any enemy tracking this.enemyCellRow = undefined; this.enemyCellCol = undefined; entity.collect(); gameGrid.grid[heroRow][heroCol] = null; // **** MODIFIED PART 1 **** // We no longer increment collectedCoins here directly. // Instead, we call our new centralized function. // Decrement projectile counter and fire projectile if needed coinsToNextProjectile--; if (coinsToNextProjectile <= 0) { // Find all uncollected coins on the grid var availableCoins = []; for (var r = 0; r < gameGrid.playableRows; r++) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { var obj = gameGrid.grid[r][c]; if (obj && obj.collect && !obj.collected) { availableCoins.push(obj); } } } if (availableCoins.length > 0) { var targetIdx = Math.floor(Math.random() * availableCoins.length); var targetCoin = availableCoins[targetIdx]; // Create projectile at hero's position // **** MODIFIED PART 2 **** // Pass the checkRoundCompletion function as a callback var projectile = new Projectile(targetCoin, checkRoundCompletion); projectile.x = hero.x; projectile.y = hero.y; game.addChild(projectile); } coinsToNextProjectile = 10; // Reset counter after firing } updateProjectileCounterUI(); // Apply combo multiplier to score (incremental: 1,2,3...) var points = this.comboMultiplier; LK.setScore(LK.getScore() + points); uiService.updateScore(LK.getScore()); AudioService.playCoinCollect(); // ... existing animation logic for the hero eating ... // Mark that we collected a coin this beat this.collectedThisBeat = true; // Increment combo multiplier by 1 for each consecutive coin this.comboMultiplier++; this.comboCount++; // **** MODIFIED PART 3 **** // Call the centralized function for player collection. checkRoundCompletion(); } } else { // ... existing logic for when hero is on an empty cell ... }
User prompt
Step 2: Modify the Projectile to Use the New System The Projectile class needs a way to call checkRoundCompletion. The best way to do this is to pass the function as a callback when the projectile is created. First, update the Projectile class to accept and use this callback. // Change this line: var Projectile = Container.expand(function (targetCoin) { // To this: var Projectile = Container.expand(function (targetCoin, onCollectCallback) { var self = Container.call(this); self.gfx = self.attachAsset('Projectile', { anchorX: 0.5, anchorY: 0.5 }); self.targetCoin = targetCoin; // ... rest of the properties ... self.update = function () { // ... existing update logic ... // Check collision (use bounding box for simplicity) if (!self.targetCoin.collected && Math.abs(self.x - self.targetCoin.x) < 40 && Math.abs(self.y - self.targetCoin.y) < 40) { // Collect the coin if (typeof self.targetCoin.collect === "function") { self.targetCoin.collect(); // **** THIS IS THE NEW PART **** // Call the callback to update game state if (onCollectCallback) { onCollectCallback(); } } self.destroy(); } // ... rest of update logic ... }; return self; });
User prompt
Step 1: Create a Central Round Management System Instead of having scattered logic, let's create a couple of functions to handle round state. Place these functions near where you define currentRound, totalCoins, etc. var currentRound = 1; var totalCoins = 0; var collectedCoins = 0; // ... other global variables ... /** * Resets the game state and starts the next round. */ function startNextRound() { AudioService.playNewLevel(); currentRound++; collectedCoins = 0; // Reset the hero's position to a new random spot var hero = playerService.getHeroInstance(); var randomRow = Math.floor(Math.random() * gameGrid.playableRows); var randomCol = Math.floor(Math.random() * GameConstants.GRID_COLS); hero.x = GameConstants.GRID_LEFT + randomCol * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; hero.y = GameConstants.GRID_TOP + randomRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; hero.currentCol = randomCol; hero.gridRow = randomRow; // Reset the grid with new coins, enemies, and get the new total coin count totalCoins = gameGrid.resetGridWithCoins(randomRow, randomCol); // Reset and re-disable cells for the new round antiCheatService.reset(); } /** * Checks if the round is complete and triggers the next round if it is. * This function will be called whenever a coin is collected. */ function checkRoundCompletion() { collectedCoins++; if (collectedCoins >= totalCoins) { // Use a short delay before starting the next round to let the last collection animation finish LK.setTimeout(startNextRound, 500); } }
User prompt
Projectiles Don't Update the collectedCoins Counter: In your Projectile class's update method, when a projectile collides with a coin, you correctly call self.targetCoin.collect(). This makes the coin animate and destroy itself. However, you do not increment the global collectedCoins variable. The game visually removes the coin, but logically, it never gets counted towards round completion.
User prompt
the first level is no longer finishable
Code edit (1 edits merged)
Please save this source code
User prompt
can you add a tween animation that makes the character pop like increase it's scale slightly then snap back to the original scale to give the illusion it jumps between cells ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
PERFECT integration! one thing that somehow got eliminated, is hsowing the hero_2 frame when the hero is facing the camera, as in showing the hero asset, but if in that stance it eats a coin, it should show hero_2 before returning to just hero stance
User prompt
now let's add more frames to our hero. Use Hero_Left_Walk when the character is moving left, Hero_RIght_Walk when moving right and Hero_Left_Eat & Hero_Right_Eat for the same direction, but if eating a coin, use the Eat variation instead. When the character is moving down use the current frame, but when moving up use Hero_Back_Walk. And when constinously moving up, use a different frame called Hero_Back_Walk_2 that's the same asset but flipped on it's x axis, so if the next cell is also a beat up, you show frame Hero_Back_Walk_2 and the next beat up you show Hero_Back_Walk and as long as the character keeps moving up, altenate between these two frames every beat
User prompt
create 1 asset named Hero_Back_Walk
User prompt
create 4 new assets name Hero_Left_Walk, Hero_Right_Walk, Hero_Left_Eat, Hero_Right_Eat
User prompt
i can't finish level 1, after collecting all the coins the level doesnt advance to level 2
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ /** * 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. */ var BeatCell = Container.expand(function (row) { var self = Container.call(this); self.active = false; self.row = row; // Know its row for external logic to interpret // Initialize with appropriate beat tile at 20% transparency var assetId = 'cell'; if (row === 0) { assetId = 'cellActive'; } else if (row === 1) { assetId = 'cellLeft'; } else if (row === 2) { assetId = 'cellRight'; } else if (row === 3) { assetId = 'cellDown'; } self.gfx = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, alpha: 0.1 }); self.toggle = function () { self.active = !self.active; }; self.setActive = function (val) { if (self.active !== val) { self.toggle(); } }; // The `down` event is removed and is now handled by the central InputService. return self; }); /** * 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; }); /** * Enemy * Responsibility: Static enemy that causes game over when touched */ var Enemy = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); return self; }); /** * AntiCheatService * Responsibility: Manages disabling random cells from each beat layer each round */ /** * 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; }); /** * Projectile * Responsibility: Projectile shot by hero, accelerates toward a target coin and collects it on hit */ var Projectile = Container.expand(function (targetCoin, onCollectCallback) { var self = Container.call(this); self.gfx = self.attachAsset('Projectile', { anchorX: 0.5, anchorY: 0.5 }); self.targetCoin = targetCoin; self.onCollectCallback = onCollectCallback; self.speed = 10; // initial speed self.maxSpeed = 50; self.acceleration = 2; self.lastX = self.x; self.lastY = self.y; self.update = function () { if (!self.targetCoin || self.targetCoin.collected) { self.destroy(); return; } // Calculate direction to target var dx = self.targetCoin.x - self.x; var dy = self.targetCoin.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 1) { dist = 1; } // Accelerate if (self.speed < self.maxSpeed) { self.speed += self.acceleration; if (self.speed > self.maxSpeed) { self.speed = self.maxSpeed; } } // Move towards target var moveX = dx / dist * self.speed; var moveY = dy / dist * self.speed; self.x += moveX; self.y += moveY; // Check collision (use bounding box for simplicity) if (!self.targetCoin.collected && Math.abs(self.x - self.targetCoin.x) < 40 && Math.abs(self.y - self.targetCoin.y) < 40) { // Collect the coin if (typeof self.targetCoin.collect === "function") { self.targetCoin.collect(); // **** THIS IS THE NEW PART **** if (typeof self.onCollectCallback === "function") { self.onCollectCallback(); } } self.destroy(); } self.lastX = self.x; self.lastY = self.y; }; return self; }); /**** * Initialize Game ****/ /************************* * SERVICES *************************/ /** * AudioService * Responsibility: To handle all requests to play sounds and music. */ /************************* * INITIALIZE & RUN GAME *************************/ // 1. Create Game Instance var game = new LK.Game({ backgroundColor: 0x181830 }); /**** * Game Code ****/ /** * AntiCheatService * Responsibility: Manages disabling random cells from each beat layer each round */ /** * GameConstants * Responsibility: To hold all static configuration and magic numbers in one place * for easy tuning and maintenance. */ /************************* * GAME CONFIGURATION *************************/ // Plugins (Unchanged) // Assets (Unchanged) /************************* * ASSETS & PLUGINS *************************/ // 2. Instantiate All Services /************************* * CORE GAME OBJECTS *************************/ /** * 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. */ function AntiCheatService(gridService, gameGrid) { this.gridService = gridService; this.gameGrid = gameGrid; this.disabledCells = []; // Array to store currently disabled cells this.prevDisabledCells = []; // Track previously disabled cells for re-enabling this.cellsPerLayer = 2; // Number of cells to disable per layer this.initialize = function () { // Disable initial cells for round 1 this.disableRandomCells(); }; this.disableRandomCells = function () { // Store previously disabled cells this.prevDisabledCells = this.disabledCells.slice(); this.disabledCells = []; // Process rows in order: up (0), left (1), right (2), down (3) for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { var availableCols = []; // Get all columns for this row, excluding previously disabled ones for (var c = 0; c < GameConstants.GRID_COLS; c++) { var isValid = true; // Check if this column was previously disabled for this row for (var p = 0; p < this.prevDisabledCells.length; p++) { if (this.prevDisabledCells[p].row === r && this.prevDisabledCells[p].col === c) { isValid = false; break; } } // Check minimum distance constraint from already disabled cells in this row for (var d = 0; d < this.disabledCells.length; d++) { if (this.disabledCells[d].row === r) { var distance = Math.abs(this.disabledCells[d].col - c); if (distance < 3) { // Minimum 2 tiles between disabled cells isValid = false; break; } } } // Check diagonal constraint for rows below the first if (r > 0) { // Only check the immediately previous row for diagonal constraints var prevRow = r - 1; for (var d = 0; d < this.disabledCells.length; d++) { if (this.disabledCells[d].row === prevRow) { var prevCol = this.disabledCells[d].col; // Check if this column is diagonally adjacent to a disabled cell in the previous row if (c === prevCol - 1 || c === prevCol || c === prevCol + 1) { isValid = false; break; } } } } if (isValid) { availableCols.push(c); } } // Select cells with minimum distance constraint var selectedCols = []; var attempts = 0; while (selectedCols.length < this.cellsPerLayer && availableCols.length > 0 && attempts < 100) { attempts++; // Pick a random column from available var randomIndex = Math.floor(Math.random() * availableCols.length); var col = availableCols[randomIndex]; // Check if this column maintains minimum distance from already selected var validChoice = true; for (var s = 0; s < selectedCols.length; s++) { if (Math.abs(selectedCols[s] - col) < 3) { validChoice = false; break; } } if (validChoice) { selectedCols.push(col); // Remove this column and nearby columns from available availableCols = availableCols.filter(function (c) { return Math.abs(c - col) >= 3; }); } else { // Remove this column as it's too close availableCols.splice(randomIndex, 1); } } // Disable the selected columns for (var i = 0; i < selectedCols.length; i++) { var col = selectedCols[i]; var cell = this.gameGrid.getBeatCell(r, col); if (cell) { this.disabledCells.push({ row: r, col: col, cell: cell }); // Hide the cell cell.visible = false; } } } }; this.reset = function () { // First, enable ALL cells in the beat grid for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { var cell = this.gameGrid.getBeatCell(r, c); if (cell) { cell.visible = true; } } } // Clear both arrays to start fresh this.prevDisabledCells = []; this.disabledCells = []; // 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 background visuals for all cells this.initBackground = function () { var initialCoinCount = 0; for (var r = 0; r < this.rows; r++) { for (var c = 0; c < this.cols; c++) { var bgCell = LK.getAsset('emptycell', { anchorX: 0.5, anchorY: 0.5, alpha: 0.3 }); 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; this.game.addChild(bgCell); // Add coin to all playable cells (hero will be positioned after coins are created) if (r < this.playableRows) { var coin = new Coin(); coin.x = bgCell.x; coin.y = bgCell.y; this.game.addChild(coin); this.grid[r][c] = coin; initialCoinCount++; } } } return initialCoinCount; }; // 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); cell.x = GameConstants.GRID_LEFT + c * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; cell.y = GameConstants.GRID_TOP + gridRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; this.game.addChild(cell); this.grid[gridRow][c] = cell; } } }; // 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 the BeatCell at a given (x, y) screen coordinate, or null if not in beat grid this.getBeatCellAt = function (x, y) { for (var r = 0; r < this.beatRows; r++) { var gridRow = this.playableRows + r; for (var c = 0; c < this.cols; c++) { var cell = this.grid[gridRow][c]; if (!cell) { continue; } var cx = GameConstants.GRID_LEFT + c * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; var cy = GameConstants.GRID_TOP + gridRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; if (x >= cx - GameConstants.CELL_SIZE / 2 && x <= cx + GameConstants.CELL_SIZE / 2 && y >= cy - GameConstants.CELL_SIZE / 2 && y <= cy + GameConstants.CELL_SIZE / 2) { return cell; } } } return null; }; // 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; }; // 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 except hero's position this.resetGridWithCoins = function (heroRow, heroCol) { // 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 cells except hero's position var coinCount = 0; var availablePositions = []; for (var r = 0; r < this.playableRows; r++) { for (var c = 0; c < this.cols; c++) { if (r !== heroRow || c !== heroCol) { // Calculate distance from hero var rowDist = Math.abs(r - heroRow); var colDist = Math.abs(c - heroCol); var distance = Math.max(rowDist, colDist); // Chebyshev distance 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; this.game.addChild(coin); this.grid[r][c] = coin; coinCount++; // Track positions that are far enough from hero for enemy placement if (distance > 2) { availablePositions.push({ r: r, c: c }); } } } } // Place enemies starting from round 2 var enemyCount = currentRound - 1; // 0 at round 1, 1 at round 2, etc. if (enemyCount > 0 && availablePositions.length > 0) { // Shuffle available positions for (var i = availablePositions.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = availablePositions[i]; availablePositions[i] = availablePositions[j]; availablePositions[j] = temp; } // Place enemies up to enemyCount or available positions var enemiesToPlace = Math.min(enemyCount, availablePositions.length); for (var i = 0; i < enemiesToPlace; i++) { var pos = availablePositions[i]; // Remove coin at this position if (this.grid[pos.r][pos.c]) { this.grid[pos.r][pos.c].destroy(); coinCount--; } // Place enemy var enemy = new Enemy(); enemy.x = GameConstants.GRID_LEFT + pos.c * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; enemy.y = GameConstants.GRID_TOP + pos.r * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; this.game.addChild(enemy); this.grid[pos.r][pos.c] = enemy; } } 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 }; /************************* * SERVICES *************************/ /** * AudioService * Responsibility: To handle all requests to play sounds and music. */ 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; 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); // 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 this.game.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; } }; this.updateScore = function (newScore) { this.scoreTxt.setText(newScore); }; // Main Highlighting Logic: highlights inactive cells in the current column this.updateColumnHighlight = function (currentColumnIndex) { // Hide all highlights first for (var i = 0; i < this.columnHighlights.length; i++) { this.columnHighlights[i].alpha = 0; } // Get active states for the current column var activeStates = gridService.getActiveStatesForColumn(currentColumnIndex); // Loop through each row in the beat grid for (var i = 0; i < GameConstants.BEAT_ROWS; i++) { if (!activeStates[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.2; // semi-transparent highlight } } }; } /** * GridService * Responsibility: To create, manage the state of, and update the visuals of the beat grid. */ function GridService(game, gameGrid) { this.game = game; this.gameGrid = gameGrid; // Centralized grid this.initialize = function () { this.gameGrid.initialize(); }; // Returns the BeatCell at a given (x, y) screen coordinate, or null if not in beat grid this.getCellAt = function (x, y) { var cell = this.gameGrid.getBeatCellAt(x, y); // Check if cell is disabled by anti-cheat if (cell && antiCheatService) { // Find the beat row of this cell var beatRow = cell.row; var col = -1; // Find column for (var c = 0; c < GameConstants.GRID_COLS; c++) { if (this.gameGrid.getBeatCell(beatRow, c) === cell) { col = c; break; } } if (col !== -1 && antiCheatService.isCellDisabled(beatRow, col)) { return null; // Return null if cell is disabled } } return cell; }; // 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 = antiCheatService && 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) { var needsChange = mode === 'add' && !cell.active || mode === 'remove' && cell.active; if (needsChange) { // If adding, remove any other active beat in this column (same col, different row) if (mode === 'add') { var col = null; // Find the column index of this cell for (var c = 0; c < GameConstants.GRID_COLS; c++) { // cell.row is the beat row (0..BEAT_ROWS-1), but grid is [playableRows+row][col] var gridRow = GameConstants.TOTAL_ROWS - GameConstants.BEAT_ROWS + cell.row; if (this.gameGrid.grid[gridRow][c] === cell) { col = c; break; } } if (col !== null) { for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { var gridRow = GameConstants.TOTAL_ROWS - GameConstants.BEAT_ROWS + r; var otherCell = this.gameGrid.grid[gridRow][col]; if (r !== cell.row && otherCell && otherCell.active) { otherCell.setActive(false); this.updateCellVisual(otherCell); } } } } cell.toggle(); this.updateCellVisual(cell); } }; this.updateCellVisual = function (cell) { cell.gfx.destroy(); var assetId = 'cell'; var alpha = 1.0; // Always show the appropriate beat tile based on row if (cell.row === 0) { assetId = 'cellActive'; } else if (cell.row === 1) { assetId = 'cellLeft'; } else if (cell.row === 2) { assetId = 'cellRight'; } else if (cell.row === 3) { assetId = 'cellDown'; } // Set transparency based on active state if (!cell.active) { alpha = 0.1; } cell.gfx = cell.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, alpha: alpha }); }; // 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 = antiCheatService && 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) { this.game = game; this.hero = null; this.initialize = function () { this.hero = new Hero(); // Select random starting position in playable grid var randomRow = Math.floor(Math.random() * gameGrid.playableRows); var randomCol = Math.floor(Math.random() * GameConstants.GRID_COLS); // Calculate position based on random grid coordinates this.hero.x = GameConstants.GRID_LEFT + randomCol * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; this.hero.y = GameConstants.GRID_TOP + randomRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; this.hero.currentCol = randomCol; this.hero.gridRow = randomRow; // Update CHAR_ROW to match random starting row GameConstants.CHAR_ROW = randomRow; this.game.addChild(this.hero); // Remove coin at hero's starting position if one exists var coin = gameGrid.grid[randomRow][randomCol]; if (coin && coin.collect) { coin.destroy(); gameGrid.grid[randomRow][randomCol] = null; totalCoins--; // Decrease initial coin count } }; 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 = 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(); // Update CHAR_ROW for collision logic GameConstants.CHAR_ROW = newRow; }; } /** * 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) { this.gridService = gridService; this.playerService = playerService; this.isPainting = false; this.paintMode = null; this.lastPaintedCell = null; this.dragHero = false; this.handleDown = function (x, y) { var cell = this.gridService.getCellAt(x, y); if (cell) { this.isPainting = true; this.paintMode = cell.active ? 'remove' : 'add'; this.lastPaintedCell = cell; this.gridService.toggleCell(cell, this.paintMode); } else { var hero = this.playerService.getHeroInstance(); var dx = x - hero.x, dy = y - hero.y; if (dx * dx + dy * dy < 200 * 200) { this.dragHero = true; } } }; this.handleUp = function () { this.isPainting = false; this.paintMode = null; this.lastPaintedCell = null; this.dragHero = false; }; this.handleMove = function (x, y) { if (this.isPainting) { var cell = this.gridService.getCellAt(x, y); if (cell && cell !== this.lastPaintedCell) { this.lastPaintedCell = cell; this.gridService.toggleCell(cell, this.paintMode); } } else if (this.dragHero) { this.playerService.handleDrag(x); } }; } /** * 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) { this.gridService = gridService; this.playerService = playerService; this.uiService = uiService; 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.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 FIRST for the current step 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. this.doStepActions(); } }; 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; var actions = this.gridService.getActiveActionsForStep(previousStep); // 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 previous step for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { var cell = gameGrid.getBeatCell(r, previousStep); if (cell && cell.active && !antiCheatService.isCellDisabled(r, previousStep)) { // Create bump animation: scale up to 110% then back to 100% tween(cell, { scaleX: 1.1, scaleY: 1.1 }, { duration: 100, easing: tween.easeOut, onFinish: function (targetCell) { return function () { tween(targetCell, { scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeIn }); }; }(cell) }); } } // Store hero's previous position before performing actions var hero = this.playerService.getHeroInstance(); var prevHeroRow = hero.gridRow; var prevHeroCol = hero.currentCol; // Perform actions for the previous 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 } } // Check for coin collection or enemy collision at current position var heroRow = hero.gridRow; var heroCol = hero.currentCol; var entity = gameGrid.grid[heroRow][heroCol]; if (entity) { // Check if it's an enemy if (!entity.collect) { // Enemy detected - store the position but don't trigger game over yet this.enemyCellRow = heroRow; this.enemyCellCol = heroCol; } else if (entity.collect) { // It's a coin - clear any enemy tracking this.enemyCellRow = undefined; this.enemyCellCol = undefined; entity.collect(); gameGrid.grid[heroRow][heroCol] = null; // Decrement projectile counter and fire projectile if needed coinsToNextProjectile--; if (coinsToNextProjectile <= 0) { // Find all uncollected coins on the grid var availableCoins = []; for (var r = 0; r < gameGrid.playableRows; r++) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { var obj = gameGrid.grid[r][c]; if (obj && obj.collect && !obj.collected) { availableCoins.push(obj); } } } if (availableCoins.length > 0) { var targetIdx = Math.floor(Math.random() * availableCoins.length); var targetCoin = availableCoins[targetIdx]; // Create projectile at hero's position, pass checkRoundCompletion as callback var projectile = new Projectile(targetCoin, checkRoundCompletion); projectile.x = hero.x; projectile.y = hero.y; game.addChild(projectile); } coinsToNextProjectile = 10; // Reset counter after firing } updateProjectileCounterUI(); // Apply combo multiplier to score (incremental: 1,2,3...) var points = this.comboMultiplier; LK.setScore(LK.getScore() + points); uiService.updateScore(LK.getScore()); AudioService.playCoinCollect(); // 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); } // Mark that we collected a coin this beat this.collectedThisBeat = true; // Increment combo multiplier by 1 for each consecutive coin this.comboMultiplier++; this.comboCount++; // Call the centralized function for player collection. checkRoundCompletion(); } } else { // No entity at current position - clear any enemy tracking this.enemyCellRow = undefined; this.enemyCellCol = undefined; // No coin collected - reset combo if we had one if (this.comboMultiplier > 1) { this.comboMultiplier = 1; this.comboCount = 0; } } }; } var currentRound = 1; var totalCoins = 0; var collectedCoins = 0; // --- Projectile Counter UI --- var coinsToNextProjectile = 10; var projectileCounterTxt = new Text2("Next shot: 10", { size: 70, fill: 0xFFD700 }); projectileCounterTxt.anchor.set(1, 0); // right-top // Place in top right, but leave 100px margin for menu LK.gui.topRight.addChild(projectileCounterTxt); // Helper to update the UI text function updateProjectileCounterUI() { projectileCounterTxt.setText("Next shot: " + coinsToNextProjectile); } updateProjectileCounterUI(); /** * Resets the game state and starts the next round. */ function startNextRound() { AudioService.playNewLevel(); currentRound++; collectedCoins = 0; // Reset the hero's position to a new random spot var hero = playerService.getHeroInstance(); var randomRow = Math.floor(Math.random() * gameGrid.playableRows); var randomCol = Math.floor(Math.random() * GameConstants.GRID_COLS); // Reset the grid with new coins, enemies, and get the new total coin count totalCoins = gameGrid.resetGridWithCoins(randomRow, randomCol); // Now update hero's position to match the logical grid hero.x = GameConstants.GRID_LEFT + randomCol * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; hero.y = GameConstants.GRID_TOP + randomRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; hero.currentCol = randomCol; hero.gridRow = randomRow; // Reset and re-disable cells for the new round antiCheatService.reset(); } /** * Checks if the round is complete and triggers the next round if it is. * This function will be called whenever a coin is collected. */ function checkRoundCompletion() { collectedCoins++; if (collectedCoins >= totalCoins) { // Use a short delay before starting the next round to let the last collection animation finish LK.setTimeout(startNextRound, 500); } } // --- Patch coin collection logic to update counter and reset on projectile fire --- // We'll patch the doStepActions method in SequencerService below var uiService = new UIService(game); var gameGrid = new GameGrid(game); var antiCheatService = new AntiCheatService(null, gameGrid); // Initialize before gridService var gridService = new GridService(game, gameGrid); var playerService = new PlayerService(game); var inputService = new InputService(gridService, playerService); var sequencerService = new SequencerService(gridService, playerService, uiService); // 3. Initialize Services in correct order totalCoins = gameGrid.initBackground(); gameGrid.initBeatGrid(); uiService.initialize(); playerService.initialize(); sequencerService.initialize(); antiCheatService.initialize(); // Manually draw the highlight for the first beat (step 0) immediately. uiService.updateColumnHighlight(sequencerService.currentStep); // 4. Connect Input Handlers to the Input Service game.down = function (x, y) { 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(); ;
===================================================================
--- original.js
+++ change.js
@@ -1230,14 +1230,15 @@
// Reset the hero's position to a new random spot
var hero = playerService.getHeroInstance();
var randomRow = Math.floor(Math.random() * gameGrid.playableRows);
var randomCol = Math.floor(Math.random() * GameConstants.GRID_COLS);
+ // Reset the grid with new coins, enemies, and get the new total coin count
+ totalCoins = gameGrid.resetGridWithCoins(randomRow, randomCol);
+ // Now update hero's position to match the logical grid
hero.x = GameConstants.GRID_LEFT + randomCol * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2;
hero.y = GameConstants.GRID_TOP + randomRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2;
hero.currentCol = randomCol;
hero.gridRow = randomRow;
- // Reset the grid with new coins, enemies, and get the new total coin count
- totalCoins = gameGrid.resetGridWithCoins(randomRow, randomCol);
// Reset and re-disable cells for the new round
antiCheatService.reset();
}
/**
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