User prompt
Quita la información de errores
User prompt
Quita la información de errores
User prompt
Quita el mini mapa
User prompt
Sentra el texto de la puerta de salida
User prompt
Has que al tocar la puerta de salida la pantalla donde dice el texto sea comple oscuro y que el textos sean más anchas y de color blanco
User prompt
Has que al tocar la puerta de salida la pantalla donde dice el texto sea comple oscuro y que el textos sean más anchas y de color blanco
User prompt
Haz que las celdas que estén lejos del jugador se descarguen pero que no cambien su generación
User prompt
Has que un camino de habitaciones que simpre conecte con la puerta de salida pero que estás habitaciones conectadas se generen conforme el jugador se acerca a la puerta de salida
User prompt
Has que la habitación de puerta de salida esté simpre conectada a las habitaciones creadas proseduralmente
User prompt
Has que el sistema procedural simpre conecte una habitación generada aleatoriamente hacia un camino para que le jugador pueda acceder a la puerta de salida y has que si el sistema detecte que la habitación de salida es imposible de acceder haga un cambio de mapa para conectar esa habitación a otra
User prompt
Has que el sistema de creación procedural haga que simpre conecte la habitación de la puerta de salida con un cuarto o pasillo generado por el sistema de creación procedural
User prompt
Has que la puerta de salida evite a toda costa generarse junto al jugador
User prompt
Habla en español
User prompt
Has que la puerta de salida aparezca aleatoriamente pero que sea posible que el jugador interactúe con ella
User prompt
Has que allá caminos pre hechos hacia la puerta de salida
User prompt
El jugador aparece pegado al piso soluciona eso
User prompt
Has que el jugador aparezca en el medio de una habitación del borde del mapa
User prompt
Has que el jugador no pueda aparecer atravesando una pared
User prompt
haz que el jugador aparesca siempre en una de las 4 esquinas del mapa
User prompt
Has que cada cara visible de la puerta de salida sea tocable
User prompt
Has que la habitación con la puerta de salida este por lo mínimo 5 chucks de lejanía del jugador y has visto que la habitaciónes estén interconectadas una tras otra para que estén conectadas a la puerta de salida
User prompt
Has que la habitación de la puerta de salida siempre sea una habitación grande con por lo menos 2 entradas desde el jugador hasta la puerta de salida
User prompt
Has que el sistema procedural haga muchas entradas a otras habitaciones pero que no dejen de existir la sabría yo es sin salida
User prompt
has que la habitacion con la puerta de salida este mas lejos del jugador
User prompt
has que el sistema procedural simpre haga diferetes conecciones a otras avitaciones
/**** * 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; // Fix door positioning - ensure it's at proper world coordinates var targetX = (worldGrid.width / 2 + 3) * worldGrid.cellSize; var targetY = (worldGrid.height / 2 + 2) * worldGrid.cellSize; // 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 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 - position at minimap center var levelText = new Text2('NIVEL 1 COMPLETADO', { size: 100, fill: 0x00FF00 }); levelText.anchor.set(0.5, 0.5); levelText.x = 800; // Match minimap X position (center of minimap area) levelText.y = -200; // Start above screen levelText.alpha = 0; LK.gui.addChild(levelText); // Animate black screen fade in tween(blackScreen, { alpha: 0.8 }, { duration: 800, onFinish: function onFinish() { // Animate level text drop down and fade in - center at minimap position 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 levelText.setText('NIVEL 2'); levelText.fill = 0xFFFFFF; 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 ErrorChecker = Container.expand(function () { var self = Container.call(this); self.errorLog = []; self.maxLogSize = 50; self.checkInterval = 60; // Check every 60 frames self.lastCheckTime = 0; // Add error to log with timestamp self.logError = function (type, message, severity) { var error = { type: type, message: message, severity: severity || 'warning', // 'error', 'warning', 'info' timestamp: Date.now(), frame: LK.ticks }; self.errorLog.push(error); // Keep log size manageable if (self.errorLog.length > self.maxLogSize) { self.errorLog.shift(); } // Log to console based on severity if (severity === 'error') { console.error('[ERROR]', type + ':', message); } else if (severity === 'warning') { console.warn('[WARNING]', type + ':', message); } else { console.log('[INFO]', type + ':', message); } }; // Check player state for errors self.checkPlayerState = function () { if (!player) { self.logError('Player', 'Player object is null or undefined', 'error'); return false; } // Check position bounds if (player.x < 0 || player.x >= worldGrid.width * worldGrid.cellSize || player.y < 0 || player.y >= worldGrid.height * worldGrid.cellSize) { self.logError('Player', 'Player position out of world bounds: (' + Math.floor(player.x) + ', ' + Math.floor(player.y) + ')', 'warning'); } // Check for NaN values if (isNaN(player.x) || isNaN(player.y) || isNaN(player.angle)) { self.logError('Player', 'Player has NaN values - x:' + player.x + ' y:' + player.y + ' angle:' + player.angle, 'error'); return false; } // Check if player is stuck in walls if (worldGrid && worldGrid.checkCollision && worldGrid.checkCollision(player.x, player.y)) { self.logError('Player', 'Player is stuck inside wall at (' + Math.floor(player.x) + ', ' + Math.floor(player.y) + ')', 'warning'); } return true; }; // Check world grid integrity self.checkWorldGrid = function () { if (!worldGrid) { self.logError('WorldGrid', 'WorldGrid object is null or undefined', 'error'); return false; } if (!worldGrid.walls) { self.logError('WorldGrid', 'WorldGrid walls array is null or undefined', 'error'); return false; } // Check grid dimensions if (worldGrid.width <= 0 || worldGrid.height <= 0) { self.logError('WorldGrid', 'Invalid grid dimensions: ' + worldGrid.width + 'x' + worldGrid.height, 'error'); return false; } // Check for corrupted wall data var wallErrors = 0; if (worldGrid && worldGrid.walls && worldGrid.width && worldGrid.height) { for (var x = 0; x < Math.min(worldGrid.width, 10); x++) { if (!worldGrid.walls[x]) { wallErrors++; continue; } for (var y = 0; y < Math.min(worldGrid.height, 10); y++) { if (typeof worldGrid.walls[x][y] !== 'boolean') { wallErrors++; } } } } if (wallErrors > 0) { self.logError('WorldGrid', 'Found ' + wallErrors + ' corrupted wall data points', 'warning'); } return true; }; // Check renderer states self.checkRenderers = function () { // Check wall renderer if (typeof wallRenderer === 'undefined' || !wallRenderer) { self.logError('Renderer', 'Wall renderer is null or undefined', 'error'); return false; } if (wallRenderer.wallStrips && wallRenderer.wallStrips.length === 0) { self.logError('Renderer', 'Wall renderer has no wall strips initialized', 'warning'); } // Check raycast renderer if (typeof raycastRenderer === 'undefined' || !raycastRenderer) { self.logError('Renderer', 'Raycast renderer is null or undefined', 'error'); return false; } if (raycastRenderer.wallColumns && raycastRenderer.wallColumns.length === 0) { self.logError('Renderer', 'Raycast renderer has no columns initialized', 'warning'); } return true; }; // Check memory usage and performance self.checkPerformance = function () { // Check FPS if (typeof fpsDisplay !== 'undefined' && fpsDisplay < 20) { self.logError('Performance', 'Low FPS detected: ' + fpsDisplay, 'warning'); } // Check object counts var totalChildren = 0; if (game && game.children) { totalChildren = game.children.length; } if (totalChildren > 1000) { self.logError('Performance', 'High object count: ' + totalChildren + ' children in game', 'warning'); } // Check error log size if (self.errorLog.length > self.maxLogSize * 0.8) { self.logError('Performance', 'Error log is getting full: ' + self.errorLog.length + '/' + self.maxLogSize, 'info'); } }; // Check asset integrity self.checkAssets = function () { // Check if critical assets exist var criticalAssets = ['wallSegment', 'floorStrip', 'ceilingStrip', 'player']; for (var i = 0; i < criticalAssets.length; i++) { var asset = criticalAssets[i]; try { var testAsset = LK.getAsset(asset, {}); if (!testAsset) { self.logError('Assets', 'Critical asset missing: ' + asset, 'error'); } } catch (e) { self.logError('Assets', 'Error loading asset ' + asset + ': ' + e.message, 'error'); } } }; // Check procedural generation state self.checkProcGen = function () { if (!procGen) { self.logError('ProcGen', 'Procedural generator is null or undefined', 'error'); return false; } // Check if chunks are being generated if (procGen.generatedChunks) { var chunkCount = Object.keys(procGen.generatedChunks).length; if (chunkCount === 0) { self.logError('ProcGen', 'No chunks have been generated', 'warning'); } else if (chunkCount > 100) { self.logError('ProcGen', 'Large number of chunks generated: ' + chunkCount, 'info'); } } return true; }; // Run comprehensive error check self.runFullCheck = function () { var startTime = Date.now(); var checksRun = 0; var errorsBefore = self.errorLog.length; try { if (self.checkPlayerState()) checksRun++; if (self.checkWorldGrid()) checksRun++; if (self.checkRenderers()) checksRun++; if (self.checkProcGen()) checksRun++; self.checkPerformance(); checksRun++; self.checkAssets(); checksRun++; } catch (e) { self.logError('ErrorChecker', 'Exception during error check: ' + e.message, 'error'); } var endTime = Date.now(); var newErrors = self.errorLog.length - errorsBefore; if (newErrors > 0) { self.logError('ErrorChecker', 'Found ' + newErrors + ' new issues in ' + checksRun + ' checks (' + (endTime - startTime) + 'ms)', 'info'); } }; // Get error summary self.getErrorSummary = function () { var summary = { total: self.errorLog.length, errors: 0, warnings: 0, info: 0, recent: 0 }; var recentTime = Date.now() - 5000; // Last 5 seconds for (var i = 0; i < self.errorLog.length; i++) { var error = self.errorLog[i]; if (error.severity === 'error') summary.errors++;else if (error.severity === 'warning') summary.warnings++;else summary.info++; if (error.timestamp > recentTime) summary.recent++; } return summary; }; // Clear error log self.clearLog = function () { self.errorLog = []; self.logError('ErrorChecker', 'Error log cleared', 'info'); }; // Check if system is healthy self.isSystemHealthy = function () { var summary = self.getErrorSummary(); return summary.errors === 0 && summary.warnings < 5; }; // Inspect game state for common issues self.inspectGameState = function () { var issues = []; // Check global variables existence if (typeof player === 'undefined' || !player) { issues.push('Player object is null or undefined'); } if (typeof worldGrid === 'undefined' || !worldGrid) { issues.push('WorldGrid object is null or undefined'); } if (typeof procGen === 'undefined' || !procGen) { issues.push('ProcGen object is null or undefined'); } if (typeof game === 'undefined' || !game) { issues.push('Game object is null or undefined'); } // Check player state if exists if (player) { if (isNaN(player.x) || isNaN(player.y) || isNaN(player.angle)) { issues.push('Player has NaN coordinates: x=' + player.x + ', y=' + player.y + ', angle=' + player.angle); } if (player.x < 0 || player.y < 0) { issues.push('Player has negative coordinates: x=' + player.x + ', y=' + player.y); } if (worldGrid && worldGrid.width && worldGrid.height && worldGrid.cellSize && (player.x > worldGrid.width * worldGrid.cellSize || player.y > worldGrid.height * worldGrid.cellSize)) { issues.push('Player is outside world bounds'); } } // Check world grid integrity if (worldGrid && worldGrid.walls && worldGrid.width && worldGrid.height) { var wallErrors = 0; var nullColumns = 0; for (var x = 0; x < Math.min(worldGrid.width, 20); x++) { if (!worldGrid.walls[x]) { nullColumns++; continue; } for (var y = 0; y < Math.min(worldGrid.height, 20); y++) { if (typeof worldGrid.walls[x][y] !== 'boolean') { wallErrors++; } } } if (nullColumns > 0) { issues.push('Found ' + nullColumns + ' null wall columns'); } if (wallErrors > 0) { issues.push('Found ' + wallErrors + ' invalid wall data points'); } } // Check renderer integrity if (typeof wallRenderer !== 'undefined' && wallRenderer) { if (!wallRenderer.wallStrips || wallRenderer.wallStrips.length === 0) { issues.push('Wall renderer has no strips initialized'); } } else { issues.push('Wall renderer is not defined'); } if (typeof raycastRenderer !== 'undefined' && raycastRenderer) { if (!raycastRenderer.wallColumns || raycastRenderer.wallColumns.length === 0) { issues.push('Raycast renderer has no columns initialized'); } } else { issues.push('Raycast renderer is not defined'); } // Check procedural generation if (typeof procGen !== 'undefined' && procGen && procGen.generatedChunks) { var chunkCount = Object.keys(procGen.generatedChunks).length; if (chunkCount === 0) { issues.push('No procedural chunks have been generated'); } else if (chunkCount > 200) { issues.push('Excessive chunk count: ' + chunkCount + ' (potential memory leak)'); } } else { issues.push('ProcGen is not defined or has no generatedChunks'); } // Check game object tree if (typeof game !== 'undefined' && game && game.children) { var childCount = game.children.length; if (childCount > 2000) { issues.push('High game object count: ' + childCount + ' children'); } } else { issues.push('Game object is not defined or has no children'); } // Performance checks if (typeof fpsDisplay !== 'undefined' && fpsDisplay < 15) { issues.push('Critical FPS: ' + fpsDisplay); } return issues; }; // Inspect memory usage and object counts self.inspectMemoryUsage = function () { var memoryInfo = { gameChildren: 0, wallStrips: 0, raycastColumns: 0, generatedChunks: 0, errorLogSize: self.errorLog.length, maxLogSize: self.maxLogSize }; if (typeof game !== 'undefined' && game && game.children) { memoryInfo.gameChildren = game.children.length; } if (typeof wallRenderer !== 'undefined' && wallRenderer && wallRenderer.wallStrips) { memoryInfo.wallStrips = wallRenderer.wallStrips.length; } if (typeof raycastRenderer !== 'undefined' && raycastRenderer && raycastRenderer.wallColumns) { memoryInfo.raycastColumns = raycastRenderer.wallColumns.length; } if (typeof procGen !== 'undefined' && procGen && procGen.generatedChunks) { memoryInfo.generatedChunks = Object.keys(procGen.generatedChunks).length; } // Check for memory issues var issues = []; if (memoryInfo.gameChildren > 1500) { issues.push('High game object count: ' + memoryInfo.gameChildren); } if (memoryInfo.generatedChunks > 150) { issues.push('High chunk count: ' + memoryInfo.generatedChunks); } if (memoryInfo.errorLogSize > memoryInfo.maxLogSize * 0.9) { issues.push('Error log nearly full: ' + memoryInfo.errorLogSize + '/' + memoryInfo.maxLogSize); } return { info: memoryInfo, issues: issues }; }; // Inspect coordinate system consistency self.inspectCoordinateSystem = function () { var issues = []; if (typeof player === 'undefined' || !player || typeof worldGrid === 'undefined' || !worldGrid) { issues.push('Cannot inspect coordinates: missing player or worldGrid'); return issues; } // Check if player coordinates make sense if (worldGrid.cellSize && worldGrid.width && worldGrid.height) { var playerGridX = Math.floor(player.x / worldGrid.cellSize); var playerGridY = Math.floor(player.y / worldGrid.cellSize); if (playerGridX < 0 || playerGridX >= worldGrid.width) { issues.push('Player grid X coordinate out of bounds: ' + playerGridX); } if (playerGridY < 0 || playerGridY >= worldGrid.height) { issues.push('Player grid Y coordinate out of bounds: ' + playerGridY); } } else { issues.push('WorldGrid missing required properties (cellSize, width, height)'); } // Check if player is in a wall if (worldGrid.hasWallAt && worldGrid.hasWallAt(player.x, player.y)) { issues.push('Player is inside a wall at (' + Math.floor(player.x) + ', ' + Math.floor(player.y) + ')'); } // Check coordinate conversion consistency if (worldGrid.screenToWorld && worldGrid.worldToScreen) { var testWorldCoord = { x: 1000, y: 1000 }; var screenCoord = worldGrid.worldToScreen(testWorldCoord.x, testWorldCoord.y); var backToWorld = worldGrid.screenToWorld(screenCoord.x, screenCoord.y); if (Math.abs(backToWorld.x - testWorldCoord.x) > 1 || Math.abs(backToWorld.y - testWorldCoord.y) > 1) { issues.push('Coordinate conversion inconsistency detected'); } } return issues; }; // Run comprehensive inspection self.runInspection = function () { var startTime = Date.now(); var allIssues = []; try { // Inspect game state var gameStateIssues = self.inspectGameState(); allIssues = allIssues.concat(gameStateIssues); // Inspect memory usage var memoryResult = self.inspectMemoryUsage(); allIssues = allIssues.concat(memoryResult.issues); // Inspect coordinates var coordIssues = self.inspectCoordinateSystem(); allIssues = allIssues.concat(coordIssues); // Log all found issues for (var i = 0; i < allIssues.length; i++) { var severity = 'warning'; if (allIssues[i].indexOf('Critical') !== -1 || allIssues[i].indexOf('NaN') !== -1) { severity = 'error'; } else if (allIssues[i].indexOf('High') !== -1 || allIssues[i].indexOf('Excessive') !== -1) { severity = 'warning'; } else { severity = 'info'; } self.logError('Inspection', allIssues[i], severity); } } catch (e) { self.logError('Inspection', 'Error during inspection: ' + e.message, 'error'); } var endTime = Date.now(); var issueCount = allIssues.length; if (issueCount > 0) { self.logError('Inspection', 'Found ' + issueCount + ' issues during inspection (' + (endTime - startTime) + 'ms)', 'info'); } else { self.logError('Inspection', 'No issues found during inspection (' + (endTime - startTime) + 'ms)', 'info'); } return allIssues; }; // Get inspection summary for display self.getInspectionSummary = function () { var issues = self.runInspection(); var summary = { totalIssues: issues.length, criticalIssues: 0, warningIssues: 0, infoIssues: 0, topIssues: [] }; // Categorize issues for (var i = 0; i < issues.length; i++) { var issue = issues[i]; if (issue.indexOf('Critical') !== -1 || issue.indexOf('NaN') !== -1) { summary.criticalIssues++; } else if (issue.indexOf('High') !== -1 || issue.indexOf('Excessive') !== -1) { summary.warningIssues++; } else { summary.infoIssues++; } // Keep top 5 issues for display if (summary.topIssues.length < 5) { summary.topIssues.push(issue); } } return summary; }; // Update method called every frame self.update = function () { // Run checks at specified interval if (LK.ticks % self.checkInterval === 0) { self.runFullCheck(); } // Run inspection every 180 frames (3 seconds at 60fps) if (LK.ticks % 180 === 0) { self.runInspection(); } // Emergency checks every frame for critical issues if (player && (isNaN(player.x) || isNaN(player.y))) { self.logError('Critical', 'Player position is NaN - emergency check', 'error'); } }; 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 Minimap = Container.expand(function () { var self = Container.call(this); self.mapSize = 400; // Size of minimap in pixels self.gridCellSize = 8; // Size of each grid cell in minimap pixels self.viewRange = 25; // Number of grid cells to show in each direction // Create background var background = self.addChild(LK.getAsset('untexturedArea', { anchorX: 0.5, anchorY: 0.5, width: self.mapSize, height: self.mapSize, alpha: 0.8 })); background.tint = 0x222222; // Create border var border = self.addChild(LK.getAsset('untexturedArea', { anchorX: 0.5, anchorY: 0.5, width: self.mapSize + 4, height: self.mapSize + 4, alpha: 1.0 })); border.tint = 0xFFFFFF; // Grid cells container self.gridContainer = self.addChild(new Container()); // Player indicator self.playerIndicator = self.addChild(LK.getAsset('player', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.8 })); self.playerIndicator.tint = 0xFF0000; // Coordinate grid lines self.gridLines = []; for (var i = 0; i <= self.viewRange * 2; i++) { // Vertical lines var vLine = self.addChild(LK.getAsset('untexturedArea', { anchorX: 0.5, anchorY: 0.5, width: 1, height: self.mapSize, alpha: 0.3 })); vLine.tint = 0x666666; self.gridLines.push(vLine); // Horizontal lines var hLine = self.addChild(LK.getAsset('untexturedArea', { anchorX: 0.5, anchorY: 0.5, width: self.mapSize, height: 1, alpha: 0.3 })); hLine.tint = 0x666666; self.gridLines.push(hLine); } // Grid cells for walls/floors self.gridCells = []; // Initialize grid cells for (var x = 0; x < self.viewRange * 2 + 1; x++) { self.gridCells[x] = []; for (var y = 0; y < self.viewRange * 2 + 1; y++) { var cell = self.gridContainer.addChild(LK.getAsset('untexturedArea', { anchorX: 0.5, anchorY: 0.5, width: self.gridCellSize - 1, height: self.gridCellSize - 1 })); cell.visible = false; self.gridCells[x][y] = cell; } } // Update minimap based on player position self.update = function (player) { if (!player || !worldGrid) return; var playerGridX = Math.floor(player.x / worldGrid.cellSize); var playerGridY = Math.floor(player.y / worldGrid.cellSize); // Position player indicator at center self.playerIndicator.x = 0; self.playerIndicator.y = 0; self.playerIndicator.rotation = player.angle + Math.PI / 2; // Adjust rotation // Update grid lines positions var lineIndex = 0; for (var i = 0; i <= self.viewRange * 2; i++) { var offset = (i - self.viewRange) * self.gridCellSize; // Vertical lines if (lineIndex < self.gridLines.length) { self.gridLines[lineIndex].x = offset; self.gridLines[lineIndex].y = 0; lineIndex++; } // Horizontal lines if (lineIndex < self.gridLines.length) { self.gridLines[lineIndex].x = 0; self.gridLines[lineIndex].y = offset; lineIndex++; } } // Update grid cells for (var x = 0; x < self.viewRange * 2 + 1; x++) { for (var y = 0; y < self.viewRange * 2 + 1; y++) { var worldX = playerGridX - self.viewRange + x; var worldY = playerGridY - self.viewRange + y; var cell = self.gridCells[x][y]; var screenX = (x - self.viewRange) * self.gridCellSize; var screenY = (y - self.viewRange) * self.gridCellSize; cell.x = screenX; cell.y = screenY; // Check if position is within world bounds if (worldX >= 0 && worldX < worldGrid.width && worldY >= 0 && worldY < worldGrid.height) { cell.visible = true; // Check if this position matches the door location var doorGridX = Math.floor(door.x / worldGrid.cellSize); var doorGridY = Math.floor(door.y / worldGrid.cellSize); if (worldX === doorGridX && worldY === doorGridY) { // Door position cell.tint = 0xFFFFFF; // White for door cell.alpha = 1.0; } else if (worldGrid.walls[worldX][worldY]) { // Wall cell.tint = 0x46f7a3; // Green for walls cell.alpha = 1.0; } else { // Floor cell.tint = 0x74391f; // Brown for floors cell.alpha = 0.6; } } else { // Outside world bounds cell.visible = false; } } } }; 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 alternative routes between rooms for navigation variety self.createAlternativeRoutes(rooms, offsetX, offsetY); // 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 with varied connection patterns 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, angle: Math.atan2(dy, dx) // Add angle for directional variety }); } } // Sort by distance roomDistances.sort(function (a, b) { return a.distance - b.distance; }); // Create varied connection patterns based on room position and existing connections var connectionPattern = Math.floor(Math.random() * 4); var connectionsToMake = 0; var connectionIndices = []; if (connectionPattern === 0) { // Star pattern - connect to nearest + 2 distant rooms for hub effect connectionsToMake = Math.min(3, roomDistances.length); connectionIndices = [0]; // Always connect to nearest if (roomDistances.length > 2) { connectionIndices.push(Math.floor(roomDistances.length * 0.6)); // Mid-distance connectionIndices.push(roomDistances.length - 1); // Farthest } } else if (connectionPattern === 1) { // Linear chain pattern - connect to 1-2 rooms in sequence connectionsToMake = Math.min(2, roomDistances.length); for (var i = 0; i < connectionsToMake; i++) { connectionIndices.push(i); } } else if (connectionPattern === 2) { // Angular spread pattern - connect to rooms in different directions connectionsToMake = Math.min(4, roomDistances.length); // Select rooms with diverse angles var usedAngles = []; for (var i = 0; i < roomDistances.length && connectionIndices.length < connectionsToMake; i++) { var roomAngle = roomDistances[i].angle; var validAngle = true; // Check if this angle is sufficiently different from used angles for (var j = 0; j < usedAngles.length; j++) { var angleDiff = Math.abs(roomAngle - usedAngles[j]); if (angleDiff < Math.PI / 3) { // Less than 60 degrees apart validAngle = false; break; } } if (validAngle) { connectionIndices.push(i); usedAngles.push(roomAngle); } } } else { // Random scattered pattern - connect to random selection connectionsToMake = Math.min(2 + Math.floor(Math.random() * 3), roomDistances.length); var availableIndices = []; for (var i = 0; i < roomDistances.length; i++) { availableIndices.push(i); } // Shuffle and pick random connections for (var i = availableIndices.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = availableIndices[i]; availableIndices[i] = availableIndices[j]; availableIndices[j] = temp; } for (var i = 0; i < connectionsToMake; i++) { connectionIndices.push(availableIndices[i]); } } // Create connections with varied corridor types for (var i = 0; i < connectionIndices.length; i++) { var targetRoom = roomDistances[connectionIndices[i]].room; var corridorType = Math.floor(Math.random() * 3); if (corridorType === 0) { // Straight corridor self.createCorridor(newRoom.centerX, newRoom.centerY, targetRoom.centerX, targetRoom.centerY); } else if (corridorType === 1) { // Curved corridor self.createCurvedCorridor(newRoom.centerX, newRoom.centerY, targetRoom.centerX, targetRoom.centerY); } else { // Multi-segment corridor with intermediate waypoint var midX = Math.floor((newRoom.centerX + targetRoom.centerX) / 2) + Math.floor(Math.random() * 6) - 3; var midY = Math.floor((newRoom.centerY + targetRoom.centerY) / 2) + Math.floor(Math.random() * 6) - 3; self.createCorridor(newRoom.centerX, newRoom.centerY, midX, midY); self.createCorridor(midX, midY, targetRoom.centerX, targetRoom.centerY); } } // Always ensure at least one connection exists if (connectionIndices.length === 0 && roomDistances.length > 0) { self.createCorridor(newRoom.centerX, newRoom.centerY, roomDistances[0].room.centerX, roomDistances[0].room.centerY); } // Add bonus cross-connections between existing rooms (15% chance) if (Math.random() < 0.15 && existingRooms.length >= 3) { var room1Index = Math.floor(Math.random() * existingRooms.length); var room2Index = Math.floor(Math.random() * existingRooms.length); if (room1Index !== room2Index && existingRooms[room1Index] !== newRoom && existingRooms[room2Index] !== newRoom) { self.createCorridor(existingRooms[room1Index].centerX, existingRooms[room1Index].centerY, existingRooms[room2Index].centerX, existingRooms[room2Index].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 alternative routes between existing rooms for better navigation variety self.createAlternativeRoutes = function (rooms, offsetX, offsetY) { if (rooms.length < 3) return; // Need at least 3 rooms for alternative routes // For each pair of rooms, occasionally create alternative connection paths for (var i = 0; i < rooms.length - 1; i++) { for (var j = i + 1; j < rooms.length; j++) { var room1 = rooms[i]; var room2 = rooms[j]; var dx = Math.abs(room1.centerX - room2.centerX); var dy = Math.abs(room1.centerY - room2.centerY); var distance = Math.sqrt(dx * dx + dy * dy); // Only create alternative routes for rooms that are moderately close (not too near, not too far) if (distance > 3 && distance < 8 && Math.random() < 0.25) { // Create alternative route with different path var routeType = Math.floor(Math.random() * 3); if (routeType === 0) { // Indirect L-shaped route through intermediate point var intermediateX = room1.centerX + Math.floor(Math.random() * 4) - 2; var intermediateY = room2.centerY + Math.floor(Math.random() * 4) - 2; self.createCorridor(room1.centerX, room1.centerY, intermediateX, intermediateY); self.createCorridor(intermediateX, intermediateY, room2.centerX, room2.centerY); } else if (routeType === 1) { // Curved bypass route var bypassX = Math.floor((room1.centerX + room2.centerX) / 2) + Math.floor(Math.random() * 8) - 4; var bypassY = Math.floor((room1.centerY + room2.centerY) / 2) + Math.floor(Math.random() * 8) - 4; self.createSmoothPath(room1.centerX, room1.centerY, bypassX, bypassY); self.createSmoothPath(bypassX, bypassY, room2.centerX, room2.centerY); } else { // Multi-segment zigzag route var segments = 2 + Math.floor(Math.random() * 2); // 2-3 segments var currentX = room1.centerX; var currentY = room1.centerY; var stepX = (room2.centerX - room1.centerX) / segments; var stepY = (room2.centerY - room1.centerY) / segments; for (var s = 1; s <= segments; s++) { var nextX = Math.floor(room1.centerX + stepX * s + Math.random() * 4 - 2); var nextY = Math.floor(room1.centerY + stepY * s + Math.random() * 4 - 2); self.createCorridor(currentX, currentY, nextX, nextY); currentX = nextX; currentY = nextY; } } } } } }; // 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(); // Pre-load entire map by generating all possible chunks procGen.generateEntireMap = function () { var chunkSize = this.chunkSize; var maxChunksX = Math.ceil(worldGrid.width / chunkSize); var maxChunksY = Math.ceil(worldGrid.height / chunkSize); console.log('Pre-loading entire map: ' + maxChunksX + 'x' + maxChunksY + ' chunks'); // Generate all chunks across the entire world grid for (var chunkX = 0; chunkX < maxChunksX; chunkX++) { for (var chunkY = 0; chunkY < maxChunksY; chunkY++) { this.generateChunk(chunkX, chunkY); } } console.log('Map pre-loading complete. Generated ' + Object.keys(this.generatedChunks).length + ' chunks'); }; // Pre-load the entire map at initialization procGen.generateEntireMap(); // 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 error checker instance var errorChecker = new ErrorChecker(); game.addChild(errorChecker); // Create error status display var errorStatusText = new Text2('System OK', { size: 50, fill: 0x00FF00 }); errorStatusText.anchor.set(0, 0); errorStatusText.x = 120; // Avoid top-left 100x100 area errorStatusText.y = 520; LK.gui.addChild(errorStatusText); // Create detailed error display (hidden by default) var errorDetailsText = new Text2('', { size: 40, fill: 0xFFFFFF }); errorDetailsText.anchor.set(0, 0); errorDetailsText.x = 120; errorDetailsText.y = 600; errorDetailsText.visible = false; LK.gui.addChild(errorDetailsText); // 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); // Create minimap and position it more to the left var minimap = new Minimap(); minimap.x = 800; // Move to left side (was 1366 center) minimap.y = 1024; // Center vertically (2048/2) LK.gui.addChild(minimap); // Create door instance and position it in world var door = new Door(); // Find a good position for the door - place it in an open area near spawn var doorX = (worldGrid.width / 2 + 3) * worldGrid.cellSize; var doorY = (worldGrid.height / 2 + 2) * worldGrid.cellSize; door.x = doorX; door.y = doorY; // Keep door at exact world level for proper collision and rendering game.addChild(door); // Ensure a room is always generated around the door position var doorGridX = Math.floor(doorX / worldGrid.cellSize); var doorGridY = Math.floor(doorY / worldGrid.cellSize); // 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 }; // Create a guaranteed 5x5 room around the door for better connectivity for (var dx = -2; dx <= 2; dx++) { for (var dy = -2; dy <= 2; dy++) { var roomX = doorGridX + dx; var roomY = doorGridY + dy; if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) { worldGrid.walls[roomX][roomY] = false; } } } // Create an adjacent connected room to the door room var connectedRoomX = doorGridX + 4; // Place connected room 4 cells to the right var connectedRoomY = doorGridY; // Create a guaranteed 4x4 connected room for (var dx = 0; dx < 4; dx++) { for (var dy = -1; dy <= 2; dy++) { var roomX = connectedRoomX + dx; var roomY = connectedRoomY + dy; if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) { worldGrid.walls[roomX][roomY] = false; } } } // Create a wide corridor connecting the door room to the adjacent room for (var x = doorGridX + 2; x < connectedRoomX; x++) { for (var y = doorGridY - 1; y <= doorGridY + 1; y++) { if (x >= 0 && x < worldGrid.width && y >= 0 && y < worldGrid.height) { worldGrid.walls[x][y] = false; } } } // Create corridors leading to the door room from spawn area to ensure connectivity var spawnGridX = Math.floor(worldGrid.width / 2); var spawnGridY = Math.floor(worldGrid.height / 2); // Create horizontal corridor var minX = Math.min(spawnGridX, doorGridX); var maxX = Math.max(spawnGridX, doorGridX); for (var x = minX; x <= maxX; x++) { if (x >= 0 && x < worldGrid.width && spawnGridY >= 0 && spawnGridY < worldGrid.height) { worldGrid.walls[x][spawnGridY] = false; } } // Create vertical corridor var minY = Math.min(spawnGridY, doorGridY); var maxY = Math.max(spawnGridY, doorGridY); for (var y = minY; y <= maxY; y++) { if (doorGridX >= 0 && doorGridX < worldGrid.width && y >= 0 && y < worldGrid.height) { worldGrid.walls[doorGridX][y] = false; } } // Also connect the adjacent room to the main corridor network // Create additional corridor from connected room toward spawn area for (var x = connectedRoomX; x < Math.min(connectedRoomX + 6, worldGrid.width - 1); x++) { if (x >= 0 && x < worldGrid.width && spawnGridY >= 0 && spawnGridY < worldGrid.height) { worldGrid.walls[x][spawnGridY] = false; } } // 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(); }; // Error status click handler for detailed view errorStatusText.down = function (x, y, obj) { if (errorChecker.errorLog.length > 0) { // Toggle error details visibility errorDetailsText.visible = !errorDetailsText.visible; // Show last few errors var lastErrors = []; for (var i = Math.max(0, errorChecker.errorLog.length - 5); i < errorChecker.errorLog.length; i++) { var error = errorChecker.errorLog[i]; var timeAgo = Math.floor((Date.now() - error.timestamp) / 1000); lastErrors.push('[' + timeAgo + 's] ' + error.type + ': ' + error.message.substring(0, 50)); } errorDetailsText.setText(lastErrors.join('\n')); } else { // Run immediate inspection when no errors showing var inspectionSummary = errorChecker.getInspectionSummary(); if (inspectionSummary.totalIssues > 0) { errorDetailsText.visible = true; var summaryText = 'INSPECTION RESULTS:\n'; summaryText += 'Critical: ' + inspectionSummary.criticalIssues + '\n'; summaryText += 'Warnings: ' + inspectionSummary.warningIssues + '\n'; summaryText += 'Info: ' + inspectionSummary.infoIssues + '\n\n'; summaryText += 'TOP ISSUES:\n'; for (var i = 0; i < inspectionSummary.topIssues.length; i++) { summaryText += '• ' + inspectionSummary.topIssues[i].substring(0, 40) + '\n'; } errorDetailsText.setText(summaryText); } else { // Clear log if no errors errorChecker.clearLog(); errorDetailsText.visible = false; } } }; 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; // Check for isolated areas near player and create passages proactively (map is pre-loaded) if (LK.ticks % 60 === 0) { // Check every 60 frames for performance since map is pre-generated worldGrid.checkPlayerProximityForPassages(player.x, player.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; } // Update minimap minimap.update(player); // Update error checker status var errorSummary = errorChecker.getErrorSummary(); var statusText = 'System OK'; var statusColor = 0x00FF00; if (errorSummary.errors > 0) { statusText = 'ERRORS: ' + errorSummary.errors; statusColor = 0xFF0000; } else if (errorSummary.warnings > 0) { statusText = 'WARNINGS: ' + errorSummary.warnings; statusColor = 0xFFFF00; } else if (errorSummary.recent > 0) { statusText = 'RECENT: ' + errorSummary.recent; statusColor = 0x00FFFF; } else { // Show inspection status when no errors if (LK.ticks % 300 === 0) { // Every 5 seconds var inspectionSummary = errorChecker.getInspectionSummary(); if (inspectionSummary.totalIssues > 0) { statusText = 'INSPECT: ' + inspectionSummary.totalIssues; statusColor = inspectionSummary.criticalIssues > 0 ? 0xFF6600 : 0x6699FF; } } } errorStatusText.setText(statusText); errorStatusText.fill = statusColor; // Show error details on touch (every 120 frames to avoid spam) if (LK.ticks % 120 === 0 && errorSummary.total > 0) { var recentErrors = []; var currentTime = Date.now(); for (var i = errorChecker.errorLog.length - 1; i >= 0 && recentErrors.length < 3; i--) { var error = errorChecker.errorLog[i]; if (currentTime - error.timestamp < 10000) { // Last 10 seconds recentErrors.push(error.type + ': ' + error.message.substring(0, 40)); } } if (recentErrors.length > 0) { errorDetailsText.setText(recentErrors.join('\n')); errorDetailsText.visible = true; } else { errorDetailsText.visible = false; } } else if (errorSummary.total === 0) { errorDetailsText.visible = false; } }; // 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;
// Fix door positioning - ensure it's at proper world coordinates
var targetX = (worldGrid.width / 2 + 3) * worldGrid.cellSize;
var targetY = (worldGrid.height / 2 + 2) * worldGrid.cellSize;
// 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 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 - position at minimap center
var levelText = new Text2('NIVEL 1 COMPLETADO', {
size: 100,
fill: 0x00FF00
});
levelText.anchor.set(0.5, 0.5);
levelText.x = 800; // Match minimap X position (center of minimap area)
levelText.y = -200; // Start above screen
levelText.alpha = 0;
LK.gui.addChild(levelText);
// Animate black screen fade in
tween(blackScreen, {
alpha: 0.8
}, {
duration: 800,
onFinish: function onFinish() {
// Animate level text drop down and fade in - center at minimap position
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
levelText.setText('NIVEL 2');
levelText.fill = 0xFFFFFF;
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 ErrorChecker = Container.expand(function () {
var self = Container.call(this);
self.errorLog = [];
self.maxLogSize = 50;
self.checkInterval = 60; // Check every 60 frames
self.lastCheckTime = 0;
// Add error to log with timestamp
self.logError = function (type, message, severity) {
var error = {
type: type,
message: message,
severity: severity || 'warning',
// 'error', 'warning', 'info'
timestamp: Date.now(),
frame: LK.ticks
};
self.errorLog.push(error);
// Keep log size manageable
if (self.errorLog.length > self.maxLogSize) {
self.errorLog.shift();
}
// Log to console based on severity
if (severity === 'error') {
console.error('[ERROR]', type + ':', message);
} else if (severity === 'warning') {
console.warn('[WARNING]', type + ':', message);
} else {
console.log('[INFO]', type + ':', message);
}
};
// Check player state for errors
self.checkPlayerState = function () {
if (!player) {
self.logError('Player', 'Player object is null or undefined', 'error');
return false;
}
// Check position bounds
if (player.x < 0 || player.x >= worldGrid.width * worldGrid.cellSize || player.y < 0 || player.y >= worldGrid.height * worldGrid.cellSize) {
self.logError('Player', 'Player position out of world bounds: (' + Math.floor(player.x) + ', ' + Math.floor(player.y) + ')', 'warning');
}
// Check for NaN values
if (isNaN(player.x) || isNaN(player.y) || isNaN(player.angle)) {
self.logError('Player', 'Player has NaN values - x:' + player.x + ' y:' + player.y + ' angle:' + player.angle, 'error');
return false;
}
// Check if player is stuck in walls
if (worldGrid && worldGrid.checkCollision && worldGrid.checkCollision(player.x, player.y)) {
self.logError('Player', 'Player is stuck inside wall at (' + Math.floor(player.x) + ', ' + Math.floor(player.y) + ')', 'warning');
}
return true;
};
// Check world grid integrity
self.checkWorldGrid = function () {
if (!worldGrid) {
self.logError('WorldGrid', 'WorldGrid object is null or undefined', 'error');
return false;
}
if (!worldGrid.walls) {
self.logError('WorldGrid', 'WorldGrid walls array is null or undefined', 'error');
return false;
}
// Check grid dimensions
if (worldGrid.width <= 0 || worldGrid.height <= 0) {
self.logError('WorldGrid', 'Invalid grid dimensions: ' + worldGrid.width + 'x' + worldGrid.height, 'error');
return false;
}
// Check for corrupted wall data
var wallErrors = 0;
if (worldGrid && worldGrid.walls && worldGrid.width && worldGrid.height) {
for (var x = 0; x < Math.min(worldGrid.width, 10); x++) {
if (!worldGrid.walls[x]) {
wallErrors++;
continue;
}
for (var y = 0; y < Math.min(worldGrid.height, 10); y++) {
if (typeof worldGrid.walls[x][y] !== 'boolean') {
wallErrors++;
}
}
}
}
if (wallErrors > 0) {
self.logError('WorldGrid', 'Found ' + wallErrors + ' corrupted wall data points', 'warning');
}
return true;
};
// Check renderer states
self.checkRenderers = function () {
// Check wall renderer
if (typeof wallRenderer === 'undefined' || !wallRenderer) {
self.logError('Renderer', 'Wall renderer is null or undefined', 'error');
return false;
}
if (wallRenderer.wallStrips && wallRenderer.wallStrips.length === 0) {
self.logError('Renderer', 'Wall renderer has no wall strips initialized', 'warning');
}
// Check raycast renderer
if (typeof raycastRenderer === 'undefined' || !raycastRenderer) {
self.logError('Renderer', 'Raycast renderer is null or undefined', 'error');
return false;
}
if (raycastRenderer.wallColumns && raycastRenderer.wallColumns.length === 0) {
self.logError('Renderer', 'Raycast renderer has no columns initialized', 'warning');
}
return true;
};
// Check memory usage and performance
self.checkPerformance = function () {
// Check FPS
if (typeof fpsDisplay !== 'undefined' && fpsDisplay < 20) {
self.logError('Performance', 'Low FPS detected: ' + fpsDisplay, 'warning');
}
// Check object counts
var totalChildren = 0;
if (game && game.children) {
totalChildren = game.children.length;
}
if (totalChildren > 1000) {
self.logError('Performance', 'High object count: ' + totalChildren + ' children in game', 'warning');
}
// Check error log size
if (self.errorLog.length > self.maxLogSize * 0.8) {
self.logError('Performance', 'Error log is getting full: ' + self.errorLog.length + '/' + self.maxLogSize, 'info');
}
};
// Check asset integrity
self.checkAssets = function () {
// Check if critical assets exist
var criticalAssets = ['wallSegment', 'floorStrip', 'ceilingStrip', 'player'];
for (var i = 0; i < criticalAssets.length; i++) {
var asset = criticalAssets[i];
try {
var testAsset = LK.getAsset(asset, {});
if (!testAsset) {
self.logError('Assets', 'Critical asset missing: ' + asset, 'error');
}
} catch (e) {
self.logError('Assets', 'Error loading asset ' + asset + ': ' + e.message, 'error');
}
}
};
// Check procedural generation state
self.checkProcGen = function () {
if (!procGen) {
self.logError('ProcGen', 'Procedural generator is null or undefined', 'error');
return false;
}
// Check if chunks are being generated
if (procGen.generatedChunks) {
var chunkCount = Object.keys(procGen.generatedChunks).length;
if (chunkCount === 0) {
self.logError('ProcGen', 'No chunks have been generated', 'warning');
} else if (chunkCount > 100) {
self.logError('ProcGen', 'Large number of chunks generated: ' + chunkCount, 'info');
}
}
return true;
};
// Run comprehensive error check
self.runFullCheck = function () {
var startTime = Date.now();
var checksRun = 0;
var errorsBefore = self.errorLog.length;
try {
if (self.checkPlayerState()) checksRun++;
if (self.checkWorldGrid()) checksRun++;
if (self.checkRenderers()) checksRun++;
if (self.checkProcGen()) checksRun++;
self.checkPerformance();
checksRun++;
self.checkAssets();
checksRun++;
} catch (e) {
self.logError('ErrorChecker', 'Exception during error check: ' + e.message, 'error');
}
var endTime = Date.now();
var newErrors = self.errorLog.length - errorsBefore;
if (newErrors > 0) {
self.logError('ErrorChecker', 'Found ' + newErrors + ' new issues in ' + checksRun + ' checks (' + (endTime - startTime) + 'ms)', 'info');
}
};
// Get error summary
self.getErrorSummary = function () {
var summary = {
total: self.errorLog.length,
errors: 0,
warnings: 0,
info: 0,
recent: 0
};
var recentTime = Date.now() - 5000; // Last 5 seconds
for (var i = 0; i < self.errorLog.length; i++) {
var error = self.errorLog[i];
if (error.severity === 'error') summary.errors++;else if (error.severity === 'warning') summary.warnings++;else summary.info++;
if (error.timestamp > recentTime) summary.recent++;
}
return summary;
};
// Clear error log
self.clearLog = function () {
self.errorLog = [];
self.logError('ErrorChecker', 'Error log cleared', 'info');
};
// Check if system is healthy
self.isSystemHealthy = function () {
var summary = self.getErrorSummary();
return summary.errors === 0 && summary.warnings < 5;
};
// Inspect game state for common issues
self.inspectGameState = function () {
var issues = [];
// Check global variables existence
if (typeof player === 'undefined' || !player) {
issues.push('Player object is null or undefined');
}
if (typeof worldGrid === 'undefined' || !worldGrid) {
issues.push('WorldGrid object is null or undefined');
}
if (typeof procGen === 'undefined' || !procGen) {
issues.push('ProcGen object is null or undefined');
}
if (typeof game === 'undefined' || !game) {
issues.push('Game object is null or undefined');
}
// Check player state if exists
if (player) {
if (isNaN(player.x) || isNaN(player.y) || isNaN(player.angle)) {
issues.push('Player has NaN coordinates: x=' + player.x + ', y=' + player.y + ', angle=' + player.angle);
}
if (player.x < 0 || player.y < 0) {
issues.push('Player has negative coordinates: x=' + player.x + ', y=' + player.y);
}
if (worldGrid && worldGrid.width && worldGrid.height && worldGrid.cellSize && (player.x > worldGrid.width * worldGrid.cellSize || player.y > worldGrid.height * worldGrid.cellSize)) {
issues.push('Player is outside world bounds');
}
}
// Check world grid integrity
if (worldGrid && worldGrid.walls && worldGrid.width && worldGrid.height) {
var wallErrors = 0;
var nullColumns = 0;
for (var x = 0; x < Math.min(worldGrid.width, 20); x++) {
if (!worldGrid.walls[x]) {
nullColumns++;
continue;
}
for (var y = 0; y < Math.min(worldGrid.height, 20); y++) {
if (typeof worldGrid.walls[x][y] !== 'boolean') {
wallErrors++;
}
}
}
if (nullColumns > 0) {
issues.push('Found ' + nullColumns + ' null wall columns');
}
if (wallErrors > 0) {
issues.push('Found ' + wallErrors + ' invalid wall data points');
}
}
// Check renderer integrity
if (typeof wallRenderer !== 'undefined' && wallRenderer) {
if (!wallRenderer.wallStrips || wallRenderer.wallStrips.length === 0) {
issues.push('Wall renderer has no strips initialized');
}
} else {
issues.push('Wall renderer is not defined');
}
if (typeof raycastRenderer !== 'undefined' && raycastRenderer) {
if (!raycastRenderer.wallColumns || raycastRenderer.wallColumns.length === 0) {
issues.push('Raycast renderer has no columns initialized');
}
} else {
issues.push('Raycast renderer is not defined');
}
// Check procedural generation
if (typeof procGen !== 'undefined' && procGen && procGen.generatedChunks) {
var chunkCount = Object.keys(procGen.generatedChunks).length;
if (chunkCount === 0) {
issues.push('No procedural chunks have been generated');
} else if (chunkCount > 200) {
issues.push('Excessive chunk count: ' + chunkCount + ' (potential memory leak)');
}
} else {
issues.push('ProcGen is not defined or has no generatedChunks');
}
// Check game object tree
if (typeof game !== 'undefined' && game && game.children) {
var childCount = game.children.length;
if (childCount > 2000) {
issues.push('High game object count: ' + childCount + ' children');
}
} else {
issues.push('Game object is not defined or has no children');
}
// Performance checks
if (typeof fpsDisplay !== 'undefined' && fpsDisplay < 15) {
issues.push('Critical FPS: ' + fpsDisplay);
}
return issues;
};
// Inspect memory usage and object counts
self.inspectMemoryUsage = function () {
var memoryInfo = {
gameChildren: 0,
wallStrips: 0,
raycastColumns: 0,
generatedChunks: 0,
errorLogSize: self.errorLog.length,
maxLogSize: self.maxLogSize
};
if (typeof game !== 'undefined' && game && game.children) {
memoryInfo.gameChildren = game.children.length;
}
if (typeof wallRenderer !== 'undefined' && wallRenderer && wallRenderer.wallStrips) {
memoryInfo.wallStrips = wallRenderer.wallStrips.length;
}
if (typeof raycastRenderer !== 'undefined' && raycastRenderer && raycastRenderer.wallColumns) {
memoryInfo.raycastColumns = raycastRenderer.wallColumns.length;
}
if (typeof procGen !== 'undefined' && procGen && procGen.generatedChunks) {
memoryInfo.generatedChunks = Object.keys(procGen.generatedChunks).length;
}
// Check for memory issues
var issues = [];
if (memoryInfo.gameChildren > 1500) {
issues.push('High game object count: ' + memoryInfo.gameChildren);
}
if (memoryInfo.generatedChunks > 150) {
issues.push('High chunk count: ' + memoryInfo.generatedChunks);
}
if (memoryInfo.errorLogSize > memoryInfo.maxLogSize * 0.9) {
issues.push('Error log nearly full: ' + memoryInfo.errorLogSize + '/' + memoryInfo.maxLogSize);
}
return {
info: memoryInfo,
issues: issues
};
};
// Inspect coordinate system consistency
self.inspectCoordinateSystem = function () {
var issues = [];
if (typeof player === 'undefined' || !player || typeof worldGrid === 'undefined' || !worldGrid) {
issues.push('Cannot inspect coordinates: missing player or worldGrid');
return issues;
}
// Check if player coordinates make sense
if (worldGrid.cellSize && worldGrid.width && worldGrid.height) {
var playerGridX = Math.floor(player.x / worldGrid.cellSize);
var playerGridY = Math.floor(player.y / worldGrid.cellSize);
if (playerGridX < 0 || playerGridX >= worldGrid.width) {
issues.push('Player grid X coordinate out of bounds: ' + playerGridX);
}
if (playerGridY < 0 || playerGridY >= worldGrid.height) {
issues.push('Player grid Y coordinate out of bounds: ' + playerGridY);
}
} else {
issues.push('WorldGrid missing required properties (cellSize, width, height)');
}
// Check if player is in a wall
if (worldGrid.hasWallAt && worldGrid.hasWallAt(player.x, player.y)) {
issues.push('Player is inside a wall at (' + Math.floor(player.x) + ', ' + Math.floor(player.y) + ')');
}
// Check coordinate conversion consistency
if (worldGrid.screenToWorld && worldGrid.worldToScreen) {
var testWorldCoord = {
x: 1000,
y: 1000
};
var screenCoord = worldGrid.worldToScreen(testWorldCoord.x, testWorldCoord.y);
var backToWorld = worldGrid.screenToWorld(screenCoord.x, screenCoord.y);
if (Math.abs(backToWorld.x - testWorldCoord.x) > 1 || Math.abs(backToWorld.y - testWorldCoord.y) > 1) {
issues.push('Coordinate conversion inconsistency detected');
}
}
return issues;
};
// Run comprehensive inspection
self.runInspection = function () {
var startTime = Date.now();
var allIssues = [];
try {
// Inspect game state
var gameStateIssues = self.inspectGameState();
allIssues = allIssues.concat(gameStateIssues);
// Inspect memory usage
var memoryResult = self.inspectMemoryUsage();
allIssues = allIssues.concat(memoryResult.issues);
// Inspect coordinates
var coordIssues = self.inspectCoordinateSystem();
allIssues = allIssues.concat(coordIssues);
// Log all found issues
for (var i = 0; i < allIssues.length; i++) {
var severity = 'warning';
if (allIssues[i].indexOf('Critical') !== -1 || allIssues[i].indexOf('NaN') !== -1) {
severity = 'error';
} else if (allIssues[i].indexOf('High') !== -1 || allIssues[i].indexOf('Excessive') !== -1) {
severity = 'warning';
} else {
severity = 'info';
}
self.logError('Inspection', allIssues[i], severity);
}
} catch (e) {
self.logError('Inspection', 'Error during inspection: ' + e.message, 'error');
}
var endTime = Date.now();
var issueCount = allIssues.length;
if (issueCount > 0) {
self.logError('Inspection', 'Found ' + issueCount + ' issues during inspection (' + (endTime - startTime) + 'ms)', 'info');
} else {
self.logError('Inspection', 'No issues found during inspection (' + (endTime - startTime) + 'ms)', 'info');
}
return allIssues;
};
// Get inspection summary for display
self.getInspectionSummary = function () {
var issues = self.runInspection();
var summary = {
totalIssues: issues.length,
criticalIssues: 0,
warningIssues: 0,
infoIssues: 0,
topIssues: []
};
// Categorize issues
for (var i = 0; i < issues.length; i++) {
var issue = issues[i];
if (issue.indexOf('Critical') !== -1 || issue.indexOf('NaN') !== -1) {
summary.criticalIssues++;
} else if (issue.indexOf('High') !== -1 || issue.indexOf('Excessive') !== -1) {
summary.warningIssues++;
} else {
summary.infoIssues++;
}
// Keep top 5 issues for display
if (summary.topIssues.length < 5) {
summary.topIssues.push(issue);
}
}
return summary;
};
// Update method called every frame
self.update = function () {
// Run checks at specified interval
if (LK.ticks % self.checkInterval === 0) {
self.runFullCheck();
}
// Run inspection every 180 frames (3 seconds at 60fps)
if (LK.ticks % 180 === 0) {
self.runInspection();
}
// Emergency checks every frame for critical issues
if (player && (isNaN(player.x) || isNaN(player.y))) {
self.logError('Critical', 'Player position is NaN - emergency check', 'error');
}
};
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 Minimap = Container.expand(function () {
var self = Container.call(this);
self.mapSize = 400; // Size of minimap in pixels
self.gridCellSize = 8; // Size of each grid cell in minimap pixels
self.viewRange = 25; // Number of grid cells to show in each direction
// Create background
var background = self.addChild(LK.getAsset('untexturedArea', {
anchorX: 0.5,
anchorY: 0.5,
width: self.mapSize,
height: self.mapSize,
alpha: 0.8
}));
background.tint = 0x222222;
// Create border
var border = self.addChild(LK.getAsset('untexturedArea', {
anchorX: 0.5,
anchorY: 0.5,
width: self.mapSize + 4,
height: self.mapSize + 4,
alpha: 1.0
}));
border.tint = 0xFFFFFF;
// Grid cells container
self.gridContainer = self.addChild(new Container());
// Player indicator
self.playerIndicator = self.addChild(LK.getAsset('player', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
}));
self.playerIndicator.tint = 0xFF0000;
// Coordinate grid lines
self.gridLines = [];
for (var i = 0; i <= self.viewRange * 2; i++) {
// Vertical lines
var vLine = self.addChild(LK.getAsset('untexturedArea', {
anchorX: 0.5,
anchorY: 0.5,
width: 1,
height: self.mapSize,
alpha: 0.3
}));
vLine.tint = 0x666666;
self.gridLines.push(vLine);
// Horizontal lines
var hLine = self.addChild(LK.getAsset('untexturedArea', {
anchorX: 0.5,
anchorY: 0.5,
width: self.mapSize,
height: 1,
alpha: 0.3
}));
hLine.tint = 0x666666;
self.gridLines.push(hLine);
}
// Grid cells for walls/floors
self.gridCells = [];
// Initialize grid cells
for (var x = 0; x < self.viewRange * 2 + 1; x++) {
self.gridCells[x] = [];
for (var y = 0; y < self.viewRange * 2 + 1; y++) {
var cell = self.gridContainer.addChild(LK.getAsset('untexturedArea', {
anchorX: 0.5,
anchorY: 0.5,
width: self.gridCellSize - 1,
height: self.gridCellSize - 1
}));
cell.visible = false;
self.gridCells[x][y] = cell;
}
}
// Update minimap based on player position
self.update = function (player) {
if (!player || !worldGrid) return;
var playerGridX = Math.floor(player.x / worldGrid.cellSize);
var playerGridY = Math.floor(player.y / worldGrid.cellSize);
// Position player indicator at center
self.playerIndicator.x = 0;
self.playerIndicator.y = 0;
self.playerIndicator.rotation = player.angle + Math.PI / 2; // Adjust rotation
// Update grid lines positions
var lineIndex = 0;
for (var i = 0; i <= self.viewRange * 2; i++) {
var offset = (i - self.viewRange) * self.gridCellSize;
// Vertical lines
if (lineIndex < self.gridLines.length) {
self.gridLines[lineIndex].x = offset;
self.gridLines[lineIndex].y = 0;
lineIndex++;
}
// Horizontal lines
if (lineIndex < self.gridLines.length) {
self.gridLines[lineIndex].x = 0;
self.gridLines[lineIndex].y = offset;
lineIndex++;
}
}
// Update grid cells
for (var x = 0; x < self.viewRange * 2 + 1; x++) {
for (var y = 0; y < self.viewRange * 2 + 1; y++) {
var worldX = playerGridX - self.viewRange + x;
var worldY = playerGridY - self.viewRange + y;
var cell = self.gridCells[x][y];
var screenX = (x - self.viewRange) * self.gridCellSize;
var screenY = (y - self.viewRange) * self.gridCellSize;
cell.x = screenX;
cell.y = screenY;
// Check if position is within world bounds
if (worldX >= 0 && worldX < worldGrid.width && worldY >= 0 && worldY < worldGrid.height) {
cell.visible = true;
// Check if this position matches the door location
var doorGridX = Math.floor(door.x / worldGrid.cellSize);
var doorGridY = Math.floor(door.y / worldGrid.cellSize);
if (worldX === doorGridX && worldY === doorGridY) {
// Door position
cell.tint = 0xFFFFFF; // White for door
cell.alpha = 1.0;
} else if (worldGrid.walls[worldX][worldY]) {
// Wall
cell.tint = 0x46f7a3; // Green for walls
cell.alpha = 1.0;
} else {
// Floor
cell.tint = 0x74391f; // Brown for floors
cell.alpha = 0.6;
}
} else {
// Outside world bounds
cell.visible = false;
}
}
}
};
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 alternative routes between rooms for navigation variety
self.createAlternativeRoutes(rooms, offsetX, offsetY);
// 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 with varied connection patterns
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,
angle: Math.atan2(dy, dx) // Add angle for directional variety
});
}
}
// Sort by distance
roomDistances.sort(function (a, b) {
return a.distance - b.distance;
});
// Create varied connection patterns based on room position and existing connections
var connectionPattern = Math.floor(Math.random() * 4);
var connectionsToMake = 0;
var connectionIndices = [];
if (connectionPattern === 0) {
// Star pattern - connect to nearest + 2 distant rooms for hub effect
connectionsToMake = Math.min(3, roomDistances.length);
connectionIndices = [0]; // Always connect to nearest
if (roomDistances.length > 2) {
connectionIndices.push(Math.floor(roomDistances.length * 0.6)); // Mid-distance
connectionIndices.push(roomDistances.length - 1); // Farthest
}
} else if (connectionPattern === 1) {
// Linear chain pattern - connect to 1-2 rooms in sequence
connectionsToMake = Math.min(2, roomDistances.length);
for (var i = 0; i < connectionsToMake; i++) {
connectionIndices.push(i);
}
} else if (connectionPattern === 2) {
// Angular spread pattern - connect to rooms in different directions
connectionsToMake = Math.min(4, roomDistances.length);
// Select rooms with diverse angles
var usedAngles = [];
for (var i = 0; i < roomDistances.length && connectionIndices.length < connectionsToMake; i++) {
var roomAngle = roomDistances[i].angle;
var validAngle = true;
// Check if this angle is sufficiently different from used angles
for (var j = 0; j < usedAngles.length; j++) {
var angleDiff = Math.abs(roomAngle - usedAngles[j]);
if (angleDiff < Math.PI / 3) {
// Less than 60 degrees apart
validAngle = false;
break;
}
}
if (validAngle) {
connectionIndices.push(i);
usedAngles.push(roomAngle);
}
}
} else {
// Random scattered pattern - connect to random selection
connectionsToMake = Math.min(2 + Math.floor(Math.random() * 3), roomDistances.length);
var availableIndices = [];
for (var i = 0; i < roomDistances.length; i++) {
availableIndices.push(i);
}
// Shuffle and pick random connections
for (var i = availableIndices.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = availableIndices[i];
availableIndices[i] = availableIndices[j];
availableIndices[j] = temp;
}
for (var i = 0; i < connectionsToMake; i++) {
connectionIndices.push(availableIndices[i]);
}
}
// Create connections with varied corridor types
for (var i = 0; i < connectionIndices.length; i++) {
var targetRoom = roomDistances[connectionIndices[i]].room;
var corridorType = Math.floor(Math.random() * 3);
if (corridorType === 0) {
// Straight corridor
self.createCorridor(newRoom.centerX, newRoom.centerY, targetRoom.centerX, targetRoom.centerY);
} else if (corridorType === 1) {
// Curved corridor
self.createCurvedCorridor(newRoom.centerX, newRoom.centerY, targetRoom.centerX, targetRoom.centerY);
} else {
// Multi-segment corridor with intermediate waypoint
var midX = Math.floor((newRoom.centerX + targetRoom.centerX) / 2) + Math.floor(Math.random() * 6) - 3;
var midY = Math.floor((newRoom.centerY + targetRoom.centerY) / 2) + Math.floor(Math.random() * 6) - 3;
self.createCorridor(newRoom.centerX, newRoom.centerY, midX, midY);
self.createCorridor(midX, midY, targetRoom.centerX, targetRoom.centerY);
}
}
// Always ensure at least one connection exists
if (connectionIndices.length === 0 && roomDistances.length > 0) {
self.createCorridor(newRoom.centerX, newRoom.centerY, roomDistances[0].room.centerX, roomDistances[0].room.centerY);
}
// Add bonus cross-connections between existing rooms (15% chance)
if (Math.random() < 0.15 && existingRooms.length >= 3) {
var room1Index = Math.floor(Math.random() * existingRooms.length);
var room2Index = Math.floor(Math.random() * existingRooms.length);
if (room1Index !== room2Index && existingRooms[room1Index] !== newRoom && existingRooms[room2Index] !== newRoom) {
self.createCorridor(existingRooms[room1Index].centerX, existingRooms[room1Index].centerY, existingRooms[room2Index].centerX, existingRooms[room2Index].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 alternative routes between existing rooms for better navigation variety
self.createAlternativeRoutes = function (rooms, offsetX, offsetY) {
if (rooms.length < 3) return; // Need at least 3 rooms for alternative routes
// For each pair of rooms, occasionally create alternative connection paths
for (var i = 0; i < rooms.length - 1; i++) {
for (var j = i + 1; j < rooms.length; j++) {
var room1 = rooms[i];
var room2 = rooms[j];
var dx = Math.abs(room1.centerX - room2.centerX);
var dy = Math.abs(room1.centerY - room2.centerY);
var distance = Math.sqrt(dx * dx + dy * dy);
// Only create alternative routes for rooms that are moderately close (not too near, not too far)
if (distance > 3 && distance < 8 && Math.random() < 0.25) {
// Create alternative route with different path
var routeType = Math.floor(Math.random() * 3);
if (routeType === 0) {
// Indirect L-shaped route through intermediate point
var intermediateX = room1.centerX + Math.floor(Math.random() * 4) - 2;
var intermediateY = room2.centerY + Math.floor(Math.random() * 4) - 2;
self.createCorridor(room1.centerX, room1.centerY, intermediateX, intermediateY);
self.createCorridor(intermediateX, intermediateY, room2.centerX, room2.centerY);
} else if (routeType === 1) {
// Curved bypass route
var bypassX = Math.floor((room1.centerX + room2.centerX) / 2) + Math.floor(Math.random() * 8) - 4;
var bypassY = Math.floor((room1.centerY + room2.centerY) / 2) + Math.floor(Math.random() * 8) - 4;
self.createSmoothPath(room1.centerX, room1.centerY, bypassX, bypassY);
self.createSmoothPath(bypassX, bypassY, room2.centerX, room2.centerY);
} else {
// Multi-segment zigzag route
var segments = 2 + Math.floor(Math.random() * 2); // 2-3 segments
var currentX = room1.centerX;
var currentY = room1.centerY;
var stepX = (room2.centerX - room1.centerX) / segments;
var stepY = (room2.centerY - room1.centerY) / segments;
for (var s = 1; s <= segments; s++) {
var nextX = Math.floor(room1.centerX + stepX * s + Math.random() * 4 - 2);
var nextY = Math.floor(room1.centerY + stepY * s + Math.random() * 4 - 2);
self.createCorridor(currentX, currentY, nextX, nextY);
currentX = nextX;
currentY = nextY;
}
}
}
}
}
};
// 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();
// Pre-load entire map by generating all possible chunks
procGen.generateEntireMap = function () {
var chunkSize = this.chunkSize;
var maxChunksX = Math.ceil(worldGrid.width / chunkSize);
var maxChunksY = Math.ceil(worldGrid.height / chunkSize);
console.log('Pre-loading entire map: ' + maxChunksX + 'x' + maxChunksY + ' chunks');
// Generate all chunks across the entire world grid
for (var chunkX = 0; chunkX < maxChunksX; chunkX++) {
for (var chunkY = 0; chunkY < maxChunksY; chunkY++) {
this.generateChunk(chunkX, chunkY);
}
}
console.log('Map pre-loading complete. Generated ' + Object.keys(this.generatedChunks).length + ' chunks');
};
// Pre-load the entire map at initialization
procGen.generateEntireMap();
// 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 error checker instance
var errorChecker = new ErrorChecker();
game.addChild(errorChecker);
// Create error status display
var errorStatusText = new Text2('System OK', {
size: 50,
fill: 0x00FF00
});
errorStatusText.anchor.set(0, 0);
errorStatusText.x = 120; // Avoid top-left 100x100 area
errorStatusText.y = 520;
LK.gui.addChild(errorStatusText);
// Create detailed error display (hidden by default)
var errorDetailsText = new Text2('', {
size: 40,
fill: 0xFFFFFF
});
errorDetailsText.anchor.set(0, 0);
errorDetailsText.x = 120;
errorDetailsText.y = 600;
errorDetailsText.visible = false;
LK.gui.addChild(errorDetailsText);
// 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);
// Create minimap and position it more to the left
var minimap = new Minimap();
minimap.x = 800; // Move to left side (was 1366 center)
minimap.y = 1024; // Center vertically (2048/2)
LK.gui.addChild(minimap);
// Create door instance and position it in world
var door = new Door();
// Find a good position for the door - place it in an open area near spawn
var doorX = (worldGrid.width / 2 + 3) * worldGrid.cellSize;
var doorY = (worldGrid.height / 2 + 2) * worldGrid.cellSize;
door.x = doorX;
door.y = doorY;
// Keep door at exact world level for proper collision and rendering
game.addChild(door);
// Ensure a room is always generated around the door position
var doorGridX = Math.floor(doorX / worldGrid.cellSize);
var doorGridY = Math.floor(doorY / worldGrid.cellSize);
// 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
};
// Create a guaranteed 5x5 room around the door for better connectivity
for (var dx = -2; dx <= 2; dx++) {
for (var dy = -2; dy <= 2; dy++) {
var roomX = doorGridX + dx;
var roomY = doorGridY + dy;
if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) {
worldGrid.walls[roomX][roomY] = false;
}
}
}
// Create an adjacent connected room to the door room
var connectedRoomX = doorGridX + 4; // Place connected room 4 cells to the right
var connectedRoomY = doorGridY;
// Create a guaranteed 4x4 connected room
for (var dx = 0; dx < 4; dx++) {
for (var dy = -1; dy <= 2; dy++) {
var roomX = connectedRoomX + dx;
var roomY = connectedRoomY + dy;
if (roomX >= 0 && roomX < worldGrid.width && roomY >= 0 && roomY < worldGrid.height) {
worldGrid.walls[roomX][roomY] = false;
}
}
}
// Create a wide corridor connecting the door room to the adjacent room
for (var x = doorGridX + 2; x < connectedRoomX; x++) {
for (var y = doorGridY - 1; y <= doorGridY + 1; y++) {
if (x >= 0 && x < worldGrid.width && y >= 0 && y < worldGrid.height) {
worldGrid.walls[x][y] = false;
}
}
}
// Create corridors leading to the door room from spawn area to ensure connectivity
var spawnGridX = Math.floor(worldGrid.width / 2);
var spawnGridY = Math.floor(worldGrid.height / 2);
// Create horizontal corridor
var minX = Math.min(spawnGridX, doorGridX);
var maxX = Math.max(spawnGridX, doorGridX);
for (var x = minX; x <= maxX; x++) {
if (x >= 0 && x < worldGrid.width && spawnGridY >= 0 && spawnGridY < worldGrid.height) {
worldGrid.walls[x][spawnGridY] = false;
}
}
// Create vertical corridor
var minY = Math.min(spawnGridY, doorGridY);
var maxY = Math.max(spawnGridY, doorGridY);
for (var y = minY; y <= maxY; y++) {
if (doorGridX >= 0 && doorGridX < worldGrid.width && y >= 0 && y < worldGrid.height) {
worldGrid.walls[doorGridX][y] = false;
}
}
// Also connect the adjacent room to the main corridor network
// Create additional corridor from connected room toward spawn area
for (var x = connectedRoomX; x < Math.min(connectedRoomX + 6, worldGrid.width - 1); x++) {
if (x >= 0 && x < worldGrid.width && spawnGridY >= 0 && spawnGridY < worldGrid.height) {
worldGrid.walls[x][spawnGridY] = false;
}
}
// 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();
};
// Error status click handler for detailed view
errorStatusText.down = function (x, y, obj) {
if (errorChecker.errorLog.length > 0) {
// Toggle error details visibility
errorDetailsText.visible = !errorDetailsText.visible;
// Show last few errors
var lastErrors = [];
for (var i = Math.max(0, errorChecker.errorLog.length - 5); i < errorChecker.errorLog.length; i++) {
var error = errorChecker.errorLog[i];
var timeAgo = Math.floor((Date.now() - error.timestamp) / 1000);
lastErrors.push('[' + timeAgo + 's] ' + error.type + ': ' + error.message.substring(0, 50));
}
errorDetailsText.setText(lastErrors.join('\n'));
} else {
// Run immediate inspection when no errors showing
var inspectionSummary = errorChecker.getInspectionSummary();
if (inspectionSummary.totalIssues > 0) {
errorDetailsText.visible = true;
var summaryText = 'INSPECTION RESULTS:\n';
summaryText += 'Critical: ' + inspectionSummary.criticalIssues + '\n';
summaryText += 'Warnings: ' + inspectionSummary.warningIssues + '\n';
summaryText += 'Info: ' + inspectionSummary.infoIssues + '\n\n';
summaryText += 'TOP ISSUES:\n';
for (var i = 0; i < inspectionSummary.topIssues.length; i++) {
summaryText += '• ' + inspectionSummary.topIssues[i].substring(0, 40) + '\n';
}
errorDetailsText.setText(summaryText);
} else {
// Clear log if no errors
errorChecker.clearLog();
errorDetailsText.visible = false;
}
}
};
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;
// Check for isolated areas near player and create passages proactively (map is pre-loaded)
if (LK.ticks % 60 === 0) {
// Check every 60 frames for performance since map is pre-generated
worldGrid.checkPlayerProximityForPassages(player.x, player.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;
}
// Update minimap
minimap.update(player);
// Update error checker status
var errorSummary = errorChecker.getErrorSummary();
var statusText = 'System OK';
var statusColor = 0x00FF00;
if (errorSummary.errors > 0) {
statusText = 'ERRORS: ' + errorSummary.errors;
statusColor = 0xFF0000;
} else if (errorSummary.warnings > 0) {
statusText = 'WARNINGS: ' + errorSummary.warnings;
statusColor = 0xFFFF00;
} else if (errorSummary.recent > 0) {
statusText = 'RECENT: ' + errorSummary.recent;
statusColor = 0x00FFFF;
} else {
// Show inspection status when no errors
if (LK.ticks % 300 === 0) {
// Every 5 seconds
var inspectionSummary = errorChecker.getInspectionSummary();
if (inspectionSummary.totalIssues > 0) {
statusText = 'INSPECT: ' + inspectionSummary.totalIssues;
statusColor = inspectionSummary.criticalIssues > 0 ? 0xFF6600 : 0x6699FF;
}
}
}
errorStatusText.setText(statusText);
errorStatusText.fill = statusColor;
// Show error details on touch (every 120 frames to avoid spam)
if (LK.ticks % 120 === 0 && errorSummary.total > 0) {
var recentErrors = [];
var currentTime = Date.now();
for (var i = errorChecker.errorLog.length - 1; i >= 0 && recentErrors.length < 3; i--) {
var error = errorChecker.errorLog[i];
if (currentTime - error.timestamp < 10000) {
// Last 10 seconds
recentErrors.push(error.type + ': ' + error.message.substring(0, 40));
}
}
if (recentErrors.length > 0) {
errorDetailsText.setText(recentErrors.join('\n'));
errorDetailsText.visible = true;
} else {
errorDetailsText.visible = false;
}
} else if (errorSummary.total === 0) {
errorDetailsText.visible = false;
}
};
// 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
};
};