User prompt
Tek bir top spawn olsun
User prompt
Toplar hareket etmeden önce gözükmesin
User prompt
Bir top spawn olmadan önce diğer toplar spawn olmasın ve bu spawn olma sürekli olsun oyun sonuna kadar
User prompt
Spiral yolundaki gelecek topu gösterme
User prompt
When a new ball is spawned, do not show it immediately. Wait until the previous ball moves one step, then show the new ball at the spawn point. Ensure all balls in the chain have equal spacing — they should always be close and touching each other. Maintain this fixed distance throughout the entire chain, from the first to the last ball. Balls should move like a connected snake without gaps or overlaps.
User prompt
Ball1 spawns at the spiral start point and moves forward one step at a time. When Ball1 moves one step, spawn Ball2 at the spiral start point. Ball2 follows Ball1's path with a delay of one step. When Ball2 moves one step, spawn Ball3 at the spiral start point. Ball3 follows Ball2's path with a delay of one step. Repeat this pattern for Ball4, Ball5, and so on. Each ball should follow the previous one like a snake. If Ball1 reaches the spiral end, stop the game.
User prompt
Ball1 spawns at the spiral start point and moves forward one step at a time. When Ball1 moves one step, spawn Ball2 at the spiral start point. Ball2 follows Ball1's path with a delay of one step. When Ball2 moves one step, spawn Ball3 at the spiral start point. Ball3 follows Ball2's path with a delay of one step. Repeat this pattern for Ball4, Ball5, and so on. Each ball should follow the previous one like a snake. If Ball1 reaches the spiral end, stop the game.
User prompt
Implement snake-like spawning and following for chain balls along spiral path ✅ Initialize only Ball1 at spiral start, set up snake-following state ✅ Implement snake-like stepwise movement and spawning for chain balls
User prompt
When a new ball is spawned, it should be invisible and not added to the scene. Wait until the last visible ball moves one full step forward. Then make the new ball visible and add it to the scene at the spawn point. Repeat this for each new ball to keep the chain clean and orderly.
User prompt
Spiral yol boyunca zincir toplar için yılan benzeri doğma ve takip etme sistemini uygula.
User prompt
Do it
User prompt
When a new ball is spawned, it should be invisible. Do not add it to the scene visually yet. Wait until the last ball has moved one full step forward. Then, make the new ball visible at the spawn point and start its movement. This way, only one new ball appears at a time, right after the previous one moves.
User prompt
Do not make the new ball visible when it's spawned. Only make the ball visible when the previous ball has moved at least one full step. To keep the balls close together, each ball must move only when the ball in front of it has moved. Each ball must maintain a constant distance of exactly one step from the ball in front. Do not allow gaps or overlaps between the balls. The chain must always look like a connected row of balls moving smoothly.
User prompt
When a new ball is spawned, do not show it immediately. Wait until the previous ball moves one step, then show the new ball at the spawn point. Ensure all balls in the chain have equal spacing — they should always be close and touching each other. Maintain this fixed distance throughout the entire chain, from the first to the last ball. Balls should move like a connected snake without gaps or overlaps.
User prompt
Ball1 spawns at the spiral start point and moves forward one step at a time. When Ball1 moves one step, spawn Ball2 at the spiral start point. Ball2 follows Ball1's path with a delay of one step. When Ball2 moves one step, spawn Ball3 at the spiral start point. Ball3 follows Ball2's path with a delay of one step. Repeat this pattern for Ball4, Ball5, and so on. Each ball should follow the previous one like a snake. If Ball1 reaches the spiral end, stop the game.
User prompt
At the start, ballChain is empty. Spawn a ball at the spiral start point and add it to ballChain. When the last ball in ballChain moves one step forward, spawn a new ball and add it to the end of ballChain. Each ball in ballChain should follow the position of the ball before it, like a train. Continue this process until the first ball reaches the end of the spiral, then stop spawning and end the game.
User prompt
Create a list called ballChain. At the start, ballChain is empty. Spawn a ball at the spiral start point and add it to ballChain. When the last ball in ballChain moves one step forward, spawn a new ball and add it to the end of ballChain. Each ball in ballChain should follow the position of the ball before it, like a train. Continue this process until the first ball reaches the end of the spiral, then stop spawning and end the game.
User prompt
Please fix the bug: 'ReferenceError: chainBalls is not defined' in or related to this line: 'if (chainBalls.length === 0) {' Line Number: 739
User prompt
Create a list called ballChain. At the start, ballChain is empty. Spawn a ball at the spiral start point and add it to ballChain. When the last ball in ballChain moves one step forward, spawn a new ball and add it to the end of ballChain. Each ball in ballChain should follow the position of the ball before it, like a train. Continue this process until the first ball reaches the end of the spiral, then stop spawning and end the game.
User prompt
Balls should spawn one by one from the spiral start point. Do not fill the spiral all at once. Each new ball must be added after the last ball (at the end of the chain). Wait until the last ball moves one step forward before spawning the next ball. Balls should follow each other on the spiral path like a snake. When the first ball reaches the end of the spiral, the game is over.
User prompt
Balls should spawn one by one from the spiral start point. Do not fill the spiral all at once. Each new ball must be added after the last ball (at the end of the chain). Wait until the last ball moves one step forward before spawning the next ball. Balls should follow each other on the spiral path like a snake. When the first ball reaches the end of the spiral, the game is over.
User prompt
Spiral yolun başlangıç noktasından toplar tek tek spawn olsun. Her top, kendisinden önce spawn olan top bir adım ilerleyince spawn olsun. Yeni top ön tarafa değil, en sona (kuyruğa) eklensin. Böylece spiral, yavaşça ve sırayla dolsun. İlk top spiral sonuna ulaşırsa oyun bitsin. Oyuncu shooter ile top fırlatır. Aynı renkten 3 veya daha fazla top yan yana gelirse patlasın. Patlayan topların boşluğunu arkadaki toplar doldursun. Tüm toplar yok edilirse oyuncu kazanır.
User prompt
Spiral yolun başlangıç noktasından toplar sırayla tek tek spawn olsun. Her top, bir birim ilerledikten sonra yeni bir top spawn olsun. Her yeni top, kendisinden önceki topun peşinden aynı yoldan ilerlesin. Bu döngü, spiral bitene kadar devam etsin. İlk top spiral sonuna ulaşırsa oyun bitsin. Oyuncu, shooter ile top atınca, top hareket eden topların arasına girsin. Aynı renkten 3 veya daha fazla top yan yana gelirse patlasın. Patlayan topların boşluğu kapanacak şekilde arkadaki toplar öne gelsin. Tüm toplar patlarsa oyuncu oyunu kazansın.
User prompt
Toplar ilk noktada tek tek spawn olup, tek tek ilerlesinler. Spawn ve hareket hızı birbiriyle aynı sürede olsun.
User prompt
Toplar ilk noktada tek tek spawn olup, tek tek ilerlesinler.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Ball class for both chain and shot balls var Ball = Container.expand(function () { var self = Container.call(this); // color: 'red', 'blue', 'green', 'yellow' self.color = 'red'; self.radius = 67.5; // 135/2, for collision (1.5x) self.isChain = true; // true if part of chain, false if shot // Attach correct asset (always ellipse) var assetId = 'ball_' + self.color; var ballAsset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); // For chain balls, we keep a t value (0-1) for position along the path self.t = 0; // For shot balls, we keep velocity self.vx = 0; self.vy = 0; // For chain balls, we keep index in chain self.chainIndex = 0; // For shot balls, we keep a flag if it's active self.active = true; // Set color and update asset self.setColor = function (color) { self.color = color; var assetId = 'ball_' + color; // always ellipse // Remove old asset if (ballAsset) ballAsset.destroy(); // Attach new asset (always ellipse) var newAsset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); }; return self; }); // Shooter class var Shooter = Container.expand(function () { var self = Container.call(this); // Attach shooter asset var shooterAsset = self.attachAsset('shooter', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2 }); // The ball to be shot (preview) self.previewBall = null; // The angle the shooter is aiming (radians) self.angle = -Math.PI / 2; // Set preview ball color self.setPreviewColor = function (color) { if (self.previewBall) { self.previewBall.destroy(); } self.previewBall = new Ball(); self.previewBall.isChain = false; self.previewBall.setColor(color); // Ball class always uses ellipse asset // Place preview ball at the center of the shooter self.previewBall.x = 0; self.previewBall.y = 0; self.addChild(self.previewBall); }; // Set shooter angle (radians) self.setAngle = function (angle) { self.angle = angle; self.rotation = angle + Math.PI / 2; // Do not update previewBall position here to keep nextball fixed at shooter center }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222222 }); /**** * Game Code ****/ // Play background music for the entire game session LK.playMusic('music'); /* We'll define a spiral path as a set of points (x, y) along which the chain balls move. For MVP, use a simple Archimedean spiral centered in the screen. We'll precompute N points along the spiral and interpolate between them. */ // --- Spiral Path Definition --- // Ball shapes (4 colors for MVP) // Shooter (frog or cannon, simple green ellipse for MVP) // Ball shoot sound // Ball pop sound var PATH_POINTS = []; var PATH_LENGTH = 1200; // Number of points along the path // Use almost the full screen for the spiral and shooter var SPIRAL_TURNS = 2.2; // How many turns var SPIRAL_RADIUS = 1020; // Max radius (slightly smaller diameter) var PATH_CENTER_X = 2048 / 2 + 24; // moved slightly right var PATH_CENTER_Y = 2732 / 2 - 80; // Add background image behind all elements, centered and scaled to cover the game area var bgAsset = LK.getAsset('background', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, // Scale to cover the entire area, preserving aspect ratio scaleX: Math.max(2048 / 2048, 2732 / 2148.72), scaleY: Math.max(2048 / 2048, 2732 / 2148.72) }); game.addChild(bgAsset); // Spiral path visual: draw as small dots var spiralPathDots = new Container(); game.addChild(spiralPathDots); (function generateSpiralPath() { // First, generate a high-res spiral var HIGH_RES = 6000; var spiralHighRes = []; // Perfect circle spiral, but offset the starting point toward bottom-right, keeping center fixed // We'll add an offset to the starting angle and radius to shift the spiral's start var spiralStartAngleOffset = Math.PI / 6; // 30 degrees, adjust for more/less rightward var spiralStartRadiusOffsetX = 180; // px, rightward var spiralStartRadiusOffsetY = 220; // px, downward for (var i = 0; i < HIGH_RES; i++) { var t = i / (HIGH_RES - 1); // 0 to 1 var angle = SPIRAL_TURNS * 2 * Math.PI * (1 - t) + Math.PI + spiralStartAngleOffset; // Start angle offset // Start radius at 220 to leave space for shooter, fill to edge var radius = 220 + SPIRAL_RADIUS * (1 - t); var x = PATH_CENTER_X + Math.cos(angle) * radius + spiralStartRadiusOffsetX * (1 - t); var y = PATH_CENTER_Y + Math.sin(angle) * radius + spiralStartRadiusOffsetY * (1 - t); // Mirror x around center to flip horizontally x = PATH_CENTER_X - (x - PATH_CENTER_X); spiralHighRes.push({ x: x, y: y }); } // Compute cumulative arc length var arcLen = [0]; for (var i = 1; i < spiralHighRes.length; i++) { var dx = spiralHighRes[i].x - spiralHighRes[i - 1].x; var dy = spiralHighRes[i].y - spiralHighRes[i - 1].y; arcLen[i] = arcLen[i - 1] + Math.sqrt(dx * dx + dy * dy); } var totalLen = arcLen[arcLen.length - 1]; // Now, sample PATH_LENGTH points at equal arc length intervals for (var i = 0; i < PATH_LENGTH; i++) { var targetLen = i * totalLen / (PATH_LENGTH - 1); // Binary search for the closest arcLen index var lo = 0, hi = arcLen.length - 1; while (lo < hi) { var mid = Math.floor((lo + hi) / 2); if (arcLen[mid] < targetLen) lo = mid + 1;else hi = mid; } var idx = lo; // Interpolate between idx-1 and idx var p0 = spiralHighRes[Math.max(0, idx - 1)]; var p1 = spiralHighRes[idx]; var l0 = arcLen[Math.max(0, idx - 1)]; var l1 = arcLen[idx]; var frac = l1 - l0 > 0 ? (targetLen - l0) / (l1 - l0) : 0; var x = p0.x + (p1.x - p0.x) * frac; var y = p0.y + (p1.y - p0.y) * frac; PATH_POINTS.push({ x: x, y: y }); // Draw a small dot every 8th point for performance if (i % 8 === 0) { var dot = LK.getAsset('dot', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.7, scaleY: 0.7, x: x, y: y, alpha: 0.45 }); spiralPathDots.addChild(dot); } } })(); // Helper: get position along path for t in [0,1] function getPathPos(t) { var idx = t * (PATH_LENGTH - 1); var idx0 = Math.floor(idx); var idx1 = Math.min(PATH_LENGTH - 1, idx0 + 1); var frac = idx - idx0; var p0 = PATH_POINTS[idx0]; var p1 = PATH_POINTS[idx1]; if (!p0 || !p1) { // Defensive: If out of bounds, return center of screen return { x: PATH_CENTER_X, y: PATH_CENTER_Y }; } return { x: p0.x + (p1.x - p0.x) * frac, y: p0.y + (p1.y - p0.y) * frac }; } // Helper: get tangent angle at t function getPathAngle(t) { var idx = t * (PATH_LENGTH - 1); var idx0 = Math.max(0, Math.floor(idx) - 2); var idx1 = Math.min(PATH_LENGTH - 1, idx0 + 4); var p0 = PATH_POINTS[idx0]; var p1 = PATH_POINTS[idx1]; return Math.atan2(p1.y - p0.y, p1.x - p0.x); } // --- Chain State --- var chainBalls = []; // Array of Ball (in order, head at index 0) var chainSpacing = 135; // px between centers (balls touch: 135px ball diameter, 1.5x) var chainSpeed = 0.00018; // t per tick (even slower movement for chain balls) var chainHeadT = 0; // t of the head ball (0=start, 1=end) var chainTailT = 0; // t of the tail ball // --- Shooter State --- var shooter = new Shooter(); // Place shooter at exact center of the screen var shooterAssetSize = LK.getAsset('shooter', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2 }); shooter.x = 2048 / 2; shooter.y = 2732 / 2; game.addChild(shooter); // --- Shooter Guide Line --- // We'll use a series of small dots (like spiralPathDots) to show the aim direction var shooterGuideDots = new Container(); game.addChild(shooterGuideDots); // Helper to update shooter guide line function updateShooterGuide() { // Remove old dots while (shooterGuideDots.children.length > 0) { shooterGuideDots.children[0].destroy(); } // Always show shooter guide line (removed isAiming check) // Draw dots every 50px, up to the screen edge var angle = shooter.angle; var sx = shooter.x + Math.cos(angle) * 60; var sy = shooter.y + Math.sin(angle) * 60; // Calculate how far we can go in this direction before hitting the screen edge function getEdgeDistance(x, y, angle) { // Calculate intersection with each edge, return the smallest positive distance var dx = Math.cos(angle); var dy = Math.sin(angle); var distances = []; // Left edge (x=0) if (dx < 0) { var t = (0 - x) / dx; if (t > 0) distances.push(t); } // Right edge (x=2048) if (dx > 0) { var t = (2048 - x) / dx; if (t > 0) distances.push(t); } // Top edge (y=0) if (dy < 0) { var t = (0 - y) / dy; if (t > 0) distances.push(t); } // Bottom edge (y=2732) if (dy > 0) { var t = (2732 - y) / dy; if (t > 0) distances.push(t); } if (distances.length === 0) return 0; return Math.min.apply(null, distances); } var maxDist = getEdgeDistance(shooter.x, shooter.y, angle) - 60; // minus shooter offset var lenStep = 50; var steps = Math.floor(maxDist / lenStep); for (var i = 1; i <= steps; i++) { var px = shooter.x + Math.cos(angle) * (60 + i * lenStep); var py = shooter.y + Math.sin(angle) * (60 + i * lenStep); // Don't draw outside game area (defensive) if (px < 0 || px > 2048 || py < 0 || py > 2732) break; var dot = LK.getAsset('dot', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.5, scaleY: 0.5, x: px, y: py, alpha: 0.7 }); shooterGuideDots.addChild(dot); } } // --- Shot Balls State --- var shotBalls = []; // Array of Ball // --- Game State --- var colors = ['red', 'blue', 'green', 'yellow']; var nextBallColor = colors[Math.floor(Math.random() * colors.length)]; var score = 0; var scoreTxt = new Text2('0', { size: 120, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // --- Initialize Chain --- // --- Chain spawn state --- var chainSpawnIndex = 0; var chainSpawnTimer = 0; var chainInitialCount = 10; // Number of balls to spawn at start function initChain() { chainBalls = []; chainSpawnIndex = 0; chainSpawnTimer = 0; chainHeadT = 0; chainTailT = 0; } initChain(); // --- Initialize Shooter --- shooter.setPreviewColor(nextBallColor); shooter.setAngle(-Math.PI / 2); updateShooterGuide(); // --- Dragging / Aiming State --- var isAiming = false; // --- Helper: Find angle from shooter to (x, y) --- function getShooterAngleTo(x, y) { var dx = x - shooter.x; var dy = y - shooter.y; return Math.atan2(dy, dx); } // --- Helper: Find nearest t on path to (x, y) --- function findNearestT(x, y) { // Brute force for MVP var minDist = 1e9; var minIdx = 0; for (var i = 0; i < PATH_POINTS.length; i += 8) { // Step for speed var p = PATH_POINTS[i]; var dx = p.x - x; var dy = p.y - y; var d = dx * dx + dy * dy; if (d < minDist) { minDist = d; minIdx = i; } } return minIdx / (PATH_LENGTH - 1); } // --- Game Events --- // Aim shooter game.move = function (x, y, obj) { if (!isAiming) return; var angle = getShooterAngleTo(x, y); shooter.setAngle(angle); updateShooterGuide(); }; // Start aiming game.down = function (x, y, obj) { // Only allow aiming if touch is not on top 400px (avoid accidental shots) if (y < 400) return; isAiming = true; var angle = getShooterAngleTo(x, y); shooter.setAngle(angle); updateShooterGuide(); }; // Shoot ball game.up = function (x, y, obj) { if (!isAiming) return; isAiming = false; updateShooterGuide(); // Fire a ball in the current shooter.angle var ball = new Ball(); ball.isChain = false; ball.setColor(nextBallColor); var angle = shooter.angle; var speed = 120; // px per tick (much faster for shooter) ball.vx = Math.cos(angle) * speed; ball.vy = Math.sin(angle) * speed; ball.x = shooter.x + Math.cos(angle) * 60; ball.y = shooter.y + Math.sin(angle) * 60; ball.active = true; shotBalls.push(ball); game.addChild(ball); // Play shoot sound LK.getSound('shoot').play(); // Next ball color nextBallColor = colors[Math.floor(Math.random() * colors.length)]; shooter.setPreviewColor(nextBallColor); }; // --- Helper: Insert ball into chain at index --- function insertBallIntoChain(ball, insertIdx, insertT) { // Insert ball into chainBalls at insertIdx, set t=insertT ball.isChain = true; ball.t = insertT; ball.chainIndex = insertIdx; // Remove from shotBalls for (var i = 0; i < shotBalls.length; i++) { if (shotBalls[i] === ball) { shotBalls.splice(i, 1); break; } } // Insert into chainBalls chainBalls.splice(insertIdx, 0, ball); // Re-index and re-t for (var i = 0; i < chainBalls.length; i++) { chainBalls[i].chainIndex = i; } // Adjust t for all balls after insertIdx for (var i = insertIdx + 1; i < chainBalls.length; i++) { chainBalls[i].t += chainSpacing / SPIRAL_RADIUS / (SPIRAL_TURNS * 2 * Math.PI); } } // --- Helper: Check for matches and pop balls --- // Only pop the run that contains the most recently inserted ball (if any) function checkAndPopMatches(insertedBall) { // If no insertedBall is provided, fallback to old behavior (should not happen in normal flow) if (!insertedBall) { // fallback: do nothing return false; } // Find the run containing insertedBall var idx = chainBalls.indexOf(insertedBall); if (idx === -1) return false; var color = insertedBall.color; // Expand left var left = idx; while (left > 0 && chainBalls[left - 1].color === color) { left--; } // Expand right var right = idx; while (right < chainBalls.length - 1 && chainBalls[right + 1].color === color) { right++; } var runLen = right - left + 1; // Only pop if the run at the inserted ball's position is 3 or more if (runLen >= 3) { // Pop balls left to right for (var k = left; k <= right; k++) { var b = chainBalls[k]; // Animate pop tween(b, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 220, easing: tween.easeOut, onFinish: function (ball) { return function () { ball.destroy(); }; }(b) }); } // Remove from chainBalls chainBalls.splice(left, runLen); // Play pop sound LK.getSound('pop').play(); // Update score score += runLen * 10; scoreTxt.setText(score); // Re-index and re-t for (var m = 0; m < chainBalls.length; m++) { chainBalls[m].chainIndex = m; } // After popping, check again for the new ball at left (if any) // But only if a new run is formed at the pop location // Recursively check for new matches at the pop location if (chainBalls[left]) { return checkAndPopMatches(chainBalls[left]); } return true; } // If the run is less than 3, do not pop anything return false; } // --- Game Update Loop --- game.update = function () { // --- Chain ball spawning one by one --- if (chainSpawnIndex < chainInitialCount) { // Spawn a new ball every 18 frames (~0.3s at 60fps) chainSpawnTimer++; if (chainSpawnTimer >= 18) { chainSpawnTimer = 0; var ball = new Ball(); var color = colors[Math.floor(Math.random() * colors.length)]; ball.setColor(color); ball.isChain = true; // All new balls start at t=0 (start of path) ball.t = 0; ball.chainIndex = chainSpawnIndex; var pos = getPathPos(0); ball.x = pos.x; ball.y = pos.y; game.addChild(ball); chainBalls.push(ball); chainSpawnIndex++; // After first ball, set headT and tailT to 0 chainHeadT = 0; chainTailT = 0; } } // --- Move chain balls forward --- if (chainBalls.length > 0 && chainSpawnIndex >= chainInitialCount) { // Move head forward chainHeadT += chainSpeed; // Clamp if (chainHeadT > 1) chainHeadT = 1; // --- Maintain constant spacing between balls along the path --- // Compute arc length between balls using the precomputed arcLen array // Find the arcLen for the head var HIGH_RES = 6000; var arcLen = [0]; for (var i = 1; i < PATH_POINTS.length; i++) { var dx = PATH_POINTS[i].x - PATH_POINTS[i - 1].x; var dy = PATH_POINTS[i].y - PATH_POINTS[i - 1].y; arcLen[i] = arcLen[i - 1] + Math.sqrt(dx * dx + dy * dy); } var totalLen = arcLen[arcLen.length - 1]; // Find the t for the head var headT = chainHeadT; var headIdx = Math.floor(headT * (PATH_LENGTH - 1)); var headArc = arcLen[headIdx]; // Now, for each ball, set its t so that the arc length between balls is always chainSpacing for (var i = 0; i < chainBalls.length; i++) { // The arc length for this ball should be headArc + i * chainSpacing var targetArc = headArc + i * chainSpacing; // Clamp to totalLen if (targetArc > totalLen) targetArc = totalLen; // Binary search for t such that arcLen[idx] >= targetArc var lo = 0, hi = arcLen.length - 1; while (lo < hi) { var mid = Math.floor((lo + hi) / 2); if (arcLen[mid] < targetArc) lo = mid + 1;else hi = mid; } var idx = lo; var t = idx / (PATH_LENGTH - 1); chainBalls[i].t = t; var pos = getPathPos(t); chainBalls[i].x = pos.x; chainBalls[i].y = pos.y; } chainTailT = chainBalls.length > 0 ? chainBalls[chainBalls.length - 1].t : 0; } // --- Move shot balls --- for (var i = shotBalls.length - 1; i >= 0; i--) { var ball = shotBalls[i]; if (!ball.active) continue; ball.x += ball.vx; ball.y += ball.vy; // Out of bounds if (ball.x < -100 || ball.x > 2148 || ball.y < -100 || ball.y > 2832) { ball.destroy(); shotBalls.splice(i, 1); continue; } // --- Collision with chain balls --- var minDist = 1e9; var hitIdx = -1; var hitT = 0; for (var j = 0; j < chainBalls.length; j++) { var cb = chainBalls[j]; var dx = ball.x - cb.x; var dy = ball.y - cb.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < ball.radius * 1.15) { if (dist < minDist) { minDist = dist; hitIdx = j; // Find t between cb and previous/next if (j === 0) { hitT = cb.t - chainSpacing / SPIRAL_RADIUS / (SPIRAL_TURNS * 2 * Math.PI); } else { hitT = (cb.t + chainBalls[j - 1].t) / 2; } } } } if (hitIdx !== -1) { // Insert ball into chain at hitIdx insertBallIntoChain(ball, hitIdx, hitT); // Animate insertion tween(ball, { scaleX: 1.2, scaleY: 1.2 }, { duration: 80, onFinish: function onFinish() { tween(ball, { scaleX: 1, scaleY: 1 }, { duration: 80 }); } }); // After insertion, check for matches ONLY for the inserted ball checkAndPopMatches(ball); continue; } } // --- Check for game over (tail reaches end) --- if (chainBalls.length > 0 && chainBalls[chainBalls.length - 1].t >= 1) { // Flash screen red LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); return; } // --- Check for win (all balls popped) --- if (chainBalls.length === 0) { LK.showYouWin(); return; } }; // --- Increase difficulty over time --- var difficultyTimer = LK.setInterval(function () { // Only increase speed, do not add new colors or levels chainSpeed += 0.00008; }, 9000);
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball class for both chain and shot balls
var Ball = Container.expand(function () {
var self = Container.call(this);
// color: 'red', 'blue', 'green', 'yellow'
self.color = 'red';
self.radius = 67.5; // 135/2, for collision (1.5x)
self.isChain = true; // true if part of chain, false if shot
// Attach correct asset (always ellipse)
var assetId = 'ball_' + self.color;
var ballAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
// For chain balls, we keep a t value (0-1) for position along the path
self.t = 0;
// For shot balls, we keep velocity
self.vx = 0;
self.vy = 0;
// For chain balls, we keep index in chain
self.chainIndex = 0;
// For shot balls, we keep a flag if it's active
self.active = true;
// Set color and update asset
self.setColor = function (color) {
self.color = color;
var assetId = 'ball_' + color; // always ellipse
// Remove old asset
if (ballAsset) ballAsset.destroy();
// Attach new asset (always ellipse)
var newAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
};
return self;
});
// Shooter class
var Shooter = Container.expand(function () {
var self = Container.call(this);
// Attach shooter asset
var shooterAsset = self.attachAsset('shooter', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
// The ball to be shot (preview)
self.previewBall = null;
// The angle the shooter is aiming (radians)
self.angle = -Math.PI / 2;
// Set preview ball color
self.setPreviewColor = function (color) {
if (self.previewBall) {
self.previewBall.destroy();
}
self.previewBall = new Ball();
self.previewBall.isChain = false;
self.previewBall.setColor(color); // Ball class always uses ellipse asset
// Place preview ball at the center of the shooter
self.previewBall.x = 0;
self.previewBall.y = 0;
self.addChild(self.previewBall);
};
// Set shooter angle (radians)
self.setAngle = function (angle) {
self.angle = angle;
self.rotation = angle + Math.PI / 2;
// Do not update previewBall position here to keep nextball fixed at shooter center
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// Play background music for the entire game session
LK.playMusic('music');
/*
We'll define a spiral path as a set of points (x, y) along which the chain balls move.
For MVP, use a simple Archimedean spiral centered in the screen.
We'll precompute N points along the spiral and interpolate between them.
*/
// --- Spiral Path Definition ---
// Ball shapes (4 colors for MVP)
// Shooter (frog or cannon, simple green ellipse for MVP)
// Ball shoot sound
// Ball pop sound
var PATH_POINTS = [];
var PATH_LENGTH = 1200; // Number of points along the path
// Use almost the full screen for the spiral and shooter
var SPIRAL_TURNS = 2.2; // How many turns
var SPIRAL_RADIUS = 1020; // Max radius (slightly smaller diameter)
var PATH_CENTER_X = 2048 / 2 + 24; // moved slightly right
var PATH_CENTER_Y = 2732 / 2 - 80;
// Add background image behind all elements, centered and scaled to cover the game area
var bgAsset = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
// Scale to cover the entire area, preserving aspect ratio
scaleX: Math.max(2048 / 2048, 2732 / 2148.72),
scaleY: Math.max(2048 / 2048, 2732 / 2148.72)
});
game.addChild(bgAsset);
// Spiral path visual: draw as small dots
var spiralPathDots = new Container();
game.addChild(spiralPathDots);
(function generateSpiralPath() {
// First, generate a high-res spiral
var HIGH_RES = 6000;
var spiralHighRes = [];
// Perfect circle spiral, but offset the starting point toward bottom-right, keeping center fixed
// We'll add an offset to the starting angle and radius to shift the spiral's start
var spiralStartAngleOffset = Math.PI / 6; // 30 degrees, adjust for more/less rightward
var spiralStartRadiusOffsetX = 180; // px, rightward
var spiralStartRadiusOffsetY = 220; // px, downward
for (var i = 0; i < HIGH_RES; i++) {
var t = i / (HIGH_RES - 1); // 0 to 1
var angle = SPIRAL_TURNS * 2 * Math.PI * (1 - t) + Math.PI + spiralStartAngleOffset; // Start angle offset
// Start radius at 220 to leave space for shooter, fill to edge
var radius = 220 + SPIRAL_RADIUS * (1 - t);
var x = PATH_CENTER_X + Math.cos(angle) * radius + spiralStartRadiusOffsetX * (1 - t);
var y = PATH_CENTER_Y + Math.sin(angle) * radius + spiralStartRadiusOffsetY * (1 - t);
// Mirror x around center to flip horizontally
x = PATH_CENTER_X - (x - PATH_CENTER_X);
spiralHighRes.push({
x: x,
y: y
});
}
// Compute cumulative arc length
var arcLen = [0];
for (var i = 1; i < spiralHighRes.length; i++) {
var dx = spiralHighRes[i].x - spiralHighRes[i - 1].x;
var dy = spiralHighRes[i].y - spiralHighRes[i - 1].y;
arcLen[i] = arcLen[i - 1] + Math.sqrt(dx * dx + dy * dy);
}
var totalLen = arcLen[arcLen.length - 1];
// Now, sample PATH_LENGTH points at equal arc length intervals
for (var i = 0; i < PATH_LENGTH; i++) {
var targetLen = i * totalLen / (PATH_LENGTH - 1);
// Binary search for the closest arcLen index
var lo = 0,
hi = arcLen.length - 1;
while (lo < hi) {
var mid = Math.floor((lo + hi) / 2);
if (arcLen[mid] < targetLen) lo = mid + 1;else hi = mid;
}
var idx = lo;
// Interpolate between idx-1 and idx
var p0 = spiralHighRes[Math.max(0, idx - 1)];
var p1 = spiralHighRes[idx];
var l0 = arcLen[Math.max(0, idx - 1)];
var l1 = arcLen[idx];
var frac = l1 - l0 > 0 ? (targetLen - l0) / (l1 - l0) : 0;
var x = p0.x + (p1.x - p0.x) * frac;
var y = p0.y + (p1.y - p0.y) * frac;
PATH_POINTS.push({
x: x,
y: y
});
// Draw a small dot every 8th point for performance
if (i % 8 === 0) {
var dot = LK.getAsset('dot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.7,
scaleY: 0.7,
x: x,
y: y,
alpha: 0.45
});
spiralPathDots.addChild(dot);
}
}
})();
// Helper: get position along path for t in [0,1]
function getPathPos(t) {
var idx = t * (PATH_LENGTH - 1);
var idx0 = Math.floor(idx);
var idx1 = Math.min(PATH_LENGTH - 1, idx0 + 1);
var frac = idx - idx0;
var p0 = PATH_POINTS[idx0];
var p1 = PATH_POINTS[idx1];
if (!p0 || !p1) {
// Defensive: If out of bounds, return center of screen
return {
x: PATH_CENTER_X,
y: PATH_CENTER_Y
};
}
return {
x: p0.x + (p1.x - p0.x) * frac,
y: p0.y + (p1.y - p0.y) * frac
};
}
// Helper: get tangent angle at t
function getPathAngle(t) {
var idx = t * (PATH_LENGTH - 1);
var idx0 = Math.max(0, Math.floor(idx) - 2);
var idx1 = Math.min(PATH_LENGTH - 1, idx0 + 4);
var p0 = PATH_POINTS[idx0];
var p1 = PATH_POINTS[idx1];
return Math.atan2(p1.y - p0.y, p1.x - p0.x);
}
// --- Chain State ---
var chainBalls = []; // Array of Ball (in order, head at index 0)
var chainSpacing = 135; // px between centers (balls touch: 135px ball diameter, 1.5x)
var chainSpeed = 0.00018; // t per tick (even slower movement for chain balls)
var chainHeadT = 0; // t of the head ball (0=start, 1=end)
var chainTailT = 0; // t of the tail ball
// --- Shooter State ---
var shooter = new Shooter();
// Place shooter at exact center of the screen
var shooterAssetSize = LK.getAsset('shooter', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
shooter.x = 2048 / 2;
shooter.y = 2732 / 2;
game.addChild(shooter);
// --- Shooter Guide Line ---
// We'll use a series of small dots (like spiralPathDots) to show the aim direction
var shooterGuideDots = new Container();
game.addChild(shooterGuideDots);
// Helper to update shooter guide line
function updateShooterGuide() {
// Remove old dots
while (shooterGuideDots.children.length > 0) {
shooterGuideDots.children[0].destroy();
}
// Always show shooter guide line (removed isAiming check)
// Draw dots every 50px, up to the screen edge
var angle = shooter.angle;
var sx = shooter.x + Math.cos(angle) * 60;
var sy = shooter.y + Math.sin(angle) * 60;
// Calculate how far we can go in this direction before hitting the screen edge
function getEdgeDistance(x, y, angle) {
// Calculate intersection with each edge, return the smallest positive distance
var dx = Math.cos(angle);
var dy = Math.sin(angle);
var distances = [];
// Left edge (x=0)
if (dx < 0) {
var t = (0 - x) / dx;
if (t > 0) distances.push(t);
}
// Right edge (x=2048)
if (dx > 0) {
var t = (2048 - x) / dx;
if (t > 0) distances.push(t);
}
// Top edge (y=0)
if (dy < 0) {
var t = (0 - y) / dy;
if (t > 0) distances.push(t);
}
// Bottom edge (y=2732)
if (dy > 0) {
var t = (2732 - y) / dy;
if (t > 0) distances.push(t);
}
if (distances.length === 0) return 0;
return Math.min.apply(null, distances);
}
var maxDist = getEdgeDistance(shooter.x, shooter.y, angle) - 60; // minus shooter offset
var lenStep = 50;
var steps = Math.floor(maxDist / lenStep);
for (var i = 1; i <= steps; i++) {
var px = shooter.x + Math.cos(angle) * (60 + i * lenStep);
var py = shooter.y + Math.sin(angle) * (60 + i * lenStep);
// Don't draw outside game area (defensive)
if (px < 0 || px > 2048 || py < 0 || py > 2732) break;
var dot = LK.getAsset('dot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5,
x: px,
y: py,
alpha: 0.7
});
shooterGuideDots.addChild(dot);
}
}
// --- Shot Balls State ---
var shotBalls = []; // Array of Ball
// --- Game State ---
var colors = ['red', 'blue', 'green', 'yellow'];
var nextBallColor = colors[Math.floor(Math.random() * colors.length)];
var score = 0;
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Initialize Chain ---
// --- Chain spawn state ---
var chainSpawnIndex = 0;
var chainSpawnTimer = 0;
var chainInitialCount = 10; // Number of balls to spawn at start
function initChain() {
chainBalls = [];
chainSpawnIndex = 0;
chainSpawnTimer = 0;
chainHeadT = 0;
chainTailT = 0;
}
initChain();
// --- Initialize Shooter ---
shooter.setPreviewColor(nextBallColor);
shooter.setAngle(-Math.PI / 2);
updateShooterGuide();
// --- Dragging / Aiming State ---
var isAiming = false;
// --- Helper: Find angle from shooter to (x, y) ---
function getShooterAngleTo(x, y) {
var dx = x - shooter.x;
var dy = y - shooter.y;
return Math.atan2(dy, dx);
}
// --- Helper: Find nearest t on path to (x, y) ---
function findNearestT(x, y) {
// Brute force for MVP
var minDist = 1e9;
var minIdx = 0;
for (var i = 0; i < PATH_POINTS.length; i += 8) {
// Step for speed
var p = PATH_POINTS[i];
var dx = p.x - x;
var dy = p.y - y;
var d = dx * dx + dy * dy;
if (d < minDist) {
minDist = d;
minIdx = i;
}
}
return minIdx / (PATH_LENGTH - 1);
}
// --- Game Events ---
// Aim shooter
game.move = function (x, y, obj) {
if (!isAiming) return;
var angle = getShooterAngleTo(x, y);
shooter.setAngle(angle);
updateShooterGuide();
};
// Start aiming
game.down = function (x, y, obj) {
// Only allow aiming if touch is not on top 400px (avoid accidental shots)
if (y < 400) return;
isAiming = true;
var angle = getShooterAngleTo(x, y);
shooter.setAngle(angle);
updateShooterGuide();
};
// Shoot ball
game.up = function (x, y, obj) {
if (!isAiming) return;
isAiming = false;
updateShooterGuide();
// Fire a ball in the current shooter.angle
var ball = new Ball();
ball.isChain = false;
ball.setColor(nextBallColor);
var angle = shooter.angle;
var speed = 120; // px per tick (much faster for shooter)
ball.vx = Math.cos(angle) * speed;
ball.vy = Math.sin(angle) * speed;
ball.x = shooter.x + Math.cos(angle) * 60;
ball.y = shooter.y + Math.sin(angle) * 60;
ball.active = true;
shotBalls.push(ball);
game.addChild(ball);
// Play shoot sound
LK.getSound('shoot').play();
// Next ball color
nextBallColor = colors[Math.floor(Math.random() * colors.length)];
shooter.setPreviewColor(nextBallColor);
};
// --- Helper: Insert ball into chain at index ---
function insertBallIntoChain(ball, insertIdx, insertT) {
// Insert ball into chainBalls at insertIdx, set t=insertT
ball.isChain = true;
ball.t = insertT;
ball.chainIndex = insertIdx;
// Remove from shotBalls
for (var i = 0; i < shotBalls.length; i++) {
if (shotBalls[i] === ball) {
shotBalls.splice(i, 1);
break;
}
}
// Insert into chainBalls
chainBalls.splice(insertIdx, 0, ball);
// Re-index and re-t
for (var i = 0; i < chainBalls.length; i++) {
chainBalls[i].chainIndex = i;
}
// Adjust t for all balls after insertIdx
for (var i = insertIdx + 1; i < chainBalls.length; i++) {
chainBalls[i].t += chainSpacing / SPIRAL_RADIUS / (SPIRAL_TURNS * 2 * Math.PI);
}
}
// --- Helper: Check for matches and pop balls ---
// Only pop the run that contains the most recently inserted ball (if any)
function checkAndPopMatches(insertedBall) {
// If no insertedBall is provided, fallback to old behavior (should not happen in normal flow)
if (!insertedBall) {
// fallback: do nothing
return false;
}
// Find the run containing insertedBall
var idx = chainBalls.indexOf(insertedBall);
if (idx === -1) return false;
var color = insertedBall.color;
// Expand left
var left = idx;
while (left > 0 && chainBalls[left - 1].color === color) {
left--;
}
// Expand right
var right = idx;
while (right < chainBalls.length - 1 && chainBalls[right + 1].color === color) {
right++;
}
var runLen = right - left + 1;
// Only pop if the run at the inserted ball's position is 3 or more
if (runLen >= 3) {
// Pop balls left to right
for (var k = left; k <= right; k++) {
var b = chainBalls[k];
// Animate pop
tween(b, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 220,
easing: tween.easeOut,
onFinish: function (ball) {
return function () {
ball.destroy();
};
}(b)
});
}
// Remove from chainBalls
chainBalls.splice(left, runLen);
// Play pop sound
LK.getSound('pop').play();
// Update score
score += runLen * 10;
scoreTxt.setText(score);
// Re-index and re-t
for (var m = 0; m < chainBalls.length; m++) {
chainBalls[m].chainIndex = m;
}
// After popping, check again for the new ball at left (if any)
// But only if a new run is formed at the pop location
// Recursively check for new matches at the pop location
if (chainBalls[left]) {
return checkAndPopMatches(chainBalls[left]);
}
return true;
}
// If the run is less than 3, do not pop anything
return false;
}
// --- Game Update Loop ---
game.update = function () {
// --- Chain ball spawning one by one ---
if (chainSpawnIndex < chainInitialCount) {
// Spawn a new ball every 18 frames (~0.3s at 60fps)
chainSpawnTimer++;
if (chainSpawnTimer >= 18) {
chainSpawnTimer = 0;
var ball = new Ball();
var color = colors[Math.floor(Math.random() * colors.length)];
ball.setColor(color);
ball.isChain = true;
// All new balls start at t=0 (start of path)
ball.t = 0;
ball.chainIndex = chainSpawnIndex;
var pos = getPathPos(0);
ball.x = pos.x;
ball.y = pos.y;
game.addChild(ball);
chainBalls.push(ball);
chainSpawnIndex++;
// After first ball, set headT and tailT to 0
chainHeadT = 0;
chainTailT = 0;
}
}
// --- Move chain balls forward ---
if (chainBalls.length > 0 && chainSpawnIndex >= chainInitialCount) {
// Move head forward
chainHeadT += chainSpeed;
// Clamp
if (chainHeadT > 1) chainHeadT = 1;
// --- Maintain constant spacing between balls along the path ---
// Compute arc length between balls using the precomputed arcLen array
// Find the arcLen for the head
var HIGH_RES = 6000;
var arcLen = [0];
for (var i = 1; i < PATH_POINTS.length; i++) {
var dx = PATH_POINTS[i].x - PATH_POINTS[i - 1].x;
var dy = PATH_POINTS[i].y - PATH_POINTS[i - 1].y;
arcLen[i] = arcLen[i - 1] + Math.sqrt(dx * dx + dy * dy);
}
var totalLen = arcLen[arcLen.length - 1];
// Find the t for the head
var headT = chainHeadT;
var headIdx = Math.floor(headT * (PATH_LENGTH - 1));
var headArc = arcLen[headIdx];
// Now, for each ball, set its t so that the arc length between balls is always chainSpacing
for (var i = 0; i < chainBalls.length; i++) {
// The arc length for this ball should be headArc + i * chainSpacing
var targetArc = headArc + i * chainSpacing;
// Clamp to totalLen
if (targetArc > totalLen) targetArc = totalLen;
// Binary search for t such that arcLen[idx] >= targetArc
var lo = 0,
hi = arcLen.length - 1;
while (lo < hi) {
var mid = Math.floor((lo + hi) / 2);
if (arcLen[mid] < targetArc) lo = mid + 1;else hi = mid;
}
var idx = lo;
var t = idx / (PATH_LENGTH - 1);
chainBalls[i].t = t;
var pos = getPathPos(t);
chainBalls[i].x = pos.x;
chainBalls[i].y = pos.y;
}
chainTailT = chainBalls.length > 0 ? chainBalls[chainBalls.length - 1].t : 0;
}
// --- Move shot balls ---
for (var i = shotBalls.length - 1; i >= 0; i--) {
var ball = shotBalls[i];
if (!ball.active) continue;
ball.x += ball.vx;
ball.y += ball.vy;
// Out of bounds
if (ball.x < -100 || ball.x > 2148 || ball.y < -100 || ball.y > 2832) {
ball.destroy();
shotBalls.splice(i, 1);
continue;
}
// --- Collision with chain balls ---
var minDist = 1e9;
var hitIdx = -1;
var hitT = 0;
for (var j = 0; j < chainBalls.length; j++) {
var cb = chainBalls[j];
var dx = ball.x - cb.x;
var dy = ball.y - cb.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < ball.radius * 1.15) {
if (dist < minDist) {
minDist = dist;
hitIdx = j;
// Find t between cb and previous/next
if (j === 0) {
hitT = cb.t - chainSpacing / SPIRAL_RADIUS / (SPIRAL_TURNS * 2 * Math.PI);
} else {
hitT = (cb.t + chainBalls[j - 1].t) / 2;
}
}
}
}
if (hitIdx !== -1) {
// Insert ball into chain at hitIdx
insertBallIntoChain(ball, hitIdx, hitT);
// Animate insertion
tween(ball, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 80,
onFinish: function onFinish() {
tween(ball, {
scaleX: 1,
scaleY: 1
}, {
duration: 80
});
}
});
// After insertion, check for matches ONLY for the inserted ball
checkAndPopMatches(ball);
continue;
}
}
// --- Check for game over (tail reaches end) ---
if (chainBalls.length > 0 && chainBalls[chainBalls.length - 1].t >= 1) {
// Flash screen red
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
return;
}
// --- Check for win (all balls popped) ---
if (chainBalls.length === 0) {
LK.showYouWin();
return;
}
};
// --- Increase difficulty over time ---
var difficultyTimer = LK.setInterval(function () {
// Only increase speed, do not add new colors or levels
chainSpeed += 0.00008;
}, 9000);
Mistik mısır ve aztek resmi çöl gibi bir spiral ve sarı renk agırlıklı low poly nesneler ve su akıyor efekti. In-Game asset. 2d. High contrast. No shadows
thick and yellow color object edges
make the ball appear on the screen much more smoothly like bubble
make the ball appear on the screen much more smoothly like bubble
make the ball appear on the screen much more smoothly like bubble
yellow