User prompt
But that time I can't grab and drag figure. Pls give that function back. And pls fix that bug "some figures stuck on the way and can't get to the ground" ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Super. Pls pls slow the speed a little bit. And increase a little bit sensitivity of screen that allow to grab a figure. But it's not bad yet. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Delete cursor asset but enhance the sensitivity of the screen that when player puts finger on figure that means player can control and drag that figure immediately. And add different shapes ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Cursor & Drag System (English for Developer) 1. Cursor (Pointer) System Display a visible arrow/cursor on the screen. The player can hover the cursor over a shape and then click-drag to move it. 2. Precise Drag Targeting Only the shape directly under the cursor should be draggable. Ensure smooth transition between selecting one shape and another. 3. Natural Falling All shapes should continue falling until they touch the ground or another block. Gravity should handle the fall completely, without interruption. 4. Drag Behavior Once the player releases a shape, they should be able to immediately drag another one without delay. Dragging should feel responsive and fluid, with no input lag or blocking ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
🔧 Updated Fixes & Rules (Technical English) 1. Touch-Based Movement Only A shape should only move if the user is directly touching and dragging it. No shape should move indirectly (e.g., when another shape is touched). 2. Collision Rule Shapes must not pass through each other. Implement solid collision detection to prevent overlap during movement and stacking. 3. Bomb Shape Design The bomb block must visually resemble a bomb (e.g., circular with a fuse or explosion symbol). It should be clearly different from regular blocks in shape and appearance. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Grid & Match Rules (For Developer) 1. Grid Size The game grid is 24 columns (left to right) and 24 rows (top to bottom). 2. Same-Color Match Rule If 4 or more blocks of the same color align horizontally, they should explode and give points. 3. Two-Color Match Rule If 8 or more blocks are aligned horizontally but consist of exactly two colors, they should also explode. 4. Three-Color Full Line Rule If a full row (24 blocks) contains three or more different colors, the entire row should explode and give bonus points. --- 🛠 Bug Fix Command (Touch Control Issue) Fix Touch Targeting: When dragging a shape, only the selected shape should move. Prevent cases where dragging one figure moves a different one. Ensure correct input mapping between touch position and selected object. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Please fix the bug: 'TypeError: setTimeout is not a function' in or related to this line: 'setTimeout(function () {' Line Number: 391
User prompt
🔧 Additional Gameplay Instructions (2D) 1. Continuous Side Movement On touch/drag, the shape moves continuously left or right across the screen, not just one cell per tap. 2. Increase Drag Speed Make the swipe/drag movement faster and smoother. 3. Random Spawn Positions Shapes should spawn not only from the top center, but also randomly from different horizontal positions at the top. --- 🧩 2D Tetris Shapes (Reference) I-shape: #### O-shape: ## ## T-shape: ### # L-shape: # # ## J-shape: # # ## S-shape: ## ## Z-shape: ## ## ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Please fix the bug: 'stability is not defined' in or related to this line: 'stabilityText.setText('Stability: ' + Math.round(stability) + '%');' Line Number: 248
User prompt
🧱 Core Mechanics (Short Version) 1. Shape Types: Not only squares – use Tetris-style shapes (L, T, Z, I, etc.). 2. Stacking: Shapes fall from the top and stack when they land on others or the ground. 3. No Mid-Fall Control: Once falling, shapes can't be moved or rotated. 4. Row Clearing: Full horizontal rows disappear and give points. 5. Speed Variations: Some shapes fall faster for challenge. 6. Special Blocks: 🎁 Gift = Bonus points 💣 Bomb = Destroys nearby blocks 🔄 Wildcard = Fills nearest gap ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
But I think you can add that function that allows player to grab and drag figures. And increase different figures. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Pls add that skills that . Player can move figures with finger tap and add more complex figures. And then when blocks and figures fall down they don't disappear. A Player with his hands can move, flip to right, left, forward and backward and etc.
Code edit (1 edits merged)
Please save this source code
User prompt
ShapeStack 3D
Initial prompt
🧱 Game Concept: ShapeStack 3D Genre: 3D Physics-Based Construction Game --- 🌟 Overview: ShapeStack 3D is a creative 3D construction game where geometric figures (blocks) fall slowly from the sky. The player must catch and place them to build larger structures. The more symmetrical, balanced, and space-efficient the structure is, the more stable it becomes — unlocking the ability to build higher, stronger, and more complex objects. --- 🧩 Core Gameplay: Falling Shapes: Random 3D Lego-like shapes (cubes, pyramids, L-blocks, etc.) slowly fall from the sky. Catch and Place: The player collects the falling shapes before they hit the ground. Build Freely: The player can stack shapes on top of each other to construct buildings, sculptures, machines, or any object. Stability Score: The game analyzes how well-aligned, symmetrical, and dense the structure is. More balanced structures = higher stability More stability = more build layers unlocked --- 🎯 Objective: Build the tallest and most stable structure possible. Unlock new building materials and tools. Earn bonus points for symmetry, creative use of space, and aesthetic design. --- 🧠 Strategy Mechanics: Gameplay Element Effect Symmetry Increases stability and score Gaps/Holes Reduces structural integrity Perfect Fit Bonus points and glowing effects Tilting/Unstable build Can cause collapse unless reinforced Advanced tools Unlock after score thresholds (e.g., glue, rotation tool) --- 🖥️ User Interface: Left Panel: Current Score and Stability Meter Right Panel: Inventory of caught shapes Bottom: Controls (Rotate, Move, Place) Top: Height indicator / Build progress --- ⚙️ Technical Notes (for development): Engine: Unity (preferred for 3D and physics) Language: C# Camera: Free orbit or third-person top-down view Physics: Realistic gravity and collision (Rigidbody, colliders) Build Grid: Optional snap-to-grid feature for clean construction Save System: Ability to save and reload structures --- 🔓 Bonus Features (optional): Sandbox mode: Unlimited shapes and no time limit Challenge mode: Build within specific time or block limits Multiplayer mode: Co-build or compete for stability Creative gallery: Share your best creations online
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var GridBlock = Container.expand(function (shapeType) { var self = Container.call(this); self.shapeType = shapeType; self.color = shapeColors[shapeType] || 0xffffff; self.gridX = 0; self.gridY = 0; var shapeGraphic = self.attachAsset(shapeType, { anchorX: 0.5, anchorY: 0.5 }); // Add bomb fuse for bomb blocks if (shapeType === 'bomb') { var fuseGraphic = self.attachAsset('bombFuse', { anchorX: 0.5, anchorY: 1, x: 0, y: -25 }); // Animate fuse flickering var _fuseFlicker = function fuseFlicker() { tween(fuseGraphic, { alpha: 0.3 }, { duration: 200, onFinish: function onFinish() { tween(fuseGraphic, { alpha: 1 }, { duration: 200, onFinish: _fuseFlicker }); } }); }; _fuseFlicker(); } return self; }); var PlacedShape = Container.expand(function (shapeType) { var self = Container.call(this); self.shapeType = shapeType; self.isStable = true; self.isDragging = false; var shapeGraphic = self.attachAsset(shapeType, { anchorX: 0.5, anchorY: 0.5 }); self.rotate = function () { // Smooth rotation animation tween(shapeGraphic, { rotation: shapeGraphic.rotation + Math.PI / 2 }, { duration: 300, easing: tween.easeInOut, onFinish: function onFinish() { calculateStability(); updateStability(); } }); }; self.down = function (x, y, obj) { // Only set as selected if no other shape is already being dragged if (!selectedShape || !selectedShape.isDragging) { self.isDragging = true; selectedShape = self; } }; self.up = function (x, y, obj) { self.isDragging = false; if (selectedShape === self) { selectedShape = null; } calculateStability(); updateStability(); }; return self; }); var TetrisBlock = Container.expand(function (shapeType) { var self = Container.call(this); self.shapeType = shapeType; self.fallSpeed = Math.random() < 0.3 ? 3.5 : 2.5; // Increased by ~66% for more dynamic gameplay self.isSpecial = specialBlocks.indexOf(shapeType) !== -1; self.gridX = Math.floor(Math.random() * (gridWidth - 3)); // Ensure space for shape self.gridY = 0; self.lastMoveTime = 0; self.targetX = 0; // For smooth movement self.isMoving = false; // Track if block is currently moving self.rotation = 0; // Track current rotation state (0, 1, 2, 3) self.blocks = []; // Array to hold individual block graphics self.hasLanded = false; // Track if shape has touched ground or another block // Rotation configurations for each shape self.rotationConfigs = { 'iblock': [[[-1, 0], [0, 0], [1, 0], [2, 0]], // horizontal [[0, -1], [0, 0], [0, 1], [0, 2]], // vertical [[-1, 0], [0, 0], [1, 0], [2, 0]], // horizontal [[0, -1], [0, 0], [0, 1], [0, 2]] // vertical ], 'oblock': [[[0, 0], [1, 0], [0, 1], [1, 1]], // same for all rotations [[0, 0], [1, 0], [0, 1], [1, 1]], [[0, 0], [1, 0], [0, 1], [1, 1]], [[0, 0], [1, 0], [0, 1], [1, 1]]], 'tblock': [[[-1, 0], [0, 0], [1, 0], [0, 1]], // T pointing down [[0, -1], [0, 0], [1, 0], [0, 1]], // T pointing left [[-1, 0], [0, 0], [1, 0], [0, -1]], // T pointing up [[0, -1], [-1, 0], [0, 0], [0, 1]] // T pointing right ], 'lblock': [[[-1, 0], [0, 0], [1, 0], [1, 1]], // L standard [[0, -1], [0, 0], [0, 1], [-1, 1]], // L rotated 90 [[-1, -1], [-1, 0], [0, 0], [1, 0]], // L rotated 180 [[0, -1], [0, 0], [0, 1], [1, -1]] // L rotated 270 ], 'jblock': [[[-1, 0], [0, 0], [1, 0], [-1, 1]], // J standard [[0, -1], [0, 0], [0, 1], [1, 1]], // J rotated 90 [[-1, 0], [0, 0], [1, 0], [1, -1]], // J rotated 180 [[0, -1], [-1, -1], [0, 0], [0, 1]] // J rotated 270 ], 'sblock': [[[-1, 1], [0, 1], [0, 0], [1, 0]], // S horizontal [[0, -1], [0, 0], [1, 0], [1, 1]], // S vertical [[-1, 1], [0, 1], [0, 0], [1, 0]], // S horizontal [[0, -1], [0, 0], [1, 0], [1, 1]] // S vertical ], 'zblock': [[[-1, 0], [0, 0], [0, 1], [1, 1]], // Z horizontal [[1, -1], [1, 0], [0, 0], [0, 1]], // Z vertical [[-1, 0], [0, 0], [0, 1], [1, 1]], // Z horizontal [[1, -1], [1, 0], [0, 0], [0, 1]] // Z vertical ] }; // Define classic Tetris shape configurations with proper proportions var shapeConfigs = { 'iblock': [[-1, 0], [0, 0], [1, 0], [2, 0] // I-piece: centered horizontal line ], 'oblock': [[0, 0], [1, 0], [0, 1], [1, 1] // O-piece: 2x2 square ], 'tblock': [[-1, 0], [0, 0], [1, 0], [0, 1] // T-piece: centered with stem down ], 'lblock': [[-1, 0], [0, 0], [1, 0], [1, 1] // L-piece: centered with corner ], 'jblock': [[-1, 0], [0, 0], [1, 0], [-1, 1] // J-piece: centered with corner ], 'sblock': [[-1, 1], [0, 1], [0, 0], [1, 0] // S-piece: classic S shape ], 'zblock': [[-1, 0], [0, 0], [0, 1], [1, 1] // Z-piece: classic Z shape ] }; // Get shape configuration or default to single block var config = shapeConfigs[shapeType] || [[0, 0]]; // Store current shape configuration after config is defined self.currentConfig = self.rotationConfigs[shapeType] && self.rotationConfigs[shapeType][0] ? self.rotationConfigs[shapeType][0] : config; var shapeColor = shapeColors[shapeType] || 0x4a90e2; // Create a single group container for all blocks self.blockGroup = new Container(); self.addChild(self.blockGroup); // Create individual blocks within the group for (var i = 0; i < config.length; i++) { var blockGraphic = self.blockGroup.attachAsset(shapeType, { anchorX: 0.5, anchorY: 0.5 }); // Position block relative to group center blockGraphic.x = config[i][0] * blockSize; blockGraphic.y = config[i][1] * blockSize; // Add subtle outline for better visibility blockGraphic.alpha = 0.9; // Completely disable interactivity on individual blocks to prevent structural changes blockGraphic.interactive = false; blockGraphic.buttonMode = false; // Prevent any touch events from reaching individual blocks blockGraphic.down = null; blockGraphic.up = null; blockGraphic.move = null; self.blocks.push({ graphic: blockGraphic, offsetX: config[i][0], offsetY: config[i][1] }); } // Calculate proper pivot point based on shape bounds for centered rotation var minX = 0, maxX = 0, minY = 0, maxY = 0; for (var i = 0; i < config.length; i++) { minX = Math.min(minX, config[i][0]); maxX = Math.max(maxX, config[i][0]); minY = Math.min(minY, config[i][1]); maxY = Math.max(maxY, config[i][1]); } var centerX = (minX + maxX) * blockSize / 2; var centerY = (minY + maxY) * blockSize / 2; self.blockGroup.pivot.set(centerX, centerY); // Add bomb fuse for bomb blocks if (shapeType === 'bomb') { var fuseGraphic = self.attachAsset('bombFuse', { anchorX: 0.5, anchorY: 1, x: 0, y: -25 }); // Animate fuse flickering var _fuseFlicker2 = function fuseFlicker() { tween(fuseGraphic, { alpha: 0.3 }, { duration: 200, onFinish: function onFinish() { tween(fuseGraphic, { alpha: 1 }, { duration: 200, onFinish: _fuseFlicker2 }); } }); }; _fuseFlicker2(); } // Position on grid self.x = self.gridX * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2; self.y = self.gridY * blockSize + blockSize / 2 + 200; self.targetX = self.x; // Initialize target position self.checkCollision = function (newGridX, newGridY, configToCheck) { // Use provided configuration or current configuration var config = configToCheck || self.currentConfig; // Check collision for each block in the configuration for (var i = 0; i < config.length; i++) { var blockX = newGridX + config[i][0]; var blockY = newGridY + config[i][1]; // Check bounds if (blockX < 0 || blockX >= gridWidth || blockY < 0 || blockY >= gridHeight) { return true; } // Check if position is occupied in grid if (blockY >= 0 && blockY < gridHeight && grid[blockY] && grid[blockY][blockX]) { return true; } // Check collision with other falling shapes for (var j = 0; j < fallingShapes.length; j++) { var otherShape = fallingShapes[j]; if (otherShape !== self) { for (var k = 0; k < otherShape.blocks.length; k++) { var otherBlock = otherShape.blocks[k]; var otherBlockX = otherShape.gridX + otherBlock.offsetX; var otherBlockY = otherShape.gridY + otherBlock.offsetY; if (otherBlockX === blockX && otherBlockY === blockY) { return true; } } } } } return false; }; self.moveToPosition = function (newGridX) { // Track that the shape has moved (for distinguishing tap from drag) if (Math.abs(newGridX - self.gridX) > 0) { self.hasMoved = true; } // Clamp to grid boundaries based on current shape configuration var minX = 0; var maxX = 0; for (var i = 0; i < self.currentConfig.length; i++) { var blockX = self.currentConfig[i][0]; minX = Math.min(minX, blockX); maxX = Math.max(maxX, blockX); } // Clamp position to keep all blocks within grid bounds newGridX = Math.max(-minX, Math.min(gridWidth - 1 - maxX, newGridX)); // Allow movement even if there might be collision - let player move freely while dragging self.gridX = newGridX; self.targetX = self.gridX * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2; self.isMoving = true; // Stop any existing movement tween tween.stop(self, { x: true }); // Instant movement for maximum responsiveness when dragging if (self.isDragging) { self.x = self.targetX; self.isMoving = false; } else { // Ultra-fast responsive movement for non-dragging cases tween(self, { x: self.targetX }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { self.isMoving = false; } }); } }; self.update = function () { var currentTime = LK.ticks; if (currentTime - self.lastMoveTime > 80 / self.fallSpeed) { self.lastMoveTime = currentTime; // Check if can move down using collision detection var canMoveDown = !self.checkCollision(self.gridX, self.gridY + 1); if (canMoveDown) { // Play light drop sound for step-down movement LK.getSound('drop').play(); self.gridY++; self.y = self.gridY * blockSize + blockSize / 2 + 200; } else { // Check if touching another shape (not ground) for contact sound var touchingShape = false; for (var j = 0; j < fallingShapes.length; j++) { var otherShape = fallingShapes[j]; if (otherShape !== self && self.checkCollision(self.gridX, self.gridY + 1)) { touchingShape = true; break; } } // Check if there are blocks below in grid var touchingGrid = false; for (var i = 0; i < self.currentConfig.length; i++) { var blockX = self.gridX + self.currentConfig[i][0]; var blockY = self.gridY + self.currentConfig[i][1] + 1; if (blockY < gridHeight && blockY >= 0 && grid[blockY] && grid[blockY][blockX]) { touchingGrid = true; break; } } // Play appropriate sound if (touchingShape || touchingGrid) { if (self.gridY + 1 >= gridHeight) { // Landing on ground LK.getSound('land').play(); } else { // Contact with another shape LK.getSound('contact').play(); } } // Mark as landed when it can't move down self.hasLanded = true; // Only place if we're in a valid position and can't move down if (self.gridY >= 0) { self.placeInGrid(); } else { // If stuck above grid, try to find a valid position var foundValidPosition = false; for (var testY = 0; testY < gridHeight; testY++) { if (!self.checkCollision(self.gridX, testY)) { self.gridY = testY; self.y = self.gridY * blockSize + blockSize / 2 + 200; foundValidPosition = true; break; } } if (!foundValidPosition) { // Stop any existing tweens before destroying tween.stop(self); tween.stop(self.blockGroup); for (var i = 0; i < self.blocks.length; i++) { tween.stop(self.blocks[i].graphic); } // Remove stuck shape var index = fallingShapes.indexOf(self); if (index > -1) { fallingShapes.splice(index, 1); } self.destroy(); } } } } }; self.placeInGrid = function () { var allBlocksValid = true; // Check if all blocks can be placed for (var i = 0; i < self.currentConfig.length; i++) { var blockX = self.gridX + self.currentConfig[i][0]; var blockY = self.gridY + self.currentConfig[i][1]; if (blockY < 0 || blockY >= gridHeight || blockX < 0 || blockX >= gridWidth) { allBlocksValid = false; break; } } if (allBlocksValid) { // Freeze the group before disassembly to maintain structural stability self.blockGroup.rotation = Math.round(self.blockGroup.rotation / (Math.PI / 2)) * (Math.PI / 2); // Place all blocks in grid - disassemble the group into static tiles for (var i = 0; i < self.currentConfig.length; i++) { var blockX = self.gridX + self.currentConfig[i][0]; var blockY = self.gridY + self.currentConfig[i][1]; grid[blockY][blockX] = self.shapeType; // Create visual block in grid (static tile) var placedBlock = new GridBlock(self.shapeType); placedBlock.gridX = blockX; placedBlock.gridY = blockY; placedBlock.x = blockX * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2; placedBlock.y = blockY * blockSize + blockSize / 2 + 200; // Apply the same rotation as the group had placedBlock.rotation = self.blockGroup.rotation; placedShapes.push(placedBlock); game.addChild(placedBlock); } LK.getSound('place').play(); score += 10 * self.blocks.length; updateScore(); checkForLines(); // Handle special blocks if (self.isSpecial) { handleSpecialBlock(self.shapeType, self.gridX, self.gridY); } } // Stop any existing tweens before destroying tween.stop(self); tween.stop(self.blockGroup); for (var i = 0; i < self.blocks.length; i++) { tween.stop(self.blocks[i].graphic); } // Remove from falling shapes var index = fallingShapes.indexOf(self); if (index > -1) { fallingShapes.splice(index, 1); } self.destroy(); // Check game over if (self.gridY <= 1) { LK.showGameOver(); } }; self.rotate = function () { // Only allow rotation if shape hasn't landed to maintain structural stability if (self.hasLanded) return; // Get rotation configurations for this shape type if (!self.rotationConfigs[self.shapeType]) return; // Calculate next rotation state var nextRotation = (self.rotation + 1) % 4; var nextConfig = self.rotationConfigs[self.shapeType][nextRotation]; // Check if rotation is valid (no collision) if (!self.checkCollision(self.gridX, self.gridY, nextConfig)) { // Update rotation state and configuration self.rotation = nextRotation; self.currentConfig = nextConfig; // Update pivot point for proper centered rotation var minX = 0, maxX = 0, minY = 0, maxY = 0; for (var i = 0; i < nextConfig.length; i++) { minX = Math.min(minX, nextConfig[i][0]); maxX = Math.max(maxX, nextConfig[i][0]); minY = Math.min(minY, nextConfig[i][1]); maxY = Math.max(maxY, nextConfig[i][1]); } var centerX = (minX + maxX) * blockSize / 2; var centerY = (minY + maxY) * blockSize / 2; self.blockGroup.pivot.set(centerX, centerY); // Update block positions based on new configuration for (var i = 0; i < self.blocks.length; i++) { if (nextConfig[i]) { self.blocks[i].graphic.x = nextConfig[i][0] * blockSize; self.blocks[i].graphic.y = nextConfig[i][1] * blockSize; self.blocks[i].offsetX = nextConfig[i][0]; self.blocks[i].offsetY = nextConfig[i][1]; } } // Play rotation sound effect LK.getSound('rotate').play(); // Animate rotation on the block group with responsive timing tween(self.blockGroup, { rotation: self.blockGroup.rotation + Math.PI / 2 }, { duration: 150, easing: tween.easeOut }); } }; self.rotateLeft = function () { // Only allow rotation if shape hasn't landed to maintain structural stability if (self.hasLanded) return; // Get rotation configurations for this shape type if (!self.rotationConfigs[self.shapeType]) return; // Calculate previous rotation state (counter-clockwise) var nextRotation = (self.rotation + 3) % 4; // +3 is same as -1 in mod 4 var nextConfig = self.rotationConfigs[self.shapeType][nextRotation]; // Check if rotation is valid (no collision) if (!self.checkCollision(self.gridX, self.gridY, nextConfig)) { // Update rotation state and configuration self.rotation = nextRotation; self.currentConfig = nextConfig; // Update pivot point for proper centered rotation var minX = 0, maxX = 0, minY = 0, maxY = 0; for (var i = 0; i < nextConfig.length; i++) { minX = Math.min(minX, nextConfig[i][0]); maxX = Math.max(maxX, nextConfig[i][0]); minY = Math.min(minY, nextConfig[i][1]); maxY = Math.max(maxY, nextConfig[i][1]); } var centerX = (minX + maxX) * blockSize / 2; var centerY = (minY + maxY) * blockSize / 2; self.blockGroup.pivot.set(centerX, centerY); // Update block positions based on new configuration for (var i = 0; i < self.blocks.length; i++) { if (nextConfig[i]) { self.blocks[i].graphic.x = nextConfig[i][0] * blockSize; self.blocks[i].graphic.y = nextConfig[i][1] * blockSize; self.blocks[i].offsetX = nextConfig[i][0]; self.blocks[i].offsetY = nextConfig[i][1]; } } // Play rotation sound effect LK.getSound('rotate').play(); // Animate rotation on the block group with responsive timing (counter-clockwise) tween(self.blockGroup, { rotation: self.blockGroup.rotation - Math.PI / 2 }, { duration: 150, easing: tween.easeOut }); } }; self.down = function (x, y, obj) { // Ignore touches on landed shapes to maintain structural stability until row completion if (self.hasLanded) return; // Store touch start time for tap detection self.touchStartTime = LK.ticks; // Clear any existing selection first if (selectedShape && selectedShape !== self) { // Reset previous shape tween(selectedShape, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 100 }); selectedShape.isDragging = false; } // Immediately select this shape when touched selectedShape = self; selectedShape.isDragging = true; // No touch offset - use direct positioning for 1:1 movement // Immediate visual feedback on the group tween(self.blockGroup, { scaleX: 1.1, scaleY: 1.1, alpha: 0.8 }, { duration: 100, easing: tween.easeOut }); }; self.up = function (x, y, obj) { // Handle tap-to-rotate for falling shapes (maintain structural stability after landing) if (selectedShape === self && !self.hasLanded) { // Check if this was a tap (not a drag) and shape hasn't moved var touchDuration = LK.ticks - (self.touchStartTime || 0); if (!self.hasMoved && touchDuration < 30) { // Quick tap under 0.5 seconds self.rotate(); } selectedShape.isDragging = false; selectedShape = null; self.hasMoved = false; // Reset movement flag // Reset visual feedback on the group tween(self.blockGroup, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 150, easing: tween.easeOut }); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x87CEEB }); /**** * Game Code ****/ var fallingShapes = []; var placedShapes = []; var score = 0; var linesCleared = 0; var level = 1; var groundY = 2400; var shapeTypes = ['lblock', 'tblock', 'zblock', 'iblock', 'jblock', 'sblock', 'oblock']; var specialBlocks = ['gift', 'bomb', 'wildcard']; var shapeColors = { 'lblock': 0xFF6600, // Orange - L piece 'tblock': 0x9900FF, // Purple - T piece 'zblock': 0xFF0000, // Red - Z piece 'iblock': 0x00FFFF, // Cyan - I piece 'jblock': 0x0000FF, // Blue - J piece 'sblock': 0x00FF00, // Green - S piece 'oblock': 0xFFFF00, // Yellow - O piece 'cube': 0x4a90e2, 'pyramid': 0xf5a623, 'triangle': 0x00b894, 'diamond': 0xfd79a8, 'star': 0xff6b35, 'cross': 0x1b998b, 'hexagon': 0x6c5ce7 }; var gridWidth = 24; var gridHeight = 24; var blockSize = 80; var grid = []; var stability = 100; var maxHeight = 0; var selectedShape = null; var inventory = []; // Initialize empty grid for (var row = 0; row < gridHeight; row++) { grid[row] = []; for (var col = 0; col < gridWidth; col++) { grid[row][col] = null; } } var nextShapeTimer = 0; var shapeSpawnDelay = 180; // 3 seconds at 60fps - slower initial spawn var maxFallingShapes = 1; // Start with only 1 falling shape // Create ground var ground = game.addChild(LK.getAsset('ground', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: groundY })); // Create faint grid lines for visual alignment var gridContainer = new Container(); game.addChild(gridContainer); var gameAreaLeft = (2048 - gridWidth * blockSize) / 2; var gameAreaTop = 200; // Create vertical grid lines for (var col = 0; col <= gridWidth; col++) { var vLine = LK.getAsset('gridLine', { anchorX: 0, anchorY: 0 }); vLine.x = gameAreaLeft + col * blockSize; vLine.y = gameAreaTop; vLine.width = 2; vLine.height = gridHeight * blockSize; vLine.tint = 0x444444; vLine.alpha = 0.3; gridContainer.addChild(vLine); } // Create horizontal grid lines for (var row = 0; row <= gridHeight; row++) { var hLine = LK.getAsset('gridLine', { anchorX: 0, anchorY: 0 }); hLine.x = gameAreaLeft; hLine.y = gameAreaTop + row * blockSize; hLine.width = gridWidth * blockSize; hLine.height = 2; hLine.tint = 0x444444; hLine.alpha = 0.3; gridContainer.addChild(hLine); } // Create UI elements var scoreText = new Text2('Score: 0', { size: 40, fill: 0xFFFFFF }); scoreText.anchor.set(0, 0); scoreText.x = 120; scoreText.y = 120; LK.gui.topLeft.addChild(scoreText); var stabilityText = new Text2('Stability: 100%', { size: 40, fill: 0xFFFFFF }); stabilityText.anchor.set(0, 0); stabilityText.x = 120; stabilityText.y = 180; LK.gui.topLeft.addChild(stabilityText); // Stability bar background var stabilityBarBg = LK.getAsset('stabilityBarBg', { anchorX: 0, anchorY: 0, x: 120, y: 220 }); LK.gui.topLeft.addChild(stabilityBarBg); // Stability bar var stabilityBar = LK.getAsset('stabilityBar', { anchorX: 0, anchorY: 0, x: 120, y: 220 }); LK.gui.topLeft.addChild(stabilityBar); var inventoryText = new Text2('Inventory: 0', { size: 40, fill: 0xFFFFFF }); inventoryText.anchor.set(1, 0); LK.gui.topRight.addChild(inventoryText); // Create rotation buttons var rotateLeftButton = new Text2('⟲', { size: 80, fill: 0xFFFFFF }); rotateLeftButton.anchor.set(0.5, 1); rotateLeftButton.x = -150; rotateLeftButton.y = -50; LK.gui.bottom.addChild(rotateLeftButton); var rotateRightButton = new Text2('⟳', { size: 80, fill: 0xFFFFFF }); rotateRightButton.anchor.set(0.5, 1); rotateRightButton.x = 150; rotateRightButton.y = -50; LK.gui.bottom.addChild(rotateRightButton); // Enable rotation for falling shapes while maintaining structural integrity after landing rotateLeftButton.down = function (x, y, obj) { // Find the first falling shape that can rotate for (var i = 0; i < fallingShapes.length; i++) { var shape = fallingShapes[i]; if (!shape.hasLanded) { shape.rotateLeft(); break; } } }; rotateRightButton.down = function (x, y, obj) { // Find the first falling shape that can rotate for (var i = 0; i < fallingShapes.length; i++) { var shape = fallingShapes[i]; if (!shape.hasLanded) { shape.rotate(); break; } } }; var heightText = new Text2('Height: 0', { size: 40, fill: 0xFFFFFF }); heightText.anchor.set(0, 0); heightText.x = 120; heightText.y = 260; LK.gui.topLeft.addChild(heightText); function spawnShape() { var shapeType; // 10% chance for special blocks if (Math.random() < 0.1) { shapeType = specialBlocks[Math.floor(Math.random() * specialBlocks.length)]; } else { shapeType = shapeTypes[Math.floor(Math.random() * shapeTypes.length)]; } var shape = new TetrisBlock(shapeType); fallingShapes.push(shape); game.addChild(shape); // Play soft spawn sound when shape enters from top LK.getSound('spawn').play(); } function updateInventoryDisplay() { inventoryText.setText('Inventory: ' + inventory.length); } function updateScore() { scoreText.setText('Score: ' + score); } function calculateStability() { // Calculate stability based on grid density and structure var totalBlocks = 0; var connectedBlocks = 0; var baseSupport = 0; // Count total blocks and base support for (var row = 0; row < gridHeight; row++) { for (var col = 0; col < gridWidth; col++) { if (grid[row][col]) { totalBlocks++; // Check if block has support below or is on ground if (row === gridHeight - 1 || grid[row + 1][col]) { baseSupport++; } // Check connections (adjacent blocks) var connections = 0; if (row > 0 && grid[row - 1][col]) connections++; if (row < gridHeight - 1 && grid[row + 1][col]) connections++; if (col > 0 && grid[row][col - 1]) connections++; if (col < gridWidth - 1 && grid[row][col + 1]) connections++; if (connections > 0) connectedBlocks++; } } } if (totalBlocks === 0) { stability = 100; } else { // Calculate stability percentage var supportRatio = baseSupport / totalBlocks; var connectionRatio = connectedBlocks / totalBlocks; stability = Math.max(10, Math.min(100, (supportRatio * 60 + connectionRatio * 40) * 100)); } } function updateStability() { stabilityText.setText('Stability: ' + Math.round(stability) + '%'); var stabilityPercent = Math.max(0, Math.min(1, stability / 100)); stabilityBar.width = 300 * stabilityPercent; // Color based on stability if (stability > 70) { stabilityBar.tint = 0x00FF00; // Green } else if (stability > 30) { stabilityBar.tint = 0xFFFF00; // Yellow } else { stabilityBar.tint = 0xFF0000; // Red } } function updateHeight() { var currentHeight = 0; for (var i = 0; i < placedShapes.length; i++) { var shapeBottom = placedShapes[i].y + 40; var heightFromGround = groundY - shapeBottom; if (heightFromGround > currentHeight) { currentHeight = heightFromGround; } } maxHeight = Math.max(maxHeight, currentHeight); heightText.setText('Height: ' + Math.round(maxHeight / 80)); } function applyGravity() { var hasFloatingBlocks = true; while (hasFloatingBlocks) { hasFloatingBlocks = false; // Check each grid position from bottom to top for (var row = gridHeight - 2; row >= 0; row--) { for (var col = 0; col < gridWidth; col++) { if (grid[row][col] && !grid[row + 1][col]) { // Block is floating - make it fall grid[row + 1][col] = grid[row][col]; grid[row][col] = null; hasFloatingBlocks = true; // Find and move corresponding visual block for (var i = 0; i < placedShapes.length; i++) { var block = placedShapes[i]; if (block.gridX === col && block.gridY === row) { // Stop any existing tween on this block first tween.stop(block); block.gridY = row + 1; tween(block, { y: block.gridY * blockSize + blockSize / 2 + 200 }, { duration: 150, easing: tween.easeOut }); break; } } } } } } } function checkForLines() { var linesToClear = []; // Check each row for full row completion (24 consecutive blocks from multiple shapes) for (var row = 0; row < gridHeight; row++) { var blocksInRow = 0; var shapesInRow = {}; // Count blocks and track unique shapes in this row for (var col = 0; col < gridWidth; col++) { if (grid[row][col]) { blocksInRow++; // Track which shapes contributed to this row for (var i = 0; i < placedShapes.length; i++) { var shape = placedShapes[i]; if (shape.gridY === row && shape.gridX === col) { // Use shape object reference as unique identifier var shapeId = shape.shapeType + '_' + shape.x + '_' + shape.y; shapesInRow[shapeId] = true; break; } } } } // Count unique shapes in this row var uniqueShapeCount = 0; for (var shapeId in shapesInRow) { uniqueShapeCount++; } // Only clear if entire row is filled (24 blocks) AND comes from multiple whole shapes if (blocksInRow === gridWidth && uniqueShapeCount >= 2) { linesToClear.push({ row: row, type: 'full_row', points: 100 }); } } // Clear matched lines if (linesToClear.length > 0) { clearLines(linesToClear); } } function clearLines(lines) { LK.getSound('lineClear').play(); var totalPoints = 0; // Calculate bonus for multiple rows cleared simultaneously var linesCount = lines.length; for (var i = 0; i < lines.length; i++) { totalPoints += lines[i].points || 100; } // Bonus scoring for multiple lines if (linesCount === 2) { totalPoints = totalPoints * 3; // Triple score for 2 lines } else if (linesCount === 3) { totalPoints = totalPoints * 5; // 5x score for 3 lines } else if (linesCount >= 4) { totalPoints = totalPoints * 8; // 8x score for 4+ lines (Tetris!) } score += totalPoints; linesCleared += linesCount; updateScore(); // Remove visual blocks and clear grid for (var i = 0; i < lines.length; i++) { var row = lines[i].row; // Remove visual blocks for (var j = placedShapes.length - 1; j >= 0; j--) { var block = placedShapes[j]; if (block.gridY === row) { tween(block, { alpha: 0, scaleX: 0, scaleY: 0 }, { duration: 300, onFinish: function onFinish() { block.destroy(); } }); placedShapes.splice(j, 1); } } // Clear grid row for (var col = 0; col < gridWidth; col++) { grid[row][col] = null; } } // Drop remaining blocks down LK.setTimeout(function () { dropBlocks(lines); }, 400); } function dropBlocks(clearedLines) { // Sort cleared lines from top to bottom to handle dropping correctly var sortedLines = []; for (var i = 0; i < clearedLines.length; i++) { sortedLines.push(clearedLines[i].row); } sortedLines.sort(function (a, b) { return a - b; }); // Process from bottom to top for (var row = gridHeight - 1; row >= 0; row--) { var dropDistance = 0; // Count how many cleared lines are below this row for (var i = 0; i < sortedLines.length; i++) { if (sortedLines[i] > row) { dropDistance++; } } if (dropDistance > 0) { // Move grid data for (var col = 0; col < gridWidth; col++) { if (grid[row][col]) { grid[row + dropDistance][col] = grid[row][col]; grid[row][col] = null; } } // Move visual blocks for (var j = 0; j < placedShapes.length; j++) { var block = placedShapes[j]; if (block.gridY === row) { block.gridY += dropDistance; tween(block, { y: block.gridY * blockSize + blockSize / 2 + 200 }, { duration: 200, easing: tween.easeOut }); } } } } } function handleSpecialBlock(type, gridX, gridY) { if (type === 'gift') { score += 50; updateScore(); LK.effects.flashObject(game, 0xffd700, 500); } else if (type === 'bomb') { LK.getSound('bomb').play(); // Destroy blocks in 3x3 area for (var row = Math.max(0, gridY - 1); row <= Math.min(gridHeight - 1, gridY + 1); row++) { for (var col = Math.max(0, gridX - 1); col <= Math.min(gridWidth - 1, gridX + 1); col++) { if (grid[row][col]) { grid[row][col] = null; // Remove visual block for (var i = placedShapes.length - 1; i >= 0; i--) { var block = placedShapes[i]; if (block.gridX === col && block.gridY === row) { tween(block, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, onFinish: function onFinish() { block.destroy(); } }); placedShapes.splice(i, 1); break; } } } } } } else if (type === 'wildcard') { // Fill nearest gap var nearestGap = findNearestGap(gridX, gridY); if (nearestGap) { grid[nearestGap.row][nearestGap.col] = 'cube'; var wildBlock = new GridBlock('cube'); wildBlock.gridX = nearestGap.col; wildBlock.gridY = nearestGap.row; wildBlock.x = nearestGap.col * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2; wildBlock.y = nearestGap.row * blockSize + blockSize / 2 + 200; placedShapes.push(wildBlock); game.addChild(wildBlock); tween(wildBlock, { scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.bounceOut, onFinish: function onFinish() { tween(wildBlock, { scaleX: 1, scaleY: 1 }, { duration: 100 }); } }); } } // Trigger stability and height recalculation after special block effects calculateStability(); updateStability(); applyGravity(); checkForLines(); } function findNearestGap(startX, startY) { var searchRadius = 1; while (searchRadius < Math.max(gridWidth, gridHeight)) { for (var row = Math.max(0, startY - searchRadius); row <= Math.min(gridHeight - 1, startY + searchRadius); row++) { for (var col = Math.max(0, startX - searchRadius); col <= Math.min(gridWidth - 1, startX + searchRadius); col++) { if (!grid[row][col]) { return { row: row, col: col }; } } } searchRadius++; } return null; } function triggerCollapse() { LK.getSound('collapse').play(); // Remove unstable shapes for (var i = placedShapes.length - 1; i >= 0; i--) { if (Math.random() < 0.3) { // 30% chance each shape collapses var shape = placedShapes[i]; tween(shape, { alpha: 0, y: shape.y + 100 }, { duration: 500, onFinish: function onFinish() { shape.destroy(); } }); placedShapes.splice(i, 1); } } score = Math.max(0, score - 50); updateScore(); } function placeShape(shape, x, y) { if (inventory.length === 0) return; var inventoryShape = inventory.shift(); var placedShape = new PlacedShape(inventoryShape.shapeType); placedShape.x = x; placedShape.y = y; // Start with smaller scale for placement animation placedShape.scaleX = 0.5; placedShape.scaleY = 0.5; placedShape.alpha = 0.7; placedShapes.push(placedShape); game.addChild(placedShape); // Animate placement with smooth scaling tween(placedShape, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 300, easing: tween.elasticOut }); LK.getSound('place').play(); score += 10; // Bonus for good placement var centerX = 1024; var distanceFromCenter = Math.abs(x - centerX); if (distanceFromCenter < 100) { score += 5; // Bonus for center placement // Add bonus glow effect for center placement tween(placedShape, { tint: 0xffff00 }, { duration: 500, onFinish: function onFinish() { tween(placedShape, { tint: 0xffffff }, { duration: 500 }); } }); } updateScore(); updateInventoryDisplay(); calculateStability(); updateHeight(); inventoryShape.destroy(); } // Event handlers game.down = function (x, y, obj) { // Touch handling is now done directly by TetrisBlock shapes // This ensures only the touched shape responds }; game.move = function (x, y, obj) { // Handle dragging of selected shape with precise 1:1 movement mapping // Prevent movement of landed shapes to maintain structural stability until row completion if (selectedShape && selectedShape.isDragging && !selectedShape.hasLanded) { var gameAreaLeft = (2048 - gridWidth * blockSize) / 2; var gameAreaRight = gameAreaLeft + gridWidth * blockSize; // Use direct finger position for movement - no offset calculation // This creates 1:1 mapping between finger movement and shape movement var clampedX = Math.max(gameAreaLeft + blockSize / 2, Math.min(gameAreaRight - blockSize / 2, x)); var targetGridX = Math.floor((clampedX - gameAreaLeft) / blockSize); targetGridX = Math.max(0, Math.min(gridWidth - 1, targetGridX)); // Move to new position only if it's actually different if (targetGridX !== selectedShape.gridX) { selectedShape.moveToPosition(targetGridX); } } }; game.up = function (x, y, obj) { // Touch release is now handled directly by TetrisBlock shapes // This ensures proper cleanup when touch ends }; game.update = function () { // One-at-a-time spawn rule: only spawn when no shapes are falling nextShapeTimer++; if (nextShapeTimer >= shapeSpawnDelay && fallingShapes.length === 0) { spawnShape(); nextShapeTimer = 0; } // Apply gravity to prevent floating blocks if (LK.ticks % 20 === 0) { applyGravity(); } // Update stability periodically if (LK.ticks % 30 === 0) { calculateStability(); updateStability(); } // Check win condition if (maxHeight > 800) { // About 10 blocks high LK.showYouWin(); } // Check game over condition - stack overflow (blocked spawn area) var spawnAreaBlocked = false; // Check if spawn area (top 3 rows) has any blocks that would prevent new shapes for (var row = 0; row < 3; row++) { for (var col = 0; col < gridWidth; col++) { if (grid[row][col]) { spawnAreaBlocked = true; break; } } if (spawnAreaBlocked) break; } // Game over if spawn area is blocked or stability is too low if (spawnAreaBlocked || stability < 10 && placedShapes.length > 0) { LK.showGameOver(); } }; // Initialize display updateScore(); updateStability(); updateInventoryDisplay(); updateHeight();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var GridBlock = Container.expand(function (shapeType) {
var self = Container.call(this);
self.shapeType = shapeType;
self.color = shapeColors[shapeType] || 0xffffff;
self.gridX = 0;
self.gridY = 0;
var shapeGraphic = self.attachAsset(shapeType, {
anchorX: 0.5,
anchorY: 0.5
});
// Add bomb fuse for bomb blocks
if (shapeType === 'bomb') {
var fuseGraphic = self.attachAsset('bombFuse', {
anchorX: 0.5,
anchorY: 1,
x: 0,
y: -25
});
// Animate fuse flickering
var _fuseFlicker = function fuseFlicker() {
tween(fuseGraphic, {
alpha: 0.3
}, {
duration: 200,
onFinish: function onFinish() {
tween(fuseGraphic, {
alpha: 1
}, {
duration: 200,
onFinish: _fuseFlicker
});
}
});
};
_fuseFlicker();
}
return self;
});
var PlacedShape = Container.expand(function (shapeType) {
var self = Container.call(this);
self.shapeType = shapeType;
self.isStable = true;
self.isDragging = false;
var shapeGraphic = self.attachAsset(shapeType, {
anchorX: 0.5,
anchorY: 0.5
});
self.rotate = function () {
// Smooth rotation animation
tween(shapeGraphic, {
rotation: shapeGraphic.rotation + Math.PI / 2
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
calculateStability();
updateStability();
}
});
};
self.down = function (x, y, obj) {
// Only set as selected if no other shape is already being dragged
if (!selectedShape || !selectedShape.isDragging) {
self.isDragging = true;
selectedShape = self;
}
};
self.up = function (x, y, obj) {
self.isDragging = false;
if (selectedShape === self) {
selectedShape = null;
}
calculateStability();
updateStability();
};
return self;
});
var TetrisBlock = Container.expand(function (shapeType) {
var self = Container.call(this);
self.shapeType = shapeType;
self.fallSpeed = Math.random() < 0.3 ? 3.5 : 2.5; // Increased by ~66% for more dynamic gameplay
self.isSpecial = specialBlocks.indexOf(shapeType) !== -1;
self.gridX = Math.floor(Math.random() * (gridWidth - 3)); // Ensure space for shape
self.gridY = 0;
self.lastMoveTime = 0;
self.targetX = 0; // For smooth movement
self.isMoving = false; // Track if block is currently moving
self.rotation = 0; // Track current rotation state (0, 1, 2, 3)
self.blocks = []; // Array to hold individual block graphics
self.hasLanded = false; // Track if shape has touched ground or another block
// Rotation configurations for each shape
self.rotationConfigs = {
'iblock': [[[-1, 0], [0, 0], [1, 0], [2, 0]],
// horizontal
[[0, -1], [0, 0], [0, 1], [0, 2]],
// vertical
[[-1, 0], [0, 0], [1, 0], [2, 0]],
// horizontal
[[0, -1], [0, 0], [0, 1], [0, 2]] // vertical
],
'oblock': [[[0, 0], [1, 0], [0, 1], [1, 1]],
// same for all rotations
[[0, 0], [1, 0], [0, 1], [1, 1]], [[0, 0], [1, 0], [0, 1], [1, 1]], [[0, 0], [1, 0], [0, 1], [1, 1]]],
'tblock': [[[-1, 0], [0, 0], [1, 0], [0, 1]],
// T pointing down
[[0, -1], [0, 0], [1, 0], [0, 1]],
// T pointing left
[[-1, 0], [0, 0], [1, 0], [0, -1]],
// T pointing up
[[0, -1], [-1, 0], [0, 0], [0, 1]] // T pointing right
],
'lblock': [[[-1, 0], [0, 0], [1, 0], [1, 1]],
// L standard
[[0, -1], [0, 0], [0, 1], [-1, 1]],
// L rotated 90
[[-1, -1], [-1, 0], [0, 0], [1, 0]],
// L rotated 180
[[0, -1], [0, 0], [0, 1], [1, -1]] // L rotated 270
],
'jblock': [[[-1, 0], [0, 0], [1, 0], [-1, 1]],
// J standard
[[0, -1], [0, 0], [0, 1], [1, 1]],
// J rotated 90
[[-1, 0], [0, 0], [1, 0], [1, -1]],
// J rotated 180
[[0, -1], [-1, -1], [0, 0], [0, 1]] // J rotated 270
],
'sblock': [[[-1, 1], [0, 1], [0, 0], [1, 0]],
// S horizontal
[[0, -1], [0, 0], [1, 0], [1, 1]],
// S vertical
[[-1, 1], [0, 1], [0, 0], [1, 0]],
// S horizontal
[[0, -1], [0, 0], [1, 0], [1, 1]] // S vertical
],
'zblock': [[[-1, 0], [0, 0], [0, 1], [1, 1]],
// Z horizontal
[[1, -1], [1, 0], [0, 0], [0, 1]],
// Z vertical
[[-1, 0], [0, 0], [0, 1], [1, 1]],
// Z horizontal
[[1, -1], [1, 0], [0, 0], [0, 1]] // Z vertical
]
};
// Define classic Tetris shape configurations with proper proportions
var shapeConfigs = {
'iblock': [[-1, 0], [0, 0], [1, 0], [2, 0] // I-piece: centered horizontal line
],
'oblock': [[0, 0], [1, 0], [0, 1], [1, 1] // O-piece: 2x2 square
],
'tblock': [[-1, 0], [0, 0], [1, 0], [0, 1] // T-piece: centered with stem down
],
'lblock': [[-1, 0], [0, 0], [1, 0], [1, 1] // L-piece: centered with corner
],
'jblock': [[-1, 0], [0, 0], [1, 0], [-1, 1] // J-piece: centered with corner
],
'sblock': [[-1, 1], [0, 1], [0, 0], [1, 0] // S-piece: classic S shape
],
'zblock': [[-1, 0], [0, 0], [0, 1], [1, 1] // Z-piece: classic Z shape
]
};
// Get shape configuration or default to single block
var config = shapeConfigs[shapeType] || [[0, 0]];
// Store current shape configuration after config is defined
self.currentConfig = self.rotationConfigs[shapeType] && self.rotationConfigs[shapeType][0] ? self.rotationConfigs[shapeType][0] : config;
var shapeColor = shapeColors[shapeType] || 0x4a90e2;
// Create a single group container for all blocks
self.blockGroup = new Container();
self.addChild(self.blockGroup);
// Create individual blocks within the group
for (var i = 0; i < config.length; i++) {
var blockGraphic = self.blockGroup.attachAsset(shapeType, {
anchorX: 0.5,
anchorY: 0.5
});
// Position block relative to group center
blockGraphic.x = config[i][0] * blockSize;
blockGraphic.y = config[i][1] * blockSize;
// Add subtle outline for better visibility
blockGraphic.alpha = 0.9;
// Completely disable interactivity on individual blocks to prevent structural changes
blockGraphic.interactive = false;
blockGraphic.buttonMode = false;
// Prevent any touch events from reaching individual blocks
blockGraphic.down = null;
blockGraphic.up = null;
blockGraphic.move = null;
self.blocks.push({
graphic: blockGraphic,
offsetX: config[i][0],
offsetY: config[i][1]
});
}
// Calculate proper pivot point based on shape bounds for centered rotation
var minX = 0,
maxX = 0,
minY = 0,
maxY = 0;
for (var i = 0; i < config.length; i++) {
minX = Math.min(minX, config[i][0]);
maxX = Math.max(maxX, config[i][0]);
minY = Math.min(minY, config[i][1]);
maxY = Math.max(maxY, config[i][1]);
}
var centerX = (minX + maxX) * blockSize / 2;
var centerY = (minY + maxY) * blockSize / 2;
self.blockGroup.pivot.set(centerX, centerY);
// Add bomb fuse for bomb blocks
if (shapeType === 'bomb') {
var fuseGraphic = self.attachAsset('bombFuse', {
anchorX: 0.5,
anchorY: 1,
x: 0,
y: -25
});
// Animate fuse flickering
var _fuseFlicker2 = function fuseFlicker() {
tween(fuseGraphic, {
alpha: 0.3
}, {
duration: 200,
onFinish: function onFinish() {
tween(fuseGraphic, {
alpha: 1
}, {
duration: 200,
onFinish: _fuseFlicker2
});
}
});
};
_fuseFlicker2();
}
// Position on grid
self.x = self.gridX * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2;
self.y = self.gridY * blockSize + blockSize / 2 + 200;
self.targetX = self.x; // Initialize target position
self.checkCollision = function (newGridX, newGridY, configToCheck) {
// Use provided configuration or current configuration
var config = configToCheck || self.currentConfig;
// Check collision for each block in the configuration
for (var i = 0; i < config.length; i++) {
var blockX = newGridX + config[i][0];
var blockY = newGridY + config[i][1];
// Check bounds
if (blockX < 0 || blockX >= gridWidth || blockY < 0 || blockY >= gridHeight) {
return true;
}
// Check if position is occupied in grid
if (blockY >= 0 && blockY < gridHeight && grid[blockY] && grid[blockY][blockX]) {
return true;
}
// Check collision with other falling shapes
for (var j = 0; j < fallingShapes.length; j++) {
var otherShape = fallingShapes[j];
if (otherShape !== self) {
for (var k = 0; k < otherShape.blocks.length; k++) {
var otherBlock = otherShape.blocks[k];
var otherBlockX = otherShape.gridX + otherBlock.offsetX;
var otherBlockY = otherShape.gridY + otherBlock.offsetY;
if (otherBlockX === blockX && otherBlockY === blockY) {
return true;
}
}
}
}
}
return false;
};
self.moveToPosition = function (newGridX) {
// Track that the shape has moved (for distinguishing tap from drag)
if (Math.abs(newGridX - self.gridX) > 0) {
self.hasMoved = true;
}
// Clamp to grid boundaries based on current shape configuration
var minX = 0;
var maxX = 0;
for (var i = 0; i < self.currentConfig.length; i++) {
var blockX = self.currentConfig[i][0];
minX = Math.min(minX, blockX);
maxX = Math.max(maxX, blockX);
}
// Clamp position to keep all blocks within grid bounds
newGridX = Math.max(-minX, Math.min(gridWidth - 1 - maxX, newGridX));
// Allow movement even if there might be collision - let player move freely while dragging
self.gridX = newGridX;
self.targetX = self.gridX * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2;
self.isMoving = true;
// Stop any existing movement tween
tween.stop(self, {
x: true
});
// Instant movement for maximum responsiveness when dragging
if (self.isDragging) {
self.x = self.targetX;
self.isMoving = false;
} else {
// Ultra-fast responsive movement for non-dragging cases
tween(self, {
x: self.targetX
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isMoving = false;
}
});
}
};
self.update = function () {
var currentTime = LK.ticks;
if (currentTime - self.lastMoveTime > 80 / self.fallSpeed) {
self.lastMoveTime = currentTime;
// Check if can move down using collision detection
var canMoveDown = !self.checkCollision(self.gridX, self.gridY + 1);
if (canMoveDown) {
// Play light drop sound for step-down movement
LK.getSound('drop').play();
self.gridY++;
self.y = self.gridY * blockSize + blockSize / 2 + 200;
} else {
// Check if touching another shape (not ground) for contact sound
var touchingShape = false;
for (var j = 0; j < fallingShapes.length; j++) {
var otherShape = fallingShapes[j];
if (otherShape !== self && self.checkCollision(self.gridX, self.gridY + 1)) {
touchingShape = true;
break;
}
}
// Check if there are blocks below in grid
var touchingGrid = false;
for (var i = 0; i < self.currentConfig.length; i++) {
var blockX = self.gridX + self.currentConfig[i][0];
var blockY = self.gridY + self.currentConfig[i][1] + 1;
if (blockY < gridHeight && blockY >= 0 && grid[blockY] && grid[blockY][blockX]) {
touchingGrid = true;
break;
}
}
// Play appropriate sound
if (touchingShape || touchingGrid) {
if (self.gridY + 1 >= gridHeight) {
// Landing on ground
LK.getSound('land').play();
} else {
// Contact with another shape
LK.getSound('contact').play();
}
}
// Mark as landed when it can't move down
self.hasLanded = true;
// Only place if we're in a valid position and can't move down
if (self.gridY >= 0) {
self.placeInGrid();
} else {
// If stuck above grid, try to find a valid position
var foundValidPosition = false;
for (var testY = 0; testY < gridHeight; testY++) {
if (!self.checkCollision(self.gridX, testY)) {
self.gridY = testY;
self.y = self.gridY * blockSize + blockSize / 2 + 200;
foundValidPosition = true;
break;
}
}
if (!foundValidPosition) {
// Stop any existing tweens before destroying
tween.stop(self);
tween.stop(self.blockGroup);
for (var i = 0; i < self.blocks.length; i++) {
tween.stop(self.blocks[i].graphic);
}
// Remove stuck shape
var index = fallingShapes.indexOf(self);
if (index > -1) {
fallingShapes.splice(index, 1);
}
self.destroy();
}
}
}
}
};
self.placeInGrid = function () {
var allBlocksValid = true;
// Check if all blocks can be placed
for (var i = 0; i < self.currentConfig.length; i++) {
var blockX = self.gridX + self.currentConfig[i][0];
var blockY = self.gridY + self.currentConfig[i][1];
if (blockY < 0 || blockY >= gridHeight || blockX < 0 || blockX >= gridWidth) {
allBlocksValid = false;
break;
}
}
if (allBlocksValid) {
// Freeze the group before disassembly to maintain structural stability
self.blockGroup.rotation = Math.round(self.blockGroup.rotation / (Math.PI / 2)) * (Math.PI / 2);
// Place all blocks in grid - disassemble the group into static tiles
for (var i = 0; i < self.currentConfig.length; i++) {
var blockX = self.gridX + self.currentConfig[i][0];
var blockY = self.gridY + self.currentConfig[i][1];
grid[blockY][blockX] = self.shapeType;
// Create visual block in grid (static tile)
var placedBlock = new GridBlock(self.shapeType);
placedBlock.gridX = blockX;
placedBlock.gridY = blockY;
placedBlock.x = blockX * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2;
placedBlock.y = blockY * blockSize + blockSize / 2 + 200;
// Apply the same rotation as the group had
placedBlock.rotation = self.blockGroup.rotation;
placedShapes.push(placedBlock);
game.addChild(placedBlock);
}
LK.getSound('place').play();
score += 10 * self.blocks.length;
updateScore();
checkForLines();
// Handle special blocks
if (self.isSpecial) {
handleSpecialBlock(self.shapeType, self.gridX, self.gridY);
}
}
// Stop any existing tweens before destroying
tween.stop(self);
tween.stop(self.blockGroup);
for (var i = 0; i < self.blocks.length; i++) {
tween.stop(self.blocks[i].graphic);
}
// Remove from falling shapes
var index = fallingShapes.indexOf(self);
if (index > -1) {
fallingShapes.splice(index, 1);
}
self.destroy();
// Check game over
if (self.gridY <= 1) {
LK.showGameOver();
}
};
self.rotate = function () {
// Only allow rotation if shape hasn't landed to maintain structural stability
if (self.hasLanded) return;
// Get rotation configurations for this shape type
if (!self.rotationConfigs[self.shapeType]) return;
// Calculate next rotation state
var nextRotation = (self.rotation + 1) % 4;
var nextConfig = self.rotationConfigs[self.shapeType][nextRotation];
// Check if rotation is valid (no collision)
if (!self.checkCollision(self.gridX, self.gridY, nextConfig)) {
// Update rotation state and configuration
self.rotation = nextRotation;
self.currentConfig = nextConfig;
// Update pivot point for proper centered rotation
var minX = 0,
maxX = 0,
minY = 0,
maxY = 0;
for (var i = 0; i < nextConfig.length; i++) {
minX = Math.min(minX, nextConfig[i][0]);
maxX = Math.max(maxX, nextConfig[i][0]);
minY = Math.min(minY, nextConfig[i][1]);
maxY = Math.max(maxY, nextConfig[i][1]);
}
var centerX = (minX + maxX) * blockSize / 2;
var centerY = (minY + maxY) * blockSize / 2;
self.blockGroup.pivot.set(centerX, centerY);
// Update block positions based on new configuration
for (var i = 0; i < self.blocks.length; i++) {
if (nextConfig[i]) {
self.blocks[i].graphic.x = nextConfig[i][0] * blockSize;
self.blocks[i].graphic.y = nextConfig[i][1] * blockSize;
self.blocks[i].offsetX = nextConfig[i][0];
self.blocks[i].offsetY = nextConfig[i][1];
}
}
// Play rotation sound effect
LK.getSound('rotate').play();
// Animate rotation on the block group with responsive timing
tween(self.blockGroup, {
rotation: self.blockGroup.rotation + Math.PI / 2
}, {
duration: 150,
easing: tween.easeOut
});
}
};
self.rotateLeft = function () {
// Only allow rotation if shape hasn't landed to maintain structural stability
if (self.hasLanded) return;
// Get rotation configurations for this shape type
if (!self.rotationConfigs[self.shapeType]) return;
// Calculate previous rotation state (counter-clockwise)
var nextRotation = (self.rotation + 3) % 4; // +3 is same as -1 in mod 4
var nextConfig = self.rotationConfigs[self.shapeType][nextRotation];
// Check if rotation is valid (no collision)
if (!self.checkCollision(self.gridX, self.gridY, nextConfig)) {
// Update rotation state and configuration
self.rotation = nextRotation;
self.currentConfig = nextConfig;
// Update pivot point for proper centered rotation
var minX = 0,
maxX = 0,
minY = 0,
maxY = 0;
for (var i = 0; i < nextConfig.length; i++) {
minX = Math.min(minX, nextConfig[i][0]);
maxX = Math.max(maxX, nextConfig[i][0]);
minY = Math.min(minY, nextConfig[i][1]);
maxY = Math.max(maxY, nextConfig[i][1]);
}
var centerX = (minX + maxX) * blockSize / 2;
var centerY = (minY + maxY) * blockSize / 2;
self.blockGroup.pivot.set(centerX, centerY);
// Update block positions based on new configuration
for (var i = 0; i < self.blocks.length; i++) {
if (nextConfig[i]) {
self.blocks[i].graphic.x = nextConfig[i][0] * blockSize;
self.blocks[i].graphic.y = nextConfig[i][1] * blockSize;
self.blocks[i].offsetX = nextConfig[i][0];
self.blocks[i].offsetY = nextConfig[i][1];
}
}
// Play rotation sound effect
LK.getSound('rotate').play();
// Animate rotation on the block group with responsive timing (counter-clockwise)
tween(self.blockGroup, {
rotation: self.blockGroup.rotation - Math.PI / 2
}, {
duration: 150,
easing: tween.easeOut
});
}
};
self.down = function (x, y, obj) {
// Ignore touches on landed shapes to maintain structural stability until row completion
if (self.hasLanded) return;
// Store touch start time for tap detection
self.touchStartTime = LK.ticks;
// Clear any existing selection first
if (selectedShape && selectedShape !== self) {
// Reset previous shape
tween(selectedShape, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 100
});
selectedShape.isDragging = false;
}
// Immediately select this shape when touched
selectedShape = self;
selectedShape.isDragging = true;
// No touch offset - use direct positioning for 1:1 movement
// Immediate visual feedback on the group
tween(self.blockGroup, {
scaleX: 1.1,
scaleY: 1.1,
alpha: 0.8
}, {
duration: 100,
easing: tween.easeOut
});
};
self.up = function (x, y, obj) {
// Handle tap-to-rotate for falling shapes (maintain structural stability after landing)
if (selectedShape === self && !self.hasLanded) {
// Check if this was a tap (not a drag) and shape hasn't moved
var touchDuration = LK.ticks - (self.touchStartTime || 0);
if (!self.hasMoved && touchDuration < 30) {
// Quick tap under 0.5 seconds
self.rotate();
}
selectedShape.isDragging = false;
selectedShape = null;
self.hasMoved = false; // Reset movement flag
// Reset visual feedback on the group
tween(self.blockGroup, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 150,
easing: tween.easeOut
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87CEEB
});
/****
* Game Code
****/
var fallingShapes = [];
var placedShapes = [];
var score = 0;
var linesCleared = 0;
var level = 1;
var groundY = 2400;
var shapeTypes = ['lblock', 'tblock', 'zblock', 'iblock', 'jblock', 'sblock', 'oblock'];
var specialBlocks = ['gift', 'bomb', 'wildcard'];
var shapeColors = {
'lblock': 0xFF6600,
// Orange - L piece
'tblock': 0x9900FF,
// Purple - T piece
'zblock': 0xFF0000,
// Red - Z piece
'iblock': 0x00FFFF,
// Cyan - I piece
'jblock': 0x0000FF,
// Blue - J piece
'sblock': 0x00FF00,
// Green - S piece
'oblock': 0xFFFF00,
// Yellow - O piece
'cube': 0x4a90e2,
'pyramid': 0xf5a623,
'triangle': 0x00b894,
'diamond': 0xfd79a8,
'star': 0xff6b35,
'cross': 0x1b998b,
'hexagon': 0x6c5ce7
};
var gridWidth = 24;
var gridHeight = 24;
var blockSize = 80;
var grid = [];
var stability = 100;
var maxHeight = 0;
var selectedShape = null;
var inventory = [];
// Initialize empty grid
for (var row = 0; row < gridHeight; row++) {
grid[row] = [];
for (var col = 0; col < gridWidth; col++) {
grid[row][col] = null;
}
}
var nextShapeTimer = 0;
var shapeSpawnDelay = 180; // 3 seconds at 60fps - slower initial spawn
var maxFallingShapes = 1; // Start with only 1 falling shape
// Create ground
var ground = game.addChild(LK.getAsset('ground', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: groundY
}));
// Create faint grid lines for visual alignment
var gridContainer = new Container();
game.addChild(gridContainer);
var gameAreaLeft = (2048 - gridWidth * blockSize) / 2;
var gameAreaTop = 200;
// Create vertical grid lines
for (var col = 0; col <= gridWidth; col++) {
var vLine = LK.getAsset('gridLine', {
anchorX: 0,
anchorY: 0
});
vLine.x = gameAreaLeft + col * blockSize;
vLine.y = gameAreaTop;
vLine.width = 2;
vLine.height = gridHeight * blockSize;
vLine.tint = 0x444444;
vLine.alpha = 0.3;
gridContainer.addChild(vLine);
}
// Create horizontal grid lines
for (var row = 0; row <= gridHeight; row++) {
var hLine = LK.getAsset('gridLine', {
anchorX: 0,
anchorY: 0
});
hLine.x = gameAreaLeft;
hLine.y = gameAreaTop + row * blockSize;
hLine.width = gridWidth * blockSize;
hLine.height = 2;
hLine.tint = 0x444444;
hLine.alpha = 0.3;
gridContainer.addChild(hLine);
}
// Create UI elements
var scoreText = new Text2('Score: 0', {
size: 40,
fill: 0xFFFFFF
});
scoreText.anchor.set(0, 0);
scoreText.x = 120;
scoreText.y = 120;
LK.gui.topLeft.addChild(scoreText);
var stabilityText = new Text2('Stability: 100%', {
size: 40,
fill: 0xFFFFFF
});
stabilityText.anchor.set(0, 0);
stabilityText.x = 120;
stabilityText.y = 180;
LK.gui.topLeft.addChild(stabilityText);
// Stability bar background
var stabilityBarBg = LK.getAsset('stabilityBarBg', {
anchorX: 0,
anchorY: 0,
x: 120,
y: 220
});
LK.gui.topLeft.addChild(stabilityBarBg);
// Stability bar
var stabilityBar = LK.getAsset('stabilityBar', {
anchorX: 0,
anchorY: 0,
x: 120,
y: 220
});
LK.gui.topLeft.addChild(stabilityBar);
var inventoryText = new Text2('Inventory: 0', {
size: 40,
fill: 0xFFFFFF
});
inventoryText.anchor.set(1, 0);
LK.gui.topRight.addChild(inventoryText);
// Create rotation buttons
var rotateLeftButton = new Text2('⟲', {
size: 80,
fill: 0xFFFFFF
});
rotateLeftButton.anchor.set(0.5, 1);
rotateLeftButton.x = -150;
rotateLeftButton.y = -50;
LK.gui.bottom.addChild(rotateLeftButton);
var rotateRightButton = new Text2('⟳', {
size: 80,
fill: 0xFFFFFF
});
rotateRightButton.anchor.set(0.5, 1);
rotateRightButton.x = 150;
rotateRightButton.y = -50;
LK.gui.bottom.addChild(rotateRightButton);
// Enable rotation for falling shapes while maintaining structural integrity after landing
rotateLeftButton.down = function (x, y, obj) {
// Find the first falling shape that can rotate
for (var i = 0; i < fallingShapes.length; i++) {
var shape = fallingShapes[i];
if (!shape.hasLanded) {
shape.rotateLeft();
break;
}
}
};
rotateRightButton.down = function (x, y, obj) {
// Find the first falling shape that can rotate
for (var i = 0; i < fallingShapes.length; i++) {
var shape = fallingShapes[i];
if (!shape.hasLanded) {
shape.rotate();
break;
}
}
};
var heightText = new Text2('Height: 0', {
size: 40,
fill: 0xFFFFFF
});
heightText.anchor.set(0, 0);
heightText.x = 120;
heightText.y = 260;
LK.gui.topLeft.addChild(heightText);
function spawnShape() {
var shapeType;
// 10% chance for special blocks
if (Math.random() < 0.1) {
shapeType = specialBlocks[Math.floor(Math.random() * specialBlocks.length)];
} else {
shapeType = shapeTypes[Math.floor(Math.random() * shapeTypes.length)];
}
var shape = new TetrisBlock(shapeType);
fallingShapes.push(shape);
game.addChild(shape);
// Play soft spawn sound when shape enters from top
LK.getSound('spawn').play();
}
function updateInventoryDisplay() {
inventoryText.setText('Inventory: ' + inventory.length);
}
function updateScore() {
scoreText.setText('Score: ' + score);
}
function calculateStability() {
// Calculate stability based on grid density and structure
var totalBlocks = 0;
var connectedBlocks = 0;
var baseSupport = 0;
// Count total blocks and base support
for (var row = 0; row < gridHeight; row++) {
for (var col = 0; col < gridWidth; col++) {
if (grid[row][col]) {
totalBlocks++;
// Check if block has support below or is on ground
if (row === gridHeight - 1 || grid[row + 1][col]) {
baseSupport++;
}
// Check connections (adjacent blocks)
var connections = 0;
if (row > 0 && grid[row - 1][col]) connections++;
if (row < gridHeight - 1 && grid[row + 1][col]) connections++;
if (col > 0 && grid[row][col - 1]) connections++;
if (col < gridWidth - 1 && grid[row][col + 1]) connections++;
if (connections > 0) connectedBlocks++;
}
}
}
if (totalBlocks === 0) {
stability = 100;
} else {
// Calculate stability percentage
var supportRatio = baseSupport / totalBlocks;
var connectionRatio = connectedBlocks / totalBlocks;
stability = Math.max(10, Math.min(100, (supportRatio * 60 + connectionRatio * 40) * 100));
}
}
function updateStability() {
stabilityText.setText('Stability: ' + Math.round(stability) + '%');
var stabilityPercent = Math.max(0, Math.min(1, stability / 100));
stabilityBar.width = 300 * stabilityPercent;
// Color based on stability
if (stability > 70) {
stabilityBar.tint = 0x00FF00; // Green
} else if (stability > 30) {
stabilityBar.tint = 0xFFFF00; // Yellow
} else {
stabilityBar.tint = 0xFF0000; // Red
}
}
function updateHeight() {
var currentHeight = 0;
for (var i = 0; i < placedShapes.length; i++) {
var shapeBottom = placedShapes[i].y + 40;
var heightFromGround = groundY - shapeBottom;
if (heightFromGround > currentHeight) {
currentHeight = heightFromGround;
}
}
maxHeight = Math.max(maxHeight, currentHeight);
heightText.setText('Height: ' + Math.round(maxHeight / 80));
}
function applyGravity() {
var hasFloatingBlocks = true;
while (hasFloatingBlocks) {
hasFloatingBlocks = false;
// Check each grid position from bottom to top
for (var row = gridHeight - 2; row >= 0; row--) {
for (var col = 0; col < gridWidth; col++) {
if (grid[row][col] && !grid[row + 1][col]) {
// Block is floating - make it fall
grid[row + 1][col] = grid[row][col];
grid[row][col] = null;
hasFloatingBlocks = true;
// Find and move corresponding visual block
for (var i = 0; i < placedShapes.length; i++) {
var block = placedShapes[i];
if (block.gridX === col && block.gridY === row) {
// Stop any existing tween on this block first
tween.stop(block);
block.gridY = row + 1;
tween(block, {
y: block.gridY * blockSize + blockSize / 2 + 200
}, {
duration: 150,
easing: tween.easeOut
});
break;
}
}
}
}
}
}
}
function checkForLines() {
var linesToClear = [];
// Check each row for full row completion (24 consecutive blocks from multiple shapes)
for (var row = 0; row < gridHeight; row++) {
var blocksInRow = 0;
var shapesInRow = {};
// Count blocks and track unique shapes in this row
for (var col = 0; col < gridWidth; col++) {
if (grid[row][col]) {
blocksInRow++;
// Track which shapes contributed to this row
for (var i = 0; i < placedShapes.length; i++) {
var shape = placedShapes[i];
if (shape.gridY === row && shape.gridX === col) {
// Use shape object reference as unique identifier
var shapeId = shape.shapeType + '_' + shape.x + '_' + shape.y;
shapesInRow[shapeId] = true;
break;
}
}
}
}
// Count unique shapes in this row
var uniqueShapeCount = 0;
for (var shapeId in shapesInRow) {
uniqueShapeCount++;
}
// Only clear if entire row is filled (24 blocks) AND comes from multiple whole shapes
if (blocksInRow === gridWidth && uniqueShapeCount >= 2) {
linesToClear.push({
row: row,
type: 'full_row',
points: 100
});
}
}
// Clear matched lines
if (linesToClear.length > 0) {
clearLines(linesToClear);
}
}
function clearLines(lines) {
LK.getSound('lineClear').play();
var totalPoints = 0;
// Calculate bonus for multiple rows cleared simultaneously
var linesCount = lines.length;
for (var i = 0; i < lines.length; i++) {
totalPoints += lines[i].points || 100;
}
// Bonus scoring for multiple lines
if (linesCount === 2) {
totalPoints = totalPoints * 3; // Triple score for 2 lines
} else if (linesCount === 3) {
totalPoints = totalPoints * 5; // 5x score for 3 lines
} else if (linesCount >= 4) {
totalPoints = totalPoints * 8; // 8x score for 4+ lines (Tetris!)
}
score += totalPoints;
linesCleared += linesCount;
updateScore();
// Remove visual blocks and clear grid
for (var i = 0; i < lines.length; i++) {
var row = lines[i].row;
// Remove visual blocks
for (var j = placedShapes.length - 1; j >= 0; j--) {
var block = placedShapes[j];
if (block.gridY === row) {
tween(block, {
alpha: 0,
scaleX: 0,
scaleY: 0
}, {
duration: 300,
onFinish: function onFinish() {
block.destroy();
}
});
placedShapes.splice(j, 1);
}
}
// Clear grid row
for (var col = 0; col < gridWidth; col++) {
grid[row][col] = null;
}
}
// Drop remaining blocks down
LK.setTimeout(function () {
dropBlocks(lines);
}, 400);
}
function dropBlocks(clearedLines) {
// Sort cleared lines from top to bottom to handle dropping correctly
var sortedLines = [];
for (var i = 0; i < clearedLines.length; i++) {
sortedLines.push(clearedLines[i].row);
}
sortedLines.sort(function (a, b) {
return a - b;
});
// Process from bottom to top
for (var row = gridHeight - 1; row >= 0; row--) {
var dropDistance = 0;
// Count how many cleared lines are below this row
for (var i = 0; i < sortedLines.length; i++) {
if (sortedLines[i] > row) {
dropDistance++;
}
}
if (dropDistance > 0) {
// Move grid data
for (var col = 0; col < gridWidth; col++) {
if (grid[row][col]) {
grid[row + dropDistance][col] = grid[row][col];
grid[row][col] = null;
}
}
// Move visual blocks
for (var j = 0; j < placedShapes.length; j++) {
var block = placedShapes[j];
if (block.gridY === row) {
block.gridY += dropDistance;
tween(block, {
y: block.gridY * blockSize + blockSize / 2 + 200
}, {
duration: 200,
easing: tween.easeOut
});
}
}
}
}
}
function handleSpecialBlock(type, gridX, gridY) {
if (type === 'gift') {
score += 50;
updateScore();
LK.effects.flashObject(game, 0xffd700, 500);
} else if (type === 'bomb') {
LK.getSound('bomb').play();
// Destroy blocks in 3x3 area
for (var row = Math.max(0, gridY - 1); row <= Math.min(gridHeight - 1, gridY + 1); row++) {
for (var col = Math.max(0, gridX - 1); col <= Math.min(gridWidth - 1, gridX + 1); col++) {
if (grid[row][col]) {
grid[row][col] = null;
// Remove visual block
for (var i = placedShapes.length - 1; i >= 0; i--) {
var block = placedShapes[i];
if (block.gridX === col && block.gridY === row) {
tween(block, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 300,
onFinish: function onFinish() {
block.destroy();
}
});
placedShapes.splice(i, 1);
break;
}
}
}
}
}
} else if (type === 'wildcard') {
// Fill nearest gap
var nearestGap = findNearestGap(gridX, gridY);
if (nearestGap) {
grid[nearestGap.row][nearestGap.col] = 'cube';
var wildBlock = new GridBlock('cube');
wildBlock.gridX = nearestGap.col;
wildBlock.gridY = nearestGap.row;
wildBlock.x = nearestGap.col * blockSize + blockSize / 2 + (2048 - gridWidth * blockSize) / 2;
wildBlock.y = nearestGap.row * blockSize + blockSize / 2 + 200;
placedShapes.push(wildBlock);
game.addChild(wildBlock);
tween(wildBlock, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.bounceOut,
onFinish: function onFinish() {
tween(wildBlock, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
}
});
}
}
// Trigger stability and height recalculation after special block effects
calculateStability();
updateStability();
applyGravity();
checkForLines();
}
function findNearestGap(startX, startY) {
var searchRadius = 1;
while (searchRadius < Math.max(gridWidth, gridHeight)) {
for (var row = Math.max(0, startY - searchRadius); row <= Math.min(gridHeight - 1, startY + searchRadius); row++) {
for (var col = Math.max(0, startX - searchRadius); col <= Math.min(gridWidth - 1, startX + searchRadius); col++) {
if (!grid[row][col]) {
return {
row: row,
col: col
};
}
}
}
searchRadius++;
}
return null;
}
function triggerCollapse() {
LK.getSound('collapse').play();
// Remove unstable shapes
for (var i = placedShapes.length - 1; i >= 0; i--) {
if (Math.random() < 0.3) {
// 30% chance each shape collapses
var shape = placedShapes[i];
tween(shape, {
alpha: 0,
y: shape.y + 100
}, {
duration: 500,
onFinish: function onFinish() {
shape.destroy();
}
});
placedShapes.splice(i, 1);
}
}
score = Math.max(0, score - 50);
updateScore();
}
function placeShape(shape, x, y) {
if (inventory.length === 0) return;
var inventoryShape = inventory.shift();
var placedShape = new PlacedShape(inventoryShape.shapeType);
placedShape.x = x;
placedShape.y = y;
// Start with smaller scale for placement animation
placedShape.scaleX = 0.5;
placedShape.scaleY = 0.5;
placedShape.alpha = 0.7;
placedShapes.push(placedShape);
game.addChild(placedShape);
// Animate placement with smooth scaling
tween(placedShape, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 300,
easing: tween.elasticOut
});
LK.getSound('place').play();
score += 10;
// Bonus for good placement
var centerX = 1024;
var distanceFromCenter = Math.abs(x - centerX);
if (distanceFromCenter < 100) {
score += 5; // Bonus for center placement
// Add bonus glow effect for center placement
tween(placedShape, {
tint: 0xffff00
}, {
duration: 500,
onFinish: function onFinish() {
tween(placedShape, {
tint: 0xffffff
}, {
duration: 500
});
}
});
}
updateScore();
updateInventoryDisplay();
calculateStability();
updateHeight();
inventoryShape.destroy();
}
// Event handlers
game.down = function (x, y, obj) {
// Touch handling is now done directly by TetrisBlock shapes
// This ensures only the touched shape responds
};
game.move = function (x, y, obj) {
// Handle dragging of selected shape with precise 1:1 movement mapping
// Prevent movement of landed shapes to maintain structural stability until row completion
if (selectedShape && selectedShape.isDragging && !selectedShape.hasLanded) {
var gameAreaLeft = (2048 - gridWidth * blockSize) / 2;
var gameAreaRight = gameAreaLeft + gridWidth * blockSize;
// Use direct finger position for movement - no offset calculation
// This creates 1:1 mapping between finger movement and shape movement
var clampedX = Math.max(gameAreaLeft + blockSize / 2, Math.min(gameAreaRight - blockSize / 2, x));
var targetGridX = Math.floor((clampedX - gameAreaLeft) / blockSize);
targetGridX = Math.max(0, Math.min(gridWidth - 1, targetGridX));
// Move to new position only if it's actually different
if (targetGridX !== selectedShape.gridX) {
selectedShape.moveToPosition(targetGridX);
}
}
};
game.up = function (x, y, obj) {
// Touch release is now handled directly by TetrisBlock shapes
// This ensures proper cleanup when touch ends
};
game.update = function () {
// One-at-a-time spawn rule: only spawn when no shapes are falling
nextShapeTimer++;
if (nextShapeTimer >= shapeSpawnDelay && fallingShapes.length === 0) {
spawnShape();
nextShapeTimer = 0;
}
// Apply gravity to prevent floating blocks
if (LK.ticks % 20 === 0) {
applyGravity();
}
// Update stability periodically
if (LK.ticks % 30 === 0) {
calculateStability();
updateStability();
}
// Check win condition
if (maxHeight > 800) {
// About 10 blocks high
LK.showYouWin();
}
// Check game over condition - stack overflow (blocked spawn area)
var spawnAreaBlocked = false;
// Check if spawn area (top 3 rows) has any blocks that would prevent new shapes
for (var row = 0; row < 3; row++) {
for (var col = 0; col < gridWidth; col++) {
if (grid[row][col]) {
spawnAreaBlocked = true;
break;
}
}
if (spawnAreaBlocked) break;
}
// Game over if spawn area is blocked or stability is too low
if (spawnAreaBlocked || stability < 10 && placedShapes.length > 0) {
LK.showGameOver();
}
};
// Initialize display
updateScore();
updateStability();
updateInventoryDisplay();
updateHeight();