/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var CeilingTileRenderer = Container.expand(function () { var self = Container.call(this); self.tiles = []; // Generate ceiling tiles in safe positions (away from corners and walls) self.generateTiles = function () { for (var x = 2; x < worldGrid.width - 2; x++) { for (var y = 2; y < worldGrid.height - 2; y++) { // Only place tiles in open areas (not near walls or corners) if (!worldGrid.hasWallAt(x * worldGrid.cellSize, y * worldGrid.cellSize) && !self.isNearCorner(x, y) && self.isSafePosition(x, y)) { // Initialize tile before using its properties var tile = { worldX: x * worldGrid.cellSize + worldGrid.cellSize / 2, worldY: y * worldGrid.cellSize + worldGrid.cellSize / 2, sprite: null }; // Add a light in the middle of the room var light = self.addChild(LK.getAsset('smallLight', { anchorX: 0.5, anchorY: 0.5 })); light.x = tile.worldX; light.y = tile.worldY; light.visible = true; // Add ceilingTile as a light texture in random positions away from corners var ceilingTile = self.addChild(LK.getAsset('ceilingTile', { anchorX: 0.5, anchorY: 0.5 })); ceilingTile.x = tile.worldX + (Math.random() * 400 - 200); ceilingTile.y = tile.worldY + (Math.random() * 400 - 200); ceilingTile.visible = true; self.tiles.push(tile); } } } }; // Check if position is near a corner self.isNearCorner = function (gridX, gridY) { // Check 3x3 area around position for wall density var wallCount = 0; for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var checkX = gridX + dx; var checkY = gridY + dy; if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) { if (worldGrid.walls && worldGrid.walls[checkX] && worldGrid.walls[checkX][checkY]) { wallCount++; } } } } return wallCount >= 3; // Near corner if 3+ walls nearby }; // Check if position is safe (center of open areas) self.isSafePosition = function (gridX, gridY) { // Ensure there's open space in all 4 cardinal directions var directions = [{ x: 0, y: -1 }, { x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }]; for (var i = 0; i < directions.length; i++) { var checkX = gridX + directions[i].x; var checkY = gridY + directions[i].y; if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) { if (worldGrid.walls && worldGrid.walls[checkX] && worldGrid.walls[checkX][checkY]) { return false; } } } return true; }; self.render = function (player) { // Clear existing sprites for (var i = 0; i < self.tiles.length; i++) { if (self.tiles[i].sprite) { self.tiles[i].sprite.visible = false; } } var visibleTiles = []; // Calculate which tiles are visible and their screen positions for (var i = 0; i < self.tiles.length; i++) { var tile = self.tiles[i]; var dx = tile.worldX - player.x; var dy = tile.worldY - player.y; var distance = Math.sqrt(dx * dx + dy * dy); // Only render tiles within reasonable distance if (distance < 800) { // Calculate angle relative to player's view direction var tileAngle = Math.atan2(dy, dx); var angleDiff = tileAngle - player.angle; // Normalize angle difference while (angleDiff > Math.PI) { angleDiff -= 2 * Math.PI; } while (angleDiff < -Math.PI) { angleDiff += 2 * Math.PI; } // Check if tile is within field of view var fov = Math.PI / 3; if (Math.abs(angleDiff) < fov / 2) { // Calculate screen X position var screenX = 1366 + angleDiff / (fov / 2) * 1366; // Only add tiles that are within horizontal screen bounds with margin if (screenX >= -50 && screenX <= 2782) { // Apply pitch offset to ceiling tiles var pitchOffset = player.pitch * 400; visibleTiles.push({ tile: tile, distance: distance, screenX: screenX, screenY: 400 - 200 * (1000 / (distance + 100)) + pitchOffset // Project to ceiling with pitch }); } } } } // Sort by distance (farthest first) visibleTiles.sort(function (a, b) { return b.distance - a.distance; }); // Render visible tiles for (var i = 0; i < visibleTiles.length; i++) { var visibleTile = visibleTiles[i]; var tile = visibleTile.tile; if (!tile.sprite) { tile.sprite = self.addChild(LK.getAsset('normalCeiling', { anchorX: 0.5, anchorY: 0.5 })); } tile.sprite.x = visibleTile.screenX; tile.sprite.y = visibleTile.screenY; tile.sprite.visible = true; // Scale based on distance var scale = Math.max(0.1, 20 / (visibleTile.distance + 20)); tile.sprite.scaleX = scale; tile.sprite.scaleY = scale; } }; return self; }); var Door = Container.expand(function () { var self = Container.call(this); var doorGraphics = self.attachAsset('door', { anchorX: 0.5, anchorY: 0.5 }); self.isInteracting = false; self.magneticRadius = 150; // Distance at which door starts attracting player self.magneticStrength = 0.02; // Strength of magnetic pull self.down = function (x, y, obj) { if (!self.isInteracting) { self.isInteracting = true; self.triggerLevelTransition(); } }; // Update method to create magnetic attraction effect // Track last player position for crossing detection self.lastPlayerPosition = self.lastPlayerPosition || { x: 0, y: 0 }; self.lastPlayerInDoor = self.lastPlayerInDoor || false; self.update = function () { if (!player || self.isInteracting) return; // Use dynamic door position from worldGrid var targetX = worldGrid.doorPosition ? worldGrid.doorPosition.x : self.x; var targetY = worldGrid.doorPosition ? worldGrid.doorPosition.y : self.y; // Keep door at exact world position (not raised above ground) if (Math.abs(self.x - targetX) > 5) { self.x = targetX; } if (Math.abs(self.y - targetY) > 5) { self.y = targetY; } // Calculate distance to player var dx = self.x - player.x; var dy = self.y - player.y; var distance = Math.sqrt(dx * dx + dy * dy); // Add controlled glitch effect to make door recognizable as exit // Slower color changes to maintain visibility var glitchTintOptions = [0xFFFFFF, 0x00FFFF, 0xFFFF00, 0xFF00FF, 0xFFFFFF]; var glitchIndex = Math.floor(LK.ticks * 0.1 % glitchTintOptions.length); doorGraphics.tint = glitchTintOptions[glitchIndex]; // Gentle scale pulsing for visibility without making door disappear var scaleGlitch = 1.0 + Math.sin(LK.ticks * 0.2) * 0.1; doorGraphics.scaleX = scaleGlitch; doorGraphics.scaleY = scaleGlitch; // Reduced jitter to prevent door from becoming invisible var jitterRange = 2; var jitterX = Math.sin(LK.ticks * 0.15) * jitterRange; var jitterY = Math.cos(LK.ticks * 0.12) * jitterRange; doorGraphics.x = jitterX; doorGraphics.y = jitterY; // Ensure door graphics stay visible doorGraphics.visible = true; doorGraphics.alpha = Math.max(0.7, 0.7 + Math.sin(LK.ticks * 0.1) * 0.3); // Check if player is currently inside door area var doorRadius = 60; // Door interaction area var currentlyInDoor = distance < doorRadius; // Detect door crossing - if player was outside and now inside, or vice versa if (!self.lastPlayerInDoor && currentlyInDoor) { // Player just entered door area - trigger level completion self.isInteracting = true; self.triggerLevelTransition(); } // Update last position tracking self.lastPlayerInDoor = currentlyInDoor; self.lastPlayerPosition.x = player.x; self.lastPlayerPosition.y = player.y; // If player is within magnetic radius, apply attraction force if (distance < self.magneticRadius && distance > 20) { // Don't pull if too close // Calculate attraction force (stronger when closer) var force = self.magneticStrength * (self.magneticRadius - distance) / self.magneticRadius; // Normalize direction vector var dirX = dx / distance; var dirY = dy / distance; // Apply magnetic pull to player's target position player.targetX += dirX * force * 10; player.targetY += dirY * force * 10; // Increase glitch intensity when attracting var intensePulse = 0.5 + Math.sin(LK.ticks * 0.5) * 0.5; doorGraphics.alpha = intensePulse; } else { doorGraphics.alpha = 1.0; // Reset alpha when not attracting } }; self.triggerLevelTransition = function () { // Create completely black screen overlay var blackScreen = LK.getAsset('untexturedArea', { anchorX: 0, anchorY: 0, width: 2732, height: 2048, alpha: 0 }); blackScreen.tint = 0x000000; LK.gui.addChild(blackScreen); // Create level completion text - wider and white var levelText = new Text2('NIVEL 1 COMPLETADO', { size: 150, fill: 0xFFFFFF }); levelText.anchor.set(0.5, 0.5); levelText.x = 1366; // Center of screen horizontally levelText.y = -200; // Start above screen levelText.alpha = 0; LK.gui.addChild(levelText); // Animate black screen fade in to completely dark tween(blackScreen, { alpha: 1.0 }, { duration: 800, onFinish: function onFinish() { // Animate level text drop down and fade in - center of screen tween(levelText, { y: 1024, alpha: 1 }, { duration: 1200, easing: tween.bounceOut, onFinish: function onFinish() { // Wait 2 seconds showing completion message LK.setTimeout(function () { // Fade out completion message tween(levelText, { alpha: 0 }, { duration: 800, onFinish: function onFinish() { // Change to next level text - larger and white levelText.setText('NIVEL 2'); levelText.fill = 0xFFFFFF; levelText.size = 150; levelText.anchor.set(0.5, 0.5); levelText.x = 1366; // Ensure centered positioning tween(levelText, { alpha: 1 }, { duration: 500, onFinish: function onFinish() { // Wait another second, then fade everything out LK.setTimeout(function () { tween(levelText, { alpha: 0 }, { duration: 1000 }); tween(blackScreen, { alpha: 0 }, { duration: 1500, onFinish: function onFinish() { blackScreen.destroy(); levelText.destroy(); self.isInteracting = false; } }); }, 1000); } }); } }); }, 2000); } }); } }); }; return self; }); var GeometricWallRenderer = Container.expand(function () { var self = Container.call(this); // Simplified wall rendering with geometric shapes self.wallStrips = []; self.numStrips = 64; // Further reduced for better performance self.maxWalls = 32; // Maximum number of wall strips to render // Initialize wall strips pool self.initWallStrips = function () { for (var i = 0; i < self.maxWalls; i++) { var wallStrip = self.addChild(LK.getAsset('wallSegment', { anchorX: 0.5, anchorY: 0.5 })); wallStrip.visible = false; self.wallStrips.push(wallStrip); } }; // Get available wall strip from pool self.getWallStrip = function () { for (var i = 0; i < self.wallStrips.length; i++) { if (!self.wallStrips[i].visible) { return self.wallStrips[i]; } } return null; }; // Render walls using simplified column-based approach self.render = function (player) { // Initialize strips if not done if (self.wallStrips.length === 0) { self.initWallStrips(); } // Hide all wall strips for (var j = 0; j < self.wallStrips.length; j++) { self.wallStrips[j].visible = false; } var fov = Math.PI / 3; // 60 degrees field of view var halfFov = fov / 2; var screenCenter = 1024; // Y center of screen var pitchOffset = player.pitch * 300; // Apply pitch for vertical look var stripWidth = 2732 / self.numStrips; var wallsRendered = 0; // Cast rays and render wall strips for (var i = 0; i < self.numStrips && wallsRendered < self.maxWalls; i++) { var rayAngle = player.angle - halfFov + i / self.numStrips * fov; var rayData = self.castSimpleRay(player.x, player.y, rayAngle); if (rayData.hit) { var distance = rayData.distance; // Apply fish-eye correction var correctedDistance = distance * Math.cos(rayAngle - player.angle); // Calculate screen X position for horizontal bounds checking var screenX = i * stripWidth + stripWidth / 2; // Only render if within strict horizontal screen bounds if (screenX >= 0 && screenX <= 2732) { // Get wall strip from pool var wallStrip = self.getWallStrip(); if (wallStrip) { // Calculate wall height based on distance var baseWallSize = worldGrid.cellSize; var wallHeight = Math.max(60, baseWallSize * (500 / (correctedDistance + 50))); // Position wall strip within screen bounds wallStrip.width = stripWidth + 2; // Add small overlap wallStrip.height = wallHeight; wallStrip.x = Math.max(0, Math.min(2732, screenX)); // Strictly clamp to screen bounds wallStrip.y = screenCenter + pitchOffset; wallStrip.visible = true; // Apply distance-based shading var shadingFactor = Math.max(0.2, 1.0 - correctedDistance / 600); var tintValue = 0xFFFFFF; // Special rendering for door if (rayData.hitType === 'door') { // Use bright, visible colors for door var doorColors = [0xFFFFFF, 0x00FFFF, 0xFFFF00, 0xFF00FF]; var colorIndex = Math.floor(LK.ticks * 0.2 % doorColors.length); wallStrip.tint = doorColors[colorIndex]; // Make door slightly taller wallStrip.height = wallHeight * 1.1; } else { wallStrip.tint = tintValue; // Add slight variation based on position for texture effect var positionVariation = (rayData.hitX + rayData.hitY) % 40; if (positionVariation < 20) { tintValue = Math.floor(tintValue * 0.9); // Slightly darker wallStrip.tint = tintValue << 16 | tintValue << 8 | tintValue; } } wallsRendered++; } } } } }; // Simplified raycasting for geometric walls self.castSimpleRay = function (startX, startY, angle) { var rayX = startX; var rayY = startY; var deltaX = Math.cos(angle) * 8; // Larger steps for performance var deltaY = Math.sin(angle) * 8; var distance = 0; var maxDistance = 600; var stepSize = 8; // Raycast until wall hit or max distance while (distance < maxDistance) { rayX += deltaX; rayY += deltaY; distance += stepSize; // Check for wall or door collision var hitWall = worldGrid.hasWallAt(rayX, rayY); var hitDoor = worldGrid.hasDoorAt(rayX, rayY); if (hitWall || hitDoor) { // Store hit type for special rendering self.lastHitType = hitDoor ? 'door' : 'wall'; return { hit: true, distance: distance, hitX: rayX, hitY: rayY, hitType: self.lastHitType }; } } return { hit: false, distance: maxDistance, hitX: rayX, hitY: rayY }; }; return self; }); var LightManager = Container.expand(function () { var self = Container.call(this); self.lights = []; // Method to add a light at a specific world position self.addLight = function (worldX, worldY) { var light = self.addChild(LK.getAsset('smallLight', { anchorX: 0.5, anchorY: 0.5 })); light.x = worldX; light.y = worldY; light.visible = true; self.lights.push(light); }; // Method to clear all lights self.clearLights = function () { for (var i = 0; i < self.lights.length; i++) { self.lights[i].visible = false; } self.lights = []; }; return self; }); var MovementCrosshair = Container.expand(function () { var self = Container.call(this); self.isActive = false; self.activeButton = null; // Create base circle var base = self.attachAsset('crosshairBase', { anchorX: 0.5, anchorY: 0.5 }); base.alpha = 0.6; // Create directional buttons var upButton = self.attachAsset('crosshairUp', { anchorX: 0.5, anchorY: 0.5 }); upButton.x = 0; upButton.y = -70; upButton.alpha = 0.7; var downButton = self.attachAsset('crosshairDown', { anchorX: 0.5, anchorY: 0.5 }); downButton.x = 0; downButton.y = 70; downButton.alpha = 0.7; var leftButton = self.attachAsset('crosshairLeft', { anchorX: 0.5, anchorY: 0.5 }); leftButton.x = -70; leftButton.y = 0; leftButton.alpha = 0.7; var rightButton = self.attachAsset('crosshairRight', { anchorX: 0.5, anchorY: 0.5 }); rightButton.x = 70; rightButton.y = 0; rightButton.alpha = 0.7; var centerButton = self.attachAsset('crosshairCenter', { anchorX: 0.5, anchorY: 0.5 }); centerButton.alpha = 0.8; // Movement state tracking self.movementState = { forward: false, backward: false, left: false, right: false }; // Button press handlers upButton.down = function (x, y, obj) { upButton.alpha = 1.0; self.movementState.forward = true; self.activeButton = 'up'; }; upButton.up = function (x, y, obj) { upButton.alpha = 0.7; self.movementState.forward = false; if (self.activeButton === 'up') { self.activeButton = null; } }; downButton.down = function (x, y, obj) { downButton.alpha = 1.0; self.movementState.backward = true; self.activeButton = 'down'; }; downButton.up = function (x, y, obj) { downButton.alpha = 0.7; self.movementState.backward = false; if (self.activeButton === 'down') { self.activeButton = null; } }; leftButton.down = function (x, y, obj) { leftButton.alpha = 1.0; self.movementState.left = true; self.activeButton = 'left'; }; leftButton.up = function (x, y, obj) { leftButton.alpha = 0.7; self.movementState.left = false; if (self.activeButton === 'left') { self.activeButton = null; } }; rightButton.down = function (x, y, obj) { rightButton.alpha = 1.0; self.movementState.right = true; self.activeButton = 'right'; }; rightButton.up = function (x, y, obj) { rightButton.alpha = 0.7; self.movementState.right = false; if (self.activeButton === 'right') { self.activeButton = null; } }; // Get current movement state self.getMovementState = function () { return self.movementState; }; // Reset all movement states self.resetMovement = function () { self.movementState.forward = false; self.movementState.backward = false; self.movementState.left = false; self.movementState.right = false; self.activeButton = null; upButton.alpha = 0.7; downButton.alpha = 0.7; leftButton.alpha = 0.7; rightButton.alpha = 0.7; }; return self; }); var Player = Container.expand(function () { var self = Container.call(this); self.x = 1366; self.y = 1024; self.angle = 0; self.pitch = 0; // Vertical look angle (up/down) self.speed = 3; self.rotSpeed = 0.1; // Smooth interpolation properties self.targetX = 1366; self.targetY = 1024; self.targetAngle = 0; self.targetPitch = 0; self.smoothingFactor = 0.15; // Player visual for debugging (will be hidden in first person) var playerGraphics = self.attachAsset('player', { anchorX: 0.5, anchorY: 0.5 }); playerGraphics.visible = false; // Hide for first person view self.moveForward = function () { var newX = self.targetX + Math.cos(self.targetAngle) * self.speed; var newY = self.targetY + Math.sin(self.targetAngle) * self.speed; // Constrain Y coordinate to not go below 0 if (newY < 0) { newY = 0; } // Check collision with world grid using improved collision detection if (!worldGrid.checkCollision(newX, newY)) { self.targetX = newX; self.targetY = newY; } else { // Wall sliding - try to move along walls instead of stopping completely // Try moving only horizontally if vertical movement is blocked if (!worldGrid.checkCollision(newX, self.targetY)) { self.targetX = newX; } // Try moving only vertically if horizontal movement is blocked else if (!worldGrid.checkCollision(self.targetX, newY)) { self.targetY = newY; } } }; self.moveBackward = function () { var newX = self.targetX - Math.cos(self.targetAngle) * self.speed; var newY = self.targetY - Math.sin(self.targetAngle) * self.speed; // Constrain Y coordinate to not go below 0 if (newY < 0) { newY = 0; } // Check collision with world grid using improved collision detection if (!worldGrid.checkCollision(newX, newY)) { self.targetX = newX; self.targetY = newY; } else { // Wall sliding - try to move along walls instead of stopping completely // Try moving only horizontally if vertical movement is blocked if (!worldGrid.checkCollision(newX, self.targetY)) { self.targetX = newX; } // Try moving only vertically if horizontal movement is blocked else if (!worldGrid.checkCollision(self.targetX, newY)) { self.targetY = newY; } } }; self.turnLeft = function () { self.targetAngle -= self.rotSpeed; }; self.turnRight = function () { self.targetAngle += self.rotSpeed; }; self.lookUp = function () { self.targetPitch = Math.max(-Math.PI / 3, self.targetPitch - self.rotSpeed); // Limit to -60 degrees }; self.lookDown = function () { self.targetPitch = Math.min(Math.PI / 3, self.targetPitch + self.rotSpeed); // Limit to +60 degrees }; self.updateSmooth = function () { // Smooth interpolation for position self.x += (self.targetX - self.x) * self.smoothingFactor; self.y += (self.targetY - self.y) * self.smoothingFactor; // Smooth interpolation for rotation with angle wrapping var angleDiff = self.targetAngle - self.angle; // Handle angle wrapping (ensure shortest rotation path) if (angleDiff > Math.PI) { angleDiff -= 2 * Math.PI; } if (angleDiff < -Math.PI) { angleDiff += 2 * Math.PI; } self.angle += angleDiff * self.smoothingFactor; // Smooth interpolation for pitch var pitchDiff = self.targetPitch - self.pitch; self.pitch += pitchDiff * self.smoothingFactor; }; return self; }); var ProcGen = Container.expand(function () { var self = Container.call(this); self.generatedChunks = {}; self.chunkSize = 6; // Much smaller chunks for tighter room placement self.roomMinSize = 1; self.roomMaxSize = 2; self.hallwayWidth = 1; // Keep narrow hallways for claustrophobic effect // Generate a procedural chunk at given chunk coordinates self.generateChunk = function (chunkX, chunkY) { var chunkKey = chunkX + ',' + chunkY; if (self.generatedChunks[chunkKey]) { return; // Already generated } self.generatedChunks[chunkKey] = true; // Calculate world grid offset for this chunk var offsetX = chunkX * self.chunkSize; var offsetY = chunkY * self.chunkSize; // Generate Backrooms-style layout for this chunk self.generateBackroomsChunk(offsetX, offsetY); }; // Generate Backrooms-style layout with guaranteed connectivity and multiple exits self.generateBackroomsChunk = function (offsetX, offsetY) { // First, fill entire chunk with walls for (var x = offsetX; x < offsetX + self.chunkSize; x++) { for (var y = offsetY; y < offsetY + self.chunkSize; y++) { if (x >= 0 && x < worldGrid.width && y >= 0 && y < worldGrid.height) { worldGrid.walls[x][y] = true; } } } // Create main room in center of chunk with irregular shape var mainRoomSize = 2; // Smaller main room for claustrophobic effect var mainRoomX = offsetX + Math.floor((self.chunkSize - mainRoomSize) / 2); var mainRoomY = offsetY + Math.floor((self.chunkSize - mainRoomSize) / 2); self.carveIrregularRoom(mainRoomX, mainRoomY, mainRoomSize, mainRoomSize); // Create 3-5 additional rooms for higher density and claustrophobic feel var numRooms = Math.floor(Math.random() * 3) + 3; // 3-5 rooms var rooms = [{ x: mainRoomX, y: mainRoomY, width: mainRoomSize, height: mainRoomSize, centerX: mainRoomX + Math.floor(mainRoomSize / 2), centerY: mainRoomY + Math.floor(mainRoomSize / 2) }]; for (var i = 0; i < numRooms; i++) { var attempts = 0; while (attempts < 30) { // Determine room size based on probabilities var roomSizes = self.getRoomSizeByProbability(); var roomW = roomSizes.width; var roomH = roomSizes.height; var roomX = offsetX + Math.floor(Math.random() * (self.chunkSize - roomW - 2)) + 1; var roomY = offsetY + Math.floor(Math.random() * (self.chunkSize - roomH - 2)) + 1; var newRoom = { x: roomX, y: roomY, width: roomW, height: roomH, centerX: roomX + Math.floor(roomW / 2), centerY: roomY + Math.floor(roomH / 2) }; if (!self.roomOverlaps(newRoom, rooms)) { self.carveIrregularRoom(roomX, roomY, roomW, roomH); rooms.push(newRoom); // Connect this room to multiple existing rooms for guaranteed connectivity self.connectToMultipleRooms(newRoom, rooms); break; } attempts++; } } // Create guaranteed multiple exits to adjacent chunks self.createMultipleChunkExits(offsetX, offsetY, rooms); // Create dead-end corridors for exploration variety self.createDeadEndCorridors(offsetX, offsetY, rooms); // Add some random pillars for atmosphere self.addRandomPillars(offsetX, offsetY); // Add pillars specifically in medium and large rooms self.addPillarsToRooms(offsetX, offsetY, rooms); // Validate and ensure all rooms are connected self.validateRoomConnectivity(rooms, offsetX, offsetY); }; // Carve out a room (remove walls) self.carveRoom = function (x, y, width, height) { for (var roomX = x; roomX < x + width; roomX++) { for (var roomY = y; roomY < y + height; roomY++) { if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) { worldGrid.walls[roomX][roomY] = false; } } } }; // Carve out an irregular room shape with random variations self.carveIrregularRoom = function (x, y, width, height) { var roomType = Math.random(); var centerX = x + Math.floor(width / 2); var centerY = y + Math.floor(height / 2); if (roomType < 0.4) { // Circular/oval room var radiusX = Math.floor(width / 2); var radiusY = Math.floor(height / 2); for (var roomX = x; roomX < x + width; roomX++) { for (var roomY = y; roomY < y + height; roomY++) { if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) { var dx = roomX - centerX; var dy = roomY - centerY; // Create elliptical shape with some randomness var distanceSquared = dx * dx / (radiusX * radiusX) + dy * dy / (radiusY * radiusY); if (distanceSquared <= 1.0 + Math.random() * 0.3 - 0.15) { worldGrid.walls[roomX][roomY] = false; } } } } } else if (roomType < 0.7) { // L-shaped room self.carveRoom(x, y, width, Math.floor(height / 2) + 1); self.carveRoom(x + Math.floor(width / 2), y + Math.floor(height / 2), Math.floor(width / 2) + 1, Math.floor(height / 2) + 1); } else if (roomType < 0.9) { // Cross-shaped room var halfW = Math.floor(width / 2); var halfH = Math.floor(height / 2); // Vertical bar self.carveRoom(centerX - 1, y, 2, height); // Horizontal bar self.carveRoom(x, centerY - 1, width, 2); } else { // Regular rectangular room with random indentations self.carveRoom(x, y, width, height); // Add random indentations var indentations = Math.floor(Math.random() * 3) + 1; for (var i = 0; i < indentations; i++) { var indentX = x + Math.floor(Math.random() * width); var indentY = y + Math.floor(Math.random() * height); var indentSize = Math.floor(Math.random() * 2) + 1; for (var ix = 0; ix < indentSize; ix++) { for (var iy = 0; iy < indentSize; iy++) { var wallX = indentX + ix; var wallY = indentY + iy; if (wallX >= 0 && wallX < worldGrid.width && wallY >= 0 && wallY < worldGrid.height) { worldGrid.walls[wallX][wallY] = true; } } } } } }; // Check if room overlaps with existing rooms self.roomOverlaps = function (room, existingRooms) { for (var i = 0; i < existingRooms.length; i++) { var existing = existingRooms[i]; // Minimal padding between rooms for claustrophobic effect - only 1 cell apart if (room.x < existing.x + existing.width + 1 && room.x + room.width + 1 > existing.x && room.y < existing.y + existing.height + 1 && room.y + room.height + 1 > existing.y) { return true; } } return false; }; // Connect room to nearest existing room self.connectToNearestRoom = function (newRoom, existingRooms) { var nearestRoom = existingRooms[0]; var minDistance = Infinity; // Find nearest room for (var i = 0; i < existingRooms.length; i++) { if (existingRooms[i] !== newRoom) { var dx = newRoom.centerX - existingRooms[i].centerX; var dy = newRoom.centerY - existingRooms[i].centerY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < minDistance) { minDistance = distance; nearestRoom = existingRooms[i]; } } } // Create L-shaped corridor self.createCorridor(newRoom.centerX, newRoom.centerY, nearestRoom.centerX, nearestRoom.centerY); }; // Connect room to multiple existing rooms for guaranteed connectivity self.connectToMultipleRooms = function (newRoom, existingRooms) { // Sort rooms by distance to find nearest connections var roomDistances = []; for (var i = 0; i < existingRooms.length; i++) { if (existingRooms[i] !== newRoom) { var dx = newRoom.centerX - existingRooms[i].centerX; var dy = newRoom.centerY - existingRooms[i].centerY; var distance = Math.sqrt(dx * dx + dy * dy); roomDistances.push({ room: existingRooms[i], distance: distance }); } } // Sort by distance roomDistances.sort(function (a, b) { return a.distance - b.distance; }); // Always connect to at least 2 rooms for guaranteed multiple exits var minConnections = Math.min(2, roomDistances.length); var maxConnections = Math.min(3, roomDistances.length); // Up to 3 connections var connectionsToMake = Math.floor(Math.random() * (maxConnections - minConnections + 1)) + minConnections; for (var i = 0; i < connectionsToMake; i++) { self.createCurvedCorridor(newRoom.centerX, newRoom.centerY, roomDistances[i].room.centerX, roomDistances[i].room.centerY); } // Add one more connection with 40% probability for extra connectivity if (Math.random() < 0.4 && roomDistances.length > connectionsToMake) { self.createCurvedCorridor(newRoom.centerX, newRoom.centerY, roomDistances[connectionsToMake].room.centerX, roomDistances[connectionsToMake].room.centerY); } }; // Connect room to nearest existing room with curved corridor (legacy method for compatibility) self.connectToNearestRoomCurved = function (newRoom, existingRooms) { // Use the new multiple connections method self.connectToMultipleRooms(newRoom, existingRooms); }; // Create multiple guaranteed exits to adjacent chunks for better connectivity self.createMultipleChunkExits = function (offsetX, offsetY, rooms) { var midX = offsetX + Math.floor(self.chunkSize / 2); var midY = offsetY + Math.floor(self.chunkSize / 2); // Find main room (usually the first/largest) var mainRoom = rooms[0]; for (var i = 1; i < rooms.length; i++) { if (rooms[i].width * rooms[i].height > mainRoom.width * mainRoom.height) { mainRoom = rooms[i]; } } // Ensure at least 2-3 exits for multiple pathways var possibleExits = []; // Check which exits are possible if (offsetX > 0) { possibleExits.push('left'); } if (offsetX + self.chunkSize < worldGrid.width) { possibleExits.push('right'); } if (offsetY > 0) { possibleExits.push('top'); } if (offsetY + self.chunkSize < worldGrid.height) { possibleExits.push('bottom'); } // Guarantee at least 2 exits if possible, 3 if we have 4 sides available var minExits = Math.min(2, possibleExits.length); var maxExits = Math.min(3, possibleExits.length); var exitsToCreate = Math.floor(Math.random() * (maxExits - minExits + 1)) + minExits; // Shuffle possible exits for randomness for (var i = possibleExits.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = possibleExits[i]; possibleExits[i] = possibleExits[j]; possibleExits[j] = temp; } // Create the guaranteed exits for (var i = 0; i < exitsToCreate; i++) { var exitDirection = possibleExits[i]; // Connect from different rooms for variety var sourceRoom = rooms[i % rooms.length]; if (exitDirection === 'left') { self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, offsetX, midY); } else if (exitDirection === 'right') { self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, offsetX + self.chunkSize - 1, midY); } else if (exitDirection === 'top') { self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, midX, offsetY); } else if (exitDirection === 'bottom') { self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, midX, offsetY + self.chunkSize - 1); } } // Create additional exits with 50% probability for extra connectivity for (var i = exitsToCreate; i < possibleExits.length; i++) { if (Math.random() < 0.5) { var exitDirection = possibleExits[i]; var sourceRoom = rooms[Math.floor(Math.random() * rooms.length)]; if (exitDirection === 'left') { self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, offsetX, midY); } else if (exitDirection === 'right') { self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, offsetX + self.chunkSize - 1, midY); } else if (exitDirection === 'top') { self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, midX, offsetY); } else if (exitDirection === 'bottom') { self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, midX, offsetY + self.chunkSize - 1); } } } }; // Create dead-end corridors for exploration variety self.createDeadEndCorridors = function (offsetX, offsetY, rooms) { // First, check each room for exits and potentially add passages for (var i = 0; i < rooms.length; i++) { var room = rooms[i]; var hasExit = self.checkRoomHasExit(room, offsetX, offsetY); // If room has no exits, add a passage with 20% probability if (!hasExit && Math.random() < 0.2) { self.createRoomPassage(room, offsetX, offsetY); } } // Create 2-4 dead-end corridors per chunk var numDeadEnds = Math.floor(Math.random() * 3) + 2; // 2-4 dead ends for (var i = 0; i < numDeadEnds; i++) { // Choose a random room as starting point var sourceRoom = rooms[Math.floor(Math.random() * rooms.length)]; // Choose a random direction for the dead-end var directions = [{ dx: 1, dy: 0 }, // East { dx: -1, dy: 0 }, // West { dx: 0, dy: 1 }, // South { dx: 0, dy: -1 } // North ]; var direction = directions[Math.floor(Math.random() * directions.length)]; // Create dead-end corridor of random length (2-5 cells) var corridorLength = Math.floor(Math.random() * 4) + 2; var startX = sourceRoom.centerX; var startY = sourceRoom.centerY; // Find a good starting point on the room edge var edgeStartX = startX; var edgeStartY = startY; // Move to room edge if (direction.dx !== 0) { edgeStartX = direction.dx > 0 ? sourceRoom.x + sourceRoom.width : sourceRoom.x - 1; } else { edgeStartY = direction.dy > 0 ? sourceRoom.y + sourceRoom.height : sourceRoom.y - 1; } // Create the dead-end corridor self.createDeadEndPath(edgeStartX, edgeStartY, direction.dx, direction.dy, corridorLength, offsetX, offsetY); } }; // Check if a room has any exits (passages leading out) self.checkRoomHasExit = function (room, chunkOffsetX, chunkOffsetY) { // Check the perimeter of the room for any open passages var directions = [{ dx: 1, dy: 0 }, // East { dx: -1, dy: 0 }, // West { dx: 0, dy: 1 }, // South { dx: 0, dy: -1 } // North ]; // Check each edge of the room for (var x = room.x; x < room.x + room.width; x++) { for (var y = room.y; y < room.y + room.height; y++) { // Skip if this position is a wall if (x >= 0 && x < worldGrid.width && y >= 0 && y < worldGrid.height && worldGrid.walls[x][y]) { continue; } // Check adjacent cells for passages for (var d = 0; d < directions.length; d++) { var checkX = x + directions[d].dx; var checkY = y + directions[d].dy; // Check bounds if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) { // If adjacent cell is open and outside the room bounds, it's an exit if (!worldGrid.walls[checkX][checkY] && (checkX < room.x || checkX >= room.x + room.width || checkY < room.y || checkY >= room.y + room.height)) { return true; // Found an exit } } } } } return false; // No exits found }; // Create a passage for a room without exits self.createRoomPassage = function (room, chunkOffsetX, chunkOffsetY) { // Determine the best direction for the passage var directions = [{ dx: 1, dy: 0, name: 'east' }, { dx: -1, dy: 0, name: 'west' }, { dx: 0, dy: 1, name: 'south' }, { dx: 0, dy: -1, name: 'north' }]; var bestDirection = directions[Math.floor(Math.random() * directions.length)]; // Create passage from room center in chosen direction var startX = room.centerX; var startY = room.centerY; // Move to room edge var edgeX = startX; var edgeY = startY; if (bestDirection.dx !== 0) { edgeX = bestDirection.dx > 0 ? room.x + room.width : room.x - 1; } else { edgeY = bestDirection.dy > 0 ? room.y + room.height : room.y - 1; } // Create passage large enough for player (width of 2-3 cells) var passageLength = Math.floor(Math.random() * 3) + 3; // 3-5 cells long self.createPlayerSizedPassage(edgeX, edgeY, bestDirection.dx, bestDirection.dy, passageLength, chunkOffsetX, chunkOffsetY); }; // Create a passage sized for player movement self.createPlayerSizedPassage = function (startX, startY, dirX, dirY, length, chunkOffsetX, chunkOffsetY) { for (var i = 0; i <= length; i++) { var passageX = startX + dirX * i; var passageY = startY + dirY * i; // Ensure passage coordinates are valid and within chunk bounds if (passageX >= chunkOffsetX && passageX < chunkOffsetX + self.chunkSize && passageY >= chunkOffsetY && passageY < chunkOffsetY + self.chunkSize && passageX >= 0 && passageX < worldGrid.width && passageY >= 0 && passageY < worldGrid.height) { // Clear main passage cell worldGrid.walls[passageX][passageY] = false; // Create wider passage (2-3 cells wide) for better player movement if (dirX !== 0) { // Horizontal passage - add vertical width if (passageY + 1 < worldGrid.height && passageY + 1 < chunkOffsetY + self.chunkSize) { worldGrid.walls[passageX][passageY + 1] = false; } if (passageY - 1 >= 0 && passageY - 1 >= chunkOffsetY) { worldGrid.walls[passageX][passageY - 1] = false; } // Occasionally add third row for wider passage if (Math.random() < 0.5 && passageY + 2 < worldGrid.height && passageY + 2 < chunkOffsetY + self.chunkSize) { worldGrid.walls[passageX][passageY + 2] = false; } } else { // Vertical passage - add horizontal width if (passageX + 1 < worldGrid.width && passageX + 1 < chunkOffsetX + self.chunkSize) { worldGrid.walls[passageX + 1][passageY] = false; } if (passageX - 1 >= 0 && passageX - 1 >= chunkOffsetX) { worldGrid.walls[passageX - 1][passageY] = false; } // Occasionally add third column for wider passage if (Math.random() < 0.5 && passageX + 2 < worldGrid.width && passageX + 2 < chunkOffsetX + self.chunkSize) { worldGrid.walls[passageX + 2][passageY] = false; } } } } }; // Create a single dead-end corridor path self.createDeadEndPath = function (startX, startY, dirX, dirY, length, chunkOffsetX, chunkOffsetY) { var currentX = startX; var currentY = startY; // Create corridor cells for (var i = 0; i < length; i++) { currentX += dirX; currentY += dirY; // Check bounds - stop if we're going outside the chunk or world if (currentX < chunkOffsetX + 1 || currentX >= chunkOffsetX + self.chunkSize - 1 || currentY < chunkOffsetY + 1 || currentY >= chunkOffsetY + self.chunkSize - 1 || currentX < 0 || currentX >= worldGrid.width || currentY < 0 || currentY >= worldGrid.height) { break; } // Carve out the corridor cell worldGrid.walls[currentX][currentY] = false; // Add some width to the corridor (occasionally) if (Math.random() < 0.3) { // Add perpendicular width var perpDirX = dirY; // Perpendicular direction var perpDirY = -dirX; var widthX = currentX + perpDirX; var widthY = currentY + perpDirY; if (widthX >= chunkOffsetX && widthX < chunkOffsetX + self.chunkSize && widthY >= chunkOffsetY && widthY < chunkOffsetY + self.chunkSize && widthX >= 0 && widthX < worldGrid.width && widthY >= 0 && widthY < worldGrid.height) { worldGrid.walls[widthX][widthY] = false; } } // Occasionally branch the dead-end if (i > 1 && Math.random() < 0.2) { // Create a short branch (1-2 cells) var branchLength = Math.floor(Math.random() * 2) + 1; var branchDirX = Math.random() < 0.5 ? dirY : -dirY; // Perpendicular directions var branchDirY = Math.random() < 0.5 ? -dirX : dirX; self.createDeadEndPath(currentX, currentY, branchDirX, branchDirY, branchLength, chunkOffsetX, chunkOffsetY); } } // Create a small room at the end of some dead-ends (30% chance) if (Math.random() < 0.3) { self.createDeadEndRoom(currentX, currentY, chunkOffsetX, chunkOffsetY); } }; // Create a small room at the end of a dead-end corridor self.createDeadEndRoom = function (centerX, centerY, chunkOffsetX, chunkOffsetY) { // Create a small 2x2 or 3x3 room var roomSize = Math.floor(Math.random() * 2) + 2; // 2x2 or 3x3 var halfSize = Math.floor(roomSize / 2); for (var dx = -halfSize; dx <= halfSize; dx++) { for (var dy = -halfSize; dy <= halfSize; dy++) { var roomX = centerX + dx; var roomY = centerY + dy; // Check bounds if (roomX >= chunkOffsetX && roomX < chunkOffsetX + self.chunkSize && roomY >= chunkOffsetY && roomY < chunkOffsetY + self.chunkSize && roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) { worldGrid.walls[roomX][roomY] = false; } } } }; // Create guaranteed exits to adjacent chunks (legacy method for compatibility) self.createChunkExits = function (offsetX, offsetY, mainRoom) { // Use the new multiple exits method self.createMultipleChunkExits(offsetX, offsetY, [mainRoom]); }; // Create L-shaped corridor between two points with much shorter segments self.createCorridor = function (x1, y1, x2, y2) { // Create narrower corridors for shorter navigation paths var halfWidth = Math.floor(self.hallwayWidth / 2); // Calculate distance and limit corridor length to make them much shorter var dx = Math.abs(x2 - x1); var dy = Math.abs(y2 - y1); var maxCorridorLength = 3; // Maximum corridor segment length // If distance is too long, create intermediate points for shorter segments if (dx > maxCorridorLength || dy > maxCorridorLength) { var midX = x1 + Math.sign(x2 - x1) * Math.min(maxCorridorLength, dx); var midY = y1 + Math.sign(y2 - y1) * Math.min(maxCorridorLength, dy); // Create first short segment self.createShortSegment(x1, y1, midX, y1, halfWidth); // Create second short segment self.createShortSegment(midX, y1, midX, midY, halfWidth); // If we haven't reached the destination, create final segment if (midX !== x2 || midY !== y2) { self.createShortSegment(midX, midY, x2, y2, halfWidth); } } else { // Choose random direction for short L-shaped corridor if (Math.random() < 0.5) { // Horizontal first, then vertical - but keep it short self.createShortSegment(x1, y1, x2, y1, halfWidth); self.createShortSegment(x2, y1, x2, y2, halfWidth); } else { // Vertical first, then horizontal - but keep it short self.createShortSegment(x1, y1, x1, y2, halfWidth); self.createShortSegment(x1, y2, x2, y2, halfWidth); } } }; // Create a short corridor segment with limited length self.createShortSegment = function (x1, y1, x2, y2, halfWidth) { var startX = Math.min(x1, x2); var endX = Math.max(x1, x2); var startY = Math.min(y1, y2); var endY = Math.max(y1, y2); // Limit segment length to make corridors extremely short for claustrophobic effect var maxSegmentLength = 1; endX = Math.min(endX, startX + maxSegmentLength); endY = Math.min(endY, startY + maxSegmentLength); for (var x = startX; x <= endX; x++) { for (var y = startY; y <= endY; y++) { for (var w = -halfWidth; w <= halfWidth; w++) { var corridorX = x + (x1 === x2 ? w : 0); var corridorY = y + (y1 === y2 ? w : 0); if (corridorX >= 0 && corridorX < worldGrid.width && corridorY >= 0 && corridorY < worldGrid.height) { worldGrid.walls[corridorX][corridorY] = false; } } } } }; // Create curved and winding corridor between two points self.createCurvedCorridor = function (x1, y1, x2, y2) { var corridorType = Math.random(); var halfWidth = Math.floor(self.hallwayWidth / 2); if (corridorType < 0.3) { // S-shaped curve var midX = Math.floor((x1 + x2) / 2) + Math.floor(Math.random() * 4) - 2; var midY = Math.floor((y1 + y2) / 2) + Math.floor(Math.random() * 4) - 2; self.createSmoothPath(x1, y1, midX, midY); self.createSmoothPath(midX, midY, x2, y2); } else if (corridorType < 0.6) { // Zigzag corridor var steps = Math.floor(Math.abs(x2 - x1) + Math.abs(y2 - y1)) / 3; var currentX = x1; var currentY = y1; var stepX = (x2 - x1) / steps; var stepY = (y2 - y1) / steps; for (var i = 0; i < steps; i++) { var nextX = Math.floor(currentX + stepX + Math.random() * 2 - 1); var nextY = Math.floor(currentY + stepY + Math.random() * 2 - 1); self.createSmoothPath(Math.floor(currentX), Math.floor(currentY), nextX, nextY); currentX = nextX; currentY = nextY; } self.createSmoothPath(Math.floor(currentX), Math.floor(currentY), x2, y2); } else { // Wide curved path with multiple control points var controlPoints = []; controlPoints.push({ x: x1, y: y1 }); // Add 1-2 random control points var numControls = Math.floor(Math.random() * 2) + 1; for (var i = 0; i < numControls; i++) { var t = (i + 1) / (numControls + 1); var controlX = Math.floor(x1 + (x2 - x1) * t + Math.random() * 6 - 3); var controlY = Math.floor(y1 + (y2 - y1) * t + Math.random() * 6 - 3); controlPoints.push({ x: controlX, y: controlY }); } controlPoints.push({ x: x2, y: y2 }); // Connect all control points for (var i = 0; i < controlPoints.length - 1; i++) { self.createSmoothPath(controlPoints[i].x, controlPoints[i].y, controlPoints[i + 1].x, controlPoints[i + 1].y); } } }; // Create smooth path between two points with width - much shorter segments self.createSmoothPath = function (x1, y1, x2, y2) { var dx = x2 - x1; var dy = y2 - y1; var distance = Math.sqrt(dx * dx + dy * dy); // Limit path length to make corridors extremely short for claustrophobic effect var maxPathLength = 2; if (distance > maxPathLength) { // Create shorter intermediate path var ratio = maxPathLength / distance; x2 = Math.floor(x1 + dx * ratio); y2 = Math.floor(y1 + dy * ratio); dx = x2 - x1; dy = y2 - y1; distance = maxPathLength; } var steps = Math.min(Math.floor(distance) + 1, 2); // Very limited steps for tight spaces var halfWidth = Math.floor(self.hallwayWidth / 2); for (var i = 0; i <= steps; i++) { var t = i / steps; var x = Math.floor(x1 + dx * t); var y = Math.floor(y1 + dy * t); // Carve corridor with width for (var w = -halfWidth; w <= halfWidth; w++) { for (var h = -halfWidth; h <= halfWidth; h++) { var corridorX = x + w; var corridorY = y + h; if (corridorX >= 0 && corridorX < worldGrid.width && corridorY >= 0 && corridorY < worldGrid.height) { worldGrid.walls[corridorX][corridorY] = false; } } } } }; // Add pillars at coordinate system corners - only around player self.addRandomPillars = function (offsetX, offsetY) { // Get player position if available var playerX = player ? Math.floor(player.x / worldGrid.cellSize) : Math.floor(worldGrid.width / 2); var playerY = player ? Math.floor(player.y / worldGrid.cellSize) : Math.floor(worldGrid.height / 2); var playerRadius = 8; // Only generate pillars within 8 cells of player // Define corner positions within the chunk var cornerPositions = [ // Top-left corner of chunk { x: offsetX + 1, y: offsetY + 1 }, // Top-right corner of chunk { x: offsetX + self.chunkSize - 2, y: offsetY + 1 }, // Bottom-left corner of chunk { x: offsetX + 1, y: offsetY + self.chunkSize - 2 }, // Bottom-right corner of chunk { x: offsetX + self.chunkSize - 2, y: offsetY + self.chunkSize - 2 }]; // Try to place pillars at corner positions for (var i = 0; i < cornerPositions.length; i++) { var corner = cornerPositions[i]; var x = corner.x; var y = corner.y; // Ensure position is within world bounds if (x >= 0 && x < worldGrid.width && y >= 0 && y < worldGrid.height) { // Check if pillar position is within player radius var distanceToPlayer = Math.abs(x - playerX) + Math.abs(y - playerY); if (distanceToPlayer > playerRadius) { continue; // Skip if too far from player } // Only add pillars in open areas (50% chance at corners) if (!worldGrid.walls[x][y] && Math.random() < 0.5) { // Check if surrounded by enough open space (2x2 area around corner) var canPlace = true; for (var dx = 0; dx <= 1; dx++) { for (var dy = 0; dy <= 1; dy++) { var checkX = x + dx; var checkY = y + dy; if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) { if (worldGrid.walls[checkX][checkY]) { canPlace = false; break; } } } if (!canPlace) { break; } } if (canPlace) { worldGrid.walls[x][y] = true; } } } } }; // Validate that all rooms are connected and fix any isolated rooms self.validateRoomConnectivity = function (rooms, offsetX, offsetY) { if (rooms.length <= 1) { return; } // Single room doesn't need validation // Use flood fill to check connectivity from main room var visited = []; for (var i = 0; i < rooms.length; i++) { visited[i] = false; } // Start flood fill from main room (first room) var connected = [0]; // Start with main room visited[0] = true; var changed = true; // Keep checking until no new connections are found while (changed) { changed = false; for (var i = 0; i < connected.length; i++) { var currentRoom = rooms[connected[i]]; // Check if any unvisited room is connected to this room for (var j = 0; j < rooms.length; j++) { if (!visited[j] && self.areRoomsConnected(currentRoom, rooms[j])) { visited[j] = true; connected.push(j); changed = true; } } } } // Connect any isolated rooms for (var i = 0; i < rooms.length; i++) { if (!visited[i]) { // This room is isolated, connect it to the nearest connected room var nearestConnectedRoom = rooms[connected[0]]; var minDistance = Infinity; for (var j = 0; j < connected.length; j++) { var connectedRoom = rooms[connected[j]]; var dx = rooms[i].centerX - connectedRoom.centerX; var dy = rooms[i].centerY - connectedRoom.centerY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < minDistance) { minDistance = distance; nearestConnectedRoom = connectedRoom; } } // Force connection to prevent isolation self.createCurvedCorridor(rooms[i].centerX, rooms[i].centerY, nearestConnectedRoom.centerX, nearestConnectedRoom.centerY); } } }; // Check if two rooms are connected via corridors self.areRoomsConnected = function (room1, room2) { // Use simple pathfinding to check if rooms are connected var visited = []; for (var x = 0; x < worldGrid.width; x++) { visited[x] = []; for (var y = 0; y < worldGrid.height; y++) { visited[x][y] = false; } } var stack = [{ x: room1.centerX, y: room1.centerY }]; while (stack.length > 0) { var current = stack.pop(); var gridX = Math.floor(current.x / worldGrid.cellSize) * worldGrid.cellSize; var gridY = Math.floor(current.y / worldGrid.cellSize) * worldGrid.cellSize; var arrayX = Math.floor(current.x / worldGrid.cellSize); var arrayY = Math.floor(current.y / worldGrid.cellSize); if (arrayX < 0 || arrayX >= worldGrid.width || arrayY < 0 || arrayY >= worldGrid.height) { continue; } if (visited[arrayX][arrayY] || worldGrid.walls[arrayX][arrayY]) { continue; } visited[arrayX][arrayY] = true; // Check if we reached room2 if (Math.abs(current.x - room2.centerX) < worldGrid.cellSize && Math.abs(current.y - room2.centerY) < worldGrid.cellSize) { return true; // Rooms are connected } // Add adjacent cells var directions = [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }]; for (var d = 0; d < directions.length; d++) { stack.push({ x: current.x + directions[d].dx * worldGrid.cellSize, y: current.y + directions[d].dy * worldGrid.cellSize }); } } return false; // No connection found }; // Get room size based on probability distribution - heavily favor small rooms for claustrophobia // Small rooms: 75%, Medium rooms: 20%, Large rooms: 5% self.getRoomSizeByProbability = function () { var random = Math.random() * 100; if (random < 75) { // Small rooms (75% probability) - 1x1 to 2x2 (mostly tiny) var size = Math.floor(Math.random() * 2) + 1; return { width: size, height: size }; } else if (random < 95) { // Medium rooms (20% probability) - 2x2 to 3x3 (reduced max size) var size = Math.floor(Math.random() * 2) + 2; return { width: size, height: size }; } else { // Large rooms (5% probability) - 3x3 to 4x4 (much smaller than before) var size = Math.floor(Math.random() * 2) + 3; return { width: size, height: size }; } }; // Generate chunks around player position self.generateAroundPlayer = function (playerX, playerY) { var playerChunkX = Math.floor(playerX / (worldGrid.cellSize * self.chunkSize)); var playerChunkY = Math.floor(playerY / (worldGrid.cellSize * self.chunkSize)); // Generate chunks in a 3x3 area around player for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var chunkX = playerChunkX + dx; var chunkY = playerChunkY + dy; self.generateChunk(chunkX, chunkY); } } }; // Add pillars specifically in medium and large rooms and also as standalone pillars in corridors - only around player self.addPillarsToRooms = function (offsetX, offsetY, rooms) { // Get player position if available var playerX = player ? Math.floor(player.x / worldGrid.cellSize) : Math.floor(worldGrid.width / 2); var playerY = player ? Math.floor(player.y / worldGrid.cellSize) : Math.floor(worldGrid.height / 2); var playerRadius = 8; // Only generate pillars within 8 cells of player for (var i = 0; i < rooms.length; i++) { var room = rooms[i]; var roomArea = room.width * room.height; // Check if room is within player radius var distanceToPlayer = Math.abs(room.centerX - playerX) + Math.abs(room.centerY - playerY); if (distanceToPlayer > playerRadius) { continue; // Skip room if too far from player } var pillarCount = 0; // Determine number of pillars based on room size - now includes small rooms if (roomArea >= 9) { // Large rooms (9+ cells): 2-4 pillars pillarCount = Math.floor(Math.random() * 3) + 2; } else if (roomArea >= 4) { // Medium rooms (4-8 cells): 1-2 pillars pillarCount = Math.floor(Math.random() * 2) + 1; } else if (roomArea >= 2) { // Small rooms (2-3 cells): 0-1 pillar with 40% probability if (Math.random() < 0.4) { pillarCount = 1; } } // Place pillars only in the center of rooms for (var p = 0; p < pillarCount; p++) { // Calculate exact room center var pillarX = room.x + Math.floor(room.width / 2); var pillarY = room.y + Math.floor(room.height / 2); // For rooms with even dimensions, slightly offset to avoid exact geometric center if (room.width % 2 === 0 && p % 2 === 1) { pillarX += Math.random() < 0.5 ? -1 : 1; } if (room.height % 2 === 0 && p % 3 === 1) { pillarY += Math.random() < 0.5 ? -1 : 1; } // Ensure pillar is within room bounds and world bounds if (pillarX >= room.x && pillarX < room.x + room.width && pillarY >= room.y && pillarY < room.y + room.height && pillarX >= 0 && pillarX < worldGrid.width && pillarY >= 0 && pillarY < worldGrid.height) { // Only place if position is currently open if (!worldGrid.walls[pillarX][pillarY]) { worldGrid.walls[pillarX][pillarY] = true; } } } } // Also add standalone pillars in corridor areas self.addStandalonePillarsInCorridors(offsetX, offsetY, rooms); }; // Check if a pillar can be placed at the given position (simplified for center placement) self.canPlacePillar = function (pillarX, pillarY, room, allRooms) { // Check that position is currently open (not already a wall) if (pillarX >= 0 && pillarX < worldGrid.width && pillarY >= 0 && pillarY < worldGrid.height) { if (worldGrid.walls[pillarX][pillarY]) { return false; // Already a wall } } // Since we're only placing in centers, just ensure the position is open return true; }; // Add standalone pillars at coordinate intersections - only around player and inside rooms self.addStandalonePillarsInCorridors = function (offsetX, offsetY, rooms) { // Get player position if available var playerX = player ? Math.floor(player.x / worldGrid.cellSize) : Math.floor(worldGrid.width / 2); var playerY = player ? Math.floor(player.y / worldGrid.cellSize) : Math.floor(worldGrid.height / 2); var playerRadius = 8; // Only generate pillars within 8 cells of player // Define coordinate intersection points within the chunk (grid intersections) var intersectionPoints = []; // Create a grid of intersection points every 2 cells for (var gridX = offsetX + 2; gridX < offsetX + self.chunkSize - 1; gridX += 2) { for (var gridY = offsetY + 2; gridY < offsetY + self.chunkSize - 1; gridY += 2) { intersectionPoints.push({ x: gridX, y: gridY }); } } // Try to place pillars at coordinate intersections (30% chance per intersection) for (var i = 0; i < intersectionPoints.length; i++) { var intersection = intersectionPoints[i]; var pillarX = intersection.x; var pillarY = intersection.y; // Check if pillar is within player radius var distanceToPlayer = Math.abs(pillarX - playerX) + Math.abs(pillarY - playerY); if (distanceToPlayer > playerRadius) { continue; // Skip if too far from player } // Check if position is in open corridor area (not in room and not wall) and near player if (self.isInCorridorArea(pillarX, pillarY, rooms) && self.canPlaceCorridorPillar(pillarX, pillarY) && Math.random() < 0.3) { if (pillarX >= 0 && pillarX < worldGrid.width && pillarY >= 0 && pillarY < worldGrid.height) { worldGrid.walls[pillarX][pillarY] = true; } } } }; // Check if position is in a corridor area (open space not in any room) self.isInCorridorArea = function (x, y, rooms) { // First check if position is open (not a wall) if (x < 0 || x >= worldGrid.width || y < 0 || y >= worldGrid.height) { return false; } if (worldGrid.walls[x][y]) { return false; // Already a wall } // Check if position is inside any room for (var i = 0; i < rooms.length; i++) { var room = rooms[i]; if (x >= room.x && x < room.x + room.width && y >= room.y && y < room.y + room.height) { return false; // Inside a room } } return true; // In corridor area }; // Check if a corridor pillar can be placed (ensure it doesn't block pathways) self.canPlaceCorridorPillar = function (x, y) { // Ensure there's enough space around the pillar (2x2 area check) var checkRadius = 1; var openSpaces = 0; var totalSpaces = 0; for (var dx = -checkRadius; dx <= checkRadius; dx++) { for (var dy = -checkRadius; dy <= checkRadius; dy++) { var checkX = x + dx; var checkY = y + dy; if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) { totalSpaces++; if (!worldGrid.walls[checkX][checkY]) { openSpaces++; } } } } // Only place pillar if at least 60% of surrounding area is open return openSpaces >= totalSpaces * 0.6; }; return self; }); var RaycastRenderer = Container.expand(function () { var self = Container.call(this); self.screenWidth = 2732; self.screenHeight = 2048; self.numRays = 128; // Number of rays to cast self.wallColumns = []; self.floorColumns = []; self.ceilingColumns = []; // Initialize rendering columns for (var i = 0; i < self.numRays; i++) { var stripWidth = self.screenWidth / self.numRays; // Wall column var wallCol = self.addChild(LK.getAsset('wallSegment', { anchorX: 0.5, anchorY: 0.5 })); wallCol.x = i * stripWidth + stripWidth / 2; wallCol.y = self.screenHeight / 2; wallCol.width = stripWidth + 1; // Small overlap to prevent gaps wallCol.visible = false; self.wallColumns.push(wallCol); // Floor column var floorCol = self.addChild(LK.getAsset('floorStrip', { anchorX: 0.5, anchorY: 0 })); floorCol.x = i * stripWidth + stripWidth / 2; floorCol.width = stripWidth + 1; floorCol.visible = false; self.floorColumns.push(floorCol); // Ceiling column var ceilCol = self.addChild(LK.getAsset('ceilingStrip', { anchorX: 0.5, anchorY: 1 })); ceilCol.x = i * stripWidth + stripWidth / 2; ceilCol.width = stripWidth + 1; ceilCol.visible = false; self.ceilingColumns.push(ceilCol); } self.render = function (player) { var fov = Math.PI / 2; // 90 degrees field of view for classic raycasting var halfFov = fov / 2; var stripWidth = self.screenWidth / self.numRays; var screenCenter = self.screenHeight / 2; var pitchOffset = player.pitch * 300; // Cast rays across the field of view for (var i = 0; i < self.numRays; i++) { var rayAngle = player.angle - halfFov + i / self.numRays * fov; var rayData = self.castRay(player.x, player.y, rayAngle); var distance = rayData.distance; var wallHeight = 0; var wallCol = self.wallColumns[i]; var floorCol = self.floorColumns[i]; floorCol.height = self.screenHeight - wallHeight; // Extend floor strip to cover entire floor area var ceilCol = self.ceilingColumns[i]; // Check if column is within strict horizontal screen bounds var columnX = wallCol.x; var withinBounds = columnX >= 0 && columnX <= self.screenWidth; if (rayData.hit && withinBounds) { // Fish-eye correction var correctedDistance = distance * Math.cos(rayAngle - player.angle); // Calculate wall height based on distance wallHeight = Math.max(50, worldGrid.cellSize * 800 / (correctedDistance + 1)); // Wall rendering wallCol.height = wallHeight; wallCol.x = Math.max(0, Math.min(self.screenWidth, wallCol.x)); // Clamp X position to screen bounds wallCol.y = screenCenter + pitchOffset; wallCol.visible = true; // Distance-based shading with special door rendering var shadingFactor = Math.max(0.15, 1.0 - correctedDistance / 800); var tintValue = 0xFFFFFF; // Special rendering for door - make it distinct and always visible if (self.lastHitType === 'door') { // Use bright glitchy colors for door visibility var doorColors = [0xFFFFFF, 0x00FFFF, 0xFFFF00, 0xFF00FF]; var colorIndex = Math.floor(LK.ticks * 0.2 % doorColors.length); wallCol.tint = doorColors[colorIndex]; // Ensure door is always visible with strong contrast wallCol.alpha = 1.0; // Make door walls slightly taller for better visibility wallCol.height = wallHeight * 1.2; } else { wallCol.tint = tintValue; wallCol.alpha = 1.0; } // Floor rendering with distance-based shading var wallBottom = screenCenter + wallHeight / 2 + pitchOffset; var floorHeight = self.screenHeight - wallBottom; floorCol.y = wallBottom; floorCol.height = Math.max(1, floorHeight); floorCol.visible = true; // Apply distance-based shading to the floor var floorShadingFactor = Math.max(0.2, 1.0 - correctedDistance / 800); floorCol.tint = 0xFFFFFF; // Ceiling rendering var ceilHeight = screenCenter - wallHeight / 2 + pitchOffset; ceilCol.y = ceilHeight; ceilCol.height = Math.max(1, ceilHeight); ceilCol.visible = true; ceilCol.tint = 0xFFFFFF; } else { // No wall hit or outside bounds - hide columns wallCol.visible = false; floorCol.visible = false; ceilCol.visible = false; } } }; // DDA (Digital Differential Analyzer) raycasting algorithm self.castRay = function (startX, startY, angle) { var rayX = startX; var rayY = startY; var rayDirX = Math.cos(angle); var rayDirY = Math.sin(angle); // Which grid cell we're in var mapX = Math.floor(rayX / worldGrid.cellSize); var mapY = Math.floor(rayY / worldGrid.cellSize); // Length of ray from current position to x or y side var deltaDistX = Math.abs(1 / rayDirX); var deltaDistY = Math.abs(1 / rayDirY); // Calculate step and initial sideDist var stepX, sideDistX; var stepY, sideDistY; if (rayDirX < 0) { stepX = -1; sideDistX = (rayX / worldGrid.cellSize - mapX) * deltaDistX; } else { stepX = 1; sideDistX = (mapX + 1.0 - rayX / worldGrid.cellSize) * deltaDistX; } if (rayDirY < 0) { stepY = -1; sideDistY = (rayY / worldGrid.cellSize - mapY) * deltaDistY; } else { stepY = 1; sideDistY = (mapY + 1.0 - rayY / worldGrid.cellSize) * deltaDistY; } // Perform DDA var hit = false; var side = 0; // 0 if x-side, 1 if y-side var maxSteps = 100; var steps = 0; while (!hit && steps < maxSteps) { steps++; // Jump to next map square, either in x-direction, or in y-direction if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; } else { sideDistY += deltaDistY; mapY += stepY; side = 1; } // Check if ray has hit a wall or door var checkX = mapX * worldGrid.cellSize; var checkY = mapY * worldGrid.cellSize; var hitWall = worldGrid.hasWallAt(checkX, checkY); var hitDoor = worldGrid.hasDoorAt(checkX, checkY); if (hitWall || hitDoor) { hit = true; // Store what type of surface was hit for special rendering self.lastHitType = hitDoor ? 'door' : 'wall'; } } var distance = 0; if (hit) { // Calculate distance if (side === 0) { distance = (mapX - rayX / worldGrid.cellSize + (1 - stepX) / 2) / rayDirX; } else { distance = (mapY - rayY / worldGrid.cellSize + (1 - stepY) / 2) / rayDirY; } distance = Math.abs(distance * worldGrid.cellSize); } return { hit: hit, distance: distance, side: side, mapX: mapX, mapY: mapY }; }; return self; }); var SensitivityConfig = Container.expand(function () { var self = Container.call(this); // Load saved sensitivity or default to 50 self.sensitivity = storage.sensitivity || 50; self.isVisible = false; // Create background panel var background = self.addChild(LK.getAsset('untexturedArea', { anchorX: 0, anchorY: 0, width: 300, height: 200, alpha: 0.8 })); background.tint = 0x222222; // Create title text var titleText = new Text2('Sensitivity', { size: 40, fill: 0xFFFFFF }); titleText.anchor.set(0.5, 0); titleText.x = 150; titleText.y = 20; self.addChild(titleText); // Create sensitivity value text var valueText = new Text2(self.sensitivity.toString(), { size: 35, fill: 0xFFFFFF }); valueText.anchor.set(0.5, 0); valueText.x = 150; valueText.y = 70; self.addChild(valueText); // Create decrease button (larger for mobile) var decreaseBtn = self.addChild(LK.getAsset('untexturedArea', { anchorX: 0.5, anchorY: 0.5, width: 70, height: 60 })); decreaseBtn.x = 80; decreaseBtn.y = 130; decreaseBtn.tint = 0x666666; var decreaseText = new Text2('-', { size: 40, fill: 0xFFFFFF }); decreaseText.anchor.set(0.5, 0.5); decreaseText.x = 80; decreaseText.y = 130; self.addChild(decreaseText); // Create increase button (larger for mobile) var increaseBtn = self.addChild(LK.getAsset('untexturedArea', { anchorX: 0.5, anchorY: 0.5, width: 70, height: 60 })); increaseBtn.x = 220; increaseBtn.y = 130; increaseBtn.tint = 0x666666; var increaseText = new Text2('+', { size: 40, fill: 0xFFFFFF }); increaseText.anchor.set(0.5, 0.5); increaseText.x = 220; increaseText.y = 130; self.addChild(increaseText); // Update sensitivity display self.updateDisplay = function () { valueText.setText(self.sensitivity.toString()); // Save to storage storage.sensitivity = self.sensitivity; }; // Toggle visibility self.toggle = function () { self.isVisible = !self.isVisible; self.visible = self.isVisible; }; // Handle decrease button with visual feedback decreaseBtn.down = function (x, y, obj) { decreaseBtn.tint = 0x888888; // Lighten on press if (self.sensitivity > 0) { self.sensitivity = Math.max(0, self.sensitivity - 5); self.updateDisplay(); } }; decreaseBtn.up = function (x, y, obj) { decreaseBtn.tint = 0x666666; // Reset color on release }; // Handle increase button with visual feedback increaseBtn.down = function (x, y, obj) { increaseBtn.tint = 0x888888; // Lighten on press if (self.sensitivity < 100) { self.sensitivity = Math.min(100, self.sensitivity + 5); self.updateDisplay(); } }; increaseBtn.up = function (x, y, obj) { increaseBtn.tint = 0x666666; // Reset color on release }; // Add background click handler to prevent game interactions background.down = function (x, y, obj) { // Prevent event from bubbling to game return true; }; background.up = function (x, y, obj) { // Prevent event from bubbling to game return true; }; background.move = function (x, y, obj) { // Prevent event from bubbling to game return true; }; // Initially hidden self.visible = false; return self; }); /**** * Initialize Game ****/ // Create player var game = new LK.Game({ backgroundColor: 0x000000, orientation: 'landscape', width: 2732, height: 2048 }); /**** * Game Code ****/ // World coordinate system - grid-based layout with procedural generation var worldGrid = { cellSize: 200, width: 100, // Expanded world size for infinite generation height: 100, walls: [], // Will store wall positions // Initialize world grid with walls initializeGrid: function initializeGrid() { // Initialize walls array first - fill entire world with walls initially this.walls = []; for (var x = 0; x < this.width; x++) { this.walls[x] = []; for (var y = 0; y < this.height; y++) { // Start with all walls - procedural generation will carve out spaces this.walls[x][y] = true; } } // Create a starting room around spawn point (3x3 room) var spawnX = Math.floor(this.width / 2); var spawnY = Math.floor(this.height / 2); for (var x = spawnX - 1; x <= spawnX + 1; x++) { for (var y = spawnY - 1; y <= spawnY + 1; y++) { if (x >= 0 && x < this.width && y >= 0 && y < this.height) { this.walls[x][y] = false; } } } }, // Check if a grid position is a floor isFloor: function isFloor(gridX, gridY) { // Define logic to determine if a grid position is a floor // For now, assume any position not marked as a wall is a floor return !this.walls[gridX][gridY]; }, // Check if a world position has a wall hasWallAt: function hasWallAt(worldX, worldY) { var gridX = Math.floor(worldX / this.cellSize); var gridY = Math.floor(worldY / this.cellSize); if (gridX < 0 || gridX >= this.width || gridY < 0 || gridY >= this.height) { return true; // Outside bounds = wall } return this.walls[gridX][gridY]; }, // Check collision with wall boundaries (with player radius) - improved precision checkCollision: function checkCollision(worldX, worldY, radius) { radius = radius || 20; // Default player radius // Enhanced bounds checking with safety margin if (worldX < radius || worldX >= this.width * this.cellSize - radius || worldY < radius || worldY >= this.height * this.cellSize - radius) { return true; // Outside safe bounds = collision } // More comprehensive collision check - check multiple points around player circle var checkPoints = [ // Center point { x: worldX, y: worldY }, // Cardinal directions (primary edges) { x: worldX - radius, y: worldY }, // Left { x: worldX + radius, y: worldY }, // Right { x: worldX, y: worldY - radius }, // Top { x: worldX, y: worldY + radius }, // Bottom // Diagonal corners for better corner collision detection { x: worldX - radius * 0.7, y: worldY - radius * 0.7 }, // Top-left { x: worldX + radius * 0.7, y: worldY - radius * 0.7 }, // Top-right { x: worldX - radius * 0.7, y: worldY + radius * 0.7 }, // Bottom-left { x: worldX + radius * 0.7, y: worldY + radius * 0.7 }, // Bottom-right // Additional edge points for smoother wall sliding { x: worldX - radius * 0.5, y: worldY }, // Half-left { x: worldX + radius * 0.5, y: worldY }, // Half-right { x: worldX, y: worldY - radius * 0.5 }, // Half-top { x: worldX, y: worldY + radius * 0.5 } // Half-bottom ]; for (var i = 0; i < checkPoints.length; i++) { var point = checkPoints[i]; var pointGridX = Math.floor(point.x / this.cellSize); var pointGridY = Math.floor(point.y / this.cellSize); // Enhanced bounds check if (pointGridX < 0 || pointGridX >= this.width || pointGridY < 0 || pointGridY >= this.height) { return true; } // Check wall collision if (this.walls[pointGridX][pointGridY] && !this.isFloor(pointGridX, pointGridY)) { return true; } } return false; }, // Convert screen coordinates to world coordinates screenToWorld: function screenToWorld(screenX, screenY) { return { x: screenX, y: screenY }; }, // Convert world coordinates to screen coordinates worldToScreen: function worldToScreen(worldX, worldY) { return { x: worldX, y: worldY }; }, // Enhanced collision checking with distance-based precision checkPreciseCollision: function checkPreciseCollision(worldX, worldY, radius, direction) { radius = radius || 20; // Calculate multiple check points based on movement direction var checkPoints = []; var numPoints = 8; // More points for better precision // Add center point checkPoints.push({ x: worldX, y: worldY }); // Add circular check points around player for (var i = 0; i < numPoints; i++) { var angle = i / numPoints * Math.PI * 2; checkPoints.push({ x: worldX + Math.cos(angle) * radius, y: worldY + Math.sin(angle) * radius }); } // Check each point for collision for (var i = 0; i < checkPoints.length; i++) { var point = checkPoints[i]; if (this.hasWallAt(point.x, point.y)) { return true; } } return false; }, // Check if player can move to position with wall sliding support canMoveTo: function canMoveTo(fromX, fromY, toX, toY, radius) { radius = radius || 20; // Direct movement check if (!this.checkCollision(toX, toY, radius)) { return { canMove: true, newX: toX, newY: toY }; } // Try wall sliding - horizontal only if (!this.checkCollision(toX, fromY, radius)) { return { canMove: true, newX: toX, newY: fromY }; } // Try wall sliding - vertical only if (!this.checkCollision(fromX, toY, radius)) { return { canMove: true, newX: fromX, newY: toY }; } // No valid movement return { canMove: false, newX: fromX, newY: fromY }; } }; // Initialize the world grid worldGrid.initializeGrid(); // Create procedural generator var procGen = new ProcGen(); // Generate initial chunks around spawn point procGen.generateAroundPlayer(worldGrid.width * worldGrid.cellSize / 2, worldGrid.height * worldGrid.cellSize / 2); // Add wall line completion system worldGrid.completeWallLines = function () { // Trace horizontal lines and complete them for (var y = 0; y < this.height; y++) { var wallStart = -1; var wallEnd = -1; // Find wall segments in this row for (var x = 0; x < this.width; x++) { if (this.walls[x][y]) { if (wallStart === -1) { wallStart = x; // Start of wall segment } wallEnd = x; // Update end of wall segment } else { // If we found a wall segment, complete the line between start and end if (wallStart !== -1 && wallEnd !== -1 && wallEnd > wallStart) { for (var fillX = wallStart; fillX <= wallEnd; fillX++) { this.walls[fillX][y] = true; // Fill the gap } } wallStart = -1; // Reset for next segment wallEnd = -1; } } // Complete any remaining segment at end of row if (wallStart !== -1 && wallEnd !== -1 && wallEnd > wallStart) { for (var fillX = wallStart; fillX <= wallEnd; fillX++) { this.walls[fillX][y] = true; } } } // Trace vertical lines and complete them for (var x = 0; x < this.width; x++) { var wallStart = -1; var wallEnd = -1; // Find wall segments in this column for (var y = 0; y < this.height; y++) { if (this.walls[x][y]) { if (wallStart === -1) { wallStart = y; // Start of wall segment } wallEnd = y; // Update end of wall segment } else { // If we found a wall segment, complete the line between start and end if (wallStart !== -1 && wallEnd !== -1 && wallEnd > wallStart) { for (var fillY = wallStart; fillY <= wallEnd; fillY++) { this.walls[x][fillY] = true; // Fill the gap } } wallStart = -1; // Reset for next segment wallEnd = -1; } } // Complete any remaining segment at end of column if (wallStart !== -1 && wallEnd !== -1 && wallEnd > wallStart) { for (var fillY = wallStart; fillY <= wallEnd; fillY++) { this.walls[x][fillY] = true; } } } }; // Apply wall line completion after initial generation worldGrid.completeWallLines(); // Add dead-end detection system that preserves room connectivity worldGrid.detectAndFillDeadEnds = function () { // Find small isolated areas (not connected to main network) and mark them as walls var visited = []; // Initialize visited array for (var x = 0; x < this.width; x++) { visited[x] = []; for (var y = 0; y < this.height; y++) { visited[x][y] = false; } } // Function to check if an area is a meaningful connected space var isSignificantArea = function isSignificantArea(startX, startY) { if (worldGrid.walls[startX][startY]) { return true; } // Wall positions are fine if (visited[startX][startY]) { return true; } // Already processed var localVisited = []; for (var x = 0; x < worldGrid.width; x++) { localVisited[x] = []; for (var y = 0; y < worldGrid.height; y++) { localVisited[x][y] = false; } } var area = []; var stack = [{ x: startX, y: startY }]; var hasChunkExit = false; // Flood fill to find connected area while (stack.length > 0) { var current = stack.pop(); var x = current.x; var y = current.y; if (x < 0 || x >= worldGrid.width || y < 0 || y >= worldGrid.height) { continue; } if (localVisited[x][y] || worldGrid.walls[x][y]) { continue; } localVisited[x][y] = true; visited[x][y] = true; // Mark as processed in main visited array area.push({ x: x, y: y }); // Check if this area connects to chunk boundaries (significant exit) if (x <= 1 || x >= worldGrid.width - 2 || y <= 1 || y >= worldGrid.height - 2) { hasChunkExit = true; } // Add adjacent cells var directions = [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }]; for (var d = 0; d < directions.length; d++) { stack.push({ x: x + directions[d].dx, y: y + directions[d].dy }); } } // Only fill small areas (less than 8 cells) that don't connect to chunk boundaries if (area.length < 8 && !hasChunkExit) { for (var i = 0; i < area.length; i++) { worldGrid.walls[area[i].x][area[i].y] = true; } return false; // Area was filled } return true; // Area is significant and kept }; // Check all open areas for significance for (var x = 0; x < this.width; x++) { for (var y = 0; y < this.height; y++) { if (!visited[x][y] && !this.walls[x][y]) { isSignificantArea(x, y); } } } }; // Apply dead-end detection after wall completion worldGrid.detectAndFillDeadEnds(); // Add passage recognition system worldGrid.passageRecognition = function () { // Find all isolated rooms (areas without proper exits) var isolatedRooms = this.findIsolatedRooms(); // Create passages for isolated rooms for (var i = 0; i < isolatedRooms.length; i++) { this.createPassageForRoom(isolatedRooms[i]); } }; // Find rooms that don't have adequate exits worldGrid.findIsolatedRooms = function () { var visited = []; var isolatedRooms = []; // Initialize visited array for (var x = 0; x < this.width; x++) { visited[x] = []; for (var y = 0; y < this.height; y++) { visited[x][y] = false; } } // Check each open area for connectivity for (var x = 1; x < this.width - 1; x++) { for (var y = 1; y < this.height - 1; y++) { if (!this.walls[x][y] && !visited[x][y]) { var room = this.analyzeRoom(x, y, visited); if (room && room.area.length >= 4) { // Only consider rooms with at least 4 cells var exitCount = this.countRoomExits(room); if (exitCount === 0) { isolatedRooms.push(room); } } } } } return isolatedRooms; }; // Analyze a room starting from given coordinates worldGrid.analyzeRoom = function (startX, startY, visited) { var room = { area: [], bounds: { minX: startX, maxX: startX, minY: startY, maxY: startY }, center: { x: 0, y: 0 } }; var stack = [{ x: startX, y: startY }]; while (stack.length > 0) { var current = stack.pop(); var x = current.x; var y = current.y; if (x < 0 || x >= this.width || y < 0 || y >= this.height) { continue; } if (visited[x][y] || this.walls[x][y]) { continue; } visited[x][y] = true; room.area.push({ x: x, y: y }); // Update bounds room.bounds.minX = Math.min(room.bounds.minX, x); room.bounds.maxX = Math.max(room.bounds.maxX, x); room.bounds.minY = Math.min(room.bounds.minY, y); room.bounds.maxY = Math.max(room.bounds.maxY, y); // Add adjacent cells var directions = [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }]; for (var d = 0; d < directions.length; d++) { stack.push({ x: x + directions[d].dx, y: y + directions[d].dy }); } } // Calculate center if (room.area.length > 0) { var centerX = Math.floor((room.bounds.minX + room.bounds.maxX) / 2); var centerY = Math.floor((room.bounds.minY + room.bounds.maxY) / 2); room.center = { x: centerX, y: centerY }; } return room.area.length > 0 ? room : null; }; // Count the number of exits a room has worldGrid.countRoomExits = function (room) { var exits = 0; var checkedPositions = []; // Check room perimeter for connections to other areas for (var i = 0; i < room.area.length; i++) { var cell = room.area[i]; var directions = [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }]; for (var d = 0; d < directions.length; d++) { var checkX = cell.x + directions[d].dx; var checkY = cell.y + directions[d].dy; // Skip if out of bounds if (checkX < 0 || checkX >= this.width || checkY < 0 || checkY >= this.height) { continue; } // If we find an open area that's not part of this room, it's a potential exit if (!this.walls[checkX][checkY]) { var isPartOfRoom = false; for (var j = 0; j < room.area.length; j++) { if (room.area[j].x === checkX && room.area[j].y === checkY) { isPartOfRoom = true; break; } } if (!isPartOfRoom) { // Check if this exit position was already counted var posKey = checkX + ',' + checkY; var alreadyCounted = false; for (var k = 0; k < checkedPositions.length; k++) { if (checkedPositions[k] === posKey) { alreadyCounted = true; break; } } if (!alreadyCounted) { exits++; checkedPositions.push(posKey); } } } } } return exits; }; // Create a passage for an isolated room worldGrid.createPassageForRoom = function (room) { if (!room || room.area.length === 0) { return; } // Find the best direction to create a passage var directions = [{ dx: 1, dy: 0, name: 'east' }, { dx: -1, dy: 0, name: 'west' }, { dx: 0, dy: 1, name: 'south' }, { dx: 0, dy: -1, name: 'north' }]; var bestDirection = null; var shortestDistance = Infinity; // For each direction, find the shortest path to open space for (var d = 0; d < directions.length; d++) { var dir = directions[d]; var distance = this.findDistanceToOpenSpace(room.center.x, room.center.y, dir.dx, dir.dy); if (distance < shortestDistance && distance > 0) { shortestDistance = distance; bestDirection = dir; } } // Create passage in the best direction if (bestDirection && shortestDistance <= 5) { // Limit passage length this.createPassageInDirection(room.center.x, room.center.y, bestDirection.dx, bestDirection.dy, shortestDistance); } else { // If no good direction found, create a passage to the nearest chunk boundary this.createPassageToChunkBoundary(room); } }; // Find distance to open space in a given direction worldGrid.findDistanceToOpenSpace = function (startX, startY, dirX, dirY) { var distance = 0; var maxDistance = 6; // Limit search distance for (var i = 1; i <= maxDistance; i++) { var checkX = startX + dirX * i; var checkY = startY + dirY * i; // Check bounds if (checkX < 1 || checkX >= this.width - 1 || checkY < 1 || checkY >= this.height - 1) { return maxDistance + 1; // Out of bounds } // If we find open space, return distance if (!this.walls[checkX][checkY]) { return i; } distance = i; } return distance; }; // Create a passage in the specified direction worldGrid.createPassageInDirection = function (startX, startY, dirX, dirY, length) { for (var i = 0; i <= length; i++) { var passageX = startX + dirX * i; var passageY = startY + dirY * i; // Ensure passage coordinates are valid if (passageX >= 0 && passageX < this.width && passageY >= 0 && passageY < this.height) { this.walls[passageX][passageY] = false; // Create wider passage (2 cells wide) for better navigation if (dirX !== 0) { // Horizontal passage if (passageY + 1 < this.height) { this.walls[passageX][passageY + 1] = false; } if (passageY - 1 >= 0) { this.walls[passageX][passageY - 1] = false; } } else { // Vertical passage if (passageX + 1 < this.width) { this.walls[passageX + 1][passageY] = false; } if (passageX - 1 >= 0) { this.walls[passageX - 1][passageY] = false; } } } } }; // Create passage to chunk boundary if no nearby open space worldGrid.createPassageToChunkBoundary = function (room) { var centerX = room.center.x; var centerY = room.center.y; // Find closest chunk boundary var distanceToLeft = centerX; var distanceToRight = this.width - 1 - centerX; var distanceToTop = centerY; var distanceToBottom = this.height - 1 - centerY; var minDistance = Math.min(distanceToLeft, distanceToRight, distanceToTop, distanceToBottom); if (minDistance === distanceToLeft) { // Create passage to left boundary this.createPassageInDirection(centerX, centerY, -1, 0, distanceToLeft); } else if (minDistance === distanceToRight) { // Create passage to right boundary this.createPassageInDirection(centerX, centerY, 1, 0, distanceToRight); } else if (minDistance === distanceToTop) { // Create passage to top boundary this.createPassageInDirection(centerX, centerY, 0, -1, distanceToTop); } else { // Create passage to bottom boundary this.createPassageInDirection(centerX, centerY, 0, 1, distanceToBottom); } }; // Apply passage recognition system after dead-end detection worldGrid.passageRecognition(); // Add method to check for isolated areas near player and create passages worldGrid.checkPlayerProximityForPassages = function (playerX, playerY) { var playerGridX = Math.floor(playerX / this.cellSize); var playerGridY = Math.floor(playerY / this.cellSize); var checkRadius = 8; // Check 8 grid cells around player // Check areas around player for potential isolation for (var dx = -checkRadius; dx <= checkRadius; dx++) { for (var dy = -checkRadius; dy <= checkRadius; dy++) { var checkX = playerGridX + dx; var checkY = playerGridY + dy; // Skip if out of bounds if (checkX < 1 || checkX >= this.width - 1 || checkY < 1 || checkY >= this.height - 1) { continue; } // If this is an open area, check if it needs a passage if (!this.walls[checkX][checkY]) { var needsPassage = this.checkIfAreaNeedsPassage(checkX, checkY, playerGridX, playerGridY); if (needsPassage) { this.createEmergencyPassage(checkX, checkY, playerGridX, playerGridY); } } } } }; // Check if an area needs an emergency passage worldGrid.checkIfAreaNeedsPassage = function (areaX, areaY, playerX, playerY) { // Quick flood fill to check if this area has limited connectivity var visited = []; for (var x = 0; x < this.width; x++) { visited[x] = []; for (var y = 0; y < this.height; y++) { visited[x][y] = false; } } var reachableCells = []; var stack = [{ x: areaX, y: areaY }]; var hasChunkExit = false; while (stack.length > 0 && reachableCells.length < 50) { // Limit search for performance var current = stack.pop(); var x = current.x; var y = current.y; if (x < 0 || x >= this.width || y < 0 || y >= this.height) { continue; } if (visited[x][y] || this.walls[x][y]) { continue; } visited[x][y] = true; reachableCells.push({ x: x, y: y }); // Check if area connects to chunk boundaries if (x <= 2 || x >= this.width - 3 || y <= 2 || y >= this.height - 3) { hasChunkExit = true; } // Add adjacent cells var directions = [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }]; for (var d = 0; d < directions.length; d++) { stack.push({ x: x + directions[d].dx, y: y + directions[d].dy }); } } // If area is small and doesn't connect to chunk boundaries, it needs a passage return reachableCells.length < 20 && !hasChunkExit; }; // Create an emergency passage from isolated area toward player or main areas worldGrid.createEmergencyPassage = function (areaX, areaY, playerX, playerY) { // Calculate direction toward player var dirToPlayerX = playerX - areaX; var dirToPlayerY = playerY - areaY; // Normalize direction var dirX = dirToPlayerX > 0 ? 1 : dirToPlayerX < 0 ? -1 : 0; var dirY = dirToPlayerY > 0 ? 1 : dirToPlayerY < 0 ? -1 : 0; // Create passage toward player or toward center var targetX = dirX !== 0 ? areaX + dirX * 3 : areaX; var targetY = dirY !== 0 ? areaY + dirY * 3 : areaY; // Ensure target is within bounds targetX = Math.max(1, Math.min(this.width - 2, targetX)); targetY = Math.max(1, Math.min(this.height - 2, targetY)); // Create L-shaped passage to target this.createSimplePassage(areaX, areaY, targetX, targetY); }; // Create a simple passage between two points worldGrid.createSimplePassage = function (x1, y1, x2, y2) { // Create horizontal segment first var minX = Math.min(x1, x2); var maxX = Math.max(x1, x2); for (var x = minX; x <= maxX; x++) { if (x >= 0 && x < this.width && y1 >= 0 && y1 < this.height) { this.walls[x][y1] = false; } } // Create vertical segment var minY = Math.min(y1, y2); var maxY = Math.max(y1, y2); for (var y = minY; y <= maxY; y++) { if (x2 >= 0 && x2 < this.width && y >= 0 && y < this.height) { this.walls[x2][y] = false; } } }; // Create geometric wall renderer var wallRenderer = new GeometricWallRenderer(); game.addChild(wallRenderer); // Create ceiling tile renderer var ceilingTileRenderer = new CeilingTileRenderer(); game.addChild(ceilingTileRenderer); ceilingTileRenderer.generateTiles(); // Create light manager var lightManager = new LightManager(); game.addChild(lightManager); // Add lights at random positions for (var i = 0; i < 10; i++) { var randomX = Math.random() * worldGrid.width * worldGrid.cellSize; var randomY = Math.random() * worldGrid.height * worldGrid.cellSize; lightManager.addLight(randomX, randomY); } // Function to find a safe spawn position function findSafeSpawnPosition(startX, startY, searchRadius) { searchRadius = searchRadius || 5; // First check if starting position is safe if (!worldGrid.checkCollision(startX, startY)) { return { x: startX, y: startY }; } // Search in expanding circles for a safe position for (var radius = 1; radius <= searchRadius; radius++) { for (var angle = 0; angle < Math.PI * 2; angle += Math.PI / 8) { var testX = startX + Math.cos(angle) * radius * worldGrid.cellSize; var testY = startY + Math.sin(angle) * radius * worldGrid.cellSize; // Check bounds if (testX >= worldGrid.cellSize && testX < (worldGrid.width - 1) * worldGrid.cellSize && testY >= worldGrid.cellSize && testY < (worldGrid.height - 1) * worldGrid.cellSize) { if (!worldGrid.checkCollision(testX, testY)) { return { x: testX, y: testY }; } } } } // If no safe position found in search radius, force create one var fallbackX = Math.floor(worldGrid.width / 2) * worldGrid.cellSize; var fallbackY = Math.floor(worldGrid.height / 2) * worldGrid.cellSize; // Clear a 3x3 area around fallback position var fallbackGridX = Math.floor(fallbackX / worldGrid.cellSize); var fallbackGridY = Math.floor(fallbackY / worldGrid.cellSize); for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var clearX = fallbackGridX + dx; var clearY = fallbackGridY + dy; if (clearX >= 0 && clearX < worldGrid.width && clearY >= 0 && clearY < worldGrid.height) { worldGrid.walls[clearX][clearY] = false; } } } return { x: fallbackX, y: fallbackY }; } // Create player var player = new Player(); // Find safe spawn position var spawnCenter = { x: worldGrid.width * worldGrid.cellSize / 2, y: worldGrid.height * worldGrid.cellSize / 2 }; var safeSpawn = findSafeSpawnPosition(spawnCenter.x, spawnCenter.y, 10); // Position player at safe spawn location player.x = safeSpawn.x; player.y = safeSpawn.y; player.targetX = safeSpawn.x; player.targetY = safeSpawn.y; game.addChild(player); // Create raycasting renderer var raycastRenderer = new RaycastRenderer(); game.addChild(raycastRenderer); // FPS counter variables var fpsCounter = 0; var fpsDisplay = 0; var lastFpsTime = Date.now(); // Create coordinate display text var coordXText = new Text2('X: 0', { size: 60, fill: 0xFFFFFF }); coordXText.anchor.set(0, 0); coordXText.x = 120; // Avoid top-left 100x100 area coordXText.y = 120; LK.gui.addChild(coordXText); var coordZText = new Text2('Z: 0', { size: 60, fill: 0xFFFFFF }); coordZText.anchor.set(0, 0); coordZText.x = 120; // Avoid top-left 100x100 area coordZText.y = 200; LK.gui.addChild(coordZText); // Create FPS display text var fpsText = new Text2('FPS: 60', { size: 60, fill: 0x00FF00 }); fpsText.anchor.set(0, 0); fpsText.x = 120; // Avoid top-left 100x100 area fpsText.y = 280; LK.gui.addChild(fpsText); // Create movement status display var movementText = new Text2('Standing Still', { size: 60, fill: 0xFFFFFF }); movementText.anchor.set(0, 0); movementText.x = 120; // Avoid top-left 100x100 area movementText.y = 360; LK.gui.addChild(movementText); // Create movement distance display var distanceText = new Text2('Distance: 0.0', { size: 50, fill: 0xFFFFFF }); distanceText.anchor.set(0, 0); distanceText.x = 120; // Avoid top-left 100x100 area distanceText.y = 440; LK.gui.addChild(distanceText); // Create room size display var roomSizeText = new Text2('Room: Unknown', { size: 50, fill: 0xFFFFFF }); roomSizeText.anchor.set(0, 0); roomSizeText.x = 120; // Avoid top-left 100x100 area roomSizeText.y = 480; LK.gui.addChild(roomSizeText); // Create settings button in top-right corner (larger for mobile) var settingsButton = LK.getAsset('untexturedArea', { anchorX: 1, anchorY: 0, width: 120, height: 120 }); settingsButton.tint = 0x444444; settingsButton.alpha = 0.7; LK.gui.topRight.addChild(settingsButton); var settingsText = new Text2('⚙', { size: 60, fill: 0xFFFFFF }); settingsText.anchor.set(0.5, 0.5); settingsText.x = -60; settingsText.y = 60; LK.gui.topRight.addChild(settingsText); // Create sensitivity configuration panel var sensitivityConfig = new SensitivityConfig(); sensitivityConfig.x = 2732 - 320; sensitivityConfig.y = 100; LK.gui.addChild(sensitivityConfig); // Create movement crosshair for better mobile controls var movementCrosshair = new MovementCrosshair(); movementCrosshair.x = 200; // Position on left side movementCrosshair.y = 2048 - 200; // Bottom left area LK.gui.addChild(movementCrosshair); // Function to find a random valid door position function findRandomDoorPosition() { var maxAttempts = 100; var attempts = 0; // Player spawn area - center of the world var spawnGridX = Math.floor(worldGrid.width / 2); var spawnGridY = Math.floor(worldGrid.height / 2); var minDistanceFromPlayer = 15; // Minimum distance in grid cells from player spawn while (attempts < maxAttempts) { // Generate random position in world bounds (avoid edges) var randomGridX = Math.floor(Math.random() * (worldGrid.width - 10)) + 5; var randomGridY = Math.floor(Math.random() * (worldGrid.height - 10)) + 5; // Calculate distance from player spawn position var distanceFromSpawn = Math.sqrt((randomGridX - spawnGridX) * (randomGridX - spawnGridX) + (randomGridY - spawnGridY) * (randomGridY - spawnGridY)); // Skip if too close to player spawn area if (distanceFromSpawn < minDistanceFromPlayer) { attempts++; continue; } var testX = randomGridX * worldGrid.cellSize; var testY = randomGridY * worldGrid.cellSize; // Check if position is in an open area (not a wall) if (!worldGrid.walls[randomGridX][randomGridY]) { // Check surrounding area for room space (3x3 area should be mostly open) var openCells = 0; var totalCells = 0; for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var checkX = randomGridX + dx; var checkY = randomGridY + dy; if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) { totalCells++; if (!worldGrid.walls[checkX][checkY]) { openCells++; } } } } // If at least 70% of surrounding area is open, it's a good position if (openCells >= totalCells * 0.7) { return { x: testX, y: testY, gridX: randomGridX, gridY: randomGridY }; } } attempts++; } // Fallback to guaranteed far position if no good position found // Place door in corner area far from spawn var fallbackGridX = worldGrid.width - 8; // Near right edge var fallbackGridY = worldGrid.height - 8; // Near bottom edge return { x: fallbackGridX * worldGrid.cellSize, y: fallbackGridY * worldGrid.cellSize, gridX: fallbackGridX, gridY: fallbackGridY }; } // Create door instance and position it randomly in world var door = new Door(); // Find a random valid position for the door var doorPosition = findRandomDoorPosition(); var doorX = doorPosition.x; var doorY = doorPosition.y; var doorGridX = doorPosition.gridX; var doorGridY = doorPosition.gridY; door.x = doorX; door.y = doorY; // Keep door at exact world level for proper collision and rendering game.addChild(door); // Add door tracking to world grid worldGrid.doorPosition = { x: doorX, y: doorY, gridX: doorGridX, gridY: doorGridY }; // Add door detection method to world grid worldGrid.hasDoorAt = function (worldX, worldY) { if (!this.doorPosition) return false; var dx = Math.abs(worldX - this.doorPosition.x); var dy = Math.abs(worldY - this.doorPosition.y); return dx < 50 && dy < 50; // Door collision area }; // Dynamic path generation system worldGrid.pathToExit = { generatedRooms: [], connectionPoints: [], lastPlayerDistance: Infinity, pathSegments: [], // Generate progressive path as player approaches door generatePathToDoor: function generatePathToDoor(playerX, playerY, doorX, doorY) { var playerGridX = Math.floor(playerX / worldGrid.cellSize); var playerGridY = Math.floor(playerY / worldGrid.cellSize); var doorGridX = Math.floor(doorX / worldGrid.cellSize); var doorGridY = Math.floor(doorY / worldGrid.cellSize); var distanceToDoor = Math.sqrt((playerGridX - doorGridX) * (playerGridX - doorGridX) + (playerGridY - doorGridY) * (playerGridY - doorGridY)); // Generate rooms when player is within 25 cells of door if (distanceToDoor <= 25 && distanceToDoor < this.lastPlayerDistance) { this.createPathSegment(playerGridX, playerGridY, doorGridX, doorGridY, distanceToDoor); } this.lastPlayerDistance = distanceToDoor; }, // Create a segment of the path with connected rooms createPathSegment: function createPathSegment(playerX, playerY, doorX, doorY, distance) { var segmentId = Math.floor(distance / 5); // Create segments every 5 units // Skip if this segment already exists for (var i = 0; i < this.pathSegments.length; i++) { if (this.pathSegments[i].id === segmentId) { return; } } // Calculate direction from player toward door var dirX = doorX - playerX; var dirY = doorY - playerY; var pathLength = Math.sqrt(dirX * dirX + dirY * dirY); if (pathLength > 0) { dirX = dirX / pathLength; dirY = dirY / pathLength; } // Create intermediate room positions along the path var numRooms = Math.min(3, Math.floor(distance / 8) + 1); var newRooms = []; for (var i = 0; i < numRooms; i++) { var t = (i + 1) / (numRooms + 1); var roomX = Math.floor(playerX + dirX * distance * t); var roomY = Math.floor(playerY + dirY * distance * t); // Add some randomness to avoid straight lines roomX += Math.floor(Math.random() * 6) - 3; roomY += Math.floor(Math.random() * 6) - 3; // Ensure room is within bounds roomX = Math.max(2, Math.min(worldGrid.width - 3, roomX)); roomY = Math.max(2, Math.min(worldGrid.height - 3, roomY)); var room = this.createConnectedRoom(roomX, roomY, 2 + Math.floor(Math.random() * 2)); if (room) { newRooms.push(room); this.generatedRooms.push(room); } } // Connect new rooms in sequence for (var i = 0; i < newRooms.length - 1; i++) { this.createRoomConnection(newRooms[i], newRooms[i + 1]); } // Connect first room to existing path or player area if (newRooms.length > 0) { var nearestExisting = this.findNearestExistingRoom(newRooms[0], playerX, playerY); if (nearestExisting) { this.createRoomConnection(newRooms[0], nearestExisting); } else { // Connect to player's current area this.createPathToPosition(newRooms[0], playerX, playerY); } // Connect last room toward door var lastRoom = newRooms[newRooms.length - 1]; this.createPathToPosition(lastRoom, doorX, doorY); } // Store segment information this.pathSegments.push({ id: segmentId, rooms: newRooms, playerDistance: distance }); }, // Create a connected room at specified position createConnectedRoom: function createConnectedRoom(centerX, centerY, size) { // Check if area is already carved var hasWalls = false; for (var dx = -size; dx <= size; dx++) { for (var dy = -size; dy <= size; dy++) { var checkX = centerX + dx; var checkY = centerY + dy; if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) { if (worldGrid.walls[checkX][checkY]) { hasWalls = true; break; } } } if (hasWalls) break; } // Only create room if there are walls to carve if (!hasWalls) { return null; } var room = { centerX: centerX, centerY: centerY, size: size, x: centerX - size, y: centerY - size, width: size * 2 + 1, height: size * 2 + 1 }; // Carve room with irregular shape this.carveConnectedRoom(centerX, centerY, size); return room; }, // Carve a room with connected shape carveConnectedRoom: function carveConnectedRoom(centerX, centerY, size) { var roomType = Math.random(); if (roomType < 0.4) { // Circular room for (var dx = -size; dx <= size; dx++) { for (var dy = -size; dy <= size; dy++) { var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= size + Math.random() * 0.5) { var roomX = centerX + dx; var roomY = centerY + dy; if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) { worldGrid.walls[roomX][roomY] = false; } } } } } else if (roomType < 0.7) { // L-shaped room // Horizontal bar for (var dx = -size; dx <= size; dx++) { for (var dy = -1; dy <= 1; dy++) { var roomX = centerX + dx; var roomY = centerY + dy; if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) { worldGrid.walls[roomX][roomY] = false; } } } // Vertical bar for (var dx = -1; dx <= 1; dx++) { for (var dy = -size; dy <= size; dy++) { var roomX = centerX + dx; var roomY = centerY + dy; if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) { worldGrid.walls[roomX][roomY] = false; } } } } else { // Rectangular room for (var dx = -size; dx <= size; dx++) { for (var dy = -size; dy <= size; dy++) { var roomX = centerX + dx; var roomY = centerY + dy; if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) { worldGrid.walls[roomX][roomY] = false; } } } } }, // Create connection between two rooms createRoomConnection: function createRoomConnection(room1, room2) { if (!room1 || !room2) return; var x1 = room1.centerX; var y1 = room1.centerY; var x2 = room2.centerX; var y2 = room2.centerY; // Create L-shaped corridor this.createWideCorridor(x1, y1, x2, y1); // Horizontal this.createWideCorridor(x2, y1, x2, y2); // Vertical }, // Create wide corridor between two points createWideCorridor: function createWideCorridor(x1, y1, x2, y2) { var minX = Math.min(x1, x2); var maxX = Math.max(x1, x2); var minY = Math.min(y1, y2); var maxY = Math.max(y1, y2); for (var x = minX; x <= maxX; x++) { for (var y = minY; y <= maxY; y++) { // Create 3-wide corridor for (var w = -1; w <= 1; w++) { var corridorX = x + (x1 === x2 ? w : 0); var corridorY = y + (y1 === y2 ? w : 0); if (corridorX >= 0 && corridorX < worldGrid.width && corridorY >= 0 && corridorY < worldGrid.height) { worldGrid.walls[corridorX][corridorY] = false; } } } } }, // Find nearest existing room to connect to findNearestExistingRoom: function findNearestExistingRoom(newRoom, playerX, playerY) { var nearestRoom = null; var minDistance = Infinity; for (var i = 0; i < this.generatedRooms.length; i++) { var room = this.generatedRooms[i]; if (room === newRoom) continue; var dx = newRoom.centerX - room.centerX; var dy = newRoom.centerY - room.centerY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < minDistance && distance < 15) { // Only connect to nearby rooms minDistance = distance; nearestRoom = room; } } return nearestRoom; }, // Create path from room to specific position createPathToPosition: function createPathToPosition(room, targetX, targetY) { if (!room) return; var distance = Math.sqrt((room.centerX - targetX) * (room.centerX - targetX) + (room.centerY - targetY) * (room.centerY - targetY)); // Only create path if target is reasonably close if (distance < 20) { this.createWideCorridor(room.centerX, room.centerY, targetX, targetY); } } }; // Create initial room around door var doorRoom = worldGrid.pathToExit.createConnectedRoom(doorGridX, doorGridY, 3); if (doorRoom) { worldGrid.pathToExit.generatedRooms.push(doorRoom); } // Create look up button (right side of screen) var lookUpButton = LK.getAsset('untexturedArea', { anchorX: 0.5, anchorY: 0.5, width: 150, height: 100 }); lookUpButton.tint = 0x444444; lookUpButton.alpha = 0.7; lookUpButton.x = 2732 - 200; // Right side lookUpButton.y = 2048 - 400; // Above look down button LK.gui.addChild(lookUpButton); var lookUpText = new Text2('▲', { size: 50, fill: 0xFFFFFF }); lookUpText.anchor.set(0.5, 0.5); lookUpText.x = 2732 - 200; lookUpText.y = 2048 - 400; LK.gui.addChild(lookUpText); // Create look down button (right side of screen) var lookDownButton = LK.getAsset('untexturedArea', { anchorX: 0.5, anchorY: 0.5, width: 150, height: 100 }); lookDownButton.tint = 0x444444; lookDownButton.alpha = 0.7; lookDownButton.x = 2732 - 200; // Right side lookDownButton.y = 2048 - 200; // Bottom right area LK.gui.addChild(lookDownButton); var lookDownText = new Text2('▼', { size: 50, fill: 0xFFFFFF }); lookDownText.anchor.set(0.5, 0.5); lookDownText.x = 2732 - 200; lookDownText.y = 2048 - 200; LK.gui.addChild(lookDownText); // Look up button handlers lookUpButton.down = function (x, y, obj) { lookUpButton.alpha = 1.0; // Visual feedback lookUp = true; }; lookUpButton.up = function (x, y, obj) { lookUpButton.alpha = 0.7; // Reset visual lookUp = false; }; // Look down button handlers lookDownButton.down = function (x, y, obj) { lookDownButton.alpha = 1.0; // Visual feedback lookDown = true; }; lookDownButton.up = function (x, y, obj) { lookDownButton.alpha = 0.7; // Reset visual lookDown = false; }; // Movement flags var moveForward = false; var moveBackward = false; var turnLeft = false; var turnRight = false; var lookUp = false; var lookDown = false; // Player movement recognition system var playerMovementRecognition = { lastX: 0, lastY: 0, lastAngle: 0, isMoving: false, wasMoving: false, movementStartTime: 0, movementDistance: 0, movementDirection: { x: 0, y: 0 }, rotationAmount: 0 }; // Touch controls for movement var touchStartX = 0; var touchStartY = 0; var touchActive = false; // Settings button click handler settingsButton.down = function (x, y, obj) { sensitivityConfig.toggle(); }; game.down = function (x, y, obj) { touchStartX = x; touchStartY = y; touchActive = true; // Forward movement on touch moveForward = true; }; game.up = function (x, y, obj) { touchActive = false; moveForward = false; moveBackward = false; turnLeft = false; turnRight = false; lookUp = false; lookDown = false; // Reset crosshair if not actively being used if (!movementCrosshair.activeButton) { movementCrosshair.resetMovement(); } }; game.move = function (x, y, obj) { if (!touchActive) { return; } var deltaX = x - touchStartX; var deltaY = y - touchStartY; // Horizontal movement for turning if (Math.abs(deltaX) > 50) { if (deltaX > 0) { turnRight = true; turnLeft = false; } else { turnLeft = true; turnRight = false; } } else { turnLeft = false; turnRight = false; } // Vertical movement - split between forward/backward and look up/down if (Math.abs(deltaY) > 50) { // If touch is in upper part of screen, use for looking up/down if (y < 1024) { // Upper half of screen for vertical look if (deltaY < 0) { lookUp = true; lookDown = false; } else { lookDown = true; lookUp = false; } moveForward = false; moveBackward = false; } else { // Lower half of screen for movement if (deltaY < 0) { moveForward = true; moveBackward = false; } else { moveBackward = true; moveForward = false; } lookUp = false; lookDown = false; } } else { lookUp = false; lookDown = false; } }; game.update = function () { // Initialize movement recognition on first frame if (playerMovementRecognition.lastX === 0 && playerMovementRecognition.lastY === 0) { playerMovementRecognition.lastX = player.x; playerMovementRecognition.lastY = player.y; playerMovementRecognition.lastAngle = player.angle; } // Update player rotation speed based on sensitivity (0-100 maps to 0.02-0.08) - reduced for lower sensitivity var sensitivityValue = sensitivityConfig.sensitivity; player.rotSpeed = 0.02 + sensitivityValue / 100 * 0.06; // Get movement state from crosshair var crosshairState = movementCrosshair.getMovementState(); // Handle movement (combine touch controls and crosshair) if (moveForward || crosshairState.forward) { player.moveForward(); } if (moveBackward || crosshairState.backward) { player.moveBackward(); } if (turnLeft || crosshairState.left) { player.turnLeft(); } if (turnRight || crosshairState.right) { player.turnRight(); } if (lookUp) { player.lookUp(); } if (lookDown) { player.lookDown(); } // Apply smooth interpolation player.updateSmooth(); // Movement Recognition System var currentTime = Date.now(); // Calculate movement deltas var deltaX = player.x - playerMovementRecognition.lastX; var deltaY = player.y - playerMovementRecognition.lastY; var deltaAngle = player.angle - playerMovementRecognition.lastAngle; // Handle angle wrapping for rotation detection if (deltaAngle > Math.PI) { deltaAngle -= 2 * Math.PI; } if (deltaAngle < -Math.PI) { deltaAngle += 2 * Math.PI; } // Calculate movement distance and rotation var movementDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); var rotationAmount = Math.abs(deltaAngle); // Update movement direction if (movementDistance > 0.1) { playerMovementRecognition.movementDirection.x = deltaX / movementDistance; playerMovementRecognition.movementDirection.y = deltaY / movementDistance; } // Determine if player is currently moving (position or rotation) var movementThreshold = 0.5; // Minimum movement to be considered "moving" var rotationThreshold = 0.01; // Minimum rotation to be considered "turning" var isCurrentlyMoving = movementDistance > movementThreshold || rotationAmount > rotationThreshold; // Update movement state playerMovementRecognition.wasMoving = playerMovementRecognition.isMoving; playerMovementRecognition.isMoving = isCurrentlyMoving; // Track movement start time if (!playerMovementRecognition.wasMoving && playerMovementRecognition.isMoving) { // Movement just started playerMovementRecognition.movementStartTime = currentTime; playerMovementRecognition.movementDistance = 0; // Start looping footstep sound LK.playMusic('4'); } // Stop footstep sound when movement stops if (playerMovementRecognition.wasMoving && !playerMovementRecognition.isMoving) { // Movement just stopped - stop footstep loop LK.stopMusic(); } // Accumulate total movement distance if (playerMovementRecognition.isMoving) { playerMovementRecognition.movementDistance += movementDistance; } // Update movement display var movementStatus = "Standing Still"; var movementColor = 0xFFFFFF; if (playerMovementRecognition.isMoving) { if (movementDistance > rotationAmount * 10) { // More movement than rotation if (deltaX > 0.1) { movementStatus = "Moving East"; } else if (deltaX < -0.1) { movementStatus = "Moving West"; } else if (deltaY > 0.1) { movementStatus = "Moving South"; } else if (deltaY < -0.1) { movementStatus = "Moving North"; } else { movementStatus = "Moving"; } movementColor = 0x00FF00; // Green for movement } else { // More rotation than movement if (deltaAngle > 0.01) { movementStatus = "Turning Right"; } else if (deltaAngle < -0.01) { movementStatus = "Turning Left"; } else { movementStatus = "Turning"; } movementColor = 0x00FFFF; // Cyan for rotation } } else if (playerMovementRecognition.wasMoving) { // Just stopped moving movementStatus = "Stopped"; movementColor = 0xFFFF00; // Yellow for just stopped } // Update display texts movementText.setText(movementStatus); movementText.fill = movementColor; // Update distance display (rounded to 1 decimal place) var totalDistance = Math.round(playerMovementRecognition.movementDistance * 10) / 10; distanceText.setText('Distance: ' + totalDistance); // Store current position and angle for next frame playerMovementRecognition.lastX = player.x; playerMovementRecognition.lastY = player.y; playerMovementRecognition.lastAngle = player.angle; playerMovementRecognition.rotationAmount = rotationAmount; // Generate new chunks as player moves if (LK.ticks % 30 === 0) { // Check every 30 frames for performance procGen.generateAroundPlayer(player.x, player.y); // Check for isolated areas near player and create passages proactively worldGrid.checkPlayerProximityForPassages(player.x, player.y); // Generate dynamic path to exit door as player approaches if (worldGrid.doorPosition) { worldGrid.pathToExit.generatePathToDoor(player.x, player.y, worldGrid.doorPosition.x, worldGrid.doorPosition.y); } } // Render the raycasted view raycastRenderer.render(player); // Render walls wallRenderer.render(player); // Render ceiling tiles ceilingTileRenderer.render(player); // Update FPS counter fpsCounter++; var currentTime = Date.now(); if (currentTime - lastFpsTime >= 1000) { // Update every second fpsDisplay = fpsCounter; fpsCounter = 0; lastFpsTime = currentTime; // Color code FPS display based on performance var fpsColor = 0x00FF00; // Green for good FPS (60+) if (fpsDisplay < 30) { fpsColor = 0xFF0000; // Red for poor FPS } else if (fpsDisplay < 50) { fpsColor = 0xFFFF00; // Yellow for moderate FPS } fpsText.fill = fpsColor; fpsText.setText('FPS: ' + fpsDisplay); } // Keep sound 1 playing continuously (check every 60 frames) if (LK.ticks % 60 === 0) { // Restart sound 1 if it's not playing to maintain continuous loop LK.getSound('1').play(); } // Update coordinate display var gridX = Math.floor(player.x / worldGrid.cellSize); var gridZ = Math.floor(player.y / worldGrid.cellSize); coordXText.setText('X: ' + gridX); coordZText.setText('Z: ' + gridZ); // Update room size display (check every 15 frames for performance) if (LK.ticks % 15 === 0) { var currentRoom = worldGrid.detectCurrentRoom(player.x, player.y); var roomDisplayText = 'Habitación: ' + currentRoom.type; if (currentRoom.area > 0) { roomDisplayText += ' (' + currentRoom.area + ' celdas)'; } roomSizeText.setText(roomDisplayText); roomSizeText.fill = currentRoom.color || 0xFFFFFF; } }; // Play background music (song 2) LK.playMusic('2'); // Play sound 3 as a loop LK.playMusic('3'); // Play sound 1 as looping sound separate from music LK.getSound('1').play(); worldGrid.isFloor = function (gridX, gridY) { // Define logic to determine if a grid position is a floor // For now, assume any position not marked as a wall is a floor return !this.walls[gridX][gridY]; }; // Room detection system worldGrid.detectCurrentRoom = function (playerX, playerY) { var playerGridX = Math.floor(playerX / this.cellSize); var playerGridY = Math.floor(playerY / this.cellSize); // Check if player is in a valid position if (playerGridX < 0 || playerGridX >= this.width || playerGridY < 0 || playerGridY >= this.height) { return { type: 'Outside', area: 0 }; } // If player is in a wall, return wall if (this.walls[playerGridX][playerGridY]) { return { type: 'Wall', area: 0 }; } // Flood fill to find connected room area var visited = []; for (var x = 0; x < this.width; x++) { visited[x] = []; for (var y = 0; y < this.height; y++) { visited[x][y] = false; } } var roomCells = []; var stack = [{ x: playerGridX, y: playerGridY }]; var bounds = { minX: playerGridX, maxX: playerGridX, minY: playerGridY, maxY: playerGridY }; // Flood fill to find all connected open cells while (stack.length > 0 && roomCells.length < 200) { // Limit for performance var current = stack.pop(); var x = current.x; var y = current.y; if (x < 0 || x >= this.width || y < 0 || y >= this.height) { continue; } if (visited[x][y] || this.walls[x][y]) { continue; } visited[x][y] = true; roomCells.push({ x: x, y: y }); // Update bounds bounds.minX = Math.min(bounds.minX, x); bounds.maxX = Math.max(bounds.maxX, x); bounds.minY = Math.min(bounds.minY, y); bounds.maxY = Math.max(bounds.maxY, y); // Add adjacent cells var directions = [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }]; for (var d = 0; d < directions.length; d++) { stack.push({ x: x + directions[d].dx, y: y + directions[d].dy }); } } var roomArea = roomCells.length; var roomType = 'Unknown'; var roomColor = 0xFFFFFF; // Classify room size based on area if (roomArea <= 4) { roomType = 'Pequeña'; roomColor = 0xFF6666; // Light red for small } else if (roomArea <= 16) { roomType = 'Mediana'; roomColor = 0xFFFF66; // Yellow for medium } else if (roomArea <= 50) { roomType = 'Grande'; roomColor = 0x66FF66; // Light green for large } else { roomType = 'Muy Grande'; roomColor = 0x66FFFF; // Cyan for very large } return { type: roomType, area: roomArea, color: roomColor, bounds: bounds, cells: roomCells }; };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var CeilingTileRenderer = Container.expand(function () {
var self = Container.call(this);
self.tiles = [];
// Generate ceiling tiles in safe positions (away from corners and walls)
self.generateTiles = function () {
for (var x = 2; x < worldGrid.width - 2; x++) {
for (var y = 2; y < worldGrid.height - 2; y++) {
// Only place tiles in open areas (not near walls or corners)
if (!worldGrid.hasWallAt(x * worldGrid.cellSize, y * worldGrid.cellSize) && !self.isNearCorner(x, y) && self.isSafePosition(x, y)) {
// Initialize tile before using its properties
var tile = {
worldX: x * worldGrid.cellSize + worldGrid.cellSize / 2,
worldY: y * worldGrid.cellSize + worldGrid.cellSize / 2,
sprite: null
};
// Add a light in the middle of the room
var light = self.addChild(LK.getAsset('smallLight', {
anchorX: 0.5,
anchorY: 0.5
}));
light.x = tile.worldX;
light.y = tile.worldY;
light.visible = true;
// Add ceilingTile as a light texture in random positions away from corners
var ceilingTile = self.addChild(LK.getAsset('ceilingTile', {
anchorX: 0.5,
anchorY: 0.5
}));
ceilingTile.x = tile.worldX + (Math.random() * 400 - 200);
ceilingTile.y = tile.worldY + (Math.random() * 400 - 200);
ceilingTile.visible = true;
self.tiles.push(tile);
}
}
}
};
// Check if position is near a corner
self.isNearCorner = function (gridX, gridY) {
// Check 3x3 area around position for wall density
var wallCount = 0;
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
var checkX = gridX + dx;
var checkY = gridY + dy;
if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) {
if (worldGrid.walls && worldGrid.walls[checkX] && worldGrid.walls[checkX][checkY]) {
wallCount++;
}
}
}
}
return wallCount >= 3; // Near corner if 3+ walls nearby
};
// Check if position is safe (center of open areas)
self.isSafePosition = function (gridX, gridY) {
// Ensure there's open space in all 4 cardinal directions
var directions = [{
x: 0,
y: -1
}, {
x: 1,
y: 0
}, {
x: 0,
y: 1
}, {
x: -1,
y: 0
}];
for (var i = 0; i < directions.length; i++) {
var checkX = gridX + directions[i].x;
var checkY = gridY + directions[i].y;
if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) {
if (worldGrid.walls && worldGrid.walls[checkX] && worldGrid.walls[checkX][checkY]) {
return false;
}
}
}
return true;
};
self.render = function (player) {
// Clear existing sprites
for (var i = 0; i < self.tiles.length; i++) {
if (self.tiles[i].sprite) {
self.tiles[i].sprite.visible = false;
}
}
var visibleTiles = [];
// Calculate which tiles are visible and their screen positions
for (var i = 0; i < self.tiles.length; i++) {
var tile = self.tiles[i];
var dx = tile.worldX - player.x;
var dy = tile.worldY - player.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Only render tiles within reasonable distance
if (distance < 800) {
// Calculate angle relative to player's view direction
var tileAngle = Math.atan2(dy, dx);
var angleDiff = tileAngle - player.angle;
// Normalize angle difference
while (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
while (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Check if tile is within field of view
var fov = Math.PI / 3;
if (Math.abs(angleDiff) < fov / 2) {
// Calculate screen X position
var screenX = 1366 + angleDiff / (fov / 2) * 1366;
// Only add tiles that are within horizontal screen bounds with margin
if (screenX >= -50 && screenX <= 2782) {
// Apply pitch offset to ceiling tiles
var pitchOffset = player.pitch * 400;
visibleTiles.push({
tile: tile,
distance: distance,
screenX: screenX,
screenY: 400 - 200 * (1000 / (distance + 100)) + pitchOffset // Project to ceiling with pitch
});
}
}
}
}
// Sort by distance (farthest first)
visibleTiles.sort(function (a, b) {
return b.distance - a.distance;
});
// Render visible tiles
for (var i = 0; i < visibleTiles.length; i++) {
var visibleTile = visibleTiles[i];
var tile = visibleTile.tile;
if (!tile.sprite) {
tile.sprite = self.addChild(LK.getAsset('normalCeiling', {
anchorX: 0.5,
anchorY: 0.5
}));
}
tile.sprite.x = visibleTile.screenX;
tile.sprite.y = visibleTile.screenY;
tile.sprite.visible = true;
// Scale based on distance
var scale = Math.max(0.1, 20 / (visibleTile.distance + 20));
tile.sprite.scaleX = scale;
tile.sprite.scaleY = scale;
}
};
return self;
});
var Door = Container.expand(function () {
var self = Container.call(this);
var doorGraphics = self.attachAsset('door', {
anchorX: 0.5,
anchorY: 0.5
});
self.isInteracting = false;
self.magneticRadius = 150; // Distance at which door starts attracting player
self.magneticStrength = 0.02; // Strength of magnetic pull
self.down = function (x, y, obj) {
if (!self.isInteracting) {
self.isInteracting = true;
self.triggerLevelTransition();
}
};
// Update method to create magnetic attraction effect
// Track last player position for crossing detection
self.lastPlayerPosition = self.lastPlayerPosition || {
x: 0,
y: 0
};
self.lastPlayerInDoor = self.lastPlayerInDoor || false;
self.update = function () {
if (!player || self.isInteracting) return;
// Use dynamic door position from worldGrid
var targetX = worldGrid.doorPosition ? worldGrid.doorPosition.x : self.x;
var targetY = worldGrid.doorPosition ? worldGrid.doorPosition.y : self.y;
// Keep door at exact world position (not raised above ground)
if (Math.abs(self.x - targetX) > 5) {
self.x = targetX;
}
if (Math.abs(self.y - targetY) > 5) {
self.y = targetY;
}
// Calculate distance to player
var dx = self.x - player.x;
var dy = self.y - player.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Add controlled glitch effect to make door recognizable as exit
// Slower color changes to maintain visibility
var glitchTintOptions = [0xFFFFFF, 0x00FFFF, 0xFFFF00, 0xFF00FF, 0xFFFFFF];
var glitchIndex = Math.floor(LK.ticks * 0.1 % glitchTintOptions.length);
doorGraphics.tint = glitchTintOptions[glitchIndex];
// Gentle scale pulsing for visibility without making door disappear
var scaleGlitch = 1.0 + Math.sin(LK.ticks * 0.2) * 0.1;
doorGraphics.scaleX = scaleGlitch;
doorGraphics.scaleY = scaleGlitch;
// Reduced jitter to prevent door from becoming invisible
var jitterRange = 2;
var jitterX = Math.sin(LK.ticks * 0.15) * jitterRange;
var jitterY = Math.cos(LK.ticks * 0.12) * jitterRange;
doorGraphics.x = jitterX;
doorGraphics.y = jitterY;
// Ensure door graphics stay visible
doorGraphics.visible = true;
doorGraphics.alpha = Math.max(0.7, 0.7 + Math.sin(LK.ticks * 0.1) * 0.3);
// Check if player is currently inside door area
var doorRadius = 60; // Door interaction area
var currentlyInDoor = distance < doorRadius;
// Detect door crossing - if player was outside and now inside, or vice versa
if (!self.lastPlayerInDoor && currentlyInDoor) {
// Player just entered door area - trigger level completion
self.isInteracting = true;
self.triggerLevelTransition();
}
// Update last position tracking
self.lastPlayerInDoor = currentlyInDoor;
self.lastPlayerPosition.x = player.x;
self.lastPlayerPosition.y = player.y;
// If player is within magnetic radius, apply attraction force
if (distance < self.magneticRadius && distance > 20) {
// Don't pull if too close
// Calculate attraction force (stronger when closer)
var force = self.magneticStrength * (self.magneticRadius - distance) / self.magneticRadius;
// Normalize direction vector
var dirX = dx / distance;
var dirY = dy / distance;
// Apply magnetic pull to player's target position
player.targetX += dirX * force * 10;
player.targetY += dirY * force * 10;
// Increase glitch intensity when attracting
var intensePulse = 0.5 + Math.sin(LK.ticks * 0.5) * 0.5;
doorGraphics.alpha = intensePulse;
} else {
doorGraphics.alpha = 1.0; // Reset alpha when not attracting
}
};
self.triggerLevelTransition = function () {
// Create completely black screen overlay
var blackScreen = LK.getAsset('untexturedArea', {
anchorX: 0,
anchorY: 0,
width: 2732,
height: 2048,
alpha: 0
});
blackScreen.tint = 0x000000;
LK.gui.addChild(blackScreen);
// Create level completion text - wider and white
var levelText = new Text2('NIVEL 1 COMPLETADO', {
size: 150,
fill: 0xFFFFFF
});
levelText.anchor.set(0.5, 0.5);
levelText.x = 1366; // Center of screen horizontally
levelText.y = -200; // Start above screen
levelText.alpha = 0;
LK.gui.addChild(levelText);
// Animate black screen fade in to completely dark
tween(blackScreen, {
alpha: 1.0
}, {
duration: 800,
onFinish: function onFinish() {
// Animate level text drop down and fade in - center of screen
tween(levelText, {
y: 1024,
alpha: 1
}, {
duration: 1200,
easing: tween.bounceOut,
onFinish: function onFinish() {
// Wait 2 seconds showing completion message
LK.setTimeout(function () {
// Fade out completion message
tween(levelText, {
alpha: 0
}, {
duration: 800,
onFinish: function onFinish() {
// Change to next level text - larger and white
levelText.setText('NIVEL 2');
levelText.fill = 0xFFFFFF;
levelText.size = 150;
levelText.anchor.set(0.5, 0.5);
levelText.x = 1366; // Ensure centered positioning
tween(levelText, {
alpha: 1
}, {
duration: 500,
onFinish: function onFinish() {
// Wait another second, then fade everything out
LK.setTimeout(function () {
tween(levelText, {
alpha: 0
}, {
duration: 1000
});
tween(blackScreen, {
alpha: 0
}, {
duration: 1500,
onFinish: function onFinish() {
blackScreen.destroy();
levelText.destroy();
self.isInteracting = false;
}
});
}, 1000);
}
});
}
});
}, 2000);
}
});
}
});
};
return self;
});
var GeometricWallRenderer = Container.expand(function () {
var self = Container.call(this);
// Simplified wall rendering with geometric shapes
self.wallStrips = [];
self.numStrips = 64; // Further reduced for better performance
self.maxWalls = 32; // Maximum number of wall strips to render
// Initialize wall strips pool
self.initWallStrips = function () {
for (var i = 0; i < self.maxWalls; i++) {
var wallStrip = self.addChild(LK.getAsset('wallSegment', {
anchorX: 0.5,
anchorY: 0.5
}));
wallStrip.visible = false;
self.wallStrips.push(wallStrip);
}
};
// Get available wall strip from pool
self.getWallStrip = function () {
for (var i = 0; i < self.wallStrips.length; i++) {
if (!self.wallStrips[i].visible) {
return self.wallStrips[i];
}
}
return null;
};
// Render walls using simplified column-based approach
self.render = function (player) {
// Initialize strips if not done
if (self.wallStrips.length === 0) {
self.initWallStrips();
}
// Hide all wall strips
for (var j = 0; j < self.wallStrips.length; j++) {
self.wallStrips[j].visible = false;
}
var fov = Math.PI / 3; // 60 degrees field of view
var halfFov = fov / 2;
var screenCenter = 1024; // Y center of screen
var pitchOffset = player.pitch * 300; // Apply pitch for vertical look
var stripWidth = 2732 / self.numStrips;
var wallsRendered = 0;
// Cast rays and render wall strips
for (var i = 0; i < self.numStrips && wallsRendered < self.maxWalls; i++) {
var rayAngle = player.angle - halfFov + i / self.numStrips * fov;
var rayData = self.castSimpleRay(player.x, player.y, rayAngle);
if (rayData.hit) {
var distance = rayData.distance;
// Apply fish-eye correction
var correctedDistance = distance * Math.cos(rayAngle - player.angle);
// Calculate screen X position for horizontal bounds checking
var screenX = i * stripWidth + stripWidth / 2;
// Only render if within strict horizontal screen bounds
if (screenX >= 0 && screenX <= 2732) {
// Get wall strip from pool
var wallStrip = self.getWallStrip();
if (wallStrip) {
// Calculate wall height based on distance
var baseWallSize = worldGrid.cellSize;
var wallHeight = Math.max(60, baseWallSize * (500 / (correctedDistance + 50)));
// Position wall strip within screen bounds
wallStrip.width = stripWidth + 2; // Add small overlap
wallStrip.height = wallHeight;
wallStrip.x = Math.max(0, Math.min(2732, screenX)); // Strictly clamp to screen bounds
wallStrip.y = screenCenter + pitchOffset;
wallStrip.visible = true;
// Apply distance-based shading
var shadingFactor = Math.max(0.2, 1.0 - correctedDistance / 600);
var tintValue = 0xFFFFFF;
// Special rendering for door
if (rayData.hitType === 'door') {
// Use bright, visible colors for door
var doorColors = [0xFFFFFF, 0x00FFFF, 0xFFFF00, 0xFF00FF];
var colorIndex = Math.floor(LK.ticks * 0.2 % doorColors.length);
wallStrip.tint = doorColors[colorIndex];
// Make door slightly taller
wallStrip.height = wallHeight * 1.1;
} else {
wallStrip.tint = tintValue;
// Add slight variation based on position for texture effect
var positionVariation = (rayData.hitX + rayData.hitY) % 40;
if (positionVariation < 20) {
tintValue = Math.floor(tintValue * 0.9); // Slightly darker
wallStrip.tint = tintValue << 16 | tintValue << 8 | tintValue;
}
}
wallsRendered++;
}
}
}
}
};
// Simplified raycasting for geometric walls
self.castSimpleRay = function (startX, startY, angle) {
var rayX = startX;
var rayY = startY;
var deltaX = Math.cos(angle) * 8; // Larger steps for performance
var deltaY = Math.sin(angle) * 8;
var distance = 0;
var maxDistance = 600;
var stepSize = 8;
// Raycast until wall hit or max distance
while (distance < maxDistance) {
rayX += deltaX;
rayY += deltaY;
distance += stepSize;
// Check for wall or door collision
var hitWall = worldGrid.hasWallAt(rayX, rayY);
var hitDoor = worldGrid.hasDoorAt(rayX, rayY);
if (hitWall || hitDoor) {
// Store hit type for special rendering
self.lastHitType = hitDoor ? 'door' : 'wall';
return {
hit: true,
distance: distance,
hitX: rayX,
hitY: rayY,
hitType: self.lastHitType
};
}
}
return {
hit: false,
distance: maxDistance,
hitX: rayX,
hitY: rayY
};
};
return self;
});
var LightManager = Container.expand(function () {
var self = Container.call(this);
self.lights = [];
// Method to add a light at a specific world position
self.addLight = function (worldX, worldY) {
var light = self.addChild(LK.getAsset('smallLight', {
anchorX: 0.5,
anchorY: 0.5
}));
light.x = worldX;
light.y = worldY;
light.visible = true;
self.lights.push(light);
};
// Method to clear all lights
self.clearLights = function () {
for (var i = 0; i < self.lights.length; i++) {
self.lights[i].visible = false;
}
self.lights = [];
};
return self;
});
var MovementCrosshair = Container.expand(function () {
var self = Container.call(this);
self.isActive = false;
self.activeButton = null;
// Create base circle
var base = self.attachAsset('crosshairBase', {
anchorX: 0.5,
anchorY: 0.5
});
base.alpha = 0.6;
// Create directional buttons
var upButton = self.attachAsset('crosshairUp', {
anchorX: 0.5,
anchorY: 0.5
});
upButton.x = 0;
upButton.y = -70;
upButton.alpha = 0.7;
var downButton = self.attachAsset('crosshairDown', {
anchorX: 0.5,
anchorY: 0.5
});
downButton.x = 0;
downButton.y = 70;
downButton.alpha = 0.7;
var leftButton = self.attachAsset('crosshairLeft', {
anchorX: 0.5,
anchorY: 0.5
});
leftButton.x = -70;
leftButton.y = 0;
leftButton.alpha = 0.7;
var rightButton = self.attachAsset('crosshairRight', {
anchorX: 0.5,
anchorY: 0.5
});
rightButton.x = 70;
rightButton.y = 0;
rightButton.alpha = 0.7;
var centerButton = self.attachAsset('crosshairCenter', {
anchorX: 0.5,
anchorY: 0.5
});
centerButton.alpha = 0.8;
// Movement state tracking
self.movementState = {
forward: false,
backward: false,
left: false,
right: false
};
// Button press handlers
upButton.down = function (x, y, obj) {
upButton.alpha = 1.0;
self.movementState.forward = true;
self.activeButton = 'up';
};
upButton.up = function (x, y, obj) {
upButton.alpha = 0.7;
self.movementState.forward = false;
if (self.activeButton === 'up') {
self.activeButton = null;
}
};
downButton.down = function (x, y, obj) {
downButton.alpha = 1.0;
self.movementState.backward = true;
self.activeButton = 'down';
};
downButton.up = function (x, y, obj) {
downButton.alpha = 0.7;
self.movementState.backward = false;
if (self.activeButton === 'down') {
self.activeButton = null;
}
};
leftButton.down = function (x, y, obj) {
leftButton.alpha = 1.0;
self.movementState.left = true;
self.activeButton = 'left';
};
leftButton.up = function (x, y, obj) {
leftButton.alpha = 0.7;
self.movementState.left = false;
if (self.activeButton === 'left') {
self.activeButton = null;
}
};
rightButton.down = function (x, y, obj) {
rightButton.alpha = 1.0;
self.movementState.right = true;
self.activeButton = 'right';
};
rightButton.up = function (x, y, obj) {
rightButton.alpha = 0.7;
self.movementState.right = false;
if (self.activeButton === 'right') {
self.activeButton = null;
}
};
// Get current movement state
self.getMovementState = function () {
return self.movementState;
};
// Reset all movement states
self.resetMovement = function () {
self.movementState.forward = false;
self.movementState.backward = false;
self.movementState.left = false;
self.movementState.right = false;
self.activeButton = null;
upButton.alpha = 0.7;
downButton.alpha = 0.7;
leftButton.alpha = 0.7;
rightButton.alpha = 0.7;
};
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
self.x = 1366;
self.y = 1024;
self.angle = 0;
self.pitch = 0; // Vertical look angle (up/down)
self.speed = 3;
self.rotSpeed = 0.1;
// Smooth interpolation properties
self.targetX = 1366;
self.targetY = 1024;
self.targetAngle = 0;
self.targetPitch = 0;
self.smoothingFactor = 0.15;
// Player visual for debugging (will be hidden in first person)
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
playerGraphics.visible = false; // Hide for first person view
self.moveForward = function () {
var newX = self.targetX + Math.cos(self.targetAngle) * self.speed;
var newY = self.targetY + Math.sin(self.targetAngle) * self.speed;
// Constrain Y coordinate to not go below 0
if (newY < 0) {
newY = 0;
}
// Check collision with world grid using improved collision detection
if (!worldGrid.checkCollision(newX, newY)) {
self.targetX = newX;
self.targetY = newY;
} else {
// Wall sliding - try to move along walls instead of stopping completely
// Try moving only horizontally if vertical movement is blocked
if (!worldGrid.checkCollision(newX, self.targetY)) {
self.targetX = newX;
}
// Try moving only vertically if horizontal movement is blocked
else if (!worldGrid.checkCollision(self.targetX, newY)) {
self.targetY = newY;
}
}
};
self.moveBackward = function () {
var newX = self.targetX - Math.cos(self.targetAngle) * self.speed;
var newY = self.targetY - Math.sin(self.targetAngle) * self.speed;
// Constrain Y coordinate to not go below 0
if (newY < 0) {
newY = 0;
}
// Check collision with world grid using improved collision detection
if (!worldGrid.checkCollision(newX, newY)) {
self.targetX = newX;
self.targetY = newY;
} else {
// Wall sliding - try to move along walls instead of stopping completely
// Try moving only horizontally if vertical movement is blocked
if (!worldGrid.checkCollision(newX, self.targetY)) {
self.targetX = newX;
}
// Try moving only vertically if horizontal movement is blocked
else if (!worldGrid.checkCollision(self.targetX, newY)) {
self.targetY = newY;
}
}
};
self.turnLeft = function () {
self.targetAngle -= self.rotSpeed;
};
self.turnRight = function () {
self.targetAngle += self.rotSpeed;
};
self.lookUp = function () {
self.targetPitch = Math.max(-Math.PI / 3, self.targetPitch - self.rotSpeed); // Limit to -60 degrees
};
self.lookDown = function () {
self.targetPitch = Math.min(Math.PI / 3, self.targetPitch + self.rotSpeed); // Limit to +60 degrees
};
self.updateSmooth = function () {
// Smooth interpolation for position
self.x += (self.targetX - self.x) * self.smoothingFactor;
self.y += (self.targetY - self.y) * self.smoothingFactor;
// Smooth interpolation for rotation with angle wrapping
var angleDiff = self.targetAngle - self.angle;
// Handle angle wrapping (ensure shortest rotation path)
if (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
if (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
self.angle += angleDiff * self.smoothingFactor;
// Smooth interpolation for pitch
var pitchDiff = self.targetPitch - self.pitch;
self.pitch += pitchDiff * self.smoothingFactor;
};
return self;
});
var ProcGen = Container.expand(function () {
var self = Container.call(this);
self.generatedChunks = {};
self.chunkSize = 6; // Much smaller chunks for tighter room placement
self.roomMinSize = 1;
self.roomMaxSize = 2;
self.hallwayWidth = 1; // Keep narrow hallways for claustrophobic effect
// Generate a procedural chunk at given chunk coordinates
self.generateChunk = function (chunkX, chunkY) {
var chunkKey = chunkX + ',' + chunkY;
if (self.generatedChunks[chunkKey]) {
return; // Already generated
}
self.generatedChunks[chunkKey] = true;
// Calculate world grid offset for this chunk
var offsetX = chunkX * self.chunkSize;
var offsetY = chunkY * self.chunkSize;
// Generate Backrooms-style layout for this chunk
self.generateBackroomsChunk(offsetX, offsetY);
};
// Generate Backrooms-style layout with guaranteed connectivity and multiple exits
self.generateBackroomsChunk = function (offsetX, offsetY) {
// First, fill entire chunk with walls
for (var x = offsetX; x < offsetX + self.chunkSize; x++) {
for (var y = offsetY; y < offsetY + self.chunkSize; y++) {
if (x >= 0 && x < worldGrid.width && y >= 0 && y < worldGrid.height) {
worldGrid.walls[x][y] = true;
}
}
}
// Create main room in center of chunk with irregular shape
var mainRoomSize = 2; // Smaller main room for claustrophobic effect
var mainRoomX = offsetX + Math.floor((self.chunkSize - mainRoomSize) / 2);
var mainRoomY = offsetY + Math.floor((self.chunkSize - mainRoomSize) / 2);
self.carveIrregularRoom(mainRoomX, mainRoomY, mainRoomSize, mainRoomSize);
// Create 3-5 additional rooms for higher density and claustrophobic feel
var numRooms = Math.floor(Math.random() * 3) + 3; // 3-5 rooms
var rooms = [{
x: mainRoomX,
y: mainRoomY,
width: mainRoomSize,
height: mainRoomSize,
centerX: mainRoomX + Math.floor(mainRoomSize / 2),
centerY: mainRoomY + Math.floor(mainRoomSize / 2)
}];
for (var i = 0; i < numRooms; i++) {
var attempts = 0;
while (attempts < 30) {
// Determine room size based on probabilities
var roomSizes = self.getRoomSizeByProbability();
var roomW = roomSizes.width;
var roomH = roomSizes.height;
var roomX = offsetX + Math.floor(Math.random() * (self.chunkSize - roomW - 2)) + 1;
var roomY = offsetY + Math.floor(Math.random() * (self.chunkSize - roomH - 2)) + 1;
var newRoom = {
x: roomX,
y: roomY,
width: roomW,
height: roomH,
centerX: roomX + Math.floor(roomW / 2),
centerY: roomY + Math.floor(roomH / 2)
};
if (!self.roomOverlaps(newRoom, rooms)) {
self.carveIrregularRoom(roomX, roomY, roomW, roomH);
rooms.push(newRoom);
// Connect this room to multiple existing rooms for guaranteed connectivity
self.connectToMultipleRooms(newRoom, rooms);
break;
}
attempts++;
}
}
// Create guaranteed multiple exits to adjacent chunks
self.createMultipleChunkExits(offsetX, offsetY, rooms);
// Create dead-end corridors for exploration variety
self.createDeadEndCorridors(offsetX, offsetY, rooms);
// Add some random pillars for atmosphere
self.addRandomPillars(offsetX, offsetY);
// Add pillars specifically in medium and large rooms
self.addPillarsToRooms(offsetX, offsetY, rooms);
// Validate and ensure all rooms are connected
self.validateRoomConnectivity(rooms, offsetX, offsetY);
};
// Carve out a room (remove walls)
self.carveRoom = function (x, y, width, height) {
for (var roomX = x; roomX < x + width; roomX++) {
for (var roomY = y; roomY < y + height; roomY++) {
if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) {
worldGrid.walls[roomX][roomY] = false;
}
}
}
};
// Carve out an irregular room shape with random variations
self.carveIrregularRoom = function (x, y, width, height) {
var roomType = Math.random();
var centerX = x + Math.floor(width / 2);
var centerY = y + Math.floor(height / 2);
if (roomType < 0.4) {
// Circular/oval room
var radiusX = Math.floor(width / 2);
var radiusY = Math.floor(height / 2);
for (var roomX = x; roomX < x + width; roomX++) {
for (var roomY = y; roomY < y + height; roomY++) {
if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) {
var dx = roomX - centerX;
var dy = roomY - centerY;
// Create elliptical shape with some randomness
var distanceSquared = dx * dx / (radiusX * radiusX) + dy * dy / (radiusY * radiusY);
if (distanceSquared <= 1.0 + Math.random() * 0.3 - 0.15) {
worldGrid.walls[roomX][roomY] = false;
}
}
}
}
} else if (roomType < 0.7) {
// L-shaped room
self.carveRoom(x, y, width, Math.floor(height / 2) + 1);
self.carveRoom(x + Math.floor(width / 2), y + Math.floor(height / 2), Math.floor(width / 2) + 1, Math.floor(height / 2) + 1);
} else if (roomType < 0.9) {
// Cross-shaped room
var halfW = Math.floor(width / 2);
var halfH = Math.floor(height / 2);
// Vertical bar
self.carveRoom(centerX - 1, y, 2, height);
// Horizontal bar
self.carveRoom(x, centerY - 1, width, 2);
} else {
// Regular rectangular room with random indentations
self.carveRoom(x, y, width, height);
// Add random indentations
var indentations = Math.floor(Math.random() * 3) + 1;
for (var i = 0; i < indentations; i++) {
var indentX = x + Math.floor(Math.random() * width);
var indentY = y + Math.floor(Math.random() * height);
var indentSize = Math.floor(Math.random() * 2) + 1;
for (var ix = 0; ix < indentSize; ix++) {
for (var iy = 0; iy < indentSize; iy++) {
var wallX = indentX + ix;
var wallY = indentY + iy;
if (wallX >= 0 && wallX < worldGrid.width && wallY >= 0 && wallY < worldGrid.height) {
worldGrid.walls[wallX][wallY] = true;
}
}
}
}
}
};
// Check if room overlaps with existing rooms
self.roomOverlaps = function (room, existingRooms) {
for (var i = 0; i < existingRooms.length; i++) {
var existing = existingRooms[i];
// Minimal padding between rooms for claustrophobic effect - only 1 cell apart
if (room.x < existing.x + existing.width + 1 && room.x + room.width + 1 > existing.x && room.y < existing.y + existing.height + 1 && room.y + room.height + 1 > existing.y) {
return true;
}
}
return false;
};
// Connect room to nearest existing room
self.connectToNearestRoom = function (newRoom, existingRooms) {
var nearestRoom = existingRooms[0];
var minDistance = Infinity;
// Find nearest room
for (var i = 0; i < existingRooms.length; i++) {
if (existingRooms[i] !== newRoom) {
var dx = newRoom.centerX - existingRooms[i].centerX;
var dy = newRoom.centerY - existingRooms[i].centerY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistance) {
minDistance = distance;
nearestRoom = existingRooms[i];
}
}
}
// Create L-shaped corridor
self.createCorridor(newRoom.centerX, newRoom.centerY, nearestRoom.centerX, nearestRoom.centerY);
};
// Connect room to multiple existing rooms for guaranteed connectivity
self.connectToMultipleRooms = function (newRoom, existingRooms) {
// Sort rooms by distance to find nearest connections
var roomDistances = [];
for (var i = 0; i < existingRooms.length; i++) {
if (existingRooms[i] !== newRoom) {
var dx = newRoom.centerX - existingRooms[i].centerX;
var dy = newRoom.centerY - existingRooms[i].centerY;
var distance = Math.sqrt(dx * dx + dy * dy);
roomDistances.push({
room: existingRooms[i],
distance: distance
});
}
}
// Sort by distance
roomDistances.sort(function (a, b) {
return a.distance - b.distance;
});
// Always connect to at least 2 rooms for guaranteed multiple exits
var minConnections = Math.min(2, roomDistances.length);
var maxConnections = Math.min(3, roomDistances.length); // Up to 3 connections
var connectionsToMake = Math.floor(Math.random() * (maxConnections - minConnections + 1)) + minConnections;
for (var i = 0; i < connectionsToMake; i++) {
self.createCurvedCorridor(newRoom.centerX, newRoom.centerY, roomDistances[i].room.centerX, roomDistances[i].room.centerY);
}
// Add one more connection with 40% probability for extra connectivity
if (Math.random() < 0.4 && roomDistances.length > connectionsToMake) {
self.createCurvedCorridor(newRoom.centerX, newRoom.centerY, roomDistances[connectionsToMake].room.centerX, roomDistances[connectionsToMake].room.centerY);
}
};
// Connect room to nearest existing room with curved corridor (legacy method for compatibility)
self.connectToNearestRoomCurved = function (newRoom, existingRooms) {
// Use the new multiple connections method
self.connectToMultipleRooms(newRoom, existingRooms);
};
// Create multiple guaranteed exits to adjacent chunks for better connectivity
self.createMultipleChunkExits = function (offsetX, offsetY, rooms) {
var midX = offsetX + Math.floor(self.chunkSize / 2);
var midY = offsetY + Math.floor(self.chunkSize / 2);
// Find main room (usually the first/largest)
var mainRoom = rooms[0];
for (var i = 1; i < rooms.length; i++) {
if (rooms[i].width * rooms[i].height > mainRoom.width * mainRoom.height) {
mainRoom = rooms[i];
}
}
// Ensure at least 2-3 exits for multiple pathways
var possibleExits = [];
// Check which exits are possible
if (offsetX > 0) {
possibleExits.push('left');
}
if (offsetX + self.chunkSize < worldGrid.width) {
possibleExits.push('right');
}
if (offsetY > 0) {
possibleExits.push('top');
}
if (offsetY + self.chunkSize < worldGrid.height) {
possibleExits.push('bottom');
}
// Guarantee at least 2 exits if possible, 3 if we have 4 sides available
var minExits = Math.min(2, possibleExits.length);
var maxExits = Math.min(3, possibleExits.length);
var exitsToCreate = Math.floor(Math.random() * (maxExits - minExits + 1)) + minExits;
// Shuffle possible exits for randomness
for (var i = possibleExits.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = possibleExits[i];
possibleExits[i] = possibleExits[j];
possibleExits[j] = temp;
}
// Create the guaranteed exits
for (var i = 0; i < exitsToCreate; i++) {
var exitDirection = possibleExits[i];
// Connect from different rooms for variety
var sourceRoom = rooms[i % rooms.length];
if (exitDirection === 'left') {
self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, offsetX, midY);
} else if (exitDirection === 'right') {
self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, offsetX + self.chunkSize - 1, midY);
} else if (exitDirection === 'top') {
self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, midX, offsetY);
} else if (exitDirection === 'bottom') {
self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, midX, offsetY + self.chunkSize - 1);
}
}
// Create additional exits with 50% probability for extra connectivity
for (var i = exitsToCreate; i < possibleExits.length; i++) {
if (Math.random() < 0.5) {
var exitDirection = possibleExits[i];
var sourceRoom = rooms[Math.floor(Math.random() * rooms.length)];
if (exitDirection === 'left') {
self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, offsetX, midY);
} else if (exitDirection === 'right') {
self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, offsetX + self.chunkSize - 1, midY);
} else if (exitDirection === 'top') {
self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, midX, offsetY);
} else if (exitDirection === 'bottom') {
self.createCorridor(sourceRoom.centerX, sourceRoom.centerY, midX, offsetY + self.chunkSize - 1);
}
}
}
};
// Create dead-end corridors for exploration variety
self.createDeadEndCorridors = function (offsetX, offsetY, rooms) {
// First, check each room for exits and potentially add passages
for (var i = 0; i < rooms.length; i++) {
var room = rooms[i];
var hasExit = self.checkRoomHasExit(room, offsetX, offsetY);
// If room has no exits, add a passage with 20% probability
if (!hasExit && Math.random() < 0.2) {
self.createRoomPassage(room, offsetX, offsetY);
}
}
// Create 2-4 dead-end corridors per chunk
var numDeadEnds = Math.floor(Math.random() * 3) + 2; // 2-4 dead ends
for (var i = 0; i < numDeadEnds; i++) {
// Choose a random room as starting point
var sourceRoom = rooms[Math.floor(Math.random() * rooms.length)];
// Choose a random direction for the dead-end
var directions = [{
dx: 1,
dy: 0
},
// East
{
dx: -1,
dy: 0
},
// West
{
dx: 0,
dy: 1
},
// South
{
dx: 0,
dy: -1
} // North
];
var direction = directions[Math.floor(Math.random() * directions.length)];
// Create dead-end corridor of random length (2-5 cells)
var corridorLength = Math.floor(Math.random() * 4) + 2;
var startX = sourceRoom.centerX;
var startY = sourceRoom.centerY;
// Find a good starting point on the room edge
var edgeStartX = startX;
var edgeStartY = startY;
// Move to room edge
if (direction.dx !== 0) {
edgeStartX = direction.dx > 0 ? sourceRoom.x + sourceRoom.width : sourceRoom.x - 1;
} else {
edgeStartY = direction.dy > 0 ? sourceRoom.y + sourceRoom.height : sourceRoom.y - 1;
}
// Create the dead-end corridor
self.createDeadEndPath(edgeStartX, edgeStartY, direction.dx, direction.dy, corridorLength, offsetX, offsetY);
}
};
// Check if a room has any exits (passages leading out)
self.checkRoomHasExit = function (room, chunkOffsetX, chunkOffsetY) {
// Check the perimeter of the room for any open passages
var directions = [{
dx: 1,
dy: 0
},
// East
{
dx: -1,
dy: 0
},
// West
{
dx: 0,
dy: 1
},
// South
{
dx: 0,
dy: -1
} // North
];
// Check each edge of the room
for (var x = room.x; x < room.x + room.width; x++) {
for (var y = room.y; y < room.y + room.height; y++) {
// Skip if this position is a wall
if (x >= 0 && x < worldGrid.width && y >= 0 && y < worldGrid.height && worldGrid.walls[x][y]) {
continue;
}
// Check adjacent cells for passages
for (var d = 0; d < directions.length; d++) {
var checkX = x + directions[d].dx;
var checkY = y + directions[d].dy;
// Check bounds
if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) {
// If adjacent cell is open and outside the room bounds, it's an exit
if (!worldGrid.walls[checkX][checkY] && (checkX < room.x || checkX >= room.x + room.width || checkY < room.y || checkY >= room.y + room.height)) {
return true; // Found an exit
}
}
}
}
}
return false; // No exits found
};
// Create a passage for a room without exits
self.createRoomPassage = function (room, chunkOffsetX, chunkOffsetY) {
// Determine the best direction for the passage
var directions = [{
dx: 1,
dy: 0,
name: 'east'
}, {
dx: -1,
dy: 0,
name: 'west'
}, {
dx: 0,
dy: 1,
name: 'south'
}, {
dx: 0,
dy: -1,
name: 'north'
}];
var bestDirection = directions[Math.floor(Math.random() * directions.length)];
// Create passage from room center in chosen direction
var startX = room.centerX;
var startY = room.centerY;
// Move to room edge
var edgeX = startX;
var edgeY = startY;
if (bestDirection.dx !== 0) {
edgeX = bestDirection.dx > 0 ? room.x + room.width : room.x - 1;
} else {
edgeY = bestDirection.dy > 0 ? room.y + room.height : room.y - 1;
}
// Create passage large enough for player (width of 2-3 cells)
var passageLength = Math.floor(Math.random() * 3) + 3; // 3-5 cells long
self.createPlayerSizedPassage(edgeX, edgeY, bestDirection.dx, bestDirection.dy, passageLength, chunkOffsetX, chunkOffsetY);
};
// Create a passage sized for player movement
self.createPlayerSizedPassage = function (startX, startY, dirX, dirY, length, chunkOffsetX, chunkOffsetY) {
for (var i = 0; i <= length; i++) {
var passageX = startX + dirX * i;
var passageY = startY + dirY * i;
// Ensure passage coordinates are valid and within chunk bounds
if (passageX >= chunkOffsetX && passageX < chunkOffsetX + self.chunkSize && passageY >= chunkOffsetY && passageY < chunkOffsetY + self.chunkSize && passageX >= 0 && passageX < worldGrid.width && passageY >= 0 && passageY < worldGrid.height) {
// Clear main passage cell
worldGrid.walls[passageX][passageY] = false;
// Create wider passage (2-3 cells wide) for better player movement
if (dirX !== 0) {
// Horizontal passage - add vertical width
if (passageY + 1 < worldGrid.height && passageY + 1 < chunkOffsetY + self.chunkSize) {
worldGrid.walls[passageX][passageY + 1] = false;
}
if (passageY - 1 >= 0 && passageY - 1 >= chunkOffsetY) {
worldGrid.walls[passageX][passageY - 1] = false;
}
// Occasionally add third row for wider passage
if (Math.random() < 0.5 && passageY + 2 < worldGrid.height && passageY + 2 < chunkOffsetY + self.chunkSize) {
worldGrid.walls[passageX][passageY + 2] = false;
}
} else {
// Vertical passage - add horizontal width
if (passageX + 1 < worldGrid.width && passageX + 1 < chunkOffsetX + self.chunkSize) {
worldGrid.walls[passageX + 1][passageY] = false;
}
if (passageX - 1 >= 0 && passageX - 1 >= chunkOffsetX) {
worldGrid.walls[passageX - 1][passageY] = false;
}
// Occasionally add third column for wider passage
if (Math.random() < 0.5 && passageX + 2 < worldGrid.width && passageX + 2 < chunkOffsetX + self.chunkSize) {
worldGrid.walls[passageX + 2][passageY] = false;
}
}
}
}
};
// Create a single dead-end corridor path
self.createDeadEndPath = function (startX, startY, dirX, dirY, length, chunkOffsetX, chunkOffsetY) {
var currentX = startX;
var currentY = startY;
// Create corridor cells
for (var i = 0; i < length; i++) {
currentX += dirX;
currentY += dirY;
// Check bounds - stop if we're going outside the chunk or world
if (currentX < chunkOffsetX + 1 || currentX >= chunkOffsetX + self.chunkSize - 1 || currentY < chunkOffsetY + 1 || currentY >= chunkOffsetY + self.chunkSize - 1 || currentX < 0 || currentX >= worldGrid.width || currentY < 0 || currentY >= worldGrid.height) {
break;
}
// Carve out the corridor cell
worldGrid.walls[currentX][currentY] = false;
// Add some width to the corridor (occasionally)
if (Math.random() < 0.3) {
// Add perpendicular width
var perpDirX = dirY; // Perpendicular direction
var perpDirY = -dirX;
var widthX = currentX + perpDirX;
var widthY = currentY + perpDirY;
if (widthX >= chunkOffsetX && widthX < chunkOffsetX + self.chunkSize && widthY >= chunkOffsetY && widthY < chunkOffsetY + self.chunkSize && widthX >= 0 && widthX < worldGrid.width && widthY >= 0 && widthY < worldGrid.height) {
worldGrid.walls[widthX][widthY] = false;
}
}
// Occasionally branch the dead-end
if (i > 1 && Math.random() < 0.2) {
// Create a short branch (1-2 cells)
var branchLength = Math.floor(Math.random() * 2) + 1;
var branchDirX = Math.random() < 0.5 ? dirY : -dirY; // Perpendicular directions
var branchDirY = Math.random() < 0.5 ? -dirX : dirX;
self.createDeadEndPath(currentX, currentY, branchDirX, branchDirY, branchLength, chunkOffsetX, chunkOffsetY);
}
}
// Create a small room at the end of some dead-ends (30% chance)
if (Math.random() < 0.3) {
self.createDeadEndRoom(currentX, currentY, chunkOffsetX, chunkOffsetY);
}
};
// Create a small room at the end of a dead-end corridor
self.createDeadEndRoom = function (centerX, centerY, chunkOffsetX, chunkOffsetY) {
// Create a small 2x2 or 3x3 room
var roomSize = Math.floor(Math.random() * 2) + 2; // 2x2 or 3x3
var halfSize = Math.floor(roomSize / 2);
for (var dx = -halfSize; dx <= halfSize; dx++) {
for (var dy = -halfSize; dy <= halfSize; dy++) {
var roomX = centerX + dx;
var roomY = centerY + dy;
// Check bounds
if (roomX >= chunkOffsetX && roomX < chunkOffsetX + self.chunkSize && roomY >= chunkOffsetY && roomY < chunkOffsetY + self.chunkSize && roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) {
worldGrid.walls[roomX][roomY] = false;
}
}
}
};
// Create guaranteed exits to adjacent chunks (legacy method for compatibility)
self.createChunkExits = function (offsetX, offsetY, mainRoom) {
// Use the new multiple exits method
self.createMultipleChunkExits(offsetX, offsetY, [mainRoom]);
};
// Create L-shaped corridor between two points with much shorter segments
self.createCorridor = function (x1, y1, x2, y2) {
// Create narrower corridors for shorter navigation paths
var halfWidth = Math.floor(self.hallwayWidth / 2);
// Calculate distance and limit corridor length to make them much shorter
var dx = Math.abs(x2 - x1);
var dy = Math.abs(y2 - y1);
var maxCorridorLength = 3; // Maximum corridor segment length
// If distance is too long, create intermediate points for shorter segments
if (dx > maxCorridorLength || dy > maxCorridorLength) {
var midX = x1 + Math.sign(x2 - x1) * Math.min(maxCorridorLength, dx);
var midY = y1 + Math.sign(y2 - y1) * Math.min(maxCorridorLength, dy);
// Create first short segment
self.createShortSegment(x1, y1, midX, y1, halfWidth);
// Create second short segment
self.createShortSegment(midX, y1, midX, midY, halfWidth);
// If we haven't reached the destination, create final segment
if (midX !== x2 || midY !== y2) {
self.createShortSegment(midX, midY, x2, y2, halfWidth);
}
} else {
// Choose random direction for short L-shaped corridor
if (Math.random() < 0.5) {
// Horizontal first, then vertical - but keep it short
self.createShortSegment(x1, y1, x2, y1, halfWidth);
self.createShortSegment(x2, y1, x2, y2, halfWidth);
} else {
// Vertical first, then horizontal - but keep it short
self.createShortSegment(x1, y1, x1, y2, halfWidth);
self.createShortSegment(x1, y2, x2, y2, halfWidth);
}
}
};
// Create a short corridor segment with limited length
self.createShortSegment = function (x1, y1, x2, y2, halfWidth) {
var startX = Math.min(x1, x2);
var endX = Math.max(x1, x2);
var startY = Math.min(y1, y2);
var endY = Math.max(y1, y2);
// Limit segment length to make corridors extremely short for claustrophobic effect
var maxSegmentLength = 1;
endX = Math.min(endX, startX + maxSegmentLength);
endY = Math.min(endY, startY + maxSegmentLength);
for (var x = startX; x <= endX; x++) {
for (var y = startY; y <= endY; y++) {
for (var w = -halfWidth; w <= halfWidth; w++) {
var corridorX = x + (x1 === x2 ? w : 0);
var corridorY = y + (y1 === y2 ? w : 0);
if (corridorX >= 0 && corridorX < worldGrid.width && corridorY >= 0 && corridorY < worldGrid.height) {
worldGrid.walls[corridorX][corridorY] = false;
}
}
}
}
};
// Create curved and winding corridor between two points
self.createCurvedCorridor = function (x1, y1, x2, y2) {
var corridorType = Math.random();
var halfWidth = Math.floor(self.hallwayWidth / 2);
if (corridorType < 0.3) {
// S-shaped curve
var midX = Math.floor((x1 + x2) / 2) + Math.floor(Math.random() * 4) - 2;
var midY = Math.floor((y1 + y2) / 2) + Math.floor(Math.random() * 4) - 2;
self.createSmoothPath(x1, y1, midX, midY);
self.createSmoothPath(midX, midY, x2, y2);
} else if (corridorType < 0.6) {
// Zigzag corridor
var steps = Math.floor(Math.abs(x2 - x1) + Math.abs(y2 - y1)) / 3;
var currentX = x1;
var currentY = y1;
var stepX = (x2 - x1) / steps;
var stepY = (y2 - y1) / steps;
for (var i = 0; i < steps; i++) {
var nextX = Math.floor(currentX + stepX + Math.random() * 2 - 1);
var nextY = Math.floor(currentY + stepY + Math.random() * 2 - 1);
self.createSmoothPath(Math.floor(currentX), Math.floor(currentY), nextX, nextY);
currentX = nextX;
currentY = nextY;
}
self.createSmoothPath(Math.floor(currentX), Math.floor(currentY), x2, y2);
} else {
// Wide curved path with multiple control points
var controlPoints = [];
controlPoints.push({
x: x1,
y: y1
});
// Add 1-2 random control points
var numControls = Math.floor(Math.random() * 2) + 1;
for (var i = 0; i < numControls; i++) {
var t = (i + 1) / (numControls + 1);
var controlX = Math.floor(x1 + (x2 - x1) * t + Math.random() * 6 - 3);
var controlY = Math.floor(y1 + (y2 - y1) * t + Math.random() * 6 - 3);
controlPoints.push({
x: controlX,
y: controlY
});
}
controlPoints.push({
x: x2,
y: y2
});
// Connect all control points
for (var i = 0; i < controlPoints.length - 1; i++) {
self.createSmoothPath(controlPoints[i].x, controlPoints[i].y, controlPoints[i + 1].x, controlPoints[i + 1].y);
}
}
};
// Create smooth path between two points with width - much shorter segments
self.createSmoothPath = function (x1, y1, x2, y2) {
var dx = x2 - x1;
var dy = y2 - y1;
var distance = Math.sqrt(dx * dx + dy * dy);
// Limit path length to make corridors extremely short for claustrophobic effect
var maxPathLength = 2;
if (distance > maxPathLength) {
// Create shorter intermediate path
var ratio = maxPathLength / distance;
x2 = Math.floor(x1 + dx * ratio);
y2 = Math.floor(y1 + dy * ratio);
dx = x2 - x1;
dy = y2 - y1;
distance = maxPathLength;
}
var steps = Math.min(Math.floor(distance) + 1, 2); // Very limited steps for tight spaces
var halfWidth = Math.floor(self.hallwayWidth / 2);
for (var i = 0; i <= steps; i++) {
var t = i / steps;
var x = Math.floor(x1 + dx * t);
var y = Math.floor(y1 + dy * t);
// Carve corridor with width
for (var w = -halfWidth; w <= halfWidth; w++) {
for (var h = -halfWidth; h <= halfWidth; h++) {
var corridorX = x + w;
var corridorY = y + h;
if (corridorX >= 0 && corridorX < worldGrid.width && corridorY >= 0 && corridorY < worldGrid.height) {
worldGrid.walls[corridorX][corridorY] = false;
}
}
}
}
};
// Add pillars at coordinate system corners - only around player
self.addRandomPillars = function (offsetX, offsetY) {
// Get player position if available
var playerX = player ? Math.floor(player.x / worldGrid.cellSize) : Math.floor(worldGrid.width / 2);
var playerY = player ? Math.floor(player.y / worldGrid.cellSize) : Math.floor(worldGrid.height / 2);
var playerRadius = 8; // Only generate pillars within 8 cells of player
// Define corner positions within the chunk
var cornerPositions = [
// Top-left corner of chunk
{
x: offsetX + 1,
y: offsetY + 1
},
// Top-right corner of chunk
{
x: offsetX + self.chunkSize - 2,
y: offsetY + 1
},
// Bottom-left corner of chunk
{
x: offsetX + 1,
y: offsetY + self.chunkSize - 2
},
// Bottom-right corner of chunk
{
x: offsetX + self.chunkSize - 2,
y: offsetY + self.chunkSize - 2
}];
// Try to place pillars at corner positions
for (var i = 0; i < cornerPositions.length; i++) {
var corner = cornerPositions[i];
var x = corner.x;
var y = corner.y;
// Ensure position is within world bounds
if (x >= 0 && x < worldGrid.width && y >= 0 && y < worldGrid.height) {
// Check if pillar position is within player radius
var distanceToPlayer = Math.abs(x - playerX) + Math.abs(y - playerY);
if (distanceToPlayer > playerRadius) {
continue; // Skip if too far from player
}
// Only add pillars in open areas (50% chance at corners)
if (!worldGrid.walls[x][y] && Math.random() < 0.5) {
// Check if surrounded by enough open space (2x2 area around corner)
var canPlace = true;
for (var dx = 0; dx <= 1; dx++) {
for (var dy = 0; dy <= 1; dy++) {
var checkX = x + dx;
var checkY = y + dy;
if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) {
if (worldGrid.walls[checkX][checkY]) {
canPlace = false;
break;
}
}
}
if (!canPlace) {
break;
}
}
if (canPlace) {
worldGrid.walls[x][y] = true;
}
}
}
}
};
// Validate that all rooms are connected and fix any isolated rooms
self.validateRoomConnectivity = function (rooms, offsetX, offsetY) {
if (rooms.length <= 1) {
return;
} // Single room doesn't need validation
// Use flood fill to check connectivity from main room
var visited = [];
for (var i = 0; i < rooms.length; i++) {
visited[i] = false;
}
// Start flood fill from main room (first room)
var connected = [0]; // Start with main room
visited[0] = true;
var changed = true;
// Keep checking until no new connections are found
while (changed) {
changed = false;
for (var i = 0; i < connected.length; i++) {
var currentRoom = rooms[connected[i]];
// Check if any unvisited room is connected to this room
for (var j = 0; j < rooms.length; j++) {
if (!visited[j] && self.areRoomsConnected(currentRoom, rooms[j])) {
visited[j] = true;
connected.push(j);
changed = true;
}
}
}
}
// Connect any isolated rooms
for (var i = 0; i < rooms.length; i++) {
if (!visited[i]) {
// This room is isolated, connect it to the nearest connected room
var nearestConnectedRoom = rooms[connected[0]];
var minDistance = Infinity;
for (var j = 0; j < connected.length; j++) {
var connectedRoom = rooms[connected[j]];
var dx = rooms[i].centerX - connectedRoom.centerX;
var dy = rooms[i].centerY - connectedRoom.centerY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistance) {
minDistance = distance;
nearestConnectedRoom = connectedRoom;
}
}
// Force connection to prevent isolation
self.createCurvedCorridor(rooms[i].centerX, rooms[i].centerY, nearestConnectedRoom.centerX, nearestConnectedRoom.centerY);
}
}
};
// Check if two rooms are connected via corridors
self.areRoomsConnected = function (room1, room2) {
// Use simple pathfinding to check if rooms are connected
var visited = [];
for (var x = 0; x < worldGrid.width; x++) {
visited[x] = [];
for (var y = 0; y < worldGrid.height; y++) {
visited[x][y] = false;
}
}
var stack = [{
x: room1.centerX,
y: room1.centerY
}];
while (stack.length > 0) {
var current = stack.pop();
var gridX = Math.floor(current.x / worldGrid.cellSize) * worldGrid.cellSize;
var gridY = Math.floor(current.y / worldGrid.cellSize) * worldGrid.cellSize;
var arrayX = Math.floor(current.x / worldGrid.cellSize);
var arrayY = Math.floor(current.y / worldGrid.cellSize);
if (arrayX < 0 || arrayX >= worldGrid.width || arrayY < 0 || arrayY >= worldGrid.height) {
continue;
}
if (visited[arrayX][arrayY] || worldGrid.walls[arrayX][arrayY]) {
continue;
}
visited[arrayX][arrayY] = true;
// Check if we reached room2
if (Math.abs(current.x - room2.centerX) < worldGrid.cellSize && Math.abs(current.y - room2.centerY) < worldGrid.cellSize) {
return true; // Rooms are connected
}
// Add adjacent cells
var directions = [{
dx: 1,
dy: 0
}, {
dx: -1,
dy: 0
}, {
dx: 0,
dy: 1
}, {
dx: 0,
dy: -1
}];
for (var d = 0; d < directions.length; d++) {
stack.push({
x: current.x + directions[d].dx * worldGrid.cellSize,
y: current.y + directions[d].dy * worldGrid.cellSize
});
}
}
return false; // No connection found
};
// Get room size based on probability distribution - heavily favor small rooms for claustrophobia
// Small rooms: 75%, Medium rooms: 20%, Large rooms: 5%
self.getRoomSizeByProbability = function () {
var random = Math.random() * 100;
if (random < 75) {
// Small rooms (75% probability) - 1x1 to 2x2 (mostly tiny)
var size = Math.floor(Math.random() * 2) + 1;
return {
width: size,
height: size
};
} else if (random < 95) {
// Medium rooms (20% probability) - 2x2 to 3x3 (reduced max size)
var size = Math.floor(Math.random() * 2) + 2;
return {
width: size,
height: size
};
} else {
// Large rooms (5% probability) - 3x3 to 4x4 (much smaller than before)
var size = Math.floor(Math.random() * 2) + 3;
return {
width: size,
height: size
};
}
};
// Generate chunks around player position
self.generateAroundPlayer = function (playerX, playerY) {
var playerChunkX = Math.floor(playerX / (worldGrid.cellSize * self.chunkSize));
var playerChunkY = Math.floor(playerY / (worldGrid.cellSize * self.chunkSize));
// Generate chunks in a 3x3 area around player
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
var chunkX = playerChunkX + dx;
var chunkY = playerChunkY + dy;
self.generateChunk(chunkX, chunkY);
}
}
};
// Add pillars specifically in medium and large rooms and also as standalone pillars in corridors - only around player
self.addPillarsToRooms = function (offsetX, offsetY, rooms) {
// Get player position if available
var playerX = player ? Math.floor(player.x / worldGrid.cellSize) : Math.floor(worldGrid.width / 2);
var playerY = player ? Math.floor(player.y / worldGrid.cellSize) : Math.floor(worldGrid.height / 2);
var playerRadius = 8; // Only generate pillars within 8 cells of player
for (var i = 0; i < rooms.length; i++) {
var room = rooms[i];
var roomArea = room.width * room.height;
// Check if room is within player radius
var distanceToPlayer = Math.abs(room.centerX - playerX) + Math.abs(room.centerY - playerY);
if (distanceToPlayer > playerRadius) {
continue; // Skip room if too far from player
}
var pillarCount = 0;
// Determine number of pillars based on room size - now includes small rooms
if (roomArea >= 9) {
// Large rooms (9+ cells): 2-4 pillars
pillarCount = Math.floor(Math.random() * 3) + 2;
} else if (roomArea >= 4) {
// Medium rooms (4-8 cells): 1-2 pillars
pillarCount = Math.floor(Math.random() * 2) + 1;
} else if (roomArea >= 2) {
// Small rooms (2-3 cells): 0-1 pillar with 40% probability
if (Math.random() < 0.4) {
pillarCount = 1;
}
}
// Place pillars only in the center of rooms
for (var p = 0; p < pillarCount; p++) {
// Calculate exact room center
var pillarX = room.x + Math.floor(room.width / 2);
var pillarY = room.y + Math.floor(room.height / 2);
// For rooms with even dimensions, slightly offset to avoid exact geometric center
if (room.width % 2 === 0 && p % 2 === 1) {
pillarX += Math.random() < 0.5 ? -1 : 1;
}
if (room.height % 2 === 0 && p % 3 === 1) {
pillarY += Math.random() < 0.5 ? -1 : 1;
}
// Ensure pillar is within room bounds and world bounds
if (pillarX >= room.x && pillarX < room.x + room.width && pillarY >= room.y && pillarY < room.y + room.height && pillarX >= 0 && pillarX < worldGrid.width && pillarY >= 0 && pillarY < worldGrid.height) {
// Only place if position is currently open
if (!worldGrid.walls[pillarX][pillarY]) {
worldGrid.walls[pillarX][pillarY] = true;
}
}
}
}
// Also add standalone pillars in corridor areas
self.addStandalonePillarsInCorridors(offsetX, offsetY, rooms);
};
// Check if a pillar can be placed at the given position (simplified for center placement)
self.canPlacePillar = function (pillarX, pillarY, room, allRooms) {
// Check that position is currently open (not already a wall)
if (pillarX >= 0 && pillarX < worldGrid.width && pillarY >= 0 && pillarY < worldGrid.height) {
if (worldGrid.walls[pillarX][pillarY]) {
return false; // Already a wall
}
}
// Since we're only placing in centers, just ensure the position is open
return true;
};
// Add standalone pillars at coordinate intersections - only around player and inside rooms
self.addStandalonePillarsInCorridors = function (offsetX, offsetY, rooms) {
// Get player position if available
var playerX = player ? Math.floor(player.x / worldGrid.cellSize) : Math.floor(worldGrid.width / 2);
var playerY = player ? Math.floor(player.y / worldGrid.cellSize) : Math.floor(worldGrid.height / 2);
var playerRadius = 8; // Only generate pillars within 8 cells of player
// Define coordinate intersection points within the chunk (grid intersections)
var intersectionPoints = [];
// Create a grid of intersection points every 2 cells
for (var gridX = offsetX + 2; gridX < offsetX + self.chunkSize - 1; gridX += 2) {
for (var gridY = offsetY + 2; gridY < offsetY + self.chunkSize - 1; gridY += 2) {
intersectionPoints.push({
x: gridX,
y: gridY
});
}
}
// Try to place pillars at coordinate intersections (30% chance per intersection)
for (var i = 0; i < intersectionPoints.length; i++) {
var intersection = intersectionPoints[i];
var pillarX = intersection.x;
var pillarY = intersection.y;
// Check if pillar is within player radius
var distanceToPlayer = Math.abs(pillarX - playerX) + Math.abs(pillarY - playerY);
if (distanceToPlayer > playerRadius) {
continue; // Skip if too far from player
}
// Check if position is in open corridor area (not in room and not wall) and near player
if (self.isInCorridorArea(pillarX, pillarY, rooms) && self.canPlaceCorridorPillar(pillarX, pillarY) && Math.random() < 0.3) {
if (pillarX >= 0 && pillarX < worldGrid.width && pillarY >= 0 && pillarY < worldGrid.height) {
worldGrid.walls[pillarX][pillarY] = true;
}
}
}
};
// Check if position is in a corridor area (open space not in any room)
self.isInCorridorArea = function (x, y, rooms) {
// First check if position is open (not a wall)
if (x < 0 || x >= worldGrid.width || y < 0 || y >= worldGrid.height) {
return false;
}
if (worldGrid.walls[x][y]) {
return false; // Already a wall
}
// Check if position is inside any room
for (var i = 0; i < rooms.length; i++) {
var room = rooms[i];
if (x >= room.x && x < room.x + room.width && y >= room.y && y < room.y + room.height) {
return false; // Inside a room
}
}
return true; // In corridor area
};
// Check if a corridor pillar can be placed (ensure it doesn't block pathways)
self.canPlaceCorridorPillar = function (x, y) {
// Ensure there's enough space around the pillar (2x2 area check)
var checkRadius = 1;
var openSpaces = 0;
var totalSpaces = 0;
for (var dx = -checkRadius; dx <= checkRadius; dx++) {
for (var dy = -checkRadius; dy <= checkRadius; dy++) {
var checkX = x + dx;
var checkY = y + dy;
if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) {
totalSpaces++;
if (!worldGrid.walls[checkX][checkY]) {
openSpaces++;
}
}
}
}
// Only place pillar if at least 60% of surrounding area is open
return openSpaces >= totalSpaces * 0.6;
};
return self;
});
var RaycastRenderer = Container.expand(function () {
var self = Container.call(this);
self.screenWidth = 2732;
self.screenHeight = 2048;
self.numRays = 128; // Number of rays to cast
self.wallColumns = [];
self.floorColumns = [];
self.ceilingColumns = [];
// Initialize rendering columns
for (var i = 0; i < self.numRays; i++) {
var stripWidth = self.screenWidth / self.numRays;
// Wall column
var wallCol = self.addChild(LK.getAsset('wallSegment', {
anchorX: 0.5,
anchorY: 0.5
}));
wallCol.x = i * stripWidth + stripWidth / 2;
wallCol.y = self.screenHeight / 2;
wallCol.width = stripWidth + 1; // Small overlap to prevent gaps
wallCol.visible = false;
self.wallColumns.push(wallCol);
// Floor column
var floorCol = self.addChild(LK.getAsset('floorStrip', {
anchorX: 0.5,
anchorY: 0
}));
floorCol.x = i * stripWidth + stripWidth / 2;
floorCol.width = stripWidth + 1;
floorCol.visible = false;
self.floorColumns.push(floorCol);
// Ceiling column
var ceilCol = self.addChild(LK.getAsset('ceilingStrip', {
anchorX: 0.5,
anchorY: 1
}));
ceilCol.x = i * stripWidth + stripWidth / 2;
ceilCol.width = stripWidth + 1;
ceilCol.visible = false;
self.ceilingColumns.push(ceilCol);
}
self.render = function (player) {
var fov = Math.PI / 2; // 90 degrees field of view for classic raycasting
var halfFov = fov / 2;
var stripWidth = self.screenWidth / self.numRays;
var screenCenter = self.screenHeight / 2;
var pitchOffset = player.pitch * 300;
// Cast rays across the field of view
for (var i = 0; i < self.numRays; i++) {
var rayAngle = player.angle - halfFov + i / self.numRays * fov;
var rayData = self.castRay(player.x, player.y, rayAngle);
var distance = rayData.distance;
var wallHeight = 0;
var wallCol = self.wallColumns[i];
var floorCol = self.floorColumns[i];
floorCol.height = self.screenHeight - wallHeight; // Extend floor strip to cover entire floor area
var ceilCol = self.ceilingColumns[i];
// Check if column is within strict horizontal screen bounds
var columnX = wallCol.x;
var withinBounds = columnX >= 0 && columnX <= self.screenWidth;
if (rayData.hit && withinBounds) {
// Fish-eye correction
var correctedDistance = distance * Math.cos(rayAngle - player.angle);
// Calculate wall height based on distance
wallHeight = Math.max(50, worldGrid.cellSize * 800 / (correctedDistance + 1));
// Wall rendering
wallCol.height = wallHeight;
wallCol.x = Math.max(0, Math.min(self.screenWidth, wallCol.x)); // Clamp X position to screen bounds
wallCol.y = screenCenter + pitchOffset;
wallCol.visible = true;
// Distance-based shading with special door rendering
var shadingFactor = Math.max(0.15, 1.0 - correctedDistance / 800);
var tintValue = 0xFFFFFF;
// Special rendering for door - make it distinct and always visible
if (self.lastHitType === 'door') {
// Use bright glitchy colors for door visibility
var doorColors = [0xFFFFFF, 0x00FFFF, 0xFFFF00, 0xFF00FF];
var colorIndex = Math.floor(LK.ticks * 0.2 % doorColors.length);
wallCol.tint = doorColors[colorIndex];
// Ensure door is always visible with strong contrast
wallCol.alpha = 1.0;
// Make door walls slightly taller for better visibility
wallCol.height = wallHeight * 1.2;
} else {
wallCol.tint = tintValue;
wallCol.alpha = 1.0;
}
// Floor rendering with distance-based shading
var wallBottom = screenCenter + wallHeight / 2 + pitchOffset;
var floorHeight = self.screenHeight - wallBottom;
floorCol.y = wallBottom;
floorCol.height = Math.max(1, floorHeight);
floorCol.visible = true;
// Apply distance-based shading to the floor
var floorShadingFactor = Math.max(0.2, 1.0 - correctedDistance / 800);
floorCol.tint = 0xFFFFFF;
// Ceiling rendering
var ceilHeight = screenCenter - wallHeight / 2 + pitchOffset;
ceilCol.y = ceilHeight;
ceilCol.height = Math.max(1, ceilHeight);
ceilCol.visible = true;
ceilCol.tint = 0xFFFFFF;
} else {
// No wall hit or outside bounds - hide columns
wallCol.visible = false;
floorCol.visible = false;
ceilCol.visible = false;
}
}
};
// DDA (Digital Differential Analyzer) raycasting algorithm
self.castRay = function (startX, startY, angle) {
var rayX = startX;
var rayY = startY;
var rayDirX = Math.cos(angle);
var rayDirY = Math.sin(angle);
// Which grid cell we're in
var mapX = Math.floor(rayX / worldGrid.cellSize);
var mapY = Math.floor(rayY / worldGrid.cellSize);
// Length of ray from current position to x or y side
var deltaDistX = Math.abs(1 / rayDirX);
var deltaDistY = Math.abs(1 / rayDirY);
// Calculate step and initial sideDist
var stepX, sideDistX;
var stepY, sideDistY;
if (rayDirX < 0) {
stepX = -1;
sideDistX = (rayX / worldGrid.cellSize - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0 - rayX / worldGrid.cellSize) * deltaDistX;
}
if (rayDirY < 0) {
stepY = -1;
sideDistY = (rayY / worldGrid.cellSize - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0 - rayY / worldGrid.cellSize) * deltaDistY;
}
// Perform DDA
var hit = false;
var side = 0; // 0 if x-side, 1 if y-side
var maxSteps = 100;
var steps = 0;
while (!hit && steps < maxSteps) {
steps++;
// Jump to next map square, either in x-direction, or in y-direction
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
// Check if ray has hit a wall or door
var checkX = mapX * worldGrid.cellSize;
var checkY = mapY * worldGrid.cellSize;
var hitWall = worldGrid.hasWallAt(checkX, checkY);
var hitDoor = worldGrid.hasDoorAt(checkX, checkY);
if (hitWall || hitDoor) {
hit = true;
// Store what type of surface was hit for special rendering
self.lastHitType = hitDoor ? 'door' : 'wall';
}
}
var distance = 0;
if (hit) {
// Calculate distance
if (side === 0) {
distance = (mapX - rayX / worldGrid.cellSize + (1 - stepX) / 2) / rayDirX;
} else {
distance = (mapY - rayY / worldGrid.cellSize + (1 - stepY) / 2) / rayDirY;
}
distance = Math.abs(distance * worldGrid.cellSize);
}
return {
hit: hit,
distance: distance,
side: side,
mapX: mapX,
mapY: mapY
};
};
return self;
});
var SensitivityConfig = Container.expand(function () {
var self = Container.call(this);
// Load saved sensitivity or default to 50
self.sensitivity = storage.sensitivity || 50;
self.isVisible = false;
// Create background panel
var background = self.addChild(LK.getAsset('untexturedArea', {
anchorX: 0,
anchorY: 0,
width: 300,
height: 200,
alpha: 0.8
}));
background.tint = 0x222222;
// Create title text
var titleText = new Text2('Sensitivity', {
size: 40,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0);
titleText.x = 150;
titleText.y = 20;
self.addChild(titleText);
// Create sensitivity value text
var valueText = new Text2(self.sensitivity.toString(), {
size: 35,
fill: 0xFFFFFF
});
valueText.anchor.set(0.5, 0);
valueText.x = 150;
valueText.y = 70;
self.addChild(valueText);
// Create decrease button (larger for mobile)
var decreaseBtn = self.addChild(LK.getAsset('untexturedArea', {
anchorX: 0.5,
anchorY: 0.5,
width: 70,
height: 60
}));
decreaseBtn.x = 80;
decreaseBtn.y = 130;
decreaseBtn.tint = 0x666666;
var decreaseText = new Text2('-', {
size: 40,
fill: 0xFFFFFF
});
decreaseText.anchor.set(0.5, 0.5);
decreaseText.x = 80;
decreaseText.y = 130;
self.addChild(decreaseText);
// Create increase button (larger for mobile)
var increaseBtn = self.addChild(LK.getAsset('untexturedArea', {
anchorX: 0.5,
anchorY: 0.5,
width: 70,
height: 60
}));
increaseBtn.x = 220;
increaseBtn.y = 130;
increaseBtn.tint = 0x666666;
var increaseText = new Text2('+', {
size: 40,
fill: 0xFFFFFF
});
increaseText.anchor.set(0.5, 0.5);
increaseText.x = 220;
increaseText.y = 130;
self.addChild(increaseText);
// Update sensitivity display
self.updateDisplay = function () {
valueText.setText(self.sensitivity.toString());
// Save to storage
storage.sensitivity = self.sensitivity;
};
// Toggle visibility
self.toggle = function () {
self.isVisible = !self.isVisible;
self.visible = self.isVisible;
};
// Handle decrease button with visual feedback
decreaseBtn.down = function (x, y, obj) {
decreaseBtn.tint = 0x888888; // Lighten on press
if (self.sensitivity > 0) {
self.sensitivity = Math.max(0, self.sensitivity - 5);
self.updateDisplay();
}
};
decreaseBtn.up = function (x, y, obj) {
decreaseBtn.tint = 0x666666; // Reset color on release
};
// Handle increase button with visual feedback
increaseBtn.down = function (x, y, obj) {
increaseBtn.tint = 0x888888; // Lighten on press
if (self.sensitivity < 100) {
self.sensitivity = Math.min(100, self.sensitivity + 5);
self.updateDisplay();
}
};
increaseBtn.up = function (x, y, obj) {
increaseBtn.tint = 0x666666; // Reset color on release
};
// Add background click handler to prevent game interactions
background.down = function (x, y, obj) {
// Prevent event from bubbling to game
return true;
};
background.up = function (x, y, obj) {
// Prevent event from bubbling to game
return true;
};
background.move = function (x, y, obj) {
// Prevent event from bubbling to game
return true;
};
// Initially hidden
self.visible = false;
return self;
});
/****
* Initialize Game
****/
// Create player
var game = new LK.Game({
backgroundColor: 0x000000,
orientation: 'landscape',
width: 2732,
height: 2048
});
/****
* Game Code
****/
// World coordinate system - grid-based layout with procedural generation
var worldGrid = {
cellSize: 200,
width: 100,
// Expanded world size for infinite generation
height: 100,
walls: [],
// Will store wall positions
// Initialize world grid with walls
initializeGrid: function initializeGrid() {
// Initialize walls array first - fill entire world with walls initially
this.walls = [];
for (var x = 0; x < this.width; x++) {
this.walls[x] = [];
for (var y = 0; y < this.height; y++) {
// Start with all walls - procedural generation will carve out spaces
this.walls[x][y] = true;
}
}
// Create a starting room around spawn point (3x3 room)
var spawnX = Math.floor(this.width / 2);
var spawnY = Math.floor(this.height / 2);
for (var x = spawnX - 1; x <= spawnX + 1; x++) {
for (var y = spawnY - 1; y <= spawnY + 1; y++) {
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
this.walls[x][y] = false;
}
}
}
},
// Check if a grid position is a floor
isFloor: function isFloor(gridX, gridY) {
// Define logic to determine if a grid position is a floor
// For now, assume any position not marked as a wall is a floor
return !this.walls[gridX][gridY];
},
// Check if a world position has a wall
hasWallAt: function hasWallAt(worldX, worldY) {
var gridX = Math.floor(worldX / this.cellSize);
var gridY = Math.floor(worldY / this.cellSize);
if (gridX < 0 || gridX >= this.width || gridY < 0 || gridY >= this.height) {
return true; // Outside bounds = wall
}
return this.walls[gridX][gridY];
},
// Check collision with wall boundaries (with player radius) - improved precision
checkCollision: function checkCollision(worldX, worldY, radius) {
radius = radius || 20; // Default player radius
// Enhanced bounds checking with safety margin
if (worldX < radius || worldX >= this.width * this.cellSize - radius || worldY < radius || worldY >= this.height * this.cellSize - radius) {
return true; // Outside safe bounds = collision
}
// More comprehensive collision check - check multiple points around player circle
var checkPoints = [
// Center point
{
x: worldX,
y: worldY
},
// Cardinal directions (primary edges)
{
x: worldX - radius,
y: worldY
},
// Left
{
x: worldX + radius,
y: worldY
},
// Right
{
x: worldX,
y: worldY - radius
},
// Top
{
x: worldX,
y: worldY + radius
},
// Bottom
// Diagonal corners for better corner collision detection
{
x: worldX - radius * 0.7,
y: worldY - radius * 0.7
},
// Top-left
{
x: worldX + radius * 0.7,
y: worldY - radius * 0.7
},
// Top-right
{
x: worldX - radius * 0.7,
y: worldY + radius * 0.7
},
// Bottom-left
{
x: worldX + radius * 0.7,
y: worldY + radius * 0.7
},
// Bottom-right
// Additional edge points for smoother wall sliding
{
x: worldX - radius * 0.5,
y: worldY
},
// Half-left
{
x: worldX + radius * 0.5,
y: worldY
},
// Half-right
{
x: worldX,
y: worldY - radius * 0.5
},
// Half-top
{
x: worldX,
y: worldY + radius * 0.5
} // Half-bottom
];
for (var i = 0; i < checkPoints.length; i++) {
var point = checkPoints[i];
var pointGridX = Math.floor(point.x / this.cellSize);
var pointGridY = Math.floor(point.y / this.cellSize);
// Enhanced bounds check
if (pointGridX < 0 || pointGridX >= this.width || pointGridY < 0 || pointGridY >= this.height) {
return true;
}
// Check wall collision
if (this.walls[pointGridX][pointGridY] && !this.isFloor(pointGridX, pointGridY)) {
return true;
}
}
return false;
},
// Convert screen coordinates to world coordinates
screenToWorld: function screenToWorld(screenX, screenY) {
return {
x: screenX,
y: screenY
};
},
// Convert world coordinates to screen coordinates
worldToScreen: function worldToScreen(worldX, worldY) {
return {
x: worldX,
y: worldY
};
},
// Enhanced collision checking with distance-based precision
checkPreciseCollision: function checkPreciseCollision(worldX, worldY, radius, direction) {
radius = radius || 20;
// Calculate multiple check points based on movement direction
var checkPoints = [];
var numPoints = 8; // More points for better precision
// Add center point
checkPoints.push({
x: worldX,
y: worldY
});
// Add circular check points around player
for (var i = 0; i < numPoints; i++) {
var angle = i / numPoints * Math.PI * 2;
checkPoints.push({
x: worldX + Math.cos(angle) * radius,
y: worldY + Math.sin(angle) * radius
});
}
// Check each point for collision
for (var i = 0; i < checkPoints.length; i++) {
var point = checkPoints[i];
if (this.hasWallAt(point.x, point.y)) {
return true;
}
}
return false;
},
// Check if player can move to position with wall sliding support
canMoveTo: function canMoveTo(fromX, fromY, toX, toY, radius) {
radius = radius || 20;
// Direct movement check
if (!this.checkCollision(toX, toY, radius)) {
return {
canMove: true,
newX: toX,
newY: toY
};
}
// Try wall sliding - horizontal only
if (!this.checkCollision(toX, fromY, radius)) {
return {
canMove: true,
newX: toX,
newY: fromY
};
}
// Try wall sliding - vertical only
if (!this.checkCollision(fromX, toY, radius)) {
return {
canMove: true,
newX: fromX,
newY: toY
};
}
// No valid movement
return {
canMove: false,
newX: fromX,
newY: fromY
};
}
};
// Initialize the world grid
worldGrid.initializeGrid();
// Create procedural generator
var procGen = new ProcGen();
// Generate initial chunks around spawn point
procGen.generateAroundPlayer(worldGrid.width * worldGrid.cellSize / 2, worldGrid.height * worldGrid.cellSize / 2);
// Add wall line completion system
worldGrid.completeWallLines = function () {
// Trace horizontal lines and complete them
for (var y = 0; y < this.height; y++) {
var wallStart = -1;
var wallEnd = -1;
// Find wall segments in this row
for (var x = 0; x < this.width; x++) {
if (this.walls[x][y]) {
if (wallStart === -1) {
wallStart = x; // Start of wall segment
}
wallEnd = x; // Update end of wall segment
} else {
// If we found a wall segment, complete the line between start and end
if (wallStart !== -1 && wallEnd !== -1 && wallEnd > wallStart) {
for (var fillX = wallStart; fillX <= wallEnd; fillX++) {
this.walls[fillX][y] = true; // Fill the gap
}
}
wallStart = -1; // Reset for next segment
wallEnd = -1;
}
}
// Complete any remaining segment at end of row
if (wallStart !== -1 && wallEnd !== -1 && wallEnd > wallStart) {
for (var fillX = wallStart; fillX <= wallEnd; fillX++) {
this.walls[fillX][y] = true;
}
}
}
// Trace vertical lines and complete them
for (var x = 0; x < this.width; x++) {
var wallStart = -1;
var wallEnd = -1;
// Find wall segments in this column
for (var y = 0; y < this.height; y++) {
if (this.walls[x][y]) {
if (wallStart === -1) {
wallStart = y; // Start of wall segment
}
wallEnd = y; // Update end of wall segment
} else {
// If we found a wall segment, complete the line between start and end
if (wallStart !== -1 && wallEnd !== -1 && wallEnd > wallStart) {
for (var fillY = wallStart; fillY <= wallEnd; fillY++) {
this.walls[x][fillY] = true; // Fill the gap
}
}
wallStart = -1; // Reset for next segment
wallEnd = -1;
}
}
// Complete any remaining segment at end of column
if (wallStart !== -1 && wallEnd !== -1 && wallEnd > wallStart) {
for (var fillY = wallStart; fillY <= wallEnd; fillY++) {
this.walls[x][fillY] = true;
}
}
}
};
// Apply wall line completion after initial generation
worldGrid.completeWallLines();
// Add dead-end detection system that preserves room connectivity
worldGrid.detectAndFillDeadEnds = function () {
// Find small isolated areas (not connected to main network) and mark them as walls
var visited = [];
// Initialize visited array
for (var x = 0; x < this.width; x++) {
visited[x] = [];
for (var y = 0; y < this.height; y++) {
visited[x][y] = false;
}
}
// Function to check if an area is a meaningful connected space
var isSignificantArea = function isSignificantArea(startX, startY) {
if (worldGrid.walls[startX][startY]) {
return true;
} // Wall positions are fine
if (visited[startX][startY]) {
return true;
} // Already processed
var localVisited = [];
for (var x = 0; x < worldGrid.width; x++) {
localVisited[x] = [];
for (var y = 0; y < worldGrid.height; y++) {
localVisited[x][y] = false;
}
}
var area = [];
var stack = [{
x: startX,
y: startY
}];
var hasChunkExit = false;
// Flood fill to find connected area
while (stack.length > 0) {
var current = stack.pop();
var x = current.x;
var y = current.y;
if (x < 0 || x >= worldGrid.width || y < 0 || y >= worldGrid.height) {
continue;
}
if (localVisited[x][y] || worldGrid.walls[x][y]) {
continue;
}
localVisited[x][y] = true;
visited[x][y] = true; // Mark as processed in main visited array
area.push({
x: x,
y: y
});
// Check if this area connects to chunk boundaries (significant exit)
if (x <= 1 || x >= worldGrid.width - 2 || y <= 1 || y >= worldGrid.height - 2) {
hasChunkExit = true;
}
// Add adjacent cells
var directions = [{
dx: 1,
dy: 0
}, {
dx: -1,
dy: 0
}, {
dx: 0,
dy: 1
}, {
dx: 0,
dy: -1
}];
for (var d = 0; d < directions.length; d++) {
stack.push({
x: x + directions[d].dx,
y: y + directions[d].dy
});
}
}
// Only fill small areas (less than 8 cells) that don't connect to chunk boundaries
if (area.length < 8 && !hasChunkExit) {
for (var i = 0; i < area.length; i++) {
worldGrid.walls[area[i].x][area[i].y] = true;
}
return false; // Area was filled
}
return true; // Area is significant and kept
};
// Check all open areas for significance
for (var x = 0; x < this.width; x++) {
for (var y = 0; y < this.height; y++) {
if (!visited[x][y] && !this.walls[x][y]) {
isSignificantArea(x, y);
}
}
}
};
// Apply dead-end detection after wall completion
worldGrid.detectAndFillDeadEnds();
// Add passage recognition system
worldGrid.passageRecognition = function () {
// Find all isolated rooms (areas without proper exits)
var isolatedRooms = this.findIsolatedRooms();
// Create passages for isolated rooms
for (var i = 0; i < isolatedRooms.length; i++) {
this.createPassageForRoom(isolatedRooms[i]);
}
};
// Find rooms that don't have adequate exits
worldGrid.findIsolatedRooms = function () {
var visited = [];
var isolatedRooms = [];
// Initialize visited array
for (var x = 0; x < this.width; x++) {
visited[x] = [];
for (var y = 0; y < this.height; y++) {
visited[x][y] = false;
}
}
// Check each open area for connectivity
for (var x = 1; x < this.width - 1; x++) {
for (var y = 1; y < this.height - 1; y++) {
if (!this.walls[x][y] && !visited[x][y]) {
var room = this.analyzeRoom(x, y, visited);
if (room && room.area.length >= 4) {
// Only consider rooms with at least 4 cells
var exitCount = this.countRoomExits(room);
if (exitCount === 0) {
isolatedRooms.push(room);
}
}
}
}
}
return isolatedRooms;
};
// Analyze a room starting from given coordinates
worldGrid.analyzeRoom = function (startX, startY, visited) {
var room = {
area: [],
bounds: {
minX: startX,
maxX: startX,
minY: startY,
maxY: startY
},
center: {
x: 0,
y: 0
}
};
var stack = [{
x: startX,
y: startY
}];
while (stack.length > 0) {
var current = stack.pop();
var x = current.x;
var y = current.y;
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
continue;
}
if (visited[x][y] || this.walls[x][y]) {
continue;
}
visited[x][y] = true;
room.area.push({
x: x,
y: y
});
// Update bounds
room.bounds.minX = Math.min(room.bounds.minX, x);
room.bounds.maxX = Math.max(room.bounds.maxX, x);
room.bounds.minY = Math.min(room.bounds.minY, y);
room.bounds.maxY = Math.max(room.bounds.maxY, y);
// Add adjacent cells
var directions = [{
dx: 1,
dy: 0
}, {
dx: -1,
dy: 0
}, {
dx: 0,
dy: 1
}, {
dx: 0,
dy: -1
}];
for (var d = 0; d < directions.length; d++) {
stack.push({
x: x + directions[d].dx,
y: y + directions[d].dy
});
}
}
// Calculate center
if (room.area.length > 0) {
var centerX = Math.floor((room.bounds.minX + room.bounds.maxX) / 2);
var centerY = Math.floor((room.bounds.minY + room.bounds.maxY) / 2);
room.center = {
x: centerX,
y: centerY
};
}
return room.area.length > 0 ? room : null;
};
// Count the number of exits a room has
worldGrid.countRoomExits = function (room) {
var exits = 0;
var checkedPositions = [];
// Check room perimeter for connections to other areas
for (var i = 0; i < room.area.length; i++) {
var cell = room.area[i];
var directions = [{
dx: 1,
dy: 0
}, {
dx: -1,
dy: 0
}, {
dx: 0,
dy: 1
}, {
dx: 0,
dy: -1
}];
for (var d = 0; d < directions.length; d++) {
var checkX = cell.x + directions[d].dx;
var checkY = cell.y + directions[d].dy;
// Skip if out of bounds
if (checkX < 0 || checkX >= this.width || checkY < 0 || checkY >= this.height) {
continue;
}
// If we find an open area that's not part of this room, it's a potential exit
if (!this.walls[checkX][checkY]) {
var isPartOfRoom = false;
for (var j = 0; j < room.area.length; j++) {
if (room.area[j].x === checkX && room.area[j].y === checkY) {
isPartOfRoom = true;
break;
}
}
if (!isPartOfRoom) {
// Check if this exit position was already counted
var posKey = checkX + ',' + checkY;
var alreadyCounted = false;
for (var k = 0; k < checkedPositions.length; k++) {
if (checkedPositions[k] === posKey) {
alreadyCounted = true;
break;
}
}
if (!alreadyCounted) {
exits++;
checkedPositions.push(posKey);
}
}
}
}
}
return exits;
};
// Create a passage for an isolated room
worldGrid.createPassageForRoom = function (room) {
if (!room || room.area.length === 0) {
return;
}
// Find the best direction to create a passage
var directions = [{
dx: 1,
dy: 0,
name: 'east'
}, {
dx: -1,
dy: 0,
name: 'west'
}, {
dx: 0,
dy: 1,
name: 'south'
}, {
dx: 0,
dy: -1,
name: 'north'
}];
var bestDirection = null;
var shortestDistance = Infinity;
// For each direction, find the shortest path to open space
for (var d = 0; d < directions.length; d++) {
var dir = directions[d];
var distance = this.findDistanceToOpenSpace(room.center.x, room.center.y, dir.dx, dir.dy);
if (distance < shortestDistance && distance > 0) {
shortestDistance = distance;
bestDirection = dir;
}
}
// Create passage in the best direction
if (bestDirection && shortestDistance <= 5) {
// Limit passage length
this.createPassageInDirection(room.center.x, room.center.y, bestDirection.dx, bestDirection.dy, shortestDistance);
} else {
// If no good direction found, create a passage to the nearest chunk boundary
this.createPassageToChunkBoundary(room);
}
};
// Find distance to open space in a given direction
worldGrid.findDistanceToOpenSpace = function (startX, startY, dirX, dirY) {
var distance = 0;
var maxDistance = 6; // Limit search distance
for (var i = 1; i <= maxDistance; i++) {
var checkX = startX + dirX * i;
var checkY = startY + dirY * i;
// Check bounds
if (checkX < 1 || checkX >= this.width - 1 || checkY < 1 || checkY >= this.height - 1) {
return maxDistance + 1; // Out of bounds
}
// If we find open space, return distance
if (!this.walls[checkX][checkY]) {
return i;
}
distance = i;
}
return distance;
};
// Create a passage in the specified direction
worldGrid.createPassageInDirection = function (startX, startY, dirX, dirY, length) {
for (var i = 0; i <= length; i++) {
var passageX = startX + dirX * i;
var passageY = startY + dirY * i;
// Ensure passage coordinates are valid
if (passageX >= 0 && passageX < this.width && passageY >= 0 && passageY < this.height) {
this.walls[passageX][passageY] = false;
// Create wider passage (2 cells wide) for better navigation
if (dirX !== 0) {
// Horizontal passage
if (passageY + 1 < this.height) {
this.walls[passageX][passageY + 1] = false;
}
if (passageY - 1 >= 0) {
this.walls[passageX][passageY - 1] = false;
}
} else {
// Vertical passage
if (passageX + 1 < this.width) {
this.walls[passageX + 1][passageY] = false;
}
if (passageX - 1 >= 0) {
this.walls[passageX - 1][passageY] = false;
}
}
}
}
};
// Create passage to chunk boundary if no nearby open space
worldGrid.createPassageToChunkBoundary = function (room) {
var centerX = room.center.x;
var centerY = room.center.y;
// Find closest chunk boundary
var distanceToLeft = centerX;
var distanceToRight = this.width - 1 - centerX;
var distanceToTop = centerY;
var distanceToBottom = this.height - 1 - centerY;
var minDistance = Math.min(distanceToLeft, distanceToRight, distanceToTop, distanceToBottom);
if (minDistance === distanceToLeft) {
// Create passage to left boundary
this.createPassageInDirection(centerX, centerY, -1, 0, distanceToLeft);
} else if (minDistance === distanceToRight) {
// Create passage to right boundary
this.createPassageInDirection(centerX, centerY, 1, 0, distanceToRight);
} else if (minDistance === distanceToTop) {
// Create passage to top boundary
this.createPassageInDirection(centerX, centerY, 0, -1, distanceToTop);
} else {
// Create passage to bottom boundary
this.createPassageInDirection(centerX, centerY, 0, 1, distanceToBottom);
}
};
// Apply passage recognition system after dead-end detection
worldGrid.passageRecognition();
// Add method to check for isolated areas near player and create passages
worldGrid.checkPlayerProximityForPassages = function (playerX, playerY) {
var playerGridX = Math.floor(playerX / this.cellSize);
var playerGridY = Math.floor(playerY / this.cellSize);
var checkRadius = 8; // Check 8 grid cells around player
// Check areas around player for potential isolation
for (var dx = -checkRadius; dx <= checkRadius; dx++) {
for (var dy = -checkRadius; dy <= checkRadius; dy++) {
var checkX = playerGridX + dx;
var checkY = playerGridY + dy;
// Skip if out of bounds
if (checkX < 1 || checkX >= this.width - 1 || checkY < 1 || checkY >= this.height - 1) {
continue;
}
// If this is an open area, check if it needs a passage
if (!this.walls[checkX][checkY]) {
var needsPassage = this.checkIfAreaNeedsPassage(checkX, checkY, playerGridX, playerGridY);
if (needsPassage) {
this.createEmergencyPassage(checkX, checkY, playerGridX, playerGridY);
}
}
}
}
};
// Check if an area needs an emergency passage
worldGrid.checkIfAreaNeedsPassage = function (areaX, areaY, playerX, playerY) {
// Quick flood fill to check if this area has limited connectivity
var visited = [];
for (var x = 0; x < this.width; x++) {
visited[x] = [];
for (var y = 0; y < this.height; y++) {
visited[x][y] = false;
}
}
var reachableCells = [];
var stack = [{
x: areaX,
y: areaY
}];
var hasChunkExit = false;
while (stack.length > 0 && reachableCells.length < 50) {
// Limit search for performance
var current = stack.pop();
var x = current.x;
var y = current.y;
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
continue;
}
if (visited[x][y] || this.walls[x][y]) {
continue;
}
visited[x][y] = true;
reachableCells.push({
x: x,
y: y
});
// Check if area connects to chunk boundaries
if (x <= 2 || x >= this.width - 3 || y <= 2 || y >= this.height - 3) {
hasChunkExit = true;
}
// Add adjacent cells
var directions = [{
dx: 1,
dy: 0
}, {
dx: -1,
dy: 0
}, {
dx: 0,
dy: 1
}, {
dx: 0,
dy: -1
}];
for (var d = 0; d < directions.length; d++) {
stack.push({
x: x + directions[d].dx,
y: y + directions[d].dy
});
}
}
// If area is small and doesn't connect to chunk boundaries, it needs a passage
return reachableCells.length < 20 && !hasChunkExit;
};
// Create an emergency passage from isolated area toward player or main areas
worldGrid.createEmergencyPassage = function (areaX, areaY, playerX, playerY) {
// Calculate direction toward player
var dirToPlayerX = playerX - areaX;
var dirToPlayerY = playerY - areaY;
// Normalize direction
var dirX = dirToPlayerX > 0 ? 1 : dirToPlayerX < 0 ? -1 : 0;
var dirY = dirToPlayerY > 0 ? 1 : dirToPlayerY < 0 ? -1 : 0;
// Create passage toward player or toward center
var targetX = dirX !== 0 ? areaX + dirX * 3 : areaX;
var targetY = dirY !== 0 ? areaY + dirY * 3 : areaY;
// Ensure target is within bounds
targetX = Math.max(1, Math.min(this.width - 2, targetX));
targetY = Math.max(1, Math.min(this.height - 2, targetY));
// Create L-shaped passage to target
this.createSimplePassage(areaX, areaY, targetX, targetY);
};
// Create a simple passage between two points
worldGrid.createSimplePassage = function (x1, y1, x2, y2) {
// Create horizontal segment first
var minX = Math.min(x1, x2);
var maxX = Math.max(x1, x2);
for (var x = minX; x <= maxX; x++) {
if (x >= 0 && x < this.width && y1 >= 0 && y1 < this.height) {
this.walls[x][y1] = false;
}
}
// Create vertical segment
var minY = Math.min(y1, y2);
var maxY = Math.max(y1, y2);
for (var y = minY; y <= maxY; y++) {
if (x2 >= 0 && x2 < this.width && y >= 0 && y < this.height) {
this.walls[x2][y] = false;
}
}
};
// Create geometric wall renderer
var wallRenderer = new GeometricWallRenderer();
game.addChild(wallRenderer);
// Create ceiling tile renderer
var ceilingTileRenderer = new CeilingTileRenderer();
game.addChild(ceilingTileRenderer);
ceilingTileRenderer.generateTiles();
// Create light manager
var lightManager = new LightManager();
game.addChild(lightManager);
// Add lights at random positions
for (var i = 0; i < 10; i++) {
var randomX = Math.random() * worldGrid.width * worldGrid.cellSize;
var randomY = Math.random() * worldGrid.height * worldGrid.cellSize;
lightManager.addLight(randomX, randomY);
}
// Function to find a safe spawn position
function findSafeSpawnPosition(startX, startY, searchRadius) {
searchRadius = searchRadius || 5;
// First check if starting position is safe
if (!worldGrid.checkCollision(startX, startY)) {
return {
x: startX,
y: startY
};
}
// Search in expanding circles for a safe position
for (var radius = 1; radius <= searchRadius; radius++) {
for (var angle = 0; angle < Math.PI * 2; angle += Math.PI / 8) {
var testX = startX + Math.cos(angle) * radius * worldGrid.cellSize;
var testY = startY + Math.sin(angle) * radius * worldGrid.cellSize;
// Check bounds
if (testX >= worldGrid.cellSize && testX < (worldGrid.width - 1) * worldGrid.cellSize && testY >= worldGrid.cellSize && testY < (worldGrid.height - 1) * worldGrid.cellSize) {
if (!worldGrid.checkCollision(testX, testY)) {
return {
x: testX,
y: testY
};
}
}
}
}
// If no safe position found in search radius, force create one
var fallbackX = Math.floor(worldGrid.width / 2) * worldGrid.cellSize;
var fallbackY = Math.floor(worldGrid.height / 2) * worldGrid.cellSize;
// Clear a 3x3 area around fallback position
var fallbackGridX = Math.floor(fallbackX / worldGrid.cellSize);
var fallbackGridY = Math.floor(fallbackY / worldGrid.cellSize);
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
var clearX = fallbackGridX + dx;
var clearY = fallbackGridY + dy;
if (clearX >= 0 && clearX < worldGrid.width && clearY >= 0 && clearY < worldGrid.height) {
worldGrid.walls[clearX][clearY] = false;
}
}
}
return {
x: fallbackX,
y: fallbackY
};
}
// Create player
var player = new Player();
// Find safe spawn position
var spawnCenter = {
x: worldGrid.width * worldGrid.cellSize / 2,
y: worldGrid.height * worldGrid.cellSize / 2
};
var safeSpawn = findSafeSpawnPosition(spawnCenter.x, spawnCenter.y, 10);
// Position player at safe spawn location
player.x = safeSpawn.x;
player.y = safeSpawn.y;
player.targetX = safeSpawn.x;
player.targetY = safeSpawn.y;
game.addChild(player);
// Create raycasting renderer
var raycastRenderer = new RaycastRenderer();
game.addChild(raycastRenderer);
// FPS counter variables
var fpsCounter = 0;
var fpsDisplay = 0;
var lastFpsTime = Date.now();
// Create coordinate display text
var coordXText = new Text2('X: 0', {
size: 60,
fill: 0xFFFFFF
});
coordXText.anchor.set(0, 0);
coordXText.x = 120; // Avoid top-left 100x100 area
coordXText.y = 120;
LK.gui.addChild(coordXText);
var coordZText = new Text2('Z: 0', {
size: 60,
fill: 0xFFFFFF
});
coordZText.anchor.set(0, 0);
coordZText.x = 120; // Avoid top-left 100x100 area
coordZText.y = 200;
LK.gui.addChild(coordZText);
// Create FPS display text
var fpsText = new Text2('FPS: 60', {
size: 60,
fill: 0x00FF00
});
fpsText.anchor.set(0, 0);
fpsText.x = 120; // Avoid top-left 100x100 area
fpsText.y = 280;
LK.gui.addChild(fpsText);
// Create movement status display
var movementText = new Text2('Standing Still', {
size: 60,
fill: 0xFFFFFF
});
movementText.anchor.set(0, 0);
movementText.x = 120; // Avoid top-left 100x100 area
movementText.y = 360;
LK.gui.addChild(movementText);
// Create movement distance display
var distanceText = new Text2('Distance: 0.0', {
size: 50,
fill: 0xFFFFFF
});
distanceText.anchor.set(0, 0);
distanceText.x = 120; // Avoid top-left 100x100 area
distanceText.y = 440;
LK.gui.addChild(distanceText);
// Create room size display
var roomSizeText = new Text2('Room: Unknown', {
size: 50,
fill: 0xFFFFFF
});
roomSizeText.anchor.set(0, 0);
roomSizeText.x = 120; // Avoid top-left 100x100 area
roomSizeText.y = 480;
LK.gui.addChild(roomSizeText);
// Create settings button in top-right corner (larger for mobile)
var settingsButton = LK.getAsset('untexturedArea', {
anchorX: 1,
anchorY: 0,
width: 120,
height: 120
});
settingsButton.tint = 0x444444;
settingsButton.alpha = 0.7;
LK.gui.topRight.addChild(settingsButton);
var settingsText = new Text2('⚙', {
size: 60,
fill: 0xFFFFFF
});
settingsText.anchor.set(0.5, 0.5);
settingsText.x = -60;
settingsText.y = 60;
LK.gui.topRight.addChild(settingsText);
// Create sensitivity configuration panel
var sensitivityConfig = new SensitivityConfig();
sensitivityConfig.x = 2732 - 320;
sensitivityConfig.y = 100;
LK.gui.addChild(sensitivityConfig);
// Create movement crosshair for better mobile controls
var movementCrosshair = new MovementCrosshair();
movementCrosshair.x = 200; // Position on left side
movementCrosshair.y = 2048 - 200; // Bottom left area
LK.gui.addChild(movementCrosshair);
// Function to find a random valid door position
function findRandomDoorPosition() {
var maxAttempts = 100;
var attempts = 0;
// Player spawn area - center of the world
var spawnGridX = Math.floor(worldGrid.width / 2);
var spawnGridY = Math.floor(worldGrid.height / 2);
var minDistanceFromPlayer = 15; // Minimum distance in grid cells from player spawn
while (attempts < maxAttempts) {
// Generate random position in world bounds (avoid edges)
var randomGridX = Math.floor(Math.random() * (worldGrid.width - 10)) + 5;
var randomGridY = Math.floor(Math.random() * (worldGrid.height - 10)) + 5;
// Calculate distance from player spawn position
var distanceFromSpawn = Math.sqrt((randomGridX - spawnGridX) * (randomGridX - spawnGridX) + (randomGridY - spawnGridY) * (randomGridY - spawnGridY));
// Skip if too close to player spawn area
if (distanceFromSpawn < minDistanceFromPlayer) {
attempts++;
continue;
}
var testX = randomGridX * worldGrid.cellSize;
var testY = randomGridY * worldGrid.cellSize;
// Check if position is in an open area (not a wall)
if (!worldGrid.walls[randomGridX][randomGridY]) {
// Check surrounding area for room space (3x3 area should be mostly open)
var openCells = 0;
var totalCells = 0;
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
var checkX = randomGridX + dx;
var checkY = randomGridY + dy;
if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) {
totalCells++;
if (!worldGrid.walls[checkX][checkY]) {
openCells++;
}
}
}
}
// If at least 70% of surrounding area is open, it's a good position
if (openCells >= totalCells * 0.7) {
return {
x: testX,
y: testY,
gridX: randomGridX,
gridY: randomGridY
};
}
}
attempts++;
}
// Fallback to guaranteed far position if no good position found
// Place door in corner area far from spawn
var fallbackGridX = worldGrid.width - 8; // Near right edge
var fallbackGridY = worldGrid.height - 8; // Near bottom edge
return {
x: fallbackGridX * worldGrid.cellSize,
y: fallbackGridY * worldGrid.cellSize,
gridX: fallbackGridX,
gridY: fallbackGridY
};
}
// Create door instance and position it randomly in world
var door = new Door();
// Find a random valid position for the door
var doorPosition = findRandomDoorPosition();
var doorX = doorPosition.x;
var doorY = doorPosition.y;
var doorGridX = doorPosition.gridX;
var doorGridY = doorPosition.gridY;
door.x = doorX;
door.y = doorY;
// Keep door at exact world level for proper collision and rendering
game.addChild(door);
// Add door tracking to world grid
worldGrid.doorPosition = {
x: doorX,
y: doorY,
gridX: doorGridX,
gridY: doorGridY
};
// Add door detection method to world grid
worldGrid.hasDoorAt = function (worldX, worldY) {
if (!this.doorPosition) return false;
var dx = Math.abs(worldX - this.doorPosition.x);
var dy = Math.abs(worldY - this.doorPosition.y);
return dx < 50 && dy < 50; // Door collision area
};
// Dynamic path generation system
worldGrid.pathToExit = {
generatedRooms: [],
connectionPoints: [],
lastPlayerDistance: Infinity,
pathSegments: [],
// Generate progressive path as player approaches door
generatePathToDoor: function generatePathToDoor(playerX, playerY, doorX, doorY) {
var playerGridX = Math.floor(playerX / worldGrid.cellSize);
var playerGridY = Math.floor(playerY / worldGrid.cellSize);
var doorGridX = Math.floor(doorX / worldGrid.cellSize);
var doorGridY = Math.floor(doorY / worldGrid.cellSize);
var distanceToDoor = Math.sqrt((playerGridX - doorGridX) * (playerGridX - doorGridX) + (playerGridY - doorGridY) * (playerGridY - doorGridY));
// Generate rooms when player is within 25 cells of door
if (distanceToDoor <= 25 && distanceToDoor < this.lastPlayerDistance) {
this.createPathSegment(playerGridX, playerGridY, doorGridX, doorGridY, distanceToDoor);
}
this.lastPlayerDistance = distanceToDoor;
},
// Create a segment of the path with connected rooms
createPathSegment: function createPathSegment(playerX, playerY, doorX, doorY, distance) {
var segmentId = Math.floor(distance / 5); // Create segments every 5 units
// Skip if this segment already exists
for (var i = 0; i < this.pathSegments.length; i++) {
if (this.pathSegments[i].id === segmentId) {
return;
}
}
// Calculate direction from player toward door
var dirX = doorX - playerX;
var dirY = doorY - playerY;
var pathLength = Math.sqrt(dirX * dirX + dirY * dirY);
if (pathLength > 0) {
dirX = dirX / pathLength;
dirY = dirY / pathLength;
}
// Create intermediate room positions along the path
var numRooms = Math.min(3, Math.floor(distance / 8) + 1);
var newRooms = [];
for (var i = 0; i < numRooms; i++) {
var t = (i + 1) / (numRooms + 1);
var roomX = Math.floor(playerX + dirX * distance * t);
var roomY = Math.floor(playerY + dirY * distance * t);
// Add some randomness to avoid straight lines
roomX += Math.floor(Math.random() * 6) - 3;
roomY += Math.floor(Math.random() * 6) - 3;
// Ensure room is within bounds
roomX = Math.max(2, Math.min(worldGrid.width - 3, roomX));
roomY = Math.max(2, Math.min(worldGrid.height - 3, roomY));
var room = this.createConnectedRoom(roomX, roomY, 2 + Math.floor(Math.random() * 2));
if (room) {
newRooms.push(room);
this.generatedRooms.push(room);
}
}
// Connect new rooms in sequence
for (var i = 0; i < newRooms.length - 1; i++) {
this.createRoomConnection(newRooms[i], newRooms[i + 1]);
}
// Connect first room to existing path or player area
if (newRooms.length > 0) {
var nearestExisting = this.findNearestExistingRoom(newRooms[0], playerX, playerY);
if (nearestExisting) {
this.createRoomConnection(newRooms[0], nearestExisting);
} else {
// Connect to player's current area
this.createPathToPosition(newRooms[0], playerX, playerY);
}
// Connect last room toward door
var lastRoom = newRooms[newRooms.length - 1];
this.createPathToPosition(lastRoom, doorX, doorY);
}
// Store segment information
this.pathSegments.push({
id: segmentId,
rooms: newRooms,
playerDistance: distance
});
},
// Create a connected room at specified position
createConnectedRoom: function createConnectedRoom(centerX, centerY, size) {
// Check if area is already carved
var hasWalls = false;
for (var dx = -size; dx <= size; dx++) {
for (var dy = -size; dy <= size; dy++) {
var checkX = centerX + dx;
var checkY = centerY + dy;
if (checkX >= 0 && checkX < worldGrid.width && checkY >= 0 && checkY < worldGrid.height) {
if (worldGrid.walls[checkX][checkY]) {
hasWalls = true;
break;
}
}
}
if (hasWalls) break;
}
// Only create room if there are walls to carve
if (!hasWalls) {
return null;
}
var room = {
centerX: centerX,
centerY: centerY,
size: size,
x: centerX - size,
y: centerY - size,
width: size * 2 + 1,
height: size * 2 + 1
};
// Carve room with irregular shape
this.carveConnectedRoom(centerX, centerY, size);
return room;
},
// Carve a room with connected shape
carveConnectedRoom: function carveConnectedRoom(centerX, centerY, size) {
var roomType = Math.random();
if (roomType < 0.4) {
// Circular room
for (var dx = -size; dx <= size; dx++) {
for (var dy = -size; dy <= size; dy++) {
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= size + Math.random() * 0.5) {
var roomX = centerX + dx;
var roomY = centerY + dy;
if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) {
worldGrid.walls[roomX][roomY] = false;
}
}
}
}
} else if (roomType < 0.7) {
// L-shaped room
// Horizontal bar
for (var dx = -size; dx <= size; dx++) {
for (var dy = -1; dy <= 1; dy++) {
var roomX = centerX + dx;
var roomY = centerY + dy;
if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) {
worldGrid.walls[roomX][roomY] = false;
}
}
}
// Vertical bar
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -size; dy <= size; dy++) {
var roomX = centerX + dx;
var roomY = centerY + dy;
if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) {
worldGrid.walls[roomX][roomY] = false;
}
}
}
} else {
// Rectangular room
for (var dx = -size; dx <= size; dx++) {
for (var dy = -size; dy <= size; dy++) {
var roomX = centerX + dx;
var roomY = centerY + dy;
if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) {
worldGrid.walls[roomX][roomY] = false;
}
}
}
}
},
// Create connection between two rooms
createRoomConnection: function createRoomConnection(room1, room2) {
if (!room1 || !room2) return;
var x1 = room1.centerX;
var y1 = room1.centerY;
var x2 = room2.centerX;
var y2 = room2.centerY;
// Create L-shaped corridor
this.createWideCorridor(x1, y1, x2, y1); // Horizontal
this.createWideCorridor(x2, y1, x2, y2); // Vertical
},
// Create wide corridor between two points
createWideCorridor: function createWideCorridor(x1, y1, x2, y2) {
var minX = Math.min(x1, x2);
var maxX = Math.max(x1, x2);
var minY = Math.min(y1, y2);
var maxY = Math.max(y1, y2);
for (var x = minX; x <= maxX; x++) {
for (var y = minY; y <= maxY; y++) {
// Create 3-wide corridor
for (var w = -1; w <= 1; w++) {
var corridorX = x + (x1 === x2 ? w : 0);
var corridorY = y + (y1 === y2 ? w : 0);
if (corridorX >= 0 && corridorX < worldGrid.width && corridorY >= 0 && corridorY < worldGrid.height) {
worldGrid.walls[corridorX][corridorY] = false;
}
}
}
}
},
// Find nearest existing room to connect to
findNearestExistingRoom: function findNearestExistingRoom(newRoom, playerX, playerY) {
var nearestRoom = null;
var minDistance = Infinity;
for (var i = 0; i < this.generatedRooms.length; i++) {
var room = this.generatedRooms[i];
if (room === newRoom) continue;
var dx = newRoom.centerX - room.centerX;
var dy = newRoom.centerY - room.centerY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistance && distance < 15) {
// Only connect to nearby rooms
minDistance = distance;
nearestRoom = room;
}
}
return nearestRoom;
},
// Create path from room to specific position
createPathToPosition: function createPathToPosition(room, targetX, targetY) {
if (!room) return;
var distance = Math.sqrt((room.centerX - targetX) * (room.centerX - targetX) + (room.centerY - targetY) * (room.centerY - targetY));
// Only create path if target is reasonably close
if (distance < 20) {
this.createWideCorridor(room.centerX, room.centerY, targetX, targetY);
}
}
};
// Create initial room around door
var doorRoom = worldGrid.pathToExit.createConnectedRoom(doorGridX, doorGridY, 3);
if (doorRoom) {
worldGrid.pathToExit.generatedRooms.push(doorRoom);
}
// Create look up button (right side of screen)
var lookUpButton = LK.getAsset('untexturedArea', {
anchorX: 0.5,
anchorY: 0.5,
width: 150,
height: 100
});
lookUpButton.tint = 0x444444;
lookUpButton.alpha = 0.7;
lookUpButton.x = 2732 - 200; // Right side
lookUpButton.y = 2048 - 400; // Above look down button
LK.gui.addChild(lookUpButton);
var lookUpText = new Text2('▲', {
size: 50,
fill: 0xFFFFFF
});
lookUpText.anchor.set(0.5, 0.5);
lookUpText.x = 2732 - 200;
lookUpText.y = 2048 - 400;
LK.gui.addChild(lookUpText);
// Create look down button (right side of screen)
var lookDownButton = LK.getAsset('untexturedArea', {
anchorX: 0.5,
anchorY: 0.5,
width: 150,
height: 100
});
lookDownButton.tint = 0x444444;
lookDownButton.alpha = 0.7;
lookDownButton.x = 2732 - 200; // Right side
lookDownButton.y = 2048 - 200; // Bottom right area
LK.gui.addChild(lookDownButton);
var lookDownText = new Text2('▼', {
size: 50,
fill: 0xFFFFFF
});
lookDownText.anchor.set(0.5, 0.5);
lookDownText.x = 2732 - 200;
lookDownText.y = 2048 - 200;
LK.gui.addChild(lookDownText);
// Look up button handlers
lookUpButton.down = function (x, y, obj) {
lookUpButton.alpha = 1.0; // Visual feedback
lookUp = true;
};
lookUpButton.up = function (x, y, obj) {
lookUpButton.alpha = 0.7; // Reset visual
lookUp = false;
};
// Look down button handlers
lookDownButton.down = function (x, y, obj) {
lookDownButton.alpha = 1.0; // Visual feedback
lookDown = true;
};
lookDownButton.up = function (x, y, obj) {
lookDownButton.alpha = 0.7; // Reset visual
lookDown = false;
};
// Movement flags
var moveForward = false;
var moveBackward = false;
var turnLeft = false;
var turnRight = false;
var lookUp = false;
var lookDown = false;
// Player movement recognition system
var playerMovementRecognition = {
lastX: 0,
lastY: 0,
lastAngle: 0,
isMoving: false,
wasMoving: false,
movementStartTime: 0,
movementDistance: 0,
movementDirection: {
x: 0,
y: 0
},
rotationAmount: 0
};
// Touch controls for movement
var touchStartX = 0;
var touchStartY = 0;
var touchActive = false;
// Settings button click handler
settingsButton.down = function (x, y, obj) {
sensitivityConfig.toggle();
};
game.down = function (x, y, obj) {
touchStartX = x;
touchStartY = y;
touchActive = true;
// Forward movement on touch
moveForward = true;
};
game.up = function (x, y, obj) {
touchActive = false;
moveForward = false;
moveBackward = false;
turnLeft = false;
turnRight = false;
lookUp = false;
lookDown = false;
// Reset crosshair if not actively being used
if (!movementCrosshair.activeButton) {
movementCrosshair.resetMovement();
}
};
game.move = function (x, y, obj) {
if (!touchActive) {
return;
}
var deltaX = x - touchStartX;
var deltaY = y - touchStartY;
// Horizontal movement for turning
if (Math.abs(deltaX) > 50) {
if (deltaX > 0) {
turnRight = true;
turnLeft = false;
} else {
turnLeft = true;
turnRight = false;
}
} else {
turnLeft = false;
turnRight = false;
}
// Vertical movement - split between forward/backward and look up/down
if (Math.abs(deltaY) > 50) {
// If touch is in upper part of screen, use for looking up/down
if (y < 1024) {
// Upper half of screen for vertical look
if (deltaY < 0) {
lookUp = true;
lookDown = false;
} else {
lookDown = true;
lookUp = false;
}
moveForward = false;
moveBackward = false;
} else {
// Lower half of screen for movement
if (deltaY < 0) {
moveForward = true;
moveBackward = false;
} else {
moveBackward = true;
moveForward = false;
}
lookUp = false;
lookDown = false;
}
} else {
lookUp = false;
lookDown = false;
}
};
game.update = function () {
// Initialize movement recognition on first frame
if (playerMovementRecognition.lastX === 0 && playerMovementRecognition.lastY === 0) {
playerMovementRecognition.lastX = player.x;
playerMovementRecognition.lastY = player.y;
playerMovementRecognition.lastAngle = player.angle;
}
// Update player rotation speed based on sensitivity (0-100 maps to 0.02-0.08) - reduced for lower sensitivity
var sensitivityValue = sensitivityConfig.sensitivity;
player.rotSpeed = 0.02 + sensitivityValue / 100 * 0.06;
// Get movement state from crosshair
var crosshairState = movementCrosshair.getMovementState();
// Handle movement (combine touch controls and crosshair)
if (moveForward || crosshairState.forward) {
player.moveForward();
}
if (moveBackward || crosshairState.backward) {
player.moveBackward();
}
if (turnLeft || crosshairState.left) {
player.turnLeft();
}
if (turnRight || crosshairState.right) {
player.turnRight();
}
if (lookUp) {
player.lookUp();
}
if (lookDown) {
player.lookDown();
}
// Apply smooth interpolation
player.updateSmooth();
// Movement Recognition System
var currentTime = Date.now();
// Calculate movement deltas
var deltaX = player.x - playerMovementRecognition.lastX;
var deltaY = player.y - playerMovementRecognition.lastY;
var deltaAngle = player.angle - playerMovementRecognition.lastAngle;
// Handle angle wrapping for rotation detection
if (deltaAngle > Math.PI) {
deltaAngle -= 2 * Math.PI;
}
if (deltaAngle < -Math.PI) {
deltaAngle += 2 * Math.PI;
}
// Calculate movement distance and rotation
var movementDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
var rotationAmount = Math.abs(deltaAngle);
// Update movement direction
if (movementDistance > 0.1) {
playerMovementRecognition.movementDirection.x = deltaX / movementDistance;
playerMovementRecognition.movementDirection.y = deltaY / movementDistance;
}
// Determine if player is currently moving (position or rotation)
var movementThreshold = 0.5; // Minimum movement to be considered "moving"
var rotationThreshold = 0.01; // Minimum rotation to be considered "turning"
var isCurrentlyMoving = movementDistance > movementThreshold || rotationAmount > rotationThreshold;
// Update movement state
playerMovementRecognition.wasMoving = playerMovementRecognition.isMoving;
playerMovementRecognition.isMoving = isCurrentlyMoving;
// Track movement start time
if (!playerMovementRecognition.wasMoving && playerMovementRecognition.isMoving) {
// Movement just started
playerMovementRecognition.movementStartTime = currentTime;
playerMovementRecognition.movementDistance = 0;
// Start looping footstep sound
LK.playMusic('4');
}
// Stop footstep sound when movement stops
if (playerMovementRecognition.wasMoving && !playerMovementRecognition.isMoving) {
// Movement just stopped - stop footstep loop
LK.stopMusic();
}
// Accumulate total movement distance
if (playerMovementRecognition.isMoving) {
playerMovementRecognition.movementDistance += movementDistance;
}
// Update movement display
var movementStatus = "Standing Still";
var movementColor = 0xFFFFFF;
if (playerMovementRecognition.isMoving) {
if (movementDistance > rotationAmount * 10) {
// More movement than rotation
if (deltaX > 0.1) {
movementStatus = "Moving East";
} else if (deltaX < -0.1) {
movementStatus = "Moving West";
} else if (deltaY > 0.1) {
movementStatus = "Moving South";
} else if (deltaY < -0.1) {
movementStatus = "Moving North";
} else {
movementStatus = "Moving";
}
movementColor = 0x00FF00; // Green for movement
} else {
// More rotation than movement
if (deltaAngle > 0.01) {
movementStatus = "Turning Right";
} else if (deltaAngle < -0.01) {
movementStatus = "Turning Left";
} else {
movementStatus = "Turning";
}
movementColor = 0x00FFFF; // Cyan for rotation
}
} else if (playerMovementRecognition.wasMoving) {
// Just stopped moving
movementStatus = "Stopped";
movementColor = 0xFFFF00; // Yellow for just stopped
}
// Update display texts
movementText.setText(movementStatus);
movementText.fill = movementColor;
// Update distance display (rounded to 1 decimal place)
var totalDistance = Math.round(playerMovementRecognition.movementDistance * 10) / 10;
distanceText.setText('Distance: ' + totalDistance);
// Store current position and angle for next frame
playerMovementRecognition.lastX = player.x;
playerMovementRecognition.lastY = player.y;
playerMovementRecognition.lastAngle = player.angle;
playerMovementRecognition.rotationAmount = rotationAmount;
// Generate new chunks as player moves
if (LK.ticks % 30 === 0) {
// Check every 30 frames for performance
procGen.generateAroundPlayer(player.x, player.y);
// Check for isolated areas near player and create passages proactively
worldGrid.checkPlayerProximityForPassages(player.x, player.y);
// Generate dynamic path to exit door as player approaches
if (worldGrid.doorPosition) {
worldGrid.pathToExit.generatePathToDoor(player.x, player.y, worldGrid.doorPosition.x, worldGrid.doorPosition.y);
}
}
// Render the raycasted view
raycastRenderer.render(player);
// Render walls
wallRenderer.render(player);
// Render ceiling tiles
ceilingTileRenderer.render(player);
// Update FPS counter
fpsCounter++;
var currentTime = Date.now();
if (currentTime - lastFpsTime >= 1000) {
// Update every second
fpsDisplay = fpsCounter;
fpsCounter = 0;
lastFpsTime = currentTime;
// Color code FPS display based on performance
var fpsColor = 0x00FF00; // Green for good FPS (60+)
if (fpsDisplay < 30) {
fpsColor = 0xFF0000; // Red for poor FPS
} else if (fpsDisplay < 50) {
fpsColor = 0xFFFF00; // Yellow for moderate FPS
}
fpsText.fill = fpsColor;
fpsText.setText('FPS: ' + fpsDisplay);
}
// Keep sound 1 playing continuously (check every 60 frames)
if (LK.ticks % 60 === 0) {
// Restart sound 1 if it's not playing to maintain continuous loop
LK.getSound('1').play();
}
// Update coordinate display
var gridX = Math.floor(player.x / worldGrid.cellSize);
var gridZ = Math.floor(player.y / worldGrid.cellSize);
coordXText.setText('X: ' + gridX);
coordZText.setText('Z: ' + gridZ);
// Update room size display (check every 15 frames for performance)
if (LK.ticks % 15 === 0) {
var currentRoom = worldGrid.detectCurrentRoom(player.x, player.y);
var roomDisplayText = 'Habitación: ' + currentRoom.type;
if (currentRoom.area > 0) {
roomDisplayText += ' (' + currentRoom.area + ' celdas)';
}
roomSizeText.setText(roomDisplayText);
roomSizeText.fill = currentRoom.color || 0xFFFFFF;
}
};
// Play background music (song 2)
LK.playMusic('2');
// Play sound 3 as a loop
LK.playMusic('3');
// Play sound 1 as looping sound separate from music
LK.getSound('1').play();
worldGrid.isFloor = function (gridX, gridY) {
// Define logic to determine if a grid position is a floor
// For now, assume any position not marked as a wall is a floor
return !this.walls[gridX][gridY];
};
// Room detection system
worldGrid.detectCurrentRoom = function (playerX, playerY) {
var playerGridX = Math.floor(playerX / this.cellSize);
var playerGridY = Math.floor(playerY / this.cellSize);
// Check if player is in a valid position
if (playerGridX < 0 || playerGridX >= this.width || playerGridY < 0 || playerGridY >= this.height) {
return {
type: 'Outside',
area: 0
};
}
// If player is in a wall, return wall
if (this.walls[playerGridX][playerGridY]) {
return {
type: 'Wall',
area: 0
};
}
// Flood fill to find connected room area
var visited = [];
for (var x = 0; x < this.width; x++) {
visited[x] = [];
for (var y = 0; y < this.height; y++) {
visited[x][y] = false;
}
}
var roomCells = [];
var stack = [{
x: playerGridX,
y: playerGridY
}];
var bounds = {
minX: playerGridX,
maxX: playerGridX,
minY: playerGridY,
maxY: playerGridY
};
// Flood fill to find all connected open cells
while (stack.length > 0 && roomCells.length < 200) {
// Limit for performance
var current = stack.pop();
var x = current.x;
var y = current.y;
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
continue;
}
if (visited[x][y] || this.walls[x][y]) {
continue;
}
visited[x][y] = true;
roomCells.push({
x: x,
y: y
});
// Update bounds
bounds.minX = Math.min(bounds.minX, x);
bounds.maxX = Math.max(bounds.maxX, x);
bounds.minY = Math.min(bounds.minY, y);
bounds.maxY = Math.max(bounds.maxY, y);
// Add adjacent cells
var directions = [{
dx: 1,
dy: 0
}, {
dx: -1,
dy: 0
}, {
dx: 0,
dy: 1
}, {
dx: 0,
dy: -1
}];
for (var d = 0; d < directions.length; d++) {
stack.push({
x: x + directions[d].dx,
y: y + directions[d].dy
});
}
}
var roomArea = roomCells.length;
var roomType = 'Unknown';
var roomColor = 0xFFFFFF;
// Classify room size based on area
if (roomArea <= 4) {
roomType = 'Pequeña';
roomColor = 0xFF6666; // Light red for small
} else if (roomArea <= 16) {
roomType = 'Mediana';
roomColor = 0xFFFF66; // Yellow for medium
} else if (roomArea <= 50) {
roomType = 'Grande';
roomColor = 0x66FF66; // Light green for large
} else {
roomType = 'Muy Grande';
roomColor = 0x66FFFF; // Cyan for very large
}
return {
type: roomType,
area: roomArea,
color: roomColor,
bounds: bounds,
cells: roomCells
};
};