Code edit (2 edits merged)
Please save this source code
User prompt
Replace with: var wallTextureManager = { activeTextures: [], clearTextures: function() { for (var i = 0; i < this.activeTextures.length; i++) { this.activeTextures[i].destroy(); } this.activeTextures = []; }, renderWallTextures: function(wallFaces) { this.clearTextures(); for (var i = 0; i < wallFaces.length; i++) { var face = wallFaces[i]; // Simplified coordinate calculation var startX = face.startRay * STRIP_WIDTH; var endX = (face.endRay + 1) * STRIP_WIDTH; var faceWidth = Math.max(STRIP_WIDTH, endX - startX); // Simpler height calculation var wallHeight = Math.max(200, Math.min(1000, WALL_HEIGHT_FACTOR / face.distance)); // Create wall texture var wallTexture = LK.getAsset('seamlessWallTexture', { anchorX: 0, anchorY: 0 }); // Force visible properties wallTexture.x = startX; wallTexture.y = (2732 - wallHeight) / 2; wallTexture.width = faceWidth; wallTexture.height = wallHeight; wallTexture.alpha = 0.8; // Force visibility for testing wallTexture.visible = true; // Add to scene - try multiple locations gameLayer.addChild(wallTexture); this.activeTextures.push(wallTexture); } } };
User prompt
Replace with: var wallFaceTracker = { currentFaces: [], detectWallFaces: function(raycastResults) { var faces = []; var currentFace = null; for (var i = 0; i < raycastResults.length; i++) { var hit = raycastResults[i]; if (hit.wallHit) { // More lenient wall face detection - only use map coordinates var wallId = hit.mapX + "_" + hit.mapY; if (!currentFace || currentFace.wallId !== wallId) { // New wall face detected if (currentFace) { currentFace.endRay = i - 1; faces.push(currentFace); } currentFace = { wallId: wallId, startRay: i, endRay: i, // Initialize endRay mapX: hit.mapX, mapY: hit.mapY, side: hit.side, distance: hit.distance }; } else { // Same face continues - update endRay currentFace.endRay = i; currentFace.distance = Math.min(currentFace.distance, hit.distance); } } else if (currentFace) { // End of current face faces.push(currentFace); currentFace = null; } } // Don't forget the last face if (currentFace) { faces.push(currentFace); } // Force at least one face for testing if any walls exist if (faces.length === 0) { // Create a test face from any wall hit for (var i = 0; i < raycastResults.length; i++) { if (raycastResults[i].wallHit) { faces.push({ wallId: "test", startRay: Math.max(0, i - 2), endRay: Math.min(raycastResults.length - 1, i + 2), mapX: raycastResults[i].mapX, mapY: raycastResults[i].mapY, side: raycastResults[i].side, distance: raycastResults[i].distance }); break; } } } return faces; } };
Code edit (1 edits merged)
Please save this source code
User prompt
Replace with: function rayCasting() { floorCaster.update(player.x, player.y, player.dir); var raycastResults = []; // Perform raycasting and store results for (var rayIdx = 0; rayIdx < NUM_RAYS; rayIdx++) { var rayAngle = player.dir - HALF_FOV + rayIdx / NUM_RAYS * FOV; var rayDirX = Math.cos(rayAngle); var rayDirY = Math.sin(rayAngle); var hit = castRayDDA(player.x, player.y, rayDirX, rayDirY); raycastResults.push(hit); if (hit.wallHit) { var actualDistance = hit.distance * Math.cos(rayAngle - player.dir); var wallHeight = Math.max(STRIP_WIDTH, Math.min(2732, WALL_HEIGHT_FACTOR / actualDistance)); // Update base wall structure var strip = rayCastView.children[rayIdx]; if (strip && strip.updateStrip) { var stripX = Math.round(rayIdx * STRIP_WIDTH + (2048 - NUM_RAYS * STRIP_WIDTH) / 2); strip.x = stripX; strip.updateStrip(STRIP_WIDTH, wallHeight, rayIdx, hit.wallType, actualDistance, 0, hit.side); } } else { var strip = rayCastView.children[rayIdx]; if (strip && strip.clearStrip) { strip.clearStrip(); } } } // Detect wall faces and render complete textures var wallFaces = wallFaceTracker.detectWallFaces(raycastResults); wallTextureManager.renderWallTextures(wallFaces); renderEntities(); }
Code edit (1 edits merged)
Please save this source code
User prompt
Replace with: var RaycastStrip = Container.expand(function () { var self = Container.call(this); var baseWallSprite = null; self.updateStrip = function (stripWidth, wallHeight, stripIdx, wallType, distance, textureX, side) { // Get base wall sprite from pool if (!baseWallSprite) { baseWallSprite = baseWallSpritePool.getSprite(); self.addChild(baseWallSprite); } // Configure base wall sprite (just for structure/lighting) baseWallSprite.width = stripWidth; baseWallSprite.height = wallHeight; baseWallSprite.x = 0; baseWallSprite.y = (2732 - wallHeight) / 2; // Apply shading var shade = Math.max(0.3, 1 - distance / MAX_RENDER_DISTANCE); if (side === 1) { shade *= 0.7; } baseWallSprite.alpha = shade; baseWallSprite.visible = true; }; self.clearStrip = function () { if (baseWallSprite) { baseWallSpritePool.releaseSprite(baseWallSprite); self.removeChild(baseWallSprite); baseWallSprite = null; } }; return self; });
Code edit (1 edits merged)
Please save this source code
User prompt
Fix it so the player clan slide along the wall without getting stuck.
User prompt
Fix the wall buffer so that the player can’t get too close to the wall but can still slide
Code edit (1 edits merged)
Please save this source code
User prompt
Do not remove the assets themselves, but change the raystrip system to just use one asset instead of all 64
User prompt
Add a single texture overlay system to the raycast walls. Keep the existing 64-tile raystrip system for structure and lighting, but add one seamless wall texture that stretches over each strip using the same distance/height/position calculations. Apply it in the rayCasting() function as an additional sprite layer on top of existing strips with matching shading. Replace 64 assets with 1 for better performance."
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'sourceTexture.baseTexture')' in or related to this line: 'var sourceBaseTexture = sourceTexture.baseTexture;' Line Number: 819
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'overlaySprite.texture.frame = new Rectangle(Math.floor(textureMapX), 0, 1, SEAMLESS_OVERLAY_TEXTURE_HEIGHT)')' in or related to this line: 'overlaySprite.texture.frame = new Rectangle(Math.floor(textureMapX), 0, 1, SEAMLESS_OVERLAY_TEXTURE_HEIGHT);' Line Number: 814
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'sourceTexture.baseTexture')' in or related to this line: 'var sourceBaseTexture = sourceTexture.baseTexture;' Line Number: 819
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'sourceTexture.baseTexture')' in or related to this line: 'var sourceBaseTexture = sourceTexture.baseTexture;' Line Number: 819
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'overlaySprite.texture.frame = new Rectangle(Math.floor(textureMapX), 0, 1, SEAMLESS_OVERLAY_TEXTURE_HEIGHT)')' in or related to this line: 'overlaySprite.texture.frame = new Rectangle(Math.floor(textureMapX), 0, 1, SEAMLESS_OVERLAY_TEXTURE_HEIGHT);' Line Number: 814
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'overlaySprite.texture.frame = new Rectangle(Math.floor(textureMapX), 0, 1, SEAMLESS_OVERLAY_TEXTURE_HEIGHT)')' in or related to this line: 'overlaySprite.texture.frame = new Rectangle(Math.floor(textureMapX), 0, 1, SEAMLESS_OVERLAY_TEXTURE_HEIGHT);' Line Number: 814
User prompt
Implement this.
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'overlaySprite.texture.frame = new Rectangle(Math.floor(textureMapX), 0, 1, SEAMLESS_OVERLAY_TEXTURE_HEIGHT)')' in or related to this line: 'overlaySprite.texture.frame = new Rectangle(Math.floor(textureMapX), 0, 1, SEAMLESS_OVERLAY_TEXTURE_HEIGHT);' Line Number: 814
User prompt
Implement it then olease
Code edit (1 edits merged)
Please save this source code
User prompt
✅ Increase player-wall collision buffer to keep player further from walls
User prompt
Keep players from getting quite so close to walls.
/**** * 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 FloorCaster = Container.expand(function () { var self = Container.call(this); // Screen dimensions var SCREEN_WIDTH = 2048; var SCREEN_HEIGHT = 2732; var HORIZON_Y = SCREEN_HEIGHT / 2; // Create containers for floor and ceiling strips var floorContainer = new Container(); var ceilingContainer = new Container(); self.addChild(floorContainer); self.addChild(ceilingContainer); // Store strip pools to avoid creating/destroying objects var floorStrips = []; var ceilingStrips = []; var stripHeight = 4; // Height of each horizontal strip var numStrips = Math.ceil(HORIZON_Y / stripHeight); // Initialize floor and ceiling rendering self.update = function (playerX, playerY, playerDir) { // Clear existing strips floorContainer.removeChildren(); ceilingContainer.removeChildren(); // Render floor (bottom half of screen) var stripIndex = 0; for (var y = HORIZON_Y; y < SCREEN_HEIGHT; y += stripHeight) { // Calculate distance from horizon var distanceFromHorizon = y - HORIZON_Y; if (distanceFromHorizon <= 0) { continue; } // Calculate world distance for perspective var worldDistance = WALL_HEIGHT_FACTOR / distanceFromHorizon; // Skip if too far away if (worldDistance > MAX_RENDER_DISTANCE) { continue; } // Calculate shade based on distance (darker = further) var shadeFactor = Math.max(0.3, 1 - worldDistance / MAX_RENDER_DISTANCE); // Create or reuse floor strip var floorStrip; if (stripIndex < floorStrips.length) { floorStrip = floorStrips[stripIndex]; } else { floorStrip = LK.getAsset('mapFloor', { anchorX: 0, anchorY: 0 }); floorStrips.push(floorStrip); } // Position and scale the strip floorStrip.x = 0; floorStrip.y = y; floorStrip.width = SCREEN_WIDTH; floorStrip.height = stripHeight; floorStrip.alpha = shadeFactor; // Add to floor container floorContainer.addChild(floorStrip); stripIndex++; } // Render ceiling (top half of screen) stripIndex = 0; for (var y = HORIZON_Y - stripHeight; y >= 0; y -= stripHeight) { // Calculate distance from horizon var distanceFromHorizon = HORIZON_Y - y; if (distanceFromHorizon <= 0) { continue; } // Calculate world distance for perspective var worldDistance = WALL_HEIGHT_FACTOR / distanceFromHorizon; // Skip if too far away if (worldDistance > MAX_RENDER_DISTANCE) { continue; } // Calculate shade based on distance (darker = further) var shadeFactor = Math.max(0.2, 1 - worldDistance / MAX_RENDER_DISTANCE); // Create or reuse ceiling strip var ceilingStrip; if (stripIndex < ceilingStrips.length) { ceilingStrip = ceilingStrips[stripIndex]; } else { ceilingStrip = LK.getAsset('ceiling', { anchorX: 0, anchorY: 0 }); ceilingStrips.push(ceilingStrip); } // Position and scale the strip ceilingStrip.x = 0; ceilingStrip.y = y; ceilingStrip.width = SCREEN_WIDTH; ceilingStrip.height = stripHeight; ceilingStrip.alpha = shadeFactor; // Add to ceiling container ceilingContainer.addChild(ceilingStrip); stripIndex++; } }; 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); // Create animation frame container var frameContainer = new Container(); self.addChild(frameContainer); // Create monster animation frames var monsterFrame1 = LK.getAsset('demonOgreWalk1', { anchorX: 0.5, anchorY: 0.5 }); var monsterFrame2 = LK.getAsset('demonOgreWalk2', { anchorX: 0.5, anchorY: 0.5 }); // Add frames to container frameContainer.addChild(monsterFrame1); frameContainer.addChild(monsterFrame2); // Set initial visibility monsterFrame1.alpha = 1; monsterFrame2.alpha = 0; // Initialize animation state self.currentFrame = 1; self.animationTick = 0; self.mapX = 0; self.mapY = 0; self.health = 3; self.lastMoveTime = 0; self.canSeePlayer = false; self.pathToPlayer = []; self.attackCooldown = 2000; // 2 seconds between attacks self.lastAttackTime = 0; // When monster last attacked self.canAttack = true; // Whether monster can attack // Update animation frames - will be called from game loop self.updateAnimation = function () { self.animationTick++; // Change animation frame every 30 ticks (half second at 60fps) if (self.animationTick >= 15) { self.animationTick = 0; self.currentFrame = self.currentFrame === 1 ? 2 : 1; // Directly change alpha values without tween if (self.currentFrame === 1) { monsterFrame1.alpha = 1; monsterFrame2.alpha = 0; } else { monsterFrame1.alpha = 0; monsterFrame2.alpha = 1; } } }; self.takeDamage = function () { self.health -= 1; LK.getSound('hit').play(); // Visual feedback for hit - flash the monster red LK.effects.flashObject(frameContainer, 0xff0000, 400); return self.health <= 0; }; return self; }); var Particle = Container.expand(function () { var self = Container.call(this); var graphics = self.attachAsset('particleRed', { anchorX: 0.5, anchorY: 0.5 }); // World coordinates and velocities self.worldX = 0; self.worldY = 0; self.worldZ = 0; // Height above the dungeon floor self.vx = 0; // World velocity X self.vy = 0; // World velocity Y self.vz = 0; // World velocity Z (vertical) self.life = 0; self.maxLife = 480; // Approx 8 seconds at 60 FPS (Doubled) self.bounces = 0; // Particle physics constants (world-based) var PARTICLE_WORLD_GRAVITY_EFFECT = 0.0035; // Gravity pulling worldZ down (Reduced by half) var PARTICLE_Z_DAMPING = 0.6; // Energy retained vertically after floor bounce (Increased for higher bounce) var PARTICLE_XY_DAMPING = 0.4; // Energy retained horizontally after wall bounce (Reduced from 0.6) var PARTICLE_GROUND_FRICTION = 0.8; // Friction for vx, vy when hitting floor (Reduced from 0.9) var PARTICLE_MAX_BOUNCES = 4; var PARTICLE_VISUAL_SIZE_AT_UNIT_DISTANCE = 0.08; // Visual size (in world units) when 1 unit away var MAX_RENDER_DISTANCE_PARTICLES = 12; // Max distance to render particles self.init = function (wX, wY, wZ, velX, velY, velZ, particleLife) { self.worldX = wX; self.worldY = wY; self.worldZ = wZ; self.vx = velX; self.vy = velY; self.vz = velZ; self.life = particleLife || self.maxLife; self.bounces = 0; graphics.alpha = 1; // Initial scale will be set in update based on distance self.visible = false; // Will be set true if in FOV during update }; self.update = function () { if (self.life <= 0 || self.bounces >= PARTICLE_MAX_BOUNCES) { self.visible = false; return false; // Indicate inactive } // Store pre-move position for collision response var prevWorldX = self.worldX; var prevWorldY = self.worldY; // Apply world velocities self.worldX += self.vx; self.worldY += self.vy; self.worldZ += self.vz; // Apply gravity to vertical velocity self.vz -= PARTICLE_WORLD_GRAVITY_EFFECT; // Floor bounce logic (worldZ = 0 is the floor) if (self.worldZ < 0) { self.worldZ = 0; self.vz *= -PARTICLE_Z_DAMPING; self.vx *= PARTICLE_GROUND_FRICTION; self.vy *= PARTICLE_GROUND_FRICTION; self.bounces++; if (Math.abs(self.vz) < 0.005 && PARTICLE_WORLD_GRAVITY_EFFECT > Math.abs(self.vz)) { // Come to rest self.vz = 0; } } // Wall bounce logic var currentMapX = Math.floor(self.worldX); var currentMapY = Math.floor(self.worldY); if (currentMapX < 0 || currentMapX >= MAP_SIZE || currentMapY < 0 || currentMapY >= MAP_SIZE || map[currentMapY] && map[currentMapY][currentMapX] && map[currentMapY][currentMapX].type === 1) { self.worldX = prevWorldX; // Revert to position before entering wall self.worldY = prevWorldY; var pt_map_prev_x = Math.floor(prevWorldX); var pt_map_prev_y = Math.floor(prevWorldY); var reflectedX = false; var reflectedY = false; // Check if collision was primarily due to X movement into a wall if (currentMapX !== pt_map_prev_x && map[pt_map_prev_y] && map[pt_map_prev_y][currentMapX] && map[pt_map_prev_y][currentMapX].type === 1) { self.vx *= -PARTICLE_XY_DAMPING; reflectedX = true; } // Check if collision was primarily due to Y movement into a wall if (currentMapY !== pt_map_prev_y && map[currentMapY] && map[currentMapY][pt_map_prev_x] && map[currentMapY][pt_map_prev_x].type === 1) { self.vy *= -PARTICLE_XY_DAMPING; reflectedY = true; } // If hit a corner or exact cause is ambiguous (e.g. started in wall), reflect based on dominant velocity or both if (!reflectedX && !reflectedY) { // This case implies it was already in a wall or hit a corner perfectly. // A simple heuristic: reflect the larger velocity component, or both if similar. if (Math.abs(self.vx) > Math.abs(self.vy) * 1.2) { self.vx *= -PARTICLE_XY_DAMPING; } else if (Math.abs(self.vy) > Math.abs(self.vx) * 1.2) { self.vy *= -PARTICLE_XY_DAMPING; } else { // Similar magnitude or started in wall, reflect both self.vx *= -PARTICLE_XY_DAMPING; self.vy *= -PARTICLE_XY_DAMPING; } } self.bounces++; } // Calculate screen position and scale (similar to projectiles/monsters) var dx = self.worldX - player.x; var dy = self.worldY - player.y; var distToPlayerPlane = Math.sqrt(dx * dx + dy * dy); if (distToPlayerPlane < 0.1) { distToPlayerPlane = 0.1; } // Avoid division by zero / extreme scales 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 && distToPlayerPlane < MAX_RENDER_DISTANCE_PARTICLES) { self.visible = true; // Screen X self.x = 2048 / 2 + angle / HALF_FOV * (2048 / 2); // Screen Y: based on horizon, distance, and particle's worldZ var screenY_horizon = 2732 / 2; var z_to_screen_pixels_factor = WALL_HEIGHT_FACTOR; // Factor for scaling worldZ to screen pixels, effectively how many screen pixels 1 world unit of height is at 1 world unit of distance. var player_camera_height_projection_factor = WALL_HEIGHT_FACTOR * 0.5; // This factor determines how the floor plane "drops" from the horizon based on distance, simulating camera height. // Using 0.5 of WALL_HEIGHT_FACTOR aligns it with how treasure bottoms are projected. // Calculate the screen Y position for a point that is ON THE FLOOR (i.e., worldZ = 0) // at the particle's current distance (distToPlayerPlane). // Closer points on the floor will have a larger Y value (lower on screen). var screenY_for_floor_at_dist = screenY_horizon + player_camera_height_projection_factor / distToPlayerPlane; // Calculate the screen Y offset caused by the particle's actual height (self.worldZ) above the floor. // This offset is relative to the screenY_for_floor_at_dist. var screenY_offset_due_to_worldZ = self.worldZ * z_to_screen_pixels_factor / distToPlayerPlane; // The final screen Y is the floor's projected Y minus the offset due to the particle's height. self.y = screenY_for_floor_at_dist - screenY_offset_due_to_worldZ; // Scale based on distance and particle's inherent visual size var particleAssetBaseSize = 25.0; // From LK.init.shape('particleRed', {width:25 ...}) var effectiveScreenSize = PARTICLE_VISUAL_SIZE_AT_UNIT_DISTANCE * WALL_HEIGHT_FACTOR / distToPlayerPlane; var screenScaleFactor = Math.max(0.05, effectiveScreenSize / particleAssetBaseSize); graphics.scale.set(screenScaleFactor); var lifeRatio = Math.max(0, self.life / self.maxLife); graphics.alpha = lifeRatio * 0.8 + 0.2; // Fade out but maintain some visibility } else { self.visible = false; } self.life--; if (self.life <= 0 || self.bounces >= PARTICLE_MAX_BOUNCES) { self.visible = false; return false; // Indicate inactive } return true; // Indicate active }; return self; }); var Projectile = Container.expand(function () { var self = Container.call(this); var projectileSprite = self.attachAsset('projectile', { anchorX: 0.5, anchorY: 0.5 }); // World position and movement properties self.worldX = 0; self.worldY = 0; self.dirX = 0; self.dirY = 0; self.speed = 0.0375; // World units per tick (reduced to 1/4 of original) self.active = false; self.distance = 0; self.maxDistance = 10; // Maximum travel distance in world units // Initialize projectile self.fire = function (screenX, screenY) { // Start at player position, slightly offset in the firing direction self.worldX = player.x + Math.cos(player.dir) * 0.3; self.worldY = player.y + Math.sin(player.dir) * 0.3; // Direction is player's current facing direction self.dirX = Math.cos(player.dir); self.dirY = Math.sin(player.dir); self.active = true; self.distance = 0; // Play attack sound LK.getSound('attack').play(); // Scale for visual effect projectileSprite.scale.set(0.2, 0.2); // Store initial screen coordinates from hand self.initialScreenX = screenX || 2048 / 2; self.initialScreenY = screenY || 2732 - 300; // Set initial position to hand location self.x = self.initialScreenX; self.y = self.initialScreenY; self.visible = true; }; // Update projectile position and check for collisions self.update = function (deltaTime) { if (!self.active) { return false; } // Move projectile in world space self.worldX += self.dirX * self.speed; self.worldY += self.dirY * self.speed; // Track distance traveled self.distance += self.speed; // Check for wall collision var mapX = Math.floor(self.worldX); var mapY = Math.floor(self.worldY); // If hit wall or traveled max distance if (mapX < 0 || mapX >= MAP_SIZE || mapY < 0 || mapY >= MAP_SIZE || map[mapY][mapX].type === 1 || self.distance >= self.maxDistance) { return true; // Remove projectile } // Position on screen based on player view (raycasting principles) self.updateScreenPosition(); return false; // Keep projectile }; // Calculate screen position from world position self.updateScreenPosition = function () { // Vector from player to projectile var dx = self.worldX - player.x; var dy = self.worldY - player.y; // Distance from player to projectile var dist = Math.sqrt(dx * dx + dy * dy); // Angle from player to projectile var angle = Math.atan2(dy, dx) - player.dir; // Normalize angle (-PI to PI) while (angle < -Math.PI) { angle += Math.PI * 2; } while (angle > Math.PI) { angle -= Math.PI * 2; } // Check if projectile is in field of view if (Math.abs(angle) < HALF_FOV) { // Calculate screen X based on angle in FOV self.x = 2048 / 2 + angle / HALF_FOV * (2048 / 2); // Calculate target Y position (center of screen with slight adjustment) var targetY = 2732 / 2 + 20 - WALL_HEIGHT_FACTOR / dist * 0.1; // Calculate transition factor based on distance // As distance increases, move closer to target Y var transitionFactor = Math.min(1.0, self.distance * 1); // Interpolate between initial hand Y and target Y self.y = self.initialScreenY * (1 - transitionFactor) + targetY * transitionFactor; // Scale based on distance var scale = Math.max(0.1, 2 / dist); projectileSprite.scale.set(scale, scale); self.visible = true; } else { self.visible = false; } }; return self; }); var RaycastStrip = Container.expand(function () { var self = Container.call(this); var activeWallSprite = null; // The currently displayed wall sprite instance var currentTileAssetName = null; // Stores the asset name of activeWallSprite, e.g., "walltile5" self.updateStrip = function (stripWidth, wallHeight, stripIdx, wallType, distance, textureX, side, tileIndex) { // self.x is now set in rayCasting for correct centering; do not set here // Validate tileIndex before constructing asset name if (tileIndex < 1 || tileIndex > 64 || isNaN(tileIndex)) { console.error("RaycastStrip: Invalid tileIndex received: " + tileIndex + ". Clearing strip."); self.clearStrip(); // Clear the strip if tileIndex is invalid return; } var newTileAssetName = 'walltile' + tileIndex; // If the current sprite is active but not the correct type for the new tileIndex, release it. if (activeWallSprite && currentTileAssetName !== newTileAssetName) { wallSpritePoolManager.releaseSprite(activeWallSprite, currentTileAssetName); self.removeChild(activeWallSprite); // Detach from this strip's container activeWallSprite = null; currentTileAssetName = null; // Clear current asset name } // If no sprite is active (either first time, or after releasing/clearing), get one from the pool. if (!activeWallSprite) { activeWallSprite = wallSpritePoolManager.getSprite(newTileAssetName); if (activeWallSprite) { self.addChild(activeWallSprite); // Add the new/reused sprite to this strip's container currentTileAssetName = newTileAssetName; } else { // This can happen if wallSpritePoolManager.getSprite returns null (e.g., invalid asset name) console.error("RaycastStrip: Failed to get sprite for " + newTileAssetName + " from pool."); self.clearStrip(); // Ensure strip is clean if sprite acquisition fails return; // Cannot render this strip without a sprite } } // Configure the active sprite's properties (position, size, appearance) activeWallSprite.width = stripWidth; activeWallSprite.height = wallHeight; // Position sprite relative to the RaycastStrip container's origin activeWallSprite.x = 0; activeWallSprite.y = (2732 - wallHeight) / 2; // Vertically center the wall segment // Apply distance-based shading var shade = Math.max(0.3, 1 - distance / MAX_RENDER_DISTANCE); if (side === 1) { // Conventionally, side 1 walls (e.g., horizontal, further from light) are darker shade *= 0.7; } activeWallSprite.alpha = shade; activeWallSprite.visible = true; // Ensure the sprite is visible }; // Clears the strip by releasing its active sprite back to the pool. self.clearStrip = function () { if (activeWallSprite) { wallSpritePoolManager.releaseSprite(activeWallSprite, currentTileAssetName); self.removeChild(activeWallSprite); // Detach from container activeWallSprite = null; currentTileAssetName = null; } // Optionally, could make the RaycastStrip container itself invisible: // self.visible = false; // But simply having no visible children often suffices. }; // Note: LK's game reset mechanism (re-initializing Game class) typically handles // destruction of game objects. If RaycastStrip instances were managed in a way // that required manual cleanup of their sprites before game reset, a specific // destroy method could be added here. For now, clearStrip and parent destruction // should manage pooled sprites correctly. 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 ****/ var baseWallSpritePool = { pool: [], getSprite: function getSprite() { if (this.pool.length > 0) { return this.pool.pop(); } else { return LK.getAsset('baseWallStructure', { anchorX: 0, anchorY: 0, visible: false }); } }, releaseSprite: function releaseSprite(sprite) { sprite.visible = false; sprite.alpha = 1.0; sprite.scale.set(1.0, 1.0); sprite.x = 0; sprite.y = 0; this.pool.push(sprite); } }; // Wall face tracking for complete textures var wallFaceTracker = { currentFaces: [], detectWallFaces: function detectWallFaces(raycastResults) { var faces = []; var currentFace = null; for (var i = 0; i < raycastResults.length; i++) { var hit = raycastResults[i]; if (hit.wallHit) { var wallId = hit.mapX + "_" + hit.mapY + "_" + hit.side; if (!currentFace || currentFace.wallId !== wallId) { // New wall face detected if (currentFace) { currentFace.endRay = i - 1; faces.push(currentFace); } currentFace = { wallId: wallId, startRay: i, mapX: hit.mapX, mapY: hit.mapY, side: hit.side, distance: hit.distance }; } else { // Same face continues currentFace.distance = Math.min(currentFace.distance, hit.distance); } } else if (currentFace) { // End of current face currentFace.endRay = i - 1; faces.push(currentFace); currentFace = null; } } // Don't forget the last face if (currentFace) { currentFace.endRay = raycastResults.length - 1; faces.push(currentFace); } return faces; } }; var dynamicEntitiesContainer; var wallSpritePoolManager = { pools: function () { // IIFE to pre-populate pools for each wall tile type, with a fixed number of pre-allocated sprites var p = {}; var PREALLOCATE_PER_TILE = 32; // Increased from 12 to reduce pop-in by having more sprites readily available for (var i = 1; i <= 64; i++) { var assetName = 'walltile' + i; p[assetName] = []; for (var j = 0; j < PREALLOCATE_PER_TILE; j++) { var sprite = LK.getAsset(assetName, { anchorX: 0, anchorY: 0, visible: false }); p[assetName].push(sprite); } } return p; }(), getSprite: function getSprite(tileAssetName) { // Validate tileAssetName and ensure its pool exists if (!this.pools[tileAssetName]) { var isValidTile = tileAssetName && tileAssetName.startsWith('walltile'); var tileNumStr = isValidTile ? tileAssetName.substring(8) : ""; var tileNum = parseInt(tileNumStr); isValidTile = isValidTile && !isNaN(tileNum) && tileNum >= 1 && tileNum <= 64; if (isValidTile) { this.pools[tileAssetName] = []; // Create pool if valid but missing console.warn("WallSpritePoolManager: Created pool on-the-fly for: " + tileAssetName); } else { console.error("WallSpritePoolManager: Attempted to get sprite for invalid asset name: " + tileAssetName); return null; // Invalid asset name, cannot provide a sprite } } if (this.pools[tileAssetName].length > 0) { var sprite = this.pools[tileAssetName].pop(); // Sprites are now cleaned on release, so no need to reset alpha/scale here. // anchorX, anchorY are set at initial LK.getAsset and should persist. // Visibility will be managed by RaycastStrip. return sprite; } else { // Pool is empty, create a new sprite on demand and return it var newSprite = LK.getAsset(tileAssetName, { anchorX: 0, anchorY: 0, visible: false }); return newSprite; } }, releaseSprite: function releaseSprite(sprite, tileAssetName) { if (!sprite || !tileAssetName) { console.error("WallSpritePoolManager: Invalid sprite or tileAssetName for release."); return; } // Validate tileAssetName and ensure its pool exists for release if (!this.pools[tileAssetName]) { var isValidTile = tileAssetName && tileAssetName.startsWith('walltile'); var tileNumStr = isValidTile ? tileAssetName.substring(8) : ""; var tileNum = parseInt(tileNumStr); isValidTile = isValidTile && !isNaN(tileNum) && tileNum >= 1 && tileNum <= 64; if (isValidTile) { this.pools[tileAssetName] = []; // Create pool if valid but missing console.warn("WallSpritePoolManager: Releasing sprite to a newly created pool for: " + tileAssetName); } else { console.error("WallSpritePoolManager: Attempted to release sprite for invalid asset name: " + tileAssetName); // If not pooling (e.g. invalid asset), it might need explicit destruction. // However, standard behavior is just not to pool it. return; } } // Sprite is assumed to be already removed from its parent by RaycastStrip. // Thoroughly reset sprite properties to a clean state: sprite.visible = false; sprite.alpha = 1.0; sprite.scale.set(1.0, 1.0); sprite.rotation = 0; sprite.x = 0; sprite.y = 0; // sprite.tint = 0xFFFFFF; // Reset tint if it were used on these sprites // Always push to pool, even if pool is larger than preallocated this.pools[tileAssetName].push(sprite); } }; // Game constants var MAP_SIZE = 16; var particlePoolManager; var particleExplosionContainer; // Container for all active particles from explosions var CELL_SIZE = 20; var MINI_MAP_SCALE = 1; var STRIP_WIDTH = 8; // Increase from 8 to 16 (half as many rays) 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_AGGRO_RANGE = 9.0; // Max distance at which monsters will notice and start chasing the player. var MIN_MONSTER_PLAYER_DISTANCE = 0.5; // Monsters stop if they get closer than this (e.g., overshoot); this is within attack range. var MAX_MONSTER_PLAYER_ENGAGEMENT_DISTANCE = 0.5; var WALL_BUFFER = 0.20; // Minimum distance from player center to wall cell's effective edge var MONSTER_COUNT = 8; // Increased by 50% from 5, rounded up var TREASURE_COUNT = 10; // Game state var map = []; var floorCaster; 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 wallSegments = []; var lastWallCheck = []; 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(); particlePoolManager = { pool: [], activeParticles: [], maxParticles: 750, // Increased from 300 to support more particles // Increased max particles getParticle: function getParticle() { var particle; if (this.pool.length > 0) { particle = this.pool.pop(); } else { particle = new Particle(); } this.activeParticles.push(particle); // The particle will be added to particleExplosionContainer by createParticleExplosion return particle; }, releaseParticle: function releaseParticle(particle) { // Remove from activeParticles var index = this.activeParticles.indexOf(particle); if (index !== -1) { this.activeParticles.splice(index, 1); } // Remove from its parent container (particleExplosionContainer) if (particle.parent) { particle.parent.removeChild(particle); } if (this.pool.length < this.maxParticles) { this.pool.push(particle); } else { // If pool is full, LK will garbage collect if it has no parent. // Explicitly calling destroy isn't standard in LK for pooled objects unless necessary. } }, updateActiveParticles: function updateActiveParticles() { for (var i = this.activeParticles.length - 1; i >= 0; i--) { var particle = this.activeParticles[i]; if (!particle.update()) { // Particle.update returns false when life is over this.releaseParticle(particle); } } } }; function createParticleExplosion(worldExplosionX, worldExplosionY, count) { if (!particleExplosionContainer) { particleExplosionContainer = new Container(); // Add to projectileLayer so particles appear above game world elements but potentially below UI projectileLayer.addChild(particleExplosionContainer); } var worldExplosionZ = 0.5; // Assume explosion originates roughly half a unit above the floor for (var i = 0; i < count; i++) { var particle = particlePoolManager.getParticle(); // Calculate 3D spherical distribution for initial velocities var phi = Math.random() * Math.PI * 2; // Angle in XY plane (0 to 2PI) var theta = Math.acos(Math.random() * 2 - 1); // Angle from Z axis (0 to PI), for uniform sphere var speed = (Math.random() * 0.03 + 0.015) * (player.level > 3 ? 1.5 : 1); // Base world speed (halved), faster for higher levels var vx = Math.sin(theta) * Math.cos(phi) * speed; var vy = Math.sin(theta) * Math.sin(phi) * speed; var vz = Math.cos(theta) * speed + 0.03; // Add a slight general upward bias // Randomize lifespan var life = 360 + Math.random() * 240; // Life between ~6 to ~10 seconds, to match doubled maxLife particle.init(worldExplosionX, worldExplosionY, worldExplosionZ, vx, vy, vz, life); particleExplosionContainer.addChild(particle); } } // 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 floor/ceiling caster first (should be below walls) floorCaster = new FloorCaster(); gameLayer.addChild(floorCaster); // Create the rayCast view container rayCastView = new Container(); gameLayer.addChild(rayCastView); // Create raycast strips - assign walltiles in cycling pattern for (var i = 0; i < NUM_RAYS; i++) { var strip = new RaycastStrip(); // Remove the walltileIndex parameter rayCastView.addChild(strip); } // Initialize container for depth-sorted dynamic entities dynamicEntitiesContainer = new Container(); gameLayer.addChild(dynamicEntitiesContainer); // 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 visuals, data structures 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 = []; // 1. Initialize the map: all cells are walls 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; cell.setType(1); // Initialize as wall map[y][x] = cell; miniMap.addChild(cell); // Add to minimap } } // 2. Implement Recursive Backtracker (a form of Growing Tree) for maze generation var stack = []; var startX = 1; // Player's typical start X (must be odd if using 2-step moves on even grid size for some variants, but here it's cell index) var startY = 1; // Player's typical start Y map[startY][startX].setType(0); // Mark starting cell as floor stack.push({ x: startX, y: startY }); while (stack.length > 0) { var current = stack[stack.length - 1]; // Get current cell (peek) var potentialMoves = [{ dx: 0, dy: -2, wallXOffset: 0, wallYOffset: -1 }, // North { dx: 2, dy: 0, wallXOffset: 1, wallYOffset: 0 }, // East { dx: 0, dy: 2, wallXOffset: 0, wallYOffset: 1 }, // South { dx: -2, dy: 0, wallXOffset: -1, wallYOffset: 0 } // West ]; // Shuffle potential moves to ensure randomness for (var i = potentialMoves.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = potentialMoves[i]; potentialMoves[i] = potentialMoves[j]; potentialMoves[j] = temp; } var moved = false; for (var i = 0; i < potentialMoves.length; i++) { var move = potentialMoves[i]; var nextX = current.x + move.dx; var nextY = current.y + move.dy; var wallBetweenX = current.x + move.wallXOffset; var wallBetweenY = current.y + move.wallYOffset; // Check bounds for the next cell (2 steps away) if (nextX >= 0 && nextX < MAP_SIZE && nextY >= 0 && nextY < MAP_SIZE && map[nextY][nextX].type === 1) { // Check bounds for the wall cell (1 step away) - should always be valid if nextX/Y is. // Carve path through the wall cell and mark the next cell as floor map[wallBetweenY][wallBetweenX].setType(0); map[nextY][nextX].setType(0); stack.push({ x: nextX, y: nextY }); // Move to the next cell moved = true; break; // Break after finding a valid move } } if (!moved) { stack.pop(); // No valid moves, backtrack } } // 3. Explicitly set outer border walls. // The maze algorithm might carve up to the edges, so we reinforce them. for (y = 0; y < MAP_SIZE; y++) { if (map[y][0].type === 0 && !(y === startY && startX === 0)) { map[y][0].setType(1); } // Avoid walling start if it's on edge if (map[y][MAP_SIZE - 1].type === 0 && !(y === startY && startX === MAP_SIZE - 1)) { map[y][MAP_SIZE - 1].setType(1); } } for (x = 0; x < MAP_SIZE; x++) { if (map[0][x].type === 0 && !(x === startX && startY === 0)) { map[0][x].setType(1); } if (map[MAP_SIZE - 1][x].type === 0 && !(x === startX && startY === MAP_SIZE - 1)) { map[MAP_SIZE - 1][x].setType(1); } } // Ensure player start is clear, especially if it was on an edge that got re-walled. // (Our startX=1, startY=1 is not on edge, so this is mostly failsafe) map[startY][startX].setType(0); // 4. Ensure all parts of the map are connected after maze generation. 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(); // Spawn monster in the center of the cell monster.mapX = x + 0.5; monster.mapY = y + 0.5; monster.health = 2 + Math.floor(player.level / 3); // Monsters get tougher with level monsters.push(monster); dynamicEntitiesContainer.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); dynamicEntitiesContainer.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; dynamicEntitiesContainer.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() { // Get a small random x offset between -40 and 40 pixels var randomXOffset = Math.random() * 80 - 40; // Store original X position if not set yet if (globalRightHand.originalX === undefined) { globalRightHand.originalX = globalRightHand.x; } // Slow scale pulse animation with random left/right movement tween(globalRightHand, { scaleX: 1.1, scaleY: 1.1, x: globalRightHand.originalX + randomXOffset, y: 2732 - 15 // Float up slightly from base position }, { duration: 1800, easing: tween.easeInOut, onFinish: function onFinish() { // Get another random x offset for the return animation var returnRandomXOffset = Math.random() * 40 - 20; tween(globalRightHand, { scaleX: 1.1, scaleY: 1.1, x: globalRightHand.originalX + returnRandomXOffset, y: 2732 // Return to original position }, { duration: 1800, easing: tween.easeInOut, onFinish: _animateHand }); } }); }; // Start the hand animation _animateHand(); } function castRayDDA(startX, startY, rayDirX, rayDirY) { var mapX = Math.floor(startX); var mapY = Math.floor(startY); var deltaDistX = Math.abs(1 / rayDirX); var deltaDistY = Math.abs(1 / rayDirY); var hit = false; var side; // 0 for vertical wall, 1 for horizontal wall var stepX, stepY; var sideDistX, sideDistY; if (rayDirX < 0) { stepX = -1; sideDistX = (startX - mapX) * deltaDistX; } else { stepX = 1; sideDistX = (mapX + 1.0 - startX) * deltaDistX; } if (rayDirY < 0) { stepY = -1; sideDistY = (startY - mapY) * deltaDistY; } else { stepY = 1; sideDistY = (mapY + 1.0 - startY) * deltaDistY; } // Perform DDA while (!hit) { if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; } else { sideDistY += deltaDistY; mapY += stepY; side = 1; } // Check if ray hit a wall or went out of bounds if (mapX < 0 || mapX >= MAP_SIZE || mapY < 0 || mapY >= MAP_SIZE) { hit = true; } else if (map[mapY] && map[mapY][mapX] && map[mapY][mapX].type === 1) { hit = true; } } // Calculate distance var distance; if (side === 0) { distance = (mapX - startX + (1 - stepX) / 2) / rayDirX; } else { distance = (mapY - startY + (1 - stepY) / 2) / rayDirY; } return { wallHit: hit, distance: distance, side: side, wallType: hit && mapX >= 0 && mapX < MAP_SIZE && mapY >= 0 && mapY < MAP_SIZE && map[mapY] && map[mapY][mapX] ? map[mapY][mapX].type : 1, mapX: mapX, mapY: mapY }; } function rayCasting() { floorCaster.update(player.x, player.y, player.dir); for (var rayIdx = 0; rayIdx < NUM_RAYS; rayIdx++) { var rayAngle = player.dir - HALF_FOV + rayIdx / NUM_RAYS * FOV; var rayDirX = Math.cos(rayAngle); var rayDirY = Math.sin(rayAngle); var hit = castRayDDA(player.x, player.y, rayDirX, rayDirY); if (hit.wallHit) { var actualDistance = hit.distance * Math.cos(rayAngle - player.dir); // Clamp wallHeight to avoid extreme values that cause scanlines/distortion var unclampedWallHeight = WALL_HEIGHT_FACTOR / actualDistance; var wallHeight = Math.max(STRIP_WIDTH, Math.min(2732, unclampedWallHeight)); // Lock texture to wall grid position var wallX; if (hit.side === 0) { wallX = player.y + hit.distance * rayDirY; } else { wallX = player.x + hit.distance * rayDirX; } var texturePosition = wallX * 64; var tileIndex = Math.floor(texturePosition) % 64 + 1; if (hit.side === 0 && rayDirX > 0) { tileIndex = 65 - tileIndex; } if (hit.side === 1 && rayDirY < 0) { tileIndex = 65 - tileIndex; } // Center the strips horizontally so the first strip is at the left edge and the last at the right edge var strip = rayCastView.children[rayIdx]; if (strip && strip.updateStrip) { // Calculate centered x position for the strip var stripX = Math.round(rayIdx * STRIP_WIDTH + (2048 - NUM_RAYS * STRIP_WIDTH) / 2); strip.x = stripX; strip.updateStrip(STRIP_WIDTH, wallHeight, rayIdx, hit.wallType, actualDistance, 0, hit.side, tileIndex); // --- Anti-scanline blending: blend with previous strip if wallHeight difference is large --- if (rayIdx > 0) { var prevStrip = rayCastView.children[rayIdx - 1]; if (prevStrip && prevStrip.children && prevStrip.children.length > 0 && strip.children && strip.children.length > 0) { var prevWall = prevStrip.children[0]; var currWall = strip.children[0]; // If wallHeight difference is large, blend the bottom/top edge alpha for smoothness var prevHeight = prevWall.height; var currHeight = currWall.height; var heightDiff = Math.abs(prevHeight - currHeight); if (heightDiff > STRIP_WIDTH * 2) { // Blend alpha at the edge to reduce scanline var blendAlpha = 0.7; if (prevHeight > currHeight) { prevWall.alpha = prevWall.alpha * blendAlpha; } else { currWall.alpha = currWall.alpha * blendAlpha; } } } } } } else { // No wall hit for this ray, so clear the strip to release its sprite if (strip && strip.clearStrip) { strip.clearStrip(); } } } renderEntities(); } function renderEntities() { var allDynamicEntities = monsters.concat(treasures); if (gate) { allDynamicEntities.push(gate); } var visibleEntities = []; for (var i = 0; i < allDynamicEntities.length; i++) { var entity = allDynamicEntities[i]; entity.visible = false; // Hide by default var dx = entity.mapX - player.x; var dy = entity.mapY - player.y; var dist = Math.sqrt(dx * dx + dy * dy); entity.renderDist = dist; // Store distance for sorting if (dist >= MAX_RENDER_DISTANCE) { // Too far to render continue; } var angle = Math.atan2(dy, dx) - player.dir; // Normalize angle to be within -PI to PI range while (angle < -Math.PI) { angle += Math.PI * 2; } while (angle > Math.PI) { angle -= Math.PI * 2; } if (Math.abs(angle) < HALF_FOV) { // Check if entity is within Field of View var rayHit = castRayToPoint(player.x, player.y, entity.mapX, entity.mapY); // Check if there's a clear Line of Sight to the entity // (dist - 0.5 is a small tolerance, e.g. for entity's own depth/size) if (!rayHit.hit || rayHit.dist > dist - 0.5) { entity.visible = true; var screenX = (0.5 + angle / FOV) * 2048; // Project to screen X var height = WALL_HEIGHT_FACTOR / dist; // Calculate perceived height var scale = height / 100; // Assuming base asset height is 100 for scaling entity.x = screenX; // Adjust Y position: Treasures are on the floor, Monsters/Gates are vertically centered if (entity instanceof Treasure) { entity.y = 2732 / 2 + height / 2; } else { // Monster or Gate entity.y = 2732 / 2; } entity.scale.set(scale, scale); visibleEntities.push(entity); } } } // Sort visible entities by distance: farthest entities first (larger distance values) // This ensures that when added to the container, farthest are added first, // and closest are added last (rendered on top). visibleEntities.sort(function (a, b) { return b.renderDist - a.renderDist; }); // Clear the container and re-add entities in the new sorted order dynamicEntitiesContainer.removeChildren(); for (var j = 0; j < visibleEntities.length; j++) { dynamicEntitiesContainer.addChild(visibleEntities[j]); } } 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 canMoveTo(targetX, targetY) { var cellX = Math.floor(targetX); var cellY = Math.floor(targetY); // Check bounds: if target is outside map, cannot move. if (cellX < 0 || cellX >= MAP_SIZE || cellY < 0 || cellY >= MAP_SIZE || !map[cellY] || !map[cellY][cellX]) { return false; } // Check if the target cell itself is a wall. Player cannot be inside a wall cell. if (map[cellY][cellX].type === 1) { return false; } // Check all 8 surrounding cells (and the target cell itself, though covered above for its type) // to ensure the player maintains WALL_BUFFER distance from any wall cell's center. // This loop effectively checks a 3x3 grid centered on (cellX, cellY). for (var offsetY = -1; offsetY <= 1; offsetY++) { for (var offsetX = -1; offsetX <= 1; offsetX++) { var checkMapX = cellX + offsetX; var checkMapY = cellY + offsetY; if (checkMapX >= 0 && checkMapX < MAP_SIZE && checkMapY >= 0 && checkMapY < MAP_SIZE && map[checkMapY] && map[checkMapY][checkMapX] && map[checkMapY][checkMapX].type === 1) { // This is a wall cell. Calculate its center. var wallCellCenterX = checkMapX + 0.5; var wallCellCenterY = checkMapY + 0.5; // Calculate squared distance from player's target position to the center of this wall cell. var distSqToWallCenter = Math.pow(targetX - wallCellCenterX, 2) + Math.pow(targetY - wallCellCenterY, 2); // Minimum distance required: 0.5 (to clear half-width of wall cell) + WALL_BUFFER. var minRequiredDist = 0.5 + WALL_BUFFER; if (distSqToWallCenter < minRequiredDist * minRequiredDist) { return false; // Player is too close to this wall cell. } } } } return true; // No collisions detected with buffer. } 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) { // Apply a ramping function to joystick X input var rampedTurnAmount = Math.sign(currentJoystickX) * Math.pow(Math.abs(currentJoystickX), 2); // Square the value while preserving sign turnAmount = rampedTurnAmount * turnSpeed * 2; // Multiply by turnSpeed and adjust sensitivity multiplier (e.g., 2) 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 and sliding var inputMoveIntent = didMove; // 'didMove' here reflects input intent from controls didMove = false; // Reset 'didMove' to track actual movement on the map this frame var targetX = player.x + dx; var targetY = player.y + dy; var currentX = player.x; // Position before this frame's movement attempt var currentY = player.y; // Attempt to move along X-axis if (dx !== 0) { // Only check if there's horizontal input if (canMoveTo(targetX, currentY)) { player.x = targetX; } } // Attempt to move along Y-axis // player.x here is either currentX (if X-move was blocked/not attempted) // or targetX (if X-move succeeded). This allows sliding. if (dy !== 0) { // Only check if there's vertical input if (canMoveTo(player.x, targetY)) { player.y = targetY; } } // Check if actual movement occurred by comparing with position before this frame's attempt if (player.x !== currentX || player.y !== currentY) { didMove = true; // Actual movement on the map happened } // Play walk sound if there was an input intent AND actual movement occurred if (didMove && inputMoveIntent && 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; // Create and fire projectile var projectile = new Projectile(); projectile.fire(globalRightHand.x, globalRightHand.y - 400); // Add hand recoil animation tween(globalRightHand, { y: 2732 - 50, rotation: -0.1 }, { duration: 100, onFinish: function onFinish() { tween(globalRightHand, { y: 2732, rotation: 0 }, { duration: 300 }); } }); // Add to projectile layer projectiles.push(projectile); projectileLayer.addChild(projectile); // Update attack button visual var attackButton = controlButtons.attack; attackButton.updateCooldown(0); // Cooldown animation 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]; if (monster.visible) { // Get the vector from player to monster in world space var monsterDx = monster.mapX - player.x; var monsterDy = monster.mapY - player.y; // Calculate distance to monster in world space var monsterDist = Math.sqrt(monsterDx * monsterDx + monsterDy * monsterDy); // Calculate the projectile's travel distance in world units var projectileWorldDist = projectile.distance; // Calculate a hit window that gets smaller as monster is closer // This is the reverse of what we had before - closer monsters need more precise hits var depthAccuracy = Math.min(1.0, projectileWorldDist / monsterDist); // Adjust hit window based on visual size of monster // Monster scale is determined by height / 100 in renderEntities var monsterScale = WALL_HEIGHT_FACTOR / (monsterDist * 100); // Calculate hit window based on screen position and monster size var screenFactor = 1 - Math.abs(monster.x - projectile.x) / (2048 / 2); // Calculate hit threshold as difference between projectile world distance and monster distance // Smaller difference = more likely hit var distanceDifference = Math.abs(projectileWorldDist - monsterDist); // Hit if projectile is close enough to monster's actual distance // AND if projectile is visually aligned with monster on screen if (distanceDifference < 0.5 && screenFactor > 0.7) { hitMonster = true; hitMonsterIndex = j; break; } } } // Handle monster hit logic (unchanged) if (hitMonster && hitMonsterIndex !== -1) { var monster = monsters[hitMonsterIndex]; var killed = monster.takeDamage(); if (killed) { // Create particle explosion at monster's world position createParticleExplosion(monster.mapX, monster.mapY, 250); // Pass world X, Y map[Math.floor(monster.mapY)][Math.floor(monster.mapX)].removeMonster(); monster.destroy(); monsters.splice(hitMonsterIndex, 1); player.score += 10; updateUI(); checkLevelCompletion(); } projectile.destroy(); projectiles.splice(i, 1); } } } function checkMonsterCollisions() { var currentTime = Date.now(); 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 && monster.canAttack) { // 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; // Set monster attack on cooldown monster.canAttack = false; monster.lastAttackTime = currentTime; // Reset monster attack after cooldown LK.setTimeout(function () { monster.canAttack = true; }, monster.attackCooldown); // Visual feedback for monster attack tween(monster, { alpha: 0.5 }, { duration: 200, onFinish: function onFinish() { tween(monster, { alpha: 1 }, { duration: 200 }); } }); // 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); // Render the scene first so monster visibility is calculated rayCasting(); // Update monsters (only move every few ticks to make movement slower) if (LK.ticks % 15 === 0) { for (var i = 0; i < monsters.length; i++) { MonsterAI.moveTowardsPlayer(monsters[i], player.x, player.y, map); } } // Update monster animations only if they're moving and visible for (var i = 0; i < monsters.length; i++) { // Check if monster is visible and has moved recently if (monsters[i].visible && monsters[i].mapX !== monsters[i].lastMoveX || monsters[i].mapY !== monsters[i].lastMoveY) { monsters[i].updateAnimation(); // Store current position for next comparison monsters[i].lastMoveX = monsters[i].mapX; monsters[i].lastMoveY = monsters[i].mapY; } } // Update projectiles updateProjectiles(deltaTime); // Update particle explosions particlePoolManager.updateActiveParticles(); }; // 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) { // Get monster's current map position var monsterX = monster.mapX; var monsterY = monster.mapY; // Vector from monster to player (in game world coordinates) var dx = playerX - monsterX; var dy = playerY - monsterY; // Calculate distance to player var dist = Math.sqrt(dx * dx + dy * dy); // Only chase if within aggro range if (dist > MONSTER_AGGRO_RANGE) { return; } // Check if player is visible using ray casting var rayHit = castRayToPoint(monsterX, monsterY, playerX, playerY); if (rayHit.hit && rayHit.dist < dist - 0.5) { // Wall is blocking line of sight, monster should stop. return; } // New: Maintain optimal distance from player // If monster is too close to the player, it should stop. if (dist < MIN_MONSTER_PLAYER_DISTANCE) { // Monster is too close, stop. // A potential future enhancement could be to make the monster step back slightly. return; } else if (dist <= MAX_MONSTER_PLAYER_ENGAGEMENT_DISTANCE) { // Monster is within the ideal engagement range (not too close, not too far), stop. // This allows the monster to hold a position from which it can attack if the player approaches. return; } // If monster is further than MAX_MONSTER_PLAYER_ENGAGEMENT_DISTANCE (but within AGGRO_RANGE and has Line of Sight), // it will proceed to move towards the player. // Normalize direction vector for movement var length = Math.sqrt(dx * dx + dy * dy); if (length > 0) { dx /= length; dy /= length; } // Movement speed (doubled from 0.1 to 0.2) var moveSpeed = 0.2; // 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) { // Check if another monster is already in this cell var cellOccupied = false; for (var i = 0; i < monsters.length; i++) { var otherMonster = monsters[i]; if (otherMonster !== monster && Math.floor(otherMonster.mapX) === cellX && Math.floor(otherMonster.mapY) === cellY) { cellOccupied = true; break; } } if (!cellOccupied) { // 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
@@ -741,8 +741,73 @@
/****
* Game Code
****/
+var baseWallSpritePool = {
+ pool: [],
+ getSprite: function getSprite() {
+ if (this.pool.length > 0) {
+ return this.pool.pop();
+ } else {
+ return LK.getAsset('baseWallStructure', {
+ anchorX: 0,
+ anchorY: 0,
+ visible: false
+ });
+ }
+ },
+ releaseSprite: function releaseSprite(sprite) {
+ sprite.visible = false;
+ sprite.alpha = 1.0;
+ sprite.scale.set(1.0, 1.0);
+ sprite.x = 0;
+ sprite.y = 0;
+ this.pool.push(sprite);
+ }
+};
+// Wall face tracking for complete textures
+var wallFaceTracker = {
+ currentFaces: [],
+ detectWallFaces: function detectWallFaces(raycastResults) {
+ var faces = [];
+ var currentFace = null;
+ for (var i = 0; i < raycastResults.length; i++) {
+ var hit = raycastResults[i];
+ if (hit.wallHit) {
+ var wallId = hit.mapX + "_" + hit.mapY + "_" + hit.side;
+ if (!currentFace || currentFace.wallId !== wallId) {
+ // New wall face detected
+ if (currentFace) {
+ currentFace.endRay = i - 1;
+ faces.push(currentFace);
+ }
+ currentFace = {
+ wallId: wallId,
+ startRay: i,
+ mapX: hit.mapX,
+ mapY: hit.mapY,
+ side: hit.side,
+ distance: hit.distance
+ };
+ } else {
+ // Same face continues
+ currentFace.distance = Math.min(currentFace.distance, hit.distance);
+ }
+ } else if (currentFace) {
+ // End of current face
+ currentFace.endRay = i - 1;
+ faces.push(currentFace);
+ currentFace = null;
+ }
+ }
+ // Don't forget the last face
+ if (currentFace) {
+ currentFace.endRay = raycastResults.length - 1;
+ faces.push(currentFace);
+ }
+ return faces;
+ }
+};
var dynamicEntitiesContainer;
var wallSpritePoolManager = {
pools: function () {
// IIFE to pre-populate pools for each wall tile type, with a fixed number of pre-allocated sprites
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