/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // AISnake (AI snake that moves toward food and steals it) var AISnake = Container.expand(function () { var self = Container.call(this); self.length = 3 + Math.floor(Math.random() * 2); // 3-4 segments self.segments = []; self.dir = 0; self.speed = Math.max(4, SNAKE_INIT_SPEED * 0.35); // AI snake moves much slower than player self.target = null; self.stealing = false; self.lastFood = null; // Create head var head = self.attachAsset('aiSnakeHead', { anchorX: 0.5, anchorY: 0.5 }); self.asset = head; self.segments.push(self); // Create body segments for (var i = 1; i < self.length; ++i) { var seg = new Container(); seg.asset = seg.attachAsset('aiSnakeBody', { anchorX: 0.5, anchorY: 0.5 }); seg.x = self.x - i * 60; seg.y = self.y; self.addChild(seg); self.segments.push(seg); } // Custom hitbox self.hitboxScale = 0.6; // Custom intersects for head self.intersects = function (other) { var scaleA = typeof self.hitboxScale === "number" ? self.hitboxScale : 1; var scaleB = typeof other.hitboxScale === "number" ? other.hitboxScale : 1; var ax = self.x, ay = self.y, aw = self.asset.width * scaleA, ah = self.asset.height * scaleA; var bx = other.x, by = other.y, bw = other.asset && other.asset.width ? other.asset.width * scaleB : 0, bh = other.asset && other.asset.height ? other.asset.height * scaleB : 0; var rA = Math.max(aw, ah) / 2; var rB = Math.max(bw, bh) / 2; var dx = ax - bx, dy = ay - by; return dx * dx + dy * dy < (rA + rB) * (rA + rB); }; // Update method self.update = function () { // Save last position for event triggers if (self.lastX === undefined) self.lastX = self.x; if (self.lastY === undefined) self.lastY = self.y; // Find food if not already targeting if (!self.target || self.target.destroyed) { if (typeof food !== "undefined" && food && !food.destroyed) { self.target = food; } else { self.target = null; } } // Move toward food using player-like movement if (self.target) { // Calculate desired direction var dx = self.target.x - self.x; var dy = self.target.y - self.y; var desiredDir = Math.atan2(dy, dx); // Smoothly turn toward the target (like player) var d = desiredDir - self.dir; while (d > Math.PI) d -= 2 * Math.PI; while (d < -Math.PI) d += 2 * Math.PI; var turn = Math.sign(d) * Math.min(Math.abs(d), SNAKE_TURN_ANGLE); self.dir += turn; // Move head forward self.x += Math.cos(self.dir) * self.speed; self.y += Math.sin(self.dir) * self.speed; // Clamp to bounds self.x = Math.max(100 + self.asset.width / 2, Math.min(2048 - 100 - self.asset.width / 2, self.x)); self.y = Math.max(100 + self.asset.height / 2, Math.min(2732 - 100 - self.asset.height / 2, self.y)); // Move body segments to follow the previous segment, like player for (var i = 1; i < self.segments.length; ++i) { var seg = self.segments[i]; var target = self.segments[i - 1]; var sdx = target.x - seg.x; var sdy = target.y - seg.y; var sdist = Math.sqrt(sdx * sdx + sdy * sdy); var desired = self.speed * 0.9; if (sdist > desired) { var move = (sdist - desired) * 0.5; seg.x += sdx / sdist * move; seg.y += sdy / sdist * move; } } // Steal food if close enough if (self.target && self.intersects(self.target)) { self.stealing = true; // Remove food from game if (typeof food !== "undefined" && food === self.target) { food.destroy(); food = null; // Always respawn food after AI steals it if (typeof spawnFood === "function") { spawnFood(); } } self.target = null; } } // Despawn if off screen or after stealing if (self.stealing) { // Move off screen quickly self.x += Math.cos(self.dir) * self.speed * 2; self.y += Math.sin(self.dir) * self.speed * 2; if (self.x < -200 || self.x > 2048 + 200 || self.y < -200 || self.y > 2732 + 200) { self._despawn = true; } } self.lastX = self.x; self.lastY = self.y; }; return self; }); // Food var Food = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('food', { anchorX: 0.5, anchorY: 0.5 }); self.hitboxScale = 0.6; return self; }); // FoodPurple var FoodPurple = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('foodPurple', { anchorX: 0.5, anchorY: 0.5 }); self.hitboxScale = 0.6; return self; }); // FoodRainbow (rare, big bonus) var FoodRainbow = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('food', { anchorX: 0.5, anchorY: 0.5 }); self.hitboxScale = 0.6; // Animate color cycling // Animate color cycling using tween plugin API tween(self.asset, { tint: 0xff0000 }, { duration: 400, easing: tween.linear, onFinish: function onFinish() { // No-op, will be repeated } }); // Manually cycle through rainbow colors in update (since tween can't do this directly) self.update = function () { var t = LK.ticks % 60 / 60; var r = Math.floor(127 * (Math.sin(2 * Math.PI * t) + 1)); var g = Math.floor(127 * (Math.sin(2 * Math.PI * t + 2) + 1)); var b = Math.floor(127 * (Math.sin(2 * Math.PI * t + 4) + 1)); self.asset.tint = r << 16 | g << 8 | b; }; return self; }); // MovingObstacle var MovingObstacle = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('obstacleMoving', { anchorX: 0.5, anchorY: 0.5 }); self.hitboxScale = 0.7; // Pick a random direction and speed var angle = Math.random() * Math.PI * 2; var speed = 4 + Math.random() * 4; self.vx = Math.cos(angle) * speed; self.vy = Math.sin(angle) * speed; self.update = function () { // Save last position for event triggers if (self.lastX === undefined) self.lastX = self.x; if (self.lastY === undefined) self.lastY = self.y; self.x += self.vx; self.y += self.vy; // Despawn if out of bounds (with 150px margin) if (self.x < -150 || self.x > 2048 + 150 || self.y < -150 || self.y > 2732 + 150) { // Mark for removal by setting a flag self._despawn = true; return; } // Bounce off walls (100px margin) if (self.x < 100 + self.asset.width / 2 && self.vx < 0) self.vx *= -1; if (self.x > 2048 - 100 - self.asset.width / 2 && self.vx > 0) self.vx *= -1; if (self.y < 100 + self.asset.height / 2 && self.vy < 0) self.vy *= -1; if (self.y > 2732 - 100 - self.asset.height / 2 && self.vy > 0) self.vy *= -1; self.lastX = self.x; self.lastY = self.y; }; return self; }); // Obstacle (static) var Obstacle = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('obstacle', { anchorX: 0.5, anchorY: 0.5 }); self.hitboxScale = 0.7; // Add despawn timer and update method for static obstacles self._despawnTimer = 0; self._despawnInterval = 60 * 60; // Try to despawn every 60 seconds (3600 ticks) self.update = function () { if (self._despawnTimer === undefined) self._despawnTimer = 0; self._despawnTimer++; // Only check every _despawnInterval ticks if (self._despawnTimer >= self._despawnInterval) { self._despawnTimer = 0; // 40% chance to despawn (less often than moving obstacles) if (Math.random() < 0.4) { self._despawn = true; } } }; return self; }); // PowerUp var PowerUp = Container.expand(function () { var self = Container.call(this); self.type = 'invincible'; // or 'shrink' self.asset = null; self.hitboxScale = 0.6; self.setType = function (type) { if (self.asset) self.removeChild(self.asset); self.type = type; if (type === 'invincible') { self.asset = self.attachAsset('powerupInvincible', { anchorX: 0.5, anchorY: 0.5 }); } else if (type === 'shrink') { self.asset = self.attachAsset('powerupShrink', { anchorX: 0.5, anchorY: 0.5 }); } }; self.setType('invincible'); return self; }); // Snake Segment (body or head) var SnakeSegment = Container.expand(function () { var self = Container.call(this); self.isHead = false; self.asset = null; // Custom hitbox size (smaller than visual asset) self.hitboxScale = 0.6; // 60% of asset size for both head and body self.setType = function (type) { if (self.asset) self.removeChild(self.asset); if (type === 'head') { self.asset = self.attachAsset('snakeHead', { anchorX: 0.5, anchorY: 0.5 }); self.isHead = true; } else { self.asset = self.attachAsset('snakeBody', { anchorX: 0.5, anchorY: 0.5 }); self.isHead = false; } }; self.setType('body'); // Custom intersects method for smaller hitbox self.intersects = function (other) { // Use hitboxScale for both this and other if available, else default to 1 var scaleA = typeof self.hitboxScale === "number" ? self.hitboxScale : 1; var scaleB = typeof other.hitboxScale === "number" ? other.hitboxScale : 1; var ax = self.x, ay = self.y, aw = self.asset.width * scaleA, ah = self.asset.height * scaleA; var bx = other.x, by = other.y, bw = other.asset && other.asset.width ? other.asset.width * scaleB : 0, bh = other.asset && other.asset.height ? other.asset.height * scaleB : 0; // Circle collision (since snake is ellipse/circle) var rA = Math.max(aw, ah) / 2; var rB = Math.max(bw, bh) / 2; var dx = ax - bx; var dy = ay - by; return dx * dx + dy * dy < (rA + rB) * (rA + rB); }; return self; }); // SpikeObstacle (destroys snake segments it touches, rare spawn) var SpikeObstacle = Container.expand(function () { var self = Container.call(this); // Add spike obstacle asset first (so aura is in front) self.asset = self.attachAsset('spikeObstacle', { anchorX: 0.5, anchorY: 0.5 }); // Add spike aura in front of the spike asset self.aura = self.attachAsset('spikeAura', { anchorX: 0.5, anchorY: 0.5 }); self.aura.alpha = 0.35; self.hitboxScale = 0.7; // Add despawn timer and update method for static obstacles self._despawnTimer = 0; self._despawnInterval = 60 * 45; // Try to despawn every 45 seconds (less often than normal obstacles) self.update = function () { if (self._despawnTimer === undefined) self._despawnTimer = 0; self._despawnTimer++; // Only check every _despawnInterval ticks if (self._despawnTimer >= self._despawnInterval) { self._despawnTimer = 0; // 60% chance to despawn (spikes are rare, so despawn more often) if (Math.random() < 0.6) { self._despawn = true; } } // Animate aura pulse if (self.aura) { var t = LK.ticks % 60 / 60; self.aura.alpha = 0.25 + 0.15 * Math.sin(2 * Math.PI * t); var scale = 1.05 + 0.08 * Math.sin(2 * Math.PI * t); self.aura.scaleX = scale; self.aura.scaleY = scale; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181818 }); /**** * Game Code ****/ // Music // Sounds // Obstacles // Power-ups // Food // Snake head and body // --- Game constants --- var SNAKE_INIT_LENGTH = 5; var SNAKE_INIT_SPEED = 16; // pixels per tick var SNAKE_MIN_SPEED = 8; var SNAKE_MAX_SPEED = 40; var SNAKE_TURN_ANGLE = Math.PI / 18; // 10 degrees per input var FOOD_SCORE = 10; var POWERUP_SCORE = 25; var POWERUP_DURATION = 300; // ticks (5 seconds) var OBSTACLE_COUNT = 3; var OBSTACLE_SPEED = 6; // --- Game state --- var snake = []; var aiSnakes = []; // Array of AI snakes var snakeDir = 0; // in radians, 0 = right var snakeNextDir = 0; var snakeSpeed = SNAKE_INIT_SPEED; var snakeGrow = 0; var food = null; var powerup = null; var powerupActive = false; var powerupType = null; var powerupTimer = 0; var obstacles = []; var score = 0; var isInvincible = false; var lastTouch = null; var dragStart = null; var dragAngle = null; var dragActive = false; var gameOver = false; // --- GUI --- var scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // --- Helper functions --- function randomPos(margin, avoidList, avoidRadius) { margin = margin || 150; avoidList = avoidList || []; avoidRadius = avoidRadius || 120; // Default: avoid within 120px of any avoidList item var tries = 0; while (tries < 50) { var x = margin + Math.random() * (2048 - 2 * margin); var y = margin + Math.random() * (2732 - 2 * margin); var ok = true; for (var i = 0; i < avoidList.length; ++i) { var obj = avoidList[i]; var dx = obj.x - x; var dy = obj.y - y; if (dx * dx + dy * dy < avoidRadius * avoidRadius) { ok = false; break; } } if (ok) { return { x: x, y: y }; } tries++; } // Fallback: just return a random position if we can't find a good one return { x: margin + Math.random() * (2048 - 2 * margin), y: margin + Math.random() * (2732 - 2 * margin) }; } function dist2(a, b) { var dx = a.x - b.x, dy = a.y - b.y; return dx * dx + dy * dy; } function angleBetween(a, b) { return Math.atan2(b.y - a.y, b.x - a.x); } function wrapPos(pos) { // Not used, but could be for wrap-around if (pos.x < 0) pos.x += 2048; if (pos.x > 2048) pos.x -= 2048; if (pos.y < 0) pos.y += 2732; if (pos.y > 2732) pos.y -= 2732; } function spawnFood() { if (food) food.destroy(); // 5% chance for rainbow food, 20% for purple, else normal var r = Math.random(); if (r < 0.05) { food = new FoodRainbow(); food.isRainbow = true; food.isPurple = false; } else if (r < 0.25) { food = new FoodPurple(); food.isPurple = true; food.isRainbow = false; } else { food = new Food(); food.isPurple = false; food.isRainbow = false; } // Avoid spawning on the snake and obstacles var avoidList = []; for (var i = 0; i < snake.length; ++i) { avoidList.push(snake[i]); } // Also avoid obstacles for (var i = 0; i < obstacles.length; ++i) { avoidList.push(obstacles[i]); } var pos = randomPos(200, avoidList, 120); food.x = pos.x; food.y = pos.y; game.addChild(food); } function spawnPowerUp() { if (powerup) powerup.destroy(); powerup = new PowerUp(); // Avoid spawning on the snake var avoidList = []; for (var i = 0; i < snake.length; ++i) { avoidList.push(snake[i]); } var pos = randomPos(200, avoidList, 120); // 67% invincible, 33% shrink var r = Math.random(); var t; if (r < 0.67) t = 'invincible';else t = 'shrink'; powerup.setType(t); powerup.x = pos.x; powerup.y = pos.y; game.addChild(powerup); } function spawnObstacles() { for (var i = 0; i < obstacles.length; ++i) { obstacles[i].destroy(); } obstacles = []; // Avoid spawning on the snake var avoidList = []; for (var j = 0; j < snake.length; ++j) { avoidList.push(snake[j]); } for (var i = 0; i < OBSTACLE_COUNT; ++i) { var obs = new Obstacle(); var pos = randomPos(300, avoidList, 140); obs.x = pos.x; obs.y = pos.y; // Add this obstacle to avoidList so next ones don't overlap avoidList.push(obs); obstacles.push(obs); game.addChild(obs); } } function resetSnake() { for (var i = 0; i < snake.length; ++i) { snake[i].destroy(); } snake = []; var startX = 2048 / 2, startY = 2732 / 2; for (var i = 0; i < SNAKE_INIT_LENGTH; ++i) { var seg = new SnakeSegment(); if (i === 0) { seg.setType('head'); // Head spawns in front of the body, not on top of the first body segment seg.x = startX; seg.y = startY; } else { seg.setType('body'); // Each body segment spawns behind the previous one, extending left from the head seg.x = startX - i * snakeSpeed * 1.2; seg.y = startY; } game.addChild(seg); snake.push(seg); } snakeDir = 0; snakeNextDir = 0; snakeSpeed = SNAKE_INIT_SPEED; snakeGrow = 0; isInvincible = false; powerupActive = false; powerupType = null; powerupTimer = 0; } function updateScore(val) { score = val; scoreTxt.setText(score); LK.setScore(score); } function activatePowerUp(type) { powerupActive = true; powerupType = type; powerupTimer = POWERUP_DURATION; if (type === 'invincible') { isInvincible = true; // Flash snake for (var i = 0; i < snake.length; ++i) { LK.effects.flashObject(snake[i], 0xf1c40f, 500); } } else if (type === 'shrink') { // Remove 3 segments from the tail, but always keep at least 5 var minLen = 5; var removeCount = Math.min(3, snake.length - minLen); for (var i = 0; i < removeCount; ++i) { var seg = snake.pop(); seg.destroy(); } } } function deactivatePowerUp() { powerupActive = false; powerupType = null; powerupTimer = 0; isInvincible = false; } function endGame() { if (gameOver) return; gameOver = true; LK.effects.flashScreen(0xff0000, 1000); LK.getSound('hit').play(); LK.showGameOver(); } // --- Input handling (touch/drag to steer) --- game.down = function (x, y, obj) { // Only allow drag from outside top left 100x100 if (x < 100 && y < 100) return; dragStart = { x: x, y: y }; dragAngle = null; dragActive = true; }; game.move = function (x, y, obj) { if (!dragActive) return; if (!dragStart) return; // Calculate angle from snake head to drag point var head = snake[0]; var dx = x - head.x; var dy = y - head.y; if (dx * dx + dy * dy < 100) return; // Ignore tiny drags var angle = Math.atan2(dy, dx); snakeNextDir = angle; dragAngle = angle; }; game.up = function (x, y, obj) { dragActive = false; dragStart = null; dragAngle = null; }; // --- Main game update loop --- game.update = function () { if (gameOver) return; // Power-up timer if (powerupActive) { powerupTimer--; if (powerupTimer <= 0) { deactivatePowerUp(); } } // --- AI Snake logic --- // Update AI snakes for (var i = aiSnakes.length - 1; i >= 0; --i) { var ai = aiSnakes[i]; if (ai.update) ai.update(); // Check if AI snake collides with any player tail segment (not head) // Only if not already despawning if (!ai._despawn && snake.length > 4) { // Start from segment 4 (skip head and first 3 for leniency) for (var s = 4; s < snake.length; ++s) { var seg = snake[s]; // Only check if segment exists and not destroyed if (seg && !seg.destroyed) { // Track last intersection state for this ai/segment pair if (!ai.lastTailIntersect) ai.lastTailIntersect = {}; if (ai.lastTailIntersect[s] === undefined) ai.lastTailIntersect[s] = false; var nowIntersect = ai.intersects(seg); if (!ai.lastTailIntersect[s] && nowIntersect) { // AI snake dies! LK.effects.flashObject(ai, 0xff0000, 600); ai._despawn = true; // Optionally, give player a score bonus for killing AI updateScore(score + 30); break; } ai.lastTailIntersect[s] = nowIntersect; } } } // Remove if marked for despawn if (ai._despawn) { ai.destroy(); aiSnakes.splice(i, 1); } } // Spawn a new AI snake less frequently (about every 14-18 seconds, only one at a time) if (aiSnakes.length === 0 && LK.ticks > 60 * 5 && LK.ticks % (60 * (14 + Math.floor(Math.random() * 5))) === 0 && food && !food.isRainbow) { // Only spawn if food exists and is not rainbow (AI doesn't steal rainbow food) var avoidList = []; for (var j = 0; j < snake.length; ++j) avoidList.push(snake[j]); for (var k = 0; k < obstacles.length; ++k) avoidList.push(obstacles[k]); // Spawn at random edge var edge = Math.floor(Math.random() * 4); var pos; if (edge === 0) { // left pos = { x: 120, y: 200 + Math.random() * (2732 - 400) }; } else if (edge === 1) { // right pos = { x: 2048 - 120, y: 200 + Math.random() * (2732 - 400) }; } else if (edge === 2) { // top pos = { x: 200 + Math.random() * (2048 - 400), y: 120 }; } else { // bottom pos = { x: 200 + Math.random() * (2048 - 400), y: 2732 - 120 }; } // Avoid spawning on snake or obstacles var ok = true; for (var i = 0; i < avoidList.length; ++i) { var obj = avoidList[i]; var dx = obj.x - pos.x; var dy = obj.y - pos.y; if (dx * dx + dy * dy < 200 * 200) { ok = false; break; } } if (ok) { var ai = new AISnake(); ai.x = pos.x; ai.y = pos.y; aiSnakes.push(ai); game.addChild(ai); } } // Update obstacles (moving, static, and spike) for (var i = obstacles.length - 1; i >= 0; --i) { if (obstacles[i].update) obstacles[i].update(); // Remove moving obstacles that have been marked for despawn if (obstacles[i] instanceof MovingObstacle && obstacles[i]._despawn) { obstacles[i].destroy(); obstacles.splice(i, 1); continue; } // Remove static obstacles that have been marked for despawn if (obstacles[i] instanceof Obstacle && obstacles[i]._despawn) { obstacles[i].destroy(); obstacles.splice(i, 1); continue; } // Remove spike obstacles that have been marked for despawn if (typeof SpikeObstacle !== "undefined" && obstacles[i] instanceof SpikeObstacle && obstacles[i]._despawn) { obstacles[i].destroy(); obstacles.splice(i, 1); continue; } } // Obstacles are static, no update needed // Snake direction: smooth turn toward nextDir var d = snakeNextDir - snakeDir; while (d > Math.PI) d -= 2 * Math.PI; while (d < -Math.PI) d += 2 * Math.PI; if (Math.abs(d) > 0.01) { var turn = Math.sign(d) * Math.min(Math.abs(d), SNAKE_TURN_ANGLE); snakeDir += turn; } // Move snake head var head = snake[0]; var prevPos = { x: head.x, y: head.y }; head.x += Math.cos(snakeDir) * snakeSpeed; head.y += Math.sin(snakeDir) * snakeSpeed; // Clamp to bounds (leave 100px margin for UI) head.x = Math.max(100 + head.asset.width / 2, Math.min(2048 - 100 - head.asset.width / 2, head.x)); head.y = Math.max(100 + head.asset.height / 2, Math.min(2732 - 100 - head.asset.height / 2, head.y)); // Move body segments to follow for (var i = 1; i < snake.length; ++i) { var seg = snake[i]; var target = snake[i - 1]; var dx = target.x - seg.x; var dy = target.y - seg.y; var dist = Math.sqrt(dx * dx + dy * dy); var desired = snakeSpeed * 0.9; if (dist > desired) { var move = (dist - desired) * 0.5; seg.x += dx / dist * move; seg.y += dy / dist * move; } } // Grow snake if needed if (snakeGrow > 0) { var tail = snake[snake.length - 1]; var newSeg = new SnakeSegment(); newSeg.setType('body'); newSeg.x = tail.x; newSeg.y = tail.y; game.addChild(newSeg); snake.push(newSeg); snakeGrow--; } // Check collision with food if (food && head.intersects(food)) { LK.getSound('eat').play(); if (food.isRainbow) { updateScore(score + FOOD_SCORE * 10); snakeGrow += 12; // Flash the whole snake rainbow for (var i = 0; i < snake.length; ++i) { LK.effects.flashObject(snake[i], 0xffffff, 800); } } else if (food.isPurple) { updateScore(score + FOOD_SCORE * 3); snakeGrow += 6; } else { updateScore(score + FOOD_SCORE); snakeGrow += 2; } food.destroy(); food = null; // If an AI snake stole the food, don't respawn (AI snake update will handle it) // Only respawn food if it was NOT stolen by an AI snake this frame if (!aiSnakes.some(function (ai) { return ai.stealing; })) { spawnFood(); } // Chance to spawn powerup if (!powerup && Math.random() < 0.2) { spawnPowerUp(); } } // Check collision with powerup if (powerup && head.intersects(powerup)) { LK.getSound('powerup').play(); updateScore(score + POWERUP_SCORE); activatePowerUp(powerup.type); // Flash the screen for feedback LK.effects.flashScreen(0xf1c40f, 400); powerup.destroy(); powerup = null; } // Check collision with obstacles for (var i = 0; i < obstacles.length; ++i) { // SpikeObstacle: destroys segments it touches, reduces score if (typeof SpikeObstacle !== "undefined" && obstacles[i] instanceof SpikeObstacle) { // Check all segments (not just head) for (var s = 0; s < snake.length; ++s) { var seg = snake[s]; if (seg.intersects(obstacles[i])) { // Only destroy if not invincible if (!isInvincible) { // Flash segment and obstacle LK.effects.flashObject(seg, 0xda2cff, 400); LK.effects.flashObject(obstacles[i], 0xda2cff, 400); // Remove segment (but always keep at least 2: head + 1) if (snake.length > 2) { seg.destroy(); snake.splice(s, 1); // Reduce score, but not below zero updateScore(Math.max(0, score - 25)); // Play hit sound LK.getSound('hit').play(); // Do NOT remove spike after hit; spike stays infinitely until despawn break; // Only one segment hit per spike per frame } else { // If only 2 left, end game endGame(); return; } } else { // Invincible: just destroy spike LK.effects.flashObject(obstacles[i], 0xffffff, 300); obstacles[i].destroy(); obstacles.splice(i, 1); i--; break; } } } continue; } // Normal/moving obstacle logic if (head.intersects(obstacles[i])) { if (!isInvincible) { endGame(); return; } else { // Flash obstacle and destroy it LK.effects.flashObject(obstacles[i], 0xffffff, 300); obstacles[i].destroy(); obstacles.splice(i, 1); i--; } } } // Check collision with self (skip first 4 segments for leniency) if (!isInvincible) { for (var i = 4; i < snake.length; ++i) { if (head.intersects(snake[i])) { endGame(); return; } } } // Evolving obstacles: every 30 seconds, add a new one (max 8) if (LK.ticks % (60 * 30) === 0 && obstacles.length < 8) { var obs; // 40% chance for spike, 30% moving, 30% static var r = Math.random(); if (r < 0.40) { obs = new SpikeObstacle(); } else if (r < 0.70) { obs = new MovingObstacle(); } else { obs = new Obstacle(); } // Avoid spawning on the snake and other obstacles var avoidList = []; for (var j = 0; j < snake.length; ++j) { avoidList.push(snake[j]); } for (var k = 0; k < obstacles.length; ++k) { avoidList.push(obstacles[k]); } var pos = randomPos(300, avoidList, 140); obs.x = pos.x; obs.y = pos.y; obstacles.push(obs); game.addChild(obs); } // After 2 minutes, every 20 seconds, add a new moving obstacle (max 12) if (LK.ticks > 60 * 120 && LK.ticks % (60 * 20) === 0 && obstacles.length < 12) { var obs2 = new MovingObstacle(); // Avoid spawning on the snake and other obstacles var avoidList2 = []; for (var j = 0; j < snake.length; ++j) { avoidList2.push(snake[j]); } for (var k = 0; k < obstacles.length; ++k) { avoidList2.push(obstacles[k]); } var pos2 = randomPos(300, avoidList2, 140); obs2.x = pos2.x; obs2.y = pos2.y; obstacles.push(obs2); game.addChild(obs2); } }; // --- Game start/reset --- function startGame() { gameOver = false; updateScore(0); resetSnake(); // Remove all AI snakes for (var i = 0; i < aiSnakes.length; ++i) { aiSnakes[i].destroy(); } aiSnakes = []; spawnFood(); if (powerup) { powerup.destroy(); powerup = null; } spawnObstacles(); deactivatePowerUp(); LK.playMusic('snakebg', { fade: { start: 0, end: 1, duration: 1000 } }); } startGame(); // --- Game over/win handling is automatic by LK --- // --- Music is started on game start ---
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// AISnake (AI snake that moves toward food and steals it)
var AISnake = Container.expand(function () {
var self = Container.call(this);
self.length = 3 + Math.floor(Math.random() * 2); // 3-4 segments
self.segments = [];
self.dir = 0;
self.speed = Math.max(4, SNAKE_INIT_SPEED * 0.35); // AI snake moves much slower than player
self.target = null;
self.stealing = false;
self.lastFood = null;
// Create head
var head = self.attachAsset('aiSnakeHead', {
anchorX: 0.5,
anchorY: 0.5
});
self.asset = head;
self.segments.push(self);
// Create body segments
for (var i = 1; i < self.length; ++i) {
var seg = new Container();
seg.asset = seg.attachAsset('aiSnakeBody', {
anchorX: 0.5,
anchorY: 0.5
});
seg.x = self.x - i * 60;
seg.y = self.y;
self.addChild(seg);
self.segments.push(seg);
}
// Custom hitbox
self.hitboxScale = 0.6;
// Custom intersects for head
self.intersects = function (other) {
var scaleA = typeof self.hitboxScale === "number" ? self.hitboxScale : 1;
var scaleB = typeof other.hitboxScale === "number" ? other.hitboxScale : 1;
var ax = self.x,
ay = self.y,
aw = self.asset.width * scaleA,
ah = self.asset.height * scaleA;
var bx = other.x,
by = other.y,
bw = other.asset && other.asset.width ? other.asset.width * scaleB : 0,
bh = other.asset && other.asset.height ? other.asset.height * scaleB : 0;
var rA = Math.max(aw, ah) / 2;
var rB = Math.max(bw, bh) / 2;
var dx = ax - bx,
dy = ay - by;
return dx * dx + dy * dy < (rA + rB) * (rA + rB);
};
// Update method
self.update = function () {
// Save last position for event triggers
if (self.lastX === undefined) self.lastX = self.x;
if (self.lastY === undefined) self.lastY = self.y;
// Find food if not already targeting
if (!self.target || self.target.destroyed) {
if (typeof food !== "undefined" && food && !food.destroyed) {
self.target = food;
} else {
self.target = null;
}
}
// Move toward food using player-like movement
if (self.target) {
// Calculate desired direction
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var desiredDir = Math.atan2(dy, dx);
// Smoothly turn toward the target (like player)
var d = desiredDir - self.dir;
while (d > Math.PI) d -= 2 * Math.PI;
while (d < -Math.PI) d += 2 * Math.PI;
var turn = Math.sign(d) * Math.min(Math.abs(d), SNAKE_TURN_ANGLE);
self.dir += turn;
// Move head forward
self.x += Math.cos(self.dir) * self.speed;
self.y += Math.sin(self.dir) * self.speed;
// Clamp to bounds
self.x = Math.max(100 + self.asset.width / 2, Math.min(2048 - 100 - self.asset.width / 2, self.x));
self.y = Math.max(100 + self.asset.height / 2, Math.min(2732 - 100 - self.asset.height / 2, self.y));
// Move body segments to follow the previous segment, like player
for (var i = 1; i < self.segments.length; ++i) {
var seg = self.segments[i];
var target = self.segments[i - 1];
var sdx = target.x - seg.x;
var sdy = target.y - seg.y;
var sdist = Math.sqrt(sdx * sdx + sdy * sdy);
var desired = self.speed * 0.9;
if (sdist > desired) {
var move = (sdist - desired) * 0.5;
seg.x += sdx / sdist * move;
seg.y += sdy / sdist * move;
}
}
// Steal food if close enough
if (self.target && self.intersects(self.target)) {
self.stealing = true;
// Remove food from game
if (typeof food !== "undefined" && food === self.target) {
food.destroy();
food = null;
// Always respawn food after AI steals it
if (typeof spawnFood === "function") {
spawnFood();
}
}
self.target = null;
}
}
// Despawn if off screen or after stealing
if (self.stealing) {
// Move off screen quickly
self.x += Math.cos(self.dir) * self.speed * 2;
self.y += Math.sin(self.dir) * self.speed * 2;
if (self.x < -200 || self.x > 2048 + 200 || self.y < -200 || self.y > 2732 + 200) {
self._despawn = true;
}
}
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
// Food
var Food = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('food', {
anchorX: 0.5,
anchorY: 0.5
});
self.hitboxScale = 0.6;
return self;
});
// FoodPurple
var FoodPurple = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('foodPurple', {
anchorX: 0.5,
anchorY: 0.5
});
self.hitboxScale = 0.6;
return self;
});
// FoodRainbow (rare, big bonus)
var FoodRainbow = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('food', {
anchorX: 0.5,
anchorY: 0.5
});
self.hitboxScale = 0.6;
// Animate color cycling
// Animate color cycling using tween plugin API
tween(self.asset, {
tint: 0xff0000
}, {
duration: 400,
easing: tween.linear,
onFinish: function onFinish() {
// No-op, will be repeated
}
});
// Manually cycle through rainbow colors in update (since tween can't do this directly)
self.update = function () {
var t = LK.ticks % 60 / 60;
var r = Math.floor(127 * (Math.sin(2 * Math.PI * t) + 1));
var g = Math.floor(127 * (Math.sin(2 * Math.PI * t + 2) + 1));
var b = Math.floor(127 * (Math.sin(2 * Math.PI * t + 4) + 1));
self.asset.tint = r << 16 | g << 8 | b;
};
return self;
});
// MovingObstacle
var MovingObstacle = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('obstacleMoving', {
anchorX: 0.5,
anchorY: 0.5
});
self.hitboxScale = 0.7;
// Pick a random direction and speed
var angle = Math.random() * Math.PI * 2;
var speed = 4 + Math.random() * 4;
self.vx = Math.cos(angle) * speed;
self.vy = Math.sin(angle) * speed;
self.update = function () {
// Save last position for event triggers
if (self.lastX === undefined) self.lastX = self.x;
if (self.lastY === undefined) self.lastY = self.y;
self.x += self.vx;
self.y += self.vy;
// Despawn if out of bounds (with 150px margin)
if (self.x < -150 || self.x > 2048 + 150 || self.y < -150 || self.y > 2732 + 150) {
// Mark for removal by setting a flag
self._despawn = true;
return;
}
// Bounce off walls (100px margin)
if (self.x < 100 + self.asset.width / 2 && self.vx < 0) self.vx *= -1;
if (self.x > 2048 - 100 - self.asset.width / 2 && self.vx > 0) self.vx *= -1;
if (self.y < 100 + self.asset.height / 2 && self.vy < 0) self.vy *= -1;
if (self.y > 2732 - 100 - self.asset.height / 2 && self.vy > 0) self.vy *= -1;
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
// Obstacle (static)
var Obstacle = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
self.hitboxScale = 0.7;
// Add despawn timer and update method for static obstacles
self._despawnTimer = 0;
self._despawnInterval = 60 * 60; // Try to despawn every 60 seconds (3600 ticks)
self.update = function () {
if (self._despawnTimer === undefined) self._despawnTimer = 0;
self._despawnTimer++;
// Only check every _despawnInterval ticks
if (self._despawnTimer >= self._despawnInterval) {
self._despawnTimer = 0;
// 40% chance to despawn (less often than moving obstacles)
if (Math.random() < 0.4) {
self._despawn = true;
}
}
};
return self;
});
// PowerUp
var PowerUp = Container.expand(function () {
var self = Container.call(this);
self.type = 'invincible'; // or 'shrink'
self.asset = null;
self.hitboxScale = 0.6;
self.setType = function (type) {
if (self.asset) self.removeChild(self.asset);
self.type = type;
if (type === 'invincible') {
self.asset = self.attachAsset('powerupInvincible', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'shrink') {
self.asset = self.attachAsset('powerupShrink', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
self.setType('invincible');
return self;
});
// Snake Segment (body or head)
var SnakeSegment = Container.expand(function () {
var self = Container.call(this);
self.isHead = false;
self.asset = null;
// Custom hitbox size (smaller than visual asset)
self.hitboxScale = 0.6; // 60% of asset size for both head and body
self.setType = function (type) {
if (self.asset) self.removeChild(self.asset);
if (type === 'head') {
self.asset = self.attachAsset('snakeHead', {
anchorX: 0.5,
anchorY: 0.5
});
self.isHead = true;
} else {
self.asset = self.attachAsset('snakeBody', {
anchorX: 0.5,
anchorY: 0.5
});
self.isHead = false;
}
};
self.setType('body');
// Custom intersects method for smaller hitbox
self.intersects = function (other) {
// Use hitboxScale for both this and other if available, else default to 1
var scaleA = typeof self.hitboxScale === "number" ? self.hitboxScale : 1;
var scaleB = typeof other.hitboxScale === "number" ? other.hitboxScale : 1;
var ax = self.x,
ay = self.y,
aw = self.asset.width * scaleA,
ah = self.asset.height * scaleA;
var bx = other.x,
by = other.y,
bw = other.asset && other.asset.width ? other.asset.width * scaleB : 0,
bh = other.asset && other.asset.height ? other.asset.height * scaleB : 0;
// Circle collision (since snake is ellipse/circle)
var rA = Math.max(aw, ah) / 2;
var rB = Math.max(bw, bh) / 2;
var dx = ax - bx;
var dy = ay - by;
return dx * dx + dy * dy < (rA + rB) * (rA + rB);
};
return self;
});
// SpikeObstacle (destroys snake segments it touches, rare spawn)
var SpikeObstacle = Container.expand(function () {
var self = Container.call(this);
// Add spike obstacle asset first (so aura is in front)
self.asset = self.attachAsset('spikeObstacle', {
anchorX: 0.5,
anchorY: 0.5
});
// Add spike aura in front of the spike asset
self.aura = self.attachAsset('spikeAura', {
anchorX: 0.5,
anchorY: 0.5
});
self.aura.alpha = 0.35;
self.hitboxScale = 0.7;
// Add despawn timer and update method for static obstacles
self._despawnTimer = 0;
self._despawnInterval = 60 * 45; // Try to despawn every 45 seconds (less often than normal obstacles)
self.update = function () {
if (self._despawnTimer === undefined) self._despawnTimer = 0;
self._despawnTimer++;
// Only check every _despawnInterval ticks
if (self._despawnTimer >= self._despawnInterval) {
self._despawnTimer = 0;
// 60% chance to despawn (spikes are rare, so despawn more often)
if (Math.random() < 0.6) {
self._despawn = true;
}
}
// Animate aura pulse
if (self.aura) {
var t = LK.ticks % 60 / 60;
self.aura.alpha = 0.25 + 0.15 * Math.sin(2 * Math.PI * t);
var scale = 1.05 + 0.08 * Math.sin(2 * Math.PI * t);
self.aura.scaleX = scale;
self.aura.scaleY = scale;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Music
// Sounds
// Obstacles
// Power-ups
// Food
// Snake head and body
// --- Game constants ---
var SNAKE_INIT_LENGTH = 5;
var SNAKE_INIT_SPEED = 16; // pixels per tick
var SNAKE_MIN_SPEED = 8;
var SNAKE_MAX_SPEED = 40;
var SNAKE_TURN_ANGLE = Math.PI / 18; // 10 degrees per input
var FOOD_SCORE = 10;
var POWERUP_SCORE = 25;
var POWERUP_DURATION = 300; // ticks (5 seconds)
var OBSTACLE_COUNT = 3;
var OBSTACLE_SPEED = 6;
// --- Game state ---
var snake = [];
var aiSnakes = []; // Array of AI snakes
var snakeDir = 0; // in radians, 0 = right
var snakeNextDir = 0;
var snakeSpeed = SNAKE_INIT_SPEED;
var snakeGrow = 0;
var food = null;
var powerup = null;
var powerupActive = false;
var powerupType = null;
var powerupTimer = 0;
var obstacles = [];
var score = 0;
var isInvincible = false;
var lastTouch = null;
var dragStart = null;
var dragAngle = null;
var dragActive = false;
var gameOver = false;
// --- GUI ---
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Helper functions ---
function randomPos(margin, avoidList, avoidRadius) {
margin = margin || 150;
avoidList = avoidList || [];
avoidRadius = avoidRadius || 120; // Default: avoid within 120px of any avoidList item
var tries = 0;
while (tries < 50) {
var x = margin + Math.random() * (2048 - 2 * margin);
var y = margin + Math.random() * (2732 - 2 * margin);
var ok = true;
for (var i = 0; i < avoidList.length; ++i) {
var obj = avoidList[i];
var dx = obj.x - x;
var dy = obj.y - y;
if (dx * dx + dy * dy < avoidRadius * avoidRadius) {
ok = false;
break;
}
}
if (ok) {
return {
x: x,
y: y
};
}
tries++;
}
// Fallback: just return a random position if we can't find a good one
return {
x: margin + Math.random() * (2048 - 2 * margin),
y: margin + Math.random() * (2732 - 2 * margin)
};
}
function dist2(a, b) {
var dx = a.x - b.x,
dy = a.y - b.y;
return dx * dx + dy * dy;
}
function angleBetween(a, b) {
return Math.atan2(b.y - a.y, b.x - a.x);
}
function wrapPos(pos) {
// Not used, but could be for wrap-around
if (pos.x < 0) pos.x += 2048;
if (pos.x > 2048) pos.x -= 2048;
if (pos.y < 0) pos.y += 2732;
if (pos.y > 2732) pos.y -= 2732;
}
function spawnFood() {
if (food) food.destroy();
// 5% chance for rainbow food, 20% for purple, else normal
var r = Math.random();
if (r < 0.05) {
food = new FoodRainbow();
food.isRainbow = true;
food.isPurple = false;
} else if (r < 0.25) {
food = new FoodPurple();
food.isPurple = true;
food.isRainbow = false;
} else {
food = new Food();
food.isPurple = false;
food.isRainbow = false;
}
// Avoid spawning on the snake and obstacles
var avoidList = [];
for (var i = 0; i < snake.length; ++i) {
avoidList.push(snake[i]);
}
// Also avoid obstacles
for (var i = 0; i < obstacles.length; ++i) {
avoidList.push(obstacles[i]);
}
var pos = randomPos(200, avoidList, 120);
food.x = pos.x;
food.y = pos.y;
game.addChild(food);
}
function spawnPowerUp() {
if (powerup) powerup.destroy();
powerup = new PowerUp();
// Avoid spawning on the snake
var avoidList = [];
for (var i = 0; i < snake.length; ++i) {
avoidList.push(snake[i]);
}
var pos = randomPos(200, avoidList, 120);
// 67% invincible, 33% shrink
var r = Math.random();
var t;
if (r < 0.67) t = 'invincible';else t = 'shrink';
powerup.setType(t);
powerup.x = pos.x;
powerup.y = pos.y;
game.addChild(powerup);
}
function spawnObstacles() {
for (var i = 0; i < obstacles.length; ++i) {
obstacles[i].destroy();
}
obstacles = [];
// Avoid spawning on the snake
var avoidList = [];
for (var j = 0; j < snake.length; ++j) {
avoidList.push(snake[j]);
}
for (var i = 0; i < OBSTACLE_COUNT; ++i) {
var obs = new Obstacle();
var pos = randomPos(300, avoidList, 140);
obs.x = pos.x;
obs.y = pos.y;
// Add this obstacle to avoidList so next ones don't overlap
avoidList.push(obs);
obstacles.push(obs);
game.addChild(obs);
}
}
function resetSnake() {
for (var i = 0; i < snake.length; ++i) {
snake[i].destroy();
}
snake = [];
var startX = 2048 / 2,
startY = 2732 / 2;
for (var i = 0; i < SNAKE_INIT_LENGTH; ++i) {
var seg = new SnakeSegment();
if (i === 0) {
seg.setType('head');
// Head spawns in front of the body, not on top of the first body segment
seg.x = startX;
seg.y = startY;
} else {
seg.setType('body');
// Each body segment spawns behind the previous one, extending left from the head
seg.x = startX - i * snakeSpeed * 1.2;
seg.y = startY;
}
game.addChild(seg);
snake.push(seg);
}
snakeDir = 0;
snakeNextDir = 0;
snakeSpeed = SNAKE_INIT_SPEED;
snakeGrow = 0;
isInvincible = false;
powerupActive = false;
powerupType = null;
powerupTimer = 0;
}
function updateScore(val) {
score = val;
scoreTxt.setText(score);
LK.setScore(score);
}
function activatePowerUp(type) {
powerupActive = true;
powerupType = type;
powerupTimer = POWERUP_DURATION;
if (type === 'invincible') {
isInvincible = true;
// Flash snake
for (var i = 0; i < snake.length; ++i) {
LK.effects.flashObject(snake[i], 0xf1c40f, 500);
}
} else if (type === 'shrink') {
// Remove 3 segments from the tail, but always keep at least 5
var minLen = 5;
var removeCount = Math.min(3, snake.length - minLen);
for (var i = 0; i < removeCount; ++i) {
var seg = snake.pop();
seg.destroy();
}
}
}
function deactivatePowerUp() {
powerupActive = false;
powerupType = null;
powerupTimer = 0;
isInvincible = false;
}
function endGame() {
if (gameOver) return;
gameOver = true;
LK.effects.flashScreen(0xff0000, 1000);
LK.getSound('hit').play();
LK.showGameOver();
}
// --- Input handling (touch/drag to steer) ---
game.down = function (x, y, obj) {
// Only allow drag from outside top left 100x100
if (x < 100 && y < 100) return;
dragStart = {
x: x,
y: y
};
dragAngle = null;
dragActive = true;
};
game.move = function (x, y, obj) {
if (!dragActive) return;
if (!dragStart) return;
// Calculate angle from snake head to drag point
var head = snake[0];
var dx = x - head.x;
var dy = y - head.y;
if (dx * dx + dy * dy < 100) return; // Ignore tiny drags
var angle = Math.atan2(dy, dx);
snakeNextDir = angle;
dragAngle = angle;
};
game.up = function (x, y, obj) {
dragActive = false;
dragStart = null;
dragAngle = null;
};
// --- Main game update loop ---
game.update = function () {
if (gameOver) return;
// Power-up timer
if (powerupActive) {
powerupTimer--;
if (powerupTimer <= 0) {
deactivatePowerUp();
}
}
// --- AI Snake logic ---
// Update AI snakes
for (var i = aiSnakes.length - 1; i >= 0; --i) {
var ai = aiSnakes[i];
if (ai.update) ai.update();
// Check if AI snake collides with any player tail segment (not head)
// Only if not already despawning
if (!ai._despawn && snake.length > 4) {
// Start from segment 4 (skip head and first 3 for leniency)
for (var s = 4; s < snake.length; ++s) {
var seg = snake[s];
// Only check if segment exists and not destroyed
if (seg && !seg.destroyed) {
// Track last intersection state for this ai/segment pair
if (!ai.lastTailIntersect) ai.lastTailIntersect = {};
if (ai.lastTailIntersect[s] === undefined) ai.lastTailIntersect[s] = false;
var nowIntersect = ai.intersects(seg);
if (!ai.lastTailIntersect[s] && nowIntersect) {
// AI snake dies!
LK.effects.flashObject(ai, 0xff0000, 600);
ai._despawn = true;
// Optionally, give player a score bonus for killing AI
updateScore(score + 30);
break;
}
ai.lastTailIntersect[s] = nowIntersect;
}
}
}
// Remove if marked for despawn
if (ai._despawn) {
ai.destroy();
aiSnakes.splice(i, 1);
}
}
// Spawn a new AI snake less frequently (about every 14-18 seconds, only one at a time)
if (aiSnakes.length === 0 && LK.ticks > 60 * 5 && LK.ticks % (60 * (14 + Math.floor(Math.random() * 5))) === 0 && food && !food.isRainbow) {
// Only spawn if food exists and is not rainbow (AI doesn't steal rainbow food)
var avoidList = [];
for (var j = 0; j < snake.length; ++j) avoidList.push(snake[j]);
for (var k = 0; k < obstacles.length; ++k) avoidList.push(obstacles[k]);
// Spawn at random edge
var edge = Math.floor(Math.random() * 4);
var pos;
if (edge === 0) {
// left
pos = {
x: 120,
y: 200 + Math.random() * (2732 - 400)
};
} else if (edge === 1) {
// right
pos = {
x: 2048 - 120,
y: 200 + Math.random() * (2732 - 400)
};
} else if (edge === 2) {
// top
pos = {
x: 200 + Math.random() * (2048 - 400),
y: 120
};
} else {
// bottom
pos = {
x: 200 + Math.random() * (2048 - 400),
y: 2732 - 120
};
}
// Avoid spawning on snake or obstacles
var ok = true;
for (var i = 0; i < avoidList.length; ++i) {
var obj = avoidList[i];
var dx = obj.x - pos.x;
var dy = obj.y - pos.y;
if (dx * dx + dy * dy < 200 * 200) {
ok = false;
break;
}
}
if (ok) {
var ai = new AISnake();
ai.x = pos.x;
ai.y = pos.y;
aiSnakes.push(ai);
game.addChild(ai);
}
}
// Update obstacles (moving, static, and spike)
for (var i = obstacles.length - 1; i >= 0; --i) {
if (obstacles[i].update) obstacles[i].update();
// Remove moving obstacles that have been marked for despawn
if (obstacles[i] instanceof MovingObstacle && obstacles[i]._despawn) {
obstacles[i].destroy();
obstacles.splice(i, 1);
continue;
}
// Remove static obstacles that have been marked for despawn
if (obstacles[i] instanceof Obstacle && obstacles[i]._despawn) {
obstacles[i].destroy();
obstacles.splice(i, 1);
continue;
}
// Remove spike obstacles that have been marked for despawn
if (typeof SpikeObstacle !== "undefined" && obstacles[i] instanceof SpikeObstacle && obstacles[i]._despawn) {
obstacles[i].destroy();
obstacles.splice(i, 1);
continue;
}
}
// Obstacles are static, no update needed
// Snake direction: smooth turn toward nextDir
var d = snakeNextDir - snakeDir;
while (d > Math.PI) d -= 2 * Math.PI;
while (d < -Math.PI) d += 2 * Math.PI;
if (Math.abs(d) > 0.01) {
var turn = Math.sign(d) * Math.min(Math.abs(d), SNAKE_TURN_ANGLE);
snakeDir += turn;
}
// Move snake head
var head = snake[0];
var prevPos = {
x: head.x,
y: head.y
};
head.x += Math.cos(snakeDir) * snakeSpeed;
head.y += Math.sin(snakeDir) * snakeSpeed;
// Clamp to bounds (leave 100px margin for UI)
head.x = Math.max(100 + head.asset.width / 2, Math.min(2048 - 100 - head.asset.width / 2, head.x));
head.y = Math.max(100 + head.asset.height / 2, Math.min(2732 - 100 - head.asset.height / 2, head.y));
// Move body segments to follow
for (var i = 1; i < snake.length; ++i) {
var seg = snake[i];
var target = snake[i - 1];
var dx = target.x - seg.x;
var dy = target.y - seg.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var desired = snakeSpeed * 0.9;
if (dist > desired) {
var move = (dist - desired) * 0.5;
seg.x += dx / dist * move;
seg.y += dy / dist * move;
}
}
// Grow snake if needed
if (snakeGrow > 0) {
var tail = snake[snake.length - 1];
var newSeg = new SnakeSegment();
newSeg.setType('body');
newSeg.x = tail.x;
newSeg.y = tail.y;
game.addChild(newSeg);
snake.push(newSeg);
snakeGrow--;
}
// Check collision with food
if (food && head.intersects(food)) {
LK.getSound('eat').play();
if (food.isRainbow) {
updateScore(score + FOOD_SCORE * 10);
snakeGrow += 12;
// Flash the whole snake rainbow
for (var i = 0; i < snake.length; ++i) {
LK.effects.flashObject(snake[i], 0xffffff, 800);
}
} else if (food.isPurple) {
updateScore(score + FOOD_SCORE * 3);
snakeGrow += 6;
} else {
updateScore(score + FOOD_SCORE);
snakeGrow += 2;
}
food.destroy();
food = null;
// If an AI snake stole the food, don't respawn (AI snake update will handle it)
// Only respawn food if it was NOT stolen by an AI snake this frame
if (!aiSnakes.some(function (ai) {
return ai.stealing;
})) {
spawnFood();
}
// Chance to spawn powerup
if (!powerup && Math.random() < 0.2) {
spawnPowerUp();
}
}
// Check collision with powerup
if (powerup && head.intersects(powerup)) {
LK.getSound('powerup').play();
updateScore(score + POWERUP_SCORE);
activatePowerUp(powerup.type);
// Flash the screen for feedback
LK.effects.flashScreen(0xf1c40f, 400);
powerup.destroy();
powerup = null;
}
// Check collision with obstacles
for (var i = 0; i < obstacles.length; ++i) {
// SpikeObstacle: destroys segments it touches, reduces score
if (typeof SpikeObstacle !== "undefined" && obstacles[i] instanceof SpikeObstacle) {
// Check all segments (not just head)
for (var s = 0; s < snake.length; ++s) {
var seg = snake[s];
if (seg.intersects(obstacles[i])) {
// Only destroy if not invincible
if (!isInvincible) {
// Flash segment and obstacle
LK.effects.flashObject(seg, 0xda2cff, 400);
LK.effects.flashObject(obstacles[i], 0xda2cff, 400);
// Remove segment (but always keep at least 2: head + 1)
if (snake.length > 2) {
seg.destroy();
snake.splice(s, 1);
// Reduce score, but not below zero
updateScore(Math.max(0, score - 25));
// Play hit sound
LK.getSound('hit').play();
// Do NOT remove spike after hit; spike stays infinitely until despawn
break; // Only one segment hit per spike per frame
} else {
// If only 2 left, end game
endGame();
return;
}
} else {
// Invincible: just destroy spike
LK.effects.flashObject(obstacles[i], 0xffffff, 300);
obstacles[i].destroy();
obstacles.splice(i, 1);
i--;
break;
}
}
}
continue;
}
// Normal/moving obstacle logic
if (head.intersects(obstacles[i])) {
if (!isInvincible) {
endGame();
return;
} else {
// Flash obstacle and destroy it
LK.effects.flashObject(obstacles[i], 0xffffff, 300);
obstacles[i].destroy();
obstacles.splice(i, 1);
i--;
}
}
}
// Check collision with self (skip first 4 segments for leniency)
if (!isInvincible) {
for (var i = 4; i < snake.length; ++i) {
if (head.intersects(snake[i])) {
endGame();
return;
}
}
}
// Evolving obstacles: every 30 seconds, add a new one (max 8)
if (LK.ticks % (60 * 30) === 0 && obstacles.length < 8) {
var obs;
// 40% chance for spike, 30% moving, 30% static
var r = Math.random();
if (r < 0.40) {
obs = new SpikeObstacle();
} else if (r < 0.70) {
obs = new MovingObstacle();
} else {
obs = new Obstacle();
}
// Avoid spawning on the snake and other obstacles
var avoidList = [];
for (var j = 0; j < snake.length; ++j) {
avoidList.push(snake[j]);
}
for (var k = 0; k < obstacles.length; ++k) {
avoidList.push(obstacles[k]);
}
var pos = randomPos(300, avoidList, 140);
obs.x = pos.x;
obs.y = pos.y;
obstacles.push(obs);
game.addChild(obs);
}
// After 2 minutes, every 20 seconds, add a new moving obstacle (max 12)
if (LK.ticks > 60 * 120 && LK.ticks % (60 * 20) === 0 && obstacles.length < 12) {
var obs2 = new MovingObstacle();
// Avoid spawning on the snake and other obstacles
var avoidList2 = [];
for (var j = 0; j < snake.length; ++j) {
avoidList2.push(snake[j]);
}
for (var k = 0; k < obstacles.length; ++k) {
avoidList2.push(obstacles[k]);
}
var pos2 = randomPos(300, avoidList2, 140);
obs2.x = pos2.x;
obs2.y = pos2.y;
obstacles.push(obs2);
game.addChild(obs2);
}
};
// --- Game start/reset ---
function startGame() {
gameOver = false;
updateScore(0);
resetSnake();
// Remove all AI snakes
for (var i = 0; i < aiSnakes.length; ++i) {
aiSnakes[i].destroy();
}
aiSnakes = [];
spawnFood();
if (powerup) {
powerup.destroy();
powerup = null;
}
spawnObstacles();
deactivatePowerUp();
LK.playMusic('snakebg', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
}
startGame();
// --- Game over/win handling is automatic by LK ---
// --- Music is started on game start ---