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