/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var BoostButton = Container.expand(function () { var self = Container.call(this); var buttonSprite = self.attachAsset('attackButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.5, scaleY: 2.5 }); // Change tint to blue to differentiate from attack button buttonSprite.tint = 0x3366ff; // Add text to indicate boost function var boostText = new Text2('BOOST', { size: 40, fill: 0xFFFFFF }); boostText.anchor.set(0.5, 0.5); self.addChild(boostText); self.pressed = false; self.active = false; self.cooldown = false; self.down = function (x, y, obj) { if (self.cooldown) return; self.pressed = true; buttonSprite.alpha = 0.7; self.active = true; // Apply boost effect PLAYER_MOVE_SPEED *= 3; }; self.up = function (x, y, obj) { self.pressed = false; if (self.active) { // Reset speed to normal PLAYER_MOVE_SPEED = PLAYER_MOVE_SPEED / 3; self.active = false; // Set cooldown self.cooldown = true; buttonSprite.alpha = 0.3; // Reset cooldown after 5 seconds LK.setTimeout(function () { self.cooldown = false; buttonSprite.alpha = 1; }, 5000); } }; return self; }); var ControlButton = Container.expand(function (direction) { var self = Container.call(this); var buttonSprite = self.attachAsset('controlButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.5, scaleY: 2.5 }); var arrowSprite = self.attachAsset('buttonArrow', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.0, scaleY: 2.0 }); // Set arrow direction based on button type if (direction === 'up') { arrowSprite.rotation = 0; } else if (direction === 'right') { arrowSprite.rotation = Math.PI / 2; } else if (direction === 'down') { arrowSprite.rotation = Math.PI; } else if (direction === 'left') { arrowSprite.rotation = Math.PI * 1.5; } else if (direction === 'attack') { arrowSprite.visible = false; buttonSprite = self.attachAsset('attackButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 3.5, scaleY: 3.5 }); } self.direction = direction; self.pressed = false; self.updateCooldown = function (ratio) { if (self.direction === 'attack') { // Update the button alpha based on cooldown ratio (0 to 1) buttonSprite.alpha = 0.3 + ratio * 0.7; } }; self.down = function (x, y, obj) { self.pressed = true; buttonSprite.alpha = 0.7; if (self.direction === 'attack' && canAttack) { // Trigger attack immediately when the attack button is pressed attackAction(); } }; self.up = function (x, y, obj) { self.pressed = false; if (self.direction === 'attack') { if (!canAttack) { buttonSprite.alpha = 0.3; // Show as disabled } else { buttonSprite.alpha = 1; } } else { buttonSprite.alpha = 1; } }; return self; }); var Gate = Container.expand(function () { var self = Container.call(this); // Create gate visual using existing wall asset but with a different color var gateSprite = self.attachAsset('wall', { anchorX: 0.5, anchorY: 0.5 }); // Make the gate distinct with a green tint gateSprite.tint = 0x00FF00; self.mapX = 0; self.mapY = 0; // Pulse animation to make gate more visible var _animateGate = function animateGate() { tween(gateSprite, { alpha: 0.7, scaleX: 1.1, scaleY: 1.1 }, { duration: 1000, onFinish: function onFinish() { tween(gateSprite, { alpha: 1, scaleX: 1.0, scaleY: 1.0 }, { duration: 1000, onFinish: _animateGate }); } }); }; // Start the pulsing animation _animateGate(); return self; }); var MapCell = Container.expand(function () { var self = Container.call(this); self.type = 0; // 0 = floor, 1 = wall self.monster = null; self.treasure = null; self.gate = null; self.setType = function (type) { self.type = type; self.updateVisual(); }; self.updateVisual = function () { self.removeChildren(); if (self.type === 1) { self.attachAsset('mapWall', { anchorX: 0, anchorY: 0 }); } else { self.attachAsset('mapFloor', { anchorX: 0, anchorY: 0 }); } }; self.addMonster = function () { if (self.type === 0 && !self.monster && !self.treasure) { self.monster = true; return true; } return false; }; self.addTreasure = function () { if (self.type === 0 && !self.monster && !self.treasure) { self.treasure = true; return true; } return false; }; self.removeMonster = function () { self.monster = null; }; self.removeTreasure = function () { self.treasure = null; }; self.addGate = function () { if (self.type === 0 && !self.monster && !self.treasure && !self.gate) { self.gate = true; return true; } return false; }; self.removeGate = function () { self.gate = null; }; return self; }); var Monster = Container.expand(function () { var self = Container.call(this); var monsterSprite = self.attachAsset('monster', { anchorX: 0.5, anchorY: 0.5 }); self.mapX = 0; self.mapY = 0; self.health = 3; self.takeDamage = function () { self.health -= 1; LK.getSound('hit').play(); // Visual feedback for hit tween(monsterSprite, { alpha: 0.2 }, { duration: 100, onFinish: function onFinish() { tween(monsterSprite, { alpha: 1 }, { duration: 100 }); } }); return self.health <= 0; }; return self; }); var Projectile = Container.expand(function () { var self = Container.call(this); var projectileSprite = self.attachAsset('projectile', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.5, scaleY: 0.5 }); self.speed = 10; // Speed of horizontal movement self.startX = 0; // Starting X position on screen self.targetX = 0; // How far to move self.visible = false; // Start invisible self.active = false; // Projectile state self.isClone = false; // Track if this is a mirrored clone // Method to flip the projectile sprite horizontally self.setMirrored = function (mirrored) { self.isClone = mirrored; if (mirrored) { // Flip the sprite horizontally by setting negative scale projectileSprite.scaleX = -0.5; } else { projectileSprite.scaleX = 0.5; } }; self.update = function (deltaTime) { if (!self.active) { return false; } // Move projectile based on speed direction self.x -= self.speed; // Create pulsating effect for better visibility if (LK.ticks % 20 === 0) { tween(projectileSprite, { scaleX: self.isClone ? -0.6 : 0.6, scaleY: 0.6 }, { duration: 200, onFinish: function onFinish() { tween(projectileSprite, { scaleX: self.isClone ? -0.5 : 0.5, scaleY: 0.5 }, { duration: 200 }); } }); } // Return true if projectile has gone off either side of screen if (self.speed > 0) { // For projectiles moving left to right (clones), check right boundary return self.x > 2048 + 100; } else { // For normal projectiles moving right to left, check left boundary return self.x < -100; } }; self.fire = function (screenX, screenY) { // Position at right side of screen self.x = 2048 + 100; // Start off-screen to the right self.y = screenY || 2732 - 400; // Position at bottom of screen instead of middle self.visible = true; self.active = true; }; return self; }); var RaycastStrip = Container.expand(function () { var self = Container.call(this); var wall = self.attachAsset('wall', { anchorX: 0, anchorY: 0 }); var ceiling = self.attachAsset('ceiling', { anchorX: 0, anchorY: 0 }); var floor = self.attachAsset('floor', { anchorX: 0, anchorY: 0 }); self.updateStrip = function (stripWidth, wallHeight, stripIdx, wallType, distance) { // Wall setup wall.width = stripWidth; wall.height = wallHeight; wall.y = (2732 - wallHeight) / 2; // Ceiling setup ceiling.width = stripWidth; ceiling.height = wall.y; ceiling.y = 0; // Floor setup floor.width = stripWidth; floor.height = ceiling.height; floor.y = wall.y + wallHeight; // Adjust positions self.x = stripIdx * stripWidth; // Add distance shading effect var shade = Math.max(0.3, 1 - distance / 10); wall.alpha = shade; ceiling.alpha = shade * 0.7; floor.alpha = shade * 0.8; }; return self; }); var Treasure = Container.expand(function () { var self = Container.call(this); var treasureSprite = self.attachAsset('treasure', { anchorX: 0.5, anchorY: 0.5 }); self.mapX = 0; self.mapY = 0; self.value = 1; // Animate the treasure to make it more appealing var _animateTreasure = function animateTreasure() { // Animation removed to stop spinning }; // No longer calling animation return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x111111 }); /**** * Game Code ****/ // Game constants var MAP_SIZE = 16; var CELL_SIZE = 20; var MINI_MAP_SCALE = 1; var STRIP_WIDTH = 32; // Increased strip width to reduce ray count var NUM_RAYS = Math.ceil(2048 / STRIP_WIDTH); var FOV = Math.PI / 3; // 60 degrees field of view var HALF_FOV = FOV / 2; var PLAYER_MOVE_SPEED = 0.002; // Reduced from 0.005 to make movement slower var PLAYER_TURN_SPEED = 0.002; // Reduced from 0.005 to make turning slower var WALL_HEIGHT_FACTOR = 600; var MAX_RENDER_DISTANCE = 16; var MONSTER_COUNT = 5; var TREASURE_COUNT = 10; // Game state var map = []; var player = { x: 1.5, y: 1.5, dir: 0, health: 5, score: 0, level: 1 }; var controls = { forward: false, backward: false, left: false, right: false, attack: false, boost: false }; var monsters = []; var treasures = []; var projectiles = []; var gate = null; var lastTime = Date.now(); var canAttack = true; var attackCooldown = 3000; // 3 second cooldown // UI elements var miniMap; var rayCastView; var healthText; var scoreText; var levelText; var monsterText; var controlButtons = {}; var playerMarker; // Setup game function setupGame() { // Create the rayCast view container rayCastView = new Container(); game.addChild(rayCastView); // Create raycast strips for (var i = 0; i < NUM_RAYS; i++) { var strip = new RaycastStrip(); rayCastView.addChild(strip); } // Create minimap container miniMap = new Container(); miniMap.x = (2048 - MAP_SIZE * CELL_SIZE * MINI_MAP_SCALE) / 2; // Center horizontally miniMap.y = 20; // Keep at top game.addChild(miniMap); // Generate map generateMap(); // Create player marker playerMarker = game.addChild(LK.getAsset('player', { anchorX: 0.5, anchorY: 0.5 })); // Create UI elements createUI(); // Create control buttons createControlButtons(); // Start ottoman ambience music in a loop LK.playMusic('ottoman_ambience', { loop: true }); } function generateMap() { // Clear existing map miniMap.removeChildren(); map = []; // Remove existing monsters and treasures for (var i = 0; i < monsters.length; i++) { monsters[i].destroy(); } monsters = []; for (var i = 0; i < treasures.length; i++) { treasures[i].destroy(); } treasures = []; // Clear projectiles for (var i = 0; i < projectiles.length; i++) { projectiles[i].destroy(); } projectiles = []; // Generate base map with borders for (var y = 0; y < MAP_SIZE; y++) { map[y] = []; for (var x = 0; x < MAP_SIZE; x++) { var cell = new MapCell(); cell.x = x * CELL_SIZE * MINI_MAP_SCALE; cell.y = y * CELL_SIZE * MINI_MAP_SCALE; // Create outer walls if (x === 0 || y === 0 || x === MAP_SIZE - 1 || y === MAP_SIZE - 1) { cell.setType(1); // Wall } else { // Random interior walls based on level difficulty var wallChance = 0.2 + player.level * 0.03; if (Math.random() < wallChance && !(x === 1 && y === 1)) { // Ensure starting position is clear cell.setType(1); // Wall } else { cell.setType(0); // Floor } } map[y][x] = cell; miniMap.addChild(cell); } } // Check map connectivity and fix walled-off areas ensureMapConnectivity(); // Create monsters var monstersToPlace = MONSTER_COUNT + Math.floor(player.level * 0.5); for (var i = 0; i < monstersToPlace; i++) { placeMonster(); } // Create treasures var treasuresToPlace = TREASURE_COUNT; for (var i = 0; i < treasuresToPlace; i++) { placeTreasure(); } // Reset player position player.x = 1.5; player.y = 1.5; player.dir = 0; // Place exit gate gate = placeGate(); } function placeMonster() { // Find a random empty cell var x, y; var attempts = 0; do { x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1; y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1; attempts++; // Make sure it's not too close to the player var distToPlayer = Math.sqrt(Math.pow(x - player.x, 2) + Math.pow(y - player.y, 2)); if (attempts > 100) { break; } // Prevent infinite loop } while (map[y][x].type !== 0 || map[y][x].monster || map[y][x].treasure || distToPlayer < 3); if (attempts <= 100) { map[y][x].addMonster(); var monster = new Monster(); monster.mapX = x; monster.mapY = y; monster.health = 2 + Math.floor(player.level / 3); // Monsters get tougher with level monsters.push(monster); game.addChild(monster); } } function placeTreasure() { // Find a random empty cell var x, y; var attempts = 0; do { x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1; y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1; attempts++; if (attempts > 100) { break; } // Prevent infinite loop } while (map[y][x].type !== 0 || map[y][x].monster || map[y][x].treasure || map[y][x].gate); if (attempts <= 100) { map[y][x].addTreasure(); var treasure = new Treasure(); treasure.mapX = x; treasure.mapY = y; treasure.value = 1 + Math.floor(Math.random() * player.level); treasures.push(treasure); game.addChild(treasure); } } function placeGate() { // Place gate in a random valid location var x, y; var attempts = 0; var validPositions = []; // Collect all valid positions first for (var i = 0; i < 100; i++) { x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1; y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1; // Check if the cell is suitable if (map[y][x].type === 0 && !map[y][x].monster && !map[y][x].treasure && !map[y][x].gate) { // Make sure it's not too close to the player (at least 2 cells away) var distToPlayer = Math.sqrt(Math.pow(x - player.x, 2) + Math.pow(y - player.y, 2)); if (distToPlayer > 2) { // Add to valid positions validPositions.push({ x: x, y: y }); } } } // If we found valid spots, choose one randomly if (validPositions.length > 0) { // Pick a random position from the valid ones var randomIndex = Math.floor(Math.random() * validPositions.length); var chosenPos = validPositions[randomIndex]; var gateX = chosenPos.x; var gateY = chosenPos.y; // Place the gate map[gateY][gateX].addGate(); var gate = new Gate(); gate.mapX = gateX; gate.mapY = gateY; game.addChild(gate); return gate; } return null; } function createUI() { // Health display healthText = new Text2('Health: ' + player.health, { size: 40, fill: 0xFF5555 }); healthText.anchor.set(0, 0); LK.gui.topRight.addChild(healthText); healthText.x = -200; healthText.y = 20; // Score display scoreText = new Text2('Score: ' + player.score, { size: 40, fill: 0xFFFF55 }); scoreText.anchor.set(0, 0); LK.gui.topRight.addChild(scoreText); scoreText.x = -200; scoreText.y = 80; // Level display levelText = new Text2('Level: ' + player.level, { size: 40, fill: 0x55FF55 }); levelText.anchor.set(0, 0); LK.gui.topRight.addChild(levelText); levelText.x = -200; levelText.y = 140; // Monster counter display monsterText = new Text2('Monsters: 0', { size: 40, fill: 0xFF9955 }); monsterText.anchor.set(0, 0); LK.gui.topRight.addChild(monsterText); monsterText.x = -200; monsterText.y = 200; // Update UI displays updateUI(); } function updateUI() { healthText.setText('Health: ' + player.health); scoreText.setText('Score: ' + player.score); levelText.setText('Level: ' + player.level); monsterText.setText('Monsters: ' + monsters.length); // Update score in LK system LK.setScore(player.score); } function createControlButtons() { // Create directional buttons with increased size and more centered position controlButtons.up = new ControlButton('up'); controlButtons.up.x = 400; controlButtons.up.y = 2732 - 700; game.addChild(controlButtons.up); controlButtons.right = new ControlButton('right'); controlButtons.right.x = 650; controlButtons.right.y = 2732 - 500; game.addChild(controlButtons.right); controlButtons.down = new ControlButton('down'); controlButtons.down.x = 400; controlButtons.down.y = 2732 - 300; game.addChild(controlButtons.down); controlButtons.left = new ControlButton('left'); controlButtons.left.x = 150; controlButtons.left.y = 2732 - 500; game.addChild(controlButtons.left); // Create attack button more centered on the right side controlButtons.attack = new ControlButton('attack'); controlButtons.attack.x = 2048 - 400; controlButtons.attack.y = 2732 - 500; game.addChild(controlButtons.attack); // Create boost button in the middle, moved a little to the right controlButtons.boost = new BoostButton(); controlButtons.boost.x = 2048 / 2 + 150; controlButtons.boost.y = 2732 - 500; game.addChild(controlButtons.boost); } function rayCasting() { var rayAngle, distToWall, rayDirX, rayDirY, mapCheckX, mapCheckY; var distX, distY; var rayStartX = player.x; var rayStartY = player.y; for (var rayIdx = 0; rayIdx < NUM_RAYS; rayIdx++) { // Calculate ray angle (center ray + offset based on ray index) rayAngle = player.dir - HALF_FOV + rayIdx / NUM_RAYS * FOV; // Get direction vector rayDirX = Math.cos(rayAngle); rayDirY = Math.sin(rayAngle); // Distance to wall distToWall = 0; hitWall = false; // Step size for ray casting var stepSizeX = Math.abs(1 / rayDirX); var stepSizeY = Math.abs(1 / rayDirY); // Which block we're checking mapCheckX = Math.floor(rayStartX); mapCheckY = Math.floor(rayStartY); // Length of ray from current position to next x or y-side var sideDistX, sideDistY; // Direction to step in x or y direction (either +1 or -1) var stepX = rayDirX >= 0 ? 1 : -1; var stepY = rayDirY >= 0 ? 1 : -1; // Calculate distance to first x and y side if (rayDirX < 0) { sideDistX = (rayStartX - mapCheckX) * stepSizeX; } else { sideDistX = (mapCheckX + 1.0 - rayStartX) * stepSizeX; } if (rayDirY < 0) { sideDistY = (rayStartY - mapCheckY) * stepSizeY; } else { sideDistY = (mapCheckY + 1.0 - rayStartY) * stepSizeY; } // Perform DDA (Digital Differential Analysis) var hit = false; var side = 0; // 0 for x-side, 1 for y-side var maxDistance = MAX_RENDER_DISTANCE; while (!hit && distToWall < maxDistance) { // Jump to next map square if (sideDistX < sideDistY) { sideDistX += stepSizeX; mapCheckX += stepX; side = 0; } else { sideDistY += stepSizeY; mapCheckY += stepY; side = 1; } // Check if ray has hit a wall if (mapCheckX < 0 || mapCheckX >= MAP_SIZE || mapCheckY < 0 || mapCheckY >= MAP_SIZE || !map[mapCheckY] || !map[mapCheckY][mapCheckX]) { hit = true; distToWall = maxDistance; } else if (map[mapCheckY][mapCheckX].type === 1) { hit = true; // Calculate exact distance to avoid fisheye effect if (side === 0) { distToWall = (mapCheckX - rayStartX + (1 - stepX) / 2) / rayDirX; } else { distToWall = (mapCheckY - rayStartY + (1 - stepY) / 2) / rayDirY; } } } // Calculate height of wall based on distance var wallHeight = Math.min(2732, WALL_HEIGHT_FACTOR / distToWall); // Update the strip var strip = rayCastView.children[rayIdx]; strip.updateStrip(STRIP_WIDTH, wallHeight, rayIdx, 1, distToWall); } // Render monsters and treasures renderEntities(); } function renderEntities() { // First, hide all entities for (var i = 0; i < monsters.length; i++) { monsters[i].visible = false; } for (var i = 0; i < treasures.length; i++) { treasures[i].visible = false; } // Hide gate initially if (gate) { gate.visible = false; } // Calculate entity positions relative to player for (var i = 0; i < monsters.length; i++) { var monster = monsters[i]; // Vector from player to monster var dx = monster.mapX - player.x; var dy = monster.mapY - player.y; // Distance to monster var dist = Math.sqrt(dx * dx + dy * dy); // Angle between player's direction and monster var angle = Math.atan2(dy, dx) - player.dir; // Normalize angle while (angle < -Math.PI) { angle += Math.PI * 2; } while (angle > Math.PI) { angle -= Math.PI * 2; } // Check if monster is in field of view if (Math.abs(angle) < HALF_FOV && dist < MAX_RENDER_DISTANCE) { // Check if there's a wall between player and monster var rayDirX = Math.cos(player.dir + angle); var rayDirY = Math.sin(player.dir + angle); var rayHit = castRayToPoint(player.x, player.y, monster.mapX, monster.mapY); if (!rayHit.hit || rayHit.dist > dist - 0.5) { // Monster is visible monster.visible = true; // Calculate screen position var screenX = (0.5 + angle / FOV) * 2048; // Calculate height based on distance var height = WALL_HEIGHT_FACTOR / dist; // Position monster in the middle of the screen monster.x = screenX; monster.y = 2732 / 2; // Scale monster based on distance var scale = height / 100; monster.scale.set(scale, scale); } } } // Render treasures with the same logic for (var i = 0; i < treasures.length; i++) { var treasure = treasures[i]; var dx = treasure.mapX - player.x; var dy = treasure.mapY - player.y; var dist = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx) - player.dir; while (angle < -Math.PI) { angle += Math.PI * 2; } while (angle > Math.PI) { angle -= Math.PI * 2; } if (Math.abs(angle) < HALF_FOV && dist < MAX_RENDER_DISTANCE) { var rayHit = castRayToPoint(player.x, player.y, treasure.mapX, treasure.mapY); if (!rayHit.hit || rayHit.dist > dist - 0.5) { treasure.visible = true; var screenX = (0.5 + angle / FOV) * 2048; var height = WALL_HEIGHT_FACTOR / dist; treasure.x = screenX; treasure.y = 2732 / 2; var scale = height / 100; treasure.scale.set(scale, scale); } } } // Render gate with the same logic as treasures and monsters if (gate) { var dx = gate.mapX - player.x; var dy = gate.mapY - player.y; var dist = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx) - player.dir; while (angle < -Math.PI) { angle += Math.PI * 2; } while (angle > Math.PI) { angle -= Math.PI * 2; } if (Math.abs(angle) < HALF_FOV && dist < MAX_RENDER_DISTANCE) { var rayHit = castRayToPoint(player.x, player.y, gate.mapX, gate.mapY); if (!rayHit.hit || rayHit.dist > dist - 0.5) { gate.visible = true; var screenX = (0.5 + angle / FOV) * 2048; var height = WALL_HEIGHT_FACTOR / dist; gate.x = screenX; gate.y = 2732 / 2; var scale = height / 100; gate.scale.set(scale, scale); } } } } function castRayToPoint(startX, startY, targetX, targetY) { var rayDirX = targetX - startX; var rayDirY = targetY - startY; var distance = Math.sqrt(rayDirX * rayDirX + rayDirY * rayDirY); rayDirX /= distance; rayDirY /= distance; var mapCheckX = Math.floor(startX); var mapCheckY = Math.floor(startY); var stepSizeX = Math.abs(1 / rayDirX); var stepSizeY = Math.abs(1 / rayDirY); var stepX = rayDirX >= 0 ? 1 : -1; var stepY = rayDirY >= 0 ? 1 : -1; var sideDistX, sideDistY; if (rayDirX < 0) { sideDistX = (startX - mapCheckX) * stepSizeX; } else { sideDistX = (mapCheckX + 1.0 - startX) * stepSizeX; } if (rayDirY < 0) { sideDistY = (startY - mapCheckY) * stepSizeY; } else { sideDistY = (mapCheckY + 1.0 - startY) * stepSizeY; } var hit = false; var side = 0; var distToWall = 0; while (!hit && distToWall < distance) { if (sideDistX < sideDistY) { sideDistX += stepSizeX; mapCheckX += stepX; side = 0; distToWall = sideDistX - stepSizeX; } else { sideDistY += stepSizeY; mapCheckY += stepY; side = 1; distToWall = sideDistY - stepSizeY; } if (mapCheckX < 0 || mapCheckX >= MAP_SIZE || mapCheckY < 0 || mapCheckY >= MAP_SIZE || !map[mapCheckY] || !map[mapCheckY][mapCheckX]) { break; } else if (map[mapCheckY][mapCheckX].type === 1) { hit = true; } } return { hit: hit, dist: distToWall }; } function updateControls() { // Read from control buttons controls.forward = controlButtons.up.pressed; controls.backward = controlButtons.down.pressed; controls.left = controlButtons.left.pressed; controls.right = controlButtons.right.pressed; controls.attack = controlButtons.attack.pressed; controls.boost = controlButtons.boost && controlButtons.boost.active; } function updatePlayerMovement(deltaTime) { var moveSpeed = PLAYER_MOVE_SPEED * deltaTime; var turnSpeed = PLAYER_TURN_SPEED * deltaTime; var dx = 0, dy = 0; var didMove = false; // Track player's last position to detect state changes if (player.lastX === undefined) { player.lastX = player.x; } if (player.lastY === undefined) { player.lastY = player.y; } // Handle rotation if (controls.left) { player.dir -= turnSpeed; while (player.dir < 0) { player.dir += Math.PI * 2; } } if (controls.right) { player.dir += turnSpeed; while (player.dir >= Math.PI * 2) { player.dir -= Math.PI * 2; } } // Handle movement if (controls.forward) { dx += Math.cos(player.dir) * moveSpeed; dy += Math.sin(player.dir) * moveSpeed; didMove = true; } if (controls.backward) { dx -= Math.cos(player.dir) * moveSpeed; dy -= Math.sin(player.dir) * moveSpeed; didMove = true; } // Collision detection var newX = player.x + dx; var newY = player.y + dy; var cellX = Math.floor(newX); var cellY = Math.floor(newY); // Check if we can move to the new position if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE && map[cellY] && map[cellY][cellX]) { if (map[cellY][cellX].type === 0) { player.x = newX; player.y = newY; if (didMove && LK.ticks % 20 === 0) { LK.getSound('walk').play(); } } } // Only attempt to attack if attack button is pressed and we can attack if (controls.attack && canAttack) { attackAction(); } // Check for collisions with monsters checkMonsterCollisions(); // Check for collisions with treasures checkTreasureCollisions(); // Check for gate collision checkGateCollision(); // Update player marker on minimap updateMiniMap(); // Update player's last position player.lastX = player.x; player.lastY = player.y; } function attackAction() { // Check if attack is on cooldown if (!canAttack) { return; } // Set attack on cooldown canAttack = false; // Play attack sound LK.getSound('attack').play(); // Create main projectile from right side var projectile = new Projectile(); // Set scale for appropriate size var scale = 1.0; projectile.scale.set(scale, scale); // Set this as the original, non-mirrored projectile projectile.setMirrored(false); // Initialize and fire the projectile from right to left projectile.fire(2048 + 100, 2732 / 2); // Add visual pulse effect to make projectile more visible tween(projectile, { alpha: 0.7, scaleX: scale * 1.2, scaleY: scale * 1.2 }, { duration: 500, onFinish: function onFinish() { tween(projectile, { alpha: 1, scaleX: scale, scaleY: scale }, { duration: 500 }); } }); // Add to the game projectiles.push(projectile); game.addChild(projectile); // Create clone projectile on opposite side (left side) var cloneProjectile = new Projectile(); cloneProjectile.scale.set(scale, scale); // Set this as the mirrored clone cloneProjectile.setMirrored(true); // Make clone start on the left side cloneProjectile.x = -100; cloneProjectile.y = 2732 / 2; cloneProjectile.visible = true; cloneProjectile.active = true; // Reverse speed to make it move right to left cloneProjectile.speed = -cloneProjectile.speed; // Add visual pulse effect to clone tween(cloneProjectile, { alpha: 0.7, scaleX: scale * 1.2, scaleY: scale * 1.2 }, { duration: 500, onFinish: function onFinish() { tween(cloneProjectile, { alpha: 1, scaleX: scale, scaleY: scale }, { duration: 500 }); } }); // Add to the game projectiles.push(cloneProjectile); game.addChild(cloneProjectile); // Start cooldown animation for attack button var attackButton = controlButtons.attack; attackButton.updateCooldown(0); // Create a tween for the cooldown visual effect tween(attackButton, { _cooldownProgress: 1 }, { duration: attackCooldown, onFinish: function onFinish() { canAttack = true; attackButton.updateCooldown(1); } }); // Update button visual during cooldown var _cooldownTick = function cooldownTick(progress) { attackButton.updateCooldown(progress); if (progress < 1) { LK.setTimeout(function () { _cooldownTick(progress + 0.05); }, attackCooldown / 20); } }; _cooldownTick(0); // Reset cooldown after specified time LK.setTimeout(function () { canAttack = true; }, attackCooldown); } function updateProjectiles(deltaTime) { for (var i = projectiles.length - 1; i >= 0; i--) { var projectile = projectiles[i]; // Update projectile position var remove = projectile.update(deltaTime); // Check if projectile has gone off screen or should be removed if (remove) { // Remove projectile projectile.destroy(); projectiles.splice(i, 1); continue; } // Check if projectile hits a monster var hitMonster = false; var hitMonsterIndex = -1; for (var j = 0; j < monsters.length; j++) { var monster = monsters[j]; // Calculate distance between player and monster for range check var distToMonster = Math.sqrt(Math.pow(monster.mapX - player.x, 2) + Math.pow(monster.mapY - player.y, 2)); // Only allow hits if monster is within range (5 map units) - stricter range check var inRange = distToMonster <= 2.5; // Simple screen space collision check with added distance constraint if (monster.visible && inRange && Math.abs(projectile.x - monster.x) < 100 && Math.abs(projectile.y - monster.y) < 100) { hitMonster = true; hitMonsterIndex = j; break; } } // Handle monster hit if (hitMonster && hitMonsterIndex !== -1) { var monster = monsters[hitMonsterIndex]; var killed = monster.takeDamage(); if (killed) { // Remove monster map[Math.floor(monster.mapY)][Math.floor(monster.mapX)].removeMonster(); monster.destroy(); monsters.splice(hitMonsterIndex, 1); // Increase score player.score += 10; updateUI(); // Check for level completion checkLevelCompletion(); } // Remove projectile on hit projectile.destroy(); projectiles.splice(i, 1); } } } function checkMonsterCollisions() { for (var i = 0; i < monsters.length; i++) { var monster = monsters[i]; var dx = monster.mapX - player.x; var dy = monster.mapY - player.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 0.5) { // Player hit by monster player.health--; updateUI(); // Visual feedback LK.effects.flashScreen(0xff0000, 300); // Play sound LK.getSound('hit').play(); // Push player back slightly player.x -= dx * 0.3; player.y -= dy * 0.3; // Check game over if (player.health <= 0) { LK.showGameOver(); } break; } } } function checkTreasureCollisions() { for (var i = treasures.length - 1; i >= 0; i--) { var treasure = treasures[i]; var dx = treasure.mapX - player.x; var dy = treasure.mapY - player.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 0.5) { // Collect treasure player.score += treasure.value * 5; updateUI(); // Play sound LK.getSound('collect').play(); // Remove treasure map[Math.floor(treasure.mapY)][Math.floor(treasure.mapX)].removeTreasure(); treasure.destroy(); treasures.splice(i, 1); // Check level completion checkLevelCompletion(); } } } function checkGateCollision() { if (!gate) { return; } var dx = gate.mapX - player.x; var dy = gate.mapY - player.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 0.7) { // Check if all monsters need to be defeated first if (monsters.length > 0) { // Display message that monsters need to be defeated var warningText = new Text2('Defeat all monsters\nbefore exiting!', { size: 60, fill: 0xFF5555 }); warningText.anchor.set(0.5, 0.5); warningText.x = 2048 / 2; warningText.y = 2732 / 2; game.addChild(warningText); // Remove text after a few seconds LK.setTimeout(function () { warningText.destroy(); }, 2000); return; } // Player reached the gate - complete level // Play collect sound LK.getSound('collect').play(); // Add points for completing level player.score += 50 * player.level; // Remove gate from map map[Math.floor(gate.mapY)][Math.floor(gate.mapX)].removeGate(); gate.destroy(); gate = null; // Level up player.level++; storage.level = player.level; // Restore health player.health = Math.min(player.health + 2, 5); // Update UI updateUI(); // Show level complete with gate messaging var levelCompleteText = new Text2('Level ' + (player.level - 1) + ' Complete!\nYou found the exit!', { size: 80, fill: 0x00FF55 }); levelCompleteText.anchor.set(0.5, 0.5); levelCompleteText.x = 2048 / 2; levelCompleteText.y = 2732 / 2; game.addChild(levelCompleteText); // Generate new level after a delay LK.setTimeout(function () { levelCompleteText.destroy(); generateMap(); }, 2000); } } function checkLevelCompletion() { // Level is only considered "complete" if all monsters are defeated // This allows the gate to activate if (monsters.length === 0) { // If gate is not visible, make it pulse more dramatically to draw attention if (gate) { tween(gate, { alpha: 0.2 }, { duration: 300, onFinish: function onFinish() { tween(gate, { alpha: 1 }, { duration: 300 }); } }); // Show hint text var gateHintText = new Text2('Find the exit gate!', { size: 60, fill: 0x00FF55 }); gateHintText.anchor.set(0.5, 0.5); gateHintText.x = 2048 / 2; gateHintText.y = 200; game.addChild(gateHintText); // Remove hint after a few seconds LK.setTimeout(function () { gateHintText.destroy(); }, 3000); } } else { // Show hint text about defeating monsters first if (gate && gate.visible && LK.ticks % 300 === 0) { var monsterHintText = new Text2('Defeat all monsters to activate the exit!', { size: 60, fill: 0xFF5555 }); monsterHintText.anchor.set(0.5, 0.5); monsterHintText.x = 2048 / 2; monsterHintText.y = 200; game.addChild(monsterHintText); // Remove hint after a few seconds LK.setTimeout(function () { monsterHintText.destroy(); }, 3000); } } } function updateMiniMap() { // Update player marker position on minimap playerMarker.x = miniMap.x + player.x * CELL_SIZE * MINI_MAP_SCALE; playerMarker.y = miniMap.y + player.y * CELL_SIZE * MINI_MAP_SCALE; // Draw player direction indicator var dirX = Math.cos(player.dir) * 15; var dirY = Math.sin(player.dir) * 15; } // Game update method game.update = function () { var currentTime = Date.now(); var deltaTime = currentTime - lastTime; lastTime = currentTime; // Update controls updateControls(); // Update player updatePlayerMovement(deltaTime); // Update monsters (only move every few ticks to make movement slower) if (LK.ticks % 10 === 0) { for (var i = 0; i < monsters.length; i++) { MonsterAI.moveTowardsPlayer(monsters[i], player.x, player.y, map); } } // Update projectiles updateProjectiles(deltaTime); // Update raycast view rayCasting(); }; // Game initialization setupGame(); // Event handlers game.down = function (x, y, obj) { // Handle general screen press }; game.up = function (x, y, obj) { // Handle general screen release }; game.move = function (x, y, obj) { // Handle general screen move }; var MonsterAI = { moveTowardsPlayer: function moveTowardsPlayer(monster, playerX, playerY, map) { // Get monster's map position var monsterX = monster.mapX; var monsterY = monster.mapY; // Vector from monster to player var dx = playerX - monsterX; var dy = playerY - monsterY; // Only move if monster is within a certain distance to the player var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 8) { return; } // Don't move if too far away // Normalize direction vector var length = Math.sqrt(dx * dx + dy * dy); if (length > 0) { dx /= length; dy /= length; } // Movement speed (slower than player) var moveSpeed = 0.05; // Increased for smoother animation // Calculate potential new position var newX = monsterX + dx * moveSpeed; var newY = monsterY + dy * moveSpeed; // Round to get map cell coordinates var cellX = Math.floor(newX); var cellY = Math.floor(newY); // Check if new position is valid (not a wall or another monster) if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE) { if (map[cellY][cellX].type === 0 && !map[cellY][cellX].monster) { // Update map cell references map[Math.floor(monsterY)][Math.floor(monsterX)].removeMonster(); map[cellY][cellX].addMonster(); // Update monster position with smooth animation monster.mapX = newX; monster.mapY = newY; // Calculate screen position for the moved monster var monsterToPlayerDX = playerX - newX; var monsterToPlayerDY = playerY - newY; var monsterDist = Math.sqrt(monsterToPlayerDX * monsterToPlayerDX + monsterToPlayerDY * monsterToPlayerDY); var monsterAngle = Math.atan2(monsterToPlayerDY, monsterToPlayerDX) - Math.atan2(dy, dx); // Position monster in the middle of the screen with updated coordinates monster.x = 2048 / 2; monster.y = 2732 / 2; // Scale monster based on distance var scale = WALL_HEIGHT_FACTOR / (monsterDist * 100); monster.scale.set(scale, scale); } } } }; function ensureMapConnectivity() { // Flood fill from player starting position to check accessibility var visited = []; for (var y = 0; y < MAP_SIZE; y++) { visited[y] = []; for (var x = 0; x < MAP_SIZE; x++) { visited[y][x] = false; } } var queue = []; // Start from player position (1,1) queue.push({ x: 1, y: 1 }); visited[1][1] = true; // Perform flood fill while (queue.length > 0) { var current = queue.shift(); var x = current.x; var y = current.y; // Check all four neighbors var neighbors = [{ x: x + 1, y: y }, { x: x - 1, y: y }, { x: x, y: y + 1 }, { x: x, y: y - 1 }]; for (var i = 0; i < neighbors.length; i++) { var nx = neighbors[i].x; var ny = neighbors[i].y; // Check if neighbor is valid and not visited if (nx >= 0 && nx < MAP_SIZE && ny >= 0 && ny < MAP_SIZE && map[ny][nx].type === 0 && !visited[ny][nx]) { visited[ny][nx] = true; queue.push({ x: nx, y: ny }); } } } // Check for unreachable areas and create paths to them for (var y = 1; y < MAP_SIZE - 1; y++) { for (var x = 1; x < MAP_SIZE - 1; x++) { // If it's a floor tile but wasn't visited, it's unreachable if (map[y][x].type === 0 && !visited[y][x]) { // Find the nearest accessible cell var nearestX = -1; var nearestY = -1; var minDist = MAP_SIZE * MAP_SIZE; for (var cy = 1; cy < MAP_SIZE - 1; cy++) { for (var cx = 1; cx < MAP_SIZE - 1; cx++) { if (visited[cy][cx]) { var dist = (cx - x) * (cx - x) + (cy - y) * (cy - y); if (dist < minDist) { minDist = dist; nearestX = cx; nearestY = cy; } } } } // Create a path from the unreachable area to the nearest accessible area if (nearestX !== -1) { createPath(x, y, nearestX, nearestY); // Update visited map by doing a mini flood fill from this newly connected point var pathQueue = [{ x: x, y: y }]; visited[y][x] = true; while (pathQueue.length > 0) { var current = pathQueue.shift(); var px = current.x; var py = current.y; var pathNeighbors = [{ x: px + 1, y: py }, { x: px - 1, y: py }, { x: px, y: py + 1 }, { x: px, y: py - 1 }]; for (var j = 0; j < pathNeighbors.length; j++) { var nx = pathNeighbors[j].x; var ny = pathNeighbors[j].y; if (nx >= 0 && nx < MAP_SIZE && ny >= 0 && ny < MAP_SIZE && map[ny][nx].type === 0 && !visited[ny][nx]) { visited[ny][nx] = true; pathQueue.push({ x: nx, y: ny }); } } } } } } } } function createPath(startX, startY, endX, endY) { // Determine direction (horizontal or vertical first) if (Math.random() < 0.5) { // Horizontal first, then vertical var x = startX; while (x !== endX) { x += x < endX ? 1 : -1; if (map[startY][x].type === 1) { map[startY][x].setType(0); // Convert wall to floor } } var y = startY; while (y !== endY) { y += y < endY ? 1 : -1; if (map[y][endX].type === 1) { map[y][endX].setType(0); // Convert wall to floor } } } else { // Vertical first, then horizontal var y = startY; while (y !== endY) { y += y < endY ? 1 : -1; if (map[y][startX].type === 1) { map[y][startX].setType(0); // Convert wall to floor } } var x = startX; while (x !== endX) { x += x < endX ? 1 : -1; if (map[endY][x].type === 1) { map[endY][x].setType(0); // Convert wall to floor } } } }
===================================================================
--- original.js
+++ change.js
@@ -437,10 +437,12 @@
// Create UI elements
createUI();
// Create control buttons
createControlButtons();
- // Start background music
- LK.playMusic('dungeon');
+ // Start ottoman ambience music in a loop
+ LK.playMusic('ottoman_ambience', {
+ loop: true
+ });
}
function generateMap() {
// Clear existing map
miniMap.removeChildren();
Tan wall. In-Game asset. 2d. High contrast. No shadows
pixel detailed A Janissary of the Ottoman Empire. In-Game asset. 2d. High contrast. No shadows
pixel ottoman dagger. In-Game asset. 2d. High contrast. No shadows
ottoman lady pixel. In-Game asset. 2d. High contrast. No shadows
ottoman walls pixel. In-Game asset. 2d. High contrast. No shadows