Code edit (16 edits merged)
Please save this source code
User prompt
give a little bit of slow scale pulse and slight float to the hand to give it some life. the current Y value should be the highest it comes ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
give a little bit of slow scale pulse and slight float to the hand to give it some life. the current Y value should be the highest it comes ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (5 edits merged)
Please save this source code
User prompt
I want the projectile to travel from center of the global hand X to 2048/2. Currently the projectile is spawning at 2048/2 because of how the world coordinate tracking works for projectiles. Fix it so that the starting X of projectiles is as I outlined above
Code edit (1 edits merged)
Please save this source code
Code edit (2 edits merged)
Please save this source code
User prompt
Change the x coordinates of the projectile spawn to match center x of righthand
User prompt
Move projectile spawn point 200 pixels right
User prompt
Move projectile spawn point 200 pixels right
User prompt
Move projectile spawn point 200 pixels right
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'var handCenterX = rightHand.x;' Line Number: 1135
User prompt
Set projectile spawn point to center X of hand
User prompt
Set projectile spawn to center x of hand
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'gameLayer.addChild')' in or related to this line: 'gameLayer.addChild(controlButtons.joystick);' Line Number: 748
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'gameLayer.addChild')' in or related to this line: 'gameLayer.addChild(monster);' Line Number: 631
User prompt
Please fix the bug: 'Can't find variable: gameLayer' in or related to this line: 'gameLayer.addChild(monster);' Line Number: 627
User prompt
Please fix the bug: 'Can't find variable: gameLayer' in or related to this line: 'gameLayer.addChild(rayCastView);' Line Number: 512
User prompt
Please fix the bug: 'Can't find variable: gameLayer' in or related to this line: 'gameLayer.addChild(rayCastView);' Line Number: 512
User prompt
Have projectiles spawn at a layer below right hand
Code edit (1 edits merged)
Please save this source code
User prompt
Move right hand down 5%
User prompt
Use the right hand asset and place it just off right of middle on the bottom of the screen. This is where projectiles will shoot from.
User prompt
Increase the speed of scaling the projectile a little.
User prompt
The scaling on the projectile happens too quickly.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ 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) { // Stop propagation to prevent the event from affecting the joystick obj.stopPropagation = true; // If joystick is active, store its current state for override if (controlButtons && controlButtons.joystick && controlButtons.joystick.active) { joystickOverrideActive = true; joystickOverrideX = controlButtons.joystick.normalizedX; joystickOverrideY = controlButtons.joystick.normalizedY; } else { // Ensure override is not active if joystick wasn't active at press time joystickOverrideActive = false; } attackAction(); } }; self.up = function (x, y, obj) { self.pressed = false; if (self.direction === 'attack') { // Clear joystick override when attack button is released joystickOverrideActive = false; 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 JoystickController = Container.expand(function () { var self = Container.call(this); // Create joystick base var baseRadius = 150; var baseSprite = self.attachAsset('controlButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 5.0, scaleY: 5.0, alpha: 0.4 }); // Create joystick handle var handleRadius = 100; var handleSprite = self.attachAsset('controlButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 3.0, scaleY: 3.0 }); // Initialize variables self.active = false; self.startX = 0; self.startY = 0; self.maxDistance = baseRadius; self.normalizedX = 0; self.normalizedY = 0; // Reset the joystick handle position self.resetHandle = function () { handleSprite.x = 0; handleSprite.y = 0; self.normalizedX = 0; self.normalizedY = 0; self.active = false; }; // Handle touch down event self.down = function (x, y, obj) { self.active = true; self.startX = x; self.startY = y; }; // Handle touch move event self.move = function (x, y, obj) { if (!self.active) { return; } // Calculate distance from start position var dx = x - self.startX; var dy = y - self.startY; var distance = Math.sqrt(dx * dx + dy * dy); // Normalize the distance to get direction vector if (distance > 0) { // Clamp to max distance if (distance > self.maxDistance) { dx = dx * self.maxDistance / distance; dy = dy * self.maxDistance / distance; distance = self.maxDistance; } // Set handle position handleSprite.x = dx; handleSprite.y = dy; // Calculate normalized values (-1 to 1) self.normalizedX = dx / self.maxDistance; self.normalizedY = dy / self.maxDistance; } else { self.resetHandle(); } }; // Handle touch up event self.up = function (x, y, obj) { self.resetHandle(); }; // Initialize with handle at center self.resetHandle(); 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 }); self.speed = 20; // Increased speed for better effect self.startScale = 2; // Starting scale for 3D effect self.endScale = 0.1; // Ending scale (smaller as it goes away) self.distance = 0; // Current travel distance self.maxDistance = 2048; // Maximum travel distance self.visible = false; // Start invisible self.active = false; // Projectile state // Store world position and player state when fired self.playerDirAtFire = 0; // Player direction at time of firing self.fireWorldX = 0; // World X position at fire time self.fireWorldY = 0; // World Y position at fire time self.fireScreenX = 0; // Screen X position at fire time self.update = function (deltaTime) { if (!self.active) { return false; } // Calculate center point of screen height var centerY = 2732 / 2; var centerX = 2048 / 2; // calculate movement progress based on distance self.distance += self.speed; var progress = self.distance / self.maxDistance; // Calculate angle difference between current player direction and fire direction var dirDifference = player.dir - self.playerDirAtFire; // Calculate world position of projectile var projectileWorldDistance = self.distance * 0.05; // Convert pixel distance to world units var projectileWorldX = self.fireWorldX + Math.cos(self.playerDirAtFire) * projectileWorldDistance; var projectileWorldY = self.fireWorldY + Math.sin(self.playerDirAtFire) * projectileWorldDistance; // Convert projectile world position to screen position based on player's current position and direction var relativeX = projectileWorldX - player.x; var relativeY = projectileWorldY - player.y; // Calculate angle from player to projectile in world space var angleToProjectile = Math.atan2(relativeY, relativeX); // Adjust angle based on player's current direction var screenAngle = angleToProjectile - player.dir; // Normalize angle to -PI to PI range while (screenAngle < -Math.PI) { screenAngle += Math.PI * 2; } while (screenAngle > Math.PI) { screenAngle -= Math.PI * 2; } // Calculate distance to projectile in world space var distanceToProjectile = Math.sqrt(relativeX * relativeX + relativeY * relativeY); // Calculate screen position if (Math.abs(screenAngle) < HALF_FOV) { // Only show if in field of view // Calculate x position based on angle in FOV self.x = centerX + screenAngle / HALF_FOV * (centerX * 0.9); // Calculate y position (height) based on distance and progress // The further away, the closer to the center of the screen (horizon) var startY = 2732 - 500; // Starting position (bottom of screen) self.y = startY * (1 - progress) + centerY * progress; self.visible = true; } else { self.visible = false; } // Calculate scale based on both distance and progress // Reduce the impact of distance to make scaling more gradual var distanceFactor = Math.max(0.3, 1 / (distanceToProjectile + 1.5)); // Increase the progress factor from 0.4 to 0.6 to speed up scaling var currentScale = self.startScale * (1 - progress * 0.6) * distanceFactor; projectileSprite.scale.set(currentScale, currentScale); // Return true if projectile has gone too far based on distance return self.distance > self.maxDistance; }; self.fire = function (screenX, screenY) { // These values don't actually matter much since they're immediately recalculated self.x = screenX || 2048 / 2; self.y = 2732 - 300; self.distance = 0; projectileSprite.scale.set(self.startScale, self.startScale); self.visible = true; self.active = true; // Store player's direction at fire time self.playerDirAtFire = player.dir; // THIS IS THE IMPORTANT PART - adjust the world starting position // Add an offset perpendicular to player direction for X adjustment var perpAngle = player.dir + Math.PI / 2; var xOffset = 0.4; // Adjust this value to move left/right (negative values move left) // Add offset behind player for Y adjustment (raising the starting point) var yOffset = 0.2; // Adjust this value to move up/down (higher values move projectile up) // Apply both offsets to create the starting world position self.fireWorldX = player.x + xOffset * Math.cos(perpAngle); self.fireWorldY = player.y + xOffset * Math.sin(perpAngle) - yOffset; self.fireScreenX = screenX || 2048 / 2; }; 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 = 8; 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 }; var monsters = []; var treasures = []; var projectiles = []; var gate = null; var lastTime = Date.now(); var canAttack = true; var attackCooldown = 500; // 500 millisecond cooldown // Joystick override state var joystickOverrideActive = false; var joystickOverrideX = 0; var joystickOverrideY = 0; // UI elements var miniMap; var rayCastView; var healthText; var scoreText; var levelText; var monsterText; var controlButtons = {}; var playerMarker; var globalRightHand; // Stores the right hand game object var activeControlForGlobalHandlers = null; // Tracks which control is active for game.down/up/move // Create layer variables at the global scope var gameLayer = new Container(); var projectileLayer = new Container(); var handLayer = new Container(); // Setup game function setupGame() { // Set up the layer system using the globally defined variables // Add layers in the correct order (projectiles below hand) game.addChild(gameLayer); game.addChild(projectileLayer); game.addChild(handLayer); // Create the rayCast view container rayCastView = new Container(); gameLayer.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 gameLayer.addChild(miniMap); // Generate map generateMap(); // Create player marker playerMarker = gameLayer.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); gameLayer.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); gameLayer.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; gameLayer.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 joystick control for movement controlButtons.joystick = new JoystickController(); controlButtons.joystick.x = 400; controlButtons.joystick.y = 2732 - 500; gameLayer.addChild(controlButtons.joystick); // Using the global layer system - no need to recreate layers here // Create attack button more centered on the right side controlButtons.attack = new ControlButton('attack'); controlButtons.attack.x = 2048 - 400; controlButtons.attack.y = 2732 - 500; gameLayer.addChild(controlButtons.attack); // Add right hand at the bottom right for projectile firing globalRightHand = LK.getAsset('rightHand', { anchorX: 0.5, anchorY: 0.9, scaleX: 1.2, scaleY: 1.2 }); globalRightHand.x = 2048 * 0.6; // Position just off right of middle globalRightHand.y = 2732; // Bottom of the screen handLayer.addChild(globalRightHand); // Add pulse and float animation to give the hand some life var _animateHand = function animateHand() { // Slow scale pulse animation tween(globalRightHand, { scaleX: 1.2, scaleY: 1.2, y: 2732 - 15 // Float up slightly from base position }, { duration: 1800, easing: tween.easeInOut, onFinish: function onFinish() { tween(globalRightHand, { scaleX: 1.1, scaleY: 1.1, y: 2732 // Return to original position }, { duration: 1800, easing: tween.easeInOut, onFinish: _animateHand }); } }); }; // Start the hand animation _animateHand(); } 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 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() { var joystick = controlButtons.joystick; var currentJoystickX, currentJoystickY, isJoystickConsideredActive; if (joystickOverrideActive) { currentJoystickX = joystickOverrideX; currentJoystickY = joystickOverrideY; isJoystickConsideredActive = true; // Movement continues based on pre-attack state } else if (joystick && joystick.active) { currentJoystickX = joystick.normalizedX; currentJoystickY = joystick.normalizedY; isJoystickConsideredActive = true; } else { currentJoystickX = 0; currentJoystickY = 0; isJoystickConsideredActive = false; } if (isJoystickConsideredActive) { controls.forward = currentJoystickY < -0.3; controls.backward = currentJoystickY > 0.3; controls.left = currentJoystickX < -0.3; controls.right = currentJoystickX > 0.3; } else { controls.forward = false; controls.backward = false; controls.left = false; controls.right = false; } // Read from attack button controls.attack = controlButtons.attack.pressed; } 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; } // Get joystick values for analog control var joystick = controlButtons.joystick; var turnAmount = 0; var moveAmount = 0; var currentJoystickX, currentJoystickY, isJoystickConsideredActiveForMovement; if (joystickOverrideActive) { currentJoystickX = joystickOverrideX; currentJoystickY = joystickOverrideY; isJoystickConsideredActiveForMovement = true; } else if (joystick && joystick.active) { currentJoystickX = joystick.normalizedX; currentJoystickY = joystick.normalizedY; isJoystickConsideredActiveForMovement = true; } else { currentJoystickX = 0; currentJoystickY = 0; isJoystickConsideredActiveForMovement = false; } // Handle rotation - use x-axis for turning if (isJoystickConsideredActiveForMovement) { turnAmount = currentJoystickX * turnSpeed; player.dir += turnAmount; while (player.dir < 0) { player.dir += Math.PI * 2; } while (player.dir >= Math.PI * 2) { player.dir -= Math.PI * 2; } } // Also support digital controls for rotation (these are now secondary if joystick was primary) else if (controls.left) { player.dir -= turnSpeed; while (player.dir < 0) { player.dir += Math.PI * 2; } } else if (controls.right) { player.dir += turnSpeed; while (player.dir >= Math.PI * 2) { player.dir -= Math.PI * 2; } } // Handle movement - use y-axis for forward/backward if (isJoystickConsideredActiveForMovement) { moveAmount = -currentJoystickY * moveSpeed; // Negative because up is negative y if (Math.abs(moveAmount) > 0.01) { dx += Math.cos(player.dir) * moveAmount; dy += Math.sin(player.dir) * moveAmount; didMove = true; } } // Also support digital controls for movement (secondary) else if (controls.forward) { dx += Math.cos(player.dir) * moveSpeed; dy += Math.sin(player.dir) * moveSpeed; didMove = true; } else 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 projectile from player perspective (bottom of screen with sharper angle) var projectile = new Projectile(); // Get the center x position of the hand var handCenterX = globalRightHand.x; // Projectiles spawn from the hand's current x-position (center, as anchorX is 0.5) // Fire the projectile from the hand center aimed toward center of screen projectile.fire(handCenterX + 400, 2732 - 500); // Add visual flash effect for firing tween(projectile, { alpha: 0.7 }, { duration: 200, onFinish: function onFinish() { tween(projectile, { alpha: 1 }, { duration: 200 }); } }); // Add to the projectile layer (below hand) projectiles.push(projectile); projectileLayer.addChild(projectile); // 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]; // Enhanced 3D perspective collision check for center-aimed projectiles // As projectile gets further away (higher Y position), collision window narrows if (monster.visible) { // Calculate collision window based on projectile's Y position and distance from center // The higher up the projectile (smaller Y), the smaller the hit window var distanceFactor = 1 - projectile.y / 2732; // Calculate distance from center of screen - projectiles are more accurate near center var centerFactor = 1 - Math.abs(monster.x - 2048 / 2) / (2048 / 2); // Combine factors for more accurate center shots var hitWindowX = 300 * distanceFactor * (0.7 + centerFactor * 0.3); // More likely to hit monsters in the center of view if (Math.abs(projectile.x - monster.x) < hitWindowX) { 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; gameLayer.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; gameLayer.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; gameLayer.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; gameLayer.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 % 20 === 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) { activeControlForGlobalHandlers = null; // Reset at the start of a new touch var eventObjForAttack = Object.assign({}, obj); // Fresh event object for attack button eventObjForAttack.stopPropagation = false; var attackBtn = controlButtons.attack; var attackBtnLocalX = x - attackBtn.x; var attackBtnLocalY = y - attackBtn.y; // Use the same generous hit radius as in the original code var attackBtnHitRadius = 150 * 3.5; var attackBtnDistSq = attackBtnLocalX * attackBtnLocalX + attackBtnLocalY * attackBtnLocalY; if (attackBtnDistSq <= attackBtnHitRadius * attackBtnHitRadius) { activeControlForGlobalHandlers = attackBtn; attackBtn.down(attackBtnLocalX, attackBtnLocalY, eventObjForAttack); // If the touch is within the attack button's area, it's considered handled by the attack button, // regardless of whether it internally decided to stop propagation (e.g. attack on cooldown). // This prevents the joystick from also processing this touch. return; } // If we reach here, the touch was NOT on the attack button. // Now check for joystick interaction using original quadrant check. var joystick = controlButtons.joystick; if (x < 800 && y > 2732 - 800) { activeControlForGlobalHandlers = joystick; var eventObjForJoystick = Object.assign({}, obj); // Fresh event object for joystick eventObjForJoystick.stopPropagation = false; // Pass local coordinates to joystick.down joystick.down(x - joystick.x, y - joystick.y, eventObjForJoystick); } }; game.up = function (x, y, obj) { if (activeControlForGlobalHandlers === controlButtons.attack) { var attackBtnLocalX = x - controlButtons.attack.x; var attackBtnLocalY = y - controlButtons.attack.y; controlButtons.attack.up(attackBtnLocalX, attackBtnLocalY, obj); } else if (activeControlForGlobalHandlers === controlButtons.joystick) { // Only call joystick.up if the joystick itself believes it's active if (controlButtons.joystick.active) { var joystickLocalX = x - controlButtons.joystick.x; var joystickLocalY = y - controlButtons.joystick.y; controlButtons.joystick.up(joystickLocalX, joystickLocalY, obj); } } activeControlForGlobalHandlers = null; // Reset after touch ends }; game.move = function (x, y, obj) { // Handle general screen move if (activeControlForGlobalHandlers === controlButtons.joystick) { // Only call joystick.move if the joystick itself believes it's active if (controlButtons.joystick.active) { var joystickLocalX = x - controlButtons.joystick.x; var joystickLocalY = y - controlButtons.joystick.y; controlButtons.joystick.move(joystickLocalX, joystickLocalY, obj); } } // Attack button doesn't typically have a .move action, so no changes needed for it here. }; var MonsterAI = { moveTowardsPlayer: function moveTowardsPlayer(monster, playerX, playerY, map) { // If monster is not visible (no line of sight or out of FOV/range), it shouldn't attempt to move. // monster.visible is set by the renderEntities function, which includes line-of-sight checks. if (!monster.visible) { return; } // 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 (aggro range) // This check is in addition to the visibility/LoS check. var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 8) { return; } // Don't move if too far away (but still visible) // 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 tween(monster, { mapX: newX, mapY: newY }, { duration: 300, // 300ms smooth animation easing: function easing(t) { return 1 - Math.pow(1 - t, 4); } // quartOut implementation }); } } } }; 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
@@ -745,18 +745,18 @@
// Add pulse and float animation to give the hand some life
var _animateHand = function animateHand() {
// Slow scale pulse animation
tween(globalRightHand, {
- scaleX: 1.25,
- scaleY: 1.25,
+ scaleX: 1.2,
+ scaleY: 1.2,
y: 2732 - 15 // Float up slightly from base position
}, {
duration: 1800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(globalRightHand, {
- scaleX: 1.2,
- scaleY: 1.2,
+ scaleX: 1.1,
+ scaleY: 1.1,
y: 2732 // Return to original position
}, {
duration: 1800,
easing: tween.easeInOut,
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!
Tan wall. In-Game asset. 2d. High contrast. No shadows
A blue glowing orb of magic. Pixel art. In-Game asset. 2d. High contrast. No shadows
A tile of grey and mossy dungeon stone floor. Pixel art.. In-Game asset. 2d. High contrast. No shadows
Arm up in the air.
A unlit metal cage sconce like you find on a dungeon wall. White candle inside. Pixel art.. In-Game asset. 2d. High contrast. No shadows
A white spider web. Pixelated retro.. In-Game asset. 2d. High contrast. No shadows
A round fireball projectile. Straight on view as if it’s coming straight towards the camera. Retro pixel art.. In-Game asset. 2d. High contrast. No shadows
A stone staircase icon. Side profile. Pixel art.. In-Game asset. 2d. High contrast. No shadows
Pixel art logo for a game called ‘Demon’s Depths’. Big demon head with the title of the game split on top and bottom. The words are made of flame. White background In-Game asset. 2d. High contrast. No shadows
Background image of gate leading into a dark dungeon. Walls are grey and mossy stones. Retro pixel art.. In-Game asset. 2d. High contrast. No shadows
SVG made of grey stone bricks that says ‘Enter’. Retro pixel art. In-Game asset. 2d. High contrast. No shadows
dungeon
Music
playerprojectile
Sound effect
wallhit
Sound effect
walk
Sound effect
impCry
Sound effect
playerhurt
Sound effect
enemyexplosion
Sound effect
pixeldrop
Sound effect
eyeball
Sound effect
treasureopen
Sound effect
fountainsplash
Sound effect
powerup
Sound effect
ogre
Sound effect
fireball
Sound effect
bosschant
Sound effect
demonlaugh
Sound effect