User prompt
The character’s movement logic should ensure that after any move (up or down), if the new `gridRow` is less than 5, it should teleport to 11; if it’s greater than 11, it should teleport to 5. - This keeps the character always within the playable area.
User prompt
so counting from bottom to top, the first 4 rows are Beat Area grid, and the characetr should not be able to travel on those, and between row 5 and 11 is the palayable grid area where the character can move, so when teleporting, use the 5th row from the bottom as the lowest playable point. so counting from bottom towards top, first 4 rows are beat grid and the character cannot move there, and counting upwards still from row 5 to 11, is the playable area, so the character teleports on row 11 or 5, COUNTING FROM THE BOTTOM TOWARDS UP
User prompt
so counting from bottom to top, the first 4 rows are Beat Area grid, and the characetr should not be able to travel on those, and between row 5 and 11 is the palayable grid area where the character can move, so when teleporting, use the 5th row from the bottom as the lowest playable point
User prompt
just as you teleport the character horizontally, also teleport it vertically when hitting the edges of the character's empty cell playable area
User prompt
when placing a beat, if that layer already has a beat on it, remove it, so only the latest placed beat remaisn as the active one
User prompt
let's move everything up by a column, and insert an extra beat layer for down movement, which should have the asset cellDown. and turn the jumping function into an upwards movement similar to left and right, so the character can move around the grid above. that means the character now starts from row 5, and no longer jumps, but moves up instead
User prompt
coins still don't spawn correctly. if the coin pattern was randomly selected to be 5 coins, the first coins must come from row 11, then the second coin must appear on the next beat, and so on so at beat 5, the 5th coin also spawns, and then they keep moving down every beat
User prompt
you are now only spawning 1 coin instead of a row of 4-6 coins
User prompt
some coins come discontinued, ensure they come one after the other even when just 4, and also ensure they spawn one by one, onstead of all of them at once
User prompt
You need to adjust the logic inside the CoinService. Instead of performing a visual intersection check between the two objects, the logic should be based on their grid coordinates. Before a coin moves down a row, perform a logical check: Identify the coin's destination: the row and column it is about to move into. Check if that destination cell matches the hero's logical position: Is the destination row the same as the hero's home row (CHAR_ROW)? Is the coin's column the same as the hero's currentCol? If both are true, then a "collection" has occurred. You should trigger the collection (update score, remove coin) and prevent the coin from moving into that cell. By checking the intended destination against the hero's logical home cell, you completely sidestep the timing problem of the jump animation. The game will feel right because a collection will happen whenever a coin reaches the hero's lane in the right column, regardless of whether the hero is on the ground or in the air. Give that thought process a try within the CoinService, and I'm confident you'll solve it! You're looking to replace the intersects() check with a grid-based coordinate check.
User prompt
let's change how coins patterns work, and instead of releasing coins spread all over 8 columns, let's make them individual to a column. so now when spawning coins, they only spawn on a random column out of the 8th, ensuring the next spawn is on a different column than the previous one, actually pick one at random from the 8 until you exhaust all of them then reset. and when spawning coins on the column, spawn anywhere between 4-6 everyt time a new batch is released
User prompt
i can no longer press the bottom 3 rows to place beats inside them :(
Code edit (1 edits merged)
Please save this source code
User prompt
detach the coin collection detection logic from the grid or cell, coins should be collectable by the character when they touch, regardless of which cell either occupy
User prompt
coins should be collected not just if the character is in the same cell as the coin, but if it simply touches the coin, so it can also be from when jumping
User prompt
points should only be awarded for collecting a coin and only 1 point per coin, not when peforming a beat
User prompt
there's a bug where the character doesn't collect coins when jumping, please fix that
User prompt
can you fix this for me?
User prompt
there's a bug where the character doesn't collect coins when jumping, please fix that by fixing how jumping logic is done
User prompt
there's a bug where the character doesn't collect coins when jumping, please fix that
User prompt
you keep moving the character higher, it should always be on row 4 countong from bottom, and when jumping it jumps to row 5
User prompt
excellent i can see it works PERFECTLY now! please extend it even further and add 2 more rows upwards so the coins now start from row 11th instead of 9
User prompt
it sort of works but the coins grid seems disconnected from the beat grid thus coins never reach the bottom? can you add empty cells asset behind the coin grids so i can see how they are positioned and why they are not a single grid?
User prompt
rethink and restructure the grid, so that the coins and the main character are part of the grid too. so if the last 3 grid would be intended for the beat blocks, the 4th on top would be for the main character to move in, and the 5th to jump in. and then between the 4th row up to row 9 which is the last counting from bottom, would be the place where coins would start spawning from, and when they move, they dont have a linear movement, rather they go one cell lower on every beat
User prompt
lovely! but actually make the coin pattrn have only 2 rows instead of 4
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ /************************* * CORE GAME OBJECTS *************************/ /** * 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 self.gfx = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); 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: To model a single coin object. (Largely unchanged). */ var Coin = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5 }); self.gridRow = 0; self.gridCol = 0; return self; }); /** * Hero * Responsibility: To model the player character. It manages its own state and animations, * but no longer triggers its own sound effects. */ var Hero = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('hero', { anchorX: 0.5, anchorY: 0.5 }); self.jumpInd = self.attachAsset('jump', { anchorX: 0.5, anchorY: 1 }); self.jumpInd.y = self.gfx.height / 2 + 10; self.jumpInd.alpha = 0; self.isJumping = false; self.currentCol = 0; self.moveTo = function (nx, ny, duration) { tween(self, { x: nx, y: ny }, { duration: duration || 180, easing: tween.cubicOut }); }; self.jump = function (jumpY, originY) { if (self.isJumping) return; self.isJumping = true; self.jumpInd.alpha = 1; tween(self, { y: jumpY }, { duration: 120, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { y: originY }, { duration: 180, easing: tween.bounceIn, onFinish: function onFinish() { self.isJumping = false; self.jumpInd.alpha = 0; } }); } }); }; 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 ****/ /** * 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 var GameConstants = { GRID_COLS: 8, TOTAL_ROWS: 11, BEAT_ROWS: 3, CHAR_ROW: 7, JUMP_ROW: 6, 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 - 80; }, 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(); }, playJump: function playJump() { LK.getSound('jumpSnd').play(); }, playMove: function playMove() { LK.getSound('moveSnd').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.stepIndicators = []; 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); // Step Indicators for (var c = 0; c < GameConstants.GRID_COLS; c++) { var ind = LK.getAsset('cellActive', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.3, scaleY: 0.3, alpha: 0.0 }); ind.x = GameConstants.GRID_LEFT + c * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; ind.y = GameConstants.GRID_TOP + GameConstants.TOTAL_ROWS * GameConstants.CELL_SIZE + 40; this.game.addChild(ind); this.stepIndicators.push(ind); } }; this.updateScore = function (newScore) { this.scoreTxt.setText(newScore); }; this.showStep = function (step) { if (this.stepIndicators[step]) { var indicator = this.stepIndicators[step]; indicator.alpha = 1.0; tween(indicator, { alpha: 0.0 }, { duration: GameConstants.STEP_INTERVAL - 40, easing: tween.linear }); } }; this.hideOldStep = function (step) { if (this.stepIndicators[step]) { this.stepIndicators[step].alpha = 0.0; } }; } /** * GridService * Responsibility: To create, manage the state of, and update the visuals of the beat grid. */ function GridService(game) { this.game = game; this.grid = []; // 2D array of BeatCell instances this.initialize = function () { // Create visual background grid for (var r = 0; r < GameConstants.TOTAL_ROWS; r++) { for (var c = 0; c < GameConstants.GRID_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); } } // Create interactive beat grid for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { this.grid[r] = []; for (var c = 0; c < GameConstants.GRID_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 + (GameConstants.TOTAL_ROWS - GameConstants.BEAT_ROWS + r) * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; this.game.addChild(cell); this.grid[r][c] = cell; } } }; this.getCellAt = function (x, y) { for (var r = 0; r < GameConstants.BEAT_ROWS; r++) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { var cell = this.grid[r][c]; var cellBounds = cell.getBounds(); if (x >= cellBounds.x && x <= cellBounds.x + cellBounds.width && y >= cellBounds.y && y <= cellBounds.y + cellBounds.height) { return cell; } } } return null; }; this.toggleCell = function (cell, mode) { var needsChange = mode === 'add' && !cell.active || mode === 'remove' && cell.active; if (needsChange) { cell.toggle(); this.updateCellVisual(cell); } }; this.updateCellVisual = function (cell) { cell.gfx.destroy(); var assetId = 'cell'; if (cell.active) { if (cell.row === 0) assetId = 'cellActive';else if (cell.row === 1) assetId = 'cellLeft';else if (cell.row === 2) assetId = 'cellRight'; } cell.gfx = cell.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); }; this.getActiveActionsForStep = function (step) { return { jump: this.grid[0][step].active, left: this.grid[1][step].active, right: this.grid[2][step].active }; }; } /** * 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(); this.hero.x = GameConstants.HERO_START_X; this.hero.y = GameConstants.HERO_START_Y; this.hero.currentCol = GameConstants.HERO_START_COL; this.game.addChild(this.hero); }; 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; if (actions.left && !actions.right) horizontalMove = -1; if (actions.right && !actions.left) horizontalMove = 1; if (actions.jump) { if (horizontalMove !== 0) { this.move(horizontalMove); // Move first } this.standAndJump(); // Then jump didAction = true; } else if (horizontalMove !== 0) { this.move(horizontalMove); didAction = true; } if (didAction) { 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(); }; this.standAndJump = function () { var jumpY = GameConstants.GRID_TOP + GameConstants.JUMP_ROW * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; this.hero.jump(jumpY, GameConstants.HERO_START_Y); AudioService.playJump(); }; } /** * CoinService * Responsibility: To manage the entire coin system: the data grid, spawning, * movement, collision detection, and score. */ function CoinService(game, uiService) { this.game = game; this.uiService = uiService; this.coinGrid = []; this.score = 0; this.beatCounter = 0; this.initialize = function () { this.score = 0; this.beatCounter = 0; for (var r = 0; r < GameConstants.TOTAL_ROWS; r++) { this.coinGrid[r] = []; for (var c = 0; c < GameConstants.GRID_COLS; c++) { this.coinGrid[r][c] = null; } } }; this.updateOnStep = function (hero) { this.moveCoins(); this.checkCollision(hero); this.spawnNewCoins(); }; this.moveCoins = function () { for (var r = GameConstants.TOTAL_ROWS - 1; r >= 0; r--) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { var coin = this.coinGrid[r][c]; if (coin) { var nextRow = r + 1; if (nextRow >= GameConstants.TOTAL_ROWS - GameConstants.BEAT_ROWS) { coin.destroy(); this.coinGrid[r][c] = null; } else { this.coinGrid[nextRow][c] = coin; this.coinGrid[r][c] = null; coin.gridRow = nextRow; coin.y = GameConstants.GRID_TOP + coin.gridRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2; } } } } }; this.checkCollision = function (hero) { for (var r = 0; r < GameConstants.TOTAL_ROWS; r++) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { var coin = this.coinGrid[r][c]; if (coin && coin.intersects(hero)) { this.collectCoin(coin, r, c); } } } }; this.collectCoin = function (coin, r, c) { coin.destroy(); this.coinGrid[r][c] = null; this.score++; this.uiService.updateScore(this.score); if (this.score >= 1000) { LK.showYouWin(); } }; this.spawnNewCoins = function () { this.beatCounter++; if (this.beatCounter >= GameConstants.COIN_SPAWN_BEATS) { this.beatCounter = 0; for (var r = 0; r < 2; r++) { for (var c = 0; c < GameConstants.GRID_COLS; c++) { if (Math.random() < GameConstants.COIN_SPAWN_CHANCE && !this.coinGrid[r][c]) { var coin = new Coin(); coin.gridRow = r; coin.gridCol = c; 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.coinGrid[r][c] = coin; } } } } }; } /** * 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, coinService, uiService) { this.gridService = gridService; this.playerService = playerService; this.coinService = coinService; this.uiService = uiService; this.currentStep = 0; this.lastStepTime = 0; this.playing = true; 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; this.uiService.hideOldStep(this.currentStep); this.currentStep = (this.currentStep + 1) % GameConstants.GRID_COLS; this.uiService.showStep(this.currentStep); this.doStepActions(); } }; this.doStepActions = function () { var actions = this.gridService.getActiveActionsForStep(this.currentStep); this.playerService.performActions(actions); var hero = this.playerService.getHeroInstance(); this.coinService.updateOnStep(hero); }; } var uiService = new UIService(game); var gridService = new GridService(game); var playerService = new PlayerService(game); var coinService = new CoinService(game, uiService); var inputService = new InputService(gridService, playerService); var sequencerService = new SequencerService(gridService, playerService, coinService, uiService); // 3. Initialize Services in correct order uiService.initialize(); gridService.initialize(); playerService.initialize(); coinService.initialize(); sequencerService.initialize(); // 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
@@ -5,88 +5,68 @@
/****
* Classes
****/
-// BeatCell: A single cell in the sequencer grid
-var BeatCell = Container.expand(function () {
+/*************************
+* CORE GAME OBJECTS
+*************************/
+/**
+* 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);
- // State: is this cell active?
self.active = false;
- // Store which row this cell is in (set externally after creation)
- self.row = 0;
- // Attach inactive asset by default
+ self.row = row; // Know its row for external logic to interpret
self.gfx = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
- // For toggling
self.toggle = function () {
self.active = !self.active;
- self.gfx.destroy();
- if (self.active) {
- // Use different asset for each row (new order: 0=jump, 1=left, 2=right)
- if (self.row === 0) {
- self.gfx = self.attachAsset('cellActive', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- } else if (self.row === 1) {
- self.gfx = self.attachAsset('cellLeft', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- } else if (self.row === 2) {
- self.gfx = self.attachAsset('cellRight', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- }
- } else {
- self.gfx = self.attachAsset('cell', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- }
};
- // Set state directly
self.setActive = function (val) {
- if (self.active !== val) self.toggle();
+ if (self.active !== val) {
+ self.toggle();
+ }
};
- // Touch/click event
- self.down = function (x, y, obj) {
- self.toggle();
- };
+ // The `down` event is removed and is now handled by the central InputService.
return self;
});
-// Coin: Individual coin that travels downward
+/**
+* Coin
+* Responsibility: To model a single coin object. (Largely unchanged).
+*/
var Coin = Container.expand(function () {
var self = Container.call(this);
self.gfx = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5
});
- // Grid position
self.gridRow = 0;
self.gridCol = 0;
return self;
});
-// Hero: The main character that reacts to the beat
+/**
+* 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
});
- // Jump indicator (hidden by default)
self.jumpInd = self.attachAsset('jump', {
anchorX: 0.5,
anchorY: 1
});
self.jumpInd.y = self.gfx.height / 2 + 10;
self.jumpInd.alpha = 0;
- // State
self.isJumping = false;
- // Move to (x, y) with tween
+ self.currentCol = 0;
self.moveTo = function (nx, ny, duration) {
tween(self, {
x: nx,
y: ny
@@ -94,25 +74,20 @@
duration: duration || 180,
easing: tween.cubicOut
});
};
- // Jump animation
- self.jump = function () {
+ self.jump = function (jumpY, originY) {
if (self.isJumping) return;
self.isJumping = true;
- // Show jump indicator
self.jumpInd.alpha = 1;
- // Animate up to jump row and back
- var origY = self.y;
- var jumpY = GRID_TOP + JUMP_ROW * CELL_SIZE + CELL_SIZE / 2;
tween(self, {
y: jumpY
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
- y: origY
+ y: originY
}, {
duration: 180,
easing: tween.bounceIn,
onFinish: function onFinish() {
@@ -121,339 +96,479 @@
}
});
}
});
- LK.getSound('jumpSnd').play();
};
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
****/
-// Music loop (background, optional)
-// Sound for move
-// Sound for jump
-// Sound for beat
-// Jump indicator
-// Character
-// Beat grid cell (active)
-// Beat grid cell (inactive)
-// --- Sequencer Grid Setup ---
-// --- Sequencer Grid Setup ---
-// Fit grid to full width (leaving 40px margin on each side)
-var GRID_COLS = 8; // Steps per bar
-var TOTAL_ROWS = 11; // 0-2: beat blocks, 3: character move, 4: character jump, 5-10: coin area
-var BEAT_ROWS = 3; // Beat blocks only
-var CHAR_ROW = 7; // Character movement row (row 4 from bottom)
-var JUMP_ROW = 6; // Character jump row (row 5 from bottom)
-var COIN_START_ROW = 5; // First row where coins can spawn (above jump row)
-var GRID_MARGIN_X = 40;
-var CELL_SIZE = Math.floor((2048 - 2 * GRID_MARGIN_X) / GRID_COLS);
-var GRID_HEIGHT = TOTAL_ROWS * CELL_SIZE;
-var GRID_TOP = 2732 - GRID_HEIGHT - 80; // 80px margin from bottom
-var GRID_LEFT = GRID_MARGIN_X;
-// Create background grid for visualization
-var bgGrid = [];
-for (var r = 0; r < TOTAL_ROWS; r++) {
- bgGrid[r] = [];
- for (var c = 0; c < GRID_COLS; c++) {
- var bgCell = LK.getAsset('emptycell', {
- anchorX: 0.5,
- anchorY: 0.5,
- alpha: 0.3
+/**
+* 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
+var GameConstants = {
+ GRID_COLS: 8,
+ TOTAL_ROWS: 11,
+ BEAT_ROWS: 3,
+ CHAR_ROW: 7,
+ JUMP_ROW: 6,
+ 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 - 80;
+ },
+ 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();
+ },
+ playJump: function playJump() {
+ LK.getSound('jumpSnd').play();
+ },
+ playMove: function playMove() {
+ LK.getSound('moveSnd').play();
+ },
+ startMusic: function startMusic() {
+ LK.playMusic('bgmusic', {
+ fade: {
+ start: 0,
+ end: 0.3,
+ duration: 1200
+ }
});
- bgCell.x = GRID_LEFT + c * CELL_SIZE + CELL_SIZE / 2;
- bgCell.y = GRID_TOP + r * CELL_SIZE + CELL_SIZE / 2;
- game.addChild(bgCell);
- bgGrid[r][c] = bgCell;
}
+};
+/**
+* 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.stepIndicators = [];
+ 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);
+ // Step Indicators
+ for (var c = 0; c < GameConstants.GRID_COLS; c++) {
+ var ind = LK.getAsset('cellActive', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ scaleX: 0.3,
+ scaleY: 0.3,
+ alpha: 0.0
+ });
+ ind.x = GameConstants.GRID_LEFT + c * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2;
+ ind.y = GameConstants.GRID_TOP + GameConstants.TOTAL_ROWS * GameConstants.CELL_SIZE + 40;
+ this.game.addChild(ind);
+ this.stepIndicators.push(ind);
+ }
+ };
+ this.updateScore = function (newScore) {
+ this.scoreTxt.setText(newScore);
+ };
+ this.showStep = function (step) {
+ if (this.stepIndicators[step]) {
+ var indicator = this.stepIndicators[step];
+ indicator.alpha = 1.0;
+ tween(indicator, {
+ alpha: 0.0
+ }, {
+ duration: GameConstants.STEP_INTERVAL - 40,
+ easing: tween.linear
+ });
+ }
+ };
+ this.hideOldStep = function (step) {
+ if (this.stepIndicators[step]) {
+ this.stepIndicators[step].alpha = 0.0;
+ }
+ };
}
-// Store grid cells (only for beat rows)
-var grid = [];
-// New row order: 0=jump, 1=left, 2=right
-for (var r = 0; r < BEAT_ROWS; r++) {
- grid[r] = [];
- for (var c = 0; c < GRID_COLS; c++) {
- var cell = new BeatCell();
- // Assign row meaning: 0=jump, 1=left, 2=right
- cell.row = r;
- cell.x = GRID_LEFT + c * CELL_SIZE + CELL_SIZE / 2;
- cell.y = GRID_TOP + (TOTAL_ROWS - BEAT_ROWS + r) * CELL_SIZE + CELL_SIZE / 2;
- game.addChild(cell);
- grid[r][c] = cell;
- }
-}
-// --- Hero Setup ---
-var hero = new Hero();
-var HERO_START_X = GRID_LEFT + Math.floor(GRID_COLS / 2) * CELL_SIZE + CELL_SIZE / 2;
-var HERO_START_Y = GRID_TOP + CHAR_ROW * CELL_SIZE + CELL_SIZE / 2; // Position in CHAR_ROW
-hero.x = HERO_START_X;
-hero.y = HERO_START_Y;
-hero.currentCol = Math.floor(GRID_COLS / 2); // Track which column hero is in
-game.addChild(hero);
-// --- Sequencer State ---
-var currentStep = 0;
-var stepInterval = 400; // ms per step (150bpm)
-var lastStepTime = Date.now();
-var playing = true;
-// --- Score & UI ---
-var score = 0;
-var scoreTxt = new Text2('0', {
- size: 100,
- fill: "#fff"
-});
-scoreTxt.anchor.set(0.5, 0);
-LK.gui.top.addChild(scoreTxt);
-// Step indicator (shows which column is active)
-var stepIndicators = [];
-for (var c = 0; c < GRID_COLS; c++) {
- var ind = LK.getAsset('cellActive', {
- anchorX: 0.5,
- anchorY: 0.5,
- scaleX: 0.3,
- scaleY: 0.3,
- alpha: 0.0
- });
- ind.x = GRID_LEFT + c * CELL_SIZE + CELL_SIZE / 2;
- ind.y = GRID_TOP + TOTAL_ROWS * CELL_SIZE + 40; // Just below the full grid
- game.addChild(ind);
- stepIndicators.push(ind);
-}
-// --- Game Board Boundaries ---
-var HERO_MIN_X = GRID_LEFT + CELL_SIZE / 2;
-var HERO_MAX_X = GRID_LEFT + (GRID_COLS - 1) * CELL_SIZE + CELL_SIZE / 2;
-// --- Coin System ---
-var coinGrid = []; // Track coins in the grid
-for (var r = 0; r < TOTAL_ROWS; r++) {
- coinGrid[r] = [];
- for (var c = 0; c < GRID_COLS; c++) {
- coinGrid[r][c] = null;
- }
-}
-var beatCounter = 0;
-// --- Touch: Drag to move hero horizontally (optional, for fun) ---
-var dragHero = false;
-// --- Touch: Drag painting on beat grid ---
-var isPainting = false;
-var paintMode = null; // 'add' or 'remove'
-var lastPaintedCell = null;
-game.down = function (x, y, obj) {
- // Check if touch is on a beat grid cell
- var cellFound = false;
- for (var r = 0; r < BEAT_ROWS; r++) {
- for (var c = 0; c < GRID_COLS; c++) {
- var cell = grid[r][c];
- var cellBounds = cell.getBounds();
- if (x >= cellBounds.x && x <= cellBounds.x + cellBounds.width && y >= cellBounds.y && y <= cellBounds.y + cellBounds.height) {
- // Start painting mode
- isPainting = true;
- paintMode = cell.active ? 'remove' : 'add';
- lastPaintedCell = cell;
- // Apply paint action to this cell
- if (paintMode === 'add') {
- cell.setActive(true);
- } else {
- cell.setActive(false);
- }
- cellFound = true;
- break;
+/**
+* GridService
+* Responsibility: To create, manage the state of, and update the visuals of the beat grid.
+*/
+function GridService(game) {
+ this.game = game;
+ this.grid = []; // 2D array of BeatCell instances
+ this.initialize = function () {
+ // Create visual background grid
+ for (var r = 0; r < GameConstants.TOTAL_ROWS; r++) {
+ for (var c = 0; c < GameConstants.GRID_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);
}
}
- if (cellFound) break;
- }
- // If not on a cell, check if touch is near hero for dragging
- if (!cellFound) {
- var dx = x - hero.x,
- dy = y - hero.y;
- if (dx * dx + dy * dy < 200 * 200) {
- dragHero = true;
+ // Create interactive beat grid
+ for (var r = 0; r < GameConstants.BEAT_ROWS; r++) {
+ this.grid[r] = [];
+ for (var c = 0; c < GameConstants.GRID_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 + (GameConstants.TOTAL_ROWS - GameConstants.BEAT_ROWS + r) * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2;
+ this.game.addChild(cell);
+ this.grid[r][c] = cell;
+ }
}
- }
-};
-game.up = function (x, y, obj) {
- dragHero = false;
- isPainting = false;
- paintMode = null;
- lastPaintedCell = null;
-};
-game.move = function (x, y, obj) {
- if (isPainting) {
- // Check which cell we're over
- for (var r = 0; r < BEAT_ROWS; r++) {
- for (var c = 0; c < GRID_COLS; c++) {
- var cell = grid[r][c];
+ };
+ this.getCellAt = function (x, y) {
+ for (var r = 0; r < GameConstants.BEAT_ROWS; r++) {
+ for (var c = 0; c < GameConstants.GRID_COLS; c++) {
+ var cell = this.grid[r][c];
var cellBounds = cell.getBounds();
if (x >= cellBounds.x && x <= cellBounds.x + cellBounds.width && y >= cellBounds.y && y <= cellBounds.y + cellBounds.height) {
- // Only paint if we moved to a different cell
- if (cell !== lastPaintedCell) {
- lastPaintedCell = cell;
- // Apply paint action based on mode
- if (paintMode === 'add') {
- cell.setActive(true);
- } else if (paintMode === 'remove') {
- cell.setActive(false);
- }
- }
- break;
+ return cell;
}
}
}
- } else if (dragHero) {
- // Clamp to board
- var nx = Math.max(HERO_MIN_X, Math.min(HERO_MAX_X, x));
- hero.x = nx;
- }
-};
-// --- Main Sequencer Logic ---
-function doStepActions(stepIdx) {
- // For each row, if cell is active, trigger action
- var didAction = false;
- // Determine which actions are active for this step (new row order: 0=jump, 1=left, 2=right)
- var jumpActive = grid[0][stepIdx].active;
- var leftActive = grid[1][stepIdx].active;
- var rightActive = grid[2][stepIdx].active;
- // If both left and right are active, they cancel out (no horizontal movement)
- var horizontalMove = 0;
- if (leftActive && !rightActive) {
- horizontalMove = -1;
- }
- if (rightActive && !leftActive) {
- horizontalMove = 1;
- }
- // If both left and right are active, horizontalMove remains 0
- // If jump is active
- if (jumpActive) {
- // If both left and right are active, only perform standstill jump
- if (horizontalMove === 0) {
- hero.jump();
- didAction = true;
- } else {
- // Blend jump with movement: jump and move in the direction
- // Move first, then jump (for visual effect)
- var nx = hero.x + horizontalMove * CELL_SIZE;
- // Teleport if out of bounds
- if (nx < HERO_MIN_X) nx = HERO_MAX_X;
- if (nx > HERO_MAX_X) nx = HERO_MIN_X;
- if (nx !== hero.x) {
- hero.moveTo(nx, hero.y, 180);
- LK.getSound('moveSnd').play();
+ return null;
+ };
+ this.toggleCell = function (cell, mode) {
+ var needsChange = mode === 'add' && !cell.active || mode === 'remove' && cell.active;
+ if (needsChange) {
+ cell.toggle();
+ this.updateCellVisual(cell);
+ }
+ };
+ this.updateCellVisual = function (cell) {
+ cell.gfx.destroy();
+ var assetId = 'cell';
+ if (cell.active) {
+ if (cell.row === 0) assetId = 'cellActive';else if (cell.row === 1) assetId = 'cellLeft';else if (cell.row === 2) assetId = 'cellRight';
+ }
+ cell.gfx = cell.attachAsset(assetId, {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ };
+ this.getActiveActionsForStep = function (step) {
+ return {
+ jump: this.grid[0][step].active,
+ left: this.grid[1][step].active,
+ right: this.grid[2][step].active
+ };
+ };
+}
+/**
+* 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();
+ this.hero.x = GameConstants.HERO_START_X;
+ this.hero.y = GameConstants.HERO_START_Y;
+ this.hero.currentCol = GameConstants.HERO_START_COL;
+ this.game.addChild(this.hero);
+ };
+ 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;
+ if (actions.left && !actions.right) horizontalMove = -1;
+ if (actions.right && !actions.left) horizontalMove = 1;
+ if (actions.jump) {
+ if (horizontalMove !== 0) {
+ this.move(horizontalMove); // Move first
}
- hero.jump();
+ this.standAndJump(); // Then jump
didAction = true;
+ } else if (horizontalMove !== 0) {
+ this.move(horizontalMove);
+ didAction = true;
}
- } else {
- // No jump, just move if left or right (but not both)
- if (horizontalMove !== 0) {
- var newCol = hero.currentCol + horizontalMove;
- // Wrap around
- if (newCol < 0) newCol = GRID_COLS - 1;
- if (newCol >= GRID_COLS) newCol = 0;
- hero.currentCol = newCol;
- var nx = GRID_LEFT + newCol * CELL_SIZE + CELL_SIZE / 2;
- if (nx !== hero.x) {
- hero.moveTo(nx, hero.y, 180);
- LK.getSound('moveSnd').play();
- didAction = true;
+ if (didAction) {
+ 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();
+ };
+ this.standAndJump = function () {
+ var jumpY = GameConstants.GRID_TOP + GameConstants.JUMP_ROW * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2;
+ this.hero.jump(jumpY, GameConstants.HERO_START_Y);
+ AudioService.playJump();
+ };
+}
+/**
+* CoinService
+* Responsibility: To manage the entire coin system: the data grid, spawning,
+* movement, collision detection, and score.
+*/
+function CoinService(game, uiService) {
+ this.game = game;
+ this.uiService = uiService;
+ this.coinGrid = [];
+ this.score = 0;
+ this.beatCounter = 0;
+ this.initialize = function () {
+ this.score = 0;
+ this.beatCounter = 0;
+ for (var r = 0; r < GameConstants.TOTAL_ROWS; r++) {
+ this.coinGrid[r] = [];
+ for (var c = 0; c < GameConstants.GRID_COLS; c++) {
+ this.coinGrid[r][c] = null;
}
}
- }
- // Play beat sound if any action
- if (didAction) {
- LK.getSound('beat').play();
- }
- // Move all coins down one row on each beat
- for (var r = TOTAL_ROWS - 1; r >= 0; r--) {
- for (var c = 0; c < GRID_COLS; c++) {
- if (coinGrid[r][c]) {
- var coin = coinGrid[r][c];
- // Check if coin will hit the beat grid (first beat row)
- if (r + 1 >= TOTAL_ROWS - BEAT_ROWS) {
- // Remove coin when it hits beat grid
- coin.destroy();
- coinGrid[r][c] = null;
- } else {
- // Check if coin and hero are touching (bounding box intersection, not grid/cell match)
- var intersects = coin.intersects(hero);
- if (intersects) {
- // Collect coin
- score += 1;
- scoreTxt.setText(score);
- if (score >= 1000) {
- LK.showYouWin();
- }
- // Remove collected coin
+ };
+ this.updateOnStep = function (hero) {
+ this.moveCoins();
+ this.checkCollision(hero);
+ this.spawnNewCoins();
+ };
+ this.moveCoins = function () {
+ for (var r = GameConstants.TOTAL_ROWS - 1; r >= 0; r--) {
+ for (var c = 0; c < GameConstants.GRID_COLS; c++) {
+ var coin = this.coinGrid[r][c];
+ if (coin) {
+ var nextRow = r + 1;
+ if (nextRow >= GameConstants.TOTAL_ROWS - GameConstants.BEAT_ROWS) {
coin.destroy();
- coinGrid[r][c] = null;
+ this.coinGrid[r][c] = null;
} else {
- // Move coin down
- coinGrid[r + 1][c] = coin;
- coinGrid[r][c] = null;
- coin.gridRow = r + 1;
- coin.y = GRID_TOP + coin.gridRow * CELL_SIZE + CELL_SIZE / 2;
+ this.coinGrid[nextRow][c] = coin;
+ this.coinGrid[r][c] = null;
+ coin.gridRow = nextRow;
+ coin.y = GameConstants.GRID_TOP + coin.gridRow * GameConstants.CELL_SIZE + GameConstants.CELL_SIZE / 2;
}
}
}
}
- }
- // Increment beat counter and spawn new coins every 8 beats
- beatCounter++;
- if (beatCounter >= 8) {
- beatCounter = 0;
- // Generate new coin pattern at top (2 rows)
- for (var r = 0; r < 2; r++) {
- for (var c = 0; c < GRID_COLS; c++) {
- if (Math.random() < 0.25 && !coinGrid[r][c]) {
- var coin = new Coin();
- coin.gridRow = r;
- coin.gridCol = c;
- coin.x = GRID_LEFT + c * CELL_SIZE + CELL_SIZE / 2;
- coin.y = GRID_TOP + r * CELL_SIZE + CELL_SIZE / 2;
- game.addChild(coin);
- coinGrid[r][c] = coin;
+ };
+ this.checkCollision = function (hero) {
+ for (var r = 0; r < GameConstants.TOTAL_ROWS; r++) {
+ for (var c = 0; c < GameConstants.GRID_COLS; c++) {
+ var coin = this.coinGrid[r][c];
+ if (coin && coin.intersects(hero)) {
+ this.collectCoin(coin, r, c);
}
}
}
- }
+ };
+ this.collectCoin = function (coin, r, c) {
+ coin.destroy();
+ this.coinGrid[r][c] = null;
+ this.score++;
+ this.uiService.updateScore(this.score);
+ if (this.score >= 1000) {
+ LK.showYouWin();
+ }
+ };
+ this.spawnNewCoins = function () {
+ this.beatCounter++;
+ if (this.beatCounter >= GameConstants.COIN_SPAWN_BEATS) {
+ this.beatCounter = 0;
+ for (var r = 0; r < 2; r++) {
+ for (var c = 0; c < GameConstants.GRID_COLS; c++) {
+ if (Math.random() < GameConstants.COIN_SPAWN_CHANCE && !this.coinGrid[r][c]) {
+ var coin = new Coin();
+ coin.gridRow = r;
+ coin.gridCol = c;
+ 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.coinGrid[r][c] = coin;
+ }
+ }
+ }
+ }
+ };
}
-// --- Game Update Loop ---
+/**
+* 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, coinService, uiService) {
+ this.gridService = gridService;
+ this.playerService = playerService;
+ this.coinService = coinService;
+ this.uiService = uiService;
+ this.currentStep = 0;
+ this.lastStepTime = 0;
+ this.playing = true;
+ 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;
+ this.uiService.hideOldStep(this.currentStep);
+ this.currentStep = (this.currentStep + 1) % GameConstants.GRID_COLS;
+ this.uiService.showStep(this.currentStep);
+ this.doStepActions();
+ }
+ };
+ this.doStepActions = function () {
+ var actions = this.gridService.getActiveActionsForStep(this.currentStep);
+ this.playerService.performActions(actions);
+ var hero = this.playerService.getHeroInstance();
+ this.coinService.updateOnStep(hero);
+ };
+}
+var uiService = new UIService(game);
+var gridService = new GridService(game);
+var playerService = new PlayerService(game);
+var coinService = new CoinService(game, uiService);
+var inputService = new InputService(gridService, playerService);
+var sequencerService = new SequencerService(gridService, playerService, coinService, uiService);
+// 3. Initialize Services in correct order
+uiService.initialize();
+gridService.initialize();
+playerService.initialize();
+coinService.initialize();
+sequencerService.initialize();
+// 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 () {
- // Step sequencer timing
- var now = Date.now();
- if (playing && now - lastStepTime >= stepInterval) {
- // Hide previous step indicator
- stepIndicators[currentStep].alpha = 0.0;
- // Advance step
- currentStep = (currentStep + 1) % GRID_COLS;
- // Show current step indicator
- stepIndicators[currentStep].alpha = 1.0;
- // Animate indicator
- tween(stepIndicators[currentStep], {
- alpha: 0.0
- }, {
- duration: stepInterval - 40,
- easing: tween.linear
- });
- // Trigger actions
- doStepActions(currentStep);
- lastStepTime = now;
- }
+ sequencerService.update();
};
-// --- Music (optional) ---
-LK.playMusic('bgmusic', {
- fade: {
- start: 0,
- end: 0.3,
- duration: 1200
- }
-});
-// (Instructions/tutorial text removed)
-// --- Game Over: Reset state automatically handled by LK ---
-// --- Prevent UI in top-left 100x100 px ---
-/* No elements placed in this area */
\ No newline at end of file
+// 6. Start background music
+AudioService.startMusic();
\ 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