/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Bullet = Container.expand(function () { var self = Container.call(this); var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 8; self.direction = { x: 0, y: 0 }; self.lifetime = 0; self.maxLifetime = 120; // 2 seconds at 60fps self.update = function () { self.x += self.direction.x * self.speed; self.y += self.direction.y * self.speed; self.lifetime++; }; return self; }); var Enemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, tint: 0xFFFFFF }); self.speed = 1; self.health = 2; self.attackRange = 40; self.attackCooldown = 0; self.attackInterval = 60; // Attack every second at 60fps self.patrolDirection = Math.random() > 0.5 ? 1 : -1; // Random initial direction self.patrolAxis = Math.random() > 0.5 ? 'x' : 'y'; // Random patrol axis (horizontal or vertical) self.patrolDistance = CELL_SIZE * 3; // Patrol 3 cells distance self.startPosition = { x: 0, y: 0 }; // Will be set when enemy is placed self.isMoving = false; self.lastPlayerDistance = Infinity; self.startPatrol = function () { if (self.isMoving) return; self.isMoving = true; var targetX = self.x; var targetY = self.y; // Calculate target position based on patrol axis if (self.patrolAxis === 'x') { targetX = self.startPosition.x + self.patrolDirection * self.patrolDistance; } else { targetY = self.startPosition.y + self.patrolDirection * self.patrolDistance; } // Check if target position is valid if (canMoveTo(targetX, targetY)) { // Store the original position for safe restoration var originalX = self.x; var originalY = self.y; // Move to target position using tween tween(self, { x: targetX, y: targetY }, { duration: 2000, easing: tween.linear, onFinish: function onFinish() { // Ensure enemy is at a valid position after movement if (!canMoveTo(self.x, self.y)) { // If somehow ended up in invalid position, restore to original self.x = originalX; self.y = originalY; } self.isMoving = false; self.patrolDirection *= -1; // Reverse direction } }); } else { // If can't move in current direction, reverse immediately self.isMoving = false; self.patrolDirection *= -1; } }; self.update = function () { // Add bouncing animation when enemy is not moving (idle/waiting state) if (!self.isMoving) { if (!self.bounceTimer) self.bounceTimer = 0; self.bounceTimer++; // Create a subtle bouncing effect var bounceOffset = Math.sin(self.bounceTimer * 0.15) * 2; // Use current position as base instead of startPosition to avoid conflicts var baseY = self.startPosition.y; self.y = baseY + bounceOffset; } else { // Reset bounce timer when moving to ensure smooth transitions self.bounceTimer = 0; } var dx = player.x - self.x; var dy = player.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); var detectionRange = CELL_SIZE * 4; // Enemy can see player from 4 cells away // Check if player is within detection range and line of sight (optimized - check every 10 frames) var canSeePlayer = false; if (distance < detectionRange) { // Only do expensive line of sight calculation every 10 frames if (!self.losCheckTimer) self.losCheckTimer = 0; self.losCheckTimer++; if (self.losCheckTimer >= 10 || !self.hasOwnProperty('lastCanSeePlayer')) { self.losCheckTimer = 0; // Simple line of sight check - check if there are walls between enemy and player var steps = Math.ceil(distance / 40); // Reduced precision from 20 to 40 var stepX = dx / steps; var stepY = dy / steps; self.lastCanSeePlayer = true; for (var step = 1; step < steps; step++) { var checkX = self.x + stepX * step; var checkY = self.y + stepY * step; if (!canMoveTo(checkX, checkY)) { self.lastCanSeePlayer = false; break; } } } canSeePlayer = self.lastCanSeePlayer; } else { canSeePlayer = self.lastCanSeePlayer; } // If can see player and not in attack range, move towards player if (canSeePlayer && distance > self.attackRange) { if (!self.isMoving) { self.isMoving = true; // Stop current patrol movement tween.stop(self); // Calculate next position towards player (one cell at a time) var moveDistance = CELL_SIZE; var normalizedDx = dx / distance; var normalizedDy = dy / distance; var targetX = self.x + normalizedDx * moveDistance; var targetY = self.y + normalizedDy * moveDistance; // Try to move towards player, prioritize the axis with larger distance if (Math.abs(dx) > Math.abs(dy)) { // Try horizontal movement first var horizontalTarget = self.x + (dx > 0 ? moveDistance : -moveDistance); if (canMoveTo(horizontalTarget, self.y)) { targetX = horizontalTarget; targetY = self.y; } else if (canMoveTo(self.x, self.y + (dy > 0 ? moveDistance : -moveDistance))) { // If horizontal blocked, try vertical targetX = self.x; targetY = self.y + (dy > 0 ? moveDistance : -moveDistance); } else { // Can't move towards player targetX = self.x; targetY = self.y; } } else { // Try vertical movement first var verticalTarget = self.y + (dy > 0 ? moveDistance : -moveDistance); if (canMoveTo(self.x, verticalTarget)) { targetX = self.x; targetY = verticalTarget; } else if (canMoveTo(self.x + (dx > 0 ? moveDistance : -moveDistance), self.y)) { // If vertical blocked, try horizontal targetX = self.x + (dx > 0 ? moveDistance : -moveDistance); targetY = self.y; } else { // Can't move towards player targetX = self.x; targetY = self.y; } } // Move towards calculated target if (targetX !== self.x || targetY !== self.y) { // Store current position for safety var safeX = self.x; var safeY = self.y; tween(self, { x: targetX, y: targetY }, { duration: 1000, easing: tween.linear, onFinish: function onFinish() { // Validate final position if (!canMoveTo(self.x, self.y)) { self.x = safeX; self.y = safeY; } self.isMoving = false; } }); } else { self.isMoving = false; } } } else if (!canSeePlayer) { // Start patrol movement if not already moving and can't see player if (!self.isMoving) { self.startPatrol(); } } // Attack player if in range if (distance < self.attackRange) { // Stop patrolling when in attack range if (!self.lastPlayerDistance || self.lastPlayerDistance >= self.attackRange) { tween.stop(self); self.isMoving = false; } // Continuously attack while in range if (self.attackCooldown <= 0) { // Perform melee attack playerHealth--; LK.effects.flashObject(player, 0xFF0000, 500); // Visual attack feedback - enemy briefly grows and turns red tween(self, { tint: 0xFF0000, scaleX: 1.3, scaleY: 1.3 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { // Return to normal appearance tween(self, { tint: 0xFFFFFF, scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); // Reset attack cooldown self.attackCooldown = self.attackInterval; } } // Update attack cooldown if (self.attackCooldown > 0) { self.attackCooldown--; } self.lastPlayerDistance = distance; }; return self; }); var Ore = Container.expand(function () { var self = Container.call(this); var oreGraphics = self.attachAsset('ore', { anchorX: 0.5, anchorY: 0.5 }); self.value = 1; self.bobTimer = 0; self.baseY = self.y; self.update = function () { self.bobTimer++; self.y = self.baseY + Math.sin(self.bobTimer * 0.1) * 3; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x2C1810 }); /**** * Game Code ****/ // Game constants var MAZE_WIDTH = 13; var MAZE_HEIGHT = 17; var CELL_SIZE = 80; var MAZE_OFFSET_X = (2048 - MAZE_WIDTH * CELL_SIZE) / 2; var MAZE_OFFSET_Y = 100; // Game variables var maze = []; var player; var enemies = []; var ores = []; var bullets = []; var playerHealth = 3; var oreCollected = 0; var oreTarget = 10; var level = 1; var exit; var gameContainer; // UI elements var healthText = new Text2('Health: 3', { size: 60, fill: 0xFF4444 }); healthText.anchor.set(0, 0); LK.gui.topLeft.addChild(healthText); var oreText = new Text2('Ore: 0/10', { size: 60, fill: 0xFFD700 }); oreText.anchor.set(0.5, 0); LK.gui.top.addChild(oreText); var levelText = new Text2('Level: 1', { size: 60, fill: 0xFFFFFF }); levelText.anchor.set(1, 0); LK.gui.topRight.addChild(levelText); // Initialize maze array function initializeMaze() { maze = []; for (var y = 0; y < MAZE_HEIGHT; y++) { maze[y] = []; for (var x = 0; x < MAZE_WIDTH; x++) { maze[y][x] = 1; // 1 = wall, 0 = floor } } } // Simple maze generation using recursive backtracking function generateMaze() { initializeMaze(); var stack = []; var startX = 1; var startY = 1; maze[startY][startX] = 0; stack.push({ x: startX, y: startY }); while (stack.length > 0) { var current = stack[stack.length - 1]; var neighbors = []; // Check all four directions var directions = [{ x: 0, y: -2 }, { x: 2, y: 0 }, { x: 0, y: 2 }, { x: -2, y: 0 }]; for (var i = 0; i < directions.length; i++) { var nx = current.x + directions[i].x; var ny = current.y + directions[i].y; if (nx > 0 && nx < MAZE_WIDTH - 1 && ny > 0 && ny < MAZE_HEIGHT - 1 && maze[ny][nx] === 1) { neighbors.push({ x: nx, y: ny, dir: directions[i] }); } } if (neighbors.length > 0) { var chosen = neighbors[Math.floor(Math.random() * neighbors.length)]; // Remove wall between current and chosen maze[current.y + chosen.dir.y / 2][current.x + chosen.dir.x / 2] = 0; maze[chosen.y][chosen.x] = 0; stack.push(chosen); } else { stack.pop(); } } } // Create visual maze function createMazeVisual() { gameContainer = game.addChild(new Container()); for (var y = 0; y < MAZE_HEIGHT; y++) { for (var x = 0; x < MAZE_WIDTH; x++) { var cellX = MAZE_OFFSET_X + x * CELL_SIZE; var cellY = MAZE_OFFSET_Y + y * CELL_SIZE; if (maze[y][x] === 1) { // Wall var wall = LK.getAsset('wall', { x: cellX, y: cellY }); gameContainer.addChild(wall); } else { // Floor var floor = LK.getAsset('floor', { x: cellX, y: cellY }); gameContainer.addChild(floor); } } } } // Place ore in maze function placeOre() { var oreCount = oreTarget + Math.floor(level / 2); var placed = 0; while (placed < oreCount) { var x = Math.floor(Math.random() * MAZE_WIDTH); var y = Math.floor(Math.random() * MAZE_HEIGHT); if (maze[y][x] === 0 && (x !== 1 || y !== 1)) { var ore = gameContainer.addChild(new Ore()); ore.x = MAZE_OFFSET_X + x * CELL_SIZE + CELL_SIZE / 2; ore.y = MAZE_OFFSET_Y + y * CELL_SIZE + CELL_SIZE / 2; ore.baseY = ore.y; ores.push(ore); placed++; } } } // Place enemies in maze function placeEnemies() { var enemyCount = 3 + level; var placed = 0; while (placed < enemyCount) { var x = Math.floor(Math.random() * MAZE_WIDTH); var y = Math.floor(Math.random() * MAZE_HEIGHT); if (maze[y][x] === 0 && (x !== 1 || y !== 1) && Math.sqrt((x - 1) * (x - 1) + (y - 1) * (y - 1)) > 5) { var enemy = gameContainer.addChild(new Enemy()); enemy.x = MAZE_OFFSET_X + x * CELL_SIZE + CELL_SIZE / 2; enemy.y = MAZE_OFFSET_Y + y * CELL_SIZE + CELL_SIZE / 2; enemy.startPosition.x = enemy.x; enemy.startPosition.y = enemy.y; enemies.push(enemy); placed++; } } } // Place exit function placeExit() { // Find a position far from start var placed = false; while (!placed) { var x = Math.floor(Math.random() * MAZE_WIDTH); var y = Math.floor(Math.random() * MAZE_HEIGHT); if (maze[y][x] === 0 && Math.sqrt((x - 1) * (x - 1) + (y - 1) * (y - 1)) > 10) { exit = gameContainer.addChild(LK.getAsset('exit', { x: MAZE_OFFSET_X + x * CELL_SIZE, y: MAZE_OFFSET_Y + y * CELL_SIZE })); placed = true; } } } // Create detailed background with various environmental elements function createDetailedBackground() { if (!gameContainer) { console.log("gameContainer not available for background creation"); return; } var backgroundElements = []; // Select one of 9 background variations randomly var backgroundType = Math.floor(Math.random() * 9); // Background variation 0: Rock dominant if (backgroundType === 0) { // Add many background rocks for (var i = 0; i < 15; i++) { var bgRock1 = gameContainer.addChild(LK.getAsset('bgRock1', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.4, rotation: Math.random() * Math.PI * 2 })); backgroundElements.push(bgRock1); } for (var i = 0; i < 20; i++) { var bgRock2 = gameContainer.addChild(LK.getAsset('bgRock2', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.3, rotation: Math.random() * Math.PI * 2 })); backgroundElements.push(bgRock2); } } // Background variation 1: Crystal cave else if (backgroundType === 1) { for (var i = 0; i < 30; i++) { var bgCrystal = gameContainer.addChild(LK.getAsset('bgCrystal', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.5, rotation: Math.random() * Math.PI * 2, scaleY: 0.8 + Math.random() * 0.4 })); backgroundElements.push(bgCrystal); } } // Background variation 2: Moss covered else if (backgroundType === 2) { for (var i = 0; i < 40; i++) { var bgMoss = gameContainer.addChild(LK.getAsset('bgMoss', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.45, rotation: Math.random() * Math.PI * 2 })); backgroundElements.push(bgMoss); } // Add some vines for (var i = 0; i < 15; i++) { var bgVines = gameContainer.addChild(LK.getAsset('bgVines', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.3, rotation: Math.random() * Math.PI * 2 })); backgroundElements.push(bgVines); } } // Background variation 3: Stalactite heavy else if (backgroundType === 3) { for (var i = 0; i < 25; i++) { var bgStalactite = gameContainer.addChild(LK.getAsset('bgStalactite', { x: Math.random() * 2048, y: Math.random() * 400, // Extended from top anchorX: 0.5, anchorY: 0, alpha: 0.4, rotation: Math.random() * 0.4 - 0.2, scaleY: 0.8 + Math.random() * 0.6 })); backgroundElements.push(bgStalactite); } } // Background variation 4: Pebble field else if (backgroundType === 4) { for (var i = 0; i < 35; i++) { var bgPebbles = gameContainer.addChild(LK.getAsset('bgPebbles', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.35, rotation: Math.random() * Math.PI * 2 })); backgroundElements.push(bgPebbles); } } // Background variation 5: Cracked terrain else if (backgroundType === 5) { for (var i = 0; i < 50; i++) { var bgCracks = gameContainer.addChild(LK.getAsset('bgCracks', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.25, rotation: Math.random() * Math.PI * 2, scaleX: 0.5 + Math.random() * 1.5 })); backgroundElements.push(bgCracks); } } // Background variation 6: Boulder field else if (backgroundType === 6) { for (var i = 0; i < 12; i++) { var bgBoulder = gameContainer.addChild(LK.getAsset('bgBoulder', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.3, rotation: Math.random() * Math.PI * 2 })); backgroundElements.push(bgBoulder); } } // Background variation 7: Mixed cave else if (backgroundType === 7) { // Mixed environment with moderate amounts of everything for (var i = 0; i < 8; i++) { var bgRock1 = gameContainer.addChild(LK.getAsset('bgRock1', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.3, rotation: Math.random() * Math.PI * 2 })); backgroundElements.push(bgRock1); } for (var i = 0; i < 10; i++) { var bgCrystal = gameContainer.addChild(LK.getAsset('bgCrystal', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.4, rotation: Math.random() * Math.PI * 2 })); backgroundElements.push(bgCrystal); } for (var i = 0; i < 15; i++) { var bgMoss = gameContainer.addChild(LK.getAsset('bgMoss', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.35, rotation: Math.random() * Math.PI * 2 })); backgroundElements.push(bgMoss); } } // Background variation 8: Vine jungle cave else if (backgroundType === 8) { for (var i = 0; i < 30; i++) { var bgVines = gameContainer.addChild(LK.getAsset('bgVines', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.4, rotation: Math.random() * Math.PI * 2, scaleX: 0.8 + Math.random() * 0.4 })); backgroundElements.push(bgVines); } for (var i = 0; i < 25; i++) { var bgMoss = gameContainer.addChild(LK.getAsset('bgMoss', { x: Math.random() * 2048, y: Math.random() * 2732, anchorX: 0.5, anchorY: 0.5, alpha: 0.3, rotation: Math.random() * Math.PI * 2 })); backgroundElements.push(bgMoss); } } // Send all background elements to back for (var i = 0; i < backgroundElements.length; i++) { gameContainer.setChildIndex(backgroundElements[i], 0); } } // Check if position is valid for movement function canMoveTo(x, y) { var mazeX = Math.floor((x - MAZE_OFFSET_X) / CELL_SIZE); var mazeY = Math.floor((y - MAZE_OFFSET_Y) / CELL_SIZE); if (mazeX < 0 || mazeX >= MAZE_WIDTH || mazeY < 0 || mazeY >= MAZE_HEIGHT) { return false; } return maze[mazeY][mazeX] === 0; } // Initialize level function initializeLevel() { // Clear existing game objects if (gameContainer) { gameContainer.destroy(); } enemies = []; ores = []; bullets = []; oreCollected = 0; // Add detailed background var background = game.addChild(LK.getAsset('background', { x: 0, y: 0, width: 2048, height: 2732 })); // Generate new maze generateMaze(); createMazeVisual(); // Create detailed background elements createDetailedBackground(); // Create player player = gameContainer.addChild(LK.getAsset('player', { anchorX: 0.5, anchorY: 0.5, x: MAZE_OFFSET_X + 1 * CELL_SIZE + CELL_SIZE / 2, y: MAZE_OFFSET_Y + 1 * CELL_SIZE + CELL_SIZE / 2 })); // Place game objects placeOre(); placeEnemies(); placeExit(); // Update UI updateUI(); } // Update UI function updateUI() { healthText.setText('Health: ' + playerHealth); oreText.setText('Ore: ' + oreCollected + '/' + oreTarget); levelText.setText('Level: ' + level); } // Shooting system var dragNode = null; var shootCooldown = 0; function shoot(targetX, targetY) { if (shootCooldown > 0) return; // Limit maximum bullets to prevent lag if (bullets.length >= 15) return; var dx = targetX - player.x; var dy = targetY - player.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 50) return; // Don't shoot if too close // Calculate base direction var baseDirectionX = dx / distance; var baseDirectionY = dy / distance; // Create three bullets with spread angles var spreadAngles = [-0.3, 0, 0.3]; // Left, center, right (in radians, about 17 degrees each side) for (var i = 0; i < spreadAngles.length; i++) { var angle = Math.atan2(baseDirectionY, baseDirectionX) + spreadAngles[i]; var bullet = gameContainer.addChild(new Bullet()); bullet.x = player.x; bullet.y = player.y; bullet.direction.x = Math.cos(angle); bullet.direction.y = Math.sin(angle); bullets.push(bullet); } shootCooldown = 15; // Quarter second cooldown LK.getSound('shoot').play(); } // Main game loop game.update = function () { if (shootCooldown > 0) shootCooldown--; // Handle hold movement if (isTracking && !isHolding) { var currentTime = Date.now(); var holdTime = currentTime - holdStartTime; // Check if we should start hold movement if (holdTime >= holdThreshold) { var dx = swipeEnd.x - swipeStart.x; var dy = swipeEnd.y - swipeStart.y; var swipeDistance = Math.sqrt(dx * dx + dy * dy); // Only start hold if not moving much (stationary hold) if (swipeDistance < swipeThreshold) { isHolding = true; lastHoldMoveTime = currentTime; // Determine hold direction based on player center to touch position var holdDx = swipeStart.x - player.x; var holdDy = swipeStart.y - player.y; var holdDistance = Math.sqrt(holdDx * holdDx + holdDy * holdDy); if (holdDistance > 30) { // Must be reasonable distance from center if (Math.abs(holdDx) > Math.abs(holdDy)) { // Horizontal hold movement holdDirection.x = holdDx > 0 ? 1 : -1; holdDirection.y = 0; } else { // Vertical hold movement holdDirection.x = 0; holdDirection.y = holdDy > 0 ? 1 : -1; } } } } } // Process hold movement if (isHolding && !isPlayerMoving) { var currentTime = Date.now(); if (currentTime - lastHoldMoveTime >= holdMoveInterval) { var moveX = holdDirection.x * playerSpeed; var moveY = holdDirection.y * playerSpeed; var newX = player.x + moveX; var newY = player.y + moveY; // Try to move in hold direction if (canMoveTo(newX, player.y) && holdDirection.x !== 0) { // Flip player sprite based on hold movement direction if (holdDirection.x > 0) { // Moving right - face right (normal) player.scaleX = 1; } else { // Moving left - face left (flipped) player.scaleX = -1; } isPlayerMoving = true; tween(player, { x: newX }, { duration: 120, easing: tween.easeOut, onFinish: function onFinish() { isPlayerMoving = false; } }); lastHoldMoveTime = currentTime; } else if (canMoveTo(player.x, newY) && holdDirection.y !== 0) { isPlayerMoving = true; tween(player, { y: newY }, { duration: 120, easing: tween.easeOut, onFinish: function onFinish() { isPlayerMoving = false; } }); lastHoldMoveTime = currentTime; } } } // Update bullets for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; // Check bullet lifetime if (bullet.lifetime >= bullet.maxLifetime) { bullet.destroy(); bullets.splice(i, 1); continue; } // Check wall collision if (!canMoveTo(bullet.x, bullet.y)) { bullet.destroy(); bullets.splice(i, 1); continue; } // Check enemy collision (optimized - only check every other frame for performance) var hitEnemy = false; if (LK.ticks % 2 === 0 || bullet.lifetime < 10) { // Always check new bullets for (var j = enemies.length - 1; j >= 0; j--) { if (bullet.intersects(enemies[j])) { enemies[j].health--; LK.getSound('enemyHit').play(); if (enemies[j].health <= 0) { // Create red circular explosion animation var dyingEnemy = enemies[j]; var explosionX = dyingEnemy.x; var explosionY = dyingEnemy.y; // Create red explosion circle var explosion = gameContainer.addChild(LK.getAsset('bullet', { anchorX: 0.5, anchorY: 0.5, x: explosionX, y: explosionY, scaleX: 0.1, scaleY: 0.1, tint: 0xFF0000 })); // Animate explosion circle expanding and fading tween(explosion, { scaleX: 8, scaleY: 8, alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { explosion.destroy(); } }); // Create enemy death animation with spinning tween(dyingEnemy, { tint: 0xFF0000, scaleX: 1.5, scaleY: 1.5, rotation: Math.PI * 2 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { // Second stage: Continue growing while fading to dark red and spinning faster tween(dyingEnemy, { tint: 0x800000, scaleX: 2.5, scaleY: 2.5, alpha: 0.7, rotation: Math.PI * 6 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { // Final stage: Fade out completely with final spin tween(dyingEnemy, { alpha: 0, scaleX: 3, scaleY: 3, rotation: Math.PI * 10 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { dyingEnemy.destroy(); } }); } }); } }); enemies.splice(j, 1); } bullet.destroy(); bullets.splice(i, 1); hitEnemy = true; break; } } } if (hitEnemy) continue; } // Check ore collection for (var i = ores.length - 1; i >= 0; i--) { if (player.intersects(ores[i])) { oreCollected++; LK.getSound('oreCollect').play(); ores[i].destroy(); ores.splice(i, 1); updateUI(); } } // Check exit condition if (exit && player.intersects(exit) && oreCollected >= oreTarget) { level++; oreTarget += 2; initializeLevel(); return; } // Check game over if (playerHealth <= 0) { LK.showGameOver(); return; } // Check win condition (survive 10 levels) if (level > 10) { LK.showYouWin(); return; } }; // Swipe and hold-based movement system var swipeStart = { x: 0, y: 0 }; var swipeEnd = { x: 0, y: 0 }; var isTracking = false; var playerSpeed = 80; // Full cell movement var swipeThreshold = 50; // Minimum distance for swipe detection var lastMoveTime = 0; var moveDelay = 200; // Delay between moves in milliseconds var isPlayerMoving = false; // Hold movement system var isHolding = false; var holdStartTime = 0; var holdThreshold = 300; // Time to distinguish between tap and hold (ms) var holdMoveInterval = 150; // Interval between moves while holding var lastHoldMoveTime = 0; var holdDirection = { x: 0, y: 0 }; game.down = function (x, y, obj) { swipeStart.x = x; swipeStart.y = y; isTracking = true; holdStartTime = Date.now(); isHolding = false; }; game.move = function (x, y, obj) { if (isTracking) { swipeEnd.x = x; swipeEnd.y = y; } }; game.up = function (x, y, obj) { if (!isTracking) return; isTracking = false; isHolding = false; // Stop hold movement var holdTime = Date.now() - holdStartTime; // Check if this was a hold operation if (holdTime >= holdThreshold) { // This was a hold, don't process as tap return; } // Check if this is a swipe for shooting var dx = swipeEnd.x - swipeStart.x; var dy = swipeEnd.y - swipeStart.y; var swipeDistance = Math.sqrt(dx * dx + dy * dy); // If swipe distance is significant, shoot towards swipe direction if (swipeDistance >= swipeThreshold) { // Calculate target position based on swipe direction var targetX = player.x + dx * 3; // Multiply for longer range var targetY = player.y + dy * 3; shoot(targetX, targetY); return; // Don't process as movement } // Tap only - move towards target position // Prevent too frequent moves and simultaneous movements var currentTime = Date.now(); if (currentTime - lastMoveTime < moveDelay || isPlayerMoving) return; // Calculate direction from player to tap position var tapDx = x - player.x; var tapDy = y - player.y; var distance = Math.sqrt(tapDx * tapDx + tapDy * tapDy); if (distance < 50) return; // Don't move if tapped too close to player // Determine movement direction (one cell at a time) var moveX = 0; var moveY = 0; if (Math.abs(tapDx) > Math.abs(tapDy)) { // Move horizontally towards tap moveX = tapDx > 0 ? playerSpeed : -playerSpeed; } else { // Move vertically towards tap moveY = tapDy > 0 ? playerSpeed : -playerSpeed; } // Calculate new position var newX = player.x + moveX; var newY = player.y + moveY; // Check if movement is valid and animate smoothly if (canMoveTo(newX, player.y) && Math.abs(moveX) > 0) { // Flip player sprite based on movement direction if (moveX > 0) { // Moving right - face right (normal) player.scaleX = 1; } else { // Moving left - face left (flipped) player.scaleX = -1; } isPlayerMoving = true; tween(player, { x: newX }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { isPlayerMoving = false; } }); lastMoveTime = currentTime; } else if (canMoveTo(player.x, newY) && Math.abs(moveY) > 0) { isPlayerMoving = true; tween(player, { y: newY }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { isPlayerMoving = false; } }); lastMoveTime = currentTime; } }; // Initialize the first level initializeLevel(); // Start background music LK.playMusic('1dwarftheme');
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 8;
self.direction = {
x: 0,
y: 0
};
self.lifetime = 0;
self.maxLifetime = 120; // 2 seconds at 60fps
self.update = function () {
self.x += self.direction.x * self.speed;
self.y += self.direction.y * self.speed;
self.lifetime++;
};
return self;
});
var Enemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xFFFFFF
});
self.speed = 1;
self.health = 2;
self.attackRange = 40;
self.attackCooldown = 0;
self.attackInterval = 60; // Attack every second at 60fps
self.patrolDirection = Math.random() > 0.5 ? 1 : -1; // Random initial direction
self.patrolAxis = Math.random() > 0.5 ? 'x' : 'y'; // Random patrol axis (horizontal or vertical)
self.patrolDistance = CELL_SIZE * 3; // Patrol 3 cells distance
self.startPosition = {
x: 0,
y: 0
}; // Will be set when enemy is placed
self.isMoving = false;
self.lastPlayerDistance = Infinity;
self.startPatrol = function () {
if (self.isMoving) return;
self.isMoving = true;
var targetX = self.x;
var targetY = self.y;
// Calculate target position based on patrol axis
if (self.patrolAxis === 'x') {
targetX = self.startPosition.x + self.patrolDirection * self.patrolDistance;
} else {
targetY = self.startPosition.y + self.patrolDirection * self.patrolDistance;
}
// Check if target position is valid
if (canMoveTo(targetX, targetY)) {
// Store the original position for safe restoration
var originalX = self.x;
var originalY = self.y;
// Move to target position using tween
tween(self, {
x: targetX,
y: targetY
}, {
duration: 2000,
easing: tween.linear,
onFinish: function onFinish() {
// Ensure enemy is at a valid position after movement
if (!canMoveTo(self.x, self.y)) {
// If somehow ended up in invalid position, restore to original
self.x = originalX;
self.y = originalY;
}
self.isMoving = false;
self.patrolDirection *= -1; // Reverse direction
}
});
} else {
// If can't move in current direction, reverse immediately
self.isMoving = false;
self.patrolDirection *= -1;
}
};
self.update = function () {
// Add bouncing animation when enemy is not moving (idle/waiting state)
if (!self.isMoving) {
if (!self.bounceTimer) self.bounceTimer = 0;
self.bounceTimer++;
// Create a subtle bouncing effect
var bounceOffset = Math.sin(self.bounceTimer * 0.15) * 2;
// Use current position as base instead of startPosition to avoid conflicts
var baseY = self.startPosition.y;
self.y = baseY + bounceOffset;
} else {
// Reset bounce timer when moving to ensure smooth transitions
self.bounceTimer = 0;
}
var dx = player.x - self.x;
var dy = player.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var detectionRange = CELL_SIZE * 4; // Enemy can see player from 4 cells away
// Check if player is within detection range and line of sight (optimized - check every 10 frames)
var canSeePlayer = false;
if (distance < detectionRange) {
// Only do expensive line of sight calculation every 10 frames
if (!self.losCheckTimer) self.losCheckTimer = 0;
self.losCheckTimer++;
if (self.losCheckTimer >= 10 || !self.hasOwnProperty('lastCanSeePlayer')) {
self.losCheckTimer = 0;
// Simple line of sight check - check if there are walls between enemy and player
var steps = Math.ceil(distance / 40); // Reduced precision from 20 to 40
var stepX = dx / steps;
var stepY = dy / steps;
self.lastCanSeePlayer = true;
for (var step = 1; step < steps; step++) {
var checkX = self.x + stepX * step;
var checkY = self.y + stepY * step;
if (!canMoveTo(checkX, checkY)) {
self.lastCanSeePlayer = false;
break;
}
}
}
canSeePlayer = self.lastCanSeePlayer;
} else {
canSeePlayer = self.lastCanSeePlayer;
}
// If can see player and not in attack range, move towards player
if (canSeePlayer && distance > self.attackRange) {
if (!self.isMoving) {
self.isMoving = true;
// Stop current patrol movement
tween.stop(self);
// Calculate next position towards player (one cell at a time)
var moveDistance = CELL_SIZE;
var normalizedDx = dx / distance;
var normalizedDy = dy / distance;
var targetX = self.x + normalizedDx * moveDistance;
var targetY = self.y + normalizedDy * moveDistance;
// Try to move towards player, prioritize the axis with larger distance
if (Math.abs(dx) > Math.abs(dy)) {
// Try horizontal movement first
var horizontalTarget = self.x + (dx > 0 ? moveDistance : -moveDistance);
if (canMoveTo(horizontalTarget, self.y)) {
targetX = horizontalTarget;
targetY = self.y;
} else if (canMoveTo(self.x, self.y + (dy > 0 ? moveDistance : -moveDistance))) {
// If horizontal blocked, try vertical
targetX = self.x;
targetY = self.y + (dy > 0 ? moveDistance : -moveDistance);
} else {
// Can't move towards player
targetX = self.x;
targetY = self.y;
}
} else {
// Try vertical movement first
var verticalTarget = self.y + (dy > 0 ? moveDistance : -moveDistance);
if (canMoveTo(self.x, verticalTarget)) {
targetX = self.x;
targetY = verticalTarget;
} else if (canMoveTo(self.x + (dx > 0 ? moveDistance : -moveDistance), self.y)) {
// If vertical blocked, try horizontal
targetX = self.x + (dx > 0 ? moveDistance : -moveDistance);
targetY = self.y;
} else {
// Can't move towards player
targetX = self.x;
targetY = self.y;
}
}
// Move towards calculated target
if (targetX !== self.x || targetY !== self.y) {
// Store current position for safety
var safeX = self.x;
var safeY = self.y;
tween(self, {
x: targetX,
y: targetY
}, {
duration: 1000,
easing: tween.linear,
onFinish: function onFinish() {
// Validate final position
if (!canMoveTo(self.x, self.y)) {
self.x = safeX;
self.y = safeY;
}
self.isMoving = false;
}
});
} else {
self.isMoving = false;
}
}
} else if (!canSeePlayer) {
// Start patrol movement if not already moving and can't see player
if (!self.isMoving) {
self.startPatrol();
}
}
// Attack player if in range
if (distance < self.attackRange) {
// Stop patrolling when in attack range
if (!self.lastPlayerDistance || self.lastPlayerDistance >= self.attackRange) {
tween.stop(self);
self.isMoving = false;
}
// Continuously attack while in range
if (self.attackCooldown <= 0) {
// Perform melee attack
playerHealth--;
LK.effects.flashObject(player, 0xFF0000, 500);
// Visual attack feedback - enemy briefly grows and turns red
tween(self, {
tint: 0xFF0000,
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Return to normal appearance
tween(self, {
tint: 0xFFFFFF,
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
// Reset attack cooldown
self.attackCooldown = self.attackInterval;
}
}
// Update attack cooldown
if (self.attackCooldown > 0) {
self.attackCooldown--;
}
self.lastPlayerDistance = distance;
};
return self;
});
var Ore = Container.expand(function () {
var self = Container.call(this);
var oreGraphics = self.attachAsset('ore', {
anchorX: 0.5,
anchorY: 0.5
});
self.value = 1;
self.bobTimer = 0;
self.baseY = self.y;
self.update = function () {
self.bobTimer++;
self.y = self.baseY + Math.sin(self.bobTimer * 0.1) * 3;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2C1810
});
/****
* Game Code
****/
// Game constants
var MAZE_WIDTH = 13;
var MAZE_HEIGHT = 17;
var CELL_SIZE = 80;
var MAZE_OFFSET_X = (2048 - MAZE_WIDTH * CELL_SIZE) / 2;
var MAZE_OFFSET_Y = 100;
// Game variables
var maze = [];
var player;
var enemies = [];
var ores = [];
var bullets = [];
var playerHealth = 3;
var oreCollected = 0;
var oreTarget = 10;
var level = 1;
var exit;
var gameContainer;
// UI elements
var healthText = new Text2('Health: 3', {
size: 60,
fill: 0xFF4444
});
healthText.anchor.set(0, 0);
LK.gui.topLeft.addChild(healthText);
var oreText = new Text2('Ore: 0/10', {
size: 60,
fill: 0xFFD700
});
oreText.anchor.set(0.5, 0);
LK.gui.top.addChild(oreText);
var levelText = new Text2('Level: 1', {
size: 60,
fill: 0xFFFFFF
});
levelText.anchor.set(1, 0);
LK.gui.topRight.addChild(levelText);
// Initialize maze array
function initializeMaze() {
maze = [];
for (var y = 0; y < MAZE_HEIGHT; y++) {
maze[y] = [];
for (var x = 0; x < MAZE_WIDTH; x++) {
maze[y][x] = 1; // 1 = wall, 0 = floor
}
}
}
// Simple maze generation using recursive backtracking
function generateMaze() {
initializeMaze();
var stack = [];
var startX = 1;
var startY = 1;
maze[startY][startX] = 0;
stack.push({
x: startX,
y: startY
});
while (stack.length > 0) {
var current = stack[stack.length - 1];
var neighbors = [];
// Check all four directions
var directions = [{
x: 0,
y: -2
}, {
x: 2,
y: 0
}, {
x: 0,
y: 2
}, {
x: -2,
y: 0
}];
for (var i = 0; i < directions.length; i++) {
var nx = current.x + directions[i].x;
var ny = current.y + directions[i].y;
if (nx > 0 && nx < MAZE_WIDTH - 1 && ny > 0 && ny < MAZE_HEIGHT - 1 && maze[ny][nx] === 1) {
neighbors.push({
x: nx,
y: ny,
dir: directions[i]
});
}
}
if (neighbors.length > 0) {
var chosen = neighbors[Math.floor(Math.random() * neighbors.length)];
// Remove wall between current and chosen
maze[current.y + chosen.dir.y / 2][current.x + chosen.dir.x / 2] = 0;
maze[chosen.y][chosen.x] = 0;
stack.push(chosen);
} else {
stack.pop();
}
}
}
// Create visual maze
function createMazeVisual() {
gameContainer = game.addChild(new Container());
for (var y = 0; y < MAZE_HEIGHT; y++) {
for (var x = 0; x < MAZE_WIDTH; x++) {
var cellX = MAZE_OFFSET_X + x * CELL_SIZE;
var cellY = MAZE_OFFSET_Y + y * CELL_SIZE;
if (maze[y][x] === 1) {
// Wall
var wall = LK.getAsset('wall', {
x: cellX,
y: cellY
});
gameContainer.addChild(wall);
} else {
// Floor
var floor = LK.getAsset('floor', {
x: cellX,
y: cellY
});
gameContainer.addChild(floor);
}
}
}
}
// Place ore in maze
function placeOre() {
var oreCount = oreTarget + Math.floor(level / 2);
var placed = 0;
while (placed < oreCount) {
var x = Math.floor(Math.random() * MAZE_WIDTH);
var y = Math.floor(Math.random() * MAZE_HEIGHT);
if (maze[y][x] === 0 && (x !== 1 || y !== 1)) {
var ore = gameContainer.addChild(new Ore());
ore.x = MAZE_OFFSET_X + x * CELL_SIZE + CELL_SIZE / 2;
ore.y = MAZE_OFFSET_Y + y * CELL_SIZE + CELL_SIZE / 2;
ore.baseY = ore.y;
ores.push(ore);
placed++;
}
}
}
// Place enemies in maze
function placeEnemies() {
var enemyCount = 3 + level;
var placed = 0;
while (placed < enemyCount) {
var x = Math.floor(Math.random() * MAZE_WIDTH);
var y = Math.floor(Math.random() * MAZE_HEIGHT);
if (maze[y][x] === 0 && (x !== 1 || y !== 1) && Math.sqrt((x - 1) * (x - 1) + (y - 1) * (y - 1)) > 5) {
var enemy = gameContainer.addChild(new Enemy());
enemy.x = MAZE_OFFSET_X + x * CELL_SIZE + CELL_SIZE / 2;
enemy.y = MAZE_OFFSET_Y + y * CELL_SIZE + CELL_SIZE / 2;
enemy.startPosition.x = enemy.x;
enemy.startPosition.y = enemy.y;
enemies.push(enemy);
placed++;
}
}
}
// Place exit
function placeExit() {
// Find a position far from start
var placed = false;
while (!placed) {
var x = Math.floor(Math.random() * MAZE_WIDTH);
var y = Math.floor(Math.random() * MAZE_HEIGHT);
if (maze[y][x] === 0 && Math.sqrt((x - 1) * (x - 1) + (y - 1) * (y - 1)) > 10) {
exit = gameContainer.addChild(LK.getAsset('exit', {
x: MAZE_OFFSET_X + x * CELL_SIZE,
y: MAZE_OFFSET_Y + y * CELL_SIZE
}));
placed = true;
}
}
}
// Create detailed background with various environmental elements
function createDetailedBackground() {
if (!gameContainer) {
console.log("gameContainer not available for background creation");
return;
}
var backgroundElements = [];
// Select one of 9 background variations randomly
var backgroundType = Math.floor(Math.random() * 9);
// Background variation 0: Rock dominant
if (backgroundType === 0) {
// Add many background rocks
for (var i = 0; i < 15; i++) {
var bgRock1 = gameContainer.addChild(LK.getAsset('bgRock1', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.4,
rotation: Math.random() * Math.PI * 2
}));
backgroundElements.push(bgRock1);
}
for (var i = 0; i < 20; i++) {
var bgRock2 = gameContainer.addChild(LK.getAsset('bgRock2', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3,
rotation: Math.random() * Math.PI * 2
}));
backgroundElements.push(bgRock2);
}
}
// Background variation 1: Crystal cave
else if (backgroundType === 1) {
for (var i = 0; i < 30; i++) {
var bgCrystal = gameContainer.addChild(LK.getAsset('bgCrystal', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
rotation: Math.random() * Math.PI * 2,
scaleY: 0.8 + Math.random() * 0.4
}));
backgroundElements.push(bgCrystal);
}
}
// Background variation 2: Moss covered
else if (backgroundType === 2) {
for (var i = 0; i < 40; i++) {
var bgMoss = gameContainer.addChild(LK.getAsset('bgMoss', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.45,
rotation: Math.random() * Math.PI * 2
}));
backgroundElements.push(bgMoss);
}
// Add some vines
for (var i = 0; i < 15; i++) {
var bgVines = gameContainer.addChild(LK.getAsset('bgVines', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3,
rotation: Math.random() * Math.PI * 2
}));
backgroundElements.push(bgVines);
}
}
// Background variation 3: Stalactite heavy
else if (backgroundType === 3) {
for (var i = 0; i < 25; i++) {
var bgStalactite = gameContainer.addChild(LK.getAsset('bgStalactite', {
x: Math.random() * 2048,
y: Math.random() * 400,
// Extended from top
anchorX: 0.5,
anchorY: 0,
alpha: 0.4,
rotation: Math.random() * 0.4 - 0.2,
scaleY: 0.8 + Math.random() * 0.6
}));
backgroundElements.push(bgStalactite);
}
}
// Background variation 4: Pebble field
else if (backgroundType === 4) {
for (var i = 0; i < 35; i++) {
var bgPebbles = gameContainer.addChild(LK.getAsset('bgPebbles', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.35,
rotation: Math.random() * Math.PI * 2
}));
backgroundElements.push(bgPebbles);
}
}
// Background variation 5: Cracked terrain
else if (backgroundType === 5) {
for (var i = 0; i < 50; i++) {
var bgCracks = gameContainer.addChild(LK.getAsset('bgCracks', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.25,
rotation: Math.random() * Math.PI * 2,
scaleX: 0.5 + Math.random() * 1.5
}));
backgroundElements.push(bgCracks);
}
}
// Background variation 6: Boulder field
else if (backgroundType === 6) {
for (var i = 0; i < 12; i++) {
var bgBoulder = gameContainer.addChild(LK.getAsset('bgBoulder', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3,
rotation: Math.random() * Math.PI * 2
}));
backgroundElements.push(bgBoulder);
}
}
// Background variation 7: Mixed cave
else if (backgroundType === 7) {
// Mixed environment with moderate amounts of everything
for (var i = 0; i < 8; i++) {
var bgRock1 = gameContainer.addChild(LK.getAsset('bgRock1', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3,
rotation: Math.random() * Math.PI * 2
}));
backgroundElements.push(bgRock1);
}
for (var i = 0; i < 10; i++) {
var bgCrystal = gameContainer.addChild(LK.getAsset('bgCrystal', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.4,
rotation: Math.random() * Math.PI * 2
}));
backgroundElements.push(bgCrystal);
}
for (var i = 0; i < 15; i++) {
var bgMoss = gameContainer.addChild(LK.getAsset('bgMoss', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.35,
rotation: Math.random() * Math.PI * 2
}));
backgroundElements.push(bgMoss);
}
}
// Background variation 8: Vine jungle cave
else if (backgroundType === 8) {
for (var i = 0; i < 30; i++) {
var bgVines = gameContainer.addChild(LK.getAsset('bgVines', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.4,
rotation: Math.random() * Math.PI * 2,
scaleX: 0.8 + Math.random() * 0.4
}));
backgroundElements.push(bgVines);
}
for (var i = 0; i < 25; i++) {
var bgMoss = gameContainer.addChild(LK.getAsset('bgMoss', {
x: Math.random() * 2048,
y: Math.random() * 2732,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3,
rotation: Math.random() * Math.PI * 2
}));
backgroundElements.push(bgMoss);
}
}
// Send all background elements to back
for (var i = 0; i < backgroundElements.length; i++) {
gameContainer.setChildIndex(backgroundElements[i], 0);
}
}
// Check if position is valid for movement
function canMoveTo(x, y) {
var mazeX = Math.floor((x - MAZE_OFFSET_X) / CELL_SIZE);
var mazeY = Math.floor((y - MAZE_OFFSET_Y) / CELL_SIZE);
if (mazeX < 0 || mazeX >= MAZE_WIDTH || mazeY < 0 || mazeY >= MAZE_HEIGHT) {
return false;
}
return maze[mazeY][mazeX] === 0;
}
// Initialize level
function initializeLevel() {
// Clear existing game objects
if (gameContainer) {
gameContainer.destroy();
}
enemies = [];
ores = [];
bullets = [];
oreCollected = 0;
// Add detailed background
var background = game.addChild(LK.getAsset('background', {
x: 0,
y: 0,
width: 2048,
height: 2732
}));
// Generate new maze
generateMaze();
createMazeVisual();
// Create detailed background elements
createDetailedBackground();
// Create player
player = gameContainer.addChild(LK.getAsset('player', {
anchorX: 0.5,
anchorY: 0.5,
x: MAZE_OFFSET_X + 1 * CELL_SIZE + CELL_SIZE / 2,
y: MAZE_OFFSET_Y + 1 * CELL_SIZE + CELL_SIZE / 2
}));
// Place game objects
placeOre();
placeEnemies();
placeExit();
// Update UI
updateUI();
}
// Update UI
function updateUI() {
healthText.setText('Health: ' + playerHealth);
oreText.setText('Ore: ' + oreCollected + '/' + oreTarget);
levelText.setText('Level: ' + level);
}
// Shooting system
var dragNode = null;
var shootCooldown = 0;
function shoot(targetX, targetY) {
if (shootCooldown > 0) return;
// Limit maximum bullets to prevent lag
if (bullets.length >= 15) return;
var dx = targetX - player.x;
var dy = targetY - player.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 50) return; // Don't shoot if too close
// Calculate base direction
var baseDirectionX = dx / distance;
var baseDirectionY = dy / distance;
// Create three bullets with spread angles
var spreadAngles = [-0.3, 0, 0.3]; // Left, center, right (in radians, about 17 degrees each side)
for (var i = 0; i < spreadAngles.length; i++) {
var angle = Math.atan2(baseDirectionY, baseDirectionX) + spreadAngles[i];
var bullet = gameContainer.addChild(new Bullet());
bullet.x = player.x;
bullet.y = player.y;
bullet.direction.x = Math.cos(angle);
bullet.direction.y = Math.sin(angle);
bullets.push(bullet);
}
shootCooldown = 15; // Quarter second cooldown
LK.getSound('shoot').play();
}
// Main game loop
game.update = function () {
if (shootCooldown > 0) shootCooldown--;
// Handle hold movement
if (isTracking && !isHolding) {
var currentTime = Date.now();
var holdTime = currentTime - holdStartTime;
// Check if we should start hold movement
if (holdTime >= holdThreshold) {
var dx = swipeEnd.x - swipeStart.x;
var dy = swipeEnd.y - swipeStart.y;
var swipeDistance = Math.sqrt(dx * dx + dy * dy);
// Only start hold if not moving much (stationary hold)
if (swipeDistance < swipeThreshold) {
isHolding = true;
lastHoldMoveTime = currentTime;
// Determine hold direction based on player center to touch position
var holdDx = swipeStart.x - player.x;
var holdDy = swipeStart.y - player.y;
var holdDistance = Math.sqrt(holdDx * holdDx + holdDy * holdDy);
if (holdDistance > 30) {
// Must be reasonable distance from center
if (Math.abs(holdDx) > Math.abs(holdDy)) {
// Horizontal hold movement
holdDirection.x = holdDx > 0 ? 1 : -1;
holdDirection.y = 0;
} else {
// Vertical hold movement
holdDirection.x = 0;
holdDirection.y = holdDy > 0 ? 1 : -1;
}
}
}
}
}
// Process hold movement
if (isHolding && !isPlayerMoving) {
var currentTime = Date.now();
if (currentTime - lastHoldMoveTime >= holdMoveInterval) {
var moveX = holdDirection.x * playerSpeed;
var moveY = holdDirection.y * playerSpeed;
var newX = player.x + moveX;
var newY = player.y + moveY;
// Try to move in hold direction
if (canMoveTo(newX, player.y) && holdDirection.x !== 0) {
// Flip player sprite based on hold movement direction
if (holdDirection.x > 0) {
// Moving right - face right (normal)
player.scaleX = 1;
} else {
// Moving left - face left (flipped)
player.scaleX = -1;
}
isPlayerMoving = true;
tween(player, {
x: newX
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
isPlayerMoving = false;
}
});
lastHoldMoveTime = currentTime;
} else if (canMoveTo(player.x, newY) && holdDirection.y !== 0) {
isPlayerMoving = true;
tween(player, {
y: newY
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
isPlayerMoving = false;
}
});
lastHoldMoveTime = currentTime;
}
}
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
// Check bullet lifetime
if (bullet.lifetime >= bullet.maxLifetime) {
bullet.destroy();
bullets.splice(i, 1);
continue;
}
// Check wall collision
if (!canMoveTo(bullet.x, bullet.y)) {
bullet.destroy();
bullets.splice(i, 1);
continue;
}
// Check enemy collision (optimized - only check every other frame for performance)
var hitEnemy = false;
if (LK.ticks % 2 === 0 || bullet.lifetime < 10) {
// Always check new bullets
for (var j = enemies.length - 1; j >= 0; j--) {
if (bullet.intersects(enemies[j])) {
enemies[j].health--;
LK.getSound('enemyHit').play();
if (enemies[j].health <= 0) {
// Create red circular explosion animation
var dyingEnemy = enemies[j];
var explosionX = dyingEnemy.x;
var explosionY = dyingEnemy.y;
// Create red explosion circle
var explosion = gameContainer.addChild(LK.getAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
y: explosionY,
scaleX: 0.1,
scaleY: 0.1,
tint: 0xFF0000
}));
// Animate explosion circle expanding and fading
tween(explosion, {
scaleX: 8,
scaleY: 8,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
// Create enemy death animation with spinning
tween(dyingEnemy, {
tint: 0xFF0000,
scaleX: 1.5,
scaleY: 1.5,
rotation: Math.PI * 2
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
// Second stage: Continue growing while fading to dark red and spinning faster
tween(dyingEnemy, {
tint: 0x800000,
scaleX: 2.5,
scaleY: 2.5,
alpha: 0.7,
rotation: Math.PI * 6
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Final stage: Fade out completely with final spin
tween(dyingEnemy, {
alpha: 0,
scaleX: 3,
scaleY: 3,
rotation: Math.PI * 10
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
dyingEnemy.destroy();
}
});
}
});
}
});
enemies.splice(j, 1);
}
bullet.destroy();
bullets.splice(i, 1);
hitEnemy = true;
break;
}
}
}
if (hitEnemy) continue;
}
// Check ore collection
for (var i = ores.length - 1; i >= 0; i--) {
if (player.intersects(ores[i])) {
oreCollected++;
LK.getSound('oreCollect').play();
ores[i].destroy();
ores.splice(i, 1);
updateUI();
}
}
// Check exit condition
if (exit && player.intersects(exit) && oreCollected >= oreTarget) {
level++;
oreTarget += 2;
initializeLevel();
return;
}
// Check game over
if (playerHealth <= 0) {
LK.showGameOver();
return;
}
// Check win condition (survive 10 levels)
if (level > 10) {
LK.showYouWin();
return;
}
};
// Swipe and hold-based movement system
var swipeStart = {
x: 0,
y: 0
};
var swipeEnd = {
x: 0,
y: 0
};
var isTracking = false;
var playerSpeed = 80; // Full cell movement
var swipeThreshold = 50; // Minimum distance for swipe detection
var lastMoveTime = 0;
var moveDelay = 200; // Delay between moves in milliseconds
var isPlayerMoving = false;
// Hold movement system
var isHolding = false;
var holdStartTime = 0;
var holdThreshold = 300; // Time to distinguish between tap and hold (ms)
var holdMoveInterval = 150; // Interval between moves while holding
var lastHoldMoveTime = 0;
var holdDirection = {
x: 0,
y: 0
};
game.down = function (x, y, obj) {
swipeStart.x = x;
swipeStart.y = y;
isTracking = true;
holdStartTime = Date.now();
isHolding = false;
};
game.move = function (x, y, obj) {
if (isTracking) {
swipeEnd.x = x;
swipeEnd.y = y;
}
};
game.up = function (x, y, obj) {
if (!isTracking) return;
isTracking = false;
isHolding = false; // Stop hold movement
var holdTime = Date.now() - holdStartTime;
// Check if this was a hold operation
if (holdTime >= holdThreshold) {
// This was a hold, don't process as tap
return;
}
// Check if this is a swipe for shooting
var dx = swipeEnd.x - swipeStart.x;
var dy = swipeEnd.y - swipeStart.y;
var swipeDistance = Math.sqrt(dx * dx + dy * dy);
// If swipe distance is significant, shoot towards swipe direction
if (swipeDistance >= swipeThreshold) {
// Calculate target position based on swipe direction
var targetX = player.x + dx * 3; // Multiply for longer range
var targetY = player.y + dy * 3;
shoot(targetX, targetY);
return; // Don't process as movement
}
// Tap only - move towards target position
// Prevent too frequent moves and simultaneous movements
var currentTime = Date.now();
if (currentTime - lastMoveTime < moveDelay || isPlayerMoving) return;
// Calculate direction from player to tap position
var tapDx = x - player.x;
var tapDy = y - player.y;
var distance = Math.sqrt(tapDx * tapDx + tapDy * tapDy);
if (distance < 50) return; // Don't move if tapped too close to player
// Determine movement direction (one cell at a time)
var moveX = 0;
var moveY = 0;
if (Math.abs(tapDx) > Math.abs(tapDy)) {
// Move horizontally towards tap
moveX = tapDx > 0 ? playerSpeed : -playerSpeed;
} else {
// Move vertically towards tap
moveY = tapDy > 0 ? playerSpeed : -playerSpeed;
}
// Calculate new position
var newX = player.x + moveX;
var newY = player.y + moveY;
// Check if movement is valid and animate smoothly
if (canMoveTo(newX, player.y) && Math.abs(moveX) > 0) {
// Flip player sprite based on movement direction
if (moveX > 0) {
// Moving right - face right (normal)
player.scaleX = 1;
} else {
// Moving left - face left (flipped)
player.scaleX = -1;
}
isPlayerMoving = true;
tween(player, {
x: newX
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
isPlayerMoving = false;
}
});
lastMoveTime = currentTime;
} else if (canMoveTo(player.x, newY) && Math.abs(moveY) > 0) {
isPlayerMoving = true;
tween(player, {
y: newY
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
isPlayerMoving = false;
}
});
lastMoveTime = currentTime;
}
};
// Initialize the first level
initializeLevel();
// Start background music
LK.playMusic('1dwarftheme');
2d sprites old dwarf hold shootgun. In-Game asset. 2d. High contrast. No shadows
2d chibi green evil underground fat ork. In-Game asset. 2d. High contrast. No shadows
2d golds ors brigh stone. In-Game asset. 2d. High contrast. No shadows
2d cave tunnel corridor. In-Game asset. 2d. High contrast. No shadows