User prompt
totalCoins is Inaccurate: In playerService.initialize, you correctly decrement totalCoins when the hero spawns on a coin. However, this logic is now inside startNextRound, but the totalCoins variable is global. When the hero spawns, the totalCoins count is off by one, which will prevent checkRoundCompletion from ever being true if the hero spawns on the last coin. The fix is to set totalCoins after all special items (hero, princess, key, enemies) have been placed and removed from the cellPool.
User prompt
totalCoins is Inaccurate: In playerService.initialize, you correctly decrement totalCoins when the hero spawns on a coin. However, this logic is now inside startNextRound, but the totalCoins variable is global. When the hero spawns, the totalCoins count is off by one, which will prevent checkRoundCompletion from ever being true if the hero spawns on the last coin. The fix is to set totalCoins after all special items (hero, princess, key, enemies) have been placed and removed from the cellPool.
User prompt
remove the logic that completes a level when collecting a coin, the only way to move to the next level now is to first collect the key then take it to the princess
User prompt
playerService.initialize() is Never Called: You correctly moved hero creation into startNextRound, but you still have a call to playerService.initialize() in the main startup sequence which you've commented out. The PlayerService is instantiated but its initialize method, which creates the hero, is never actually called. The hero is created later in startNextRound, which works, but it's a structural inconsistency. It would be cleaner to call playerService.initialize() inside startNextRound. please fix this
User prompt
The checkRoundCompletion() function should only be called when an object is confirmed to be destroyed. Remove the checkRoundCompletion() call from sequencerService.doStepActions. Modify the Coin.collect method. Move the checkRoundCompletion() call into the onComplete callback of the tween. This guarantees it only runs after the coin is gone. // In Coin.js self.collect = function() { // ... tween(self, { ... }, { // ... onComplete: function() { self.destroy(); checkRoundCompletion(); // <-- MOVED HERE } }); }; Use code with caution. JavaScript The Projectile class is already designed correctly! You are passing the callback, which is great. The issue is that the Coin's own collect method doesn't use a callback system. The fix above will solve it for both hero and projectile collections.
User prompt
after restarting the level, i can no longer disabled the beats that remained active from the previous level, please fix this
User prompt
after a new level starts the beat grid is disabled and I can no longer change the beat, pls fix this bug
User prompt
there's a bug where when a new round load, it restarts twice, as if there's 2 conflicting systems reloading the level, delete any legacy system in place. Scrutinize code for common bug patterns (e.g., off-by-one errors, null pointer exceptions, race conditions, unhandled exceptions, improper error propagation). Identify "legacy text": commented-out code blocks, unused variables, dead functions, or obsolete feature flags. Detect duplicate or near-duplicate code blocks across different files or modules. Pinpoint files or modules that appear to have no current purpose or have been superseded by newer implementations. Critically evaluate algorithms and business logic for correctness, efficiency, and clarity. Identify overly complex functions or methods that could be simplified or broken down. Look for "anti-patterns" or less-than-optimal implementations of common tasks. Assess state management for clarity, efficiency, and potential race conditions or inconsistencies.
User prompt
the beat grid is no longer visible and i can no longer place beats
User prompt
Please fix the bug: 'Cannot set properties of null (setting 'gridRow')' in or related to this line: 'playerService.initialize();' Line Number: 1299
User prompt
Please fix the bug: 'Cannot set properties of null (setting 'gridRow')' in or related to this line: 'hero.gridRow = heroCell.row;' Line Number: 1300
User prompt
Phase 5: The Final Cleanup Unify the Game Start: Go to the very bottom of your script where the game is first initialized. Remove the old, fragmented setup. This includes the original calls to playerService.initialize() and the initial setting of totalCoins. Replace them with a single call to your new startNextRound() function. Before you call it, you may need to manually set currentRound to 0, so that when startNextRound increments it, the first round correctly becomes Round 1.
User prompt
Please fix the bug: 'TypeError: Cannot set properties of null (setting 'gridRow')' in or related to this line: 'this.hero.gridRow = newRow;' Line Number: 1001
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'currentCol')' in or related to this line: 'var newCol = this.hero.currentCol + direction;' Line Number: 974
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'gridRow')' in or related to this line: 'var newRow = this.hero.gridRow !== undefined ? this.hero.gridRow : GameConstants.CHAR_ROW;' Line Number: 990
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'upWalkAlt')' in or related to this line: 'isUpWalkAlt = !hero.upWalkAlt;' Line Number: 915
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'lastDirection')' in or related to this line: 'var eatDirection = hero.lastDirection || "down";' Line Number: 1203
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'gridRow')' in or related to this line: 'var heroRow = hero.gridRow;' Line Number: 1138
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'setFrame')' in or related to this line: 'hero.setFrame(direction, false, isUpWalkAlt);' Line Number: 937
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'upWalkAlt')' in or related to this line: 'var isUpWalkAlt = hero.upWalkAlt || false;' Line Number: 903
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'gridRow')' in or related to this line: 'var prevHeroRow = hero.gridRow;' Line Number: 1122
User prompt
Please fix the bug: 'Cannot set properties of null (setting 'gridRow')' in or related to this line: 'hero.gridRow = heroCell.row;' Line Number: 1300
User prompt
hase 5: The Final Cleanup Unify the Game Start: Go to the very bottom of your script where the game is first initialized. Remove the old, fragmented setup. This includes the original calls to playerService.initialize() and the initial setting of totalCoins. Replace them with a single call to your new startNextRound() function. Before you call it, you may need to manually set currentRound to 0, so that when startNextRound increments it, the first round correctly becomes Round 1.
User prompt
Phase 4: Updating the Gameplay Interaction Now you must teach the game what to do when the player interacts with the new items. Navigate to the SequencerService: Find the doStepActions function, which is the heart of the game's turn-by-turn logic. Locate the Collision Check: Find the part of the function where it checks what entity the hero is currently standing on. Expand the "If-Else" Logic: You will add new checks for the Princess and Key here. The new order of checks should be: Check for the Key: Is the entity a Key? If so: Set your global playerHasKey variable to true. Play a special sound effect. Destroy the Key object from the grid. Check for the Princess: Is the entity a Princess? If so: Inside this check, add another check: is playerHasKey currently true? If it is, the player wins! Call your startNextRound function (perhaps with a small delay for dramatic effect). If playerHasKey is false, do nothing. The player has reached the door without the key. Check for Coins and Enemies: The existing logic for handling coins (incrementing score) and enemies (game over) remains the same.
User prompt
Phase 3: Rebuilding the Round Setup Logic This is the most critical phase. We are going to completely rebuild how a new round starts, making it the single source of truth for placing everything on the grid. Find the startNextRound Function: Locate your main function for starting a new level. Rewrite its Internal Logic Step-by-Step: You will replace its current contents with a new, robust algorithm. Follow this sequence precisely: Step A: Clean the Slate: First, ensure all old game objects (Coin, Enemy, Key, Princess) from the previous round are destroyed and removed from the grid. Step B: Reset the Rules: Reset your global variables for the new round. Increment currentRound, reset collectedCoins, and most importantly, set playerHasKey back to false. Step C: Create a "Pool" of All Available Cells: Create a temporary list that contains the row and column of every single playable grid cell. Step D: Shuffle the Pool: Randomize the order of the cells in this temporary list. This is the secret to fair, non-overlapping placement. Step E: Place the Special Items: Now, "pull" coordinates from your shuffled list one by one to place the important entities: Place the Player: Take the first coordinate from the list and move the hero's logical and visual position to that cell. Place the Princess: Take the next coordinate from the list. Place a new Princess object there. (Optional but recommended: Add a check to make sure she isn't right next to the player. If she is, just grab the next coordinate from the list instead). Place the Key: Take the next coordinate and place a new Key object there. Place the Enemies: Determine how many enemies this round should have (e.g., currentRound - 1). Loop that many times, taking a new coordinate from the list for each enemy and placing an Enemy object there. Step F: Fill the Rest with Coins: Go through all the coordinates still remaining in your shuffled list. For each one, place a new Coin object. Count how many you place and set your totalCoins variable to this final number. Step G: Final Housekeeping: After all placements are done, reset your AntiCheatService for the beat grid.
/**** * 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; }); /** * 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; }); /** * 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 ****/ /** * 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. */ /************************* * CORE GAME OBJECTS *************************/ // 2. Instantiate All Services /************************* * ASSETS & PLUGINS *************************/ // Assets (Unchanged) // Plugins (Unchanged) /************************* * GAME CONFIGURATION *************************/ /** * GameConstants * Responsibility: To hold all static configuration and magic numbers in one place * for easy tuning and maintenance. */ /** * AntiCheatService * Responsibility: Manages disabling random cells from each beat layer each round */ 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 (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; this.game.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 }; /************************* * 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, key, princess, or enemy collision at current position var heroRow = hero.gridRow; var heroCol = hero.currentCol; var entity = gameGrid.grid[heroRow][heroCol]; if (entity) { // --- Key logic --- if (entity instanceof Key) { playerHasKey = true; AudioService.playNewLevel(); // Play a special sound for key collection (reuse NewLevel for now) entity.destroy(); gameGrid.grid[heroRow][heroCol] = null; } // --- Princess logic --- else if (entity instanceof Princess) { if (playerHasKey) { // Player wins the round! Start next round after a short delay LK.setTimeout(startNextRound, 600); return; } // If player doesn't have key, do nothing (can't win yet) } // --- Enemy logic --- else if (!entity.collect) { // Enemy detected - store the position but don't trigger game over yet this.enemyCellRow = heroRow; this.enemyCellCol = heroCol; } // --- Coin 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; // 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 // Do NOT call checkRoundCompletion here! Only call it when the coin is actually destroyed by the projectile. 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++; // Do NOT call checkRoundCompletion here! Only call it when the coin is actually destroyed by the projectile or collected by the hero. // Call checkRoundCompletion after player collects a coin 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; var playerHasKey = false; // Track if player has the key for win condition // All round/coin setup is now handled exclusively in startNextRound. Remove any legacy setup elsewhere. // --- 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(); // All round/coin/hero/beat grid setup is now handled in startNextRound. No legacy setup should remain. // All beat grid and coin initialization is now handled in startNextRound. Remove any legacy grid/coin setup elsewhere. /** * Resets the game state and starts the next round. */ function startNextRound() { // --- Step A: Clean the Slate --- // Destroy all old game objects (Coin, Enemy, Key, Princess) from the previous round and clear grid 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 && typeof obj.destroy === "function") { obj.destroy(); } gameGrid.grid[r][c] = null; } } // --- Step B: Reset the Rules --- currentRound++; collectedCoins = 0; playerHasKey = false; // --- Step C: Create a "Pool" of All Available Cells --- 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 }); } } // --- Step D: 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; } // --- Step E: Place the Special Items --- var hero = playerService.getHeroInstance(); if (!hero) { // If hero does not exist, create it and add to game playerService.hero = new Hero(); hero = playerService.hero; game.addChild(hero); } // Place the Player var heroCell = cellPool.shift(); hero.gridRow = heroCell.row; hero.currentCol = heroCell.col; hero.x = GameConstants.GRID_LEFT + heroCell.col * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; hero.y = GameConstants.GRID_TOP + heroCell.row * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; GameConstants.CHAR_ROW = heroCell.row; // Place the Princess (not adjacent to hero if possible) var princessCell; for (var idx = 0; idx < cellPool.length; idx++) { var candidate = cellPool[idx]; var isAdjacent = Math.abs(candidate.row - heroCell.row) <= 1 && Math.abs(candidate.col - heroCell.col) <= 1; if (!isAdjacent) { princessCell = candidate; cellPool.splice(idx, 1); break; } } if (!princessCell) { princessCell = cellPool.shift(); } var princess = new Princess(); princess.x = GameConstants.GRID_LEFT + princessCell.col * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; princess.y = GameConstants.GRID_TOP + princessCell.row * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; game.addChild(princess); gameGrid.grid[princessCell.row][princessCell.col] = princess; // Place the Key var keyCell = cellPool.shift(); var key = new Key(); key.x = GameConstants.GRID_LEFT + keyCell.col * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; key.y = GameConstants.GRID_TOP + keyCell.row * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; game.addChild(key); gameGrid.grid[keyCell.row][keyCell.col] = key; // Place the Enemies (currentRound - 1) var numEnemies = Math.max(0, currentRound - 1); for (var e = 0; e < numEnemies && cellPool.length > 0; e++) { var enemyCell = cellPool.shift(); var enemy = new Enemy(); enemy.x = GameConstants.GRID_LEFT + enemyCell.col * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; enemy.y = GameConstants.GRID_TOP + enemyCell.row * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; game.addChild(enemy); gameGrid.grid[enemyCell.row][enemyCell.col] = enemy; } // --- Step F: Fill the Rest with Coins --- var coinCount = 0; for (var i = 0; i < cellPool.length; i++) { var cell = cellPool[i]; var coin = new Coin(); coin.x = GameConstants.GRID_LEFT + cell.col * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; coin.y = GameConstants.GRID_TOP + cell.row * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; game.addChild(coin); gameGrid.grid[cell.row][cell.col] = coin; coinCount++; } totalCoins = coinCount; // --- Step G: Final Housekeeping --- // Re-initialize the beat grid so it is visible and interactive after round setup gameGrid.initialize(); antiCheatService.reset(); AudioService.playNewLevel(); } /** * 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); // Do NOT call playerService.initialize() here; hero creation is handled in startNextRound only. var inputService = new InputService(gridService, playerService); var sequencerService = new SequencerService(gridService, playerService, uiService); // 3. Initialize Services in correct order uiService.initialize(); sequencerService.initialize(); antiCheatService.initialize(); // Set currentRound to 0 so first call to startNextRound makes it 1 currentRound = 0; startNextRound(); // Remove any legacy or duplicate round/game start logic below this point to prevent double round initialization // 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(); ; ; // End of file. All round/game start logic is now centralized in startNextRound.
===================================================================
--- original.js
+++ change.js
@@ -1209,8 +1209,9 @@
var currentRound = 1;
var totalCoins = 0;
var collectedCoins = 0;
var playerHasKey = false; // Track if player has the key for win condition
+// All round/coin setup is now handled exclusively in startNextRound. Remove any legacy setup elsewhere.
// --- Projectile Counter UI ---
var coinsToNextProjectile = 10;
var projectileCounterTxt = new Text2("Next shot: 10", {
size: 70,
@@ -1223,8 +1224,10 @@
function updateProjectileCounterUI() {
projectileCounterTxt.setText("Next shot: " + coinsToNextProjectile);
}
updateProjectileCounterUI();
+// All round/coin/hero/beat grid setup is now handled in startNextRound. No legacy setup should remain.
+// All beat grid and coin initialization is now handled in startNextRound. Remove any legacy grid/coin setup elsewhere.
/**
* Resets the game state and starts the next round.
*/
function startNextRound() {
@@ -1346,8 +1349,9 @@
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);
+// Do NOT call playerService.initialize() here; hero creation is handled in startNextRound only.
var inputService = new InputService(gridService, playerService);
var sequencerService = new SequencerService(gridService, playerService, uiService);
// 3. Initialize Services in correct order
uiService.initialize();
@@ -1355,8 +1359,9 @@
antiCheatService.initialize();
// Set currentRound to 0 so first call to startNextRound makes it 1
currentRound = 0;
startNextRound();
+// Remove any legacy or duplicate round/game start logic below this point to prevent double round initialization
// 4. Connect Input Handlers to the Input Service
game.down = function (x, y) {
inputService.handleDown(x, y);
};
@@ -1372,5 +1377,6 @@
};
// 6. Start background music
AudioService.startMusic();
;
-;
\ No newline at end of file
+;
+// End of file. All round/game start logic is now centralized in startNextRound.
\ No newline at end of file
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