User prompt
Make the monster movement work
User prompt
Make the treasure sit lower
Code edit (3 edits merged)
Please save this source code
User prompt
Give the projectiles more range
User prompt
Make it so less rays are cast
User prompt
Make it so less rays are cast
User prompt
Move the boost button a little to the right
User prompt
Move the boost button to the left
User prompt
Move the boost button to the middle
User prompt
Move it to the middle
User prompt
Move the boost button up a little and to the left
User prompt
Add a boost button that speeds up the forwards and backwards movement
User prompt
Make the two projectiles mirror images of each other ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make it so the projectile shows at the bottom of the screen instead of the middle
Code edit (1 edits merged)
Please save this source code
User prompt
Make it so the monster has to be closer to the player to be hit by the projectile
User prompt
Make it so if the monster is far enough away from the player the projectile does not hit it.
User prompt
Make the projectile only hit enemies if The scale/the shade of the enemy is greater than x
/**** * 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 background music LK.playMusic('dungeon'); } 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
@@ -27,11 +27,9 @@
self.pressed = false;
self.active = false;
self.cooldown = false;
self.down = function (x, y, obj) {
- if (self.cooldown) {
- return;
- }
+ if (self.cooldown) return;
self.pressed = true;
buttonSprite.alpha = 0.7;
self.active = true;
// Apply boost effect
@@ -810,9 +808,9 @@
treasure.visible = true;
var screenX = (0.5 + angle / FOV) * 2048;
var height = WALL_HEIGHT_FACTOR / dist;
treasure.x = screenX;
- treasure.y = 2732 / 2 + 200; // Position treasure lower on screen
+ treasure.y = 2732 / 2;
var scale = height / 100;
treasure.scale.set(scale, scale);
}
}
@@ -1086,9 +1084,9 @@
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 <= 1.75;
+ 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;
@@ -1342,18 +1340,21 @@
// Update map cell references
map[Math.floor(monsterY)][Math.floor(monsterX)].removeMonster();
map[cellY][cellX].addMonster();
// Update monster position with smooth animation
- tween(monster, {
- mapX: newX,
- mapY: newY
- }, {
- duration: 300,
- // 300ms smooth animation
- easing: function easing(t) {
- return 1 - Math.pow(1 - t, 4);
- } // quartOut implementation
- });
+ 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);
}
}
}
};
Fullscreen modern App Store landscape banner, 16:9, high definition, for a game titled "RayCaster Dungeon Crawler" and with the description "A first-person dungeon crawler using ray casting technology to create a pseudo-3D experience. Navigate maze-like dungeons, defeat monsters, and collect treasures as you explore increasingly challenging levels with authentic retro visuals.". No text on banner!
Sword. In-Game asset. 2d. High contrast. No shadows
Tan wall. In-Game asset. 2d. High contrast. No shadows
Make a bunch of empty space above it.