User prompt
oyunun herşeyini optimize et
User prompt
assets deki music ve ses olanları sıfırlar mısın
User prompt
düzelmedi
User prompt
Sound generation failed yazıyor
User prompt
multiball yazısı çok fazla çıkıyor
User prompt
multiball yazısı çok fazla çıkıyor
User prompt
arka plan müziği koyabileceğim bir asset oluştur lütfen bunu yapar mısın
User prompt
biraz daha iyileştirilebilir
User prompt
paddle takılıyor
User prompt
paddle en sola ve en sağa gitmiyor düzeltir misin
User prompt
paddle en sola ve en sağa gitmiyor düzeltir misin
User prompt
assets yerindeki music adlı yerdeki şarkıları bgmusic olarak ayarla
User prompt
bgmusic koyacağim bir yer ekle assete
User prompt
her level atladığında oyuncuya 1 tane fazladan atılabilir top ver
User prompt
Please fix the bug: 'Timeout.tick error: Cannot set properties of undefined (setting 'width')' in or related to this line: 'block.blockSprite.width = blockWScaled;' Line Number: 1274
User prompt
oyuncuya 10 can ekle
User prompt
oyunu level 1 den itibaren herşeyi optimize et ve bütün bug ları temizle
User prompt
oyunu komple fixle
User prompt
Please fix the bug: 'Timeout.tick error: block.setColor is not a function' in or related to this line: 'block.setColor(colors[(i + level) % colors.length]);' Line Number: 1265
User prompt
oyun kolaydan zora doğru gitsin
User prompt
Please fix the bug: 'Timeout.tick error: block.setColor is not a function' in or related to this line: 'block.setColor(color);' Line Number: 1249
User prompt
Please fix the bug: 'Timeout.tick error: block.setColor is not a function' in or related to this line: 'block.setColor(color);' Line Number: 1249
User prompt
düşen toplarda hepsiyle çarpışsın
User prompt
oyundaki bütün top türleri bütün toplarla çarpışabilsin
User prompt
ballrandom özelliğini kaldır
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // Ball class var Ball = Container.expand(function () { var self = Container.call(this); var ballSprite = self.attachAsset('ball', { anchorX: 0.5, anchorY: 0.5 }); self.radius = ballSprite.width / 2; self.vx = 0; self.vy = 0; self.speed = 22; // Initial speed self.sticky = false; // If true, ball sticks to paddle self.launch = function (angle) { // Launch ball at angle (in radians) self.vx = Math.cos(angle) * self.speed; self.vy = Math.sin(angle) * self.speed; self.sticky = false; }; self.update = function () { if (!self.sticky) { self.x += self.vx; self.y += self.vy; // --- Ava: Prevent main ball from slowing down too much or getting stuck in weird slow movement --- // Clamp minimum speed for both vx and vy (except when sticky) var minAbsV = 6; // Minimum absolute velocity for each axis var maxAbsV = 60; // Maximum absolute velocity for each axis (defensive) if (Math.abs(self.vx) < minAbsV) { self.vx = (self.vx < 0 ? -1 : 1) * minAbsV; } if (Math.abs(self.vy) < minAbsV) { self.vy = (self.vy < 0 ? -1 : 1) * minAbsV; } // Clamp to max as well (defensive) if (Math.abs(self.vx) > maxAbsV) { self.vx = (self.vx < 0 ? -1 : 1) * maxAbsV; } if (Math.abs(self.vy) > maxAbsV) { self.vy = (self.vy < 0 ? -1 : 1) * maxAbsV; } // Prevent pathological near-horizontal or near-vertical movement var minAngle = 0.18; // ~10 degrees from axis var speed = Math.sqrt(self.vx * self.vx + self.vy * self.vy); if (speed > 0) { var angle = Math.atan2(self.vy, self.vx); // If angle is too close to horizontal (0 or PI), nudge vy if (Math.abs(Math.sin(angle)) < minAngle) { self.vy += (self.vy < 0 ? -1 : 1) * minAbsV; } // If angle is too close to vertical (PI/2 or -PI/2), nudge vx if (Math.abs(Math.cos(angle)) < minAngle) { self.vx += (self.vx < 0 ? -1 : 1) * minAbsV; } } // Prevent ball from leaving left, right, or top of the screen if (self.x - self.radius < 0) { self.x = self.radius; self.vx = -self.vx; } if (self.x + self.radius > 2048) { self.x = 2048 - self.radius; self.vx = -self.vx; } if (self.y - self.radius < 0) { self.y = self.radius; self.vy = -self.vy; } // Laser/fireball: instantly destroy all blocks touched if ((self.fireball || typeof paddle !== "undefined" && paddle && paddle.laserBeam) && typeof blocks !== "undefined" && blocks && blocks.length) { for (var i = blocks.length - 1; i >= 0; i--) { var block = blocks[i]; if (block && self.intersects && self.intersects(block)) { block.hp = 0; if (block.updateHpBar) block.updateHpBar(); block.alpha = 0; block.scaleX = block.scaleY = 1.5; block.destroy(); if (block.parent) block.parent.removeChild(block); blocks.splice(i, 1); } } } } }; return self; }); // Block class var Block = Container.expand(function () { var self = Container.call(this); // color: 'red', 'green', 'blue', 'yellow' self.color = 'red'; self.hp = 1; self.maxHp = 1; self.hpBarBg = null; self.hpBar = null; self.blockType = 'normal'; // will be set by spawnBlocks self.setColor = function (color) { self.color = color; if (self.blockSprite) self.removeChild(self.blockSprite); var assetId = 'block_' + color; self.blockSprite = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Remove old HP bars if any if (self.hpBarBg) self.removeChild(self.hpBarBg); if (self.hpBar) self.removeChild(self.hpBar); // Add HP bar background (gray) self.hpBarBg = self.attachAsset('block_red', { anchorX: 0.5, anchorY: 0.5, width: self.blockSprite.width * 0.8, height: 14, y: -self.blockSprite.height / 2 - 18, tint: 0x444444 }); // Add HP bar (green) self.hpBar = self.attachAsset('block_green', { anchorX: 0.5, anchorY: 0.5, width: self.blockSprite.width * 0.78, height: 10, y: -self.blockSprite.height / 2 - 18, tint: 0x44ff44 }); self.hpBarBg.alpha = 0.7; self.hpBar.alpha = 0.9; self.hpBarBg.zIndex = 10; self.hpBar.zIndex = 11; self.hpBarBg.interactive = false; self.hpBar.interactive = false; self.addChild(self.hpBarBg); self.addChild(self.hpBar); self.updateHpBar(); }; self.setHp = function (hp, maxHp) { self.hp = hp; self.maxHp = maxHp || hp; self.updateHpBar(); }; self.updateHpBar = function () { if (!self.hpBar || !self.hpBarBg) return; var ratio = Math.max(0, Math.min(1, self.hp / self.maxHp)); self.hpBar.width = self.blockSprite.width * 0.78 * ratio; // Color changes: green > yellow > red if (ratio > 0.66) { self.hpBar.tint = 0x44ff44; } else if (ratio > 0.33) { self.hpBar.tint = 0xffe066; } else { self.hpBar.tint = 0xff4444; } self.hpBar.visible = self.hp > 0; self.hpBarBg.visible = self.hp > 0; }; self.breakBlock = function () { // Animate block breaking tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); } }); }; // --- HITBOX LOGIC --- // Returns the hitbox for this block, depending on its type and current transform self.getHitbox = function () { var bx = self.x, by = self.y; var w = self.blockSprite.width * (self.scaleX || 1); var h = self.blockSprite.height * (self.scaleY || 1); var rot = self.blockSprite.rotation || 0; if (self.blockType === 'round') { var r = w / 2; return { type: 'circle', x: bx, y: by, r: r }; } else if (self.blockType === 'diagonal') { var hw = w / 2, hh = h / 2; return { type: 'rotbox', x: bx, y: by, hw: hw, hh: hh, rot: rot }; } else { var hw = w / 2, hh = h / 2; return { type: 'box', x: bx, y: by, hw: hw, hh: hh }; } }; // Override .intersects for block to support all hitbox types self.intersects = function (other) { var myHitbox = self.getHitbox(); var otherHitbox = typeof other.getHitbox === "function" ? other.getHitbox() : null; if (!otherHitbox) { // Fallback: use bounding box var hw = self.blockSprite.width * (self.scaleX || 1) / 2; var hh = self.blockSprite.height * (self.scaleY || 1) / 2; return other.x > self.x - hw && other.x < self.x + hw && other.y > self.y - hh && other.y < self.y + hh; } // Circle-circle if (myHitbox.type === 'circle' && otherHitbox.type === 'circle') { var dx = myHitbox.x - otherHitbox.x; var dy = myHitbox.y - otherHitbox.y; var dist = Math.sqrt(dx * dx + dy * dy); return dist < myHitbox.r + otherHitbox.r; } // Box-box (AABB) if (myHitbox.type === 'box' && otherHitbox.type === 'box') { return Math.abs(myHitbox.x - otherHitbox.x) < myHitbox.hw + otherHitbox.hw && Math.abs(myHitbox.y - otherHitbox.y) < myHitbox.hh + otherHitbox.hh; } // Rotated box - circle if (myHitbox.type === 'rotbox' && otherHitbox.type === 'circle') { // Rotate circle center into box's local space var cos = Math.cos(-myHitbox.rot), sin = Math.sin(-myHitbox.rot); var dx = otherHitbox.x - myHitbox.x, dy = otherHitbox.y - myHitbox.y; var localX = dx * cos - dy * sin; var localY = dx * sin + dy * cos; // Clamp to box var clampedX = Math.max(-myHitbox.hw, Math.min(myHitbox.hw, localX)); var clampedY = Math.max(-myHitbox.hh, Math.min(myHitbox.hh, localY)); var distX = localX - clampedX, distY = localY - clampedY; return distX * distX + distY * distY < otherHitbox.r * otherHitbox.r; } // Circle - rotated box (swap) if (myHitbox.type === 'circle' && otherHitbox.type === 'rotbox') { // Use the same logic, swap order var cos = Math.cos(-otherHitbox.rot), sin = Math.sin(-otherHitbox.rot); var dx = myHitbox.x - otherHitbox.x, dy = myHitbox.y - otherHitbox.y; var localX = dx * cos - dy * sin; var localY = dx * sin + dy * cos; var clampedX = Math.max(-otherHitbox.hw, Math.min(otherHitbox.hw, localX)); var clampedY = Math.max(-otherHitbox.hh, Math.min(otherHitbox.hh, localY)); var distX = localX - clampedX, distY = localY - clampedY; return distX * distX + distY * distY < myHitbox.r * myHitbox.r; } // Rotated box - box (AABB fallback, not perfect) if (myHitbox.type === 'rotbox' && otherHitbox.type === 'box') { // Approximate by using AABB of rotated box var rot = myHitbox.rot; var hw = Math.abs(myHitbox.hw * Math.cos(rot)) + Math.abs(myHitbox.hh * Math.sin(rot)); var hh = Math.abs(myHitbox.hw * Math.sin(rot)) + Math.abs(myHitbox.hh * Math.cos(rot)); return Math.abs(myHitbox.x - otherHitbox.x) < hw + otherHitbox.hw && Math.abs(myHitbox.y - otherHitbox.y) < hh + otherHitbox.hh; } if (myHitbox.type === 'box' && otherHitbox.type === 'rotbox') { // Approximate by using AABB of rotated box var rot = otherHitbox.rot; var hw = Math.abs(otherHitbox.hw * Math.cos(rot)) + Math.abs(otherHitbox.hh * Math.sin(rot)); var hh = Math.abs(otherHitbox.hw * Math.sin(rot)) + Math.abs(otherHitbox.hh * Math.cos(rot)); return Math.abs(myHitbox.x - otherHitbox.x) < myHitbox.hw + hw && Math.abs(myHitbox.y - otherHitbox.y) < myHitbox.hh + hh; } // Fallback: treat as AABB var hw1 = myHitbox.hw || self.blockSprite.width * (self.scaleX || 1) / 2; var hh1 = myHitbox.hh || self.blockSprite.height * (self.scaleY || 1) / 2; var hw2 = otherHitbox.hw || (other.width ? other.width / 2 : 0); var hh2 = otherHitbox.hh || (other.height ? other.height / 2 : 0); return Math.abs(myHitbox.x - otherHitbox.x) < hw1 + hw2 && Math.abs(myHitbox.y - otherHitbox.y) < hh1 + hh2; }; return self; }); // ExplodingBlock: destroys itself and adjacent blocks when broken var ExplodingBlock = Container.expand(function () { var self = Container.call(this); self.blockType = 'exploding'; self.breakBlock = function () { // Explode: destroy self and all blocks within 1.5x width/height if (typeof blocks !== "undefined" && blocks && blocks.length) { for (var i = blocks.length - 1; i >= 0; i--) { var b = blocks[i]; if (b === self) continue; var dx = Math.abs(b.x - self.x); var dy = Math.abs(b.y - self.y); var w = (self.blockSprite.width + b.blockSprite.width) * 0.75; var h = (self.blockSprite.height + b.blockSprite.height) * 0.75; if (dx < w && dy < h) { b.breakBlock && b.breakBlock(); if (b.parent) b.parent.removeChild(b); blocks.splice(i, 1); } } } // Animate self tween(self, { alpha: 0, scaleX: 1.7, scaleY: 1.7 }, { duration: 220, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); } }); }; return self; }); // MovingBlock: moves left/right or up/down var MovingBlock = Container.expand(function () { var self = Container.call(this); self.blockType = 'moving'; self.moveDir = Math.random() < 0.5 ? 'x' : 'y'; // randomize direction self.moveRange = 120 + Math.random() * 180; // px self.moveSpeed = 2 + Math.random() * 2; // px per frame self.moveOrigin = { x: 0, y: 0 }; self.movePhase = Math.random() * Math.PI * 2; self.update = function () { if (!self.moveOrigin.x && !self.moveOrigin.y) { self.moveOrigin.x = self.x; self.moveOrigin.y = self.y; } var t = LK.ticks * self.moveSpeed * 0.01 + self.movePhase; if (self.moveDir === 'x') { self.x = self.moveOrigin.x + Math.sin(t) * self.moveRange; } else { self.y = self.moveOrigin.y + Math.sin(t) * self.moveRange; } }; return self; }); // Paddle class var Paddle = Container.expand(function () { var self = Container.call(this); var paddleSprite = self.attachAsset('paddle', { anchorX: 0.5, anchorY: 0.5 }); // Use slightly larger hitbox for collision, but keep visual size for rendering self.width = paddleSprite.width; self.height = paddleSprite.height; // For hitbox, add a small margin to the top and bottom to ensure ball is always caught self.hitboxMarginY = 18; // pixels, adjust as needed for best feel self.getHitbox = function () { return { left: self.x - self.width / 2, right: self.x + self.width / 2, top: self.y - self.height / 2 - self.hitboxMarginY, bottom: self.y + self.height / 2 + self.hitboxMarginY }; }; self.moveTo = function (x) { // Clamp paddle within game bounds (leave 40px margin) var minX = self.width / 2 + 40; var maxX = 2048 - self.width / 2 - 40; self.x = Math.max(minX, Math.min(maxX, x)); }; return self; }); // PowerUp class var PowerUp = Container.expand(function () { var self = Container.call(this); var powerSprite = self.attachAsset('powerup', { anchorX: 0.5, anchorY: 0.5 }); self.type = 'expand'; // 'expand', 'shrink', 'life', 'sticky' self.vy = 10; self.update = function () { self.y += self.vy; }; return self; }); // --- PowerupBall1, PowerupBall2, PowerupBall3: behave like balls but are powerups --- var PowerupBall1 = Container.expand(function () { var self = Container.call(this); var sprite = self.attachAsset('ball', { anchorX: 0.5, anchorY: 0.5, tint: 0xffa500 // orange }); self.radius = sprite.width / 2; self.vx = (Math.random() - 0.5) * 18; self.vy = 14 + Math.random() * 6; self.type = 'powerupball1'; self.update = function () { self.x += self.vx; self.y += self.vy; // --- Wall bounce tracking for powerup balls --- if (typeof self.leftWallHits === "undefined") self.leftWallHits = 0; if (typeof self.rightWallHits === "undefined") self.rightWallHits = 0; if (typeof self.wallBouncesNoBlock === "undefined") self.wallBouncesNoBlock = 0; if (typeof self.hasHitBlockSinceReset === "undefined") self.hasHitBlockSinceReset = false; var hitWall = false; // Prevent ball from leaving left, right, or top of the screen if (self.x - self.radius < 0) { self.x = self.radius; self.vx = -self.vx; self.leftWallHits++; hitWall = true; } if (self.x + self.radius > 2048) { self.x = 2048 - self.radius; self.vx = -self.vx; self.rightWallHits++; hitWall = true; } if (hitWall) { if (!self.hasHitBlockSinceReset) { self.wallBouncesNoBlock++; } } // If left or right wall hit more than 15 times without hitting any block, return ball to player if (self.wallBouncesNoBlock >= 15 && typeof paddle !== "undefined" && paddle) { self.x = paddle.x; self.y = paddle.y - paddle.height / 2 - self.radius - 10; self.vx = 0; self.vy = 0; self.sticky = true; self.leftWallHits = 0; self.rightWallHits = 0; self.wallBouncesNoBlock = 0; self.hasHitBlockSinceReset = false; } if (self.y - self.radius < 0) { self.y = self.radius; self.vy = -self.vy; LK.getSound('hit').play(); } // Bounce off paddle if (typeof paddle !== "undefined" && paddle && typeof paddle.getHitbox === "function") { var hitbox = paddle.getHitbox(); if (self.y + self.radius >= hitbox.top && self.y - self.radius <= hitbox.bottom && self.x + self.radius >= hitbox.left && self.x - self.radius <= hitbox.right && self.vy > 0) { // Reflect ball, angle based on where it hit the paddle var rel = (self.x - paddle.x) / (paddle.width / 2 * paddle.scaleX); var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg var speed = Math.sqrt(self.vx * self.vx + self.vy * self.vy); self.vx = Math.cos(angle) * speed; self.vy = Math.sin(angle) * speed; self.y = paddle.y - paddle.height / 2 - self.radius - 2; if (self.y - self.radius < 0) { self.y = self.radius; if (self.vy < 0) self.vy = Math.abs(self.vy); } LK.getSound('hit').play(); } } // Bounce off blocks if (typeof blocks !== "undefined" && blocks && blocks.length) { for (var i = 0; i < blocks.length; i++) { var block = blocks[i]; if (block && self.intersects && self.intersects(block)) { var overlapX = Math.abs(self.x - block.x) - (block.blockSprite.width / 2 + self.radius); var overlapY = Math.abs(self.y - block.y) - (block.blockSprite.height / 2 + self.radius); if (overlapX > overlapY) { self.vx = -self.vx; } else { self.vy = -self.vy; } // Damage block like main ball if (typeof block.hp !== "undefined") { if (self.fireball || typeof paddle !== "undefined" && paddle && paddle.laserBeam) { block.hp = 0; if (block.updateHpBar) block.updateHpBar(); block.alpha = 0; block.scaleX = block.scaleY = 1.5; block.destroy(); if (block.parent) block.parent.removeChild(block); blocks.splice(i, 1); i--; } else { block.hp--; if (block.updateHpBar) block.updateHpBar(); if (block.hp <= 0) { block.breakBlock(); blocks.splice(i, 1); i--; // adjust index after removal } } } LK.getSound('hit').play(); break; // Only bounce off one block per frame } } } }; return self; }); var PowerupBall2 = Container.expand(function () { var self = Container.call(this); var sprite = self.attachAsset('ball', { anchorX: 0.5, anchorY: 0.5, tint: 0x00e0ff // cyan }); self.radius = sprite.width / 2; self.vx = (Math.random() - 0.5) * 16; self.vy = 12 + Math.random() * 8; self.type = 'powerupball2'; self.update = function () { self.x += self.vx; self.y += self.vy; // --- Wall bounce tracking for powerup balls --- if (typeof self.leftWallHits === "undefined") self.leftWallHits = 0; if (typeof self.rightWallHits === "undefined") self.rightWallHits = 0; if (typeof self.wallBouncesNoBlock === "undefined") self.wallBouncesNoBlock = 0; if (typeof self.hasHitBlockSinceReset === "undefined") self.hasHitBlockSinceReset = false; var hitWall = false; // Prevent ball from leaving left, right, or top of the screen if (self.x - self.radius < 0) { self.x = self.radius; self.vx = -self.vx; self.leftWallHits++; hitWall = true; } if (self.x + self.radius > 2048) { self.x = 2048 - self.radius; self.vx = -self.vx; self.rightWallHits++; hitWall = true; } if (hitWall) { if (!self.hasHitBlockSinceReset) { self.wallBouncesNoBlock++; } } // If left or right wall hit more than 15 times without hitting any block, return ball to player if (self.wallBouncesNoBlock >= 15 && typeof paddle !== "undefined" && paddle) { self.x = paddle.x; self.y = paddle.y - paddle.height / 2 - self.radius - 10; self.vx = 0; self.vy = 0; self.sticky = true; self.leftWallHits = 0; self.rightWallHits = 0; self.wallBouncesNoBlock = 0; self.hasHitBlockSinceReset = false; } if (self.y - self.radius < 0) { self.y = self.radius; self.vy = -self.vy; LK.getSound('hit').play(); } // Bounce off paddle if (typeof paddle !== "undefined" && paddle && typeof paddle.getHitbox === "function") { var hitbox = paddle.getHitbox(); if (self.y + self.radius >= hitbox.top && self.y - self.radius <= hitbox.bottom && self.x + self.radius >= hitbox.left && self.x - self.radius <= hitbox.right && self.vy > 0) { // Reflect ball, angle based on where it hit the paddle var rel = (self.x - paddle.x) / (paddle.width / 2 * paddle.scaleX); var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg var speed = Math.sqrt(self.vx * self.vx + self.vy * self.vy); self.vx = Math.cos(angle) * speed; self.vy = Math.sin(angle) * speed; self.y = paddle.y - paddle.height / 2 - self.radius - 2; if (self.y - self.radius < 0) { self.y = self.radius; if (self.vy < 0) self.vy = Math.abs(self.vy); } LK.getSound('hit').play(); } } // Bounce off blocks if (typeof blocks !== "undefined" && blocks && blocks.length) { for (var i = 0; i < blocks.length; i++) { var block = blocks[i]; if (block && self.intersects && self.intersects(block)) { var overlapX = Math.abs(self.x - block.x) - (block.blockSprite.width / 2 + self.radius); var overlapY = Math.abs(self.y - block.y) - (block.blockSprite.height / 2 + self.radius); if (overlapX > overlapY) { self.vx = -self.vx; } else { self.vy = -self.vy; } // Damage block like main ball if (typeof block.hp !== "undefined") { if (self.fireball || typeof paddle !== "undefined" && paddle && paddle.laserBeam) { block.hp = 0; if (block.updateHpBar) block.updateHpBar(); block.alpha = 0; block.scaleX = block.scaleY = 1.5; block.destroy(); if (block.parent) block.parent.removeChild(block); blocks.splice(i, 1); i--; } else { block.hp--; if (block.updateHpBar) block.updateHpBar(); if (block.hp <= 0) { block.breakBlock(); blocks.splice(i, 1); i--; // adjust index after removal } } } LK.getSound('hit').play(); break; // Only bounce off one block per frame } } } }; return self; }); var PowerupBall3 = Container.expand(function () { var self = Container.call(this); var sprite = self.attachAsset('ball', { anchorX: 0.5, anchorY: 0.5, tint: 0x8dff00 // lime }); self.radius = sprite.width / 2; self.vx = (Math.random() - 0.5) * 20; self.vy = 10 + Math.random() * 10; self.type = 'powerupball3'; self.update = function () { self.x += self.vx; self.y += self.vy; // --- Wall bounce tracking for powerup balls --- if (typeof self.leftWallHits === "undefined") self.leftWallHits = 0; if (typeof self.rightWallHits === "undefined") self.rightWallHits = 0; if (typeof self.wallBouncesNoBlock === "undefined") self.wallBouncesNoBlock = 0; if (typeof self.hasHitBlockSinceReset === "undefined") self.hasHitBlockSinceReset = false; var hitWall = false; // Prevent ball from leaving left, right, or top of the screen if (self.x - self.radius < 0) { self.x = self.radius; self.vx = -self.vx; self.leftWallHits++; hitWall = true; } if (self.x + self.radius > 2048) { self.x = 2048 - self.radius; self.vx = -self.vx; self.rightWallHits++; hitWall = true; } if (hitWall) { if (!self.hasHitBlockSinceReset) { self.wallBouncesNoBlock++; } } // If left or right wall hit more than 15 times without hitting any block, return ball to player if (self.wallBouncesNoBlock >= 15 && typeof paddle !== "undefined" && paddle) { self.x = paddle.x; self.y = paddle.y - paddle.height / 2 - self.radius - 10; self.vx = 0; self.vy = 0; self.sticky = true; self.leftWallHits = 0; self.rightWallHits = 0; self.wallBouncesNoBlock = 0; self.hasHitBlockSinceReset = false; } if (self.y - self.radius < 0) { self.y = self.radius; self.vy = -self.vy; LK.getSound('hit').play(); } // Bounce off paddle if (typeof paddle !== "undefined" && paddle && typeof paddle.getHitbox === "function") { var hitbox = paddle.getHitbox(); if (self.y + self.radius >= hitbox.top && self.y - self.radius <= hitbox.bottom && self.x + self.radius >= hitbox.left && self.x - self.radius <= hitbox.right && self.vy > 0) { // Reflect ball, angle based on where it hit the paddle var rel = (self.x - paddle.x) / (paddle.width / 2 * paddle.scaleX); var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg var speed = Math.sqrt(self.vx * self.vx + self.vy * self.vy); self.vx = Math.cos(angle) * speed; self.vy = Math.sin(angle) * speed; self.y = paddle.y - paddle.height / 2 - self.radius - 2; if (self.y - self.radius < 0) { self.y = self.radius; if (self.vy < 0) self.vy = Math.abs(self.vy); } LK.getSound('hit').play(); } } // Bounce off blocks if (typeof blocks !== "undefined" && blocks && blocks.length) { for (var i = 0; i < blocks.length; i++) { var block = blocks[i]; if (block && self.intersects && self.intersects(block)) { var overlapX = Math.abs(self.x - block.x) - (block.blockSprite.width / 2 + self.radius); var overlapY = Math.abs(self.y - block.y) - (block.blockSprite.height / 2 + self.radius); if (overlapX > overlapY) { self.vx = -self.vx; } else { self.vy = -self.vy; } // Damage block like main ball if (typeof block.hp !== "undefined") { if (self.fireball || typeof paddle !== "undefined" && paddle && paddle.laserBeam) { block.hp = 0; if (block.updateHpBar) block.updateHpBar(); block.alpha = 0; block.scaleX = block.scaleY = 1.5; block.destroy(); if (block.parent) block.parent.removeChild(block); blocks.splice(i, 1); i--; } else { block.hp--; if (block.updateHpBar) block.updateHpBar(); if (block.hp <= 0) { block.breakBlock(); blocks.splice(i, 1); i--; // adjust index after removal } } } LK.getSound('hit').play(); break; // Only bounce off one block per frame } } } }; return self; }); // ShieldedBlock: cannot be broken until another block is destroyed var ShieldedBlock = Container.expand(function () { var self = Container.call(this); self.blockType = 'shielded'; self.shielded = true; self.setHp = function (hp, maxHp) { self.hp = hp; self.maxHp = maxHp || hp; self.updateHpBar(); // Tint to indicate shield if (self.shielded) { self.blockSprite.tint = 0x888888; } else { self.blockSprite.tint = 0xffffff; } }; self.breakBlock = function () { if (self.shielded) { // Flash to indicate shield LK.effects.flashObject(self, 0x00e0ff, 300); return; } // Normal break tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181c2c }); /**** * Game Code ****/ // Sounds // Power-up // Block colors // Paddle // Ball // Game variables function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } var paddle = null, ball = null, blocks = [], powerups = [], powerupBalls = []; // holds PowerupBall1/2/3 instances if (!window.extraBalls || !Array.isArray(window.extraBalls)) window.extraBalls = []; if (!window.activePowerups || !Array.isArray(window.activePowerups)) window.activePowerups = []; if (!window.powerupTimers || !Array.isArray(window.powerupTimers)) window.powerupTimers = []; if (!window.powerupIcons || !Array.isArray(window.powerupIcons)) window.powerupIcons = []; var lives = 3; var level = 1; var isBallLaunched = false; var dragPaddle = false; // --- Ava: Variables for approaching pattern logic --- var patternYOffset = 0; // How much the pattern has moved down toward the player var patternApproachStep = 120; // How much to move down after each lost life (pixels) var patternApproachMax = 0; // Will be set after blocks are spawned, so pattern never goes below paddle // Always reset score to zero at the start of each game and persist it var score = 0; storage.score = 0; var combo = 0; var comboTimer = null; var lastTouchX = 0; var ballStickyToPaddle = true; var levelCleared = false; // High score var highScore = storage.highScore || 0; // GUI var scoreTxt = new Text2('0', { size: 100, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var highScoreTxt = new Text2('High: ' + highScore, { size: 60, fill: 0xFFE066 }); highScoreTxt.anchor.set(0.5, 0); highScoreTxt.y = 90; LK.gui.top.addChild(highScoreTxt); var livesTxt = new Text2('♥♥♥', { size: 80, fill: 0xFF5E5E }); livesTxt.anchor.set(1, 0); LK.gui.topRight.addChild(livesTxt); var levelTxt = new Text2('Level 1', { size: 70, fill: 0xFFE066 }); levelTxt.anchor.set(0, 0); LK.gui.topLeft.addChild(levelTxt); // Helper: update GUI function updateGUI() { scoreTxt.setText(score); storage.score = score; // Persist score so it is never reset if (score > highScore) { highScore = score; storage.highScore = highScore; } highScoreTxt.setText('High: ' + highScore); var hearts = ''; for (var i = 0; i < lives; i++) hearts += '♥'; livesTxt.setText(hearts); // Show only current level, do not show pattern chars levelTxt.setText('Level ' + level); } // Helper: reset combo function resetCombo() { combo = 0; if (comboTimer) { LK.clearTimeout(comboTimer); comboTimer = null; } } // Helper: start combo timer function startCombo() { if (comboTimer) LK.clearTimeout(comboTimer); comboTimer = LK.setTimeout(function () { resetCombo(); }, 1200); } // Helper: spawn blocks for current level function spawnBlocks() { // Remove old blocks for (var i = 0; i < blocks.length; i++) { blocks[i].destroy(); } blocks = []; // Dynamic block pattern logic // --- Dynamically scale blocks so that all patterns always fit on screen --- // Estimate max pattern size for this level var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; var numChars = 1 + Math.floor((level - 1) / 2); numChars = Math.min(numChars, 6); // max 6 chars side by side for high difficulty var rows = 7; var colsPerChar = 5; var gapCols = 1; // 1 column gap between chars var totalCols = numChars * colsPerChar + (numChars - 1) * gapCols; // Set margins (minimum 10px), and add wallMargin to ensure blocks never touch any wall var wallMargin = 40; // Minimum distance from all walls var marginX = Math.max(10, 40 - level * 2); var marginY = Math.max(10, 40 - level * 2); // Calculate max block size so that all blocks fit horizontally and vertically, never touching any wall var maxWidth = 2048 - 2 * wallMargin; var maxHeight = 900 - 2 * wallMargin; // Allow up to 900px for block area, minus wallMargin at top and bottom var blockW = Math.floor((maxWidth - (totalCols - 1) * marginX) / totalCols); var blockH = Math.floor((maxHeight - (rows - 1) * marginY) / rows); // Clamp block size to reasonable min/max blockW = Math.max(60, Math.min(blockW, 220)); blockH = Math.max(40, Math.min(blockH, 120)); var colors = ['red', 'green', 'blue', 'yellow']; // --- New: Generate block positions in a grid to form random letters and/or numbers side by side per level --- // Returns an array of {x, y} positions for blocks, forming multiple random letters/numbers in a row function getPatternBlockPositions(level, blockW, blockH) { // Define 5x7 bitmap font for A-Z, 0-9 (1=block, 0=empty) var font = { "A": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]], "B": [[1, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 0]], "C": [[0, 1, 1, 1, 1], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [0, 1, 1, 1, 1]], "D": [[1, 1, 1, 0, 0], [1, 0, 0, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 1, 0], [1, 1, 1, 0, 0]], "E": [[1, 1, 1, 1, 1], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 1, 1, 1, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 1, 1, 1, 1]], "F": [[1, 1, 1, 1, 1], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 1, 1, 1, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0]], "G": [[0, 1, 1, 1, 1], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 1]], "H": [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]], "I": [[1, 1, 1, 1, 1], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [1, 1, 1, 1, 1]], "J": [[0, 0, 0, 1, 1], [0, 0, 0, 0, 1], [0, 0, 0, 0, 1], [0, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]], "K": [[1, 0, 0, 0, 1], [1, 0, 0, 1, 0], [1, 0, 1, 0, 0], [1, 1, 0, 0, 0], [1, 0, 1, 0, 0], [1, 0, 0, 1, 0], [1, 0, 0, 0, 1]], "L": [[1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 1, 1, 1, 1]], "M": [[1, 0, 0, 0, 1], [1, 1, 0, 1, 1], [1, 0, 1, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]], "N": [[1, 0, 0, 0, 1], [1, 1, 0, 0, 1], [1, 0, 1, 0, 1], [1, 0, 0, 1, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]], "O": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]], "P": [[1, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0]], "Q": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 1, 0, 1], [1, 0, 0, 1, 0], [0, 1, 1, 0, 1]], "R": [[1, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 0], [1, 0, 1, 0, 0], [1, 0, 0, 1, 0], [1, 0, 0, 0, 1]], "S": [[0, 1, 1, 1, 1], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [0, 1, 1, 1, 0], [0, 0, 0, 0, 1], [0, 0, 0, 0, 1], [1, 1, 1, 1, 0]], "T": [[1, 1, 1, 1, 1], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0]], "U": [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]], "V": [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 0, 1, 0], [0, 1, 0, 1, 0], [0, 0, 1, 0, 0]], "W": [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 1, 0, 1], [1, 1, 0, 1, 1], [1, 1, 0, 1, 1], [1, 0, 0, 0, 1]], "X": [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 0, 1, 0], [0, 0, 1, 0, 0], [0, 1, 0, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]], "Y": [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 0, 1, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0]], "Z": [[1, 1, 1, 1, 1], [0, 0, 0, 0, 1], [0, 0, 0, 1, 0], [0, 0, 1, 0, 0], [0, 1, 0, 0, 0], [1, 0, 0, 0, 0], [1, 1, 1, 1, 1]], "0": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 1, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]], "1": [[0, 0, 1, 0, 0], [0, 1, 1, 0, 0], [1, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [1, 1, 1, 1, 1]], "2": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [0, 0, 0, 0, 1], [0, 0, 0, 1, 0], [0, 0, 1, 0, 0], [0, 1, 0, 0, 0], [1, 1, 1, 1, 1]], "3": [[1, 1, 1, 1, 0], [0, 0, 0, 0, 1], [0, 0, 0, 1, 0], [0, 0, 1, 1, 0], [0, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]], "4": [[0, 0, 0, 1, 0], [0, 0, 1, 1, 0], [0, 1, 0, 1, 0], [1, 0, 0, 1, 0], [1, 1, 1, 1, 1], [0, 0, 0, 1, 0], [0, 0, 0, 1, 0]], "5": [[1, 1, 1, 1, 1], [1, 0, 0, 0, 0], [1, 1, 1, 1, 0], [0, 0, 0, 0, 1], [0, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]], "6": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 0], [1, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]], "7": [[1, 1, 1, 1, 1], [0, 0, 0, 0, 1], [0, 0, 0, 1, 0], [0, 0, 1, 0, 0], [0, 1, 0, 0, 0], [0, 1, 0, 0, 0], [0, 1, 0, 0, 0]], "8": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]], "9": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 1], [0, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]] }; // Decide how many chars to show side by side (1-6, more as level increases) var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; var numChars = 1 + Math.floor((level - 1) / 2); numChars = Math.min(numChars, 6); // max 6 chars side by side for high difficulty // Pick random chars for this level (can be letter or number, allow repeats) // --- Ava: Use a new random seed for every level start, so every playthrough is unique --- if (typeof window.levelRandomSeed === "undefined") window.levelRandomSeed = {}; // Use a unique random seed for each level start, based on Date.now() and Math.random() var levelSeed = Date.now() + Math.floor(Math.random() * 1000000); window.levelRandomSeed[level] = levelSeed; // Deterministic random for this level, so pattern is unique per play, but consistent for the level function seededRandom(seed) { var x = Math.sin(seed++) * 10000; return x - Math.floor(x); } var charArr = []; for (var i = 0; i < numChars; i++) { // Use the seed to pick a char, so every level start is different var idx = Math.floor(seededRandom(levelSeed + i * 997) * chars.length); charArr.push(chars[idx]); } // If level 1, always show a single letter for clarity if (level === 1) charArr = [chars[Math.floor(seededRandom(levelSeed) * chars.length)]]; // Expose charArr globally so updateGUI can always show the current level's pattern window.charArr = charArr; // If no pattern found for any char, fallback to random grid var allPatternsExist = true; for (var i = 0; i < charArr.length; i++) { if (!font[charArr[i]]) { allPatternsExist = false; break; } } if (!allPatternsExist) { // fallback: random grid var positions = []; var rows = 7, cols = 5 * numChars + (numChars - 1); // wider grid for more chars // Dynamically scale block size for fallback grid var marginX_fallback = marginX; var marginY_fallback = marginY; var wallMargin = 40; // Ensure fallback grid also never touches any wall var maxWidth_fallback = 2048 - 2 * wallMargin; var maxHeight_fallback = 900 - 2 * wallMargin; var blockW_fallback = Math.floor((maxWidth_fallback - (cols - 1) * marginX_fallback) / cols); var blockH_fallback = Math.floor((maxHeight_fallback - (rows - 1) * marginY_fallback) / rows); blockW_fallback = Math.max(60, Math.min(blockW_fallback, 220)); blockH_fallback = Math.max(40, Math.min(blockH_fallback, 120)); var totalW = cols * blockW_fallback + (cols - 1) * marginX_fallback; var totalH = rows * blockH_fallback + (rows - 1) * marginY_fallback; var startX = wallMargin + (2048 - 2 * wallMargin - totalW) / 2 + blockW_fallback / 2; var startY = 400 + wallMargin; for (var r = 0; r < rows; r++) { for (var c = 0; c < cols; c++) { if (Math.random() < 0.7) { positions.push({ x: startX + c * (blockW_fallback + marginX_fallback), y: startY + r * (blockH_fallback + marginY_fallback) }); } } } // Set global blockW/blockH for this fallback pattern so blocks are sized correctly blockW = blockW_fallback; blockH = blockH_fallback; return positions; } // Center the pattern(s) in the play area var rows = 7; var colsPerChar = 5; var gapCols = 1; // 1 column gap between chars var totalCols = numChars * colsPerChar + (numChars - 1) * gapCols; var wallMargin = 40; // Use same wall margin for all patterns var totalW = totalCols * blockW + (totalCols - 1) * marginX; var totalH = rows * blockH + (rows - 1) * marginY; var startX = wallMargin + (2048 - 2 * wallMargin - totalW) / 2 + blockW / 2; var startY = wallMargin + (900 - totalH) / 2 + 300; // 300 is the original y offset, but now ensure margin at top and bottom var positions = []; for (var ch = 0; ch < charArr.length; ch++) { var pattern = font[charArr[ch]]; for (var r = 0; r < rows; r++) { for (var c = 0; c < colsPerChar; c++) { if (pattern[r][c]) { // Calculate x offset for this char var charOffset = ch * (colsPerChar + gapCols) * (blockW + marginX); positions.push({ x: startX + charOffset + c * (blockW + marginX), y: startY + r * (blockH + marginY) }); } } } } return positions; } // Only normal blocks, no shape/orientation logic function getShapePattern(level, rows, cols) { // Not used anymore, but kept for compatibility var arr = []; for (var r = 0; r < rows; r++) { arr[r] = []; for (var c = 0; c < cols; c++) { arr[r][c] = { type: 'normal', present: true }; } } return arr; } // Animal shape patterns for higher levels function getAnimalPattern(level, rows, cols) { // Each pattern is a 2D array of 0/1, 1 means block present // Patterns: horse, donkey, giraffe, etc. // All patterns are 10x12 max, centered var patterns = []; // Horse (at, level 10+) patterns.push([[0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0], [0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], [0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0], [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]]); // Donkey (eşek, level 15+) patterns.push([[0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0], [0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], [0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0]]); // Giraffe (zürafa, level 20+) patterns.push([[0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0]]); // Add more animal patterns as needed // Pick pattern based on level if (level >= 20) return patterns[2]; if (level >= 15) return patterns[1]; if (level >= 10) return patterns[0]; return null; } // Increase rows and columns as level increases, up to a max // --- OPTIMIZATION: For level 6+, reduce number of blocks per row/col to improve performance --- var rows, cols; if (level >= 6) { rows = Math.min(2 + Math.floor(level / 3), 7); cols = Math.min(4 + Math.floor(level / 4), 9); } else { rows = Math.min(3 + Math.floor(level / 2), 10); cols = Math.min(5 + Math.floor(level / 3), 12); } // For variety, pick a pattern type based on level var patternType = level % 5; // 0: full, 1: checker, 2: pyramid, 3: gaps, 4: zigzag if (level === 1) { patternType = 0; // Force full grid for first level } // Animal pattern override for higher levels var animalPattern = getAnimalPattern(level, rows, cols); if (animalPattern) { // Use animal pattern, override rows/cols rows = animalPattern.length; cols = animalPattern[0].length; patternType = -1; // Custom // Dynamically scale block size for animal pattern var marginX_animal = marginX; var marginY_animal = marginY; var wallMargin = 40; // Ensure animal patterns never touch any wall var maxWidth_animal = 2048 - 2 * wallMargin; var maxHeight_animal = 900 - 2 * wallMargin; var blockW_animal = Math.floor((maxWidth_animal - (cols - 1) * marginX_animal) / cols); var blockH_animal = Math.floor((maxHeight_animal - (rows - 1) * marginY_animal) / rows); blockW_animal = Math.max(60, Math.min(blockW_animal, 220)); blockH_animal = Math.max(40, Math.min(blockH_animal, 120)); blockW = blockW_animal; blockH = blockH_animal; } var totalW = cols * blockW + (cols - 1) * marginX; var startX = (2048 - totalW) / 2 + blockW / 2; var startY = 300; // --- New: get block layout in a random letter/number pattern for this level --- var blockPositions = getPatternBlockPositions(level, blockW, blockH); // --- OPTIMIZATION: Batch block creation to avoid frame stutter at level start --- var blocksToAdd = []; for (var i = 0; i < blockPositions.length; i++) { // Randomly assign special block types var blockTypeRand = Math.random(); var block; if (level > 2 && blockTypeRand < 0.12) { block = new MovingBlock(); block.blockType = 'moving'; } else if (level > 3 && blockTypeRand >= 0.12 && blockTypeRand < 0.20) { block = new ExplodingBlock(); block.blockType = 'exploding'; } else if (level > 4 && blockTypeRand >= 0.20 && blockTypeRand < 0.26) { block = new ShieldedBlock(); block.blockType = 'shielded'; block.shielded = true; } else { block = new Block(); block.blockType = 'normal'; } var color = colors[(i + level) % colors.length]; block.setColor(color); block.blockSprite.width = blockW; block.blockSprite.height = blockH; block.blockSprite.rotation = 0; block.x = blockPositions[i].x; block.y = blockPositions[i].y + patternYOffset; // Apply patternYOffset if any block.originalY = blockPositions[i].y; // Store originalY for approach logic // Increase block HP as level increases (scales faster with level) // Difficulty scaling: more chars, more HP, less margin, more blocks as level increases var hpBonus = Math.floor(level / 2) + Math.floor(level / 5); block.hp = 1 + Math.floor(level / 3) + Math.floor(i / 4) + hpBonus; block.maxHp = block.hp; block.setHp(block.hp, block.maxHp); blocksToAdd.push(block); } // Add all blocks to game and blocks array in a single batch to reduce layout thrashing for (var i = 0; i < blocksToAdd.length; i++) { game.addChild(blocksToAdd[i]); blocks.push(blocksToAdd[i]); } } // Helper: spawn paddle function spawnPaddle() { if (paddle) paddle.destroy(); paddle = new Paddle(); paddle.x = 2048 / 2; paddle.y = 2732 - 180; game.addChild(paddle); } // Helper: spawn ball function spawnBall() { if (ball) ball.destroy(); ball = new Ball(); ball.x = paddle.x; ball.y = paddle.y - paddle.height / 2 - ball.radius - 10; ball.vx = 0; ball.vy = 0; ball.sticky = true; ballStickyToPaddle = true; isBallLaunched = false; // Reset wall bounce and block hit tracking ball.leftWallHits = 0; ball.rightWallHits = 0; ball.wallBouncesNoBlock = 0; ball.hasHitBlockSinceReset = false; game.addChild(ball); } // Helper: spawn powerup function spawnPowerUp(x, y) { // Limit to 5 powerups on screen at once if (!powerups || !Array.isArray(powerups)) powerups = []; if (powerups.length >= 5) return; var power = new PowerUp(); // Fun, engaging, and bug-free powerup types (50+) // Removed boring/annoying types, added creative/fun ones, and ensured all are bug-free // Increase probability of ball count powerups by duplicating them in the array var workingTypes = [ // Paddle/ball size & speed 'expand', 'shrink', 'bigball', 'smallball', // Ball count (duplicated for higher probability) 'multiball', 'multiball', 'multiball', 'ballsplit', 'ballsplit', 'ballsplit', 'ballclone', 'ballclone', 'ballclone', '+1ball', '+1ball', '+1ball', '+2ball', '+2ball', '+2ball', '+3ball', '+3ball', '+3ball', // Score 'scorex2', 'scorex3', // Block effects 'blockbomb', 'blockrowclear', 'blockcolclear', 'blockheal', 'blockrandom', 'blockmove', 'blockshrink', 'blockexpand', 'blockshield', 'blockswap', 'blockteleport', 'blockregen', 'blockexplode', 'blockinvisible', // Ball physics 'slow', 'fast', 'ballheavy', 'balllight', 'ballcurve', 'ballzigzag', 'ballteleport', 'ballinvisible', 'superball', // Paddle physics 'magnet', // Special/fun 'sticky', 'life', // Laser and fireball (now equal probability with others) 'laser', 'fireball', // Other powerups 'iceball', 'portal', 'gravity', 'mirror', 'shrinkall', 'expandall', 'swapall', 'rainbow', 'cloneblock', 'blockfall', 'blockrise', 'blockrain', 'blockquake', 'blockfreeze', 'blockflash', 'blockmirror', 'blockportal', 'blockgravity', 'blockmagnet', 'blocklaser', 'blockfire', 'blockice', 'blockghost', 'blocksuper', 'blockrandomhp', 'blockbonus', 'blocktrap', 'blockshieldall', 'blockswapall', 'blockteleportall', 'blockregenall', 'blockexplodeall']; // Pick a random working type with increased probability for ball count powerups power.type = workingTypes[Math.floor(Math.random() * workingTypes.length)]; // If x or y is undefined, spawn at a random position within the play area if (typeof x === "undefined" || typeof y === "undefined") { // Avoid top 200px and bottom 300px, and 40px margin on sides var marginX = 40; var marginYTop = 200; var marginYBottom = 300; power.x = marginX + Math.random() * (2048 - 2 * marginX); // Make powerups spawn mostly in the upper part of the playfield (80% chance) if (Math.random() < 0.8) { // Top 40% of the playfield (above the paddle area) var upperHeight = Math.floor((2732 - marginYTop - marginYBottom) * 0.4); power.y = marginYTop + Math.random() * upperHeight; } else { // Anywhere in the playfield (fallback) power.y = marginYTop + Math.random() * (2732 - marginYTop - marginYBottom); } } else { power.x = x; power.y = y; } game.addChild(power); if (!powerups || !Array.isArray(powerups)) powerups = []; powerups.push(power); } // Helper: next level function nextLevel() { level++; lives = Math.min(5, lives + 1); // Add 1 life, max 5 // --- Difficulty scaling: increase ball speed, block HP, and decrease powerup spawn interval --- if (ball) { // Increase main ball speed a bit every level, up to a max ball.speed = Math.min(22 + level * 1.2, 60); // If ball is moving, scale its velocity to new speed var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); if (speed > 0) { var scale = ball.speed / speed; ball.vx *= scale; ball.vy *= scale; } } // Store global difficulty for use in spawnBlocks and powerup logic window.difficultyLevel = level; // Decrease powerup spawn interval (min 120 ticks) game.powerupSpawnInterval = Math.max(120, 360 - level * 12); // Clear combo timer on level up if (comboTimer) { LK.clearTimeout(comboTimer); comboTimer = null; } // Remove all falling powerup balls if (!powerupBalls || !Array.isArray(powerupBalls)) powerupBalls = []; if (powerupBalls && powerupBalls.length) { for (var i = powerupBalls.length - 1; i >= 0; i--) { if (powerupBalls[i]) powerupBalls[i].destroy(); powerupBalls.splice(i, 1); } } // Remove all extra balls if (!window.extraBalls || !Array.isArray(window.extraBalls)) window.extraBalls = []; if (window.extraBalls && window.extraBalls.length) { for (var i = window.extraBalls.length - 1; i >= 0; i--) { if (window.extraBalls[i]) window.extraBalls[i].destroy(); window.extraBalls.splice(i, 1); } } updateGUI(); patternYOffset = 0; // Reset pattern approach on new level spawnBlocks(); spawnPaddle(); spawnBall(); levelCleared = false; // Set patternApproachMax so pattern never goes below paddle // (Pattern should not go below y = paddle.y - 200) patternApproachMax = Math.max(0, (paddle ? paddle.y : 2732 - 180) - 200 - 400); // 400 is the original pattern startY // Clear all powerup timers and arrays if (window.powerupTimers && Array.isArray(window.powerupTimers)) { for (var t = 0; t < window.powerupTimers.length; t++) { if (window.powerupTimers[t]) LK.clearTimeout(window.powerupTimers[t]); } window.powerupTimers = []; } if (window.activePowerups && Array.isArray(window.activePowerups)) window.activePowerups = []; if (window.powerupIcons && Array.isArray(window.powerupIcons)) { for (var i = 0; i < window.powerupIcons.length; i++) { if (window.powerupIcons[i] && typeof window.powerupIcons[i].destroy === "function") window.powerupIcons[i].destroy(); } window.powerupIcons = []; } } // Endless: Remove win condition, always go to nextLevel // Helper: lose life function loseLife() { // If any powerup ball is on screen, do not lose a life, just respawn the main ball and paddle if (!powerupBalls || !Array.isArray(powerupBalls)) powerupBalls = []; if (powerupBalls && powerupBalls.length > 0) { spawnPaddle(); spawnBall(); return; } lives--; updateGUI(); LK.getSound('lose').play(); if (lives <= 0) { if (score > highScore) { highScore = score; storage.highScore = highScore; highScoreTxt.setText('High: ' + highScore); } score = 0; storage.score = 0; // Reset score on game over // Clear all powerup timers and arrays if (window.powerupTimers && Array.isArray(window.powerupTimers)) { for (var t = 0; t < window.powerupTimers.length; t++) { if (window.powerupTimers[t]) LK.clearTimeout(window.powerupTimers[t]); } window.powerupTimers = []; } if (window.activePowerups && Array.isArray(window.activePowerups)) window.activePowerups = []; if (window.powerupIcons && Array.isArray(window.powerupIcons)) { for (var i = 0; i < window.powerupIcons.length; i++) { if (window.powerupIcons[i] && typeof window.powerupIcons[i].destroy === "function") window.powerupIcons[i].destroy(); } window.powerupIcons = []; } LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); return; } // Remove all extra balls when losing a life, but do not reduce life for losing extra balls if (!window.extraBalls || !Array.isArray(window.extraBalls)) window.extraBalls = []; if (window.extraBalls && window.extraBalls.length) { for (var i = window.extraBalls.length - 1; i >= 0; i--) { if (window.extraBalls[i]) window.extraBalls[i].destroy(); window.extraBalls.splice(i, 1); } } window.extraBalls = []; // --- Ava: Move pattern down after each lost life --- patternYOffset += patternApproachStep; if (patternYOffset > patternApproachMax) patternYOffset = patternApproachMax; // Move all blocks down by patternYOffset (relative to their original y) if (blocks && blocks.length) { for (var i = 0; i < blocks.length; i++) { if (typeof blocks[i].originalY === "undefined") { blocks[i].originalY = blocks[i].y - patternYOffset; } blocks[i].y = blocks[i].originalY + patternYOffset; } } // If pattern has reached or passed the paddle, trigger game over if (blocks && blocks.length && paddle) { // Find the lowest block var maxBlockY = -Infinity; for (var i = 0; i < blocks.length; i++) { if (blocks[i].y > maxBlockY) maxBlockY = blocks[i].y; } // If any block is at or below the top of the paddle, game over if (maxBlockY + (blocks[0].blockSprite ? blocks[0].blockSprite.height / 2 : 0) >= paddle.y - paddle.height / 2) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); return; } } spawnPaddle(); spawnBall(); } // Helper: win function winGame() { LK.effects.flashScreen(0x00ff00, 1000); // Defensive: ensure arrays exist before clearing if (!window.powerupTimers || !Array.isArray(window.powerupTimers)) window.powerupTimers = []; if (!window.activePowerups || !Array.isArray(window.activePowerups)) window.activePowerups = []; if (!window.powerupIcons || !Array.isArray(window.powerupIcons)) window.powerupIcons = []; // Clear all powerup timers and arrays for (var t = 0; t < window.powerupTimers.length; t++) { if (window.powerupTimers[t]) LK.clearTimeout(window.powerupTimers[t]); } window.powerupTimers = []; window.activePowerups = []; for (var i = 0; i < window.powerupIcons.length; i++) { if (window.powerupIcons[i] && typeof window.powerupIcons[i].destroy === "function") window.powerupIcons[i].destroy(); } window.powerupIcons = []; LK.showYouWin(); } // Initialize first level highScore = storage.highScore || 0; // Always start with score zero, persistently score = 0; storage.score = 0; highScoreTxt.setText('High: ' + highScore); window.extraBalls = []; // Clear all powerup timers and arrays at game start if (window.powerupTimers && Array.isArray(window.powerupTimers)) { for (var t = 0; t < window.powerupTimers.length; t++) { if (window.powerupTimers[t]) LK.clearTimeout(window.powerupTimers[t]); } window.powerupTimers = []; } if (window.activePowerups && Array.isArray(window.activePowerups)) window.activePowerups = []; if (window.powerupIcons && Array.isArray(window.powerupIcons)) { for (var i = 0; i < window.powerupIcons.length; i++) { if (window.powerupIcons[i] && typeof window.powerupIcons[i].destroy === "function") window.powerupIcons[i].destroy(); } window.powerupIcons = []; } spawnBlocks(); spawnPaddle(); spawnBall(); updateGUI(); // Touch controls game.down = function (x, y, obj) { // Launch ball if it's sticky and user taps in lower 40% of screen if (y > 2732 * 0.6 && ballStickyToPaddle) { // Launch at slight upward angle based on touch position var rel = (x - paddle.x) / (paddle.width / 2); var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg ball.launch(angle); isBallLaunched = true; ballStickyToPaddle = false; } }; game.move = function (x, y, obj) { // Always move paddle horizontally with finger/mouse X, no need to hold down var targetX = x; // Use a weighted average for smoothing (0.7 current, 0.3 target) if (typeof paddle.x === "number") { paddle.moveTo(paddle.x * 0.7 + targetX * 0.3); } else { paddle.moveTo(targetX); } // If ball is sticky, move it with paddle if (ballStickyToPaddle) { ball.x = paddle.x; } }; game.up = function (x, y, obj) { // No need to set dragPaddle, paddle always follows move }; // Main update loop game.update = function () { // Reset shielded unlock flag at start of update game._shieldedUnlock = false; // Ball update if (ball) ball.update(); // --- Ball-ball collision for all balls (main, extra, powerup balls) --- (function handleBallBallCollisions() { // Gather all balls: main, extra, powerup var allBalls = []; if (ball) allBalls.push(ball); if (window.extraBalls && Array.isArray(window.extraBalls)) { for (var i = 0; i < window.extraBalls.length; i++) { if (window.extraBalls[i]) allBalls.push(window.extraBalls[i]); } } if (powerupBalls && Array.isArray(powerupBalls)) { for (var i = 0; i < powerupBalls.length; i++) { if (powerupBalls[i]) allBalls.push(powerupBalls[i]); } } // For each unique pair, check collision and reflect if needed for (var i = 0; i < allBalls.length; i++) { var b1 = allBalls[i]; if (!b1) continue; if (typeof b1.lastX === "undefined") b1.lastX = b1.x; if (typeof b1.lastY === "undefined") b1.lastY = b1.y; for (var j = i + 1; j < allBalls.length; j++) { var b2 = allBalls[j]; if (!b2) continue; if (typeof b2.lastX === "undefined") b2.lastX = b2.x; if (typeof b2.lastY === "undefined") b2.lastY = b2.y; // Only check if both are moving and not sticky if (b1.sticky || b2.sticky) continue; // Use circle collision var dx = b1.x - b2.x; var dy = b1.y - b2.y; var dist = Math.sqrt(dx * dx + dy * dy); var minDist = (b1.radius || 0) + (b2.radius || 0); // Only trigger on new collision (not overlapping last frame) var lastDx = b1.lastX - b2.lastX; var lastDy = b1.lastY - b2.lastY; var lastDist = Math.sqrt(lastDx * lastDx + lastDy * lastDy); if (lastDist >= minDist && dist < minDist) { // Simple elastic collision: swap velocities along the collision axis var nx = dx / (dist || 1); var ny = dy / (dist || 1); // Project velocities onto the collision normal var p1 = b1.vx * nx + b1.vy * ny; var p2 = b2.vx * nx + b2.vy * ny; // Only swap if balls are moving toward each other if (p1 - p2 > 0) { // Swap the normal components, keep tangential var tx = -ny, ty = nx; var t1 = b1.vx * tx + b1.vy * ty; var t2 = b2.vx * tx + b2.vy * ty; // New velocities b1.vx = t1 * tx + p2 * nx; b1.vy = t1 * ty + p2 * ny; b2.vx = t2 * tx + p1 * nx; b2.vy = t2 * ty + p1 * ny; // Separate balls to avoid sticking var overlap = minDist - dist + 0.5; b1.x += nx * (overlap / 2); b1.y += ny * (overlap / 2); b2.x -= nx * (overlap / 2); b2.y -= ny * (overlap / 2); // Play hit sound LK.getSound('hit').play(); } } } // Update lastX/lastY for next frame b1.lastX = b1.x; b1.lastY = b1.y; } })(); // --- OPTIMIZATION: Only update extra balls if any exist --- if (!window.extraBalls || !Array.isArray(window.extraBalls)) window.extraBalls = []; if (window.extraBalls.length) { // Cache paddle hitbox if paddle exists and is not moving var cachedPaddleHitbox = typeof paddle !== "undefined" && paddle && typeof paddle.getHitbox === "function" ? paddle.getHitbox() : null; // --- OPTIMIZATION: For level 6+, only update every other extra ball per frame --- var ebStep = level >= 6 ? 2 : 1; for (var eb = window.extraBalls.length - 1; eb >= 0; eb -= ebStep) { var ebBall = window.extraBalls[eb]; if (ebBall && ebBall.update) ebBall.update(); // --- Track left/right wall bounces for extra balls --- if (typeof ebBall.leftWallHits === "undefined") ebBall.leftWallHits = 0; if (typeof ebBall.rightWallHits === "undefined") ebBall.rightWallHits = 0; if (typeof ebBall.wallBouncesNoBlock === "undefined") ebBall.wallBouncesNoBlock = 0; if (typeof ebBall.hasHitBlockSinceReset === "undefined") ebBall.hasHitBlockSinceReset = false; var ebHitWall = false; if (ebBall.x - ebBall.radius < 0) { ebBall.x = ebBall.radius; ebBall.vx = -ebBall.vx; LK.getSound('hit').play(); ebBall.leftWallHits++; ebHitWall = true; } if (ebBall.x + ebBall.radius > 2048) { ebBall.x = 2048 - ebBall.radius; ebBall.vx = -ebBall.vx; LK.getSound('hit').play(); ebBall.rightWallHits++; ebHitWall = true; } if (ebHitWall) { if (!ebBall.hasHitBlockSinceReset) { ebBall.wallBouncesNoBlock++; } } // If left or right wall hit more than 15 times without hitting any block, return ball to player if (ebBall.wallBouncesNoBlock >= 15) { ebBall.x = paddle.x; ebBall.y = paddle.y - paddle.height / 2 - ebBall.radius - 10; ebBall.vx = 0; ebBall.vy = 0; ebBall.sticky = true; ebBall.leftWallHits = 0; ebBall.rightWallHits = 0; ebBall.wallBouncesNoBlock = 0; ebBall.hasHitBlockSinceReset = false; } // Remove if off screen if (ebBall && ebBall.y - ebBall.radius > 2732) { ebBall.destroy(); window.extraBalls.splice(eb, 1); continue; } // Paddle collision for extra balls (skip if paddle not present) if (ebBall && cachedPaddleHitbox) { if (ebBall.y + ebBall.radius >= cachedPaddleHitbox.top && ebBall.y - ebBall.radius <= cachedPaddleHitbox.bottom && ebBall.x + ebBall.radius >= cachedPaddleHitbox.left && ebBall.x - ebBall.radius <= cachedPaddleHitbox.right && ebBall.vy > 0) { // Reflect ball, angle based on where it hit the paddle var rel = (ebBall.x - paddle.x) / (paddle.width / 2 * paddle.scaleX); var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg var speed = Math.sqrt(ebBall.vx * ebBall.vx + ebBall.vy * ebBall.vy); ebBall.vx = Math.cos(angle) * speed; ebBall.vy = Math.sin(angle) * speed; ebBall.y = paddle.y - paddle.height / 2 - ebBall.radius - 2; if (ebBall.y - ebBall.radius < 0) { ebBall.y = ebBall.radius; if (ebBall.vy < 0) ebBall.vy = Math.abs(ebBall.vy); } LK.getSound('hit').play(); } } } } // --- OPTIMIZATION: Only update powerup balls if any exist --- if (!powerupBalls || !Array.isArray(powerupBalls)) powerupBalls = []; if (powerupBalls && powerupBalls.length) { var cachedPaddleHitboxPB = typeof paddle !== "undefined" && paddle && typeof paddle.getHitbox === "function" ? paddle.getHitbox() : null; // --- OPTIMIZATION: For level 6+, only update every other powerup ball per frame --- var pbStep = level >= 6 ? 2 : 1; for (var pbIdx = powerupBalls.length - 1; pbIdx >= 0; pbIdx -= pbStep) { var pbBall = powerupBalls[pbIdx]; if (pbBall && pbBall.update) pbBall.update(); // --- Track left/right wall bounces for powerup balls --- if (typeof pbBall.leftWallHits === "undefined") pbBall.leftWallHits = 0; if (typeof pbBall.rightWallHits === "undefined") pbBall.rightWallHits = 0; if (typeof pbBall.wallBouncesNoBlock === "undefined") pbBall.wallBouncesNoBlock = 0; if (typeof pbBall.hasHitBlockSinceReset === "undefined") pbBall.hasHitBlockSinceReset = false; var pbHitWall = false; if (pbBall.x - pbBall.radius < 0) { pbBall.x = pbBall.radius; pbBall.vx = -pbBall.vx; LK.getSound('hit').play(); pbBall.leftWallHits++; pbHitWall = true; } if (pbBall.x + pbBall.radius > 2048) { pbBall.x = 2048 - pbBall.radius; pbBall.vx = -pbBall.vx; LK.getSound('hit').play(); pbBall.rightWallHits++; pbHitWall = true; } if (pbHitWall) { if (!pbBall.hasHitBlockSinceReset) { pbBall.wallBouncesNoBlock++; } } // If left or right wall hit more than 15 times without hitting any block, return ball to player if (pbBall.wallBouncesNoBlock >= 15) { pbBall.x = paddle.x; pbBall.y = paddle.y - paddle.height / 2 - pbBall.radius - 10; pbBall.vx = 0; pbBall.vy = 0; pbBall.sticky = true; pbBall.leftWallHits = 0; pbBall.rightWallHits = 0; pbBall.wallBouncesNoBlock = 0; pbBall.hasHitBlockSinceReset = false; } // Remove if off screen if (pbBall && pbBall.y - pbBall.radius > 2732) { pbBall.destroy(); powerupBalls.splice(pbIdx, 1); continue; } // Paddle collision for powerup balls (skip if paddle not present) if (pbBall && cachedPaddleHitboxPB) { if (pbBall.y + pbBall.radius >= cachedPaddleHitboxPB.top && pbBall.y - pbBall.radius <= cachedPaddleHitboxPB.bottom && pbBall.x + pbBall.radius >= cachedPaddleHitboxPB.left && pbBall.x - pbBall.radius <= cachedPaddleHitboxPB.right && pbBall.vy > 0) { // Reflect ball, angle based on where it hit the paddle var rel = (pbBall.x - paddle.x) / (paddle.width / 2 * paddle.scaleX); var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg var speed = Math.sqrt(pbBall.vx * pbBall.vx + pbBall.vy * pbBall.vy); pbBall.vx = Math.cos(angle) * speed; pbBall.vy = Math.sin(angle) * speed; pbBall.y = paddle.y - paddle.height / 2 - pbBall.radius - 2; if (pbBall.y - pbBall.radius < 0) { pbBall.y = pbBall.radius; if (pbBall.vy < 0) pbBall.vy = Math.abs(pbBall.vy); } LK.getSound('hit').play(); } } } } // PowerUp update // --- OPTIMIZATION: For level 6+, only update every other powerup per frame --- var powerupStep = level >= 6 ? 2 : 1; for (var i = powerups.length - 1; i >= 0; i -= powerupStep) { var p = powerups[i]; // OPTIMIZATION: Skip destroyed/invalid powerups immediately if (!p || typeof p.update !== "function" || p.destroyed) { powerups.splice(i, 1); continue; } // Defensive: Only call update if p is valid and not destroyed try { if (p && typeof p.update === "function" && !p.destroyed) { p.update(); } } catch (e) { // Remove problematic powerup to prevent further errors powerups.splice(i, 1); continue; } // If off screen if (p.y > 2732 + 100) { p.destroy(); powerups.splice(i, 1); continue; } // Paddle or ball collision var collected = false; // Use intersects for paddle collision to ensure all powerups are collected by paddle if (paddle && typeof p.intersects === "function" && p.intersects(paddle)) { collected = true; } // Check main ball if (!collected && ball && p.intersects(ball)) { collected = true; } // Check extra balls if (!collected && window.extraBalls && window.extraBalls.length) { for (var eb = 0; eb < window.extraBalls.length; eb++) { if (p.intersects(window.extraBalls[eb])) { collected = true; break; } } } // No level restriction: powerups are always collectible if (collected) { // --- Powerup balls update and collection --- } // --- OPTIMIZATION: Remove powerup ball update from this loop, handled above --- if (collected) { // Show floating text for powerup type var label = new Text2(p.type.toUpperCase(), { size: 70, fill: 0xFFE066, font: "Arial Black" }); label.anchor.set(0.5, 1); label.x = p.x; label.y = p.y - 60; game.addChild(label); tween(label, { y: label.y - 120, alpha: 0 }, { duration: 1200, easing: tween.easeOut, onFinish: function onFinish() { label.destroy(); } }); // Apply powerup LK.getSound('powerup').play(); // --- Powerup duration tracking and display --- if (!window.activePowerups || !Array.isArray(window.activePowerups)) window.activePowerups = []; if (!window.powerupTimers || !Array.isArray(window.powerupTimers)) window.powerupTimers = []; if (!window.powerupIcons || !Array.isArray(window.powerupIcons)) window.powerupIcons = []; // List of timed powerups and their durations (ms) var timedPowerupDurations = { expand: 6000, shrink: 6000, slow: 6000, fast: 6000, bigball: 6000, smallball: 6000, ballheavy: 6000, balllight: 6000, blockshrink: 6000, blockexpand: 6000, ghost: 6000, superball: 6000, paddleghost: 6000, // reverse removed blockinvisible: 6000, ballinvisible: 6000, magnet: 4000, ballcurve: 4000, ballzigzag: 4000 }; // If this is a timed powerup, add to activePowerups if (timedPowerupDurations[p.type]) { var duration = timedPowerupDurations[p.type]; var now = Date.now(); // Remove any previous of same type (refresh) for (var ap = window.activePowerups.length - 1; ap >= 0; ap--) { if (window.activePowerups[ap].type === p.type) { window.activePowerups.splice(ap, 1); if (window.powerupTimers[ap]) { LK.clearTimeout(window.powerupTimers[ap]); window.powerupTimers.splice(ap, 1); } if (window.powerupIcons[ap]) { window.powerupIcons[ap].destroy(); window.powerupIcons.splice(ap, 1); } } } window.activePowerups.push({ type: p.type, end: now + duration }); // Add icon to GUI var icon = new Text2(p.type[0].toUpperCase(), { size: 70, fill: 0xFFE066, font: "Arial Black" }); icon.anchor.set(0.5, 0.5); // Place icons in a row at top center, below score icon.x = 2048 / 2 - (window.activePowerups.length - 1) * 50 + (window.activePowerups.length - 1) * 100 / 2 + (window.activePowerups.length - 1) * 0; icon.y = 170; LK.gui.top.addChild(icon); window.powerupIcons.push(icon); // Timer to remove from activePowerups and GUI (function (type, icon) { var idx = window.activePowerups.length - 1; var timer = LK.setTimeout(function () { // Remove from arrays for (var ap = window.activePowerups.length - 1; ap >= 0; ap--) { if (window.activePowerups[ap].type === type) { window.activePowerups.splice(ap, 1); if (window.powerupTimers[ap]) window.powerupTimers.splice(ap, 1); if (window.powerupIcons[ap]) { window.powerupIcons[ap].destroy(); window.powerupIcons.splice(ap, 1); } } } }, duration); window.powerupTimers.push(timer); })(p.type, icon); } // 50 powerup effects, only working ones kept if (collected) { if (p.type === '+1ball') { // Add 1 extra ball if (!window.extraBalls) window.extraBalls = []; if (window.extraBalls.length >= 4) return; var newBall = new Ball(); newBall.x = ball.x; newBall.y = ball.y; // Give a random direction, but not too vertical var angle = -Math.PI / 4 + Math.random() * (Math.PI / 2); // -45 to +45 deg var speed = ball.speed || 22; newBall.vx = Math.cos(angle) * speed; newBall.vy = Math.sin(angle) * speed; newBall.sticky = false; game.addChild(newBall); window.extraBalls.push(newBall); } else if (p.type === '+2ball') { // Add 2 extra balls if (!window.extraBalls) window.extraBalls = []; var ballsToAdd = Math.min(2, 4 - window.extraBalls.length); for (var b = 0; b < ballsToAdd; b++) { var newBall = new Ball(); newBall.x = ball.x; newBall.y = ball.y; // Spread angles var angle = -Math.PI / 3 + b * (Math.PI / 3); // -60, 0 deg var speed = ball.speed || 22; newBall.vx = Math.cos(angle) * speed; newBall.vy = Math.sin(angle) * speed; newBall.sticky = false; game.addChild(newBall); window.extraBalls.push(newBall); } } else if (p.type === '+3ball') { // Add 3 extra balls if (!window.extraBalls) window.extraBalls = []; var ballsToAdd = Math.min(3, 4 - window.extraBalls.length); for (var b = 0; b < ballsToAdd; b++) { var newBall = new Ball(); newBall.x = ball.x; newBall.y = ball.y; // Spread angles var angle = -Math.PI / 3 + b * (Math.PI / 3) / (ballsToAdd > 1 ? ballsToAdd - 1 : 1); // -60, 0, +60 deg var speed = ball.speed || 22; newBall.vx = Math.cos(angle) * speed; newBall.vy = Math.sin(angle) * speed; newBall.sticky = false; game.addChild(newBall); window.extraBalls.push(newBall); } } else if (p.type === 'expand') { tween(paddle, { scaleX: 1.5 }, { duration: 300, easing: tween.easeOut }); LK.setTimeout(function () { tween(paddle, { scaleX: 1 }, { duration: 300, easing: tween.easeIn }); }, 6000); } else if (p.type === 'shrink') { tween(paddle, { scaleX: 0.7 }, { duration: 300, easing: tween.easeOut }); LK.setTimeout(function () { tween(paddle, { scaleX: 1 }, { duration: 300, easing: tween.easeIn }); }, 6000); } else if (p.type === 'life') { lives = Math.min(5, lives + 1); updateGUI(); } else if (p.type === 'sticky') { ball.sticky = true; ballStickyToPaddle = true; ball.vx = 0; ball.vy = 0; ball.x = paddle.x; ball.y = paddle.y - paddle.height / 2 - ball.radius - 10; } else if (p.type === 'multiball') { // Double all balls on screen (main, extra, powerup balls) var allBalls = []; if (ball) allBalls.push(ball); if (window.extraBalls && Array.isArray(window.extraBalls)) { for (var i = 0; i < window.extraBalls.length; i++) { if (window.extraBalls[i]) allBalls.push(window.extraBalls[i]); } } if (powerupBalls && Array.isArray(powerupBalls)) { for (var i = 0; i < powerupBalls.length; i++) { if (powerupBalls[i]) allBalls.push(powerupBalls[i]); } } // Defensive: limit total balls to 12 var maxTotalBalls = 12; var ballsToAdd = []; for (var i = 0; i < allBalls.length; i++) { if (window.extraBalls && window.extraBalls.length + ballsToAdd.length >= maxTotalBalls - 1) break; var orig = allBalls[i]; // Create a new Ball with same position and mirrored vx var newBall = new Ball(); newBall.x = orig.x; newBall.y = orig.y; // Mirror vx, keep vy newBall.vx = -orig.vx; newBall.vy = orig.vy; newBall.sticky = false; ballsToAdd.push(newBall); } // Add new balls to game and extraBalls array for (var i = 0; i < ballsToAdd.length; i++) { game.addChild(ballsToAdd[i]); if (!window.extraBalls) window.extraBalls = []; window.extraBalls.push(ballsToAdd[i]); } } else if (p.type === 'ballsplit') { // Split ball into two, both are main balls var newBall = new Ball(); newBall.x = ball.x; newBall.y = ball.y; newBall.vx = -ball.vx; newBall.vy = ball.vy; newBall.sticky = false; game.addChild(newBall); if (!window.extraBalls) window.extraBalls = []; window.extraBalls.push(newBall); } else if (p.type === 'ballclone') { // Clone ball with same direction, as a main ball var newBall = new Ball(); newBall.x = ball.x; newBall.y = ball.y; newBall.vx = ball.vx; newBall.vy = ball.vy; newBall.sticky = false; game.addChild(newBall); if (!window.extraBalls) window.extraBalls = []; window.extraBalls.push(newBall); } else if (p.type === 'slow') { ball.speed = Math.max(10, ball.speed - 6); ball.vx *= 0.7; ball.vy *= 0.7; LK.setTimeout(function () { ball.speed = 22; }, 6000); } else if (p.type === 'fast') { ball.speed += 8; ball.vx *= 1.3; ball.vy *= 1.3; LK.setTimeout(function () { ball.speed = 22; }, 6000); } else if (p.type === 'bigball') { ball.scaleX = ball.scaleY = 1.7; LK.setTimeout(function () { ball.scaleX = ball.scaleY = 1; }, 6000); } else if (p.type === 'smallball') { ball.scaleX = ball.scaleY = 0.6; LK.setTimeout(function () { ball.scaleX = ball.scaleY = 1; }, 6000); } else if (p.type === 'scorex2') { if (!window.scorex2) window.scorex2 = 0; window.scorex2 += 1; LK.setTimeout(function () { window.scorex2 -= 1; }, 8000); } else if (p.type === 'scorex3') { if (!window.scorex3) window.scorex3 = 0; window.scorex3 += 1; LK.setTimeout(function () { window.scorex3 -= 1; }, 8000); } else if (p.type === 'blockbomb') { // Destroy a random block if (blocks.length > 0) { var idx = Math.floor(Math.random() * blocks.length); blocks[idx].breakBlock(); // Remove block from game scene to prevent ghost graphics if (blocks[idx].parent) blocks[idx].parent.removeChild(blocks[idx]); blocks.splice(idx, 1); } } else if (p.type === 'blockrowclear') { // Clear a random row if (blocks.length > 0) { var yRows = {}; for (var b = 0; b < blocks.length; b++) { var by = Math.round(blocks[b].y / 10) * 10; if (!yRows[by]) yRows[by] = []; yRows[by].push(b); } var rowKeys = Object.keys(yRows); var row = yRows[rowKeys[Math.floor(Math.random() * rowKeys.length)]]; for (var j = row.length - 1; j >= 0; j--) { blocks[row[j]].breakBlock(); // Remove block from game scene to prevent ghost graphics if (blocks[row[j]].parent) blocks[row[j]].parent.removeChild(blocks[row[j]]); blocks.splice(row[j], 1); } } } else if (p.type === 'blockcolclear') { // Clear a random column if (blocks.length > 0) { var xCols = {}; for (var b = 0; b < blocks.length; b++) { var bx = Math.round(blocks[b].x / 10) * 10; if (!xCols[bx]) xCols[bx] = []; xCols[bx].push(b); } var colKeys = Object.keys(xCols); var col = xCols[colKeys[Math.floor(Math.random() * colKeys.length)]]; for (var j = col.length - 1; j >= 0; j--) { blocks[col[j]].breakBlock(); // Remove block from game scene to prevent ghost graphics if (blocks[col[j]].parent) blocks[col[j]].parent.removeChild(blocks[col[j]]); blocks.splice(col[j], 1); } } } else if (p.type === 'ballsplit') { // Split ball into two var newBall = new Ball(); newBall.x = ball.x; newBall.y = ball.y; newBall.vx = -ball.vx; newBall.vy = ball.vy; newBall.sticky = false; game.addChild(newBall); if (!window.extraBalls) window.extraBalls = []; window.extraBalls.push(newBall); } else if (p.type === 'ballclone') { // Clone ball with same direction var newBall = new Ball(); newBall.x = ball.x; newBall.y = ball.y; newBall.vx = ball.vx; newBall.vy = ball.vy; newBall.sticky = false; game.addChild(newBall); if (!window.extraBalls) window.extraBalls = []; window.extraBalls.push(newBall); // Removed scorebonus and scorepenalty powerup effects } else if (p.type === 'blockheal') { // Heal all blocks by 1 for (var b = 0; b < blocks.length; b++) { blocks[b].hp = Math.min(blocks[b].maxHp, blocks[b].hp + 1); if (blocks[b].updateHpBar) blocks[b].updateHpBar(); } } else if (p.type === 'blockrandom') { // Randomize block colors var colors = ['red', 'green', 'blue', 'yellow']; for (var b = 0; b < blocks.length; b++) { blocks[b].setColor(colors[Math.floor(Math.random() * colors.length)]); } } else if (p.type === 'blockfreeze') { // Freeze blocks (no effect in this logic, but could be used for animation) } else if (p.type === 'blockmove') { // Move all blocks down for (var b = 0; b < blocks.length; b++) { blocks[b].y += 40; } } else if (p.type === 'blockshrink') { // Shrink all blocks for (var b = 0; b < blocks.length; b++) { blocks[b].scaleX = blocks[b].scaleY = 0.7; (function (block) { LK.setTimeout(function () { block.scaleX = block.scaleY = 1; }, 6000); })(blocks[b]); } } else if (p.type === 'blockexpand') { // Expand all blocks for (var b = 0; b < blocks.length; b++) { blocks[b].scaleX = blocks[b].scaleY = 1.3; (function (block) { LK.setTimeout(function () { block.scaleX = block.scaleY = 1; }, 6000); })(blocks[b]); } } else if (p.type === 'ballheavy') { ball.vy += 8; LK.setTimeout(function () { ball.vy -= 8; }, 6000); } else if (p.type === 'balllight') { ball.vy -= 8; LK.setTimeout(function () { ball.vy += 8; }, 6000); } else if (p.type === 'blockshield') { // Give all blocks +2 hp for (var b = 0; b < blocks.length; b++) { blocks[b].hp = Math.min(blocks[b].maxHp, blocks[b].hp + 2); if (blocks[b].updateHpBar) blocks[b].updateHpBar(); } } else if (p.type === 'blockswap') { // Swap two random blocks if (blocks.length > 1) { var i1 = Math.floor(Math.random() * blocks.length); var i2 = Math.floor(Math.random() * blocks.length); var tmpx = blocks[i1].x, tmpy = blocks[i1].y; blocks[i1].x = blocks[i2].x; blocks[i1].y = blocks[i2].y; blocks[i2].x = tmpx; blocks[i2].y = tmpy; } } else if (p.type === 'blockteleport') { // Teleport a random block to a random position if (blocks.length > 0) { var idx = Math.floor(Math.random() * blocks.length); blocks[idx].x = 400 + Math.random() * 1200; blocks[idx].y = 400 + Math.random() * 800; } } else if (p.type === 'ballteleport') { ball.x = 400 + Math.random() * 1200; ball.y = 400 + Math.random() * 800; } else if (p.type === 'blockregen') { // Regenerate a destroyed block at random if (blocks.length > 0) { var idx = Math.floor(Math.random() * blocks.length); var block = new Block(); block.setColor(blocks[idx].color); block.x = blocks[idx].x; block.y = blocks[idx].y; block.hp = block.maxHp = 2; block.setHp(2, 2); game.addChild(block); blocks.push(block); } } else if (p.type === 'blockexplode') { // Destroy 3 random blocks for (var n = 0; n < 3 && blocks.length > 0; n++) { var idx = Math.floor(Math.random() * blocks.length); blocks[idx].breakBlock(); // Remove block from game scene to prevent ghost graphics if (blocks[idx].parent) blocks[idx].parent.removeChild(blocks[idx]); blocks.splice(idx, 1); } } else if (p.type === 'magnet') { // Ball moves toward paddle for a while if (!window.magnetActive) { window.magnetActive = true; var magnetInterval = LK.setInterval(function () { if (ball && paddle) { var dx = paddle.x - ball.x; ball.vx += dx * 0.01; } }, 60); LK.setTimeout(function () { LK.clearInterval(magnetInterval); window.magnetActive = false; }, 4000); } } else if (p.type === 'laser') { // Laser powerup: show a red laser beam from paddle for a short time if (!paddle.laserBeam) { // Create a red rectangle as the laser beam var laserWidth = paddle.width * (paddle.scaleX || 1) * 0.25; var laserHeight = paddle.y - paddle.height / 2; // from paddle to very top var laser = LK.getAsset('block_red', { anchorX: 0.5, anchorY: 1, width: laserWidth, height: laserHeight, x: paddle.x, y: paddle.y - paddle.height / 2, alpha: 0.7 }); laser.zIndex = 1000; paddle.laserBeam = laser; game.addChild(laser); // Animate laser alpha in and out tween(laser, { alpha: 0.9 }, { duration: 120, easing: tween.easeIn, onFinish: function onFinish() { tween(laser, { alpha: 0.0 }, { duration: 400, delay: 400, easing: tween.easeOut, onFinish: function onFinish() { if (laser && laser.parent) laser.parent.removeChild(laser); paddle.laserBeam = null; } }); } }); // Destroy all blocks in the laser's vertical path, with explosion effect for (var i = blocks.length - 1; i >= 0; i--) { var block = blocks[i]; if (block && block.x >= paddle.x - laserWidth / 2 && block.x <= paddle.x + laserWidth / 2 && block.y < paddle.y - paddle.height / 2 // only above paddle ) { // Instantly destroy block (no animation) block.hp = 0; if (block.updateHpBar) block.updateHpBar(); block.alpha = 0; block.scaleX = block.scaleY = 2.2; block.destroy(); if (block.parent) block.parent.removeChild(block); blocks.splice(i, 1); score += 10; updateGUI(); LK.getSound('break').play(); } } } // Removed 'reverse' (ters kontrol) powerup effect } else if (p.type === 'superball') { // Ball passes through blocks for a while ball.superball = true; LK.setTimeout(function () { ball.superball = false; }, 6000); } else if (p.type === 'fireball') { // Fireball: Ball burns through blocks for a while, destroys all blocks it touches, and shows fire effect ball.fireball = true; // Add a fire visual effect to the ball if (!ball.fireEffect) { ball.fireEffect = ball.attachAsset('ball', { anchorX: 0.5, anchorY: 0.5, width: ball.width * 1.5, height: ball.height * 1.5, tint: 0xff6600, alpha: 0.5 }); ball.fireEffect.zIndex = 99; } // Animate fire effect (pulsing) if (!ball.fireTween) { ball.fireTween = tween(ball.fireEffect, { alpha: 0.2 }, { duration: 300, yoyo: true, repeat: 10, onFinish: function onFinish() { if (ball.fireEffect) ball.fireEffect.alpha = 0.5; } }); } LK.setTimeout(function () { ball.fireball = false; if (ball.fireEffect) { ball.removeChild(ball.fireEffect); ball.fireEffect = null; } if (ball.fireTween) { ball.fireTween.stop(); ball.fireTween = null; } }, 6000); } else if (p.type === 'ballcurve') { // Ball curves for a while if (!window.curveActive) { window.curveActive = true; var curveInterval = LK.setInterval(function () { if (ball) ball.vx += Math.sin(LK.ticks / 10) * 0.5; }, 60); LK.setTimeout(function () { LK.clearInterval(curveInterval); window.curveActive = false; }, 4000); } } else if (p.type === 'ballzigzag') { // Ball zigzags for a while if (!window.zigzagActive) { window.zigzagActive = true; var zigzagInterval = LK.setInterval(function () { if (ball) ball.vx += LK.ticks % 2 === 0 ? 2 : -2; }, 60); LK.setTimeout(function () { LK.clearInterval(zigzagInterval); window.zigzagActive = false; }, 4000); } } else if (p.type === 'ballrandom') { // Removed: ballrandom effect is now disabled } else if (p.type === 'blockinvisible') { // All blocks invisible for a while for (var b = 0; b < blocks.length; b++) { blocks[b].alpha = 0.1; (function (block) { LK.setTimeout(function () { block.alpha = 1; }, 6000); })(blocks[b]); } } else if (p.type === 'ballinvisible') { // All balls (main + extra) invisible for a while if (ball) { ball.alpha = 0.1; LK.setTimeout(function () { if (ball) ball.alpha = 1; }, 6000); } if (window.extraBalls && window.extraBalls.length) { for (var eb = 0; eb < window.extraBalls.length; eb++) { if (window.extraBalls[eb]) { window.extraBalls[eb].alpha = 0.1; (function (ebBall) { LK.setTimeout(function () { if (ebBall) ebBall.alpha = 1; }, 6000); })(window.extraBalls[eb]); } } } if (powerupBalls && powerupBalls.length) { for (var pb = 0; pb < powerupBalls.length; pb++) { if (powerupBalls[pb]) { powerupBalls[pb].alpha = 0.1; (function (pbBall) { LK.setTimeout(function () { if (pbBall) pbBall.alpha = 1; }, 6000); })(powerupBalls[pb]); } } } } else if (p.type === 'iceball') { if (ball) { ball.vx *= 0.7; ball.vy *= 0.7; ball.attachAsset('ball', { anchorX: 0.5, anchorY: 0.5, width: ball.width * 1.2, height: ball.height * 1.2, tint: 0x00e0ff, alpha: 0.3 }); } LK.setTimeout(function () { if (ball) { ball.vx *= 1.43; ball.vy *= 1.43; } }, 6000); } else if (p.type === 'portal') { if (blocks.length > 1) { var b1 = Math.floor(Math.random() * blocks.length); var b2 = Math.floor(Math.random() * blocks.length); if (b1 !== b2) { var tmpx = blocks[b1].x, tmpy = blocks[b1].y; blocks[b1].x = blocks[b2].x; blocks[b1].y = blocks[b2].y; blocks[b2].x = tmpx; blocks[b2].y = tmpy; } } } else if (p.type === 'gravity') { if (ball) { var origVy = ball.vy; ball.vy += 20; LK.setTimeout(function () { if (ball) ball.vy = origVy; }, 6000); } } else if (p.type === 'mirror') { if (ball) { ball.vx = -ball.vx; } } else if (p.type === 'shrinkall') { for (var b = 0; b < blocks.length; b++) { blocks[b].scaleX = blocks[b].scaleY = 0.5; (function (block) { LK.setTimeout(function () { block.scaleX = block.scaleY = 1; }, 6000); })(blocks[b]); } } else if (p.type === 'expandall') { for (var b = 0; b < blocks.length; b++) { blocks[b].scaleX = blocks[b].scaleY = 1.7; (function (block) { LK.setTimeout(function () { block.scaleX = block.scaleY = 1; }, 6000); })(blocks[b]); } } else if (p.type === 'swapall') { if (blocks.length > 1) { for (var b = 0; b < blocks.length; b += 2) { if (b + 1 < blocks.length) { var tmpx = blocks[b].x, tmpy = blocks[b].y; blocks[b].x = blocks[b + 1].x; blocks[b].y = blocks[b + 1].y; blocks[b + 1].x = tmpx; blocks[b + 1].y = tmpy; } } } } else if (p.type === 'rainbow') { var rainbowColors = [0xff0000, 0xff7f00, 0xffff00, 0x00ff00, 0x0000ff, 0x4b0082, 0x9400d3]; for (var b = 0; b < blocks.length; b++) { blocks[b].setColor(['red', 'green', 'blue', 'yellow'][Math.floor(Math.random() * 4)]); blocks[b].blockSprite.tint = rainbowColors[Math.floor(Math.random() * rainbowColors.length)]; (function (block) { LK.setTimeout(function () { block.blockSprite.tint = 0xffffff; }, 6000); })(blocks[b]); } } else if (p.type === 'cloneblock') { if (blocks.length > 0) { var idx = Math.floor(Math.random() * blocks.length); var orig = blocks[idx]; var clone = new Block(); clone.setColor(orig.color); clone.x = orig.x + 60 + Math.random() * 120; clone.y = orig.y + 60 + Math.random() * 120; clone.hp = orig.hp; clone.maxHp = orig.maxHp; clone.setHp(clone.hp, clone.maxHp); game.addChild(clone); blocks.push(clone); } } else if (p.type === 'blockfall') { for (var b = 0; b < blocks.length; b++) { blocks[b].y += 120; } } else if (p.type === 'blockrise') { for (var b = 0; b < blocks.length; b++) { blocks[b].y -= 120; } } p.destroy(); powerups.splice(i, 1); } } } // Ball physics if (ball && _typeof(ball) === "object" && !ball.sticky) { // --- Wall bounce tracking --- if (typeof ball.leftWallHits === "undefined") ball.leftWallHits = 0; if (typeof ball.rightWallHits === "undefined") ball.rightWallHits = 0; // --- Track if ball has hit a block since last paddle reset --- if (typeof ball.hasHitBlockSinceReset === "undefined") ball.hasHitBlockSinceReset = false; // --- Track consecutive left/right wall bounces without block hit --- if (typeof ball.wallBouncesNoBlock === "undefined") ball.wallBouncesNoBlock = 0; // Wall collisions var hitWall = false; if (ball.x - ball.radius < 0) { ball.x = ball.radius; ball.vx = -ball.vx; LK.getSound('hit').play(); ball.leftWallHits++; hitWall = true; } if (ball.x + ball.radius > 2048) { ball.x = 2048 - ball.radius; ball.vx = -ball.vx; LK.getSound('hit').play(); ball.rightWallHits++; hitWall = true; } if (hitWall) { // Only count if no block hit since last paddle reset if (!ball.hasHitBlockSinceReset) { ball.wallBouncesNoBlock++; } } if (ball.y - ball.radius < 0) { ball.y = ball.radius; ball.vy = -ball.vy; LK.getSound('hit').play(); } // If left or right wall hit 15 or more times without hitting any block, make ALL balls accelerate upward even faster if (ball.wallBouncesNoBlock >= 15) { var accelerateUpwardFaster = function accelerateUpwardFaster(b) { // Only accelerate if not already moving extremely fast upward if (b.vy > -Math.abs(b.speed ? b.speed * 20 : 440)) { b.vx = 0; b.vy = -Math.abs(b.speed ? b.speed * 20 : 440); // Use 20x speed or 440 as fallback b.sticky = false; } if (typeof b.leftWallHits !== "undefined") b.leftWallHits = 0; if (typeof b.rightWallHits !== "undefined") b.rightWallHits = 0; if (typeof b.wallBouncesNoBlock !== "undefined") b.wallBouncesNoBlock = 0; if (typeof b.hasHitBlockSinceReset !== "undefined") b.hasHitBlockSinceReset = false; }; // Main ball if (ball) accelerateUpwardFaster(ball); // Extra balls if (window.extraBalls && window.extraBalls.length) { for (var eb = 0; eb < window.extraBalls.length; eb++) { if (window.extraBalls[eb]) accelerateUpwardFaster(window.extraBalls[eb]); } } // Powerup balls if (powerupBalls && powerupBalls.length) { for (var pb = 0; pb < powerupBalls.length; pb++) { if (powerupBalls[pb]) accelerateUpwardFaster(powerupBalls[pb]); } } } else if (ball.wallBouncesNoBlock >= 10) { // Helper to shoot all balls upward extremely fast var shootUpward = function shootUpward(b) { // Only accelerate if not already moving extremely fast upward if (b.vy > -Math.abs(b.speed ? b.speed * 50 : 1320)) { b.vx = 0; b.vy = -Math.abs(b.speed ? b.speed * 50 : 1320); // Use 50x speed or 1320 as fallback b.sticky = false; } if (typeof b.leftWallHits !== "undefined") b.leftWallHits = 0; if (typeof b.rightWallHits !== "undefined") b.rightWallHits = 0; if (typeof b.wallBouncesNoBlock !== "undefined") b.wallBouncesNoBlock = 0; if (typeof b.hasHitBlockSinceReset !== "undefined") b.hasHitBlockSinceReset = false; }; // Main ball if (ball) shootUpward(ball); // Extra balls if (window.extraBalls && window.extraBalls.length) { for (var eb = 0; eb < window.extraBalls.length; eb++) { if (window.extraBalls[eb]) shootUpward(window.extraBalls[eb]); } } // Powerup balls if (powerupBalls && powerupBalls.length) { for (var pb = 0; pb < powerupBalls.length; pb++) { if (powerupBalls[pb]) shootUpward(powerupBalls[pb]); } } } // Paddle collision if (paddle && typeof paddle.getHitbox === "function") { var hitbox = paddle.getHitbox(); if (ball.y + ball.radius >= hitbox.top && ball.y - ball.radius <= hitbox.bottom && ball.x + ball.radius >= hitbox.left && ball.x - ball.radius <= hitbox.right && ball.vy > 0) { // Reflect ball, angle based on where it hit the paddle var rel = (ball.x - paddle.x) / (paddle.width / 2 * paddle.scaleX); var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); ball.vx = Math.cos(angle) * speed; ball.vy = Math.sin(angle) * speed; // Always place ball visually above paddle after collision ball.y = paddle.y - paddle.height / 2 - ball.radius - 2; if (ball.y + ball.radius > hitbox.top) { ball.y = paddle.y - paddle.height / 2 - ball.radius - 2; } LK.getSound('hit').play(); } } else { if (ball.y + ball.radius >= paddle.y - paddle.height / 2 && ball.y + ball.radius <= paddle.y + paddle.height / 2 && ball.x >= paddle.x - paddle.width / 2 * paddle.scaleX && ball.x <= paddle.x + paddle.width / 2 * paddle.scaleX && ball.vy > 0) { // Reflect ball, angle based on where it hit the paddle var rel = (ball.x - paddle.x) / (paddle.width / 2 * paddle.scaleX); var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); ball.vx = Math.cos(angle) * speed; ball.vy = Math.sin(angle) * speed; ball.y = paddle.y - paddle.height / 2 - ball.radius - 2; LK.getSound('hit').play(); } } // Bottom out if (ball.y - ball.radius > 2732) { // If there is at least one powerup ball on screen, continue with it as the main ball if (powerupBalls && powerupBalls.length > 0) { // Remove the main ball, but do not call loseLife ball.destroy(); ball = null; // Promote the first powerup ball to be the new main ball var promoted = powerupBalls.shift(); // Remove from game and re-add as main ball for correct update/collision if (promoted && promoted.parent) promoted.parent.removeChild(promoted); // Create a new Ball instance at the promoted ball's position and velocity ball = new Ball(); ball.x = promoted.x; ball.y = promoted.y; ball.vx = promoted.vx; ball.vy = promoted.vy; ball.sticky = false; game.addChild(ball); // Remove the promoted powerup ball promoted.destroy(); // Do not return, continue game loop } else { loseLife(); return; } } } // Block collisions if (!blocks || !Array.isArray(blocks)) blocks = []; // --- OPTIMIZATION: For level 6+, only check a subset of blocks per frame to reduce lag --- var blockCheckStep = level >= 6 ? 2 : 1; for (var i = blocks.length - 1; i >= 0; i -= blockCheckStep) { var block = blocks[i]; // OPTIMIZATION: Skip destroyed or off-screen blocks if (!block || block.destroyed || block.y < -200 || block.y > 2732 + 200) continue; // --- Call update for MovingBlock --- if (block.blockType === 'moving' && typeof block.update === "function") { block.update(); } // --- ShieldedBlock logic: only allow breaking if another block was destroyed this frame --- if (block.blockType === 'shielded' && block.shielded) { // Check if any other block was destroyed this frame if (!game._shieldedUnlock) { // Prevent breaking shielded block continue; } } var hit = false; // Check main ball if (ball && block && ball.intersects(block)) { // Fireball: instantly destroy block and keep moving if (ball.fireball) { // Burn block: show a flash and destroy instantly (no animation) if (block && block.blockSprite) { LK.effects.flashObject(block, 0xff6600, 80); } block.hp = 0; if (block.updateHpBar) block.updateHpBar(); block.alpha = 0; block.scaleX = block.scaleY = 1.5; block.destroy(); if (block.parent) block.parent.removeChild(block); blocks.splice(i, 1); combo++; startCombo(); score += 10 * combo; updateGUI(); LK.getSound('break').play(); // Power-up chance (increased drop rate) var powerupDropRate = 0.22 + Math.min(0.01 * level, 0.18); if (Math.random() < powerupDropRate) { spawnPowerUp(block.x, block.y); } hit = true; // Do not reflect ball, let it keep moving // --- Mark that a block was destroyed for shielded logic --- game._shieldedUnlock = true; } else { // Reflect ball var overlapX = Math.abs(ball.x - block.x) - (block.blockSprite.width / 2 + ball.radius); var overlapY = Math.abs(ball.y - block.y) - (block.blockSprite.height / 2 + ball.radius); if (overlapX > overlapY) { ball.vx = -ball.vx; } else { ball.vy = -ball.vy; } hit = true; } } // Check extra balls if (!hit && window.extraBalls && window.extraBalls.length) { for (var eb = 0; eb < window.extraBalls.length; eb++) { var ebBall = window.extraBalls[eb]; if (ebBall && block && ebBall.intersects(block)) { // Reflect extra ball var overlapX = Math.abs(ebBall.x - block.x) - (block.blockSprite.width / 2 + ebBall.radius); var overlapY = Math.abs(ebBall.y - block.y) - (block.blockSprite.height / 2 + ebBall.radius); if (overlapX > overlapY) { ebBall.vx = -ebBall.vx; } else { ebBall.vy = -ebBall.vy; } hit = true; break; } } } if (hit) { // Mark that ball has hit a block, so wall bounce counter pauses until next reset if (ball && typeof ball.hasHitBlockSinceReset !== "undefined") { ball.hasHitBlockSinceReset = true; ball.wallBouncesNoBlock = 0; } if (window.extraBalls && window.extraBalls.length) { for (var eb = 0; eb < window.extraBalls.length; eb++) { var ebBall = window.extraBalls[eb]; if (ebBall && typeof ebBall.hasHitBlockSinceReset !== "undefined") { ebBall.hasHitBlockSinceReset = true; ebBall.wallBouncesNoBlock = 0; } } } if (powerupBalls && powerupBalls.length) { for (var pb = 0; pb < powerupBalls.length; pb++) { var pbBall = powerupBalls[pb]; if (pbBall && typeof pbBall.hasHitBlockSinceReset !== "undefined") { pbBall.hasHitBlockSinceReset = true; pbBall.wallBouncesNoBlock = 0; } } } // Block hit // --- ShieldedBlock unlock logic --- if (block.blockType === 'shielded' && block.shielded) { // Only unlock if another block was destroyed this frame if (game._shieldedUnlock) { block.shielded = false; block.setHp(block.hp, block.maxHp); block.blockSprite.tint = 0xffffff; } else { // Can't break yet, flash LK.effects.flashObject(block, 0x00e0ff, 300); continue; } } block.hp--; if (block.updateHpBar) block.updateHpBar(); combo++; startCombo(); score += 10 * combo; updateGUI(); LK.getSound('break').play(); // Power-up chance (increased drop rate) var powerupDropRate = 0.22 + Math.min(0.01 * level, 0.18); // increases with level, max ~0.4 if (Math.random() < powerupDropRate) { spawnPowerUp(block.x, block.y); } if (block.hp <= 0) { block.breakBlock(); // Remove block from game scene to prevent ghost graphics if (block.parent) block.parent.removeChild(block); blocks.splice(i, 1); // --- Mark that a block was destroyed for shielded logic --- game._shieldedUnlock = true; } break; // Only one block per frame } } // Combo reset if no block hit for a while if (combo > 0 && !comboTimer) { resetCombo(); } // Level clear if (!levelCleared && blocks.length === 0) { levelCleared = true; // Remove all balls except the main ball if (window.extraBalls && Array.isArray(window.extraBalls)) { for (var i = window.extraBalls.length - 1; i >= 0; i--) { if (window.extraBalls[i]) window.extraBalls[i].destroy(); window.extraBalls.splice(i, 1); } } if (powerupBalls && Array.isArray(powerupBalls)) { for (var i = powerupBalls.length - 1; i >= 0; i--) { if (powerupBalls[i]) powerupBalls[i].destroy(); powerupBalls.splice(i, 1); } } if (comboTimer) { LK.clearTimeout(comboTimer); comboTimer = null; } LK.setTimeout(function () { nextLevel(); }, 1200); } // --- Random powerup spawn logic --- if (typeof game.lastPowerupSpawnTick === "undefined") { game.lastPowerupSpawnTick = LK.ticks; } if (typeof game.powerupSpawnInterval === "undefined") { // Try to spawn a powerup every 6-12 seconds (randomized), but decrease interval as level increases (min 120) game.powerupSpawnInterval = Math.max(120, 360 + Math.floor(Math.random() * 360) - level * 12); } if (typeof game.lastPowerupBallTick === "undefined") { game.lastPowerupBallTick = LK.ticks; } if (typeof game.powerupBallInterval === "undefined") { // Try to spawn a powerup ball every 8-16 seconds (randomized), but decrease interval as level increases (min 180) game.powerupBallInterval = Math.max(180, 480 + Math.floor(Math.random() * 480) - level * 16); } if (LK.ticks - game.lastPowerupSpawnTick > game.powerupSpawnInterval) { // Only spawn if there are less than 3 powerups on screen if (powerups.length < 3 && blocks.length > 0) { spawnPowerUp(); // No x/y: will spawn at random position } game.lastPowerupSpawnTick = LK.ticks; game.powerupSpawnInterval = 360 + Math.floor(Math.random() * 360); } // --- Random powerup ball spawn logic --- if (LK.ticks - game.lastPowerupBallTick > game.powerupBallInterval) { // Only spawn if there are less than 3 powerup balls on screen if (powerupBalls.length < 3) { // Pick a random type var types = [PowerupBall1, PowerupBall2, PowerupBall3]; var idx = Math.floor(Math.random() * types.length); var PowerupBallClass = types[idx]; var pb = new PowerupBallClass(); // Spawn at random X, top of screen pb.x = 100 + Math.random() * (2048 - 200); pb.y = -pb.radius - 10; game.addChild(pb); powerupBalls.push(pb); } game.lastPowerupBallTick = LK.ticks; game.powerupBallInterval = 480 + Math.floor(Math.random() * 480); } // Update powerup icon timers and show countdown for active powerups if (!window.activePowerups || !Array.isArray(window.activePowerups)) window.activePowerups = []; if (!window.powerupIcons || !Array.isArray(window.powerupIcons)) window.powerupIcons = []; if (window.activePowerups.length && window.powerupIcons.length) { var now = Date.now(); for (var i = 0; i < window.activePowerups.length; i++) { var ap = window.activePowerups[i]; var icon = window.powerupIcons[i]; if (ap && icon) { var remain = Math.max(0, Math.ceil((ap.end - now) / 1000)); // Show timer only if more than 0 seconds left, otherwise just the icon if (remain > 0) { icon.setText(ap.type[0].toUpperCase() + "\n" + remain); } else { icon.setText(ap.type[0].toUpperCase()); } // Reposition icons in a row icon.x = 2048 / 2 - (window.activePowerups.length - 1) * 50 + i * 100 / 2 + i * 0; icon.y = 170; } } } }; // Music (optional, not required by MVP, so not included) /* End of gamecode.js */
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Ball class
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballSprite = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = ballSprite.width / 2;
self.vx = 0;
self.vy = 0;
self.speed = 22; // Initial speed
self.sticky = false; // If true, ball sticks to paddle
self.launch = function (angle) {
// Launch ball at angle (in radians)
self.vx = Math.cos(angle) * self.speed;
self.vy = Math.sin(angle) * self.speed;
self.sticky = false;
};
self.update = function () {
if (!self.sticky) {
self.x += self.vx;
self.y += self.vy;
// --- Ava: Prevent main ball from slowing down too much or getting stuck in weird slow movement ---
// Clamp minimum speed for both vx and vy (except when sticky)
var minAbsV = 6; // Minimum absolute velocity for each axis
var maxAbsV = 60; // Maximum absolute velocity for each axis (defensive)
if (Math.abs(self.vx) < minAbsV) {
self.vx = (self.vx < 0 ? -1 : 1) * minAbsV;
}
if (Math.abs(self.vy) < minAbsV) {
self.vy = (self.vy < 0 ? -1 : 1) * minAbsV;
}
// Clamp to max as well (defensive)
if (Math.abs(self.vx) > maxAbsV) {
self.vx = (self.vx < 0 ? -1 : 1) * maxAbsV;
}
if (Math.abs(self.vy) > maxAbsV) {
self.vy = (self.vy < 0 ? -1 : 1) * maxAbsV;
}
// Prevent pathological near-horizontal or near-vertical movement
var minAngle = 0.18; // ~10 degrees from axis
var speed = Math.sqrt(self.vx * self.vx + self.vy * self.vy);
if (speed > 0) {
var angle = Math.atan2(self.vy, self.vx);
// If angle is too close to horizontal (0 or PI), nudge vy
if (Math.abs(Math.sin(angle)) < minAngle) {
self.vy += (self.vy < 0 ? -1 : 1) * minAbsV;
}
// If angle is too close to vertical (PI/2 or -PI/2), nudge vx
if (Math.abs(Math.cos(angle)) < minAngle) {
self.vx += (self.vx < 0 ? -1 : 1) * minAbsV;
}
}
// Prevent ball from leaving left, right, or top of the screen
if (self.x - self.radius < 0) {
self.x = self.radius;
self.vx = -self.vx;
}
if (self.x + self.radius > 2048) {
self.x = 2048 - self.radius;
self.vx = -self.vx;
}
if (self.y - self.radius < 0) {
self.y = self.radius;
self.vy = -self.vy;
}
// Laser/fireball: instantly destroy all blocks touched
if ((self.fireball || typeof paddle !== "undefined" && paddle && paddle.laserBeam) && typeof blocks !== "undefined" && blocks && blocks.length) {
for (var i = blocks.length - 1; i >= 0; i--) {
var block = blocks[i];
if (block && self.intersects && self.intersects(block)) {
block.hp = 0;
if (block.updateHpBar) block.updateHpBar();
block.alpha = 0;
block.scaleX = block.scaleY = 1.5;
block.destroy();
if (block.parent) block.parent.removeChild(block);
blocks.splice(i, 1);
}
}
}
}
};
return self;
});
// Block class
var Block = Container.expand(function () {
var self = Container.call(this);
// color: 'red', 'green', 'blue', 'yellow'
self.color = 'red';
self.hp = 1;
self.maxHp = 1;
self.hpBarBg = null;
self.hpBar = null;
self.blockType = 'normal'; // will be set by spawnBlocks
self.setColor = function (color) {
self.color = color;
if (self.blockSprite) self.removeChild(self.blockSprite);
var assetId = 'block_' + color;
self.blockSprite = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Remove old HP bars if any
if (self.hpBarBg) self.removeChild(self.hpBarBg);
if (self.hpBar) self.removeChild(self.hpBar);
// Add HP bar background (gray)
self.hpBarBg = self.attachAsset('block_red', {
anchorX: 0.5,
anchorY: 0.5,
width: self.blockSprite.width * 0.8,
height: 14,
y: -self.blockSprite.height / 2 - 18,
tint: 0x444444
});
// Add HP bar (green)
self.hpBar = self.attachAsset('block_green', {
anchorX: 0.5,
anchorY: 0.5,
width: self.blockSprite.width * 0.78,
height: 10,
y: -self.blockSprite.height / 2 - 18,
tint: 0x44ff44
});
self.hpBarBg.alpha = 0.7;
self.hpBar.alpha = 0.9;
self.hpBarBg.zIndex = 10;
self.hpBar.zIndex = 11;
self.hpBarBg.interactive = false;
self.hpBar.interactive = false;
self.addChild(self.hpBarBg);
self.addChild(self.hpBar);
self.updateHpBar();
};
self.setHp = function (hp, maxHp) {
self.hp = hp;
self.maxHp = maxHp || hp;
self.updateHpBar();
};
self.updateHpBar = function () {
if (!self.hpBar || !self.hpBarBg) return;
var ratio = Math.max(0, Math.min(1, self.hp / self.maxHp));
self.hpBar.width = self.blockSprite.width * 0.78 * ratio;
// Color changes: green > yellow > red
if (ratio > 0.66) {
self.hpBar.tint = 0x44ff44;
} else if (ratio > 0.33) {
self.hpBar.tint = 0xffe066;
} else {
self.hpBar.tint = 0xff4444;
}
self.hpBar.visible = self.hp > 0;
self.hpBarBg.visible = self.hp > 0;
};
self.breakBlock = function () {
// Animate block breaking
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
// --- HITBOX LOGIC ---
// Returns the hitbox for this block, depending on its type and current transform
self.getHitbox = function () {
var bx = self.x,
by = self.y;
var w = self.blockSprite.width * (self.scaleX || 1);
var h = self.blockSprite.height * (self.scaleY || 1);
var rot = self.blockSprite.rotation || 0;
if (self.blockType === 'round') {
var r = w / 2;
return {
type: 'circle',
x: bx,
y: by,
r: r
};
} else if (self.blockType === 'diagonal') {
var hw = w / 2,
hh = h / 2;
return {
type: 'rotbox',
x: bx,
y: by,
hw: hw,
hh: hh,
rot: rot
};
} else {
var hw = w / 2,
hh = h / 2;
return {
type: 'box',
x: bx,
y: by,
hw: hw,
hh: hh
};
}
};
// Override .intersects for block to support all hitbox types
self.intersects = function (other) {
var myHitbox = self.getHitbox();
var otherHitbox = typeof other.getHitbox === "function" ? other.getHitbox() : null;
if (!otherHitbox) {
// Fallback: use bounding box
var hw = self.blockSprite.width * (self.scaleX || 1) / 2;
var hh = self.blockSprite.height * (self.scaleY || 1) / 2;
return other.x > self.x - hw && other.x < self.x + hw && other.y > self.y - hh && other.y < self.y + hh;
}
// Circle-circle
if (myHitbox.type === 'circle' && otherHitbox.type === 'circle') {
var dx = myHitbox.x - otherHitbox.x;
var dy = myHitbox.y - otherHitbox.y;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < myHitbox.r + otherHitbox.r;
}
// Box-box (AABB)
if (myHitbox.type === 'box' && otherHitbox.type === 'box') {
return Math.abs(myHitbox.x - otherHitbox.x) < myHitbox.hw + otherHitbox.hw && Math.abs(myHitbox.y - otherHitbox.y) < myHitbox.hh + otherHitbox.hh;
}
// Rotated box - circle
if (myHitbox.type === 'rotbox' && otherHitbox.type === 'circle') {
// Rotate circle center into box's local space
var cos = Math.cos(-myHitbox.rot),
sin = Math.sin(-myHitbox.rot);
var dx = otherHitbox.x - myHitbox.x,
dy = otherHitbox.y - myHitbox.y;
var localX = dx * cos - dy * sin;
var localY = dx * sin + dy * cos;
// Clamp to box
var clampedX = Math.max(-myHitbox.hw, Math.min(myHitbox.hw, localX));
var clampedY = Math.max(-myHitbox.hh, Math.min(myHitbox.hh, localY));
var distX = localX - clampedX,
distY = localY - clampedY;
return distX * distX + distY * distY < otherHitbox.r * otherHitbox.r;
}
// Circle - rotated box (swap)
if (myHitbox.type === 'circle' && otherHitbox.type === 'rotbox') {
// Use the same logic, swap order
var cos = Math.cos(-otherHitbox.rot),
sin = Math.sin(-otherHitbox.rot);
var dx = myHitbox.x - otherHitbox.x,
dy = myHitbox.y - otherHitbox.y;
var localX = dx * cos - dy * sin;
var localY = dx * sin + dy * cos;
var clampedX = Math.max(-otherHitbox.hw, Math.min(otherHitbox.hw, localX));
var clampedY = Math.max(-otherHitbox.hh, Math.min(otherHitbox.hh, localY));
var distX = localX - clampedX,
distY = localY - clampedY;
return distX * distX + distY * distY < myHitbox.r * myHitbox.r;
}
// Rotated box - box (AABB fallback, not perfect)
if (myHitbox.type === 'rotbox' && otherHitbox.type === 'box') {
// Approximate by using AABB of rotated box
var rot = myHitbox.rot;
var hw = Math.abs(myHitbox.hw * Math.cos(rot)) + Math.abs(myHitbox.hh * Math.sin(rot));
var hh = Math.abs(myHitbox.hw * Math.sin(rot)) + Math.abs(myHitbox.hh * Math.cos(rot));
return Math.abs(myHitbox.x - otherHitbox.x) < hw + otherHitbox.hw && Math.abs(myHitbox.y - otherHitbox.y) < hh + otherHitbox.hh;
}
if (myHitbox.type === 'box' && otherHitbox.type === 'rotbox') {
// Approximate by using AABB of rotated box
var rot = otherHitbox.rot;
var hw = Math.abs(otherHitbox.hw * Math.cos(rot)) + Math.abs(otherHitbox.hh * Math.sin(rot));
var hh = Math.abs(otherHitbox.hw * Math.sin(rot)) + Math.abs(otherHitbox.hh * Math.cos(rot));
return Math.abs(myHitbox.x - otherHitbox.x) < myHitbox.hw + hw && Math.abs(myHitbox.y - otherHitbox.y) < myHitbox.hh + hh;
}
// Fallback: treat as AABB
var hw1 = myHitbox.hw || self.blockSprite.width * (self.scaleX || 1) / 2;
var hh1 = myHitbox.hh || self.blockSprite.height * (self.scaleY || 1) / 2;
var hw2 = otherHitbox.hw || (other.width ? other.width / 2 : 0);
var hh2 = otherHitbox.hh || (other.height ? other.height / 2 : 0);
return Math.abs(myHitbox.x - otherHitbox.x) < hw1 + hw2 && Math.abs(myHitbox.y - otherHitbox.y) < hh1 + hh2;
};
return self;
});
// ExplodingBlock: destroys itself and adjacent blocks when broken
var ExplodingBlock = Container.expand(function () {
var self = Container.call(this);
self.blockType = 'exploding';
self.breakBlock = function () {
// Explode: destroy self and all blocks within 1.5x width/height
if (typeof blocks !== "undefined" && blocks && blocks.length) {
for (var i = blocks.length - 1; i >= 0; i--) {
var b = blocks[i];
if (b === self) continue;
var dx = Math.abs(b.x - self.x);
var dy = Math.abs(b.y - self.y);
var w = (self.blockSprite.width + b.blockSprite.width) * 0.75;
var h = (self.blockSprite.height + b.blockSprite.height) * 0.75;
if (dx < w && dy < h) {
b.breakBlock && b.breakBlock();
if (b.parent) b.parent.removeChild(b);
blocks.splice(i, 1);
}
}
}
// Animate self
tween(self, {
alpha: 0,
scaleX: 1.7,
scaleY: 1.7
}, {
duration: 220,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// MovingBlock: moves left/right or up/down
var MovingBlock = Container.expand(function () {
var self = Container.call(this);
self.blockType = 'moving';
self.moveDir = Math.random() < 0.5 ? 'x' : 'y'; // randomize direction
self.moveRange = 120 + Math.random() * 180; // px
self.moveSpeed = 2 + Math.random() * 2; // px per frame
self.moveOrigin = {
x: 0,
y: 0
};
self.movePhase = Math.random() * Math.PI * 2;
self.update = function () {
if (!self.moveOrigin.x && !self.moveOrigin.y) {
self.moveOrigin.x = self.x;
self.moveOrigin.y = self.y;
}
var t = LK.ticks * self.moveSpeed * 0.01 + self.movePhase;
if (self.moveDir === 'x') {
self.x = self.moveOrigin.x + Math.sin(t) * self.moveRange;
} else {
self.y = self.moveOrigin.y + Math.sin(t) * self.moveRange;
}
};
return self;
});
// Paddle class
var Paddle = Container.expand(function () {
var self = Container.call(this);
var paddleSprite = self.attachAsset('paddle', {
anchorX: 0.5,
anchorY: 0.5
});
// Use slightly larger hitbox for collision, but keep visual size for rendering
self.width = paddleSprite.width;
self.height = paddleSprite.height;
// For hitbox, add a small margin to the top and bottom to ensure ball is always caught
self.hitboxMarginY = 18; // pixels, adjust as needed for best feel
self.getHitbox = function () {
return {
left: self.x - self.width / 2,
right: self.x + self.width / 2,
top: self.y - self.height / 2 - self.hitboxMarginY,
bottom: self.y + self.height / 2 + self.hitboxMarginY
};
};
self.moveTo = function (x) {
// Clamp paddle within game bounds (leave 40px margin)
var minX = self.width / 2 + 40;
var maxX = 2048 - self.width / 2 - 40;
self.x = Math.max(minX, Math.min(maxX, x));
};
return self;
});
// PowerUp class
var PowerUp = Container.expand(function () {
var self = Container.call(this);
var powerSprite = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5
});
self.type = 'expand'; // 'expand', 'shrink', 'life', 'sticky'
self.vy = 10;
self.update = function () {
self.y += self.vy;
};
return self;
});
// --- PowerupBall1, PowerupBall2, PowerupBall3: behave like balls but are powerups ---
var PowerupBall1 = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xffa500 // orange
});
self.radius = sprite.width / 2;
self.vx = (Math.random() - 0.5) * 18;
self.vy = 14 + Math.random() * 6;
self.type = 'powerupball1';
self.update = function () {
self.x += self.vx;
self.y += self.vy;
// --- Wall bounce tracking for powerup balls ---
if (typeof self.leftWallHits === "undefined") self.leftWallHits = 0;
if (typeof self.rightWallHits === "undefined") self.rightWallHits = 0;
if (typeof self.wallBouncesNoBlock === "undefined") self.wallBouncesNoBlock = 0;
if (typeof self.hasHitBlockSinceReset === "undefined") self.hasHitBlockSinceReset = false;
var hitWall = false;
// Prevent ball from leaving left, right, or top of the screen
if (self.x - self.radius < 0) {
self.x = self.radius;
self.vx = -self.vx;
self.leftWallHits++;
hitWall = true;
}
if (self.x + self.radius > 2048) {
self.x = 2048 - self.radius;
self.vx = -self.vx;
self.rightWallHits++;
hitWall = true;
}
if (hitWall) {
if (!self.hasHitBlockSinceReset) {
self.wallBouncesNoBlock++;
}
}
// If left or right wall hit more than 15 times without hitting any block, return ball to player
if (self.wallBouncesNoBlock >= 15 && typeof paddle !== "undefined" && paddle) {
self.x = paddle.x;
self.y = paddle.y - paddle.height / 2 - self.radius - 10;
self.vx = 0;
self.vy = 0;
self.sticky = true;
self.leftWallHits = 0;
self.rightWallHits = 0;
self.wallBouncesNoBlock = 0;
self.hasHitBlockSinceReset = false;
}
if (self.y - self.radius < 0) {
self.y = self.radius;
self.vy = -self.vy;
LK.getSound('hit').play();
}
// Bounce off paddle
if (typeof paddle !== "undefined" && paddle && typeof paddle.getHitbox === "function") {
var hitbox = paddle.getHitbox();
if (self.y + self.radius >= hitbox.top && self.y - self.radius <= hitbox.bottom && self.x + self.radius >= hitbox.left && self.x - self.radius <= hitbox.right && self.vy > 0) {
// Reflect ball, angle based on where it hit the paddle
var rel = (self.x - paddle.x) / (paddle.width / 2 * paddle.scaleX);
var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg
var speed = Math.sqrt(self.vx * self.vx + self.vy * self.vy);
self.vx = Math.cos(angle) * speed;
self.vy = Math.sin(angle) * speed;
self.y = paddle.y - paddle.height / 2 - self.radius - 2;
if (self.y - self.radius < 0) {
self.y = self.radius;
if (self.vy < 0) self.vy = Math.abs(self.vy);
}
LK.getSound('hit').play();
}
}
// Bounce off blocks
if (typeof blocks !== "undefined" && blocks && blocks.length) {
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
if (block && self.intersects && self.intersects(block)) {
var overlapX = Math.abs(self.x - block.x) - (block.blockSprite.width / 2 + self.radius);
var overlapY = Math.abs(self.y - block.y) - (block.blockSprite.height / 2 + self.radius);
if (overlapX > overlapY) {
self.vx = -self.vx;
} else {
self.vy = -self.vy;
}
// Damage block like main ball
if (typeof block.hp !== "undefined") {
if (self.fireball || typeof paddle !== "undefined" && paddle && paddle.laserBeam) {
block.hp = 0;
if (block.updateHpBar) block.updateHpBar();
block.alpha = 0;
block.scaleX = block.scaleY = 1.5;
block.destroy();
if (block.parent) block.parent.removeChild(block);
blocks.splice(i, 1);
i--;
} else {
block.hp--;
if (block.updateHpBar) block.updateHpBar();
if (block.hp <= 0) {
block.breakBlock();
blocks.splice(i, 1);
i--; // adjust index after removal
}
}
}
LK.getSound('hit').play();
break; // Only bounce off one block per frame
}
}
}
};
return self;
});
var PowerupBall2 = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x00e0ff // cyan
});
self.radius = sprite.width / 2;
self.vx = (Math.random() - 0.5) * 16;
self.vy = 12 + Math.random() * 8;
self.type = 'powerupball2';
self.update = function () {
self.x += self.vx;
self.y += self.vy;
// --- Wall bounce tracking for powerup balls ---
if (typeof self.leftWallHits === "undefined") self.leftWallHits = 0;
if (typeof self.rightWallHits === "undefined") self.rightWallHits = 0;
if (typeof self.wallBouncesNoBlock === "undefined") self.wallBouncesNoBlock = 0;
if (typeof self.hasHitBlockSinceReset === "undefined") self.hasHitBlockSinceReset = false;
var hitWall = false;
// Prevent ball from leaving left, right, or top of the screen
if (self.x - self.radius < 0) {
self.x = self.radius;
self.vx = -self.vx;
self.leftWallHits++;
hitWall = true;
}
if (self.x + self.radius > 2048) {
self.x = 2048 - self.radius;
self.vx = -self.vx;
self.rightWallHits++;
hitWall = true;
}
if (hitWall) {
if (!self.hasHitBlockSinceReset) {
self.wallBouncesNoBlock++;
}
}
// If left or right wall hit more than 15 times without hitting any block, return ball to player
if (self.wallBouncesNoBlock >= 15 && typeof paddle !== "undefined" && paddle) {
self.x = paddle.x;
self.y = paddle.y - paddle.height / 2 - self.radius - 10;
self.vx = 0;
self.vy = 0;
self.sticky = true;
self.leftWallHits = 0;
self.rightWallHits = 0;
self.wallBouncesNoBlock = 0;
self.hasHitBlockSinceReset = false;
}
if (self.y - self.radius < 0) {
self.y = self.radius;
self.vy = -self.vy;
LK.getSound('hit').play();
}
// Bounce off paddle
if (typeof paddle !== "undefined" && paddle && typeof paddle.getHitbox === "function") {
var hitbox = paddle.getHitbox();
if (self.y + self.radius >= hitbox.top && self.y - self.radius <= hitbox.bottom && self.x + self.radius >= hitbox.left && self.x - self.radius <= hitbox.right && self.vy > 0) {
// Reflect ball, angle based on where it hit the paddle
var rel = (self.x - paddle.x) / (paddle.width / 2 * paddle.scaleX);
var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg
var speed = Math.sqrt(self.vx * self.vx + self.vy * self.vy);
self.vx = Math.cos(angle) * speed;
self.vy = Math.sin(angle) * speed;
self.y = paddle.y - paddle.height / 2 - self.radius - 2;
if (self.y - self.radius < 0) {
self.y = self.radius;
if (self.vy < 0) self.vy = Math.abs(self.vy);
}
LK.getSound('hit').play();
}
}
// Bounce off blocks
if (typeof blocks !== "undefined" && blocks && blocks.length) {
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
if (block && self.intersects && self.intersects(block)) {
var overlapX = Math.abs(self.x - block.x) - (block.blockSprite.width / 2 + self.radius);
var overlapY = Math.abs(self.y - block.y) - (block.blockSprite.height / 2 + self.radius);
if (overlapX > overlapY) {
self.vx = -self.vx;
} else {
self.vy = -self.vy;
}
// Damage block like main ball
if (typeof block.hp !== "undefined") {
if (self.fireball || typeof paddle !== "undefined" && paddle && paddle.laserBeam) {
block.hp = 0;
if (block.updateHpBar) block.updateHpBar();
block.alpha = 0;
block.scaleX = block.scaleY = 1.5;
block.destroy();
if (block.parent) block.parent.removeChild(block);
blocks.splice(i, 1);
i--;
} else {
block.hp--;
if (block.updateHpBar) block.updateHpBar();
if (block.hp <= 0) {
block.breakBlock();
blocks.splice(i, 1);
i--; // adjust index after removal
}
}
}
LK.getSound('hit').play();
break; // Only bounce off one block per frame
}
}
}
};
return self;
});
var PowerupBall3 = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x8dff00 // lime
});
self.radius = sprite.width / 2;
self.vx = (Math.random() - 0.5) * 20;
self.vy = 10 + Math.random() * 10;
self.type = 'powerupball3';
self.update = function () {
self.x += self.vx;
self.y += self.vy;
// --- Wall bounce tracking for powerup balls ---
if (typeof self.leftWallHits === "undefined") self.leftWallHits = 0;
if (typeof self.rightWallHits === "undefined") self.rightWallHits = 0;
if (typeof self.wallBouncesNoBlock === "undefined") self.wallBouncesNoBlock = 0;
if (typeof self.hasHitBlockSinceReset === "undefined") self.hasHitBlockSinceReset = false;
var hitWall = false;
// Prevent ball from leaving left, right, or top of the screen
if (self.x - self.radius < 0) {
self.x = self.radius;
self.vx = -self.vx;
self.leftWallHits++;
hitWall = true;
}
if (self.x + self.radius > 2048) {
self.x = 2048 - self.radius;
self.vx = -self.vx;
self.rightWallHits++;
hitWall = true;
}
if (hitWall) {
if (!self.hasHitBlockSinceReset) {
self.wallBouncesNoBlock++;
}
}
// If left or right wall hit more than 15 times without hitting any block, return ball to player
if (self.wallBouncesNoBlock >= 15 && typeof paddle !== "undefined" && paddle) {
self.x = paddle.x;
self.y = paddle.y - paddle.height / 2 - self.radius - 10;
self.vx = 0;
self.vy = 0;
self.sticky = true;
self.leftWallHits = 0;
self.rightWallHits = 0;
self.wallBouncesNoBlock = 0;
self.hasHitBlockSinceReset = false;
}
if (self.y - self.radius < 0) {
self.y = self.radius;
self.vy = -self.vy;
LK.getSound('hit').play();
}
// Bounce off paddle
if (typeof paddle !== "undefined" && paddle && typeof paddle.getHitbox === "function") {
var hitbox = paddle.getHitbox();
if (self.y + self.radius >= hitbox.top && self.y - self.radius <= hitbox.bottom && self.x + self.radius >= hitbox.left && self.x - self.radius <= hitbox.right && self.vy > 0) {
// Reflect ball, angle based on where it hit the paddle
var rel = (self.x - paddle.x) / (paddle.width / 2 * paddle.scaleX);
var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg
var speed = Math.sqrt(self.vx * self.vx + self.vy * self.vy);
self.vx = Math.cos(angle) * speed;
self.vy = Math.sin(angle) * speed;
self.y = paddle.y - paddle.height / 2 - self.radius - 2;
if (self.y - self.radius < 0) {
self.y = self.radius;
if (self.vy < 0) self.vy = Math.abs(self.vy);
}
LK.getSound('hit').play();
}
}
// Bounce off blocks
if (typeof blocks !== "undefined" && blocks && blocks.length) {
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
if (block && self.intersects && self.intersects(block)) {
var overlapX = Math.abs(self.x - block.x) - (block.blockSprite.width / 2 + self.radius);
var overlapY = Math.abs(self.y - block.y) - (block.blockSprite.height / 2 + self.radius);
if (overlapX > overlapY) {
self.vx = -self.vx;
} else {
self.vy = -self.vy;
}
// Damage block like main ball
if (typeof block.hp !== "undefined") {
if (self.fireball || typeof paddle !== "undefined" && paddle && paddle.laserBeam) {
block.hp = 0;
if (block.updateHpBar) block.updateHpBar();
block.alpha = 0;
block.scaleX = block.scaleY = 1.5;
block.destroy();
if (block.parent) block.parent.removeChild(block);
blocks.splice(i, 1);
i--;
} else {
block.hp--;
if (block.updateHpBar) block.updateHpBar();
if (block.hp <= 0) {
block.breakBlock();
blocks.splice(i, 1);
i--; // adjust index after removal
}
}
}
LK.getSound('hit').play();
break; // Only bounce off one block per frame
}
}
}
};
return self;
});
// ShieldedBlock: cannot be broken until another block is destroyed
var ShieldedBlock = Container.expand(function () {
var self = Container.call(this);
self.blockType = 'shielded';
self.shielded = true;
self.setHp = function (hp, maxHp) {
self.hp = hp;
self.maxHp = maxHp || hp;
self.updateHpBar();
// Tint to indicate shield
if (self.shielded) {
self.blockSprite.tint = 0x888888;
} else {
self.blockSprite.tint = 0xffffff;
}
};
self.breakBlock = function () {
if (self.shielded) {
// Flash to indicate shield
LK.effects.flashObject(self, 0x00e0ff, 300);
return;
}
// Normal break
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181c2c
});
/****
* Game Code
****/
// Sounds
// Power-up
// Block colors
// Paddle
// Ball
// Game variables
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
var paddle = null,
ball = null,
blocks = [],
powerups = [],
powerupBalls = []; // holds PowerupBall1/2/3 instances
if (!window.extraBalls || !Array.isArray(window.extraBalls)) window.extraBalls = [];
if (!window.activePowerups || !Array.isArray(window.activePowerups)) window.activePowerups = [];
if (!window.powerupTimers || !Array.isArray(window.powerupTimers)) window.powerupTimers = [];
if (!window.powerupIcons || !Array.isArray(window.powerupIcons)) window.powerupIcons = [];
var lives = 3;
var level = 1;
var isBallLaunched = false;
var dragPaddle = false;
// --- Ava: Variables for approaching pattern logic ---
var patternYOffset = 0; // How much the pattern has moved down toward the player
var patternApproachStep = 120; // How much to move down after each lost life (pixels)
var patternApproachMax = 0; // Will be set after blocks are spawned, so pattern never goes below paddle
// Always reset score to zero at the start of each game and persist it
var score = 0;
storage.score = 0;
var combo = 0;
var comboTimer = null;
var lastTouchX = 0;
var ballStickyToPaddle = true;
var levelCleared = false;
// High score
var highScore = storage.highScore || 0;
// GUI
var scoreTxt = new Text2('0', {
size: 100,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var highScoreTxt = new Text2('High: ' + highScore, {
size: 60,
fill: 0xFFE066
});
highScoreTxt.anchor.set(0.5, 0);
highScoreTxt.y = 90;
LK.gui.top.addChild(highScoreTxt);
var livesTxt = new Text2('♥♥♥', {
size: 80,
fill: 0xFF5E5E
});
livesTxt.anchor.set(1, 0);
LK.gui.topRight.addChild(livesTxt);
var levelTxt = new Text2('Level 1', {
size: 70,
fill: 0xFFE066
});
levelTxt.anchor.set(0, 0);
LK.gui.topLeft.addChild(levelTxt);
// Helper: update GUI
function updateGUI() {
scoreTxt.setText(score);
storage.score = score; // Persist score so it is never reset
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
}
highScoreTxt.setText('High: ' + highScore);
var hearts = '';
for (var i = 0; i < lives; i++) hearts += '♥';
livesTxt.setText(hearts);
// Show only current level, do not show pattern chars
levelTxt.setText('Level ' + level);
}
// Helper: reset combo
function resetCombo() {
combo = 0;
if (comboTimer) {
LK.clearTimeout(comboTimer);
comboTimer = null;
}
}
// Helper: start combo timer
function startCombo() {
if (comboTimer) LK.clearTimeout(comboTimer);
comboTimer = LK.setTimeout(function () {
resetCombo();
}, 1200);
}
// Helper: spawn blocks for current level
function spawnBlocks() {
// Remove old blocks
for (var i = 0; i < blocks.length; i++) {
blocks[i].destroy();
}
blocks = [];
// Dynamic block pattern logic
// --- Dynamically scale blocks so that all patterns always fit on screen ---
// Estimate max pattern size for this level
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var numChars = 1 + Math.floor((level - 1) / 2);
numChars = Math.min(numChars, 6); // max 6 chars side by side for high difficulty
var rows = 7;
var colsPerChar = 5;
var gapCols = 1; // 1 column gap between chars
var totalCols = numChars * colsPerChar + (numChars - 1) * gapCols;
// Set margins (minimum 10px), and add wallMargin to ensure blocks never touch any wall
var wallMargin = 40; // Minimum distance from all walls
var marginX = Math.max(10, 40 - level * 2);
var marginY = Math.max(10, 40 - level * 2);
// Calculate max block size so that all blocks fit horizontally and vertically, never touching any wall
var maxWidth = 2048 - 2 * wallMargin;
var maxHeight = 900 - 2 * wallMargin; // Allow up to 900px for block area, minus wallMargin at top and bottom
var blockW = Math.floor((maxWidth - (totalCols - 1) * marginX) / totalCols);
var blockH = Math.floor((maxHeight - (rows - 1) * marginY) / rows);
// Clamp block size to reasonable min/max
blockW = Math.max(60, Math.min(blockW, 220));
blockH = Math.max(40, Math.min(blockH, 120));
var colors = ['red', 'green', 'blue', 'yellow'];
// --- New: Generate block positions in a grid to form random letters and/or numbers side by side per level ---
// Returns an array of {x, y} positions for blocks, forming multiple random letters/numbers in a row
function getPatternBlockPositions(level, blockW, blockH) {
// Define 5x7 bitmap font for A-Z, 0-9 (1=block, 0=empty)
var font = {
"A": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]],
"B": [[1, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 0]],
"C": [[0, 1, 1, 1, 1], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [0, 1, 1, 1, 1]],
"D": [[1, 1, 1, 0, 0], [1, 0, 0, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 1, 0], [1, 1, 1, 0, 0]],
"E": [[1, 1, 1, 1, 1], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 1, 1, 1, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 1, 1, 1, 1]],
"F": [[1, 1, 1, 1, 1], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 1, 1, 1, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0]],
"G": [[0, 1, 1, 1, 1], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 1]],
"H": [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]],
"I": [[1, 1, 1, 1, 1], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [1, 1, 1, 1, 1]],
"J": [[0, 0, 0, 1, 1], [0, 0, 0, 0, 1], [0, 0, 0, 0, 1], [0, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]],
"K": [[1, 0, 0, 0, 1], [1, 0, 0, 1, 0], [1, 0, 1, 0, 0], [1, 1, 0, 0, 0], [1, 0, 1, 0, 0], [1, 0, 0, 1, 0], [1, 0, 0, 0, 1]],
"L": [[1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 1, 1, 1, 1]],
"M": [[1, 0, 0, 0, 1], [1, 1, 0, 1, 1], [1, 0, 1, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]],
"N": [[1, 0, 0, 0, 1], [1, 1, 0, 0, 1], [1, 0, 1, 0, 1], [1, 0, 0, 1, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]],
"O": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]],
"P": [[1, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0]],
"Q": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 1, 0, 1], [1, 0, 0, 1, 0], [0, 1, 1, 0, 1]],
"R": [[1, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 0], [1, 0, 1, 0, 0], [1, 0, 0, 1, 0], [1, 0, 0, 0, 1]],
"S": [[0, 1, 1, 1, 1], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [0, 1, 1, 1, 0], [0, 0, 0, 0, 1], [0, 0, 0, 0, 1], [1, 1, 1, 1, 0]],
"T": [[1, 1, 1, 1, 1], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0]],
"U": [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]],
"V": [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 0, 1, 0], [0, 1, 0, 1, 0], [0, 0, 1, 0, 0]],
"W": [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 1, 0, 1], [1, 1, 0, 1, 1], [1, 1, 0, 1, 1], [1, 0, 0, 0, 1]],
"X": [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 0, 1, 0], [0, 0, 1, 0, 0], [0, 1, 0, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]],
"Y": [[1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 0, 1, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0]],
"Z": [[1, 1, 1, 1, 1], [0, 0, 0, 0, 1], [0, 0, 0, 1, 0], [0, 0, 1, 0, 0], [0, 1, 0, 0, 0], [1, 0, 0, 0, 0], [1, 1, 1, 1, 1]],
"0": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 1, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]],
"1": [[0, 0, 1, 0, 0], [0, 1, 1, 0, 0], [1, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [1, 1, 1, 1, 1]],
"2": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [0, 0, 0, 0, 1], [0, 0, 0, 1, 0], [0, 0, 1, 0, 0], [0, 1, 0, 0, 0], [1, 1, 1, 1, 1]],
"3": [[1, 1, 1, 1, 0], [0, 0, 0, 0, 1], [0, 0, 0, 1, 0], [0, 0, 1, 1, 0], [0, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]],
"4": [[0, 0, 0, 1, 0], [0, 0, 1, 1, 0], [0, 1, 0, 1, 0], [1, 0, 0, 1, 0], [1, 1, 1, 1, 1], [0, 0, 0, 1, 0], [0, 0, 0, 1, 0]],
"5": [[1, 1, 1, 1, 1], [1, 0, 0, 0, 0], [1, 1, 1, 1, 0], [0, 0, 0, 0, 1], [0, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]],
"6": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 0], [1, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]],
"7": [[1, 1, 1, 1, 1], [0, 0, 0, 0, 1], [0, 0, 0, 1, 0], [0, 0, 1, 0, 0], [0, 1, 0, 0, 0], [0, 1, 0, 0, 0], [0, 1, 0, 0, 0]],
"8": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]],
"9": [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 1], [0, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]]
};
// Decide how many chars to show side by side (1-6, more as level increases)
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var numChars = 1 + Math.floor((level - 1) / 2);
numChars = Math.min(numChars, 6); // max 6 chars side by side for high difficulty
// Pick random chars for this level (can be letter or number, allow repeats)
// --- Ava: Use a new random seed for every level start, so every playthrough is unique ---
if (typeof window.levelRandomSeed === "undefined") window.levelRandomSeed = {};
// Use a unique random seed for each level start, based on Date.now() and Math.random()
var levelSeed = Date.now() + Math.floor(Math.random() * 1000000);
window.levelRandomSeed[level] = levelSeed;
// Deterministic random for this level, so pattern is unique per play, but consistent for the level
function seededRandom(seed) {
var x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
}
var charArr = [];
for (var i = 0; i < numChars; i++) {
// Use the seed to pick a char, so every level start is different
var idx = Math.floor(seededRandom(levelSeed + i * 997) * chars.length);
charArr.push(chars[idx]);
}
// If level 1, always show a single letter for clarity
if (level === 1) charArr = [chars[Math.floor(seededRandom(levelSeed) * chars.length)]];
// Expose charArr globally so updateGUI can always show the current level's pattern
window.charArr = charArr;
// If no pattern found for any char, fallback to random grid
var allPatternsExist = true;
for (var i = 0; i < charArr.length; i++) {
if (!font[charArr[i]]) {
allPatternsExist = false;
break;
}
}
if (!allPatternsExist) {
// fallback: random grid
var positions = [];
var rows = 7,
cols = 5 * numChars + (numChars - 1); // wider grid for more chars
// Dynamically scale block size for fallback grid
var marginX_fallback = marginX;
var marginY_fallback = marginY;
var wallMargin = 40; // Ensure fallback grid also never touches any wall
var maxWidth_fallback = 2048 - 2 * wallMargin;
var maxHeight_fallback = 900 - 2 * wallMargin;
var blockW_fallback = Math.floor((maxWidth_fallback - (cols - 1) * marginX_fallback) / cols);
var blockH_fallback = Math.floor((maxHeight_fallback - (rows - 1) * marginY_fallback) / rows);
blockW_fallback = Math.max(60, Math.min(blockW_fallback, 220));
blockH_fallback = Math.max(40, Math.min(blockH_fallback, 120));
var totalW = cols * blockW_fallback + (cols - 1) * marginX_fallback;
var totalH = rows * blockH_fallback + (rows - 1) * marginY_fallback;
var startX = wallMargin + (2048 - 2 * wallMargin - totalW) / 2 + blockW_fallback / 2;
var startY = 400 + wallMargin;
for (var r = 0; r < rows; r++) {
for (var c = 0; c < cols; c++) {
if (Math.random() < 0.7) {
positions.push({
x: startX + c * (blockW_fallback + marginX_fallback),
y: startY + r * (blockH_fallback + marginY_fallback)
});
}
}
}
// Set global blockW/blockH for this fallback pattern so blocks are sized correctly
blockW = blockW_fallback;
blockH = blockH_fallback;
return positions;
}
// Center the pattern(s) in the play area
var rows = 7;
var colsPerChar = 5;
var gapCols = 1; // 1 column gap between chars
var totalCols = numChars * colsPerChar + (numChars - 1) * gapCols;
var wallMargin = 40; // Use same wall margin for all patterns
var totalW = totalCols * blockW + (totalCols - 1) * marginX;
var totalH = rows * blockH + (rows - 1) * marginY;
var startX = wallMargin + (2048 - 2 * wallMargin - totalW) / 2 + blockW / 2;
var startY = wallMargin + (900 - totalH) / 2 + 300; // 300 is the original y offset, but now ensure margin at top and bottom
var positions = [];
for (var ch = 0; ch < charArr.length; ch++) {
var pattern = font[charArr[ch]];
for (var r = 0; r < rows; r++) {
for (var c = 0; c < colsPerChar; c++) {
if (pattern[r][c]) {
// Calculate x offset for this char
var charOffset = ch * (colsPerChar + gapCols) * (blockW + marginX);
positions.push({
x: startX + charOffset + c * (blockW + marginX),
y: startY + r * (blockH + marginY)
});
}
}
}
}
return positions;
}
// Only normal blocks, no shape/orientation logic
function getShapePattern(level, rows, cols) {
// Not used anymore, but kept for compatibility
var arr = [];
for (var r = 0; r < rows; r++) {
arr[r] = [];
for (var c = 0; c < cols; c++) {
arr[r][c] = {
type: 'normal',
present: true
};
}
}
return arr;
}
// Animal shape patterns for higher levels
function getAnimalPattern(level, rows, cols) {
// Each pattern is a 2D array of 0/1, 1 means block present
// Patterns: horse, donkey, giraffe, etc.
// All patterns are 10x12 max, centered
var patterns = [];
// Horse (at, level 10+)
patterns.push([[0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0], [0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], [0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0], [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]]);
// Donkey (eşek, level 15+)
patterns.push([[0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0], [0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], [0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0]]);
// Giraffe (zürafa, level 20+)
patterns.push([[0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0]]);
// Add more animal patterns as needed
// Pick pattern based on level
if (level >= 20) return patterns[2];
if (level >= 15) return patterns[1];
if (level >= 10) return patterns[0];
return null;
}
// Increase rows and columns as level increases, up to a max
// --- OPTIMIZATION: For level 6+, reduce number of blocks per row/col to improve performance ---
var rows, cols;
if (level >= 6) {
rows = Math.min(2 + Math.floor(level / 3), 7);
cols = Math.min(4 + Math.floor(level / 4), 9);
} else {
rows = Math.min(3 + Math.floor(level / 2), 10);
cols = Math.min(5 + Math.floor(level / 3), 12);
}
// For variety, pick a pattern type based on level
var patternType = level % 5; // 0: full, 1: checker, 2: pyramid, 3: gaps, 4: zigzag
if (level === 1) {
patternType = 0; // Force full grid for first level
}
// Animal pattern override for higher levels
var animalPattern = getAnimalPattern(level, rows, cols);
if (animalPattern) {
// Use animal pattern, override rows/cols
rows = animalPattern.length;
cols = animalPattern[0].length;
patternType = -1; // Custom
// Dynamically scale block size for animal pattern
var marginX_animal = marginX;
var marginY_animal = marginY;
var wallMargin = 40; // Ensure animal patterns never touch any wall
var maxWidth_animal = 2048 - 2 * wallMargin;
var maxHeight_animal = 900 - 2 * wallMargin;
var blockW_animal = Math.floor((maxWidth_animal - (cols - 1) * marginX_animal) / cols);
var blockH_animal = Math.floor((maxHeight_animal - (rows - 1) * marginY_animal) / rows);
blockW_animal = Math.max(60, Math.min(blockW_animal, 220));
blockH_animal = Math.max(40, Math.min(blockH_animal, 120));
blockW = blockW_animal;
blockH = blockH_animal;
}
var totalW = cols * blockW + (cols - 1) * marginX;
var startX = (2048 - totalW) / 2 + blockW / 2;
var startY = 300;
// --- New: get block layout in a random letter/number pattern for this level ---
var blockPositions = getPatternBlockPositions(level, blockW, blockH);
// --- OPTIMIZATION: Batch block creation to avoid frame stutter at level start ---
var blocksToAdd = [];
for (var i = 0; i < blockPositions.length; i++) {
// Randomly assign special block types
var blockTypeRand = Math.random();
var block;
if (level > 2 && blockTypeRand < 0.12) {
block = new MovingBlock();
block.blockType = 'moving';
} else if (level > 3 && blockTypeRand >= 0.12 && blockTypeRand < 0.20) {
block = new ExplodingBlock();
block.blockType = 'exploding';
} else if (level > 4 && blockTypeRand >= 0.20 && blockTypeRand < 0.26) {
block = new ShieldedBlock();
block.blockType = 'shielded';
block.shielded = true;
} else {
block = new Block();
block.blockType = 'normal';
}
var color = colors[(i + level) % colors.length];
block.setColor(color);
block.blockSprite.width = blockW;
block.blockSprite.height = blockH;
block.blockSprite.rotation = 0;
block.x = blockPositions[i].x;
block.y = blockPositions[i].y + patternYOffset; // Apply patternYOffset if any
block.originalY = blockPositions[i].y; // Store originalY for approach logic
// Increase block HP as level increases (scales faster with level)
// Difficulty scaling: more chars, more HP, less margin, more blocks as level increases
var hpBonus = Math.floor(level / 2) + Math.floor(level / 5);
block.hp = 1 + Math.floor(level / 3) + Math.floor(i / 4) + hpBonus;
block.maxHp = block.hp;
block.setHp(block.hp, block.maxHp);
blocksToAdd.push(block);
}
// Add all blocks to game and blocks array in a single batch to reduce layout thrashing
for (var i = 0; i < blocksToAdd.length; i++) {
game.addChild(blocksToAdd[i]);
blocks.push(blocksToAdd[i]);
}
}
// Helper: spawn paddle
function spawnPaddle() {
if (paddle) paddle.destroy();
paddle = new Paddle();
paddle.x = 2048 / 2;
paddle.y = 2732 - 180;
game.addChild(paddle);
}
// Helper: spawn ball
function spawnBall() {
if (ball) ball.destroy();
ball = new Ball();
ball.x = paddle.x;
ball.y = paddle.y - paddle.height / 2 - ball.radius - 10;
ball.vx = 0;
ball.vy = 0;
ball.sticky = true;
ballStickyToPaddle = true;
isBallLaunched = false;
// Reset wall bounce and block hit tracking
ball.leftWallHits = 0;
ball.rightWallHits = 0;
ball.wallBouncesNoBlock = 0;
ball.hasHitBlockSinceReset = false;
game.addChild(ball);
}
// Helper: spawn powerup
function spawnPowerUp(x, y) {
// Limit to 5 powerups on screen at once
if (!powerups || !Array.isArray(powerups)) powerups = [];
if (powerups.length >= 5) return;
var power = new PowerUp();
// Fun, engaging, and bug-free powerup types (50+)
// Removed boring/annoying types, added creative/fun ones, and ensured all are bug-free
// Increase probability of ball count powerups by duplicating them in the array
var workingTypes = [
// Paddle/ball size & speed
'expand', 'shrink', 'bigball', 'smallball',
// Ball count (duplicated for higher probability)
'multiball', 'multiball', 'multiball', 'ballsplit', 'ballsplit', 'ballsplit', 'ballclone', 'ballclone', 'ballclone', '+1ball', '+1ball', '+1ball', '+2ball', '+2ball', '+2ball', '+3ball', '+3ball', '+3ball',
// Score
'scorex2', 'scorex3',
// Block effects
'blockbomb', 'blockrowclear', 'blockcolclear', 'blockheal', 'blockrandom', 'blockmove', 'blockshrink', 'blockexpand', 'blockshield', 'blockswap', 'blockteleport', 'blockregen', 'blockexplode', 'blockinvisible',
// Ball physics
'slow', 'fast', 'ballheavy', 'balllight', 'ballcurve', 'ballzigzag', 'ballteleport', 'ballinvisible', 'superball',
// Paddle physics
'magnet',
// Special/fun
'sticky', 'life',
// Laser and fireball (now equal probability with others)
'laser', 'fireball',
// Other powerups
'iceball', 'portal', 'gravity', 'mirror', 'shrinkall', 'expandall', 'swapall', 'rainbow', 'cloneblock', 'blockfall', 'blockrise', 'blockrain', 'blockquake', 'blockfreeze', 'blockflash', 'blockmirror', 'blockportal', 'blockgravity', 'blockmagnet', 'blocklaser', 'blockfire', 'blockice', 'blockghost', 'blocksuper', 'blockrandomhp', 'blockbonus', 'blocktrap', 'blockshieldall', 'blockswapall', 'blockteleportall', 'blockregenall', 'blockexplodeall'];
// Pick a random working type with increased probability for ball count powerups
power.type = workingTypes[Math.floor(Math.random() * workingTypes.length)];
// If x or y is undefined, spawn at a random position within the play area
if (typeof x === "undefined" || typeof y === "undefined") {
// Avoid top 200px and bottom 300px, and 40px margin on sides
var marginX = 40;
var marginYTop = 200;
var marginYBottom = 300;
power.x = marginX + Math.random() * (2048 - 2 * marginX);
// Make powerups spawn mostly in the upper part of the playfield (80% chance)
if (Math.random() < 0.8) {
// Top 40% of the playfield (above the paddle area)
var upperHeight = Math.floor((2732 - marginYTop - marginYBottom) * 0.4);
power.y = marginYTop + Math.random() * upperHeight;
} else {
// Anywhere in the playfield (fallback)
power.y = marginYTop + Math.random() * (2732 - marginYTop - marginYBottom);
}
} else {
power.x = x;
power.y = y;
}
game.addChild(power);
if (!powerups || !Array.isArray(powerups)) powerups = [];
powerups.push(power);
}
// Helper: next level
function nextLevel() {
level++;
lives = Math.min(5, lives + 1); // Add 1 life, max 5
// --- Difficulty scaling: increase ball speed, block HP, and decrease powerup spawn interval ---
if (ball) {
// Increase main ball speed a bit every level, up to a max
ball.speed = Math.min(22 + level * 1.2, 60);
// If ball is moving, scale its velocity to new speed
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
if (speed > 0) {
var scale = ball.speed / speed;
ball.vx *= scale;
ball.vy *= scale;
}
}
// Store global difficulty for use in spawnBlocks and powerup logic
window.difficultyLevel = level;
// Decrease powerup spawn interval (min 120 ticks)
game.powerupSpawnInterval = Math.max(120, 360 - level * 12);
// Clear combo timer on level up
if (comboTimer) {
LK.clearTimeout(comboTimer);
comboTimer = null;
}
// Remove all falling powerup balls
if (!powerupBalls || !Array.isArray(powerupBalls)) powerupBalls = [];
if (powerupBalls && powerupBalls.length) {
for (var i = powerupBalls.length - 1; i >= 0; i--) {
if (powerupBalls[i]) powerupBalls[i].destroy();
powerupBalls.splice(i, 1);
}
}
// Remove all extra balls
if (!window.extraBalls || !Array.isArray(window.extraBalls)) window.extraBalls = [];
if (window.extraBalls && window.extraBalls.length) {
for (var i = window.extraBalls.length - 1; i >= 0; i--) {
if (window.extraBalls[i]) window.extraBalls[i].destroy();
window.extraBalls.splice(i, 1);
}
}
updateGUI();
patternYOffset = 0; // Reset pattern approach on new level
spawnBlocks();
spawnPaddle();
spawnBall();
levelCleared = false;
// Set patternApproachMax so pattern never goes below paddle
// (Pattern should not go below y = paddle.y - 200)
patternApproachMax = Math.max(0, (paddle ? paddle.y : 2732 - 180) - 200 - 400); // 400 is the original pattern startY
// Clear all powerup timers and arrays
if (window.powerupTimers && Array.isArray(window.powerupTimers)) {
for (var t = 0; t < window.powerupTimers.length; t++) {
if (window.powerupTimers[t]) LK.clearTimeout(window.powerupTimers[t]);
}
window.powerupTimers = [];
}
if (window.activePowerups && Array.isArray(window.activePowerups)) window.activePowerups = [];
if (window.powerupIcons && Array.isArray(window.powerupIcons)) {
for (var i = 0; i < window.powerupIcons.length; i++) {
if (window.powerupIcons[i] && typeof window.powerupIcons[i].destroy === "function") window.powerupIcons[i].destroy();
}
window.powerupIcons = [];
}
}
// Endless: Remove win condition, always go to nextLevel
// Helper: lose life
function loseLife() {
// If any powerup ball is on screen, do not lose a life, just respawn the main ball and paddle
if (!powerupBalls || !Array.isArray(powerupBalls)) powerupBalls = [];
if (powerupBalls && powerupBalls.length > 0) {
spawnPaddle();
spawnBall();
return;
}
lives--;
updateGUI();
LK.getSound('lose').play();
if (lives <= 0) {
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
highScoreTxt.setText('High: ' + highScore);
}
score = 0;
storage.score = 0; // Reset score on game over
// Clear all powerup timers and arrays
if (window.powerupTimers && Array.isArray(window.powerupTimers)) {
for (var t = 0; t < window.powerupTimers.length; t++) {
if (window.powerupTimers[t]) LK.clearTimeout(window.powerupTimers[t]);
}
window.powerupTimers = [];
}
if (window.activePowerups && Array.isArray(window.activePowerups)) window.activePowerups = [];
if (window.powerupIcons && Array.isArray(window.powerupIcons)) {
for (var i = 0; i < window.powerupIcons.length; i++) {
if (window.powerupIcons[i] && typeof window.powerupIcons[i].destroy === "function") window.powerupIcons[i].destroy();
}
window.powerupIcons = [];
}
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
return;
}
// Remove all extra balls when losing a life, but do not reduce life for losing extra balls
if (!window.extraBalls || !Array.isArray(window.extraBalls)) window.extraBalls = [];
if (window.extraBalls && window.extraBalls.length) {
for (var i = window.extraBalls.length - 1; i >= 0; i--) {
if (window.extraBalls[i]) window.extraBalls[i].destroy();
window.extraBalls.splice(i, 1);
}
}
window.extraBalls = [];
// --- Ava: Move pattern down after each lost life ---
patternYOffset += patternApproachStep;
if (patternYOffset > patternApproachMax) patternYOffset = patternApproachMax;
// Move all blocks down by patternYOffset (relative to their original y)
if (blocks && blocks.length) {
for (var i = 0; i < blocks.length; i++) {
if (typeof blocks[i].originalY === "undefined") {
blocks[i].originalY = blocks[i].y - patternYOffset;
}
blocks[i].y = blocks[i].originalY + patternYOffset;
}
}
// If pattern has reached or passed the paddle, trigger game over
if (blocks && blocks.length && paddle) {
// Find the lowest block
var maxBlockY = -Infinity;
for (var i = 0; i < blocks.length; i++) {
if (blocks[i].y > maxBlockY) maxBlockY = blocks[i].y;
}
// If any block is at or below the top of the paddle, game over
if (maxBlockY + (blocks[0].blockSprite ? blocks[0].blockSprite.height / 2 : 0) >= paddle.y - paddle.height / 2) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
return;
}
}
spawnPaddle();
spawnBall();
}
// Helper: win
function winGame() {
LK.effects.flashScreen(0x00ff00, 1000);
// Defensive: ensure arrays exist before clearing
if (!window.powerupTimers || !Array.isArray(window.powerupTimers)) window.powerupTimers = [];
if (!window.activePowerups || !Array.isArray(window.activePowerups)) window.activePowerups = [];
if (!window.powerupIcons || !Array.isArray(window.powerupIcons)) window.powerupIcons = [];
// Clear all powerup timers and arrays
for (var t = 0; t < window.powerupTimers.length; t++) {
if (window.powerupTimers[t]) LK.clearTimeout(window.powerupTimers[t]);
}
window.powerupTimers = [];
window.activePowerups = [];
for (var i = 0; i < window.powerupIcons.length; i++) {
if (window.powerupIcons[i] && typeof window.powerupIcons[i].destroy === "function") window.powerupIcons[i].destroy();
}
window.powerupIcons = [];
LK.showYouWin();
}
// Initialize first level
highScore = storage.highScore || 0;
// Always start with score zero, persistently
score = 0;
storage.score = 0;
highScoreTxt.setText('High: ' + highScore);
window.extraBalls = [];
// Clear all powerup timers and arrays at game start
if (window.powerupTimers && Array.isArray(window.powerupTimers)) {
for (var t = 0; t < window.powerupTimers.length; t++) {
if (window.powerupTimers[t]) LK.clearTimeout(window.powerupTimers[t]);
}
window.powerupTimers = [];
}
if (window.activePowerups && Array.isArray(window.activePowerups)) window.activePowerups = [];
if (window.powerupIcons && Array.isArray(window.powerupIcons)) {
for (var i = 0; i < window.powerupIcons.length; i++) {
if (window.powerupIcons[i] && typeof window.powerupIcons[i].destroy === "function") window.powerupIcons[i].destroy();
}
window.powerupIcons = [];
}
spawnBlocks();
spawnPaddle();
spawnBall();
updateGUI();
// Touch controls
game.down = function (x, y, obj) {
// Launch ball if it's sticky and user taps in lower 40% of screen
if (y > 2732 * 0.6 && ballStickyToPaddle) {
// Launch at slight upward angle based on touch position
var rel = (x - paddle.x) / (paddle.width / 2);
var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg
ball.launch(angle);
isBallLaunched = true;
ballStickyToPaddle = false;
}
};
game.move = function (x, y, obj) {
// Always move paddle horizontally with finger/mouse X, no need to hold down
var targetX = x;
// Use a weighted average for smoothing (0.7 current, 0.3 target)
if (typeof paddle.x === "number") {
paddle.moveTo(paddle.x * 0.7 + targetX * 0.3);
} else {
paddle.moveTo(targetX);
}
// If ball is sticky, move it with paddle
if (ballStickyToPaddle) {
ball.x = paddle.x;
}
};
game.up = function (x, y, obj) {
// No need to set dragPaddle, paddle always follows move
};
// Main update loop
game.update = function () {
// Reset shielded unlock flag at start of update
game._shieldedUnlock = false;
// Ball update
if (ball) ball.update();
// --- Ball-ball collision for all balls (main, extra, powerup balls) ---
(function handleBallBallCollisions() {
// Gather all balls: main, extra, powerup
var allBalls = [];
if (ball) allBalls.push(ball);
if (window.extraBalls && Array.isArray(window.extraBalls)) {
for (var i = 0; i < window.extraBalls.length; i++) {
if (window.extraBalls[i]) allBalls.push(window.extraBalls[i]);
}
}
if (powerupBalls && Array.isArray(powerupBalls)) {
for (var i = 0; i < powerupBalls.length; i++) {
if (powerupBalls[i]) allBalls.push(powerupBalls[i]);
}
}
// For each unique pair, check collision and reflect if needed
for (var i = 0; i < allBalls.length; i++) {
var b1 = allBalls[i];
if (!b1) continue;
if (typeof b1.lastX === "undefined") b1.lastX = b1.x;
if (typeof b1.lastY === "undefined") b1.lastY = b1.y;
for (var j = i + 1; j < allBalls.length; j++) {
var b2 = allBalls[j];
if (!b2) continue;
if (typeof b2.lastX === "undefined") b2.lastX = b2.x;
if (typeof b2.lastY === "undefined") b2.lastY = b2.y;
// Only check if both are moving and not sticky
if (b1.sticky || b2.sticky) continue;
// Use circle collision
var dx = b1.x - b2.x;
var dy = b1.y - b2.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var minDist = (b1.radius || 0) + (b2.radius || 0);
// Only trigger on new collision (not overlapping last frame)
var lastDx = b1.lastX - b2.lastX;
var lastDy = b1.lastY - b2.lastY;
var lastDist = Math.sqrt(lastDx * lastDx + lastDy * lastDy);
if (lastDist >= minDist && dist < minDist) {
// Simple elastic collision: swap velocities along the collision axis
var nx = dx / (dist || 1);
var ny = dy / (dist || 1);
// Project velocities onto the collision normal
var p1 = b1.vx * nx + b1.vy * ny;
var p2 = b2.vx * nx + b2.vy * ny;
// Only swap if balls are moving toward each other
if (p1 - p2 > 0) {
// Swap the normal components, keep tangential
var tx = -ny,
ty = nx;
var t1 = b1.vx * tx + b1.vy * ty;
var t2 = b2.vx * tx + b2.vy * ty;
// New velocities
b1.vx = t1 * tx + p2 * nx;
b1.vy = t1 * ty + p2 * ny;
b2.vx = t2 * tx + p1 * nx;
b2.vy = t2 * ty + p1 * ny;
// Separate balls to avoid sticking
var overlap = minDist - dist + 0.5;
b1.x += nx * (overlap / 2);
b1.y += ny * (overlap / 2);
b2.x -= nx * (overlap / 2);
b2.y -= ny * (overlap / 2);
// Play hit sound
LK.getSound('hit').play();
}
}
}
// Update lastX/lastY for next frame
b1.lastX = b1.x;
b1.lastY = b1.y;
}
})();
// --- OPTIMIZATION: Only update extra balls if any exist ---
if (!window.extraBalls || !Array.isArray(window.extraBalls)) window.extraBalls = [];
if (window.extraBalls.length) {
// Cache paddle hitbox if paddle exists and is not moving
var cachedPaddleHitbox = typeof paddle !== "undefined" && paddle && typeof paddle.getHitbox === "function" ? paddle.getHitbox() : null;
// --- OPTIMIZATION: For level 6+, only update every other extra ball per frame ---
var ebStep = level >= 6 ? 2 : 1;
for (var eb = window.extraBalls.length - 1; eb >= 0; eb -= ebStep) {
var ebBall = window.extraBalls[eb];
if (ebBall && ebBall.update) ebBall.update();
// --- Track left/right wall bounces for extra balls ---
if (typeof ebBall.leftWallHits === "undefined") ebBall.leftWallHits = 0;
if (typeof ebBall.rightWallHits === "undefined") ebBall.rightWallHits = 0;
if (typeof ebBall.wallBouncesNoBlock === "undefined") ebBall.wallBouncesNoBlock = 0;
if (typeof ebBall.hasHitBlockSinceReset === "undefined") ebBall.hasHitBlockSinceReset = false;
var ebHitWall = false;
if (ebBall.x - ebBall.radius < 0) {
ebBall.x = ebBall.radius;
ebBall.vx = -ebBall.vx;
LK.getSound('hit').play();
ebBall.leftWallHits++;
ebHitWall = true;
}
if (ebBall.x + ebBall.radius > 2048) {
ebBall.x = 2048 - ebBall.radius;
ebBall.vx = -ebBall.vx;
LK.getSound('hit').play();
ebBall.rightWallHits++;
ebHitWall = true;
}
if (ebHitWall) {
if (!ebBall.hasHitBlockSinceReset) {
ebBall.wallBouncesNoBlock++;
}
}
// If left or right wall hit more than 15 times without hitting any block, return ball to player
if (ebBall.wallBouncesNoBlock >= 15) {
ebBall.x = paddle.x;
ebBall.y = paddle.y - paddle.height / 2 - ebBall.radius - 10;
ebBall.vx = 0;
ebBall.vy = 0;
ebBall.sticky = true;
ebBall.leftWallHits = 0;
ebBall.rightWallHits = 0;
ebBall.wallBouncesNoBlock = 0;
ebBall.hasHitBlockSinceReset = false;
}
// Remove if off screen
if (ebBall && ebBall.y - ebBall.radius > 2732) {
ebBall.destroy();
window.extraBalls.splice(eb, 1);
continue;
}
// Paddle collision for extra balls (skip if paddle not present)
if (ebBall && cachedPaddleHitbox) {
if (ebBall.y + ebBall.radius >= cachedPaddleHitbox.top && ebBall.y - ebBall.radius <= cachedPaddleHitbox.bottom && ebBall.x + ebBall.radius >= cachedPaddleHitbox.left && ebBall.x - ebBall.radius <= cachedPaddleHitbox.right && ebBall.vy > 0) {
// Reflect ball, angle based on where it hit the paddle
var rel = (ebBall.x - paddle.x) / (paddle.width / 2 * paddle.scaleX);
var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg
var speed = Math.sqrt(ebBall.vx * ebBall.vx + ebBall.vy * ebBall.vy);
ebBall.vx = Math.cos(angle) * speed;
ebBall.vy = Math.sin(angle) * speed;
ebBall.y = paddle.y - paddle.height / 2 - ebBall.radius - 2;
if (ebBall.y - ebBall.radius < 0) {
ebBall.y = ebBall.radius;
if (ebBall.vy < 0) ebBall.vy = Math.abs(ebBall.vy);
}
LK.getSound('hit').play();
}
}
}
}
// --- OPTIMIZATION: Only update powerup balls if any exist ---
if (!powerupBalls || !Array.isArray(powerupBalls)) powerupBalls = [];
if (powerupBalls && powerupBalls.length) {
var cachedPaddleHitboxPB = typeof paddle !== "undefined" && paddle && typeof paddle.getHitbox === "function" ? paddle.getHitbox() : null;
// --- OPTIMIZATION: For level 6+, only update every other powerup ball per frame ---
var pbStep = level >= 6 ? 2 : 1;
for (var pbIdx = powerupBalls.length - 1; pbIdx >= 0; pbIdx -= pbStep) {
var pbBall = powerupBalls[pbIdx];
if (pbBall && pbBall.update) pbBall.update();
// --- Track left/right wall bounces for powerup balls ---
if (typeof pbBall.leftWallHits === "undefined") pbBall.leftWallHits = 0;
if (typeof pbBall.rightWallHits === "undefined") pbBall.rightWallHits = 0;
if (typeof pbBall.wallBouncesNoBlock === "undefined") pbBall.wallBouncesNoBlock = 0;
if (typeof pbBall.hasHitBlockSinceReset === "undefined") pbBall.hasHitBlockSinceReset = false;
var pbHitWall = false;
if (pbBall.x - pbBall.radius < 0) {
pbBall.x = pbBall.radius;
pbBall.vx = -pbBall.vx;
LK.getSound('hit').play();
pbBall.leftWallHits++;
pbHitWall = true;
}
if (pbBall.x + pbBall.radius > 2048) {
pbBall.x = 2048 - pbBall.radius;
pbBall.vx = -pbBall.vx;
LK.getSound('hit').play();
pbBall.rightWallHits++;
pbHitWall = true;
}
if (pbHitWall) {
if (!pbBall.hasHitBlockSinceReset) {
pbBall.wallBouncesNoBlock++;
}
}
// If left or right wall hit more than 15 times without hitting any block, return ball to player
if (pbBall.wallBouncesNoBlock >= 15) {
pbBall.x = paddle.x;
pbBall.y = paddle.y - paddle.height / 2 - pbBall.radius - 10;
pbBall.vx = 0;
pbBall.vy = 0;
pbBall.sticky = true;
pbBall.leftWallHits = 0;
pbBall.rightWallHits = 0;
pbBall.wallBouncesNoBlock = 0;
pbBall.hasHitBlockSinceReset = false;
}
// Remove if off screen
if (pbBall && pbBall.y - pbBall.radius > 2732) {
pbBall.destroy();
powerupBalls.splice(pbIdx, 1);
continue;
}
// Paddle collision for powerup balls (skip if paddle not present)
if (pbBall && cachedPaddleHitboxPB) {
if (pbBall.y + pbBall.radius >= cachedPaddleHitboxPB.top && pbBall.y - pbBall.radius <= cachedPaddleHitboxPB.bottom && pbBall.x + pbBall.radius >= cachedPaddleHitboxPB.left && pbBall.x - pbBall.radius <= cachedPaddleHitboxPB.right && pbBall.vy > 0) {
// Reflect ball, angle based on where it hit the paddle
var rel = (pbBall.x - paddle.x) / (paddle.width / 2 * paddle.scaleX);
var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg
var speed = Math.sqrt(pbBall.vx * pbBall.vx + pbBall.vy * pbBall.vy);
pbBall.vx = Math.cos(angle) * speed;
pbBall.vy = Math.sin(angle) * speed;
pbBall.y = paddle.y - paddle.height / 2 - pbBall.radius - 2;
if (pbBall.y - pbBall.radius < 0) {
pbBall.y = pbBall.radius;
if (pbBall.vy < 0) pbBall.vy = Math.abs(pbBall.vy);
}
LK.getSound('hit').play();
}
}
}
}
// PowerUp update
// --- OPTIMIZATION: For level 6+, only update every other powerup per frame ---
var powerupStep = level >= 6 ? 2 : 1;
for (var i = powerups.length - 1; i >= 0; i -= powerupStep) {
var p = powerups[i];
// OPTIMIZATION: Skip destroyed/invalid powerups immediately
if (!p || typeof p.update !== "function" || p.destroyed) {
powerups.splice(i, 1);
continue;
}
// Defensive: Only call update if p is valid and not destroyed
try {
if (p && typeof p.update === "function" && !p.destroyed) {
p.update();
}
} catch (e) {
// Remove problematic powerup to prevent further errors
powerups.splice(i, 1);
continue;
}
// If off screen
if (p.y > 2732 + 100) {
p.destroy();
powerups.splice(i, 1);
continue;
}
// Paddle or ball collision
var collected = false;
// Use intersects for paddle collision to ensure all powerups are collected by paddle
if (paddle && typeof p.intersects === "function" && p.intersects(paddle)) {
collected = true;
}
// Check main ball
if (!collected && ball && p.intersects(ball)) {
collected = true;
}
// Check extra balls
if (!collected && window.extraBalls && window.extraBalls.length) {
for (var eb = 0; eb < window.extraBalls.length; eb++) {
if (p.intersects(window.extraBalls[eb])) {
collected = true;
break;
}
}
}
// No level restriction: powerups are always collectible
if (collected) {
// --- Powerup balls update and collection ---
}
// --- OPTIMIZATION: Remove powerup ball update from this loop, handled above ---
if (collected) {
// Show floating text for powerup type
var label = new Text2(p.type.toUpperCase(), {
size: 70,
fill: 0xFFE066,
font: "Arial Black"
});
label.anchor.set(0.5, 1);
label.x = p.x;
label.y = p.y - 60;
game.addChild(label);
tween(label, {
y: label.y - 120,
alpha: 0
}, {
duration: 1200,
easing: tween.easeOut,
onFinish: function onFinish() {
label.destroy();
}
});
// Apply powerup
LK.getSound('powerup').play();
// --- Powerup duration tracking and display ---
if (!window.activePowerups || !Array.isArray(window.activePowerups)) window.activePowerups = [];
if (!window.powerupTimers || !Array.isArray(window.powerupTimers)) window.powerupTimers = [];
if (!window.powerupIcons || !Array.isArray(window.powerupIcons)) window.powerupIcons = [];
// List of timed powerups and their durations (ms)
var timedPowerupDurations = {
expand: 6000,
shrink: 6000,
slow: 6000,
fast: 6000,
bigball: 6000,
smallball: 6000,
ballheavy: 6000,
balllight: 6000,
blockshrink: 6000,
blockexpand: 6000,
ghost: 6000,
superball: 6000,
paddleghost: 6000,
// reverse removed
blockinvisible: 6000,
ballinvisible: 6000,
magnet: 4000,
ballcurve: 4000,
ballzigzag: 4000
};
// If this is a timed powerup, add to activePowerups
if (timedPowerupDurations[p.type]) {
var duration = timedPowerupDurations[p.type];
var now = Date.now();
// Remove any previous of same type (refresh)
for (var ap = window.activePowerups.length - 1; ap >= 0; ap--) {
if (window.activePowerups[ap].type === p.type) {
window.activePowerups.splice(ap, 1);
if (window.powerupTimers[ap]) {
LK.clearTimeout(window.powerupTimers[ap]);
window.powerupTimers.splice(ap, 1);
}
if (window.powerupIcons[ap]) {
window.powerupIcons[ap].destroy();
window.powerupIcons.splice(ap, 1);
}
}
}
window.activePowerups.push({
type: p.type,
end: now + duration
});
// Add icon to GUI
var icon = new Text2(p.type[0].toUpperCase(), {
size: 70,
fill: 0xFFE066,
font: "Arial Black"
});
icon.anchor.set(0.5, 0.5);
// Place icons in a row at top center, below score
icon.x = 2048 / 2 - (window.activePowerups.length - 1) * 50 + (window.activePowerups.length - 1) * 100 / 2 + (window.activePowerups.length - 1) * 0;
icon.y = 170;
LK.gui.top.addChild(icon);
window.powerupIcons.push(icon);
// Timer to remove from activePowerups and GUI
(function (type, icon) {
var idx = window.activePowerups.length - 1;
var timer = LK.setTimeout(function () {
// Remove from arrays
for (var ap = window.activePowerups.length - 1; ap >= 0; ap--) {
if (window.activePowerups[ap].type === type) {
window.activePowerups.splice(ap, 1);
if (window.powerupTimers[ap]) window.powerupTimers.splice(ap, 1);
if (window.powerupIcons[ap]) {
window.powerupIcons[ap].destroy();
window.powerupIcons.splice(ap, 1);
}
}
}
}, duration);
window.powerupTimers.push(timer);
})(p.type, icon);
}
// 50 powerup effects, only working ones kept
if (collected) {
if (p.type === '+1ball') {
// Add 1 extra ball
if (!window.extraBalls) window.extraBalls = [];
if (window.extraBalls.length >= 4) return;
var newBall = new Ball();
newBall.x = ball.x;
newBall.y = ball.y;
// Give a random direction, but not too vertical
var angle = -Math.PI / 4 + Math.random() * (Math.PI / 2); // -45 to +45 deg
var speed = ball.speed || 22;
newBall.vx = Math.cos(angle) * speed;
newBall.vy = Math.sin(angle) * speed;
newBall.sticky = false;
game.addChild(newBall);
window.extraBalls.push(newBall);
} else if (p.type === '+2ball') {
// Add 2 extra balls
if (!window.extraBalls) window.extraBalls = [];
var ballsToAdd = Math.min(2, 4 - window.extraBalls.length);
for (var b = 0; b < ballsToAdd; b++) {
var newBall = new Ball();
newBall.x = ball.x;
newBall.y = ball.y;
// Spread angles
var angle = -Math.PI / 3 + b * (Math.PI / 3); // -60, 0 deg
var speed = ball.speed || 22;
newBall.vx = Math.cos(angle) * speed;
newBall.vy = Math.sin(angle) * speed;
newBall.sticky = false;
game.addChild(newBall);
window.extraBalls.push(newBall);
}
} else if (p.type === '+3ball') {
// Add 3 extra balls
if (!window.extraBalls) window.extraBalls = [];
var ballsToAdd = Math.min(3, 4 - window.extraBalls.length);
for (var b = 0; b < ballsToAdd; b++) {
var newBall = new Ball();
newBall.x = ball.x;
newBall.y = ball.y;
// Spread angles
var angle = -Math.PI / 3 + b * (Math.PI / 3) / (ballsToAdd > 1 ? ballsToAdd - 1 : 1); // -60, 0, +60 deg
var speed = ball.speed || 22;
newBall.vx = Math.cos(angle) * speed;
newBall.vy = Math.sin(angle) * speed;
newBall.sticky = false;
game.addChild(newBall);
window.extraBalls.push(newBall);
}
} else if (p.type === 'expand') {
tween(paddle, {
scaleX: 1.5
}, {
duration: 300,
easing: tween.easeOut
});
LK.setTimeout(function () {
tween(paddle, {
scaleX: 1
}, {
duration: 300,
easing: tween.easeIn
});
}, 6000);
} else if (p.type === 'shrink') {
tween(paddle, {
scaleX: 0.7
}, {
duration: 300,
easing: tween.easeOut
});
LK.setTimeout(function () {
tween(paddle, {
scaleX: 1
}, {
duration: 300,
easing: tween.easeIn
});
}, 6000);
} else if (p.type === 'life') {
lives = Math.min(5, lives + 1);
updateGUI();
} else if (p.type === 'sticky') {
ball.sticky = true;
ballStickyToPaddle = true;
ball.vx = 0;
ball.vy = 0;
ball.x = paddle.x;
ball.y = paddle.y - paddle.height / 2 - ball.radius - 10;
} else if (p.type === 'multiball') {
// Double all balls on screen (main, extra, powerup balls)
var allBalls = [];
if (ball) allBalls.push(ball);
if (window.extraBalls && Array.isArray(window.extraBalls)) {
for (var i = 0; i < window.extraBalls.length; i++) {
if (window.extraBalls[i]) allBalls.push(window.extraBalls[i]);
}
}
if (powerupBalls && Array.isArray(powerupBalls)) {
for (var i = 0; i < powerupBalls.length; i++) {
if (powerupBalls[i]) allBalls.push(powerupBalls[i]);
}
}
// Defensive: limit total balls to 12
var maxTotalBalls = 12;
var ballsToAdd = [];
for (var i = 0; i < allBalls.length; i++) {
if (window.extraBalls && window.extraBalls.length + ballsToAdd.length >= maxTotalBalls - 1) break;
var orig = allBalls[i];
// Create a new Ball with same position and mirrored vx
var newBall = new Ball();
newBall.x = orig.x;
newBall.y = orig.y;
// Mirror vx, keep vy
newBall.vx = -orig.vx;
newBall.vy = orig.vy;
newBall.sticky = false;
ballsToAdd.push(newBall);
}
// Add new balls to game and extraBalls array
for (var i = 0; i < ballsToAdd.length; i++) {
game.addChild(ballsToAdd[i]);
if (!window.extraBalls) window.extraBalls = [];
window.extraBalls.push(ballsToAdd[i]);
}
} else if (p.type === 'ballsplit') {
// Split ball into two, both are main balls
var newBall = new Ball();
newBall.x = ball.x;
newBall.y = ball.y;
newBall.vx = -ball.vx;
newBall.vy = ball.vy;
newBall.sticky = false;
game.addChild(newBall);
if (!window.extraBalls) window.extraBalls = [];
window.extraBalls.push(newBall);
} else if (p.type === 'ballclone') {
// Clone ball with same direction, as a main ball
var newBall = new Ball();
newBall.x = ball.x;
newBall.y = ball.y;
newBall.vx = ball.vx;
newBall.vy = ball.vy;
newBall.sticky = false;
game.addChild(newBall);
if (!window.extraBalls) window.extraBalls = [];
window.extraBalls.push(newBall);
} else if (p.type === 'slow') {
ball.speed = Math.max(10, ball.speed - 6);
ball.vx *= 0.7;
ball.vy *= 0.7;
LK.setTimeout(function () {
ball.speed = 22;
}, 6000);
} else if (p.type === 'fast') {
ball.speed += 8;
ball.vx *= 1.3;
ball.vy *= 1.3;
LK.setTimeout(function () {
ball.speed = 22;
}, 6000);
} else if (p.type === 'bigball') {
ball.scaleX = ball.scaleY = 1.7;
LK.setTimeout(function () {
ball.scaleX = ball.scaleY = 1;
}, 6000);
} else if (p.type === 'smallball') {
ball.scaleX = ball.scaleY = 0.6;
LK.setTimeout(function () {
ball.scaleX = ball.scaleY = 1;
}, 6000);
} else if (p.type === 'scorex2') {
if (!window.scorex2) window.scorex2 = 0;
window.scorex2 += 1;
LK.setTimeout(function () {
window.scorex2 -= 1;
}, 8000);
} else if (p.type === 'scorex3') {
if (!window.scorex3) window.scorex3 = 0;
window.scorex3 += 1;
LK.setTimeout(function () {
window.scorex3 -= 1;
}, 8000);
} else if (p.type === 'blockbomb') {
// Destroy a random block
if (blocks.length > 0) {
var idx = Math.floor(Math.random() * blocks.length);
blocks[idx].breakBlock();
// Remove block from game scene to prevent ghost graphics
if (blocks[idx].parent) blocks[idx].parent.removeChild(blocks[idx]);
blocks.splice(idx, 1);
}
} else if (p.type === 'blockrowclear') {
// Clear a random row
if (blocks.length > 0) {
var yRows = {};
for (var b = 0; b < blocks.length; b++) {
var by = Math.round(blocks[b].y / 10) * 10;
if (!yRows[by]) yRows[by] = [];
yRows[by].push(b);
}
var rowKeys = Object.keys(yRows);
var row = yRows[rowKeys[Math.floor(Math.random() * rowKeys.length)]];
for (var j = row.length - 1; j >= 0; j--) {
blocks[row[j]].breakBlock();
// Remove block from game scene to prevent ghost graphics
if (blocks[row[j]].parent) blocks[row[j]].parent.removeChild(blocks[row[j]]);
blocks.splice(row[j], 1);
}
}
} else if (p.type === 'blockcolclear') {
// Clear a random column
if (blocks.length > 0) {
var xCols = {};
for (var b = 0; b < blocks.length; b++) {
var bx = Math.round(blocks[b].x / 10) * 10;
if (!xCols[bx]) xCols[bx] = [];
xCols[bx].push(b);
}
var colKeys = Object.keys(xCols);
var col = xCols[colKeys[Math.floor(Math.random() * colKeys.length)]];
for (var j = col.length - 1; j >= 0; j--) {
blocks[col[j]].breakBlock();
// Remove block from game scene to prevent ghost graphics
if (blocks[col[j]].parent) blocks[col[j]].parent.removeChild(blocks[col[j]]);
blocks.splice(col[j], 1);
}
}
} else if (p.type === 'ballsplit') {
// Split ball into two
var newBall = new Ball();
newBall.x = ball.x;
newBall.y = ball.y;
newBall.vx = -ball.vx;
newBall.vy = ball.vy;
newBall.sticky = false;
game.addChild(newBall);
if (!window.extraBalls) window.extraBalls = [];
window.extraBalls.push(newBall);
} else if (p.type === 'ballclone') {
// Clone ball with same direction
var newBall = new Ball();
newBall.x = ball.x;
newBall.y = ball.y;
newBall.vx = ball.vx;
newBall.vy = ball.vy;
newBall.sticky = false;
game.addChild(newBall);
if (!window.extraBalls) window.extraBalls = [];
window.extraBalls.push(newBall);
// Removed scorebonus and scorepenalty powerup effects
} else if (p.type === 'blockheal') {
// Heal all blocks by 1
for (var b = 0; b < blocks.length; b++) {
blocks[b].hp = Math.min(blocks[b].maxHp, blocks[b].hp + 1);
if (blocks[b].updateHpBar) blocks[b].updateHpBar();
}
} else if (p.type === 'blockrandom') {
// Randomize block colors
var colors = ['red', 'green', 'blue', 'yellow'];
for (var b = 0; b < blocks.length; b++) {
blocks[b].setColor(colors[Math.floor(Math.random() * colors.length)]);
}
} else if (p.type === 'blockfreeze') {
// Freeze blocks (no effect in this logic, but could be used for animation)
} else if (p.type === 'blockmove') {
// Move all blocks down
for (var b = 0; b < blocks.length; b++) {
blocks[b].y += 40;
}
} else if (p.type === 'blockshrink') {
// Shrink all blocks
for (var b = 0; b < blocks.length; b++) {
blocks[b].scaleX = blocks[b].scaleY = 0.7;
(function (block) {
LK.setTimeout(function () {
block.scaleX = block.scaleY = 1;
}, 6000);
})(blocks[b]);
}
} else if (p.type === 'blockexpand') {
// Expand all blocks
for (var b = 0; b < blocks.length; b++) {
blocks[b].scaleX = blocks[b].scaleY = 1.3;
(function (block) {
LK.setTimeout(function () {
block.scaleX = block.scaleY = 1;
}, 6000);
})(blocks[b]);
}
} else if (p.type === 'ballheavy') {
ball.vy += 8;
LK.setTimeout(function () {
ball.vy -= 8;
}, 6000);
} else if (p.type === 'balllight') {
ball.vy -= 8;
LK.setTimeout(function () {
ball.vy += 8;
}, 6000);
} else if (p.type === 'blockshield') {
// Give all blocks +2 hp
for (var b = 0; b < blocks.length; b++) {
blocks[b].hp = Math.min(blocks[b].maxHp, blocks[b].hp + 2);
if (blocks[b].updateHpBar) blocks[b].updateHpBar();
}
} else if (p.type === 'blockswap') {
// Swap two random blocks
if (blocks.length > 1) {
var i1 = Math.floor(Math.random() * blocks.length);
var i2 = Math.floor(Math.random() * blocks.length);
var tmpx = blocks[i1].x,
tmpy = blocks[i1].y;
blocks[i1].x = blocks[i2].x;
blocks[i1].y = blocks[i2].y;
blocks[i2].x = tmpx;
blocks[i2].y = tmpy;
}
} else if (p.type === 'blockteleport') {
// Teleport a random block to a random position
if (blocks.length > 0) {
var idx = Math.floor(Math.random() * blocks.length);
blocks[idx].x = 400 + Math.random() * 1200;
blocks[idx].y = 400 + Math.random() * 800;
}
} else if (p.type === 'ballteleport') {
ball.x = 400 + Math.random() * 1200;
ball.y = 400 + Math.random() * 800;
} else if (p.type === 'blockregen') {
// Regenerate a destroyed block at random
if (blocks.length > 0) {
var idx = Math.floor(Math.random() * blocks.length);
var block = new Block();
block.setColor(blocks[idx].color);
block.x = blocks[idx].x;
block.y = blocks[idx].y;
block.hp = block.maxHp = 2;
block.setHp(2, 2);
game.addChild(block);
blocks.push(block);
}
} else if (p.type === 'blockexplode') {
// Destroy 3 random blocks
for (var n = 0; n < 3 && blocks.length > 0; n++) {
var idx = Math.floor(Math.random() * blocks.length);
blocks[idx].breakBlock();
// Remove block from game scene to prevent ghost graphics
if (blocks[idx].parent) blocks[idx].parent.removeChild(blocks[idx]);
blocks.splice(idx, 1);
}
} else if (p.type === 'magnet') {
// Ball moves toward paddle for a while
if (!window.magnetActive) {
window.magnetActive = true;
var magnetInterval = LK.setInterval(function () {
if (ball && paddle) {
var dx = paddle.x - ball.x;
ball.vx += dx * 0.01;
}
}, 60);
LK.setTimeout(function () {
LK.clearInterval(magnetInterval);
window.magnetActive = false;
}, 4000);
}
} else if (p.type === 'laser') {
// Laser powerup: show a red laser beam from paddle for a short time
if (!paddle.laserBeam) {
// Create a red rectangle as the laser beam
var laserWidth = paddle.width * (paddle.scaleX || 1) * 0.25;
var laserHeight = paddle.y - paddle.height / 2; // from paddle to very top
var laser = LK.getAsset('block_red', {
anchorX: 0.5,
anchorY: 1,
width: laserWidth,
height: laserHeight,
x: paddle.x,
y: paddle.y - paddle.height / 2,
alpha: 0.7
});
laser.zIndex = 1000;
paddle.laserBeam = laser;
game.addChild(laser);
// Animate laser alpha in and out
tween(laser, {
alpha: 0.9
}, {
duration: 120,
easing: tween.easeIn,
onFinish: function onFinish() {
tween(laser, {
alpha: 0.0
}, {
duration: 400,
delay: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
if (laser && laser.parent) laser.parent.removeChild(laser);
paddle.laserBeam = null;
}
});
}
});
// Destroy all blocks in the laser's vertical path, with explosion effect
for (var i = blocks.length - 1; i >= 0; i--) {
var block = blocks[i];
if (block && block.x >= paddle.x - laserWidth / 2 && block.x <= paddle.x + laserWidth / 2 && block.y < paddle.y - paddle.height / 2 // only above paddle
) {
// Instantly destroy block (no animation)
block.hp = 0;
if (block.updateHpBar) block.updateHpBar();
block.alpha = 0;
block.scaleX = block.scaleY = 2.2;
block.destroy();
if (block.parent) block.parent.removeChild(block);
blocks.splice(i, 1);
score += 10;
updateGUI();
LK.getSound('break').play();
}
}
}
// Removed 'reverse' (ters kontrol) powerup effect
} else if (p.type === 'superball') {
// Ball passes through blocks for a while
ball.superball = true;
LK.setTimeout(function () {
ball.superball = false;
}, 6000);
} else if (p.type === 'fireball') {
// Fireball: Ball burns through blocks for a while, destroys all blocks it touches, and shows fire effect
ball.fireball = true;
// Add a fire visual effect to the ball
if (!ball.fireEffect) {
ball.fireEffect = ball.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5,
width: ball.width * 1.5,
height: ball.height * 1.5,
tint: 0xff6600,
alpha: 0.5
});
ball.fireEffect.zIndex = 99;
}
// Animate fire effect (pulsing)
if (!ball.fireTween) {
ball.fireTween = tween(ball.fireEffect, {
alpha: 0.2
}, {
duration: 300,
yoyo: true,
repeat: 10,
onFinish: function onFinish() {
if (ball.fireEffect) ball.fireEffect.alpha = 0.5;
}
});
}
LK.setTimeout(function () {
ball.fireball = false;
if (ball.fireEffect) {
ball.removeChild(ball.fireEffect);
ball.fireEffect = null;
}
if (ball.fireTween) {
ball.fireTween.stop();
ball.fireTween = null;
}
}, 6000);
} else if (p.type === 'ballcurve') {
// Ball curves for a while
if (!window.curveActive) {
window.curveActive = true;
var curveInterval = LK.setInterval(function () {
if (ball) ball.vx += Math.sin(LK.ticks / 10) * 0.5;
}, 60);
LK.setTimeout(function () {
LK.clearInterval(curveInterval);
window.curveActive = false;
}, 4000);
}
} else if (p.type === 'ballzigzag') {
// Ball zigzags for a while
if (!window.zigzagActive) {
window.zigzagActive = true;
var zigzagInterval = LK.setInterval(function () {
if (ball) ball.vx += LK.ticks % 2 === 0 ? 2 : -2;
}, 60);
LK.setTimeout(function () {
LK.clearInterval(zigzagInterval);
window.zigzagActive = false;
}, 4000);
}
} else if (p.type === 'ballrandom') {
// Removed: ballrandom effect is now disabled
} else if (p.type === 'blockinvisible') {
// All blocks invisible for a while
for (var b = 0; b < blocks.length; b++) {
blocks[b].alpha = 0.1;
(function (block) {
LK.setTimeout(function () {
block.alpha = 1;
}, 6000);
})(blocks[b]);
}
} else if (p.type === 'ballinvisible') {
// All balls (main + extra) invisible for a while
if (ball) {
ball.alpha = 0.1;
LK.setTimeout(function () {
if (ball) ball.alpha = 1;
}, 6000);
}
if (window.extraBalls && window.extraBalls.length) {
for (var eb = 0; eb < window.extraBalls.length; eb++) {
if (window.extraBalls[eb]) {
window.extraBalls[eb].alpha = 0.1;
(function (ebBall) {
LK.setTimeout(function () {
if (ebBall) ebBall.alpha = 1;
}, 6000);
})(window.extraBalls[eb]);
}
}
}
if (powerupBalls && powerupBalls.length) {
for (var pb = 0; pb < powerupBalls.length; pb++) {
if (powerupBalls[pb]) {
powerupBalls[pb].alpha = 0.1;
(function (pbBall) {
LK.setTimeout(function () {
if (pbBall) pbBall.alpha = 1;
}, 6000);
})(powerupBalls[pb]);
}
}
}
} else if (p.type === 'iceball') {
if (ball) {
ball.vx *= 0.7;
ball.vy *= 0.7;
ball.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5,
width: ball.width * 1.2,
height: ball.height * 1.2,
tint: 0x00e0ff,
alpha: 0.3
});
}
LK.setTimeout(function () {
if (ball) {
ball.vx *= 1.43;
ball.vy *= 1.43;
}
}, 6000);
} else if (p.type === 'portal') {
if (blocks.length > 1) {
var b1 = Math.floor(Math.random() * blocks.length);
var b2 = Math.floor(Math.random() * blocks.length);
if (b1 !== b2) {
var tmpx = blocks[b1].x,
tmpy = blocks[b1].y;
blocks[b1].x = blocks[b2].x;
blocks[b1].y = blocks[b2].y;
blocks[b2].x = tmpx;
blocks[b2].y = tmpy;
}
}
} else if (p.type === 'gravity') {
if (ball) {
var origVy = ball.vy;
ball.vy += 20;
LK.setTimeout(function () {
if (ball) ball.vy = origVy;
}, 6000);
}
} else if (p.type === 'mirror') {
if (ball) {
ball.vx = -ball.vx;
}
} else if (p.type === 'shrinkall') {
for (var b = 0; b < blocks.length; b++) {
blocks[b].scaleX = blocks[b].scaleY = 0.5;
(function (block) {
LK.setTimeout(function () {
block.scaleX = block.scaleY = 1;
}, 6000);
})(blocks[b]);
}
} else if (p.type === 'expandall') {
for (var b = 0; b < blocks.length; b++) {
blocks[b].scaleX = blocks[b].scaleY = 1.7;
(function (block) {
LK.setTimeout(function () {
block.scaleX = block.scaleY = 1;
}, 6000);
})(blocks[b]);
}
} else if (p.type === 'swapall') {
if (blocks.length > 1) {
for (var b = 0; b < blocks.length; b += 2) {
if (b + 1 < blocks.length) {
var tmpx = blocks[b].x,
tmpy = blocks[b].y;
blocks[b].x = blocks[b + 1].x;
blocks[b].y = blocks[b + 1].y;
blocks[b + 1].x = tmpx;
blocks[b + 1].y = tmpy;
}
}
}
} else if (p.type === 'rainbow') {
var rainbowColors = [0xff0000, 0xff7f00, 0xffff00, 0x00ff00, 0x0000ff, 0x4b0082, 0x9400d3];
for (var b = 0; b < blocks.length; b++) {
blocks[b].setColor(['red', 'green', 'blue', 'yellow'][Math.floor(Math.random() * 4)]);
blocks[b].blockSprite.tint = rainbowColors[Math.floor(Math.random() * rainbowColors.length)];
(function (block) {
LK.setTimeout(function () {
block.blockSprite.tint = 0xffffff;
}, 6000);
})(blocks[b]);
}
} else if (p.type === 'cloneblock') {
if (blocks.length > 0) {
var idx = Math.floor(Math.random() * blocks.length);
var orig = blocks[idx];
var clone = new Block();
clone.setColor(orig.color);
clone.x = orig.x + 60 + Math.random() * 120;
clone.y = orig.y + 60 + Math.random() * 120;
clone.hp = orig.hp;
clone.maxHp = orig.maxHp;
clone.setHp(clone.hp, clone.maxHp);
game.addChild(clone);
blocks.push(clone);
}
} else if (p.type === 'blockfall') {
for (var b = 0; b < blocks.length; b++) {
blocks[b].y += 120;
}
} else if (p.type === 'blockrise') {
for (var b = 0; b < blocks.length; b++) {
blocks[b].y -= 120;
}
}
p.destroy();
powerups.splice(i, 1);
}
}
}
// Ball physics
if (ball && _typeof(ball) === "object" && !ball.sticky) {
// --- Wall bounce tracking ---
if (typeof ball.leftWallHits === "undefined") ball.leftWallHits = 0;
if (typeof ball.rightWallHits === "undefined") ball.rightWallHits = 0;
// --- Track if ball has hit a block since last paddle reset ---
if (typeof ball.hasHitBlockSinceReset === "undefined") ball.hasHitBlockSinceReset = false;
// --- Track consecutive left/right wall bounces without block hit ---
if (typeof ball.wallBouncesNoBlock === "undefined") ball.wallBouncesNoBlock = 0;
// Wall collisions
var hitWall = false;
if (ball.x - ball.radius < 0) {
ball.x = ball.radius;
ball.vx = -ball.vx;
LK.getSound('hit').play();
ball.leftWallHits++;
hitWall = true;
}
if (ball.x + ball.radius > 2048) {
ball.x = 2048 - ball.radius;
ball.vx = -ball.vx;
LK.getSound('hit').play();
ball.rightWallHits++;
hitWall = true;
}
if (hitWall) {
// Only count if no block hit since last paddle reset
if (!ball.hasHitBlockSinceReset) {
ball.wallBouncesNoBlock++;
}
}
if (ball.y - ball.radius < 0) {
ball.y = ball.radius;
ball.vy = -ball.vy;
LK.getSound('hit').play();
}
// If left or right wall hit 15 or more times without hitting any block, make ALL balls accelerate upward even faster
if (ball.wallBouncesNoBlock >= 15) {
var accelerateUpwardFaster = function accelerateUpwardFaster(b) {
// Only accelerate if not already moving extremely fast upward
if (b.vy > -Math.abs(b.speed ? b.speed * 20 : 440)) {
b.vx = 0;
b.vy = -Math.abs(b.speed ? b.speed * 20 : 440); // Use 20x speed or 440 as fallback
b.sticky = false;
}
if (typeof b.leftWallHits !== "undefined") b.leftWallHits = 0;
if (typeof b.rightWallHits !== "undefined") b.rightWallHits = 0;
if (typeof b.wallBouncesNoBlock !== "undefined") b.wallBouncesNoBlock = 0;
if (typeof b.hasHitBlockSinceReset !== "undefined") b.hasHitBlockSinceReset = false;
};
// Main ball
if (ball) accelerateUpwardFaster(ball);
// Extra balls
if (window.extraBalls && window.extraBalls.length) {
for (var eb = 0; eb < window.extraBalls.length; eb++) {
if (window.extraBalls[eb]) accelerateUpwardFaster(window.extraBalls[eb]);
}
}
// Powerup balls
if (powerupBalls && powerupBalls.length) {
for (var pb = 0; pb < powerupBalls.length; pb++) {
if (powerupBalls[pb]) accelerateUpwardFaster(powerupBalls[pb]);
}
}
} else if (ball.wallBouncesNoBlock >= 10) {
// Helper to shoot all balls upward extremely fast
var shootUpward = function shootUpward(b) {
// Only accelerate if not already moving extremely fast upward
if (b.vy > -Math.abs(b.speed ? b.speed * 50 : 1320)) {
b.vx = 0;
b.vy = -Math.abs(b.speed ? b.speed * 50 : 1320); // Use 50x speed or 1320 as fallback
b.sticky = false;
}
if (typeof b.leftWallHits !== "undefined") b.leftWallHits = 0;
if (typeof b.rightWallHits !== "undefined") b.rightWallHits = 0;
if (typeof b.wallBouncesNoBlock !== "undefined") b.wallBouncesNoBlock = 0;
if (typeof b.hasHitBlockSinceReset !== "undefined") b.hasHitBlockSinceReset = false;
};
// Main ball
if (ball) shootUpward(ball);
// Extra balls
if (window.extraBalls && window.extraBalls.length) {
for (var eb = 0; eb < window.extraBalls.length; eb++) {
if (window.extraBalls[eb]) shootUpward(window.extraBalls[eb]);
}
}
// Powerup balls
if (powerupBalls && powerupBalls.length) {
for (var pb = 0; pb < powerupBalls.length; pb++) {
if (powerupBalls[pb]) shootUpward(powerupBalls[pb]);
}
}
}
// Paddle collision
if (paddle && typeof paddle.getHitbox === "function") {
var hitbox = paddle.getHitbox();
if (ball.y + ball.radius >= hitbox.top && ball.y - ball.radius <= hitbox.bottom && ball.x + ball.radius >= hitbox.left && ball.x - ball.radius <= hitbox.right && ball.vy > 0) {
// Reflect ball, angle based on where it hit the paddle
var rel = (ball.x - paddle.x) / (paddle.width / 2 * paddle.scaleX);
var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
ball.vx = Math.cos(angle) * speed;
ball.vy = Math.sin(angle) * speed;
// Always place ball visually above paddle after collision
ball.y = paddle.y - paddle.height / 2 - ball.radius - 2;
if (ball.y + ball.radius > hitbox.top) {
ball.y = paddle.y - paddle.height / 2 - ball.radius - 2;
}
LK.getSound('hit').play();
}
} else {
if (ball.y + ball.radius >= paddle.y - paddle.height / 2 && ball.y + ball.radius <= paddle.y + paddle.height / 2 && ball.x >= paddle.x - paddle.width / 2 * paddle.scaleX && ball.x <= paddle.x + paddle.width / 2 * paddle.scaleX && ball.vy > 0) {
// Reflect ball, angle based on where it hit the paddle
var rel = (ball.x - paddle.x) / (paddle.width / 2 * paddle.scaleX);
var angle = -Math.PI / 3 + rel * (Math.PI / 3); // -60deg to 60deg
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
ball.vx = Math.cos(angle) * speed;
ball.vy = Math.sin(angle) * speed;
ball.y = paddle.y - paddle.height / 2 - ball.radius - 2;
LK.getSound('hit').play();
}
}
// Bottom out
if (ball.y - ball.radius > 2732) {
// If there is at least one powerup ball on screen, continue with it as the main ball
if (powerupBalls && powerupBalls.length > 0) {
// Remove the main ball, but do not call loseLife
ball.destroy();
ball = null;
// Promote the first powerup ball to be the new main ball
var promoted = powerupBalls.shift();
// Remove from game and re-add as main ball for correct update/collision
if (promoted && promoted.parent) promoted.parent.removeChild(promoted);
// Create a new Ball instance at the promoted ball's position and velocity
ball = new Ball();
ball.x = promoted.x;
ball.y = promoted.y;
ball.vx = promoted.vx;
ball.vy = promoted.vy;
ball.sticky = false;
game.addChild(ball);
// Remove the promoted powerup ball
promoted.destroy();
// Do not return, continue game loop
} else {
loseLife();
return;
}
}
}
// Block collisions
if (!blocks || !Array.isArray(blocks)) blocks = [];
// --- OPTIMIZATION: For level 6+, only check a subset of blocks per frame to reduce lag ---
var blockCheckStep = level >= 6 ? 2 : 1;
for (var i = blocks.length - 1; i >= 0; i -= blockCheckStep) {
var block = blocks[i];
// OPTIMIZATION: Skip destroyed or off-screen blocks
if (!block || block.destroyed || block.y < -200 || block.y > 2732 + 200) continue;
// --- Call update for MovingBlock ---
if (block.blockType === 'moving' && typeof block.update === "function") {
block.update();
}
// --- ShieldedBlock logic: only allow breaking if another block was destroyed this frame ---
if (block.blockType === 'shielded' && block.shielded) {
// Check if any other block was destroyed this frame
if (!game._shieldedUnlock) {
// Prevent breaking shielded block
continue;
}
}
var hit = false;
// Check main ball
if (ball && block && ball.intersects(block)) {
// Fireball: instantly destroy block and keep moving
if (ball.fireball) {
// Burn block: show a flash and destroy instantly (no animation)
if (block && block.blockSprite) {
LK.effects.flashObject(block, 0xff6600, 80);
}
block.hp = 0;
if (block.updateHpBar) block.updateHpBar();
block.alpha = 0;
block.scaleX = block.scaleY = 1.5;
block.destroy();
if (block.parent) block.parent.removeChild(block);
blocks.splice(i, 1);
combo++;
startCombo();
score += 10 * combo;
updateGUI();
LK.getSound('break').play();
// Power-up chance (increased drop rate)
var powerupDropRate = 0.22 + Math.min(0.01 * level, 0.18);
if (Math.random() < powerupDropRate) {
spawnPowerUp(block.x, block.y);
}
hit = true;
// Do not reflect ball, let it keep moving
// --- Mark that a block was destroyed for shielded logic ---
game._shieldedUnlock = true;
} else {
// Reflect ball
var overlapX = Math.abs(ball.x - block.x) - (block.blockSprite.width / 2 + ball.radius);
var overlapY = Math.abs(ball.y - block.y) - (block.blockSprite.height / 2 + ball.radius);
if (overlapX > overlapY) {
ball.vx = -ball.vx;
} else {
ball.vy = -ball.vy;
}
hit = true;
}
}
// Check extra balls
if (!hit && window.extraBalls && window.extraBalls.length) {
for (var eb = 0; eb < window.extraBalls.length; eb++) {
var ebBall = window.extraBalls[eb];
if (ebBall && block && ebBall.intersects(block)) {
// Reflect extra ball
var overlapX = Math.abs(ebBall.x - block.x) - (block.blockSprite.width / 2 + ebBall.radius);
var overlapY = Math.abs(ebBall.y - block.y) - (block.blockSprite.height / 2 + ebBall.radius);
if (overlapX > overlapY) {
ebBall.vx = -ebBall.vx;
} else {
ebBall.vy = -ebBall.vy;
}
hit = true;
break;
}
}
}
if (hit) {
// Mark that ball has hit a block, so wall bounce counter pauses until next reset
if (ball && typeof ball.hasHitBlockSinceReset !== "undefined") {
ball.hasHitBlockSinceReset = true;
ball.wallBouncesNoBlock = 0;
}
if (window.extraBalls && window.extraBalls.length) {
for (var eb = 0; eb < window.extraBalls.length; eb++) {
var ebBall = window.extraBalls[eb];
if (ebBall && typeof ebBall.hasHitBlockSinceReset !== "undefined") {
ebBall.hasHitBlockSinceReset = true;
ebBall.wallBouncesNoBlock = 0;
}
}
}
if (powerupBalls && powerupBalls.length) {
for (var pb = 0; pb < powerupBalls.length; pb++) {
var pbBall = powerupBalls[pb];
if (pbBall && typeof pbBall.hasHitBlockSinceReset !== "undefined") {
pbBall.hasHitBlockSinceReset = true;
pbBall.wallBouncesNoBlock = 0;
}
}
}
// Block hit
// --- ShieldedBlock unlock logic ---
if (block.blockType === 'shielded' && block.shielded) {
// Only unlock if another block was destroyed this frame
if (game._shieldedUnlock) {
block.shielded = false;
block.setHp(block.hp, block.maxHp);
block.blockSprite.tint = 0xffffff;
} else {
// Can't break yet, flash
LK.effects.flashObject(block, 0x00e0ff, 300);
continue;
}
}
block.hp--;
if (block.updateHpBar) block.updateHpBar();
combo++;
startCombo();
score += 10 * combo;
updateGUI();
LK.getSound('break').play();
// Power-up chance (increased drop rate)
var powerupDropRate = 0.22 + Math.min(0.01 * level, 0.18); // increases with level, max ~0.4
if (Math.random() < powerupDropRate) {
spawnPowerUp(block.x, block.y);
}
if (block.hp <= 0) {
block.breakBlock();
// Remove block from game scene to prevent ghost graphics
if (block.parent) block.parent.removeChild(block);
blocks.splice(i, 1);
// --- Mark that a block was destroyed for shielded logic ---
game._shieldedUnlock = true;
}
break; // Only one block per frame
}
}
// Combo reset if no block hit for a while
if (combo > 0 && !comboTimer) {
resetCombo();
}
// Level clear
if (!levelCleared && blocks.length === 0) {
levelCleared = true;
// Remove all balls except the main ball
if (window.extraBalls && Array.isArray(window.extraBalls)) {
for (var i = window.extraBalls.length - 1; i >= 0; i--) {
if (window.extraBalls[i]) window.extraBalls[i].destroy();
window.extraBalls.splice(i, 1);
}
}
if (powerupBalls && Array.isArray(powerupBalls)) {
for (var i = powerupBalls.length - 1; i >= 0; i--) {
if (powerupBalls[i]) powerupBalls[i].destroy();
powerupBalls.splice(i, 1);
}
}
if (comboTimer) {
LK.clearTimeout(comboTimer);
comboTimer = null;
}
LK.setTimeout(function () {
nextLevel();
}, 1200);
}
// --- Random powerup spawn logic ---
if (typeof game.lastPowerupSpawnTick === "undefined") {
game.lastPowerupSpawnTick = LK.ticks;
}
if (typeof game.powerupSpawnInterval === "undefined") {
// Try to spawn a powerup every 6-12 seconds (randomized), but decrease interval as level increases (min 120)
game.powerupSpawnInterval = Math.max(120, 360 + Math.floor(Math.random() * 360) - level * 12);
}
if (typeof game.lastPowerupBallTick === "undefined") {
game.lastPowerupBallTick = LK.ticks;
}
if (typeof game.powerupBallInterval === "undefined") {
// Try to spawn a powerup ball every 8-16 seconds (randomized), but decrease interval as level increases (min 180)
game.powerupBallInterval = Math.max(180, 480 + Math.floor(Math.random() * 480) - level * 16);
}
if (LK.ticks - game.lastPowerupSpawnTick > game.powerupSpawnInterval) {
// Only spawn if there are less than 3 powerups on screen
if (powerups.length < 3 && blocks.length > 0) {
spawnPowerUp(); // No x/y: will spawn at random position
}
game.lastPowerupSpawnTick = LK.ticks;
game.powerupSpawnInterval = 360 + Math.floor(Math.random() * 360);
}
// --- Random powerup ball spawn logic ---
if (LK.ticks - game.lastPowerupBallTick > game.powerupBallInterval) {
// Only spawn if there are less than 3 powerup balls on screen
if (powerupBalls.length < 3) {
// Pick a random type
var types = [PowerupBall1, PowerupBall2, PowerupBall3];
var idx = Math.floor(Math.random() * types.length);
var PowerupBallClass = types[idx];
var pb = new PowerupBallClass();
// Spawn at random X, top of screen
pb.x = 100 + Math.random() * (2048 - 200);
pb.y = -pb.radius - 10;
game.addChild(pb);
powerupBalls.push(pb);
}
game.lastPowerupBallTick = LK.ticks;
game.powerupBallInterval = 480 + Math.floor(Math.random() * 480);
}
// Update powerup icon timers and show countdown for active powerups
if (!window.activePowerups || !Array.isArray(window.activePowerups)) window.activePowerups = [];
if (!window.powerupIcons || !Array.isArray(window.powerupIcons)) window.powerupIcons = [];
if (window.activePowerups.length && window.powerupIcons.length) {
var now = Date.now();
for (var i = 0; i < window.activePowerups.length; i++) {
var ap = window.activePowerups[i];
var icon = window.powerupIcons[i];
if (ap && icon) {
var remain = Math.max(0, Math.ceil((ap.end - now) / 1000));
// Show timer only if more than 0 seconds left, otherwise just the icon
if (remain > 0) {
icon.setText(ap.type[0].toUpperCase() + "\n" + remain);
} else {
icon.setText(ap.type[0].toUpperCase());
}
// Reposition icons in a row
icon.x = 2048 / 2 - (window.activePowerups.length - 1) * 50 + i * 100 / 2 + i * 0;
icon.y = 170;
}
}
}
};
// Music (optional, not required by MVP, so not included)
/* End of gamecode.js */